add bower for client deps
This commit is contained in:
+104
@@ -0,0 +1,104 @@
|
||||
# The default `.` adapter thats comes with Rivets.js. Allows subscribing to
|
||||
# properties on plain objects, implemented in ES5 natives using
|
||||
# `Object.defineProperty`.
|
||||
Rivets.public.adapters['.'] =
|
||||
id: '_rv'
|
||||
counter: 0
|
||||
weakmap: {}
|
||||
|
||||
weakReference: (obj) ->
|
||||
unless obj.hasOwnProperty @id
|
||||
id = @counter++
|
||||
Object.defineProperty obj, @id, value: id
|
||||
|
||||
@weakmap[obj[@id]] or= callbacks: {}
|
||||
|
||||
cleanupWeakReference: (ref, id) ->
|
||||
unless Object.keys(ref.callbacks).length
|
||||
unless ref.pointers and Object.keys(ref.pointers).length
|
||||
delete @weakmap[id]
|
||||
|
||||
stubFunction: (obj, fn) ->
|
||||
original = obj[fn]
|
||||
map = @weakReference obj
|
||||
weakmap = @weakmap
|
||||
|
||||
obj[fn] = ->
|
||||
response = original.apply obj, arguments
|
||||
|
||||
for r, k of map.pointers
|
||||
callback() for callback in weakmap[r]?.callbacks[k] ? []
|
||||
|
||||
response
|
||||
|
||||
observeMutations: (obj, ref, keypath) ->
|
||||
if Array.isArray obj
|
||||
map = @weakReference obj
|
||||
|
||||
unless map.pointers?
|
||||
map.pointers = {}
|
||||
functions = ['push', 'pop', 'shift', 'unshift', 'sort', 'reverse', 'splice']
|
||||
@stubFunction obj, fn for fn in functions
|
||||
|
||||
map.pointers[ref] ?= []
|
||||
|
||||
unless keypath in map.pointers[ref]
|
||||
map.pointers[ref].push keypath
|
||||
|
||||
unobserveMutations: (obj, ref, keypath) ->
|
||||
if Array.isArray(obj) and obj[@id]?
|
||||
if map = @weakmap[obj[@id]]
|
||||
if pointers = map.pointers[ref]
|
||||
if (idx = pointers.indexOf(keypath)) >= 0
|
||||
pointers.splice idx, 1
|
||||
|
||||
delete map.pointers[ref] unless pointers.length
|
||||
@cleanupWeakReference map, obj[@id]
|
||||
|
||||
observe: (obj, keypath, callback) ->
|
||||
callbacks = @weakReference(obj).callbacks
|
||||
|
||||
unless callbacks[keypath]?
|
||||
callbacks[keypath] = []
|
||||
desc = Object.getOwnPropertyDescriptor obj, keypath
|
||||
|
||||
unless desc?.get or desc?.set
|
||||
value = obj[keypath]
|
||||
|
||||
Object.defineProperty obj, keypath,
|
||||
enumerable: true
|
||||
get: -> value
|
||||
set: (newValue) =>
|
||||
if newValue isnt value
|
||||
@unobserveMutations value, obj[@id], keypath
|
||||
value = newValue
|
||||
|
||||
if map = @weakmap[obj[@id]]
|
||||
callbacks = map.callbacks
|
||||
|
||||
if callbacks[keypath]
|
||||
cb() for cb in callbacks[keypath].slice() when cb in callbacks[keypath]
|
||||
@observeMutations newValue, obj[@id], keypath
|
||||
|
||||
unless callback in callbacks[keypath]
|
||||
callbacks[keypath].push callback
|
||||
|
||||
@observeMutations obj[keypath], obj[@id], keypath
|
||||
|
||||
unobserve: (obj, keypath, callback) ->
|
||||
if map = @weakmap[obj[@id]]
|
||||
if callbacks = map.callbacks[keypath]
|
||||
if (idx = callbacks.indexOf(callback)) >= 0
|
||||
callbacks.splice idx, 1
|
||||
|
||||
unless callbacks.length
|
||||
delete map.callbacks[keypath]
|
||||
@unobserveMutations obj[keypath], obj[@id], keypath
|
||||
|
||||
@cleanupWeakReference map, obj[@id]
|
||||
|
||||
get: (obj, keypath) ->
|
||||
obj[keypath]
|
||||
|
||||
set: (obj, keypath, value) ->
|
||||
obj[keypath] = value
|
||||
+255
@@ -0,0 +1,255 @@
|
||||
# Basic set of core binders that are included with Rivets.js.
|
||||
|
||||
# Sets the element's text value.
|
||||
Rivets.public.binders.text = (el, value) ->
|
||||
if el.textContent?
|
||||
el.textContent = if value? then value else ''
|
||||
else
|
||||
el.innerText = if value? then value else ''
|
||||
|
||||
# Sets the element's HTML content.
|
||||
Rivets.public.binders.html = (el, value) ->
|
||||
el.innerHTML = if value? then value else ''
|
||||
|
||||
# Shows the element when value is true.
|
||||
Rivets.public.binders.show = (el, value) ->
|
||||
el.style.display = if value then '' else 'none'
|
||||
|
||||
# Hides the element when value is true (negated version of `show` binder).
|
||||
Rivets.public.binders.hide = (el, value) ->
|
||||
el.style.display = if value then 'none' else ''
|
||||
|
||||
# Enables the element when value is true.
|
||||
Rivets.public.binders.enabled = (el, value) ->
|
||||
el.disabled = !value
|
||||
|
||||
# Disables the element when value is true (negated version of `enabled` binder).
|
||||
Rivets.public.binders.disabled = (el, value) ->
|
||||
el.disabled = !!value
|
||||
|
||||
# Checks a checkbox or radio input when the value is true. Also sets the model
|
||||
# property when the input is checked or unchecked (two-way binder).
|
||||
Rivets.public.binders.checked =
|
||||
publishes: true
|
||||
priority: 2000
|
||||
|
||||
bind: (el) ->
|
||||
Rivets.Util.bindEvent el, 'change', @publish
|
||||
|
||||
unbind: (el) ->
|
||||
Rivets.Util.unbindEvent el, 'change', @publish
|
||||
|
||||
routine: (el, value) ->
|
||||
if el.type is 'radio'
|
||||
el.checked = el.value?.toString() is value?.toString()
|
||||
else
|
||||
el.checked = !!value
|
||||
|
||||
# Unchecks a checkbox or radio input when the value is true (negated version of
|
||||
# `checked` binder). Also sets the model property when the input is checked or
|
||||
# unchecked (two-way binder).
|
||||
Rivets.public.binders.unchecked =
|
||||
publishes: true
|
||||
priority: 2000
|
||||
|
||||
bind: (el) ->
|
||||
Rivets.Util.bindEvent el, 'change', @publish
|
||||
|
||||
unbind: (el) ->
|
||||
Rivets.Util.unbindEvent el, 'change', @publish
|
||||
|
||||
routine: (el, value) ->
|
||||
if el.type is 'radio'
|
||||
el.checked = el.value?.toString() isnt value?.toString()
|
||||
else
|
||||
el.checked = !value
|
||||
|
||||
# Sets the element's value. Also sets the model property when the input changes
|
||||
# (two-way binder).
|
||||
Rivets.public.binders.value =
|
||||
publishes: true
|
||||
priority: 3000
|
||||
|
||||
bind: (el) ->
|
||||
unless el.tagName is 'INPUT' and el.type is 'radio'
|
||||
@event = if el.tagName is 'SELECT' then 'change' else 'input'
|
||||
Rivets.Util.bindEvent el, @event, @publish
|
||||
|
||||
unbind: (el) ->
|
||||
unless el.tagName is 'INPUT' and el.type is 'radio'
|
||||
Rivets.Util.unbindEvent el, @event, @publish
|
||||
|
||||
routine: (el, value) ->
|
||||
if el.tagName is 'INPUT' and el.type is 'radio'
|
||||
el.setAttribute 'value', value
|
||||
else if window.jQuery?
|
||||
el = jQuery el
|
||||
|
||||
if value?.toString() isnt el.val()?.toString()
|
||||
el.val if value? then value else ''
|
||||
else
|
||||
if el.type is 'select-multiple'
|
||||
o.selected = o.value in value for o in el if value?
|
||||
else if value?.toString() isnt el.value?.toString()
|
||||
el.value = if value? then value else ''
|
||||
|
||||
# Inserts and binds the element and it's child nodes into the DOM when true.
|
||||
Rivets.public.binders.if =
|
||||
block: true
|
||||
priority: 4000
|
||||
|
||||
bind: (el) ->
|
||||
unless @marker?
|
||||
attr = [@view.prefix, @type].join('-').replace '--', '-'
|
||||
declaration = el.getAttribute attr
|
||||
|
||||
@marker = document.createComment " rivets: #{@type} #{declaration} "
|
||||
@bound = false
|
||||
|
||||
el.removeAttribute attr
|
||||
el.parentNode.insertBefore @marker, el
|
||||
el.parentNode.removeChild el
|
||||
|
||||
unbind: ->
|
||||
if @nested
|
||||
@nested.unbind()
|
||||
@bound = false
|
||||
|
||||
routine: (el, value) ->
|
||||
if !!value is not @bound
|
||||
if value
|
||||
models = {}
|
||||
models[key] = model for key, model of @view.models
|
||||
|
||||
(@nested or= new Rivets.View(el, models, @view.options())).bind()
|
||||
@marker.parentNode.insertBefore el, @marker.nextSibling
|
||||
@bound = true
|
||||
else
|
||||
el.parentNode.removeChild el
|
||||
@nested.unbind()
|
||||
@bound = false
|
||||
|
||||
update: (models) ->
|
||||
@nested?.update models
|
||||
|
||||
# Removes and unbinds the element and it's child nodes into the DOM when true
|
||||
# (negated version of `if` binder).
|
||||
Rivets.public.binders.unless =
|
||||
block: true
|
||||
priority: 4000
|
||||
|
||||
bind: (el) ->
|
||||
Rivets.public.binders.if.bind.call @, el
|
||||
|
||||
unbind: ->
|
||||
Rivets.public.binders.if.unbind.call @
|
||||
|
||||
routine: (el, value) ->
|
||||
Rivets.public.binders.if.routine.call @, el, not value
|
||||
|
||||
update: (models) ->
|
||||
Rivets.public.binders.if.update.call @, models
|
||||
|
||||
# Binds an event handler on the element.
|
||||
Rivets.public.binders['on-*'] =
|
||||
function: true
|
||||
priority: 1000
|
||||
|
||||
unbind: (el) ->
|
||||
Rivets.Util.unbindEvent el, @args[0], @handler if @handler
|
||||
|
||||
routine: (el, value) ->
|
||||
Rivets.Util.unbindEvent el, @args[0], @handler if @handler
|
||||
Rivets.Util.bindEvent el, @args[0], @handler = @eventHandler value
|
||||
|
||||
# Appends bound instances of the element in place for each item in the array.
|
||||
Rivets.public.binders['each-*'] =
|
||||
block: true
|
||||
priority: 4000
|
||||
|
||||
bind: (el) ->
|
||||
unless @marker?
|
||||
attr = [@view.prefix, @type].join('-').replace '--', '-'
|
||||
@marker = document.createComment " rivets: #{@type} "
|
||||
@iterated = []
|
||||
|
||||
el.removeAttribute attr
|
||||
el.parentNode.insertBefore @marker, el
|
||||
el.parentNode.removeChild el
|
||||
else
|
||||
for view in @iterated
|
||||
view.bind()
|
||||
return;
|
||||
|
||||
unbind: (el) ->
|
||||
view.unbind() for view in @iterated if @iterated?
|
||||
return
|
||||
|
||||
routine: (el, collection) ->
|
||||
modelName = @args[0]
|
||||
collection = collection or []
|
||||
|
||||
if @iterated.length > collection.length
|
||||
for i in Array @iterated.length - collection.length
|
||||
view = @iterated.pop()
|
||||
view.unbind()
|
||||
@marker.parentNode.removeChild view.els[0]
|
||||
|
||||
for model, index in collection
|
||||
data = {index}
|
||||
data[Rivets.public.iterationAlias modelName] = index
|
||||
data[modelName] = model
|
||||
|
||||
if not @iterated[index]?
|
||||
for key, model of @view.models
|
||||
data[key] ?= model
|
||||
|
||||
previous = if @iterated.length
|
||||
@iterated[@iterated.length - 1].els[0]
|
||||
else
|
||||
@marker
|
||||
|
||||
options = @view.options()
|
||||
options.preloadData = true
|
||||
|
||||
template = el.cloneNode true
|
||||
view = new Rivets.View(template, data, options)
|
||||
view.bind()
|
||||
@iterated.push view
|
||||
|
||||
@marker.parentNode.insertBefore template, previous.nextSibling
|
||||
else if @iterated[index].models[modelName] isnt model
|
||||
@iterated[index].update data
|
||||
|
||||
if el.nodeName is 'OPTION'
|
||||
for binding in @view.bindings
|
||||
if binding.el is @marker.parentNode and binding.type is 'value'
|
||||
binding.sync()
|
||||
return
|
||||
|
||||
update: (models) ->
|
||||
data = {}
|
||||
|
||||
for key, model of models
|
||||
data[key] = model unless key is @args[0]
|
||||
|
||||
view.update data for view in @iterated
|
||||
return
|
||||
|
||||
# Adds or removes the class from the element when value is true or false.
|
||||
Rivets.public.binders['class-*'] = (el, value) ->
|
||||
elClass = " #{el.className} "
|
||||
|
||||
if !value is (elClass.indexOf(" #{@args[0]} ") isnt -1)
|
||||
el.className = if value
|
||||
"#{el.className} #{@args[0]}"
|
||||
else
|
||||
elClass.replace(" #{@args[0]} ", ' ').trim()
|
||||
|
||||
# Sets the attribute on the element. If no binder above is matched it will fall
|
||||
# back to using this binder.
|
||||
Rivets.public.binders['*'] = (el, value) ->
|
||||
if value?
|
||||
el.setAttribute @type, value
|
||||
else
|
||||
el.removeAttribute @type
|
||||
+294
@@ -0,0 +1,294 @@
|
||||
# Rivets.Binding
|
||||
# --------------
|
||||
|
||||
# A single binding between a model attribute and a DOM element.
|
||||
class Rivets.Binding
|
||||
# All information about the binding is passed into the constructor; the
|
||||
# containing view, the DOM node, the type of binding, the model object and the
|
||||
# keypath at which to listen for changes.
|
||||
constructor: (@view, @el, @type, @keypath, @options = {}) ->
|
||||
@formatters = @options.formatters or []
|
||||
@dependencies = []
|
||||
@formatterObservers = {}
|
||||
@model = undefined
|
||||
@setBinder()
|
||||
|
||||
# Sets the binder to use when binding and syncing.
|
||||
setBinder: =>
|
||||
unless @binder = @view.binders[@type]
|
||||
for identifier, value of @view.binders
|
||||
if identifier isnt '*' and identifier.indexOf('*') isnt -1
|
||||
regexp = new RegExp "^#{identifier.replace(/\*/g, '.+')}$"
|
||||
if regexp.test @type
|
||||
@binder = value
|
||||
@args = new RegExp("^#{identifier.replace(/\*/g, '(.+)')}$").exec @type
|
||||
@args.shift()
|
||||
|
||||
@binder or= @view.binders['*']
|
||||
@binder = {routine: @binder} if @binder instanceof Function
|
||||
|
||||
observe: (obj, keypath, callback) =>
|
||||
Rivets.sightglass obj, keypath, callback,
|
||||
root: @view.rootInterface
|
||||
adapters: @view.adapters
|
||||
|
||||
parseTarget: =>
|
||||
token = Rivets.TypeParser.parse @keypath
|
||||
|
||||
if token.type is Rivets.TypeParser.types.primitive
|
||||
@value = token.value
|
||||
else
|
||||
@observer = @observe @view.models, @keypath, @sync
|
||||
@model = @observer.target
|
||||
|
||||
parseFormatterArguments: (args, formatterIndex) =>
|
||||
args = (Rivets.TypeParser.parse(arg) for arg in args)
|
||||
processedArgs = []
|
||||
|
||||
for arg, ai in args
|
||||
processedArgs.push if arg.type is Rivets.TypeParser.types.primitive
|
||||
arg.value
|
||||
else
|
||||
@formatterObservers[formatterIndex] or= {}
|
||||
|
||||
unless observer = @formatterObservers[formatterIndex][ai]
|
||||
observer = @observe @view.models, arg.value, @sync
|
||||
@formatterObservers[formatterIndex][ai] = observer
|
||||
|
||||
observer.value()
|
||||
|
||||
processedArgs
|
||||
|
||||
# Applies all the current formatters to the supplied value and returns the
|
||||
# formatted value.
|
||||
formattedValue: (value) =>
|
||||
for formatter, fi in @formatters
|
||||
args = formatter.match /[^\s']+|'([^']|'[^\s])*'|"([^"]|"[^\s])*"/g
|
||||
id = args.shift()
|
||||
formatter = @view.formatters[id]
|
||||
|
||||
processedArgs = @parseFormatterArguments args, fi
|
||||
|
||||
if formatter?.read instanceof Function
|
||||
value = formatter.read.call @model, value, processedArgs...
|
||||
else if formatter instanceof Function
|
||||
value = formatter.call @model, value, processedArgs...
|
||||
|
||||
value
|
||||
|
||||
# Returns an event handler for the binding around the supplied function.
|
||||
eventHandler: (fn) =>
|
||||
handler = (binding = @).view.handler
|
||||
(ev) -> handler.call fn, @, ev, binding
|
||||
|
||||
# Sets the value for the binding. This Basically just runs the binding routine
|
||||
# with the suplied value formatted.
|
||||
set: (value) =>
|
||||
# Since 0.9 : doesn't execute function unless backward compatibility is active
|
||||
value = if (value instanceof Function and !@binder.function and Rivets.public.executeFunctions)
|
||||
@formattedValue value.call @model
|
||||
else
|
||||
@formattedValue value
|
||||
|
||||
@binder.routine?.call @, @el, value
|
||||
|
||||
# Syncs up the view binding with the model.
|
||||
sync: =>
|
||||
@set if @observer
|
||||
if @model isnt @observer.target
|
||||
observer.unobserve() for observer in @dependencies
|
||||
@dependencies = []
|
||||
|
||||
if (@model = @observer.target)? and @options.dependencies?.length
|
||||
for dependency in @options.dependencies
|
||||
observer = @observe @model, dependency, @sync
|
||||
@dependencies.push observer
|
||||
|
||||
@observer.value()
|
||||
else
|
||||
@value
|
||||
|
||||
# Publishes the value currently set on the input element back to the model.
|
||||
publish: =>
|
||||
if @observer
|
||||
value = @getValue @el
|
||||
lastformatterIndex = @formatters.length - 1
|
||||
|
||||
for formatter, fiReversed in @formatters.slice(0).reverse()
|
||||
fi = lastformatterIndex - fiReversed
|
||||
args = formatter.split /\s+/
|
||||
id = args.shift()
|
||||
|
||||
processedArgs = @parseFormatterArguments args, fi
|
||||
|
||||
if @view.formatters[id]?.publish
|
||||
value = @view.formatters[id].publish value, processedArgs...
|
||||
|
||||
@observer.setValue value
|
||||
|
||||
# Subscribes to the model for changes at the specified keypath. Bi-directional
|
||||
# routines will also listen for changes on the element to propagate them back
|
||||
# to the model.
|
||||
bind: =>
|
||||
@parseTarget()
|
||||
@binder.bind?.call @, @el
|
||||
|
||||
if @model? and @options.dependencies?.length
|
||||
for dependency in @options.dependencies
|
||||
observer = @observe @model, dependency, @sync
|
||||
@dependencies.push observer
|
||||
|
||||
@sync() if @view.preloadData
|
||||
|
||||
# Unsubscribes from the model and the element.
|
||||
unbind: =>
|
||||
@binder.unbind?.call @, @el
|
||||
@observer?.unobserve()
|
||||
|
||||
observer.unobserve() for observer in @dependencies
|
||||
@dependencies = []
|
||||
|
||||
for fi, args of @formatterObservers
|
||||
observer.unobserve() for ai, observer of args
|
||||
|
||||
@formatterObservers = {}
|
||||
|
||||
# Updates the binding's model from what is currently set on the view. Unbinds
|
||||
# the old model first and then re-binds with the new model.
|
||||
update: (models = {}) =>
|
||||
@model = @observer?.target
|
||||
@binder.update?.call @, models
|
||||
|
||||
# Returns elements value
|
||||
getValue: (el) =>
|
||||
if @binder and @binder.getValue?
|
||||
@binder.getValue.call @, el
|
||||
else
|
||||
Rivets.Util.getInputValue el
|
||||
|
||||
# Rivets.ComponentBinding
|
||||
# -----------------------
|
||||
|
||||
# A component view encapsulated as a binding within it's parent view.
|
||||
class Rivets.ComponentBinding extends Rivets.Binding
|
||||
# Initializes a component binding for the specified view. The raw component
|
||||
# element is passed in along with the component type. Attributes and scope
|
||||
# inflections are determined based on the components defined attributes.
|
||||
constructor: (@view, @el, @type) ->
|
||||
@component = @view.components[@type]
|
||||
@static = {}
|
||||
@observers = {}
|
||||
@upstreamObservers = {}
|
||||
|
||||
bindingRegExp = view.bindingRegExp()
|
||||
|
||||
for attribute in @el.attributes or []
|
||||
unless bindingRegExp.test attribute.name
|
||||
propertyName = @camelCase attribute.name
|
||||
|
||||
token = Rivets.TypeParser.parse(attribute.value)
|
||||
|
||||
if propertyName in (@component.static ? [])
|
||||
@static[propertyName] = attribute.value
|
||||
else if token.type is Rivets.TypeParser.types.primitive
|
||||
@static[propertyName] = token.value
|
||||
else
|
||||
@observers[propertyName] = attribute.value
|
||||
|
||||
# Intercepts `Rivets.Binding::sync` since component bindings are not bound to
|
||||
# a particular model to update it's value.
|
||||
sync: ->
|
||||
|
||||
# Intercepts `Rivets.Binding::update` since component bindings are not bound
|
||||
# to a particular model to update it's value.
|
||||
update: ->
|
||||
|
||||
# Intercepts `Rivets.Binding::publish` since component bindings are not bound
|
||||
# to a particular model to update it's value.
|
||||
publish: ->
|
||||
|
||||
# Returns an object map using the component's scope inflections.
|
||||
locals: =>
|
||||
result = {}
|
||||
|
||||
for key, value of @static
|
||||
result[key] = value
|
||||
|
||||
for key, observer of @observers
|
||||
result[key] = observer.value()
|
||||
|
||||
result
|
||||
|
||||
# Returns a camel-cased version of the string. Used when translating an
|
||||
# element's attribute name into a property name for the component's scope.
|
||||
camelCase: (string) ->
|
||||
string.replace /-([a-z])/g, (grouped) ->
|
||||
grouped[1].toUpperCase()
|
||||
|
||||
# Intercepts `Rivets.Binding::bind` to build `@componentView` with a localized
|
||||
# map of models from the root view. Bind `@componentView` on subsequent calls.
|
||||
bind: =>
|
||||
unless @bound
|
||||
for key, keypath of @observers
|
||||
@observers[key] = @observe @view.models, keypath, ((key) => =>
|
||||
@componentView.models[key] = @observers[key].value()
|
||||
).call(@, key)
|
||||
|
||||
@bound = true
|
||||
|
||||
if @componentView?
|
||||
@componentView.bind()
|
||||
else
|
||||
@el.innerHTML = @component.template.call this
|
||||
scope = @component.initialize.call @, @el, @locals()
|
||||
@el._bound = true
|
||||
|
||||
options = {}
|
||||
|
||||
for option in Rivets.extensions
|
||||
options[option] = {}
|
||||
options[option][k] = v for k, v of @component[option] if @component[option]
|
||||
options[option][k] ?= v for k, v of @view[option]
|
||||
|
||||
for option in Rivets.options
|
||||
options[option] = @component[option] ? @view[option]
|
||||
|
||||
@componentView = new Rivets.View(Array.prototype.slice.call(@el.childNodes), scope, options)
|
||||
@componentView.bind()
|
||||
|
||||
for key, observer of @observers
|
||||
@upstreamObservers[key] = @observe @componentView.models, key, ((key, observer) => =>
|
||||
observer.setValue @componentView.models[key]
|
||||
).call(@, key, observer)
|
||||
return
|
||||
|
||||
# Intercept `Rivets.Binding::unbind` to be called on `@componentView`.
|
||||
unbind: =>
|
||||
for key, observer of @upstreamObservers
|
||||
observer.unobserve()
|
||||
|
||||
for key, observer of @observers
|
||||
observer.unobserve()
|
||||
|
||||
@componentView?.unbind.call @
|
||||
|
||||
# Rivets.TextBinding
|
||||
# -----------------------
|
||||
|
||||
# A text node binding, defined internally to deal with text and element node
|
||||
# differences while avoiding it being overwritten.
|
||||
class Rivets.TextBinding extends Rivets.Binding
|
||||
# Initializes a text binding for the specified view and text node.
|
||||
constructor: (@view, @el, @type, @keypath, @options = {}) ->
|
||||
@formatters = @options.formatters or []
|
||||
@dependencies = []
|
||||
@formatterObservers = {}
|
||||
|
||||
# A standard routine binder used for text node bindings.
|
||||
binder:
|
||||
routine: (node, value) ->
|
||||
node.data = value ? ''
|
||||
|
||||
# Wrap the call to `sync` in fat-arrow to avoid function context issues.
|
||||
sync: =>
|
||||
super
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
# Rivets.factory
|
||||
# --------------
|
||||
|
||||
# Rivets.js module factory.
|
||||
Rivets.factory = (sightglass) ->
|
||||
# Integrate sightglass.
|
||||
Rivets.sightglass = sightglass
|
||||
|
||||
# Allow access to private members (for testing).
|
||||
Rivets.public._ = Rivets
|
||||
|
||||
# Return the public interface.
|
||||
Rivets.public
|
||||
|
||||
# Exports Rivets.js for CommonJS, AMD and the browser.
|
||||
if typeof module?.exports is 'object'
|
||||
module.exports = Rivets.factory require('sightglass')
|
||||
else if typeof define is 'function' and define.amd
|
||||
define ['sightglass'], (sightglass) ->
|
||||
@rivets = Rivets.factory sightglass
|
||||
else
|
||||
@rivets = Rivets.factory sightglass
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
# Core formatters
|
||||
|
||||
# Calls a function with arguments
|
||||
Rivets.public.formatters['call'] = (value, args...) ->
|
||||
value.call @, args...
|
||||
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
# Rivets.TypeParser
|
||||
# ---------------------
|
||||
|
||||
# Parser and tokenizer for getting the type and value of a primitive or keypath.
|
||||
class Rivets.TypeParser
|
||||
@types:
|
||||
primitive: 0
|
||||
keypath: 1
|
||||
|
||||
@parse: (string) ->
|
||||
if /^'.*'$|^".*"$/.test string
|
||||
type: @types.primitive
|
||||
value: string.slice 1, -1
|
||||
else if string is 'true'
|
||||
type: @types.primitive
|
||||
value: true
|
||||
else if string is 'false'
|
||||
type: @types.primitive
|
||||
value: false
|
||||
else if string is 'null'
|
||||
type: @types.primitive
|
||||
value: null
|
||||
else if string is 'undefined'
|
||||
type: @types.primitive
|
||||
value: undefined
|
||||
else if string is ''
|
||||
type: @types.primitive
|
||||
value: undefined
|
||||
else if isNaN(Number(string)) is false
|
||||
type: @types.primitive
|
||||
value: Number string
|
||||
else
|
||||
type: @types.keypath
|
||||
value: string
|
||||
|
||||
# Rivets.TextTemplateParser
|
||||
# -------------------------
|
||||
|
||||
# Rivets.js text template parser and tokenizer for mustache-style text content
|
||||
# binding declarations.
|
||||
class Rivets.TextTemplateParser
|
||||
@types:
|
||||
text: 0
|
||||
binding: 1
|
||||
|
||||
# Parses the template and returns a set of tokens, separating static portions
|
||||
# of text from binding declarations.
|
||||
@parse: (template, delimiters) ->
|
||||
tokens = []
|
||||
length = template.length
|
||||
index = 0
|
||||
lastIndex = 0
|
||||
|
||||
while lastIndex < length
|
||||
index = template.indexOf delimiters[0], lastIndex
|
||||
|
||||
if index < 0
|
||||
tokens.push type: @types.text, value: template.slice lastIndex
|
||||
break
|
||||
else
|
||||
if index > 0 and lastIndex < index
|
||||
tokens.push type: @types.text, value: template.slice lastIndex, index
|
||||
|
||||
lastIndex = index + delimiters[0].length
|
||||
index = template.indexOf delimiters[1], lastIndex
|
||||
|
||||
if index < 0
|
||||
substring = template.slice lastIndex - delimiters[1].length
|
||||
lastToken = tokens[tokens.length - 1]
|
||||
|
||||
if lastToken?.type is @types.text
|
||||
lastToken.value += substring
|
||||
else
|
||||
tokens.push type: @types.text, value: substring
|
||||
|
||||
break
|
||||
|
||||
value = template.slice(lastIndex, index).trim()
|
||||
tokens.push type: @types.binding, value: value
|
||||
lastIndex = index + delimiters[1].length
|
||||
|
||||
tokens
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
# The Rivets namespace.
|
||||
Rivets =
|
||||
options: [
|
||||
'prefix'
|
||||
'templateDelimiters'
|
||||
'rootInterface'
|
||||
'preloadData'
|
||||
'handler',
|
||||
'executeFunctions'
|
||||
]
|
||||
|
||||
extensions: [
|
||||
'binders'
|
||||
'formatters'
|
||||
'components'
|
||||
'adapters'
|
||||
]
|
||||
|
||||
# The public interface (this is the exported module object).
|
||||
public:
|
||||
# Global binders.
|
||||
binders: {}
|
||||
|
||||
# Global components.
|
||||
components: {}
|
||||
|
||||
# Global formatters.
|
||||
formatters: {}
|
||||
|
||||
# Global sightglass adapters.
|
||||
adapters: {}
|
||||
|
||||
# Default attribute prefix.
|
||||
prefix: 'rv'
|
||||
|
||||
# Default template delimiters.
|
||||
templateDelimiters: ['{', '}']
|
||||
|
||||
# Default sightglass root interface.
|
||||
rootInterface: '.'
|
||||
|
||||
# Preload data by default.
|
||||
preloadData: true,
|
||||
|
||||
# Execute functions in bindings. Defaultis false since rivets 0.9. Set to true to be backward compatible with rivets 0.8.
|
||||
executeFunctions: false,
|
||||
|
||||
# Alias for index in rv-each binder
|
||||
iterationAlias : (modelName) ->
|
||||
return '%' + modelName + '%'
|
||||
|
||||
# Default event handler.
|
||||
handler: (context, ev, binding) ->
|
||||
@call context, ev, binding.view.models
|
||||
|
||||
# Merges an object literal into the corresponding global options.
|
||||
configure: (options = {}) ->
|
||||
for option, value of options
|
||||
if option in ['binders', 'components', 'formatters', 'adapters']
|
||||
for key, descriptor of value
|
||||
Rivets[option][key] = descriptor
|
||||
else
|
||||
Rivets.public[option] = value
|
||||
|
||||
return
|
||||
|
||||
# Binds some data to a template / element. Returns a Rivets.View instance.
|
||||
bind: (el, models = {}, options = {}) ->
|
||||
view = new Rivets.View(el, models, options)
|
||||
view.bind()
|
||||
view
|
||||
|
||||
# Initializes a new instance of a component on the specified element and
|
||||
# returns a Rivets.View instance.
|
||||
init: (component, el, data = {}) ->
|
||||
el ?= document.createElement 'div'
|
||||
component = Rivets.public.components[component]
|
||||
template = component.template.call @, el
|
||||
if template instanceof HTMLElement
|
||||
while el.firstChild
|
||||
el.removeChild(el.firstChild)
|
||||
el.appendChild(template)
|
||||
else
|
||||
el.innerHTML = template
|
||||
scope = component.initialize.call @, el, data
|
||||
|
||||
view = new Rivets.View(el, scope)
|
||||
view.bind()
|
||||
view
|
||||
Vendored
+31
@@ -0,0 +1,31 @@
|
||||
# Rivets.Util
|
||||
# -----------
|
||||
|
||||
if window['jQuery'] or window['$']
|
||||
jQuery = window['jQuery'] or window['$']
|
||||
[bindMethod, unbindMethod] = if 'on' of jQuery.prototype then ['on', 'off'] else ['bind', 'unbind']
|
||||
|
||||
Rivets.Util =
|
||||
bindEvent: (el, event, handler) -> jQuery(el)[bindMethod] event, handler
|
||||
unbindEvent: (el, event, handler) -> jQuery(el)[unbindMethod] event, handler
|
||||
getInputValue: (el) ->
|
||||
$el = jQuery el
|
||||
|
||||
if $el.attr('type') is 'checkbox' then $el.is ':checked'
|
||||
else do $el.val
|
||||
else
|
||||
Rivets.Util =
|
||||
bindEvent: do ->
|
||||
if 'addEventListener' of window then return (el, event, handler) ->
|
||||
el.addEventListener event, handler, false
|
||||
|
||||
(el, event, handler) -> el.attachEvent 'on' + event, handler
|
||||
unbindEvent: do ->
|
||||
if 'removeEventListener' of window then return (el, event, handler) ->
|
||||
el.removeEventListener event, handler, false
|
||||
|
||||
(el, event, handler) -> el.detachEvent 'on' + event, handler
|
||||
getInputValue: (el) ->
|
||||
if el.type is 'checkbox' then el.checked
|
||||
else if el.type is 'select-multiple' then o.value for o in el when o.selected
|
||||
else el.value
|
||||
Vendored
+144
@@ -0,0 +1,144 @@
|
||||
# Rivets.View
|
||||
# -----------
|
||||
|
||||
# A collection of bindings built from a set of parent nodes.
|
||||
class Rivets.View
|
||||
# The DOM elements and the model objects for binding are passed into the
|
||||
# constructor along with any local options that should be used throughout the
|
||||
# context of the view and it's bindings.
|
||||
constructor: (@els, @models, options = {}) ->
|
||||
@els = [@els] unless (@els.jquery or @els instanceof Array)
|
||||
|
||||
for option in Rivets.extensions
|
||||
@[option] = {}
|
||||
@[option][k] = v for k, v of options[option] if options[option]
|
||||
@[option][k] ?= v for k, v of Rivets.public[option]
|
||||
|
||||
for option in Rivets.options
|
||||
@[option] = options[option] ? Rivets.public[option]
|
||||
|
||||
@build()
|
||||
|
||||
options: =>
|
||||
options = {}
|
||||
|
||||
for option in Rivets.extensions.concat Rivets.options
|
||||
options[option] = @[option]
|
||||
|
||||
options
|
||||
|
||||
# Regular expression used to match binding attributes.
|
||||
bindingRegExp: =>
|
||||
new RegExp "^#{@prefix}-"
|
||||
|
||||
buildBinding: (binding, node, type, declaration) =>
|
||||
options = {}
|
||||
|
||||
pipes = (pipe.trim() for pipe in declaration.match /((?:'[^']*')*(?:(?:[^\|']*(?:'[^']*')+[^\|']*)+|[^\|]+))|^$/g)
|
||||
context = (ctx.trim() for ctx in pipes.shift().split '<')
|
||||
keypath = context.shift()
|
||||
|
||||
options.formatters = pipes
|
||||
|
||||
if dependencies = context.shift()
|
||||
options.dependencies = dependencies.split /\s+/
|
||||
|
||||
@bindings.push new Rivets[binding] @, node, type, keypath, options
|
||||
|
||||
# Parses the DOM tree and builds `Rivets.Binding` instances for every matched
|
||||
# binding declaration.
|
||||
build: =>
|
||||
@bindings = []
|
||||
|
||||
parse = (node) =>
|
||||
if node.nodeType is 3
|
||||
parser = Rivets.TextTemplateParser
|
||||
|
||||
if delimiters = @templateDelimiters
|
||||
if (tokens = parser.parse(node.data, delimiters)).length
|
||||
unless tokens.length is 1 and tokens[0].type is parser.types.text
|
||||
for token in tokens
|
||||
text = document.createTextNode token.value
|
||||
node.parentNode.insertBefore text, node
|
||||
|
||||
if token.type is 1
|
||||
@buildBinding 'TextBinding', text, null, token.value
|
||||
node.parentNode.removeChild node
|
||||
else if node.nodeType is 1
|
||||
block = @traverse node
|
||||
|
||||
unless block
|
||||
parse childNode for childNode in (n for n in node.childNodes)
|
||||
return
|
||||
|
||||
parse el for el in @els
|
||||
|
||||
@bindings.sort (a, b) ->
|
||||
(b.binder?.priority or 0) - (a.binder?.priority or 0)
|
||||
|
||||
return
|
||||
|
||||
traverse: (node) =>
|
||||
bindingRegExp = @bindingRegExp()
|
||||
block = node.nodeName is 'SCRIPT' or node.nodeName is 'STYLE'
|
||||
|
||||
for attribute in node.attributes
|
||||
if bindingRegExp.test attribute.name
|
||||
type = attribute.name.replace bindingRegExp, ''
|
||||
|
||||
unless binder = @binders[type]
|
||||
for identifier, value of @binders
|
||||
if identifier isnt '*' and identifier.indexOf('*') isnt -1
|
||||
regexp = new RegExp "^#{identifier.replace(/\*/g, '.+')}$"
|
||||
if regexp.test type
|
||||
binder = value
|
||||
|
||||
binder or= @binders['*']
|
||||
|
||||
if binder.block
|
||||
block = true
|
||||
attributes = [attribute]
|
||||
|
||||
for attribute in attributes or node.attributes
|
||||
if bindingRegExp.test attribute.name
|
||||
type = attribute.name.replace bindingRegExp, ''
|
||||
@buildBinding 'Binding', node, type, attribute.value
|
||||
|
||||
unless block
|
||||
type = node.nodeName.toLowerCase()
|
||||
|
||||
if @components[type] and not node._bound
|
||||
@bindings.push new Rivets.ComponentBinding @, node, type
|
||||
block = true
|
||||
|
||||
block
|
||||
|
||||
# Returns an array of bindings where the supplied function evaluates to true.
|
||||
select: (fn) =>
|
||||
binding for binding in @bindings when fn binding
|
||||
|
||||
# Binds all of the current bindings for this view.
|
||||
bind: =>
|
||||
binding.bind() for binding in @bindings
|
||||
return
|
||||
|
||||
# Unbinds all of the current bindings for this view.
|
||||
unbind: =>
|
||||
binding.unbind() for binding in @bindings
|
||||
return
|
||||
|
||||
# Syncs up the view with the model by running the routines on all bindings.
|
||||
sync: =>
|
||||
binding.sync?() for binding in @bindings
|
||||
return
|
||||
|
||||
# Publishes the input values from the view back to the model (reverse sync).
|
||||
publish: =>
|
||||
binding.publish() for binding in @select (b) -> b.binder?.publishes
|
||||
return
|
||||
|
||||
# Updates the view's models along with any affected bindings.
|
||||
update: (models = {}) =>
|
||||
@models[key] = model for key, model of models
|
||||
binding.update? models for binding in @bindings
|
||||
return
|
||||
Reference in New Issue
Block a user