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,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Monaco diff editor manager. Refresh version/CDN URLs: pnpm run monaco -- --skip
|
|
3
|
+
*/
|
|
4
|
+
import trimLeft from "./trimLeft.js";
|
|
5
|
+
// autogenerate v
|
|
6
|
+
export const MONACO_GENERATED = {
|
|
7
|
+
"version": "0.55.1",
|
|
8
|
+
"vs": [
|
|
9
|
+
"https://cdn.jsdelivr.net/npm/monaco-editor@0.55.1/min/vs",
|
|
10
|
+
"https://unpkg.com/monaco-editor@0.55.1/min/vs",
|
|
11
|
+
"/monaco/vs"
|
|
12
|
+
]
|
|
13
|
+
};
|
|
14
|
+
let cachedMonaco = null;
|
|
15
|
+
let cachedVsBase = null;
|
|
16
|
+
let loadingPromise = null;
|
|
17
|
+
/** Monaco AMD injects editor CSS into `document`; shadow roots need their own copy. */
|
|
18
|
+
const MONACO_VS_STYLESHEET = /\/vs\/(base|editor|platform)/;
|
|
19
|
+
/**
|
|
20
|
+
* Marker on `<link rel="stylesheet">` nodes we inject into a shadow root.
|
|
21
|
+
* Purpose: tag “our” Monaco CSS copies so we do not insert duplicates on reconnect
|
|
22
|
+
* or second editor in the same shadow tree. Usage: set on each cloned/created link
|
|
23
|
+
* before append; `ensureMonacoStylesInShadowRoot` bails out if one is already present.
|
|
24
|
+
*/
|
|
25
|
+
const MONACO_SHADOW_STYLES_ATTR = "data-monaco-shadow-styles";
|
|
26
|
+
/** Waits until a CSS file linked in the page has finished loading (or is already loaded). */
|
|
27
|
+
function loadStylesheetLink(link) {
|
|
28
|
+
if (link.sheet) {
|
|
29
|
+
return Promise.resolve();
|
|
30
|
+
}
|
|
31
|
+
return new Promise((resolve, reject) => {
|
|
32
|
+
link.addEventListener("load", () => resolve(), { once: true });
|
|
33
|
+
link.addEventListener("error", () => reject(new Error(`Failed to load Monaco stylesheet: ${link.href}`)), {
|
|
34
|
+
once: true,
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Puts Monaco’s CSS inside the web component’s shadow DOM so the editor looks correct
|
|
40
|
+
* there (normal page CSS does not reach inside a shadow root).
|
|
41
|
+
*
|
|
42
|
+
* Each injected stylesheet is marked with {@link MONACO_SHADOW_STYLES_ATTR} so this
|
|
43
|
+
* runs only once per shadow root.
|
|
44
|
+
*/
|
|
45
|
+
async function ensureMonacoStylesInShadowRoot(container) {
|
|
46
|
+
const root = container.getRootNode();
|
|
47
|
+
if (!(root instanceof ShadowRoot)) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (root.querySelector(`[${MONACO_SHADOW_STYLES_ATTR}]`)) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const documentLinks = Array.from(document.querySelectorAll("link[rel='stylesheet']")).filter((link) => {
|
|
54
|
+
const href = link.getAttribute("href") ?? "";
|
|
55
|
+
return MONACO_VS_STYLESHEET.test(href);
|
|
56
|
+
});
|
|
57
|
+
const loads = [];
|
|
58
|
+
for (const documentLink of documentLinks) {
|
|
59
|
+
const clone = documentLink.cloneNode(true);
|
|
60
|
+
clone.setAttribute(MONACO_SHADOW_STYLES_ATTR, "");
|
|
61
|
+
loads.push(loadStylesheetLink(clone));
|
|
62
|
+
root.insertBefore(clone, root.firstChild);
|
|
63
|
+
}
|
|
64
|
+
if (documentLinks.length === 0 && cachedVsBase) {
|
|
65
|
+
const link = document.createElement("link");
|
|
66
|
+
link.rel = "stylesheet";
|
|
67
|
+
link.href = `${cachedVsBase}/editor/editor.main.css`;
|
|
68
|
+
link.setAttribute(MONACO_SHADOW_STYLES_ATTR, "");
|
|
69
|
+
loads.push(loadStylesheetLink(link));
|
|
70
|
+
root.insertBefore(link, root.firstChild);
|
|
71
|
+
}
|
|
72
|
+
await Promise.all(loads);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Downloads and starts Monaco from a given base URL (CDN or local `/monaco/vs`),
|
|
76
|
+
* using the same loader script the official samples use.
|
|
77
|
+
*/
|
|
78
|
+
function loadMonaco(vsBase) {
|
|
79
|
+
return new Promise((resolve, reject) => {
|
|
80
|
+
const win = window;
|
|
81
|
+
const finish = () => {
|
|
82
|
+
win.require?.config({ paths: { vs: vsBase } });
|
|
83
|
+
win.require?.(["vs/editor/editor.main"], () => {
|
|
84
|
+
if (win.monaco) {
|
|
85
|
+
resolve(win.monaco);
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
reject(new Error(`Monaco did not initialize from ${vsBase}`));
|
|
89
|
+
}
|
|
90
|
+
}, (err) => {
|
|
91
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
92
|
+
});
|
|
93
|
+
};
|
|
94
|
+
if (win.monaco) {
|
|
95
|
+
resolve(win.monaco);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (win.require) {
|
|
99
|
+
finish();
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const script = document.createElement("script");
|
|
103
|
+
script.src = `${vsBase}/loader.js`;
|
|
104
|
+
script.async = true;
|
|
105
|
+
script.onload = () => finish();
|
|
106
|
+
script.onerror = () => reject(new Error(`Failed to load Monaco loader from ${vsBase}`));
|
|
107
|
+
document.head.appendChild(script);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Loads Monaco once and keeps it in memory. Tries each configured URL until one works,
|
|
112
|
+
* so the app still runs if a CDN is down.
|
|
113
|
+
*/
|
|
114
|
+
export function hydrateCache(generated = MONACO_GENERATED) {
|
|
115
|
+
if (cachedMonaco) {
|
|
116
|
+
return Promise.resolve(cachedMonaco);
|
|
117
|
+
}
|
|
118
|
+
if (loadingPromise) {
|
|
119
|
+
return loadingPromise;
|
|
120
|
+
}
|
|
121
|
+
loadingPromise = (async () => {
|
|
122
|
+
const errors = [];
|
|
123
|
+
for (const vsBase of generated.vs) {
|
|
124
|
+
try {
|
|
125
|
+
cachedMonaco = await loadMonaco(vsBase);
|
|
126
|
+
cachedVsBase = vsBase;
|
|
127
|
+
return cachedMonaco;
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
errors.push(err instanceof Error ? err : new Error(String(err)));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
loadingPromise = null;
|
|
134
|
+
throw new AggregateError(errors, `Failed to load monaco-editor@${generated.version} from all sources`);
|
|
135
|
+
})();
|
|
136
|
+
return loadingPromise;
|
|
137
|
+
}
|
|
138
|
+
export const SCRIPT_TYPE_ORIGINAL = "text/original";
|
|
139
|
+
export const SCRIPT_TYPE_MODIFIED = "text/modified";
|
|
140
|
+
/**
|
|
141
|
+
* Reads the “before” and “after” code from `<script>` tags inside the element (HTML-first setup).
|
|
142
|
+
* Returns nothing if there are no scripts; throws if the markup is incomplete or wrong.
|
|
143
|
+
*/
|
|
144
|
+
export function readDeclarativeDiffScripts(host) {
|
|
145
|
+
const scripts = Array.from(host.querySelectorAll(":scope > script"));
|
|
146
|
+
if (scripts.length === 0) {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
if (scripts.length < 2) {
|
|
150
|
+
throw new Error(`<composite-monaco-diff>: expected exactly at leasttwo <script> elements (type="${SCRIPT_TYPE_ORIGINAL}" and type="${SCRIPT_TYPE_MODIFIED}"), found ${scripts.length}`);
|
|
151
|
+
}
|
|
152
|
+
let originalScript;
|
|
153
|
+
let modifiedScript;
|
|
154
|
+
for (const script of scripts) {
|
|
155
|
+
const type = script.getAttribute("type");
|
|
156
|
+
if (type === SCRIPT_TYPE_ORIGINAL) {
|
|
157
|
+
if (originalScript) {
|
|
158
|
+
throw new Error(`<composite-monaco-diff>: duplicate <script type="${SCRIPT_TYPE_ORIGINAL}">`);
|
|
159
|
+
}
|
|
160
|
+
originalScript = script;
|
|
161
|
+
}
|
|
162
|
+
else if (type === SCRIPT_TYPE_MODIFIED) {
|
|
163
|
+
if (modifiedScript) {
|
|
164
|
+
throw new Error(`<composite-monaco-diff>: duplicate <script type="${SCRIPT_TYPE_MODIFIED}">`);
|
|
165
|
+
}
|
|
166
|
+
modifiedScript = script;
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
throw new Error(`<composite-monaco-diff>: <script> must have type="${SCRIPT_TYPE_ORIGINAL}" or type="${SCRIPT_TYPE_MODIFIED}"`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (!originalScript || !modifiedScript) {
|
|
173
|
+
const missing = !originalScript ? SCRIPT_TYPE_ORIGINAL : SCRIPT_TYPE_MODIFIED;
|
|
174
|
+
throw new Error(`<composite-monaco-diff>: missing <script type="${missing}">`);
|
|
175
|
+
}
|
|
176
|
+
let original = originalScript.textContent ?? "";
|
|
177
|
+
let modified = modifiedScript.textContent ?? "";
|
|
178
|
+
const originalLanguage = originalScript.getAttribute("lang") ?? undefined;
|
|
179
|
+
const modifiedLanguage = modifiedScript.getAttribute("lang") ?? undefined;
|
|
180
|
+
let originalOffset = parseInt(originalScript.getAttribute("data-offset"), 10) ?? 0;
|
|
181
|
+
let modifiedOffset = parseInt(modifiedScript.getAttribute("data-offset"), 10) ?? 0;
|
|
182
|
+
if (!(originalOffset > 0)) {
|
|
183
|
+
throw new Error(`<composite-monaco-diff><script type='text/original'>: data-offset must be a positive integer >${originalOffset}<`);
|
|
184
|
+
}
|
|
185
|
+
if (!(modifiedOffset > 0)) {
|
|
186
|
+
throw new Error(`<composite-monaco-diff><script type='text/modified'>: data-offset must be a positive integer >${modifiedOffset}<`);
|
|
187
|
+
}
|
|
188
|
+
original = trimLeft(original, originalOffset);
|
|
189
|
+
modified = trimLeft(modified, modifiedOffset);
|
|
190
|
+
originalScript.remove();
|
|
191
|
+
modifiedScript.remove();
|
|
192
|
+
return { original, modified, originalLanguage, modifiedLanguage };
|
|
193
|
+
}
|
|
194
|
+
const DEFAULT_LANGUAGE = "javascript";
|
|
195
|
+
/**
|
|
196
|
+
* Figures out what text and language to show on each side: from options, from HTML scripts,
|
|
197
|
+
* or empty strings with a sensible default language.
|
|
198
|
+
*/
|
|
199
|
+
function resolveDiffContent(options) {
|
|
200
|
+
let original = options.original ?? "";
|
|
201
|
+
let modified = options.modified ?? "";
|
|
202
|
+
let originalLanguage = options.originalLanguage;
|
|
203
|
+
let modifiedLanguage = options.modifiedLanguage;
|
|
204
|
+
if (options.host) {
|
|
205
|
+
const declarative = readDeclarativeDiffScripts(options.host);
|
|
206
|
+
if (declarative) {
|
|
207
|
+
original = declarative.original;
|
|
208
|
+
modified = declarative.modified;
|
|
209
|
+
originalLanguage = originalLanguage ?? declarative.originalLanguage;
|
|
210
|
+
modifiedLanguage = modifiedLanguage ?? declarative.modifiedLanguage;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
const sharedLanguage = options.language;
|
|
214
|
+
return {
|
|
215
|
+
original,
|
|
216
|
+
modified,
|
|
217
|
+
originalLanguage: originalLanguage ?? sharedLanguage ?? DEFAULT_LANGUAGE,
|
|
218
|
+
modifiedLanguage: modifiedLanguage ?? sharedLanguage ?? DEFAULT_LANGUAGE,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
export class MonacoDiffManager {
|
|
222
|
+
_readyPromise;
|
|
223
|
+
_editor = null;
|
|
224
|
+
_resizeObserver = null;
|
|
225
|
+
_layoutRaf = null;
|
|
226
|
+
_container;
|
|
227
|
+
/**
|
|
228
|
+
* Mounts a side-by-side diff editor into the given element: loads Monaco, applies styles,
|
|
229
|
+
* fills in original/modified text, and starts watching size changes.
|
|
230
|
+
*/
|
|
231
|
+
constructor(container, options) {
|
|
232
|
+
this._container = container;
|
|
233
|
+
container.style.height = "100%";
|
|
234
|
+
container.style.width = "100%";
|
|
235
|
+
const { original, modified, originalLanguage, modifiedLanguage } = resolveDiffContent(options);
|
|
236
|
+
this._readyPromise = (async () => {
|
|
237
|
+
const monaco = await hydrateCache(MONACO_GENERATED);
|
|
238
|
+
await ensureMonacoStylesInShadowRoot(container);
|
|
239
|
+
this._editor = monaco.editor.createDiffEditor(container, {
|
|
240
|
+
automaticLayout: false,
|
|
241
|
+
scrollbar: {
|
|
242
|
+
vertical: "auto",
|
|
243
|
+
},
|
|
244
|
+
scrollBeyondLastLine: false,
|
|
245
|
+
...options.editorOptions,
|
|
246
|
+
});
|
|
247
|
+
this._editor.setModel({
|
|
248
|
+
original: monaco.editor.createModel(original, originalLanguage),
|
|
249
|
+
modified: monaco.editor.createModel(modified, modifiedLanguage),
|
|
250
|
+
});
|
|
251
|
+
this._scheduleLayout();
|
|
252
|
+
this._resizeObserver = new ResizeObserver(() => this._scheduleLayout());
|
|
253
|
+
this._resizeObserver.observe(container);
|
|
254
|
+
})();
|
|
255
|
+
}
|
|
256
|
+
/** Promise that resolves when the editor has finished loading and is safe to use. */
|
|
257
|
+
whenReady() {
|
|
258
|
+
return this._readyPromise;
|
|
259
|
+
}
|
|
260
|
+
/** Returns the underlying Monaco diff editor instance (or null if not ready yet). */
|
|
261
|
+
getEditor() {
|
|
262
|
+
return this._editor;
|
|
263
|
+
}
|
|
264
|
+
/** Returns the global Monaco API instance (or null if not loaded yet). */
|
|
265
|
+
getMonaco() {
|
|
266
|
+
return cachedMonaco;
|
|
267
|
+
}
|
|
268
|
+
/** Tears down the editor, frees memory, and stops listening for resize events. */
|
|
269
|
+
destroy() {
|
|
270
|
+
this._resizeObserver?.disconnect();
|
|
271
|
+
this._resizeObserver = null;
|
|
272
|
+
if (this._layoutRaf !== null) {
|
|
273
|
+
cancelAnimationFrame(this._layoutRaf);
|
|
274
|
+
this._layoutRaf = null;
|
|
275
|
+
}
|
|
276
|
+
const model = this._editor?.getModel();
|
|
277
|
+
this._editor?.dispose();
|
|
278
|
+
this._editor = null;
|
|
279
|
+
model?.original.dispose();
|
|
280
|
+
model?.modified.dispose();
|
|
281
|
+
}
|
|
282
|
+
/** Updates the language for both sides of the diff editor. Falls back to the default language when undefined. */
|
|
283
|
+
setLanguage(language) {
|
|
284
|
+
const model = this._editor?.getModel();
|
|
285
|
+
if (!model)
|
|
286
|
+
return;
|
|
287
|
+
const lang = language ?? DEFAULT_LANGUAGE;
|
|
288
|
+
cachedMonaco?.editor.setModelLanguage(model.original, lang);
|
|
289
|
+
cachedMonaco?.editor.setModelLanguage(model.modified, lang);
|
|
290
|
+
}
|
|
291
|
+
/** Resizes the editor to match its container on the next animation frame (avoids jank). */
|
|
292
|
+
_scheduleLayout() {
|
|
293
|
+
if (this._layoutRaf !== null)
|
|
294
|
+
return;
|
|
295
|
+
this._layoutRaf = requestAnimationFrame(() => {
|
|
296
|
+
this._layoutRaf = null;
|
|
297
|
+
if (!this._editor)
|
|
298
|
+
return;
|
|
299
|
+
const { width, height } = this._container.getBoundingClientRect();
|
|
300
|
+
if (width === 0 || height === 0) {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
this._editor.layout({ width, height });
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<composite-monaco-diff>` — declarative diff editor with optional embedded `<script>` sources.
|
|
3
|
+
*
|
|
4
|
+
* Import this module to register the element (`customElements.define` at file bottom).
|
|
5
|
+
* Before querying the DOM or calling instance APIs from other modules, wait until the
|
|
6
|
+
* browser knows the tag (same pattern as other custom elements in this repo):
|
|
7
|
+
*
|
|
8
|
+
* import { MonacoDiffElement } from "./composite-monaco-diff.js";
|
|
9
|
+
*
|
|
10
|
+
* await customElements.whenDefined(MonacoDiffElement.tagName);
|
|
11
|
+
*
|
|
12
|
+
* const diff = document.querySelector(MonacoDiffElement.tagName);
|
|
13
|
+
* if (!(diff instanceof MonacoDiffElement)) throw new Error("Missing <composite-monaco-diff>");
|
|
14
|
+
* await diff.whenReady();
|
|
15
|
+
*
|
|
16
|
+
* @example HTML
|
|
17
|
+
* ```html
|
|
18
|
+
* <composite-monaco-diff theme="vs-dark">
|
|
19
|
+
* <script type="text/original" lang="javascript">
|
|
20
|
+
* const a = 1;
|
|
21
|
+
* </script>
|
|
22
|
+
* <script type="text/modified" lang="typescript">
|
|
23
|
+
* const a = 2;
|
|
24
|
+
* </script>
|
|
25
|
+
* </composite-monaco-diff>
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
import { hydrateCache, MonacoDiffManager } from "./MonacoDiffManager.js";
|
|
29
|
+
/** Custom element tag name (`"composite-monaco-diff"`). Pass to `customElements.whenDefined(tagName)`. */
|
|
30
|
+
export const tagName = "composite-monaco-diff";
|
|
31
|
+
export const MONACO_THEMES = ["vs", "vs-dark", "hc-black", "hc-light"];
|
|
32
|
+
export function isMonacoTheme(value) {
|
|
33
|
+
return MONACO_THEMES.includes(value);
|
|
34
|
+
}
|
|
35
|
+
function parseThemeAttribute(value) {
|
|
36
|
+
if (value && isMonacoTheme(value)) {
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
export class MonacoDiffElement extends HTMLElement {
|
|
42
|
+
static tagName = tagName;
|
|
43
|
+
static get observedAttributes() {
|
|
44
|
+
return ["theme", "language"];
|
|
45
|
+
}
|
|
46
|
+
_container;
|
|
47
|
+
_manager = null;
|
|
48
|
+
constructor() {
|
|
49
|
+
super();
|
|
50
|
+
this.attachShadow({ mode: "open" });
|
|
51
|
+
this.shadowRoot.innerHTML = `
|
|
52
|
+
<style>
|
|
53
|
+
:host {
|
|
54
|
+
display: block;
|
|
55
|
+
width: 100%;
|
|
56
|
+
height: 100%;
|
|
57
|
+
min-height: 200px;
|
|
58
|
+
}
|
|
59
|
+
.container {
|
|
60
|
+
width: 100%;
|
|
61
|
+
height: 100%;
|
|
62
|
+
}
|
|
63
|
+
</style>
|
|
64
|
+
<div class="container"></div>
|
|
65
|
+
`;
|
|
66
|
+
this._container = this.shadowRoot.querySelector(".container");
|
|
67
|
+
}
|
|
68
|
+
connectedCallback() {
|
|
69
|
+
if (this._manager) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const theme = parseThemeAttribute(this.getAttribute("theme"));
|
|
73
|
+
const language = this.getAttribute("language") ?? undefined;
|
|
74
|
+
this._manager = new MonacoDiffManager(this._container, {
|
|
75
|
+
host: this,
|
|
76
|
+
language,
|
|
77
|
+
editorOptions: {
|
|
78
|
+
theme,
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
attributeChangedCallback(name, _oldValue, newValue) {
|
|
83
|
+
if (name === "theme") {
|
|
84
|
+
void this._applyTheme(parseThemeAttribute(newValue));
|
|
85
|
+
}
|
|
86
|
+
else if (name === "language") {
|
|
87
|
+
void this._applyLanguage(newValue ?? undefined);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
disconnectedCallback() {
|
|
91
|
+
this._manager?.destroy();
|
|
92
|
+
this._manager = null;
|
|
93
|
+
}
|
|
94
|
+
/** Promise that resolves when the editor has finished loading and is safe to use. */
|
|
95
|
+
whenReady() {
|
|
96
|
+
if (!this._manager) {
|
|
97
|
+
throw new Error("<composite-monaco-diff>: not connected");
|
|
98
|
+
}
|
|
99
|
+
return this._manager.whenReady();
|
|
100
|
+
}
|
|
101
|
+
getManager() {
|
|
102
|
+
if (!this._manager) {
|
|
103
|
+
throw new Error("<composite-monaco-diff>: not connected");
|
|
104
|
+
}
|
|
105
|
+
return this._manager;
|
|
106
|
+
}
|
|
107
|
+
async _applyTheme(theme) {
|
|
108
|
+
if (!this._manager) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
await this.whenReady();
|
|
112
|
+
const monaco = await hydrateCache();
|
|
113
|
+
monaco.editor.setTheme(theme ?? "vs");
|
|
114
|
+
}
|
|
115
|
+
async _applyLanguage(language) {
|
|
116
|
+
if (!this._manager) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
await this.whenReady();
|
|
120
|
+
this._manager.setLanguage(language);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
customElements.define(tagName, MonacoDiffElement);
|
|
@@ -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
|
+
}
|