/**
* HTML5 canvas visual directed graph creation tool
*
* @module raska
* @main installUsing
*/
(function (w, d) {
'use strict';
/**
* An utility that wraps the commom taks to avoid code repetition
*
* @module raska
* @submodule _helpers
*/
var $ = d.querySelector.bind(d),
_helpers = (function () {
/// A shim to allow a better animation frame timing
w.requestAnimationFrame = function () {
var _timer = null;
return w.requestAnimationFrame || w.webkitRequestAnimationFrame ||
w.mozRequestAnimationFrame || w.msRequestAnimationFrame ||
w.oRequestAnimationFrame || function (f) {
if (_timer !== null) {
w.clearTimeout(_timer);
}
_timer = w.setTimeout(f, _activeConfiguration.frameRefreshRate);
}
}();
return {
/**
* DOM manipulation related helpers
*
* @class $dom
* @module raska
* @submodule _helpers
* @static
*/
$dom: (function () {
var DOMElementHelper = function (ele) {
if (_helpers.$obj.is(ele.raw, "function")) {
ele = ele.raw();
}
return {
/**
* Get/Sets the styling of a given element
*
* @method css
* @param {string} name Style attribute
* @param {string} value Style value
* @chainable
*/
css: function (name, value) {
if (_helpers.$obj.isType(name, "string")) {
if (_helpers.$obj.isUndefined(value)) {
return ele.style[name];
}
if (value === "") {
return DOMElementHelper(ele).removeCss(name);
}
ele.style[name] = value;
} else {
for (var attr in name) {
DOMElementHelper(ele).css(attr, name[attr]);
}
}
return DOMElementHelper(ele);
},
/**
* Removes the styling attrribute of a given element
*
* @method css
* @param {string} name Style attribute
* @chainable
*/
removeCss: function (name) {
if (ele.style.removeProperty) {
ele.style.removeProperty(name);
} else {
ele.style[name] = "";
}
return DOMElementHelper(ele);
},
/**
* Gets/Sets the value for a given attribute of an HTML element
*
* @method attr
* @param {string} name Attribute name
* @param {string} value Attribute value
* @chainable
*/
attr: function (name, value) {
if (_helpers.$obj.isType(name, "string")) {
if (_helpers.$obj.isUndefined(value)) {
return ele.getAttribute(name);
}
ele.setAttribute(name, value);
} else {
for (var attr in name) {
DOMElementHelper(ele).attr(attr, name[attr]);
}
}
return DOMElementHelper(ele);
},
/**
* Retrieves the raw HTML element wraped by this helper
*
* @method raw
* @return {HTMLElement} The element itself
*/
raw: function () {
return ele;
},
/**
* Gathers UI iteraction X/Y coordinates from an event
*
* @method getXYPositionFrom
* @param {HTMLElement} container The element that contains the bounding rect we'll use to gather relative positioning data
* @param {event} evt The event we're extracting information from
* @return {x,y} Values
*/
getXYPositionFrom: function (evt) {
if (_helpers.$device.isTouch
&& _helpers.$obj.is(evt.touches.length, "number")
&& evt.touches.length > 0) {
evt = evt.touches[0];
}
var eleRect = ele.getBoundingClientRect();
return {
x: ((evt.clientX - eleRect.left) * (ele.width / eleRect.width)),
y: ((evt.clientY - eleRect.top) * (ele.height / eleRect.height))
};
},
/**
* Creates a new child element relative to this HTML element
*
* @method addChild
* @param {string} type Element node type
* @chainable
*/
addChild: function (type) {
if (_helpers.$obj.isType(type, "string")) {
var childElement = $thisDOM.create(type);
ele.appendChild(childElement.raw());
return childElement;
} else {
ele.appendChild(type);
return DOMElementHelper(type);
}
},
/**
* Get the node name for this element
*
* @method type
* @return {string} The node name for this element
*/
type: function () {
return ele.nodeName;
},
/**
* Gets the parent node for this element
*
* @method getParent
* @chainable
*/
getParent: function () {
return DOMElementHelper((ele.parentElement) ? ele.parentElement : ele.parentNode);
},
/**
* Adds a sibling element
*
* @method addSibling
* @param {string} type Element node type
* @chainable
*/
addSibling: function (type) {
return DOMElementHelper(ele).getParent().addChild(type);
},
/**
* Gets\Sets the innerHTML content for the HTML element
*
* @method html
* @param {string} content
* @chainable
*/
html: function (content) {
if (_helpers.$obj.isType(content, "string")) {
ele.innerHTML = content;
return DOMElementHelper(ele);
}
return ele.innerHTML;
},
/**
* Gets\Sets the innerText content for the HTML element
*
* @method html
* @param {string} content
* @chainable
*/
text: function (content) {
if (_helpers.$obj.isType(content, "string")) {
ele.innerText = content;
return DOMElementHelper(ele);
}
return ele.innerText;
},
/**
* Registers a delegate to a given element event
*
* @method on
* @param {HTMLElement} targetElement The element that we're interested in
* @param {String} iteractionType The event name
* @param {Function} triggerWrapper The delegate
* @chainable
*/
on: function (iteractionType, triggerWrapper) {
// modern browsers including IE9+
if (w.addEventListener) { ele.addEventListener(iteractionType, triggerWrapper, false); }
// IE8 and below
else if (w.attachEvent) { ele.attachEvent("on" + iteractionType, triggerWrapper); }
else { ele["on" + iteractionType] = triggerWrapper; }
return DOMElementHelper(ele);
},
/**
* Selects the first occurent of child elements that matches the selector
*
* @method child
* @param {string} selector Element's selector
* @return {DOMElementHelper} The element wraped in a helper object
*/
first: function (selector) {
//https://developer.mozilla.org/en-US/docs/Web/CSS/:scope#Browser_compatibility
var result = ele.querySelectorAll(":scope > " + selector);
if (_helpers.$obj.isType(result, "nodelist")) {
return DOMElementHelper(result[0]);
}
return null;
}
};
},
$thisDOM = {
/**
* Creates and returns an element
*
* @method create
* @param {string} type Element node type
* @param {HTMLElement} parent Element's parent node
* @return {DOMElementHelper} The element wraped in a helper object
*/
create: function (type, parent) {
var newElement = d.createElement(type);
newElement.id = _helpers.$obj.generateId();
if (_helpers.$obj.isValid(parent)) {
DOMElementHelper(parent).addChild(newElement);
}
return DOMElementHelper(newElement);
},
/**
* Gathers an element using a given selector query
*
* @method get
* @param {string} selector Element's selector
* @return {DOMElementHelper} The element wraped in a helper object
*/
get: function (selector) {
var element = _helpers.$obj.isType(selector, "string") ? $(selector) : selector;
return DOMElementHelper(element);
},
/**
* Gathers an element using a given id
*
* @method getById
* @param {string} id Element's id
* @return {DOMElementHelper} The element wraped in a helper object
*/
getById: function (id) {
return DOMElementHelper($("#" + id));
}
};
return $thisDOM;
})(),
/**
* Device related helpers
*
* @class $device
* @module raska
* @submodule _helpers
* @static
*/
$device: (function () {
var _isTouch = (('ontouchstart' in w)
|| (navigator.MaxTouchPoints > 0)
|| (navigator.msMaxTouchPoints > 0)),
_this = {
/**
* Whether or not to the current devide is touchscreen
*
* @property isTouch
* @type Bool
*/
isTouch: _isTouch,
/**
* Gathers UI iteraction X/Y coordinates from an event
*
* @method gathersXYPositionFrom
* @param {HTMLElement} container The element that contains the bounding rect we'll use to gather relative positioning data
* @param {event} evt The event we're extracting information from
* @return {x,y} Values
*/
gathersXYPositionFrom: function (container, evt) {
return _helpers.$dom.get(container).getXYPositionFrom(evt);
},
/**
* Registers a delegate to a given element event
*
* @method on
* @param {HTMLElement} targetElement The element that we're interested in
* @param {String} iteractionType The event name
* @param {Function} triggerWrapper The delegate
* @chainable
*/
on: function (targetElement, iteractionType, triggerWrapper) {
_helpers.$dom.get(targetElement).on(iteractionType, triggerWrapper);
return _this;
}
};
return _this;
})(),
/**
* Outputs messages to the 'console'
*
* @class $log
* @module raska
* @submodule _helpers
* @static
*/
$log: {
/**
* Whether or not to actually log the messages (should be 'false' in production code)
*
* @property active
* @type Bool
* @default false
*/
active: false,
/**
* Prints an informational message to the console
*
* @method info
* @param {String} msg The message to be shown
* @param {Any} o Any extra message data
*/
info: function (msg, o) {
if (this.active === true) {
console.info(msg, o);
}
}
},
/**
* Handles commom prototype element' tasks
*
* @class $obj
* @module raska
* @submodule _helpers
* @static
*/
$obj: (function () {
var _this;
function s4() {
return Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
}
function normalizeChain(elementRoot, resultFull) {
var item, links;
for (var i = 0; i < elementRoot.length; i++) {
item = elementRoot[i];
if ((item.graphNode === true) && (((links = item.getLinksFrom()) === null) || (links.length === 0))
&& (((links = item.getLinksTo()) === null) || (links.length === 0))) {
throw new _defaultConfigurations.errors.elementDoesNotHaveALink(item.name);
}
if (item.isSerializable()) {
/// Adds the normalized item
resultFull.push(item.normalize());
/// Check for child elements
normalizeChain(item.getChildElements(), resultFull);
}
}
}
return _this = {
/**
* Executes a given delegate for each item in the collection
*
* @method forEach
* @param {Array} arr The array that need to be enumerated
* @param {Delegate} what What to do to a given item (obj item, number index)
* @return Array of data acquired during array enumaration
*/
forEach: function (arr, what) {
var result = [];
if (_this.isArray(arr)) {
for (var i = 0; i < arr.length; i++) {
result.push(what(arr[i], i));
}
}
return result;
},
/**
* Whether or not a given object type is of the type you expect (typeof)
*
* @method is
* @param {Object} obj The object we whant to know about
* @param {String} what The string representing the name of the type
* @return {Bool} Whether or not the object matches the specified type
*/
is: function (obj, what) {
return typeof obj === what;
},
/**
* Whether or not a given object type is of the type you expect (constructor call)
*
* @method isType
* @param {Object} obj The object we whant to know about
* @param {String} typeName The string representing the name of the type
* @return {Bool} Whether or not the object matches the specified type
*/
isType: function (obj, typeName) {
return this.isValid(obj) && Object.prototype.toString.call(obj).toLowerCase() === "[object " + typeName.toLowerCase() + "]";
},
/**
* Generates a pseudo-random Id
*
* @method generateId
* @return {String} A pseudo-random Id
*/
generateId: function () {
return "__" + s4() + s4() + s4() + s4() + s4() + s4() + s4() + s4();
},
/**
* Whether or not a given object type is valid to be handled
*
* @method isValid
* @param {Object} obj The object we whant to know if it's valid to be handled
* @return {Bool} Whether or not the object is valid to be handled
*/
isValid: function (obj) {
return !this.is(obj, 'undefined') && obj !== null;
},
/**
* Whether or not a given object is an Array
*
* @method isArray
* @param {Object} obj The object we whant to know is it's valid to be handled
* @return {Bool} Whether or not the object is valid to be handled
*/
isArray: function (obj) {
return this.isValid(obj) && this.isType(obj, "Array");
},
/**
* Whether or not a given object type is undefined
*
* @method isUndefined
* @param {Object} obj The object we whant to know if it's undefined
* @return {Bool} Whether or not the object is undefined
*/
isUndefined: function (obj) {
return this.is(obj, 'undefined');
},
/**
* Serializes a given _basicElement array to a JSON string
*
* @method deconstruct
* @param {_basicElement[]} basicElementArray The array of _basicElement we're going to work with
* @return {string} The corresponding string
*/
deconstruct: function (elements) {
var resultFull = [];
if (_helpers.$obj.isArray(elements)) {
normalizeChain(elements, resultFull);
}
return JSON.stringify(resultFull);
},
/**
* Tries to recreate a previously serialized object into a new raska instance
*
* @method recreate
* @param {JSON} jsonElement The JSON representation of a raska object
* @return {_basicElement} The recreated element or null if the element type is invalid
*/
recreate: function (jsonElement) {
if (this.isValid(jsonElement.type) && (jsonElement.type in _elementTypes)) {
var resultElement = this.extend(new _defaultConfigurations[jsonElement.type], jsonElement);
resultElement.getType = function () { return jsonElement.type; }
return resultElement;
}
return null;
},
/**
* Recreate all links of the object
*
* @method recreate
*/
recreateLinks: function (targetElement, baseElement, findElementDelegate) {
/// Copy data back as it should be
this.forEach(this.forEach(baseElement.linksTo, findElementDelegate),
targetElement.addLinkTo.bind(targetElement));
this.forEach(this.forEach(baseElement.childElements, findElementDelegate),
targetElement.addChild.bind(targetElement));
if (baseElement.parent !== null) {
findElementDelegate(baseElement.parent).addChild(targetElement);
}
},
/**
* Extends any object using a base template object as reference
*
* @method extend
* @param {Object} baseObject The object we whant to copy from
* @param {Object} impl The object with the data we want use
* @param {Bool} addNewMembers Whether or not we allow new members on the 'impl' object to be used in the result
* @return {Object} Extended object
*/
extend: function (baseObject, impl, addNewMembers) {
var result = {}, element = null;
if (this.isUndefined(impl)) {
for (element in baseObject) {
result[element] = baseObject[element];
}
} else {
if (addNewMembers === true) { result = impl; }
for (element in baseObject) {
if (!result.hasOwnProperty(element)) {
result[element] = impl.hasOwnProperty(element) ? impl[element] : baseObject[element];
}
}
}
return result;
}
};
})()
};
})(),
/**
* Gathers all elements being ploted to the canvas and organizes it as a directed graph JSON
*
* @private
* @class _graphNodeInfo
* @param {_basicElement} nodeElement Element whose dependencies we're about to transpose searching for links
* @param {_graphNodeInfo} parentNode The parent no to this element
* @param {array} links The linked data to this element
*/
_graphNodeInfo = function (nodeElement, parentNode, links) {
var _parent = parentNode || null,
_links = [];
/**
* The element wraped by this node
*
* @property element
* @type _basicElement
* @final
*/
this.element = nodeElement;
/**
* Whether or not this node has a parent element
*
* @property hasParent
* @type bool
* @final
*/
this.hasParent = function (node) {
return ((_parent !== null)
&& ((_parent.element === node) || (_parent.getName() === node.name) || _parent.hasParent(node)));
};
/**
* Tries to find a parent node compatible with the element passed as parameter
*
* @method getParentGraphNodeFor
* @param {_basicElement} bElement Element wraped by a parent node
* @type _graphNodeInfo
*/
this.getParentGraphNodeFor = function (bElement) {
if ((this.element === bElement) || (this.getName() === bElement.name)) {
return this;
}
if (_parent !== null) {
return this.getParent(bElement);
}
return null;
};
this.getName = function () {
return nodeElement.name;
};
this.getParent = function () {
return _parent;
};
this.getLinks = function () {
return _links;
};
for (var j = 0; j < links.length; j++) {
if (links[j].graphNode === true) {
if (this.hasParent(links[j])) {
/// We've reached a recursive relation. In this case, simply gathers the reference to the parent and
/// link it here for better navegability between nodes dependecies
_helpers.$log.info("ingored parent node", this.getParentGraphNodeFor(links[j]));
} else {
_links.push(new _graphNodeInfo(links[j], this, links[j].getLinksTo()));
}
}
}
},
/**
* The valid position types for a Raskel element
*
* @private
* @property _positionTypes
* @static
* @final
*/
_positionTypes = {
/**
* Element position will be relative to its parent (if any)
*
* @property relative
* @type Number
* @default 50
* @readOnly
*/
relative: 0,
/**
* Element position will be absolute and will have no regard about its parent position whatsoever
*
* @property relative
* @type Number
* @default 50
* @readOnly
*/
absolute: 1
},
_elementTypes = {
basic: "basic",
html: "htmlElement",
arrow: "arrow",
circle: "circle",
square: "square",
label: "label",
triangle: "triangle"
},
/**
* The basic shape of an Raska element
*
* @private
* @class _basicElement
* @module raska
*/
_basicElement = function () {
var
/**
* A simple canvas used to perform a pixel model hit test agains this element
*
* @property $hitTestCanvas
* @type {CanvasRenderingContext2D}
* @protected
*/
$hitTestCanvas = _helpers.$dom.create("canvas").attr({ "width": "1", "height": "1" }),
$hitTestContext = $hitTestCanvas.raw().getContext("2d"),
/**
* A simple helper function to be used whenever this instance gets disable (if ever)
*
* @private
* @method $disabled
*/
$disabled = function () { },
$wasDisabled = false,
/**
* Points to a parent Raska element (if any)
*
* @private
* @property $parent
* @default null
*/
$parent = null,
/**
* Points to a Raska element [as an arrow] (if any) related as a dependency from this node
*
* @private
* @property $linksTo
* @type _basicElement
* @default []
*/
$linksTo = [],
/**
* Points to a Raska element [as an arrow] (if any) that depends on this instance
*
* @private
* @property $linksFrom
* @type _basicElement
* @default []
*/
$linksFrom = [],
/**
* Holds the reference to the delegate triggered before a link gets removed from an element
* @property $beforeRemoveLinkFrom
* @type function
* @default null
*/
$beforeRemoveLinkFrom = null,
/**
* Holds the reference to the delegate triggered before a link gets removed to an element
* @property $beforeRemoveLinkTo
* @type function
* @default null
*/
$beforeRemoveLinkTo = null,
/**
* Points to series of child Raska element (if any) that, usually, are contained inside this node
*
* @private
* @property $childElements
* @type _basicElement
* @default []
*/
$childElements = [], $this, $disabledStateSubscribers = [];
/**
* Clears all links that point to and from this Raska element
*
* @private
* @method clearLinksFrom
* @param {Object} source Object that defines the source of the link removal (start node)
* @param {Function} each Delegates that handle the process of removing the references
* @return {Array} Empty array
*/
function clearLinksFrom(source, each) {
for (var i = 0; i < source.length; i++) {
each(source[i]);
}
return [];
}
return $this = {
/**
* Just an empty attribute bag where you can save extra stuff that you need/want to serialize
*
* @property attr
* @default "{}"
* @type Object
*/
attr: {},
/**
* The element name
*
* @property name
* @default "anonymous"
* @type String
*/
name: "anonymous",
/**
* The element type
*
* @property getType
* @default _elementTypes.basic
* @type _elementTypes
*/
getType: function () { return _elementTypes.basic; },
/**
* Wheter or not this element is part of the graph as a node
*
* @property graphNode
* @default true
* @type Bool
*/
graphNode: true,
/**
* Allows to better check whether or not a new link can be created beteen two elements
*
* @method canLink
* @param {_basicElement} from Element that will be linked as a source
* @param {_basicElement} to Element that will be linked as a destination
* @return {Bool} Whether or not the link can be created
*/
canLink: function (from, to) {
if (from === this) {
return _helpers.$obj.isValid(to) && (to.canReach(this) === false);
}
return true;
},
/**
* Clears all references and child elements from this instance
*
* @method disable
* @chainable
*/
disable: function () {
$wasDisabled = true;
_helpers.$obj.forEach($disabledStateSubscribers, function (target) {
if (_helpers.$obj.is(target, "function")) {
target(this);
} else {
target.elementDisabledNotification(this);
}
}.bind(this));
$disabledStateSubscribers.length = 0; /// Let us free some space
this.clearAllLinks();
_helpers.$obj.forEach($childElements, function (ele) { ele.disable(); });
$childElements.length = 0;
this.drawTo = $disabled;
return this;
},
/**
* Notifies a given element whenever (if ever) this element gets disabled
*
* @method notifyDisableStateOn
* @chainable
*/
notifyDisableStateOn: function (target) {
if (($disabledStateSubscribers.indexOf(target) === -1)
&& (_helpers.$obj.isValid(target.elementDisabledNotification) || _helpers.$obj.is(target, "function"))) {
$disabledStateSubscribers.push(target);
}
return this;
},
/**
* Handles element disabling notifications
*
* @method elementDisabledNotification
* @chainable
*/
elementDisabledNotification: function (element) {
return this;
},
/**
* Whether or not this element can reach another element via a linked relation
*
* @method canReach
* @return {Bool} Whether or not a link exists
*/
canReach: function (node) {
if ($linksTo.length > 0) {
for (var i = 0; i < $linksTo.length; i++) {
if ($linksTo[i].canReach(node)) {
return true;
}
}
}
return (this === node);
},
/**
* Is this element passive of being linked at all?
*
* @method isLinkable
* @return {Bool} Whether or not a link can be created either from or to this element
*/
isLinkable: function () {
return !$wasDisabled;
},
/**
* Is this element passive of being serialized at all?
*
* @method isSerializable
* @return {Bool} Whether or not this element is suposed to be serialized
*/
isSerializable: function () {
return !$wasDisabled;
},
/**
* Creates a link between this and another element, using the later as a dependency of this instance
*
* @method addLinkTo
* @param {_basicElement} element Element that will be linked as a destination from this element node
* @return {Bool} Whether or not the link was added
*/
addLinkTo: function (element) {
if (_helpers.$obj.isValid(element.isLinkable) && element.isLinkable()
&& this.isLinkable() && ($linksTo.indexOf(element) === -1)
&& (element !== this) && this.canLink(this, element)) {
$linksTo.push(element);
element.addLinkFrom(this);
return true;
}
return false;
},
/**
* Links this element to another Raska element as this instance being the target node
*
* @method addLinkFrom
* @param {_basicElement} element Element that will be linked as a source from this element node
* @return {Bool} Whether or not the link was added
*/
addLinkFrom: function (element) {
if (_helpers.$obj.isValid(element.isLinkable) && element.isLinkable()
&& this.isLinkable() && ($linksFrom.indexOf(element) === -1)
&& (element !== this) && this.canLink(element, this)) {
$linksFrom.push(element);
element.addLinkTo(this);
return true;
}
return false;
},
/**
* Removes the dependency from this element to another Raska element
*
* @method removeLinkTo
* @param {_basicElement} element Element that will have its link to this element removed
* @return {_basicElement} Updated Raska element reference
* @chainable
*/
removeLinkTo: function (element) {
if ($linksTo.indexOf(element) > -1) {
$linksTo.splice($linksTo.indexOf(element), 1);
if (_helpers.$obj.isType($beforeRemoveLinkFrom, "function") === true) {
$beforeRemoveLinkFrom.call(this, element);
}
element.removeLinkFrom(this);
}
return this;
},
/**
* Event triggered before a link gets removed from this element
*
* @method beforeRemoveLinkFrom
* @param {function} doWhat A delegate with the 'this' poiting to this instance and the first
* paramenter being the reference to the element which is being remove as a link to this instance
* @chainable
*/
beforeRemoveLinkFrom: function (doWhat) {
$beforeRemoveLinkFrom = doWhat;
return this;
},
/**
* Transposes the getter data to a field attached to this instance
*
* @method normalize
* @chainable
*/
normalize: function () {
var n = function (item) { return item.name; };
var normalized = _helpers.$obj.extend(this, {
linksTo: _helpers.$obj.forEach(this.getLinksTo(), n),
childElements: _helpers.$obj.forEach(this.getChildElements(), n),
parent: _helpers.$obj.isValid($parent) ? $parent.name : null,
type: this.getType()
}, true);
delete normalized.on; /// We don't want/need this
return normalized;
},
/**
* Gathers all elements that this instance depends on
*
* @method getLinksTo
* @return {Array} All elements that this node references TO
*/
getLinksTo: function () {
return $linksTo;
},
/**
* Gathers all dependecies/linked elements related to this instance
*
* @method getLinksFrom
* @return {Array} All elements that this node references from
*/
getLinksFrom: function () {
return $linksFrom;
},
/**
* Removes the link to this element from another Raska element
*
* @method removeLinkFrom
* @param {_basicElement} element Element that will have its link from this element removed
* @return {_basicElement} Updated Raska element reference
* @chainable
*/
removeLinkFrom: function (element) {
if ($linksFrom.indexOf(element) > -1) {
$linksFrom.splice($linksFrom.indexOf(element), 1);
if (_helpers.$obj.isType($beforeRemoveLinkTo, "function") === true) {
$beforeRemoveLinkTo.call(this, element);
}
element.removeLinkTo(this);
}
return this;
},
/**
* Event triggered before a link gets removed to this element
*
* @method beforeRemoveLinkTo
* @param {function} doWhat A delegate with the 'this' poiting to this instance and the first
* paramenter being the reference to the element which is being remove as a link from this instance
* @chainable
*/
beforeRemoveLinkTo: function (doWhat) {
$beforeRemoveLinkTo = doWhat;
return this;
},
/**
* Removes every and each link that references this element
*
* @method clearAllLinks
* @return {_basicElement} Updated Raska element reference
* @chainable
*/
clearAllLinks: function () {
$linksTo = clearLinksFrom($linksTo, function (e) { e.removeLinkFrom(this); }.bind(this));
$linksFrom = clearLinksFrom($linksFrom, function (e) { e.removeLinkTo(this); }.bind(this));
return this;
},
/**
* Gathers all child elements from this node
*
* @method getChildElements
* @return {Array} All child elements
*/
getChildElements: function () {
return $childElements;
},
/**
* Adds a new child element to this node
*
* @method addChild
* @param {_basicElement} child Element that will be added to the child array into this element
* @return {_basicElement} Updated Raska element reference
* @chainable
*/
addChild: function (child) {
child.setParent(this);
$childElements.push(child);
return this;
},
/**
* Get the parent node to this element
*
* @method getParent
* @return {_basicElement} Reference to de parent node (null if none)
*/
getParent: function () {
return $parent;
},
/**
* Retrieves the element boundaries information
*
* @method getBoundaries
* @return {Object} Data regarding x, y, minX and minY from this element
*/
getBoundaries: function () {
return {
x: this.getWidth(),
y: this.getHeight(),
minX: 0,
minY: 0
};
},
/**
* Sets the parent node to this element
*
* @method setParent
* @param {_basicElement} parent Element that will be added as a parent to this element
* @return {_basicElement} Updated Raska element reference
* @chainable
*/
setParent: function (parent) {
$parent = parent;
return this;
},
/**
* Details regarding this element' border
*
* @attribute border
*/
border: {
/**
* Element border color
*
* @property color
* @type String
* @default null
*/
color: null,
/**
* Whether or not a border should be rendered
*
* @property active
* @type Bool
* @default false
*/
active: false,
/**
* Border width
*
* @property width
* @type Number
* @default 0
*/
width: 0
},
/**
* Element position type
*
* @property position
* @type _positionTypes
* @default _positionTypes.relative
*/
position: _positionTypes.relative,
/**
* Element x position
*
* @property x
* @type Number
* @default 50
*/
x: 50,
/**
* Element y position
*
* @property y
* @type Number
* @default 50
*/
y: 50,
/**
* Gathers adjusted x/y coordinates for the currente element taking into consideration
* the type of positining set to it and wheter or not a parent node is present
*
* @method getAdjustedCoordinates
* @return {x:Number,y:Number} Adjusted coordinates
*/
getAdjustedCoordinates: function () {
if ((this.position === _positionTypes.relative) && ($parent !== null)) {
var adjustedParent = $parent.getAdjustedCoordinates();
return {
x: this.x + adjustedParent.x,
y: this.y + adjustedParent.y
};
}
return { x: this.x, y: this.y };
},
/**
* Whether or not this element handle event interactions
*
* @property canHandleEvents
* @type bool
* @default false
*/
canHandleEvents: function () { return true; },
on: (function () {
var __clickDelegates = [],
__releaseDelegates = [];
function triggerDelegatesUsing(x, y, ele, evt, arr) {
_helpers.$obj.forEach(arr, function (el) {
el(x, y, ele, evt);
});
}
return {
/**
* Triggered whenever a click iteraction occurs within the boundaries of this element.
* - This event is only supported on selected elements. Check for the *canHandleEvents*
* property value before relying on this delegate
*
* @function click
* @param {number} x Element's current X position
* @param {number} y Element's current Y position
* @param {_basicElement} ele The element that was clicked
* @param {event} evt Event that triggered the delegate
* @chainable
*/
click: function (x, y, ele, evt) {
if (_helpers.$obj.isType(x, "number") === true) {
var foundInner = false;
if ($childElements.length > 0) {
var parentAdjustedPosition = ele.getAdjustedCoordinates();
_helpers.$obj.forEach($childElements, function (el) {
var newPosition = {
x: x - parentAdjustedPosition.x,
y: y - parentAdjustedPosition.y
}
if (el.canHandleEvents() && el.existsIn(newPosition.x, newPosition.y)) {
el.on.click(newPosition.x, newPosition.y, el, evt);
foundInner = true;
}
});
}
if (foundInner === false) {
triggerDelegatesUsing(x, y, ele, evt, __clickDelegates);
}
} else if (_helpers.$obj.isType(x, "function") === true) {
__clickDelegates.push(x);
}
return $this;
},
/**
* Triggered whenever a 'clickUp' iteraction occurs within the boundaries of this element.
* - This event is only supported on selected elements. Check for the *canHandleEvents*
* property value before relying on this delegate
*
* @function release
* @param {number} x Element's current X position
* @param {number} y Element's current Y position
* @param {_basicElement} ele The element that was clicked
* @param {event} evt Event that triggered the delegate
* @chainable
*/
release: function (x, y, ele, evt) {
if (_helpers.$obj.isType(x, "number") === true) {
var foundInner = false;
if ($childElements.length > 0) {
var parentAdjustedPosition = ele.getAdjustedCoordinates();
_helpers.$obj.forEach($childElements, function (el) {
var newPosition = {
x: x - parentAdjustedPosition.x,
y: y - parentAdjustedPosition.y
}
if (el.canHandleEvents() && el.existsIn(newPosition.x, newPosition.y)) {
el.on.release(newPosition.x, newPosition.y, el, evt);
foundInner = true;
}
});
}
if (foundInner === false) {
triggerDelegatesUsing(x, y, ele, evt, __releaseDelegates);
}
} else if (_helpers.$obj.isType(x, "function") === true) {
__releaseDelegates.push(x);
}
return $this;
}
};
})(),
/**
* Whether or not this element existis withing the boudaries of
* the given x/y coordinates
*
* @method existsIn
* @param {Number} x X Coordinate
* @param {Number} y Y Coordinate
* @return {Bool} If this element is contained within the X/Y coordinates
*/
existsIn: function (x, y) {
$hitTestContext.setTransform(1, 0, 0, 1, -x, -y);
this.drawTo($hitTestCanvas, $hitTestContext);
var hit = false;
try { hit = $hitTestContext.getImageData(0, 0, 1, 1).data[3] > 1; }
catch (e) { }
$hitTestContext.setTransform(1, 0, 0, 1, 0, 0);
$hitTestContext.clearRect(0, 0, 2, 2);
return hit;
},
/**
* [ABSTRACT] Adjusts the position of the current element taking in consideration it's parent
* positioning constraints
*
* @method adjustPosition
* @param {Number} newX X position
* @param {Number} newY Y position
* @chainable
*/
adjustPosition: function (newX, newY) {
console.error(_defaultConfigurations.errors.notImplementedException);
throw _defaultConfigurations.errors.notImplementedException;
},
/**
* [ABSTRACT] Sets the current width for this element
*
* @method setWidth
* @param {Number} newWidth The width for this element
* @chainable
* @throws {_defaultConfigurations.errors.notImplementedException} Not implemented
*/
setWidth: function (newWidth) {
console.error(_defaultConfigurations.errors.notImplementedException);
throw _defaultConfigurations.errors.notImplementedException;
},
/**
* [ABSTRACT] Gets the current width for this element
*
* @method getWidth
* @return {Number} The width of this element
* @throws {_defaultConfigurations.errors.notImplementedException} Not implemented
*/
getWidth: function () {
console.error(_defaultConfigurations.errors.notImplementedException);
throw _defaultConfigurations.errors.notImplementedException;
},
getAdjustedWidth: function () { return this.getWidth(); },
/**
* [ABSTRACT] Sets the current Height for this element
*
* @method setHeight
* @param {Number} newHeight The height for this element
* @chainable
* @throws {_defaultConfigurations.errors.notImplementedException} Not implemented
*/
setHeight: function (newHeight) {
console.error(_defaultConfigurations.errors.notImplementedException);
throw _defaultConfigurations.errors.notImplementedException;
},
/**
* [ABSTRACT] Gets the current Height for this element
*
* @method getHeight
* @return {Number} The Height of this element
* @throws {_defaultConfigurations.errors.notImplementedException} Not implemented
*/
getHeight: function () {
console.error(_defaultConfigurations.errors.notImplementedException);
throw _defaultConfigurations.errors.notImplementedException;
},
getAdjustedHeight: function () { return this.getHeight(); },
/**
* [ABSTRACT] Whether or not this element existis withing the boudaries of
* the given x/y coordinates
*
* @method existsIn
* @param {HTMLElement|Node} The Canvas element
* @param {Node} tdContext Canvas' 2d context
* @throws {_defaultConfigurations.errors.notImplementedException} Not implemented
*/
drawTo: function (canvasElement, tdContext) {
console.error(_defaultConfigurations.errors.notImplementedException);
throw _defaultConfigurations.errors.notImplementedException;
}
};
},
/**
* A utility module to control complex canvas' (HTML) iteraction
*
* @private
* @module raska
* @submodule _canvasController
* @readOnly
*/
_canvasController = (function () {
var _inFullscreen = false,
_defaultValues = {
container: {
"width": 0,
"height": 0,
"position": 0,
"z-index": 0,
"left": 0,
"top": 0
},
canvas: {
"width": 0,
"height": 0
}
};
return {
/**
* Toggles canvas in/out fullscreen mode
*
* @method toggleFullScreen
* @param {HTMLElement|Node} The Canvas element
* @param {string} strContainerId The id for the canvas toolbar (or undefined if nothing needs to be done there)
*/
toggleFullScreen: function (canvas, strContainerId) {
var canvasElementContainer = _helpers.$dom.getById(_activeConfiguration.targetCanvasContainerId),
canvasElement = _helpers.$dom.get(canvas),
bottomPadding = strContainerId ? 40/*the estimated Height size for the toolbox*/ : 0;
if (_inFullscreen === false) {
/// Recovery mode for the container
for (var attr in _defaultValues.container) {
_defaultValues.container[attr] = canvasElementContainer.css(attr);
}
canvasElementContainer.css({
"width": d.body.clientWidth,
"height": d.body.clientHeight - bottomPadding,
"position": "fixed",
"z-index": 1000,
"background-color": "white",
"left": 0,
"top": 0
});
/// Recovery mode for the canvas
for (var attr in _defaultValues.canvas) {
_defaultValues.canvas[attr] = canvasElement.attr(attr);
}
canvasElement.attr({ "width": d.body.clientWidth, "height": d.body.clientHeight - bottomPadding });
if (_helpers.$obj.isValid(strContainerId)) {
_helpers.$dom.getById(strContainerId)
.first("button")
.html("<span class='glyphicon glyphicon-resize-small'></span> Back to normal");
}
_inFullscreen = true;
} else {
for (var attr in _defaultValues.container) {
canvasElementContainer.css(attr, _defaultValues.container[attr]);
}
for (var attr in _defaultValues.canvas) {
canvasElement.attr(attr, _defaultValues.canvas[attr]);
}
if (_helpers.$obj.isValid(strContainerId)) {
_helpers.$dom.getById(strContainerId)
.first("button")
.html("<span class='glyphicon glyphicon-resize-full'></span> Fullscreen");
}
_inFullscreen = false;
}
}
};
})(),
/**
* A utility container that holds default instance information for the Raska library
*
* @private
* @module raska
* @submodule _defaultConfigurations
* @readOnly
*/
_defaultConfigurations = {
/**
* Raska's default configuration data
*
* @property library
* @static
* @final
*/
library: {
readonly: false,
frameRefreshRate: 30,
targetCanvasId: "",
targetCanvasContainerId: "____raska" + _helpers.$obj.generateId(),
toolboxButtons: [
{
/// THIS WILL BE SET AUTOMATICALLY WHEN THE BUTTON GETS RENDERED
id: "", /// THIS WILL BE SET AUTOMATICALLY WHEN THE BUTTON GETS RENDERED
/// THIS WILL BE SET AUTOMATICALLY WHEN THE BUTTON GETS RENDERED
name: "fullscreen",
enabled: true,
onclick: function (canvas) {
_canvasController.toggleFullScreen(canvas, this.id);
},
template: "<button class='btn btn-primary btn-sm'><span class='glyphicon glyphicon-resize-full'></span> Fullscreen</button>"
}
]
},
/**
* Raska's exception types
*
* @property errors
* @static
* @final
*/
errors: {
notImplementedException: {
message: "Not implemented",
code: 0
},
nullParameterException: function (parameterName) {
if ((typeof parameterName === 'undefined') || (parameterName === null)) {
throw new this.nullParameterException("parameterName");
}
this.message = "Parameter can't be null";
this.parameterName = parameterName;
this.code = 1;
},
elementDoesNotHaveALink: function (elementName) {
if ((typeof elementName === 'undefined') || (elementName === null)) {
throw new this.nullParameterException("elementName");
}
return {
message: "Element '" + elementName + "' doesn't have a valid linked sibling",
elementName: elementName,
code: 2
};
},
itemNotFoundException: function (elementName) {
if ((typeof elementName === 'undefined') || (elementName === null)) {
throw new this.nullParameterException("elementName");
}
return {
message: "Element '" + elementName + "' wasn't found",
elementName: elementName,
code: 3
};
}
},
/**
* Creates a label
*
* @class label
* @extends _basicElement
*/
label: function () {
return _helpers.$obj.extend(new _basicElement(), {
name: "label" + _helpers.$obj.generateId(),
graphNode: false,
getType: function () { return _elementTypes.label; },
canLink: function () { return false; },
isLinkable: function () { return false; },
text: "",
color: "gray",
x: 0,
y: 0,
border: { color: "white", active: true, width: 2 },
font: { family: "Arial", size: "12px", decoration: "" },
getWidth: function () {
return this.text.length * 5;
},
getHeight: function () {
return this.font.size;
},
drawTo: function (canvas, context) {
var coordinates = this.getAdjustedCoordinates();
context.save();
context.font = (this.font.decoration || "") + " " + this.font.size + " " + this.font.family;
if (this.border.active === true) {
context.lineJoin = "round";
context.lineWidth = this.border.width;
context.strokeStyle = this.border.color;
context.strokeText(this.text, coordinates.x, coordinates.y);
}
context.fillStyle = this.color;
context.fillText(this.text, coordinates.x, coordinates.y);
context.restore();
},
existsIn: function (x, y) {
/// Efective as a non-draggable element
return false;
}
}, true);
},
/**
* Creates a square
*
* @class square
* @extends _basicElement
*/
square: function (desiredDimensions) {
var _dimensions = desiredDimensions || {
width: 50,
height: 50
}, ctx = null;
return _helpers.$obj.extend(new _basicElement(), {
name: "square" + _helpers.$obj.generateId(),
getType: function () { return _elementTypes.square; },
border: { color: "gray", active: true, width: 2 },
fillColor: "silver",
dimensions: _dimensions,
globalAlpha: 1,
getWidth: function () {
return this.dimensions.width;
},
getHeight: function () {
return this.dimensions.height;
},
drawTo: function (canvas, context) {
var coordinates = this.getAdjustedCoordinates();
context.beginPath();
context.rect(coordinates.x, coordinates.y, this.dimensions.width, this.dimensions.height);
context.fillStyle = this.fillColor;
if (this.border.active === true) {
context.lineWidth = this.border.width;
context.strokeStyle = this.border.color;
}
context.globalAlpha = this.globalAlpha;
context.fill();
context.stroke();
},
existsIn: function (x, y) {
return ((x >= this.x) && (x <= this.x + this.dimensions.width)
&& (y >= this.y) && (y <= this.y + this.dimensions.height));
},
adjustPosition: function (newX, newY) {
var $parent = this.getParent();
if ((this.position === _positionTypes.relative) && ($parent !== null)) {
var adjustedParent = $parent.getAdjustedCoordinates();
var w = $parent.getWidth() / 2;
var h = $parent.getHeight() / 2;
var diffX = (newX - adjustedParent.x) - w;
var diffH = (newY - adjustedParent.y) - h;
this.x = diffX < 0 ? Math.max(diffX, w * -1) : Math.min(diffX, w);;
this.y = diffH < 0 ? Math.max(diffH, h * -1) : Math.min(diffH, h);
} else {
this.x = newX;
this.y = newY;
}
return this;
}
}, true);
},
/**
* Creates an arrow
*
* @class arrow
* @extends _basicElement
*/
arrow: function (target) {
if (_helpers.$obj.isUndefined(target) || (target === null)) {
throw new _defaultConfigurations.errors.nullParameterException("target");
}
var _target = target,
_drawArrowhead = function (context, x, y, radians, sizeW, sizeH, arrowColor) {
context.fillStyle = arrowColor;
context.save();
context.beginPath();
context.translate(x, y);
context.rotate(radians);
context.moveTo(0, -10);
context.lineTo(sizeH, sizeW);
context.lineTo(sizeH * -1, sizeW);
context.closePath();
context.restore();
context.fill();
};
return _helpers.$obj.extend(new _basicElement(), {
name: "arrow" + _helpers.$obj.generateId(),
clearAllLinks: function () {
if (_helpers.$obj.isValid(this.getParent())
&& _helpers.$obj.is(this.getParent().removeLinkFrom, "function")
&& _helpers.$obj.is(_target.removeLinkTo, "function")) {
this.getParent().removeLinkFrom(_target).removeLinkTo(_target);
_target.removeLinkFrom(this.getParent()).removeLinkTo(this.getParent());
}
return this;
},
elementDisabledNotification: function (element) {
if ((element === _target) || (element === this.getParent())) {
this.disable();
}
},
graphNode: false,
canHandleEvents: function () { return false; },
isSerializable: function () { return false; },
getType: function () { return _elementTypes.arrow; },
canLink: function () { return false; },
isLinkable: function () { return false; },
border: { color: "gray", active: true, width: 2 },
fillColor: "rgba(0,0,0,0.3)",
getWidth: function () { return 1; },
getHeight: function () { return 1; },
drawTo: function (canvas, context) {
if (_helpers.$obj.isValid(_target.notifyDisableStateOn)) {
_target.notifyDisableStateOn(this);
}
if (_helpers.$obj.isValid(this.getParent().notifyDisableStateOn)) {
this.getParent().notifyDisableStateOn(this);
}
var adjustedTargedCoordinates = _target.getAdjustedCoordinates ? _target.getAdjustedCoordinates() : { x: _target.x, y: _target.y },
parent = this.getParent(),
adjustedParentCoordinates = parent.getAdjustedCoordinates(),
parentX = (adjustedParentCoordinates.x + (parent.getAdjustedWidth ? (parent.getAdjustedWidth() / 2) : 0)),
parentY = (adjustedParentCoordinates.y + (parent.getAdjustedHeight ? (parent.getAdjustedHeight() / 2) : 0));
this.x = (adjustedTargedCoordinates.x + (_target.getAdjustedWidth ? (_target.getAdjustedWidth() / 2) : 0));
this.y = (adjustedTargedCoordinates.y + (_target.getAdjustedHeight ? (_target.getAdjustedHeight() / 2) : 0));
context.beginPath();
context.fillStyle = this.fillColor;
if (this.border.active === true) {
var grad = context.createLinearGradient(this.x, this.y, parentX, parentY);
grad.addColorStop(0, this.border.color);
grad.addColorStop(0.5, this.fillColor);
grad.addColorStop(1, this.border.color);
context.lineWidth = this.border.width;
context.strokeStyle = grad;
}
context.moveTo(this.x, this.y);
context.lineTo(parentX, parentY);
context.stroke();
var startRadians = Math.atan((parentY - this.y) / (parentX - this.x));
startRadians += ((parentX > this.x) ? -90 : 90) * Math.PI / 180;
_drawArrowhead(context, this.x, this.y, startRadians, 5, 8, this.fillColor);
if (this.border.active === true) {
_drawArrowhead(context, this.x, this.y, startRadians, 3, 5, this.border.color);
} else {
_drawArrowhead(context, this.x, this.y, startRadians, 3, 5, "white");
}
}
}, true);
},
/**
* Wraps any HTML element as a Raska element
*
* @class htmlElement
* @constructor
* @extends _basicElement
*/
htmlElement: function (element) {
if (!_helpers.$obj.isValid(element)) {
var nullObj = new _defaultConfigurations.errors.nullParameterException("element");
console.error(nullObj.message);
throw nullObj;
}
var targetElement = element;
return _helpers.$obj.extend(new _basicElement(), {
name: "htmlElement" + _helpers.$obj.generateId(),
canHandleEvents: function () { return false; },
getType: function () { return _elementTypes.html; },
graphNode: false,
isSerializable: function () { return false; },
canLink: function () { return false; },
isLinkable: function () { return false; },
border: { active: false },
fillColor: "",
getWidth: function () { return element.clientWidth; },
getHeight: function () { return element.clientHeight; },
drawTo: function (canvas, context) { },
existsIn: function (x, y) { return false; },
handleInteractions: true,
onIteraction: function (iteractionType, trigger) {
var triggerWrapper = function (evt) {
trigger(evt, targetElement, iteractionType);
};
_helpers.$device.on(targetElement, iteractionType, triggerWrapper);
}
}, true);
},
/**
* Creates a triangle.
*
* @class triangle
* @extends _basicElement
*/
triangle: function (pointingUp) {
var _bcData = {
p2: { x: 0, y: 0 },
p3: { x: 0, y: 0 }
};
return _helpers.$obj.extend(new _basicElement(), {
name: "triangle" + _helpers.$obj.generateId(),
border: { color: "gray", active: true, width: 2 },
getType: function () { return _elementTypes.triangle; },
fillColor: "silver",
pointingUp: (pointingUp !== false),
dimensions: {
width: 50,
height: 50
},
setWidth: function (width) {
this.dimensions.width = width;
return this;
},
getWidth: function () {
return this.dimensions.width;
},
setHeight: function (height) {
this.dimensions.height = height;
return this;
},
getAdjustedHeight: function () {
return (this.pointingUp === true) ? (this.dimensions.height * -1) : (this.dimensions.height / 2);
},
getHeight: function () {
return this.dimensions.height;
},
drawTo: function (canvas, context) {
var trasform = this.pointingUp === false ? function (x, y) { return x + y; } : function (x, y) { return x - y; },
coordinates = this.getAdjustedCoordinates();
context.beginPath();
context.fillStyle = this.fillColor;
context.moveTo(coordinates.x, coordinates.y);
context.lineTo(_bcData.p2.x = (coordinates.x + (this.dimensions.width / 2)),
_bcData.p2.y = trasform(coordinates.y, this.dimensions.height));
context.lineTo(_bcData.p3.x = coordinates.x + this.dimensions.width,
_bcData.p3.y = coordinates.y);
context.closePath();
if (this.border.active === true) {
context.lineWidth = this.border.width;
context.strokeStyle = this.border.color;
}
context.fill();
context.stroke();
},
existsIn: function (x, y) {
//https://en.wikipedia.org/wiki/Barycentric_coordinate_system_%28mathematics%29
var coordinates = this.getAdjustedCoordinates();
var p1 = coordinates, p2 = _bcData.p2, p3 = _bcData.p3, p = { x: x, y: y };
var alpha = ((p2.y - p3.y) * (p.x - p3.x) + (p3.x - p2.x) * (p.y - p3.y)) / ((p2.y - p3.y) * (p1.x - p3.x) + (p3.x - p2.x) * (p1.y - p3.y)),
beta = ((p3.y - p1.y) * (p.x - p3.x) + (p1.x - p3.x) * (p.y - p3.y)) / ((p2.y - p3.y) * (p1.x - p3.x) + (p3.x - p2.x) * (p1.y - p3.y)),
gamma = 1 - alpha - beta;
return alpha > 0 && beta > 0 && gamma > 0;
},
adjustPosition: function (newX, newY) {
var $parent = this.getParent();
if ((this.position === _positionTypes.relative) && ($parent !== null)) {
var adjustedParent = $parent.getAdjustedCoordinates();
var w = $parent.getWidth() / 2;
var h = $parent.getHeight() / 2;
var diffX = (newX - adjustedParent.x) - w;
var diffH = (newY - adjustedParent.y) - h;
this.x = diffX < 0 ? Math.max(diffX, w * -1) : Math.min(diffX, w);;
this.y = diffH < 0 ? Math.max(diffH, h * -1) : Math.min(diffH, h);
} else {
this.x = newX;
this.y = newY;
}
return this;
}
}, true);
},
/**
* Creates a cricle.
*
* @class circle
* @extends _basicElement
*/
circle: function () {
return _helpers.$obj.extend(new _basicElement(), {
name: "circle" + _helpers.$obj.generateId(),
border: { color: "gray", active: true, width: 2 },
getType: function () { return _elementTypes.circle; },
fillColor: "silver",
radius: 20,
setWidth: function (r) {
this.radius = r / 2;
return this;
},
getWidth: function () {
return this.radius * 2;
},
getAdjustedWidth: function () {
return this.radius / 2;
},
setHeight: function (r) {
this.radius = r / 2;
return this;
},
getAdjustedHeight: function () {
return this.radius / 2;
},
getHeight: function () {
return this.radius * 2;
},
drawTo: function (canvas, context) {
var coordinates = this.getAdjustedCoordinates();
context.beginPath();
context.arc(coordinates.x, coordinates.y, this.radius, 0, 2 * Math.PI, false);
context.fillStyle = this.fillColor;
context.fill();
if (this.border.active === true) {
context.lineWidth = this.border.width;
context.strokeStyle = this.border.color;
context.stroke();
}
},
existsIn: function (x, y) {
var dx = this.x - x;
var dy = this.y - y;
return (dx * dx + dy * dy < this.radius * this.radius);
},
getBoundaries: function () {
var p = this.radius / 2;
return {
x: p,
y: p,
minX: p * -1,
minY: p * -1
};
},
adjustPosition: function (newX, newY) {
var $parent = this.getParent();
if ((this.position === _positionTypes.relative) && ($parent !== null)) {
var adjustedParent = $parent.getAdjustedCoordinates();
var parentBoundaries = $parent.getBoundaries();
var w = parentBoundaries.x;
var h = parentBoundaries.y;
var diffX = (newX - adjustedParent.x) - w;
var diffH = (newY - adjustedParent.y) - h;
this.x = diffX < 0 ? Math.max(diffX, parentBoundaries.minX) : Math.min(diffX, w);;
this.y = diffH < 0 ? Math.max(diffH, parentBoundaries.minY) : Math.min(diffH, h);
} else {
this.x = newX;
this.y = newY;
}
return this;
}
}, true);
}
},
/**
* Holds the value for the active configuration on this Raska instance
*
* @private
* @attribute _activeConfiguration
* @default null
* @module raska
*/
_activeConfiguration = null,
_elementInteractionEventData = (function () {
var interactionTypes = { "click": 0, "mousemove": 1 },
createTriggerUsing = function (originalTrigger) {
return function (evt, targetElement, interactionType) {
originalTrigger({
x: _drawing.mouseHelper.getX(evt),
y: _drawing.mouseHelper.getY(evt),
details: {
interactionType: interactionType,
targetElement: targetElement
}
});
};
};
return {
getInteractionTypes: interactionTypes,
register: function (targetElement, iteractionType, trigger) {
if ((iteractionType in interactionTypes)
&& _helpers.$obj.isValid(targetElement)
&& (targetElement.handleInteractions === true)) {
targetElement.onIteraction(iteractionType, createTriggerUsing(trigger));
}
}
};
})(),
/**
* An utility container that holds all the drawing related implementations
*
* @class _drawing
* @module raska
* @readOnly
* @static
*/
_drawing = (function () {
var
/**
* Holds the elements that are to be drawn to the target canvas
*
* @private
* @property _elements
* @type Array
* @default []
*/
_elements = [],
/**
* Whether or not we have a timer running
*
* @private
* @property _timerRunning
* @type Bool
* @default false
*/
_timerRunning = false,
/**
* The Canvas element we're targeting
*
* @private
* @property _canvas
* @type HTMLElement
* @default null
*/
_canvas = null,
/**
* The Canvas element (wraped by a Raska _basicElement)
*
* @private
* @property _canvasElement
* @type _basicElement
* @default null
*/
_canvasElement = null,
/**
* The 2dContext from the canvas element we're targeting
*
* @private
* @property _2dContext
* @type Object
* @default null
*/
_2dContext = null,
/**
* Handles the periodic draw of the elements in this Raska instance
*
* @method _timedDrawing
* @private
*/
_timedDrawing = function () {
if (_timerRunning === true) {
_draw();
w.requestAnimationFrame(_timedDrawing);
}
},
/**
* Plots the element itself and all its related nodes into the canvas
*
* @method _drawAllIn
* @param {_basicElement} element The element being drawn to the canvas
* @private
*/
_drawAllIn = function (element) {
element.drawTo(_canvas, _2dContext);
var childElements = element.getChildElements();
for (var i = 0; i < childElements.length; i++) {
_drawAllIn(childElements[i]);
}
},
/**
* A utility that detects mouse's relative position on the canvas
*
* @class _mouse
* @for _drawing
* @static
*/
_mouse = {
/**
* Gets mouses's X position relative to the current canvas
*
* @method getX
* @param {Event} evt Event we're bubbling in
* @for _mouse
* @return {Number} X value
* @static
*/
getX: function (evt) {
return _helpers.$device.gathersXYPositionFrom(_canvas, evt).x;
},
/**
* Gets mouses's Y position relative to the current canvas
*
* @method getY
* @param {Event} evt Event we're bubbling in
* @for _mouse
* @return {Number} Y value
* @static
*/
getY: function (evt) {
return _helpers.$device.gathersXYPositionFrom(_canvas, evt).y;
},
/**
* Gathers the mouse coordinates without the need of an event bubble
*
* @class staticCoordinates
* @for _mouse
* @static
*/
staticCoordinates: (function () {
var _evt = { clientX: 0, clientY: 0 },
started = false;
return {
/**
* Gets mouses' X and Y positions relative to the current canvas
*
* @method getXY
* @return {Object} X and Y values
* @static
*/
getXY: function () {
if (!started) {
_canvas.onmousemove = function (e) {
_evt.clientX = e.clientX;
_evt.clientY = e.clientY + 30;
}
started = true;
}
return {
x: _mouse.getX(_evt),
y: _mouse.getY(_evt)
};
}
};
})()
},
/**
* A utility that tracks the state of the element being draged (if any)
*
* @class _elementBeingDraged
* @for _drawing
*/
_elementBeingDraged = {
/**
* The element being draged
*
* @property reference
* @type _basicElement
* @default null
* @for _elementBeingDraged
* @static
*/
reference: null,
/**
* Whether or not the user is holding the CTRL key
*
* @property holdingCTRL
* @type Bool
* @default false
* @for _elementBeingDraged
* @static
*/
holdingCTRL: false,
/**
* A copy from the original border specification for the element being draged
*
* @property originalBorder
* @type Object
* @default {}
* @for _elementBeingDraged
* @static
*/
originalBorder: {},
/**
* The types of drag that can be applied to an element
*
* It can be either:
* 1 - moving
* 3 - linking
*
* @attribute dragTypes
*/
dragTypes: {
/**
* The element is being moved
*
* @element moving
* @parents dragTypes
*/
moving: 1,
/**
* The element is being linked
*
* @element moving
* @parents dragTypes
*/
linking: 3
},
/**
* Default drag type
*
* @property dragType
* @type Number
* @default 0
* @for _elementBeingDraged
* @static
*/
dragType: 0,
temporaryLinkArrow: (function () {
return {
/**
* Whenever possible/necessary it plots the linking arrow to the canvas
*
* @method tryRender
* @private
*/
tryRender: function () {
if ((_elementBeingDraged.reference !== null)
&& (_elementBeingDraged.dragType === _elementBeingDraged.dragTypes.linking)
&& _elementBeingDraged.reference.isLinkable() === true) {
var targetXY = _mouse.staticCoordinates.getXY(),
arrow = new _defaultConfigurations.arrow({
x: targetXY.x,
y: targetXY.y - 30,
getHeight: function () { return 0; }
});
arrow.name = "_temp";
arrow.setParent(_elementBeingDraged.reference);
arrow.drawTo(_canvas, _2dContext);
}
return this;
}
};
})(),
border: {
whenMoving: {
color: "yellow",
active: true
},
whenLiking: {
color: "green",
active: true
}
}
},
/**
* Handles de mouse move envent
*
* @method _whenMouseMove
* @param {Event} evt Event we're bubbling in
* @for _drawing
* @private
*/
_whenMouseMove = function (evt) {
if ((_elementBeingDraged.reference !== null)
&& (_elementBeingDraged.dragType === _elementBeingDraged.dragTypes.moving)) {
_helpers.$log.info("Moving element", _elementBeingDraged);
_elementBeingDraged.reference.x = _mouse.getX(evt);
_elementBeingDraged.reference.y = _mouse.getY(evt);
}
},
/**
* Handles de mouse up envent
*
* @method _whenMouseUp
* @param {Event} evt Event we're bubbling in
* @private
*/
_whenMouseUp = function (evt) {
if (_elementBeingDraged.reference !== null) {
if (_elementBeingDraged.dragType === _elementBeingDraged.dragTypes.linking) {
for (var i = 0; i < _elements.length; i++) {
if (_elements[i] !== _elementBeingDraged.reference
&& _elements[i].existsIn(_mouse.getX(evt), _mouse.getY(evt))) {
if (_elementBeingDraged.reference.addLinkTo(_elements[i])) {
_elements.push(new _defaultConfigurations
.arrow(_elements[i])
.setParent(_elementBeingDraged.reference));
break;
}
}
}
}
_elementBeingDraged.reference.on.release(_mouse.getX(evt), _mouse.getY(evt),
_elementBeingDraged.reference, evt);
_elementBeingDraged.reference.border = _elementBeingDraged.originalBorder;
_elementBeingDraged.reference = null;
}
},
/**
* Removes a given element from the canvas
*
* @method _removeElement
* @param {_basicElement} e Element that is to be removed
* @private
* @chainable
*/
_removeElement = function (e) {
_helpers.$log.info("Removing element", e);
e.disable();
_elements.splice(_elements.indexOf(e), 1);
return this;
},
/**
* Adds a given element from the canvas
*
* @method _addElement
* @param {_basicElement} e Element that is to be added
* @private
* @chainable
*/
_addElement = function (e) {
_helpers.$log.info("Adding element", e);
_elements.push(e);
return this;
},
/**
* Handles de key up envent
*
* @method _whenKeyUp
* @param {Event} evt Event we're bubbling in
* @private
* @chainable
*/
_whenKeyUp = function (evt) {
_elementBeingDraged.holdingCTRL = false;
return this;
},
/**
* Handles de key down envent
*
* @method _whenKeyDown
* @param {Event} evt Event we're bubbling in
* @private
* @chainable
*/
_whenKeyDown = function (e) {
var key = e.keyCode || e.charCode;
if (((key === 46) || (key === 8))
&& (_elementBeingDraged.reference !== null)) {
_removeElement(_elementBeingDraged.reference.clearAllLinks());
_elementBeingDraged.reference = null;
} else {
_elementBeingDraged.holdingCTRL = (key === 17);
}
return this;
},
/**
* Handles the click event
*
* @method _checkClick
* @param {Event} evt Event we're bubbling in
* @private
*/
_checkClick = function (evt) {
evt = evt || w.event;
for (var i = 0; i < _elements.length; i++) {
if (_elements[i].existsIn(_mouse.getX(evt), _mouse.getY(evt))) {
if ((_elementBeingDraged.reference = _elements[i]).canHandleEvents()) {
_elementBeingDraged.reference.on.click(
_mouse.getX(evt),
_mouse.getY(evt),
_elementBeingDraged.reference, evt);
}
}
}
if (_elementBeingDraged.reference !== null) {
var dragType = _elementBeingDraged.dragTypes.moving;
if (!_helpers.$device.isTouch) {
dragType = _elementBeingDraged.holdingCTRL === true && _elementBeingDraged.reference.isLinkable() === true ?
_elementBeingDraged.dragTypes.linking :
evt.which;
}
_elementBeingDraged.originalBorder = _elementBeingDraged.reference.border;
_elementBeingDraged.reference.border =
((_elementBeingDraged.dragType = dragType) === _elementBeingDraged.dragTypes.moving) ?
_elementBeingDraged.border.whenMoving :
_elementBeingDraged.border.whenLiking;
_helpers.$log.info("Dragging element", { r: _elementBeingDraged.reference, d: dragType });
}
if (evt.preventDefault) {
evt.preventDefault();
} else if (evt.returnValue) {
evt.returnValue = false;
}
return false;
},
/**
* Plots each and every element to the canvas and registers all the event handlers delegates
*
* @method _draw
* @private
* @chainable
*/
_draw = function () {
if (_canvas === null) {
_2dContext = (_canvas = $("#" + _activeConfiguration.targetCanvasId))
.getContext("2d");
/// Canvas related events
_helpers.$dom.get(_canvas)
.on("mousedown", _checkClick)
.on("mousemove", _whenMouseMove)
.on("contextmenu", function (e) {
e.preventDefault();
return false;
});
if (_helpers.$obj.isArray(_activeConfiguration.toolboxButtons)) {
var wCanvas = _helpers.$dom.get(_canvas);
if (wCanvas.getParent().attr("id") !== _activeConfiguration.targetCanvasContainerId) {
wCanvas
.getParent()
.addChild("div")
.attr("id", _activeConfiguration.targetCanvasContainerId)
.addChild(wCanvas.raw());
}
_helpers.$obj.forEach(_activeConfiguration.toolboxButtons, function (button) {
wCanvas
.addSibling("div").attr("id", button.id = _helpers.$obj.generateId())
.html(button.template)
.on("click", function () {
button.onclick(_canvas);
});
});
}
/// Window events
_helpers.$dom.get(w)
.on("mouseup", _whenMouseUp)
.on("keydown", _whenKeyDown)
.on("keyup", _whenKeyUp);
if (_helpers.$device.isTouch === true) {
/// If we're in a touch device
_helpers.$dom.get(_canvas)
.on("touchstart", _checkClick)
.on("touchmove", _whenMouseMove);
_helpers.$dom.get(w)
.on("touchend", _whenMouseUp)
.on("touchcancel", _whenMouseUp);
}
_canvasElement = _helpers.$obj.extend(new _defaultConfigurations.htmlElement(_canvas), {});
}
_2dContext.clearRect(0, 0, _canvas.width, _canvas.height);
for (var i = 0; i < _elements.length; i++) {
_drawAllIn(_elements[i]);
}
_elementBeingDraged.temporaryLinkArrow.tryRender();
return this;
},
/**
* Gathers all elements being ploted to the canvas and organizes it as a directed graph JSON
*
* @method _getElementsSlim
* @return {_graphNodeInfo} Graph node information
* @private
*/
_getElementsSlim = function () {
var result = {},
element,
links;
for (var i = 0; i < _elements.length; i++) {
element = _elements[i];
if (element.graphNode === true) {
if ((((links = element.getLinksFrom()) === null) || (links.length === 0))
&& (((links = element.getLinksTo()) === null) || (links.length === 0))) {
throw new _defaultConfigurations.errors.elementDoesNotHaveALink(element.name);
}
result[element.name] = new _graphNodeInfo(element, null, element.getLinksTo());
}
}
return result;
};
return {
mouseHelper: _mouse,
/**
* Add a given element to the drawing elements array
*
* @method addElement
* @param {_basicElement} element The element to be drawn
* @chainable
*/
addElement: function (element) {
_addElement(element);
return this;
},
/**
* Gets the canvas' dataURL
*
* @method getDataUrl
* @param {bool} cropImage Whether or not to crop the exported image
*/
getDataUrl: function (cropImage) {
if (cropImage === true) {
var silkCanvas = _helpers.$dom.create("canvas").raw(),
silkCanvasContext = silkCanvas.getContext("2d"),
sorter = function (a, b) { return a - b },
sourceCanvasWidth = _canvas.width,
sourceCanvasHeight = _canvas.height,
pix = { x: [], y: [] },
imageData = _2dContext.getImageData(0, 0, sourceCanvasWidth, sourceCanvasHeight),
index = 0;
for (var y = 0; y < sourceCanvasHeight; y++) {
for (var x = 0; x < sourceCanvasWidth; x++) {
index = (y * sourceCanvasWidth + x) * 4;
if (imageData.data[index + 3] > 0) {
pix.x.push(x);
pix.y.push(y);
}
}
}
pix.x.sort(sorter);
pix.y.sort(sorter);
var n = pix.x.length - 1;
sourceCanvasWidth = pix.x[n] - pix.x[0];
sourceCanvasHeight = pix.y[n] - pix.y[0];
var cut = _2dContext.getImageData(pix.x[0], pix.y[0], sourceCanvasWidth, sourceCanvasHeight);
silkCanvas.width = sourceCanvasWidth;
silkCanvas.height = sourceCanvasHeight;
silkCanvasContext.putImageData(cut, 0, 0);
return silkCanvas.toDataURL();
}
return _canvas.toDataURL();
},
/**
* Removes a given element from the canvas
*
* @method remove
* @param {_basicElement} element Element that is to be removed
* @private
* @chainable
*/
remove: function (element) {
_removeElement(element);
return this;
},
/**
* Gathers all elements being ploted to the canvas and organizes it as a directed graph JSON
*
* @method getElementsSlim
* @return {_graphNodeInfo} Graph node information
*/
getElementsSlim: function () {
return _getElementsSlim();
},
/**
* Gathers all elements being ploted to the canvas
*
* @method getElements
* @return {_basicElement[]} Graph node information
*/
getElements: function () {
return _elements;
},
/**
* Redefines the elements that are supposed to be ploted to the canvas
*
* @method reloadUsing
* @param {_basicElements[]} elements The elements that are going to be ploted
* @static
* @chainable
*/
reloadUsing: function (elements) {
_elements = elements;
this.initializeTimedDrawing();
},
/**
* Returns the corresponding Raska Canvas element
*
* @method getCanvasElement
*/
getCanvasElement: function () {
return _canvasElement;
},
/**
* Returns the corresponding Raska Canvas element
*
* @method getCanvasElement
* @return {HTMLElement} Canvas
*/
getCanvas: function () {
return _canvas;
},
/**
* Initializes the drawing process
*
* @method initializeTimedDrawing
*/
initializeTimedDrawing: function () {
if (_timerRunning === false) {
_timerRunning = true;
_timedDrawing();
}
}
};
})(),
/**
* All public avaliable methods from the Raska library
*
* @class _public
*/
_public = {
/**
* Adds a new Label to the target canvas
*
* @method newLabel
* @return {_defaultConfigurations.label} Copy of '_defaultConfigurations.label' object
* @static
*/
newLabel: function (defaultConfiguration) {
return _helpers.$obj.extend(new _defaultConfigurations.label(), defaultConfiguration);
},
/**
* Adds a new Square to the target canvas
*
* @method newSquare
* @return {_defaultConfigurations.square} Copy of '_defaultConfigurations.square' object
* @static
*/
newSquare: function () {
return _helpers.$obj.extend(new _defaultConfigurations.square(), {});
},
/**
* Adds a new Triangle to the target canvas
*
* @method newTriangle
* @return {_defaultConfigurations.square} Copy of '_defaultConfigurations.triangle' object
* @static
*/
newTriangle: function (pointingUp) {
return _helpers.$obj.extend(new _defaultConfigurations.triangle(pointingUp), {});
},
/**
* Adds a new Circle to the target canvas
*
* @method newCircle
* @return {_defaultConfigurations.circle} Copy of '_defaultConfigurations.circle' object
* @static
*/
newCircle: function () {
return _helpers.$obj.extend(new _defaultConfigurations.circle(), {});
},
/**
* Plots an element to the canvas
*
* @method plot
* @return {_public} Reference to the '_public' pointer
* @static
* @chainable
*/
plot: function (element) {
_drawing.addElement(element);
return _public;
},
/**
* Exports current canvas data as an image to a new window
*
* @method exportImage
* @param {bool} cropImage Whether or not to crop the exported image
* @return {_public} Reference to the '_public' pointer
* @chainable
* @static
*/
exportImage: function (cropImage) {
w.open(_drawing.getDataUrl(cropImage), '_blank');
return _public;
},
/**
* Retrieves the raw elements from the drawing stack
*
* @method getElementsRaw
* @return {json} The JSON object that represents ALL the Raska elements in the canvas
* @static
*/
getElementsRaw: function () {
return _drawing.getElements();
},
/**
* Retrieves the directed graph represented by the elements in the canvas
*
* @method getElementsSlim
* @return {json} The JSON object that represents the current directed graph drawn to the canvas
* @static
*/
getElementsSlim: function () {
return _drawing.getElementsSlim();
},
/**
* Retrieves the ENTIRE directed graph represented by the elements in the canvas
*
* @method getElementsData
* @return {String} The stringfied JSON that represents the current directed graph drawn to the canvas
* @static
*/
getElementsString: function () {
return _helpers.$obj.deconstruct(_drawing.getElements());
},
/**
* Redefines the elements that are supposed to be ploted to the canvas
*
* @method loadElementsFrom
* @param {_basicElements[]} source The elements that are going to be ploted
* @static
* @chainable
*/
loadElementsFrom: function (source) {
var preparsedSource = JSON.parse(source);
if (_helpers.$obj.isArray(preparsedSource)) {
var realSource = [], childElements = [], i = 0, parsed = null;
/// Create a basic element instance
for (i = 0; i < preparsedSource.length; i++) {
if ((parsed = _helpers.$obj.recreate(preparsedSource[i])) === null) {
alert("Invalid JSON. See the console for more info");
console.error("Could not deserialize this element", preparsedSource[i]);
return this;
}
if (preparsedSource[i].parent === null) {
parsed.originalIndex = i;
realSource.push(parsed);
} else {
childElements.push(parsed);
}
}
/// Adds back the links between elements
var findElement = function (itemName) {
if (!_helpers.$obj.isValid(itemName)) {
return null;
}
function find(arr) {
for (var j = 0; j < arr.length; j++) {
if (arr[j].name === itemName) {
return arr[j];
}
}
}
var itemFound = find(realSource) || find(childElements);
if (!itemFound) {
throw _defaultConfigurations.errors.itemNotFoundException(itemName);
}
return itemFound;
};
for (i = 0; i < realSource.length; i++) {
_helpers.$obj.recreateLinks(realSource[i], preparsedSource[realSource[i].originalIndex], findElement);
delete realSource[i].originalIndex;
}
/// Recriates all arrows
var linksTo = [], item;
for (i = 0; i < realSource.length; i++) {
item = realSource[i];
if ((item.isLinkable() === true)
&& ((linksTo = item.getLinksTo()).length > 0)) {
for (var k = 0; k < linksTo.length; k++) {
realSource.push(new _defaultConfigurations
.arrow(linksTo[k])
.setParent(item));
}
}
}
_drawing.reloadUsing(realSource);
}
return this;
},
/**
* Toggles fullscreen mode on/off
*
* @property toggleFullScreen
* @static
* @chainable
*/
toggleFullScreen: function () {
_canvasController.toggleFullScreen(_drawing.getCanvas());
return _public;
},
/**
* Retrieves all possible position types
*
* @property positionTypes
* @type _positionTypes
* @static
*/
positionTypes: (function () {
return _positionTypes;
})(),
/**
* Configures the Raska library to target a given canvas using the configuration passed
* as a parameter
*
* @method installUsing
* @param {_defaultConfigurations.library} configuration Configuration data that should be used to configure this Raska instance
* @return {_public} Reference to the '_public' pointer
* @static
* @chainable
*/
installUsing: function (configuration) {
_activeConfiguration = _helpers.$obj.extend(_defaultConfigurations.library, configuration);
_drawing.initializeTimedDrawing();
return _public;
},
/**
* Registers a handler to be trigered by any interaction taken place against the canvas
*
* @method onElementInteraction
* @param {string} iteractionType When to trigger the iteraction handler
* @param {Function} trigger What to do whenever an element iteraction happens
* @static
* @chainable
*/
onCanvasInteraction: function (iteractionType, trigger) {
_elementInteractionEventData.register(_drawing.getCanvasElement(), iteractionType, trigger);
return this;
},
/**
* Checks whether or not an element does exists at a given coordinate
*
* @method checkCollisionOn
* @param {number} x X position
* @param {number} y Y position
* @return {bool} Wheter or not an element can be found at these coordinates
* @static
*/
checkCollisionOn: function (x, y) {
return _public.tryGetElementOn(x, y) !== null;
},
/**
* Tries to get the element that exists at a given coordinate
*
* @method tryGetElementOn
* @param {number} x X position
* @param {number} y Y position
* @return {_basicElement} Raska basic element (if any) or null
* @static
*/
tryGetElementOn: function (x, y) {
var elements = _drawing.getElements();
for (var i = 0; i < elements.length; i++) {
if (elements[i].existsIn(x, y) === true) {
return elements[i];
}
}
return null;
},
/**
* Removes a given element from the canvas
*
* @method remove
* @param {_basicElement} element Element that is to be removed
* @private
* @chainable
*/
remove: function (element) {
_drawing.remove(element);
return this;
},
/**
* Gathers the target canvas boundaries
*
* @method getCanvasBoundaries
* @static
* @return {maxW:number, maxH:number}
*/
getCanvasBoundaries: function () {
var el = _drawing.getCanvasElement();
return { maxH: el.getHeight(), maxW: el.getWidth() };
},
/**
* Clears all elements from the canvas
*
* @method clear
* @static
* @chainable
*/
clear: function () {
_drawing.reloadUsing([]);
return this;
},
$$: { $h: _helpers, $q: $, $c: _activeConfiguration }
};
w.raska = _public;
})(window, document);