]> BookStack Code Mirror - bookstack/commitdiff
Started support for WYSIWYG details/summary blocks
authorDan Brown <redacted>
Tue, 8 Feb 2022 23:08:00 +0000 (23:08 +0000)
committerDan Brown <redacted>
Tue, 8 Feb 2022 23:08:00 +0000 (23:08 +0000)
resources/js/wysiwyg/config.js
resources/js/wysiwyg/plugins-details.js [new file with mode: 0644]
resources/sass/_pages.scss
resources/sass/_tinymce.scss

index 8e7669acc9720ca16a2230232db450f1a7718c39..13d15e1c5b388c41af489df97c0255c3e14361e1 100644 (file)
@@ -8,6 +8,7 @@ import {getPlugin as getDrawioPlugin} from "./plugin-drawio";
 import {getPlugin as getCustomhrPlugin} from "./plugins-customhr";
 import {getPlugin as getImagemanagerPlugin} from "./plugins-imagemanager";
 import {getPlugin as getAboutPlugin} from "./plugins-about";
+import {getPlugin as getDetailsPlugin} from "./plugins-details";
 
 const style_formats = [
     {title: "Large Header", format: "h2", preview: 'color: blue;'},
@@ -27,7 +28,6 @@ const style_formats = [
 ];
 
 const formats = {
-    codeeditor: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div'},
     alignleft: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-left'},
     aligncenter: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-center'},
     alignright: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-right'},
@@ -79,7 +79,7 @@ function buildToolbar(options) {
         insertoverflow: {
             icon: 'more-drawer',
             tooltip: 'More',
-            items: 'hr codeeditor drawio media'
+            items: 'hr codeeditor drawio media details'
         }
     };
 
@@ -121,6 +121,7 @@ function gatherPlugins(options) {
         "media",
         "imagemanager",
         "about",
+        "details",
         options.textDirection === 'rtl' ? 'directionality' : '',
     ];
 
@@ -128,6 +129,7 @@ function gatherPlugins(options) {
     window.tinymce.PluginManager.add('customhr', getCustomhrPlugin(options));
     window.tinymce.PluginManager.add('imagemanager', getImagemanagerPlugin(options));
     window.tinymce.PluginManager.add('about', getAboutPlugin(options));
+    window.tinymce.PluginManager.add('details', getDetailsPlugin(options));
 
     if (options.drawioUrl) {
         window.tinymce.PluginManager.add('drawio', getDrawioPlugin(options));
@@ -240,7 +242,7 @@ export function build(options) {
         statusbar: false,
         menubar: false,
         paste_data_images: false,
-        extended_valid_elements: 'pre[*],svg[*],div[drawio-diagram]',
+        extended_valid_elements: 'pre[*],svg[*],div[drawio-diagram],details[*],summary[*]',
         automatic_uploads: false,
         valid_children: "-div[p|h1|h2|h3|h4|h5|h6|blockquote],+div[pre],+div[img]",
         plugins: gatherPlugins(options),
diff --git a/resources/js/wysiwyg/plugins-details.js b/resources/js/wysiwyg/plugins-details.js
new file mode 100644 (file)
index 0000000..90fdf84
--- /dev/null
@@ -0,0 +1,207 @@
+/**
+ * @param {Editor} editor
+ * @param {String} url
+ */
+
+function register(editor, url) {
+
+    editor.ui.registry.addIcon('details', '<svg width="24" height="24"><path d="M8.2 9a.5.5 0 0 0-.4.8l4 5.6a.5.5 0 0 0 .8 0l4-5.6a.5.5 0 0 0-.4-.8ZM20.122 18.151h-16c-.964 0-.934 2.7 0 2.7h16c1.139 0 1.173-2.7 0-2.7zM20.122 3.042h-16c-.964 0-.934 2.7 0 2.7h16c1.139 0 1.173-2.7 0-2.7z"/></svg>');
+
+    editor.ui.registry.addButton('details', {
+        icon: 'details',
+        tooltip: 'Insert collapsible block',
+        onAction() {
+            editor.execCommand('InsertDetailsBlock');
+        }
+    });
+
+    editor.ui.registry.addButton('removedetails', {
+        icon: 'table-delete-table',
+        tooltip: 'Unwrap collapsible block',
+        onAction() {
+            unwrapDetailsInSelection(editor)
+        }
+    });
+
+    editor.ui.registry.addButton('editdetials', {
+        icon: 'tag',
+        tooltip: 'Edit label',
+        onAction() {
+            const details = getSelectedDetailsBlock(editor);
+            const dialog = editor.windowManager.open(detailsDialog(editor));
+            dialog.setData({summary: getSummaryTextFromDetails(details)});
+        }
+    });
+
+    editor.ui.registry.addButton('collapsedetails', {
+        icon: 'action-prev',
+        tooltip: 'Collapse',
+        onAction() {
+            const details = getSelectedDetailsBlock(editor);
+            details.removeAttribute('open');
+            editor.focus();
+        }
+    });
+
+    editor.ui.registry.addButton('expanddetails', {
+        icon: 'action-next',
+        tooltip: 'Expand',
+        onAction() {
+            const details = getSelectedDetailsBlock(editor);
+            details.setAttribute('open', 'open');
+            editor.focus();
+        }
+    });
+
+    editor.addCommand('InsertDetailsBlock', function () {
+        const content = editor.selection.getContent({format: 'html'});
+        const details = document.createElement('details');
+        const summary = document.createElement('summary');
+        details.appendChild(summary);
+        details.innerHTML += content;
+
+        editor.insertContent(details.outerHTML);
+    });
+
+    editor.ui.registry.addContextToolbar('details', {
+        predicate: function (node) {
+            return node.nodeName.toLowerCase() === 'details';
+        },
+        items: 'removedetails editdetials collapsedetails expanddetails',
+        position: 'node',
+        scope: 'node'
+    });
+
+    editor.on('PreInit', () => {
+        setupElementFilters(editor);
+    });
+}
+
+/**
+ * @param {Editor} editor
+ */
+function getSelectedDetailsBlock(editor) {
+    return editor.selection.getNode().closest('details');
+}
+
+/**
+ * @param {Element} element
+ */
+function getSummaryTextFromDetails(element) {
+    const summary = element.querySelector('summary');
+    if (!summary) {
+        return '';
+    }
+    return summary.textContent;
+}
+
+/**
+ * @param {Editor} editor
+ */
+function detailsDialog(editor) {
+    return {
+        title: 'Edit collapsible block',
+        body: {
+            type: 'panel',
+            items: [
+                {
+                    type: 'input',
+                    name: 'summary',
+                    label: 'Toggle label text',
+                },
+            ],
+        },
+        buttons: [
+            {
+                type: 'cancel',
+                text: 'Cancel'
+            },
+            {
+                type: 'submit',
+                text: 'Save',
+                primary: true,
+            }
+        ],
+        onSubmit(api) {
+            const {summary} = api.getData();
+            setSummary(editor, summary);
+            api.close();
+        }
+    }
+}
+
+function setSummary(editor, summaryContent) {
+    const details = getSelectedDetailsBlock(editor);
+    if (!details) return;
+
+    editor.undoManager.transact(() => {
+        let summary = details.querySelector('summary');
+        if (!summary) {
+            summary = document.createElement('summary');
+            details.appendChild(summary);
+        }
+        summary.textContent = summaryContent;
+    });
+}
+
+/**
+ * @param {Editor} editor
+ */
+function unwrapDetailsInSelection(editor) {
+    const details = editor.selection.getNode().closest('details');
+    if (details) {
+        const summary = details.querySelector('summary');
+        editor.undoManager.transact(() => {
+            if (summary) {
+                summary.remove();
+            }
+            while (details.firstChild) {
+                details.parentNode.insertBefore(details.firstChild, details);
+            }
+            details.remove();
+        });
+    }
+    editor.focus();
+}
+
+/**
+ * @param {Editor} editor
+ */
+function setupElementFilters(editor) {
+    editor.parser.addNodeFilter('details', function(elms) {
+        for (const el of elms) {
+            // el.attr('contenteditable', 'false');
+            // console.log(el);
+            // let wrap = el.find('div[detailswrap]');
+            // if (!wrap) {
+            //    wrap = document.createElement('div');
+            //    wrap.setAttribute('detailswrap', 'true');
+            // }
+            //
+            // for (const child of el.children) {
+            //     if (child.nodeName.toLowerCase() === 'summary' || child.hasAttribute('detailswrap')) {
+            //         continue;
+            //     }
+            //     wrap.appendChild(child);
+            // }
+            //
+            // el.appendChild(wrap);
+            // wrap.setAttribute('contenteditable', 'true');
+        }
+    });
+
+    editor.serializer.addNodeFilter('details', function(elms) {
+        for (const summaryEl of elms) {
+            summaryEl.attr('contenteditable', null);
+        }
+    });
+}
+
+
+/**
+ * @param {WysiwygConfigOptions} options
+ * @return {register}
+ */
+export function getPlugin(options) {
+    return register;
+}
\ No newline at end of file
index 23f5150a7c8b6407d28b999dde32e66b8a8e1010..49493729906c66a2de9cdc3011ac865dc93504bc 100755 (executable)
@@ -135,6 +135,27 @@ body.tox-fullscreen, body.markdown-fullscreen {
     background: #FFECEC;
   }
 
+  details {
+    border: 1px solid #DDD;
+    margin-bottom: 1em;
+    padding: $-s;
+  }
+  details > summary {
+    margin-top: -$-s;
+    margin-left: -$-s;
+    margin-right: -$-s;
+    margin-bottom: -$-s;
+    font-weight: bold;
+    background-color: #EEEEEE;
+    padding: $-xs $-s;
+  }
+  details[open] > summary {
+    margin-bottom: 0;
+  }
+  details > summary + * {
+    margin-top: .2em;
+  }
+
   &.page-revision {
     pre code {
       white-space: pre-wrap;
index e846b138f2d81780b1174b095f333f751e90f58c..c5cc179d0edc97577b88d5ae8c5b93c51673ff06 100644 (file)
@@ -37,6 +37,11 @@ body.page-content.mce-content-body  {
   pointer-events: none;
 }
 
+// Prevent details summary clicks {
+.page-content.mce-content-body details summary {
+  pointer-events: none;
+}
+
 /**
  * Dark Mode Overrides
  */