Skip to content

Commit

Permalink
✨ Koenig - Keep cursor on screen when typing or moving via keyboard
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
- for scroll-into-view at the bottom there are two slightly different methods
    - if the cursor is near the bottom of the document we scroll so that the bottom padding of the editor is visible
    - if the cursor is mid-document then we scroll just enough to bring the cursor into the viewport
  • Loading branch information
kevinansfield committed May 24, 2018
1 parent 0f8ef2b commit dd63cbf
Show file tree
Hide file tree
Showing 4 changed files with 107 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}}
101 changes: 98 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,73 @@ export default Component.extend({
if (this.element && config.environment === 'test') {
this.element[TESTING_EXPANDO_PROPERTY] = editor;
}
},

_scrollCursorIntoView() {
// 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._scrollContainer || 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) {
// cursorTop is relative to the window rather than document or scroll container
let {top: cursorTop, height: cursorHeight} = 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 ? topElement.offsetHeight : 0;
}

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

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

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

// keep cursor in view at the top
if (cursorTop < 0 + offsetTop) {
this._scrollContainer.scrollTop = scrollTop - offsetTop + cursorTop - 20;
return;
}

let cursorBottom = cursorTop + cursorHeight;
let paddingBottom = 0;
let distanceFromViewportBottom = cursorBottom - viewportHeight;
let atBottom = false;

// if we're at the bottom of the doc we should keep the bottom
// padding in view, otherwise just scroll to keep the cursor in view
if (this._scrollContainer.scrollTop + this._scrollContainer.offsetHeight + 200 >= this._scrollContainer.scrollHeight) {
atBottom = true;
paddingBottom = parseFloat(getComputedStyle(this.element.parentNode).getPropertyValue('padding-bottom'));
}

if (cursorBottom > viewportHeight - offsetBottom - paddingBottom) {
if (atBottom) {
this._scrollContainer.scrollTop = this._scrollContainer.scrollHeight;
} else {
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 dd63cbf

Please sign in to comment.