diff --git a/ghost/admin/lib/koenig-editor/addon/components/koenig-editor.js b/ghost/admin/lib/koenig-editor/addon/components/koenig-editor.js index 8fb59c5d00..a4b99c22a3 100644 --- a/ghost/admin/lib/koenig-editor/addon/components/koenig-editor.js +++ b/ghost/admin/lib/koenig-editor/addon/components/koenig-editor.js @@ -174,6 +174,7 @@ export default Component.extend({ // create a new editor let editorOptions = this.get('editorOptions'); editorOptions.mobiledoc = mobiledoc; + editorOptions.showLinkTooltips = false; let componentHooks = { // triggered when a card section is added to the mobiledoc diff --git a/ghost/admin/lib/koenig-editor/addon/components/koenig-link-toolbar.js b/ghost/admin/lib/koenig-editor/addon/components/koenig-link-toolbar.js new file mode 100644 index 0000000000..e6d8a92eac --- /dev/null +++ b/ghost/admin/lib/koenig-editor/addon/components/koenig-link-toolbar.js @@ -0,0 +1,233 @@ +import Component from '@ember/component'; +import layout from '../templates/components/koenig-link-toolbar'; +// import {TOOLBAR_MARGIN} from './koenig-toolbar'; +import {computed} from '@ember/object'; +import {getEventTargetMatchingTag} from 'mobiledoc-kit/utils/element-utils'; +import {htmlSafe} from '@ember/string'; +import {run} from '@ember/runloop'; + +// gap between link and toolbar bottom +const TOOLBAR_MARGIN = 8; +// extra padding to reduce the likelyhood of unexpected hiding +// TODO: improve behaviour with a mouseout timeout or creating a bounding box +// and watching mousemove +const TOOLBAR_PADDING = 12; + +// ms to wait before showing the tooltip +const DELAY = 200; + +export default Component.extend({ + layout, + + attributeBindings: ['style'], + classNames: ['absolute', 'z-999'], + + // public attrs + container: null, + editor: null, + linkRange: null, + selectedRange: null, + + // internal attrs + url: 'http://example.com', + showToolbar: false, + top: null, + left: -1000, + right: null, + + // private attrs + _canShowToolbar: true, + _eventListeners: null, + + // closure actions + editLink() {}, + + /* computed properties -------------------------------------------------- */ + + style: computed('showToolbar', 'top', 'left', 'right', function () { + let position = this.getProperties('top', 'left', 'right'); + let styles = Object.keys(position).map((style) => { + if (position[style] !== null) { + return `${style}: ${position[style]}px`; + } + }); + + // ensure hidden toolbar is non-interactive + if (this.get('showToolbar')) { + styles.push('pointer-events: auto !important'); + // add margin-bottom so that there's no gap between the link and + // the toolbar to avoid closing when mouse moves between elements + styles.push(`padding-bottom: ${TOOLBAR_PADDING}px`); + } else { + styles.push('pointer-events: none !important'); + } + + return htmlSafe(styles.compact().join('; ')); + }), + + /* lifecycle hooks ------------------------------------------------------ */ + + init() { + this._super(...arguments); + this._eventListeners = []; + }, + + didReceiveAttrs() { + this._super(...arguments); + + // don't show popups if link edit or formatting toolbar is shown + // TODO: have a service for managing UI state? + if (this.get('linkRange') || (this.get('selectedRange') && !this.get('selectedRange').isCollapsed)) { + this._cancelTimeouts(); + this.set('showToolbar', false); + this._canShowToolbar = false; + } else { + this._canShowToolbar = true; + } + }, + + didInsertElement() { + this._super(...arguments); + + let container = this.get('container'); + this._addEventListener(container, 'mouseover', this._handleMouseover); + this._addEventListener(container, 'mouseout', this._handleMouseout); + }, + + willDestroyElement() { + this._super(...arguments); + this._removeAllEventListeners(); + }, + + /* actions -------------------------------------------------------------- */ + + actions: { + edit() { + // get range that covers link + let linkRange = this._getLinkRange(); + this.editLink(linkRange); + }, + + remove() { + let editor = this.get('editor'); + let linkRange = this._getLinkRange(); + let editorRange = editor.range; + editor.run((postEditor) => { + postEditor.toggleMarkup('a', linkRange); + }); + this.set('showToolbar', false); + editor.selectRange(editorRange); + } + }, + + /* private methods ------------------------------------------------------ */ + + _getLinkRange() { + if (!this._target) { + return; + } + + let editor = this.get('editor'); + let rect = this._target.getBoundingClientRect(); + let x = rect.x + rect.width / 2; + let y = rect.y + rect.height / 2; + let position = editor.positionAtPoint(x, y); + let linkMarkup = position.marker && position.marker.markups.findBy('tagName', 'a'); + if (linkMarkup) { + let linkRange = position.toRange().expandByMarker(marker => !!marker.markups.includes(linkMarkup)); + return linkRange; + } + }, + + _handleMouseover(event) { + if (this._canShowToolbar) { + let target = getEventTargetMatchingTag('a', event.target, this.get('container')); + if (target && target.isContentEditable) { + this._timeout = run.later(this, function () { + this._showToolbar(target); + }, DELAY); + } + } + }, + + _handleMouseout(event) { + this._cancelTimeouts(); + + if (this.get('showToolbar')) { + let toElement = event.toElement || event.relatedTarget; + if (toElement && !(toElement === this.element || toElement === this._target || toElement.closest(`#${this.elementId}`))) { + this.set('showToolbar', false); + } + } + }, + + _showToolbar(target) { + this._target = target; + this.set('url', target.href); + this.set('showToolbar', true); + run.schedule('afterRender', this, function () { + this._positionToolbar(target); + }); + }, + + _cancelTimeouts() { + run.cancel(this._timeout); + if (this._elementObserver) { + this._elementObserver.cancel(); + } + }, + + _positionToolbar(target) { + let containerRect = this.element.parentNode.getBoundingClientRect(); + let targetRect = target.getBoundingClientRect(); + let {width, height} = this.element.getBoundingClientRect(); + let newPosition = {}; + + // targetRect is relative to the viewport so we need to subtract the + // container measurements to get a position relative to the container + newPosition = { + top: targetRect.top - containerRect.top - height - TOOLBAR_MARGIN + TOOLBAR_PADDING, + left: targetRect.left - containerRect.left + targetRect.width / 2 - width / 2, + right: null + }; + + // don't overflow left boundary + if (newPosition.left < 0) { + newPosition.left = 0; + } + // same for right boundary + if (newPosition.left + width > containerRect.width) { + newPosition.left = null; + newPosition.right = 0; + } + + // update the toolbar position + this.setProperties(newPosition); + }, + + _addStyleElement(styles) { + let styleElement = document.createElement('style'); + styleElement.id = `${this.elementId}-style`; + styleElement.innerHTML = `#${this.elementId} > ul:after { ${styles} }`; + document.head.appendChild(styleElement); + }, + + _removeStyleElement() { + let styleElement = document.querySelector(`#${this.elementId}-style`); + if (styleElement) { + styleElement.remove(); + } + }, + + _addEventListener(element, type, listener) { + let boundListener = run.bind(this, listener); + element.addEventListener(type, boundListener); + this._eventListeners.push([element, type, boundListener]); + }, + + _removeAllEventListeners() { + this._eventListeners.forEach(([element, type, listener]) => { + element.removeEventListener(type, listener); + }); + } +}); diff --git a/ghost/admin/lib/koenig-editor/addon/templates/components/koenig-editor.hbs b/ghost/admin/lib/koenig-editor/addon/templates/components/koenig-editor.hbs index a606f6029b..97f46d6074 100644 --- a/ghost/admin/lib/koenig-editor/addon/templates/components/koenig-editor.hbs +++ b/ghost/admin/lib/koenig-editor/addon/templates/components/koenig-editor.hbs @@ -13,6 +13,15 @@ editLink=(action "editLink") }} +{{!-- pop-up link hover toolbar --}} +{{koenig-link-toolbar + editor=editor + container=element + linkRange=linkRange + selectedRange=selectedRange + editLink=(action "editLink") +}} + {{!-- pop-up link editing toolbar --}} {{#if linkRange}} {{koenig-link-input diff --git a/ghost/admin/lib/koenig-editor/addon/templates/components/koenig-link-toolbar.hbs b/ghost/admin/lib/koenig-editor/addon/templates/components/koenig-link-toolbar.hbs new file mode 100644 index 0000000000..7c71cb940c --- /dev/null +++ b/ghost/admin/lib/koenig-editor/addon/templates/components/koenig-link-toolbar.hbs @@ -0,0 +1,29 @@ +{{#if showToolbar}} +