turbograft 0.4.7 → 0.5.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.
Files changed (29) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -4
  3. data/lib/assets/javascripts/turbograft/click.js +44 -0
  4. data/lib/assets/javascripts/turbograft/component_url.js +53 -0
  5. data/lib/assets/javascripts/turbograft/csrf_token.js +23 -0
  6. data/lib/assets/javascripts/turbograft/document.js +20 -0
  7. data/lib/assets/javascripts/turbograft/initializers.js +83 -0
  8. data/lib/assets/javascripts/turbograft/link.js +58 -0
  9. data/lib/assets/javascripts/turbograft/page.js +105 -0
  10. data/lib/assets/javascripts/turbograft/remote.js +248 -0
  11. data/lib/assets/javascripts/turbograft/response.js +48 -0
  12. data/lib/assets/javascripts/turbograft/turbohead.js +159 -0
  13. data/lib/assets/javascripts/turbograft/turbolinks.js +504 -0
  14. data/lib/assets/javascripts/turbograft.js +37 -0
  15. data/lib/turbograft/version.rb +1 -1
  16. data/lib/turbograft/x_domain_blocker.rb +2 -2
  17. metadata +28 -28
  18. data/lib/assets/javascripts/turbograft/click.coffee +0 -34
  19. data/lib/assets/javascripts/turbograft/component_url.coffee +0 -24
  20. data/lib/assets/javascripts/turbograft/csrf_token.coffee +0 -9
  21. data/lib/assets/javascripts/turbograft/document.coffee +0 -11
  22. data/lib/assets/javascripts/turbograft/initializers.coffee +0 -67
  23. data/lib/assets/javascripts/turbograft/link.coffee +0 -41
  24. data/lib/assets/javascripts/turbograft/page.coffee +0 -77
  25. data/lib/assets/javascripts/turbograft/remote.coffee +0 -179
  26. data/lib/assets/javascripts/turbograft/response.coffee +0 -31
  27. data/lib/assets/javascripts/turbograft/turbohead.coffee +0 -142
  28. data/lib/assets/javascripts/turbograft/turbolinks.coffee +0 -361
  29. data/lib/assets/javascripts/turbograft.coffee +0 -30
@@ -1,31 +0,0 @@
1
- class TurboGraft.Response
2
- constructor: (@xhr, intendedURL) ->
3
- if intendedURL && intendedURL.withoutHash() != @xhr.responseURL
4
- redirectedTo = @xhr.responseURL
5
- else
6
- redirectedTo = @xhr.getResponseHeader('X-XHR-Redirected-To')
7
-
8
- @finalURL = redirectedTo || intendedURL
9
-
10
- valid: -> @hasRenderableHttpStatus() && @hasValidContent()
11
-
12
- document: ->
13
- if @valid()
14
- TurboGraft.Document.create(@xhr.responseText)
15
-
16
- hasRenderableHttpStatus: ->
17
- return true if @xhr.status == 422 # we want to render form validations
18
- !(400 <= @xhr.status < 600)
19
-
20
- hasValidContent: ->
21
- if contentType = @xhr.getResponseHeader('Content-Type')
22
- contentType.match(/^(?:text\/html|application\/xhtml\+xml|application\/xml)(?:;|$)/)
23
- else
24
- throw new Error("Error encountered for XHR Response: #{this}")
25
-
26
- toString: () ->
27
- "URL: #{@xhr.responseURL}, " +
28
- "ReadyState: #{@xhr.readyState}, " +
29
- "Headers: #{@xhr.getAllResponseHeaders()}"
30
-
31
- TurboGraft.location = () -> location.href
@@ -1,142 +0,0 @@
1
- TRACKED_ASSET_SELECTOR = '[data-turbolinks-track]'
2
- TRACKED_ATTRIBUTE_NAME = 'turbolinksTrack'
3
- ANONYMOUS_TRACK_VALUE = 'true'
4
-
5
- scriptPromises = {}
6
- resolvePreviousRequest = null
7
-
8
- waitForCompleteDownloads = ->
9
- loadingPromises = Object.keys(scriptPromises).map (url) ->
10
- scriptPromises[url]
11
- Promise.all(loadingPromises)
12
-
13
- class TurboGraft.TurboHead
14
- constructor: (@activeDocument, @upstreamDocument) ->
15
- @activeAssets = extractTrackedAssets(@activeDocument)
16
- @upstreamAssets = extractTrackedAssets(@upstreamDocument)
17
- @newScripts = @upstreamAssets
18
- .filter(attributeMatches('nodeName', 'SCRIPT'))
19
- .filter(noAttributeMatchesIn('src', @activeAssets))
20
-
21
- @newLinks = @upstreamAssets
22
- .filter(attributeMatches('nodeName', 'LINK'))
23
- .filter(noAttributeMatchesIn('href', @activeAssets))
24
-
25
- @_testAPI: {
26
- reset: ->
27
- scriptPromises = {}
28
- resolvePreviousRequest = null
29
- }
30
-
31
- hasChangedAnonymousAssets: () ->
32
- anonymousUpstreamAssets = @upstreamAssets
33
- .filter(datasetMatches(TRACKED_ATTRIBUTE_NAME, ANONYMOUS_TRACK_VALUE))
34
- anonymousActiveAssets = @activeAssets
35
- .filter(datasetMatches(TRACKED_ATTRIBUTE_NAME, ANONYMOUS_TRACK_VALUE))
36
-
37
- if anonymousActiveAssets.length != anonymousUpstreamAssets.length
38
- return true
39
-
40
- noMatchingSrc = noAttributeMatchesIn('src', anonymousUpstreamAssets)
41
- noMatchingHref = noAttributeMatchesIn('href', anonymousUpstreamAssets)
42
-
43
- anonymousActiveAssets.some((node) ->
44
- noMatchingSrc(node) || noMatchingHref(node)
45
- )
46
-
47
- movingFromTrackedToUntracked: () ->
48
- @upstreamAssets.length == 0 && @activeAssets.length > 0
49
-
50
- hasNamedAssetConflicts: () ->
51
- @newScripts
52
- .concat(@newLinks)
53
- .filter(noDatasetMatches(TRACKED_ATTRIBUTE_NAME, ANONYMOUS_TRACK_VALUE))
54
- .some(datasetMatchesIn(TRACKED_ATTRIBUTE_NAME, @activeAssets))
55
-
56
- hasAssetConflicts: () ->
57
- @movingFromTrackedToUntracked() ||
58
- @hasNamedAssetConflicts() ||
59
- @hasChangedAnonymousAssets()
60
-
61
- waitForAssets: () ->
62
- resolvePreviousRequest?(isCanceled: true)
63
-
64
- new Promise((resolve) =>
65
- resolvePreviousRequest = resolve
66
- waitForCompleteDownloads()
67
- .then(@_insertNewAssets)
68
- .then(waitForCompleteDownloads)
69
- .then(resolve)
70
- )
71
-
72
- _insertNewAssets: () =>
73
- updateLinkTags(@activeDocument, @newLinks)
74
- updateScriptTags(@activeDocument, @newScripts)
75
-
76
- extractTrackedAssets = (doc) ->
77
- [].slice.call(doc.querySelectorAll(TRACKED_ASSET_SELECTOR))
78
-
79
- attributeMatches = (attribute, value) ->
80
- (node) -> node[attribute] == value
81
-
82
- attributeMatchesIn = (attribute, collection) ->
83
- (node) ->
84
- collection.some((nodeFromCollection) -> node[attribute] == nodeFromCollection[attribute])
85
-
86
- noAttributeMatchesIn = (attribute, collection) ->
87
- (node) ->
88
- !collection.some((nodeFromCollection) -> node[attribute] == nodeFromCollection[attribute])
89
-
90
- datasetMatches = (attribute, value) ->
91
- (node) -> node.dataset[attribute] == value
92
-
93
- noDatasetMatches = (attribute, value) ->
94
- (node) -> node.dataset[attribute] != value
95
-
96
- datasetMatchesIn = (attribute, collection) ->
97
- (node) ->
98
- value = node.dataset[attribute]
99
- collection.some(datasetMatches(attribute, value))
100
-
101
- noDatasetMatchesIn = (attribute, collection) ->
102
- (node) ->
103
- value = node.dataset[attribute]
104
- !collection.some(datasetMatches(attribute, value))
105
-
106
- updateLinkTags = (activeDocument, newLinks) ->
107
- # style tag load events don't work in all browsers
108
- # as such we just hope they load ¯\_(ツ)_/¯
109
- newLinks.forEach((linkNode) ->
110
- newNode = linkNode.cloneNode()
111
- activeDocument.head.appendChild(newNode)
112
- triggerEvent("page:after-link-inserted", newNode)
113
- )
114
-
115
- updateScriptTags = (activeDocument, newScripts) ->
116
- promise = Promise.resolve()
117
- newScripts.forEach (scriptNode) ->
118
- promise = promise.then(-> insertScript(activeDocument, scriptNode))
119
- promise
120
-
121
- insertScript = (activeDocument, scriptNode) ->
122
- url = scriptNode.src
123
- if scriptPromises[url]
124
- return scriptPromises[url]
125
-
126
- # Clone script tags to guarantee browser execution.
127
- newNode = activeDocument.createElement('SCRIPT')
128
- newNode.setAttribute(attr.name, attr.value) for attr in scriptNode.attributes
129
- newNode.appendChild(activeDocument.createTextNode(scriptNode.innerHTML))
130
-
131
- scriptPromises[url] = new Promise((resolve) ->
132
- onAssetEvent = (event) ->
133
- triggerEvent("page:#script-error", event) if event.type == 'error'
134
- newNode.removeEventListener('load', onAssetEvent)
135
- newNode.removeEventListener('error', onAssetEvent)
136
- resolve()
137
-
138
- newNode.addEventListener('load', onAssetEvent)
139
- newNode.addEventListener('error', onAssetEvent)
140
- activeDocument.head.appendChild(newNode)
141
- triggerEvent("page:after-script-inserted", newNode)
142
- )
@@ -1,361 +0,0 @@
1
- Response = TurboGraft.Response
2
- TurboHead = TurboGraft.TurboHead
3
- jQuery = window.jQuery
4
-
5
- xhr = null
6
- activeDocument = document
7
-
8
- installDocumentReadyPageEventTriggers = ->
9
- activeDocument.addEventListener 'DOMContentLoaded', ( ->
10
- triggerEvent 'page:change'
11
- triggerEvent 'page:update'
12
- ), true
13
-
14
- installJqueryAjaxSuccessPageUpdateTrigger = ->
15
- if typeof jQuery isnt 'undefined'
16
- jQuery(activeDocument).on 'ajaxSuccess', (event, xhr, settings) ->
17
- return unless jQuery.trim xhr.responseText
18
- triggerEvent 'page:update'
19
-
20
- # Handle bug in Firefox 26/27 where history.state is initially undefined
21
- historyStateIsDefined =
22
- window.history.state != undefined or navigator.userAgent.match /Firefox\/2[6|7]/
23
-
24
- browserSupportsPushState =
25
- window.history and window.history.pushState and window.history.replaceState and historyStateIsDefined
26
-
27
- window.triggerEvent = (name, data) ->
28
- event = activeDocument.createEvent 'Events'
29
- event.data = data if data
30
- event.initEvent name, true, true
31
- activeDocument.dispatchEvent event
32
-
33
- window.triggerEventFor = (name, node, data) ->
34
- event = activeDocument.createEvent 'Events'
35
- event.data = data if data
36
- event.initEvent name, true, true
37
- node.dispatchEvent event
38
-
39
- popCookie = (name) ->
40
- value = activeDocument.cookie.match(new RegExp(name+"=(\\w+)"))?[1].toUpperCase() or ''
41
- activeDocument.cookie = name + '=; expires=Thu, 01-Jan-70 00:00:01 GMT; path=/'
42
- value
43
-
44
- requestMethodIsSafe =
45
- popCookie('request_method') in ['GET','']
46
-
47
- browserSupportsTurbolinks = browserSupportsPushState and requestMethodIsSafe
48
-
49
- browserSupportsCustomEvents =
50
- activeDocument.addEventListener and activeDocument.createEvent
51
-
52
- if browserSupportsCustomEvents
53
- installDocumentReadyPageEventTriggers()
54
- installJqueryAjaxSuccessPageUpdateTrigger()
55
-
56
- replaceNode = (newNode, oldNode) ->
57
- replacedNode = oldNode.parentNode.replaceChild(newNode, oldNode)
58
- triggerEvent('page:after-node-removed', replacedNode)
59
-
60
- removeNode = (node) ->
61
- removedNode = node.parentNode.removeChild(node)
62
- triggerEvent('page:after-node-removed', removedNode)
63
-
64
- # TODO: triggerEvent should be accessible to all these guys
65
- # on some kind of eventbus
66
- # TODO: clean up everything above me ^
67
- # TODO: decide on the public API
68
- class window.Turbolinks
69
- currentState = null
70
- referer = null
71
-
72
- fetch = (url, options = {}) ->
73
- return if pageChangePrevented(url)
74
- url = new ComponentUrl(url)
75
-
76
- rememberReferer()
77
-
78
- fetchReplacement(url, options)
79
-
80
- isPartialReplace = (response, options) ->
81
- Boolean(
82
- options.partialReplace ||
83
- options.onlyKeys?.length ||
84
- options.exceptKeys?.length
85
- )
86
-
87
- @fullPageNavigate: (url) ->
88
- if url?
89
- url = (new ComponentUrl(url)).absolute
90
- triggerEvent('page:before-full-refresh', url: url)
91
- activeDocument.location.href = url
92
- return
93
-
94
- @pushState: (state, title, url) ->
95
- window.history.pushState(state, title, url)
96
-
97
- @replaceState: (state, title, url) ->
98
- window.history.replaceState(state, title, url)
99
-
100
- @document: (documentToUse) ->
101
- activeDocument = documentToUse if documentToUse
102
- activeDocument
103
-
104
- fetchReplacement = (url, options) ->
105
- triggerEvent 'page:fetch', url: url.absolute
106
-
107
- if xhr?
108
- # Workaround for sinon xhr.abort()
109
- # https://github.com/sinonjs/sinon/issues/432#issuecomment-216917023
110
- xhr.readyState = 0
111
- xhr.statusText = "abort"
112
- xhr.abort()
113
-
114
- xhr = new XMLHttpRequest
115
-
116
- xhr.open 'GET', url.withoutHashForIE10compatibility(), true
117
- xhr.setRequestHeader 'Accept', 'text/html, application/xhtml+xml, application/xml'
118
- xhr.setRequestHeader 'X-XHR-Referer', referer
119
- options.headers ?= {}
120
-
121
- for k,v of options.headers
122
- xhr.setRequestHeader k, v
123
-
124
- xhr.onload = ->
125
- if xhr.status >= 500
126
- Turbolinks.fullPageNavigate(url)
127
- else
128
- Turbolinks.loadPage(url, xhr, options)
129
- xhr = null
130
-
131
- xhr.onerror = ->
132
- # Workaround for sinon xhr.abort()
133
- if xhr.statusText == "abort"
134
- xhr = null
135
- return
136
- Turbolinks.fullPageNavigate(url)
137
-
138
- xhr.send()
139
-
140
- return
141
-
142
- @loadPage: (url, xhr, options = {}) ->
143
- triggerEvent 'page:receive'
144
- response = new Response(xhr, url)
145
- options.updatePushState ?= true
146
- options.partialReplace = isPartialReplace(response, options)
147
-
148
- unless upstreamDocument = response.document()
149
- triggerEvent 'page:error', xhr
150
- Turbolinks.fullPageNavigate(response.finalURL)
151
- return
152
-
153
- if options.partialReplace
154
- updateBody(upstreamDocument, response, options)
155
- return
156
-
157
- turbohead = new TurboHead(activeDocument, upstreamDocument)
158
- if turbohead.hasAssetConflicts()
159
- return Turbolinks.fullPageNavigate(response.finalURL)
160
-
161
- turbohead.waitForAssets().then((result) ->
162
- updateBody(upstreamDocument, response, options) unless result?.isCanceled
163
- )
164
-
165
- updateBody = (upstreamDocument, response, options) ->
166
- nodes = changePage(
167
- upstreamDocument.querySelector('title')?.textContent,
168
- removeNoscriptTags(upstreamDocument.querySelector('body')),
169
- CSRFToken.get(upstreamDocument).token,
170
- 'runScripts',
171
- options
172
- )
173
- reflectNewUrl(response.finalURL) if options.updatePushState
174
-
175
- Turbolinks.resetScrollPosition() unless options.partialReplace
176
-
177
- options.callback?()
178
- triggerEvent 'page:load', nodes
179
-
180
- changePage = (title, body, csrfToken, runScripts, options = {}) ->
181
- activeDocument.title = title if title
182
-
183
- if options.onlyKeys?.length
184
- nodesToRefresh = [].concat(getNodesWithRefreshAlways(), getNodesMatchingRefreshKeys(options.onlyKeys))
185
- nodes = refreshNodes(nodesToRefresh, body)
186
- setAutofocusElement() if anyAutofocusElement(nodes)
187
- return nodes
188
- else
189
- refreshNodes(getNodesWithRefreshAlways(), body)
190
- persistStaticElements(body)
191
- if options.exceptKeys?.length
192
- refreshAllExceptWithKeys(options.exceptKeys, body)
193
- else
194
- deleteRefreshNeverNodes(body)
195
-
196
- triggerEvent 'page:before-replace'
197
- replaceNode(body, activeDocument.body)
198
- CSRFToken.update csrfToken if csrfToken?
199
- setAutofocusElement()
200
- executeScriptTags() if runScripts
201
- currentState = window.history.state
202
- triggerEvent 'page:change'
203
- triggerEvent 'page:update'
204
-
205
- return
206
-
207
- getNodesMatchingRefreshKeys = (keys) ->
208
- matchingNodes = []
209
- for key in keys
210
- for node in TurboGraft.querySelectorAllTGAttribute(activeDocument, 'refresh', key)
211
- matchingNodes.push(node)
212
-
213
- return matchingNodes
214
-
215
- getNodesWithRefreshAlways = ->
216
- matchingNodes = []
217
- for node in TurboGraft.querySelectorAllTGAttribute(activeDocument, 'refresh-always')
218
- matchingNodes.push(node)
219
-
220
- return matchingNodes
221
-
222
- anyAutofocusElement = (nodes) ->
223
- for node in nodes
224
- if node.querySelectorAll('input[autofocus], textarea[autofocus]').length > 0
225
- return true
226
-
227
- false
228
-
229
- setAutofocusElement = ->
230
- autofocusElement = (list = activeDocument.querySelectorAll 'input[autofocus], textarea[autofocus]')[list.length - 1]
231
- if autofocusElement and activeDocument.activeElement isnt autofocusElement
232
- autofocusElement.focus()
233
-
234
- deleteRefreshNeverNodes = (body) ->
235
- for node in TurboGraft.querySelectorAllTGAttribute(body, 'refresh-never')
236
- removeNode(node)
237
-
238
- return
239
-
240
- refreshNodes = (allNodesToBeRefreshed, body) ->
241
- triggerEvent 'page:before-partial-replace', allNodesToBeRefreshed
242
-
243
- parentIsRefreshing = (node) ->
244
- for potentialParent in allNodesToBeRefreshed when node != potentialParent
245
- return true if potentialParent.contains(node)
246
- false
247
-
248
- refreshedNodes = []
249
- for existingNode in allNodesToBeRefreshed
250
- continue if parentIsRefreshing(existingNode)
251
-
252
- unless nodeId = existingNode.getAttribute('id')
253
- throw new Error "Turbolinks refresh: Refresh key elements must have an id."
254
-
255
- if newNode = body.querySelector("##{ nodeId }")
256
- newNode = newNode.cloneNode(true)
257
- replaceNode(newNode, existingNode)
258
-
259
- if newNode.nodeName == 'SCRIPT' && newNode.dataset.turbolinksEval != "false"
260
- executeScriptTag(newNode)
261
- else
262
- refreshedNodes.push(newNode)
263
-
264
- else if !TurboGraft.hasTGAttribute(existingNode, "refresh-always")
265
- removeNode(existingNode)
266
-
267
- refreshedNodes
268
-
269
- keepNodes = (body, allNodesToKeep) ->
270
- for existingNode in allNodesToKeep
271
- unless nodeId = existingNode.getAttribute('id')
272
- throw new Error("TurboGraft refresh: Kept nodes must have an id.")
273
-
274
- if remoteNode = body.querySelector("##{ nodeId }")
275
- replaceNode(existingNode, remoteNode)
276
-
277
- persistStaticElements = (body) ->
278
- allNodesToKeep = []
279
-
280
- nodes = TurboGraft.querySelectorAllTGAttribute(activeDocument, 'tg-static')
281
- allNodesToKeep.push(node) for node in nodes
282
-
283
- keepNodes(body, allNodesToKeep)
284
- return
285
-
286
- refreshAllExceptWithKeys = (keys, body) ->
287
- allNodesToKeep = []
288
-
289
- for key in keys
290
- for node in TurboGraft.querySelectorAllTGAttribute(activeDocument, 'refresh', key)
291
- allNodesToKeep.push(node)
292
-
293
- keepNodes(body, allNodesToKeep)
294
- return
295
-
296
- executeScriptTags = ->
297
- scripts = Array::slice.call activeDocument.body.querySelectorAll 'script:not([data-turbolinks-eval="false"])'
298
- for script in scripts when script.type in ['', 'text/javascript']
299
- executeScriptTag(script)
300
- return
301
-
302
- executeScriptTag = (script) ->
303
- copy = activeDocument.createElement 'script'
304
- copy.setAttribute attr.name, attr.value for attr in script.attributes
305
- copy.appendChild activeDocument.createTextNode script.innerHTML
306
- { parentNode, nextSibling } = script
307
- parentNode.removeChild script
308
- parentNode.insertBefore copy, nextSibling
309
- return
310
-
311
- removeNoscriptTags = (node) ->
312
- node.innerHTML = node.innerHTML.replace /<noscript[\S\s]*?<\/noscript>/ig, ''
313
- node
314
-
315
- reflectNewUrl = (url) ->
316
- if (url = new ComponentUrl url).absolute isnt referer
317
- Turbolinks.pushState { turbolinks: true, url: url.absolute }, '', url.absolute
318
- return
319
-
320
- rememberReferer = ->
321
- referer = activeDocument.location.href
322
-
323
- @rememberCurrentUrl: ->
324
- Turbolinks.replaceState { turbolinks: true, url: activeDocument.location.href }, '', activeDocument.location.href
325
-
326
- @rememberCurrentState: ->
327
- currentState = window.history.state
328
-
329
- recallScrollPosition = (page) ->
330
- window.scrollTo page.positionX, page.positionY
331
-
332
- @resetScrollPosition: ->
333
- if activeDocument.location.hash
334
- activeDocument.location.href = activeDocument.location.href
335
- else
336
- window.scrollTo 0, 0
337
-
338
- pageChangePrevented = (url) ->
339
- !triggerEvent('page:before-change', url)
340
-
341
- installHistoryChangeHandler = (event) ->
342
- if event.state?.turbolinks
343
- Turbolinks.visit event.target.location.href
344
-
345
- # Delay execution of function long enough to miss the popstate event
346
- # some browsers fire on the initial page load.
347
- bypassOnLoadPopstate = (fn) ->
348
- setTimeout fn, 500
349
-
350
- if browserSupportsTurbolinks
351
- @visit = fetch
352
- @rememberCurrentUrl()
353
- @rememberCurrentState()
354
-
355
- activeDocument.addEventListener 'click', Click.installHandlerLast, true
356
-
357
- bypassOnLoadPopstate ->
358
- window.addEventListener 'popstate', installHistoryChangeHandler, false
359
-
360
- else
361
- @visit = (url) -> activeDocument.location.href = url
@@ -1,30 +0,0 @@
1
- #= require_self
2
- #= require_tree ./turbograft
3
-
4
- window.TurboGraft ?= { handlers: {} }
5
-
6
- TurboGraft.tgAttribute = (attr) ->
7
- tgAttr = if attr[0...3] == 'tg-'
8
- "data-#{attr}"
9
- else
10
- "data-tg-#{attr}"
11
-
12
- TurboGraft.getTGAttribute = (node, attr) ->
13
- tgAttr = TurboGraft.tgAttribute(attr)
14
- node.getAttribute(tgAttr) || node.getAttribute(attr)
15
-
16
- TurboGraft.removeTGAttribute = (node, attr) ->
17
- tgAttr = TurboGraft.tgAttribute(attr)
18
- node.removeAttribute(tgAttr)
19
- node.removeAttribute(attr)
20
-
21
- TurboGraft.hasTGAttribute = (node, attr) ->
22
- tgAttr = TurboGraft.tgAttribute(attr)
23
- node.hasAttribute(tgAttr) || node.hasAttribute(attr)
24
-
25
- TurboGraft.querySelectorAllTGAttribute = (node, attr, value = null) ->
26
- tgAttr = TurboGraft.tgAttribute(attr)
27
- if value
28
- node.querySelectorAll("[#{tgAttr}=#{value}], [#{attr}=#{value}]")
29
- else
30
- node.querySelectorAll("[#{tgAttr}], [#{attr}]")