From ad3a47759e67a411f3534309cdd704f12f6930a7 Mon Sep 17 00:00:00 2001 From: Guillermo Iguaran Date: Fri, 25 Nov 2016 10:27:07 -0500 Subject: Add rails-ujs to Action View --- actionview/app/assets/javascripts/config.coffee | 37 ++++++++ .../app/assets/javascripts/features/confirm.coffee | 26 ++++++ .../app/assets/javascripts/features/disable.coffee | 78 ++++++++++++++++ .../app/assets/javascripts/features/method.coffee | 34 +++++++ .../app/assets/javascripts/features/remote.coffee | 100 +++++++++++++++++++++ actionview/app/assets/javascripts/rails-ujs.coffee | 76 ++++++++++++++++ .../app/assets/javascripts/utils/ajax.coffee | 95 ++++++++++++++++++++ .../app/assets/javascripts/utils/csrf.coffee | 25 ++++++ actionview/app/assets/javascripts/utils/dom.coffee | 28 ++++++ .../app/assets/javascripts/utils/event.coffee | 40 +++++++++ .../app/assets/javascripts/utils/form.coffee | 61 +++++++++++++ 11 files changed, 600 insertions(+) create mode 100644 actionview/app/assets/javascripts/config.coffee create mode 100644 actionview/app/assets/javascripts/features/confirm.coffee create mode 100644 actionview/app/assets/javascripts/features/disable.coffee create mode 100644 actionview/app/assets/javascripts/features/method.coffee create mode 100644 actionview/app/assets/javascripts/features/remote.coffee create mode 100644 actionview/app/assets/javascripts/rails-ujs.coffee create mode 100644 actionview/app/assets/javascripts/utils/ajax.coffee create mode 100644 actionview/app/assets/javascripts/utils/csrf.coffee create mode 100644 actionview/app/assets/javascripts/utils/dom.coffee create mode 100644 actionview/app/assets/javascripts/utils/event.coffee create mode 100644 actionview/app/assets/javascripts/utils/form.coffee (limited to 'actionview/app') diff --git a/actionview/app/assets/javascripts/config.coffee b/actionview/app/assets/javascripts/config.coffee new file mode 100644 index 0000000000..3d4706b0e1 --- /dev/null +++ b/actionview/app/assets/javascripts/config.coffee @@ -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]' diff --git a/actionview/app/assets/javascripts/features/confirm.coffee b/actionview/app/assets/javascripts/features/confirm.coffee new file mode 100644 index 0000000000..72b5aaa218 --- /dev/null +++ b/actionview/app/assets/javascripts/features/confirm.coffee @@ -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 diff --git a/actionview/app/assets/javascripts/features/disable.coffee b/actionview/app/assets/javascripts/features/disable.coffee new file mode 100644 index 0000000000..e8cce7da40 --- /dev/null +++ b/actionview/app/assets/javascripts/features/disable.coffee @@ -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) diff --git a/actionview/app/assets/javascripts/features/method.coffee b/actionview/app/assets/javascripts/features/method.coffee new file mode 100644 index 0000000000..d04d9414dd --- /dev/null +++ b/actionview/app/assets/javascripts/features/method.coffee @@ -0,0 +1,34 @@ +#= require_tree ../utils + +{ stopEverything } = Rails + +# Handles "data-method" on links such as: +# Delete +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 = "" + + if csrfParam? and csrfToken? and not Rails.isCrossDomain(href) + formContent += "" + + # 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 += '' + + 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) diff --git a/actionview/app/assets/javascripts/features/remote.coffee b/actionview/app/assets/javascripts/features/remote.coffee new file mode 100644 index 0000000000..30a5dc21fa --- /dev/null +++ b/actionview/app/assets/javascripts/features/remote.coffee @@ -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 diff --git a/actionview/app/assets/javascripts/rails-ujs.coffee b/actionview/app/assets/javascripts/rails-ujs.coffee new file mode 100644 index 0000000000..f96d2eb6fd --- /dev/null +++ b/actionview/app/assets/javascripts/rails-ujs.coffee @@ -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() diff --git a/actionview/app/assets/javascripts/utils/ajax.coffee b/actionview/app/assets/javascripts/utils/ajax.coffee new file mode 100644 index 0000000000..9af515beda --- /dev/null +++ b/actionview/app/assets/javascripts/utils/ajax.coffee @@ -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 diff --git a/actionview/app/assets/javascripts/utils/csrf.coffee b/actionview/app/assets/javascripts/utils/csrf.coffee new file mode 100644 index 0000000000..4eb5ebb414 --- /dev/null +++ b/actionview/app/assets/javascripts/utils/csrf.coffee @@ -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 diff --git a/actionview/app/assets/javascripts/utils/dom.coffee b/actionview/app/assets/javascripts/utils/dom.coffee new file mode 100644 index 0000000000..6bef618147 --- /dev/null +++ b/actionview/app/assets/javascripts/utils/dom.coffee @@ -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)) diff --git a/actionview/app/assets/javascripts/utils/event.coffee b/actionview/app/assets/javascripts/utils/event.coffee new file mode 100644 index 0000000000..049b2a3ecd --- /dev/null +++ b/actionview/app/assets/javascripts/utils/event.coffee @@ -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() diff --git a/actionview/app/assets/javascripts/utils/form.coffee b/actionview/app/assets/javascripts/utils/form.coffee new file mode 100644 index 0000000000..251113deda --- /dev/null +++ b/actionview/app/assets/javascripts/utils/form.coffee @@ -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 -- cgit v1.2.3