UNPKG

text-editor-drcsystems

Version:
1,469 lines (1,249 loc) 811 kB
/** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ 'use strict'; var React = require('react'); var html = require('@lexical/html'); var LexicalAutoFocusPlugin = require('@lexical/react/LexicalAutoFocusPlugin'); var LexicalCharacterLimitPlugin = require('@lexical/react/LexicalCharacterLimitPlugin'); var LexicalCheckListPlugin = require('@lexical/react/LexicalCheckListPlugin'); var LexicalClearEditorPlugin = require('@lexical/react/LexicalClearEditorPlugin'); var LexicalCollaborationPlugin = require('@lexical/react/LexicalCollaborationPlugin'); var LexicalComposerContext = require('@lexical/react/LexicalComposerContext'); var LexicalErrorBoundary = require('@lexical/react/LexicalErrorBoundary'); var LexicalHashtagPlugin = require('@lexical/react/LexicalHashtagPlugin'); var LexicalHistoryPlugin = require('@lexical/react/LexicalHistoryPlugin'); var LexicalHorizontalRulePlugin = require('@lexical/react/LexicalHorizontalRulePlugin'); var LexicalListPlugin = require('@lexical/react/LexicalListPlugin'); var LexicalOnChangePlugin = require('@lexical/react/LexicalOnChangePlugin'); var LexicalPlainTextPlugin = require('@lexical/react/LexicalPlainTextPlugin'); var LexicalRichTextPlugin = require('@lexical/react/LexicalRichTextPlugin'); var LexicalTabIndentationPlugin = require('@lexical/react/LexicalTabIndentationPlugin'); var LexicalTablePlugin = require('@lexical/react/LexicalTablePlugin'); var code = require('@lexical/code'); var markdown = require('@lexical/markdown'); var LexicalCollaborationContext = require('@lexical/react/LexicalCollaborationContext'); var utils = require('@lexical/utils'); var yjs$1 = require('@lexical/yjs'); var lexical = require('lexical'); var ReactDOM = require('react-dom'); var LexicalHorizontalRuleNode = require('@lexical/react/LexicalHorizontalRuleNode'); var table = require('@lexical/table'); var LexicalBlockWithAlignableContents = require('@lexical/react/LexicalBlockWithAlignableContents'); var LexicalDecoratorBlockNode = require('@lexical/react/LexicalDecoratorBlockNode'); var selection = require('@lexical/selection'); var LexicalAutoEmbedPlugin = require('@lexical/react/LexicalAutoEmbedPlugin'); var LexicalAutoLinkPlugin$1 = require('@lexical/react/LexicalAutoLinkPlugin'); var link = require('@lexical/link'); var lodash = require('lodash'); var mark = require('@lexical/mark'); var LexicalComposer = require('@lexical/react/LexicalComposer'); var text = require('@lexical/text'); var yjs = require('yjs'); var list = require('@lexical/list'); var LexicalTypeaheadMenuPlugin = require('@lexical/react/LexicalTypeaheadMenuPlugin'); var richText = require('@lexical/rich-text'); var LexicalMarkdownShortcutPlugin = require('@lexical/react/LexicalMarkdownShortcutPlugin'); var useLexicalEditable = require('@lexical/react/useLexicalEditable'); var LexicalTableOfContents__EXPERIMENTAL = require('@lexical/react/LexicalTableOfContents__EXPERIMENTAL'); var hashtag = require('@lexical/hashtag'); var overflow = require('@lexical/overflow'); var useDebounce$1 = require('use-debounce'); var LexicalNestedComposer = require('@lexical/react/LexicalNestedComposer'); var useLexicalNodeSelection = require('@lexical/react/useLexicalNodeSelection'); var useLexicalTextEntity = require('@lexical/react/useLexicalTextEntity'); var LexicalLinkPlugin = require('@lexical/react/LexicalLinkPlugin'); var LexicalTreeView = require('@lexical/react/LexicalTreeView'); var yWebsocket = require('y-websocket'); var LexicalContentEditable$1 = require('@lexical/react/LexicalContentEditable'); var clipboard = require('@lexical/clipboard'); /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * */ const CAN_USE_DOM = typeof window !== 'undefined' && typeof window.document !== 'undefined' && typeof window.document.createElement !== 'undefined'; /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * */ const url = new URL(window.location.href); const params = new URLSearchParams(url.search); const WEBSOCKET_ENDPOINT = params.get('collabEndpoint') || 'ws://localhost:1234'; const WEBSOCKET_SLUG = 'playground'; const WEBSOCKET_ID = params.get('collabId') || '0'; // parent dom -> child doc function createWebsocketProvider(id, yjsDocMap) { let doc = yjsDocMap.get(id); if (doc === undefined) { doc = new yjs.Doc(); yjsDocMap.set(id, doc); } else { doc.load(); } return new yWebsocket.WebsocketProvider(WEBSOCKET_ENDPOINT, WEBSOCKET_SLUG + '/' + WEBSOCKET_ID + '/' + id, doc, { connect: false }); } /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * */ const Context$2 = /*#__PURE__*/React.createContext({}); const SharedHistoryContext = ({ children }) => { const historyContext = React.useMemo(() => ({ historyState: LexicalHistoryPlugin.createEmptyHistoryState() }), []); return /*#__PURE__*/React.createElement(Context$2.Provider, { value: historyContext }, children); }; const useSharedHistoryContext = () => { return React.useContext(Context$2); }; /* eslint-disable header/header */ const EditorComposerContext = /*#__PURE__*/React.createContext(null); function useEditorComposerContext() { const editorContext = React.useContext(EditorComposerContext); if (editorContext == null) { { throw Error(`Cannot find an EditorComposerContext`); } } return editorContext; } /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * */ function PortalImpl({ onClose, children, title, closeOnClickOutside }) { const modalRef = React.useRef(null); React.useEffect(() => { if (modalRef.current !== null) { modalRef.current.focus(); } }, []); React.useEffect(() => { let modalOverlayElement = null; const handler = event => { if (event.keyCode === 27) { onClose(); } }; const clickOutsideHandler = event => { const target = event.target; if (modalRef.current !== null && !modalRef.current.contains(target) && closeOnClickOutside) { onClose(); } }; const modelElement = modalRef.current; if (modelElement !== null) { modalOverlayElement = modelElement.parentElement; if (modalOverlayElement !== null) { modalOverlayElement.addEventListener('click', clickOutsideHandler); } } window.addEventListener('keydown', handler); return () => { window.removeEventListener('keydown', handler); if (modalOverlayElement !== null) { modalOverlayElement?.removeEventListener('click', clickOutsideHandler); } }; }, [closeOnClickOutside, onClose]); return /*#__PURE__*/React.createElement("div", { className: "Modal__overlay", role: "dialog" }, /*#__PURE__*/React.createElement("div", { className: "Modal__modal", tabIndex: -1, ref: modalRef }, /*#__PURE__*/React.createElement("h2", { className: "Modal__title" }, title), /*#__PURE__*/React.createElement("button", { className: "Modal__closeButton", "aria-label": "Close modal", type: "button", onClick: onClose }, "X"), /*#__PURE__*/React.createElement("div", { className: "Modal__content" }, children))); } function Modal({ onClose, children, title, closeOnClickOutside = false }) { return /*#__PURE__*/ReactDOM.createPortal( /*#__PURE__*/React.createElement(PortalImpl, { onClose: onClose, title: title, closeOnClickOutside: closeOnClickOutside }, children), document.body); } /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * */ function useModal() { const [modalContent, setModalContent] = React.useState(null); const onClose = React.useCallback(() => { setModalContent(null); }, []); const modal = React.useMemo(() => { if (modalContent === null) { return null; } const { title, content, closeOnClickOutside } = modalContent; return /*#__PURE__*/React.createElement(Modal, { onClose: onClose, title: title, closeOnClickOutside: closeOnClickOutside }, content); }, [modalContent, onClose]); const showModal = React.useCallback((title, getContent, closeOnClickOutside = false) => { setModalContent({ closeOnClickOutside, content: getContent(onClose), title }); }, [onClose]); return [modal, showModal]; } function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); } /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * */ function joinClasses(...args) { return args.filter(Boolean).join(' '); } function Button({ 'data-test-id': dataTestId, children, className, onClick, disabled, small, title }) { return /*#__PURE__*/React.createElement("button", _extends({ type: "button", disabled: disabled, className: joinClasses('Button__root', disabled && 'Button__disabled', small && 'Button__small', className), onClick: onClick, title: title, "aria-label": title }, dataTestId && { 'data-test-id': dataTestId }), children); } const ImageComponent$2 = /*#__PURE__*/React.lazy( // @ts-ignore () => Promise.resolve().then(function () { return ImageComponent$1; })); function convertImageElement(domNode) { if (domNode instanceof HTMLImageElement) { const { alt: altText, src, width, height } = domNode; const node = $createImageNode({ altText, height, src, width }); return { node }; } return null; } const genClassName = theme => { return joinClasses(theme.image, 'editor-image'); }; class ImageNode extends lexical.DecoratorNode { // Captions cannot yet be used within editor cells static getType() { return 'image'; } static clone(node) { return new ImageNode(node.__src, node.__altText, node.__maxWidth, node.__width, node.__height, node.__showCaption, node.__caption, node.__captionsEnabled, node.__key, node.__file); } static importJSON(serializedNode) { const { altText, height, width, maxWidth, caption, src, showCaption } = serializedNode; const node = $createImageNode({ altText, height, maxWidth, showCaption, src, width }); const nestedEditor = node.__caption; const editorState = nestedEditor.parseEditorState(caption.editorState); if (!editorState.isEmpty()) { nestedEditor.setEditorState(editorState); } return node; } exportDOM(editor) { const className = genClassName(editor._config.theme); const element = document.createElement('img'); element.className = className; element.setAttribute('src', this.__src); element.setAttribute('alt', this.__altText); element.setAttribute('width', this.__width.toString()); element.setAttribute('height', this.__height.toString()); return { element }; } static importDOM() { return { img: node => ({ conversion: convertImageElement, priority: 0 }) }; } constructor(src, altText, maxWidth, width, height, showCaption, caption, captionsEnabled, key, file) { super(key); _defineProperty(this, "__src", void 0); _defineProperty(this, "__altText", void 0); _defineProperty(this, "__width", void 0); _defineProperty(this, "__height", void 0); _defineProperty(this, "__maxWidth", void 0); _defineProperty(this, "__showCaption", void 0); _defineProperty(this, "__caption", void 0); _defineProperty(this, "__captionsEnabled", void 0); _defineProperty(this, "__file", void 0); this.__src = src; this.__altText = altText; this.__maxWidth = maxWidth; this.__width = width || 'inherit'; this.__height = height || 'inherit'; this.__showCaption = showCaption || false; this.__caption = caption || lexical.createEditor(); this.__captionsEnabled = captionsEnabled || captionsEnabled === undefined; this.__file = file; } exportJSON() { return { altText: this.getAltText(), caption: this.__caption.toJSON(), height: this.__height === 'inherit' ? 0 : this.__height, maxWidth: this.__maxWidth, showCaption: this.__showCaption, src: this.getSrc(), type: 'image', version: 1, width: this.__width === 'inherit' ? 0 : this.__width }; } setWidthAndHeight(width, height) { const writable = this.getWritable(); writable.__width = width; writable.__height = height; } setShowCaption(showCaption) { const writable = this.getWritable(); writable.__showCaption = showCaption; } setSrc(src) { const writable = this.getWritable(); writable.__src = src; } settext(id) { const writable = this.getWritable(); writable.__altText = id; } setFile(file) { const writable = this.getWritable(); writable.__file = file; } // View createDOM(config) { const span = document.createElement('span'); const theme = config.theme; const className = genClassName(theme); if (className !== undefined) { span.className = className; } return span; } updateDOM() { return false; } getSrc() { return this.__src; } getAltText() { return this.__altText; } getFile() { return this.__file; } decorate() { return /*#__PURE__*/React.createElement(React.Suspense, { fallback: null }, /*#__PURE__*/React.createElement(ImageComponent$2, { src: this.__src, altText: this.__altText, width: this.__width, height: this.__height, maxWidth: this.__maxWidth, nodeKey: this.getKey(), showCaption: this.__showCaption, caption: this.__caption, captionsEnabled: this.__captionsEnabled, resizable: true })); } } function $createImageNode({ altText, height, maxWidth = 500, captionsEnabled, src, width, showCaption, caption, key, file }) { return lexical.$applyNodeReplacement(new ImageNode(src, altText, maxWidth, width, height, showCaption, caption, captionsEnabled, key, file)); } function $isImageNode(node) { return node instanceof ImageNode; } const WIDGET_SCRIPT_URL = 'https://platform.twitter.com/widgets.js'; function convertTweetElement(domNode) { const id = domNode.getAttribute('data-lexical-tweet-id'); if (id) { const node = $createTweetNode(id); return { node }; } return null; } let isTwitterScriptLoading = true; function TweetComponent({ className, format, loadingComponent, nodeKey, onError, onLoad, tweetID }) { const containerRef = React.useRef(null); const previousTweetIDRef = React.useRef(''); const [isTweetLoading, setIsTweetLoading] = React.useState(false); const createTweet = React.useCallback(async () => { try { // @ts-expect-error Twitter is attached to the window. await window.twttr.widgets.createTweet(tweetID, containerRef.current); setIsTweetLoading(false); isTwitterScriptLoading = false; if (onLoad) { onLoad(); } } catch (error) { if (onError) { onError(String(error)); } } }, [onError, onLoad, tweetID]); React.useEffect(() => { if (tweetID !== previousTweetIDRef.current) { setIsTweetLoading(true); if (isTwitterScriptLoading) { const script = document.createElement('script'); script.src = WIDGET_SCRIPT_URL; script.async = true; document.body?.appendChild(script); script.onload = createTweet; if (onError) { script.onerror = onError; } } else { createTweet(); } if (previousTweetIDRef) { previousTweetIDRef.current = tweetID; } } }, [createTweet, onError, tweetID]); return /*#__PURE__*/React.createElement(LexicalBlockWithAlignableContents.BlockWithAlignableContents, { className: className, format: format, nodeKey: nodeKey }, isTweetLoading ? loadingComponent : null, /*#__PURE__*/React.createElement("div", { style: { display: 'inline-block', width: '550px' }, ref: containerRef })); } class TweetNode extends LexicalDecoratorBlockNode.DecoratorBlockNode { static getType() { return 'tweet'; } static clone(node) { return new TweetNode(node.__id, node.__format, node.__key); } static importJSON(serializedNode) { const node = $createTweetNode(serializedNode.id); node.setFormat(serializedNode.format); return node; } exportJSON() { return { ...super.exportJSON(), id: this.getId(), type: 'tweet', version: 1 }; } static importDOM() { return { div: domNode => { if (!domNode.hasAttribute('data-lexical-tweet-id')) { return null; } return { conversion: convertTweetElement, priority: 2 }; } }; } exportDOM() { const element = document.createElement('div'); element.setAttribute('data-lexical-tweet-id', this.__id); const text = document.createTextNode(this.getTextContent()); element.append(text); return { element }; } constructor(id, format, key) { super(format, key); _defineProperty(this, "__id", void 0); this.__id = id; } getId() { return this.__id; } getTextContent(_includeInert, _includeDirectionless) { return `https://twitter.com/i/web/status/${this.__id}`; } decorate(editor, config) { const embedBlockTheme = config.theme.embedBlock || {}; const className = { base: embedBlockTheme.base || '', focus: embedBlockTheme.focus || '' }; return /*#__PURE__*/React.createElement(TweetComponent, { className: className, format: this.__format, loadingComponent: "Loading...", nodeKey: this.getKey(), tweetID: this.__id }); } isInline() { return false; } } function $createTweetNode(tweetID) { return new TweetNode(tweetID); } function $isTweetNode(node) { return node instanceof TweetNode; } /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * */ const HR = { dependencies: [LexicalHorizontalRuleNode.HorizontalRuleNode], export: node => { return LexicalHorizontalRuleNode.$isHorizontalRuleNode(node) ? '***' : null; }, regExp: /^(---|\*\*\*|___)\s?$/, replace: (parentNode, _1, _2, isImport) => { const line = LexicalHorizontalRuleNode.$createHorizontalRuleNode(); // TODO: Get rid of isImport flag if (isImport || parentNode.getNextSibling() != null) { parentNode.replace(line); } else { parentNode.insertBefore(line); } line.selectNext(); }, type: 'element' }; const IMAGE = { dependencies: [ImageNode], export: (node, exportChildren, exportFormat) => { if (!$isImageNode(node)) { return null; } return `![${node.getAltText()}](${node.getSrc()})`; }, importRegExp: /!(?:\[([^[]*)\])(?:\(([^(]+)\))/, regExp: /!(?:\[([^[]*)\])(?:\(([^(]+)\))$/, replace: (textNode, match) => { const [, altText, src] = match; const imageNode = $createImageNode({ altText, maxWidth: 800, src }); textNode.replace(imageNode); }, trigger: ')', type: 'text-match' }; const TWEET = { dependencies: [TweetNode], export: node => { if (!$isTweetNode(node)) { return null; } return `<tweet id="${node.getId()}" />`; }, regExp: /<tweet id="([^"]+?)"\s?\/>\s?$/, replace: (textNode, _1, match) => { const [, id] = match; const tweetNode = $createTweetNode(id); textNode.replace(tweetNode); }, type: 'element' }; // Very primitive table setup const TABLE_ROW_REG_EXP = /^(?:\|)(.+)(?:\|)\s?$/; const TABLE = { // TODO: refactor transformer for new TableNode dependencies: [table.TableNode, table.TableRowNode, table.TableCellNode], export: (node, exportChildren) => { if (!table.$isTableNode(node)) { return null; } const output = []; for (const row of node.getChildren()) { const rowOutput = []; if (table.$isTableRowNode(row)) { for (const cell of row.getChildren()) { // It's TableCellNode (hence ElementNode) so it's just to make flow happy if (lexical.$isElementNode(cell)) { rowOutput.push(exportChildren(cell)); } } } output.push(`| ${rowOutput.join(' | ')} |`); } return output.join('\n'); }, regExp: TABLE_ROW_REG_EXP, replace: (parentNode, _1, match) => { const matchCells = mapToTableCells(match[0]); if (matchCells == null) { return; } const rows = [matchCells]; let sibling = parentNode.getPreviousSibling(); let maxCells = matchCells.length; while (sibling) { if (!lexical.$isParagraphNode(sibling)) { break; } if (sibling.getChildrenSize() !== 1) { break; } const firstChild = sibling.getFirstChild(); if (!lexical.$isTextNode(firstChild)) { break; } const cells = mapToTableCells(firstChild.getTextContent()); if (cells == null) { break; } maxCells = Math.max(maxCells, cells.length); rows.unshift(cells); const previousSibling = sibling.getPreviousSibling(); sibling.remove(); sibling = previousSibling; } const table$1 = table.$createTableNode(); for (const cells of rows) { const tableRow = table.$createTableRowNode(); table$1.append(tableRow); for (let i = 0; i < maxCells; i++) { tableRow.append(i < cells.length ? cells[i] : createTableCell(null)); } } const previousSibling = parentNode.getPreviousSibling(); if (table.$isTableNode(previousSibling) && getTableColumnsSize(previousSibling) === maxCells) { previousSibling.append(...table$1.getChildren()); parentNode.remove(); } else { parentNode.replace(table$1); } table$1.selectEnd(); }, type: 'element' }; function getTableColumnsSize(table$1) { const row = table$1.getFirstChild(); return table.$isTableRowNode(row) ? row.getChildrenSize() : 0; } const createTableCell = textContent => { const cell = table.$createTableCellNode(table.TableCellHeaderStates.NO_STATUS); const paragraph = lexical.$createParagraphNode(); if (textContent != null) { paragraph.append(lexical.$createTextNode(textContent.trim())); } cell.append(paragraph); return cell; }; const mapToTableCells = textContent => { // TODO: // For now plain text, single node. Can be expanded to more complex content // including formatted text const match = textContent.match(TABLE_ROW_REG_EXP); if (!match || !match[1]) { return null; } return match[1].split('|').map(text => createTableCell(text)); }; const PLAYGROUND_TRANSFORMERS = [TABLE, HR, IMAGE, TWEET, markdown.CHECK_LIST, ...markdown.ELEMENT_TRANSFORMERS, ...markdown.TEXT_FORMAT_TRANSFORMERS, ...markdown.TEXT_MATCH_TRANSFORMERS]; /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * */ async function validateEditorState(editor) { const stringifiedEditorState = JSON.stringify(editor.getEditorState()); let response = null; try { response = await fetch('http://localhost:1235/validateEditorState', { body: stringifiedEditorState, headers: { Accept: 'application/json', 'Content-type': 'application/json' }, method: 'POST' }); } catch {// NO-OP } if (response !== null && response.status === 403) { throw new Error('Editor state validation failed! Server did not accept changes.'); } } function ActionsPlugin({ isRichText }) { const [editor] = LexicalComposerContext.useLexicalComposerContext(); const [isEditable, setIsEditable] = React.useState(() => editor.isEditable()); React.useState(false); const [connected, setConnected] = React.useState(false); const [isEditorEmpty, setIsEditorEmpty] = React.useState(true); const [modal, showModal] = useModal(); const { isCollabActive } = LexicalCollaborationContext.useCollaborationContext(); React.useEffect(() => { return utils.mergeRegister(editor.registerEditableListener(editable => { setIsEditable(editable); }), editor.registerCommand(yjs$1.CONNECTED_COMMAND, payload => { const isConnected = payload; setConnected(isConnected); return false; }, lexical.COMMAND_PRIORITY_EDITOR)); }, [editor]); React.useEffect(() => { return editor.registerUpdateListener(({ dirtyElements, prevEditorState, tags }) => { // If we are in read only mode, send the editor state // to server and ask for validation if possible. if (!isEditable && dirtyElements.size > 0 && !tags.has('historic') && !tags.has('collaboration')) { validateEditorState(editor); } editor.getEditorState().read(() => { const root = lexical.$getRoot(); const children = root.getChildren(); if (children.length > 1) { setIsEditorEmpty(false); } else { if (lexical.$isParagraphNode(children[0])) { const paragraphChildren = children[0].getChildren(); setIsEditorEmpty(paragraphChildren.length === 0); } else { setIsEditorEmpty(false); } } }); }); }, [editor, isEditable]); React.useCallback(() => { editor.update(() => { const root = lexical.$getRoot(); const firstChild = root.getFirstChild(); if (code.$isCodeNode(firstChild) && firstChild.getLanguage() === 'markdown') { markdown.$convertFromMarkdownString(firstChild.getTextContent(), PLAYGROUND_TRANSFORMERS); } else { const markdown$1 = markdown.$convertToMarkdownString(PLAYGROUND_TRANSFORMERS); root.clear().append(code.$createCodeNode('markdown').append(lexical.$createTextNode(markdown$1))); } root.selectEnd(); }); }, [editor]); return /*#__PURE__*/React.createElement("div", { className: "actions" }, isCollabActive && /*#__PURE__*/React.createElement("button", { type: "button", className: "action-button connect", onClick: () => { editor.dispatchCommand(yjs$1.TOGGLE_CONNECT_COMMAND, !connected); }, title: `${connected ? 'Disconnect' : 'Connect'} Collaborative Editing`, "aria-label": `${connected ? 'Disconnect from' : 'Connect to'} a collaborative editing server` }, /*#__PURE__*/React.createElement("i", { className: connected ? 'disconnect' : 'connect' })), modal); } /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * */ const Context$1 = /*#__PURE__*/React.createContext([_cb => () => { return; }, _newSuggestion => { return; }]); const SharedAutocompleteContext = ({ children }) => { const context = React.useMemo(() => { let suggestion = null; const listeners = new Set(); return [cb => { cb(suggestion); listeners.add(cb); return () => { listeners.delete(cb); }; }, newSuggestion => { suggestion = newSuggestion; for (const listener of listeners) { listener(newSuggestion); } }]; }, []); return /*#__PURE__*/React.createElement(Context$1.Provider, { value: context }, children); }; const useSharedAutocompleteContext = () => { const [subscribe, publish] = React.useContext(Context$1); const [suggestion, setSuggestion] = React.useState(null); React.useEffect(() => { return subscribe(newSuggestion => { setSuggestion(newSuggestion); }); }, [subscribe]); return [suggestion, publish]; }; class AutocompleteNode extends lexical.DecoratorNode { // TODO add comment static clone(node) { return new AutocompleteNode(node.__key); } static getType() { return 'autocomplete'; } static importJSON(serializedNode) { const node = $createAutocompleteNode(serializedNode.uuid); return node; } exportJSON() { return { ...super.exportJSON(), type: 'autocomplete', uuid: this.__uuid, version: 1 }; } constructor(uuid, key) { super(key); _defineProperty(this, "__uuid", void 0); this.__uuid = uuid; } updateDOM(prevNode, dom, config) { return false; } createDOM(config) { return document.createElement('span'); } decorate() { if (this.__uuid !== uuid) { return null; } return /*#__PURE__*/React.createElement(AutocompleteComponent, null); } } function $createAutocompleteNode(uuid) { return new AutocompleteNode(uuid); } function AutocompleteComponent() { const [suggestion] = useSharedAutocompleteContext(); const userAgentData = window.navigator.userAgentData; const isMobile = userAgentData !== undefined ? userAgentData.mobile : window.innerWidth <= 800 && window.innerHeight <= 600; // TODO Move to theme return /*#__PURE__*/React.createElement("span", { style: { color: '#ccc' }, spellCheck: "false" }, suggestion, " ", isMobile ? '(SWIPE \u2B95)' : '(TAB)'); } /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * */ const elements = new WeakMap(); function readTouch(e) { const touch = e.changedTouches[0]; if (touch === undefined) { return null; } return [touch.clientX, touch.clientY]; } function addListener(element, cb) { let elementValues = elements.get(element); if (elementValues === undefined) { const listeners = new Set(); const handleTouchstart = e => { if (elementValues !== undefined) { elementValues.start = readTouch(e); } }; const handleTouchend = e => { if (elementValues === undefined) { return; } const start = elementValues.start; if (start === null) { return; } const end = readTouch(e); for (const listener of listeners) { if (end !== null) { listener([end[0] - start[0], end[1] - start[1]], e); } } }; element.addEventListener('touchstart', handleTouchstart); element.addEventListener('touchend', handleTouchend); elementValues = { handleTouchend, handleTouchstart, listeners, start: null }; elements.set(element, elementValues); } elementValues.listeners.add(cb); return () => deleteListener(element, cb); } function deleteListener(element, cb) { const elementValues = elements.get(element); if (elementValues === undefined) { return; } const listeners = elementValues.listeners; listeners.delete(cb); if (listeners.size === 0) { elements.delete(element); element.removeEventListener('touchstart', elementValues.handleTouchstart); element.removeEventListener('touchend', elementValues.handleTouchend); } } function addSwipeRightListener(element, cb) { return addListener(element, (force, e) => { const [x, y] = force; if (x > 0 && x > Math.abs(y)) { cb(x, e); } }); } const uuid = Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 5); // TODO lookup should be custom function $search(selection$1) { if (!lexical.$isRangeSelection(selection$1) || !selection$1.isCollapsed()) { return [false, '']; } const node = selection$1.getNodes()[0]; const anchor = selection$1.anchor; // Check siblings? if (!lexical.$isTextNode(node) || !node.isSimpleText() || !selection.$isAtNodeEnd(anchor)) { return [false, '']; } const word = []; const text = node.getTextContent(); let i = node.getTextContentSize(); let c; while (i-- && i >= 0 && (c = text[i]) !== ' ') { word.push(c); } if (word.length === 0) { return [false, '']; } return [true, word.reverse().join('')]; } // TODO query should be custom function useQuery() { return React.useCallback(searchText => { const server = new AutocompleteServer(); console.time('query'); const response = server.query(searchText); console.timeEnd('query'); return response; }, []); } function AutocompletePlugin() { const [editor] = LexicalComposerContext.useLexicalComposerContext(); const [, setSuggestion] = useSharedAutocompleteContext(); const query = useQuery(); React.useEffect(() => { let autocompleteNodeKey = null; let lastMatch = null; let lastSuggestion = null; let searchPromise = null; function $clearSuggestion() { const autocompleteNode = autocompleteNodeKey !== null ? lexical.$getNodeByKey(autocompleteNodeKey) : null; if (autocompleteNode !== null && autocompleteNode.isAttached()) { autocompleteNode.remove(); autocompleteNodeKey = null; } if (searchPromise !== null) { searchPromise.dismiss(); searchPromise = null; } lastMatch = null; lastSuggestion = null; setSuggestion(null); } function updateAsyncSuggestion(refSearchPromise, newSuggestion) { if (searchPromise !== refSearchPromise || newSuggestion === null) { // Outdated or no suggestion return; } editor.update(() => { const selection = lexical.$getSelection(); const [hasMatch, match] = $search(selection); if (!hasMatch || match !== lastMatch || !lexical.$isRangeSelection(selection)) { // Outdated return; } const selectionCopy = selection.clone(); const node = $createAutocompleteNode(uuid); autocompleteNodeKey = node.getKey(); selection.insertNodes([node]); lexical.$setSelection(selectionCopy); lastSuggestion = newSuggestion; setSuggestion(newSuggestion); }, { tag: 'history-merge' }); } function handleAutocompleteNodeTransform(node) { const key = node.getKey(); if (node.__uuid === uuid && key !== autocompleteNodeKey) { // Max one Autocomplete node per session $clearSuggestion(); } } function handleUpdate() { editor.update(() => { const selection = lexical.$getSelection(); const [hasMatch, match] = $search(selection); if (!hasMatch) { $clearSuggestion(); return; } if (match === lastMatch) { return; } $clearSuggestion(); searchPromise = query(match); searchPromise.promise.then(newSuggestion => { if (searchPromise !== null) { updateAsyncSuggestion(searchPromise, newSuggestion); } }).catch(e => { console.error(e); }); lastMatch = match; }); } function $handleAutocompleteIntent() { if (lastSuggestion === null || autocompleteNodeKey === null) { return false; } const autocompleteNode = lexical.$getNodeByKey(autocompleteNodeKey); if (autocompleteNode === null) { return false; } const textNode = lexical.$createTextNode(lastSuggestion); autocompleteNode.replace(textNode); textNode.selectNext(); $clearSuggestion(); return true; } function $handleKeypressCommand(e) { if ($handleAutocompleteIntent()) { e.preventDefault(); return true; } return false; } function handleSwipeRight(_force, e) { editor.update(() => { if ($handleAutocompleteIntent()) { e.preventDefault(); } }); } function unmountSuggestion() { editor.update(() => { $clearSuggestion(); }); } const rootElem = editor.getRootElement(); return utils.mergeRegister(editor.registerNodeTransform(AutocompleteNode, handleAutocompleteNodeTransform), editor.registerUpdateListener(handleUpdate), editor.registerCommand(lexical.KEY_TAB_COMMAND, $handleKeypressCommand, lexical.COMMAND_PRIORITY_LOW), editor.registerCommand(lexical.KEY_ARROW_RIGHT_COMMAND, $handleKeypressCommand, lexical.COMMAND_PRIORITY_LOW), ...(rootElem !== null ? [addSwipeRightListener(rootElem, handleSwipeRight)] : []), unmountSuggestion); }, [editor, query, setSuggestion]); return null; } /* * Simulate an asynchronous autocomplete server (typical in more common use cases like GMail where * the data is not static). */ class AutocompleteServer { constructor() { _defineProperty(this, "DATABASE", DICTIONARY); _defineProperty(this, "LATENCY", 200); _defineProperty(this, "query", searchText => { let isDismissed = false; const dismiss = () => { isDismissed = true; }; const promise = new Promise((resolve, reject) => { setTimeout(() => { if (isDismissed) { // TODO cache result return reject('Dismissed'); } const searchTextLength = searchText.length; if (searchText === '' || searchTextLength < 4) { return resolve(null); } const char0 = searchText.charCodeAt(0); const isCapitalized = char0 >= 65 && char0 <= 90; const caseInsensitiveSearchText = isCapitalized ? String.fromCharCode(char0 + 32) + searchText.substring(1) : searchText; const match = this.DATABASE.find(dictionaryWord => dictionaryWord.startsWith(caseInsensitiveSearchText) ?? null); if (match === undefined) { return resolve(null); } const matchCapitalized = isCapitalized ? String.fromCharCode(match.charCodeAt(0) - 32) + match.substring(1) : match; const autocompleteChunk = matchCapitalized.substring(searchTextLength); if (autocompleteChunk === '') { return resolve(null); } return resolve(autocompleteChunk); }, this.LATENCY); }); return { dismiss, promise }; }); } } // https://raw.githubusercontent.com/first20hours/google-10000-english/master/google-10000-english-usa-no-swears-long.txt const DICTIONARY = ['information', 'available', 'copyright', 'university', 'management', 'international', 'development', 'education', 'community', 'technology', 'following', 'resources', 'including', 'directory', 'government', 'department', 'description', 'insurance', 'different', 'categories', 'conditions', 'accessories', 'september', 'questions', 'application', 'financial', 'equipment', 'performance', 'experience', 'important', 'activities', 'additional', 'something', 'professional', 'committee', 'washington', 'california', 'reference', 'companies', 'computers', 'president', 'australia', 'discussion', 'entertainment', 'agreement', 'marketing', 'association', 'collection', 'solutions', 'electronics', 'technical', 'microsoft', 'conference', 'environment', 'statement', 'downloads', 'applications', 'requirements', 'individual', 'subscribe', 'everything', 'production', 'commercial', 'advertising', 'treatment', 'newsletter', 'knowledge', 'currently', 'construction', 'registered', 'protection', 'engineering', 'published', 'corporate', 'customers', 'materials', 'countries', 'standards', 'political', 'advertise', 'environmental', 'availability', 'employment', 'commission', 'administration', 'institute', 'sponsored', 'electronic', 'condition', 'effective', 'organization', 'selection', 'corporation', 'executive', 'necessary', 'according', 'particular', 'facilities', 'opportunities', 'appropriate', 'statistics', 'investment', 'christmas', 'registration', 'furniture', 'wednesday', 'structure', 'distribution', 'industrial', 'potential', 'responsible', 'communications', 'associated', 'foundation', 'documents', 'communication', 'independent', 'operating', 'developed', 'telephone', 'population', 'navigation', 'operations', 'therefore', 'christian', 'understand', 'publications', 'worldwide', 'connection', 'publisher', 'introduction', 'properties', 'accommodation', 'excellent', 'opportunity', 'assessment', 'especially', 'interface', 'operation', 'restaurants', 'beautiful', 'locations', 'significant', 'technologies', 'manufacturer', 'providing', 'authority', 'considered', 'programme', 'enterprise', 'educational', 'employees', 'alternative', 'processing', 'responsibility', 'resolution', 'publication', 'relations', 'photography', 'components', 'assistance', 'completed', 'organizations', 'otherwise', 'transportation', 'disclaimer', 'membership', 'recommended', 'background', 'character', 'maintenance', 'functions', 'trademarks', 'phentermine', 'submitted', 'television', 'interested', 'throughout', 'established', 'programming', 'regarding', 'instructions', 'increased', 'understanding', 'beginning', 'associates', 'instruments', 'businesses', 'specified', 'restaurant', 'procedures', 'relationship', 'traditional', 'sometimes', 'themselves', 'transport', 'interesting', 'evaluation', 'implementation', 'galleries', 'references', 'presented', 'literature', 'respective', 'definition', 'secretary', 'networking', 'australian', 'magazines', 'francisco', 'individuals', 'guidelines', 'installation', 'described', 'attention', 'difference', 'regulations', 'certificate', 'directions', 'documentation', 'automotive', 'successful', 'communities', 'situation', 'publishing', 'emergency', 'developing', 'determine', 'temperature', 'announcements', 'historical', 'ringtones', 'difficult', 'scientific', 'satellite', 'particularly', 'functional', 'monitoring', 'architecture', 'recommend', 'dictionary', 'accounting', 'manufacturing', 'professor', 'generally', 'continued', 'techniques', 'permission', 'generation', 'component', 'guarantee', 'processes', 'interests', 'paperback', 'classifieds', 'supported', 'competition', 'providers', 'characters', 'thousands', 'apartments', 'generated', 'administrative', 'practices', 'reporting', 'essential', 'affiliate', 'immediately', 'designated', 'integrated', 'configuration', 'comprehensive', 'universal', 'presentation', 'languages', 'compliance', 'improvement', 'pennsylvania', 'challenge', 'acceptance', 'strategies', 'affiliates', 'multimedia', 'certified', 'computing', 'interactive', 'procedure', 'leadership', 'religious', 'breakfast', 'developer', 'approximately', 'recommendations', 'comparison', 'automatically', 'minnesota', 'adventure', 'institutions', 'assistant', 'advertisement', 'headlines', 'yesterday', 'determined', 'wholesale', 'extension', 'statements', 'completely', 'electrical', 'applicable', 'manufacturers', 'classical', 'dedicated', 'direction', 'basketball', 'wisconsin', 'personnel', 'identified', 'professionals', 'advantage', 'newsletters', 'estimated', 'anonymous', 'miscellaneous', 'integration', 'interview', 'framework', 'installed', 'massachusetts', 'associate', 'frequently', 'discussions', 'laboratory', 'destination', 'intelligence', 'specifications', 'tripadvisor', 'residential', 'decisions', 'industries', 'partnership', 'editorial', 'expression', 'provisions', 'principles', 'suggestions', 'replacement', 'strategic', 'economics', 'compatible', 'apartment', 'netherlands', 'consulting', 'recreation', 'participants', 'favorites', 'translation', 'estimates', 'protected', 'philadelphia', 'officials', 'contained', 'legislation', 'parameters', 'relationships', 'tennessee', 'representative', 'frequency', 'introduced', 'departments', 'residents', 'displayed', 'performed', 'administrator', 'addresses', 'permanent', 'agriculture', 'constitutes', 'portfolio', 'practical', 'delivered', 'collectibles', 'infrastructure', 'exclusive', 'originally', 'utilities', 'philosophy', 'regulation', 'reduction', 'nutrition', 'recording', 'secondary', 'wonderful', 'announced', 'prevention', 'mentioned', 'automatic', 'healthcare', 'maintained', 'increasing', 'connected', 'directors', 'participation', 'containing', 'combination', 'amendment', 'guaranteed', 'libraries', 'distributed', 'singapore', 'enterprises', 'convention', 'principal', 'certification', 'previously', 'buildings', 'household', 'batteries', 'positions', 'subscription', 'contemporary', 'panasonic', 'permalink', 'signature', 'provision', 'certainly', 'newspaper', 'liability', 'trademark', 'trackback', 'americans', 'promotion', 'conversion', 'reasonable', 'broadband', 'influence', 'importance', 'webmaster', 'prescription', 'specifically', 'represent', 'conservation', 'louisiana', 'javascript', 'marketplace', 'evolution', 'certificates', 'objectives', 'suggested', 'concerned', 'structures', 'encyclopedia', 'continuing', 'interracial', 'competitive', 'suppliers', 'preparation', 'receiving', 'accordance', 'discussed', 'elizabeth', 'reservations', 'playstation', 'instruction', 'annotation', 'differences', 'establish', 'expressed', 'paragraph', 'mathematics', 'compensation', 'conducted', 'percentage', 'mississippi', 'requested', 'connecticut', 'personals', 'immediate', 'agricultural', 'supporting', 'collections', 'participate', 'specialist', 'experienced', 'investigation', 'institution', 'searching', 'proceedings', 'transmission', 'characteristics', 'experiences', 'extremely', 'verzeichnis', 'contracts', 'concerning', 'developers', 'equivalent', 'chemistry', 'neighborhood', 'variables', 'continues', 'curriculum', 'psychology', 'responses', 'circumstances', 'identification', 'appliances', 'elementary', 'unlimited', 'printable', 'enforcement', 'hardcover', 'celebrity', 'chocolate', 'hampshire', 'bluetooth', 'controlled', 'requirement', 'authorities', 'representatives', 'pregnancy', 'biography', 'attractions', 'transactions', 'authorized', 'retirement', 'financing', 'efficiency', 'efficient', 'commitment', 'specialty', 'interviews', 'qualified', 'discovery', 'classified', 'confidence', 'lifestyle', 'consistent', 'clearance', 'connections', 'inventory', 'converter', 'organisation', 'objective', 'indicated', 'securities', 'volunteer', 'democratic', 'switzerland', 'parameter', 'processor', 'dimensions', 'contribute', 'challenges', 'recognition', 'submission', 'encourage', 'regulatory', 'inspection', 'consumers', 'territory', 'transaction', 'manchester', 'contributions', 'continuous', 'resulting', 'cambridge', 'initiative', 'execution', 'disability', 'increases', 'contractor', 'examination', 'indicates', 'committed', 'extensive', 'affordable', 'candidate', 'databases', 'outstanding', 'perspective', 'messenger', 'tournament', 'consideration', 'discounts', 'catalogue', 'publishers', 'caribbean', 'reservation', 'remaining', 'depending', 'expansion', 'purchased', 'performing', 'collected', 'absolutely', 'featuring', 'implement', 'scheduled', 'calculator', 'significantly', 'temporary', 'sufficient', 'awareness', 'vancouver', 'contribution', 'measurement', 'constitution', 'packaging', 'consultation', 'northwest', 'classroom', 'democracy', 'wallpaper', 'merchandise', 'resistance', 'baltimore', 'candidates', 'charlotte', 'biological', 'transition', 'preferences', 'instrument', 'classification', 'physician', 'hollywood', 'wikipedia', 'spiritual', 'photographs', 'relatively', 'satisfaction', 'represents', 'pittsburgh', 'preferred', 'intellectual', 'comfortable', 'interaction', 'listening', 'effectively', 'experimental', 'revolution', 'consolidation', 'landscape', 'dependent', 'mechanical', 'consultants', 'applicant', 'cooperation', 'acquisition', 'implemented', 'directories', 'recognized', 'notification', 'licensing', 'textbooks', 'diversity', 'cleveland', 'investments', 'accessibility', 'sensitive', 'templates', 'completion', 'universities', 'technique', 'contractors', 'subscriptions', 'calculate', 'alexander', 'broadcast', 'converted', 'anniversary', 'improvements', 'specification', 'accessible', 'accessory', 'typically', 'representation', 'arrangements', 'conferences', 'uniprotkb', 'consumption', 'birmingham', 'afternoon', 'consultant', 'controller', 'ownership', 'committees', 'legislative', 'researchers', 'unsubscribe', 'molecular', 'residence', 'attorneys', 'operators', 'sustainable', 'philippines', 'statistical', 'innovation', 'employers', 'definitions', 'elections', 'stainless', 'newspapers', 'hospitals', 'exception', 'successfully', 'indonesia', 'primarily', 'capabilities', 'recommendation', 'recruitment', 'organized', 'improving', 'expensive', 'organisations', 'explained', 'programmes', 'expertise', 'mechanism', 'jewellery', 'eventually',