twine-rails 0.1.7 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 54b8e1f074841bc61a92ee18560a166a1db32871
4
- data.tar.gz: fd7623c885bb21e68bf97913a813b07894af7777
3
+ metadata.gz: 4386189c38efbea2da170dd03b7d30d11c69d0e7
4
+ data.tar.gz: 90f46d3522191a2e65f21f9bc0accef50accc453
5
5
  SHA512:
6
- metadata.gz: 4f772fd9cf6a82afce7f31e4e1b6ce6305c9bae4d0970a0764972511184fc129322a18ff46af2b2c3009f71e2f76851a6b5a5fdd7cb3804dac59eea2240fd9d9
7
- data.tar.gz: c28f13443238ddd6861ad96a21d0016f816d30e285729890c4ca12f29c477bcf2e37d7caa64bf7a8dc9a08efe29d2bccacb0c009484c897aef1d1fe9905ed703
6
+ metadata.gz: a841a7bc1f7a30ddd0dd06dbb317f4aef580ae384865bf6a7f3df2c1789c36ef22df1bc4a4ece8501f4b6349100994bc0d6bb48f1cb5d79bf5172f58ed41c09c
7
+ data.tar.gz: f66a43f275158666c7012acd90ca5a634c379ef8545c6255a6efb920f9cb82109c42ef7c7a98b492880949ea92af60eaab5f736acf5edffa490f247a44ea6194
data/README.md CHANGED
@@ -23,7 +23,9 @@ Twine is available on bower via `bower install twine` if that is your preference
23
23
 
24
24
  Twine comes as `dist/twine.js` and `dist/twine.min.js` in this repo and in the bower package.
25
25
 
26
- Twine is also available as a gem. In your Gemfile, add `gem 'twine-rails'` and include it in your `application.js` manifest via `//= require twine`
26
+ Twine is also available as a gem. In your Gemfile, add `gem 'twine-rails'` and include it in your `application.js` manifest via `//= require twine`
27
+
28
+ AMD, CommonJS and Browser global (using UMD) are also supported.
27
29
 
28
30
  ## Usage
29
31
 
@@ -129,6 +131,24 @@ Example:
129
131
  $target.hasClass('disabled')
130
132
  ```
131
133
 
134
+ ## Twine.register
135
+
136
+ Lets you add constructors, modules, functions, etc to Twine that are not globally available. This means you can keep your classes etc
137
+ as local variables and Twine will find them for you within `define`s & `eval`s.
138
+
139
+ ```coffee
140
+ # local_class.coffee
141
+
142
+ class LocalClass
143
+ # ...
144
+
145
+ Twine.register('LocalClass', LocalClass)
146
+ ```
147
+
148
+ ```html
149
+ <div define="{localClass: new LocalClass()}"></div>
150
+ ```
151
+
132
152
  ## Dev Console
133
153
 
134
154
  To get the current context in the dev console, inspect an element then type:
@@ -0,0 +1,431 @@
1
+ ((root, factory) ->
2
+ if typeof root.define == 'function' && root.define.amd
3
+ root.define([], factory)
4
+ else if typeof module == 'object' && module.exports
5
+ module.exports = factory()
6
+ else
7
+ root.Twine = factory()
8
+ )(this, ->
9
+ Twine = {}
10
+ Twine.shouldDiscardEvent = {}
11
+
12
+ # Map of node binding ids to objects that describe a node's bindings.
13
+ elements = {}
14
+
15
+ # Registered components to look up
16
+ registry = {}
17
+
18
+ # The number of nodes bound since the last call to Twine.reset().
19
+ # Used to determine the next binding id.
20
+ nodeCount = 0
21
+
22
+ # Storage for all bindable data, provided by the caller of Twine.reset().
23
+ rootContext = null
24
+
25
+ keypathRegex = /^[a-z]\w*(\.[a-z]\w*|\[\d+\])*$/i # Tests if a string is a pure keypath.
26
+ refreshQueued = false
27
+ rootNode = null
28
+
29
+ currentBindingCallbacks = null
30
+
31
+ Twine.getAttribute = (node, attr) ->
32
+ node.getAttribute("data-#{attr}") || node.getAttribute(attr)
33
+
34
+ # Cleans up all existing bindings and sets the root node and context.
35
+ Twine.reset = (newContext, node = document.documentElement) ->
36
+ for key of elements
37
+ if bindings = elements[key]?.bindings
38
+ obj.teardown() for obj in bindings when obj.teardown
39
+
40
+ elements = {}
41
+
42
+ rootContext = newContext
43
+ rootNode = node
44
+ rootNode.bindingId = nodeCount = 1
45
+
46
+ this
47
+
48
+ Twine.bind = (node = rootNode, context = Twine.context(node)) ->
49
+ bind(context, node, getIndexesForElement(node), true)
50
+
51
+ Twine.afterBound = (callback) ->
52
+ if currentBindingCallbacks
53
+ currentBindingCallbacks.push(callback)
54
+ else
55
+ callback()
56
+
57
+ bind = (context, node, indexes, forceSaveContext) ->
58
+ currentBindingCallbacks = []
59
+ if node.bindingId
60
+ Twine.unbind(node)
61
+
62
+ if defineArrayAttr = Twine.getAttribute(node, 'define-array')
63
+ newIndexes = defineArray(node, context, defineArrayAttr)
64
+ indexes ?= {}
65
+ for key, value of indexes when !newIndexes.hasOwnProperty(key)
66
+ newIndexes[key] = value
67
+ indexes = newIndexes
68
+ # register the element early because subsequent bindings on the same node might need to make use of the index
69
+ element = findOrCreateElementForNode(node)
70
+ element.indexes = indexes
71
+
72
+ for type, binding of Twine.bindingTypes when definition = Twine.getAttribute(node, type)
73
+ element = findOrCreateElementForNode(node)
74
+ element.bindings ?= []
75
+ element.indexes ?= indexes
76
+
77
+ fn = binding(node, context, definition, element)
78
+ element.bindings.push(fn) if fn
79
+
80
+ if newContextKey = Twine.getAttribute(node, 'context')
81
+ keypath = keypathForKey(node, newContextKey)
82
+ if keypath[0] == '$root'
83
+ context = rootContext
84
+ keypath = keypath.slice(1)
85
+ context = getValue(context, keypath) || setValue(context, keypath, {})
86
+
87
+ if element || newContextKey || forceSaveContext
88
+ element = findOrCreateElementForNode(node)
89
+ element.childContext = context
90
+ element.indexes ?= indexes if indexes?
91
+
92
+ callbacks = currentBindingCallbacks
93
+
94
+ # IE and Safari don't support node.children for DocumentFragment and SVGElement nodes.
95
+ # If the element supports children we continue to traverse the children, otherwise
96
+ # we stop traversing that subtree.
97
+ # https://developer.mozilla.org/en-US/docs/Web/API/ParentNode.children
98
+ # As a result, Twine are unsupported within DocumentFragment and SVGElement nodes.
99
+ bind(context, childNode, if newContextKey? then null else indexes) for childNode in (node.children || [])
100
+ Twine.count = nodeCount
101
+
102
+ for callback in callbacks || []
103
+ callback()
104
+ currentBindingCallbacks = null
105
+
106
+ Twine
107
+
108
+ findOrCreateElementForNode = (node) ->
109
+ node.bindingId ?= ++nodeCount
110
+ elements[node.bindingId] ?= {}
111
+ elements[node.bindingId]
112
+
113
+ # Queues a refresh of the DOM, batching up calls for the current synchronous block.
114
+ Twine.refresh = ->
115
+ return if refreshQueued
116
+ refreshQueued = true
117
+ setTimeout(Twine.refreshImmediately, 0)
118
+
119
+ refreshElement = (element) ->
120
+ (obj.refresh() if obj.refresh?) for obj in element.bindings if element.bindings
121
+ return
122
+
123
+ Twine.refreshImmediately = ->
124
+ refreshQueued = false
125
+ refreshElement(element) for key, element of elements
126
+ return
127
+
128
+ Twine.register = (name, component) ->
129
+ if registry[name]
130
+ throw new Error("Twine error: '#{name}' is already registered with Twine")
131
+ else
132
+ registry[name] = component
133
+
134
+ # Force the binding system to recognize programmatic changes to a node's value.
135
+ Twine.change = (node, bubble = false) ->
136
+ event = document.createEvent("HTMLEvents")
137
+ event.initEvent('change', bubble, true) # for IE 9/10 compatibility.
138
+ node.dispatchEvent(event)
139
+
140
+ # Cleans up everything related to a node and its subtree.
141
+ Twine.unbind = (node) ->
142
+ if id = node.bindingId
143
+ if bindings = elements[id]?.bindings
144
+ obj.teardown() for obj in bindings when obj.teardown
145
+ delete elements[id]
146
+ delete node.bindingId
147
+
148
+
149
+ # IE and Safari don't support node.children for DocumentFragment or SVGElement,
150
+ # See explaination in bind()
151
+ Twine.unbind(childNode) for childNode in (node.children || [])
152
+ this
153
+
154
+ # Returns the binding context for a node by looking up the tree.
155
+ Twine.context = (node) -> getContext(node, false)
156
+ Twine.childContext = (node) -> getContext(node, true)
157
+
158
+ getContext = (node, child) ->
159
+ while node
160
+ return rootContext if node == rootNode
161
+ node = node.parentNode if !child
162
+ if !node
163
+ console.warn "Unable to find context; please check that the node is attached to the DOM that Twine has bound, or that bindings have been initiated on this node's DOM"
164
+ return null
165
+ if (id = node.bindingId) && (context = elements[id]?.childContext)
166
+ return context
167
+ node = node.parentNode if child
168
+
169
+ getIndexesForElement = (node) ->
170
+ firstContext = null
171
+ while node
172
+ return elements[id]?.indexes if id = node.bindingId
173
+ node = node.parentNode
174
+
175
+ # Returns the fully qualified key for a node's context
176
+ Twine.contextKey = (node, lastContext) ->
177
+ keys = []
178
+ addKey = (context) ->
179
+ for key, val of context when lastContext == val
180
+ keys.unshift(key)
181
+ break
182
+ lastContext = context
183
+
184
+ while node && node != rootNode && node = node.parentNode
185
+ addKey(context) if (id = node.bindingId) && (context = elements[id]?.childContext)
186
+
187
+ addKey(rootContext) if node == rootNode
188
+ keys.join('.')
189
+
190
+ valueAttributeForNode = (node) ->
191
+ name = node.nodeName.toLowerCase()
192
+ if name in ['input', 'textarea', 'select']
193
+ if node.getAttribute('type') in ['checkbox', 'radio'] then 'checked' else 'value'
194
+ else
195
+ 'textContent'
196
+
197
+ keypathForKey = (node, key) ->
198
+ keypath = []
199
+ for key, i in key.split('.')
200
+ if (start = key.indexOf('[')) != -1
201
+ if i == 0
202
+ keypath.push(keyWithArrayIndex(key.substr(0, start), node)...)
203
+ else
204
+ keypath.push(key.substr(0, start))
205
+ key = key.substr(start)
206
+
207
+ while (end = key.indexOf(']')) != -1
208
+ keypath.push(parseInt(key.substr(1, end), 10))
209
+ key = key.substr(end + 1)
210
+ else
211
+ if i == 0
212
+ keypath.push(keyWithArrayIndex(key, node)...)
213
+ else
214
+ keypath.push(key)
215
+ keypath
216
+
217
+ keyWithArrayIndex = (key, node) ->
218
+ index = elements[node.bindingId]?.indexes?[key]
219
+ if index?
220
+ [key, index]
221
+ else
222
+ [key]
223
+
224
+ getValue = (object, keypath) ->
225
+ object = object[key] for key in keypath when object?
226
+ object
227
+
228
+ setValue = (object, keypath, value) ->
229
+ [keypath..., lastKey] = keypath
230
+ for key in keypath
231
+ object = object[key] ?= {}
232
+ object[lastKey] = value
233
+
234
+ stringifyNodeAttributes = (node) ->
235
+ nAttributes = node.attributes.length
236
+ i = 0
237
+ result = ""
238
+ while i < nAttributes
239
+ attr = node.attributes.item(i)
240
+ result += "#{attr.nodeName}='#{attr.textContent}'"
241
+ i+=1
242
+ result
243
+
244
+ wrapFunctionString = (code, args, node) ->
245
+ if isKeypath(code) && keypath = keypathForKey(node, code)
246
+ if keypath[0] == '$root'
247
+ ($context, $root) -> getValue($root, keypath)
248
+ else
249
+ ($context, $root) -> getValue($context, keypath)
250
+ else
251
+ code = "return #{code}"
252
+ code = "with($arrayPointers) { #{code} }" if nodeHasArrayIndexes(node)
253
+ code = "with($registry) { #{code} }" if requiresRegistry(args)
254
+ try
255
+ new Function(args, "with($context) { #{code} }")
256
+ catch e
257
+ throw "Twine error: Unable to create function on #{node.nodeName} node with attributes #{stringifyNodeAttributes(node)}"
258
+
259
+ requiresRegistry = (args) -> /\$registry/.test(args)
260
+
261
+ nodeHasArrayIndexes = (node) ->
262
+ return unless node.bindingId?
263
+ !!elements[node.bindingId]?.indexes?
264
+
265
+ arrayPointersForNode = (node, context) ->
266
+ return {} unless node.bindingId?
267
+ indexes = elements[node.bindingId]?.indexes
268
+ return {} unless indexes?
269
+
270
+ result = {}
271
+ for key, index of indexes
272
+ result[key] = context[key][index]
273
+ result
274
+
275
+ isKeypath = (value) ->
276
+ value not in ['true', 'false', 'null', 'undefined'] and keypathRegex.test(value)
277
+
278
+ fireCustomChangeEvent = (node) ->
279
+ event = document.createEvent('CustomEvent')
280
+ event.initCustomEvent('bindings:change', true, false, {})
281
+ node.dispatchEvent(event)
282
+
283
+ Twine.bindingTypes =
284
+ bind: (node, context, definition) ->
285
+ valueAttribute = valueAttributeForNode(node)
286
+ value = node[valueAttribute]
287
+ lastValue = undefined
288
+ teardown = undefined
289
+
290
+ # Radio buttons only set the value to the node value if checked.
291
+ checkedValueType = node.getAttribute('type') == 'radio'
292
+ fn = wrapFunctionString(definition, '$context,$root,$arrayPointers', node)
293
+
294
+ refresh = ->
295
+ newValue = fn.call(node, context, rootContext, arrayPointersForNode(node, context))
296
+ return if newValue == lastValue # return if we can and avoid a DOM operation
297
+
298
+ lastValue = newValue
299
+ return if newValue == node[valueAttribute]
300
+
301
+ node[valueAttribute] = if checkedValueType then newValue == node.value else newValue
302
+ fireCustomChangeEvent(node)
303
+
304
+ return {refresh} unless isKeypath(definition)
305
+
306
+ refreshContext = ->
307
+ if checkedValueType
308
+ return unless node.checked
309
+ setValue(context, keypath, node.value)
310
+ else
311
+ setValue(context, keypath, node[valueAttribute])
312
+
313
+ keypath = keypathForKey(node, definition)
314
+ twoWayBinding = valueAttribute != 'textContent' && node.type != 'hidden'
315
+
316
+ if keypath[0] == '$root'
317
+ context = rootContext
318
+ keypath = keypath.slice(1)
319
+
320
+ if value? && (twoWayBinding || value != '') && !(oldValue = getValue(context, keypath))?
321
+ refreshContext()
322
+
323
+ if twoWayBinding
324
+ changeHandler = ->
325
+ return if getValue(context, keypath) == this[valueAttribute]
326
+ refreshContext()
327
+ Twine.refreshImmediately()
328
+ $(node).on 'input keyup change', changeHandler
329
+ teardown = ->
330
+ $(node).off 'input keyup change', changeHandler
331
+
332
+ {refresh, teardown}
333
+
334
+ 'bind-show': (node, context, definition) ->
335
+ fn = wrapFunctionString(definition, '$context,$root,$arrayPointers', node)
336
+ lastValue = undefined
337
+ return refresh: ->
338
+ newValue = !fn.call(node, context, rootContext, arrayPointersForNode(node, context))
339
+ return if newValue == lastValue
340
+ $(node).toggleClass('hide', lastValue = newValue)
341
+
342
+ 'bind-class': (node, context, definition) ->
343
+ fn = wrapFunctionString(definition, '$context,$root,$arrayPointers', node)
344
+ lastValue = {}
345
+ return refresh: ->
346
+ newValue = fn.call(node, context, rootContext, arrayPointersForNode(node, context))
347
+ for key, value of newValue when !lastValue[key] != !value
348
+ $(node).toggleClass(key, !!value)
349
+ lastValue = newValue
350
+
351
+ 'bind-attribute': (node, context, definition) ->
352
+ fn = wrapFunctionString(definition, '$context,$root,$arrayPointers', node)
353
+ lastValue = {}
354
+ return refresh: ->
355
+ newValue = fn.call(node, context, rootContext, arrayPointersForNode(node, context))
356
+ for key, value of newValue when lastValue[key] != value
357
+ $(node).attr(key, value || null)
358
+ lastValue = newValue
359
+
360
+ define: (node, context, definition) ->
361
+ fn = wrapFunctionString(definition, '$context,$root,$registry,$arrayPointers', node)
362
+ object = fn.call(node, context, rootContext, registry, arrayPointersForNode(node, context))
363
+ context[key] = value for key, value of object
364
+ return
365
+
366
+ eval: (node, context, definition) ->
367
+ fn = wrapFunctionString(definition, '$context,$root,$registry,$arrayPointers', node)
368
+ fn.call(node, context, rootContext, registry, arrayPointersForNode(node, context))
369
+ return
370
+
371
+ defineArray = (node, context, definition) ->
372
+ fn = wrapFunctionString(definition, '$context,$root', node)
373
+ object = fn.call(node, context, rootContext)
374
+
375
+ indexes = {}
376
+
377
+ for key, value of object
378
+ context[key] ?= []
379
+ throw "Twine error: expected '#{key}' to be an array" unless context[key] instanceof Array
380
+
381
+ indexes[key] = context[key].length
382
+ context[key].push(value)
383
+
384
+ indexes
385
+
386
+ setupAttributeBinding = (attributeName, bindingName) ->
387
+ booleanAttribute = attributeName in ['checked', 'indeterminate', 'disabled', 'readOnly']
388
+
389
+ Twine.bindingTypes["bind-#{bindingName}"] = (node, context, definition) ->
390
+ fn = wrapFunctionString(definition, '$context,$root,$arrayPointers', node)
391
+ lastValue = undefined
392
+ return refresh: ->
393
+ newValue = fn.call(node, context, rootContext, arrayPointersForNode(node, context))
394
+ newValue = !!newValue if booleanAttribute
395
+ return if newValue == lastValue
396
+ node[attributeName] = lastValue = newValue
397
+
398
+ fireCustomChangeEvent(node) if attributeName == 'checked'
399
+
400
+ for attribute in ['placeholder', 'checked', 'indeterminate', 'disabled', 'href', 'title', 'readOnly', 'src']
401
+ setupAttributeBinding(attribute, attribute)
402
+
403
+ setupAttributeBinding('innerHTML', 'unsafe-html')
404
+
405
+ preventDefaultForEvent = (event) ->
406
+ (event.type == 'submit' || event.currentTarget.nodeName.toLowerCase() == 'a') &&
407
+ Twine.getAttribute(event.currentTarget, 'allow-default') in ['false', false, 0, undefined, null]
408
+
409
+ setupEventBinding = (eventName) ->
410
+ Twine.bindingTypes["bind-event-#{eventName}"] = (node, context, definition) ->
411
+ onEventHandler = (event, data) ->
412
+ discardEvent = Twine.shouldDiscardEvent[eventName]?(event)
413
+ if discardEvent || preventDefaultForEvent(event)
414
+ event.preventDefault()
415
+
416
+ return if discardEvent
417
+
418
+ wrapFunctionString(definition, '$context,$root,$arrayPointers,event,data', node).call(node, context, rootContext, arrayPointersForNode(node, context), event, data)
419
+ Twine.refreshImmediately()
420
+ $(node).on eventName, onEventHandler
421
+
422
+ return teardown: ->
423
+ $(node).off eventName, onEventHandler
424
+
425
+ for eventName in ['click', 'dblclick', 'mouseenter', 'mouseleave', 'mouseover', 'mouseout', 'mousedown', 'mouseup',
426
+ 'submit', 'dragenter', 'dragleave', 'dragover', 'drop', 'drag', 'change', 'keypress', 'keydown', 'keyup', 'input',
427
+ 'error', 'done', 'success', 'fail', 'blur', 'focus', 'load', 'paste']
428
+ setupEventBinding(eventName)
429
+
430
+ Twine
431
+ )
@@ -1,3 +1,3 @@
1
1
  module TwineRails
2
- VERSION = '0.1.7'
2
+ VERSION = '1.0.0'
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: twine-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.7
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justin Li
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2016-04-14 00:00:00.000000000 Z
13
+ date: 2016-06-23 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: coffee-rails
@@ -63,7 +63,7 @@ extra_rdoc_files: []
63
63
  files:
64
64
  - LICENSE
65
65
  - README.md
66
- - lib/assets/javascripts/twine.js.coffee
66
+ - lib/assets/javascripts/twine.coffee
67
67
  - lib/twine-rails.rb
68
68
  - lib/twine-rails/version.rb
69
69
  homepage: https://github.com/Shopify/twine
@@ -86,7 +86,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
86
86
  version: '0'
87
87
  requirements: []
88
88
  rubyforge_project:
89
- rubygems_version: 2.4.5.1
89
+ rubygems_version: 2.5.1
90
90
  signing_key:
91
91
  specification_version: 4
92
92
  summary: Minimalistic two-way bindings
@@ -1,339 +0,0 @@
1
- window.Twine = {}
2
- Twine.shouldDiscardEvent = {}
3
-
4
- # Map of node binding ids to objects that describe a node's bindings.
5
- elements = {}
6
-
7
- # The number of nodes bound since the last call to Twine.reset().
8
- # Used to determine the next binding id.
9
- nodeCount = 0
10
-
11
- # Storage for all bindable data, provided by the caller of Twine.reset().
12
- rootContext = null
13
-
14
- keypathRegex = /^[a-z]\w*(\.[a-z]\w*|\[\d+\])*$/i # Tests if a string is a pure keypath.
15
- refreshQueued = false
16
- rootNode = null
17
-
18
- currentBindingCallbacks = null
19
-
20
- Twine.getAttribute = (node, attr) ->
21
- node.getAttribute("data-#{attr}") || node.getAttribute(attr)
22
-
23
- # Cleans up all existing bindings and sets the root node and context.
24
- Twine.reset = (newContext, node = document.documentElement) ->
25
- for key of elements
26
- if bindings = elements[key]?.bindings
27
- obj.teardown() for obj in bindings when obj.teardown
28
-
29
- elements = {}
30
-
31
- rootContext = newContext
32
- rootNode = node
33
- rootNode.bindingId = nodeCount = 1
34
-
35
- this
36
-
37
- Twine.bind = (node = rootNode, context = Twine.context(node)) ->
38
- bind(context, node, true)
39
-
40
- Twine.afterBound = (callback) ->
41
- if currentBindingCallbacks
42
- currentBindingCallbacks.push(callback)
43
- else
44
- callback()
45
-
46
- bind = (context, node, forceSaveContext) ->
47
- currentBindingCallbacks = []
48
- if node.bindingId
49
- Twine.unbind(node)
50
-
51
- for type, binding of Twine.bindingTypes when definition = Twine.getAttribute(node, type)
52
- element = {bindings: []} unless element # Defer allocation to prevent GC pressure
53
- fn = binding(node, context, definition, element)
54
- element.bindings.push(fn) if fn
55
-
56
- if newContextKey = Twine.getAttribute(node, 'context')
57
- keypath = keypathForKey(newContextKey)
58
- if keypath[0] == '$root'
59
- context = rootContext
60
- keypath = keypath.slice(1)
61
- context = getValue(context, keypath) || setValue(context, keypath, {})
62
-
63
- if element || newContextKey || forceSaveContext
64
- (element ?= {}).childContext = context
65
- elements[node.bindingId ?= ++nodeCount] = element
66
-
67
- callbacks = currentBindingCallbacks
68
-
69
- # IE and Safari don't support node.children for DocumentFragment and SVGElement nodes.
70
- # If the element supports children we continue to traverse the children, otherwise
71
- # we stop traversing that subtree.
72
- # https://developer.mozilla.org/en-US/docs/Web/API/ParentNode.children
73
- # As a result, Twine are unsupported within DocumentFragment and SVGElement nodes.
74
- bind(context, childNode) for childNode in (node.children || [])
75
- Twine.count = nodeCount
76
-
77
- for callback in callbacks || []
78
- callback()
79
- currentBindingCallbacks = null
80
-
81
- Twine
82
-
83
- # Queues a refresh of the DOM, batching up calls for the current synchronous block.
84
- Twine.refresh = ->
85
- return if refreshQueued
86
- refreshQueued = true
87
- setTimeout(Twine.refreshImmediately, 0)
88
-
89
- refreshElement = (element) ->
90
- (obj.refresh() if obj.refresh?) for obj in element.bindings if element.bindings
91
- return
92
-
93
- Twine.refreshImmediately = ->
94
- refreshQueued = false
95
- refreshElement(element) for key, element of elements
96
- return
97
-
98
- # Force the binding system to recognize programmatic changes to a node's value.
99
- Twine.change = (node, bubble = false) ->
100
- event = document.createEvent("HTMLEvents")
101
- event.initEvent('change', bubble, true) # for IE 9/10 compatibility.
102
- node.dispatchEvent(event)
103
-
104
- # Cleans up everything related to a node and its subtree.
105
- Twine.unbind = (node) ->
106
- if id = node.bindingId
107
- if bindings = elements[id]?.bindings
108
- obj.teardown() for obj in bindings when obj.teardown
109
- delete elements[id]
110
- delete node.bindingId
111
-
112
-
113
- # IE and Safari don't support node.children for DocumentFragment or SVGElement,
114
- # See explaination in bind()
115
- Twine.unbind(childNode) for childNode in (node.children || [])
116
- this
117
-
118
- # Returns the binding context for a node by looking up the tree.
119
- Twine.context = (node) -> getContext(node, false)
120
- Twine.childContext = (node) -> getContext(node, true)
121
-
122
- getContext = (node, child) ->
123
- while node
124
- return rootContext if node == rootNode
125
- node = node.parentNode if !child
126
- if !node
127
- console.warn "Unable to find context; please check that the node is attached to the DOM that Twine has bound, or that bindings have been initiated on this node's DOM"
128
- return null
129
- if (id = node.bindingId) && (context = elements[id]?.childContext)
130
- return context
131
- node = node.parentNode if child
132
-
133
- # Returns the fully qualified key for a node's context
134
- Twine.contextKey = (node, lastContext) ->
135
- keys = []
136
- addKey = (context) ->
137
- for key, val of context when lastContext == val
138
- keys.unshift(key)
139
- break
140
- lastContext = context
141
-
142
- while node && node != rootNode && node = node.parentNode
143
- addKey(context) if (id = node.bindingId) && (context = elements[id]?.childContext)
144
-
145
- addKey(rootContext) if node == rootNode
146
- keys.join('.')
147
-
148
- valueAttributeForNode = (node) ->
149
- name = node.nodeName.toLowerCase()
150
- if name in ['input', 'textarea', 'select']
151
- if node.getAttribute('type') in ['checkbox', 'radio'] then 'checked' else 'value'
152
- else
153
- 'textContent'
154
-
155
- keypathForKey = (key) ->
156
- keypath = []
157
- for key in key.split('.')
158
- if (start = key.indexOf('[')) != -1
159
- keypath.push(key.substr(0, start))
160
- key = key.substr(start)
161
-
162
- while (end = key.indexOf(']')) != -1
163
- keypath.push(parseInt(key.substr(1, end), 10))
164
- key = key.substr(end + 1)
165
- else
166
- keypath.push(key)
167
- keypath
168
-
169
- getValue = (object, keypath) ->
170
- object = object[key] for key in keypath when object?
171
- object
172
-
173
- setValue = (object, keypath, value) ->
174
- [keypath..., lastKey] = keypath
175
- for key in keypath
176
- object = object[key] ?= {}
177
- object[lastKey] = value
178
-
179
- stringifyNodeAttributes = (node) ->
180
- nAttributes = node.attributes.length
181
- i = 0
182
- result = ""
183
- while i < nAttributes
184
- attr = node.attributes.item(i)
185
- result += "#{attr.nodeName}='#{attr.textContent}'"
186
- i+=1
187
- result
188
-
189
- wrapFunctionString = (code, args, node) ->
190
- if isKeypath(code) && keypath = keypathForKey(code)
191
- if keypath[0] == '$root'
192
- ($context, $root) -> getValue($root, keypath)
193
- else
194
- ($context, $root) -> getValue($context, keypath)
195
- else
196
- try
197
- new Function(args, "with($context) { return #{code} }")
198
- catch e
199
- throw "Twine error: Unable to create function on #{node.nodeName} node with attributes #{stringifyNodeAttributes(node)}"
200
-
201
- isKeypath = (value) ->
202
- value not in ['true', 'false', 'null', 'undefined'] and keypathRegex.test(value)
203
-
204
- fireCustomChangeEvent = (node) ->
205
- event = document.createEvent('CustomEvent')
206
- event.initCustomEvent('bindings:change', true, false, {})
207
- node.dispatchEvent(event)
208
-
209
- Twine.bindingTypes =
210
- bind: (node, context, definition) ->
211
- valueAttribute = valueAttributeForNode(node)
212
- value = node[valueAttribute]
213
- lastValue = undefined
214
- teardown = undefined
215
-
216
- # Radio buttons only set the value to the node value if checked.
217
- checkedValueType = node.getAttribute('type') == 'radio'
218
- fn = wrapFunctionString(definition, '$context,$root', node)
219
-
220
- refresh = ->
221
- newValue = fn.call(node, context, rootContext)
222
- return if newValue == lastValue # return if we can and avoid a DOM operation
223
-
224
- lastValue = newValue
225
- return if newValue == node[valueAttribute]
226
-
227
- node[valueAttribute] = if checkedValueType then newValue == node.value else newValue
228
- fireCustomChangeEvent(node)
229
-
230
- return {refresh} unless isKeypath(definition)
231
-
232
- refreshContext = ->
233
- if checkedValueType
234
- return unless node.checked
235
- setValue(context, keypath, node.value)
236
- else
237
- setValue(context, keypath, node[valueAttribute])
238
-
239
- keypath = keypathForKey(definition)
240
- twoWayBinding = valueAttribute != 'textContent' && node.type != 'hidden'
241
-
242
- if keypath[0] == '$root'
243
- context = rootContext
244
- keypath = keypath.slice(1)
245
-
246
- if value? && (twoWayBinding || value != '') && !(oldValue = getValue(context, keypath))?
247
- refreshContext()
248
-
249
- if twoWayBinding
250
- changeHandler = ->
251
- return if getValue(context, keypath) == this[valueAttribute]
252
- refreshContext()
253
- Twine.refreshImmediately()
254
- $(node).on 'input keyup change', changeHandler
255
- teardown = ->
256
- $(node).off 'input keyup change', changeHandler
257
-
258
- {refresh, teardown}
259
-
260
- 'bind-show': (node, context, definition) ->
261
- fn = wrapFunctionString(definition, '$context,$root', node)
262
- lastValue = undefined
263
- return refresh: ->
264
- newValue = !fn.call(node, context, rootContext)
265
- return if newValue == lastValue
266
- $(node).toggleClass('hide', lastValue = newValue)
267
-
268
- 'bind-class': (node, context, definition) ->
269
- fn = wrapFunctionString(definition, '$context,$root', node)
270
- lastValue = {}
271
- return refresh: ->
272
- newValue = fn.call(node, context, rootContext)
273
- for key, value of newValue when !lastValue[key] != !value
274
- $(node).toggleClass(key, !!value)
275
- lastValue = newValue
276
-
277
- 'bind-attribute': (node, context, definition) ->
278
- fn = wrapFunctionString(definition, '$context,$root', node)
279
- lastValue = {}
280
- return refresh: ->
281
- newValue = fn.call(node, context, rootContext)
282
- for key, value of newValue when lastValue[key] != value
283
- $(node).attr(key, value || null)
284
- lastValue = newValue
285
-
286
- define: (node, context, definition) ->
287
- fn = wrapFunctionString(definition, '$context,$root', node)
288
- object = fn.call(node, context, rootContext)
289
- context[key] = value for key, value of object
290
- return
291
-
292
- eval: (node, context, definition) ->
293
- fn = wrapFunctionString(definition, '$context,$root', node)
294
- fn.call(node, context, rootContext)
295
- return
296
-
297
- setupAttributeBinding = (attributeName, bindingName) ->
298
- booleanAttribute = attributeName in ['checked', 'indeterminate', 'disabled', 'readOnly']
299
-
300
- Twine.bindingTypes["bind-#{bindingName}"] = (node, context, definition) ->
301
- fn = wrapFunctionString(definition, '$context,$root', node)
302
- lastValue = undefined
303
- return refresh: ->
304
- newValue = fn.call(node, context, rootContext)
305
- newValue = !!newValue if booleanAttribute
306
- return if newValue == lastValue
307
- node[attributeName] = lastValue = newValue
308
-
309
- fireCustomChangeEvent(node) if attributeName == 'checked'
310
-
311
- for attribute in ['placeholder', 'checked', 'indeterminate', 'disabled', 'href', 'title', 'readOnly', 'src']
312
- setupAttributeBinding(attribute, attribute)
313
-
314
- setupAttributeBinding('innerHTML', 'unsafe-html')
315
-
316
- preventDefaultForEvent = (event) ->
317
- (event.type == 'submit' || event.currentTarget.nodeName.toLowerCase() == 'a') &&
318
- Twine.getAttribute(event.currentTarget, 'allow-default') != '1'
319
-
320
- setupEventBinding = (eventName) ->
321
- Twine.bindingTypes["bind-event-#{eventName}"] = (node, context, definition) ->
322
- onEventHandler = (event, data) ->
323
- discardEvent = Twine.shouldDiscardEvent[eventName]?(event)
324
- if discardEvent || preventDefaultForEvent(event)
325
- event.preventDefault()
326
-
327
- return if discardEvent
328
-
329
- wrapFunctionString(definition, '$context,$root,event,data', node).call(node, context, rootContext, event, data)
330
- Twine.refreshImmediately()
331
- $(node).on eventName, onEventHandler
332
-
333
- return teardown: ->
334
- $(node).off eventName, onEventHandler
335
-
336
- for eventName in ['click', 'dblclick', 'mouseenter', 'mouseleave', 'mouseover', 'mouseout', 'mousedown', 'mouseup',
337
- 'submit', 'dragenter', 'dragleave', 'dragover', 'drop', 'drag', 'change', 'keypress', 'keydown', 'keyup', 'input',
338
- 'error', 'done', 'success', 'fail', 'blur', 'focus', 'load', 'paste']
339
- setupEventBinding(eventName)