var kit; kit = window.kit = window.kit || {}; kit.service = kit.service || {}; kit.config = kit.config || {}; (function(global){ /** * @class kitConfig */ var defaultConfig = { version: '2.1', assetsPath: '', requiredDependencies: [], autoloadPluginList: [], debug: true, }; var stateList = { undefined: 0, registered: 1, loading: 3, loaded: 4, installing: 5, installed: 6, }; /** * @see https://requirejs.org/ * @see https://sazzer.github.io/blog/2015/05/12/Javascript-modules-ES5-vs-ES6/ * @see https://habr.com/ru/articles/501198/ */ var MD_TYPE = { // module definition ES: 'ES', // CMAScript2020 or ES11 UMD: 'UMD', // https://github.com/umdjs/umd // Node/CommonJS HMD: 'HMD', // HandlerMD - function(require, exports, module, global){module.exports = {}; return {};} default: 'default', }; /** * @param pluginId * @param config * @constructor */ function Plugin(pluginId, config = {}){ this.id = pluginId; this.state = stateList.undefined; this.state2 = stateList.undefined; this.module = { code: '', exports: {}, }; this.config = { autoRequire: false, loadJsAfterModule: false, isSetTimestamp: true, path: '', module: '', // file.js type: 'default', handler: undefined, onRegister: function(plugin){}, onLoad: function(plugin){}, onInstall: function(plugin){}, js: [], css: [], alias: [], deps: [], debug: false, }; this.pluginManager = undefined, this.promise = new Promise(function(resolve, reject){resolve()}); this.errors = []; this.register(config); } Plugin.prototype.setPluginManager = function (pluginManager){ this.pluginManager = pluginManager; }; function isAssetLoaded(src) { src = src.match(/^[^#?]*/)[0]; return document.querySelector('script[src^="'+src+'"]') || document.querySelector('link[href^="'+src+'"]') || document.querySelector('meta[property="kit:file-load"][content="'+src+'"]'); } function getUrlWithTimestamp(url){ var t = ((new Date()).getTime() / 1000 / 3600 >> 0) * 3600; try { var uri = new URL(url); uri.searchParams.append('__pm', String(t)); url = uri.toString(); } catch (e) { url = String(url).split('?')[0]; url = '' + url + '?__pm' + t; } return url; } Plugin.prototype.queuePush = function (callbackPromise, argArr = []){ var self = this; if(typeof callbackPromise !== 'function'){ return; } this.promise = this.promise.then(function(m){ var p = callbackPromise.apply(this, argArr); if(p){ return p; } }); }; Plugin.prototype.__queue = function (admission, callback, argArr = [], context = undefined){ var self = this; if(context === undefined) { context = self; } if(self.__checkState(admission)){ callback.apply(context, argArr); } else { self.promise = self.promise.then(function(){ return new Promise(function (resolve, reject){ callback.apply(context, argArr); resolve(); }) }.bind(self)); } }; Plugin.prototype.__exec = function (admission, callback, argArr = [], context = undefined) { var self = this; if(context === undefined) { context = self; } if(self.__checkState(admission)){ callback.apply(context, argArr); } else { self.promise = self.promise.then(function(){ callback.apply(context, argArr); }.bind(self)); } }; Plugin.prototype.__checkState = function(state, stateTo = undefined){ if(stateTo === undefined){ return this.state >= state; } return this.state >= state && this.state <= stateTo; }; Plugin.prototype.__check2State = function(state, stateTo = undefined){ if(stateTo === undefined){ return this.state2 >= state; } return this.state2 >= state && this.state2 <= stateTo; }; Plugin.prototype.__getValidConfig = function (c = {}){ var config = Object.assign({}, c); if(typeof config.onLoad !== 'function') { config.onLoad = function(plugin){}; } if(typeof config.onInstall !== 'function') { config.onInstall = function(plugin){}; } if(typeof config.onRegister !== 'function') { config.onRegister = function(plugin){}; } if(typeof config.path !== 'string') { config.path = ''; } if(typeof config.js === 'string') { config.js = [config.js]; } else if(!Array.isArray(config.js)) { config.js = []; } if(typeof config.css === 'string') { config.css = [config.css]; } else if(!Array.isArray(config.css)) { config.css = []; } if(typeof config.alias === 'string') { config.alias = [config.alias]; } else if(!Array.isArray(config.alias)) { config.alias = []; } config.autoRequire = config.hasOwnProperty('autoRequire')?!!config.autoRequire:false; config.loadJsAfterModule = config.hasOwnProperty('loadJsAfterModule')?!!config.loadJsAfterModule:false; config.isSetTimestamp = config.hasOwnProperty('isSetTimestamp')?!!config.isSetTimestamp:true; config.type = config.type || 'default'; if(typeof config.deps === 'string') { config.deps = [config.deps]; } else if(!Array.isArray(config.deps)) { config.deps = []; } return config; } Plugin.prototype.__setState = function (state, force = false) { var self = this; if(force){ self.state = state; return; } if(self.__checkState(state)){ self.state = state; } } Plugin.prototype.register = function (config = {}) { var self = this; var pluginId = self.id; //console.warn('Plugin.register: plugin ' + pluginId + ' ('+(this.plugins[pluginId]?this.plugins[pluginId].state:'0')+')'); if( this.__checkState(stateList.installed) ) { throw new Error('Plugin ' + pluginId + ' уже невозможно конфигурировать ('+self.state+'/'+self.state2+')'); } var handler; if(typeof config === 'function') { handler = config; config = this.config; config.handler = handler; config.type = MD_TYPE.HMD; } else { if( this.__checkState(stateList.loaded) || this.__check2State(stateList.loading) ) { throw new Error('Plugin ' + pluginId + ' уже невозможно конфигурировать ('+self.state+')'); } if(typeof config === 'object') { config = this.__getValidConfig(config); } else { throw new Error('Plugin.register: plugin ' + pluginId + ' is not a valid format') } Object.assign(this.config, config); } if(!this.__checkState(stateList.registered)) { self.config.debug && console.log(`Plugin ${self.id}: onRegister + event.`); this.state = stateList.registered; this.config.onRegister(this); var event = new CustomEvent("onRegister" + this.id + "Plugin", { "detail": {plugin: this} }); document.dispatchEvent(event); } if(config.autoRequire) { //this.require(); } }; Plugin.prototype.load = function (callback){ var self = this; // console.log('Plugin.load: plugin ' + self.id + ' ('+self.state+')'); if(typeof callback !== 'function') { callback = function(){}; } var pluginId = self.id; if(!self.__checkState(stateList.registered)) { console.warn(`Plugin.load(${pluginId}): is not register!.`); return self; } if(self.__checkState(stateList.loaded)) { // console.warn(`Plugin.load(${pluginId}): уже загружен!`); callback(); return self; } if(self.__check2State(stateList.loading)) { // console.log('Plugin.load: plugin ' + pluginId + ' уже грузится'); self.__exec(stateList.loaded, callback); return self; } self.state2 = stateList.loading; var config = self.config; var getModulePromiseCallback = function (){ var src = config.path + config.module; if(typeof config.module === 'string' && config.module !== '' && config.type === 'ES') { return function(){ return self.getESModulePromise(src).then(function(m) { m.__esModule = true; self.module.exports = m; }.bind(self)); } } else if(typeof config.module === 'string' && config.module !== '' && config.type === 'UMD') { if (isAssetLoaded(src)) { self.config.debug && console.warn(`Plugin ${self.id}: Скрипт ${src} уже загружен!`); return; } else { return function (){ return self.httpGet(src) .then(function (m) { self.module.code = m; }.bind(self)); }; } } else if(config.type === 'HMD' && !(config.hasOwnProperty('handler') && typeof config.handler === 'function')) { return function (){ return self.getScriptPromise(src); }; } else if(typeof config.module === 'string' && config.module !== '' && config.type === 'default') { if (isAssetLoaded(src)) { self.config.debug && console.warn(`Plugin ${self.id}: Скрипт ${src} уже загружен!`); return; } else { return function (){ return self.getScriptPromise(src); }; } } return; }; config.css.forEach(function(item, i, array){ self.getStylePromise(config.path + item); }); var f0; if(self.pluginManager){ f0 = self.pluginManager.__getDepsPromiseCallback(self.config.deps); self.queuePush(f0); } var f1 = getModulePromiseCallback(); var f2 = self.getJsAssetsPromiseCallback(config.js); if(config.loadJsAfterModule) { self.queuePush(f1); self.queuePush(f2); } else { self.queuePush(f2); self.queuePush(f1); } if(!f0 && !f1 && !f2){ self.state = stateList.loaded; } self.__queue(stateList.loaded, function(){ self.config.debug && console.log(`Plugin ${self.id}: onLoad + event.`); if(!self.isLoaded()){ self.state = stateList.loaded; } config.onLoad.call(self, self); var event = new CustomEvent('onLoad' + self.id + 'Plugin', { "detail": {plugin: self} }); document.dispatchEvent(event); }); self.__exec(stateList.loaded, function(){ callback(); }.bind(self)); return self; }; Plugin.prototype.install = function (callback){ var self = this, global = self.pluginManager.global, require = self.pluginManager.require.bind(self.pluginManager); // console.warn('Plugin.install: plugin ' + self.id + ' ('+self.state+')'); if(typeof callback !== 'function') { callback = function(){}; } var pluginId = self.id; if(!self.__checkState(stateList.registered)) { console.warn(`Plugin ${pluginId}: is not register!`); return self; } if(self.__checkState(stateList.installed)) { callback(); return self; } if(self.__check2State(stateList.installing)) { //console.warn(`Plugin.install(${pluginId}): уже устанавливается (${plugin.state})!`); self.__exec(stateList.installed, callback); return self; } if(!self.__checkState(stateList.loaded)) { this.load(); } self.state2 = stateList.installing; var setGlobals = function (global, data) { if(data === undefined) { return; } var pluginId = self.id; var aliasList = self.config.alias; if(!global.hasOwnProperty(pluginId)) { global[pluginId] = data; } if(Array.isArray(aliasList) && aliasList.length) { aliasList.forEach(function (alias) { if(!global.hasOwnProperty(alias)) { global[alias] = data; } }) } }; var module = self.module; var installer = function(){ var __exports = self.module.exports; if(self.config.type === 'ES' && typeof self.config.module === 'string' && self.config.module !== '') { __exports = (typeof self.module.exports === 'object' && self.module.exports.default !== undefined) ?self.module.exports.default :self.module.exports; } else if(self.config.type === 'UMD' && typeof self.config.module === 'string' && self.config.module !== '') { (function(require, exports, module, global){ var self = global; eval(module.code); }.bind(global)) .call(global, require, self.module.exports, module, global); __exports = (typeof self.module.exports === 'object' && self.module.exports.default !== undefined) ?self.module.exports.default :self.module.exports; } else if(self.config.type === 'HMD' && typeof self.config.handler === 'function') { __exports = self.config.handler.bind(global) .call(global, require, self.module.exports, module, global); if(__exports === undefined && self.module.exports){ __exports = self.module.exports; } } if(typeof global === 'object'){ setGlobals(global, __exports); } self.state = stateList.installed; console.log(`%cPlugin ${pluginId}: installed.`, "color: green;"); self.config.onInstall.call(self, self); var event = new CustomEvent('onInstall' + self.id + 'Plugin', { "detail": {plugin: self} }); document.dispatchEvent(event); return true; }.bind(this); this.__queue(stateList.loaded, installer); this.__exec(stateList.loaded, function(){ callback(); }); return self; }; Plugin.prototype.exec = function(callback, argArr = [], context = undefined){ var self = this; if(context === undefined) { context = self; } if(!self.__checkState(stateList.installed)){ self.load(); self.install(); } self.__exec(stateList.installed, callback, argArr, context); }; Plugin.prototype.getESModulePromise = function (src) { var self = this; return new Promise(function(resolve, reject) { import(src).then(function (m){ resolve(m); }, function (error){ self.errors.push(error); console.log(`%cPlugin ${self.id}: error import module!`, "color: red;"); reject(error); }) }); }; Plugin.prototype.httpGet = function (url) { var self = this; return new Promise(function(resolve, reject) { self.config.debug && console.log(`Plugin ${self.id}: httpGet ${url}`); var url2 = url; if(self.config.isSetTimestamp){ url2 = getUrlWithTimestamp(url); } p = fetch(url2, { method: 'GET', redirect: 'follow' }) .then(function (response) { if (response.ok) { return response.text(); } console.log(`%cPlugin.load(${self.id}): bad status ${response.status}!`, "color: red;"); throw ['bad status', response.status, response.statusText].filter().join(' '); }, function(reason){ reject('Network Error'); }) .then(function(data){ resolve(data); }, function(reason){ reject(reason); }); // var xhr = new XMLHttpRequest(); // xhr.open('GET', url2, true); // // xhr.onload = function() { // if (this.status == 200) { // resolve(this.response); // } else { // console.log(`%cPlugin.load(${self.id}): error load file!`, "color: red;"); // var error = new Error(this.statusText); // error.code = this.status; // reject(error); // } // }; // // xhr.onerror = function() { // console.log(`%cPlugin.load(${self.id}): error load file!`, "color: red;"); // reject(new Error("Network Error")); // }; // // xhr.send(); }); } Plugin.prototype.getScriptPromise = function (src) { var self = this; if (isAssetLoaded(src)) { self.config.debug && console.warn(`Plugin ${self.id}: Скрипт ${src} уже загружен!`); return undefined; } return new Promise(function(resolve, reject) { self.config.debug && console.log(`Plugin ${self.id}: load script ${src}`); var script = document.createElement('script'); script.async = true; script.onload = function() {resolve(script);}; script.onerror = function() { var error = new Error(`Ошибка загрузки скрипта ${src}`); self.errors.push(error); console.log(`%cPlugin.load(${self.id}): error load script ${src}!`, "color: red;"); reject(error); }; script.src = src; if(self.config.isSetTimestamp){ script.src = getUrlWithTimestamp(src); } document.head.append(script); }); }; Plugin.prototype.getStylePromise = function (src) { var self = this; return new Promise(function(resolve, reject) { if(isAssetLoaded(src)) { self.config.debug && console.warn(`Plugin ${self.id}: Стиль ${src} уже загружен`); resolve(); } else { var link = document.createElement('link'); link.rel = 'stylesheet'; link.type = 'text/css'; link.media = 'all'; link.href = src; if(self.config.isSetTimestamp){ link.href = getUrlWithTimestamp(src); } document.head.append(link); resolve(link); } }); } Plugin.prototype.getAsyncScriptListPromise = function (list) { var self = this; var pList = list.map(self.getScriptPromise.bind(self)).filter((p) => p !== undefined); if(pList.length){ return Promise.all( pList ); } return undefined; } Plugin.prototype.getJsAssetsPromiseCallback = function(list = []){ var self = this; if(!list.length){ return; } return function(){ var p = new Promise(function(resolve, reject){resolve();}); list.forEach(function(item, i, array){ if(Array.isArray(item)) { p = p.then(function () { return self.getAsyncScriptListPromise(item.map((src) => self.config.path + src)); }); } else { p = p.then(function () { return self.getScriptPromise(self.config.path + item); }); } }); return p; }; }; Plugin.prototype.__getRealId = function (pluginId) { var self = this; var pluginIdLowerCase = pluginId.toLowerCase(); if(self.id.toLowerCase() === pluginIdLowerCase) { return self.id; } var alias; for(alias of self.config.alias) { if(pluginIdLowerCase === alias.toLowerCase()) { return self.id; } } return undefined; }; Plugin.prototype.__is = function(pluginId){ var self = this; return self.id === self.__getRealId(pluginId); }; Plugin.prototype.isLoaded = function(){ var self = this; return self.__checkState(stateList.loaded); }; Plugin.prototype.isInstalled = function(){ var self = this; return self.__checkState(stateList.installed); }; Plugin.prototype.require = function(){ var self = this; if(!self.isInstalled()){ self.config.debug && console.warn(`Plugin ${self.id}: not instaled!`); } return self.module.exports; }; /** * @class PluginManager * @param global * @param plugins * @memberOf kit */ var PluginManager = function(global){ this.plugins = {}; this.global = global; }; var __proto = PluginManager.prototype; /** * @typedef PluginManager.registerPlugins * * @param plugins * @return {PluginManager} */ PluginManager.prototype.registerPlugins = function (plugins) { for (var pluginId in plugins) { if(plugins.hasOwnProperty(pluginId)) { this.register(pluginId, plugins[pluginId]); } } return this; }; /** * @typedef PluginManager.register * @param pluginId * @param config */ PluginManager.prototype.register = function (pluginId, config = {}) { if(typeof pluginId !== 'string' || pluginId === ''){ console.warn(`Invalid pluginId!`, pluginId); return; } var plugin = this.__getPlugin(pluginId); if(plugin) { plugin.register(config); } else { plugin = new Plugin(pluginId, config); plugin.setPluginManager(this); this.plugins[pluginId] = plugin; } }; /** * * @param pluginId * @returns {undefined|Plugin} * @private */ PluginManager.prototype.__getPlugin = function (pluginId) { var self = this; var pid, plugin; for(pid in self.plugins) { if(self.plugins.hasOwnProperty(pid)){ plugin = self.plugins[pid]; if(plugin.__is(pluginId)) { return plugin; } } } return undefined; }; /** * * @param pluginIdList * @returns {undefined|(function(): Promise[]>)} * @private */ PluginManager.prototype.__getDepsPromiseCallback = function (pluginIdList = []) { var self = this; var pList = pluginIdList.map(function(pluginId){ var plugin = self.__getPlugin(pluginId); if(plugin){ plugin.install(); if(plugin.isInstalled()){ return undefined; } return plugin.promise; } console.log(`%cPluginManager: plugin ${pluginId} not found!`, "color: red;"); return undefined; }).filter((p) => p !== undefined); if(!pList.length){ return undefined; } return function(){ return Promise.all(pList); }; }; PluginManager.prototype.__getRealIdList = function (pluginIdList = []) { var self = this; var pList = pluginIdList.map(function(pluginId){ var plugin = self.__getPlugin(pluginId); if(plugin){ return plugin.id; } return undefined; }); return pList; }; /** * @typedef PluginManager.exec * * @param deps * @param callback * @param argArr * @param context * @return {Promise} */ PluginManager.prototype.exec = function (deps, callback, argArr = [], context = undefined) { var self = this; if(typeof deps === 'function') { context = argArr; argArr = callback; callback = deps; deps = []; } if(typeof deps === 'string'){ deps = [deps]; } if(context === undefined) { context = self; } if(typeof argArr === 'undefined') { argArr = []; } if(typeof callback !== 'function') { callback = function(){}; } if(global.config && global.config.requiredDependencies) { deps = Array.from(new Set(deps.concat(global.config.requiredDependencies))); } var depsPromiseCallback = self.__getDepsPromiseCallback(deps); if(depsPromiseCallback) { return depsPromiseCallback().then(function(){ return new Promise(function(resolve, reject){ callback.apply(context, argArr) resolve(); }); }); } else { return new Promise(function(resolve, reject){ callback.apply(context, argArr) resolve(); }); } }; /** * @typedef PluginManager.define * @see https://requirejs.org/ * @param deps * @param callback * @return {Promise} */ __proto.define = function (deps, callback) { var self = this; var context = this; if(typeof deps === 'string'){ deps = [deps]; } if(typeof callback !== 'function') { callback = function (){}; } var depsPromiseCallback = self.__getDepsPromiseCallback(deps); var getArgArr = function () { var argArr = []; deps.forEach(function (pluginId) { var plugin = self.__getPlugin(pluginId); if(plugin){ argArr.push(self.require(pluginId)); return; } argArr.push(undefined); }.bind(self)); return argArr; }.bind(self); if(depsPromiseCallback) { return depsPromiseCallback().then(function(){ return new Promise(function(resolve, reject){ callback.apply(context, getArgArr()); resolve(); }); }); } else { return new Promise(function(resolve, reject){ callback.apply(context, getArgArr()) resolve(); }); } }; /** * @typedef PluginManager.ready * * @param {(string|string[])} deps * @param {function} callback * @return {Promise} */ __proto.ready = function(deps = [], callback = undefined) { var self = this; if(typeof deps === 'function') { callback = deps; deps = []; } if(typeof callback !== 'function') { callback = function (){}; } return self.__ready(self.exec, [deps, callback], self); }; __proto.__ready = function(fn, args = [], context = window) { var promise; var self = this; context = context || this; if (/complete|interactive|loaded/.test(document.readyState)) { promise = new Promise(function(resolve, reject){ let result = fn.apply(context, args); if (result && typeof result.then === 'function' && result instanceof Promise) { result.then(function(){ resolve(); }); } else { resolve(); } }); } else { promise = new Promise(function(resolve, reject){ document.addEventListener("DOMContentLoaded", function () { resolve(); }.bind(this)); }).then(function(){ return fn.apply(context, args); }); } return promise; }; /** * @typedef PluginManager.loaded * * @param {(string|string[])} deps * @param {function} callback */ __proto.loaded = function(deps = [], callback = undefined) { var self = this; if(typeof deps === 'function') { callback = deps; deps = []; } if(typeof callback !== 'function') { callback = function (){}; } return self.__loaded(self.exec, [deps, callback], self); }; __proto.__loaded = function(fn, args = [], context = window) { var promise; var self = this; context = context || this; if (/complete|interactive|loaded/.test(document.readyState)) { promise = new Promise(function(resolve, reject){ fn.apply(context, args); resolve(); }); } else { promise = new Promise(function(resolve, reject){ window.addEventListener("load", function () { resolve(); }.bind(this)); }).then(function(){ return fn.apply(context, args); }); } return promise; }; /** * @typedef PluginManager.require * * @param pluginId * @param callback * @returns {undefined|*} * */ __proto.require = function (pluginId, callback) { var plugin = this.__getPlugin(pluginId); if(!plugin) { console.warn('PluginManager.require: plugin ' + pluginId + ' not found!'); return undefined; } pluginId = plugin.id; var module = plugin.module; if(typeof callback !== 'function') { callback = function(){}; } plugin.install(callback); return module.exports; }; /** * @typedef PluginManager.install * * @param pluginId * @param callback * @returns {undefined} */ __proto.install = function (pluginId, callback) { var plugin = this.__getPlugin(pluginId); if(!plugin) { console.warn('PluginManager.install: plugin ' + pluginId + ' not found!'); return undefined; } pluginId = plugin.id; var module = plugin.module; plugin.install(callback); }; function init(global){ document.addEventListener("DOMContentLoaded", function () { console.log('%cDOMContentLoaded', 'color:#2053ff'); }); window.addEventListener("load", (event) => { console.log('%cWindowLoaded', 'color:#2053ff'); }); var manager = new PluginManager(global); /** * @type {PluginManager.ready} * @memberOf kit */ global.ready = manager.ready.bind(manager); /** * @type {PluginManager.loaded} * @memberOf kit */ global.loaded = manager.loaded.bind(manager); /** * @type {PluginManager.exec} * @memberOf kit */ global.exec = manager.exec.bind(manager); /** * @type {PluginManager.require} * @memberOf kit */ global.require = manager.require.bind(manager); /** * @type {PluginManager} * @memberOf kit */ var pm; /** * @type {PluginManager.define} * @memberOf kit */ global.define = manager.define.bind(manager); global.PluginManager = PluginManager; manager.register('pluginManager', { alias: ['pm'], type: MD_TYPE.HMD, handler: function(require, exports, module, global) { if(typeof window === 'object'){ window.__PluginManager__ = manager; /** * @type {kit} */ window.__pmGlobal__ = global; } return module.exports = manager; }, onInstall: function(plugin){ }, }); manager.install('pluginManager'); /** * Создаем плагин который выполняет функцию глобального конфига * PluginManager тоже использует его в своей работе */ manager.register('config', { type: MD_TYPE.HMD, handler: function(require, exports, module, global) { var script = document.currentScript; if(script){ try { var uri = new URL(script.src); defaultConfig.assetsPath = uri.pathname.split('/').slice(0, -2).join('/'); } catch (e) { var path = script.src.split('?')[0]; defaultConfig.assetsPath = path.split('/').slice(0, -2).join('/'); } } var config = global.config || {}; global.config = Object.assign({}, defaultConfig, config); if(typeof window === 'object'){ window.__pmConfig__ = global.config; } }, onInstall: function(plugin){ if(typeof window === 'object'){ if(window.hasOwnProperty('__PluginManagerConfigHandler__') && typeof __PluginManagerConfigHandler__ === 'function'){ manager.registerPlugins(__PluginManagerConfigHandler__()); } } }, }); manager.install('config'); } init(global); })(kit);