UNPKG

8.7 kBJavaScriptView Raw
1import Delta from 'quill-delta';
2import Parchment from 'parchment';
3import Quill from '../core/quill';
4import logger from '../core/logger';
5import Module from '../core/module';
6
7let debug = logger('quill:toolbar');
8
9
10class Toolbar extends Module {
11 constructor(quill, options) {
12 super(quill, options);
13 if (Array.isArray(this.options.container)) {
14 let container = document.createElement('div');
15 addControls(container, this.options.container);
16 quill.container.parentNode.insertBefore(container, quill.container);
17 this.container = container;
18 } else if (typeof this.options.container === 'string') {
19 this.container = document.querySelector(this.options.container);
20 } else {
21 this.container = this.options.container;
22 }
23 if (!(this.container instanceof HTMLElement)) {
24 return debug.error('Container required for toolbar', this.options);
25 }
26 this.container.classList.add('ql-toolbar');
27 this.controls = [];
28 this.handlers = {};
29 Object.keys(this.options.handlers).forEach((format) => {
30 this.addHandler(format, this.options.handlers[format]);
31 });
32 [].forEach.call(this.container.querySelectorAll('button, select'), (input) => {
33 this.attach(input);
34 });
35 this.quill.on(Quill.events.EDITOR_CHANGE, (type, range) => {
36 if (type === Quill.events.SELECTION_CHANGE) {
37 this.update(range);
38 }
39 });
40 this.quill.on(Quill.events.SCROLL_OPTIMIZE, () => {
41 let [range, ] = this.quill.selection.getRange(); // quill.getSelection triggers update
42 this.update(range);
43 });
44 }
45
46 addHandler(format, handler) {
47 this.handlers[format] = handler;
48 }
49
50 attach(input) {
51 let format = [].find.call(input.classList, (className) => {
52 return className.indexOf('ql-') === 0;
53 });
54 if (!format) return;
55 format = format.slice('ql-'.length);
56 if (input.tagName === 'BUTTON') {
57 input.setAttribute('type', 'button');
58 }
59 if (this.handlers[format] == null) {
60 if (this.quill.scroll.whitelist != null && this.quill.scroll.whitelist[format] == null) {
61 debug.warn('ignoring attaching to disabled format', format, input);
62 return;
63 }
64 if (Parchment.query(format) == null) {
65 debug.warn('ignoring attaching to nonexistent format', format, input);
66 return;
67 }
68 }
69 let eventName = input.tagName === 'SELECT' ? 'change' : 'click';
70 input.addEventListener(eventName, (e) => {
71 let value;
72 if (input.tagName === 'SELECT') {
73 if (input.selectedIndex < 0) return;
74 let selected = input.options[input.selectedIndex];
75 if (selected.hasAttribute('selected')) {
76 value = false;
77 } else {
78 value = selected.value || false;
79 }
80 } else {
81 if (input.classList.contains('ql-active')) {
82 value = false;
83 } else {
84 value = input.value || !input.hasAttribute('value');
85 }
86 e.preventDefault();
87 }
88 this.quill.focus();
89 let [range, ] = this.quill.selection.getRange();
90 if (this.handlers[format] != null) {
91 this.handlers[format].call(this, value);
92 } else if (Parchment.query(format).prototype instanceof Parchment.Embed) {
93 value = prompt(`Enter ${format}`);
94 if (!value) return;
95 this.quill.updateContents(new Delta()
96 .retain(range.index)
97 .delete(range.length)
98 .insert({ [format]: value })
99 , Quill.sources.USER);
100 } else {
101 this.quill.format(format, value, Quill.sources.USER);
102 }
103 this.update(range);
104 });
105 // TODO use weakmap
106 this.controls.push([format, input]);
107 }
108
109 update(range) {
110 let formats = range == null ? {} : this.quill.getFormat(range);
111 this.controls.forEach(function(pair) {
112 let [format, input] = pair;
113 if (input.tagName === 'SELECT') {
114 let option;
115 if (range == null) {
116 option = null;
117 } else if (formats[format] == null) {
118 option = input.querySelector('option[selected]');
119 } else if (!Array.isArray(formats[format])) {
120 let value = formats[format];
121 if (typeof value === 'string') {
122 value = value.replace(/\"/g, '\\"');
123 }
124 option = input.querySelector(`option[value="${value}"]`);
125 }
126 if (option == null) {
127 input.value = ''; // TODO make configurable?
128 input.selectedIndex = -1;
129 } else {
130 option.selected = true;
131 }
132 } else {
133 if (range == null) {
134 input.classList.remove('ql-active');
135 } else if (input.hasAttribute('value')) {
136 // both being null should match (default values)
137 // '1' should match with 1 (headers)
138 let isActive = formats[format] === input.getAttribute('value') ||
139 (formats[format] != null && formats[format].toString() === input.getAttribute('value')) ||
140 (formats[format] == null && !input.getAttribute('value'));
141 input.classList.toggle('ql-active', isActive);
142 } else {
143 input.classList.toggle('ql-active', formats[format] != null);
144 }
145 }
146 });
147 }
148}
149Toolbar.DEFAULTS = {};
150
151
152function addButton(container, format, value) {
153 let input = document.createElement('button');
154 input.setAttribute('type', 'button');
155 input.classList.add('ql-' + format);
156 if (value != null) {
157 input.value = value;
158 }
159 container.appendChild(input);
160}
161
162function addControls(container, groups) {
163 if (!Array.isArray(groups[0])) {
164 groups = [groups];
165 }
166 groups.forEach(function(controls) {
167 let group = document.createElement('span');
168 group.classList.add('ql-formats');
169 controls.forEach(function(control) {
170 if (typeof control === 'string') {
171 addButton(group, control);
172 } else {
173 let format = Object.keys(control)[0];
174 let value = control[format];
175 if (Array.isArray(value)) {
176 addSelect(group, format, value);
177 } else {
178 addButton(group, format, value);
179 }
180 }
181 });
182 container.appendChild(group);
183 });
184}
185
186function addSelect(container, format, values) {
187 let input = document.createElement('select');
188 input.classList.add('ql-' + format);
189 values.forEach(function(value) {
190 let option = document.createElement('option');
191 if (value !== false) {
192 option.setAttribute('value', value);
193 } else {
194 option.setAttribute('selected', 'selected');
195 }
196 input.appendChild(option);
197 });
198 container.appendChild(input);
199}
200
201Toolbar.DEFAULTS = {
202 container: null,
203 handlers: {
204 clean: function() {
205 let range = this.quill.getSelection();
206 if (range == null) return;
207 if (range.length == 0) {
208 let formats = this.quill.getFormat();
209 Object.keys(formats).forEach((name) => {
210 // Clean functionality in existing apps only clean inline formats
211 if (Parchment.query(name, Parchment.Scope.INLINE) != null) {
212 this.quill.format(name, false);
213 }
214 });
215 } else {
216 this.quill.removeFormat(range, Quill.sources.USER);
217 }
218 },
219 direction: function(value) {
220 let align = this.quill.getFormat()['align'];
221 if (value === 'rtl' && align == null) {
222 this.quill.format('align', 'right', Quill.sources.USER);
223 } else if (!value && align === 'right') {
224 this.quill.format('align', false, Quill.sources.USER);
225 }
226 this.quill.format('direction', value, Quill.sources.USER);
227 },
228 indent: function(value) {
229 let range = this.quill.getSelection();
230 let formats = this.quill.getFormat(range);
231 let indent = parseInt(formats.indent || 0);
232 if (value === '+1' || value === '-1') {
233 let modifier = (value === '+1') ? 1 : -1;
234 if (formats.direction === 'rtl') modifier *= -1;
235 this.quill.format('indent', indent + modifier, Quill.sources.USER);
236 }
237 },
238 link: function(value) {
239 if (value === true) {
240 value = prompt('Enter link URL:');
241 }
242 this.quill.format('link', value, Quill.sources.USER);
243 },
244 list: function(value) {
245 let range = this.quill.getSelection();
246 let formats = this.quill.getFormat(range);
247 if (value === 'check') {
248 if (formats['list'] === 'checked' || formats['list'] === 'unchecked') {
249 this.quill.format('list', false, Quill.sources.USER);
250 } else {
251 this.quill.format('list', 'unchecked', Quill.sources.USER);
252 }
253 } else {
254 this.quill.format('list', value, Quill.sources.USER);
255 }
256 }
257 }
258}
259
260
261export { Toolbar as default, addControls };