MOBILE-2253 core: Implement core directives for login site
parent
7d71868e92
commit
80961c23d9
|
@ -1031,7 +1031,7 @@ export class CoreSite {
|
|||
* @param {string} [alertMessage] If defined, an alert will be shown before opening the inappbrowser.
|
||||
* @return {Promise<any>} Promise resolved when done, rejected otherwise.
|
||||
*/
|
||||
openInAppWithAutoLoginIfSameSite(url: string, options: any, alertMessage?: string) : Promise<any> {
|
||||
openInAppWithAutoLoginIfSameSite(url: string, options?: any, alertMessage?: string) : Promise<any> {
|
||||
return this.openWithAutoLoginIfSameSite(true, url, options, alertMessage);
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Directive, Input, AfterViewInit, ElementRef } from '@angular/core';
|
||||
import { CoreDomUtilsProvider } from '../providers/utils/dom';
|
||||
|
||||
/**
|
||||
* Directive to auto focus an element when a view is loaded.
|
||||
*
|
||||
* You can apply it conditionallity assigning it a boolean value: <ion-input [mm-auto-focus]="{{showKeyboard}}">
|
||||
*/
|
||||
@Directive({
|
||||
selector: '[core-auto-focus]'
|
||||
})
|
||||
export class CoreAutoFocusDirective implements AfterViewInit {
|
||||
@Input('core-auto-focus') coreAutoFocus: boolean = true;
|
||||
|
||||
protected element: HTMLElement;
|
||||
|
||||
constructor(element: ElementRef, private domUtils: CoreDomUtilsProvider) {
|
||||
this.element = element.nativeElement || element;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function after the view is initialized.
|
||||
*/
|
||||
ngAfterViewInit() {
|
||||
this.coreAutoFocus = typeof this.coreAutoFocus != 'boolean' ? true : this.coreAutoFocus;
|
||||
if (this.coreAutoFocus) {
|
||||
// If it's a ion-input or ion-textarea, search the right input to use.
|
||||
let element = this.element;
|
||||
if (this.element.tagName == 'ION-INPUT') {
|
||||
element = this.element.querySelector('input') || element;
|
||||
} else if (this.element.tagName == 'ION-TEXTAREA') {
|
||||
element = this.element.querySelector('textarea') || element;
|
||||
}
|
||||
|
||||
// Wait a bit to make sure the view is loaded.
|
||||
setTimeout(() => {
|
||||
this.domUtils.focusElement(element);
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CoreAutoFocusDirective } from './auto-focus';
|
||||
import { CoreExternalContentDirective } from './external-content';
|
||||
import { CoreFormatTextDirective } from './format-text';
|
||||
import { CoreLinkDirective } from './link';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
CoreAutoFocusDirective,
|
||||
CoreExternalContentDirective,
|
||||
CoreFormatTextDirective,
|
||||
CoreLinkDirective
|
||||
],
|
||||
imports: [],
|
||||
exports: [
|
||||
CoreAutoFocusDirective,
|
||||
CoreExternalContentDirective,
|
||||
CoreFormatTextDirective,
|
||||
CoreLinkDirective
|
||||
]
|
||||
})
|
||||
export class CoreDirectivesModule {}
|
|
@ -0,0 +1,218 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Directive, Input, OnInit, ElementRef } from '@angular/core';
|
||||
import { Platform } from 'ionic-angular';
|
||||
import { CoreAppProvider } from '../providers/app';
|
||||
import { CoreLoggerProvider } from '../providers/logger';
|
||||
import { CoreFilepoolProvider } from '../providers/filepool';
|
||||
import { CoreSitesProvider } from '../providers/sites';
|
||||
import { CoreDomUtilsProvider } from '../providers/utils/dom';
|
||||
import { CoreUrlUtilsProvider } from '../providers/utils/url';
|
||||
|
||||
/**
|
||||
* Directive to handle external content.
|
||||
*
|
||||
* This directive should be used with any element that links to external content
|
||||
* which we want to have available when the app is offline. Typically media and links.
|
||||
*
|
||||
* If a file is downloaded, its URL will be replaced by the local file URL.
|
||||
*/
|
||||
@Directive({
|
||||
selector: '[core-external-content]'
|
||||
})
|
||||
export class CoreExternalContentDirective implements OnInit {
|
||||
@Input() siteId?: string; // Site ID to use.
|
||||
@Input() component?: string; // Component to link the file to.
|
||||
@Input() componentId?: string|number; // Component ID to use in conjunction with the component.
|
||||
|
||||
protected element: HTMLElement;
|
||||
protected logger;
|
||||
|
||||
constructor(element: ElementRef, logger: CoreLoggerProvider, private filepoolProvider: CoreFilepoolProvider,
|
||||
private platform: Platform, private sitesProvider: CoreSitesProvider, private domUtils: CoreDomUtilsProvider,
|
||||
private urlUtils: CoreUrlUtilsProvider, private appProvider: CoreAppProvider) {
|
||||
// This directive can be added dynamically. In that case, the first param is the HTMLElement.
|
||||
this.element = element.nativeElement || element;
|
||||
this.logger = logger.getInstance('CoreExternalContentDirective');
|
||||
}
|
||||
|
||||
/**
|
||||
* Function executed when the component is initialized.
|
||||
*/
|
||||
ngOnInit() {
|
||||
let currentSite = this.sitesProvider.getCurrentSite(),
|
||||
siteId = this.siteId || (currentSite && currentSite.getId()),
|
||||
targetAttr,
|
||||
sourceAttr,
|
||||
tagName = this.element.tagName;
|
||||
|
||||
if (tagName === 'A') {
|
||||
targetAttr = 'href';
|
||||
sourceAttr = 'href';
|
||||
|
||||
} else if (tagName === 'IMG') {
|
||||
targetAttr = 'src';
|
||||
sourceAttr = 'src';
|
||||
|
||||
} else if (tagName === 'AUDIO' || tagName === 'VIDEO' || tagName === 'SOURCE' || tagName === 'TRACK') {
|
||||
targetAttr = 'src';
|
||||
sourceAttr = 'targetSrc';
|
||||
|
||||
if (tagName === 'VIDEO') {
|
||||
let poster = (<HTMLVideoElement>this.element).poster;
|
||||
if (poster) {
|
||||
// Handle poster.
|
||||
this.handleExternalContent('poster', poster, siteId).catch(() => {
|
||||
// Ignore errors.
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
// Unsupported tag.
|
||||
this.logger.warn('Directive attached to non-supported tag: ' + tagName);
|
||||
return;
|
||||
}
|
||||
|
||||
let url = this.element.getAttribute(sourceAttr) || this.element.getAttribute(targetAttr);
|
||||
this.handleExternalContent(targetAttr, url, siteId).catch(() => {
|
||||
// Ignore errors.
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new source with a certain URL as a sibling of the current element.
|
||||
*
|
||||
* @param {string} url URL to use in the source.
|
||||
*/
|
||||
protected addSource(url: string) : void {
|
||||
if (this.element.tagName !== 'SOURCE') {
|
||||
return;
|
||||
}
|
||||
|
||||
let newSource = document.createElement('source'),
|
||||
type = this.element.getAttribute('type');
|
||||
|
||||
newSource.setAttribute('src', url);
|
||||
|
||||
if (type) {
|
||||
if (this.platform.is('android') && type == 'video/quicktime') {
|
||||
// Fix for VideoJS/Chrome bug https://github.com/videojs/video.js/issues/423 .
|
||||
newSource.setAttribute('type', 'video/mp4');
|
||||
} else {
|
||||
newSource.setAttribute('type', type);
|
||||
}
|
||||
}
|
||||
this.element.parentNode.insertBefore(newSource, this.element);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle external content, setting the right URL.
|
||||
*
|
||||
* @param {string} targetAttr Attribute to modify.
|
||||
* @param {string} url Original URL to treat.
|
||||
* @param {string} [siteId] Site ID.
|
||||
* @return {Promise<any>} Promise resolved if the element is successfully treated.
|
||||
*/
|
||||
protected handleExternalContent(targetAttr: string, url: string, siteId?: string) : Promise<any> {
|
||||
|
||||
const tagName = this.element.tagName;
|
||||
|
||||
if (tagName == 'VIDEO' && targetAttr != 'poster') {
|
||||
let video = <HTMLVideoElement> this.element;
|
||||
if (video.textTracks) {
|
||||
// It's a video with subtitles. In iOS, subtitles position is wrong so it needs to be fixed.
|
||||
video.textTracks.onaddtrack = (event) => {
|
||||
let track = <TextTrack> event.track;
|
||||
if (track) {
|
||||
track.oncuechange = () => {
|
||||
var line = this.platform.is('tablet') || this.platform.is('android') ? 90 : 80;
|
||||
// Position all subtitles to a percentage of video height.
|
||||
Array.from(track.cues).forEach((cue: any) => {
|
||||
cue.snapToLines = false;
|
||||
cue.line = line;
|
||||
cue.size = 100; // This solves some Android issue.
|
||||
});
|
||||
// Delete listener.
|
||||
track.oncuechange = null;
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (!url || !url.match(/^https?:\/\//i) || (tagName === 'A' && !this.urlUtils.isDownloadableUrl(url))) {
|
||||
this.logger.debug('Ignoring non-downloadable URL: ' + url);
|
||||
if (tagName === 'SOURCE') {
|
||||
// Restoring original src.
|
||||
this.addSource(url);
|
||||
}
|
||||
return Promise.reject(null);
|
||||
}
|
||||
|
||||
// Get the webservice pluginfile URL, we ignore failures here.
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
if (!site.canDownloadFiles() && this.urlUtils.isPluginFileUrl(url)) {
|
||||
this.element.parentElement.removeChild(this.element); // Remove element since it'll be broken.
|
||||
return Promise.reject(null);
|
||||
}
|
||||
|
||||
// Download images, tracks and posters if size is unknown.
|
||||
let promise,
|
||||
dwnUnknown = tagName == 'IMG' || tagName == 'TRACK' || targetAttr == 'poster';
|
||||
|
||||
if (targetAttr === 'src' && tagName !== 'SOURCE' && tagName !== 'TRACK') {
|
||||
promise = this.filepoolProvider.getSrcByUrl(siteId, url, this.component, this.componentId, 0, true, dwnUnknown);
|
||||
} else {
|
||||
promise = this.filepoolProvider.getUrlByUrl(siteId, url, this.component, this.componentId, 0, true, dwnUnknown);
|
||||
}
|
||||
|
||||
return promise.then((finalUrl) => {
|
||||
this.logger.debug('Using URL ' + finalUrl + ' for ' + url);
|
||||
if (tagName === 'SOURCE') {
|
||||
// The browser does not catch changes in SRC, we need to add a new source.
|
||||
// @todo: Check if changing src works in Android 4.4, maybe the problem was only in 4.1-4.3.
|
||||
this.addSource(finalUrl);
|
||||
} else {
|
||||
this.element.setAttribute(targetAttr, finalUrl);
|
||||
}
|
||||
|
||||
// Set events to download big files (not downloaded automatically).
|
||||
if (finalUrl.indexOf('http') === 0 && targetAttr != 'poster' &&
|
||||
(tagName == 'VIDEO' || tagName == 'AUDIO' || tagName == 'A' || tagName == 'SOURCE')) {
|
||||
let eventName = tagName == 'A' ? 'click' : 'play',
|
||||
clickableEl = this.element;
|
||||
|
||||
if (tagName == 'SOURCE') {
|
||||
clickableEl = <HTMLElement> this.domUtils.closest(this.element, 'video,audio');
|
||||
if (!clickableEl) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
clickableEl.addEventListener(eventName, () => {
|
||||
// User played media or opened a downloadable link.
|
||||
// Download the file if in wifi and it hasn't been downloaded already (for big files).
|
||||
if (!this.appProvider.isNetworkAccessLimited()) {
|
||||
// We aren't using the result, so it doesn't matter which of the 2 functions we call.
|
||||
this.filepoolProvider.getUrlByUrl(siteId, url, this.component, this.componentId, 0, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,444 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Directive, ElementRef, Input, OnInit, Output, EventEmitter } from '@angular/core';
|
||||
import { Platform } from 'ionic-angular';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { CoreAppProvider } from '../providers/app';
|
||||
import { CoreFilepoolProvider } from '../providers/filepool';
|
||||
import { CoreLoggerProvider } from '../providers/logger';
|
||||
import { CoreSitesProvider } from '../providers/sites';
|
||||
import { CoreDomUtilsProvider } from '../providers/utils/dom';
|
||||
import { CoreTextUtilsProvider } from '../providers/utils/text';
|
||||
import { CoreUrlUtilsProvider } from '../providers/utils/url';
|
||||
import { CoreUtilsProvider } from '../providers/utils/utils';
|
||||
import { CoreSite } from '../classes/site';
|
||||
import { CoreLinkDirective } from '../directives/link';
|
||||
import { CoreExternalContentDirective } from '../directives/external-content';
|
||||
|
||||
/**
|
||||
* Directive to format text rendered. It renders the HTML and treats all links and media, using CoreLinkDirective
|
||||
* and CoreExternalContentDirective.
|
||||
*
|
||||
* Example usage:
|
||||
* <core-format-text [text]="myText" [component]="component" [componentId]="componentId"></core-format-text>
|
||||
*
|
||||
*/
|
||||
@Directive({
|
||||
selector: 'core-format-text'
|
||||
})
|
||||
export class CoreFormatTextDirective implements OnInit {
|
||||
@Input() text: string; // The text to format.
|
||||
@Input() siteId?: string; // Site ID to use.
|
||||
@Input() component?: string; // Component for CoreExternalContentDirective.
|
||||
@Input() componentId?: string|number; // Component ID to use in conjunction with the component.
|
||||
@Input() adaptImg?: boolean = true; // Whether to adapt images to screen width.
|
||||
@Input() clean?: boolean; // Whether all the HTML tags should be removed.
|
||||
@Input() singleLine?: boolean; // Whether new lines should be removed (all text in single line). Only valid if clean=true.
|
||||
@Input() maxHeight?: number; // Max height in pixels to render the content box. It should be 50 at least to make sense.
|
||||
// Using this parameter will force display: block to calculate height better. If you want to
|
||||
// avoid this use class="inline" at the same time to use display: inline-block.
|
||||
@Input() fullOnClick?: boolean; // Whether it should open a new page with the full contents on click. Only if "max-height"
|
||||
// is set and the content has been collapsed.
|
||||
@Input() brOnFull?: boolean; // Whether new lines should be replaced by <br> on full view.
|
||||
@Input() fullTitle?: string; // Title to use in full view. Defaults to "Description".
|
||||
@Output() afterRender?: EventEmitter<any>; // Called when the data is rendered.
|
||||
|
||||
protected tagsToIgnore = ['AUDIO', 'VIDEO', 'BUTTON', 'INPUT', 'SELECT', 'TEXTAREA', 'A'];
|
||||
protected element: HTMLElement;
|
||||
|
||||
constructor(element: ElementRef, private sitesProvider: CoreSitesProvider, private domUtils: CoreDomUtilsProvider,
|
||||
private textUtils: CoreTextUtilsProvider, private translate: TranslateService, private platform: Platform,
|
||||
private utils: CoreUtilsProvider, private urlUtils: CoreUrlUtilsProvider, private loggerProvider: CoreLoggerProvider,
|
||||
private filepoolProvider: CoreFilepoolProvider, private appProvider: CoreAppProvider) {
|
||||
this.element = element.nativeElement;
|
||||
this.element.classList.add('opacity-hide'); // Hide contents until they're treated.
|
||||
this.afterRender = new EventEmitter();
|
||||
}
|
||||
|
||||
/**
|
||||
* Function executed when the directive is initialized.
|
||||
*/
|
||||
ngOnInit() : void {
|
||||
this.formatAndRenderContents();
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply CoreExternalContentDirective to a certain element.
|
||||
*
|
||||
* @param {HTMLElement} element Element to add the attributes to.
|
||||
*/
|
||||
protected addExternalContent(element: HTMLElement) : void {
|
||||
// Angular 2 doesn't let adding directives dynamically. Create the CoreExternalContentDirective manually.
|
||||
let extContent = new CoreExternalContentDirective(<any>element, this.loggerProvider, this.filepoolProvider, this.platform,
|
||||
this.sitesProvider, this.domUtils, this.urlUtils, this.appProvider);
|
||||
|
||||
extContent.component = this.component;
|
||||
extContent.componentId = this.componentId;
|
||||
extContent.siteId = this.siteId;
|
||||
|
||||
extContent.ngOnInit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add class to adapt media to a certain element.
|
||||
*
|
||||
* @param {HTMLElement} element Element to add the class to.
|
||||
*/
|
||||
protected addMediaAdaptClass(element: HTMLElement) : void {
|
||||
element.classList.add('mm-media-adapt-width');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a container for an image to adapt its width.
|
||||
*
|
||||
* @param {number} elWidth Width of the directive's element.
|
||||
* @param {HTMLElement} img Image to adapt.
|
||||
* @return {HTMLElement} Container.
|
||||
*/
|
||||
protected createMagnifyingGlassContainer(elWidth: number, img: HTMLElement) : HTMLElement {
|
||||
// Check if image width has been adapted. If so, add an icon to view the image at full size.
|
||||
let imgWidth = this.getElementWidth(img),
|
||||
// Wrap the image in a new div with position relative.
|
||||
container = document.createElement('span');
|
||||
|
||||
container.classList.add('mm-adapted-img-container');
|
||||
container.style.cssFloat = img.style.cssFloat; // Copy the float to correctly position the search icon.
|
||||
if (img.classList.contains('atto_image_button_right')) {
|
||||
container.classList.add('atto_image_button_right');
|
||||
} else if (img.classList.contains('atto_image_button_left')) {
|
||||
container.classList.add('atto_image_button_left');
|
||||
}
|
||||
container.appendChild(img);
|
||||
|
||||
if (imgWidth > elWidth) {
|
||||
let imgSrc = this.textUtils.escapeHTML(img.getAttribute('src')),
|
||||
label = this.textUtils.escapeHTML(this.translate.instant('mm.core.openfullimage'));
|
||||
|
||||
container.innerHTML += '<a href="#" class="mm-image-viewer-icon" mm-image-viewer img="' + imgSrc +
|
||||
'" aria-label="' + label + '"><ion-icon name="search"></ion-icon></a>';
|
||||
}
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finish the rendering, displaying the element again and calling afterRender.
|
||||
*/
|
||||
protected finishRender() : void {
|
||||
// Show the element again.
|
||||
this.element.classList.remove('opacity-hide');
|
||||
// Emit the afterRender output.
|
||||
this.afterRender.emit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Format contents and render.
|
||||
*/
|
||||
protected formatAndRenderContents() : void {
|
||||
if (!this.text) {
|
||||
this.finishRender();
|
||||
return;
|
||||
}
|
||||
|
||||
this.text = this.text.trim();
|
||||
|
||||
this.formatContents().then((div: HTMLElement) => {
|
||||
if (this.maxHeight && div.innerHTML != "") {
|
||||
// Move the children to the current element to be able to calculate the height.
|
||||
// @todo: Display the element?
|
||||
this.domUtils.moveChildren(div, this.element);
|
||||
|
||||
// Height cannot be calculated if the element is not shown while calculating.
|
||||
// Force shorten if it was previously shortened.
|
||||
// @todo: Work on calculate this height better.
|
||||
let height = this.element.style.maxHeight ? 0 : this.getElementHeight(this.element);
|
||||
|
||||
// If cannot calculate height, shorten always.
|
||||
if (!height || height > this.maxHeight) {
|
||||
let expandInFullview = this.utils.isTrueOrOne(this.fullOnClick) || false;
|
||||
|
||||
this.element.innerHTML += '<div class="mm-show-more">' + this.translate.instant('mm.core.showmore') + '</div>';
|
||||
|
||||
if (expandInFullview) {
|
||||
this.element.classList.add('mm-expand-in-fullview');
|
||||
}
|
||||
this.element.classList.add('mm-text-formatted mm-shortened');
|
||||
this.element.style.maxHeight = this.maxHeight + 'px';
|
||||
|
||||
this.element.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
let target = <HTMLElement> e.target;
|
||||
|
||||
if (this.tagsToIgnore.indexOf(target.tagName) === -1 || (target.tagName === 'A' &&
|
||||
!target.getAttribute('href'))) {
|
||||
if (!expandInFullview) {
|
||||
// Change class.
|
||||
this.element.classList.toggle('mm-shortened');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Open a new state with the contents.
|
||||
this.textUtils.expandText(this.fullTitle || this.translate.instant('mm.core.description'),
|
||||
this.text, this.brOnFull, this.component, this.componentId);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.domUtils.moveChildren(div, this.element);
|
||||
}
|
||||
|
||||
this.element.classList.add('mm-enabled-media-adapt');
|
||||
|
||||
this.finishRender();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply formatText and set sub-directives.
|
||||
*
|
||||
* @return {Promise<HTMLElement>} Promise resolved with a div element containing the code.
|
||||
*/
|
||||
protected formatContents() : Promise<HTMLElement> {
|
||||
|
||||
let site: CoreSite;
|
||||
|
||||
// Retrieve the site since it might be needed later.
|
||||
return this.sitesProvider.getSite(this.siteId).catch(() => {
|
||||
// Error getting the site. This probably means that there is no current site and no siteId was supplied.
|
||||
}).then((siteInstance: CoreSite) => {
|
||||
site = siteInstance;
|
||||
|
||||
// Apply format text function.
|
||||
return this.textUtils.formatText(this.text, this.clean, this.singleLine);
|
||||
}).then((formatted) => {
|
||||
|
||||
let div = document.createElement('div'),
|
||||
canTreatVimeo = site && site.isVersionGreaterEqualThan(['3.3.4', '3.4']),
|
||||
images,
|
||||
anchors,
|
||||
audios,
|
||||
videos,
|
||||
iframes,
|
||||
buttons;
|
||||
|
||||
div.innerHTML = formatted;
|
||||
images = Array.from(div.querySelectorAll('img'));
|
||||
anchors = Array.from(div.querySelectorAll('a'));
|
||||
audios = Array.from(div.querySelectorAll('audio'));
|
||||
videos = Array.from(div.querySelectorAll('video'));
|
||||
iframes = Array.from(div.querySelectorAll('iframe'));
|
||||
buttons = Array.from(div.querySelectorAll('.button'));
|
||||
|
||||
// Walk through the content to find the links and add our directive to it.
|
||||
// Important: We need to look for links first because in 'img' we add new links without mm-link.
|
||||
anchors.forEach((anchor) => {
|
||||
// Angular 2 doesn't let adding directives dynamically. Create the CoreLinkDirective manually.
|
||||
let linkDir = new CoreLinkDirective(anchor, this.domUtils, this.utils, this.sitesProvider, this.urlUtils);
|
||||
linkDir.capture = true;
|
||||
linkDir.ngOnInit();
|
||||
|
||||
this.addExternalContent(anchor);
|
||||
});
|
||||
|
||||
if (images && images.length > 0) {
|
||||
// If cannot calculate element's width, use a medium number to avoid false adapt image icons appearing.
|
||||
let elWidth = this.getElementWidth(this.element) || 100;
|
||||
|
||||
// Walk through the content to find images, and add our directive.
|
||||
images.forEach((img: HTMLElement) => {
|
||||
this.addMediaAdaptClass(img);
|
||||
this.addExternalContent(img);
|
||||
if (this.adaptImg) {
|
||||
// Create a container for the image and use it instead of the image.
|
||||
let container = this.createMagnifyingGlassContainer(elWidth, img);
|
||||
div.replaceChild(container, img);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
audios.forEach((audio) => {
|
||||
this.treatMedia(audio);
|
||||
if (this.platform.is('ios')) {
|
||||
// Set data-tap-disabled="true" to make slider work in iOS.
|
||||
audio.setAttribute('data-tap-disabled', true);
|
||||
}
|
||||
});
|
||||
|
||||
videos.forEach((video) => {
|
||||
this.treatVideoFilters(video);
|
||||
this.treatMedia(video);
|
||||
// Set data-tap-disabled="true" to make controls work in Android (see MOBILE-1452).
|
||||
video.setAttribute('data-tap-disabled', true);
|
||||
});
|
||||
|
||||
iframes.forEach((iframe) => {
|
||||
this.treatIframe(iframe, site, canTreatVimeo);
|
||||
});
|
||||
|
||||
// Handle buttons with inner links.
|
||||
buttons.forEach((button: HTMLElement) => {
|
||||
// Check if it has a link inside.
|
||||
if (button.querySelector('a')) {
|
||||
button.classList.add('mm-button-with-inner-link');
|
||||
}
|
||||
});
|
||||
|
||||
return div;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the element width in pixels.
|
||||
*
|
||||
* @param {HTMLElement} element Element to get width from.
|
||||
* @return {number} The width of the element in pixels. When 0 is returned it means the element is not visible.
|
||||
*/
|
||||
protected getElementWidth(element: HTMLElement) : number {
|
||||
let width = this.domUtils.getElementWidth(element);
|
||||
|
||||
if (!width) {
|
||||
// All elements inside are floating or inline. Change display mode to allow calculate the width.
|
||||
let parentWidth = this.domUtils.getElementWidth(element.parentNode, true, false, false, true),
|
||||
previousDisplay = getComputedStyle(element, null).display;
|
||||
|
||||
element.style.display = 'inline-block';
|
||||
|
||||
width = this.domUtils.getElementWidth(element);
|
||||
|
||||
// If width is incorrectly calculated use parent width instead.
|
||||
if (parentWidth > 0 && (!width || width > parentWidth)) {
|
||||
width = parentWidth;
|
||||
}
|
||||
|
||||
element.style.display = previousDisplay;
|
||||
}
|
||||
|
||||
return width;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the element height in pixels.
|
||||
*
|
||||
* @param {HTMLElement} elementAng Element to get height from.
|
||||
* @return {number} The height of the element in pixels. When 0 is returned it means the element is not visible.
|
||||
*/
|
||||
protected getElementHeight(element: HTMLElement) : number {
|
||||
let height;
|
||||
|
||||
// Disable media adapt to correctly calculate the height.
|
||||
element.classList.remove('mm-enabled-media-adapt');
|
||||
|
||||
height = this.domUtils.getElementHeight(element);
|
||||
|
||||
element.classList.add('mm-enabled-media-adapt');
|
||||
|
||||
return height || 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Treat video filters. Currently only treating youtube video using video JS.
|
||||
*
|
||||
* @param {HTMLElement} el Video element.
|
||||
*/
|
||||
protected treatVideoFilters(video: HTMLElement) : void {
|
||||
// Treat Video JS Youtube video links and translate them to iframes.
|
||||
if (!video.classList.contains('video-js')) {
|
||||
return;
|
||||
}
|
||||
|
||||
let data = JSON.parse(video.getAttribute('data-setup') || video.getAttribute('data-setup-lazy') || '{}'),
|
||||
youtubeId = data.techOrder && data.techOrder[0] && data.techOrder[0] == 'youtube' && data.sources && data.sources[0] &&
|
||||
data.sources[0].src && this.youtubeGetId(data.sources[0].src);
|
||||
|
||||
if (!youtubeId) {
|
||||
return;
|
||||
}
|
||||
|
||||
let iframe = document.createElement('iframe');
|
||||
iframe.id = video.id;
|
||||
iframe.src = 'https://www.youtube.com/embed/' + youtubeId;
|
||||
iframe.setAttribute('frameborder', '0');
|
||||
iframe.width = '100%';
|
||||
iframe.height = '300';
|
||||
|
||||
// Replace video tag by the iframe.
|
||||
video.parentNode.replaceChild(iframe, video);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add media adapt class and apply CoreExternalContentDirective to the media element and its sources and tracks.
|
||||
*
|
||||
* @param {HTMLElement} element Video or audio to treat.
|
||||
*/
|
||||
protected treatMedia(element: HTMLElement) : void {
|
||||
this.addMediaAdaptClass(element);
|
||||
this.addExternalContent(element);
|
||||
|
||||
let sources = Array.from(element.querySelectorAll('source')),
|
||||
tracks = Array.from(element.querySelectorAll('track'));
|
||||
|
||||
sources.forEach((source) => {
|
||||
source.setAttribute('target-src', source.getAttribute('src'));
|
||||
source.removeAttribute('src');
|
||||
this.addExternalContent(source);
|
||||
});
|
||||
|
||||
tracks.forEach((track) => {
|
||||
this.addExternalContent(track);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add media adapt class and treat the iframe source.
|
||||
*
|
||||
* @param {HTMLIFrameElement} iframe Iframe to treat.
|
||||
* @param {CoreSite} site Site instance.
|
||||
* @param {Boolean} canTreatVimeo Whether Vimeo videos can be treated in the site.
|
||||
*/
|
||||
protected treatIframe(iframe: HTMLIFrameElement, site: CoreSite, canTreatVimeo: boolean) : void {
|
||||
this.addMediaAdaptClass(iframe);
|
||||
|
||||
if (iframe.src && canTreatVimeo) {
|
||||
// Check if it's a Vimeo video. If it is, use the wsplayer script instead to make restricted videos work.
|
||||
let matches = iframe.src.match(/https?:\/\/player\.vimeo\.com\/video\/([^\/]*)/);
|
||||
if (matches && matches[1]) {
|
||||
let newUrl = this.textUtils.concatenatePaths(site.getURL(), '/media/player/vimeo/wsplayer.php?video=') +
|
||||
matches[1] + '&token=' + site.getToken();
|
||||
if (iframe.width) {
|
||||
newUrl = newUrl + '&width=' + iframe.width;
|
||||
}
|
||||
if (iframe.height) {
|
||||
newUrl = newUrl + '&height=' + iframe.height;
|
||||
}
|
||||
|
||||
iframe.src = newUrl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function to extract YouTube Id to translate to embedded video.
|
||||
* Based on http://stackoverflow.com/questions/3452546/javascript-regex-how-to-get-youtube-video-id-from-url
|
||||
*
|
||||
* @param {string} url URL of the video.
|
||||
*/
|
||||
protected youtubeGetId(url: string) : string {
|
||||
let regExp = /^.*(?:(?:youtu.be\/)|(?:v\/)|(?:\/u\/\w\/)|(?:embed\/)|(?:watch\?))\??v?=?([^#\&\?]*).*/,
|
||||
match = url.match(regExp);
|
||||
return (match && match[1].length == 11) ? match[1] : '';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,142 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Directive, Input, OnInit, ElementRef } from '@angular/core';
|
||||
import { CoreSitesProvider } from '../providers/sites';
|
||||
import { CoreDomUtilsProvider } from '../providers/utils/dom';
|
||||
import { CoreUrlUtilsProvider } from '../providers/utils/url';
|
||||
import { CoreUtilsProvider } from '../providers/utils/utils';
|
||||
import { CoreConfigConstants } from '../configconstants';
|
||||
|
||||
/**
|
||||
* Directive to open a link in external browser.
|
||||
*/
|
||||
@Directive({
|
||||
selector: '[core-link]'
|
||||
})
|
||||
export class CoreLinkDirective implements OnInit {
|
||||
@Input() capture?: boolean; // If the link needs to be captured by the app.
|
||||
@Input() inApp?: boolean; // True to open in embedded browser, false to open in system browser.
|
||||
@Input() autoLogin? = 'check'; // If the link should be open with auto-login. Accepts the following values:
|
||||
// "yes" -> Always auto-login.
|
||||
// "no" -> Never auto-login.
|
||||
// "check" -> Auto-login only if it points to the current site. Default value.
|
||||
|
||||
protected element: HTMLElement;
|
||||
|
||||
constructor(element: ElementRef, private domUtils: CoreDomUtilsProvider, private utils: CoreUtilsProvider,
|
||||
private sitesProvider: CoreSitesProvider, private urlUtils: CoreUrlUtilsProvider) {
|
||||
// This directive can be added dynamically. In that case, the first param is the anchor HTMLElement.
|
||||
this.element = element.nativeElement || element;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function executed when the component is initialized.
|
||||
*/
|
||||
ngOnInit() {
|
||||
this.element.addEventListener('click', (event) => {
|
||||
// If the event prevented default action, do nothing.
|
||||
if (!event.defaultPrevented) {
|
||||
const href = this.element.getAttribute('href');
|
||||
if (href) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
if (this.capture) {
|
||||
// @todo: Handle link using content links helper.
|
||||
// $mmContentLinksHelper.handleLink(href).then((treated) => {
|
||||
// if (!treated) {
|
||||
this.navigate(href);
|
||||
// }
|
||||
// });
|
||||
} else {
|
||||
this.navigate(href);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function to correctly navigate, open file or url in the browser.
|
||||
*
|
||||
* @param {string} href HREF to be opened.
|
||||
*/
|
||||
protected navigate(href: string) : void {
|
||||
const contentLinksScheme = CoreConfigConstants.customurlscheme + '://link=';
|
||||
|
||||
if (href.indexOf('cdvfile://') === 0 || href.indexOf('file://') === 0) {
|
||||
// We have a local file.
|
||||
this.utils.openFile(href).catch((error) => {
|
||||
this.domUtils.showErrorModal(error);
|
||||
});
|
||||
} else if (href.charAt(0) == '#') {
|
||||
href = href.substr(1);
|
||||
// In site links
|
||||
if (href.charAt(0) == '/') {
|
||||
// @todo: Investigate how to achieve this behaviour.
|
||||
// $location.url(href);
|
||||
} else {
|
||||
// Look for id or name.
|
||||
let scrollEl = <HTMLElement> this.domUtils.closest(this.element, 'scroll-content');
|
||||
this.domUtils.scrollToElement(scrollEl, document.body, "#" + href + ", [name='" + href + "']");
|
||||
}
|
||||
} else if (href.indexOf(contentLinksScheme) === 0) {
|
||||
// Link should be treated by Custom URL Scheme. Encode the right part, otherwise ':' is removed in iOS.
|
||||
href = contentLinksScheme + encodeURIComponent(href.replace(contentLinksScheme, ''));
|
||||
this.utils.openInBrowser(href);
|
||||
} else {
|
||||
|
||||
// It's an external link, we will open with browser. Check if we need to auto-login.
|
||||
if (!this.sitesProvider.isLoggedIn()) {
|
||||
// Not logged in, cannot auto-login.
|
||||
if (this.inApp) {
|
||||
this.utils.openInApp(href);
|
||||
} else {
|
||||
this.utils.openInBrowser(href);
|
||||
}
|
||||
} else {
|
||||
// Check if URL does not have any protocol, so it's a relative URL.
|
||||
if (!this.urlUtils.isAbsoluteURL(href)) {
|
||||
// Add the site URL at the begining.
|
||||
if (href.charAt(0) == '/') {
|
||||
href = this.sitesProvider.getCurrentSite().getURL() + href;
|
||||
} else {
|
||||
href = this.sitesProvider.getCurrentSite().getURL() + '/' + href;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.autoLogin == 'yes') {
|
||||
if (this.inApp) {
|
||||
this.sitesProvider.getCurrentSite().openInAppWithAutoLogin(href);
|
||||
} else {
|
||||
this.sitesProvider.getCurrentSite().openInBrowserWithAutoLogin(href);
|
||||
}
|
||||
} else if (this.autoLogin == 'no') {
|
||||
if (this.inApp) {
|
||||
this.utils.openInApp(href);
|
||||
} else {
|
||||
this.utils.openInBrowser(href);
|
||||
}
|
||||
} else {
|
||||
if (this.inApp) {
|
||||
this.sitesProvider.getCurrentSite().openInAppWithAutoLoginIfSameSite(href);
|
||||
} else {
|
||||
this.sitesProvider.getCurrentSite().openInBrowserWithAutoLoginIfSameSite(href);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -436,6 +436,18 @@ export class CoreDomUtilsProvider {
|
|||
return !this.platform.is('ios');
|
||||
}
|
||||
|
||||
/**
|
||||
* Move children from one HTMLElement to another.
|
||||
*
|
||||
* @param {HTMLElement} oldParent The old parent.
|
||||
* @param {HTMLElement} newParent The new parent.
|
||||
*/
|
||||
moveChildren(oldParent: HTMLElement, newParent: HTMLElement) : void {
|
||||
while (oldParent.childNodes.length > 0) {
|
||||
newParent.appendChild(oldParent.childNodes[0]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Search and remove a certain element from inside another element.
|
||||
*
|
||||
|
|
Loading…
Reference in New Issue