-/**
- * @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}