diff --git a/js/app.js b/js/app.js index b60bef4b..0e5ce63f 100644 --- a/js/app.js +++ b/js/app.js @@ -28,6 +28,7 @@ requirejs.config({ velocityUI: 'lib/velocity.ui.min', // v5.0.4 plugin for velocity - http://julian.com/research/velocity/#uiPack slidebars: 'lib/slidebars', // v0.10 Slidebars - side menu plugin http://plugins.adchsm.me/slidebars/ jsPlumb: 'lib/dom.jsPlumb-1.7.6-min', // v1.7.6 jsPlumb (Vanilla)- main map draw plugin https://jsplumbtoolkit.com/ + farahey: 'lib/farahey-0.5', // v0.5 jsPlumb "magnetizing" extension - https://github.com/jsplumb/farahey customScrollbar: 'lib/jquery.mCustomScrollbar.concat.min', // v3.0.9 Custom scroll bars - http://manos.malihu.gr/ datatables: 'lib/datatables/jquery.dataTables.min', // v1.10.7 DataTables - https://datatables.net/ //datatablesBootstrap: 'lib/datatables/dataTables.bootstrap', // DataTables - not used (bootstrap style) @@ -70,6 +71,9 @@ requirejs.config({ bootstrap: { deps: ['jquery'] }, + farahey: { + deps: ['jsPlumb'] + }, velocity: { deps: ['jquery'] }, diff --git a/js/app/map/magnetizing.js b/js/app/map/magnetizing.js new file mode 100644 index 00000000..a4e2afb1 --- /dev/null +++ b/js/app/map/magnetizing.js @@ -0,0 +1,156 @@ +/** + * Map "magnetizing" feature + * jsPlumb extension used: http://morrisonpitt.com/farahey/ + */ + +define([ + 'jquery', + 'farahey' +], function($) { + + 'use strict'; + + var config = { + systemClass: 'pf-system' // class for all systems + }; + + /** + * Cached current "Magnetizer" object + * @type {Magnetizer} + */ + var m8 = null; + + /** + * init a jsPlumb (map) Element for "magnetised" function. + * this is optional and prevents systems from being overlapped + */ + $.fn.initMagnetizer = function(){ + var mapContainer = this; + + var systemsOnMap = mapContainer.find('.' + config.systemClass); + + /** + * helper function + * get current system offset + * @param system + * @returns {{left, top}} + * @private + */ + var _offset = function(system) { + + var _ = function(p) { + var v = system.style[p]; + return parseInt(v.substring(0, v.length - 2)); + }; + + return { + left:_('left'), + top:_('top') + }; + }; + + /** + * helper function + * set new syste offset + * @param system + * @param o + * @private + */ + var _setOffset = function(system, o) { + + var markAsUpdated = false; + + // new position must be within parent container + // no negative offset! + if( + o.left >= 0 && + o.left <= 2300 + ){ + markAsUpdated = true; + system.style.left = o.left + 'px'; + } + + if( + o.top >= 0 && + o.top <= 498 + ){ + markAsUpdated = true; + system.style.top = o.top + 'px'; + } + + if(markAsUpdated === true){ + $(system).markAsChanged(); + } + }; + + /** + * helper function + * exclude current dragged element(s) from position update + * @param id + * @returns {boolean} + * @private + */ + var _dragFilter = function(id) { + + return !$('#' + id).hasClass('jsPlumb_dragged'); + }; + + // main init for "magnetize" feature ------------------------------------------------------ + m8 = new Magnetizer({ + container: mapContainer, + getContainerPosition:function(c) { + return c.offset(); + }, + getPosition:_offset, + getSize: function(system) { + return [ $(system).outerWidth(), $(system).outerHeight() ]; + }, + getId : function(system) { + return $(system).attr('id'); + }, + setPosition:_setOffset, + elements: systemsOnMap, + filter:_dragFilter, + padding:[8, 8] + }); + + }; + + $.fn.destroyMagnetizer = function(){ + var mapContainer = this; + + // remove cached "magnetizer" instance + m8 = null; + }; + + /** + * update system positions for "all" systems that are effected by drag&drop + * @param map + * @param e + */ + var executeAtEvent = function(map, e){ + + // check if magnetizer is active + if(m8 !== null && e ){ + m8.executeAtEvent(e); + map.repaintEverything(); + } + }; + + /** + * rearrange all systems of a map + * needs "magnetization" to be active + * @param map + */ + var executeAtCenter = function(map){ + if(m8 !== null){ + m8.executeAtCenter(); + map.repaintEverything(); + } + }; + + return { + executeAtCenter: executeAtCenter, + executeAtEvent: executeAtEvent + }; +}); \ No newline at end of file diff --git a/js/app/map/map.js b/js/app/map/map.js index dc5e09f2..c4b92e3e 100644 --- a/js/app/map/map.js +++ b/js/app/map/map.js @@ -1,3 +1,7 @@ +/** + * Main map functionality + */ + define([ 'jquery', 'app/init', @@ -5,12 +9,12 @@ define([ 'app/render', 'bootbox', 'app/ccp', - 'jsPlumb', + 'app/map/magnetizing', 'dragToSelect', 'select2', 'app/map/contextmenu', 'app/map/overlay' -], function($, Init, Util, Render, bootbox, CCP) { +], function($, Init, Util, Render, bootbox, CCP, MagnetizerWrapper) { 'use strict'; @@ -22,7 +26,8 @@ define([ }, mapSnapToGridDimension: 20, // px for grid snapping (grid YxY) - mapSnapToGrid: false, // Snap systems to grid while dragging + mapSnapToGrid: false, // "Snap to Grid" feature for drag&drop systems on map (optional) + mapMagnetizer: false, // "Magnetizer" feature for drag&drop systems on map (optional) mapTabContentClass: 'pf-map-tab-content', // Tab-Content element (parent element) mapWrapperClass: 'pf-map-wrapper', // wrapper div (scrollable) headMapTrackingId: 'pf-head-map-tracking', // id for "map tracking" toggle (checkbox) @@ -616,6 +621,8 @@ define([ var mapContainer = mapConfig.map.getContainer(); + var newSystems = 0; + if(mapContainer === undefined){ // add new map @@ -688,6 +695,7 @@ define([ if( addNewSystem === true){ drawSystem(mapConfig.map, systemData); + newSystems++; } } @@ -780,6 +788,16 @@ define([ } }); + + + // init/update map "magnetization" feature if new systems where added + if( + config.mapMagnetizer === true && + newSystems > 0 + ){ + mapContainer.initMagnetizer(); + } + } return mapContainer; @@ -876,7 +894,10 @@ define([ connectorElements.velocity('transition.fadeIn', { stagger: 30, - duration: 120 + duration: 120, + complete: function(){ + callback(); + } }); // show overlay elements (if some exist) @@ -884,10 +905,7 @@ define([ overlayElements.delay(500).velocity('transition.fadeIn', { stagger: 50, duration: 180, - display: 'auto', - complete: function(){ - callback(); - } + display: 'auto' }); } } @@ -1677,7 +1695,7 @@ define([ constrain: true, //scroll: true, // not working because of customized scrollbar filter: '.' + config.systemHeadNameClass, // disable drag on "system name" - snapThreshold: config.mapSnapToGridDimension, // distance for grid snapping "magnet" effect + snapThreshold: config.mapSnapToGridDimension, // distance for grid snapping "magnet" effect (optional) start: function(params, a, b){ var dragSystem = $(params.el); @@ -1707,9 +1725,13 @@ define([ // move them to the "top" $(selectedSystems).updateSystemZIndex(); }, - drag: function(){ + drag: function(p){ // start map update timer mapOverlayTimer.startMapUpdateCounter(); + + // update system positions for "all" systems that are effected by drag&drop + // this requires "magnet" feature to be active! (optional) + MagnetizerWrapper.executeAtEvent(map, p.e); }, stop: function(params){ var dragSystem = $(params.el); @@ -1736,7 +1758,6 @@ define([ // set all selected systems as "changes" for update tempSystem.markAsChanged(); - // set new position for popover edit field (system name) var tempPosition = tempSystem.position(); @@ -2035,7 +2056,6 @@ define([ // connection element this.setParameter('updated', 0); } - }); }; @@ -2419,32 +2439,44 @@ define([ // catch events ========================================================= - // toggle "snap to grid" option - $(mapContainer).on('pf:menuGrid', function(e, data){ + // toggle global map option (e.g. "grid snap", "magnetization" + $(mapContainer).on('pf:menuMapOption', function(e, data){ var currentMapElement = $(this); - config.mapSnapToGrid = !config.mapSnapToGrid; + // toggle map option + config[data.option] = !config[data.option]; - // toggle grid class - currentMapElement.toggleClass(config.mapGridClass); + // toggle map class (e.g. for grid) + if(data.class){ + currentMapElement.toggleClass( config[data.class] ); + } // toggle button class $(data.button).toggleClass('active'); var notificationText = 'disabled'; - if(config.mapSnapToGrid){ + if( config[data.option] ){ + + // call optional jQuery extension on mapElement + if(data.onEnable){ + $.fn[ data.onEnable ].apply( currentMapElement ); + } + + // show map overlay info icon notificationText = 'enabled'; - - // show map overlay grid info - currentMapElement.getMapOverlay('info').updateOverlayIcon('grid', 'show'); + currentMapElement.getMapOverlay('info').updateOverlayIcon(data.option, 'show'); }else{ + // call optional jQuery extension on mapElement + if(data.onDisable){ + $.fn[ data.onDisable ].apply( currentMapElement ); + } - // hide map overlay grid info - currentMapElement.getMapOverlay('info').updateOverlayIcon('grid', 'hide'); + // hide map overlay info icon + currentMapElement.getMapOverlay('info').updateOverlayIcon(data.option, 'hide'); } - Util.showNotify({title: 'Grid snapping', text: notificationText, type: 'info'}); + Util.showNotify({title: data.description, text: notificationText, type: 'info'}); }); // delete system event @@ -2902,6 +2934,15 @@ define([ Util.showNotify({title: 'New system', text: newSystemData.name, type: 'success'}); + // re-init "magnetizer" with new added system + if(config.mapMagnetizer === true){ + var mapContainer = this.map.getContainer(); + $(mapContainer).initMagnetizer(); + + // re/arrange systems (prevent overlapping) + MagnetizerWrapper.executeAtCenter(this.map); + } + if(callback){ callback(); } @@ -3319,19 +3360,19 @@ define([ jsPlumb.Defaults.LogEnabled = true; var newJsPlumbInstance = jsPlumb.getInstance({ - Anchor: 'Continuous', // anchors on each site - Container: null, // will be set as soon as container is connected to DOM + Anchor: 'Continuous', // anchors on each site + Container: null, // will be set as soon as container is connected to DOM PaintStyle:{ - lineWidth: 4, // width of a Connector's line. An integer. - strokeStyle: 'red', // color for a Connector - outlineColor: 'red', // color of the outline for an Endpoint or Connector. see fillStyle examples. - outlineWidth: 2 // width of the outline for an Endpoint or Connector. An integer. + lineWidth: 4, // width of a Connector's line. An integer. + strokeStyle: 'red', // color for a Connector + outlineColor: 'red', // color of the outline for an Endpoint or Connector. see fillStyle examples. + outlineWidth: 2 // width of the outline for an Endpoint or Connector. An integer. }, - Connector:[ 'Bezier', { curviness: 40 } ], // default connector style (this is not used!) all connections have their own style (by scope) + Connector:[ 'Bezier', { curviness: 40 } ], // default connector style (this is not used!) all connections have their own style (by scope) Endpoints: [ [ 'Dot', { radius: 5 } ], [ 'Dot', { radius: 5 } ] ], // Endpoint: 'Blank', // does not work... :( - ReattachConnections: false, // re-attach connection if dragged with mouse to "nowhere" - Scope: Init.defaultMapScope, // default map scope for connections + ReattachConnections: false, // re-attach connection if dragged with mouse to "nowhere" + Scope: Init.defaultMapScope, // default map scope for connections LogEnabled: true }); @@ -3447,18 +3488,23 @@ define([ } // callback function after tab switch - function switchTabCallback( mapName ){ + function switchTabCallback( mapName, mapContainer ){ Util.showNotify({title: 'Map initialized', text: mapName + ' - loaded', type: 'success'}); + if( config.mapMagnetizer === true ){ + mapContainer.initMagnetizer(); + } + return false; } if(options.showAnimation){ // show nice visualization effect mapContainer.visualizeMap('show', function(){ - switchTabCallback( mapConfig.config.name ); + switchTabCallback( mapConfig.config.name, mapContainer ); }); } + }); }; diff --git a/js/app/map/overlay.js b/js/app/map/overlay.js index e2c47d1a..00e79471 100644 --- a/js/app/map/overlay.js +++ b/js/app/map/overlay.js @@ -19,12 +19,27 @@ define([ // map overlays mapOverlayClass: 'pf-map-overlay', // class for all map overlays mapOverlayTimerClass: 'pf-map-overlay-timer', // class for map overlay timer e.g. map timer - mapOverlayInfoClass: 'pf-map-overlay-info', // class for map overlay info e.g. map info + mapOverlayInfoClass: 'pf-map-overlay-info' // class for map overlay info e.g. map info - // map overlay icons - mapOverlayFilterClass: 'pf-map-overlay-filter', // class for "filter" icon within a overlay - mapOverlayGridClass: 'pf-map-overlay-grid' // class for "grid" icon within a overlay + }; + // overlay options (all available map options shown in overlay) + var options = { + filter: { + title: 'active filter', + class: 'pf-map-overlay-filter', + iconClass: ['fa', 'fa-fw', 'fa-filter'] + }, + mapSnapToGrid: { + title: 'active grid', + class: 'pf-map-overlay-grid', + iconClass: ['glyphicon', 'glyphicon-th'] + }, + mapMagnetizer: { + title: 'active magnetizer', + class: 'pf-map-overlay-magnetizer', + iconClass: ['fa', 'fa-fw', 'fa-magnet'] + } }; /** @@ -172,24 +187,18 @@ define([ /** * update (show/hide) a overlay icon in the "info"-overlay * show/hide the overlay itself is no icons are visible - * @param iconName + * @param option * @param viewType */ - $.fn.updateOverlayIcon = function(iconName, viewType){ + $.fn.updateOverlayIcon = function(option, viewType){ var mapOverlayInfo = $(this); var showOverlay = false; + var mapOverlayIconClass = options[option].class; + // look for the overlay icon that should be updated - var iconElement = null; - switch(iconName){ - case 'filter': - iconElement = mapOverlayInfo.find('.' + config.mapOverlayFilterClass); - break; - case 'grid': - iconElement = mapOverlayInfo.find('.' + config.mapOverlayGridClass); - break; - } + var iconElement = mapOverlayInfo.find('.' + mapOverlayIconClass); if(iconElement){ if(viewType === 'show'){ @@ -198,7 +207,7 @@ define([ }else if(viewType === 'hide'){ iconElement.hide(); - // check if ther is any visible icon remaining + // check if there is any visible icon remaining var visibleIcons = mapOverlayInfo.find('i:visible'); if(visibleIcons.length > 0){ showOverlay = true; @@ -228,33 +237,33 @@ define([ */ $.fn.initMapOverlays = function(){ return this.each(function(){ - var parentElemtn = $(this); + var parentElement = $(this); var mapOverlayTimer = $('