2018-01-18 16:38:11 +01:00
|
|
|
// (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.
|
|
|
|
|
2018-05-25 08:54:55 +02:00
|
|
|
import { Component, Input, Output, EventEmitter, ViewChild, ElementRef, AfterContentInit, OnDestroy } from '@angular/core';
|
2018-01-18 16:38:11 +01:00
|
|
|
import { TextInput } from 'ionic-angular';
|
2018-03-01 16:55:49 +01:00
|
|
|
import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
2018-01-18 16:38:11 +01:00
|
|
|
import { FormControl } from '@angular/forms';
|
|
|
|
import { Keyboard } from '@ionic-native/keyboard';
|
2018-05-25 08:54:55 +02:00
|
|
|
import { Subscription } from 'rxjs';
|
2018-01-18 16:38:11 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Directive to display a rich text editor if enabled.
|
|
|
|
*
|
2018-01-29 10:05:20 +01:00
|
|
|
* If enabled, this directive will show a rich text editor. Otherwise it'll show a regular textarea.
|
2018-01-18 16:38:11 +01:00
|
|
|
*
|
|
|
|
* 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:
|
2018-01-25 13:19:11 +01:00
|
|
|
* <core-rich-text-editor item-content [control]="control" [placeholder]="field.name"></core-rich-text-editor>
|
2018-01-18 16:38:11 +01:00
|
|
|
*
|
|
|
|
* 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'
|
|
|
|
})
|
2018-05-25 08:54:55 +02:00
|
|
|
export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy {
|
2018-01-18 16:38:11 +01:00
|
|
|
// Based on: https://github.com/judgewest2000/Ionic3RichText/
|
|
|
|
// @todo: Resize, images, anchor button, fullscreen...
|
|
|
|
|
2018-03-15 15:24:22 +01:00
|
|
|
@Input() placeholder = ''; // Placeholder to set in textarea.
|
2018-01-18 16:38:11 +01:00
|
|
|
@Input() control: FormControl; // Form control.
|
2018-04-04 09:00:29 +02:00
|
|
|
@Input() name = 'core-rich-text-editor'; // Name to set to the textarea.
|
2018-01-29 10:05:20 +01:00
|
|
|
@Output() contentChanged: EventEmitter<string>;
|
2018-01-18 16:38:11 +01:00
|
|
|
|
|
|
|
@ViewChild('editor') editor: ElementRef; // WYSIWYG editor.
|
|
|
|
@ViewChild('textarea') textarea: TextInput; // Textarea editor.
|
|
|
|
@ViewChild('decorate') decorate: ElementRef; // Buttons.
|
|
|
|
|
2018-01-29 10:05:20 +01:00
|
|
|
rteEnabled = false;
|
2018-01-18 16:38:11 +01:00
|
|
|
uniqueId = `rte{Math.floor(Math.random() * 1000000)}`;
|
|
|
|
editorElement: HTMLDivElement;
|
|
|
|
|
2018-05-25 08:54:55 +02:00
|
|
|
protected valueChangeSubscription: Subscription;
|
|
|
|
|
2018-01-18 16:38:11 +01:00
|
|
|
constructor(private domUtils: CoreDomUtilsProvider, private keyboard: Keyboard) {
|
|
|
|
this.contentChanged = new EventEmitter<string>();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Init editor
|
|
|
|
*/
|
2018-01-29 10:05:20 +01:00
|
|
|
ngAfterContentInit(): void {
|
2018-01-18 16:38:11 +01:00
|
|
|
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.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);
|
|
|
|
|
2018-05-25 08:54:55 +02:00
|
|
|
// 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) => {
|
|
|
|
this.editorElement.innerHTML = param;
|
|
|
|
});
|
|
|
|
|
2018-01-18 16:38:11 +01:00
|
|
|
// Setup button actions.
|
2018-01-29 10:05:20 +01:00
|
|
|
const buttons = (this.decorate.nativeElement as HTMLDivElement).getElementsByTagName('button');
|
2018-01-18 16:38:11 +01:00
|
|
|
for (let i = 0; i < buttons.length; i++) {
|
2018-01-29 10:05:20 +01:00
|
|
|
const button = buttons[i];
|
|
|
|
let command = button.getAttribute('data-command');
|
2018-01-18 16:38:11 +01:00
|
|
|
|
|
|
|
if (command) {
|
|
|
|
if (command.includes('|')) {
|
2018-01-29 10:05:20 +01:00
|
|
|
const parameter = command.split('|')[1];
|
2018-01-18 16:38:11 +01:00
|
|
|
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.
|
2018-01-29 10:05:20 +01:00
|
|
|
*
|
|
|
|
* @param {Event} $event The event.
|
2018-01-18 16:38:11 +01:00
|
|
|
*/
|
2018-01-29 10:05:20 +01:00
|
|
|
onChange($event: Event): void {
|
2018-01-18 16:38:11 +01:00
|
|
|
if (this.rteEnabled) {
|
|
|
|
if (this.isNullOrWhiteSpace(this.editorElement.innerText)) {
|
|
|
|
this.clearText();
|
|
|
|
} else {
|
2018-05-25 08:54:55 +02:00
|
|
|
// Don't emit event so our valueChanges doesn't get notified by this change.
|
|
|
|
this.control.setValue(this.editorElement.innerHTML, {emitEvent: false});
|
2018-04-04 09:00:29 +02:00
|
|
|
this.textarea.value = this.editorElement.innerHTML;
|
2018-01-18 16:38:11 +01:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if (this.isNullOrWhiteSpace(this.textarea.value)) {
|
|
|
|
this.clearText();
|
|
|
|
} else {
|
2018-05-25 08:54:55 +02:00
|
|
|
// Don't emit event so our valueChanges doesn't get notified by this change.
|
|
|
|
this.control.setValue(this.textarea.value, {emitEvent: false});
|
2018-01-18 16:38:11 +01:00
|
|
|
}
|
|
|
|
}
|
2018-04-04 09:00:29 +02:00
|
|
|
|
2018-01-18 16:38:11 +01:00
|
|
|
this.contentChanged.emit(this.control.value);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Toggle from rte editor to textarea syncing values.
|
2018-01-29 10:05:20 +01:00
|
|
|
*
|
|
|
|
* @param {Event} $event The event.
|
2018-01-18 16:38:11 +01:00
|
|
|
*/
|
2018-01-29 10:05:20 +01:00
|
|
|
toggleEditor($event: Event): void {
|
2018-01-18 16:38:11 +01:00
|
|
|
$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();
|
2018-01-29 10:05:20 +01:00
|
|
|
|
|
|
|
const range = document.createRange();
|
2018-01-18 16:38:11 +01:00
|
|
|
range.selectNodeContents(this.editorElement);
|
|
|
|
range.collapse(false);
|
2018-01-29 10:05:20 +01:00
|
|
|
|
|
|
|
const sel = window.getSelection();
|
2018-01-18 16:38:11 +01:00
|
|
|
sel.removeAllRanges();
|
|
|
|
sel.addRange(range);
|
|
|
|
} else {
|
|
|
|
this.textarea.setFocus();
|
|
|
|
}
|
|
|
|
setTimeout(() => {
|
|
|
|
this.keyboard.show();
|
|
|
|
}, 1);
|
|
|
|
}, 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check if text is empty.
|
|
|
|
* @param {string} value text
|
|
|
|
*/
|
2018-01-29 10:05:20 +01:00
|
|
|
protected isNullOrWhiteSpace(value: string): boolean {
|
|
|
|
if (value == null || typeof value == 'undefined') {
|
2018-01-18 16:38:11 +01:00
|
|
|
return true;
|
|
|
|
}
|
2018-01-29 10:05:20 +01:00
|
|
|
|
2018-01-18 16:38:11 +01:00
|
|
|
value = value.replace(/[\n\r]/g, '');
|
|
|
|
value = value.split(' ').join('');
|
|
|
|
|
|
|
|
return value.length === 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Clear the text.
|
|
|
|
*/
|
2018-01-29 10:05:20 +01:00
|
|
|
clearText(): void {
|
2018-01-18 16:38:11 +01:00
|
|
|
this.editorElement.innerHTML = '<p></p>';
|
|
|
|
this.textarea.value = '';
|
2018-05-25 08:54:55 +02:00
|
|
|
// Don't emit event so our valueChanges doesn't get notified by this change.
|
|
|
|
this.control.setValue(null, {emitEvent: false});
|
2018-01-18 16:38:11 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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.
|
|
|
|
*/
|
2018-01-29 10:05:20 +01:00
|
|
|
protected buttonAction($event: any, command: string, parameters: any = null): void {
|
2018-01-18 16:38:11 +01:00
|
|
|
$event.preventDefault();
|
|
|
|
$event.stopPropagation();
|
|
|
|
document.execCommand(command, false, parameters);
|
|
|
|
}
|
2018-05-25 08:54:55 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Component being destroyed.
|
|
|
|
*/
|
|
|
|
ngOnDestroy(): void {
|
|
|
|
this.valueChangeSubscription && this.valueChangeSubscription.unsubscribe();
|
|
|
|
}
|
2018-01-29 10:05:20 +01:00
|
|
|
}
|