feat: make editor ui more user friendly on mobile

This commit is contained in:
thecodrr
2022-05-12 22:28:25 +05:00
parent 68968b59c8
commit 00490285cd
76 changed files with 3169 additions and 1330 deletions

View File

@@ -2,6 +2,7 @@ import { PropsWithChildren } from "react";
import { FlexProps } from "rebass"; import { FlexProps } from "rebass";
import { MenuOptions } from "./useMenu"; import { MenuOptions } from "./useMenu";
import { MenuItem as MenuItemType } from "./types"; import { MenuItem as MenuItemType } from "./types";
import "react-spring-bottom-sheet/dist/style.css";
declare type MenuProps = MenuContainerProps & { declare type MenuProps = MenuContainerProps & {
items: MenuItemType[]; items: MenuItemType[];
closeMenu: () => void; closeMenu: () => void;
@@ -10,12 +11,24 @@ export declare function Menu(props: MenuProps): JSX.Element;
declare type MenuContainerProps = FlexProps & { declare type MenuContainerProps = FlexProps & {
title?: string; title?: string;
}; };
export declare type PopupType = "sheet" | "menu" | "none";
export declare type PopupPresenterProps = MenuPresenterProps & ActionSheetPresenterProps & {
mobile?: PopupType;
desktop?: PopupType;
};
export declare function PopupPresenter(props: PropsWithChildren<PopupPresenterProps>): JSX.Element | null;
export declare type MenuPresenterProps = MenuContainerProps & { export declare type MenuPresenterProps = MenuContainerProps & {
items: MenuItemType[]; items?: MenuItemType[];
options: MenuOptions; onClose?: () => void;
isOpen: boolean; isOpen: boolean;
onClose: () => void; options?: MenuOptions;
className?: string;
}; };
export declare function MenuPresenter(props: PropsWithChildren<MenuPresenterProps>): JSX.Element; export declare function MenuPresenter(props: PropsWithChildren<MenuPresenterProps>): JSX.Element;
export declare type ActionSheetPresenterProps = MenuContainerProps & {
items?: MenuItemType[];
isOpen: boolean;
onClose?: () => void;
blocking?: boolean;
};
export declare function ActionSheetPresenter(props: PropsWithChildren<ActionSheetPresenterProps>): JSX.Element;
export {}; export {};

View File

@@ -44,6 +44,9 @@ import { getPosition } from "./useMenu";
import MenuItem from "./menuitem"; import MenuItem from "./menuitem";
// import { useMenuTrigger, useMenu, getPosition } from "../../hooks/useMenu"; // import { useMenuTrigger, useMenu, getPosition } from "../../hooks/useMenu";
import Modal from "react-modal"; import Modal from "react-modal";
import { BottomSheet } from "react-spring-bottom-sheet";
import "react-spring-bottom-sheet/dist/style.css";
import { useIsMobile } from "../../toolbar/stores/toolbar-store";
// import { store as selectionStore } from "../../stores/selectionstore"; // import { store as selectionStore } from "../../stores/selectionstore";
function useMenuFocus(items, onAction, onClose) { function useMenuFocus(items, onAction, onClose) {
var _a = __read(useState(-1), 2), focusIndex = _a[0], setFocusIndex = _a[1]; var _a = __read(useState(-1), 2), focusIndex = _a[0], setFocusIndex = _a[1];
@@ -202,8 +205,18 @@ function MenuContainer(props) {
wordWrap: "break-word", wordWrap: "break-word",
} }, { children: title }))), children] }))); } }, { children: title }))), children] })));
} }
export function PopupPresenter(props) {
var _a = props.mobile, mobile = _a === void 0 ? "menu" : _a, _b = props.desktop, desktop = _b === void 0 ? "menu" : _b, restProps = __rest(props, ["mobile", "desktop"]);
var isMobile = useIsMobile();
if (isMobile && mobile === "sheet")
return _jsx(ActionSheetPresenter, __assign({}, restProps));
else if (mobile === "menu" || desktop === "menu")
return _jsx(MenuPresenter, __assign({}, restProps));
else
return props.isOpen ? _jsx(_Fragment, { children: props.children }) : null;
}
export function MenuPresenter(props) { export function MenuPresenter(props) {
var className = props.className, options = props.options, items = props.items, isOpen = props.isOpen, onClose = props.onClose, children = props.children, containerProps = __rest(props, ["className", "options", "items", "isOpen", "onClose", "children"]); var className = props.className, _a = props.options, options = _a === void 0 ? { type: "menu", position: {} } : _a, _b = props.items, items = _b === void 0 ? [] : _b, isOpen = props.isOpen, _c = props.onClose, onClose = _c === void 0 ? function () { } : _c, children = props.children, containerProps = __rest(props, ["className", "options", "items", "isOpen", "onClose", "children"]);
var position = options.position, type = options.type; var position = options.position, type = options.type;
var isAutocomplete = type === "autocomplete"; var isAutocomplete = type === "autocomplete";
var contentRef = useRef(); var contentRef = useRef();
@@ -255,3 +268,7 @@ export function MenuPresenter(props) {
}, },
} }, { children: props.children ? (props.children) : (_jsx(Menu, __assign({ items: items, closeMenu: onClose }, containerProps))) }))); } }, { children: props.children ? (props.children) : (_jsx(Menu, __assign({ items: items, closeMenu: onClose }, containerProps))) })));
} }
export function ActionSheetPresenter(props) {
var _a = props.items, items = _a === void 0 ? [] : _a, isOpen = props.isOpen, _b = props.onClose, onClose = _b === void 0 ? function () { } : _b, children = props.children, sx = props.sx, _c = props.blocking, blocking = _c === void 0 ? true : _c, containerProps = __rest(props, ["items", "isOpen", "onClose", "children", "sx", "blocking"]);
return (_jsx(BottomSheet, __assign({ open: isOpen, onDismiss: onClose, blocking: blocking }, { children: props.children ? (props.children) : (_jsx(Menu, __assign({ items: items, closeMenu: onClose, sx: __assign({ flex: 1, boxShadow: "none", border: "none" }, sx) }, containerProps))) })));
}

View File

@@ -0,0 +1,9 @@
import { PropsWithChildren } from "react";
declare type ResponsiveContainerProps = {
mobile?: JSX.Element;
desktop?: JSX.Element;
};
export declare function ResponsiveContainer(props: ResponsiveContainerProps): JSX.Element | null;
export declare function DesktopOnly(props: PropsWithChildren<{}>): JSX.Element;
export declare function MobileOnly(props: PropsWithChildren<{}>): JSX.Element;
export {};

View File

@@ -0,0 +1,15 @@
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
import { useToolbarStore } from "../../toolbar/stores/toolbar-store";
export function ResponsiveContainer(props) {
var isMobile = useToolbarStore(function (store) { return store.isMobile; });
if (isMobile)
return props.mobile || null;
else
return props.desktop || null;
}
export function DesktopOnly(props) {
return _jsx(ResponsiveContainer, { desktop: _jsx(_Fragment, { children: props.children }) });
}
export function MobileOnly(props) {
return _jsx(ResponsiveContainer, { mobile: _jsx(_Fragment, { children: props.children }) });
}

View File

@@ -32,6 +32,7 @@ import { ThemeProvider } from "emotion-theming";
import { Resizable } from "re-resizable"; import { Resizable } from "re-resizable";
import { ToolButton } from "../../toolbar/components/tool-button"; import { ToolButton } from "../../toolbar/components/tool-button";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { PopupPresenter, } from "../../components/menu/menu";
import { EmbedPopup } from "../../toolbar/popups/embed-popup"; import { EmbedPopup } from "../../toolbar/popups/embed-popup";
export function EmbedComponent(props) { export function EmbedComponent(props) {
var _a = props.node.attrs, src = _a.src, width = _a.width, height = _a.height, align = _a.align; var _a = props.node.attrs, src = _a.src, width = _a.width, height = _a.height, align = _a.align;
@@ -58,14 +59,27 @@ export function EmbedComponent(props) {
width: ref.clientWidth, width: ref.clientWidth,
height: ref.clientHeight, height: ref.clientHeight,
}); });
}, lockAspectRatio: true }, { children: [_jsx(Flex, __assign({ sx: { position: "relative", justifyContent: "end" } }, { children: isToolbarVisible && (_jsx(ImageToolbar, { editor: editor, align: align, height: height || 0, width: width || 0, src: src })) })), _jsx(Box, __assign({ as: "iframe", ref: embedRef, src: src, width: "100%", height: "100%", sx: { }, lockAspectRatio: true }, { children: [_jsx(Flex, __assign({ width: "100%", sx: {
border: isActive position: "relative",
? "2px solid var(--primary)" justifyContent: "end",
: "2px solid transparent", borderTop: "20px solid var(--bgSecondary)",
borderRadius: "default", // borderLeft: "20px solid var(--bgSecondary)",
borderTopLeftRadius: "default",
borderTopRightRadius: "default",
borderColor: isActive ? "border" : "bgSecondary",
cursor: "pointer",
":hover": {
borderColor: "border",
},
} }, { children: isToolbarVisible && (_jsx(EmbedToolbar, { editor: editor, align: align, height: height || 0, width: width || 0, src: src })) })), _jsx(Box, __assign({ as: "iframe", ref: embedRef, src: src, width: "100%", height: "100%", sx: {
border: "none",
// border: isActive
// ? "2px solid var(--primary)"
// : "2px solid transparent",
// borderRadius: "default",
} }, props))] })) })) })) })); } }, props))] })) })) })) }));
} }
function ImageToolbar(props) { function EmbedToolbar(props) {
var editor = props.editor, height = props.height, width = props.width, src = props.src; var editor = props.editor, height = props.height, width = props.width, src = props.src;
var _a = __read(useState(false), 2), isOpen = _a[0], setIsOpen = _a[1]; var _a = __read(useState(false), 2), isOpen = _a[0], setIsOpen = _a[1];
return (_jsxs(Flex, __assign({ sx: { return (_jsxs(Flex, __assign({ sx: {
@@ -101,5 +115,5 @@ function ImageToolbar(props) {
mr: 1, mr: 1,
borderRight: "1px solid var(--border)", borderRight: "1px solid var(--border)",
":last-of-type": { mr: 0, pr: 0, borderRight: "none" }, ":last-of-type": { mr: 0, pr: 0, borderRight: "none" },
} }, { children: _jsx(ToolButton, { toggled: isOpen, title: "Embed properties", id: "embedProperties", icon: "more", onClick: function () { return setIsOpen(function (s) { return !s; }); } }) }))] })), isOpen && (_jsx(EmbedPopup, { title: "Embed properties", icon: "close", onClose: function () { return setIsOpen(false); }, embed: props, onSourceChanged: function (src) { }, onSizeChanged: function (size) { return editor.commands.setEmbedSize(size); } }))] }))); } }, { children: _jsx(ToolButton, { toggled: isOpen, title: "Embed properties", id: "embedProperties", icon: "more", onClick: function () { return setIsOpen(function (s) { return !s; }); } }) }))] })), _jsx(PopupPresenter, __assign({ isOpen: isOpen, desktop: "none", mobile: "sheet", onClose: function () { return setIsOpen(false); }, blocking: true }, { children: _jsx(EmbedPopup, { title: "Embed properties", icon: "close", onClose: function () { return setIsOpen(false); }, embed: props, onSourceChanged: function (src) { }, onSizeChanged: function (size) { return editor.commands.setEmbedSize(size); } }) }))] })));
} }

View File

@@ -26,15 +26,15 @@ var __read = (this && this.__read) || function (o, n) {
return ar; return ar;
}; };
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { Box, Flex, Image, Text } from "rebass"; import { Box, Flex, Image } from "rebass";
import { NodeViewWrapper } from "@tiptap/react"; import { NodeViewWrapper } from "@tiptap/react";
import { ThemeProvider } from "emotion-theming"; import { ThemeProvider } from "emotion-theming";
import { Resizable } from "re-resizable"; import { Resizable } from "re-resizable";
import { ToolButton } from "../../toolbar/components/tool-button"; import { ToolButton } from "../../toolbar/components/tool-button";
import { useCallback, useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { PopupPresenter, } from "../../components/menu/menu";
import { Popup } from "../../toolbar/components/popup"; import { Popup } from "../../toolbar/components/popup";
import { Toggle } from "../../components/toggle"; import { ImageProperties } from "../../toolbar/popups/image-properties";
import { Input } from "@rebass/forms";
export function ImageComponent(props) { export function ImageComponent(props) {
var _a = props.node var _a = props.node
.attrs, src = _a.src, alt = _a.alt, title = _a.title, width = _a.width, height = _a.height, align = _a.align, float = _a.float; .attrs, src = _a.src, alt = _a.alt, title = _a.title, width = _a.width, height = _a.height, align = _a.align, float = _a.float;
@@ -75,23 +75,6 @@ export function ImageComponent(props) {
function ImageToolbar(props) { function ImageToolbar(props) {
var editor = props.editor, float = props.float, height = props.height, width = props.width; var editor = props.editor, float = props.float, height = props.height, width = props.width;
var _a = __read(useState(false), 2), isOpen = _a[0], setIsOpen = _a[1]; var _a = __read(useState(false), 2), isOpen = _a[0], setIsOpen = _a[1];
var onSizeChange = useCallback(function (newWidth, newHeight) {
var size = newWidth
? {
width: newWidth,
height: newWidth * (height / width),
}
: newHeight
? {
width: newHeight * (width / height),
height: newHeight,
}
: {
width: 0,
height: 0,
};
editor.chain().setImageSize(size).run();
}, [width, height]);
return (_jsxs(Flex, __assign({ sx: { return (_jsxs(Flex, __assign({ sx: {
flexDirection: "column", flexDirection: "column",
position: "absolute", position: "absolute",
@@ -125,21 +108,10 @@ function ImageToolbar(props) {
mr: 1, mr: 1,
borderRight: "1px solid var(--border)", borderRight: "1px solid var(--border)",
":last-of-type": { mr: 0, pr: 0, borderRight: "none" }, ":last-of-type": { mr: 0, pr: 0, borderRight: "none" },
} }, { children: _jsx(ToolButton, { toggled: isOpen, title: "Image properties", id: "imageProperties", icon: "more", onClick: function () { return setIsOpen(function (s) { return !s; }); } }) }))] })), isOpen && (_jsx(Popup, __assign({ title: "Image properties", action: { } }, { children: _jsx(ToolButton, { toggled: isOpen, title: "Image properties", id: "imageProperties", icon: "more", onClick: function () { return setIsOpen(function (s) { return !s; }); } }) }))] })), _jsx(PopupPresenter, __assign({ mobile: "sheet", desktop: "none", isOpen: isOpen, onClose: function () { return setIsOpen(false); }, blocking: false }, { children: _jsx(Popup, __assign({ title: "Image properties", action: {
icon: "close", icon: "close",
onClick: function () { onClick: function () {
setIsOpen(false); setIsOpen(false);
}, },
} }, { children: _jsxs(Flex, __assign({ sx: { width: 200, flexDirection: "column", p: 1 } }, { children: [_jsxs(Flex, __assign({ sx: { justifyContent: "space-between", alignItems: "center" } }, { children: [_jsx(Text, __assign({ variant: "body" }, { children: "Floating?" })), _jsx(Toggle, { checked: float, onClick: function () { } }, { children: _jsx(ImageProperties, __assign({}, props)) })) }))] })));
return editor
.chain()
.setImageAlignment({ float: !float, align: "left" })
.run();
} })] })), _jsxs(Flex, __assign({ sx: { alignItems: "center", mt: 2 } }, { children: [_jsx(Input, { type: "number", placeholder: "Width", value: width, sx: {
mr: 2,
p: 1,
fontSize: "body",
}, onChange: function (e) { return onSizeChange(e.target.valueAsNumber); } }), _jsx(Input, { type: "number", placeholder: "Height", value: height, sx: { p: 1, fontSize: "body" }, onChange: function (e) {
return onSizeChange(undefined, e.target.valueAsNumber);
} })] }))] })) })))] })));
} }

View File

@@ -31,12 +31,12 @@ import { Button, Text } from "rebass";
import { Icon } from "./icon"; import { Icon } from "./icon";
import { Icons } from "../icons"; import { Icons } from "../icons";
import { MenuPresenter } from "../../components/menu/menu"; import { MenuPresenter } from "../../components/menu/menu";
import { useToolbarContext } from "../hooks/useToolbarContext"; import { useToolbarLocation } from "../stores/toolbar-store";
export function Dropdown(props) { export function Dropdown(props) {
var items = props.items, selectedItem = props.selectedItem, buttonRef = props.buttonRef, menuWidth = props.menuWidth; var items = props.items, selectedItem = props.selectedItem, buttonRef = props.buttonRef, menuWidth = props.menuWidth;
var internalRef = useRef(); var internalRef = useRef();
var _a = __read(useState(false), 2), isOpen = _a[0], setIsOpen = _a[1]; var _a = __read(useState(false), 2), isOpen = _a[0], setIsOpen = _a[1];
var toolbarLocation = useToolbarContext().toolbarLocation; var toolbarLocation = useToolbarLocation();
return (_jsxs(_Fragment, { children: [_jsxs(Button, __assign({ ref: function (ref) { return (_jsxs(_Fragment, { children: [_jsxs(Button, __assign({ ref: function (ref) {
internalRef.current = ref; internalRef.current = ref;
if (buttonRef) if (buttonRef)

View File

@@ -29,9 +29,9 @@ export function Popup(props) {
return (_jsxs(Flex, __assign({ sx: { return (_jsxs(Flex, __assign({ sx: {
bg: "background", bg: "background",
flexDirection: "column", flexDirection: "column",
borderRadius: "default", // borderRadius: "default",
border: "1px solid var(--border)", // border: "1px solid var(--border)",
boxShadow: "menu", // boxShadow: "menu",
} }, { children: [title && (_jsxs(Flex, __assign({ sx: { } }, { children: [title && (_jsxs(Flex, __assign({ sx: {
justifyContent: "space-between", justifyContent: "space-between",
alignItems: "center", alignItems: "center",
@@ -41,5 +41,5 @@ export function Popup(props) {
} }
function PopupButton(props) { function PopupButton(props) {
var text = props.text, loading = props.loading, icon = props.icon, iconColor = props.iconColor, iconSize = props.iconSize, restProps = __rest(props, ["text", "loading", "icon", "iconColor", "iconSize"]); var text = props.text, loading = props.loading, icon = props.icon, iconColor = props.iconColor, iconSize = props.iconSize, restProps = __rest(props, ["text", "loading", "icon", "iconColor", "iconSize"]);
return (_jsx(Button, __assign({ variant: "dialog", sx: { p: 1, px: 2 } }, restProps, { children: loading ? (_jsx(Icon, { path: Icons.loading, size: 16, rotate: true, color: "primary" })) : icon ? (_jsx(Icon, { path: Icons[icon], size: iconSize || 18, color: iconColor || "icon" })) : (text) }))); return (_jsx(Button, __assign({ variant: "icon", sx: { p: 1, px: 2 } }, restProps, { children: loading ? (_jsx(Icon, { path: Icons.loading, size: 16, rotate: true, color: "primary" })) : icon ? (_jsx(Icon, { path: Icons[icon], size: iconSize || 18, color: iconColor || "icon" })) : (text) })));
} }

View File

@@ -43,12 +43,12 @@ import { Icons } from "../icons";
import { Icon } from "./icon"; import { Icon } from "./icon";
import { ToolButton } from "./tool-button"; import { ToolButton } from "./tool-button";
import { MenuPresenter } from "../../components/menu/menu"; import { MenuPresenter } from "../../components/menu/menu";
import { useToolbarContext } from "../hooks/useToolbarContext"; import { useToolbarLocation } from "../stores/toolbar-store";
export function SplitButton(props) { export function SplitButton(props) {
var menuPresenterProps = props.menuPresenterProps, children = props.children, toolButtonProps = __rest(props, ["menuPresenterProps", "children"]); var menuPresenterProps = props.menuPresenterProps, children = props.children, toolButtonProps = __rest(props, ["menuPresenterProps", "children"]);
var _a = __read(useState(false), 2), isOpen = _a[0], setIsOpen = _a[1]; var _a = __read(useState(false), 2), isOpen = _a[0], setIsOpen = _a[1];
var ref = useRef(null); var ref = useRef(null);
var toolbarLocation = useToolbarContext().toolbarLocation; var toolbarLocation = useToolbarLocation();
return (_jsxs(_Fragment, { children: [_jsxs(Flex, __assign({ ref: ref, sx: { return (_jsxs(_Fragment, { children: [_jsxs(Flex, __assign({ ref: ref, sx: {
borderRadius: "default", borderRadius: "default",
bg: isOpen ? "hover" : "transparent", bg: isOpen ? "hover" : "transparent",

View File

@@ -8,5 +8,6 @@ export declare type ToolButtonProps = ButtonProps & {
iconSize?: number; iconSize?: number;
toggled: boolean; toggled: boolean;
buttonRef?: React.MutableRefObject<HTMLButtonElement | null | undefined>; buttonRef?: React.MutableRefObject<HTMLButtonElement | null | undefined>;
variant?: "small" | "normal";
}; };
export declare function ToolButton(props: ToolButtonProps): JSX.Element; export declare function ToolButton(props: ToolButtonProps): JSX.Element;

View File

@@ -25,8 +25,8 @@ import { Button } from "rebass";
import { Icons } from "../icons"; import { Icons } from "../icons";
import { Icon } from "./icon"; import { Icon } from "./icon";
export function ToolButton(props) { export function ToolButton(props) {
var id = props.id, icon = props.icon, iconSize = props.iconSize, iconColor = props.iconColor, toggled = props.toggled, sx = props.sx, buttonRef = props.buttonRef, buttonProps = __rest(props, ["id", "icon", "iconSize", "iconColor", "toggled", "sx", "buttonRef"]); var id = props.id, icon = props.icon, iconSize = props.iconSize, iconColor = props.iconColor, toggled = props.toggled, sx = props.sx, buttonRef = props.buttonRef, _a = props.variant, variant = _a === void 0 ? "normal" : _a, buttonProps = __rest(props, ["id", "icon", "iconSize", "iconColor", "toggled", "sx", "buttonRef", "variant"]);
return (_jsx(Button, __assign({ ref: buttonRef, tabIndex: -1, id: "tool-".concat(id), sx: __assign({ p: 1, m: 0, bg: toggled ? "hover" : "transparent", mr: 1, ":hover": { bg: "hover" }, ":last-of-type": { return (_jsx(Button, __assign({ ref: buttonRef, tabIndex: -1, id: "tool-".concat(id), sx: __assign({ p: variant === "small" ? "3px" : 1, borderRadius: variant === "small" ? "small" : "default", m: 0, bg: toggled ? "hover" : "transparent", mr: variant === "small" ? 0 : 1, ":hover": { bg: "hover" }, ":last-of-type": {
mr: 0, mr: 0,
} }, sx) }, buttonProps, { children: _jsx(Icon, { path: Icons[icon], color: iconColor || "text", size: iconSize || 18 }) }))); } }, sx) }, buttonProps, { children: _jsx(Icon, { path: Icons[icon], color: iconColor || "text", size: iconSize || variant === "small" ? 16 : 18 }) })));
} }

View File

@@ -9,9 +9,10 @@ var __assign = (this && this.__assign) || function () {
}; };
return __assign.apply(this, arguments); return __assign.apply(this, arguments);
}; };
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime"; import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
import { TableRowFloatingMenu, TableColumnFloatingMenu } from "./table"; import { TableRowFloatingMenu, TableColumnFloatingMenu, TableFloatingMenu, } from "./table/table";
import { SearchReplaceFloatingMenu } from "./search-replace"; import { SearchReplaceFloatingMenu } from "./search-replace";
import { DesktopOnly, MobileOnly } from "../../components/responsive";
export function EditorFloatingMenus(props) { export function EditorFloatingMenus(props) {
return (_jsxs(_Fragment, { children: [_jsx(TableRowFloatingMenu, __assign({}, props)), _jsx(TableColumnFloatingMenu, __assign({}, props)), _jsx(SearchReplaceFloatingMenu, __assign({}, props))] })); return (_jsxs(_Fragment, { children: [_jsxs(DesktopOnly, { children: [_jsx(TableRowFloatingMenu, __assign({}, props)), _jsx(TableColumnFloatingMenu, __assign({}, props))] }), _jsx(MobileOnly, { children: _jsx(TableFloatingMenu, __assign({}, props)) }), _jsx(SearchReplaceFloatingMenu, __assign({}, props))] }));
} }

View File

@@ -9,88 +9,21 @@ var __assign = (this && this.__assign) || function () {
}; };
return __assign.apply(this, arguments); return __assign.apply(this, arguments);
}; };
var __read = (this && this.__read) || function (o, n) { import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
var m = typeof Symbol === "function" && o[Symbol.iterator]; import { PopupPresenter, } from "../../components/menu/menu";
if (!m) return o; import { SearchReplacePopup } from "../popups/search-replace";
var i = m.call(o), r, ar = [], e;
try {
while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
}
catch (error) { e = { error: error }; }
finally {
try {
if (r && !r.done && (m = i["return"])) m.call(i);
}
finally { if (e) throw e.error; }
}
return ar;
};
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { Input } from "@rebass/forms";
import { useCallback, useEffect, useRef, useState } from "react";
import { Flex } from "rebass";
import { MenuPresenter } from "../../components/menu/menu";
import { Popup } from "../components/popup";
import { ToolButton } from "../components/tool-button";
export function SearchReplaceFloatingMenu(props) { export function SearchReplaceFloatingMenu(props) {
var editor = props.editor; var editor = props.editor;
var _a = editor.storage var isSearching = editor.storage.searchreplace.isSearching;
.searchreplace, isSearching = _a.isSearching, selectedText = _a.selectedText;
var _b = __read(useState(false), 2), matchCase = _b[0], setMatchCase = _b[1];
var _c = __read(useState(false), 2), matchWholeWord = _c[0], setMatchWholeWord = _c[1];
var _d = __read(useState(false), 2), enableRegex = _d[0], setEnableRegex = _d[1];
var replaceText = useRef("");
var searchInputRef = useRef();
var search = useCallback(function (term) {
editor.commands.search(term, {
matchCase: matchCase,
enableRegex: enableRegex,
matchWholeWord: matchWholeWord,
});
}, [matchCase, enableRegex, matchWholeWord]);
useEffect(function () {
if (!searchInputRef.current)
return;
search(searchInputRef.current.value);
}, [search, matchCase, matchWholeWord, enableRegex]);
useEffect(function () {
if (isSearching && selectedText) {
if (searchInputRef.current) {
var input_1 = searchInputRef.current;
setTimeout(function () {
input_1.value = selectedText;
input_1.focus();
}, 0);
}
search(selectedText);
}
}, [isSearching, selectedText, search]);
if (!isSearching) if (!isSearching)
return null; return null;
return (_jsx(MenuPresenter, __assign({ isOpen: true, items: [], onClose: function () { }, options: { return (_jsx(_Fragment, { children: _jsx(PopupPresenter, __assign({ mobile: "sheet", desktop: "menu", isOpen: true, onClose: function () { return editor.commands.endSearch(); }, options: {
type: "autocomplete", type: "autocomplete",
position: { position: {
target: document.querySelector(".editor-toolbar") || "mouse", target: document.querySelector(".editor-toolbar") || "mouse",
isTargetAbsolute: true, isTargetAbsolute: true,
location: "below", location: "below",
align: "end", align: "end",
}, },
} }, { children: _jsx(Popup, { children: _jsxs(Flex, __assign({ sx: { p: 1, flexDirection: "column" } }, { children: [_jsxs(Flex, __assign({ sx: { alignItems: "start", flexShrink: 0 } }, { children: [_jsxs(Flex, __assign({ sx: { } }, { children: _jsx(SearchReplacePopup, { editor: editor }) })) }));
position: "relative",
mr: 1,
width: 200,
alignItems: "center",
} }, { children: [_jsx(Input, { defaultValue: selectedText, ref: searchInputRef, autoFocus: true, sx: { p: 1 }, placeholder: "Find", onChange: function (e) {
search(e.target.value);
} }), _jsxs(Flex, __assign({ sx: {
position: "absolute",
right: 0,
mr: 0,
} }, { children: [_jsx(ToolButton, { sx: {
mr: 0,
}, toggled: matchCase, title: "Match case", id: "matchCase", icon: "caseSensitive", onClick: function () { return setMatchCase(function (s) { return !s; }); }, iconSize: 14 }), _jsx(ToolButton, { sx: {
mr: 0,
}, toggled: matchWholeWord, title: "Match whole word", id: "matchWholeWord", icon: "wholeWord", onClick: function () { return setMatchWholeWord(function (s) { return !s; }); }, iconSize: 14 }), _jsx(ToolButton, { sx: {
mr: 0,
}, toggled: enableRegex, title: "Enable regex", id: "enableRegex", icon: "regex", onClick: function () { return setEnableRegex(function (s) { return !s; }); }, iconSize: 14 })] }))] })), _jsx(ToolButton, { toggled: false, title: "Previous match", id: "previousMatch", icon: "previousMatch", onClick: function () { return editor.commands.moveToPreviousResult(); }, sx: { mr: 0 }, iconSize: 16 }), _jsx(ToolButton, { toggled: false, title: "Next match", id: "nextMatch", icon: "nextMatch", onClick: function () { return editor.commands.moveToNextResult(); }, sx: { mr: 0 }, iconSize: 16 }), _jsx(ToolButton, { toggled: false, title: "Close", id: "close", icon: "close", onClick: function () { return editor.chain().focus().endSearch().run(); }, iconSize: 16, sx: { mr: 0 } })] })), _jsxs(Flex, __assign({ sx: { alignItems: "start", flexShrink: 0, mt: 1 } }, { children: [_jsx(Input, { sx: { p: 1, width: 200, mr: 1 }, placeholder: "Replace", onChange: function (e) { return (replaceText.current = e.target.value); } }), _jsx(ToolButton, { toggled: false, title: "Replace", id: "replace", icon: "replaceOne", onClick: function () { return editor.commands.replace(replaceText.current); }, sx: { mr: 0 }, iconSize: 16 }), _jsx(ToolButton, { toggled: false, title: "Replace all", id: "replaceAll", icon: "replaceAll", onClick: function () { return editor.commands.replaceAll(replaceText.current); }, sx: { mr: 0 }, iconSize: 16 })] }))] })) }) })));
} }

View File

@@ -45,6 +45,17 @@ var __generator = (this && this.__generator) || function (thisArg, body) {
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
} }
}; };
var __rest = (this && this.__rest) || function (s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
};
var __read = (this && this.__read) || function (o, n) { var __read = (this && this.__read) || function (o, n) {
var m = typeof Symbol === "function" && o[Symbol.iterator]; var m = typeof Symbol === "function" && o[Symbol.iterator];
if (!m) return o; if (!m) return o;
@@ -74,15 +85,15 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
import { Slider } from "@rebass/forms"; import { Slider } from "@rebass/forms";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { Flex, Text } from "rebass"; import { Flex, Text } from "rebass";
import { MenuPresenter } from "../../components/menu/menu"; import { ActionSheetPresenter, MenuPresenter, } from "../../components/menu/menu";
import { Popup } from "../components/popup"; import { Popup } from "../components/popup";
import { ToolButton } from "../components/tool-button"; import { ToolButton } from "../components/tool-button";
import { selectedRect } from "prosemirror-tables"; import { selectedRect } from "prosemirror-tables";
import { DesktopOnly, MobileOnly } from "../../components/responsive";
export function TableRowFloatingMenu(props) { export function TableRowFloatingMenu(props) {
var editor = props.editor; var editor = props.editor;
var theme = editor.storage.theme; // const theme = editor.storage.theme as Theme;
var _a = __read(useState(null), 2), position = _a[0], setPosition = _a[1]; var _a = __read(useState(null), 2), position = _a[0], setPosition = _a[1];
var _b = __read(useState(false), 2), isMenuOpen = _b[0], setIsMenuOpen = _b[1];
useEffect(function () { useEffect(function () {
var _a; var _a;
if (!editor.isActive("tableCell") && if (!editor.isActive("tableCell") &&
@@ -111,18 +122,23 @@ export function TableRowFloatingMenu(props) {
}, [editor.state.selection]); }, [editor.state.selection]);
if (!position) if (!position)
return null; return null;
return (_jsxs(MenuPresenter, __assign({ isOpen: true, items: [], onClose: function () { }, options: { return (_jsx(MenuPresenter, __assign({ isOpen: true, items: [], onClose: function () { }, options: {
type: "autocomplete", type: "autocomplete",
position: position, position: position,
} }, { children: [_jsxs(Flex, __assign({ sx: { } }, { children: _jsxs(Flex, __assign({ sx: {
bg: "background", bg: "background",
flexWrap: "nowrap", flexWrap: "nowrap",
borderRadius: "default", borderRadius: "default",
opacity: isMenuOpen ? 1 : 0.3, // opacity: isMenuOpen ? 1 : 0.3,
":hover": { ":hover": {
opacity: 1, opacity: 1,
}, },
} }, { children: [_jsx(ToolButton, { toggled: isMenuOpen, title: "Row properties", id: "properties", icon: "more", onClick: function () { return setIsMenuOpen(true); }, iconSize: 16, sx: { mr: 0, p: "3px", borderRadius: "small" } }), _jsx(ToolButton, { toggled: false, title: "Insert row below", id: "insertRowBelow", icon: "insertRowBelow", onClick: function () { return editor.chain().focus().addRowAfter().run(); }, sx: { mr: 0, p: "3px", borderRadius: "small" }, iconSize: 16 })] })), _jsx(MenuPresenter, { isOpen: isMenuOpen, onClose: function () { } }, { children: [_jsx(RowProperties, { title: "Row properties", editor: editor, variant: "small", icon: "more" }), _jsx(InsertRowBelow, { title: "Insert row below", icon: "insertRowBelow", editor: editor, variant: "small" })] })) })));
}
function RowProperties(props) {
var editor = props.editor, toolProps = __rest(props, ["editor"]);
var _a = __read(useState(false), 2), isMenuOpen = _a[0], setIsMenuOpen = _a[1];
return (_jsxs(_Fragment, { children: [_jsx(ToolButton, __assign({ toggled: isMenuOpen }, toolProps, { onClick: function () { return setIsMenuOpen(true); } })), _jsx(MenuPresenter, { isOpen: isMenuOpen, onClose: function () {
setIsMenuOpen(false); setIsMenuOpen(false);
editor.commands.focus(); editor.commands.focus();
}, options: { }, options: {
@@ -157,47 +173,20 @@ export function TableRowFloatingMenu(props) {
onClick: function () { return editor.chain().focus().deleteRow().run(); }, onClick: function () { return editor.chain().focus().deleteRow().run(); },
icon: "deleteRow", icon: "deleteRow",
}, },
] })] }))); ] })] }));
} }
export function TableColumnFloatingMenu(props) { function InsertRowBelow(props) {
var editor = props.editor, toolProps = __rest(props, ["editor"]);
return (_jsx(ToolButton, __assign({ toggled: false }, toolProps, { onClick: function () { return editor.chain().focus().addRowAfter().run(); } })));
}
function ColumnProperties(props) {
var _this = this; var _this = this;
var editor = props.editor; var editor = props.editor, currentCell = props.currentCell, toolProps = __rest(props, ["editor", "currentCell"]);
var _a = __read(useState(null), 2), position = _a[0], setPosition = _a[1]; var _a = __read(useState(false), 2), isMenuOpen = _a[0], setIsMenuOpen = _a[1];
var isInsideCellSelection = !editor.state.selection.empty && var isInsideCellSelection = !editor.state.selection.empty &&
editor.state.selection.$anchor.node().type.name === "tableCell"; editor.state.selection.$anchor.node().type.name === "tableCell";
var _b = __read(useState(false), 2), isMenuOpen = _b[0], setIsMenuOpen = _b[1]; var _b = __read(useState(false), 2), showCellProps = _b[0], setShowCellProps = _b[1];
var _c = __read(useState(false), 2), showCellProps = _c[0], setShowCellProps = _c[1]; var _c = __read(useState(null), 2), menuPosition = _c[0], setMenuPosition = _c[1];
var _d = __read(useState(null), 2), menuPosition = _d[0], setMenuPosition = _d[1];
useEffect(function () {
var _a;
if (!editor.isActive("tableCell") &&
!editor.isActive("tableRow") &&
!editor.isActive("tableHeader")) {
setPosition(null);
return;
}
var $from = editor.state.selection.$from;
var selectedNode = $from.node();
var pos = selectedNode.isTextblock ? $from.before() : $from.pos;
var currentCell = (_a = editor.view.nodeDOM(pos)) === null || _a === void 0 ? void 0 : _a.closest("td,th");
var currentTable = currentCell === null || currentCell === void 0 ? void 0 : currentCell.closest("table");
if (!currentCell || !currentTable)
return;
setPosition(function (old) {
if ((old === null || old === void 0 ? void 0 : old.target) === currentCell)
return old;
return {
isTargetAbsolute: true,
location: "top",
align: "center",
yAnchor: currentTable,
yOffset: -2,
target: currentCell,
};
});
}, [editor.state.selection]);
if (!position)
return null;
var columnProperties = [ var columnProperties = [
{ {
key: "addColumnLeft", key: "addColumnLeft",
@@ -259,7 +248,7 @@ export function TableColumnFloatingMenu(props) {
onClick: function () { onClick: function () {
setShowCellProps(true); setShowCellProps(true);
setMenuPosition({ setMenuPosition({
target: position.target || undefined, target: currentCell || undefined,
isTargetAbsolute: true, isTargetAbsolute: true,
yOffset: 10, yOffset: 10,
location: "below", location: "below",
@@ -277,20 +266,9 @@ export function TableColumnFloatingMenu(props) {
onClick: function () { return editor.chain().focus().deleteTable().run(); }, onClick: function () { return editor.chain().focus().deleteTable().run(); },
}, },
]; ];
return (_jsxs(MenuPresenter, __assign({ isOpen: true, items: [], onClose: function () { }, options: { return (_jsxs(_Fragment, { children: [_jsx(ToolButton, __assign({ toggled: isMenuOpen }, toolProps, { onClick: function () { return __awaiter(_this, void 0, void 0, function () { return __generator(this, function (_a) {
type: "autocomplete", return [2 /*return*/, setIsMenuOpen(true)];
position: position, }); }); } })), _jsx(MenuPresenter, { isOpen: isMenuOpen, onClose: function () {
} }, { children: [_jsxs(Flex, __assign({ sx: {
bg: "background",
flexWrap: "nowrap",
borderRadius: "default",
opacity: isMenuOpen || showCellProps ? 1 : 0.3,
":hover": {
opacity: 1,
},
} }, { children: [_jsx(ToolButton, { toggled: isMenuOpen, title: "Column properties", id: "properties", icon: "more", onClick: function () { return __awaiter(_this, void 0, void 0, function () { return __generator(this, function (_a) {
return [2 /*return*/, setIsMenuOpen(true)];
}); }); }, iconSize: 16, sx: { mr: 0, p: "3px", borderRadius: "small" } }), _jsx(ToolButton, { toggled: false, title: "Insert column right", id: "insertColumnRight", icon: "plus", onClick: function () { return editor.chain().focus().addColumnAfter().run(); }, sx: { mr: 0, p: "3px", borderRadius: "small" }, iconSize: 16 })] })), _jsx(MenuPresenter, { isOpen: isMenuOpen, onClose: function () {
setIsMenuOpen(false); setIsMenuOpen(false);
editor.commands.focus(); editor.commands.focus();
}, options: { }, options: {
@@ -301,13 +279,67 @@ export function TableColumnFloatingMenu(props) {
{ type: "seperator", key: "cellSeperator" } { type: "seperator", key: "cellSeperator" }
], false), __read(cellProperties), false), [ ], false), __read(cellProperties), false), [
{ type: "seperator", key: "tableSeperator" } { type: "seperator", key: "tableSeperator" }
], false), __read(tableProperties), false) }), _jsx(MenuPresenter, __assign({ isOpen: showCellProps, onClose: function () { ], false), __read(tableProperties), false) }), _jsx(DesktopOnly, { children: _jsx(MenuPresenter, __assign({ isOpen: showCellProps, onClose: function () {
setShowCellProps(false); setShowCellProps(false);
editor.commands.focus(); editor.commands.focus();
}, options: { }, options: {
type: "menu", type: "menu",
position: menuPosition || {}, position: menuPosition || {},
}, items: [] }, { children: _jsx(CellProperties, { editor: editor, onClose: function () { return setShowCellProps(false); } }) }))] }))); }, items: [] }, { children: _jsx(CellProperties, { editor: editor, onClose: function () { return setShowCellProps(false); } }) })) }), _jsx(MobileOnly, { children: _jsx(ActionSheetPresenter, __assign({ isOpen: showCellProps, onClose: function () {
setShowCellProps(false);
editor.commands.focus();
}, items: [] }, { children: _jsx(CellProperties, { editor: editor, onClose: function () { return setShowCellProps(false); } }) })) })] }));
}
function InsertColumnRight(props) {
var editor = props.editor, toolProps = __rest(props, ["editor"]);
return (_jsx(ToolButton, __assign({}, toolProps, { toggled: false, onClick: function () { return editor.chain().focus().addColumnAfter().run(); } })));
}
export function TableColumnFloatingMenu(props) {
var editor = props.editor;
var _a = __read(useState(null), 2), position = _a[0], setPosition = _a[1];
useEffect(function () {
var _a;
if (!editor.isActive("tableCell") &&
!editor.isActive("tableRow") &&
!editor.isActive("tableHeader")) {
setPosition(null);
return;
}
var $from = editor.state.selection.$from;
var selectedNode = $from.node();
var pos = selectedNode.isTextblock ? $from.before() : $from.pos;
var currentCell = (_a = editor.view.nodeDOM(pos)) === null || _a === void 0 ? void 0 : _a.closest("td,th");
var currentTable = currentCell === null || currentCell === void 0 ? void 0 : currentCell.closest("table");
if (!currentCell || !currentTable)
return;
setPosition(function (old) {
if ((old === null || old === void 0 ? void 0 : old.target) === currentCell)
return old;
return {
isTargetAbsolute: true,
location: "top",
align: "center",
yAnchor: currentTable,
yOffset: 2,
target: currentCell,
};
});
}, [editor.state.selection]);
if (!position)
return null;
return (_jsx(MenuPresenter, __assign({ isOpen: true, items: [], onClose: function () { }, options: {
type: "autocomplete",
position: position,
} }, { children: _jsxs(Flex, __assign({ sx: {
bg: "background",
flexWrap: "nowrap",
borderRadius: "default",
// opacity: 0.3,
// opacity: isMenuOpen || showCellProps ? 1 : 0.3,
":hover": {
opacity: 1,
},
} }, { children: [_jsx(ColumnProperties, { currentCell: position.target, title: "Column properties", editor: editor, icon: "more", variant: "small" }), _jsx(InsertColumnRight, { editor: editor, title: "Insert column right", variant: "small", icon: "plus" })] })) })));
} }
function CellProperties(props) { function CellProperties(props) {
var editor = props.editor, onClose = props.onClose; var editor = props.editor, onClose = props.onClose;
@@ -317,7 +349,7 @@ function CellProperties(props) {
icon: "close", icon: "close",
iconColor: "error", iconColor: "error",
onClick: onClose, onClick: onClose,
} }, { children: _jsxs(Flex, __assign({ sx: { flexDirection: "column", width: 200, px: 1, mb: 2 } }, { children: [_jsx(ColorPickerTool, { color: attributes.backgroundColor, title: "Background color", icon: "backgroundColor", onColorChange: function (color) { } }, { children: _jsxs(Flex, __assign({ sx: { flexDirection: "column", px: 1, mb: 2 } }, { children: [_jsx(ColorPickerTool, { color: attributes.backgroundColor, title: "Background color", icon: "backgroundColor", onColorChange: function (color) {
return editor.commands.setCellAttribute("backgroundColor", color); return editor.commands.setCellAttribute("backgroundColor", color);
} }), _jsx(ColorPickerTool, { color: attributes.color, title: "Text color", icon: "textColor", onColorChange: function (color) { } }), _jsx(ColorPickerTool, { color: attributes.color, title: "Text color", icon: "textColor", onColorChange: function (color) {
return editor.commands.setCellAttribute("color", color); return editor.commands.setCellAttribute("color", color);

View File

@@ -0,0 +1,6 @@
import { Editor } from "@tiptap/core";
declare function moveColumnRight(editor: Editor): void;
declare function moveColumnLeft(editor: Editor): void;
declare function moveRowDown(editor: Editor): void;
declare function moveRowUp(editor: Editor): void;
export { moveColumnLeft, moveColumnRight, moveRowDown, moveRowUp };

View File

@@ -0,0 +1,108 @@
import { selectedRect } from "prosemirror-tables";
function moveColumnRight(editor) {
var tr = editor.state.tr;
var rect = selectedRect(editor.state);
if (rect.right === rect.map.width)
return;
var transaction = moveColumn(tr, rect, rect.left, rect.left + 1);
if (!transaction)
return;
editor.view.dispatch(transaction);
}
function moveColumnLeft(editor) {
var tr = editor.state.tr;
var rect = selectedRect(editor.state);
if (rect.left === 0)
return;
var transaction = moveColumn(tr, rect, rect.left, rect.left - 1);
if (!transaction)
return;
editor.view.dispatch(transaction);
}
function moveRowDown(editor) {
var tr = editor.state.tr;
var rect = selectedRect(editor.state);
if (rect.top + 1 === rect.map.height)
return;
var transaction = moveRow(tr, rect, rect.top, rect.top + 1);
if (!transaction)
return;
editor.view.dispatch(transaction);
}
function moveRowUp(editor) {
var tr = editor.state.tr;
var rect = selectedRect(editor.state);
if (rect.top === 0)
return;
var transaction = moveRow(tr, rect, rect.top, rect.top - 1);
if (!transaction)
return;
editor.view.dispatch(transaction);
}
function moveColumn(tr, rect, from, to) {
var fromCells = getColumnCells(rect, from);
var toCells = getColumnCells(rect, to);
return moveCells(tr, rect, fromCells, toCells);
}
function getColumnCells(_a, col) {
var map = _a.map, table = _a.table;
var cells = [];
for (var row = 0; row < map.height;) {
var index = row * map.width + col;
if (index >= map.map.length)
break;
var pos = map.map[index];
var cell = table.nodeAt(pos);
if (!cell)
continue;
cells.push({ cell: cell, pos: pos });
row += cell.attrs.rowspan;
console.log(cell.textContent);
}
return cells;
}
function moveRow(tr, rect, from, to) {
var fromCells = getRowCells(rect, from);
var toCells = getRowCells(rect, to);
return moveCells(tr, rect, fromCells, toCells);
}
function getRowCells(_a, row) {
var map = _a.map, table = _a.table;
var cells = [];
for (var col = 0, index = row * map.width; col < map.width; col++, index++) {
if (index >= map.map.length)
break;
var pos = map.map[index];
var cell = table.nodeAt(pos);
if (!cell)
continue;
cells.push({ cell: cell, pos: pos });
col += cell.attrs.colspan - 1;
}
return cells;
}
function moveCells(tr, rect, fromCells, toCells) {
if (fromCells.length !== toCells.length)
return;
var mapStart = tr.mapping.maps.length;
for (var i = 0; i < toCells.length; ++i) {
var fromCell = fromCells[i];
var toCell = toCells[i];
var fromStart = tr.mapping
.slice(mapStart)
.map(rect.tableStart + fromCell.pos);
var fromEnd = fromStart + fromCell.cell.nodeSize;
var fromSlice = tr.doc.slice(fromStart, fromEnd);
var toStart = tr.mapping
.slice(mapStart)
.map(rect.tableStart + toCell.pos);
var toEnd = toStart + toCell.cell.nodeSize;
var toSlice = tr.doc.slice(toStart, toEnd);
tr.replace(toStart, toEnd, fromSlice);
fromStart = tr.mapping.slice(mapStart).map(rect.tableStart + fromCell.pos);
fromEnd = fromStart + fromCell.cell.nodeSize;
tr.replace(fromStart, fromEnd, toSlice);
}
return tr;
}
export { moveColumnLeft, moveColumnRight, moveRowDown, moveRowUp };

View File

@@ -0,0 +1 @@
export { TableColumnFloatingMenu, TableFloatingMenu, TableRowFloatingMenu, } from "./table";

View File

@@ -0,0 +1 @@
export { TableColumnFloatingMenu, TableFloatingMenu, TableRowFloatingMenu, } from "./table";

View File

@@ -0,0 +1,5 @@
/// <reference types="react" />
import { FloatingMenuProps } from "../types";
export declare function TableRowFloatingMenu(props: FloatingMenuProps): JSX.Element | null;
export declare function TableColumnFloatingMenu(props: FloatingMenuProps): JSX.Element | null;
export declare function TableFloatingMenu(props: FloatingMenuProps): JSX.Element | null;

View File

@@ -0,0 +1,149 @@
var __assign = (this && this.__assign) || function () {
__assign = Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
var __read = (this && this.__read) || function (o, n) {
var m = typeof Symbol === "function" && o[Symbol.iterator];
if (!m) return o;
var i = m.call(o), r, ar = [], e;
try {
while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
}
catch (error) { e = { error: error }; }
finally {
try {
if (r && !r.done && (m = i["return"])) m.call(i);
}
finally { if (e) throw e.error; }
}
return ar;
};
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { useEffect, useState } from "react";
import { Flex } from "rebass";
import { MenuPresenter } from "../../../components/menu/menu";
import { ColumnProperties, InsertColumnRight, InsertRowBelow, RowProperties, } from "./tools";
import { getToolbarElement } from "../../utils/dom";
import { useToolbarLocation } from "../../stores/toolbar-store";
export function TableRowFloatingMenu(props) {
var editor = props.editor;
// const theme = editor.storage.theme as Theme;
var _a = __read(useState(null), 2), position = _a[0], setPosition = _a[1];
useEffect(function () {
var _a;
if (!editor.isActive("tableCell") &&
!editor.isActive("tableRow") &&
!editor.isActive("tableHeader")) {
setPosition(null);
return;
}
var $from = editor.state.selection.$from;
var selectedNode = $from.node();
var pos = selectedNode.isTextblock ? $from.before() : $from.pos;
var currentRow = (_a = editor.view.nodeDOM(pos)) === null || _a === void 0 ? void 0 : _a.closest("tr");
if (!currentRow)
return;
setPosition(function (old) {
if ((old === null || old === void 0 ? void 0 : old.target) === currentRow)
return old;
return {
isTargetAbsolute: true,
location: "left",
xOffset: -5,
target: currentRow,
// parent: editor.view.dom as HTMLElement,
};
});
}, [editor.state.selection]);
if (!position)
return null;
return (_jsx(MenuPresenter, __assign({ isOpen: true, items: [], onClose: function () { }, options: {
type: "autocomplete",
position: position,
} }, { children: _jsxs(Flex, __assign({ sx: {
bg: "background",
flexWrap: "nowrap",
borderRadius: "default",
// opacity: isMenuOpen ? 1 : 0.3,
":hover": {
opacity: 1,
},
} }, { children: [_jsx(RowProperties, { title: "Row properties", editor: editor, variant: "small", icon: "more" }), _jsx(InsertRowBelow, { title: "Insert row below", icon: "insertRowBelow", editor: editor, variant: "small" })] })) })));
}
export function TableColumnFloatingMenu(props) {
var editor = props.editor;
var _a = __read(useState(null), 2), position = _a[0], setPosition = _a[1];
useEffect(function () {
var _a;
if (!editor.isActive("tableCell") &&
!editor.isActive("tableRow") &&
!editor.isActive("tableHeader")) {
setPosition(null);
return;
}
var $from = editor.state.selection.$from;
var selectedNode = $from.node();
var pos = selectedNode.isTextblock ? $from.before() : $from.pos;
var currentCell = (_a = editor.view.nodeDOM(pos)) === null || _a === void 0 ? void 0 : _a.closest("td,th");
var currentTable = currentCell === null || currentCell === void 0 ? void 0 : currentCell.closest("table");
if (!currentCell || !currentTable)
return;
setPosition(function (old) {
if ((old === null || old === void 0 ? void 0 : old.target) === currentCell)
return old;
return {
isTargetAbsolute: true,
location: "top",
align: "center",
yAnchor: currentTable,
yOffset: 2,
target: currentCell,
};
});
}, [editor.state.selection]);
if (!position)
return null;
return (_jsx(MenuPresenter, __assign({ isOpen: true, items: [], onClose: function () { }, options: {
type: "autocomplete",
position: position,
} }, { children: _jsxs(Flex, __assign({ sx: {
bg: "background",
flexWrap: "nowrap",
borderRadius: "default",
// opacity: 0.3,
// opacity: isMenuOpen || showCellProps ? 1 : 0.3,
":hover": {
opacity: 1,
},
} }, { children: [_jsx(ColumnProperties, { currentCell: position.target, title: "Column properties", editor: editor, icon: "more", variant: "small" }), _jsx(InsertColumnRight, { editor: editor, title: "Insert column right", variant: "small", icon: "plus" })] })) })));
}
export function TableFloatingMenu(props) {
var editor = props.editor;
var toolbarLocation = useToolbarLocation();
if (!editor.isActive("table"))
return null;
return (_jsx(MenuPresenter, __assign({ isOpen: true, items: [], onClose: function () { }, options: {
type: "autocomplete",
position: {
isTargetAbsolute: true,
target: getToolbarElement(),
location: toolbarLocation === "bottom" ? "top" : "below",
},
} }, { children: _jsxs(Flex, __assign({ sx: {
bg: "background",
flexWrap: "nowrap",
borderRadius: "default",
// opacity: 0.3,
// opacity: isMenuOpen || showCellProps ? 1 : 0.3,
":hover": {
opacity: 1,
},
} }, { children: [_jsx(RowProperties, { title: "Row properties", editor: editor, variant: "normal", icon: "rowProperties" }), _jsx(InsertRowBelow, { title: "Insert row below", icon: "insertRowBelow", editor: editor, variant: "normal" }), _jsx(ColumnProperties, { title: "Column properties", editor: editor, icon: "columnProperties", variant: "normal" }), _jsx(InsertColumnRight, { editor: editor, title: "Insert column right", variant: "normal", icon: "insertColumnRight" })] })) })));
}

View File

@@ -0,0 +1,14 @@
/// <reference types="react" />
import { ToolButtonProps } from "../../components/tool-button";
import { ToolProps } from "../../types";
declare type TableToolProps = ToolProps & {
variant: ToolButtonProps["variant"];
};
export declare function RowProperties(props: TableToolProps): JSX.Element;
export declare function InsertRowBelow(props: TableToolProps): JSX.Element;
declare type ColumnPropertiesProps = TableToolProps & {
currentCell?: HTMLElement;
};
export declare function ColumnProperties(props: ColumnPropertiesProps): JSX.Element;
export declare function InsertColumnRight(props: TableToolProps): JSX.Element;
export {};

View File

@@ -0,0 +1,242 @@
var __assign = (this && this.__assign) || function () {
__assign = Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (_) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
var __rest = (this && this.__rest) || function (s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
};
var __read = (this && this.__read) || function (o, n) {
var m = typeof Symbol === "function" && o[Symbol.iterator];
if (!m) return o;
var i = m.call(o), r, ar = [], e;
try {
while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
}
catch (error) { e = { error: error }; }
finally {
try {
if (r && !r.done && (m = i["return"])) m.call(i);
}
finally { if (e) throw e.error; }
}
return ar;
};
var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
if (ar || !(i in from)) {
if (!ar) ar = Array.prototype.slice.call(from, 0, i);
ar[i] = from[i];
}
}
return to.concat(ar || Array.prototype.slice.call(from));
};
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
import { useState } from "react";
import { MenuPresenter, PopupPresenter, } from "../../../components/menu/menu";
import { ToolButton } from "../../components/tool-button";
import { CellProperties } from "../../popups/cell-properties";
import { moveColumnLeft, moveColumnRight, moveRowDown, moveRowUp, } from "./actions";
export function RowProperties(props) {
var editor = props.editor, toolProps = __rest(props, ["editor"]);
var _a = __read(useState(false), 2), isMenuOpen = _a[0], setIsMenuOpen = _a[1];
return (_jsxs(_Fragment, { children: [_jsx(ToolButton, __assign({ toggled: isMenuOpen }, toolProps, { onClick: function () { return setIsMenuOpen(true); } })), _jsx(MenuPresenter, { isOpen: isMenuOpen, onClose: function () {
setIsMenuOpen(false);
editor.commands.focus();
}, options: {
type: "menu",
position: {},
}, items: [
{
key: "addRowAbove",
type: "menuitem",
title: "Add row above",
onClick: function () { return editor.chain().focus().addRowBefore().run(); },
icon: "insertRowAbove",
},
{
key: "moveRowUp",
type: "menuitem",
title: "Move row up",
onClick: function () { return moveRowUp(editor); },
icon: "moveRowUp",
},
{
key: "moveRowDown",
type: "menuitem",
title: "Move row down",
onClick: function () { return moveRowDown(editor); },
icon: "moveRowDown",
},
{
key: "deleteRow",
type: "menuitem",
title: "Delete row",
onClick: function () { return editor.chain().focus().deleteRow().run(); },
icon: "deleteRow",
},
] })] }));
}
export function InsertRowBelow(props) {
var editor = props.editor, toolProps = __rest(props, ["editor"]);
return (_jsx(ToolButton, __assign({ toggled: false }, toolProps, { onClick: function () { return editor.chain().focus().addRowAfter().run(); } })));
}
export function ColumnProperties(props) {
var _this = this;
var editor = props.editor, currentCell = props.currentCell, toolProps = __rest(props, ["editor", "currentCell"]);
var _a = __read(useState(false), 2), isMenuOpen = _a[0], setIsMenuOpen = _a[1];
var isInsideCellSelection = !editor.state.selection.empty &&
editor.state.selection.$anchor.node().type.name === "tableCell";
var _b = __read(useState(false), 2), showCellProps = _b[0], setShowCellProps = _b[1];
var _c = __read(useState(null), 2), menuPosition = _c[0], setMenuPosition = _c[1];
var columnProperties = [
{
key: "addColumnLeft",
type: "menuitem",
title: "Add column left",
onClick: function () { return editor.chain().focus().addColumnBefore().run(); },
icon: "insertColumnLeft",
},
{
key: "addColumnRight",
type: "menuitem",
title: "Add column right",
onClick: function () { return editor.chain().focus().addColumnAfter().run(); },
icon: "insertColumnRight",
},
{
key: "moveColumnLeft",
type: "menuitem",
title: "Move column left",
onClick: function () { return moveColumnLeft(editor); },
icon: "moveColumnLeft",
},
{
key: "moveColumnRight",
type: "menuitem",
title: "Move column right",
onClick: function () { return moveColumnRight(editor); },
icon: "moveColumnRight",
},
{
key: "deleteColumn",
type: "menuitem",
title: "Delete column",
onClick: function () { return editor.chain().focus().deleteColumn().run(); },
icon: "deleteColumn",
},
];
var mergeSplitProperties = [
{
key: "splitCells",
type: "menuitem",
title: "Split cells",
onClick: function () { return editor.chain().focus().splitCell().run(); },
icon: "splitCells",
},
{
key: "mergeCells",
type: "menuitem",
title: "Merge cells",
onClick: function () { return editor.chain().focus().mergeCells().run(); },
icon: "mergeCells",
},
];
var cellProperties = [
{
key: "cellProperties",
type: "menuitem",
title: "Cell properties",
onClick: function () {
setShowCellProps(true);
setMenuPosition({
target: currentCell || undefined,
isTargetAbsolute: true,
yOffset: 10,
location: "below",
});
},
icon: "cellProperties",
},
];
var tableProperties = [
{
key: "deleteTable",
type: "menuitem",
title: "Delete table",
icon: "deleteTable",
onClick: function () { return editor.chain().focus().deleteTable().run(); },
},
];
return (_jsxs(_Fragment, { children: [_jsx(ToolButton, __assign({ toggled: isMenuOpen }, toolProps, { onClick: function () { return __awaiter(_this, void 0, void 0, function () { return __generator(this, function (_a) {
return [2 /*return*/, setIsMenuOpen(true)];
}); }); } })), _jsx(PopupPresenter, { isOpen: isMenuOpen, onClose: function () {
setIsMenuOpen(false);
editor.commands.focus();
}, mobile: "sheet", items: isInsideCellSelection
? __spreadArray(__spreadArray([], __read(mergeSplitProperties), false), __read(cellProperties), false) : __spreadArray(__spreadArray(__spreadArray(__spreadArray(__spreadArray([], __read(columnProperties), false), [
{ type: "seperator", key: "cellSeperator" }
], false), __read(cellProperties), false), [
{ type: "seperator", key: "tableSeperator" }
], false), __read(tableProperties), false) }), _jsx(PopupPresenter, __assign({ isOpen: showCellProps, onClose: function () {
setShowCellProps(false);
editor.commands.focus();
}, options: {
type: "menu",
position: menuPosition || {},
}, mobile: "sheet" }, { children: _jsx(CellProperties, { editor: editor, onClose: function () { return setShowCellProps(false); } }) }))] }));
}
export function InsertColumnRight(props) {
var editor = props.editor, toolProps = __rest(props, ["editor"]);
return (_jsx(ToolButton, __assign({}, toolProps, { toggled: false, onClick: function () { return editor.chain().focus().addColumnAfter().run(); } })));
}

View File

@@ -1,12 +1,9 @@
import React from "react"; import React from "react";
export declare type ToolbarLocation = "top" | "bottom";
export declare const ToolbarContext: React.Context<{ export declare const ToolbarContext: React.Context<{
currentPopup?: string | undefined; currentPopup?: string | undefined;
setCurrentPopup?: React.Dispatch<React.SetStateAction<string | undefined>> | undefined; setCurrentPopup?: React.Dispatch<React.SetStateAction<string | undefined>> | undefined;
toolbarLocation?: ToolbarLocation | undefined;
}>; }>;
export declare function useToolbarContext(): { export declare function useToolbarContext(): {
currentPopup?: string | undefined; currentPopup?: string | undefined;
setCurrentPopup?: React.Dispatch<React.SetStateAction<string | undefined>> | undefined; setCurrentPopup?: React.Dispatch<React.SetStateAction<string | undefined>> | undefined;
toolbarLocation?: ToolbarLocation | undefined;
}; };

View File

@@ -31,12 +31,14 @@ export declare const Icons: {
upload: string; upload: string;
attachment: string; attachment: string;
table: string; table: string;
rowProperties: string;
insertRowBelow: string; insertRowBelow: string;
insertRowAbove: string; insertRowAbove: string;
moveRowDown: string; moveRowDown: string;
moveRowUp: string; moveRowUp: string;
deleteRow: string; deleteRow: string;
toggleHeaderRow: string; toggleHeaderRow: string;
columnProperties: string;
insertColumnRight: string; insertColumnRight: string;
insertColumnLeft: string; insertColumnLeft: string;
moveColumnRight: string; moveColumnRight: string;

View File

@@ -1,4 +1,4 @@
import { mdiAttachment, mdiBorderHorizontal, mdiCheck, mdiChevronDown, mdiCodeBraces, mdiCodeTags, mdiDotsVertical, mdiFormatAlignCenter, mdiFormatAlignJustify, mdiFormatAlignLeft, mdiFormatAlignRight, mdiFormatBold, mdiFormatClear, mdiFormatColorHighlight, mdiFormatColorText, mdiFormatItalic, mdiFormatListBulleted, mdiFormatListNumbered, mdiFormatQuoteClose, mdiFormatStrikethrough, mdiFormatSubscript, mdiFormatSuperscript, mdiFormatTextdirectionLToR, mdiFormatTextdirectionRToL, mdiFormatUnderline, mdiImage, mdiInvertColorsOff, mdiLinkPlus, mdiLoading, mdiTable, mdiTableBorder, mdiTableRowPlusBefore, mdiTableRowRemove, mdiTableColumnPlusAfter, mdiTableColumnPlusBefore, mdiTableColumnRemove, mdiUploadOutline, mdiPlus, mdiSquareRoundedBadgeOutline, mdiFormatColorFill, mdiBorderAllVariant, mdiClose, mdiSortDescending, mdiArrowExpandRight, mdiArrowExpandLeft, mdiArrowExpandDown, mdiArrowExpandUp, mdiTrashCanOutline, mdiTableMergeCells, mdiTableSplitCell, mdiDeleteOutline, mdiDownloadOutline, mdiFormatListCheckbox, mdiDrag, mdiCheckboxMarkedOutline, mdiChevronUp, mdiArrowUp, mdiArrowDown, mdiRegex, mdiFormatLetterCase, mdiFormatLetterMatches, mdiMoviePlusOutline, mdiLink, mdiChevronRight, } from "@mdi/js"; import { mdiAttachment, mdiBorderHorizontal, mdiCheck, mdiChevronDown, mdiCodeBraces, mdiCodeTags, mdiDotsVertical, mdiFormatAlignCenter, mdiFormatAlignJustify, mdiFormatAlignLeft, mdiFormatAlignRight, mdiFormatBold, mdiFormatClear, mdiFormatColorHighlight, mdiFormatColorText, mdiFormatItalic, mdiFormatListBulleted, mdiFormatListNumbered, mdiFormatQuoteClose, mdiFormatStrikethrough, mdiFormatSubscript, mdiFormatSuperscript, mdiFormatTextdirectionLToR, mdiFormatTextdirectionRToL, mdiFormatUnderline, mdiImage, mdiInvertColorsOff, mdiLinkPlus, mdiLoading, mdiTable, mdiTableBorder, mdiTableRowPlusAfter, mdiTableRowPlusBefore, mdiTableRowRemove, mdiTableColumnPlusAfter, mdiTableColumnPlusBefore, mdiTableColumnRemove, mdiUploadOutline, mdiPlus, mdiSquareRoundedBadgeOutline, mdiFormatColorFill, mdiBorderAllVariant, mdiClose, mdiSortDescending, mdiArrowExpandRight, mdiArrowExpandLeft, mdiArrowExpandDown, mdiArrowExpandUp, mdiTrashCanOutline, mdiTableMergeCells, mdiTableSplitCell, mdiDeleteOutline, mdiDownloadOutline, mdiFormatListCheckbox, mdiDrag, mdiCheckboxMarkedOutline, mdiChevronUp, mdiArrowUp, mdiArrowDown, mdiRegex, mdiFormatLetterCase, mdiFormatLetterMatches, mdiMoviePlusOutline, mdiLink, mdiChevronRight, mdiTableColumnWidth, mdiTableRowHeight, } from "@mdi/js";
export var Icons = { export var Icons = {
bold: mdiFormatBold, bold: mdiFormatBold,
italic: mdiFormatItalic, italic: mdiFormatItalic,
@@ -32,12 +32,14 @@ export var Icons = {
upload: mdiUploadOutline, upload: mdiUploadOutline,
attachment: mdiAttachment, attachment: mdiAttachment,
table: mdiTable, table: mdiTable,
insertRowBelow: mdiPlus, rowProperties: mdiTableRowHeight,
insertRowBelow: mdiTableRowPlusAfter,
insertRowAbove: mdiTableRowPlusBefore, insertRowAbove: mdiTableRowPlusBefore,
moveRowDown: mdiArrowExpandDown, moveRowDown: mdiArrowExpandDown,
moveRowUp: mdiArrowExpandUp, moveRowUp: mdiArrowExpandUp,
deleteRow: mdiTableRowRemove, deleteRow: mdiTableRowRemove,
toggleHeaderRow: mdiTableBorder, toggleHeaderRow: mdiTableBorder,
columnProperties: mdiTableColumnWidth,
insertColumnRight: mdiTableColumnPlusAfter, insertColumnRight: mdiTableColumnPlusAfter,
insertColumnLeft: mdiTableColumnPlusBefore, insertColumnLeft: mdiTableColumnPlusBefore,
moveColumnRight: mdiArrowExpandRight, moveColumnRight: mdiArrowExpandRight,

View File

@@ -0,0 +1,8 @@
/// <reference types="react" />
import { Editor } from "@tiptap/core";
declare type CellPropertiesProps = {
editor: Editor;
onClose: () => void;
};
export declare function CellProperties(props: CellPropertiesProps): JSX.Element;
export {};

View File

@@ -0,0 +1,83 @@
var __assign = (this && this.__assign) || function () {
__assign = Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
var __read = (this && this.__read) || function (o, n) {
var m = typeof Symbol === "function" && o[Symbol.iterator];
if (!m) return o;
var i = m.call(o), r, ar = [], e;
try {
while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
}
catch (error) { e = { error: error }; }
finally {
try {
if (r && !r.done && (m = i["return"])) m.call(i);
}
finally { if (e) throw e.error; }
}
return ar;
};
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
import { Slider } from "@rebass/forms";
import { useRef, useState } from "react";
import { Flex, Text } from "rebass";
import { MenuPresenter } from "../../components/menu/menu";
import { Popup } from "../components/popup";
import { ToolButton } from "../components/tool-button";
export function CellProperties(props) {
var editor = props.editor, onClose = props.onClose;
var attributes = editor.getAttributes("tableCell");
return (_jsx(Popup, __assign({ title: "Cell properties", action: {
icon: "close",
iconColor: "error",
onClick: onClose,
} }, { children: _jsxs(Flex, __assign({ sx: { flexDirection: "column", px: 1, mb: 2 } }, { children: [_jsx(ColorPickerTool, { color: attributes.backgroundColor, title: "Background color", icon: "backgroundColor", onColorChange: function (color) {
return editor.commands.setCellAttribute("backgroundColor", color);
} }), _jsx(ColorPickerTool, { color: attributes.color, title: "Text color", icon: "textColor", onColorChange: function (color) {
return editor.commands.setCellAttribute("color", color);
} }), _jsx(ColorPickerTool, { color: attributes.borderColor, title: "Border color", icon: "borderColor", onColorChange: function (color) {
return editor.commands.setCellAttribute("borderColor", color);
} }), _jsxs(Flex, __assign({ sx: { flexDirection: "column" } }, { children: [_jsxs(Flex, __assign({ sx: {
justifyContent: "space-between",
alignItems: "center",
mt: 1,
} }, { children: [_jsx(Text, __assign({ variant: "body" }, { children: "Border width" })), _jsxs(Text, __assign({ variant: "body" }, { children: [attributes.borderWidth || 1, "px"] }))] })), _jsx(Slider, { min: 1, max: 5, value: attributes.borderWidth || 1, onChange: function (e) {
editor.commands.setCellAttribute("borderWidth", e.target.valueAsNumber);
} })] }))] })) })));
}
function ColorPickerTool(props) {
var color = props.color, title = props.title, icon = props.icon, onColorChange = props.onColorChange;
var _a = __read(useState(false), 2), isOpen = _a[0], setIsOpen = _a[1];
var buttonRef = useRef(null);
return (_jsxs(_Fragment, { children: [_jsxs(Flex, __assign({ sx: { justifyContent: "space-between", alignItems: "center", mt: 1 } }, { children: [_jsx(Text, __assign({ variant: "body" }, { children: title })), _jsx(ToolButton, { buttonRef: buttonRef, toggled: isOpen, title: title, id: icon, icon: icon, iconSize: 16, sx: {
p: "2.5px",
borderRadius: "small",
backgroundColor: color || "transparent",
":hover": { bg: color, filter: "brightness(90%)" },
}, onClick: function () { return setIsOpen(true); } })] })), _jsx(MenuPresenter, __assign({ isOpen: isOpen, onClose: function () { return setIsOpen(false); }, items: [], options: {
type: "menu",
position: {
target: buttonRef.current || undefined,
location: "below",
align: "center",
isTargetAbsolute: true,
yOffset: 5,
},
} }, { children: _jsx(Flex, { sx: {
flexDirection: "column",
bg: "background",
boxShadow: "menu",
border: "1px solid var(--border)",
borderRadius: "default",
p: 1,
width: 160,
} }) }))] }));
}

View File

@@ -92,23 +92,28 @@ export function EmbedPopup(props) {
src: _src, src: _src,
}); });
}, },
} }, { children: _jsxs(Flex, __assign({ sx: { width: 300, flexDirection: "column", p: 1 } }, { children: [error && (_jsxs(Text, __assign({ variant: "error", sx: { } }, { children: _jsxs(Flex, __assign({ sx: { flexDirection: "column", p: 1 } }, { children: [error && (_jsxs(Text, __assign({ variant: "error", sx: {
bg: "errorBg", bg: "errorBg",
color: "error", color: "error",
p: 1, p: 1,
borderRadius: "default", borderRadius: "default",
} }, { children: ["Error: ", error] }))), _jsxs(Flex, { children: [_jsx(Button, __assign({ variant: "dialog", sx: { } }, { children: ["Error: ", error] }))), _jsxs(Flex, __assign({ sx: { mb: 1 } }, { children: [_jsx(Button, __assign({ variant: "dialog", sx: {
p: 1, pb: 1,
mr: 1, mr: 1,
borderRadius: 0,
color: embedSource === "url" ? "primary" : "text", color: embedSource === "url" ? "primary" : "text",
}, onClick: function () { return setEmbedSource("url"); } }, { children: "From link" })), _jsx(Button, __assign({ variant: "dialog", sx: { borderBottom: "2px solid",
p: 1, borderBottomColor: embedSource === "url" ? "primary" : "transparent",
}, onClick: function () { return setEmbedSource("url"); } }, { children: "From URL" })), _jsx(Button, __assign({ variant: "dialog", sx: {
pb: 1,
borderRadius: 0,
color: embedSource === "code" ? "primary" : "text", color: embedSource === "code" ? "primary" : "text",
}, onClick: function () { return setEmbedSource("code"); } }, { children: "From code" }))] }), embedSource === "url" ? (_jsx(Input, { placeholder: "Embed source URL", value: src, autoFocus: true, onChange: function (e) { return setSrc(e.target.value); }, sx: { p: 1, mt: 1, fontSize: "body" } })) : (_jsx(Textarea, { autoFocus: true, variant: "forms.input", sx: { fontSize: "subBody", fontFamily: "monospace", mt: 1 }, minHeight: 100, onChange: function (e) { return setSrc(e.target.value); }, placeholder: "Paste embed code here. Only iframes are supported." })), embedSource === "url" ? (_jsxs(Flex, __assign({ sx: { alignItems: "center", mt: 2 } }, { children: [_jsx(Input, { type: "number", placeholder: "Width", value: width, sx: { borderBottom: "2px solid",
mr: 2, borderBottomColor: embedSource === "code" ? "primary" : "transparent",
p: 1, }, onClick: function () { return setEmbedSource("code"); } }, { children: "From code" }))] })), embedSource === "url" ? (_jsx(Input, { placeholder: "Enter embed source URL", value: src, autoFocus: true, onChange: function (e) { return setSrc(e.target.value); }, sx: { mt: 1, fontSize: "body" } })) : (_jsx(Textarea, { autoFocus: true, variant: "forms.input", sx: { fontSize: "subBody", fontFamily: "monospace", mt: 1 }, minHeight: [200, 100], onChange: function (e) { return setSrc(e.target.value); }, placeholder: "Paste embed code here. Only iframes are supported." })), embedSource === "url" ? (_jsxs(Flex, __assign({ sx: { alignItems: "center", mt: 1 } }, { children: [_jsx(Input, { type: "number", placeholder: "Width", value: width, sx: {
mr: 1,
fontSize: "body", fontSize: "body",
}, onChange: function (e) { return onSizeChange(e.target.valueAsNumber); } }), _jsx(Input, { type: "number", placeholder: "Height", value: height, sx: { p: 1, fontSize: "body" }, onChange: function (e) { return onSizeChange(undefined, e.target.valueAsNumber); } })] }))) : null] })) }))); }, onChange: function (e) { return onSizeChange(e.target.valueAsNumber); } }), _jsx(Input, { type: "number", placeholder: "Height", value: height, sx: { fontSize: "body" }, onChange: function (e) { return onSizeChange(undefined, e.target.valueAsNumber); } })] }))) : null] })) })));
} }
function getAttribute(document, id) { function getAttribute(document, id) {
var element = document.querySelector("[".concat(id, "]")); var element = document.querySelector("[".concat(id, "]"));

View File

@@ -0,0 +1,7 @@
/// <reference types="react" />
import { ImageAlignmentOptions, ImageSizeOptions } from "../../extensions/image";
import { Editor } from "@tiptap/core";
export declare type ImagePropertiesProps = ImageSizeOptions & ImageAlignmentOptions & {
editor: Editor;
};
export declare function ImageProperties(props: ImagePropertiesProps): JSX.Element;

View File

@@ -0,0 +1,46 @@
var __assign = (this && this.__assign) || function () {
__assign = Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { Flex, Text } from "rebass";
import { useCallback } from "react";
import { Toggle } from "../../components/toggle";
import { Input } from "@rebass/forms";
export function ImageProperties(props) {
var height = props.height, width = props.width, float = props.float, editor = props.editor;
var onSizeChange = useCallback(function (newWidth, newHeight) {
var size = newWidth
? {
width: newWidth,
height: newWidth * (height / width),
}
: newHeight
? {
width: newHeight * (width / height),
height: newHeight,
}
: {
width: 0,
height: 0,
};
editor.chain().setImageSize(size).run();
}, [width, height]);
return (_jsxs(Flex, __assign({ sx: { width: 200, flexDirection: "column", p: 1 } }, { children: [_jsxs(Flex, __assign({ sx: { justifyContent: "space-between", alignItems: "center" } }, { children: [_jsx(Text, __assign({ variant: "body" }, { children: "Floating?" })), _jsx(Toggle, { checked: float, onClick: function () {
return editor
.chain()
.setImageAlignment({ float: !float, align: "left" })
.run();
} })] })), _jsxs(Flex, __assign({ sx: { alignItems: "center", mt: 2 } }, { children: [_jsx(Input, { type: "number", placeholder: "Width", value: width, sx: {
mr: 2,
p: 1,
fontSize: "body",
}, onChange: function (e) { return onSizeChange(e.target.valueAsNumber); } }), _jsx(Input, { type: "number", placeholder: "Height", value: height, sx: { p: 1, fontSize: "body" }, onChange: function (e) { return onSizeChange(undefined, e.target.valueAsNumber); } })] }))] })));
}

View File

@@ -0,0 +1,6 @@
/// <reference types="react" />
import { Editor } from "@tiptap/core";
export declare type SearchReplacePopupProps = {
editor: Editor;
};
export declare function SearchReplacePopup(props: SearchReplacePopupProps): JSX.Element;

View File

@@ -0,0 +1,103 @@
var __assign = (this && this.__assign) || function () {
__assign = Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
var __read = (this && this.__read) || function (o, n) {
var m = typeof Symbol === "function" && o[Symbol.iterator];
if (!m) return o;
var i = m.call(o), r, ar = [], e;
try {
while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
}
catch (error) { e = { error: error }; }
finally {
try {
if (r && !r.done && (m = i["return"])) m.call(i);
}
finally { if (e) throw e.error; }
}
return ar;
};
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { Input } from "@rebass/forms";
import { useCallback, useEffect, useRef, useState } from "react";
import { Flex } from "rebass";
import { ToolButton } from "../components/tool-button";
export function SearchReplacePopup(props) {
var editor = props.editor;
var selectedText = editor.storage.searchreplace.selectedText;
var _a = __read(useState(false), 2), matchCase = _a[0], setMatchCase = _a[1];
var _b = __read(useState(false), 2), matchWholeWord = _b[0], setMatchWholeWord = _b[1];
var _c = __read(useState(false), 2), enableRegex = _c[0], setEnableRegex = _c[1];
var replaceText = useRef("");
var searchInputRef = useRef();
var search = useCallback(function (term) {
editor.commands.search(term, {
matchCase: matchCase,
enableRegex: enableRegex,
matchWholeWord: matchWholeWord,
});
}, [matchCase, enableRegex, matchWholeWord]);
useEffect(function () {
if (!searchInputRef.current)
return;
search(searchInputRef.current.value);
}, [search, matchCase, matchWholeWord, enableRegex]);
useEffect(function () {
if (selectedText) {
if (searchInputRef.current) {
var input_1 = searchInputRef.current;
setTimeout(function () {
input_1.value = selectedText;
input_1.focus();
}, 0);
}
search(selectedText);
}
}, [selectedText, search]);
return (
// <MenuPresenter
// isOpen
// items={[]}
// onClose={() => {}}
// options={{
// type: "autocomplete",
// position: {
// target:
// document.querySelector<HTMLElement>(".editor-toolbar") || "mouse",
// isTargetAbsolute: true,
// location: "below",
// align: "end",
// },
// }}
// >
// <Popup>
_jsxs(Flex, __assign({ sx: { p: 1, flexDirection: "column" } }, { children: [_jsxs(Flex, __assign({ sx: { alignItems: "start", flexShrink: 0 } }, { children: [_jsxs(Flex, __assign({ sx: {
position: "relative",
mr: 1,
width: 200,
alignItems: "center",
} }, { children: [_jsx(Input, { defaultValue: selectedText, ref: searchInputRef, autoFocus: true, sx: { p: 1 }, placeholder: "Find", onChange: function (e) {
search(e.target.value);
} }), _jsxs(Flex, __assign({ sx: {
position: "absolute",
right: 0,
mr: 0,
} }, { children: [_jsx(ToolButton, { sx: {
mr: 0,
}, toggled: matchCase, title: "Match case", id: "matchCase", icon: "caseSensitive", onClick: function () { return setMatchCase(function (s) { return !s; }); }, iconSize: 14 }), _jsx(ToolButton, { sx: {
mr: 0,
}, toggled: matchWholeWord, title: "Match whole word", id: "matchWholeWord", icon: "wholeWord", onClick: function () { return setMatchWholeWord(function (s) { return !s; }); }, iconSize: 14 }), _jsx(ToolButton, { sx: {
mr: 0,
}, toggled: enableRegex, title: "Enable regex", id: "enableRegex", icon: "regex", onClick: function () { return setEnableRegex(function (s) { return !s; }); }, iconSize: 14 })] }))] })), _jsx(ToolButton, { toggled: false, title: "Previous match", id: "previousMatch", icon: "previousMatch", onClick: function () { return editor.commands.moveToPreviousResult(); }, sx: { mr: 0 }, iconSize: 16 }), _jsx(ToolButton, { toggled: false, title: "Next match", id: "nextMatch", icon: "nextMatch", onClick: function () { return editor.commands.moveToNextResult(); }, sx: { mr: 0 }, iconSize: 16 }), _jsx(ToolButton, { toggled: false, title: "Close", id: "close", icon: "close", onClick: function () { return editor.chain().focus().endSearch().run(); }, iconSize: 16, sx: { mr: 0 } })] })), _jsxs(Flex, __assign({ sx: { alignItems: "start", flexShrink: 0, mt: 1 } }, { children: [_jsx(Input, { sx: { p: 1, width: 200, mr: 1 }, placeholder: "Replace", onChange: function (e) { return (replaceText.current = e.target.value); } }), _jsx(ToolButton, { toggled: false, title: "Replace", id: "replace", icon: "replaceOne", onClick: function () { return editor.commands.replace(replaceText.current); }, sx: { mr: 0 }, iconSize: 16 }), _jsx(ToolButton, { toggled: false, title: "Replace all", id: "replaceAll", icon: "replaceAll", onClick: function () { return editor.commands.replaceAll(replaceText.current); }, sx: { mr: 0 }, iconSize: 16 })] }))] }))
// </Popup>
// </MenuPresenter>
);
}

View File

@@ -4,7 +4,9 @@ declare type TableSize = {
rows: number; rows: number;
}; };
export declare type TablePopupProps = { export declare type TablePopupProps = {
onClose: (size: TableSize) => void; onInsertTable: (size: TableSize) => void;
cellSize?: number;
autoExpand?: boolean;
}; };
export declare function TablePopup(props: TablePopupProps): JSX.Element; export declare function TablePopup(props: TablePopupProps): JSX.Element;
export {}; export {};

View File

@@ -28,12 +28,14 @@ var __read = (this && this.__read) || function (o, n) {
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { Box, Flex, Text } from "rebass"; import { Box, Flex, Text } from "rebass";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Popup } from "../components/popup";
import { Input } from "@rebass/forms";
var MAX_COLUMNS = 20; var MAX_COLUMNS = 20;
var MAX_ROWS = 20; var MAX_ROWS = 20;
var MIN_COLUMNS = 12; var MIN_COLUMNS = 12;
var MIN_ROWS = 6; var MIN_ROWS = 6;
export function TablePopup(props) { export function TablePopup(props) {
var onClose = props.onClose; var onInsertTable = props.onInsertTable, cellSize = props.cellSize, autoExpand = props.autoExpand;
var _a = __read(useState({ var _a = __read(useState({
column: 0, column: 0,
row: 0, row: 0,
@@ -43,6 +45,8 @@ export function TablePopup(props) {
rows: MIN_ROWS, rows: MIN_ROWS,
}), 2), tableSize = _b[0], setTableSize = _b[1]; }), 2), tableSize = _b[0], setTableSize = _b[1];
useEffect(function () { useEffect(function () {
if (!autoExpand)
return;
setTableSize(function (old) { setTableSize(function (old) {
var columns = old.columns, rows = old.rows; var columns = old.columns, rows = old.rows;
var column = cellLocation.column, row = cellLocation.row; var column = cellLocation.column, row = cellLocation.row;
@@ -58,31 +62,57 @@ export function TablePopup(props) {
: Math.min(old.rows + rowFactor, MAX_ROWS), : Math.min(old.rows + rowFactor, MAX_ROWS),
}; };
}); });
}, [cellLocation]); }, [cellLocation, autoExpand]);
return (_jsxs(Flex, __assign({ sx: { p: 1, flexDirection: "column", alignItems: "center" } }, { children: [_jsx(Box, __assign({ sx: { return (_jsx(Popup, __assign({ title: "Insert table", action: {
display: "grid", icon: "check",
gridTemplateColumns: "1fr ".repeat(tableSize.columns), onClick: function () {
gap: "3px", onInsertTable({
bg: "background", columns: cellLocation.column,
} }, { children: Array(tableSize.columns * tableSize.rows) rows: cellLocation.row,
.fill(0) });
.map(function (_, index) { return (_jsx(Box, { width: 15, height: 15, sx: { },
border: "1px solid var(--disabled)", } }, { children: _jsxs(Flex, __assign({ sx: { p: 1, flexDirection: "column", alignItems: "center" } }, { children: [_jsx(Box, __assign({ sx: {
borderRadius: "2px", display: "grid",
bg: isCellHighlighted(index, cellLocation, tableSize) gridTemplateColumns: "repeat(".concat(tableSize.columns, ", minmax(").concat(cellSize || 15, "px, 1fr))"),
? "disabled" gap: "3px",
: "transparent", bg: "background",
":hover": { width: "100%",
bg: "disabled", }, onTouchMove: function (e) {
}, var touch = e.touches.item(0);
}, onMouseEnter: function () { var element = document.elementFromPoint(touch.pageX, touch.pageY);
setCellLocation(getCellLocation(index, tableSize)); if (!element)
}, onClick: function () { return;
onClose({ var index = element.dataset.index;
columns: cellLocation.column, if (!index)
rows: cellLocation.row, return;
}); setCellLocation(getCellLocation(parseInt(index), tableSize));
} })); }) })), _jsxs(Text, __assign({ variant: "body", sx: { mt: 1 } }, { children: [cellLocation.column, "x", cellLocation.row] }))] }))); } }, { children: Array(tableSize.columns * tableSize.rows)
.fill(0)
.map(function (_, index) { return (_jsx(Box, { "data-index": index, height: cellSize || 15, sx: {
border: "1px solid var(--disabled)",
borderRadius: "2px",
bg: isCellHighlighted(index, cellLocation, tableSize)
? "disabled"
: "transparent",
}, onTouchStart: function () {
setCellLocation(getCellLocation(index, tableSize));
}, onMouseEnter: function () {
setCellLocation(getCellLocation(index, tableSize));
}, onClick: function () {
onInsertTable({
columns: cellLocation.column,
rows: cellLocation.row,
});
} })); }) })), _jsxs(Flex, __assign({ sx: {
display: ["flex", "none"],
my: 1,
alignItems: "center",
justifyContent: "center",
} }, { children: [_jsx(Input, { placeholder: "".concat(cellLocation.column, " columns"), sx: { mr: 1 }, type: "number", value: cellLocation.column, onChange: function (e) {
setCellLocation(function (l) { return (__assign(__assign({}, l), { column: e.target.valueAsNumber || 0 })); });
} }), _jsx(Input, { placeholder: "".concat(cellLocation.row, " rows"), type: "number", value: cellLocation.row, onChange: function (e) {
setCellLocation(function (l) { return (__assign(__assign({}, l), { row: e.target.valueAsNumber || 0 })); });
} })] })), _jsxs(Text, __assign({ variant: "body", sx: { mt: 1, display: ["none", "block"] } }, { children: [cellLocation.column, "x", cellLocation.row] }))] })) })));
} }
function getCellLocation(index, tableSize) { function getCellLocation(index, tableSize) {
var cellIndex = index + 1; var cellIndex = index + 1;

View File

@@ -0,0 +1,11 @@
export declare type ToolbarLocation = "top" | "bottom";
interface ToolbarState {
isMobile: boolean;
setIsMobile: (isMobile: boolean) => void;
toolbarLocation: ToolbarLocation;
setToolbarLocation: (location: ToolbarLocation) => void;
}
export declare const useToolbarStore: import("zustand").UseBoundStore<ToolbarState, import("zustand").StoreApi<ToolbarState>>;
export declare function useToolbarLocation(): ToolbarLocation;
export declare function useIsMobile(): boolean;
export {};

View File

@@ -0,0 +1,21 @@
import create from "zustand";
export var useToolbarStore = create(function (set) { return ({
isMobile: false,
setIsMobile: function (isMobile) {
return set(function (state) {
state.isMobile = isMobile;
});
},
toolbarLocation: "top",
setToolbarLocation: function (location) {
return set(function (state) {
state.toolbarLocation = location;
});
},
}); });
export function useToolbarLocation() {
return useToolbarStore(function (store) { return store.toolbarLocation; });
}
export function useIsMobile() {
return useToolbarStore(function (store) { return store.isMobile; });
}

View File

@@ -1,10 +1,11 @@
/// <reference types="react" /> /// <reference types="react" />
import { ThemeConfig } from "@notesnook/theme/dist/theme/types"; import { ThemeConfig } from "@notesnook/theme/dist/theme/types";
import { Editor } from "@tiptap/core"; import { Editor } from "@tiptap/core";
import { ToolbarLocation } from "./hooks/useToolbarContext"; import { ToolbarLocation } from "./stores/toolbar-store";
declare type ToolbarProps = ThemeConfig & { declare type ToolbarProps = ThemeConfig & {
editor: Editor | null; editor: Editor | null;
location: ToolbarLocation; location: ToolbarLocation;
isMobile?: boolean;
}; };
export declare function Toolbar(props: ToolbarProps): JSX.Element | null; export declare function Toolbar(props: ToolbarProps): JSX.Element | null;
export {}; export {};

View File

@@ -44,14 +44,20 @@ import { ThemeProvider } from "emotion-theming";
import { EditorFloatingMenus } from "./floating-menus"; import { EditorFloatingMenus } from "./floating-menus";
import { getToolDefinition } from "./tool-definitions"; import { getToolDefinition } from "./tool-definitions";
import { ToolButton } from "./components/tool-button"; import { ToolButton } from "./components/tool-button";
import { useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { MenuPresenter } from "../components/menu"; import { MenuPresenter } from "../components/menu";
import { Popup } from "./components/popup"; import { Popup } from "./components/popup";
import { ToolbarContext, useToolbarContext, } from "./hooks/useToolbarContext"; import { ToolbarContext, useToolbarContext } from "./hooks/useToolbarContext";
import { useToolbarLocation, useToolbarStore, } from "./stores/toolbar-store";
export function Toolbar(props) { export function Toolbar(props) {
var editor = props.editor, theme = props.theme, accent = props.accent, scale = props.scale, location = props.location; var editor = props.editor, theme = props.theme, accent = props.accent, scale = props.scale, location = props.location, isMobile = props.isMobile;
var themeProperties = useTheme({ accent: accent, theme: theme, scale: scale }); var themeProperties = useTheme({ accent: accent, theme: theme, scale: scale });
var _a = __read(useState(), 2), currentPopup = _a[0], setCurrentPopup = _a[1]; var _a = __read(useState(), 2), currentPopup = _a[0], setCurrentPopup = _a[1];
var _b = useToolbarStore(), setIsMobile = _b.setIsMobile, setToolbarLocation = _b.setToolbarLocation;
useEffect(function () {
setIsMobile(isMobile || false);
setToolbarLocation(location);
}, [isMobile, location]);
var tools = [ var tools = [
["insertBlock"], ["insertBlock"],
[ [
@@ -75,7 +81,10 @@ export function Toolbar(props) {
]; ];
if (!editor) if (!editor)
return null; return null;
return (_jsxs(ThemeProvider, __assign({ theme: themeProperties }, { children: [_jsx(ToolbarContext.Provider, __assign({ value: { setCurrentPopup: setCurrentPopup, currentPopup: currentPopup, toolbarLocation: location } }, { children: _jsx(Flex, __assign({ className: "editor-toolbar", sx: { flexWrap: ["nowrap", "wrap"], overflowX: ["auto", "hidden"] } }, { children: tools.map(function (tools) { return (_jsxs(ThemeProvider, __assign({ theme: themeProperties }, { children: [_jsx(ToolbarContext.Provider, __assign({ value: {
setCurrentPopup: setCurrentPopup,
currentPopup: currentPopup,
} }, { children: _jsx(Flex, __assign({ className: "editor-toolbar", sx: { flexWrap: ["nowrap", "wrap"], overflowX: ["auto", "hidden"] } }, { children: tools.map(function (tools) {
return (_jsx(ToolbarGroup, { tools: tools, editor: editor, sx: { return (_jsx(ToolbarGroup, { tools: tools, editor: editor, sx: {
flexShrink: 0, flexShrink: 0,
pr: 2, pr: 2,
@@ -94,13 +103,14 @@ function ToolbarGroup(props) {
else { else {
var Component = findToolById(toolId); var Component = findToolById(toolId);
var toolDefinition = getToolDefinition(toolId); var toolDefinition = getToolDefinition(toolId);
return _jsx(Component, __assign({ editor: editor, id: toolId }, toolDefinition)); return _jsx(Component, __assign({ editor: editor }, toolDefinition));
} }
}) }))); }) })));
} }
function MoreTools(props) { function MoreTools(props) {
var popupId = props.popupId; var popupId = props.popupId;
var _a = useToolbarContext(), currentPopup = _a.currentPopup, setCurrentPopup = _a.setCurrentPopup, toolbarLocation = _a.toolbarLocation; var _a = useToolbarContext(), currentPopup = _a.currentPopup, setCurrentPopup = _a.setCurrentPopup;
var toolbarLocation = useToolbarLocation();
var buttonRef = useRef(); var buttonRef = useRef();
var show = popupId === currentPopup; var show = popupId === currentPopup;
var setShow = function (state) { var setShow = function (state) {

View File

@@ -27,16 +27,17 @@ var __read = (this && this.__read) || function (o, n) {
}; };
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime"; import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
import { Icons } from "../icons"; import { Icons } from "../icons";
import { MenuPresenter } from "../../components/menu/menu"; import { ActionSheetPresenter, } from "../../components/menu/menu";
import { useRef, useState } from "react"; import { useRef, useState } from "react";
import { Icon } from "../components/icon"; import { Icon } from "../components/icon";
import { Button } from "rebass"; import { Button } from "rebass";
import { EmbedPopup } from "../popups/embed-popup";
import { TablePopup } from "../popups/table-popup"; import { TablePopup } from "../popups/table-popup";
import { useToolbarContext } from "../hooks/useToolbarContext"; import { useToolbarLocation } from "../stores/toolbar-store";
export function InsertBlock(props) { export function InsertBlock(props) {
var buttonRef = useRef(); var buttonRef = useRef();
var _a = __read(useState(false), 2), isOpen = _a[0], setIsOpen = _a[1]; var _a = __read(useState(false), 2), isOpen = _a[0], setIsOpen = _a[1];
var toolbarLocation = useToolbarContext().toolbarLocation; var toolbarLocation = useToolbarLocation();
return (_jsxs(_Fragment, { children: [_jsx(Button, __assign({ ref: buttonRef, sx: { return (_jsxs(_Fragment, { children: [_jsx(Button, __assign({ ref: buttonRef, sx: {
p: 1, p: 1,
m: 0, m: 0,
@@ -48,23 +49,15 @@ export function InsertBlock(props) {
":last-of-type": { ":last-of-type": {
mr: 0, mr: 0,
}, },
}, onMouseDown: function (e) { return e.preventDefault(); }, onClick: function () { return setIsOpen(function (s) { return !s; }); } }, { children: _jsx(Icon, { path: Icons.plus, size: 18, color: "primary" }) })), _jsx(MenuPresenter, { options: { }, onMouseDown: function (e) { return e.preventDefault(); }, onClick: function () { return setIsOpen(function (s) { return !s; }); } }, { children: _jsx(Icon, { path: Icons.plus, size: 18, color: "primary" }) })), _jsx(ActionSheetPresenter, { title: "Choose a block to insert", isOpen: isOpen, items: [
type: "menu",
position: {
target: buttonRef.current || undefined,
isTargetAbsolute: true,
location: toolbarLocation === "bottom" ? "top" : "below",
yOffset: 5,
},
}, isOpen: isOpen, items: [
tasklist(editor), tasklist(editor),
horizontalRule(editor), horizontalRule(editor),
codeblock(editor), codeblock(editor),
blockquote(editor), blockquote(editor),
image(editor), imageActionSheet(editor),
attachment(editor), attachment(editor),
embed(editor), embedActionSheet(editor),
table(editor), tableActionSheet(editor),
], onClose: function () { return setIsOpen(false); } })] })); ], onClose: function () { return setIsOpen(false); } })] }));
} }
var horizontalRule = function (editor) { return ({ var horizontalRule = function (editor) { return ({
@@ -113,6 +106,38 @@ var image = function (editor) { return ({
}, },
], ],
}); }; }); };
var imageActionSheet = function (editor) { return ({
key: "image",
type: "menuitem",
title: "Image",
icon: "image",
items: [
{
key: "imageOptions",
type: "menuitem",
component: function (_a) {
var onClick = _a.onClick;
var _b = __read(useState(true), 2), isOpen = _b[0], setIsOpen = _b[1];
return (_jsx(ActionSheetPresenter, { isOpen: isOpen, onClose: function () { return setIsOpen(false); }, items: [
{
key: "upload-from-disk",
type: "menuitem",
title: "Upload from disk",
icon: "upload",
onClick: function () { },
},
{
key: "upload-from-url",
type: "menuitem",
title: "Attach from URL",
icon: "link",
onClick: function () { },
},
] }));
},
},
],
}); };
var embed = function (editor) { return ({ var embed = function (editor) { return ({
key: "embed", key: "embed",
type: "menuitem", type: "menuitem",
@@ -128,7 +153,7 @@ var table = function (editor) { return ({
{ {
key: "table-size-selector", key: "table-size-selector",
type: "menuitem", type: "menuitem",
component: function (props) { return (_jsx(TablePopup, { onClose: function (size) { component: function (props) { return (_jsx(TablePopup, { onInsertTable: function (size) {
var _a; var _a;
editor === null || editor === void 0 ? void 0 : editor.chain().focus().insertTable({ editor === null || editor === void 0 ? void 0 : editor.chain().focus().insertTable({
rows: size.rows, rows: size.rows,
@@ -139,6 +164,51 @@ var table = function (editor) { return ({
}, },
], ],
}); }; }); };
var embedActionSheet = function (editor) { return ({
key: "embed",
type: "menuitem",
title: "Embed",
icon: "embed",
items: [
{
key: "table-size-selector",
type: "menuitem",
component: function (_a) {
var onClick = _a.onClick;
var _b = __read(useState(true), 2), isOpen = _b[0], setIsOpen = _b[1];
return (_jsx(ActionSheetPresenter, __assign({ isOpen: isOpen, onClose: function () { return setIsOpen(false); }, items: [] }, { children: _jsx(EmbedPopup, { title: "Insert embed", icon: "check", onClose: function (embed) {
editor === null || editor === void 0 ? void 0 : editor.chain().insertEmbed(embed).run();
setIsOpen(false);
onClick === null || onClick === void 0 ? void 0 : onClick();
} }) })));
},
},
],
}); };
var tableActionSheet = function (editor) { return ({
key: "table",
type: "menuitem",
title: "Table",
icon: "table",
items: [
{
key: "table-size-selector",
type: "menuitem",
component: function (_a) {
var onClick = _a.onClick;
var _b = __read(useState(true), 2), isOpen = _b[0], setIsOpen = _b[1];
return (_jsx(ActionSheetPresenter, __assign({ isOpen: isOpen, onClose: function () { return setIsOpen(false); }, items: [] }, { children: _jsx(TablePopup, { cellSize: 30, autoExpand: false, onInsertTable: function (size) {
editor === null || editor === void 0 ? void 0 : editor.chain().focus().insertTable({
rows: size.rows,
cols: size.columns,
}).run();
setIsOpen(false);
onClick === null || onClick === void 0 ? void 0 : onClick();
} }) })));
},
},
],
}); };
var attachment = function (editor) { return ({ var attachment = function (editor) { return ({
key: "attachment", key: "attachment",
type: "menuitem", type: "menuitem",

View File

@@ -1,3 +1,14 @@
var __assign = (this && this.__assign) || function () {
__assign = Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
var __read = (this && this.__read) || function (o, n) { var __read = (this && this.__read) || function (o, n) {
var m = typeof Symbol === "function" && o[Symbol.iterator]; var m = typeof Symbol === "function" && o[Symbol.iterator];
if (!m) return o; if (!m) return o;
@@ -14,23 +25,32 @@ var __read = (this && this.__read) || function (o, n) {
} }
return ar; return ar;
}; };
import { jsx as _jsx } from "react/jsx-runtime"; import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
import { Dropdown } from "../components/dropdown"; import { Dropdown } from "../components/dropdown";
import { Box, Button } from "rebass";
var defaultFontSizes = [ var defaultFontSizes = [
12, 14, 16, 18, 20, 24, 28, 32, 36, 42, 48, 60, 72, 100, 8, 12, 14, 16, 18, 20, 24, 28, 32, 36, 42, 48, 60, 72, 100,
]; ];
export function FontSize(props) { export function FontSize(props) {
var editor = props.editor; var editor = props.editor;
var currentFontSize = defaultFontSizes.find(function (size) { var currentFontSize = defaultFontSizes.find(function (size) {
return editor.isActive("textStyle", { fontSize: "".concat(size, "px") }); return editor.isActive("textStyle", { fontSize: "".concat(size, "px") });
}) || 16; }) || 16;
return (_jsx(Dropdown, { selectedItem: "".concat(currentFontSize, "px"), items: defaultFontSizes.map(function (size) { return ({ return (_jsx(Dropdown, { selectedItem: "".concat(currentFontSize, "px"), items: [
key: "".concat(size, "px"), {
type: "menuitem", key: "font-sizes",
title: "".concat(size, "px"), type: "menuitem",
isChecked: size === currentFontSize, component: function () { return (_jsx(Box, __assign({ sx: { display: "grid", gridTemplateColumns: "repeat(5, 1fr)" } }, { children: defaultFontSizes.map(function (size) { return (_jsxs(Button, __assign({ variant: "menuitem" }, { children: [size, "px"] }))); }) }))); },
onClick: function () { return editor.chain().focus().setFontSize("".concat(size, "px")).run(); }, },
}); }), menuWidth: 100 })); ],
// items={defaultFontSizes.map((size) => ({
// key: `${size}px`,
// type: "menuitem",
// title: `${size}px`,
// isChecked: size === currentFontSize,
// onClick: () => editor.chain().focus().setFontSize(`${size}px`).run(),
// }))}
menuWidth: 100 }));
} }
var fontFamilies = { var fontFamilies = {
System: "Open Sans", System: "Open Sans",

View File

@@ -33,8 +33,8 @@ import { Flex } from "rebass";
import { Input } from "@rebass/forms"; import { Input } from "@rebass/forms";
import { Popup } from "../components/popup"; import { Popup } from "../components/popup";
function InlineTool(props) { function InlineTool(props) {
var editor = props.editor, title = props.title, id = props.id, icon = props.icon, isToggled = props.isToggled, onClick = props.onClick; var editor = props.editor, title = props.title, icon = props.icon, isToggled = props.isToggled, onClick = props.onClick;
return (_jsx(ToolButton, { title: title, id: id, icon: icon, onClick: function () { return onClick(editor); }, toggled: isToggled(editor) })); return (_jsx(ToolButton, { title: title, id: icon, icon: icon, onClick: function () { return onClick(editor); }, toggled: isToggled(editor) }));
} }
export function Italic(props) { export function Italic(props) {
return (_jsx(InlineTool, __assign({}, props, { isToggled: function (editor) { return editor.isActive("italic"); }, onClick: function (editor) { return editor.chain().focus().toggleItalic().run(); } }))); return (_jsx(InlineTool, __assign({}, props, { isToggled: function (editor) { return editor.isActive("italic"); }, onClick: function (editor) { return editor.chain().focus().toggleItalic().run(); } })));
@@ -63,7 +63,7 @@ export function ClearFormatting(props) {
} }))); } })));
} }
export function Link(props) { export function Link(props) {
var editor = props.editor, id = props.id, title = props.title, icon = props.icon; var editor = props.editor, title = props.title, icon = props.icon;
var buttonRef = useRef(null); var buttonRef = useRef(null);
var targetRef = useRef(); var targetRef = useRef();
var _a = __read(useState(false), 2), isOpen = _a[0], setIsOpen = _a[1]; var _a = __read(useState(false), 2), isOpen = _a[0], setIsOpen = _a[1];
@@ -71,7 +71,7 @@ export function Link(props) {
var _c = __read(useState(), 2), text = _c[0], setText = _c[1]; var _c = __read(useState(), 2), text = _c[0], setText = _c[1];
var currentUrl = editor.getAttributes("link").href; var currentUrl = editor.getAttributes("link").href;
var isEditing = !!currentUrl; var isEditing = !!currentUrl;
return (_jsxs(_Fragment, { children: [_jsx(ToolButton, { ref: buttonRef, title: title, id: id, icon: icon, onClick: function () { return (_jsxs(_Fragment, { children: [_jsx(ToolButton, { id: icon, ref: buttonRef, title: title, icon: icon, onClick: function () {
if (isEditing) if (isEditing)
setHref(currentUrl); setHref(currentUrl);
var _a = editor.state.selection, from = _a.from, to = _a.to, $from = _a.$from; var _a = editor.state.selection, from = _a.from, to = _a.to, $from = _a.$from;

View File

@@ -1,9 +1,7 @@
import { Editor } from "@tiptap/core"; import { Editor } from "@tiptap/core";
import { IconNames } from "./icons"; import { IconNames } from "./icons";
import { ToolId } from "./tools";
export declare type ToolProps = ToolDefinition & { export declare type ToolProps = ToolDefinition & {
editor: Editor; editor: Editor;
id: ToolId;
}; };
export declare type ToolDefinition = { export declare type ToolDefinition = {
icon: IconNames; icon: IconNames;

View File

@@ -0,0 +1 @@
export declare function getToolbarElement(): HTMLElement;

View File

@@ -0,0 +1,3 @@
export function getToolbarElement() {
return (document.querySelector(".editor-toolbar") || undefined);
}

View File

@@ -41,6 +41,7 @@
"re-resizable": "^6.9.5", "re-resizable": "^6.9.5",
"react-color": "^2.19.3", "react-color": "^2.19.3",
"react-modal": "^3.14.4", "react-modal": "^3.14.4",
"react-spring-bottom-sheet": "^3.4.0",
"react-toggle": "^4.1.2", "react-toggle": "^4.1.2",
"reactjs-popup": "^2.0.5", "reactjs-popup": "^2.0.5",
"rebass": "^4.0.7", "rebass": "^4.0.7",
@@ -3081,6 +3082,11 @@
"@jridgewell/sourcemap-codec": "^1.4.10" "@jridgewell/sourcemap-codec": "^1.4.10"
} }
}, },
"node_modules/@juggle/resize-observer": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.3.1.tgz",
"integrity": "sha512-zMM9Ds+SawiUkakS7y94Ymqx+S0ORzpG3frZirN3l+UlXUmSUR7hF4wxCVqW+ei94JzV5kt0uXBcoOEAuiydrw=="
},
"node_modules/@mdi/js": { "node_modules/@mdi/js": {
"version": "6.6.96", "version": "6.6.96",
"resolved": "https://registry.npmjs.org/@mdi/js/-/js-6.6.96.tgz", "resolved": "https://registry.npmjs.org/@mdi/js/-/js-6.6.96.tgz",
@@ -3245,6 +3251,33 @@
"url": "https://opencollective.com/popperjs" "url": "https://opencollective.com/popperjs"
} }
}, },
"node_modules/@reach/portal": {
"version": "0.13.2",
"resolved": "https://registry.npmjs.org/@reach/portal/-/portal-0.13.2.tgz",
"integrity": "sha512-g74BnCdtuTGthzzHn2cWW+bcyIYb0iIE/yRsm89i8oNzNgpopbkh9UY8TPbhNlys52h7U60s4kpRTmcq+JqsTA==",
"dependencies": {
"@reach/utils": "0.13.2",
"tslib": "^2.1.0"
},
"peerDependencies": {
"react": "^16.8.0 || 17.x",
"react-dom": "^16.8.0 || 17.x"
}
},
"node_modules/@reach/utils": {
"version": "0.13.2",
"resolved": "https://registry.npmjs.org/@reach/utils/-/utils-0.13.2.tgz",
"integrity": "sha512-3ir6cN60zvUrwjOJu7C6jec/samqAeyAB12ZADK+qjnmQPdzSYldrFWwDVV5H0WkhbYXR3uh+eImu13hCetNPQ==",
"dependencies": {
"@types/warning": "^3.0.0",
"tslib": "^2.1.0",
"warning": "^4.0.3"
},
"peerDependencies": {
"react": "^16.8.0 || 17.x",
"react-dom": "^16.8.0 || 17.x"
}
},
"node_modules/@rebass/forms": { "node_modules/@rebass/forms": {
"version": "4.0.6", "version": "4.0.6",
"resolved": "https://registry.npmjs.org/@rebass/forms/-/forms-4.0.6.tgz", "resolved": "https://registry.npmjs.org/@rebass/forms/-/forms-4.0.6.tgz",
@@ -4703,6 +4736,11 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/@types/warning": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.0.tgz",
"integrity": "sha1-DSUBJorY+ZYrdA04fEZU9fjiPlI="
},
"node_modules/@types/webpack": { "node_modules/@types/webpack": {
"version": "4.41.32", "version": "4.41.32",
"resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.32.tgz", "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.32.tgz",
@@ -5123,6 +5161,28 @@
"@xtuc/long": "4.2.2" "@xtuc/long": "4.2.2"
} }
}, },
"node_modules/@xstate/react": {
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/@xstate/react/-/react-1.6.3.tgz",
"integrity": "sha512-NCUReRHPGvvCvj2yLZUTfR0qVp6+apc8G83oXSjN4rl89ZjyujiKrTff55bze/HrsvCsP/sUJASf2n0nzMF1KQ==",
"dependencies": {
"use-isomorphic-layout-effect": "^1.0.0",
"use-subscription": "^1.3.0"
},
"peerDependencies": {
"@xstate/fsm": "^1.0.0",
"react": "^16.8.0 || ^17.0.0",
"xstate": "^4.11.0"
},
"peerDependenciesMeta": {
"@xstate/fsm": {
"optional": true
},
"xstate": {
"optional": true
}
}
},
"node_modules/@xtuc/ieee754": { "node_modules/@xtuc/ieee754": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
@@ -6333,6 +6393,11 @@
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
"dev": true "dev": true
}, },
"node_modules/body-scroll-lock": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/body-scroll-lock/-/body-scroll-lock-3.1.5.tgz",
"integrity": "sha512-Yi1Xaml0EvNA0OYWxXiYNqY24AfWkbA6w5vxE7GWxtKfzIbZM+Qw+aSmkgsbWzbHiy/RCSkUZBplVxTA+E4jJg=="
},
"node_modules/bonjour": { "node_modules/bonjour": {
"version": "3.5.0", "version": "3.5.0",
"resolved": "https://registry.npmjs.org/bonjour/-/bonjour-3.5.0.tgz", "resolved": "https://registry.npmjs.org/bonjour/-/bonjour-3.5.0.tgz",
@@ -10562,6 +10627,14 @@
"readable-stream": "^2.3.6" "readable-stream": "^2.3.6"
} }
}, },
"node_modules/focus-trap": {
"version": "6.9.2",
"resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-6.9.2.tgz",
"integrity": "sha512-gBEuXOPNOKPrLdZpMFUSTyIo1eT2NSZRrwZ9r/0Jqw5tmT3Yvxfmu8KBHw8xW2XQkw6E/JoG+OlEq7UDtSUNgw==",
"dependencies": {
"tabbable": "^5.3.2"
}
},
"node_modules/follow-redirects": { "node_modules/follow-redirects": {
"version": "1.14.9", "version": "1.14.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz",
@@ -18891,6 +18964,37 @@
"semver": "bin/semver" "semver": "bin/semver"
} }
}, },
"node_modules/react-spring": {
"version": "8.0.27",
"resolved": "https://registry.npmjs.org/react-spring/-/react-spring-8.0.27.tgz",
"integrity": "sha512-nDpWBe3ZVezukNRandTeLSPcwwTMjNVu1IDq9qA/AMiUqHuRN4BeSWvKr3eIxxg1vtiYiOLy4FqdfCP5IoP77g==",
"dependencies": {
"@babel/runtime": "^7.3.1",
"prop-types": "^15.5.8"
},
"peerDependencies": {
"react": ">= 16.8.0",
"react-dom": ">= 16.8.0"
}
},
"node_modules/react-spring-bottom-sheet": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/react-spring-bottom-sheet/-/react-spring-bottom-sheet-3.4.0.tgz",
"integrity": "sha512-zKwTymxrTRMHPjfBiMw8reQlWoVqlCGMTefmMYkAlBvR7n3hBe5sntuQJAEjmrAnA+cLSGp44mtmgBtT2ksL5Q==",
"dependencies": {
"@juggle/resize-observer": "^3.2.0",
"@reach/portal": "^0.13.0",
"@xstate/react": "^1.2.0",
"body-scroll-lock": "^3.1.5",
"focus-trap": "^6.2.2",
"react-spring": "^8.0.27",
"react-use-gesture": "^8.0.1",
"xstate": "^4.15.1"
},
"peerDependencies": {
"react": "^16.14.0 || 17"
}
},
"node_modules/react-toggle": { "node_modules/react-toggle": {
"version": "4.1.2", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/react-toggle/-/react-toggle-4.1.2.tgz", "resolved": "https://registry.npmjs.org/react-toggle/-/react-toggle-4.1.2.tgz",
@@ -18904,6 +19008,15 @@
"react-dom": ">= 15.3.0 < 18" "react-dom": ">= 15.3.0 < 18"
} }
}, },
"node_modules/react-use-gesture": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/react-use-gesture/-/react-use-gesture-8.0.1.tgz",
"integrity": "sha512-CXzUNkulUdgouaAlvAsC5ZVo0fi9KGSBSk81WrE4kOIcJccpANe9zZkAYr5YZZhqpicIFxitsrGVS4wmoMun9A==",
"deprecated": "This package is no longer maintained. Please use @use-gesture/react instead",
"peerDependencies": {
"react": ">= 16.8.0"
}
},
"node_modules/reactcss": { "node_modules/reactcss": {
"version": "1.2.3", "version": "1.2.3",
"resolved": "https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz", "resolved": "https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz",
@@ -21553,6 +21666,11 @@
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
"dev": true "dev": true
}, },
"node_modules/tabbable": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-5.3.2.tgz",
"integrity": "sha512-6G/8EWRFx8CiSe2++/xHhXkmCRq2rHtDtZbQFHx34cvDfZzIBfvwG9zGUNTWMXWLCYvDj3aQqOzdl3oCxKuBkQ=="
},
"node_modules/table": { "node_modules/table": {
"version": "6.8.0", "version": "6.8.0",
"resolved": "https://registry.npmjs.org/table/-/table-6.8.0.tgz", "resolved": "https://registry.npmjs.org/table/-/table-6.8.0.tgz",
@@ -22158,8 +22276,7 @@
"node_modules/tslib": { "node_modules/tslib": {
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==", "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
"dev": true
}, },
"node_modules/tsutils": { "node_modules/tsutils": {
"version": "3.21.0", "version": "3.21.0",
@@ -22675,6 +22792,38 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/use-isomorphic-layout-effect": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz",
"integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/use-subscription": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/use-subscription/-/use-subscription-1.7.0.tgz",
"integrity": "sha512-87x6MjiIVE/BWqtxfiRvM6jfvGudN+UeVOnWi7qKYp2c0YJn5+Z5Jt0kZw6Tt+8hs7kw/BWo2WBhizJSAZsQJA==",
"dependencies": {
"use-sync-external-store": "^1.1.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/use-sync-external-store": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.1.0.tgz",
"integrity": "sha512-SEnieB2FPKEVne66NpXPd1Np4R1lTNKfjuy3XdIoPQKYBAFdzbzSZlSn1KJZUiihQLQC5Znot4SBz1EOTBwQAQ==",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/util": { "node_modules/util": {
"version": "0.11.1", "version": "0.11.1",
"resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz",
@@ -24723,6 +24872,15 @@
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
"dev": true "dev": true
}, },
"node_modules/xstate": {
"version": "4.32.0",
"resolved": "https://registry.npmjs.org/xstate/-/xstate-4.32.0.tgz",
"integrity": "sha512-62gETqwnw4pBRe+tVWMt8hLgWEU8lq2qO8VN5PWmTELceRVt3I1bu1cwdraVRHUn4Bb2lnhNzn1A73oShuC+8g==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/xstate"
}
},
"node_modules/xtend": { "node_modules/xtend": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
@@ -27036,6 +27194,11 @@
"@jridgewell/sourcemap-codec": "^1.4.10" "@jridgewell/sourcemap-codec": "^1.4.10"
} }
}, },
"@juggle/resize-observer": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.3.1.tgz",
"integrity": "sha512-zMM9Ds+SawiUkakS7y94Ymqx+S0ORzpG3frZirN3l+UlXUmSUR7hF4wxCVqW+ei94JzV5kt0uXBcoOEAuiydrw=="
},
"@mdi/js": { "@mdi/js": {
"version": "6.6.96", "version": "6.6.96",
"resolved": "https://registry.npmjs.org/@mdi/js/-/js-6.6.96.tgz", "resolved": "https://registry.npmjs.org/@mdi/js/-/js-6.6.96.tgz",
@@ -27160,6 +27323,25 @@
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.5.tgz", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.5.tgz",
"integrity": "sha512-9X2obfABZuDVLCgPK9aX0a/x4jaOEweTTWE2+9sr0Qqqevj2Uv5XorvusThmc9XGYpS9yI+fhh8RTafBtGposw==" "integrity": "sha512-9X2obfABZuDVLCgPK9aX0a/x4jaOEweTTWE2+9sr0Qqqevj2Uv5XorvusThmc9XGYpS9yI+fhh8RTafBtGposw=="
}, },
"@reach/portal": {
"version": "0.13.2",
"resolved": "https://registry.npmjs.org/@reach/portal/-/portal-0.13.2.tgz",
"integrity": "sha512-g74BnCdtuTGthzzHn2cWW+bcyIYb0iIE/yRsm89i8oNzNgpopbkh9UY8TPbhNlys52h7U60s4kpRTmcq+JqsTA==",
"requires": {
"@reach/utils": "0.13.2",
"tslib": "^2.1.0"
}
},
"@reach/utils": {
"version": "0.13.2",
"resolved": "https://registry.npmjs.org/@reach/utils/-/utils-0.13.2.tgz",
"integrity": "sha512-3ir6cN60zvUrwjOJu7C6jec/samqAeyAB12ZADK+qjnmQPdzSYldrFWwDVV5H0WkhbYXR3uh+eImu13hCetNPQ==",
"requires": {
"@types/warning": "^3.0.0",
"tslib": "^2.1.0",
"warning": "^4.0.3"
}
},
"@rebass/forms": { "@rebass/forms": {
"version": "4.0.6", "version": "4.0.6",
"resolved": "https://registry.npmjs.org/@rebass/forms/-/forms-4.0.6.tgz", "resolved": "https://registry.npmjs.org/@rebass/forms/-/forms-4.0.6.tgz",
@@ -28246,6 +28428,11 @@
} }
} }
}, },
"@types/warning": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.0.tgz",
"integrity": "sha1-DSUBJorY+ZYrdA04fEZU9fjiPlI="
},
"@types/webpack": { "@types/webpack": {
"version": "4.41.32", "version": "4.41.32",
"resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.32.tgz", "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.32.tgz",
@@ -28582,6 +28769,15 @@
"@xtuc/long": "4.2.2" "@xtuc/long": "4.2.2"
} }
}, },
"@xstate/react": {
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/@xstate/react/-/react-1.6.3.tgz",
"integrity": "sha512-NCUReRHPGvvCvj2yLZUTfR0qVp6+apc8G83oXSjN4rl89ZjyujiKrTff55bze/HrsvCsP/sUJASf2n0nzMF1KQ==",
"requires": {
"use-isomorphic-layout-effect": "^1.0.0",
"use-subscription": "^1.3.0"
}
},
"@xtuc/ieee754": { "@xtuc/ieee754": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
@@ -29556,6 +29752,11 @@
} }
} }
}, },
"body-scroll-lock": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/body-scroll-lock/-/body-scroll-lock-3.1.5.tgz",
"integrity": "sha512-Yi1Xaml0EvNA0OYWxXiYNqY24AfWkbA6w5vxE7GWxtKfzIbZM+Qw+aSmkgsbWzbHiy/RCSkUZBplVxTA+E4jJg=="
},
"bonjour": { "bonjour": {
"version": "3.5.0", "version": "3.5.0",
"resolved": "https://registry.npmjs.org/bonjour/-/bonjour-3.5.0.tgz", "resolved": "https://registry.npmjs.org/bonjour/-/bonjour-3.5.0.tgz",
@@ -32882,6 +33083,14 @@
"readable-stream": "^2.3.6" "readable-stream": "^2.3.6"
} }
}, },
"focus-trap": {
"version": "6.9.2",
"resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-6.9.2.tgz",
"integrity": "sha512-gBEuXOPNOKPrLdZpMFUSTyIo1eT2NSZRrwZ9r/0Jqw5tmT3Yvxfmu8KBHw8xW2XQkw6E/JoG+OlEq7UDtSUNgw==",
"requires": {
"tabbable": "^5.3.2"
}
},
"follow-redirects": { "follow-redirects": {
"version": "1.14.9", "version": "1.14.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz",
@@ -39463,6 +39672,30 @@
} }
} }
}, },
"react-spring": {
"version": "8.0.27",
"resolved": "https://registry.npmjs.org/react-spring/-/react-spring-8.0.27.tgz",
"integrity": "sha512-nDpWBe3ZVezukNRandTeLSPcwwTMjNVu1IDq9qA/AMiUqHuRN4BeSWvKr3eIxxg1vtiYiOLy4FqdfCP5IoP77g==",
"requires": {
"@babel/runtime": "^7.3.1",
"prop-types": "^15.5.8"
}
},
"react-spring-bottom-sheet": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/react-spring-bottom-sheet/-/react-spring-bottom-sheet-3.4.0.tgz",
"integrity": "sha512-zKwTymxrTRMHPjfBiMw8reQlWoVqlCGMTefmMYkAlBvR7n3hBe5sntuQJAEjmrAnA+cLSGp44mtmgBtT2ksL5Q==",
"requires": {
"@juggle/resize-observer": "^3.2.0",
"@reach/portal": "^0.13.0",
"@xstate/react": "^1.2.0",
"body-scroll-lock": "^3.1.5",
"focus-trap": "^6.2.2",
"react-spring": "^8.0.27",
"react-use-gesture": "^8.0.1",
"xstate": "^4.15.1"
}
},
"react-toggle": { "react-toggle": {
"version": "4.1.2", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/react-toggle/-/react-toggle-4.1.2.tgz", "resolved": "https://registry.npmjs.org/react-toggle/-/react-toggle-4.1.2.tgz",
@@ -39471,6 +39704,12 @@
"classnames": "^2.2.5" "classnames": "^2.2.5"
} }
}, },
"react-use-gesture": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/react-use-gesture/-/react-use-gesture-8.0.1.tgz",
"integrity": "sha512-CXzUNkulUdgouaAlvAsC5ZVo0fi9KGSBSk81WrE4kOIcJccpANe9zZkAYr5YZZhqpicIFxitsrGVS4wmoMun9A==",
"requires": {}
},
"reactcss": { "reactcss": {
"version": "1.2.3", "version": "1.2.3",
"resolved": "https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz", "resolved": "https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz",
@@ -41631,6 +41870,11 @@
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
"dev": true "dev": true
}, },
"tabbable": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-5.3.2.tgz",
"integrity": "sha512-6G/8EWRFx8CiSe2++/xHhXkmCRq2rHtDtZbQFHx34cvDfZzIBfvwG9zGUNTWMXWLCYvDj3aQqOzdl3oCxKuBkQ=="
},
"table": { "table": {
"version": "6.8.0", "version": "6.8.0",
"resolved": "https://registry.npmjs.org/table/-/table-6.8.0.tgz", "resolved": "https://registry.npmjs.org/table/-/table-6.8.0.tgz",
@@ -42093,8 +42337,7 @@
"tslib": { "tslib": {
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==", "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
"dev": true
}, },
"tsutils": { "tsutils": {
"version": "3.21.0", "version": "3.21.0",
@@ -42476,6 +42719,26 @@
"integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==",
"dev": true "dev": true
}, },
"use-isomorphic-layout-effect": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz",
"integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==",
"requires": {}
},
"use-subscription": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/use-subscription/-/use-subscription-1.7.0.tgz",
"integrity": "sha512-87x6MjiIVE/BWqtxfiRvM6jfvGudN+UeVOnWi7qKYp2c0YJn5+Z5Jt0kZw6Tt+8hs7kw/BWo2WBhizJSAZsQJA==",
"requires": {
"use-sync-external-store": "^1.1.0"
}
},
"use-sync-external-store": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.1.0.tgz",
"integrity": "sha512-SEnieB2FPKEVne66NpXPd1Np4R1lTNKfjuy3XdIoPQKYBAFdzbzSZlSn1KJZUiihQLQC5Znot4SBz1EOTBwQAQ==",
"requires": {}
},
"util": { "util": {
"version": "0.11.1", "version": "0.11.1",
"resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz",
@@ -44176,6 +44439,11 @@
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
"dev": true "dev": true
}, },
"xstate": {
"version": "4.32.0",
"resolved": "https://registry.npmjs.org/xstate/-/xstate-4.32.0.tgz",
"integrity": "sha512-62gETqwnw4pBRe+tVWMt8hLgWEU8lq2qO8VN5PWmTELceRVt3I1bu1cwdraVRHUn4Bb2lnhNzn1A73oShuC+8g=="
},
"xtend": { "xtend": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",

View File

@@ -37,6 +37,7 @@
"re-resizable": "^6.9.5", "re-resizable": "^6.9.5",
"react-color": "^2.19.3", "react-color": "^2.19.3",
"react-modal": "^3.14.4", "react-modal": "^3.14.4",
"react-spring-bottom-sheet": "^3.4.0",
"react-toggle": "^4.1.2", "react-toggle": "^4.1.2",
"reactjs-popup": "^2.0.5", "reactjs-popup": "^2.0.5",
"rebass": "^4.0.7", "rebass": "^4.0.7",

View File

@@ -14,6 +14,9 @@ import { MenuItem as MenuItemType /*ResolvedMenuItem*/ } from "./types";
// import { useMenuTrigger, useMenu, getPosition } from "../../hooks/useMenu"; // import { useMenuTrigger, useMenu, getPosition } from "../../hooks/useMenu";
import Modal from "react-modal"; import Modal from "react-modal";
import { ThemeProvider } from "emotion-theming"; import { ThemeProvider } from "emotion-theming";
import { BottomSheet } from "react-spring-bottom-sheet";
import "react-spring-bottom-sheet/dist/style.css";
import { useIsMobile } from "../../toolbar/stores/toolbar-store";
// import { store as selectionStore } from "../../stores/selectionstore"; // import { store as selectionStore } from "../../stores/selectionstore";
function useMenuFocus( function useMenuFocus(
@@ -258,20 +261,35 @@ function MenuContainer(props: PropsWithChildren<MenuContainerProps>) {
); );
} }
export type PopupType = "sheet" | "menu" | "none";
export type PopupPresenterProps = MenuPresenterProps &
ActionSheetPresenterProps & {
mobile?: PopupType;
desktop?: PopupType;
};
export function PopupPresenter(props: PropsWithChildren<PopupPresenterProps>) {
const { mobile = "menu", desktop = "menu", ...restProps } = props;
const isMobile = useIsMobile();
if (isMobile && mobile === "sheet")
return <ActionSheetPresenter {...restProps} />;
else if (mobile === "menu" || desktop === "menu")
return <MenuPresenter {...restProps} />;
else return props.isOpen ? <>{props.children}</> : null;
}
export type MenuPresenterProps = MenuContainerProps & { export type MenuPresenterProps = MenuContainerProps & {
items: MenuItemType[]; items?: MenuItemType[];
options: MenuOptions; onClose?: () => void;
isOpen: boolean; isOpen: boolean;
onClose: () => void; options?: MenuOptions;
className?: string;
}; };
export function MenuPresenter(props: PropsWithChildren<MenuPresenterProps>) { export function MenuPresenter(props: PropsWithChildren<MenuPresenterProps>) {
const { const {
className, className,
options, options = { type: "menu", position: {} },
items, items = [],
isOpen, isOpen,
onClose, onClose = () => {},
children, children,
...containerProps ...containerProps
} = props; } = props;
@@ -384,3 +402,43 @@ export function MenuPresenter(props: PropsWithChildren<MenuPresenterProps>) {
</Modal> </Modal>
); );
} }
export type ActionSheetPresenterProps = MenuContainerProps & {
items?: MenuItemType[];
isOpen: boolean;
onClose?: () => void;
blocking?: boolean;
};
export function ActionSheetPresenter(
props: PropsWithChildren<ActionSheetPresenterProps>
) {
const {
items = [],
isOpen,
onClose = () => {},
children,
sx,
blocking = true,
...containerProps
} = props;
return (
<BottomSheet open={isOpen} onDismiss={onClose} blocking={blocking}>
{props.children ? (
props.children
) : (
<Menu
items={items}
closeMenu={onClose}
sx={{
flex: 1,
boxShadow: "none",
border: "none",
...sx,
}}
{...containerProps}
/>
)}
</BottomSheet>
);
}

View File

@@ -0,0 +1,21 @@
import React, { PropsWithChildren } from "react";
import { useToolbarStore } from "../../toolbar/stores/toolbar-store";
type ResponsiveContainerProps = {
mobile?: JSX.Element;
desktop?: JSX.Element;
};
export function ResponsiveContainer(props: ResponsiveContainerProps) {
const isMobile = useToolbarStore((store) => store.isMobile);
if (isMobile) return props.mobile || null;
else return props.desktop || null;
}
export function DesktopOnly(props: PropsWithChildren<{}>) {
return <ResponsiveContainer desktop={<>{props.children}</>} />;
}
export function MobileOnly(props: PropsWithChildren<{}>) {
return <ResponsiveContainer mobile={<>{props.children}</>} />;
}

View File

@@ -1,4 +1,4 @@
import { Box, Flex, Image, ImageProps, Text } from "rebass"; import { Box, Button, Flex, Image, ImageProps, Text } from "rebass";
import { NodeViewWrapper, NodeViewProps, FloatingMenu } from "@tiptap/react"; import { NodeViewWrapper, NodeViewProps, FloatingMenu } from "@tiptap/react";
import { ThemeConfig } from "@notesnook/theme/dist/theme/types"; import { ThemeConfig } from "@notesnook/theme/dist/theme/types";
import { ThemeProvider } from "emotion-theming"; import { ThemeProvider } from "emotion-theming";
@@ -8,7 +8,11 @@ import { ToolButton } from "../../toolbar/components/tool-button";
import { findToolById, ToolId } from "../../toolbar/tools"; import { findToolById, ToolId } from "../../toolbar/tools";
import { Editor } from "@tiptap/core"; import { Editor } from "@tiptap/core";
import { useCallback, useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import { MenuPresenter } from "../../components/menu/menu"; import {
ActionSheetPresenter,
MenuPresenter,
PopupPresenter,
} from "../../components/menu/menu";
import { Popup } from "../../toolbar/components/popup"; import { Popup } from "../../toolbar/components/popup";
import { Toggle } from "../../components/toggle"; import { Toggle } from "../../components/toggle";
import { Input } from "@rebass/forms"; import { Input } from "@rebass/forms";
@@ -18,6 +22,9 @@ import {
EmbedSizeOptions, EmbedSizeOptions,
} from "./embed"; } from "./embed";
import { EmbedPopup } from "../../toolbar/popups/embed-popup"; import { EmbedPopup } from "../../toolbar/popups/embed-popup";
import { Icon } from "../../toolbar/components/icon";
import { Icons } from "../../toolbar/icons";
import { DesktopOnly, MobileOnly } from "../../components/responsive";
export function EmbedComponent(props: NodeViewProps) { export function EmbedComponent(props: NodeViewProps) {
const { src, width, height, align } = props.node.attrs as EmbedAttributes & const { src, width, height, align } = props.node.attrs as EmbedAttributes &
@@ -61,9 +68,27 @@ export function EmbedComponent(props: NodeViewProps) {
}} }}
lockAspectRatio={true} lockAspectRatio={true}
> >
<Flex sx={{ position: "relative", justifyContent: "end" }}> {/* <Flex sx={{ position: "relative", justifyContent: "end" }}>
</Flex> */}
<Flex
width={"100%"}
sx={{
position: "relative",
justifyContent: "end",
borderTop: "20px solid var(--bgSecondary)",
// borderLeft: "20px solid var(--bgSecondary)",
borderTopLeftRadius: "default",
borderTopRightRadius: "default",
borderColor: isActive ? "border" : "bgSecondary",
cursor: "pointer",
":hover": {
borderColor: "border",
},
}}
>
{isToolbarVisible && ( {isToolbarVisible && (
<ImageToolbar <EmbedToolbar
editor={editor} editor={editor}
align={align} align={align}
height={height || 0} height={height || 0}
@@ -79,10 +104,11 @@ export function EmbedComponent(props: NodeViewProps) {
width={"100%"} width={"100%"}
height={"100%"} height={"100%"}
sx={{ sx={{
border: isActive border: "none",
? "2px solid var(--primary)" // border: isActive
: "2px solid transparent", // ? "2px solid var(--primary)"
borderRadius: "default", // : "2px solid transparent",
// borderRadius: "default",
}} }}
{...props} {...props}
/> />
@@ -98,7 +124,7 @@ type ImageToolbarProps = Required<EmbedAttributes> &
editor: Editor; editor: Editor;
}; };
function ImageToolbar(props: ImageToolbarProps) { function EmbedToolbar(props: ImageToolbarProps) {
const { editor, height, width, src } = props; const { editor, height, width, src } = props;
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
@@ -184,7 +210,13 @@ function ImageToolbar(props: ImageToolbarProps) {
</Flex> </Flex>
</Flex> </Flex>
{isOpen && ( <PopupPresenter
isOpen={isOpen}
desktop="none"
mobile="sheet"
onClose={() => setIsOpen(false)}
blocking={true}
>
<EmbedPopup <EmbedPopup
title="Embed properties" title="Embed properties"
icon="close" icon="close"
@@ -193,7 +225,7 @@ function ImageToolbar(props: ImageToolbarProps) {
onSourceChanged={(src) => {}} onSourceChanged={(src) => {}}
onSizeChanged={(size) => editor.commands.setEmbedSize(size)} onSizeChanged={(size) => editor.commands.setEmbedSize(size)}
/> />
)} </PopupPresenter>
</Flex> </Flex>
); );
} }

View File

@@ -13,10 +13,16 @@ import { ToolButton } from "../../toolbar/components/tool-button";
import { findToolById, ToolId } from "../../toolbar/tools"; import { findToolById, ToolId } from "../../toolbar/tools";
import { Editor } from "@tiptap/core"; import { Editor } from "@tiptap/core";
import { useCallback, useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import { MenuPresenter } from "../../components/menu/menu"; import {
ActionSheetPresenter,
MenuPresenter,
PopupPresenter,
} from "../../components/menu/menu";
import { Popup } from "../../toolbar/components/popup"; import { Popup } from "../../toolbar/components/popup";
import { Toggle } from "../../components/toggle"; import { Toggle } from "../../components/toggle";
import { Input } from "@rebass/forms"; import { Input } from "@rebass/forms";
import { ImageProperties } from "../../toolbar/popups/image-properties";
import { DesktopOnly, MobileOnly } from "../../components/responsive";
export function ImageComponent(props: ImageProps & NodeViewProps) { export function ImageComponent(props: ImageProps & NodeViewProps) {
const { src, alt, title, width, height, align, float } = props.node const { src, alt, title, width, height, align, float } = props.node
@@ -106,28 +112,6 @@ function ImageToolbar(props: ImageToolbarProps) {
const { editor, float, height, width } = props; const { editor, float, height, width } = props;
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const onSizeChange = useCallback(
(newWidth?: number, newHeight?: number) => {
const size: ImageSizeOptions = newWidth
? {
width: newWidth,
height: newWidth * (height / width),
}
: newHeight
? {
width: newHeight * (width / height),
height: newHeight,
}
: {
width: 0,
height: 0,
};
editor.chain().setImageSize(size).run();
},
[width, height]
);
return ( return (
<Flex <Flex
sx={{ sx={{
@@ -210,7 +194,13 @@ function ImageToolbar(props: ImageToolbarProps) {
</Flex> </Flex>
</Flex> </Flex>
{isOpen && ( <PopupPresenter
mobile="sheet"
desktop="none"
isOpen={isOpen}
onClose={() => setIsOpen(false)}
blocking={false}
>
<Popup <Popup
title="Image properties" title="Image properties"
action={{ action={{
@@ -220,46 +210,9 @@ function ImageToolbar(props: ImageToolbarProps) {
}, },
}} }}
> >
<Flex sx={{ width: 200, flexDirection: "column", p: 1 }}> <ImageProperties {...props} />
<Flex
sx={{ justifyContent: "space-between", alignItems: "center" }}
>
<Text variant={"body"}>Floating?</Text>
<Toggle
checked={float}
onClick={() =>
editor
.chain()
.setImageAlignment({ float: !float, align: "left" })
.run()
}
/>
</Flex>
<Flex sx={{ alignItems: "center", mt: 2 }}>
<Input
type="number"
placeholder="Width"
value={width}
sx={{
mr: 2,
p: 1,
fontSize: "body",
}}
onChange={(e) => onSizeChange(e.target.valueAsNumber)}
/>
<Input
type="number"
placeholder="Height"
value={height}
sx={{ p: 1, fontSize: "body" }}
onChange={(e) =>
onSizeChange(undefined, e.target.valueAsNumber)
}
/>
</Flex>
</Flex>
</Popup> </Popup>
)} </PopupPresenter>
</Flex> </Flex>
); );
} }

View File

@@ -5,6 +5,7 @@ import { Icons } from "../icons";
import { MenuPresenter, MenuPresenterProps } from "../../components/menu/menu"; import { MenuPresenter, MenuPresenterProps } from "../../components/menu/menu";
import { MenuItem } from "../../components/menu/types"; import { MenuItem } from "../../components/menu/types";
import { useToolbarContext } from "../hooks/useToolbarContext"; import { useToolbarContext } from "../hooks/useToolbarContext";
import { useToolbarLocation } from "../stores/toolbar-store";
type DropdownProps = { type DropdownProps = {
selectedItem: string | JSX.Element; selectedItem: string | JSX.Element;
@@ -16,7 +17,7 @@ export function Dropdown(props: DropdownProps) {
const { items, selectedItem, buttonRef, menuWidth } = props; const { items, selectedItem, buttonRef, menuWidth } = props;
const internalRef = useRef<any>(); const internalRef = useRef<any>();
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const { toolbarLocation } = useToolbarContext(); const toolbarLocation = useToolbarLocation();
return ( return (
<> <>

View File

@@ -17,9 +17,9 @@ export function Popup(props: PropsWithChildren<PopupProps>) {
sx={{ sx={{
bg: "background", bg: "background",
flexDirection: "column", flexDirection: "column",
borderRadius: "default", // borderRadius: "default",
border: "1px solid var(--border)", // border: "1px solid var(--border)",
boxShadow: "menu", // boxShadow: "menu",
}} }}
> >
{title && ( {title && (
@@ -52,7 +52,7 @@ type PopupButtonProps = ButtonProps & {
function PopupButton(props: PopupButtonProps) { function PopupButton(props: PopupButtonProps) {
const { text, loading, icon, iconColor, iconSize, ...restProps } = props; const { text, loading, icon, iconColor, iconSize, ...restProps } = props;
return ( return (
<Button variant="dialog" sx={{ p: 1, px: 2 }} {...restProps}> <Button variant="icon" sx={{ p: 1, px: 2 }} {...restProps}>
{loading ? ( {loading ? (
<Icon path={Icons.loading} size={16} rotate color="primary" /> <Icon path={Icons.loading} size={16} rotate color="primary" />
) : icon ? ( ) : icon ? (

View File

@@ -6,6 +6,7 @@ import { Icon } from "./icon";
import { ToolButton, ToolButtonProps } from "./tool-button"; import { ToolButton, ToolButtonProps } from "./tool-button";
import { MenuPresenter, MenuPresenterProps } from "../../components/menu/menu"; import { MenuPresenter, MenuPresenterProps } from "../../components/menu/menu";
import { useToolbarContext } from "../hooks/useToolbarContext"; import { useToolbarContext } from "../hooks/useToolbarContext";
import { useToolbarLocation } from "../stores/toolbar-store";
type SplitButtonProps = ToolButtonProps & { type SplitButtonProps = ToolButtonProps & {
menuPresenterProps?: Partial<MenuPresenterProps>; menuPresenterProps?: Partial<MenuPresenterProps>;
@@ -15,7 +16,7 @@ export function SplitButton(props: PropsWithChildren<SplitButtonProps>) {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
const { toolbarLocation } = useToolbarContext(); const toolbarLocation = useToolbarLocation();
return ( return (
<> <>

View File

@@ -10,6 +10,7 @@ export type ToolButtonProps = ButtonProps & {
iconSize?: number; iconSize?: number;
toggled: boolean; toggled: boolean;
buttonRef?: React.MutableRefObject<HTMLButtonElement | null | undefined>; buttonRef?: React.MutableRefObject<HTMLButtonElement | null | undefined>;
variant?: "small" | "normal";
}; };
export function ToolButton(props: ToolButtonProps) { export function ToolButton(props: ToolButtonProps) {
const { const {
@@ -20,6 +21,7 @@ export function ToolButton(props: ToolButtonProps) {
toggled, toggled,
sx, sx,
buttonRef, buttonRef,
variant = "normal",
...buttonProps ...buttonProps
} = props; } = props;
@@ -29,10 +31,11 @@ export function ToolButton(props: ToolButtonProps) {
tabIndex={-1} tabIndex={-1}
id={`tool-${id}`} id={`tool-${id}`}
sx={{ sx={{
p: 1, p: variant === "small" ? "3px" : 1,
borderRadius: variant === "small" ? "small" : "default",
m: 0, m: 0,
bg: toggled ? "hover" : "transparent", bg: toggled ? "hover" : "transparent",
mr: 1, mr: variant === "small" ? 0 : 1,
":hover": { bg: "hover" }, ":hover": { bg: "hover" },
":last-of-type": { ":last-of-type": {
mr: 0, mr: 0,
@@ -44,7 +47,7 @@ export function ToolButton(props: ToolButtonProps) {
<Icon <Icon
path={Icons[icon]} path={Icons[icon]}
color={iconColor || "text"} color={iconColor || "text"}
size={iconSize || 18} size={iconSize || variant === "small" ? 16 : 18}
/> />
</Button> </Button>
); );

View File

@@ -1,12 +1,22 @@
import { TableRowFloatingMenu, TableColumnFloatingMenu } from "./table"; import {
TableRowFloatingMenu,
TableColumnFloatingMenu,
TableFloatingMenu,
} from "./table/table";
import { SearchReplaceFloatingMenu } from "./search-replace"; import { SearchReplaceFloatingMenu } from "./search-replace";
import { FloatingMenuProps } from "./types"; import { FloatingMenuProps } from "./types";
import { DesktopOnly, MobileOnly } from "../../components/responsive";
export function EditorFloatingMenus(props: FloatingMenuProps) { export function EditorFloatingMenus(props: FloatingMenuProps) {
return ( return (
<> <>
<TableRowFloatingMenu {...props} /> <DesktopOnly>
<TableColumnFloatingMenu {...props} /> <TableRowFloatingMenu {...props} />
<TableColumnFloatingMenu {...props} />
</DesktopOnly>
<MobileOnly>
<TableFloatingMenu {...props} />
</MobileOnly>
<SearchReplaceFloatingMenu {...props} /> <SearchReplaceFloatingMenu {...props} />
</> </>
); );

View File

@@ -1,187 +1,38 @@
import { Input } from "@rebass/forms"; import {
import { useCallback, useEffect, useRef, useState } from "react"; ActionSheetPresenter,
import { Flex } from "rebass"; MenuPresenter,
import { MenuPresenter } from "../../components/menu/menu"; PopupPresenter,
} from "../../components/menu/menu";
import { SearchStorage } from "../../extensions/search-replace"; import { SearchStorage } from "../../extensions/search-replace";
import { Popup } from "../components/popup";
import { ToolButton } from "../components/tool-button";
import { FloatingMenuProps } from "./types"; import { FloatingMenuProps } from "./types";
import { SearchReplacePopup } from "../popups/search-replace";
import { DesktopOnly, MobileOnly } from "../../components/responsive";
export function SearchReplaceFloatingMenu(props: FloatingMenuProps) { export function SearchReplaceFloatingMenu(props: FloatingMenuProps) {
const { editor } = props; const { editor } = props;
const { isSearching, selectedText } = editor.storage const { isSearching } = editor.storage.searchreplace as SearchStorage;
.searchreplace as SearchStorage;
const [matchCase, setMatchCase] = useState(false);
const [matchWholeWord, setMatchWholeWord] = useState(false);
const [enableRegex, setEnableRegex] = useState(false);
const replaceText = useRef("");
const searchInputRef = useRef<HTMLInputElement>();
const search = useCallback(
(term: string) => {
editor.commands.search(term, {
matchCase,
enableRegex,
matchWholeWord,
});
},
[matchCase, enableRegex, matchWholeWord]
);
useEffect(() => {
if (!searchInputRef.current) return;
search(searchInputRef.current.value);
}, [search, matchCase, matchWholeWord, enableRegex]);
useEffect(() => {
if (isSearching && selectedText) {
if (searchInputRef.current) {
const input = searchInputRef.current;
setTimeout(() => {
input.value = selectedText;
input.focus();
}, 0);
}
search(selectedText);
}
}, [isSearching, selectedText, search]);
if (!isSearching) return null; if (!isSearching) return null;
return ( return (
<MenuPresenter <>
isOpen <PopupPresenter
items={[]} mobile="sheet"
onClose={() => {}} desktop="menu"
options={{ isOpen
type: "autocomplete", onClose={() => editor.commands.endSearch()}
position: { options={{
target: type: "autocomplete",
document.querySelector<HTMLElement>(".editor-toolbar") || "mouse", position: {
isTargetAbsolute: true, target:
location: "below", document.querySelector<HTMLElement>(".editor-toolbar") || "mouse",
align: "end", isTargetAbsolute: true,
}, location: "below",
}} align: "end",
> },
<Popup> }}
<Flex sx={{ p: 1, flexDirection: "column" }}> >
<Flex sx={{ alignItems: "start", flexShrink: 0 }}> <SearchReplacePopup editor={editor} />
<Flex </PopupPresenter>
sx={{ </>
position: "relative",
mr: 1,
width: 200,
alignItems: "center",
}}
>
<Input
defaultValue={selectedText}
ref={searchInputRef}
autoFocus
sx={{ p: 1 }}
placeholder="Find"
onChange={(e) => {
search(e.target.value);
}}
/>
<Flex
sx={{
position: "absolute",
right: 0,
mr: 0,
}}
>
<ToolButton
sx={{
mr: 0,
}}
toggled={matchCase}
title="Match case"
id="matchCase"
icon="caseSensitive"
onClick={() => setMatchCase((s) => !s)}
iconSize={14}
/>
<ToolButton
sx={{
mr: 0,
}}
toggled={matchWholeWord}
title="Match whole word"
id="matchWholeWord"
icon="wholeWord"
onClick={() => setMatchWholeWord((s) => !s)}
iconSize={14}
/>
<ToolButton
sx={{
mr: 0,
}}
toggled={enableRegex}
title="Enable regex"
id="enableRegex"
icon="regex"
onClick={() => setEnableRegex((s) => !s)}
iconSize={14}
/>
</Flex>
</Flex>
<ToolButton
toggled={false}
title="Previous match"
id="previousMatch"
icon="previousMatch"
onClick={() => editor.commands.moveToPreviousResult()}
sx={{ mr: 0 }}
iconSize={16}
/>
<ToolButton
toggled={false}
title="Next match"
id="nextMatch"
icon="nextMatch"
onClick={() => editor.commands.moveToNextResult()}
sx={{ mr: 0 }}
iconSize={16}
/>
<ToolButton
toggled={false}
title="Close"
id="close"
icon="close"
onClick={() => editor.chain().focus().endSearch().run()}
iconSize={16}
sx={{ mr: 0 }}
/>
</Flex>
<Flex sx={{ alignItems: "start", flexShrink: 0, mt: 1 }}>
<Input
sx={{ p: 1, width: 200, mr: 1 }}
placeholder="Replace"
onChange={(e) => (replaceText.current = e.target.value)}
/>
<ToolButton
toggled={false}
title="Replace"
id="replace"
icon="replaceOne"
onClick={() => editor.commands.replace(replaceText.current)}
sx={{ mr: 0 }}
iconSize={16}
/>
<ToolButton
toggled={false}
title="Replace all"
id="replaceAll"
icon="replaceAll"
onClick={() => editor.commands.replaceAll(replaceText.current)}
sx={{ mr: 0 }}
iconSize={16}
/>
</Flex>
</Flex>
</Popup>
</MenuPresenter>
); );
} }

View File

@@ -1,665 +0,0 @@
import { Theme } from "@notesnook/theme";
import { Slider } from "@rebass/forms";
import { Editor, findParentNodeClosestToPos } from "@tiptap/core";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Flex, Text } from "rebass";
import { MenuPresenter } from "../../components/menu/menu";
import { getElementPosition, MenuOptions } from "../../components/menu/useMenu";
import { Popup } from "../components/popup";
import { ToolButton } from "../components/tool-button";
import { IconNames } from "../icons";
// import { ColorPicker, DEFAULT_COLORS } from "../tools/colors";
import { FloatingMenuProps } from "./types";
import { selectedRect, TableMap, TableRect } from "prosemirror-tables";
import { Transaction } from "prosemirror-state";
import { MenuItem } from "../../components/menu/types";
export function TableRowFloatingMenu(props: FloatingMenuProps) {
const { editor } = props;
const theme = editor.storage.theme as Theme;
const [position, setPosition] = useState<MenuOptions["position"] | null>(
null
);
const [isMenuOpen, setIsMenuOpen] = useState(false);
useEffect(() => {
if (
!editor.isActive("tableCell") &&
!editor.isActive("tableRow") &&
!editor.isActive("tableHeader")
) {
setPosition(null);
return;
}
let { $from } = editor.state.selection;
const selectedNode = $from.node();
const pos = selectedNode.isTextblock ? $from.before() : $from.pos;
const currentRow = (editor.view.nodeDOM(pos) as HTMLElement)?.closest("tr");
if (!currentRow) return;
setPosition((old) => {
if (old?.target === currentRow) return old;
return {
isTargetAbsolute: true,
location: "left",
xOffset: -5,
target: currentRow,
// parent: editor.view.dom as HTMLElement,
};
});
}, [editor.state.selection]);
if (!position) return null;
return (
<MenuPresenter
isOpen
items={[]}
onClose={() => {}}
options={{
type: "autocomplete",
position,
}}
>
<Flex
sx={{
bg: "background",
flexWrap: "nowrap",
borderRadius: "default",
opacity: isMenuOpen ? 1 : 0.3,
":hover": {
opacity: 1,
},
}}
>
<ToolButton
toggled={isMenuOpen}
title="Row properties"
id="properties"
icon="more"
onClick={() => setIsMenuOpen(true)}
iconSize={16}
sx={{ mr: 0, p: "3px", borderRadius: "small" }}
/>
<ToolButton
toggled={false}
title="Insert row below"
id="insertRowBelow"
icon="insertRowBelow"
onClick={() => editor.chain().focus().addRowAfter().run()}
sx={{ mr: 0, p: "3px", borderRadius: "small" }}
iconSize={16}
/>
</Flex>
<MenuPresenter
isOpen={isMenuOpen}
onClose={() => {
setIsMenuOpen(false);
editor.commands.focus();
}}
options={{
type: "menu",
position: {},
}}
items={[
{
key: "addRowAbove",
type: "menuitem",
title: "Add row above",
onClick: () => editor.chain().focus().addRowBefore().run(),
icon: "insertRowAbove",
},
{
key: "moveRowUp",
type: "menuitem",
title: "Move row up",
onClick: () => moveRowUp(editor),
icon: "moveRowUp",
},
{
key: "moveRowDown",
type: "menuitem",
title: "Move row down",
onClick: () => moveRowDown(editor),
icon: "moveRowDown",
},
{
key: "deleteRow",
type: "menuitem",
title: "Delete row",
onClick: () => editor.chain().focus().deleteRow().run(),
icon: "deleteRow",
},
]}
/>
</MenuPresenter>
);
}
export function TableColumnFloatingMenu(props: FloatingMenuProps) {
const { editor } = props;
const [position, setPosition] = useState<MenuOptions["position"] | null>(
null
);
const isInsideCellSelection =
!editor.state.selection.empty &&
editor.state.selection.$anchor.node().type.name === "tableCell";
const [isMenuOpen, setIsMenuOpen] = useState(false);
const [showCellProps, setShowCellProps] = useState(false);
const [menuPosition, setMenuPosition] = useState<
MenuOptions["position"] | null
>(null);
useEffect(() => {
if (
!editor.isActive("tableCell") &&
!editor.isActive("tableRow") &&
!editor.isActive("tableHeader")
) {
setPosition(null);
return;
}
let { $from } = editor.state.selection;
const selectedNode = $from.node();
const pos = selectedNode.isTextblock ? $from.before() : $from.pos;
const currentCell = (editor.view.nodeDOM(pos) as HTMLElement)?.closest(
"td,th"
);
const currentTable = currentCell?.closest("table");
if (!currentCell || !currentTable) return;
setPosition((old) => {
if (old?.target === currentCell) return old;
return {
isTargetAbsolute: true,
location: "top",
align: "center",
yAnchor: currentTable,
yOffset: -2,
target: currentCell as HTMLElement,
};
});
}, [editor.state.selection]);
if (!position) return null;
const columnProperties: MenuItem[] = [
{
key: "addColumnLeft",
type: "menuitem",
title: "Add column left",
onClick: () => editor.chain().focus().addColumnBefore().run(),
icon: "insertColumnLeft",
},
{
key: "addColumnRight",
type: "menuitem",
title: "Add column right",
onClick: () => editor.chain().focus().addColumnAfter().run(),
icon: "insertColumnRight",
},
{
key: "moveColumnLeft",
type: "menuitem",
title: "Move column left",
onClick: () => moveColumnLeft(editor),
icon: "moveColumnLeft",
},
{
key: "moveColumnRight",
type: "menuitem",
title: "Move column right",
onClick: () => moveColumnRight(editor),
icon: "moveColumnRight",
},
{
key: "deleteColumn",
type: "menuitem",
title: "Delete column",
onClick: () => editor.chain().focus().deleteColumn().run(),
icon: "deleteColumn",
},
];
const mergeSplitProperties: MenuItem[] = [
{
key: "splitCells",
type: "menuitem",
title: "Split cells",
onClick: () => editor.chain().focus().splitCell().run(),
icon: "splitCells",
},
{
key: "mergeCells",
type: "menuitem",
title: "Merge cells",
onClick: () => editor.chain().focus().mergeCells().run(),
icon: "mergeCells",
},
];
const cellProperties: MenuItem[] = [
{
key: "cellProperties",
type: "menuitem",
title: "Cell properties",
onClick: () => {
setShowCellProps(true);
setMenuPosition({
target: position.target || undefined,
isTargetAbsolute: true,
yOffset: 10,
location: "below",
});
},
icon: "cellProperties",
},
];
const tableProperties: MenuItem[] = [
{
key: "deleteTable",
type: "menuitem",
title: "Delete table",
icon: "deleteTable",
onClick: () => editor.chain().focus().deleteTable().run(),
},
];
return (
<MenuPresenter
isOpen
items={[]}
onClose={() => {}}
options={{
type: "autocomplete",
position,
}}
>
<Flex
sx={{
bg: "background",
flexWrap: "nowrap",
borderRadius: "default",
opacity: isMenuOpen || showCellProps ? 1 : 0.3,
":hover": {
opacity: 1,
},
}}
>
<ToolButton
toggled={isMenuOpen}
title="Column properties"
id="properties"
icon="more"
onClick={async () => setIsMenuOpen(true)}
iconSize={16}
sx={{ mr: 0, p: "3px", borderRadius: "small" }}
/>
<ToolButton
toggled={false}
title="Insert column right"
id="insertColumnRight"
icon="plus"
onClick={() => editor.chain().focus().addColumnAfter().run()}
sx={{ mr: 0, p: "3px", borderRadius: "small" }}
iconSize={16}
/>
</Flex>
<MenuPresenter
isOpen={isMenuOpen}
onClose={() => {
setIsMenuOpen(false);
editor.commands.focus();
}}
options={{
type: "menu",
position: {},
}}
items={
isInsideCellSelection
? [...mergeSplitProperties, ...cellProperties]
: [
...columnProperties,
{ type: "seperator", key: "cellSeperator" },
...cellProperties,
{ type: "seperator", key: "tableSeperator" },
...tableProperties,
]
}
/>
<MenuPresenter
isOpen={showCellProps}
onClose={() => {
setShowCellProps(false);
editor.commands.focus();
}}
options={{
type: "menu",
position: menuPosition || {},
}}
items={[]}
>
<CellProperties
editor={editor}
onClose={() => setShowCellProps(false)}
/>
</MenuPresenter>
</MenuPresenter>
);
}
type CellPropertiesProps = { editor: Editor; onClose: () => void };
function CellProperties(props: CellPropertiesProps) {
const { editor, onClose } = props;
const attributes = editor.getAttributes("tableCell");
console.log(attributes);
return (
<Popup
title="Cell properties"
action={{
icon: "close",
iconColor: "error",
onClick: onClose,
}}
>
<Flex sx={{ flexDirection: "column", width: 200, px: 1, mb: 2 }}>
<ColorPickerTool
color={attributes.backgroundColor}
title="Background color"
icon="backgroundColor"
onColorChange={(color) =>
editor.commands.setCellAttribute("backgroundColor", color)
}
/>
<ColorPickerTool
color={attributes.color}
title="Text color"
icon="textColor"
onColorChange={(color) =>
editor.commands.setCellAttribute("color", color)
}
/>
<ColorPickerTool
color={attributes.borderColor}
title="Border color"
icon="borderColor"
onColorChange={(color) =>
editor.commands.setCellAttribute("borderColor", color)
}
/>
<Flex sx={{ flexDirection: "column" }}>
<Flex
sx={{
justifyContent: "space-between",
alignItems: "center",
mt: 1,
}}
>
<Text variant={"body"}>Border width</Text>
<Text variant={"body"}>{attributes.borderWidth || 1}px</Text>
</Flex>
<Slider
min={1}
max={5}
value={attributes.borderWidth || 1}
onChange={(e) => {
editor.commands.setCellAttribute(
"borderWidth",
e.target.valueAsNumber
);
}}
/>
</Flex>
</Flex>
</Popup>
);
}
type ColorPickerToolProps = {
color: string;
title: string;
icon: IconNames;
onColorChange: (color?: string) => void;
};
function ColorPickerTool(props: ColorPickerToolProps) {
const { color, title, icon, onColorChange } = props;
const [isOpen, setIsOpen] = useState(false);
const buttonRef = useRef<HTMLButtonElement>(null);
return (
<>
<Flex
sx={{ justifyContent: "space-between", alignItems: "center", mt: 1 }}
>
<Text variant={"body"}>{title}</Text>
<ToolButton
buttonRef={buttonRef}
toggled={isOpen}
title={title}
id={icon}
icon={icon}
iconSize={16}
sx={{
p: "2.5px",
borderRadius: "small",
backgroundColor: color || "transparent",
":hover": { bg: color, filter: "brightness(90%)" },
}}
onClick={() => setIsOpen(true)}
/>
</Flex>
<MenuPresenter
isOpen={isOpen}
onClose={() => setIsOpen(false)}
items={[]}
options={{
type: "menu",
position: {
target: buttonRef.current || undefined,
location: "below",
align: "center",
isTargetAbsolute: true,
yOffset: 5,
},
}}
>
<Flex
sx={{
flexDirection: "column",
bg: "background",
boxShadow: "menu",
border: "1px solid var(--border)",
borderRadius: "default",
p: 1,
width: 160,
}}
>
{/* <ColorPicker
colors={DEFAULT_COLORS}
color={color}
onClear={() => onColorChange()}
onChange={(color) => onColorChange(color)}
/> */}
</Flex>
</MenuPresenter>
</>
);
}
/**
* Done:
* insertTable
*
* addRowBefore
* addRowAfter
* deleteRow
*
* addColumnBefore
* addColumnAfter
* deleteColumn
*
* setCellAttribute
*
* deleteTable
*
* mergeCells
* splitCell
* mergeOrSplit
*
* toggleHeaderColumn
* toggleHeaderRow
* toggleHeaderCell
* fixTables
* goToNextCell
* goToPreviousCell
*/
function moveColumnRight(editor: Editor) {
const { tr } = editor.state;
const rect = selectedRect(editor.state);
if (rect.right === rect.map.width) return;
const transaction = moveColumn(tr, rect, rect.left, rect.left + 1);
if (!transaction) return;
editor.view.dispatch(transaction);
}
function moveColumnLeft(editor: Editor) {
const { tr } = editor.state;
const rect = selectedRect(editor.state);
if (rect.left === 0) return;
const transaction = moveColumn(tr, rect, rect.left, rect.left - 1);
if (!transaction) return;
editor.view.dispatch(transaction);
}
function moveRowDown(editor: Editor) {
const { tr } = editor.state;
const rect = selectedRect(editor.state);
if (rect.top + 1 === rect.map.height) return;
const transaction = moveRow(tr, rect, rect.top, rect.top + 1);
if (!transaction) return;
editor.view.dispatch(transaction);
}
function moveRowUp(editor: Editor) {
const { tr } = editor.state;
const rect = selectedRect(editor.state);
if (rect.top === 0) return;
const transaction = moveRow(tr, rect, rect.top, rect.top - 1);
if (!transaction) return;
editor.view.dispatch(transaction);
}
function moveColumn(
tr: Transaction<any>,
rect: TableRect,
from: number,
to: number
) {
let fromCells = getColumnCells(rect, from);
let toCells = getColumnCells(rect, to);
return moveCells(tr, rect, fromCells, toCells);
}
function getColumnCells({ map, table }: TableRect, col: number) {
let cells = [];
for (let row = 0; row < map.height; ) {
let index = row * map.width + col;
if (index >= map.map.length) break;
let pos = map.map[index];
let cell = table.nodeAt(pos);
if (!cell) continue;
cells.push({ cell, pos });
row += cell.attrs.rowspan;
console.log(cell.textContent);
}
return cells;
}
function moveRow(
tr: Transaction<any>,
rect: TableRect,
from: number,
to: number
) {
let fromCells = getRowCells(rect, from);
let toCells = getRowCells(rect, to);
return moveCells(tr, rect, fromCells, toCells);
}
function getRowCells({ map, table }: TableRect, row: number) {
let cells = [];
for (let col = 0, index = row * map.width; col < map.width; col++, index++) {
if (index >= map.map.length) break;
let pos = map.map[index];
let cell = table.nodeAt(pos);
if (!cell) continue;
cells.push({ cell, pos });
col += cell.attrs.colspan - 1;
}
return cells;
}
function moveCells(
tr: Transaction<any>,
rect: TableRect,
fromCells: any[],
toCells: any[]
) {
if (fromCells.length !== toCells.length) return;
let mapStart = tr.mapping.maps.length;
for (let i = 0; i < toCells.length; ++i) {
const fromCell = fromCells[i];
const toCell = toCells[i];
let fromStart = tr.mapping
.slice(mapStart)
.map(rect.tableStart + fromCell.pos);
let fromEnd = fromStart + fromCell.cell.nodeSize;
const fromSlice = tr.doc.slice(fromStart, fromEnd);
const toStart = tr.mapping
.slice(mapStart)
.map(rect.tableStart + toCell.pos);
const toEnd = toStart + toCell.cell.nodeSize;
const toSlice = tr.doc.slice(toStart, toEnd);
tr.replace(toStart, toEnd, fromSlice);
fromStart = tr.mapping.slice(mapStart).map(rect.tableStart + fromCell.pos);
fromEnd = fromStart + fromCell.cell.nodeSize;
tr.replace(fromStart, fromEnd, toSlice);
}
return tr;
}

View File

@@ -0,0 +1,163 @@
import { Theme } from "@notesnook/theme";
import { Slider } from "@rebass/forms";
import { Editor, findParentNodeClosestToPos } from "@tiptap/core";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Flex, Text } from "rebass";
import {
ActionSheetPresenter,
MenuPresenter,
} from "../../../components/menu/menu";
import {
getElementPosition,
MenuOptions,
} from "../../../components/menu/useMenu";
import { Popup } from "../../components/popup";
import { ToolButton, ToolButtonProps } from "../../components/tool-button";
import { IconNames } from "../../icons";
// import { ColorPicker, DEFAULT_COLORS } from "../tools/colors";
import { FloatingMenuProps } from "../types";
import { selectedRect, TableMap, TableRect } from "prosemirror-tables";
import { Transaction } from "prosemirror-state";
import { MenuItem } from "../../../components/menu/types";
import { DesktopOnly, MobileOnly } from "../../../components/responsive";
import { ToolProps } from "../../types";
function moveColumnRight(editor: Editor) {
const { tr } = editor.state;
const rect = selectedRect(editor.state);
if (rect.right === rect.map.width) return;
const transaction = moveColumn(tr, rect, rect.left, rect.left + 1);
if (!transaction) return;
editor.view.dispatch(transaction);
}
function moveColumnLeft(editor: Editor) {
const { tr } = editor.state;
const rect = selectedRect(editor.state);
if (rect.left === 0) return;
const transaction = moveColumn(tr, rect, rect.left, rect.left - 1);
if (!transaction) return;
editor.view.dispatch(transaction);
}
function moveRowDown(editor: Editor) {
const { tr } = editor.state;
const rect = selectedRect(editor.state);
if (rect.top + 1 === rect.map.height) return;
const transaction = moveRow(tr, rect, rect.top, rect.top + 1);
if (!transaction) return;
editor.view.dispatch(transaction);
}
function moveRowUp(editor: Editor) {
const { tr } = editor.state;
const rect = selectedRect(editor.state);
if (rect.top === 0) return;
const transaction = moveRow(tr, rect, rect.top, rect.top - 1);
if (!transaction) return;
editor.view.dispatch(transaction);
}
function moveColumn(
tr: Transaction<any>,
rect: TableRect,
from: number,
to: number
) {
let fromCells = getColumnCells(rect, from);
let toCells = getColumnCells(rect, to);
return moveCells(tr, rect, fromCells, toCells);
}
function getColumnCells({ map, table }: TableRect, col: number) {
let cells = [];
for (let row = 0; row < map.height; ) {
let index = row * map.width + col;
if (index >= map.map.length) break;
let pos = map.map[index];
let cell = table.nodeAt(pos);
if (!cell) continue;
cells.push({ cell, pos });
row += cell.attrs.rowspan;
console.log(cell.textContent);
}
return cells;
}
function moveRow(
tr: Transaction<any>,
rect: TableRect,
from: number,
to: number
) {
let fromCells = getRowCells(rect, from);
let toCells = getRowCells(rect, to);
return moveCells(tr, rect, fromCells, toCells);
}
function getRowCells({ map, table }: TableRect, row: number) {
let cells = [];
for (let col = 0, index = row * map.width; col < map.width; col++, index++) {
if (index >= map.map.length) break;
let pos = map.map[index];
let cell = table.nodeAt(pos);
if (!cell) continue;
cells.push({ cell, pos });
col += cell.attrs.colspan - 1;
}
return cells;
}
function moveCells(
tr: Transaction<any>,
rect: TableRect,
fromCells: any[],
toCells: any[]
) {
if (fromCells.length !== toCells.length) return;
let mapStart = tr.mapping.maps.length;
for (let i = 0; i < toCells.length; ++i) {
const fromCell = fromCells[i];
const toCell = toCells[i];
let fromStart = tr.mapping
.slice(mapStart)
.map(rect.tableStart + fromCell.pos);
let fromEnd = fromStart + fromCell.cell.nodeSize;
const fromSlice = tr.doc.slice(fromStart, fromEnd);
const toStart = tr.mapping
.slice(mapStart)
.map(rect.tableStart + toCell.pos);
const toEnd = toStart + toCell.cell.nodeSize;
const toSlice = tr.doc.slice(toStart, toEnd);
tr.replace(toStart, toEnd, fromSlice);
fromStart = tr.mapping.slice(mapStart).map(rect.tableStart + fromCell.pos);
fromEnd = fromStart + fromCell.cell.nodeSize;
tr.replace(fromStart, fromEnd, toSlice);
}
return tr;
}
export { moveColumnLeft, moveColumnRight, moveRowDown, moveRowUp };

View File

@@ -0,0 +1,5 @@
export {
TableColumnFloatingMenu,
TableFloatingMenu,
TableRowFloatingMenu,
} from "./table";

View File

@@ -0,0 +1,234 @@
import { useEffect, useState } from "react";
import { Flex } from "rebass";
import { MenuPresenter } from "../../../components/menu/menu";
import { MenuOptions } from "../../../components/menu/useMenu";
// import { ColorPicker, DEFAULT_COLORS } from "../tools/colors";
import { FloatingMenuProps } from "../types";
import {
ColumnProperties,
InsertColumnRight,
InsertRowBelow,
RowProperties,
} from "./tools";
import { getToolbarElement } from "../../utils/dom";
import { useToolbarContext } from "../../hooks/useToolbarContext";
import { useToolbarLocation } from "../../stores/toolbar-store";
export function TableRowFloatingMenu(props: FloatingMenuProps) {
const { editor } = props;
// const theme = editor.storage.theme as Theme;
const [position, setPosition] = useState<MenuOptions["position"] | null>(
null
);
useEffect(() => {
if (
!editor.isActive("tableCell") &&
!editor.isActive("tableRow") &&
!editor.isActive("tableHeader")
) {
setPosition(null);
return;
}
let { $from } = editor.state.selection;
const selectedNode = $from.node();
const pos = selectedNode.isTextblock ? $from.before() : $from.pos;
const currentRow = (editor.view.nodeDOM(pos) as HTMLElement)?.closest("tr");
if (!currentRow) return;
setPosition((old) => {
if (old?.target === currentRow) return old;
return {
isTargetAbsolute: true,
location: "left",
xOffset: -5,
target: currentRow,
// parent: editor.view.dom as HTMLElement,
};
});
}, [editor.state.selection]);
if (!position) return null;
return (
<MenuPresenter
isOpen
items={[]}
onClose={() => {}}
options={{
type: "autocomplete",
position,
}}
>
<Flex
sx={{
bg: "background",
flexWrap: "nowrap",
borderRadius: "default",
// opacity: isMenuOpen ? 1 : 0.3,
":hover": {
opacity: 1,
},
}}
>
<RowProperties
title="Row properties"
editor={editor}
variant="small"
icon="more"
/>
<InsertRowBelow
title="Insert row below"
icon="insertRowBelow"
editor={editor}
variant="small"
/>
</Flex>
</MenuPresenter>
);
}
export function TableColumnFloatingMenu(props: FloatingMenuProps) {
const { editor } = props;
const [position, setPosition] = useState<MenuOptions["position"] | null>(
null
);
useEffect(() => {
if (
!editor.isActive("tableCell") &&
!editor.isActive("tableRow") &&
!editor.isActive("tableHeader")
) {
setPosition(null);
return;
}
let { $from } = editor.state.selection;
const selectedNode = $from.node();
const pos = selectedNode.isTextblock ? $from.before() : $from.pos;
const currentCell = (editor.view.nodeDOM(pos) as HTMLElement)?.closest(
"td,th"
);
const currentTable = currentCell?.closest("table");
if (!currentCell || !currentTable) return;
setPosition((old) => {
if (old?.target === currentCell) return old;
return {
isTargetAbsolute: true,
location: "top",
align: "center",
yAnchor: currentTable,
yOffset: 2,
target: currentCell as HTMLElement,
};
});
}, [editor.state.selection]);
if (!position) return null;
return (
<MenuPresenter
isOpen
items={[]}
onClose={() => {}}
options={{
type: "autocomplete",
position,
}}
>
<Flex
sx={{
bg: "background",
flexWrap: "nowrap",
borderRadius: "default",
// opacity: 0.3,
// opacity: isMenuOpen || showCellProps ? 1 : 0.3,
":hover": {
opacity: 1,
},
}}
>
<ColumnProperties
currentCell={position.target as HTMLElement}
title="Column properties"
editor={editor}
icon="more"
variant={"small"}
/>
<InsertColumnRight
editor={editor}
title="Insert column right"
variant={"small"}
icon="plus"
/>
</Flex>
</MenuPresenter>
);
}
export function TableFloatingMenu(props: FloatingMenuProps) {
const { editor } = props;
const toolbarLocation = useToolbarLocation();
if (!editor.isActive("table")) return null;
return (
<MenuPresenter
isOpen
items={[]}
onClose={() => {}}
options={{
type: "autocomplete",
position: {
isTargetAbsolute: true,
target: getToolbarElement(),
location: toolbarLocation === "bottom" ? "top" : "below",
},
}}
>
<Flex
sx={{
bg: "background",
flexWrap: "nowrap",
borderRadius: "default",
// opacity: 0.3,
// opacity: isMenuOpen || showCellProps ? 1 : 0.3,
":hover": {
opacity: 1,
},
}}
>
<RowProperties
title="Row properties"
editor={editor}
variant="normal"
icon="rowProperties"
/>
<InsertRowBelow
title="Insert row below"
icon="insertRowBelow"
editor={editor}
variant="normal"
/>
<ColumnProperties
title="Column properties"
editor={editor}
icon="columnProperties"
variant={"normal"}
/>
<InsertColumnRight
editor={editor}
title="Insert column right"
variant={"normal"}
icon="insertColumnRight"
/>
</Flex>
</MenuPresenter>
);
}

View File

@@ -0,0 +1,262 @@
import { Theme } from "@notesnook/theme";
import { Slider } from "@rebass/forms";
import { Editor, findParentNodeClosestToPos } from "@tiptap/core";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Flex, Text } from "rebass";
import {
ActionSheetPresenter,
MenuPresenter,
PopupPresenter,
} from "../../../components/menu/menu";
import {
getElementPosition,
MenuOptions,
} from "../../../components/menu/useMenu";
import { Popup } from "../../components/popup";
import { ToolButton, ToolButtonProps } from "../../components/tool-button";
import { IconNames } from "../../icons";
// import { ColorPicker, DEFAULT_COLORS } from "../tools/colors";
import { FloatingMenuProps } from "../types";
import { selectedRect, TableMap, TableRect } from "prosemirror-tables";
import { Transaction } from "prosemirror-state";
import { MenuItem } from "../../../components/menu/types";
import { DesktopOnly, MobileOnly } from "../../../components/responsive";
import { ToolProps } from "../../types";
import { CellProperties } from "../../popups/cell-properties";
import {
moveColumnLeft,
moveColumnRight,
moveRowDown,
moveRowUp,
} from "./actions";
type TableToolProps = ToolProps & { variant: ToolButtonProps["variant"] };
export function RowProperties(props: TableToolProps) {
const { editor, ...toolProps } = props;
const [isMenuOpen, setIsMenuOpen] = useState(false);
return (
<>
<ToolButton
toggled={isMenuOpen}
{...toolProps}
onClick={() => setIsMenuOpen(true)}
// iconSize={16}
// sx={{ mr: 0, p: "3px", borderRadius: "small" }}
/>
<MenuPresenter
isOpen={isMenuOpen}
onClose={() => {
setIsMenuOpen(false);
editor.commands.focus();
}}
options={{
type: "menu",
position: {},
}}
items={[
{
key: "addRowAbove",
type: "menuitem",
title: "Add row above",
onClick: () => editor.chain().focus().addRowBefore().run(),
icon: "insertRowAbove",
},
{
key: "moveRowUp",
type: "menuitem",
title: "Move row up",
onClick: () => moveRowUp(editor),
icon: "moveRowUp",
},
{
key: "moveRowDown",
type: "menuitem",
title: "Move row down",
onClick: () => moveRowDown(editor),
icon: "moveRowDown",
},
{
key: "deleteRow",
type: "menuitem",
title: "Delete row",
onClick: () => editor.chain().focus().deleteRow().run(),
icon: "deleteRow",
},
]}
/>
</>
);
}
export function InsertRowBelow(props: TableToolProps) {
const { editor, ...toolProps } = props;
return (
<ToolButton
toggled={false}
{...toolProps}
onClick={() => editor.chain().focus().addRowAfter().run()}
// sx={{ mr: 0, p: "3px", borderRadius: "small" }}
// iconSize={16}
/>
);
}
type ColumnPropertiesProps = TableToolProps & {
currentCell?: HTMLElement;
};
export function ColumnProperties(props: ColumnPropertiesProps) {
const { editor, currentCell, ...toolProps } = props;
const [isMenuOpen, setIsMenuOpen] = useState(false);
const isInsideCellSelection =
!editor.state.selection.empty &&
editor.state.selection.$anchor.node().type.name === "tableCell";
const [showCellProps, setShowCellProps] = useState(false);
const [menuPosition, setMenuPosition] = useState<
MenuOptions["position"] | null
>(null);
const columnProperties: MenuItem[] = [
{
key: "addColumnLeft",
type: "menuitem",
title: "Add column left",
onClick: () => editor.chain().focus().addColumnBefore().run(),
icon: "insertColumnLeft",
},
{
key: "addColumnRight",
type: "menuitem",
title: "Add column right",
onClick: () => editor.chain().focus().addColumnAfter().run(),
icon: "insertColumnRight",
},
{
key: "moveColumnLeft",
type: "menuitem",
title: "Move column left",
onClick: () => moveColumnLeft(editor),
icon: "moveColumnLeft",
},
{
key: "moveColumnRight",
type: "menuitem",
title: "Move column right",
onClick: () => moveColumnRight(editor),
icon: "moveColumnRight",
},
{
key: "deleteColumn",
type: "menuitem",
title: "Delete column",
onClick: () => editor.chain().focus().deleteColumn().run(),
icon: "deleteColumn",
},
];
const mergeSplitProperties: MenuItem[] = [
{
key: "splitCells",
type: "menuitem",
title: "Split cells",
onClick: () => editor.chain().focus().splitCell().run(),
icon: "splitCells",
},
{
key: "mergeCells",
type: "menuitem",
title: "Merge cells",
onClick: () => editor.chain().focus().mergeCells().run(),
icon: "mergeCells",
},
];
const cellProperties: MenuItem[] = [
{
key: "cellProperties",
type: "menuitem",
title: "Cell properties",
onClick: () => {
setShowCellProps(true);
setMenuPosition({
target: currentCell || undefined,
isTargetAbsolute: true,
yOffset: 10,
location: "below",
});
},
icon: "cellProperties",
},
];
const tableProperties: MenuItem[] = [
{
key: "deleteTable",
type: "menuitem",
title: "Delete table",
icon: "deleteTable",
onClick: () => editor.chain().focus().deleteTable().run(),
},
];
return (
<>
<ToolButton
toggled={isMenuOpen}
{...toolProps}
onClick={async () => setIsMenuOpen(true)}
/>
<PopupPresenter
isOpen={isMenuOpen}
onClose={() => {
setIsMenuOpen(false);
editor.commands.focus();
}}
mobile="sheet"
items={
isInsideCellSelection
? [...mergeSplitProperties, ...cellProperties]
: [
...columnProperties,
{ type: "seperator", key: "cellSeperator" },
...cellProperties,
{ type: "seperator", key: "tableSeperator" },
...tableProperties,
]
}
/>
<PopupPresenter
isOpen={showCellProps}
onClose={() => {
setShowCellProps(false);
editor.commands.focus();
}}
options={{
type: "menu",
position: menuPosition || {},
}}
mobile="sheet"
>
<CellProperties
editor={editor}
onClose={() => setShowCellProps(false)}
/>
</PopupPresenter>
</>
);
}
export function InsertColumnRight(props: TableToolProps) {
const { editor, ...toolProps } = props;
return (
<ToolButton
{...toolProps}
toggled={false}
onClick={() => editor.chain().focus().addColumnAfter().run()}
// sx={{ mr: 0, p: "3px", borderRadius: "small" }}
// iconSize={16}
/>
);
}

View File

@@ -1,11 +1,8 @@
import React, { useContext } from "react"; import React, { useContext } from "react";
export type ToolbarLocation = "top" | "bottom";
export const ToolbarContext = React.createContext<{ export const ToolbarContext = React.createContext<{
currentPopup?: string; currentPopup?: string;
setCurrentPopup?: React.Dispatch<React.SetStateAction<string | undefined>>; setCurrentPopup?: React.Dispatch<React.SetStateAction<string | undefined>>;
toolbarLocation?: ToolbarLocation;
}>({}); }>({});
export function useToolbarContext() { export function useToolbarContext() {

View File

@@ -78,6 +78,10 @@ import {
mdiMoviePlusOutline, mdiMoviePlusOutline,
mdiLink, mdiLink,
mdiChevronRight, mdiChevronRight,
mdiTableRow,
mdiTableColumn,
mdiTableColumnWidth,
mdiTableRowHeight,
} from "@mdi/js"; } from "@mdi/js";
export const Icons = { export const Icons = {
@@ -113,12 +117,14 @@ export const Icons = {
upload: mdiUploadOutline, upload: mdiUploadOutline,
attachment: mdiAttachment, attachment: mdiAttachment,
table: mdiTable, table: mdiTable,
insertRowBelow: mdiPlus, rowProperties: mdiTableRowHeight,
insertRowBelow: mdiTableRowPlusAfter,
insertRowAbove: mdiTableRowPlusBefore, insertRowAbove: mdiTableRowPlusBefore,
moveRowDown: mdiArrowExpandDown, moveRowDown: mdiArrowExpandDown,
moveRowUp: mdiArrowExpandUp, moveRowUp: mdiArrowExpandUp,
deleteRow: mdiTableRowRemove, deleteRow: mdiTableRowRemove,
toggleHeaderRow: mdiTableBorder, toggleHeaderRow: mdiTableBorder,
columnProperties: mdiTableColumnWidth,
insertColumnRight: mdiTableColumnPlusAfter, insertColumnRight: mdiTableColumnPlusAfter,
insertColumnLeft: mdiTableColumnPlusBefore, insertColumnLeft: mdiTableColumnPlusBefore,
moveColumnRight: mdiArrowExpandRight, moveColumnRight: mdiArrowExpandRight,

View File

@@ -0,0 +1,145 @@
import { Slider } from "@rebass/forms";
import { Editor } from "@tiptap/core";
import { useRef, useState } from "react";
import { Flex, Text } from "rebass";
import { MenuPresenter } from "../../components/menu/menu";
import { Popup } from "../components/popup";
import { ToolButton } from "../components/tool-button";
import { IconNames } from "../icons";
type CellPropertiesProps = { editor: Editor; onClose: () => void };
export function CellProperties(props: CellPropertiesProps) {
const { editor, onClose } = props;
const attributes = editor.getAttributes("tableCell");
return (
<Popup
title="Cell properties"
action={{
icon: "close",
iconColor: "error",
onClick: onClose,
}}
>
<Flex sx={{ flexDirection: "column", px: 1, mb: 2 }}>
<ColorPickerTool
color={attributes.backgroundColor}
title="Background color"
icon="backgroundColor"
onColorChange={(color) =>
editor.commands.setCellAttribute("backgroundColor", color)
}
/>
<ColorPickerTool
color={attributes.color}
title="Text color"
icon="textColor"
onColorChange={(color) =>
editor.commands.setCellAttribute("color", color)
}
/>
<ColorPickerTool
color={attributes.borderColor}
title="Border color"
icon="borderColor"
onColorChange={(color) =>
editor.commands.setCellAttribute("borderColor", color)
}
/>
<Flex sx={{ flexDirection: "column" }}>
<Flex
sx={{
justifyContent: "space-between",
alignItems: "center",
mt: 1,
}}
>
<Text variant={"body"}>Border width</Text>
<Text variant={"body"}>{attributes.borderWidth || 1}px</Text>
</Flex>
<Slider
min={1}
max={5}
value={attributes.borderWidth || 1}
onChange={(e) => {
editor.commands.setCellAttribute(
"borderWidth",
e.target.valueAsNumber
);
}}
/>
</Flex>
</Flex>
</Popup>
);
}
type ColorPickerToolProps = {
color: string;
title: string;
icon: IconNames;
onColorChange: (color?: string) => void;
};
function ColorPickerTool(props: ColorPickerToolProps) {
const { color, title, icon, onColorChange } = props;
const [isOpen, setIsOpen] = useState(false);
const buttonRef = useRef<HTMLButtonElement>(null);
return (
<>
<Flex
sx={{ justifyContent: "space-between", alignItems: "center", mt: 1 }}
>
<Text variant={"body"}>{title}</Text>
<ToolButton
buttonRef={buttonRef}
toggled={isOpen}
title={title}
id={icon}
icon={icon}
iconSize={16}
sx={{
p: "2.5px",
borderRadius: "small",
backgroundColor: color || "transparent",
":hover": { bg: color, filter: "brightness(90%)" },
}}
onClick={() => setIsOpen(true)}
/>
</Flex>
<MenuPresenter
isOpen={isOpen}
onClose={() => setIsOpen(false)}
items={[]}
options={{
type: "menu",
position: {
target: buttonRef.current || undefined,
location: "below",
align: "center",
isTargetAbsolute: true,
yOffset: 5,
},
}}
>
<Flex
sx={{
flexDirection: "column",
bg: "background",
boxShadow: "menu",
border: "1px solid var(--border)",
borderRadius: "default",
p: 1,
width: 160,
}}
>
{/* <ColorPicker
colors={DEFAULT_COLORS}
color={color}
onClear={() => onColorChange()}
onChange={(color) => onColorChange(color)}
/> */}
</Flex>
</MenuPresenter>
</>
);
}

View File

@@ -100,7 +100,7 @@ export function EmbedPopup(props: EmbedPopupProps) {
}, },
}} }}
> >
<Flex sx={{ width: 300, flexDirection: "column", p: 1 }}> <Flex sx={{ flexDirection: "column", p: 1 }}>
{error && ( {error && (
<Text <Text
variant={"error"} variant={"error"}
@@ -115,23 +115,31 @@ export function EmbedPopup(props: EmbedPopupProps) {
</Text> </Text>
)} )}
{/* <Flex sx={{ position: "relative", alignItems: "center" }}> */} {/* <Flex sx={{ position: "relative", alignItems: "center" }}> */}
<Flex> <Flex sx={{ mb: 1 }}>
<Button <Button
variant={"dialog"} variant={"dialog"}
sx={{ sx={{
p: 1, pb: 1,
mr: 1, mr: 1,
borderRadius: 0,
color: embedSource === "url" ? "primary" : "text", color: embedSource === "url" ? "primary" : "text",
borderBottom: "2px solid",
borderBottomColor:
embedSource === "url" ? "primary" : "transparent",
}} }}
onClick={() => setEmbedSource("url")} onClick={() => setEmbedSource("url")}
> >
From link From URL
</Button> </Button>
<Button <Button
variant={"dialog"} variant={"dialog"}
sx={{ sx={{
p: 1, pb: 1,
borderRadius: 0,
color: embedSource === "code" ? "primary" : "text", color: embedSource === "code" ? "primary" : "text",
borderBottom: "2px solid",
borderBottomColor:
embedSource === "code" ? "primary" : "transparent",
}} }}
onClick={() => setEmbedSource("code")} onClick={() => setEmbedSource("code")}
> >
@@ -140,31 +148,30 @@ export function EmbedPopup(props: EmbedPopupProps) {
</Flex> </Flex>
{embedSource === "url" ? ( {embedSource === "url" ? (
<Input <Input
placeholder="Embed source URL" placeholder="Enter embed source URL"
value={src} value={src}
autoFocus autoFocus
onChange={(e) => setSrc(e.target.value)} onChange={(e) => setSrc(e.target.value)}
sx={{ p: 1, mt: 1, fontSize: "body" }} sx={{ mt: 1, fontSize: "body" }}
/> />
) : ( ) : (
<Textarea <Textarea
autoFocus autoFocus
variant={"forms.input"} variant={"forms.input"}
sx={{ fontSize: "subBody", fontFamily: "monospace", mt: 1 }} sx={{ fontSize: "subBody", fontFamily: "monospace", mt: 1 }}
minHeight={100} minHeight={[200, 100]}
onChange={(e) => setSrc(e.target.value)} onChange={(e) => setSrc(e.target.value)}
placeholder="Paste embed code here. Only iframes are supported." placeholder="Paste embed code here. Only iframes are supported."
/> />
)} )}
{embedSource === "url" ? ( {embedSource === "url" ? (
<Flex sx={{ alignItems: "center", mt: 2 }}> <Flex sx={{ alignItems: "center", mt: 1 }}>
<Input <Input
type="number" type="number"
placeholder="Width" placeholder="Width"
value={width} value={width}
sx={{ sx={{
mr: 2, mr: 1,
p: 1,
fontSize: "body", fontSize: "body",
}} }}
onChange={(e) => onSizeChange(e.target.valueAsNumber)} onChange={(e) => onSizeChange(e.target.valueAsNumber)}
@@ -173,7 +180,7 @@ export function EmbedPopup(props: EmbedPopupProps) {
type="number" type="number"
placeholder="Height" placeholder="Height"
value={height} value={height}
sx={{ p: 1, fontSize: "body" }} sx={{ fontSize: "body" }}
onChange={(e) => onSizeChange(undefined, e.target.valueAsNumber)} onChange={(e) => onSizeChange(undefined, e.target.valueAsNumber)}
/> />
</Flex> </Flex>

View File

@@ -0,0 +1,75 @@
import { Button, Flex, Text } from "rebass";
import { useCallback, useEffect, useState } from "react";
import { Popup } from "../components/popup";
import { Toggle } from "../../components/toggle";
import { Input, Textarea } from "@rebass/forms";
import {
ImageAlignmentOptions,
ImageSizeOptions,
} from "../../extensions/image";
import { Editor } from "@tiptap/core";
export type ImagePropertiesProps = ImageSizeOptions &
ImageAlignmentOptions & { editor: Editor };
export function ImageProperties(props: ImagePropertiesProps) {
const { height, width, float, editor } = props;
const onSizeChange = useCallback(
(newWidth?: number, newHeight?: number) => {
const size: ImageSizeOptions = newWidth
? {
width: newWidth,
height: newWidth * (height / width),
}
: newHeight
? {
width: newHeight * (width / height),
height: newHeight,
}
: {
width: 0,
height: 0,
};
editor.chain().setImageSize(size).run();
},
[width, height]
);
return (
<Flex sx={{ width: 200, flexDirection: "column", p: 1 }}>
<Flex sx={{ justifyContent: "space-between", alignItems: "center" }}>
<Text variant={"body"}>Floating?</Text>
<Toggle
checked={float}
onClick={() =>
editor
.chain()
.setImageAlignment({ float: !float, align: "left" })
.run()
}
/>
</Flex>
<Flex sx={{ alignItems: "center", mt: 2 }}>
<Input
type="number"
placeholder="Width"
value={width}
sx={{
mr: 2,
p: 1,
fontSize: "body",
}}
onChange={(e) => onSizeChange(e.target.valueAsNumber)}
/>
<Input
type="number"
placeholder="Height"
value={height}
sx={{ p: 1, fontSize: "body" }}
onChange={(e) => onSizeChange(undefined, e.target.valueAsNumber)}
/>
</Flex>
</Flex>
);
}

View File

@@ -0,0 +1,184 @@
import { Input } from "@rebass/forms";
import { useCallback, useEffect, useRef, useState } from "react";
import { Flex } from "rebass";
import { SearchStorage } from "../../extensions/search-replace";
import { ToolButton } from "../components/tool-button";
import { Editor } from "@tiptap/core";
export type SearchReplacePopupProps = { editor: Editor };
export function SearchReplacePopup(props: SearchReplacePopupProps) {
const { editor } = props;
const { selectedText } = editor.storage.searchreplace as SearchStorage;
const [matchCase, setMatchCase] = useState(false);
const [matchWholeWord, setMatchWholeWord] = useState(false);
const [enableRegex, setEnableRegex] = useState(false);
const replaceText = useRef("");
const searchInputRef = useRef<HTMLInputElement>();
const search = useCallback(
(term: string) => {
editor.commands.search(term, {
matchCase,
enableRegex,
matchWholeWord,
});
},
[matchCase, enableRegex, matchWholeWord]
);
useEffect(() => {
if (!searchInputRef.current) return;
search(searchInputRef.current.value);
}, [search, matchCase, matchWholeWord, enableRegex]);
useEffect(() => {
if (selectedText) {
if (searchInputRef.current) {
const input = searchInputRef.current;
setTimeout(() => {
input.value = selectedText;
input.focus();
}, 0);
}
search(selectedText);
}
}, [selectedText, search]);
return (
// <MenuPresenter
// isOpen
// items={[]}
// onClose={() => {}}
// options={{
// type: "autocomplete",
// position: {
// target:
// document.querySelector<HTMLElement>(".editor-toolbar") || "mouse",
// isTargetAbsolute: true,
// location: "below",
// align: "end",
// },
// }}
// >
// <Popup>
<Flex sx={{ p: 1, flexDirection: "column" }}>
<Flex sx={{ alignItems: "start", flexShrink: 0 }}>
<Flex
sx={{
position: "relative",
mr: 1,
width: 200,
alignItems: "center",
}}
>
<Input
defaultValue={selectedText}
ref={searchInputRef}
autoFocus
sx={{ p: 1 }}
placeholder="Find"
onChange={(e) => {
search(e.target.value);
}}
/>
<Flex
sx={{
position: "absolute",
right: 0,
mr: 0,
}}
>
<ToolButton
sx={{
mr: 0,
}}
toggled={matchCase}
title="Match case"
id="matchCase"
icon="caseSensitive"
onClick={() => setMatchCase((s) => !s)}
iconSize={14}
/>
<ToolButton
sx={{
mr: 0,
}}
toggled={matchWholeWord}
title="Match whole word"
id="matchWholeWord"
icon="wholeWord"
onClick={() => setMatchWholeWord((s) => !s)}
iconSize={14}
/>
<ToolButton
sx={{
mr: 0,
}}
toggled={enableRegex}
title="Enable regex"
id="enableRegex"
icon="regex"
onClick={() => setEnableRegex((s) => !s)}
iconSize={14}
/>
</Flex>
</Flex>
<ToolButton
toggled={false}
title="Previous match"
id="previousMatch"
icon="previousMatch"
onClick={() => editor.commands.moveToPreviousResult()}
sx={{ mr: 0 }}
iconSize={16}
/>
<ToolButton
toggled={false}
title="Next match"
id="nextMatch"
icon="nextMatch"
onClick={() => editor.commands.moveToNextResult()}
sx={{ mr: 0 }}
iconSize={16}
/>
<ToolButton
toggled={false}
title="Close"
id="close"
icon="close"
onClick={() => editor.chain().focus().endSearch().run()}
iconSize={16}
sx={{ mr: 0 }}
/>
</Flex>
<Flex sx={{ alignItems: "start", flexShrink: 0, mt: 1 }}>
<Input
sx={{ p: 1, width: 200, mr: 1 }}
placeholder="Replace"
onChange={(e) => (replaceText.current = e.target.value)}
/>
<ToolButton
toggled={false}
title="Replace"
id="replace"
icon="replaceOne"
onClick={() => editor.commands.replace(replaceText.current)}
sx={{ mr: 0 }}
iconSize={16}
/>
<ToolButton
toggled={false}
title="Replace all"
id="replaceAll"
icon="replaceAll"
onClick={() => editor.commands.replaceAll(replaceText.current)}
sx={{ mr: 0 }}
iconSize={16}
/>
</Flex>
</Flex>
// </Popup>
// </MenuPresenter>
);
}

View File

@@ -1,6 +1,9 @@
import { Box, Flex, Text } from "rebass"; import { Box, Button, Flex, Text } from "rebass";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Popup } from "../components/popup"; import { Popup } from "../components/popup";
import { Input } from "@rebass/forms";
import { Icon } from "../components/icon";
import { Icons } from "../icons";
const MAX_COLUMNS = 20; const MAX_COLUMNS = 20;
const MAX_ROWS = 20; const MAX_ROWS = 20;
@@ -11,10 +14,12 @@ type CellLocation = { column: number; row: number };
type TableSize = { columns: number; rows: number }; type TableSize = { columns: number; rows: number };
export type TablePopupProps = { export type TablePopupProps = {
onClose: (size: TableSize) => void; onInsertTable: (size: TableSize) => void;
cellSize?: number;
autoExpand?: boolean;
}; };
export function TablePopup(props: TablePopupProps) { export function TablePopup(props: TablePopupProps) {
const { onClose } = props; const { onInsertTable, cellSize, autoExpand } = props;
const [cellLocation, setCellLocation] = useState<CellLocation>({ const [cellLocation, setCellLocation] = useState<CellLocation>({
column: 0, column: 0,
row: 0, row: 0,
@@ -25,6 +30,7 @@ export function TablePopup(props: TablePopupProps) {
}); });
useEffect(() => { useEffect(() => {
if (!autoExpand) return;
setTableSize((old) => { setTableSize((old) => {
const { columns, rows } = old; const { columns, rows } = old;
const { column, row } = cellLocation; const { column, row } = cellLocation;
@@ -43,50 +49,109 @@ export function TablePopup(props: TablePopupProps) {
: Math.min(old.rows + rowFactor, MAX_ROWS), : Math.min(old.rows + rowFactor, MAX_ROWS),
}; };
}); });
}, [cellLocation]); }, [cellLocation, autoExpand]);
return ( return (
<Flex sx={{ p: 1, flexDirection: "column", alignItems: "center" }}> <Popup
<Box title="Insert table"
sx={{ action={{
display: "grid", icon: "check",
gridTemplateColumns: "1fr ".repeat(tableSize.columns), onClick: () => {
gap: "3px", onInsertTable({
bg: "background", columns: cellLocation.column,
}} rows: cellLocation.row,
> });
{Array(tableSize.columns * tableSize.rows) },
.fill(0) }}
.map((_, index) => ( >
<Box <Flex sx={{ p: 1, flexDirection: "column", alignItems: "center" }}>
width={15} <Box
height={15} sx={{
sx={{ display: "grid",
border: "1px solid var(--disabled)", gridTemplateColumns: `repeat(${tableSize.columns}, minmax(${
borderRadius: "2px", cellSize || 15
bg: isCellHighlighted(index, cellLocation, tableSize) }px, 1fr))`, // "1fr ".repeat(tableSize.columns),
? "disabled" gap: "3px",
: "transparent", bg: "background",
":hover": { width: "100%",
bg: "disabled", }}
}, onTouchMove={(e) => {
}} const touch = e.touches.item(0);
onMouseEnter={() => { const element = document.elementFromPoint(
setCellLocation(getCellLocation(index, tableSize)); touch.pageX,
}} touch.pageY
onClick={() => { ) as HTMLElement;
onClose({ if (!element) return;
columns: cellLocation.column, const index = element.dataset.index;
rows: cellLocation.row, if (!index) return;
}); setCellLocation(getCellLocation(parseInt(index), tableSize));
}} }}
/> >
))} {Array(tableSize.columns * tableSize.rows)
</Box> .fill(0)
<Text variant={"body"} sx={{ mt: 1 }}> .map((_, index) => (
{cellLocation.column}x{cellLocation.row} <Box
</Text> data-index={index}
</Flex> height={cellSize || 15}
sx={{
border: "1px solid var(--disabled)",
borderRadius: "2px",
bg: isCellHighlighted(index, cellLocation, tableSize)
? "disabled"
: "transparent",
}}
onTouchStart={() => {
setCellLocation(getCellLocation(index, tableSize));
}}
onMouseEnter={() => {
setCellLocation(getCellLocation(index, tableSize));
}}
onClick={() => {
onInsertTable({
columns: cellLocation.column,
rows: cellLocation.row,
});
}}
/>
))}
</Box>
<Flex
sx={{
display: ["flex", "none"],
my: 1,
alignItems: "center",
justifyContent: "center",
}}
>
<Input
placeholder={`${cellLocation.column} columns`}
sx={{ mr: 1 }}
type="number"
value={cellLocation.column}
onChange={(e) => {
setCellLocation((l) => ({
...l,
column: e.target.valueAsNumber || 0,
}));
}}
/>
<Input
placeholder={`${cellLocation.row} rows`}
type="number"
value={cellLocation.row}
onChange={(e) => {
setCellLocation((l) => ({
...l,
row: e.target.valueAsNumber || 0,
}));
}}
/>
</Flex>
<Text variant={"body"} sx={{ mt: 1, display: ["none", "block"] }}>
{cellLocation.column}x{cellLocation.row}
</Text>
</Flex>
</Popup>
); );
} }

View File

@@ -0,0 +1,31 @@
import create from "zustand";
export type ToolbarLocation = "top" | "bottom";
interface ToolbarState {
isMobile: boolean;
setIsMobile: (isMobile: boolean) => void;
toolbarLocation: ToolbarLocation;
setToolbarLocation: (location: ToolbarLocation) => void;
}
export const useToolbarStore = create<ToolbarState>((set) => ({
isMobile: false,
setIsMobile: (isMobile) =>
set((state) => {
state.isMobile = isMobile;
}),
toolbarLocation: "top",
setToolbarLocation: (location) =>
set((state) => {
state.toolbarLocation = location;
}),
}));
export function useToolbarLocation() {
return useToolbarStore((store) => store.toolbarLocation);
}
export function useIsMobile() {
return useToolbarStore((store) => store.isMobile);
}

View File

@@ -8,14 +8,16 @@ import { EditorFloatingMenus } from "./floating-menus";
import { getToolDefinition } from "./tool-definitions"; import { getToolDefinition } from "./tool-definitions";
import { Dropdown } from "./components/dropdown"; import { Dropdown } from "./components/dropdown";
import { ToolButton } from "./components/tool-button"; import { ToolButton } from "./components/tool-button";
import { useContext, useRef, useState } from "react"; import { useContext, useEffect, useRef, useState } from "react";
import { MenuPresenter } from "../components/menu"; import { MenuPresenter } from "../components/menu";
import { Popup } from "./components/popup"; import { Popup } from "./components/popup";
import { ToolbarContext, useToolbarContext } from "./hooks/useToolbarContext";
import { import {
ToolbarContext,
ToolbarLocation, ToolbarLocation,
useToolbarContext, useToolbarLocation,
} from "./hooks/useToolbarContext"; useToolbarStore,
} from "./stores/toolbar-store";
// type Colors = { // type Colors = {
// text: string; // text: string;
// background: string; // background: string;
@@ -31,11 +33,18 @@ type ToolbarDefinition = ToolbarGroupDefinition[];
type ToolbarProps = ThemeConfig & { type ToolbarProps = ThemeConfig & {
editor: Editor | null; editor: Editor | null;
location: ToolbarLocation; location: ToolbarLocation;
isMobile?: boolean;
}; };
export function Toolbar(props: ToolbarProps) { export function Toolbar(props: ToolbarProps) {
const { editor, theme, accent, scale, location } = props; const { editor, theme, accent, scale, location, isMobile } = props;
const themeProperties = useTheme({ accent, theme, scale }); const themeProperties = useTheme({ accent, theme, scale });
const [currentPopup, setCurrentPopup] = useState<string | undefined>(); const [currentPopup, setCurrentPopup] = useState<string | undefined>();
const { setIsMobile, setToolbarLocation } = useToolbarStore();
useEffect(() => {
setIsMobile(isMobile || false);
setToolbarLocation(location);
}, [isMobile, location]);
const tools: ToolbarDefinition = [ const tools: ToolbarDefinition = [
["insertBlock"], ["insertBlock"],
@@ -63,7 +72,10 @@ export function Toolbar(props: ToolbarProps) {
return ( return (
<ThemeProvider theme={themeProperties}> <ThemeProvider theme={themeProperties}>
<ToolbarContext.Provider <ToolbarContext.Provider
value={{ setCurrentPopup, currentPopup, toolbarLocation: location }} value={{
setCurrentPopup,
currentPopup,
}}
> >
<Flex <Flex
className="editor-toolbar" className="editor-toolbar"
@@ -112,7 +124,7 @@ function ToolbarGroup(props: ToolbarGroupProps) {
} else { } else {
const Component = findToolById(toolId); const Component = findToolById(toolId);
const toolDefinition = getToolDefinition(toolId); const toolDefinition = getToolDefinition(toolId);
return <Component editor={editor} id={toolId} {...toolDefinition} />; return <Component editor={editor} {...toolDefinition} />;
} }
})} })}
</Flex> </Flex>
@@ -126,8 +138,9 @@ type MoreToolsProps = {
}; };
function MoreTools(props: MoreToolsProps) { function MoreTools(props: MoreToolsProps) {
const { popupId } = props; const { popupId } = props;
const { currentPopup, setCurrentPopup, toolbarLocation } = const { currentPopup, setCurrentPopup } = useToolbarContext();
useToolbarContext(); const toolbarLocation = useToolbarLocation();
const buttonRef = useRef<HTMLButtonElement | null>(); const buttonRef = useRef<HTMLButtonElement | null>();
const show = popupId === currentPopup; const show = popupId === currentPopup;

View File

@@ -3,7 +3,10 @@ import { Editor } from "@tiptap/core";
import { ToolButton } from "../components/tool-button"; import { ToolButton } from "../components/tool-button";
import { ToolId } from "."; import { ToolId } from ".";
import { IconNames, Icons } from "../icons"; import { IconNames, Icons } from "../icons";
import { MenuPresenter } from "../../components/menu/menu"; import {
ActionSheetPresenter,
MenuPresenter,
} from "../../components/menu/menu";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { Dropdown } from "../components/dropdown"; import { Dropdown } from "../components/dropdown";
import { Icon } from "../components/icon"; import { Icon } from "../components/icon";
@@ -12,12 +15,12 @@ import { Popup } from "../components/popup";
import { EmbedPopup } from "../popups/embed-popup"; import { EmbedPopup } from "../popups/embed-popup";
import { TablePopup } from "../popups/table-popup"; import { TablePopup } from "../popups/table-popup";
import { MenuItem } from "../../components/menu/types"; import { MenuItem } from "../../components/menu/types";
import { useToolbarContext } from "../hooks/useToolbarContext"; import { useToolbarLocation } from "../stores/toolbar-store";
export function InsertBlock(props: ToolProps) { export function InsertBlock(props: ToolProps) {
const buttonRef = useRef<HTMLButtonElement | null>(); const buttonRef = useRef<HTMLButtonElement | null>();
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const { toolbarLocation } = useToolbarContext(); const toolbarLocation = useToolbarLocation();
return ( return (
<> <>
@@ -40,7 +43,7 @@ export function InsertBlock(props: ToolProps) {
> >
<Icon path={Icons.plus} size={18} color={"primary"} /> <Icon path={Icons.plus} size={18} color={"primary"} />
</Button> </Button>
<MenuPresenter {/* <MenuPresenter
options={{ options={{
type: "menu", type: "menu",
position: { position: {
@@ -62,6 +65,21 @@ export function InsertBlock(props: ToolProps) {
table(editor), table(editor),
]} ]}
onClose={() => setIsOpen(false)} onClose={() => setIsOpen(false)}
/> */}
<ActionSheetPresenter
title="Choose a block to insert"
isOpen={isOpen}
items={[
tasklist(editor),
horizontalRule(editor),
codeblock(editor),
blockquote(editor),
imageActionSheet(editor),
attachment(editor),
embedActionSheet(editor),
tableActionSheet(editor),
]}
onClose={() => setIsOpen(false)}
/> />
</> </>
); );
@@ -117,6 +135,44 @@ const image = (editor: Editor | null): MenuItem => ({
], ],
}); });
const imageActionSheet = (editor: Editor | null): MenuItem => ({
key: "image",
type: "menuitem",
title: "Image",
icon: "image",
items: [
{
key: "imageOptions",
type: "menuitem",
component: function ({ onClick }) {
const [isOpen, setIsOpen] = useState(true);
return (
<ActionSheetPresenter
isOpen={isOpen}
onClose={() => setIsOpen(false)}
items={[
{
key: "upload-from-disk",
type: "menuitem",
title: "Upload from disk",
icon: "upload",
onClick: () => {},
},
{
key: "upload-from-url",
type: "menuitem",
title: "Attach from URL",
icon: "link",
onClick: () => {},
},
]}
/>
);
},
},
],
});
const embed = (editor: Editor | null): MenuItem => ({ const embed = (editor: Editor | null): MenuItem => ({
key: "embed", key: "embed",
type: "menuitem", type: "menuitem",
@@ -135,7 +191,7 @@ const table = (editor: Editor | null): MenuItem => ({
type: "menuitem", type: "menuitem",
component: (props) => ( component: (props) => (
<TablePopup <TablePopup
onClose={(size) => { onInsertTable={(size) => {
editor editor
?.chain() ?.chain()
.focus() .focus()
@@ -152,6 +208,82 @@ const table = (editor: Editor | null): MenuItem => ({
], ],
}); });
const embedActionSheet = (editor: Editor | null): MenuItem => ({
key: "embed",
type: "menuitem",
title: "Embed",
icon: "embed",
items: [
{
key: "table-size-selector",
type: "menuitem",
component: function ({ onClick }) {
const [isOpen, setIsOpen] = useState(true);
return (
<ActionSheetPresenter
isOpen={isOpen}
onClose={() => setIsOpen(false)}
items={[]}
>
<EmbedPopup
title="Insert embed"
icon="check"
onClose={(embed) => {
editor?.chain().insertEmbed(embed).run();
setIsOpen(false);
onClick?.();
}}
// embed={props}
// onSourceChanged={(src) => {}}
// onSizeChanged={(size) => editor.commands.setEmbedSize(size)}
/>
</ActionSheetPresenter>
);
},
},
],
});
const tableActionSheet = (editor: Editor | null): MenuItem => ({
key: "table",
type: "menuitem",
title: "Table",
icon: "table",
items: [
{
key: "table-size-selector",
type: "menuitem",
component: function ({ onClick }) {
const [isOpen, setIsOpen] = useState(true);
return (
<ActionSheetPresenter
isOpen={isOpen}
onClose={() => setIsOpen(false)}
items={[]}
>
<TablePopup
cellSize={30}
autoExpand={false}
onInsertTable={(size) => {
editor
?.chain()
.focus()
.insertTable({
rows: size.rows,
cols: size.columns,
})
.run();
setIsOpen(false);
onClick?.();
}}
/>
</ActionSheetPresenter>
);
},
},
],
});
const attachment = (editor: Editor | null): MenuItem => ({ const attachment = (editor: Editor | null): MenuItem => ({
key: "attachment", key: "attachment",
type: "menuitem", type: "menuitem",

View File

@@ -3,9 +3,11 @@ import { Dropdown } from "../components/dropdown";
import { ToolId } from "."; import { ToolId } from ".";
import { MenuItem } from "../../components/menu/types"; import { MenuItem } from "../../components/menu/types";
import { Editor } from "@tiptap/core"; import { Editor } from "@tiptap/core";
import { Box, Button, Flex } from "rebass";
import { Slider } from "@rebass/forms";
const defaultFontSizes = [ const defaultFontSizes = [
12, 14, 16, 18, 20, 24, 28, 32, 36, 42, 48, 60, 72, 100, 8, 12, 14, 16, 18, 20, 24, 28, 32, 36, 42, 48, 60, 72, 100,
]; ];
export function FontSize(props: ToolProps) { export function FontSize(props: ToolProps) {
const { editor } = props; const { editor } = props;
@@ -17,13 +19,28 @@ export function FontSize(props: ToolProps) {
return ( return (
<Dropdown <Dropdown
selectedItem={`${currentFontSize}px`} selectedItem={`${currentFontSize}px`}
items={defaultFontSizes.map((size) => ({ items={[
key: `${size}px`, {
type: "menuitem", key: "font-sizes",
title: `${size}px`, type: "menuitem",
isChecked: size === currentFontSize, component: () => (
onClick: () => editor.chain().focus().setFontSize(`${size}px`).run(), <Box
}))} sx={{ display: "grid", gridTemplateColumns: "repeat(5, 1fr)" }}
>
{defaultFontSizes.map((size) => (
<Button variant={"menuitem"}>{size}px</Button>
))}
</Box>
),
},
]}
// items={defaultFontSizes.map((size) => ({
// key: `${size}px`,
// type: "menuitem",
// title: `${size}px`,
// isChecked: size === currentFontSize,
// onClick: () => editor.chain().focus().setFontSize(`${size}px`).run(),
// }))}
menuWidth={100} menuWidth={100}
/> />
); );

View File

@@ -12,11 +12,11 @@ type InlineToolProps = ToolProps & {
onClick: (editor: Editor) => boolean; onClick: (editor: Editor) => boolean;
}; };
function InlineTool(props: InlineToolProps) { function InlineTool(props: InlineToolProps) {
const { editor, title, id, icon, isToggled, onClick } = props; const { editor, title, icon, isToggled, onClick } = props;
return ( return (
<ToolButton <ToolButton
title={title} title={title}
id={id} id={icon}
icon={icon} icon={icon}
onClick={() => onClick(editor)} onClick={() => onClick(editor)}
toggled={isToggled(editor)} toggled={isToggled(editor)}
@@ -107,7 +107,7 @@ export function ClearFormatting(props: ToolProps) {
} }
export function Link(props: ToolProps) { export function Link(props: ToolProps) {
const { editor, id, title, icon } = props; const { editor, title, icon } = props;
const buttonRef = useRef<HTMLButtonElement>(null); const buttonRef = useRef<HTMLButtonElement>(null);
const targetRef = useRef<HTMLElement>(); const targetRef = useRef<HTMLElement>();
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
@@ -119,9 +119,9 @@ export function Link(props: ToolProps) {
return ( return (
<> <>
<ToolButton <ToolButton
id={icon}
ref={buttonRef} ref={buttonRef}
title={title} title={title}
id={id}
icon={icon} icon={icon}
onClick={() => { onClick={() => {
if (isEditing) setHref(currentUrl); if (isEditing) setHref(currentUrl);

View File

@@ -1,10 +1,8 @@
import { Editor } from "@tiptap/core"; import { Editor } from "@tiptap/core";
import { IconNames } from "./icons"; import { IconNames } from "./icons";
import { ToolId } from "./tools";
export type ToolProps = ToolDefinition & { export type ToolProps = ToolDefinition & {
editor: Editor; editor: Editor;
id: ToolId;
}; };
export type ToolDefinition = { export type ToolDefinition = {

View File

@@ -0,0 +1,5 @@
export function getToolbarElement() {
return (
(document.querySelector(".editor-toolbar") as HTMLElement) || undefined
);
}

View File

@@ -80,12 +80,19 @@ What's next:
## Optimize toolbar & editor UI for mobile ## Optimize toolbar & editor UI for mobile
1. Refactor tools to be more easily configurable 1. Refactor tools to be more easily configurable (partially done)
2. Implement sub groups in toolbar 2. Implement sub groups in toolbar (done)
3. Move all popups to /popups directory 3. Move all popups to /popups directory (partially done)
4. Implement mobile positioning logic in menu/popup presenter 4. Implement mobile positioning logic in menu/popup presenter (done)
5. Add support for repositioning toolbar (top/bottom) 5. Add support for repositioning toolbar (top/bottom) (done)
6. Move all popups to be shown as bottom sheets on mobile 6. Move all popups to be shown as bottom sheets on mobile (partially done)
7. Figure out how to make interactive widgets selectable in editor (e.g. iframe & table)
8. Create popup header for use in action sheet (done)
1. Header contains title & action(s)
9. Implement logic to open inline popups (image/cell properties) as an action sheet (done)
10. Open search replace popup as action sheet (done)
11. Improve font size menu (done)
12. Move table context toolbars to bottom (done)
### Refactor tools ### Refactor tools