PageSourceSearch

https://trmnl.com/assets/controllers/dirty_form_controller-f755ab40.js

js trmnl.com collected 2026-10-03 22:57:04 UTC 3,284 bytes, 103 lines download raw bytes

1import { Controller } from "@hotwired/stimulus"
2
3const BUTTON_TYPES = ["submit", "button", "reset"]
4
5// Tracks form field changes and shows a sticky save bar when fields are dirty.
6export default class extends Controller {
7  static targets = ["bar", "cleanNotice", "form", "submit"]
8  static values = { showSubmitWhenClean: Boolean }
9
10  connect() {
11    this.rememberCurrentValues()
12    this.watchForChanges()
13    this.warnBeforeLeaving()
14    this.setDirtyState(false)
15  }
16
17  disconnect() {
18    window.removeEventListener("beforeunload", this.beforeUnloadHandler)
19    document.removeEventListener("input", this.checkHandler)
20    document.removeEventListener("change", this.checkHandler)
21    this.form.removeEventListener("submit", this.submitHandler)
22  }
23
24  check() {
25    const dirty = this.snapshot() !== this.initialSnapshot
26    this.setDirtyState(dirty)
27
28    if (dirty) {
29      window.addEventListener("beforeunload", this.beforeUnloadHandler)
30    } else {
31      window.removeEventListener("beforeunload", this.beforeUnloadHandler)
32    }
33  }
34
35  discard() {
36    this.initialValues.forEach((value, field) => {
37      this.restore(field, value)
38      field.dispatchEvent(new Event("change", { bubbles: true }))
39    })
40    this.setDirtyState(false)
41    window.removeEventListener("beforeunload", this.beforeUnloadHandler)
42  }
43
44  setDirtyState(dirty) {
45    this.form.dataset.dirty = String(dirty)
46    this.barTargets.forEach((bar) => bar.classList.toggle("hidden", !dirty))
47    this.cleanNoticeTargets.forEach((notice) => notice.classList.toggle("hidden", dirty))
48    this.submitTargets.forEach((submit) => {
49      submit.disabled = !dirty
50      submit.classList.toggle("hidden", !dirty && !this.showSubmitWhenCleanValue)
51    })
52  }
53
54  get form() {
55    return this.hasFormTarget ? this.formTarget : this.element
56  }
57
58  rememberCurrentValues() {
59    this.initialValues = new Map(this.submittableFields().map((field) => [field, this.fieldValue(field)]))
60    this.initialSnapshot = this.snapshot()
61  }
62
63  watchForChanges() {
64    // Fields attached with form="..." sit outside the form, so their events never bubble to it.
65    this.checkHandler = (event) => { if (event.target.form === this.form) this.check() }
66    document.addEventListener("input", this.checkHandler)
67    document.addEventListener("change", this.checkHandler)
68  }
69
70  warnBeforeLeaving() {
71    this.beforeUnloadHandler = (event) => event.preventDefault()
72    this.submitHandler = () => {
73      this.form.dataset.dirty = "false"
74      window.removeEventListener("beforeunload", this.beforeUnloadHandler)
75    }
76    this.form.addEventListener("submit", this.submitHandler)
77  }
78
79  snapshot() {
80    return this.submittableFields().map((field) => `${field.name}=${this.fieldValue(field)}`).join("&")
81  }
82
83  submittableFields() {
84    return Array.from(this.form.elements)
85      .filter((field) => field.name && !field.disabled && !BUTTON_TYPES.includes(field.type))
86  }
87
88  fieldValue(field) {
89    return this.usesCheckedState(field) ? field.checked.toString() : field.value
90  }
91
92  restore(field, value) {
93    if (this.usesCheckedState(field)) {
94      field.checked = value === "true"
95    } else {
96      field.value = value
97    }
98  }
99
100  usesCheckedState(field) {
101    return field.type === "checkbox" || field.type === "radio"
102  }
103}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.