text-editor-drcsystems
Version:
Text Editor Made with Love by DRC Systems
1,469 lines (1,249 loc) • 811 kB
JavaScript
/**
* 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 `})`;
},
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',