Initial commit as of 2018-10-16
This commit is contained in:
264
assets/js/themes/base.js
Normal file
264
assets/js/themes/base.js
Normal file
@@ -0,0 +1,264 @@
|
||||
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 };
|
111
assets/js/themes/bubble.js
Normal file
111
assets/js/themes/bubble.js
Normal file
@@ -0,0 +1,111 @@
|
||||
import extend from 'extend';
|
||||
import Emitter from '../core/emitter';
|
||||
import BaseTheme, { BaseTooltip } from './base';
|
||||
import { Range } from '../core/selection';
|
||||
import icons from '../ui/icons';
|
||||
|
||||
|
||||
const TOOLBAR_CONFIG = [
|
||||
['bold', 'italic', 'link'],
|
||||
[{ header: 1 }, { header: 2 }, 'blockquote']
|
||||
];
|
||||
|
||||
class BubbleTheme extends BaseTheme {
|
||||
constructor(quill, options) {
|
||||
if (options.modules.toolbar != null && options.modules.toolbar.container == null) {
|
||||
options.modules.toolbar.container = TOOLBAR_CONFIG;
|
||||
}
|
||||
super(quill, options);
|
||||
this.quill.container.classList.add('ql-bubble');
|
||||
}
|
||||
|
||||
extendToolbar(toolbar) {
|
||||
this.tooltip = new BubbleTooltip(this.quill, this.options.bounds);
|
||||
this.tooltip.root.appendChild(toolbar.container);
|
||||
this.buildButtons([].slice.call(toolbar.container.querySelectorAll('button')), icons);
|
||||
this.buildPickers([].slice.call(toolbar.container.querySelectorAll('select')), icons);
|
||||
}
|
||||
}
|
||||
BubbleTheme.DEFAULTS = extend(true, {}, BaseTheme.DEFAULTS, {
|
||||
modules: {
|
||||
toolbar: {
|
||||
handlers: {
|
||||
link: function(value) {
|
||||
if (!value) {
|
||||
this.quill.format('link', false);
|
||||
} else {
|
||||
this.quill.theme.tooltip.edit();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
class BubbleTooltip extends BaseTooltip {
|
||||
constructor(quill, bounds) {
|
||||
super(quill, bounds);
|
||||
this.quill.on(Emitter.events.EDITOR_CHANGE, (type, range, oldRange, source) => {
|
||||
if (type !== Emitter.events.SELECTION_CHANGE) return;
|
||||
if (range != null && range.length > 0 && source === Emitter.sources.USER) {
|
||||
this.show();
|
||||
// Lock our width so we will expand beyond our offsetParent boundaries
|
||||
this.root.style.left = '0px';
|
||||
this.root.style.width = '';
|
||||
this.root.style.width = this.root.offsetWidth + 'px';
|
||||
let lines = this.quill.getLines(range.index, range.length);
|
||||
if (lines.length === 1) {
|
||||
this.position(this.quill.getBounds(range));
|
||||
} else {
|
||||
let lastLine = lines[lines.length - 1];
|
||||
let index = this.quill.getIndex(lastLine);
|
||||
let length = Math.min(lastLine.length() - 1, range.index + range.length - index);
|
||||
let bounds = this.quill.getBounds(new Range(index, length));
|
||||
this.position(bounds);
|
||||
}
|
||||
} else if (document.activeElement !== this.textbox && this.quill.hasFocus()) {
|
||||
this.hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
listen() {
|
||||
super.listen();
|
||||
this.root.querySelector('.ql-close').addEventListener('click', () => {
|
||||
this.root.classList.remove('ql-editing');
|
||||
});
|
||||
this.quill.on(Emitter.events.SCROLL_OPTIMIZE, () => {
|
||||
// Let selection be restored by toolbar handlers before repositioning
|
||||
setTimeout(() => {
|
||||
if (this.root.classList.contains('ql-hidden')) return;
|
||||
let range = this.quill.getSelection();
|
||||
if (range != null) {
|
||||
this.position(this.quill.getBounds(range));
|
||||
}
|
||||
}, 1);
|
||||
});
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.show();
|
||||
}
|
||||
|
||||
position(reference) {
|
||||
let shift = super.position(reference);
|
||||
let arrow = this.root.querySelector('.ql-tooltip-arrow');
|
||||
arrow.style.marginLeft = '';
|
||||
if (shift === 0) return shift;
|
||||
arrow.style.marginLeft = (-1*shift - arrow.offsetWidth/2) + 'px';
|
||||
}
|
||||
}
|
||||
BubbleTooltip.TEMPLATE = [
|
||||
'<span class="ql-tooltip-arrow"></span>',
|
||||
'<div class="ql-tooltip-editor">',
|
||||
'<input type="text" data-formula="e=mc^2" data-link="https://quilljs.com" data-video="Embed URL">',
|
||||
'<a class="ql-close"></a>',
|
||||
'</div>'
|
||||
].join('');
|
||||
|
||||
|
||||
export { BubbleTooltip, BubbleTheme as default };
|
120
assets/js/themes/snow.js
Normal file
120
assets/js/themes/snow.js
Normal file
@@ -0,0 +1,120 @@
|
||||
import extend from 'extend';
|
||||
import Emitter from '../core/emitter';
|
||||
import BaseTheme, { BaseTooltip } from './base';
|
||||
import LinkBlot from '../formats/link';
|
||||
import { Range } from '../core/selection';
|
||||
import icons from '../ui/icons';
|
||||
|
||||
|
||||
const TOOLBAR_CONFIG = [
|
||||
[{ header: ['1', '2', '3', false] }],
|
||||
['bold', 'italic', 'underline', 'link'],
|
||||
[{ list: 'ordered' }, { list: 'bullet' }],
|
||||
['clean']
|
||||
];
|
||||
|
||||
class SnowTheme extends BaseTheme {
|
||||
constructor(quill, options) {
|
||||
if (options.modules.toolbar != null && options.modules.toolbar.container == null) {
|
||||
options.modules.toolbar.container = TOOLBAR_CONFIG;
|
||||
}
|
||||
super(quill, options);
|
||||
this.quill.container.classList.add('ql-snow');
|
||||
}
|
||||
|
||||
extendToolbar(toolbar) {
|
||||
toolbar.container.classList.add('ql-snow');
|
||||
this.buildButtons([].slice.call(toolbar.container.querySelectorAll('button')), icons);
|
||||
this.buildPickers([].slice.call(toolbar.container.querySelectorAll('select')), icons);
|
||||
this.tooltip = new SnowTooltip(this.quill, this.options.bounds);
|
||||
if (toolbar.container.querySelector('.ql-link')) {
|
||||
this.quill.keyboard.addBinding({ key: 'K', shortKey: true }, function(range, context) {
|
||||
toolbar.handlers['link'].call(toolbar, !context.format.link);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
SnowTheme.DEFAULTS = extend(true, {}, BaseTheme.DEFAULTS, {
|
||||
modules: {
|
||||
toolbar: {
|
||||
handlers: {
|
||||
link: function(value) {
|
||||
if (value) {
|
||||
let range = this.quill.getSelection();
|
||||
if (range == null || range.length == 0) return;
|
||||
let preview = this.quill.getText(range);
|
||||
if (/^\S+@\S+\.\S+$/.test(preview) && preview.indexOf('mailto:') !== 0) {
|
||||
preview = 'mailto:' + preview;
|
||||
}
|
||||
let tooltip = this.quill.theme.tooltip;
|
||||
tooltip.edit('link', preview);
|
||||
} else {
|
||||
this.quill.format('link', false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
class SnowTooltip extends BaseTooltip {
|
||||
constructor(quill, bounds) {
|
||||
super(quill, bounds);
|
||||
this.preview = this.root.querySelector('a.ql-preview');
|
||||
}
|
||||
|
||||
listen() {
|
||||
super.listen();
|
||||
this.root.querySelector('a.ql-action').addEventListener('click', (event) => {
|
||||
if (this.root.classList.contains('ql-editing')) {
|
||||
this.save();
|
||||
} else {
|
||||
this.edit('link', this.preview.textContent);
|
||||
}
|
||||
event.preventDefault();
|
||||
});
|
||||
this.root.querySelector('a.ql-remove').addEventListener('click', (event) => {
|
||||
if (this.linkRange != null) {
|
||||
let range = this.linkRange;
|
||||
this.restoreFocus();
|
||||
this.quill.formatText(range, 'link', false, Emitter.sources.USER);
|
||||
delete this.linkRange;
|
||||
}
|
||||
event.preventDefault();
|
||||
this.hide();
|
||||
});
|
||||
this.quill.on(Emitter.events.SELECTION_CHANGE, (range, oldRange, source) => {
|
||||
if (range == null) return;
|
||||
if (range.length === 0 && source === Emitter.sources.USER) {
|
||||
let [link, offset] = this.quill.scroll.descendant(LinkBlot, range.index);
|
||||
if (link != null) {
|
||||
this.linkRange = new Range(range.index - offset, link.length());
|
||||
let preview = LinkBlot.formats(link.domNode);
|
||||
this.preview.textContent = preview;
|
||||
this.preview.setAttribute('href', preview);
|
||||
this.show();
|
||||
this.position(this.quill.getBounds(this.linkRange));
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
delete this.linkRange;
|
||||
}
|
||||
this.hide();
|
||||
});
|
||||
}
|
||||
|
||||
show() {
|
||||
super.show();
|
||||
this.root.removeAttribute('data-mode');
|
||||
}
|
||||
}
|
||||
SnowTooltip.TEMPLATE = [
|
||||
'<a class="ql-preview" target="_blank" href="about:blank"></a>',
|
||||
'<input type="text" data-formula="e=mc^2" data-link="https://quilljs.com" data-video="Embed URL">',
|
||||
'<a class="ql-action"></a>',
|
||||
'<a class="ql-remove"></a>'
|
||||
].join('');
|
||||
|
||||
|
||||
export default SnowTheme;
|
Reference in New Issue
Block a user