Merge branch 'main' into tooltip-orient
* main: (161 commits) Save expanded state to localStorage Update seeds to use the workflow stages Disable autocomplete when editing columns Reset column form and disable autocomplete Remove unused job Fix Stream workflow stage BG We need the grid layout when filtering events Fix: proper filtering by collection in the collection perma Fix test Fix test Position expander menu Use parent layout when using with_manual_pagination Reinstate gap to cards list Fix: include collection filter in the collection perma Add some basic caching for the new columns approach Format Fix tests No need to use extra method here Move actions to stream templates Prevent recursive loop now that we have nested frames ...
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
import { nextFrame } from "helpers/timing_helpers";
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = [ "clickable" ]
|
||||
|
||||
async click() {
|
||||
await nextFrame()
|
||||
this.clickableTarget.click()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
|
||||
export default class extends Controller {
|
||||
static classes = [ "collapsed" ]
|
||||
static targets = [ "column", "button" ]
|
||||
|
||||
connect() {
|
||||
this.#restoreColumns()
|
||||
}
|
||||
|
||||
toggle({ target }) {
|
||||
const column = target.closest('[data-collapsible-columns-target="column"]')
|
||||
this.#toggleColumn(column);
|
||||
}
|
||||
|
||||
preventToggle(event) {
|
||||
if (event.detail.attributeName === "class") {
|
||||
event.preventDefault()
|
||||
}
|
||||
}
|
||||
|
||||
#toggleColumn(column) {
|
||||
this.#collapseAllExcept(column)
|
||||
|
||||
if (this.#isCollapsed(column)) {
|
||||
this.#expand(column)
|
||||
} else {
|
||||
this.#collapse(column)
|
||||
}
|
||||
|
||||
console.log("TOGGLE")
|
||||
console.log(localStorage)
|
||||
}
|
||||
|
||||
#collapseAllExcept(clickedColumn) {
|
||||
this.columnTargets.forEach(column => {
|
||||
if (column !== clickedColumn) {
|
||||
this.#collapse(column)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#isCollapsed(column) {
|
||||
return column.classList.contains(this.collapsedClass)
|
||||
}
|
||||
|
||||
#collapse(column) {
|
||||
const key = this.#localStorageKeyFor(column)
|
||||
|
||||
this.#buttonFor(column).setAttribute("aria-expanded", "false")
|
||||
column.classList.add(this.collapsedClass)
|
||||
localStorage.removeItem(key)
|
||||
}
|
||||
|
||||
#expand(column) {
|
||||
const key = this.#localStorageKeyFor(column)
|
||||
|
||||
this.#buttonFor(column).setAttribute("aria-expanded", "true")
|
||||
column.classList.remove(this.collapsedClass)
|
||||
localStorage.setItem(key, true)
|
||||
}
|
||||
|
||||
#buttonFor(column) {
|
||||
return this.buttonTargets.find(button => column.contains(button))
|
||||
}
|
||||
|
||||
#restoreColumns() {
|
||||
this.columnTargets.forEach(column => {
|
||||
const key = this.#localStorageKeyFor(column)
|
||||
if (localStorage.getItem(key)) {
|
||||
this.#expand(column)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#localStorageKeyFor(column) {
|
||||
return `expand-${column.getAttribute("id")}`
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ export default class extends Controller {
|
||||
}
|
||||
|
||||
connect() {
|
||||
this.dialogTarget.setAttribute('aria-hidden', 'true')
|
||||
this.dialogTarget.setAttribute("aria-hidden", "true")
|
||||
}
|
||||
|
||||
open() {
|
||||
@@ -21,7 +21,7 @@ export default class extends Controller {
|
||||
orient(this.dialogTarget)
|
||||
}
|
||||
|
||||
this.dialogTarget.setAttribute('aria-hidden', 'false')
|
||||
this.dialogTarget.setAttribute("aria-hidden", "false")
|
||||
this.dispatch("show")
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ export default class extends Controller {
|
||||
|
||||
close() {
|
||||
this.dialogTarget.close()
|
||||
this.dialogTarget.setAttribute('aria-hidden', 'true')
|
||||
this.dialogTarget.setAttribute("aria-hidden", "true")
|
||||
this.dialogTarget.blur()
|
||||
orient(this.dialogTarget, false)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import { nextFrame } from "helpers/timing_helpers"
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = [ "item", "container" ]
|
||||
static values = { url: String }
|
||||
static classes = [ "draggedItem", "hoverContainer" ]
|
||||
|
||||
// Actions
|
||||
@@ -38,8 +37,10 @@ export default class extends Controller {
|
||||
if (!container || container === this.sourceContainer) { return }
|
||||
|
||||
this.wasDropped = true
|
||||
|
||||
this.#decreaseCounter(this.sourceContainer)
|
||||
const sourceContainer = this.sourceContainer
|
||||
await this.#submitDropRequest(this.dragItem, container)
|
||||
this.#reloadSourceFrame(sourceContainer);
|
||||
}
|
||||
|
||||
dragEnd() {
|
||||
@@ -67,15 +68,30 @@ export default class extends Controller {
|
||||
this.containerTargets.forEach(container => container.classList.remove(this.hoverContainerClass))
|
||||
}
|
||||
|
||||
// Private
|
||||
|
||||
async #submitDropRequest(item, container) {
|
||||
const body = new FormData()
|
||||
const id = item.dataset.id
|
||||
const containerTarget = container.dataset.dropTarget
|
||||
const url = container.dataset.dragAndDropUrl.replaceAll("__id__", id)
|
||||
|
||||
body.append("dropped_item_id", id)
|
||||
body.append("drop_target", containerTarget)
|
||||
return post(this.urlValue, { body, headers: { Accept: "text/vnd.turbo-stream.html" } })
|
||||
return post(url, { body, headers: { Accept: "text/vnd.turbo-stream.html" } })
|
||||
}
|
||||
|
||||
#reloadSourceFrame(sourceContainer) {
|
||||
const frame = sourceContainer.querySelector("[data-drag-and-drop-refresh]")
|
||||
if (frame) frame.reload()
|
||||
}
|
||||
|
||||
#decreaseCounter(sourceContainer) {
|
||||
const counterElement = sourceContainer.querySelector("[data-drag-and-drop-counter]")
|
||||
if (counterElement) {
|
||||
const currentValue = counterElement.textContent.trim()
|
||||
|
||||
if (!/^\d+$/.test(currentValue)) return
|
||||
|
||||
const count = parseInt(currentValue)
|
||||
if (count > 0) {
|
||||
counterElement.textContent = count - 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
|
||||
const INSTRUMENTS = [
|
||||
[
|
||||
"/audio/vibes/B3.mp3",
|
||||
"/audio/vibes/C3.mp3",
|
||||
"/audio/vibes/D4.mp3",
|
||||
"/audio/vibes/E3.mp3",
|
||||
"/audio/vibes/Fsharp4.mp3",
|
||||
"/audio/vibes/G3.mp3"
|
||||
],
|
||||
[
|
||||
"/audio/banjo/B3.mp3",
|
||||
"/audio/banjo/C3.mp3",
|
||||
"/audio/banjo/D4.mp3",
|
||||
"/audio/banjo/E3.mp3",
|
||||
"/audio/banjo/Fsharp4.mp3",
|
||||
"/audio/banjo/G3.mp3"
|
||||
],
|
||||
[
|
||||
"/audio/harpsichord/B3.mp3",
|
||||
"/audio/harpsichord/C3.mp3",
|
||||
"/audio/harpsichord/D4.mp3",
|
||||
"/audio/harpsichord/E3.mp3",
|
||||
"/audio/harpsichord/Fsharp4.mp3",
|
||||
"/audio/harpsichord/G3.mp3"
|
||||
],
|
||||
[
|
||||
"/audio/mandolin/B3.mp3",
|
||||
"/audio/mandolin/C3.mp3",
|
||||
"/audio/mandolin/D4.mp3",
|
||||
"/audio/mandolin/E3.mp3",
|
||||
"/audio/mandolin/Fsharp4.mp3",
|
||||
"/audio/mandolin/G3.mp3"
|
||||
],
|
||||
[
|
||||
"/audio/piano/B3.mp3",
|
||||
"/audio/piano/C3.mp3",
|
||||
"/audio/piano/D4.mp3",
|
||||
"/audio/piano/E3.mp3",
|
||||
"/audio/piano/Fsharp4.mp3",
|
||||
"/audio/piano/G3.mp3"
|
||||
],
|
||||
]
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = [ "container" ]
|
||||
|
||||
connect() {
|
||||
this.instrumentIndex = 0
|
||||
this.preloadedAudioFiles = []
|
||||
document.addEventListener("keydown", this.handleKeyDown.bind(this));
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
document.removeEventListener("keydown", this.handleKeyDown.bind(this));
|
||||
}
|
||||
|
||||
handleKeyDown(event) {
|
||||
if (event.shiftKey) {
|
||||
this.instrumentIndex = this.#getInstrumentIndex(event)
|
||||
|
||||
if (this.instrumentIndex < INSTRUMENTS.length) {
|
||||
this.#preloadAudioFiles(this.instrumentIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dragEnter(event) {
|
||||
event.preventDefault()
|
||||
const container = this.#containerContaining(event.target)
|
||||
|
||||
if (!container) { return }
|
||||
|
||||
if (container !== this.sourceContainer && event.shiftKey) {
|
||||
this.#playSound()
|
||||
}
|
||||
}
|
||||
|
||||
#getInstrumentIndex(event) {
|
||||
const number = Number(event.code.replace("Digit", ""))
|
||||
return isNaN(number) ? 0 : number
|
||||
}
|
||||
|
||||
#preloadAudioFiles(instrumentIndex) {
|
||||
this.preloadedAudioFiles = []
|
||||
const audioFiles = INSTRUMENTS[instrumentIndex];
|
||||
|
||||
if (audioFiles) {
|
||||
this.preloadedAudioFiles = audioFiles.map(file => {
|
||||
const audio = new Audio(file)
|
||||
audio.load()
|
||||
return audio
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#containerContaining(element) {
|
||||
return this.containerTargets.find(container => container.contains(element) || container === element)
|
||||
}
|
||||
|
||||
#playSound() {
|
||||
const randomIndex = Math.floor(Math.random() * this.preloadedAudioFiles.length)
|
||||
const audio = this.preloadedAudioFiles[randomIndex]
|
||||
const audioInstance = new Audio(audio.src)
|
||||
|
||||
audioInstance.play()
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import { post } from "@rails/request.js"
|
||||
export default class extends Controller {
|
||||
static classes = ["filtersSet"]
|
||||
static targets = ["field", "form"]
|
||||
static values = { refreshUrl: String }
|
||||
static values = { refreshUrl: String, noFilteringUrl: String }
|
||||
|
||||
initialize() {
|
||||
this.debouncedToggle = debounce(this.#toggle.bind(this), 50)
|
||||
@@ -15,11 +15,18 @@ export default class extends Controller {
|
||||
this.#toggle()
|
||||
}
|
||||
|
||||
change() {
|
||||
change(event) {
|
||||
this.#toggle()
|
||||
this.#refreshSaveToggleButton()
|
||||
}
|
||||
|
||||
resetIfNoFiltering(event) {
|
||||
if (!this.#hasFiltersSet) {
|
||||
this.#showNoFilteringUrl()
|
||||
event.stopImmediatePropagation()
|
||||
}
|
||||
}
|
||||
|
||||
async fieldTargetConnected(field) {
|
||||
this.debouncedToggle()
|
||||
}
|
||||
@@ -65,4 +72,8 @@ export default class extends Controller {
|
||||
|
||||
return formData
|
||||
}
|
||||
|
||||
#showNoFilteringUrl() {
|
||||
Turbo.visit(this.noFilteringUrlValue, { frame: "cards_container", action: "advance" })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,10 @@ export default class extends Controller {
|
||||
this.submit()
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.element.reset()
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.cancelTarget?.click()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
import { Turbo } from "@hotwired/turbo-rails"
|
||||
|
||||
export default class extends Controller {
|
||||
morphRender({ detail }) {
|
||||
detail.render = function (currentElement, newElement) {
|
||||
Turbo.morphChildren(currentElement, newElement)
|
||||
}
|
||||
}
|
||||
|
||||
morphReload(event) {
|
||||
const newElement = event.detail.newElement
|
||||
if (newElement && newElement.tagName === "TURBO-FRAME" && newElement.matches('[data-controller~="frame"]')) {
|
||||
event.preventDefault()
|
||||
this.element.reload()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
import { createElement } from "helpers/html_helpers"
|
||||
import { delay, nextEvent } from "helpers/timing_helpers"
|
||||
import { keepingScrollPosition } from "helpers/scroll_helpers"
|
||||
import { get } from "@rails/request.js"
|
||||
|
||||
const DELAY_BEFORE_OBSERVING = 400
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = [ "paginationLink" ]
|
||||
static values = {
|
||||
paginateOnIntersection: { type: Boolean, default: false },
|
||||
discardFrame: Boolean,
|
||||
manualActivation: Boolean
|
||||
}
|
||||
|
||||
initialize() {
|
||||
if (!this.manualActivation) {
|
||||
this.activate()
|
||||
}
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this.observer?.disconnect()
|
||||
}
|
||||
|
||||
async activate() {
|
||||
await delay(DELAY_BEFORE_OBSERVING)
|
||||
|
||||
if (this.paginateOnIntersectionValue) {
|
||||
this.observer = new IntersectionObserver(this.#intersect, { rootMargin: "300px", threshold: 1 })
|
||||
}
|
||||
}
|
||||
|
||||
async paginationLinkTargetConnected(linkElement) {
|
||||
if (this.paginateOnIntersectionValue) {
|
||||
await delay(DELAY_BEFORE_OBSERVING)
|
||||
this.observer?.observe(linkElement)
|
||||
}
|
||||
}
|
||||
|
||||
// Actions
|
||||
|
||||
loadPage({ target }) {
|
||||
this.#loadPaginationLink(target)
|
||||
}
|
||||
|
||||
// Private
|
||||
|
||||
#intersect = ([ entry ]) => {
|
||||
if (entry?.isIntersecting && entry.intersectionRatio === 1) {
|
||||
this.#loadPaginationLink(entry.target)
|
||||
}
|
||||
}
|
||||
|
||||
#loadPaginationLink(linkElement) {
|
||||
this.observer?.unobserve(linkElement)
|
||||
|
||||
keepingScrollPosition(this.#closestSiblingTo(linkElement) || linkElement.parentNode, this.#expandPaginationLink(linkElement))
|
||||
}
|
||||
|
||||
#closestSiblingTo(element) {
|
||||
return element.nextElementSibling || element.previousElementSibling
|
||||
}
|
||||
|
||||
async #expandPaginationLink(linkElement) {
|
||||
linkElement.setAttribute("aria-busy", "true")
|
||||
|
||||
if (this.discardFrameValue) {
|
||||
await this.#replacePaginationLinkWithFrameContents(linkElement)
|
||||
} else {
|
||||
await this.#replacePaginationLinkWithFrame(linkElement)
|
||||
}
|
||||
|
||||
linkElement.removeAttribute("aria-busy")
|
||||
}
|
||||
|
||||
async #replacePaginationLinkWithFrameContents(linkElement) {
|
||||
linkElement.outerHTML = await this.#loadHtmlFrom(linkElement)
|
||||
}
|
||||
|
||||
async #loadHtmlFrom(linkElement) {
|
||||
const response = await get(linkElement.href, { responseKind: "html" })
|
||||
const html = await response.text
|
||||
const doc = new DOMParser().parseFromString(html, "text/html")
|
||||
const element = doc.querySelector(`turbo-frame#${linkElement.dataset.frame}`)
|
||||
return element ? element.innerHTML.trim() : ""
|
||||
}
|
||||
|
||||
#replacePaginationLinkWithFrame(linkElement) {
|
||||
const turboFrame = this.#buildTurboFrameFor(linkElement)
|
||||
this.#insertTurboFrameAtPosition(linkElement, turboFrame)
|
||||
linkElement.remove()
|
||||
}
|
||||
|
||||
#buildTurboFrameFor(linkElement) {
|
||||
const turboFrame = createElement("turbo-frame", {
|
||||
id: linkElement.dataset.frame,
|
||||
src: linkElement.href,
|
||||
refresh: "morph",
|
||||
target: "_top"
|
||||
})
|
||||
|
||||
this.#keepScrollPositionOnFrameRender(turboFrame, linkElement)
|
||||
|
||||
return turboFrame
|
||||
}
|
||||
|
||||
async #keepScrollPositionOnFrameRender(turboFrame, linkElement) {
|
||||
await nextEvent(turboFrame, "turbo:before-frame-render")
|
||||
|
||||
keepingScrollPosition(linkElement, nextEvent(turboFrame, "turbo:frame-render"))
|
||||
}
|
||||
|
||||
#insertTurboFrameAtPosition(linkElement, turboFrame) {
|
||||
const container = linkElement.parentNode.parentNode
|
||||
|
||||
if (linkElement.parentNode.firstElementChild === linkElement) {
|
||||
container.prepend(turboFrame)
|
||||
} else {
|
||||
container.append(turboFrame)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user