forked from fixin.me/fixin.me
f1578fad75
Use CSS `visibility` instead of `display` for #show/#hide. Access data attributes through `dataset` property.
200 lines
6.4 KiB
JavaScript
200 lines
6.4 KiB
JavaScript
// Configure your import map in config/importmap.rb. Read more:
|
|
// https://github.com/rails/importmap-rails
|
|
import "@hotwired/turbo-rails"
|
|
import "./turbo_actions.js"
|
|
// TODO: access `data-` attributes through
|
|
// `keyname in`/`dataset.keyname`/`delete dataset.keyname`
|
|
|
|
|
|
// Show page if hidden for testing purposes.
|
|
function showPage(event) {
|
|
document.documentElement.style.visibility="visible"
|
|
}
|
|
document.addEventListener('turbo:load', showPage)
|
|
|
|
|
|
// <details> event handlers.
|
|
function detailsChange(event) {
|
|
var target = event.currentTarget
|
|
var count = target.querySelectorAll('input:checked:not([disabled])').length
|
|
var span = target.querySelector('summary > span')
|
|
var button = target.querySelector('button')
|
|
if (count > 0) {
|
|
span.textContent = count + ' selected';
|
|
Turbo.StreamElement.prototype.enableElement(button)
|
|
} else {
|
|
span.textContent = span.getAttribute('data-prompt')
|
|
Turbo.StreamElement.prototype.disableElement(button)
|
|
}
|
|
}
|
|
window.detailsChange = detailsChange
|
|
|
|
// Close open <details> when focus lost.
|
|
function detailsClose(event) {
|
|
if (!event.relatedTarget ||
|
|
event.relatedTarget.closest("details") != event.currentTarget) {
|
|
event.currentTarget.removeAttribute("open")
|
|
}
|
|
}
|
|
window.detailsClose = detailsClose
|
|
|
|
window.detailsObserver = new MutationObserver((mutations) => {
|
|
mutations[0].target.dispatchEvent(new Event('change', {bubbles: true}))
|
|
});
|
|
|
|
|
|
// Validation of multiple-submit forms with different field requirements.
|
|
function formValidate(event) {
|
|
var id = event.submitter.getAttribute("data-validate")
|
|
if (!id) return;
|
|
|
|
var input = document.getElementById(id)
|
|
if (!input.checkValidity()) {
|
|
input.reportValidity()
|
|
event.preventDefault()
|
|
}
|
|
}
|
|
window.formValidate = formValidate
|
|
|
|
// Pattern for number precision verification. Should accept all number formats
|
|
// accepted by Kernel#Float. Lookahead checks the number of significant digits
|
|
// in mantissa, following expression makes sure there is only one dot.
|
|
const numberRegExp = /^\-?(?=(?:\.?0)*(?:\.?\d){1,15}?(?:\.?0)*\.?(?:[eE]|$))(?<mantissa>\d*\.?\d+|\d+\.)(?:[eE]\-?\d{1,3})?$/
|
|
|
|
// Validation of input[type=number] value precision.
|
|
function numberValidate(event) {
|
|
var input = event.target
|
|
|
|
input.setCustomValidity("")
|
|
if (!input.validity.valid) return
|
|
|
|
var match = numberRegExp.exec(input.value)
|
|
if (!match ||
|
|
(parseFloat(input.value) == 0.0 && match.groups['mantissa'].match(/[1-9]/))) {
|
|
// TODO: move message to locales
|
|
input.setCustomValidity("Please select a value within range of double precision floating-point format.")
|
|
}
|
|
}
|
|
window.numberValidate = numberValidate
|
|
|
|
|
|
// Keyboard event processing handlers.
|
|
function formProcessKey(event) {
|
|
switch (event.key) {
|
|
case "Escape":
|
|
event.currentTarget.querySelector("a[name=cancel]").click()
|
|
break
|
|
// TODO: allow new line inside <textarea> (submit with ctrl-enter?)
|
|
case "Enter":
|
|
event.currentTarget.querySelector("button[name=button]").click()
|
|
event.preventDefault()
|
|
break
|
|
}
|
|
}
|
|
window.formProcessKey = formProcessKey
|
|
|
|
function detailsProcessKey(event) {
|
|
// TODO: up/down arrows to move focus to prev/next line
|
|
switch (event.key) {
|
|
case "Escape":
|
|
if (event.currentTarget.hasAttribute("open")) {
|
|
event.currentTarget.removeAttribute("open")
|
|
event.stopPropagation()
|
|
}
|
|
break
|
|
case "Enter":
|
|
var button = event.currentTarget.querySelector("button:not([disabled])")
|
|
if (button) {
|
|
button.click()
|
|
// Autofocus won't be respected unless target is blurred.
|
|
event.target.blur()
|
|
event.preventDefault()
|
|
event.stopPropagation()
|
|
}
|
|
break
|
|
}
|
|
}
|
|
window.detailsProcessKey = detailsProcessKey;
|
|
|
|
|
|
// Items table drag and drop support.
|
|
var lastEnterTime
|
|
function dragStart(event) {
|
|
lastEnterTime = event.timeStamp
|
|
var row = event.currentTarget
|
|
row.closest("table").querySelectorAll("thead tr").forEach((tr) => {
|
|
tr.toggleAttribute("hidden")
|
|
})
|
|
event.dataTransfer.setData("text/plain", row.getAttribute("data-drag-path"))
|
|
var rowRectangle = row.getBoundingClientRect()
|
|
event.dataTransfer.setDragImage(row, event.x - rowRectangle.left,
|
|
event.y - rowRectangle.top)
|
|
event.dataTransfer.dropEffect = "move"
|
|
}
|
|
window.dragStart = dragStart
|
|
|
|
/*
|
|
* Drag tracking assumptions (based on FF 122.0 experience):
|
|
* * Enter/Leave events at the same timeStamp may not be logically ordered
|
|
* (e.g. E -> E -> L, not E -> L -> E),
|
|
* * not every Enter event has corresponding Leave event, especially during
|
|
* rapid pointer moves.
|
|
* NOTE: sometimes Leave is not emitted when pointer goes fast over table
|
|
* and outside. This should probably be fixed in browser, than patched here.
|
|
* TODO: replace Leave table with Enter document?
|
|
* https://www.quirksmode.org/js/events_order.html#link4
|
|
*/
|
|
function dragEnter(event) {
|
|
//console.log(event.timeStamp + " " + event.type + ": " + event.currentTarget.id)
|
|
dragLeave(event)
|
|
lastEnterTime = event.timeStamp
|
|
const id = event.currentTarget.getAttribute("data-drop-id")
|
|
document.getElementById(id).classList.add("dropzone")
|
|
}
|
|
window.dragEnter = dragEnter
|
|
|
|
function dragOver(event) {
|
|
event.preventDefault()
|
|
}
|
|
window.dragOver = dragOver
|
|
|
|
function dragLeave(event) {
|
|
//console.log(event.timeStamp + " " + event.type + ": " + event.currentTarget.id)
|
|
// Leave has been accounted for by Enter at the same timestamp, processed earlier.
|
|
if (event.timeStamp <= lastEnterTime) return
|
|
event.currentTarget.closest("table").querySelectorAll(".dropzone").forEach((tr) => {
|
|
tr.classList.remove("dropzone")
|
|
})
|
|
}
|
|
window.dragLeave = dragLeave
|
|
|
|
function dragEnd(event) {
|
|
dragLeave(event)
|
|
event.currentTarget.closest("table").querySelectorAll("thead tr").forEach((tr) => {
|
|
tr.toggleAttribute("hidden")
|
|
})
|
|
}
|
|
window.dragEnd = dragEnd
|
|
|
|
function drop(event) {
|
|
event.preventDefault()
|
|
|
|
var params = new URLSearchParams()
|
|
var id_param = event.currentTarget.getAttribute("data-drop-id-param")
|
|
var id = event.currentTarget.getAttribute("data-drop-id").split("_").pop()
|
|
params.append(id_param, id)
|
|
|
|
fetch(event.dataTransfer.getData("text/plain"), {
|
|
body: params,
|
|
headers: {
|
|
"Accept": "text/vnd.turbo-stream.html",
|
|
"X-CSRF-Token": document.head.querySelector("meta[name=csrf-token]").content,
|
|
"X-Requested-With": "XMLHttpRequest"
|
|
},
|
|
method: "POST"
|
|
})
|
|
.then(response => response.text())
|
|
.then(html => Turbo.renderStreamMessage(html))
|
|
}
|
|
window.drop = drop
|