0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-02-24 23:48:13 -05:00

Koenig - Show (+) button on blank paragraph mouseover

refs https://github.com/TryGhost/Ghost/issues/9311
- add a mousemove event handler that shows the (+) next to blank paragraphs when the pointer is over them
- fix sticky button when adding a card mid-document by hiding it, we get another `didReceiveAttrs` call with the new range when adding a blank paragraph so it's still shown in that situation
- fix the incorrect button position when adding a card at the bottom of the doc by re-positioning in the next runloop. Problem seems to stem from the component card being rendered after we get the new range so our position calculations are out of sync
This commit is contained in:
Kevin Ansfield 2018-02-02 12:56:34 +01:00
parent 605b2f3f19
commit f65c87a829

View file

@ -21,6 +21,9 @@ export default Component.extend({
// private properties // private properties
_onResizeHandler: null, _onResizeHandler: null,
_onWindowMousedownHandler: null, _onWindowMousedownHandler: null,
_lastEditorRange: null,
_hasCursorButton: false,
_onMousemoveHandler: null,
style: computed('top', function () { style: computed('top', function () {
return htmlSafe(`top: ${this.get('top')}px`); return htmlSafe(`top: ${this.get('top')}px`);
@ -31,31 +34,29 @@ export default Component.extend({
this._onResizeHandler = run.bind(this, this._handleResize); this._onResizeHandler = run.bind(this, this._handleResize);
window.addEventListener('resize', this._onResizeHandler); window.addEventListener('resize', this._onResizeHandler);
this._onMousemoveHandler = run.bind(this, this._mousemoveRaf);
window.addEventListener('mousemove', this._onMousemoveHandler);
}, },
didReceiveAttrs() { didReceiveAttrs() {
this._super(...arguments); this._super(...arguments);
if (!this.get('showMenu')) { let editorRange = this.get('editorRange');
let editorRange = this.get('editorRange');
if (!editorRange) { // show the (+) button when the cursor as on a blank P tag
this.set('showButton', false); if (!this.get('showMenu') && editorRange !== this._lastEditorRange) {
this._hideMenu(); this._showOrHideButton(editorRange);
return; this._hasCursorButton = this.get('showButton');
}
let {head: {section}} = editorRange;
// 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 === '')) {
this._showButton();
this._hideMenu();
} else {
this.set('showButton', false);
this._hideMenu();
}
} }
// re-position again on next runloop, prevents incorrect position after
// adding a card at the bottom of the doc
if (this.get('showButton')) {
run.next(this, this._positionMenu);
}
this._lastEditorRange = editorRange;
}, },
willDestroyElement() { willDestroyElement() {
@ -63,6 +64,7 @@ export default Component.extend({
run.cancel(this._throttleResize); run.cancel(this._throttleResize);
window.removeEventListener('mousedown', this._onWindowMousedownHandler); window.removeEventListener('mousedown', this._onWindowMousedownHandler);
window.removeEventListener('resize', this._onResizeHandler); window.removeEventListener('resize', this._onResizeHandler);
window.removeEventListener('mousemove', this._onMousemoveHandler);
}, },
actions: { actions: {
@ -92,6 +94,7 @@ export default Component.extend({
postEditor.setRange(newSection.tailPosition()); postEditor.setRange(newSection.tailPosition());
} }
this._hideButton();
this._hideMenu(); this._hideMenu();
}); });
}, },
@ -113,11 +116,35 @@ export default Component.extend({
} }
}, },
_showOrHideButton(editorRange) {
if (!editorRange) {
this._hideButton();
this._hideMenu();
return;
}
let {head: {section}} = editorRange;
// show the button if the range is a blank paragraph
if (editorRange && editorRange.isCollapsed && section && !section.isListItem && (section.isBlank || section.text === '')) {
this._editorRange = editorRange;
this._showButton();
this._hideMenu();
} else {
this._hideButton();
this._hideMenu();
}
},
_showButton() { _showButton() {
this._positionMenu(); this._positionMenu();
this.set('showButton', true); this.set('showButton', true);
}, },
_hideButton() {
this.set('showButton', false);
},
// find the "top" position by grabbing the current sections // find the "top" position by grabbing the current sections
// render node and querying it's bounding rect. Setting "top" // render node and querying it's bounding rect. Setting "top"
// positions the button+menu container element .koenig-plus-menu // positions the button+menu container element .koenig-plus-menu
@ -139,6 +166,15 @@ export default Component.extend({
_showMenu() { _showMenu() {
this.set('showMenu', true); this.set('showMenu', true);
// move the cursor to the blank paragraph, ensures any selected card
// gets inserted in the correct place because editorRange will be
// wherever the cursor currently is if the menu was opened via a
// mouseover button
this.set('editorRange', this._editorRange);
this.get('editor').run((postEditor) => {
postEditor.setRange(this._editorRange);
});
// focus the search immediately so that you can filter immediately // focus the search immediately so that you can filter immediately
run.schedule('afterRender', this, function () { run.schedule('afterRender', this, function () {
this._focusSearch(); this._focusSearch();
@ -150,10 +186,6 @@ export default Component.extend({
this._handleWindowMousedown(event); this._handleWindowMousedown(event);
}); });
window.addEventListener('mousedown', this._onWindowMousedownHandler); window.addEventListener('mousedown', this._onWindowMousedownHandler);
// store a reference to our range because it will change underneath
// us as editor focus is lost
this._editorRange = this.get('editorRange');
}, },
_hideMenu() { _hideMenu() {
@ -182,6 +214,46 @@ export default Component.extend({
} }
}, },
_mousemoveRaf(event) {
if (!this._mousemoveTicking) {
requestAnimationFrame(run.bind(this, this._handleMousemove, event));
}
this._mousemoveTicking = true;
},
// show the (+) button when the mouse is over a blank P tag
_handleMousemove(event) {
if (!this.get('showMenu')) {
let {pageX, pageY} = event;
let editor = this.get('editor');
// add a horizontal buffer to the pointer position so that the
// (+) button doesn't disappear when the mouse hovers over it due
// to it being outside of the editor canvas
let containerRect = this.element.parentNode.getBoundingClientRect();
if (pageX < containerRect.left) {
pageX = pageX + 40;
}
// grab a range from the editor position under the pointer. We can
// rely on the same show/hide behaviour of our cursor implementation
let position = editor.positionAtPoint(pageX, pageY);
if (position) {
let pointerRange = position.toRange();
this._showOrHideButton(pointerRange);
}
// if the button is hidden due to the pointer not being over a blank
// P but we have a valid cursor position then fall back to the cursor
// positioning
if (!this.get('showButton') && this._hasCursorButton) {
this._showOrHideButton(this.get('editorRange'));
}
}
this._mousemoveTicking = false;
},
_handleResize() { _handleResize() {
if (this.get('showButton')) { if (this.get('showButton')) {
this._throttleResize = run.throttle(this, this._positionMenu, 100); this._throttleResize = run.throttle(this, this._positionMenu, 100);