phx-react
Version:
PHX REACT
219 lines • 11.8 kB
JavaScript
import { $isLinkNode, TOGGLE_LINK_COMMAND } from '@lexical/link';
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { mergeRegister } from '@lexical/utils';
import { $getSelection, $isParagraphNode, $isRangeSelection, $isTextNode, COMMAND_PRIORITY_LOW, FORMAT_TEXT_COMMAND, SELECTION_CHANGE_COMMAND, } from 'lexical';
import { useCallback, useEffect, useRef, useState } from 'react';
import * as React from 'react';
import { createPortal } from 'react-dom';
import { getDOMRangeRect } from '../../utils/getDOMRangeRect';
import { getSelectedNode } from '../../utils/getSelectedNode';
import { setFloatingElemPosition } from '../../utils/setFloatingElemPosition';
function TextFormatFloatingToolbar({ anchorElem, editor, isBold, isCode, isItalic, isLink, isStrikethrough, isSubscript, isSuperscript, isUnderline, }) {
const popupCharStylesEditorRef = useRef(null);
const insertLink = useCallback(() => {
if (!isLink) {
editor.dispatchCommand(TOGGLE_LINK_COMMAND, 'https://');
}
else {
editor.dispatchCommand(TOGGLE_LINK_COMMAND, null);
}
}, [editor, isLink]);
const insertComment = () => {
console.log('insert comment');
};
function mouseMoveListener(e) {
if ((popupCharStylesEditorRef === null || popupCharStylesEditorRef === void 0 ? void 0 : popupCharStylesEditorRef.current) && (e.buttons === 1 || e.buttons === 3)) {
if (popupCharStylesEditorRef.current.style.pointerEvents !== 'none') {
const x = e.clientX;
const y = e.clientY;
const elementUnderMouse = document.elementFromPoint(x, y);
if (!popupCharStylesEditorRef.current.contains(elementUnderMouse)) {
// Mouse is not over the target element => not a normal click, but probably a drag
popupCharStylesEditorRef.current.style.pointerEvents = 'none';
}
}
}
}
function mouseUpListener() {
if (popupCharStylesEditorRef === null || popupCharStylesEditorRef === void 0 ? void 0 : popupCharStylesEditorRef.current) {
if (popupCharStylesEditorRef.current.style.pointerEvents !== 'auto') {
popupCharStylesEditorRef.current.style.pointerEvents = 'auto';
}
}
}
useEffect(() => {
if (popupCharStylesEditorRef === null || popupCharStylesEditorRef === void 0 ? void 0 : popupCharStylesEditorRef.current) {
document.addEventListener('mousemove', mouseMoveListener);
document.addEventListener('mouseup', mouseUpListener);
return () => {
document.removeEventListener('mousemove', mouseMoveListener);
document.removeEventListener('mouseup', mouseUpListener);
};
}
return () => {
console.log('clean up');
};
}, [popupCharStylesEditorRef]);
const updateTextFormatFloatingToolbar = useCallback(() => {
const selection = $getSelection();
const popupCharStylesEditorElem = popupCharStylesEditorRef.current;
const nativeSelection = window.getSelection();
if (popupCharStylesEditorElem === null) {
return;
}
const rootElement = editor.getRootElement();
if (selection !== null &&
nativeSelection !== null &&
!nativeSelection.isCollapsed &&
rootElement !== null &&
rootElement.contains(nativeSelection.anchorNode)) {
const rangeRect = getDOMRangeRect(nativeSelection, rootElement);
setFloatingElemPosition(rangeRect, popupCharStylesEditorElem, anchorElem, isLink);
}
}, [editor, anchorElem, isLink]);
useEffect(() => {
const scrollerElem = anchorElem.parentElement;
const update = () => {
editor.getEditorState().read(() => {
updateTextFormatFloatingToolbar();
});
};
window.addEventListener('resize', update);
if (scrollerElem) {
scrollerElem.addEventListener('scroll', update);
}
return () => {
window.removeEventListener('resize', update);
if (scrollerElem) {
scrollerElem.removeEventListener('scroll', update);
}
};
}, [editor, updateTextFormatFloatingToolbar, anchorElem]);
useEffect(() => {
editor.getEditorState().read(() => {
updateTextFormatFloatingToolbar();
});
return mergeRegister(editor.registerUpdateListener(({ editorState }) => {
editorState.read(() => {
updateTextFormatFloatingToolbar();
});
}), editor.registerCommand(SELECTION_CHANGE_COMMAND, () => {
updateTextFormatFloatingToolbar();
return false;
}, COMMAND_PRIORITY_LOW));
}, [editor, updateTextFormatFloatingToolbar]);
return (React.createElement("div", { ref: popupCharStylesEditorRef, className: 'floating-text-format-popup' },
editor.isEditable() && (React.createElement(React.Fragment, null,
React.createElement("button", { "aria-label": 'Format text as bold', className: 'popup-item spaced ' + (isBold ? 'active' : ''), onClick: () => {
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'bold');
}, type: 'button' },
React.createElement("i", { className: 'format bold' })),
React.createElement("button", { "aria-label": 'Format text as italics', className: 'popup-item spaced ' + (isItalic ? 'active' : ''), onClick: () => {
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'italic');
}, type: 'button' },
React.createElement("i", { className: 'format italic' })),
React.createElement("button", { "aria-label": 'Format text to underlined', className: 'popup-item spaced ' + (isUnderline ? 'active' : ''), onClick: () => {
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'underline');
}, type: 'button' },
React.createElement("i", { className: 'format underline' })),
React.createElement("button", { "aria-label": 'Format text with a strikethrough', className: 'popup-item spaced ' + (isStrikethrough ? 'active' : ''), onClick: () => {
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'strikethrough');
}, type: 'button' },
React.createElement("i", { className: 'format strikethrough' })),
React.createElement("button", { "aria-label": 'Format Subscript', className: 'popup-item spaced ' + (isSubscript ? 'active' : ''), onClick: () => {
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'subscript');
}, title: 'Subscript', type: 'button' },
React.createElement("i", { className: 'format subscript' })),
React.createElement("button", { "aria-label": 'Format Superscript', className: 'popup-item spaced ' + (isSuperscript ? 'active' : ''), onClick: () => {
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'superscript');
}, title: 'Superscript', type: 'button' },
React.createElement("i", { className: 'format superscript' })),
React.createElement("button", { "aria-label": 'Insert code block', className: 'popup-item spaced ' + (isCode ? 'active' : ''), onClick: () => {
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'code');
}, type: 'button' },
React.createElement("i", { className: 'format code' })),
React.createElement("button", { "aria-label": 'Insert link', className: 'popup-item spaced ' + (isLink ? 'active' : ''), onClick: insertLink, type: 'button' },
React.createElement("i", { className: 'format link' })))),
React.createElement("button", { "aria-label": 'Insert comment', className: 'popup-item spaced insert-comment', onClick: insertComment, type: 'button' },
React.createElement("i", { className: 'format add-comment' }))));
}
function useFloatingTextFormatToolbar(editor, anchorElem) {
const [isText, setIsText] = useState(false);
const [isLink, setIsLink] = useState(false);
const [isBold, setIsBold] = useState(false);
const [isItalic, setIsItalic] = useState(false);
const [isUnderline, setIsUnderline] = useState(false);
const [isStrikethrough, setIsStrikethrough] = useState(false);
const [isSubscript, setIsSubscript] = useState(false);
const [isSuperscript, setIsSuperscript] = useState(false);
const [isCode, setIsCode] = useState(false);
const updatePopup = useCallback(() => {
editor.getEditorState().read(() => {
// Should not to pop up the floating toolbar when using IME input
if (editor.isComposing()) {
return;
}
const selection = $getSelection();
const nativeSelection = window.getSelection();
const rootElement = editor.getRootElement();
if (nativeSelection !== null &&
(!$isRangeSelection(selection) || rootElement === null || !rootElement.contains(nativeSelection.anchorNode))) {
setIsText(false);
return;
}
if (!$isRangeSelection(selection)) {
return;
}
const node = getSelectedNode(selection);
// Update text format
setIsBold(selection.hasFormat('bold'));
setIsItalic(selection.hasFormat('italic'));
setIsUnderline(selection.hasFormat('underline'));
setIsStrikethrough(selection.hasFormat('strikethrough'));
setIsSubscript(selection.hasFormat('subscript'));
setIsSuperscript(selection.hasFormat('superscript'));
setIsCode(selection.hasFormat('code'));
// Update links
const parent = node.getParent();
if ($isLinkNode(parent) || $isLinkNode(node)) {
setIsLink(true);
}
else {
setIsLink(false);
}
if (selection.getTextContent() !== '') {
setIsText($isTextNode(node) || $isParagraphNode(node));
}
else {
setIsText(false);
}
const rawTextContent = selection.getTextContent().replace(/\n/g, '');
if (!selection.isCollapsed() && rawTextContent === '') {
setIsText(false);
return;
}
});
}, [editor]);
useEffect(() => {
document.addEventListener('selectionchange', updatePopup);
return () => {
document.removeEventListener('selectionchange', updatePopup);
};
}, [updatePopup]);
useEffect(() => mergeRegister(editor.registerUpdateListener(() => {
updatePopup();
}), editor.registerRootListener(() => {
if (editor.getRootElement() === null) {
setIsText(false);
}
})), [editor, updatePopup]);
if (!isText) {
return null;
}
return createPortal(React.createElement(TextFormatFloatingToolbar, { anchorElem: anchorElem, editor: editor, isBold: isBold, isCode: isCode, isItalic: isItalic, isLink: isLink, isStrikethrough: isStrikethrough, isSubscript: isSubscript, isSuperscript: isSuperscript, isUnderline: isUnderline }), anchorElem);
}
export default function FloatingTextFormatToolbarPlugin({ anchorElem = document.body, }) {
const [editor] = useLexicalComposerContext();
return useFloatingTextFormatToolbar(editor, anchorElem);
}
//# sourceMappingURL=index.js.map