twine-rails 1.0.0 → 2.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
- SHA1:
3
- metadata.gz: 4386189c38efbea2da170dd03b7d30d11c69d0e7
4
- data.tar.gz: 90f46d3522191a2e65f21f9bc0accef50accc453
2
+ SHA256:
3
+ metadata.gz: 7245233240a25c03d7714bbeab59980acd3a0743118611db224c5eacaf52f7c8
4
+ data.tar.gz: e91b4658d70df7a8106b7ef6705429681819da11f411ea7fc4cd3e9fdfb18300
5
5
  SHA512:
6
- metadata.gz: a841a7bc1f7a30ddd0dd06dbb317f4aef580ae384865bf6a7f3df2c1789c36ef22df1bc4a4ece8501f4b6349100994bc0d6bb48f1cb5d79bf5172f58ed41c09c
7
- data.tar.gz: f66a43f275158666c7012acd90ca5a634c379ef8545c6255a6efb920f9cb82109c42ef7c7a98b492880949ea92af60eaab5f736acf5edffa490f247a44ea6194
6
+ metadata.gz: 1cc8f28bdd59879b679ea684021b5b500540e1d3928593b550488ae0cb9bd75a761bdc631ff70223028797812e9095e69c43ab9c2d77b2671a059be61d20a0a1
7
+ data.tar.gz: c8f4f398ef794db3e6062208f4d3ef8496d84f64968032bef0150b1a4b6f3f8044119bc362f182f0a0d15f09e4ea7818ed99a1ba62cb330e104ece89ff9a5f49
data/README.md CHANGED
@@ -171,6 +171,7 @@ Where context expects a node and `$0` is shorthand for the current node in the d
171
171
  ## Releasing
172
172
 
173
173
  1. Update version number in `package.json`, `bower.json`, and `lib/twine-rails/version.rb`
174
- 2. Run `bundle install` to update `Gemfile.lock`
175
- 3. Run make .all && make .uglify to update JS
174
+ 2. Run `dev up` to update `Gemfile.lock`
175
+ 3. Run `make .all && make .uglify` to update JS
176
176
  4. Push the new tag to GitHub and the new version to rubygems with `bundle exec rake release`
177
+ 5. Publish the new version to NPM with `npm publish`.
@@ -24,6 +24,7 @@
24
24
 
25
25
  keypathRegex = /^[a-z]\w*(\.[a-z]\w*|\[\d+\])*$/i # Tests if a string is a pure keypath.
26
26
  refreshQueued = false
27
+ refreshCallbacks = []
27
28
  rootNode = null
28
29
 
29
30
  currentBindingCallbacks = null
@@ -56,6 +57,7 @@
56
57
 
57
58
  bind = (context, node, indexes, forceSaveContext) ->
58
59
  currentBindingCallbacks = []
60
+ element = null
59
61
  if node.bindingId
60
62
  Twine.unbind(node)
61
63
 
@@ -69,13 +71,26 @@
69
71
  element = findOrCreateElementForNode(node)
70
72
  element.indexes = indexes
71
73
 
72
- for type, binding of Twine.bindingTypes when definition = Twine.getAttribute(node, type)
73
- element = findOrCreateElementForNode(node)
74
+ bindingConstructors = null
75
+ for attribute in node.attributes
76
+ type = attribute.name
77
+ type = type.slice(5) if isDataAttribute(type)
78
+
79
+ constructor = Twine.bindingTypes[type]
80
+ continue unless constructor
81
+
82
+ bindingConstructors ?= []
83
+ definition = attribute.value
84
+ bindingConstructors.push([type, constructor, definition])
85
+
86
+ if bindingConstructors
87
+ element ?= findOrCreateElementForNode(node)
74
88
  element.bindings ?= []
75
89
  element.indexes ?= indexes
76
90
 
77
- fn = binding(node, context, definition, element)
78
- element.bindings.push(fn) if fn
91
+ for [_, constructor, definition] in bindingConstructors.sort(bindingOrder)
92
+ binding = constructor(node, context, definition, element)
93
+ element.bindings.push(binding) if binding
79
94
 
80
95
  if newContextKey = Twine.getAttribute(node, 'context')
81
96
  keypath = keypathForKey(node, newContextKey)
@@ -85,18 +100,15 @@
85
100
  context = getValue(context, keypath) || setValue(context, keypath, {})
86
101
 
87
102
  if element || newContextKey || forceSaveContext
88
- element = findOrCreateElementForNode(node)
103
+ element ?= findOrCreateElementForNode(node)
89
104
  element.childContext = context
90
105
  element.indexes ?= indexes if indexes?
91
106
 
92
107
  callbacks = currentBindingCallbacks
93
108
 
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 || [])
109
+ # IE and Safari don't support node.children for DocumentFragment or SVGElement,
110
+ # See explanation in childrenForNode()
111
+ bind(context, childNode, if newContextKey? then null else indexes) for childNode in childrenForNode(node)
100
112
  Twine.count = nodeCount
101
113
 
102
114
  for callback in callbacks || []
@@ -105,13 +117,26 @@
105
117
 
106
118
  Twine
107
119
 
120
+ # IE and Safari don't support node.children for DocumentFragment and SVGElement nodes.
121
+ # If the element supports children we continue to traverse the children, otherwise
122
+ # we stop traversing that subtree.
123
+ # https://developer.mozilla.org/en-US/docs/Web/API/ParentNode.children
124
+ # As a result, Twine are unsupported within DocumentFragment and SVGElement nodes.
125
+ #
126
+ # We also prevent nodes from being iterated over more than once by cacheing the
127
+ # lookup for children nodes, which prevents nodes that are dynamically inserted
128
+ # or removed as siblings from causing double/ missed binds and unbinds.
129
+ childrenForNode = (node) ->
130
+ if node.children then Array::slice.call(node.children, 0) else []
131
+
108
132
  findOrCreateElementForNode = (node) ->
109
133
  node.bindingId ?= ++nodeCount
110
134
  elements[node.bindingId] ?= {}
111
- elements[node.bindingId]
112
135
 
113
136
  # Queues a refresh of the DOM, batching up calls for the current synchronous block.
114
- Twine.refresh = ->
137
+ # The callback will be called once when the refresh has completed.
138
+ Twine.refresh = (callback) ->
139
+ refreshCallbacks.push(callback) if callback
115
140
  return if refreshQueued
116
141
  refreshQueued = true
117
142
  setTimeout(Twine.refreshImmediately, 0)
@@ -123,6 +148,10 @@
123
148
  Twine.refreshImmediately = ->
124
149
  refreshQueued = false
125
150
  refreshElement(element) for key, element of elements
151
+
152
+ callbacks = refreshCallbacks
153
+ refreshCallbacks = []
154
+ cb() for cb in callbacks
126
155
  return
127
156
 
128
157
  Twine.register = (name, component) ->
@@ -145,10 +174,9 @@
145
174
  delete elements[id]
146
175
  delete node.bindingId
147
176
 
148
-
149
177
  # 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 || [])
178
+ # See explanation in childrenForNode()
179
+ Twine.unbind(childNode) for childNode in childrenForNode(node)
152
180
  this
153
181
 
154
182
  # Returns the binding context for a node by looking up the tree.
@@ -187,7 +215,7 @@
187
215
  addKey(rootContext) if node == rootNode
188
216
  keys.join('.')
189
217
 
190
- valueAttributeForNode = (node) ->
218
+ valuePropertyForNode = (node) ->
191
219
  name = node.nodeName.toLowerCase()
192
220
  if name in ['input', 'textarea', 'select']
193
221
  if node.getAttribute('type') in ['checkbox', 'radio'] then 'checked' else 'value'
@@ -232,14 +260,7 @@
232
260
  object[lastKey] = value
233
261
 
234
262
  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
263
+ [].map.call(node.attributes, (attr) -> "#{attr.name}=#{JSON.stringify(attr.value)}").join(' ')
243
264
 
244
265
  wrapFunctionString = (code, args, node) ->
245
266
  if isKeypath(code) && keypath = keypathForKey(node, code)
@@ -249,7 +270,7 @@
249
270
  ($context, $root) -> getValue($context, keypath)
250
271
  else
251
272
  code = "return #{code}"
252
- code = "with($arrayPointers) { #{code} }" if nodeHasArrayIndexes(node)
273
+ code = "with($arrayPointers) { #{code} }" if nodeArrayIndexes(node)
253
274
  code = "with($registry) { #{code} }" if requiresRegistry(args)
254
275
  try
255
276
  new Function(args, "with($context) { #{code} }")
@@ -258,14 +279,12 @@
258
279
 
259
280
  requiresRegistry = (args) -> /\$registry/.test(args)
260
281
 
261
- nodeHasArrayIndexes = (node) ->
262
- return unless node.bindingId?
263
- !!elements[node.bindingId]?.indexes?
282
+ nodeArrayIndexes = (node) ->
283
+ node.bindingId? && elements[node.bindingId]?.indexes
264
284
 
265
285
  arrayPointersForNode = (node, context) ->
266
- return {} unless node.bindingId?
267
- indexes = elements[node.bindingId]?.indexes
268
- return {} unless indexes?
286
+ indexes = nodeArrayIndexes(node)
287
+ return {} unless indexes
269
288
 
270
289
  result = {}
271
290
  for key, index of indexes
@@ -273,17 +292,35 @@
273
292
  result
274
293
 
275
294
  isKeypath = (value) ->
276
- value not in ['true', 'false', 'null', 'undefined'] and keypathRegex.test(value)
295
+ value not in ['true', 'false', 'null', 'undefined'] && keypathRegex.test(value)
296
+
297
+ isDataAttribute = (value) ->
298
+ value[0] == 'd' &&
299
+ value[1] == 'a' &&
300
+ value[2] == 't' &&
301
+ value[3] == 'a' &&
302
+ value[4] == '-'
277
303
 
278
304
  fireCustomChangeEvent = (node) ->
279
305
  event = document.createEvent('CustomEvent')
280
306
  event.initCustomEvent('bindings:change', true, false, {})
281
307
  node.dispatchEvent(event)
282
308
 
309
+ bindingOrder = ([firstType], [secondType]) ->
310
+ ORDERED_BINDINGS = {
311
+ define: 1,
312
+ bind: 2,
313
+ eval: 3
314
+ }
315
+ return 1 unless ORDERED_BINDINGS[firstType]
316
+ return -1 unless ORDERED_BINDINGS[secondType]
317
+
318
+ ORDERED_BINDINGS[firstType] - ORDERED_BINDINGS[secondType]
319
+
283
320
  Twine.bindingTypes =
284
321
  bind: (node, context, definition) ->
285
- valueAttribute = valueAttributeForNode(node)
286
- value = node[valueAttribute]
322
+ valueProp = valuePropertyForNode(node)
323
+ value = node[valueProp]
287
324
  lastValue = undefined
288
325
  teardown = undefined
289
326
 
@@ -296,9 +333,9 @@
296
333
  return if newValue == lastValue # return if we can and avoid a DOM operation
297
334
 
298
335
  lastValue = newValue
299
- return if newValue == node[valueAttribute]
336
+ return if newValue == node[valueProp]
300
337
 
301
- node[valueAttribute] = if checkedValueType then newValue == node.value else newValue
338
+ node[valueProp] = if checkedValueType then newValue == node.value else newValue
302
339
  fireCustomChangeEvent(node)
303
340
 
304
341
  return {refresh} unless isKeypath(definition)
@@ -308,10 +345,10 @@
308
345
  return unless node.checked
309
346
  setValue(context, keypath, node.value)
310
347
  else
311
- setValue(context, keypath, node[valueAttribute])
348
+ setValue(context, keypath, node[valueProp])
312
349
 
313
350
  keypath = keypathForKey(node, definition)
314
- twoWayBinding = valueAttribute != 'textContent' && node.type != 'hidden'
351
+ twoWayBinding = valueProp != 'textContent' && node.type != 'hidden'
315
352
 
316
353
  if keypath[0] == '$root'
317
354
  context = rootContext
@@ -321,13 +358,14 @@
321
358
  refreshContext()
322
359
 
323
360
  if twoWayBinding
361
+ events = ['input', 'keyup', 'change']
324
362
  changeHandler = ->
325
- return if getValue(context, keypath) == this[valueAttribute]
363
+ return if getValue(context, keypath) == this[valueProp]
326
364
  refreshContext()
327
365
  Twine.refreshImmediately()
328
- $(node).on 'input keyup change', changeHandler
366
+ node.addEventListener(eventName, changeHandler) for eventName in events
329
367
  teardown = ->
330
- $(node).off 'input keyup change', changeHandler
368
+ node.removeEventListener(eventName, changeHandler) for eventName in events
331
369
 
332
370
  {refresh, teardown}
333
371
 
@@ -337,16 +375,24 @@
337
375
  return refresh: ->
338
376
  newValue = !fn.call(node, context, rootContext, arrayPointersForNode(node, context))
339
377
  return if newValue == lastValue
340
- $(node).toggleClass('hide', lastValue = newValue)
378
+ node.classList.toggle('hide', lastValue = newValue)
341
379
 
342
380
  'bind-class': (node, context, definition) ->
343
381
  fn = wrapFunctionString(definition, '$context,$root,$arrayPointers', node)
344
- lastValue = {}
382
+ lastValues = {}
345
383
  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
384
+ newValues = fn.call(node, context, rootContext, arrayPointersForNode(node, context))
385
+
386
+ for key, value of newValues
387
+ newValue = newValues[key] = !!newValues[key]
388
+ currValue = lastValues[key] ? node.classList.contains(key)
389
+ if currValue != newValue
390
+ if newValue
391
+ node.classList.add(key)
392
+ else
393
+ node.classList.remove(key)
394
+
395
+ lastValues = newValues
350
396
 
351
397
  'bind-attribute': (node, context, definition) ->
352
398
  fn = wrapFunctionString(definition, '$context,$root,$arrayPointers', node)
@@ -354,7 +400,10 @@
354
400
  return refresh: ->
355
401
  newValue = fn.call(node, context, rootContext, arrayPointersForNode(node, context))
356
402
  for key, value of newValue when lastValue[key] != value
357
- $(node).attr(key, value || null)
403
+ if (!value)
404
+ node.removeAttribute(key)
405
+ else
406
+ node.setAttribute(key, value)
358
407
  lastValue = newValue
359
408
 
360
409
  define: (node, context, definition) ->
@@ -383,24 +432,24 @@
383
432
 
384
433
  indexes
385
434
 
386
- setupAttributeBinding = (attributeName, bindingName) ->
387
- booleanAttribute = attributeName in ['checked', 'indeterminate', 'disabled', 'readOnly']
435
+ setupPropertyBinding = (attributeName, bindingName) ->
436
+ booleanProp = attributeName in ['checked', 'indeterminate', 'disabled', 'readOnly', 'draggable']
388
437
 
389
- Twine.bindingTypes["bind-#{bindingName}"] = (node, context, definition) ->
438
+ Twine.bindingTypes["bind-#{bindingName.toLowerCase()}"] = (node, context, definition) ->
390
439
  fn = wrapFunctionString(definition, '$context,$root,$arrayPointers', node)
391
440
  lastValue = undefined
392
441
  return refresh: ->
393
442
  newValue = fn.call(node, context, rootContext, arrayPointersForNode(node, context))
394
- newValue = !!newValue if booleanAttribute
443
+ newValue = !!newValue if booleanProp
395
444
  return if newValue == lastValue
396
445
  node[attributeName] = lastValue = newValue
397
446
 
398
447
  fireCustomChangeEvent(node) if attributeName == 'checked'
399
448
 
400
- for attribute in ['placeholder', 'checked', 'indeterminate', 'disabled', 'href', 'title', 'readOnly', 'src']
401
- setupAttributeBinding(attribute, attribute)
449
+ for attribute in ['placeholder', 'checked', 'indeterminate', 'disabled', 'href', 'title', 'readOnly', 'src', 'draggable']
450
+ setupPropertyBinding(attribute, attribute)
402
451
 
403
- setupAttributeBinding('innerHTML', 'unsafe-html')
452
+ setupPropertyBinding('innerHTML', 'unsafe-html')
404
453
 
405
454
  preventDefaultForEvent = (event) ->
406
455
  (event.type == 'submit' || event.currentTarget.nodeName.toLowerCase() == 'a') &&
@@ -408,7 +457,7 @@
408
457
 
409
458
  setupEventBinding = (eventName) ->
410
459
  Twine.bindingTypes["bind-event-#{eventName}"] = (node, context, definition) ->
411
- onEventHandler = (event, data) ->
460
+ onEventHandler = (event, data = event.detail) ->
412
461
  discardEvent = Twine.shouldDiscardEvent[eventName]?(event)
413
462
  if discardEvent || preventDefaultForEvent(event)
414
463
  event.preventDefault()
@@ -417,10 +466,10 @@
417
466
 
418
467
  wrapFunctionString(definition, '$context,$root,$arrayPointers,event,data', node).call(node, context, rootContext, arrayPointersForNode(node, context), event, data)
419
468
  Twine.refreshImmediately()
420
- $(node).on eventName, onEventHandler
469
+ node.addEventListener(eventName, onEventHandler)
421
470
 
422
471
  return teardown: ->
423
- $(node).off eventName, onEventHandler
472
+ node.removeEventListener(eventName, onEventHandler)
424
473
 
425
474
  for eventName in ['click', 'dblclick', 'mouseenter', 'mouseleave', 'mouseover', 'mouseout', 'mousedown', 'mouseup',
426
475
  'submit', 'dragenter', 'dragleave', 'dragover', 'drop', 'drag', 'change', 'keypress', 'keydown', 'keyup', 'input',
data/lib/twine-rails.rb CHANGED
@@ -1,5 +1,4 @@
1
1
  require "rails"
2
- require "active_support"
3
2
 
4
3
  require "twine-rails/version"
5
4
 
@@ -1,3 +1,3 @@
1
1
  module TwineRails
2
- VERSION = '1.0.0'
2
+ VERSION = '2.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: 1.0.0
4
+ version: 2.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-06-23 00:00:00.000000000 Z
13
+ date: 2021-02-24 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: coffee-rails
@@ -26,6 +26,20 @@ dependencies:
26
26
  - - ">="
27
27
  - !ruby/object:Gem::Version
28
28
  version: '0'
29
+ - !ruby/object:Gem::Dependency
30
+ name: railties
31
+ requirement: !ruby/object:Gem::Requirement
32
+ requirements:
33
+ - - ">="
34
+ - !ruby/object:Gem::Version
35
+ version: '0'
36
+ type: :runtime
37
+ prerelease: false
38
+ version_requirements: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: '0'
29
43
  - !ruby/object:Gem::Dependency
30
44
  name: bundler
31
45
  requirement: !ruby/object:Gem::Requirement
@@ -69,7 +83,11 @@ files:
69
83
  homepage: https://github.com/Shopify/twine
70
84
  licenses:
71
85
  - MIT
72
- metadata: {}
86
+ metadata:
87
+ bug_tracker_uri: https://github.com/Shopify/twine/issues
88
+ changelog_uri: https://github.com/Shopify/twine/releases
89
+ source_code_uri: https://github.com/Shopify/twine
90
+ allowed_push_host: https://rubygems.org
73
91
  post_install_message:
74
92
  rdoc_options: []
75
93
  require_paths:
@@ -85,8 +103,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
85
103
  - !ruby/object:Gem::Version
86
104
  version: '0'
87
105
  requirements: []
88
- rubyforge_project:
89
- rubygems_version: 2.5.1
106
+ rubygems_version: 3.0.3
90
107
  signing_key:
91
108
  specification_version: 4
92
109
  summary: Minimalistic two-way bindings