MOBILE-3053 rte: Scrollable toolbar
parent
167cb339ff
commit
51086fa848
|
@ -1,33 +1,81 @@
|
|||
<div [hidden]="!rteEnabled">
|
||||
<div #editor contenteditable="true" class="core-rte-editor" tappable [attr.data-placeholder-text]="placeholder" role="textbox">
|
||||
</div>
|
||||
<div [hidden]="!rteEnabled" #editor contenteditable="true" class="core-rte-editor" tappable (focus)="showToolbar()" (longPress)="showToolbar()" [attr.data-placeholder-text]="placeholder" role="textbox">
|
||||
</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 -->
|
||||
<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>
|
||||
<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 [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>
|
||||
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
> 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 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-evenly;
|
||||
ion-slides {
|
||||
width: 240px;
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
|
||||
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;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
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;
|
||||
|
||||
&.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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,7 +56,6 @@ 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;
|
||||
|
@ -71,6 +70,20 @@ 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;
|
||||
|
||||
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,
|
||||
|
@ -123,6 +136,8 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy
|
|||
this.kbHeight = kbHeight;
|
||||
this.maximizeEditorSize();
|
||||
});
|
||||
|
||||
this.updateToolbarButtons();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -390,13 +405,16 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy
|
|||
this.rteEnabled = !this.rteEnabled;
|
||||
|
||||
// Set focus and cursor at the end.
|
||||
setTimeout(() => {
|
||||
// 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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -508,6 +526,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('|')) {
|
||||
|
@ -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.
|
||||
*/
|
||||
|
|
Loading…
Reference in New Issue