Skip to content

Commit

Permalink
Koenig - Keep cursor on screen
Browse files Browse the repository at this point in the history
refs TryGhost/Ghost#9505
- when cursor changes through the normal `cursorDidChange` or through certain programmatic changes we trigger a check to see if the cursor is out of the viewport and scroll it into view if necessary
- disable our scroll-into-view routine if the mouse button or shift key is down so that we don't interfere with default browser behaviour which works well in that situation
  • Loading branch information
kevinansfield committed May 24, 2018
1 parent 0f8ef2b commit 41720b4
Show file tree
Hide file tree
Showing 4 changed files with 98 additions and 5 deletions.
3 changes: 3 additions & 0 deletions app/templates/components/gh-koenig-editor.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,8 @@
onChange=(action "onBodyChange")
didCreateEditor=(action "onEditorCreated")
cursorDidExitAtTop=(action "focusTitle")
scrollContainerSelector=scrollContainerSelector
scrollOffsetTopSelector=scrollOffsetTopSelector
scrollOffsetBottomSelector=scrollOffsetBottomSelector
}}
</div>
5 changes: 4 additions & 1 deletion app/templates/editor.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@
bodyAutofocus=shouldFocusEditor
onBodyChange=(action "updateScratch")
headerOffset=editor.headerHeight
scrollContainerSelector=".gh-koenig-editor"
scrollOffsetTopSelector=".gh-editor-header-small"
scrollOffsetBottomSelector=".gh-mobile-nav-bar"
}}

{{else}}
Expand Down Expand Up @@ -205,4 +208,4 @@
{{/liquid-wormhole}}
{{/if}}

{{outlet}}
{{outlet}}
92 changes: 89 additions & 3 deletions lib/koenig-editor/addon/components/koenig-editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,6 @@ export default Component.extend({
autofocus: false,
spellcheck: true,
options: null,
scrollContainer: '',
headerOffset: 0,

// internal properties
Expand Down Expand Up @@ -146,8 +145,7 @@ export default Component.extend({

/* computed properties -------------------------------------------------- */

// merge in named options with the `options` property data-bag
// TODO: what is the `options` property data-bag and when/where does it get set?
// merge in named options with any passed in `options` property data-bag
editorOptions: computed(function () {
let options = this.options || {};
let atoms = this.atoms || [];
Expand Down Expand Up @@ -190,6 +188,16 @@ export default Component.extend({
ctrl: false
};

// track mousedown/mouseup on the window rather than the ember component
// so that we're sure to get the events even when they start outside of
// this component or end outside the window.
// Mouse events are used to track when a mousebutton is down so that we
// can disable automatic cursor-in-viewport scrolling
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._startedRunLoop = false;
},

Expand Down Expand Up @@ -346,6 +354,10 @@ export default Component.extend({

this._pasteHandler = run.bind(this, this.handlePaste);
editorElement.addEventListener('paste', this._pasteHandler);

if (this.scrollContainerSelector) {
this._scrollContainer = document.querySelector(this.scrollContainerSelector);
}
},

// our ember component has rendered, now we need to render the mobiledoc
Expand Down Expand Up @@ -538,6 +550,7 @@ export default Component.extend({
if (this._skipCursorChange) {
this._skipCursorChange = false;
this.set('selectedRange', editor.range);
this._scrollCursorIntoView();
return;
}

Expand Down Expand Up @@ -584,6 +597,7 @@ export default Component.extend({

// pass the selected range through to the toolbar + menu components
this.set('selectedRange', editor.range);
this._scrollCursorIntoView();
},

// fired when the active section(s) or markup(s) at the current cursor
Expand Down Expand Up @@ -711,6 +725,19 @@ export default Component.extend({
}
},

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;
}
},

/* Ember event handlers ------------------------------------------------- */

// disable dragging
Expand Down Expand Up @@ -879,5 +906,64 @@ export default Component.extend({
if (this.element && config.environment === 'test') {
this.element[TESTING_EXPANDO_PROPERTY] = editor;
}
},

_scrollCursorIntoView() {
if (!this._scrollContainer) {
return;
}

// getting the bounding rects can be expensive so we want to throttle
run.throttle(this, this.__scrollIntoView, 200);
},

__scrollIntoView() {
// disable auto-scroll if the mouse or shift key is being used to create
// a selection - the browser handles scrolling well in this case
if (this._isMouseDown || this._modifierKeys.shift) {
return;
}

let {range} = this.editor;
let selection = window.getSelection();
let windowRange = selection && selection.getRangeAt(0);
let element = range.head && range.head.section && range.head.section.renderNode && range.head.section.renderNode.element;

if (windowRange) {
let {top, height} = windowRange.getBoundingClientRect();
let viewportHeight = window.innerHeight;
let offsetTop = 0;
let offsetBottom = 0;
let scrollTop = this._scrollContainer.scrollTop;

if (this.scrollOffsetTopSelector) {
let topElement = document.querySelector(this.scrollOffsetTopSelector);
offsetTop = topElement.offsetHeight;
}

if (this.scrollOffsetBottomSelector) {
let bottomElement = document.querySelector(this.scrollOffsetBottomSelector);
offsetBottom = bottomElement.offsetHeight;
}

// for empty paragraphs the window selection range will be 0,0,0,0
// so grab the element's bounding rect instead
if (top === 0 && height === 0) {
if (!element) {
return;
}

({top, height} = element.getBoundingClientRect());
}

let bottom = top + height;
let distanceFromViewportBottom = bottom - viewportHeight;

if (top < 0 + offsetTop) {
this._scrollContainer.scrollTop = scrollTop - offsetTop + top - 20;
} else if (bottom > viewportHeight - offsetBottom) {
this._scrollContainer.scrollTop = scrollTop + offsetBottom + distanceFromViewportBottom + 20;
}
}
}
});
3 changes: 2 additions & 1 deletion lib/koenig-editor/addon/options/key-commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {

export const DEFAULT_KEY_COMMANDS = [{
str: 'ENTER',
run(editor) {
run(editor, koenig) {
let {isCollapsed, head: {offset, section}} = editor.range;

// if cursor is at beginning of a heading, insert a blank paragraph above
Expand All @@ -20,6 +20,7 @@ export const DEFAULT_KEY_COMMANDS = [{
let collection = section.parent.sections;
postEditor.insertSectionBefore(collection, newPara, section);
});
koenig._scrollCursorIntoView();
return;
}

Expand Down

0 comments on commit 41720b4

Please sign in to comment.