MOBILE-2353 rte: Improve editor with resizing
parent
ea6bc89cfd
commit
c492921df1
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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. -->
|
||||
|
|
|
@ -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. -->
|
||||
|
|
|
@ -32,7 +32,7 @@
|
|||
.img-responsive {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
/* height: auto; */
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.opacity-hide { opacity: 0; }
|
||||
|
|
|
@ -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>"><pre></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>
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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?",
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
Loading…
Reference in New Issue