Merge branch 'main' into latest-activity
* main: Don't round up in time distance expression Tidy Gemfile Banner for draft state Drafts should always use the dashed outline wherever they're displayed Unnecessary parens Use distance to nearest edge Simplify overlap logic Fix divider drag jankiness Use cache-friendly "time ago" positionedClass -> installedClass Pull out moveDividerTo Before comes before After DividerElement -> Divider Clean up divider-controller Drag bubble divider
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
|
||||
const MOVE_ITEM_DATA_TYPE = "x-fizzy/move"
|
||||
const DIVIDER_ITEM_NODE_NAME = "LI"
|
||||
const OVERLAP_THRESHOLD = 0.25
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = [ "divider", "dragImage", "count" ]
|
||||
static classes = [ "installed", "dragging" ]
|
||||
static values = { startCount: Number, maxCount: Number }
|
||||
|
||||
connect() {
|
||||
this.install()
|
||||
}
|
||||
|
||||
install() {
|
||||
this.#moveDividerTo(this.startCountValue)
|
||||
this.dividerTarget.classList.add(this.installedClass)
|
||||
}
|
||||
|
||||
configureDrag(event) {
|
||||
if (event.target == this.dividerTarget) {
|
||||
event.dataTransfer.dropEffect = "move"
|
||||
event.dataTransfer.setData(MOVE_ITEM_DATA_TYPE, event.target)
|
||||
event.dataTransfer.setDragImage(this.dragImageTarget, 0, 0)
|
||||
this.element.classList.add(this.draggingClass)
|
||||
}
|
||||
}
|
||||
|
||||
acceptDrop(event) {
|
||||
if (event.dataTransfer.types.includes(MOVE_ITEM_DATA_TYPE)) {
|
||||
event.preventDefault()
|
||||
}
|
||||
}
|
||||
|
||||
moveDivider(event) {
|
||||
if (event.target.nodeName == DIVIDER_ITEM_NODE_NAME) {
|
||||
const rect = this.dividerTarget.getBoundingClientRect()
|
||||
const distanceToTop = Math.abs(event.clientY - rect.top)
|
||||
const distanceToBottom = Math.abs(event.clientY - (rect.top + rect.height))
|
||||
const distanceToNearestEdge = Math.min(distanceToTop, distanceToBottom)
|
||||
const overlap = distanceToNearestEdge / rect.height
|
||||
|
||||
if (overlap > OVERLAP_THRESHOLD) {
|
||||
this.#moveDividerTo(this.#items.indexOf(event.target))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
drop() {
|
||||
this.element.classList.remove(this.draggingClass)
|
||||
}
|
||||
|
||||
#moveDividerTo(index) {
|
||||
if (index <= this.maxCountValue) {
|
||||
if (this.#dividerIndex < index) {
|
||||
this.#positionDividerAfter(index)
|
||||
} else if (this.#dividerIndex > index) {
|
||||
this.#positionDividerBefore(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#positionDividerBefore(index) {
|
||||
const position = Math.max(index, 1)
|
||||
this.#items[position].before(this.dividerTarget)
|
||||
this.countTarget.textContent = position
|
||||
}
|
||||
|
||||
#positionDividerAfter(index) {
|
||||
const position = Math.min(index, this.#items.length - 1, this.maxCountValue)
|
||||
this.#items[position].after(this.dividerTarget)
|
||||
this.countTarget.textContent = position
|
||||
}
|
||||
|
||||
get #items() {
|
||||
return Array.from(this.element.children)
|
||||
}
|
||||
|
||||
get #dividerIndex() {
|
||||
return this.#items.indexOf(this.dividerTarget)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = ["time", "date", "datetime", "ago"]
|
||||
|
||||
initialize() {
|
||||
this.timeFormatter = new Intl.DateTimeFormat(undefined, { timeStyle: "short" })
|
||||
this.dateFormatter = new Intl.DateTimeFormat(undefined, { dateStyle: "long" })
|
||||
this.dateTimeFormatter = new Intl.DateTimeFormat(undefined, { timeStyle: "short", dateStyle: "short" })
|
||||
this.agoFormatter = new AgoFormatter()
|
||||
}
|
||||
|
||||
timeTargetConnected(target) {
|
||||
this.#formatTime(this.timeFormatter, target)
|
||||
}
|
||||
|
||||
dateTargetConnected(target) {
|
||||
this.#formatTime(this.dateFormatter, target)
|
||||
}
|
||||
|
||||
datetimeTargetConnected(target) {
|
||||
this.#formatTime(this.dateTimeFormatter, target)
|
||||
}
|
||||
|
||||
agoTargetConnected(target) {
|
||||
this.#formatTime(this.agoFormatter, target)
|
||||
}
|
||||
|
||||
#formatTime(formatter, target) {
|
||||
const dt = new Date(target.getAttribute("datetime"))
|
||||
target.textContent = formatter.format(dt)
|
||||
target.title = this.dateTimeFormatter.format(dt)
|
||||
}
|
||||
}
|
||||
|
||||
class AgoFormatter {
|
||||
format(dt) {
|
||||
const now = new Date()
|
||||
const seconds = (now - dt) / 1000
|
||||
const minutes = seconds / 60
|
||||
const hours = minutes / 60
|
||||
const days = hours / 24
|
||||
const weeks = days / 7
|
||||
const months = days / (365 / 12)
|
||||
const years = days / 365
|
||||
|
||||
if (years >= 1) return this.#pluralize("year", years)
|
||||
if (months >= 1) return this.#pluralize("month", months)
|
||||
if (weeks >= 1) return this.#pluralize("week", weeks)
|
||||
if (days >= 1) return this.#pluralize("day", days)
|
||||
if (hours >= 1) return this.#pluralize("hour", hours)
|
||||
if (minutes >= 1) return this.#pluralize("minute", minutes)
|
||||
|
||||
return "Less than a minute ago"
|
||||
}
|
||||
|
||||
#pluralize(word, quantity) {
|
||||
quantity = Math.floor(quantity)
|
||||
const suffix = (quantity === 1) ? "" : "s"
|
||||
return `${quantity} ${word}${suffix} ago`
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
|
||||
// FIXME: Can we do this without a controller? https://github.com/basecamp/fizzy/pull/130#discussion_r1833094616
|
||||
export default class extends Controller {
|
||||
merge({ params: { key, value } }) {
|
||||
const url = new URL(window.location.href)
|
||||
url.searchParams.set(key, value)
|
||||
Turbo.visit(url)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user