forked from EVOgeek/Vmeda.Online
		
	MOBILE-2253 core: Implement core directives for login site
This commit is contained in:
		
							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); | ||||
|     } | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										55
									
								
								src/directives/auto-focus.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								src/directives/auto-focus.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										36
									
								
								src/directives/directives.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								src/directives/directives.module.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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 {} | ||||
							
								
								
									
										218
									
								
								src/directives/external-content.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										218
									
								
								src/directives/external-content.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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); | ||||
|                         } | ||||
|                     }); | ||||
|                 } | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										444
									
								
								src/directives/format-text.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										444
									
								
								src/directives/format-text.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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] : ''; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										142
									
								
								src/directives/link.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										142
									
								
								src/directives/link.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user