twine-rails 0.1.7 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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)