MOBILE-2353 rte: Improve editor with resizing

This commit is contained in:
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 --> <!-- Edit -->
<ion-item text-wrap *ngIf="edit && loaded"> <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" [component]="component" [componentId]="assign.cmid"></core-rich-text-editor>
<core-rich-text-editor item-content [control]="control" [placeholder]="plugin.name" name="assignfeedbackcomments_editor"></core-rich-text-editor>
</ion-item> </ion-item>

View File

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

View File

@ -38,9 +38,7 @@
</ion-item> </ion-item>
<ion-item> <ion-item>
<ion-label stacked>{{ 'addon.mod_forum.message' | translate }}</ion-label> <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> <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>
<!-- @todo: Attributes that were passed to RTE in Ionic 1 but now they aren't supported yet:
[component]="component" [componentId]="componentId" -->
</ion-item> </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> <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> <ion-grid>

View File

@ -19,9 +19,7 @@
</ion-item> </ion-item>
<ion-item> <ion-item>
<ion-label stacked>{{ 'addon.mod_forum.message' | translate }}</ion-label> <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> <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>
<!-- @todo: Attributes that were passed to RTE in Ionic 1 but now they aren't supported yet:
[component]="component" [componentId]="forum.cmid" -->
</ion-item> </ion-item>
<ion-item *ngIf="showGroups"> <ion-item *ngIf="showGroups">
<ion-label id="addon-mod-forum-groupslabel">{{ 'addon.mod_forum.group' | translate }}</ion-label> <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-toc-background-color: $gray-light !default;
addon-mod-wiki-index { addon-mod-wiki-index {
background-color: $white;
.core-tabs-content-container,
.addon-mod_wiki-page-content {
background-color: $white;
}
.wiki-toc { .wiki-toc {
border: 1px solid $addon-mod-wiki-toc-border-color; border: 1px solid $addon-mod-wiki-toc-border-color;
background: $addon-mod-wiki-toc-background-color; background: $addon-mod-wiki-toc-background-color;

View File

@ -17,8 +17,7 @@
</ion-item> </ion-item>
<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" [component]="component" [componentId]="componentId"></core-rich-text-editor>
<core-rich-text-editor item-content [control]="contentControl" [placeholder]="'core.content' | translate" name="wiki_page_content"></core-rich-text-editor>
</ion-item> </ion-item>
<ion-badge color="danger" *ngIf="wrongVersionLock" item-end>{{ 'addon.mod_wiki.wrongversionlock' | translate }}</ion-badge> <!-- @todo: Check this. --> <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. --> <!-- 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> <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. --> <!-- 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> <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>
<!-- @todo: Attributes that were passed to RTE in Ionic 1 but now they aren't supported yet:
[component]="component" [componentId]="componentId" -->
</ion-item> </ion-item>
<!-- Draft files not supported. --> <!-- Draft files not supported. -->

View File

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

View File

@ -3,7 +3,8 @@
</div> </div>
<!-- https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand --> <!-- https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand -->
<div #decorate class="formatOptions"> <div #decorate class="core-rte-toolbar">
<div class="core-rte-buttons">
<button data-command="bold"><strong>B</strong></button> <button data-command="bold"><strong>B</strong></button>
<button data-command="italic"><i>I</i></button> <button data-command="italic"><i>I</i></button>
<button data-command="underline"><u>U</u></button> <button data-command="underline"><u>U</u></button>
@ -11,18 +12,21 @@
<button data-command="formatBlock|<h1>">H1</button> <button data-command="formatBlock|<h1>">H1</button>
<button data-command="formatBlock|<h2>">H2</button> <button data-command="formatBlock|<h2>">H2</button>
<button data-command="formatBlock|<h3>">H3</button> <button data-command="formatBlock|<h3>">H3</button>
<button data-command="formatBlock|<pre>">Pre</button> <button data-command="formatBlock|<pre>">&lt;pre&gt;</button>
<button data-command="insertOrderedList">OL</button> <button data-command="insertOrderedList"><ion-icon name="list" md="ios-list"></ion-icon></button>
<button data-command="insertUnorderedList">UL</button> <button data-command="insertUnorderedList">1,2,3</button>
<button data-command="removeFormat">Tx</button> <button data-command="removeFormat"><ion-icon name="brush"></ion-icon></button>
<button (click)="toggleEditor($event)">Toggle Editor</button> <button (click)="toggleEditor($event)"><ion-icon name="eye-off"></ion-icon> {{ 'core.viewcode' | translate }}</button>
</div>
</div> </div>
</div> </div>
<div [hidden]="rteEnabled"> <div [hidden]="rteEnabled">
<ion-textarea #textarea class="core-textarea" [placeholder]="placeholder" [attr.name]="name" ngControl="control" (ionChange)="onChange($event)"></ion-textarea> <ion-textarea #textarea class="core-textarea" [placeholder]="placeholder" [attr.name]="name" ngControl="control" (ionChange)="onChange($event)"></ion-textarea>
<div class="formatOptions"> <div class="core-rte-toolbar">
<button tappable (click)="toggleEditor($event)">Toggle Editor</button> <div #decorate class="core-rte-buttons">
<button tappable (click)="toggleEditor($event)"><ion-icon name="eye"></ion-icon> {{ 'core.vieweditor' | translate }}</button>
</div>
</div> </div>
</div> </div>

View File

@ -10,7 +10,6 @@ core-rich-text-editor {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.core-rte-editor, .core-textarea { .core-rte-editor, .core-textarea {
padding: 2px; padding: 2px;
margin: 2px; margin: 2px;
@ -51,21 +50,33 @@ core-rich-text-editor {
overflow-y: auto; overflow-y: auto;
} }
div.formatOptions { div.core-rte-toolbar {
background: $gray-dark; background: $gray-darker;
margin: 5px 1px 15px 1px; margin: 0px 1px 15px 1px;
text-align: center; text-align: center;
flex-grow: 0; flex-grow: 0;
width: 100%; width: 100%;
z-index: 1; z-index: 1;
.core-rte-buttons {
display: flex;
align-items: center;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-evenly;
button { button {
background: $gray-dark; background: $gray-darker;
color: $white; color: $white;
font-size: 1.1em; font-size: 1.1em;
height: 35px; height: 35px;
min-width: 30px; min-width: 30px;
padding-left: 1px; padding-left: 3px;
padding-right: 1px; 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 // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { Component, Input, Output, EventEmitter, ViewChild, ElementRef, AfterContentInit, OnDestroy } from '@angular/core'; import { Component, Input, Output, EventEmitter, ViewChild, ElementRef, AfterContentInit, OnDestroy, Optional }
import { TextInput } from 'ionic-angular'; 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 { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreUrlUtilsProvider } from '@providers/utils/url';
import { FormControl } from '@angular/forms'; import { FormControl } from '@angular/forms';
import { Keyboard } from '@ionic-native/keyboard'; import { Keyboard } from '@ionic-native/keyboard';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
@ -39,25 +43,32 @@ import { Subscription } from 'rxjs';
}) })
export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy { export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy {
// Based on: https://github.com/judgewest2000/Ionic3RichText/ // 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() placeholder = ''; // Placeholder to set in textarea.
@Input() control: FormControl; // Form control. @Input() control: FormControl; // Form control.
@Input() name = 'core-rich-text-editor'; // Name to set to the textarea. @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>; @Output() contentChanged: EventEmitter<string>;
@ViewChild('editor') editor: ElementRef; // WYSIWYG editor. @ViewChild('editor') editor: ElementRef; // WYSIWYG editor.
@ViewChild('textarea') textarea: TextInput; // Textarea editor. @ViewChild('textarea') textarea: TextInput; // Textarea editor.
@ViewChild('decorate') decorate: ElementRef; // Buttons. @ViewChild('decorate') decorate: ElementRef; // Buttons.
rteEnabled = false; protected element: HTMLDivElement;
uniqueId = `rte{Math.floor(Math.random() * 1000000)}`; protected editorElement: HTMLDivElement;
editorElement: HTMLDivElement; protected resizeFunction;
protected valueChangeSubscription: Subscription; 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.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); }, 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. * Check if text is empty.
* @param {string} value text * @param {string} value text
@ -215,5 +302,6 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy
*/ */
ngOnDestroy(): void { ngOnDestroy(): void {
this.valueChangeSubscription && this.valueChangeSubscription.unsubscribe(); this.valueChangeSubscription && this.valueChangeSubscription.unsubscribe();
window.removeEventListener('resize', this.resizeFunction);
} }
} }

View File

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

View File

@ -365,13 +365,16 @@ export class CoreDomUtilsProvider {
let surround = 0; let surround = 0;
if (usePadding) { 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) { 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) { 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) { if (innerMeasure) {
measure = measure > surround ? measure - surround : 0; measure = measure > surround ? measure - surround : 0;
@ -381,7 +384,17 @@ export class CoreDomUtilsProvider {
} }
return measure; 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;
} }
/** /**