Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom default paragraph is adding 2 paragraphs when you press the 'Add' button if it's the last block #2770

Open
paul-cofnod opened this issue Jul 10, 2024 · 0 comments

Comments

@paul-cofnod
Copy link

I'm not sure if this is a bug as it doesn't happen when you use the default paragraph. So I've added this here as I suspect it's my code causing the problem.

I've created a bilingual paragraph plugin as my editor has to support 2 languages. Most things seem to be working quite well (I'm fairly sure I haven't done a brilliant job of it but hey!). The only issue I have is when you press the plus ('Add') button and add a new Text block, and ONLY when you press this on the last block in the editor. This adds 2 new paragraphs when it adds 1 at all other times. If you press it on a block in the middle this works correctly and only adds one block

I have quite a lot of code... but I was wondering if anyone can spot anything obvious that might be causing this?

My editor is initiated like this

let editorData = $(`#${editor.id}`).find(".editordata").data("json");

pageEditorHolder[editor.id] = new EditorJS({
    // Id of Element that should contain Editor instance
    holder: editor.id,
    tools: {
        header: {
            class: HeaderBilingual,
            inlineToolbar: true
        },
        paragraph: {
            class: TextBilingual,
            inlineToolbar: true
        },
        image: {
            class: InlineImage,
            inlineToolbar: true
        }
    },
    defaultBlock: 'paragraph',
    data: editorData,
    minHeight: 30,

    // turn this on and off based on Auth
    readOnly: false,

    i18n: i18n,

    onReady: () => {
        editorsReady++;
        if (editorsReady == pageEditors.length) {
            //console.log("All editors loaded");
            editorsLoaded();
        }
    },
    onChange: (api, event) => {
        _enableSavebutton();
    }
});

I have a base class for my blocks with some standard methods for building a bilingual editor

// import utilities
import * as utilities from '././utils/utilities.js';
export default class EditorBlock {
    constructor({ data, api, config, readOnly, block }) {
        this.block = block;
        this.data = {};
        this.showControls = false;
        this.wrapper = undefined;
        this.blockName = { 'en-GB': 'Block settings', 'cy-GB': 'Gosodiadau bloc' };
        this.settings = [];
        this.controls = [];
        this.controlsHolder = [];
        this.available = false;
        this.id = block.id;
        this.api = api;
    }

    save(blockContent) {
        return this.data;
    }

    moved() {
        this._enableSavebutton();
    }

    rendered() {
        // create select 2 now they are added to the page
        this.selectBoxes.forEach((selectBox) => {
            $("#" + selectBox.id).select2();
            // add event listeners to update content on screen 
            $("#" + selectBox.id).on("input", (e) => {
                //console.log("update block", this, selectBox.value);
                this._updateBlock(selectBox.target, $("#" + selectBox.id).val());
            });
        });
        this.available = true;
    }

    renderSettings() {
        return this.settings.map(tune => ({
            icon: tune.icon,
            label: tune.title,
            name: tune.name,
            onActivate: () => this._toggleTune(tune),
            toggle: true,
            isActive: this.data[tune.name],
        }));
    }

    _enableSavebutton() {
        window.unsavedChanges = true;
        window.saveEditorContent.prop('disabled', false);
    }

    _toggleBlockSettings(openOrClose = null, wrapper = this.wrapper) {
        let settingsPanel = wrapper.querySelector(".block-control-container");
        // calculate settingsPanel height
        let panelHeight = (settingsPanel.querySelector(".card-header").clientHeight + settingsPanel.querySelector(".card-body").clientHeight) + "px";
        let parentPanel = document.querySelector('*[data-id="' + this.block.id + '"]');
        // console.log("toggle: ", openOrClose, wrapper, this.block, settingsPanel, panelHeight, parentPanel);
        if (openOrClose == "open") {
            this.showControls = true;
        } else if (openOrClose == "open") {
            this.showControls = false;
        } else {
            this.showControls = !this.showControls;
        }
        if (this.showControls) {
            gsap.to(settingsPanel, { opacity: 1, maxHeight: panelHeight, duration: window.transitionDuration })
            parentPanel.classList.add("controls-open");
            settingsPanel.scrollIntoView({ block: "center" });
            // select first input
            $(wrapper).find('input, [contenteditable], textarea, select').first().focus();
        } else {
            gsap.to(settingsPanel, { opacity: 0, maxHeight: '0px', duration: window.transitionDuration })
            parentPanel.classList.remove("controls-open");
        }
    }

    _updateBlock(target, inputVal, language, wrapper = this.wrapper) {
        // placeholder function - children extend this to update relevant components
        this._updateBlockSettingsSize(wrapper)
    }

    _updateBlockSettingsSize(wrapper = this.wrapper) {
        let settingsPanel = wrapper.querySelector(".block-control-container");
        // calculate settingsPanel height
        let panelHeight = (settingsPanel.querySelector(".card-header").clientHeight + settingsPanel.querySelector(".card-body").clientHeight) + "px";
        let parentPanel = document.querySelector('*[data-id="' + this.block.id + '"]');
        if (this.showControls) {
            gsap.to(settingsPanel, { opacity: 1, maxHeight: panelHeight, duration: window.transitionDuration })
            parentPanel.classList.add("controls-open");
            settingsPanel.scrollIntoView({ block: "center" });
        } else {
            gsap.to(settingsPanel, { opacity: 0, maxHeight: '0px', duration: window.transitionDuration })
            parentPanel.classList.remove("controls-open");
        }
    }

    _isBlockAvailable(parent) {
        console.log("isAvailable: ", parent);
        return new Promise(function (resolve, reject) {
            (function waitForBlock() {
                if (parent.available) return resolve();
                setTimeout(waitForBlock, 30);
            })();
        });
    }

    selectBoxes = [];

    _addControlsToPanel(wrapper = this.wrapper) {
        var parent = this;
        let blockControlCard = document.createElement("div");
        blockControlCard.classList.add("card", "block-control-container", "border", "border-info", "border-2");
        let blockHeader = document.createElement("div");
        blockHeader.classList.add("card-header", "p-3");
        let blockTitle = document.createElement("h4");
        blockTitle.classList.add("mb-0");
        blockTitle.innerHTML = utilities.getLocalizedText(this.blockName, window.currentLanguage);
        let blockClose = document.createElement("a");
        blockClose.innerHTML = '<svg  xmlns="http:https://www.w3.org/2000/svg"  width="24"  height="24"  viewBox="0 0 24 24"  fill="none"  stroke="currentColor"  stroke-width="2"  stroke-linecap="round"  stroke-linejoin="round"  class="icon icon-tabler icons-tabler-outline icon-tabler-x"><path d="M18 6l-12 12" /><path d="M6 6l12 12" /></svg>';
        blockClose.addEventListener("click", (e) => {
            this._toggleBlockSettings();
        });
        blockHeader.appendChild(blockTitle);
        blockHeader.appendChild(blockClose);
        blockControlCard.appendChild(blockHeader);
        let blockControls = document.createElement("div");
        blockControls.classList.add("card-body", "p-3", "gap-2");
        blockControlCard.appendChild(blockControls);
        this.controls.forEach(control => {
            if (control.bilingual) {
                // setup input group
                let InputGroup = document.createElement("div");
                InputGroup.classList.add("row");
                // add label
                let InputLabel = document.createElement("label");
                InputLabel.classList.add("col-sm-2", "col-form-label");
                InputLabel.innerHTML = control.title;
                InputGroup.appendChild(InputLabel);

                window.languages.forEach(language => {
                    // setup floating holder
                    let FloatingHolder = document.createElement("div");
                    FloatingHolder.classList.add("form-floating");
                    // setup input
                    this.controlsHolder[control.name + control.div + language] = document.createElement(control.div);
                    this.controlsHolder[control.name + control.div + language].classList.add("form-control", "mb-2");
                    this.controlsHolder[control.name + control.div + language].placeholder = window.resources[(control.placeholder + language).toLowerCase()];
                    this.controlsHolder[control.name + control.div + language].id = control.name + control.div + language + this.block.id;
                    this.controlsHolder[control.name + control.div + language].innerHTML = this.data && this.data[control.dataMap][language] ? this.data[control.dataMap][language] : "";
                    // enable editor.js inline toolbar
                    this.controlsHolder[control.name + control.div + language].contentEditable = true;

                    FloatingHolder.appendChild(this.controlsHolder[control.name + control.div + language])
                    // add floating label
                    let FloatingLabel = document.createElement("label");
                    FloatingLabel.htmlFor = control.name + control.div + language + this.block.id;
                    FloatingLabel.innerHTML = window.resources[(control.placeholder + language).toLowerCase()];
                    FloatingHolder.appendChild(FloatingLabel);
                    InputGroup.appendChild(FloatingHolder);

                    // add event listeners to update content on screen 
                    this.controlsHolder[control.name + control.div + language].addEventListener("input", (e) => {
                        //console.log("update block", this, this.controlsHolder[control.name + control.div + language].innerHTML)
                        this._updateBlock(control.target, this.controlsHolder[control.name + control.div + language].innerHTML, language)
                    });
                });

                blockControls.appendChild(InputGroup);


            } else {
                switch (control.div) {
                    case "file":
                        // setup input group
                        let FileHolder = document.createElement("div");
                        FileHolder.classList.add("row");
                        // add label
                        let FileHolderLabel = document.createElement("label");
                        FileHolderLabel.classList.add("col-sm-2", "col-form-label");
                        FileHolderLabel.innerHTML = control.title;
                        FileHolder.appendChild(FileHolderLabel);
                        // setup input group
                        let FileInputGroup = document.createElement("div");
                        FileInputGroup.classList.add("input-group");

                        // add media modal button
                        this.controlsHolder[control.name + control.div] = document.createElement("input");
                        this.controlsHolder[control.name + control.div].classList.add("form-control");
                        this.controlsHolder[control.name + control.div].id = control.name + control.div + this.block.id;
                        this.controlsHolder[control.name + control.div].placeholder = window.resources[control.placeholder];
                        this.controlsHolder[control.name + control.div].setAttribute('readonly', true);
                        this.controlsHolder[control.name + control.div].value = this.data && this.data[control.dataMap] ? this.data[control.dataMap] : "";
                        FileInputGroup.appendChild(this.controlsHolder[control.name + control.div]);
                        let ModalButton = document.createElement("button");
                        ModalButton.classList.add("btn", "btn-primary");
                        ModalButton.dataset.bsToggle = "modal";
                        ModalButton.dataset.bsTarget = "#attachmentModal";
                        ModalButton.dataset.currValue = "#" + control.name + control.div + this.block.id;
                        ModalButton.innerHTML = window.resources["select image"];
                        FileInputGroup.appendChild(ModalButton);
                        // on click, set the modal submit button to return the selected value
                        $(ModalButton).on("click", function () {
                            // set selected value (if there is one)
                            let selectedFileName = $($(this).data("currValue")).val().replaceAll("\\", "!!backslash!!");
                            $("#attachmentModalResults").data("selected", selectedFileName);
                            $($(`[data-value="${selectedFileName}"]`)).trigger("click");
                            $("#attachmentSubmitField").val($($(this).data("currValue")).val());
                            // remove listeners on submit button
                            $("#attachmentSubmitButton").off();
                            $("#attachmentSubmitButton").on("click", (event) => {
                                // update the block
                                parent._updateBlock(control.target, $("#attachmentSubmitField").val(), "");
                                // update the input value in the editor block
                                parent.controlsHolder[control.name + control.div].value = $("#attachmentSubmitField").val();
                                // close the modal
                                $('#attachmentModal').modal('hide');
                            });
                        });

                        blockControls.appendChild(FileHolder);
                        FileHolder.appendChild(FileInputGroup);

                        break;
                    case "select":
                        // setup input group
                        let SelectHolder = document.createElement("div");
                        SelectHolder.classList.add("row");
                        // add label
                        let SelectHolderLabel = document.createElement("label");
                        SelectHolderLabel.classList.add("col-sm-2", "col-form-label");
                        SelectHolderLabel.innerHTML = control.title;
                        SelectHolder.appendChild(SelectHolderLabel);
                        // setup input group
                        let SelectGroup = document.createElement("div");
                        SelectGroup.classList.add("form-floating");
                        // setup select
                        this.controlsHolder[control.name + control.div] = document.createElement(control.div);
                        this.controlsHolder[control.name + control.div].classList.add("form-select");
                        this.controlsHolder[control.name + control.div].id = control.name + control.div + this.block.id;
                        this.controlsHolder[control.name + control.div].placeholder = window.resources[control.placeholder];
                        this.controlsHolder[control.name + control.div].value = this.data && this.data[control.dataMap] ? this.data[control.dataMap] : "";
                        // add options
                        for (let step = 0; step < control.options.length; step++) {
                            let option = document.createElement("option");
                            option.value = control.options[step].value;
                            option.innerHTML = control.options[step].name;
                            // check if the value matches
                            if (option.value == this.data[control.dataMap]) {
                                option.selected = true;
                            }
                            this.controlsHolder[control.name + control.div].appendChild(option);
                        }

                        SelectGroup.appendChild(this.controlsHolder[control.name + control.div]);

                        SelectHolder.appendChild(SelectGroup);
                        blockControls.appendChild(SelectHolder);

                        let selectBox = {
                            id: control.name + control.div + this.block.id,
                            target: control.target,
                            value: this.data[control.dataMap]
                        }

                        this.selectBoxes.push(selectBox);
                        
                        break;
                    default:
                        // setup input group
                        let InputHolder = document.createElement("div");
                        InputHolder.classList.add("row");
                        // add label
                        let HolderLabel = document.createElement("label");
                        HolderLabel.classList.add("col-sm-2", "col-form-label");
                        HolderLabel.innerHTML = control.title;
                        InputHolder.appendChild(HolderLabel);
                        // setup input group
                        let InputGroup = document.createElement("div");
                        InputGroup.classList.add("form-floating");
                        // setup input
                        this.controlsHolder[control.name + control.div] = document.createElement(control.div);
                        this.controlsHolder[control.name + control.div].classList.add("form-control");
                        this.controlsHolder[control.name + control.div].id = control.name + control.div + this.block.id;
                        this.controlsHolder[control.name + control.div].placeholder = window.resources[control.placeholder];
                        this.controlsHolder[control.name + control.div].value = this.data && this.data[control.dataMap] ? this.data[control.dataMap] : "";
                        InputGroup.appendChild(this.controlsHolder[control.name + control.div])
                        // add label
                        let InputLabel = document.createElement("label");
                        InputLabel.classList.add("form-label");
                        InputLabel.htmlFor = control.name + control.div + this.block.id;
                        InputLabel.innerHTML = window.resources[control.placeholder];
                        InputGroup.appendChild(InputLabel);
                        InputHolder.appendChild(InputGroup);
                        blockControls.appendChild(InputHolder);

                        break;
                }
                
            }
        });
        wrapper.appendChild(blockControlCard);        
    }
}

and my default bilingual paragraph block is here

import EditorBlock from './EditorBlock.js';
import * as utilities from './utils/utilities.js';
export class TextBilingual extends EditorBlock {
    static get toolbox() {
        return {
            title: 'Text',
            icon: '<svg  xmlns="http:https://www.w3.org/2000/svg"  width="24"  height="24"  viewBox="0 0 24 24"  fill="none"  stroke="currentColor"  stroke-width="2"  stroke-linecap="round"  stroke-linejoin="round"  class="icon icon-tabler icons-tabler-outline icon-tabler-letter-t"><path d="M6 4l12 0" /><path d="M12 4l0 16" /></svg>'
        };
    }
    static get pasteConfig() {
        return {
            tags: ['P'],
        };
    }
    constructor({ data, api, config, readOnly, block }) {
        super({ data, block }); // pull in functions and base settings from ./EditorBlock.js
        this.data = {
            textBilingual: data.textBilingual || { 'en-GB': '', 'cy-GB': '' },
        };
        this.blockName = { 'en-GB': 'Text', 'cy-GB': 'Testun' };
        this.settings = [
            {
                name: 'editContent',
                icon: `<svg  xmlns="http:https://www.w3.org/2000/svg"  width="24"  height="24"  viewBox="0 0 24 24"  fill="none"  stroke="currentColor"  stroke-width="2"  stroke-linecap="round"  stroke-linejoin="round"  class="icon icon-tabler icons-tabler-outline icon-tabler-adjustments"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 10a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" /><path d="M6 4v4" /><path d="M6 12v8" /><path d="M10 16a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" /><path d="M12 4v10" /><path d="M12 18v2" /><path d="M16 7a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" /><path d="M18 4v1" /><path d="M18 9v11" /></svg>`,
                title: window.resources["edit content"]
            }
        ];
        this.controls = [
            {
                name: 'text',
                title: window.resources.text,
                div: 'div',
                bilingual: true,
                placeholder: 'textph',
                dataMap: 'textBilingual',
                target: 'text'
            }
        ];
        this.api = api;
    }

    validate(savedData) {
        if (!savedData.textBilingual["en-GB"].trim() && !savedData.textBilingual["cy-GB"].trim()) {
            return false;
        }

        return true;
    }

    render() {
        let inputData = this.data;
        let settings = this.settings;
        // load wrapper element from html file
        const wrapperConstructor = document.createElement('div');
        wrapperConstructor.classList.add("block-wrapper");

        // pass through functions that call the main components functions after the content has loaded in the JQuery load loop
        let createTextBilingual = (_text) => {
            this._createTextBilingual(_text, wrapperConstructor)
        }

        let acceptTuneView = () => {
            this._acceptTuneView(wrapperConstructor);
        }

        let addControlsToPanel = () => {
            this._addControlsToPanel(wrapperConstructor);
        }

        let toggleBlockSettings = () => {
            this._toggleBlockSettings();
        }

        let handlePaste = (e) => {
            this._handlePaste(e);
        }

        let blankText = this.blankText;

        // sequential method of loading
        $(wrapperConstructor).load("/partials/TextBilingual.html", function () {
            // setup and add controls to adminbar
            addControlsToPanel(wrapperConstructor);

            // apply data
            if (inputData && inputData.textBilingual) {
                createTextBilingual(inputData.textBilingual);
                acceptTuneView();
            }
            // add double click listener (has to be done after html content is loaded)
            wrapperConstructor.querySelector('[data-loader="wrapper"]').addEventListener('dblclick', (event) => {
                toggleBlockSettings();
            });
            // add paste listener to prevent pasted data without tags going straight into the div
            wrapperConstructor.querySelector('[data-loader="wrapper"]').addEventListener('paste', (event) => {
                handlePaste(event);
            });


            // open the settings on a new / empty block
            const textHolder = $(wrapperConstructor).find('*[data-loader="text"]');
            if (textHolder.html() == blankText) {
                toggleBlockSettings("open");
            }
        });

        // add input listener
        wrapperConstructor.addEventListener('keydown', (event) => {
            if (event.keyCode === 13) {
                this._toggleBlockSettings("close", wrapperConstructor);
            }
        });
        

        this.wrapper = wrapperConstructor;

        // return the element to the page
        return this.wrapper;
    }

    rendered() {
        super.rendered();
    }

    /**
   * On paste callback fired from Editor.
   *
   * @param {HTMLPasteEvent} event - event with pasted data
   */
    onPaste(event) {
        //console.log(`onPaste: ${event}`);

        let thisBlock = this;

        this._isBlockAvailable(thisBlock).then(function () {
            thisBlock._updateBlock("text", event.detail.data.innerHTML, window.currentLanguage);
            // now update the block controls too
            // go through the blocks controls and find the one for the current language
            Object.keys(thisBlock.controlsHolder).forEach(function (key) {
                if (key.endsWith(window.currentLanguage)) {
                    // set the innerhtml as this is an editorjs editable div, NOT an input
                    thisBlock.controlsHolder[key].innerHTML = event.detail.data.innerHTML;
                }
            })
        });
    }

    _handlePaste(e) {
        var clipboardData, pastedData;

        // Stop data actually being pasted into div
        e.stopPropagation();
        e.preventDefault();

        // Get pasted data via clipboard API
        clipboardData = e.clipboardData || window.clipboardData;
        pastedData = clipboardData.getData('Text');

        // strip html tags from text
        pastedData = utilities.removeTags(pastedData);

        let enGB = "";
        let cyGB = "";
        if (window.currentLanguage == "en-GB") enGB = pastedData;
        if (window.currentLanguage == "cy-GB") cyGB = pastedData;

        let blockData = {
            textBilingual: { 'en-GB': enGB, 'cy-GB': cyGB },
        };

        console.log(this);

        this.api.blocks.insert("paragraph", blockData);
    }

    /**
   * @private
   * create text
   * @param {string} text
   */
    blankText = '<span class="opacity-50">' + window.resources["nocontent"] + '</span>';

    _createTextBilingual(text, wrapper = this.wrapper) {
        //console.log(`_createTextBilingual: ${this.blockName["en-GB"]}`, this.data, text);
        // locate html placeholders in template
        const contentHolder = $(wrapper).find('*[data-loader="wrapper"]');
        const textHolder = $(wrapper).find('*[data-loader="text"]');
        contentHolder.removeClass("d-none");
        let textVal = utilities.getLocalizedText(text, window.currentLanguage) || this.blankText;
        if (textVal != null) {
            textHolder.html(textVal);
            textHolder.removeClass("d-none");
        }
    }
    

    _updateBlock(target, inputVal, language, wrapper = this.wrapper) {
        //console.log(`_updateBlock: ${this.blockName["en-GB"]}`, target, inputVal, language, this.data);
        super._updateBlock();
        // locate html placeholders in template
        const text = $(wrapper).find('*[data-loader="text"]');
        // using targets update values in the page as you edit
        switch (target) {
            case "text":
                this.data.textBilingual[language] = inputVal;
                let textVal = utilities.getLocalizedText(this.data.textBilingual, window.currentLanguage) || this.blankText;
                if (textVal != null) {
                    text.html(textVal);
                    text.removeClass("d-none");
                } else {
                    text.addClass("d-none");
                }
                this._enableSavebutton();
                break;
            default:
                break;
        }
    }

    /**
   * @private
   * Click on the Settings Button
   * @param {string} tune — tune name from this.settings
   */
    _toggleTune(tune) {
        switch (tune.name) {
            case "editContent":
                // show / hide the settings panel
                this._toggleBlockSettings();
                break;
            default:
                break;
        }
        this._acceptTuneView();
    }

    /**
   * Add specified class corresponds with activated tunes
   * @private
   */
    _acceptTuneView(wrapper = this.wrapper) {
        let contentHolder = wrapper.querySelector(".text-bilingual");
        this.settings.forEach(tune => {
            contentHolder.classList.toggle(tune.name, !!this.data[tune.name]);
        });
    }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant