composite-monaco-diff 1.0.2
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/LICENSE +338 -0
- package/dist/cjs/CenterAndHeightResizer.cjs +325 -0
- package/dist/cjs/CenterResizer.cjs +195 -0
- package/dist/cjs/Module.cjs +12 -0
- package/dist/cjs/MonacoDiffManager.cjs +306 -0
- package/dist/cjs/composite-monaco-diff.cjs +123 -0
- package/dist/cjs/manager/index.cjs +177 -0
- package/dist/cjs/react.cjs +64 -0
- package/dist/cjs/trimLeft.cjs +28 -0
- package/dist/cjs/urlchange/ChildSection.cjs +174 -0
- package/dist/cjs/urlchange/index.cjs +229 -0
- package/dist/cjs/urlchange/toolsURLSearchParams.cjs +111 -0
- package/dist/cjs/urlchange/urlchange.cjs +197 -0
- package/dist/cjs/web-component/from-js/index.cjs +160 -0
- package/dist/cjs/web-component/from-scripts/index.cjs +114 -0
- package/dist/esm/CenterAndHeightResizer.js +325 -0
- package/dist/esm/CenterResizer.js +195 -0
- package/dist/esm/Module.js +12 -0
- package/dist/esm/MonacoDiffManager.js +306 -0
- package/dist/esm/composite-monaco-diff.js +123 -0
- package/dist/esm/manager/index.js +177 -0
- package/dist/esm/react.js +64 -0
- package/dist/esm/trimLeft.js +28 -0
- package/dist/esm/urlchange/ChildSection.js +174 -0
- package/dist/esm/urlchange/index.js +229 -0
- package/dist/esm/urlchange/toolsURLSearchParams.js +111 -0
- package/dist/esm/urlchange/urlchange.js +197 -0
- package/dist/esm/web-component/from-js/index.js +160 -0
- package/dist/esm/web-component/from-scripts/index.js +114 -0
- package/dist/types/CenterAndHeightResizer.d.ts +27 -0
- package/dist/types/CenterResizer.d.ts +16 -0
- package/dist/types/Module.d.ts +11 -0
- package/dist/types/MonacoDiffManager.d.ts +62 -0
- package/dist/types/composite-monaco-diff.d.ts +48 -0
- package/dist/types/manager/index.d.ts +1 -0
- package/dist/types/react.d.ts +19 -0
- package/dist/types/trimLeft.d.ts +1 -0
- package/dist/types/urlchange/ChildSection.d.ts +41 -0
- package/dist/types/urlchange/index.d.ts +1 -0
- package/dist/types/urlchange/toolsURLSearchParams.d.ts +40 -0
- package/dist/types/urlchange/urlchange.d.ts +107 -0
- package/dist/types/web-component/from-js/index.d.ts +1 -0
- package/dist/types/web-component/from-scripts/index.d.ts +1 -0
- package/package.json +44 -0
- package/web-component/Module.js +413 -0
- package/web-component/MonacoDiffManager.js +306 -0
- package/web-component/composite-monaco-diff.js +123 -0
- package/web-component/react.js +64 -0
- package/web-component/trimLeft.js +28 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
export const radioOptions = ["radio1", "radio2", "radio3"];
|
|
2
|
+
export const defaultRadioOption = radioOptions[1];
|
|
3
|
+
export const selectOptions = ["item1", "item2", "item3", "item4"];
|
|
4
|
+
/** HTML template for one demo instance (form controls + JSON dump). Injected via `innerHTML`. */
|
|
5
|
+
function generateSectionHtml(index) {
|
|
6
|
+
const radioOptionsHtml = radioOptions
|
|
7
|
+
.map((opt) => `
|
|
8
|
+
<label class="url-ser-label-margin">
|
|
9
|
+
<input type="radio" name="radio-${index}" value="${opt}" data-role="radio" />
|
|
10
|
+
${opt}
|
|
11
|
+
</label>`)
|
|
12
|
+
.join("");
|
|
13
|
+
const selectOptionsHtml = selectOptions.map((opt) => `<option value="${opt}">${opt}</option>`).join("");
|
|
14
|
+
return `
|
|
15
|
+
<div class="url-ser-flex">
|
|
16
|
+
<form class="url-ser-form" data-role="form">
|
|
17
|
+
<label>
|
|
18
|
+
<strong>Text Input:</strong>
|
|
19
|
+
<br />
|
|
20
|
+
<input type="text" class="url-ser-input" data-role="text" />
|
|
21
|
+
</label>
|
|
22
|
+
|
|
23
|
+
<fieldset>
|
|
24
|
+
<legend><strong>Radio Group:</strong></legend>
|
|
25
|
+
${radioOptionsHtml}
|
|
26
|
+
</fieldset>
|
|
27
|
+
|
|
28
|
+
<label>
|
|
29
|
+
<strong>Multiple Select:</strong>
|
|
30
|
+
<br />
|
|
31
|
+
<select multiple class="url-ser-select" data-role="multi-select">
|
|
32
|
+
${selectOptionsHtml}
|
|
33
|
+
</select>
|
|
34
|
+
</label>
|
|
35
|
+
|
|
36
|
+
<fieldset>
|
|
37
|
+
<legend><strong>Checkboxes:</strong></legend>
|
|
38
|
+
<label class="url-ser-label-margin">
|
|
39
|
+
<input type="checkbox" data-role="checkbox-a" />
|
|
40
|
+
Checkbox A
|
|
41
|
+
</label>
|
|
42
|
+
<label>
|
|
43
|
+
<input type="checkbox" data-role="checkbox-b" />
|
|
44
|
+
Checkbox B
|
|
45
|
+
</label>
|
|
46
|
+
</fieldset>
|
|
47
|
+
|
|
48
|
+
<div class="buttons">
|
|
49
|
+
<button type="button" class="url-ser-delete-btn red" data-role="delete">
|
|
50
|
+
Delete Component #${index}
|
|
51
|
+
</button>
|
|
52
|
+
<button type="button" class="url-ser-delete-btn" data-role="reconfigure">
|
|
53
|
+
Reconfigure #${index}
|
|
54
|
+
</button>
|
|
55
|
+
</div>
|
|
56
|
+
</form>
|
|
57
|
+
|
|
58
|
+
<div class="url-ser-dump-container">
|
|
59
|
+
<pre class="url-ser-pre" data-role="dump"></pre>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
`;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* ChildSection encapsulates all aspects of DOM manipulation for a single
|
|
66
|
+
* index/instance of the test components, keeping DOM queries and mutations
|
|
67
|
+
* isolated from the URL-handling business logic.
|
|
68
|
+
*/
|
|
69
|
+
export class ChildSection {
|
|
70
|
+
index;
|
|
71
|
+
root;
|
|
72
|
+
textInput;
|
|
73
|
+
multiSelect;
|
|
74
|
+
checkboxA;
|
|
75
|
+
checkboxB;
|
|
76
|
+
dumpPre;
|
|
77
|
+
radioInputs;
|
|
78
|
+
constructor(container, index) {
|
|
79
|
+
this.index = index;
|
|
80
|
+
this.root = document.createElement("div");
|
|
81
|
+
this.root.className = "url-ser-container";
|
|
82
|
+
this.root.dataset.index = String(index);
|
|
83
|
+
this.root.innerHTML = generateSectionHtml(index);
|
|
84
|
+
container.appendChild(this.root);
|
|
85
|
+
const form = this.root.querySelector('[data-role="form"]');
|
|
86
|
+
this.textInput = this.root.querySelector('[data-role="text"]');
|
|
87
|
+
this.multiSelect = this.root.querySelector('[data-role="multi-select"]');
|
|
88
|
+
this.checkboxA = this.root.querySelector('[data-role="checkbox-a"]');
|
|
89
|
+
this.checkboxB = this.root.querySelector('[data-role="checkbox-b"]');
|
|
90
|
+
this.dumpPre = this.root.querySelector('[data-role="dump"]');
|
|
91
|
+
this.radioInputs = Array.from(this.root.querySelectorAll('[data-role="radio"]'));
|
|
92
|
+
form.addEventListener("submit", (e) => e.preventDefault());
|
|
93
|
+
}
|
|
94
|
+
// Getters & Setters
|
|
95
|
+
getText() {
|
|
96
|
+
return this.textInput.value;
|
|
97
|
+
}
|
|
98
|
+
setText(value) {
|
|
99
|
+
this.textInput.value = value;
|
|
100
|
+
}
|
|
101
|
+
getRadio() {
|
|
102
|
+
const checkedRadio = this.radioInputs.find((input) => input.checked);
|
|
103
|
+
return (checkedRadio ? checkedRadio.value : defaultRadioOption);
|
|
104
|
+
}
|
|
105
|
+
setRadio(value) {
|
|
106
|
+
for (const input of this.radioInputs) {
|
|
107
|
+
input.checked = input.value === value;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
getMultiSelect() {
|
|
111
|
+
return Array.from(this.multiSelect.selectedOptions, (o) => o.value);
|
|
112
|
+
}
|
|
113
|
+
setMultiSelect(values) {
|
|
114
|
+
for (const option of Array.from(this.multiSelect.options)) {
|
|
115
|
+
option.selected = values.includes(option.value);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
getCheckboxA() {
|
|
119
|
+
return this.checkboxA.checked;
|
|
120
|
+
}
|
|
121
|
+
setCheckboxA(value) {
|
|
122
|
+
this.checkboxA.checked = value;
|
|
123
|
+
}
|
|
124
|
+
getCheckboxB() {
|
|
125
|
+
return this.checkboxB.checked;
|
|
126
|
+
}
|
|
127
|
+
setCheckboxB(value) {
|
|
128
|
+
this.checkboxB.checked = value;
|
|
129
|
+
}
|
|
130
|
+
setDump(data) {
|
|
131
|
+
this.dumpPre.textContent = JSON.stringify(data, null, 2);
|
|
132
|
+
}
|
|
133
|
+
// Event Registrations
|
|
134
|
+
onText(callback) {
|
|
135
|
+
this.textInput.addEventListener("input", () => {
|
|
136
|
+
callback(this.index, this.textInput.value);
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
onRadio(callback) {
|
|
140
|
+
for (const input of this.radioInputs) {
|
|
141
|
+
input.addEventListener("change", () => {
|
|
142
|
+
if (input.checked) {
|
|
143
|
+
callback(this.index, input.value);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
onMultiSelect(callback) {
|
|
149
|
+
this.multiSelect.addEventListener("change", () => {
|
|
150
|
+
callback(this.index, this.getMultiSelect());
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
onCheckboxA(callback) {
|
|
154
|
+
this.checkboxA.addEventListener("change", () => {
|
|
155
|
+
callback(this.index, this.checkboxA.checked);
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
onCheckboxB(callback) {
|
|
159
|
+
this.checkboxB.addEventListener("change", () => {
|
|
160
|
+
callback(this.index, this.checkboxB.checked);
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
onDelete(callback) {
|
|
164
|
+
const deleteBtn = this.root.querySelector('[data-role="delete"]');
|
|
165
|
+
deleteBtn.addEventListener("click", () => callback(this.index));
|
|
166
|
+
}
|
|
167
|
+
onReconfigure(callback) {
|
|
168
|
+
const reconfigureBtn = this.root.querySelector('[data-role="reconfigure"]');
|
|
169
|
+
reconfigureBtn.addEventListener("click", () => callback(this.index));
|
|
170
|
+
}
|
|
171
|
+
destroy() {
|
|
172
|
+
this.root.remove();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Demo page for vanilla `trackUrl` / `modURLSearchParams`.
|
|
3
|
+
* Mirrors ModURLSearchParamsComponent.tsx: multiple indexed instances, each syncing UI ↔ URL.
|
|
4
|
+
*
|
|
5
|
+
* All query-string reads/writes go through helpers in `toolsURLSearchParams.ts`.
|
|
6
|
+
*/
|
|
7
|
+
import modURLSearchParams, { onUrlChange } from "./urlchange.js";
|
|
8
|
+
import { cloneSearchParams, compareNormalizedSearchParams, syncURLSearchParams, buildUrlWithSearchParams, } from "./toolsURLSearchParams.js";
|
|
9
|
+
import { ChildSection, selectOptions, defaultRadioOption, } from "./ChildSection.js";
|
|
10
|
+
/** Schema for one instance: local names, short URL keys (`t`, `r`, …), defaults, encode/decode. */
|
|
11
|
+
const urlParamConfig = {
|
|
12
|
+
text: {
|
|
13
|
+
default: "default text",
|
|
14
|
+
getParam: "t",
|
|
15
|
+
encode: (value) => value,
|
|
16
|
+
decode: (value) => value,
|
|
17
|
+
},
|
|
18
|
+
radio: {
|
|
19
|
+
default: defaultRadioOption,
|
|
20
|
+
getParam: "r",
|
|
21
|
+
encode: (value) => value,
|
|
22
|
+
decode: (value) => value,
|
|
23
|
+
},
|
|
24
|
+
multiSelect: {
|
|
25
|
+
default: ["item1", "item2"],
|
|
26
|
+
getParam: "m",
|
|
27
|
+
encode: (value) => value.join("."),
|
|
28
|
+
decode: (value) => value.split("."),
|
|
29
|
+
},
|
|
30
|
+
checkboxA: {
|
|
31
|
+
default: false,
|
|
32
|
+
getParam: "c1",
|
|
33
|
+
encode: (value) => (value ? "1" : "0"),
|
|
34
|
+
decode: (value) => value === "1",
|
|
35
|
+
},
|
|
36
|
+
checkboxB: {
|
|
37
|
+
default: true,
|
|
38
|
+
getParam: "c2",
|
|
39
|
+
encode: (value) => (value ? "1" : "0"),
|
|
40
|
+
decode: (value) => value === "1",
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
const instanceKeyFn = (key, i) => `${key}-${i}`;
|
|
44
|
+
const { trackUrl, separateIndexedSearchParams } = modURLSearchParams(urlParamConfig, instanceKeyFn);
|
|
45
|
+
/** Parent-only key: lists instance indexes without writing default-valued tracked params. */
|
|
46
|
+
const INSTANCE_IDS_KEY = "ids";
|
|
47
|
+
/**
|
|
48
|
+
* Attempt to find list of indices to determine how many html formations to render
|
|
49
|
+
*
|
|
50
|
+
* Collects all instance indexes present in the query string.
|
|
51
|
+
* Uses the parent `ids=1,2` list plus any key ending in `-{n}` (e.g. `t-3` from deep links).
|
|
52
|
+
*/
|
|
53
|
+
function parseInstanceIds(params) {
|
|
54
|
+
const indexes = new Set();
|
|
55
|
+
const raw = params.get(INSTANCE_IDS_KEY);
|
|
56
|
+
if (raw) {
|
|
57
|
+
for (const part of raw.split(",")) {
|
|
58
|
+
const n = parseInt(part.trim(), 10);
|
|
59
|
+
if (!isNaN(n))
|
|
60
|
+
indexes.add(n);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
params.forEach((_, key) => {
|
|
64
|
+
if (key === INSTANCE_IDS_KEY)
|
|
65
|
+
return;
|
|
66
|
+
const match = key.match(/-(\d+)$/);
|
|
67
|
+
if (match)
|
|
68
|
+
indexes.add(parseInt(match[1], 10));
|
|
69
|
+
});
|
|
70
|
+
return Array.from(indexes).sort((a, b) => a - b);
|
|
71
|
+
}
|
|
72
|
+
/** Reads the current page URL and returns which instance indexes should be rendered. */
|
|
73
|
+
function getInstanceList() {
|
|
74
|
+
return parseInstanceIds(cloneSearchParams(new URLSearchParams(window.location.search)));
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Returns a patch for `ids` only. Apply with `syncURLSearchParams` so an empty list removes the key.
|
|
78
|
+
*/
|
|
79
|
+
function instanceIdsPatch(ids) {
|
|
80
|
+
const patch = new URLSearchParams();
|
|
81
|
+
if (ids.length > 0) {
|
|
82
|
+
patch.set(INSTANCE_IDS_KEY, ids.join(","));
|
|
83
|
+
}
|
|
84
|
+
return patch;
|
|
85
|
+
}
|
|
86
|
+
/** All indexed query keys for one instance (`t-1`, `r-1`, …) — used when removing an instance from the URL. */
|
|
87
|
+
function governedKeysForInstance(i) {
|
|
88
|
+
return Object.values(urlParamConfig).map((def) => instanceKeyFn(def.getParam, i));
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Commits a full query string to the address bar when it differs from the current location (normalized).
|
|
92
|
+
*/
|
|
93
|
+
function updateUrl(next) {
|
|
94
|
+
const current = cloneSearchParams(new URLSearchParams(window.location.search));
|
|
95
|
+
if (compareNormalizedSearchParams(next, current))
|
|
96
|
+
return;
|
|
97
|
+
const url = buildUrlWithSearchParams(window.location.href, next);
|
|
98
|
+
history.replaceState(history.state, "", url);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Syncs only `governed` keys from `patch` onto `base` (defaults to current location) and commits.
|
|
102
|
+
* Keys absent from `patch` are removed — required for default elision and instance teardown.
|
|
103
|
+
*/
|
|
104
|
+
function replaceSearchSynced(governed, patch, base) {
|
|
105
|
+
updateUrl(syncURLSearchParams(base ?? cloneSearchParams(new URLSearchParams(window.location.search)), governed, patch));
|
|
106
|
+
}
|
|
107
|
+
/** "Add Text Param" — registers the next instance index in `ids` and mounts a new section. */
|
|
108
|
+
function addComponent() {
|
|
109
|
+
const current = cloneSearchParams(new URLSearchParams(window.location.search));
|
|
110
|
+
const list = getInstanceList();
|
|
111
|
+
const nextIndex = list.length > 0 ? Math.max(...list) + 1 : 1;
|
|
112
|
+
replaceSearchSynced([INSTANCE_IDS_KEY], instanceIdsPatch([...list, nextIndex]), current);
|
|
113
|
+
updateUrlDisplay();
|
|
114
|
+
reconcileSections();
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Removes one instance: drops all of its indexed tracked keys and removes `i` from `ids`.
|
|
118
|
+
* Called from each section's Delete button.
|
|
119
|
+
*/
|
|
120
|
+
function deleteItem(i) {
|
|
121
|
+
const current = cloneSearchParams(new URLSearchParams(window.location.search));
|
|
122
|
+
const childSlice = separateIndexedSearchParams(current, i);
|
|
123
|
+
const withoutChild = syncURLSearchParams(current, governedKeysForInstance(i), childSlice);
|
|
124
|
+
const ids = parseInstanceIds(withoutChild).filter((id) => id !== i);
|
|
125
|
+
replaceSearchSynced([INSTANCE_IDS_KEY], instanceIdsPatch(ids), withoutChild);
|
|
126
|
+
updateUrlDisplay();
|
|
127
|
+
reconcileSections();
|
|
128
|
+
}
|
|
129
|
+
const urlDisplayEl = document.getElementById("url-display");
|
|
130
|
+
/** Keeps the top `<pre id="url-display">` in sync with the full browser URL after every change. */
|
|
131
|
+
const updateUrlDisplay = (url = window.location.href) => {
|
|
132
|
+
if (urlDisplayEl)
|
|
133
|
+
urlDisplayEl.textContent = url;
|
|
134
|
+
};
|
|
135
|
+
const sectionsEl = document.getElementById("sections");
|
|
136
|
+
const instanceListEl = document.getElementById("instance-list");
|
|
137
|
+
const addBtn = document.getElementById("add-btn");
|
|
138
|
+
const linkOff = document.getElementById("link-off");
|
|
139
|
+
linkOff.href = window.location.href.split("?")[0];
|
|
140
|
+
const sections = new Map();
|
|
141
|
+
/**
|
|
142
|
+
* Syncs mounted sections with `getInstanceList()`: create missing, destroy removed, reorder DOM.
|
|
143
|
+
* Runs on load, on URL changes (back/forward, links), and after add/delete.
|
|
144
|
+
*/
|
|
145
|
+
function reconcileSections() {
|
|
146
|
+
const list = getInstanceList();
|
|
147
|
+
instanceListEl.textContent = list.length > 0 ? list.join(", ") : "(none)";
|
|
148
|
+
for (const i of list) {
|
|
149
|
+
if (!sections.has(i)) {
|
|
150
|
+
const section = new ChildSection(sectionsEl, i);
|
|
151
|
+
const handle = trackUrl((params, updatedURLSearchParams, governedKeys) => {
|
|
152
|
+
console.log(`RENDER ${i} >${updatedURLSearchParams.toString()}<`, params);
|
|
153
|
+
section.setText(params.text);
|
|
154
|
+
section.setRadio(params.radio);
|
|
155
|
+
section.setMultiSelect(params.multiSelect);
|
|
156
|
+
section.setCheckboxA(params.checkboxA);
|
|
157
|
+
section.setCheckboxB(params.checkboxB);
|
|
158
|
+
section.setDump({ params, path: updatedURLSearchParams.toString() });
|
|
159
|
+
updateUrlDisplay();
|
|
160
|
+
const current = new URLSearchParams(window.location.search);
|
|
161
|
+
const next = syncURLSearchParams(current, governedKeys, updatedURLSearchParams);
|
|
162
|
+
if (next.toString() !== current.toString()) {
|
|
163
|
+
const url = buildUrlWithSearchParams(window.location.href, next);
|
|
164
|
+
history.replaceState(history.state, "", url);
|
|
165
|
+
}
|
|
166
|
+
}, { ctx: i, fireOnMount: true });
|
|
167
|
+
section.onText((idx, val) => {
|
|
168
|
+
handle.setParam("text", val);
|
|
169
|
+
});
|
|
170
|
+
section.onRadio((idx, val) => {
|
|
171
|
+
handle.setParam("radio", val);
|
|
172
|
+
});
|
|
173
|
+
section.onMultiSelect((idx, val) => {
|
|
174
|
+
handle.setParam("multiSelect", val);
|
|
175
|
+
});
|
|
176
|
+
section.onCheckboxA((idx, val) => {
|
|
177
|
+
handle.setParam("checkboxA", val);
|
|
178
|
+
});
|
|
179
|
+
section.onCheckboxB((idx, val) => {
|
|
180
|
+
handle.setParam("checkboxB", val);
|
|
181
|
+
});
|
|
182
|
+
section.onDelete((idx) => {
|
|
183
|
+
deleteItem(idx);
|
|
184
|
+
});
|
|
185
|
+
section.onReconfigure((idx) => {
|
|
186
|
+
const params = handle.getParams();
|
|
187
|
+
if (params.radio === "radio2") {
|
|
188
|
+
handle.setParams({
|
|
189
|
+
text: `text-${idx} second state`,
|
|
190
|
+
radio: "radio3",
|
|
191
|
+
multiSelect: [selectOptions[1], selectOptions[selectOptions.length - 2]],
|
|
192
|
+
checkboxA: true,
|
|
193
|
+
checkboxB: false,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
handle.setParams({
|
|
198
|
+
text: `text-${idx}`,
|
|
199
|
+
radio: "radio2",
|
|
200
|
+
multiSelect: [selectOptions[0], selectOptions[selectOptions.length - 1]],
|
|
201
|
+
checkboxA: false,
|
|
202
|
+
checkboxB: true,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
sections.set(i, { section, handle });
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
for (const [i, record] of sections) {
|
|
210
|
+
if (!list.includes(i)) {
|
|
211
|
+
record.handle.disconnect();
|
|
212
|
+
record.section.destroy();
|
|
213
|
+
sections.delete(i);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
for (const i of list) {
|
|
217
|
+
const el = sections.get(i).section.root;
|
|
218
|
+
sectionsEl.appendChild(el);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
addBtn.addEventListener("click", addComponent);
|
|
222
|
+
// Initial paint + react to back/forward and any URL change that affects instance list or params.
|
|
223
|
+
onUrlChange(() => {
|
|
224
|
+
console.log("any change");
|
|
225
|
+
updateUrlDisplay();
|
|
226
|
+
reconcileSections();
|
|
227
|
+
});
|
|
228
|
+
updateUrlDisplay();
|
|
229
|
+
reconcileSections();
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Merges multiple URLSearchParams instances. The first parameter acts as the base.
|
|
3
|
+
* Subsequent parameters overwrite existing keys. If a string array is provided,
|
|
4
|
+
* it acts as an allowlist, meaning only those keys will be merged from subsequent instances.
|
|
5
|
+
* The final result is sorted alphabetically.
|
|
6
|
+
*/
|
|
7
|
+
export function mergeURLSearchParams(...args) {
|
|
8
|
+
const result = new URLSearchParams();
|
|
9
|
+
if (args.length === 0)
|
|
10
|
+
return result;
|
|
11
|
+
// Find the filter array if it exists
|
|
12
|
+
const filterIndex = args.findIndex((arg) => Array.isArray(arg));
|
|
13
|
+
const filterKeys = filterIndex !== -1 ? args[filterIndex] : null;
|
|
14
|
+
const filterSet = filterKeys ? new Set(filterKeys) : null;
|
|
15
|
+
for (let i = 0; i < args.length; i++) {
|
|
16
|
+
const params = args[i];
|
|
17
|
+
if (params instanceof URLSearchParams) {
|
|
18
|
+
// The first URLSearchParams contributes all its keys.
|
|
19
|
+
// Subsequent ones only contribute keys in the filterSet (if defined).
|
|
20
|
+
const isFirstParams = i === args.findIndex((arg) => arg instanceof URLSearchParams);
|
|
21
|
+
const uniqueKeys = new Set();
|
|
22
|
+
params.forEach((_, key) => uniqueKeys.add(key));
|
|
23
|
+
const keys = Array.from(uniqueKeys);
|
|
24
|
+
const keysToProcess = !isFirstParams && filterSet ? keys.filter((k) => filterSet.has(k)) : keys;
|
|
25
|
+
for (const key of keysToProcess) {
|
|
26
|
+
result.delete(key);
|
|
27
|
+
params.getAll(key).forEach((value) => {
|
|
28
|
+
result.append(key, value);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
result.sort();
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Creates a deep, distinct copy of a URLSearchParams instance, preserving its data
|
|
38
|
+
* but decoupling its object reference so mutations don't affect the original.
|
|
39
|
+
*/
|
|
40
|
+
export function cloneSearchParams(params) {
|
|
41
|
+
return new URLSearchParams(params);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Returns a new URLSearchParams instance with all keys sorted alphabetically.
|
|
45
|
+
* Useful for ensuring consistent parameter ordering before serialization.
|
|
46
|
+
*/
|
|
47
|
+
export function normalizeSearchParams(params) {
|
|
48
|
+
const result = new URLSearchParams(params);
|
|
49
|
+
result.sort();
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Compares two URLSearchParams instances for equality by normalizing and sorting them.
|
|
54
|
+
* Returns true if they contain the exact same keys and values regardless of original order.
|
|
55
|
+
*/
|
|
56
|
+
export function compareNormalizedSearchParams(a, b) {
|
|
57
|
+
const na = normalizeSearchParams(a);
|
|
58
|
+
const nb = normalizeSearchParams(b);
|
|
59
|
+
return na.toString() === nb.toString();
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Returns a new URLSearchParams instance sorted primarily by keys,
|
|
63
|
+
* and secondarily by values if the keys are identical.
|
|
64
|
+
*/
|
|
65
|
+
export function sortSearchParamsByKeyThenValue(params) {
|
|
66
|
+
const entries = [];
|
|
67
|
+
params.forEach((value, key) => {
|
|
68
|
+
entries.push([key, value]);
|
|
69
|
+
});
|
|
70
|
+
return new URLSearchParams(entries.sort(([k1, v1], [k2, v2]) => {
|
|
71
|
+
if (k1 === k2)
|
|
72
|
+
return v1.localeCompare(v2);
|
|
73
|
+
return k1.localeCompare(k2);
|
|
74
|
+
}));
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Merges consecutive source URLSearchParams into a base URLSearchParams, but strictly
|
|
78
|
+
* limits updates and deletions to a specific list of governed keys.
|
|
79
|
+
* Sources are applied in sequence. For each key in governedKeys and each source:
|
|
80
|
+
* - If the key is present in the source, it overwrites the value in result.
|
|
81
|
+
* - If the key is absent from the source, it is deleted from the result.
|
|
82
|
+
*/
|
|
83
|
+
export function syncURLSearchParams(base, governedKeys, ...sources) {
|
|
84
|
+
const result = new URLSearchParams(base);
|
|
85
|
+
const keys = Array.isArray(governedKeys) ? governedKeys : Array.from(governedKeys);
|
|
86
|
+
for (const source of sources) {
|
|
87
|
+
for (const key of keys) {
|
|
88
|
+
if (source.has(key)) {
|
|
89
|
+
result.delete(key);
|
|
90
|
+
source.getAll(key).forEach((value) => {
|
|
91
|
+
result.append(key, value);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
result.delete(key);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
result.sort();
|
|
100
|
+
return result;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Updates a URL string with new search parameters, preserving origin, pathname, and hash.
|
|
104
|
+
* Handles both absolute and relative locations.
|
|
105
|
+
*/
|
|
106
|
+
export function buildUrlWithSearchParams(location, nextParams) {
|
|
107
|
+
const search = typeof nextParams === "string" ? nextParams : nextParams.toString();
|
|
108
|
+
const url = new URL(location, "http://dummy");
|
|
109
|
+
url.search = search ? `?${search}` : "";
|
|
110
|
+
return url.origin === "http://dummy" ? `${url.pathname}${url.search}${url.hash}` : url.toString();
|
|
111
|
+
}
|