MOBILE-3053 rte: Scrollable toolbar
parent
167cb339ff
commit
51086fa848
|
@ -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>
|
|
||||||
|
|
||||||
<!-- 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>
|
</div>
|
||||||
|
|
||||||
<div [hidden]="rteEnabled">
|
<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>
|
||||||
<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 #toolbar class="core-rte-toolbar" [class.toolbar-hidden]="toolbarHidden">
|
||||||
<div #decorate class="core-rte-buttons">
|
<button *ngIf="toolbarArrows" class="toolbar-arrow" [class.toolbar-arrow-hidden]="toolbarPrevHidden" (click)="toolbarPrev()">
|
||||||
<button tappable [core-suppress-events] (onClick)="toggleEditor($event)"><core-icon name="fa-pencil-square-o"></core-icon> {{ 'core.vieweditor' | translate }}</button>
|
<ion-icon name="arrow-back" md="ios-arrow-back"></ion-icon>
|
||||||
</div>
|
</button>
|
||||||
</div>
|
<ion-slides [slidesPerView]="numToolbarButtons" (ionSlideDidChange)="updateToolbarArrows()">
|
||||||
|
<!-- https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand -->
|
||||||
|
<ion-slide>
|
||||||
|
<button [disabled]="!rteEnabled" (click)="buttonAction($event, 'bold')">
|
||||||
|
<core-icon name="fa-bold"></core-icon>
|
||||||
|
</button>
|
||||||
|
</ion-slide>
|
||||||
|
<ion-slide>
|
||||||
|
<button [disabled]="!rteEnabled" (click)="buttonAction($event, 'italic')">
|
||||||
|
<core-icon name="fa-italic"></core-icon>
|
||||||
|
</button>
|
||||||
|
</ion-slide>
|
||||||
|
<ion-slide>
|
||||||
|
<button [disabled]="!rteEnabled" (click)="buttonAction($event, 'underline')">
|
||||||
|
<core-icon name="fa-underline"></core-icon>
|
||||||
|
</button>
|
||||||
|
</ion-slide>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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: flex;
|
||||||
display: block;
|
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 {
|
.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 {
|
||||||
|
width: 240px;
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-shrink: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-direction: row;
|
width: 36px;
|
||||||
flex-wrap: wrap;
|
height: 36px;
|
||||||
justify-content: space-evenly;
|
margin: 0 auto;
|
||||||
|
font-size: 18px;
|
||||||
|
background-color: $white;
|
||||||
|
border-radius: 4px;
|
||||||
|
@include core-transition(background-color, 200ms);
|
||||||
|
color: $text-color;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
button {
|
&.toolbar-button-enable {
|
||||||
background: $gray-darker;
|
width: 100%;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&: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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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.focus();
|
this.editorElement.removeAttribute('hidden');
|
||||||
} else {
|
this.textarea.getNativeElement().setAttribute('hidden', '');
|
||||||
this.textarea.setFocus();
|
this.editorElement.focus();
|
||||||
}
|
} else {
|
||||||
});
|
this.editorElement.setAttribute('hidden', '');
|
||||||
|
this.textarea.getNativeElement().removeAttribute('hidden');
|
||||||
|
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.
|
||||||
*/
|
*/
|
||||||
|
|
Loading…
Reference in New Issue