mirror of
https://github.com/TryGhost/Ghost-Admin.git
synced 2023-12-14 02:33:04 +01:00
Koenig - Re-position toolbar and plus menu on window resize
refs https://github.com/TryGhost/Ghost/issues/9311 - extract positioning routines into methods - throttle positioning method calls on window resizes
This commit is contained in:
parent
3a200a8b0d
commit
60a1a781e7
2 changed files with 145 additions and 97 deletions
|
@ -4,16 +4,6 @@ import {computed} from '@ember/object';
|
|||
import {htmlSafe} from '@ember/string';
|
||||
import {run} from '@ember/runloop';
|
||||
|
||||
// clicking on anything in the menu will change the selection because the click
|
||||
// event propagates, this then closes the menu
|
||||
|
||||
// focusing the search input removes the selection in the editor, again closing
|
||||
// the menu
|
||||
|
||||
// when the menu is open we want to:
|
||||
// - close if clicked outside the menu
|
||||
// - keep the selected range around in case it gets changed
|
||||
|
||||
export default Component.extend({
|
||||
layout,
|
||||
|
||||
|
@ -28,10 +18,21 @@ export default Component.extend({
|
|||
showMenu: false,
|
||||
top: 0,
|
||||
|
||||
// private properties
|
||||
_onResizeHandler: null,
|
||||
_onWindowMousedownHandler: null,
|
||||
|
||||
style: computed('top', function () {
|
||||
return htmlSafe(`top: ${this.get('top')}px`);
|
||||
}),
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
|
||||
this._onResizeHandler = run.bind(this, this._handleResize);
|
||||
window.addEventListener('resize', this._onResizeHandler);
|
||||
},
|
||||
|
||||
didReceiveAttrs() {
|
||||
this._super(...arguments);
|
||||
|
||||
|
@ -48,16 +49,7 @@ export default Component.extend({
|
|||
|
||||
// show the button if the cursor is at the beginning of a blank paragraph
|
||||
if (editorRange && editorRange.isCollapsed && section && !section.isListItem && (section.isBlank || section.text === '')) {
|
||||
// find the "top" position by grabbing the current sections
|
||||
// render node and querying it's bounding rect. Setting "top"
|
||||
// positions the button+menu container element .koenig-plus-menu
|
||||
let containerRect = this.element.parentNode.getBoundingClientRect();
|
||||
let selectedElement = editorRange.head.section.renderNode.element;
|
||||
let selectedElementRect = selectedElement.getBoundingClientRect();
|
||||
let top = selectedElementRect.top - containerRect.top;
|
||||
|
||||
this.set('top', top);
|
||||
this.set('showButton', true);
|
||||
this._showButton();
|
||||
this._hideMenu();
|
||||
} else {
|
||||
this.set('showButton', false);
|
||||
|
@ -68,7 +60,9 @@ export default Component.extend({
|
|||
|
||||
willDestroyElement() {
|
||||
this._super(...arguments);
|
||||
window.removeEventListener('mousedown', this._bodyMousedownHandler);
|
||||
run.cancel(this._throttleResize);
|
||||
window.removeEventListener('mousedown', this._onWindowMousedownHandler);
|
||||
window.removeEventListener('resize', this.this._onResizeHandler);
|
||||
},
|
||||
|
||||
actions: {
|
||||
|
@ -119,6 +113,29 @@ export default Component.extend({
|
|||
}
|
||||
},
|
||||
|
||||
_showButton() {
|
||||
this._positionMenu();
|
||||
this.set('showButton', true);
|
||||
},
|
||||
|
||||
// find the "top" position by grabbing the current sections
|
||||
// render node and querying it's bounding rect. Setting "top"
|
||||
// positions the button+menu container element .koenig-plus-menu
|
||||
_positionMenu() {
|
||||
// use the cached range if available because `editorRange` may have been
|
||||
// lost due to clicks on the open menu
|
||||
let {head: {section}} = this._editorRange || this.get('editorRange');
|
||||
|
||||
if (section) {
|
||||
let containerRect = this.element.parentNode.getBoundingClientRect();
|
||||
let selectedElement = section.renderNode.element;
|
||||
let selectedElementRect = selectedElement.getBoundingClientRect();
|
||||
let top = selectedElementRect.top - containerRect.top;
|
||||
|
||||
this.set('top', top);
|
||||
}
|
||||
},
|
||||
|
||||
_showMenu() {
|
||||
this.set('showMenu', true);
|
||||
|
||||
|
@ -129,10 +146,10 @@ export default Component.extend({
|
|||
|
||||
// watch the window for mousedown events so that we can close the menu
|
||||
// when we detect a click outside
|
||||
this._bodyMousedownHandler = run.bind(this, (event) => {
|
||||
this._handleBodyMousedown(event);
|
||||
this._onWindowMousedownHandler = run.bind(this, (event) => {
|
||||
this._handleWindowMousedown(event);
|
||||
});
|
||||
window.addEventListener('mousedown', this._bodyMousedownHandler);
|
||||
window.addEventListener('mousedown', this._onWindowMousedownHandler);
|
||||
|
||||
// store a reference to our range because it will change underneath
|
||||
// us as editor focus is lost
|
||||
|
@ -145,7 +162,7 @@ export default Component.extend({
|
|||
this._editorRange = null;
|
||||
|
||||
// stop watching the body for clicks
|
||||
window.removeEventListener('mousedown', this._bodyMousedownHandler);
|
||||
window.removeEventListener('mousedown', this._onWindowMousedownHandler);
|
||||
|
||||
// hide the menu
|
||||
this.set('showMenu', false);
|
||||
|
@ -159,10 +176,16 @@ export default Component.extend({
|
|||
}
|
||||
},
|
||||
|
||||
_handleBodyMousedown(event) {
|
||||
_handleWindowMousedown(event) {
|
||||
if (!event.target.closest(`#${this.elementId}`)) {
|
||||
this._hideMenu();
|
||||
}
|
||||
},
|
||||
|
||||
_handleResize() {
|
||||
if (this.get('showButton')) {
|
||||
this._throttleResize = run.throttle(this, this._positionMenu, 100);
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
|
|
@ -2,6 +2,7 @@ import Component from '@ember/component';
|
|||
import layout from '../templates/components/koenig-toolbar';
|
||||
import {computed} from '@ember/object';
|
||||
import {htmlSafe} from '@ember/string';
|
||||
import {run} from '@ember/runloop';
|
||||
import {task, timeout} from 'ember-concurrency';
|
||||
|
||||
// initially rendered offscreen with opacity 0 so that sizing is available
|
||||
|
@ -31,9 +32,10 @@ export default Component.extend({
|
|||
|
||||
// private properties
|
||||
_isMouseDown: false,
|
||||
_onMousedownHandler: false,
|
||||
_onMouseupHandler: false,
|
||||
_hasSelectedRange: false,
|
||||
_onMousedownHandler: null,
|
||||
_onMouseupHandler: null,
|
||||
_onResizeHandler: null,
|
||||
|
||||
/* computed properties -------------------------------------------------- */
|
||||
|
||||
|
@ -56,20 +58,12 @@ export default Component.extend({
|
|||
// track mousedown/mouseup on the window so that we're sure to get the
|
||||
// events even when they start outside of this component or end outside
|
||||
// the window
|
||||
this._onMousedownHandler = (event) => {
|
||||
// we only care about the left mouse button
|
||||
if (event.which === 1) {
|
||||
this._isMouseDown = true;
|
||||
}
|
||||
};
|
||||
this._onMouseupHandler = (event) => {
|
||||
if (event.which === 1) {
|
||||
this._isMouseDown = false;
|
||||
this.get('_toggleVisibility').perform();
|
||||
}
|
||||
};
|
||||
this._onMousedownHandler = run.bind(this, this._handleMousedown);
|
||||
window.addEventListener('mousedown', this._onMousedownHandler);
|
||||
this._onMouseupHandler = run.bind(this, this._handleMouseup);
|
||||
window.addEventListener('mouseup', this._onMouseupHandler);
|
||||
this._onResizeHandler = run.bind(this, this._handleResize);
|
||||
window.addEventListener('resize', this._onResizeHandler);
|
||||
},
|
||||
|
||||
didReceiveAttrs() {
|
||||
|
@ -88,8 +82,10 @@ export default Component.extend({
|
|||
willDestroyElement() {
|
||||
this._super(...arguments);
|
||||
this._removeStyleElement();
|
||||
run.cancel(this._throttleResize);
|
||||
window.removeEventListener('mousedown', this._onMousedownHandler);
|
||||
window.removeEventListener('mouseup', this._onMouseupHandler);
|
||||
window.removeEventListener('resize', this._onResizeHandler);
|
||||
},
|
||||
|
||||
_toggleVisibility: task(function* () {
|
||||
|
@ -107,67 +103,96 @@ export default Component.extend({
|
|||
|
||||
// if we have a range, show the toolbnar once the mouse is lifted
|
||||
if (this._hasSelectedRange && !this._isMouseDown) {
|
||||
let containerRect = this.element.parentNode.getBoundingClientRect();
|
||||
let range = window.getSelection().getRangeAt(0);
|
||||
let rangeRect = range.getBoundingClientRect();
|
||||
let {width, height} = this.element.getBoundingClientRect();
|
||||
let newPosition = {};
|
||||
|
||||
// rangeRect is relative to the viewport so we need to subtract the
|
||||
// container measurements to get a position relative to the container
|
||||
newPosition = {
|
||||
top: rangeRect.top - containerRect.top - height - TOOLBAR_TOP_MARGIN,
|
||||
left: rangeRect.left - containerRect.left + rangeRect.width / 2 - width / 2,
|
||||
right: null
|
||||
};
|
||||
|
||||
let tickPosition = 50;
|
||||
// don't overflow left boundary
|
||||
if (newPosition.left < 0) {
|
||||
newPosition.left = 0;
|
||||
|
||||
// calculate the tick percentage position
|
||||
let absTickPosition = rangeRect.left - containerRect.left + rangeRect.width / 2;
|
||||
tickPosition = absTickPosition / width * 100;
|
||||
if (tickPosition < 5) {
|
||||
tickPosition = 5;
|
||||
}
|
||||
}
|
||||
// same for right boundary
|
||||
if (newPosition.left + width > containerRect.width) {
|
||||
newPosition.left = null;
|
||||
newPosition.right = 0;
|
||||
|
||||
// calculate the tick percentage position
|
||||
let absTickPosition = rangeRect.right - containerRect.right - rangeRect.width / 2;
|
||||
tickPosition = 100 + absTickPosition / width * 100;
|
||||
if (tickPosition > 95) {
|
||||
tickPosition = 95;
|
||||
}
|
||||
}
|
||||
|
||||
// the tick is a pseudo-element so we the only way we can affect it's
|
||||
// style is by adding a style element to the head
|
||||
this._removeStyleElement(); // reset to base styles
|
||||
if (tickPosition !== 50) {
|
||||
this._addStyleElement(`left: ${tickPosition}%`);
|
||||
}
|
||||
|
||||
// update the toolbar position and show it
|
||||
this.setProperties(newPosition);
|
||||
|
||||
// show the toolbar
|
||||
this.set('showToolbar', true);
|
||||
|
||||
// track displayed range so that we don't re-position unnecessarily
|
||||
this._lastRange = this.get('editorRange');
|
||||
this._showToolbar();
|
||||
} else {
|
||||
// hide the toolbar
|
||||
this.set('showToolbar', false);
|
||||
this._lastRange = null;
|
||||
this._hideToolbar();
|
||||
}
|
||||
}).restartable(),
|
||||
|
||||
_handleMousedown(event) {
|
||||
// we only care about the left mouse button
|
||||
if (event.which === 1) {
|
||||
this._isMouseDown = true;
|
||||
}
|
||||
},
|
||||
|
||||
_handleMouseup(event) {
|
||||
if (event.which === 1) {
|
||||
this._isMouseDown = false;
|
||||
this.get('_toggleVisibility').perform();
|
||||
}
|
||||
},
|
||||
|
||||
_handleResize() {
|
||||
if (this.get('showToolbar')) {
|
||||
this._throttleResize = run.throttle(this, this._positionToolbar, 100);
|
||||
}
|
||||
},
|
||||
|
||||
_showToolbar() {
|
||||
this._positionToolbar();
|
||||
this.set('showToolbar', true);
|
||||
|
||||
// track displayed range so that we don't re-position unnecessarily
|
||||
this._lastRange = this.get('editorRange');
|
||||
},
|
||||
|
||||
_hideToolbar() {
|
||||
this.set('showToolbar', false);
|
||||
this._lastRange = null;
|
||||
},
|
||||
|
||||
_positionToolbar() {
|
||||
let containerRect = this.element.parentNode.getBoundingClientRect();
|
||||
let range = window.getSelection().getRangeAt(0);
|
||||
let rangeRect = range.getBoundingClientRect();
|
||||
let {width, height} = this.element.getBoundingClientRect();
|
||||
let newPosition = {};
|
||||
|
||||
// rangeRect is relative to the viewport so we need to subtract the
|
||||
// container measurements to get a position relative to the container
|
||||
newPosition = {
|
||||
top: rangeRect.top - containerRect.top - height - TOOLBAR_TOP_MARGIN,
|
||||
left: rangeRect.left - containerRect.left + rangeRect.width / 2 - width / 2,
|
||||
right: null
|
||||
};
|
||||
|
||||
let tickPosition = 50;
|
||||
// don't overflow left boundary
|
||||
if (newPosition.left < 0) {
|
||||
newPosition.left = 0;
|
||||
|
||||
// calculate the tick percentage position
|
||||
let absTickPosition = rangeRect.left - containerRect.left + rangeRect.width / 2;
|
||||
tickPosition = absTickPosition / width * 100;
|
||||
if (tickPosition < 5) {
|
||||
tickPosition = 5;
|
||||
}
|
||||
}
|
||||
// same for right boundary
|
||||
if (newPosition.left + width > containerRect.width) {
|
||||
newPosition.left = null;
|
||||
newPosition.right = 0;
|
||||
|
||||
// calculate the tick percentage position
|
||||
let absTickPosition = rangeRect.right - containerRect.right - rangeRect.width / 2;
|
||||
tickPosition = 100 + absTickPosition / width * 100;
|
||||
if (tickPosition > 95) {
|
||||
tickPosition = 95;
|
||||
}
|
||||
}
|
||||
|
||||
// the tick is a pseudo-element so we the only way we can affect it's
|
||||
// style is by adding a style element to the head
|
||||
this._removeStyleElement(); // reset to base styles
|
||||
if (tickPosition !== 50) {
|
||||
this._addStyleElement(`left: ${tickPosition}%`);
|
||||
}
|
||||
|
||||
// update the toolbar position
|
||||
this.setProperties(newPosition);
|
||||
},
|
||||
|
||||
_addStyleElement(styles) {
|
||||
let styleElement = document.createElement('style');
|
||||
styleElement.id = `${this.elementId}-style`;
|
||||
|
|
Loading…
Reference in a new issue