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,177 @@
|
|
|
1
|
+
import { CenterAndHeightResizer } from "../CenterAndHeightResizer.js";
|
|
2
|
+
import modURLSearchParams from "../urlchange/urlchange.js";
|
|
3
|
+
import { syncURLSearchParams, buildUrlWithSearchParams } from "../urlchange/toolsURLSearchParams.js";
|
|
4
|
+
import { MonacoDiffManager } from "../MonacoDiffManager.js";
|
|
5
|
+
import { isMonacoTheme } from "../composite-monaco-diff.js";
|
|
6
|
+
await customElements.whenDefined(CenterAndHeightResizer.tagName);
|
|
7
|
+
const container = document.getElementById("container");
|
|
8
|
+
if (!container) {
|
|
9
|
+
throw new Error("Missing #container element");
|
|
10
|
+
}
|
|
11
|
+
const original = `
|
|
12
|
+
const loadMonaco = (vsPath = VS_PATH) =>
|
|
13
|
+
new Promise((resolve, reject) => {
|
|
14
|
+
const win = window;
|
|
15
|
+
|
|
16
|
+
const finish = () => {
|
|
17
|
+
win.require.config({ paths: { vs: vsPath } });
|
|
18
|
+
win.require(["vs/editor/editor.main"], () => resolve(win.monaco));
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
if (win.require && win.monaco) return resolve(win.monaco);
|
|
22
|
+
if (win.require) return finish();
|
|
23
|
+
|
|
24
|
+
const script = document.createElement("script");
|
|
25
|
+
script.src = \`\${vsPath}/loader.js\`;
|
|
26
|
+
script.async = true;
|
|
27
|
+
script.onload = () => finish();
|
|
28
|
+
script.onerror = () => reject(new Error(\`Failed to load Monaco loader from \${vsPath}\`));
|
|
29
|
+
document.head.appendChild(script);
|
|
30
|
+
});
|
|
31
|
+
`;
|
|
32
|
+
const modified = `
|
|
33
|
+
const loadMonaco = (vsPath = VS_PATH) =>
|
|
34
|
+
new Promise((resolve, reject) => {
|
|
35
|
+
const win = window;
|
|
36
|
+
|
|
37
|
+
const finish = () => {
|
|
38
|
+
win.require.config({ paths: { vs: vsPath } });
|
|
39
|
+
win.require(["vs/editor/editor.main"], () => resolve(win.monaco));
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
if (win.require && win.moneco) return resolve(win.monaco);
|
|
43
|
+
|
|
44
|
+
const script = document.createElement("script");
|
|
45
|
+
script.src = \`\${vsPath}/loader.js\`;
|
|
46
|
+
script.async = true;
|
|
47
|
+
script.onload = () => finish();
|
|
48
|
+
script.added = 'stuff'
|
|
49
|
+
script.onerror = () => reject(new Error(\`Failed to load Monaco loader from \${vsPath}\`));
|
|
50
|
+
document.head.appendChild(script);
|
|
51
|
+
});
|
|
52
|
+
`;
|
|
53
|
+
const mgr = new MonacoDiffManager(container, {
|
|
54
|
+
original,
|
|
55
|
+
modified,
|
|
56
|
+
language: "javascript",
|
|
57
|
+
});
|
|
58
|
+
/**
|
|
59
|
+
* This is actually important for mgr to
|
|
60
|
+
* be ready before continuing with trackUrl()
|
|
61
|
+
*/
|
|
62
|
+
await mgr.whenReady();
|
|
63
|
+
const themeSelect = document.getElementById("theme-select");
|
|
64
|
+
if (!(themeSelect instanceof HTMLSelectElement)) {
|
|
65
|
+
throw new Error("Missing #theme-select element");
|
|
66
|
+
}
|
|
67
|
+
const languageSelect = document.getElementById("language-select");
|
|
68
|
+
if (!(languageSelect instanceof HTMLSelectElement)) {
|
|
69
|
+
throw new Error("Missing #language-select element");
|
|
70
|
+
}
|
|
71
|
+
const config = {
|
|
72
|
+
left: {
|
|
73
|
+
default: "100px",
|
|
74
|
+
getParam: "l",
|
|
75
|
+
encode: (value) => value,
|
|
76
|
+
decode: (value) => value,
|
|
77
|
+
},
|
|
78
|
+
center: {
|
|
79
|
+
default: "1200px",
|
|
80
|
+
getParam: "c",
|
|
81
|
+
encode: (value) => value,
|
|
82
|
+
decode: (value) => value,
|
|
83
|
+
},
|
|
84
|
+
height: {
|
|
85
|
+
default: "100px",
|
|
86
|
+
getParam: "h",
|
|
87
|
+
encode: (value) => value,
|
|
88
|
+
decode: (value) => value,
|
|
89
|
+
},
|
|
90
|
+
theme: {
|
|
91
|
+
default: "",
|
|
92
|
+
getParam: "theme",
|
|
93
|
+
encode: (value) => value,
|
|
94
|
+
decode: (value) => value,
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
document.querySelectorAll(CenterAndHeightResizer.tagName).forEach((el, index) => {
|
|
98
|
+
const resizer = el;
|
|
99
|
+
const { trackUrl } = modURLSearchParams(config, (key, i) => {
|
|
100
|
+
let t;
|
|
101
|
+
const cond = /^\d+$/.test(String(i));
|
|
102
|
+
if (cond) {
|
|
103
|
+
t = `${key}-${i}`;
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
t = key;
|
|
107
|
+
}
|
|
108
|
+
// console.log("instanceKeyFn", { cond, key, i }, "t: ", t);
|
|
109
|
+
return t;
|
|
110
|
+
});
|
|
111
|
+
const { setParams } = trackUrl((params, updatedURLSearchParams, governedKeys) => {
|
|
112
|
+
// console.log("trackUrl", index, JSON.stringify(params));
|
|
113
|
+
resizer.setAttribute("left", params.left);
|
|
114
|
+
resizer.setAttribute("center", params.center);
|
|
115
|
+
resizer.setAttribute("height", params.height);
|
|
116
|
+
const current = new URLSearchParams(window.location.search);
|
|
117
|
+
const next = syncURLSearchParams(current, governedKeys, updatedURLSearchParams);
|
|
118
|
+
if (next.toString() !== current.toString()) {
|
|
119
|
+
const url = buildUrlWithSearchParams(window.location.href, next);
|
|
120
|
+
history.replaceState(history.state, "", url);
|
|
121
|
+
}
|
|
122
|
+
}, { ctx: index, fireOnMount: true });
|
|
123
|
+
const syncToUrl = () => {
|
|
124
|
+
// console.log("syncToUrl: ", index);
|
|
125
|
+
setParams({
|
|
126
|
+
left: resizer.getAttribute("left") ?? config.left.default,
|
|
127
|
+
center: resizer.getAttribute("center") ?? config.center.default,
|
|
128
|
+
height: resizer.getAttribute("height") ?? config.height.default,
|
|
129
|
+
});
|
|
130
|
+
};
|
|
131
|
+
resizer.addEventListener("onLeft", syncToUrl);
|
|
132
|
+
resizer.addEventListener("onCenter", syncToUrl);
|
|
133
|
+
resizer.addEventListener("onHeight", syncToUrl);
|
|
134
|
+
});
|
|
135
|
+
function applyThemeAttribute(theme) {
|
|
136
|
+
console.log("applyThemeAttribute", theme);
|
|
137
|
+
mgr.getMonaco()?.editor.setTheme(theme || "vs");
|
|
138
|
+
}
|
|
139
|
+
function applyLanguageAttribute(language) {
|
|
140
|
+
console.log("applyLanguageAttribute", language);
|
|
141
|
+
mgr.setLanguage(language || undefined);
|
|
142
|
+
}
|
|
143
|
+
const { trackUrl: trackUrlNoIndex } = modURLSearchParams({
|
|
144
|
+
theme: {
|
|
145
|
+
default: "",
|
|
146
|
+
getParam: "theme",
|
|
147
|
+
encode: (value) => value,
|
|
148
|
+
decode: (value) => (isMonacoTheme(value) ? value : ""),
|
|
149
|
+
},
|
|
150
|
+
language: {
|
|
151
|
+
default: "javascript",
|
|
152
|
+
getParam: "lang",
|
|
153
|
+
encode: (value) => value,
|
|
154
|
+
decode: (value) => value,
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
const { setParam } = trackUrlNoIndex((params, updatedURLSearchParams, governedKeys) => {
|
|
158
|
+
console.log("trackUrlNoIndex", params);
|
|
159
|
+
themeSelect.value = params.theme;
|
|
160
|
+
applyThemeAttribute(params.theme);
|
|
161
|
+
languageSelect.value = params.language;
|
|
162
|
+
applyLanguageAttribute(params.language);
|
|
163
|
+
const current = new URLSearchParams(window.location.search);
|
|
164
|
+
const next = syncURLSearchParams(current, governedKeys, updatedURLSearchParams);
|
|
165
|
+
if (next.toString() !== current.toString()) {
|
|
166
|
+
const url = buildUrlWithSearchParams(window.location.href, next);
|
|
167
|
+
history.replaceState(history.state, "", url);
|
|
168
|
+
}
|
|
169
|
+
}, { fireOnMount: true });
|
|
170
|
+
themeSelect.addEventListener("change", () => {
|
|
171
|
+
console.log("themeSelect.value", themeSelect.value);
|
|
172
|
+
setParam("theme", themeSelect.value);
|
|
173
|
+
});
|
|
174
|
+
languageSelect.addEventListener("change", () => {
|
|
175
|
+
console.log("languageSelect.value", languageSelect.value);
|
|
176
|
+
setParam("language", languageSelect.value);
|
|
177
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// @ts-ignore
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import "./composite-monaco-diff.js";
|
|
4
|
+
export const MonacoDiff = React.forwardRef((props, ref) => {
|
|
5
|
+
const { theme, language, original, modified, originalLanguage, modifiedLanguage, children, ...rest } = props;
|
|
6
|
+
const internalRef = React.useRef(null);
|
|
7
|
+
const setRef = React.useCallback((node) => {
|
|
8
|
+
internalRef.current = node;
|
|
9
|
+
if (typeof ref === "function") {
|
|
10
|
+
ref(node);
|
|
11
|
+
}
|
|
12
|
+
else if (ref) {
|
|
13
|
+
ref.current = node;
|
|
14
|
+
}
|
|
15
|
+
}, [ref]);
|
|
16
|
+
React.useLayoutEffect(() => {
|
|
17
|
+
const el = internalRef.current;
|
|
18
|
+
if (el && el.getManager) {
|
|
19
|
+
let active = true;
|
|
20
|
+
(async () => {
|
|
21
|
+
try {
|
|
22
|
+
await el.whenReady();
|
|
23
|
+
if (!active)
|
|
24
|
+
return;
|
|
25
|
+
const mgr = el.getManager();
|
|
26
|
+
const editor = mgr.getEditor();
|
|
27
|
+
if (editor) {
|
|
28
|
+
const model = editor.getModel();
|
|
29
|
+
if (model) {
|
|
30
|
+
if (original !== undefined && model.original.getValue() !== original) {
|
|
31
|
+
model.original.setValue(original);
|
|
32
|
+
}
|
|
33
|
+
if (modified !== undefined && model.modified.getValue() !== modified) {
|
|
34
|
+
model.modified.setValue(modified);
|
|
35
|
+
}
|
|
36
|
+
const monaco = mgr.getMonaco();
|
|
37
|
+
if (monaco) {
|
|
38
|
+
if (originalLanguage !== undefined) {
|
|
39
|
+
monaco.editor.setModelLanguage(model.original, originalLanguage);
|
|
40
|
+
}
|
|
41
|
+
if (modifiedLanguage !== undefined) {
|
|
42
|
+
monaco.editor.setModelLanguage(model.modified, modifiedLanguage);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
console.error("MonacoDiff wrapper error:", err);
|
|
50
|
+
}
|
|
51
|
+
})();
|
|
52
|
+
return () => {
|
|
53
|
+
active = false;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
}, [original, modified, originalLanguage, modifiedLanguage]);
|
|
57
|
+
const wcProps = { ...rest, ref: setRef };
|
|
58
|
+
if (theme !== undefined)
|
|
59
|
+
wcProps["theme"] = theme;
|
|
60
|
+
if (language !== undefined)
|
|
61
|
+
wcProps["language"] = language;
|
|
62
|
+
return React.createElement("composite-monaco-diff", wcProps, children);
|
|
63
|
+
});
|
|
64
|
+
export default MonacoDiff;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export default function trimLeft(str, offset = 0) {
|
|
2
|
+
let o = typeof offset === "number" ? offset : parseInt(offset, 10);
|
|
3
|
+
if (isNaN(o)) {
|
|
4
|
+
throw new Error(`offset must be a number, ${offset}`);
|
|
5
|
+
}
|
|
6
|
+
if (o < 0) {
|
|
7
|
+
throw new Error(`offset must be a non-negative number, ${offset}`);
|
|
8
|
+
}
|
|
9
|
+
// Auto-detect indentation
|
|
10
|
+
const lines = str.split("\n");
|
|
11
|
+
let diff = Infinity;
|
|
12
|
+
lines.forEach((line) => {
|
|
13
|
+
if (!/^\s*$/.test(line)) {
|
|
14
|
+
const lengthBefore = line.length;
|
|
15
|
+
const lengthAfter = line.replace(/^\s+/, "").length;
|
|
16
|
+
const indentation = lengthBefore - lengthAfter;
|
|
17
|
+
if (indentation < diff) {
|
|
18
|
+
diff = indentation;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
let result = lines.map((line) => line.substring(diff));
|
|
23
|
+
if (o > 0) {
|
|
24
|
+
const spaces = " ".repeat(o);
|
|
25
|
+
result = result.map((line) => `${spaces}${line}`);
|
|
26
|
+
}
|
|
27
|
+
return result.join("\n");
|
|
28
|
+
}
|
|
@@ -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();
|