Merge branch 'main' into mobile-app/prepare-webviews
* main: (63 commits) Ignore hotkeys with modifiers Fix 1Password account ID (was user UUID) (#2278) Switch 1Password account to 37signals.1password.com (#2276) Remove CSS testing comments Max card count equals geared pagination size Block IPv4-compatible IPv6 addresses in SSRF protection (#2273) Only enable transitions on user interaction Don't update counter if value hasn't changed Add padding to upgrade message on larger screens Add test coverage for autolinking multiple URLs Add "noopener" to autolinks' rel attribute Avoid string manipulation when autolinking. Only bump z-index when nav is open Move nav and related elements above footer Delete Dockerfile.dev fix: use the right gh-cli arch package (#2232) Bump actions/attest-build-provenance from 3.0.0 to 3.1.0 (#2257) Bump docker/setup-buildx-action from 3.11.1 to 3.12.0 (#2256) Consider user avatars always public Implement authorization for Active Storage endpoints ...
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
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) || this.#hasModifier(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")
|
||||
}
|
||||
|
||||
#hasModifier(event) {
|
||||
return event.metaKey || event.ctrlKey || event.altKey || event.shiftKey
|
||||
}
|
||||
|
||||
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") && !this.#hotkeysDisabled(focusedList)) {
|
||||
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])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#hotkeysDisabled(navigableList) {
|
||||
return navigableList?.element.dataset.cardHotkeysDisabled === "true"
|
||||
}
|
||||
|
||||
#keyHandlers = {
|
||||
"["(event) { this.#postponeCard(event) },
|
||||
"]"(event) { this.#closeCard(event) },
|
||||
m(event) { this.#assignToMe(event) }
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ export default class extends Controller {
|
||||
static targets = [ "item", "counter" ]
|
||||
static values = {
|
||||
propertyName: String,
|
||||
maxValue: { type: Number, default: 20 }
|
||||
maxValue: { type: Number, default: 15 } // should match first geared pagination page size
|
||||
}
|
||||
|
||||
initialize() {
|
||||
@@ -13,7 +13,9 @@ export default class extends Controller {
|
||||
}
|
||||
|
||||
connect() {
|
||||
this.#updateCounter()
|
||||
if (this.itemTargets.length > 0) {
|
||||
this.#updateCounter()
|
||||
}
|
||||
}
|
||||
|
||||
itemTargetConnected() {
|
||||
|
||||
@@ -5,11 +5,13 @@ export default class extends Controller {
|
||||
static targets = [ "dialog" ]
|
||||
static values = {
|
||||
modal: { type: Boolean, default: false },
|
||||
sizing: { type: Boolean, default: true }
|
||||
sizing: { type: Boolean, default: true },
|
||||
autoOpen: { type: Boolean, default: false }
|
||||
}
|
||||
|
||||
connect() {
|
||||
this.dialogTarget.setAttribute("aria-hidden", "true")
|
||||
if (this.autoOpenValue) this.open()
|
||||
}
|
||||
|
||||
open() {
|
||||
|
||||
@@ -51,6 +51,10 @@ export default class extends Controller {
|
||||
this.selectItem(target, true)
|
||||
}
|
||||
|
||||
hoverSelect({ currentTarget }) {
|
||||
this.selectItem(currentTarget)
|
||||
}
|
||||
|
||||
selectCurrentOrReset(event) {
|
||||
if (this.currentItem) {
|
||||
this.#setCurrentFrom(this.currentItem)
|
||||
@@ -227,6 +231,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) {
|
||||
@@ -259,6 +277,6 @@ export default class extends Controller {
|
||||
} else {
|
||||
this.#clickCurrentItem(event)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,13 +26,27 @@ export default class extends Controller {
|
||||
set #theme(theme) {
|
||||
localStorage.setItem("theme", theme)
|
||||
|
||||
if (theme === "auto") {
|
||||
document.documentElement.removeAttribute("data-theme")
|
||||
} else {
|
||||
document.documentElement.setAttribute("data-theme", theme)
|
||||
const currentTheme = document.documentElement.getAttribute("data-theme") || "auto"
|
||||
const hasChanged = currentTheme !== theme
|
||||
|
||||
const prefersReducedMotion = window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches
|
||||
const animate = hasChanged && !prefersReducedMotion
|
||||
|
||||
const applyTheme = () => {
|
||||
if (theme === "auto") {
|
||||
document.documentElement.removeAttribute("data-theme")
|
||||
} else {
|
||||
document.documentElement.setAttribute("data-theme", theme)
|
||||
}
|
||||
|
||||
this.#updateButtons()
|
||||
}
|
||||
|
||||
this.#updateButtons()
|
||||
if (animate && document.startViewTransition) {
|
||||
document.startViewTransition(applyTheme)
|
||||
} else {
|
||||
applyTheme()
|
||||
}
|
||||
}
|
||||
|
||||
#applyStoredTheme() {
|
||||
|
||||
Reference in New Issue
Block a user