MOBILE-3053 rte: Scrollable toolbar

main
Albert Gasset 2019-06-19 15:31:21 +02:00
parent 167cb339ff
commit 51086fa848
3 changed files with 256 additions and 70 deletions

View File

@ -1,33 +1,81 @@
<div [hidden]="!rteEnabled"> <div [hidden]="!rteEnabled" #editor contenteditable="true" class="core-rte-editor" tappable (focus)="showToolbar()" (longPress)="showToolbar()" [attr.data-placeholder-text]="placeholder" role="textbox">
<div #editor contenteditable="true" class="core-rte-editor" tappable [attr.data-placeholder-text]="placeholder" role="textbox"> </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 --> <!-- https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand -->
<div #decorate class="core-rte-toolbar"> <ion-slide>
<div class="core-rte-buttons"> <button [disabled]="!rteEnabled" (click)="buttonAction($event, 'bold')">
<button [core-suppress-events] (onClick)="buttonAction($event, 'bold')"><core-icon name="fa-bold"></core-icon></button> <core-icon name="fa-bold"></core-icon>
<button [core-suppress-events] (onClick)="buttonAction($event, 'italic')"><core-icon name="fa-italic"></core-icon></button> </button>
<button [core-suppress-events] (onClick)="buttonAction($event, 'underline')"><core-icon name="fa-underline"></core-icon></button> </ion-slide>
<button [core-suppress-events] (onClick)="buttonAction($event, 'strikeThrough')"><core-icon name="fa-strikethrough"></core-icon></button> <ion-slide>
<button [core-suppress-events] (onClick)="buttonAction($event, 'formatBlock|<p>')"><core-icon name="fa-paragraph"></core-icon></button> <button [disabled]="!rteEnabled" (click)="buttonAction($event, 'italic')">
<button [core-suppress-events] (onClick)="buttonAction($event, 'formatBlock|<h1>')"><core-icon name="fa-header"></core-icon>1</button> <core-icon name="fa-italic"></core-icon>
<button [core-suppress-events] (onClick)="buttonAction($event, 'formatBlock|<h2>')"><core-icon name="fa-header"></core-icon>2</button> </button>
<button [core-suppress-events] (onClick)="buttonAction($event, 'formatBlock|<h3>')"><core-icon name="fa-header"></core-icon>3</button> </ion-slide>
<button [core-suppress-events] (onClick)="buttonAction($event, 'insertUnorderedList')"><core-icon name="fa-list-ul"></core-icon></button> <ion-slide>
<button [core-suppress-events] (onClick)="buttonAction($event, 'insertOrderedList')"><core-icon name="fa-list-ol"></core-icon></button> <button [disabled]="!rteEnabled" (click)="buttonAction($event, 'underline')">
<button [core-suppress-events] (onClick)="buttonAction($event, 'removeFormat')"><core-icon name="fa-eraser"></core-icon></button> <core-icon name="fa-underline"></core-icon>
<button [core-suppress-events] (onClick)="toggleEditor($event)"><core-icon name="fa-code"></core-icon> {{ 'core.viewcode' | translate }}</button> </button>
</div> </ion-slide>
</div> <ion-slide>
<button [disabled]="!rteEnabled" (click)="buttonAction($event, 'strikeThrough')">
<core-icon name="fa-strikethrough"></core-icon>
</button>
</ion-slide>
<ion-slide>
<button [disabled]="!rteEnabled" (click)="buttonAction($event, 'formatBlock|<p>')">
<core-icon name="fa-paragraph"></core-icon>
</button>
</ion-slide>
<ion-slide>
<button [disabled]="!rteEnabled" (click)="buttonAction($event, 'formatBlock|<h1>')">
<core-icon name="fa-header"></core-icon>1
</button>
</ion-slide>
<ion-slide>
<button [disabled]="!rteEnabled" (click)="buttonAction($event, 'formatBlock|<h2>')">
<core-icon name="fa-header"></core-icon>2
</button>
</ion-slide>
<ion-slide>
<button [disabled]="!rteEnabled" (click)="buttonAction($event, 'formatBlock|<h3>')">
<core-icon name="fa-header"></core-icon>3
</button>
</ion-slide>
<ion-slide>
<button [disabled]="!rteEnabled"(click)="buttonAction($event, 'insertUnorderedList')">
<core-icon name="fa-list-ul"></core-icon>
</button>
</ion-slide>
<ion-slide>
<button [disabled]="!rteEnabled" (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> </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>
</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: 200px; /* Just in case vh is not supported */
min-height: 40vh; min-height: 40vh;
width: 100%; width: 100%;
position: relative;
display: block;
> div {
position: absolute;
@include position(0, 0, 0, 0);
height: 100%;
width: 100%;
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;
width: 100%; width: 100%;
resize: none; resize: none;
background-color: $white; background-color: $white;
flex-grow: 1;
} }
.core-rte-editor { .core-rte-editor {
flex-grow: 1;
flex-shrink: 1;
-webkit-user-select: auto !important; -webkit-user-select: auto !important;
word-wrap: break-word; word-wrap: break-word;
overflow-x: hidden; overflow-x: hidden;
@ -48,6 +41,8 @@ ion-app.app-root core-rich-text-editor {
} }
.core-textarea { .core-textarea {
flex-grow: 1;
flex-shrink: 1;
position: relative; position: relative;
textarea { textarea {
@ -64,33 +59,64 @@ ion-app.app-root core-rich-text-editor {
} }
div.core-rte-toolbar { div.core-rte-toolbar {
background: $gray-darker; display: flex;
@include margin(0px, 1px, 15px, 1px);
text-align: center;
flex-grow: 0;
width: 100%; width: 100%;
z-index: 1; 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 {
display: flex; width: 240px;
align-items: center; flex-grow: 1;
flex-direction: row; flex-shrink: 1;
flex-wrap: wrap; }
justify-content: space-evenly;
button { button {
background: $gray-darker; display: flex;
color: $white; justify-content: center;
font-size: 1.1em; align-items: center;
height: 35px; width: 36px;
min-width: 30px; height: 36px;
@include padding(null, 3px, null, 3px); margin: 0 auto;
@include border-end(qpx, solid, $gray-dark); font-size: 18px;
border-bottom: 1px solid $gray-dark; background-color: $white;
@include position(-6px, 0, null, null); border-radius: 4px;
flex-grow: 1; @include core-transition(background-color, 200ms);
margin: 0; color: $text-color;
cursor: pointer;
&.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 } import { Component, Input, Output, EventEmitter, ViewChild, ElementRef, AfterContentInit, OnDestroy, Optional }
from '@angular/core'; from '@angular/core';
import { TextInput, Content, Platform } from 'ionic-angular'; import { TextInput, Content, Platform, Slides } from 'ionic-angular';
import { CoreSitesProvider } from '@providers/sites'; import { CoreSitesProvider } from '@providers/sites';
import { CoreFilepoolProvider } from '@providers/filepool'; import { CoreFilepoolProvider } from '@providers/filepool';
import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreDomUtilsProvider } from '@providers/utils/dom';
@ -56,7 +56,6 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy
@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.
protected element: HTMLDivElement; protected element: HTMLDivElement;
protected editorElement: HTMLDivElement; protected editorElement: HTMLDivElement;
@ -71,6 +70,20 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy
rteEnabled = false; rteEnabled = false;
editorSupported = true; 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;
protected isCurrentView = true;
protected toolbarButtonWidth = 40;
protected toolbarArrowWidth = 28;
constructor(private domUtils: CoreDomUtilsProvider, private urlUtils: CoreUrlUtilsProvider, constructor(private domUtils: CoreDomUtilsProvider, private urlUtils: CoreUrlUtilsProvider,
private sitesProvider: CoreSitesProvider, private filepoolProvider: CoreFilepoolProvider, private sitesProvider: CoreSitesProvider, private filepoolProvider: CoreFilepoolProvider,
@Optional() private content: Content, elementRef: ElementRef, private events: CoreEventsProvider, @Optional() private content: Content, elementRef: ElementRef, private events: CoreEventsProvider,
@ -123,6 +136,8 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy
this.kbHeight = kbHeight; this.kbHeight = kbHeight;
this.maximizeEditorSize(); this.maximizeEditorSize();
}); });
this.updateToolbarButtons();
} }
/** /**
@ -390,13 +405,16 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy
this.rteEnabled = !this.rteEnabled; this.rteEnabled = !this.rteEnabled;
// Set focus and cursor at the end. // Set focus and cursor at the end.
setTimeout(() => { // Modify the DOM directly so the keyboard stays open.
if (this.rteEnabled) { if (this.rteEnabled) {
this.editorElement.removeAttribute('hidden');
this.textarea.getNativeElement().setAttribute('hidden', '');
this.editorElement.focus(); this.editorElement.focus();
} else { } else {
this.editorElement.setAttribute('hidden', '');
this.textarea.getNativeElement().removeAttribute('hidden');
this.textarea.setFocus(); this.textarea.setFocus();
} }
});
} }
/** /**
@ -508,6 +526,7 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy
protected buttonAction($event: any, command: string): void { protected buttonAction($event: any, command: string): void {
$event.preventDefault(); $event.preventDefault();
$event.stopPropagation(); $event.stopPropagation();
this.editorElement.focus();
if (command) { if (command) {
if (command.includes('|')) { if (command.includes('|')) {
@ -521,6 +540,99 @@ 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();
}
/**
* 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. * Component being destroyed.
*/ */