import Delta from 'quill-delta'; import Parchment from 'parchment'; import Quill from '../core/quill'; import logger from '../core/logger'; import Module from '../core/module'; let debug = logger('quill:toolbar'); class Toolbar extends Module { constructor(quill, options) { super(quill, options); if (Array.isArray(this.options.container)) { let container = document.createElement('div'); addControls(container, this.options.container); quill.container.parentNode.insertBefore(container, quill.container); this.container = container; } else if (typeof this.options.container === 'string') { this.container = document.querySelector(this.options.container); } else { this.container = this.options.container; } if (!(this.container instanceof HTMLElement)) { return debug.error('Container required for toolbar', this.options); } this.container.classList.add('ql-toolbar'); this.controls = []; this.handlers = {}; Object.keys(this.options.handlers).forEach((format) => { this.addHandler(format, this.options.handlers[format]); }); [].forEach.call(this.container.querySelectorAll('button, select'), (input) => { this.attach(input); }); this.quill.on(Quill.events.EDITOR_CHANGE, (type, range) => { if (type === Quill.events.SELECTION_CHANGE) { this.update(range); } }); this.quill.on(Quill.events.SCROLL_OPTIMIZE, () => { let [range, ] = this.quill.selection.getRange(); // quill.getSelection triggers update this.update(range); }); } addHandler(format, handler) { this.handlers[format] = handler; } attach(input) { let format = [].find.call(input.classList, (className) => { return className.indexOf('ql-') === 0; }); if (!format) return; format = format.slice('ql-'.length); if (input.tagName === 'BUTTON') { input.setAttribute('type', 'button'); } if (this.handlers[format] == null) { if (this.quill.scroll.whitelist != null && this.quill.scroll.whitelist[format] == null) { debug.warn('ignoring attaching to disabled format', format, input); return; } if (Parchment.query(format) == null) { debug.warn('ignoring attaching to nonexistent format', format, input); return; } } let eventName = input.tagName === 'SELECT' ? 'change' : 'click'; input.addEventListener(eventName, (e) => { let value; if (input.tagName === 'SELECT') { if (input.selectedIndex < 0) return; let selected = input.options[input.selectedIndex]; if (selected.hasAttribute('selected')) { value = false; } else { value = selected.value || false; } } else { if (input.classList.contains('ql-active')) { value = false; } else { value = input.value || !input.hasAttribute('value'); } e.preventDefault(); } this.quill.focus(); let [range, ] = this.quill.selection.getRange(); if (this.handlers[format] != null) { this.handlers[format].call(this, value); } else if (Parchment.query(format).prototype instanceof Parchment.Embed) { value = prompt(`Enter ${format}`); if (!value) return; this.quill.updateContents(new Delta() .retain(range.index) .delete(range.length) .insert({ [format]: value }) , Quill.sources.USER); } else { this.quill.format(format, value, Quill.sources.USER); } this.update(range); }); // TODO use weakmap this.controls.push([format, input]); } update(range) { let formats = range == null ? {} : this.quill.getFormat(range); this.controls.forEach(function(pair) { let [format, input] = pair; if (input.tagName === 'SELECT') { let option; if (range == null) { option = null; } else if (formats[format] == null) { option = input.querySelector('option[selected]'); } else if (!Array.isArray(formats[format])) { let value = formats[format]; if (typeof value === 'string') { value = value.replace(/\"/g, '\\"'); } option = input.querySelector(`option[value="${value}"]`); } if (option == null) { input.value = ''; // TODO make configurable? input.selectedIndex = -1; } else { option.selected = true; } } else { if (range == null) { input.classList.remove('ql-active'); } else if (input.hasAttribute('value')) { // both being null should match (default values) // '1' should match with 1 (headers) let isActive = formats[format] === input.getAttribute('value') || (formats[format] != null && formats[format].toString() === input.getAttribute('value')) || (formats[format] == null && !input.getAttribute('value')); input.classList.toggle('ql-active', isActive); } else { input.classList.toggle('ql-active', formats[format] != null); } } }); } } Toolbar.DEFAULTS = {}; function addButton(container, format, value) { let input = document.createElement('button'); input.setAttribute('type', 'button'); input.classList.add('ql-' + format); if (value != null) { input.value = value; } container.appendChild(input); } function addControls(container, groups) { if (!Array.isArray(groups[0])) { groups = [groups]; } groups.forEach(function(controls) { let group = document.createElement('span'); group.classList.add('ql-formats'); controls.forEach(function(control) { if (typeof control === 'string') { addButton(group, control); } else { let format = Object.keys(control)[0]; let value = control[format]; if (Array.isArray(value)) { addSelect(group, format, value); } else { addButton(group, format, value); } } }); container.appendChild(group); }); } function addSelect(container, format, values) { let input = document.createElement('select'); input.classList.add('ql-' + format); values.forEach(function(value) { let option = document.createElement('option'); if (value !== false) { option.setAttribute('value', value); } else { option.setAttribute('selected', 'selected'); } input.appendChild(option); }); container.appendChild(input); } Toolbar.DEFAULTS = { container: null, handlers: { clean: function() { let range = this.quill.getSelection(); if (range == null) return; if (range.length == 0) { let formats = this.quill.getFormat(); Object.keys(formats).forEach((name) => { // Clean functionality in existing apps only clean inline formats if (Parchment.query(name, Parchment.Scope.INLINE) != null) { this.quill.format(name, false); } }); } else { this.quill.removeFormat(range, Quill.sources.USER); } }, direction: function(value) { let align = this.quill.getFormat()['align']; if (value === 'rtl' && align == null) { this.quill.format('align', 'right', Quill.sources.USER); } else if (!value && align === 'right') { this.quill.format('align', false, Quill.sources.USER); } this.quill.format('direction', value, Quill.sources.USER); }, indent: function(value) { let range = this.quill.getSelection(); let formats = this.quill.getFormat(range); let indent = parseInt(formats.indent || 0); if (value === '+1' || value === '-1') { let modifier = (value === '+1') ? 1 : -1; if (formats.direction === 'rtl') modifier *= -1; this.quill.format('indent', indent + modifier, Quill.sources.USER); } }, link: function(value) { if (value === true) { value = prompt('Enter link URL:'); } this.quill.format('link', value, Quill.sources.USER); }, list: function(value) { let range = this.quill.getSelection(); let formats = this.quill.getFormat(range); if (value === 'check') { if (formats['list'] === 'checked' || formats['list'] === 'unchecked') { this.quill.format('list', false, Quill.sources.USER); } else { this.quill.format('list', 'unchecked', Quill.sources.USER); } } else { this.quill.format('list', value, Quill.sources.USER); } } } } export { Toolbar as default, addControls };