UNPKG

prosemirror-slash-menu-react

Version:
388 lines (379 loc) 21.9 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var React = require('react'); var prosemirrorSlashMenu = require('prosemirror-slash-menu'); var reactPopper = require('react-popper'); var prosemirrorCommands = require('prosemirror-commands'); function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } var React__default = /*#__PURE__*/_interopDefaultLegacy(React); const ListItem = ({ menuState, el, view, Icon, idx, clickable, RightIcon }) => { React.useEffect(() => { const element = document.getElementById(el.id); if (!element) return; if (el.id === menuState.selected) { element.classList.add("menu-element-selected"); return; } if (element.classList.contains("menu-element-selected")) { element.classList.remove("menu-element-selected"); } }, [menuState.selected]); const handleOnClick = React.useCallback(() => { if (el.type === "command") { el.command(view); prosemirrorSlashMenu.dispatchWithMeta(view, prosemirrorSlashMenu.SlashMenuKey, { type: prosemirrorSlashMenu.SlashMetaTypes.execute, }); } if (el.type === "submenu") { prosemirrorSlashMenu.dispatchWithMeta(view, prosemirrorSlashMenu.SlashMenuKey, { type: prosemirrorSlashMenu.SlashMetaTypes.openSubMenu, element: el, }); } }, [el]); return (React__default["default"].createElement("div", { className: clickable ? "menu-element-wrapper-clickable" : "menu-element-wrapper", id: el.id, key: `${el.id}-${idx}`, onClick: handleOnClick }, Icon ? (React__default["default"].createElement("div", { className: "menu-element-icon" }, React__default["default"].createElement(Icon, null))) : null, React__default["default"].createElement("div", { className: "menu-element-label" }, el.label), RightIcon ? (React__default["default"].createElement("div", { className: "menu-element-right-icon" }, React__default["default"].createElement(RightIcon, null))) : null)); }; exports.Icons = void 0; (function (Icons) { Icons["HeaderMenu"] = "HeaderMenu"; Icons["Level1"] = "Level1"; Icons["Level2"] = "Level2"; Icons["Level3"] = "Level3"; Icons["Bold"] = "Bold"; Icons["Italic"] = "Italic"; Icons["Link"] = "Link"; Icons["Code"] = "Code"; })(exports.Icons || (exports.Icons = {})); const H1Command = { id: exports.Icons.Level1, label: "H1", type: "command", command: (view) => { prosemirrorCommands.setBlockType(view.state.schema.nodes.heading, { level: 1 })(view.state, view.dispatch, view); }, available: (view) => true, }; const H2Command = { id: exports.Icons.Level2, label: "H2", type: "command", command: (view) => { prosemirrorCommands.setBlockType(view.state.schema.nodes.heading, { level: 2 })(view.state, view.dispatch, view); }, available: (view) => true, }; const H3Command = { id: exports.Icons.Level3, label: "H3", type: "command", command: (view) => { prosemirrorCommands.setBlockType(view.state.schema.nodes.heading, { level: 3 })(view.state, view.dispatch, view); }, available: (view) => true, }; const BoldCommand = { id: exports.Icons.Bold, label: "Bold", type: "command", command: (view) => { const markType = view.state.schema.marks.strong; prosemirrorCommands.toggleMark(markType)(view.state, view.dispatch, view); }, available: (view) => true, }; const ItalicCommand = { id: exports.Icons.Italic, label: "Italic", type: "command", command: (view) => { const markType = view.state.schema.marks.em; prosemirrorCommands.toggleMark(markType)(view.state, view.dispatch, view); }, available: (view) => true, }; const CodeCommand = { id: exports.Icons.Code, label: "Code", type: "command", command: (view) => { const markType = view.state.schema.marks.code; prosemirrorCommands.toggleMark(markType)(view.state, view.dispatch, view); }, available: (view) => true, }; const LinkCommand = { id: exports.Icons.Link, label: "Link", type: "command", command: (view) => { const markType = view.state.schema.marks.link; prosemirrorCommands.toggleMark(markType)(view.state, view.dispatch, view); }, available: (view) => true, }; const HeadingsMenu = { id: exports.Icons.HeaderMenu, label: "Headings", type: "submenu", available: (view) => true, elements: [H1Command, H2Command, H3Command], }; const defaultElements = [ HeadingsMenu, BoldCommand, ItalicCommand, CodeCommand, LinkCommand, ]; const H1Icon = () => (React__default["default"].createElement("svg", { width: "36", height: "36", viewBox: "0 0 36 36", fill: "none", xmlns: "http://www.w3.org/2000/svg" }, React__default["default"].createElement("path", { d: "M17 17H11V12C11 11.4477 10.5523 11 10 11C9.44772 11 9 11.4477 9 12V24C9 24.5523 9.44772 25 10 25C10.5523 25 11 24.5523 11 24V19H17V24C17 24.5523 17.4477 25 18 25C18.5523 25 19 24.5523 19 24V12C19 11.4477 18.5523 11 18 11C17.4477 11 17 11.4477 17 12V17Z", fill: "#050038" }), React__default["default"].createElement("path", { d: "M26 25C26.5523 25 27 24.5523 27 24V12C27 11.4477 26.5523 11 26 11H23C22.4477 11 22 11.4477 22 12C22 12.5523 22.4477 13 23 13H25V24C25 24.5523 25.4477 25 26 25Z", fill: "#050038" }))); const H2Icon = () => (React__default["default"].createElement("svg", { width: "36", height: "36", viewBox: "0 0 36 36", fill: "none", xmlns: "http://www.w3.org/2000/svg" }, React__default["default"].createElement("path", { d: "M16 17H11V13C11 12.4477 10.5523 12 10 12C9.44772 12 9 12.4477 9 13V23C9 23.5523 9.44772 24 10 24C10.5523 24 11 23.5523 11 23V19H16V23C16 23.5523 16.4477 24 17 24C17.5523 24 18 23.5523 18 23V13C18 12.4477 17.5523 12 17 12C16.4477 12 16 12.4477 16 13V17Z", fill: "#050038" }), React__default["default"].createElement("path", { d: "M26 22H22V19H26C26.5523 19 27 18.5523 27 18V13C27 12.4477 26.5523 12 26 12H21C20.4477 12 20 12.4477 20 13C20 13.5523 20.4477 14 21 14H25V17H21C20.4477 17 20 17.4477 20 18V23C20 23.5523 20.4477 24 21 24H26C26.5523 24 27 23.5523 27 23C27 22.4477 26.5523 22 26 22Z", fill: "#050038" }))); const H3Icon = () => (React__default["default"].createElement("svg", { width: "36", height: "36", viewBox: "0 0 36 36", fill: "none", xmlns: "http://www.w3.org/2000/svg" }, React__default["default"].createElement("path", { d: "M12 17H16V14C16 13.4477 16.4477 13 17 13C17.5523 13 18 13.4477 18 14V22C18 22.5523 17.5523 23 17 23C16.4477 23 16 22.5523 16 22V19H12V22C12 22.5523 11.5523 23 11 23C10.4477 23 10 22.5523 10 22V14C10 13.4477 10.4477 13 11 13C11.5523 13 12 13.4477 12 14V17Z", fill: "#050038" }), React__default["default"].createElement("path", { d: "M20 14C20 13.4477 20.4477 13 21 13H25C25.5523 13 26 13.4477 26 14V22C26 22.5523 25.5523 23 25 23H21C20.4477 23 20 22.5523 20 22C20 21.4477 20.4477 21 21 21H24V19H21C20.4477 19 20 18.5523 20 18C20 17.4477 20.4477 17 21 17H24V15H21C20.4477 15 20 14.5523 20 14Z", fill: "#050038" }))); const ItalicIcon = () => (React__default["default"].createElement("svg", { width: "25", height: "24", viewBox: "0 0 25 24", fill: "none", xmlns: "http://www.w3.org/2000/svg" }, React__default["default"].createElement("path", { d: "M8.38197 18L14.382 6H11.5C10.9477 6 10.5 5.55228 10.5 5C10.5 4.44772 10.9477 4 11.5 4H19.5C20.0523 4 20.5 4.44772 20.5 5C20.5 5.55228 20.0523 6 19.5 6H16.618L10.618 18H13.5C14.0523 18 14.5 18.4477 14.5 19C14.5 19.5523 14.0523 20 13.5 20H5.5C4.94772 20 4.5 19.5523 4.5 19C4.5 18.4477 4.94772 18 5.5 18H8.38197Z", fill: "#050038" }))); const BoldIcon = () => (React__default["default"].createElement("svg", { width: "25", height: "24", viewBox: "0 0 25 24", fill: "none", xmlns: "http://www.w3.org/2000/svg" }, React__default["default"].createElement("path", { fillRule: "evenodd", clipRule: "evenodd", d: "M19.5 15C19.5 17.7614 17.2614 20 14.5 20H8.5C7.94772 20 7.5 19.5523 7.5 19V5C7.5 4.44772 7.94772 4 8.5 4H14C16.4853 4 18.5 6.01472 18.5 8.5C18.5 9.4786 18.1876 10.3842 17.6572 11.1226C18.7818 12.0395 19.5 13.4359 19.5 15ZM10.5 10H14C14.8284 10 15.5 9.32843 15.5 8.5C15.5 7.67157 14.8284 7 14 7H10.5V10ZM10.5 17V13H14.5C15.6046 13 16.5 13.8954 16.5 15C16.5 16.1046 15.6046 17 14.5 17H10.5Z", fill: "#050038" }))); const ArrowLeft = () => (React__default["default"].createElement("svg", { xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className: "feather feather-arrow-left" }, React__default["default"].createElement("line", { x1: "19", y1: "12", x2: "5", y2: "12" }), React__default["default"].createElement("polyline", { points: "12 19 5 12 12 5" }))); const ArrowRight = () => (React__default["default"].createElement("svg", { xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className: "feather feather-arrow-right" }, React__default["default"].createElement("line", { x1: "5", y1: "12", x2: "19", y2: "12" }), React__default["default"].createElement("polyline", { points: "12 5 19 12 12 19" }))); const CodeIcon = () => (React__default["default"].createElement("svg", { xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className: "feather feather-code" }, React__default["default"].createElement("polyline", { points: "16 18 22 12 16 6" }), React__default["default"].createElement("polyline", { points: "8 6 2 12 8 18" }))); const LinkIcon = () => (React__default["default"].createElement("svg", { xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className: "feather feather-link-2" }, React__default["default"].createElement("path", { d: "M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3" }), React__default["default"].createElement("line", { x1: "8", y1: "12", x2: "16", y2: "12" }))); const defaultIcons = { H1Icon, H2Icon, H3Icon, LinkIcon, BoldIcon, CodeIcon, ItalicIcon, ArrowLeft, ArrowRight, }; exports.Placement = void 0; (function (Placement) { Placement["auto"] = "auto"; Placement["autoStart"] = "auto-start"; Placement["autoEnd"] = "auto-end"; Placement["top"] = "top"; Placement["topStart"] = "top-start"; Placement["topEnd"] = "top-end"; Placement["bottom"] = "bottom"; Placement["bottomStart"] = "bottom-start"; Placement["bottomEnd"] = "bottom-end"; Placement["right"] = "right"; Placement["rightStart"] = "right-start"; Placement["rightEnd"] = "right-end"; Placement["left"] = "left"; Placement["leftStart"] = "left-start"; Placement["leftEnd"] = "left-end"; })(exports.Placement || (exports.Placement = {})); const SlashMenuReact = ({ editorState, editorView, icons, rightIcons, subMenuIcon, filterFieldIcon, filterPlaceHolder, mainMenuLabel, popperReference, popperOptions, clickable, disableInput, emptySearchComponent, }) => { var _a; const menuState = React.useMemo(() => { if (!editorState) return; return prosemirrorSlashMenu.SlashMenuKey.getState(editorState); }, [editorState]); const elements = React.useMemo(() => { if (!menuState) return; return menuState.filteredElements; }, [menuState]); const rootRef = React.useRef(null); React.useEffect(() => { if (!rootRef) return; const outsideClickHandler = (event) => { if (rootRef.current && (!event.target || !(event.target instanceof Node) || !rootRef.current.contains(event === null || event === void 0 ? void 0 : event.target))) { prosemirrorSlashMenu.dispatchWithMeta(editorView, prosemirrorSlashMenu.SlashMenuKey, { type: prosemirrorSlashMenu.SlashMetaTypes.close, }); } }; document.addEventListener("mousedown", outsideClickHandler); return () => { document.removeEventListener("mousedown", outsideClickHandler); }; }, [rootRef]); const [popperElement, setPopperElement] = React__default["default"].useState(null); const virtualReference = React.useMemo(() => { var _a; const domNode = (_a = editorView.domAtPos(editorState.selection.to)) === null || _a === void 0 ? void 0 : _a.node; const cursorPosition = editorView.state.selection.to; const cursorLeft = editorView.coordsAtPos(cursorPosition).left; if (!(domNode instanceof HTMLElement)) return; const { top, height } = domNode.getBoundingClientRect(); return { getBoundingClientRect() { return { top: top, right: cursorLeft, bottom: top, left: cursorLeft, width: 0, height: height, x: cursorLeft, y: top, toJSON: () => JSON.stringify({ top: top, right: cursorLeft, bottom: top, left: cursorLeft, width: 0, height: height, x: cursorLeft, y: top, }), }; }, }; }, [editorView, editorState, window.scrollY]); const offsetModifier = React.useMemo(() => { return { name: "offset", options: { offset: [0, 36], }, }; }, [popperReference]); const { styles, attributes } = reactPopper.usePopper(popperReference || virtualReference, popperElement, { placement: (popperOptions === null || popperOptions === void 0 ? void 0 : popperOptions.placement) ? popperOptions.placement : exports.Placement.bottomStart, modifiers: [ { name: "flip", enabled: true }, { name: "preventOverflow", }, (popperOptions === null || popperOptions === void 0 ? void 0 : popperOptions.offsetModifier) ? popperOptions.offsetModifier : offsetModifier, ], }); React.useEffect(() => { if (!menuState) return; const element = document.getElementById(menuState.selected); if (!element || !rootRef.current) return; const isTopElement = menuState.selected === menuState.filteredElements[0].id; if (isTopElement) { rootRef.current.scrollTop = 0; return; } const height = element.clientHeight + parseInt(window.getComputedStyle(element).getPropertyValue("margin-top")) + parseInt(window.getComputedStyle(element).getPropertyValue("margin-bottom")) + parseInt(window.getComputedStyle(element).getPropertyValue("padding-top")) + parseInt(window.getComputedStyle(element).getPropertyValue("padding-bottom")); const { bottom, top } = element.getBoundingClientRect(); const containerRect = rootRef.current.getBoundingClientRect(); const scrollUp = top - height < containerRect.top; const visible = scrollUp ? top - containerRect.top > height : !(bottom > containerRect.bottom); if (!visible) { if (scrollUp) { rootRef.current.scrollTop = element.offsetTop - height / 2; } else { rootRef.current.scrollTop = element.offsetTop - containerRect.height + height + height / 4; } } }, [menuState]); React.useEffect(() => { if (rootRef.current === null) { return; } rootRef.current.scrollTop = 0; }, [menuState === null || menuState === void 0 ? void 0 : menuState.filteredElements]); const subMenuLabel = React.useMemo(() => { var _a; if (menuState === null || menuState === void 0 ? void 0 : menuState.subMenuId) { return (_a = prosemirrorSlashMenu.getElementById(menuState.subMenuId, menuState)) === null || _a === void 0 ? void 0 : _a.label; } }, [menuState]); const closeSubMenu = React.useCallback(() => { if (menuState === null || menuState === void 0 ? void 0 : menuState.subMenuId) { prosemirrorSlashMenu.dispatchWithMeta(editorView, prosemirrorSlashMenu.SlashMenuKey, { type: prosemirrorSlashMenu.SlashMetaTypes.closeSubMenu, element: prosemirrorSlashMenu.getElementById(menuState.subMenuId, menuState), }); } }, [menuState]); // These two useEffects prevent a bug where the user navigates with clicks, which then blurs the editor and key presses stop working React.useEffect(() => { editorView.focus(); }, [menuState === null || menuState === void 0 ? void 0 : menuState.open]); React.useEffect(() => { editorView.focus(); }, [menuState === null || menuState === void 0 ? void 0 : menuState.subMenuId]); const groups = React.useMemo(() => { if (!elements) { return; } return elements.reduce((acc, el) => { if (el.group) { acc[el.group] = acc[el.group] || []; acc[el.group].push(el); } else { acc[""] = acc[""] || []; acc[""].push(el); } return acc; }, {}); }, [elements]); return (React__default["default"].createElement(React__default["default"].Fragment, null, (menuState === null || menuState === void 0 ? void 0 : menuState.open) ? (React__default["default"].createElement("div", Object.assign({ // @ts-ignore ref: setPopperElement, style: Object.assign({}, styles.popper) }, attributes.popper), disableInput ? null : menuState.filter ? (React__default["default"].createElement("div", { className: "menu-filter-wrapper" }, filterFieldIcon ? (React__default["default"].createElement("div", { className: "menu-filter-icon" }, filterFieldIcon)) : null, React__default["default"].createElement("div", { id: "menu-filter", className: "menu-filter" }, menuState.filter))) : (React__default["default"].createElement("div", { className: "menu-filter-wrapper" }, filterFieldIcon ? (React__default["default"].createElement("div", { className: "menu-filter-icon" }, filterFieldIcon)) : null, React__default["default"].createElement("div", { className: "menu-filter-placeholder" }, filterPlaceHolder))), React__default["default"].createElement("div", { id: "slashDisplay", ref: rootRef, className: "menu-display-root" }, menuState.subMenuId ? (React__default["default"].createElement("div", { className: "menu-element-wrapper", onClick: clickable ? closeSubMenu : undefined, style: { cursor: clickable ? "pointer" : undefined } }, React__default["default"].createElement("div", { className: "menu-element-icon-left" }, subMenuIcon || defaultIcons.ArrowLeft()), React__default["default"].createElement("div", { className: "submenu-label" }, subMenuLabel))) : null, !menuState.subMenuId && mainMenuLabel ? (React__default["default"].createElement("div", { className: "menu-element-wrapper" }, React__default["default"].createElement("div", { className: "submenu-label" }, mainMenuLabel))) : null, groups && ((_a = Object.keys(groups)) === null || _a === void 0 ? void 0 : _a.length) > 1 ? Object.keys(groups).map((group, x) => { return (React__default["default"].createElement("div", { key: x, className: "group-wrapper" }, React__default["default"].createElement("div", { className: "group-label" }, group), groups === null || groups === void 0 ? void 0 : groups[group].map((el, idx) => (React__default["default"].createElement(ListItem, { key: el.id, menuState: menuState, Icon: icons === null || icons === void 0 ? void 0 : icons[el.id], RightIcon: rightIcons === null || rightIcons === void 0 ? void 0 : rightIcons[el.id], idx: idx, clickable: clickable, el: el, view: editorView }))))); }) : elements === null || elements === void 0 ? void 0 : elements.map((el, idx) => (React__default["default"].createElement(ListItem, { key: el.id, menuState: menuState, Icon: icons === null || icons === void 0 ? void 0 : icons[el.id], RightIcon: rightIcons === null || rightIcons === void 0 ? void 0 : rightIcons[el.id], idx: idx, clickable: clickable, el: el, view: editorView }))), (elements === null || elements === void 0 ? void 0 : elements.length) === 0 ? emptySearchComponent || (React__default["default"].createElement("div", { className: "menu-placeholder" }, "No matching items")) : null))) : null)); }; exports.SlashMenuReact = SlashMenuReact; exports.defaultElements = defaultElements; exports.defaultIcons = defaultIcons; //# sourceMappingURL=index.js.map