twine-rails 0.0.5

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