You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
			
				
					265 lines
				
				8.6 KiB
			
		
		
			
		
	
	
					265 lines
				
				8.6 KiB
			| 
								 
											4 years ago
										 
									 | 
							
								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 };
							 |