UNPKG

8.82 kBJavaScriptView Raw
1import extend from 'extend';
2import Delta from 'quill-delta';
3import Emitter from '../core/emitter';
4import Keyboard from '../modules/keyboard';
5import Theme from '../core/theme';
6import ColorPicker from '../ui/color-picker';
7import IconPicker from '../ui/icon-picker';
8import Picker from '../ui/picker';
9import Tooltip from '../ui/tooltip';
10
11
12const ALIGNS = [ false, 'center', 'right', 'justify' ];
13
14const COLORS = [
15 "#000000", "#e60000", "#ff9900", "#ffff00", "#008a00", "#0066cc", "#9933ff",
16 "#ffffff", "#facccc", "#ffebcc", "#ffffcc", "#cce8cc", "#cce0f5", "#ebd6ff",
17 "#bbbbbb", "#f06666", "#ffc266", "#ffff66", "#66b966", "#66a3e0", "#c285ff",
18 "#888888", "#a10000", "#b26b00", "#b2b200", "#006100", "#0047b2", "#6b24b2",
19 "#444444", "#5c0000", "#663d00", "#666600", "#003700", "#002966", "#3d1466"
20];
21
22const FONTS = [ false, 'serif', 'monospace' ];
23
24const HEADERS = [ '1', '2', '3', false ];
25
26const SIZES = [ 'small', false, 'large', 'huge' ];
27
28
29class BaseTheme extends Theme {
30 constructor(quill, options) {
31 super(quill, options);
32 let listener = (e) => {
33 if (!document.body.contains(quill.root)) {
34 return document.body.removeEventListener('click', listener);
35 }
36 if (this.tooltip != null && !this.tooltip.root.contains(e.target) &&
37 document.activeElement !== this.tooltip.textbox && !this.quill.hasFocus()) {
38 this.tooltip.hide();
39 }
40 if (this.pickers != null) {
41 this.pickers.forEach(function(picker) {
42 if (!picker.container.contains(e.target)) {
43 picker.close();
44 }
45 });
46 }
47 };
48 document.body.addEventListener('click', listener);
49 }
50
51 addModule(name) {
52 let module = super.addModule(name);
53 if (name === 'toolbar') {
54 this.extendToolbar(module);
55 }
56 return module;
57 }
58
59 buildButtons(buttons, icons) {
60 buttons.forEach((button) => {
61 let className = button.getAttribute('class') || '';
62 className.split(/\s+/).forEach((name) => {
63 if (!name.startsWith('ql-')) return;
64 name = name.slice('ql-'.length);
65 if (icons[name] == null) return;
66 if (name === 'direction') {
67 button.innerHTML = icons[name][''] + icons[name]['rtl'];
68 } else if (typeof icons[name] === 'string') {
69 button.innerHTML = icons[name];
70 } else {
71 let value = button.value || '';
72 if (value != null && icons[name][value]) {
73 button.innerHTML = icons[name][value];
74 }
75 }
76 });
77 });
78 }
79
80 buildPickers(selects, icons) {
81 this.pickers = selects.map((select) => {
82 if (select.classList.contains('ql-align')) {
83 if (select.querySelector('option') == null) {
84 fillSelect(select, ALIGNS);
85 }
86 return new IconPicker(select, icons.align);
87 } else if (select.classList.contains('ql-background') || select.classList.contains('ql-color')) {
88 let format = select.classList.contains('ql-background') ? 'background' : 'color';
89 if (select.querySelector('option') == null) {
90 fillSelect(select, COLORS, format === 'background' ? '#ffffff' : '#000000');
91 }
92 return new ColorPicker(select, icons[format]);
93 } else {
94 if (select.querySelector('option') == null) {
95 if (select.classList.contains('ql-font')) {
96 fillSelect(select, FONTS);
97 } else if (select.classList.contains('ql-header')) {
98 fillSelect(select, HEADERS);
99 } else if (select.classList.contains('ql-size')) {
100 fillSelect(select, SIZES);
101 }
102 }
103 return new Picker(select);
104 }
105 });
106 let update = () => {
107 this.pickers.forEach(function(picker) {
108 picker.update();
109 });
110 };
111 this.quill.on(Emitter.events.EDITOR_CHANGE, update);
112 }
113}
114BaseTheme.DEFAULTS = extend(true, {}, Theme.DEFAULTS, {
115 modules: {
116 toolbar: {
117 handlers: {
118 formula: function() {
119 this.quill.theme.tooltip.edit('formula');
120 },
121 image: function() {
122 let fileInput = this.container.querySelector('input.ql-image[type=file]');
123 if (fileInput == null) {
124 fileInput = document.createElement('input');
125 fileInput.setAttribute('type', 'file');
126 fileInput.setAttribute('accept', 'image/png, image/gif, image/jpeg, image/bmp, image/x-icon');
127 fileInput.classList.add('ql-image');
128 fileInput.addEventListener('change', () => {
129 if (fileInput.files != null && fileInput.files[0] != null) {
130 let reader = new FileReader();
131 reader.onload = (e) => {
132 let range = this.quill.getSelection(true);
133 this.quill.updateContents(new Delta()
134 .retain(range.index)
135 .delete(range.length)
136 .insert({ image: e.target.result })
137 , Emitter.sources.USER);
138 this.quill.setSelection(range.index + 1, Emitter.sources.SILENT);
139 fileInput.value = "";
140 }
141 reader.readAsDataURL(fileInput.files[0]);
142 }
143 });
144 this.container.appendChild(fileInput);
145 }
146 fileInput.click();
147 },
148 video: function() {
149 this.quill.theme.tooltip.edit('video');
150 }
151 }
152 }
153 }
154});
155
156
157class BaseTooltip extends Tooltip {
158 constructor(quill, boundsContainer) {
159 super(quill, boundsContainer);
160 this.textbox = this.root.querySelector('input[type="text"]');
161 this.listen();
162 }
163
164 listen() {
165 this.textbox.addEventListener('keydown', (event) => {
166 if (Keyboard.match(event, 'enter')) {
167 this.save();
168 event.preventDefault();
169 } else if (Keyboard.match(event, 'escape')) {
170 this.cancel();
171 event.preventDefault();
172 }
173 });
174 }
175
176 cancel() {
177 this.hide();
178 }
179
180 edit(mode = 'link', preview = null) {
181 this.root.classList.remove('ql-hidden');
182 this.root.classList.add('ql-editing');
183 if (preview != null) {
184 this.textbox.value = preview;
185 } else if (mode !== this.root.getAttribute('data-mode')) {
186 this.textbox.value = '';
187 }
188 this.position(this.quill.getBounds(this.quill.selection.savedRange));
189 this.textbox.select();
190 this.textbox.setAttribute('placeholder', this.textbox.getAttribute(`data-${mode}`) || '');
191 this.root.setAttribute('data-mode', mode);
192 }
193
194 restoreFocus() {
195 let scrollTop = this.quill.scrollingContainer.scrollTop;
196 this.quill.focus();
197 this.quill.scrollingContainer.scrollTop = scrollTop;
198 }
199
200 save() {
201 let value = this.textbox.value;
202 switch(this.root.getAttribute('data-mode')) {
203 case 'link': {
204 let scrollTop = this.quill.root.scrollTop;
205 if (this.linkRange) {
206 this.quill.formatText(this.linkRange, 'link', value, Emitter.sources.USER);
207 delete this.linkRange;
208 } else {
209 this.restoreFocus();
210 this.quill.format('link', value, Emitter.sources.USER);
211 }
212 this.quill.root.scrollTop = scrollTop;
213 break;
214 }
215 case 'video': {
216 value = extractVideoUrl(value);
217 } // eslint-disable-next-line no-fallthrough
218 case 'formula': {
219 if (!value) break;
220 let range = this.quill.getSelection(true);
221 if (range != null) {
222 let index = range.index + range.length;
223 this.quill.insertEmbed(index, this.root.getAttribute('data-mode'), value, Emitter.sources.USER);
224 if (this.root.getAttribute('data-mode') === 'formula') {
225 this.quill.insertText(index + 1, ' ', Emitter.sources.USER);
226 }
227 this.quill.setSelection(index + 2, Emitter.sources.USER);
228 }
229 break;
230 }
231 default:
232 }
233 this.textbox.value = '';
234 this.hide();
235 }
236}
237
238
239function extractVideoUrl(url) {
240 let match = url.match(/^(?:(https?):\/\/)?(?:(?:www|m)\.)?youtube\.com\/watch.*v=([a-zA-Z0-9_-]+)/) ||
241 url.match(/^(?:(https?):\/\/)?(?:(?:www|m)\.)?youtu\.be\/([a-zA-Z0-9_-]+)/);
242 if (match) {
243 return (match[1] || 'https') + '://www.youtube.com/embed/' + match[2] + '?showinfo=0';
244 }
245 if (match = url.match(/^(?:(https?):\/\/)?(?:www\.)?vimeo\.com\/(\d+)/)) { // eslint-disable-line no-cond-assign
246 return (match[1] || 'https') + '://player.vimeo.com/video/' + match[2] + '/';
247 }
248 return url;
249}
250
251function fillSelect(select, values, defaultValue = false) {
252 values.forEach(function(value) {
253 let option = document.createElement('option');
254 if (value === defaultValue) {
255 option.setAttribute('selected', 'selected');
256 } else {
257 option.setAttribute('value', value);
258 }
259 select.appendChild(option);
260 });
261}
262
263
264export { BaseTooltip, BaseTheme as default };