From c084553d05c12b7e9dc897bf1df9772dd095d0fb Mon Sep 17 00:00:00 2001 From: Antonio Garrote Date: Mon, 28 Jun 2010 09:48:29 +0200 Subject: [PATCH] Javascript client library --- js/css/plaza-ui.css | 129 +++++ js/plaza-ui.js | 765 +++++++++++++++++++++++++++ js/plaza.js | 843 ++++++++++++++++++++++++++++++ js/tests/plaza_test.html | 625 ++++++++++++++++++++++ js/tests/qunit.css | 119 +++++ js/tests/qunit.js | 1069 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 3550 insertions(+) create mode 100644 js/css/plaza-ui.css create mode 100644 js/plaza-ui.js create mode 100644 js/plaza.js create mode 100644 js/tests/plaza_test.html create mode 100644 js/tests/qunit.css create mode 100644 js/tests/qunit.js diff --git a/js/css/plaza-ui.css b/js/css/plaza-ui.css new file mode 100644 index 0000000..d0bbbab --- /dev/null +++ b/js/css/plaza-ui.css @@ -0,0 +1,129 @@ +.plaza-widget{ + margin: 0; + padding: 0; + border: 0; + outline: 0; + font-weight: normal; + font-style: normal; + font-family: Tahoma, Geneva, arial, sans-serif; + vertical-align: baseline; + background-color:#eeeeee; + border:2px solid #aaaaaa; + margin: 5px +} + +.widget-header{ + background-color: black; + font-size: 120%; + color: white; + padding: 10px +} + + +/* entity debugger */ +.entity-debugger-widget{ + padding: 0px; + font-size: 70% +} + +.entity-debugger-widget-header{ + font-size: 120% +} + +.entity-debugger-widget-prop{ + background: white; + border:1px solid #aaaaaa; + margin: 3px; +} + +.entity-debugger-widget-prop-alias{ + padding: 3px; + margin-bottom: 3px; + background-color: #aaaaaa; + font-style: bold +} + +.entity-debugger-widget-prop-uri{ + padding: 3px +} + +.entity-debugger-widget-prop-val{ + padding: 3px +} + +/* TripleSpacesBrowser */ +.triple-spaces-browser-widget { + font-size: 70% +} + +.triple-space-browser-panel { + background: white; + border:1px solid #aaaaaa; + margin: 3px +} + +.triple-space-browser-panel-header { + background: #aaaaaa; + padding: 3px +} + +.triple-space-browser-panel-body { + text-align: right +} + +.triple-space-actions { + padding: 5px +} + +.triple-space-browser-panel-body input { + width: 350px; + margin: 5px +} + +.collection-triple-space-actions { + padding: 5px +} + +.overline { + border-top:1px solid #aaaaaa; + font-size: 80% +} + +/* Toolbox */ +.plaza-toolbox-widget { + font-size: 70% +} + +.toolbox-legend-fld { + width: 350px; + margin-left: 20px +} + +.entity-search-fld { + width: 450px; + margin-right: 20px +} + +.schema-uri-fld { + width: 450px; + margin-right: 10px; +} + +/* TriplesTable */ +.plaza-triples-table-widget { + font-size: 70% +} + +.plaza-triples-table-widget table { + width: 100% +} + +.plaza-triples-table-widget thead { + background-color: #E7EEF6; + font-weight: bold +} + +.plaza-triples-table-widget td { + background-color: white; + padding: 3px +} diff --git a/js/plaza-ui.js b/js/plaza-ui.js new file mode 100644 index 0000000..ba2254c --- /dev/null +++ b/js/plaza-ui.js @@ -0,0 +1,765 @@ +/** + * + * Plaza JS UI library + * @author Antonio Garrote + * @date 26.06.2010 + * + */ + +PlazaUI = {}; + +/** + * PlazaUI.Base + */ +PlazaUI.Base = function(){ + var base = { + build: function() { + this.__plz_id = "plz_"+PlazaUI._widgetCounter++; + }, + + extend: function() { + var F = function(){}; + F.prototype = this; + var w = new F(); + w.build(); + return w; + } + }; + + return base; +}(); + +// Counter for the widgets built +PlazaUI._widgetCounter = 0; + + +/** + * PlazaUI.EntityWidget + */ +PlazaUI.EntityWidget = function(){ + var w = PlazaUI.Base.extend(); + + w.build = function() { + PlazaUI.Base.build.apply(this,[]); + + this.entity = null; + this.connectedTo = null; + this.connections = []; + this.handlers = {} + }; + + w.updatedAdapter = function(uri, event, data){ + var fh = this.handlers["onUpdate"]; + fh.apply(this,[data]); + }; + + w.destroyedAdapter = function(uri, event, data){ + var fh = this.handlers["onDestroy"]; + fh.apply(this,[data]); + this.entity = null; + }; + + w.setEntity = function(uriOrEntity) { + var uri = uriOrEntity; + var ent = uriOrEntity; + + if(typeof(uriOrEntity) == "string") { + ent = Plaza.ABox.entitiesRegistry[uriOrEntity]; + } else { + uri = ent._uri; + } + + if(this.entity != null) { + Plaza.ABox.stopObservingEntity(this.entity._uri, Plaza.ABox.EVENTS.UPDATED, this); + Plaza.ABox.stopObservingEntity(this.entity._uri, Plaza.ABox.EVENTS.DESTROYED, this); + } + + this.entity = ent + Plaza.ABox.startObservingEntity(uri, Plaza.ABox.EVENTS.UPDATED, this, this.updatedAdapter); + Plaza.ABox.startObservingEntity(uri, Plaza.ABox.EVENTS.DESTROYED, this, this.destroyedAdapter); + + this.updatedAdapter(this.entity._uri, Plaza.ABox.EVENTS.UPDATED, this.entity); + + this.disconnect(); + + for(var i in this.connections) { + var c = this.connections[i]; + c.connectionChangeAdapter(this.entity); + } + }; + + w.connectionChangeAdapter = function(entity) { + if(this.entity != null) { + Plaza.ABox.stopObservingEntity(this.entity._uri, Plaza.ABox.EVENTS.UPDATED, this); + Plaza.ABox.stopObservingEntity(this.entity._uri, Plaza.ABox.EVENTS.DESTROYED, this); + } + + this.entity = entity; + this.updatedAdapter(this.entity._uri, Plaza.ABox.EVENTS.UPDATED, this.entity); + + if(entity != null) { + Plaza.ABox.startObservingEntity(entity._uri, Plaza.ABox.EVENTS.UPDATED, this, this.updatedAdapter); + Plaza.ABox.startObservingEntity(entity._uri, Plaza.ABox.EVENTS.DESTROYED, this, this.destroyedAdapter); + } + + for(var i in this.connections) { + var c = this.connections[i]; + c.connectionChangeAdapter(entity); + } + }; + + w.connect = function(widget) { + if(this.entity != null) { + Plaza.ABox.stopObservingEntity(this.entity._uri, Plaza.ABox.EVENTS.UPDATED, this); + Plaza.ABox.stopObservingEntity(this.entity._uri, Plaza.ABox.EVENTS.DESTROYED, this); + } + + this.entity = widget.entity; + this.updatedAdapter(this.entity); + + if(this.entity != null) { + Plaza.ABox.startObservingEntity(entity._uri, Plaza.ABox.EVENTS.UPDATED, this, this.updatedAdapter); + Plaza.ABox.startObservingEntity(entity._uri, Plaza.ABox.EVENTS.DESTROYED, this, this.destroyedAdapter); + } + + for(var i in this.connections) { + var c = this.connections[i]; + c.connectionChangeAdapter(entity); + } + + this.disconnect(); + + this.connectedTo = widget; + widget.connections.push(this); + }; + + w.disconnectConnection = function(otherWidget) { + var newConnections = []; + for(var i in this.connections) { + if(this.connections[i] != otherWidget) { + newConnections.push(this.connections[i]); + } + } + this.connections = newConnections; + } + + w.disconnect = function() { + if(this.connectedTo != null) { + this.connectedTo.disconnectConnection(this); + } + }; + + return w; +}(); + + +/** + * PlazaUI.SpaceWidget + */ +PlazaUI.SpaceWidget = function(){ + var w = PlazaUI.Base.extend(); + + w.build = function() { + PlazaUI.Base.build.apply(this,[]); + + this.space = null; + this.connectedTo = null; + this.connections = []; + this.handlers = {} + }; + + w.createdAdapter = function(spaceId, event, data){ + var fh = this.handlers["onCreate"]; + fh.apply(this,[data]); + }; + + w.updatedAdapter = function(spaceId, event, data){ + var fh = this.handlers["onUpdate"]; + fh.apply(this,[data]); + }; + + w.destroyedAdapter = function(spaceId, event, data){ + var fh = this.handlers["onDestroy"]; + fh.apply(this,[data]); + this.entity = null; + }; + + w.setSpace = function(idOrSpace) { + var id = idOrSpace; + var ent = idOrSpace; + + if(typeof(idOrSpace) == "string") { + ent = Plaza.ABox.spacesRegistry[idOrSpace]; + } else { + id = ent.id; + } + + if(this.space != null) { + Plaza.ABox.stopObservingSpace(this.space.id, Plaza.ABox.EVENTS.CREATED, this); + Plaza.ABox.stopObservingSpace(this.space.id, Plaza.ABox.EVENTS.UPDATED, this); + Plaza.ABox.stopObservingSpace(this.space.id, Plaza.ABox.EVENTS.DESTROYED, this); + } + + this.space = ent + Plaza.ABox.startObservingSpace(id, Plaza.ABox.EVENTS.CREATED, this, this.createdAdapter); + Plaza.ABox.startObservingSpace(id, Plaza.ABox.EVENTS.UPDATED, this, this.updatedAdapter); + Plaza.ABox.startObservingSpace(id, Plaza.ABox.EVENTS.DESTROYED, this, this.destroyedAdapter); + + if (this.space != null) { + for(var i in this.space.entities) { + var e = this.space.entities[i]; + this.createdAdapter(this.space.id, Plaza.ABox.EVENTS.CREATED, e.value); + } + } + + this.disconnect(); + + for(var i in this.connections) { + var c = this.connections[i]; + c.connectionChangeAdapter(this.space); + } + }; + + w.connectionChangeAdapter = function(space) { + if(this.space != null) { + Plaza.ABox.stopObservingSpace(this.space.id, Plaza.ABox.EVENTS.CREATED, this); + Plaza.ABox.stopObservingSpace(this.space.id, Plaza.ABox.EVENTS.UPDATED, this); + Plaza.ABox.stopObservingSpace(this.space.id, Plaza.ABox.EVENTS.DESTROYED, this); + } + + if(this.space != null) { + for(var i in this.space.entities) { + var e = this.space.entities[i]; + this.destroyedAdapter(space.id, Plaza.ABox.EVENTS.DESTROYED, e.value); + } + } + + this.space = space; + + if(space != null) { + + for(var i in this.space.entities) { + var e = this.space.entities[i]; + this.createdAdapter(this.space.id, Plaza.ABox.EVENTS.CREATED, e.value); + } + + Plaza.ABox.startObservingSpace(this.space.id, Plaza.ABox.EVENTS.CREATED, this, this.createdAdapter); + Plaza.ABox.startObservingSpace(this.space.id, Plaza.ABox.EVENTS.UPDATED, this, this.updatedAdapter); + Plaza.ABox.startObservingSpace(this.space.id, Plaza.ABox.EVENTS.DESTROYED, this, this.destroyedAdapter); + } + + for(var i in this.connections) { + var c = this.connections[i]; + c.connectionChangeAdapter(this.space); + } + }; + + w.connect = function(widget) { + if(this.space != null) { + for(var i in this.space.entities) { + var e = this.space.entities[i]; + this.destroyedAdapter(this.space.id, Plaza.ABox.EVENTS.DESTROYED, e.value); + } + + Plaza.ABox.stopObservingSpace(this.space.id, Plaza.ABox.EVENTS.CREATED, this); + Plaza.ABox.stopObservingSpace(this.space.id, Plaza.ABox.EVENTS.UPDATED, this); + Plaza.ABox.stopObservingSpace(this.space.id, Plaza.ABox.EVENTS.DESTROYED, this); + } + + this.space = widget.space; + this.updatedAdapter(this.space); + + if(this.space != null) { + for(var i in this.space.entities) { + var e = this.space.entities[i]; + this.createdAdapter(this.space.id, Plaza.ABox.EVENTS.CREATED, e.value); + } + + Plaza.ABox.startObservingSpace(this.space.id, Plaza.ABox.EVENTS.CREATED, this, this.createdAdapter); + Plaza.ABox.startObservingSpace(space.id, Plaza.ABox.EVENTS.UPDATED, this, this.updatedAdapter); + Plaza.ABox.startObservingSpace(space.id, Plaza.ABox.EVENTS.DESTROYED, this, this.destroyedAdapter); + } + + for(var i in this.connections) { + var c = this.connections[i]; + c.connectionChangeAdapter(space); + } + + this.disconnect(); + + this.connectedTo = widget; + widget.connections.push(this); + }; + + w.disconnectConnection = function(otherWidget) { + var newConnections = []; + for(var i in this.connections) { + if(this.connections[i] != otherWidget) { + newConnections.push(this.connections[i]); + } + } + this.connections = newConnections; + } + + w.disconnect = function() { + if(this.connectedTo != null) { + this.connectedTo.disconnectConnection(this); + } + }; + + return w; +}(); + + +/** + * PlazaUI.Widgets namespace + */ +PlazaUI.Widgets = { + makeClosable: function(widget) { + widget.find(".widget-header").append(''); + widget.find('.remove-widget').bind("click", function(){ + widget.remove(); + }); + } +}; + +/** + * PlazaUI.Widgets.EntityDebugger + */ + +PlazaUI.Widgets.EntityDebugger = function(){ + + var w = PlazaUI.EntityWidget.extend(); + + // Element where the widget will be inserted + w.attachedElement = null; + + w.build = function() { + PlazaUI.EntityWidget.build.apply(this,[]) + this.handlers = {}; + this.handlers["onUpdate"] = w.onUpdated; + this.handlers["onDestroy"] = w.onDestroyed; + }, + + // Insertes the widget in the DOM + w.attach = function() { + var elem = null; + if( arguments.length == 0) { + elem = jQuery("body"); + } else { + elem = arguments[0]; + }; + + this.attachedElement = jQuery("
"); + this.attachedElement.append("
"); + this.attachedElement.append("
"); + + if(this.entity == null) { + this.onDestroyed(null); + } else { + this.onUpdated(this.entity); + } + + elem.append(this.attachedElement); + + // UI tweaks + if(this.entity == null) { + this.attachedElement.css("width",300) + } else { + this.attachedElement.css("width",this.entity._uri.length * 8) + } + + var attached = this.attachedElement; + PlazaUI.Widgets.makeClosable(attached); + attached.resizable(); + this.attachedElement.find(".entity-debugger-widget-header").bind('mousedown',function(event){ + attached.draggable(); + }); + this.attachedElement.find(".entity-debugger-widget-header").bind('mouseup',function(event){ + attached.draggable("destroy"); + }); + }; + + w.onUpdated = function(entity) { + if(this.attachedElement != null) { + if(entity != null) { + this.attachedElement.find(".entity-debugger-widget-header").html(entity._uri); + this.attachedElement.find(".entity-debugger-widget-body").empty(); + + for(var p in entity) { + var val = "
" + p + "
"; + var uri = Plaza.TBox.findPropertyUri(p); + if(uri != undefined) { + val = val + "
uri: " + uri + "
" + } + val = val + "
value: " + entity[p] + "
" + var props = jQuery(val); + + this.attachedElement.find(".entity-debugger-widget-body").append(props); + } + } + } + }; + + w.onDestroyed = function(entity) { + if(this.attachedElement != null) { + this.attachedElement.find(".entity-debugger-widget-header").html(); + this.attachedElement.find(".entity-debugger-widget-body").empty(); + } + }; + + return w; +}(); + +/** + * PlazaUI.Widgets.TripleSpacesBrowser + */ +PlazaUI.Widgets.TripleSpacesBrowser = function(){ + + var w = PlazaUI.EntityWidget.extend(); + + w.build = function() { + PlazaUI.EntityWidget.build.apply(this,[]) + }; + + var makeSpacePanel = function(id, space){ + var spaceId = space.id; + var spaceSingleResource = space.singleResource; + var spaceCollectionResource = space.collectionResource; + + var panel = jQuery("
" + spaceId + "
"); + + if(spaceSingleResource != null) { + var spaceUri = Plaza.Services.servicesRegistry[spaceSingleResource]; + panel.find(".triple-space-browser-panel-body").append(jQuery("
")); + } + + if(spaceCollectionResource != null) { + var spaceUri = Plaza.Services.servicesRegistry[spaceCollectionResource]; + panel.find(".triple-space-browser-panel-body").append(jQuery("
")); + } + + var actions = "
" + panel.append(jQuery(actions)); + panel.find(".triple-space-show-triples-btn").bind("click", function() { + var w = PlazaUI.Widgets.TriplesTable.extend(); + w.attach(); + w.setSpace(spaceId); + }); + + if(spaceCollectionResource != null) { + var collectionResourceActions = "

collection resource actions

" + panel.append(jQuery(collectionResourceActions)); + + panel.find(".collection-triple-space-actions").bind("click",function() { + Plaza.ABox.loadInstances(spaceId, {}); + }); + } + + return panel; + }; + + // Inserts the widget in the DOM + w.attach = function() { + var elem = null; + if( arguments.length == 0) { + elem = jQuery("body"); + } else { + elem = arguments[0]; + }; + + var attachedElement = jQuery("
"); + attachedElement.append(jQuery("
Registered Triple Spaces
")); + + for (var i in Plaza.ABox.spacesRegistry) { + var panel = makeSpacePanel(i,Plaza.ABox.spacesRegistry[i]); + attachedElement.append(panel); + } + + elem.append(attachedElement); + + // UI tweaks + attachedElement.css("width",500) + + PlazaUI.Widgets.makeClosable(attachedElement); + attachedElement.resizable(); + attachedElement.find(".triple-spaces-browser-header").bind('mousedown',function(event){ + attachedElement.draggable(); + }); + attachedElement.find(".triple-spaces-browser-header").bind('mouseup',function(event){ + attachedElement.draggable("destroy"); + }); + }; + + return w; +}(); + + +/** + * PlazaUI.Widgets.Toolbox + */ + +PlazaUI.Widgets.Toolbox = function(){ + + var w = PlazaUI.Base.extend(); + + w.build = function() { + PlazaUI.Base.build.apply(this,[]) + }; + + var _addFeature = function(container, header, cls, featureBody) { + var body = container.find(".toolbox-widget-body") + + var elmt = "

" + header + "

"; + var panel = jQuery(elmt) + + body.append(panel); + + var elmt = "
" + var panel = jQuery(elmt); + panel.append(featureBody); + + body.append(panel); + }; + + + var makeSearchTriples = function() { + var code = "
" + var elem = jQuery(code) + + elem.find(".entity-search-fld").autocomplete({source: function(term,callback) { + var matches = []; + for(var uri in Plaza.ABox.entitiesRegistry) { + if(uri.indexOf(term.term) != -1 || term == "") { + matches.push(uri); + } + } + callback(matches); + }}); + + elem.find(".entitySearchBtn").bind("click", function(event) { + var uri = elem.find(".entity-search-fld").val() + var w = PlazaUI.Widgets.EntityDebugger.extend(); + w.setEntity(Plaza.ABox.findEntityByURI(uri)); + w.attach(); + }); + return elem; + }; + + var makeLoadSchemas = function() { + var code = "
" + var elem = jQuery(code) + + elem.find(".schema-load-btn").bind("click", function(event) { + var uri = elem.find(".schema-uri-fld").val(); + if(uri != "") { + Plaza.TBox.registerSchema(uri, function(loadedUri){ + var dialog = jQuery("

The schema located at " + loadedUri + " has been loaded successfully

"); + jQuery("body").append(dialog); + dialog.dialog(); + }); + } else { + alert("A URI must be provided to load schema information"); + } + }); + return elem; + }; + + var makeInspectTBox = function() { + var code = "
"; + code = code + "
"; + code = code + "
"; + + var elem = jQuery(code); + + elem.find('.browse-services-btn').bind("click", function(){ + var w = PlazaUI.Widgets.TripleSpacesBrowser.extend(); + w.attach(); + }); + + return elem; + }; + + var makeConnectToService = function() { + var code = "
" + code = code + "
"; + code = code + "
" + code = code + "
" + code = code + "
"; + var elem = jQuery(code) + + elem.find(".service-connect-btn").bind("click", function(event) { + var tsId = elem.find(".ts-id-fld").val(); + var uriSingle = elem.find(".single-service-fld").val(); + var uriCollection = elem.find(".collection-service-fld").val(); + + if(tsId == "") { + alert("A name for the triple space must be provided"); + } else { + if(uriSingle == "" && uriCollection == "") { + alert("URIs for single resource, collection resource or both of them must be provided"); + } else { + if(Plaza.ABox.spacesRegistry[tsId] != null) { + alert("The name for the triple space is already in use"); + } else { + var params = {} + if(uriCollection != "") { + params["singleResource"] = uriSingle; + } + if(uriSingle != "") { + params["collectionResource"] = uriCollection; + } + elem.find(".ts-id-fld").val("") + elem.find(".single-service-fld").val(""); + elem.find(".collection-service-fld").val(""); + + Plaza.ABox.TripleSpace.connect(tsId, params, function() { + var dialog = jQuery("

The service " + tsId + " has been registered successfully

"); + jQuery("body").append(dialog); + dialog.dialog(); + }); + } + } + } + }); + return elem; + } + + // Insertes the widget in the DOM + w.attach = function() { + var elem = null; + if( arguments.length == 0) { + elem = jQuery("body"); + } else { + elem = arguments[0]; + }; + + this.attachedElement = jQuery("
"); + this.attachedElement.append("
( plaza ) toolbox
"); + this.attachedElement.append("
"); + + _addFeature(this.attachedElement, "Schemas connection", "", makeLoadSchemas()); + _addFeature(this.attachedElement, "Services connection", "", makeConnectToService()); + _addFeature(this.attachedElement, "Search entity", "", makeSearchTriples()); + _addFeature(this.attachedElement, "Inspect tbox", "", makeInspectTBox()); + + elem.append(this.attachedElement); + this.attachedElement.find(".toolbox-widget-body").accordion({collapsible: true, active: false}); + + // UI tweaks + var attached = this.attachedElement; + PlazaUI.Widgets.makeClosable(attached); + this.attachedElement.css("width",600) + this.attachedElement.find(".widget-header").bind('mousedown',function(event){ + attached.draggable(); + }); + this.attachedElement.find(".widget-header").bind('mouseup',function(event){ + attached.draggable("destroy"); + }); + }; + + return w; +}(); + + +/** + * PlazaUI.Widgets.TriplesTable + */ + +PlazaUI.Widgets.TriplesTable = function(){ + + var w = PlazaUI.SpaceWidget.extend(); + + // Element where the widget will be inserted + w.attachedElement = null; + + w.build = function() { + PlazaUI.SpaceWidget.build.apply(this,[]) + this.handlers = {}; + this.handlers["onCreate"] = w.onCreated; + this.handlers["onUpdate"] = w.onUpdated; + this.handlers["onDestroy"] = w.onDestroyed; + this.rowsCounter = 0; + // uri -> id + this.rowsMap = {}; + }; + + w.onCreated = function(data){ + var titleId = "#plaza-triples-table-"+this.__plz_id; + jQuery(titleId).html(this.space.id); + + if(this.attachedElement != null) { + var rowClass = "row-" + this.rowsCounter++; + var subject = data._uri; + for(var p in data) { + if(p != "_uri") { + var predicate = Plaza.TBox.findPropertyUri(p); + if(predicate == null) { + predicate = p; + } + var object = data[p]; + var txt = "" + subject + "" + predicate + "" + object + ""; + this.attachedElement.find("tbody").append(jQuery(txt)); + } + } + } + }; + + + w.onUpdated = function(){ + if(this.attachedElement != null) { + // @todo + } + }; + + w.onDestroyed = function() { + if(this.attachedElement != null) { + // @todo + } + } + + // Insertes the widget in the DOM + w.attach = function() { + var elem = null; + if( arguments.length == 0) { + elem = jQuery("body"); + } else { + elem = arguments[0]; + }; + + var spaceId = ""; + if(this.space != null) { + spaceId = this.space.id; + } + + this.attachedElement = jQuery("
"); + this.attachedElement.append("
" + spaceId + "
"); + this.attachedElement.append("
subjectpredicateobject
"); + + + elem.append(this.attachedElement); + + if(this.space != null) { + for(var i in this.space.elements) { + var elt = this.space.elements[i]; + + this.createdAdapter(this.space.id, Plaza.ABox.EVENTS.CREATED, elt.value) + } + } + + // UI tweaks + var attached = this.attachedElement; + + PlazaUI.Widgets.makeClosable(attached); + + this.attachedElement.css("width",1000) + this.attachedElement.find(".widget-header").bind('mousedown',function(event){ + attached.draggable(); + }); + this.attachedElement.find(".widget-header").bind('mouseup',function(event){ + attached.draggable("destroy"); + }); + }; + + return w; +}(); diff --git a/js/plaza.js b/js/plaza.js new file mode 100644 index 0000000..12d55b0 --- /dev/null +++ b/js/plaza.js @@ -0,0 +1,843 @@ +/* + * + * Plaza JS client library and utilities + * @author Antonio Garrote + * @date 23.06.2010 + * + */ + +Plaza = { + + // Sequences a list of currified functions + setup: function() { + var fns = arguments; + var cdr = []; + var car = null + + var fnsLength = fns.length; + + for(var i=0; i "test" +// {"value":"25","datatype":"http://www.w3.org/2001/XMLSchema#int"} -> 25 +// "25^^http://www.w3.org/2001/XMLSchema#int"} -> 25 +Plaza.XSD.parseType= function(uri) { + var uriObj = null; + + if(typeof(uri) == "string") { + var parts = uri.split("^^"); + uriObj = { value: parts[0], datatype: parts[1] }; + } else { + uriObj = uri; + } + + var alias = Plaza.XSD.DATATYPES_INV[uriObj.datatype]; + return Plaza.XSD.DATATYPES_PARSERS[alias](uriObj); +} + + +/* Utils */ +Plaza.Utils = { + + /** + * Registers a new namespace in the javascript + * runtime. + */ + registerNamespace: function() { + var nsPath = ""; + for (var i=0; i something + * - http://test.com/something/else -> else + * - http://test.com/something/else/ -> else + * - http://test.com/something#more -> more + */ + extractQLocal: function(uri) { + if(uri.indexOf("#") != -1) { + var parts = uri.split("#"); + return parts[parts.length - 1]; + } else { + var parts = uri.split("/"); + if(parts[parts.length - 1] == "") { + return parts[parts.length - 2]; + } else { + return parts[parts.length - 1]; + } + } + }, + + cleanTypedLiteral: function(typed) { + var parts = typed.split("^^"); + var content = parts[0]; + var datatype = parts[1]; + + if(content[0] == '"' && content[content.length-1] == '"') { + content = content.substring(1, content.length-1); + } + if(datatype[0] == '<' && datatype[datatype.length-1] == '>') { + datatype = datatype.substring(1, datatype.length-1); + } + + return {"value": content, "datatype": datatype}; + } + +}; + +/* TBox */ +Plaza.TBox = { + + // Map URI -> alias + classesMap: {}, + + // Map alias -> URI + classesRegistry: {}, + + // Map URI -> definition + propertiesMap: {"http://www.w3.org/1999/02/22-rdf-syntax-ns#type": {"uri": "http://www.w3.org/1999/02/22-rdf-syntax-ns#type", + "domain": "http://www.w3.org/2000/01/rdf-schema#Resource", + "range": "http://www.w3.org/2000/01/rdf-schema#Resource" }, + + "http://plaza.org/vocabularies/restResourceId": {"uri": "http://plaza.org/vocabularies/restResourceId", + "domain": "http://www.w3.org/2000/01/rdf-schema#Resource", + "range": "http://www.w3.org/2000/01/rdf-schema#Resource"}}, + + // Map alias -> URI + propertiesRegistry: {"rdf_type": "http://www.w3.org/1999/02/22-rdf-syntax-ns#type", + "plaza_id": "http://plaza.org/vocabularies/restResourceId" }, + + // Map URI -> alias + propertiesInvRegistry: {"http://www.w3.org/1999/02/22-rdf-syntax-ns#type": "rdf_type", + "http://plaza.org/vocabularies/restResourceId": "plaza_id" }, + + // Retrieves a service in JSON format provided the URI + _retrieveSchema: function(uri, callback) { + jQuery.ajax({ + url: uri, + dataType: 'json', + success: function(data) { + callback(data); + } + }) + }, + + // Process a Class definition + _processSchema: function(clsDef) { + var uri=clsDef.uri; + if(uri != null) { + var alias = Plaza.Utils.extractQLocal(uri); + Plaza.TBox.classesMap[uri] = alias; + + if(null == Plaza.TBox.classesRegistry[alias]) { + Plaza.TBox.classesRegistry[alias] = [uri]; + } else { + Plaza.TBox.classesRegistry[alias].push(uri); + } + } + }, + + // Process a Property definition + _processProperty: function(propDef) { + var uri= propDef.uri; + if(uri != null) { + var alias = Plaza.Utils.extractQLocal(uri); + Plaza.TBox.propertiesMap[uri] = propDef; + Plaza.TBox.propertiesInvRegistry[uri] = alias; + + if(Plaza.TBox.propertiesRegistry[alias] == null) { + Plaza.TBox.propertiesRegistry[alias] = [uri]; + } else { + Plaza.TBox.propertiesRegistry[alias].push(uri); + } + } + }, + + // Registers a schema using an associated URI + // * arguments: + // - uri : URI of the schema + // - callback: Optional callback function that will be notified whe + // when the registration is successfull + // If no callback is provided a currified version of the function will be + // returned. + registerSchema: function() { + var uri = arguments[0]; + var clbk = arguments[1]; + + var cont = function(callback) { + Plaza.TBox._retrieveSchema(uri, function(schema) { + for(var i in schema) { + if(schema[i].type != null) { + Plaza.TBox._processSchema(schema[i]); + } else { + Plaza.TBox._processProperty(schema[i]); + } + } + callback(uri); + }); + }; + + if(clbk == null) { + return cont; + } else { + cont(clbk); + } + }, + + // Finds the alias registered for an URI + findPropertyAlias: function(uri) { + return Plaza.TBox.propertiesInvRegistry[uri]; + }, + + // Finds the URI registered for an alias + findPropertyUri: function(alias) { + return Plaza.TBox.propertiesRegistry[alias]; + } + + +}; + +/* ABbox */ +Plaza.ABox = { + + // Default events + EVENTS: {"CREATED": 0, "DESTROYED": 1, "UPDATED": 2}, + + // Mapping URI -> Entity + entitiesRegistry: {}, + + // Mapping space -> triples + spacesRegistry: {}, + + // Registers an observer for an entity + startObservingEntity: function(uri,event,observer,callback) { + var entity = Plaza.ABox.entitiesRegistry[uri]; + if(entity != null) { + var observers = entity.observers[event]; + if(observers == null) { + observers = [] + entity.observers[event] = observers; + } + observers.push({"observer": observer, "callback": callback}); + } + }, + + // Unregisters an observer in an entity + stopObservingEntity: function(uri, event, observer) { + var entity = Plaza.ABox.entitiesRegistry[uri]; + if(entity != null) { + var observers = entity.observers[event]; + if(observers != null) { + var observersMod = [] + for(var i in observers) { + var o = observers[i]; + if(o.observer != observer) { + observersMod.push(o) + } + } + entity.observers = observersMod; + } + } + }, + + // Sends a notification to the observers of an event on a an entity + notifyEntityObservers: function(entityUri, event, data) { + var entity = Plaza.ABox.entitiesRegistry[entityUri]; + for(var i in entity.observers[event]) { + var o = entity.observers[event][i]; + + o.callback.apply(o.observer,[entity.uri, event, data]); + } + Plaza.ABox.notifySpaceObservers(entity.space.id, event, data); + }, + + // Registers an observer for a space + startObservingSpace: function(spaceId, event, observer, callback) { + var space = Plaza.ABox.spacesRegistry[spaceId]; + if(space != null) { + var observers = space.observers[event]; + if(observers == null) { + observers = [] + space.observers[event] = observers; + } + observers.push({"observer": observer, "callback": callback}); + } + }, + + // Unregisters an observer in an space + stopObservingSpace: function(spaceId, event, observer) { + var space = Plaza.ABox.spacesRegistry[spaceId]; + if(space != null) { + var observers = space.observers[event]; + if(observers != null) { + var observersMod = [] + for(var i in observers) { + var o = observers[i]; + if(o.observer != observer) { + observersMod.push(o) + } + } + space.observers = observersMod; + } + } + }, + + // Sends a notification to the observers of an event on a space + notifySpaceObservers: function(spaceId, event, data) { + var space = Plaza.ABox.spacesRegistry[spaceId]; + if(space != null) { + for(var i in space.observers[event]) { + var o = space.observers[event][i]; + o.callback.apply(o.observer,[space.id, event, data]); + } + } + }, + + // Initalizes a new ABox entity setting some meta-data for the entity + makeEntityMap: function(uri, triples) { + return { "uri": uri, "dirty": false, "value": triples, "observers": {}, "space": null }; + }, + + // Registers a new triple space for handling entitities + // * spaceId: Identifier of the space + // * callback: function where life cycle notifications of the entity will be notified + registerSpace: function(spaceId, callback) { + if(Plaza.ABox.spacesRegistry[spaceId] != null) { + throw "Space already registered:" + spaceId; + } + + Plaza.ABox.spacesRegistry[spaceId] = { "id": spaceId, "entities": [], "callback": callback, "observers":{} }; + }, + + // Registers some triples in the ABox + // * URI: uri of the new Entity + // * triples: value of the entity + // * spaceId: identifier of the space managing the entity (e.g. service URI) + registerEntity: function(uri, triples, spaceId) { + // Entity creation + var entity = Plaza.ABox.makeEntityMap(uri, triples); + entity.space = Plaza.ABox.spacesRegistry[spaceId]; + + // Registering the entity + if(Plaza.ABox.spacesRegistry[spaceId] == null) { + // @todo What should we do here? + throw "error, unknown space:"+ spaceId; + } + Plaza.ABox.spacesRegistry[spaceId].entities.push(entity); + + if(Plaza.ABox.entitiesRegistry[uri] != null) { + //@todo What should we do here? + throw "error, entity already registered:"+ uri; + } + Plaza.ABox.entitiesRegistry[uri] = entity; + + // Creation notifications + Plaza.ABox.notifySpaceObservers(spaceId, Plaza.ABox.EVENTS.CREATED, entity.value); + }, + + // Finds a space provided its identifier + findSpace: function(spaceId) { + return Plaza.ABox.spacesRegistry[spaceId]; + }, + + // Returns all the entities in a space provided its identifier + spaceEntities: function(spaceId) { + var entities = Plaza.ABox.findSpace(spaceId).entities; + var vals = []; + for (var i in entities) { + var entity = entities[i]; + vals.push(entity.value); + } + + return vals; + }, + + updateEntity: function(uri, triples) { + var entity = Plaza.ABox.entitiesRegistry[uri]; + + if(entity != null) { + entity.dirty = true; + entity.value = triples; + + entity.space.callback(Plaza.ABox.EVENTS.UPDATED, uri, entity); + + Plaza.ABox.notifyEntityObservers(uri, Plaza.ABox.EVENTS.UPDATED, entity.value); + } + }, + + destroyEntity: function(uri) { + var entity = Plaza.ABox.entitiesRegistry[uri]; + + if(entity != null) { + entity.space.callback(Plaza.ABox.EVENTS.DESTROYED, uri, entity); + Plaza.ABox.notifyEntityObservers(uri, Plaza.ABox.EVENTS.DESTROYED, entity.value); + + // we remove the entity + var entities = entity.space.entities; + var entitiesMod = [] + for(var i in entities) { + if(entities[i].uri != uri) { + entitiesMod.push(entities[i]); + } + } + entity.space.entities = entitiesMod; + + delete Plaza.ABox.entitiesRegistry[uri]; + } + }, + + findEntityByURI: function(uri) { + return Plaza.ABox.entitiesRegistry[uri].value; + } +}; + +/* JSON */ +Plaza.JSON = { + + // Builds a JSON object retrieving the alias for the properties from + // the TBox + fromTriples: function(triples) { + var acum = {} + for(var i in triples) { + var obj = null; + var triple = triples[i]; + var subject = triple[0]; + var predicate = triple[1]; + var object = triple[2]; + var alias = Plaza.TBox.findPropertyAlias(predicate); + + if(acum[subject] == null) { + obj = {"_uri": subject}; + acum[subject] = obj; + } else { + obj = acum[subject]; + } + + if(alias != null) { + if(typeof(object) == "object" || (typeof(object) == "string" && object.indexOf("http://") != 0)) { + obj[alias] = Plaza.XSD.parseType(object); + } else { + obj[alias] = object; + } + } + } + + var objs = []; + for(var i in acum) { + objs.push(acum[i]) + } + return objs; + } + +} + +/* Services */ +Plaza.Services = { + + // Map URI -> alias + servicesMap: {}, + + // Map alias -> URI + servicesRegistry: {}, + + // Retrieves a service in JSON format provided the URI + _retrieveService: function(uri, callback) { + jQuery.ajax({ + url: uri, + dataType: 'json', + success: function(data) { + callback(data); + } + }) + }, + + // Registers a service using an associated URI + registerService: function(alias, uri, callback) { + this._retrieveService(uri, function(srv) { + Plaza.Services.servicesMap[uri] = srv; + Plaza.Services.servicesRegistry[alias] = uri; + callback(alias); + }); + }, + + // Retrieves a service from the provided alias + findByAlias: function(alias) { + return Plaza.Services.servicesMap[Plaza.Services.servicesRegistry[alias]]; + }, + + // Retrieves a service from the provided uri + findByUri: function(uri) { + return Plaza.Services.servicesMap[uri]; + }, + + // Creates a map with urlReplacements and values extracted from the data + // passed as arguments + _computeReplacements: function(data, messages) { + var mapping = {}; + + for (var i in messages) { + var msg = messages[i]; + var modelReference = msg.modelReference; + var urlReplacement = Plaza.Utils.cleanTypedLiteral(msg.urlReplacement).value; + var modelReferenceAlias = Plaza.TBox.findPropertyAlias(modelReference); + + var value = data[modelReferenceAlias]; + if(value == null) { + value = data[modelReference]; + } + if(value != null) { + mapping[urlReplacement] = value; + } + } + + return mapping; + }, + + // Provided a map of URL replacements and a URI template, returns + // a URI and a map of parameters + _urlAndParameters: function(urlTemplate, replacements) { + var mapping = {}; + + for (var replacement in replacements) { + var rplc = "{" + replacement + "}"; + var value = replacements[replacement]; + + if(urlTemplate.indexOf(rplc) != -1) { + urlTemplate = urlTemplate.replace(rplc, escape(value)); + } else { + mapping[replacement] = value; + } + } + + return {"url": urlTemplate, "parameters": mapping}; + }, + + // Consumes a services + consume: function(alias, method, data, callback) { + var service = Plaza.Services.findByAlias(alias); + if(service == null) { + throw("Cannot find service to consume for alias: " + alias); + } + + var operation = null; + for (var i in service.operations) { + var op = service.operations[i] + if(op.method.toLowerCase() == method.toLowerCase()) { + operation = op; + } + } + if(operation == null) { + throw("Cannot find operation for service " + alias +" and method " + method); + } + + var replacements = Plaza.Services._computeReplacements(data, operation.inputMessages); + + var urlAndParameters = Plaza.Services._urlAndParameters(operation.addressTemplate, replacements); + var url = urlAndParameters.url + ".js3"; + var parameters = urlAndParameters.parameters; + + var realMethod = operation.method; + if(operation.method.toLowerCase() == "put") { + parameters["_method"] = "put"; + realMethod = "post"; + } else if(operation.method.toLowerCase() == "delete") { + realMethod = "get"; + parameters["_method"] = "delete"; + } + + jQuery.ajax({ + url:url, + type: realMethod.toUpperCase(), + dataType: 'json', + data: parameters, + success: function(triples) { + callback(alias, method, triples); + }, + error: function(e) { + throw "Error consuming service: " + url + " method: " + method + " parameters: " + parameters + " -> " + e; + } + }); + + } +}; + + +/* Triple Sapces */ +Plaza.ABox.TripleSpace = { + + _singleResourceName: function(spaceName) { + return spaceName + "-single"; + }, + + _collectionResourceName: function(spaceName) { + return spaceName + "-collection"; + }, + + //creates a new triple space from a certain number of services + connect: function() { + var name = arguments[0]; + var opts = arguments[1]; + var clbk = arguments[2]; + + var singleResourceServiceUri = opts.singleResource; + var collectionResourceServiceUri = opts.collectionResource; + + var toCall = function(callback) { + // Registration of the Triple Space service + var tripleSpaceRegistrationFn = function() { + + // Function for provided as callback for managing engities + Plaza.ABox.registerSpace(name,function(event, uri, entity) { + + // Updated + if(event == Plaza.ABox.EVENTS.UPDATED) { + Plaza.Services.consume(Plaza.ABox.TripleSpace._singleResourceName(name), "put", entity.value, function(alias, method, triples){ + entity.dirty = false; + }); + } + + // Destroyed + if(event == Plaza.ABox.EVENTS.DESTROYED) { + Plaza.Services.consume(Plaza.ABox.TripleSpace._singleResourceName(name), "delete", entity.value, function(alias, method, triples){}); + } + + }); + + var space = Plaza.ABox.spacesRegistry[name]; + + space["singleResource"] = Plaza.ABox.TripleSpace._singleResourceName(name); + space["collectionResource"] = Plaza.ABox.TripleSpace._collectionResourceName(name); + + callback(name); + }; + + // Registration of services + if(singleResourceServiceUri != null) { + Plaza.Services.registerService(Plaza.ABox.TripleSpace._singleResourceName(name), singleResourceServiceUri, function(_alias) { + if(collectionResourceServiceUri != null) { + Plaza.Services.registerService(Plaza.ABox.TripleSpace._collectionResourceName(name), collectionResourceServiceUri, function(_alias){ + tripleSpaceRegistrationFn(); + }); + } else { + tripleSpaceRegistrationFn(); + } + }); + } else { + Plaza.Services.registerService(Plaza.ABox.TripleSpace._collectionResourceName(name), collectionResourceServiceUri, function(_alias){ + tripleSpaceRegistrationFn(); + }); + } + }; + + if (clbk == null) { + return toCall; + } else { + toCall(clbk); + } + } +}; + +// Load a set of instances +Plaza.ABox.loadInstances = function(spaceId, data){ + var space = Plaza.ABox.spacesRegistry[spaceId]; + + var collectionResource = space["collectionResource"]; + if(collectionResource != null) { + Plaza.Services.consume(collectionResource, "get", data, function(alias, method, triples){ + var results = Plaza.JSON.fromTriples(triples); + for (var i in results) { + var entity = results[i]; + var found = Plaza.ABox.entitiesRegistry[entity._uri]; + if(found != null) { + if(found.dirty == false) { + Plaza.ABox.updateEntity(entity._uri, entity); + } else { + //@todo merging is correct? + for(var p in entity) { + if(found.value[p] == null) { + found.value[p] = entity.p + } + } + Plaza.ABox.updateEntity(found._uri, found.value); + } + } else { + Plaza.ABox.registerEntity(entity._uri, entity, spaceId); + } + } + }); + } else { + throw "Uknown triple space:" + spaceId; + } +}; + +// Creates a new instance +Plaza.ABox.createEntity = function(spaceId, data) { + var space = Plaza.ABox.spacesRegistry[spaceId]; + + var collectionResource = space["collectionResource"]; + if(collectionResource != null) { + Plaza.Services.consume(collectionResource, "post", data, function(alias, method, triples){ + var results = Plaza.JSON.fromTriples(triples); + var entity = results[0]; + + Plaza.ABox.registerEntity(entity._uri, entity, spaceId); + }); + } else { + throw "Uknown triple space:" + spaceId; + } +} diff --git a/js/tests/plaza_test.html b/js/tests/plaza_test.html new file mode 100644 index 0000000..2d49eb1 --- /dev/null +++ b/js/tests/plaza_test.html @@ -0,0 +1,625 @@ + + + + + + + + + + Unit Tests for Plaza.js + + +

Plaza JS Tests

+

+

+
    + + diff --git a/js/tests/qunit.css b/js/tests/qunit.css new file mode 100644 index 0000000..5714bf4 --- /dev/null +++ b/js/tests/qunit.css @@ -0,0 +1,119 @@ + +ol#qunit-tests { + font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial; + margin:0; + padding:0; + list-style-position:inside; + + font-size: smaller; +} +ol#qunit-tests li{ + padding:0.4em 0.5em 0.4em 2.5em; + border-bottom:1px solid #fff; + font-size:small; + list-style-position:inside; +} +ol#qunit-tests li ol{ + box-shadow: inset 0px 2px 13px #999; + -moz-box-shadow: inset 0px 2px 13px #999; + -webkit-box-shadow: inset 0px 2px 13px #999; + margin-top:0.5em; + margin-left:0; + padding:0.5em; + background-color:#fff; + border-radius:15px; + -moz-border-radius: 15px; + -webkit-border-radius: 15px; +} +ol#qunit-tests li li{ + border-bottom:none; + margin:0.5em; + background-color:#fff; + list-style-position: inside; + padding:0.4em 0.5em 0.4em 0.5em; +} + +ol#qunit-tests li li.pass{ + border-left:26px solid #C6E746; + background-color:#fff; + color:#5E740B; + } +ol#qunit-tests li li.fail{ + border-left:26px solid #EE5757; + background-color:#fff; + color:#710909; +} +ol#qunit-tests li.pass{ + background-color:#D2E0E6; + color:#528CE0; +} +ol#qunit-tests li.fail{ + background-color:#EE5757; + color:#000; +} +ol#qunit-tests li strong { + cursor:pointer; +} +h1#qunit-header{ + background-color:#0d3349; + margin:0; + padding:0.5em 0 0.5em 1em; + color:#fff; + font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial; + border-top-right-radius:15px; + border-top-left-radius:15px; + -moz-border-radius-topright:15px; + -moz-border-radius-topleft:15px; + -webkit-border-top-right-radius:15px; + -webkit-border-top-left-radius:15px; + text-shadow: rgba(0, 0, 0, 0.5) 4px 4px 1px; +} +h2#qunit-banner{ + font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial; + height:5px; + margin:0; + padding:0; +} +h2#qunit-banner.qunit-pass{ + background-color:#C6E746; +} +h2#qunit-banner.qunit-fail, #qunit-testrunner-toolbar { + background-color:#EE5757; +} +#qunit-testrunner-toolbar { + font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial; + padding:0; + /*width:80%;*/ + padding:0em 0 0.5em 2em; + font-size: small; +} +h2#qunit-userAgent { + font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial; + background-color:#2b81af; + margin:0; + padding:0; + color:#fff; + font-size: small; + padding:0.5em 0 0.5em 2.5em; + text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; +} +p#qunit-testresult{ + font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial; + margin:0; + font-size: small; + color:#2b81af; + border-bottom-right-radius:15px; + border-bottom-left-radius:15px; + -moz-border-radius-bottomright:15px; + -moz-border-radius-bottomleft:15px; + -webkit-border-bottom-right-radius:15px; + -webkit-border-bottom-left-radius:15px; + background-color:#D2E0E6; + padding:0.5em 0.5em 0.5em 2.5em; +} +strong b.fail{ + color:#710909; + } +strong b.pass{ + color:#5E740B; + } diff --git a/js/tests/qunit.js b/js/tests/qunit.js new file mode 100644 index 0000000..9ef5f8d --- /dev/null +++ b/js/tests/qunit.js @@ -0,0 +1,1069 @@ +/* + * QUnit - A JavaScript Unit Testing Framework + * + * http://docs.jquery.com/QUnit + * + * Copyright (c) 2009 John Resig, Jörn Zaefferer + * Dual licensed under the MIT (MIT-LICENSE.txt) + * and GPL (GPL-LICENSE.txt) licenses. + */ + +(function(window) { + +var QUnit = { + + // Initialize the configuration options + init: function() { + config = { + stats: { all: 0, bad: 0 }, + moduleStats: { all: 0, bad: 0 }, + started: +new Date, + updateRate: 1000, + blocking: false, + autorun: false, + assertions: [], + filters: [], + queue: [] + }; + + var tests = id("qunit-tests"), + banner = id("qunit-banner"), + result = id("qunit-testresult"); + + if ( tests ) { + tests.innerHTML = ""; + } + + if ( banner ) { + banner.className = ""; + } + + if ( result ) { + result.parentNode.removeChild( result ); + } + }, + + // call on start of module test to prepend name to all tests + module: function(name, testEnvironment) { + config.currentModule = name; + + synchronize(function() { + if ( config.currentModule ) { + QUnit.moduleDone( config.currentModule, config.moduleStats.bad, config.moduleStats.all ); + } + + config.currentModule = name; + config.moduleTestEnvironment = testEnvironment; + config.moduleStats = { all: 0, bad: 0 }; + + QUnit.moduleStart( name, testEnvironment ); + }); + }, + + asyncTest: function(testName, expected, callback) { + if ( arguments.length === 2 ) { + callback = expected; + expected = 0; + } + + QUnit.test(testName, expected, callback, true); + }, + + test: function(testName, expected, callback, async) { + var name = testName, testEnvironment, testEnvironmentArg; + + if ( arguments.length === 2 ) { + callback = expected; + expected = null; + } + // is 2nd argument a testEnvironment? + if ( expected && typeof expected === 'object') { + testEnvironmentArg = expected; + expected = null; + } + + if ( config.currentModule ) { + name = config.currentModule + " module: " + name; + } + + if ( !validTest(name) ) { + return; + } + + synchronize(function() { + QUnit.testStart( testName ); + + testEnvironment = extend({ + setup: function() {}, + teardown: function() {} + }, config.moduleTestEnvironment); + if (testEnvironmentArg) { + extend(testEnvironment,testEnvironmentArg); + } + + // allow utility functions to access the current test environment + QUnit.current_testEnvironment = testEnvironment; + + config.assertions = []; + config.expected = expected; + + try { + if ( !config.pollution ) { + saveGlobal(); + } + + testEnvironment.setup.call(testEnvironment); + } catch(e) { + QUnit.ok( false, "Setup failed on " + name + ": " + e.message ); + } + + if ( async ) { + QUnit.stop(); + } + + try { + callback.call(testEnvironment); + } catch(e) { + fail("Test " + name + " died, exception and test follows", e, callback); + QUnit.ok( false, "Died on test #" + (config.assertions.length + 1) + ": " + e.message ); + // else next test will carry the responsibility + saveGlobal(); + + // Restart the tests if they're blocking + if ( config.blocking ) { + start(); + } + } + }); + + synchronize(function() { + try { + checkPollution(); + testEnvironment.teardown.call(testEnvironment); + } catch(e) { + QUnit.ok( false, "Teardown failed on " + name + ": " + e.message ); + } + + try { + QUnit.reset(); + } catch(e) { + fail("reset() failed, following Test " + name + ", exception and reset fn follows", e, reset); + } + + if ( config.expected && config.expected != config.assertions.length ) { + QUnit.ok( false, "Expected " + config.expected + " assertions, but " + config.assertions.length + " were run" ); + } + + var good = 0, bad = 0, + tests = id("qunit-tests"); + + config.stats.all += config.assertions.length; + config.moduleStats.all += config.assertions.length; + + if ( tests ) { + var ol = document.createElement("ol"); + ol.style.display = "none"; + + for ( var i = 0; i < config.assertions.length; i++ ) { + var assertion = config.assertions[i]; + + var li = document.createElement("li"); + li.className = assertion.result ? "pass" : "fail"; + li.appendChild(document.createTextNode(assertion.message || "(no message)")); + ol.appendChild( li ); + + if ( assertion.result ) { + good++; + } else { + bad++; + config.stats.bad++; + config.moduleStats.bad++; + } + } + + var b = document.createElement("strong"); + b.innerHTML = name + " (" + bad + ", " + good + ", " + config.assertions.length + ")"; + + addEvent(b, "click", function() { + var next = b.nextSibling, display = next.style.display; + next.style.display = display === "none" ? "block" : "none"; + }); + + addEvent(b, "dblclick", function(e) { + var target = e && e.target ? e.target : window.event.srcElement; + if ( target.nodeName.toLowerCase() === "strong" ) { + var text = "", node = target.firstChild; + + while ( node.nodeType === 3 ) { + text += node.nodeValue; + node = node.nextSibling; + } + + text = text.replace(/(^\s*|\s*$)/g, ""); + + if ( window.location ) { + window.location.href = window.location.href.match(/^(.+?)(\?.*)?$/)[1] + "?" + encodeURIComponent(text); + } + } + }); + + var li = document.createElement("li"); + li.className = bad ? "fail" : "pass"; + li.appendChild( b ); + li.appendChild( ol ); + tests.appendChild( li ); + + if ( bad ) { + var toolbar = id("qunit-testrunner-toolbar"); + if ( toolbar ) { + toolbar.style.display = "block"; + id("qunit-filter-pass").disabled = null; + id("qunit-filter-missing").disabled = null; + } + } + + } else { + for ( var i = 0; i < config.assertions.length; i++ ) { + if ( !config.assertions[i].result ) { + bad++; + config.stats.bad++; + config.moduleStats.bad++; + } + } + } + + QUnit.testDone( testName, bad, config.assertions.length ); + + if ( !window.setTimeout && !config.queue.length ) { + done(); + } + }); + + if ( window.setTimeout && !config.doneTimer ) { + config.doneTimer = window.setTimeout(function(){ + if ( !config.queue.length ) { + done(); + } else { + synchronize( done ); + } + }, 13); + } + }, + + /** + * Specify the number of expected assertions to gurantee that failed test (no assertions are run at all) don't slip through. + */ + expect: function(asserts) { + config.expected = asserts; + }, + + /** + * Asserts true. + * @example ok( "asdfasdf".length > 5, "There must be at least 5 chars" ); + */ + ok: function(a, msg) { + QUnit.log(a, msg); + + config.assertions.push({ + result: !!a, + message: msg + }); + }, + + /** + * Checks that the first two arguments are equal, with an optional message. + * Prints out both actual and expected values. + * + * Prefered to ok( actual == expected, message ) + * + * @example equal( format("Received {0} bytes.", 2), "Received 2 bytes." ); + * + * @param Object actual + * @param Object expected + * @param String message (optional) + */ + equal: function(actual, expected, message) { + push(expected == actual, actual, expected, message); + }, + + notEqual: function(actual, expected, message) { + push(expected != actual, actual, expected, message); + }, + + deepEqual: function(a, b, message) { + push(QUnit.equiv(a, b), a, b, message); + }, + + notDeepEqual: function(a, b, message) { + push(!QUnit.equiv(a, b), a, b, message); + }, + + strictEqual: function(actual, expected, message) { + push(expected === actual, actual, expected, message); + }, + + notStrictEqual: function(actual, expected, message) { + push(expected !== actual, actual, expected, message); + }, + + start: function() { + // A slight delay, to avoid any current callbacks + if ( window.setTimeout ) { + window.setTimeout(function() { + if ( config.timeout ) { + clearTimeout(config.timeout); + } + + config.blocking = false; + process(); + }, 13); + } else { + config.blocking = false; + process(); + } + }, + + stop: function(timeout) { + config.blocking = true; + + if ( timeout && window.setTimeout ) { + config.timeout = window.setTimeout(function() { + QUnit.ok( false, "Test timed out" ); + QUnit.start(); + }, timeout); + } + }, + + /** + * Resets the test setup. Useful for tests that modify the DOM. + */ + reset: function() { + if ( window.jQuery ) { + jQuery("#main").html( config.fixture ); + jQuery.event.global = {}; + jQuery.ajaxSettings = extend({}, config.ajaxSettings); + } + }, + + /** + * Trigger an event on an element. + * + * @example triggerEvent( document.body, "click" ); + * + * @param DOMElement elem + * @param String type + */ + triggerEvent: function( elem, type, event ) { + if ( document.createEvent ) { + event = document.createEvent("MouseEvents"); + event.initMouseEvent(type, true, true, elem.ownerDocument.defaultView, + 0, 0, 0, 0, 0, false, false, false, false, 0, null); + elem.dispatchEvent( event ); + + } else if ( elem.fireEvent ) { + elem.fireEvent("on"+type); + } + }, + + // Safe object type checking + is: function( type, obj ) { + return Object.prototype.toString.call( obj ) === "[object "+ type +"]"; + }, + + // Logging callbacks + done: function(failures, total) {}, + log: function(result, message) {}, + testStart: function(name) {}, + testDone: function(name, failures, total) {}, + moduleStart: function(name, testEnvironment) {}, + moduleDone: function(name, failures, total) {} +}; + +// Backwards compatibility, deprecated +QUnit.equals = QUnit.equal; +QUnit.same = QUnit.deepEqual; + +// Maintain internal state +var config = { + // The queue of tests to run + queue: [], + + // block until document ready + blocking: true +}; + +// Load paramaters +(function() { + var location = window.location || { search: "", protocol: "file:" }, + GETParams = location.search.slice(1).split('&'); + + for ( var i = 0; i < GETParams.length; i++ ) { + GETParams[i] = decodeURIComponent( GETParams[i] ); + if ( GETParams[i] === "noglobals" ) { + GETParams.splice( i, 1 ); + i--; + config.noglobals = true; + } else if ( GETParams[i].search('=') > -1 ) { + GETParams.splice( i, 1 ); + i--; + } + } + + // restrict modules/tests by get parameters + config.filters = GETParams; + + // Figure out if we're running the tests from a server or not + QUnit.isLocal = !!(location.protocol === 'file:'); +})(); + +// Expose the API as global variables, unless an 'exports' +// object exists, in that case we assume we're in CommonJS +if ( typeof exports === "undefined" || typeof require === "undefined" ) { + extend(window, QUnit); + window.QUnit = QUnit; +} else { + extend(exports, QUnit); + exports.QUnit = QUnit; +} + +if ( typeof document === "undefined" || document.readyState === "complete" ) { + config.autorun = true; +} + +addEvent(window, "load", function() { + // Initialize the config, saving the execution queue + var oldconfig = extend({}, config); + QUnit.init(); + extend(config, oldconfig); + + config.blocking = false; + + var userAgent = id("qunit-userAgent"); + if ( userAgent ) { + userAgent.innerHTML = navigator.userAgent; + } + + var toolbar = id("qunit-testrunner-toolbar"); + if ( toolbar ) { + toolbar.style.display = "none"; + + var filter = document.createElement("input"); + filter.type = "checkbox"; + filter.id = "qunit-filter-pass"; + filter.disabled = true; + addEvent( filter, "click", function() { + var li = document.getElementsByTagName("li"); + for ( var i = 0; i < li.length; i++ ) { + if ( li[i].className.indexOf("pass") > -1 ) { + li[i].style.display = filter.checked ? "none" : ""; + } + } + }); + toolbar.appendChild( filter ); + + var label = document.createElement("label"); + label.setAttribute("for", "qunit-filter-pass"); + label.innerHTML = "Hide passed tests"; + toolbar.appendChild( label ); + + var missing = document.createElement("input"); + missing.type = "checkbox"; + missing.id = "qunit-filter-missing"; + missing.disabled = true; + addEvent( missing, "click", function() { + var li = document.getElementsByTagName("li"); + for ( var i = 0; i < li.length; i++ ) { + if ( li[i].className.indexOf("fail") > -1 && li[i].innerHTML.indexOf('missing test - untested code is broken code') > - 1 ) { + li[i].parentNode.parentNode.style.display = missing.checked ? "none" : "block"; + } + } + }); + toolbar.appendChild( missing ); + + label = document.createElement("label"); + label.setAttribute("for", "qunit-filter-missing"); + label.innerHTML = "Hide missing tests (untested code is broken code)"; + toolbar.appendChild( label ); + } + + var main = id('main'); + if ( main ) { + config.fixture = main.innerHTML; + } + + if ( window.jQuery ) { + config.ajaxSettings = window.jQuery.ajaxSettings; + } + + QUnit.start(); +}); + +function done() { + if ( config.doneTimer && window.clearTimeout ) { + window.clearTimeout( config.doneTimer ); + config.doneTimer = null; + } + + if ( config.queue.length ) { + config.doneTimer = window.setTimeout(function(){ + if ( !config.queue.length ) { + done(); + } else { + synchronize( done ); + } + }, 13); + + return; + } + + config.autorun = true; + + // Log the last module results + if ( config.currentModule ) { + QUnit.moduleDone( config.currentModule, config.moduleStats.bad, config.moduleStats.all ); + } + + var banner = id("qunit-banner"), + tests = id("qunit-tests"), + html = ['Tests completed in ', + +new Date - config.started, ' milliseconds.
    ', + '', config.stats.all - config.stats.bad, ' tests of ', config.stats.all, ' passed, ', config.stats.bad,' failed.'].join(''); + + if ( banner ) { + banner.className = (config.stats.bad ? "qunit-fail" : "qunit-pass"); + } + + if ( tests ) { + var result = id("qunit-testresult"); + + if ( !result ) { + result = document.createElement("p"); + result.id = "qunit-testresult"; + result.className = "result"; + tests.parentNode.insertBefore( result, tests.nextSibling ); + } + + result.innerHTML = html; + } + + QUnit.done( config.stats.bad, config.stats.all ); +} + +function validTest( name ) { + var i = config.filters.length, + run = false; + + if ( !i ) { + return true; + } + + while ( i-- ) { + var filter = config.filters[i], + not = filter.charAt(0) == '!'; + + if ( not ) { + filter = filter.slice(1); + } + + if ( name.indexOf(filter) !== -1 ) { + return !not; + } + + if ( not ) { + run = true; + } + } + + return run; +} + +function push(result, actual, expected, message) { + message = message || (result ? "okay" : "failed"); + QUnit.ok( result, result ? message + ": " + QUnit.jsDump.parse(expected) : message + ", expected: " + QUnit.jsDump.parse(expected) + " result: " + QUnit.jsDump.parse(actual) ); +} + +function synchronize( callback ) { + config.queue.push( callback ); + + if ( config.autorun && !config.blocking ) { + process(); + } +} + +function process() { + var start = (new Date()).getTime(); + + while ( config.queue.length && !config.blocking ) { + if ( config.updateRate <= 0 || (((new Date()).getTime() - start) < config.updateRate) ) { + config.queue.shift()(); + + } else { + setTimeout( process, 13 ); + break; + } + } +} + +function saveGlobal() { + config.pollution = []; + + if ( config.noglobals ) { + for ( var key in window ) { + config.pollution.push( key ); + } + } +} + +function checkPollution( name ) { + var old = config.pollution; + saveGlobal(); + + var newGlobals = diff( old, config.pollution ); + if ( newGlobals.length > 0 ) { + ok( false, "Introduced global variable(s): " + newGlobals.join(", ") ); + config.expected++; + } + + var deletedGlobals = diff( config.pollution, old ); + if ( deletedGlobals.length > 0 ) { + ok( false, "Deleted global variable(s): " + deletedGlobals.join(", ") ); + config.expected++; + } +} + +// returns a new Array with the elements that are in a but not in b +function diff( a, b ) { + var result = a.slice(); + for ( var i = 0; i < result.length; i++ ) { + for ( var j = 0; j < b.length; j++ ) { + if ( result[i] === b[j] ) { + result.splice(i, 1); + i--; + break; + } + } + } + return result; +} + +function fail(message, exception, callback) { + if ( typeof console !== "undefined" && console.error && console.warn ) { + console.error(message); + console.error(exception); + console.warn(callback.toString()); + + } else if ( window.opera && opera.postError ) { + opera.postError(message, exception, callback.toString); + } +} + +function extend(a, b) { + for ( var prop in b ) { + a[prop] = b[prop]; + } + + return a; +} + +function addEvent(elem, type, fn) { + if ( elem.addEventListener ) { + elem.addEventListener( type, fn, false ); + } else if ( elem.attachEvent ) { + elem.attachEvent( "on" + type, fn ); + } else { + fn(); + } +} + +function id(name) { + return !!(typeof document !== "undefined" && document && document.getElementById) && + document.getElementById( name ); +} + +// Test for equality any JavaScript type. +// Discussions and reference: http://philrathe.com/articles/equiv +// Test suites: http://philrathe.com/tests/equiv +// Author: Philippe Rathé +QUnit.equiv = function () { + + var innerEquiv; // the real equiv function + var callers = []; // stack to decide between skip/abort functions + var parents = []; // stack to avoiding loops from circular referencing + + + // Determine what is o. + function hoozit(o) { + if (QUnit.is("String", o)) { + return "string"; + + } else if (QUnit.is("Boolean", o)) { + return "boolean"; + + } else if (QUnit.is("Number", o)) { + + if (isNaN(o)) { + return "nan"; + } else { + return "number"; + } + + } else if (typeof o === "undefined") { + return "undefined"; + + // consider: typeof null === object + } else if (o === null) { + return "null"; + + // consider: typeof [] === object + } else if (QUnit.is( "Array", o)) { + return "array"; + + // consider: typeof new Date() === object + } else if (QUnit.is( "Date", o)) { + return "date"; + + // consider: /./ instanceof Object; + // /./ instanceof RegExp; + // typeof /./ === "function"; // => false in IE and Opera, + // true in FF and Safari + } else if (QUnit.is( "RegExp", o)) { + return "regexp"; + + } else if (typeof o === "object") { + return "object"; + + } else if (QUnit.is( "Function", o)) { + return "function"; + } else { + return undefined; + } + } + + // Call the o related callback with the given arguments. + function bindCallbacks(o, callbacks, args) { + var prop = hoozit(o); + if (prop) { + if (hoozit(callbacks[prop]) === "function") { + return callbacks[prop].apply(callbacks, args); + } else { + return callbacks[prop]; // or undefined + } + } + } + + var callbacks = function () { + + // for string, boolean, number and null + function useStrictEquality(b, a) { + if (b instanceof a.constructor || a instanceof b.constructor) { + // to catch short annotaion VS 'new' annotation of a declaration + // e.g. var i = 1; + // var j = new Number(1); + return a == b; + } else { + return a === b; + } + } + + return { + "string": useStrictEquality, + "boolean": useStrictEquality, + "number": useStrictEquality, + "null": useStrictEquality, + "undefined": useStrictEquality, + + "nan": function (b) { + return isNaN(b); + }, + + "date": function (b, a) { + return hoozit(b) === "date" && a.valueOf() === b.valueOf(); + }, + + "regexp": function (b, a) { + return hoozit(b) === "regexp" && + a.source === b.source && // the regex itself + a.global === b.global && // and its modifers (gmi) ... + a.ignoreCase === b.ignoreCase && + a.multiline === b.multiline; + }, + + // - skip when the property is a method of an instance (OOP) + // - abort otherwise, + // initial === would have catch identical references anyway + "function": function () { + var caller = callers[callers.length - 1]; + return caller !== Object && + typeof caller !== "undefined"; + }, + + "array": function (b, a) { + var i, j, loop; + var len; + + // b could be an object literal here + if ( ! (hoozit(b) === "array")) { + return false; + } + + len = a.length; + if (len !== b.length) { // safe and faster + return false; + } + + //track reference to avoid circular references + parents.push(a); + for (i = 0; i < len; i++) { + loop = false; + for(j=0;j= 0) { + type = "array"; + } else { + type = typeof obj; + } + return type; + }, + separator:function() { + return this.multiline ? this.HTML ? '
    ' : '\n' : this.HTML ? ' ' : ' '; + }, + indent:function( extra ) {// extra can be a number, shortcut for increasing-calling-decreasing + if ( !this.multiline ) + return ''; + var chr = this.indentChar; + if ( this.HTML ) + chr = chr.replace(/\t/g,' ').replace(/ /g,' '); + return Array( this._depth_ + (extra||0) ).join(chr); + }, + up:function( a ) { + this._depth_ += a || 1; + }, + down:function( a ) { + this._depth_ -= a || 1; + }, + setParser:function( name, parser ) { + this.parsers[name] = parser; + }, + // The next 3 are exposed so you can use them + quote:quote, + literal:literal, + join:join, + // + _depth_: 1, + // This is the list of parsers, to modify them, use jsDump.setParser + parsers:{ + window: '[Window]', + document: '[Document]', + error:'[ERROR]', //when no parser is found, shouldn't happen + unknown: '[Unknown]', + 'null':'null', + undefined:'undefined', + 'function':function( fn ) { + var ret = 'function', + name = 'name' in fn ? fn.name : (reName.exec(fn)||[])[1];//functions never have name in IE + if ( name ) + ret += ' ' + name; + ret += '('; + + ret = [ ret, this.parse( fn, 'functionArgs' ), '){'].join(''); + return join( ret, this.parse(fn,'functionCode'), '}' ); + }, + array: array, + nodelist: array, + arguments: array, + object:function( map ) { + var ret = [ ]; + this.up(); + for ( var key in map ) + ret.push( this.parse(key,'key') + ': ' + this.parse(map[key]) ); + this.down(); + return join( '{', ret, '}' ); + }, + node:function( node ) { + var open = this.HTML ? '<' : '<', + close = this.HTML ? '>' : '>'; + + var tag = node.nodeName.toLowerCase(), + ret = open + tag; + + for ( var a in this.DOMAttrs ) { + var val = node[this.DOMAttrs[a]]; + if ( val ) + ret += ' ' + a + '=' + this.parse( val, 'attribute' ); + } + return ret + close + open + '/' + tag + close; + }, + functionArgs:function( fn ) {//function calls it internally, it's the arguments part of the function + var l = fn.length; + if ( !l ) return ''; + + var args = Array(l); + while ( l-- ) + args[l] = String.fromCharCode(97+l);//97 is 'a' + return ' ' + args.join(', ') + ' '; + }, + key:quote, //object calls it internally, the key part of an item in a map + functionCode:'[code]', //function calls it internally, it's the content of the function + attribute:quote, //node calls it internally, it's an html attribute value + string:quote, + date:quote, + regexp:literal, //regex + number:literal, + 'boolean':literal + }, + DOMAttrs:{//attributes to dump from nodes, name=>realName + id:'id', + name:'name', + 'class':'className' + }, + HTML:false,//if true, entities are escaped ( <, >, \t, space and \n ) + indentChar:' ',//indentation unit + multiline:false //if true, items in a collection, are separated by a \n, else just a space. + }; + + return jsDump; +})(); + +})(this);