UNPKG

@onesy/ui-react

Version:
1,166 lines (1,138 loc) 59.4 kB
import _extends from "@babel/runtime/helpers/extends"; import _objectWithoutProperties from "@babel/runtime/helpers/objectWithoutProperties"; import _defineProperty from "@babel/runtime/helpers/defineProperty"; const _excluded = ["tonal", "color", "version", "size", "valueDefault", "value", "inputProps", "onChange", "error", "name", "placeholder", "edit", "mention", "multiline", "onChangeMention", "optionsMention", "error", "helperText", "mentionLabel", "onKeyDown", "pasteText", "readOnly", "additional", "miniMenuExtended", "HelperTextProps", "ColorTextFieldProps", "className"], _excluded2 = ["color"], _excluded3 = ["version", "onUpdate", "onClose"], _excluded4 = ["label", "labelButton", "value", "onChange", "onClick", "placeholder", "InputComponent", "InputProps"], _excluded5 = ["open", "element", "anchorElement", "onClose", "children"], _excluded6 = ["open", "name", "children"]; function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } import React from 'react'; import { hash, innerHTMLToText, is, isEnvironment, parse, stringToColor, textToInnerHTML } from '@onesy/utils'; import { classNames, colors, style as styleMethod, useOnesyTheme } from '@onesy/style-react'; import IconMaterialFormatAlignLeft from '@onesy/icons-material-rounded-react/IconMaterialFormatAlignLeftW100'; import IconMaterialFormatAlignCenter from '@onesy/icons-material-rounded-react/IconMaterialFormatAlignCenterW100'; import IconMaterialFormatAlignRight from '@onesy/icons-material-rounded-react/IconMaterialFormatAlignRightW100'; import IconMaterialFormatAlignJustify from '@onesy/icons-material-rounded-react/IconMaterialFormatAlignJustifyW100'; import IconMaterialFormatColorText from '@onesy/icons-material-rounded-react/IconMaterialFormatColorTextW100'; import IconMaterialFormatColorFill from '@onesy/icons-material-rounded-react/IconMaterialFormatColorFillW100'; import IconMaterialFormatListNumbered from '@onesy/icons-material-rounded-react/IconMaterialFormatListNumberedW100'; import IconMaterialFormatListBulleted from '@onesy/icons-material-rounded-react/IconMaterialFormatListBulletedW100'; import IconMaterialSuperscript from '@onesy/icons-material-rounded-react/IconMaterialSuperscriptW100'; import IconMaterialSubscript from '@onesy/icons-material-rounded-react/IconMaterialSubscriptW100'; import IconMaterialFormatItalic from '@onesy/icons-material-rounded-react/IconMaterialFormatItalicW100'; import IconMaterialFormatUnderlined from '@onesy/icons-material-rounded-react/IconMaterialFormatUnderlinedW100'; import IconMaterialStrikethroughS from '@onesy/icons-material-rounded-react/IconMaterialStrikethroughSW100'; import IconMaterialFormatBold from '@onesy/icons-material-rounded-react/IconMaterialFormatBoldW100'; import IconMaterialAddLink from '@onesy/icons-material-rounded-react/IconMaterialAddLinkW100'; import IconMaterialLinkOff from '@onesy/icons-material-rounded-react/IconMaterialLinkOffW100'; import IconMaterialFormatClear from '@onesy/icons-material-rounded-react/IconMaterialFormatClearW100'; import ColorTextFieldElement from '../ColorTextField'; import TextFieldElement from '../TextField/TextField'; import LineElement from '../Line'; import ListElement from '../List'; import ListItemElement from '../ListItem'; import AvatarElement from '../Avatar'; import TypeElement from '../Type'; import SurfaceElement from '../Surface'; import ButtonElement from '../Button'; import AppendElement from '../Append'; import FadeElement from '../Fade'; import TooltipElement from '../Tooltip'; import ToggleButtonElement from '../ToggleButton'; import ClickListenerElement from '../ClickListener'; import ToggleButtonsElement from '../ToggleButtons'; import MenuElement from '../Menu'; import DividerElement from '../Divider'; import { sanitize, caret, keyboardStyleCommands, staticClassName } from '../utils'; const useStyle = styleMethod(theme => ({ root: { minHeight: '20px', width: '100%', '& .onesy-TextField-input': _objectSpread({ wordBreak: 'break-word', color: theme.methods.palette.color.value('primary', 10) }, theme.typography.values.b2), '& ul, & ol': { listStylePosition: 'inside' } }, root_type: { minHeight: '20px', width: '100%', cursor: 'text', '& .onesy-TextField-input': _objectSpread({ wordBreak: 'break-word', color: theme.methods.palette.color.value('primary', 10) }, theme.typography.values.b2), '& ul, & ol': { listStylePosition: 'inside' } }, menu: { width: '100vw', maxWidth: '240px', borderRadius: theme.methods.shape.radius.value(2.5), boxShadow: '0px 4px 32px 0px rgba(0, 0, 0, 0.04)', background: theme.palette.light ? theme.palette.color.neutral[99] : theme.palette.color.neutral[20] }, list: { maxHeight: '240px', overflow: 'hidden auto' }, textMiniMenuWrapper: { paddingBlock: theme.methods.space.value(1, 'px') }, textMiniMenu: { padding: theme.methods.space.value(1.5, 'px'), borderRadius: theme.methods.shape.radius.value(140, 'px'), boxShadow: theme.shadows.values.default[2], maxWidth: 'calc(100vw - 24px)', rowGap: '4px' }, textMiniMenuAdditionalMenu: { minWidth: 'clamp(140px, 90%, 250px)', zIndex: '1514', boxShadow: theme.shadows.values.default[2] }, inputWrapper: { padding: theme.methods.space.value(1, 'px'), borderRadius: theme.methods.shape.radius.value(1, 'px'), boxShadow: theme.shadows.values.default[2] }, singleLine: { whiteSpace: 'nowrap', overflow: 'hidden' }, input: { '&.onesy-TextField-root': { flex: '1 1 auto !important' } }, palette: { padding: theme.methods.space.value(1.5, 'px'), borderRadius: theme.methods.shape.radius.value(1, 'px'), boxShadow: theme.shadows.values.default[2] }, paletteItem: { position: 'relative', width: '17px', height: '17px', cursor: 'pointer', borderRadius: theme.methods.shape.radius.value(40, 'px'), boxShadow: theme.shadows.values.default[1], transition: theme.methods.transitions.make('box-shadow'), '&:hover': { boxShadow: theme.shadows.values.default[2] } }, textFieldColor: { '&.onesy-ColorTextField-root': { flex: '1 1 auto' } }, helperText: { display: 'inline-flex', color: theme.palette.text.default.secondary, userSelect: 'none' }, error_color: { color: [theme.palette.light ? theme.palette.color.error[40] : theme.palette.color.error[80], '!important'] }, error_hover_color: { color: [theme.palette.light ? theme.palette.color.error[20] : theme.palette.color.error[90], '!important'] } }), { name: 'onesy-SmartTextField' }); const SmartTextField = /*#__PURE__*/React.forwardRef((props_, ref) => { const theme = useOnesyTheme(); const l = theme.l; const props = React.useMemo(() => _objectSpread(_objectSpread(_objectSpread({}, theme?.ui?.elements?.all?.props?.default), theme?.ui?.elements?.onesySmartTextField?.props?.default), props_), [props_]); const Line = React.useMemo(() => theme?.elements?.Line || LineElement, [theme]); const TextField = React.useMemo(() => theme?.elements?.TextField || TextFieldElement, [theme]); const List = React.useMemo(() => theme?.elements?.List || ListElement, [theme]); const ListItem = React.useMemo(() => theme?.elements?.ListItem || ListItemElement, [theme]); const Avatar = React.useMemo(() => theme?.elements?.Avatar || AvatarElement, [theme]); const Type = React.useMemo(() => theme?.elements?.Type || TypeElement, [theme]); const Surface = React.useMemo(() => theme?.elements?.Surface || SurfaceElement, [theme]); const Button = React.useMemo(() => theme?.elements?.Button || ButtonElement, [theme]); const Append = React.useMemo(() => theme?.elements?.Append || AppendElement, [theme]); const Fade = React.useMemo(() => theme?.elements?.Fade || FadeElement, [theme]); const Tooltip = React.useMemo(() => theme?.elements?.Tooltip || TooltipElement, [theme]); const ToggleButton = React.useMemo(() => theme?.elements?.ToggleButton || ToggleButtonElement, [theme]); const ClickListener = React.useMemo(() => theme?.elements?.ClickListener || ClickListenerElement, [theme]); const ToggleButtons = React.useMemo(() => theme?.elements?.ToggleButtons || ToggleButtonsElement, [theme]); const Menu = React.useMemo(() => theme?.elements?.Menu || MenuElement, [theme]); const Divider = React.useMemo(() => theme?.elements?.Divider || DividerElement, [theme]); const ColorTextField = React.useMemo(() => theme?.elements?.ColorTextField || ColorTextFieldElement, [theme]); const { tonal = true, color = 'default', version = 'text', size = 'regular', valueDefault, value: value_, inputProps, onChange, error: error_, name, placeholder, edit, mention, multiline, onChangeMention, optionsMention: optionsMention_, error, helperText, mentionLabel, onKeyDown: onKeyDown_, pasteText = true, readOnly, additional, miniMenuExtended, HelperTextProps, ColorTextFieldProps, className } = props, other = _objectWithoutProperties(props, _excluded); const { classes } = useStyle(); const [value, setValue] = React.useState(valueDefault || ''); const [openMenu, setOpenMenu] = React.useState(false); const [offset, setOffset] = React.useState([0, 8]); const [selectionMenu, setSelectionMenu] = React.useState(); const [optionsMention, setOptionsMention] = React.useState([]); const [textSelection, setTextSelection] = React.useState(); const [textSelected, setTextSelected] = React.useState([]); const [open, setOpen] = React.useState({}); const [inputValues, setInputValues] = React.useState({}); const refs = { root: React.useRef(null), input: React.useRef(null), searchElement: React.useRef(null), error: React.useRef(null), search: React.useRef(null), open: React.useRef(null), openMenu: React.useRef(null), selectionMenu: React.useRef(null), caret: React.useRef(null), value: React.useRef(null), version: React.useRef(version), optionsMention: React.useRef(optionsMention), validateMeta: React.useRef({}), range: React.useRef(null), inputValues: React.useRef(null), miniMenu: React.useRef(null), miniMenuElements: { color: React.useRef(null), colorPalette: React.useRef(null), background: React.useRef(null), backgroundPalette: React.useRef(null), linkAdd: React.useRef(null), linkAddInput: React.useRef(null), linkRemove: React.useRef(null) }, elements: { color: React.useRef(null), background: React.useRef(null), linkAdd: React.useRef(null), linkRemove: React.useRef(null), quote: React.useRef(null), image: React.useRef(null), video: React.useRef(null), videoYoutube: React.useRef(null), table: React.useRef(null), drawing: React.useRef(null), drawingSvg: React.useRef(null), drawingSize: React.useRef(null), drawingSelect: React.useRef(null), drawingPalette: React.useRef(null), code: React.useRef(null) }, textSelected: React.useRef(null), textSelection: React.useRef(null), rootDocument: React.useRef(null), rootWindow: React.useRef(null), edit: React.useRef(edit), multiline: React.useRef(multiline), readOnly: React.useRef(readOnly), onChange: React.useRef(onChange), props: React.useRef(props) }; refs.value.current = value; refs.version.current = version; refs.open.current = open; refs.openMenu.current = openMenu; refs.selectionMenu.current = selectionMenu; refs.optionsMention.current = optionsMention; refs.inputValues.current = inputValues; refs.textSelected.current = textSelected; refs.textSelection.current = textSelection; refs.edit.current = edit; refs.multiline.current = multiline; refs.readOnly.current = readOnly; refs.onChange.current = onChange; refs.props.current = props; const rootDocument = isEnvironment('browser') && (refs.root.current?.ownerDocument || window.document); const rootWindow = rootDocument && (rootDocument.defaultView || window); refs.rootDocument.current = rootDocument; refs.rootWindow.current = rootWindow; const init = React.useCallback(() => { if (version === 'text') { const valueNew = valueDefault || value || ''; if (refs.root.current) { refs.root.current.innerHTML = valueNew; } } }, [version, value, valueDefault]); React.useEffect(() => { // init init(); }, []); React.useEffect(() => { setOptionsMention([...(optionsMention_ || [])]); }, [hash(optionsMention_)]); React.useEffect(() => { const valuePrevious = (refs.root.current.innerHTML || '').replaceAll('&nbsp;', ' '); const valueNew = textToInnerHTML(value_); if (value_ !== undefined && valuePrevious !== valueNew) validate(!value_ ? '' : sanitize(value_)); }, [value_]); // Save selection to revive, use it on execComand React.useEffect(() => { const selection_ = refs.rootWindow.current.getSelection(); if (selection_ && selection_.anchorNode && !selection_.anchorNode?.className?.includes('TextField')) refs.range.current = selection_.getRangeAt(0); }, [open]); const updateInputValues = (property, itemValue) => { setInputValues(values => _objectSpread(_objectSpread({}, values), {}, { [property]: itemValue })); }; const updateOpen = (property, itemValue) => { setOpen(values => _objectSpread(_objectSpread({}, values), {}, { [property]: itemValue })); }; const validateElement = React.useCallback(element => { if (element?.children) { Array.from(element?.children).forEach(child => { // Any tag if (child.dataset.onesyVersion === 'mention-user') { // Replace the child if label & textContent don't match if (child.dataset.onesyLabel !== child.innerHTML) { element.replaceChild(window.document.createTextNode(child.textContent), child); refs.validateMeta.current.restore = true; } } if (child?.children) validateElement(child); }); } }, []); const validate = React.useCallback(function () { let valueNew = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : undefined; // Save caret position refs.caret.current = caret.save(refs.root.current); // Update the root innerHTML if (valueNew !== undefined) refs.root.current.innerHTML = valueNew; refs.validateMeta.current.restore = false; // Find all span onesy-mention children // if they aren't valid, replace them with text node of their textContent validateElement(refs.root.current); if (refs.validateMeta.current.restore) caret.restore(refs.root.current, refs.caret.current); return refs.root.current.innerHTML; }, []); const getAtSearchData = React.useCallback(() => { if (refs.root.current) { const windowSelection = window.document.getSelection(); if (!(windowSelection && windowSelection.rangeCount > 0)) return; const range = windowSelection?.getRangeAt(0); if (!range) return; // Only allow for text not in span user mention element if (windowSelection.anchorNode?.parentElement?.dataset?.onesyVersion === 'mention-user') return ''; // Text if (!['text', '#text'].includes(range?.commonAncestorContainer?.nodeName)) return ''; const text = windowSelection.anchorNode?.textContent || ''; const start = text.slice(0, range.endOffset).split('').reverse().join(''); let result = []; for (const valueText of start) { // All characters other than space, numbers if (!!valueText.match(/[^<>\\/\n\r\t]/)) result.unshift(valueText);else break; if (['@'].includes(valueText)) break; } result = result.includes('@') ? result.join('') : ''; // Allow 1 space only if (result && result.match(/\s{2}/g)) return ''; return result; } }, []); refs.search.current = getAtSearchData(); const onInput = React.useCallback(event => { if (refs.root.current) { let valueInput = refs.root.current.innerHTML || ''; if (refs.root.current.textContent === '') { valueInput = ''; refs.root.current.innerHTML = ''; } if (!refs.multiline.current) { const previous = valueInput; valueInput = valueInput.replace(/\r?\n|\r/gm, ''); if (previous !== valueInput) refs.root.current.innerHTML = valueInput; } valueInput = innerHTMLToText(valueInput); setValue(valueInput); if (is('function', refs.onChange.current)) refs.onChange.current(valueInput, event); } }, []); React.useEffect(() => { if (mention && refs.search.current !== undefined) onChangeMention?.(refs.search.current, '@'); }, [mention, refs.search.current]); React.useEffect(() => { const method = () => { if (window.document.activeElement === refs.input.current) onInput(); }; window.document.addEventListener('selectionchange', method); onChangeMention?.(refs.search.current, '@'); return () => { window.document.removeEventListener('selectionchange', method); }; }, []); const updateSelection = React.useCallback(() => { const windowSelection = window.document.getSelection(); if (!(windowSelection && windowSelection.rangeCount > 0)) return; const range = windowSelection?.getRangeAt(0); if (range) setSelectionMenu(range?.getBoundingClientRect()); }, []); const onOpenSearch = React.useCallback(async () => { if (!refs.openMenu.current && !!refs.optionsMention.current.length) { // open setOpenMenu(true); // selection updateSelection(); } }, []); const onCloseSearch = React.useCallback(() => { if (refs.openMenu.current) { setOpenMenu(false); setSelectionMenu(null); } }, []); const updateOffset = React.useCallback(() => { setOffset([-(refs.searchElement.current?.clientWidth || 0), 8]); }, []); const onFocus = React.useCallback(() => { // setFocus(true); }, []); const onBlur = React.useCallback(() => { // setFocus(false); }, []); const onKeyDown = React.useCallback(event => { const windowSelection = window.getSelection(); if (windowSelection) { // if (event.key === 'Enter') { // event.preventDefault(); // const anchor = windowSelection.anchorNode as HTMLElement; // const element = anchor.parentElement; // const multiple = anchor.tagName === 'P' && anchor.children.length === 1 && anchor.children[0].tagName === 'BR'; // window.document.execCommand('insertHTML', false, `<p><br /></p>`.repeat(multiple ? 2 : 1)); // const range = new Range(); // const rangeStartElement = ((multiple ? anchor : element) as any)?.nextElementSibling; // if (rangeStartElement) range.setStart(rangeStartElement, 0); // windowSelection.removeAllRanges(); // windowSelection.addRange(range); // } if (event.key === 'ArrowRight') { // If in user mention span // move out of it with adding new empty space root node after the user mention span // only if caret is at the end of the span, & span is last child of it's parent if (!(windowSelection && windowSelection.rangeCount > 0)) return; const range = windowSelection.getRangeAt(0); const caretElement = windowSelection.anchorNode.parentElement; const caretElementParent = caretElement.parentElement; const caretElementParentChildNodes = Array.from(caretElementParent.childNodes); if (caretElement?.dataset?.onesyVersion === 'mention-user' && range.endOffset === windowSelection.anchorNode.textContent.length && caretElementParentChildNodes[caretElementParentChildNodes.length - 1] === caretElement) { caretElementParent.insertBefore(window.document.createTextNode('\u00A0'), caretElement.nextElementSibling); } } } if (!refs.multiline.current) { if (event.key === 'Enter') { event.preventDefault(); } } if (!refs.edit.current && (event.metaKey && keyboardStyleCommands.includes(event.key) || event.ctrlKey && keyboardStyleCommands.includes(event.key))) { event.preventDefault(); } if (is('function', onKeyDown_)) onKeyDown_(event); }, [onKeyDown_]); const onPaste = React.useCallback(event => { // event.preventDefault(); // const text = event.clipboardData?.getData('text/plain'); // window.document.execCommand('insertText', false, text); }, []); const onPasteText = React.useCallback(event => { event.preventDefault(); const text = event.clipboardData?.getData('text/plain'); window.document.execCommand('insertText', false, text); }, []); const onDrop = React.useCallback(event => { event.preventDefault(); }, []); const addTag = React.useCallback(function (item) { let versionMention = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'user'; // Save caret position refs.caret.current = caret.save(refs.root.current); const itemName = item.name; const classesElement = ['onesy-mention']; // Query text node that equals selection anchorNode // and replace its textContent's search value with new span const windowSelection = window.document.getSelection(); if (!(windowSelection && windowSelection.rangeCount > 0)) return; const range = windowSelection?.getRangeAt(0); if (!range) return; const textNode = windowSelection.anchorNode; const textContent = textNode.textContent; // Bug fix // only be able to insert data within the input // if by accident selection went outside the input // basically do nothing with it if (refs.root.current.contains(textNode.parentElement)) { const end = range.endOffset; const start = end - refs.search.current.length; const pre = textContent.slice(0, start); const post = textContent.slice(end); // Insert before anchorNode pre, item mention, and post if (pre) textNode.parentElement.insertBefore(window.document.createTextNode(pre), windowSelection.anchorNode); const span = window.document.createElement('span'); span.className = classesElement.join(' '); span.dataset.onesyLabel = `@${itemName}`; span.dataset.onesyVersion = `mention-${versionMention}`; span.dataset.onesyObject = `${versionMention}`; span.dataset.onesyId = item.id; span.innerHTML = `@${itemName}`; textNode.parentElement.insertBefore(span, windowSelection.anchorNode); // 1 space only following the item mention textNode.parentElement.insertBefore(window.document.createTextNode('\u00A0'), windowSelection.anchorNode); if (post) textNode.parentElement.insertBefore(window.document.createTextNode(post), windowSelection.anchorNode); // Remove the text node textNode.remove(); // Invoke onChange method with new value const valueInput = innerHTMLToText(refs.root.current.innerHTML); if (is('function', refs.onChange.current)) refs.onChange.current(valueInput, { target: refs.root.current }); // Update the caret position to be outside the span mention // for amount of added characters + 1 space const added = `@${itemName}`.length - refs.search.current.length + 1; refs.caret.current.start = refs.caret.current.end += added; caret.restore(refs.root.current, refs.caret.current); } // Close the search onCloseSearch(); }, [onChange]); // If users response & not open, open // else if no users response & opened, close the search React.useEffect(() => { if (mention) { if (refs.openMenu.current && !optionsMention.length) onCloseSearch();else if (!refs.openMenu.current && refs.search.current && !!optionsMention.length) onOpenSearch(); } }, [mention, optionsMention]); React.useEffect(() => { if (mention) { // Validate validate(); // Update // open, selection & offset setTimeout(() => { refs.search.current ? onOpenSearch() : onCloseSearch(); if (refs.openMenu.current) updateSelection(); updateOffset(); }); } }, [mention, value]); React.useEffect(() => { // setError(error_); }, [error_]); const label = React.useCallback(() => is('function', mentionLabel) ? mentionLabel(optionsMention, { addTag }) : /*#__PURE__*/React.createElement(Line, { className: classes.menu }, /*#__PURE__*/React.createElement(List, { size: "small", className: classes.list }, optionsMention?.map((item, index) => /*#__PURE__*/React.createElement(ListItem, { key: index, start: /*#__PURE__*/React.createElement(Avatar, { color: stringToColor(item.name), size: "small" }, item.name?.slice(0, 1)), primary: /*#__PURE__*/React.createElement(Type, { version: "l3" }, item.name), onClick: () => addTag(item), button: true })))), [optionsMention]); const paste = async () => { const valueClipboard = await navigator.clipboard.read(); if (valueClipboard) { let values = ''; for (const item of Array.from(valueClipboard)) { const valueItem = await item.getType('text/html'); values += await valueItem.text(); } rootDocument.execCommand('insertHTML', undefined, values); } }; const query = command => { return parse(refs.rootDocument.current.queryCommandValue(command)); }; const textQueryUpdate = () => { const selected = []; const updateOptionValues = [{ name: 'italic', command: 'italic' }, { name: 'underline', command: 'underline' }, { name: 'bold', command: 'bold' }, { name: 'strike-line', command: 'strikeThrough' }]; updateOptionValues.forEach(item => { if (query(item.command)) selected.push(item.name); }); setTextSelected(selected); }; const onMouseUp = React.useCallback(() => { if (!refs.edit.current) return; const selection_ = refs.rootWindow.current.getSelection(); if (!selection_) return; if (!selection_.anchorNode || !refs.root.current.contains(selection_.anchorNode)) return setTextSelection(null); setTimeout(() => { const rect = selection_.getRangeAt(0).getBoundingClientRect(); setTextSelection(Math.round(rect.width) ? { selection: rect, element: selection_.anchorNode?.parentElement } : null); textQueryUpdate(); }, 140); }, []); const onMouseDown = React.useCallback(() => { if (!refs.edit.current) return; textQueryUpdate(); }, []); const onKeyUp = React.useCallback(() => { if (!refs.edit.current) return; textQueryUpdate(); }, []); const textMethod = React.useCallback(command => argument => { switch (command) { // updates case 'italic': refs.rootDocument.current.execCommand('italic'); if (query('italic')) setTextSelected(values => [...values, 'italic']);else setTextSelected(values => values.filter(item => item !== 'italic')); break; case 'underline': refs.rootDocument.current.execCommand('underline'); if (query('underline')) setTextSelected(values => [...values, 'underline']);else setTextSelected(values => values.filter(item => item !== 'underline')); break; case 'bold': refs.rootDocument.current.execCommand('bold'); if (query('bold')) setTextSelected(values => [...values, 'bold']);else setTextSelected(values => values.filter(item => item !== 'bold')); break; case 'strike-line': refs.rootDocument.current.execCommand('strikeThrough'); if (query('strikeThrough')) setTextSelected(values => [...values, 'strike-line']);else setTextSelected(values => values.filter(item => item !== 'strike-line')); break; case 'align-left': refs.rootDocument.current.execCommand('justifyLeft'); if (query('justifyLeft')) setTextSelected(values => [...values.filter(item => !item.includes('align')), 'align-left']);else setTextSelected(values => values.filter(item => item !== 'align-left')); break; case 'align-center': refs.rootDocument.current.execCommand('justifyCenter'); if (query('justifyCenter')) setTextSelected(values => [...values.filter(item => !item.includes('align')), 'align-center']);else setTextSelected(values => values.filter(item => item !== 'align-center')); break; case 'align-right': refs.rootDocument.current.execCommand('justifyRight'); if (query('justifyRight')) setTextSelected(values => [...values.filter(item => !item.includes('align')), 'align-right']);else setTextSelected(values => values.filter(item => item !== 'align-right')); break; case 'align-justify': refs.rootDocument.current.execCommand('justifyFull'); if (query('justifyFull')) setTextSelected(values => [...values.filter(item => !item.includes('align')), 'align-justify']);else setTextSelected(values => values.filter(item => item !== 'align-justify')); break; case 'superscript': refs.rootDocument.current.execCommand('superscript'); if (query('superscript')) setTextSelected(values => [...values, 'superscript']);else setTextSelected(values => values.filter(item => item !== 'superscript')); break; case 'subscript': refs.rootDocument.current.execCommand('subscript'); if (query('subscript')) setTextSelected(values => [...values, 'subscript']);else setTextSelected(values => values.filter(item => item !== 'subscript')); break; case 'indent': refs.rootDocument.current.execCommand('indent'); break; case 'outdent': refs.rootDocument.current.execCommand('outdent'); break; case 'font-version': refs.rootDocument.current.execCommand('formatBlock', undefined, argument); break; case 'font-family': refs.rootDocument.current.execCommand('styleWithCSS', true); refs.rootDocument.current.execCommand('fontName', undefined, argument); refs.rootDocument.current.execCommand('styleWithCSS', false); break; case 'font-size': refs.rootDocument.current.execCommand('styleWithCSS', true); refs.rootDocument.current.execCommand('fontSize', undefined, argument); refs.rootDocument.current.execCommand('styleWithCSS', false); break; case 'font-color': refs.rootDocument.current.execCommand('styleWithCSS', true); refs.rootDocument.current.execCommand('foreColor', undefined, argument); refs.rootDocument.current.execCommand('styleWithCSS', false); break; case 'font-background': refs.rootDocument.current.execCommand('styleWithCSS', true); refs.rootDocument.current.execCommand('backColor', undefined, argument); refs.rootDocument.current.execCommand('styleWithCSS', false); break; case 'list-ordered': refs.rootDocument.current.execCommand('insertOrderedList'); if (query('insertOrderedList')) setTextSelected(values => [...values.filter(item => !item.includes('list')), 'list-ordered']);else setTextSelected(values => values.filter(item => item !== 'list-ordered')); break; case 'list-unordered': refs.rootDocument.current.execCommand('insertUnorderedList'); if (query('insertUnorderedList')) setTextSelected(values => [...values.filter(item => !item.includes('list')), 'list-unordered']);else setTextSelected(values => values.filter(item => item !== 'list-unordered')); break; case 'horizontal-rule': refs.rootDocument.current.execCommand('insertHorizontalRule'); break; case 'link-add': refs.rootDocument.current.execCommand('createLink', undefined, argument); break; case 'link-remove': refs.rootDocument.current.execCommand('unlink'); break; case 'image': refs.rootDocument.current.execCommand('insertImage', undefined, argument); break; case 'html': refs.rootDocument.current.execCommand('insertHTML', undefined, argument); break; // actions case 'copy': refs.rootDocument.current.execCommand('copy'); break; case 'cut': refs.rootDocument.current.execCommand('cut'); break; case 'paste': if (refs.rootDocument.current.queryCommandSupported('paste')) refs.rootDocument.current.execCommand('paste');else paste(); break; case 'delete': refs.rootDocument.current.execCommand('delete'); break; case 'clear': refs.rootDocument.current.execCommand('removeFormat'); break; case 'select-all': refs.rootDocument.current.execCommand('selectAll'); break; case 'undo': refs.rootDocument.current.execCommand('undo'); break; case 'redo': refs.rootDocument.current.execCommand('redo'); break; default: break; } const element = refs.textSelection.current?.element; if (element) { let valueElement = element.innerHTML || ''; if (element.textContent === '') { valueElement = ''; element.innerHTML = ''; } if (valueElement) valueElement = innerHTMLToText(valueElement); } textQueryUpdate(); }, []); const PaletteItem = React.useCallback(propsItem => { const { color: color_ } = propsItem, other_ = _objectWithoutProperties(propsItem, _excluded2); return /*#__PURE__*/React.createElement("span", _extends({ className: classNames([staticClassName('RichTextEditor', theme) && ['onesy-RichTextEditor-palette-item'], classes.paletteItem]), style: { background: color_ } }, other_)); }, []); const Palette = React.useCallback(/*#__PURE__*/React.forwardRef((propsPalette, ref_) => { const { version: version_, onUpdate: onUpdate_, onClose } = propsPalette, other_ = _objectWithoutProperties(propsPalette, _excluded3); const onUpdateColor = itemColor => { if (refs.range.current) { const selection_ = refs.rootWindow.current.getSelection(); selection_.removeAllRanges(); selection_.addRange(refs.range.current); } onUpdate_(itemColor); onClose(); }; return /*#__PURE__*/React.createElement(Line, _extends({ ref: ref_, gap: 1, direction: "column", tonal: tonal, color: refs.props.current.color !== undefined ? refs.props.current.color : 'themed', Component: Surface, className: classNames([staticClassName('RichTextEditor', theme) && ['onesy-RichTextEditor-palette'], classes.palette]) }, other_), /*#__PURE__*/React.createElement(Line, { gap: 0.5, style: { maxHeight: 136, padding: '10px 10px 0', overflow: 'hidden auto' } }, /*#__PURE__*/React.createElement(Line, { gap: 0.5, direction: "row", style: { width: '100%' } }, /*#__PURE__*/React.createElement(PaletteItem, { color: "#000000", onClick: () => { onUpdateColor('#000000'); } }), /*#__PURE__*/React.createElement(PaletteItem, { color: "#ffffff", onClick: () => { onUpdateColor('#ffffff'); } })), Object.keys(colors).filter(item => !['black', 'white'].includes(item)).map((item, index) => /*#__PURE__*/React.createElement(Line, { key: index, gap: 0.5, direction: "row", style: { width: '100%' } }, Object.keys(colors[item]).map((item_, index_) => /*#__PURE__*/React.createElement(PaletteItem, { key: index_, color: colors[item][item_], onClick: () => { onUpdateColor(colors[item][item_]); } }))))), /*#__PURE__*/React.createElement(Divider, null), /*#__PURE__*/React.createElement(Line, { gap: 0.5, direction: "row", align: "center", fullWidth: true, style: { padding: '0px 10px 10px' } }, /*#__PURE__*/React.createElement(ColorTextField, _extends({ tonal: tonal, color: color, name: l('Custom color'), version: "outlined", size: "small", value: refs.inputValues.current[version_], onChange: valueNew => updateInputValues(version_, valueNew) }, ColorTextFieldProps, { className: classNames([staticClassName('RichTextEditor', theme) && ['onesy-RichTextEditor-text-field-color'], ColorTextFieldProps?.className, classes.textFieldColor]) })), /*#__PURE__*/React.createElement(Button, { tonal: tonal, color: "inherit", version: "text", size: "small", onClick: () => { if (refs.range.current) { const selection_ = refs.rootWindow.current.getSelection(); selection_.removeAllRanges(); selection_.addRange(refs.range.current); } onUpdate_(refs.inputValues.current[version_]); onClose(); } }, "Apply"))); }), []); const AppendProps = { padding: [14, 14] }; const IconProps = { size: 'small' }; const TooltipProps = { position: 'bottom', interactive: false }; const ToggleButtonsProps = { tonal: false, color: 'default', version: 'text', border: false }; const ToggleButtonProps = { size: 'small' }; const Input = React.useCallback(/*#__PURE__*/React.forwardRef((propsInput, ref_) => { const { label: labelInput, labelButton, value: value__, onChange: onChange__, onClick, placeholder: placeholderInputProps, InputComponent = TextField, InputProps } = propsInput, other_ = _objectWithoutProperties(propsInput, _excluded4); return /*#__PURE__*/React.createElement(Line, _extends({ ref: ref_, gap: 1, direction: "column", color: "themed", Component: Surface }, other_, { className: classNames([other_?.className, classes.inputWrapper]) }), /*#__PURE__*/React.createElement(Line, { gap: 0.5, direction: "row", align: "center", style: { width: '100%' } }, /*#__PURE__*/React.createElement(InputComponent, _extends({ name: labelInput, version: "outlined", size: "small", valueDefault: value__, onChange: onChange__, placeholder: placeholderInputProps, flex: true }, InputProps, { style: { width: 'unset', flex: '1 1 auto' } })), /*#__PURE__*/React.createElement(Button, { color: "inherit", version: "text", size: "small", onClick: onClick }, labelButton))); }), []); const WrapperAppend = React.useCallback(propsWrapper => { const { open: open_, element, anchorElement, onClose, children: childrenWrapperAppend } = propsWrapper, other_ = _objectWithoutProperties(propsWrapper, _excluded5); return /*#__PURE__*/React.createElement(Append, _extends({ open: open_, element: /*#__PURE__*/React.createElement("div", { className: classNames([staticClassName('SmartTextField', theme) && ['onesy-SmartTextField-mini-menu-additional'], classes.textMiniMenuAdditionalMenu]) }, /*#__PURE__*/React.createElement(Fade, { in: open_, add: true }, /*#__PURE__*/React.cloneElement(element))), anchorElement: anchorElement, portal: true, alignment: "center", position: "bottom" }, AppendProps), /*#__PURE__*/React.cloneElement(childrenWrapperAppend, _objectSpread(_objectSpread({}, other_), childrenWrapperAppend.props))); }, []); const WrapperToggleButton = React.useCallback(/*#__PURE__*/React.forwardRef((propsWrapperToggleButton, ref_) => { const { open: open_, name: nameWrapperToogleButton, children: childrenWrapperToggleButton } = propsWrapperToggleButton, other_ = _objectWithoutProperties(propsWrapperToggleButton, _excluded6); return /*#__PURE__*/React.createElement(Tooltip, _extends({ open: open_ !== undefined ? open_ : undefined, name: nameWrapperToogleButton }, TooltipProps), /*#__PURE__*/React.cloneElement(childrenWrapperToggleButton, _objectSpread(_objectSpread({}, other_), childrenWrapperToggleButton.props))); }), []); const updateElements = { 'italic': /*#__PURE__*/React.createElement(WrapperToggleButton, { name: l('Italic') }, /*#__PURE__*/React.createElement(ToggleButton, _extends({}, ToggleButtonProps, { selected: refs.textSelected.current.includes('italic'), onClick: textMethod('italic') }), /*#__PURE__*/React.createElement(IconMaterialFormatItalic, IconProps))), 'underline': /*#__PURE__*/React.createElement(WrapperToggleButton, { name: l('Underline') }, /*#__PURE__*/React.createElement(ToggleButton, _extends({}, ToggleButtonProps, { selected: refs.textSelected.current.includes('underline'), onClick: textMethod('underline') }), /*#__PURE__*/React.createElement(IconMaterialFormatUnderlined, IconProps))), 'bold': /*#__PURE__*/React.createElement(WrapperToggleButton, { name: l('Bold'), onClick: textMethod('bold') }, /*#__PURE__*/React.createElement(ToggleButton, _extends({}, ToggleButtonProps, { selected: refs.textSelected.current.includes('bold') }), /*#__PURE__*/React.createElement(IconMaterialFormatBold, IconProps))), 'superscript': /*#__PURE__*/React.createElement(WrapperToggleButton, { name: l('Superscript'), onClick: textMethod('superscript') }, /*#__PURE__*/React.createElement(ToggleButton, _extends({}, ToggleButtonProps, { selected: refs.textSelected.current.includes('superscript') }), /*#__PURE__*/React.createElement(IconMaterialSuperscript, IconProps))), 'subscript': /*#__PURE__*/React.createElement(WrapperToggleButton, { name: l('Subscript'), onClick: textMethod('subscript') }, /*#__PURE__*/React.createElement(ToggleButton, _extends({}, ToggleButtonProps, { selected: refs.textSelected.current.includes('subscript') }), /*#__PURE__*/React.createElement(IconMaterialSubscript, IconProps))), 'strike-line': /*#__PURE__*/React.createElement(WrapperToggleButton, { name: l('Strike Line'), onClick: textMethod('strike-line') }, /*#__PURE__*/React.createElement(ToggleButton, _extends({}, ToggleButtonProps, { selected: refs.textSelected.current.includes('strike-line') }), /*#__PURE__*/React.createElement(IconMaterialStrikethroughS, IconProps))), 'link-add': /*#__PURE__*/React.createElement(WrapperAppend, { open: refs.open.current.linkMiniMenu, anchorElement: refs.miniMenuElements.linkAdd, element: /*#__PURE__*/React.createElement(ClickListener, { onClickOutside: () => updateOpen('linkMiniMenu', false), include: [refs.miniMenuElements.linkAdd] }, /*#__PURE__*/React.createElement(Input, { ref: refs.miniMenuElements.linkAddInput, name: l('Link'), labelButton: l('Add'), value: refs.inputValues.current.link, onChange: valueNew => updateInputValues('link', valueNew), placeholder: l('URL'), onClick: () => { if (refs.range.current) { const selection_ = refs.rootWindow.current.getSelection(); if (!selection_) return; selection_.removeAllRanges(); selection_.addRange(refs.range.current); } textMethod('link-add')(refs.inputValues.current.link); updateOpen('linkMiniMenu', false); updateInputValues('link', ''); }, className: classes.input })) }, /*#__PURE__*/React.createElement(WrapperToggleButton, { name: l('Insert Link'), open: refs.open.current.linkMiniMenu ? false : undefined }, /*#__PURE__*/React.createElement(ToggleButton, _extends({ ref: refs.miniMenuElements.linkAdd }, ToggleButtonProps, { selected: refs.open.current.linkMiniMenu, onClick: () => updateOpen('linkMiniMenu', !refs.open.current.linkMiniMenu) }), /*#__PURE__*/React.createElement(IconMaterialAddLink, IconProps)))), 'link-remove': /*#__PURE__*/React.createElement(WrapperToggleButton, { name: l('Remove Link') }, /*#__PURE__*/React.createElement(ToggleButton, _extends({}, ToggleButtonProps, { onClick: textMethod('link-remove') }), /*#__PURE__*/React.createElement(IconMaterialLinkOff, IconProps))), 'font-color': /*#__PURE__*/React.createElement(WrapperAppend, { open: refs.open.current.color, anchorElement: refs.elements.color.current, element: /*#__PURE__*/React.createElement(ClickListener, { onClickOutside: () => updateOpen('color', false), include: [refs.elements.color.current] }, /*#__PURE__*/React.createElement(Palette, { version: "font-color", onClose: () => updateOpen('color', false), onUpdate: () => { if (refs.range.current) { const selection_ = refs.rootWindow.current.getSelection(); selection_.removeAllRanges(); selection_.addRange(refs.range.current); } textMethod('font-color'); } })) }, /*#__PURE__*/React.createElement(WrapperToggleButton, { name: l('Text Color'), open: refs.open.current.color ? false : undefined }, /*#__PURE__*/React.createElement(ToggleButton, _extends({ ref: refs.elements.color }, ToggleButtonProps, { selected: refs.open.current.color, onClick: () => updateOpen('color', !refs.open.current.color) }), /*#__PURE__*/React.createElement(IconMaterialFormatColorText, IconProps)))), 'font-color-mini-menu': /*#__PURE__*/React.createElement(WrapperAppend, { open: refs.open.current.colorMiniMenu, anchorElement: refs.miniMenuElements.color, element: /*#__PURE__*/React.createElement(ClickListener, { onClickOutside: () => updateOpen('colorMiniMenu', false), include: [refs.miniMenuElements.color] }, /*#__PURE__*/React.createElement(Palette, { ref: refs.miniMenuElements.colorPalette, version: "font-color", onClose: () => updateOpen('colorMiniMenu', false), onUpdate: textMethod('font-color') })) }, /*#__PURE__*/React.createElement(WrapperToggleButton, { name: l('Text Color'), open: refs.open.current.colorMiniMenu ? false : undefined }, /*#__PURE__*/React.createElement(ToggleButton, _extends({ ref: refs.miniMenuElements.color }, ToggleButtonProps, { selected: refs.open.current.colorMiniMenu, onClick: () => updateOpen('colorMiniMenu', !refs.open.current.colorMiniMenu) }), /*#__PURE__*/React.createElement(IconMaterialFormatColorText, IconProps)))), 'font-background': /*#__PURE__*/React.createElement(WrapperAppend, { open: refs.open.current.background, anchorElement: refs.elements.background.current, element: /*#__PURE__*/React.createElement(ClickListener, { onClickOutside: () => updateOpen('background', false), include: [refs.elements.background.current] }, /*#__PURE__*/React.createElement(Palette, { version: "font-background", onClose: () => updateOpen('background', false), onUpdate: textMethod('font-background') })) }, /*#__PURE__*/React.createElement(WrapperToggleButton, { name: l('Background Color'), open: refs.open.current.background ? false : undefined }, /*#__PURE__*/React.createElement(ToggleButton, _extends({ ref: refs.elements.background }, ToggleButtonProps, { selected: refs.open.current.background, onClick: () => updateOpen('background', !refs.open.current.background) }), /*#__PURE__*/React.createElement(IconMaterialFormatColorFill, IconProps)))), 'font-background-mini-menu': /*#__PURE__*/React.createElement(WrapperAppend, { open: refs.open.current.backgroundMiniMenu, anchorElement: refs.miniMenuElements.background, element: /*#__PURE__*/React.createElement(ClickListener, { onClickOutside: () => updateOpen('backgroundMiniMenu', false), include: [refs.miniMenuElements.background] }, /*#__PURE__*/React.createElement(Palette, { ref: refs.miniMenuElements.backgroundPalette, version: "font-background", onClose: () => updateOpen('backgroundMiniMenu', false), onUpdate: textMethod('font-background') })) }, /*#__PURE__*/React.createElement(WrapperToggleButton, { name: l('Text Color'), open: refs.open.current.backgroundMiniMenu ? false : undefined }, /*#__PURE__*/React.createElement(ToggleButton, _extends({ ref: refs.miniMenuElements.background }, ToggleButtonProps, { selected: refs.open.current.backgroundMiniMenu, onClick: () => updateOpen('backgroundMiniMenu', !refs.open.