]> BookStack Code Mirror - bookstack/blobdiff - resources/js/wysiwyg/plugins-tasklist.js
New translations editor.php (Italian)
[bookstack] / resources / js / wysiwyg / plugins-tasklist.js
index 07f93446327a4a86074d2a8ead2801771ee0c83d..5b0e1c1f046563d8a0ff652d81e23df4a6775fbe 100644 (file)
-/**
- * @param {Editor} editor
- */
-function defineTaskListCustomElement(editor) {
-    const doc = editor.getDoc();
-    const win = doc.defaultView;
-
-    class TaskListElement extends win.HTMLElement {
-        constructor() {
-            super();
-            // this.attachShadow({mode: 'open'});
-            //
-            // const input = doc.createElement('input');
-            // input.setAttribute('type', 'checkbox');
-            // input.setAttribute('disabled', 'disabled');
-            //
-            // if (this.hasAttribute('selected')) {
-            //     input.setAttribute('selected', 'selected');
-            // }
-            //
-            // this.shadowRoot.append(input);
-            // this.shadowRoot.close();
-        }
-    }
-
-    win.customElements.define('task-list-item', TaskListElement);
-}
-
 /**
  * @param {Editor} editor
  * @param {String} url
  */
 function register(editor, url) {
 
-    // editor.on('NewBlock', ({ newBlock}) => {
-    //     ensureElementHasCheckbox(newBlock);
-    // });
+    // Tasklist UI buttons
+    editor.ui.registry.addIcon('tasklist', '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M22,8c0-0.55-0.45-1-1-1h-7c-0.55,0-1,0.45-1,1s0.45,1,1,1h7C21.55,9,22,8.55,22,8z M13,16c0,0.55,0.45,1,1,1h7 c0.55,0,1-0.45,1-1c0-0.55-0.45-1-1-1h-7C13.45,15,13,15.45,13,16z M10.47,4.63c0.39,0.39,0.39,1.02,0,1.41l-4.23,4.25 c-0.39,0.39-1.02,0.39-1.42,0L2.7,8.16c-0.39-0.39-0.39-1.02,0-1.41c0.39-0.39,1.02-0.39,1.41,0l1.42,1.42l3.54-3.54 C9.45,4.25,10.09,4.25,10.47,4.63z M10.48,12.64c0.39,0.39,0.39,1.02,0,1.41l-4.23,4.25c-0.39,0.39-1.02,0.39-1.42,0L2.7,16.16 c-0.39-0.39-0.39-1.02,0-1.41s1.02-0.39,1.41,0l1.42,1.42l3.54-3.54C9.45,12.25,10.09,12.25,10.48,12.64L10.48,12.64z"/></svg>');
+    editor.ui.registry.addToggleButton('tasklist', {
+        tooltip: 'Task list',
+        icon: 'tasklist',
+        active: false,
+        onAction(api) {
+            if (api.isActive()) {
+                editor.execCommand('RemoveList');
+            } else {
+                editor.execCommand('InsertUnorderedList', null, {
+                    'list-item-attributes': {
+                        class: 'task-list-item',
+                    },
+                    'list-style-type': 'tasklist',
+                });
+            }
+        },
+        onSetup(api) {
+            editor.on('NodeChange', event => {
+                const parentListEl = event.parents.find(el => el.nodeName === 'LI');
+                const inList = parentListEl && parentListEl.classList.contains('task-list-item');
+                api.setActive(inList);
+            });
+        }
+    });
 
-    editor.on('PreInit', () => {
+    // Tweak existing bullet list button active state to not be active
+    // when we're in a task list.
+    const existingBullListButton = editor.ui.registry.getAll().buttons.bullist;
+    existingBullListButton.onSetup = function(api) {
+        editor.on('NodeChange', event => {
+            const parentList = event.parents.find(el => el.nodeName === 'LI');
+            const inTaskList = parentList && parentList.classList.contains('task-list-item');
+            const inUlList = parentList && parentList.parentNode.nodeName === 'UL';
+            api.setActive(inUlList && !inTaskList);
+        });
+    };
+    existingBullListButton.onAction = function() {
+        // Cheeky hack to prevent list toggle action treating tasklists as normal
+        // unordered lists which would unwrap the list on toggle from tasklist to bullet list.
+        // Instead we quickly jump through an ordered list first if we're within a tasklist.
+        if (elementWithinTaskList(editor.selection.getNode())) {
+            editor.execCommand('InsertOrderedList', null, {
+                'list-item-attributes': {class: null}
+            });
+        }
 
-        defineTaskListCustomElement(editor);
+        editor.execCommand('InsertUnorderedList', null, {
+            'list-item-attributes': {class: null}
+        });
+    };
+    // Tweak existing number list to not allow classes on child items
+    const existingNumListButton = editor.ui.registry.getAll().buttons.numlist;
+    existingNumListButton.onAction = function() {
+        editor.execCommand('InsertOrderedList', null, {
+            'list-item-attributes': {class: null}
+        });
+    };
 
-        editor.parser.addNodeFilter('li', function(elms) {
-            for (const elem of elms) {
-                if (elem.attributes.map.class === 'task-list-item') {
-                    replaceTaskListNode(elem);
+    // Setup filters on pre-init
+    editor.on('PreInit', () => {
+        editor.parser.addNodeFilter('li', function(nodes) {
+            for (const node of nodes) {
+                if (node.attributes.map.class === 'task-list-item') {
+                    parseTaskListNode(node);
                 }
             }
         });
+        editor.serializer.addNodeFilter('li', function(nodes) {
+            for (const node of nodes) {
+                if (node.attributes.map.class === 'task-list-item') {
+                    serializeTaskListNode(node);
+                }
+            }
+        });
+    });
 
-        // editor.serializer.addNodeFilter('li', function(elms) {
-        //     for (const elem of elms) {
-        //         if (elem.attributes.map.class === 'task-list-item') {
-        //             ensureNodeHasCheckbox(elem);
-        //         }
-        //     }
-        // });
-
+    // Handle checkbox click in editor
+    editor.on('click', function(event) {
+        const clickedEl = event.target;
+        if (clickedEl.nodeName === 'LI' && clickedEl.classList.contains('task-list-item')) {
+            handleTaskListItemClick(event, clickedEl, editor);
+            event.preventDefault();
+        }
     });
+}
 
+/**
+ * @param {Element} element
+ * @return {boolean}
+ */
+function elementWithinTaskList(element) {
+    const listEl = element.closest('li');
+    return listEl && listEl.parentNode.nodeName === 'UL' && listEl.classList.contains('task-list-item');
 }
 
 /**
- * @param {AstNode} node
+ * @param {MouseEvent} event
+ * @param {Element} clickedEl
+ * @param {Editor} editor
  */
-function replaceTaskListNode(node) {
+function handleTaskListItemClick(event, clickedEl, editor) {
+    const bounds = clickedEl.getBoundingClientRect();
+    const withinBounds = event.clientX <= bounds.right
+                        && event.clientX >= bounds.left
+                        && event.clientY >= bounds.top
+                        && event.clientY <= bounds.bottom;
+
+    // Outside of the task list item bounds mean we're probably clicking the pseudo-element.
+    if (!withinBounds) {
+        editor.undoManager.transact(() => {
+            if (clickedEl.hasAttribute('checked')) {
+                clickedEl.removeAttribute('checked');
+            }  else {
+                clickedEl.setAttribute('checked', 'checked');
+            }
+        });
+    }
+}
 
-    const taskListItem = new tinymce.html.Node.create('task-list-item', {
-    });
+/**
+ * @param {AstNode} node
+ */
+function parseTaskListNode(node) {
+    // Force task list item class
+    node.attr('class', 'task-list-item');
 
+    // Copy checkbox status and remove checkbox within editor
     for (const child of node.children()) {
-        if (node.name !== 'input') {
-            taskListItem.append(child);
+        if (child.name === 'input') {
+            if (child.attr('checked') === 'checked') {
+                node.attr('checked', 'checked');
+            }
+            child.remove();
         }
     }
-
-    node.replace(taskListItem);
 }
 
-// /**
-//  * @param {Element} elem
-//  */
-// function ensureElementHasCheckbox(elem) {
-//     const hasCheckbox = elem.querySelector(':scope > input[type="checkbox"]') !== null;
-//     if (hasCheckbox) {
-//         return;
-//     }
-//
-//     const input = elem.ownerDocument.createElement('input');
-//     input.setAttribute('type', 'checkbox');
-//     input.setAttribute('disabled', 'disabled');
-//     elem.prepend(input);
-// }
-
 /**
- * @param {AstNode} elem
+ * @param {AstNode} node
  */
-function ensureNodeHasCheckbox(elem) {
-    // Stop if there's already an input
-    if (elem.firstChild && elem.firstChild.name === 'input') {
-        return;
+function serializeTaskListNode(node) {
+    // Get checked status and clean it from list node
+    const isChecked = node.attr('checked') === 'checked';
+    node.attr('checked', null);
+
+    const inputAttrs = {type: 'checkbox', disabled: 'disabled'};
+    if (isChecked) {
+        inputAttrs.checked = 'checked';
     }
 
-    const input = new tinymce.html.Node.create('input', {
-        type: 'checkbox',
-        disabled: 'disabled',
-    });
-
-    if (elem.firstChild) {
-        elem.insert(input, elem.firstChild, true);
-    } else {
-        elem.append(input);
-    }
+    // Create & insert checkbox input element
+    const checkbox = new tinymce.html.Node.create('input', inputAttrs);
+    checkbox.shortEnded = true;
+    node.firstChild ? node.insert(checkbox, node.firstChild, true) : node.append(checkbox);
 }
 
-
 /**
  * @param {WysiwygConfigOptions} options
  * @return {register}