UNPKG

@quillforms/block-editor

Version:
343 lines (320 loc) 11.2 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _data = require("@wordpress/data"); var _hooks = require("@wordpress/hooks"); var _element = require("@wordpress/element"); var _i18n = require("@wordpress/i18n"); var _adminComponents = require("@quillforms/admin-components"); var _emotion = require("emotion"); var _classnames = _interopRequireDefault(require("classnames")); var _jsxRuntime = require("react/jsx-runtime"); /** * QuillForms Dependencies */ /** * External Dependencies */ /** * Custom hook for debounced updates */const useDebounce = (callback, delay) => { const timeoutRef = (0, _element.useRef)(null); const debouncedCallback = (0, _element.useCallback)((...args) => { // Clear the previous timeout if (timeoutRef.current) { clearTimeout(timeoutRef.current); } // Set a new timeout timeoutRef.current = setTimeout(() => { callback(...args); }, delay); }, [callback, delay]); // Cleanup on unmount (0, _element.useEffect)(() => { return () => { if (timeoutRef.current) { clearTimeout(timeoutRef.current); } }; }, []); // Function to immediately execute pending updates const flush = (0, _element.useCallback)(() => { if (timeoutRef.current) { clearTimeout(timeoutRef.current); timeoutRef.current = null; } }, []); return { debouncedCallback, flush }; }; /** * BlockEditor Component */ const BlockEditor = ({ type, childId, childIndex, parentId }) => { // Selectors and Dispatch const { currentBlock, isAnimating, currentChildBlockId } = (0, _data.useSelect)(select => ({ currentBlock: select("quillForms/block-editor").getCurrentBlock(), isAnimating: select("quillForms/renderer-core").isAnimating(), currentChildBlockId: select("quillForms/block-editor").getCurrentChildBlockId() })); const lastFocusedRef = (0, _element.useRef)(false); const [isFocused, setIsFocused] = (0, _element.useState)(false); const { prevFields, correctIncorrectQuiz, blockTypes } = (0, _data.useSelect)(select => { return { blockTypes: select('quillForms/blocks').getBlockTypes(), prevFields: select('quillForms/block-editor').getPreviousEditableFieldsWithOrder(currentBlock?.id), correctIncorrectQuiz: select('quillForms/quiz-editor').getState() }; }); const { setBlockAttributes } = (0, _data.useDispatch)("quillForms/block-editor"); // Destructure current block attributes let { attributes, id } = currentBlock || {}; let isChildBlock = false; if (type === "label" && childIndex !== undefined && childIndex > -1 && currentBlock?.innerBlocks?.[childIndex]) { attributes = currentBlock.innerBlocks[childIndex].attributes; id = currentBlock.innerBlocks[childIndex].id; isChildBlock = true; } const label = attributes?.label || ""; const description = attributes?.description || ""; // Editor instance const editor = (0, _element.useMemo)(() => (0, _adminComponents.__unstableCreateEditor)(), []); // Local editor value for immediate UI updates const [editorValue, setEditorValue] = (0, _element.useState)(() => { if (type === "label") { return (0, _adminComponents.__unstableHtmlDeserialize)(isChildBlock ? attributes?.label || "" : label); } if (type === "description") { return (0, _adminComponents.__unstableHtmlDeserialize)(description); } return []; }); // Debounced update function const updateBlockAttributes = (0, _element.useCallback)(serializedValue => { if (type === "label") { if (isChildBlock) { setBlockAttributes(childId, { label: serializedValue }, parentId); } else { setBlockAttributes(id, { label: serializedValue }); } } else if (type === "description") { setBlockAttributes(id, { description: serializedValue }); } }, [type, isChildBlock, childId, parentId, id, setBlockAttributes]); // Create debounced version with 300ms delay const { debouncedCallback: debouncedUpdate, flush } = useDebounce(updateBlockAttributes, 0); // Handle focus state changes (0, _element.useEffect)(() => { const editorNode = _adminComponents.__unstableReactEditor.toDOMNode(editor, editor); const handleEditorFocus = () => { lastFocusedRef.current = true; }; const handleEditorBlur = () => { lastFocusedRef.current = false; // Flush any pending updates when user leaves the editor flush(); }; editorNode.addEventListener('focusin', handleEditorFocus); editorNode.addEventListener('focusout', handleEditorBlur); return () => { editorNode.removeEventListener('focusin', handleEditorFocus); editorNode.removeEventListener('focusout', handleEditorBlur); }; }, [editor, flush]); // Update Editor Value When `type` or Attributes Change (0, _element.useEffect)(() => { if (type === "label") { setEditorValue((0, _adminComponents.__unstableHtmlDeserialize)(isChildBlock ? attributes?.label || "" : label)); } else if (type === "description") { setEditorValue((0, _adminComponents.__unstableHtmlDeserialize)(description)); } }, [attributes, type, childIndex]); // Auto-focus and scroll logic (0, _element.useEffect)(() => { let timeoutId; if (!isAnimating && type === "label") { if (currentChildBlockId === childId || !isChildBlock && !currentChildBlockId) { timeoutId = setTimeout(() => { const editorEl = _adminComponents.__unstableReactEditor.toDOMNode(editor, editor); const rect = editorEl.getBoundingClientRect(); const isInViewport = rect.top >= 0 && rect.left >= 0 && rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && rect.right <= (window.innerWidth || document.documentElement.clientWidth); if (!isInViewport) { editorEl.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } }, 100); } } return () => { if (timeoutId) { clearTimeout(timeoutId); } }; }, [isAnimating, currentChildBlockId, childId, isChildBlock, type, editor]); // Optimized editor change handler const handleEditorChange = (0, _element.useCallback)(value => { const currentSerialized = (0, _adminComponents.__unstableHtmlSerialize)(editorValue); const newSerialized = (0, _adminComponents.__unstableHtmlSerialize)(value); // Only update if content actually changed if (newSerialized !== currentSerialized) { // Update local state immediately for responsive UI setEditorValue(value); // Debounce the actual block attribute update debouncedUpdate(newSerialized); } }, [editorValue, debouncedUpdate]); const editorRef = (0, _element.useRef)(null); const handleFocus = (0, _element.useCallback)(() => { if (!_adminComponents.__unstableReactEditor.isFocused(editor)) { _adminComponents.__unstableReactEditor.focus(editor); } }, [editor]); const handleBlur = (0, _element.useCallback)(() => { setIsFocused(false); // Ensure any pending updates are saved when user blurs flush(); }, [flush]); // Memoize merge tags to prevent unnecessary recalculations const mergeTags = (0, _element.useMemo)(() => { let tags = prevFields.map(field => ({ type: 'field', label: field?.attributes?.label, modifier: field.id, icon: blockTypes[field.name]?.icon, color: blockTypes[field.name]?.color, order: field.order })); tags = tags.concat((0, _hooks.applyFilters)('QuillForms.Builder.MergeTags', [])); if (correctIncorrectQuiz?.enabled) { tags = tags.concat([{ type: 'quiz', label: 'Correct Answers Count', modifier: 'correct_answers_count', icon: 'yes', color: '#4caf50', order: undefined }, { type: 'quiz', label: 'Incorrect Answers Count', modifier: 'incorrect_answers_count', icon: 'no-alt', color: '#f44336', order: undefined }, { type: 'quiz', label: 'Quiz Summary', modifier: 'summary', icon: 'editor-table', color: '#4caf50', order: undefined }]); } return tags; }, [prevFields, blockTypes, correctIncorrectQuiz?.enabled]); // Memoized styles const editorStyle = (0, _element.useMemo)(() => (0, _emotion.css)` p { color: inherit !important; font-family: inherit !important; margin: 0; @media (min-width: 768px) { font-size: inherit !important; line-height: inherit !important; } @media (max-width: 767px) { font-size: inherit !important; line-height: inherit !important; } } `, []); const descriptionStyle = (0, _element.useMemo)(() => (0, _emotion.css)` p { color: inherit !important; font-family: inherit !important; @media (min-width: 768px) { font-size: inherit !important; line-height: inherit !important; } @media (max-width: 767px) { font-size: inherit !important; line-height: inherit !important; } } `, []); const wrapperStyles = (0, _element.useMemo)(() => (0, _emotion.css)` &.block-editor-block-edit-label__editor:not(.is-focused) { [data-slate-placeholder="true"] { color: #757575 !important; opacity: 0.87 !important; } } .richtext__editor { position: relative; } [contenteditable] { position: relative; z-index: 1; } `, []); // Cleanup on unmount (0, _element.useEffect)(() => { return () => { // Ensure any pending updates are saved when component unmounts flush(); }; }, [flush]); return /*#__PURE__*/(0, _jsxRuntime.jsx)("div", { className: (0, _classnames.default)("block-editor-block-edit__editor", `block-editor-block-edit-${type}__editor`, { 'is-focused': isFocused }, wrapperStyles), onBlur: handleBlur, onClick: handleFocus, ref: editorRef, children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_adminComponents.__experimentalEditor, { editor: editor, placeholder: type === "label" ? (0, _i18n.__)("Type question here. Recall information with @.", "quillforms") : (0, _i18n.__)("Add a description", "quillforms"), className: type === "label" ? editorStyle : descriptionStyle, mergeTags: mergeTags, value: editorValue, onFocus: handleFocus, onChange: handleEditorChange, allowedFormats: ["bold", "italic", "link", "color"] }) }); }; var _default = exports.default = BlockEditor; //# sourceMappingURL=editor.js.map