MOBILE-2353 rte: Improve editor with resizing

main
Pau Ferrer Ocaña 2018-05-29 16:56:47 +02:00 committed by Dani Palou
parent ea6bc89cfd
commit c492921df1
13 changed files with 169 additions and 53 deletions

View File

@ -19,6 +19,5 @@
<!-- Edit -->
<ion-item text-wrap *ngIf="edit && loaded">
<!-- @todo: [component]="component" [componentId]="assign.cmid" -->
<core-rich-text-editor item-content [control]="control" [placeholder]="plugin.name" name="assignfeedbackcomments_editor"></core-rich-text-editor>
<core-rich-text-editor item-content [control]="control" [placeholder]="plugin.name" name="assignfeedbackcomments_editor" [component]="component" [componentId]="assign.cmid"></core-rich-text-editor>
</ion-item>

View File

@ -15,7 +15,6 @@
<p>{{ 'core.numwords' | translate: {'$a': words + ' / ' + configs.wordlimit} }}</p>
</ion-item>
<ion-item text-wrap>
<!-- @todo: [component]="component" [componentId]="assign.cmid" -->
<core-rich-text-editor item-content [control]="control" [placeholder]="plugin.name" name="onlinetext_editor_text" (contentChanged)="onChange($event)"></core-rich-text-editor>
<core-rich-text-editor item-content [control]="control" [placeholder]="plugin.name" name="onlinetext_editor_text" (contentChanged)="onChange($event)" [component]="component" [componentId]="assign.cmid"></core-rich-text-editor>
</ion-item>
</div>

View File

@ -38,9 +38,7 @@
</ion-item>
<ion-item>
<ion-label stacked>{{ 'addon.mod_forum.message' | translate }}</ion-label>
<core-rich-text-editor item-content [control]="messageControl" (contentChanged)="onMessageChange($event)" [placeholder]="'addon.mod_forum.message' | translate" [name]="'mod_forum_reply_' + post.id"></core-rich-text-editor>
<!-- @todo: Attributes that were passed to RTE in Ionic 1 but now they aren't supported yet:
[component]="component" [componentId]="componentId" -->
<core-rich-text-editor item-content [control]="messageControl" (contentChanged)="onMessageChange($event)" [placeholder]="'addon.mod_forum.message' | translate" [name]="'mod_forum_reply_' + post.id" [component]="component" [componentId]="componentId"></core-rich-text-editor>
</ion-item>
<core-attachments *ngIf="forum.id && forum.maxattachments > 0" [files]="replyData.files" [maxSize]="forum.maxbytes" [maxSubmissions]="forum.maxattachments" [component]="component" [componentId]="forum.cmid" [allowOffline]="true"></core-attachments>
<ion-grid>

View File

@ -19,9 +19,7 @@
</ion-item>
<ion-item>
<ion-label stacked>{{ 'addon.mod_forum.message' | translate }}</ion-label>
<core-rich-text-editor item-content [control]="messageControl" (contentChanged)="onMessageChange($event)" [placeholder]="'addon.mod_forum.message' | translate" name="addon_mod_forum_new_discussion"></core-rich-text-editor>
<!-- @todo: Attributes that were passed to RTE in Ionic 1 but now they aren't supported yet:
[component]="component" [componentId]="forum.cmid" -->
<core-rich-text-editor item-content [control]="messageControl" (contentChanged)="onMessageChange($event)" [placeholder]="'addon.mod_forum.message' | translate" name="addon_mod_forum_new_discussion" [component]="component" [componentId]="forum.cmid"></core-rich-text-editor>
</ion-item>
<ion-item *ngIf="showGroups">
<ion-label id="addon-mod-forum-groupslabel">{{ 'addon.mod_forum.group' | translate }}</ion-label>

View File

@ -5,6 +5,13 @@ $addon-mod-wiki-toc-border-color: $gray-dark !default;
$addon-mod-wiki-toc-background-color: $gray-light !default;
addon-mod-wiki-index {
background-color: $white;
.core-tabs-content-container,
.addon-mod_wiki-page-content {
background-color: $white;
}
.wiki-toc {
border: 1px solid $addon-mod-wiki-toc-border-color;
background: $addon-mod-wiki-toc-background-color;

View File

@ -17,8 +17,7 @@
</ion-item>
<ion-item>
<!-- @todo: [component]="component" [componentId]="componentId" -->
<core-rich-text-editor item-content [control]="contentControl" [placeholder]="'core.content' | translate" name="wiki_page_content"></core-rich-text-editor>
<core-rich-text-editor item-content [control]="contentControl" [placeholder]="'core.content' | translate" name="wiki_page_content" [component]="component" [componentId]="componentId"></core-rich-text-editor>
</ion-item>
<ion-badge color="danger" *ngIf="wrongVersionLock" item-end>{{ 'addon.mod_wiki.wrongversionlock' | translate }}</ion-badge> <!-- @todo: Check this. -->

View File

@ -11,9 +11,7 @@
<!-- Plain text textarea. -->
<ion-textarea *ngIf="question.isPlainText" class="core-question-textarea" [ngClass]='{"core-monospaced": question.isMonospaced}' placeholder="{{ 'core.question.answer' | translate }}" [attr.name]="question.textarea.name" aria-multiline="true" [ngModel]="question.textarea.text"></ion-textarea>
<!-- Rich text editor. -->
<core-rich-text-editor item-content *ngIf="!question.isPlainText" placeholder="{{ 'core.question.answer' | translate }}" [control]="formControl" [name]="question.textarea.name"></core-rich-text-editor>
<!-- @todo: Attributes that were passed to RTE in Ionic 1 but now they aren't supported yet:
[component]="component" [componentId]="componentId" -->
<core-rich-text-editor item-content *ngIf="!question.isPlainText" placeholder="{{ 'core.question.answer' | translate }}" [control]="formControl" [name]="question.textarea.name" [component]="component" [componentId]="componentId"></core-rich-text-editor>
</ion-item>
<!-- Draft files not supported. -->

View File

@ -32,7 +32,7 @@
.img-responsive {
display: block;
max-width: 100%;
/* height: auto; */
height: auto;
}
.opacity-hide { opacity: 0; }

View File

@ -3,26 +3,30 @@
</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 #decorate class="core-rte-toolbar">
<div class="core-rte-buttons">
<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>">&lt;pre&gt;</button>
<button data-command="insertOrderedList"><ion-icon name="list" md="ios-list"></ion-icon></button>
<button data-command="insertUnorderedList">1,2,3</button>
<button data-command="removeFormat"><ion-icon name="brush"></ion-icon></button>
<button (click)="toggleEditor($event)"><ion-icon name="eye-off"></ion-icon> {{ 'core.viewcode' | translate }}</button>
</div>
</div>
</div>
<div [hidden]="rteEnabled">
<ion-textarea #textarea class="core-textarea" [placeholder]="placeholder" [attr.name]="name" ngControl="control" (ionChange)="onChange($event)"></ion-textarea>
<div class="formatOptions">
<button tappable (click)="toggleEditor($event)">Toggle Editor</button>
<div class="core-rte-toolbar">
<div #decorate class="core-rte-buttons">
<button tappable (click)="toggleEditor($event)"><ion-icon name="eye"></ion-icon> {{ 'core.vieweditor' | translate }}</button>
</div>
</div>
</div>

View File

@ -10,7 +10,6 @@ core-rich-text-editor {
display: flex;
flex-direction: column;
}
.core-rte-editor, .core-textarea {
padding: 2px;
margin: 2px;
@ -51,21 +50,33 @@ core-rich-text-editor {
overflow-y: auto;
}
div.formatOptions {
background: $gray-dark;
margin: 5px 1px 15px 1px;
div.core-rte-toolbar {
background: $gray-darker;
margin: 0px 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;
.core-rte-buttons {
display: flex;
align-items: center;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-evenly;
button {
background: $gray-darker;
color: $white;
font-size: 1.1em;
height: 35px;
min-width: 30px;
padding-left: 3px;
padding-right: 3px;
border-right: 1px solid $gray-dark;
border-bottom: 1px solid $gray-dark;
flex-grow: 1;
}
}
}

View File

@ -12,9 +12,13 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, Input, Output, EventEmitter, ViewChild, ElementRef, AfterContentInit, OnDestroy } from '@angular/core';
import { TextInput } from 'ionic-angular';
import { Component, Input, Output, EventEmitter, ViewChild, ElementRef, AfterContentInit, OnDestroy, Optional }
from '@angular/core';
import { TextInput, Content } from 'ionic-angular';
import { CoreSitesProvider } from '@providers/sites';
import { CoreFilepoolProvider } from '@providers/filepool';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreUrlUtilsProvider } from '@providers/utils/url';
import { FormControl } from '@angular/forms';
import { Keyboard } from '@ionic-native/keyboard';
import { Subscription } from 'rxjs';
@ -39,25 +43,32 @@ import { Subscription } from 'rxjs';
})
export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy {
// Based on: https://github.com/judgewest2000/Ionic3RichText/
// @todo: Resize, images, anchor button, fullscreen...
// @todo: Anchor button, fullscreen...
@Input() placeholder = ''; // Placeholder to set in textarea.
@Input() control: FormControl; // Form control.
@Input() name = 'core-rich-text-editor'; // Name to set to the textarea.
@Input() component?: string; // The component to link the files to.
@Input() componentId?: number; // An ID to use in conjunction with the component.
@Output() contentChanged: EventEmitter<string>;
@ViewChild('editor') editor: ElementRef; // WYSIWYG editor.
@ViewChild('textarea') textarea: TextInput; // Textarea editor.
@ViewChild('decorate') decorate: ElementRef; // Buttons.
rteEnabled = false;
uniqueId = `rte{Math.floor(Math.random() * 1000000)}`;
editorElement: HTMLDivElement;
protected element: HTMLDivElement;
protected editorElement: HTMLDivElement;
protected resizeFunction;
protected valueChangeSubscription: Subscription;
constructor(private domUtils: CoreDomUtilsProvider, private keyboard: Keyboard) {
rteEnabled = false;
constructor(private domUtils: CoreDomUtilsProvider, private keyboard: Keyboard, private urlUtils: CoreUrlUtilsProvider,
private sitesProvider: CoreSitesProvider, private filepoolProvider: CoreFilepoolProvider,
@Optional() private content: Content, elementRef: ElementRef) {
this.contentChanged = new EventEmitter<string>();
this.element = elementRef.nativeElement as HTMLDivElement;
}
/**
@ -104,6 +115,58 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy
}
}
}
this.treatExternalContent();
this.resizeFunction = this.maximizeEditorSize.bind(this);
window.addEventListener('resize', this.resizeFunction);
setTimeout(this.resizeFunction, 1000);
}
/**
* Resize editor to maximize the space occupied.
*/
protected maximizeEditorSize(): void {
this.content.resize();
const contentVisibleHeight = this.content.contentHeight;
// Editor is ready, adjust Height if needed.
if (contentVisibleHeight > 0) {
const height = this.getSurroundingHeight(this.element);
if (contentVisibleHeight > height) {
this.element.style.height = this.domUtils.formatPixelsSize(contentVisibleHeight - height);
} else {
this.element.style.height = '';
}
}
}
/**
* Get the height of the surrounding elements from the current to the top element.
*
* @param {any} element Directive DOM element to get surroundings elements from.
* @return {number} Surrounding height in px.
*/
protected getSurroundingHeight(element: any): number {
let height = 0;
while (element.parentNode && element.parentNode.tagName != 'ION-CONTENT') {
const parent = element.parentNode;
if (element.tagName && element.tagName != 'CORE-LOADING') {
parent.childNodes.forEach((child) => {
if (child.tagName && child != element) {
height += this.domUtils.getElementHeight(child, false, true, true);
}
});
}
element = parent;
}
const cs = getComputedStyle(element);
height += this.domUtils.getComputedStyleMeasure(cs, 'paddingTop') +
this.domUtils.getComputedStyleMeasure(cs, 'paddingBottom');
return height;
}
/**
@ -171,6 +234,30 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy
}, 1);
}
/**
* Treat elements that can contain external content.
* We only search for images because the editor should receive unfiltered text, so the multimedia filter won't be applied.
* Treating videos and audios in here is complex, so if a user manually adds one he won't be able to play it in the editor.
*/
protected treatExternalContent(): void {
const elements = Array.from(this.editorElement.querySelectorAll('img')),
siteId = this.sitesProvider.getCurrentSiteId(),
canDownloadFiles = this.sitesProvider.getCurrentSite().canDownloadFiles();
elements.forEach((el) => {
const url = el.src;
if (!url || !this.urlUtils.isDownloadableUrl(url) || (!canDownloadFiles && this.urlUtils.isPluginFileUrl(url))) {
// Nothing to treat.
return;
}
// Check if it's downloaded.
return this.filepoolProvider.getSrcByUrl(siteId, url, this.component, this.componentId).then((finalUrl) => {
el.setAttribute('src', finalUrl);
});
});
}
/**
* Check if text is empty.
* @param {string} value text
@ -215,5 +302,6 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy
*/
ngOnDestroy(): void {
this.valueChangeSubscription && this.valueChangeSubscription.unsubscribe();
window.removeEventListener('resize', this.resizeFunction);
}
}

View File

@ -227,6 +227,8 @@
"usernotfullysetup": "User not fully set-up",
"users": "Users",
"view": "View",
"viewcode": "View code",
"vieweditor": "View editor",
"viewprofile": "View profile",
"warningofflinedatadeleted": "Offline data of {{component}} '{{name}}' has been deleted. {{error}}",
"whatisyourage": "What is your age?",

View File

@ -365,13 +365,16 @@ export class CoreDomUtilsProvider {
let surround = 0;
if (usePadding) {
surround += parseInt(computedStyle['padding' + priorSide], 10) + parseInt(computedStyle['padding' + afterSide], 10);
surround += this.getComputedStyleMeasure(computedStyle, 'padding' + priorSide) +
this.getComputedStyleMeasure(computedStyle, 'padding' + afterSide);
}
if (useMargin) {
surround += parseInt(computedStyle['margin' + priorSide], 10) + parseInt(computedStyle['margin' + afterSide], 10);
surround += this.getComputedStyleMeasure(computedStyle, 'margin' + priorSide) +
this.getComputedStyleMeasure(computedStyle, 'margin' + afterSide);
}
if (useBorder) {
surround += parseInt(computedStyle['border' + priorSide], 10) + parseInt(computedStyle['border' + afterSide], 10);
surround += this.getComputedStyleMeasure(computedStyle, 'border' + priorSide + 'Width') +
this.getComputedStyleMeasure(computedStyle, 'border' + afterSide + 'Width');
}
if (innerMeasure) {
measure = measure > surround ? measure - surround : 0;
@ -381,7 +384,17 @@ export class CoreDomUtilsProvider {
}
return measure;
}
/**
* Returns the computed style measure or 0 if not found or NaN.
*
* @param {any} style Style from getComputedStyle.
* @param {string} measure Measure to get.
* @return {number} Result of the measure.
*/
getComputedStyleMeasure(style: any, measure: string): number {
return parseInt(style[measure], 10) || 0;
}
/**