2018-01-31 17:49:20 +03:00
|
|
|
import Component from '@ember/component';
|
|
|
|
import layout from '../templates/components/koenig-plus-menu';
|
|
|
|
import {computed} from '@ember/object';
|
|
|
|
import {htmlSafe} from '@ember/string';
|
|
|
|
import {run} from '@ember/runloop';
|
|
|
|
|
|
|
|
export default Component.extend({
|
|
|
|
layout,
|
|
|
|
|
|
|
|
// public attrs
|
|
|
|
classNames: 'koenig-plus-menu',
|
|
|
|
attributeBindings: ['style'],
|
|
|
|
editor: null,
|
|
|
|
editorRange: null,
|
|
|
|
|
|
|
|
// internal properties
|
|
|
|
showButton: false,
|
|
|
|
showMenu: false,
|
|
|
|
top: 0,
|
|
|
|
|
2018-02-01 19:48:16 +03:00
|
|
|
// private properties
|
|
|
|
_onResizeHandler: null,
|
|
|
|
_onWindowMousedownHandler: null,
|
2018-02-02 14:56:34 +03:00
|
|
|
_lastEditorRange: null,
|
|
|
|
_hasCursorButton: false,
|
|
|
|
_onMousemoveHandler: null,
|
2018-03-14 13:42:36 +03:00
|
|
|
_onKeydownHandler: null,
|
2018-02-01 19:48:16 +03:00
|
|
|
|
2018-02-02 18:22:37 +03:00
|
|
|
// closure actions
|
|
|
|
replaceWithCardSection() {},
|
|
|
|
|
2018-01-31 17:49:20 +03:00
|
|
|
style: computed('top', function () {
|
|
|
|
return htmlSafe(`top: ${this.get('top')}px`);
|
|
|
|
}),
|
|
|
|
|
2018-02-01 19:48:16 +03:00
|
|
|
init() {
|
|
|
|
this._super(...arguments);
|
|
|
|
|
|
|
|
this._onResizeHandler = run.bind(this, this._handleResize);
|
|
|
|
window.addEventListener('resize', this._onResizeHandler);
|
2018-02-02 14:56:34 +03:00
|
|
|
|
|
|
|
this._onMousemoveHandler = run.bind(this, this._mousemoveRaf);
|
|
|
|
window.addEventListener('mousemove', this._onMousemoveHandler);
|
2018-02-01 19:48:16 +03:00
|
|
|
},
|
|
|
|
|
2018-01-31 17:49:20 +03:00
|
|
|
didReceiveAttrs() {
|
|
|
|
this._super(...arguments);
|
|
|
|
|
2018-02-02 14:56:34 +03:00
|
|
|
let editorRange = this.get('editorRange');
|
2018-01-31 17:49:20 +03:00
|
|
|
|
2018-02-03 19:54:19 +03:00
|
|
|
// show the (+) button when the cursor is on a blank P tag
|
2018-02-02 14:56:34 +03:00
|
|
|
if (!this.get('showMenu') && editorRange !== this._lastEditorRange) {
|
|
|
|
this._showOrHideButton(editorRange);
|
|
|
|
this._hasCursorButton = this.get('showButton');
|
|
|
|
}
|
2018-01-31 17:49:20 +03:00
|
|
|
|
2018-02-02 14:56:34 +03:00
|
|
|
// 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);
|
2018-01-31 17:49:20 +03:00
|
|
|
}
|
2018-02-02 14:56:34 +03:00
|
|
|
|
|
|
|
this._lastEditorRange = editorRange;
|
2018-01-31 17:49:20 +03:00
|
|
|
},
|
|
|
|
|
|
|
|
willDestroyElement() {
|
|
|
|
this._super(...arguments);
|
2018-02-01 19:48:16 +03:00
|
|
|
run.cancel(this._throttleResize);
|
|
|
|
window.removeEventListener('mousedown', this._onWindowMousedownHandler);
|
2018-02-01 20:29:33 +03:00
|
|
|
window.removeEventListener('resize', this._onResizeHandler);
|
2018-02-02 14:56:34 +03:00
|
|
|
window.removeEventListener('mousemove', this._onMousemoveHandler);
|
2018-03-14 13:42:36 +03:00
|
|
|
window.removeEventListener('keydown', this._onKeydownHandler);
|
2018-01-31 17:49:20 +03:00
|
|
|
},
|
|
|
|
|
|
|
|
actions: {
|
|
|
|
openMenu() {
|
|
|
|
this._showMenu();
|
|
|
|
},
|
|
|
|
|
|
|
|
closeMenu() {
|
|
|
|
this._hideMenu();
|
|
|
|
},
|
|
|
|
|
|
|
|
replaceWithCardSection(cardName) {
|
|
|
|
let range = this._editorRange;
|
|
|
|
|
2018-02-02 18:22:37 +03:00
|
|
|
this.replaceWithCardSection(cardName, range);
|
2018-01-31 17:49:20 +03:00
|
|
|
|
2018-02-02 18:22:37 +03:00
|
|
|
this._hideButton();
|
|
|
|
this._hideMenu();
|
2018-01-31 17:49:20 +03:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2018-02-02 14:56:34 +03:00
|
|
|
_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();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2018-02-01 19:48:16 +03:00
|
|
|
_showButton() {
|
|
|
|
this._positionMenu();
|
|
|
|
this.set('showButton', true);
|
|
|
|
},
|
|
|
|
|
2018-02-02 14:56:34 +03:00
|
|
|
_hideButton() {
|
|
|
|
this.set('showButton', false);
|
|
|
|
},
|
|
|
|
|
2018-02-01 19:48:16 +03:00
|
|
|
// 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;
|
2018-04-13 18:42:20 +03:00
|
|
|
if (selectedElement) {
|
|
|
|
let selectedElementRect = selectedElement.getBoundingClientRect();
|
|
|
|
let top = selectedElementRect.top - containerRect.top;
|
2018-02-01 19:48:16 +03:00
|
|
|
|
2018-04-13 18:42:20 +03:00
|
|
|
this.set('top', top);
|
|
|
|
}
|
2018-02-01 19:48:16 +03:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2018-01-31 17:49:20 +03:00
|
|
|
_showMenu() {
|
|
|
|
this.set('showMenu', true);
|
|
|
|
|
2018-02-02 14:56:34 +03:00
|
|
|
// 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
|
2018-03-14 13:42:36 +03:00
|
|
|
this._moveCaretToCachedEditorRange();
|
2018-02-02 14:56:34 +03:00
|
|
|
|
2018-01-31 17:49:20 +03:00
|
|
|
// focus the search immediately so that you can filter immediately
|
|
|
|
run.schedule('afterRender', this, function () {
|
|
|
|
this._focusSearch();
|
|
|
|
});
|
|
|
|
|
|
|
|
// watch the window for mousedown events so that we can close the menu
|
|
|
|
// when we detect a click outside
|
2018-03-14 13:42:36 +03:00
|
|
|
this._onWindowMousedownHandler = run.bind(this, this._handleWindowMousedown);
|
2018-02-01 19:48:16 +03:00
|
|
|
window.addEventListener('mousedown', this._onWindowMousedownHandler);
|
2018-03-14 13:42:36 +03:00
|
|
|
|
|
|
|
// watch for keydown events so that we can close the mnu on Escape
|
|
|
|
this._onKeydownHandler = run.bind(this, this._handleKeydown);
|
|
|
|
window.addEventListener('keydown', this._onKeydownHandler);
|
2018-01-31 17:49:20 +03:00
|
|
|
},
|
|
|
|
|
|
|
|
_hideMenu() {
|
|
|
|
if (this.get('showMenu')) {
|
|
|
|
// reset our cached editorRange
|
|
|
|
this._editorRange = null;
|
|
|
|
|
2018-03-14 13:42:36 +03:00
|
|
|
// stop watching the body for clicks and keydown
|
2018-02-01 19:48:16 +03:00
|
|
|
window.removeEventListener('mousedown', this._onWindowMousedownHandler);
|
2018-03-14 13:42:36 +03:00
|
|
|
window.removeEventListener('keydown', this._onKeydownHandler);
|
2018-01-31 17:49:20 +03:00
|
|
|
|
|
|
|
// hide the menu
|
|
|
|
this.set('showMenu', false);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
_focusSearch() {
|
|
|
|
let search = this.element.querySelector('input');
|
|
|
|
if (search) {
|
|
|
|
search.focus();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2018-02-01 19:48:16 +03:00
|
|
|
_handleWindowMousedown(event) {
|
2018-01-31 17:49:20 +03:00
|
|
|
if (!event.target.closest(`#${this.elementId}`)) {
|
|
|
|
this._hideMenu();
|
|
|
|
}
|
2018-02-01 19:48:16 +03:00
|
|
|
},
|
|
|
|
|
2018-02-02 14:56:34 +03:00
|
|
|
_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;
|
|
|
|
},
|
|
|
|
|
2018-03-14 13:42:36 +03:00
|
|
|
_handleKeydown(event) {
|
|
|
|
if (event.code === 'Escape') {
|
|
|
|
// reset the caret position so we have a caret after closing
|
|
|
|
this._moveCaretToCachedEditorRange();
|
|
|
|
this._hideMenu();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2018-02-01 19:48:16 +03:00
|
|
|
_handleResize() {
|
|
|
|
if (this.get('showButton')) {
|
|
|
|
this._throttleResize = run.throttle(this, this._positionMenu, 100);
|
|
|
|
}
|
2018-03-14 13:42:36 +03:00
|
|
|
},
|
|
|
|
|
|
|
|
_moveCaretToCachedEditorRange() {
|
|
|
|
this.set('editorRange', this._editorRange);
|
|
|
|
this.get('editor').run((postEditor) => {
|
|
|
|
postEditor.setRange(this._editorRange);
|
|
|
|
});
|
2018-01-31 17:49:20 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
});
|