ckeditor / ckeditor5

Powerful rich text editor framework with a modular architecture, modern integrations, and features like collaborative editing.
https://ckeditor.com/ckeditor-5
Other
9.34k stars 3.68k forks source link

Incorporate toggleable sidebars into ckeditor5-ui #16467

Open Reinmar opened 3 months ago

Reinmar commented 3 months ago

📝 Provide a description of the improvement

The new builder returns these snippets to implement the toggleable sidebars:


/**
 * The `AnnotationsSidebarToggler` adds an icon to the right side of the editor.
 *
 * It allows to toggle the right annotations bar visibility.
 */
class AnnotationsSidebarToggler extends Plugin {
    static get requires() {
        return ['AnnotationsUIs'];
    }

    static get pluginName() {
        return 'AnnotationsSidebarToggler';
    }

    init() {
        const NON_COLLAPSE_ANNOTATION_ICON =
            '<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" transform="matrix(-1,0,0,1,0,0)"><path d="M11.463 5.187a.888.888 0 1 1 1.254 1.255L9.16 10l3.557 3.557a.888.888 0 1 1-1.254 1.255L7.26 10.61a.888.888 0 0 1 .16-1.382l4.043-4.042z"></path></svg>';
        const COLLAPSE_ANNOTATION_ICON =
            '<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" transform="matrix(1,0,0,1,0,0)"><path d="M11.463 5.187a.888.888 0 1 1 1.254 1.255L9.16 10l3.557 3.557a.888.888 0 1 1-1.254 1.255L7.26 10.61a.888.888 0 0 1 .16-1.382l4.043-4.042z"/></svg>';

        const button = new ButtonView(this.editor.locale);
        const annotationsUIsPlugin = this.editor.plugins.get('AnnotationsUIs');
        const annotationsContainer = this.editor.config.get('sidebar.container');
        const sidebarContainer = annotationsContainer?.parentElement;
        const navigationContainer = sidebarContainer?.querySelector('.editor-container__sidebar-navigation');

        // Remove existing buttons, e.g., when reloading the editor in run-time.
        Array.from(navigationContainer?.children ?? []).forEach(child => child.remove());

        button.set({
            label: 'Toggle annotations sidebar',
            tooltip: 'Hide annotations sidebar',
            tooltipPosition: 'se',
            icon: COLLAPSE_ANNOTATION_ICON
        });

        button.on('execute', () => {
            // Toggle a CSS class on the annotations sidebar container to manage the visibility of the sidebar.
            annotationsContainer?.classList.toggle('ck-hidden');

            // Change the look of the button to reflect the state of the annotations container.
            if (annotationsContainer?.classList.contains('ck-hidden')) {
                button.icon = NON_COLLAPSE_ANNOTATION_ICON;
                button.tooltip = 'Show annotations sidebar';
                annotationsUIsPlugin.switchTo('inline');
            } else {
                button.icon = COLLAPSE_ANNOTATION_ICON;
                button.tooltip = 'Hide annotations sidebar';
                annotationsUIsPlugin.switchTo('wideSidebar');
            }

            // Keep the focus in the editor whenever the button is clicked.
            this.editor.editing.view.focus();
        });

        button.render();

        if (!button.element) {
            return;
        }

        // Append the button.
        navigationContainer?.appendChild(button.element);
    }
}

/**
 * The `DocumentOutlineToggler` adds an icon to the left side of the editor.
 *
 * It allows to toggle document outline visibility.
 */
class DocumentOutlineToggler extends Plugin {
    static get requires() {
        return [];
    }

    static get pluginName() {
        return 'DocumentOutlineToggler';
    }

    init() {
        const DOCUMENT_OUTLINE_ICON =
            '<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M5 9.5a.5.5 0 0 0 .5-.5v-.5A.5.5 0 0 0 5 8H3.5a.5.5 0 0 0-.5.5V9a.5.5 0 0 0 .5.5H5Z"/><path d="M5.5 12a.5.5 0 0 1-.5.5H3.5A.5.5 0 0 1 3 12v-.5a.5.5 0 0 1 .5-.5H5a.5.5 0 0 1 .5.5v.5Z"/><path d="M5 6.5a.5.5 0 0 0 .5-.5v-.5A.5.5 0 0 0 5 5H3.5a.5.5 0 0 0-.5.5V6a.5.5 0 0 0 .5.5H5Z"/><path clip-rule="evenodd" d="M2 19a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H2Zm6-1.5h10a.5.5 0 0 0 .5-.5V3a.5.5 0 0 0-.5-.5H8v15Zm-1.5-15H2a.5.5 0 0 0-.5.5v14a.5.5 0 0 0 .5.5h4.5v-15Z"/></svg>';
        const COLLAPSE_OUTLINE_ICON =
            '<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M11.463 5.187a.888.888 0 1 1 1.254 1.255L9.16 10l3.557 3.557a.888.888 0 1 1-1.254 1.255L7.26 10.61a.888.888 0 0 1 .16-1.382l4.043-4.042z"/></svg>';

        const button = new ButtonView(this.editor.locale);
        const documentOutlineContainer = this.editor.config.get('documentOutline.container');
        const sidebarContainer = documentOutlineContainer?.parentElement;
        const navigationContainer = sidebarContainer?.querySelector('.editor-container__sidebar-navigation');

        // Remove existing buttons, e.g., when reloading the editor in run-time.
        Array.from(navigationContainer?.children ?? []).forEach(child => child.remove());

        button.set({
            label: 'Toggle document outline',
            tooltip: 'Hide document outline',
            tooltipPosition: 'se',
            icon: COLLAPSE_OUTLINE_ICON
        });

        button.on('execute', () => {
            // Toggle a CSS class on the document outline container to manage the visibility of the outline.
            documentOutlineContainer?.classList.toggle('ck-hidden');

            // Change the look of the button to reflect the state of the document outline feature.
            if (documentOutlineContainer?.classList.contains('ck-hidden')) {
                button.icon = DOCUMENT_OUTLINE_ICON;
                button.tooltip = 'Show document outline';
            } else {
                button.icon = COLLAPSE_OUTLINE_ICON;
                button.tooltip = 'Hide document outline';
            }

            // Keep the focus in the editor whenever the button is clicked.
            this.editor.editing.view.focus();
        });

        button.render();

        if (!button.element) {
            return;
        }

        // Append the button.
        navigationContainer?.appendChild(button.element);
    }
}

They are awfully long.

I think it's worth optimizing the length of the integration code and standardizing some of these samples, as long as we're not doing too much magic there.

So, the "least magical" option would be to register the buttons in the component factory and probably giving them some nice API to plug the necessary logic. We'd probably need to move a bit of CSS too. However, in this option, the logic of how the DOM is managed to toggle those sidebars would still be in the sample. 

The advantage of this option is that the integrator will still be able to relatively simply replace this with their own implementation of sidebars toggling. We can be pretty sure that many integrators have custom implementations anyway.

A more comprehensive option would be to also wrap the logic of what happens upon clicking those buttons. This would become a full sidebars implementation (together with default styles). We'd need to pay attention to what's configurable, but we could e.g. propose an opinionated solution to handling "stickiness". 

The advantage of this option is that the builder snippet would be much shorter. Integrators looking for quick drop-in solutions would be happy. But there's a big question of how many of them need custom sidebars anyway and whether they wouldn't be confused on what's happening. To prevent that, the plugin/component would need to act sort of as an "enhancer" of a plain DOM element. So, removing it would bring back a plain sidebar.


If you'd like to see this improvement implemented, add a 👍 reaction to this post.

Witoso commented 3 months ago

The advantage of this option is that the integrator will still be able to relatively simply replace this with their own implementation of sidebars toggling. We can be pretty sure that many integrators have custom implementations anyway.

Could you name a few options on how this could differ between integrations, and where the differences could be?

scofalik commented 3 months ago

Do I understand correctly, that you would like to make these "collapse document outline" and "collapse sidebar" buttons a part of the CKE5 code?

I am not sure if I necessarily agree with that, given that these UI components may be quite customized in various applications.

I like this solution more:

A more comprehensive option would be to also wrap the logic of what happens upon clicking those buttons. This would become a full sidebars implementation (together with default styles). We'd need to pay attention to what's configurable, but we could e.g. propose an opinionated solution to handling "stickiness". 

But then we need to define what sidebars are, what kinds of sidebars we have, and how they behave. Still, I am not sure if this is something that we should propose.

Could you name a few options on how this could differ between integrations, and where the differences could be?

Some integrators have sidebar next to the content (the default way), why other have positioned absolutely somewhere far outside of the editor editable (and e.g. have 100% height for that). Other applications combine our sidebar with their own features and implement tabs to navigate. Then we have implementations that may use multiple editors, or even fields between them. Then, of course styling, of all of this can differ.