@unabridged/midwest 0.19.1 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/README.md +1 -1
  2. package/app/assets/javascript/midwest/index.ts +8 -0
  3. package/app/assets/javascript/midwest/template_helpers.ts +27 -0
  4. package/app/assets/javascript/midwest.js +724 -0
  5. package/app/assets/javascript/midwest.js.map +1 -1
  6. package/app/assets/stylesheets/midwest.css +1 -1
  7. package/app/assets/stylesheets/midwest.tailwind.css +5 -1
  8. package/dist/css/midwest.css +1 -1
  9. package/dist/javascript/collection/app/assets/javascript/midwest/index.js +8 -0
  10. package/dist/javascript/collection/app/assets/javascript/midwest/index.js.map +1 -1
  11. package/dist/javascript/collection/app/assets/javascript/midwest/template_helpers.js +10 -0
  12. package/dist/javascript/collection/app/assets/javascript/midwest/template_helpers.js.map +1 -0
  13. package/dist/javascript/collection/app/components/midwest/card_component/card_component_controller.js.map +1 -1
  14. package/dist/javascript/collection/app/components/midwest/command_palette_component/command_palette_component_controller.js +350 -0
  15. package/dist/javascript/collection/app/components/midwest/command_palette_component/command_palette_component_controller.js.map +1 -0
  16. package/dist/javascript/collection/app/components/midwest/form/live_summary_component/live_summary_component_controller.js +24 -0
  17. package/dist/javascript/collection/app/components/midwest/form/live_summary_component/live_summary_component_controller.js.map +1 -1
  18. package/dist/javascript/collection/app/components/midwest/pagination_component/pagination_component_controller.js +52 -0
  19. package/dist/javascript/collection/app/components/midwest/pagination_component/pagination_component_controller.js.map +1 -0
  20. package/dist/javascript/collection/app/components/midwest/show_if_component/show_if_component_controller.js +80 -0
  21. package/dist/javascript/collection/app/components/midwest/show_if_component/show_if_component_controller.js.map +1 -0
  22. package/dist/javascript/collection/app/components/midwest/table_component/table_component_controller.js +19 -0
  23. package/dist/javascript/collection/app/components/midwest/table_component/table_component_controller.js.map +1 -1
  24. package/dist/javascript/collection/app/components/midwest/tree_view_component/tree_view_component_controller.js +204 -0
  25. package/dist/javascript/collection/app/components/midwest/tree_view_component/tree_view_component_controller.js.map +1 -0
  26. package/dist/javascript/midwest.js +724 -0
  27. package/dist/javascript/midwest.js.map +1 -1
  28. package/package.json +1 -1
@@ -3244,8 +3244,12 @@ class FormLiveSummary extends Controller {
3244
3244
  return;
3245
3245
  if (this.element.contains(element))
3246
3246
  return;
3247
+ if (element.closest("fieldset.midwest-show-if[hidden]"))
3248
+ return;
3247
3249
  if (element instanceof HTMLButtonElement)
3248
3250
  return;
3251
+ if (element.closest(".midwest-tree-view"))
3252
+ return;
3249
3253
  if (element instanceof HTMLInputElement) {
3250
3254
  if (SKIPPED_INPUT_TYPES.has(element.type))
3251
3255
  return;
@@ -3306,6 +3310,26 @@ class FormLiveSummary extends Controller {
3306
3310
  const selected = Array.from(ac.querySelectorAll(".midwest-autocomplete-tokens .value")).map((el) => el.textContent?.trim()).filter(Boolean).join(", ");
3307
3311
  fields.push({ label, value: selected || "\u2014" });
3308
3312
  });
3313
+ const seenTreeNames = /* @__PURE__ */ new Set();
3314
+ form.querySelectorAll(".midwest-tree-view").forEach((tree) => {
3315
+ if (this.element.contains(tree))
3316
+ return;
3317
+ const checked = Array.from(
3318
+ tree.querySelectorAll(".tree-node-checkbox:checked")
3319
+ );
3320
+ const firstName = checked[0]?.name || tree.querySelectorAll(".tree-node-checkbox")[0]?.name;
3321
+ if (!firstName)
3322
+ return;
3323
+ if (seenTreeNames.has(firstName))
3324
+ return;
3325
+ seenTreeNames.add(firstName);
3326
+ const label = this.prettifyName(firstName.replace(/\[\]$/, ""));
3327
+ const values = checked.map((cb) => {
3328
+ const nodeLabel = cb.closest(".midwest-tree-node")?.querySelector(":scope > .tree-node-trigger > .tree-node-label");
3329
+ return nodeLabel?.textContent?.trim() || cb.value;
3330
+ });
3331
+ fields.push({ label, value: values.join(", ") || "\u2014" });
3332
+ });
3309
3333
  return fields;
3310
3334
  }
3311
3335
  // Builds a small inline colour swatch to display alongside a colour value.
@@ -4784,6 +4808,25 @@ class Table extends Controller {
4784
4808
  cb.checked = checked;
4785
4809
  });
4786
4810
  }
4811
+ navigateRow(event) {
4812
+ const target = event.target;
4813
+ if (target.closest('a, button, input, select, textarea, [role="button"]'))
4814
+ return;
4815
+ const row = event.currentTarget;
4816
+ const href = row.dataset.href;
4817
+ if (!href)
4818
+ return;
4819
+ if (event.metaKey || event.ctrlKey) {
4820
+ window.open(href, "_blank");
4821
+ } else {
4822
+ const Turbo = window.Turbo;
4823
+ if (Turbo?.visit) {
4824
+ Turbo.visit(href);
4825
+ } else {
4826
+ window.location.href = href;
4827
+ }
4828
+ }
4829
+ }
4787
4830
  updateSelectAll() {
4788
4831
  if (!this.hasSelectAllTarget)
4789
4832
  return;
@@ -4858,6 +4901,683 @@ class LoadMore extends Controller {
4858
4901
  }
4859
4902
  }
4860
4903
 
4904
+ class Pagination extends Controller {
4905
+ static targets = ["jumpInput"];
4906
+ static values = { urlTemplate: String, turboFrameId: String };
4907
+ debounceTimer = null;
4908
+ // Intercepts the jump-to-page form submit and navigates to the resolved URL.
4909
+ jump(event) {
4910
+ event.preventDefault();
4911
+ if (!this.hasJumpInputTarget)
4912
+ return;
4913
+ const page = parseInt(this.jumpInputTarget.value, 10);
4914
+ if (!isNaN(page) && page >= 1) {
4915
+ this.visitUrl(this.urlTemplateValue.replace("__PAGE__", String(page)));
4916
+ }
4917
+ }
4918
+ // Debounced handler for the `incremental` input — navigates 400 ms after the
4919
+ // user stops typing, and only when the value is within the valid page range.
4920
+ jumpIncremental() {
4921
+ if (this.debounceTimer)
4922
+ clearTimeout(this.debounceTimer);
4923
+ this.debounceTimer = setTimeout(() => {
4924
+ if (!this.hasJumpInputTarget)
4925
+ return;
4926
+ const input = this.jumpInputTarget;
4927
+ const page = parseInt(input.value, 10);
4928
+ const max = parseInt(input.max, 10);
4929
+ if (!isNaN(page) && page >= 1 && (!max || page <= max)) {
4930
+ this.visitUrl(this.urlTemplateValue.replace("__PAGE__", String(page)));
4931
+ }
4932
+ }, 400);
4933
+ }
4934
+ // Navigates to the URL selected in the per-page <select> element.
4935
+ navigate(event) {
4936
+ const select = event.target;
4937
+ const href = select.value;
4938
+ if (href && href !== "#") {
4939
+ this.visitUrl(href);
4940
+ }
4941
+ }
4942
+ // Navigates to a URL via Turbo visit, updating the browser URL.
4943
+ visitUrl(url) {
4944
+ if (window.Turbo) {
4945
+ window.Turbo.visit(url);
4946
+ } else {
4947
+ window.location.href = url;
4948
+ }
4949
+ }
4950
+ }
4951
+
4952
+ class ShowIf extends Controller {
4953
+ static values = {
4954
+ watch: { type: String, default: "" },
4955
+ is: { type: String, default: "" },
4956
+ negate: { type: Boolean, default: false }
4957
+ };
4958
+ sourceElement = null;
4959
+ listenedElements = [];
4960
+ changeListener = null;
4961
+ connect() {
4962
+ if (!this.watchValue)
4963
+ return;
4964
+ this.sourceElement = document.querySelector(this.watchValue);
4965
+ if (!this.sourceElement)
4966
+ return;
4967
+ this.changeListener = () => this.evaluate();
4968
+ if (this.sourceElement instanceof HTMLInputElement && this.sourceElement.type === "radio") {
4969
+ const radios = document.querySelectorAll(
4970
+ `input[type="radio"][name="${this.sourceElement.name}"]`
4971
+ );
4972
+ const listener = this.changeListener;
4973
+ radios.forEach((radio) => {
4974
+ radio.addEventListener("change", listener);
4975
+ this.listenedElements.push(radio);
4976
+ });
4977
+ } else {
4978
+ this.sourceElement.addEventListener("input", this.changeListener);
4979
+ this.sourceElement.addEventListener("change", this.changeListener);
4980
+ this.listenedElements.push(this.sourceElement);
4981
+ }
4982
+ this.evaluate();
4983
+ }
4984
+ disconnect() {
4985
+ if (this.changeListener) {
4986
+ for (const el of this.listenedElements) {
4987
+ el.removeEventListener("input", this.changeListener);
4988
+ el.removeEventListener("change", this.changeListener);
4989
+ }
4990
+ }
4991
+ this.changeListener = null;
4992
+ this.sourceElement = null;
4993
+ this.listenedElements = [];
4994
+ }
4995
+ evaluate() {
4996
+ const el = this.sourceElement;
4997
+ if (!el)
4998
+ return;
4999
+ const currentValue = this.readValue(el);
5000
+ const match = currentValue === this.isValue;
5001
+ const visible = this.negateValue ? !match : match;
5002
+ this.element.toggleAttribute("hidden", !visible);
5003
+ this.element.toggleAttribute("disabled", !visible);
5004
+ }
5005
+ readValue(el) {
5006
+ if (el instanceof HTMLInputElement) {
5007
+ if (el.type === "radio") {
5008
+ const checked = document.querySelector(
5009
+ `input[name="${el.name}"]:checked`
5010
+ );
5011
+ return checked ? checked.value : "";
5012
+ }
5013
+ if (el.type === "checkbox") {
5014
+ return el.checked ? el.value || "true" : "";
5015
+ }
5016
+ return el.value;
5017
+ }
5018
+ if (el instanceof HTMLSelectElement) {
5019
+ return el.value;
5020
+ }
5021
+ if (el instanceof HTMLTextAreaElement) {
5022
+ return el.value;
5023
+ }
5024
+ return el.getAttribute("value") ?? "";
5025
+ }
5026
+ }
5027
+
5028
+ class TreeView extends Controller {
5029
+ // ── Expand / Collapse ───────────────────────────────────────────────
5030
+ toggle(event) {
5031
+ const chevron = event.currentTarget.closest(".tree-node-chevron");
5032
+ if (!chevron)
5033
+ return;
5034
+ const node = chevron.closest(".midwest-tree-node");
5035
+ if (!node)
5036
+ return;
5037
+ const expanded = node.getAttribute("aria-expanded") === "true";
5038
+ node.setAttribute("aria-expanded", String(!expanded));
5039
+ this.focusNode(node);
5040
+ }
5041
+ // ── Checkbox Cascading ──────────────────────────────────────────────
5042
+ checkboxChanged(event) {
5043
+ const checkbox = event.target;
5044
+ const node = checkbox.closest(".midwest-tree-node");
5045
+ if (!node)
5046
+ return;
5047
+ const descendants = node.querySelectorAll(
5048
+ ".tree-node-children .tree-node-checkbox"
5049
+ );
5050
+ for (const desc of descendants) {
5051
+ desc.checked = checkbox.checked;
5052
+ desc.indeterminate = false;
5053
+ }
5054
+ this.updateAncestors(node);
5055
+ }
5056
+ updateAncestors(node) {
5057
+ const parentGroup = node.parentElement;
5058
+ if (!parentGroup || !parentGroup.classList.contains("tree-node-children"))
5059
+ return;
5060
+ const parentNode = parentGroup.closest(".midwest-tree-node");
5061
+ if (!parentNode)
5062
+ return;
5063
+ const parentCheckbox = parentNode.querySelector(
5064
+ ":scope > .tree-node-trigger > .tree-node-checkbox"
5065
+ );
5066
+ if (!parentCheckbox)
5067
+ return;
5068
+ const childCheckboxes = parentGroup.querySelectorAll(
5069
+ ":scope > .midwest-tree-node > .tree-node-trigger > .tree-node-checkbox"
5070
+ );
5071
+ const total = childCheckboxes.length;
5072
+ let checked = 0;
5073
+ let indeterminate = 0;
5074
+ for (const cb of childCheckboxes) {
5075
+ if (cb.indeterminate)
5076
+ indeterminate++;
5077
+ else if (cb.checked)
5078
+ checked++;
5079
+ }
5080
+ if (indeterminate > 0 || checked > 0 && checked < total) {
5081
+ parentCheckbox.checked = false;
5082
+ parentCheckbox.indeterminate = true;
5083
+ } else if (checked === total) {
5084
+ parentCheckbox.checked = true;
5085
+ parentCheckbox.indeterminate = false;
5086
+ } else {
5087
+ parentCheckbox.checked = false;
5088
+ parentCheckbox.indeterminate = false;
5089
+ }
5090
+ this.updateAncestors(parentNode);
5091
+ }
5092
+ // ── Keyboard Navigation (WAI-ARIA TreeView) ────────────────────────
5093
+ connect() {
5094
+ this.element.addEventListener("keydown", this.handleKeydown);
5095
+ const firstNode = this.element.querySelector(".midwest-tree-node");
5096
+ if (firstNode) {
5097
+ firstNode.setAttribute("tabindex", "0");
5098
+ }
5099
+ }
5100
+ disconnect() {
5101
+ this.element.removeEventListener("keydown", this.handleKeydown);
5102
+ }
5103
+ handleKeydown = (event) => {
5104
+ const target = event.target.closest(".midwest-tree-node");
5105
+ if (!target)
5106
+ return;
5107
+ switch (event.key) {
5108
+ case "ArrowDown":
5109
+ event.preventDefault();
5110
+ this.moveFocus(target, "next");
5111
+ break;
5112
+ case "ArrowUp":
5113
+ event.preventDefault();
5114
+ this.moveFocus(target, "previous");
5115
+ break;
5116
+ case "ArrowRight":
5117
+ event.preventDefault();
5118
+ this.handleArrowRight(target);
5119
+ break;
5120
+ case "ArrowLeft":
5121
+ event.preventDefault();
5122
+ this.handleArrowLeft(target);
5123
+ break;
5124
+ case "Home":
5125
+ event.preventDefault();
5126
+ this.focusFirst();
5127
+ break;
5128
+ case "End":
5129
+ event.preventDefault();
5130
+ this.focusLast();
5131
+ break;
5132
+ case "Enter":
5133
+ case " ":
5134
+ event.preventDefault();
5135
+ this.handleActivate(target);
5136
+ break;
5137
+ }
5138
+ };
5139
+ getVisibleNodes() {
5140
+ const nodes = [];
5141
+ const walk = (container) => {
5142
+ const children = container.querySelectorAll(
5143
+ ":scope > .midwest-tree-node"
5144
+ );
5145
+ for (const child of children) {
5146
+ nodes.push(child);
5147
+ if (child.getAttribute("aria-expanded") === "true") {
5148
+ const group = child.querySelector(":scope > .tree-node-children");
5149
+ if (group)
5150
+ walk(group);
5151
+ }
5152
+ }
5153
+ };
5154
+ walk(this.element);
5155
+ return nodes;
5156
+ }
5157
+ moveFocus(current, direction) {
5158
+ const visible = this.getVisibleNodes();
5159
+ const index = visible.indexOf(current);
5160
+ if (index === -1)
5161
+ return;
5162
+ const nextIndex = direction === "next" ? index + 1 : index - 1;
5163
+ if (nextIndex >= 0 && nextIndex < visible.length) {
5164
+ this.focusNode(visible[nextIndex]);
5165
+ }
5166
+ }
5167
+ handleArrowRight(node) {
5168
+ const isExpanded = node.getAttribute("aria-expanded");
5169
+ if (isExpanded === "false") {
5170
+ node.setAttribute("aria-expanded", "true");
5171
+ } else if (isExpanded === "true") {
5172
+ const firstChild = node.querySelector(
5173
+ ":scope > .tree-node-children > .midwest-tree-node"
5174
+ );
5175
+ if (firstChild)
5176
+ this.focusNode(firstChild);
5177
+ }
5178
+ }
5179
+ handleArrowLeft(node) {
5180
+ const isExpanded = node.getAttribute("aria-expanded");
5181
+ if (isExpanded === "true") {
5182
+ node.setAttribute("aria-expanded", "false");
5183
+ } else {
5184
+ const parentGroup = node.parentElement;
5185
+ if (parentGroup && parentGroup.classList.contains("tree-node-children")) {
5186
+ const parentNode = parentGroup.closest(".midwest-tree-node");
5187
+ if (parentNode)
5188
+ this.focusNode(parentNode);
5189
+ }
5190
+ }
5191
+ }
5192
+ handleActivate(node) {
5193
+ const checkbox = node.querySelector(
5194
+ ":scope > .tree-node-trigger > .tree-node-checkbox"
5195
+ );
5196
+ if (checkbox) {
5197
+ checkbox.checked = !checkbox.checked;
5198
+ checkbox.dispatchEvent(new Event("change", { bubbles: true }));
5199
+ return;
5200
+ }
5201
+ const expanded = node.getAttribute("aria-expanded");
5202
+ if (expanded !== null) {
5203
+ node.setAttribute("aria-expanded", String(expanded !== "true"));
5204
+ }
5205
+ }
5206
+ focusNode(node) {
5207
+ const current = this.element.querySelector(
5208
+ '.midwest-tree-node[tabindex="0"]'
5209
+ );
5210
+ if (current)
5211
+ current.setAttribute("tabindex", "-1");
5212
+ node.setAttribute("tabindex", "0");
5213
+ node.focus();
5214
+ }
5215
+ focusFirst() {
5216
+ const first = this.element.querySelector(".midwest-tree-node");
5217
+ if (first)
5218
+ this.focusNode(first);
5219
+ }
5220
+ focusLast() {
5221
+ const visible = this.getVisibleNodes();
5222
+ if (visible.length > 0) {
5223
+ this.focusNode(visible[visible.length - 1]);
5224
+ }
5225
+ }
5226
+ }
5227
+
5228
+ function cloneTemplate(template) {
5229
+ const fragment = template.content.cloneNode(true);
5230
+ const el = fragment.firstElementChild;
5231
+ if (!el)
5232
+ throw new Error("Template has no element child");
5233
+ return el;
5234
+ }
5235
+
5236
+ class CommandPalette extends Controller {
5237
+ static targets = ["input", "list", "empty", "body", "turboList", "itemTemplate", "groupTemplate"];
5238
+ static values = {
5239
+ hotkey: { type: String, default: "k" },
5240
+ items: { type: Array, default: [] },
5241
+ src: { type: String, default: "" },
5242
+ turboFrameUrl: { type: String, default: "" }
5243
+ };
5244
+ highlightedIndex = -1;
5245
+ fetchAbort = null;
5246
+ searchTimer;
5247
+ allItems = [];
5248
+ connect() {
5249
+ this.allItems = this.itemsValue;
5250
+ document.addEventListener("keydown", this.handleGlobalKeydown);
5251
+ this.element.addEventListener("click", this.handleBackdropClick);
5252
+ if (!this.turboFrameUrlValue) {
5253
+ this.renderItems(this.allItems);
5254
+ }
5255
+ }
5256
+ disconnect() {
5257
+ document.removeEventListener("keydown", this.handleGlobalKeydown);
5258
+ this.element.removeEventListener("click", this.handleBackdropClick);
5259
+ if (this.fetchAbort)
5260
+ this.fetchAbort.abort();
5261
+ if (this.searchTimer)
5262
+ clearTimeout(this.searchTimer);
5263
+ }
5264
+ // ── Turbo frame lifecycle ───────────────────────────────────────────
5265
+ turboListTargetConnected(el) {
5266
+ el.addEventListener("turbo:frame-load", this.onFrameLoad);
5267
+ }
5268
+ turboListTargetDisconnected(el) {
5269
+ el.removeEventListener("turbo:frame-load", this.onFrameLoad);
5270
+ }
5271
+ onFrameLoad = () => {
5272
+ if (!this.hasListTarget)
5273
+ return;
5274
+ const items = this.listTarget.querySelectorAll(".command-palette-item");
5275
+ if (items.length > 0) {
5276
+ this.highlightedIndex = 0;
5277
+ this.updateHighlight();
5278
+ this.emptyTarget.hidden = true;
5279
+ if (this.hasListTarget)
5280
+ this.listTarget.hidden = false;
5281
+ } else {
5282
+ this.emptyTarget.hidden = false;
5283
+ if (this.hasListTarget)
5284
+ this.listTarget.hidden = true;
5285
+ }
5286
+ };
5287
+ // ── Public actions ──────────────────────────────────────────────────
5288
+ open() {
5289
+ if (this.element.open)
5290
+ return;
5291
+ this.element.showModal();
5292
+ this.inputTarget.value = "";
5293
+ this.highlightedIndex = -1;
5294
+ if (this.turboFrameUrlValue) {
5295
+ this.navigateFrame("");
5296
+ } else {
5297
+ this.renderItems(this.allItems);
5298
+ }
5299
+ requestAnimationFrame(() => this.inputTarget.focus());
5300
+ }
5301
+ close() {
5302
+ if (!this.element.open)
5303
+ return;
5304
+ this.element.close();
5305
+ }
5306
+ filter() {
5307
+ const query = this.inputTarget.value.trim().toLowerCase();
5308
+ if (this.turboFrameUrlValue) {
5309
+ this.debouncedNavigateFrame(query);
5310
+ return;
5311
+ }
5312
+ if (this.srcValue && query.length > 0) {
5313
+ this.fetchRemote(query);
5314
+ return;
5315
+ }
5316
+ if (query.length === 0) {
5317
+ this.renderItems(this.allItems);
5318
+ return;
5319
+ }
5320
+ const filtered = this.allItems.filter((item) => {
5321
+ const haystack = `${item.label} ${item.description || ""} ${item.group || ""}`.toLowerCase();
5322
+ return haystack.includes(query);
5323
+ });
5324
+ this.renderItems(filtered);
5325
+ }
5326
+ navigate(event) {
5327
+ const items = this.visibleItems;
5328
+ switch (event.key) {
5329
+ case "ArrowDown":
5330
+ event.preventDefault();
5331
+ this.highlightedIndex = Math.min(this.highlightedIndex + 1, items.length - 1);
5332
+ this.updateHighlight();
5333
+ break;
5334
+ case "ArrowUp":
5335
+ event.preventDefault();
5336
+ this.highlightedIndex = Math.max(this.highlightedIndex - 1, 0);
5337
+ this.updateHighlight();
5338
+ break;
5339
+ case "Enter":
5340
+ event.preventDefault();
5341
+ if (this.highlightedIndex >= 0 && this.highlightedIndex < items.length) {
5342
+ this.selectItem(items[this.highlightedIndex]);
5343
+ }
5344
+ break;
5345
+ case "Escape":
5346
+ event.preventDefault();
5347
+ this.close();
5348
+ break;
5349
+ }
5350
+ }
5351
+ select(event) {
5352
+ const el = event.target.closest(".command-palette-item");
5353
+ if (!el)
5354
+ return;
5355
+ const index = parseInt(el.dataset.index || "-1", 10);
5356
+ const items = this.visibleItems;
5357
+ if (index >= 0 && index < items.length) {
5358
+ this.selectItem(items[index]);
5359
+ }
5360
+ }
5361
+ // ── Remote search ───────────────────────────────────────────────────
5362
+ fetchRemote(query) {
5363
+ if (this.fetchAbort)
5364
+ this.fetchAbort.abort();
5365
+ if (this.searchTimer)
5366
+ clearTimeout(this.searchTimer);
5367
+ this.searchTimer = window.setTimeout(async () => {
5368
+ this.fetchAbort = new AbortController();
5369
+ try {
5370
+ const url = new URL(this.srcValue, window.location.origin);
5371
+ url.searchParams.set("q", query);
5372
+ const response = await fetch(url.toString(), {
5373
+ headers: { Accept: "application/json" },
5374
+ signal: this.fetchAbort.signal
5375
+ });
5376
+ if (response.ok) {
5377
+ const data = await response.json();
5378
+ this.renderItems(data);
5379
+ }
5380
+ } catch (e) {
5381
+ if (e.name !== "AbortError")
5382
+ throw e;
5383
+ }
5384
+ }, 200);
5385
+ }
5386
+ // ── Turbo frame navigation ──────────────────────────────────────────
5387
+ navigateFrame(query) {
5388
+ if (!this.hasTurboListTarget)
5389
+ return;
5390
+ const url = new URL(this.turboFrameUrlValue, window.location.href);
5391
+ if (query)
5392
+ url.searchParams.set("q", query);
5393
+ this.turboListTarget.setAttribute("src", url.toString());
5394
+ }
5395
+ debouncedNavigateFrame(query) {
5396
+ if (this.searchTimer)
5397
+ clearTimeout(this.searchTimer);
5398
+ this.searchTimer = window.setTimeout(() => {
5399
+ this.navigateFrame(query);
5400
+ }, 200);
5401
+ }
5402
+ // ── Rendering ───────────────────────────────────────────────────────
5403
+ renderItems(items) {
5404
+ if (!this.hasListTarget)
5405
+ return;
5406
+ this.listTarget.innerHTML = "";
5407
+ this.highlightedIndex = -1;
5408
+ if (items.length === 0) {
5409
+ this.emptyTarget.hidden = false;
5410
+ this.listTarget.hidden = true;
5411
+ return;
5412
+ }
5413
+ this.emptyTarget.hidden = true;
5414
+ this.listTarget.hidden = false;
5415
+ const groups = /* @__PURE__ */ new Map();
5416
+ for (const item of items) {
5417
+ const group = item.group || "";
5418
+ if (!groups.has(group))
5419
+ groups.set(group, []);
5420
+ groups.get(group).push(item);
5421
+ }
5422
+ let index = 0;
5423
+ for (const [groupName, groupItems] of groups) {
5424
+ if (groupName) {
5425
+ this.listTarget.appendChild(this.buildGroup(groupName));
5426
+ }
5427
+ for (const item of groupItems) {
5428
+ this.listTarget.appendChild(this.buildItem(item, index));
5429
+ index++;
5430
+ }
5431
+ }
5432
+ if (items.length > 0) {
5433
+ this.highlightedIndex = 0;
5434
+ this.updateHighlight();
5435
+ }
5436
+ }
5437
+ buildGroup(name) {
5438
+ const li = cloneTemplate(this.groupTemplateTarget);
5439
+ const h6 = li.querySelector(".midwest-label");
5440
+ if (h6)
5441
+ h6.textContent = name;
5442
+ return li;
5443
+ }
5444
+ buildItem(item, index) {
5445
+ const li = cloneTemplate(this.itemTemplateTarget);
5446
+ li.dataset.index = String(index);
5447
+ if (item.url)
5448
+ li.dataset.url = item.url;
5449
+ if (item.action)
5450
+ li.dataset.paletteAction = item.action;
5451
+ if (item.method)
5452
+ li.dataset.turboMethod = item.method;
5453
+ if (item.confirm)
5454
+ li.dataset.turboConfirm = item.confirm;
5455
+ const iconSpan = li.querySelector(".command-palette-item-icon");
5456
+ if (item.icon) {
5457
+ iconSpan.innerHTML = this.getIconSvg(item.icon);
5458
+ iconSpan.hidden = false;
5459
+ } else {
5460
+ iconSpan.remove();
5461
+ }
5462
+ const label = li.querySelector(".command-palette-item-label");
5463
+ label.textContent = item.label;
5464
+ const desc = li.querySelector(".command-palette-item-description");
5465
+ if (item.description) {
5466
+ desc.textContent = item.description;
5467
+ desc.hidden = false;
5468
+ } else {
5469
+ desc.remove();
5470
+ }
5471
+ const shortcutSpan = li.querySelector(".command-palette-item-shortcut");
5472
+ if (item.shortcut) {
5473
+ item.shortcut.split("+").forEach((key) => {
5474
+ const kbd = document.createElement("kbd");
5475
+ kbd.className = "midwest-kbd";
5476
+ kbd.textContent = key.trim();
5477
+ shortcutSpan.appendChild(kbd);
5478
+ });
5479
+ shortcutSpan.hidden = false;
5480
+ } else {
5481
+ shortcutSpan.remove();
5482
+ }
5483
+ return li;
5484
+ }
5485
+ // ── Selection ───────────────────────────────────────────────────────
5486
+ selectItem(item) {
5487
+ if (item.confirm && !window.confirm(item.confirm))
5488
+ return;
5489
+ this.close();
5490
+ if (item.url) {
5491
+ this.visit(item.url, item.method);
5492
+ return;
5493
+ }
5494
+ if (item.action) {
5495
+ this.element.dispatchEvent(
5496
+ new CustomEvent("midwest-command-palette:action", {
5497
+ bubbles: true,
5498
+ detail: { action: item.action, item }
5499
+ })
5500
+ );
5501
+ }
5502
+ }
5503
+ visit(url, method) {
5504
+ if (method && method.toLowerCase() !== "get") {
5505
+ const link = document.createElement("a");
5506
+ link.href = url;
5507
+ link.dataset.turboMethod = method;
5508
+ link.style.display = "none";
5509
+ document.body.appendChild(link);
5510
+ link.click();
5511
+ link.remove();
5512
+ return;
5513
+ }
5514
+ const turbo = window.Turbo;
5515
+ if (turbo?.visit) {
5516
+ turbo.visit(url);
5517
+ } else {
5518
+ window.location.href = url;
5519
+ }
5520
+ }
5521
+ // ── Highlight ───────────────────────────────────────────────────────
5522
+ get visibleItems() {
5523
+ if (!this.hasListTarget)
5524
+ return [];
5525
+ const items = [];
5526
+ const els = this.listTarget.querySelectorAll(".command-palette-item");
5527
+ for (const el of els) {
5528
+ items.push({
5529
+ label: el.querySelector(".command-palette-item-label")?.textContent || "",
5530
+ url: el.dataset.url,
5531
+ action: el.dataset.paletteAction,
5532
+ method: el.dataset.turboMethod,
5533
+ confirm: el.dataset.turboConfirm
5534
+ });
5535
+ }
5536
+ return items;
5537
+ }
5538
+ updateHighlight() {
5539
+ if (!this.hasListTarget)
5540
+ return;
5541
+ const items = this.listTarget.querySelectorAll(".command-palette-item");
5542
+ items.forEach((el, i) => {
5543
+ el.setAttribute("aria-selected", String(i === this.highlightedIndex));
5544
+ });
5545
+ const highlighted = items[this.highlightedIndex];
5546
+ if (highlighted) {
5547
+ highlighted.scrollIntoView({ block: "nearest" });
5548
+ this.inputTarget.setAttribute("aria-activedescendant", highlighted.id || "");
5549
+ }
5550
+ }
5551
+ // ── Hotkey ──────────────────────────────────────────────────────────
5552
+ handleGlobalKeydown = (event) => {
5553
+ if ((event.metaKey || event.ctrlKey) && event.key === this.hotkeyValue) {
5554
+ event.preventDefault();
5555
+ if (this.element.open) {
5556
+ this.close();
5557
+ } else {
5558
+ this.open();
5559
+ }
5560
+ }
5561
+ };
5562
+ handleBackdropClick = (event) => {
5563
+ if (event.target === this.element)
5564
+ this.close();
5565
+ };
5566
+ // ── Helpers ─────────────────────────────────────────────────────────
5567
+ getIconSvg(name) {
5568
+ const existing = document.querySelector(`.Midwest-Icon__Svg[aria-label="${name}"]`);
5569
+ if (existing)
5570
+ return existing.outerHTML;
5571
+ const allIcons = document.querySelectorAll(".Midwest-Icon__Svg");
5572
+ for (const icon of allIcons) {
5573
+ if (icon.getAttribute("aria-label")?.toLowerCase() === name.toLowerCase()) {
5574
+ return icon.outerHTML;
5575
+ }
5576
+ }
5577
+ return "";
5578
+ }
5579
+ }
5580
+
4861
5581
  function registerMidwestControllers(application) {
4862
5582
  application.register("midwest-card", Card);
4863
5583
  application.register("midwest-banner", Banner);
@@ -4885,6 +5605,10 @@ function registerMidwestControllers(application) {
4885
5605
  application.register("midwest-horizontal-scroll", HorizontalScroll);
4886
5606
  application.register("midwest-table", Table);
4887
5607
  application.register("midwest-load-more", LoadMore);
5608
+ application.register("midwest-pagination", Pagination);
5609
+ application.register("midwest-show-if", ShowIf);
5610
+ application.register("midwest-tree-view", TreeView);
5611
+ application.register("midwest-command-palette", CommandPalette);
4888
5612
  }
4889
5613
 
4890
5614
  export { Banner, Card, Chart, CountdownTimer, registerMidwestControllers };