@squiz/formatted-text-editor 2.4.0 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/demo/{App.tsx → diff/App.tsx} +3 -2
- package/demo/{AppContext.tsx → diff/AppContext.tsx} +1 -2
- package/demo/diff/index.html +14 -0
- package/demo/{main.tsx → diff/main.tsx} +1 -1
- package/demo/index.html +47 -2
- package/demo/portals/Accordion.tsx +50 -0
- package/demo/portals/App.tsx +150 -0
- package/demo/portals/index.html +13 -0
- package/demo/portals/index.scss +8 -0
- package/demo/portals/index.tsx +12 -0
- package/demo/portals/preview.html +91 -0
- package/demo/portals/preview.tsx +10 -0
- package/lib/Editor/Editor.d.ts +11 -6
- package/lib/Editor/Editor.js +17 -26
- package/lib/EditorToolbar/Toolbar.d.ts +2 -1
- package/lib/EditorToolbar/Toolbar.js +4 -2
- package/lib/EditorToolbar/Tools/Image/Form/ImageForm.js +0 -3
- package/lib/Extensions/CodeBlockExtension/CodeBlockExtension.d.ts +13 -3
- package/lib/Extensions/CodeBlockExtension/CodeBlockExtension.js +74 -8
- package/lib/Extensions/Extensions.d.ts +1 -1
- package/lib/Extensions/Extensions.js +3 -3
- package/lib/Extensions/PreformattedExtension/PreformattedExtension.d.ts +5 -2
- package/lib/Extensions/PreformattedExtension/PreformattedExtension.js +8 -1
- package/lib/hooks/index.d.ts +3 -2
- package/lib/hooks/index.js +3 -2
- package/lib/hooks/useFocus/useFocus.d.ts +6 -0
- package/lib/hooks/{useFocus.js → useFocus/useFocus.js} +29 -15
- package/lib/index.css +7 -2
- package/lib/ui/EditorInput/EditorInput.d.ts +3 -0
- package/lib/ui/EditorInput/EditorInput.js +49 -0
- package/lib/ui/EditorInput/EditorInput.props.d.ts +4 -0
- package/lib/ui/EditorInput/EditorInput.props.js +2 -0
- package/lib/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.js +0 -3
- package/lib/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.js +0 -6
- package/package.json +1 -1
- package/src/Editor/Editor.spec.tsx +35 -10
- package/src/Editor/Editor.tsx +48 -44
- package/src/Editor/_editor.scss +4 -0
- package/src/EditorToolbar/Toolbar.tsx +8 -4
- package/src/EditorToolbar/Tools/Image/Form/ImageForm.tsx +0 -3
- package/src/EditorToolbar/Tools/TextType/CodeBlock/CodeBlockButton.tsx +3 -3
- package/src/EditorToolbar/_toolbar.scss +3 -2
- package/src/Extensions/CodeBlockExtension/CodeBlockExtension.props.ts +3 -0
- package/src/Extensions/CodeBlockExtension/CodeBlockExtension.spec.ts +59 -0
- package/src/Extensions/CodeBlockExtension/CodeBlockExtension.ts +82 -7
- package/src/Extensions/Extensions.ts +4 -4
- package/src/Extensions/PreformattedExtension/PreformattedExtension.ts +15 -3
- package/src/hooks/index.ts +3 -2
- package/src/hooks/useFocus/useFocus.spec.tsx +48 -0
- package/src/hooks/useFocus/useFocus.ts +71 -0
- package/src/ui/EditorInput/EditorInput.props.ts +5 -0
- package/src/ui/EditorInput/EditorInput.spec.tsx +38 -0
- package/src/ui/EditorInput/EditorInput.tsx +30 -0
- package/src/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.spec.ts +1 -3
- package/src/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.ts +0 -4
- package/src/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.spec.ts +1 -4
- package/src/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.ts +0 -5
- package/lib/hooks/useFocus.d.ts +0 -8
- package/src/hooks/useFocus.ts +0 -61
- /package/demo/{index.scss → diff/index.scss} +0 -0
- /package/demo/{resources.json → diff/resources.json} +0 -0
- /package/demo/{sources.json → diff/sources.json} +0 -0
- /package/demo/{vite-env.d.ts → diff/vite-env.d.ts} +0 -0
- /package/lib/hooks/{useExpandedSelection.d.ts → useExpandedSelection/useExpandedSelection.d.ts} +0 -0
- /package/lib/hooks/{useExpandedSelection.js → useExpandedSelection/useExpandedSelection.js} +0 -0
- /package/lib/hooks/{useExtensionNames.d.ts → useExtensionNames/useExtensionNames.d.ts} +0 -0
- /package/lib/hooks/{useExtensionNames.js → useExtensionNames/useExtensionNames.js} +0 -0
- /package/src/hooks/{useExpandedSelection.ts → useExpandedSelection/useExpandedSelection.ts} +0 -0
- /package/src/hooks/{useExtensionNames.ts → useExtensionNames/useExtensionNames.ts} +0 -0
| @@ -1,18 +1,65 @@ | |
| 1 1 | 
             
            "use strict";
         | 
| 2 | 
            +
            var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
         | 
| 3 | 
            +
                var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
         | 
| 4 | 
            +
                if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
         | 
| 5 | 
            +
                else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
         | 
| 6 | 
            +
                return c > 3 && r && Object.defineProperty(target, key, r), r;
         | 
| 7 | 
            +
            };
         | 
| 2 8 | 
             
            Object.defineProperty(exports, "__esModule", { value: true });
         | 
| 3 | 
            -
            exports. | 
| 4 | 
            -
            const  | 
| 5 | 
            -
             | 
| 9 | 
            +
            exports.CodeBlockExtension = void 0;
         | 
| 10 | 
            +
            const core_1 = require("@remirror/core");
         | 
| 11 | 
            +
            const remirror_1 = require("remirror");
         | 
| 12 | 
            +
            let CodeBlockExtension = class CodeBlockExtension extends core_1.NodeExtension {
         | 
| 13 | 
            +
                get name() {
         | 
| 14 | 
            +
                    return 'codeBlock';
         | 
| 15 | 
            +
                }
         | 
| 16 | 
            +
                createTags() {
         | 
| 17 | 
            +
                    return [core_1.ExtensionTag.Block, core_1.ExtensionTag.Code];
         | 
| 18 | 
            +
                }
         | 
| 19 | 
            +
                createNodeSpec(extra, override) {
         | 
| 20 | 
            +
                    return {
         | 
| 21 | 
            +
                        content: 'text*',
         | 
| 22 | 
            +
                        marks: '',
         | 
| 23 | 
            +
                        defining: true,
         | 
| 24 | 
            +
                        isolating: true,
         | 
| 25 | 
            +
                        draggable: false,
         | 
| 26 | 
            +
                        ...override,
         | 
| 27 | 
            +
                        code: true,
         | 
| 28 | 
            +
                        attrs: {
         | 
| 29 | 
            +
                            ...extra.defaults(),
         | 
| 30 | 
            +
                        },
         | 
| 31 | 
            +
                        parseDOM: [
         | 
| 32 | 
            +
                            {
         | 
| 33 | 
            +
                                tag: 'code',
         | 
| 34 | 
            +
                                preserveWhitespace: 'full',
         | 
| 35 | 
            +
                            },
         | 
| 36 | 
            +
                            {
         | 
| 37 | 
            +
                                tag: 'pre',
         | 
| 38 | 
            +
                                preserveWhitespace: 'full',
         | 
| 39 | 
            +
                                getAttrs: (node) => {
         | 
| 40 | 
            +
                                    if (!(0, remirror_1.isElementDomNode)(node) || !(0, remirror_1.isElementDomNode)(node.querySelector('code'))) {
         | 
| 41 | 
            +
                                        return false;
         | 
| 42 | 
            +
                                    }
         | 
| 43 | 
            +
                                    return extra.parse(node);
         | 
| 44 | 
            +
                                },
         | 
| 45 | 
            +
                            },
         | 
| 46 | 
            +
                        ],
         | 
| 47 | 
            +
                        toDOM: (node) => {
         | 
| 48 | 
            +
                            return ['code', extra.dom(node), 0];
         | 
| 49 | 
            +
                        },
         | 
| 50 | 
            +
                    };
         | 
| 51 | 
            +
                }
         | 
| 6 52 | 
             
                createNodeViews() {
         | 
| 7 | 
            -
                     | 
| 8 | 
            -
                         | 
| 53 | 
            +
                    if (!this.options.enableDecorations) {
         | 
| 54 | 
            +
                        return {};
         | 
| 55 | 
            +
                    }
         | 
| 56 | 
            +
                    return () => {
         | 
| 9 57 | 
             
                        // This is the pre container for the code block
         | 
| 10 58 | 
             
                        const dom = document.createElement('pre');
         | 
| 11 59 | 
             
                        dom.setAttribute('spellcheck', 'false');
         | 
| 12 60 | 
             
                        dom.classList.add(`code-block`);
         | 
| 13 61 | 
             
                        // This is the actual code content in the code block
         | 
| 14 62 | 
             
                        const contentDOM = document.createElement('code');
         | 
| 15 | 
            -
                        contentDOM.setAttribute('data-code-block-language', language);
         | 
| 16 63 | 
             
                        // Divider between code block and pre container
         | 
| 17 64 | 
             
                        const dividerElement = document.createElement('div');
         | 
| 18 65 | 
             
                        dividerElement.classList.add('block-divider');
         | 
| @@ -26,5 +73,24 @@ class ExtendedCodeBlockExtension extends extension_code_block_1.CodeBlockExtensi | |
| 26 73 | 
             
                        return { dom, contentDOM };
         | 
| 27 74 | 
             
                    };
         | 
| 28 75 | 
             
                }
         | 
| 29 | 
            -
             | 
| 30 | 
            -
             | 
| 76 | 
            +
                /**
         | 
| 77 | 
            +
                 * Toggle the <code> for the current block.
         | 
| 78 | 
            +
                 */
         | 
| 79 | 
            +
                toggleCodeBlock() {
         | 
| 80 | 
            +
                    return (0, core_1.toggleBlockItem)({
         | 
| 81 | 
            +
                        type: this.type,
         | 
| 82 | 
            +
                        toggleType: 'paragraph',
         | 
| 83 | 
            +
                    });
         | 
| 84 | 
            +
                }
         | 
| 85 | 
            +
            };
         | 
| 86 | 
            +
            __decorate([
         | 
| 87 | 
            +
                (0, core_1.command)()
         | 
| 88 | 
            +
            ], CodeBlockExtension.prototype, "toggleCodeBlock", null);
         | 
| 89 | 
            +
            CodeBlockExtension = __decorate([
         | 
| 90 | 
            +
                (0, core_1.extension)({
         | 
| 91 | 
            +
                    defaultOptions: {
         | 
| 92 | 
            +
                        enableDecorations: false,
         | 
| 93 | 
            +
                    },
         | 
| 94 | 
            +
                })
         | 
| 95 | 
            +
            ], CodeBlockExtension);
         | 
| 96 | 
            +
            exports.CodeBlockExtension = CodeBlockExtension;
         | 
| @@ -15,4 +15,4 @@ export declare enum MarkName { | |
| 15 15 | 
             
                Link = "link",
         | 
| 16 16 | 
             
                AssetLink = "assetLink"
         | 
| 17 17 | 
             
            }
         | 
| 18 | 
            -
            export declare const createExtensions: (context: EditorContextOptions) => () => Extension[];
         | 
| 18 | 
            +
            export declare const createExtensions: (context: EditorContextOptions, enableDecorations?: boolean) => () => Extension[];
         | 
| @@ -32,7 +32,7 @@ var MarkName; | |
| 32 32 | 
             
                MarkName["Link"] = "link";
         | 
| 33 33 | 
             
                MarkName["AssetLink"] = "assetLink";
         | 
| 34 34 | 
             
            })(MarkName = exports.MarkName || (exports.MarkName = {}));
         | 
| 35 | 
            -
            const createExtensions = (context) => {
         | 
| 35 | 
            +
            const createExtensions = (context, enableDecorations = true) => {
         | 
| 36 36 | 
             
                return () => {
         | 
| 37 37 | 
             
                    return [
         | 
| 38 38 | 
             
                        new CommandsExtension_1.CommandsExtension(),
         | 
| @@ -43,8 +43,8 @@ const createExtensions = (context) => { | |
| 43 43 | 
             
                        new extensions_1.NodeFormattingExtension({ indents: [] }),
         | 
| 44 44 | 
             
                        new extensions_1.ParagraphExtension(),
         | 
| 45 45 | 
             
                        new extensions_1.HardBreakExtension(),
         | 
| 46 | 
            -
                        new  | 
| 47 | 
            -
                        new  | 
| 46 | 
            +
                        new CodeBlockExtension_1.CodeBlockExtension({ enableDecorations }),
         | 
| 47 | 
            +
                        new PreformattedExtension_1.PreformattedExtension({ enableDecorations }),
         | 
| 48 48 | 
             
                        new extensions_1.UnderlineExtension(),
         | 
| 49 49 | 
             
                        new extensions_1.HistoryExtension(),
         | 
| 50 50 | 
             
                        new ImageExtension_1.ImageExtension(),
         | 
| @@ -1,10 +1,13 @@ | |
| 1 1 | 
             
            import { ApplySchemaAttributes, CommandFunction, NodeExtension, NodeExtensionSpec, NodeSpecOverride } from '@remirror/core';
         | 
| 2 2 | 
             
            import { NodeViewMethod } from 'remirror';
         | 
| 3 | 
            -
            export  | 
| 3 | 
            +
            export type PreformattedExtensionOptions = {
         | 
| 4 | 
            +
                enableDecorations?: boolean;
         | 
| 5 | 
            +
            };
         | 
| 6 | 
            +
            export declare class PreformattedExtension extends NodeExtension<PreformattedExtensionOptions> {
         | 
| 4 7 | 
             
                get name(): "preformatted";
         | 
| 5 8 | 
             
                createTags(): ("block" | "formattingNode" | "textBlock")[];
         | 
| 6 9 | 
             
                createNodeSpec(extra: ApplySchemaAttributes, override: NodeSpecOverride): NodeExtensionSpec;
         | 
| 7 | 
            -
                createNodeViews(): NodeViewMethod | 
| 10 | 
            +
                createNodeViews(): NodeViewMethod | Record<string, never>;
         | 
| 8 11 | 
             
                /**
         | 
| 9 12 | 
             
                 * Toggle the <pre> for the current block.
         | 
| 10 13 | 
             
                 */
         | 
| @@ -35,6 +35,9 @@ let PreformattedExtension = class PreformattedExtension extends core_1.NodeExten | |
| 35 35 | 
             
                    };
         | 
| 36 36 | 
             
                }
         | 
| 37 37 | 
             
                createNodeViews() {
         | 
| 38 | 
            +
                    if (!this.options.enableDecorations) {
         | 
| 39 | 
            +
                        return {};
         | 
| 40 | 
            +
                    }
         | 
| 38 41 | 
             
                    return (node) => {
         | 
| 39 42 | 
             
                        const { nodeTextAlignment } = node.attrs;
         | 
| 40 43 | 
             
                        // This is the pre container for the code block
         | 
| @@ -71,6 +74,10 @@ __decorate([ | |
| 71 74 | 
             
                (0, core_1.command)()
         | 
| 72 75 | 
             
            ], PreformattedExtension.prototype, "togglePreformatted", null);
         | 
| 73 76 | 
             
            PreformattedExtension = __decorate([
         | 
| 74 | 
            -
                (0, core_1.extension)({ | 
| 77 | 
            +
                (0, core_1.extension)({
         | 
| 78 | 
            +
                    defaultOptions: {
         | 
| 79 | 
            +
                        enableDecorations: false,
         | 
| 80 | 
            +
                    },
         | 
| 81 | 
            +
                })
         | 
| 75 82 | 
             
            ], PreformattedExtension);
         | 
| 76 83 | 
             
            exports.PreformattedExtension = PreformattedExtension;
         | 
    
        package/lib/hooks/index.d.ts
    CHANGED
    
    | @@ -1,2 +1,3 @@ | |
| 1 | 
            -
            export * from './useExtensionNames';
         | 
| 2 | 
            -
            export * from './useExpandedSelection';
         | 
| 1 | 
            +
            export * from './useExtensionNames/useExtensionNames';
         | 
| 2 | 
            +
            export * from './useExpandedSelection/useExpandedSelection';
         | 
| 3 | 
            +
            export * from './useFocus/useFocus';
         | 
    
        package/lib/hooks/index.js
    CHANGED
    
    | @@ -14,5 +14,6 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) { | |
| 14 14 | 
             
                for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
         | 
| 15 15 | 
             
            };
         | 
| 16 16 | 
             
            Object.defineProperty(exports, "__esModule", { value: true });
         | 
| 17 | 
            -
            __exportStar(require("./useExtensionNames"), exports);
         | 
| 18 | 
            -
            __exportStar(require("./useExpandedSelection"), exports);
         | 
| 17 | 
            +
            __exportStar(require("./useExtensionNames/useExtensionNames"), exports);
         | 
| 18 | 
            +
            __exportStar(require("./useExpandedSelection/useExpandedSelection"), exports);
         | 
| 19 | 
            +
            __exportStar(require("./useFocus/useFocus"), exports);
         | 
| @@ -0,0 +1,6 @@ | |
| 1 | 
            +
            import { FocusEvent as ReactFocusEvent, FocusEventHandler } from 'react';
         | 
| 2 | 
            +
            export declare const useFocus: (initialState: boolean, isChildElement: (element: Element) => boolean) => {
         | 
| 3 | 
            +
                handleFocus: (event: ReactFocusEvent) => void;
         | 
| 4 | 
            +
                handleBlur: FocusEventHandler<HTMLDivElement>;
         | 
| 5 | 
            +
                isFocused: boolean;
         | 
| 6 | 
            +
            };
         | 
| @@ -1,16 +1,23 @@ | |
| 1 1 | 
             
            "use strict";
         | 
| 2 2 | 
             
            Object.defineProperty(exports, "__esModule", { value: true });
         | 
| 3 | 
            +
            exports.useFocus = void 0;
         | 
| 3 4 | 
             
            const react_1 = require("react");
         | 
| 4 | 
            -
            const useFocus = (initialState) => {
         | 
| 5 | 
            -
                const  | 
| 6 | 
            -
                const  | 
| 5 | 
            +
            const useFocus = (initialState, isChildElement) => {
         | 
| 6 | 
            +
                const [isFocused, setIsFocused] = (0, react_1.useState)(initialState);
         | 
| 7 | 
            +
                const getFocusedElement = (0, react_1.useCallback)(() => {
         | 
| 8 | 
            +
                    let element = document.activeElement;
         | 
| 9 | 
            +
                    while (element instanceof HTMLIFrameElement) {
         | 
| 10 | 
            +
                        element = element.contentDocument?.activeElement || null;
         | 
| 11 | 
            +
                    }
         | 
| 12 | 
            +
                    return element?.parentElement ? element : null;
         | 
| 13 | 
            +
                }, []);
         | 
| 7 14 | 
             
                const handleFocus = (0, react_1.useCallback)((event) => {
         | 
| 8 15 | 
             
                    // Ignore elements flagged to be ignored, this allows us to add extra, clickable, elements
         | 
| 9 16 | 
             
                    // without triggering a focus, such as action menus.
         | 
| 10 17 | 
             
                    if (!event.target?.classList?.contains('fte-ignore') && !event.target?.closest('.fte-ignore')) {
         | 
| 11 | 
            -
                         | 
| 18 | 
            +
                        setIsFocused(true);
         | 
| 12 19 | 
             
                    }
         | 
| 13 | 
            -
                }, [ | 
| 20 | 
            +
                }, []);
         | 
| 14 21 | 
             
                const handleBlur = (0, react_1.useCallback)((event) => {
         | 
| 15 22 | 
             
                    // React event bubbling is interesting, it bubbles up the React tree rather than the DOM tree.
         | 
| 16 23 | 
             
                    // The tree deviates when rendering portals (eg. for modals).
         | 
| @@ -30,14 +37,21 @@ const useFocus = (initialState) => { | |
| 30 37 | 
             
                    //
         | 
| 31 38 | 
             
                    // An assumption here is that anything in a portal will only blur to another element that is also in the portal
         | 
| 32 39 | 
             
                    // (and therefore still in our React tree resulting in the element still effectively being focused).
         | 
| 33 | 
            -
                     | 
| 34 | 
            -
                     | 
| 35 | 
            -
             | 
| 36 | 
            -
             | 
| 37 | 
            -
             | 
| 38 | 
            -
                         | 
| 39 | 
            -
             | 
| 40 | 
            -
             | 
| 41 | 
            -
             | 
| 40 | 
            +
                    // TODO: PLATFORM-1611 this shit still doesn't work properly, notably issues with the link/image modals.
         | 
| 41 | 
            +
                    requestAnimationFrame(() => {
         | 
| 42 | 
            +
                        // "relatedTarget" in the event object will be null if the element gaining focus is in a different
         | 
| 43 | 
            +
                        // document to the element being blurred (eg. floating toolbar rendered in top level frame,
         | 
| 44 | 
            +
                        // editor rendered in iframe). instead grab the active element to determine current focus.
         | 
| 45 | 
            +
                        const isBlurringEditor = isChildElement(event.target);
         | 
| 46 | 
            +
                        const focusedElement = event.relatedTarget || getFocusedElement();
         | 
| 47 | 
            +
                        const isFocusedInEditor = focusedElement && isChildElement(focusedElement);
         | 
| 48 | 
            +
                        // Detect if the blur event happens when the related/clicked target is the floating popover
         | 
| 49 | 
            +
                        const isClickingFloatingToolbar = !!focusedElement?.closest('.squiz-fte-scope__floating-popover');
         | 
| 50 | 
            +
                        if (isBlurringEditor && !isFocusedInEditor && !isClickingFloatingToolbar) {
         | 
| 51 | 
            +
                            setIsFocused(false);
         | 
| 52 | 
            +
                        }
         | 
| 53 | 
            +
                    });
         | 
| 54 | 
            +
                }, [getFocusedElement, isChildElement]);
         | 
| 55 | 
            +
                return { handleFocus, handleBlur, isFocused };
         | 
| 42 56 | 
             
            };
         | 
| 43 | 
            -
            exports. | 
| 57 | 
            +
            exports.useFocus = useFocus;
         | 
    
        package/lib/index.css
    CHANGED
    
    | @@ -5145,6 +5145,11 @@ | |
| 5145 5145 | 
             
              border-width: 2px;
         | 
| 5146 5146 | 
             
              border-style: solid;
         | 
| 5147 5147 | 
             
            }
         | 
| 5148 | 
            +
            .squiz-fte-scope.squiz-fte-scope__editor--empty {
         | 
| 5149 | 
            +
              height: 0px;
         | 
| 5150 | 
            +
              width: 0px;
         | 
| 5151 | 
            +
              border-width: 0px;
         | 
| 5152 | 
            +
            }
         | 
| 5148 5153 | 
             
            .squiz-fte-scope.squiz-fte-scope__editor--is-disabled {
         | 
| 5149 5154 | 
             
              cursor: not-allowed;
         | 
| 5150 5155 | 
             
              border-width: 0px;
         | 
| @@ -5511,11 +5516,11 @@ | |
| 5511 5516 | 
             
              padding: 6px 8px;
         | 
| 5512 5517 | 
             
            }
         | 
| 5513 5518 | 
             
            .squiz-fte-scope .header-toolbar {
         | 
| 5519 | 
            +
              opacity: 0;
         | 
| 5520 | 
            +
              max-height: 0;
         | 
| 5514 5521 | 
             
              transition-duration: 0.3s;
         | 
| 5515 5522 | 
             
              transition-property: max-height, opacity;
         | 
| 5516 5523 | 
             
              transition-timing-function: ease-out;
         | 
| 5517 | 
            -
              opacity: 0;
         | 
| 5518 | 
            -
              max-height: 0;
         | 
| 5519 5524 | 
             
            }
         | 
| 5520 5525 | 
             
            .squiz-fte-scope .header-toolbar.show-toolbar {
         | 
| 5521 5526 | 
             
              opacity: 1;
         | 
| @@ -0,0 +1,49 @@ | |
| 1 | 
            +
            "use strict";
         | 
| 2 | 
            +
            var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
         | 
| 3 | 
            +
                if (k2 === undefined) k2 = k;
         | 
| 4 | 
            +
                var desc = Object.getOwnPropertyDescriptor(m, k);
         | 
| 5 | 
            +
                if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
         | 
| 6 | 
            +
                  desc = { enumerable: true, get: function() { return m[k]; } };
         | 
| 7 | 
            +
                }
         | 
| 8 | 
            +
                Object.defineProperty(o, k2, desc);
         | 
| 9 | 
            +
            }) : (function(o, m, k, k2) {
         | 
| 10 | 
            +
                if (k2 === undefined) k2 = k;
         | 
| 11 | 
            +
                o[k2] = m[k];
         | 
| 12 | 
            +
            }));
         | 
| 13 | 
            +
            var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
         | 
| 14 | 
            +
                Object.defineProperty(o, "default", { enumerable: true, value: v });
         | 
| 15 | 
            +
            }) : function(o, v) {
         | 
| 16 | 
            +
                o["default"] = v;
         | 
| 17 | 
            +
            });
         | 
| 18 | 
            +
            var __importStar = (this && this.__importStar) || function (mod) {
         | 
| 19 | 
            +
                if (mod && mod.__esModule) return mod;
         | 
| 20 | 
            +
                var result = {};
         | 
| 21 | 
            +
                if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
         | 
| 22 | 
            +
                __setModuleDefault(result, mod);
         | 
| 23 | 
            +
                return result;
         | 
| 24 | 
            +
            };
         | 
| 25 | 
            +
            Object.defineProperty(exports, "__esModule", { value: true });
         | 
| 26 | 
            +
            exports.EditorInput = void 0;
         | 
| 27 | 
            +
            const react_1 = require("@remirror/react");
         | 
| 28 | 
            +
            const react_2 = __importStar(require("react"));
         | 
| 29 | 
            +
            const react_dom_1 = require("react-dom");
         | 
| 30 | 
            +
            const EditorInput = ({ container, ...other }) => {
         | 
| 31 | 
            +
                const { getRootProps } = (0, react_1.useRemirrorContext)();
         | 
| 32 | 
            +
                const { key, ...rootProps } = getRootProps();
         | 
| 33 | 
            +
                const preventImagePaste = (0, react_2.useCallback)((event) => {
         | 
| 34 | 
            +
                    const { clipboardData } = event;
         | 
| 35 | 
            +
                    const pastedData = clipboardData?.files[0];
         | 
| 36 | 
            +
                    if (pastedData?.type &&
         | 
| 37 | 
            +
                        pastedData?.type.startsWith('image/') &&
         | 
| 38 | 
            +
                        // Still allow paste of any text that came through (Word, etc)
         | 
| 39 | 
            +
                        !clipboardData?.types.includes('text/plain')) {
         | 
| 40 | 
            +
                        event.preventDefault();
         | 
| 41 | 
            +
                    }
         | 
| 42 | 
            +
                    // Allow other paste event handlers to be run.
         | 
| 43 | 
            +
                    return false;
         | 
| 44 | 
            +
                }, []);
         | 
| 45 | 
            +
                const input = react_2.default.createElement("div", { key: key, ...rootProps, ...other });
         | 
| 46 | 
            +
                (0, react_1.useEditorEvent)('paste', preventImagePaste);
         | 
| 47 | 
            +
                return container ? (0, react_dom_1.createPortal)(input, container) : input;
         | 
| 48 | 
            +
            };
         | 
| 49 | 
            +
            exports.EditorInput = EditorInput;
         | 
| @@ -8,9 +8,6 @@ const resolveNodeTag = (node) => { | |
| 8 8 | 
             
                if (node.type.name === Extensions_1.NodeName.Text) {
         | 
| 9 9 | 
             
                    return 'span';
         | 
| 10 10 | 
             
                }
         | 
| 11 | 
            -
                if (node.type.name === Extensions_1.NodeName.CodeBlock) {
         | 
| 12 | 
            -
                    return 'code';
         | 
| 13 | 
            -
                }
         | 
| 14 11 | 
             
                if (node.type.spec?.toDOM) {
         | 
| 15 12 | 
             
                    const domNode = node.type.spec.toDOM(node);
         | 
| 16 13 | 
             
                    if (domNode instanceof window.Node) {
         | 
| @@ -75,12 +75,6 @@ const getNodeAttributes = (node) => { | |
| 75 75 | 
             
                        title: node.attributes?.title,
         | 
| 76 76 | 
             
                    };
         | 
| 77 77 | 
             
                }
         | 
| 78 | 
            -
                else if (node.type === 'tag' && node.tag === 'code') {
         | 
| 79 | 
            -
                    return {
         | 
| 80 | 
            -
                        language: node.attributes?.language || 'markup',
         | 
| 81 | 
            -
                        wrap: node.attributes?.wrap || true,
         | 
| 82 | 
            -
                    };
         | 
| 83 | 
            -
                }
         | 
| 84 78 | 
             
                else if (node.type === 'matrix-image') {
         | 
| 85 79 | 
             
                    return {
         | 
| 86 80 | 
             
                        matrixAssetId: node.matrixAssetId,
         | 
    
        package/package.json
    CHANGED
    
    
| @@ -5,7 +5,12 @@ import { MatrixResourceBrowserPluginProps } from '@squiz/matrix-resource-browser | |
| 5 5 | 
             
            import Editor from './Editor';
         | 
| 6 6 | 
             
            import { renderWithEditor, mockResourceBrowserContext } from '../../tests';
         | 
| 7 7 | 
             
            import ImageButton from '../EditorToolbar/Tools/Image/ImageButton';
         | 
| 8 | 
            -
            import * as  | 
| 8 | 
            +
            import * as hooks from '../hooks';
         | 
| 9 | 
            +
             | 
| 10 | 
            +
            jest.mock('../hooks', () => ({
         | 
| 11 | 
            +
              __esModule: true,
         | 
| 12 | 
            +
              ...jest.requireActual('../hooks'),
         | 
| 13 | 
            +
            }));
         | 
| 9 14 |  | 
| 10 15 | 
             
            const handleFocusMock = jest.fn();
         | 
| 11 16 | 
             
            const handleBlurMock = jest.fn();
         | 
| @@ -341,11 +346,10 @@ describe('Formatted text editor', () => { | |
| 341 346 | 
             
              });
         | 
| 342 347 |  | 
| 343 348 | 
             
              it('triggers handleFocus when editor is focused', async () => {
         | 
| 344 | 
            -
                jest.spyOn( | 
| 345 | 
            -
                   | 
| 349 | 
            +
                jest.spyOn(hooks, 'useFocus').mockReturnValue({
         | 
| 350 | 
            +
                  isFocused: false,
         | 
| 346 351 | 
             
                  handleFocus: handleFocusMock,
         | 
| 347 352 | 
             
                  handleBlur: handleBlurMock,
         | 
| 348 | 
            -
                  wrapperRef: { current: null },
         | 
| 349 353 | 
             
                });
         | 
| 350 354 |  | 
| 351 355 | 
             
                const { getByLabelText } = render(<Editor />);
         | 
| @@ -358,11 +362,10 @@ describe('Formatted text editor', () => { | |
| 358 362 | 
             
              });
         | 
| 359 363 |  | 
| 360 364 | 
             
              it('triggers handleBlur when editor is blurred', () => {
         | 
| 361 | 
            -
                jest.spyOn( | 
| 362 | 
            -
                   | 
| 365 | 
            +
                jest.spyOn(hooks, 'useFocus').mockReturnValue({
         | 
| 366 | 
            +
                  isFocused: false,
         | 
| 363 367 | 
             
                  handleFocus: handleFocusMock,
         | 
| 364 368 | 
             
                  handleBlur: handleBlurMock,
         | 
| 365 | 
            -
                  wrapperRef: { current: null },
         | 
| 366 369 | 
             
                });
         | 
| 367 370 |  | 
| 368 371 | 
             
                const { getByLabelText } = render(<Editor />);
         | 
| @@ -375,11 +378,10 @@ describe('Formatted text editor', () => { | |
| 375 378 | 
             
              });
         | 
| 376 379 |  | 
| 377 380 | 
             
              it('should apply hide class when focus hook returns false', () => {
         | 
| 378 | 
            -
                jest.spyOn( | 
| 379 | 
            -
                   | 
| 381 | 
            +
                jest.spyOn(hooks, 'useFocus').mockReturnValue({
         | 
| 382 | 
            +
                  isFocused: true,
         | 
| 380 383 | 
             
                  handleFocus: handleFocusMock,
         | 
| 381 384 | 
             
                  handleBlur: handleBlurMock,
         | 
| 382 | 
            -
                  wrapperRef: { current: null },
         | 
| 383 385 | 
             
                });
         | 
| 384 386 |  | 
| 385 387 | 
             
                const { container } = render(<Editor />);
         | 
| @@ -462,4 +464,27 @@ describe('Formatted text editor', () => { | |
| 462 464 | 
             
                expect(within(document.body).getByRole('button', { name: 'Link (Ctrl+K)', hidden: true })).toBeInTheDocument();
         | 
| 463 465 | 
             
                expect(document.querySelector('.show-toolbar')).toBeInTheDocument();
         | 
| 464 466 | 
             
              });
         | 
| 467 | 
            +
             | 
| 468 | 
            +
              it('Renders the input and toolbar in container elements if provided', () => {
         | 
| 469 | 
            +
                const toolbar = document.createElement('div');
         | 
| 470 | 
            +
                const input = document.createElement('div');
         | 
| 471 | 
            +
                document.body.append(toolbar, input);
         | 
| 472 | 
            +
             | 
| 473 | 
            +
                const { container, unmount } = render(<Editor containers={{ toolbar, input }} />);
         | 
| 474 | 
            +
             | 
| 475 | 
            +
                // Toolbar and input should be rendered in the provided containers.
         | 
| 476 | 
            +
                expect(within(input).getByRole('textbox')).toBeInTheDocument();
         | 
| 477 | 
            +
                expect(within(toolbar).getByRole('button', { name: 'Bold (Ctrl+B)' })).toBeInTheDocument();
         | 
| 478 | 
            +
             | 
| 479 | 
            +
                // They should not be rendered inside of the editor element.
         | 
| 480 | 
            +
                expect(within(container).queryByRole('textbox')).not.toBeInTheDocument();
         | 
| 481 | 
            +
                expect(within(container).queryByRole('button', { name: 'Bold (Ctrl+B)' })).not.toBeInTheDocument();
         | 
| 482 | 
            +
                expect(container.querySelector('.squiz-fte-scope__editor--empty')).toBeInTheDocument();
         | 
| 483 | 
            +
             | 
| 484 | 
            +
                unmount();
         | 
| 485 | 
            +
             | 
| 486 | 
            +
                // Toolbar and input should be removed on unmount.
         | 
| 487 | 
            +
                expect(within(input).queryByRole('textbox')).not.toBeInTheDocument();
         | 
| 488 | 
            +
                expect(within(toolbar).queryByRole('button', { name: 'Bold (Ctrl+B)' })).not.toBeInTheDocument();
         | 
| 489 | 
            +
              });
         | 
| 465 490 | 
             
            });
         | 
    
        package/src/Editor/Editor.tsx
    CHANGED
    
    | @@ -1,61 +1,49 @@ | |
| 1 | 
            -
            import React, { useContext, useCallback, ReactNode, useEffect } from 'react';
         | 
| 2 | 
            -
            import {  | 
| 1 | 
            +
            import React, { useContext, useCallback, ReactNode, useEffect, useRef } from 'react';
         | 
| 2 | 
            +
            import { Remirror, useRemirror } from '@remirror/react';
         | 
| 3 3 | 
             
            import { RemirrorContentType, RemirrorEventListener, Extension } from '@remirror/core';
         | 
| 4 | 
            -
            import { ClipboardEventHandler } from '@remirror/extension-events/dist-types/events-extension';
         | 
| 5 4 | 
             
            import clsx from 'clsx';
         | 
| 6 5 | 
             
            import { Toolbar, FloatingToolbar } from '../EditorToolbar';
         | 
| 7 6 | 
             
            import { EditorContext } from './EditorContext';
         | 
| 8 7 | 
             
            import { createExtensions } from '../Extensions/Extensions';
         | 
| 9 | 
            -
            import useFocus from '../hooks | 
| 8 | 
            +
            import { useFocus } from '../hooks';
         | 
| 10 9 | 
             
            import { TableComponents } from '@remirror/extension-react-tables';
         | 
| 10 | 
            +
            import { EditorInput } from '../ui/EditorInput/EditorInput';
         | 
| 11 11 |  | 
| 12 12 | 
             
            type EditorProps = {
         | 
| 13 | 
            +
              attributes?: Record<string, string>;
         | 
| 14 | 
            +
              border?: boolean;
         | 
| 15 | 
            +
              children?: ReactNode;
         | 
| 13 16 | 
             
              className?: string;
         | 
| 17 | 
            +
              containers?: {
         | 
| 18 | 
            +
                input?: Element | null;
         | 
| 19 | 
            +
                toolbar?: Element | null;
         | 
| 20 | 
            +
              };
         | 
| 14 21 | 
             
              content?: RemirrorContentType;
         | 
| 15 | 
            -
              onChange?: RemirrorEventListener<Extension>;
         | 
| 16 22 | 
             
              editable?: boolean;
         | 
| 17 | 
            -
               | 
| 18 | 
            -
               | 
| 23 | 
            +
              enableDecorations?: boolean;
         | 
| 24 | 
            +
              enableTableTool?: boolean;
         | 
| 19 25 | 
             
              isFocused?: boolean;
         | 
| 20 26 | 
             
              label?: string;
         | 
| 21 | 
            -
               | 
| 22 | 
            -
              enableTableTool?: boolean;
         | 
| 23 | 
            -
            };
         | 
| 24 | 
            -
             | 
| 25 | 
            -
            const WrappedEditor = () => {
         | 
| 26 | 
            -
              const preventImagePaste = useCallback((event) => {
         | 
| 27 | 
            -
                const { clipboardData } = event;
         | 
| 28 | 
            -
                const pastedData = clipboardData?.files[0];
         | 
| 29 | 
            -
                if (
         | 
| 30 | 
            -
                  pastedData?.type &&
         | 
| 31 | 
            -
                  pastedData?.type.startsWith('image/') &&
         | 
| 32 | 
            -
                  // Still allow paste of any text that came through (Word, etc)
         | 
| 33 | 
            -
                  !clipboardData?.types.includes('text/plain')
         | 
| 34 | 
            -
                ) {
         | 
| 35 | 
            -
                  event.preventDefault();
         | 
| 36 | 
            -
                }
         | 
| 37 | 
            -
             | 
| 38 | 
            -
                // Allow other paste event handlers to be run.
         | 
| 39 | 
            -
                return false;
         | 
| 40 | 
            -
              }, []) as ClipboardEventHandler;
         | 
| 41 | 
            -
             | 
| 42 | 
            -
              useEditorEvent('paste', preventImagePaste);
         | 
| 43 | 
            -
              return <EditorComponent />;
         | 
| 27 | 
            +
              onChange?: RemirrorEventListener<Extension>;
         | 
| 44 28 | 
             
            };
         | 
| 45 29 |  | 
| 46 30 | 
             
            const Editor = ({
         | 
| 47 | 
            -
               | 
| 48 | 
            -
              className,
         | 
| 31 | 
            +
              attributes,
         | 
| 49 32 | 
             
              border = true,
         | 
| 50 | 
            -
              editable = true,
         | 
| 51 | 
            -
              onChange,
         | 
| 52 33 | 
             
              children,
         | 
| 53 | 
            -
               | 
| 54 | 
            -
               | 
| 34 | 
            +
              className,
         | 
| 35 | 
            +
              containers,
         | 
| 36 | 
            +
              content,
         | 
| 37 | 
            +
              editable = true,
         | 
| 38 | 
            +
              enableDecorations = true,
         | 
| 55 39 | 
             
              enableTableTool = false,
         | 
| 40 | 
            +
              isFocused: isInitiallyFocused = false,
         | 
| 41 | 
            +
              label = 'Text editor',
         | 
| 42 | 
            +
              onChange,
         | 
| 56 43 | 
             
            }: EditorProps) => {
         | 
| 44 | 
            +
              const isEmpty = containers?.toolbar && containers.input && !children;
         | 
| 57 45 | 
             
              const { manager, state, setState } = useRemirror({
         | 
| 58 | 
            -
                extensions: createExtensions(useContext(EditorContext)),
         | 
| 46 | 
            +
                extensions: createExtensions(useContext(EditorContext), enableDecorations),
         | 
| 59 47 | 
             
                content,
         | 
| 60 48 | 
             
                selection: 'start',
         | 
| 61 49 | 
             
                stringHandler: 'html',
         | 
| @@ -66,11 +54,24 @@ const Editor = ({ | |
| 66 54 | 
             
                onChange?.(parameter);
         | 
| 67 55 | 
             
              };
         | 
| 68 56 |  | 
| 69 | 
            -
              const  | 
| 57 | 
            +
              const wrapperRef = useRef<HTMLDivElement>(null);
         | 
| 58 | 
            +
              const { isFocused, handleFocus, handleBlur } = useFocus(
         | 
| 59 | 
            +
                isInitiallyFocused,
         | 
| 60 | 
            +
                useCallback(
         | 
| 61 | 
            +
                  (element: Node) => {
         | 
| 62 | 
            +
                    return Boolean(
         | 
| 63 | 
            +
                      wrapperRef.current?.contains(element) ||
         | 
| 64 | 
            +
                        containers?.input?.contains(element) ||
         | 
| 65 | 
            +
                        containers?.toolbar?.contains(element),
         | 
| 66 | 
            +
                    );
         | 
| 67 | 
            +
                  },
         | 
| 68 | 
            +
                  [containers?.input, containers?.toolbar],
         | 
| 69 | 
            +
                ),
         | 
| 70 | 
            +
              );
         | 
| 70 71 |  | 
| 71 72 | 
             
              // On initial load, check if we need to focus the actual text content
         | 
| 72 73 | 
             
              useEffect(() => {
         | 
| 73 | 
            -
                if ( | 
| 74 | 
            +
                if (isInitiallyFocused) {
         | 
| 74 75 | 
             
                  manager.view.dom.focus();
         | 
| 75 76 | 
             
                }
         | 
| 76 77 |  | 
| @@ -85,13 +86,14 @@ const Editor = ({ | |
| 85 86 | 
             
              return (
         | 
| 86 87 | 
             
                <div
         | 
| 87 88 | 
             
                  ref={wrapperRef}
         | 
| 88 | 
            -
                   | 
| 89 | 
            +
                  onBlurCapture={handleBlur}
         | 
| 89 90 | 
             
                  onFocusCapture={handleFocus}
         | 
| 90 91 | 
             
                  className={clsx(
         | 
| 91 92 | 
             
                    'squiz-fte-scope',
         | 
| 92 93 | 
             
                    'squiz-fte-scope__editor',
         | 
| 93 94 | 
             
                    !editable && 'squiz-fte-scope__editor--is-disabled',
         | 
| 94 95 | 
             
                    border && 'squiz-fte-scope__editor--bordered',
         | 
| 96 | 
            +
                    isEmpty && 'squiz-fte-scope__editor--empty',
         | 
| 95 97 | 
             
                    className,
         | 
| 96 98 | 
             
                  )}
         | 
| 97 99 | 
             
                >
         | 
| @@ -101,14 +103,16 @@ const Editor = ({ | |
| 101 103 | 
             
                    editable={editable}
         | 
| 102 104 | 
             
                    onChange={handleChange}
         | 
| 103 105 | 
             
                    placeholder="Write something"
         | 
| 104 | 
            -
                    label= | 
| 106 | 
            +
                    label={label}
         | 
| 105 107 | 
             
                    attributes={attributes}
         | 
| 106 108 | 
             
                  >
         | 
| 107 | 
            -
                    {editable &&  | 
| 109 | 
            +
                    {editable && (
         | 
| 110 | 
            +
                      <Toolbar isVisible={isFocused} enableTableTool={enableTableTool} container={containers?.toolbar} />
         | 
| 111 | 
            +
                    )}
         | 
| 108 112 | 
             
                    {children && <div className="squiz-fte-scope__editor__children">{children}</div>}
         | 
| 109 | 
            -
                    < | 
| 113 | 
            +
                    <EditorInput container={containers?.input} />
         | 
| 110 114 | 
             
                    {enableTableTool && <TableComponents enableTableCellMenu={false} />}
         | 
| 111 | 
            -
                    {editable &&  | 
| 115 | 
            +
                    {editable && isFocused && <FloatingToolbar />}
         | 
| 112 116 | 
             
                  </Remirror>
         | 
| 113 117 | 
             
                </div>
         | 
| 114 118 | 
             
              );
         | 
    
        package/src/Editor/_editor.scss
    CHANGED
    
    
| @@ -17,17 +17,19 @@ import HorizontalLineButton from './Tools/HorizontalLine/HorizontalLineButton'; | |
| 17 17 | 
             
            import TableButton from './Tools/Table/TableButton';
         | 
| 18 18 | 
             
            import ContentToolsDropdown from './Tools/ContentTools/ContentToolsDropdown';
         | 
| 19 19 | 
             
            import { useExtensionNames } from '../hooks';
         | 
| 20 | 
            +
            import { createPortal } from 'react-dom';
         | 
| 20 21 |  | 
| 21 22 | 
             
            type ToolbarProps = {
         | 
| 22 23 | 
             
              isVisible: boolean;
         | 
| 23 24 | 
             
              enableTableTool: boolean;
         | 
| 25 | 
            +
              container?: Element | null;
         | 
| 24 26 | 
             
            };
         | 
| 25 | 
            -
            export const Toolbar = ({ isVisible, enableTableTool }: ToolbarProps) => {
         | 
| 26 | 
            -
              const extensionNames = useExtensionNames();
         | 
| 27 27 |  | 
| 28 | 
            -
             | 
| 28 | 
            +
            export const Toolbar = ({ isVisible, enableTableTool, container }: ToolbarProps) => {
         | 
| 29 | 
            +
              const extensionNames = useExtensionNames();
         | 
| 30 | 
            +
              const toolbar = (
         | 
| 29 31 | 
             
                <RemirrorToolbar
         | 
| 30 | 
            -
                  className={clsx('editor-toolbar header-toolbar', isVisible && 'show-toolbar')}
         | 
| 32 | 
            +
                  className={clsx('editor-toolbar header-toolbar', isVisible && 'show-toolbar', container && 'fte-portal-toolbar')}
         | 
| 31 33 | 
             
                  role="toolbar"
         | 
| 32 34 | 
             
                  tabIndex={0}
         | 
| 33 35 | 
             
                >
         | 
| @@ -59,4 +61,6 @@ export const Toolbar = ({ isVisible, enableTableTool }: ToolbarProps) => { | |
| 59 61 | 
             
                  </div>
         | 
| 60 62 | 
             
                </RemirrorToolbar>
         | 
| 61 63 | 
             
              );
         | 
| 64 | 
            +
             | 
| 65 | 
            +
              return container ? createPortal(toolbar, container) : toolbar;
         | 
| 62 66 | 
             
            };
         |