@unabridged/midwest 0.25.0 → 0.26.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.
- package/app/assets/javascript/midwest/index.ts +6 -0
- package/app/assets/javascript/midwest.js +605 -21
- package/app/assets/javascript/midwest.js.map +1 -1
- package/app/assets/stylesheets/midwest.css +1 -1
- package/app/assets/stylesheets/midwest.tailwind.css +6 -1
- package/dist/css/midwest.css +1 -1
- package/dist/javascript/collection/app/assets/javascript/midwest/index.js +6 -0
- package/dist/javascript/collection/app/assets/javascript/midwest/index.js.map +1 -1
- package/dist/javascript/collection/app/components/midwest/assets_component/assets_component_controller.js +47 -0
- package/dist/javascript/collection/app/components/midwest/assets_component/assets_component_controller.js.map +1 -0
- package/dist/javascript/collection/app/components/midwest/chart_component/chart_component_controller.js +122 -2
- package/dist/javascript/collection/app/components/midwest/chart_component/chart_component_controller.js.map +1 -1
- package/dist/javascript/collection/app/components/midwest/context_menu_component/context_menu_component_controller.js +71 -0
- package/dist/javascript/collection/app/components/midwest/context_menu_component/context_menu_component_controller.js.map +1 -0
- package/dist/javascript/collection/app/components/midwest/dialog_component/dialog_component_controller.js +33 -1
- package/dist/javascript/collection/app/components/midwest/dialog_component/dialog_component_controller.js.map +1 -1
- package/dist/javascript/collection/app/components/midwest/split_view_component/split_view_component_controller.js +99 -0
- package/dist/javascript/collection/app/components/midwest/split_view_component/split_view_component_controller.js.map +1 -0
- package/dist/javascript/collection/app/components/midwest/table_component/table_component_controller.js +239 -15
- package/dist/javascript/collection/app/components/midwest/table_component/table_component_controller.js.map +1 -1
- package/dist/javascript/midwest.d.ts +16 -1
- package/dist/javascript/midwest.js +605 -21
- package/dist/javascript/midwest.js.map +1 -1
- package/package.json +1 -1
|
@@ -2688,13 +2688,18 @@ class Prose extends Controller {
|
|
|
2688
2688
|
class Dialog extends Controller {
|
|
2689
2689
|
static targets = ["remoteContent", "clearableForm"];
|
|
2690
2690
|
static values = {
|
|
2691
|
-
resetFrame: { type: Boolean, default: false }
|
|
2691
|
+
resetFrame: { type: Boolean, default: false },
|
|
2692
|
+
history: { type: Boolean, default: false }
|
|
2692
2693
|
};
|
|
2693
2694
|
_previousFocus = null;
|
|
2695
|
+
// True while a close is being driven by a history navigation (Back button),
|
|
2696
|
+
// so onClose knows not to push another history entry in response.
|
|
2697
|
+
_closingViaHistory = false;
|
|
2694
2698
|
connect() {
|
|
2695
2699
|
document.addEventListener("turbo:frame-load", this.bindToggleEls);
|
|
2696
2700
|
document.addEventListener("turbo:before-visit", this.dismiss);
|
|
2697
2701
|
document.addEventListener("click", this.handleClickOutside);
|
|
2702
|
+
window.addEventListener("popstate", this.handlePopState);
|
|
2698
2703
|
this.dialog.addEventListener("close", this.onClose);
|
|
2699
2704
|
this.bindToggleEls();
|
|
2700
2705
|
}
|
|
@@ -2702,8 +2707,18 @@ class Dialog extends Controller {
|
|
|
2702
2707
|
document.removeEventListener("turbo:frame-load", this.bindToggleEls);
|
|
2703
2708
|
document.removeEventListener("turbo:before-visit", this.dismiss);
|
|
2704
2709
|
document.removeEventListener("click", this.handleClickOutside);
|
|
2710
|
+
window.removeEventListener("popstate", this.handlePopState);
|
|
2705
2711
|
this.dialog.removeEventListener("close", this.onClose);
|
|
2706
2712
|
}
|
|
2713
|
+
// History-aware modals: a Back navigation (popstate) closes the open dialog.
|
|
2714
|
+
// The flag tells onClose this close came from history, so it must not push
|
|
2715
|
+
// another entry back onto the stack.
|
|
2716
|
+
handlePopState = () => {
|
|
2717
|
+
if (!this.historyValue || !this.dialog.open)
|
|
2718
|
+
return;
|
|
2719
|
+
this._closingViaHistory = true;
|
|
2720
|
+
this.dialog.close();
|
|
2721
|
+
};
|
|
2707
2722
|
get dialog() {
|
|
2708
2723
|
return this.element;
|
|
2709
2724
|
}
|
|
@@ -2755,6 +2770,8 @@ class Dialog extends Controller {
|
|
|
2755
2770
|
if (!detail.success)
|
|
2756
2771
|
return;
|
|
2757
2772
|
const redirectUrl = detail.fetchResponse?.location?.href;
|
|
2773
|
+
if (redirectUrl)
|
|
2774
|
+
this._closingViaHistory = true;
|
|
2758
2775
|
this.dismiss();
|
|
2759
2776
|
if (redirectUrl)
|
|
2760
2777
|
window.Turbo?.visit(redirectUrl);
|
|
@@ -2796,10 +2813,25 @@ class Dialog extends Controller {
|
|
|
2796
2813
|
this._previousFocus?.focus();
|
|
2797
2814
|
this._previousFocus = null;
|
|
2798
2815
|
this.htmlEl.classList.toggle("overflow-hidden", false);
|
|
2816
|
+
this.restoreHistory();
|
|
2799
2817
|
await this.delay(500);
|
|
2800
2818
|
this.resetForm();
|
|
2801
2819
|
this.resetFrame();
|
|
2802
2820
|
};
|
|
2821
|
+
// When a history-aware modal is closed manually (Escape, backdrop, close
|
|
2822
|
+
// button), pop the modal's advanced URL off the stack so the address bar
|
|
2823
|
+
// returns to the page behind it. A close driven by the Back button already
|
|
2824
|
+
// moved history, so it is skipped.
|
|
2825
|
+
restoreHistory() {
|
|
2826
|
+
if (!this.historyValue)
|
|
2827
|
+
return;
|
|
2828
|
+
if (this._closingViaHistory) {
|
|
2829
|
+
this._closingViaHistory = false;
|
|
2830
|
+
return;
|
|
2831
|
+
}
|
|
2832
|
+
if (window.history.length > 1)
|
|
2833
|
+
window.history.back();
|
|
2834
|
+
}
|
|
2803
2835
|
resetFrame() {
|
|
2804
2836
|
if (!this.resetFrameValue || this.frameEl == null)
|
|
2805
2837
|
return;
|
|
@@ -3708,8 +3740,29 @@ class ConfirmationController extends Controller {
|
|
|
3708
3740
|
}
|
|
3709
3741
|
|
|
3710
3742
|
class Chart extends Controller {
|
|
3711
|
-
static targets = ["tooltip", "svg"];
|
|
3712
|
-
static values = {
|
|
3743
|
+
static targets = ["tooltip", "svg", "data"];
|
|
3744
|
+
static values = {
|
|
3745
|
+
tooltipEnabled: { type: Boolean, default: true },
|
|
3746
|
+
filename: { type: String, default: "chart" }
|
|
3747
|
+
};
|
|
3748
|
+
// Presentation properties copied inline when serializing the SVG, so exported
|
|
3749
|
+
// files render faithfully without the external component stylesheet.
|
|
3750
|
+
static STYLE_PROPS = [
|
|
3751
|
+
"fill",
|
|
3752
|
+
"stroke",
|
|
3753
|
+
"stroke-width",
|
|
3754
|
+
"stroke-dasharray",
|
|
3755
|
+
"stroke-linecap",
|
|
3756
|
+
"stroke-linejoin",
|
|
3757
|
+
"opacity",
|
|
3758
|
+
"fill-opacity",
|
|
3759
|
+
"stroke-opacity",
|
|
3760
|
+
"font-family",
|
|
3761
|
+
"font-size",
|
|
3762
|
+
"font-weight",
|
|
3763
|
+
"text-anchor",
|
|
3764
|
+
"color"
|
|
3765
|
+
];
|
|
3713
3766
|
connect() {
|
|
3714
3767
|
if (this.tooltipEnabledValue && this.hasTooltipTarget) {
|
|
3715
3768
|
this.attachListeners();
|
|
@@ -3789,6 +3842,105 @@ class Chart extends Controller {
|
|
|
3789
3842
|
this.tooltipTarget.style.left = `${left}px`;
|
|
3790
3843
|
this.tooltipTarget.style.top = `${top}px`;
|
|
3791
3844
|
}
|
|
3845
|
+
// --- Export actions -------------------------------------------------------
|
|
3846
|
+
// Download the chart as a standalone SVG file (styles inlined).
|
|
3847
|
+
exportSvg() {
|
|
3848
|
+
if (!this.hasSvgTarget)
|
|
3849
|
+
return;
|
|
3850
|
+
const blob = new Blob([this.serializeSvg()], { type: "image/svg+xml;charset=utf-8" });
|
|
3851
|
+
this.downloadBlob(blob, `${this.filenameValue}.svg`);
|
|
3852
|
+
}
|
|
3853
|
+
// Rasterize the chart to a PNG via an offscreen canvas (2× for crisp output).
|
|
3854
|
+
exportPng() {
|
|
3855
|
+
if (!this.hasSvgTarget)
|
|
3856
|
+
return;
|
|
3857
|
+
const box = this.svgTarget.viewBox.baseVal;
|
|
3858
|
+
const rect = this.svgTarget.getBoundingClientRect();
|
|
3859
|
+
const width = Math.max(1, Math.round(rect.width || box.width));
|
|
3860
|
+
const height = Math.max(1, Math.round(rect.height || box.height));
|
|
3861
|
+
const scale = 2;
|
|
3862
|
+
const blob = new Blob([this.serializeSvg()], { type: "image/svg+xml;charset=utf-8" });
|
|
3863
|
+
const url = URL.createObjectURL(blob);
|
|
3864
|
+
const img = new Image();
|
|
3865
|
+
img.onload = () => {
|
|
3866
|
+
const canvas = document.createElement("canvas");
|
|
3867
|
+
canvas.width = width * scale;
|
|
3868
|
+
canvas.height = height * scale;
|
|
3869
|
+
const ctx = canvas.getContext("2d");
|
|
3870
|
+
if (ctx == null) {
|
|
3871
|
+
URL.revokeObjectURL(url);
|
|
3872
|
+
return;
|
|
3873
|
+
}
|
|
3874
|
+
ctx.fillStyle = "#ffffff";
|
|
3875
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
3876
|
+
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
|
3877
|
+
URL.revokeObjectURL(url);
|
|
3878
|
+
canvas.toBlob((png) => {
|
|
3879
|
+
if (png != null)
|
|
3880
|
+
this.downloadBlob(png, `${this.filenameValue}.png`);
|
|
3881
|
+
}, "image/png");
|
|
3882
|
+
};
|
|
3883
|
+
img.onerror = () => {
|
|
3884
|
+
URL.revokeObjectURL(url);
|
|
3885
|
+
};
|
|
3886
|
+
img.src = url;
|
|
3887
|
+
}
|
|
3888
|
+
// Download the underlying data (chart.data_table) as a CSV file.
|
|
3889
|
+
exportCsv() {
|
|
3890
|
+
if (!this.hasDataTarget)
|
|
3891
|
+
return;
|
|
3892
|
+
const rows = JSON.parse(this.dataTarget.dataset.rows ?? "[]");
|
|
3893
|
+
const csv = rows.map((row) => row.map((cell) => this.csvCell(cell)).join(",")).join("\r\n");
|
|
3894
|
+
const blob = new Blob([`\uFEFF${csv}`], { type: "text/csv;charset=utf-8" });
|
|
3895
|
+
this.downloadBlob(blob, `${this.filenameValue}.csv`);
|
|
3896
|
+
}
|
|
3897
|
+
csvCell(value) {
|
|
3898
|
+
if (value == null)
|
|
3899
|
+
return "";
|
|
3900
|
+
const str = String(value);
|
|
3901
|
+
return /["\n\r,]/.test(str) ? `"${str.replace(/"/g, '""')}"` : str;
|
|
3902
|
+
}
|
|
3903
|
+
// Clone the live SVG, inline computed presentation styles onto every node, and
|
|
3904
|
+
// serialize to a standalone XML string usable as a file or a canvas source.
|
|
3905
|
+
serializeSvg() {
|
|
3906
|
+
const clone = this.svgTarget.cloneNode(true);
|
|
3907
|
+
this.inlineStyles(this.svgTarget, clone);
|
|
3908
|
+
clone.setAttribute("xmlns", "http://www.w3.org/2000/svg");
|
|
3909
|
+
clone.setAttribute("xmlns:xlink", "http://www.w3.org/1999/xlink");
|
|
3910
|
+
const box = this.svgTarget.viewBox.baseVal;
|
|
3911
|
+
if (box?.width) {
|
|
3912
|
+
clone.setAttribute("width", String(box.width));
|
|
3913
|
+
clone.setAttribute("height", String(box.height));
|
|
3914
|
+
}
|
|
3915
|
+
const xml = new XMLSerializer().serializeToString(clone);
|
|
3916
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
3917
|
+
${xml}`;
|
|
3918
|
+
}
|
|
3919
|
+
// Copy the whitelisted computed styles from each source node to the matching
|
|
3920
|
+
// clone node. The trees are structurally identical, so index alignment holds.
|
|
3921
|
+
inlineStyles(source, target) {
|
|
3922
|
+
const sourceEls = [source, ...Array.from(source.querySelectorAll("*"))];
|
|
3923
|
+
const targetEls = [target, ...Array.from(target.querySelectorAll("*"))];
|
|
3924
|
+
sourceEls.forEach((el, i) => {
|
|
3925
|
+
const computed = window.getComputedStyle(el);
|
|
3926
|
+
const decls = Chart.STYLE_PROPS.map((prop) => {
|
|
3927
|
+
const value = computed.getPropertyValue(prop);
|
|
3928
|
+
return value ? `${prop}:${value}` : null;
|
|
3929
|
+
}).filter((decl) => decl !== null);
|
|
3930
|
+
if (decls.length > 0)
|
|
3931
|
+
targetEls[i].setAttribute("style", decls.join(";"));
|
|
3932
|
+
});
|
|
3933
|
+
}
|
|
3934
|
+
downloadBlob(blob, filename) {
|
|
3935
|
+
const url = URL.createObjectURL(blob);
|
|
3936
|
+
const link = document.createElement("a");
|
|
3937
|
+
link.href = url;
|
|
3938
|
+
link.download = filename;
|
|
3939
|
+
document.body.appendChild(link);
|
|
3940
|
+
link.click();
|
|
3941
|
+
link.remove();
|
|
3942
|
+
setTimeout(() => URL.revokeObjectURL(url), 1e3);
|
|
3943
|
+
}
|
|
3792
3944
|
}
|
|
3793
3945
|
|
|
3794
3946
|
class Range extends Controller {
|
|
@@ -4777,7 +4929,7 @@ var MMCQ = function() {
|
|
|
4777
4929
|
};
|
|
4778
4930
|
}();
|
|
4779
4931
|
|
|
4780
|
-
class Image extends Controller {
|
|
4932
|
+
let Image$1 = class Image extends Controller {
|
|
4781
4933
|
static targets = ["dialog"];
|
|
4782
4934
|
static values = { poster: String };
|
|
4783
4935
|
connect() {
|
|
@@ -4817,7 +4969,7 @@ class Image extends Controller {
|
|
|
4817
4969
|
this.dialogTarget.close();
|
|
4818
4970
|
}
|
|
4819
4971
|
}
|
|
4820
|
-
}
|
|
4972
|
+
};
|
|
4821
4973
|
|
|
4822
4974
|
class ColorPicker extends Controller {
|
|
4823
4975
|
static targets = ["input", "swatch", "customSwatch", "colorInput"];
|
|
@@ -5013,16 +5165,69 @@ class HorizontalScroll extends Controller {
|
|
|
5013
5165
|
}
|
|
5014
5166
|
|
|
5015
5167
|
class Table extends Controller {
|
|
5016
|
-
static targets = ["selectAll", "rowCheckbox"];
|
|
5168
|
+
static targets = ["selectAll", "rowCheckbox", "editable"];
|
|
5169
|
+
static values = {
|
|
5170
|
+
resizable: { type: Boolean, default: false },
|
|
5171
|
+
resizeStorageKey: { type: String, default: "" }
|
|
5172
|
+
};
|
|
5173
|
+
static MIN_COLUMN_WIDTH = 48;
|
|
5174
|
+
// Active resize gesture state.
|
|
5175
|
+
_resizeTh = null;
|
|
5176
|
+
_resizeStartX = 0;
|
|
5177
|
+
_resizeStartWidth = 0;
|
|
5178
|
+
connect() {
|
|
5179
|
+
if (this.resizableValue)
|
|
5180
|
+
this.initResize();
|
|
5181
|
+
}
|
|
5182
|
+
// ─── selection ─────────────────────────────────────────────────────────────
|
|
5017
5183
|
toggleAll() {
|
|
5018
5184
|
const checked = this.selectAllTarget.checked;
|
|
5019
5185
|
this.rowCheckboxTargets.forEach((cb) => {
|
|
5020
5186
|
cb.checked = checked;
|
|
5021
5187
|
});
|
|
5022
5188
|
}
|
|
5189
|
+
updateSelectAll() {
|
|
5190
|
+
if (!this.hasSelectAllTarget)
|
|
5191
|
+
return;
|
|
5192
|
+
const total = this.rowCheckboxTargets.length;
|
|
5193
|
+
const checkedCount = this.rowCheckboxTargets.filter((cb) => cb.checked).length;
|
|
5194
|
+
if (checkedCount === 0) {
|
|
5195
|
+
this.selectAllTarget.checked = false;
|
|
5196
|
+
this.selectAllTarget.indeterminate = false;
|
|
5197
|
+
} else if (checkedCount === total) {
|
|
5198
|
+
this.selectAllTarget.checked = true;
|
|
5199
|
+
this.selectAllTarget.indeterminate = false;
|
|
5200
|
+
} else {
|
|
5201
|
+
this.selectAllTarget.checked = false;
|
|
5202
|
+
this.selectAllTarget.indeterminate = true;
|
|
5203
|
+
}
|
|
5204
|
+
}
|
|
5205
|
+
// Shift-clicking a sort header extends the multi-column sort chain instead of
|
|
5206
|
+
// replacing it: follow the header's data-multi-sort-url. A plain click falls
|
|
5207
|
+
// through to the anchor's single-sort href. Navigation stays frame-scoped by
|
|
5208
|
+
// driving the enclosing <turbo-frame>'s src when present.
|
|
5209
|
+
sortColumn(event) {
|
|
5210
|
+
if (!event.shiftKey)
|
|
5211
|
+
return;
|
|
5212
|
+
const link = event.currentTarget;
|
|
5213
|
+
const url = link.dataset.multiSortUrl;
|
|
5214
|
+
if (!url)
|
|
5215
|
+
return;
|
|
5216
|
+
event.preventDefault();
|
|
5217
|
+
const frame = link.closest("turbo-frame");
|
|
5218
|
+
if (frame != null) {
|
|
5219
|
+
frame.src = url;
|
|
5220
|
+
} else {
|
|
5221
|
+
const Turbo = window.Turbo;
|
|
5222
|
+
if (Turbo?.visit)
|
|
5223
|
+
Turbo.visit(url);
|
|
5224
|
+
else
|
|
5225
|
+
window.location.href = url;
|
|
5226
|
+
}
|
|
5227
|
+
}
|
|
5023
5228
|
navigateRow(event) {
|
|
5024
5229
|
const target = event.target;
|
|
5025
|
-
if (target.closest('a, button, input, select, textarea, [role="button"]'))
|
|
5230
|
+
if (target.closest('a, button, input, select, textarea, [role="button"], .editable'))
|
|
5026
5231
|
return;
|
|
5027
5232
|
const row = event.currentTarget;
|
|
5028
5233
|
const href = row.dataset.href;
|
|
@@ -5039,22 +5244,193 @@ class Table extends Controller {
|
|
|
5039
5244
|
}
|
|
5040
5245
|
}
|
|
5041
5246
|
}
|
|
5042
|
-
|
|
5043
|
-
|
|
5247
|
+
// ─── column resizing ────────────────────────────────────────────────────────
|
|
5248
|
+
// Freeze the current (auto-laid-out) column widths as explicit values, switch
|
|
5249
|
+
// to a fixed layout so drags don't reflow other columns, then apply any widths
|
|
5250
|
+
// saved from a previous session.
|
|
5251
|
+
initResize() {
|
|
5252
|
+
const headers = this.headerCells();
|
|
5253
|
+
if (headers.length === 0)
|
|
5044
5254
|
return;
|
|
5045
|
-
|
|
5046
|
-
|
|
5047
|
-
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
|
|
5051
|
-
|
|
5052
|
-
|
|
5053
|
-
|
|
5054
|
-
|
|
5055
|
-
|
|
5255
|
+
headers.forEach((th) => {
|
|
5256
|
+
th.style.width = `${th.offsetWidth}px`;
|
|
5257
|
+
});
|
|
5258
|
+
this.table?.classList.add("is-fixed");
|
|
5259
|
+
this.restoreWidths();
|
|
5260
|
+
}
|
|
5261
|
+
startResize(event) {
|
|
5262
|
+
const handle = event.target;
|
|
5263
|
+
const th = handle.closest("th");
|
|
5264
|
+
if (th == null)
|
|
5265
|
+
return;
|
|
5266
|
+
event.preventDefault();
|
|
5267
|
+
this._resizeTh = th;
|
|
5268
|
+
this._resizeStartX = event.clientX;
|
|
5269
|
+
this._resizeStartWidth = th.offsetWidth;
|
|
5270
|
+
this.element.classList.add("is-resizing");
|
|
5271
|
+
window.addEventListener("pointermove", this.onResizeMove);
|
|
5272
|
+
window.addEventListener("pointerup", this.onResizeEnd);
|
|
5273
|
+
}
|
|
5274
|
+
// Double-clicking a handle clears that column's explicit width so it auto-sizes
|
|
5275
|
+
// to its content again.
|
|
5276
|
+
autoSizeColumn(event) {
|
|
5277
|
+
const th = event.target.closest("th");
|
|
5278
|
+
if (th == null)
|
|
5279
|
+
return;
|
|
5280
|
+
th.style.removeProperty("width");
|
|
5281
|
+
this.persistWidths();
|
|
5282
|
+
}
|
|
5283
|
+
onResizeMove = (event) => {
|
|
5284
|
+
if (this._resizeTh == null)
|
|
5285
|
+
return;
|
|
5286
|
+
const delta = event.clientX - this._resizeStartX;
|
|
5287
|
+
const width = Math.max(Table.MIN_COLUMN_WIDTH, this._resizeStartWidth + delta);
|
|
5288
|
+
this._resizeTh.style.width = `${width}px`;
|
|
5289
|
+
};
|
|
5290
|
+
onResizeEnd = () => {
|
|
5291
|
+
this._resizeTh = null;
|
|
5292
|
+
this.element.classList.remove("is-resizing");
|
|
5293
|
+
window.removeEventListener("pointermove", this.onResizeMove);
|
|
5294
|
+
window.removeEventListener("pointerup", this.onResizeEnd);
|
|
5295
|
+
this.persistWidths();
|
|
5296
|
+
};
|
|
5297
|
+
persistWidths() {
|
|
5298
|
+
if (!this.resizeStorageKeyValue)
|
|
5299
|
+
return;
|
|
5300
|
+
const widths = {};
|
|
5301
|
+
this.headerCells().forEach((th) => {
|
|
5302
|
+
const index = th.dataset.colIndex;
|
|
5303
|
+
if (index != null && th.style.width)
|
|
5304
|
+
widths[index] = th.style.width;
|
|
5305
|
+
});
|
|
5306
|
+
try {
|
|
5307
|
+
window.localStorage.setItem(this.storageKey, JSON.stringify(widths));
|
|
5308
|
+
} catch {
|
|
5056
5309
|
}
|
|
5057
5310
|
}
|
|
5311
|
+
restoreWidths() {
|
|
5312
|
+
if (!this.resizeStorageKeyValue)
|
|
5313
|
+
return;
|
|
5314
|
+
let widths = {};
|
|
5315
|
+
try {
|
|
5316
|
+
widths = JSON.parse(window.localStorage.getItem(this.storageKey) ?? "{}");
|
|
5317
|
+
} catch {
|
|
5318
|
+
return;
|
|
5319
|
+
}
|
|
5320
|
+
this.headerCells().forEach((th) => {
|
|
5321
|
+
const saved = th.dataset.colIndex != null ? widths[th.dataset.colIndex] : void 0;
|
|
5322
|
+
if (saved)
|
|
5323
|
+
th.style.width = saved;
|
|
5324
|
+
});
|
|
5325
|
+
}
|
|
5326
|
+
get storageKey() {
|
|
5327
|
+
return `midwest-table:${this.resizeStorageKeyValue}`;
|
|
5328
|
+
}
|
|
5329
|
+
headerCells() {
|
|
5330
|
+
return Array.from(this.element.querySelectorAll("thead th[data-col-index]"));
|
|
5331
|
+
}
|
|
5332
|
+
// ─── inline editing ─────────────────────────────────────────────────────────
|
|
5333
|
+
// Start editing when Enter or F2 is pressed on a focused editable cell. Ignore
|
|
5334
|
+
// keydowns bubbling up from the edit input itself (its own handler owns those).
|
|
5335
|
+
editCellKeydown(event) {
|
|
5336
|
+
if (event.target !== event.currentTarget)
|
|
5337
|
+
return;
|
|
5338
|
+
if (event.key !== "Enter" && event.key !== "F2")
|
|
5339
|
+
return;
|
|
5340
|
+
const cell = event.currentTarget;
|
|
5341
|
+
if (cell.dataset.editing === "true")
|
|
5342
|
+
return;
|
|
5343
|
+
event.preventDefault();
|
|
5344
|
+
this.startEditing(cell);
|
|
5345
|
+
}
|
|
5346
|
+
editCell(event) {
|
|
5347
|
+
this.startEditing(event.currentTarget);
|
|
5348
|
+
}
|
|
5349
|
+
startEditing(cell) {
|
|
5350
|
+
if (cell.dataset.editing === "true")
|
|
5351
|
+
return;
|
|
5352
|
+
cell.dataset.editing = "true";
|
|
5353
|
+
const input = document.createElement("input");
|
|
5354
|
+
input.type = "text";
|
|
5355
|
+
input.className = "midwest-table-edit-input";
|
|
5356
|
+
input.value = cell.dataset.value ?? cell.textContent?.trim() ?? "";
|
|
5357
|
+
cell.textContent = "";
|
|
5358
|
+
cell.appendChild(input);
|
|
5359
|
+
input.focus();
|
|
5360
|
+
input.select();
|
|
5361
|
+
input.addEventListener("keydown", (e) => {
|
|
5362
|
+
if (e.key === "Enter") {
|
|
5363
|
+
e.preventDefault();
|
|
5364
|
+
this.commitEdit(cell, input);
|
|
5365
|
+
} else if (e.key === "Escape") {
|
|
5366
|
+
e.preventDefault();
|
|
5367
|
+
this.cancelEdit(cell);
|
|
5368
|
+
}
|
|
5369
|
+
});
|
|
5370
|
+
input.addEventListener("blur", () => {
|
|
5371
|
+
this.commitEdit(cell, input);
|
|
5372
|
+
});
|
|
5373
|
+
}
|
|
5374
|
+
commitEdit(cell, input) {
|
|
5375
|
+
if (cell.dataset.editing !== "true")
|
|
5376
|
+
return;
|
|
5377
|
+
const newValue = input.value;
|
|
5378
|
+
const previousValue = cell.dataset.value ?? "";
|
|
5379
|
+
this.finishEditing(cell, newValue);
|
|
5380
|
+
if (newValue === previousValue)
|
|
5381
|
+
return;
|
|
5382
|
+
this.dispatch("edit", {
|
|
5383
|
+
bubbles: true,
|
|
5384
|
+
detail: {
|
|
5385
|
+
field: cell.dataset.field,
|
|
5386
|
+
value: newValue,
|
|
5387
|
+
previousValue,
|
|
5388
|
+
rowId: cell.closest("tr")?.dataset.rowId,
|
|
5389
|
+
cell
|
|
5390
|
+
}
|
|
5391
|
+
});
|
|
5392
|
+
this.persistEdit(cell, newValue);
|
|
5393
|
+
}
|
|
5394
|
+
cancelEdit(cell) {
|
|
5395
|
+
if (cell.dataset.editing !== "true")
|
|
5396
|
+
return;
|
|
5397
|
+
this.finishEditing(cell, cell.dataset.value ?? "");
|
|
5398
|
+
}
|
|
5399
|
+
// Replace the input with the resolved text, record the value, and return
|
|
5400
|
+
// focus to the cell for keyboard users.
|
|
5401
|
+
finishEditing(cell, value) {
|
|
5402
|
+
cell.dataset.editing = "false";
|
|
5403
|
+
cell.dataset.value = value;
|
|
5404
|
+
cell.textContent = value;
|
|
5405
|
+
cell.focus();
|
|
5406
|
+
}
|
|
5407
|
+
// PATCH the new value when the cell declares an edit URL. Persistence is
|
|
5408
|
+
// otherwise the host app's responsibility via the midwest-table:edit event.
|
|
5409
|
+
persistEdit(cell, value) {
|
|
5410
|
+
const url = cell.dataset.editUrl;
|
|
5411
|
+
if (!url)
|
|
5412
|
+
return;
|
|
5413
|
+
const token = document.querySelector('meta[name="csrf-token"]')?.getAttribute("content") ?? "";
|
|
5414
|
+
void fetch(url, {
|
|
5415
|
+
method: "PATCH",
|
|
5416
|
+
headers: {
|
|
5417
|
+
"Content-Type": "application/json",
|
|
5418
|
+
"X-CSRF-Token": token,
|
|
5419
|
+
Accept: "text/vnd.turbo-stream.html, application/json"
|
|
5420
|
+
},
|
|
5421
|
+
body: JSON.stringify({ field: cell.dataset.field, value })
|
|
5422
|
+
}).then(async (response) => {
|
|
5423
|
+
const contentType = response.headers.get("Content-Type") ?? "";
|
|
5424
|
+
if (contentType.includes("turbo-stream")) {
|
|
5425
|
+
const Turbo = window.Turbo;
|
|
5426
|
+
Turbo?.renderStreamMessage?.(await response.text());
|
|
5427
|
+
}
|
|
5428
|
+
}).catch(() => {
|
|
5429
|
+
});
|
|
5430
|
+
}
|
|
5431
|
+
get table() {
|
|
5432
|
+
return this.element.querySelector("table");
|
|
5433
|
+
}
|
|
5058
5434
|
}
|
|
5059
5435
|
|
|
5060
5436
|
function observeViewport(element, options = {}) {
|
|
@@ -8888,6 +9264,211 @@ class Popover extends Controller {
|
|
|
8888
9264
|
}
|
|
8889
9265
|
}
|
|
8890
9266
|
|
|
9267
|
+
class ContextMenu extends Controller {
|
|
9268
|
+
static targets = ["menu"];
|
|
9269
|
+
boundOutside = (event) => this.onOutside(event);
|
|
9270
|
+
boundKeydown = (event) => this.onKeydown(event);
|
|
9271
|
+
boundDismiss = () => this.close();
|
|
9272
|
+
open(event) {
|
|
9273
|
+
event.preventDefault();
|
|
9274
|
+
this.show(event.clientX, event.clientY);
|
|
9275
|
+
}
|
|
9276
|
+
show(x, y) {
|
|
9277
|
+
const menu = this.menuTarget;
|
|
9278
|
+
if (menu.matches(":popover-open"))
|
|
9279
|
+
menu.hidePopover();
|
|
9280
|
+
menu.style.left = "0px";
|
|
9281
|
+
menu.style.top = "0px";
|
|
9282
|
+
menu.showPopover();
|
|
9283
|
+
const rect = menu.getBoundingClientRect();
|
|
9284
|
+
const vw = document.documentElement.clientWidth;
|
|
9285
|
+
const vh = document.documentElement.clientHeight;
|
|
9286
|
+
const left = Math.min(x, Math.max(0, vw - rect.width - 4));
|
|
9287
|
+
const top = Math.min(y, Math.max(0, vh - rect.height - 4));
|
|
9288
|
+
menu.style.left = `${left}px`;
|
|
9289
|
+
menu.style.top = `${top}px`;
|
|
9290
|
+
this.addListeners();
|
|
9291
|
+
}
|
|
9292
|
+
close() {
|
|
9293
|
+
if (this.menuTarget.matches(":popover-open"))
|
|
9294
|
+
this.menuTarget.hidePopover();
|
|
9295
|
+
this.removeListeners();
|
|
9296
|
+
}
|
|
9297
|
+
// Delegated from the menu: activating a leaf item closes the menu, but a click
|
|
9298
|
+
// that opens a sub-surface must keep it open. Both a submenu parent
|
|
9299
|
+
// (`as: :nested`) and a confirmation trigger open a popover via
|
|
9300
|
+
// `popovertarget`, so any such trigger is left alone — the confirmation dialog
|
|
9301
|
+
// (or submenu) then lives on inside the still-open menu.
|
|
9302
|
+
select(event) {
|
|
9303
|
+
const target = event.target;
|
|
9304
|
+
if (target.closest("[popovertarget]"))
|
|
9305
|
+
return;
|
|
9306
|
+
if (target.closest("a.midwest-dropdown-item, button.midwest-dropdown-item"))
|
|
9307
|
+
this.close();
|
|
9308
|
+
}
|
|
9309
|
+
onOutside(event) {
|
|
9310
|
+
if (!this.menuTarget.contains(event.target))
|
|
9311
|
+
this.close();
|
|
9312
|
+
}
|
|
9313
|
+
onKeydown(event) {
|
|
9314
|
+
if (event.key === "Escape")
|
|
9315
|
+
this.close();
|
|
9316
|
+
}
|
|
9317
|
+
addListeners() {
|
|
9318
|
+
document.addEventListener("pointerdown", this.boundOutside, true);
|
|
9319
|
+
document.addEventListener("keydown", this.boundKeydown, true);
|
|
9320
|
+
window.addEventListener("scroll", this.boundDismiss, true);
|
|
9321
|
+
window.addEventListener("resize", this.boundDismiss);
|
|
9322
|
+
}
|
|
9323
|
+
removeListeners() {
|
|
9324
|
+
document.removeEventListener("pointerdown", this.boundOutside, true);
|
|
9325
|
+
document.removeEventListener("keydown", this.boundKeydown, true);
|
|
9326
|
+
window.removeEventListener("scroll", this.boundDismiss, true);
|
|
9327
|
+
window.removeEventListener("resize", this.boundDismiss);
|
|
9328
|
+
}
|
|
9329
|
+
disconnect() {
|
|
9330
|
+
this.removeListeners();
|
|
9331
|
+
}
|
|
9332
|
+
}
|
|
9333
|
+
|
|
9334
|
+
class SplitView extends Controller {
|
|
9335
|
+
static targets = ["primary", "divider"];
|
|
9336
|
+
static values = {
|
|
9337
|
+
orientation: { type: String, default: "horizontal" },
|
|
9338
|
+
size: { type: Number, default: 50 },
|
|
9339
|
+
min: { type: Number, default: 10 },
|
|
9340
|
+
max: { type: Number, default: 90 },
|
|
9341
|
+
storageKey: { type: String, default: "" }
|
|
9342
|
+
};
|
|
9343
|
+
current = 50;
|
|
9344
|
+
boundMove = (event) => this.onPointerMove(event);
|
|
9345
|
+
boundUp = () => this.stopDrag();
|
|
9346
|
+
connect() {
|
|
9347
|
+
this.current = this.restore() ?? this.sizeValue;
|
|
9348
|
+
this.apply(this.current);
|
|
9349
|
+
}
|
|
9350
|
+
startDrag(event) {
|
|
9351
|
+
if (event.button !== 0)
|
|
9352
|
+
return;
|
|
9353
|
+
event.preventDefault();
|
|
9354
|
+
this.element.classList.add("is-dragging");
|
|
9355
|
+
window.addEventListener("pointermove", this.boundMove);
|
|
9356
|
+
window.addEventListener("pointerup", this.boundUp);
|
|
9357
|
+
}
|
|
9358
|
+
onPointerMove(event) {
|
|
9359
|
+
const rect = this.element.getBoundingClientRect();
|
|
9360
|
+
const pct = this.orientationValue === "vertical" ? (event.clientY - rect.top) / rect.height * 100 : (event.clientX - rect.left) / rect.width * 100;
|
|
9361
|
+
this.apply(pct);
|
|
9362
|
+
}
|
|
9363
|
+
stopDrag() {
|
|
9364
|
+
this.element.classList.remove("is-dragging");
|
|
9365
|
+
window.removeEventListener("pointermove", this.boundMove);
|
|
9366
|
+
window.removeEventListener("pointerup", this.boundUp);
|
|
9367
|
+
this.persist();
|
|
9368
|
+
}
|
|
9369
|
+
onKeydown(event) {
|
|
9370
|
+
const vertical = this.orientationValue === "vertical";
|
|
9371
|
+
if (event.key === "Home") {
|
|
9372
|
+
event.preventDefault();
|
|
9373
|
+
this.setAndPersist(this.minValue);
|
|
9374
|
+
return;
|
|
9375
|
+
}
|
|
9376
|
+
if (event.key === "End") {
|
|
9377
|
+
event.preventDefault();
|
|
9378
|
+
this.setAndPersist(this.maxValue);
|
|
9379
|
+
return;
|
|
9380
|
+
}
|
|
9381
|
+
const increaseKey = vertical ? "ArrowDown" : "ArrowRight";
|
|
9382
|
+
const decreaseKey = vertical ? "ArrowUp" : "ArrowLeft";
|
|
9383
|
+
if (event.key !== increaseKey && event.key !== decreaseKey)
|
|
9384
|
+
return;
|
|
9385
|
+
event.preventDefault();
|
|
9386
|
+
const step = event.shiftKey ? 10 : 2;
|
|
9387
|
+
this.setAndPersist(this.current + (event.key === increaseKey ? step : -step));
|
|
9388
|
+
}
|
|
9389
|
+
// Double-click restores the initial size.
|
|
9390
|
+
reset() {
|
|
9391
|
+
this.setAndPersist(this.sizeValue);
|
|
9392
|
+
}
|
|
9393
|
+
setAndPersist(pct) {
|
|
9394
|
+
this.apply(pct);
|
|
9395
|
+
this.persist();
|
|
9396
|
+
}
|
|
9397
|
+
apply(pct) {
|
|
9398
|
+
this.current = Math.min(this.maxValue, Math.max(this.minValue, pct));
|
|
9399
|
+
this.element.style.setProperty("--split-size", `${this.current}%`);
|
|
9400
|
+
this.dividerTarget.setAttribute("aria-valuenow", String(Math.round(this.current)));
|
|
9401
|
+
}
|
|
9402
|
+
persist() {
|
|
9403
|
+
if (!this.storageKeyValue)
|
|
9404
|
+
return;
|
|
9405
|
+
try {
|
|
9406
|
+
window.localStorage.setItem(this.storageKeyValue, String(this.current));
|
|
9407
|
+
} catch {
|
|
9408
|
+
}
|
|
9409
|
+
}
|
|
9410
|
+
restore() {
|
|
9411
|
+
if (!this.storageKeyValue)
|
|
9412
|
+
return null;
|
|
9413
|
+
try {
|
|
9414
|
+
const raw = window.localStorage.getItem(this.storageKeyValue);
|
|
9415
|
+
if (raw === null)
|
|
9416
|
+
return null;
|
|
9417
|
+
const value = Number.parseFloat(raw);
|
|
9418
|
+
return Number.isFinite(value) ? value : null;
|
|
9419
|
+
} catch {
|
|
9420
|
+
return null;
|
|
9421
|
+
}
|
|
9422
|
+
}
|
|
9423
|
+
disconnect() {
|
|
9424
|
+
window.removeEventListener("pointermove", this.boundMove);
|
|
9425
|
+
window.removeEventListener("pointerup", this.boundUp);
|
|
9426
|
+
}
|
|
9427
|
+
}
|
|
9428
|
+
|
|
9429
|
+
class Assets extends Controller {
|
|
9430
|
+
static values = {
|
|
9431
|
+
css: String,
|
|
9432
|
+
js: String
|
|
9433
|
+
};
|
|
9434
|
+
connect() {
|
|
9435
|
+
if (this.cssValue)
|
|
9436
|
+
this.ensureStylesheet(this.cssValue);
|
|
9437
|
+
if (this.jsValue)
|
|
9438
|
+
this.ensureScript(this.jsValue);
|
|
9439
|
+
}
|
|
9440
|
+
ensureStylesheet(href) {
|
|
9441
|
+
const url = this.absolute(href);
|
|
9442
|
+
const present = Array.from(
|
|
9443
|
+
document.querySelectorAll('link[rel="stylesheet"]')
|
|
9444
|
+
).some((link2) => link2.href === url);
|
|
9445
|
+
if (present)
|
|
9446
|
+
return;
|
|
9447
|
+
const link = document.createElement("link");
|
|
9448
|
+
link.rel = "stylesheet";
|
|
9449
|
+
link.href = href;
|
|
9450
|
+
document.head.appendChild(link);
|
|
9451
|
+
}
|
|
9452
|
+
ensureScript(src) {
|
|
9453
|
+
const url = this.absolute(src);
|
|
9454
|
+
const present = Array.from(
|
|
9455
|
+
document.querySelectorAll("script[src]")
|
|
9456
|
+
).some((script2) => script2.src === url);
|
|
9457
|
+
if (present)
|
|
9458
|
+
return;
|
|
9459
|
+
const script = document.createElement("script");
|
|
9460
|
+
script.type = "module";
|
|
9461
|
+
script.src = src;
|
|
9462
|
+
document.head.appendChild(script);
|
|
9463
|
+
}
|
|
9464
|
+
// Resolve a possibly-relative URL to its absolute form for comparison.
|
|
9465
|
+
absolute(url) {
|
|
9466
|
+
const anchor = document.createElement("a");
|
|
9467
|
+
anchor.href = url;
|
|
9468
|
+
return anchor.href;
|
|
9469
|
+
}
|
|
9470
|
+
}
|
|
9471
|
+
|
|
8891
9472
|
function registerMidwestControllers(application) {
|
|
8892
9473
|
application.register("midwest-card", Card);
|
|
8893
9474
|
application.register("midwest-banner", Banner);
|
|
@@ -8908,7 +9489,7 @@ function registerMidwestControllers(application) {
|
|
|
8908
9489
|
application.register("midwest-range", Range);
|
|
8909
9490
|
application.register("midwest-rating", Rating);
|
|
8910
9491
|
application.register("midwest-autocomplete", Autocomplete);
|
|
8911
|
-
application.register("midwest-image", Image);
|
|
9492
|
+
application.register("midwest-image", Image$1);
|
|
8912
9493
|
application.register("midwest-color-picker", ColorPicker);
|
|
8913
9494
|
application.register("midwest-notification", Notification);
|
|
8914
9495
|
application.register("midwest-horizontal-scroll", HorizontalScroll);
|
|
@@ -8934,6 +9515,9 @@ function registerMidwestControllers(application) {
|
|
|
8934
9515
|
application.register("midwest-map", Map$1);
|
|
8935
9516
|
application.register("midwest-address", Address);
|
|
8936
9517
|
application.register("midwest-popover", Popover);
|
|
9518
|
+
application.register("midwest-context-menu", ContextMenu);
|
|
9519
|
+
application.register("midwest-split-view", SplitView);
|
|
9520
|
+
application.register("midwest-assets", Assets);
|
|
8937
9521
|
}
|
|
8938
9522
|
|
|
8939
9523
|
export { Banner, Card, Chart, CountdownTimer, registerMidwestControllers };
|