diff --git a/src/addon/mod/assign/feedback/comments/component/comments.html b/src/addon/mod/assign/feedback/comments/component/comments.html index 1a0fcbcad..3732d55bb 100644 --- a/src/addon/mod/assign/feedback/comments/component/comments.html +++ b/src/addon/mod/assign/feedback/comments/component/comments.html @@ -19,6 +19,5 @@ - - + diff --git a/src/addon/mod/assign/submission/onlinetext/component/onlinetext.html b/src/addon/mod/assign/submission/onlinetext/component/onlinetext.html index e74173388..fde89ebe0 100644 --- a/src/addon/mod/assign/submission/onlinetext/component/onlinetext.html +++ b/src/addon/mod/assign/submission/onlinetext/component/onlinetext.html @@ -15,7 +15,6 @@

{{ 'core.numwords' | translate: {'$a': words + ' / ' + configs.wordlimit} }}

- - + diff --git a/src/addon/mod/forum/components/post/post.html b/src/addon/mod/forum/components/post/post.html index 3e89f3252..9e6fc37f1 100644 --- a/src/addon/mod/forum/components/post/post.html +++ b/src/addon/mod/forum/components/post/post.html @@ -38,9 +38,7 @@ {{ 'addon.mod_forum.message' | translate }} - - + diff --git a/src/addon/mod/forum/pages/new-discussion/new-discussion.html b/src/addon/mod/forum/pages/new-discussion/new-discussion.html index 096edd01d..228adea02 100644 --- a/src/addon/mod/forum/pages/new-discussion/new-discussion.html +++ b/src/addon/mod/forum/pages/new-discussion/new-discussion.html @@ -19,9 +19,7 @@ {{ 'addon.mod_forum.message' | translate }} - - + {{ 'addon.mod_forum.group' | translate }} diff --git a/src/addon/mod/wiki/components/index/index.scss b/src/addon/mod/wiki/components/index/index.scss index 72166f42a..e3404377f 100644 --- a/src/addon/mod/wiki/components/index/index.scss +++ b/src/addon/mod/wiki/components/index/index.scss @@ -5,6 +5,13 @@ $addon-mod-wiki-toc-border-color: $gray-dark !default; $addon-mod-wiki-toc-background-color: $gray-light !default; addon-mod-wiki-index { + background-color: $white; + + .core-tabs-content-container, + .addon-mod_wiki-page-content { + background-color: $white; + } + .wiki-toc { border: 1px solid $addon-mod-wiki-toc-border-color; background: $addon-mod-wiki-toc-background-color; diff --git a/src/addon/mod/wiki/pages/edit/edit.html b/src/addon/mod/wiki/pages/edit/edit.html index e41b3541c..b8b029fb2 100644 --- a/src/addon/mod/wiki/pages/edit/edit.html +++ b/src/addon/mod/wiki/pages/edit/edit.html @@ -17,8 +17,7 @@ - - + {{ 'addon.mod_wiki.wrongversionlock' | translate }} diff --git a/src/addon/qtype/essay/component/essay.html b/src/addon/qtype/essay/component/essay.html index cb1a94af4..f763428f2 100644 --- a/src/addon/qtype/essay/component/essay.html +++ b/src/addon/qtype/essay/component/essay.html @@ -11,9 +11,7 @@ - - + diff --git a/src/app/app.scss b/src/app/app.scss index 8d03df376..510ee0f62 100644 --- a/src/app/app.scss +++ b/src/app/app.scss @@ -32,7 +32,7 @@ .img-responsive { display: block; max-width: 100%; - /* height: auto; */ + height: auto; } .opacity-hide { opacity: 0; } diff --git a/src/components/rich-text-editor/rich-text-editor.html b/src/components/rich-text-editor/rich-text-editor.html index e4a71afd3..14b4c5dde 100644 --- a/src/components/rich-text-editor/rich-text-editor.html +++ b/src/components/rich-text-editor/rich-text-editor.html @@ -3,26 +3,30 @@ -
- - - - - - - - - - - - +
+
+ + + + + + + + + + + + +
-
- +
+
+ +
diff --git a/src/components/rich-text-editor/rich-text-editor.scss b/src/components/rich-text-editor/rich-text-editor.scss index a850981f3..366bfef2d 100644 --- a/src/components/rich-text-editor/rich-text-editor.scss +++ b/src/components/rich-text-editor/rich-text-editor.scss @@ -10,7 +10,6 @@ core-rich-text-editor { display: flex; flex-direction: column; } - .core-rte-editor, .core-textarea { padding: 2px; margin: 2px; @@ -51,21 +50,33 @@ core-rich-text-editor { overflow-y: auto; } - div.formatOptions { - background: $gray-dark; - margin: 5px 1px 15px 1px; + div.core-rte-toolbar { + background: $gray-darker; + margin: 0px 1px 15px 1px; text-align: center; flex-grow: 0; width: 100%; z-index: 1; - button { - background: $gray-dark; - color: $white; - font-size: 1.1em; - height: 35px; - min-width: 30px; - padding-left: 1px; - padding-right: 1px; + + .core-rte-buttons { + display: flex; + align-items: center; + flex-direction: row; + flex-wrap: wrap; + justify-content: space-evenly; + + button { + background: $gray-darker; + color: $white; + font-size: 1.1em; + height: 35px; + min-width: 30px; + padding-left: 3px; + padding-right: 3px; + border-right: 1px solid $gray-dark; + border-bottom: 1px solid $gray-dark; + flex-grow: 1; + } } } diff --git a/src/components/rich-text-editor/rich-text-editor.ts b/src/components/rich-text-editor/rich-text-editor.ts index 4eac21804..2bc66557c 100644 --- a/src/components/rich-text-editor/rich-text-editor.ts +++ b/src/components/rich-text-editor/rich-text-editor.ts @@ -12,9 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, Input, Output, EventEmitter, ViewChild, ElementRef, AfterContentInit, OnDestroy } from '@angular/core'; -import { TextInput } from 'ionic-angular'; +import { Component, Input, Output, EventEmitter, ViewChild, ElementRef, AfterContentInit, OnDestroy, Optional } + from '@angular/core'; +import { TextInput, Content } from 'ionic-angular'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreFilepoolProvider } from '@providers/filepool'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreUrlUtilsProvider } from '@providers/utils/url'; import { FormControl } from '@angular/forms'; import { Keyboard } from '@ionic-native/keyboard'; import { Subscription } from 'rxjs'; @@ -39,25 +43,32 @@ import { Subscription } from 'rxjs'; }) export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy { // Based on: https://github.com/judgewest2000/Ionic3RichText/ - // @todo: Resize, images, anchor button, fullscreen... + // @todo: Anchor button, fullscreen... @Input() placeholder = ''; // Placeholder to set in textarea. @Input() control: FormControl; // Form control. @Input() name = 'core-rich-text-editor'; // Name to set to the textarea. + @Input() component?: string; // The component to link the files to. + @Input() componentId?: number; // An ID to use in conjunction with the component. @Output() contentChanged: EventEmitter; @ViewChild('editor') editor: ElementRef; // WYSIWYG editor. @ViewChild('textarea') textarea: TextInput; // Textarea editor. @ViewChild('decorate') decorate: ElementRef; // Buttons. - rteEnabled = false; - uniqueId = `rte{Math.floor(Math.random() * 1000000)}`; - editorElement: HTMLDivElement; + protected element: HTMLDivElement; + protected editorElement: HTMLDivElement; + protected resizeFunction; protected valueChangeSubscription: Subscription; - constructor(private domUtils: CoreDomUtilsProvider, private keyboard: Keyboard) { + rteEnabled = false; + + constructor(private domUtils: CoreDomUtilsProvider, private keyboard: Keyboard, private urlUtils: CoreUrlUtilsProvider, + private sitesProvider: CoreSitesProvider, private filepoolProvider: CoreFilepoolProvider, + @Optional() private content: Content, elementRef: ElementRef) { this.contentChanged = new EventEmitter(); + this.element = elementRef.nativeElement as HTMLDivElement; } /** @@ -104,6 +115,58 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy } } } + + this.treatExternalContent(); + + this.resizeFunction = this.maximizeEditorSize.bind(this); + window.addEventListener('resize', this.resizeFunction); + setTimeout(this.resizeFunction, 1000); + } + + /** + * Resize editor to maximize the space occupied. + */ + protected maximizeEditorSize(): void { + this.content.resize(); + const contentVisibleHeight = this.content.contentHeight; + + // Editor is ready, adjust Height if needed. + if (contentVisibleHeight > 0) { + const height = this.getSurroundingHeight(this.element); + if (contentVisibleHeight > height) { + this.element.style.height = this.domUtils.formatPixelsSize(contentVisibleHeight - height); + } else { + this.element.style.height = ''; + } + } + } + + /** + * Get the height of the surrounding elements from the current to the top element. + * + * @param {any} element Directive DOM element to get surroundings elements from. + * @return {number} Surrounding height in px. + */ + protected getSurroundingHeight(element: any): number { + let height = 0; + + while (element.parentNode && element.parentNode.tagName != 'ION-CONTENT') { + const parent = element.parentNode; + if (element.tagName && element.tagName != 'CORE-LOADING') { + parent.childNodes.forEach((child) => { + if (child.tagName && child != element) { + height += this.domUtils.getElementHeight(child, false, true, true); + } + }); + } + element = parent; + } + + const cs = getComputedStyle(element); + height += this.domUtils.getComputedStyleMeasure(cs, 'paddingTop') + + this.domUtils.getComputedStyleMeasure(cs, 'paddingBottom'); + + return height; } /** @@ -171,6 +234,30 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy }, 1); } + /** + * Treat elements that can contain external content. + * We only search for images because the editor should receive unfiltered text, so the multimedia filter won't be applied. + * Treating videos and audios in here is complex, so if a user manually adds one he won't be able to play it in the editor. + */ + protected treatExternalContent(): void { + const elements = Array.from(this.editorElement.querySelectorAll('img')), + siteId = this.sitesProvider.getCurrentSiteId(), + canDownloadFiles = this.sitesProvider.getCurrentSite().canDownloadFiles(); + elements.forEach((el) => { + const url = el.src; + + if (!url || !this.urlUtils.isDownloadableUrl(url) || (!canDownloadFiles && this.urlUtils.isPluginFileUrl(url))) { + // Nothing to treat. + return; + } + + // Check if it's downloaded. + return this.filepoolProvider.getSrcByUrl(siteId, url, this.component, this.componentId).then((finalUrl) => { + el.setAttribute('src', finalUrl); + }); + }); + } + /** * Check if text is empty. * @param {string} value text @@ -215,5 +302,6 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy */ ngOnDestroy(): void { this.valueChangeSubscription && this.valueChangeSubscription.unsubscribe(); + window.removeEventListener('resize', this.resizeFunction); } } diff --git a/src/lang/en.json b/src/lang/en.json index 0a5d19c35..84768daed 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -227,6 +227,8 @@ "usernotfullysetup": "User not fully set-up", "users": "Users", "view": "View", + "viewcode": "View code", + "vieweditor": "View editor", "viewprofile": "View profile", "warningofflinedatadeleted": "Offline data of {{component}} '{{name}}' has been deleted. {{error}}", "whatisyourage": "What is your age?", diff --git a/src/providers/utils/dom.ts b/src/providers/utils/dom.ts index e52e75121..c66f47855 100644 --- a/src/providers/utils/dom.ts +++ b/src/providers/utils/dom.ts @@ -365,13 +365,16 @@ export class CoreDomUtilsProvider { let surround = 0; if (usePadding) { - surround += parseInt(computedStyle['padding' + priorSide], 10) + parseInt(computedStyle['padding' + afterSide], 10); + surround += this.getComputedStyleMeasure(computedStyle, 'padding' + priorSide) + + this.getComputedStyleMeasure(computedStyle, 'padding' + afterSide); } if (useMargin) { - surround += parseInt(computedStyle['margin' + priorSide], 10) + parseInt(computedStyle['margin' + afterSide], 10); + surround += this.getComputedStyleMeasure(computedStyle, 'margin' + priorSide) + + this.getComputedStyleMeasure(computedStyle, 'margin' + afterSide); } if (useBorder) { - surround += parseInt(computedStyle['border' + priorSide], 10) + parseInt(computedStyle['border' + afterSide], 10); + surround += this.getComputedStyleMeasure(computedStyle, 'border' + priorSide + 'Width') + + this.getComputedStyleMeasure(computedStyle, 'border' + afterSide + 'Width'); } if (innerMeasure) { measure = measure > surround ? measure - surround : 0; @@ -381,7 +384,17 @@ export class CoreDomUtilsProvider { } return measure; + } + /** + * Returns the computed style measure or 0 if not found or NaN. + * + * @param {any} style Style from getComputedStyle. + * @param {string} measure Measure to get. + * @return {number} Result of the measure. + */ + getComputedStyleMeasure(style: any, measure: string): number { + return parseInt(style[measure], 10) || 0; } /**