diff --git a/benchmark.js b/benchmark.js index 8e4c7bc3..e39ac7c1 100644 --- a/benchmark.js +++ b/benchmark.js @@ -8,17 +8,19 @@ var HAWKEJSbase = "\n
\n <% assign('main') %>\n
\n"; var HAWKEJSmain = "\n<% expands('base') %>\n<% start('main') %>\n\t<% implement('topbar') %>\n\t
\n\t\t<% assign('container-fluid') %>\n\t
\n<% end('main') %>\n"; var HAWKEJStopbar = "\n
\n\t
\n\t\t
\n\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t\tElric\n\t\t\t
\n\t\t\t\t

\n\t\t\t\t\tLogged in as <%= username %>\n\t\t\t\t

\n\t\t\t\t
    \n\t\t\t\t\t
  • <% add_link('/', {title: 'Home'}) %>
  • \n\t\t\t\t
\n\t\t\t
\n\t\t
\n\t
\n
\n"; var HAWKEJSindex = "\n<% expands('main') %>\n<% start('container-fluid') %>\n

Welcome on the index page!

\nTimestamp: <%= timestamp %>\n<% end('container-fluid') %>\n"; +var HAWKEJSlink = "
  • <% add_link('/admin', {title: 'Dashboard', matchref: {class: 'active'}}) %>
  • "; hawkejs.storeTemplate("base", HAWKEJSbase); hawkejs.storeTemplate("main", HAWKEJSmain); hawkejs.storeTemplate("topbar", HAWKEJStopbar); hawkejs.storeTemplate("index", HAWKEJSindex); +hawkejs.storeTemplate("link", HAWKEJSlink); var timestamp = new Date().getTime(); var username = "Skerit"; // add tests -suite.add('Hawkejs', function() { +suite.add('Hawkejs (big template)', function() { hawkejs.render('index', {username: username, @@ -27,10 +29,15 @@ suite.add('Hawkejs', function() { // Receive the result object }); }) -/*.add('String#indexOf', function() { - 'Hello World!'.indexOf('o') > -1; +.add('Hawkejs#add_link', function() { + hawkejs.render('link', + {username: username, + timestamp: timestamp}, + false, function($result, ne, payload) { + // Receive the result object + }); }) -.add('String#match', function() { +/*.add('String#match', function() { !!'Hello World!'.match(/o/); })*/ // add listeners @@ -38,7 +45,7 @@ suite.add('Hawkejs', function() { console.log(String(event.target)); }) .on('complete', function() { - console.log('Fastest is ' + this.filter('fastest').pluck('name')); + //console.log('Fastest is ' + this.filter('fastest').pluck('name')); }) // run async .run({ 'async': true }); \ No newline at end of file diff --git a/lib/hawkejs-client.js b/lib/hawkejs-client.js index 11cd2566..97093711 100644 --- a/lib/hawkejs-client.js +++ b/lib/hawkejs-client.js @@ -15,7 +15,7 @@ window.onload=function() { delete payload[i]; } } - + console.log(payload); payload.__reAddHelpers = true; history.pushState(payload, null, url); diff --git a/lib/hawkejs.js b/lib/hawkejs.js index 2fe4782a..0477dcd3 100644 --- a/lib/hawkejs.js +++ b/lib/hawkejs.js @@ -3,6 +3,7 @@ if (ClientSide === undefined) var ClientSide = false; // Noclient> var uglify = require('uglify-js') var cheerio = require('cheerio'); +//var ce = require('cloneextend'); var jQuery = require('jquery'); var path = require('path'); var ent = require('ent'); @@ -63,6 +64,15 @@ var µ = {}; // //// Overwrite the root //clone._root = _root; + + // Version 3: +/- 492 ops/s + //µ.getObject(object); + //var clone = cheerio.load(object.$.root()); + + // Version 4: +/- 570 ops/s + // Using cloneextend + //µ.getObject(object); + //var clone = ce.clone(object.$); return clone; } @@ -231,6 +241,19 @@ hp._clientBrowserPath = false; */ hp._debug = false; +// Place to store values on the client side +hp.storage = { + + // Match instructions + match: {}, + + // Previous match states AS instructions + previous: [], + + // Next match states AS instructions + next: [] +}; + // Noclient> /** * Enable client-side suport @@ -606,7 +629,7 @@ hp.render = function render (template, variables, $update, callback) { // If this isn't an ajax call, and client side support is enabled, // inject the hawkejs-client-side.js file - if (thisHawk._client /*&& !variables.hawkejs.ajax*/) { + if (thisHawk._client && !variables.hawkejs.ajax) { thisHawk._addScript({path: thisHawk._clientBrowserPath}, $result); } @@ -864,6 +887,18 @@ hp._preparePayload = function _preparePayload (variables, forAjax) { // Tags to be injected are stored here payload.request.tags = {}; + // Update instructions when the given location matches our url + // The client needs to know about this, so it is injected as javascript + payload.request.match = {}; + + // History saves + payload.history = {}; + + // Previous page + // @todo: implement this! + payload.history.previous = false; + payload.history.next = false; + // Also create a link to the variables in the payload, // even though we'll also add them one level further down payload.scope.variables = variables; @@ -936,10 +971,191 @@ hp.applyChanges = function applyChanges ($document, payload) { // Insert blocks into spaces this._insertBlock($document, payload); + // Before doing url matches, revert to the previous state + if (ClientSide && this.storage.previous.length) { + // @todo: check for history action (going back/forward) + this._applyAttributes($document, this.storage.previous[0], false); + } + + // Do url match events + this._matchLocation($document, payload); + // Return the element return $document; } +/** + * Perform all the href matching. + * Also stores the instruction on the client side + * + * @author Jelle De Loecker + * @since 2013.01.25 + * @version 2013.01.25 + * + * @param {Cheerio} $origin The origin element + * @param {Object} payload The payload + */ +hp._matchLocation = function _matchLocation ($origin, payload) { + + // Store the match instructions on the client + this._extendClientVar('match', payload.request.match, $origin); + + if (ClientSide) { + var matchObject = this.storage.match; + } else { + var matchObject = payload.request.match; + } + + // The source page href we are on now + var here = payload.hawkejs.matchSource; + + // Store the previous status in here + var previous = {here: here, $this: false, status: {}}; + + // See if there is an entry for this href + if (matchObject[here]) { + + for (var elementid in matchObject[here]) { + + // Create a previous entry + previous.status[elementid] = {}; + + // Get the instructions + var instructions = matchObject[here][elementid]; + + // Select the destination + var $this = µ.select($origin, '#' + elementid); + + // Store this object if we're on the client side + if (ClientSide) previous.$this = $this; + + // Apply the changes + this._applyAttributes($this, instructions, previous.status[elementid]); + + } + } + + // Store the previous settings on the client + this._unshiftClientVar('previous', previous, $origin); + + // Delete $this, history api doesn't allow it + var undoMatch = jQuery.extend({}, previous); + delete undoMatch.$this; + + // And also store them in the payload, for history stuff + payload.request.undoMatch = undoMatch; +} + +/** + * Apply attributes to the given $element + * + * @author Jelle De Loecker + * @since 2013.01.25 + * @version 2013.01.25 + * + * @param {object} $this The document to update + * @param {object} ins The instructions to execute + * @param {object} previous Place where previous states can be stored + * AS instructions + */ +hp._applyAttributes = function _applyAttributes ($this, ins, previous) { + + var instructions = ins; + + // If a status entry is available, then this is + // an array of elementid's we need to apply attributes to + if (ins && ins.status) { + + // We were passed an entire object of elements to update ... + for (var elementid in ins.status) { + + var givePrev = false; + + // Create an entry for this element + if (previous && !previous[elementid]) { + previous[elementid] = {}; + givePrev = previous[elementid]; + } + + // Select the target + var $target = µ.select($this, '#' + elementid); + + // Recursively apply attributes + this._applyAttributes($target, ins.status[elementid], givePrev); + } + + // Do not execute the code any further + return; + } + + // Store the attributes as they are now + for (var attribute in instructions) { + + // Certain attributes do not have to be processed + if (attribute == 'sourceId') continue; + + // Get the new value + var value = instructions[attribute]; + + // Recurse when we reach a parent object + if (attribute == 'parent') { + + var prevlink = false; + + // Create a previous entry for the parent + if (previous) { + previous[attribute] = {}; + prevlink = previous[attribute]; + } + + hp._applyAttributes($this.parent(), value, prevlink); + continue; + } + + if (attribute == 'content') { + + var oldVal = $this.text(); + + } else { + + // Get the old value + var oldVal = $this.attr(attribute); + + // If oldVal is undefined, we need to add an empty string + if (oldVal === undefined) oldVal = ' '; + } + + // Do not replace class, but append + if (attribute == 'class') { + + $this.addClass(value); + + // History, however, must remove this class + if (previous) previous['replaceClass'] = oldVal; + continue; + + } if (attribute == 'replaceClass') { + + // Remove all current classes + $this.removeClass(); + + // Add the new class + $this.addClass(value); + + if (previous) previous['replaceClass'] = oldVal; + + } else if (attribute == 'content') { + $this.html(value); + } else { + $this.attr(attribute, value); + } + + // Add it to the previous entry + if (previous) previous[attribute] = oldVal; + } + +} + /** * Fill in all implementation spaces * You pass the Cheerio document, and it looks for things inside the payload @@ -1052,12 +1268,142 @@ hp._insertBlock = function _insertBlock ($origin, payload) { } } +/** + * Extend a hawkejs variable on the client side *RECURSIVELY* + * + * @author Jelle De Loecker + * @since 2013.01.24 + * @version 2013.01.24 + * + * @param {string} name The name of the variable + * @param {object} value The value + * @param {object} $element doc we can add the script to (if from server) + */ +hp._extendClientVar = function _extendClientVar (name, value, $element) { + + if (ClientSide) { + // Make a deep extend + $.extend(true, this.storage[name], value); + } else { + // This code is sent to the client, he will then do the code above us + this.execOnClient('hawkejs._extendClientVar', [name, value], $element); + } +} + +/** + * Merge a hawkejs array on the client side + * + * @author Jelle De Loecker + * @since 2013.01.24 + * @version 2013.01.24 + * + * @param {string} name The name of the variable + * @param {object} value The value + * @param {object} $element doc we can add the script to (if from server) + */ +hp._mergeClientVar = function _mergeClientVar (name, value, $element) { + + if (ClientSide) { + $.merge(this.storage[name], value); + } else { + // This code is sent to the client, he will then do the code above us + this.execOnClient('hawkejs._mergeClientVar', [name, value], $element); + } +} + +/** + * Unshift a value to a hawkejs array on the client side + * + * @author Jelle De Loecker + * @since 2013.01.25 + * @version 2013.01.25 + * + * @param {string} name The name of the variable + * @param {object} value The value + * @param {object} $element doc we can add the script to (if from server) + */ +hp._unshiftClientVar = function _unshiftClientVar (name, value, $element) { + + if (ClientSide) { + if (this.storage[name] === undefined) this.storage[name] = []; + this.storage[name].unshift(value); + + log('Unshifted ' + name); + log(this.storage[name]); + + } else { + // This code is sent to the client, he will then do the code above us + this.execOnClient('hawkejs._unshiftClientVar', [name, value], $element); + } +} + +/** + * Push a value to a hawkejs array on the client side + * + * @author Jelle De Loecker + * @since 2013.01.24 + * @version 2013.01.24 + * + * @param {string} name The name of the variable + * @param {object} value The value + * @param {object} $element doc we can add the script to (if from server) + */ +hp._pushClientVar = function _pushClientVar (name, value, $element) { + + if (ClientSide) { + if (this.storage[name] === undefined) this.storage[name] = []; + this.storage[name].push(value); + } else { + // This code is sent to the client, he will then do the code above us + this.execOnClient('hawkejs._pushClientVar', [name, value], $element); + } +} + +/** + * Execute this function on the client, from the server. + * Basically a simple function call. + * + * @author Jelle De Loecker + * @since 2013.01.24 + * @version 2013.01.24 + * + * @param {string} functionname The function to be called + * @param {array} parameters The parameters to be passed + * @param {object} $element doc we can add the script to + * @param {string} destination Where to put the code + */ +hp.execOnClient = function execOnClient (functionname, parameters, $element, destination) { + + // Destination is the bottom of the document by default + if (destination === undefined) destination = 'bottom'; + + var passParam = ''; + + // Create the parameters part of the call + for (var i in parameters) { + var p = parameters[i]; + + if (passParam) passParam += ', '; + + passParam += JSON.stringify(p); + } + + var code = functionname + '(' + passParam + ');'; + + // Add the code to the page + this._addScript({code: code, destination: destination}, $element); +} + /** * Add a script to the element * + * script: + * - path {string} Is this a link to a javascript file? + * - code {string} Or is this a code block? + * * @author Jelle De Loecker * @since 2013.01.19 - * @version 2013.01.20 + * @version 2013.01.24 * * @param {object} script The script object * @param {Cheerio} $element The element to insert it into @@ -1068,7 +1414,15 @@ hp._addScript = function _addScript (script, $element) { if (!script.destination) script.destination = 'anywhere'; - var html = ''; + var html = '