twine-rails 0.0.5

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 5e3dd60b497b3a0ceea4b930d21396ce2b589421
4
+ data.tar.gz: 98f97ae534740b9acf588183825169cec31fe172
5
+ SHA512:
6
+ metadata.gz: 7e440905d00db7bdd0505d717ddd831579887ea4070dfce795fb77de64cec83a621875153a3c7ece28e4ff67aae011e049548d788dd3def4a63aa49da9ef569c
7
+ data.tar.gz: 6299d2b9385b20b9086b6f4d100d02bc030270e78f3d6c8eb7fc90265ee6432cae7e6ec66f45469867a011a3bd724658a5a0b6a0996bd1cc0fe30130563a2e6b
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2014 Shopify Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,111 @@
1
+ twine
2
+ -----
3
+
4
+ [![Build Status](https://secure.travis-ci.org/Shopify/twine.png)](http://travis-ci.org/Shopify/twine)
5
+
6
+ [Demonstration](http://shopify.github.io/twine/)
7
+
8
+ Twine is a minimalistic two-way binding system.
9
+
10
+ Features:
11
+ - It's just JS - no new syntax to learn for bindings
12
+ - Small and easy to understand codebase
13
+
14
+ Non-features:
15
+ - No creation of new nodes (e.g. no iteration)
16
+ - No special declaration of bindable data (i.e. bind to any JS data)
17
+
18
+ ## Installation
19
+
20
+ Twine is available on bower via `bower install twine` if that is your preference.
21
+
22
+ Twine comes as `dist/twine.js` and `dist/twine.min.js` in this repo and in the bower package.
23
+
24
+ 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`
25
+
26
+ ## Usage
27
+
28
+ Twine can be initialized simply with the following:
29
+
30
+ ```html
31
+ <script type="text/javascript">
32
+ context = {}
33
+ $(function() {
34
+ Twine.reset(context).bind().refresh()
35
+ });
36
+ </script>
37
+ ```
38
+
39
+ Above, `context` will be considered the context root, and this will work until you navigate to a new page. On a simple app, this may be all you need to do.
40
+
41
+ ### With [rails/turbolinks](https://github.com/rails/turbolinks)
42
+
43
+ Turbolinks requires a bit more consideration, as the executing JS context will remain the same -- you have the same `window` object throughout operation. When the page changes and new nodes come in, they need to be re-bound manually. Twine leaves this to you, rather than attempting to guess.
44
+
45
+ Here's a sample snippet that you might use:
46
+
47
+ ```coffee
48
+ context = {}
49
+
50
+ document.addEventListener 'page:change', ->
51
+ Twine.reset(context).bind().refresh()
52
+ return
53
+ ```
54
+
55
+ If you're using the [jquery.turbolinks gem](https://github.com/kossnocorp/jquery.turbolinks), then you can use:
56
+
57
+ ```coffee
58
+ context = {}
59
+
60
+ $ ->
61
+ Twine.reset(context).bind().refresh()
62
+ return
63
+ ```
64
+
65
+ ### With [Shopify/turbograft](https://github.com/Shopify/turbograft)
66
+
67
+ With TurboGraft, you may have cases where you want to keep parts of the page around, and thus, their bindings should continue to live.
68
+
69
+ The following snippet may help:
70
+
71
+ ```coffee
72
+ context = {}
73
+
74
+ reset = (nodes) ->
75
+ if nodes
76
+ Twine.bind(node) for node in nodes
77
+ else
78
+ Twine.reset(context).bind()
79
+
80
+ Twine.refreshImmediately()
81
+ return
82
+
83
+ document.addEventListener 'DOMContentLoaded', -> reset()
84
+
85
+ document.addEventListener 'page:load', (event) ->
86
+ reset(event.data)
87
+ return
88
+
89
+ document.addEventListener 'page:before-partial-replace', (event) ->
90
+ nodes = event.data
91
+ Twine.unbind(node) for node in nodes
92
+ return
93
+
94
+ $(document).ajaxComplete ->
95
+ Twine.refresh()
96
+ ```
97
+
98
+ ## Contributing
99
+
100
+ 1. Clone the repo: `git clone git@github.com:Shopify/twine`
101
+ 2. `cd twine`
102
+ 3. `npm install`
103
+ 4. `npm install -g testem coffee-script`
104
+ 5. Run the tests using `testem`, or `testem ci`
105
+ 6. Submit a PR
106
+
107
+ ## Releasing
108
+
109
+ 1. Update version number in `package.json`, `bower.json`, and `lib/twine-rails/version.rb`
110
+ 2. Run `bundle install` to update `Gemfile.lock`
111
+ 3. Push the new tag to GitHub and the new version to rubygems with `bundle exec rake release`
@@ -0,0 +1,293 @@
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
+ # Cleans up all existing bindings and sets the root node and context.
19
+ Twine.reset = (newContext, node = document.documentElement) ->
20
+ for bindingsArr in elements
21
+ obj.teardown() for obj in bindingsArr when obj.teardown
22
+ elements = {}
23
+
24
+ rootContext = newContext
25
+ rootNode = node
26
+ rootNode.bindingId = nodeCount = 1
27
+
28
+ this
29
+
30
+ Twine.bind = (node = rootNode, context = Twine.context(node)) ->
31
+ bind(context, node, true)
32
+
33
+ bind = (context, node, forceSaveContext) ->
34
+ if node.bindingId
35
+ Twine.unbind(node)
36
+
37
+ for type, binding of Twine.bindingTypes when definition = node.getAttribute(type)
38
+ element = {bindings: []} unless element # Defer allocation to prevent GC pressure
39
+ fn = binding(node, context, definition, element)
40
+ element.bindings.push(fn) if fn
41
+
42
+ if newContextKey = node.getAttribute('context')
43
+ keypath = keypathForKey(newContextKey)
44
+ if keypath[0] == '$root'
45
+ context = rootContext
46
+ keypath = keypath.slice(1)
47
+ context = getValue(context, keypath) || setValue(context, keypath, {})
48
+
49
+ if element || newContextKey || forceSaveContext
50
+ (element ?= {}).childContext = context
51
+ elements[node.bindingId ?= ++nodeCount] = element
52
+
53
+ # IE and Safari don't support node.children for DocumentFragment and SVGElement nodes.
54
+ # If the element supports children we continue to traverse the children, otherwise
55
+ # we stop traversing that subtree.
56
+ # https://developer.mozilla.org/en-US/docs/Web/API/ParentNode.children
57
+ # As a result, Twine are unsupported within DocumentFragment and SVGElement nodes.
58
+ bind(context, childNode) for childNode in (node.children || [])
59
+ Twine.count = nodeCount
60
+ Twine
61
+
62
+ # Queues a refresh of the DOM, batching up calls for the current synchronous block.
63
+ Twine.refresh = ->
64
+ return if refreshQueued
65
+ refreshQueued = true
66
+ setTimeout(Twine.refreshImmediately, 0)
67
+
68
+ refreshElement = (element) ->
69
+ (obj.refresh() if obj.refresh?) for obj in element.bindings if element.bindings
70
+ return
71
+
72
+ Twine.refreshImmediately = ->
73
+ refreshQueued = false
74
+ refreshElement(element) for key, element of elements
75
+ return
76
+
77
+ # Force the binding system to recognize programmatic changes to a node's value.
78
+ Twine.change = (node, bubble = false) ->
79
+ event = document.createEvent("HTMLEvents")
80
+ event.initEvent('change', bubble, true) # for IE 9/10 compatibility.
81
+ node.dispatchEvent(event)
82
+
83
+ # Cleans up everything related to a node and its subtree.
84
+ Twine.unbind = (node) ->
85
+ if id = node.bindingId
86
+ if bindings = elements[id]?.bindings
87
+ obj.teardown() for obj in bindings when obj.teardown
88
+ delete elements[id]
89
+ # IE and Safari don't support node.children for DocumentFragment or SVGElement,
90
+ # See explaination in bind()
91
+ Twine.unbind(childNode) for childNode in (node.children || [])
92
+ this
93
+
94
+ # Returns the binding context for a node by looking up the tree.
95
+ Twine.context = (node) -> getContext(node, false)
96
+ Twine.childContext = (node) -> getContext(node, true)
97
+
98
+ getContext = (node, child) ->
99
+ while node
100
+ return rootContext if node == rootNode
101
+ node = node.parentNode if !child
102
+ if (id = node.bindingId) && (context = elements[id]?.childContext)
103
+ return context
104
+ node = node.parentNode if child
105
+
106
+ # Returns the fully qualified key for a node's context
107
+ Twine.contextKey = (node, lastContext) ->
108
+ keys = []
109
+ addKey = (context) ->
110
+ for key, val of context when lastContext == val
111
+ keys.unshift(key)
112
+ break
113
+ lastContext = context
114
+
115
+ while node && node != rootNode && node = node.parentNode
116
+ addKey(context) if (id = node.bindingId) && (context = elements[id]?.childContext)
117
+
118
+ addKey(rootContext) if node == rootNode
119
+ keys.join('.')
120
+
121
+ valueAttributeForNode = (node) ->
122
+ name = node.nodeName.toLowerCase()
123
+ if name in ['input', 'textarea', 'select']
124
+ if node.getAttribute('type') in ['checkbox', 'radio'] then 'checked' else 'value'
125
+ else
126
+ 'textContent'
127
+
128
+ keypathForKey = (key) ->
129
+ keypath = []
130
+ for key in key.split('.')
131
+ if (start = key.indexOf('[')) != -1
132
+ keypath.push(key.substr(0, start))
133
+ key = key.substr(start)
134
+
135
+ while (end = key.indexOf(']')) != -1
136
+ keypath.push(parseInt(key.substr(1, end), 10))
137
+ key = key.substr(end + 1)
138
+ else
139
+ keypath.push(key)
140
+ keypath
141
+
142
+ getValue = (object, keypath) ->
143
+ object = object[key] for key in keypath when object?
144
+ object
145
+
146
+ setValue = (object, keypath, value) ->
147
+ [keypath..., lastKey] = keypath
148
+ for key in keypath
149
+ object = object[key] ?= {}
150
+ object[lastKey] = value
151
+
152
+ stringifyNodeAttributes = (node) ->
153
+ nAttributes = node.attributes.length
154
+ i = 0
155
+ result = ""
156
+ while i < nAttributes
157
+ attr = node.attributes.item(i)
158
+ result += "#{attr.nodeName}='#{attr.textContent}'"
159
+ i+=1
160
+ result
161
+
162
+ wrapFunctionString = (code, args, node) ->
163
+ if isKeypath(code) && keypath = keypathForKey(code)
164
+ if keypath[0] == '$root'
165
+ ($context, $root) -> getValue($root, keypath)
166
+ else
167
+ ($context, $root) -> getValue($context, keypath)
168
+ else
169
+ try
170
+ new Function(args, "with($context) { return #{code} }")
171
+ catch e
172
+ throw "Twine error: Unable to create function on #{node.nodeName} node with attributes #{stringifyNodeAttributes(node)}"
173
+
174
+ isKeypath = (value) ->
175
+ value not in ['true', 'false', 'null', 'undefined'] and keypathRegex.test(value)
176
+
177
+ fireCustomChangeEvent = (node) ->
178
+ event = document.createEvent('CustomEvent')
179
+ event.initCustomEvent('bindings:change', true, false, {})
180
+ node.dispatchEvent(event)
181
+
182
+ Twine.bindingTypes =
183
+ bind: (node, context, definition) ->
184
+ valueAttribute = valueAttributeForNode(node)
185
+ value = node[valueAttribute]
186
+ lastValue = undefined
187
+ teardown = undefined
188
+
189
+ # Radio buttons only set the value to the node value if checked.
190
+ checkedValueType = node.getAttribute('type') == 'radio'
191
+ fn = wrapFunctionString(definition, '$context,$root', node)
192
+
193
+ refresh = ->
194
+ newValue = fn.call(node, context, rootContext)
195
+ return if newValue == lastValue
196
+
197
+ lastValue = newValue
198
+ return if newValue == node[valueAttribute]
199
+
200
+ node[valueAttribute] = if checkedValueType then newValue == node.value else newValue
201
+ fireCustomChangeEvent(node)
202
+
203
+ return {refresh} unless isKeypath(definition)
204
+
205
+ refreshContext = ->
206
+ if checkedValueType
207
+ return unless node.checked
208
+ setValue(context, keypath, node.value)
209
+ else
210
+ setValue(context, keypath, node[valueAttribute])
211
+
212
+ keypath = keypathForKey(definition)
213
+ twoWayBinding = valueAttribute != 'textContent' && node.type != 'hidden'
214
+
215
+ if keypath[0] == '$root'
216
+ context = rootContext
217
+ keypath = keypath.slice(1)
218
+
219
+ if value? && (twoWayBinding || value != '') && !(oldValue = getValue(context, keypath))?
220
+ refreshContext()
221
+
222
+ if twoWayBinding
223
+ changeHandler = ->
224
+ return if getValue(context, keypath) == this[valueAttribute]
225
+ refreshContext()
226
+ Twine.refreshImmediately()
227
+ node.addEventListener(eventKey, changeHandler) for eventKey in ['input', 'keyup', 'change']
228
+ teardown = ->
229
+ node.removeEventListener(eventKey, changeHandler) for eventKey in ['input', 'keyup', 'change']
230
+
231
+ {refresh, teardown}
232
+
233
+ 'bind-show': (node, context, definition) ->
234
+ fn = wrapFunctionString(definition, '$context,$root', node)
235
+ lastValue = undefined
236
+ return refresh: ->
237
+ newValue = !fn.call(node, context, rootContext)
238
+ return if newValue == lastValue
239
+ $(node).toggleClass('hide', lastValue = newValue)
240
+
241
+ 'bind-class': (node, context, definition) ->
242
+ fn = wrapFunctionString(definition, '$context,$root', node)
243
+ lastValue = {}
244
+ return refresh: ->
245
+ newValue = fn.call(node, context, rootContext)
246
+ for key, value of newValue when !lastValue[key] != !value
247
+ $(node).toggleClass(key, !!value)
248
+ lastValue = newValue
249
+
250
+ define: (node, context, definition) ->
251
+ fn = wrapFunctionString(definition, '$context,$root', node)
252
+ object = fn.call(node, context, rootContext)
253
+ context[key] = value for key, value of object
254
+ return
255
+
256
+ setupAttributeBinding = (attributeName, bindingName) ->
257
+ booleanAttribute = attributeName in ['checked', 'disabled', 'readOnly']
258
+
259
+ Twine.bindingTypes["bind-#{bindingName}"] = (node, context, definition) ->
260
+ fn = wrapFunctionString(definition, '$context,$root', node)
261
+ lastValue = undefined
262
+ return refresh: ->
263
+ newValue = fn.call(node, context, rootContext)
264
+ newValue = !!newValue if booleanAttribute
265
+ return if newValue == lastValue
266
+ node[attributeName] = lastValue = newValue
267
+
268
+ for attribute in ['placeholder', 'checked', 'disabled', 'href', 'title', 'readOnly']
269
+ setupAttributeBinding(attribute, attribute)
270
+
271
+ setupAttributeBinding('innerHTML', 'unsafe-html')
272
+
273
+ preventDefaultForEvent = (event) ->
274
+ (event.type == 'submit' || event.currentTarget.nodeName.toLowerCase() == 'a') && event.currentTarget.getAttribute('allow-default') != '1'
275
+
276
+ setupEventBinding = (eventName) ->
277
+ Twine.bindingTypes["bind-event-#{eventName}"] = (node, context, definition) ->
278
+ onEventHandler = (event, data) ->
279
+ discardEvent = Twine.shouldDiscardEvent[eventName]?(event)
280
+ if discardEvent || preventDefaultForEvent(event)
281
+ event.preventDefault()
282
+
283
+ return if discardEvent
284
+
285
+ wrapFunctionString(definition, '$context,$root,event,data', node).call(node, context, rootContext, event, data)
286
+ Twine.refreshImmediately()
287
+ $(node).on eventName, onEventHandler
288
+
289
+ return teardown: ->
290
+ $(node).off eventName, onEventHandler
291
+
292
+ for eventName in ['click', 'dblclick', 'mousedown', 'mouseup', 'submit', 'dragenter', 'dragleave', 'dragover', 'drop', 'drag', 'change', 'keypress', 'keydown', 'keyup', 'input', 'error', 'done', 'fail', 'blur', 'focus']
293
+ setupEventBinding(eventName)
@@ -0,0 +1,3 @@
1
+ module TwineRails
2
+ VERSION = '0.0.5'
3
+ end
@@ -0,0 +1,9 @@
1
+ require "rails"
2
+ require "active_support"
3
+
4
+ require "twine-rails/version"
5
+
6
+ module TwineRails
7
+ class Engine < ::Rails::Engine
8
+ end
9
+ end
metadata ADDED
@@ -0,0 +1,93 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: twine-rails
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.5
5
+ platform: ruby
6
+ authors:
7
+ - Justin Li
8
+ - Kristian Plettenberg-Dussault
9
+ - Anthony Cameron
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2014-10-05 00:00:00.000000000 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: coffee-rails
17
+ requirement: !ruby/object:Gem::Requirement
18
+ requirements:
19
+ - - ">="
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ version: '0'
29
+ - !ruby/object:Gem::Dependency
30
+ name: bundler
31
+ requirement: !ruby/object:Gem::Requirement
32
+ requirements:
33
+ - - ">="
34
+ - !ruby/object:Gem::Version
35
+ version: '0'
36
+ type: :development
37
+ prerelease: false
38
+ version_requirements: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: '0'
43
+ - !ruby/object:Gem::Dependency
44
+ name: rake
45
+ requirement: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: '0'
50
+ type: :development
51
+ prerelease: false
52
+ version_requirements: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: '0'
57
+ description:
58
+ email:
59
+ - jli@shopify.com
60
+ executables: []
61
+ extensions: []
62
+ extra_rdoc_files: []
63
+ files:
64
+ - LICENSE
65
+ - README.md
66
+ - lib/assets/javascripts/twine.js.coffee
67
+ - lib/twine-rails.rb
68
+ - lib/twine-rails/version.rb
69
+ homepage: https://github.com/Shopify/twine
70
+ licenses:
71
+ - MIT
72
+ metadata: {}
73
+ post_install_message:
74
+ rdoc_options: []
75
+ require_paths:
76
+ - lib
77
+ required_ruby_version: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ required_rubygems_version: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: '0'
87
+ requirements: []
88
+ rubyforge_project:
89
+ rubygems_version: 2.2.2
90
+ signing_key:
91
+ specification_version: 4
92
+ summary: Minimalistic two-way bindings
93
+ test_files: []