1 | import extend from 'extend';
|
2 | import Delta from 'quill-delta';
|
3 | import Emitter from '../core/emitter';
|
4 | import Keyboard from '../modules/keyboard';
|
5 | import Theme from '../core/theme';
|
6 | import ColorPicker from '../ui/color-picker';
|
7 | import IconPicker from '../ui/icon-picker';
|
8 | import Picker from '../ui/picker';
|
9 | import Tooltip from '../ui/tooltip';
|
10 |
|
11 |
|
12 | const ALIGNS = [ false, 'center', 'right', 'justify' ];
|
13 |
|
14 | const 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 |
|
22 | const FONTS = [ false, 'serif', 'monospace' ];
|
23 |
|
24 | const HEADERS = [ '1', '2', '3', false ];
|
25 |
|
26 | const SIZES = [ 'small', false, 'large', 'huge' ];
|
27 |
|
28 |
|
29 | class 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 | }
|
114 | BaseTheme.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 |
|
157 | class 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 | }
|
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 |
|
239 | function 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+)/)) {
|
246 | return (match[1] || 'https') + '://player.vimeo.com/video/' + match[2] + '/';
|
247 | }
|
248 | return url;
|
249 | }
|
250 |
|
251 | function 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 |
|
264 | export { BaseTooltip, BaseTheme as default };
|