Add hotkeys for triaging cards in columns
Hover to select items
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
import { post } from "@rails/request.js"
|
||||
|
||||
export default class extends Controller {
|
||||
static outlets = [ "navigable-list" ]
|
||||
|
||||
connect() {
|
||||
this.morphCompletePromise = null
|
||||
this.morphCompleteResolver = null
|
||||
}
|
||||
|
||||
handleKeydown(event) {
|
||||
if (this.#shouldIgnore(event)) return
|
||||
|
||||
const handler = this.#keyHandlers[event.key.toLowerCase()]
|
||||
if (handler) {
|
||||
handler.call(this, event)
|
||||
}
|
||||
}
|
||||
|
||||
// Called when turbo:morph completes - resolves our waiting promise
|
||||
handleMorphComplete() {
|
||||
if (this.morphCompleteResolver) {
|
||||
this.morphCompleteResolver()
|
||||
this.morphCompleteResolver = null
|
||||
this.morphCompletePromise = null
|
||||
}
|
||||
}
|
||||
|
||||
// Private
|
||||
|
||||
#shouldIgnore(event) {
|
||||
const target = event.target
|
||||
return target.tagName === "INPUT" ||
|
||||
target.tagName === "TEXTAREA" ||
|
||||
target.isContentEditable ||
|
||||
target.closest("input, textarea, [contenteditable], lexxy-editor")
|
||||
}
|
||||
|
||||
get #selectedCard() {
|
||||
// Find the navigable-list that currently has focus
|
||||
const focusedList = this.navigableListOutlets.find(list => list.hasFocus)
|
||||
if (!focusedList) return null
|
||||
|
||||
const currentItem = focusedList.currentItem
|
||||
if (currentItem?.classList.contains("card")) {
|
||||
return { card: currentItem, controller: focusedList }
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
async #postponeCard(event) {
|
||||
const selection = this.#selectedCard
|
||||
if (!selection) return
|
||||
|
||||
const url = selection.card.dataset.cardNotNowUrl
|
||||
if (url) {
|
||||
event.preventDefault()
|
||||
await this.#performCardAction(url, selection)
|
||||
}
|
||||
}
|
||||
|
||||
async #closeCard(event) {
|
||||
const selection = this.#selectedCard
|
||||
if (!selection) return
|
||||
|
||||
const url = selection.card.dataset.cardClosureUrl
|
||||
if (url) {
|
||||
event.preventDefault()
|
||||
await this.#performCardAction(url, selection)
|
||||
}
|
||||
}
|
||||
|
||||
async #assignToMe(event) {
|
||||
const selection = this.#selectedCard
|
||||
if (!selection) return
|
||||
|
||||
const url = selection.card.dataset.cardAssignToMeUrl
|
||||
if (url) {
|
||||
event.preventDefault()
|
||||
await post(url, { responseKind: "turbo-stream" })
|
||||
}
|
||||
}
|
||||
|
||||
async #performCardAction(url, selection) {
|
||||
const { controller } = selection
|
||||
const visibleItems = controller.visibleItems
|
||||
const currentIndex = visibleItems.indexOf(selection.card)
|
||||
const wasLastItem = currentIndex === visibleItems.length - 1
|
||||
|
||||
// Set up promise to wait for morph completion
|
||||
this.morphCompletePromise = new Promise(resolve => {
|
||||
this.morphCompleteResolver = resolve
|
||||
})
|
||||
|
||||
await post(url, { responseKind: "turbo-stream" })
|
||||
|
||||
// Wait for Turbo Stream morph to complete
|
||||
await Promise.race([
|
||||
this.morphCompletePromise,
|
||||
new Promise(resolve => setTimeout(resolve, 200)) // Fallback timeout
|
||||
])
|
||||
|
||||
// Select the next card (or previous if it was the last)
|
||||
const newVisibleItems = controller.visibleItems
|
||||
if (newVisibleItems.length === 0) {
|
||||
controller.clearSelection()
|
||||
return
|
||||
}
|
||||
|
||||
if (wasLastItem) {
|
||||
controller.selectLast()
|
||||
} else {
|
||||
const nextIndex = Math.min(currentIndex, newVisibleItems.length - 1)
|
||||
if (newVisibleItems[nextIndex]) {
|
||||
await controller.selectItem(newVisibleItems[nextIndex])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#keyHandlers = {
|
||||
"["(event) { this.#postponeCard(event) },
|
||||
"]"(event) { this.#closeCard(event) },
|
||||
m(event) { this.#assignToMe(event) }
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,10 @@ export default class extends Controller {
|
||||
this.selectItem(target, true)
|
||||
}
|
||||
|
||||
hoverSelect({ currentTarget }) {
|
||||
this.selectItem(currentTarget)
|
||||
}
|
||||
|
||||
selectCurrentOrReset(event) {
|
||||
if (this.currentItem) {
|
||||
this.#setCurrentFrom(this.currentItem)
|
||||
@@ -221,6 +225,20 @@ export default class extends Controller {
|
||||
})
|
||||
}
|
||||
|
||||
// Public accessors for card_hotkeys_controller outlet
|
||||
get visibleItems() {
|
||||
return this.#visibleItems
|
||||
}
|
||||
|
||||
clearSelection() {
|
||||
this.#clearSelection()
|
||||
this.currentItem = null
|
||||
}
|
||||
|
||||
get hasFocus() {
|
||||
return this.element.contains(document.activeElement)
|
||||
}
|
||||
|
||||
#keyHandlers = {
|
||||
ArrowDown(event) {
|
||||
if (this.supportsVerticalNavigationValue) {
|
||||
@@ -253,6 +271,6 @@ export default class extends Controller {
|
||||
} else {
|
||||
this.#clickCurrentItem(event)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user