From 61a477afd516bbf72b4221fb08b8f5cbc9230bf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Thu, 18 Jan 2018 16:38:11 +0100 Subject: [PATCH] MOBILE-2317 rte: Initial implementation --- src/components/components.module.ts | 7 +- .../mark-required/mark-required.scss | 14 +- .../rich-text-editor/rich-text-editor.html | 29 +++ .../rich-text-editor/rich-text-editor.scss | 71 +++++++ .../rich-text-editor/rich-text-editor.ts | 194 ++++++++++++++++++ 5 files changed, 305 insertions(+), 10 deletions(-) create mode 100644 src/components/rich-text-editor/rich-text-editor.html create mode 100644 src/components/rich-text-editor/rich-text-editor.scss create mode 100644 src/components/rich-text-editor/rich-text-editor.ts diff --git a/src/components/components.module.ts b/src/components/components.module.ts index a9b03da57..7d62c20f4 100644 --- a/src/components/components.module.ts +++ b/src/components/components.module.ts @@ -36,6 +36,7 @@ import { CoreLocalFileComponent } from './local-file/local-file'; import { CoreSitePickerComponent } from './site-picker/site-picker'; import { CoreTabsComponent } from './tabs/tabs'; import { CoreTabComponent } from './tabs/tab'; +import { CoreRichTextEditorComponent } from './rich-text-editor/rich-text-editor'; @NgModule({ declarations: [ @@ -57,7 +58,8 @@ import { CoreTabComponent } from './tabs/tab'; CoreLocalFileComponent, CoreSitePickerComponent, CoreTabsComponent, - CoreTabComponent + CoreTabComponent, + CoreRichTextEditorComponent ], entryComponents: [ CoreContextMenuPopoverComponent, @@ -86,7 +88,8 @@ import { CoreTabComponent } from './tabs/tab'; CoreLocalFileComponent, CoreSitePickerComponent, CoreTabsComponent, - CoreTabComponent + CoreTabComponent, + CoreRichTextEditorComponent ] }) export class CoreComponentsModule {} diff --git a/src/components/mark-required/mark-required.scss b/src/components/mark-required/mark-required.scss index deacc173b..1e6bebc83 100644 --- a/src/components/mark-required/mark-required.scss +++ b/src/components/mark-required/mark-required.scss @@ -1,9 +1,7 @@ -*[core-mark-required] { - .core-input-required-asterisk, .icon.core-input-required-asterisk { - color: $red !important; - font-size: 8px; - padding-left: 4px; - line-height: 100%; - vertical-align: top; - } +.core-input-required-asterisk, .icon.core-input-required-asterisk { + color: $red !important; + font-size: 8px; + padding-left: 4px; + line-height: 100%; + vertical-align: top; } diff --git a/src/components/rich-text-editor/rich-text-editor.html b/src/components/rich-text-editor/rich-text-editor.html new file mode 100644 index 000000000..72eee6e14 --- /dev/null +++ b/src/components/rich-text-editor/rich-text-editor.html @@ -0,0 +1,29 @@ +
+
+
+ + +
+ + + + + + + + + + + + +
+
+ +
+ +
+ +
+
+ + diff --git a/src/components/rich-text-editor/rich-text-editor.scss b/src/components/rich-text-editor/rich-text-editor.scss new file mode 100644 index 000000000..096779bb8 --- /dev/null +++ b/src/components/rich-text-editor/rich-text-editor.scss @@ -0,0 +1,71 @@ +core-rich-text-editor { + height: 40vh; + overflow: hidden; + min-height: 30vh; + + > div { + height: 100%; + width: 100%; + display: flex; + flex-direction: column; + } + + .core-rte-editor, .core-textarea { + padding: 2px; + margin: 2px; + width: 100%; + resize: none; + background-color: $white; + flex-grow: 1; + * { + overflow: hidden; + } + } + + .core-rte-editor { + -webkit-user-select: auto !important; + word-wrap: break-word; + overflow-x: hidden; + overflow-y: auto; + cursor: text; + img { + padding-left: 2px; + max-width: 95%; + } + &:empty:before { + content: attr(data-placeholder-text); + display: block; + color: $gray-light; + font-weight: bold; + } + } + + .core-textarea textarea { + margin: 0 !important; + padding: 0; + height: 100% !important; + width: 100% !important; + resize: none; + overflow-x: hidden; + overflow-y: auto; + } + + div.formatOptions { + background: $gray-dark; + margin: 5px 1px 15px 1px; + text-align: center; + flex-grow: 0; + width: 100%; + z-index: 1; + button { + background: $gray-dark; + color: $white; + font-size: 1.1em; + height: 35px; + min-width: 30px; + padding-left: 1px; + padding-right: 1px; + } + } + +} \ No newline at end of file diff --git a/src/components/rich-text-editor/rich-text-editor.ts b/src/components/rich-text-editor/rich-text-editor.ts new file mode 100644 index 000000000..3f41f9f49 --- /dev/null +++ b/src/components/rich-text-editor/rich-text-editor.ts @@ -0,0 +1,194 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + +import { Component, Input, Output, EventEmitter, ViewChild, ElementRef } from '@angular/core'; +import { TextInput } from 'ionic-angular'; +import { CoreDomUtilsProvider } from '../../providers/utils/dom'; +import { FormControl } from '@angular/forms'; +import { Keyboard } from '@ionic-native/keyboard'; + +/** + * Directive to display a rich text editor if enabled. + * +* If enabled, this directive will show a rich text editor. Otherwise it'll show a regular textarea. + * + * This directive requires an OBJECT model. The text written in the editor or textarea will be stored inside + * a "text" property in that object. This is to ensure 2-way data-binding, since using a string as a model + * could be easily broken. + * + * Example: + * + * + * + * In the example above, the text written in the editor will be stored in newpost.text. + */ +@Component({ + selector: 'core-rich-text-editor', + templateUrl: 'rich-text-editor.html' +}) +export class CoreRichTextEditorComponent { + // Based on: https://github.com/judgewest2000/Ionic3RichText/ + // @todo: Resize, images, anchor button, fullscreen... + + @Input() placeholder?: string = ""; // Placeholder to set in textarea. + @Input() control: FormControl; // Form control. + @Output() public contentChanged: EventEmitter; + + @ViewChild('editor') editor: ElementRef; // WYSIWYG editor. + @ViewChild('textarea') textarea: TextInput; // Textarea editor. + @ViewChild('decorate') decorate: ElementRef; // Buttons. + + rteEnabled: boolean = false; + uniqueId = `rte{Math.floor(Math.random() * 1000000)}`; + editorElement: HTMLDivElement; + + constructor(private domUtils: CoreDomUtilsProvider, private keyboard: Keyboard) { + this.contentChanged = new EventEmitter(); + } + + /** + * Init editor + */ + ngAfterContentInit() { + this.domUtils.isRichTextEditorEnabled().then((enabled) => { + this.rteEnabled = !!enabled; + }); + + // Setup the editor. + this.editorElement = this.editor.nativeElement as HTMLDivElement; + this.editorElement.innerHTML = this.control.value; + this.textarea.value = this.control.value; + this.control.setValue(this.control.value); + + this.editorElement.onchange = this.onChange.bind(this); + this.editorElement.onkeyup = this.onChange.bind(this); + this.editorElement.onpaste = this.onChange.bind(this); + this.editorElement.oninput = this.onChange.bind(this); + + // Setup button actions. + let buttons = (this.decorate.nativeElement as HTMLDivElement).getElementsByTagName('button'); + for (let i = 0; i < buttons.length; i++) { + let button = buttons[i], + command = button.getAttribute('data-command'); + + if (command) { + if (command.includes('|')) { + let parameter = command.split('|')[1]; + command = command.split('|')[0]; + + button.addEventListener('click', ($event) => { + this.buttonAction($event, command, parameter); + }); + } else { + button.addEventListener('click', ($event) => { + this.buttonAction($event, command); + }); + } + } + } + } + + /** + * On change function to sync with form data. + */ + onChange($event) { + if (this.rteEnabled) { + if (this.isNullOrWhiteSpace(this.editorElement.innerText)) { + this.clearText(); + } else { + this.control.setValue(this.editorElement.innerHTML); + } + } else { + if (this.isNullOrWhiteSpace(this.textarea.value)) { + this.clearText(); + } else { + this.control.setValue(this.textarea.value); + } + } + this.contentChanged.emit(this.control.value); + } + + /** + * Toggle from rte editor to textarea syncing values. + */ + toggleEditor($event) { + $event.preventDefault(); + $event.stopPropagation(); + + if (this.isNullOrWhiteSpace(this.control.value)) { + this.clearText(); + } else { + this.editorElement.innerHTML = this.control.value; + this.textarea.value = this.control.value; + } + + this.rteEnabled = !this.rteEnabled; + + // Set focus and cursor at the end. + setTimeout(() => { + if (this.rteEnabled) { + this.editorElement.focus(); + let range = document.createRange(); + range.selectNodeContents(this.editorElement); + range.collapse(false); + let sel = window.getSelection(); + sel.removeAllRanges(); + sel.addRange(range); + } else { + this.textarea.setFocus(); + } + setTimeout(() => { + this.keyboard.show(); + }, 1); + }, 1); + } + + /** + * Check if text is empty. + * @param {string} value text + */ + private isNullOrWhiteSpace(value: string) { + if (value == null || typeof value == "undefined") { + return true; + } + value = value.replace(/[\n\r]/g, ''); + value = value.split(' ').join(''); + + return value.length === 0; + } + + /** + * Clear the text. + */ + clearText() { + this.editorElement.innerHTML = '

'; + this.textarea.value = ''; + this.control.setValue(null); + } + + /** + * Execute an action over the selected text. + * API docs: https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand + * + * @param {any} $event Event data + * @param {string} command Command to execute. + * @param {any} [parameters] Parameters of the command. + */ + private buttonAction($event: any, command: string, parameters: any = null) { + $event.preventDefault(); + $event.stopPropagation(); + document.execCommand(command, false, parameters); + } +} \ No newline at end of file