diff --git a/.jshintrc b/.jshintrc index 2ebd120f..c61df68c 100644 --- a/.jshintrc +++ b/.jshintrc @@ -35,7 +35,7 @@ "latedef": false, // Enforce line length to 100 characters - "maxlen": 200, + "maxlen": 220, // Require capitalized names for constructor functions. "newcap": true, diff --git a/app/Controller/Api/Map.php b/app/Controller/Api/Map.php index 2abd9224..48760da6 100644 --- a/app/Controller/Api/Map.php +++ b/app/Controller/Api/Map.php @@ -170,7 +170,7 @@ class Map extends Controller\AccessController { // get program routes ------------------------------------------------------------------------------------- $return->routes = [ - 'ssoLogin' => $this->getF3()->alias( 'sso', ['action' => 'requestAuthorization'] ) + 'ssoLogin' => $this->getF3()->alias('sso', ['action' => 'requestAuthorization']) ]; // get third party APIs ----------------------------------------------------------------------------------- @@ -182,6 +182,11 @@ class Map extends Controller\AccessController { 'anoik' => Config::getPathfinderData('api.anoik') ]; + // get Plugin config -------------------------------------------------------------------------------------- + $return->plugin = [ + 'modules' => Config::getPluginConfig('modules') + ]; + // Character default config ------------------------------------------------------------------------------- $return->character = [ 'autoLocationSelect' => (bool)Config::getPathfinderData('character.auto_location_select') @@ -1065,7 +1070,7 @@ class Map extends Controller\AccessController { ){ // check distance between systems (in jumps) // -> if > 1 it is !very likely! a wormhole - $route = (new Route())->searchRoute($sourceSystem->systemId, $targetSystem->systemId, 1); + $route = (new Controller\Api\Rest\Route())->searchRoute($sourceSystem->systemId, $targetSystem->systemId, 1); if(!$route['routePossible']){ $addSourceSystem = true; @@ -1165,7 +1170,7 @@ class Map extends Controller\AccessController { // .. now we need to check jump distance between systems // -> if > 1 it is !very likely! podded jump if(empty($route)){ - $route = (new Route())->searchRoute($sourceSystem->systemId, $targetSystem->systemId, 1); + $route = (new Controller\Api\Rest\Route())->searchRoute($sourceSystem->systemId, $targetSystem->systemId, 1); } if(!$route['routePossible']){ diff --git a/app/Controller/Api/Rest/Connection.php b/app/Controller/Api/Rest/Connection.php index 2177e1bd..b73666ae 100644 --- a/app/Controller/Api/Rest/Connection.php +++ b/app/Controller/Api/Rest/Connection.php @@ -13,6 +13,51 @@ use Exodus4D\Pathfinder\Model\Pathfinder; class Connection extends AbstractRestController { + /** + * @param \Base $f3 + * @param $params + * @throws \Exception + */ + public function get(\Base $f3, $params){ + $requestData = $this->getRequestData($f3); + $connectionIds = array_map('intval', explode(',', (string)$params['id'])); + $addData = (array)$requestData['addData']; + $filterData = (array)$requestData['filterData']; + $connectionData = []; + + if($mapId = (int)$requestData['mapId']){ + $activeCharacter = $this->getCharacter(); + + /** + * @var $map Pathfinder\MapModel + */ + $map = Pathfinder\AbstractPathfinderModel::getNew('MapModel'); + $map->getById($mapId); + + if($map->hasAccess($activeCharacter)){ + $connections = $map->getConnections($connectionIds, 'wh'); + foreach($connections as $connection){ + $check = true; + $data = $connection->getData(in_array('signatures', $addData), in_array('logs', $addData)); + // filter result + if(in_array('signatures', $filterData) && !$data->signatures){ + $check = false; + } + + if(in_array('logs', $filterData) && !$data->logs){ + $check = false; + } + + if($check){ + $connectionData[] = $data; + } + } + } + } + + $this->out($connectionData); + } + /** * save a new connection or updates an existing (drag/drop) between two systems * if a connection is changed (drag&drop) to another system. -> this function is called for update diff --git a/app/Controller/Api/Route.php b/app/Controller/Api/Rest/Route.php similarity index 98% rename from app/Controller/Api/Route.php rename to app/Controller/Api/Rest/Route.php index 8360b948..c0aa9b2a 100644 --- a/app/Controller/Api/Route.php +++ b/app/Controller/Api/Rest/Route.php @@ -1,24 +1,15 @@ get('POST'); + public function post(\Base $f3){ + $requestData = $this->getRequestData($f3); $activeCharacter = $this->getCharacter(); @@ -861,20 +852,6 @@ class Route extends Controller\AccessController { } } - echo json_encode($return); + $this->out($return); } - -} - - - - - - - - - - - - - +} \ No newline at end of file diff --git a/app/Controller/Api/Rest/SystemGraph.php b/app/Controller/Api/Rest/SystemGraph.php new file mode 100644 index 00000000..b84a7905 --- /dev/null +++ b/app/Controller/Api/Rest/SystemGraph.php @@ -0,0 +1,112 @@ +getRequestData($f3); + $systemIds = (array)$requestData['systemIds']; + $graphsData = []; + + // valid response (data found) should be cached by server + client + $cacheResponse = false; + + // number of log entries in each table per system (24 = 24h) + $logEntryCount = Pathfinder\AbstractSystemApiBasicModel::DATA_COLUMN_COUNT; + + $ttl = 60 * 10; + + // table names with system data + $logTables = [ + 'jumps' => 'SystemJumpModel', + 'shipKills' => 'SystemShipKillModel', + 'podKills' => 'SystemPodKillModel', + 'factionKills' => 'SystemFactionKillModel' + ]; + + $exists = false; + + foreach($systemIds as $systemId){ + $cacheKey = $this->getSystemGraphCacheKey($systemId); + if(!$exists = $f3->exists($cacheKey, $graphData)){ + $graphData = []; + $cacheSystem = false; + + foreach($logTables as $label => $className){ + $systemLogModel = Pathfinder\AbstractSystemApiBasicModel::getNew($className); + $systemLogExists = false; + + // 10min cache (could be up to 1h cache time) + $systemLogModel->getByForeignKey('systemId', $systemId); + if($systemLogModel->valid()){ + $systemLogExists = true; + $cacheSystem = true; + $cacheResponse = true; + } + + $systemLogData = $systemLogModel->getData(); + + // podKills share graph with shipKills -> skip + if($label != 'podKills'){ + $graphData[$label]['logExists'] = $systemLogExists; + $graphData[$label]['updated'] = $systemLogData->updated; + } + + $logValueCount = range(0, $logEntryCount - 1); + foreach($logValueCount as $i){ + if($label == 'podKills'){ + $graphData['shipKills']['data'][$i]['z'] = $systemLogData->values[$i]; + }else{ + $graphData[$label]['data'][] = [ + 'x' => ($logEntryCount - $i - 1) . 'h', + 'y' => $systemLogData->values[$i] + ]; + } + } + } + + if($cacheSystem){ + $f3->set($cacheKey, $graphData, $ttl); + } + }else{ + // server cache data exists -> client should cache as well + $cacheResponse = true; + } + $graphsData[$systemId] = $graphData; + } + + if($cacheResponse){ + // send client cache header + $f3->expire(Config::ttlLeft($exists, $ttl)); + } + + $this->out($graphsData); + } + + // ---------------------------------------------------------------------------------------------------------------- + + /** + * get system graph cache key + * @param int $systemId + * @return string + */ + protected function getSystemGraphCacheKey(int $systemId): string { + return sprintf(self::CACHE_KEY_GRAPH, 'SYSTEM_' . $systemId); + } +} \ No newline at end of file diff --git a/app/Controller/Api/System.php b/app/Controller/Api/System.php index 9a6cf039..d2314f63 100644 --- a/app/Controller/Api/System.php +++ b/app/Controller/Api/System.php @@ -14,103 +14,6 @@ use Exodus4D\Pathfinder\Model\Pathfinder; class System extends Controller\AccessController { - // cache keys - const CACHE_KEY_GRAPH = 'CACHED_SYSTEM_GRAPH_%s'; - - /** - * get system graph cache key - * @param int $systemId - * @return string - */ - protected function getSystemGraphCacheKey(int $systemId): string { - return sprintf(self::CACHE_KEY_GRAPH, 'SYSTEM_' . $systemId); - } - - /** - * get system log data from CCP API import - * system Kills, Jumps,.... - * @param \Base $f3 - * @throws \Exception - */ - public function graphData(\Base $f3){ - $graphsData = []; - $systemIds = (array)$f3->get('GET.systemIds'); - - // valid response (data found) should be cached by server + client - $cacheResponse = false; - - // number of log entries in each table per system (24 = 24h) - $logEntryCount = Pathfinder\AbstractSystemApiBasicModel::DATA_COLUMN_COUNT; - - $ttl = 60 * 10; - - // table names with system data - $logTables = [ - 'jumps' => 'SystemJumpModel', - 'shipKills' => 'SystemShipKillModel', - 'podKills' => 'SystemPodKillModel', - 'factionKills' => 'SystemFactionKillModel' - ]; - - $exists = false; - - foreach($systemIds as $systemId){ - $cacheKey = $this->getSystemGraphCacheKey($systemId); - if(!$exists = $f3->exists($cacheKey, $graphData)){ - $graphData = []; - $cacheSystem = false; - - foreach($logTables as $label => $className){ - $systemLogModel = Pathfinder\AbstractSystemApiBasicModel::getNew($className); - $systemLogExists = false; - - // 10min cache (could be up to 1h cache time) - $systemLogModel->getByForeignKey('systemId', $systemId); - if($systemLogModel->valid()){ - $systemLogExists = true; - $cacheSystem = true; - $cacheResponse = true; - } - - $systemLogData = $systemLogModel->getData(); - - // podKills share graph with shipKills -> skip - if($label != 'podKills'){ - $graphData[$label]['logExists'] = $systemLogExists; - $graphData[$label]['updated'] = $systemLogData->updated; - } - - $logValueCount = range(0, $logEntryCount - 1); - foreach($logValueCount as $i){ - if($label == 'podKills'){ - $graphData['shipKills']['data'][$i]['z'] = $systemLogData->values[$i]; - }else{ - $graphData[$label]['data'][] = [ - 'x' => ($logEntryCount - $i - 1) . 'h', - 'y' => $systemLogData->values[$i] - ]; - } - } - } - - if($cacheSystem){ - $f3->set($cacheKey, $graphData, $ttl); - } - }else{ - // server cache data exists -> client should cache as well - $cacheResponse = true; - } - $graphsData[$systemId] = $graphData; - } - - if($cacheResponse){ - // send client cache header - $f3->expire(Config::ttlLeft($exists, $ttl)); - } - - echo json_encode($graphsData); - } - /** * set destination for system, station or structure * @param \Base $f3 diff --git a/app/Controller/Controllerr.php b/app/Controller/Controllerr.php index 5e45250e..8a1a27dd 100644 --- a/app/Controller/Controllerr.php +++ b/app/Controller/Controllerr.php @@ -93,7 +93,7 @@ class Controller { header('Pf-Maintenance: ' . $modeMaintenance); } }else{ - $this->initResource($f3); + $f3->set('tplResource', $this->initResource($f3)); $this->setTemplate(Config::getPathfinderData('view.index')); @@ -161,15 +161,18 @@ class Controller { /** * init new Resource handler * @param \Base $f3 + * @return Resource */ protected function initResource(\Base $f3){ $resource = Resource::instance(); + $resource->setOption('basePath', $f3->get('BASE')); $resource->setOption('filePath', [ - 'style' => $f3->get('BASE') . '/public/css/' . Config::getPathfinderData('version'), - 'script' => $f3->get('BASE') . '/public/js/' . Config::getPathfinderData('version'), - 'font' => $f3->get('BASE') . '/public/fonts', - 'document' => $f3->get('BASE') . '/public/templates', - 'image' => $f3->get('BASE') . '/public/img' + 'style' => sprintf('/%scss/%s', $f3->get('UI'), Config::getPathfinderData('version')), + 'script' => sprintf('/%sjs/%s', $f3->get('UI'), Config::getPathfinderData('version')), + 'font' => sprintf('/%sfonts', $f3->get('UI')), + 'document' => sprintf('/%stemplates', $f3->get('UI')), + 'image' => sprintf('/%simg', $f3->get('UI')), + 'favicon' => $f3->get('FAVICON') ], true); $resource->register('style', 'pathfinder'); @@ -187,7 +190,7 @@ class Controller { $resource->register('url', Config::getPathfinderData('api.ccp_image_server'), 'dns-prefetch'); $resource->register('url', '//i.ytimg.com', 'dns-prefetch'); // YouTube tiny embed domain - $f3->set('tplResource', $resource); + return $resource; } /** diff --git a/app/Controller/Setup.php b/app/Controller/Setup.php index 81d0c650..5ec156d3 100644 --- a/app/Controller/Setup.php +++ b/app/Controller/Setup.php @@ -148,7 +148,7 @@ class Setup extends Controller { * @return bool */ function beforeroute(\Base $f3, $params): bool { - $this->initResource($f3); + $f3->set('tplResource', $this->initResource($f3)); // page title $f3->set('tplPageTitle', 'Setup | ' . Config::getPathfinderData('name')); diff --git a/app/Lib/Config.php b/app/Lib/Config.php index a422a6cf..e7659ed0 100644 --- a/app/Lib/Config.php +++ b/app/Lib/Config.php @@ -39,6 +39,11 @@ class Config extends \Prefab { */ const HIVE_KEY_ENVIRONMENT = 'ENVIRONMENT'; + /** + * Hive key for custom plugins (js map modules) + */ + const HIVE_KEY_PLUGIN = 'PLUGIN'; + /** * Hive key for Socket validation check */ @@ -374,7 +379,7 @@ class Config extends \Prefab { * get SMTP config values * @return \stdClass */ - static function getSMTPConfig(): \stdClass{ + static function getSMTPConfig() : \stdClass{ $config = new \stdClass(); $config->host = self::getEnvironmentData('SMTP_HOST'); $config->port = self::getEnvironmentData('SMTP_PORT'); @@ -392,9 +397,9 @@ class Config extends \Prefab { * @param \stdClass $config * @return bool */ - static function isValidSMTPConfig(\stdClass $config): bool { + static function isValidSMTPConfig(\stdClass $config) : bool { // validate email from either an configured array or plain string - $validateMailConfig = function($mailConf = null): bool { + $validateMailConfig = function($mailConf = null) : bool { $email = null; if(is_array($mailConf)){ reset($mailConf); @@ -436,13 +441,35 @@ class Config extends \Prefab { return $mapConfig; } + /** + * get Plugin config from `plugin.ini` + * @param string|null $key + * @param bool $checkEnabled + * @return array|null + */ + static function getPluginConfig(?string $key, bool $checkEnabled = true) : ?array { + $isEnabled = $checkEnabled ? + filter_var(\Base::instance()->get( + self::HIVE_KEY_PLUGIN . '.' . strtoupper($key) . '_ENABLED'), + FILTER_VALIDATE_BOOLEAN + ) : + true; + + $data = null; + if($isEnabled){ + $hiveKey = self::HIVE_KEY_PLUGIN . '.' . strtoupper($key); + $data = (array)\Base::instance()->get($hiveKey); + } + return $data; + } + /** * get custom $message for a a HTTP $status * -> use this in addition to the very general Base::HTTP_XXX labels * @param int $status * @return string */ - static function getMessageFromHTTPStatus(int $status): string { + static function getMessageFromHTTPStatus(int $status) : string { switch($status){ case 403: $message = 'Access denied: User not found'; break; diff --git a/app/Lib/Resource.php b/app/Lib/Resource.php index d1e47a76..7a9b6ba1 100644 --- a/app/Lib/Resource.php +++ b/app/Lib/Resource.php @@ -15,45 +15,52 @@ class Resource extends \Prefab { * default link "rel" attribute * @link https://w3c.github.io/preload/#x2.link-type-preload */ - const ATTR_REL = 'preload'; + const ATTR_REL = 'preload'; /** * default link "as" attributes */ const ATTR_AS = [ - 'style' => 'style', - 'script' => 'script', - 'font' => 'font', - 'document' => 'document', - 'image' => 'image', - 'url' => '' + 'style' => 'style', + 'script' => 'script', + 'font' => 'font', + 'document' => 'document', + 'image' => 'image', + 'url' => '' ]; /** * default link "type" attributes */ const ATTR_TYPE = [ - 'font' => 'font/woff2' + 'font' => 'font/woff2' ]; /** * default additional attributes by $group */ const ATTR_ADD = [ - 'font' => ['crossorigin' => 'anonymous'] + 'font' => ['crossorigin' => 'anonymous'] ]; + /** + * BASE path + * @var string + */ + private $basePath = ''; + /** * absolute file path -> use setOption() for update * @var array */ private $filePath = [ - 'style' => '', - 'script' => '', - 'font' => '', - 'document' => '', - 'image' => '', - 'url' => '' + 'style' => '', + 'script' => '', + 'font' => '', + 'document' => '', + 'image' => '', + 'favicon' => '', + 'url' => '' ]; /** @@ -62,10 +69,10 @@ class Resource extends \Prefab { * @var array */ private $fileExt = [ - 'style' => 'css', - 'script' => 'js', - 'document' => 'html', - 'font' => 'woff2' + 'style' => 'css', + 'script' => 'js', + 'document' => 'html', + 'font' => 'woff2' ]; /** @@ -76,7 +83,7 @@ class Resource extends \Prefab { * @see buildHeader() * @var string */ - private $output = 'inline'; + private $output = 'inline'; /** * resource file cache @@ -134,7 +141,7 @@ class Resource extends \Prefab { * @return string */ public function getPath(string $group) : string { - return $this->filePath[$group]; + return rtrim($this->basePath, '/\\') . $this->filePath[$group]; } /** diff --git a/app/Model/Pathfinder/ConnectionModel.php b/app/Model/Pathfinder/ConnectionModel.php index 33eff594..25675887 100644 --- a/app/Model/Pathfinder/ConnectionModel.php +++ b/app/Model/Pathfinder/ConnectionModel.php @@ -9,7 +9,7 @@ namespace Exodus4D\Pathfinder\Model\Pathfinder; use DB\SQL\Schema; -use Exodus4D\Pathfinder\Controller\Api\Route; +use Exodus4D\Pathfinder\Controller\Api\Rest\Route; use Exodus4D\Pathfinder\Lib\Logging; use Exodus4D\Pathfinder\Exception; @@ -234,9 +234,7 @@ class ConnectionModel extends AbstractMapTrackingModel { $this->scope = 'abyssal'; $this->type = ['abyssal']; }else{ - $routeController = new Route(); - $route = $routeController->searchRoute($this->source->systemId, $this->target->systemId, 1); - + $route = (new Route())->searchRoute($this->source->systemId, $this->target->systemId, 1); if($route['routePossible']){ $this->scope = 'stargate'; $this->type = ['stargate']; diff --git a/app/config.ini b/app/config.ini index c582fad9..917148b7 100644 --- a/app/config.ini +++ b/app/config.ini @@ -137,6 +137,8 @@ CONF.DEFAULT = app/ [configs] {{@CONF.DEFAULT}}routes.ini = true {{@CONF.DEFAULT}}pathfinder.ini = true +{{@CONF.DEFAULT}}plugin.ini = true {{@CONF.CUSTOM}}pathfinder.ini = true +{{@CONF.CUSTOM}}plugin.ini = true {{@CONF.DEFAULT}}requirements.ini = true {{@CONF.DEFAULT}}cron.ini = true \ No newline at end of file diff --git a/app/plugin.ini b/app/plugin.ini new file mode 100644 index 00000000..694c48ae --- /dev/null +++ b/app/plugin.ini @@ -0,0 +1,6 @@ +[PLUGIN] +MODULES_ENABLED = 1 + +[PLUGIN.MODULES] +DEMO = ./app/ui/module/demo +EMPTY = ./app/ui/module/empty diff --git a/composer.json b/composer.json index 7a5b403d..b7c984ed 100644 --- a/composer.json +++ b/composer.json @@ -27,7 +27,7 @@ } ], "require": { - "php-64bit": ">=7.1", + "php-64bit": ">=7.2", "ext-pdo": "*", "ext-openssl": "*", "ext-curl": "*", @@ -40,7 +40,7 @@ "ikkez/f3-sheet": "0.4.*", "xfra35/f3-cron": "1.2.*", "monolog/monolog": "2.*", - "swiftmailer/swiftmailer": "6.2.x", + "swiftmailer/swiftmailer": "6.2.*", "league/html-to-markdown": "4.9.*", "cache/redis-adapter": "1.0.*", "cache/filesystem-adapter": "1.0.*", diff --git a/config.rb b/config.rb index 8335fdf8..a79d3478 100644 --- a/config.rb +++ b/config.rb @@ -32,3 +32,11 @@ cache_path = '.sass-cache' # preferred_syntax = :sass # and then run: # sass-convert -R --from scss --to sass sass scss && rm -rf sass && mv scss sass + +# custom SASS functions +module Sass::Script::Functions + def currentYear() + # return Sass::Script::String.new(Time.now.to_s) + return Sass::Script::String.new(Time.now.year.to_s) + end +end diff --git a/gulpfile.js b/gulpfile.js index 00c65202..cedaf9fc 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -514,7 +514,7 @@ gulp.task('task:hintJS', () => { * concat/build JS files by modules */ gulp.task('task:concatJS', () => { - let modules = ['login', 'mappage', 'setup', 'admin', 'notification', 'datatables.loader']; + let modules = ['login', 'mappage', 'setup', 'admin', 'PNotify.loader', 'datatables.loader']; let srcModules = ['./js/app/*(' + modules.join('|') + ').js']; return gulp.src(srcModules, {base: 'js'}) diff --git a/js/app.js b/js/app.js index 1de1cc05..bd9913f4 100644 --- a/js/app.js +++ b/js/app.js @@ -25,7 +25,6 @@ requirejs.config({ mappage: './app/mappage', // initial start "map page" view setup: './app/setup', // initial start "setup page" view admin: './app/admin', // initial start "admin page" view - notification: './app/notification', // "notification" view jquery: 'lib/jquery-3.4.1.min', // v3.4.1 jQuery bootstrap: 'lib/bootstrap.min', // v3.3.0 Bootstrap js code - http://getbootstrap.com/javascript @@ -56,7 +55,7 @@ requirejs.config({ bootstrapConfirmation: 'lib/bootstrap-confirmation.min', // v1.0.7 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.7 LazyLoader images - https://appelsiini.net/projects/lazyload/ - sortable: 'lib/sortable.min', // v1.6.0 Sortable - drag&drop reorder - https://github.com/rubaxa/Sortable + sortable: 'lib/sortable.min', // v1.10.1 Sortable - drag&drop reorder - https://github.com/SortableJS/Sortable 'summernote.loader': './app/summernote.loader', // v0.8.10 Summernote WYSIWYG editor -https://summernote.org 'summernote': 'lib/summernote/summernote.min', @@ -65,7 +64,7 @@ requirejs.config({ easePack: 'lib/EasePack.min', tweenLite: 'lib/TweenLite.min', - // datatables // v1.10.18 DataTables - https://datatables.net + // DataTables // v1.10.18 DataTables - https://datatables.net 'datatables.loader': './app/datatables.loader', 'datatables.net': 'lib/datatables/DataTables-1.10.18/js/jquery.dataTables.min', 'datatables.net-buttons': 'lib/datatables/Buttons-1.5.6/js/dataTables.buttons.min', @@ -74,15 +73,14 @@ requirejs.config({ 'datatables.net-select': 'lib/datatables/Select-1.3.0/js/dataTables.select.min', 'datatables.plugins.render.ellipsis': 'lib/datatables/plugins/render/ellipsis', - // notification plugin - pnotify: 'lib/pnotify/pnotify', // v3.2.1 PNotify - notification core file - https://sciactive.com/pnotify/ - 'pnotify.buttons': 'lib/pnotify/pnotify.buttons', // PNotify - buttons notification extension - 'pnotify.confirm': 'lib/pnotify/pnotify.confirm', // PNotify - confirmation notification extension - 'pnotify.nonblock': 'lib/pnotify/pnotify.nonblock', // PNotify - notification non-block extension (hover effect) - 'pnotify.desktop': 'lib/pnotify/pnotify.desktop', // PNotify - desktop push notification extension - 'pnotify.history': 'lib/pnotify/pnotify.history', // PNotify - history push notification history extension - 'pnotify.callbacks': 'lib/pnotify/pnotify.callbacks', // PNotify - callbacks push notification extension - 'pnotify.reference': 'lib/pnotify/pnotify.reference' // PNotify - reference push notification extension + // PNotify // v4.0.0 PNotify - notification core file - https://sciactive.com/pnotify + 'PNotify.loader': './app/pnotify.loader', + 'PNotify': 'lib/pnotify/PNotify', + 'PNotifyButtons': 'lib/pnotify/PNotifyButtons', + 'PNotifyNonBlock': 'lib/pnotify/PNotifyNonBlock', + 'PNotifyDesktop': 'lib/pnotify/PNotifyDesktop', + 'PNotifyCallbacks': 'lib/pnotify/PNotifyCallbacks', + 'NonBlock': 'lib/pnotify/NonBlock' // v1.0.8 NonBlock.js - for PNotify "nonblock" feature }, shim: { bootstrap: { @@ -138,9 +136,6 @@ requirejs.config({ window.Raphael = Raphael; } }, - pnotify: { - deps: ['jquery'] - }, easyPieChart: { deps: ['jquery'] }, diff --git a/js/app/datatables.loader.js b/js/app/datatables.loader.js index 6f0aa089..202f3f51 100644 --- a/js/app/datatables.loader.js +++ b/js/app/datatables.loader.js @@ -5,11 +5,12 @@ define([ 'app/promises/promise.deferred', 'app/promises/promise.timeout', 'datatables.net', + 'datatables.net-select', 'datatables.net-buttons', 'datatables.net-buttons-html', - 'datatables.net-responsive', - 'datatables.net-select' + 'datatables.net-responsive' ], ($, Init, Counter, DeferredPromise, TimeoutPromise) => { + 'use strict'; // all Datatables stuff is available... diff --git a/js/app/init.js b/js/app/init.js index 5345b4dc..baaa7671 100644 --- a/js/app/init.js +++ b/js/app/init.js @@ -44,11 +44,8 @@ define([], () => { getMapConnectionData: '/api/map/getConnectionData', // ajax URL - get connection data getMapLogData: '/api/map/getLogData', // ajax URL - get logs data // system API - getSystemGraphData: '/api/system/graphData', // ajax URL - get all system graph data setDestination: '/api/system/setDestination', // ajax URL - set destination pokeRally: '/api/system/pokeRally', // ajax URL - send rally point pokes - // route API - searchRoute: '/api/route/search', // ajax URL - search system routes // stats API getStatisticsData: '/api/statistic/getData', // ajax URL - get statistics data (activity log) // universe API diff --git a/js/app/lib/cache.js b/js/app/lib/cache.js index 0ef63b56..de8cb11a 100644 --- a/js/app/lib/cache.js +++ b/js/app/lib/cache.js @@ -127,7 +127,7 @@ define([], () => { constructor(config){ this.config = Object.assign({},{ - name: 'Default', // custom name for identification + name: 'Default', // custom unique name for identification ttl: 3600, // default ttl for cache entries maxSize: 600, // max cache entries bufferSize: 10, // cache entry count in percent to be removed if maxSize reached diff --git a/js/app/lib/cron.js b/js/app/lib/cron.js index 3f7ef16c..70f9a8bd 100644 --- a/js/app/lib/cron.js +++ b/js/app/lib/cron.js @@ -107,7 +107,7 @@ define([ delete(){ let isDeleted = false; if(this._manager){ - isDeleted = this._manager.delete(this.name); + isDeleted = this._manager.delete(this._name); } return isDeleted; } diff --git a/js/app/lib/dataStore.js b/js/app/lib/dataStore.js new file mode 100644 index 00000000..3c8d81ee --- /dev/null +++ b/js/app/lib/dataStore.js @@ -0,0 +1,55 @@ +define([], () => { + 'use strict'; + + /* + // Example usage -------------------------------------------------------------------------------------------------- + // global accessible DataStore instance + window.dataStore = new DataStore(); + + // extend HTMLElement class with an interface to set/get data to it + HTMLElement.prototype.setData = function(key, value){ + window.dataStore.set(this, key, value); + }; + + HTMLElement.prototype.getData = function(key){ + return window.dataStore.get(this, key); + }; + */ + + /** + * Stores data to an object + * -> can be used as a replacement for jQuery $.data() + */ + return class DataStore { + constructor() { + this._store = new WeakMap(); + } + + set(obj, key, value) { + if (!this._store.has(obj)) { + this._store.set(obj, new Map()); + } + this._store.get(obj).set(key, value); + return obj; + } + + get(obj, key) { + return this._store.has(obj) && this._store.get(obj).get(key); + } + + has(obj, key) { + return this._store.has(obj) && this._store.get(obj).has(key); + } + + remove(obj, key) { + let ret = false; + if (this._store.has(obj)) { + ret = this._store.get(obj).delete(key); + if (!this._store.get(obj).size) { + this._store.delete(obj); + } + } + return ret; + } + }; +}); \ No newline at end of file diff --git a/js/app/lib/localStore.js b/js/app/lib/localStore.js new file mode 100644 index 00000000..6df7828b --- /dev/null +++ b/js/app/lib/localStore.js @@ -0,0 +1,394 @@ +define([ + 'localForage', + 'app/promises/promise.queue', + 'app/promises/promise.deferred', +], (LocalForage, PromiseQueue, DeferredPromise) => { + 'use strict'; + + /** + * Instances of LocalStore handle its own LocalForage instance + */ + class LocalStore { + constructor(config, LocalForageConfig){ + this._config = Object.assign({}, this.constructor.defaultConfig, config); + + let initPromise = new DeferredPromise(); + this._processQueue = new PromiseQueue(); + this._processQueue.enqueue(() => initPromise); + + this._localforage = LocalForage.createInstance(Object.assign({}, LocalStore.LocalForageConfig, LocalForageConfig)); + this._localforage.ready().then(() => initPromise.resolve()); + + this._manager = null; // reference to LocalStoreManager() that manages this LocalStore instance + + this.debug = (msg,...data) => { + if(this._config.debug){ + data = (data || []); + data.unshift(this.constructor.name, this._config.name); + console.debug('debug: %s %o | ' + msg, ...data); + } + }; + } + + /** + * set scope for this instance + * -> all read/write actions are scoped + * this is a prefix for all keys! + * @param scope + */ + set scope(scope){ + if(LocalStore.isString(scope)){ + this._config.scope = scope; + }else{ + throw new TypeError('Scope must be instance of "String", Type of "' + typeof scope + '" given'); + } + } + + get scope(){ + return this._config.scope; + } + + /** + * get item + * @param key + * @param successCallback + * @returns {Promise} + */ + getItem(key, successCallback = undefined){ + key = this.fixKey(key); + let propArray = LocalStore.keyToArray(key); + let rootKey = propArray.shift(); + + let getItem = () => this._localforage.getItem(key, successCallback); + if(propArray.length){ + getItem = () => { + return this._localforage.getItem(rootKey) + .then(data => { + if(LocalStore.isObject(data)){ + // find nested property + return LocalStore.findObjProp(data, propArray); + }else{ + // rootKey not found -> propArray path not exists + return Promise.resolve(null); + } + }); + }; + } + + return this._processQueue.enqueue(() => getItem()); + } + + /** + * set/update existing value + * @param key e.g. nested object key' first.a.b.test' + * @param value + * @param successCallback + * @returns {Promise} + */ + setItem(key, value, successCallback = undefined){ + key = this.fixKey(key); + let propArray = LocalStore.keyToArray(key); + let rootKey = propArray.shift(); + + let getItem = () => Promise.resolve(value); + if(propArray.length){ + getItem = () => { + return this._localforage.getItem(rootKey) + .then(rootVal => { + rootVal = (rootVal === null) ? {} : rootVal; + // update data with new value (merge obj) + LocalStore.updateObjProp(rootVal, value, propArray); + return rootVal; + }); + }; + } + + return this._processQueue.enqueue(() => + getItem() + .then(rootVal => this._localforage.setItem(rootKey, rootVal, successCallback)) + .then(() => Promise.resolve(value)) + ); + } + + /** + * remove item by key + * -> allows deep obj delete if key points to a nested obj prop + * @param key + * @param successCallback + * @returns {Promise} + */ + removeItem(key, successCallback = undefined){ + key = this.fixKey(key); + let propArray = LocalStore.keyToArray(key); + let rootKey = propArray.shift(); + + let removeItem = () => this._localforage.removeItem(rootKey, successCallback); + if(propArray.length){ + removeItem = () => { + return this._localforage.getItem(rootKey) + .then(data => { + if(LocalStore.isObject(data)){ + // update data -> delete nested prop + LocalStore.deleteObjProp(data, propArray); + return data; + }else{ + // rootKey not found -> nothing to delete + return Promise.reject(new RangeError('No data found for key: ' + rootKey)); + } + }) + .then(value => this._localforage.setItem(rootKey, value, successCallback)) + .catch(e => this.debug('removeItem() error',e)); + }; + } + + return this._processQueue.enqueue(() => removeItem()); + } + + /** + * clear all items in store + * @param successCallback + * @returns {Promise} + */ + clear(successCallback = undefined){ + return this._processQueue.enqueue(() => this._localforage.clear(successCallback)); + } + + /** + * get number of keys in store + * @param successCallback + * @returns {Promise} + */ + length(successCallback = undefined){ + return this._processQueue.enqueue(() => this._localforage.length(successCallback)); + } + + /** + * Get the name of a key based on its index + * @param keyIndex + * @param successCallback + * @returns {Promise|void} + */ + key(keyIndex, successCallback = undefined){ + return this._processQueue.enqueue(() => this._localforage.key(keyIndex, successCallback)); + } + + /** + * get list of all keys in store + * @param successCallback + * @returns {Promise|void} + */ + keys(successCallback = undefined){ + return this._processQueue.enqueue(() => this._localforage.keys(successCallback)); + } + + /** + * drop current LocalForage instance + * -> removes this from LocalStoreManager + * @returns {Promise|void} + */ + dropInstance(){ + return this._processQueue.enqueue(() => + this._localforage.dropInstance().then(() => this._manager.deleteStore(this._config.name)) + ); + } + + /** + * set LocalStoreManager for this instance + * @param {LocalStoreManager} manager + */ + setManager(manager){ + if(manager instanceof LocalStoreManager){ + this._manager = manager; + }else{ + throw new TypeError('Parameter must be instance of LocalStoreManager. Type of "' + typeof manager + '" given'); + } + } + + /** + * check if key is Int or String with Int at pos 0 + * -> prefix key + * @param key + * @returns {string} + */ + fixKey(key){ + if(LocalStore.isString(this.scope) && this.scope.length){ + key = [this.scope, key].join('.'); + } + + if( + Number.isInteger(key) || + (LocalStore.isString(key) && parseInt(key.charAt(0), 10)) + ){ + key = [this._config.name, key].join('_'); + } + return key; + } + + /** + * find data from obj prop + * -> deep object search + * @param obj + * @param propArray + * @returns {null|*} + */ + static findObjProp(obj, propArray){ + let [head, ...rest] = propArray; + if(!rest.length){ + return obj[head]; + }else{ + if(LocalStore.isObject(obj[head])){ + return LocalStore.findObjProp(obj[head], rest); + }else{ + return null; + } + } + } + + /** + * update/extend obj with new value + * -> deep object manipulation + * @param obj + * @param value + * @param propArray + */ + static updateObjProp(obj, value, propArray){ + let [head, ...rest] = propArray; + if(!rest.length){ + obj[head] = value; + }else{ + if(!LocalStore.isObject(obj[head])) obj[head] = {}; + LocalStore.updateObjProp(obj[head], value, rest); + } + } + + /** + * delete object prop by propArray path + * -> deep object delete + * @param obj + * @param propArray + */ + static deleteObjProp(obj, propArray){ + let [head, ...rest] = propArray; + if(!rest.length){ + delete obj[head]; + }else{ + if(LocalStore.isObject(obj[head])){ + LocalStore.deleteObjProp(obj[head], rest); + } + } + } + + /** + * converts string key to array + * @param propPath + * @returns {*|string[]} + */ + static keyToArray(propPath){ + return propPath.split('.'); + } + + /** + * build DB name + * @param name + * @returns {string} + */ + static buildDbName(name){ + return [LocalStore.dbNamePrefix, name].join(' '); + } + + /** + * check var for Object + * @param obj + * @returns {boolean|boolean} + */ + static isObject(obj){ + return (!!obj) && (obj.constructor === Object); + } + + /** + * check var for Array + * @param arr + * @returns {boolean} + */ + static isArray(arr){ + return (!!arr) && (arr.constructor === Array); + } + + /** + * check var for String + * @param str + * @returns {boolean} + */ + static isString(str){ + return typeof str === 'string'; + } + } + + LocalStore.defaultConfig = { + name: 'default', // custom unique name for identification + debug: false + }; + + LocalStore.dbNamePrefix = 'PathfinderDB'; + LocalStore.LocalForageConfig = { + driver: [LocalForage.INDEXEDDB, LocalForage.WEBSQL, LocalForage.LOCALSTORAGE], + name: LocalStore.dbNamePrefix + }; + + /** + * An instance of LocalStoreManager() handles multiple LocalStore()´s + * -> LocalStore()´s can be set()/delete() from LocalStore() instance + */ + class LocalStoreManager { + + constructor(){ + if(!this.constructor.instance){ + this._store = new Map(); + this.constructor.instance = this; + } + + return this.constructor.instance; + } + + /** + * get LocalStore instance by name + * @param name + * @returns {LocalStore} + */ + getStore(name){ + return this.newStore(name); + } + + /** + * get either existing LocalStore instance + * -> or create new instance + * @param name + * @returns {LocalStore|undefined} + */ + newStore(name){ + if(!this._store.has(name)){ + let store = new LocalStore({ + name: name + }, { + name: LocalStore.buildDbName(name) + }); + store.setManager(this); + this._store.set(name, store); + } + return this._store.get(name); + } + + /** + * removes LocalStore instance from Manager + * -> this will not drop LocalForage instance! + * check LocalStore.dropInstance() for graceful delete + * @param name + * @returns {boolean} + */ + deleteStore(name){ + return this._store.delete(name); + } + } + + return new LocalStoreManager(); +}); \ No newline at end of file diff --git a/js/app/lib/prototypes.js b/js/app/lib/prototypes.js index 3ec05362..b63e10eb 100644 --- a/js/app/lib/prototypes.js +++ b/js/app/lib/prototypes.js @@ -1,6 +1,44 @@ -define([], () => { +define([ + 'app/lib/dataStore' +], (DataStore) => { 'use strict'; + // DOM node data store ============================================================================================ + window.dataStore = new DataStore(); + + /** + * @param key + * @param value + * @returns {HTMLElement} + */ + HTMLElement.prototype.setData = function(key, value){ + return window.dataStore.set(this, key, value); + }; + + /** + * @param key + * @returns {*} + */ + HTMLElement.prototype.getData = function(key){ + return window.dataStore.get(this, key); + }; + + /** + * @param key + * @returns {*} + */ + HTMLElement.prototype.hasData = function(key){ + return window.dataStore.has(this, key); + }; + + /** + * @param key + * @returns {*} + */ + HTMLElement.prototype.removeData = function(key){ + return window.dataStore.remove(this, key); + }; + /** * Array diff * [1,2,3,4,5].diff([4,5,6]) => [1,2,3] diff --git a/js/app/logging.js b/js/app/logging.js index dd85f9f1..13a6260a 100644 --- a/js/app/logging.js +++ b/js/app/logging.js @@ -37,6 +37,7 @@ define([ if(logDialog.length){ // dialog is open let statusArea = logDialog.find('.' + config.taskDialogStatusAreaClass); + statusArea.destroyTooltips(true); requirejs(['text!templates/modules/sync_status.html', 'mustache'], (templateSyncStatus, Mustache) => { let data = { timestampCounterClass: config.timestampCounterClass, @@ -57,7 +58,7 @@ define([ let counterElements = syncStatusElement.find('.' + config.timestampCounterClass); Counter.initTimestampCounter(counterElements); - syncStatusElement.initTooltips({ + statusArea.initTooltips({ placement: 'right' }); }); @@ -92,9 +93,9 @@ define([ // init log table logDataTable = logTable.DataTable({ - dom: '<"row"<"col-xs-3"l><"col-xs-5"B><"col-xs-4"fS>>' + - '<"row"<"col-xs-12"tr>>' + - '<"row"<"col-xs-5"i><"col-xs-7"p>>', + dom: '<"flex-row flex-between"<"flex-col"l><"flex-col"B><"flex-col"fS>>' + + '<"flex-row"<"flex-col flex-grow"tr>>' + + '<"flex-row flex-between"<"flex-col"i><"flex-col"p>>', buttons: { name: 'tableTools', buttons: [ @@ -286,23 +287,24 @@ define([ parseTime: false, ymin: 0, yLabelFormat: labelYFormat, - padding: 10, + padding: 8, hideHover: true, - pointSize: 3, + pointSize: 2.5, lineColors: ['#375959'], pointFillColors: ['#477372'], pointStrokeColors: ['#313335'], - lineWidth: 2, - grid: false, + lineWidth: 1.5, + grid: true, gridStrokeWidth: 0.3, gridTextSize: 9, gridTextFamily: 'Oxygen Bold', gridTextColor: '#63676a', - behaveLikeLine: true, + behaveLikeLine: false, goals: [], + goalStrokeWidth: 1, goalLineColors: ['#66c84f'], smooth: false, - fillOpacity: 0.3, + fillOpacity: 0.2, resize: true }); diff --git a/js/app/login.js b/js/app/login.js index 170dffdc..75a386f8 100644 --- a/js/app/login.js +++ b/js/app/login.js @@ -529,16 +529,16 @@ define([ setVersionLinkObserver(); // mark panel as "shown" - Util.getLocalStorage().setItem(storageKey, currentVersion); + Util.getLocalStore('default').setItem(storageKey, currentVersion); } }); }); }; - Util.getLocalStorage().getItem(storageKey).then(function(data){ + Util.getLocalStore('default').getItem(storageKey).then(data => { // check if panel was shown before if(data){ - if(data !== this.version){ + if(data !== currentVersion){ // show current panel showNotificationPanel(); } @@ -546,9 +546,7 @@ define([ // show current panel showNotificationPanel(); } - }.bind({ - version: currentVersion - })); + }); }; /** diff --git a/js/app/map/contextmenu.js b/js/app/map/contextmenu.js index 55c01859..502b043a 100644 --- a/js/app/map/contextmenu.js +++ b/js/app/map/contextmenu.js @@ -10,6 +10,7 @@ define([ 'use strict'; let config = { + contextMenuContainerId: 'pf-contextmenu-container', // id for container element that holds (hidden) context menus mapContextMenuId: 'pf-map-contextmenu', // id for "maps" context menu connectionContextMenuId: 'pf-map-connection-contextmenu', // id for "connections" context menu endpointContextMenuId: 'pf-map-endpoint-contextmenu', // id for "endpoints" context menu diff --git a/js/app/map/local.js b/js/app/map/local.js index 9b693e34..1c51675c 100644 --- a/js/app/map/local.js +++ b/js/app/map/local.js @@ -100,7 +100,7 @@ define([ let isOpenStatus = isOpen(overlayMain); // store current state in indexDB (client) - MapUtil.storeLocalData('map', mapId, 'showLocal', !isOpenStatus ); + Util.getLocalStore('map').setItem(`${mapId}.showLocal`, !isOpenStatus); // trigger open/close if( isOpenStatus ){ @@ -111,8 +111,8 @@ define([ }); // trigger table re-draw() ------------------------------------------------------------------------------------ - let mapWrapper = overlay.parents('.' + MapUtil.config.mapWrapperClass); - mapWrapper.on('pf:mapResize', function(e){ + let areaMap = overlay.closest('.' + Util.getMapTabContentAreaClass('map')); + areaMap.on('pf:mapResize', function(e){ let tableElement = overlay.find('.' + config.overlayLocalTableClass); let tableApi = tableElement.DataTable(); tableApi.draw('full-hold'); @@ -120,7 +120,6 @@ define([ // tooltips --------------------------------------------------------------------------------------------------- overlayMain.initTooltips({ - container: 'body', placement: 'bottom' }); }; @@ -247,8 +246,7 @@ define([ // open Overlay ------------------------------------------------------------------------------------------- if( !isOpen(overlay) ){ - let promiseStore = MapUtil.getLocaleData('map', mapId); - promiseStore.then(dataStore => { + Util.getLocalStore('map').getItem(mapId).then(dataStore => { if( dataStore && dataStore.showLocal @@ -358,12 +356,12 @@ define([ // init local table --------------------------------------------------------------------------------------- table.on('preDraw.dt', function(e, settings){ let table = $(this); - let mapWrapper = table.parents('.' + MapUtil.config.mapWrapperClass); + let areaMap = table.closest('.' + Util.getMapTabContentAreaClass('map')); - // mapWrapper should always exist - if(mapWrapper && mapWrapper.length) { + // areaMap should always exist + if(areaMap && areaMap.length) { // check available maxHeight for "locale" table based on current map height (resizable) - let mapHeight = mapWrapper[0].offsetHeight; + let mapHeight = areaMap[0].offsetHeight; let localOverlay = MapOverlayUtil.getMapOverlay(table, 'local'); let paginationElement = localOverlay.find('.dataTables_paginate'); @@ -401,7 +399,6 @@ define([ table.on('draw.dt', function(e, settings){ // init table tooltips $(this).find('td').initTooltips({ - container: 'body', placement: 'left' }); }); @@ -410,7 +407,6 @@ define([ table.on('init.dt', function(){ // init table head tooltips $(this).initTooltips({ - container: 'body', placement: 'top' }); }); diff --git a/js/app/map/map.js b/js/app/map/map.js index 045066ff..e379e6d8 100644 --- a/js/app/map/map.js +++ b/js/app/map/map.js @@ -27,9 +27,6 @@ define([ zIndexCounter: 110, maxActiveConnections: 8, - mapWrapperClass: 'pf-map-wrapper', // wrapper div (scrollable) - - mapClass: 'pf-map', // class for all maps mapIdPrefix: 'pf-map-', // id prefix for all maps systemClass: 'pf-system', // class for all systems systemActiveClass: 'pf-system-active', // class for an active system on a map @@ -45,7 +42,6 @@ define([ systemBodyItemStatusClass: 'pf-user-status', // class for player status in system body systemBodyItemNameClass: 'pf-system-body-item-name', // class for player name in system body systemBodyRightClass: 'pf-system-body-right', // class for player ship name in system body - dynamicElementWrapperId: 'pf-dialog-wrapper', // wrapper div for dynamic content (dialogs, context-menus,...) // endpoint classes endpointSourceClass: 'pf-map-endpoint-source', @@ -578,7 +574,7 @@ define([ * @param system */ let systemActions = (action, system) => { - let mapContainer = system.closest('.' + config.mapClass); + let mapContainer = system.closest('.' + Util.config.mapClass); let map = MapUtil.getMapInstance(system.attr('data-mapid')); let systemData = {}; @@ -685,8 +681,7 @@ define([ let filterScope = action.split('_')[1]; let filterScopeLabel = MapUtil.getScopeInfoForConnection( filterScope, 'label'); - let promiseStore = MapUtil.getLocaleData('map', mapId); - promiseStore.then(data => { + Util.getLocalStore('map').getItem(mapId).then(data => { let filterScopes = []; if(data && data.filterScopes){ filterScopes = data.filterScopes; @@ -704,7 +699,7 @@ define([ } // store filterScopes in IndexDB - MapUtil.storeLocalData('map', mapId, 'filterScopes', filterScopes); + Util.getLocalStore('map').setItem(`${mapId}.filterScopes`, filterScopes); MapUtil.filterMapByScopes(map, filterScopes); Util.showNotify({title: 'Scope filter changed', text: filterScopeLabel, type: 'success'}); @@ -1085,17 +1080,17 @@ define([ }; /** - * set map wrapper observer - * @param mapWrapper + * set map area observer + * @param areaMap * @param mapConfig */ - let setMapWrapperObserver = (mapWrapper, mapConfig) => { + let setMapAreaObserver = (areaMap, mapConfig) => { /** * save current map dimension to local storage * @param entry */ - let saveMapSize = (entry) => { + let saveMapSize = entry => { let width = ''; let height = ''; if(entry.constructor.name === 'HTMLDivElement'){ @@ -1109,10 +1104,9 @@ define([ width = parseInt(width.substring(0, width.length - 2)) || 0; height = parseInt(height.substring(0, height.length - 2)) || 0; - mapWrapper.trigger('pf:mapResize'); + areaMap.trigger('pf:mapResize'); - let promiseStore = MapUtil.getLocaleData('map', mapConfig.config.id ); - promiseStore.then((data) => { + Util.getLocalStore('map').getItem(mapConfig.config.id).then((data) => { let storeData = true; if( @@ -1125,7 +1119,7 @@ define([ } if(storeData){ - MapUtil.storeLocalData('map', mapConfig.config.id, 'style', { + Util.getLocalStore('map').setItem(`${mapConfig.config.id}.style`, { width: width, height: height }); @@ -1148,7 +1142,7 @@ define([ } }); - wrapperResize.observe(mapWrapper[0]); + wrapperResize.observe(areaMap[0]); }else if(requestAnimationFrame){ // ResizeObserver() not supported let checkMapSize = (entry) => { @@ -1156,17 +1150,18 @@ define([ return setTimeout(checkMapSize, 500, entry); }; - checkMapSize(mapWrapper[0]); + checkMapSize(areaMap[0]); } }; /** * get a mapMapElement - * @param parentElement + * @param areaMap * @param mapConfig * @returns {Promise} */ - let newMapElement = (parentElement, mapConfig) => { + let newMapElement = (areaMap, mapConfig) => { + areaMap = $(areaMap); /** * new map element promise @@ -1175,45 +1170,37 @@ define([ */ let newMapElementExecutor = (resolve, reject) => { // get map dimension from local storage - let promiseStore = MapUtil.getLocaleData('map', mapConfig.config.id ); - promiseStore.then((data) => { + Util.getLocalStore('map').getItem(mapConfig.config.id).then(data => { let height = 0; if(data && data.style){ height = data.style.height; } - // create map wrapper - let mapWrapper = $('
', { - class: config.mapWrapperClass, - height: height - }); + areaMap.css('height', height); - setMapWrapperObserver(mapWrapper, mapConfig); + setMapAreaObserver(areaMap, mapConfig); let mapId = mapConfig.config.id; // create new map container let mapContainer = $('
', { id: config.mapIdPrefix + mapId, - class: config.mapClass + class: Util.config.mapClass }).data('id', mapId); - mapWrapper.append(mapContainer); - - // append mapWrapper to parent element (at the top) - parentElement.prepend(mapWrapper); + areaMap.append(mapContainer); // set main Container for current map -> the container exists now in DOM !! very important mapConfig.map.setContainer(mapContainer); // init custom scrollbars and add overlay - initMapScrollbar(mapWrapper); + initMapScrollbar(areaMap); // set map observer setMapObserver(mapConfig.map); // set shortcuts - mapWrapper.setMapShortcuts(); + areaMap.setMapShortcuts(); // show static overlay actions let mapOverlay = MapOverlayUtil.getMapOverlay(mapContainer, 'info'); @@ -1417,8 +1404,7 @@ define([ */ let filterMapByScopes = payload => { let filterMapByScopesExecutor = resolve => { - let promiseStore = MapUtil.getLocaleData('map', payload.data.mapConfig.config.id); - promiseStore.then(dataStore => { + Util.getLocalStore('map').getItem(payload.data.mapConfig.config.id).then(dataStore => { let scopes = []; if(dataStore && dataStore.filterScopes){ scopes = dataStore.filterScopes; @@ -1439,8 +1425,7 @@ define([ */ let showInfoSignatureOverlays = payload => { let showInfoSignatureOverlaysExecutor = resolve => { - let promiseStore = MapUtil.getLocaleData('map', payload.data.mapConfig.config.id); - promiseStore.then(dataStore => { + Util.getLocalStore('map').getItem(payload.data.mapConfig.config.id).then(dataStore => { if(dataStore && dataStore.mapSignatureOverlays){ MapOverlay.showInfoSignatureOverlays($(payload.data.mapConfig.map.getContainer())); } @@ -1625,7 +1610,6 @@ define([ title: 'System alias', placement: 'top', onblur: 'submit', - container: 'body', toggle: 'manual', // is triggered manually on dblClick showbuttons: false }); @@ -1751,7 +1735,7 @@ define([ options.id = MapContextMenu.config.systemContextMenuId; options.selectCallback = systemActions; - let mapContainer = system.closest('.' + config.mapClass); + let mapContainer = system.closest('.' + Util.config.mapClass); // hidden menu actions if(system.data('locked') === true){ @@ -1795,8 +1779,7 @@ define([ let mapContainer = $(map.getContainer()); // active menu actions - let promiseStore = MapUtil.getLocaleData('map', mapContainer.data('id')); - promiseStore.then(dataStore => { + Util.getLocalStore('map').getItem(mapContainer.data('id')).then(dataStore => { if(dataStore && dataStore.filterScopes){ options.active = dataStore.filterScopes.map(scope => 'filter_' + scope); } @@ -2005,10 +1988,9 @@ define([ let systemTooltipOptions = { toggle: 'tooltip', placement: 'right', - container: 'body', viewport: system.id }; - system.find('.fas').tooltip(systemTooltipOptions); + //system.find('.fas').tooltip(systemTooltipOptions); // system click events ======================================================================================== let double = function(e){ @@ -2316,9 +2298,9 @@ define([ // store new zoom level in IndexDB if(zoom === 1){ - MapUtil.deleteLocalData('map', mapId, 'mapZoom'); + Util.getLocalStore('map').removeItem(`${mapId}.mapZoom`); }else{ - MapUtil.storeLocalData('map', mapId, 'mapZoom', zoom); + Util.getLocalStore('map').setItem(`${mapId}.mapZoom`, zoom); } }); @@ -2406,7 +2388,7 @@ define([ e.stopPropagation(); // make sure map is clicked and NOT a connection - if($(e.target).hasClass(config.mapClass)){ + if($(e.target).hasClass(Util.config.mapClass)){ getContextMenuConfig(map).then(payload => { let context = { component: map @@ -2532,6 +2514,20 @@ define([ selector: '.' + config.systemClass + ' .' + config.systemHeadExpandClass }); + mapContainer.hoverIntent({ + over: function(e){ + $(this).tooltip({ + trigger: 'manual', + placement: 'right', + viewport: this.closest(`.${config.systemClass}`) + }).tooltip('show'); + }, + out: function(e){ + $(this).tooltip('destroy'); + }, + selector: `.${config.systemClass} .fas[title]` + }); + // system "active users" popover ------------------------------------------------------------------------------ mapContainer.hoverIntent({ over: function(e){ @@ -2611,8 +2607,7 @@ define([ // get map menu config options let mapOption = mapOptions[data.option]; - let promiseStore = MapUtil.getLocaleData('map', mapContainer.data('id')); - promiseStore.then(function(dataStore){ + Util.getLocalStore('map').getItem(mapContainer.data('id')).then(function(dataStore){ let notificationText = 'disabled'; let button = $('#' + this.mapOption.buttonId); let dataExists = false; @@ -2643,7 +2638,7 @@ define([ MapOverlayUtil.getMapOverlay(this.mapContainer, 'info').updateOverlayIcon(this.data.option, 'hide'); // delete map option - MapUtil.deleteLocalData('map', this.mapContainer.data('id'), this.data.option); + Util.getLocalStore('map').removeItem(`${this.mapContainer.data('id')}.${this.data.option}`); }else{ // toggle button class button.addClass('active'); @@ -2662,7 +2657,7 @@ define([ MapOverlayUtil.getMapOverlay(this.mapContainer, 'info').updateOverlayIcon(this.data.option, 'show'); // store map option - MapUtil.storeLocalData('map', this.mapContainer.data('id'), this.data.option, 1); + Util.getLocalStore('map').setItem(`${this.mapContainer.data('id')}.${this.data.option}`, 1); notificationText = 'enabled'; } @@ -2701,8 +2696,8 @@ define([ } if(select){ - let mapWrapper = mapContainer.closest('.' + config.mapWrapperClass); - Scrollbar.scrollToCenter(mapWrapper, system); + let areaMap = mapContainer.closest('.' + Util.getMapTabContentAreaClass('map')); + Scrollbar.scrollToCenter(areaMap, system); // select system MapUtil.showSystemInfo(map, system); } @@ -3135,12 +3130,12 @@ define([ /** * load OR updates system map - * @param tabContentElement parent element where the map will be loaded + * @param areaMap parent element where the map will be loaded * @param mapConfig * @param options * @returns {Promise} */ - let loadMap = (tabContentElement, mapConfig, options) => { + let loadMap = (areaMap, mapConfig, options) => { /** * load map promise @@ -3155,7 +3150,7 @@ define([ if(mapConfig.map.getContainer() === undefined){ // map not loaded -> create & update - newMapElement(tabContentElement, mapConfig) + newMapElement(areaMap, mapConfig) .then(payload => updateMap(payload.data.mapConfig)) .then(payload => resolve(payload)); }else{ @@ -3172,14 +3167,14 @@ define([ /** * init scrollbar for Map element - * @param mapWrapper + * @param areaMap */ - let initMapScrollbar = mapWrapper => { - let mapElement = mapWrapper.find('.' + config.mapClass); + let initMapScrollbar = areaMap => { + let mapElement = areaMap.find('.' + Util.config.mapClass); let mapId = mapElement.data('id'); let dragSelect; - Scrollbar.initScrollbar(mapWrapper, { + Scrollbar.initScrollbar(areaMap, { callbacks: { onInit: function(){ let scrollWrapper = this; @@ -3215,7 +3210,7 @@ define([ let animationFrameId = 0; let toggleDragScroll = active => { - mapElement.toggleClass('disabled', active).toggleClass(' pf-map-move', active); + mapElement.toggleClass('disabled', active).toggleClass('pf-map-move', active); }; let stopDragScroll = () => { @@ -3303,7 +3298,7 @@ define([ mapElement.attr('data-scroll-top', this.mcs.top); // store new map scrollOffset -> localDB - MapUtil.storeLocalData('map', mapId, 'scrollOffset', { + Util.getLocalStore('map').setItem(`${mapId}.scrollOffset`, { x: Math.abs(this.mcs.left), y: Math.abs(this.mcs.top) }); @@ -3326,8 +3321,8 @@ define([ // ------------------------------------------------------------------------------------------------------------ // add map overlays after scrollbar is initialized // because of its absolute position - mapWrapper.initMapOverlays(); - mapWrapper.initLocalOverlay(mapId); + areaMap.initMapOverlays(); + areaMap.initLocalOverlay(mapId); }; return { diff --git a/js/app/map/overlay/overlay.js b/js/app/map/overlay/overlay.js index cc408159..f5e0b988 100644 --- a/js/app/map/overlay/overlay.js +++ b/js/app/map/overlay/overlay.js @@ -17,7 +17,7 @@ define([ * @returns {*} */ let getMapObjectFromOverlayIcon = overlayIcon => { - return MapUtil.getMapInstance(Util.getMapElementFromOverlay(overlayIcon).data('id')); + return MapUtil.getMapInstance(MapOverlayUtil.getMapElementFromOverlay(overlayIcon).data('id')); }; /** @@ -323,7 +323,7 @@ define([ iconClass: ['fas', 'fa-fw', 'fa-filter'], onClick: function(e){ // clear all filter - let mapElement = Util.getMapElementFromOverlay(this); + let mapElement = MapOverlayUtil.getMapElementFromOverlay(this); let map = getMapObjectFromOverlayIcon(this); MapUtil.storeLocalData('map', mapElement.data('id'), 'filterScopes', []); @@ -349,7 +349,7 @@ define([ iconClass: ['fas', 'fa-fw', 'fa-tags'], hoverIntent: { over: function(e){ - let mapElement = Util.getMapElementFromOverlay(this); + let mapElement = MapOverlayUtil.getMapElementFromOverlay(this); mapElement.find('.' + MapOverlayUtil.config.systemHeadClass).each(function(){ let systemHead = $(this); // init popover if not already exists @@ -373,7 +373,7 @@ define([ }); }, out: function(e){ - let mapElement = Util.getMapElementFromOverlay(this); + let mapElement = MapOverlayUtil.getMapElementFromOverlay(this); mapElement.find('.' + MapOverlayUtil.config.systemHeadClass).popover('hide'); } } @@ -556,7 +556,7 @@ define([ duration: Init.animationSpeed.mapOverlay, complete: function(){ counterChart.data('interval', false); - Util.getMapElementFromOverlay(mapOverlayTimer).trigger('pf:unlocked'); + MapOverlayUtil.getMapElementFromOverlay(mapOverlayTimer).trigger('pf:unlocked'); } }); } diff --git a/js/app/map/overlay/util.js b/js/app/map/overlay/util.js index 21fc49ab..379a5e02 100644 --- a/js/app/map/overlay/util.js +++ b/js/app/map/overlay/util.js @@ -13,8 +13,6 @@ define([ let config = { logTimerCount: 3, // map log timer in seconds - mapWrapperClass: 'pf-map-wrapper', // wrapper div (scrollable) - // map overlays sections mapOverlayClass: 'pf-map-overlay', // class for all map overlays mapOverlayTimerClass: 'pf-map-overlay-timer', // class for map overlay timer e.g. map timer @@ -53,27 +51,36 @@ define([ * @returns {null} */ let getMapOverlay = (element, overlayType) => { - let mapWrapperElement = $(element).parents('.' + config.mapWrapperClass); + let areaMap = $(element).closest('.' + Util.getMapTabContentAreaClass('map')); let mapOverlay = null; switch(overlayType){ case 'timer': - mapOverlay = mapWrapperElement.find('.' + config.mapOverlayTimerClass); + mapOverlay = areaMap.find('.' + config.mapOverlayTimerClass); break; case 'info': - mapOverlay = mapWrapperElement.find('.' + config.mapOverlayInfoClass); + mapOverlay = areaMap.find('.' + config.mapOverlayInfoClass); break; case 'zoom': - mapOverlay = mapWrapperElement.find('.' + config.mapOverlayZoomClass); + mapOverlay = areaMap.find('.' + config.mapOverlayZoomClass); break; case 'local': - mapOverlay = mapWrapperElement.find('.' + config.overlayLocalClass); + mapOverlay = areaMap.find('.' + config.overlayLocalClass); break; } return mapOverlay; }; + /** + * get mapElement from overlay or any child of that + * @param mapOverlay + * @returns {jQuery} + */ + let getMapElementFromOverlay = mapOverlay => { + return $(mapOverlay).closest('.' + Util.getMapTabContentAreaClass('map')).find('.' + Util.config.mapClass); + }; + /** * get the map counter chart from overlay * @param element @@ -91,6 +98,7 @@ define([ return { config: config, getMapOverlay: getMapOverlay, + getMapElementFromOverlay: getMapElementFromOverlay, getMapCounter: getMapCounter, getMapOverlayInterval: getMapOverlayInterval }; diff --git a/js/app/map/scrollbar.js b/js/app/map/scrollbar.js index 9a072db5..b19e4d36 100644 --- a/js/app/map/scrollbar.js +++ b/js/app/map/scrollbar.js @@ -226,25 +226,25 @@ define([ /** * scroll to a specific position on map * demo: http://manos.malihu.gr/repository/custom-scrollbar/demo/examples/scrollTo_demo.html - * @param scrollWrapper + * @param scrollArea * @param position * @param options */ - let scrollToPosition = (scrollWrapper, position, options) => { - $(scrollWrapper).mCustomScrollbar('scrollTo', position, options); + let scrollToPosition = (scrollArea, position, options) => { + $(scrollArea).mCustomScrollbar('scrollTo', position, options); }; /** * scroll to center an element * -> subtract some offset for tooltips/connections - * @param scrollWrapper + * @param scrollArea * @param element */ - let scrollToCenter = (scrollWrapper, element) => { + let scrollToCenter = (scrollArea, element) => { // no scroll if element is already FULL visible in scrollable viewport if(!isInView(element)){ // get scrollTo position for centered element - scrollToPosition(scrollWrapper, getCenterScrollPosition(element)); + scrollToPosition(scrollArea, getCenterScrollPosition(element)); } }; diff --git a/js/app/map/system.js b/js/app/map/system.js index f04c9836..bc6daf9c 100644 --- a/js/app/map/system.js +++ b/js/app/map/system.js @@ -19,8 +19,6 @@ define([ y: 0 }, - mapClass: 'pf-map', // class for all maps - systemHeadInfoClass: 'pf-system-head-info', // class for system info systemHeadInfoLeftClass: 'pf-system-head-info-left', // class for left system info systemHeadInfoRightClass: 'pf-system-head-info-right', // class for right system info @@ -618,7 +616,7 @@ define([ html: true, animation: true, template: template, - viewport: system.closest('.' + config.mapClass) + viewport: system.closest('.' + Util.config.mapClass) }; // init new tooltip -> Do not show automatic maybe system is currently dragged diff --git a/js/app/map/util.js b/js/app/map/util.js index 3edb62ec..31f424eb 100644 --- a/js/app/map/util.js +++ b/js/app/map/util.js @@ -18,14 +18,6 @@ define([ zoomMax: 1.5, zoomMin: 0.5, - // local storage - characterLocalStoragePrefix: 'character_', // prefix for character data local storage key - mapLocalStoragePrefix: 'map_', // prefix for map data local storage key - mapTabContentClass: 'pf-map-tab-content', // Tab-Content element (parent element) - - mapWrapperClass: 'pf-map-wrapper', // wrapper div (scrollable) - - mapClass: 'pf-map', // class for all maps mapGridClass: 'pf-grid-small', // class for map grid snapping mapCompactClass: 'pf-compact', // class for map compact system UI @@ -500,7 +492,7 @@ define([ connectionData && connectionData.signatures // signature data is required... ){ - let SystemSignatures = require('module/system_signature'); + let SystemSignatureModule = require('module/system_signature'); let sourceEndpoint = connection.endpoints[0]; let targetEndpoint = connection.endpoints[1]; @@ -531,7 +523,7 @@ define([ // ... get endpoint label for source || target system if(tmpSystem && tmpSystem){ // ... get all available signature type (wormholes) names - let availableSigTypeNames = SystemSignatures.getSignatureTypeOptionsBySystem(tmpSystem, 5); + let availableSigTypeNames = SystemSignatureModule.getSignatureTypeOptionsBySystem(tmpSystem, 5); let flattenSigTypeNames = Util.flattenXEditableSelectArray(availableSigTypeNames); if(flattenSigTypeNames.hasOwnProperty(signatureData.typeId)){ @@ -609,7 +601,7 @@ define([ * @param element * @returns {*} */ - let getTabContentElementByMapElement = element => $(element).closest('.' + config.mapTabContentClass); + let getTabContentElementByMapElement = element => $(element).closest('.' + Util.config.mapTabContentClass); /** * checks if there is an "active" connection on a map @@ -753,11 +745,11 @@ define([ 'height': scrollableHeight ? scaledHeight + 'px' : (wrapperHeight) + 'px', }); - let mapWrapperElement = mapContainer.closest('.mCustomScrollbar'); + let areaMap = mapContainer.closest('.mCustomScrollbar'); if(scrollableWidth && scrollableHeight){ - mapWrapperElement.mCustomScrollbar('update'); + areaMap.mCustomScrollbar('update'); }else{ - mapWrapperElement.mCustomScrollbar('scrollTo', '#' + mapContainer.attr('id'), { + areaMap.mCustomScrollbar('scrollTo', '#' + mapContainer.attr('id'), { scrollInertia: 0, scrollEasing: 'linear', timeout: 0, @@ -1001,7 +993,12 @@ define([ setSystemActive(map, system); // get parent Tab Content and fire update event - getTabContentElementByMapElement(system).trigger('pf:drawSystemModules'); + let mapContainer = $(map.getContainer()); + + getTabContentElementByMapElement(mapContainer).trigger('pf:renderSystemModules', { + mapId: parseInt(mapContainer.data('id')), + payload: Util.getCurrentSystemData() + }); }; /** @@ -1015,9 +1012,9 @@ define([ // get parent Tab Content and fire update event let mapContainer = $(map.getContainer()); - getTabContentElementByMapElement(mapContainer).trigger('pf:drawConnectionModules', { - connections: connections, - mapId: parseInt(mapContainer.data('id')) + getTabContentElementByMapElement(mapContainer).trigger('pf:renderConnectionModules', { + mapId: parseInt(mapContainer.data('id')), + payload: connections }); }; @@ -1046,9 +1043,11 @@ define([ let showFindRouteDialog = (mapContainer, systemToData) => { // get parent Tab Content and fire update event getTabContentElementByMapElement(mapContainer).trigger('pf:updateRouteModules', { - task: 'showFindRouteDialog', - systemToData: systemToData, - mapId: parseInt(mapContainer.data('id')) + mapId: parseInt(mapContainer.data('id')), + payload: { + task: 'showFindRouteDialog', + systemToData: systemToData + } }); }; @@ -1059,9 +1058,11 @@ define([ */ let findRoute = (mapContainer, systemToData) => { getTabContentElementByMapElement(mapContainer).trigger('pf:updateRouteModules', { - task: 'findRoute', - systemToData: systemToData, - mapId: parseInt(mapContainer.data('id')) + mapId: parseInt(mapContainer.data('id')), + payload: { + task: 'findRoute', + systemToData: systemToData + } }); }; @@ -1267,84 +1268,6 @@ define([ return scopeInfo; }; - /** - * store local data for current user (IndexDB) - * @param key - * @param value - */ - let storeLocaleCharacterData = (key, value) => { - if(key.length && value){ - let userData = Util.getCurrentUserData(); - if( - userData && - userData.character - ){ - storeLocalData('character', userData.character.id, key, value); - } - } - }; - - /** - * get key prefix for local storage data - * @param type - * @returns {boolean} - */ - let getLocalStoragePrefixByType = (type) => { - let prefix = false; - switch(type){ - case 'character': prefix = config.characterLocalStoragePrefix; break; - case 'map': prefix = config.mapLocalStoragePrefix; break; - default: prefix = config.mapLocalStoragePrefix; - } - return prefix; - }; - - /** - * get stored local data from client cache (IndexedDB) - * @param type - * @param objectId - * @returns {*} - */ - let getLocaleData = (type, objectId) => { - if(objectId > 0){ - let storageKey = getLocalStoragePrefixByType(type) + objectId; - return Util.getLocalStorage().getItem(storageKey); - }else{ - console.warn('Local storage requires object id > 0'); - } - }; - - /** - * store local config data to client cache (IndexedDB) - * @param type - * @param objectId - * @param key - * @param value - */ - let storeLocalData = (type, objectId, key, value) => { - if(objectId > 0){ - // get current map config - let storageKey = getLocalStoragePrefixByType(type) + objectId; - Util.getLocalStorage().getItem(storageKey).then(function(data){ - // This code runs once the value has been loaded - // from the offline store. - data = (data === null) ? {} : data; - // set/update value - data[this.key] = this.value; - Util.getLocalStorage().setItem(this.storageKey, data); - }.bind({ - key: key, - value: value, - storageKey: storageKey - })).catch(function(err){ - // This code runs if there were any errors - console.error('Map local storage can not be accessed!'); - }); - }else{ - console.warn('storeLocalData(): Local storage requires object id > 0'); - } - }; - /** * show map animations when a new map gets visual * @param mapElement @@ -1531,11 +1454,10 @@ define([ // -> implementation would be difficult... if(map.getZoom() === 1){ let mapElement = $(map.getContainer()); - let promiseStore = getLocaleData('map', mapElement.data('id')); - promiseStore.then(data => { + Util.getLocalStore('map').getItem(mapElement.data('id')).then(data => { if(data && data.scrollOffset){ - let mapWrapper = mapElement.parents('.' + config.mapWrapperClass); - Scrollbar.scrollToPosition(mapWrapper, [data.scrollOffset.y, data.scrollOffset.x]); + let areaMap = mapElement.closest('.' + Util.getMapTabContentAreaClass('map')); + Scrollbar.scrollToPosition(areaMap, [data.scrollOffset.y, data.scrollOffset.x]); } resolve(payload); @@ -1557,8 +1479,7 @@ define([ let zoomToDefaultScaleExecutor = resolve => { let mapElement = $(map.getContainer()); - let promiseStore = getLocaleData('map', mapElement.data('id')); - promiseStore.then(data => { + Util.getLocalStore('map').getItem(mapElement.data('id')).then(data => { if(data && data.mapZoom){ setZoom(map, data.mapZoom); } @@ -1573,32 +1494,6 @@ define([ return new Promise(zoomToDefaultScaleExecutor); }; - /** - * delete local map configuration by key (IndexedDB) - * @param type - * @param objectId - * @param key - */ - let deleteLocalData = (type, objectId, key) => { - if(objectId > 0){ - // get current map config - let storageKey = getLocalStoragePrefixByType(type) + objectId; - Util.getLocalStorage().getItem(storageKey).then(function(data){ - if( - data && - data.hasOwnProperty(key) - ){ - delete data[key]; - Util.getLocalStorage().setItem(this.storageKey, data); - } - }.bind({ - storageKey: storageKey - })); - }else{ - console.warn('deleteLocalData(): Local storage requires object id > 0'); - } - }; - /** * set or change rallyPoint for systems * @param rallyUpdated @@ -1650,8 +1545,7 @@ define([ // check if desktop notification was already send let mapId = system.data('mapid'); let systemId = system.data('id'); - let promiseStore = getLocaleData('map', mapId); - promiseStore.then(function(data){ + Util.getLocalStore('map').getItem(mapId).then(function(data){ // This code runs once the value has been loaded // from the offline store. let rallyPokeData = {}; @@ -1669,7 +1563,7 @@ define([ rallyPokeData[this.systemId] !== rallyUpdated // already send to that system but in the past ){ rallyPokeData[this.systemId] = rallyUpdated; - storeLocalData('map', this.mapId, 'rallyPoke', rallyPokeData); + Util.getLocalStore('map').setItem(`${this.mapId}.rallyPoke`, rallyPokeData); notificationOptions.type = 'info'; Util.showNotify(notificationOptions, { @@ -1686,7 +1580,7 @@ define([ } // update active "route" module -> add rally point row -------------------------------------------- - let mapContainer = system.parents('.' + config.mapClass); + let mapContainer = system.parents('.' + Util.config.mapClass); findRoute(mapContainer, { systemId: system.data('systemId'), name: system.data('name'), @@ -1711,28 +1605,27 @@ define([ * set map "shortcut" events */ $.fn.setMapShortcuts = function(){ - return this.each((i, mapWrapper) => { - mapWrapper = $(mapWrapper); - let mapElement = mapWrapper.find('.' + config.mapClass); + return this.each((i, areaMap) => { + areaMap = $(areaMap); + let mapElement = areaMap.find('.' + Util.config.mapClass); // 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')); + let map = Map.getMapInstance(mapElement.data('id')); - mapWrapper.watchKey('mapSystemAdd', (mapWrapper) => { + areaMap.watchKey('mapSystemAdd', areaMap => { System.showNewSystemDialog(map, {position: {x: 0, y: 0}}, Map.saveSystemCallback); },{focus: true}); - mapWrapper.watchKey('mapSystemsSelect', (mapWrapper) => { + areaMap.watchKey('mapSystemsSelect', areaMap => { mapElement.selectAllSystems(); },{focus: true}); - mapWrapper.watchKey('mapSystemsDelete', (mapWrapper) => { + areaMap.watchKey('mapSystemsDelete', areaMap => { let selectedSystems = mapElement.getSelectedSystems(); $.fn.showDeleteSystemDialog(map, selectedSystems); },{focus: true}); - }); }; @@ -2309,10 +2202,6 @@ define([ changeZoom: changeZoom, setZoom: setZoom, toggleSystemAliasEditable: toggleSystemAliasEditable, - storeLocaleCharacterData: storeLocaleCharacterData, - getLocaleData: getLocaleData, - storeLocalData: storeLocalData, - deleteLocalData: deleteLocalData, visualizeMap: visualizeMap, setMapDefaultOptions: setMapDefaultOptions, getSystemPosition: getSystemPosition, diff --git a/js/app/mappage.js b/js/app/mappage.js index e129b841..f0cf008f 100644 --- a/js/app/mappage.js +++ b/js/app/mappage.js @@ -14,602 +14,603 @@ define([ 'app/key', 'app/ui/form_element' ], ($, Init, Util, Logging, Page, MapWorker, MapUtil, ModuleMap) => { - 'use strict'; + // main update intervals/trigger (heartbeat) + let updateTimeouts = { + mapUpdate: 0, + userUpdate: 0 + }; + + // log keys ----------------------------------------------------------------------------------------------- + let logKeyServerMapData = Init.performanceLogging.keyServerMapData; + let logKeyServerUserData = Init.performanceLogging.keyServerUserData; + let locationToggle = $('#' + Util.config.headMapTrackingId); + + let initApp = rootEl => new Promise(resolve => { + Page.renderPage(rootEl) + .then(pageEls => { + // passive event listener + Util.initPassiveEvents(); + + // clear sessionStorage + //Util.clearSessionStorage(); + + // set default tooltip config + Util.initDefaultTooltipConfig(pageEls.pageEl); + + // set default popover config + Util.initDefaultPopoverConfig(pageEls.pageEl); + + // set default confirmation popover config + Util.initDefaultConfirmationConfig(); + + // set default xEditable config + Util.initDefaultEditableConfig(pageEls.pageEl); + + // set default select2 config + Util.initDefaultSelect2Config(); + + // set default dialog config + Util.initDefaultBootboxConfig(); + + // show app information in browser console + Util.showVersionInfo(); + + // init logging + Logging.init(); + + return Promise.resolve(pageEls); + }) + .then(Page.loadPageStructure) + .then(() => resolve(document.getElementById(Util.config.mapModuleId))); + }); + /** - * main init "map" page + * get static init data and store response + * @returns {Promise} */ - $(() => { - Util.initPrototypes(); - - // clear sessionStorage - //Util.clearSessionStorage(); - - // set default AJAX config - Util.ajaxSetup(); - - // set default dialog config - Util.initDefaultBootboxConfig(); - - // set default confirmation popover config - Util.initDefaultConfirmationConfig(); - - // set default select2 config - Util.initDefaultSelect2Config(); - - // set default xEditable config - Util.initDefaultEditableConfig(); - - // load page - Page.loadPageStructure(); - - // show app information in browser console - Util.showVersionInfo(); - - // init logging - Logging.init(); - - let mapModule = $('#' + Util.config.mapModuleId); - - // main update intervals/trigger (heartbeat) - let updateTimeouts = { - mapUpdate: 0, - userUpdate: 0 - }; + let initData = () => { /** - * clear both main update timeouts - * -> stop program from working -> shutdown + * add wormhole size data for each wormhole + * @param wormholes + * @returns {*} */ - let clearUpdateTimeouts = () => { - for(let intervalKey in updateTimeouts){ - if(updateTimeouts.hasOwnProperty(intervalKey)){ - clearTimeout(updateTimeouts[intervalKey]); - } - } - }; - - /** - * Ajax error response handler function for main-ping functions - * @param jqXHR - * @param status - * @param error - */ - let handleAjaxErrorResponse = (jqXHR, status, error) => { - // clear both main update request trigger timer - clearUpdateTimeouts(); - - let reason = status + ' ' + jqXHR.status + ': ' + error; - let errorData = []; - let redirect = false; // redirect user to other page e.g. login - let reload = true; // reload current page (default: true) - - if(jqXHR.responseJSON){ - // handle JSON - let responseObj = jqXHR.responseJSON; - if( - responseObj.error && - responseObj.error.length > 0 - ){ - errorData = responseObj.error; - } - - if(responseObj.reroute){ - redirect = responseObj.reroute; - } - }else{ - // handle HTML - errorData.push({ - type: 'error', - message: 'Please restart and reload this page' - }); - } - - console.error(' ↪ %s Error response: %o', jqXHR.url, errorData); - $(document).trigger('pf:shutdown', { - status: jqXHR.status, - reason: reason, - error: errorData, - redirect: redirect, - reload: reload - }); - }; - - // map init functions ========================================================================================= - - /** - * get static init data and store response - * @returns {Promise} - */ - let initData = () => { - - /** - * add wormhole size data for each wormhole - * @param wormholes - * @returns {*} - */ - let addWormholeSizeData = wormholes => { - for(let [wormholeName, wormholeData] of Object.entries(wormholes)){ - wormholeData.class = Util.getSecurityClassForSystem(wormholeData.security); - - for(let [sizeName, sizeData] of Object.entries(Init.wormholeSizes)){ - if(wormholeData.massIndividual >= sizeData.jumpMassMin){ - wormholeData.size = sizeData; - break; - } + let addWormholeSizeData = wormholes => { + for(let wormholeData of Object.values(wormholes)){ + wormholeData.class = Util.getSecurityClassForSystem(wormholeData.security); + for(let sizeData of Object.values(Init.wormholeSizes)){ + if(wormholeData.massIndividual >= sizeData.jumpMassMin){ + wormholeData.size = sizeData; + break; } } - return wormholes; - }; - - let initDataExecutor = (resolve, reject) => { - $.getJSON(Init.path.initData).done(response => { - if( response.error.length > 0 ){ - for(let i = 0; i < response.error.length; i++){ - Util.showNotify({ - title: response.error[i].title, - text: response.error[i].message, - type: response.error[i].type - }); - } - } - - Init.timer = response.timer; - Init.mapTypes = response.mapTypes; - Init.mapScopes = response.mapScopes; - Init.connectionScopes = response.connectionScopes; - Init.systemStatus = response.systemStatus; - Init.systemType = response.systemType; - Init.wormholes = addWormholeSizeData(response.wormholes); - Init.characterStatus = response.characterStatus; - Init.routes = response.routes; - Init.url = response.url; - Init.character = response.character; - Init.slack = response.slack; - Init.discord = response.discord; - Init.structureStatus = response.structureStatus; - Init.universeCategories = response.universeCategories; - Init.routeSearch = response.routeSearch; - - resolve({ - action: 'initData', - data: false - }); - }).fail((jqXHR, status, error) => { - reject({ - action: 'shutdown', - data: { - jqXHR: jqXHR, - status: status, - error: error - } - }); - }); - }; - - return new Promise(initDataExecutor); + } + return wormholes; }; - /** - * get mapAccess Data for WebSocket subscription - * @returns {Promise} - */ - let getMapAccessData = () => { - - let getMapAccessDataExecutor = (resolve, reject) => { - $.getJSON(Init.path.getAccessData).done(response => { - resolve({ - action: 'mapAccessData', - data: response - }); - }).fail((jqXHR, status, error) => { - reject({ - action: 'shutdown', - data: { - jqXHR: jqXHR, - status: status, - error: error - } - }); - }); - }; - - return new Promise(getMapAccessDataExecutor); - }; - - /** - * init main mapModule - * -> initData() needs to be resolved first! - * @param payload - * @returns {Promise} - */ - let initMapModule = payload => { - - let initMapModuleExecutor = (resolve, reject) => { - // init browser tab change observer, Once the timers are available - Page.initTabChangeObserver(); - - // init hidden context menu elements - Page.renderMapContextMenus(); - - // init map module - mapModule.initMapModule(); - - resolve({ - action: 'initMapModule', - data: false - }); - }; - - return new Promise(initMapModuleExecutor); - }; - - /** - * request all map access data (tokens) -> required wor WebSocket subscription - * -> initData() needs to be resolved first! - * @param payloadMapAccessData - * @returns {Promise} - */ - let initMapWorker = payloadMapAccessData => { - - let initMapWorkerExecutor = (resolve, reject) => { - let getPayload = command => { - return { - action: 'initMapWorker', - data: { - syncStatus: Init.syncStatus.type, - command: command - } - }; - }; - - let validMapAccessData = false; - - if(payloadMapAccessData && payloadMapAccessData.action === 'mapAccessData'){ - let response = payloadMapAccessData.data; - if(response.status === 'OK'){ - validMapAccessData = true; - - // init SharedWorker for maps - MapWorker.init({ - characterId: response.data.id, - callbacks: { - onInit: (MsgWorkerMessage) => { - Util.setSyncStatus(MsgWorkerMessage.command); - - }, - onOpen: (MsgWorkerMessage) => { - Util.setSyncStatus(MsgWorkerMessage.command, MsgWorkerMessage.meta()); - MapWorker.send('subscribe', response.data); - - resolve(getPayload(MsgWorkerMessage.command)); - }, - onGet: (MsgWorkerMessage) => { - switch(MsgWorkerMessage.task()){ - case 'mapUpdate': - Util.updateCurrentMapData(MsgWorkerMessage.data()); - ModuleMap.updateMapModule(mapModule); - break; - case 'mapAccess': - case 'mapDeleted': - Util.deleteCurrentMapData(MsgWorkerMessage.data()); - ModuleMap.updateMapModule(mapModule); - break; - case 'mapSubscriptions': - Util.updateCurrentMapUserData(MsgWorkerMessage.data()); - ModuleMap.updateActiveMapUserData(mapModule); - break; - } - - Util.setSyncStatus('ws:get'); - }, - onClosed: (MsgWorkerMessage) => { - Util.setSyncStatus(MsgWorkerMessage.command, MsgWorkerMessage.meta()); - reject(getPayload(MsgWorkerMessage.command)); - - }, - onError: (MsgWorkerMessage) => { - Util.setSyncStatus(MsgWorkerMessage.command, MsgWorkerMessage.meta()); - reject(getPayload(MsgWorkerMessage.command)); - } - } + let initDataExecutor = (resolve, reject) => { + $.getJSON(Init.path.initData).done(response => { + if(response.error.length > 0){ + for(let i = 0; i < response.error.length; i++){ + Util.showNotify({ + title: response.error[i].title, + text: response.error[i].message, + type: response.error[i].type }); } } - if( !validMapAccessData ){ - reject(getPayload('Invalid mapAccessData')); - } - }; + Init.timer = response.timer; + Init.mapTypes = response.mapTypes; + Init.mapScopes = response.mapScopes; + Init.connectionScopes = response.connectionScopes; + Init.systemStatus = response.systemStatus; + Init.systemType = response.systemType; + Init.wormholes = addWormholeSizeData(response.wormholes); + Init.characterStatus = response.characterStatus; + Init.routes = response.routes; + Init.url = response.url; + Init.plugin = response.plugin; + Init.character = response.character; + Init.slack = response.slack; + Init.discord = response.discord; + Init.structureStatus = response.structureStatus; + Init.universeCategories = response.universeCategories; + Init.routeSearch = response.routeSearch; - return new Promise(initMapWorkerExecutor); + resolve({ + action: 'initData', + data: false + }); + }).fail((jqXHR, status, error) => { + reject({ + action: 'shutdown', + data: { + jqXHR: jqXHR, + status: status, + error: error + } + }); + }); }; - // run all init functions for mainModule and WebSocket configuration async - Promise.all([initData(), getMapAccessData()]) - .then(payload => Promise.all([initMapModule(payload[0]), initMapWorker(payload[1])])) - .then(payload => { - // mapModule initialized and WebSocket configuration working - console.ok('Client syncStatus: %s. %O resolved by command: %s!', - payload[1].data.syncStatus, - payload[1].action + '()', - payload[1].data.command - ); - }) - .catch(payload => { - switch(payload.action){ - case 'shutdown': - // ajax error - handleAjaxErrorResponse(payload.data.jqXHR, payload.data.status, payload.data.error); - break; - case 'initMapWorker': - // WebSocket not working -> no error here -> fallback to Ajax - console.info('Client syncStatus: %s. %O rejected by command: %s! payload: %o', - payload.data.syncStatus, - payload.action + '()', - payload.data.command, - payload.data - ); - break; - default: - console.error('Unhandled error thrown while initialization: %o ', payload); + return new Promise(initDataExecutor); + }; + + /** + * get mapAccess Data for WebSocket subscription + * @returns {Promise} + */ + let getMapAccessData = () => new Promise((resolve, reject) => { + $.getJSON(Init.path.getAccessData).done(accessData => { + resolve(accessData); + }).fail((jqXHR, status, error) => { + reject({ + action: 'shutdown', + data: { + jqXHR: jqXHR, + status: status, + error: error } }); - - /** - * main function for init all map relevant trigger calls - */ - $.fn.initMapModule = function(){ - let mapModule = $(this); - - // log keys ----------------------------------------------------------------------------------------------- - let logKeyServerMapData = Init.performanceLogging.keyServerMapData; - let logKeyServerUserData = Init.performanceLogging.keyServerUserData; - let locationToggle = $('#' + Util.config.headMapTrackingId); - - // ping for main map update =============================================================================== - /** - * @param forceUpdateMapData // force request to be send - */ - let triggerMapUpdatePing = (forceUpdateMapData) => { - - // check each interval if map module is still available - let check = $('#' + mapModule.attr('id')).length; - - if(check === 0){ - // program crash stop any update - return; - } - - // get updated map data - let updatedMapData = { - mapData: ModuleMap.getMapModuleDataForUpdate(mapModule), - getUserData: Util.getCurrentUserData() ? 0 : 1 - }; - - // check if mapUpdate trigger should be send - // -> if "syncType" === "ajax" -> send always - // -> if "syncType" === "webSocket" -> send initial AND on map changes - if( - forceUpdateMapData || - Util.getSyncType() === 'ajax' || - ( - Util.getSyncType() === 'webSocket' && - updatedMapData.mapData.length - ) - ){ - // start log - Util.timeStart(logKeyServerMapData); - - // store updatedMapData - $.ajax({ - type: 'POST', - url: Init.path.updateMapData, - data: updatedMapData, - dataType: 'json' - }).done((data) => { - // log request time - let duration = Util.timeStop(logKeyServerMapData); - Util.log(logKeyServerMapData, {duration: duration, type: 'server', description: 'request map data'}); - - Util.setSyncStatus('ajax:get'); - - if( - data.error && - data.error.length > 0 - ){ - // any error in the main trigger functions result in a user log-off - Util.triggerMenuAction(document, 'Logout'); - }else{ - $(document).setProgramStatus('online'); - - if(data.userData !== undefined){ - // store current user data global (cache) - Util.setCurrentUserData(data.userData); - } - - // map data found - Util.setCurrentMapData(data.mapData); - - // load/update main map module - ModuleMap.updateMapModule(mapModule).then(() => { - // map update done, init new trigger - - // get the current update delay (this can change if a user is inactive) - let mapUpdateDelay = Util.getCurrentTriggerDelay(logKeyServerMapData, 0); - - // init new trigger - initMapUpdatePing(false); - - // initial start for the userUpdate trigger - // this should only be called at the first time! - if(updateTimeouts.userUpdate === 0){ - // start user update trigger after map loaded - updateTimeouts.userUpdate = setTimeout(() => { - triggerUserUpdatePing(); - }, 500); - } - }); - } - - }).fail(handleAjaxErrorResponse); - }else{ - // skip this mapUpdate trigger and init next one - initMapUpdatePing(false); - } - }; - - // ping for user data update ============================================================================== - let triggerUserUpdatePing = () => { - - // IMPORTANT: Get user data for ONE map that is currently visible - // On later releases this can be easy changed to "full update" all maps for a user - let mapIds = []; - let newSystemPositions = null; - let activeMap = Util.getMapModule().getActiveMap(); - - if(activeMap){ - mapIds = [activeMap.data('id')]; - newSystemPositions = MapUtil.newSystemPositionsByMap(activeMap); - } - - let updatedUserData = { - mapIds: mapIds, - getMapUserData: Util.getSyncType() === 'webSocket' ? 0 : 1, - mapTracking: locationToggle.is(':checked') ? 1 : 0, // location tracking - systemData: Util.getCurrentSystemData() - }; - - if(newSystemPositions){ - updatedUserData.newSystemPositions = newSystemPositions; - } - - Util.timeStart(logKeyServerUserData); - - $.ajax({ - type: 'POST', - url: Init.path.updateUserData, - data: updatedUserData, - dataType: 'json' - }).done((data) => { - // log request time - let duration = Util.timeStop(logKeyServerUserData); - Util.log(logKeyServerUserData, {duration: duration, type: 'server', description:'request user data'}); - - if( - data.error && - data.error.length > 0 - ){ - // any error in the main trigger functions result in a user log-off - Util.triggerMenuAction(document, 'Logout'); - }else{ - $(document).setProgramStatus('online'); - - if(data.userData !== undefined){ - // store current user data global (cache) - Util.setCurrentUserData(data.userData); - - // update system info panels - if(data.system){ - ModuleMap.updateSystemModulesData(mapModule, data.system); - } - - // store current map user data (cache) - if(data.mapUserData !== undefined){ - Util.setCurrentMapUserData(data.mapUserData); - } - - // update map module character data - ModuleMap.updateActiveMapUserData(mapModule).then(() => { - // map module update done, init new trigger - initMapUserUpdatePing(); - }); - } - } - - }).fail(handleAjaxErrorResponse); - }; - - /** - * init (schedule) next MapUpdate Ping - */ - let initMapUpdatePing = (forceUpdateMapData) => { - // get the current update delay (this can change if a user is inactive) - let delay = Util.getCurrentTriggerDelay(logKeyServerMapData, 0); - - updateTimeouts.mapUpdate = setTimeout((forceUpdateMapData) => { - triggerMapUpdatePing(forceUpdateMapData); - }, delay, forceUpdateMapData); - }; - - /** - * init (schedule) next MapUserUpdate Ping - */ - let initMapUserUpdatePing = () => { - // get the current update delay (this can change if a user is inactive) - let delay = Util.getCurrentTriggerDelay(logKeyServerUserData, 0); - - updateTimeouts.userUpdate = setTimeout(() => { - triggerUserUpdatePing(); - }, delay); - }; - - // initial start of the map update function - triggerMapUpdatePing(true); - - /** - * handles "final" map update request before window.unload event - * -> navigator.sendBeacon browser support required - * ajax would not work here, because browsers might cancel the request! - * @param mapModule - */ - let mapUpdateUnload = mapModule => { - // get updated map data - let mapData = ModuleMap.getMapModuleDataForUpdate(mapModule); - - if(mapData.length){ - let fd = new FormData(); - fd.set('mapData', JSON.stringify(mapData)); - navigator.sendBeacon(Init.path.updateUnloadData, fd); - - console.info('Map update request send by: %O', navigator.sendBeacon); - } - }; - - // Send map update request on tab close/reload, in order to save map changes that - // haven´t been saved through default update trigger - window.addEventListener('beforeunload', function(e){ - // close "SharedWorker" connection - MapWorker.close(); - - // clear periodic update timeouts - // -> this function will handle the final map update request - clearUpdateTimeouts(); - - // save unsaved map changes ... - if(navigator.sendBeacon){ - mapUpdateUnload(mapModule); - }else{ - // fallback if sendBeacon() is not supported by browser - triggerMapUpdatePing(); - } - - // check if character should be switched on reload or current character should be loaded afterwards - let characterSwitch = Boolean( $('body').data('characterSwitch') ); - if(!characterSwitch){ - let characterId = Util.getCurrentCharacterId(); - if(characterId){ - Util.setCookie('old_char_id', characterId, 3, 's'); - } - } - - // IMPORTANT, return false in order to not "abort" ajax request in background! - return false; - }, false); - - }; - + }); }); + /** + * init main mapModule + * -> initData() needs to be resolved first! + * @param mapModule + * @returns {Promise} + */ + let initMapModule = mapModule => new Promise(resolve => { + Promise.all([ + Page.initTabChangeObserver(), // init browser tab change observer, Once the timers are available + Page.renderMapContextMenus() // init hidden context menu elements + ]).then(() => { + // initial start of the map update function + triggerMapUpdatePing(mapModule, true); + }).then(() => resolve({ + action: 'initMapModule', + data: false + })); + }); + + /** + * request all map access data (tokens) -> required wor WebSocket subscription + * -> initData() needs to be resolved first! + * @param mapModule + * @param accessData + * @returns {Promise} + */ + let initMapWorker = (mapModule, accessData) => new Promise((resolve, reject) => { + let getPayload = command => ({ + action: 'initMapWorker', + data: { + syncStatus: Init.syncStatus.type, + command: command + } + }); + + if(accessData && accessData.status === 'OK'){ + // init SharedWorker for maps + MapWorker.init({ + characterId: accessData.data.id, + callbacks: { + onInit: (MsgWorkerMessage) => { + Util.setSyncStatus(MsgWorkerMessage.command); + + }, + onOpen: (MsgWorkerMessage) => { + Util.setSyncStatus(MsgWorkerMessage.command, MsgWorkerMessage.meta()); + MapWorker.send('subscribe', accessData.data); + + resolve(getPayload(MsgWorkerMessage.command)); + }, + onGet: (MsgWorkerMessage) => { + switch(MsgWorkerMessage.task()){ + case 'mapUpdate': + Util.updateCurrentMapData(MsgWorkerMessage.data()); + ModuleMap.updateMapModule(mapModule); + break; + case 'mapAccess': + case 'mapDeleted': + Util.deleteCurrentMapData(MsgWorkerMessage.data()); + ModuleMap.updateMapModule(mapModule); + break; + case 'mapSubscriptions': + Util.updateCurrentMapUserData(MsgWorkerMessage.data()); + ModuleMap.updateActiveMapUserData(mapModule); + break; + } + + Util.setSyncStatus('ws:get'); + }, + onClosed: (MsgWorkerMessage) => { + Util.setSyncStatus(MsgWorkerMessage.command, MsgWorkerMessage.meta()); + reject(getPayload(MsgWorkerMessage.command)); + + }, + onError: (MsgWorkerMessage) => { + Util.setSyncStatus(MsgWorkerMessage.command, MsgWorkerMessage.meta()); + reject(getPayload(MsgWorkerMessage.command)); + } + } + }); + }else{ + reject(getPayload('Invalid mapAccessData')); + } + }); + + /** + * init 'beforeunload' event + * @param mapModule + * @returns {Promise} + */ + let initUnload = mapModule => new Promise(resolve => { + + /** + * handles "final" map update request before window.unload event + * -> navigator.sendBeacon browser support required + * ajax would not work here, because browsers might cancel the request! + * @param mapModule + */ + let mapUpdateUnload = mapModule => { + // get updated map data + let mapData = ModuleMap.getMapModuleDataForUpdate(mapModule); + + if(mapData.length){ + let fd = new FormData(); + fd.set('mapData', JSON.stringify(mapData)); + navigator.sendBeacon(Init.path.updateUnloadData, fd); + + console.info('Map update request send by: %O', navigator.sendBeacon); + } + }; + + // Send map update request on tab close/reload, in order to save map changes that + // haven´t been saved through default update trigger + window.addEventListener('beforeunload', e => { + // close "SharedWorker" connection + MapWorker.close(); + + // clear periodic update timeouts + // -> this function will handle the final map update request + clearUpdateTimeouts(); + + // save unsaved map changes ... + if(navigator.sendBeacon){ + mapUpdateUnload(mapModule); + }else{ + // fallback if sendBeacon() is not supported by browser + triggerMapUpdatePing(); + } + + // check if character should be switched on reload or current character should be loaded afterwards + let characterSwitch = Boolean( $('body').data('characterSwitch') ); + if(!characterSwitch){ + let characterId = Util.getCurrentCharacterId(); + if(characterId){ + Util.setCookie('old_char_id', characterId, 3, 's'); + } + } + + // IMPORTANT, return false in order to not "abort" ajax request in background! + return false; + }, false); + + resolve({ + action: 'initUnload', + data: false + }); + }); + + + /** + * clear both main update timeouts + * -> stop program from working -> shutdown + */ + let clearUpdateTimeouts = () => { + for(let intervalKey in updateTimeouts){ + if(updateTimeouts.hasOwnProperty(intervalKey)){ + clearTimeout(updateTimeouts[intervalKey]); + } + } + }; + + + // ping for main map update ======================================================================================= + /** + * @param forceUpdateMapData // force request to be send + * @param mapModule + * @param forceUpdateMapData + */ + let triggerMapUpdatePing = (mapModule, forceUpdateMapData) => { + + // check each interval if map module is still available + if(!mapModule){ + // program crash stop any update + return; + } + + // get updated map data + let updatedMapData = { + mapData: ModuleMap.getMapModuleDataForUpdate(mapModule), + getUserData: Util.getCurrentUserData() ? 0 : 1 + }; + + // check if mapUpdate trigger should be send + // -> if "syncType" === "ajax" -> send always + // -> if "syncType" === "webSocket" -> send initial AND on map changes + if( + forceUpdateMapData || + Util.getSyncType() === 'ajax' || + ( + Util.getSyncType() === 'webSocket' && + updatedMapData.mapData.length + ) + ){ + // start log + Util.timeStart(logKeyServerMapData); + + // store updatedMapData + $.ajax({ + type: 'POST', + url: Init.path.updateMapData, + data: updatedMapData, + dataType: 'json' + }).done((data) => { + // log request time + let duration = Util.timeStop(logKeyServerMapData); + Util.log(logKeyServerMapData, {duration: duration, type: 'server', description: 'request map data'}); + + Util.setSyncStatus('ajax:get'); + + if( + data.error && + data.error.length > 0 + ){ + // any error in the main trigger functions result in a user log-off + Util.triggerMenuAction(document, 'Logout'); + }else{ + $(document).setProgramStatus('online'); + + if(data.userData !== undefined){ + // store current user data global (cache) + Util.setCurrentUserData(data.userData); + } + + // map data found + Util.setCurrentMapData(data.mapData); + + // load/update main map module + ModuleMap.updateMapModule(mapModule).then(() => { + // map update done, init new trigger + + // get the current update delay (this can change if a user is inactive) + let mapUpdateDelay = Util.getCurrentTriggerDelay(logKeyServerMapData, 0); + + // init new trigger + initMapUpdatePing(mapModule, false); + + // initial start for the userUpdate trigger + // this should only be called at the first time! + if(updateTimeouts.userUpdate === 0){ + // start user update trigger after map loaded + updateTimeouts.userUpdate = setTimeout(() => { + triggerUserUpdatePing(mapModule); + }, 500); + } + }); + } + + }).fail(handleAjaxErrorResponse); + }else{ + // skip this mapUpdate trigger and init next one + initMapUpdatePing(mapModule, false); + } + }; + + // ping for user data update ============================================================================== + let triggerUserUpdatePing = mapModule => { + + // IMPORTANT: Get user data for ONE map that is currently visible + // On later releases this can be easy changed to "full update" all maps for a user + let mapIds = []; + let newSystemPositions = null; + let activeMap = Util.getMapModule().getActiveMap(); + + if(activeMap){ + mapIds = [activeMap.data('id')]; + newSystemPositions = MapUtil.newSystemPositionsByMap(activeMap); + } + + let updatedUserData = { + mapIds: mapIds, + getMapUserData: Util.getSyncType() === 'webSocket' ? 0 : 1, + mapTracking: locationToggle.is(':checked') ? 1 : 0, // location tracking + systemData: Util.getCurrentSystemData() + }; + + if(newSystemPositions){ + updatedUserData.newSystemPositions = newSystemPositions; + } + + Util.timeStart(logKeyServerUserData); + + $.ajax({ + type: 'POST', + url: Init.path.updateUserData, + data: updatedUserData, + dataType: 'json' + }).done((data) => { + // log request time + let duration = Util.timeStop(logKeyServerUserData); + Util.log(logKeyServerUserData, {duration: duration, type: 'server', description:'request user data'}); + + if( + data.error && + data.error.length > 0 + ){ + // any error in the main trigger functions result in a user log-off + Util.triggerMenuAction(document, 'Logout'); + }else{ + $(document).setProgramStatus('online'); + + if(data.userData !== undefined){ + // store current user data global (cache) + Util.setCurrentUserData(data.userData); + + // update system info panels + if(data.system){ + ModuleMap.updateSystemModulesData(mapModule, data.system); + } + + // store current map user data (cache) + if(data.mapUserData !== undefined){ + Util.setCurrentMapUserData(data.mapUserData); + } + + // update map module character data + ModuleMap.updateActiveMapUserData(mapModule).then(() => { + // map module update done, init new trigger + initMapUserUpdatePing(mapModule); + }); + } + } + }).fail(handleAjaxErrorResponse); + }; + + /** + * init (schedule) next MapUpdate Ping + * @param mapModule + * @param forceUpdateMapData + */ + let initMapUpdatePing = (mapModule, forceUpdateMapData) => { + // get the current update delay (this can change if a user is inactive) + let delay = Util.getCurrentTriggerDelay(logKeyServerMapData, 0); + + updateTimeouts.mapUpdate = setTimeout((mapModule, forceUpdateMapData) => { + triggerMapUpdatePing(mapModule, forceUpdateMapData); + }, delay, mapModule, forceUpdateMapData); + }; + + /** + * init (schedule) next MapUserUpdate Ping + * @param mapModule + */ + let initMapUserUpdatePing = mapModule => { + // get the current update delay (this can change if a user is inactive) + let delay = Util.getCurrentTriggerDelay(logKeyServerUserData, 0); + + updateTimeouts.userUpdate = setTimeout(mapModule => { + triggerUserUpdatePing(mapModule); + }, delay, mapModule); + }; + + /** + * Ajax error response handler function for main-ping functions + * @param jqXHR + * @param status + * @param error + */ + let handleAjaxErrorResponse = (jqXHR, status, error) => { + // clear both main update request trigger timer + clearUpdateTimeouts(); + + let reason = status + ' ' + jqXHR.status + ': ' + error; + let errorData = []; + let redirect = false; // redirect user to other page e.g. login + let reload = true; // reload current page (default: true) + + if(jqXHR.responseJSON){ + // handle JSON + let responseObj = jqXHR.responseJSON; + if( + responseObj.error && + responseObj.error.length > 0 + ){ + errorData = responseObj.error; + } + + if(responseObj.reroute){ + redirect = responseObj.reroute; + } + }else{ + // handle HTML + errorData.push({ + type: 'error', + message: 'Please restart and reload this page' + }); + } + + console.error(' ↪ %s Error response: %o', jqXHR.url, errorData); + $(document).trigger('pf:shutdown', { + status: jqXHR.status, + reason: reason, + error: errorData, + redirect: redirect, + reload: reload + }); + }; + + // ================================================================================================================ + // main thread -> init "map" page + // ================================================================================================================ + // set default AJAX config + Util.ajaxSetup(); + + /** + * run app + * @param rootEl + * @returns {Promise} + */ + let run = (rootEl = document.body) => new Promise(resolve => { + // run all init functions for mainModule and WebSocket configuration async + Promise.all([ + initApp(rootEl), + initData(), + getMapAccessData() + ]) + .then(([mapModule, accessData]) => Promise.all([ + initMapModule(mapModule), + initMapWorker(mapModule,accessData), + initUnload(mapModule) + ])) + .then(([payloadMapModule, payloadMapWorker]) => { + // mapModule initialized and WebSocket configuration working + console.ok('Client syncStatus: %s. %O resolved by command: %s!', + payloadMapWorker.data.syncStatus, + payloadMapWorker.action + '()', + payloadMapWorker.data.command + ); + resolve('OK'); + }) + .catch(payload => { + switch(payload.action){ + case 'shutdown': + // ajax error + handleAjaxErrorResponse(payload.data.jqXHR, payload.data.status, payload.data.error); + break; + case 'initMapWorker': + // WebSocket not working -> no error here -> fallback to Ajax + console.info('Client syncStatus: %s. %O rejected by command: %s! payload: %o', + payload.data.syncStatus, + payload.action + '()', + payload.data.command, + payload.data + ); + break; + default: + console.error('Unhandled error thrown while initialization: %o ', payload); + } + }); + }); + + if(document.readyState === 'loading'){ // Loading hasn't finished yet + document.addEventListener('DOMContentLoaded', run); + }else{ // `DOMContentLoaded` has already fired + run(); + } }); \ No newline at end of file diff --git a/js/app/module_map.js b/js/app/module_map.js index 51bf9aa8..0bf8a254 100644 --- a/js/app/module_map.js +++ b/js/app/module_map.js @@ -4,7 +4,9 @@ define([ 'app/util', 'app/map/map', 'app/map/util', + 'app/lib/eventHandler', 'sortable', + 'module/base', 'module/system_info', 'module/system_graph', 'module/system_signature', @@ -19,7 +21,9 @@ define([ Util, Map, MapUtil, + EventHandler, Sortable, + BaseModule, SystemInfoModule, SystemGraphModule, SystemSignatureModule, @@ -32,28 +36,31 @@ define([ let config = { mapTabElementId: 'pf-map-tab-element', // id for map tab element (tabs + content) - mapTabBarId: 'pf-map-tabs', // id for map tab bar mapTabIdPrefix: 'pf-map-tab-', // id prefix for a map tab mapTabClass: 'pf-map-tab', // class for a map tab - mapTabDragHandlerClass: 'pf-map-tab-handler', // class for drag handler mapTabIconClass: 'pf-map-tab-icon', // class for map icon mapTabLinkTextClass: 'nav-tabs-link', // class for span elements in a tab mapTabSharedIconClass: 'pf-map-tab-shared-icon', // class for map shared icon - mapTabContentClass: 'pf-map-tab-content', // class for tab content container - mapTabContentSystemInfoClass: 'pf-map-tab-content-system', - mapWrapperClass: 'pf-map-wrapper', // scrollable - mapClass: 'pf-map', // class for each map - - // TabContentStructure - mapTabContentRow: 'pf-map-content-row', // main row for Tab content (grid) - mapTabContentCell: 'pf-map-content-col', // column - mapTabContentCellFirst: 'pf-map-content-col-first', // first column - mapTabContentCellSecond: 'pf-map-content-col-second', // second column + mapTabContentWrapperClass: 'pf-map-tab-content-wrapper', // class for map tab content wrapper // module moduleClass: 'pf-module', // class for a module moduleSpacerClass: 'pf-module-spacer', // class for "spacer" module (preserves height during hide/show animation) - moduleClosedClass: 'pf-module-closed' // class for a closed module + moduleCollapsedClass: 'collapsed', // class for a collapsed module + + // sortable + sortableHandleClass: 'pf-sortable-handle', + sortableDropzoneClass: 'pf-sortable-dropzone', + sortableGhostClass: 'pf-sortable-ghost', + sortableChosenClass: 'pf-sortable-chosen', + + // editable 'settings' popover + editableSettingsClass: 'pf-editable-settings', + editableToggleClass: 'pf-editable-toggle', + editableToggleItemClass: 'pf-editable-toggle-item', + + mapTabContentLayoutOptions: ['left', 'right'], + defaultMapTabContentLayout: 'right', }; let mapTabChangeBlocked = false; // flag for preventing map tab switch @@ -63,14 +70,14 @@ define([ * @param mapModule * @returns {jQuery} */ - let getMaps = mapModule => $(mapModule).find('.' + config.mapClass); + let getMaps = mapModule => $(mapModule).find('.' + Util.config.mapClass); /** * get the current active mapElement - * @returns {JQuery|*|T|{}|jQuery} + * @returns {bool|jQuery} */ $.fn.getActiveMap = function(){ - let map = $(this).find('.active.' + config.mapTabContentClass + ' .' + config.mapClass); + let map = $(this).find('.active.' + Util.config.mapTabContentClass + ' .' + Util.config.mapClass); if(!map.length){ map = false; } @@ -78,341 +85,397 @@ define([ }; /** - * set mapContent Observer, events are triggered within map.js - * @param tabElement + * set map tab content wrapper observer. + * -> Events are triggered within map.js + * @param tabContentWrapperEl */ - let setMapContentObserver = (tabElement) => { - - tabElement.on('pf:drawSystemModules', '.' + config.mapTabContentClass, function(e){ - drawSystemModules($(e.target)); + let setMapTabContentWrapperObserver = tabContentWrapperEl => { + $(tabContentWrapperEl).on('pf:renderSystemModules', `.${Util.config.mapTabContentClass}`, function(e, data){ + getModules() + .then(modules => filterModules(modules, 'system')) + .then(modules => renderModules(modules, e.target, data)); }); - tabElement.on('pf:removeSystemModules', '.' + config.mapTabContentClass, function(e){ - removeSystemModules($(e.target)); + $(tabContentWrapperEl).on('pf:removeSystemModules', `.${Util.config.mapTabContentClass}`, e => { + getModules() + .then(modules => filterModules(modules, 'system')) + .then(modules => removeModules(modules, e.target)); }); - tabElement.on('pf:drawConnectionModules', '.' + config.mapTabContentClass, function(e, data){ - drawConnectionModules($(e.target), data); + $(tabContentWrapperEl).on('pf:renderConnectionModules', `.${Util.config.mapTabContentClass}`, (e, data) => { + getModules() + .then(modules => filterModules(modules, 'connection')) + .then(modules => renderModules(modules, e.target, data)); }); - tabElement.on('pf:removeConnectionModules', '.' + config.mapTabContentClass, function(e){ - removeConnectionModules($(e.target)); + $(tabContentWrapperEl).on('pf:removeConnectionModules', `.${Util.config.mapTabContentClass}`, e => { + getModules() + .then(modules => filterModules(modules, 'connection')) + .then(modules => removeModules(modules, e.target)); }); - tabElement.on('pf:updateSystemModules', '.' + config.mapTabContentClass, function(e, data){ - updateSystemModules($(e.target), data); + $(tabContentWrapperEl).on('pf:updateSystemModules', `.${Util.config.mapTabContentClass}`, (e, data) => { + getModules() + .then(modules => filterModules(modules, true, 'fullDataUpdate')) + .then(modules => updateModules(modules, e.target, data)); }); - tabElement.on('pf:updateRouteModules', '.' + config.mapTabContentClass, function(e, data){ - updateRouteModules($(e.target), data); + $(tabContentWrapperEl).on('pf:updateRouteModules', `.${Util.config.mapTabContentClass}`, (e, data) => { + getModules() + .then(modules => filterModules(modules, 'SystemRouteModule', 'name')) + .then(modules => updateModules(modules, e.target, data)); }); }; /** - * update (multiple) modules - * @param tabContentElement - * @param modules - * @param data + * get/load module classes + * -> default modules + custom plugin modules + * @returns {Promise} */ - let updateModules = (tabContentElement, modules, data) => { - for(let Module of modules){ - let moduleElement = tabContentElement.find('.' + Module.config.moduleTypeClass); - if(moduleElement.length > 0){ - Module.updateModule(moduleElement, data); + let getModules = () => { + return new Promise(resolve => { + let modules = [ + SystemInfoModule, + SystemGraphModule, + SystemSignatureModule, + SystemRouteModule, + SystemIntelModule, + SystemKillboardModule, + ConnectionInfoModule + ]; + + // try to load custom plugin modules (see: plugin.ihi) + let pluginModulesConfig = Util.getObjVal(Init, 'plugin.modules'); + if(pluginModulesConfig === Object(pluginModulesConfig)){ + requirejs(Object.values(pluginModulesConfig), (...pluginModules) => { + modules.push(...pluginModules); + resolve(modules); + }, err => { + console.error(err.message); + resolve(modules); + }); + }else{ + // custom plugins disabled + resolve(modules); } + }); + }; + + /** + * filer array of module classes by property filterVal(s) + * @param modules + * @param filterVal + * @param filterProp + * @returns BaseModule[] + */ + let filterModules = (modules, filterVal = false, filterProp = 'scope') => modules.filter(Module => + filterVal ? + ( + Array.isArray(filterVal) ? + filterVal.includes(Module[filterProp]) : + Module[filterProp] === filterVal + ) : + true + ); + + /** + * @param modules + * @param tabContentElement + * @param data + * @returns {PromiseLike | Promise | *} + */ + let renderModules = (modules, tabContentElement, data) => { + /** + * @param dataStore + * @returns {Promise} + */ + let render = dataStore => { + let promiseRenderAll = []; + for(let Module of modules){ + let defaultGridArea = Module.sortArea || 'a'; + let defaultPosition = Module.position || 0; + + for(let areaAlias of Util.config.mapTabContentAreaAliases){ + let key = 'modules_area_' + areaAlias; + if(dataStore && dataStore[key]){ + let positionIndex = dataStore[key].indexOf(Module.name); + if(positionIndex !== -1){ + // first index (0) => is position 1 + defaultPosition = positionIndex + 1; + defaultGridArea = areaAlias; + break; + } + } + } + + // check if gridArea exists + let gridArea = tabContentElement.getElementsByClassName(Util.getMapTabContentAreaClass(defaultGridArea)); + if(gridArea.length){ + gridArea = gridArea[0]; + promiseRenderAll.push(renderModule(Module, gridArea, defaultPosition, data.mapId, data.payload)); + }else{ + console.warn( + 'renderModules() failed for %o. GridArea class=%o not found', + Module.name, + Util.getMapTabContentAreaClass(defaultGridArea) + ); + } + } + + return Promise.all(promiseRenderAll); + }; + + let renderModulesAndUpdateExecutor = resolve => { + // get local data for map + // -> filter out disabled modules + // -> get default module positions + Util.getLocalStore('map').getItem(data.mapId).then(dataStore => { + // filter disabled modules (layout settings) + let modulesDisabled = Util.getObjVal(dataStore, 'modulesDisabled') || []; + modules = modules.filter(Module => !modulesDisabled.includes(Module.name)); + + // check if modules require "additional" data (e.g. structures, description) + // -> this is used to update some modules after initial draw + let requestSystemData = false; + for(let Module of modules){ + if(Module.scope === 'system' && Module.fullDataUpdate){ + requestSystemData = true; + } + } + + let renderPromises = []; + if(requestSystemData){ + renderPromises.push(Util.request('GET', 'system', data.payload.id, {mapId: data.mapId})); + } + renderPromises.push(render(dataStore)); + + Promise.all(renderPromises) + .then(payload => { + let promiseUpdateAll = []; + + let systemData; + if(requestSystemData){ + // get systemData from first Promise (ajax call) + let responseData = payload.shift(); + systemData = Util.getObjVal(responseData, 'data'); + } + + if(systemData){ + // get all rendered modules + let modules = payload.shift().map(payload => payload.data.module); + + // get modules that require "additional" data + let systemModules = modules.filter(Module => Module.scope === 'system' && Module.fullDataUpdate); + promiseUpdateAll.push(updateModules(systemModules, tabContentElement, { + payload: systemData + })); + } + + Promise.all(promiseUpdateAll).then(payload => resolve(payload)); + }); + }); + }; + + return new Promise(renderModulesAndUpdateExecutor); + }; + + /** + * @param Module + * @param gridArea + * @param defaultPosition + * @param mapId + * @param payload + * @returns {Promise} + */ + let renderModule = (Module, gridArea, defaultPosition, mapId, payload) => { + let renderModuleExecutor = (resolve, reject) => { + + /** + * remove "Spacer" Module + * @param gridArea + * @param Module + */ + let removeSpacerModule = (gridArea, Module) => { + for(let spacerEl of gridArea.querySelectorAll('.' + Module.className + '-spacer[data-module="' + Module.name + '"]')){ + spacerEl.remove(); + } + }; + + /** + * render module + * @param Module + * @param gridArea + * @param defaultPosition + * @param mapId + * @param payload + */ + let render = (Module, gridArea, defaultPosition, mapId, payload) => { + let payBack = { + action: 'renderModule', + data: { + module: Module + } + }; + + // hide "spacer" Module (in case it exist) + // -> Must be done BEFORE position calculation! Spacer Modules should not be counted! + removeSpacerModule(gridArea, Module); + + let module = new Module({ + position: defaultPosition + }); + + let moduleElement = module.handle('render', mapId, payload); + + if(!(moduleElement instanceof HTMLElement)){ + // module should not be rendered + resolve(payBack); + return; + } + + // find correct position for new moduleElement + let position = getModulePosition(gridArea, '.' + Module.className, defaultPosition); + + // insert at correct position + // -> no :nth-child or :nth-of-type here because there might be temporary "spacer" div "modules" + // that should be ignored for positioning + let prevModuleElement = [...gridArea.getElementsByClassName(Module.className)].find((el, i) => ++i === position); + if(prevModuleElement){ + prevModuleElement.insertAdjacentElement('afterend', moduleElement); + }else{ + gridArea.prepend(moduleElement); + } + + // show animation ------------------------------------------------------------------------------------- + $(moduleElement).velocity({ + opacity: [1, 0], + translateY: [0, +20] + }, { + duration: Init.animationSpeed.mapModule, + easing: 'easeOutSine', + complete: moduleElement => { + moduleElement[0].getData('module').handle('init'); + resolve(payBack); + } + }); + }; + + removeModule(Module, gridArea, true).then(abc => render(Module, gridArea, defaultPosition, mapId, payload)); + }; + + return new Promise(renderModuleExecutor); + }; + + /** + * update multiple modules + * @param modules + * @param tabContentElement + * @param data + * @returns {Promise} + */ + let updateModules = (modules, tabContentElement, data) => { + let promiseUpdateAll = []; + for(let Module of modules){ + promiseUpdateAll.push(updateModule(Module, tabContentElement, data.payload)); } + return Promise.all(promiseUpdateAll); }; /** - * update system modules with new data - * @param tabContentElement - * @param data + * update module + * @param Module + * @param parentElement + * @param payload + * @returns {Promise} */ - let updateSystemModules = (tabContentElement, data) => { - let systemModules = [SystemInfoModule, SystemSignatureModule, SystemIntelModule]; - updateModules(tabContentElement, systemModules, data); - }; + let updateModule = (Module, parentElement, payload) => { + let updateModuleExecutor = resolve => { + let promiseUpdateAll = []; + let moduleElements = parentElement.querySelectorAll('.' + Module.className + '[data-module="' + Module.name + '"]'); + for(let moduleElement of moduleElements){ + promiseUpdateAll.push(moduleElement.getData('module').handle('update', payload)); + } + Promise.all(promiseUpdateAll).then(payload => resolve(payload)); + }; - /** - * update route modules with new data - * @param tabContentElement - * @param data - */ - let updateRouteModules = (tabContentElement, data) => { - let routeModules = [SystemRouteModule]; - updateModules(tabContentElement, routeModules, data); + return new Promise(updateModuleExecutor); }; /** * remove multiple modules - * @param tabContentElement * @param modules + * @param tabContentElement + * @returns {Promise} */ - let removeModules = (tabContentElement, modules) => { + let removeModules = (modules, tabContentElement) => { + let promiseRemoveAll = []; for(let Module of modules){ - let moduleElement = tabContentElement.find('.' + Module.config.moduleTypeClass); - removeModule(moduleElement, Module); + promiseRemoveAll.push(removeModule(Module, tabContentElement)); } + return Promise.all(promiseRemoveAll); }; /** - * clear all system modules and remove them - * @param tabContentElement - */ - let removeSystemModules = (tabContentElement) => { - let systemModules = [SystemInfoModule, SystemGraphModule, SystemSignatureModule, SystemRouteModule, SystemIntelModule, SystemKillboardModule]; - removeModules(tabContentElement, systemModules); - }; - - /** - * clear all connection modules and remove them - * @param tabContentElement - */ - let removeConnectionModules = (tabContentElement) => { - let connectionModules = [ConnectionInfoModule]; - removeModules(tabContentElement, connectionModules); - }; - - /** - * remove a single module - * @param moduleElement + * remove module * @param Module - * @param callback - * @param addSpacer - */ - let removeModule = (moduleElement, Module, callback, addSpacer) => { - if(moduleElement.length > 0){ - if(typeof Module.beforeHide === 'function'){ - Module.beforeHide(moduleElement); - } - - moduleElement.velocity('reverse',{ - complete: function(moduleElement){ - moduleElement = $(moduleElement); - let oldModuleHeight = moduleElement.outerHeight(); - - if(typeof Module.beforeDestroy === 'function'){ - Module.beforeDestroy(moduleElement); - } - - // [optional] add a "spacer"
that fakes Module height during hide->show animation - if(addSpacer){ - moduleElement.after($('
', { - class: [config.moduleSpacerClass, Module.config.moduleTypeClass + '-spacer'].join(' '), - css: { - height: oldModuleHeight + 'px' - } - })); - } - - moduleElement.remove(); - - if(typeof callback === 'function'){ - callback(); - } - } - }); - } - }; - - /** - * generic function that draws a modulePanel for a given Module object * @param parentElement - * @param Module - * @param mapId - * @param data + * @param addSpacer + * @returns {Promise} */ - let drawModule = (parentElement, Module, mapId, data) => { + let removeModule = (Module, parentElement, addSpacer = false) => { - let drawModuleExecutor = (resolve, reject) => { + let removeModuleElement = moduleElement => { + let removeModuleElementExecutor = (resolve, reject) => { + let payload = { + action: 'removeModule', + data: {} + }; - /** - * remove "Spacer" Module - * @param parentElement - * @param Module - */ - let removeSpacerModule = (parentElement, Module) => { - parentElement.find('.' + Module.config.moduleTypeClass + '-spacer').remove(); - }; + // get module instance + let module = moduleElement.getData('module'); + if(module instanceof BaseModule){ + module.handle('beforeHide'); - /** - * show/render a Module - * @param parentElement - * @param Module - * @param mapId - * @param data - */ - let showPanel = (parentElement, Module, mapId, data) => { - let moduleElement = Module.getModule(parentElement, mapId, data); - if(moduleElement){ - // store Module object to DOM element for further access - moduleElement.data('module', Module); - moduleElement.data('data', data); - moduleElement.addClass([config.moduleClass, Module.config.moduleTypeClass].join(' ')); - moduleElement.css({opacity: 0}); // will be animated + $(moduleElement).velocity('reverse', { + complete: moduleElement => { + moduleElement = moduleElement[0]; + let module = moduleElement.getData('module'); + module.handle('beforeDestroy'); - // check module position from local storage - let promiseStore = MapUtil.getLocaleData('map', mapId); - promiseStore.then(function(dataStore){ - let Module = this.moduleElement.data('module'); - let defaultPosition = Module.config.modulePosition || 0; + // [optional] add a "spacer"
that fakes Module height during hide->show animation + if(addSpacer){ + let spacerEl = document.createElement('div'); + spacerEl.classList.add(Module.className + '-spacer'); + spacerEl.setAttribute('data-module', Module.name); + spacerEl.style.height = moduleElement.offsetHeight + 'px'; - // hide "spacer" Module (in case it exist) - // -> Must be done BEFORE position calculation! Spacer Modules should not be counted! - removeSpacerModule(this.parentElement, Module); - - // check for stored module order in indexDB (client) ---------------------------------------------- - let key = 'modules_cell_' + this.parentElement.attr('data-position'); - if( - dataStore && - dataStore[key] - ){ - let positionIndex = dataStore[key].indexOf(Module.config.moduleName); - if(positionIndex !== -1){ - // first index (0) => is position 1 - defaultPosition = positionIndex + 1; + moduleElement.insertAdjacentElement('afterend', spacerEl); } + + moduleElement.remove(); + + resolve(payload); } - - // find correct position for new moduleElement ---------------------------------------------------- - let position = getModulePosition(this.parentElement, '.' + config.moduleClass, defaultPosition); - - this.moduleElement.attr('data-position', defaultPosition); - this.moduleElement.attr('data-module', Module.config.moduleName); - - // insert at correct position --------------------------------------------------------------------- - // -> no :nth-child or :nth-of-type here because there might be temporary "spacer" div "modules" - // that should be ignored for positioning - let prevModuleElement = this.parentElement.find('.' + config.moduleClass).filter(i => ++i === position); - if(prevModuleElement.length){ - this.moduleElement.insertAfter(prevModuleElement); - }else{ - this.parentElement.prepend(this.moduleElement); - } - - if(typeof Module.beforeShow === 'function'){ - Module.beforeShow(this.moduleElement, moduleElement.data('data')); - } - - // show animation --------------------------------------------------------------------------------- - this.moduleElement.velocity({ - opacity: [1, 0], - translateY: [0, +20] - }, { - duration: Init.animationSpeed.mapModule, - easing: 'easeOutSine', - complete: function(moduleElement){ - moduleElement = $(moduleElement); - let Module = $(moduleElement).data('module'); - if(typeof Module.initModule === 'function'){ - Module.initModule(moduleElement, mapId, moduleElement.data('data')); - } - - resolve({ - action: 'drawModule', - data: { - module: Module - } - }); - } - }); - }.bind({ - parentElement: parentElement, - moduleElement: moduleElement - })); + }); }else{ - // Module should not be shown (e.g. "Graph" module on WH systems) - removeSpacerModule(parentElement, Module); + console.warn('Invalid module. Instance of %O expected for %o', BaseModule, moduleElement); + resolve(payload); } }; - // check if module already exists - let moduleElement = parentElement.find('.' + Module.config.moduleTypeClass); - if(moduleElement.length > 0){ - removeModule(moduleElement, Module, () => { - showPanel(parentElement, Module, mapId, data); - }, true); - }else{ - showPanel(parentElement, Module, mapId, data); - } + return new Promise(removeModuleElementExecutor); }; - return new Promise(drawModuleExecutor); - }; + let removeModuleExecutor = resolve => { + let promiseRemoveAll = []; + let moduleElements = parentElement.querySelectorAll('.' + Module.className + '[data-module="' + Module.name + '"]'); + for(let moduleElement of moduleElements){ + promiseRemoveAll.push(removeModuleElement(moduleElement)); + } + Promise.all(promiseRemoveAll).then(payload => resolve(payload)); + }; - /** - * clears and updates the system info element (signature table, system info,...) - * @param tabContentElement - */ - let drawSystemModules = (tabContentElement) => { - - require(['datatables.loader'], () => { - let currentSystemData = Util.getCurrentSystemData(); - - let promiseDrawAll = []; - - // request "additional" system data (e.g. Structures, Description) - // -> this is used to update some modules after initial draw - let promiseRequestData = Util.request('GET', 'system', currentSystemData.id, {mapId: currentSystemData.mapId}); - - // draw modules ------------------------------------------------------------------------------------------- - - let firstCell = tabContentElement.find('.' + config.mapTabContentCellFirst); - let secondCell = tabContentElement.find('.' + config.mapTabContentCellSecond); - - // draw system info module - let promiseInfo = drawModule(firstCell, SystemInfoModule, currentSystemData.mapId, currentSystemData); - - // draw system graph module - drawModule(firstCell, SystemGraphModule, currentSystemData.mapId, currentSystemData); - - // draw signature table module - let promiseSignature = drawModule(firstCell, SystemSignatureModule, currentSystemData.mapId, currentSystemData); - - // draw system routes module - drawModule(secondCell, SystemRouteModule, currentSystemData.mapId, currentSystemData); - - // draw system intel module - let promiseIntel = drawModule(secondCell, SystemIntelModule, currentSystemData.mapId, currentSystemData); - - // draw system killboard module - drawModule(secondCell, SystemKillboardModule, currentSystemData.mapId, currentSystemData); - - // update some modules ------------------------------------------------------------------------------------ - promiseDrawAll.push(promiseRequestData, promiseInfo, promiseSignature, promiseIntel); - - // update "some" modules after draw AND additional system data was requested - Promise.all(promiseDrawAll).then(payload => { - // get systemData from first Promise (ajax call) - let responseData = payload.shift(); - if(responseData.data){ - let systemData = responseData.data; - - // update all Modules - let modules = []; - for(let responseData of payload){ - modules.push(responseData.data.module); - } - updateModules(tabContentElement, modules, systemData); - } - }); - }); - }; - - /** - * clears and updates the connection info element (mass log) - * @param tabContentElement - * @param data - */ - let drawConnectionModules = (tabContentElement, data) => { - require(['datatables.loader'], function(){ - - // get grid cells - let firstCell = $(tabContentElement).find('.' + config.mapTabContentCellFirst); - - // draw connection info module - drawModule(firstCell, ConnectionInfoModule, this.mapId, this.connections); - }.bind(data)); + return new Promise(removeModuleExecutor); }; /** @@ -420,17 +483,11 @@ define([ * @param mapModule * @returns {Promise} */ - let updateActiveMapUserData = (mapModule) => { - - let updateActiveMapModuleExecutor = (resolve, reject) => { - // get all active map elements for module - let mapElement = mapModule.getActiveMap(); - - updateMapUserData(mapElement).then(payload => resolve()); - }; - - return new Promise(updateActiveMapModuleExecutor); - }; + let updateActiveMapUserData = mapModule => new Promise(resolve => { + // get all active map elements for module + let mapElement = $(mapModule).getActiveMap(); + updateMapUserData(mapElement).then(() => resolve()); + }); /** * updates mapElement with user data @@ -438,12 +495,12 @@ define([ * @param mapElement * @returns {Promise} */ - let updateMapUserData = (mapElement) => { + let updateMapUserData = mapElement => { // performance logging (time measurement) let logKeyClientUserData = Init.performanceLogging.keyClientUserData; Util.timeStart(logKeyClientUserData); - let updateMapUserDataExecutor = (resolve, reject) => { + let updateMapUserDataExecutor = resolve => { if(mapElement !== false){ let mapId = mapElement.data('id'); let currentMapUserData = Util.getCurrentMapUserData(mapId); @@ -482,155 +539,251 @@ define([ systemData.id === currentSystemData.id ){ // trigger system update events - let tabContentElement = $('#' + config.mapTabIdPrefix + systemData.mapId + '.' + config.mapTabContentClass); - tabContentElement.trigger('pf:updateSystemModules', [systemData]); + let tabContentEl = document.getElementById(config.mapTabIdPrefix + systemData.mapId); + $(tabContentEl).trigger('pf:updateSystemModules', { + payload: systemData + }); } } }; /** - * set observer for tab content (area where modules will be shown) - * @param contentStructure + * set observer for tab content (areas where modules will be shown) + * @param tabContent * @param mapId */ - let setContentStructureObserver = (contentStructure, mapId) => { - contentStructure.find('.' + config.mapTabContentCell).each((index, cellElement) => { - let sortable = Sortable.create(cellElement, { - group: { - name: 'cell_' + cellElement.getAttribute('data-position') + let setTabContentObserver = (tabContent, mapId) => { + + let defaultSortableOptions = { + animation: Init.animationSpeed.mapModule, + handle: '.' + config.sortableHandleClass, + draggable: '.' + config.moduleClass, + ghostClass: config.sortableGhostClass, + chosenClass: config.sortableChosenClass, + scroll: true, + scrollSensitivity: 50, + scrollSpeed: 20, + dataIdAttr: 'data-module', + sort: true, + store: { + get: function(sortable){ + return []; }, - animation: Init.animationSpeed.mapModule, - handle: '.pf-module-handler-drag', - draggable: '.' + config.moduleClass, - ghostClass: 'pf-sortable-ghost', - scroll: true, - scrollSensitivity: 50, - scrollSpeed: 20, - dataIdAttr: 'data-module', - sort: true, - store: { - get: function(sortable){ - return []; - }, - set: function(sortable){ - let key = 'modules_' + sortable.options.group.name; - MapUtil.storeLocalData('map', mapId, key, sortable.toArray()); - } - }, - onStart: function(e){ - // Element dragging started - // -> save initial sort state -> see store.set() - this.save(); + set: function(sortable){ + // function is called to frequently for different "groups" + // if an element moved between groups -> async local store can not handle this in time + // -> queue up store calls + let key = 'modules_' + sortable.options.group.name; + Util.getLocalStore('map').setItem(`${mapId}.${key}`, sortable.toArray()); } - }); + }, + onStart: function(e){ + // Element dragging started + // -> save initial sort state -> see store.set() + this.save(); + + // highlight valid grid areas where module could be dropped + let module = e.item.getData('module'); + let sortTargetAreas = module.config.sortTargetAreas || []; + + tabContent.querySelectorAll('.' + Util.getMapTabContentAreaClass()).forEach(gridArea => { + if(sortTargetAreas.includes(gridArea.getAttribute('data-area'))){ + gridArea.classList.add(config.sortableDropzoneClass); + }else{ + gridArea.classList.remove(config.sortableDropzoneClass); + } + }); + }, + onEnd: function(e){ + // remove highlight grid areas + tabContent.querySelectorAll('.' + Util.getMapTabContentAreaClass()).forEach(gridArea => { + gridArea.classList.remove(config.sortableDropzoneClass); + }); + } + }; + + [ + 'onChoose', + 'onStart', + 'onEnd', + 'onAdd', + 'onUpdate', + 'onSort', + 'onRemove', + 'onChange', + 'onUnchoose', + //'onMove' + ].forEach(name => { + defaultSortableOptions[name] = function(e){ + // onMove is the only event where e.item does not exist + // -> e.related is the element that is moved by the dragged one + let target = e.item || e.related; + let module = target.getData('module'); + + switch(name){ + case 'onStart': + // Element dragging started + // -> save initial sort state -> see store.set() + this.save(); + + // highlight valid grid areas where module could be dropped + let sortTargetAreas = module.config.sortTargetAreas || []; + tabContent.querySelectorAll('.' + Util.getMapTabContentAreaClass()).forEach(gridArea => { + if(sortTargetAreas.includes(gridArea.getAttribute('data-area'))){ + gridArea.classList.add(config.sortableDropzoneClass); + }else{ + gridArea.classList.remove(config.sortableDropzoneClass); + } + }); + break; + case 'onEnd': + // remove highlight grid areas + tabContent.querySelectorAll('.' + Util.getMapTabContentAreaClass()).forEach(gridArea => { + gridArea.classList.remove(config.sortableDropzoneClass); + }); + break; + + } + // pipe events to module + module.handle('onSortableEvent', name, e); + }; }); - // toggle height for a module - contentStructure.on('click.toggleModuleHeight', '.' + config.moduleClass, function(e){ - let moduleElement = $(this); - // get click position - let posX = moduleElement.offset().left; - let posY = moduleElement.offset().top; - let clickX = e.pageX - posX; - let clickY = e.pageY - posY; + /** + * sortable map modules + */ + tabContent.querySelectorAll('.' + Util.getMapTabContentAreaClass()).forEach(gridArea => { - // check for top-left click - if(clickX <= 9 && clickY <= 9 && clickX >= 0 && clickY >= 0){ + let sortable = Sortable.create(gridArea, Object.assign({}, defaultSortableOptions, { + group: { + name: 'area_' + gridArea.getAttribute('data-area'), + pull: (to, from, dragEl, e) => { + // set allowed droppable target areas for module + let module = dragEl.getData('module'); + return (module.config.sortTargetAreas || []).map(area => 'area_' + area); + }, + put: (to, from, dragEl, e) => { + return true; + } + } + })); + }); + + /** + * toggle module height + * @param e + */ + let toggleModuleHeight = e => { + if( + e.target.classList.contains(config.moduleClass) && + e.layerX <= 9 && e.layerY <= 9 && e.layerX >= 0 && e.layerY >= 0 + ){ + e.stopPropagation(); + let moduleElement = e.target; // remember height - if( !moduleElement.data('origHeight') ){ - moduleElement.data('origHeight', moduleElement.outerHeight()); + if(!moduleElement.dataset.origHeight){ + moduleElement.dataset.origHeight = moduleElement.offsetHeight; } - if(moduleElement.hasClass(config.moduleClosedClass)){ - let moduleHeight = moduleElement.data('origHeight'); - moduleElement.velocity('finish').velocity({ - height: [ moduleHeight + 'px', [ 400, 15 ] ] + if(moduleElement.classList.contains(config.moduleCollapsedClass)){ + $(moduleElement).velocity('finish').velocity({ + height: [moduleElement.dataset.origHeight + 'px', [400, 15]] },{ duration: 400, easing: 'easeOutSine', - complete: function(moduleElement){ - moduleElement = $(moduleElement); - moduleElement.removeClass(config.moduleClosedClass); - moduleElement.removeData('origHeight'); - moduleElement.css({height: ''}); + complete: moduleElement => { + moduleElement[0].classList.remove(config.moduleCollapsedClass); + delete moduleElement[0].dataset.origHeight; + moduleElement[0].style.height = null; } }); }else{ - moduleElement.velocity('finish').velocity({ - height: [ '35px', [ 400, 15 ] ] + $(moduleElement).velocity('finish').velocity({ + height: ['38px', [400, 15]] },{ duration: 400, easing: 'easeOutSine', - complete: function(moduleElement){ - moduleElement = $(moduleElement); - moduleElement.addClass(config.moduleClosedClass); + complete: moduleElement => { + moduleElement[0].classList.add(config.moduleCollapsedClass); } }); } } - }); + }; + + EventHandler.addEventListener(tabContent, 'click.toggleModuleHeight', toggleModuleHeight, {passive: false}); }; /** - * load all structure elements into a TabsContent div (tab body) - * @param tabContentElements + * get grid item (area) elements for map tab content + * @returns {[]} */ - let initContentStructure = (tabContentElements) => { - tabContentElements.each(function(){ - let tabContentElement = $(this); - let mapId = parseInt( tabContentElement.attr('data-mapid') ); + let getTabContentAreaElements = () => { + let gridAreas = []; + for(let areaAlias of Util.config.mapTabContentAreaAliases){ + let gridArea = document.createElement('div'); + gridArea.classList.add(Util.getMapTabContentAreaClass(), Util.getMapTabContentAreaClass(areaAlias)); + gridArea.setAttribute('data-area', areaAlias); - // "add" tab does not need a structure and observer... - if(mapId > 0){ - let contentStructure = $('
', { - class: ['row', config.mapTabContentRow].join(' ') - }).append( - $('
', { - class: ['col-xs-12', 'col-md-8', config.mapTabContentCellFirst, config.mapTabContentCell].join(' ') - }).attr('data-position', 1) - ).append( - $('
', { - class: ['col-xs-12', 'col-md-4', config.mapTabContentCellSecond, config.mapTabContentCell].join(' ') - }).attr('data-position', 2) - ); - - // append grid structure - tabContentElement.append(contentStructure); - - // set content structure observer - setContentStructureObserver(contentStructure, mapId); - } - }); + gridAreas.push(gridArea); + } + return gridAreas; }; /** - * get a fresh tab element - * @param options - * @param currentUserData - * @returns {*|jQuery|HTMLElement} + * new tabs element + * @returns {HTMLDivElement} */ - let getMapTabElement = (options, currentUserData) => { - let tabElement = $('
', { + let newMapTabsElement = () => { + let tabEl = Object.assign(document.createElement('div'), { id: config.mapTabElementId }); - let tabBar = $('