tldraw
Version:
A tiny little drawing editor.
462 lines (461 loc) • 20.7 kB
JavaScript
;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var DefaultPageMenu_exports = {};
__export(DefaultPageMenu_exports, {
DefaultPageMenu: () => DefaultPageMenu
});
module.exports = __toCommonJS(DefaultPageMenu_exports);
var import_jsx_runtime = require("react/jsx-runtime");
var import_editor = require("@tldraw/editor");
var import_react = require("react");
var import_constants = require("../../constants");
var import_breakpoints = require("../../context/breakpoints");
var import_events = require("../../context/events");
var import_useMenuIsOpen = require("../../hooks/useMenuIsOpen");
var import_useReadonly = require("../../hooks/useReadonly");
var import_useTranslation = require("../../hooks/useTranslation/useTranslation");
var import_TldrawUiButton = require("../primitives/Button/TldrawUiButton");
var import_TldrawUiButtonCheck = require("../primitives/Button/TldrawUiButtonCheck");
var import_TldrawUiButtonIcon = require("../primitives/Button/TldrawUiButtonIcon");
var import_TldrawUiButtonLabel = require("../primitives/Button/TldrawUiButtonLabel");
var import_TldrawUiPopover = require("../primitives/TldrawUiPopover");
var import_layout = require("../primitives/layout");
var import_PageItemInput = require("./PageItemInput");
var import_PageItemSubmenu = require("./PageItemSubmenu");
var import_edit_pages_shared = require("./edit-pages-shared");
const DefaultPageMenu = (0, import_react.memo)(function DefaultPageMenu2() {
const editor = (0, import_editor.useEditor)();
const trackEvent = (0, import_events.useUiEvents)();
const msg = (0, import_useTranslation.useTranslation)();
const breakpoint = (0, import_breakpoints.useBreakpoint)();
const handleOpenChange = (0, import_react.useCallback)(() => setIsEditing(false), []);
const [isOpen, onOpenChange] = (0, import_useMenuIsOpen.useMenuIsOpen)("page-menu", handleOpenChange);
const ITEM_HEIGHT = 36;
const rSortableContainer = (0, import_react.useRef)(null);
const pages = (0, import_editor.useValue)("pages", () => editor.getPages(), [editor]);
const currentPage = (0, import_editor.useValue)("currentPage", () => editor.getCurrentPage(), [editor]);
const currentPageId = (0, import_editor.useValue)("currentPageId", () => editor.getCurrentPageId(), [editor]);
const isReadonlyMode = (0, import_useReadonly.useReadonly)();
const maxPageCountReached = (0, import_editor.useValue)(
"maxPageCountReached",
() => editor.getPages().length >= editor.options.maxPages,
[editor]
);
const isCoarsePointer = (0, import_editor.useValue)(
"isCoarsePointer",
() => editor.getInstanceState().isCoarsePointer,
[editor]
);
const [isEditing, setIsEditing] = (0, import_react.useState)(false);
(0, import_react.useEffect)(
function closePageMenuOnEnterPressAfterPressingEnterToConfirmRename() {
function handleKeyDown2() {
if (isEditing) return;
if (document.activeElement === document.body) {
editor.menus.clearOpenMenus();
}
}
document.addEventListener("keydown", handleKeyDown2, { passive: true });
return () => {
document.removeEventListener("keydown", handleKeyDown2);
};
},
[editor, isEditing]
);
const toggleEditing = (0, import_react.useCallback)(() => {
if (isReadonlyMode) return;
setIsEditing((s) => !s);
}, [isReadonlyMode]);
const rMutables = (0, import_react.useRef)({
isPointing: false,
status: "idle",
pointing: null,
startY: 0,
startIndex: 0,
dragIndex: 0
});
const [sortablePositionItems, setSortablePositionItems] = (0, import_react.useState)(
Object.fromEntries(
pages.map((page, i) => [page.id, { y: i * ITEM_HEIGHT, offsetY: 0, isSelected: false }])
)
);
(0, import_react.useLayoutEffect)(() => {
setSortablePositionItems(
Object.fromEntries(
pages.map((page, i) => [page.id, { y: i * ITEM_HEIGHT, offsetY: 0, isSelected: false }])
)
);
}, [ITEM_HEIGHT, pages]);
(0, import_react.useEffect)(() => {
if (!isOpen) return;
editor.timers.requestAnimationFrame(() => {
const elm = document.querySelector(`[data-pageid="${currentPageId}"]`);
if (elm) {
elm.querySelector("button")?.focus();
const container = rSortableContainer.current;
if (!container) return;
const elmTopPosition = elm.offsetTop;
const containerScrollTopPosition = container.scrollTop;
if (elmTopPosition < containerScrollTopPosition) {
container.scrollTo({ top: elmTopPosition });
}
const elmBottomPosition = elmTopPosition + ITEM_HEIGHT;
const containerScrollBottomPosition = container.scrollTop + container.offsetHeight;
if (elmBottomPosition > containerScrollBottomPosition) {
container.scrollTo({ top: elmBottomPosition - container.offsetHeight });
}
}
});
}, [ITEM_HEIGHT, currentPageId, isOpen, editor]);
const handlePointerDown = (0, import_react.useCallback)(
(e) => {
const { clientY, currentTarget } = e;
const {
dataset: { id, index }
} = currentTarget;
if (!id || !index) return;
const mut = rMutables.current;
(0, import_editor.setPointerCapture)(e.currentTarget, e);
mut.status = "pointing";
mut.pointing = { id, index: +index };
const current = sortablePositionItems[id];
const dragY = current.y;
mut.startY = clientY;
mut.startIndex = Math.max(0, Math.min(Math.round(dragY / ITEM_HEIGHT), pages.length - 1));
},
[ITEM_HEIGHT, pages.length, sortablePositionItems]
);
const handlePointerMove = (0, import_react.useCallback)(
(e) => {
const mut = rMutables.current;
if (mut.status === "pointing") {
const { clientY } = e;
const offset = clientY - mut.startY;
if (Math.abs(offset) > 5) {
mut.status = "dragging";
}
}
if (mut.status === "dragging") {
const { clientY } = e;
const offsetY = clientY - mut.startY;
const current = sortablePositionItems[mut.pointing.id];
const { startIndex, pointing } = mut;
const dragY = current.y + offsetY;
const dragIndex = Math.max(0, Math.min(Math.round(dragY / ITEM_HEIGHT), pages.length - 1));
const next = { ...sortablePositionItems };
next[pointing.id] = {
y: current.y,
offsetY,
isSelected: true
};
if (dragIndex !== mut.dragIndex) {
mut.dragIndex = dragIndex;
for (let i = 0; i < pages.length; i++) {
const item = pages[i];
if (item.id === mut.pointing.id) {
continue;
}
let { y } = next[item.id];
if (dragIndex === startIndex) {
y = i * ITEM_HEIGHT;
} else if (dragIndex < startIndex) {
if (dragIndex <= i && i < startIndex) {
y = (i + 1) * ITEM_HEIGHT;
} else {
y = i * ITEM_HEIGHT;
}
} else if (dragIndex > startIndex) {
if (dragIndex >= i && i > startIndex) {
y = (i - 1) * ITEM_HEIGHT;
} else {
y = i * ITEM_HEIGHT;
}
}
if (y !== next[item.id].y) {
next[item.id] = { y, offsetY: 0, isSelected: true };
}
}
}
setSortablePositionItems(next);
}
},
[ITEM_HEIGHT, pages, sortablePositionItems]
);
const handlePointerUp = (0, import_react.useCallback)(
(e) => {
const mut = rMutables.current;
if (mut.status === "dragging") {
const { id, index } = mut.pointing;
(0, import_edit_pages_shared.onMovePage)(editor, id, index, mut.dragIndex, trackEvent);
}
(0, import_editor.releasePointerCapture)(e.currentTarget, e);
mut.status = "idle";
},
[editor, trackEvent]
);
const handleKeyDown = (0, import_react.useCallback)(
(e) => {
const mut = rMutables.current;
if (e.key === "Escape") {
if (mut.status === "dragging") {
setSortablePositionItems(
Object.fromEntries(
pages.map((page, i) => [
page.id,
{ y: i * ITEM_HEIGHT, offsetY: 0, isSelected: false }
])
)
);
}
mut.status = "idle";
}
},
[ITEM_HEIGHT, pages]
);
const handleCreatePageClick = (0, import_react.useCallback)(() => {
if (isReadonlyMode) return;
editor.run(() => {
editor.markHistoryStoppingPoint("creating page");
const newPageId = import_editor.PageRecordType.createId();
editor.createPage({ name: msg("page-menu.new-page-initial-name"), id: newPageId });
editor.setCurrentPage(newPageId);
setIsEditing(true);
editor.timers.requestAnimationFrame(() => {
const elm = document.querySelector(`[data-pageid="${newPageId}"]`);
if (elm) {
elm.querySelector("button")?.focus();
}
});
});
trackEvent("new-page", { source: "page-menu" });
}, [editor, msg, isReadonlyMode, trackEvent]);
const changePage = (0, import_react.useCallback)(
(id) => {
editor.setCurrentPage(id);
trackEvent("change-page", { source: "page-menu" });
},
[editor, trackEvent]
);
const renamePage = (0, import_react.useCallback)(
(id, name) => {
editor.renamePage(id, name);
trackEvent("rename-page", { source: "page-menu" });
},
[editor, trackEvent]
);
const shouldUseWindowPrompt = breakpoint < import_constants.PORTRAIT_BREAKPOINT.TABLET_SM && isCoarsePointer;
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_TldrawUiPopover.TldrawUiPopover, { id: "pages", onOpenChange, open: isOpen, children: [
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_TldrawUiPopover.TldrawUiPopoverTrigger, { "data-testid": "main.page-menu", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
import_TldrawUiButton.TldrawUiButton,
{
type: "menu",
title: currentPage.name,
"data-testid": "page-menu.button",
className: "tlui-page-menu__trigger",
children: [
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "tlui-page-menu__name", children: currentPage.name }),
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_TldrawUiButtonIcon.TldrawUiButtonIcon, { icon: "chevron-down", small: true })
]
}
) }),
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
import_TldrawUiPopover.TldrawUiPopoverContent,
{
side: "bottom",
align: "start",
sideOffset: 0,
disableEscapeKeyDown: isEditing,
children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "tlui-page-menu__wrapper", children: [
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "tlui-page-menu__header", children: [
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "tlui-page-menu__header__title", children: msg("page-menu.title") }),
!isReadonlyMode && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_layout.TldrawUiRow, { children: [
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
import_TldrawUiButton.TldrawUiButton,
{
type: "icon",
"data-testid": "page-menu.edit",
title: msg(isEditing ? "page-menu.edit-done" : "page-menu.edit-start"),
onClick: toggleEditing,
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_TldrawUiButtonIcon.TldrawUiButtonIcon, { icon: isEditing ? "check" : "edit" })
}
),
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
import_TldrawUiButton.TldrawUiButton,
{
type: "icon",
"data-testid": "page-menu.create",
title: msg(
maxPageCountReached ? "page-menu.max-page-count-reached" : "page-menu.create-new-page"
),
disabled: maxPageCountReached,
onClick: handleCreatePageClick,
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_TldrawUiButtonIcon.TldrawUiButtonIcon, { icon: "plus" })
}
)
] })
] }),
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
"div",
{
"data-testid": "page-menu.list",
className: "tlui-page-menu__list tlui-menu__group",
style: { height: ITEM_HEIGHT * pages.length + 4 },
ref: rSortableContainer,
children: pages.map((page, index) => {
const position = sortablePositionItems[page.id] ?? {
position: index * 40,
offsetY: 0
};
return isEditing ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
"div",
{
"data-testid": "page-menu.item",
"data-pageid": page.id,
className: "tlui-page_menu__item__sortable",
style: {
zIndex: page.id === currentPage.id ? 888 : index,
transform: `translate(0px, ${position.y + position.offsetY}px)`
},
children: [
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
import_TldrawUiButton.TldrawUiButton,
{
type: "icon",
tabIndex: -1,
className: "tlui-page_menu__item__sortable__handle",
onPointerDown: handlePointerDown,
onPointerUp: handlePointerUp,
onPointerMove: handlePointerMove,
onKeyDown: handleKeyDown,
"data-id": page.id,
"data-index": index,
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_TldrawUiButtonIcon.TldrawUiButtonIcon, { icon: "drag-handle-dots" })
}
),
shouldUseWindowPrompt ? (
// sigh, this is a workaround for iOS Safari
// because the device and the radix popover seem
// to be fighting over scroll position. Nothing
// else seems to work!
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
import_TldrawUiButton.TldrawUiButton,
{
type: "normal",
className: "tlui-page-menu__item__button",
onClick: () => {
const name = window.prompt(msg("action.rename"), page.name);
if (name && name !== page.name) {
renamePage(page.id, name);
}
},
onDoubleClick: toggleEditing,
children: [
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_TldrawUiButtonCheck.TldrawUiButtonCheck, { checked: page.id === currentPage.id }),
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_TldrawUiButtonLabel.TldrawUiButtonLabel, { children: page.name })
]
}
)
) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
"div",
{
className: "tlui-page_menu__item__sortable__title",
style: { height: ITEM_HEIGHT },
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
import_PageItemInput.PageItemInput,
{
id: page.id,
name: page.name,
isCurrentPage: page.id === currentPage.id,
onComplete: () => {
setIsEditing(false);
},
onCancel: () => {
setIsEditing(false);
}
}
)
}
),
!isReadonlyMode && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "tlui-page_menu__item__submenu", "data-isediting": isEditing, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_PageItemSubmenu.PageItemSubmenu, { index, item: page, listSize: pages.length }) })
]
},
page.id + "_editing"
) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
"div",
{
"data-pageid": page.id,
"data-testid": "page-menu.item",
className: "tlui-page-menu__item",
children: [
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
import_TldrawUiButton.TldrawUiButton,
{
type: "normal",
className: "tlui-page-menu__item__button",
onClick: () => changePage(page.id),
onDoubleClick: toggleEditing,
title: msg("page-menu.go-to-page"),
onKeyDown: (e) => {
if (e.key === "Enter") {
if (page.id === currentPage.id) {
toggleEditing();
editor.markEventAsHandled(e);
}
}
},
children: [
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_TldrawUiButtonCheck.TldrawUiButtonCheck, { checked: page.id === currentPage.id }),
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_TldrawUiButtonLabel.TldrawUiButtonLabel, { children: page.name })
]
}
),
!isReadonlyMode && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "tlui-page_menu__item__submenu", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
import_PageItemSubmenu.PageItemSubmenu,
{
index,
item: page,
listSize: pages.length,
onRename: () => {
if (shouldUseWindowPrompt) {
const name = window.prompt(msg("action.rename"), page.name);
if (name && name !== page.name) {
renamePage(page.id, name);
}
} else {
setIsEditing(true);
if (currentPageId !== page.id) {
changePage(page.id);
}
}
}
}
) })
]
},
page.id
);
})
}
)
] })
}
)
] });
});
//# sourceMappingURL=DefaultPageMenu.js.map