parent
98fc37530d
commit
06abd9883a
@ -1,6 +1,14 @@ |
||||
import { computeAllSolutions } from "../shared/solver.ts"; |
||||
import { rules } from "../shared/tictactoe-rules.ts"; |
||||
import { ProgressiveForm } from "./progressive-form.ts"; |
||||
import { ButtonTracker, SpanTracker } from "./query-tracker.ts"; |
||||
|
||||
const allSolutions = computeAllSolutions(3, 3, rules); |
||||
console.log(allSolutions.size); |
||||
console.log(allSolutions); |
||||
customElements.define("progressive-form", ProgressiveForm, { extends: "form" }); |
||||
customElements.define("span-query-tracker", SpanTracker, { extends: "span" }); |
||||
customElements.define("button-query-tracker", ButtonTracker, { extends: "button" }); |
||||
|
||||
document.addEventListener("DOMContentLoaded", function () { |
||||
const allSolutions = computeAllSolutions(3, 3, rules); |
||||
console.log(allSolutions.size); |
||||
console.log(allSolutions); |
||||
}); |
||||
|
@ -0,0 +1,31 @@ |
||||
// Until https://developer.mozilla.org/en-US/docs/Web/API/Navigation_API support is widespread
|
||||
|
||||
let lastUrl = document.location.href; |
||||
|
||||
const observers = new Set<() => void>(); |
||||
|
||||
const checkChanges = () => { |
||||
if (document.location.href !== lastUrl) { |
||||
lastUrl = document.location.href; |
||||
for (const observer of observers) { |
||||
observer(); |
||||
} |
||||
} |
||||
}; |
||||
|
||||
let pollingInterval: number | null = null; |
||||
|
||||
const startPolling = () => { |
||||
if (pollingInterval === null) { |
||||
pollingInterval = window.setInterval(checkChanges, 20); |
||||
} |
||||
}; |
||||
|
||||
export const onLocationChange = (observer: () => void) => { |
||||
observers.add(observer); |
||||
startPolling(); |
||||
}; |
||||
|
||||
export const onLocationChangeUnsubscribe = (observer: () => void) => { |
||||
observers.delete(observer); |
||||
}; |
@ -0,0 +1,26 @@ |
||||
const submitListener = function (this: HTMLFormElement, e: SubmitEvent) { |
||||
const targetUrl = new URL(this.action); |
||||
const formData = new FormData(this, e.submitter); |
||||
for (const [key, value] of formData.entries()) { |
||||
if (typeof value !== "string") { |
||||
throw new Error("File fields are not supported; falling back to regular form submission"); |
||||
} |
||||
|
||||
targetUrl.searchParams.set(key, value); |
||||
} |
||||
|
||||
window.history.pushState({}, "", targetUrl.toString()); |
||||
e.preventDefault(); |
||||
}; |
||||
|
||||
export class ProgressiveForm extends HTMLFormElement { |
||||
connectedCallback() { |
||||
if (this.method.toUpperCase() !== "POST") { |
||||
throw new Error( |
||||
"Only supported when method=POST and progressive form router is used on backend; falling back to regular forms", |
||||
); |
||||
} |
||||
|
||||
this.addEventListener("submit", submitListener); |
||||
} |
||||
} |
@ -0,0 +1,99 @@ |
||||
import { onLocationChange, onLocationChangeUnsubscribe } from "./navigation-utils.ts"; |
||||
|
||||
const createQueryStringTracker = (key: string, onChange: (newValue: string | null) => void) => { |
||||
let currentValue = new URLSearchParams(window.location.search).get(key); |
||||
const observer = () => { |
||||
const newValue = new URLSearchParams(window.location.search).get(key); |
||||
if (newValue !== currentValue) { |
||||
currentValue = newValue; |
||||
onChange(newValue); |
||||
} |
||||
}; |
||||
|
||||
onLocationChange(observer); |
||||
return () => { |
||||
onLocationChangeUnsubscribe(observer); |
||||
}; |
||||
}; |
||||
|
||||
class TrackingTools< |
||||
TElement extends HTMLElement & { handleTrackedValueUpdate(this: TElement, newValue: string | null): void }, |
||||
> { |
||||
private currentTrackKey: string | null = null; |
||||
private clearTracker: (() => void) | null = null; |
||||
|
||||
constructor(private readonly element: TElement) {} |
||||
|
||||
private setTrackerIfNeeded() { |
||||
const trackKey = this.element.getAttribute("track"); |
||||
if (trackKey != this.currentTrackKey) { |
||||
if (this.clearTracker) { |
||||
this.clearTracker(); |
||||
} |
||||
|
||||
if (trackKey) { |
||||
this.clearTracker = createQueryStringTracker( |
||||
trackKey, |
||||
this.element.handleTrackedValueUpdate.bind(this.element), |
||||
); |
||||
} |
||||
|
||||
this.currentTrackKey = trackKey; |
||||
} |
||||
} |
||||
|
||||
connectedCallback() { |
||||
this.setTrackerIfNeeded(); |
||||
} |
||||
|
||||
attributeChangedCallback() { |
||||
this.setTrackerIfNeeded(); |
||||
} |
||||
|
||||
disconnectedCallback() { |
||||
if (this.clearTracker) { |
||||
this.clearTracker(); |
||||
} |
||||
} |
||||
} |
||||
|
||||
export class SpanTracker extends HTMLSpanElement { |
||||
private readonly trackingTools = new TrackingTools(this); |
||||
|
||||
handleTrackedValueUpdate(newValue: string | null) { |
||||
this.innerText = newValue ?? "[null]"; |
||||
} |
||||
|
||||
connectedCallback() { |
||||
this.trackingTools.connectedCallback(); |
||||
} |
||||
|
||||
attributeChangedCallback() { |
||||
this.trackingTools.attributeChangedCallback(); |
||||
} |
||||
|
||||
disconnectedCallback() { |
||||
this.trackingTools.disconnectedCallback(); |
||||
} |
||||
} |
||||
|
||||
export class ButtonTracker extends HTMLButtonElement { |
||||
private readonly trackingTools = new TrackingTools(this); |
||||
|
||||
handleTrackedValueUpdate(newValue: string | null) { |
||||
const delta = parseInt(this.getAttribute("delta") ?? "0", 10); |
||||
this.value = (parseInt(newValue ?? "0", 10) + delta).toString(); |
||||
} |
||||
|
||||
connectedCallback() { |
||||
this.trackingTools.connectedCallback(); |
||||
} |
||||
|
||||
attributeChangedCallback() { |
||||
this.trackingTools.attributeChangedCallback(); |
||||
} |
||||
|
||||
disconnectedCallback() { |
||||
this.trackingTools.disconnectedCallback(); |
||||
} |
||||
} |
@ -0,0 +1,11 @@ |
||||
declare module "preact/jsx-runtime" { |
||||
// eslint-disable-next-line @typescript-eslint/no-namespace -- preact uses namespaces, so we have too
|
||||
namespace JSX { |
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions -- this is how declaration merging is done
|
||||
interface HTMLAttributes { |
||||
// custom attribute used by query-tracker web components
|
||||
delta?: string; |
||||
track?: string; |
||||
} |
||||
} |
||||
} |
Loading…
Reference in new issue