2 * @param {Editor} editor
5 function register(editor, url) {
8 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>');
9 editor.ui.registry.addToggleButton('tasklist', {
15 editor.execCommand('RemoveList');
17 editor.execCommand('InsertUnorderedList', null, {
18 'list-item-attributes': {
19 class: 'task-list-item',
21 'list-style-type': 'tasklist',
26 editor.on('NodeChange', event => {
27 const parentListEl = event.parents.find(el => el.nodeName === 'LI');
28 const inList = parentListEl && parentListEl.classList.contains('task-list-item');
29 api.setActive(inList);
34 // Tweak existing bullet list button active state to not be active
35 // when we're in a task list.
36 const existingBullListButton = editor.ui.registry.getAll().buttons.bullist;
37 existingBullListButton.onSetup = function(api) {
38 editor.on('NodeChange', event => {
39 const parentList = event.parents.find(el => el.nodeName === 'LI');
40 const inTaskList = parentList && parentList.classList.contains('task-list-item');
41 const inUlList = parentList && parentList.parentNode.nodeName === 'UL';
42 api.setActive(inUlList && !inTaskList);
45 existingBullListButton.onAction = function() {
46 // Cheeky hack to prevent list toggle action treating tasklists as normal
47 // unordered lists which would unwrap the list on toggle from tasklist to bullet list.
48 // Instead we quickly jump through an ordered list first if we're within a tasklist.
49 if (elementWithinTaskList(editor.selection.getNode())) {
50 editor.execCommand('InsertOrderedList', null, {
51 'list-item-attributes': {class: null}
55 editor.execCommand('InsertUnorderedList', null, {
56 'list-item-attributes': {class: null}
59 // Tweak existing number list to not allow classes on child items
60 const existingNumListButton = editor.ui.registry.getAll().buttons.numlist;
61 existingNumListButton.onAction = function() {
62 editor.execCommand('InsertOrderedList', null, {
63 'list-item-attributes': {class: null}
67 // Setup filters on pre-init
68 editor.on('PreInit', () => {
69 editor.parser.addNodeFilter('li', function(nodes) {
70 for (const node of nodes) {
71 if (node.attributes.map.class === 'task-list-item') {
72 parseTaskListNode(node);
76 editor.serializer.addNodeFilter('li', function(nodes) {
77 for (const node of nodes) {
78 if (node.attributes.map.class === 'task-list-item') {
79 serializeTaskListNode(node);
85 // Handle checkbox click in editor
86 editor.on('click', function(event) {
87 const clickedEl = event.target;
88 if (clickedEl.nodeName === 'LI' && clickedEl.classList.contains('task-list-item')) {
89 handleTaskListItemClick(event, clickedEl, editor);
90 event.preventDefault();
96 * @param {Element} element
99 function elementWithinTaskList(element) {
100 const listEl = element.closest('li');
101 return listEl && listEl.parentNode.nodeName === 'UL' && listEl.classList.contains('task-list-item');
105 * @param {MouseEvent} event
106 * @param {Element} clickedEl
107 * @param {Editor} editor
109 function handleTaskListItemClick(event, clickedEl, editor) {
110 const bounds = clickedEl.getBoundingClientRect();
111 const withinBounds = event.clientX <= bounds.right
112 && event.clientX >= bounds.left
113 && event.clientY >= bounds.top
114 && event.clientY <= bounds.bottom;
116 // Outside of the task list item bounds mean we're probably clicking the pseudo-element.
118 editor.undoManager.transact(() => {
119 if (clickedEl.hasAttribute('checked')) {
120 clickedEl.removeAttribute('checked');
122 clickedEl.setAttribute('checked', 'checked');
129 * @param {AstNode} node
131 function parseTaskListNode(node) {
132 // Force task list item class
133 node.attr('class', 'task-list-item');
135 // Copy checkbox status and remove checkbox within editor
136 for (const child of node.children()) {
137 if (child.name === 'input') {
138 if (child.attr('checked') === 'checked') {
139 node.attr('checked', 'checked');
147 * @param {AstNode} node
149 function serializeTaskListNode(node) {
150 // Get checked status and clean it from list node
151 const isChecked = node.attr('checked') === 'checked';
152 node.attr('checked', null);
154 const inputAttrs = {type: 'checkbox', disabled: 'disabled'};
156 inputAttrs.checked = 'checked';
159 // Create & insert checkbox input element
160 const checkbox = new tinymce.html.Node.create('input', inputAttrs);
161 checkbox.shortEnded = true;
162 node.firstChild ? node.insert(checkbox, node.firstChild, true) : node.append(checkbox);
166 * @param {WysiwygConfigOptions} options
169 export function getPlugin(options) {