define([ 'jquery', 'app/init', 'app/util', 'app/map/map', 'app/map/util', 'app/lib/eventHandler', 'sortable', 'module/base', 'module/system_info', 'module/system_graph', 'module/system_signature', 'module/system_route', 'module/system_intel', 'module/system_killboard', 'module/global_thera', 'module/connection_info', 'app/counter' ], ( $, Init, Util, Map, MapUtil, EventHandler, Sortable, BaseModule, SystemInfoModule, SystemGraphModule, SystemSignatureModule, SystemRouteModule, SystemIntelModule, SystemKillboardModule, TheraModule, ConnectionInfoModule ) => { 'use strict'; let config = { mapTabElementId: 'pf-map-tab-element', // id for map tab element (tabs + content) mapTabIdPrefix: 'pf-map-tab-', // id prefix for a map tab mapTabClass: 'pf-map-tab', // class for a map tab mapTabIconClass: 'pf-map-tab-icon', // class for map icon mapTabLinkTextClass: 'nav-tabs-link', // class for span elements in a tab mapTabSharedIconClass: 'pf-map-tab-shared-icon', // class for map shared icon mapTabContentWrapperClass: 'pf-map-tab-content-wrapper', // class for map tab content wrapper // module moduleClass: 'pf-module', // class for a module moduleSpacerClass: 'pf-module-spacer', // class for "spacer" module (preserves height during hide/show animation) moduleCollapsedClass: 'collapsed', // class for a collapsed module // sortable sortableHandleClass: 'pf-sortable-handle', sortableDropzoneClass: 'pf-sortable-dropzone', sortableGhostClass: 'pf-sortable-ghost', sortableChosenClass: 'pf-sortable-chosen', // editable 'settings' popover editableSettingsClass: 'pf-editable-settings', editableHeadlineClass: 'pf-editable-headline', editableToggleClass: 'pf-editable-toggle', editableToggleItemClass: 'pf-editable-toggle-item', mapTabContentLayoutOptions: ['left', 'right'], defaultMapTabContentLayout: 'right', }; let mapTabChangeBlocked = false; // flag for preventing map tab switch /** * get the current active mapElement * @returns {bool|jQuery} */ $.fn.getActiveMap = function(){ let map = $(this).find('.active.' + Util.config.mapTabContentClass + ' .' + Util.config.mapClass); if(!map.length){ map = false; } return map; }; /** * set map tab content wrapper observer. * -> Events are triggered within map.js * @param tabContentWrapperEl */ let setMapTabContentWrapperObserver = tabContentWrapperEl => { $(tabContentWrapperEl).on('pf:renderGlobalModules', `.${Util.config.mapTabContentClass}`, function(e, data){ getModules() .then(modules => filterModules(modules, 'global')) .then(modules => renderModules(modules, e.target, data)); }); $(tabContentWrapperEl).on('pf:renderSystemModules', `.${Util.config.mapTabContentClass}`, function(e, data){ getModules() .then(modules => filterModules(modules, 'system')) .then(modules => renderModules(modules, e.target, data)); }); $(tabContentWrapperEl).on('pf:removeSystemModules', `.${Util.config.mapTabContentClass}`, e => { getModules() .then(modules => filterModules(modules, 'system')) .then(modules => removeModules(modules, e.target)); }); $(tabContentWrapperEl).on('pf:renderConnectionModules', `.${Util.config.mapTabContentClass}`, (e, data) => { getModules() .then(modules => filterModules(modules, 'connection')) .then(modules => renderModules(modules, e.target, data)); }); $(tabContentWrapperEl).on('pf:removeConnectionModules', `.${Util.config.mapTabContentClass}`, e => { getModules() .then(modules => filterModules(modules, 'connection')) .then(modules => removeModules(modules, e.target)); }); $(tabContentWrapperEl).on('pf:updateGlobalModules', `.${Util.config.mapTabContentClass}`, (e, data) => { getModules() .then(modules => filterModules(modules, 'global')) .then(modules => updateModules(modules, e.target, data)); }); $(tabContentWrapperEl).on('pf:updateSystemModules', `.${Util.config.mapTabContentClass}`, (e, data) => { getModules() .then(modules => filterModules(modules, true, 'fullDataUpdate')) .then(modules => updateModules(modules, e.target, data)); }); $(tabContentWrapperEl).on('pf:updateRouteModules', `.${Util.config.mapTabContentClass}`, (e, data) => { getModules() .then(modules => filterModules(modules, 'SystemRouteModule', 'name')) .then(modules => updateModules(modules, e.target, data)); }); }; /** * get/load module classes * -> default modules + custom plugin modules * @returns {Promise} */ let getModules = () => { return new Promise(resolve => { let modules = [ SystemInfoModule, SystemGraphModule, SystemSignatureModule, SystemRouteModule, SystemIntelModule, SystemKillboardModule, TheraModule, ConnectionInfoModule ]; // try to load custom plugin modules (see: plugin.ihi) let pluginModulesConfig = Util.getObjVal(Init, 'plugin.modules'); if(pluginModulesConfig === Object(pluginModulesConfig)){ requirejs(Object.values(pluginModulesConfig), (...pluginModules) => { modules.push(...pluginModules); resolve(modules); }, err => { console.error(err.message); resolve(modules); }); }else{ // custom plugins disabled resolve(modules); } }); }; /** * filer array of module classes by property filterVal(s) * @param modules * @param filterVal * @param filterProp * @returns BaseModule[] */ let filterModules = (modules, filterVal = false, filterProp = 'scope') => modules.filter(Module => filterVal ? ( Array.isArray(filterVal) ? filterVal.includes(Module[filterProp]) : Module[filterProp] === filterVal ) : true ); /** * @param modules * @param tabContentElement * @param data * @returns {PromiseLike | Promise | *} */ let renderModules = (modules, tabContentElement, data) => { /** * @param dataStore * @returns {Promise} */ let render = dataStore => { let promiseRenderAll = []; for(let Module of modules){ let defaultGridArea = Module.sortArea || 'a'; let defaultPosition = Module.position || 0; for(let areaAlias of Util.config.mapTabContentAreaAliases){ let key = 'modules_area_' + areaAlias; if(dataStore && dataStore[key]){ let positionIndex = dataStore[key].indexOf(Module.name); if(positionIndex !== -1){ // first index (0) => is position 1 defaultPosition = positionIndex + 1; defaultGridArea = areaAlias; break; } } } // check if gridArea exists let gridArea = tabContentElement.getElementsByClassName(Util.getMapTabContentAreaClass(defaultGridArea)); if(gridArea.length){ gridArea = gridArea[0]; promiseRenderAll.push(renderModule(Module, gridArea, defaultPosition, data.mapId, data.payload)); }else{ console.warn( 'renderModules() failed for %o. GridArea class=%o not found', Module.name, Util.getMapTabContentAreaClass(defaultGridArea) ); } } return Promise.all(promiseRenderAll); }; let renderModulesAndUpdateExecutor = resolve => { // get local data for map // -> filter out disabled modules // -> get default module positions Util.getLocalStore('map').getItem(data.mapId).then(dataStore => { // filter disabled modules (layout settings) let modulesDisabled = Util.getObjVal(dataStore, 'modulesDisabled') || []; modules = modules.filter(Module => !modulesDisabled.includes(Module.name)); // check if modules require "additional" data (e.g. structures, description) // -> this is used to update some modules after initial draw let requestSystemData = false; for(let Module of modules){ if(Module.scope === 'system' && Module.fullDataUpdate){ requestSystemData = true; } } let renderPromises = []; if(requestSystemData){ renderPromises.push(Util.request('GET', 'system', data.payload.id, {mapId: data.mapId})); } renderPromises.push(render(dataStore)); Promise.all(renderPromises) .then(payload => { let promiseUpdateAll = []; let systemData; if(requestSystemData){ // get systemData from first Promise (ajax call) let responseData = payload.shift(); systemData = Util.getObjVal(responseData, 'data'); } if(systemData){ // get all rendered modules let modules = payload.shift().map(payload => payload.data.module); // get modules that require "additional" data let systemModules = modules.filter(Module => Module.scope === 'system' && Module.fullDataUpdate); promiseUpdateAll.push(updateModules(systemModules, tabContentElement, { payload: systemData })); } Promise.all(promiseUpdateAll).then(payload => resolve(payload)); }); }); }; return new Promise(renderModulesAndUpdateExecutor); }; /** * @param Module * @param gridArea * @param defaultPosition * @param mapId * @param payload * @returns {Promise} */ let renderModule = (Module, gridArea, defaultPosition, mapId, payload) => { let renderModuleExecutor = (resolve, reject) => { /** * remove "Spacer" Module * @param gridArea * @param Module */ let removeSpacerModule = (gridArea, Module) => { for(let spacerEl of gridArea.querySelectorAll('.' + Module.className + '-spacer[data-module="' + Module.name + '"]')){ spacerEl.remove(); } }; /** * render module * @param Module * @param gridArea * @param defaultPosition * @param mapId * @param payload */ let render = (Module, gridArea, defaultPosition, mapId, payload) => { let payBack = { action: 'renderModule', data: { module: Module } }; // hide "spacer" Module (in case it exist) // -> Must be done BEFORE position calculation! Spacer Modules should not be counted! removeSpacerModule(gridArea, Module); let module = new Module({ position: defaultPosition }); let moduleElement = module.handle('render', mapId, payload); if(!(moduleElement instanceof HTMLElement)){ // module should not be rendered resolve(payBack); return; } // find correct position for new moduleElement let position = getModulePosition(gridArea, '.' + Module.className, defaultPosition); // insert at correct position // -> no :nth-child or :nth-of-type here because there might be temporary "spacer" div "modules" // that should be ignored for positioning let prevModuleElement = [...gridArea.getElementsByClassName(Module.className)].find((el, i) => ++i === position); if(prevModuleElement){ prevModuleElement.insertAdjacentElement('afterend', moduleElement); }else{ gridArea.prepend(moduleElement); } // show animation ------------------------------------------------------------------------------------- $(moduleElement).velocity({ opacity: [1, 0], translateY: [0, +20], translateZ: 0 // Force HA by animating a 3D property }, { duration: Init.animationSpeed.mapModule, easing: 'easeOutSine', complete: moduleElement => { moduleElement[0].getData('module').handle('init'); resolve(payBack); } }); }; removeModule(Module, gridArea, false).then(abc => render(Module, gridArea, defaultPosition, mapId, payload)); }; return new Promise(renderModuleExecutor); }; /** * update multiple modules * @param modules * @param tabContentElement * @param data * @returns {Promise} */ let updateModules = (modules, tabContentElement, data) => { let promiseUpdateAll = []; for(let Module of modules){ promiseUpdateAll.push(updateModule(Module, tabContentElement, data.payload)); } return Promise.all(promiseUpdateAll); }; /** * update module * @param Module * @param parentElement * @param payload * @returns {Promise} */ let updateModule = (Module, parentElement, payload) => { let updateModuleExecutor = resolve => { let promiseUpdateAll = []; let moduleElements = parentElement.querySelectorAll('.' + Module.className + '[data-module="' + Module.name + '"]'); for(let moduleElement of moduleElements){ promiseUpdateAll.push(moduleElement.getData('module').handle('update', payload)); } Promise.all(promiseUpdateAll).then(payload => resolve(payload)); }; return new Promise(updateModuleExecutor); }; /** * remove multiple modules * @param modules * @param tabContentElement * @returns {Promise} */ let removeModules = (modules, tabContentElement) => { let promiseRemoveAll = []; for(let Module of modules){ promiseRemoveAll.push(removeModule(Module, tabContentElement)); } return Promise.all(promiseRemoveAll); }; /** * remove module * @param Module * @param parentElement * @param addSpacer * @returns {Promise} */ let removeModule = (Module, parentElement, addSpacer = false) => { let removeModuleElement = moduleElement => { let removeModuleElementExecutor = (resolve, reject) => { let payload = { action: 'removeModule', data: {} }; // get module instance let module = moduleElement.getData('module'); if(module instanceof BaseModule){ module.handle('beforeHide'); $(moduleElement).velocity('reverse', { complete: moduleElement => { moduleElement = moduleElement[0]; let module = moduleElement.getData('module'); module.handle('beforeDestroy'); // [optional] add a "spacer"
that fakes Module height during hide->show animation if(addSpacer){ let spacerEl = document.createElement('div'); spacerEl.classList.add(Module.className + '-spacer'); spacerEl.setAttribute('data-module', Module.name); spacerEl.style.height = moduleElement.offsetHeight + 'px'; moduleElement.insertAdjacentElement('afterend', spacerEl); } moduleElement.remove(); resolve(payload); } }); }else{ console.warn('Invalid module. Instance of %O expected for %o', BaseModule, moduleElement); resolve(payload); } }; return new Promise(removeModuleElementExecutor); }; let removeModuleExecutor = resolve => { let promiseRemoveAll = []; let moduleElements = parentElement.querySelectorAll('.' + Module.className + '[data-module="' + Module.name + '"]'); for(let moduleElement of moduleElements){ promiseRemoveAll.push(removeModuleElement(moduleElement)); } Promise.all(promiseRemoveAll).then(payload => resolve(payload)); }; return new Promise(removeModuleExecutor); }; /** * updates current visible/active mapElement in mapModule with user data * @param mapModule * @returns {Promise} */ let updateActiveMapUserData = mapModule => new Promise(resolve => { // get all active map elements for module let mapElement = $(mapModule).getActiveMap(); updateMapUserData(mapElement).then(() => resolve()); }); /** * updates mapElement with user data * update * @param mapElement * @returns {Promise} */ let updateMapUserData = mapElement => { // performance logging (time measurement) let logKeyClientUserData = Init.performanceLogging.keyClientUserData; Util.timeStart(logKeyClientUserData); let updateMapUserDataExecutor = resolve => { if(mapElement !== false){ let mapId = mapElement.data('id'); let currentMapUserData = Util.getCurrentMapUserData(mapId); if(currentMapUserData){ // trigger "update local" for this map => async mapElement.trigger('pf:updateLocal', currentMapUserData); // update map with current user data Map.updateUserData(mapElement, currentMapUserData); } } resolve(); }; return new Promise(updateMapUserDataExecutor).then(payload => { // log client map update time let duration = Util.timeStop(logKeyClientUserData); Util.log(logKeyClientUserData, {duration: duration, type: 'client', description: 'update users'}); }); }; /** * update active system modules (below map) * @param mapModule * @param systemData */ let updateSystemModulesData = (mapModule, systemData) => { if(systemData){ // check if current open system is still the requested info system let currentSystemData = Util.getCurrentSystemData(systemData.mapId); if( currentSystemData && systemData.id === currentSystemData.id ){ // trigger system update events let tabContentEl = document.getElementById(config.mapTabIdPrefix + systemData.mapId); $(tabContentEl).trigger('pf:updateSystemModules', { payload: systemData }); } } }; /** * set observer for tab content (areas where modules will be shown) * @param tabContent * @param mapId */ let setTabContentObserver = (tabContent, mapId) => { let defaultSortableOptions = { invertSwap: true, animation: Init.animationSpeed.mapModule, handle: '.' + config.sortableHandleClass, draggable: '.' + config.moduleClass, ghostClass: config.sortableGhostClass, chosenClass: config.sortableChosenClass, scroll: true, scrollSensitivity: 50, scrollSpeed: 20, dataIdAttr: 'data-module', sort: true, store: { get: function(sortable){ return []; }, set: function(sortable){ // function is called to frequently for different "groups" // if an element moved between groups -> async local store can not handle this in time // -> queue up store calls let key = 'modules_' + sortable.options.group.name; Util.getLocalStore('map').setItem(`${mapId}.${key}`, sortable.toArray()); } }, onStart: function(e){ // Element dragging started // -> save initial sort state -> see store.set() this.save(); // highlight valid grid areas where module could be dropped let module = e.item.getData('module'); let sortTargetAreas = module.config.sortTargetAreas || []; tabContent.querySelectorAll('.' + Util.getMapTabContentAreaClass()).forEach(gridArea => { if(sortTargetAreas.includes(gridArea.getAttribute('data-area'))){ gridArea.classList.add(config.sortableDropzoneClass); }else{ gridArea.classList.remove(config.sortableDropzoneClass); } }); }, onEnd: function(e){ // remove highlight grid areas tabContent.querySelectorAll('.' + Util.getMapTabContentAreaClass()).forEach(gridArea => { gridArea.classList.remove(config.sortableDropzoneClass); }); } }; [ 'onChoose', 'onStart', 'onEnd', 'onAdd', 'onUpdate', 'onSort', 'onRemove', 'onChange', 'onUnchoose', //'onMove' ].forEach(name => { defaultSortableOptions[name] = function(e){ // onMove is the only event where e.item does not exist // -> e.related is the element that is moved by the dragged one let target = e.item || e.related; let module = target.getData('module'); switch(name){ case 'onStart': // Element dragging started // -> save initial sort state -> see store.set() this.save(); // highlight valid grid areas where module could be dropped let sortTargetAreas = module.config.sortTargetAreas || []; tabContent.querySelectorAll('.' + Util.getMapTabContentAreaClass()).forEach(gridArea => { if(sortTargetAreas.includes(gridArea.getAttribute('data-area'))){ gridArea.classList.add(config.sortableDropzoneClass); }else{ gridArea.classList.remove(config.sortableDropzoneClass); } }); break; case 'onEnd': // remove highlight grid areas tabContent.querySelectorAll('.' + Util.getMapTabContentAreaClass()).forEach(gridArea => { gridArea.classList.remove(config.sortableDropzoneClass); }); break; } // pipe events to module module.handle('onSortableEvent', name, e); }; }); /** * sortable map modules */ tabContent.querySelectorAll('.' + Util.getMapTabContentAreaClass()).forEach(gridArea => { let sortable = Sortable.create(gridArea, Object.assign({}, defaultSortableOptions, { group: { name: 'area_' + gridArea.getAttribute('data-area'), pull: (to, from, dragEl, e) => { // set allowed droppable target areas for module let module = dragEl.getData('module'); return (module.config.sortTargetAreas || []).map(area => 'area_' + area); }, put: (to, from, dragEl, e) => { return true; } } })); }); /** * toggle module height * @param e */ let toggleModuleHeight = e => { if( e.target.classList.contains(config.moduleClass) && e.layerX <= 9 && e.layerY <= 9 && e.layerX >= 0 && e.layerY >= 0 ){ e.stopPropagation(); let moduleElement = e.target; // remember height if(!moduleElement.dataset.origHeight){ moduleElement.dataset.origHeight = moduleElement.offsetHeight; } if(moduleElement.classList.contains(config.moduleCollapsedClass)){ $(moduleElement).velocity('finish').velocity({ height: [moduleElement.dataset.origHeight + 'px', [400, 15]] },{ duration: 400, easing: 'easeOutSine', complete: moduleElement => { moduleElement[0].classList.remove(config.moduleCollapsedClass); delete moduleElement[0].dataset.origHeight; moduleElement[0].style.height = null; } }); }else{ $(moduleElement).velocity('finish').velocity({ height: ['38px', [400, 15]] },{ duration: 400, easing: 'easeOutSine', complete: moduleElement => { moduleElement[0].classList.add(config.moduleCollapsedClass); } }); } } }; EventHandler.addEventListener(tabContent, 'click.toggleModuleHeight', toggleModuleHeight, {passive: false}); }; /** * get grid item (area) elements for map tab content * @returns {[]} */ let getTabContentAreaElements = () => { let gridAreas = []; for(let areaAlias of Util.config.mapTabContentAreaAliases){ let gridArea = document.createElement('div'); gridArea.classList.add(Util.getMapTabContentAreaClass(), Util.getMapTabContentAreaClass(areaAlias)); gridArea.setAttribute('data-area', areaAlias); gridAreas.push(gridArea); } return gridAreas; }; /** * new tabs element * @returns {HTMLDivElement} */ let newMapTabsElement = () => { let tabEl = Object.assign(document.createElement('div'), { id: config.mapTabElementId }); /** * new tabBar element * @param options * @returns {HTMLUListElement} */ let newTabBar = options => { let tabBarEl = document.createElement('ul'); tabBarEl.id = Util.config.mapTabBarIdPrefix + options.area; tabBarEl.dataset.area = options.area; tabBarEl.classList.add('nav', 'nav-tabs', Util.config.mapTabBarClass); tabBarEl.setAttribute('role', 'tablist'); return tabBarEl; }; /** * new tabContent wrapper element * @param options * @returns {HTMLDivElement} */ let newTabContentWrapper = options => { let tabContentWrapperEl = document.createElement('div'); tabContentWrapperEl.dataset.target = Util.config.mapTabBarIdPrefix + options.area; tabContentWrapperEl.classList.add('tab-content', config.mapTabContentWrapperClass); return tabContentWrapperEl; }; let tabBarEls = [ newTabBar({area: 'left'}), //newTabBar({area: 'right'}) ]; let tabContentWrapperEls = [ newTabContentWrapper({area: 'left'}), //newTabContentWrapper({area: 'right'}) ]; tabEl.append( ...tabBarEls, ...tabContentWrapperEls ); tabBarEls.forEach(tabBarEl => setMapTabBarObserver(tabBarEl)); tabContentWrapperEls.forEach(tabContentWrapperEl => setMapTabContentWrapperObserver(tabContentWrapperEl)); return tabEl; }; /** * get module position * @param parentElement * @param childSelector * @param defaultPosition * @returns {number} */ let getModulePosition = (parentElement, childSelector, defaultPosition) => { let position = 0; if(defaultPosition > 0){ $(parentElement).children(childSelector).each((i, moduleElement) => { position = i + 1; let tempPosition = parseInt(moduleElement.getAttribute('data-position')) || 0; if(tempPosition >= defaultPosition){ position--; return false; } }); } return position; }; /** * set map tab bar observer * @param tabBarEl */ let setMapTabBarObserver = tabBarEl => { // set tab sortable ------------------------------------------------------------------------------------------- let sortable = Sortable.create(tabBarEl, { group: { name: 'tabs_' + tabBarEl.dataset.area, pull: (to, from, dragEl, e) => { // set allowed droppable target areas for module return ['left', 'right'].map(area => 'tabs_' + area); }, put: (to, from, dragEl, e) => { return true; } }, animation: Init.animationSpeed.mapModule, handle: '.' + config.sortableHandleClass, draggable: '.' + config.mapTabClass + ':not(.noSort)', ghostClass: config.sortableGhostClass, scroll: false, dataIdAttr: 'data-sort-id', sort: true, direction: 'horizontal', store: { get: function(sortable){ return []; }, set: function(sortable){ let key = `map_${sortable.options.group.name}`; Util.getLocalStore('character').setItem(`${Util.getCurrentCharacterId()}.${key}`, sortable.toArray()); } }, onStart: function(e){ // Element dragging started // -> save initial sort state -> see store.set() this.save(); // highlight dropable tabBarEls areas [...document.getElementsByClassName(Util.config.mapTabBarClass)].forEach(tabBarEl => { tabBarEl.classList.add(config.sortableDropzoneClass); }); }, onEnd: function(e){ // remove highlight dropable tabBarEls areas [...document.getElementsByClassName(Util.config.mapTabBarClass)].forEach(tabBarEl => { tabBarEl.classList.remove(config.sortableDropzoneClass); }); } }); // set tab click ---------------------------------------------------------------------------------------------- $(tabBarEl).on('click', 'a', e => { e.preventDefault(); // callback function after tab switch let switchTabCallback = (mapElement, linkEl) => { $(linkEl).tab('show'); // unfreeze map mapElement.data('frozen', false); return false; }; let linkEl = e.currentTarget; let tabType = linkEl.dataset.tabType; let mapId = parseInt(linkEl.dataset.mapId) || 0; // ignore "add"/"settings" tab. no need for map change if(tabType === 'map' && mapId > 0){ if(mapTabChangeBlocked === false){ let mapElement = $(document.getElementById(config.mapTabElementId)).getActiveMap(); if(mapId !== mapElement.data('id')){ // block tabs until switch is done mapTabChangeBlocked = true; // freeze active map -> no user data update while map switch mapElement.data('frozen', true); // hide current map with animation MapUtil.visualizeMap(mapElement, 'hide').then(payload => { // un-block map tabs mapTabChangeBlocked = switchTabCallback(mapElement, linkEl); }); } } }else{ e.stopPropagation(); if(tabType === 'add'){ // "add" tab clicked Util.triggerMenuAction(document, 'ShowMapSettings', {tab: 'new'}); }else if(tabType === 'settings'){ // "settings" tab clicked $(linkEl).editable('show'); }else{ console.error('Invalid tabType = %o for %O', tabType, linkEl); } } }); // tab switch ------------------------------------------------------------------------------------------------- $(tabBarEl).on('show.bs.tab', 'a', e => { let linkEl = e.currentTarget; let tabType = linkEl.dataset.tabType; let mapId = parseInt(linkEl.dataset.mapId) || 0; if(tabType === 'map' && mapId > 0){ mapTabOnShow(tabBarEl, mapId); } }); $(tabBarEl).on('shown.bs.tab', 'a', function(e){ // load new map right after tab-change let linkEl = e.currentTarget; let mapId = parseInt(linkEl.dataset.mapId) || 0; let defaultSystemId = parseInt(linkEl.dataset.defaultSystemId) || 0; let tabMapData = Util.getCurrentMapData(mapId); let tabContentEl = document.getElementById(config.mapTabIdPrefix + mapId); // tabContentEl does not exist in case of error where all map elements got removed if(tabMapData !== false && tabContentEl){ // load map let areaMap = tabContentEl.querySelector(`.${Util.getMapTabContentAreaClass('map')}`); Map.loadMap(areaMap, tabMapData, {showAnimation: true}).then(payload => { // "wake up" scrollbar for map and get previous state back let mapConfig = payload.data.mapConfig; let mapElement = mapConfig.map.getContainer(); let areaMap = mapElement.closest('.mCustomScrollbar'); $(areaMap).mCustomScrollbar('update'); // show "global" map panels of map was initial loaded if(payload.isFirstLoad){ MapUtil.showMapInfo(mapConfig.map); } // if there is an already an "active" system -> setCurrentSystemData for that again let activeSystemEl = mapElement.querySelector(`.${MapUtil.config.systemActiveClass}`); if(activeSystemEl){ MapUtil.setSystemActive(mapConfig.map, $(activeSystemEl)); }else if(defaultSystemId){ // currently no system "active" check if there is a default system set for this mapTab // -> e.g. from URL link let systemId = MapUtil.getSystemId(mapConfig.config.id, defaultSystemId); let systemEl = mapElement.querySelector(`#${systemId}`); if(systemEl){ // system exists on map -> make active and show panels MapUtil.showSystemInfo(mapConfig.map, $(systemEl)); } } // change url to unique map URL if(history.pushState){ history.pushState({}, '', MapUtil.getMapDeeplinkUrl(mapConfig.config.id)); } // update map user data (do not wait until next update is triggered) updateMapUserData($(mapElement)); }); } }); $(tabBarEl).on('hide.bs.tab', 'a', e => { let oldLinkEl = e.currentTarget; let newLinkEl = e.relatedTarget; let oldMapId = parseInt(oldLinkEl.dataset.mapId) || 0; let newMapId = parseInt(newLinkEl.dataset.mapId) || 0; // skip "add button" if(newMapId > 0){ let currentTabContentEl = document.getElementById(config.mapTabIdPrefix + oldMapId); // disable scrollbar for map that will be hidden. "freeze" current state let areaMap = currentTabContentEl.querySelector(`.${Util.getMapTabContentAreaClass('map')}`); $(areaMap).mCustomScrollbar('disable', false); } }); }; /** * set data for a map tab, or update an existing map tab with new data return promise * @param tabLinkEl * @param options * @returns {Promise} */ let updateTabData = (tabLinkEl, options) => new Promise(resolve => { // set "main" data tabLinkEl.dataset.mapId = options.id; // add updated timestamp (not available for "add" tab if(Util.getObjVal(options, 'updated.updated')){ tabLinkEl.dataset.updated = options.updated.updated; } // change "tab" link tabLinkEl.setAttribute('href', `#${config.mapTabIdPrefix}${options.id}`); // change "map" icon let mapIconEl = tabLinkEl.querySelector(`.${config.mapTabIconClass}`); mapIconEl.classList.remove(...mapIconEl.classList); mapIconEl.classList.add(config.mapTabIconClass, 'fas', 'fa-fw', options.icon); // change "shared" icon let mapSharedIconEl = tabLinkEl.querySelector(`.${config.mapTabSharedIconClass}`); mapSharedIconEl.style.display = 'none'; // check if the map is a "shared" map if(options.access){ if( options.access.character.length > 1 || options.access.corporation.length > 1 || options.access.alliance.length > 1 ){ mapSharedIconEl.style.display = 'initial'; } } // change map name label let textEl = tabLinkEl.querySelector(`.${config.mapTabLinkTextClass}`); textEl.textContent = options.name; // change tabClass let listEl = tabLinkEl.parentNode; // new tab classes let tabClasses = [config.mapTabClass, options.type.classTab]; if(options.draggable === false){ tabClasses.push('noSort'); } // check if tab was "active" before if(listEl.classList.contains('active')){ tabClasses.push('active'); } listEl.classList.remove(...listEl.classList); listEl.classList.add(...tabClasses); // set title for tooltip if(options.type.name !== undefined){ textEl.setAttribute('title', `${options.type.name} map`); } let mapTooltipOptions = { placement: 'bottom', container: 'body', trigger: 'hover', delay: 150 }; $(listEl.querySelector('[title]')).tooltip(mapTooltipOptions).tooltip('fixTitle'); resolve({ action: 'update', data: { mapId: options.id, mapName: options.name } }); }); /** * add a new tab to tab-map-module end return promise * @param tabEl * @param options * @returns {Promise} */ let addTab = (tabEl, options) => { /** * get new
  • element used as map tab * @param mapId * @param tabType * @param tabSortId * @returns {HTMLLIElement} */ let newTabListElement = (mapId, tabType, tabSortId) => { let listEl = document.createElement('li'); listEl.dataset.sortId = tabSortId; listEl.setAttribute('role', 'presentation'); // link element let linkEl = document.createElement('a'); linkEl.dataset.tabType = tabType; linkEl.setAttribute('role', 'tab'); // tab drag handler element if(mapId > 0){ linkEl.append(Object.assign(document.createElement('i'), { className: config.sortableHandleClass })); } // map icon element linkEl.append(Object.assign(document.createElement('i'), { className: config.mapTabIconClass })); // text element linkEl.append(Object.assign(document.createElement('span'), { className: config.mapTabLinkTextClass })); // map shared icon element linkEl.append(Object.assign(document.createElement('i'), { className: [config.mapTabSharedIconClass, 'fas', 'fa-fw', 'fa-share-alt'].join(' '), title: 'shared map' })); listEl.append(linkEl); return listEl; }; /** * get tab content element * @param mapId * @returns {HTMLDivElement} */ let newTabContentElement = mapId => { let contentEl = document.createElement('div'); contentEl.id = config.mapTabIdPrefix + parseInt(mapId); contentEl.classList.add(Util.config.mapTabContentClass, 'tab-pane'); contentEl.dataset.mapId = mapId; return contentEl; }; /** * add tab promise * @param resolve */ let addTabExecutor = resolve => { Util.getLocalStore('character').getItem(Util.getCurrentCharacterId()).then(localDataCharacter => { let mapId = options.id || 0; let defaultTabArea = options.area || 'left'; // whether tab should be added to left or right list let defaultPosition = options.position || 0; let tabType = options.tabType || 'map'; let tabSortId = [tabType, mapId].join('_'); // check for stored map tab order in indexDB (client) ------------------------------------------------- if(localDataCharacter){ for(let tabArea of ['left', 'right']){ let positionIndex = (Util.getObjVal(localDataCharacter, `map_tabs_${tabArea}`) || []).indexOf(tabSortId); if(positionIndex !== -1){ // first index (0) => is position 1 defaultPosition = positionIndex + 1; defaultTabArea = tabArea; break; } } } let tabBarId = Util.config.mapTabBarIdPrefix + defaultTabArea; let tabBar = tabEl.querySelector('#' + tabBarId); let tabContentWrapperEl = tabEl.querySelector(`.${config.mapTabContentWrapperClass}[data-target="${tabBarId}"]`); let listEl = newTabListElement(mapId, tabType, tabSortId); let tabContentEl = newTabContentElement(mapId); listEl.dataset.position = String(defaultPosition); // find correct position for new tabs ----------------------------------------------------------------- let position = getModulePosition(tabBar, `.${config.mapTabClass}`, defaultPosition); // insert at correct position ------------------------------------------------------------------------- let prevListEl = tabBar.querySelector(`li:nth-child(${position})`); if(prevListEl){ prevListEl.insertAdjacentElement('afterend', listEl); }else{ tabBar.insertAdjacentElement('afterbegin', listEl); } // update Tab element -> set data updateTabData(listEl.querySelector('a'), options); // add grid area elements for the new tab if(mapId){ tabContentEl.append(...getTabContentAreaElements()); setTabContentObserver(tabContentEl, mapId); } tabContentWrapperEl.insertAdjacentElement('beforeend', tabContentEl); resolve({ action: 'add', data: { mapId: mapId, mapName: options.name } }); }); }; return new Promise(addTabExecutor); }; /** * deletes tab from tab-map-module end return promise * @param tabEl * @param mapId * @returns {Promise} */ let deleteTab = (tabEl, mapId) => { /** * delete tab promise * @param resolve */ let deleteTabExecutor = resolve => { let linkEl = tabEl.querySelector(`a[href="#${config.mapTabIdPrefix + mapId}"]`); let deletedTabName = ''; if(linkEl){ deletedTabName = linkEl.querySelector(`.${config.mapTabLinkTextClass}`).textContent; let listEl = linkEl.parentNode; let tabContentEl = tabEl.querySelector(`#${config.mapTabIdPrefix + mapId}`); $(listEl).remove(); $(tabContentEl).remove(); // remove map instance from local cache MapUtil.clearMapInstance(mapId); } resolve({ action: 'delete', data: { mapId: mapId, mapName: deletedTabName } }); }; return new Promise(deleteTabExecutor); }; /** * clear all active maps * @param mapModule * @returns {Promise} */ let clearMapModule = mapModule => { let promiseDeleteTab = []; let tabEl = document.getElementById(config.mapTabElementId); if(tabEl){ let tabLinkEls = Util.getMapTabLinkElements(mapModule); for(let i = 0; i < tabLinkEls.length; i++){ let tabLinkEl = tabLinkEls[i]; let mapId = parseInt(tabLinkEl.dataset.mapId) || 0; if(mapId > 0){ promiseDeleteTab.push(deleteTab(tabEl, mapId)); } } } return Promise.all(promiseDeleteTab); }; /** * get last URL segment e.g. https://pathfinder/map/test -> test * @returns {string | undefined} */ let getLastUrlSegment = () => { let parts = window.location.pathname.split('/'); return parts.pop() || parts.pop(); }; /** * extract data from map url * @returns {Array} */ let getMapDataFromUrl = () => { let data = []; let lastURLSegment = getLastUrlSegment(); if(lastURLSegment.length){ try{ data = lastURLSegment.split('_').map(part => parseInt(atob(decodeURIComponent(part))) || 0); }catch(e){ // data could not be extracted from URL -> ignore } } return data; }; /** * set "default" map tab * -> default mapId might be available in local storage * @param tabEl * @returns {Promise} */ let showDefaultTab = tabEl => { let getActiveTabLinkElement = mapId => { return tabEl.querySelector(`.${config.mapTabClass} > a[data-map-id="${mapId}"]`); }; /** * show default tab promise * @param resolve */ let showDefaultTabExecutor = resolve => { Util.getLocalStore('character').getItem(Util.getCurrentCharacterId()).then(data => { let linkEl = null; // check for existing mapId URL identifier ------------------------------------------------------------ let urlData = getMapDataFromUrl(); let defaultMapId = urlData[0] || 0; let defaultSystemId = urlData[1] || 0; if(defaultMapId){ linkEl = getActiveTabLinkElement(defaultMapId); if(defaultSystemId && linkEl){ linkEl.dataset.defaultSystemId = defaultSystemId; } } // ... else check for existing cached default mapId --------------------------------------------------- if(!linkEl && data && data.defaultMapId){ // make specific map tab active linkEl = getActiveTabLinkElement(data.defaultMapId); } // ... else make first map tab active (default) ------------------------------------------------------- if(!linkEl){ linkEl = tabEl.querySelector(`.${config.mapTabClass} > a`); } if(linkEl){ $(linkEl).tab('show'); } resolve(); }); }; return new Promise(showDefaultTabExecutor); }; /** * load/update map module into element (all maps) * @param mapModule * @returns {Promise} */ let updateMapModule = mapModule => { // performance logging (time measurement) let logKeyClientMapData = Init.performanceLogging.keyClientMapData; Util.timeStart(logKeyClientMapData); let updateMapModuleExecutor = resolve => { // check if tabs module is already loaded let tabEl = document.getElementById(config.mapTabElementId); // store current map data global (cache) // temp store current map data to prevent data-change while function execution! let tempMapData = Util.getCurrentMapData(); if(tempMapData.length === 0){ // clear all existing maps ============================================================================ clearMapModule(mapModule) .then(payload => { // no map data available -> show "new map" dialog Util.triggerMenuAction(document, 'ShowMapSettings', {tab: 'new'}); }) .then(payload => resolve()); }else{ if(tabEl){ // tab element exists -> update =================================================================== let promisesAddTab = []; let promiseDeleteTab = []; let promiseUpdateTab = []; let tabDeletedCallback = payload => { Util.showNotify({title: 'Map removed', text: payload.data.mapName + ' deleted', type: 'warning'}); }; let tabAddCallback = payload => { Util.showNotify({title: 'Map added', text: payload.data.mapName + ' added', type: 'success'}); }; // tab element already exists let tabLinkEls = Util.getMapTabLinkElements(mapModule); // mapIds that are currently active let activeMapIds = []; // check whether a tab/map is still active for(let i = 0; i < tabLinkEls.length; i++){ let tabLinkEl = tabLinkEls[i]; let mapId = parseInt(tabLinkEl.dataset.mapId) || 0; if(mapId > 0){ let tabMapData = Util.getCurrentMapData(mapId); if(tabMapData !== false){ // map data available -> activeMapIds.push(mapId); // check for map data change and update tab if(tabMapData.config.updated.updated > (parseInt(tabLinkEl.dataset.updated) || 0)){ promiseUpdateTab.push(updateTabData(tabLinkEl, tabMapData.config)); } }else{ // map data not available -> remove tab promiseDeleteTab.push(deleteTab(tabEl, mapId).then(tabDeletedCallback)); } } } // add new tabs for new maps for(let data of tempMapData){ if(activeMapIds.indexOf(data.config.id) === -1){ // add new map tab promisesAddTab.push(addTab(tabEl, data.config).then(tabAddCallback)); } } // wait until ALL "add", "delete", "update" promises are fulfilled let promisesAll = promisesAddTab.concat(promiseDeleteTab, promiseUpdateTab); Promise.all(promisesAll).then(payload => { // if there is an active map ... let activeMap = Util.getMapModule().getActiveMap(); if(activeMap){ let activeMapId = activeMap.data('id'); let activeMapData = Util.getCurrentMapData(activeMapId); if(activeMapData !== false){ // .. active map found, just update no tab switch let tabContentEl = document.getElementById(config.mapTabIdPrefix + activeMapId); let areaMap = tabContentEl.querySelector(`.${Util.getMapTabContentAreaClass('map')}`); return Map.loadMap(areaMap, activeMapData, {}); }else{ console.error('No active map found!'); } }else{ // .. no map active, make one active return showDefaultTab(tabEl); } }).then(payload => resolve()); }else{ // tab Element does not exists -> create ========================================================== let promisesAddTab = []; tabEl = newMapTabsElement(); mapModule.prepend(tabEl); // add new tab for each map for(let j = 0; j < tempMapData.length; j++){ let data = tempMapData[j]; promisesAddTab.push(addTab(tabEl, data.config)); } // "add" map tab let tabAddOptions = { id: 0, type: { classTab: MapUtil.getInfoForMap('standard', 'classTab'), name: 'new' }, icon: 'fa-plus', name: 'add', area: 'left', tabType: 'add', position: 90 // always the most right tab }; // "settings" tab let tabSettingsOptions = { id: 0, type: { classTab: MapUtil.getInfoForMap('standard', 'classTab') }, icon: 'fa-tv', name: '', area: 'left', tabType: 'settings', position: 100, // always the most right tab draggable: false }; promisesAddTab.push(addTab(tabEl, tabAddOptions)); promisesAddTab.push(addTab(tabEl, tabSettingsOptions)); Promise.all(promisesAddTab) .then(payload => showDefaultTab(tabEl)) .then(payload => resolve()); } } }; return new Promise(updateMapModuleExecutor).then(payload => { // log client map update time let duration = Util.timeStop(logKeyClientMapData); Util.log(logKeyClientMapData, {duration: duration, type: 'client', description: 'update map'}); }); }; let mapTabOnShow = (tabBarEl, mapId) => { // save mapId as new "default" (local storage) Util.getLocalStore('character').setItem(`${Util.getCurrentCharacterId()}.defaultMapId`, mapId); // update mapTab element updateMapTabElement(mapId); }; /** * update mapTab element * -> e.g. "Settings" popover data * -> e.g. update mapModule layout * @param mapId */ let updateMapTabElement = mapId => { let tabEl = document.getElementById(config.mapTabElementId); if(!tabEl || !mapId){ return; } let setMapTabLayout = (tabEl, layoutNew) => { config.mapTabContentLayoutOptions.forEach(layout => tabEl.classList.toggle(layout, layout === layoutNew)); }; Promise.all([ getModules(), Util.getLocalStore('map').getItem(mapId) ]).then(payload => { let modules = payload[0]; let localDataMap = payload[1]; let layoutCurrent = Util.getObjVal(localDataMap, 'layout') || config.defaultMapTabContentLayout; let disabledValues = Util.getObjVal(localDataMap, 'modulesDisabled'); // update mapModule with current layout class setMapTabLayout(tabEl, layoutCurrent); // prepare select options for modules let modulePrioCounts = Array(BaseModule.scopeOrder.length).fill(0); let sourceOptions = modules.sort((a, b) => a.getOrderPrio() - b.getOrderPrio()).map(Module => ({ value: Module.name, text: Module.label, metaData: { scope: Module.scope, orderPrio: Module.getOrderPrio(), prioCount: ++modulePrioCounts[Module.getOrderPrio()], isPlugin: Module.isPlugin } })); // default -> all modules selected -> plugin modules disabled if(!disabledValues){ disabledValues = sourceOptions.reduce((acc, optionData) => { if(optionData.metaData.isPlugin){ acc.push(optionData.value); } return acc; }, [] ); Util.getLocalStore('map').setItem(`${mapId}.modulesDisabled`, disabledValues); } let settingsLinkEl = tabEl.querySelector(`.${config.mapTabClass} > a[data-tab-type="settings"]`); if(settingsLinkEl){ // settings settingsLinkEl should always exist settingsLinkEl = $(settingsLinkEl); /** * we store "unselected" options only -> new modules should be visible by default! * @param sourceOptions * @param values * @returns [] */ let invertValues = (sourceOptions, values = []) => sourceOptions .filter(optionData => !values.includes(optionData.value)) .map(optionData => optionData.value); let selectedValues = invertValues(sourceOptions, disabledValues); if(settingsLinkEl.data('editable')){ settingsLinkEl.editable('setValue', selectedValues); settingsLinkEl.editable('option', 'pk', mapId); settingsLinkEl.editable('option', 'pfLayoutCurrent', layoutCurrent); }else{ settingsLinkEl.on('shown', (e, editable) => { let layoutCurrent = Util.getObjVal(editable, 'options.pfLayoutCurrent'); // add "layout" toggle to popover ----------------------------------------------------------------- let anchorEl = editable.container.$form[0].querySelector(`.${config.editableSettingsClass}`); let gridEl = Object.assign(document.createElement('div'), { className: config.editableToggleClass }); let gridItemEls = config.mapTabContentLayoutOptions.map(layout => { let gridItemEl = Object.assign(document.createElement('div'), { className: config.editableToggleItemClass + (layout === layoutCurrent ? ' active' : '') }); gridItemEl.style.setProperty('--bg-image',`url("/public/img/icons/grid_${layout}.png")`); gridItemEl.dataset.value = layout; return gridItemEl; }); gridItemEls.splice(1, 0, Object.assign(document.createElement('i'), { className: ['fas', 'fa-lg', 'fa-exchange-alt'].join(' ') })); gridEl.append(...gridItemEls); anchorEl.insertAdjacentElement('beforebegin', gridEl); // click event for layout switch -> change layout -> store new layout setting EventHandler.addEventListener(gridEl, 'click.toggleSelect', e => { if(e.target.classList.contains(config.editableToggleItemClass)){ let layoutNew = e.target.dataset.value; if(layoutNew !== layoutCurrent){ gridItemEls.forEach(gridItemEl => gridItemEl.classList.toggle('active')); let activeMapId = Util.getObjVal(editable, 'options.pk'); Util.getLocalStore('map').setItem(`${activeMapId}.layout`, layoutNew).then(layoutNew => { setMapTabLayout(tabEl, layoutNew); // for next "toggle" detection layoutCurrent = layoutNew; editable.option('pfLayoutCurrent', layoutNew); }); } } }, {passive: false}); // add "headlines" to Modules checklist ------------------------------------------------------- anchorEl.childNodes.forEach((gridItem, i) => { if(sourceOptions[i].metaData.prioCount === 1){ gridItem.classList.add(config.editableHeadlineClass); gridItem.setAttribute('data-count', modulePrioCounts[sourceOptions[i].metaData.orderPrio] ); gridItem.setAttribute('data-headline', BaseModule.scopeOrder[sourceOptions[i].metaData.orderPrio] ); } }); }); settingsLinkEl.on('save', {sourceOptions: sourceOptions}, (e, params) => { let editable = $(e.target).data('editable'); let activeMapId = Util.getObjVal(editable, 'options.pk'); let oldValue = editable.value; let newValue = invertValues(e.data.sourceOptions, params.newValue || []); let map = MapUtil.getMapInstance(activeMapId); let tabContentEl = document.getElementById(config.mapTabIdPrefix + activeMapId); Util.getLocalStore('map').setItem(`${activeMapId}.modulesDisabled`, newValue).then(newValue => { let hideModules = filterModules(modules, oldValue.diff(params.newValue), 'name'); let showModules = filterModules(modules, params.newValue.diff(oldValue), 'name'); removeModules(hideModules, tabContentEl).then(payload => { let showGlobalModules = showModules.filter(Module => Module.scope === 'global'); let showSystemModules = showModules.filter(Module => Module.scope === 'system'); let showConnectionModules = showModules.filter(Module => Module.scope === 'connection'); if(showGlobalModules.length){ renderModules(showGlobalModules, tabContentEl, { mapId: activeMapId, payload: null }); } if( showSystemModules.length && Util.getCurrentSystemData(activeMapId) ){ renderModules(showSystemModules, tabContentEl, { mapId: activeMapId, payload: Util.getCurrentSystemData(activeMapId) }); } if( showConnectionModules.length && MapUtil.getConnectionsByType(map, 'state_active').length ){ renderModules(showConnectionModules, tabContentEl, { mapId: activeMapId, payload: MapUtil.getConnectionsByType(map, 'state_active') }); } }); }); }); settingsLinkEl.editable({ toggle: 'manual', mode: 'popup', type: 'checklist', showbuttons: false, onblur: 'submit', highlight: false, title: 'layout settings', placement: 'left', pk: mapId, value: selectedValues, //prepend: prependOptions, source: sourceOptions, emptyclass: '', emptytext: '', display: function(value, sourceData){ // update filter badge if( value && sourceData && value.length < sourceData.length ){ this.dataset.badge = String(value.length); }else{ delete this.dataset.badge; } }, tpl: `
    `, pfLayoutCurrent: layoutCurrent }); } } }); }; /** * collect all data (systems/connections) for export/save from each active map in the map module * if no change detected -> do not attach map data to return array * @param {HTMLElement} mapModule * @param filter * @returns {[]} */ let getMapModuleDataForUpdate = (mapModule, filter = ['hasId', 'hasChanged']) => { let data = []; [...mapModule.getElementsByClassName(Util.config.mapClass)].forEach(mapElement => { // get all changed (system / connection) data from this map let mapData = Map.getMapDataForSync(mapElement, filter); if( mapData && ( (Util.getObjVal(mapData, 'data.systems') || []).length || (Util.getObjVal(mapData, 'data.connections') || []).length ) ){ data.push(mapData); } }); return data; }; return { updateTabData: updateTabData, updateMapModule: updateMapModule, updateActiveMapUserData: updateActiveMapUserData, updateSystemModulesData: updateSystemModulesData, getMapModuleDataForUpdate: getMapModuleDataForUpdate }; });