diff --git a/app/main/controller/api/map.php b/app/main/controller/api/map.php index d806ab0de..14ce90de8 100644 --- a/app/main/controller/api/map.php +++ b/app/main/controller/api/map.php @@ -231,13 +231,6 @@ public function initData(\Base $f3){ } } - // Add data that should not be cached ========================================================================= - - // program mode (e.g. "maintenance") -------------------------------------------------------------------------- - $return->programMode = [ - 'maintenance' => Config::getPathfinderData('login.mode_maintenance') - ]; - // get SSO error messages that should be shown immediately ---------------------------------------------------- // -> e.g. errors while character switch from previous HTTP requests if($f3->exists(Controller\Ccp\Sso::SESSION_KEY_SSO_ERROR, $message)){ diff --git a/app/main/controller/api/system.php b/app/main/controller/api/system.php index bb80eed5e..890ed6b2d 100644 --- a/app/main/controller/api/system.php +++ b/app/main/controller/api/system.php @@ -10,6 +10,7 @@ use Controller; use Model; +use Exception; class System extends Controller\AccessController { @@ -79,24 +80,34 @@ public function save(\Base $f3){ } if( !is_null($systemModel) ){ - // set/update system custom data - $systemModel->copyfrom($systemData, ['statusId', 'locked', 'rallyUpdated', 'position', 'description']); - - if($systemModel->save($activeCharacter)){ - // get data from "fresh" model (e.g. some relational data has changed: "statusId") - /** - * @var $newSystemModel Model\SystemModel - */ - $newSystemModel = Model\BasicModel::getNew('SystemModel'); - $newSystemModel->getById( $systemModel->_id, 0); - $newSystemModel->clearCacheData(); - $return->systemData = $newSystemModel->getData(); - - // broadcast map changes - $this->broadcastMapData($newSystemModel->mapId); - }else{ - $return->error = $systemModel->getErrors(); + try{ + // set/update system custom data + $systemModel->copyfrom($systemData, ['statusId', 'locked', 'rallyUpdated', 'position', 'description']); + + if($systemModel->save($activeCharacter)){ + // get data from "fresh" model (e.g. some relational data has changed: "statusId") + /** + * @var $newSystemModel Model\SystemModel + */ + $newSystemModel = Model\BasicModel::getNew('SystemModel'); + $newSystemModel->getById( $systemModel->_id, 0); + $newSystemModel->clearCacheData(); + $return->systemData = $newSystemModel->getData(); + + // broadcast map changes + $this->broadcastMapData($newSystemModel->mapId); + }else{ + $return->error = $systemModel->getErrors(); + } + }catch(Exception\ValidationException $e){ + $validationError = (object) []; + $validationError->type = 'error'; + $validationError->field = $e->getField(); + $validationError->message = $e->getMessage(); + $return->error[] = $validationError; } + + } } @@ -273,13 +284,14 @@ public function getData(\Base $f3){ $requestData = (array)$f3->get('POST'); $mapId = (int)$requestData['mapId']; $systemId = (int)$requestData['systemId']; + $isCcpId = (bool)$requestData['isCcpId']; $activeCharacter = $this->getCharacter(); $return = (object) []; if( !is_null($map = $activeCharacter->getMap($mapId)) && - !is_null($system = $map->getSystemById($systemId)) + !is_null($system = $isCcpId ? $map->getSystemByCCPId($systemId) : $map->getSystemById($systemId)) ){ $return->system = $system->getData(); $return->system->signatures = $system->getSignaturesData(); diff --git a/app/main/model/systemmodel.php b/app/main/model/systemmodel.php index 7704d5a3a..5d685093a 100644 --- a/app/main/model/systemmodel.php +++ b/app/main/model/systemmodel.php @@ -94,10 +94,11 @@ class SystemModel extends AbstractMapTrackingModel { 'activity-log' => true ], 'description' => [ - 'type' => Schema::DT_VARCHAR512, + 'type' => Schema::DT_TEXT, 'nullable' => false, 'default' => '', - 'activity-log' => true + 'activity-log' => true, + 'validate' => true ], 'posX' => [ 'type' => Schema::DT_INT, @@ -262,6 +263,21 @@ protected function validate_statusId(string $key, int $val): bool { return $valid; } + /** + * @param string $key + * @param string $val + * @return bool + * @throws \Exception\ValidationException + */ + protected function validate_description(string $key, string $val): bool { + $valid = true; + if(mb_strlen($val) > 9000){ + $valid = false; + $this->throwValidationException($key); + } + return $valid; + } + /** * setter for system alias * @param string $alias diff --git a/app/pathfinder.ini b/app/pathfinder.ini index 59876bc7f..ab2259003 100644 --- a/app/pathfinder.ini +++ b/app/pathfinder.ini @@ -3,7 +3,7 @@ [PATHFINDER] NAME = Pathfinder ; installed version (used for CSS/JS cache busting) -VERSION = v1.4.1 +VERSION = v1.4.2 ; contact information [optional] CONTACT = https://github.com/exodus4d ; public contact email [optional] diff --git a/composer-dev.json b/composer-dev.json index 313993615..e1d076963 100644 --- a/composer-dev.json +++ b/composer-dev.json @@ -23,6 +23,7 @@ "php-64bit": ">=7.0", "ext-curl": "*", "ext-json": "*", + "ext-mbstring": "*", "ext-ctype": "*", "ext-zmq": ">=1.1.3", "react/zmq": "0.3.*", diff --git a/composer.json b/composer.json index 0f020b5f3..6507b0cda 100644 --- a/composer.json +++ b/composer.json @@ -23,6 +23,7 @@ "php-64bit": ">=7.0", "ext-curl": "*", "ext-json": "*", + "ext-mbstring": "*", "ext-ctype": "*", "ext-zmq": ">=1.1.3", "react/zmq": "0.3.*", diff --git a/js/app.js b/js/app.js index 2b8dd65f5..de330a66b 100644 --- a/js/app.js +++ b/js/app.js @@ -25,37 +25,40 @@ requirejs.config({ admin: './app/admin', // initial start "admin page" view notification: './app/notification', // "notification" view - jquery: 'lib/jquery-3.3.1.min', // v3.3.1 jQuery - bootstrap: 'lib/bootstrap.min', // v3.3.0 Bootstrap js code - http://getbootstrap.com/javascript - text: 'lib/requirejs/text', // v2.0.12 A RequireJS/AMD loader plugin for loading text resources. - mustache: 'lib/mustache.min', // v1.0.0 Javascript template engine - http://mustache.github.io - localForage: 'lib/localforage.min', // v1.4.2 localStorage library - https://mozilla.github.io/localForage - velocity: 'lib/velocity.min', // v1.5.1 animation engine - http://julian.com/research/velocity - velocityUI: 'lib/velocity.ui.min', // v5.2.0 plugin for velocity - http://julian.com/research/velocity/#uiPack - slidebars: 'lib/slidebars', // v0.10 Slidebars - side menu plugin http://plugins.adchsm.me/slidebars - jsPlumb: 'lib/dom.jsPlumb-1.7.6', // v1.7.6 jsPlumb (Vanilla)- main map draw plugin https://jsplumbtoolkit.com - farahey: 'lib/farahey-0.5', // v0.5 jsPlumb "magnetizing" extension - https://github.com/jsplumb/farahey - customScrollbar: 'lib/jquery.mCustomScrollbar.min', // v3.1.5 Custom scroll bars - http://manos.malihu.gr - mousewheel: 'lib/jquery.mousewheel.min', // v3.1.13 Mousewheel - https://github.com/jquery/jquery-mousewheel - xEditable: 'lib/bootstrap-editable.min', // v1.5.1 X-editable - in placed editing - morris: 'lib/morris.min', // v0.5.1 Morris.js - graphs and charts - raphael: 'lib/raphael-min', // v2.1.2 Raphaël - required for morris (dependency) - bootbox: 'lib/bootbox.min', // v4.4.0 Bootbox.js - custom dialogs - http://bootboxjs.com - easyPieChart: 'lib/jquery.easypiechart.min', // v2.1.6 Easy Pie Chart - HTML 5 pie charts - http://rendro.github.io/easy-pie-chart - peityInlineChart: 'lib/jquery.peity.min', // v3.2.1 Inline Chart - http://benpickles.github.io/peity/ - dragToSelect: 'lib/jquery.dragToSelect', // v1.1 Drag to Select - http://andreaslagerkvist.com/jquery/drag-to-select - hoverIntent: 'lib/jquery.hoverIntent.min', // v1.9.0 Hover intention - http://cherne.net/brian/resources/jquery.hoverIntent.html - fullScreen: 'lib/jquery.fullscreen.min', // v0.6.0 Full screen mode - https://github.com/private-face/jquery.fullscreen - select2: 'lib/select2.min', // v4.0.3 Drop Down customization - https://select2.github.io - validator: 'lib/validator.min', // v0.10.1 Validator for Bootstrap 3 - https://github.com/1000hz/bootstrap-validator - lazylinepainter: 'lib/jquery.lazylinepainter-1.5.1.min', // v1.5.1 SVG line animation plugin - http://lazylinepainter.info - blueImpGallery: 'lib/blueimp-gallery', // v2.21.3 Image Gallery - https://github.com/blueimp/Gallery - blueImpGalleryHelper: 'lib/blueimp-helper', // helper function for Blue Imp Gallery - blueImpGalleryBootstrap: 'lib/bootstrap-image-gallery', // v3.4.2 Bootstrap extension for Blue Imp Gallery - https://blueimp.github.io/Bootstrap-Image-Gallery - bootstrapConfirmation: 'lib/bootstrap-confirmation', // v1.0.5 Bootstrap extension for inline confirm dialog - https://github.com/tavicu/bs-confirmation - bootstrapToggle: 'lib/bootstrap-toggle.min', // v2.2.0 Bootstrap Toggle (Checkbox) - http://www.bootstraptoggle.com - lazyload: 'lib/jquery.lazyload.min', // v1.9.5 LazyLoader images - http://www.appelsiini.net/projects/lazyload - sortable: 'lib/sortable.min', // v1.6.0 Sortable - drag&drop reorder - https://github.com/rubaxa/Sortable + jquery: 'lib/jquery-3.3.1.min', // v3.3.1 jQuery + bootstrap: 'lib/bootstrap.min', // v3.3.0 Bootstrap js code - http://getbootstrap.com/javascript + text: 'lib/requirejs/text', // v2.0.12 A RequireJS/AMD loader plugin for loading text resources. + mustache: 'lib/mustache.min', // v1.0.0 Javascript template engine - http://mustache.github.io + localForage: 'lib/localforage.min', // v1.4.2 localStorage library - https://mozilla.github.io/localForage + velocity: 'lib/velocity.min', // v1.5.1 animation engine - http://julian.com/research/velocity + velocityUI: 'lib/velocity.ui.min', // v5.2.0 plugin for velocity - http://julian.com/research/velocity/#uiPack + slidebars: 'lib/slidebars', // v0.10 Slidebars - side menu plugin http://plugins.adchsm.me/slidebars + jsPlumb: 'lib/dom.jsPlumb-1.7.6', // v1.7.6 jsPlumb (Vanilla)- main map draw plugin https://jsplumbtoolkit.com + farahey: 'lib/farahey-0.5', // v0.5 jsPlumb "magnetizing" extension - https://github.com/jsplumb/farahey + customScrollbar: 'lib/jquery.mCustomScrollbar.min', // v3.1.5 Custom scroll bars - http://manos.malihu.gr + mousewheel: 'lib/jquery.mousewheel.min', // v3.1.13 Mousewheel - https://github.com/jquery/jquery-mousewheel + xEditable: 'lib/bootstrap-editable.min', // v1.5.1 X-editable - in placed editing + morris: 'lib/morris.min', // v0.5.1 Morris.js - graphs and charts + raphael: 'lib/raphael-min', // v2.1.2 Raphaël - required for morris (dependency) + bootbox: 'lib/bootbox.min', // v4.4.0 Bootbox.js - custom dialogs - http://bootboxjs.com + easyPieChart: 'lib/jquery.easypiechart.min', // v2.1.6 Easy Pie Chart - HTML 5 pie charts - http://rendro.github.io/easy-pie-chart + peityInlineChart: 'lib/jquery.peity.min', // v3.2.1 Inline Chart - http://benpickles.github.io/peity/ + dragToSelect: 'lib/jquery.dragToSelect', // v1.1 Drag to Select - http://andreaslagerkvist.com/jquery/drag-to-select + hoverIntent: 'lib/jquery.hoverIntent.min', // v1.9.0 Hover intention - http://cherne.net/brian/resources/jquery.hoverIntent.html + fullScreen: 'lib/jquery.fullscreen.min', // v0.6.0 Full screen mode - https://github.com/private-face/jquery.fullscreen + select2: 'lib/select2.min', // v4.0.3 Drop Down customization - https://select2.github.io + validator: 'lib/validator.min', // v0.10.1 Validator for Bootstrap 3 - https://github.com/1000hz/bootstrap-validator + lazylinepainter: 'lib/jquery.lazylinepainter-1.5.1.min', // v1.5.1 SVG line animation plugin - http://lazylinepainter.info + blueImpGallery: 'lib/blueimp-gallery', // v2.21.3 Image Gallery - https://github.com/blueimp/Gallery + blueImpGalleryHelper: 'lib/blueimp-helper', // helper function for Blue Imp Gallery + blueImpGalleryBootstrap: 'lib/bootstrap-image-gallery', // v3.4.2 Bootstrap extension for Blue Imp Gallery - https://blueimp.github.io/Bootstrap-Image-Gallery + bootstrapConfirmation: 'lib/bootstrap-confirmation', // v1.0.5 Bootstrap extension for inline confirm dialog - https://github.com/tavicu/bs-confirmation + bootstrapToggle: 'lib/bootstrap-toggle.min', // v2.2.0 Bootstrap Toggle (Checkbox) - http://www.bootstraptoggle.com + lazyload: 'lib/jquery.lazyload.min', // v1.9.5 LazyLoader images - http://www.appelsiini.net/projects/lazyload + sortable: 'lib/sortable.min', // v1.6.0 Sortable - drag&drop reorder - https://github.com/rubaxa/Sortable + + 'summernote.loader': './app/summernote.loader', // v0.8.10 Summernote WYSIWYG editor -https://summernote.org + 'summernote': 'lib/summernote/summernote.min', // header animation easePack: 'lib/EasePack.min', @@ -135,44 +138,47 @@ requirejs.config({ } }, pnotify: { - deps : ['jquery'] + deps: ['jquery'] }, easyPieChart: { - deps : ['jquery'] + deps: ['jquery'] }, peityInlineChart: { - deps : ['jquery'] + deps: ['jquery'] }, dragToSelect: { - deps : ['jquery'] + deps: ['jquery'] }, hoverIntent: { - deps : ['jquery'] + deps: ['jquery'] }, fullScreen: { - deps : ['jquery'] + deps: ['jquery'] }, select2: { - deps : ['jquery', 'mousewheel'], + deps: ['jquery', 'mousewheel'], exports: 'Select2' }, validator: { - deps : ['jquery', 'bootstrap'] + deps: ['jquery', 'bootstrap'] }, lazylinepainter: { - deps : ['jquery', 'bootstrap'] + deps: ['jquery', 'bootstrap'] }, blueImpGallery: { - deps : ['jquery'] + deps: ['jquery'] }, bootstrapConfirmation: { - deps : ['bootstrap'] + deps: ['bootstrap'] }, bootstrapToggle: { - deps : ['jquery'] + deps: ['jquery'] }, lazyload: { - deps : ['jquery'] + deps: ['jquery'] + }, + summernote: { + deps: ['jquery'] } } }); diff --git a/js/app/key.js b/js/app/key.js index 99523d1ef..bab7d1e89 100644 --- a/js/app/key.js +++ b/js/app/key.js @@ -275,7 +275,8 @@ define([ // exclude some HTML Tags from watcher if( e.target.tagName !== 'INPUT' && - e.target.tagName !== 'TEXTAREA' + e.target.tagName !== 'TEXTAREA' && + !e.target.classList.contains('note-editable') // Summerstyle editor ){ let key = e.key.toUpperCase(); map[key] = true; diff --git a/js/app/map/map.js b/js/app/map/map.js index 944bae5aa..8fa473438 100644 --- a/js/app/map/map.js +++ b/js/app/map/map.js @@ -54,11 +54,6 @@ define([ connectionContextMenuId: 'pf-map-connection-contextmenu', systemContextMenuId: 'pf-map-system-contextmenu', - // dialogs - systemDialogId: 'pf-system-dialog', // id for system dialog - systemDialogSelectClass: 'pf-system-dialog-select', // class for system select Element - systemDialogStatusSelectId: 'pf-system-dialog-status-select', // id for "status" select - // system security classes systemSec: 'pf-system-sec' }; @@ -1325,228 +1320,6 @@ define([ } }; - /** - * save a new system and add it to the map - * @param requestData - * @param context - */ - let saveSystem = (requestData, context) => { - $.ajax({ - type: 'POST', - url: Init.path.saveSystem, - data: requestData, - dataType: 'json', - context: context - }).done(function(responseData){ - let newSystemData = responseData.systemData; - - if( !$.isEmptyObject(newSystemData) ){ - Util.showNotify({title: 'New system', text: newSystemData.name, type: 'success'}); - - // draw new system to map - drawSystem(this.map, newSystemData, this.sourceSystem); - - // re/arrange systems (prevent overlapping) - MagnetizerWrapper.setElements(this.map); - - if(this.onSuccess){ - this.onSuccess(); - } - } - - // show errors - if( - responseData.error && - responseData.error.length > 0 - ){ - for(let i = 0; i < responseData.error.length; i++){ - let error = responseData.error[i]; - Util.showNotify({title: error.field + ' error', text: 'System: ' + error.message, type: error.type}); - } - } - }).fail(function(jqXHR, status, error){ - let reason = status + ' ' + error; - Util.showNotify({title: jqXHR.status + ': saveSystem', text: reason, type: 'warning'}); - $(document).setProgramStatus('problem'); - }).always(function(){ - if(this.onAlways){ - this.onAlways(this); - } - }); - }; - - /** - * open "new system" dialog and add the system to map - * optional the new system is connected to a "sourceSystem" (if available) - * - * @param map - * @param options - */ - let showNewSystemDialog = (map, options) => { - let mapContainer = $(map.getContainer()); - - // format system status for form select ----------------------------------------------------------------------- - // "default" selection (id = 0) prevents status from being overwritten - // -> e.g. keep status information if system was just inactive (active = 0) - let statusData = [{id: 0, text: 'auto'}]; - - // get current map data --------------------------------------------------------------------------------------- - let mapData = mapContainer.getMapDataFromClient({forceData: true}); - let mapSystems = mapData.data.systems; - let mapSystemCount = mapSystems.length; - let mapTypeName = mapContainer.data('typeName'); - let maxAllowedSystems = Init.mapTypes[mapTypeName].defaultConfig.max_systems; - - // show error if system max count reached --------------------------------------------------------------------- - if(mapSystemCount >= maxAllowedSystems){ - Util.showNotify({title: 'Max system count exceeded', text: 'Limit of ' + maxAllowedSystems + ' systems reached', type: 'warning'}); - return; - } - - // disable systems that are already on it --------------------------------------------------------------------- - let mapSystemIds = []; - for(let i = 0; i < mapSystems.length; i++ ){ - mapSystemIds.push( mapSystems[i].systemId ); - } - - // dialog data ------------------------------------------------------------------------------------------------ - let data = { - id: config.systemDialogId, - select2Class: Util.config.select2Class, - selectClass: config.systemDialogSelectClass, - statusSelectId: config.systemDialogStatusSelectId, - statusData: statusData - }; - - // set current position as "default" system to add ------------------------------------------------------------ - let currentCharacterLog = Util.getCurrentCharacterLog(); - - if( - currentCharacterLog !== false && - mapSystemIds.indexOf( currentCharacterLog.system.id ) === -1 - ){ - // current system is NOT already on this map - // set current position as "default" system to add - data.currentSystem = currentCharacterLog.system; - } - - requirejs(['text!templates/dialog/system.html', 'mustache'], function(template, Mustache){ - - let content = Mustache.render(template, data); - - let systemDialog = bootbox.dialog({ - title: 'Add new system', - message: content, - show: false, - buttons: { - close: { - label: 'cancel', - className: 'btn-default' - }, - success: { - label: ' save', - className: 'btn-success', - callback: function(e){ - // get form Values - let form = $('#' + config.systemDialogId).find('form'); - - let systemDialogData = $(form).getFormValues(); - - // validate form - form.validator('validate'); - - // check whether the form is valid - let formValid = form.isValidForm(); - - if(formValid === false){ - // don't close dialog - return false; - } - - // calculate new system position ---------------------------------------------------------- - let newPosition = { - x: 0, - y: 0 - }; - - let sourceSystem = null; - - // add new position - if(options.sourceSystem !== undefined){ - - sourceSystem = options.sourceSystem; - - // get new position - newPosition = System.calculateNewSystemPosition(sourceSystem); - - }else{ - // check mouse cursor position (add system to map) - newPosition = { - x: options.position.x, - y: options.position.y - }; - } - - systemDialogData.position = newPosition; - - // ---------------------------------------------------------------------------------------- - - let requestData = { - systemData: systemDialogData, - mapData: { - id: mapContainer.data('id') - } - }; - - this.find('.modal-content').showLoadingAnimation(); - - saveSystem(requestData, { - map: map, - sourceSystem: sourceSystem, - systemDialog: this, - onSuccess: () => { - bootbox.hideAll(); - }, - onAlways: (context) => { - context.systemDialog.find('.modal-content').hideLoadingAnimation(); - } - }); - return false; - } - } - } - }); - - systemDialog.on('show.bs.modal', function(e){ - let modalContent = $('#' + config.systemDialogId); - - // init "status" select2 - for(let [statusName, data] of Object.entries(Init.systemStatus)){ - statusData.push({id: data.id, text: data.label, class: data.class}); - } - - modalContent.find('#' + config.systemDialogStatusSelectId).initStatusSelect({ - data: statusData, - iconClass: 'fa-tag' - }); - }); - - systemDialog.on('shown.bs.modal', function(e){ - let modalContent = $('#' + config.systemDialogId); - - // init system select live search - some delay until modal transition has finished - let selectElement = modalContent.find('.' + config.systemDialogSelectClass); - selectElement.delay(240).initSystemSelect({ - key: 'id', - disabledOptions: mapSystemIds - }); - }); - - // show dialog - systemDialog.modal('show'); - }); - }; - /** * make a system name/alias editable by x-editable * @param system @@ -2057,7 +1830,7 @@ define([ switch(action){ case 'add_system': // add a new system - showNewSystemDialog(map, {sourceSystem: currentSystem} ); + System.showNewSystemDialog(map, {sourceSystem: currentSystem}, saveSystemCallback); break; case 'lock_system': @@ -2166,6 +1939,20 @@ define([ Util.singleDoubleClick(system, single, double); }; + /** + * callback after system save + * @param map + * @param newSystemData + * @param sourceSystem + */ + let saveSystemCallback = (map, newSystemData, sourceSystem) => { + // draw new system to map + drawSystem(map, newSystemData, sourceSystem); + + // re/arrange systems (prevent overlapping) + MagnetizerWrapper.setElements(map); + }; + /** * mark a dom element (map, system, connection) as changed */ @@ -2480,7 +2267,7 @@ define([ position.y = dimensions[0].top; } - showNewSystemDialog(currentMap, {position: position}); + System.showNewSystemDialog(currentMap, {position: position}, saveSystemCallback); break; case 'select_all': currentMapElement.selectAllSystems(); @@ -3022,7 +2809,7 @@ define([ let interval = mapElement.getMapOverlayInterval(); if( - ! interval || + !interval || options.forceData === true ){ @@ -3280,7 +3067,7 @@ define([ return { getMapInstance: getMapInstance, loadMap: loadMap, - showNewSystemDialog: showNewSystemDialog + saveSystemCallback: saveSystemCallback }; }); \ No newline at end of file diff --git a/js/app/map/scrollbar.js b/js/app/map/scrollbar.js index c660e8a98..a2c2d09d0 100644 --- a/js/app/map/scrollbar.js +++ b/js/app/map/scrollbar.js @@ -5,7 +5,7 @@ define([ 'jquery', 'app/init', 'app/util' -], function($, Init, Util){ +], ($, Init, Util) => { 'use strict'; /** diff --git a/js/app/map/system.js b/js/app/map/system.js index 6987f06ab..be248a877 100644 --- a/js/app/map/system.js +++ b/js/app/map/system.js @@ -29,8 +29,21 @@ define([ systemTooltipInnerIdPrefix: 'pf-system-tooltip-inner-', // id prefix for system tooltip content systemTooltipInnerClass: 'pf-system-tooltip-inner', // class for system tooltip content - dialogRallyId: 'pf-rally-dialog', // id for "Rally point" dialog + // dialogs + dialogSystemId: 'pf-system-dialog', // id for system dialog + dialogSystemSelectClass: 'pf-system-dialog-select', // class for system select element + dialogSystemStatusSelectId: 'pf-system-dialog-status-select', // id for "status" select + dialogSystemLockId: 'pf-system-dialog-lock', // id for "locked" checkbox + dialogSystemRallyId: 'pf-system-dialog-rally', // id for "rally" checkbox + + dialogSystemSectionInfoId: 'pf-system-dialog-section-info', // id for "info" section element + dialogSystemSectionInfoStatusId: 'pf-system-dialog-section-info-status', // id for "status" message in "info" element + dialogSystemAliasId: 'pf-system-dialog-alias', // id for "alias" static element + dialogSystemDescriptionId: 'pf-system-dialog-description', // id for "description" static element + dialogSystemCreatedId: 'pf-system-dialog-created', // id for "created" static element + dialogSystemUpdatedId: 'pf-system-dialog-updated', // id for "updated" static element + dialogRallyId: 'pf-rally-dialog', // id for "Rally point" dialog dialogRallyPokeDesktopId: 'pf-rally-dialog-poke-desktop', // id for "desktop" poke checkbox dialogRallyPokeSlackId: 'pf-rally-dialog-poke-slack', // id for "Slack" poke checkbox dialogRallyPokeDiscordId: 'pf-rally-dialog-poke-discord', // id for "Discord" poke checkbox @@ -43,6 +56,316 @@ define([ '- DPS and Logistic ships needed' }; + + /** + * save a new system and add it to the map + * @param requestData + * @param context + * @param callback + */ + let saveSystem = (requestData, context, callback) => { + $.ajax({ + type: 'POST', + url: Init.path.saveSystem, + data: requestData, + dataType: 'json', + context: context + }).done(function(responseData){ + let newSystemData = responseData.systemData; + + if( !$.isEmptyObject(newSystemData) ){ + Util.showNotify({title: 'New system', text: newSystemData.name, type: 'success'}); + + callback(newSystemData); + } + + if( + responseData.error && + responseData.error.length > 0 + ){ + for(let i = 0; i < responseData.error.length; i++){ + let error = responseData.error[i]; + Util.showNotify({title: error.field + ' error', text: 'System: ' + error.message, type: error.type}); + } + } + }).fail(function(jqXHR, status, error){ + let reason = status + ' ' + error; + Util.showNotify({title: jqXHR.status + ': saveSystem', text: reason, type: 'warning'}); + $(document).setProgramStatus('problem'); + }).always(function(){ + this.systemDialog.find('.modal-content').hideLoadingAnimation(); + }); + }; + + /** + * open "new system" dialog and add the system to map + * optional the new system is connected to a "sourceSystem" (if available) + * @param map + * @param options + * @param callback + */ + let showNewSystemDialog = (map, options, callback) => { + let mapContainer = $(map.getContainer()); + let mapId = mapContainer.data('id'); + + /** + * update new system dialog with some "additional" data + * -> if system was mapped before + * @param dialogElement + * @param systemData + */ + let updateDialog = (dialogElement, systemData = null) => { + let labelEmpty = 'empty'; + let labelUnknown = 'unknown'; + let labelExist = 'loaded'; + + let showInfoHeadline = 'fadeOut'; + let showInfoSection = 'hide'; + let info = labelEmpty; + + let statusId = false; // -> no value change + let alias = labelEmpty; + let description = labelEmpty; + let createdTime = labelUnknown; + let updatedTime = labelUnknown; + + if(systemData){ + // system data found for selected system + showInfoHeadline = 'fadeIn'; + showInfoSection = 'show'; + info = labelExist; + statusId = parseInt(Util.getObjVal(systemData, 'status.id')) || statusId; + alias = systemData.alias.length ? Util.htmlEncode(systemData.alias) : alias; + description = systemData.description.length ? systemData.description : description; + + let dateCreated = new Date(systemData.created.created * 1000); + let dateUpdated = new Date(systemData.updated.updated * 1000); + let dateCreatedUTC = Util.convertDateToUTC(dateCreated); + let dateUpdatedUTC = Util.convertDateToUTC(dateUpdated); + + createdTime = Util.convertDateToString(dateCreatedUTC); + updatedTime = Util.convertDateToString(dateUpdatedUTC); + + }else if(systemData === null){ + // no system found for selected system + showInfoHeadline = 'fadeIn'; + } + + // update new system dialog with new default data + dialogElement.find('#' + config.dialogSystemSectionInfoStatusId).html(info); + if(statusId !== false){ + dialogElement.find('#' + config.dialogSystemStatusSelectId).val(statusId).trigger('change'); + } + dialogElement.find('#' + config.dialogSystemAliasId).html(alias); + dialogElement.find('#' + config.dialogSystemDescriptionId).html(description); + dialogElement.find('#' + config.dialogSystemCreatedId).html(' ' + createdTime); + dialogElement.find('#' + config.dialogSystemUpdatedId).html(' ' + updatedTime); + dialogElement.find('[data-target="#' + config.dialogSystemSectionInfoId + '"]').velocity('stop').velocity(showInfoHeadline, {duration: 120}); + dialogElement.find('[data-type="spinner"]').removeClass('in'); + dialogElement.find('#' + config.dialogSystemSectionInfoId).collapse(showInfoSection); + }; + + /** + * request system data from server for persistent data -> update dialog + * @param dialogElement + * @param mapId + * @param systemId + */ + let requestSystemData = (dialogElement, mapId, systemId) => { + // show loading animation + dialogElement.find('[data-type="spinner"]').addClass('in'); + + MapUtil.requestSystemData({ + mapId: mapId, + systemId: systemId, + isCcpId: 1 + }, { + dialogElement: dialogElement + }).then(payload => updateDialog(payload.context.dialogElement, payload.data)) + .catch(payload => updateDialog(payload.context.dialogElement)); + }; + + // format system status for form select ----------------------------------------------------------------------- + // "default" selection (id = 0) prevents status from being overwritten + // -> e.g. keep status information if system was just inactive (active = 0) + let statusData = [{id: 0, text: 'auto'}]; + + // get current map data --------------------------------------------------------------------------------------- + let mapData = mapContainer.getMapDataFromClient({forceData: true}); + let mapSystems = mapData.data.systems; + let mapSystemCount = mapSystems.length; + let mapTypeName = mapContainer.data('typeName'); + let maxAllowedSystems = Init.mapTypes[mapTypeName].defaultConfig.max_systems; + + // show error if system max count reached --------------------------------------------------------------------- + if(mapSystemCount >= maxAllowedSystems){ + Util.showNotify({title: 'Max system count exceeded', text: 'Limit of ' + maxAllowedSystems + ' systems reached', type: 'warning'}); + return; + } + + // disable systems that are already on it --------------------------------------------------------------------- + let mapSystemIds = mapSystems.map(systemData => systemData.systemId); + + // dialog data ------------------------------------------------------------------------------------------------ + let data = { + id: config.dialogSystemId, + select2Class: Util.config.select2Class, + systemSelectClass: config.dialogSystemSelectClass, + statusSelectId: config.dialogSystemStatusSelectId, + lockId: config.dialogSystemLockId, + rallyId: config.dialogSystemRallyId, + + sectionInfoId: config.dialogSystemSectionInfoId, + sectionInfoStatusId: config.dialogSystemSectionInfoStatusId, + aliasId: config.dialogSystemAliasId, + descriptionId: config.dialogSystemDescriptionId, + createdId: config.dialogSystemCreatedId, + updatedId: config.dialogSystemUpdatedId, + statusData: statusData + }; + + // set current position as "default" system to add ------------------------------------------------------------ + let currentCharacterLog = Util.getCurrentCharacterLog(); + + if( + currentCharacterLog !== false && + mapSystemIds.indexOf( currentCharacterLog.system.id ) === -1 + ){ + // current system is NOT already on this map + // set current position as "default" system to add + data.currentSystem = currentCharacterLog.system; + } + + requirejs(['text!templates/dialog/system.html', 'mustache'], (template, Mustache) => { + + let content = Mustache.render(template, data); + + let systemDialog = bootbox.dialog({ + title: 'Add new system', + message: content, + show: false, + buttons: { + close: { + label: 'cancel', + className: 'btn-default' + }, + success: { + label: ' save', + className: 'btn-success', + callback: function(e){ + // get form Values + let form = this.find('form'); + + let systemDialogData = $(form).getFormValues(); + + // validate form + form.validator('validate'); + + // check whether the form is valid + let formValid = form.isValidForm(); + + // don't close dialog on invalid data + if(formValid === false) return false; + + // calculate new system position ---------------------------------------------------------- + let newPosition = { + x: 0, + y: 0 + }; + + // add new position + let sourceSystem = null; + if(options.sourceSystem !== undefined){ + sourceSystem = options.sourceSystem; + + // get new position + newPosition = calculateNewSystemPosition(sourceSystem); + }else{ + // check mouse cursor position (add system to map) + newPosition = { + x: options.position.x, + y: options.position.y + }; + } + + systemDialogData.position = newPosition; + + // ---------------------------------------------------------------------------------------- + + let requestData = { + systemData: systemDialogData, + mapData: { + id: mapId + } + }; + + this.find('.modal-content').showLoadingAnimation(); + + saveSystem(requestData, { + systemDialog: this + }, (newSystemData) => { + // success callback + callback(map, newSystemData, sourceSystem); + + bootbox.hideAll(); + }); + return false; + } + } + } + }); + + systemDialog.on('show.bs.modal', function(e){ + let dialogElement = $(this); + + // init "status" select2 ------------------------------------------------------------------------------ + for(let [statusName, data] of Object.entries(Init.systemStatus)){ + statusData.push({id: data.id, text: data.label, class: data.class}); + } + + dialogElement.find('#' + config.dialogSystemStatusSelectId).initStatusSelect({ + data: statusData, + iconClass: 'fa-tag' + }); + + // initial dialog update with persistent system data -------------------------------------------------- + // -> only if system is preselected (e.g. current active system) + let systemId = parseInt(dialogElement.find('.' + config.dialogSystemSelectClass).val()) || 0; + if(systemId){ + requestSystemData(dialogElement, mapId, systemId); + } + }); + + systemDialog.on('shown.bs.modal', function(e){ + let dialogElement = $(this); + + // no system selected + updateDialog(dialogElement, false); + + dialogElement.initTooltips(); + + // init system select live search - some delay until modal transition has finished + let selectElement = dialogElement.find('.' + config.dialogSystemSelectClass); + selectElement.delay(240).initSystemSelect({ + key: 'id', + disabledOptions: mapSystemIds, + onChange: systemId => { + // on system select -> update dialog with persistent system data + if(systemId){ + requestSystemData(dialogElement, mapId, systemId); + }else{ + // no system selected + updateDialog(dialogElement, false); + } + } + }); + }); + + // show dialog + systemDialog.modal('show'); + }); + }; + /** * show "set rally point" dialog for system * @param system @@ -535,9 +858,9 @@ define([ }; return { + showNewSystemDialog: showNewSystemDialog, deleteSystems: deleteSystems, removeSystems: removeSystems, - calculateNewSystemPosition: calculateNewSystemPosition, getHeadInfoElement: getHeadInfoElement }; }); \ No newline at end of file diff --git a/js/app/map/util.js b/js/app/map/util.js index 297c63439..9db974d72 100644 --- a/js/app/map/util.js +++ b/js/app/map/util.js @@ -1314,10 +1314,11 @@ define([ // dynamic require Map module -> otherwise there is a require(), loop let Map = require('app/map/map'); + let System = require('app/map/system'); let map = Map.getMapInstance( mapElement.data('id')); mapWrapper.watchKey('mapSystemAdd', (mapWrapper) => { - Map.showNewSystemDialog(map, {position: {x: 0, y: 0}}); + System.showNewSystemDialog(map, {position: {x: 0, y: 0}}, Map.saveSystemCallback); },{focus: true}); mapWrapper.watchKey('mapSystemsSelect', (mapWrapper) => { @@ -1617,6 +1618,10 @@ define([ let requestSystemData = (requestData, context) => { let requestSystemDataExecutor = (resolve, reject) => { + let payload = { + action: 'systemData' + }; + $.ajax({ url: Init.path.getSystemData, type: 'POST', @@ -1624,17 +1629,20 @@ define([ data: requestData, context: context }).done(function(data){ + payload.context = this; + if(data.system){ - resolve({ - action: 'systemData', - context: this, - data: data.system - }); + // system data found + payload.data = data.system; + resolve(payload); }else{ - console.warn('Missing systemData in response!', requestData); + // no system data returned/found + reject(payload); } }).fail(function(jqXHR, status, error){ console.warn('Fail request systemData!', requestData); + payload.context = this; + reject(payload); }); }; diff --git a/js/app/mappage.js b/js/app/mappage.js index f006d4519..3ddef1fb2 100644 --- a/js/app/mappage.js +++ b/js/app/mappage.js @@ -136,7 +136,6 @@ define([ Init.structureStatus = response.structureStatus; Init.universeCategories = response.universeCategories; Init.routeSearch = response.routeSearch; - Init.programMode = response.programMode; resolve({ action: 'initData', diff --git a/js/app/summernote.loader.js b/js/app/summernote.loader.js new file mode 100644 index 000000000..636417af0 --- /dev/null +++ b/js/app/summernote.loader.js @@ -0,0 +1,187 @@ +define([ + 'jquery', + 'app/init', + 'summernote' +], ($, Init) => { + 'use strict'; + + // all Summernote stuff is available... + let initDefaultSummernoteConfig = () => { + // "length" hint plugin --------------------------------------------------------------------------------------- + $.extend($.summernote.plugins, { + /** + * @param {Object} context - context object has status of editor. + */ + lengthField: function (context){ + let self = this; + let ui = $.summernote.ui; + + // add counter + context.memo('button.lengthField', () => { + return $('', { + class: ['text-right', 'txt-color'].join(' ') + }); + }); + + /** + * update counter element with left chars + * @param contents + */ + let updateCounter = (contents) => { + let maxTextLength = context.options.maxTextLength; + let textLength = contents.length; + let counter = context.layoutInfo.toolbar.find('kbd'); + let counterLeft = maxTextLength - textLength; + + counter.text(counterLeft).data('charCount', counterLeft); + counter.toggleClass('txt-color-red', maxTextLength <= textLength); + + // disable "save" button + let saveBtn = context.layoutInfo.toolbar.find('.btn-save'); + saveBtn.prop('disabled', maxTextLength < textLength); + }; + + // events + this.events = { + 'summernote.init': function (we, e) { + updateCounter(context.$note.summernote('code')); + }, + 'summernote.change': function(we, contents){ + updateCounter(contents); + + } + }; + } + }); + + // "discard" button plugin ------------------------------------------------------------------------------------ + $.extend($.summernote.plugins, { + /** + * @param {Object} context - context object has status of editor. + */ + discardBtn: function (context){ + let self = this; + let ui = $.summernote.ui; + + // add button + context.memo('button.discardBtn', () => { + let button = ui.button({ + contents: '', + container: 'body', + click: function(){ + // show confirmation dialog + $(this).confirmation('show'); + } + }); + let $button = button.render(); + + // show "discard" changes confirmation + let confirmationSettings = { + container: 'body', + placement: 'top', + btnCancelClass: 'btn btn-sm btn-default', + btnCancelLabel: 'cancel', + title: 'discard changes', + btnOkClass: 'btn btn-sm btn-warning', + btnOkLabel: 'discard', + btnOkIcon: 'fas fa-fw fa-ban', + onConfirm: (e, target) => { + // discard all changes + context.$note.summernote('reset'); + context.$note.summernote('destroy'); + } + }; + $button.confirmation(confirmationSettings); + + return $button; + }); + } + }); + }; + + /** + * init new Summernote editor + * @param element + * @param options + */ + let initSummernote = (element, options) => { + + let defaultOptions = { + dialogsInBody: true, + dialogsFade: true, + //textareaAutoSync: false, + //hintDirection: 'right', + //tooltip: 'right', + //container: 'body', + styleTags: ['p', 'h2', 'h3', 'blockquote'], + linkTargetBlank: true, + tableClassName: 'table table-condensed table-bordered', + insertTableMaxSize: { + col: 5, + row: 5 + }, + icons: { + //'align': 'note-icon-align', + 'alignCenter': 'fas fa-align-center', + 'alignJustify': 'fas fa-align-justify', + 'alignLeft': 'fas fa-align-left', + 'alignRight': 'fas fa-align-right', + //'rowBelow': 'note-icon-row-below', + //'colBefore': 'note-icon-col-before', + //'colAfter': 'note-icon-col-after', + //'rowAbove': 'note-icon-row-above', + //'rowRemove': 'note-icon-row-remove', + //'colRemove': 'note-icon-col-remove', + 'indent': 'fas fa-indent', + 'outdent': 'fas fa-outdent', + 'arrowsAlt': 'fas fa-expand-arrows-alt', + 'bold': 'fas fa-bold', + 'caret': 'fas fa-caret-down', + 'circle': 'fas fa-circle', + 'close': 'fas fa-time', + 'code': 'fas fa-code', + 'eraser': 'fas fa-eraser', + 'font': 'fas fa-font', + //'frame': 'note-icon-frame', + 'italic': 'fas fa-italic', + 'link': 'fas fa-link', + 'unlink': 'fas fa-unlink', + 'magic': 'fas fa-magic', + 'menuCheck': 'fas fa-check', + 'minus': 'fas fa-minus', + 'orderedlist': 'fas fa-list-ol', + 'pencil': 'fa-pen', + 'picture': 'fas fa-image', + 'question': 'fas fa-question', + 'redo': 'fas fa-redo', + 'square': 'fas fa-square', + 'strikethrough': 'fas fa-strikethrough', + 'subscript': 'fas fa-subscript', + 'superscript': 'fas fa-superscript', + 'table': 'fas fa-table', + 'textHeight': 'fas fa-text-height', + 'trash': 'fas fa-trash', + 'underline': 'fas fa-underline', + 'undo': 'fas fa-undo', + 'unorderedlist': 'fas fa-list-ul', + 'video': 'fab fa-youtube' + }, + colors: [ + ['#5cb85c', '#e28a0d', '#d9534f', '#e06fdf', '#9fa8da', '#e2ce48', '#428bca'] + ], + colorsName: [ + ['Green', 'Orange', 'Red', 'Pink', 'Indigo', 'Yellow', 'Blue'] + ], + }; + + options = $.extend({}, defaultOptions, options); + + element.summernote(options); + }; + + initDefaultSummernoteConfig(); + + return { + initSummernote: initSummernote + }; +}); \ No newline at end of file diff --git a/js/app/ui/dialog/map_info.js b/js/app/ui/dialog/map_info.js index 8d9c90cb2..2a5b07eb6 100644 --- a/js/app/ui/dialog/map_info.js +++ b/js/app/ui/dialog/map_info.js @@ -1254,7 +1254,7 @@ define([ */ $.fn.showMapInfoDialog = function(options){ let activeMap = Util.getMapModule().getActiveMap(); - let mapData = activeMap.getMapDataFromClient({forceData: true}); + let mapData = activeMap ? activeMap.getMapDataFromClient({forceData: true}) : false; if(mapData !== false){ // "log" tab -> get "Origin", not all config options are set in mapData @@ -1351,6 +1351,13 @@ define([ }); }); + }else{ + // no active map found (e.g. not loaded yet, or no map exists) + Util.showNotify({ + title: 'Map data not found', + text: 'No map initialized at this point', + type: 'warning'} + ); } }; diff --git a/js/app/ui/form_element.js b/js/app/ui/form_element.js index 44489a890..327433b89 100644 --- a/js/app/ui/form_element.js +++ b/js/app/ui/form_element.js @@ -333,11 +333,14 @@ define([ dropdownParent: selectElement.parents('.modal-body'), minimumInputLength: 3, templateResult: formatResultData, - placeholder: 'System name', + placeholder: 'Name or ID', allowClear: true, maximumSelectionLength: options.maxSelectionLength }).on('change', function(e){ // select changed + if(options.onChange){ + options.onChange(parseInt($(this).val()) || 0); + } }).on('select2:open', function(){ // clear selected system (e.g. default system) // => improves usability (not necessary). There is a small "x" if field can be cleared manually diff --git a/js/app/ui/module/connection_info.js b/js/app/ui/module/connection_info.js index ecf2f5ffc..b9e4a6a76 100644 --- a/js/app/ui/module/connection_info.js +++ b/js/app/ui/module/connection_info.js @@ -30,6 +30,9 @@ define([ connectionInfoPanelClass: 'pf-connection-info-panel', // class for connection info panels connectionInfoPanelId: 'pf-connection-info-panel-', // id prefix for connection info panels + dynamicAreaClass: 'pf-dynamic-area', // class for "dynamic" areas + controlAreaClass: 'pf-module-control-area', // class for "control" areas + // info table moduleTableClass: 'pf-module-table', // class for module tables connectionInfoTableLabelSourceClass: 'pf-connection-info-label-source', // class for source label @@ -107,7 +110,7 @@ define([ */ let getInfoPanelControl = (mapId) => { let connectionElement = getConnectionElement(mapId, 0).append($('
', { - class: 'pf-dynamic-area', + class: [config.dynamicAreaClass, config.controlAreaClass].join(' '), html: ' add connection  ctrl + click' })); @@ -125,7 +128,7 @@ define([ let scopeLabel = MapUtil.getScopeInfoForConnection(connectionData.scope, 'label'); let element = $('
', { - class: 'pf-dynamic-area' + class: [config.dynamicAreaClass, config.controlAreaClass].join(' ') }).append( $('', { class: ['table', 'table-condensed', 'pf-table-fixed', config.moduleTableClass].join(' ') diff --git a/js/app/ui/module/system_info.js b/js/app/ui/module/system_info.js index 0630e1595..81562a369 100644 --- a/js/app/ui/module/system_info.js +++ b/js/app/ui/module/system_info.js @@ -6,9 +6,8 @@ define([ 'jquery', 'app/init', 'app/util', - 'app/render', 'app/map/util' -], ($, Init, Util, Render, MapUtil) => { +], ($, Init, Util, MapUtil) => { 'use strict'; let config = { @@ -38,50 +37,55 @@ define([ systemInfoWormholeClass: 'pf-system-info-wormhole-', // class prefix for static wormhole element // description field - descriptionArea: 'pf-system-info-description-area', // class for "description" area + descriptionAreaClass: 'pf-system-info-description-area', // class for "description" area addDescriptionButtonClass: 'pf-system-info-description-button', // class for "add description" button - moduleElementToolbarClass: 'pf-table-tools', // class for "module toolbar" element - tableToolsActionClass: 'pf-table-tools-action', // class for "edit" action - descriptionTextareaElementClass: 'pf-system-info-description', // class for "description" textarea element (xEditable) - descriptionTextareaCharCounter: 'pf-form-field-char-count', // class for "character counter" element for form field // fonts - fontTriglivianClass: 'pf-triglivian' // class for "Triglivian" names (e.g. Abyssal systems) - }; - - // disable Module update temporary (in case e.g. textarea is currently active) - let disableModuleUpdate = false; + fontTriglivianClass: 'pf-triglivian', // class for "Triglivian" names (e.g. Abyssal systems) - // animation speed values - let animationSpeedToolbarAction = 200; + // Summernote + defaultBgColor: '#e2ce48' + }; // max character length for system description - let maxDescriptionLength = 512; + let maxDescriptionLength = 9000; /** - * shows the tool action element by animation - * @param toolsActionElement + * save system (description) + * @param requestData + * @param context + * @param callback */ - let showToolsActionElement = (toolsActionElement) => { - toolsActionElement.velocity('stop').velocity({ - opacity: 1, - height: '100%' - },{ - duration: animationSpeedToolbarAction, - display: 'block', - visibility: 'visible' - }); - }; + let saveSystem = (requestData, context, callback) => { + context.descriptionArea.showLoadingAnimation(); + + $.ajax({ + type: 'POST', + url: Init.path.saveSystem, + data: requestData, + dataType: 'json', + context: context + }).done(function(responseData){ + let newSystemData = responseData.systemData; + + if( !$.isEmptyObject(newSystemData) ){ + callback(newSystemData); + } - /** - * hides the tool action element by animation - * @param toolsActionElement - */ - let hideToolsActionElement = (toolsActionElement) => { - toolsActionElement.velocity('stop').velocity('reverse', { - display: 'none', - visibility: 'hidden' + if( + responseData.error && + responseData.error.length > 0 + ){ + for(let error of responseData.error){ + Util.showNotify({title: error.field + ' error', text: 'System: ' + error.message, type: error.type}); + } + } + }).fail(function(jqXHR, status, error){ + let reason = status + ' ' + error; + Util.showNotify({title: jqXHR.status + ': saveSystem', text: reason, type: 'warning'}); + }).always(function(){ + this.descriptionArea.hideLoadingAnimation(); }); }; @@ -93,8 +97,24 @@ define([ */ let updateModule = (moduleElement, systemData) => { let systemId = moduleElement.data('id'); + let updated = moduleElement.data('updated'); + + if( + systemId === systemData.id && + updated !== systemData.updated.updated + ){ + let setUpdated = true; + + // created/updated tooltip -------------------------------------------------------------------------------- + let nameRowElement = moduleElement.find('.' + config.systemInfoNameClass); + + let tooltipData = { + created: systemData.created, + updated: systemData.updated + }; + + nameRowElement.addCharacterInfoTooltip( tooltipData ); - if(systemId === systemData.id){ // update system status ----------------------------------------------------------------------------------- let systemStatusLabelElement = moduleElement.find('.' + config.systemInfoStatusLabelClass); let systemStatusId = parseInt( systemStatusLabelElement.attr( config.systemInfoStatusAttributeName ) ); @@ -112,45 +132,33 @@ define([ // update description textarea ---------------------------------------------------------------------------- let descriptionTextareaElement = moduleElement.find('.' + config.descriptionTextareaElementClass); - let description = descriptionTextareaElement.editable('getValue', true); + 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 + '

'; + } - if( - !disableModuleUpdate && // don´t update if field is active - description !== systemData.description - ){ - // description changed - let descriptionButton = moduleElement.find('.' + config.addDescriptionButtonClass); - - // set new value - descriptionTextareaElement.editable('setValue', systemData.description); - - let actionElement = descriptionButton.siblings('.' + config.tableToolsActionClass); - - if(systemData.description.length === 0){ - // show/activate description field - // show button if value is empty - descriptionButton.show(); - hideToolsActionElement(actionElement); - }else{ - // hide/disable description field - // hide tool button - descriptionButton.hide(); - showToolsActionElement(actionElement); + descriptionTextareaElement.html(newDescription); + } } } - // created/updated tooltip -------------------------------------------------------------------------------- - let nameRowElement = moduleElement.find('.' + config.systemInfoNameClass); - - let tooltipData = { - created: systemData.created, - updated: systemData.updated - }; - - nameRowElement.addCharacterInfoTooltip( tooltipData ); + if(setUpdated){ + moduleElement.data('updated', systemData.updated.updated); + } } - moduleElement.find('.' + config.descriptionArea).hideLoadingAnimation(); + moduleElement.find('.' + config.descriptionAreaClass).hideLoadingAnimation(); }; /** @@ -181,188 +189,7 @@ define([ let effectName = MapUtil.getEffectInfoForSystem(systemData.effect, 'name'); let effectClass = MapUtil.getEffectInfoForSystem(systemData.effect, 'class'); - // systemInfo template config - let moduleConfig = { - name: 'modules/system_info', - position: moduleElement, - link: 'append', - functions: { - after: function(conf){ - let tempModuleElement = conf.position; - // lock "description" field until first update - tempModuleElement.find('.' + config.descriptionArea).showLoadingAnimation(); - // "add description" button - let descriptionButton = tempModuleElement.find('.' + config.addDescriptionButtonClass); - // description textarea element - let descriptionTextareaElement = tempModuleElement.find('.' + config.descriptionTextareaElementClass); - - // init description textarea - descriptionTextareaElement.editable({ - url: Init.path.saveSystem, - dataType: 'json', - pk: systemData.id, - type: 'textarea', - mode: 'inline', - emptytext: '', - onblur: 'cancel', - showbuttons: true, - value: '', // value is set by trigger function updateModule() - rows: 5, - name: 'description', - inputclass: config.descriptionTextareaElementClass, - tpl: '', - params: function(params){ - params.mapData = { - id: mapId - }; - - params.systemData = {}; - params.systemData.id = params.pk; - params.systemData[params.name] = params.value; - - // clear unnecessary data - delete params.pk; - delete params.name; - delete params.value; - - return params; - }, - validate: function(value){ - if(value.length > 0 && $.trim(value).length === 0){ - return {newValue: ''}; - } - }, - success: function(response, newValue){ - Util.showNotify({title: 'System updated', text: 'Name: ' + response.name, type: 'success'}); - }, - error: function(jqXHR, newValue){ - let reason = ''; - let status = ''; - if(jqXHR.name){ - // save error new sig (mass save) - reason = jqXHR.name; - status = 'Error'; - }else{ - reason = jqXHR.responseJSON.text; - status = jqXHR.status; - } - - Util.showNotify({title: status + ': save system information', text: reason, type: 'warning'}); - $(document).setProgramStatus('problem'); - return reason; - } - }); - - // on xEditable open ------------------------------------------------------------------------------ - descriptionTextareaElement.on('shown', function(e, editable){ - let textarea = editable.input.$input; - - // disable module update until description field is open - disableModuleUpdate = true; - - // create character counter - let charCounter = $('', { - class: [config.descriptionTextareaCharCounter, 'txt-color', 'text-right'].join(' ') - }); - textarea.parent().next().append(charCounter); - - // update character counter - Util.updateCounter(textarea, charCounter, maxDescriptionLength); - - textarea.on('keyup', function(){ - Util.updateCounter($(this), charCounter, maxDescriptionLength); - }); - }); - - // on xEditable close ----------------------------------------------------------------------------- - descriptionTextareaElement.on('hidden', function(e){ - let value = $(this).editable('getValue', true); - if(value.length === 0){ - // show button if value is empty - hideToolsActionElement(descriptionButton.siblings('.' + config.tableToolsActionClass)); - descriptionButton.show(); - } - - // enable module update - disableModuleUpdate = false; - }); - - // enable xEditable field on Button click --------------------------------------------------------- - descriptionButton.on('click', function(e){ - e.stopPropagation(); - let descriptionButton = $(this); - - // hide tool buttons - descriptionButton.hide(); - - // show field *before* showing the element - descriptionTextareaElement.editable('show'); - - showToolsActionElement(descriptionButton.siblings('.' + config.tableToolsActionClass)); - }); - - // init tooltips ---------------------------------------------------------------------------------- - let tooltipElements = tempModuleElement.find('[data-toggle="tooltip"]'); - tooltipElements.tooltip({ - container: 'body', - placement: 'top' - }); - - // init system effect popover --------------------------------------------------------------------- - tempModuleElement.find('.' + config.systemInfoEffectClass).addSystemEffectTooltip(systemData.security, systemData.effect); - - // init planets popover --------------------------------------------------------------------------- - tempModuleElement.find('.' + config.systemInfoPlanetsClass).addSystemPlanetsTooltip(systemData.planets); - - // init static wormhole information --------------------------------------------------------------- - for(let staticData of staticsData){ - let staticRowElement = tempModuleElement.find('.' + config.systemInfoWormholeClass + staticData.name); - staticRowElement.addWormholeInfoTooltip(staticData); - } - - // copy system deeplink URL ----------------------------------------------------------------------- - tempModuleElement.find('.' + config.urlLinkClass).on('click', function(){ - let mapUrl = $(this).attr('data-url'); - Util.copyToClipboard(mapUrl).then(payload => { - if(payload.data){ - Util.showNotify({title: 'Copied to clipbaord', text: mapUrl, type: 'success'}); - } - }); - }); - - // constellation popover -------------------------------------------------------------------------- - tempModuleElement.find('a.popup-ajax').popover({ - html: true, - trigger: 'hover', - placement: 'top', - delay: 200, - container: 'body', - content: function(){ - return details_in_popup(this); - } - }); - - function details_in_popup(popoverElement){ - popoverElement = $(popoverElement); - let popover = popoverElement.data('bs.popover'); - - $.ajax({ - url: popoverElement.data('url'), - success: function(data){ - let systemEffectTable = Util.getSystemsInfoTable( data.systemsData ); - popover.options.content = systemEffectTable; - // reopen popover (new content size) - popover.show(); - } - }); - return 'Loading...'; - } - - } - } - }; - - let moduleData = { + let data = { system: systemData, static: staticsData, moduleHeadlineIconClass: config.moduleHeadlineIconClass, @@ -385,9 +212,8 @@ define([ trueSecClass: Util.getTrueSecClassForSystem( systemData.trueSec ), effectName: effectName, effectClass: effectClass, - moduleToolbarClass: config.moduleElementToolbarClass, + descriptionAreaClass: config.descriptionAreaClass, descriptionButtonClass: config.addDescriptionButtonClass, - tableToolsActionClass: config.tableToolsActionClass, descriptionTextareaClass: config.descriptionTextareaElementClass, systemNameClass: () => { return (val, render) => { @@ -409,7 +235,189 @@ define([ systemUrlLinkClass: config.urlLinkClass }; - Render.showModule(moduleConfig, moduleData); + requirejs(['text!templates/modules/system_info.html', 'mustache', 'summernote.loader'], (template, Mustache, Summernote) => { + let content = Mustache.render(template, data); + moduleElement.append(content); + + let descriptionArea = moduleElement.find('.' + config.descriptionAreaClass); + let descriptionButton = moduleElement.find('.' + config.addDescriptionButtonClass); + let descriptionTextareaElement = moduleElement.find('.' + config.descriptionTextareaElementClass); + + // lock "description" field 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; + + 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() + saveSystem({ + mapData: { + id: mapId + }, + systemData: { + id: systemData.id, + description: description + } + }, { + descriptionArea: descriptionArea + }, (systemData) => { + // .. save callback + context.$note.summernote('destroy'); + updateModule(moduleElement, systemData); + }); + } + }else{ + // ... no changes -> no save() + context.$note.summernote('destroy'); + } + } + }); + + return button.render(); + } + }, + callbacks: { + onInit: function(context){ + // make editable field a big larger + context.editable.css('height', '150px'); + + // set default background color + // -> could not figure out how to set by API as default color + context.toolbar.find('.note-current-color-button').attr('data-backcolor', config.defaultBgColor) + .find('.note-recent-color').css('background-color', config.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(); + } + } + }); + }); + + // init system effect popover ----------------------------------------------------------------------------- + moduleElement.find('.' + config.systemInfoEffectClass).addSystemEffectTooltip(systemData.security, systemData.effect); + + // init planets popover ----------------------------------------------------------------------------------- + moduleElement.find('.' + config.systemInfoPlanetsClass).addSystemPlanetsTooltip(systemData.planets); + + // init static wormhole information ----------------------------------------------------------------------- + for(let staticData of staticsData){ + let staticRowElement = moduleElement.find('.' + config.systemInfoWormholeClass + staticData.name); + staticRowElement.addWormholeInfoTooltip(staticData); + } + + // copy system deeplink URL ------------------------------------------------------------------------------- + moduleElement.find('.' + config.urlLinkClass).on('click', function(){ + let mapUrl = $(this).attr('data-url'); + Util.copyToClipboard(mapUrl).then(payload => { + if(payload.data){ + Util.showNotify({title: 'Copied to clipbaord', text: mapUrl, type: 'success'}); + } + }); + }); + + // constellation popover ---------------------------------------------------------------------------------- + moduleElement.find('a.popup-ajax').popover({ + html: true, + trigger: 'hover', + placement: 'top', + delay: 200, + container: 'body', + content: function(){ + return details_in_popup(this); + } + }); + + let details_in_popup = popoverElement => { + popoverElement = $(popoverElement); + let popover = popoverElement.data('bs.popover'); + + $.ajax({ + url: popoverElement.data('url'), + success: function(data){ + popover.options.content = Util.getSystemsInfoTable(data.systemsData); + // reopen popover (new content size) + popover.show(); + } + }); + return 'Loading...'; + }; + + // init tooltips ------------------------------------------------------------------------------------------ + let tooltipElements = moduleElement.find('[data-toggle="tooltip"]'); + tooltipElements.tooltip({ + container: 'body', + placement: 'top' + }); + }); return moduleElement; }; @@ -418,10 +426,8 @@ define([ * efore module destroy callback * @param moduleElement */ - let beforeDestroy = (moduleElement) => { - // remove xEditable description textarea - let descriptionTextareaElement = moduleElement.find('.' + config.descriptionTextareaElementClass); - descriptionTextareaElement.editable('destroy'); + let beforeDestroy = moduleElement => { + moduleElement.find('.' + config.descriptionTextareaElementClass).summernote('destroy'); moduleElement.destroyPopover(true); }; @@ -433,6 +439,3 @@ define([ beforeDestroy: beforeDestroy }; }); - - - diff --git a/js/app/ui/module/system_killboard.js b/js/app/ui/module/system_killboard.js index e0a61bd93..eb568d7f9 100644 --- a/js/app/ui/module/system_killboard.js +++ b/js/app/ui/module/system_killboard.js @@ -22,7 +22,6 @@ define([ // system killboard module moduleTypeClass: 'pf-system-killboard-module', // class for this module - systemKillboardGraphKillsClass: 'pf-system-killboard-graph-kills', // class for system kill graph // system killboard list systemKillboardListClass: 'pf-system-killboard-list', // class for a list with kill entries @@ -30,94 +29,162 @@ define([ systemKillboardListImgShip: 'pf-system-killboard-img-ship', // class for all ship images systemKillboardListImgChar: 'pf-system-killboard-img-char', // class for all character logos systemKillboardListImgAlly: 'pf-system-killboard-img-ally', // class for all alliance logos - systemKillboardListImgCorp: 'pf-system-killboard-img-corp' // class for all corp logos - }; + systemKillboardListImgCorp: 'pf-system-killboard-img-corp', // class for all corp logos + + labelRecentKillsClass: 'pf-system-killboard-label-recent', // class for "recent kills" label + dynamicAreaClass: 'pf-dynamic-area', // class for "dynamic" areas + controlAreaClass: 'pf-module-control-area', // class for "control" areas - let cache = { - systemKillsGraphData: {} // data for system kills info graph + minCountKills: 5, + chunkCountKills: 5, + maxCountKills: 43 }; + let cache = {}; + /** * * @param text * @param options * @returns {jQuery} */ - let getLabel = (text, options) => { - let label = $('', { - class: ['label', options.type, options.align].join(' ') - }).text( text ); + let getLabel = (text, options) => $('', { + class: ['label', options.type, options.align, options.class].join(' ') + }).text(text); + + /** + * get killmail data from ESI + * @param requestData + * @param context + * @param callback + */ + let loadKillmailData = (requestData, context, callback) => { + let cacheKey = 'killmail_' + requestData.killId; + if(cache[cacheKey]){ + // ... already cached -> return from cache + callback(context, cache[cacheKey]) + .then(payload => showKills(payload.data.killboardElement, payload.data.systemId, payload.data.chunkSize)); + }else{ + // ...not cached -> request data + let url = 'https://esi.evetech.net/latest/killmails/' + requestData.killId + '/' + requestData.hash + '/'; + + $.ajax({ + type: 'GET', + url: url, + dataType: 'json', + context: context + }).done(function(responseData){ + cache[cacheKey] = responseData; + + callback(this, responseData) + .then(payload => showKills(payload.data.killboardElement, payload.data.systemId, payload.data.chunkSize)); + }).fail(function(jqXHR, status, error){ + // request failed -> skip this and load next + showKills(this.killboardElement, this.systemId, this.chunkSize); + }); + } - return label; }; /** - * show killMails - * @param moduleElement - * @param killboardData + * load a chunk of killmails and render them + * @param killboardElement + * @param systemId + * @param chunkSize */ - let showKillmails = (moduleElement, killboardData) => { - - // show number of killMails - let killMailCounterMax = 20; - let killMailCounter = 0; - - // change order (show right to left) - killboardData.tableData.reverse(); - - let data = { - tableData: killboardData.tableData, - systemKillboardListClass: config.systemKillboardListClass, - systemKillboardListEntryClass: config.systemKillboardListEntryClass, - systemKillboardListImgShip: config.systemKillboardListImgShip, - systemKillboardListImgChar: config.systemKillboardListImgChar, - systemKillboardListImgAlly: config.systemKillboardListImgAlly, - systemKillboardListImgCorp: config.systemKillboardListImgCorp, - zKillboardUrl: 'https://zkillboard.com', - ccpImageServerUrl: Init.url.ccpImageServer, - - dateFormat: () => { - return (val, render) => { - let killDate = Util.convertDateToUTC(new Date(render(val))); - return Util.convertDateToString(killDate); - }; - }, - iskFormat: () => { - return (val, render) => { - return Util.formatPrice(render(val)); - }; - }, - checkRender : () => { - return (val, render) => { - if(killMailCounter < killMailCounterMax){ - return render(val); - } - }; - }, - increaseCount : () => { - return (val, render) => { - killMailCounter++; - }; + let showKills = (killboardElement, systemId, chunkSize) => { + if(chunkSize){ + if( + killboardElement.children().length < config.maxCountKills && + cache['zkb_' + systemId].length + ){ + // next killmail to load + let nextZkb = cache['zkb_' + systemId].shift(); + + loadKillmailData({ + killId: parseInt(nextZkb.killmail_id) || 0, + hash: nextZkb.zkb.hash + }, { + chunkSize: --chunkSize, + zkb: nextZkb.zkb, + systemId: systemId, + killboardElement: killboardElement + }, renderKillmail); + }else{ + // no more kills available OR max kills reached + killboardElement.closest('.' + config.moduleTypeClass).find('.' + config.controlAreaClass).hide(); } - }; - requirejs(['text!templates/modules/killboard.html', 'mustache'], function(template, Mustache){ - let content = Mustache.render(template, data); + } + }; - moduleElement.append(content); + /** + * render a single killmail + * @param context + * @param killmailData + * @returns {Promise} + */ + let renderKillmail = (context, killmailData) => { - // animate kill li-elements - $('.' + config.systemKillboardListEntryClass).velocity('transition.expandIn', { - stagger: 50, - complete: function(){ - // init tooltips - moduleElement.find('[title]').tooltip({ - container: 'body' - }); + let renderKillmailExecutor = (resolve, reject) => { + // calculate time diff in hours + let serverDate= Util.getServerTime(); + let killDate = Util.convertDateToUTC(new Date(killmailData.killmail_time)); + + // get time diff + let timeDiffMin = Math.round((serverDate - killDate) / 1000 / 60); + let timeDiffHour = Math.floor(timeDiffMin / 60); + + let data = { + zkb: context.zkb, + killmail: killmailData, + systemKillboardListEntryClass: config.systemKillboardListEntryClass, + systemKillboardListImgShip: config.systemKillboardListImgShip, + systemKillboardListImgChar: config.systemKillboardListImgChar, + systemKillboardListImgCorp: config.systemKillboardListImgCorp, + systemKillboardListImgAlly: config.systemKillboardListImgAlly, + zKillboardUrl: 'https://zkillboard.com', + ccpImageServerUrl: Init.url.ccpImageServer, + dateFormat: () => { + return (val, render) => { + let killDate = Util.convertDateToUTC(new Date(render(val))); + return Util.convertDateToString(killDate); + }; + }, + iskFormat: () => { + return (val, render) => { + return Util.formatPrice(render(val)); + }; + }, + }; + requirejs(['text!templates/modules/killmail.html', 'mustache'], (template, Mustache) => { + // show hint for recent kills ------------------------------------------------------------------------- + if(timeDiffHour === 0){ + context.killboardElement.siblings('.' + config.labelRecentKillsClass).css('display', 'block'); } + + // render killmail entry ------------------------------------------------------------------------------ + let content = Mustache.render(template, data); + context.killboardElement.append(content); + + // animate kill li-element ---------------------------------------------------------------------------- + context.killboardElement.children().last().velocity('transition.expandIn', { + complete: function(){ + $(this).find('[title]').tooltip({ + container: 'body' + }); + } + }); + + resolve({ + action: 'renderKillmail', + data: context + }); }); - }); + }; + + return new Promise(renderKillmailExecutor); }; /** @@ -127,214 +194,69 @@ define([ $.fn.updateSystemInfoGraphs = function(systemData){ let moduleElement = $(this); - let killboardGraphElement = $('
', { - class: config.systemKillboardGraphKillsClass - }); - - moduleElement.append(killboardGraphElement); - - let showHours = 24; - let maxKillmailCount = 200; // limited by API - let labelOptions = { align: 'center-block' }; let label = ''; - // private function draws a "system kills" graph - let drawGraph = function(data){ - let tableData = data.tableData; - - // change order (show right to left) - tableData.reverse(); + // get kills within the last 24h + let timeFrameInSeconds = 60 * 60 * 24; - if(data.count === 0){ - labelOptions.type = 'label-success'; - label = getLabel('No kills found within the last 24h', labelOptions ); - killboardGraphElement.append( label ); + // if system is w-space system -> add link modifier + let wSpaceLinkModifier = ''; + if(systemData.type.id === 1){ + wSpaceLinkModifier = 'w-space/'; + } - minifyKillboardGraphElement(killboardGraphElement); - return; - } + let url = Init.url.zKillboard + '/'; + url += 'no-items/' + wSpaceLinkModifier + 'no-attackers/npc/0/solarSystemID/' + systemData.systemId + '/pastSeconds/' + timeFrameInSeconds + '/'; - let labelYFormat = function(y){ - return Math.round(y); - }; + moduleElement.showLoadingAnimation(); - // draw chart - Morris.Bar({ - element: killboardGraphElement, - resize: true, - redraw: true, - grid: true, - gridStrokeWidth: 0.3, - gridTextSize: 9, - gridTextColor: '#63676a', - gridTextFamily: 'Oxygen Bold', - hideHover: true, - data: tableData, - xkey: 'label', - ykeys: ['kills'], - labels: ['Kills'], - yLabelFormat: labelYFormat, - xLabelMargin: 10, - padding: 10, - parseTime: false, - barOpacity: 0.8, - barRadius: [2, 2, 0, 0], - barSizeRatio: 0.5, - barGap: 3, - barColors: function(row, series, type){ - if(type === 'bar'){ - // highlight last row -> recent kills found - if(this.xmax === row.x){ - return '#c2760c'; - } - } + $.ajax({ + url: url, + type: 'GET', + dataType: 'json' + }).done(function(result){ + // zkb result needs to be cached and becomes reduced on "load more" + let cacheKey = 'zkb_' + systemData.systemId; + cache[cacheKey] = result; - return '#375959'; - } - }); - - // show hint for recent kills - if(tableData[tableData.length - 1].kills > 0){ + if(result.length){ + // kills found -> insert hidden warning for recent kills labelOptions.type = 'label-warning'; - label = getLabel( tableData[tableData.length - 1].kills + ' kills within the last hour', labelOptions ); - killboardGraphElement.prepend( label ); + labelOptions.class = config.labelRecentKillsClass; + let label = getLabel('recent kills within the last hour', labelOptions); + moduleElement.append(label); + + let killboardElement = $('
    ', { + class: config.systemKillboardListClass + }); + moduleElement.append(killboardElement); + moduleElement.append(getControlElement()); + + showKills(killboardElement, systemData.systemId, config.chunkCountKills); + }else{ + // no kills found + labelOptions.type = 'label-success'; + label = getLabel('No kills found within the last 24h', labelOptions); + moduleElement.append(label); } - }; - - // get recent KB stats (last 24h)) - let localDate = new Date(); - - // cache result for 5min - let cacheKey = systemData.systemId + '_' + localDate.getHours() + '_' + ( Math.ceil( localDate.getMinutes() / 5 ) * 5); - - if(cache.systemKillsGraphData.hasOwnProperty(cacheKey) ){ - // cached results - - drawGraph( cache.systemKillsGraphData[cacheKey] ); - - // show killmail information - showKillmails(moduleElement, cache.systemKillsGraphData[cacheKey]); - }else{ - - // chart data - let chartData = []; - - for(let i = 0; i < showHours; i++){ - let tempData = { - label: i + 'h', - kills: 0 - }; - - chartData.push(tempData); - } - - // get kills within the last 24h - let timeFrameInSeconds = 60 * 60 * 24; - - // get current server time - let serverDate= Util.getServerTime(); - - // if system is w-space system -> add link modifier - let wSpaceLinkModifier = ''; - if(systemData.type.id === 1){ - wSpaceLinkModifier = 'w-space/'; - } - - let url = Init.url.zKillboard + '/'; - url += 'no-items/' + wSpaceLinkModifier + 'no-attackers/solarSystemID/' + systemData.systemId + '/pastSeconds/' + timeFrameInSeconds + '/'; - - killboardGraphElement.showLoadingAnimation(); - - $.ajax({ - url: url, - type: 'GET', - dataType: 'json' - }).done(function(kbData){ - - // the API wont return more than 200KMs ! - remember last bar block with complete KM information - let lastCompleteDiffHourData = 0; - - // loop kills and count kills by hour - for(let i = 0; i < kbData.length; i++){ - let killmailData = kbData[i]; - let killDate = Util.convertDateToUTC(new Date(killmailData.killmail_time)); - - // get time diff - let timeDiffMin = Math.round(( serverDate - killDate ) / 1000 / 60); - let timeDiffHour = Math.floor(timeDiffMin / 60); - - // update chart data - if(chartData[timeDiffHour]){ - chartData[timeDiffHour].kills++; - - // add kill mail data - if(chartData[timeDiffHour].killmails === undefined){ - chartData[timeDiffHour].killmails = []; - } - chartData[timeDiffHour].killmails.push(killmailData); - - if(timeDiffHour > lastCompleteDiffHourData){ - lastCompleteDiffHourData = timeDiffHour; - } - } - - } - - // remove empty chart Data - if(kbData.length >= maxKillmailCount){ - chartData = chartData.splice(0, lastCompleteDiffHourData + 1); - } - - // fill cache - cache.systemKillsGraphData[cacheKey] = {}; - cache.systemKillsGraphData[cacheKey].tableData = chartData; - cache.systemKillsGraphData[cacheKey].count = kbData.length; - - // draw table - drawGraph(cache.systemKillsGraphData[cacheKey]); - - // show killmail information - showKillmails(moduleElement, cache.systemKillsGraphData[cacheKey]); - - killboardGraphElement.hideLoadingAnimation(); - }).fail(function(e){ - - labelOptions.type = 'label-danger'; - label = getLabel( 'zKillboard is not responding', labelOptions ); - killboardGraphElement.prepend( label ); - - killboardGraphElement.hideLoadingAnimation(); - - minifyKillboardGraphElement(killboardGraphElement); - - Util.showNotify({title: e.status + ': Get system kills', text: 'Loading failed', type: 'error'}); - }); - } - - + }).fail(function(e){ + labelOptions.type = 'label-danger'; + label = getLabel('zKillboard is not responding', labelOptions); + moduleElement.find('.' + config.moduleHeadClass).after(label); + + Util.showNotify({title: e.status + ': Get system kills', text: 'Loading failed', type: 'error'}); + }).always(function(){ + moduleElement.hideLoadingAnimation(); + }); // init tooltips let tooltipElements = moduleElement.find('[data-toggle="tooltip"]'); tooltipElements.tooltip({ container: 'body' }); - - }; - - /** - * minify the killboard graph element e.g. if no kills where found, or on error - * @param killboardGraphElement - */ - let minifyKillboardGraphElement = (killboardGraphElement) => { - killboardGraphElement.velocity({ - height: '20px', - marginBottom: '0px' - },{ - duration: Init.animationSpeed.mapModule - }); }; /** @@ -364,6 +286,29 @@ define([ return headlineToolbar; }; + /** + * get info control element + * @returns {void|jQuery|*} + */ + let getControlElement = () => { + let controlElement = $('
    ', { + class: [config.dynamicAreaClass, config.controlAreaClass, config.moduleHeadlineIconClass].join(' '), + html: '  load more' + }); + return controlElement; + }; + + /** + * @param moduleElement + * @param systemData + */ + let setModuleObserver = (moduleElement, systemData) => { + moduleElement.on('click', '.' + config.controlAreaClass, function(){ + let killboardElement = moduleElement.find('.' + config.systemKillboardListClass); + showKills(killboardElement, systemData.systemId, config.chunkCountKills); + }); + }; + /** * before module "show" callback * @param moduleElement @@ -396,6 +341,8 @@ define([ ) ); + setModuleObserver(moduleElement, systemData); + return moduleElement; }; diff --git a/js/app/ui/module/system_signature.js b/js/app/ui/module/system_signature.js index bb552edf5..c48866a26 100644 --- a/js/app/ui/module/system_signature.js +++ b/js/app/ui/module/system_signature.js @@ -606,7 +606,12 @@ define([ let signatureOptions = { deleteOld: (formData.deleteOld) ? 1 : 0 }; - updateSignatureTableByClipboard(moduleElement, systemData, formData.clipboard, signatureOptions); + + let mapId = moduleElement.data('mapId'); + let systemId = moduleElement.data('systemId'); + let tableApi = getDataTableInstance(mapId, systemId, 'primary'); + + updateSignatureTableByClipboard(tableApi, systemData, formData.clipboard, signatureOptions); } } } @@ -975,7 +980,6 @@ define([ let activateNextCell = (tableApi, cell, columnSelectors) => { let nextCell = searchNextCell(tableApi, cell, columnSelectors); activateCell(nextCell); - let test; }; /** @@ -1149,6 +1153,22 @@ define([ // xEditable sets 'tabindex = -1' }; + /** + * en/disables xEditable element (select) + * -> disables if there are no source options found + * @param element + */ + let editableSelectCheck = element => { + if(element.data('editable')){ + let options = element.data('editable').options.source(); + if(options.length > 0){ + editableEnable(element); + }else{ + editableDisable(element); + } + } + }; + /** * get dataTables default options for signature tables * @param mapId @@ -1328,31 +1348,16 @@ define([ } tableApi.draw(); - // find related "type" select (same row) and change options + // find related "type" select (same row) and change options --------------------------- let signatureTypeCell = getNeighboringCell(tableApi, cell, 'type:name'); let signatureTypeField = signatureTypeCell.nodes().to$(); + editableSelectCheck(signatureTypeField); - let typeOptions = getAllSignatureNames( - systemData, - systemData.type.id, - Util.getAreaIdBySecurity(systemData.security), - newValue - ); - signatureTypeField.editable('option', 'source', typeOptions); - - if( - newValue > 0 && - typeOptions.length > 0 - ){ - editableEnable(signatureTypeField); - }else{ - editableDisable(signatureTypeField); - } signatureTypeCell.data(0); signatureTypeField.editable('setValue', 0); - // find "connection" select (same row) and change "enabled" flag + // find "connection" select (same row) and change "enabled" flag ---------------------- let signatureConnectionCell = getNeighboringCell(tableApi, cell, 'connection:name'); let signatureConnectionField = signatureConnectionCell.nodes().to$(); @@ -2255,6 +2260,26 @@ define([ // xEditable is active -> should always be active! // set new value even if no change -> e.g. render selected Ids as text labels let oldValue = node.editable('getValue', true); + + // ... some editable cells depend on each other (e.g. group->type, group->connection) + switch(node.data('editable').options.name){ + case 'typeId': + // ... disable if no type options found + editableSelectCheck(node); + break; + case 'connectionId': + // disables if no wormhole group set + let groupId = cell.cell(rowIndex, 'group:name').data(); + if(groupId === 5){ + // wormhole + editableEnable(node); + }else{ + editableDisable(node); + } + break; + } + + // values should be set AFTER en/disabling of a field node.editable('setValue', cell.data()); if(oldValue !== cell.data()){ @@ -2340,6 +2365,9 @@ define([ // table data changed -> draw() table changes tableApi.draw(); + // check for "leads to" conflicts -> important if there are just "update" (no add/delete) changes + checkConnectionConflicts(); + if(!updateEmptyTable){ // no notifications if table was empty just progressbar notification is needed // sum payloads by "action" diff --git a/js/app/util.js b/js/app/util.js index bd6306e80..4df9a4157 100644 --- a/js/app/util.js +++ b/js/app/util.js @@ -25,6 +25,9 @@ define([ ajaxOverlayClass: 'pf-loading-overlay', ajaxOverlayWrapperClass: 'pf-loading-overlay-wrapper', + // page + noScrollClass: 'no-scroll', + // form formEditableFieldClass: 'pf-editable', // class for all xEditable fields formErrorContainerClass: 'pf-dialog-error-container', // class for "error" containers in dialogs @@ -497,6 +500,8 @@ define([ let data = {}; if( + tooltipData.created && + tooltipData.updated && tooltipData.created.character && tooltipData.updated.character ){ @@ -522,39 +527,41 @@ define([ createdStatusClass: statusCreatedClass, updatedStatusClass: statusUpdatedClass }; - } - let defaultOptions = { - placement: 'top', - html: true, - trigger: 'hover', - container: 'body', - title: 'Created / Updated', - delay: { - show: 150, - hide: 0 - } - }; + let defaultOptions = { + placement: 'top', + html: true, + trigger: 'hover', + container: 'body', + title: 'Created / Updated', + delay: { + show: 150, + hide: 0 + } + }; - options = $.extend({}, defaultOptions, options); + options = $.extend({}, defaultOptions, options); - return this.each(function(){ - let element = $(this); + return this.each(function(){ + let element = $(this); - requirejs(['text!templates/tooltip/character_info.html', 'mustache'], (template, Mustache) => { - let content = Mustache.render(template, data); + requirejs(['text!templates/tooltip/character_info.html', 'mustache'], (template, Mustache) => { + let content = Mustache.render(template, data); - element.popover(options); + element.popover(options); - // set new popover content - let popover = element.data('bs.popover'); - popover.options.content = content; + // set new popover content + let popover = element.data('bs.popover'); + popover.options.content = content; - if(options.show){ - element.popover('show'); - } + if(options.show){ + element.popover('show'); + } + }); }); - }); + }else{ + return this; + } }; /** @@ -778,9 +785,7 @@ define([ default: console.error('insertElement: %s is not specified!', defaultOptions.insertElement); } - //containerElement.children().first().velocity('stop').velocity('fadeIn'); $('#' + defaultOptions.messageId).velocity('stop').velocity('fadeIn'); - }); }; @@ -1074,7 +1079,12 @@ define([ callbacks: { alwaysTriggerOffsets: false, // only trigger callback.onTotalScroll() once onTotalScrollOffset: 300, // trigger callback.onTotalScroll() 100px before end - onTotalScroll: function(a){ + onInit: function(){ + // disable page scroll -> otherwise page AND customScrollbars will scroll + // -> this is because the initPassiveEvents() delegates the mouseWheel events + togglePageScroll(false); + }, + onTotalScroll: function(){ // we want to "trigger" Select2´s 'scroll' event // in order to make its "infinite scrolling" function working this.mcs.content.find(':first-child').trigger('scroll'); @@ -1117,6 +1127,10 @@ define([ // the only way to prevent this is to remove the element // https://stackoverflow.com/questions/17995057/prevent-select2-from-autmatically-focussing-its-search-input-when-dropdown-is-op $(this).parents('.editableform').find(this).next().find('.select2-selection').remove(); + + // re-enable page scroll -> might be disabled before by mCustomScrollbar onInit() event + // -> in case there is a custom ",f.noCloneChecked=!!t.cloneNode(!0).lastChild.defaultValue}();var yt=i.documentElement,vt=/^key/,bt=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,Ct=/^([^.]*)(?:\.(.+)|)/;function xt(){return!0}function wt(){return!1}function St(){try{return i.activeElement}catch(t){}}function _t(t,e,n,i,a,s){var o,r;if("object"==typeof e){for(r in"string"!=typeof n&&(i=i||n,n=void 0),e)_t(t,r,n,i,e[r],s);return t}if(null==i&&null==a?(a=n,i=n=void 0):null==a&&("string"==typeof n?(a=i,i=void 0):(a=i,i=n,n=void 0)),!1===a)a=wt;else if(!a)return t;return 1===s&&(o=a,(a=function(t){return C().off(t),o.apply(this,arguments)}).guid=o.guid||(o.guid=C.guid++)),t.each(function(){C.event.add(this,e,a,i,n)})}C.event={global:{},add:function(t,e,n,i,a){var s,o,r,l,c,u,d,p,h,f,m,g=G.get(t);if(g)for(n.handler&&(n=(s=n).handler,a=s.selector),a&&C.find.matchesSelector(yt,a),n.guid||(n.guid=C.guid++),(l=g.events)||(l=g.events={}),(o=g.handle)||(o=g.handle=function(e){return void 0!==C&&C.event.triggered!==e.type?C.event.dispatch.apply(t,arguments):void 0}),c=(e=(e||"").match(L)||[""]).length;c--;)h=m=(r=Ct.exec(e[c])||[])[1],f=(r[2]||"").split(".").sort(),h&&(d=C.event.special[h]||{},h=(a?d.delegateType:d.bindType)||h,d=C.event.special[h]||{},u=C.extend({type:h,origType:m,data:i,handler:n,guid:n.guid,selector:a,needsContext:a&&C.expr.match.needsContext.test(a),namespace:f.join(".")},s),(p=l[h])||((p=l[h]=[]).delegateCount=0,d.setup&&!1!==d.setup.call(t,i,f,o)||t.addEventListener&&t.addEventListener(h,o)),d.add&&(d.add.call(t,u),u.handler.guid||(u.handler.guid=n.guid)),a?p.splice(p.delegateCount++,0,u):p.push(u),C.event.global[h]=!0)},remove:function(t,e,n,i,a){var s,o,r,l,c,u,d,p,h,f,m,g=G.hasData(t)&&G.get(t);if(g&&(l=g.events)){for(c=(e=(e||"").match(L)||[""]).length;c--;)if(h=m=(r=Ct.exec(e[c])||[])[1],f=(r[2]||"").split(".").sort(),h){for(d=C.event.special[h]||{},p=l[h=(i?d.delegateType:d.bindType)||h]||[],r=r[2]&&new RegExp("(^|\\.)"+f.join("\\.(?:.*\\.|)")+"(\\.|$)"),o=s=p.length;s--;)u=p[s],!a&&m!==u.origType||n&&n.guid!==u.guid||r&&!r.test(u.namespace)||i&&i!==u.selector&&("**"!==i||!u.selector)||(p.splice(s,1),u.selector&&p.delegateCount--,d.remove&&d.remove.call(t,u));o&&!p.length&&(d.teardown&&!1!==d.teardown.call(t,f,g.handle)||C.removeEvent(t,h,g.handle),delete l[h])}else for(h in l)C.event.remove(t,h+e[c],n,i,!0);C.isEmptyObject(l)&&G.remove(t,"handle events")}},dispatch:function(t){var e,n,i,a,s,o,r=C.event.fix(t),l=new Array(arguments.length),c=(G.get(this,"events")||{})[r.type]||[],u=C.event.special[r.type]||{};for(l[0]=r,e=1;e=1))for(;c!==this;c=c.parentNode||this)if(1===c.nodeType&&("click"!==t.type||!0!==c.disabled)){for(s=[],o={},n=0;n-1:C.find(a,this,null,[c]).length),o[a]&&s.push(i);s.length&&r.push({elem:c,handlers:s})}return c=this,l\x20\t\r\n\f]*)[^>]*)\/>/gi,Tt=/\s*$/g;function Et(t,e){return k(t,"table")&&k(11!==e.nodeType?e:e.firstChild,"tr")&&C(t).children("tbody")[0]||t}function Pt(t){return t.type=(null!==t.getAttribute("type"))+"/"+t.type,t}function At(t){return"true/"===(t.type||"").slice(0,5)?t.type=t.type.slice(5):t.removeAttribute("type"),t}function jt(t,e){var n,i,a,s,o,r,l,c;if(1===e.nodeType){if(G.hasData(t)&&(s=G.access(t),o=G.set(e,s),c=s.events))for(a in delete o.handle,o.events={},c)for(n=0,i=c[a].length;n1&&"string"==typeof g&&!f.checkClone&&kt.test(g))return t.each(function(a){var s=t.eq(a);y&&(e[0]=g.call(this,a,s.html())),Mt(s,e,n,i)});if(p&&(s=(a=gt(e,t[0].ownerDocument,!1,t,i)).firstChild,1===a.childNodes.length&&(a=s),s||i)){for(l=(r=C.map(ht(a,"script"),Pt)).length;d")},clone:function(t,e,n){var i,a,s,o,r=t.cloneNode(!0),l=C.contains(t.ownerDocument,t);if(!(f.noCloneChecked||1!==t.nodeType&&11!==t.nodeType||C.isXMLDoc(t)))for(o=ht(r),i=0,a=(s=ht(t)).length;i0&&ft(o,!l&&ht(t,"script")),r},cleanData:function(t){for(var e,n,i,a=C.event.special,s=0;void 0!==(n=t[s]);s++)if(Y(n)){if(e=n[G.expando]){if(e.events)for(i in e.events)a[i]?C.event.remove(n,i):C.removeEvent(n,i,e.handle);n[G.expando]=void 0}n[K.expando]&&(n[K.expando]=void 0)}}}),C.fn.extend({detach:function(t){return Lt(this,t,!0)},remove:function(t){return Lt(this,t)},text:function(t){return H(this,function(t){return void 0===t?C.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=t)})},null,t,arguments.length)},append:function(){return Mt(this,arguments,function(t){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||Et(this,t).appendChild(t)})},prepend:function(){return Mt(this,arguments,function(t){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var e=Et(this,t);e.insertBefore(t,e.firstChild)}})},before:function(){return Mt(this,arguments,function(t){this.parentNode&&this.parentNode.insertBefore(t,this)})},after:function(){return Mt(this,arguments,function(t){this.parentNode&&this.parentNode.insertBefore(t,this.nextSibling)})},empty:function(){for(var t,e=0;null!=(t=this[e]);e++)1===t.nodeType&&(C.cleanData(ht(t,!1)),t.textContent="");return this},clone:function(t,e){return t=null!=t&&t,e=null==e?t:e,this.map(function(){return C.clone(this,t,e)})},html:function(t){return H(this,function(t){var e=this[0]||{},n=0,i=this.length;if(void 0===t&&1===e.nodeType)return e.innerHTML;if("string"==typeof t&&!Tt.test(t)&&!pt[(ut.exec(t)||["",""])[1].toLowerCase()]){t=C.htmlPrefilter(t);try{for(;n=0&&(l+=Math.max(0,Math.ceil(t["offset"+e[0].toUpperCase()+e.slice(1)]-s-l-r-.5))),l}function Kt(t,e,n){var i=Ft(t),a=$t(t,e,i),s="border-box"===C.css(t,"boxSizing",!1,i),o=s;if(Nt.test(a)){if(!n)return a;a="auto"}return o=o&&(f.boxSizingReliable()||a===t.style[e]),("auto"===a||!parseFloat(a)&&"inline"===C.css(t,"display",!1,i))&&(a=t["offset"+e[0].toUpperCase()+e.slice(1)],o=!0),(a=parseFloat(a)||0)+Gt(t,e,n||(s?"border":"content"),o,i,a)+"px"}function Qt(t,e,n,i,a){return new Qt.prototype.init(t,e,n,i,a)}C.extend({cssHooks:{opacity:{get:function(t,e){if(e){var n=$t(t,"opacity");return""===n?"1":n}}}},cssNumber:{animationIterationCount:!0,columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{},style:function(t,e,n,i){if(t&&3!==t.nodeType&&8!==t.nodeType&&t.style){var a,s,o,r=V(e),l=Ht.test(e),c=t.style;if(l||(e=Yt(r)),o=C.cssHooks[e]||C.cssHooks[r],void 0===n)return o&&"get"in o&&void 0!==(a=o.get(t,!1,i))?a:c[e];"string"==(s=typeof n)&&(a=et.exec(n))&&a[1]&&(n=st(t,e,a),s="number"),null!=n&&n==n&&("number"===s&&(n+=a&&a[3]||(C.cssNumber[r]?"":"px")),f.clearCloneStyle||""!==n||0!==e.indexOf("background")||(c[e]="inherit"),o&&"set"in o&&void 0===(n=o.set(t,n,i))||(l?c.setProperty(e,n):c[e]=n))}},css:function(t,e,n,i){var a,s,o,r=V(e);return Ht.test(e)||(e=Yt(r)),(o=C.cssHooks[e]||C.cssHooks[r])&&"get"in o&&(a=o.get(t,!0,n)),void 0===a&&(a=$t(t,e,i)),"normal"===a&&e in zt&&(a=zt[e]),""===n||n?(s=parseFloat(a),!0===n||isFinite(s)?s||0:a):a}}),C.each(["height","width"],function(t,e){C.cssHooks[e]={get:function(t,n,i){if(n)return!Ut.test(C.css(t,"display"))||t.getClientRects().length&&t.getBoundingClientRect().width?Kt(t,e,i):at(t,qt,function(){return Kt(t,e,i)})},set:function(t,n,i){var a,s=Ft(t),o="border-box"===C.css(t,"boxSizing",!1,s),r=i&&Gt(t,e,i,o,s);return o&&f.scrollboxSize()===s.position&&(r-=Math.ceil(t["offset"+e[0].toUpperCase()+e.slice(1)]-parseFloat(s[e])-Gt(t,e,"border",!1,s)-.5)),r&&(a=et.exec(n))&&"px"!==(a[3]||"px")&&(t.style[e]=n,n=C.css(t,e)),Xt(0,n,r)}}}),C.cssHooks.marginLeft=Bt(f.reliableMarginLeft,function(t,e){if(e)return(parseFloat($t(t,"marginLeft"))||t.getBoundingClientRect().left-at(t,{marginLeft:0},function(){return t.getBoundingClientRect().left}))+"px"}),C.each({margin:"",padding:"",border:"Width"},function(t,e){C.cssHooks[t+e]={expand:function(n){for(var i=0,a={},s="string"==typeof n?n.split(" "):[n];i<4;i++)a[t+nt[i]+e]=s[i]||s[i-2]||s[0];return a}},"margin"!==t&&(C.cssHooks[t+e].set=Xt)}),C.fn.extend({css:function(t,e){return H(this,function(t,e,n){var i,a,s={},o=0;if(Array.isArray(e)){for(i=Ft(t),a=e.length;o1)}}),C.Tween=Qt,Qt.prototype={constructor:Qt,init:function(t,e,n,i,a,s){this.elem=t,this.prop=n,this.easing=a||C.easing._default,this.options=e,this.start=this.now=this.cur(),this.end=i,this.unit=s||(C.cssNumber[n]?"":"px")},cur:function(){var t=Qt.propHooks[this.prop];return t&&t.get?t.get(this):Qt.propHooks._default.get(this)},run:function(t){var e,n=Qt.propHooks[this.prop];return this.options.duration?this.pos=e=C.easing[this.easing](t,this.options.duration*t,0,1,this.options.duration):this.pos=e=t,this.now=(this.end-this.start)*e+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):Qt.propHooks._default.set(this),this}},Qt.prototype.init.prototype=Qt.prototype,Qt.propHooks={_default:{get:function(t){var e;return 1!==t.elem.nodeType||null!=t.elem[t.prop]&&null==t.elem.style[t.prop]?t.elem[t.prop]:(e=C.css(t.elem,t.prop,""))&&"auto"!==e?e:0},set:function(t){C.fx.step[t.prop]?C.fx.step[t.prop](t):1!==t.elem.nodeType||null==t.elem.style[C.cssProps[t.prop]]&&!C.cssHooks[t.prop]?t.elem[t.prop]=t.now:C.style(t.elem,t.prop,t.now+t.unit)}}},Qt.propHooks.scrollTop=Qt.propHooks.scrollLeft={set:function(t){t.elem.nodeType&&t.elem.parentNode&&(t.elem[t.prop]=t.now)}},C.easing={linear:function(t){return t},swing:function(t){return.5-Math.cos(t*Math.PI)/2},_default:"swing"},C.fx=Qt.prototype.init,C.fx.step={};var Zt,Jt,te=/^(?:toggle|show|hide)$/,ee=/queueHooks$/;function ne(){Jt&&(!1===i.hidden&&t.requestAnimationFrame?t.requestAnimationFrame(ne):t.setTimeout(ne,C.fx.interval),C.fx.tick())}function ie(){return t.setTimeout(function(){Zt=void 0}),Zt=Date.now()}function ae(t,e){var n,i=0,a={height:t};for(e=e?1:0;i<4;i+=2-e)a["margin"+(n=nt[i])]=a["padding"+n]=t;return e&&(a.opacity=a.width=t),a}function se(t,e,n){for(var i,a=(oe.tweeners[e]||[]).concat(oe.tweeners["*"]),s=0,o=a.length;s1)},removeAttr:function(t){return this.each(function(){C.removeAttr(this,t)})}}),C.extend({attr:function(t,e,n){var i,a,s=t.nodeType;if(3!==s&&8!==s&&2!==s)return void 0===t.getAttribute?C.prop(t,e,n):(1===s&&C.isXMLDoc(t)||(a=C.attrHooks[e.toLowerCase()]||(C.expr.match.bool.test(e)?re:void 0)),void 0!==n?null===n?void C.removeAttr(t,e):a&&"set"in a&&void 0!==(i=a.set(t,n,e))?i:(t.setAttribute(e,n+""),n):a&&"get"in a&&null!==(i=a.get(t,e))?i:null==(i=C.find.attr(t,e))?void 0:i)},attrHooks:{type:{set:function(t,e){if(!f.radioValue&&"radio"===e&&k(t,"input")){var n=t.value;return t.setAttribute("type",e),n&&(t.value=n),e}}}},removeAttr:function(t,e){var n,i=0,a=e&&e.match(L);if(a&&1===t.nodeType)for(;n=a[i++];)t.removeAttribute(n)}}),re={set:function(t,e,n){return!1===e?C.removeAttr(t,n):t.setAttribute(n,n),n}},C.each(C.expr.match.bool.source.match(/\w+/g),function(t,e){var n=le[e]||C.find.attr;le[e]=function(t,e,i){var a,s,o=e.toLowerCase();return i||(s=le[o],le[o]=a,a=null!=n(t,e,i)?o:null,le[o]=s),a}});var ce=/^(?:input|select|textarea|button)$/i,ue=/^(?:a|area)$/i;function de(t){return(t.match(L)||[]).join(" ")}function pe(t){return t.getAttribute&&t.getAttribute("class")||""}function he(t){return Array.isArray(t)?t:"string"==typeof t&&t.match(L)||[]}C.fn.extend({prop:function(t,e){return H(this,C.prop,t,e,arguments.length>1)},removeProp:function(t){return this.each(function(){delete this[C.propFix[t]||t]})}}),C.extend({prop:function(t,e,n){var i,a,s=t.nodeType;if(3!==s&&8!==s&&2!==s)return 1===s&&C.isXMLDoc(t)||(e=C.propFix[e]||e,a=C.propHooks[e]),void 0!==n?a&&"set"in a&&void 0!==(i=a.set(t,n,e))?i:t[e]=n:a&&"get"in a&&null!==(i=a.get(t,e))?i:t[e]},propHooks:{tabIndex:{get:function(t){var e=C.find.attr(t,"tabindex");return e?parseInt(e,10):ce.test(t.nodeName)||ue.test(t.nodeName)&&t.href?0:-1}}},propFix:{for:"htmlFor",class:"className"}}),f.optSelected||(C.propHooks.selected={get:function(t){var e=t.parentNode;return e&&e.parentNode&&e.parentNode.selectedIndex,null},set:function(t){var e=t.parentNode;e&&(e.selectedIndex,e.parentNode&&e.parentNode.selectedIndex)}}),C.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){C.propFix[this.toLowerCase()]=this}),C.fn.extend({addClass:function(t){var e,n,i,a,s,o,r,l=0;if(m(t))return this.each(function(e){C(this).addClass(t.call(this,e,pe(this)))});if((e=he(t)).length)for(;n=this[l++];)if(a=pe(n),i=1===n.nodeType&&" "+de(a)+" "){for(o=0;s=e[o++];)i.indexOf(" "+s+" ")<0&&(i+=s+" ");a!==(r=de(i))&&n.setAttribute("class",r)}return this},removeClass:function(t){var e,n,i,a,s,o,r,l=0;if(m(t))return this.each(function(e){C(this).removeClass(t.call(this,e,pe(this)))});if(!arguments.length)return this.attr("class","");if((e=he(t)).length)for(;n=this[l++];)if(a=pe(n),i=1===n.nodeType&&" "+de(a)+" "){for(o=0;s=e[o++];)for(;i.indexOf(" "+s+" ")>-1;)i=i.replace(" "+s+" "," ");a!==(r=de(i))&&n.setAttribute("class",r)}return this},toggleClass:function(t,e){var n=typeof t,i="string"===n||Array.isArray(t);return"boolean"==typeof e&&i?e?this.addClass(t):this.removeClass(t):m(t)?this.each(function(n){C(this).toggleClass(t.call(this,n,pe(this),e),e)}):this.each(function(){var e,a,s,o;if(i)for(a=0,s=C(this),o=he(t);e=o[a++];)s.hasClass(e)?s.removeClass(e):s.addClass(e);else void 0!==t&&"boolean"!==n||((e=pe(this))&&G.set(this,"__className__",e),this.setAttribute&&this.setAttribute("class",e||!1===t?"":G.get(this,"__className__")||""))})},hasClass:function(t){var e,n,i=0;for(e=" "+t+" ";n=this[i++];)if(1===n.nodeType&&(" "+de(pe(n))+" ").indexOf(e)>-1)return!0;return!1}});var fe=/\r/g;C.fn.extend({val:function(t){var e,n,i,a=this[0];return arguments.length?(i=m(t),this.each(function(n){var a;1===this.nodeType&&(null==(a=i?t.call(this,n,C(this).val()):t)?a="":"number"==typeof a?a+="":Array.isArray(a)&&(a=C.map(a,function(t){return null==t?"":t+""})),(e=C.valHooks[this.type]||C.valHooks[this.nodeName.toLowerCase()])&&"set"in e&&void 0!==e.set(this,a,"value")||(this.value=a))})):a?(e=C.valHooks[a.type]||C.valHooks[a.nodeName.toLowerCase()])&&"get"in e&&void 0!==(n=e.get(a,"value"))?n:"string"==typeof(n=a.value)?n.replace(fe,""):null==n?"":n:void 0}}),C.extend({valHooks:{option:{get:function(t){var e=C.find.attr(t,"value");return null!=e?e:de(C.text(t))}},select:{get:function(t){var e,n,i,a=t.options,s=t.selectedIndex,o="select-one"===t.type,r=o?null:[],l=o?s+1:a.length;for(i=s<0?l:o?s:0;i-1)&&(n=!0);return n||(t.selectedIndex=-1),s}}}}),C.each(["radio","checkbox"],function(){C.valHooks[this]={set:function(t,e){if(Array.isArray(e))return t.checked=C.inArray(C(t).val(),e)>-1}},f.checkOn||(C.valHooks[this].get=function(t){return null===t.getAttribute("value")?"on":t.value})}),f.focusin="onfocusin"in t;var me=/^(?:focusinfocus|focusoutblur)$/,ge=function(t){t.stopPropagation()};C.extend(C.event,{trigger:function(e,n,a,s){var o,r,l,c,u,p,h,f,y=[a||i],v=d.call(e,"type")?e.type:e,b=d.call(e,"namespace")?e.namespace.split("."):[];if(r=f=l=a=a||i,3!==a.nodeType&&8!==a.nodeType&&!me.test(v+C.event.triggered)&&(v.indexOf(".")>-1&&(v=(b=v.split(".")).shift(),b.sort()),u=v.indexOf(":")<0&&"on"+v,(e=e[C.expando]?e:new C.Event(v,"object"==typeof e&&e)).isTrigger=s?2:3,e.namespace=b.join("."),e.rnamespace=e.namespace?new RegExp("(^|\\.)"+b.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,e.result=void 0,e.target||(e.target=a),n=null==n?[e]:C.makeArray(n,[e]),h=C.event.special[v]||{},s||!h.trigger||!1!==h.trigger.apply(a,n))){if(!s&&!h.noBubble&&!g(a)){for(c=h.delegateType||v,me.test(c+v)||(r=r.parentNode);r;r=r.parentNode)y.push(r),l=r;l===(a.ownerDocument||i)&&y.push(l.defaultView||l.parentWindow||t)}for(o=0;(r=y[o++])&&!e.isPropagationStopped();)f=r,e.type=o>1?c:h.bindType||v,(p=(G.get(r,"events")||{})[e.type]&&G.get(r,"handle"))&&p.apply(r,n),(p=u&&r[u])&&p.apply&&Y(r)&&(e.result=p.apply(r,n),!1===e.result&&e.preventDefault());return e.type=v,s||e.isDefaultPrevented()||h._default&&!1!==h._default.apply(y.pop(),n)||!Y(a)||u&&m(a[v])&&!g(a)&&((l=a[u])&&(a[u]=null),C.event.triggered=v,e.isPropagationStopped()&&f.addEventListener(v,ge),a[v](),e.isPropagationStopped()&&f.removeEventListener(v,ge),C.event.triggered=void 0,l&&(a[u]=l)),e.result}},simulate:function(t,e,n){var i=C.extend(new C.Event,n,{type:t,isSimulated:!0});C.event.trigger(i,null,e)}}),C.fn.extend({trigger:function(t,e){return this.each(function(){C.event.trigger(t,e,this)})},triggerHandler:function(t,e){var n=this[0];if(n)return C.event.trigger(t,e,n,!0)}}),f.focusin||C.each({focus:"focusin",blur:"focusout"},function(t,e){var n=function(t){C.event.simulate(e,t.target,C.event.fix(t))};C.event.special[e]={setup:function(){var i=this.ownerDocument||this,a=G.access(i,e);a||i.addEventListener(t,n,!0),G.access(i,e,(a||0)+1)},teardown:function(){var i=this.ownerDocument||this,a=G.access(i,e)-1;a?G.access(i,e,a):(i.removeEventListener(t,n,!0),G.remove(i,e))}}});var ye=t.location,ve=Date.now(),be=/\?/;C.parseXML=function(e){var n;if(!e||"string"!=typeof e)return null;try{n=(new t.DOMParser).parseFromString(e,"text/xml")}catch(t){n=void 0}return n&&!n.getElementsByTagName("parsererror").length||C.error("Invalid XML: "+e),n};var Ce=/\[\]$/,xe=/\r?\n/g,we=/^(?:submit|button|image|reset|file)$/i,Se=/^(?:input|select|textarea|keygen)/i;function _e(t,e,n,i){var a;if(Array.isArray(e))C.each(e,function(e,a){n||Ce.test(t)?i(t,a):_e(t+"["+("object"==typeof a&&null!=a?e:"")+"]",a,n,i)});else if(n||"object"!==b(e))i(t,e);else for(a in e)_e(t+"["+a+"]",e[a],n,i)}C.param=function(t,e){var n,i=[],a=function(t,e){var n=m(e)?e():e;i[i.length]=encodeURIComponent(t)+"="+encodeURIComponent(null==n?"":n)};if(Array.isArray(t)||t.jquery&&!C.isPlainObject(t))C.each(t,function(){a(this.name,this.value)});else for(n in t)_e(n,t[n],e,a);return i.join("&")},C.fn.extend({serialize:function(){return C.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var t=C.prop(this,"elements");return t?C.makeArray(t):this}).filter(function(){var t=this.type;return this.name&&!C(this).is(":disabled")&&Se.test(this.nodeName)&&!we.test(t)&&(this.checked||!ct.test(t))}).map(function(t,e){var n=C(this).val();return null==n?null:Array.isArray(n)?C.map(n,function(t){return{name:e.name,value:t.replace(xe,"\r\n")}}):{name:e.name,value:n.replace(xe,"\r\n")}}).get()}});var De=/%20/g,Te=/#.*$/,ke=/([?&])_=[^&]*/,Ie=/^(.*?):[ \t]*([^\r\n]*)$/gm,Ee=/^(?:GET|HEAD)$/,Pe=/^\/\//,Ae={},je={},Oe="*/".concat("*"),Me=i.createElement("a");function Le(t){return function(e,n){"string"!=typeof e&&(n=e,e="*");var i,a=0,s=e.toLowerCase().match(L)||[];if(m(n))for(;i=s[a++];)"+"===i[0]?(i=i.slice(1)||"*",(t[i]=t[i]||[]).unshift(n)):(t[i]=t[i]||[]).push(n)}}function Ne(t,e,n,i){var a={},s=t===je;function o(r){var l;return a[r]=!0,C.each(t[r]||[],function(t,r){var c=r(e,n,i);return"string"!=typeof c||s||a[c]?s?!(l=c):void 0:(e.dataTypes.unshift(c),o(c),!1)}),l}return o(e.dataTypes[0])||!a["*"]&&o("*")}function Fe(t,e){var n,i,a=C.ajaxSettings.flatOptions||{};for(n in e)void 0!==e[n]&&((a[n]?t:i||(i={}))[n]=e[n]);return i&&C.extend(!0,t,i),t}Me.href=ye.href,C.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:ye.href,type:"GET",isLocal:/^(?:about|app|app-storage|.+-extension|file|res|widget):$/.test(ye.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Oe,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":C.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(t,e){return e?Fe(Fe(t,C.ajaxSettings),e):Fe(C.ajaxSettings,t)},ajaxPrefilter:Le(Ae),ajaxTransport:Le(je),ajax:function(e,n){"object"==typeof e&&(n=e,e=void 0),n=n||{};var a,s,o,r,l,c,u,d,p,h,f=C.ajaxSetup({},n),m=f.context||f,g=f.context&&(m.nodeType||m.jquery)?C(m):C.event,y=C.Deferred(),v=C.Callbacks("once memory"),b=f.statusCode||{},x={},w={},S="canceled",_={readyState:0,getResponseHeader:function(t){var e;if(u){if(!r)for(r={};e=Ie.exec(o);)r[e[1].toLowerCase()]=e[2];e=r[t.toLowerCase()]}return null==e?null:e},getAllResponseHeaders:function(){return u?o:null},setRequestHeader:function(t,e){return null==u&&(t=w[t.toLowerCase()]=w[t.toLowerCase()]||t,x[t]=e),this},overrideMimeType:function(t){return null==u&&(f.mimeType=t),this},statusCode:function(t){var e;if(t)if(u)_.always(t[_.status]);else for(e in t)b[e]=[b[e],t[e]];return this},abort:function(t){var e=t||S;return a&&a.abort(e),D(0,e),this}};if(y.promise(_),f.url=((e||f.url||ye.href)+"").replace(Pe,ye.protocol+"//"),f.type=n.method||n.type||f.method||f.type,f.dataTypes=(f.dataType||"*").toLowerCase().match(L)||[""],null==f.crossDomain){c=i.createElement("a");try{c.href=f.url,c.href=c.href,f.crossDomain=Me.protocol+"//"+Me.host!=c.protocol+"//"+c.host}catch(t){f.crossDomain=!0}}if(f.data&&f.processData&&"string"!=typeof f.data&&(f.data=C.param(f.data,f.traditional)),Ne(Ae,f,n,_),u)return _;for(p in(d=C.event&&f.global)&&0==C.active++&&C.event.trigger("ajaxStart"),f.type=f.type.toUpperCase(),f.hasContent=!Ee.test(f.type),s=f.url.replace(Te,""),f.hasContent?f.data&&f.processData&&0===(f.contentType||"").indexOf("application/x-www-form-urlencoded")&&(f.data=f.data.replace(De,"+")):(h=f.url.slice(s.length),f.data&&(f.processData||"string"==typeof f.data)&&(s+=(be.test(s)?"&":"?")+f.data,delete f.data),!1===f.cache&&(s=s.replace(ke,"$1"),h=(be.test(s)?"&":"?")+"_="+ve+++h),f.url=s+h),f.ifModified&&(C.lastModified[s]&&_.setRequestHeader("If-Modified-Since",C.lastModified[s]),C.etag[s]&&_.setRequestHeader("If-None-Match",C.etag[s])),(f.data&&f.hasContent&&!1!==f.contentType||n.contentType)&&_.setRequestHeader("Content-Type",f.contentType),_.setRequestHeader("Accept",f.dataTypes[0]&&f.accepts[f.dataTypes[0]]?f.accepts[f.dataTypes[0]]+("*"!==f.dataTypes[0]?", "+Oe+"; q=0.01":""):f.accepts["*"]),f.headers)_.setRequestHeader(p,f.headers[p]);if(f.beforeSend&&(!1===f.beforeSend.call(m,_,f)||u))return _.abort();if(S="abort",v.add(f.complete),_.done(f.success),_.fail(f.error),a=Ne(je,f,n,_)){if(_.readyState=1,d&&g.trigger("ajaxSend",[_,f]),u)return _;f.async&&f.timeout>0&&(l=t.setTimeout(function(){_.abort("timeout")},f.timeout));try{u=!1,a.send(x,D)}catch(t){if(u)throw t;D(-1,t)}}else D(-1,"No Transport");function D(e,n,i,r){var c,p,h,x,w,S=n;u||(u=!0,l&&t.clearTimeout(l),a=void 0,o=r||"",_.readyState=e>0?4:0,c=e>=200&&e<300||304===e,i&&(x=function(t,e,n){for(var i,a,s,o,r=t.contents,l=t.dataTypes;"*"===l[0];)l.shift(),void 0===i&&(i=t.mimeType||e.getResponseHeader("Content-Type"));if(i)for(a in r)if(r[a]&&r[a].test(i)){l.unshift(a);break}if(l[0]in n)s=l[0];else{for(a in n){if(!l[0]||t.converters[a+" "+l[0]]){s=a;break}o||(o=a)}s=s||o}if(s)return s!==l[0]&&l.unshift(s),n[s]}(f,_,i)),x=function(t,e,n,i){var a,s,o,r,l,c={},u=t.dataTypes.slice();if(u[1])for(o in t.converters)c[o.toLowerCase()]=t.converters[o];for(s=u.shift();s;)if(t.responseFields[s]&&(n[t.responseFields[s]]=e),!l&&i&&t.dataFilter&&(e=t.dataFilter(e,t.dataType)),l=s,s=u.shift())if("*"===s)s=l;else if("*"!==l&&l!==s){if(!(o=c[l+" "+s]||c["* "+s]))for(a in c)if((r=a.split(" "))[1]===s&&(o=c[l+" "+r[0]]||c["* "+r[0]])){!0===o?o=c[a]:!0!==c[a]&&(s=r[0],u.unshift(r[1]));break}if(!0!==o)if(o&&t.throws)e=o(e);else try{e=o(e)}catch(t){return{state:"parsererror",error:o?t:"No conversion from "+l+" to "+s}}}return{state:"success",data:e}}(f,x,_,c),c?(f.ifModified&&((w=_.getResponseHeader("Last-Modified"))&&(C.lastModified[s]=w),(w=_.getResponseHeader("etag"))&&(C.etag[s]=w)),204===e||"HEAD"===f.type?S="nocontent":304===e?S="notmodified":(S=x.state,p=x.data,c=!(h=x.error))):(h=S,!e&&S||(S="error",e<0&&(e=0))),_.status=e,_.statusText=(n||S)+"",c?y.resolveWith(m,[p,S,_]):y.rejectWith(m,[_,S,h]),_.statusCode(b),b=void 0,d&&g.trigger(c?"ajaxSuccess":"ajaxError",[_,f,c?p:h]),v.fireWith(m,[_,S]),d&&(g.trigger("ajaxComplete",[_,f]),--C.active||C.event.trigger("ajaxStop")))}return _},getJSON:function(t,e,n){return C.get(t,e,n,"json")},getScript:function(t,e){return C.get(t,void 0,e,"script")}}),C.each(["get","post"],function(t,e){C[e]=function(t,n,i,a){return m(n)&&(a=a||i,i=n,n=void 0),C.ajax(C.extend({url:t,type:e,dataType:a,data:n,success:i},C.isPlainObject(t)&&t))}}),C._evalUrl=function(t){return C.ajax({url:t,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,throws:!0})},C.fn.extend({wrapAll:function(t){var e;return this[0]&&(m(t)&&(t=t.call(this[0])),e=C(t,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&e.insertBefore(this[0]),e.map(function(){for(var t=this;t.firstElementChild;)t=t.firstElementChild;return t}).append(this)),this},wrapInner:function(t){return m(t)?this.each(function(e){C(this).wrapInner(t.call(this,e))}):this.each(function(){var e=C(this),n=e.contents();n.length?n.wrapAll(t):e.append(t)})},wrap:function(t){var e=m(t);return this.each(function(n){C(this).wrapAll(e?t.call(this,n):t)})},unwrap:function(t){return this.parent(t).not("body").each(function(){C(this).replaceWith(this.childNodes)}),this}}),C.expr.pseudos.hidden=function(t){return!C.expr.pseudos.visible(t)},C.expr.pseudos.visible=function(t){return!!(t.offsetWidth||t.offsetHeight||t.getClientRects().length)},C.ajaxSettings.xhr=function(){try{return new t.XMLHttpRequest}catch(t){}};var Re={0:200,1223:204},$e=C.ajaxSettings.xhr();f.cors=!!$e&&"withCredentials"in $e,f.ajax=$e=!!$e,C.ajaxTransport(function(e){var n,i;if(f.cors||$e&&!e.crossDomain)return{send:function(a,s){var o,r=e.xhr();if(r.open(e.type,e.url,e.async,e.username,e.password),e.xhrFields)for(o in e.xhrFields)r[o]=e.xhrFields[o];for(o in e.mimeType&&r.overrideMimeType&&r.overrideMimeType(e.mimeType),e.crossDomain||a["X-Requested-With"]||(a["X-Requested-With"]="XMLHttpRequest"),a)r.setRequestHeader(o,a[o]);n=function(t){return function(){n&&(n=i=r.onload=r.onerror=r.onabort=r.ontimeout=r.onreadystatechange=null,"abort"===t?r.abort():"error"===t?"number"!=typeof r.status?s(0,"error"):s(r.status,r.statusText):s(Re[r.status]||r.status,r.statusText,"text"!==(r.responseType||"text")||"string"!=typeof r.responseText?{binary:r.response}:{text:r.responseText},r.getAllResponseHeaders()))}},r.onload=n(),i=r.onerror=r.ontimeout=n("error"),void 0!==r.onabort?r.onabort=i:r.onreadystatechange=function(){4===r.readyState&&t.setTimeout(function(){n&&i()})},n=n("abort");try{r.send(e.hasContent&&e.data||null)}catch(t){if(n)throw t}},abort:function(){n&&n()}}}),C.ajaxPrefilter(function(t){t.crossDomain&&(t.contents.script=!1)}),C.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(t){return C.globalEval(t),t}}}),C.ajaxPrefilter("script",function(t){void 0===t.cache&&(t.cache=!1),t.crossDomain&&(t.type="GET")}),C.ajaxTransport("script",function(t){var e,n;if(t.crossDomain)return{send:function(a,s){e=C("