265 lines
8.6 KiB
JavaScript
265 lines
8.6 KiB
JavaScript
|
import extend from 'extend';
|
||
|
import Delta from 'quill-delta';
|
||
|
import Emitter from '../core/emitter';
|
||
|
import Keyboard from '../modules/keyboard';
|
||
|
import Theme from '../core/theme';
|
||
|
import ColorPicker from '../ui/color-picker';
|
||
|
import IconPicker from '../ui/icon-picker';
|
||
|
import Picker from '../ui/picker';
|
||
|
import Tooltip from '../ui/tooltip';
|
||
|
|
||
|
|
||
|
const ALIGNS = [ false, 'center', 'right', 'justify' ];
|
||
|
|
||
|
const COLORS = [
|
||
|
"#000000", "#e60000", "#ff9900", "#ffff00", "#008a00", "#0066cc", "#9933ff",
|
||
|
"#ffffff", "#facccc", "#ffebcc", "#ffffcc", "#cce8cc", "#cce0f5", "#ebd6ff",
|
||
|
"#bbbbbb", "#f06666", "#ffc266", "#ffff66", "#66b966", "#66a3e0", "#c285ff",
|
||
|
"#888888", "#a10000", "#b26b00", "#b2b200", "#006100", "#0047b2", "#6b24b2",
|
||
|
"#444444", "#5c0000", "#663d00", "#666600", "#003700", "#002966", "#3d1466"
|
||
|
];
|
||
|
|
||
|
const FONTS = [ false, 'serif', 'monospace' ];
|
||
|
|
||
|
const HEADERS = [ '1', '2', '3', false ];
|
||
|
|
||
|
const SIZES = [ 'small', false, 'large', 'huge' ];
|
||
|
|
||
|
|
||
|
class BaseTheme extends Theme {
|
||
|
constructor(quill, options) {
|
||
|
super(quill, options);
|
||
|
let listener = (e) => {
|
||
|
if (!document.body.contains(quill.root)) {
|
||
|
return document.body.removeEventListener('click', listener);
|
||
|
}
|
||
|
if (this.tooltip != null && !this.tooltip.root.contains(e.target) &&
|
||
|
document.activeElement !== this.tooltip.textbox && !this.quill.hasFocus()) {
|
||
|
this.tooltip.hide();
|
||
|
}
|
||
|
if (this.pickers != null) {
|
||
|
this.pickers.forEach(function(picker) {
|
||
|
if (!picker.container.contains(e.target)) {
|
||
|
picker.close();
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
};
|
||
|
quill.emitter.listenDOM('click', document.body, listener);
|
||
|
}
|
||
|
|
||
|
addModule(name) {
|
||
|
let module = super.addModule(name);
|
||
|
if (name === 'toolbar') {
|
||
|
this.extendToolbar(module);
|
||
|
}
|
||
|
return module;
|
||
|
}
|
||
|
|
||
|
buildButtons(buttons, icons) {
|
||
|
buttons.forEach((button) => {
|
||
|
let className = button.getAttribute('class') || '';
|
||
|
className.split(/\s+/).forEach((name) => {
|
||
|
if (!name.startsWith('ql-')) return;
|
||
|
name = name.slice('ql-'.length);
|
||
|
if (icons[name] == null) return;
|
||
|
if (name === 'direction') {
|
||
|
button.innerHTML = icons[name][''] + icons[name]['rtl'];
|
||
|
} else if (typeof icons[name] === 'string') {
|
||
|
button.innerHTML = icons[name];
|
||
|
} else {
|
||
|
let value = button.value || '';
|
||
|
if (value != null && icons[name][value]) {
|
||
|
button.innerHTML = icons[name][value];
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
});
|
||
|
}
|
||
|
|
||
|
buildPickers(selects, icons) {
|
||
|
this.pickers = selects.map((select) => {
|
||
|
if (select.classList.contains('ql-align')) {
|
||
|
if (select.querySelector('option') == null) {
|
||
|
fillSelect(select, ALIGNS);
|
||
|
}
|
||
|
return new IconPicker(select, icons.align);
|
||
|
} else if (select.classList.contains('ql-background') || select.classList.contains('ql-color')) {
|
||
|
let format = select.classList.contains('ql-background') ? 'background' : 'color';
|
||
|
if (select.querySelector('option') == null) {
|
||
|
fillSelect(select, COLORS, format === 'background' ? '#ffffff' : '#000000');
|
||
|
}
|
||
|
return new ColorPicker(select, icons[format]);
|
||
|
} else {
|
||
|
if (select.querySelector('option') == null) {
|
||
|
if (select.classList.contains('ql-font')) {
|
||
|
fillSelect(select, FONTS);
|
||
|
} else if (select.classList.contains('ql-header')) {
|
||
|
fillSelect(select, HEADERS);
|
||
|
} else if (select.classList.contains('ql-size')) {
|
||
|
fillSelect(select, SIZES);
|
||
|
}
|
||
|
}
|
||
|
return new Picker(select);
|
||
|
}
|
||
|
});
|
||
|
let update = () => {
|
||
|
this.pickers.forEach(function(picker) {
|
||
|
picker.update();
|
||
|
});
|
||
|
};
|
||
|
this.quill.on(Emitter.events.EDITOR_CHANGE, update);
|
||
|
}
|
||
|
}
|
||
|
BaseTheme.DEFAULTS = extend(true, {}, Theme.DEFAULTS, {
|
||
|
modules: {
|
||
|
toolbar: {
|
||
|
handlers: {
|
||
|
formula: function() {
|
||
|
this.quill.theme.tooltip.edit('formula');
|
||
|
},
|
||
|
image: function() {
|
||
|
let fileInput = this.container.querySelector('input.ql-image[type=file]');
|
||
|
if (fileInput == null) {
|
||
|
fileInput = document.createElement('input');
|
||
|
fileInput.setAttribute('type', 'file');
|
||
|
fileInput.setAttribute('accept', 'image/png, image/gif, image/jpeg, image/bmp, image/x-icon');
|
||
|
fileInput.classList.add('ql-image');
|
||
|
fileInput.addEventListener('change', () => {
|
||
|
if (fileInput.files != null && fileInput.files[0] != null) {
|
||
|
let reader = new FileReader();
|
||
|
reader.onload = (e) => {
|
||
|
let range = this.quill.getSelection(true);
|
||
|
this.quill.updateContents(new Delta()
|
||
|
.retain(range.index)
|
||
|
.delete(range.length)
|
||
|
.insert({ image: e.target.result })
|
||
|
, Emitter.sources.USER);
|
||
|
this.quill.setSelection(range.index + 1, Emitter.sources.SILENT);
|
||
|
fileInput.value = "";
|
||
|
}
|
||
|
reader.readAsDataURL(fileInput.files[0]);
|
||
|
}
|
||
|
});
|
||
|
this.container.appendChild(fileInput);
|
||
|
}
|
||
|
fileInput.click();
|
||
|
},
|
||
|
video: function() {
|
||
|
this.quill.theme.tooltip.edit('video');
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
|
||
|
|
||
|
class BaseTooltip extends Tooltip {
|
||
|
constructor(quill, boundsContainer) {
|
||
|
super(quill, boundsContainer);
|
||
|
this.textbox = this.root.querySelector('input[type="text"]');
|
||
|
this.listen();
|
||
|
}
|
||
|
|
||
|
listen() {
|
||
|
this.textbox.addEventListener('keydown', (event) => {
|
||
|
if (Keyboard.match(event, 'enter')) {
|
||
|
this.save();
|
||
|
event.preventDefault();
|
||
|
} else if (Keyboard.match(event, 'escape')) {
|
||
|
this.cancel();
|
||
|
event.preventDefault();
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
cancel() {
|
||
|
this.hide();
|
||
|
}
|
||
|
|
||
|
edit(mode = 'link', preview = null) {
|
||
|
this.root.classList.remove('ql-hidden');
|
||
|
this.root.classList.add('ql-editing');
|
||
|
if (preview != null) {
|
||
|
this.textbox.value = preview;
|
||
|
} else if (mode !== this.root.getAttribute('data-mode')) {
|
||
|
this.textbox.value = '';
|
||
|
}
|
||
|
this.position(this.quill.getBounds(this.quill.selection.savedRange));
|
||
|
this.textbox.select();
|
||
|
this.textbox.setAttribute('placeholder', this.textbox.getAttribute(`data-${mode}`) || '');
|
||
|
this.root.setAttribute('data-mode', mode);
|
||
|
}
|
||
|
|
||
|
restoreFocus() {
|
||
|
let scrollTop = this.quill.scrollingContainer.scrollTop;
|
||
|
this.quill.focus();
|
||
|
this.quill.scrollingContainer.scrollTop = scrollTop;
|
||
|
}
|
||
|
|
||
|
save() {
|
||
|
let value = this.textbox.value;
|
||
|
switch(this.root.getAttribute('data-mode')) {
|
||
|
case 'link': {
|
||
|
let scrollTop = this.quill.root.scrollTop;
|
||
|
if (this.linkRange) {
|
||
|
this.quill.formatText(this.linkRange, 'link', value, Emitter.sources.USER);
|
||
|
delete this.linkRange;
|
||
|
} else {
|
||
|
this.restoreFocus();
|
||
|
this.quill.format('link', value, Emitter.sources.USER);
|
||
|
}
|
||
|
this.quill.root.scrollTop = scrollTop;
|
||
|
break;
|
||
|
}
|
||
|
case 'video': {
|
||
|
value = extractVideoUrl(value);
|
||
|
} // eslint-disable-next-line no-fallthrough
|
||
|
case 'formula': {
|
||
|
if (!value) break;
|
||
|
let range = this.quill.getSelection(true);
|
||
|
if (range != null) {
|
||
|
let index = range.index + range.length;
|
||
|
this.quill.insertEmbed(index, this.root.getAttribute('data-mode'), value, Emitter.sources.USER);
|
||
|
if (this.root.getAttribute('data-mode') === 'formula') {
|
||
|
this.quill.insertText(index + 1, ' ', Emitter.sources.USER);
|
||
|
}
|
||
|
this.quill.setSelection(index + 2, Emitter.sources.USER);
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
default:
|
||
|
}
|
||
|
this.textbox.value = '';
|
||
|
this.hide();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
function extractVideoUrl(url) {
|
||
|
let match = url.match(/^(?:(https?):\/\/)?(?:(?:www|m)\.)?youtube\.com\/watch.*v=([a-zA-Z0-9_-]+)/) ||
|
||
|
url.match(/^(?:(https?):\/\/)?(?:(?:www|m)\.)?youtu\.be\/([a-zA-Z0-9_-]+)/);
|
||
|
if (match) {
|
||
|
return (match[1] || 'https') + '://www.youtube.com/embed/' + match[2] + '?showinfo=0';
|
||
|
}
|
||
|
if (match = url.match(/^(?:(https?):\/\/)?(?:www\.)?vimeo\.com\/(\d+)/)) { // eslint-disable-line no-cond-assign
|
||
|
return (match[1] || 'https') + '://player.vimeo.com/video/' + match[2] + '/';
|
||
|
}
|
||
|
return url;
|
||
|
}
|
||
|
|
||
|
function fillSelect(select, values, defaultValue = false) {
|
||
|
values.forEach(function(value) {
|
||
|
let option = document.createElement('option');
|
||
|
if (value === defaultValue) {
|
||
|
option.setAttribute('selected', 'selected');
|
||
|
} else {
|
||
|
option.setAttribute('value', value);
|
||
|
}
|
||
|
select.appendChild(option);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
|
||
|
export { BaseTooltip, BaseTheme as default };
|