diff --git a/pkg/init.dev.js b/pkg/init.dev.js index 38c386e..402f8e3 100644 --- a/pkg/init.dev.js +++ b/pkg/init.dev.js @@ -30,7 +30,6 @@ JX.__rawEventQueue = function(what) { master_event_queue.push(what); - // Evade static analysis - JX.Stratcom var Stratcom = JX['Stratcom']; if (Stratcom && Stratcom.ready) { @@ -62,7 +61,8 @@ var target = what.srcElement || what.target; if (target && (what.type in {click: 1, submit: 1}) && - (/ FI_CAPTURE /).test(' ' + target.className + ' ')) { + target.getAttribute && + target.getAttribute('data-mustcapture') === '1') { what.returnValue = false; what.preventDefault && what.preventDefault(); document.body.id = 'event_capture'; diff --git a/pkg/init.min.js b/pkg/init.min.js index af32f43..950bb23 100644 --- a/pkg/init.min.js +++ b/pkg/init.min.js @@ -1 +1 @@ -(function(){if(window.JX)return;window.JX={};window.__DEV__=window.__DEV__||0;var d=false;var f=[];var e=[];var h=document.documentElement;var b=!!h.addEventListener;JX.__rawEventQueue=function(o){e.push(o);var j=JX.Stratcom;if(j&&j.ready){var m=e;e=[];for(var l=0;l<\/sc'+'ript\>');}JX.onload=function(j){if(d){j();}else f.push(j);};})(); \ No newline at end of file +(function(){if(window.JX)return;window.JX={};window.__DEV__=window.__DEV__||0;var d=false;var f=[];var e=[];var h=document.documentElement;var b=!!h.addEventListener;JX.__rawEventQueue=function(o){e.push(o);var j=JX.Stratcom;if(j&&j.ready){var m=e;e=[];for(var l=0;l<\/sc'+'ript\>');}JX.onload=function(j){if(d){j();}else f.push(j);};})(); \ No newline at end of file diff --git a/pkg/javelin.dev.js b/pkg/javelin.dev.js index bc82cd0..df32e95 100644 --- a/pkg/javelin.dev.js +++ b/pkg/javelin.dev.js @@ -822,7 +822,6 @@ JX.install('Event', { * } * }); * - * * @return string|null ##null## if there is no associated special key, * or one of the strings 'delete', 'tab', 'return', * 'esc', 'left', 'up', 'right', or 'down'. @@ -834,21 +833,17 @@ JX.install('Event', { return null; } - var c = r.keyCode; - do { - c = JX.Event._keymap[c] || null; - } while (c && JX.Event._keymap[c]) - - return c; + return JX.Event._keymap[r.keyCode] || null; }, + /** * Get the node corresponding to the specified key in this event's node map. * This is a simple helper method that makes the API for accessing nodes * less ugly. * * JX.Stratcom.listen('click', 'tag:a', function(e) { - * var a = e.getNode('nearest:a'); + * var a = e.getNode('tag:a'); * // do something with the link that was clicked * }); * @@ -860,10 +855,29 @@ JX.install('Event', { * - sigil - first node of each sigil * @task info */ - getNode: function(key) { + getNode : function(key) { return this.getNodes()[key] || null; - } + }, + + /** + * Get the metadata associated with the node that corresponds to the key + * in this event's node map. This is a simple helper method that makes + * the API for accessing metadata associated with specific nodes less ugly. + * + * JX.Stratcom.listen('click', 'tag:a', function(event) { + * var anchorData = event.getNodeData('tag:a'); + * // do something with the metadata of the link that was clicked + * }); + * + * @param string sigil or stratcom node key + * @return dict dictionary of the node's metadata + * @task info + */ + getNodeData : function(key) { + // Evade static analysis - JX.Stratcom + return JX['Stratcom'].getData(this.getNode(key)); + } }, statics : { @@ -876,10 +890,10 @@ JX.install('Event', { 38 : 'up', 39 : 'right', 40 : 'down', - 63232 : 38, - 63233 : 40, - 62234 : 37, - 62235 : 39 + 63232 : 'up', + 63233 : 'down', + 62234 : 'left', + 62235 : 'right' } }, @@ -1006,6 +1020,7 @@ JX.install('Event', { * @task listen Listening to Events * @task handle Responding to Events * @task sigil Managing Sigils + * @task meta Managing Metadata * @task internal Internals */ JX.install('Stratcom', { @@ -1014,8 +1029,6 @@ JX.install('Stratcom', { _targets : {}, _handlers : [], _need : {}, - _matchName : /\bFN_([^ ]+)/, - _matchData : /\bFD_([^ ]+)_([^ ]+)/, _auto : '*', _data : {}, _execContext : [], @@ -1037,13 +1050,13 @@ JX.install('Stratcom', { /** * Within each datablock, data is identified by a unique index. The data - * pointer on a node looks like this: + * pointer (data-meta attribute) on a node looks like this: * - * FD_1_2 + * 1_2 * * ...where 1 is the block, and 2 is the index within that block. Normally, * blocks are filled on the server side, so index allocation takes place - * there. However, when data is provided with JX.Stratcom.sigilize(), we + * there. However, when data is provided with JX.Stratcom.addData(), we * need to allocate indexes on the client. */ _dataIndex : 0, @@ -1183,9 +1196,12 @@ JX.install('Stratcom', { if (path[kk] == 'tag:#document') { throw new Error( 'JX.Stratcom.listen(..., "tag:#document", ...): ' + - 'listen for document events as "tag:window", not ' + - '"tag:#document", in order to get consistent behavior ' + - 'across browsers.'); + 'listen for all events using null, not "tag:#document"'); + } + if (path[kk] == 'tag:window') { + throw new Error( + 'JX.Stratcom.listen(..., "tag:window", ...): ' + + 'listen for window events using null, not "tag:window"'); } } if (!type_target[path[kk]]) { @@ -1217,29 +1233,27 @@ JX.install('Stratcom', { * @task internal */ dispatch : function(event) { - // TODO: simplify this :P - var target; - try { - target = event.srcElement || event.target; - if (target === window || (!target || target.nodeName == '#document')) { - target = {nodeName: 'window'}; - } - } catch (x) { - target = null; - } - var path = []; var nodes = {}; var push = function(key, node) { // we explicitly only store the first occurrence of each key - if (!(key in nodes)) { + if (!nodes.hasOwnProperty(key)) { nodes[key] = node; path.push(key); } }; + var target = event.srcElement || event.target; + + // Since you can only listen by tag, id or sigil, which are all + // attributes of an Element (the DOM interface), we unset the target + // if it isn't an Element (window and Document are Nodes but not Elements) + if (!target || !target.getAttribute) { + target = null; + } + var cursor = target; - while (cursor) { + while (cursor && cursor.getAttribute) { push('tag:' + cursor.nodeName.toLowerCase(), cursor); var id = cursor.id; @@ -1247,11 +1261,12 @@ JX.install('Stratcom', { push('id:' + id, cursor); } - var source = cursor.className || ''; - // className is an SVGAnimatedString for SVG elements, use baseVal - var token = ((source.baseVal || source).match(this._matchName) || [])[1]; - if (token) { - push(token, cursor); + var sigils = cursor.getAttribute('data-sigil'); + if (sigils) { + sigils = sigils.split(' '); + for (var ii = 0; ii < sigils.length; ii++) { + push(sigils[ii], cursor); + } } cursor = cursor.parentNode; @@ -1262,16 +1277,10 @@ JX.install('Stratcom', { etype = this._typeMap[etype]; } - var data = {}; - for (var key in nodes) { - data[key] = this.getData(nodes[key]); - } - var proxy = new JX.Event() .setRawEvent(event) .setType(etype) .setTarget(target) - .setData(data) .setNodes(nodes) .setPath(path.reverse()); @@ -1407,118 +1416,129 @@ JX.install('Stratcom', { /** - * Attach a sigil (and, optionally, metadata) to a node. Note that you can - * not overwrite, remove or replace a sigil. + * Determine if a node has a specific sigil. + * + * @param Node Node to test. + * @param string Sigil to check for. + * @return bool True if the node has the sigil. * - * @param Node Node without any sigil. - * @param string Sigil to name the node with. - * @param object? Optional metadata object to attach to the node. - * @return void * @task sigil */ - sigilize : function(node, sigil, data) { + hasSigil : function(node, sigil) { if (__DEV__) { - if (node.className.match(this._matchName)) { - throw new Error( - 'JX.Stratcom.sigilize(, ' + sigil + ', ...): ' + - 'node already has a sigil, sigils may not be overwritten.'); - } - if (typeof data != 'undefined' && - (data === null || typeof data != 'object')) { + if (!node || !node.getAttribute) { throw new Error( - 'JX.Stratcom.sigilize(..., ..., ): ' + - 'data to attach to node is not an object. You must use ' + - 'objects, not primitives, for metadata.'); + 'JX.Stratcom.hasSigil(, ...): ' + + 'node is not an element. Most likely, you\'re passing window or ' + + 'document, which are not elements and can\'t have sigils.'); } } - if (data) { - JX.Stratcom._setData(node, data); - } - - node.className = 'FN_' + sigil + ' ' + node.className; + var sigils = node.getAttribute('data-sigil'); + return sigils && (' ' + sigils + ' ').indexOf(' ' + sigil + ' ') > -1; }, /** - * Determine if a node has a specific sigil. - * - * @param Node Node to test. - * @param string Sigil to check for. - * @return bool True if the node has the sigil. + * Add a sigil to a node. * + * @param Node Node to add the sigil to. + * @param string Sigil to name the node with. + * @return void * @task sigil */ - hasSigil : function(node, sigil) { - if (!node.className) { - // Some nodes don't have a className, notably 'document'. We hit - // 'document' when following .parentNode chains, e.g. in - // JX.DOM.nearest(), so exit early if we don't have a className to avoid - // fataling on 'node.className.match' being undefined. - return false; - } - return (node.className.match(this._matchName) || [])[1] == sigil; + addSigil: function(node, sigil) { + if (__DEV__) { + if (!node || !node.getAttribute) { + throw new Error( + 'JX.Stratcom.addSigil(, ...): ' + + 'node is not an element. Most likely, you\'re passing window or ' + + 'document, which are not elements and can\'t have sigils.'); + } + } + + var sigils = node.getAttribute('data-sigil'); + if (sigils && !JX.Stratcom.hasSigil(node, sigil)) { + sigil = sigils + ' ' + sigil; + } + + node.setAttribute('data-sigil', sigil); }, /** * Retrieve a node's metadata. * - * @param Node Node from which to retrieve data. - * @return object Data attached to the node, or an empty dictionary if - * the node has no data attached. In this case, the empty - * dictionary is set as the node's metadata -- i.e., - * subsequent calls to getData() will retrieve the same - * object. - * - * @task sigil + * @param Node Node from which to retrieve data. + * @return object Data attached to the node. If no data has been attached + * to the node yet, an empty object will be returned, but + * subsequent calls to this method will always retrieve the + * same object. + * @task meta */ getData : function(node) { if (__DEV__) { - if (!node) { + if (!node || !node.getAttribute) { throw new Error( - 'JX.Stratcom.getData(): ' + - 'you must provide a node to get associated data from.'); + 'JX.Stratcom.getData(): ' + + 'node is not an element. Most likely, you\'re passing window or ' + + 'document, which are not elements and can\'t have data.'); } } - var matches = (node.className || '').match(this._matchData); - if (matches) { - var block = this._data[matches[1]]; - var index = matches[2]; + var meta_id = (node.getAttribute('data-meta') || '').split('_'); + if (meta_id[0] && meta_id[1]) { + var block = this._data[meta_id[0]]; + var index = meta_id[1]; if (block && (index in block)) { return block[index]; } } - return JX.Stratcom._setData(node, {}); + var data = {}; + if (!this._data[1]) { // data block 1 is reserved for JavaScript + this._data[1] = {}; + } + this._data[1][this._dataIndex] = data; + node.setAttribute('data-meta', '1_' + (this._dataIndex++)); + return data; }, - /** - * @task internal + /** + * Add data to a node's metadata. + * + * @param Node Node which data should be attached to. + * @param object Data to add to the node's metadata. + * @return object Data attached to the node that is returned by + * JX.Stratcom.getData(). + * @task meta */ - allocateMetadataBlock : function() { - return this._dataBlock++; + addData : function(node, data) { + if (__DEV__) { + if (!node || !node.getAttribute) { + throw new Error( + 'JX.Stratcom.addData(, ...): ' + + 'node is not an element. Most likely, you\'re passing window or ' + + 'document, which are not elements and can\'t have sigils.'); + } + if (!data || typeof data != 'object') { + throw new Error( + 'JX.Stratcom.addData(..., ): ' + + 'data to attach to node is not an object. You must use ' + + 'objects, not primitives, for metadata.'); + } + } + + return JX.copy(JX.Stratcom.getData(node), data); }, + /** - * Attach metadata to a node. This data can later be retrieved through - * @{JX.Stratcom.getData()}, or @{JX.Event.getData()}. - * - * @param Node Node which data should be attached to. - * @param object Data to attach. - * @return object Attached data. - * * @task internal */ - _setData : function(node, data) { - if (!this._data[1]) { // data block 1 is reserved for javascript - this._data[1] = {}; - } - this._data[1][this._dataIndex] = data; - node.className = 'FD_1_' + (this._dataIndex++) + ' ' + node.className; - return data; + allocateMetadataBlock : function() { + return this._dataBlock++; } } }); @@ -1533,7 +1553,7 @@ JX.install('Stratcom', { JX.behavior = function(name, control_function) { if (__DEV__) { - if (name in JX.behavior._behaviors) { + if (JX.behavior._behaviors.hasOwnProperty(name)) { throw new Error( 'JX.behavior("'+name+'", ...): '+ 'behavior is already registered.'); @@ -1564,7 +1584,7 @@ JX.initBehaviors = function(map) { } var configs = map[name]; if (!configs.length) { - if (name in JX.behavior._initialized) { + if (JX.behavior._initialized.hasOwnProperty(name)) { continue; } else { configs = [null]; @@ -1630,7 +1650,7 @@ JX.install('Request', { xport.onreadystatechange = JX.bind(this, this._onreadystatechange); var data = this.getData() || {}; - data.__async__ = true; + data.__ajax__ = true; this._block = JX.Stratcom.allocateMetadataBlock(); data.__metablock__ = this._block; @@ -1825,7 +1845,7 @@ JX.install('Request', { }, initialize : function() { - JX.Stratcom.listen('unload', 'tag:window', JX.Request.shutdown); + JX.Stratcom.listen('unload', null, JX.Request.shutdown); } }); @@ -2408,8 +2428,12 @@ JX.$N = function(tag, attr, content) { } if (attr.sigil) { - JX.Stratcom.sigilize(node, attr.sigil, attr.meta); + JX.Stratcom.addSigil(node, attr.sigil); delete attr.sigil; + } + + if (attr.meta) { + JX.Stratcom.addData(node, attr.meta); delete attr.meta; } @@ -2419,17 +2443,6 @@ JX.$N = function(tag, attr, content) { '$N(' + tag + ', ...): ' + 'use the key "meta" to specify metadata, not "data" or "metadata".'); } - if (attr.meta) { - throw new Error( - '$N(' + tag + ', ...): ' + - 'if you specify "meta" metadata, you must also specify a "sigil".'); - } - } - - // prevent sigil from being wiped by blind copying the className - if (attr.className) { - JX.DOM.alterClass(node, attr.className, true); - delete attr.className; } JX.copy(node, attr); @@ -2593,7 +2606,7 @@ JX.install('DOM', { * @author jgabbard */ nearest : function(node, sigil) { - while (node && !JX.Stratcom.hasSigil(node, sigil)) { + while (node && node.getAttribute && !JX.Stratcom.hasSigil(node, sigil)) { node = node.parentNode; } return node; diff --git a/pkg/javelin.min.js b/pkg/javelin.min.js index c237103..526d12a 100644 --- a/pkg/javelin.min.js +++ b/pkg/javelin.min.js @@ -1 +1 @@ -JX.$A=function(b){var c=[];for(var a=0;a=300){this._z();return;}var text=xport.responseText.substring('for (;;);'.length);var response=eval('('+text+')');}catch(exception){this._z();return;}try{if(response.error){this._z(response.error);}else{JX.Stratcom.mergeData(this._v,response.javelin_metadata||{});this._zb(response);JX.initBehaviors(response.javelin_behaviors||{});}}catch(exception){JX.defer(function(){throw exception;});}},_z:function(a){this._za();this.invoke('error',a,this);this.invoke('finally');},_zb:function(b){this._za();if(b.onload)for(var a=0;a-1);if(a&&!c){d.className+=' '+b;}else if(c&&!a)d.className=d.className.replace(new RegExp('(^|\\s)'+b+'(?:\\s|$)','g'),' ');},htmlize:function(a){return (''+a).replace(/&/g,'&').replace(/"/g,'"').replace(//g,'>');},show:function(){for(var a=0;a')));var a=JX.$V.getDim(d);document.body.removeChild(d);return a;},scry:function(d,f,e){var b=d.getElementsByTagName(f);if(!e)return JX.$A(b);var c=[];for(var a=0;a-1;},addSigil:function(a,b){var c=a.getAttribute('data-sigil');if(c&&!JX.Stratcom.hasSigil(a,b))b=c+' '+b;a.setAttribute('data-sigil',b);},getData:function(e){var d=(e.getAttribute('data-meta')||'').split('_');if(d[0]&&d[1]){var a=this._h[d[0]];var c=d[1];if(a&&(c in a))return a[c];}var b={};if(!this._h[1])this._h[1]={};this._h[1][this._l]=b;e.setAttribute('data-meta','1_'+(this._l++));return b;},addData:function(b,a){return JX.copy(JX.Stratcom.getData(b),a);},allocateMetadataBlock:function(){return this._k++;}}});JX.behavior=function(b,a){JX.behavior._n[b]=a;};JX.initBehaviors=function(c){for(var d in c){var a=c[d];if(!a.length)if(JX.behavior._o.hasOwnProperty(d)){continue;}else a=[null];for(var b=0;b=300){this._w();return;}var text=xport.responseText.substring('for (;;);'.length);var response=eval('('+text+')');}catch(exception){this._w();return;}try{if(response.error){this._w(response.error);}else{JX.Stratcom.mergeData(this._s,response.javelin_metadata||{});this._y(response);JX.initBehaviors(response.javelin_behaviors||{});}}catch(exception){JX.defer(function(){throw exception;});}},_w:function(a){this._x();this.invoke('error',a,this);this.invoke('finally');},_y:function(b){this._x();if(b.onload)for(var a=0;a-1);if(a&&!c){d.className+=' '+b;}else if(c&&!a)d.className=d.className.replace(new RegExp('(^|\\s)'+b+'(?:\\s|$)','g'),' ');},htmlize:function(a){return (''+a).replace(/&/g,'&').replace(/"/g,'"').replace(//g,'>');},show:function(){for(var a=0;a')));var a=JX.$V.getDim(d);document.body.removeChild(d);return a;},scry:function(d,f,e){var b=d.getElementsByTagName(f);if(!e)return JX.$A(b);var c=[];for(var a=0;a=0&&this._i=0&&this._d[this._i]){this._f(this._d[this._i]);return true;}else{result=this.invoke('query',this._b.value);if(result.getPrevented())return true;}return false;},setValue:function(a){this._b.value=a;},getValue:function(){return this._b.value;},_m:function(event){var a=event&&event.getSpecialKey();if(a&&event.getType()=='keydown')switch(a){case 'up':if(this._d.length&&this._j(-1))event.prevent();break;case 'down':if(this._d.length&&this._j(1))event.prevent();break;case 'return':if(this.submit()){event.prevent();return;}break;case 'esc':if(this._d.length&&this.getAllowNullSelection()){this.hide();event.prevent();}break;case 'tab':return;}JX.defer(JX.bind(this,function(){if(this._g==this._b.value)return;this.refresh();}));},handleEvent:function(a){if(this._h||a.getPrevented())return;var b=a.getType();if(b=='blur'){this.hide();}else this._m(a);}}});JX.install('TypeaheadNormalizer',{statics:{normalize:function(a){return (''+a).toLowerCase().replace(/[^a-z0-9 ]/g,'').replace(/ +/g,' ').replace(/^\s*|\s*$/g,'');}}});JX.install('TypeaheadSource',{construct:function(){this._n={};this._o={};this.setNormalizer(JX.TypeaheadNormalizer.normalize);},properties:{normalizer:null,transformer:null,maximumResultCount:5},members:{_n:null,_o:null,_p:null,_q:null,bindToTypeahead:function(a){this._p=a;a.listen('change',JX.bind(this,this.didChange));a.listen('start',JX.bind(this,this.didStart));},didChange:function(a){return;},didStart:function(){return;},addResult:function(b){b=(this.getTransformer()||this._r)(b);if(b.id in this._n)return;this._n[b.id]=b;var c=this.tokenize(b.name);for(var a=0;a=0&&this._i=0&&this._d[this._i]){this._f(this._d[this._i]);return true;}else{result=this.invoke('query',this._b.value);if(result.getPrevented())return true;}return false;},setValue:function(a){this._b.value=a;},getValue:function(){return this._b.value;},_m:function(event){var a=event&&event.getSpecialKey();if(a&&event.getType()=='keydown')switch(a){case 'up':if(this._d.length&&this._j(-1))event.prevent();break;case 'down':if(this._d.length&&this._j(1))event.prevent();break;case 'return':if(this.submit()){event.prevent();return;}break;case 'esc':if(this._d.length&&this.getAllowNullSelection()){this.hide();event.prevent();}break;case 'tab':return;}JX.defer(JX.bind(this,function(){if(this._g==this._b.value)return;this.refresh();}));},handleEvent:function(a){if(this._h||a.getPrevented())return;var b=a.getType();if(b=='blur'){this.hide();}else this._m(a);}}});JX.install('TypeaheadNormalizer',{statics:{normalize:function(a){return (''+a).toLowerCase().replace(/[^a-z0-9 ]/g,'').replace(/ +/g,' ').replace(/^\s*|\s*$/g,'');}}});JX.install('TypeaheadSource',{construct:function(){this._n={};this._o={};this.setNormalizer(JX.TypeaheadNormalizer.normalize);},properties:{normalizer:null,transformer:null,maximumResultCount:5},members:{_n:null,_o:null,_p:null,_q:null,bindToTypeahead:function(a){this._p=a;a.listen('change',JX.bind(this,this.didChange));a.listen('start',JX.bind(this,this.didStart));},didChange:function(a){return;},didStart:function(){return;},addResult:function(b){b=(this.getTransformer()||this._r)(b);if(b.id in this._n)return;this._n[b.id]=b;var c=this.tokenize(b.name);for(var a=0;a, ...): '+ + 'bogus URI provided when creating workflow.'); + } + } + this.setURI(uri); + this.setData(data || {}); + }, + + events : ['error', 'finally', 'submit'], + + statics : { + _stack : [], + newFromForm : function(form, data) { + var inputs = [].concat( + JX.DOM.scry(form, 'input'), + JX.DOM.scry(form, 'button'), + JX.DOM.scry(form, 'textarea')); + + for (var ii = 0; ii < inputs.length; ii++) { + if (inputs[ii].disabled) { + delete inputs[ii]; + } else { + inputs[ii].disabled = true; + } + } + + var workflow = new JX.Workflow( + form.getAttribute('action'), + JX.copy(data || {}, JX.DOM.serialize(form))); + workflow.setMethod(form.getAttribute('method')); + workflow.listen('finally', function() { + for (var ii = 0; ii < inputs.length; ii++) { + inputs[ii] && (inputs[ii].disabled = false); + } + }); + return workflow; + }, + newFromLink : function(link) { + var workflow = new JX.Workflow(link.href); + return workflow; + }, + _push : function(workflow) { + JX.Mask.show(); + JX.Workflow._stack.push(workflow); + }, + _pop : function() { + var dialog = JX.Workflow._stack.pop(); + (dialog.getCloseHandler() || JX.bag)(); + dialog._destroy(); + JX.Mask.hide(); + }, + disable : function() { + JX.Workflow._disabled = true; + }, + _onbutton : function(event) { + + if (JX.Stratcom.pass()) { + return; + } + + if (JX.Workflow._disabled) { + return; + } + var t = event.getTarget(); + if (t.name == '__cancel__' || t.name == '__close__') { + JX.Workflow._pop(); + } else { + + var form = event.getNode('jx-dialog'); + var data = JX.DOM.serialize(form); + data[t.name] = true; + data.__wflow__ = true; + + var active = JX.Workflow._stack[JX.Workflow._stack.length - 1]; + var e = active.invoke('submit', {form: form, data: data}); + if (!e.getStopped()) { + active._destroy(); + active + .setURI(form.getAttribute('action') || active.getURI()) + .setData(data) + .start(); + } + } + event.prevent(); + } + }, + + members : { + _root : null, + _pushed : false, + _onload : function(r) { + // It is permissible to send back a falsey redirect to force a page + // reload, so we need to take this branch if the key is present. + if (r && (typeof r.redirect != 'undefined')) { + JX.go(r.redirect, true); + } else if (r && r.dialog) { + this._push(); + this._root = JX.$N( + 'div', + {className: 'jx-client-dialog'}, + JX.HTML(r.dialog)); + JX.DOM.listen( + this._root, + 'click', + 'tag:button', + JX.Workflow._onbutton); + document.body.appendChild(this._root); + var d = JX.$V.getDim(this._root); + var v = JX.$V.getViewport(); + var s = JX.$V.getScroll(); + JX.$V((v.x - d.x) / 2, s.y + 100).setPos(this._root); + try { + JX.DOM.focus(JX.DOM.find(this._root, 'button', '__default__')); + var inputs = JX.DOM.scry(this._root, 'input') + .concat(JX.DOM.scry(this._root, 'textarea')); + var miny = Number.POSITIVE_INFINITY; + var target = null; + for (var ii = 0; ii < inputs.length; ++ii) { + if (inputs[ii].type != 'hidden') { + // Find the topleft-most displayed element. + var p = JX.$V(inputs[ii]); + if (p.y < miny) { + miny = p.y; + target = inputs[ii]; + } + } + } + target && JX.DOM.focus(target); + } catch (_ignored) {} + } else if (this.getHandler()) { + this.getHandler()(r); + this._pop(); + } else if (r) { + if (__DEV__) { + throw new Error('Response to workflow request went unhandled.'); + } + } + }, + _push : function() { + if (!this._pushed) { + this._pushed = true; + JX.Workflow._push(this); + } + }, + _pop : function() { + if (this._pushed) { + this._pushed = false; + JX.Workflow._pop(); + } + }, + _destroy : function() { + if (this._root) { + JX.DOM.remove(this._root); + this._root = null; + } + }, + start : function() { + var uri = this.getURI(); + var method = this.getMethod(); + var r = new JX.Request(uri, JX.bind(this, this._onload)); + r.setData(this.getData()); + r.setDataSerializer(this.getDataSerializer()); + if (method) { + r.setMethod(method); + } + r.listen('finally', JX.bind(this, this.invoke, 'finally')); + r.listen('error', JX.bind(this, function(error) { + var e = this.invoke('error', error); + if (e.getStopped()) { + return; + } + // TODO: Default error behavior? On Facebook Lite, we just shipped the + // user to "/error/". We could emit a blanket 'workflow-failed' type + // event instead. + })); + r.send(); + } + }, + + properties : { + handler : null, + closeHandler : null, + data : null, + dataSerializer : null, + method : null, + URI : null + } + +}); diff --git a/pkg/workflow.min.js b/pkg/workflow.min.js new file mode 100644 index 0000000..0620721 --- /dev/null +++ b/pkg/workflow.min.js @@ -0,0 +1 @@ +JX.install('Mask',{statics:{_a:0,_b:null,show:function(){if(!JX.Mask._a){JX.Mask._b=JX.$N('div',{className:'jx-mask'});document.body.appendChild(JX.Mask._b);JX.$V.getDocument().setDim(JX.Mask._b);}++JX.Mask._a;},hide:function(){--JX.Mask._a;if(!JX.Mask._a){JX.DOM.remove(JX.Mask._b);JX.Mask._b=null;}}}});JX.install('Workflow',{construct:function(b,a){this.setURI(b);this.setData(a||{});},events:['error','finally','submit'],statics:{_c:[],newFromForm:function(b,a){var d=[].concat(JX.DOM.scry(b,'input'),JX.DOM.scry(b,'button'),JX.DOM.scry(b,'textarea'));for(var c=0;c, ' + sigil + ', ...): ' + - 'node already has a sigil, sigils may not be overwritten.'); + 'JX.Stratcom.hasSigil(, ...): ' + + 'node is not an element. Most likely, you\'re passing window or ' + + 'document, which are not elements and can\'t have sigils.'); } - if (typeof data != 'undefined' && - (data === null || typeof data != 'object')) { - throw new Error( - 'JX.Stratcom.sigilize(..., ..., ): ' + - 'data to attach to node is not an object. You must use ' + - 'objects, not primitives, for metadata.'); - } - } - - if (data) { - JX.Stratcom._setData(node, data); } - node.className = 'FN_' + sigil + ' ' + node.className; + var sigils = node.getAttribute('data-sigil'); + return sigils && (' ' + sigils + ' ').indexOf(' ' + sigil + ' ') > -1; }, /** - * Determine if a node has a specific sigil. - * - * @param Node Node to test. - * @param string Sigil to check for. - * @return bool True if the node has the sigil. + * Add a sigil to a node. * + * @param Node Node to add the sigil to. + * @param string Sigil to name the node with. + * @return void * @task sigil */ - hasSigil : function(node, sigil) { - if (!node.className) { - // Some nodes don't have a className, notably 'document'. We hit - // 'document' when following .parentNode chains, e.g. in - // JX.DOM.nearest(), so exit early if we don't have a className to avoid - // fataling on 'node.className.match' being undefined. - return false; + addSigil: function(node, sigil) { + if (__DEV__) { + if (!node || !node.getAttribute) { + throw new Error( + 'JX.Stratcom.addSigil(, ...): ' + + 'node is not an element. Most likely, you\'re passing window or ' + + 'document, which are not elements and can\'t have sigils.'); + } + } + + var sigils = node.getAttribute('data-sigil'); + if (sigils && !JX.Stratcom.hasSigil(node, sigil)) { + sigil = sigils + ' ' + sigil; } - return (node.className.match(this._matchName) || [])[1] == sigil; + + node.setAttribute('data-sigil', sigil); }, /** * Retrieve a node's metadata. * - * @param Node Node from which to retrieve data. - * @return object Data attached to the node, or an empty dictionary if - * the node has no data attached. In this case, the empty - * dictionary is set as the node's metadata -- i.e., - * subsequent calls to getData() will retrieve the same - * object. - * - * @task sigil + * @param Node Node from which to retrieve data. + * @return object Data attached to the node. If no data has been attached + * to the node yet, an empty object will be returned, but + * subsequent calls to this method will always retrieve the + * same object. + * @task meta */ getData : function(node) { if (__DEV__) { - if (!node) { + if (!node || !node.getAttribute) { throw new Error( - 'JX.Stratcom.getData(): ' + - 'you must provide a node to get associated data from.'); + 'JX.Stratcom.getData(): ' + + 'node is not an element. Most likely, you\'re passing window or ' + + 'document, which are not elements and can\'t have data.'); } } - var matches = (node.className || '').match(this._matchData); - if (matches) { - var block = this._data[matches[1]]; - var index = matches[2]; + var meta_id = (node.getAttribute('data-meta') || '').split('_'); + if (meta_id[0] && meta_id[1]) { + var block = this._data[meta_id[0]]; + var index = meta_id[1]; if (block && (index in block)) { return block[index]; } } - return JX.Stratcom._setData(node, {}); + var data = {}; + if (!this._data[1]) { // data block 1 is reserved for JavaScript + this._data[1] = {}; + } + this._data[1][this._dataIndex] = data; + node.setAttribute('data-meta', '1_' + (this._dataIndex++)); + return data; }, - /** - * @task internal + /** + * Add data to a node's metadata. + * + * @param Node Node which data should be attached to. + * @param object Data to add to the node's metadata. + * @return object Data attached to the node that is returned by + * JX.Stratcom.getData(). + * @task meta */ - allocateMetadataBlock : function() { - return this._dataBlock++; + addData : function(node, data) { + if (__DEV__) { + if (!node || !node.getAttribute) { + throw new Error( + 'JX.Stratcom.addData(, ...): ' + + 'node is not an element. Most likely, you\'re passing window or ' + + 'document, which are not elements and can\'t have sigils.'); + } + if (!data || typeof data != 'object') { + throw new Error( + 'JX.Stratcom.addData(..., ): ' + + 'data to attach to node is not an object. You must use ' + + 'objects, not primitives, for metadata.'); + } + } + + return JX.copy(JX.Stratcom.getData(node), data); }, + /** - * Attach metadata to a node. This data can later be retrieved through - * @{JX.Stratcom.getData()}, or @{JX.Event.getData()}. - * - * @param Node Node which data should be attached to. - * @param object Data to attach. - * @return object Attached data. - * * @task internal */ - _setData : function(node, data) { - if (!this._data[1]) { // data block 1 is reserved for javascript - this._data[1] = {}; - } - this._data[1][this._dataIndex] = data; - node.className = 'FD_1_' + (this._dataIndex++) + ' ' + node.className; - return data; + allocateMetadataBlock : function() { + return this._dataBlock++; } } }); diff --git a/src/Workflow.js b/src/Workflow.js new file mode 100644 index 0000000..f2b700c --- /dev/null +++ b/src/Workflow.js @@ -0,0 +1,206 @@ +/** + * @requires javelin-stratcom + * javelin-request + * javelin-dom + * javelin-vector + * javelin-install + * javelin-util + * javelin-mask + * @provides javelin-workflow + * @javelin + */ + +JX.install('Workflow', { + construct : function(uri, data) { + if (__DEV__) { + if (!uri || uri == '#') { + throw new Error( + 'new JX.Workflow(, ...): '+ + 'bogus URI provided when creating workflow.'); + } + } + this.setURI(uri); + this.setData(data || {}); + }, + + events : ['error', 'finally', 'submit'], + + statics : { + _stack : [], + newFromForm : function(form, data) { + var inputs = [].concat( + JX.DOM.scry(form, 'input'), + JX.DOM.scry(form, 'button'), + JX.DOM.scry(form, 'textarea')); + + for (var ii = 0; ii < inputs.length; ii++) { + if (inputs[ii].disabled) { + delete inputs[ii]; + } else { + inputs[ii].disabled = true; + } + } + + var workflow = new JX.Workflow( + form.getAttribute('action'), + JX.copy(data || {}, JX.DOM.serialize(form))); + workflow.setMethod(form.getAttribute('method')); + workflow.listen('finally', function() { + for (var ii = 0; ii < inputs.length; ii++) { + inputs[ii] && (inputs[ii].disabled = false); + } + }); + return workflow; + }, + newFromLink : function(link) { + var workflow = new JX.Workflow(link.href); + return workflow; + }, + _push : function(workflow) { + JX.Mask.show(); + JX.Workflow._stack.push(workflow); + }, + _pop : function() { + var dialog = JX.Workflow._stack.pop(); + (dialog.getCloseHandler() || JX.bag)(); + dialog._destroy(); + JX.Mask.hide(); + }, + disable : function() { + JX.Workflow._disabled = true; + }, + _onbutton : function(event) { + + if (JX.Stratcom.pass()) { + return; + } + + if (JX.Workflow._disabled) { + return; + } + var t = event.getTarget(); + if (t.name == '__cancel__' || t.name == '__close__') { + JX.Workflow._pop(); + } else { + + var form = event.getNode('jx-dialog'); + var data = JX.DOM.serialize(form); + data[t.name] = true; + data.__wflow__ = true; + + var active = JX.Workflow._stack[JX.Workflow._stack.length - 1]; + var e = active.invoke('submit', {form: form, data: data}); + if (!e.getStopped()) { + active._destroy(); + active + .setURI(form.getAttribute('action') || active.getURI()) + .setData(data) + .start(); + } + } + event.prevent(); + } + }, + + members : { + _root : null, + _pushed : false, + _onload : function(r) { + // It is permissible to send back a falsey redirect to force a page + // reload, so we need to take this branch if the key is present. + if (r && (typeof r.redirect != 'undefined')) { + JX.go(r.redirect, true); + } else if (r && r.dialog) { + this._push(); + this._root = JX.$N( + 'div', + {className: 'jx-client-dialog'}, + JX.HTML(r.dialog)); + JX.DOM.listen( + this._root, + 'click', + 'tag:button', + JX.Workflow._onbutton); + document.body.appendChild(this._root); + var d = JX.$V.getDim(this._root); + var v = JX.$V.getViewport(); + var s = JX.$V.getScroll(); + JX.$V((v.x - d.x) / 2, s.y + 100).setPos(this._root); + try { + JX.DOM.focus(JX.DOM.find(this._root, 'button', '__default__')); + var inputs = JX.DOM.scry(this._root, 'input') + .concat(JX.DOM.scry(this._root, 'textarea')); + var miny = Number.POSITIVE_INFINITY; + var target = null; + for (var ii = 0; ii < inputs.length; ++ii) { + if (inputs[ii].type != 'hidden') { + // Find the topleft-most displayed element. + var p = JX.$V(inputs[ii]); + if (p.y < miny) { + miny = p.y; + target = inputs[ii]; + } + } + } + target && JX.DOM.focus(target); + } catch (_ignored) {} + } else if (this.getHandler()) { + this.getHandler()(r); + this._pop(); + } else if (r) { + if (__DEV__) { + throw new Error('Response to workflow request went unhandled.'); + } + } + }, + _push : function() { + if (!this._pushed) { + this._pushed = true; + JX.Workflow._push(this); + } + }, + _pop : function() { + if (this._pushed) { + this._pushed = false; + JX.Workflow._pop(); + } + }, + _destroy : function() { + if (this._root) { + JX.DOM.remove(this._root); + this._root = null; + } + }, + start : function() { + var uri = this.getURI(); + var method = this.getMethod(); + var r = new JX.Request(uri, JX.bind(this, this._onload)); + r.setData(this.getData()); + r.setDataSerializer(this.getDataSerializer()); + if (method) { + r.setMethod(method); + } + r.listen('finally', JX.bind(this, this.invoke, 'finally')); + r.listen('error', JX.bind(this, function(error) { + var e = this.invoke('error', error); + if (e.getStopped()) { + return; + } + // TODO: Default error behavior? On Facebook Lite, we just shipped the + // user to "/error/". We could emit a blanket 'workflow-failed' type + // event instead. + })); + r.send(); + } + }, + + properties : { + handler : null, + closeHandler : null, + data : null, + dataSerializer : null, + method : null, + URI : null + } + +}); diff --git a/src/behavior.js b/src/behavior.js index 8c04809..b3a5b8c 100644 --- a/src/behavior.js +++ b/src/behavior.js @@ -9,7 +9,7 @@ JX.behavior = function(name, control_function) { if (__DEV__) { - if (name in JX.behavior._behaviors) { + if (JX.behavior._behaviors.hasOwnProperty(name)) { throw new Error( 'JX.behavior("'+name+'", ...): '+ 'behavior is already registered.'); @@ -40,7 +40,7 @@ JX.initBehaviors = function(map) { } var configs = map[name]; if (!configs.length) { - if (name in JX.behavior._initialized) { + if (JX.behavior._initialized.hasOwnProperty(name)) { continue; } else { configs = [null]; diff --git a/src/control/tokenizer/Tokenizer.js b/src/control/tokenizer/Tokenizer.js index 7bdcab3..0aab4b6 100644 --- a/src/control/tokenizer/Tokenizer.js +++ b/src/control/tokenizer/Tokenizer.js @@ -64,11 +64,7 @@ JX.install('Tokenizer', { this._tokens = []; this._tokenMap = {}; - var focus = JX.$N('input', { - className: 'jx-tokenizer-input', - type: 'text', - value: this._orig.value - }); + var focus = this.buildInput(this._orig.value); this._focus = focus; JX.DOM.listen( @@ -84,8 +80,8 @@ JX.install('Tokenizer', { JX.bind( this, function(e) { - if (e.getNodes().remove) { - this._remove(e.getData().token.key); + if (e.getNode('remove')) { + this._remove(e.getNodeData('token').key); } else if (e.getTarget() == this._root) { this.focus(); } @@ -225,20 +221,7 @@ JX.install('Tokenizer', { var focus = this._focus; var root = this._root; - - var token = JX.$N('a', { - className: 'jx-tokenizer-token' - }, value); - - var input = JX.$N('input', { - type: 'hidden', - value: key, - name: this._orig.name+'['+(this._seq++)+']' - }); - - var remove = JX.$N('a', { - className: 'jx-tokenizer-x' - }, JX.HTML('×')); + var token = this.buildToken(key, value); this._tokenMap[key] = { value : value, @@ -247,17 +230,42 @@ JX.install('Tokenizer', { }; this._tokens.push(key); - JX.Stratcom.sigilize(token, 'token', {key : key}); - JX.Stratcom.sigilize(remove, 'remove'); - - token.appendChild(input); - token.appendChild(remove); - root.insertBefore(token, focus); return true; }, + buildInput: function(value) { + return JX.$N('input', { + className: 'jx-tokenizer-input', + type: 'text', + value: value + }); + }, + + /** + * Generate a token based on a key and value. The "token" and "remove" + * sigils are observed by a listener in start(). + */ + buildToken: function(key, value) { + var input = JX.$N('input', { + type: 'hidden', + value: key, + name: this._orig.name + '[' + (this._seq++) + ']' + }); + + var remove = JX.$N('a', { + className: 'jx-tokenizer-x', + sigil: 'remove' + }, JX.HTML('×')); + + return JX.$N('a', { + className: 'jx-tokenizer-token', + sigil: 'token', + meta: {key: key} + }, [value, input, remove]); + }, + getTokens : function() { var result = {}; for (var key in this._tokenMap) { diff --git a/src/control/typeahead/Typeahead.js b/src/control/typeahead/Typeahead.js index c105716..30080d1 100644 --- a/src/control/typeahead/Typeahead.js +++ b/src/control/typeahead/Typeahead.js @@ -84,7 +84,7 @@ JX.install('Typeahead', { 'mousedown', 'tag:a', JX.bind(this, function(e) { - this._choose(e.getTarget()); + this._choose(e.getNode('tag:a')); e.prevent(); })); diff --git a/src/control/typeahead/source/TypeaheadSource.js b/src/control/typeahead/source/TypeaheadSource.js index 1120e67..51ca20d 100644 --- a/src/control/typeahead/source/TypeaheadSource.js +++ b/src/control/typeahead/source/TypeaheadSource.js @@ -179,20 +179,25 @@ JX.install('TypeaheadSource', { var n = Math.min(this.getMaximumResultCount(), hits.length); var nodes = []; for (var kk = 0; kk < n; kk++) { - var data = this._raw[hits[kk]]; - nodes.push(JX.$N( - 'a', - { - href: data.uri, - name: data.name, - rel: data.id, - className: 'jx-result' - }, - data.display)); + nodes.push(this.createNode(this._raw[hits[kk]])); } this._typeahead.showResults(nodes); }, + + createNode : function(data) { + return JX.$N( + 'a', + { + href: data.uri, + name: data.name, + rel: data.id, + className: 'jx-result' + }, + data.display + ); + }, + normalize : function(str) { return (this.getNormalizer() || JX.bag())(str); }, diff --git a/src/init.js b/src/init.js index 38c386e..402f8e3 100644 --- a/src/init.js +++ b/src/init.js @@ -30,7 +30,6 @@ JX.__rawEventQueue = function(what) { master_event_queue.push(what); - // Evade static analysis - JX.Stratcom var Stratcom = JX['Stratcom']; if (Stratcom && Stratcom.ready) { @@ -62,7 +61,8 @@ var target = what.srcElement || what.target; if (target && (what.type in {click: 1, submit: 1}) && - (/ FI_CAPTURE /).test(' ' + target.className + ' ')) { + target.getAttribute && + target.getAttribute('data-mustcapture') === '1') { what.returnValue = false; what.preventDefault && what.preventDefault(); document.body.id = 'event_capture'; diff --git a/support/facebook/sync-from-svn.sh b/support/facebook/sync-from-svn.sh index 4dbc939..f5a409a 100755 --- a/support/facebook/sync-from-svn.sh +++ b/support/facebook/sync-from-svn.sh @@ -13,6 +13,7 @@ STRIP="$1/scripts/javelin/strip.php" COREFILES="init.js Event.js install.js util.js Stratcom.js" LIBFILES="behavior.js Request.js Vector.js DOM.js JSON.js" CONTROLFILES="typeahead/Typeahead.js typeahead/source/TypeaheadSource.js typeahead/source/TypeaheadPreloadedSource.js typeahead/source/TypeaheadOnDemandSource.js typeahead/normalizer/TypeaheadNormalizer.js tokenizer/Tokenizer.js" +WORKFLOWFILES="Mask.js Workflow.js" for f in $COREFILES; do cat $1/html/js/javelin/core/$f | $STRIP > src/$f; @@ -29,13 +30,21 @@ for f in $CONTROLFILES; do chmod -x src/control/$f; done; +for f in $WORKFLOWFILES; do + cat $1/html/js/javelin/lib/$f | $STRIP > src/$f; + chmod -x src/$f; +done; + INITFILES="src/init.js" LIBFILES="src/util.js src/install.js src/Event.js src/Stratcom.js src/behavior.js src/Request.js src/Vector.js src/DOM.js src/JSON.js" TYPEAHEADFILES="src/control/typeahead/Typeahead.js src/control/typeahead/normalizer/TypeaheadNormalizer.js src/control/typeahead/source/TypeaheadSource.js src/control/typeahead/source/TypeaheadPreloadedSource.js src/control/typeahead/source/TypeaheadOnDemandSource.js src/control/tokenizer/Tokenizer.js" +WORKFLOWFILES="src/Mask.js src/Workflow.js" cat $INITFILES | $PROCESS > pkg/init.min.js cat $LIBFILES | $PROCESS > pkg/javelin.min.js cat $TYPEAHEADFILES | $PROCESS > pkg/typeahead.min.js +cat $WORKFLOWFILES | $PROCESS > pkg/workflow.min.js cat $INITFILES > pkg/init.dev.js cat $LIBFILES > pkg/javelin.dev.js cat $TYPEAHEADFILES > pkg/typeahead.dev.js +cat $WORKFLOWFILES > pkg/workflow.dev.js diff --git a/support/php/Javelin.php b/support/php/Javelin.php index c2d2752..77e2b81 100644 --- a/support/php/Javelin.php +++ b/support/php/Javelin.php @@ -18,21 +18,20 @@ class Javelin { public static function renderTag($tag, $content, $attributes = array()) { $javelin = self::getInstance(); - $classes = array(); foreach ($attributes as $k => $v) { switch ($k) { case 'sigil': - $classes[] = 'FN_'.$v; + $attributes['data-sigil'] = $v; unset($attributes[$k]); break; case 'meta': $id = count($javelin->metadata); $javelin->metadata[$id] = $v; - $classes[] = 'FD_'.$javelin->block.'_'.$id; + $attributes['data-meta'] = $javelin->block.'_'.$id; unset($attributes[$k]); break; case 'mustcapture': - $classes[] = 'FI_CAPTURE'; + $attributes['data-mustcapture'] = '1'; unset($attributes[$k]); break; default: @@ -40,12 +39,6 @@ public static function renderTag($tag, $content, $attributes = array()) { } } - if (isset($attributes['class'])) { - $classes[] = $attributes['class']; - } - $classes = implode(' ', $classes); - $attributes['class'] = $classes; - foreach ($attributes as $k => $v) { $v = htmlspecialchars($v, ENT_QUOTES, 'UTF-8'); $attributes[$k] = ' '.$k.'="'.$v.'"';