Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
283 changes: 204 additions & 79 deletions src/BloomBrowserUI/bookEdit/pageThumbnailList/pageThumbnailList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,27 @@

import $ from "jquery";
import { css } from "@emotion/react";
import { Menu } from "@mui/material";

import * as React from "react";
import { useState, useEffect, useMemo } from "react";
import { useState, useEffect } from "react";
import * as ReactDOM from "react-dom";
import theOneLocalizationManager from "../../lib/localizationManager/localizationManager";

import * as toastr from "toastr";
import "errorHandler";
import WebSocketManager from "../../utils/WebSocketManager";
import { Responsive } from "react-grid-layout";
import { get, postJson, postString, useApiData } from "../../utils/bloomApi";
import {
get,
getAsync,
postJson,
postString,
useApiData,
} from "../../utils/bloomApi";
import { PageThumbnail } from "./PageThumbnail";
import LazyLoad, { forceCheck } from "react-lazyload";
import { LocalizableMenuItem } from "../../react_components/localizableMenuItem";

// We're using the Responsive version of react-grid-layout because
// (1) the previous version of the page thumbnails, which this replaces,
Expand Down Expand Up @@ -57,6 +65,19 @@ export interface IPage {
content: string;
}

interface IPageMenuItem {
id: string;
label: string;
l10nId: string;
enabled?: boolean;
}

interface IContextMenuPoint {
mouseX: number;
mouseY: number;
pageId: string;
}

// This map goes from page ID to a callback that we get from the page thumbnail
// which should be called when the main Bloom program informs us that
// the thumbnail needs to be updated.
Expand All @@ -75,13 +96,43 @@ const PageList: React.FunctionComponent<{ pageLayout: string }> = (props) => {
// when the websocket detects a request for this.
const [resetValue, setResetValue] = useState(1);
const [twoColumns, setTwoColumns] = useState(true);
const [contextMenuPoint, setContextMenuPoint] =
useState<IContextMenuPoint>();
const [contextMenuItems, setContextMenuItems] = useState<IPageMenuItem[]>(
[],
);

const [selectedPageId, setSelectedPageId] = useState("");
const bookAttributesThatMayAffectDisplay = useApiData<any>(
"pageList/bookAttributesThatMayAffectDisplay",
{},
);

const pageMenuDefinition: IPageMenuItem[] = [
{
id: "duplicatePage",
label: "Duplicate Page",
l10nId: "EditTab.DuplicatePageButton",
},
{
id: "duplicatePageManyTimes",
label: "Duplicate Page Many Times...",
l10nId: "EditTab.DuplicatePageMultiple",
},
{ id: "copyPage", label: "Copy Page", l10nId: "EditTab.CopyPage" },
{ id: "pastePage", label: "Paste Page", l10nId: "EditTab.PastePage" },
{
id: "removePage",
label: "Remove Page",
l10nId: "EditTab.DeletePageButton",
},
{
id: "chooseDifferentLayout",
label: "Choose Different Layout",
l10nId: "EditTab.ChooseLayoutButton",
},
];

// All the code in this useEffect is one-time initialization.
useEffect(() => {
let localizedNotification = "";
Expand Down Expand Up @@ -198,13 +249,13 @@ const PageList: React.FunctionComponent<{ pageLayout: string }> = (props) => {
// https://stackoverflow.com/questions/61191496/why-is-my-react-lazyload-component-not-working)
// This actually runs each time a page is deleted.
forceCheck();
}, [realPageList]);
}, [realPageList, selectedPageId]);

// this is embedded so that we have access to realPageList
const handleGridItemClick = (e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation();
e.preventDefault();
NotifyCSharpOfClick();
closeContextMenu();

// for manual testing
if (e.getModifierState("Control") && e.getModifierState("Alt")) {
Expand All @@ -230,17 +281,70 @@ const PageList: React.FunctionComponent<{ pageLayout: string }> = (props) => {
}
};

const openContextMenuCount = React.useRef(0);

const closeContextMenu = () => {
openContextMenuCount.current = 0;
setContextMenuPoint(undefined);
setContextMenuItems([]);
};

const openContextMenu = (pageId: string, x: number, y: number) => {
openContextMenuCount.current++;
const currentCount = openContextMenuCount.current;
// If the user right-clicks on a page that is not selected, ignore the click and
// close any existing context menu.
if (pageId !== selectedPageId) {
closeContextMenu();
return;
}

Promise.all(
pageMenuDefinition.map(async (item) => {
const response = await getAsync(
`pageList/contextMenuItemEnabled?commandId=${encodeURIComponent(item.id)}&pageId=${encodeURIComponent(pageId)}`,
);
return {
id: item.id,
label: item.label,
l10nId: item.l10nId,
enabled: !!response.data,
};
}),
).then((menuItems) => {
if (currentCount !== openContextMenuCount.current) {
// Either the menu was closed or a newer context menu has been opened since this async
// call was made, so ignore the results.
return;
}
setContextMenuItems(menuItems);
setContextMenuPoint({
mouseX: x - 2,
mouseY: y - 4,
pageId,
});
});
};

const onContextMenuItemClick = (commandId: string) => {
if (!contextMenuPoint) return;

postJson("pageList/contextMenuItemClicked", {
pageId: contextMenuPoint.pageId,
commandId,
});
closeContextMenu();
};

const handleContextMenu = (e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation();
e.preventDefault();
NotifyCSharpOfClick();
if (e.currentTarget) {
const pageElt = e.currentTarget.closest("[id]")!;
const pageId = pageElt.getAttribute("id");
if (pageId === selectedPageId)
postJson("pageList/menuClicked", {
pageId,
});
if (pageId) {
openContextMenu(pageId, e.clientX, e.clientY);
}
}
};

Expand All @@ -255,66 +359,63 @@ const PageList: React.FunctionComponent<{ pageLayout: string }> = (props) => {
},
...realPageList,
];
const pages = useMemo(() => {
const pages1 = pageList.map((pageContent, index) => {
return (
<div
key={pageContent.key} // for efficient react manipulation of list
id={pageContent.key} // used by C# code to identify page
data-caption={pageContent.caption}
className={
"gridItem " +
(pageContent.key === "placeholder"
? " placeholder"
: "") +
(selectedPageId === pageContent.key
? " gridSelected"
: "")
const pages = pageList.map((pageContent, index) => {
return (
<div
key={pageContent.key} // for efficient react manipulation of list
id={pageContent.key} // used by C# code to identify page
data-caption={pageContent.caption}
className={
"gridItem " +
(pageContent.key === "placeholder" ? " placeholder" : "") +
(selectedPageId === pageContent.key ? " gridSelected" : "")
}
css={css`
.lazyload-wrapper {
height: 100%;
}
css={css`
.lazyload-wrapper {
height: 100%;
}
`}
`}
>
<LazyLoad
height={rowHeight}
scrollContainer="#pageGridWrapper"
resize={true} // expand lazy elements as needed when container resizes
>
<LazyLoad
height={rowHeight}
scrollContainer="#pageGridWrapper"
resize={true} // expand lazy elements as needed when container resizes
>
<PageThumbnail
page={pageContent}
left={!(index % 2)}
pageLayout={props.pageLayout}
configureReloadCallback={(id, callback) =>
pageIdToRefreshMap.set(id, callback)
}
onClick={handleGridItemClick}
onContextMenu={handleContextMenu}
/>
{selectedPageId === pageContent.key && (
<div id="menuIconHolder" className="menuHolder">
<svg
xmlns="http://www.w3.org/2000/svg"
width="18"
height="18"
viewBox="0 0 18 18"
onClick={() => {
postJson("pageList/menuClicked", {
pageId: pageContent.key,
});
}}
>
<path d="M5 8l4 4 4-4z" fill="white" />
</svg>
</div>
)}
</LazyLoad>
</div>
);
});
return pages1;
}, [pageList]);
<PageThumbnail
page={pageContent}
left={!(index % 2)}
pageLayout={props.pageLayout}
configureReloadCallback={(id, callback) =>
pageIdToRefreshMap.set(id, callback)
}
onClick={handleGridItemClick}
onContextMenu={handleContextMenu}
/>
{selectedPageId === pageContent.key && (
<div id="menuIconHolder" className="menuHolder">
<svg
xmlns="http://www.w3.org/2000/svg"
width="18"
height="18"
viewBox="0 0 18 18"
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
openContextMenu(
pageContent.key,
e.clientX,
e.clientY,
);
}}
>
<path d="M5 8l4 4 4-4z" fill="white" />
</svg>
</div>
)}
</LazyLoad>
</div>
);
});

// Set up some objects and functions we need as params for our main element.
// Some of them come in sets "lg" and "sm". Currently the "lg" (two-column)
Expand Down Expand Up @@ -391,6 +492,42 @@ const PageList: React.FunctionComponent<{ pageLayout: string }> = (props) => {
>
{pages}
</Responsive>
{contextMenuPoint && (
<Menu
keepMounted={true}
open={!!contextMenuPoint}
onClose={closeContextMenu}
anchorReference="anchorPosition"
anchorPosition={{
top: contextMenuPoint.mouseY,
left: contextMenuPoint.mouseX,
}}
disableAutoFocusItem={true} // don't automatically focus the first item
>
{contextMenuItems.map((item) => (
<LocalizableMenuItem
key={item.id}
english={item.label}
l10nId={item.l10nId}
disabled={!item.enabled}
onClick={() => onContextMenuItemClick(item.id)}
dontGiveAffordanceForCheckbox={true}
css={css`
display: block !important;
min-height: 24px !important;
padding: 3px 8px !important;
`}
labelCss={css`
font-size: 10pt !important;
white-space: normal !important;
line-height: normal !important;
`}
>
{item.label}
</LocalizableMenuItem>
))}
</Menu>
)}
</div>
);
};
Expand All @@ -402,20 +539,8 @@ $(window).ready(() => {
<PageList pageLayout={pageLayout} />,
document.getElementById("pageGridWrapper"),
);

// If the user clicks outside of the context menu, we want to close it.
// Since it is currently a winforms menu, we do that by sending a message
// back to c#-land.
// We can remove this in 5.6, or whenever we replace the winforms context menu with a rect menu.
$(window).click(() => {
NotifyCSharpOfClick();
});
});

function NotifyCSharpOfClick() {
(window as any).chrome?.webview?.postMessage("browser-clicked");
}

// Function invoked when dragging a page ends. Note that it is often
// called when all the user intended was to click the page, presumably
// because some tiny movement was made while the mouse is down.
Expand Down
Loading