Commit 19fa29c6 authored by Maarten de Waard's avatar Maarten de Waard 🤘🏻
Browse files

Merge branch '66-clean-up-of-code-from-mobile-phone-compatibility-issue' into 'master'

Resolve "Clean up of code from mobile phone compatibility issue"

Closes #66

See merge request totem/ind!47
parents a4b403af be308c0b
INDie, *Interactive Network Diagrams in edX* is meant to easily make diagrams in
edX studio that can be shown to learners in the LMS. The diagram creator can add
simple interactions to cells in the diagrams. The triggers "on hover" and "on
click" can be applied on a cell. These triggers make sure other cells are
highlighted, or otherwise hidden cells are shown.
simple interactions to cells in the diagrams: hovering over or clicking on such
a cell will trigger an animation, which will highlight, hide or show other
cells.
INDie is based on mxGraph_, the library behind Draw.io_.
......@@ -15,7 +15,7 @@ For Graph Editors
Adding INDie to a course
------------------------
1. Ask your system administator to install INDie on your edX platform. The
1. Ask your system administator to install INDie on your edX platform. The
steps to do so are listed in the Developers section.
2. Log in to edX studio
3. Select the course where you want to enable the edX XBlock
......@@ -53,13 +53,13 @@ Adding interactions
1. Click a cell that you want to use for the "trigger"
2. Open the *Arrange* panel in the right sidebar. You should see an
*Interaction* panel at the bottom. If you don't see the *Interaction* panel:
*Interactions* panel at the bottom. If you don't see the *Interactions* panel:
- Make sure you selected exactly 1 cell, you can not use more than 1 cell as
a trigger at the moment.
- The Arrange panel only shows interactions for cells that are not in a
group. You can ungroup cells by clicking the *Ungroup* button if it exists.
3. Click *Add interaction*. You can now choose from three animations:
Highlight
......@@ -76,16 +76,16 @@ Adding interactions
5. Choose a target cell. This is the cell that will get animated (either
highlighted, shown or hidden).
1. Click the *Pick cell* button
2. Click the cell you want to animate
3. The *Pick cell* button will show a numeric ID for the cell you picked. If
it doesn't, try again.
**Note:** It is possible to animate several cells with 1 action, but only
if they are *grouped*. You should follow these steps to make a group
**before** choosing a target cell:
1. Drag-select, or click several cells while holding the Shift or Ctrl
button.
2. Under the *Arrange* panel click *Group*.
......
......@@ -3,15 +3,14 @@ function GraphEditorXBlock(runtime, element, data) {
// can only be used if we know that all keys are defined in the language
// specific file) mxResources.loadDefaultBundle = false;
var bundle = mxResources.getDefaultBundle(RESOURCE_BASE, mxLanguage);
//|| mxResources.getSpecialBundle(RESOURCE_BASE, mxLanguage);
//|| mxResources.getSpecialBundle(RESOURCE_BASE, mxLanguage);
// Add indie resources.
mxResources.add(mxClient.basePath + '/resources/icons');
// Fixes possible asynchronous requests
mxUtils.getAll([bundle, [data.resource_url]], function(xhr)
{
mxUtils.getAll([bundle, [data.resource_url]], function(xhr) {
// Adds bundle text to resources
mxResources.parse(xhr[0].getText());
......@@ -23,8 +22,7 @@ function GraphEditorXBlock(runtime, element, data) {
var read_only = (data.read_only == 'true');
// Main
$(element).find('.mx-graph-editor').each(function(i, editorDiv)
{
$(element).find('.mx-graph-editor').each(function(i, editorDiv) {
// Add the Totem icons to the StencilRegistry
mxStencilRegistry.loadStencilSet(STENCIL_PATH + '/totem-security.indie',
null);
......@@ -55,15 +53,18 @@ function GraphEditorXBlock(runtime, element, data) {
// This makes edX update the graph in the course preview
// when saving succeeded.
var callback = function() {
runtime.notify('save', {state: 'end'});
runtime.notify('save', {
state: 'end'
});
};
runtime.notify('save', {state: 'start'});
runtime.notify('save', {
state: 'start'
});
editorUi.saveFile(false, callback);
});
}
});
}, function()
{
}, function() {
document.body.innerHTML =
'<center style="margin-top:10%;">Error loading resource files. ' +
'Please check browser console.</center>';
......@@ -82,8 +83,7 @@ function GraphEditorXBlock(runtime, element, data) {
* graph - graph object to operate on
* callback - callback function to run after picking cell
*/
function pickCell(graph, callback)
{
function pickCell(graph, callback) {
var listener = {
mouseDown: function(sender, evt) {},
mouseMove: function(sender, evt) {},
......@@ -124,8 +124,7 @@ function pickCell(graph, callback)
* forceDialog - ignored parameter from original implementation
* callback - function that gets called on success (optional)
*/
EditorUi.prototype.saveFile = function(forceDialog, callback)
{
EditorUi.prototype.saveFile = function(forceDialog, callback) {
var ui = this;
$.ajax({
type: 'POST',
......@@ -136,7 +135,7 @@ EditorUi.prototype.saveFile = function(forceDialog, callback)
success: function(data) {
// Disable the "Are you sure you want to leave the page?" dialog
ui.editor.setModified(false);
if (typeof callback === "function") {
callback();
}
......@@ -159,26 +158,19 @@ EditorUi.prototype.saveFile = function(forceDialog, callback)
*
* @return {Menus} the edited menu
*/
EditorUi.prototype.createMenus = function()
{
EditorUi.prototype.createMenus = function() {
var menus = new Menus(this);
menus.put('file',
new Menu(mxUtils.bind(menus, function(menu, parent)
{
menus.addMenuItems(
menu,
['save', '-', 'pageSetup', 'print'], parent);
}
)
)
new Menu(mxUtils.bind(menus, function(menu, parent) {
menus.addMenuItems(
menu,
['save', '-', 'pageSetup', 'print'], parent);
}))
);
menus.put('help',
new Menu(mxUtils.bind(menus, function(menu, parent)
{
this.addMenuItems(menu, ['about']);
}
)
)
new Menu(mxUtils.bind(menus, function(menu, parent) {
this.addMenuItems(menu, ['about']);
}))
);
return menus;
};
......@@ -207,28 +199,28 @@ mxGraph.prototype.pageFormat = new mxRectangle(0, 0, 760, 740);
*/
Sidebar.prototype.init = function() {
this.addSearchPalette(true);
var title = mxResources.get('totem-security');
var style = ';whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#000000;strokeWidth=2';
var stencilFile = STENCIL_PATH + '/totem-security.xml';
this.addStencilPalette('totem-security', title, stencilFile, style);
title = mxResources.get('totem-security-line');
stencilFile = STENCIL_PATH + '/totem-security-line.xml';
this.addStencilPalette('totem-security-line', title, stencilFile, style);
this.addGeneralPalette(false);
this.addMiscPalette(false);
this.addAdvancedPalette(false);
this.addBasicPalette(STENCIL_PATH);
title = mxResources.get('arrows');
stencilFile = STENCIL_PATH + '/arrows.xml';
this.addStencilPalette('arrows', title, stencilFile, style);
this.addUmlPalette(false);
this.addBpmnPalette(STENCIL_PATH, false);
stencilFile = STENCIL_PATH + '/flowchart.xml';
this.addStencilPalette('flowchart', 'Flowchart', stencilFile, style);
};
......@@ -255,9 +247,9 @@ Sidebar.prototype.addPaletteFunctions = function(id, title, expanded, fns) {
* prevent that call, without modifying the library code.
*/
var oldHTMLFocus = HTMLElement.prototype.focus;
HTMLElement.prototype.focus = function () {
HTMLElement.prototype.focus = function() {
// Only prevent execution for this specific div:
if (this.className !== 'geDiagramContainer') {
oldHTMLFocus.apply(this, arguments);
}
};
\ No newline at end of file
oldHTMLFocus.apply(this, arguments);
}
};
This diff is collapsed.
......@@ -19,9 +19,10 @@ var interactions = {
* Function: install
*
* Install interactive components of a graph.
* Do this by scanning the graph for special data attributes that
* specify the interactions, parsing the attribute values into actions,
* and installing event handlers to run these actions.
*
* Scans the graph for attributes that specify interactions, parses the
* attribute values into interaction rules and installs event handlers that
* will trigger the animations defined in those rules.
*
* Parameters:
*
......@@ -32,79 +33,122 @@ var interactions = {
// Check if this cell has a getAttribute function; if not it's not
// relevant here.
if (typeof cell.value.getAttribute != 'function') {
return;
return;
}
// Newer graphs have all their rules in the onclick attribute:
var clickRules = JSON.parse(cell.value.getAttribute('onclick')) || [];
// Older graphs may have rules in the onhover attribute:
var hoverRules = JSON.parse(cell.value.getAttribute('onhover')) || [];
// Get the node and all nodes of the children of cell:
var triggerNodes = interactions.cellNodes(graph, cell, true);
var handlers = [];
var actionString = cell.value.getAttribute('onclick');
if (actionString != null && actionString != '') {
handlers = interactions.createEventHandlers(graph, actionString);
handlers.forEach(function(handler) {
// To support mobile, both click and hover will trigger the
// defined onclick action:
interactions.installClick(handler, graph, triggerNodes);
interactions.installHover(handler, graph, triggerNodes);
});
}
actionString = cell.value.getAttribute('onhover');
if (actionString != null && actionString != '') {
handlers = interactions.createEventHandlers(graph, actionString);
handlers.forEach(function(handler) {
// To support mobile, both click and hover will trigger the
// defined onhover action:
interactions.installClick(handler, graph, triggerNodes);
interactions.installHover(handler, graph, triggerNodes);
});
}
clickRules.concat(hoverRules).forEach(function(rule) {
try {
var handler = interactions.createHandler(graph, rule);
interactions.installHandler(handler, graph, triggerNodes);
} catch (e) {
// Report a debug message but continue creating handlers:
var message = 'Installing an interaction handler failed: ';
debug(message + e.message);
}
});
});
},
/**
* Function: installClick
* Function: createHandler
*
* Install event handlers to run a handler when the given node is clicked.
* Convert an interaction rule object into an event handler.
*
* The rule object, generally directly parsed from JSON, should contain at
* least the following properties:
* - "action": specifies the animation ("popup", "hide" or "highlight")
* - "targets": an array of objects, each with a "cell_id" (int) property
* But it can contain other, action/animation specific, properties like:
* - "colour": a valid CSS colour value (needed by "highlight" for example)
*
* Examples:
* - {"action": "popup", "targets": [{"cell_id": 2}, {"cell_id": 6}]}
* - {"action": "highlight", "targets": [{"cell_id": 3}], "colour": "red"}
*
* Returns a single handler function, ready to be applied to a graph; that
* handler function returns another function to apply to the graph to
* reset/stop the action (for example when the hover ends, etc.).
*
* Parameters:
*
* handler - handler function representing action to run
* graph - graph object to install handlers for
* nodes - nodes that should trigger the action when clicked
* graph - graph object that the action should operate on
* rule - interaction rule object
*/
installClick: function(handler, graph, nodes) {
nodes.forEach(function(node) {
node.addEventListener('click', function(e) {
var reset = handler(graph);
// Install a handler to run the `reset` function of the given
// handler when the node is clicked again.
$(node).one('click', function(e) {
// Catch the click so it's only used for our reset action,
// not for anything else.
e.stopPropagation();
// Run the `reset` function of the given handler.
reset();
});
});
});
createHandler: function(graph, rule) {
if (!rule) {
throw new Error("rule is empty or undefined");
}
if (typeof rule.action == 'undefined') {
throw new Error("field 'action' not specified");
}
if (!Array.isArray(rule.targets) || !rule.targets.length) {
throw new Error('no target specified for rule');
}
var cells = interactions.getTargetCells(graph, rule.targets);
if (!Array.isArray(cells) || !cells.length) {
throw new Error("no valid cells specified by field 'targets'");
}
switch (rule.action) {
case 'hide':
// fall through
case 'popup':
// for 'popup', visibility is TRUE during the event, but
// FALSE before and after the event. For 'hide' it is the
// other way around.
var visibleDuringEvent = rule.action === 'popup';
// Reverse the visibility before the event:
interactions.setCellVisibility(graph, cells, !visibleDuringEvent);
return function() {
// Apply the visibility when the event starts:
interactions.setCellVisibility(graph, cells, visibleDuringEvent);
return function() {
// Reverse the visibility again when the event ends:
interactions.setCellVisibility(graph, cells, !visibleDuringEvent);
};
};
case 'highlight':
if (typeof rule.colour == 'undefined') {
rule.colour = '#0000FF';
}
return function() {
// Add the highlights when the event starts.
var h = interactions.highlightCells(graph, cells, rule.colour);
return function() {
// Remove the highlights when the event ends.
for (var i = 0; i < h.length; i++) {
h[i].hide();
}
};
};
default:
throw new Error('unknown action type "' + rule.action + '"');
}
},
/**
* Function: installHover
* Function: installHandler
*
* Install event handlers to run an action when the given node is hovered
* over.
* Install an event handler to be triggered when the specified nodes are
* clicked or hovered over. Click and hover are treated as the same trigger
* to ensure compatibility between desktop and mobile usage.
*
* Parameters:
*
* handler - handler function representing action to run
* graph - graph object to install handlers for
* nodes - nodes that should trigger the action when hovered over
* nodes - nodes that should trigger the action when clicked or hovered
*/
installHover: function(handler, graph, nodes) {
installHandler: function(handler, graph, nodes) {
nodes.forEach(function(node) {
// Let the handler be triggered by hover events:
var reset;
node.addEventListener('mouseover', function(e) {
reset = handler(graph);
......@@ -112,112 +156,32 @@ var interactions = {
node.addEventListener('mouseout', function(e) {
reset();
});
});
},
/**
* Function: createEventHandlers
*
* Parse the action string (e.g., value of an onhover attribute)
* and create the corresponding event handlers.
*
* The action string should be formatted in json. It should be a list of
* dictionaries, each representing an action. Every dictionary should have
* at least an "action" field, with a string specifying which action to
* perform (currently one of "hide", "popup" or "highlight"), plus
* additional attributes depending on the specific action.
*
* Example:
* [{"action": "popup", "targets": [{"cell_id": 2}, {"cell_id": 6}]},
* {"action": "highlight", "targets": [{"cell_id": 3}], "colour": "red"}]
*
* Returns a list of actions: every action is a function to apply to a
* graph; that function returns another function to apply to the graph to
* reset/stop the action (for example when the hover ends, etc.).
*
* Parameters:
*
* graph - graph object that the action should operate on
* actionString - attribute value to parse
*/
createEventHandlers: function(graph, actionString) {
var rules = JSON.parse(actionString);
var handlers = [];
rules.forEach(function(rule) {
if (typeof rule.action == 'undefined') {
debug("field 'action' not specified");
return;
}
switch (rule.action) {
case 'hide':
// fall through
case 'popup':
// for 'popup', visibility is TRUE during the event, but
// FALSE before and after the event. For 'hide' it is the
// other way around.
var visibleDuringEvent = rule.action === 'popup';
if (Array.isArray(rule.targets))
{
rule.targets.forEach(function(target) {
var cells = interactions.getTargetCells(graph, target);
// Reverse the visibility before the event:
interactions.setCellVisibility(graph, cells, !visibleDuringEvent);
handlers.push(function() {
// Apply the visibility when the event starts:
interactions.setCellVisibility(graph, cells, visibleDuringEvent);
return function() {
// Reverse the visibility again when the event ends:
interactions.setCellVisibility(graph, cells, !visibleDuringEvent);
};
});
});
}
else
{
debug('Warning: No target specified for rule');
}
break;
case 'highlight':
if (typeof rule.colour == 'undefined') {
rule.colour = '#0000FF';
}
if (Array.isArray(rule.targets))
{
rule.targets.forEach(function(target) {
var cells = interactions.getTargetCells(graph, target);
handlers.push(function() {
// Add the highlights when the event starts.
var h = interactions.highlightCells(graph, cells, rule.colour);
return function() {
// Remove the highlights when the event ends.
for (var i = 0; i < h.length; i++) {
h[i].hide();
}
};
});
});
}
else
{
debug('Warning: No target specified for rule');
}
break;
default:
debug('action \"' + rule.action + '\" not found');
}
// let the handler be triggerd by click events:
node.addEventListener('click', function(e) {
var reset = handler(graph);
// Install a handler to run the `reset` function of the given
// handler when the node is clicked again.
$(node).one('click', function(e) {
// Catch the click so it's only used for our reset action,
// not for anything else.
e.stopPropagation();
// Run the `reset` function of the given handler.
reset();
});
});
});
return handlers;
},
/**
* Function: getTargetCells
*
* Get an array of cells from a target specification.
*
* Get an array of cells from an array of target specifications.
*
* Parameters:
*
* graph - graph object to operate on
* target - object containing cell specification; currently this should be
* targets - array of cell specification objects; currently they should have
* a "cell_id" property containing the numeric id of the cell.
*
* Returns:
......@@ -225,24 +189,36 @@ var interactions = {
* An array with the cell object. If the cell is a group, it returns an
* array of all the cells in the group.
*/
getTargetCells: function(graph, target) {
if (typeof target.cell_id == 'undefined') {
debug('could not obtain target from specification:');
debug(target);
return [];
}
var cell = graph.getModel().getCell(target.cell_id);
if (typeof cell == 'undefined') {
debug('Target cell is undefined.');
return [];
}
var children = graph.getModel().getChildren(cell) || [];
if (children.length > 0) {
// Assume this is a group and return the children (not the parent)
return children;
getTargetCells: function(graph, targets) {
var cells = [];
var cell = null;
for (var i = 0; i < targets.length; i++) {
if (typeof targets[i].cell_id === 'undefined') {
debug('Could not obtain target from specification:');
debug(targets[i]);
continue;
}
cell = graph.getModel().getCell(targets[i].cell_id);
if (typeof cell === 'undefined') {
debug('Could not obtain a cell with this ID:');
debug(targets[i].cell_id);
continue;
}
// Test whether the cell is a group with child cells:
var children = graph.getModel().getChildren(cell) || [];
if (children.length > 0) {
// Assume this is a group and add the children (not the parent)
cells = cells.concat(children);
} else {
// No children, so assume not a group and add the single cell
cells.push(cell);
}
}
// Not a group, so just return the single cell in an array:
return [cell];
return cells;
},
/**
......@@ -294,12 +270,15 @@ var interactions = {
*
* nodes - array of nodes
*/
allNodes: function(graph)
{
allNodes: function(graph) {
var nodes = [];
var cells = graph.model.cells;
for (var i in cells) {
nodes = nodes.concat(interactions.cellNodes(graph, cells[i], false));
var cellNodes = [];
// 'cells' is not an array but a map from ID to cell:
for (var id in cells) {
cellNodes = interactions.cellNodes(graph, cells[id], false);
nodes = nodes.concat(cellNodes);
}