UNPKG

@edtr-io/plugin-text

Version:
1,557 lines (1,528 loc) 55.6 kB
import { serializedScalar } from '@edtr-io/plugin'; import { Element, Editor, Range, Transforms, Node, createEditor } from 'slate'; import { HotKeys } from '@edtr-io/core'; import { HoverOverlay } from '@edtr-io/editor-ui/beta'; import { withLists as withLists$1, withListsReact, ListType, ListsEditor, onKeyDown } from '@prezly/slate-lists'; import React__default, { useCallback, useMemo, useState, useContext, useRef, useEffect, createElement, Fragment, forwardRef } from 'react'; import { useSlate, ReactEditor, useSelected, withReact, Slate, Editable } from 'slate-react'; import { styled, EdtrIcon, edtrColorText, edtrBold, edtrItalic, edtrLink, edtrText, edtrClose, edtrListNumbered, edtrListBullets, edtrFormula, Icon, faCode, merge, useTheme, faExternalLinkAlt, faTrashAlt } from '@edtr-io/ui'; import isHotkey from 'is-hotkey'; import { useScopedStore, PreferenceContext } from '@edtr-io/core/beta'; import { RegistryContext } from '@edtr-io/plugin-rows/internal'; import { replace } from '@edtr-io/store'; import { has } from 'ramda'; import { styled as styled$1, EditorBottomToolbar } from '@edtr-io/editor-ui'; import { MathEditor } from '@edtr-io/math'; import KaTeX from 'katex'; 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); } function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; } function _createForOfIteratorHelperLoose(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (it) return (it = it.call(o)).next.bind(it); if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; return function () { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var HoveringToolbarColorIcon = /*#__PURE__*/styled.div(function (_ref) { var color = _ref.color; return { display: 'inline-block', backgroundColor: color, borderRadius: ' 100%', width: '19px', height: '19px', margin: '3px', verticalAlign: 'middle' }; }); var ColoredText = /*#__PURE__*/styled.span({ position: 'relative', verticalAlign: 'middle', display: 'inline-block' }); var FlexContainer = /*#__PURE__*/styled.span({ display: 'flex', alignItems: 'center', flexDirection: 'column' }); var Line = /*#__PURE__*/styled.span(function (_ref) { var index = _ref.index, colorsTheme = _ref.colorsTheme; return { border: "2px solid " + (index === undefined ? colorsTheme.defaultColor : colorsTheme.colors[index % colorsTheme.colors.length]), borderRadius: '4px', bottom: '0', width: '80%', position: 'absolute' }; }); var HoveringToolbarColorTextIcon = function HoveringToolbarColorTextIcon(_ref2) { var index = _ref2.index, colorsTheme = _ref2.colorsTheme; return React__default.createElement(ColoredText, null, React__default.createElement(FlexContainer, null, React__default.createElement(EdtrIcon, { icon: edtrColorText }), React__default.createElement(Line, { colorsTheme: colorsTheme, index: index }))); }; var withLinks = function withLinks(editor) { var isInline = editor.isInline; editor.isInline = function (element) { return element.type === 'a' ? true : isInline(element); }; return editor; }; // TODO: Use enum for types here as in https://www.npmjs.com/package/@prezly/slate-lists // TODO: Fix "as ..." parts in the functions var withLists = function withLists(editor) { var editorWithListsPlugin = withLists$1({ isConvertibleToListTextNode: function isConvertibleToListTextNode(node) { return Element.isElementType(node, 'p'); }, isDefaultTextNode: function isDefaultTextNode(node) { return Element.isElementType(node, 'p'); }, isListNode: function isListNode(node, type) { if (type) { return Element.isElementType(node, type); } return Element.isElementType(node, 'ordered-list') || Element.isElementType(node, 'unordered-list'); }, isListItemNode: function isListItemNode(node) { return Element.isElementType(node, 'list-item'); }, isListItemTextNode: function isListItemTextNode(node) { return Element.isElementType(node, 'list-item-child'); }, createDefaultTextNode: function createDefaultTextNode(props) { if (props === void 0) { props = {}; } return _extends({ children: [{ text: '' }] }, props, { type: 'p' }); }, createListNode: function createListNode(type, props) { if (type === void 0) { type = ListType.UNORDERED; } if (props === void 0) { props = {}; } var nodeType = type === ListType.ORDERED ? 'ordered-list' : 'unordered-list'; return _extends({ children: [{ text: '' }] }, props, { type: nodeType }); }, createListItemNode: function createListItemNode(props) { if (props === void 0) { props = {}; } return _extends({ children: [{ text: '' }] }, props, { type: 'list-item' }); }, createListItemTextNode: function createListItemTextNode(props) { if (props === void 0) { props = {}; } return _extends({ children: [{ text: '' }] }, props, { type: 'list-item-child' }); } }); return withListsReact(editorWithListsPlugin(editor)); }; var withMath = function withMath(editor) { var isInline = editor.isInline, isVoid = editor.isVoid; editor.isInline = function (element) { return element.type === 'math' ? true : isInline(element); }; editor.isVoid = function (element) { return element.type === 'math' ? true : isVoid(element); }; return editor; }; /** @public */ var TextEditorControl; (function (TextEditorControl) { TextEditorControl["code"] = "code"; TextEditorControl["colors"] = "colors"; TextEditorControl["headings"] = "headings"; TextEditorControl["katex"] = "katex"; TextEditorControl["links"] = "links"; TextEditorControl["lists"] = "lists"; TextEditorControl["math"] = "math"; TextEditorControl["paragraphs"] = "paragraphs"; TextEditorControl["richText"] = "richText"; })(TextEditorControl || (TextEditorControl = {})); var isAnyColorActive = function isAnyColorActive(editor) { var _SlateEditor$marks; return typeof ((_SlateEditor$marks = Editor.marks(editor)) == null ? void 0 : _SlateEditor$marks.color) === 'number'; }; var isColorActive = function isColorActive(colorIndex) { return function (editor) { var _SlateEditor$marks2; return ((_SlateEditor$marks2 = Editor.marks(editor)) == null ? void 0 : _SlateEditor$marks2.color) === colorIndex; }; }; var resetColor = function resetColor(editor) { Editor.removeMark(editor, 'color'); }; var toggleColor = function toggleColor(colorIndex) { return function (editor) { if (isColorActive(colorIndex)(editor)) { Editor.removeMark(editor, 'color'); } else { Editor.addMark(editor, 'color', colorIndex); } }; }; var getColorIndex = function getColorIndex(editor) { var _SlateEditor$marks3; return (_SlateEditor$marks3 = Editor.marks(editor)) == null ? void 0 : _SlateEditor$marks3.color; }; function selectionHasElement(predicate, editor) { var selection = editor.selection; if (!selection) return false; var _Array$from = Array.from(Editor.nodes(editor, { at: Editor.unhangRange(editor, selection), match: function match(n) { return !Editor.isEditor(n) && Element.isElement(n) && predicate(n); } })), match = _Array$from[0]; return !!match; } function trimSelection(editor) { var selection = editor.selection; if (!selection) return null; var selectedText = Editor.string(editor, selection); var isBackwardSelection = Range.isBackward(selection); var anchorOffset = selection.anchor.offset; var focusOffset = selection.focus.offset; while (selectedText.startsWith(' ')) { isBackwardSelection ? focusOffset++ : anchorOffset++; selectedText = selectedText.substring(1); } while (selectedText.endsWith(' ')) { isBackwardSelection ? anchorOffset-- : focusOffset--; selectedText = selectedText.substring(0, selectedText.length - 1); } Transforms.setSelection(editor, { anchor: _extends({}, selection.anchor, { offset: anchorOffset }), focus: _extends({}, selection.focus, { offset: focusOffset }) }); } function matchLinks(node) { return Element.isElement(node) && node.type === 'a'; } function isLinkActive(editor) { return selectionHasElement(function (e) { return e.type === 'a'; }, editor); } function getLinkElement(editor) { var _Array$from = Array.from(Editor.nodes(editor, { match: matchLinks })), match = _Array$from[0]; return match && match[0]; } function toggleLink(editor) { if (isLinkActive(editor)) { Transforms.unwrapNodes(editor, { match: matchLinks }); return; } var selection = editor.selection; var isCollapsed = selection && Range.isCollapsed(selection); if (isCollapsed) { Transforms.insertNodes(editor, { type: 'a', href: '', children: [{ text: ' ' }] }); return; } Transforms.wrapNodes(editor, { type: 'a', href: '', children: [] }, { split: true }); Transforms.collapse(editor, { edge: 'end' }); } function isOrderedListActive(editor) { return selectionHasElement(function (e) { return e.type === 'ordered-list'; }, editor); } function isUnorderedListActive(editor) { return selectionHasElement(function (e) { return e.type === 'unordered-list'; }, editor); } function toggleOrderedList(editor) { if (isUnorderedListActive(editor)) { ListsEditor.unwrapList(editor); ListsEditor.wrapInList(editor, ListType.ORDERED); } else if (isOrderedListActive(editor)) { ListsEditor.unwrapList(editor); } else { ListsEditor.wrapInList(editor, ListType.ORDERED); } } function toggleUnorderedList(editor) { if (isOrderedListActive(editor)) { ListsEditor.unwrapList(editor); ListsEditor.wrapInList(editor, ListType.UNORDERED); } else if (isUnorderedListActive(editor)) { ListsEditor.unwrapList(editor); } else { ListsEditor.wrapInList(editor, ListType.UNORDERED); } } function isMathActive(editor) { return selectionHasElement(function (e) { return e.type === 'math'; }, editor); } function toggleMath(editor) { if (isMathActive(editor)) { Transforms.removeNodes(editor, { match: function match(n) { return Element.isElement(n) && n.type === 'math'; } }); } else { var selection = editor.selection; if (!selection) return; var isCollapsed = Range.isCollapsed(selection); if (isCollapsed) { Transforms.insertNodes(editor, { type: 'math', src: '', inline: true, children: [{ text: '' }] }); } else { Transforms.insertNodes(editor, [{ type: 'math', src: Editor.string(editor, selection) || '', inline: true, children: [] }], { at: selection }); Transforms.move(editor, { distance: 1, reverse: true }); } } } function isBoldActive(editor) { var _SlateEditor$marks; return ((_SlateEditor$marks = Editor.marks(editor)) == null ? void 0 : _SlateEditor$marks.strong) === true; } function toggleBoldMark(editor) { trimSelection(editor); if (isBoldActive(editor)) { Editor.removeMark(editor, 'strong'); } else { Editor.addMark(editor, 'strong', true); } } function isItalicActive(editor) { var _SlateEditor$marks2; return ((_SlateEditor$marks2 = Editor.marks(editor)) == null ? void 0 : _SlateEditor$marks2.em) === true; } function toggleItalicMark(editor) { trimSelection(editor); if (isItalicActive(editor)) { Editor.removeMark(editor, 'em'); } else { Editor.addMark(editor, 'em', true); } } function isCodeActive(editor) { var _SlateEditor$marks3; return ((_SlateEditor$marks3 = Editor.marks(editor)) == null ? void 0 : _SlateEditor$marks3.code) === true; } function toggleCode(editor) { if (isCodeActive(editor)) { Editor.removeMark(editor, 'code'); } else { Editor.addMark(editor, 'code', true); } } function isAnyHeadingActive(editor) { return selectionHasElement(function (e) { return e.type === 'h'; }, editor); } var isHeadingActive = function isHeadingActive(heading) { return function (editor) { return selectionHasElement(function (e) { return e.type === 'h' && e.level === heading; }, editor); }; }; var toggleHeading = function toggleHeading(heading) { return function (editor) { if (isHeadingActive(heading)(editor)) { Transforms.setNodes(editor, { type: 'p' }); Transforms.unsetNodes(editor, 'level'); } else { Transforms.setNodes(editor, { type: 'h', level: heading }); } }; }; var _textPluginsMapper; var textPluginsMapper = (_textPluginsMapper = {}, _textPluginsMapper[TextEditorControl.math] = withMath, _textPluginsMapper[TextEditorControl.links] = withLinks, _textPluginsMapper[TextEditorControl.lists] = withLists, _textPluginsMapper); var isRegisteredTextPlugin = function isRegisteredTextPlugin(control) { return control in textPluginsMapper; }; var toggleLinkAndFlag = function toggleLinkAndFlag(setIsLinkNewlyCreated) { return function (editor) { toggleLink(editor); setIsLinkNewlyCreated(true); }; }; var registeredHotkeys = function registeredHotkeys(setIsLinkNewlyCreated) { return [{ hotkey: 'mod+b', control: TextEditorControl.richText, handler: toggleBoldMark }, { hotkey: 'mod+i', control: TextEditorControl.richText, handler: toggleItalicMark }, { hotkey: 'mod+k', control: TextEditorControl.links, handler: toggleLinkAndFlag(setIsLinkNewlyCreated) }, { hotkey: 'mod+m', control: TextEditorControl.math, handler: toggleMath }]; }; var useControls = function useControls(config, setIsLinkNewlyCreated) { var controls = config.controls; var createTextEditor = useCallback(function (baseEditor) { return controls.reduce(function (currentEditor, currentControl) { // If there is no control initialization function for the current control, // return the editor as it was received if (!isRegisteredTextPlugin(currentControl)) { return currentEditor; } // Otherwise, apply the control initialization functions to the editor return textPluginsMapper[currentControl](currentEditor); }, baseEditor); }, [controls]); var toolbarControls = useMemo(function () { return createToolbarControls(config, setIsLinkNewlyCreated); }, [config, setIsLinkNewlyCreated]); var handleHotkeys = useCallback(function (event, editor) { // Go through the registered hotkeys for (var _iterator = _createForOfIteratorHelperLoose(registeredHotkeys(setIsLinkNewlyCreated)), _step; !(_step = _iterator()).done;) { var _step$value = _step.value, hotkey = _step$value.hotkey, control = _step$value.control, handler = _step$value.handler; // Check if their respective control is enabled // and if the keyboard event contains the hotkey combination if (controls.includes(control) && isHotkey(hotkey, event)) { // If so, prevent the default event behavior, // handle the hotkey and break out of the loop event.preventDefault(); handler(editor); break; } } }, [controls, setIsLinkNewlyCreated]); return { createTextEditor: createTextEditor, toolbarControls: toolbarControls, handleHotkeys: handleHotkeys }; }; function createToolbarControls(_ref, setIsLinkNewlyCreated) { var i18n = _ref.i18n, theme = _ref.theme, controls = _ref.controls; var allControls = [ // Bold { name: TextEditorControl.richText, title: i18n.richText.toggleStrongTitle, isActive: isBoldActive, onClick: toggleBoldMark, renderIcon: function renderIcon() { return React__default.createElement(EdtrIcon, { icon: edtrBold }); } }, // Italic { name: TextEditorControl.richText, title: i18n.richText.toggleEmphasizeTitle, isActive: isItalicActive, onClick: toggleItalicMark, renderIcon: function renderIcon() { return React__default.createElement(EdtrIcon, { icon: edtrItalic }); } }, // Link { name: TextEditorControl.links, title: i18n.link.toggleTitle, isActive: isLinkActive, onClick: toggleLinkAndFlag(setIsLinkNewlyCreated), renderIcon: function renderIcon() { return React__default.createElement(EdtrIcon, { icon: edtrLink }); } }, // Headings { name: TextEditorControl.headings, title: i18n.headings.openMenuTitle, closeMenuTitle: i18n.headings.closeMenuTitle, isActive: isAnyHeadingActive, renderIcon: function renderIcon() { return React__default.createElement(EdtrIcon, { icon: edtrText }); }, renderCloseMenuIcon: function renderCloseMenuIcon() { return React__default.createElement(EdtrIcon, { icon: edtrClose }); }, children: theme.controls.headings.map(function (heading) { return { name: TextEditorControl.headings, title: i18n.headings.setHeadingTitle(heading), isActive: isHeadingActive(heading), onClick: toggleHeading(heading), renderIcon: function renderIcon() { return React__default.createElement("span", null, "H", heading); } }; }) }, // Colors { name: TextEditorControl.colors, title: i18n.colors.openMenuTitle, closeMenuTitle: i18n.colors.closeMenuTitle, isActive: function isActive() { return false; }, renderIcon: function renderIcon(editor) { return React__default.createElement(HoveringToolbarColorTextIcon, { index: getColorIndex(editor), colorsTheme: theme.controls.colors }); }, renderCloseMenuIcon: function renderCloseMenuIcon() { return React__default.createElement(EdtrIcon, { icon: edtrClose }); }, children: [{ name: TextEditorControl.colors, title: i18n.colors.resetColorTitle, isActive: function isActive(editor) { return !isAnyColorActive(editor); }, onClick: resetColor, renderIcon: function renderIcon() { return React__default.createElement(HoveringToolbarColorIcon, { color: theme.controls.colors.defaultColor }); } }].concat(theme.controls.colors.colors.map(function (color, colorIndex) { return { name: TextEditorControl.colors, title: i18n.colors.colorNames[colorIndex], isActive: isColorActive(colorIndex), onClick: toggleColor(colorIndex), renderIcon: function renderIcon() { return React__default.createElement(HoveringToolbarColorIcon, { color: color }); } }; })) }, // Ordered list { name: TextEditorControl.lists, title: i18n.list.toggleOrderedList, isActive: isOrderedListActive, onClick: toggleOrderedList, renderIcon: function renderIcon() { return React__default.createElement(EdtrIcon, { icon: edtrListNumbered }); } }, // Unordered list { name: TextEditorControl.lists, title: i18n.list.toggleUnorderedList, isActive: isUnorderedListActive, onClick: toggleUnorderedList, renderIcon: function renderIcon() { return React__default.createElement(EdtrIcon, { icon: edtrListBullets }); } }, // Math { name: TextEditorControl.math, title: i18n.math.toggleTitle, isActive: isMathActive, onClick: toggleMath, renderIcon: function renderIcon() { return React__default.createElement(EdtrIcon, { icon: edtrFormula }); } }, // Code { name: TextEditorControl.code, title: i18n.code.toggleTitle, isActive: isCodeActive, onClick: toggleCode, renderIcon: function renderIcon() { return React__default.createElement(Icon, { icon: faCode }); } }]; return allControls.filter(function (control) { return controls.includes(TextEditorControl[control.name]); }); } var hotKeysMap = { SELECT_UP: 'up', SELECT_DOWN: 'down', INSERT: 'enter' }; var useSuggestions = function useSuggestions(args) { var _useState = useState(0), selected = _useState[0], setSelected = _useState[1]; var store = useScopedStore(); var text = args.text, id = args.id, editable = args.editable, focused = args.focused; var plugins = useContext(RegistryContext); var allOptions = mapPlugins(plugins, text); var showSuggestions = editable && focused && text.startsWith('/') && allOptions.length > 0; var options = showSuggestions ? allOptions : []; var currentValue = text.substring(1); var closure = useRef({ showSuggestions: showSuggestions, selected: selected, options: options }); closure.current = { showSuggestions: showSuggestions, selected: selected, options: options }; useEffect(function () { if (options.length < selected) { setSelected(0); } }, [options.length, selected]); var handleSelectionChange = function handleSelectionChange(direction) { return function () { if (closure.current.showSuggestions) { setSelected(function (currentSelected) { var optionsCount = closure.current.options.length; var value = direction === 'up' ? optionsCount - 1 : 1; if (optionsCount === 0) return 0; return (currentSelected + value) % optionsCount; }); } }; }; var handleSuggestionInsert = function handleSuggestionInsert() { if (closure.current.showSuggestions) { var option = closure.current.options[closure.current.selected]; if (!option) return; setTimeout(function () { insertPlugin(option.name); }); } }; var hotKeysHandlers = { SELECT_UP: handleSelectionChange('up'), SELECT_DOWN: handleSelectionChange('down'), INSERT: handleSuggestionInsert }; return { showSuggestions: showSuggestions, suggestionsProps: { options: options, currentValue: currentValue, selected: selected, onMouseDown: insertPlugin }, hotKeysProps: { keyMap: hotKeysMap, handlers: hotKeysHandlers }, handleHotkeys: handleHotkeys }; function insertPlugin(plugin) { store.dispatch(replace({ id: id, plugin: plugin })); } function handleHotkeys(event) { if (closure.current.showSuggestions) { if (['ArrowDown', 'ArrowUp', 'Enter'].includes(event.key)) { event.preventDefault(); return; } } } }; function mapPlugins(registry, text) { var search = text.replace('/', '').toLowerCase(); var startingWithSearchString = registry.filter(function (_ref) { var _title$toLowerCase; var title = _ref.title; if (!search.length) return true; return title == null ? void 0 : (_title$toLowerCase = title.toLowerCase()) == null ? void 0 : _title$toLowerCase.startsWith(search); }); var containingSearchString = registry.filter(function (_ref2) { var title = _ref2.title; var value = title == null ? void 0 : title.toLowerCase(); return (value == null ? void 0 : value.includes(search)) && !(value != null && value.startsWith(search)); }); return [].concat(startingWithSearchString, containingSearchString); } var defaultEnabledControls = [TextEditorControl.code, TextEditorControl.colors, TextEditorControl.headings, TextEditorControl.links, TextEditorControl.lists, TextEditorControl.math, TextEditorControl.richText]; var colors = [{ value: '#1794c1', name: 'Blue' }, { value: '#469a40', name: 'Green' }, { value: '#ff6703', name: 'Orange' }]; function useTextConfig(config) { var _config$placeholder = config.placeholder, placeholder = _config$placeholder === void 0 ? "Write something or add elements with \u2295." : _config$placeholder, _config$i18n = config.i18n, i18n = _config$i18n === void 0 ? {} : _config$i18n, _config$theme = config.theme, theme = _config$theme === void 0 ? {} : _config$theme, blockquote = config.blockquote, noLinebreaks = config.noLinebreaks; var _useTheme = useTheme(), editor = _useTheme.editor; return { controls: config.controls || defaultEnabledControls, placeholder: placeholder, i18n: merge({ fallback: { blockquote: { toggleTitle: 'Quote' }, code: { toggleTitle: 'Code' }, colors: { setColorTitle: 'Set color', resetColorTitle: 'Reset color', openMenuTitle: 'Colors', closeMenuTitle: 'Close sub menu', colorNames: colors.map(function (color) { return color.name; }) }, headings: { setHeadingTitle: function setHeadingTitle(level) { return "Heading " + level; }, openMenuTitle: 'Headings', closeMenuTitle: 'Close sub menu' }, link: { toggleTitle: 'Link (Strg + K)', placeholder: 'Enter URL', openInNewTabTitle: 'Open in new tab' }, list: { toggleOrderedList: 'Ordered list', toggleUnorderedList: 'Unordered list', openMenuTitle: 'Lists', closeMenuTitle: 'Close sub menu' }, math: { toggleTitle: 'Math formula (Strg + M)', displayBlockLabel: 'Display as block', placeholder: '[formula]', editors: { visual: 'visual', latex: 'LaTeX', noVisualEditorAvailableMessage: 'Only LaTeX editor available' }, helpText: function helpText(KeySpan) { return createElement(Fragment, null, "Shortcuts:", createElement("br", null), createElement("br", null), createElement("p", null, "Fraction: ", createElement(KeySpan, null, "/")), createElement("p", null, "Superscript: ", createElement(KeySpan, null, "\u2191"), " or ", createElement(KeySpan, null, "^")), createElement("p", null, "Subscript: ", createElement(KeySpan, null, "\u2193"), " oder ", createElement(KeySpan, null, "_")), createElement("p", null, "\u03C0, \u03B1, \u03B2, \u03B3: ", createElement(KeySpan, null, "pi"), ", ", createElement(KeySpan, null, "alpha"), ",", ' ', createElement(KeySpan, null, "beta"), ",", createElement(KeySpan, null, "gamma")), createElement("p", null, "\u2264, \u2265: ", createElement(KeySpan, null, '<='), ", ", createElement(KeySpan, null, '>=')), createElement("p", null, "Root: ", createElement(KeySpan, null, "\\sqrt"), ", ", createElement(KeySpan, null, "\\nthroot")), createElement("p", null, "Math symbols: ", createElement(KeySpan, null, '\\<NAME>'), ", e.g.", ' ', createElement(KeySpan, null, "\\neq"), " (\u2260), ", createElement(KeySpan, null, "\\pm"), " (\xB1), ..."), createElement("p", null, "Functions: ", createElement(KeySpan, null, "sin"), ", ", createElement(KeySpan, null, "cos"), ",", ' ', createElement(KeySpan, null, "ln"), ", ...")); } }, richText: { toggleStrongTitle: 'Bold (Strg + B)', toggleEmphasizeTitle: 'Italic (Strg + I)' }, suggestions: { noResultsMessage: 'No items found' } }, values: i18n }), theme: merge({ fallback: { backgroundColor: 'transparent', color: editor.color, hoverColor: editor.primary.background, borderColor: editor.backgroundColor, borderRadius: '4px', active: { backgroundColor: '#b6b6b6', color: editor.backgroundColor }, dropDown: { backgroundColor: editor.backgroundColor }, suggestions: { background: { "default": 'transparent', highlight: editor.primary.background }, text: { "default": editor.color, highlight: editor.danger.background } }, overlay: { backgroundColor: editor.backgroundColor, boxShadow: '0 2px 4px 0 rgba(0,0,0,0.50)', color: editor.color }, controls: { colors: { colors: colors.map(function (color) { return color.value; }), defaultColor: 'black' }, headings: [1, 2, 3] } }, values: theme }), blockquote: blockquote, noLinebreaks: noLinebreaks }; } var handleMarkdown = function handleMarkdown(chars, editor) { switch (chars) { case '*': case '-': case '+': return createUnorderedList(editor); case '#': return createHeading(1, editor); case '##': return createHeading(2, editor); case '###': return createHeading(3, editor); default: return undefined; } }; function createUnorderedList(editor) { ListsEditor.wrapInList(editor, ListType.UNORDERED); return true; } function createHeading(level, editor) { Transforms.setNodes(editor, { type: 'h', level: level }); return true; } var onSpace = function onSpace(event, editor) { var selection = editor.selection; if (selection) { var nodes = Array.from(Editor.nodes(editor, { at: selection })); if (nodes.length >= 2) { var startBlock = nodes[2][0]; var text = Node.string(startBlock); var chars = text.slice(0, selection == null ? void 0 : selection.focus.offset).replace(/\s*/g, ''); var handled = handleMarkdown(chars, editor); if (handled) { event.preventDefault(); editor.deleteBackward('word'); } } } }; var markdownShortcuts = function markdownShortcuts() { return { onKeyDown: function onKeyDown(event, editor) { switch (event.key) { case ' ': return onSpace(event, editor); default: return; } } }; }; function isTouchDevice() { return typeof window !== 'undefined' && ('ontouchstart' in window || navigator.maxTouchPoints > 0); } var HoveringToolbarButton = /*#__PURE__*/styled.button(function (_ref) { var active = _ref.active, theme = _ref.theme; return { backgroundColor: active ? theme.active.backgroundColor : theme.backgroundColor, cursor: 'pointer', boxShadow: active ? 'inset 0 1px 3px 0 rgba(0,0,0,0.50)' : undefined, color: active ? theme.active.color : theme.color, outline: 'none', height: '25px', border: 'none', borderRadius: theme.borderRadius, margin: '5px', padding: '0px', width: '25px', '&:hover': { color: theme.hoverColor } }; }); function isNestedControlButton(control) { return has('children', control); } function HoveringToolbarControls(props) { var theme = props.theme, controls = props.controls, editor = props.editor; var _React$useState = React__default.useState(), subMenu = _React$useState[0], setSubMenu = _React$useState[1]; if (typeof subMenu !== 'number') { return React__default.createElement(React__default.Fragment, null, controls.map(function (control, index) { return React__default.createElement(HoveringToolbarButton, { active: control.isActive(editor), theme: theme, title: control.title, onMouseDown: function onMouseDown(event) { event.preventDefault(); isNestedControlButton(control) ? setSubMenu(index) : control.onClick(editor); }, key: index }, control.renderIcon(editor)); })); } var activeControl = controls[subMenu]; if (!isNestedControlButton(activeControl)) return null; var closeSubMenuControl = { isActive: function isActive() { return false; }, renderIcon: function renderIcon() { return activeControl.renderCloseMenuIcon(); }, onClick: function onClick() { setSubMenu(undefined); }, title: activeControl.closeMenuTitle }; var subMenuControls = [].concat(activeControl.children, [closeSubMenuControl]); return React__default.createElement(React__default.Fragment, null, subMenuControls.map(function (control, index) { return React__default.createElement(HoveringToolbarButton, { active: control.isActive(editor), theme: theme, title: control.title, onMouseDown: function onMouseDown(event) { event.preventDefault(); control.onClick(editor); }, key: index }, control.renderIcon(editor)); })); } var InlineOverlayPosition; (function (InlineOverlayPosition) { InlineOverlayPosition["above"] = "above"; InlineOverlayPosition["below"] = "below"; })(InlineOverlayPosition || (InlineOverlayPosition = {})); function isAbove(position) { return position === InlineOverlayPosition.above; } var Wrapper = /*#__PURE__*/styled.div({ position: 'absolute', top: '-10000px', left: '-10000px', opacity: 0, transition: 'opacity 0.5s', zIndex: 95, whiteSpace: 'nowrap' }); var Content = /*#__PURE__*/styled.div(function (_ref) { var theme = _ref.theme; return { boxShadow: theme.overlay.boxShadow, backgroundColor: theme.overlay.backgroundColor, color: theme.overlay.color, borderRadius: theme.borderRadius }; }); var Triangle = /*#__PURE__*/styled.div(function (_ref2) { var _ref3; var theme = _ref2.theme, position = _ref2.position; var borderPosition = isAbove(position) ? 'borderTop' : 'borderBottom'; return _ref3 = { position: 'relative', width: 0, height: 0, borderLeft: '5px solid transparent', borderRight: '5px solid transparent' }, _ref3[borderPosition] = "10px solid " + theme.borderColor, _ref3; }); function InlineOverlay(_ref4) { var config = _ref4.config, children = _ref4.children, initialPosition = _ref4.initialPosition, hidden = _ref4.hidden; var editor = useSlate(); var wrapper = React__default.useRef(null); var triangle = React__default.useRef(null); var _React$useState = React__default.useState(initialPosition), position = _React$useState[0], setPosition = _React$useState[1]; // eslint-disable-next-line react-hooks/exhaustive-deps React__default.useLayoutEffect(function () { if (!wrapper.current || !triangle.current) return; var selection = editor.selection; if (!selection) return; if (hidden) { wrapper.current.style.top = ''; wrapper.current.style.left = ''; return; } var domSelection = window.getSelection(); if (!domSelection || domSelection.rangeCount === 0) return; var domRange = domSelection.getRangeAt(0); var rect = domRange.getBoundingClientRect(); if (!rect || rect.height === 0) return; if (!wrapper.current.offsetParent) return; var parentRect = wrapper.current.offsetParent.getBoundingClientRect(); wrapper.current.style.opacity = '1'; var aboveValue = rect.top - wrapper.current.offsetHeight - 6; setPosition(initialPosition === InlineOverlayPosition.above && aboveValue >= 0 ? InlineOverlayPosition.above : InlineOverlayPosition.below); wrapper.current.style.top = (position === InlineOverlayPosition.above ? aboveValue : rect.bottom + 6) - parentRect.top + "px"; wrapper.current.style.left = Math.min(Math.max(rect.left - parentRect.left - wrapper.current.offsetWidth / 2 + rect.width / 2, 0), parentRect.width - wrapper.current.offsetWidth - 5) + "px"; triangle.current.style.left = rect.left - wrapper.current.offsetLeft - parentRect.left - triangle.current.offsetWidth / 2 + rect.width / 2 + "px"; }); return React__default.createElement(Wrapper, { ref: wrapper }, !isAbove(position) && React__default.createElement(Triangle, { ref: triangle, theme: config.theme, position: position }), React__default.createElement(Content, { theme: config.theme }, children), isAbove(position) && React__default.createElement(Triangle, { ref: triangle, theme: config.theme, position: position })); } var TimeoutBottomToolbarWrapper = /*#__PURE__*/styled$1(EditorBottomToolbar)(function (props) { var touchStyles = props.isTouch ? { bottom: 'unset', top: 0, transform: 'translate(-50%, 50%)' } : {}; return _extends({ opacity: props.visible ? 1 : 0, transition: '500ms opacity ease-in-out' }, touchStyles); }); var initialPosition = /*#__PURE__*/isTouchDevice() ? InlineOverlayPosition.below : InlineOverlayPosition.above; function HoveringToolbar(props) { var _useState = useState(false), isBottomToolbarActive = _useState[0], setIsBottomToolbarActive = _useState[1]; var editor = props.editor, config = props.config, controls = props.controls, text = props.text, focused = props.focused; var selection = editor.selection; var isSelectionCollapsed = selection && Range.isCollapsed(selection); var memoized = useRef({ value: text, isSelectionCollapsed: isSelectionCollapsed }); var showBottomToolbar = function showBottomToolbar() { return setIsBottomToolbarActive(true); }; useEffect(function () { var debounceTimeout = setTimeout(showBottomToolbar, 2500); var hasValueChanged = memoized.current.value !== text; if (hasValueChanged || memoized.current.isSelectionCollapsed !== isSelectionCollapsed) { memoized.current = { value: text, isSelectionCollapsed: isSelectionCollapsed }; if (debounceTimeout) { clearTimeout(debounceTimeout); } var timeout = hasValueChanged ? 2500 : 1000; if (isSelectionCollapsed) { debounceTimeout = setTimeout(showBottomToolbar, timeout); } setIsBottomToolbarActive(false); } return function () { clearTimeout(debounceTimeout); }; }, [text, isSelectionCollapsed]); return React__default.createElement(React__default.Fragment, null, React__default.createElement(InlineOverlay, { config: config, initialPosition: initialPosition, hidden: !selection || !focused || isSelectionCollapsed || Editor.string(editor, selection) === '' }, React__default.createElement(HoveringToolbarControls, { theme: config.theme, controls: controls, editor: editor })), React__default.createElement(TimeoutBottomToolbarWrapper, { isTouch: isTouchDevice(), visible: !!isSelectionCollapsed && isBottomToolbarActive }, isBottomToolbarActive && React__default.createElement(HoveringToolbarControls, { theme: config.theme, controls: controls, editor: editor }))); } var InputInner = /*#__PURE__*/styled.input(function (_ref) { var theme = _ref.theme; return { backgroundColor: theme.backgroundColor, border: 'none', borderBottom: "2px solid " + theme.color, color: theme.color, '&:focus': { outline: 'none', borderBottom: "2px solid " + theme.hoverColor } }; }); var InputRefForward = function InputRefForward(props, ref) { return createElement(InputInner, Object.assign({}, props, { ref: ref })); }; var LinkControlsInput = /*#__PURE__*/forwardRef(InputRefForward); var InlinePreview = /*#__PURE__*/styled.span({ padding: '0px 8px' }); var ChangeButton = /*#__PURE__*/styled.div(function (_ref) { var theme = _ref.theme; return { padding: '5px 5px 5px 10px', display: 'inline-block', borderLeft: "2px solid " + theme.borderColor, cursor: 'pointer', margin: '2px', '&:hover': { color: theme.hoverColor } }; }); function LinkControls(_ref2) { var hasSelectionChanged = _ref2.hasSelectionChanged, editor = _ref2.editor, config = _ref2.config, isLinkNewlyCreated = _ref2.isLinkNewlyCreated, setIsLinkNewlyCreated = _ref2.setIsLinkNewlyCreated; var _React$useState = useState(null), element = _React$useState[0], setElement = _React$useState[1]; var _React$useState2 = useState(''), value = _React$useState2[0], setValue = _React$useState2[1]; var input = useRef(null); var selection = editor.selection; useEffect(function () { if (!selection) return; var isCollapsed = selection && Range.isCollapsed(selection); if (isCollapsed && isLinkActive(editor)) { var _element = getLinkElement(editor) || null; setElement(_element); setValue(_element ? _element.href : ''); } else { setElement(null); } }, [hasSelectionChanged, selection, editor]); useEffect(function () { if (element && isLinkNewlyCreated) { setTimeout(function () { var _input$current; setIsLinkNewlyCreated(false); (_input$current = input.current) == null ? void 0 : _input$current.focus(); }); } }, [element, isLinkNewlyCreated, setIsLinkNewlyCreated]); if (!element) return null; return createElement(InlineOverlay, { config: config, initialPosition: InlineOverlayPosition.below }, createElement(InlinePreview, null, createElement(LinkControlsInput, { ref: input, theme: config.theme, value: value, placeholder: config.i18n.link.placeholder, onChange: function onChange(event) { setValue(event.target.value); var path = ReactEditor.findPath(editor, element); Transforms.setNodes(editor, { href: event.target.value }, { at: path }); } })), createElement(ChangeButton, { theme: config.theme, as: "a", target: "_blank", href: value, rel: "noopener noreferrer" }, createElement(Icon, { icon: faExternalLinkAlt })), createElement(ChangeButton, { theme: config.theme, onClick: function onClick() { setElement(null); var path = ReactEditor.findPath(editor, element); Transforms.unwrapNodes(editor, { at: path }); } }, createElement(Icon, { icon: faTrashAlt }))); } var KaTeXSpan = /*#__PURE__*/styled.span(function (_ref) { var element = _ref.element; if (!element.inline) { return { display: 'block', margin: '1em 0', textAlign: 'center' }; } }); function MathFormula(_ref2) { var element = _ref2.element; var html = KaTeX.renderToString("" + (element.inline ? '' : '\\displaystyle ') + element.src, { displayMode: false, throwOnError: false }); return React__default.createElement(KaTeXSpan, { dangerouslySetInnerHTML: { __html: html }, element: element }); } var visualEditorPreferenceKey = 'text:math:visual-editor'; function MathElement(_ref) { var element = _ref.element, attributes = _ref.attributes, focused = _ref.focused, children = _ref.children; var editor = useSlate(); var selected = useSelected(); var preferences = useContext(PreferenceContext); var shouldShowMathEditor = focused && selected && editor.selection && Range.isCollapsed(editor.selection); if (!shouldShowMathEditor) { return React__default.createElement("span", Object.assign({}, attributes), React__default.createElement(MathFormula, { element: element }), children); } var isVisualMode = !!preferences.getKey(visualEditorPreferenceKey); function updateElement(update) { var path = ReactEditor.findPath(editor, element); Transforms.setNodes(editor, update, { at: path }); } function transformOutOfElement(_temp) { var _ref2 = _temp === void 0 ? {} : _temp, _ref2$reverse = _ref2.reverse, reverse = _ref2$reverse === void 0 ? false : _ref2$reverse, _ref2$shouldDelete = _ref2.shouldDelete, shouldDelete = _ref2$shouldDelete === void 0 ? false : _ref2$shouldDelete; var unit = 'character'; Transforms.move(editor, { unit: unit, reverse: reverse }); if (shouldDelete) { Transforms["delete"](editor, { unit: unit, reverse: reverse }); } ReactEditor.focus(editor); } /* TODO: We need to define export interface MathEditorProps { config: DeepPartial<MathEditorConfig> } */ return React__default.createElement("span", Object.assign({}, attributes, { tabIndex: -1 }), React__default.createElement(MathEditor, { autofocus: true, state: element.src, inline: element.inline, readOnly: false, visual: isVisualMode, disableBlock: false, onInlineChange: function onInlineChange(inline) { updateElement({ inline: inline }); }, onChange: function onChange(src) { return updateElement({ src: src }); }, onMoveOutRight: transformOutOfElement, onMoveOutLeft: function onMoveOutLeft() { transformOutOfElement({ reverse: true }); }, onDeleteOutRight: function onDeleteOutRight() { transformOutOfElement({ shouldDelete: true }); }, onDeleteOutLeft: function onDeleteOutLeft() { transformOutOfElement({ shouldDelete: true, reverse: true }); }, config: {}, onEditorChange: function onEditorChange(visual) { return preferences.setKey(visualEditorPreferenceKey, visual); } }), children); } function escapeRegExp(string) { return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } var Suggestion = /*#__PURE__*/styled.div(function (_ref) { var isActive = _ref.isActive, theme = _ref.theme; return { height: '32px', padding: '4px 8px', cursor: 'pointer', backgroundColor: isActive ? theme.suggestions.background.highlight : theme.suggestions.background["default"], borderRadius: '4px', '&:hover': { background: theme.suggestions.background.highlight } }; }); var Container = /*#__PURE__*/styled.div({ padding: '10px' }); var StyledText = /*#__PURE__*/styled.span(function (_ref2) { var isHighlighted = _ref2.isHighlighted, theme = _ref2.theme; return { color: isHighlighted ? theme.suggestions.text.highlight : theme.suggestions.text["default"] }; }); var Suggestions = function Suggestions(props) { var config = props.config, options = props.options, currentValue = props.currentValue, selected = props.selected, _onMouseDown = props.onMouseDown; var i18n = config.i18n, theme = config.theme; if (options.length === 0) { return React__default.createElement(Container, null, i18n.suggestions.noResultsMessage); } return React__default.createElement(Container, null, options.map(function (_ref3, index) { var name = _ref3.name, title = _ref3.title; var fragments = (title != null ? title : name).split(new RegExp("(" + escapeRegExp(currentValue) + ")", 'i')).map(function (text) { return { text: text, isHighlighted: text.toLowerCase() === currentValue.toLowerCase() }; }); return React__default.createElement(Suggestion, { key: index, isActive: index === selected, onMouseDown: function onMouseDown() { return _onMouseDown(name); }, theme: theme }, fragments.map(function (fragment, fragmentIndex) { return React__default.createElement(StyledText, { key: fragmentIndex, isHighlighted: fragment.isHighlighted, theme: theme }, fragment.text); })); })); }; function TextEditor(props) { var _useState = useState(0), hasSelectionChanged = _useState[0], setHasSelectionChanged = _useState[1]; var _useState2 = useState(false), isLinkNewlyCreated = _useState2[0], setIsLinkNewlyCreated = _useState2[1]; var state = props.state, id = props.id, editable = props.editable, focused = props.focused; var _state$value = state.value, selection = _state$value.selection, value = _state$value.value; var config = useTextConfig(props.config); var textControls = useControls(config, setIsLinkNewlyCreated); var createTextEditor = textControls.createTextEditor, toolbarControls = textControls.toolbarControls; var editor = useMemo(function () { return createTextEditor(withReact(createEditor())); }, [createTextEditor]); var text = Node.string(editor); var suggestions = useSuggestions({ text: text, id: id, editable: editable, focused: focused }); var showSuggestions = suggestions.showSuggestions, hotKeysProps = suggestions.hotKeysProps, suggestionsProps = suggestions.suggestionsProps; var previousValue = useRef(value); var previousSelection = useRef(selection); useEffect(function () { // The selection can only be null when the text plugin is initialized // (In this case an update of the slate editor is not necessary) if (!selection) return; Transforms.setSelection(editor, selectio