phx-react
Version:
PHX REACT
230 lines • 13.3 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = ToolbarPlugin;
const tslib_1 = require("tslib");
const link_1 = require("@lexical/link");
const list_1 = require("@lexical/list");
const LexicalComposerContext_1 = require("@lexical/react/LexicalComposerContext");
const rich_text_1 = require("@lexical/rich-text");
const selection_1 = require("@lexical/selection");
const utils_1 = require("@lexical/utils");
const lexical_1 = require("lexical");
const react_1 = require("react");
const React = tslib_1.__importStar(require("react"));
const getSelectedNode_1 = require("../../utils/getSelectedNode");
const url_1 = require("../../utils/url");
const heading_1 = tslib_1.__importStar(require("./components/heading"));
const font_size_1 = tslib_1.__importDefault(require("./components/font-size"));
const bold_text_1 = tslib_1.__importDefault(require("./components/bold-text"));
const italic_text_1 = tslib_1.__importDefault(require("./components/italic-text"));
const underline_text_1 = tslib_1.__importDefault(require("./components/underline-text"));
const text_color_1 = tslib_1.__importDefault(require("./components/text-color"));
const text_align_1 = tslib_1.__importDefault(require("./components/text-align"));
const bullet_1 = tslib_1.__importDefault(require("./components/bullet"));
const number_bullet_1 = tslib_1.__importDefault(require("./components/number-bullet"));
const upload_pdf_1 = tslib_1.__importDefault(require("./components/upload-pdf"));
const upload_image_1 = tslib_1.__importDefault(require("./components/upload-image"));
const link_2 = tslib_1.__importDefault(require("./components/link"));
const ImageNode_1 = require("../../nodes/ImageNode");
const InlineImageNode_1 = require("../../nodes/InlineImageNode");
const blockTypeToBlockName = {
bullet: 'Bulleted List',
check: 'Check List',
code: 'Code Block',
h1: 'Heading 1',
h2: 'Heading 2',
h3: 'Heading 3',
h4: 'Heading 4',
h5: 'Heading 5',
h6: 'Heading 6',
number: 'Numbered List',
paragraph: 'Normal',
quote: 'Quote',
};
/**
* Checks whether a block type is a heading type.
* @param value Block type to test.
* @returns True when the value is a heading block type.
*/
function isHeadingBlockType(value) {
return value in heading_1.DEFAULT_HEADING_FONT_SIZES;
}
/**
* Gets the fallback block type represented by a list item font size.
* @param fontSize Font size from the selected list item.
* @returns Matching heading type, or paragraph when there is no match.
*/
function getBlockTypeFromFontSize(fontSize) {
var _a;
const headingType = (_a = Object.entries(heading_1.DEFAULT_HEADING_FONT_SIZES).find(([, size]) => size === fontSize)) === null || _a === void 0 ? void 0 : _a[0];
return headingType !== undefined ? headingType : 'paragraph';
}
/**
* Reads font size from a serialized style string.
* @param style Serialized CSS style string.
* @returns Font size value when present.
*/
function getFontSizeFromStyle(style) {
return (0, selection_1.getStyleObjectFromCSS)(style)['font-size'];
}
function DisabledToolbarGroup({ children, disabled, }) {
return (React.createElement("div", { className: disabled ? 'relative cursor-not-allowed opacity-40' : undefined },
children,
disabled && React.createElement("div", { className: 'absolute inset-0 z-10 cursor-not-allowed' })));
}
function ToolbarPlugin({ apiCdnUpload, disabled, }) {
const [editor] = (0, LexicalComposerContext_1.useLexicalComposerContext)();
const [activeEditor, setActiveEditor] = (0, react_1.useState)(editor);
const [blockType, setBlockType] = (0, react_1.useState)('paragraph');
const [listBlockType, setListBlockType] = (0, react_1.useState)(null);
const [fontSize, setFontSize] = (0, react_1.useState)('14px');
const [fontColor, setFontColor] = (0, react_1.useState)('#000');
const [elementFormat, setElementFormat] = (0, react_1.useState)('left');
const [isImageSelected, setIsImageSelected] = (0, react_1.useState)(false);
const [isLink, setIsLink] = (0, react_1.useState)(false);
const [isBold, setIsBold] = (0, react_1.useState)(false);
const [isItalic, setIsItalic] = (0, react_1.useState)(false);
const [isUnderline, setIsUnderline] = (0, react_1.useState)(false);
const $updateToolbar = (0, react_1.useCallback)(() => {
const selection = (0, lexical_1.$getSelection)();
if ((0, lexical_1.$isNodeSelection)(selection)) {
const node = selection.getNodes()[0];
if ((0, ImageNode_1.$isImageNode)(node) || (0, InlineImageNode_1.$isInlineImageNode)(node)) {
setIsImageSelected(true);
setElementFormat((node.getPosition() || 'center'));
}
else {
setIsImageSelected(false);
}
return;
}
if ((0, lexical_1.$isRangeSelection)(selection)) {
setIsImageSelected(false);
const anchorNode = selection.anchor.getNode();
let element = anchorNode.getKey() === 'root'
? anchorNode
: (0, utils_1.$findMatchingParent)(anchorNode, (e) => {
const parent = e.getParent();
return parent !== null && (0, lexical_1.$isRootOrShadowRoot)(parent);
});
if (element === null) {
element = anchorNode.getTopLevelElementOrThrow();
}
const elementKey = element.getKey();
const elementDOM = activeEditor.getElementByKey(elementKey);
// Update text format
setIsBold(selection.hasFormat('bold'));
setIsItalic(selection.hasFormat('italic'));
setIsUnderline(selection.hasFormat('underline'));
// Update links
const node = (0, getSelectedNode_1.getSelectedNode)(selection);
const parent = node.getParent();
if ((0, link_1.$isLinkNode)(parent) || (0, link_1.$isLinkNode)(node)) {
setIsLink(true);
}
else {
setIsLink(false);
}
const defaultFontSize = (0, rich_text_1.$isHeadingNode)(element) ? heading_1.DEFAULT_HEADING_FONT_SIZES[element.getTag()] : '14px';
let selectionFontSize = (0, selection_1.$getSelectionStyleValueForProperty)(selection, 'font-size', defaultFontSize);
if (elementDOM !== null) {
const parentList = (0, utils_1.$getNearestNodeOfType)(anchorNode, list_1.ListNode);
if (parentList !== null || (0, list_1.$isListNode)(element)) {
const listItemNode = (0, list_1.$isListItemNode)(anchorNode)
? anchorNode
: (0, utils_1.$findMatchingParent)(anchorNode, (node) => (0, list_1.$isListItemNode)(node));
const listItemFontSize = (0, list_1.$isListItemNode)(listItemNode)
? getFontSizeFromStyle(listItemNode.getTextStyle())
: undefined;
const listItemBlockType = (0, list_1.$isListItemNode)(listItemNode) ? (0, heading_1.getListItemBlockType)(listItemNode) : null;
// @ts-ignore
const type = parentList ? parentList.getListType() : element.getListType();
if (listItemFontSize !== undefined && !selection.isCollapsed()) {
selectionFontSize = listItemFontSize;
}
setListBlockType(type);
setBlockType(listItemBlockType !== null && listItemBlockType !== void 0 ? listItemBlockType : getBlockTypeFromFontSize(selectionFontSize));
}
else {
setListBlockType(null);
const type = (0, rich_text_1.$isHeadingNode)(element) ? element.getTag() : element.getType();
if (type in blockTypeToBlockName) {
setBlockType(type);
}
}
}
// Handle buttons
setFontSize(selectionFontSize);
setFontColor((0, selection_1.$getSelectionStyleValueForProperty)(selection, 'color', '#000'));
let matchingParent;
if ((0, link_1.$isLinkNode)(parent)) {
// If node is a link, we need to fetch the parent paragraph node to set format
matchingParent = (0, utils_1.$findMatchingParent)(node, (parentNode) => (0, lexical_1.$isElementNode)(parentNode) && !parentNode.isInline());
}
// If matchingParent is a valid node, pass it's format type
setElementFormat((0, lexical_1.$isElementNode)(matchingParent)
? matchingParent.getFormatType()
: (0, lexical_1.$isElementNode)(node)
? node.getFormatType()
: (parent === null || parent === void 0 ? void 0 : parent.getFormatType()) || 'left');
}
}, [activeEditor]);
(0, react_1.useEffect)(() => activeEditor.registerCommand(lexical_1.SELECTION_CHANGE_COMMAND, (_payload, newEditor) => {
$updateToolbar();
setActiveEditor(newEditor);
return false;
}, lexical_1.COMMAND_PRIORITY_CRITICAL), [activeEditor, $updateToolbar]);
(0, react_1.useEffect)(() => (0, utils_1.mergeRegister)(activeEditor.registerUpdateListener(({ editorState }) => {
editorState.read(() => {
$updateToolbar();
});
})), [$updateToolbar, activeEditor]);
(0, react_1.useEffect)(() => activeEditor.registerCommand(lexical_1.KEY_MODIFIER_COMMAND, (payload) => {
const event = payload;
const { code, ctrlKey, metaKey } = event;
if (code === 'KeyK' && (ctrlKey || metaKey)) {
event.preventDefault();
return activeEditor.dispatchCommand(link_1.TOGGLE_LINK_COMMAND, (0, url_1.sanitizeUrl)('https://'));
}
return false;
}, lexical_1.COMMAND_PRIORITY_NORMAL), [activeEditor, isLink]);
(0, react_1.useEffect)(() => {
if (!window.pdfjsLib) {
const script = document.createElement('script');
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js';
document.body.appendChild(script);
}
}, []);
(0, react_1.useEffect)(() => {
if (activeEditor) {
activeEditor.setEditable(!disabled);
}
}, [activeEditor, disabled]);
const parsedFontSize = parseInt(fontSize, 10);
const activeListHeadingTag = listBlockType !== null && isHeadingBlockType(blockType) ? blockType : null;
return (React.createElement("div", { className: 'flex relative flex-wrap gap-1 p-2 bg-gray-100 border-b border-gray-300' },
disabled && React.createElement("div", { className: 'cursor-not-allowed z-20 absolute inset-0 bg-gray-200 opacity-20' }),
React.createElement(DisabledToolbarGroup, { disabled: isImageSelected }, blockType in blockTypeToBlockName && activeEditor === editor && (React.createElement(heading_1.default, { value: blockType, editor: activeEditor }))),
React.createElement(DisabledToolbarGroup, { disabled: isImageSelected },
React.createElement(font_size_1.default, { editor: activeEditor, fontSize: Number.isNaN(parsedFontSize) ? null : parsedFontSize, listHeadingTag: activeListHeadingTag })),
React.createElement(DisabledToolbarGroup, { disabled: isImageSelected },
React.createElement(bold_text_1.default, { isActive: isBold, editor: activeEditor })),
React.createElement(DisabledToolbarGroup, { disabled: isImageSelected },
React.createElement(italic_text_1.default, { isActive: isItalic, editor: activeEditor })),
React.createElement(DisabledToolbarGroup, { disabled: isImageSelected },
React.createElement(underline_text_1.default, { isActive: isUnderline, editor: activeEditor })),
React.createElement(DisabledToolbarGroup, { disabled: isImageSelected },
React.createElement(text_color_1.default, { editor: activeEditor, color: fontColor })),
React.createElement(text_align_1.default, { value: elementFormat, editor: activeEditor, isImageSelected: isImageSelected }),
React.createElement(DisabledToolbarGroup, { disabled: isImageSelected },
React.createElement(bullet_1.default, { isActive: listBlockType === 'bullet', editor: activeEditor })),
React.createElement(DisabledToolbarGroup, { disabled: isImageSelected },
React.createElement(number_bullet_1.default, { isActive: listBlockType === 'number', editor: activeEditor })),
React.createElement(DisabledToolbarGroup, { disabled: isImageSelected },
React.createElement(upload_pdf_1.default, { editor: activeEditor, apiCdnUpload: apiCdnUpload })),
React.createElement(DisabledToolbarGroup, { disabled: isImageSelected },
React.createElement(upload_image_1.default, { editor: activeEditor, apiCdnUpload: apiCdnUpload })),
React.createElement(DisabledToolbarGroup, { disabled: isImageSelected },
React.createElement(link_2.default, { editor: activeEditor }))));
}
//# sourceMappingURL=index.js.map