PageSourceSearch

https://www.appliedmldays.org/assets/controllers/positions_controller-04c3213b.js

js appliedmldays.org collected 2026-09-24 18:34:33 UTC 2,638 bytes, 75 lines download raw bytes

1import { Controller } from "@hotwired/stimulus";
2import Sortable from "sortablejs";
3
4// Drag-and-drop ordering (mouse + touch) for nested form rows. Position is
5// derived from the row order and stays hidden. With several lists on one
6// controller (e.g. partner tiers), rows can be dragged between them and the
7// list's data-tier / data-kind is written to the row's hidden tier / kind field.
8// When a list has aria-labelledby, each row's select is labelled by that heading
9// plus the row's position label, so the name follows the row across lists.
10export default class extends Controller {
11  static targets = ["list"];
12
13  initialize() {
14    this.sortables = new Map();
15  }
16
17  connect() {
18    this.renumber();
19  }
20
21  disconnect() {
22    for (const sortable of this.sortables.values()) sortable.destroy();
23    this.sortables.clear();
24  }
25
26  listTargetConnected(list) {
27    // Lists within one controller instance share a group so rows can move
28    // between them; instances stay isolated from each other. Target callbacks
29    // run before connect(), so initialize the group lazily here.
30    this.group ||= crypto.randomUUID();
31
32    const sortable = Sortable.create(list, {
33      group: this.group,
34      handle: "[data-drag-handle]",
35      draggable: "[data-nested-form-row]",
36      ghostClass: "opacity-50",
37      dragClass: "opacity-50",
38      animation: 150,
39      onEnd: ({ item, from, to }) => {
40        this.dispatch("moved", { detail: { item, from, to } });
41        this.renumber();
42      },
43    });
44    this.sortables.set(list, sortable);
45  }
46
47  listTargetDisconnected(list) {
48    this.sortables.get(list)?.destroy();
49    this.sortables.delete(list);
50  }
51
52  renumber() {
53    for (const list of this.listTargets) {
54      const labelledby = list.getAttribute("aria-labelledby");
55      let position = 0;
56
57      for (const row of list.querySelectorAll(":scope > [data-nested-form-row]")) {
58        if (row.classList.contains("hidden")) continue; // marked for destruction
59
60        position += 1;
61        for (const field of ["tier", "kind"]) {
62          if (list.dataset[field]) row.querySelector(`input[name*='[${field}]']`).value = list.dataset[field];
63        }
64        row.querySelector("input[name*='[position]']").value = position;
65        const positionLabel = row.querySelector("[data-position-label]");
66        positionLabel.textContent = `#${position}`;
67        if (labelledby) {
68          row.querySelector("select")?.setAttribute("aria-labelledby", `${labelledby} ${positionLabel.id}`);
69        }
70      }
71
72      list.querySelector(":scope > [data-empty-message]")?.classList.toggle("hidden", position > 0);
73    }
74  }
75}

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.