diff --git a/app/templates/components/gh-koenig-editor.hbs b/app/templates/components/gh-koenig-editor.hbs index e7629ff940..19ed4e2bcb 100644 --- a/app/templates/components/gh-koenig-editor.hbs +++ b/app/templates/components/gh-koenig-editor.hbs @@ -21,5 +21,8 @@ onChange=(action "onBodyChange") didCreateEditor=(action "onEditorCreated") cursorDidExitAtTop=(action "focusTitle") + scrollContainerSelector=scrollContainerSelector + scrollOffsetTopSelector=scrollOffsetTopSelector + scrollOffsetBottomSelector=scrollOffsetBottomSelector }} diff --git a/app/templates/editor.hbs b/app/templates/editor.hbs index 5980ef4b72..0b386d9a64 100644 --- a/app/templates/editor.hbs +++ b/app/templates/editor.hbs @@ -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}} @@ -205,4 +208,4 @@ {{/liquid-wormhole}} {{/if}} -{{outlet}} +{{outlet}} \ No newline at end of file diff --git a/lib/koenig-editor/addon/components/koenig-editor.js b/lib/koenig-editor/addon/components/koenig-editor.js index 7174f034db..07ff0f1bac 100644 --- a/lib/koenig-editor/addon/components/koenig-editor.js +++ b/lib/koenig-editor/addon/components/koenig-editor.js @@ -117,7 +117,6 @@ export default Component.extend({ autofocus: false, spellcheck: true, options: null, - scrollContainer: '', headerOffset: 0, // internal properties @@ -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 || []; @@ -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; }, @@ -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 @@ -538,6 +550,7 @@ export default Component.extend({ if (this._skipCursorChange) { this._skipCursorChange = false; this.set('selectedRange', editor.range); + this._scrollCursorIntoView(); return; } @@ -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 @@ -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 @@ -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; + } + } + } } }); diff --git a/lib/koenig-editor/addon/options/key-commands.js b/lib/koenig-editor/addon/options/key-commands.js index 2274d52d2a..86e1f95e9b 100644 --- a/lib/koenig-editor/addon/options/key-commands.js +++ b/lib/koenig-editor/addon/options/key-commands.js @@ -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 @@ -20,6 +20,7 @@ export const DEFAULT_KEY_COMMANDS = [{ let collection = section.parent.sections; postEditor.insertSectionBefore(collection, newPara, section); }); + koenig._scrollCursorIntoView(); return; }