]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/plugins-tasklist.js
Added functioning wysiwyg tasklist toolbar button
[bookstack] / resources / js / wysiwyg / plugins-tasklist.js
1 /**
2  * @param {Editor} editor
3  * @param {String} url
4  */
5 function register(editor, url) {
6
7     // Tasklist UI buttons
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', {
10         tooltip: 'Task list',
11         icon: 'tasklist',
12         active: false,
13         onAction(api) {
14             if (api.isActive()) {
15                 editor.execCommand('RemoveList');
16             } else {
17                 editor.execCommand('InsertUnorderedList', null, {
18                     'list-item-attributes': {
19                         class: 'task-list-item',
20                     },
21                     'list-style-type': 'tasklist',
22                 });
23             }
24         },
25         onSetup(api) {
26             editor.on('NodeChange', event => {
27                 const inList = event.parents.find(el => el.nodeName === 'LI' && el.classList.contains('task-list-item')) !== undefined;
28                 api.setActive(inList);
29             });
30         }
31     });
32
33     // Tweak existing bullet list button active state to not be active
34     // when we're in a task list.
35     const existingBullListButton = editor.ui.registry.getAll().buttons.bullist;
36     existingBullListButton.onSetup = function(api) {
37         editor.on('NodeChange', event => {
38             const notInTaskList = event.parents.find(el => el.nodeName === 'LI' && el.classList.contains('task-list-item')) === undefined;
39             const inList = event.parents.find(el => el.nodeName === 'UL') !== undefined;
40             api.setActive(inList && notInTaskList);
41         });
42     };
43     existingBullListButton.onAction = function() {
44         editor.execCommand('InsertUnorderedList', null, {
45             'list-item-attributes': {class: null}
46         });
47     };
48     // Tweak existing number list to not allow classes on child items
49     const existingNumListButton = editor.ui.registry.getAll().buttons.numlist;
50     existingNumListButton.onAction = function() {
51         editor.execCommand('InsertOrderedList', null, {
52             'list-item-attributes': {class: null}
53         });
54     };
55
56     // Setup filters on pre-init
57     editor.on('PreInit', () => {
58         editor.parser.addNodeFilter('li', function(nodes) {
59             for (const node of nodes) {
60                 if (node.attributes.map.class === 'task-list-item') {
61                     parseTaskListNode(node);
62                 }
63             }
64         });
65         editor.serializer.addNodeFilter('li', function(nodes) {
66             for (const node of nodes) {
67                 if (node.attributes.map.class === 'task-list-item') {
68                     serializeTaskListNode(node);
69                 }
70             }
71         });
72     });
73
74     // Handle checkbox click in editor
75     editor.on('click', function(event) {
76         const clickedEl = event.originalTarget;
77         if (clickedEl.nodeName === 'LI' && clickedEl.classList.contains('task-list-item')) {
78             handleTaskListItemClick(event, clickedEl, editor);
79         }
80     });
81 }
82
83 /**
84  * @param {MouseEvent} event
85  * @param {Element} clickedEl
86  * @param {Editor} editor
87  */
88 function handleTaskListItemClick(event, clickedEl, editor) {
89     const bounds = clickedEl.getBoundingClientRect();
90     const withinBounds = event.clientX <= bounds.right
91                         && event.clientX >= bounds.left
92                         && event.clientY >= bounds.top
93                         && event.clientY <= bounds.bottom;
94
95     // Outside of the task list item bounds mean we're probably clicking the pseudo-element.
96     if (!withinBounds) {
97         editor.undoManager.transact(() => {
98             if (clickedEl.hasAttribute('checked')) {
99                 clickedEl.removeAttribute('checked');
100             }  else {
101                 clickedEl.setAttribute('checked', 'checked');
102             }
103         });
104     }
105 }
106
107 /**
108  * @param {AstNode} node
109  */
110 function parseTaskListNode(node) {
111     // Force task list item class
112     node.attr('class', 'task-list-item');
113
114     // Copy checkbox status and remove checkbox within editor
115     for (const child of node.children()) {
116         if (child.name === 'input') {
117             if (child.attr('checked') === 'checked') {
118                 node.attr('checked', 'checked');
119             }
120             child.remove();
121         }
122     }
123 }
124
125 /**
126  * @param {AstNode} node
127  */
128 function serializeTaskListNode(node) {
129     const isChecked = node.attr('checked') === 'checked';
130     node.attr('checked', null);
131
132     const inputAttrs = {type: 'checkbox', disabled: 'disabled'};
133     if (isChecked) {
134         inputAttrs.checked = 'checked';
135     }
136
137     const checkbox = new tinymce.html.Node.create('input', inputAttrs);
138     checkbox.shortEnded = true;
139     node.firstChild ? node.insert(checkbox, node.firstChild, true) : node.append(checkbox);
140 }
141
142 /**
143  * @param {WysiwygConfigOptions} options
144  * @return {register}
145  */
146 export function getPlugin(options) {
147     return register;
148 }