diff --git a/src/components/rich-text-editor/core-rich-text-editor.html b/src/components/rich-text-editor/core-rich-text-editor.html index 7a02610dd..7874c0162 100644 --- a/src/components/rich-text-editor/core-rich-text-editor.html +++ b/src/components/rich-text-editor/core-rich-text-editor.html @@ -1,33 +1,81 @@ -
-
-
- - -
-
- - - - - - - - - - - - -
-
+
-
- -
-
- -
-
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
- - diff --git a/src/components/rich-text-editor/rich-text-editor.scss b/src/components/rich-text-editor/rich-text-editor.scss index 8bce984e8..0cf3a5960 100644 --- a/src/components/rich-text-editor/rich-text-editor.scss +++ b/src/components/rich-text-editor/rich-text-editor.scss @@ -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; } } diff --git a/src/components/rich-text-editor/rich-text-editor.ts b/src/components/rich-text-editor/rich-text-editor.ts index 59fdabe77..45e572d37 100644 --- a/src/components/rich-text-editor/rich-text-editor.ts +++ b/src/components/rich-text-editor/rich-text-editor.ts @@ -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} Resolved with calculated editor size. */ - protected maximizeEditorSize(): Promise { + protected maximizeEditorSize = (): Promise => { 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(); }