forked from fixin.me/fixin.me
Update and move turbo_stream actions to separate file
Use CSS `visibility` instead of `display` for #show/#hide. Access data attributes through `dataset` property.
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
// Configure your import map in config/importmap.rb. Read more:
|
// Configure your import map in config/importmap.rb. Read more:
|
||||||
// https://github.com/rails/importmap-rails
|
// https://github.com/rails/importmap-rails
|
||||||
import "@hotwired/turbo-rails"
|
import "@hotwired/turbo-rails"
|
||||||
|
import "./turbo_actions.js"
|
||||||
// TODO: access `data-` attributes through
|
// TODO: access `data-` attributes through
|
||||||
// `keyname in`/`dataset.keyname`/`delete dataset.keyname`
|
// `keyname in`/`dataset.keyname`/`delete dataset.keyname`
|
||||||
|
|
||||||
@@ -77,111 +78,13 @@ function numberValidate(event) {
|
|||||||
window.numberValidate = numberValidate
|
window.numberValidate = numberValidate
|
||||||
|
|
||||||
|
|
||||||
// Turbo stream actions.
|
|
||||||
const FORM_CONTROLS = ["BUTTON", "FIELDSET", "INPUT", "OPTGROUP", "OPTION", "SELECT",
|
|
||||||
"TEXTAREA"]
|
|
||||||
Turbo.StreamElement.prototype.disableElement = function(element) {
|
|
||||||
if (FORM_CONTROLS.includes(element.tagName)) {
|
|
||||||
element.setAttribute("disabled", "disabled")
|
|
||||||
} else {
|
|
||||||
element.setAttribute("aria-disabled", "true")
|
|
||||||
// NOTE: tabindex should not be disabled?
|
|
||||||
element.setAttribute("tabindex", "-1")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Turbo.StreamActions.disable = function() {
|
|
||||||
this.targetElements.forEach((e) => { this.disableElement(e) })
|
|
||||||
}
|
|
||||||
|
|
||||||
Turbo.StreamElement.prototype.enableElement = function(element) {
|
|
||||||
if (FORM_CONTROLS.includes(element.tagName)) {
|
|
||||||
element.removeAttribute("disabled")
|
|
||||||
} else {
|
|
||||||
element.removeAttribute("aria-disabled")
|
|
||||||
// Assume 'tabindex' is not used explicitly, so removing it is safe.
|
|
||||||
element.removeAttribute("tabindex")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Turbo.StreamActions.enable = function() {
|
|
||||||
this.targetElements.forEach((e) => { this.enableElement(e) })
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: change to visibility = collapse to avoid width change?
|
|
||||||
Turbo.StreamActions.hide = function() {
|
|
||||||
this.targetElements.forEach((e) => { e.style.display = "none" })
|
|
||||||
}
|
|
||||||
|
|
||||||
Turbo.StreamActions.show = function() {
|
|
||||||
this.targetElements.forEach((e) => { e.style.removeProperty("display") })
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Turbo.StreamActions.collapse = function() {
|
|
||||||
this.targetElements.forEach((e) => { e.style.visibility = "collapse" })
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
Turbo.StreamActions.close_form = function() {
|
|
||||||
this.targetElements.forEach((e) => {
|
|
||||||
// Move focus if there's no focus or focus inside form being closed.
|
|
||||||
const focused = document.activeElement
|
|
||||||
if (!focused || (focused == document.body) || e.contains(focused)) {
|
|
||||||
let nextForm = e.parentElement.querySelector(`#${e.id} ~ tr:has([autofocus])`)
|
|
||||||
nextForm ??= e.parentElement.querySelector("tr:has([autofocus])")
|
|
||||||
nextForm?.querySelector("[autofocus]").focus()
|
|
||||||
}
|
|
||||||
document.getElementById(e.getAttribute("data-form")).remove()
|
|
||||||
if (e.hasAttribute("data-link")) {
|
|
||||||
this.enableElement(document.getElementById(e.getAttribute("data-link")))
|
|
||||||
}
|
|
||||||
if (e.hasAttribute("data-hidden-row")) {
|
|
||||||
document.getElementById(e.getAttribute("data-hidden-row")).removeAttribute("style")
|
|
||||||
}
|
|
||||||
e.remove()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
Turbo.StreamActions.unselect = function() {
|
|
||||||
this.targetElements.forEach((e) => {
|
|
||||||
e.checked = false
|
|
||||||
this.enableElement(e)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
Turbo.StreamActions.validate = function() {
|
|
||||||
this.targetElements.forEach((form) => {
|
|
||||||
this.templateContent.querySelectorAll("span").forEach((span) => {
|
|
||||||
var control = document.querySelector(
|
|
||||||
`[form="${form.id}"][name="${span.dataset.name}"]`
|
|
||||||
)
|
|
||||||
control.setCustomValidity(span.textContent)
|
|
||||||
// Add event listener in capture phase to clear custom validity before
|
|
||||||
// `[oninput]` or any other bubble phase handler is invoked.
|
|
||||||
// https://www.quirksmode.org/js/events_order.html
|
|
||||||
// TODO: keep invalidating current value whenever user re-enters it. Store
|
|
||||||
// invalid value and validation message in `[data-invalid-message/value]`
|
|
||||||
// and check input value on "input" event (below) (using `[pattern]` may not
|
|
||||||
// be suitable due to inability to customize validtion message; also
|
|
||||||
// pattern may be used for other, conflicting purposes).
|
|
||||||
// After user executes any other action (while form is open), remove `[data-]`
|
|
||||||
// attributes and custom validation, as value may no longer be invalid.
|
|
||||||
control.addEventListener(
|
|
||||||
"input",
|
|
||||||
function(event) { event.target.setCustomValidity("") },
|
|
||||||
{capture: true, once: true}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
form.reportValidity()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Keyboard event processing handlers.
|
// Keyboard event processing handlers.
|
||||||
function formProcessKey(event) {
|
function formProcessKey(event) {
|
||||||
switch (event.key) {
|
switch (event.key) {
|
||||||
case "Escape":
|
case "Escape":
|
||||||
event.currentTarget.querySelector("a[name=cancel]").click()
|
event.currentTarget.querySelector("a[name=cancel]").click()
|
||||||
break
|
break
|
||||||
|
// TODO: allow new line inside <textarea> (submit with ctrl-enter?)
|
||||||
case "Enter":
|
case "Enter":
|
||||||
event.currentTarget.querySelector("button[name=button]").click()
|
event.currentTarget.querySelector("button[name=button]").click()
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
// Turbo Streams actions.
|
||||||
|
|
||||||
|
const FORM_CONTROLS = ["BUTTON", "FIELDSET", "INPUT", "OPTGROUP", "OPTION", "SELECT",
|
||||||
|
"TEXTAREA"]
|
||||||
|
|
||||||
|
Object.assign(Turbo.StreamElement.prototype, {
|
||||||
|
disableElement(element) {
|
||||||
|
if (FORM_CONTROLS.includes(element.tagName)) {
|
||||||
|
element.setAttribute("disabled", "disabled")
|
||||||
|
} else {
|
||||||
|
element.setAttribute("aria-disabled", "true")
|
||||||
|
// NOTE: tabindex should not be disabled?
|
||||||
|
element.setAttribute("tabindex", "-1")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
enableElement(element) {
|
||||||
|
if (FORM_CONTROLS.includes(element.tagName)) {
|
||||||
|
element.removeAttribute("disabled")
|
||||||
|
} else {
|
||||||
|
element.removeAttribute("aria-disabled")
|
||||||
|
// Assume 'tabindex' is not used explicitly, so removing it is safe.
|
||||||
|
element.removeAttribute("tabindex")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
showElement(element) { element.style.visibility = "visible" }
|
||||||
|
})
|
||||||
|
|
||||||
|
Object.assign(Turbo.StreamActions, {
|
||||||
|
disable() {
|
||||||
|
this.targetElements.forEach((e) => { this.disableElement(e) })
|
||||||
|
},
|
||||||
|
|
||||||
|
enable() {
|
||||||
|
this.targetElements.forEach((e) => { this.enableElement(e) })
|
||||||
|
},
|
||||||
|
|
||||||
|
hide() {
|
||||||
|
this.targetElements.forEach((e) => { e.style.visibility = "collapse" })
|
||||||
|
},
|
||||||
|
|
||||||
|
show() {
|
||||||
|
this.targetElements.forEach((e) => { this.showElement(e) })
|
||||||
|
},
|
||||||
|
|
||||||
|
unselect() {
|
||||||
|
this.targetElements.forEach((e) => { e.checked = false; this.enableElement(e) })
|
||||||
|
},
|
||||||
|
|
||||||
|
validate_form() {
|
||||||
|
this.targetElements.forEach((form) => {
|
||||||
|
this.templateContent.querySelectorAll("span").forEach((span) => {
|
||||||
|
var control = document.querySelector(
|
||||||
|
`[form="${form.id}"][name="${span.dataset.name}"]`
|
||||||
|
)
|
||||||
|
control.setCustomValidity(span.textContent)
|
||||||
|
// Add event listener in capture phase to clear custom validity before
|
||||||
|
// `[oninput]` or any other bubble phase handler is invoked.
|
||||||
|
// https://www.quirksmode.org/js/events_order.html
|
||||||
|
// TODO: keep invalidating current value whenever user re-enters it. Store
|
||||||
|
// invalid value and validation message in `[data-invalid-message/value]`
|
||||||
|
// and check input value on "input" event (below) (using `[pattern]` may not
|
||||||
|
// be suitable due to inability to customize validtion message; also
|
||||||
|
// pattern may be used for other, conflicting purposes).
|
||||||
|
// After user executes any other action (while form is open), remove `[data-]`
|
||||||
|
// attributes and custom validation, as value may no longer be invalid.
|
||||||
|
control.addEventListener(
|
||||||
|
"input",
|
||||||
|
function(event) { event.target.setCustomValidity("") },
|
||||||
|
{capture: true, once: true}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
form.reportValidity()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
close_form() {
|
||||||
|
this.targetElements.forEach((e) => {
|
||||||
|
// Move focus if there's no focus or focus inside form being closed.
|
||||||
|
const focused = document.activeElement
|
||||||
|
if (!focused || (focused == document.body) || e.contains(focused)) {
|
||||||
|
let nextForm = e.parentElement.querySelector(`#${e.id} ~ tr:has([autofocus])`)
|
||||||
|
nextForm ??= e.parentElement.querySelector("tr:has([autofocus])")
|
||||||
|
nextForm?.querySelector("[autofocus]").focus()
|
||||||
|
}
|
||||||
|
document.getElementById(e.dataset.form).remove()
|
||||||
|
if ('link' in e.dataset) {
|
||||||
|
this.enableElement(document.getElementById(e.dataset.link))
|
||||||
|
}
|
||||||
|
if ('hiddenRow' in e.dataset) {
|
||||||
|
this.showElement(document.getElementById(e.dataset.hiddenRow))
|
||||||
|
}
|
||||||
|
e.remove()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
<%= turbo_stream.validate dom_target(*@target, :form) do %>
|
<%= turbo_stream.validate_form dom_target(*@target, :form) do %>
|
||||||
<% @record.errors.attribute_names.map do |attr| %>
|
<% @record.errors.attribute_names.map do |attr| %>
|
||||||
<%= tag.span data: {name: field_name(dom_class(@record), attr)} do %>
|
<%= tag.span data: {name: field_name(dom_class(@record), attr)} do %>
|
||||||
<%- @record.errors.full_messages_for(attr).map { |m| m + '.' }.join(' ') %>
|
<%- @record.errors.full_messages_for(attr).map { |m| m + '.' }.join(' ') %>
|
||||||
|
|||||||
@@ -23,19 +23,15 @@ ActiveSupport.on_load :turbo_streams_tag_builder do
|
|||||||
action :show, target, allow_inferred_rendering: false
|
action :show, target, allow_inferred_rendering: false
|
||||||
end
|
end
|
||||||
|
|
||||||
#def collapse(target)
|
|
||||||
# action :collapse, target, allow_inferred_rendering: false
|
|
||||||
#end
|
|
||||||
|
|
||||||
def close_form(target)
|
|
||||||
action :close_form, target, allow_inferred_rendering: false
|
|
||||||
end
|
|
||||||
|
|
||||||
def unselect(target)
|
def unselect(target)
|
||||||
action :unselect, target, allow_inferred_rendering: false
|
action :unselect, target, allow_inferred_rendering: false
|
||||||
end
|
end
|
||||||
|
|
||||||
def validate(target, content = nil, **rendering, &block)
|
def validate_form(target, content = nil, **rendering, &block)
|
||||||
action :validate, target, content, **rendering, &block
|
action :validate_form, target, content, **rendering, &block
|
||||||
|
end
|
||||||
|
|
||||||
|
def close_form(target)
|
||||||
|
action :close_form, target, allow_inferred_rendering: false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user