MOBILE-2378 remotethemes: Implement remote themes
parent
bb524a384d
commit
237bbadc7c
|
@ -16,7 +16,6 @@ import { Injectable } from '@angular/core';
|
||||||
import { CoreSitesProvider } from '@providers/sites';
|
import { CoreSitesProvider } from '@providers/sites';
|
||||||
import { CoreMimetypeUtilsProvider } from '@providers/utils/mimetype';
|
import { CoreMimetypeUtilsProvider } from '@providers/utils/mimetype';
|
||||||
import { CoreSite } from '@classes/site';
|
import { CoreSite } from '@classes/site';
|
||||||
import { Md5 } from 'ts-md5/dist/md5';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service to handle my files and site files.
|
* Service to handle my files and site files.
|
||||||
|
|
|
@ -100,7 +100,7 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
|
||||||
*/
|
*/
|
||||||
protected addMessage(message: any, keep: boolean = true): void {
|
protected addMessage(message: any, keep: boolean = true): void {
|
||||||
// Use smallmessage instead of message ID because ID changes when a message is read.
|
// Use smallmessage instead of message ID because ID changes when a message is read.
|
||||||
message.hash = Md5.hashStr(message.smallmessage) + '#' + message.timecreated + '#' + message.useridfrom;
|
message.hash = Md5.hashAsciiStr(message.smallmessage) + '#' + message.timecreated + '#' + message.useridfrom;
|
||||||
if (typeof this.keepMessageMap[message.hash] === 'undefined') {
|
if (typeof this.keepMessageMap[message.hash] === 'undefined') {
|
||||||
// Message not added to the list. Add it now.
|
// Message not added to the list. Add it now.
|
||||||
this.messages.push(message);
|
this.messages.push(message);
|
||||||
|
|
|
@ -0,0 +1,345 @@
|
||||||
|
// (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 { Injectable } from '@angular/core';
|
||||||
|
import { Http } from '@angular/http';
|
||||||
|
import { CoreFileProvider } from '@providers/file';
|
||||||
|
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 { CoreUtilsProvider } from '@providers/utils/utils';
|
||||||
|
import { CoreConstants } from '@core/constants';
|
||||||
|
import { Md5 } from 'ts-md5/dist/md5';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service to handle remote themes. A remote theme is a CSS sheet stored in the site that allows customising the Mobile app.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class AddonRemoteThemesProvider {
|
||||||
|
static COMPONENT = 'mmaRemoteStyles';
|
||||||
|
|
||||||
|
protected logger;
|
||||||
|
protected stylesEls: {[siteId: string]: {element: HTMLStyleElement, hash: string}} = {};
|
||||||
|
|
||||||
|
constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private fileProvider: CoreFileProvider,
|
||||||
|
private filepoolProvider: CoreFilepoolProvider, private http: Http, private utils: CoreUtilsProvider,
|
||||||
|
private domUtils: CoreDomUtilsProvider, private textUtils: CoreTextUtilsProvider) {
|
||||||
|
this.logger = logger.getInstance('AddonRemoteThemesProvider');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a style element for a site and load the styles for that element. The style will be disabled.
|
||||||
|
*
|
||||||
|
* @param {string} siteId Site ID.
|
||||||
|
* @return {Promise<any>} Promise resolved when added and loaded.
|
||||||
|
*/
|
||||||
|
addSite(siteId: string): Promise<any> {
|
||||||
|
if (!siteId || this.stylesEls[siteId]) {
|
||||||
|
// Invalid site ID or style already added.
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the style and add it to the header.
|
||||||
|
const styleEl = document.createElement('style');
|
||||||
|
styleEl.setAttribute('id', 'mobilecssurl-' + siteId);
|
||||||
|
this.disableElement(styleEl, true);
|
||||||
|
|
||||||
|
document.head.appendChild(styleEl);
|
||||||
|
this.stylesEls[siteId] = {
|
||||||
|
element: styleEl,
|
||||||
|
hash: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.load(siteId, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear styles added to the DOM, disabling them all.
|
||||||
|
*/
|
||||||
|
clear(): void {
|
||||||
|
// Disable all the styles.
|
||||||
|
const styles = <HTMLStyleElement[]> Array.from(document.querySelectorAll('style[id*=mobilecssurl]'));
|
||||||
|
styles.forEach((style) => {
|
||||||
|
this.disableElement(style, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enabled or disable a certain style element.
|
||||||
|
*
|
||||||
|
* @param {HTMLStyleElement} element The element to enable or disable.
|
||||||
|
* @param {boolean} disable Whether to disable or enable the element.
|
||||||
|
*/
|
||||||
|
disableElement(element: HTMLStyleElement, disable: boolean): void {
|
||||||
|
// Setting disabled should be enough, but we also set the attribute so it can be seen in the DOM which ones are disabled.
|
||||||
|
if (disable) {
|
||||||
|
element.disabled = true;
|
||||||
|
element.setAttribute('disabled', 'disabled');
|
||||||
|
} else {
|
||||||
|
element.disabled = false;
|
||||||
|
element.removeAttribute('disabled');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Downloads a CSS file and remove old files if needed.
|
||||||
|
*
|
||||||
|
* @param {string} siteId Site ID.
|
||||||
|
* @param {string} url File URL.
|
||||||
|
* @return {Promise<any>} Promise resolved when the file is downloaded.
|
||||||
|
*/
|
||||||
|
protected downloadFileAndRemoveOld(siteId: string, url: string): Promise<any> {
|
||||||
|
// Check if the file is downloaded.
|
||||||
|
return this.filepoolProvider.getFileStateByUrl(siteId, url).then((state) => {
|
||||||
|
return state !== CoreConstants.NOT_DOWNLOADED;
|
||||||
|
}).catch(() => {
|
||||||
|
return true; // An error occurred while getting state (shouldn't happen). Don't delete downloaded file.
|
||||||
|
}).then((isDownloaded) => {
|
||||||
|
if (!isDownloaded) {
|
||||||
|
// File not downloaded, URL has changed or first time. Delete downloaded CSS files.
|
||||||
|
return this.filepoolProvider.removeFilesByComponent(siteId, AddonRemoteThemesProvider.COMPONENT, 1);
|
||||||
|
}
|
||||||
|
}).then(() => {
|
||||||
|
|
||||||
|
return this.filepoolProvider.downloadUrl(siteId, url, false, AddonRemoteThemesProvider.COMPONENT, 1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable the styles of a certain site.
|
||||||
|
*
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
*/
|
||||||
|
enable(siteId?: string): void {
|
||||||
|
siteId = siteId || this.sitesProvider.getCurrentSiteId();
|
||||||
|
|
||||||
|
if (this.stylesEls[siteId]) {
|
||||||
|
this.disableElement(this.stylesEls[siteId].element, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get remote styles of a certain site.
|
||||||
|
*
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<{fileUrl: string, styles: string}>} Promise resolved with the styles and the URL of the CSS file.
|
||||||
|
*/
|
||||||
|
get(siteId?: string): Promise<{fileUrl: string, styles: string}> {
|
||||||
|
siteId = siteId || this.sitesProvider.getCurrentSiteId();
|
||||||
|
|
||||||
|
let fileUrl;
|
||||||
|
|
||||||
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||||
|
const infos = site.getInfo();
|
||||||
|
if (infos && infos.mobilecssurl) {
|
||||||
|
fileUrl = infos.mobilecssurl;
|
||||||
|
|
||||||
|
if (this.fileProvider.isAvailable()) {
|
||||||
|
// The file system is available. Download the file and remove old CSS files if needed.
|
||||||
|
return this.downloadFileAndRemoveOld(siteId, fileUrl);
|
||||||
|
} else {
|
||||||
|
// Return the online URL.
|
||||||
|
return fileUrl;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (infos.mobilecssurl === '') {
|
||||||
|
// CSS URL is empty. Delete downloaded files (if any).
|
||||||
|
this.filepoolProvider.removeFilesByComponent(siteId, AddonRemoteThemesProvider.COMPONENT, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject(null);
|
||||||
|
}
|
||||||
|
}).then((url) => {
|
||||||
|
this.logger.debug('Loading styles from: ', url);
|
||||||
|
|
||||||
|
// Get the CSS content using HTTP because we will treat the styles before saving them in the file.
|
||||||
|
return this.http.get(url).toPromise();
|
||||||
|
}).then((response): any => {
|
||||||
|
const text = response && response.text();
|
||||||
|
if (typeof text == 'string') {
|
||||||
|
return {fileUrl: fileUrl, styles: text};
|
||||||
|
} else {
|
||||||
|
return Promise.reject(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load styles for a certain site.
|
||||||
|
*
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @param {boolean} [disabled] Whether loaded styles should be disabled.
|
||||||
|
* @return {Promise<any>} Promise resolved when styles are loaded.
|
||||||
|
*/
|
||||||
|
load(siteId?: string, disabled?: boolean): Promise<any> {
|
||||||
|
siteId = siteId || this.sitesProvider.getCurrentSiteId();
|
||||||
|
disabled = !!disabled;
|
||||||
|
|
||||||
|
this.logger.debug('Load site', siteId, disabled);
|
||||||
|
|
||||||
|
if (siteId && this.stylesEls[siteId]) {
|
||||||
|
// Enable or disable the styles.
|
||||||
|
this.disableElement(this.stylesEls[siteId].element, disabled);
|
||||||
|
|
||||||
|
return this.get(siteId).then((data) => {
|
||||||
|
const hash = <string> Md5.hashAsciiStr(data.styles);
|
||||||
|
|
||||||
|
// Update the styles only if they have changed.
|
||||||
|
if (this.stylesEls[siteId].hash !== hash) {
|
||||||
|
this.stylesEls[siteId].element.innerHTML = data.styles;
|
||||||
|
this.stylesEls[siteId].hash = hash;
|
||||||
|
|
||||||
|
// Adding styles to a style element automatically enables it. Disable it again.
|
||||||
|
if (disabled) {
|
||||||
|
this.disableElement(this.stylesEls[siteId].element, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Styles have been loaded, now treat the CSS.
|
||||||
|
this.treatCSSCode(siteId, data.fileUrl, data.styles).catch(() => {
|
||||||
|
// Ignore errors.
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load styles for a temporary site. These styles aren't prefetched.
|
||||||
|
*
|
||||||
|
* @param {string} url URL to get the styles from.
|
||||||
|
* @return {Promise<any>} Promise resolved when loaded.
|
||||||
|
*/
|
||||||
|
loadTmpStyles(url: string): Promise<any> {
|
||||||
|
if (!url) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.http.get(url).toPromise().then((response) => {
|
||||||
|
const text = response && response.text();
|
||||||
|
if (typeof text == 'string') {
|
||||||
|
const styleEl = document.createElement('style');
|
||||||
|
styleEl.setAttribute('id', 'mobilecssurl-tmpsite');
|
||||||
|
styleEl.innerHTML = text;
|
||||||
|
|
||||||
|
document.head.appendChild(styleEl);
|
||||||
|
this.stylesEls.tmpsite = {
|
||||||
|
element: styleEl,
|
||||||
|
hash: ''
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return Promise.reject(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preload the styles of the current site (stored in DB).
|
||||||
|
*
|
||||||
|
* @return {Promise<any>} Promise resolved when loaded.
|
||||||
|
*/
|
||||||
|
preloadCurrentSite(): Promise<any> {
|
||||||
|
return this.sitesProvider.getStoredCurrentSiteId().then((siteId) => {
|
||||||
|
return this.addSite(siteId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preload the styles of all the stored sites.
|
||||||
|
*
|
||||||
|
* @return {Promise<any>} Promise resolved when loaded.
|
||||||
|
*/
|
||||||
|
preloadSites(): Promise<any> {
|
||||||
|
return this.sitesProvider.getSitesIds().then((ids) => {
|
||||||
|
const promises = [];
|
||||||
|
ids.forEach((siteId) => {
|
||||||
|
promises.push(this.addSite(siteId));
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.utils.allPromises(promises);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the styles of a certain site.
|
||||||
|
*
|
||||||
|
* @param {string} siteId Site ID.
|
||||||
|
*/
|
||||||
|
removeSite(siteId: string): void {
|
||||||
|
if (siteId && this.stylesEls[siteId]) {
|
||||||
|
document.head.removeChild(this.stylesEls[siteId].element);
|
||||||
|
delete this.stylesEls[siteId];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search for files in a CSS code and try to download them. Once downloaded, replace their URLs
|
||||||
|
* and store the result in the CSS file.
|
||||||
|
*
|
||||||
|
* @param {string} siteId Site ID.
|
||||||
|
* @param {string} fileUrl CSS file URL.
|
||||||
|
* @param {string} cssCode CSS code.
|
||||||
|
* @return {Promise<string>} Promise resolved with the CSS code.
|
||||||
|
*/
|
||||||
|
protected treatCSSCode(siteId: string, fileUrl: string, cssCode: string): Promise<string> {
|
||||||
|
if (!this.fileProvider.isAvailable()) {
|
||||||
|
return Promise.reject(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
const urls = this.domUtils.extractUrlsFromCSS(cssCode),
|
||||||
|
promises = [];
|
||||||
|
let filePath,
|
||||||
|
updated = false;
|
||||||
|
|
||||||
|
// Get the path of the CSS file.
|
||||||
|
promises.push(this.filepoolProvider.getFilePathByUrl(siteId, fileUrl).then((path) => {
|
||||||
|
filePath = path;
|
||||||
|
}));
|
||||||
|
|
||||||
|
urls.forEach((url) => {
|
||||||
|
// Download the file only if it's an online URL.
|
||||||
|
if (url.indexOf('http') == 0) {
|
||||||
|
promises.push(this.filepoolProvider.downloadUrl(siteId, url, false, AddonRemoteThemesProvider.COMPONENT, 2)
|
||||||
|
.then((fileUrl) => {
|
||||||
|
if (fileUrl != url) {
|
||||||
|
cssCode = cssCode.replace(new RegExp(this.textUtils.escapeForRegex(url), 'g'), fileUrl);
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
}).catch((error) => {
|
||||||
|
// It shouldn't happen. Ignore errors.
|
||||||
|
this.logger.warn('Error treating file ', url, error);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.all(promises).then(() => {
|
||||||
|
// All files downloaded. Store the result if it has changed.
|
||||||
|
if (updated) {
|
||||||
|
return this.fileProvider.writeFile(filePath, cssCode);
|
||||||
|
}
|
||||||
|
}).then(() => {
|
||||||
|
return cssCode;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unload styles for a temporary site.
|
||||||
|
*/
|
||||||
|
unloadTmpStyles(): void {
|
||||||
|
return this.removeSite('tmpsite');
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,107 @@
|
||||||
|
// (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 { AddonRemoteThemesProvider } from './providers/remotethemes';
|
||||||
|
import { CoreEventsProvider } from '@providers/events';
|
||||||
|
import { CoreInitDelegate } from '@providers/init';
|
||||||
|
import { CoreSitesProvider } from '@providers/sites';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
AddonRemoteThemesProvider
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class AddonRemoteThemesModule {
|
||||||
|
constructor(initDelegate: CoreInitDelegate, remoteThemesProvider: AddonRemoteThemesProvider, eventsProvider: CoreEventsProvider,
|
||||||
|
sitesProvider: CoreSitesProvider) {
|
||||||
|
|
||||||
|
// Preload the current site styles.
|
||||||
|
initDelegate.registerProcess({
|
||||||
|
name: 'AddonRemoteThemesPreloadCurrent',
|
||||||
|
priority: CoreInitDelegate.MAX_RECOMMENDED_PRIORITY + 250,
|
||||||
|
blocking: true,
|
||||||
|
load: remoteThemesProvider.preloadCurrentSite.bind(remoteThemesProvider)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Preload the styles of the rest of sites.
|
||||||
|
initDelegate.registerProcess({
|
||||||
|
name: 'AddonRemoteThemesPreload',
|
||||||
|
blocking: true,
|
||||||
|
load: remoteThemesProvider.preloadSites.bind(remoteThemesProvider)
|
||||||
|
});
|
||||||
|
|
||||||
|
let addingSite,
|
||||||
|
unloadTmpStyles;
|
||||||
|
|
||||||
|
// When a new site is added to the app, add its styles.
|
||||||
|
eventsProvider.on(CoreEventsProvider.SITE_ADDED, (data) => {
|
||||||
|
addingSite = data.siteId;
|
||||||
|
|
||||||
|
remoteThemesProvider.addSite(data.siteId).finally(() => {
|
||||||
|
if (addingSite == data.siteId) {
|
||||||
|
addingSite = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unloadTmpStyles == data.siteId) {
|
||||||
|
// This site had some tmp styles loaded, unload them.
|
||||||
|
remoteThemesProvider.unloadTmpStyles();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update styles when current site is updated.
|
||||||
|
eventsProvider.on(CoreEventsProvider.SITE_UPDATED, (data) => {
|
||||||
|
if (data.siteId === sitesProvider.getCurrentSiteId()) {
|
||||||
|
remoteThemesProvider.load(data.siteId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Enable styles of current site on login.
|
||||||
|
eventsProvider.on(CoreEventsProvider.LOGIN, (data) => {
|
||||||
|
remoteThemesProvider.enable(data.siteId);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Disable added styles on logout.
|
||||||
|
eventsProvider.on(CoreEventsProvider.LOGOUT, (data) => {
|
||||||
|
remoteThemesProvider.clear();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove site styles when a site is deleted.
|
||||||
|
eventsProvider.on(CoreEventsProvider.SITE_DELETED, (site) => {
|
||||||
|
remoteThemesProvider.removeSite(site.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load temporary styles when site config is checked in login.
|
||||||
|
eventsProvider.on(CoreEventsProvider.LOGIN_SITE_CHECKED, (data) => {
|
||||||
|
remoteThemesProvider.loadTmpStyles(data.config.mobilecssurl);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Unload temporary styles when site config is "unchecked" in login.
|
||||||
|
eventsProvider.on(CoreEventsProvider.LOGIN_SITE_UNCHECKED, (data) => {
|
||||||
|
if (data.siteId && data.siteid == addingSite) {
|
||||||
|
// The tmp styles are from a site that is being added permanently.
|
||||||
|
// Wait for the final site styles to be loaded before removing the tmp styles so there is no blink effect.
|
||||||
|
unloadTmpStyles = data.siteId;
|
||||||
|
} else {
|
||||||
|
// The tmp styles are from a site that wasn't added in the end. Just remove them.
|
||||||
|
remoteThemesProvider.unloadTmpStyles();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -76,6 +76,7 @@ import { AddonModBookModule } from '@addon/mod/book/book.module';
|
||||||
import { AddonModLabelModule } from '@addon/mod/label/label.module';
|
import { AddonModLabelModule } from '@addon/mod/label/label.module';
|
||||||
import { AddonMessagesModule } from '@addon/messages/messages.module';
|
import { AddonMessagesModule } from '@addon/messages/messages.module';
|
||||||
import { AddonPushNotificationsModule } from '@addon/pushnotifications/pushnotifications.module';
|
import { AddonPushNotificationsModule } from '@addon/pushnotifications/pushnotifications.module';
|
||||||
|
import { AddonRemoteThemesModule } from '@addon/remotethemes/remotethemes.module';
|
||||||
|
|
||||||
// For translate loader. AoT requires an exported function for factories.
|
// For translate loader. AoT requires an exported function for factories.
|
||||||
export function createTranslateLoader(http: HttpClient): TranslateHttpLoader {
|
export function createTranslateLoader(http: HttpClient): TranslateHttpLoader {
|
||||||
|
@ -152,7 +153,8 @@ export const CORE_PROVIDERS: any[] = [
|
||||||
AddonModBookModule,
|
AddonModBookModule,
|
||||||
AddonModLabelModule,
|
AddonModLabelModule,
|
||||||
AddonMessagesModule,
|
AddonMessagesModule,
|
||||||
AddonPushNotificationsModule
|
AddonPushNotificationsModule,
|
||||||
|
AddonRemoteThemesModule
|
||||||
],
|
],
|
||||||
bootstrap: [IonicApp],
|
bootstrap: [IonicApp],
|
||||||
entryComponents: [
|
entryComponents: [
|
||||||
|
|
|
@ -200,6 +200,10 @@ export class CoreDomUtilsProvider {
|
||||||
const urls = [],
|
const urls = [],
|
||||||
matches = code.match(/url\(\s*["']?(?!data:)([^)]+)\)/igm);
|
matches = code.match(/url\(\s*["']?(?!data:)([^)]+)\)/igm);
|
||||||
|
|
||||||
|
if (!matches) {
|
||||||
|
return urls;
|
||||||
|
}
|
||||||
|
|
||||||
// Extract the URL form each match.
|
// Extract the URL form each match.
|
||||||
matches.forEach((match) => {
|
matches.forEach((match) => {
|
||||||
const submatches = match.match(/url\(\s*['"]?([^'"]*)['"]?\s*\)/im);
|
const submatches = match.match(/url\(\s*['"]?([^'"]*)['"]?\s*\)/im);
|
||||||
|
|
Loading…
Reference in New Issue