MOBILE-2317 rte: Initial implementation
parent
ba1267410e
commit
61a477afd5
|
@ -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 {}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue