Commit 678c98fa authored by Remon Huijts's avatar Remon Huijts
Browse files

Improve comments and replace dummy handlers with error reporting

parent 8d5b9609
......@@ -122991,9 +122991,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:
*
......@@ -123015,8 +123016,14 @@ var interactions = {
var triggerNodes = interactions.cellNodes(graph, cell, true);
 
clickRules.concat(hoverRules).forEach(function(rule) {
var handler = interactions.createHandler(graph, rule);
interactions.installHandler(handler, graph, triggerNodes);
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);
}
});
});
},
......@@ -123024,27 +123031,22 @@ var interactions = {
/**
* Function: createHandler
*
* Covert the interaction rule object into an event handler.
* Convert an interaction rule object into an event handler.
*
* 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.
*
* The rule object, generally directly parsed from JSON, should contain the
* following properties:
* 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
* - "colour": a valid CSS colour value (only needed for "highlight")
* 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; the
* function returns another function to apply to the graph to reset/stop the
* action (for example when the hover ends, etc.).
* 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:
*
......@@ -123052,24 +123054,15 @@ var interactions = {
* rule - interaction rule object
*/
createHandler: function(graph, rule) {
// Create a dummy fallback handler:
var dummyHandler = function() {
debug('Dummy fallback handler has been called');
return function() {};
};
if (typeof rule.action == 'undefined') {
debug("field 'action' not specified");
return dummyHandler;
throw new Error("field 'action' not specified");
}
if (!Array.isArray(rule.targets) || !rule.targets.length) {
debug('Warning: No target specified for rule');
return dummyHandler;
throw new Error('no target specified for rule');
}
var cells = interactions.getTargetCells(graph, rule.targets);
if (!Array.isArray(cells) || !cells.length) {
debug("No valid cells were specified by field 'targets'.");
return dummyHandler;
throw new Error("no valid cells specified by field 'targets'");
}
 
switch (rule.action) {
......@@ -123105,8 +123098,7 @@ var interactions = {
};
};
default:
debug('action \"' + rule.action + '\" not found');
return dummyHandler;
throw new Error('unknown action type "' + rule.action + '"');
}
},
 
......@@ -123250,8 +123242,12 @@ var interactions = {
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);
}
return nodes;
},
......@@ -123370,16 +123366,20 @@ ArrangePanel.prototype.init = function() {
};
 
/**
* For compatibility between desktop and mobile browsing we treat the 'click',
* 'hover' and 'tap' events as one and the same trigger. This class represents
* the UI for the list of interactions that is linked to a single triggering
* cell. Interactions define actions/animations that happen when the event gets
* fired. Example animations are "popup a cell" and "highlight a cell in a
* This class represents the UI for the list of interaction rules that is linked
* to the currently selected cell.
*
* The currently selected cell (which can be a group of elements) is the trigger
* cell. An interaction rule defines an action/animation that happens when the
* trigger cell is clicked or hover over. For compatibility between desktop and
* mobile browsing we treat the 'click' and 'hover' events as one and the same
* event. Example animations are "popup a cell" and "highlight a cell in a
* specific colour".
*
* We parse the interactions from the 'onclick' attribute on an element, and for
* backwards compatibility also from the 'onhover' attribute. This is the reason
* that each interaction can only be defined on one single element/group.
* A textual representation of the interaction rules is stored in the 'onclick'
* attribute on the 'value' object of the trigger cell. This is the reason that
* each interaction can only be defined on one single cell. For backwards
* compatibility we also read the 'onhover' attribute, but don't write to it.
*/
InteractionsUi = function(panel) {
this.rules = [];
......@@ -123407,23 +123407,24 @@ InteractionsUi = function(panel) {
};
 
/**
* Render all the rules that are currently in action and under that a button to
* add new rules.
* Render the UI elements for the rules that are connected to the trigger cell
* we are editing, and display a button to add new interaction rules to it.
*
* By changing any of these rules, the event will be saved and re-rendered. This
* re-rendering is caused by the `this.graph.getModel().setValue()` function,
* which always re-renders the panels on the right of the screen.
* Beware that whenever any of the rule properties are changed, automatically
* the `this.graph.getModel().setValue()` function is called, which triggers a
* re-rendering of the panels on the right of the screen. References to UI
* elements may therefore no longer be valid after each user input.
*/
InteractionsUi.prototype.render = function(div) {
var that = this;
 
// Render the rules that are already in place
// Render the UI elements for already defined interaction rules:
this.rules.forEach(function(rule) {
that.rulesDiv = rule.render(that.rulesDiv);
});
div.appendChild(this.rulesDiv);
 
// Button to add an interaction.
// Display a button to add a new interaction rule:
var name = 'Add interaction';
var addRuleButton = mxUtils.button(name, function(evt) {
var rule = new Rule(that);
......@@ -123438,8 +123439,14 @@ InteractionsUi.prototype.render = function(div) {
};
 
/**
* Get the "value" that contains all the attributes for a cell. The code to
* create this if it's not a node is copied from grapheditor code.
* Get the 'value' object of the currently selected trigger cell, so that we can
* read or write attribute values. If the cell has no such value object or just
* a string value, a proper value object is created (that code is copied from
* grapheditor code).
*
* Warning: when writing attributes to this object, you need to call
* `this.graph.getModel().setValue(this.cell, cellValue);` afterwards, because
* the value object might not yet be connected to the trigger cell.
*/
InteractionsUi.prototype.getCellValue = function() {
var value = this.graph.getModel().getValue(this.cell);
......@@ -123454,13 +123461,13 @@ InteractionsUi.prototype.getCellValue = function() {
 
/**
* Loop through the rules, get their (possibly updated) values and save that
* to the attributes of the selected cell.
* to the onclick attribute of the selected trigger cell.
*
* Attributes are saved as a list of rules. From each rule only the `attributes`
* attribute is saved. The shape of these attributes can not be changed anymore!
* It is possible to add more attributes, but by removing or changing currently
* existing attributes, you **will** break currently existing interactive
* graphs.
* Each interaction rule is stored as a JSON encoded object holding only the
* attributes of the rule. The structure and syntax of these rule attributes
* cannot be changed anymore! It is possible to add more attributes, but by
* removing or changing currently existing attributes, you **will** break
* currently existing interactive graphs.
*/
InteractionsUi.prototype.save = function() {
var value = [];
......@@ -123468,17 +123475,19 @@ InteractionsUi.prototype.save = function() {
for (var i = 0; i < this.rules.length; i++) {
value.push(this.rules[i].attributes);
}
// Get the value object that holds the onclick attribute for the trigger:
var cellValue = this.getCellValue();
// Store all interactions in the onclick attribute:
// Store all interaction rules in the onclick attribute:
cellValue.setAttribute('onclick', JSON.stringify(value));
// Clear any interactions in the onhover attribute of legacy graphs:
// Clear any interaction rules in the onhover attribute of legacy graphs:
cellValue.removeAttribute('onhover');
// Make sure the modified value object is attached to the trigger cell:
this.graph.getModel().setValue(this.cell, cellValue);
};
 
/**
* Remove a specific rule object from `this.rules` and save this InteractionsUi's
* data.
* Remove a specific interaction rule from the currently selected trigger cell
* and re-render the list of interaction rules.
*
* Parameters:
*
......@@ -123487,27 +123496,33 @@ InteractionsUi.prototype.save = function() {
InteractionsUi.prototype.removeRule = function(rule) {
var index = -1;
this.rules.forEach(function(r, i) {
if (r === rule)
if (r === rule) {
index = i;
}
});
if (index != -1)
if (index !== -1) {
this.rules.splice(index, 1);
} else {
debug('Warning: unable to remove this interaction rule:');
debug(rule);
}
this.save();
};
 
/**
* A `Rule` defines a specific action/animation that needs to happen when an
* event (click, hover or tap) is triggered.
* This class represents the UI for an interaction rule. Each interaction rule
* specifies an action/animation that needs to happen when an event (click,
* hover or tap) is fired on a trigger cell.
*
* Parameters:
*
* parentUi - The InteractionsUi object that this rule belongs to.
* parentUi - The InteractionsUi object that this rule UI is part of.
* attributes - Set of currently selected or default attributes for this rule.
* Currently existing attributes are 'action', 'colour', and
* 'targets', the latter being special (see `setTargetCell`).
*
* Note that you should not change existing attributes, because
* that will risk breaking existing interactive graphs.
* that will break existing interactive graphs.
*/
Rule = function(parentUi, attributes) {
this.parentUi = parentUi;
......@@ -123532,9 +123547,12 @@ Rule.prototype.addStyle = function() {
};
 
/**
* Renders the rule. Always renders a select with which you can choose actions.
* Depending on the selected action, it is possible that more settings can be
* chosen. These settings will also be rendered by this function.
* Renders the UI elements for this rule.
*
* A select/dropdown form element is rendered to choose the action/animation for
* this rule and other form elements are rendered to choose different settings
* for the animation. Which settings are available, depends on the chosen
* animation type.
*
* Parameters:
*
......@@ -123542,24 +123560,24 @@ Rule.prototype.addStyle = function() {
*/
Rule.prototype.render = function(div) {
this.addStyle();
// Add button to remove this rule to the ruleDiv
// Create a button to remove this rule and add it to the ruleDiv
this.ruleDiv = this.addRemoveButton();
var actionSelect = new RuleSettingSelect(
this,
'action',
['hide', 'popup', 'highlight'],
this.attributes['action']);
this.attributes.action);
this.ruleDiv = actionSelect.render(this.ruleDiv);
 
var that = this;
 
// Add action-specific settings
switch (this.attributes['action']) {
switch (this.attributes.action) {
case 'highlight':
var colourTextField = new RuleSettingColourInput(
that,
'colour',
this.attributes['colour']
this.attributes.colour
);
this.ruleDiv.appendChild(document.createElement('br'));
this.ruleDiv = colourTextField.render(this.ruleDiv);
......@@ -123599,10 +123617,11 @@ Rule.prototype.render = function(div) {
};
 
/**
* Sets the 'Targets' attribute to a list with objects that can describe cells.
* Specifies which cell this rule should target (the cell that should get
* animated) by storing the ID of that cell.
*
* Currently only works for 1 cell, but the data format is future proof for
* actions that work for several cells.
* Currently only works for one cell, but the data is stored as a list of
* targets in case we want to support rules having multiple targets.
*/
Rule.prototype.setTargetCell = function(id) {
this.setAttribute('targets', [{
......@@ -123611,12 +123630,8 @@ Rule.prototype.setTargetCell = function(id) {
};
 
/**
* Adds a button to remove a specific interaction from the selected node and
* from the interface.
*
* parameters:
*
* ruleDiv - The div that displays the interaction
* Adds a button to remove this interaction rule from the trigger cell and to
* re-render the list of interaction rules for the trigger cell.
*/
Rule.prototype.addRemoveButton = function() {
var removeAttr = document.createElement('a');
......@@ -123650,8 +123665,8 @@ Rule.prototype.addRemoveButton = function() {
};
 
/**
* Set an attribute in this rule and save the parent `EventRules` (triggers
* save & re-render).
* Set an attribute in this rule and re-render the UI that holds the list of
* interaction rules.
*
* Parameters:
*
......@@ -123666,9 +123681,10 @@ Rule.prototype.setAttribute = function(attributeName, value) {
};
 
/**
* A rule can have multiple settings: for example a highlighting rule has a
* setting for the target cell, and a setting for the highlighting colour.
* This class describes a single such setting.
* An interaction rule can have multiple settings: for example a rule that
* specifies a highlighting animation, has a setting for the target cell, and a
* setting for the highlighting colour. This class describes a single such
* setting.
*
* This class is not meant to be used as-is. Specific settings are implemented
* by derived classes. These classes must:
......@@ -61,16 +61,20 @@ ArrangePanel.prototype.init = function() {
};
/**
* For compatibility between desktop and mobile browsing we treat the 'click',
* 'hover' and 'tap' events as one and the same trigger. This class represents
* the UI for the list of interactions that is linked to a single triggering
* cell. Interactions define actions/animations that happen when the event gets
* fired. Example animations are "popup a cell" and "highlight a cell in a
* This class represents the UI for the list of interaction rules that is linked
* to the currently selected cell.
*
* The currently selected cell (which can be a group of elements) is the trigger
* cell. An interaction rule defines an action/animation that happens when the
* trigger cell is clicked or hover over. For compatibility between desktop and
* mobile browsing we treat the 'click' and 'hover' events as one and the same
* event. Example animations are "popup a cell" and "highlight a cell in a
* specific colour".
*
* We parse the interactions from the 'onclick' attribute on an element, and for
* backwards compatibility also from the 'onhover' attribute. This is the reason
* that each interaction can only be defined on one single element/group.
* A textual representation of the interaction rules is stored in the 'onclick'
* attribute on the 'value' object of the trigger cell. This is the reason that
* each interaction can only be defined on one single cell. For backwards
* compatibility we also read the 'onhover' attribute, but don't write to it.
*/
InteractionsUi = function(panel) {
this.rules = [];
......@@ -98,23 +102,24 @@ InteractionsUi = function(panel) {
};
/**
* Render all the rules that are currently in action and under that a button to
* add new rules.
* Render the UI elements for the rules that are connected to the trigger cell
* we are editing, and display a button to add new interaction rules to it.
*
* By changing any of these rules, the event will be saved and re-rendered. This
* re-rendering is caused by the `this.graph.getModel().setValue()` function,
* which always re-renders the panels on the right of the screen.
* Beware that whenever any of the rule properties are changed, automatically
* the `this.graph.getModel().setValue()` function is called, which triggers a
* re-rendering of the panels on the right of the screen. References to UI
* elements may therefore no longer be valid after each user input.
*/
InteractionsUi.prototype.render = function(div) {
var that = this;
// Render the rules that are already in place
// Render the UI elements for already defined interaction rules:
this.rules.forEach(function(rule) {
that.rulesDiv = rule.render(that.rulesDiv);
});
div.appendChild(this.rulesDiv);
// Button to add an interaction.
// Display a button to add a new interaction rule:
var name = 'Add interaction';
var addRuleButton = mxUtils.button(name, function(evt) {
var rule = new Rule(that);
......@@ -129,8 +134,14 @@ InteractionsUi.prototype.render = function(div) {
};
/**
* Get the "value" that contains all the attributes for a cell. The code to
* create this if it's not a node is copied from grapheditor code.
* Get the 'value' object of the currently selected trigger cell, so that we can
* read or write attribute values. If the cell has no such value object or just
* a string value, a proper value object is created (that code is copied from
* grapheditor code).
*
* Warning: when writing attributes to this object, you need to call
* `this.graph.getModel().setValue(this.cell, cellValue);` afterwards, because
* the value object might not yet be connected to the trigger cell.
*/
InteractionsUi.prototype.getCellValue = function() {
var value = this.graph.getModel().getValue(this.cell);
......@@ -145,13 +156,13 @@ InteractionsUi.prototype.getCellValue = function() {
/**
* Loop through the rules, get their (possibly updated) values and save that
* to the attributes of the selected cell.
* to the onclick attribute of the selected trigger cell.
*
* Attributes are saved as a list of rules. From each rule only the `attributes`
* attribute is saved. The shape of these attributes can not be changed anymore!
* It is possible to add more attributes, but by removing or changing currently
* existing attributes, you **will** break currently existing interactive
* graphs.
* Each interaction rule is stored as a JSON encoded object holding only the
* attributes of the rule. The structure and syntax of these rule attributes
* cannot be changed anymore! It is possible to add more attributes, but by
* removing or changing currently existing attributes, you **will** break
* currently existing interactive graphs.
*/
InteractionsUi.prototype.save = function() {
var value = [];
......@@ -159,17 +170,19 @@ InteractionsUi.prototype.save = function() {
for (var i = 0; i < this.rules.length; i++) {
value.push(this.rules[i].attributes);
}
// Get the value object that holds the onclick attribute for the trigger:
var cellValue = this.getCellValue();
// Store all interactions in the onclick attribute:
// Store all interaction rules in the onclick attribute:
cellValue.setAttribute('onclick', JSON.stringify(value));
// Clear any interactions in the onhover attribute of legacy graphs:
// Clear any interaction rules in the onhover attribute of legacy graphs:
cellValue.removeAttribute('onhover');
// Make sure the modified value object is attached to the trigger cell:
this.graph.getModel().setValue(this.cell, cellValue);
};
/**
* Remove a specific rule object from `this.rules` and save this InteractionsUi's
* data.
* Remove a specific interaction rule from the currently selected trigger cell
* and re-render the list of interaction rules.
*
* Parameters:
*
......@@ -178,27 +191,33 @@ InteractionsUi.prototype.save = function() {
InteractionsUi.prototype.removeRule = function(rule) {
var index = -1;
this.rules.forEach(function(r, i) {
if (r === rule)
if (r === rule) {
index = i;
}
});
if (index != -1)
if (index !== -1) {
this.rules.splice(index, 1);
} else {
debug('Warning: unable to remove this interaction rule:');
debug(rule);
}
this.save();
};
/**
* A `Rule` defines a specific action/animation that needs to happen when an
* event (click, hover or tap) is triggered.
* This class represents the UI for an interaction rule. Each interaction rule
* specifies an action/animation that needs to happen when an event (click,
* hover or tap) is fired on a trigger cell.
*
* Parameters:
*
* parentUi - The InteractionsUi object that this rule belongs to.
* parentUi - The InteractionsUi object that this rule UI is part of.
* attributes - Set of currently selected or default attributes for this rule.
* Currently existing attributes are 'action', 'colour', and
* 'targets', the latter being special (see `setTargetCell`).
*
* Note that you should not change existing attributes, because
* that will risk breaking existing interactive graphs.
* that will break existing interactive graphs.
*/
Rule = function(parentUi, attributes) {
this.parentUi = parentUi;
......@@ -223,9 +242,12 @@ Rule.prototype.addStyle = function() {
};
/**
* Renders the rule. Always renders a select with which you can choose actions.
* Depending on the selected action, it is possible that more settings can be
* chosen. These settings will also be rendered by this function.
* Renders the UI elements for this rule.
*
* A select/dropdown form element is rendered to choose the action/animation for
* this rule and other form elements are rendered to choose different settings
* for the animation. Which settings are available, depends on the chosen
* animation type.
*
* Parameters:
*
......@@ -233,24 +255,24 @@ Rule.prototype.addStyle = function() {
*/
Rule.prototype.render = function(div) {
this.addStyle();
// Add button to remove this rule to the ruleDiv
// Create a button to remove this rule and add it to the ruleDiv
this.ruleDiv = this.addRemoveButton();
var actionSelect = new RuleSettingSelect(
this,
'action',
['hide', 'popup', 'highlight'],
this.attributes['action']);
this.attributes.action);
this.ruleDiv = actionSelect.render(this.ruleDiv);
var that = this;
// Add action-specific settings
switch (this.attributes['action']) {
switch (this.attributes.action) {
case 'highlight':
var colourTextField = new RuleSettingColourInput(
that,
'colour',
this.attributes['colour']
this.attributes.colour
);
this.ruleDiv.appendChild(document.createElement('br'));
this.ruleDiv = colourTextField.render(this.ruleDiv);
......@@ -290,10 +312,11 @@ Rule.prototype.render = function(div) {
};
/**
* Sets the 'Targets' attribute to a list with objects that can describe cells.
* Specifies which cell this rule should target (the cell that should get
* animated) by storing the ID of that cell.
*
* Currently only works for 1 cell, but the data format is future proof for
* actions that work for several cells.
* Currently only works for one cell, but the data is stored as a list of
* targets in case we want to support rules having multiple targets.
*/
Rule.prototype.setTargetCell = function(id) {
this.setAttribute('targets', [{
......@@ -302,12 +325,8 @@ Rule.prototype.setTargetCell = function(id) {
};
/**
* Adds a button to remove a specific interaction from the selected node and
* from the interface.
*
* parameters:
*
* ruleDiv - The div that displays the interaction
* Adds a button to remove this interaction rule from the trigger cell and to
* re-render the list of interaction rules for the trigger cell.
*/
Rule.prototype.addRemoveButton = function() {
var removeAttr = document.createElement('a');
......@@ -341,8 +360,8 @@ Rule.prototype.addRemoveButton = function() {
};
/**
* Set an attribute in this rule and save the parent `EventRules` (triggers
* save & re-render).
* Set an attribute in this rule and re-render the UI that holds the list of
* interaction rules.
*
* Parameters:
*
......@@ -357,9 +376,10 @@ Rule.prototype.setAttribute = function(attributeName, value) {
};
/**
* A rule can have multiple settings: for example a highlighting rule has a
* setting for the target cell, and a setting for the highlighting colour.
* This class describes a single such setting.
* An interaction rule can have multiple settings: for example a rule that