/** * System info module */ define([ 'jquery', 'app/init', 'app/util', 'app/map/util', 'module/base' ], ($, Init, Util, MapUtil, BaseModule) => { 'use strict'; let SystemInfoModule = class SystemInfoModule extends BaseModule { constructor(config = {}) { super(Object.assign({}, new.target.defaultConfig, config)); } /** * custom header for this module * @returns {*} */ newHeaderElement(){ let headEl = this.newHeadElement(); let headAliasEl = this.newHeadlineElement(this._systemData.alias || this._systemData.name); headAliasEl.setAttribute('title', 'alias'); headAliasEl.classList.add('pull-right'); if(this._systemData.security === 'A'){ headAliasEl.classList.add(this._config.fontTriglivianClass); } let iconEl = this.newIconElement(['fa-fw', 'fa-angle-double-right']); let headSysTypeEl = this.newHeadlineElement(); let sysTypeEl = document.createElement('span'); sysTypeEl.setAttribute('title', 'type'); sysTypeEl.classList.add(this._config.typeLinkClass); sysTypeEl.textContent = MapUtil.getSystemTypeInfo(this._systemData.type.id, 'name'); headSysTypeEl.append(sysTypeEl, iconEl); let headSysRegionEl = this.newHeadlineElement(); let sysRegionEl = document.createElement('span'); sysRegionEl.setAttribute('title', 'region'); sysRegionEl.classList.add(this._config.regionLinkClass); if(this._systemData.security === 'A'){ sysRegionEl.classList.add(this._config.fontTriglivianClass); } sysRegionEl.textContent = this._systemData.region.name; headSysRegionEl.append(sysRegionEl, iconEl.cloneNode()); let headSysConstellationEl = this.newHeadlineElement(); let sysConstellationEl = document.createElement('span'); sysConstellationEl.classList.add(this._config.constellationLinkClass, this._config.linkClass, Util.config.popoverTriggerClass); if(this._systemData.security === 'A'){ sysConstellationEl.classList.add(this._config.fontTriglivianClass); } sysConstellationEl.textContent = this._systemData.constellation.name; sysConstellationEl.setAttribute('popup-ajax', Init.path.getConstellationData + '/' + this._systemData.constellation.id); headSysConstellationEl.append(sysConstellationEl, iconEl.cloneNode()); let headSysNameEl = this.newHeadlineElement(); let sysNameEl = document.createElement('span'); sysNameEl.setAttribute('title', 'system'); if(this._systemData.security === 'A'){ sysNameEl.classList.add(this._config.fontTriglivianClass); } sysNameEl.textContent = this._systemData.name; let iconCopyEl = this.newIconElement(['fa-fw', 'fa-copy', this._config.moduleHeadlineIconClass, this._config.textActionIconCopyClass]); iconCopyEl.setAttribute('title', 'copy url'); iconCopyEl.dataset.copy = MapUtil.getMapDeeplinkUrl(this._systemData.mapId, this._systemData.id); headSysNameEl.append(sysNameEl, iconCopyEl); if(this._systemData.locked){ let iconLockedEl = this.newIconElement(['fa-fw', 'fa-lock', this._config.moduleHeadlineIconClass]); iconLockedEl.setAttribute('title', 'locked'); headSysNameEl.append(iconLockedEl); } for(let linkData of this.getThirdPartySystemLinks(['dotlan', 'eveeye', 'anoik'])){ if(linkData.showInModuleHead){ let headSysLinkEl = document.createElement('a'); headSysLinkEl.classList.add('pf-bg-icon-inline'); headSysLinkEl.style.setProperty('--bg-image', `url("${Util.imgRoot()}icons/logo_${linkData.page}.png")`); headSysLinkEl.setAttribute('title', linkData.title); headSysLinkEl.setAttribute('href', linkData.url); headSysLinkEl.setAttribute('target', '_blank'); headSysLinkEl.setAttribute('rel', 'noopener'); headSysNameEl.append(headSysLinkEl); } } headEl.append( this.newHandlerElement(), headAliasEl, headSysTypeEl, headSysRegionEl, headSysConstellationEl, headSysNameEl ); return headEl; } /** * update module * @param systemData * @returns {Promise} */ update(systemData){ return super.update(systemData).then(systemData => new Promise(resolve => { if( this._systemData.id === systemData.id && this._updated !== systemData.updated.updated ){ let setUpdated = true; // created/updated tooltip ------------------------------------------------------------------------ let nameRowElement = $(this.moduleElement).find('.' + this._config.systemInfoNameClass); let tooltipData = { created: systemData.created, updated: systemData.updated }; nameRowElement.addCharacterInfoTooltip(tooltipData); // update system status --------------------------------------------------------------------------- let systemStatusLabelElement = $(this.moduleElement).find('.' + this._config.systemInfoStatusLabelClass); let systemStatusId = parseInt(systemStatusLabelElement.attr('data-status')); if(systemStatusId !== systemData.status.id){ // status changed let currentStatusClass = Util.getStatusInfoForSystem(systemStatusId, 'class'); let newStatusClass = Util.getStatusInfoForSystem(systemData.status.id, 'class'); let newStatusLabel = Util.getStatusInfoForSystem(systemData.status.id, 'label'); systemStatusLabelElement.removeClass(currentStatusClass).addClass(newStatusClass).text(newStatusLabel); // set new status attribute systemStatusLabelElement.attr('data-status', systemData.status.id); } // update description textarea -------------------------------------------------------------------- let descriptionTextareaElement = $(this.moduleElement).find('.' + this._config.descriptionTextareaElementClass); if(descriptionTextareaElement.length){ let description = descriptionTextareaElement.html(); if(description !== systemData.description){ // description has changed if(typeof descriptionTextareaElement.data().summernote === 'object'){ // "Summernote" editor is currently open setUpdated = false; }else{ // not open let newDescription = systemData.description; if(!Util.isValidHtml(newDescription)){ // try to convert raw text into valid html newDescription = newDescription.replace(/(\r\n|\n|\r)/g, '
'); newDescription = '

' + newDescription + '

'; } descriptionTextareaElement.html(newDescription); } } } if(setUpdated){ this._updated = systemData.updated.updated; } } $(this.moduleElement).hideLoadingAnimation(); resolve({ action: 'update', data: { module: this } }); })); } /** * render module * @param mapId * @param systemData * @returns {HTMLElement} */ render(mapId, systemData){ this._systemData = systemData; let rowEl = document.createElement('div'); rowEl.classList.add(this._config.bodyClassName, 'grid'); let colInfoEl, colSovEl, colDescEl; colInfoEl = document.createElement('div'); colInfoEl.classList.add(this._config.systemInfoSectionClass); rowEl.append(colInfoEl); colSovEl = document.createElement('div'); colSovEl.classList.add(this._config.systemSovSectionClass, 'pf-dynamic-area'); rowEl.append(colSovEl); colDescEl = document.createElement('div'); colDescEl.classList.add(this._config.descriptionSectionClass); rowEl.append(colDescEl); this.moduleElement.append(rowEl); require(['text!templates/modules/system_info.html', 'mustache', 'summernote.loader'], (template, Mustache, Summernote) => { SystemInfoModule.Mustache = Mustache; SystemInfoModule.Summernote = Summernote; template = new DOMParser().parseFromString(template, 'text/html'); SystemInfoModule.tplInfoSection = template.getElementById('tplInfoSection').innerHTML; SystemInfoModule.tplSovSection = template.getElementById('tplSovSection').innerHTML; SystemInfoModule.tplDescSection = template.getElementById('tplDescSection').innerHTML; // optional parse() for cache parsed templates SystemInfoModule.Mustache.parse(SystemInfoModule.tplInfoSection); SystemInfoModule.Mustache.parse(SystemInfoModule.tplSovSection); SystemInfoModule.Mustache.parse(SystemInfoModule.tplDescSection); this.renderInfoSection(colInfoEl, SystemInfoModule.tplInfoSection); this.renderSovSection(colSovEl, SystemInfoModule.tplSovSection); this.renderDescSection(colDescEl, SystemInfoModule.tplDescSection); this.setModuleObserver(); }); return this.moduleElement; } /** * render 'sovereignty' section * @param parentEl * @param template */ renderInfoSection(parentEl, template){ if(SystemInfoModule.Mustache && parentEl && template){ let data = { config: this._config, system: this._systemData, systemStatusClass: Util.getStatusInfoForSystem(this._systemData.status.id, 'class'), systemStatusLabel: Util.getStatusInfoForSystem(this._systemData.status.id, 'label'), systemNameClass: this._systemData.security === 'A' ? this._config.fontTriglivianClass : '', systemSecurityClass: Util.getSecurityClassForSystem(this._systemData.security), trueSec: this._systemData.trueSec.toFixed(1), trueSecClass: Util.getTrueSecClassForSystem(this._systemData.trueSec), systemEffectName: MapUtil.getEffectInfoForSystem(this._systemData.effect, 'name'), systemEffectClass: MapUtil.getEffectInfoForSystem(this._systemData.effect, 'class'), systemPlanetCount: this._systemData.planets ? this._systemData.planets.length : 0, systemStaticData: (Util.getObjVal(this._systemData, 'statics') || []).reduce((acc, wormholeName) => { acc.push(Object.assign({}, Init.wormholes[wormholeName])); return acc; }, []), systemShatteredClass: Util.getSecurityClassForSystem('SH'), popoverTriggerClass: Util.config.popoverTriggerClass }; parentEl.innerHTML = SystemInfoModule.Mustache.render(template, data); this.setInfoSectionObserver(parentEl); } } /** * render 'sovereignty' section * @param parentEl * @param template */ renderSovSection(parentEl, template){ if(SystemInfoModule.Mustache && parentEl && template){ // system "sovereignty" data // "primary" data is either "alliance" -> 0.0 space // or "faction" -> Empire Regions (LS, HS) let sovereigntyDefault = { row1Label: 'Sov.', row1Val: '???', row1Img: undefined, row1ImgTitle: undefined, row2Label: undefined, row2Val: undefined, row3Label: undefined, row3Val: undefined }; let sovereigntyPrimary; let sovereigntySecondary; if(this._systemData.sovereignty){ let sovDataFact = Util.getObjVal(this._systemData.sovereignty, 'faction'); let sovDataAlly = Util.getObjVal(this._systemData.sovereignty, 'alliance'); let sovDataCorp = Util.getObjVal(this._systemData.sovereignty, 'corporation'); if(sovDataFact){ sovereigntyPrimary = { row1Val: 'Faction', row1Img: Util.eveImageUrl('factions', sovDataFact.id, 64), row1ImgTitle: sovDataFact.name, row2Val: sovDataFact.name }; }else{ if(sovDataAlly){ sovereigntyPrimary = { row1Val: 'Alliance', row1Img: Util.eveImageUrl('alliances', sovDataAlly.id, 64), row1ImgTitle: sovDataAlly.name, row2Val: '<' + sovDataAlly.ticker + '>', row3Label: 'Ally', row3Val: sovDataAlly.name }; } if(sovDataCorp){ sovereigntySecondary = { row1Label: 'Corp', row1Val: sovDataCorp.name, row1Img: Util.eveImageUrl('corporations', sovDataCorp.id, 64) }; } } } let data = { config: this._config, sovereigntyPrimary: sovereigntyPrimary ? Object.assign({}, sovereigntyDefault, sovereigntyPrimary) : undefined, sovereigntySecondary: sovereigntySecondary ? Object.assign({}, sovereigntyDefault, sovereigntySecondary) : undefined, }; // show only if sov data exists if(data.sovereigntyPrimary){ parentEl.innerHTML = SystemInfoModule.Mustache.render(template, data); this.setSovSectionObserver(parentEl); }else{ parentEl.parentNode.classList.add(this._config.bodyClassName + '-small'); parentEl.remove(); } } } /** * render 'description' section * @param parentEl * @param template */ renderDescSection(parentEl, template){ if(SystemInfoModule.Mustache && parentEl && template){ let data = { config: this._config, summernoteClass: Util.config.summernoteClass }; parentEl.innerHTML = SystemInfoModule.Mustache.render(template, data); this.setDescSectionObserver(parentEl); } } /** * init module */ init(){ super.init(); } /** * set module observer */ setModuleObserver(){ // init copy system deeplink URL $(this.moduleElement).find('.' + this._config.textActionIconCopyClass).on('click', function(){ let mapUrl = $(this).attr('data-copy'); Util.copyToClipboard(mapUrl).then(payload => { if(payload.data){ Util.showNotify({title: 'Copied to clipboard', text: mapUrl, type: 'success'}); } }); }); // init constellation popover $(this.moduleElement).find('[popup-ajax]').popover({ html: true, trigger: 'hover', placement: 'top', delay: 200, container: 'body', content: function(){ //return details_in_popup(this); let popoverElement = $(this); let popover = popoverElement.data('bs.popover'); $.ajax({ url: popoverElement.attr('popup-ajax'), success: function(data){ popover.options.content = Util.getSystemsInfoTable(data.systemsData); // reopen popover (new content size) popover.show(); } }); return 'Loading...'; } }); // init tooltips $(this.moduleElement).initTooltips({ placement: 'top' }); } /** * observer 'info' section * @param parentEl */ setInfoSectionObserver(parentEl){ // init system effect popover $(parentEl).find('.' + this._config.systemInfoEffectClass).addSystemEffectTooltip( this._systemData.security, this._systemData.effect, { placement: 'left' }); // init planets popover $(parentEl).find('.' + this._config.systemInfoPlanetsClass).addSystemPlanetsTooltip( this._systemData.planets, { placement: 'left' }); // init static wormhole popover MapUtil.initWormholeInfoTooltip($(parentEl), '[data-name]', { placement: 'left' }); } /** * observer 'sovereignty' section * @param parentEl */ setSovSectionObserver(parentEl){ // "lock" area until first update $(parentEl).showLoadingAnimation(); } /** * observer 'description' section * @param parentEl */ setDescSectionObserver(parentEl){ let descriptionArea = $(parentEl).find('.' + this._config.descriptionAreaClass); let descriptionButton = $(parentEl).find('.' + this._config.addDescriptionButtonClass); let descriptionTextareaElement = $(parentEl).find('.' + this._config.descriptionTextareaElementClass); let maxDescriptionLength = this._config.maxDescriptionLength; let systemId = this._systemData.id; let saveCallback = this.update.bind(this); // "lock" area until first update descriptionArea.showLoadingAnimation(); // WYSIWYG init on button click --------------------------------------------------------------------------- descriptionButton.on('click', function(e){ e.stopPropagation(); let descriptionButton = $(this); // hide edit button descriptionButton.hide(); // content has changed let descriptionChanged = false; SystemInfoModule.Summernote.initSummernote(descriptionTextareaElement, { height: 75, // set editor height minHeight: 75, // set minimum height of editor maxHeight: 500, // set maximum height of editor focus: true, placeholder: false, maxTextLength: maxDescriptionLength, disableDragAndDrop: true, shortcuts: false, toolbar: [ ['style', ['style']], ['font', ['underline', 'strikethrough', 'clear']], ['color', ['color']], ['para', ['ul', 'ol', 'paragraph']], ['table', ['table']], ['insert', ['link', 'hr']], //['view', ['codeview', 'help']], ['misc', ['undo', 'redo']], ['lengthField'], ['customBtn', ['discardBtn', 'saveBtn']] ], buttons: { saveBtn: context => { let ui = $.summernote.ui; let button = ui.button({ contents: '', container: 'body', className: ['btn-success', 'btn-save'], click: e => { context.layoutInfo.editable.removeClass('has-error'); // save changes if(descriptionChanged){ let validDescription = true; let description = ''; if(context.$note.summernote('isEmpty')){ // ... isEmpty -> clear empty default tags as well context.$note.summernote('code', ''); }else{ description = context.$note.summernote('code'); if(!Util.isValidHtml(description)){ // ... not valid HTML validDescription = false; context.layoutInfo.editable.addClass('has-error'); Util.showNotify({title: 'Validation failed', text: 'HTML not valid', type: 'error'}); } } if(validDescription){ // ... valid -> save() descriptionArea.showLoadingAnimation(); Util.request('PATCH', 'System', systemId, { description: description }, { descriptionArea: descriptionArea }, context => { // always do context.descriptionArea.hideLoadingAnimation(); }).then( payload => { context.$note.summernote('destroy'); saveCallback(payload.data); }, Util.handleAjaxErrorResponse ); } }else{ // ... no changes -> no save() context.$note.summernote('destroy'); } } }); return button.render(); } }, callbacks: { onInit: function(context){ // make editable field a bit larger context.editable.css('height', '150px'); // set default background color // -> could not figure out how to set by API as default color let defaultBgColor = '#e2ce48'; context.toolbar.find('.note-current-color-button').attr('data-backcolor', defaultBgColor) .find('.note-recent-color').css('background-color', defaultBgColor); }, onChange: function(contents){ descriptionChanged = true; }, onPaste: function(e){ let bufferText = ((e.originalEvent || e).clipboardData || window.clipboardData).getData('Text'); e.preventDefault(); // Firefox fix setTimeout(() => { document.execCommand('insertText', false, bufferText); }, 10); }, onDestroy: function(context){ descriptionButton.show(); } } }); }); } /** * get 3rd party system link configuration * @param pages * @returns {[]} */ getThirdPartySystemLinks(pages){ let links = []; let isWormhole = MapUtil.getSystemTypeInfo(Util.getObjVal(this._systemData, 'type.id'), 'name') === 'w-space'; let systemName = Util.getObjVal(this._systemData, 'name') || ''; let regionName = Util.getObjVal(this._systemData, 'region.name') || ''; for(let i = 0; i < pages.length; i++){ let link = null; let showInModuleHead = true; let domain = Util.getObjVal(Init, 'url.' + pages[i]); if(domain){ // linkOut url let url = false; switch(pages[i]){ case 'dotlan': let systemNameTemp = systemName.replace(/ /g, '_'); let regionNameTemp = regionName.replace(/ /g, '_'); if(isWormhole){ url = domain + '/system/' + systemNameTemp; }else{ url = domain + '/map/' + regionNameTemp + '/' + systemNameTemp; } break; case 'eveeye': if(!isWormhole){ url = domain + '/?m=' + encodeURIComponent(regionName) + '&s=' + encodeURIComponent(systemName); url += '&t=eswkc&o=thera,con_svc,node_sov,sub_sec,sector_fac,tag_mk'; } break; case 'anoik': if(isWormhole){ url = domain + '/systems/' + systemName; } break; } if(url){ let urlObj = new URL(url); link = { title: urlObj.hostname, url: url }; } } if(link){ links.push(Object.assign({}, link, { page: pages[i], showInModuleHead: showInModuleHead })); } } return links; } beforeDestroy(){ super.beforeDestroy(); let descriptionTextareaEl = this.moduleElement.querySelector(`.${this._config.descriptionTextareaElementClass}`); if(descriptionTextareaEl && $(descriptionTextareaEl).summernote){ $(descriptionTextareaEl).summernote('destroy'); } } }; SystemInfoModule.isPlugin = false; // module is defined as 'plugin' SystemInfoModule.scope = 'system'; // module scope controls how module gets updated and what type of data is injected SystemInfoModule.sortArea = 'a'; // default sortable area SystemInfoModule.position = 2; // default sort/order position within sortable area SystemInfoModule.label = 'Information'; // static module label (e.g. description) SystemInfoModule.fullDataUpdate = true; // static module requires additional data (e.g. system description,...) SystemInfoModule.defaultConfig = { className: 'pf-system-info-module', // class for module sortTargetAreas: ['a', 'b', 'c'], // sortable areas where module can be dragged into // headline toolbar textActionIconCopyClass: 'pf-module-icon-button-copy', // class for text action "copy" // breadcrumb constellationLinkClass: 'pf-system-info-constellation', // class for "constellation" name regionLinkClass: 'pf-system-info-region', // class for "region" name typeLinkClass: 'pf-system-info-type', // class for "type" name // info area systemInfoSectionClass: 'pf-system-info-section', // class for system info section systemInfoTableClass: 'pf-module-table', // class for system info table systemInfoNameClass: 'pf-system-info-name', // class for "name" information element systemInfoEffectClass: 'pf-system-info-effect', // class for "effect" information element systemInfoPlanetsClass: 'pf-system-info-planets', // class for "planets" information element systemInfoStatusLabelClass: 'pf-system-info-status-label', // class for "status" information element // sovereignty area systemSovSectionClass: 'pf-system-sov-section', // class for system sov. section systemSovTableClass: 'pf-module-table', // class for system sov. table systemSovFwContestedRowClass: 'pf-system-sov-fw-contested-row', // class for "contested" sov. table row systemSovFwOccupationRowClass: 'pf-system-sov-fw-occupation-row', // class for "-occupation" sov. table row systemSovFwContestedClass: 'pf-system-sov-fw-contested', systemSovFwPercentageClass: 'pf-system-sov-fw-percentage', systemSovFwOccupationClass: 'pf-system-sov-fw-occupation', systemSovFwOccupationImageClass: 'pf-system-sov-fw-occupation-image', systemSovFwStatusIconClass: 'pf-system-sov-fw-status-icon', // description area descriptionSectionClass: 'pf-system-description-section', // class for system description section descriptionAreaClass: 'pf-system-info-description-area', // class for description area addDescriptionButtonClass: 'pf-system-info-description-button', // class for "add description" button descriptionTextareaElementClass: 'pf-system-info-description', // class for description textarea element (Summernote) maxDescriptionLength: 9000, // max character length for system description // fonts fontTriglivianClass: 'pf-triglivian', // class for "Triglivian" names (e.g. Abyssal systems) linkClass: 'pf-link' }; return SystemInfoModule; });