Add rails-ujs to Action View

This commit is contained in:
Guillermo Iguaran 2016-11-25 10:27:07 -05:00
parent 0cafbd4e9e
commit ad3a47759e
14 changed files with 649 additions and 0 deletions

2
actionview/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/lib/assets/compiled
/tmp

View File

@ -0,0 +1,37 @@
#= export Rails
@Rails =
# Link elements bound by jquery-ujs
linkClickSelector: 'a[data-confirm], a[data-method], a[data-remote]:not([disabled]), a[data-disable-with], a[data-disable]'
# Button elements bound by jquery-ujs
buttonClickSelector:
selector: 'button[data-remote]:not([form]), button[data-confirm]:not([form])'
exclude: 'form button'
# Select elements bound by jquery-ujs
inputChangeSelector: 'select[data-remote], input[data-remote], textarea[data-remote]'
# Form elements bound by jquery-ujs
formSubmitSelector: 'form'
# Form input elements bound by jquery-ujs
formInputClickSelector: 'form input[type=submit], form input[type=image], form button[type=submit], form button:not([type]), input[type=submit][form], input[type=image][form], button[type=submit][form], button[form]:not([type])'
# Form input elements disabled during form submission
formDisableSelector: 'input[data-disable-with]:enabled, button[data-disable-with]:enabled, textarea[data-disable-with]:enabled, input[data-disable]:enabled, button[data-disable]:enabled, textarea[data-disable]:enabled'
# Form input elements re-enabled after form submission
formEnableSelector: 'input[data-disable-with]:disabled, button[data-disable-with]:disabled, textarea[data-disable-with]:disabled, input[data-disable]:disabled, button[data-disable]:disabled, textarea[data-disable]:disabled'
# Form required input elements
requiredInputSelector: 'input[name][required]:not([disabled]), textarea[name][required]:not([disabled])'
# Form file input elements
fileInputSelector: 'input[name][type=file]:not([disabled])'
# Link onClick disable selector with possible reenable after remote submission
linkDisableSelector: 'a[data-disable-with], a[data-disable]'
# Button onClick disable selector with possible reenable after remote submission
buttonDisableSelector: 'button[data-remote][data-disable-with], button[data-remote][data-disable]'

View File

@ -0,0 +1,26 @@
#= require_tree ../utils
{ fire, stopEverything } = Rails
Rails.handleConfirm = (e) ->
stopEverything(e) unless allowAction(this)
# For 'data-confirm' attribute:
# - Fires `confirm` event
# - Shows the confirmation dialog
# - Fires the `confirm:complete` event
#
# Returns `true` if no function stops the chain and user chose yes `false` otherwise.
# Attaching a handler to the element's `confirm` event that returns a `falsy` value cancels the confirmation dialog.
# Attaching a handler to the element's `confirm:complete` event that returns a `falsy` value makes this function
# return false. The `confirm:complete` event is fired whether or not the user answered true or false to the dialog.
allowAction = (element) ->
message = element.getAttribute('data-confirm')
return true unless message
answer = false
if fire(element, 'confirm')
try answer = confirm(message)
callback = fire(element, 'confirm:complete', [answer])
answer and callback

View File

@ -0,0 +1,78 @@
#= require_tree ../utils
{ matches, getData, setData, stopEverything, formElements } = Rails
# Unified function to enable an element (link, button and form)
Rails.enableElement = (e) ->
element = if e instanceof Event then e.target else e
if matches(element, Rails.linkDisableSelector)
enableLinkElement(element)
else if matches(element, Rails.buttonDisableSelector) or matches(element, Rails.formEnableSelector)
enableFormElement(element)
else if matches(element, Rails.formSubmitSelector)
enableFormElements(element)
# Unified function to disable an element (link, button and form)
Rails.disableElement = (e) ->
element = if e instanceof Event then e.target else e
if matches(element, Rails.linkDisableSelector)
disableLinkElement(element)
else if matches(element, Rails.buttonDisableSelector) or matches(element, Rails.formDisableSelector)
disableFormElement(element)
else if matches(element, Rails.formSubmitSelector)
disableFormElements(element)
# Replace element's html with the 'data-disable-with' after storing original html
# and prevent clicking on it
disableLinkElement = (element) ->
replacement = element.getAttribute('data-disable-with')
if replacement?
setData(element, 'ujs:enable-with', element.innerHTML) # store enabled state
element.innerHTML = replacement
element.addEventListener('click', stopEverything) # prevent further clicking
setData(element, 'ujs:disabled', true)
# Restore element to its original state which was disabled by 'disableLinkElement' above
enableLinkElement = (element) ->
originalText = getData(element, 'ujs:enable-with')
if originalText?
element.innerHTML = originalText # set to old enabled state
setData(element, 'ujs:enable-with', null) # clean up cache
element.removeEventListener('click', stopEverything) # enable element
setData(element, 'ujs:disabled', null)
# Disables form elements:
# - Caches element value in 'ujs:enable-with' data store
# - Replaces element text with value of 'data-disable-with' attribute
# - Sets disabled property to true
disableFormElements = (form) ->
formElements(form, Rails.formDisableSelector).forEach(disableFormElement)
disableFormElement = (element) ->
replacement = element.getAttribute('data-disable-with')
if replacement?
if matches(element, 'button')
setData(element, 'ujs:enable-with', element.innerHTML)
element.innerHTML = replacement
else
setData(element, 'ujs:enable-with', element.value)
element.value = replacement
element.disabled = true
setData(element, 'ujs:disabled', true)
# Re-enables disabled form elements:
# - Replaces element text with cached value from 'ujs:enable-with' data store (created in `disableFormElements`)
# - Sets disabled property to false
enableFormElements = (form) ->
formElements(form, Rails.formEnableSelector).forEach(enableFormElement)
enableFormElement = (element) ->
originalText = getData(element, 'ujs:enable-with')
if originalText?
if matches(element, 'button')
element.innerHTML = originalText
else
element.value = originalText
setData(element, 'ujs:enable-with', null) # clean up cache
element.disabled = false
setData(element, 'ujs:disabled', null)

View File

@ -0,0 +1,34 @@
#= require_tree ../utils
{ stopEverything } = Rails
# Handles "data-method" on links such as:
# <a href="/users/5" data-method="delete" rel="nofollow" data-confirm="Are you sure?">Delete</a>
Rails.handleMethod = (e) ->
link = this
method = link.getAttribute('data-method')
return unless method
href = Rails.href(link)
csrfToken = Rails.csrfToken()
csrfParam = Rails.csrfParam()
form = document.createElement('form')
formContent = "<input name='_method' value='#{method}' type='hidden' />"
if csrfParam? and csrfToken? and not Rails.isCrossDomain(href)
formContent += "<input name='#{csrfParam}' value='#{csrfToken}' type='hidden' />"
# Must trigger submit by click on a button, else "submit" event handler won't work!
# https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/submit
formContent += '<input type="submit" />'
form.method = 'post'
form.action = href
form.target = link.target
form.innerHTML = formContent
form.style.display = 'none'
document.body.appendChild(form)
form.querySelector('[type="submit"]').click()
stopEverything(e)

View File

@ -0,0 +1,100 @@
#= require_tree ../utils
{
matches, getData, setData
fire, stopEverything
ajax, isCrossDomain
blankInputs, serializeElement
} = Rails
# Checks "data-remote" if true to handle the request through a XHR request.
isRemote = (element) ->
value = element.getAttribute('data-remote')
value? and value isnt 'false'
# Submits "remote" forms and links with ajax
Rails.handleRemote = (e) ->
element = this
return true unless isRemote(element)
unless fire(element, 'ajax:before')
fire(element, 'ajax:stopped')
return false
withCredentials = element.getAttribute('data-with-credentials')
dataType = element.getAttribute('data-type') or 'script'
if matches(element, Rails.formSubmitSelector)
# memoized value from clicked submit button
button = getData(element, 'ujs:submit-button')
method = getData(element, 'ujs:submit-button-formmethod') or element.method
url = getData(element, 'ujs:submit-button-formaction') or element.getAttribute('action') or location.href
# strip query string if it's a GET request
url = url.replace(/\?.*$/, '') if method.toUpperCase() is 'GET'
if element.enctype is 'multipart/form-data'
data = new FormData(element)
data.append(button.name, button.value) if button?
else
data = serializeElement(element, button)
setData(element, 'ujs:submit-button', null)
setData(element, 'ujs:submit-button-formmethod', null)
setData(element, 'ujs:submit-button-formaction', null)
else if matches(element, Rails.buttonClickSelector) or matches(element, Rails.inputChangeSelector)
method = element.getAttribute('data-method')
url = element.getAttribute('data-url')
data = serializeElement(element, element.getAttribute('data-params'))
else
method = element.getAttribute('data-method')
url = Rails.href(element)
data = element.getAttribute('data-params')
ajax(
type: method or 'GET'
url: url
data: data
dataType: dataType
# stopping the "ajax:beforeSend" event will cancel the ajax request
beforeSend: (xhr, options) ->
if fire(element, 'ajax:beforeSend', [xhr, options])
fire(element, 'ajax:send', [xhr])
else
fire(element, 'ajax:stopped')
xhr.abort()
success: (args...) -> fire(element, 'ajax:success', args)
error: (args...) -> fire(element, 'ajax:error', args)
complete: (args...) -> fire(element, 'ajax:complete', args)
crossDomain: isCrossDomain(url)
withCredentials: withCredentials? and withCredentials isnt 'false'
)
stopEverything(e)
# Check whether any required fields are empty
# In both ajax mode and normal mode
Rails.validateForm = (e) ->
form = this
return if form.noValidate or getData(form, 'ujs:formnovalidate-button')
# Skip other logic when required values are missing or file upload is present
blankRequiredInputs = blankInputs(form, Rails.requiredInputSelector, false)
if blankRequiredInputs.length > 0 and fire(form, 'ajax:aborted:required', [blankRequiredInputs])
stopEverything(e)
Rails.formSubmitButtonClick = (e) ->
button = this
form = button.form
return unless form
# Register the pressed submit button
setData(form, 'ujs:submit-button', name: button.name, value: button.value) if button.name
# Save attributes from button
setData(form, 'ujs:formnovalidate-button', button.formNoValidate)
setData(form, 'ujs:submit-button-formaction', button.getAttribute('formaction'))
setData(form, 'ujs:submit-button-formmethod', button.getAttribute('formmethod'))
Rails.handleMetaClick = (e) ->
link = this
method = (link.getAttribute('data-method') or 'GET').toUpperCase()
data = link.getAttribute('data-params')
metaClick = e.metaKey or e.ctrlKey
e.stopImmediatePropagation() if metaClick and method is 'GET' and not data

View File

@ -0,0 +1,76 @@
#
# Unobtrusive JavaScript
# https://github.com/rails/rails-ujs
#
# Released under the MIT license
#
#= require ./config
#= require_tree ./utils
#= require_tree ./features
{
fire, delegate
getData, $
refreshCSRFTokens, CSRFProtection
enableElement, disableElement
handleConfirm
handleRemote, validateForm, formSubmitButtonClick, handleMetaClick
handleMethod
} = Rails
# For backward compatibility
if jQuery? and not jQuery.rails
jQuery.rails = Rails
jQuery.ajaxPrefilter (options, originalOptions, xhr) ->
CSRFProtection(xhr) unless options.crossDomain
Rails.start = ->
# Cut down on the number of issues from people inadvertently including jquery_ujs twice
# by detecting and raising an error when it happens.
throw new Error('jquery-ujs has already been loaded!') if window._rails_loaded
# This event works the same as the load event, except that it fires every
# time the page is loaded.
# See https://github.com/rails/jquery-ujs/issues/357
# See https://developer.mozilla.org/en-US/docs/Using_Firefox_1.5_caching
window.addEventListener 'pageshow', ->
$(Rails.formEnableSelector).forEach (el) ->
enableElement(el) if getData(el, 'ujs:disabled')
$(Rails.linkDisableSelector).forEach (el) ->
enableElement(el) if getData(el, 'ujs:disabled')
delegate document, Rails.linkDisableSelector, 'ajax:complete', enableElement
delegate document, Rails.linkDisableSelector, 'ajax:stopped', enableElement
delegate document, Rails.buttonDisableSelector, 'ajax:complete', enableElement
delegate document, Rails.buttonDisableSelector, 'ajax:stopped', enableElement
delegate document, Rails.linkClickSelector, 'click', handleConfirm
delegate document, Rails.linkClickSelector, 'click', handleMetaClick
delegate document, Rails.linkClickSelector, 'click', disableElement
delegate document, Rails.linkClickSelector, 'click', handleRemote
delegate document, Rails.linkClickSelector, 'click', handleMethod
delegate document, Rails.buttonClickSelector, 'click', handleConfirm
delegate document, Rails.buttonClickSelector, 'click', disableElement
delegate document, Rails.buttonClickSelector, 'click', handleRemote
delegate document, Rails.inputChangeSelector, 'change', handleConfirm
delegate document, Rails.inputChangeSelector, 'change', handleRemote
delegate document, Rails.formSubmitSelector, 'submit', handleConfirm
delegate document, Rails.formSubmitSelector, 'submit', validateForm
delegate document, Rails.formSubmitSelector, 'submit', handleRemote
# Normal mode submit
# Slight timeout so that the submit button gets properly serialized
delegate document, Rails.formSubmitSelector, 'submit', (e) -> setTimeout((-> disableElement(e)), 13)
delegate document, Rails.formSubmitSelector, 'ajax:send', disableElement
delegate document, Rails.formSubmitSelector, 'ajax:complete', enableElement
delegate document, Rails.formInputClickSelector, 'click', handleConfirm
delegate document, Rails.formInputClickSelector, 'click', formSubmitButtonClick
document.addEventListener('DOMContentLoaded', refreshCSRFTokens)
window._rails_loaded = true
if window.Rails is Rails and fire(document, 'rails:attachBindings')
Rails.start()

View File

@ -0,0 +1,95 @@
#= require ./csrf
#= require ./event
{ CSRFProtection, fire } = Rails
AcceptHeaders =
'*': '*/*'
text: 'text/plain'
html: 'text/html'
xml: 'application/xml, text/xml'
json: 'application/json, text/javascript'
script: 'text/javascript, application/javascript, application/ecmascript, application/x-ecmascript'
Rails.ajax = (options) ->
options = prepareOptions(options)
xhr = createXHR options, ->
response = processResponse(xhr.response, xhr.getResponseHeader('Content-Type'))
if xhr.status // 100 == 2
options.success?(response, xhr.statusText, xhr)
else
options.error?(response, xhr.statusText, xhr)
options.complete?(xhr, xhr.statusText)
# Call beforeSend hook
options.beforeSend?(xhr, options)
# Send the request
if xhr.readyState is XMLHttpRequest.OPENED
xhr.send(options.data)
else
fire(document, 'ajaxStop') # to be compatible with jQuery.ajax
prepareOptions = (options) ->
options.type = options.type.toUpperCase()
# append data to url if it's a GET request
if options.type is 'GET' and options.data
if options.url.indexOf('?') < 0
options.url += '?' + options.data
else
options.url += '&' + options.data
# Use "*" as default dataType
options.dataType = '*' unless AcceptHeaders[options.dataType]?
options.accept = AcceptHeaders[options.dataType]
options.accept += ', */*; q=0.01' if options.dataType isnt '*'
options
createXHR = (options, done) ->
xhr = new XMLHttpRequest()
# Open and setup xhr
xhr.open(options.type, options.url, true)
xhr.setRequestHeader('Accept', options.accept)
# Set Content-Type only when sending a string
# Sending FormData will automatically set Content-Type to multipart/form-data
if typeof options.data is 'string'
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8')
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest') unless options.crossDomain
# Add X-CSRF-Token
CSRFProtection(xhr)
xhr.withCredentials = !!options.withCredentials
xhr.onreadystatechange = ->
done(xhr) if xhr.readyState is XMLHttpRequest.DONE
xhr
processResponse = (response, type) ->
if typeof response is 'string' and typeof type is 'string'
if type.match(/\bjson\b/)
try response = JSON.parse(response)
else if type.match(/\bjavascript\b/)
script = document.createElement('script')
script.innerHTML = response
document.body.appendChild(script)
else if type.match(/\b(xml|html|svg)\b/)
parser = new DOMParser()
type = type.replace(/;.+/, '') # remove something like ';charset=utf-8'
try response = parser.parseFromString(response, type)
response
# Default way to get an element's href. May be overridden at Rails.href.
Rails.href = (element) -> element.href
# Determines if the request is a cross domain request.
Rails.isCrossDomain = (url) ->
originAnchor = document.createElement('a')
originAnchor.href = location.href
urlAnchor = document.createElement('a')
try
urlAnchor.href = url
# If URL protocol is false or is a string containing a single colon
# *and* host are false, assume it is not a cross-domain request
# (should only be the case for IE7 and IE compatibility mode).
# Otherwise, evaluate protocol and host of the URL against the origin
# protocol and host.
!(((!urlAnchor.protocol || urlAnchor.protocol == ':') && !urlAnchor.host) ||
(originAnchor.protocol + '//' + originAnchor.host == urlAnchor.protocol + '//' + urlAnchor.host))
catch e
# If there is an error parsing the URL, assume it is crossDomain.
true

View File

@ -0,0 +1,25 @@
#= require ./dom
{ $ } = Rails
# Up-to-date Cross-Site Request Forgery token
csrfToken = Rails.csrfToken = ->
meta = document.querySelector('meta[name=csrf-token]')
meta and meta.content
# URL param that must contain the CSRF token
csrfParam = Rails.csrfParam = ->
meta = document.querySelector('meta[name=csrf-param]')
meta and meta.content
# Make sure that every Ajax request sends the CSRF token
Rails.CSRFProtection = (xhr) ->
token = csrfToken()
xhr.setRequestHeader('X-CSRF-Token', token) if token?
# Make sure that all forms have actual up-to-date tokens (cached forms contain old ones)
Rails.refreshCSRFTokens = ->
token = csrfToken()
param = csrfParam()
if token? and param?
$('form input[name="' + param + '"]').forEach (input) -> input.value = token

View File

@ -0,0 +1,28 @@
m = Element.prototype.matches or
Element.prototype.matchesSelector or
Element.prototype.mozMatchesSelector or
Element.prototype.msMatchesSelector or
Element.prototype.oMatchesSelector or
Element.prototype.webkitMatchesSelector
Rails.matches = (element, selector) ->
if selector.exclude?
m.call(element, selector.selector) and not m.call(element, selector.exclude)
else
m.call(element, selector)
# get and set data on a given element using "expando properties"
# See: https://developer.mozilla.org/en-US/docs/Glossary/Expando
expando = '_ujsData'
Rails.getData = (element, key) ->
element[expando]?[key]
Rails.setData = (element, key, value) ->
element[expando] ?= {}
element[expando][key] = value
# a wrapper for document.querySelectorAll
# returns an Array
Rails.$ = (selector) ->
Array.prototype.slice.call(document.querySelectorAll(selector))

View File

@ -0,0 +1,40 @@
#= require ./dom
{ matches } = Rails
# Polyfill for CustomEvent in IE9+
# https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent#Polyfill
CustomEvent = window.CustomEvent
if typeof CustomEvent is 'function'
CustomEvent = (event, params) ->
evt = document.createEvent('CustomEvent')
evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail)
evt
CustomEvent.prototype = window.Event.prototype
# Triggers an custom event on an element and returns false if the event result is false
fire = Rails.fire = (obj, name, data) ->
event = new CustomEvent(
name,
bubbles: true,
cancelable: true,
detail: data,
)
obj.dispatchEvent(event)
!event.defaultPrevented
# Helper function, needed to provide consistent behavior in IE
Rails.stopEverything = (e) ->
fire(e.target, 'ujs:everythingStopped')
e.preventDefault()
e.stopPropagation()
e.stopImmediatePropagation()
Rails.delegate = (element, selector, eventType, handler) ->
element.addEventListener eventType, (e) ->
target = e.target
target = target.parentNode until not (target instanceof Element) or matches(target, selector)
if target instanceof Element and handler.call(target, e) == false
e.preventDefault()
e.stopPropagation()

View File

@ -0,0 +1,61 @@
#= require ./dom
{ matches } = Rails
toArray = (e) -> Array.prototype.slice.call(e)
Rails.serializeElement = (element, additionalParam) ->
inputs = [element]
inputs = toArray(element.elements) if matches(element, 'form')
params = []
inputs.forEach (input) ->
return unless input.name
if matches(input, 'select')
toArray(input.options).forEach (option) ->
params.push(name: input.name, value: option.value) if option.selected
else if input.type isnt 'radio' and input.type isnt 'checkbox' or input.checked
params.push(name: input.name, value: input.value)
params.push(additionalParam) if additionalParam
params.map (param) ->
if param.name?
"#{encodeURIComponent(param.name)}=#{encodeURIComponent(param.value)}"
else
param
.join('&')
# Helper function that returns form elements that match the specified CSS selector
# If form is actually a "form" element this will return associated elements outside the from that have
# the html form attribute set
Rails.formElements = (form, selector) ->
if matches(form, 'form')
toArray(form.elements).filter (el) -> matches(el, selector)
else
toArray(form.querySelectorAll(selector))
# Helper function which checks for blank inputs in a form that match the specified CSS selector
Rails.blankInputs = (form, selector, nonBlank) ->
foundInputs = []
requiredInputs = toArray(form.querySelectorAll(selector or 'input, textarea'))
checkedRadioButtonNames = {}
requiredInputs.forEach (input) ->
if input.type is 'radio'
# Don't count unchecked required radio as blank if other radio with same name is checked,
# regardless of whether same-name radio input has required attribute or not. The spec
# states https://www.w3.org/TR/html5/forms.html#the-required-attribute
radioName = input.name
# Skip if we've already seen the radio with this name.
unless checkedRadioButtonNames[radioName]
# If none checked
if form.querySelectorAll("input[type=radio][name='#{radioName}']:checked").length == 0
radios = form.querySelectorAll("input[type=radio][name='#{radioName}']")
foundInputs = foundInputs.concat(toArray(radios))
# We only need to check each name once.
checkedRadioButtonNames[radioName] = radioName
else
valueToCheck = if input.type is 'checkbox' then input.checked else !!input.value
foundInputs.push(input) if valueToCheck is nonBlank
foundInputs

11
actionview/blade.yml Normal file
View File

@ -0,0 +1,11 @@
load_paths:
- app/assets/javascripts
logical_paths:
- rails-ujs.js
build:
logical_paths:
- rails-ujs.js
path: lib/assets/compiled
clean: true

36
actionview/package.json Normal file
View File

@ -0,0 +1,36 @@
{
"name": "rails-ujs",
"version": "0.0.1",
"description": "Ruby on Rails unobtrusive scripting adapter",
"main": "lib/assets/compiled/rails-ujs.js",
"files": [
"lib/assets/compiled/*.js"
],
"directories": {
"test": "test"
},
"scripts": {
"build": "bundle exec blade build",
"test": "echo \"See the README: https://github.com/rails/rails-ujs#how-to-run-tests\" && exit 1",
"lint": "coffeelint src && eslint test/public/test",
},
"repository": {
"type": "git",
"url": "rails/rails"
},
"contributors": [
"Stephen St. Martin",
"Steve Schwartz",
"Dangyi Liu",
"All contributors"
],
"license": "MIT",
"bugs": {
"url": "https://github.com/rails/rails/issues"
},
"homepage": "http://rubyonrails.org/",
"devDependencies": {
"coffeelint": "^1.15.7",
"eslint": "^2.13.1"
}
}