961 lines
28 KiB
JavaScript
961 lines
28 KiB
JavaScript
;(function(){ 'use strict';
|
|
|
|
var defaults = {
|
|
container: false // can be a HTML or jQuery element or jQuery selector
|
|
, viewLiveFramerate: 0 // set false to update graph pan only on drag end; set 0 to do it instantly; set a number (frames per second) to update not more than N times per second
|
|
, dblClickDelay: 200 // milliseconds
|
|
, removeCustomContainer: true // destroy the container specified by user on plugin destroy
|
|
, rerenderDelay: 500 // ms to throttle rerender updates to the panzoom for performance
|
|
};
|
|
|
|
var debounce = (function(){
|
|
/**
|
|
* lodash 3.1.1 (Custom Build) <https://lodash.com/>
|
|
* Build: `lodash modern modularize exports="npm" -o ./`
|
|
* Copyright 2012-2015 The Dojo Foundation <http://dojofoundation.org/>
|
|
* Based on Underscore.js 1.8.3 <http://underscorejs.org/LICENSE>
|
|
* Copyright 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
|
|
* Available under MIT license <https://lodash.com/license>
|
|
*/
|
|
/** Used as the `TypeError` message for "Functions" methods. */
|
|
var FUNC_ERROR_TEXT = 'Expected a function';
|
|
|
|
/* Native method references for those with the same name as other `lodash` methods. */
|
|
var nativeMax = Math.max,
|
|
nativeNow = Date.now;
|
|
|
|
/**
|
|
* Gets the number of milliseconds that have elapsed since the Unix epoch
|
|
* (1 January 1970 00:00:00 UTC).
|
|
*
|
|
* @static
|
|
* @memberOf _
|
|
* @category Date
|
|
* @example
|
|
*
|
|
* _.defer(function(stamp) {
|
|
* console.log(_.now() - stamp);
|
|
* }, _.now());
|
|
* // => logs the number of milliseconds it took for the deferred function to be invoked
|
|
*/
|
|
var now = nativeNow || function() {
|
|
return new Date().getTime();
|
|
};
|
|
|
|
/**
|
|
* Creates a debounced function that delays invoking `func` until after `wait`
|
|
* milliseconds have elapsed since the last time the debounced function was
|
|
* invoked. The debounced function comes with a `cancel` method to cancel
|
|
* delayed invocations. Provide an options object to indicate that `func`
|
|
* should be invoked on the leading and/or trailing edge of the `wait` timeout.
|
|
* Subsequent calls to the debounced function return the result of the last
|
|
* `func` invocation.
|
|
*
|
|
* **Note:** If `leading` and `trailing` options are `true`, `func` is invoked
|
|
* on the trailing edge of the timeout only if the the debounced function is
|
|
* invoked more than once during the `wait` timeout.
|
|
*
|
|
* See [David Corbacho's article](http://drupalmotion.com/article/debounce-and-throttle-visual-explanation)
|
|
* for details over the differences between `_.debounce` and `_.throttle`.
|
|
*
|
|
* @static
|
|
* @memberOf _
|
|
* @category Function
|
|
* @param {Function} func The function to debounce.
|
|
* @param {number} [wait=0] The number of milliseconds to delay.
|
|
* @param {Object} [options] The options object.
|
|
* @param {boolean} [options.leading=false] Specify invoking on the leading
|
|
* edge of the timeout.
|
|
* @param {number} [options.maxWait] The maximum time `func` is allowed to be
|
|
* delayed before it's invoked.
|
|
* @param {boolean} [options.trailing=true] Specify invoking on the trailing
|
|
* edge of the timeout.
|
|
* @returns {Function} Returns the new debounced function.
|
|
* @example
|
|
*
|
|
* // avoid costly calculations while the window size is in flux
|
|
* jQuery(window).on('resize', _.debounce(calculateLayout, 150));
|
|
*
|
|
* // invoke `sendMail` when the click event is fired, debouncing subsequent calls
|
|
* jQuery('#postbox').on('click', _.debounce(sendMail, 300, {
|
|
* 'leading': true,
|
|
* 'trailing': false
|
|
* }));
|
|
*
|
|
* // ensure `batchLog` is invoked once after 1 second of debounced calls
|
|
* var source = new EventSource('/stream');
|
|
* jQuery(source).on('message', _.debounce(batchLog, 250, {
|
|
* 'maxWait': 1000
|
|
* }));
|
|
*
|
|
* // cancel a debounced call
|
|
* var todoChanges = _.debounce(batchLog, 1000);
|
|
* Object.observe(models.todo, todoChanges);
|
|
*
|
|
* Object.observe(models, function(changes) {
|
|
* if (_.find(changes, { 'user': 'todo', 'type': 'delete'})) {
|
|
* todoChanges.cancel();
|
|
* }
|
|
* }, ['delete']);
|
|
*
|
|
* // ...at some point `models.todo` is changed
|
|
* models.todo.completed = true;
|
|
*
|
|
* // ...before 1 second has passed `models.todo` is deleted
|
|
* // which cancels the debounced `todoChanges` call
|
|
* delete models.todo;
|
|
*/
|
|
function debounce(func, wait, options) {
|
|
var args,
|
|
maxTimeoutId,
|
|
result,
|
|
stamp,
|
|
thisArg,
|
|
timeoutId,
|
|
trailingCall,
|
|
lastCalled = 0,
|
|
maxWait = false,
|
|
trailing = true;
|
|
|
|
if (typeof func != 'function') {
|
|
throw new TypeError(FUNC_ERROR_TEXT);
|
|
}
|
|
wait = wait < 0 ? 0 : (+wait || 0);
|
|
if (options === true) {
|
|
var leading = true;
|
|
trailing = false;
|
|
} else if (isObject(options)) {
|
|
leading = !!options.leading;
|
|
maxWait = 'maxWait' in options && nativeMax(+options.maxWait || 0, wait);
|
|
trailing = 'trailing' in options ? !!options.trailing : trailing;
|
|
}
|
|
|
|
function cancel() {
|
|
if (timeoutId) {
|
|
clearTimeout(timeoutId);
|
|
}
|
|
if (maxTimeoutId) {
|
|
clearTimeout(maxTimeoutId);
|
|
}
|
|
lastCalled = 0;
|
|
maxTimeoutId = timeoutId = trailingCall = undefined;
|
|
}
|
|
|
|
function complete(isCalled, id) {
|
|
if (id) {
|
|
clearTimeout(id);
|
|
}
|
|
maxTimeoutId = timeoutId = trailingCall = undefined;
|
|
if (isCalled) {
|
|
lastCalled = now();
|
|
result = func.apply(thisArg, args);
|
|
if (!timeoutId && !maxTimeoutId) {
|
|
args = thisArg = undefined;
|
|
}
|
|
}
|
|
}
|
|
|
|
function delayed() {
|
|
var remaining = wait - (now() - stamp);
|
|
if (remaining <= 0 || remaining > wait) {
|
|
complete(trailingCall, maxTimeoutId);
|
|
} else {
|
|
timeoutId = setTimeout(delayed, remaining);
|
|
}
|
|
}
|
|
|
|
function maxDelayed() {
|
|
complete(trailing, timeoutId);
|
|
}
|
|
|
|
function debounced() {
|
|
args = arguments;
|
|
stamp = now();
|
|
thisArg = this;
|
|
trailingCall = trailing && (timeoutId || !leading);
|
|
|
|
if (maxWait === false) {
|
|
var leadingCall = leading && !timeoutId;
|
|
} else {
|
|
if (!maxTimeoutId && !leading) {
|
|
lastCalled = stamp;
|
|
}
|
|
var remaining = maxWait - (stamp - lastCalled),
|
|
isCalled = remaining <= 0 || remaining > maxWait;
|
|
|
|
if (isCalled) {
|
|
if (maxTimeoutId) {
|
|
maxTimeoutId = clearTimeout(maxTimeoutId);
|
|
}
|
|
lastCalled = stamp;
|
|
result = func.apply(thisArg, args);
|
|
}
|
|
else if (!maxTimeoutId) {
|
|
maxTimeoutId = setTimeout(maxDelayed, remaining);
|
|
}
|
|
}
|
|
if (isCalled && timeoutId) {
|
|
timeoutId = clearTimeout(timeoutId);
|
|
}
|
|
else if (!timeoutId && wait !== maxWait) {
|
|
timeoutId = setTimeout(delayed, wait);
|
|
}
|
|
if (leadingCall) {
|
|
isCalled = true;
|
|
result = func.apply(thisArg, args);
|
|
}
|
|
if (isCalled && !timeoutId && !maxTimeoutId) {
|
|
args = thisArg = undefined;
|
|
}
|
|
return result;
|
|
}
|
|
debounced.cancel = cancel;
|
|
return debounced;
|
|
}
|
|
|
|
/**
|
|
* Checks if `value` is the [language type](https://es5.github.io/#x8) of `Object`.
|
|
* (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`)
|
|
*
|
|
* @static
|
|
* @memberOf _
|
|
* @category Lang
|
|
* @param {*} value The value to check.
|
|
* @returns {boolean} Returns `true` if `value` is an object, else `false`.
|
|
* @example
|
|
*
|
|
* _.isObject({});
|
|
* // => true
|
|
*
|
|
* _.isObject([1, 2, 3]);
|
|
* // => true
|
|
*
|
|
* _.isObject(1);
|
|
* // => false
|
|
*/
|
|
function isObject(value) {
|
|
// Avoid a V8 JIT bug in Chrome 19-20.
|
|
// See https://code.google.com/p/v8/issues/detail?id=2291 for more details.
|
|
var type = typeof value;
|
|
return !!value && (type == 'object' || type == 'function');
|
|
}
|
|
|
|
return debounce;
|
|
|
|
})();
|
|
|
|
// ported lodash throttle function
|
|
var throttle = function( func, wait, options ){
|
|
var leading = true,
|
|
trailing = true;
|
|
|
|
if( options === false ){
|
|
leading = false;
|
|
} else if( typeof options === typeof {} ){
|
|
leading = 'leading' in options ? options.leading : leading;
|
|
trailing = 'trailing' in options ? options.trailing : trailing;
|
|
}
|
|
options = options || {};
|
|
options.leading = leading;
|
|
options.maxWait = wait;
|
|
options.trailing = trailing;
|
|
|
|
return debounce( func, wait, options );
|
|
};
|
|
|
|
var Navigator = function ( element, options ) {
|
|
this._init(element, options)
|
|
};
|
|
|
|
var extend = function() {
|
|
for(var i = 1; i < arguments.length; i++) {
|
|
for(var key in arguments[i]) {
|
|
if(arguments[i].hasOwnProperty(key)) {
|
|
arguments[0][key] = arguments[i][key];
|
|
}
|
|
}
|
|
}
|
|
return arguments[0];
|
|
};
|
|
|
|
var wid = function(elem) {
|
|
return elem.getBoundingClientRect().width;
|
|
};
|
|
|
|
var hei = function(elem) {
|
|
return elem.getBoundingClientRect().height;
|
|
};
|
|
|
|
Navigator.prototype = {
|
|
|
|
constructor: Navigator
|
|
|
|
/****************************
|
|
Main functions
|
|
****************************/
|
|
|
|
, bb: function(){
|
|
var bb = this.cy.elements().boundingBox()
|
|
|
|
if( bb.w === 0 || bb.h === 0 ){
|
|
return {
|
|
x1: 0,
|
|
x2: Infinity,
|
|
y1: 0,
|
|
y2: Infinity,
|
|
w: Infinity,
|
|
h: Infinity
|
|
} // => hide interactive overlay
|
|
}
|
|
|
|
return bb
|
|
}
|
|
|
|
, _addCyListener: function(events, handler){
|
|
this._cyListeners.push({
|
|
events: events,
|
|
handler: handler
|
|
})
|
|
|
|
this.cy.on(events, handler)
|
|
}
|
|
|
|
, _removeCyListeners: function(){
|
|
var cy = this.cy
|
|
|
|
this._cyListeners.forEach(function(l){
|
|
cy.off(l.events, l.handler)
|
|
})
|
|
|
|
cy.offRender(this._onRenderHandler)
|
|
}
|
|
|
|
, _init: function ( cy, options ) {
|
|
this._cyListeners = []
|
|
|
|
this.$element = cy.container()
|
|
this.options = extend({}, defaults, options)
|
|
|
|
this.cy = cy
|
|
|
|
// Cache bounding box
|
|
this.boundingBox = this.bb()
|
|
|
|
// Cache sizes
|
|
this.width = wid(this.$element);
|
|
this.height = hei(this.$element)
|
|
|
|
// Init components
|
|
this._initPanel()
|
|
this._initThumbnail()
|
|
this._initView()
|
|
this._initOverlay()
|
|
}
|
|
|
|
, destroy: function () {
|
|
this._removeEventsHandling();
|
|
|
|
// If container is not created by navigator and its removal is prohibited
|
|
if (this.options.container && !this.options.removeCustomContainer) {
|
|
this.$panel.innerHTML = '';
|
|
} else {
|
|
this.$panel.parentElement.removeChild(this.$panel);
|
|
}
|
|
}
|
|
|
|
/****************************
|
|
Navigator elements functions
|
|
****************************/
|
|
|
|
/*
|
|
* Used inner attributes
|
|
*
|
|
* w {number} width
|
|
* h {number} height
|
|
*/
|
|
, _initPanel: function () {
|
|
var options = this.options
|
|
if(options.container && typeof options.container === 'string' && options.container.length > 0) {
|
|
// to not break users which gives a jquery string selector
|
|
if (options.container.indexOf('#') !== -1) {
|
|
this.$panel = document.getElementById(options.container.replace('#', ''));
|
|
} else {
|
|
this.$panel = document.getElementsByClassName(options.container.replace('.', ''))[0];
|
|
}
|
|
} else {
|
|
this.$panel = document.createElement('div');
|
|
this.$panel.className = 'cytoscape-navigator';
|
|
document.body.appendChild(this.$panel);
|
|
}
|
|
this._setupPanel()
|
|
this._addCyListener('resize', this.resize.bind(this))
|
|
}
|
|
|
|
, _setupPanel: function () {
|
|
// Cache sizes
|
|
this.panelWidth = wid(this.$panel);
|
|
this.panelHeight = hei(this.$panel);
|
|
}
|
|
|
|
/*
|
|
* Used inner attributes
|
|
*
|
|
* zoom {number}
|
|
* pan {object} - {x: 0, y: 0}
|
|
*/
|
|
, _initThumbnail: function () {
|
|
// Create thumbnail
|
|
this.$thumbnail = document.createElement('img');
|
|
|
|
// Add thumbnail canvas to the DOM
|
|
this.$panel.appendChild(this.$thumbnail);
|
|
|
|
// Setup thumbnail
|
|
this._setupThumbnailSizes()
|
|
this._setupThumbnail()
|
|
}
|
|
|
|
, _setupThumbnail: function () {
|
|
this._updateThumbnailImage()
|
|
}
|
|
|
|
, _setupThumbnailSizes: function () {
|
|
// Update bounding box cache
|
|
this.boundingBox = this.bb()
|
|
|
|
this.thumbnailZoom = Math.min(this.panelHeight / this.boundingBox.h, this.panelWidth / this.boundingBox.w)
|
|
|
|
// Used on thumbnail generation
|
|
this.thumbnailPan = {
|
|
x: (this.panelWidth - this.thumbnailZoom * (this.boundingBox.x1 + this.boundingBox.x2))/2
|
|
, y: (this.panelHeight - this.thumbnailZoom * (this.boundingBox.y1 + this.boundingBox.y2))/2
|
|
}
|
|
}
|
|
|
|
// If bounding box has changed then update sizes
|
|
// Otherwise just update the thumbnail
|
|
, _checkThumbnailSizesAndUpdate: function () {
|
|
// Cache previous values
|
|
var _zoom = this.thumbnailZoom
|
|
, _pan_x = this.thumbnailPan.x
|
|
, _pan_y = this.thumbnailPan.y
|
|
|
|
this._setupThumbnailSizes()
|
|
|
|
if (_zoom != this.thumbnailZoom || _pan_x != this.thumbnailPan.x || _pan_y != this.thumbnailPan.y) {
|
|
this._setupThumbnail()
|
|
this._setupView()
|
|
} else {
|
|
this._updateThumbnailImage()
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Used inner attributes
|
|
*
|
|
* w {number} width
|
|
* h {number} height
|
|
* x {number}
|
|
* y {number}
|
|
* borderWidth {number}
|
|
* locked {boolean}
|
|
*/
|
|
, _initView: function () {
|
|
this.$view = document.createElement('div');
|
|
this.$view.className = 'cytoscape-navigatorView';
|
|
this.$panel.appendChild(this.$view)
|
|
// Compute borders
|
|
this.viewBorderTop = parseInt(this.$view.style['border-top-width'], 10) || 0;
|
|
this.viewBorderRight = parseInt(this.$view.style['border-right-width'], 10) || 0;
|
|
this.viewBorderBottom = parseInt(this.$view.style['border-bottom-width'], 10) || 0;
|
|
this.viewBorderLeft = parseInt(this.$view.style['border-left-width'], 10) || 0;
|
|
|
|
// Abstract borders
|
|
this.viewBorderHorizontal = this.viewBorderLeft + this.viewBorderRight
|
|
this.viewBorderVertical = this.viewBorderTop + this.viewBorderBottom
|
|
|
|
this._setupView()
|
|
|
|
// Hook graph zoom and pan
|
|
this._addCyListener('zoom pan', this._setupView.bind(this))
|
|
}
|
|
|
|
, _setupView: function () {
|
|
if (this.viewLocked)
|
|
return
|
|
|
|
var cyZoom = this.cy.zoom()
|
|
, cyPan = this.cy.pan()
|
|
|
|
// Horizontal computation
|
|
this.viewW = this.width / cyZoom * this.thumbnailZoom
|
|
this.viewX = -cyPan.x * this.viewW / this.width + this.thumbnailPan.x - this.viewBorderLeft
|
|
|
|
// Vertical computation
|
|
this.viewH = this.height / cyZoom * this.thumbnailZoom
|
|
this.viewY = -cyPan.y * this.viewH / this.height + this.thumbnailPan.y - this.viewBorderTop
|
|
|
|
// CSS view
|
|
this.$view.style['width'] = this.viewW + 'px';
|
|
this.$view.style['height'] = this.viewH + 'px';
|
|
this.$view.style['position'] = 'absolute';
|
|
this.$view.style['left'] = this.viewX + 'px';
|
|
this.$view.style['top'] = this.viewY + 'px';
|
|
}
|
|
|
|
/*
|
|
* Used inner attributes
|
|
*
|
|
* timeout {number} used to keep stable frame rate
|
|
* lastMoveStartTime {number}
|
|
* inMovement {boolean}
|
|
* hookPoint {object} {x: 0, y: 0}
|
|
*/
|
|
, _initOverlay: function () {
|
|
// Used to capture mouse events
|
|
this.$overlay = document.createElement('div');
|
|
this.$overlay.className = 'cytoscape-navigatorOverlay';
|
|
|
|
// Add overlay to the DOM
|
|
this.$panel.appendChild(this.$overlay)
|
|
|
|
// Init some attributes
|
|
this.overlayHookPointX = 0;
|
|
this.overlayHookPointY = 0;
|
|
|
|
// Listen for events
|
|
this._initEventsHandling()
|
|
}
|
|
|
|
/****************************
|
|
Event handling functions
|
|
****************************/
|
|
|
|
, resize: function () {
|
|
// Cache sizes
|
|
this.width = wid(this.$element);
|
|
this.height = hei(this.$element);
|
|
this._thumbnailSetup = false
|
|
this._setupPanel()
|
|
this._checkThumbnailSizesAndUpdate()
|
|
this._setupView()
|
|
}
|
|
|
|
, _initEventsHandling: function () {
|
|
var that = this
|
|
, eventsLocal = [
|
|
// Mouse events
|
|
'mousedown'
|
|
, 'mousewheel'
|
|
, 'DOMMouseScroll' // Mozilla specific event
|
|
// Touch events
|
|
, 'touchstart'
|
|
]
|
|
, eventsGlobal = [
|
|
'mouseup'
|
|
, 'mouseout'
|
|
, 'mousemove'
|
|
// Touch events
|
|
, 'touchmove'
|
|
, 'touchend'
|
|
]
|
|
|
|
// handle events and stop their propagation
|
|
var overlayListener = function (ev) {
|
|
// Touch events
|
|
if (ev.type == 'touchstart') {
|
|
// Will count as middle of View
|
|
Object.defineProperty(ev, 'offsetX', {
|
|
value: that.viewX + that.viewW / 2,
|
|
writable: true
|
|
});
|
|
Object.defineProperty(ev, 'offsetY', {
|
|
value: that.viewY + that.viewH / 2,
|
|
writable: true
|
|
});
|
|
}
|
|
|
|
// Normalize offset for browsers which do not provide that value
|
|
if (ev.offsetX === undefined || ev.offsetY === undefined) {
|
|
var rect = ev.target.getBoundingClientRect();
|
|
var targetOffset = {
|
|
top: rect.top + window.scrollY,
|
|
left: rect.left + window.scrollX,
|
|
};
|
|
Object.defineProperty(ev, 'offsetX', {
|
|
value: ev.pageX - targetOffset.left,
|
|
writable: true
|
|
});
|
|
Object.defineProperty(ev, 'offsetY', {
|
|
value: ev.pageY - targetOffset.top,
|
|
writable: true
|
|
});
|
|
}
|
|
|
|
if (ev.type == 'mousedown' || ev.type == 'touchstart') {
|
|
that._eventMoveStart(ev)
|
|
} else if (ev.type == 'mousewheel' || ev.type == 'DOMMouseScroll') {
|
|
that._eventZoom(ev)
|
|
}
|
|
|
|
// Prevent default and propagation
|
|
// Don't use peventPropagation as it breaks mouse events
|
|
return false;
|
|
};
|
|
|
|
// Hook global events
|
|
var globalListener = function (ev) {
|
|
|
|
// Do not make any computations if it is has no effect on Navigator
|
|
if (!that.overlayInMovement)
|
|
return;
|
|
|
|
// Touch events
|
|
if (ev.type == 'touchend') {
|
|
// Will count as middle of View
|
|
Object.defineProperty(ev, 'offsetX', {
|
|
value: that.viewX + that.viewW / 2,
|
|
writable: true
|
|
});
|
|
Object.defineProperty(ev, 'offsetY', {
|
|
value: that.viewY + that.viewH / 2,
|
|
writable: true
|
|
});
|
|
} else if (ev.type == 'touchmove') {
|
|
// Hack - we take in account only first touch
|
|
Object.defineProperty(ev, 'pageX', {
|
|
value: ev.originalEvent.touches[0].pageX,
|
|
writable: true
|
|
});
|
|
Object.defineProperty(ev, 'pageY', {
|
|
value: ev.originalEvent.touches[0].pageY,
|
|
writable: true
|
|
});
|
|
}
|
|
|
|
// Normalize offset for browsers which do not provide that value
|
|
if (ev.offsetX === undefined || ev.offsetY === undefined) {
|
|
var rect = ev.target.getBoundingClientRect();
|
|
var targetOffset = {
|
|
top: rect.top + window.scrollY,
|
|
left: rect.left + window.scrollX,
|
|
};
|
|
Object.defineProperty(ev, 'offsetX', {
|
|
value: ev.pageX - targetOffset.left,
|
|
writable: true
|
|
});
|
|
Object.defineProperty(ev, 'offsetY', {
|
|
value: ev.pageY - targetOffset.top,
|
|
writable: true
|
|
});
|
|
}
|
|
|
|
// Translate global events into local coordinates
|
|
if (ev.target !== that.$overlay) {
|
|
var rect = ev.target.getBoundingClientRect();
|
|
var rect2 = that.$overlay.getBoundingClientRect();
|
|
var targetOffset = {
|
|
top: rect.top + window.scrollY,
|
|
left: rect.left + window.scrollX,
|
|
};
|
|
var overlayOffset = {
|
|
top: rect2.top + window.scrollY,
|
|
left: rect2.left + window.scrollX,
|
|
};
|
|
|
|
if(targetOffset && overlayOffset) {
|
|
Object.defineProperty(ev, 'offsetX', {
|
|
value: ev.offsetX - overlayOffset.left + targetOffset.left,
|
|
writable: true
|
|
});
|
|
Object.defineProperty(ev, 'offsetY', {
|
|
value: ev.offsetY - overlayOffset.top + targetOffset.top,
|
|
writable: true
|
|
});
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (ev.type == 'mousemove' || ev.type == 'touchmove') {
|
|
that._eventMove(ev)
|
|
} else if (ev.type == 'mouseup' || ev.type == 'touchend') {
|
|
that._eventMoveEnd(ev)
|
|
}
|
|
|
|
// Prevent default and propagation
|
|
// Don't use peventPropagation as it breaks mouse events
|
|
return false;
|
|
};
|
|
|
|
for (var i = 0; i < eventsLocal.length; i++) {
|
|
this.$overlay.addEventListener(eventsLocal[i], overlayListener, false);
|
|
}
|
|
|
|
for (var i = 0; i < eventsGlobal.length; i++) {
|
|
window.addEventListener(eventsGlobal[i], globalListener, false);
|
|
}
|
|
|
|
this._removeEventsHandling = function(){
|
|
|
|
for (var i = 0; i < eventsLocal.length; i++) {
|
|
this.$overlay.removeEventListener(eventsLocal[i], overlayListener);
|
|
}
|
|
|
|
for (var i = 0; i < eventsGlobal.length; i++) {
|
|
window.removeEventListener(eventsGlobal[i], globalListener);
|
|
}
|
|
}
|
|
}
|
|
|
|
, _eventMoveStart: function (ev) {
|
|
var now = new Date().getTime()
|
|
|
|
// Check if it was double click
|
|
if (this.overlayLastMoveStartTime
|
|
&& this.overlayLastMoveStartTime + this.options.dblClickDelay > now) {
|
|
// Reset lastMoveStartTime
|
|
this.overlayLastMoveStartTime = 0
|
|
// Enable View in order to move it to the center
|
|
this.overlayInMovement = true
|
|
|
|
// Set hook point as View center
|
|
this.overlayHookPointX = this.viewW / 2
|
|
this.overlayHookPointY = this.viewH / 2
|
|
|
|
// Move View to start point
|
|
if (this.options.viewLiveFramerate !== false) {
|
|
this._eventMove({
|
|
offsetX: this.panelWidth / 2
|
|
, offsetY: this.panelHeight / 2
|
|
})
|
|
} else {
|
|
this._eventMoveEnd({
|
|
offsetX: this.panelWidth / 2
|
|
, offsetY: this.panelHeight / 2
|
|
})
|
|
}
|
|
|
|
// View should be inactive as we don't want to move it right after double click
|
|
this.overlayInMovement = false
|
|
}
|
|
// This is a single click
|
|
// Take care as single click happens before double click 2 times
|
|
else {
|
|
this.overlayLastMoveStartTime = now
|
|
this.overlayInMovement = true
|
|
// Lock view moving caused by cy events
|
|
this.viewLocked = true
|
|
|
|
// if event started in View
|
|
if (ev.offsetX >= this.viewX && ev.offsetX <= this.viewX + this.viewW
|
|
&& ev.offsetY >= this.viewY && ev.offsetY <= this.viewY + this.viewH
|
|
) {
|
|
this.overlayHookPointX = ev.offsetX - this.viewX
|
|
this.overlayHookPointY = ev.offsetY - this.viewY
|
|
}
|
|
// if event started in Thumbnail (outside of View)
|
|
else {
|
|
// Set hook point as View center
|
|
this.overlayHookPointX = this.viewW / 2
|
|
this.overlayHookPointY = this.viewH / 2
|
|
|
|
// Move View to start point
|
|
this._eventMove(ev)
|
|
}
|
|
}
|
|
}
|
|
|
|
, _eventMove: function (ev) {
|
|
var that = this
|
|
|
|
this._checkMousePosition(ev)
|
|
|
|
// break if it is useless event
|
|
if (!this.overlayInMovement) {
|
|
return;
|
|
}
|
|
|
|
// Update cache
|
|
this.viewX = ev.offsetX - this.overlayHookPointX
|
|
this.viewY = ev.offsetY - this.overlayHookPointY
|
|
|
|
// Update view position
|
|
this.$view.style['left'] = this.viewX + 'px';
|
|
this.$view.style['top'] = this.viewY + 'px';
|
|
|
|
// Move Cy
|
|
if (this.options.viewLiveFramerate !== false) {
|
|
// trigger instantly
|
|
if (this.options.viewLiveFramerate == 0) {
|
|
this._moveCy()
|
|
}
|
|
// trigger less often than frame rate
|
|
else if (!this.overlayTimeout) {
|
|
// Set a timeout for graph movement
|
|
this.overlayTimeout = setTimeout(function () {
|
|
that._moveCy()
|
|
that.overlayTimeout = false
|
|
}, 1000 / this.options.viewLiveFramerate)
|
|
}
|
|
}
|
|
}
|
|
|
|
, _checkMousePosition: function (ev) {
|
|
// If mouse in over View
|
|
if(ev.offsetX > this.viewX && ev.offsetX < this.viewX + this.viewBorderHorizontal + this.viewW
|
|
&& ev.offsetY > this.viewY && ev.offsetY < this.viewY + this.viewBorderVertical + this.viewH) {
|
|
this.$panel.classList.add('mouseover-view')
|
|
} else {
|
|
this.$panel.classList.remove('mouseover-view')
|
|
}
|
|
}
|
|
|
|
, _eventMoveEnd: function (ev) {
|
|
// Unlock view changing caused by graph events
|
|
this.viewLocked = false
|
|
|
|
// Remove class when mouse is not over Navigator
|
|
this.$panel.classList.remove('mouseover-view')
|
|
|
|
if (!this.overlayInMovement) {
|
|
return;
|
|
}
|
|
|
|
// Trigger one last move
|
|
this._eventMove(ev)
|
|
|
|
// If mode is not live then move graph on drag end
|
|
if (this.options.viewLiveFramerate === false) {
|
|
this._moveCy()
|
|
}
|
|
|
|
// Stop movement permission
|
|
this.overlayInMovement = false
|
|
}
|
|
|
|
, _eventZoom: function (ev) {
|
|
var ev2 = extend({}, ev.originalEvent);
|
|
var delta = ev.wheelDeltaY / 1000 || ev.wheelDelta / 1000 || ev.detail / -32 || ev2.wheelDeltaY / 1000 || ev2.wheelDelta / 1000 || ev2.detail / -32;
|
|
var zoomRate = Math.pow(10, delta)
|
|
, mousePosition = {
|
|
left: ev.offsetX
|
|
, top: ev.offsetY
|
|
}
|
|
|
|
if (this.cy.zoomingEnabled()) {
|
|
this._zoomCy(zoomRate, mousePosition)
|
|
}
|
|
}
|
|
|
|
, _updateThumbnailImage: function () {
|
|
var that = this;
|
|
|
|
if( this._thumbnailUpdating ){
|
|
return;
|
|
}
|
|
|
|
this._thumbnailUpdating = true;
|
|
|
|
var render = function() {
|
|
that._checkThumbnailSizesAndUpdate();
|
|
that._setupView();
|
|
|
|
var $img = that.$thumbnail;
|
|
var img = $img;
|
|
|
|
var w = that.panelWidth;
|
|
var h = that.panelHeight;
|
|
var bb = that.boundingBox;
|
|
var zoom = Math.min( w/bb.w, h/bb.h );
|
|
|
|
var png = that.cy.png({
|
|
full: true,
|
|
scale: zoom,
|
|
maxHeight: h,
|
|
maxWidth: w
|
|
});
|
|
if( png.indexOf('image/png') < 0 ){
|
|
img.removeAttribute( 'src' );
|
|
} else {
|
|
img.setAttribute( 'src', png );
|
|
}
|
|
|
|
var translate = {
|
|
x: (w - zoom*( bb.w ))/2,
|
|
y: (h - zoom*( bb.h ))/2
|
|
};
|
|
|
|
$img.style['position'] = 'absolute';
|
|
$img.style['left'] = translate.x + 'px';
|
|
$img.style['top'] = translate.y + 'px';
|
|
|
|
}
|
|
|
|
this._onRenderHandler = throttle(render, that.options.rerenderDelay)
|
|
|
|
this.cy.onRender( this._onRenderHandler )
|
|
}
|
|
|
|
/****************************
|
|
Navigator view moving
|
|
****************************/
|
|
|
|
, _moveCy: function () {
|
|
this.cy.pan({
|
|
x: -(this.viewX + this.viewBorderLeft - this.thumbnailPan.x) * this.width / this.viewW
|
|
, y: -(this.viewY + this.viewBorderLeft - this.thumbnailPan.y) * this.height / this.viewH
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Zooms graph.
|
|
*
|
|
* @this {cytoscapeNavigator}
|
|
* @param {number} zoomRate The zoom rate value. 1 is 100%.
|
|
*/
|
|
, _zoomCy: function (zoomRate, zoomCenterRaw) {
|
|
var zoomCenter
|
|
, isZoomCenterInView = false
|
|
|
|
zoomCenter = {
|
|
x: this.width / 2
|
|
, y: this.height / 2
|
|
};
|
|
|
|
this.cy.zoom({
|
|
level: this.cy.zoom() * zoomRate
|
|
, renderedPosition: zoomCenter
|
|
})
|
|
}
|
|
}
|
|
|
|
// registers the extension on a cytoscape lib ref
|
|
var register = function( cytoscape ){
|
|
|
|
if (!cytoscape){ return; } // can't register if cytoscape unspecified
|
|
|
|
cytoscape( 'core', 'navigator', function( options ){
|
|
var cy = this;
|
|
|
|
return new Navigator( cy, options );
|
|
} );
|
|
|
|
};
|
|
|
|
if (typeof module !== 'undefined' && module.exports) { // expose as a commonjs module
|
|
module.exports = function( cytoscape ){
|
|
register( cytoscape );
|
|
};
|
|
} else if (typeof define !== 'undefined' && define.amd) { // expose as an amd/requirejs module
|
|
define('cytoscape-navigator', function(){
|
|
return register;
|
|
});
|
|
}
|
|
|
|
if (typeof cytoscape !== 'undefined') { // expose to global cytoscape (i.e. window.cytoscape)
|
|
register(cytoscape);
|
|
}
|
|
|
|
})();
|