/** @namespace */ var edges = { ////////////////////////////////////////////////////// /** @namespace */ /** function to run to start a new Edge */ /** * @param selector="body" {String} The jquery selector for the element where the edge will be deployed. * @param search_url {String} The base search url which will respond to elasticsearch queries. Generally ends with _search * @param datatype="jsonp" {String} Datatype for ajax requests to use - overall recommend using jsonp * @param preflightQueries {Dictionary} Dictionary of queries to be run before the primary query is executed: preflight id : es.newQuery(....). Results will appear with the same ids in this.preflightResults. Preflight queries are /not/ subject to the base query * @param baseQuery {String} Query that forms the basis of all queries that are assembled and run. Note that baseQuery is inviolable - it's requirements will always be enforced * @param openingQuery {String} Query to use to initialise the search. Use this to set your opening values for things like page size, initial search terms, etc. Any request to reset the interface will return to this query. * @param secondaryQueries {?} Dictionary of functions that will generate secondary queries which also need to be run at the point that cycle() is called. These functions and their resulting queries will be run /after/ the primary query (so can take advantage of the results). Their results will be stored in this.secondaryResults. Secondary queries are not subject the base query, although the functions may of course apply the base query too if they wish. secondary id : function() * @param initialSearch {Boolean} Should the init process do a search * @param staticFiles {Object} List of static files (e.g. data files) to be loaded at startup, and made available on the object for use by components. {"id" : internal id to give the file, "url" : file url, "processor" : edges.csv.newObjectByRow, "datatype" : "text", "opening" : function to be run after processing for initial state} * @param manageUrl {Boolean} Should the search url be synchronised with the browser's url bar after search and should queries be retrieved from the url on init * @param urlQuerySource="source" {String} Query parameter in which the query for this edge instance will be stored. * @param template {Object} Template object that will be used to draw the frame for the edge. May be left blank, in which case the edge will assume that the elements are already rendered on the page by the caller * @param components {Array} List of all the components that are involved in this edge * @param renderPacks=[edges.bs3, edges.nvd3, edges.highcharts, edges.google, edges.d3] {Array} Render packs to use to source automatically assigned rendering objects. Defaults to [edges.bs3, edges.nvd3, edges.highcharts, edges.google, edges.d3] */ newEdge : function(params) { if (!params) { params = {} } return new edges.Edge(params); }, /** @class */ Edge : function(params) { ///////////////////////////////////////////// // parameters that can be set via params arg // the jquery selector for the element where the edge will be deployed this.selector = edges.getParam(params.selector, "body"); // the base search url which will respond to elasticsearch queries. Generally ends with _search this.search_url = edges.getParam(params.search_url, false); // datatype for ajax requests to use - overall recommend using jsonp this.datatype = edges.getParam(params.datatype, "jsonp"); // dictionary of queries to be run before the primary query is executed // {"<preflight id>" : es.newQuery(....)} // results will appear with the same ids in this.preflightResults // preflight queries are /not/ subject to the base query this.preflightQueries = edges.getParam(params.preflightQueries, false); // query that forms the basis of all queries that are assembled and run // Note that baseQuery is inviolable - it's requirements will always be enforced this.baseQuery = edges.getParam(params.baseQuery, false); // query to use to initialise the search. Use this to set your opening // values for things like page size, initial search terms, etc. Any request to // reset the interface will return to this query this.openingQuery = edges.getParam(params.openingQuery, typeof es !== 'undefined' ? es.newQuery() : false); // dictionary of functions that will generate secondary queries which also need to be // run at the point that cycle() is called. These functions and their resulting // queries will be run /after/ the primary query (so can take advantage of the // results). Their results will be stored in this.secondaryResults. // secondary queries are not subject the base query, although the functions // may of course apply the base query too if they wish // {"<secondary id>" : function() } this.secondaryQueries = edges.getParam(params.secondaryQueries, false); // dictionary mapping keys to urls that will be used for search. These should be // the same keys as used in secondaryQueries, if those secondary queries should be // issued against different urls than the primary search_url. this.secondaryUrls = edges.getParam(params.secondaryUrls, false); // should the init process do a search this.initialSearch = edges.getParam(params.initialSearch, true); // list of static files (e.g. data files) to be loaded at startup, and made available // on the object for use by components // {"id" : "<internal id to give the file>", "url" : "<file url>", "processor" : edges.csv.newObjectByRow, "datatype" : "text", "opening" : <function to be run after processing for initial state>} this.staticFiles = edges.getParam(params.staticFiles, []); // should the search url be synchronised with the browser's url bar after search // and should queries be retrieved from the url on init this.manageUrl = edges.getParam(params.manageUrl, false); // query parameter in which the query for this edge instance will be stored this.urlQuerySource = edges.getParam(params.urlQuerySource, "source"); // options to be passed to es.Query.objectify when prepping the query to be placed in the URL this.urlQueryOptions = edges.getParam(params.urlQueryOptions, false); // template object that will be used to draw the frame for the edge. May be left // blank, in which case the edge will assume that the elements are already rendered // on the page by the caller this.template = edges.getParam(params.template, false); // list of all the components that are involved in this edge this.components = edges.getParam(params.components, []); // the query adapter this.queryAdapter = edges.getParam(params.queryAdapter, edges.newESQueryAdapter()); // render packs to use to source automatically assigned rendering objects this.renderPacks = edges.getParam(params.renderPacks, [edges.bs3, edges.nvd3, edges.highcharts, edges.google, edges.d3]); // list of callbacks to be run synchronously with the edge instance as the argument // (these bind at the same points as all the events are triggered, and are keyed the same way) this.callbacks = edges.getParam(params.callbacks, {}); ///////////////////////////////////////////// // operational properties // the query most recently read from the url this.urlQuery = false; // original url parameters this.urlParams = {}; // the short url for this page this.shortUrl = false; // the last primary ES query object that was executed this.currentQuery = false; // the last result object from the ES layer this.result = false; // the results of the preflight queries, keyed by their id this.preflightResults = {}; // the actual secondary queries derived from the functions in this.secondaryQueries; this.realisedSecondaryQueries = {}; // results of the secondary queries, keyed by their id this.secondaryResults = {}; // if the search is currently executing this.searching = false; // jquery object that represents the selected element this.context = false; // raw access to this.staticFiles loaded resources, keyed by id this.static = {}; // access to processed static files, keyed by id this.resources = {}; // list of static resources where errors were encountered this.errorLoadingStatic = []; //////////////////////////////////////////////////////// // startup functions // at the bottom of this constructor, we'll call this function this.startup = function() { // obtain the jquery context for all our operations this.context = $(this.selector); // trigger the edges:init event this.trigger("edges:pre-init"); // if we are to manage the URL, attempt to pull a query from it if (this.manageUrl) { var urlParams = this.getUrlParams(); if (this.urlQuerySource in urlParams) { this.urlQuery = es.newQuery({raw : urlParams[this.urlQuerySource]}); delete urlParams[this.urlQuerySource]; } this.urlParams = urlParams; } // render the template if necessary if (this.template) { this.template.draw(this); } // call each of the components to initialise themselves for (var i = 0; i < this.components.length; i++) { var component = this.components[i]; component.init(this); } // now call each component to render itself (pre-search) this.draw(); // load any static files - this will happen asynchronously, so afterwards // we call finaliseStartup to finish the process var onward = edges.objClosure(this, "startupPart2"); this.loadStaticsAsync(onward); }; this.startupPart2 = function() { // FIXME: at this point we should check whether the statics all loaded correctly var onward = edges.objClosure(this, "startupPart3"); this.runPreflightQueries(onward); }; this.startupPart3 = function() { // determine whether to initialise with either the openingQuery or the urlQuery var requestedQuery = this.openingQuery; if (this.urlQuery) { // if there is a URL query, then we open with that, and then forget it requestedQuery = this.urlQuery; this.urlQuery = false } // request the components to contribute to the query for (var i = 0; i < this.components.length; i++) { var component = this.components[i]; component.contrib(requestedQuery); } // finally push the query, which will reconcile it with the baseQuery this.pushQuery(requestedQuery); // trigger the edges:post-init event this.trigger("edges:post-init"); // now issue a query this.cycle(); }; ///////////////////////////////////////////////////////// // Edges lifecycle functions this.doQuery = function() { // the original doQuery has become doPrimaryQuery, so this has been aliased for this.cycle this.cycle(); }; this.cycle = function() { // if a search is currently executing, don't do anything, else turn it on // FIXME: should we queue them up? - see the d3 map for an example of how to do this if (this.searching) { return; } this.searching = true; // invalidate the short url this.shortUrl = false; // pre query event this.trigger("edges:pre-query"); // if we are managing the url space, use pushState to set it if (this.manageUrl) { this.updateUrl(); } // if there's a search url, do a query, otherwise call synchronise and draw directly if (this.search_url) { var onward = edges.objClosure(this, "cyclePart2"); this.doPrimaryQuery(onward); } else { this.cyclePart2(); } }; this.cyclePart2 = function() { var onward = edges.objClosure(this, "cyclePart3"); this.runSecondaryQueries(onward); }; this.cyclePart3 = function() { this.synchronise(); // pre-render trigger this.trigger("edges:pre-render"); // render this.draw(); // post render trigger this.trigger("edges:post-render"); // searching has completed, so flip the switch back this.searching = false; }; this.synchronise = function() { // ask the components to synchronise themselves with the latest state for (var i = 0; i < this.components.length; i++) { var component = this.components[i]; component.synchronise() } }; this.draw = function() { for (var i = 0; i < this.components.length; i++) { var component = this.components[i]; component.draw(this); } }; // reset the query to the start and re-issue the query this.reset = function() { // tell the world we're about to reset this.trigger("edges:pre-reset"); // clone from the opening query var requestedQuery = this.cloneOpeningQuery(); // request the components to contribute to the query for (var i = 0; i < this.components.length; i++) { var component = this.components[i]; component.contrib(requestedQuery); } // push the query, which will reconcile it with the baseQuery this.pushQuery(requestedQuery); // tell the world that we've done the reset this.trigger("edges:post-reset"); // now execute the query // this.doQuery(); this.cycle(); }; this.sleep = function() { for (var i = 0; i < this.components.length; i++) { var component = this.components[i]; component.sleep(); } }; this.wake = function() { for (var i = 0; i < this.components.length; i++) { var component = this.components[i]; component.wake(); } }; //////////////////////////////////////////////////// // functions for working with the queries this.cloneQuery = function() { if (this.currentQuery) { return $.extend(true, {}, this.currentQuery); } return false; }; this.pushQuery = function(query) { if (this.baseQuery) { query.merge(this.baseQuery); } this.currentQuery = query; }; this.cloneBaseQuery = function() { if (this.baseQuery) { return $.extend(true, {}, this.baseQuery); } return es.newQuery(); }; this.cloneOpeningQuery = function() { if (this.openingQuery) { return $.extend(true, {}, this.openingQuery); } return es.newQuery(); }; //////////////////////////////////////////////////// // functions to handle the query lifecycle // execute the query and all the associated workflow // FIXME: could replace this with an async group for neatness this.doPrimaryQuery = function(callback) { var context = {"callback" : callback}; this.queryAdapter.doQuery({ edge: this, success: edges.objClosure(this, "querySuccess", ["result"], context), error: edges.objClosure(this, "queryFail", ["response"], context) }); }; this.queryFail = function(params) { var callback = params.callback; var response = params.response; this.trigger("edges:query-fail"); if (response.hasOwnProperty("responseText")) { console.log("ERROR: query fail: " + response.responseText); } if (response.hasOwnProperty("error")) { console.log("ERROR: search execution fail: " + response.error); } callback(); }; this.querySuccess = function(params) { this.result = params.result; var callback = params.callback; // success trigger this.trigger("edges:query-success"); callback(); }; this.runPreflightQueries = function(callback) { if (!this.preflightQueries || Object.keys(this.preflightQueries).length == 0) { callback(); return; } this.trigger("edges:pre-preflight"); var entries = []; var ids = Object.keys(this.preflightQueries); for (var i = 0; i < ids.length; i++) { var id = ids[i]; entries.push({id: id, query: this.preflightQueries[id]}); } var that = this; var pg = edges.newAsyncGroup({ list: entries, action: function(params) { var entry = params.entry; var success = params.success_callback; var error = params.error_callback; es.doQuery({ search_url: that.search_url, queryobj: entry.query.objectify(), datatype: that.datatype, success: success, error: error }); }, successCallbackArgs: ["result"], success: function(params) { var result = params.result; var entry = params.entry; that.preflightResults[entry.id] = result; }, errorCallbackArgs : ["result"], error: function(params) { that.trigger("edges:error-preflight"); }, carryOn: function() { that.trigger("edges:post-preflight"); callback(); } }); pg.process(); }; this.runSecondaryQueries = function(callback) { this.realisedSecondaryQueries = {}; if (!this.secondaryQueries || Object.keys(this.secondaryQueries).length == 0) { callback(); return; } // generate the query objects to be executed var entries = []; for (var key in this.secondaryQueries) { var entry = {}; entry["query"] = this.secondaryQueries[key](this); entry["id"] = key; entry["search_url"] = this.search_url; if (this.secondaryUrls !== false && this.secondaryUrls.hasOwnProperty(key)) { entry["search_url"] = this.secondaryUrls[key] } entries.push(entry); this.realisedSecondaryQueries[key] = entry.query; } var that = this; var pg = edges.newAsyncGroup({ list: entries, action: function(params) { var entry = params.entry; var success = params.success_callback; var error = params.error_callback; es.doQuery({ search_url: entry.search_url, queryobj: entry.query.objectify(), datatype: that.datatype, success: success, complete: false }); }, successCallbackArgs: ["result"], success: function(params) { var result = params.result; var entry = params.entry; that.secondaryResults[entry.id] = result; }, errorCallbackArgs : ["result"], error: function(params) { // FIXME: not really sure what to do about this }, carryOn: function() { callback(); } }); pg.process(); }; //////////////////////////////////////////////// // various utility functions this.getComponent = function(params) { var id = params.id; for (var i = 0; i < this.components.length; i++) { var component = this.components[i]; if (component.id === id) { return component; } } return false; }; // return components in the requested category this.category = function(cat) { var comps = []; for (var i = 0; i < this.components.length; i++) { var component = this.components[i]; if (component.category === cat) { comps.push(component); } } return comps; }; this.getRenderPackObject = function(oname, params) { for (var i = 0; i < this.renderPacks.length; i++) { var rp = this.renderPacks[i]; if (rp && rp.hasOwnProperty(oname)) { return rp[oname](params); } } }; // get the jquery object for the desired element, in the correct context // you should ALWAYS use this, rather than the standard jquery $ object this.jq = function(selector) { return $(selector, this.context); }; this.trigger = function(event_name) { if (event_name in this.callbacks) { this.callbacks[event_name](this); } this.context.trigger(event_name); }; ///////////////////////////////////////////////////// // URL management functions this.getUrlParams = function() { return edges.getUrlParams(); }; this.urlQueryArg = function(objectify_options) { if (!objectify_options) { if (this.urlQueryOptions) { objectify_options = this.urlQueryOptions } else { objectify_options = { include_query_string : true, include_filters : true, include_paging : true, include_sort : true, include_fields : false, include_aggregations : false } } } var q = JSON.stringify(this.currentQuery.objectify(objectify_options)); var obj = {}; obj[this.urlQuerySource] = encodeURIComponent(q); return obj; }; this.fullQueryArgs = function() { var args = $.extend(true, {}, this.urlParams); $.extend(args, this.urlQueryArg()); return args; }; this.fullUrlQueryString = function() { return this._makeUrlQuery(this.fullQueryArgs()) }; this._makeUrlQuery = function(args) { var keys = Object.keys(args); var entries = []; for (var i = 0; i < keys.length; i++) { var key = keys[i]; var val = args[key]; entries.push(key + "=" + val); // NOTE we do not escape - this should already be done } return entries.join("&"); }; this.fullUrl = function() { var args = this.fullQueryArgs(); var fragment = ""; if (args["#"]) { fragment = "#" + args["#"]; delete args["#"]; } var wloc = window.location.toString(); var bits = wloc.split("?"); var url = bits[0] + "?" + this._makeUrlQuery(args) + fragment; return url; }; this.updateUrl = function() { var currentQs = window.location.search; var qs = "?" + this.fullUrlQueryString(); if (currentQs === qs) { return; // no need to push the state } var url = new URL(window.location.href); url.search = qs; if (currentQs === "") { window.history.replaceState("", "", url.toString()); } else { window.history.pushState("", "", url.toString()); } }; ///////////////////////////////////////////// // static file management this.loadStaticsAsync = function(callback) { if (!this.staticFiles || this.staticFiles.length == 0) { this.trigger("edges:post-load-static"); callback(); return; } var that = this; var pg = edges.newAsyncGroup({ list: this.staticFiles, action: function(params) { var entry = params.entry; var success = params.success_callback; var error = params.error_callback; var id = entry.id; var url = entry.url; var datatype = edges.getParam(entry.datatype, "text"); $.ajax({ type: "get", url: url, dataType: datatype, success: success, error: error }) }, successCallbackArgs: ["data"], success: function(params) { var data = params.data; var entry = params.entry; if (entry.processor) { var processed = entry.processor({data : data}); that.resources[entry.id] = processed; if (entry.opening) { entry.opening({resource : processed, edge: that}); } } that.static[entry.id] = data; }, errorCallbackArgs : ["data"], error: function(params) { that.errorLoadingStatic.push(params.entry.id); that.trigger("edges:error-load-static"); }, carryOn: function() { that.trigger("edges:post-load-static"); callback(); } }); pg.process(); }; ///////////////////////////////////////////// // final bits of construction this.startup(); }, ////////////////////////////////////////////////// // Asynchronous resource loading feature newAsyncGroup : function(params) { if (!params) { params = {} } return new edges.AsyncGroup(params); }, AsyncGroup : function(params) { this.list = params.list; this.successCallbackArgs = params.successCallbackArgs; this.errorCallbackArgs = params.errorCallbackArgs; var action = params.action; var success = params.success; var carryOn = params.carryOn; var error = params.error; this.functions = { action: action, success: success, carryOn: carryOn, error: error }; this.checkList = []; this.finished = false; this.construct = function(params) { for (var i = 0; i < this.list.length; i++) { this.checkList.push(0); } }; this.process = function(params) { if (this.list.length == 0) { this.functions.carryOn(); } for (var i = 0; i < this.list.length; i++) { var context = {index: i}; var success_callback = edges.objClosure(this, "_actionSuccess", this.successCallbackArgs, context); var error_callback = edges.objClosure(this, "_actionError", this.successCallbackArgs, context); var complete_callback = false; this.functions.action({entry: this.list[i], success_callback: success_callback, error_callback: error_callback, complete_callback: complete_callback }); } }; this._actionSuccess = function(params) { var index = params.index; delete params.index; params["entry"] = this.list[index]; this.functions.success(params); this.checkList[index] = 1; if (this._isComplete()) { this._finalise(); } }; this._actionError = function(params) { var index = params.index; delete params.index; params["entry"] = this.list[index]; this.functions.error(params); this.checkList[index] = -1; if (this._isComplete()) { this._finalise(); } }; this._actionComplete = function(params) { }; this._isComplete = function() { return $.inArray(0, this.checkList) === -1; }; this._finalise = function() { if (this.finished) { return; } this.finished = true; this.functions.carryOn(); }; //////////////////////////////////////// this.construct(); }, ///////////////////////////////////////////// // Query adapter base class and core ES implementation newQueryAdapter : function(params) { if (!params) { params = {} } return edges.instantiate(edges.QueryAdapter, params); }, QueryAdapter : function(params) { this.doQuery = function(params) {}; }, newESQueryAdapter : function(params) { if (!params) { params = {} } return edges.instantiate(edges.ESQueryAdapter, params); }, ESQueryAdapter : function(params) { this.doQuery = function(params) { var edge = params.edge; var query = params.query; var success = params.success; var error = params.error; if (!query) { query = edge.currentQuery; } es.doQuery({ search_url: edge.search_url, queryobj: query.objectify(), datatype: edge.datatype, success: success, error: error }); }; }, ///////////////////////////////////////////// // Base classes for the various kinds of components newRenderer : function(params) { if (!params) { params = {} } return new edges.Renderer(params); }, Renderer : function(params) { this.component = params.component || false; this.init = function(component) { this.component = component }; this.draw = function(component) {}; this.sleep = function() {}; this.wake = function() {} }, newComponent : function(params) { if (!params) { params = {} } return new edges.Component(params); }, Component : function(params) { this.id = params.id; this.renderer = params.renderer; this.category = params.category || "none"; this.defaultRenderer = params.defaultRenderer || "newRenderer"; this.init = function(edge) { // record a reference to the parent object this.edge = edge; this.context = this.edge.jq("#" + this.id); // set the renderer from default if necessary if (!this.renderer) { this.renderer = this.edge.getRenderPackObject(this.defaultRenderer); } if (this.renderer) { this.renderer.init(this); } }; this.draw = function() { if (this.renderer) { this.renderer.draw(); } }; this.contrib = function(query) {}; this.synchronise = function() {}; this.sleep = function() { if (this.renderer) { this.renderer.sleep(); } }; this.wake = function() { if (this.renderer) { this.renderer.wake(); } }; // convenience method for any renderer rendering a component this.jq = function(selector) { return this.edge.jq(selector); } }, newSelector : function(params) { if (!params) { params = {} } edges.Selector.prototype = edges.newComponent(params); return new edges.Selector(params); }, Selector : function(params) { // field upon which to build the selector this.field = params.field; // display name for the UI this.display = params.display || this.field; // whether the facet should be displayed at all (e.g. you may just want the data for a callback) this.active = params.active || true; // whether the facet should be acted upon in any way. This might be useful if you want to enable/disable facets under different circumstances via a callback this.disabled = params.disabled || false; this.category = params.category || "selector"; }, newTemplate : function(params) { if (!params) { params = {} } return new edges.Template(params); }, Template : function(params) { this.draw = function(edge) {} }, newNestedEdge : function(params) { if (!params) { params = {}} params.category = params.category || "edge"; params.renderer = false; params.defaultRenderer = false; return edges.instantiate(edges.NestedEdge, params, edges.newComponent) }, NestedEdge : function(params) { this.constructOnInit = edges.getParam(params.constructOnInit, false); this.constructArgs = edges.getParam(params.constructArgs, {}); this.inner = false; this.init = function(edge) { this.edge = edge; if (this.constructOnInit) { this.construct_and_bind(); } }; this.setConstructArg = function(key, value) { this.constructArgs[key] = value; }; this.getConstructArg = function(key, def) { if (this.constructArgs.hasOwnProperty(key)) { return this.constructArgs[key]; } return def; }; this.construct_and_bind = function() { this.construct(); if (this.inner) { this.inner.outer = this; } }; this.construct = function() {}; this.destroy = function() { if (this.inner) { this.inner.context.empty(); this.inner.context.hide(); } this.inner = false; }; this.sleep = function() { this.inner.sleep(); this.inner.context.hide(); }; this.wake = function() { if (this.inner) { this.inner.context.show(); this.inner.wake(); } else { this.construct_and_bind(); } } }, /////////////////////////////////////////////////////////// // Object construction tools // instantiate an object with the parameters and the (optional) // prototype instantiate : function(clazz, params, protoConstructor) { if (!params) { params = {} } if (protoConstructor) { clazz.prototype = protoConstructor(params); } var inst = new clazz(params); if (protoConstructor) { inst.__proto_constructor__ = protoConstructor; } return inst; }, // call a method on the parent class up : function(inst, fn, args) { var parent = new inst.__proto_constructor__(); parent[fn].apply(inst, args); }, ////////////////////////////////////////////////////////////////// // Closures for integrating the object with other modules // returns a function that will call the named function (fn) on // a specified object instance (obj), with all "arguments" // supplied to the closure by the caller // // if the args property is specified here, instead a parameters object // will be constructed with a one to one mapping between the names in args // and the values in the "arguments" supplied to the closure, until all // values in "args" are exhausted. // // so, for example, // // objClosure(this, "function")(arg1, arg2, arg3) // results in a call to // this.function(arg1, arg2, arg3, ...) // // and // objClosure(this, "function", ["one", "two"])(arg1, arg2, arg3) // results in a call to // this.function({one: arg1, two: arg2}) // objClosure : function(obj, fn, args, context_params) { return function() { if (args) { var params = {}; for (var i = 0; i < args.length; i++) { if (arguments.length > i) { params[args[i]] = arguments[i]; } } if (context_params) { params = $.extend(params, context_params); } obj[fn](params); } else { var slice = Array.prototype.slice; var theArgs = slice.apply(arguments); if (context_params) { theArgs.push(context_params); } obj[fn].apply(obj, theArgs); } } }, // returns a function that is suitable for triggering by an event, and which will // call the specified function (fn) on the specified object (obj) with the element // which fired the event as the argument // // if "conditional" is specified, this is a function (which can take the event as an argument) // which is called to determine whether the event will propagate to the object function. // // so, for example // // eventClosure(this, "handler")(event) // results in a call to // this.handler(element) // // and // // eventClosure(this, "handler", function(event) { return event.type === "click" })(event) // results in a call (only in the case that the event is a click), to // this.handler(element) // eventClosure : function(obj, fn, conditional, preventDefault) { if (preventDefault === undefined) { preventDefault = true; } return function(event) { if (conditional) { if (!conditional(event)) { return; } } if (preventDefault) { event.preventDefault(); } obj[fn](this, event); } }, ////////////////////////////////////////////////////////////////// // CSS normalising/canonicalisation tools css_classes : function(namespace, field, renderer) { var cl = namespace + "-" + field; if (renderer) { cl += " " + cl + "-" + renderer.component.id; } return cl; }, css_class_selector : function(namespace, field, renderer) { var sel = "." + namespace + "-" + field; if (renderer) { sel += sel + "-" + renderer.component.id; } return sel; }, css_id : function(namespace, field, renderer) { var id = namespace + "-" + field; if (renderer) { id += "-" + renderer.component.id; } return id; }, css_id_selector : function(namespace, field, renderer) { return "#" + edges.css_id(namespace, field, renderer); }, ////////////////////////////////////////////////////////////////// // Event binding utilities on : function(selector, event, caller, targetFunction, delay, conditional, preventDefault) { if (preventDefault === undefined) { preventDefault = true; } // if the caller has an inner component (i.e. it is a Renderer), use the component's id // otherwise, if it has a namespace (which is true of Renderers or Templates) use that if (caller.component && caller.component.id) { event = event + "." + caller.component.id; } else if (caller.namespace) { event = event + "." + caller.namespace; } // create the closure to be called on the event var clos = edges.eventClosure(caller, targetFunction, conditional, preventDefault); // now bind the closure directly or with delay // if the caller has an inner component (i.e. it is a Renderer) use the components jQuery selector // otherwise, if it has an inner, use the selector on that. if (delay) { if (caller.component) { caller.component.jq(selector).bindWithDelay(event, clos, delay); } else if (caller.edge) { caller.edge.jq(selector).bindWithDelay(event, clos, delay); } else { $(selector).bindWithDelay(event, clos, delay); } } else { if (caller.component) { var element = caller.component.jq(selector); element.off(event); element.on(event, clos); } else if (caller.edge) { var element = caller.edge.jq(selector); element.off(event); element.on(event, clos); } else { var element = $(selector); element.off(event); element.on(event, clos); } } }, off : function(selector, event, caller) { // if the caller has an inner component (i.e. it is a Renderer), use the component's id // otherwise, if it has a namespace (which is true of Renderers or Templates) use that if (caller.component && caller.component.id) { event = event + "." + caller.component.id; } else if (caller.namespace) { event = event + "." + caller.namespace; } if (caller.component) { var element = caller.component.jq(selector); element.off(event); } else if (caller.edge) { var element = caller.edge.jq(selector); element.off(event); } else { var element = $(selector); element.off(event); } }, ////////////////////////////////////////////////////////////////// // Shared utilities getUrlParams : function() { var params = {}; var url = window.location.href; var fragment = false; // break the anchor off the url if (url.indexOf("#") > -1) { fragment = url.slice(url.indexOf('#')); url = url.substring(0, url.indexOf('#')); } // extract and split the query args var args = url.slice(url.indexOf('?') + 1).split('&'); for (var i = 0; i < args.length; i++) { var kv = args[i].split('='); if (kv.length === 2) { var key = kv[0].replace(/\+/g, "%20"); key = decodeURIComponent(key); var val = kv[1].replace(/\+/g, "%20"); val = decodeURIComponent(val); if (val[0] == "[" || val[0] == "{") { // if it looks like a JSON object in string form... // remove " (double quotes) at beginning and end of string to make it a valid // representation of a JSON object, or the parser will complain val = val.replace(/^"/,"").replace(/"$/,""); val = JSON.parse(val); } params[key] = val; } } // record the fragment identifier if required if (fragment) { params['#'] = fragment; } return params; }, escapeHtml : function(unsafe, def) { if (def === undefined) { def = ""; } if (unsafe === undefined || unsafe == null) { return def; } try { if (typeof unsafe.replace !== "function") { return unsafe } return unsafe .replace(/&/g, "&") .replace(/</g, "<") .replace(/>/g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } catch(err) { return def; } }, /** * Determine if the object has a property at the given path in the object * * @param obj * @param path * @returns {boolean} */ hasProp : function(obj, path) { var bits = path.split("."); var ctx = obj; for (var i = 0; i < bits.length; i++) { if (!ctx.hasOwnProperty(bits[i])) { return false; } ctx = ctx[bits[i]]; } return true; }, /** * Get the value of an element at the given path in an object * * @param path * @param rec * @param def * @returns {*} */ objVal : function(path, rec, def) { if (def === undefined) { def = false; } var bits = path.split("."); var val = rec; for (var i = 0; i < bits.length; i++) { var field = bits[i]; if (field in val) { val = val[field]; } else { return def; } } return val; }, /** * The same as objVal but will handle arrays at every level and condense the * results into a single array result. Always returns an array, even if it is * a single result, OR returns a default, which is returned exactly as specified * * @param path * @param rec * @param def * @returns {*} */ objVals : function(path, rec, def) { if (def === undefined) { def = false; } var bits = path.split("."); var contexts = [rec]; for (var i = 0; i < bits.length; i++) { var pathElement = bits[i]; var nextContexts = []; for (var j = 0; j < contexts.length; j++) { var context = contexts[j]; if (pathElement in context) { var nextLevel = context[pathElement]; if (i === bits.length - 1) { // if this is the last path element, then // we make the assumption that this is a well defined leaf node, for performance purposes nextContexts.push(nextLevel); } else { // there are more path elements to retrieve, so we have to handle the various types if ($.isArray(nextLevel)) { nextContexts = nextContexts.concat(nextLevel); } else if ($.isPlainObject(nextLevel)) { nextContexts.push(nextLevel); } // if the value is a leaf node already then we throw it away } } } contexts = nextContexts; } if (contexts.length === 0) { return def; } return contexts; }, getParam : function(value, def) { return value !== undefined ? value : def; }, safeId : function(unsafe) { return unsafe.replace(/&/g, "_") .replace(/</g, "_") .replace(/>/g, "_") .replace(/"/g, "_") .replace(/'/g, "_") .replace(/\./gi,'_') .replace(/\:/gi,'_') .replace(/\s/gi,"_"); }, numFormat : function(params) { var reflectNonNumbers = edges.getParam(params.reflectNonNumbers, false); var prefix = edges.getParam(params.prefix, ""); var zeroPadding = edges.getParam(params.zeroPadding, false); var decimalPlaces = edges.getParam(params.decimalPlaces, false); var thousandsSeparator = edges.getParam(params.thousandsSeparator, false); var decimalSeparator = edges.getParam(params.decimalSeparator, "."); var suffix = edges.getParam(params.suffix, ""); return function(number) { // ensure this is really a number var num = parseFloat(number); if (isNaN(num)) { if (reflectNonNumbers) { return number; } else { return num; } } // first off we need to convert the number to a string, which we can do directly, or using toFixed if that // is suitable here if (decimalPlaces !== false) { num = num.toFixed(decimalPlaces); } else { num = num.toString(); } // now "num" is a string containing the formatted number that we can work on var bits = num.split("."); if (zeroPadding !== false) { var zeros = zeroPadding - bits[0].length; var pad = ""; for (var i = 0; i < zeros; i++) { pad += "0"; } bits[0] = pad + bits[0]; } if (thousandsSeparator !== false) { bits[0] = bits[0].replace(/\B(?=(\d{3})+(?!\d))/g, thousandsSeparator); } if (bits.length == 1) { return prefix + bits[0] + suffix; } else { return prefix + bits[0] + decimalSeparator + bits[1] + suffix; } } }, numParse : function(params) { var commaRx = new RegExp(",", "g"); return function(num) { num = num.trim(); num = num.replace(commaRx, ""); if (num === "") { return 0.0; } return parseFloat(num); } }, isEmptyObject: function(obj) { for(var key in obj) { if(obj.hasOwnProperty(key)) return false; } return true; } };