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 +4 -4
- data/README.md +21 -1
- data/lib/assets/javascripts/twine.coffee +431 -0
- data/lib/twine-rails/version.rb +1 -1
- metadata +4 -4
- data/lib/assets/javascripts/twine.js.coffee +0 -339
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4386189c38efbea2da170dd03b7d30d11c69d0e7
|
4
|
+
data.tar.gz: 90f46d3522191a2e65f21f9bc0accef50accc453
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
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
|
+
)
|
data/lib/twine-rails/version.rb
CHANGED
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.
|
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-
|
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.
|
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.
|
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)
|