From f9fdf1f92075a89311cadec25d1894381c2be01f Mon Sep 17 00:00:00 2001 From: thecodrr Date: Sat, 25 Jun 2022 10:23:39 +0500 Subject: [PATCH] feat: add hover popup for link --- packages/editor/dist/components/button.d.ts | 2 +- .../editor/dist/components/menu/usefocus.d.ts | 1 - .../dist/components/popuppresenter/index.d.ts | 2 +- .../dist/components/popuppresenter/index.js | 2 + packages/editor/dist/extensions.d.ts | 1 - packages/editor/dist/extensions.js | 1 - packages/editor/dist/extensions/link/link.js | 108 +++++-- packages/editor/dist/index.js | 2 +- .../dist/toolbar/components/toolbargroup.d.ts | 3 + .../dist/toolbar/components/toolbargroup.js | 4 +- .../dist/toolbar/components/toolbutton.d.ts | 4 +- .../toolbar/floatingmenus/hover/handler.d.ts | 5 + .../toolbar/floatingmenus/hover/handler.js | 25 ++ .../toolbar/floatingmenus/hover/link.d.ts | 2 + .../dist/toolbar/floatingmenus/hover/link.js | 4 + .../floatingmenus/hoverpopup/index.d.ts | 8 + .../toolbar/floatingmenus/hoverpopup/index.js | 77 +++++ .../floatingmenus/hoverpopup/link.d.ts | 7 + .../toolbar/floatingmenus/hoverpopup/link.js | 17 ++ .../dist/toolbar/floatingmenus/index.js | 5 +- .../dist/toolbar/floatingmenus/types.d.ts | 2 +- packages/editor/dist/toolbar/icons.d.ts | 2 + packages/editor/dist/toolbar/icons.js | 4 +- packages/editor/dist/toolbar/toolbar.js | 4 +- .../editor/dist/toolbar/tooldefinitions.js | 21 +- packages/editor/dist/toolbar/tools/index.d.ts | 10 +- packages/editor/dist/toolbar/tools/index.js | 10 +- .../editor/dist/toolbar/tools/inline.d.ts | 2 - packages/editor/dist/toolbar/tools/inline.js | 75 +---- packages/editor/dist/toolbar/tools/link.d.ts | 7 + packages/editor/dist/toolbar/tools/link.js | 149 ++++++++++ packages/editor/dist/toolbar/types.d.ts | 3 + .../dist/toolbar/utils/prosemirror.d.ts | 10 +- .../editor/dist/toolbar/utils/prosemirror.js | 8 + packages/editor/dist/utils/position.js | 11 +- packages/editor/package-lock.json | 264 ++++++++++++++---- packages/editor/package.json | 1 + .../src/components/popup-presenter/index.tsx | 3 + packages/editor/src/extensions.ts | 1 - packages/editor/src/extensions/link/index.ts | 5 - packages/editor/src/extensions/link/link.ts | 25 -- packages/editor/src/index.ts | 4 +- .../src/toolbar/components/toolbar-group.tsx | 7 +- .../floating-menus/hover-popup/index.tsx | 95 +++++++ .../floating-menus/hover-popup/link.tsx | 31 ++ .../src/toolbar/floating-menus/index.tsx | 2 + .../src/toolbar/floating-menus/types.ts | 2 +- packages/editor/src/toolbar/icons.ts | 4 + .../editor/src/toolbar/tool-definitions.ts | 21 +- packages/editor/src/toolbar/toolbar.tsx | 4 +- packages/editor/src/toolbar/tools/index.ts | 10 +- packages/editor/src/toolbar/tools/inline.tsx | 155 ---------- packages/editor/src/toolbar/tools/link.tsx | 203 ++++++++++++++ packages/editor/src/toolbar/types.ts | 3 + .../editor/src/toolbar/utils/prosemirror.ts | 19 +- packages/editor/src/utils/position.ts | 13 +- 56 files changed, 1092 insertions(+), 378 deletions(-) create mode 100644 packages/editor/dist/toolbar/floatingmenus/hover/handler.d.ts create mode 100644 packages/editor/dist/toolbar/floatingmenus/hover/handler.js create mode 100644 packages/editor/dist/toolbar/floatingmenus/hover/link.d.ts create mode 100644 packages/editor/dist/toolbar/floatingmenus/hover/link.js create mode 100644 packages/editor/dist/toolbar/floatingmenus/hoverpopup/index.d.ts create mode 100644 packages/editor/dist/toolbar/floatingmenus/hoverpopup/index.js create mode 100644 packages/editor/dist/toolbar/floatingmenus/hoverpopup/link.d.ts create mode 100644 packages/editor/dist/toolbar/floatingmenus/hoverpopup/link.js create mode 100644 packages/editor/dist/toolbar/tools/link.d.ts create mode 100644 packages/editor/dist/toolbar/tools/link.js delete mode 100644 packages/editor/src/extensions/link/index.ts delete mode 100644 packages/editor/src/extensions/link/link.ts create mode 100644 packages/editor/src/toolbar/floating-menus/hover-popup/index.tsx create mode 100644 packages/editor/src/toolbar/floating-menus/hover-popup/link.tsx create mode 100644 packages/editor/src/toolbar/tools/link.tsx diff --git a/packages/editor/dist/components/button.d.ts b/packages/editor/dist/components/button.d.ts index ccd1c4605..f118730d4 100644 --- a/packages/editor/dist/components/button.d.ts +++ b/packages/editor/dist/components/button.d.ts @@ -1,3 +1,3 @@ /// import { ButtonProps } from "rebass"; -export declare const Button: import("react").ForwardRefExoticComponent & import("react").RefAttributes>; +export declare const Button: import("react").ForwardRefExoticComponent & import("react").RefAttributes>; diff --git a/packages/editor/dist/components/menu/usefocus.d.ts b/packages/editor/dist/components/menu/usefocus.d.ts index 36b4ee2ca..66f324efc 100644 --- a/packages/editor/dist/components/menu/usefocus.d.ts +++ b/packages/editor/dist/components/menu/usefocus.d.ts @@ -1,4 +1,3 @@ -/// import { MenuItem } from "./types"; export declare function useFocus(items: MenuItem[], onAction: (event: KeyboardEvent) => void, onClose: (event: KeyboardEvent) => void): { focusIndex: number; diff --git a/packages/editor/dist/components/popuppresenter/index.d.ts b/packages/editor/dist/components/popuppresenter/index.d.ts index 56597fe8d..14ae76e5d 100644 --- a/packages/editor/dist/components/popuppresenter/index.d.ts +++ b/packages/editor/dist/components/popuppresenter/index.d.ts @@ -26,5 +26,5 @@ declare type ShowPopupOptions = { theme: Theme; popup: (closePopup: () => void) => React.ReactNode; } & Partial; -export declare function showPopup(options: ShowPopupOptions): void; +export declare function showPopup(options: ShowPopupOptions): () => void; export {}; diff --git a/packages/editor/dist/components/popuppresenter/index.js b/packages/editor/dist/components/popuppresenter/index.js index cac9e03de..63a438a54 100644 --- a/packages/editor/dist/components/popuppresenter/index.js +++ b/packages/editor/dist/components/popuppresenter/index.js @@ -43,6 +43,7 @@ function _PopupPresenter(props) { var popupPosition = getPosition(popup, position); popup.style.top = popupPosition.top + "px"; popup.style.left = popupPosition.left + "px"; + console.log("popup", popupPosition); }, [position]); useEffect(function () { repositionPopup(); @@ -216,4 +217,5 @@ export function showPopup(options) { align: "end", yOffset: 10, }, blocking: true, focusOnRender: true }, props, { children: popup(hide) })) })), getPopupContainer()); + return hide; } diff --git a/packages/editor/dist/extensions.d.ts b/packages/editor/dist/extensions.d.ts index 27031ae24..04757fdce 100644 --- a/packages/editor/dist/extensions.d.ts +++ b/packages/editor/dist/extensions.d.ts @@ -25,7 +25,6 @@ import "./extensions/search-replace"; import "./extensions/embed"; import "./extensions/code-block"; import "./extensions/list-item"; -import "./extensions/link"; import "./extensions/outline-list"; import "./extensions/outline-list-item"; import "./extensions/table"; diff --git a/packages/editor/dist/extensions.js b/packages/editor/dist/extensions.js index 27031ae24..04757fdce 100644 --- a/packages/editor/dist/extensions.js +++ b/packages/editor/dist/extensions.js @@ -25,7 +25,6 @@ import "./extensions/search-replace"; import "./extensions/embed"; import "./extensions/code-block"; import "./extensions/list-item"; -import "./extensions/link"; import "./extensions/outline-list"; import "./extensions/outline-list-item"; import "./extensions/table"; diff --git a/packages/editor/dist/extensions/link/link.js b/packages/editor/dist/extensions/link/link.js index 35ac3919e..64bb22597 100644 --- a/packages/editor/dist/extensions/link/link.js +++ b/packages/editor/dist/extensions/link/link.js @@ -1,23 +1,91 @@ +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 } from "react/jsx-runtime"; import TiptapLink from "@tiptap/extension-link"; +import { Plugin, PluginKey } from "prosemirror-state"; +import { showPopup } from "../../components/popup-presenter"; +import { ToolbarGroup } from "../../toolbar/components/toolbar-group"; +var linkHoverPluginKey = new PluginKey("linkHover"); export var Link = TiptapLink.extend({ -// addProseMirrorPlugins() { -// return [ -// ...(this.parent?.() || []), -// new Plugin({ -// key: new PluginKey("hoverHandler"), -// props: { -// handleDOMEvents: { -// mouseover: (view, event) => { -// if ( -// event.target instanceof HTMLElement && -// event.target.nodeName === "A" -// ) { -// console.log("Got it!"); -// } -// }, -// }, -// }, -// }), -// ]; -// }, + addProseMirrorPlugins: function () { + var _this = this; + var _a; + var linkRef = null; + return __spreadArray(__spreadArray([], __read((((_a = this.parent) === null || _a === void 0 ? void 0 : _a.call(this)) || [])), false), [ + new Plugin({ + key: linkHoverPluginKey, + props: { + handleDOMEvents: { + mouseover: function (view, event) { + var _a; + if (event.target instanceof HTMLElement && + ((_a = event.target) === null || _a === void 0 ? void 0 : _a.classList.contains("ProseMirror"))) { + return; + } + if (event.target instanceof HTMLElement && + event.target.nodeName === "A") { + if (linkRef) + return; + var pos_1 = view.posAtDOM(event.target, 0); + var node_1 = view.state.doc.nodeAt(pos_1); + console.log(node_1, pos_1); + if (!(node_1 === null || node_1 === void 0 ? void 0 : node_1.isText) || + node_1.marks.length <= 0 || + !node_1.marks.some(function (mark) { return mark.type === _this.type; })) + return; + linkRef = showPopup({ + popup: function () { return (_jsx(ToolbarGroup, { force: true, tools: ["editLink", "removeLink", "openLink"], editor: _this.editor, selectedNode: { + node: node_1, + from: pos_1, + to: pos_1 + node_1.nodeSize, + }, sx: { + bg: "background", + boxShadow: "menu", + borderRadius: "default", + p: 1, + } })); }, + theme: _this.editor.storage.theme, + blocking: false, + focusOnRender: false, + position: { + target: event.target, + align: "center", + location: "top", + isTargetAbsolute: true, + }, + }); + } + else if (linkRef) { + linkRef(); + linkRef = null; + } + }, + }, + }, + }), + ], false); + }, }); diff --git a/packages/editor/dist/index.js b/packages/editor/dist/index.js index e7f3fd760..f330d0604 100644 --- a/packages/editor/dist/index.js +++ b/packages/editor/dist/index.js @@ -52,7 +52,7 @@ import { SearchReplace } from "./extensions/search-replace"; import { EmbedNode } from "./extensions/embed"; import { CodeBlock } from "./extensions/code-block"; import { ListItem } from "./extensions/list-item"; -import { Link } from "./extensions/link"; +import { Link } from "@tiptap/extension-link"; import { NodeViewSelectionNotifier, usePortalProvider, } from "./extensions/react"; import { OutlineList } from "./extensions/outline-list"; import { OutlineListItem } from "./extensions/outline-list-item"; diff --git a/packages/editor/dist/toolbar/components/toolbargroup.d.ts b/packages/editor/dist/toolbar/components/toolbargroup.d.ts index 4e13e964c..a76c88195 100644 --- a/packages/editor/dist/toolbar/components/toolbargroup.d.ts +++ b/packages/editor/dist/toolbar/components/toolbargroup.d.ts @@ -2,9 +2,12 @@ import { ToolbarGroupDefinition, ToolButtonVariant } from "../types"; import { FlexProps } from "rebass"; import { Editor } from "@tiptap/core"; +import { NodeWithOffset } from "../utils/prosemirror"; export declare type ToolbarGroupProps = FlexProps & { tools: ToolbarGroupDefinition; editor: Editor; variant?: ToolButtonVariant; + force?: boolean; + selectedNode?: NodeWithOffset; }; export declare function ToolbarGroup(props: ToolbarGroupProps): JSX.Element; diff --git a/packages/editor/dist/toolbar/components/toolbargroup.js b/packages/editor/dist/toolbar/components/toolbargroup.js index f39dce669..b969c71eb 100644 --- a/packages/editor/dist/toolbar/components/toolbargroup.js +++ b/packages/editor/dist/toolbar/components/toolbargroup.js @@ -26,7 +26,7 @@ import { Flex } from "rebass"; import { MoreTools } from "./more-tools"; import { getToolDefinition } from "../tool-definitions"; export function ToolbarGroup(props) { - var tools = props.tools, editor = props.editor, flexProps = __rest(props, ["tools", "editor"]); + var tools = props.tools, editor = props.editor, force = props.force, selectedNode = props.selectedNode, flexProps = __rest(props, ["tools", "editor", "force", "selectedNode"]); return (_jsx(Flex, __assign({ className: "toolbar-group" }, flexProps, { children: tools.map(function (toolId) { if (Array.isArray(toolId)) { return (_jsx(MoreTools, { title: "More", icon: "more", popupId: toolId.join(""), tools: toolId, editor: editor }, "more-tools")); @@ -34,7 +34,7 @@ export function ToolbarGroup(props) { else { var Component = findTool(toolId); var toolDefinition = getToolDefinition(toolId); - return (_jsx(Component, __assign({ editor: editor }, toolDefinition), toolDefinition.title)); + return (_jsx(Component, __assign({ editor: editor, force: force, selectedNode: selectedNode }, toolDefinition), toolDefinition.title)); } }) }))); } diff --git a/packages/editor/dist/toolbar/components/toolbutton.d.ts b/packages/editor/dist/toolbar/components/toolbutton.d.ts index daa747276..9040ee68a 100644 --- a/packages/editor/dist/toolbar/components/toolbutton.d.ts +++ b/packages/editor/dist/toolbar/components/toolbutton.d.ts @@ -14,8 +14,8 @@ export declare type ToolButtonProps = ButtonProps & { }; export declare const ToolButton: React.NamedExoticComponent | undefined; variant?: ToolButtonVariant | undefined; diff --git a/packages/editor/dist/toolbar/floatingmenus/hover/handler.d.ts b/packages/editor/dist/toolbar/floatingmenus/hover/handler.d.ts new file mode 100644 index 000000000..e02fa5f5e --- /dev/null +++ b/packages/editor/dist/toolbar/floatingmenus/hover/handler.d.ts @@ -0,0 +1,5 @@ +import { Editor } from "../../../types"; +export interface ElementHoverHandler { + nodeName: T; + handler: (editor: Editor) => void; +} diff --git a/packages/editor/dist/toolbar/floatingmenus/hover/handler.js b/packages/editor/dist/toolbar/floatingmenus/hover/handler.js new file mode 100644 index 000000000..f72411272 --- /dev/null +++ b/packages/editor/dist/toolbar/floatingmenus/hover/handler.js @@ -0,0 +1,25 @@ +import { useEffect } from "react"; +import { LinkHandler } from "./link"; +var elementHandlers = [LinkHandler]; +function HoverHandler(props) { + var editor = props.editor; + useEffect(function () { + function onMouseOver(e) { + var _a; + if (!(e.target instanceof HTMLElement)) + return; + if ((_a = e.target) === null || _a === void 0 ? void 0 : _a.classList.contains("ProseMirror")) + return; + var nodeName = e.target.nodeName.toLowerCase(); + var handler = elementHandlers.find(function (h) { return h.nodeName === nodeName; }); + if (!handler) + return; + handler.handler(editor); + } + window.addEventListener("mouseover", onMouseOver); + return function () { + window.removeEventListener("mouseover", onMouseOver); + }; + }, []); + return null; +} diff --git a/packages/editor/dist/toolbar/floatingmenus/hover/link.d.ts b/packages/editor/dist/toolbar/floatingmenus/hover/link.d.ts new file mode 100644 index 000000000..13f34fc08 --- /dev/null +++ b/packages/editor/dist/toolbar/floatingmenus/hover/link.d.ts @@ -0,0 +1,2 @@ +import { ElementHoverHandler } from "./handler"; +export declare const LinkHandler: ElementHoverHandler<"a">; diff --git a/packages/editor/dist/toolbar/floatingmenus/hover/link.js b/packages/editor/dist/toolbar/floatingmenus/hover/link.js new file mode 100644 index 000000000..3ce2270bc --- /dev/null +++ b/packages/editor/dist/toolbar/floatingmenus/hover/link.js @@ -0,0 +1,4 @@ +export var LinkHandler = { + nodeName: "a", + handler: function (editor) { }, +}; diff --git a/packages/editor/dist/toolbar/floatingmenus/hoverpopup/index.d.ts b/packages/editor/dist/toolbar/floatingmenus/hoverpopup/index.d.ts new file mode 100644 index 000000000..e23f86eea --- /dev/null +++ b/packages/editor/dist/toolbar/floatingmenus/hoverpopup/index.d.ts @@ -0,0 +1,8 @@ +import { Editor } from "../../../types"; +import { NodeWithOffset } from "../../utils/prosemirror"; +import { FloatingMenuProps } from "../types"; +export declare type HoverPopupProps = { + editor: Editor; + selectedNode: NodeWithOffset; +}; +export declare function HoverPopupHandler(props: FloatingMenuProps): null; diff --git a/packages/editor/dist/toolbar/floatingmenus/hoverpopup/index.js b/packages/editor/dist/toolbar/floatingmenus/hoverpopup/index.js new file mode 100644 index 000000000..242147ed9 --- /dev/null +++ b/packages/editor/dist/toolbar/floatingmenus/hoverpopup/index.js @@ -0,0 +1,77 @@ +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 } from "react/jsx-runtime"; +import { useEffect, useRef } from "react"; +import { showPopup } from "../../../components/popup-presenter"; +import { LinkHoverPopupHandler } from "./link"; +var handlers = __assign({}, LinkHoverPopupHandler); +var HOVER_TIMEOUT = 500; +export function HoverPopupHandler(props) { + var editor = props.editor; + var hoverTimeoutId = useRef(); + var activePopup = useRef(); + useEffect(function () { + function onMouseOver(e) { + if (!e.target || + !(e.target instanceof HTMLElement) || + e.target.classList.contains("ProseMirror")) + return; + var element = e.target; + if (activePopup.current) { + var isOutsideEditor = !element.closest(".ProseMirror"); + var isInsidePopup = element.closest(".popup-presenter-portal"); + var isActiveElement = activePopup.current.element === element; + if (isInsidePopup) + return; + if (isOutsideEditor || !isActiveElement) { + console.log("HIDING", isOutsideEditor, isActiveElement, element); + activePopup.current.hide(); + activePopup.current = undefined; + return; + } + } + clearTimeout(hoverTimeoutId.current); + hoverTimeoutId.current = setTimeout(function () { + var nodeName = element.nodeName.toLowerCase(); + var PopupHandler = handlers[nodeName]; + if (!PopupHandler || !editor.current) + return; + var pos = editor.current.view.posAtDOM(element, 0); + var node = editor.current.view.state.doc.nodeAt(pos); + if (!node) + return; + var hidePopup = showPopup({ + popup: function () { return (_jsx(PopupHandler, { editor: editor, selectedNode: { + node: node, + from: pos, + to: pos + node.nodeSize, + } })); }, + theme: editor.storage.theme, + blocking: false, + focusOnRender: false, + position: { + target: element, + align: "center", + location: "top", + isTargetAbsolute: true, + }, + }); + activePopup.current = { element: element, hide: hidePopup }; + }, HOVER_TIMEOUT, {}); + } + window.addEventListener("mouseover", onMouseOver); + return function () { + window.removeEventListener("mouseover", onMouseOver); + }; + }, []); + return null; +} diff --git a/packages/editor/dist/toolbar/floatingmenus/hoverpopup/link.d.ts b/packages/editor/dist/toolbar/floatingmenus/hoverpopup/link.d.ts new file mode 100644 index 000000000..6463e2600 --- /dev/null +++ b/packages/editor/dist/toolbar/floatingmenus/hoverpopup/link.d.ts @@ -0,0 +1,7 @@ +/// +import { HoverPopupProps } from "."; +declare function LinkHoverPopup(props: HoverPopupProps): JSX.Element | null; +export declare const LinkHoverPopupHandler: { + a: typeof LinkHoverPopup; +}; +export {}; diff --git a/packages/editor/dist/toolbar/floatingmenus/hoverpopup/link.js b/packages/editor/dist/toolbar/floatingmenus/hoverpopup/link.js new file mode 100644 index 000000000..688b0be4d --- /dev/null +++ b/packages/editor/dist/toolbar/floatingmenus/hoverpopup/link.js @@ -0,0 +1,17 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import { ToolbarGroup } from "../../components/toolbar-group"; +function LinkHoverPopup(props) { + var editor = props.editor, selectedNode = props.selectedNode; + var node = selectedNode.node; + if (!node.isText || + node.marks.length <= 0 || + !node.marks.some(function (mark) { return mark.type.name === "link"; })) + return null; + return (_jsx(ToolbarGroup, { force: true, tools: ["openLink", "editLink", "removeLink"], editor: editor, selectedNode: selectedNode, sx: { + bg: "background", + boxShadow: "menu", + borderRadius: "default", + p: 1, + } })); +} +export var LinkHoverPopupHandler = { a: LinkHoverPopup }; diff --git a/packages/editor/dist/toolbar/floatingmenus/index.js b/packages/editor/dist/toolbar/floatingmenus/index.js index 329ddc6f0..4da653a3a 100644 --- a/packages/editor/dist/toolbar/floatingmenus/index.js +++ b/packages/editor/dist/toolbar/floatingmenus/index.js @@ -9,8 +9,9 @@ var __assign = (this && this.__assign) || function () { }; return __assign.apply(this, arguments); }; -import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime"; +import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime"; +import { HoverPopupHandler } from "./hover-popup"; import { SearchReplaceFloatingMenu } from "./search-replace"; export function EditorFloatingMenus(props) { - return (_jsx(_Fragment, { children: _jsx(SearchReplaceFloatingMenu, __assign({}, props)) })); + return (_jsxs(_Fragment, { children: [_jsx(SearchReplaceFloatingMenu, __assign({}, props)), _jsx(HoverPopupHandler, __assign({}, props))] })); } diff --git a/packages/editor/dist/toolbar/floatingmenus/types.d.ts b/packages/editor/dist/toolbar/floatingmenus/types.d.ts index ce475de33..f3ea5ebf6 100644 --- a/packages/editor/dist/toolbar/floatingmenus/types.d.ts +++ b/packages/editor/dist/toolbar/floatingmenus/types.d.ts @@ -1,4 +1,4 @@ -import { Editor } from "@tiptap/core"; +import { Editor } from "../../types"; export declare type FloatingMenuProps = { editor: Editor; }; diff --git a/packages/editor/dist/toolbar/icons.d.ts b/packages/editor/dist/toolbar/icons.d.ts index 76bc98db7..f5147bf49 100644 --- a/packages/editor/dist/toolbar/icons.d.ts +++ b/packages/editor/dist/toolbar/icons.d.ts @@ -23,7 +23,9 @@ export declare const Icons: { textColor: string; link: string; linkRemove: string; + openLink: string; linkEdit: string; + linkSettings: string; url: string; image: string; imageSettings: string; diff --git a/packages/editor/dist/toolbar/icons.js b/packages/editor/dist/toolbar/icons.js index 0a02c91b2..7a9147fe4 100644 --- a/packages/editor/dist/toolbar/icons.js +++ b/packages/editor/dist/toolbar/icons.js @@ -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, mdiTableRowPlusAfter, mdiTableRowPlusBefore, mdiTableRowRemove, mdiTableColumnPlusAfter, mdiTableColumnPlusBefore, mdiTableColumnRemove, mdiUploadOutline, mdiPlus, mdiFormatColorFill, mdiClose, mdiSortDescending, mdiArrowExpandRight, mdiArrowExpandLeft, mdiArrowExpandDown, mdiArrowExpandUp, mdiTableMergeCells, mdiTableSplitCell, mdiDeleteOutline, mdiDownloadOutline, mdiFormatListCheckbox, mdiDrag, mdiCheckboxMarkedOutline, mdiChevronUp, mdiArrowUp, mdiArrowDown, mdiRegex, mdiFormatLetterCase, mdiFormatLetterMatches, mdiMoviePlusOutline, mdiLink, mdiChevronRight, mdiTableColumnWidth, mdiTableRowHeight, mdiMinus, mdiPaletteOutline, mdiCircle, mdiChevronLeft, mdiTableCog, mdiTableOff, mdiRectangle, mdiImageEditOutline, mdiArrowLeft, mdiMovieCogOutline, mdiLinkOff, } 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, mdiFormatColorFill, mdiClose, mdiSortDescending, mdiArrowExpandRight, mdiArrowExpandLeft, mdiArrowExpandDown, mdiArrowExpandUp, mdiTableMergeCells, mdiTableSplitCell, mdiDeleteOutline, mdiDownloadOutline, mdiFormatListCheckbox, mdiDrag, mdiCheckboxMarkedOutline, mdiChevronUp, mdiArrowUp, mdiArrowDown, mdiRegex, mdiFormatLetterCase, mdiFormatLetterMatches, mdiMoviePlusOutline, mdiLink, mdiChevronRight, mdiTableColumnWidth, mdiTableRowHeight, mdiMinus, mdiPaletteOutline, mdiCircle, mdiChevronLeft, mdiTableCog, mdiTableOff, mdiRectangle, mdiImageEditOutline, mdiArrowLeft, mdiMovieCogOutline, mdiLinkOff, mdiOpenInNew, } from "@mdi/js"; export var Icons = { bold: mdiFormatBold, italic: mdiFormatItalic, @@ -24,7 +24,9 @@ export var Icons = { textColor: mdiFormatColorText, link: mdiLinkPlus, linkRemove: mdiLinkOff, + openLink: mdiOpenInNew, linkEdit: "m19 14 1.28 1.28c.22.21.22.56 0 .77l-1 1L17.23 15l1-1c.11-.11.25-.17.39-.17s.27.06.38.17m-.3 3.63-6.06 6.07h-2.06v-2.06l6.07-6.06zM7 7h4v2H7c-1.6568542 0-3 1.343146-3 3s1.3431458 3 3 3h4v2H7c-2.7614237 0-5-2.238576-5-5 0-2.7614237 2.2385763-5 5-5m10 0c2.761424 0 5 2.2385763 5 5h-2c0-1.656854-1.343146-3-3-3h-4V7h4m-9 4h8v2H8v-2", + linkSettings: "M7 7h4v2H7c-1.6568542 0-3 1.343146-3 3s1.3431458 3 3 3h4v2H7c-2.7614237 0-5-2.238576-5-5 0-2.7614237 2.2385763-5 5-5m10 0c2.761424 0 5 2.2385763 5 5h-2c0-1.656854-1.343146-3-3-3h-4V7h4m-9 4h8v2H8v-2m15.119777 8.323608-1.07-.82c.02-.17.04-.33.04-.5 0-.17-.01-.33-.04-.5l1.06-.82c.09258-.07939.117526-.212463.06-.32l-1-1.73c-.06-.13-.19-.13-.33-.13l-1.22.5c-.28-.18-.54-.35-.85-.47l-.19-1.32c-.01-.12-.12-.21-.24-.21h-2c-.12 0-.23.09-.25.21l-.19 1.32c-.3.13-.59.29-.85.47l-1.24-.5c-.11 0-.24 0-.31.13l-1 1.73c-.06.11-.04.24.06.32l1.06.82c-.03989.33214-.03989.66786 0 1l-1.06.82c-.09258.07939-.117526.212462-.06.32l1 1.73c.06.13.19.13.31.13l1.24-.5c.26.18.54.35.85.47l.19 1.32c.02.12.12.21.25.21h2c.12 0 .23-.09.25-.21l.19-1.32c.3-.13.56-.29.84-.47l1.22.5c.14 0 .27 0 .34-.13l1-1.73c.05753-.107538.03258-.240607-.06-.32m-4.78.18c-.83 0-1.5-.67-1.5-1.5s.68-1.5 1.5-1.5 1.5.67 1.5 1.5-.66 1.5-1.5 1.5z", url: mdiLink, image: mdiImage, imageSettings: mdiImageEditOutline, diff --git a/packages/editor/dist/toolbar/toolbar.js b/packages/editor/dist/toolbar/toolbar.js index 2a647d319..8f2313852 100644 --- a/packages/editor/dist/toolbar/toolbar.js +++ b/packages/editor/dist/toolbar/toolbar.js @@ -32,7 +32,7 @@ export function Toolbar(props) { "imageSettings", "embedSettings", "attachmentSettings", - "linkRemove", + "linkSettings", "codeRemove", ], [ @@ -51,7 +51,7 @@ export function Toolbar(props) { ["fontSize"], ["headings", "fontFamily"], ["numberedList", "bulletList"], - ["link"], + ["addLink"], ["alignCenter", ["alignLeft", "alignRight", "alignJustify", "ltr", "rtl"]], ["clearformatting"], ]; diff --git a/packages/editor/dist/toolbar/tooldefinitions.js b/packages/editor/dist/toolbar/tooldefinitions.js index 94d007542..2367c6561 100644 --- a/packages/editor/dist/toolbar/tooldefinitions.js +++ b/packages/editor/dist/toolbar/tooldefinitions.js @@ -15,13 +15,28 @@ var tools = { icon: "strikethrough", title: "Strikethrough", }, - link: { + addLink: { icon: "link", title: "Link", }, - linkRemove: { + editLink: { + icon: "linkEdit", + title: "Edit link", + conditional: true, + }, + removeLink: { icon: "linkRemove", - title: "Link remove", + title: "Remove link", + conditional: true, + }, + openLink: { + icon: "openLink", + title: "Open link", + conditional: true, + }, + linkSettings: { + icon: "linkSettings", + title: "Link settings", conditional: true, }, code: { diff --git a/packages/editor/dist/toolbar/tools/index.d.ts b/packages/editor/dist/toolbar/tools/index.d.ts index 63eab4098..598ca9b2d 100644 --- a/packages/editor/dist/toolbar/tools/index.d.ts +++ b/packages/editor/dist/toolbar/tools/index.d.ts @@ -1,6 +1,6 @@ import React from "react"; import { ToolProps } from "../types"; -import { Bold, Italic, Underline, Strikethrough, Code, Subscript, Superscript, ClearFormatting, Link, LinkRemove, CodeRemove } from "./inline"; +import { Bold, Italic, Underline, Strikethrough, Code, Subscript, Superscript, ClearFormatting, CodeRemove } from "./inline"; import { InsertBlock } from "./block"; import { FontSize, FontFamily } from "./font"; import { AlignCenter, AlignLeft, AlignRight, AlignJustify } from "./alignment"; @@ -12,6 +12,7 @@ import { TableSettings, ColumnProperties, RowProperties, CellProperties, CellBac import { ImageSettings, ImageAlignCenter, ImageAlignLeft, ImageAlignRight, ImageProperties } from "./image"; import { AttachmentSettings, DownloadAttachment, RemoveAttachment } from "./attachment"; import { EmbedAlignCenter, EmbedAlignLeft, EmbedAlignRight, EmbedProperties, EmbedSettings } from "./embed"; +import { AddLink, EditLink, RemoveLink, LinkSettings, OpenLink } from "./link"; export declare type ToolId = keyof typeof tools; declare const tools: { bold: typeof Bold; @@ -23,8 +24,11 @@ declare const tools: { subscript: typeof Subscript; superscript: typeof Superscript; clearformatting: typeof ClearFormatting; - link: typeof Link; - linkRemove: typeof LinkRemove; + addLink: typeof AddLink; + editLink: typeof EditLink; + removeLink: typeof RemoveLink; + linkSettings: typeof LinkSettings; + openLink: typeof OpenLink; insertBlock: typeof InsertBlock; numberedList: typeof NumberedList; bulletList: typeof BulletList; diff --git a/packages/editor/dist/toolbar/tools/index.js b/packages/editor/dist/toolbar/tools/index.js index 5d8af3eee..bcb64f5dd 100644 --- a/packages/editor/dist/toolbar/tools/index.js +++ b/packages/editor/dist/toolbar/tools/index.js @@ -1,4 +1,4 @@ -import { Bold, Italic, Underline, Strikethrough, Code, Subscript, Superscript, ClearFormatting, Link, LinkRemove, CodeRemove, } from "./inline"; +import { Bold, Italic, Underline, Strikethrough, Code, Subscript, Superscript, ClearFormatting, CodeRemove, } from "./inline"; import { InsertBlock } from "./block"; import { FontSize, FontFamily } from "./font"; import { AlignCenter, AlignLeft, AlignRight, AlignJustify } from "./alignment"; @@ -10,6 +10,7 @@ import { TableSettings, ColumnProperties, RowProperties, CellProperties, InsertC import { ImageSettings, ImageAlignCenter, ImageAlignLeft, ImageAlignRight, ImageProperties, } from "./image"; import { AttachmentSettings, DownloadAttachment, RemoveAttachment, } from "./attachment"; import { EmbedAlignCenter, EmbedAlignLeft, EmbedAlignRight, EmbedProperties, EmbedSettings, } from "./embed"; +import { AddLink, EditLink, RemoveLink, LinkSettings, OpenLink } from "./link"; var tools = { bold: Bold, italic: Italic, @@ -20,8 +21,11 @@ var tools = { subscript: Subscript, superscript: Superscript, clearformatting: ClearFormatting, - link: Link, - linkRemove: LinkRemove, + addLink: AddLink, + editLink: EditLink, + removeLink: RemoveLink, + linkSettings: LinkSettings, + openLink: OpenLink, insertBlock: InsertBlock, numberedList: NumberedList, bulletList: BulletList, diff --git a/packages/editor/dist/toolbar/tools/inline.d.ts b/packages/editor/dist/toolbar/tools/inline.d.ts index 71a3fec4e..50238429c 100644 --- a/packages/editor/dist/toolbar/tools/inline.d.ts +++ b/packages/editor/dist/toolbar/tools/inline.d.ts @@ -8,6 +8,4 @@ export declare function Bold(props: ToolProps): JSX.Element; export declare function Subscript(props: ToolProps): JSX.Element; export declare function Superscript(props: ToolProps): JSX.Element; export declare function ClearFormatting(props: ToolProps): JSX.Element; -export declare function LinkRemove(props: ToolProps): JSX.Element | null; export declare function CodeRemove(props: ToolProps): JSX.Element | null; -export declare function Link(props: ToolProps): JSX.Element; diff --git a/packages/editor/dist/toolbar/tools/inline.js b/packages/editor/dist/toolbar/tools/inline.js index f6b32e755..e1a71f437 100644 --- a/packages/editor/dist/toolbar/tools/inline.js +++ b/packages/editor/dist/toolbar/tools/inline.js @@ -9,28 +9,8 @@ var __assign = (this && this.__assign) || function () { }; 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, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime"; +import { jsx as _jsx } from "react/jsx-runtime"; import { ToolButton } from "../components/tool-button"; -import { useCallback, useRef, useState } from "react"; -import { ResponsivePresenter } from "../../components/responsive"; -import { Popup } from "../components/popup"; -import { LinkPopup } from "../popups/link-popup"; import { useToolbarLocation } from "../stores/toolbar-store"; export function Italic(props) { var editor = props.editor; @@ -67,13 +47,6 @@ export function ClearFormatting(props) { return (_a = editor.current) === null || _a === void 0 ? void 0 : _a.chain().focus().clearNodes().unsetAllMarks().unsetMark("link").run(); } }))); } -export function LinkRemove(props) { - var editor = props.editor; - var isBottom = useToolbarLocation() === "bottom"; - if (!editor.isActive("link") || !isBottom) - return null; - return (_jsx(ToolButton, __assign({}, props, { toggled: false, onClick: function () { var _a; return (_a = editor.current) === null || _a === void 0 ? void 0 : _a.chain().focus().unsetMark("link").run(); } }))); -} export function CodeRemove(props) { var editor = props.editor; var isBottom = useToolbarLocation() === "bottom"; @@ -81,49 +54,3 @@ export function CodeRemove(props) { return null; return (_jsx(ToolButton, __assign({}, props, { toggled: false, onClick: function () { var _a; return (_a = editor.current) === null || _a === void 0 ? void 0 : _a.chain().focus().unsetMark("code").run(); } }))); } -export function Link(props) { - var editor = props.editor, title = props.title, icon = props.icon; - var buttonRef = useRef(null); - var _a = __read(useState(false), 2), isOpen = _a[0], setIsOpen = _a[1]; - var _b = __read(useState(), 2), href = _b[0], setHref = _b[1]; - var _c = __read(useState(), 2), text = _c[0], setText = _c[1]; - var currentUrl = editor.getAttributes("link").href; - var isEditing = !!currentUrl; - var onDone = useCallback(function (href, text) { - var _a; - if (!href) - return; - var commandChain = (_a = editor.current) === null || _a === void 0 ? void 0 : _a.chain().focus(); - if (!commandChain) - return; - commandChain - .extendMarkRange("link") - .toggleLink({ href: href, target: "_blank" }) - .insertContent(text || href) - .focus() - .unsetMark("link") - .insertContent(" ") - .run(); - setIsOpen(false); - }, []); - return (_jsxs(_Fragment, { children: [_jsx(ToolButton, { id: icon, buttonRef: buttonRef, title: title, icon: isEditing ? "linkEdit" : icon, onClick: function () { - if (isEditing) - setHref(currentUrl); - var _a = editor.state.selection, from = _a.from, to = _a.to, $from = _a.$from; - var selectedNode = $from.node(); - var selectedText = isEditing - ? selectedNode.textContent - : editor.state.doc.textBetween(from, to); - setText(selectedText); - setIsOpen(true); - }, toggled: isOpen || !!isEditing }), _jsx(ResponsivePresenter, __assign({ mobile: "sheet", desktop: "menu", position: { - target: buttonRef.current || undefined, - isTargetAbsolute: true, - location: "below", - align: "center", - yOffset: 5, - }, title: isEditing ? "Edit link" : "Insert link", isOpen: isOpen, items: [], onClose: function () { return setIsOpen(false); }, focusOnRender: false }, { children: _jsx(Popup, __assign({ title: isEditing ? "Edit link" : "Insert link", onClose: function () { return setIsOpen(false); } }, { children: _jsx(LinkPopup, { href: href, text: text, isEditing: isEditing, onDone: function (_a) { - var href = _a.href, text = _a.text; - onDone(href, text); - } }) })) }))] })); -} diff --git a/packages/editor/dist/toolbar/tools/link.d.ts b/packages/editor/dist/toolbar/tools/link.d.ts new file mode 100644 index 000000000..58223feb4 --- /dev/null +++ b/packages/editor/dist/toolbar/tools/link.d.ts @@ -0,0 +1,7 @@ +/// +import { ToolProps } from "../types"; +export declare function LinkSettings(props: ToolProps): JSX.Element | null; +export declare function AddLink(props: ToolProps): JSX.Element; +export declare function EditLink(props: ToolProps): JSX.Element; +export declare function RemoveLink(props: ToolProps): JSX.Element; +export declare function OpenLink(props: ToolProps): JSX.Element | null; diff --git a/packages/editor/dist/toolbar/tools/link.js b/packages/editor/dist/toolbar/tools/link.js new file mode 100644 index 000000000..a9e0231d5 --- /dev/null +++ b/packages/editor/dist/toolbar/tools/link.js @@ -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, Fragment as _Fragment } from "react/jsx-runtime"; +import { ToolButton } from "../components/tool-button"; +import { useCallback, useRef, useState } from "react"; +import { ResponsivePresenter } from "../../components/responsive"; +import { Popup } from "../components/popup"; +import { LinkPopup } from "../popups/link-popup"; +import { useToolbarLocation } from "../stores/toolbar-store"; +import { MoreTools } from "../components/more-tools"; +import { useRefValue } from "../../hooks/use-ref-value"; +import { findMark, selectionToOffset } from "../utils/prosemirror"; +import { setTextSelection } from "prosemirror-utils"; +import { Flex, Text } from "rebass"; +export function LinkSettings(props) { + var editor = props.editor; + var isBottom = useToolbarLocation() === "bottom"; + if (!editor.isActive("link") || !isBottom) + return null; + return (_jsx(MoreTools, __assign({}, props, { autoCloseOnUnmount: true, popupId: "linkSettings", tools: ["openLink", "editLink", "removeLink"] }))); +} +export function AddLink(props) { + var editor = props.editor; + var isActive = props.editor.isActive("link"); + var onDone = useCallback(function (href, text) { + var _a; + if (!href) + return; + var commandChain = (_a = editor.current) === null || _a === void 0 ? void 0 : _a.chain().focus(); + if (!commandChain) + return; + commandChain + .extendMarkRange("link") + .toggleLink({ href: href, target: "_blank" }) + .insertContent(text || href) + .focus() + .unsetMark("link") + .insertContent(" ") + .run(); + }, []); + if (isActive) + return _jsx(EditLink, __assign({}, props)); + return (_jsx(LinkTool, __assign({}, props, { onDone: onDone, onClick: function () { + var _a = editor.state.selection, from = _a.from, to = _a.to; + var selectedText = editor.state.doc.textBetween(from, to); + return { text: selectedText }; + } }))); +} +export function EditLink(props) { + var editor = props.editor, _selectedNode = props.selectedNode; + var selectedNode = useRefValue(_selectedNode || selectionToOffset(editor.state.selection)); + var onDone = useCallback(function (href, text) { + if (!href || !editor.current) + return; + var _a = selectedNode.current, from = _a.from, node = _a.node, to = _a.to; + var mark = findMark(node, "link"); + if (!mark) + return; + editor.current + .chain() + .command(function (_a) { + var tr = _a.tr; + tr.removeMark(from, to, mark.type); + tr.addMark(from, to, mark.type.create({ href: href })); + tr.insertText(text || node.textContent, from, to); + setTextSelection(tr.mapping.map(from))(tr); + return true; + }) + .focus(undefined, { scrollIntoView: true }) + .run(); + }, []); + return (_jsx(LinkTool, __assign({}, props, { isEditing: true, onDone: onDone, onClick: function () { + var node = selectedNode.current.node; + var selectedText = node.textContent; + var mark = findMark(node, "link"); + if (!mark) + return; + return { text: selectedText, href: mark.attrs.href }; + } }))); +} +export function RemoveLink(props) { + var editor = props.editor, selectedNode = props.selectedNode; + return (_jsx(ToolButton, __assign({}, props, { toggled: false, onClick: function () { + var _a; + if (selectedNode) + editor.commands.setTextSelection(selectedNode.from); + (_a = editor.current) === null || _a === void 0 ? void 0 : _a.chain().focus().unsetLink().run(); + } }))); +} +export function OpenLink(props) { + var editor = props.editor, selectedNode = props.selectedNode; + var node = (selectedNode === null || selectedNode === void 0 ? void 0 : selectedNode.node) || editor.state.selection.$anchor.node(); + var link = selectedNode ? findMark(node, "link") : null; + if (!link) + return null; + var href = link === null || link === void 0 ? void 0 : link.attrs.href; + return (_jsxs(Flex, __assign({ sx: { alignItems: "center" } }, { children: [_jsx(Text, __assign({ as: "a", href: href, target: "_blank", variant: "body", sx: { mr: 1 } }, { children: href })), _jsx(ToolButton, __assign({}, props, { toggled: false, onClick: function () { return window.open(href, "_blank"); } }))] }))); +} +function LinkTool(props) { + var isEditing = props.isEditing, onClick = props.onClick, onDone = props.onDone; + var buttonRef = useRef(null); + var _a = __read(useState(false), 2), isOpen = _a[0], setIsOpen = _a[1]; + var _b = __read(useState(), 2), href = _b[0], setHref = _b[1]; + var _c = __read(useState(), 2), text = _c[0], setText = _c[1]; + return (_jsxs(_Fragment, { children: [_jsx(ToolButton, __assign({}, props, { buttonRef: buttonRef, onClick: function () { + var result = onClick(); + if (!result) + return; + var text = result.text, href = result.href; + setHref(href); + setText(text); + setIsOpen(true); + }, toggled: isOpen })), _jsx(ResponsivePresenter, __assign({ mobile: "sheet", desktop: "menu", position: { + target: buttonRef.current || undefined, + isTargetAbsolute: true, + location: "below", + align: "center", + yOffset: 5, + }, title: isEditing ? "Edit link" : "Insert link", isOpen: isOpen, items: [], onClose: function () { return setIsOpen(false); }, focusOnRender: false }, { children: _jsx(Popup, __assign({ title: isEditing ? "Edit link" : "Insert link", onClose: function () { return setIsOpen(false); } }, { children: _jsx(LinkPopup, { href: href, text: text, isEditing: isEditing, onDone: function (_a) { + var href = _a.href, text = _a.text; + onDone(href, text); + setIsOpen(false); + } }) })) }))] })); +} diff --git a/packages/editor/dist/toolbar/types.d.ts b/packages/editor/dist/toolbar/types.d.ts index b15f7fc8c..08982355e 100644 --- a/packages/editor/dist/toolbar/types.d.ts +++ b/packages/editor/dist/toolbar/types.d.ts @@ -1,10 +1,13 @@ import { Editor } from "../types"; import { IconNames } from "./icons"; import { ToolId } from "./tools"; +import { NodeWithOffset } from "./utils/prosemirror"; export declare type ToolButtonVariant = "small" | "normal"; export declare type ToolProps = ToolDefinition & { editor: Editor; variant?: ToolButtonVariant; + force?: boolean; + selectedNode?: NodeWithOffset; }; export declare type ToolDefinition = { icon: IconNames; diff --git a/packages/editor/dist/toolbar/utils/prosemirror.d.ts b/packages/editor/dist/toolbar/utils/prosemirror.d.ts index bfd6b17fe..5dd849997 100644 --- a/packages/editor/dist/toolbar/utils/prosemirror.d.ts +++ b/packages/editor/dist/toolbar/utils/prosemirror.d.ts @@ -1,4 +1,12 @@ import { Editor } from "@tiptap/core"; -import { Node } from "prosemirror-model"; +import { Node, Mark } from "prosemirror-model"; +import { Selection } from "prosemirror-state"; +export declare type NodeWithOffset = { + node: Node; + from: number; + to: number; +}; export declare function findSelectedDOMNode(editor: Editor, types: string[]): HTMLElement | null; export declare function findSelectedNode(editor: Editor, type: string): Node | null; +export declare function findMark(node: Node, type: string): Mark | undefined; +export declare function selectionToOffset(selection: Selection): NodeWithOffset; diff --git a/packages/editor/dist/toolbar/utils/prosemirror.js b/packages/editor/dist/toolbar/utils/prosemirror.js index f7acbd3ea..da12ccabb 100644 --- a/packages/editor/dist/toolbar/utils/prosemirror.js +++ b/packages/editor/dist/toolbar/utils/prosemirror.js @@ -21,3 +21,11 @@ export function findSelectedNode(editor, type) { return null; return editor.state.doc.nodeAt(pos); } +export function findMark(node, type) { + var mark = node.marks.find(function (m) { return m.type.name === type; }); + return mark; +} +export function selectionToOffset(selection) { + var $from = selection.$from, from = selection.from; + return { node: $from.node(), from: from, to: from + $from.node().nodeSize }; +} diff --git a/packages/editor/dist/utils/position.js b/packages/editor/dist/utils/position.js index a5befc6a8..fb3f7ae25 100644 --- a/packages/editor/dist/utils/position.js +++ b/packages/editor/dist/utils/position.js @@ -41,11 +41,14 @@ export function getPosition(element, options) { else if (location === "top") position.top = y - elementHeight; } - if (target !== "mouse" && align === "center" && elementWidth > 0) { - position.left -= elementWidth / 2 - target.clientWidth / 2; + if (width && target !== "mouse" && align === "center" && elementWidth > 0) { + position.left -= elementWidth / 2 - width / 2; } - else if (target !== "mouse" && align === "end" && elementWidth > 0) { - position.left -= elementWidth - target.clientWidth; + else if (width && + target !== "mouse" && + align === "end" && + elementWidth > 0) { + position.left -= elementWidth - width; } // Adjust menu height if (elementHeight > windowHeight - position.top) { diff --git a/packages/editor/package-lock.json b/packages/editor/package-lock.json index 3685e4ece..cd20c2f32 100644 --- a/packages/editor/package-lock.json +++ b/packages/editor/package-lock.json @@ -52,6 +52,7 @@ "shortid": "^2.2.16", "strip-indent": "^4.0.0", "tinycolor2": "^1.4.2", + "unfurl.js": "^5.7.0", "zustand": "^3.7.2" }, "devDependencies": { @@ -6745,8 +6746,7 @@ "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" }, "node_modules/buffer-indexof": { "version": "1.1.1", @@ -11730,7 +11730,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "dev": true, "bin": { "he": "bin/he" } @@ -12159,7 +12158,6 @@ "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3" }, @@ -12377,8 +12375,7 @@ "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "node_modules/ini": { "version": "1.3.8", @@ -15917,8 +15914,7 @@ "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/multicast-dns": { "version": "6.2.3", @@ -16064,7 +16060,6 @@ "version": "2.6.7", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", - "dev": true, "dependencies": { "whatwg-url": "^5.0.0" }, @@ -20301,8 +20296,7 @@ "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/sane": { "version": "4.1.0", @@ -21389,7 +21383,6 @@ "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -21399,7 +21392,6 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -21722,7 +21714,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, "dependencies": { "safe-buffer": "~5.2.0" } @@ -21731,7 +21722,6 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, "funding": [ { "type": "github", @@ -22756,8 +22746,7 @@ "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=", - "dev": true + "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" }, "node_modules/tryer": { "version": "1.0.1", @@ -23103,6 +23092,111 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/unfurl.js": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/unfurl.js/-/unfurl.js-5.7.0.tgz", + "integrity": "sha512-r6jvA/I6bDFMaSCGLVv3KznzbsNr+pQGcanH8ezYH1bjEJEDVdZr2jcPrgdrnTlFRCDGy+sns2jG/kVwm/kMzg==", + "dependencies": { + "debug": "^3.1.0", + "he": "^1.2.0", + "htmlparser2": "^3.9.2", + "iconv-lite": "^0.4.24", + "node-fetch": "^2.6.7", + "source-map-support": "^0.5.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/unfurl.js/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/unfurl.js/node_modules/dom-serializer": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", + "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==", + "dependencies": { + "domelementtype": "^2.0.1", + "entities": "^2.0.0" + } + }, + "node_modules/unfurl.js/node_modules/dom-serializer/node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/unfurl.js/node_modules/dom-serializer/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/unfurl.js/node_modules/domelementtype": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", + "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==" + }, + "node_modules/unfurl.js/node_modules/domhandler": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz", + "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==", + "dependencies": { + "domelementtype": "1" + } + }, + "node_modules/unfurl.js/node_modules/domutils": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", + "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", + "dependencies": { + "dom-serializer": "0", + "domelementtype": "1" + } + }, + "node_modules/unfurl.js/node_modules/entities": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", + "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==" + }, + "node_modules/unfurl.js/node_modules/htmlparser2": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", + "integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==", + "dependencies": { + "domelementtype": "^1.3.1", + "domhandler": "^2.3.0", + "domutils": "^1.5.1", + "entities": "^1.1.1", + "inherits": "^2.0.1", + "readable-stream": "^3.1.1" + } + }, + "node_modules/unfurl.js/node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", @@ -23409,8 +23503,7 @@ "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, "node_modules/util.promisify": { "version": "1.0.0", @@ -25046,7 +25139,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", - "dev": true, "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" @@ -25055,8 +25147,7 @@ "node_modules/whatwg-url/node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=", - "dev": true + "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" }, "node_modules/which": { "version": "2.0.2", @@ -30562,8 +30653,7 @@ "buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" }, "buffer-indexof": { "version": "1.1.1", @@ -34436,8 +34526,7 @@ "he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "dev": true + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" }, "hex-color-regex": { "version": "1.1.0", @@ -34794,7 +34883,6 @@ "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, "requires": { "safer-buffer": ">= 2.1.2 < 3" } @@ -34947,8 +35035,7 @@ "inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "ini": { "version": "1.3.8", @@ -37644,8 +37731,7 @@ "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "multicast-dns": { "version": "6.2.3", @@ -37772,7 +37858,6 @@ "version": "2.6.7", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", - "dev": true, "requires": { "whatwg-url": "^5.0.0" } @@ -41213,8 +41298,7 @@ "safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "sane": { "version": "4.1.0", @@ -42102,7 +42186,6 @@ "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, "requires": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -42111,8 +42194,7 @@ "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" } } }, @@ -42391,7 +42473,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, "requires": { "safe-buffer": "~5.2.0" }, @@ -42399,8 +42480,7 @@ "safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" } } }, @@ -43188,8 +43268,7 @@ "tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=", - "dev": true + "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" }, "tryer": { "version": "1.0.1", @@ -43424,6 +43503,100 @@ "which-boxed-primitive": "^1.0.2" } }, + "unfurl.js": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/unfurl.js/-/unfurl.js-5.7.0.tgz", + "integrity": "sha512-r6jvA/I6bDFMaSCGLVv3KznzbsNr+pQGcanH8ezYH1bjEJEDVdZr2jcPrgdrnTlFRCDGy+sns2jG/kVwm/kMzg==", + "requires": { + "debug": "^3.1.0", + "he": "^1.2.0", + "htmlparser2": "^3.9.2", + "iconv-lite": "^0.4.24", + "node-fetch": "^2.6.7", + "source-map-support": "^0.5.9" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "requires": { + "ms": "^2.1.1" + } + }, + "dom-serializer": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", + "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==", + "requires": { + "domelementtype": "^2.0.1", + "entities": "^2.0.0" + }, + "dependencies": { + "domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==" + }, + "entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==" + } + } + }, + "domelementtype": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", + "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==" + }, + "domhandler": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz", + "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==", + "requires": { + "domelementtype": "1" + } + }, + "domutils": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", + "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", + "requires": { + "dom-serializer": "0", + "domelementtype": "1" + } + }, + "entities": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", + "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==" + }, + "htmlparser2": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", + "integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==", + "requires": { + "domelementtype": "^1.3.1", + "domhandler": "^2.3.0", + "domutils": "^1.5.1", + "entities": "^1.1.1", + "inherits": "^2.0.1", + "readable-stream": "^3.1.1" + } + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, "unicode-canonical-property-names-ecmascript": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", @@ -43674,8 +43847,7 @@ "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, "util.promisify": { "version": "1.0.0", @@ -45010,7 +45182,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", - "dev": true, "requires": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" @@ -45019,8 +45190,7 @@ "webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=", - "dev": true + "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" } } }, diff --git a/packages/editor/package.json b/packages/editor/package.json index 4ce44ad1c..fad4ce9d7 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -48,6 +48,7 @@ "shortid": "^2.2.16", "strip-indent": "^4.0.0", "tinycolor2": "^1.4.2", + "unfurl.js": "^5.7.0", "zustand": "^3.7.2" }, "devDependencies": { diff --git a/packages/editor/src/components/popup-presenter/index.tsx b/packages/editor/src/components/popup-presenter/index.tsx index d4c3db9fe..54e38179c 100644 --- a/packages/editor/src/components/popup-presenter/index.tsx +++ b/packages/editor/src/components/popup-presenter/index.tsx @@ -54,6 +54,7 @@ function _PopupPresenter(props: PropsWithChildren) { const popupPosition = getPosition(popup, position); popup.style.top = popupPosition.top + "px"; popup.style.left = popupPosition.left + "px"; + console.log("popup", popupPosition); }, [position]); useEffect(() => { @@ -348,4 +349,6 @@ export function showPopup(options: ShowPopupOptions) { , getPopupContainer() ); + + return hide; } diff --git a/packages/editor/src/extensions.ts b/packages/editor/src/extensions.ts index 27031ae24..04757fdce 100644 --- a/packages/editor/src/extensions.ts +++ b/packages/editor/src/extensions.ts @@ -25,7 +25,6 @@ import "./extensions/search-replace"; import "./extensions/embed"; import "./extensions/code-block"; import "./extensions/list-item"; -import "./extensions/link"; import "./extensions/outline-list"; import "./extensions/outline-list-item"; import "./extensions/table"; diff --git a/packages/editor/src/extensions/link/index.ts b/packages/editor/src/extensions/link/index.ts deleted file mode 100644 index b44e8fde1..000000000 --- a/packages/editor/src/extensions/link/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { Link } from "./link"; - -export * from "./link"; - -export default Link; diff --git a/packages/editor/src/extensions/link/link.ts b/packages/editor/src/extensions/link/link.ts deleted file mode 100644 index 315756968..000000000 --- a/packages/editor/src/extensions/link/link.ts +++ /dev/null @@ -1,25 +0,0 @@ -import TiptapLink from "@tiptap/extension-link"; -import { Plugin, PluginKey } from "prosemirror-state"; - -export const Link = TiptapLink.extend({ - // addProseMirrorPlugins() { - // return [ - // ...(this.parent?.() || []), - // new Plugin({ - // key: new PluginKey("hoverHandler"), - // props: { - // handleDOMEvents: { - // mouseover: (view, event) => { - // if ( - // event.target instanceof HTMLElement && - // event.target.nodeName === "A" - // ) { - // console.log("Got it!"); - // } - // }, - // }, - // }, - // }), - // ]; - // }, -}); diff --git a/packages/editor/src/index.ts b/packages/editor/src/index.ts index b38616b44..d8b0be02a 100644 --- a/packages/editor/src/index.ts +++ b/packages/editor/src/index.ts @@ -31,10 +31,8 @@ import { SearchReplace } from "./extensions/search-replace"; import { EmbedNode } from "./extensions/embed"; import { CodeBlock } from "./extensions/code-block"; import { ListItem } from "./extensions/list-item"; -import { Link } from "./extensions/link"; +import { Link } from "@tiptap/extension-link"; import { - PortalProviderAPI, - EventDispatcher, NodeViewSelectionNotifier, usePortalProvider, } from "./extensions/react"; diff --git a/packages/editor/src/toolbar/components/toolbar-group.tsx b/packages/editor/src/toolbar/components/toolbar-group.tsx index fb416e62f..b4a451ad2 100644 --- a/packages/editor/src/toolbar/components/toolbar-group.tsx +++ b/packages/editor/src/toolbar/components/toolbar-group.tsx @@ -4,14 +4,17 @@ import { Flex, FlexProps } from "rebass"; import { Editor } from "@tiptap/core"; import { MoreTools } from "./more-tools"; import { getToolDefinition } from "../tool-definitions"; +import { NodeWithOffset } from "../utils/prosemirror"; export type ToolbarGroupProps = FlexProps & { tools: ToolbarGroupDefinition; editor: Editor; variant?: ToolButtonVariant; + force?: boolean; + selectedNode?: NodeWithOffset; }; export function ToolbarGroup(props: ToolbarGroupProps) { - const { tools, editor, ...flexProps } = props; + const { tools, editor, force, selectedNode, ...flexProps } = props; return ( @@ -34,6 +37,8 @@ export function ToolbarGroup(props: ToolbarGroupProps) { ); diff --git a/packages/editor/src/toolbar/floating-menus/hover-popup/index.tsx b/packages/editor/src/toolbar/floating-menus/hover-popup/index.tsx new file mode 100644 index 000000000..38dbd7052 --- /dev/null +++ b/packages/editor/src/toolbar/floating-menus/hover-popup/index.tsx @@ -0,0 +1,95 @@ +import { useEffect, useRef } from "react"; +import { showPopup } from "../../../components/popup-presenter"; +import { Editor } from "../../../types"; +import { NodeWithOffset } from "../../utils/prosemirror"; +import { FloatingMenuProps } from "../types"; +import { LinkHoverPopupHandler } from "./link"; + +export type HoverPopupProps = { + editor: Editor; + selectedNode: NodeWithOffset; +}; + +const handlers: Record JSX.Element | null> = + { ...LinkHoverPopupHandler }; + +const HOVER_TIMEOUT = 500; + +export function HoverPopupHandler(props: FloatingMenuProps) { + const { editor } = props; + const hoverTimeoutId = useRef(); + const activePopup = useRef<{ element: HTMLElement; hide: () => void }>(); + + useEffect(() => { + function onMouseOver(e: MouseEvent) { + if ( + !e.target || + !(e.target instanceof HTMLElement) || + e.target.classList.contains("ProseMirror") + ) + return; + + const element = e.target; + + if (activePopup.current) { + const isOutsideEditor = !element.closest(".ProseMirror"); + const isInsidePopup = element.closest(".popup-presenter-portal"); + const isActiveElement = activePopup.current.element === element; + if (isInsidePopup) return; + + if (isOutsideEditor || !isActiveElement) { + console.log("HIDING", isOutsideEditor, isActiveElement, element); + activePopup.current.hide(); + activePopup.current = undefined; + return; + } + } + + clearTimeout(hoverTimeoutId.current); + + hoverTimeoutId.current = setTimeout( + () => { + const nodeName = element.nodeName.toLowerCase(); + const PopupHandler = handlers[nodeName]; + if (!PopupHandler || !editor.current) return; + + const pos = editor.current.view.posAtDOM(element, 0); + const node = editor.current.view.state.doc.nodeAt(pos); + + if (!node) return; + + const hidePopup = showPopup({ + popup: () => ( + + ), + theme: editor.storage.theme, + blocking: false, + focusOnRender: false, + position: { + target: element, + align: "center", + location: "top", + isTargetAbsolute: true, + }, + }); + activePopup.current = { element, hide: hidePopup }; + }, + HOVER_TIMEOUT, + {} + ); + } + window.addEventListener("mouseover", onMouseOver); + return () => { + window.removeEventListener("mouseover", onMouseOver); + }; + }, []); + + return null; +} diff --git a/packages/editor/src/toolbar/floating-menus/hover-popup/link.tsx b/packages/editor/src/toolbar/floating-menus/hover-popup/link.tsx new file mode 100644 index 000000000..3185a766d --- /dev/null +++ b/packages/editor/src/toolbar/floating-menus/hover-popup/link.tsx @@ -0,0 +1,31 @@ +import { ToolbarGroup } from "../../components/toolbar-group"; +import { HoverPopupProps } from "."; + +function LinkHoverPopup(props: HoverPopupProps) { + const { editor, selectedNode } = props; + const { node } = selectedNode; + + if ( + !node.isText || + node.marks.length <= 0 || + !node.marks.some((mark) => mark.type.name === "link") + ) + return null; + + return ( + + ); +} + +export const LinkHoverPopupHandler = { a: LinkHoverPopup }; diff --git a/packages/editor/src/toolbar/floating-menus/index.tsx b/packages/editor/src/toolbar/floating-menus/index.tsx index 68559a8b5..5d8fd6b0b 100644 --- a/packages/editor/src/toolbar/floating-menus/index.tsx +++ b/packages/editor/src/toolbar/floating-menus/index.tsx @@ -1,3 +1,4 @@ +import { HoverPopupHandler } from "./hover-popup"; import { SearchReplaceFloatingMenu } from "./search-replace"; import { FloatingMenuProps } from "./types"; @@ -5,6 +6,7 @@ export function EditorFloatingMenus(props: FloatingMenuProps) { return ( <> + ); } diff --git a/packages/editor/src/toolbar/floating-menus/types.ts b/packages/editor/src/toolbar/floating-menus/types.ts index 7e3bdf5b4..ff5a1d681 100644 --- a/packages/editor/src/toolbar/floating-menus/types.ts +++ b/packages/editor/src/toolbar/floating-menus/types.ts @@ -1,2 +1,2 @@ -import { Editor } from "@tiptap/core"; +import { Editor } from "../../types"; export type FloatingMenuProps = { editor: Editor }; diff --git a/packages/editor/src/toolbar/icons.ts b/packages/editor/src/toolbar/icons.ts index b1161b8fd..bfe0e4b03 100644 --- a/packages/editor/src/toolbar/icons.ts +++ b/packages/editor/src/toolbar/icons.ts @@ -97,6 +97,7 @@ import { mdiArrowLeft, mdiMovieCogOutline, mdiLinkOff, + mdiOpenInNew, } from "@mdi/js"; export const Icons = { @@ -124,8 +125,11 @@ export const Icons = { textColor: mdiFormatColorText, link: mdiLinkPlus, linkRemove: mdiLinkOff, + openLink: mdiOpenInNew, linkEdit: "m19 14 1.28 1.28c.22.21.22.56 0 .77l-1 1L17.23 15l1-1c.11-.11.25-.17.39-.17s.27.06.38.17m-.3 3.63-6.06 6.07h-2.06v-2.06l6.07-6.06zM7 7h4v2H7c-1.6568542 0-3 1.343146-3 3s1.3431458 3 3 3h4v2H7c-2.7614237 0-5-2.238576-5-5 0-2.7614237 2.2385763-5 5-5m10 0c2.761424 0 5 2.2385763 5 5h-2c0-1.656854-1.343146-3-3-3h-4V7h4m-9 4h8v2H8v-2", + linkSettings: + "M7 7h4v2H7c-1.6568542 0-3 1.343146-3 3s1.3431458 3 3 3h4v2H7c-2.7614237 0-5-2.238576-5-5 0-2.7614237 2.2385763-5 5-5m10 0c2.761424 0 5 2.2385763 5 5h-2c0-1.656854-1.343146-3-3-3h-4V7h4m-9 4h8v2H8v-2m15.119777 8.323608-1.07-.82c.02-.17.04-.33.04-.5 0-.17-.01-.33-.04-.5l1.06-.82c.09258-.07939.117526-.212463.06-.32l-1-1.73c-.06-.13-.19-.13-.33-.13l-1.22.5c-.28-.18-.54-.35-.85-.47l-.19-1.32c-.01-.12-.12-.21-.24-.21h-2c-.12 0-.23.09-.25.21l-.19 1.32c-.3.13-.59.29-.85.47l-1.24-.5c-.11 0-.24 0-.31.13l-1 1.73c-.06.11-.04.24.06.32l1.06.82c-.03989.33214-.03989.66786 0 1l-1.06.82c-.09258.07939-.117526.212462-.06.32l1 1.73c.06.13.19.13.31.13l1.24-.5c.26.18.54.35.85.47l.19 1.32c.02.12.12.21.25.21h2c.12 0 .23-.09.25-.21l.19-1.32c.3-.13.56-.29.84-.47l1.22.5c.14 0 .27 0 .34-.13l1-1.73c.05753-.107538.03258-.240607-.06-.32m-4.78.18c-.83 0-1.5-.67-1.5-1.5s.68-1.5 1.5-1.5 1.5.67 1.5 1.5-.66 1.5-1.5 1.5z", url: mdiLink, image: mdiImage, diff --git a/packages/editor/src/toolbar/tool-definitions.ts b/packages/editor/src/toolbar/tool-definitions.ts index 29df22592..25971db12 100644 --- a/packages/editor/src/toolbar/tool-definitions.ts +++ b/packages/editor/src/toolbar/tool-definitions.ts @@ -18,13 +18,28 @@ const tools: Record = { icon: "strikethrough", title: "Strikethrough", }, - link: { + addLink: { icon: "link", title: "Link", }, - linkRemove: { + editLink: { + icon: "linkEdit", + title: "Edit link", + conditional: true, + }, + removeLink: { icon: "linkRemove", - title: "Link remove", + title: "Remove link", + conditional: true, + }, + openLink: { + icon: "openLink", + title: "Open link", + conditional: true, + }, + linkSettings: { + icon: "linkSettings", + title: "Link settings", conditional: true, }, code: { diff --git a/packages/editor/src/toolbar/toolbar.tsx b/packages/editor/src/toolbar/toolbar.tsx index 972bdda97..3e9cedf5b 100644 --- a/packages/editor/src/toolbar/toolbar.tsx +++ b/packages/editor/src/toolbar/toolbar.tsx @@ -57,7 +57,7 @@ export function Toolbar(props: ToolbarProps) { "imageSettings", "embedSettings", "attachmentSettings", - "linkRemove", + "linkSettings", "codeRemove", ], [ @@ -76,7 +76,7 @@ export function Toolbar(props: ToolbarProps) { ["fontSize"], ["headings", "fontFamily"], ["numberedList", "bulletList"], - ["link"], + ["addLink"], ["alignCenter", ["alignLeft", "alignRight", "alignJustify", "ltr", "rtl"]], ["clearformatting"], ]; diff --git a/packages/editor/src/toolbar/tools/index.ts b/packages/editor/src/toolbar/tools/index.ts index 24fa7f82f..c1f6164c9 100644 --- a/packages/editor/src/toolbar/tools/index.ts +++ b/packages/editor/src/toolbar/tools/index.ts @@ -9,8 +9,6 @@ import { Subscript, Superscript, ClearFormatting, - Link, - LinkRemove, CodeRemove, } from "./inline"; import { InsertBlock } from "./block"; @@ -62,6 +60,7 @@ import { EmbedProperties, EmbedSettings, } from "./embed"; +import { AddLink, EditLink, RemoveLink, LinkSettings, OpenLink } from "./link"; export type ToolId = keyof typeof tools; const tools = { @@ -74,8 +73,11 @@ const tools = { subscript: Subscript, superscript: Superscript, clearformatting: ClearFormatting, - link: Link, - linkRemove: LinkRemove, + addLink: AddLink, + editLink: EditLink, + removeLink: RemoveLink, + linkSettings: LinkSettings, + openLink: OpenLink, insertBlock: InsertBlock, numberedList: NumberedList, bulletList: BulletList, diff --git a/packages/editor/src/toolbar/tools/inline.tsx b/packages/editor/src/toolbar/tools/inline.tsx index 4f77c5d44..9ffd8eb8f 100644 --- a/packages/editor/src/toolbar/tools/inline.tsx +++ b/packages/editor/src/toolbar/tools/inline.tsx @@ -1,9 +1,5 @@ import { ToolProps } from "../types"; import { ToolButton } from "../components/tool-button"; -import { useCallback, useMemo, useRef, useState } from "react"; -import { ResponsivePresenter } from "../../components/responsive"; -import { Popup } from "../components/popup"; -import { LinkPopup } from "../popups/link-popup"; import { useToolbarLocation } from "../stores/toolbar-store"; export function Italic(props: ToolProps) { @@ -104,19 +100,6 @@ export function ClearFormatting(props: ToolProps) { ); } -export function LinkRemove(props: ToolProps) { - const { editor } = props; - const isBottom = useToolbarLocation() === "bottom"; - if (!editor.isActive("link") || !isBottom) return null; - return ( - editor.current?.chain().focus().unsetMark("link").run()} - /> - ); -} - export function CodeRemove(props: ToolProps) { const { editor } = props; const isBottom = useToolbarLocation() === "bottom"; @@ -129,141 +112,3 @@ export function CodeRemove(props: ToolProps) { /> ); } - -export function Link(props: ToolProps) { - const { editor, title, icon } = props; - const buttonRef = useRef(null); - const [isOpen, setIsOpen] = useState(false); - - const [href, setHref] = useState(); - const [text, setText] = useState(); - const currentUrl = editor.getAttributes("link").href; - const isEditing = !!currentUrl; - - const onDone = useCallback((href: string, text: string) => { - if (!href) return; - - let commandChain = editor.current?.chain().focus(); - if (!commandChain) return; - - commandChain - .extendMarkRange("link") - .toggleLink({ href, target: "_blank" }) - .insertContent(text || href) - .focus() - .unsetMark("link") - .insertContent(" ") - .run(); - - setIsOpen(false); - }, []); - - return ( - <> - { - if (isEditing) setHref(currentUrl); - - let { from, to, $from } = editor.state.selection; - const selectedNode = $from.node(); - - const selectedText = isEditing - ? selectedNode.textContent - : editor.state.doc.textBetween(from, to); - - setText(selectedText); - setIsOpen(true); - }} - toggled={isOpen || !!isEditing} - /> - setIsOpen(false)} - focusOnRender={false} - > - setIsOpen(false)} - > - { - onDone(href, text); - }} - /> - - - - ); -} - -// export function Link(props: ToolProps) { -// const { editor, title, icon } = props; -// const buttonRef = useRef(null); -// const [isOpen, setIsOpen] = useState(false); - -// const [href, setHref] = useState(); -// const [text, setText] = useState(); -// const currentUrl = editor.getAttributes("link").href; -// const isEditing = !!currentUrl; - -// const onDone = useCallback((href: string, text: string) => { -// if (!href) return; - -// let commandChain = editor.current?.chain().focus(); -// if (!commandChain) return; - -// commandChain -// .extendMarkRange("link") -// .toggleLink({ href, target: "_blank" }) -// .insertContent(text || href) -// .focus() -// .unsetMark("link") -// .insertContent(" ") -// .run(); - -// setIsOpen(false); -// }, []); - -// return ( -// <> -// { -// if (isEditing) setHref(currentUrl); - -// let { from, to, $from } = editor.state.selection; -// const selectedNode = $from.node(); - -// const selectedText = isEditing -// ? selectedNode.textContent -// : editor.state.doc.textBetween(from, to); - -// setText(selectedText); -// setIsOpen(true); -// }} -// toggled={isOpen || !!isEditing} -// /> -// -// ); -// } diff --git a/packages/editor/src/toolbar/tools/link.tsx b/packages/editor/src/toolbar/tools/link.tsx new file mode 100644 index 000000000..e50609085 --- /dev/null +++ b/packages/editor/src/toolbar/tools/link.tsx @@ -0,0 +1,203 @@ +import { ToolProps } from "../types"; +import { ToolButton } from "../components/tool-button"; +import { useCallback, useRef, useState } from "react"; +import { ResponsivePresenter } from "../../components/responsive"; +import { Popup } from "../components/popup"; +import { LinkPopup } from "../popups/link-popup"; +import { useToolbarLocation } from "../stores/toolbar-store"; +import { MoreTools } from "../components/more-tools"; +import { useRefValue } from "../../hooks/use-ref-value"; +import { findMark, selectionToOffset } from "../utils/prosemirror"; +import { setTextSelection } from "prosemirror-utils"; +import { Flex, Text } from "rebass"; + +export function LinkSettings(props: ToolProps) { + const { editor } = props; + const isBottom = useToolbarLocation() === "bottom"; + if (!editor.isActive("link") || !isBottom) return null; + + return ( + + ); +} + +export function AddLink(props: ToolProps) { + const { editor } = props; + + const isActive = props.editor.isActive("link"); + + const onDone = useCallback((href: string, text: string) => { + if (!href) return; + + let commandChain = editor.current?.chain().focus(); + if (!commandChain) return; + + commandChain + .extendMarkRange("link") + .toggleLink({ href, target: "_blank" }) + .insertContent(text || href) + .focus() + .unsetMark("link") + .insertContent(" ") + .run(); + }, []); + + if (isActive) return ; + return ( + { + let { from, to } = editor.state.selection; + const selectedText = editor.state.doc.textBetween(from, to); + return { text: selectedText }; + }} + /> + ); +} + +export function EditLink(props: ToolProps) { + const { editor, selectedNode: _selectedNode } = props; + const selectedNode = useRefValue( + _selectedNode || selectionToOffset(editor.state.selection) + ); + + const onDone = useCallback((href: string, text: string) => { + if (!href || !editor.current) return; + + const { from, node, to } = selectedNode.current; + const mark = findMark(node, "link"); + if (!mark) return; + + editor.current + .chain() + .command(({ tr }) => { + tr.removeMark(from, to, mark.type); + tr.addMark(from, to, mark.type.create({ href })); + tr.insertText(text || node.textContent, from, to); + setTextSelection(tr.mapping.map(from))(tr); + return true; + }) + .focus(undefined, { scrollIntoView: true }) + .run(); + }, []); + + return ( + { + const { node } = selectedNode.current; + const selectedText = node.textContent; + const mark = findMark(node, "link"); + + if (!mark) return; + return { text: selectedText, href: mark.attrs.href }; + }} + /> + ); +} + +export function RemoveLink(props: ToolProps) { + const { editor, selectedNode } = props; + return ( + { + if (selectedNode) editor.commands.setTextSelection(selectedNode.from); + editor.current?.chain().focus().unsetLink().run(); + }} + /> + ); +} + +export function OpenLink(props: ToolProps) { + const { editor, selectedNode } = props; + const node = selectedNode?.node || editor.state.selection.$anchor.node(); + const link = selectedNode ? findMark(node, "link") : null; + if (!link) return null; + const href = link?.attrs.href; + + return ( + + + {href} + + window.open(href, "_blank")} + /> + + ); +} + +type LinkToolProps = ToolProps & { + isEditing?: boolean; + onDone: (href: string, text: string) => void; + onClick: () => { href?: string; text: string } | undefined; +}; +function LinkTool(props: LinkToolProps) { + const { isEditing, onClick, onDone } = props; + const buttonRef = useRef(null); + const [isOpen, setIsOpen] = useState(false); + + const [href, setHref] = useState(); + const [text, setText] = useState(); + + return ( + <> + { + const result = onClick(); + if (!result) return; + const { text, href } = result; + setHref(href); + setText(text); + setIsOpen(true); + }} + toggled={isOpen} + /> + setIsOpen(false)} + focusOnRender={false} + > + setIsOpen(false)} + > + { + onDone(href, text); + setIsOpen(false); + }} + /> + + + + ); +} diff --git a/packages/editor/src/toolbar/types.ts b/packages/editor/src/toolbar/types.ts index 06b429e7d..653bcab87 100644 --- a/packages/editor/src/toolbar/types.ts +++ b/packages/editor/src/toolbar/types.ts @@ -1,11 +1,14 @@ import { Editor } from "../types"; import { IconNames } from "./icons"; import { ToolId } from "./tools"; +import { NodeWithOffset } from "./utils/prosemirror"; export type ToolButtonVariant = "small" | "normal"; export type ToolProps = ToolDefinition & { editor: Editor; variant?: ToolButtonVariant; + force?: boolean; + selectedNode?: NodeWithOffset; }; export type ToolDefinition = { diff --git a/packages/editor/src/toolbar/utils/prosemirror.ts b/packages/editor/src/toolbar/utils/prosemirror.ts index 4b2173ab1..4a53bb6b2 100644 --- a/packages/editor/src/toolbar/utils/prosemirror.ts +++ b/packages/editor/src/toolbar/utils/prosemirror.ts @@ -4,7 +4,14 @@ import { findParentNodeClosestToPos, isNodeSelection, } from "@tiptap/core"; -import { Node } from "prosemirror-model"; +import { Node, Mark } from "prosemirror-model"; +import { Selection } from "prosemirror-state"; + +export type NodeWithOffset = { + node: Node; + from: number; + to: number; +}; export function findSelectedDOMNode( editor: Editor, @@ -37,3 +44,13 @@ export function findSelectedNode(editor: Editor, type: string): Node | null { return editor.state.doc.nodeAt(pos); } + +export function findMark(node: Node, type: string): Mark | undefined { + const mark = node.marks.find((m) => m.type.name === type); + return mark; +} + +export function selectionToOffset(selection: Selection): NodeWithOffset { + const { $from, from } = selection; + return { node: $from.node(), from, to: from + $from.node().nodeSize }; +} diff --git a/packages/editor/src/utils/position.ts b/packages/editor/src/utils/position.ts index a20a0d931..cc77c5140 100644 --- a/packages/editor/src/utils/position.ts +++ b/packages/editor/src/utils/position.ts @@ -77,10 +77,15 @@ export function getPosition( else if (location === "top") position.top = y - elementHeight; } - if (target !== "mouse" && align === "center" && elementWidth > 0) { - position.left -= elementWidth / 2 - target.clientWidth / 2; - } else if (target !== "mouse" && align === "end" && elementWidth > 0) { - position.left -= elementWidth - target.clientWidth; + if (width && target !== "mouse" && align === "center" && elementWidth > 0) { + position.left -= elementWidth / 2 - width / 2; + } else if ( + width && + target !== "mouse" && + align === "end" && + elementWidth > 0 + ) { + position.left -= elementWidth - width; } // Adjust menu height