mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2024-11-18 23:21:30 +08:00
2975 lines
92 KiB
Text
2975 lines
92 KiB
Text
//= require comments
|
|
|
|
//************************************
|
|
// CONSTANTS
|
|
//************************************
|
|
|
|
var DRAG_INVALID = 0;
|
|
var DRAG_MOUSE = 1;
|
|
var DRAG_TOUCH = 2;
|
|
|
|
// This JS code also contains some .css styling instructions.
|
|
// Those defaults are used in code where there is a "CSS_STYLE" comment.
|
|
var DEFAULT_ENDPOINT_STYLE = "Blank";
|
|
var DEFAULT_CONNECTION_HOVER_STYLE =
|
|
{
|
|
lineWidth: 5
|
|
};
|
|
var DEFAULT_CONNECTION_OVERLAY_STYLE =
|
|
[ "Arrow", {
|
|
location: 1,
|
|
id: "arrow",
|
|
length: 12,
|
|
width: 10,
|
|
foldback: 1
|
|
} ];
|
|
var DEFAULT_CONNECTION_LABEL_STYLE =
|
|
{
|
|
label: "x",
|
|
id: "label",
|
|
cssClass: "connLabel"
|
|
};
|
|
var DEFAULT_ANCHOR_STYLE = "Continuous";
|
|
var DEFAULT_CONNECTOR_STYLE =
|
|
[ "Straight", {
|
|
gap: 2
|
|
} ];
|
|
var DEFAULT_CONNECTOR_STYLE_2 =
|
|
{
|
|
strokeStyle: '<%= Constants::COLOR_WHITE %>',
|
|
lineWidth: 1.5,
|
|
outlineColor: "transparent",
|
|
outlineWidth: 0
|
|
};
|
|
|
|
// Zoom-level specific variables
|
|
//var GRID_DIST_EDIT_X = 300;
|
|
//var GRID_DIST_EDIT_Y = 140; //135
|
|
var GRID_DIST_EDIT_X = 10;
|
|
var GRID_DIST_EDIT_Y = 10;
|
|
var GRID_DIST_EDIT_STEP_X = 10;
|
|
var GRID_DIST_EDIT_STEP_Y = 10;
|
|
var EDIT_ENDPOINT_STYLE =
|
|
[ "Dot", {
|
|
radius: 4,
|
|
cssClass: "ep-normal",
|
|
hoverClass: "ep-hover"
|
|
} ];
|
|
var EDIT_CONNECTOR_STYLE_2 =
|
|
{
|
|
strokeStyle: '<%= Constants::COLOR_WHITE %>',
|
|
lineWidth: 3,
|
|
outlineColor: "transparent",
|
|
outlineWidth: 0
|
|
};
|
|
/*
|
|
var GRID_DIST_FULL_X = 340;
|
|
var GRID_DIST_FULL_Y = 201;
|
|
var GRID_DIST_MEDIUM_X = 250;
|
|
var GRID_DIST_MEDIUM_Y = 88;
|
|
var GRID_DIST_SMALL_X = 100;
|
|
var GRID_DIST_SMALL_Y = 100;
|
|
*/
|
|
/*
|
|
Migrations: multiply my_modules (x by 32, y by 16)
|
|
UPDATE my_modules SET x=x*32, y=y*16
|
|
themodel.connection.execute("UPDATE my_modules SET x=x*32, y=y*16")
|
|
*/
|
|
var GRID_DIST_FULL_X = 10;
|
|
var GRID_DIST_FULL_Y = 10;
|
|
var GRID_DIST_MEDIUM_X = 7.3;
|
|
var GRID_DIST_MEDIUM_Y = 4;
|
|
var GRID_DIST_SMALL_X = 2.4;
|
|
var GRID_DIST_SMALL_Y = 5;
|
|
var SUBMIT_FORM_NAME_SEPARATOR = "|";
|
|
|
|
//************************************
|
|
// GLOBAL VARIABLES
|
|
//************************************
|
|
|
|
// Current GUI mode
|
|
var currentMode = "full_zoom";
|
|
|
|
// JSNetworkX graph structure, used for graph analysis
|
|
var graph;
|
|
|
|
// Instance of jsPlumb, a library for canvas manipulation
|
|
var instance;
|
|
|
|
// ID "generator" for new modules
|
|
var newModuleIndex = 0;
|
|
|
|
// Global variables for module dragging
|
|
var leftInitial = 0, topInitial = 0, collided = false;
|
|
|
|
// Global variables for canvas dragging
|
|
var x_start = 0, y_start = 0;
|
|
var drag_type = DRAG_INVALID;
|
|
var draggable = null;
|
|
|
|
// Draggable position (initial values specified here)
|
|
var draggableLeft = 0.5;
|
|
var draggableTop = 0.5;
|
|
|
|
var ignoreUnsavedWorkAlert;
|
|
|
|
// Global variable for hammer js
|
|
var hammertime;
|
|
|
|
/*
|
|
* As a guideline, all module elements should contain
|
|
* the following attributes:
|
|
*
|
|
* id - ID of the module.
|
|
* data-module-name - Name of the module.
|
|
* data-module-id - ID of the module.
|
|
* data-module-group - ID of the group the module belongs to (if it exists).
|
|
* data-module-x - X position of the module (integer).
|
|
* data-module-y - Y position of the module (integer).
|
|
* data-module-conns - List of module IDs this module is connected
|
|
* to (outbound connections).
|
|
*/
|
|
|
|
//************************************
|
|
// DEFAULT INITIALIZATION CODE
|
|
//************************************
|
|
|
|
/**
|
|
* Initializes page
|
|
*/
|
|
function init() {
|
|
bindModeChange();
|
|
bindAjax();
|
|
bindWindowResizeEvent();
|
|
initializeGraph(".diagram .module-large");
|
|
initializeFullZoom();
|
|
}
|
|
|
|
jsPlumb.ready(function () {
|
|
init();
|
|
});
|
|
|
|
//************************************
|
|
// INDIVIDUAL ACTION INIT & DESTROY
|
|
//************************************
|
|
|
|
function initializeEdit() {
|
|
newModuleIndex = 0;
|
|
ignoreUnsavedWorkAlert = false;
|
|
|
|
// Read permissions from the data attributes of the form
|
|
var canEditModules = _.isEqual($("#update-canvas").data("can-edit-modules"), "yes");
|
|
var canCreateModules = _.isEqual($("#update-canvas").data("can-create-modules"), "yes");
|
|
var canCloneModules = _.isEqual($("#update-canvas").data("can-clone-modules"), "yes");
|
|
var canMoveModules = _.isEqual($("#update-canvas").data("can-move-modules"), "yes");
|
|
var canDeleteModules = _.isEqual($("#update-canvas").data("can-delete-modules"), "yes");
|
|
var canDragModules = _.isEqual($("#update-canvas").data("can-reposition-modules"), "yes");
|
|
var canEditConnections = _.isEqual($("#update-canvas").data("can-edit-connections"), "yes");
|
|
|
|
$("#canvas-container").addClass("canvas-container-edit-mode");
|
|
|
|
// Hide sidebar & also its toggle button
|
|
$("#wrapper").addClass("hidden2");
|
|
$(".navbar-secondary").addClass("navbar-without-sidebar");
|
|
$("#sidebar-arrow").addClass("invisible");
|
|
|
|
// Also, hide zoom levels button group
|
|
$("#diagram-buttons").hide();
|
|
|
|
// Resize container
|
|
resizeContainer();
|
|
|
|
positionModules(".diagram .module", GRID_DIST_EDIT_X, GRID_DIST_EDIT_Y);
|
|
initJsPlumb(
|
|
"#diagram-container",
|
|
"#diagram",
|
|
"div.module",
|
|
{
|
|
scrollEnabled: true,
|
|
gridDistX: GRID_DIST_EDIT_STEP_X,
|
|
gridDistY: GRID_DIST_EDIT_STEP_Y,
|
|
endpointStyle: EDIT_ENDPOINT_STYLE,
|
|
connectorStyle2: EDIT_CONNECTOR_STYLE_2,
|
|
zoomEnabled: true,
|
|
modulesDraggable: canDragModules,
|
|
connectionsEditable: canEditConnections
|
|
}
|
|
);
|
|
bindEditModeDropdownHandlers();
|
|
if (canCreateModules) {
|
|
bindNewModuleAction(GRID_DIST_EDIT_X, GRID_DIST_EDIT_Y);
|
|
}
|
|
bindEditFormSubmission(GRID_DIST_EDIT_X, GRID_DIST_EDIT_Y);
|
|
|
|
if (canEditModules) {
|
|
initEditModules();
|
|
$(".edit-module").on("click touchstart", editModuleHandler);
|
|
}
|
|
|
|
if (canCloneModules) {
|
|
bindCloneModuleAction(
|
|
$(".module-options a.clone-module"),
|
|
".diagram .module",
|
|
GRID_DIST_EDIT_X,
|
|
GRID_DIST_EDIT_Y);
|
|
bindCloneModuleGroupAction(
|
|
$(".module-options a.clone-module-group"),
|
|
".diagram .module",
|
|
GRID_DIST_EDIT_X,
|
|
GRID_DIST_EDIT_Y);
|
|
}
|
|
if (canMoveModules) {
|
|
initMoveModules();
|
|
$(".move-module").on("click touchstart", moveModuleHandler);
|
|
|
|
initMoveModuleGroups();
|
|
$(".move-module-group").on("click touchstart", moveModuleGroupHandler);
|
|
}
|
|
if (canDeleteModules) {
|
|
bindDeleteModuleAction();
|
|
bindDeleteModuleGroupAction();
|
|
}
|
|
|
|
bindEditModeCloseWindow();
|
|
bindTouchDropdowns($(".dropdown-toggle"));
|
|
|
|
// Restore draggable position
|
|
restoreDraggablePosition($("#diagram"), $("#canvas-container"));
|
|
|
|
preventCanvasReloadOnSave();
|
|
$("#canvas-container").submit(function (){
|
|
animateSpinner(
|
|
this,
|
|
true,
|
|
{ color: 'white', shadow: true }
|
|
);
|
|
});
|
|
}
|
|
|
|
function destroyEdit() {
|
|
// Read permissions from the data attributes of the form
|
|
var canCreateModules = _.isEqual($("#update-canvas").data("can-create-modules"), "yes");
|
|
var canCloneModules = _.isEqual($("#update-canvas").data("can-clone-modules"), "yes");
|
|
var canMoveModules = _.isEqual($("#update-canvas").data("can-move-modules"), "yes");
|
|
var canDeleteModules = _.isEqual($("#update-canvas").data("can-delete-modules"), "yes");
|
|
|
|
instance.cleanupListeners();
|
|
$(".dropdown").off("show.bs.dropdown hide.bs.dropdown");
|
|
$("#diagram-container").off("mousewheel mousedown mouseup mousemove");
|
|
hammertime.off('pinch');
|
|
$("form#update-canvas").off("submit");
|
|
if (canDeleteModules) {
|
|
$(".delete-container a").off("click");
|
|
$("#modal-delete-module").off("show.bs.modal hide.bs.modal");
|
|
$("#modal-delete-module").find("button[data-action='confirm']").off("click");
|
|
|
|
$(".buttons-container a.delete-module").off("click touchstart");
|
|
$(".buttons-container a.delete-module-group").off("click touchstart");
|
|
$("#modal-delete-module-group").off("show.bs.modal hide.bs.modal");
|
|
$("#modal-delete-module-group").find("button[data-action='confirm']").off("click");
|
|
}
|
|
if (canCreateModules) {
|
|
$("#modal-new-module").off("show.bs.modal shown.bs.modal hide.bs.modal");
|
|
$("#modal-new-module").find("button[data-action='confirm']").off("click");
|
|
$("#canvas-new-module").draggable("destroy");
|
|
$("#canvas-new-module").off("click");
|
|
}
|
|
if (canCloneModules) {
|
|
$(".buttons-container a.clone-module").off("click touchstart");
|
|
$(".buttons-container a.clone-module-group").off("click touchstart");
|
|
}
|
|
if (canMoveModules) {
|
|
$(".move-module").off("click touchstart");
|
|
$(".move-module-group").off("click touchstart");
|
|
}
|
|
|
|
$("#update-canvas .cancel-edit-canvas").off("click");
|
|
$(window).off("beforeunload");
|
|
$(document).off("page:before-change");
|
|
$(".dropdown-toggle").off("touchstart");
|
|
|
|
// Remember the draggable position
|
|
rememberDraggablePosition($("#diagram"), $("#canvas-container"));
|
|
}
|
|
|
|
function initializeFullZoom() {
|
|
// Resize container
|
|
resizeContainer();
|
|
|
|
positionModules(".diagram .module-large", GRID_DIST_FULL_X, GRID_DIST_FULL_Y);
|
|
initJsPlumb(
|
|
"#diagram-container",
|
|
"#diagram",
|
|
"div.module-large",
|
|
{
|
|
scrollEnabled: true,
|
|
gridDistX: GRID_DIST_FULL_X,
|
|
gridDistY: GRID_DIST_FULL_Y
|
|
});
|
|
bindEditDueDateAjax();
|
|
bindEditTagsAjax($("div.module-large"));
|
|
bindFullZoomAjaxTabs();
|
|
initModulesHover($("div.module-large"), $("#slide-panel"));
|
|
initSidebarClicks($("div.module-large"), $("#slide-panel"), $("#diagram"), $("#canvas-container"), 20);
|
|
|
|
// Restore draggable position
|
|
restoreDraggablePosition($("#diagram"), $("#canvas-container"));
|
|
|
|
// Initialize comments
|
|
Comments.initCommentOptions("ul.content-comments", false);
|
|
Comments.initEditComments(".panel.module-large .tab-content");
|
|
Comments.initDeleteComments(".panel.module-large .tab-content");
|
|
}
|
|
|
|
function destroyFullZoom() {
|
|
instance.cleanupListeners();
|
|
$("#diagram-container").off("mousedown mouseup mousemove");
|
|
$(".module-large .buttons-container [role=tab]").off("ajax:before ajax:success ajax:error");
|
|
$("div.module-large").off("mouseenter mouseleave");
|
|
$("div.module-large a.due-date-link").off("ajax:success ajax:error");
|
|
$("#manage-module-due-date-modal [data-action='submit']").off("click");
|
|
$("div.module-large a.edit-tags-link").off("ajax:before ajax:success");
|
|
$("li[data-module-group]").off("mouseenter mouseleave");
|
|
$("li[data-module-group] > span > a.canvas-center-on").off("click");
|
|
$("li[data-module-id]").off("mouseenter mouseleave");
|
|
$("li[data-module-id] > span > a.canvas-center-on").off("click");
|
|
|
|
// Clean up comments listeners
|
|
$(document).off("scroll");
|
|
$(".panel.module-large .tab-content")
|
|
.off("click", "[data-action=edit-comment]");
|
|
$(".panel.module-large .tab-content")
|
|
.off("click", "[data-action=delete-comment]");
|
|
|
|
// Remember the draggable position
|
|
rememberDraggablePosition($("#diagram"), $("#canvas-container"));
|
|
}
|
|
|
|
function initializeMediumZoom() {
|
|
// Resize container
|
|
resizeContainer();
|
|
|
|
positionModules(".diagram .module-medium", GRID_DIST_MEDIUM_X, GRID_DIST_MEDIUM_Y);
|
|
initJsPlumb("#diagram-container", "#diagram", "div.module-medium", { scrollEnabled: true, gridDistX: GRID_DIST_MEDIUM_X, gridDistY: GRID_DIST_MEDIUM_Y });
|
|
bindEditTagsAjax($("div.module-medium"));
|
|
initModulesHover($("div.module-medium"), $("#slide-panel"));
|
|
initSidebarClicks($("div.module-medium"), $("#slide-panel"), $("#diagram"), $("#canvas-container"), 20);
|
|
|
|
// Restore draggable position
|
|
restoreDraggablePosition($("#diagram"), $("#canvas-container"));
|
|
}
|
|
|
|
function destroyMediumZoom() {
|
|
instance.cleanupListeners();
|
|
$("#diagram-container").off("mousedown mouseup mousemove");
|
|
$("div.module-medium").off("mouseenter mouseleave");
|
|
$("div.module-medium a.edit-tags-link").off("ajax:before ajax:success");
|
|
$("li[data-module-group]").off("mouseenter mouseleave");
|
|
$("li[data-module-group] > span > a.canvas-center-on").off("click");
|
|
$("li[data-module-id]").off("mouseenter mouseleave");
|
|
$("li[data-module-id] > span > a.canvas-center-on").off("click");
|
|
|
|
// Remember the draggable position
|
|
rememberDraggablePosition($("#diagram"), $("#canvas-container"));
|
|
}
|
|
|
|
function initializeSmallZoom() {
|
|
// Resize container
|
|
resizeContainer();
|
|
|
|
positionModules(".diagram .module-small", GRID_DIST_SMALL_X, GRID_DIST_SMALL_Y);
|
|
initJsPlumb("#diagram-container", "#diagram", "div.module-small", { scrollEnabled: true, gridDistX: GRID_DIST_SMALL_X, gridDistY: GRID_DIST_SMALL_Y });
|
|
initModulesHover($("div.module-small"), $("#slide-panel"));
|
|
initSidebarClicks($("div.module-small"), $("#slide-panel"), $("#diagram"), $("#canvas-container"), 20);
|
|
|
|
// Restore draggable position
|
|
restoreDraggablePosition($("#diagram"), $("#canvas-container"));
|
|
}
|
|
|
|
function destroySmallZoom() {
|
|
instance.cleanupListeners();
|
|
$("#diagram-container").off("mousedown mouseup mousemove");
|
|
$("div.module-small").off("mouseenter mouseleave");
|
|
$("li[data-module-group]").off("mouseenter mouseleave");
|
|
$("li[data-module-group] > span > a.canvas-center-on").off("click");
|
|
$("li[data-module-id]").off("mouseenter mouseleave");
|
|
$("li[data-module-id] > span > a.canvas-center-on").off("click");
|
|
|
|
// Remember the draggable position
|
|
rememberDraggablePosition($("#diagram"), $("#canvas-container"));
|
|
}
|
|
|
|
//************************************
|
|
// FUNCTIONS
|
|
//************************************
|
|
|
|
/**
|
|
* Enable/disable canvas events (related to dragging, zooming, ...).
|
|
* @param activate - True to activate events; false
|
|
* to deactivate them.
|
|
*/
|
|
|
|
var canHammer = function(recognizer, event) {
|
|
event = event || window.event;
|
|
if (event==null) {
|
|
return false;
|
|
}
|
|
var result = (event.target instanceof HTMLInputElement && event.target.type == 'text');
|
|
return !result;
|
|
}
|
|
|
|
|
|
function toggleCanvasEvents(activate) {
|
|
var cmd = "pause";
|
|
if (activate) {
|
|
cmd = "active";
|
|
}
|
|
$("#diagram-container").eventPause(cmd,
|
|
"mousedown mouseup mouseout mousewheel touchstart touchend touchcancel touchmove");
|
|
hammertime.get('pinch').set({ enable: canHammer });
|
|
}
|
|
|
|
/**
|
|
* Gets or sets the left CSS position of the element.
|
|
* @param el - The element.
|
|
* @param newVal - The new left CSS value, if setting value.
|
|
* @return The new float value of the element's left CSS position.
|
|
*/
|
|
function elLeft(el, newVal) {
|
|
if (_.isUndefined(newVal)) {
|
|
return parseFloat($(el).css("left").replace("px", ""));
|
|
} else {
|
|
$(el).css("left", newVal + "px");
|
|
return newVal;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets or sets the top CSS position of the element.
|
|
* @param el - The element.
|
|
* @param newVal - The new top CSS value, if setting value.
|
|
* @return The new float value of the element's top CSS position.
|
|
*/
|
|
function elTop(el, newVal) {
|
|
if (_.isUndefined(newVal)) {
|
|
return parseFloat($(el).css("top").replace("px", ""));
|
|
} else {
|
|
$(el).css("top", newVal + "px");
|
|
return newVal;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Animate the reposition of the specified element.
|
|
* @param el - The element to be repositioned.
|
|
* @param left - The new left CSS property.
|
|
* @param top - The new top CSS property.
|
|
*/
|
|
function animateReposition(el, left, top) {
|
|
var leftMove, topMove, leftDir, topDir;
|
|
if (_.isUndefined($(el).css("left"))) {
|
|
leftMove = left;
|
|
} else {
|
|
leftMove = (-parseInt($(el).css("left").replace("px", ""), 10) + left);
|
|
}
|
|
if (_.isUndefined($(el).css("top"))) {
|
|
topMove = top;
|
|
} else {
|
|
topMove = (-parseInt($(el).css("top").replace("px", ""), 10) + top);
|
|
}
|
|
leftDir = leftMove >=0 ? "+=" : "-=";
|
|
topDir = topMove >=0 ? "+=" : "-=";
|
|
el.animate({
|
|
left: leftDir + Math.abs(leftMove) + "px",
|
|
top: topDir + Math.abs(topMove) + "px"
|
|
}, 300);
|
|
}
|
|
|
|
/**
|
|
* Bind the change of the canvas mode.
|
|
*/
|
|
function bindModeChange() {
|
|
var buttons = $('#diagram-buttons').find("a[type='button']");
|
|
|
|
buttons.on('click', function() {
|
|
var action = $(this).data("action");
|
|
|
|
// Ignore clicks on the currently active button
|
|
if (_.isEqual(action, currentMode)) {
|
|
return false;
|
|
}
|
|
|
|
// Else, call destroy action function
|
|
switch (action) {
|
|
case "edit":
|
|
destroyEdit();
|
|
break;
|
|
case "full_zoom":
|
|
destroyFullZoom();
|
|
break;
|
|
case "medium_zoom":
|
|
destroyMediumZoom();
|
|
break;
|
|
case "small_zoom":
|
|
destroySmallZoom();
|
|
break;
|
|
}
|
|
});
|
|
}
|
|
|
|
function bindTouchDropdowns(selector) {
|
|
selector.on("touchstart", function(event) {
|
|
event.stopPropagation();
|
|
});
|
|
}
|
|
|
|
function bindEditModeCloseWindow() {
|
|
var alertText = $("#update-canvas").attr("data-unsaved-work-text");
|
|
|
|
$("#update-canvas .cancel-edit-canvas").click(function(ev) {
|
|
ignoreUnsavedWorkAlert = true;
|
|
});
|
|
$(window).on("beforeunload", function(ev) {
|
|
if (ignoreUnsavedWorkAlert) {
|
|
// Remove unload listeners
|
|
$(window).off("beforeunload");
|
|
$(document).off("page:before-change");
|
|
|
|
ev.returnValue = undefined;
|
|
return undefined;
|
|
} else {
|
|
return alertText;
|
|
}
|
|
});
|
|
$(document).on("page:before-change", function(ev) {
|
|
var exit;
|
|
if (ignoreUnsavedWorkAlert) {
|
|
exit = true;
|
|
} else {
|
|
exit = confirm(alertText);
|
|
}
|
|
|
|
if (exit) {
|
|
// Remove unload listeners
|
|
$(window).off("beforeunload");
|
|
$(document).off("page:before-change");
|
|
}
|
|
|
|
return exit;
|
|
});
|
|
}
|
|
|
|
function bindEditModeDropdownHandlers(node) {
|
|
// When "module clone/delete" dropdowns are opened,
|
|
// module needs to increase z-index in order for the dropdown
|
|
// menu to be above connections etc.
|
|
$(".dropdown", node).on("show.bs.dropdown", function(event) {
|
|
$(this).parents(".module").css("z-index", "30");
|
|
});
|
|
$(".dropdown", node).on("hide.bs.dropdown", function(event) {
|
|
$(this).parents(".module").css("z-index", "20");
|
|
});
|
|
}
|
|
|
|
function resizeContainer() {
|
|
// Resize diagram container
|
|
var cont = $("#diagram-container");
|
|
|
|
if (cont.length > 0) {
|
|
cont.css(
|
|
"height",
|
|
($(window).height() - cont.offset().top - 15) + "px"
|
|
);
|
|
}
|
|
}
|
|
|
|
function bindWindowResizeEvent() {
|
|
$(window).resize(function() {
|
|
resizeContainer();
|
|
});
|
|
}
|
|
|
|
function bindFullZoomAjaxTabs() {
|
|
var manageUsersModal = null;
|
|
var manageUsersModalBody = null;
|
|
|
|
// Initialize users editing modal remote loading.
|
|
function initUsersEditLink($el) {
|
|
$el.find(".manage-users-link")
|
|
.on("ajax:before", function () {
|
|
var moduleId = $(this).closest(".panel-default").attr("data-module-id");
|
|
manageUsersModal.attr("data-module-id", moduleId);
|
|
manageUsersModal.modal('show');
|
|
})
|
|
.on("ajax:success", function (e, data) {
|
|
$("#manage-module-users-modal-module").text(data.my_module.name);
|
|
initUsersModalBody(data);
|
|
});
|
|
}
|
|
|
|
// Initialize reloading manage user modal content after posting new
|
|
// user.
|
|
function initAddUserForm() {
|
|
manageUsersModalBody.find(".add-user-form")
|
|
.on("ajax:success", function (e, data) {
|
|
initUsersModalBody(data);
|
|
});
|
|
}
|
|
|
|
// Initialize remove user from my_module links.
|
|
function initRemoveUserLinks() {
|
|
manageUsersModalBody.find(".remove-user-link")
|
|
.on("ajax:success", function (e, data) {
|
|
initUsersModalBody(data);
|
|
});
|
|
}
|
|
|
|
// Initialize ajax listeners and elements style on modal body. This
|
|
// function must be called when modal body is changed.
|
|
function initUsersModalBody(data) {
|
|
manageUsersModalBody.html(data.html);
|
|
manageUsersModalBody.find(".selectpicker").selectpicker();
|
|
initAddUserForm();
|
|
initRemoveUserLinks();
|
|
}
|
|
|
|
manageUsersModal = $("#manage-module-users-modal");
|
|
manageUsersModalBody = manageUsersModal.find(".modal-body");
|
|
|
|
// Reload users tab HTML element when modal is closed
|
|
manageUsersModal.on("hide.bs.modal", function () {
|
|
var moduleEl = $("div[data-module-id='" +
|
|
$(this).attr("data-module-id") + "']");
|
|
// Load HTML to refresh users list
|
|
$.ajax({
|
|
url: moduleEl.attr("data-module-users-tab-url"),
|
|
type: "GET",
|
|
dataType: "json",
|
|
success: function (data) {
|
|
moduleEl.find("#" + moduleEl.attr("data-module-id") + "_users").html(data.html);
|
|
CounterBadge.updateCounterBadge(data.counter,
|
|
data.my_module_id, 'users');
|
|
initUsersEditLink(moduleEl);
|
|
},
|
|
error: function (data) {
|
|
// TODO
|
|
}
|
|
});
|
|
});
|
|
|
|
// Remove users modal content when modal window is closed.
|
|
manageUsersModal.on("hidden.bs.modal", function () {
|
|
manageUsersModalBody.html("");
|
|
});
|
|
|
|
// initialize my_module tab remote loading
|
|
var elements = $(".module-large .buttons-container [role=tab]");
|
|
elements.on("ajax:before", function (e) {
|
|
var $this = $(this);
|
|
var parentNode = $this.parents("li");
|
|
var targetId = $this.attr("aria-controls");
|
|
|
|
if (parentNode.hasClass("active")) {
|
|
parentNode.removeClass("active");
|
|
$("#" + targetId).removeClass("active");
|
|
$this.parents(".module-large").addClass("expanded");
|
|
return false;
|
|
}
|
|
})
|
|
.on("ajax:success", function (e, data, status, xhr) {
|
|
|
|
// Hide all potentially shown tabs
|
|
elements.parents("li").removeClass("active");
|
|
$(".tab-content").children().removeClass("active");
|
|
$(".module-large").removeClass("expanded");
|
|
|
|
var $this = $(this);
|
|
var targetId = $this.attr("aria-controls");
|
|
var target = $("#" + targetId);
|
|
var targetContents = target.attr("data-contents");
|
|
var parentNode = $this.parents("ul").parent();
|
|
|
|
target.html(data.html);
|
|
if (targetContents === "info") {
|
|
initEditDescription(parentNode);
|
|
} else if (targetContents === "users") {
|
|
initUsersEditLink(parentNode);
|
|
} else if (targetContents === "comments") {
|
|
Comments.form(parentNode);
|
|
Comments.moreComments(parentNode);
|
|
}
|
|
|
|
$this.parents("ul").parent().find(".active").removeClass("active");
|
|
$this.parents("li").addClass("active");
|
|
target.addClass("active");
|
|
$this.parents(".module-large").addClass("expanded");
|
|
|
|
// Call scrollBotton after the comments are displayed
|
|
// so that the scrollHight can be calculated
|
|
if ( targetContents === 'comments' ) {
|
|
Comments.scrollBottom(parentNode);
|
|
}
|
|
})
|
|
.on("ajax:error", function (e, xhr, status, error) {
|
|
// TODO
|
|
});
|
|
}
|
|
|
|
function bindEditDueDateAjax() {
|
|
var editDueDateModal = null;
|
|
var editDueDateModalBody = null;
|
|
var editDueDateModalTitle = null;
|
|
var editDueDateModalSubmitBtn = null;
|
|
|
|
editDueDateModal = $("#manage-module-due-date-modal");
|
|
editDueDateModalBody = editDueDateModal.find(".modal-body");
|
|
editDueDateModalTitle = editDueDateModal.find("#manage-module-due-date-modal-label");
|
|
editDueDateModalSubmitBtn = editDueDateModal.find("[data-action='submit']");
|
|
|
|
$("div.module-large .panel-body .due-date-link")
|
|
.on("ajax:success", function(ev, data, status) {
|
|
var dueDateLink = $(this);
|
|
if (!dueDateLink.hasClass("due-date-refresh")) {
|
|
dueDateLink = dueDateLink.parent().next().find(".due-date-refresh");
|
|
}
|
|
var moduleEl = dueDateLink.closest("div.module-large");
|
|
|
|
// Load contents
|
|
editDueDateModalBody.html(data.html);
|
|
editDueDateModalTitle.text(data.title);
|
|
|
|
// Add listener to form inside modal
|
|
editDueDateModalBody.find("form")
|
|
.on("ajax:success", function(ev2, data2, status2) {
|
|
// Update module's due date
|
|
dueDateLink.html(data2.due_date_label);
|
|
|
|
// Update module's classes if needed
|
|
moduleEl
|
|
.removeClass("alert-red")
|
|
.removeClass("alert-yellow");
|
|
_.each(data2.alerts, function(alert) {
|
|
moduleEl.addClass(alert);
|
|
});
|
|
|
|
// Close modal
|
|
editDueDateModal.modal("hide");
|
|
})
|
|
.on("ajax:error", function(ev2, data2, status2) {
|
|
// Display errors if needed
|
|
$(this).renderFormErrors("my_module", data2.responseJSON);
|
|
});
|
|
|
|
// Disable canvas dragging events
|
|
toggleCanvasEvents(false);
|
|
|
|
// Open modal
|
|
editDueDateModal.modal("show");
|
|
})
|
|
.on("ajax:error", function(ev, data, status) {
|
|
// TODO
|
|
});
|
|
|
|
editDueDateModalSubmitBtn.on("click", function() {
|
|
// Submit the form inside the modal
|
|
editDueDateModalBody.find("form").submit();
|
|
});
|
|
|
|
editDueDateModal.on("hidden.bs.modal", function() {
|
|
editDueDateModalBody.find("form").off("ajax:success ajax:error");
|
|
editDueDateModalBody.html("");
|
|
|
|
// Re-activate canvas dragging events
|
|
toggleCanvasEvents(true);
|
|
});
|
|
}
|
|
|
|
function bindEditTagsAjax(elements) {
|
|
var manageTagsModal = null;
|
|
var manageTagsModalBody = null;
|
|
|
|
// Initialize reloading of manage tags modal content after posting new
|
|
// tag.
|
|
function initAddTagForm() {
|
|
manageTagsModalBody.find(".add-tag-form")
|
|
.on("ajax:success", function (e, data) {
|
|
initTagsModalBody(data);
|
|
});
|
|
}
|
|
|
|
// Initialize edit tag & remove tag functionality from my_module links.
|
|
function initTagRowLinks() {
|
|
manageTagsModalBody.find(".edit-tag-link")
|
|
.on("click", function (e) {
|
|
var $this = $(this);
|
|
var li = $this.parents("li.list-group-item");
|
|
var editDiv = $(li.find("div.tag-edit"));
|
|
|
|
// Revert all rows to their original states
|
|
manageTagsModalBody.find("li.list-group-item").each(function(){
|
|
var li = $(this);
|
|
li.css("background-color", li.data("color"));
|
|
li.find(".edit-tag-form").clearFormErrors();
|
|
li.find("input[type=text]").val(li.data("name"));
|
|
});
|
|
|
|
// Hide all other edit divs, show all show divs
|
|
manageTagsModalBody.find("div.tag-edit").hide();
|
|
manageTagsModalBody.find("div.tag-show").show();
|
|
|
|
editDiv.find("input[type=text]").val(li.data("name"));
|
|
editDiv.find('.edit-tag-color').colorselector('setColor', li.data("color"));
|
|
|
|
li.find("div.tag-show").hide();
|
|
editDiv.show();
|
|
});
|
|
manageTagsModalBody.find("div.tag-edit .dropdown-colorselector > .dropdown-menu li a")
|
|
.on("click", function (e) {
|
|
// Change background of the <li>
|
|
var $this = $(this);
|
|
var li = $this.parents("li.list-group-item");
|
|
li.css("background-color", $this.data("value"));
|
|
});
|
|
manageTagsModalBody.find(".remove-tag-link")
|
|
.on("ajax:success", function (e, data) {
|
|
initTagsModalBody(data);
|
|
});
|
|
manageTagsModalBody.find(".delete-tag-form")
|
|
.on("ajax:success", function (e, data) {
|
|
initTagsModalBody(data);
|
|
});
|
|
manageTagsModalBody.find(".edit-tag-form")
|
|
.on("ajax:success", function (e, data) {
|
|
initTagsModalBody(data);
|
|
})
|
|
.on("ajax:error", function (e, data) {
|
|
$(this).renderFormErrors("tag", data.responseJSON);
|
|
});
|
|
manageTagsModalBody.find(".cancel-tag-link")
|
|
.on("click", function (e, data) {
|
|
var $this = $(this);
|
|
var li = $this.parents("li.list-group-item");
|
|
|
|
li.css("background-color", li.data("color"));
|
|
li.find(".edit-tag-form").clearFormErrors();
|
|
|
|
li.find("div.tag-edit").hide();
|
|
li.find("div.tag-show").show();
|
|
});
|
|
}
|
|
|
|
// Initialize ajax listeners and elements style on modal body. This
|
|
// function must be called when modal body is changed.
|
|
function initTagsModalBody(data) {
|
|
manageTagsModalBody.html(data.html);
|
|
manageTagsModalBody.find(".selectpicker").selectpicker();
|
|
initAddTagForm();
|
|
initTagRowLinks();
|
|
}
|
|
|
|
manageTagsModal = $("#manage-module-tags-modal");
|
|
manageTagsModalBody = manageTagsModal.find(".modal-body");
|
|
|
|
// Reload tags HTML element when modal is closed
|
|
manageTagsModal.on("hide.bs.modal", function(){
|
|
var tasks = $("div.panel");
|
|
|
|
tasks.each(function(){
|
|
var task = $(this);
|
|
// Load HTML
|
|
$.ajax({
|
|
url: task.attr("data-module-tags-url"),
|
|
type: "GET",
|
|
dataType: "json",
|
|
success: function(data){
|
|
task.find(".edit-tags-link")
|
|
.html(data.html_canvas);
|
|
},
|
|
error: function(data){
|
|
// TODO
|
|
}
|
|
})
|
|
});
|
|
});
|
|
|
|
// Remove modal content when modal window is closed.
|
|
manageTagsModal.on("hidden.bs.modal", function () {
|
|
manageTagsModalBody.html("");
|
|
});
|
|
|
|
// initialize my_module tab remote loading
|
|
$(elements).find("a.edit-tags-link")
|
|
.on("ajax:before", function () {
|
|
var moduleId = $(this).closest(".panel-default").attr("data-module-id");
|
|
manageTagsModal.attr("data-module-id", moduleId);
|
|
manageTagsModal.modal('show');
|
|
})
|
|
.on("ajax:success", function (e, data) {
|
|
$("#manage-module-tags-modal-module").text(data.my_module.name);
|
|
initTagsModalBody(data);
|
|
})
|
|
.on('click', function(){
|
|
$(this).addClass('updated-module-tags');
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Bind change of GUI buttons to Ajax success callback.
|
|
*/
|
|
function bindAjax() {
|
|
$('#diagram-buttons .ajax').on('ajax:success', function(evt, data) {
|
|
// Set toggled button state
|
|
$("#diagram-buttons a").removeClass("active");
|
|
$("#diagram-buttons a").removeAttr("aria-pressed");
|
|
$("#diagram-buttons a").removeData("toggle");
|
|
$(evt.target).addClass("active");
|
|
$(evt.target).attr("aria-pressed", true);
|
|
$(evt.target).data("toggle", "button");
|
|
|
|
// Fill contents of container with AJAX content
|
|
var target = $('#canvas-container');
|
|
$(target).html(data);
|
|
|
|
// Re-run canvas GUI initialization code
|
|
var action = $(evt.target).data("action");
|
|
switch (action) {
|
|
case "edit":
|
|
initializeEdit();
|
|
break;
|
|
case "full_zoom":
|
|
initializeFullZoom();
|
|
break;
|
|
case "medium_zoom":
|
|
initializeMediumZoom();
|
|
break;
|
|
case "small_zoom":
|
|
initializeSmallZoom();
|
|
break;
|
|
}
|
|
|
|
currentMode = action;
|
|
});
|
|
$('#diagram-buttons .ajax').on('ajax:error', function(evt, data) {
|
|
// Redirect to provided URL
|
|
var json = $.parseJSON(data.responseText);
|
|
$(location).attr('href', json.redirect_url);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Add a new node to the graph.
|
|
* @param moduleId - The ID of the module to add.
|
|
* @param module - The module jQuery element.
|
|
*/
|
|
function addNode(moduleId, module) {
|
|
var connsAttr = module.attr("data-module-conns");
|
|
var conns = _.isUndefined(connsAttr) ? [] : connsAttr.split(", ");
|
|
graph.addNode(
|
|
moduleId,
|
|
{
|
|
name: module.data["module-name"],
|
|
x: module.data["module-x"],
|
|
y: module.data["module-y"],
|
|
conns: conns
|
|
}
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Initialize the global graph variable from modules.
|
|
* @param modulesSel - The jQuery selector text of module elements.
|
|
*/
|
|
function initializeGraph(modulesSel) {
|
|
var modules = $(modulesSel);
|
|
|
|
graph = new jsnx.DiGraph();
|
|
|
|
var module, moduleId;
|
|
_.each(modules, function(m) {
|
|
module = $(m);
|
|
moduleId = module.attr("data-module-id");
|
|
if (!graph.hasNode(moduleId)) {
|
|
addNode(moduleId, module);
|
|
}
|
|
var outs = module.attr("data-module-conns").split(", ");
|
|
_.each(outs, function(targetId) {
|
|
if (targetId === "") {
|
|
return;
|
|
}
|
|
if (!graph.hasNode(targetId)) {
|
|
addNode(targetId, $(".diagram .module[data-module-id=" + targetId + "]"));
|
|
}
|
|
|
|
graph.addEdge(module.attr("data-module-id"), targetId);
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get the connected components of a specified graph and module. Alas, this
|
|
* function doesn't exist in jsnetworkx.
|
|
* @param graph - The graph instance.
|
|
* @param moduleId - We're only interested in the connected component in which
|
|
* the specified module is located.
|
|
* @return A list of node IDs representing a connected component.
|
|
*/
|
|
function connectedComponents(graph, moduleId) {
|
|
function getNeighbors(graph, node, visited) {
|
|
visited.push(node);
|
|
var neighbours = _.union(graph.predecessors(node), graph.successors(node));
|
|
var unvisitedNeighbors = _.filter(neighbours, function(n) {
|
|
return !_.contains(visited, n);
|
|
});
|
|
var result = _.flatten(_.map(unvisitedNeighbors, function(neighbour) {
|
|
nodes = getNeighbors(graph, neighbour, visited);
|
|
_.each(nodes, function(n) {
|
|
if (!_.contains(visited, n)) {
|
|
visited.push(n);
|
|
}
|
|
});
|
|
return nodes;
|
|
}));
|
|
result.push(node);
|
|
return result;
|
|
}
|
|
|
|
return _.uniq(getNeighbors(graph, moduleId, []));
|
|
}
|
|
|
|
/**
|
|
* Create a virtual new module (without links & functionality).
|
|
* @param event - The event, can be null.
|
|
*/
|
|
function createVirtualModule(event) {
|
|
// Generate new module div
|
|
var newModule = document.createElement("div");
|
|
$(newModule)
|
|
.addClass("panel panel-default module new")
|
|
.css("z-index", "900")
|
|
.attr("data-module-name", "")
|
|
.attr("data-module-group-name","")
|
|
.attr("data-module-x", "")
|
|
.attr("data-module-y", "")
|
|
.attr("data-module-conns", "")
|
|
.appendTo(draggable);
|
|
|
|
var panelHeading = document.createElement("div");
|
|
$(panelHeading)
|
|
.addClass("panel-heading")
|
|
.appendTo($(newModule));
|
|
|
|
var panelTitle = document.createElement("div");
|
|
$(panelTitle)
|
|
.addClass("panel-title")
|
|
.html("")
|
|
.appendTo($(panelHeading));
|
|
|
|
if (_.isEqual($("#update-canvas").data("can-edit-connections"), "yes")) {
|
|
var panelBody = document.createElement("div");
|
|
$(panelBody)
|
|
.addClass("panel-body ep")
|
|
.appendTo($(newModule));
|
|
}
|
|
|
|
var overlayContainer = document.createElement("div");
|
|
$(overlayContainer)
|
|
.addClass("overlay")
|
|
.appendTo($(newModule));
|
|
|
|
return $(newModule);
|
|
}
|
|
|
|
/**
|
|
* Update a previously created virtual module with HTML elements.
|
|
* @param module - The jQuery module selector.
|
|
* @param id - The new module id.
|
|
* @param name - The module name.
|
|
* @param gridDistX - The grid distance in X direction.
|
|
* @param gridDistY - The grid distance in Y direction.
|
|
* @return The updated module.
|
|
*/
|
|
function updateModuleHtml(module, id, name, gridDistX, gridDistY) {
|
|
// Update some stuff inside the module
|
|
module
|
|
.attr("id", id)
|
|
.attr("data-module-id", id)
|
|
.attr("data-module-name", name)
|
|
.css("z-index", 20);
|
|
|
|
var panelHeading = module.find(".panel-heading");
|
|
|
|
module.find(".panel-title").html(name);
|
|
|
|
module.find(".ep").html($("#drag-connections-placeholder").text().trim());
|
|
|
|
// Add dropdown
|
|
var dropdown = document.createElement("div");
|
|
$(dropdown)
|
|
.addClass("dropdown pull-right module-options")
|
|
.appendTo($(panelHeading));
|
|
|
|
var dropdownToggle = document.createElement("a");
|
|
$(dropdownToggle)
|
|
.addClass("dropdown-toggle")
|
|
.attr("id", id + "_options")
|
|
.attr("data-toggle", "dropdown")
|
|
.attr("aria-haspopup", "true")
|
|
.attr("aria-expanded", "true")
|
|
.appendTo(dropdown);
|
|
|
|
var toggleIcon = document.createElement("span");
|
|
$(toggleIcon)
|
|
.addClass("fas")
|
|
.addClass("fa-caret-down")
|
|
.attr("aria-hidden", "true")
|
|
.appendTo(dropdownToggle);
|
|
|
|
var dropdownMenu = document.createElement("ul");
|
|
$(dropdownMenu)
|
|
.addClass("dropdown-menu")
|
|
.addClass("custom-dropdown-menu")
|
|
.addClass("no-scale")
|
|
.attr("aria-labelledby", id + "_options")
|
|
.appendTo(dropdown);
|
|
|
|
var dropdownMenuHeader = document.createElement("li");
|
|
$(dropdownMenuHeader)
|
|
.addClass("dropdown-header")
|
|
.html($("#dropdown-header-placeholder").text().trim())
|
|
.appendTo(dropdownMenu);
|
|
|
|
// Add edit links if neccesary
|
|
if (_.isEqual($("#update-canvas").data("can-edit-modules"), "yes")) {
|
|
var editModuleItem = document.createElement("li");
|
|
$(editModuleItem).appendTo(dropdownMenu);
|
|
|
|
var editModuleLink = document.createElement("a");
|
|
$(editModuleLink)
|
|
.attr("href", "")
|
|
.attr("data-module-id", id)
|
|
.addClass("edit-module")
|
|
.html($("#edit-link-placeholder").text().trim())
|
|
.appendTo(editModuleItem);
|
|
|
|
// Add click handler for the edit module
|
|
$(editModuleLink).on("click touchstart", editModuleHandler);
|
|
}
|
|
|
|
// Add clone links if neccesary
|
|
if (_.isEqual($("#update-canvas").data("can-clone-modules"), "yes")) {
|
|
var cloneModuleItem = document.createElement("li");
|
|
$(cloneModuleItem).appendTo(dropdownMenu);
|
|
|
|
var cloneModuleLink = document.createElement("a");
|
|
$(cloneModuleLink)
|
|
.attr("href", "")
|
|
.attr("data-module-id", id)
|
|
.addClass("clone-module")
|
|
.html($("#clone-link-placeholder").text().trim())
|
|
.appendTo(cloneModuleItem);
|
|
|
|
// Add clone click handler for the new module
|
|
bindCloneModuleAction($(cloneModuleLink), ".diagram .module", gridDistX, gridDistY);
|
|
|
|
var cloneModuleGroupItem = document.createElement("li");
|
|
$(cloneModuleGroupItem).appendTo(dropdownMenu);
|
|
$(cloneModuleGroupItem).hide();
|
|
|
|
var cloneModuleGroupLink = document.createElement("a");
|
|
$(cloneModuleGroupLink)
|
|
.attr("href", "")
|
|
.attr("data-module-id", id)
|
|
.addClass("clone-module-group")
|
|
.html($("#clone-group-link-placeholder").text().trim())
|
|
.appendTo(cloneModuleGroupItem);
|
|
|
|
// Add clone click handler for the new module
|
|
bindCloneModuleGroupAction($(cloneModuleGroupLink), ".diagram .module", gridDistX, gridDistY);
|
|
|
|
bindEditModeDropdownHandlers(module);
|
|
}
|
|
|
|
// Add move links if neccesary
|
|
if (_.isEqual($("#update-canvas").data("can-move-modules"), "yes")) {
|
|
var moveModuleItem = document.createElement("li");
|
|
$(moveModuleItem).appendTo(dropdownMenu);
|
|
|
|
var moveModuleLink = document.createElement("a");
|
|
$(moveModuleLink)
|
|
.attr("href", "")
|
|
.attr("data-module-id", id)
|
|
.addClass("move-module")
|
|
.html($("#move-link-placeholder").text().trim())
|
|
.appendTo(moveModuleItem);
|
|
|
|
// Add clone click handler for the new module
|
|
$(moveModuleLink).on("click touchstart", moveModuleHandler);
|
|
|
|
// Add buttons for module groups
|
|
var moveModuleGroupItem = document.createElement("li");
|
|
$(moveModuleGroupItem).appendTo(dropdownMenu);
|
|
$(moveModuleGroupItem).hide();
|
|
|
|
var moveModuleGroupLink = document.createElement("a");
|
|
$(moveModuleGroupLink)
|
|
.attr("href", "")
|
|
.attr("data-module-id", id)
|
|
.addClass("move-module-group")
|
|
.html($("#move-group-link-placeholder").text().trim())
|
|
.appendTo(moveModuleGroupItem);
|
|
|
|
$(moveModuleGroupLink).on("click touchstart", moveModuleGroupHandler);
|
|
}
|
|
|
|
// Add delete links if neccesary
|
|
if (_.isEqual($("#update-canvas").data("can-delete-modules"), "yes")) {
|
|
var deleteModuleItem = document.createElement("li");
|
|
$(deleteModuleItem).appendTo(dropdownMenu);
|
|
|
|
var deleteModuleLink = document.createElement("a");
|
|
$(deleteModuleLink)
|
|
.attr("href", "")
|
|
.attr("data-module-id", id)
|
|
.addClass("delete-module")
|
|
.html($("#delete-link-placeholder").text().trim())
|
|
.appendTo(deleteModuleItem);
|
|
|
|
// Add delete click handler for the new module
|
|
$(deleteModuleLink).on("click touchstart", deleteModuleHandler);
|
|
|
|
var deleteModuleGroupItem = document.createElement("li");
|
|
$(deleteModuleGroupItem).appendTo(dropdownMenu);
|
|
$(deleteModuleGroupItem).hide();
|
|
|
|
var deleteModuleGroupLink = document.createElement("a");
|
|
$(deleteModuleGroupLink)
|
|
.attr("href", "")
|
|
.attr("data-module-id", id)
|
|
.addClass("delete-module-group")
|
|
.html($("#delete-group-link-placeholder").text().trim())
|
|
.appendTo(deleteModuleGroupItem);
|
|
|
|
// Add delete click handler for the new module
|
|
$(deleteModuleGroupLink).on("click touchstart", deleteModuleGroupHandler);
|
|
}
|
|
|
|
// Set it up for jsPlumb, depending on permissions
|
|
if (_.isEqual($("#update-canvas").data("can-reposition-modules"), "yes")) {
|
|
addDraggablesToInstance(module, gridDistX, gridDistY);
|
|
}
|
|
|
|
if (_.isEqual($("#update-canvas").data("can-edit-connections"), "yes")) {
|
|
setElementsAsDropTargets(module);
|
|
setElementsAsDragSources(module, null, null, EDIT_CONNECTOR_STYLE_2);
|
|
}
|
|
|
|
|
|
// Add dropdown touch support
|
|
bindTouchDropdowns($(dropdownToggle));
|
|
|
|
// Re-zoom dropdown menu, so the new module's no-scale dropdown gets
|
|
// rescaled
|
|
$(dropdownMenu).css("transform", "scale(" + (1.0 / instance.getZoom()) + ")");
|
|
$(dropdownMenu).css("transform-origin", "0 0");
|
|
|
|
// Add IDs to the form
|
|
var formAddInput = $('#update-canvas form input#add');
|
|
var formAddNameInput = $('#update-canvas form input#add-names');
|
|
var inputVal = formAddInput.attr("value");
|
|
var inputNameVal = formAddNameInput.attr("value");
|
|
if (_.isUndefined(inputVal) || inputVal === "") {
|
|
formAddInput.attr("value", id);
|
|
formAddNameInput.attr("value", name);
|
|
} else {
|
|
formAddInput.attr("value", inputVal + "," + id);
|
|
formAddNameInput.attr("value", inputNameVal + SUBMIT_FORM_NAME_SEPARATOR + name);
|
|
}
|
|
|
|
return module;
|
|
}
|
|
|
|
/**
|
|
* Bind the new module button action.
|
|
* @param gridDistX - The canvas grid distance in X direction.
|
|
* @param gridDistY - The canvas grid distance in Y direction.
|
|
*/
|
|
function bindNewModuleAction(gridDistX, gridDistY) {
|
|
function handleDragStart(event, ui) {
|
|
collided = false;
|
|
}
|
|
|
|
function handleDrag(event, ui) {
|
|
// Custom grid implementation is needed
|
|
// (so the new module snaps on the same grid offset
|
|
// as the other modules)
|
|
var l = ui.position.left;
|
|
var t = ui.position.top;
|
|
var gdx = gridDistX;
|
|
var gdy = gridDistY;
|
|
var z = instance.getZoom();
|
|
|
|
ui.position.left = Math.floor(l / (gdx * z)) * gdx;
|
|
ui.position.top = Math.floor(t / (gdy * z)) * gdy;
|
|
|
|
// Check if collision occured
|
|
var modules = $(".module");
|
|
var module;
|
|
for (var i = 0; i < modules.length; i++) {
|
|
module = $(modules[i]);
|
|
if (module.hasClass("new")) {
|
|
continue;
|
|
}
|
|
|
|
if (_.isEqual(ui.helper.position(), module.position())) {
|
|
// Collision!
|
|
collided = true;
|
|
break;
|
|
} else {
|
|
collided = false;
|
|
}
|
|
}
|
|
|
|
if (collided) {
|
|
ui.helper.addClass("collided");
|
|
} else {
|
|
ui.helper.removeClass("collided");
|
|
}
|
|
}
|
|
|
|
function handleDragStop(event, ui) {
|
|
if (!collided) {
|
|
// Disable scroll on canvas temporarily, as it can be
|
|
// dragged from modal area
|
|
toggleCanvasEvents(false);
|
|
|
|
// Copy the ui.helper, since it's gonna vanish soon!
|
|
var clone = ui.helper.clone(true, true);
|
|
clone.appendTo(draggable);
|
|
clone.css("z-index", "20");
|
|
|
|
// Open modal window
|
|
$("#modal-new-module").modal({
|
|
"backdrop": "static"
|
|
});
|
|
}
|
|
|
|
collided = false;
|
|
}
|
|
|
|
function handleNewNameConfirm(ev) {
|
|
var input = $("#new-module-name-input");
|
|
// Validate module name
|
|
var moduleNameValid = textValidator(ev, input,
|
|
<%= Constants::NAME_MIN_LENGTH %>, <%= Constants::NAME_MAX_LENGTH %>,
|
|
true);
|
|
if (moduleNameValid) {
|
|
// Set the "clicked" property to true
|
|
modal.data("submit", "true");
|
|
}
|
|
return moduleNameValid;
|
|
}
|
|
|
|
var newModuleBtn = $("#canvas-new-module");
|
|
var modal = $("#modal-new-module");
|
|
|
|
newModuleBtn.draggable({
|
|
cursor: "move",
|
|
helper: createVirtualModule,
|
|
start: handleDragStart,
|
|
drag: handleDrag,
|
|
stop: handleDragStop
|
|
});
|
|
|
|
// Prevent "new module" button from submitting form
|
|
newModuleBtn.click(function(event) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
return false;
|
|
});
|
|
|
|
// Bind the confirm button on modal
|
|
modal.find("button[data-action='confirm']").on("click", function(ev) {
|
|
handleNewNameConfirm(ev);
|
|
});
|
|
|
|
// Also, bind on modal window open & close
|
|
modal.on("show.bs.modal", function(ev) {
|
|
// Clear input
|
|
$(this).removeData("submit");
|
|
$(this).find("#new-module-name-input").val("");
|
|
|
|
// Remove potential error classes from form
|
|
$(this).find("#new-module-name-input").parent().removeClass("has-error");
|
|
$(this).find("span.help-block").remove();
|
|
|
|
// Bind onto input keypress (to prevent form from being submitted)
|
|
$(this).find("#new-module-name-input").keydown(function(ev) {
|
|
if (ev.keyCode == 13) {
|
|
if (handleNewNameConfirm(ev)) {
|
|
// Close modal
|
|
modal.modal("hide");
|
|
}
|
|
|
|
// In any case, prevent form submission
|
|
ev.preventDefault();
|
|
ev.stopPropagation();
|
|
return false;
|
|
}
|
|
});
|
|
});
|
|
|
|
modal.on("shown.bs.modal", function(event) {
|
|
// Focus the text element
|
|
$(this).find("#new-module-name-input").focus();
|
|
});
|
|
|
|
modal.on("hide.bs.modal", function (event) {
|
|
var newModule = $(".module.new");
|
|
|
|
$(this).find("#new-module-name-input").off("keydown");
|
|
|
|
if (_.isEqual($(event.target).data("submit"), "true")) {
|
|
// If modal was successfully submitted, generate the module
|
|
var id = "n" + newModuleIndex++;
|
|
graph.addNode(id);
|
|
var name = $(this).find("#new-module-name-input").val();
|
|
updateModuleHtml(newModule, id, name, gridDistX, gridDistY);
|
|
newModule.removeClass("new");
|
|
} else {
|
|
// Else, remove the element
|
|
newModule.remove();
|
|
}
|
|
|
|
// In any case, enable scrolling on edit screen again
|
|
toggleCanvasEvents(true);
|
|
});
|
|
}
|
|
|
|
function initEditModules() {
|
|
|
|
function handleRenameConfirm(modal, ev) {
|
|
var input = modal.find("#edit-module-name-input");
|
|
// Validate module name
|
|
var moduleNameValid = textValidator(ev, input,
|
|
<%= Constants::NAME_MIN_LENGTH %>, <%= Constants::NAME_MAX_LENGTH %>,
|
|
true);
|
|
if (moduleNameValid) {
|
|
var newName = input.val();
|
|
var moduleId = modal.attr("data-module-id");
|
|
var moduleEl = $("#" + moduleId);
|
|
// Update the module's name in GUI
|
|
moduleEl.attr("data-module-name", newName);
|
|
moduleEl.find(".panel-heading .panel-title").html(newName);
|
|
|
|
// Add this information to form
|
|
var formAddInput = $('#update-canvas form input#add');
|
|
var formAddNameInput = $('#update-canvas form input#add-names');
|
|
var formRenameInput = $("#update-canvas form input#rename");
|
|
var addedIds = formAddInput.attr("value").split(",");
|
|
var existingIndex = _.indexOf(addedIds, moduleEl.attr("data-module-id"));
|
|
if (existingIndex === -1) {
|
|
// Actually rename an existing module
|
|
var renameVal = JSON.parse(formRenameInput.attr("value"));
|
|
renameVal[moduleEl.attr("data-module-id")] = newName;
|
|
formRenameInput.attr("value", JSON.stringify(renameVal));
|
|
} else {
|
|
// Just rename the add-name entry
|
|
var addedNames = formAddNameInput.attr("value").split(SUBMIT_FORM_NAME_SEPARATOR);
|
|
addedNames[existingIndex] = newName;
|
|
formAddNameInput.attr("value", addedNames.join(SUBMIT_FORM_NAME_SEPARATOR));
|
|
}
|
|
|
|
// Hide modal
|
|
modal.modal("hide");
|
|
}
|
|
}
|
|
|
|
$("#modal-edit-module")
|
|
.on("show.bs.modal", function (event) {
|
|
var modal = $(this);
|
|
var moduleId = modal.attr("data-module-id");
|
|
var moduleEl = $("#" + moduleId);
|
|
var input = modal.find("#edit-module-name-input");
|
|
|
|
// Set the input to the current module's name
|
|
input.attr("value", moduleEl.attr("data-module-name"));
|
|
input.val(moduleEl.attr("data-module-name"));
|
|
|
|
// Bind on enter button
|
|
input.keydown(function(ev) {
|
|
if (ev.keyCode == 13) {
|
|
// "Submit" modal
|
|
handleRenameConfirm(modal, ev);
|
|
|
|
// In any case, prevent form submission
|
|
ev.preventDefault();
|
|
ev.stopPropagation();
|
|
return false;
|
|
}
|
|
});
|
|
})
|
|
.on("shown.bs.modal", function(event) {
|
|
// Focus the text element
|
|
$(this).find("#edit-module-name-input").focus();
|
|
})
|
|
.on("hide.bs.modal", function (event) {
|
|
// Remove potential error classes
|
|
$(this).find("#edit-module-name-input").parent().removeClass("has-error");
|
|
$(this).find("span.help-block").remove();
|
|
|
|
$(this).find("#edit-module-name-input").off("keydown");
|
|
|
|
// When hiding modal, re-enable events
|
|
toggleCanvasEvents(true);
|
|
});
|
|
|
|
// Bind the confirm button on modal
|
|
$("#modal-edit-module").find("button[data-action='confirm']").on("click", function(ev) {
|
|
var modal = $(this).closest(".modal");
|
|
handleRenameConfirm(modal, ev);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Handler when editing a specific module.
|
|
*/
|
|
editModuleHandler = function(ev) {
|
|
var modal = $("#modal-edit-module");
|
|
var moduleEl = $(this).closest(".module");
|
|
|
|
// Set modal's module id
|
|
modal.attr("data-module-id", moduleEl.attr("data-module-id"));
|
|
|
|
// Disable dragging & zooming events on canvas temporarily
|
|
toggleCanvasEvents(false);
|
|
|
|
// Show modal
|
|
modal.modal("show");
|
|
|
|
ev.preventDefault();
|
|
ev.stopPropagation();
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* Initialize editing of module groups.
|
|
*/
|
|
|
|
|
|
/**
|
|
* Handler when editing a module group.
|
|
*/
|
|
|
|
function initMoveModules() {
|
|
function handleMoveConfirm(modal) {
|
|
var moduleId = modal.attr("data-module-id");
|
|
var moduleEl = $("#" + moduleId);
|
|
var input = modal.find('.selectpicker');
|
|
var moveToExperimentId = input.val();
|
|
|
|
// Add this information to form
|
|
var formMoveInput = $("#update-canvas form input#move");
|
|
|
|
// Save mapping to input
|
|
var moveVal = JSON.parse(formMoveInput.attr("value"));
|
|
moveVal[moduleEl.attr("data-module-id")] = moveToExperimentId;
|
|
formMoveInput.attr("value", JSON.stringify(moveVal));
|
|
|
|
updateFormWithModulesData(moduleEl, '', GRID_DIST_EDIT_X, GRID_DIST_EDIT_Y);
|
|
|
|
// Delete module from canvas
|
|
deleteModule(moduleEl.attr("data-module-id"), true);
|
|
|
|
// Hide modal
|
|
modal.modal("hide");
|
|
}
|
|
|
|
$("#modal-move-module")
|
|
.on("show.bs.modal", function (event) {
|
|
var modal = $(this);
|
|
var moduleId = modal.attr("data-module-id");
|
|
var moduleEl = $("#" + moduleId);
|
|
var input = modal.find('.selectpicker');
|
|
|
|
// Bind on enter button
|
|
input.keydown(function(ev) {
|
|
if (ev.keyCode == 13) {
|
|
// "Submit" modal
|
|
handleMoveConfirm(modal);
|
|
|
|
// In any case, prevent form submission
|
|
ev.preventDefault();
|
|
ev.stopPropagation();
|
|
return false;
|
|
}
|
|
});
|
|
})
|
|
.on("shown.bs.modal", function(event) {
|
|
// Focus the text element
|
|
$(this).find(".selectpicker").selectpicker().focus();
|
|
})
|
|
.on("hide.bs.modal", function (event) {
|
|
// When hiding modal, re-enable events
|
|
toggleCanvasEvents(true);
|
|
});
|
|
|
|
// Bind the confirm button on modal
|
|
$("#modal-move-module").find("button[data-action='confirm']").on("click", function(event) {
|
|
var modal = $(this).closest(".modal");
|
|
handleMoveConfirm(modal);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Handler when trying to move a specific module.
|
|
*/
|
|
moveModuleHandler = function(ev) {
|
|
var modal = $("#modal-move-module");
|
|
var moduleEl = $(this).closest(".module");
|
|
|
|
// Set modal's module id
|
|
modal.attr("data-module-id", moduleEl.attr("data-module-id"));
|
|
|
|
// Disable dragging & zooming events on canvas temporarily
|
|
toggleCanvasEvents(false);
|
|
|
|
// Show modal
|
|
modal.modal("show");
|
|
|
|
ev.preventDefault();
|
|
ev.stopPropagation();
|
|
return false;
|
|
};
|
|
|
|
|
|
/**
|
|
* Initialize editing of module groups.
|
|
*/
|
|
function initMoveModuleGroups() {
|
|
function handleMoveModuleGroupConfirm(modal) {
|
|
var moduleId = modal.attr("data-module-id");
|
|
var moduleEl = $("#" + moduleId);
|
|
var input = modal.find('.selectpicker');
|
|
var moveToExperimentId = input.val();
|
|
|
|
// Retrieve all modules in this module group
|
|
var components = connectedComponents(graph, moduleId.toString());
|
|
var group = _.map(components, function(id) { return $("#" + id); });
|
|
|
|
var conns = _.filter(graph.edges(), function(conn) {
|
|
return _.contains(components, conn[0]) || _.contains(components, conn[1]);
|
|
});
|
|
|
|
updateFormWithModulesData(group, conns.toString(), GRID_DIST_EDIT_X, GRID_DIST_EDIT_Y);
|
|
|
|
// Add move information to form
|
|
var formMoveInput = $("#update-canvas form input#move");
|
|
|
|
moveModules = [];
|
|
_.each(group, function(m) {
|
|
moveModules.push(m.attr("data-module-id"));
|
|
});
|
|
|
|
// Put the array into input
|
|
var moveVal = JSON.parse(formMoveInput.attr("value"));
|
|
moveVal[moveModules] = moveToExperimentId;
|
|
formMoveInput.attr("value", JSON.stringify(moveVal));
|
|
|
|
_.each(group, function(m) {
|
|
deleteModule(m.attr("data-module-id"));
|
|
});
|
|
|
|
// Hide modal
|
|
modal.modal("hide");
|
|
}
|
|
|
|
$("#modal-move-module-group")
|
|
.on("show.bs.modal", function (event) {
|
|
var modal = $(this);
|
|
var moduleId = modal.attr("data-module-id");
|
|
var moduleEl = $("#" + moduleId);
|
|
var input = modal.find('.selectpicker');
|
|
|
|
// Bind on enter button
|
|
input.keydown(function(ev) {
|
|
if (ev.keyCode == 13) {
|
|
// "Submit" modal
|
|
handleMoveConfirm(modal);
|
|
|
|
// In any case, prevent form submission
|
|
ev.preventDefault();
|
|
ev.stopPropagation();
|
|
return false;
|
|
}
|
|
});
|
|
})
|
|
.on("shown.bs.modal", function(event) {
|
|
// Focus the text element
|
|
$(this).find(".selectpicker").selectpicker().focus();
|
|
})
|
|
.on("hide.bs.modal", function (event) {
|
|
// When hiding modal, re-enable events
|
|
toggleCanvasEvents(true);
|
|
});
|
|
|
|
// Bind the confirm button on modal
|
|
$("#modal-move-module-group").find("button[data-action='confirm']").on("click", function(event) {
|
|
var modal = $(this).closest(".modal");
|
|
handleMoveModuleGroupConfirm(modal);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Handler when editing a module group.
|
|
*/
|
|
moveModuleGroupHandler = function(ev) {
|
|
var modal = $("#modal-move-module-group");
|
|
var moduleEl = $(this).closest(".module");
|
|
|
|
// Set modal's module id
|
|
modal.attr("data-module-id", moduleEl.attr("data-module-id"));
|
|
|
|
// Disable dragging & zooming events on canvas temporarily
|
|
toggleCanvasEvents(false);
|
|
|
|
// Show modal
|
|
modal.modal("show");
|
|
|
|
ev.preventDefault();
|
|
ev.stopPropagation();
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* Bind the delete module buttons actions.
|
|
*/
|
|
function bindDeleteModuleAction() {
|
|
// First, bind the delete module handler onto all "delete module" links
|
|
$(".module-options a.delete-module").on("click touchstart", deleteModuleHandler);
|
|
|
|
// Then, bind on modal events
|
|
var modal = $("#modal-delete-module");
|
|
|
|
// Bind the confirm button on modal
|
|
modal.find("button[data-action='confirm']").on("click", function(event) {
|
|
// Set the "clicked" property to true
|
|
modal.data("submit", "true");
|
|
});
|
|
|
|
// Also, bind on modal window open & close
|
|
modal.on("show.bs.modal", function(event) {
|
|
// Remove submit flag
|
|
$(this).removeData("submit");
|
|
|
|
// Disable dragging & zooming events on canvas temporarily
|
|
toggleCanvasEvents(false);
|
|
});
|
|
|
|
modal.on("hide.bs.modal", function (event) {
|
|
if (_.isEqual($(event.target).data("submit"), "true")) {
|
|
// If modal was successfully submitted, delete the module
|
|
var id = $(event.target).data("module-id");
|
|
|
|
deleteModule(id.toString(), true);
|
|
}
|
|
|
|
// In any case, re-enable events on canvas
|
|
toggleCanvasEvents(true);
|
|
});
|
|
}
|
|
|
|
function deleteModule(id, linkConnections) {
|
|
var ins = graph.inEdges(id);
|
|
var outs = graph.outEdges(id);
|
|
var tempModuleEl;
|
|
|
|
// Remove id from the graph structure, along with all connections
|
|
if (graph.hasNode(id)) {
|
|
graph.removeNode(id);
|
|
}
|
|
|
|
// Remove the module <div>, along with all connections
|
|
instance.remove($("#" + id));
|
|
|
|
// Connect the sources to destinations
|
|
if (linkConnections) {
|
|
_.each(ins, function(inEdge) {
|
|
_.each(outs, function(outEdge) {
|
|
// Only connect 2 nodes if
|
|
// such a connection doesn't exist already
|
|
if (!graph.hasEdge(inEdge[0], outEdge[1])) {
|
|
graph.addEdge(inEdge[0], outEdge[1]);
|
|
instance.connect({
|
|
source: $("#" + inEdge[0]),
|
|
target: $("#" + outEdge[1])
|
|
});
|
|
}
|
|
});
|
|
});
|
|
|
|
//Hide module group options for unconnected modules
|
|
if (outs.length === 0) { // If node is sink
|
|
_.each (ins, function(inEdge) {
|
|
if (graph.degree(inEdge[0]) === 0) {
|
|
tempModuleEl = $("#" + inEdge[0]);
|
|
tempModuleEl.find(".clone-module-group").parents("li").hide();
|
|
tempModuleEl.find(".move-module-group").parents("li").hide();
|
|
tempModuleEl.find(".delete-module-group").parents("li").hide();
|
|
}
|
|
});
|
|
}
|
|
if (ins.length === 0) { // If node is source
|
|
_.each (outs, function(outEdge) {
|
|
if (graph.degree(outEdge[1]) === 0) {
|
|
tempModuleEl = $("#" + outEdge[1]);
|
|
tempModuleEl.find(".clone-module-group").parents("li").hide();
|
|
tempModuleEl.find(".move-module-group").parents("li").hide();
|
|
tempModuleEl.find(".delete-module-group").parents("li").hide();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// Add ID to the form
|
|
var formAddInput = $('#update-canvas form input#add');
|
|
var formAddNamesInput = $('#update-canvas form input#add-names');
|
|
var formClonedInput = $('#update-canvas form input#cloned');
|
|
var formRemoveInput = $('#update-canvas form input#remove');
|
|
var formMoveInput = $('#update-canvas form input#move');
|
|
var inputVal, newVal;
|
|
var vals, idx;
|
|
var addToRemoveList = true;
|
|
|
|
// If the module was moved, we don't need to do anything with it
|
|
inputVal = formMoveInput.attr("value");
|
|
if (!_.isUndefined(inputVal) && inputVal !== "") {
|
|
moved = [];
|
|
$.each(JSON.parse(formMoveInput.val()), function(key, value) {
|
|
if (key.match(/.*,.*/))
|
|
moved = moved.concat(key.split(','));
|
|
else
|
|
moved.push(key);
|
|
});
|
|
|
|
if (_.contains(moved, id)) {
|
|
addToRemoveList = false;
|
|
return;
|
|
}
|
|
}
|
|
|
|
// If the module we are deleting was added via JS
|
|
// (and hasn't been saved yet), we don't need to "add" it
|
|
// neither "remove" it, it simply ceases to exist
|
|
inputVal = formAddInput.attr("value");
|
|
if (!_.isUndefined(inputVal) && inputVal !== "") {
|
|
vals = inputVal.split(",");
|
|
if (_.contains(vals, id)) {
|
|
addToRemoveList = false;
|
|
idx = vals.indexOf(id);
|
|
vals.splice(idx, 1);
|
|
formAddInput.attr("value", vals.join());
|
|
vals = formAddNamesInput.attr("value").split(SUBMIT_FORM_NAME_SEPARATOR);
|
|
vals.splice(idx, 1);
|
|
formAddNamesInput.attr("value", vals.join(SUBMIT_FORM_NAME_SEPARATOR));
|
|
}
|
|
}
|
|
|
|
// Okay, the module was not created, but it might be cloned,
|
|
// so we need to check that as well
|
|
if (!addToRemoveList) {
|
|
inputVal = formClonedInput.attr("value");
|
|
if (!_.isUndefined(inputVal) && inputVal !== "") {
|
|
vals = _.map(inputVal.split(";"), function(val) {
|
|
return val.split(",")[1];
|
|
});
|
|
if (_.contains(vals, id)) {
|
|
addToRemoveList = false;
|
|
|
|
// Remove the cloned module from the cloned list
|
|
newVal = "";
|
|
_.each(inputVal.split(";"), function(val) {
|
|
if (!_.isEqual(val.split(",")[1], id)) {
|
|
newVal = (newVal === "" ? "" : (newVal + ";")) + val;
|
|
}
|
|
});
|
|
formClonedInput.attr("value", newVal);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (addToRemoveList) {
|
|
inputVal = formRemoveInput.attr("value");
|
|
if (_.isUndefined(inputVal) || inputVal === "") {
|
|
formRemoveInput.attr("value", id);
|
|
} else {
|
|
formRemoveInput.attr("value", inputVal + "," + id);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handler function when deleting a single module.
|
|
*/
|
|
deleteModuleHandler =function() {
|
|
var id = $(this).data("module-id");
|
|
var modal = $("#modal-delete-module");
|
|
|
|
var name = $(".module#" + id).data("module-name");
|
|
var template = modal.find("#message-template").text().trim();
|
|
|
|
// Set the modal message
|
|
modal.find("#delete-message").text(template.replace("%{module}", name));
|
|
|
|
// Send module id to modal
|
|
modal.data("module-id", id);
|
|
|
|
// Display delete modal
|
|
modal.modal({
|
|
"backdrop": "static"
|
|
});
|
|
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* Bind the delete module group buttons actions.
|
|
*/
|
|
function bindDeleteModuleGroupAction() {
|
|
// First, bind the delete module group handler onto all
|
|
// "delete module group" links
|
|
$(".module-options a.delete-module-group").on("click touchstart", deleteModuleGroupHandler);
|
|
|
|
// Then, bind on modal events
|
|
var modal = $("#modal-delete-module-group");
|
|
|
|
// Bind the confirm button on modal
|
|
modal.find("button[data-action='confirm']").on("click", function(event) {
|
|
// Set the "clicked" property to true
|
|
modal.data("submit", "true");
|
|
});
|
|
|
|
// Also, bind on modal window open & close
|
|
modal.on("show.bs.modal", function(event) {
|
|
// Remove submit flag
|
|
$(this).removeData("submit");
|
|
|
|
// Disable dragging & zooming events on canvas temporarily
|
|
toggleCanvasEvents(false);
|
|
});
|
|
|
|
modal.on("hide.bs.modal", function (event) {
|
|
if (_.isEqual($(event.target).data("submit"), "true")) {
|
|
// If modal was successfully submitted, delete the module
|
|
var id = $(event.target).data("module-id");
|
|
|
|
// Find all modules in the connected component
|
|
var modules = connectedComponents(graph, id.toString());
|
|
|
|
// Delete all modules of the module group
|
|
_.each(modules, function(moduleId) {
|
|
deleteModule(moduleId, false);
|
|
});
|
|
}
|
|
|
|
// In any case, re-enable events on canvas
|
|
toggleCanvasEvents(true);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Handler function when deleting module group.
|
|
*/
|
|
deleteModuleGroupHandler = function() {
|
|
var id = $(this).data("module-id");
|
|
var modal = $("#modal-delete-module-group");
|
|
|
|
var name = $(".module#" + id).data("module-name");
|
|
var template = modal.find("#message-template").text().trim();
|
|
|
|
// Set the modal message
|
|
modal.find("#delete-message").text(template.replace("%{module}", name));
|
|
|
|
// Send module id to modal
|
|
modal.data("module-id", id);
|
|
|
|
// Display delete modal
|
|
modal.modal({
|
|
"backdrop": "static"
|
|
});
|
|
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* Bind the clone module action.
|
|
* @param element - jQUery selector for the element on which the click action will run.
|
|
* @param modulesSel - The selector string for all modules.
|
|
* @param gridDistX - The grid distance in X direction.
|
|
* @param gridDistY - The grid distance in Y direction.
|
|
*/
|
|
function bindCloneModuleAction(element, modulesSel, gridDistX, gridDistY) {
|
|
element.on("click touchstart", function(event) {
|
|
cloneModuleHandler($(this).data("module-id"), modulesSel, gridDistX, gridDistY);
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
return false;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Handler function when cloning a single module.
|
|
* @param moduleId - The ID of the original module.
|
|
* @param modulesSel - The selector string for all modules.
|
|
* @param gridDistX - The grid distance in X direction.
|
|
* @param gridDistY - The grid distance in Y direction.
|
|
*/
|
|
cloneModuleHandler = function(moduleId, modulesSel, gridDistX, gridDistY) {
|
|
var modules = $(modulesSel);
|
|
var module = modules.filter("#" + moduleId);
|
|
|
|
// Position for the cloned module
|
|
var top = elTop(module) + (3 * gridDistY);
|
|
var left = elLeft(module) + (3 * gridDistX);
|
|
|
|
cloneModule(module, gridDistX, gridDistY, left, top);
|
|
|
|
// Hide all open dropdowns
|
|
$(".module-options").removeClass("open");
|
|
module.css("z-index", 20);
|
|
};
|
|
|
|
/**
|
|
* Clone the original module.
|
|
* @param originalModule - The jQuery original module selector.
|
|
* @param gridDistX - The grid distance in X direction.
|
|
* @param gridDistY - The grid distance in Y direction.
|
|
* @param left - The left position of the new module.
|
|
* @param top - The top position of the new module.
|
|
* @return The new module.
|
|
*/
|
|
function cloneModule(originalModule, gridDistX, gridDistY, left, top) {
|
|
var moduleId = originalModule.data("module-id");
|
|
|
|
// Create new module element
|
|
var id = "n" + newModuleIndex++;
|
|
graph.addNode(id);
|
|
var newModule = createVirtualModule();
|
|
elLeft(newModule, left);
|
|
elTop(newModule, top);
|
|
updateModuleHtml(newModule, id, originalModule.data("module-name"), gridDistX, gridDistY);
|
|
newModule.removeClass("new");
|
|
|
|
// Add the cloned module id into the hidden input field
|
|
var formAddInput = $('#update-canvas form input#add');
|
|
var formAddNamesInput = $('#update-canvas form input#add-names');
|
|
var formClonedInput = $('#update-canvas form input#cloned');
|
|
var inputVal, inputNameVal;
|
|
|
|
// If we cloned a module with virtual id, there are 2 possibilities:
|
|
// 1. Original module is newly created, which means that our cloned
|
|
// module can also simply be treated as a new module;
|
|
// 2. Original module is a cloned module, which means we want to extend
|
|
// original module's original module into this new module
|
|
var originalId = moduleId;
|
|
var originalWasCloned = false;
|
|
var fillClonedInput = true;
|
|
|
|
if (_.isEqual(moduleId.toString().charAt(0), "n")) {
|
|
// Find the original module's "original module", and retrieve its id
|
|
// If such ID cannot be found, original module was not cloned
|
|
fillClonedInput = false;
|
|
inputVal = formClonedInput.attr("value");
|
|
_.each(inputVal.split(";"), function(val) {
|
|
var val2 = val.split(",");
|
|
if (_.isEqual(val2[1], moduleId)) {
|
|
originalId = val2[0];
|
|
fillClonedInput = true;
|
|
}
|
|
});
|
|
}
|
|
|
|
if (fillClonedInput) {
|
|
inputVal = formClonedInput.attr("value");
|
|
if (_.isUndefined(inputVal) || inputVal === "") {
|
|
formClonedInput.attr("value", originalId + "," + id);
|
|
} else {
|
|
formClonedInput.attr("value", inputVal + ";" + originalId + "," + id);
|
|
}
|
|
}
|
|
|
|
instance.repaintEverything();
|
|
|
|
return newModule;
|
|
}
|
|
|
|
/**
|
|
* Bind the clone module group action.
|
|
* @param element - jQUery selector for the element on which the click action will run.
|
|
* @param modulesSel - The selector string for all modules.
|
|
* @param gridDistX - The grid distance in X direction.
|
|
* @param gridDistY - The grid distance in Y direction.
|
|
*/
|
|
function bindCloneModuleGroupAction(element, modulesSel, gridDistX, gridDistY) {
|
|
element.on("click touchstart", function(event) {
|
|
cloneModuleGroupHandler($(this).data("module-id"), modulesSel, gridDistX, gridDistY);
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
return false;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Handler function when cloning a module group.
|
|
* @param moduleId - The ID of the original module.
|
|
* @param modulesSel - The selector string for all modules.
|
|
* @param gridDistX - The grid distance in X direction.
|
|
* @param gridDistY - The grid distance in Y direction.
|
|
*/
|
|
cloneModuleGroupHandler = function(moduleId, modulesSel, gridDistX, gridDistY) {
|
|
var modules = $(modulesSel);
|
|
|
|
// Retrieve all modules in this module group
|
|
var components = connectedComponents(graph, moduleId.toString());
|
|
var group = _.map(components, function(id) { return $("#" + id); });
|
|
|
|
// Calculate the size of the rectangle containing the whole workflow
|
|
var width, height;
|
|
var minX = Number.MAX_VALUE, maxX = -Number.MAX_VALUE;
|
|
var minY = Number.MAX_VALUE, maxY = -Number.MAX_VALUE;
|
|
|
|
_.each(group, function(m) {
|
|
var l = elLeft(m);
|
|
var t = elTop(m);
|
|
if (l < minX) { minX = l; }
|
|
if (l > maxX) { maxX = l; }
|
|
if (t < minY) { minY = t; }
|
|
if (t > maxY) { maxY = t; }
|
|
});
|
|
m_width = modules.first().width();
|
|
m_height = modules.first().height();
|
|
width = maxX - minX + m_width + (3 * gridDistX);
|
|
height = maxY - minY + m_height + (3 * gridDistY);
|
|
|
|
// Find the appropriate "free space"
|
|
var left = minX != Number.MAX_VALUE ? minX : 0;
|
|
var top = maxY != -Number.MAX_VALUE ? maxY : 0;
|
|
var offset = 0;
|
|
var moduleContained;
|
|
|
|
while (true) {
|
|
moduleContained = false;
|
|
for (var i = 0; i < modules.length; i++) {
|
|
var module = $(modules[i]);
|
|
|
|
// Skip modules from the module group
|
|
if (_.contains(components, module.data("module-id").toString())) {
|
|
continue;
|
|
}
|
|
|
|
var ml = elLeft(module);
|
|
var mt = elTop(module);
|
|
|
|
if (ml >= left &&
|
|
ml <= left + width - gridDistX &&
|
|
mt >= top + offset &&
|
|
mt <= top + offset + height - gridDistY) {
|
|
moduleContained = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// If no module contained, exit
|
|
if (!moduleContained) {
|
|
break;
|
|
}
|
|
|
|
offset += gridDistY;
|
|
}
|
|
|
|
// Alright, clone all modules from the group and
|
|
// move them by the vertical offset
|
|
clones = {};
|
|
_.each(group, function(m) {
|
|
var nm = cloneModule(m, gridDistX, gridDistY, elLeft(m), elTop(m) + height + offset - gridDistY);
|
|
|
|
//Show module group options
|
|
nm.find(".clone-module-group").parents("li").show();
|
|
nm.find(".move-module-group").parents("li").show();
|
|
nm.find(".delete-module-group").parents("li").show();
|
|
|
|
clones[m.attr("data-module-id")] = nm.attr("data-module-id");
|
|
});
|
|
|
|
// Also, copy the outbound connections
|
|
_.each(_.keys(clones), function(originalId) {
|
|
var clonedId = clones[originalId];
|
|
|
|
_.each(graph.successors(originalId), function(outNode) {
|
|
graph.addEdge(clonedId, clones[outNode]);
|
|
instance.connect({
|
|
source: $("#" + clonedId),
|
|
target: $("#" + clones[outNode])
|
|
});
|
|
});
|
|
});
|
|
|
|
// Hide all open dropdowns
|
|
$(".module-options").removeClass("open");
|
|
$("#" + moduleId.toString()).css("z-index", 20);
|
|
|
|
// Repainting is needed twice (weird, huh)
|
|
instance.repaintEverything();
|
|
instance.repaintEverything();
|
|
};
|
|
|
|
/**
|
|
* Before submission, graph & module group info needs to be
|
|
* copied into hidden input fields via form
|
|
* submission callback.
|
|
* @param gridDistX - The canvas grid distance in X direction.
|
|
* @param gridDistY - The canvas grid distance in Y direction.
|
|
*/
|
|
function bindEditFormSubmission(gridDistX, gridDistY) {
|
|
$('#update-canvas form').submit(function(){
|
|
var modules = $(".diagram .module");
|
|
|
|
updateFormWithModulesData(modules, graph.edges().toString(), gridDistX, gridDistY)
|
|
|
|
ignoreUnsavedWorkAlert = true;
|
|
return true;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Update form with given modules position and connections.
|
|
* @param modules - Modules, which should be inserted into form
|
|
* @param connections- Connections, which should be inserted into form (empty if
|
|
* no connections)
|
|
* @param gridDistX - The canvas grid distance in X direction.
|
|
* @param gridDistY - The canvas grid distance in Y direction.
|
|
*/
|
|
function updateFormWithModulesData(modules, connections, gridDistX, gridDistY) {
|
|
var connectionsDiv = $('#update-canvas form input#connections');
|
|
var positionsDiv = $('#update-canvas form input#positions');
|
|
var moduleNamesDiv = $('#update-canvas form input#module-groups');
|
|
|
|
// Connections are easy, just copy graph data
|
|
if (connections) {
|
|
if (connectionsDiv.val())
|
|
connectionsDiv.attr("value", connectionsDiv.val() + ',' + connections.toString());
|
|
else
|
|
connectionsDiv.attr("value", connections.toString());
|
|
}
|
|
|
|
// Positions are a bit more tricky, but still pretty straightforward
|
|
var moduleGroupNames = {};
|
|
var positionsVal = "";
|
|
var module, id, x, y;
|
|
_.each(modules, function(m) {
|
|
module = $(m);
|
|
id = module.attr("data-module-id");
|
|
x = elLeft(module) / gridDistX;
|
|
y = elTop(module) / gridDistY;
|
|
positionsVal += id + "," + x + "," + y + ";";
|
|
moduleGroupNames[id] = module.attr("data-module-group-name");
|
|
});
|
|
positionsDiv.attr("value", positionsDiv.val() + positionsVal);
|
|
|
|
if (moduleNamesDiv.val())
|
|
moduleNamesDiv.attr("value", JSON.stringify($.extend(JSON.parse(moduleNamesDiv.val()),
|
|
moduleGroupNames)));
|
|
else
|
|
moduleNamesDiv.attr("value", JSON.stringify(moduleGroupNames));
|
|
}
|
|
|
|
/**
|
|
* Position the modules onto the canvas.
|
|
* @param modulesSel - The jQuery selector text of module elements.
|
|
* @param gridDistX - The X canvas grid distance.
|
|
* @param gridDistY - The Y canvas grid distance.
|
|
*/
|
|
function positionModules(modulesSel, gridDistX, gridDistY) {
|
|
var modules = $(modulesSel);
|
|
|
|
var module, x, y;
|
|
_.each(modules, function(m) {
|
|
module = $(m);
|
|
x = module.data("module-x");
|
|
y = module.data("module-y");
|
|
elLeft(module, x * gridDistX);
|
|
elTop(module, y * gridDistY);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Add draggable element/s to the jsPlumb instance.
|
|
* @param elements - The elements selector.
|
|
* @param gridDistX - The grid distance in X direction.
|
|
* @param gridDistY - The grid distance in Y direction.
|
|
*/
|
|
function addDraggablesToInstance(elements, gridDistX, gridDistY) {
|
|
function handleDragStart(event, ui) {
|
|
var draggedModule = $(event.el);
|
|
|
|
leftInitial = elLeft(draggedModule);
|
|
topInitial = elTop(draggedModule);
|
|
collided = false;
|
|
|
|
draggedModule
|
|
.css("z-index", "25")
|
|
.addClass("dragged");
|
|
}
|
|
|
|
function handleDrag(event, ui) {
|
|
var draggedModule = $(event.el);
|
|
var modules = $(".module");
|
|
|
|
// Check if collision occured
|
|
var module;
|
|
for (var i = 0; i < modules.length; i++) {
|
|
module = $(modules[i]);
|
|
if (_.isEqual(module, draggedModule)) {
|
|
continue;
|
|
}
|
|
|
|
if (_.isEqual(draggedModule.position(), module.position())) {
|
|
// Collision!
|
|
collided = true;
|
|
break;
|
|
} else {
|
|
collided = false;
|
|
}
|
|
}
|
|
|
|
if (collided) {
|
|
draggedModule.addClass("collided");
|
|
} else {
|
|
draggedModule.removeClass("collided");
|
|
}
|
|
}
|
|
|
|
function handleDragStop(event, ui) {
|
|
var draggedModule = $(event.el);
|
|
|
|
draggedModule
|
|
.css("z-index", "20")
|
|
.removeClass("dragged");
|
|
|
|
// Reposition element to back where it was
|
|
if (collided) {
|
|
draggedModule.removeClass("collided");
|
|
elLeft(draggedModule, leftInitial);
|
|
elTop(draggedModule, topInitial);
|
|
instance.repaintEverything();
|
|
}
|
|
|
|
collided = false;
|
|
}
|
|
|
|
instance.draggable(elements, {
|
|
snapThreshold: Math.max(gridDistX, gridDistY),
|
|
grid: [gridDistX, gridDistY],
|
|
start: handleDragStart,
|
|
drag: handleDrag,
|
|
stop: handleDragStop
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Set the specified elements as drop targets in jsPlumb instance.
|
|
* @param elements - The elements to be used as drop targets.
|
|
*/
|
|
function setElementsAsDropTargets(elements) {
|
|
instance.makeTarget(elements, {
|
|
dropOptions: { hoverClass: "dragHover" },
|
|
anchor: "AutoDefault",
|
|
allowLoopback: false
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Set the specified elements as drag sources in jsPlumb instance.
|
|
* @param elements - The elements to be used as drag sources.
|
|
* @param anchorStyle - Anchor style.
|
|
* @param connectorStyle - Connector style.
|
|
* @param connectorStyle2 - Connector style 2.
|
|
*/
|
|
function setElementsAsDragSources(elements, anchorStyle, connectorStyle, connectorStyle2) {
|
|
// CSS_STYLE
|
|
var newAnchorStyle = anchorStyle || DEFAULT_ANCHOR_STYLE;
|
|
var newConnectorStyle = connectorStyle || DEFAULT_CONNECTOR_STYLE;
|
|
var newConnectorStyle2 = connectorStyle2 || DEFAULT_CONNECTOR_STYLE_2;
|
|
|
|
instance.makeSource(elements, {
|
|
filter: ".ep",
|
|
anchor: newAnchorStyle,
|
|
connector: newConnectorStyle,
|
|
allowLoopback: false,
|
|
connectorStyle: newConnectorStyle2,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Zooms the specified instance element, while retaining
|
|
* the non-zoomable children.
|
|
* @param zoomableElement - The element to be zoomed.
|
|
* @param zoom - The zoom level (value between 0 and 1).
|
|
* @param origin - The zoom origin, can be null.
|
|
*/
|
|
function zoomInstance(zoomableElement, zoom, origin) {
|
|
zoomableElement.css("transform", "scale(" + zoom + ")");
|
|
|
|
if (!_.isUndefined(origin)) {
|
|
zoomableElement.css("transform-origin", origin);
|
|
}
|
|
|
|
instance.setZoom(zoom);
|
|
|
|
// Make sure the no-scale elements are kept on original scale:
|
|
var noscale = zoomableElement.find(".no-scale");
|
|
noscale.css("transform", "scale(" + (1.0 / zoom) + ")");
|
|
noscale.css("transform-origin", "0 0");
|
|
}
|
|
|
|
/**
|
|
* Calculates the draggable element's size.
|
|
* @param draggable - The draggable element.
|
|
* @return - An object {width: <width>, height: <height>}.
|
|
*/
|
|
function calculateDraggableSize(draggable) {
|
|
// Since draggable's width & height is usually 0,
|
|
// we need to calculate its actual width & height
|
|
// by extracting positions of its children (e.g. modules)
|
|
var minX = Number.MAX_VALUE, maxX = -Number.MAX_VALUE;
|
|
var minY = Number.MAX_VALUE, maxY = -Number.MAX_VALUE;
|
|
var child, left, top, right, bottom;
|
|
_.each(draggable.children(), function(c) {
|
|
child = $(c);
|
|
left = elLeft(child);
|
|
top = elTop(child);
|
|
right = left + child.width();
|
|
bottom = top + child.height();
|
|
if (left < minX) { minX = left; }
|
|
if (right > maxX) { maxX = right; }
|
|
if (top < minY) { minY = top; }
|
|
if (bottom > maxY) { maxY = bottom; }
|
|
});
|
|
if (minX === Number.MAX_VALUE) { minX = -1; }
|
|
if (maxX === -Number.MAX_VALUE) { maxX = 1; }
|
|
if (minY === Number.MAX_VALUE) { minY = -1; }
|
|
if (maxY === -Number.MAX_VALUE) { maxY = 1; }
|
|
|
|
return { width: maxX - minX, height: maxY - minY };
|
|
}
|
|
|
|
/**
|
|
* Remembers the draggable element's position.
|
|
* @param draggable - The draggable element.
|
|
* @param parent - The parent element to which the draggable element
|
|
* is relative to.
|
|
*/
|
|
function rememberDraggablePosition(draggable, parent) {
|
|
// Calculate the center of the draggable's parent X & Y
|
|
var centerX = parent.width() / 2 - elLeft(draggable);
|
|
var centerY = parent.height() / 2 - elTop(draggable);
|
|
|
|
var draggableSize = calculateDraggableSize(draggable);
|
|
|
|
draggableLeft = centerX / draggableSize.width;
|
|
draggableTop = centerY / draggableSize.height;
|
|
}
|
|
|
|
/**
|
|
* Restores the draggable element's position.
|
|
* @param draggable - The draggable element.
|
|
*/
|
|
function restoreDraggablePosition(draggable, parent) {
|
|
// Calculate the center of the draggable's parent X & Y
|
|
var centerX = parent.width() / 2;
|
|
var centerY = parent.height() / 2;
|
|
|
|
var draggableSize = calculateDraggableSize(draggable);
|
|
|
|
var left = draggableLeft * draggableSize.width;
|
|
var top = draggableTop * draggableSize.height;
|
|
|
|
elLeft(draggable, centerX - left);
|
|
elTop(draggable, centerY - top);
|
|
}
|
|
|
|
/**
|
|
* Initialize the modules group hover animation.
|
|
* @param modules - The modules jQuery selector.
|
|
* @param sidebar - The sidebar jQuery selector.
|
|
*/
|
|
function initModulesHover(modules, sidebar) {
|
|
function handlerIn() {
|
|
var groupId = $(this).data("module-group");
|
|
var groupModules = modules.filter("[data-module-group='" + groupId + "']");
|
|
var groupMenu = sidebar.find("li[data-module-group='" + groupId + "']");
|
|
groupModules.addClass("group-hover");
|
|
groupMenu.addClass("group-hover");
|
|
|
|
var moduleId = $(this).data("module-id");
|
|
if (!_.isUndefined(moduleId)) {
|
|
var currentModule = modules.filter("[data-module-id='" + moduleId + "']");
|
|
var currentMenu = sidebar.find("li[data-module-id='" + moduleId + "']");
|
|
currentModule.addClass("module-hover");
|
|
currentMenu.addClass("module-hover");
|
|
}
|
|
}
|
|
function handlerOut() {
|
|
var groupId = $(this).data("module-group");
|
|
var groupModules = modules.filter("[data-module-group='" + groupId + "']");
|
|
var groupMenu = $("li[data-module-group='" + groupId + "']");
|
|
groupModules.removeClass("group-hover");
|
|
groupMenu.removeClass("group-hover");
|
|
|
|
var moduleId = $(this).data("module-id");
|
|
if (!_.isUndefined(moduleId)) {
|
|
var currentModule = modules.filter("[data-module-id='" + moduleId + "']");
|
|
var currentMenu = sidebar.find("li[data-module-id='" + moduleId + "']");
|
|
currentModule.removeClass("module-hover");
|
|
currentMenu.removeClass("module-hover");
|
|
}
|
|
}
|
|
modules.hover(handlerIn, handlerOut);
|
|
sidebar.find("li[data-module-id]").hover(handlerIn, handlerOut);
|
|
sidebar.find("li[data-module-group]").hover(handlerIn, handlerOut);
|
|
}
|
|
|
|
/**
|
|
* Calculates the specified module group size.
|
|
* @param modules - The modules belonging to this module group.
|
|
* @return - An object {width: <width>, height: <height>}.
|
|
*/
|
|
function calculateModuleGroupSize(modules) {
|
|
var minX = Number.MAX_VALUE, maxX = -Number.MAX_VALUE;
|
|
var minY = Number.MAX_VALUE, maxY = -Number.MAX_VALUE;
|
|
var module, left, top, right, bottom;
|
|
_.each(modules, function(m) {
|
|
module = $(m);
|
|
left = elLeft(module);
|
|
top = elTop(module);
|
|
right = left + module.width();
|
|
bottom = top + module.height();
|
|
if (left < minX) { minX = left; }
|
|
if (right > maxX) { maxX = right; }
|
|
if (top < minY) { minY = top; }
|
|
if (bottom > maxY) { maxY = bottom; }
|
|
});
|
|
if (minX === Number.MAX_VALUE) { minX = -1; }
|
|
if (maxX === -Number.MAX_VALUE) { maxX = 1; }
|
|
if (minY === Number.MAX_VALUE) { minY = -1; }
|
|
if (maxY === -Number.MAX_VALUE) { maxY = 1; }
|
|
|
|
return {
|
|
left: minX,
|
|
top: minY,
|
|
width: maxX - minX,
|
|
height: maxY - minY
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Initialize the modules & groups click action on sidebar
|
|
* so the modules/groups are then centered in canvas.
|
|
* @param modules - The modules jQuery selector.
|
|
* @param sidebar - The sidebar jQuery selector.
|
|
* @param draggable - The canvas draggable jQuery selector.
|
|
* @param parent - The parent of the draggable element.
|
|
* @param modulePadding - The top-left padding form module display.
|
|
*/
|
|
function initSidebarClicks(modules, sidebar, draggable, parent, modulePadding) {
|
|
function moduleHandler(event) {
|
|
var moduleId = $(this).closest("li").data("module-id");
|
|
var module = modules.filter("[data-module-id='" + moduleId + "']");
|
|
var centerX = parent.width() / 2;
|
|
var centerY = parent.height() / 2;
|
|
var left = centerX - elLeft(module) - (module.width() / 2);
|
|
var top = centerY - elTop(module) - (module.height() / 2);
|
|
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
animateReposition(draggable, left, top);
|
|
return false;
|
|
}
|
|
function moduleGroupHandler(event) {
|
|
var groupId = $(this).closest("li").data("module-group");
|
|
var groupModules = modules.filter("[data-module-group='" + groupId + "']");
|
|
var groupSize = calculateModuleGroupSize(groupModules);
|
|
var centerX = parent.width() / 2;
|
|
var centerY = parent.height() / 2;
|
|
var left, top;
|
|
if (groupSize.width > parent.width() || groupSize.height > parent.height()) {
|
|
left = -groupSize.left + modulePadding;
|
|
top = -groupSize.top + modulePadding;
|
|
} else {
|
|
left = centerX - groupSize.left - (groupSize.width / 2);
|
|
top = centerY - groupSize.top - (groupSize.height / 2);
|
|
}
|
|
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
animateReposition(draggable, left, top);
|
|
return false;
|
|
}
|
|
sidebar.find("li[data-module-id] > span > a.canvas-center-on").click(moduleHandler);
|
|
sidebar.find("li[data-module-group] > span > a.canvas-center-on").click(moduleGroupHandler);
|
|
}
|
|
|
|
/**
|
|
* Initialize the jsPlumb by creating a new jsPlumb instance
|
|
* (overrides the currently set global variable 'instance').
|
|
* @param containerSel - The jQuery selector text of the canvas container.
|
|
* @param containerChildSel - The jQuery selector text of the canvas container child.
|
|
* @param modulesSel - The jQuery selector text of module elements.
|
|
* @param params - Various parameters:
|
|
* @param params.gridDistX - Grid distance between modules in X direction.
|
|
* @param params.gridDistY - Grid distance between modules in Y direction.
|
|
* @param params.modulesDraggable - True if modules are draggable.
|
|
* @param params.connectionsEditable - True if connections can be created/edited/removed.
|
|
* @param params.zoomEnabled - True if zooming in/out is enabled.
|
|
* @param params.zoomMin - The minimum zoom level (0. or greater, 1.0 is no zoom).
|
|
* @param params.zoomMax - The maximum zoom level (0. or greater, 1.0 is no zoom).
|
|
* @param params.zoomDist - The distance to travel towards mouse position on zoom.
|
|
* @param params.scrollEnabled - True if dragging/scrolling of content is enabled.
|
|
* @param params.endpointStyle - jsPlumb endpoint style.
|
|
* @param params.connectionHoverStyle - jsPlumb style.
|
|
* @param params.connectionOverlayStyle - jsPlumb style.
|
|
* @param params.connectionLabelStyle - jsPlumb style.
|
|
* @param params.anchorStyle - jsPlumb anchor style.
|
|
* @param params.connectorStyle - jsPlumb endpoint style.
|
|
* @param params.connectorStyle2 - jsPlumb endpoint style.
|
|
*/
|
|
function initJsPlumb(containerSel, containerChildSel, modulesSel, params) {
|
|
// Functions used for scrolling
|
|
function mouseUp(event) {
|
|
container.off("mousemove touchmove");
|
|
}
|
|
function mouseDown(event) {
|
|
var source = $(event.target);
|
|
if (!$(modulesSel).is(source) &&
|
|
$(modulesSel).has(source).length === 0 &&
|
|
source.is($(container))) {
|
|
// Only do drag & drop if it doesn't
|
|
// origin from module element
|
|
drag_type = DRAG_INVALID;
|
|
if (event.type == "mousedown" && (event.which || event.button) == 1) {
|
|
drag_type = DRAG_MOUSE;
|
|
} else if (event.type == "touchstart" && event.originalEvent.touches.length == 1) {
|
|
drag_type = DRAG_TOUCH;
|
|
}
|
|
if (drag_type > 0) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
x_start = calcOffsetX(event);
|
|
y_start = calcOffsetY(event);
|
|
container.on("mousemove touchmove", moveDiagram);
|
|
}
|
|
}
|
|
function moveDiagram(event, x_offset, y_offset) {
|
|
// This function is invoked on mousemove
|
|
var x_pos = 0, y_pos = 0, x_el = 0, y_el = 0;
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
x_el = draggable.offset().left - draggable.parent().offset().left;
|
|
y_el = draggable.offset().top - draggable.parent().offset().top;
|
|
// Scale offset for X
|
|
// (otherwise, this function only works on scale = 1.0)
|
|
x_so = draggable.parent().width() / 2 * (1 - instance.getZoom());
|
|
|
|
var fastOffsetX = calcOffsetX(event);
|
|
var fastOffsetY = calcOffsetY(event);
|
|
|
|
x_pos = x_el - x_so + (fastOffsetX - x_start);
|
|
y_pos = y_el + (fastOffsetY - y_start);
|
|
x_start = fastOffsetX;
|
|
y_start = fastOffsetY;
|
|
if (draggable !== null) {
|
|
elLeft(draggable, x_pos);
|
|
elTop(draggable, y_pos);
|
|
}
|
|
}
|
|
}
|
|
// This needs to be here due to Firefox
|
|
function calcOffsetX(e) {
|
|
return (drag_type == DRAG_TOUCH ?
|
|
(e.originalEvent.touches[0].offsetX || e.originalEvent.touches[0].pageX) :
|
|
(e.offsetX || e.pageX)
|
|
) - $(e.target).offset().left;
|
|
}
|
|
function calcOffsetY(e) {
|
|
return (drag_type == DRAG_TOUCH ?
|
|
(e.originalEvent.touches[0].offsetY || e.originalEvent.touches[0].pageY) :
|
|
(e.offsetY || e.pageY)
|
|
) - $(e.target).offset().top;
|
|
}
|
|
|
|
// Default parameter values
|
|
var params2 = params ? params : {};
|
|
var gridDistX = params2.gridDistX || 350;
|
|
var gridDistY = params2.gridDistY || 350;
|
|
var modulesDraggable = params2.modulesDraggable || false;
|
|
var connectionsEditable = params2.connectionsEditable || false;
|
|
var zoomEnabled = params2.zoomEnabled || false;
|
|
var zoomMin = params2.zoomMin || 0.3;
|
|
var zoomMax = params2.zoomMax || 1.0;
|
|
var zoomDist = params2.zoomDist || 150;
|
|
var scrollEnabled = params2.scrollEnabled || true;
|
|
|
|
// CSS_STYLE
|
|
var endpointStyle = params2.endpointStyle || DEFAULT_ENDPOINT_STYLE;
|
|
var connectionHoverStyle = params2.connectionHoverStyle || DEFAULT_CONNECTION_HOVER_STYLE;
|
|
var connectionOverlayStyle = params2.connectionOverlayStyle || DEFAULT_CONNECTION_OVERLAY_STYLE;
|
|
var connectionLabelStyle = params2.connectionLabelStyle || DEFAULT_CONNECTION_LABEL_STYLE;
|
|
var anchorStyle = params2.anchorStyle || null;
|
|
var connectorStyle = params2.connectorStyle || null;
|
|
var connectorStyle2 = params2.connectorStyle2 || null;
|
|
|
|
// End of parameters block
|
|
|
|
var container = $(containerSel);
|
|
var containerChild = $(containerChildSel);
|
|
|
|
// Script for multitouch events
|
|
hammertime = new Hammer(document.getElementById("canvas-container"));
|
|
hammertime.get('pinch').set({ enable: canHammer });
|
|
|
|
function hammerZoom (event) {
|
|
zoom = instance.getZoom() * event.scale;
|
|
if (zoom < zoomMin)
|
|
zoom = zoomMin;
|
|
else if (zoom > zoomMax)
|
|
zoom = zoomMax;
|
|
zoomInstance(containerChild, zoom);
|
|
}
|
|
|
|
// Setup some styling defaults for jsPlumb
|
|
instance = jsPlumb.getInstance({
|
|
Endpoint: endpointStyle,
|
|
ConnectionOverlays: [ connectionOverlayStyle ],
|
|
Container: containerChild
|
|
});
|
|
|
|
window.jsp = instance;
|
|
var modules = $(modulesSel);
|
|
var jsp_modules = jsPlumb.getSelector(modulesSel);
|
|
draggable = $(containerChildSel);
|
|
|
|
if (modulesDraggable) {
|
|
// Initialize draggable elements
|
|
addDraggablesToInstance(jsp_modules, gridDistX, gridDistY);
|
|
}
|
|
|
|
if (connectionsEditable) {
|
|
instance.bind("beforeDetach", function(connection) {
|
|
});
|
|
|
|
// Prevent a connection to be made if it already exists between 2 modules
|
|
instance.bind("beforeDrop", function(info) {
|
|
var newConnection = info.connection;
|
|
var allConnections = instance.getAllConnections();
|
|
var conn;
|
|
for (var i = 0; i < allConnections.length; i++) {
|
|
conn = allConnections[i];
|
|
if (_.isEqual(conn.source, newConnection.source) &&
|
|
_.isEqual(conn.target, newConnection.target)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Now, check if we created a cycle
|
|
var srcNode = $(newConnection.source).attr("data-module-id");
|
|
var targetNode = $(newConnection.target).attr("data-module-id");
|
|
graph.addEdge(srcNode, targetNode);
|
|
if (!jsnx.isDirectedAcyclicGraph(graph)) {
|
|
graph.removeEdge(srcNode, targetNode);
|
|
return false;
|
|
}
|
|
|
|
var srcModuleEl = $("#" + srcNode);
|
|
var targetModuleEl = $("#" + targetNode);
|
|
|
|
//Modules should belong to module group now
|
|
//Show module group options for target and source
|
|
|
|
srcModuleEl.find(".clone-module-group").parents("li").show();
|
|
srcModuleEl.find(".move-module-group").parents("li").show();
|
|
srcModuleEl.find(".delete-module-group").parents("li").show();
|
|
|
|
targetModuleEl.find(".clone-module-group").parents("li").show();
|
|
targetModuleEl.find(".move-module-group").parents("li").show();
|
|
targetModuleEl.find(".delete-module-group").parents("li").show();
|
|
return true;
|
|
});
|
|
|
|
// Bind a connection listener. Note that the parameter passed to this function contains more than
|
|
// just the new connection - see the documentation for a full list of what is included in 'info'.
|
|
// this listener sets the connection's internal
|
|
// id as the label overlay's text.
|
|
instance.bind("connection", function (info) {
|
|
_.each(instance.getAllConnections(), function(conn) {
|
|
conn.endpoints[0].setEnabled(false);
|
|
conn.endpoints[1].setEnabled(false);
|
|
});
|
|
});
|
|
|
|
// Bind a click listener to each connection
|
|
instance.bind("click", function (c) {
|
|
// Remove the edge from our graph data structure
|
|
graph.removeEdge(c.sourceId, c.targetId);
|
|
|
|
// Remove edge from GUI
|
|
instance.detach(c);
|
|
|
|
// Hide module group options if source or target module
|
|
// is not part of a module group anymore
|
|
|
|
var srcModuleEl = $("#" + c.sourceId);
|
|
var targetModuleEl = $("#" + c.targetId);
|
|
//First source
|
|
if (graph.degree(c.sourceId) === 0) {
|
|
srcModuleEl.find(".clone-module-group").parents("li").hide();
|
|
srcModuleEl.find(".move-module-group").parents("li").hide();
|
|
srcModuleEl.find(".delete-module-group").parents("li").hide();
|
|
}
|
|
if (graph.degree(c.targetId) === 0) {
|
|
targetModuleEl.find(".clone-module-group").parents("li").hide();
|
|
targetModuleEl.find(".move-module-group").parents("li").hide();
|
|
targetModuleEl.find(".delete-module-group").parents("li").hide();
|
|
}
|
|
});
|
|
}
|
|
|
|
// Suspend drawing and initialize
|
|
if (modules.length > 0) {
|
|
instance.batch(function () {
|
|
// Set elements as connection sources
|
|
setElementsAsDragSources(jsp_modules, anchorStyle, connectorStyle, connectorStyle2);
|
|
|
|
// Initialise all elements as connection targets
|
|
setElementsAsDropTargets(jsp_modules);
|
|
|
|
// Initialize module connections
|
|
var module, outs;
|
|
_.each(modules, function(m) {
|
|
module = $(m);
|
|
outs = graph.successors(module.attr("data-module-id"));
|
|
_.each(outs, function(out) {
|
|
instance.connect({source: module, target: $("div[data-module-id=" + out + "]")})
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
// Enable/disable connection endpoints
|
|
/*
|
|
_.each(instance.getAllConnections(), function(conn) {
|
|
conn.endpoints[0].setEnabled(connectionsEditable);
|
|
conn.endpoints[1].setEnabled(connectionsEditable);
|
|
});
|
|
*/
|
|
_.each(instance.getAllConnections(), function(conn) {
|
|
conn.endpoints[0].setEnabled(false);
|
|
conn.endpoints[1].setEnabled(false);
|
|
});
|
|
// Update style on existing connections
|
|
if (connectionsEditable) {
|
|
_.each(instance.getAllConnections(), function(conn) {
|
|
conn.setHoverPaintStyle(connectionHoverStyle);
|
|
conn.setLabel(connectionLabelStyle);
|
|
});
|
|
}
|
|
|
|
// Make sure the new connections will have same style
|
|
if (connectionsEditable) {
|
|
instance.importDefaults({
|
|
HoverPaintStyle: connectionHoverStyle,
|
|
ConnectionOverlays: [
|
|
connectionOverlayStyle,
|
|
[ "Label", connectionLabelStyle ]
|
|
]
|
|
});
|
|
} else {
|
|
instance.importDefaults({
|
|
HoverPaintStyle: connectionHoverStyle,
|
|
ConnectionOverlays: [ connectionOverlayStyle ]
|
|
});
|
|
}
|
|
|
|
// Enable / disable instance configs
|
|
if (modules.length > 0) {
|
|
instance.setDraggable(jsp_modules, modulesDraggable);
|
|
instance.setSourceEnabled(jsp_modules, connectionsEditable);
|
|
instance.setTargetEnabled(jsp_modules, connectionsEditable);
|
|
}
|
|
|
|
// Bind the mouse scroll event to scaling
|
|
if (zoomEnabled) {
|
|
container.mousewheel(function(event) {
|
|
zoom = instance.getZoom() + (event.deltaY / 20);
|
|
if (zoom < zoomMin || zoom > zoomMax) {
|
|
return;
|
|
}
|
|
zoomInstance(containerChild, zoom);
|
|
});
|
|
|
|
hammertime.on('pinch', hammerZoom);
|
|
}
|
|
|
|
if (scrollEnabled) {
|
|
// Make the jsPlumb container movable/draggable for "google maps" effect
|
|
container.on("mousedown touchstart", mouseDown);
|
|
container.on("mouseup mouseout touchend", mouseUp);
|
|
}
|
|
|
|
// If this is not triggered, dropdown in edit mode
|
|
// don't work on Firefox prior to zooming the canvas
|
|
zoomInstance(containerChild, 1.0);
|
|
|
|
jsPlumb.fire("jsPlumbLoaded", instance);
|
|
}
|
|
|
|
// Opens edit mode if redirected from empty experiment
|
|
(function noWorkflowimgEditMode(){
|
|
if( getParam('editMode') ){
|
|
$("#edit-canvas-button").click();
|
|
}
|
|
})();
|
|
|
|
/** prevent reload page */
|
|
var preventCanvasReloadOnSave = (function() {
|
|
'use strict';
|
|
|
|
function confirmReload() {
|
|
if( confirm(I18n.t('experiments.canvas.reload_on_submit')) ) {
|
|
return true;
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
|
|
function preventCanvasReload() {
|
|
document.onkeydown = function(){
|
|
switch (event.keyCode){
|
|
case 116:
|
|
event.returnValue = false;
|
|
return confirmReload();
|
|
case 82:
|
|
if (event.ctrlKey){
|
|
event.returnValue = false;
|
|
return confirmReload();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function bindToCanvasSave(fun) {
|
|
$('#canvas-save').on('click', function() {
|
|
fun();
|
|
})
|
|
}
|
|
|
|
return function() { bindToCanvasSave(preventCanvasReload) };
|
|
})();
|