@mdzip/editor 1.4.8 → 1.5.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/README.md +59 -0
- package/dist/library-info.d.ts +1 -1
- package/dist/library-info.js +1 -1
- package/dist/view.d.ts +121 -0
- package/dist/view.d.ts.map +1 -1
- package/dist/view.js +260 -7
- package/dist/view.js.map +1 -1
- package/dist/workspace-view.d.ts +21 -0
- package/dist/workspace-view.d.ts.map +1 -1
- package/dist/workspace-view.js +50 -0
- package/dist/workspace-view.js.map +1 -1
- package/package.json +2 -2
package/dist/view.js
CHANGED
|
@@ -12,7 +12,7 @@ import { MdzipAssetSession, mdzipArchiveSourceId, sniffImageSize } from './asset
|
|
|
12
12
|
import { MD_MARKDOWN_ICON } from './icons/md-markdown.js';
|
|
13
13
|
import { MDZIP_RUNTIME_LIBRARIES } from './library-info.js';
|
|
14
14
|
import { MdzipWorkspaceService, extensionForMime, normalizeArchivePath, relativeArchivePath } from './workspace.js';
|
|
15
|
-
import { buildMdzipNavTree, canEditMdzipPath, escapeHtml, isOrphanedMdzipAsset, mdzipEntryIconKind, isMdzipManifestPath, isMdzipWorkspaceRelativeLink,
|
|
15
|
+
import { buildMdzipNavTree, canEditMdzipPath, escapeHtml, isOrphanedMdzipAsset, mdzipEntryIconKind, isMdzipManifestPath, isMdzipDefaultPolicyExternalLink, isMdzipExternalLink, isMdzipWorkspaceRelativeLink, parseMdzipLink, renderMdzipPreviewHtml } from './workspace-view.js';
|
|
16
16
|
import { escapeMarkdownImageAlt, findImageReferenceAtOffset, formatImageEditMarkdown } from './image-edit.js';
|
|
17
17
|
import { MdzipRenderingService, defaultSafeMarkdownRenderer, groupTokensIntoChunks, chunkSourceKey, collectMdzipHeadingIds, MDZIP_HEADING_ID_PREFIX, tokenEmbedsImage, tokenIsHeading } from './rendering.js';
|
|
18
18
|
import { mdzipFrontMatterExtension } from './front-matter-extension.js';
|
|
@@ -23,6 +23,14 @@ const isImageFile = (path) => IMAGE_EXTENSIONS.test(path);
|
|
|
23
23
|
function isThenable(value) {
|
|
24
24
|
return typeof value?.then === 'function';
|
|
25
25
|
}
|
|
26
|
+
function restoreAttribute(element, name, value) {
|
|
27
|
+
if (value === null) {
|
|
28
|
+
element.removeAttribute(name);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
element.setAttribute(name, value);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
26
34
|
const NAV_ICON_CLASS = 'nav-lucide-icon';
|
|
27
35
|
const TOOLBAR_ICON_CLASS = 'toggle-icon';
|
|
28
36
|
const MANIFEST_ICON_HTML = lucideIcon(FileBraces, NAV_ICON_CLASS);
|
|
@@ -1006,6 +1014,21 @@ export class MdzipWorkspaceView {
|
|
|
1006
1014
|
// until that document's preview has mounted so there is a heading to scroll
|
|
1007
1015
|
// to. Consumed by firePreviewRendered.
|
|
1008
1016
|
this.pendingAnchorFragment = null;
|
|
1017
|
+
// The previewGeneration whose preview last mounted (or was claimed by an
|
|
1018
|
+
// entry renderer); scrollToAnchor() waits until it catches up.
|
|
1019
|
+
this.previewMountedGeneration = -1;
|
|
1020
|
+
this.previewMountWaiters = [];
|
|
1021
|
+
// True while onPreviewRendered runs — see scrollToAnchor().
|
|
1022
|
+
this.firingPreviewRendered = false;
|
|
1023
|
+
this.destroyed = false;
|
|
1024
|
+
// After an anchor scroll, content above the target can still change height
|
|
1025
|
+
// (images loading or easing open, a diagram rendering) and push it out of
|
|
1026
|
+
// view. This holds the target at the pane's top edge until the user
|
|
1027
|
+
// scrolls either pane or the preview re-renders.
|
|
1028
|
+
this.anchorPin = null;
|
|
1029
|
+
// Each link's own target/rel from before the external-link policy touched
|
|
1030
|
+
// it, so a policy change can restore them before re-applying.
|
|
1031
|
+
this.externalLinkOriginals = new WeakMap();
|
|
1009
1032
|
// Guards syncScrollToPreviewBottom against overlapping drains: set to the
|
|
1010
1033
|
// chunkedRenderState generation currently being force-drained, null when
|
|
1011
1034
|
// none is in flight. A second bottom-edge sync that arrives mid-drain
|
|
@@ -1442,6 +1465,9 @@ export class MdzipWorkspaceView {
|
|
|
1442
1465
|
this.teardownEntryRenderer();
|
|
1443
1466
|
// Release any whenRendered() waiters so their promises do not hang.
|
|
1444
1467
|
this.flushRenderedWaiters();
|
|
1468
|
+
this.destroyed = true;
|
|
1469
|
+
this.markPreviewMounted();
|
|
1470
|
+
this.releaseAnchorPin();
|
|
1445
1471
|
try {
|
|
1446
1472
|
this.workspace?.dispose();
|
|
1447
1473
|
}
|
|
@@ -1594,6 +1620,54 @@ export class MdzipWorkspaceView {
|
|
|
1594
1620
|
setImageEditOptions(options) {
|
|
1595
1621
|
this.options.imageEditHandler = options.imageEditHandler;
|
|
1596
1622
|
}
|
|
1623
|
+
/**
|
|
1624
|
+
* Replaces the external-link rendering policy and re-applies it to the
|
|
1625
|
+
* links already in the preview, without re-rendering. `undefined` restores
|
|
1626
|
+
* every link's own `target`/`rel`.
|
|
1627
|
+
*/
|
|
1628
|
+
setExternalLinks(policy) {
|
|
1629
|
+
this.options.externalLinks = policy;
|
|
1630
|
+
this.applyExternalLinkPolicy(this.elPreviewContent);
|
|
1631
|
+
}
|
|
1632
|
+
applyExternalLinkPolicy(root) {
|
|
1633
|
+
const policy = this.options.externalLinks;
|
|
1634
|
+
for (const link of Array.from(root.querySelectorAll('a[href]'))) {
|
|
1635
|
+
const original = this.externalLinkOriginals.get(link);
|
|
1636
|
+
if (original) {
|
|
1637
|
+
this.externalLinkOriginals.delete(link);
|
|
1638
|
+
restoreAttribute(link, 'target', original.target);
|
|
1639
|
+
restoreAttribute(link, 'rel', original.rel);
|
|
1640
|
+
}
|
|
1641
|
+
if (!policy) {
|
|
1642
|
+
continue;
|
|
1643
|
+
}
|
|
1644
|
+
const rel = policy.rel ?? (policy.target === '_blank' ? 'noopener noreferrer' : undefined);
|
|
1645
|
+
if (policy.target === undefined && rel === undefined) {
|
|
1646
|
+
continue;
|
|
1647
|
+
}
|
|
1648
|
+
const href = link.getAttribute('href') ?? '';
|
|
1649
|
+
if (!isMdzipExternalLink(href)) {
|
|
1650
|
+
continue;
|
|
1651
|
+
}
|
|
1652
|
+
let external = false;
|
|
1653
|
+
try {
|
|
1654
|
+
external = policy.predicate ? policy.predicate(href) : isMdzipDefaultPolicyExternalLink(href);
|
|
1655
|
+
}
|
|
1656
|
+
catch (error) {
|
|
1657
|
+
this.options.onFailed?.(error);
|
|
1658
|
+
}
|
|
1659
|
+
if (!external) {
|
|
1660
|
+
continue;
|
|
1661
|
+
}
|
|
1662
|
+
this.externalLinkOriginals.set(link, { target: link.getAttribute('target'), rel: link.getAttribute('rel') });
|
|
1663
|
+
if (policy.target !== undefined) {
|
|
1664
|
+
link.setAttribute('target', policy.target);
|
|
1665
|
+
}
|
|
1666
|
+
if (rel !== undefined) {
|
|
1667
|
+
link.setAttribute('rel', rel);
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1597
1671
|
applyDensityClasses() {
|
|
1598
1672
|
this.elRoot.classList.remove('toolbar-density-comfortable', 'toolbar-density-compact', 'toolbar-density-dense', 'content-density-comfortable', 'content-density-compact');
|
|
1599
1673
|
this.elRoot.classList.add(`toolbar-density-${this.toolbarDensity}`);
|
|
@@ -1681,6 +1755,7 @@ export class MdzipWorkspaceView {
|
|
|
1681
1755
|
// There is no built-in preview to wait for; release any waiters.
|
|
1682
1756
|
this.previewHydrated = true;
|
|
1683
1757
|
this.flushRenderedWaiters();
|
|
1758
|
+
this.markPreviewMounted();
|
|
1684
1759
|
return;
|
|
1685
1760
|
}
|
|
1686
1761
|
const memo = this.previewMemo;
|
|
@@ -1816,6 +1891,7 @@ export class MdzipWorkspaceView {
|
|
|
1816
1891
|
if (codeBlockHandle) {
|
|
1817
1892
|
this.previewHandles.push(codeBlockHandle);
|
|
1818
1893
|
}
|
|
1894
|
+
this.applyExternalLinkPolicy(this.elPreviewContent);
|
|
1819
1895
|
// No archive images to resolve on this path: the preview is ready now.
|
|
1820
1896
|
this.firePreviewRendered(snapshot, generation);
|
|
1821
1897
|
this.fireAssetsHydrated(snapshot, generation);
|
|
@@ -1838,6 +1914,7 @@ export class MdzipWorkspaceView {
|
|
|
1838
1914
|
if (codeBlockHandle) {
|
|
1839
1915
|
this.previewHandles.push(codeBlockHandle);
|
|
1840
1916
|
}
|
|
1917
|
+
this.applyExternalLinkPolicy(this.elPreviewContent);
|
|
1841
1918
|
this.firePreviewRendered(snapshot, generation);
|
|
1842
1919
|
this.hydrateImages(pending, context, generation, animateImageHydration, () => {
|
|
1843
1920
|
this.fireAssetsHydrated(snapshot, generation);
|
|
@@ -2466,6 +2543,7 @@ export class MdzipWorkspaceView {
|
|
|
2466
2543
|
if (codeBlockHandle) {
|
|
2467
2544
|
record.handles.push(codeBlockHandle);
|
|
2468
2545
|
}
|
|
2546
|
+
this.applyExternalLinkPolicy(record.root);
|
|
2469
2547
|
}
|
|
2470
2548
|
return { cursor, mountedRoots };
|
|
2471
2549
|
}
|
|
@@ -2813,12 +2891,26 @@ export class MdzipWorkspaceView {
|
|
|
2813
2891
|
}
|
|
2814
2892
|
this.restorePendingPreviewScroll(generation);
|
|
2815
2893
|
this.flushPendingAnchorSoon();
|
|
2894
|
+
this.markPreviewMounted();
|
|
2895
|
+
this.firingPreviewRendered = true;
|
|
2816
2896
|
try {
|
|
2817
2897
|
this.options.onPreviewRendered?.(snapshot);
|
|
2818
2898
|
}
|
|
2819
2899
|
catch (error) {
|
|
2820
2900
|
this.options.onFailed?.(error);
|
|
2821
2901
|
}
|
|
2902
|
+
finally {
|
|
2903
|
+
this.firingPreviewRendered = false;
|
|
2904
|
+
}
|
|
2905
|
+
}
|
|
2906
|
+
/** Releases {@link scrollToAnchor} calls waiting for the current preview to mount. */
|
|
2907
|
+
markPreviewMounted() {
|
|
2908
|
+
this.previewMountedGeneration = this.previewGeneration;
|
|
2909
|
+
const waiters = this.previewMountWaiters;
|
|
2910
|
+
this.previewMountWaiters = [];
|
|
2911
|
+
for (const resolve of waiters) {
|
|
2912
|
+
resolve();
|
|
2913
|
+
}
|
|
2822
2914
|
}
|
|
2823
2915
|
/** See `pendingPreviewScrollRestore`'s doc comment. */
|
|
2824
2916
|
restorePendingPreviewScroll(generation) {
|
|
@@ -3147,7 +3239,10 @@ export class MdzipWorkspaceView {
|
|
|
3147
3239
|
// scroll, on every keystroke. Wheel/touch/mousedown (which also covers
|
|
3148
3240
|
// a scrollbar-thumb drag, since that lands on this same element) are
|
|
3149
3241
|
// unambiguous and cover the vast majority of real scrolling.
|
|
3150
|
-
const markEditorGesture = () => {
|
|
3242
|
+
const markEditorGesture = () => {
|
|
3243
|
+
self.lastEditorGestureTime = performance.now();
|
|
3244
|
+
self.releaseAnchorPin();
|
|
3245
|
+
};
|
|
3151
3246
|
scroller.addEventListener('wheel', markEditorGesture, { passive: true });
|
|
3152
3247
|
scroller.addEventListener('touchstart', markEditorGesture, { passive: true });
|
|
3153
3248
|
scroller.addEventListener('mousedown', markEditorGesture);
|
|
@@ -3944,7 +4039,10 @@ export class MdzipWorkspaceView {
|
|
|
3944
4039
|
void this.replaceFileFromPicker(path, file);
|
|
3945
4040
|
}
|
|
3946
4041
|
});
|
|
3947
|
-
const markPreviewGesture = () => {
|
|
4042
|
+
const markPreviewGesture = () => {
|
|
4043
|
+
this.lastPreviewGestureTime = performance.now();
|
|
4044
|
+
this.releaseAnchorPin();
|
|
4045
|
+
};
|
|
3948
4046
|
this.elPreviewPane.addEventListener('wheel', markPreviewGesture, { passive: true });
|
|
3949
4047
|
this.elPreviewPane.addEventListener('touchstart', markPreviewGesture, { passive: true });
|
|
3950
4048
|
this.elPreviewPane.addEventListener('mousedown', markPreviewGesture);
|
|
@@ -3963,17 +4061,20 @@ export class MdzipWorkspaceView {
|
|
|
3963
4061
|
return;
|
|
3964
4062
|
}
|
|
3965
4063
|
const href = link.getAttribute('href') ?? '';
|
|
4064
|
+
const parsed = parseMdzipLink(href, snapshot.currentPath, snapshot.content.paths);
|
|
4065
|
+
if (this.options.onLinkActivated && this.dispatchLinkActivated(event, link, href, parsed, snapshot)) {
|
|
4066
|
+
return;
|
|
4067
|
+
}
|
|
3966
4068
|
if (href.startsWith('#')) {
|
|
3967
4069
|
event.preventDefault();
|
|
3968
4070
|
void this.scrollPreviewToAnchor(href.slice(1));
|
|
3969
4071
|
return;
|
|
3970
4072
|
}
|
|
3971
|
-
|
|
3972
|
-
if (targetPath) {
|
|
4073
|
+
if (parsed.targetPath) {
|
|
3973
4074
|
event.preventDefault();
|
|
3974
4075
|
const hashIndex = href.indexOf('#');
|
|
3975
4076
|
const fragment = hashIndex >= 0 ? href.slice(hashIndex + 1) : '';
|
|
3976
|
-
void this.openPathAndScrollToAnchor(targetPath, fragment);
|
|
4077
|
+
void this.openPathAndScrollToAnchor(parsed.targetPath, fragment);
|
|
3977
4078
|
return;
|
|
3978
4079
|
}
|
|
3979
4080
|
if (this.options.onUnresolvedLinkClick && isMdzipWorkspaceRelativeLink(href)) {
|
|
@@ -4025,6 +4126,132 @@ export class MdzipWorkspaceView {
|
|
|
4025
4126
|
await this.scrollPreviewToAnchor(pending);
|
|
4026
4127
|
}
|
|
4027
4128
|
}
|
|
4129
|
+
/** Returns true when the host prevented the click, ending all further handling. */
|
|
4130
|
+
dispatchLinkActivated(domEvent, link, href, parsed, snapshot) {
|
|
4131
|
+
let prevented = false;
|
|
4132
|
+
const event = {
|
|
4133
|
+
href,
|
|
4134
|
+
text: (link.textContent ?? '').trim(),
|
|
4135
|
+
kind: parsed.kind,
|
|
4136
|
+
isExternal: parsed.kind === 'external',
|
|
4137
|
+
isAnchor: parsed.kind === 'anchor',
|
|
4138
|
+
path: parsed.path,
|
|
4139
|
+
anchor: parsed.anchor,
|
|
4140
|
+
sourcePath: snapshot.currentPath,
|
|
4141
|
+
targetPath: parsed.targetPath,
|
|
4142
|
+
ctrlKey: domEvent.ctrlKey,
|
|
4143
|
+
metaKey: domEvent.metaKey,
|
|
4144
|
+
shiftKey: domEvent.shiftKey,
|
|
4145
|
+
altKey: domEvent.altKey,
|
|
4146
|
+
domEvent,
|
|
4147
|
+
snapshot,
|
|
4148
|
+
preventDefault: () => {
|
|
4149
|
+
prevented = true;
|
|
4150
|
+
},
|
|
4151
|
+
get defaultPrevented() {
|
|
4152
|
+
return prevented;
|
|
4153
|
+
}
|
|
4154
|
+
};
|
|
4155
|
+
try {
|
|
4156
|
+
this.options.onLinkActivated?.(event);
|
|
4157
|
+
}
|
|
4158
|
+
catch (error) {
|
|
4159
|
+
this.options.onFailed?.(error);
|
|
4160
|
+
}
|
|
4161
|
+
if (prevented) {
|
|
4162
|
+
domEvent.preventDefault();
|
|
4163
|
+
}
|
|
4164
|
+
return prevented;
|
|
4165
|
+
}
|
|
4166
|
+
/**
|
|
4167
|
+
* Scrolls the preview to a heading or an explicit `id`/`name` anchor.
|
|
4168
|
+
* Accepts the fragment with or without its leading `#` and matches it the
|
|
4169
|
+
* way a clicked `#fragment` link does: case-insensitive, percent-decoded,
|
|
4170
|
+
* against both bare and `user-content-`-prefixed heading ids.
|
|
4171
|
+
*
|
|
4172
|
+
* Safe to call at any point — straight after `open()`/`openWorkspace()`,
|
|
4173
|
+
* or from `onPreviewRendered`: it waits for the current document's
|
|
4174
|
+
* preview to mount, and under progressive rendering mounts the chunks up
|
|
4175
|
+
* to the target first. Resolves `true` when it scrolled to a matching
|
|
4176
|
+
* anchor (`''` and `'top'` scroll to the top and resolve `true`), and
|
|
4177
|
+
* `false` when nothing matches, an entry renderer owns the pane, or the
|
|
4178
|
+
* view is destroyed first.
|
|
4179
|
+
*/
|
|
4180
|
+
async scrollToAnchor(anchor) {
|
|
4181
|
+
const fragment = anchor.startsWith('#') ? anchor.slice(1) : anchor;
|
|
4182
|
+
// onPreviewRendered runs inside the first chunk batch's completion,
|
|
4183
|
+
// before the lazy-continuation sentinel is armed — same race as
|
|
4184
|
+
// flushPendingAnchorSoon, so a call from it (or one that had to wait for
|
|
4185
|
+
// the mount) is deferred a frame.
|
|
4186
|
+
let deferFrame = this.firingPreviewRendered;
|
|
4187
|
+
while (!this.destroyed && this.previewMountedGeneration !== this.previewGeneration) {
|
|
4188
|
+
deferFrame = true;
|
|
4189
|
+
await new Promise((resolve) => this.previewMountWaiters.push(resolve));
|
|
4190
|
+
}
|
|
4191
|
+
if (this.destroyed) {
|
|
4192
|
+
return false;
|
|
4193
|
+
}
|
|
4194
|
+
if (deferFrame) {
|
|
4195
|
+
const generation = this.previewGeneration;
|
|
4196
|
+
await new Promise((resolve) => requestAnimationFrame(() => resolve()));
|
|
4197
|
+
if (this.destroyed) {
|
|
4198
|
+
return false;
|
|
4199
|
+
}
|
|
4200
|
+
if (generation !== this.previewGeneration) {
|
|
4201
|
+
return this.scrollToAnchor(anchor);
|
|
4202
|
+
}
|
|
4203
|
+
}
|
|
4204
|
+
if (this.entryState) {
|
|
4205
|
+
return false;
|
|
4206
|
+
}
|
|
4207
|
+
const found = await this.scrollPreviewToAnchor(fragment);
|
|
4208
|
+
return found || !fragment || fragment.toLowerCase() === 'top';
|
|
4209
|
+
}
|
|
4210
|
+
/**
|
|
4211
|
+
* The anchors {@link scrollToAnchor} can reach in the current preview, in
|
|
4212
|
+
* document order: every heading id (without the `user-content-` prefix),
|
|
4213
|
+
* including headings in chunks progressive rendering hasn't mounted yet,
|
|
4214
|
+
* plus explicit `id`/`name` anchors on links in the mounted preview.
|
|
4215
|
+
* Empty before the preview first mounts.
|
|
4216
|
+
*/
|
|
4217
|
+
getAvailableAnchors() {
|
|
4218
|
+
if (this.entryState) {
|
|
4219
|
+
return [];
|
|
4220
|
+
}
|
|
4221
|
+
const anchors = [];
|
|
4222
|
+
const seen = new Set();
|
|
4223
|
+
const add = (value) => {
|
|
4224
|
+
if (!value)
|
|
4225
|
+
return;
|
|
4226
|
+
const bare = value.startsWith(MDZIP_HEADING_ID_PREFIX) ? value.slice(MDZIP_HEADING_ID_PREFIX.length) : value;
|
|
4227
|
+
if (bare && !seen.has(bare)) {
|
|
4228
|
+
seen.add(bare);
|
|
4229
|
+
anchors.push(bare);
|
|
4230
|
+
}
|
|
4231
|
+
};
|
|
4232
|
+
const fromDom = (root) => {
|
|
4233
|
+
for (const element of Array.from(root.querySelectorAll('h1[id],h2[id],h3[id],h4[id],h5[id],h6[id],a[id],a[name]'))) {
|
|
4234
|
+
add(element.getAttribute('id'));
|
|
4235
|
+
add(element.getAttribute('name'));
|
|
4236
|
+
}
|
|
4237
|
+
};
|
|
4238
|
+
const state = this.chunkedRenderState;
|
|
4239
|
+
if (state && state.generation === this.previewGeneration) {
|
|
4240
|
+
// Walk records in order so unmounted chunks' headings land in place.
|
|
4241
|
+
state.records.forEach((record) => {
|
|
4242
|
+
if (record.root) {
|
|
4243
|
+
fromDom(record.root);
|
|
4244
|
+
}
|
|
4245
|
+
else {
|
|
4246
|
+
collectMdzipHeadingIds(record.tokens).forEach(add);
|
|
4247
|
+
}
|
|
4248
|
+
});
|
|
4249
|
+
}
|
|
4250
|
+
else {
|
|
4251
|
+
fromDom(this.elPreviewContent);
|
|
4252
|
+
}
|
|
4253
|
+
return anchors;
|
|
4254
|
+
}
|
|
4028
4255
|
// Deferred a frame: firePreviewRendered runs inside the first chunk batch's
|
|
4029
4256
|
// completion callback, before the lazy-continuation sentinel for the rest is
|
|
4030
4257
|
// armed. Scrolling then (which may need to drain those chunks) would race it.
|
|
@@ -4103,9 +4330,35 @@ export class MdzipWorkspaceView {
|
|
|
4103
4330
|
}
|
|
4104
4331
|
return false;
|
|
4105
4332
|
}
|
|
4106
|
-
|
|
4333
|
+
this.scrollPaneToElement(target);
|
|
4334
|
+
this.pinPreviewAnchor(target);
|
|
4107
4335
|
return true;
|
|
4108
4336
|
}
|
|
4337
|
+
scrollPaneToElement(target) {
|
|
4338
|
+
const pane = this.elPreviewPane;
|
|
4339
|
+
pane.scrollTop += target.getBoundingClientRect().top - pane.getBoundingClientRect().top;
|
|
4340
|
+
}
|
|
4341
|
+
/** See `anchorPin`. */
|
|
4342
|
+
pinPreviewAnchor(target) {
|
|
4343
|
+
this.releaseAnchorPin();
|
|
4344
|
+
if (typeof ResizeObserver === 'undefined') {
|
|
4345
|
+
return;
|
|
4346
|
+
}
|
|
4347
|
+
const generation = this.previewGeneration;
|
|
4348
|
+
const observer = new ResizeObserver(() => {
|
|
4349
|
+
if (generation !== this.previewGeneration || !target.isConnected) {
|
|
4350
|
+
this.releaseAnchorPin();
|
|
4351
|
+
return;
|
|
4352
|
+
}
|
|
4353
|
+
this.scrollPaneToElement(target);
|
|
4354
|
+
});
|
|
4355
|
+
observer.observe(this.elPreviewContent);
|
|
4356
|
+
this.anchorPin = observer;
|
|
4357
|
+
}
|
|
4358
|
+
releaseAnchorPin() {
|
|
4359
|
+
this.anchorPin?.disconnect();
|
|
4360
|
+
this.anchorPin = null;
|
|
4361
|
+
}
|
|
4109
4362
|
async openPath(path) {
|
|
4110
4363
|
if (!this.workspace) {
|
|
4111
4364
|
return;
|