Merge pull request #1978 from albertgasset/MOBILE-3053

Mobile 3053
main
Juan Leyva 2019-07-09 14:56:12 +02:00 committed by GitHub
commit 5c9f90d849
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 307 additions and 77 deletions

View File

@ -1,33 +1,81 @@
<div [hidden]="!rteEnabled">
<div #editor contenteditable="true" class="core-rte-editor" tappable [attr.data-placeholder-text]="placeholder" role="textbox">
</div>
<!-- https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand -->
<div #decorate class="core-rte-toolbar">
<div class="core-rte-buttons">
<button [core-suppress-events] (onClick)="buttonAction($event, 'bold')"><core-icon name="fa-bold"></core-icon></button>
<button [core-suppress-events] (onClick)="buttonAction($event, 'italic')"><core-icon name="fa-italic"></core-icon></button>
<button [core-suppress-events] (onClick)="buttonAction($event, 'underline')"><core-icon name="fa-underline"></core-icon></button>
<button [core-suppress-events] (onClick)="buttonAction($event, 'strikeThrough')"><core-icon name="fa-strikethrough"></core-icon></button>
<button [core-suppress-events] (onClick)="buttonAction($event, 'formatBlock|<p>')"><core-icon name="fa-paragraph"></core-icon></button>
<button [core-suppress-events] (onClick)="buttonAction($event, 'formatBlock|<h1>')"><core-icon name="fa-header"></core-icon>1</button>
<button [core-suppress-events] (onClick)="buttonAction($event, 'formatBlock|<h2>')"><core-icon name="fa-header"></core-icon>2</button>
<button [core-suppress-events] (onClick)="buttonAction($event, 'formatBlock|<h3>')"><core-icon name="fa-header"></core-icon>3</button>
<button [core-suppress-events] (onClick)="buttonAction($event, 'insertUnorderedList')"><core-icon name="fa-list-ul"></core-icon></button>
<button [core-suppress-events] (onClick)="buttonAction($event, 'insertOrderedList')"><core-icon name="fa-list-ol"></core-icon></button>
<button [core-suppress-events] (onClick)="buttonAction($event, 'removeFormat')"><core-icon name="fa-eraser"></core-icon></button>
<button [core-suppress-events] (onClick)="toggleEditor($event)"><core-icon name="fa-code"></core-icon> {{ 'core.viewcode' | translate }}</button>
</div>
</div>
<div [hidden]="!rteEnabled" #editor contenteditable="true" class="core-rte-editor" tappable (focus)="showToolbar()" (longPress)="showToolbar()" [attr.data-placeholder-text]="placeholder" role="textbox">
</div>
<div [hidden]="rteEnabled">
<ion-textarea #textarea class="core-textarea" [placeholder]="placeholder" [attr.name]="name" ngControl="control" (ionChange)="onChange($event)" role="textbox"></ion-textarea>
<div class="core-rte-toolbar" [hidden]="!editorSupported">
<div #decorate class="core-rte-buttons">
<button tappable [core-suppress-events] (onClick)="toggleEditor($event)"><core-icon name="fa-pencil-square-o"></core-icon> {{ 'core.vieweditor' | translate }}</button>
</div>
</div>
<ion-textarea [hidden]="rteEnabled" #textarea class="core-textarea" [placeholder]="placeholder" [attr.name]="name" ngControl="control" (ionChange)="onChange($event)" (focus)="showToolbar()" (longPress)="showToolbar()" (longPress)="showToolbar()" role="textbox"></ion-textarea>
<div #toolbar class="core-rte-toolbar" [class.toolbar-hidden]="toolbarHidden">
<button *ngIf="toolbarArrows" class="toolbar-arrow" [class.toolbar-arrow-hidden]="toolbarPrevHidden" (click)="toolbarPrev()">
<ion-icon name="arrow-back" md="ios-arrow-back"></ion-icon>
</button>
<ion-slides [slidesPerView]="numToolbarButtons" (ionSlideDidChange)="updateToolbarArrows()">
<!-- https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand -->
<ion-slide>
<button [disabled]="!rteEnabled" [attr.aria-pressed]="toolbarStyles.b" (click)="buttonAction($event, 'bold')">
<core-icon name="fa-bold"></core-icon>
</button>
</ion-slide>
<ion-slide>
<button [disabled]="!rteEnabled" [attr.aria-pressed]="toolbarStyles.i" (click)="buttonAction($event, 'italic')">
<core-icon name="fa-italic"></core-icon>
</button>
</ion-slide>
<ion-slide>
<button [disabled]="!rteEnabled" [attr.aria-pressed]="toolbarStyles.u" (click)="buttonAction($event, 'underline')">
<core-icon name="fa-underline"></core-icon>
</button>
</ion-slide>
<ion-slide>
<button [disabled]="!rteEnabled" [attr.aria-pressed]="toolbarStyles.strike" (click)="buttonAction($event, 'strikeThrough')">
<core-icon name="fa-strikethrough"></core-icon>
</button>
</ion-slide>
<ion-slide>
<button [disabled]="!rteEnabled" [attr.aria-pressed]="toolbarStyles.p" (click)="buttonAction($event, 'formatBlock|<p>')">
<core-icon name="fa-paragraph"></core-icon>
</button>
</ion-slide>
<ion-slide>
<button [disabled]="!rteEnabled" [attr.aria-pressed]="toolbarStyles.h1" (click)="buttonAction($event, 'formatBlock|<h1>')">
<core-icon name="fa-header"></core-icon>1
</button>
</ion-slide>
<ion-slide>
<button [disabled]="!rteEnabled" [attr.aria-pressed]="toolbarStyles.h2" (click)="buttonAction($event, 'formatBlock|<h2>')">
<core-icon name="fa-header"></core-icon>2
</button>
</ion-slide>
<ion-slide>
<button [disabled]="!rteEnabled" [attr.aria-pressed]="toolbarStyles.h3" (click)="buttonAction($event, 'formatBlock|<h3>')">
<core-icon name="fa-header"></core-icon>3
</button>
</ion-slide>
<ion-slide>
<button [disabled]="!rteEnabled" [attr.aria-pressed]="toolbarStyles.ul" (click)="buttonAction($event, 'insertUnorderedList')">
<core-icon name="fa-list-ul"></core-icon>
</button>
</ion-slide>
<ion-slide>
<button [disabled]="!rteEnabled" [attr.aria-pressed]="toolbarStyles.ol" (click)="buttonAction($event, 'insertOrderedList')">
<core-icon name="fa-list-ol"></core-icon>
</button>
</ion-slide>
<ion-slide>
<button [disabled]="!rteEnabled" (click)="buttonAction($event, 'removeFormat')">
<core-icon name="fa-eraser"></core-icon>
</button>
</ion-slide>
<ion-slide>
<button [attr.aria-pressed]="rteEnabled ? 'false' : 'true'" (click)="toggleEditor($event)">
<core-icon name="fa-code"></core-icon>
</button>
</ion-slide>
<ion-slide *ngIf="isPhone">
<button (click)="hideToolbar()">
<core-icon name="fa-close"></core-icon>
</button>
</ion-slide>
</ion-slides>
<button *ngIf="toolbarArrows" class="toolbar-arrow" [class.toolbar-arrow-hidden]="toolbarNextHidden" (click)="toolbarNext()">
<ion-icon name="arrow-forward" md="ios-arrow-forward"></ion-icon>
</button>
</div>

View File

@ -4,27 +4,20 @@ ion-app.app-root core-rich-text-editor {
min-height: 200px; /* Just in case vh is not supported */
min-height: 40vh;
width: 100%;
position: relative;
display: block;
display: flex;
flex-direction: column;
> div {
position: absolute;
@include position(0, 0, 0, 0);
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;
}
.core-rte-editor {
flex-grow: 1;
flex-shrink: 1;
-webkit-user-select: auto !important;
word-wrap: break-word;
overflow-x: hidden;
@ -48,6 +41,8 @@ ion-app.app-root core-rich-text-editor {
}
.core-textarea {
flex-grow: 1;
flex-shrink: 1;
position: relative;
textarea {
@ -64,33 +59,64 @@ ion-app.app-root core-rich-text-editor {
}
div.core-rte-toolbar {
background: $gray-darker;
@include margin(0px, 1px, 15px, 1px);
text-align: center;
flex-grow: 0;
display: flex;
width: 100%;
z-index: 1;
flex-grow: 0;
flex-shrink: 0;
background-color: $white;
@include padding(5px, null);
border-top: 1px solid $gray;
.core-rte-buttons {
ion-slides {
width: 240px;
flex-grow: 1;
flex-shrink: 1;
}
button {
display: flex;
justify-content: center;
align-items: center;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-evenly;
width: 36px;
height: 36px;
margin: 0 auto;
font-size: 18px;
background-color: $white;
border-radius: 4px;
@include core-transition(background-color, 200ms);
color: $text-color;
cursor: pointer;
button {
background: $gray-darker;
color: $white;
font-size: 1.1em;
height: 35px;
min-width: 30px;
@include padding(null, 3px, null, 3px);
@include border-end(qpx, solid, $gray-dark);
border-bottom: 1px solid $gray-dark;
@include position(-6px, 0, null, null);
flex-grow: 1;
margin: 0;
&.toolbar-button-enable {
width: 100%;
}
&:active, &[aria-pressed="true"] {
background-color: $gray;
}
&.toolbar-arrow {
width: 28px;
flex-grow: 0;
flex-shrink: 0;
opacity: 1;
@include core-transition(opacity, 200ms);
&:active {
background-color: $white;
}
&.toolbar-arrow-hidden {
opacity: 0;
}
}
}
&.toolbar-hidden {
visibility: none;
height: 0;
border: none;
}
}

View File

@ -14,7 +14,7 @@
import { Component, Input, Output, EventEmitter, ViewChild, ElementRef, AfterContentInit, OnDestroy, Optional }
from '@angular/core';
import { TextInput, Content, Platform } from 'ionic-angular';
import { TextInput, Content, Platform, Slides } from 'ionic-angular';
import { CoreSitesProvider } from '@providers/sites';
import { CoreFilepoolProvider } from '@providers/filepool';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
@ -56,11 +56,9 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy
@ViewChild('editor') editor: ElementRef; // WYSIWYG editor.
@ViewChild('textarea') textarea: TextInput; // Textarea editor.
@ViewChild('decorate') decorate: ElementRef; // Buttons.
protected element: HTMLDivElement;
protected editorElement: HTMLDivElement;
protected resizeFunction;
protected kbHeight = 0; // Last known keyboard height.
protected minHeight = 200; // Minimum height of the editor.
@ -71,6 +69,31 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy
rteEnabled = false;
editorSupported = true;
// Toolbar.
@ViewChild('toolbar') toolbar: ElementRef;
@ViewChild(Slides) toolbarSlides: Slides;
isPhone = this.platform.is('mobile') && !this.platform.is('tablet');
toolbarHidden = this.isPhone;
numToolbarButtons = 6;
toolbarArrows = false;
toolbarPrevHidden = true;
toolbarNextHidden = false;
toolbarStyles = {
b: 'false',
i: 'false',
u: 'false',
strike: 'false',
p: 'false',
h1: 'false',
h2: 'false',
h3: 'false',
ul: 'false',
ol: 'false',
};
protected isCurrentView = true;
protected toolbarButtonWidth = 40;
protected toolbarArrowWidth = 28;
constructor(private domUtils: CoreDomUtilsProvider, private urlUtils: CoreUrlUtilsProvider,
private sitesProvider: CoreSitesProvider, private filepoolProvider: CoreFilepoolProvider,
@Optional() private content: Content, elementRef: ElementRef, private events: CoreEventsProvider,
@ -106,8 +129,8 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy
// Use paragraph on enter.
document.execCommand('DefaultParagraphSeparator', false, 'p');
this.resizeFunction = this.maximizeEditorSize.bind(this);
window.addEventListener('resize', this.resizeFunction);
window.addEventListener('resize', this.maximizeEditorSize);
document.addEventListener('selectionchange', this.updateToolbarStyles);
let i = 0;
this.initHeightInterval = setInterval(() => {
@ -123,6 +146,8 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy
this.kbHeight = kbHeight;
this.maximizeEditorSize();
});
this.updateToolbarButtons();
}
/**
@ -130,13 +155,17 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy
*
* @return {Promise<number>} Resolved with calculated editor size.
*/
protected maximizeEditorSize(): Promise<number> {
protected maximizeEditorSize = (): Promise<number> => {
this.content.resize();
const deferred = this.utils.promiseDefer();
setTimeout(() => {
const contentVisibleHeight = this.domUtils.getContentHeight(this.content) - this.kbHeight;
let contentVisibleHeight = this.domUtils.getContentHeight(this.content);
if (!this.platform.is('android')) {
// In Android we ignore the keyboard height because it is not part of the web view.
contentVisibleHeight -= this.kbHeight;
}
if (contentVisibleHeight <= 0) {
deferred.resolve(0);
@ -149,7 +178,7 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy
let height;
if (this.platform.is('android')) {
// Android, ignore keyboard height because web view is resized.
// In Android we ignore the keyboard height because it is not part of the web view.
height = this.domUtils.getContentHeight(this.content) - this.getSurroundingHeight(this.element);
} else if (this.platform.is('ios') && this.kbHeight > 0) {
// Keyboard open in iOS.
@ -386,13 +415,16 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy
this.rteEnabled = !this.rteEnabled;
// Set focus and cursor at the end.
setTimeout(() => {
if (this.rteEnabled) {
this.editorElement.focus();
} else {
this.textarea.setFocus();
}
});
// Modify the DOM directly so the keyboard stays open.
if (this.rteEnabled) {
this.editorElement.removeAttribute('hidden');
this.textarea.getNativeElement().setAttribute('hidden', '');
this.editorElement.focus();
} else {
this.editorElement.setAttribute('hidden', '');
this.textarea.getNativeElement().removeAttribute('hidden');
this.textarea.setFocus();
}
}
/**
@ -504,6 +536,7 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy
protected buttonAction($event: any, command: string): void {
$event.preventDefault();
$event.stopPropagation();
this.editorElement.focus();
if (command) {
if (command.includes('|')) {
@ -517,12 +550,135 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy
}
}
/**
* Hide the toolbar.
*/
hideToolbar(): void {
this.editorElement.focus();
this.toolbarHidden = true;
}
/**
* Show the toolbar.
*/
showToolbar(): void {
this.editorElement.focus();
this.toolbarHidden = false;
}
/**
* Method that shows the next toolbar buttons.
*/
toolbarNext(): void {
if (!this.toolbarNextHidden) {
const currentIndex = this.toolbarSlides.getActiveIndex() || 0;
this.toolbarSlides.slideTo(currentIndex + this.numToolbarButtons);
}
this.editorElement.focus();
}
/**
* Method that shows the previous toolbar buttons.
*/
toolbarPrev(): void {
if (!this.toolbarPrevHidden) {
const currentIndex = this.toolbarSlides.getActiveIndex() || 0;
this.toolbarSlides.slideTo(currentIndex - this.numToolbarButtons);
}
this.editorElement.focus();
}
/**
* Update the number of toolbar buttons displayed.
*/
updateToolbarButtons(): void {
if (!this.isCurrentView) {
// Don't calculate if component isn't in current view, the calculations are wrong.
return;
}
if (!(this.toolbarSlides as any)._init) {
// Slides is not initialized yet, try later.
setTimeout(this.updateToolbarButtons.bind(this), 100);
return;
}
const width = this.domUtils.getElementWidth(this.toolbar.nativeElement);
if (width > this.toolbarSlides.length() * this.toolbarButtonWidth) {
this.numToolbarButtons = this.toolbarSlides.length();
this.toolbarArrows = false;
} else {
this.numToolbarButtons = Math.floor((width - this.toolbarArrowWidth * 2) / this.toolbarButtonWidth);
this.toolbarArrows = true;
}
this.toolbarSlides.update();
this.updateToolbarArrows();
}
/**
* Show or hide next/previous toolbar arrows.
*/
updateToolbarArrows(): void {
const currentIndex = this.toolbarSlides.getActiveIndex() || 0;
this.toolbarPrevHidden = currentIndex <= 0;
this.toolbarNextHidden = currentIndex + this.numToolbarButtons >= this.toolbarSlides.length();
}
/**
* Update highlighted toolbar styles.
*/
updateToolbarStyles = (): void => {
const node = document.getSelection().focusNode;
if (!node) {
return;
}
let element = node.nodeType == 1 ? node as HTMLElement : node.parentElement;
const styles = {};
while (element != null && element !== this.editorElement) {
const tagName = element.tagName.toLowerCase();
if (this.toolbarStyles[tagName]) {
styles[tagName] = 'true';
}
element = element.parentElement;
}
for (const tagName in this.toolbarStyles) {
this.toolbarStyles[tagName] = 'false';
}
if (element === this.editorElement) {
Object.assign(this.toolbarStyles, styles);
}
}
/**
* User entered the page that contains the component.
*/
ionViewDidEnter(): void {
this.isCurrentView = true;
this.updateToolbarButtons();
}
/**
* User left the page that contains the component.
*/
ionViewDidLeave(): void {
this.isCurrentView = false;
}
/**
* Component being destroyed.
*/
ngOnDestroy(): void {
this.valueChangeSubscription && this.valueChangeSubscription.unsubscribe();
window.removeEventListener('resize', this.resizeFunction);
window.removeEventListener('resize', this.maximizeEditorSize);
document.removeEventListener('selectionchange', this.updateToolbarStyles);
clearInterval(this.initHeightInterval);
this.keyboardObs && this.keyboardObs.off();
}