mirror of
https://github.com/espocrm/espocrm.git
synced 2026-07-01 08:26:04 +00:00
775 lines
21 KiB
JavaScript
775 lines
21 KiB
JavaScript
/************************************************************************
|
||
* This file is part of EspoCRM.
|
||
*
|
||
* EspoCRM - Open Source CRM application.
|
||
* Copyright (C) 2014-2022 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||
* Website: https://www.espocrm.com
|
||
*
|
||
* EspoCRM is free software: you can redistribute it and/or modify
|
||
* it under the terms of the GNU General Public License as published by
|
||
* the Free Software Foundation, either version 3 of the License, or
|
||
* (at your option) any later version.
|
||
*
|
||
* EspoCRM is distributed in the hope that it will be useful,
|
||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
* GNU General Public License for more details.
|
||
*
|
||
* You should have received a copy of the GNU General Public License
|
||
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
||
*
|
||
* The interactive user interfaces in modified source and object code versions
|
||
* of this program must display Appropriate Legal Notices, as required under
|
||
* Section 5 of the GNU General Public License version 3.
|
||
*
|
||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||
************************************************************************/
|
||
|
||
(function (_, $) {
|
||
|
||
let root = this;
|
||
|
||
if (!root.Espo) {
|
||
root.Espo = {};
|
||
}
|
||
|
||
/**
|
||
* A callback with resolved dependencies passed as parameters.
|
||
* Should return a value to define a module.
|
||
*
|
||
* @callback Espo.Loader~requireCallback
|
||
* @param {...any} arguments Resolved dependencies.
|
||
* @returns {*}
|
||
*/
|
||
|
||
/**
|
||
* A loader. Used for loading and defining AMD modules, resource loading.
|
||
* Handles caching.
|
||
*
|
||
* @class
|
||
* @param {?module:cache.Class} [cache=null]
|
||
* @param {?int} [_cacheTimestamp=null]
|
||
*/
|
||
Espo.Loader = function (cache, _cacheTimestamp) {
|
||
this._cacheTimestamp = _cacheTimestamp || null;
|
||
this._cache = cache || null;
|
||
this._libsConfig = {};
|
||
this._loadCallbacks = {};
|
||
this._pathsBeingLoaded = {};
|
||
this._dataLoaded = {};
|
||
this._loadingSubject = null;
|
||
this._responseCache = null;
|
||
this._basePath = '';
|
||
this._internalModuleList = [];
|
||
this._internalModuleMap = {};
|
||
|
||
this.isDeveloperMode = false;
|
||
|
||
/**
|
||
* @type {string}
|
||
* @private
|
||
*/
|
||
this._baseUrl = window.location.origin + window.location.pathname;
|
||
};
|
||
|
||
_.extend(Espo.Loader.prototype, /** @lends Espo.Loader.prototype */{
|
||
|
||
_classMap: Espo,
|
||
|
||
/**
|
||
* @param {string} basePath
|
||
*/
|
||
setBasePath: function (basePath) {
|
||
this._basePath = basePath;
|
||
},
|
||
|
||
/**
|
||
* @returns {Number}
|
||
*/
|
||
getCacheTimestamp: function () {
|
||
return this._cacheTimestamp;
|
||
},
|
||
|
||
/**
|
||
* @param {Number} cacheTimestamp
|
||
*/
|
||
setCacheTimestamp: function (cacheTimestamp) {
|
||
this._cacheTimestamp = cacheTimestamp;
|
||
},
|
||
|
||
/**
|
||
* @param {module:cache.Class} cache
|
||
*/
|
||
setCache: function (cache) {
|
||
this._cache = cache;
|
||
},
|
||
|
||
/**
|
||
* @param {Cache} responseCache
|
||
*/
|
||
setResponseCache: function (responseCache) {
|
||
this._responseCache = responseCache;
|
||
},
|
||
|
||
/**
|
||
* @param {string[]} internalModuleList
|
||
*/
|
||
setInternalModuleList: function (internalModuleList) {
|
||
this._internalModuleList = internalModuleList;
|
||
this._internalModuleMap = {};
|
||
},
|
||
|
||
/**
|
||
* @private
|
||
*/
|
||
_getClass: function (name) {
|
||
if (name in this._classMap) {
|
||
return this._classMap[name];
|
||
}
|
||
|
||
return false;
|
||
},
|
||
|
||
/**
|
||
* @private
|
||
*/
|
||
_setClass: function (name, o) {
|
||
this._classMap[name] = o;
|
||
},
|
||
|
||
/**
|
||
* @private
|
||
*/
|
||
_nameToPath: function (name) {
|
||
if (name.indexOf(':') === -1) {
|
||
return 'client/src/' + name + '.js';
|
||
}
|
||
|
||
let arr = name.split(':');
|
||
let namePart = arr[1];
|
||
let modulePart = arr[0];
|
||
|
||
if (modulePart === 'custom') {
|
||
return 'client/custom/src/' + namePart + '.js' ;
|
||
}
|
||
|
||
if (this._isModuleInternal(modulePart)) {
|
||
return 'client/modules/' + modulePart + '/src/' + namePart + '.js';
|
||
}
|
||
|
||
return 'client/custom/modules/' + modulePart + '/src/' + namePart + '.js';
|
||
},
|
||
|
||
/**
|
||
* @private
|
||
* @param {string} script
|
||
* @param {string} name
|
||
* @param {string} path
|
||
*/
|
||
_execute: function (script, name, path) {
|
||
/** @var {?string} */
|
||
let module = null;
|
||
|
||
let colonIndex = name.indexOf(':');
|
||
|
||
if (colonIndex > 0) {
|
||
module = name.substring(0, colonIndex);
|
||
}
|
||
|
||
let noStrictMode = false;
|
||
|
||
if (!module && name.indexOf('lib!') === 0) {
|
||
noStrictMode = true;
|
||
|
||
if (!this.isDeveloperMode) {
|
||
let readName = name.substring(4);
|
||
|
||
let hasSourceMap = (this._libsConfig[readName] || {}).sourceMap
|
||
|
||
if (hasSourceMap) {
|
||
let realPath = path.split('?')[0];
|
||
|
||
script += `\n//# sourceMappingURL=${this._baseUrl + realPath}.map`;
|
||
}
|
||
}
|
||
}
|
||
|
||
script += `\n//# sourceURL=${this._baseUrl + path}`;
|
||
|
||
// For bc.
|
||
if (module && module !== 'crm') {
|
||
noStrictMode = true;
|
||
}
|
||
|
||
if (noStrictMode) {
|
||
(new Function(script)).call(root);
|
||
|
||
return;
|
||
}
|
||
|
||
(new Function("'use strict'; " + script))();
|
||
},
|
||
|
||
/**
|
||
* @private
|
||
*/
|
||
_executeLoadCallback: function (subject, o) {
|
||
if (subject in this._loadCallbacks) {
|
||
this._loadCallbacks[subject].forEach(callback => callback(o));
|
||
|
||
delete this._loadCallbacks[subject];
|
||
}
|
||
},
|
||
|
||
/**
|
||
* Define a module.
|
||
*
|
||
* @param {string} subject A module name to be defined.
|
||
* @param {string[]} dependency A dependency list.
|
||
* @param {Espo.Loader~requireCallback} callback A callback with resolved dependencies
|
||
* passed as parameters. Should return a value to define the module.
|
||
*/
|
||
define: function (subject, dependency, callback) {
|
||
if (subject) {
|
||
subject = this._normalizeClassName(subject);
|
||
}
|
||
|
||
if (this._loadingSubject) {
|
||
subject = subject || this._loadingSubject;
|
||
|
||
this._loadingSubject = null;
|
||
}
|
||
|
||
if (!dependency) {
|
||
this._defineProceed(callback, subject, []);
|
||
|
||
return;
|
||
}
|
||
|
||
this.require(dependency, (...arguments) => {
|
||
this._defineProceed(callback, subject, arguments);
|
||
});
|
||
},
|
||
|
||
/**
|
||
* @private
|
||
*/
|
||
_defineProceed: function (callback, subject, args) {
|
||
let o = callback.apply(root, args);
|
||
|
||
if (!o) {
|
||
if (this._cache) {
|
||
this._cache.clear('a', subject);
|
||
}
|
||
|
||
throw new Error("Could not load '" + subject + "'");
|
||
}
|
||
|
||
this._setClass(subject, o);
|
||
this._executeLoadCallback(subject, o);
|
||
},
|
||
|
||
/**
|
||
* Require a module or multiple modules.
|
||
*
|
||
* @param {string|string[]} subject A module or modules to require.
|
||
* @param {Espo.Loader~requireCallback} callback A callback with resolved dependencies.
|
||
* @param {Function|null} [errorCallback] An error callback.
|
||
*/
|
||
require: function (subject, callback, errorCallback) {
|
||
let list;
|
||
|
||
if (Object.prototype.toString.call(subject) === '[object Array]') {
|
||
list = subject;
|
||
|
||
list.forEach((item, i) => {
|
||
list[i] = this._normalizeClassName(item);
|
||
});
|
||
}
|
||
else if (subject) {
|
||
subject = this._normalizeClassName(subject);
|
||
|
||
list = [subject];
|
||
}
|
||
else {
|
||
list = [];
|
||
}
|
||
|
||
let totalCount = list.length;
|
||
|
||
if (totalCount === 1) {
|
||
this.load(list[0], callback, errorCallback);
|
||
|
||
return;
|
||
}
|
||
|
||
if (totalCount) {
|
||
let readyCount = 0;
|
||
let loaded = {};
|
||
|
||
list.forEach(name => {
|
||
this.load(name, c => {
|
||
loaded[name] = c;
|
||
|
||
readyCount++;
|
||
|
||
if (readyCount === totalCount) {
|
||
let args = [];
|
||
|
||
for (let i in list) {
|
||
args.push(loaded[list[i]]);
|
||
}
|
||
|
||
callback.apply(root, args);
|
||
}
|
||
});
|
||
});
|
||
|
||
return;
|
||
}
|
||
|
||
callback.apply(root);
|
||
},
|
||
|
||
/**
|
||
* @private
|
||
*/
|
||
_convertCamelCaseToHyphen: function (string) {
|
||
if (string === null) {
|
||
return string;
|
||
}
|
||
|
||
return string.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
|
||
},
|
||
|
||
/**
|
||
* @private
|
||
*/
|
||
_normalizeClassName: function (name) {
|
||
let normalizedName = name;
|
||
|
||
if (~name.indexOf('.') && !~name.indexOf('!')) {
|
||
console.warn(
|
||
name + ': ' +
|
||
'class name should use slashes for a directory separator and hyphen format.'
|
||
);
|
||
}
|
||
|
||
if (!!/[A-Z]/.exec(name[0])) {
|
||
if (name.indexOf(':') !== -1) {
|
||
let arr = name.split(':');
|
||
let modulePart = arr[0];
|
||
let namePart = arr[1];
|
||
|
||
return this._convertCamelCaseToHyphen(modulePart) + ':' +
|
||
this._convertCamelCaseToHyphen(namePart)
|
||
.split('.')
|
||
.join('/');
|
||
}
|
||
|
||
return normalizedName = this._convertCamelCaseToHyphen(name).split('.').join('/');
|
||
}
|
||
|
||
return normalizedName;
|
||
},
|
||
|
||
/**
|
||
* @private
|
||
*/
|
||
_addLoadCallback: function (name, callback) {
|
||
if (!(name in this._loadCallbacks)) {
|
||
this._loadCallbacks[name] = [];
|
||
}
|
||
|
||
this._loadCallbacks[name].push(callback);
|
||
},
|
||
|
||
/**
|
||
* @private
|
||
*/
|
||
load: function (name, callback, errorCallback) {
|
||
let dataType, type, path, exportsTo, exportsAs;
|
||
|
||
let realName = name;
|
||
|
||
let noAppCache = false;
|
||
|
||
if (name.indexOf('lib!') === 0) {
|
||
dataType = 'script';
|
||
type = 'lib';
|
||
|
||
realName = name.substr(4);
|
||
path = realName;
|
||
|
||
exportsTo = 'window';
|
||
exportsAs = realName;
|
||
|
||
if (realName in this._libsConfig) {
|
||
let libData = this._libsConfig[realName] || {};
|
||
|
||
path = libData.path || path;
|
||
|
||
if (this.isDeveloperMode) {
|
||
path = libData.devPath || path;
|
||
}
|
||
|
||
exportsTo = libData.exportsTo || exportsTo;
|
||
exportsAs = libData.exportsAs || exportsAs;
|
||
}
|
||
|
||
noAppCache = true;
|
||
|
||
let obj = this._fetchObject(exportsTo, exportsAs);
|
||
|
||
if (obj) {
|
||
callback(obj);
|
||
|
||
return;
|
||
}
|
||
}
|
||
else if (name.indexOf('res!') === 0) {
|
||
dataType = 'text';
|
||
type = 'res';
|
||
|
||
realName = name.substr(4);
|
||
path = realName;
|
||
}
|
||
else {
|
||
dataType = 'script';
|
||
type = 'class';
|
||
|
||
if (!name || name === '') {
|
||
throw new Error("Can not load empty class name");
|
||
}
|
||
|
||
let classObj = this._getClass(name);
|
||
|
||
if (classObj) {
|
||
callback(classObj);
|
||
|
||
return;
|
||
}
|
||
|
||
path = this._nameToPath(name);
|
||
}
|
||
|
||
if (name in this._dataLoaded) {
|
||
callback(this._dataLoaded[name]);
|
||
|
||
return;
|
||
}
|
||
|
||
let dto = {
|
||
name: name,
|
||
type: type,
|
||
dataType: dataType,
|
||
noAppCache: noAppCache,
|
||
path: path,
|
||
callback: callback,
|
||
errorCallback: errorCallback,
|
||
exportsAs: exportsAs,
|
||
exportsTo: exportsTo,
|
||
};
|
||
|
||
if (this._cache && !this._responseCache) {
|
||
let cached = this._cache.get('a', name);
|
||
|
||
if (cached) {
|
||
this._processCached(dto, cached);
|
||
|
||
return;
|
||
}
|
||
}
|
||
|
||
if (path in this._pathsBeingLoaded) {
|
||
this._addLoadCallback(name, callback);
|
||
|
||
return;
|
||
}
|
||
|
||
this._pathsBeingLoaded[path] = true;
|
||
|
||
let useCache = false;
|
||
|
||
if (this._cacheTimestamp) {
|
||
useCache = true;
|
||
|
||
let sep = (path.indexOf('?') > -1) ? '&' : '?';
|
||
|
||
path += sep + 'r=' + this._cacheTimestamp;
|
||
}
|
||
|
||
let url = this._basePath + path;
|
||
|
||
dto.path = path;
|
||
dto.url = url;
|
||
dto.useCache = useCache;
|
||
|
||
if (!this._responseCache) {
|
||
this._processRequest(dto);
|
||
|
||
return;
|
||
}
|
||
|
||
this._responseCache
|
||
.match(new Request(url))
|
||
.then(response => {
|
||
if (!response) {
|
||
this._processRequest(dto);
|
||
|
||
return;
|
||
}
|
||
|
||
response
|
||
.text()
|
||
.then(cached => {
|
||
this._handleResponse(dto, cached);
|
||
});
|
||
});
|
||
},
|
||
|
||
/**
|
||
* @private
|
||
*/
|
||
_fetchObject: function (exportsTo, exportsAs) {
|
||
let from = root;
|
||
|
||
if (exportsTo === 'window') {
|
||
from = root;
|
||
}
|
||
else {
|
||
for (let item of exportsTo.split('.')) {
|
||
from = from[item];
|
||
|
||
if (typeof from === 'undefined') {
|
||
return null;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (exportsAs in from) {
|
||
return from[exportsAs];
|
||
}
|
||
},
|
||
|
||
/**
|
||
* @private
|
||
*/
|
||
_processCached: function (dto, cached) {
|
||
let name = dto.name;
|
||
let callback = dto.callback;
|
||
let type = dto.type;
|
||
let dataType = dto.dataType;
|
||
let exportsAs = dto.exportsAs;
|
||
let exportsTo = dto.exportsTo;
|
||
|
||
if (type === 'class') {
|
||
this._loadingSubject = name;
|
||
}
|
||
|
||
if (dataType === 'script') {
|
||
this._execute(cached, name, dto.path);
|
||
}
|
||
|
||
if (type === 'class') {
|
||
let classObj = this._getClass(name);
|
||
|
||
if (classObj) {
|
||
callback(classObj);
|
||
|
||
return;
|
||
}
|
||
|
||
this._addLoadCallback(name, callback);
|
||
|
||
return;
|
||
}
|
||
|
||
let data = cached;
|
||
|
||
if (exportsTo && exportsAs) {
|
||
data = this._fetchObject(exportsTo, exportsAs);
|
||
}
|
||
|
||
this._dataLoaded[name] = data;
|
||
|
||
callback(data);
|
||
},
|
||
|
||
/**
|
||
* @private
|
||
*/
|
||
_processRequest: function (dto) {
|
||
let name = dto.name;
|
||
let url = dto.url;
|
||
let errorCallback = dto.errorCallback;
|
||
let path = dto.path;
|
||
let useCache = dto.useCache;
|
||
let noAppCache = dto.noAppCache;
|
||
|
||
$.ajax({
|
||
type: 'GET',
|
||
cache: useCache,
|
||
dataType: 'text',
|
||
mimeType: 'text/plain',
|
||
local: true,
|
||
url: url,
|
||
})
|
||
.then(response => {
|
||
if (this._cache && !noAppCache && !this._responseCache) {
|
||
this._cache.set('a', name, response);
|
||
}
|
||
|
||
if (this._responseCache) {
|
||
this._responseCache.put(url, new Response(response));
|
||
}
|
||
|
||
this._handleResponse(dto, response);
|
||
})
|
||
.catch(() => {
|
||
if (typeof errorCallback === 'function') {
|
||
errorCallback();
|
||
|
||
return;
|
||
}
|
||
|
||
throw new Error("Could not load file '" + path + "'");
|
||
});
|
||
},
|
||
|
||
/**
|
||
* @private
|
||
*/
|
||
_handleResponse: function (dto, response) {
|
||
let name = dto.name;
|
||
let callback = dto.callback;
|
||
let type = dto.type;
|
||
let dataType = dto.dataType;
|
||
let exportsAs = dto.exportsAs;
|
||
let exportsTo = dto.exportsTo;
|
||
|
||
this._addLoadCallback(name, callback);
|
||
|
||
if (type === 'class') {
|
||
this._loadingSubject = name;
|
||
}
|
||
|
||
if (dataType === 'script') {
|
||
this._execute(response, name, dto.path);
|
||
}
|
||
|
||
let data;
|
||
|
||
if (type === 'class') {
|
||
data = this._getClass(name);
|
||
|
||
if (data && typeof data === 'function') {
|
||
this._executeLoadCallback(name, data);
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
data = response;
|
||
|
||
if (exportsTo && exportsAs) {
|
||
data = this._fetchObject(exportsTo, exportsAs);
|
||
}
|
||
|
||
this._dataLoaded[name] = data;
|
||
|
||
this._executeLoadCallback(name, data);
|
||
},
|
||
|
||
/**
|
||
* @param {Object} data
|
||
* @internal
|
||
*/
|
||
addLibsConfig: function (data) {
|
||
this._libsConfig = _.extend(this._libsConfig, data);
|
||
},
|
||
|
||
/**
|
||
* @private
|
||
*/
|
||
_isModuleInternal: function (moduleName) {
|
||
if (!(moduleName in this._internalModuleMap)) {
|
||
this._internalModuleMap[moduleName] = this._internalModuleList.indexOf(moduleName) !== -1;
|
||
}
|
||
|
||
return this._internalModuleMap[moduleName];
|
||
},
|
||
|
||
/**
|
||
* Require a module or multiple modules.
|
||
*
|
||
* @param {string|string[]} subject A module or modules to require.
|
||
* @returns {Promise<unknown>}
|
||
*/
|
||
requirePromise: function (subject) {
|
||
return new Promise((resolve, reject) => {
|
||
this.require(
|
||
subject,
|
||
() => resolve(),
|
||
() => reject()
|
||
);
|
||
});
|
||
},
|
||
});
|
||
|
||
Espo.loader = new Espo.Loader();
|
||
|
||
/**
|
||
* Require a module or multiple modules.
|
||
*
|
||
* @param {string|string[]} subject A module or modules to require.
|
||
* @param {Espo.Loader~requireCallback} callback A callback with resolved dependencies.
|
||
* @param {Object} [context] A context.
|
||
* @param {Function|null} [errorCallback] An error callback.
|
||
*/
|
||
root.require = Espo.require = function (subject, callback, context, errorCallback) {
|
||
if (context) {
|
||
callback = callback.bind(context);
|
||
}
|
||
|
||
Espo.loader.require(subject, callback, errorCallback);
|
||
};
|
||
|
||
/**
|
||
* Define an [AMD](https://github.com/amdjs/amdjs-api/blob/master/AMD.md) module.
|
||
*
|
||
* 3 signatures:
|
||
* 1. `(callback)` – Unnamed, no dependencies.
|
||
* 2. `(dependencyList, callback)` – Unnamed, with dependencies.
|
||
* 3. `(moduleName, dependencyList, callback)` – Named.
|
||
*
|
||
* @param {string|string[]|Espo.Loader~requireCallback} arg1 A module name to be defined,
|
||
* a dependency list or a callback.
|
||
* @param {string[]|Espo.Loader~requireCallback} [arg2] A dependency list or a callback with resolved
|
||
* dependencies.
|
||
* @param {Espo.Loader~requireCallback} [arg3] A callback with resolved dependencies.
|
||
*/
|
||
root.define = Espo.define = function (arg1, arg2, arg3) {
|
||
let subject = null;
|
||
let dependency = null;
|
||
let callback = null;
|
||
|
||
if (typeof arg1 === 'function') {
|
||
callback = arg1;
|
||
}
|
||
else if (typeof arg1 !== 'undefined' && typeof arg2 === 'function') {
|
||
dependency = arg1;
|
||
callback = arg2;
|
||
}
|
||
else {
|
||
subject = arg1;
|
||
dependency = arg2;
|
||
callback = arg3;
|
||
}
|
||
|
||
Espo.loader.define(subject, dependency, callback);
|
||
};
|
||
|
||
}).call(this, _, $);
|