UNPKG

rule

Version:

Coffeescript friendly css selector based templating

281 lines (265 loc) 11.4 kB
# Rule v1.0.5 # templating library # http://rulejs.com # http://github.com/etler/rule # # Copyright 2014, Tim Etler (tmetler@gmail.com) # Licensed under the MIT or GPL Version 2 licenses. class Rule # Build a new Rule with a rule object and optional template constructor: (rule, template) -> @rule = rule if rule @template = template if template # Apply a rule to a cloned template, taking data that is passed to rule functions # Optionally takes an element and applies modifications directly to that element render: (data, parent) -> env = @constructor.env or Rule.env # Set parent to a copy of the template if it is not already set # Normalize to ensure parent is a DOM Element if parent parent = normalizeElement(parent) else parent = normalizeElement(@template).cloneNode(true) # Pre-calculate classes to dictionary for fast lookup lookup = splatify(parent, {}) # If the object property 'rule' is set, do not use the inherited rules if @hasOwnProperty 'rule' rules = @rule else rules = combineRules @ for key, rule of rules # Apply each rule to the parent object. [selector, attribute, position] = @constructor.split key # Lookup tag names are stored as uppercase strings if selector and selector[0] not in ['#', '.'] selector = selector.toUpperCase() # Match selector string to css lookup table if selector selection = lookup[selector] or [] # Need to iterate backwards so splice does not invalidate array bounds for index in [selection.length - 1..0] by -1 value = selection[index] # Remove child from selection if it was removed if not isChildOf(parent, value) selection.splice(index, 1) # Empty selector selects the parent as an array else selection = [parent] # Add will return the selection and sibling elements generator = (selector) => # A singular rule failing should not crash the entire program try @constructor.parse rule, data, selector, @ catch error console.error 'RuleError: ', error.stack # If there is an error, we want to skip it, so return undefined return result = @constructor.add generator, selection, attribute, position return parent # Parse the rule to get the content object @parse: (rule, data, selector, context) -> env = @constructor.env or Rule.env # Bind the function to the data and current selector and parse its results if rule instanceof Function @parse (rule.call data, selector, context), data, selector, context # Parse each item in the array and return a flat array else if rule instanceof Array result = [] result = result.concat (@parse item, data, selector, context) for item in rule return result # Pass the data to the rule object, if the rule object # does not have a template then use the current selector # and apply changes directly to it. # Return undefined in that case so it is not added twice. else if rule instanceof Rule if rule.hasOwnProperty 'template' rule.render data else rule.render data, selector.selection return undefined # Return objects that can be added to the dom directly as is # If null or undefined return as is to be ignored else if rule instanceof env.Node or !rule? rule # A helper case for jQuery style objects. else if env.$?.fn.isPrototypeOf(rule) rule.get() # If the object has a custom toString then use it else if rule.toString isnt Object::toString rule.toString() # If the object does not have a custom toString # create a new rule from the object else if Object::isPrototypeOf rule newRule = (new @ rule) newRule.parent = context @parse.call @, newRule, data, selector, context # Add a content object to an array of selection or attributes # of the selections at the position specified # Returns the selections and any siblings as an array of Nodes @add: (generator, selections, attribute, position) -> env = @constructor.env or Rule.env result = [] # Make sure content generator is always a generator if !(generator instanceof Function) # The generator value is bound so the value within the closure # will not be overwritten generator = ((value) -> value).bind(@, generator) for selection in selections content = generator selection: selection attribute: attribute position: position # Nothing to do here continue unless content? and selection instanceof env.Element # Attribute is specified, so modify attribute if attribute? and content? content = content.join('') if content instanceof Array previous = (selection.getAttribute attribute) ? '' selection.setAttribute attribute, if position is '<' then content + previous else if position is '>' then previous + content else if position is '-' then content + ' ' + previous else if position is '+' then previous + ' ' + content else content # Attribute not specified so modify selected element else # Add the content to various positions parent = target = selection # Content is being added to the top level position if position in ['-', '+', '='] parent = selection.parentElement # To insert after the selection if position is '+' target = selection.nextSibling # To insert at the start if position is '<' target = selection.firstChild # To insert at the end if position is '>' or !position? target = null # Remove all children if !position? selection.removeChild selection.firstChild while selection.firstChild? content = [content] if !(content instanceof Array) for element in content # If content is not a DOM Node already, always convert to a TextNode element = if !(element instanceof env.Node) then env.document.createTextNode element else element # Add selection either before or after in the right order result.push selection if position is '+' result.push element result.push selection if position is '-' # Parent must be an HTMLElement to insure we can add to it # We can assume parent is a Node, but not all Nodes can be added too if parent instanceof env.HTMLElement parent.insertBefore element, target # If position is =, the old selection must be removed parent?.removeChild target if position is '=' # Only return result array if we are adding siblings, otherwise return the top level selections return if position in ['-', '+', '='] and !attribute? then result else selections # Parse the selector for selection, attribute, and position @split: (key) -> # Splits [(selector)][@(attribute)][(-<=>+)] # Regexes are not used for speed position = key[-1...] if position in ['-','+','<','>','='] then key = key[0...-1] else position = undefined [selector, attribute] = key.split('@', 2) selector = undefined if selector is '' return [selector, attribute, position] @env: window ? undefined # Compatibility fallbacks for certain browsers that don't support indexOf and querySelectorAll indexOf = Array::indexOf ? (item) -> for value, index in @ if index of @ and value is item return index return -1 querySelectorAll = ((query) -> env = @constructor.env or Rule.env # Hack to support IE8, does not support accessing DOM constructors querySelectorAll = env.document.createElement('div').querySelectorAll ? (query) -> ((env.$ @).find query).get() querySelectorAll(query) ).bind(@) # Shim to support IE8, does not support getObjectPrototype getPrototypeOf = Object.getPrototypeOf ? (object) -> prototype = object.constructor.prototype # Someone has put a constructor property on an object instance. # How dumb. # (Even dumber is if someone overwrote the prototype's constructor # property, but you can also overwrite Object.getPrototypeOf, so we # can only handle so much.) if (object.hasOwnProperty 'constructor' and object isnt prototype) or # Or object is already the prototype object is prototype # If the object is currently the prototype, delete its constructor to # expose the prototype's prototype's constructor which contains the # prototype's prototype. Then put the constructor back where it was. constructor = object.constructor delete object.constructor prototype = object.constructor.prototype object.constructor = constructor return prototype # Converts a single Node object, or a jQuery style object # object to a javascript Node object normalizeElement = ((element) -> env = @constructor.env or Rule.env # Using $.fn instead of instanceof $ because zepto does not support latter if env.$?.fn.isPrototypeOf(element) element.get(0) else if element instanceof Function normalizeElement do element else if element[0] element[0] else element ).bind(@) # A recursive function to combine all prototype rules so they are # applied with the oldest prototype rules first. combineRules = (object) -> if (object.hasOwnProperty 'constructor') and (object.constructor is Rule) return {} rules = combineRules getPrototypeOf object if object.hasOwnProperty 'rule' for key, rule of object.rule delete rules[key] rules[key] = rule return rules # Convert an element tree into a dictionary of classes that reference the # child elements within the tree splatify = (element, hash = {}) -> child = element.firstElementChild while (child) # If no class name you can skip setting up the dictionary if typeof(child.getAttribute('class')) is 'string' for elementKey in child.getAttribute('class').split(' ') elementKey = '.' + elementKey # Check existing dictionary array elementArray = hash[elementKey] ? hash[elementKey] = [] # Push is used for speed elementArray.push(child) # Index id if (child.id isnt '') elementKey = '#' + child.id elementArray = hash[elementKey] ? hash[elementKey] = [] elementArray.push(child) # Index tag names elementKey = child.tagName.toUpperCase() elementArray = hash[elementKey] ? hash[elementKey] = [] elementArray.push(child) # Recurse splatify(child, hash) # Use nextElementSibling for browser speed optimization child = child.nextElementSibling return hash isChildOf = (parent, child) -> nextParent = child while nextParent = nextParent.parentElement return true if nextParent is parent return false # Check if the javascript environment is node, or the browser # In node module is defined within the global closure, # but 'this' is an empty object if module? and @module isnt module module.exports = Rule else window.Rule = Rule return Rule