MOBILE-2317 rte: Initial implementation

main
Pau Ferrer Ocaña 2018-01-18 16:38:11 +01:00
parent ba1267410e
commit 61a477afd5
5 changed files with 305 additions and 10 deletions

View File

@ -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 {}

View File

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

View File

@ -0,0 +1,29 @@
<div [hidden]="!rteEnabled">
<div #editor contenteditable="true" class="core-rte-editor" tappable [attr.data-placeholder-text]="placeholder">
</div>
<!-- https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand -->
<div #decorate class="formatOptions">
<button data-command="bold"><strong>B</strong></button>
<button data-command="italic"><i>I</i></button>
<button data-command="underline"><u>U</u></button>
<button data-command="formatBlock|<p>">Normal</button>
<button data-command="formatBlock|<h1>">H1</button>
<button data-command="formatBlock|<h2>">H2</button>
<button data-command="formatBlock|<h3>">H3</button>
<button data-command="formatBlock|<pre>">Pre</button>
<button data-command="insertOrderedList">OL</button>
<button data-command="insertUnorderedList">UL</button>
<button data-command="removeFormat">Tx</button>
<button (click)="toggleEditor($event)">Toggle Editor</button>
</div>
</div>
<div [hidden]="rteEnabled">
<ion-textarea #textarea class="core-textarea" [placeholder]="placeholder" ngControl="control" (ionChange)="onChange($event)"></ion-textarea>
<div class="formatOptions">
<button tappable (click)="toggleEditor($event)">Toggle Editor</button>
</div>
</div>

View File

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

View File

@ -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:
* <core-rich-text-editor model="newpost" placeholder="{{ 'mma.mod_forum.message' | translate }}" scroll-handle="mmaScrollHandle">
* </core-rich-text-editor>
*
* 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<string>;
@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<string>();
}
/**
* 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 = '<p></p>';
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);
}
}