From 327c12e7337e646f1a77d4b8ca60c44d616987c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Tue, 12 Jun 2018 10:08:48 +0200 Subject: [PATCH] MOBILE-2430 rte: Improve rte buttons --- .../core-rich-text-editor.html | 26 +-- .../rich-text-editor/rich-text-editor.scss | 5 + .../rich-text-editor/rich-text-editor.ts | 165 ++++++++++++++++-- 3 files changed, 172 insertions(+), 24 deletions(-) diff --git a/src/components/rich-text-editor/core-rich-text-editor.html b/src/components/rich-text-editor/core-rich-text-editor.html index 14b4c5dde..6a38e8470 100644 --- a/src/components/rich-text-editor/core-rich-text-editor.html +++ b/src/components/rich-text-editor/core-rich-text-editor.html @@ -5,18 +5,18 @@
- - - - - - - - - - - - + + + + + + + + + + + +
@@ -25,7 +25,7 @@
- +
diff --git a/src/components/rich-text-editor/rich-text-editor.scss b/src/components/rich-text-editor/rich-text-editor.scss index bcd6e6391..53e3ddd8a 100644 --- a/src/components/rich-text-editor/rich-text-editor.scss +++ b/src/components/rich-text-editor/rich-text-editor.scss @@ -39,6 +39,11 @@ core-rich-text-editor { font-weight: bold; } + // Make empty elements selectable (to move the cursor). + *:empty:after { + content: '\200B'; + } + ul { list-style-type: disc; } diff --git a/src/components/rich-text-editor/rich-text-editor.ts b/src/components/rich-text-editor/rich-text-editor.ts index c082aba9b..1bfb1f0a3 100644 --- a/src/components/rich-text-editor/rich-text-editor.ts +++ b/src/components/rich-text-editor/rich-text-editor.ts @@ -88,6 +88,7 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy this.editorElement.onkeyup = this.onChange.bind(this); this.editorElement.onpaste = this.onChange.bind(this); this.editorElement.oninput = this.onChange.bind(this); + this.editorElement.onkeydown = this.moveCursor.bind(this); // Listen for changes on the control to update the editor (if it is updated from outside of this component). this.valueChangeSubscription = this.control.valueChanges.subscribe((param) => { @@ -116,17 +117,28 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy } } + // Use paragraph on enter. + document.execCommand('DefaultParagraphSeparator', false, 'p'); + this.treatExternalContent(); this.resizeFunction = this.maximizeEditorSize.bind(this); window.addEventListener('resize', this.resizeFunction); - setTimeout(this.resizeFunction, 1000); + + let i = 0; + const interval = setInterval(() => { + const height = this.maximizeEditorSize(); + if (i >= 5 || height != 0) { + clearInterval(interval); + } + i++; + }, 750); } /** * Resize editor to maximize the space occupied. */ - protected maximizeEditorSize(): void { + protected maximizeEditorSize(): number { this.content.resize(); const contentVisibleHeight = this.content.contentHeight; @@ -138,7 +150,11 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy } else { this.element.style.height = ''; } + + return contentVisibleHeight - height; } + + return 0; } /** @@ -195,6 +211,132 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy this.contentChanged.emit(this.control.value); } + /** + * On key down function to move the cursor. + * https://stackoverflow.com/questions/6249095/how-to-set-caretcursor-position-in-contenteditable-element-div + * + * @param {Event} $event The event. + */ + moveCursor($event: Event): void { + if (!this.rteEnabled) { + return; + } + + if ($event['key'] != 'ArrowLeft' && $event['key'] != 'ArrowRight') { + return; + } + + $event.preventDefault(); + $event.stopPropagation(); + + const move = $event['key'] == 'ArrowLeft' ? -1 : +1, + cursor = this.getCurrentCursorPosition(this.editorElement); + + this.setCurrentCursorPosition(this.editorElement, cursor + move); + } + + /** + * Returns the number of chars from the beggining where is placed the cursor. + * + * @param {Node} parent Parent where to get the position from. + * @return {number} Position in chars. + */ + protected getCurrentCursorPosition(parent: Node): number { + const selection = window.getSelection(); + + let charCount = -1, + node; + + if (selection.focusNode) { + if (parent.contains(selection.focusNode)) { + node = selection.focusNode; + charCount = selection.focusOffset; + + while (node) { + if (node.isSameNode(parent)) { + break; + } + + if (node.previousSibling) { + node = node.previousSibling; + charCount += node.textContent.length; + } else { + node = node.parentNode; + if (node === null) { + break; + } + } + } + } + } + + return charCount; + } + + /** + * Set the caret position on the character number. + * + * @param {Node} parent Parent where to set the position. + * @param {number} [chars] Number of chars where to place the caret. If not defined it will go to the end. + */ + protected setCurrentCursorPosition(parent: Node, chars?: number): void { + /** + * Loops round all the child text nodes within the supplied node and sets a range from the start of the initial node to + * the characters. + * + * @param {Node} node Node where to start. + * @param {Range} range Previous calculated range. + * @param {any} chars Object with counting of characters (input-output param). + * @return {Range} Selection range. + */ + const setRange = (node: Node, range: Range, chars: any): Range => { + if (chars.count === 0) { + range.setEnd(node, 0); + } else if (node && chars.count > 0) { + if (node.hasChildNodes()) { + // Navigate through children. + for (let lp = 0; lp < node.childNodes.length; lp++) { + range = setRange(node.childNodes[lp], range, chars); + + if (chars.count === 0) { + break; + } + } + } else if (node.textContent.length < chars.count) { + // Jump this node. + // @todo: empty nodes will be omitted. + chars.count -= node.textContent.length; + } else { + // The cursor will be placed in this element. + range.setEnd(node, chars.count); + chars.count = 0; + } + } + + return range; + }; + + let range = document.createRange(); + if (typeof chars === 'undefined') { + // Select all so it will go to the end. + range.selectNode(parent); + range.selectNodeContents(parent); + } else if (chars < 0 || chars > parent.textContent.length) { + return; + } else { + range.selectNode(parent); + range.setStart(parent, 0); + range = setRange(parent, range, {count: chars}); + } + + if (range) { + const selection = window.getSelection(); + range.collapse(false); + selection.removeAllRanges(); + selection.addRange(range); + } + } + /** * Toggle from rte editor to textarea syncing values. * @@ -204,7 +346,8 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy $event.preventDefault(); $event.stopPropagation(); - if (this.isNullOrWhiteSpace(this.control.value)) { + const isNull = this.isNullOrWhiteSpace(this.control.value); + if (isNull) { this.clearText(); } else { this.editorElement.innerHTML = this.control.value; @@ -217,14 +360,7 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy setTimeout(() => { if (this.rteEnabled) { this.editorElement.focus(); - - const range = document.createRange(); - range.selectNodeContents(this.editorElement); - range.collapse(false); - - const sel = window.getSelection(); - sel.removeAllRanges(); - sel.addRange(range); + this.setCurrentCursorPosition(this.editorElement.firstChild); } else { this.textarea.setFocus(); } @@ -279,8 +415,15 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy clearText(): void { this.editorElement.innerHTML = '

'; this.textarea.value = ''; + // Don't emit event so our valueChanges doesn't get notified by this change. this.control.setValue(null, {emitEvent: false}); + + setTimeout(() => { + if (this.rteEnabled) { + this.setCurrentCursorPosition(this.editorElement); + } + }, 1); } /**