diff --git a/quill.js b/quill.js index 932f964700..7c91196883 100644 --- a/quill.js +++ b/quill.js @@ -34,7 +34,6 @@ import Picker from './ui/picker'; import ColorPicker from './ui/color-picker'; import IconPicker from './ui/icon-picker'; import Tooltip from './ui/tooltip'; -import LinkTooltip from './ui/link-tooltip'; import BubbleTheme from './themes/bubble'; import SnowTheme from './themes/snow'; @@ -91,8 +90,7 @@ Quill.register({ 'ui/picker': Picker, 'ui/icon-picker': IconPicker, 'ui/color-picker': ColorPicker, - 'ui/tooltip': Tooltip, - 'ui/link-tooltip': LinkTooltip + 'ui/tooltip': Tooltip }, true); diff --git a/themes/bubble.js b/themes/bubble.js index d3d256628e..55b140f14b 100644 --- a/themes/bubble.js +++ b/themes/bubble.js @@ -6,128 +6,114 @@ import { Range } from '../core/selection'; import Tooltip from '../ui/tooltip'; -class BubbleTooltip extends Tooltip { - constructor(root, containers) { - super(root, containers); - this.root.innerHTML = [ - '', - '' - ].join(''); - this.input = this.root.querySelector('input'); - this.listen(); +class BubbleTheme extends BaseTheme { + constructor(quill, options) { + super(quill, options); + this.quill.container.classList.add('ql-bubble'); } - listen() { - ['mousedown', 'touchstart'].forEach((name) => { - this.root.querySelector('.ql-close').addEventListener(name, (event) => { - this.root.classList.remove('ql-editing'); - event.preventDefault(); - }); - }); + 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'))); + this.buildPickers([].slice.call(toolbar.container.querySelectorAll('select'))); } - - position(reference) { - let shift = super.position(reference); - if (shift === 0) return shift; - let arrow = this.root.querySelector('.ql-tooltip-arrow'); - arrow.style.marginLeft = ''; - arrow.style.marginLeft = (-1*shift - arrow.offsetWidth/2) + 'px'; +} +BubbleTheme.DEFAULTS = { + modules: { + toolbar: { + container: [ + ['bold', 'italic', 'link'], + [{ header: 1 }, { header: 2 }, 'blockquote'] + ], + handlers: { + link: function(value) { + if (!value) { + this.quill.format('link', false); + } else { + this.quill.theme.tooltip.edit(); + } + } + } + } } } -class BubbleTheme extends BaseTheme { - constructor(quill, options) { - super(quill, options); - this.quill.container.classList.add('ql-bubble'); - } - - buildLinkEditor(input) { +class BubbleTooltip extends Tooltip { + constructor(quill, bounds) { + super(quill, bounds); this.quill.on(Emitter.events.SELECTION_CHANGE, (range) => { if (range != null && range.length > 0) { - this.tooltip.root.classList.remove('ql-editing'); - this.tooltip.show(); + this.show(); // Lock our width so we will expand beyond our offsetParent boundaries - this.tooltip.root.style.left = '0px'; - this.tooltip.root.style.width = ''; - this.tooltip.root.style.width = this.tooltip.root.offsetWidth + 'px'; + this.root.style.left = '0px'; + this.root.style.width = ''; + this.root.style.width = this.root.offsetWidth + 'px'; let lines = this.quill.scroll.lines(range.index, range.length); if (lines.length === 1) { - this.tooltip.position(this.quill.getBounds(range)); + this.position(this.quill.getBounds(range)); } else { let lastLine = lines[lines.length - 1]; let index = lastLine.offset(this.quill.scroll); let length = Math.min(lastLine.length() - 1, range.index + range.length - index); let bounds = this.quill.getBounds(new Range(index, length)); - this.tooltip.position(bounds); + this.position(bounds); } - } else if (document.activeElement !== input && !!this.quill.hasFocus()) { - this.tooltip.hide(); + } else if (document.activeElement !== this.textbox && this.quill.hasFocus()) { + this.hide(); } }); + } + + listen() { + super.listen(); + ['mousedown', 'touchstart'].forEach((name) => { + this.root.querySelector('.ql-close').addEventListener(name, (event) => { + this.root.classList.remove('ql-editing'); + event.preventDefault(); + }); + }); this.quill.on(Emitter.events.SCROLL_OPTIMIZE, () => { // Let selection be restored by toolbar handlers before repositioning setTimeout(() => { - if (this.tooltip.root.classList.contains('ql-hidden')) return; + if (this.root.classList.contains('ql-hidden')) return; let range = this.quill.getSelection(); if (range != null) { - this.tooltip.position(this.quill.getBounds(range)); + this.position(this.quill.getBounds(range)); } }, 1); }); - input.addEventListener('keydown', (event) => { - if (Keyboard.match(event, 'enter')) { - let scrollTop = this.quill.root.scrollTop; - this.quill.focus(); - this.quill.root.scrollTop = scrollTop; - this.quill.format('link', input.value); - this.tooltip.hide(); - input.value = ''; - event.preventDefault(); - } else if (Keyboard.match(event, 'escape')) { - this.tooltip.root.classList.remove('ql-editing'); - event.preventDefault(); - } - }); } - extendToolbar(toolbar) { - let container = this.quill.addContainer('ql-tooltip', this.quill.root); - this.tooltip = new BubbleTooltip(container, { - bounds: this.options.bounds, - scroll: this.quill.root - }); - this.buildLinkEditor(container.querySelector('input')); - container.appendChild(toolbar.container); - this.buildButtons([].slice.call(toolbar.container.querySelectorAll('button'))); - this.buildPickers([].slice.call(toolbar.container.querySelectorAll('select'))); - this.tooltip.hide(); + cancel() { + this.show(); } -} -BubbleTheme.DEFAULTS = { - modules: { - toolbar: { - container: [ - ['bold', 'italic', 'link'], - [{ header: 1 }, { header: 2 }, 'blockquote'] - ], - handlers: { - link: function(value) { - if (!value) { - this.quill.format('link', false); - } else { - let tooltip = this.quill.theme.tooltip; - tooltip.root.classList.add('ql-editing'); - tooltip.input.focus(); - } - } - } - } + + position(reference) { + let shift = super.position(reference); + if (shift === 0) return shift; + let arrow = this.root.querySelector('.ql-tooltip-arrow'); + arrow.style.marginLeft = ''; + arrow.style.marginLeft = (-1*shift - arrow.offsetWidth/2) + 'px'; + } + + save() { + let scrollTop = this.quill.root.scrollTop; + this.quill.focus(); + this.quill.root.scrollTop = scrollTop; + this.quill.format('link', this.textbox.value); + this.hide(); + this.textbox.value = ''; } } +BubbleTooltip.TEMPLATE = [ + '', + '' +].join(''); export default BubbleTheme; diff --git a/themes/snow.js b/themes/snow.js index 125702f7e1..5517431f3a 100644 --- a/themes/snow.js +++ b/themes/snow.js @@ -1,7 +1,9 @@ import Emitter from '../core/emitter'; import BaseTheme from './base'; -import LinkTooltip from '../ui/link-tooltip'; +import LinkBlot from '../formats/link'; import Picker from '../ui/picker'; +import { Range } from '../core/selection'; +import Tooltip from '../ui/tooltip'; class SnowTheme extends BaseTheme { @@ -15,7 +17,7 @@ class SnowTheme extends BaseTheme { this.buildButtons([].slice.call(toolbar.container.querySelectorAll('button'))); this.buildPickers([].slice.call(toolbar.container.querySelectorAll('select'))); if (toolbar.container.querySelector('.ql-link')) { - this.linkTooltip = new LinkTooltip(this.quill); + this.tooltip = new SnowTooltip(this.quill); this.quill.keyboard.addBinding({ key: 'K', shortKey: true }, function(range, context) { toolbar.handlers['link'].call(toolbar, !context.format.link); }); @@ -35,7 +37,7 @@ SnowTheme.DEFAULTS = { link: function(value) { if (value) { let savedRange = this.quill.selection.savedRange; - this.quill.theme.linkTooltip.open(savedRange); + this.quill.theme.tooltip.open(savedRange); } else { this.quill.format('link', false); } @@ -46,4 +48,106 @@ SnowTheme.DEFAULTS = { } +class SnowTooltip extends Tooltip { + constructor(quill, bounds) { + super(quill, bounds); + this.tooltip = new Tooltip(this.container, { + bounds: quill.theme.options.bounds, + scroll: quill.root + }); + this.hide(); + this.preview = this.container.querySelector('a.ql-preview'); + this.textbox = this.container.querySelector('input[type=text]'); + this.textbox.addEventListener('keydown', (event) => { + if (Keyboard.match(event, 'enter')) { + this.save(); + event.preventDefault(); + } else if (Keyboard.match(event, 'escape')) { + this.hide(); + event.preventDefault(); + } + }); + this.container.querySelector('a.ql-action').addEventListener('click', (event) => { + if (this.container.classList.contains('ql-editing')) { + this.save(); + event.preventDefault(); + } else { + this.edit(); + event.preventDefault(); + } + }); + this.container.querySelector('a.ql-remove').addEventListener('click', (event) => { + this.remove(); + event.preventDefault(); + }); + quill.on(Emitter.events.SELECTION_CHANGE, (range) => { + if (range == null) return; + if (range.length === 0) { + let offset; + [this.link, offset] = this.quill.scroll.descendant(LinkBlot, range.index); + if (this.link != null) { + this.range = new Range(range.index - offset, this.link.length()); + return this.show(); + } + } + this.hide(); + }); + } + + edit() { + this.container.classList.add('ql-editing'); + this.textbox.focus(); + this.textbox.setSelectionRange(0, this.textbox.value.length); + } + + open() { + this.range = new Range(this.quill.selection.savedRange.index, this.quill.selection.savedRange.length); + this.show(); + this.edit(); + } + + hide() { + this.range = this.link = null; + this.tooltip.hide(); + } + + remove() { + this.quill.formatText(this.range, 'link', false, Emitter.sources.USER); + this.hide(); + } + + save() { + let url = this.textbox.value; + let scrollTop = this.quill.root.scrollTop; + this.quill.formatText(this.range, 'link', url, Emitter.sources.USER); + this.quill.root.scrollTop = scrollTop; + this.hide(); + } + + show() { + this.container.classList.remove('ql-editing'); + this.tooltip.show(); + let preview, bounds; + let range = this.quill.selection.savedRange; + if (this.link != null) { + preview = this.link.formats()['link']; + } else { + preview = this.quill.getText(range); + if (/^\S+@\S+\.\S+$/.test(preview)) { + preview = 'mailto:' + preview; + } + } + this.preview.textContent = this.textbox.value = preview; + this.preview.setAttribute('href', preview); + this.tooltip.position(this.quill.getBounds(this.range)); + } +} +SnowTooltip.TEMPLATE = [ + '', + '', + '', + '' +].join(''); + + export default SnowTheme; diff --git a/ui/link-tooltip.js b/ui/link-tooltip.js deleted file mode 100644 index 22484b5464..0000000000 --- a/ui/link-tooltip.js +++ /dev/null @@ -1,112 +0,0 @@ -import Quill from '../core/quill'; -import { Range } from '../core/selection'; -import Keyboard from '../modules/keyboard'; -import LinkBlot from '../formats/link'; -import Tooltip from './tooltip'; - - -class LinkTooltip { - constructor(quill) { - this.quill = quill; - this.container = this.quill.addContainer('ql-link-tooltip'); - this.container.innerHTML = this.constructor.TEMPLATE; - this.tooltip = new Tooltip(this.container, { - bounds: quill.theme.options.bounds, - scroll: quill.root - }); - this.hide(); - this.preview = this.container.querySelector('a.ql-preview'); - this.textbox = this.container.querySelector('input[type=text]'); - this.textbox.addEventListener('keydown', (event) => { - if (Keyboard.match(event, 'enter')) { - this.save(); - event.preventDefault(); - } else if (Keyboard.match(event, 'escape')) { - this.hide(); - event.preventDefault(); - } - }); - this.container.querySelector('a.ql-action').addEventListener('click', (event) => { - if (this.container.classList.contains('ql-editing')) { - this.save(); - event.preventDefault(); - } else { - this.edit(); - event.preventDefault(); - } - }); - this.container.querySelector('a.ql-remove').addEventListener('click', (event) => { - this.remove(); - event.preventDefault(); - }); - quill.on(Quill.events.SELECTION_CHANGE, (range) => { - if (range == null) return; - if (range.length === 0) { - let offset; - [this.link, offset] = this.quill.scroll.descendant(LinkBlot, range.index); - if (this.link != null) { - this.range = new Range(range.index - offset, this.link.length()); - return this.show(); - } - } - this.hide(); - }); - } - - edit() { - this.container.classList.add('ql-editing'); - this.textbox.focus(); - this.textbox.setSelectionRange(0, this.textbox.value.length); - } - - open() { - this.range = new Range(this.quill.selection.savedRange.index, this.quill.selection.savedRange.length); - this.show(); - this.edit(); - } - - hide() { - this.range = this.link = null; - this.tooltip.hide(); - } - - remove() { - this.quill.formatText(this.range, 'link', false, Quill.sources.USER); - this.hide(); - } - - save() { - let url = this.textbox.value; - let scrollTop = this.quill.root.scrollTop; - this.quill.formatText(this.range, 'link', url, Quill.sources.USER); - this.quill.root.scrollTop = scrollTop; - this.hide(); - } - - show() { - this.container.classList.remove('ql-editing'); - this.tooltip.show(); - let preview, bounds; - let range = this.quill.selection.savedRange; - if (this.link != null) { - preview = this.link.formats()['link']; - } else { - preview = this.quill.getText(range); - if (/^\S+@\S+\.\S+$/.test(preview)) { - preview = 'mailto:' + preview; - } - } - this.preview.textContent = this.textbox.value = preview; - this.preview.setAttribute('href', preview); - this.tooltip.position(this.quill.getBounds(this.range)); - } -} -LinkTooltip.TEMPLATE = [ - '', - '', - '', - '' -].join(''); - - -export default LinkTooltip; diff --git a/ui/tooltip.js b/ui/tooltip.js index 577a560fec..2e3c3fe0c7 100644 --- a/ui/tooltip.js +++ b/ui/tooltip.js @@ -1,26 +1,39 @@ +import Keyboard from '../modules/keyboard'; + + class Tooltip { - constructor(root, containers = {}) { - this.containers = containers; - this.root = root; - this.root.classList.add('ql-tooltip'); - if (this.containers.scroll instanceof HTMLElement) { - let offset = parseInt(window.getComputedStyle(this.root).marginTop); - this.containers.scroll.addEventListener('scroll', () => { - this.root.style.marginTop = (-1*this.containers.scroll.scrollTop) + offset + 'px'; - }); - } + constructor(quill, boundsContainer) { + this.quill = quill; + this.boundsContainer = boundsContainer; + this.root = quill.addContainer('ql-tooltip'); + this.root.innerHTML = this.constructor.TEMPLATE; + let offset = parseInt(window.getComputedStyle(this.root).marginTop); + this.quill.root.addEventListener('scroll', () => { + this.root.style.marginTop = (-1*this.quill.root.scrollTop) + offset + 'px'; + }); + this.textbox = this.root.querySelector('input[type="text"]'); + this.listen(); + this.hide(); + } + + 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(); + } + }); } position(reference) { let left = reference.left + reference.width/2 - this.root.offsetWidth/2; - let top = reference.bottom; - if (this.containers.scroll instanceof HTMLElement) { - top += this.containers.scroll.scrollTop; - } + let top = reference.bottom + this.quill.root.scrollTop; this.root.style.left = left + 'px'; this.root.style.top = top + 'px'; - if (!(this.containers.bounds instanceof HTMLElement)) return; - let containerBounds = this.containers.bounds.getBoundingClientRect(); + let containerBounds = this.boundsContainer.getBoundingClientRect(); let rootBounds = this.root.getBoundingClientRect(); let shift = 0; if (rootBounds.right > containerBounds.right) { @@ -34,7 +47,22 @@ class Tooltip { return shift; } + edit() { + this.root.classList.remove('ql-hidden'); + this.root.classList.add('ql-editing'); + this.textbox.focus(); + } + + save() { + + } + + cancel() { + + } + show() { + this.root.classList.remove('ql-editing'); this.root.classList.remove('ql-hidden'); }