@mdzip/editor 2.1.0 → 2.3.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 +35 -3
- package/dist/asset-cache.d.ts +15 -0
- package/dist/asset-cache.d.ts.map +1 -1
- package/dist/asset-cache.js +57 -0
- package/dist/asset-cache.js.map +1 -1
- package/dist/diff-view-css.d.ts.map +1 -1
- package/dist/diff-view-css.js +43 -39
- package/dist/diff-view-css.js.map +1 -1
- package/dist/library-info.d.ts +2 -2
- package/dist/library-info.js +2 -2
- package/dist/mdz-archive.worker.js +111 -44
- package/dist/rendering.d.ts.map +1 -1
- package/dist/rendering.js +47 -1
- package/dist/rendering.js.map +1 -1
- package/dist/svg-inline.d.ts +11 -0
- package/dist/svg-inline.d.ts.map +1 -0
- package/dist/svg-inline.js +105 -0
- package/dist/svg-inline.js.map +1 -0
- package/dist/view-css.d.ts.map +1 -1
- package/dist/view-css.js +17 -0
- package/dist/view-css.js.map +1 -1
- package/dist/view.d.ts +119 -5
- package/dist/view.d.ts.map +1 -1
- package/dist/view.js +266 -18
- package/dist/view.js.map +1 -1
- package/package.json +2 -2
package/dist/view.js
CHANGED
|
@@ -19,6 +19,7 @@ import { escapeMarkdownImageAlt, findImageReferenceAtOffset, formatImageEditMark
|
|
|
19
19
|
import { MdzipRenderingService, defaultSafeMarkdownRenderer, groupTokensIntoChunks, chunkSourceKey, collectMdzipHeadingIds, MDZIP_HEADING_ID_PREFIX, tokenEmbedsImage, tokenIsHeading } from './rendering.js';
|
|
20
20
|
import { mdzipFrontMatterExtension } from './front-matter-extension.js';
|
|
21
21
|
import { WORKSPACE_CSS } from './view-css.js';
|
|
22
|
+
import { INLINE_SVG_SHADOW_CSS } from './svg-inline.js';
|
|
22
23
|
const STYLE_ATTR = 'data-mdzip-ws-styles';
|
|
23
24
|
const IMAGE_EXTENSIONS = /\.(jpg|jpeg|png|gif|webp|svg|bmp|ico|tiff?)$/i;
|
|
24
25
|
const isImageFile = (path) => IMAGE_EXTENSIONS.test(path);
|
|
@@ -193,6 +194,29 @@ const PREVIEW_WIDTH_ICONS_HTML = {
|
|
|
193
194
|
wide: PAGE_WIDTH_WIDE_ICON_HTML,
|
|
194
195
|
full: PAGE_WIDTH_FULL_ICON_HTML
|
|
195
196
|
};
|
|
197
|
+
const HOST_FILE_ACTION_PREFIX = 'host:';
|
|
198
|
+
/** The data type carrying dropped file addresses, if the drag has one. */
|
|
199
|
+
function droppedUriListType(dataTransfer) {
|
|
200
|
+
const types = dataTransfer?.types ?? [];
|
|
201
|
+
for (const type of ['application/vnd.code.uri-list', 'text/uri-list']) {
|
|
202
|
+
if (types.includes(type))
|
|
203
|
+
return type;
|
|
204
|
+
}
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
/** URIs from a uri-list: one per line, `#` lines are comments (RFC 2483). */
|
|
208
|
+
function parseUriList(text) {
|
|
209
|
+
return text.split('\n').map((line) => line.trim()).filter((line) => line && !line.startsWith('#'));
|
|
210
|
+
}
|
|
211
|
+
function fileActionTarget(target) {
|
|
212
|
+
return {
|
|
213
|
+
path: target.path,
|
|
214
|
+
isMarkdown: target.isMarkdown,
|
|
215
|
+
isImage: target.isImage,
|
|
216
|
+
isManifest: target.isManifest,
|
|
217
|
+
isEntryPoint: target.isEntryPoint
|
|
218
|
+
};
|
|
219
|
+
}
|
|
196
220
|
const ALL_HEADINGS = [1, 2, 3, 4, 5, 6];
|
|
197
221
|
const ALL_LAYOUT_CONTROLS = {
|
|
198
222
|
source: true,
|
|
@@ -662,6 +686,12 @@ const noSpellcheckHighlight = ViewPlugin.fromClass(class {
|
|
|
662
686
|
const IMAGE_EDIT_AFFORDANCE_CLICK_EVENT = 'mdzip-image-edit-affordance-click';
|
|
663
687
|
/** Holds an archive image's original `src` in the preview, whose `src` becomes a blob: URL. */
|
|
664
688
|
const PREVIEW_IMAGE_SOURCE_ATTRIBUTE = 'data-mdzip-source';
|
|
689
|
+
// Holds an archive-relative image source between parsing the preview HTML and
|
|
690
|
+
// the image pass (collectPendingImages), so the browser never sees it as a
|
|
691
|
+
// `src` and never requests it from the host page's server (#61).
|
|
692
|
+
const PREVIEW_IMAGE_PENDING_ATTRIBUTE = 'data-mdzip-pending-src';
|
|
693
|
+
// External, protocol-relative, data, blob and fragment URLs: left as they are.
|
|
694
|
+
const NON_ARCHIVE_IMAGE_SOURCE_RE = /^(?:[a-z][a-z\d+.-]*:|\/\/|#)/i;
|
|
665
695
|
// Preview chrome the editor adds around rendered content: removed, or
|
|
666
696
|
// unwrapped (children kept), when the selection is copied as HTML.
|
|
667
697
|
const PREVIEW_CHROME_REMOVE_SELECTOR = [
|
|
@@ -940,9 +970,13 @@ export class MdzipWorkspaceView {
|
|
|
940
970
|
/** Choice whose icon the theme menu button shows, so render() only swaps it on change. */
|
|
941
971
|
this.themeMenuIconPreference = null;
|
|
942
972
|
this.colorSchemePreference = 'system';
|
|
973
|
+
/** The host theme System follows instead of the OS setting (see setSystemColorScheme). */
|
|
974
|
+
this.systemColorSource = null;
|
|
975
|
+
/** The System item's label and icon last rendered, so render() only rewrites them on change. */
|
|
976
|
+
this.systemItemKey = null;
|
|
943
977
|
/** Re-resolves the scheme when the OS setting changes while 'system' is chosen. */
|
|
944
978
|
this.onSystemColorSchemeChange = () => {
|
|
945
|
-
if (this.colorSchemePreference !== 'system' || this.destroyed) {
|
|
979
|
+
if (this.colorSchemePreference !== 'system' || this.systemColorSource || this.destroyed) {
|
|
946
980
|
return;
|
|
947
981
|
}
|
|
948
982
|
const next = this.resolveColorScheme('system');
|
|
@@ -1159,6 +1193,7 @@ export class MdzipWorkspaceView {
|
|
|
1159
1193
|
// matches() does not re-run on every snapshot render of the same entry.
|
|
1160
1194
|
this.entryMatchMissKey = null;
|
|
1161
1195
|
this.entryGeneration = 0;
|
|
1196
|
+
this.pendingAddFilesDir = null;
|
|
1162
1197
|
this.options = options;
|
|
1163
1198
|
this.controlPolicy = resolveMdzipControlPolicy(options.controls);
|
|
1164
1199
|
this.navigationMode = options.navigationMode ?? 'editor';
|
|
@@ -1170,6 +1205,7 @@ export class MdzipWorkspaceView {
|
|
|
1170
1205
|
this.layout = options.initialLayout ?? defaultLayoutForPolicy(this.controlPolicy);
|
|
1171
1206
|
this.systemColorSchemeQuery = container.ownerDocument.defaultView?.matchMedia?.('(prefers-color-scheme: dark)') ?? null;
|
|
1172
1207
|
this.systemColorSchemeQuery?.addEventListener?.('change', this.onSystemColorSchemeChange);
|
|
1208
|
+
this.systemColorSource = options.systemColorScheme ?? null;
|
|
1173
1209
|
this.colorSchemePreference = options.initialColorScheme ?? 'system';
|
|
1174
1210
|
this.colorScheme = this.resolveColorScheme(this.colorSchemePreference);
|
|
1175
1211
|
this.navVisible = options.navigationButtonActive ?? this.navVisible;
|
|
@@ -1269,6 +1305,7 @@ export class MdzipWorkspaceView {
|
|
|
1269
1305
|
this.elDeleteDialogText = q('[data-ref="delete-dialog-text"]');
|
|
1270
1306
|
this.elDeleteConfirmBtn = q('[data-ref="delete-confirm-btn"]');
|
|
1271
1307
|
this.elReplaceInput = q('[data-ref="replace-input"]');
|
|
1308
|
+
this.elAddFilesInput = q('[data-ref="add-files-input"]');
|
|
1272
1309
|
this.elTooltip = q('[data-ref="tooltip"]');
|
|
1273
1310
|
this.elCopyToast = q('[data-ref="copy-toast"]');
|
|
1274
1311
|
this.elEmptyState = q('[data-ref="empty-state"]');
|
|
@@ -1740,6 +1777,10 @@ export class MdzipWorkspaceView {
|
|
|
1740
1777
|
this.applyExternalLinkPolicy(this.elPreviewContent);
|
|
1741
1778
|
}
|
|
1742
1779
|
applyExternalLinkPolicy(root) {
|
|
1780
|
+
for (const host of Array.from(root.querySelectorAll('.mdzip-inline-svg'))) {
|
|
1781
|
+
if (host.shadowRoot)
|
|
1782
|
+
this.applyExternalLinkPolicy(host.shadowRoot);
|
|
1783
|
+
}
|
|
1743
1784
|
const policy = this.options.externalLinks;
|
|
1744
1785
|
for (const link of Array.from(root.querySelectorAll('a[href]'))) {
|
|
1745
1786
|
const original = this.externalLinkOriginals.get(link);
|
|
@@ -2081,7 +2122,7 @@ export class MdzipWorkspaceView {
|
|
|
2081
2122
|
* has resolved and had its final `src` assigned (or there are none).
|
|
2082
2123
|
*/
|
|
2083
2124
|
mountProgressivePreview(html, snapshot, context, generation, animateImageHydration) {
|
|
2084
|
-
this.elPreviewContent.
|
|
2125
|
+
this.elPreviewContent.replaceChildren(this.inertPreviewFragment(html));
|
|
2085
2126
|
// Cheap pass over every <img> happens before extensions/code-block
|
|
2086
2127
|
// controls mount, same relative order as always — see
|
|
2087
2128
|
// collectPendingImages for why it's split from the (expensive) slot
|
|
@@ -2100,9 +2141,9 @@ export class MdzipWorkspaceView {
|
|
|
2100
2141
|
}
|
|
2101
2142
|
/**
|
|
2102
2143
|
* Cheap, attribute-only pass over every `<img>` under `roots`: no DOM tree
|
|
2103
|
-
* mutation. Archive-relative sources
|
|
2104
|
-
*
|
|
2105
|
-
*
|
|
2144
|
+
* mutation. Archive-relative sources (already moved off `src` by
|
|
2145
|
+
* {@link inertPreviewFragment}, so the browser never requested them) are
|
|
2146
|
+
* returned for {@link hydrateImages};
|
|
2106
2147
|
* external/data/fragment sources are left untouched (just get their align
|
|
2107
2148
|
* class, if any — they never get a slot). Returns `[]` without touching
|
|
2108
2149
|
* anything when there's no asset session to resolve archive images
|
|
@@ -2119,6 +2160,69 @@ export class MdzipWorkspaceView {
|
|
|
2119
2160
|
* the image, since a fresh `<img>` element never carries over its
|
|
2120
2161
|
* predecessor's already-resolved `src`.
|
|
2121
2162
|
*/
|
|
2163
|
+
/**
|
|
2164
|
+
* Parses preview HTML without loading anything: a `<template>`'s content is
|
|
2165
|
+
* inert, whereas `innerHTML` on any element of a live document (attached or
|
|
2166
|
+
* not) starts fetching every `<img src>` at once, resolved against the host
|
|
2167
|
+
* page's URL. Archive-relative sources move to a holding attribute for
|
|
2168
|
+
* {@link collectPendingImages}; with no asset session to resolve them
|
|
2169
|
+
* against, they're left as they are (that pass then skips them too).
|
|
2170
|
+
*/
|
|
2171
|
+
inertPreviewFragment(html) {
|
|
2172
|
+
const template = this.elPreviewContent.ownerDocument.createElement('template');
|
|
2173
|
+
template.innerHTML = html;
|
|
2174
|
+
if (this.assetSession) {
|
|
2175
|
+
for (const image of Array.from(template.content.querySelectorAll('img[src]'))) {
|
|
2176
|
+
const source = image.getAttribute('src');
|
|
2177
|
+
if (!source || NON_ARCHIVE_IMAGE_SOURCE_RE.test(source))
|
|
2178
|
+
continue;
|
|
2179
|
+
image.setAttribute(PREVIEW_IMAGE_PENDING_ATTRIBUTE, source);
|
|
2180
|
+
image.removeAttribute('src');
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
return template.content;
|
|
2184
|
+
}
|
|
2185
|
+
inlineSvgLinksEnabled() {
|
|
2186
|
+
return this.options.inlineSvgLinks !== false;
|
|
2187
|
+
}
|
|
2188
|
+
/**
|
|
2189
|
+
* Replaces `image` with a shadow-root host holding a copy of the cleaned
|
|
2190
|
+
* `svg` (see `svg-inline.ts`). The host carries the image's alt text,
|
|
2191
|
+
* size, alignment and source path, so the layout matches the `<img>`.
|
|
2192
|
+
*/
|
|
2193
|
+
mountInlineSvg(image, svg) {
|
|
2194
|
+
const doc = this.elPreviewContent.ownerDocument;
|
|
2195
|
+
const host = doc.createElement('span');
|
|
2196
|
+
host.className = 'mdzip-inline-svg';
|
|
2197
|
+
for (const name of [PREVIEW_IMAGE_SOURCE_ATTRIBUTE, 'title']) {
|
|
2198
|
+
const value = image.getAttribute(name);
|
|
2199
|
+
if (value !== null)
|
|
2200
|
+
host.setAttribute(name, value);
|
|
2201
|
+
}
|
|
2202
|
+
for (const className of Array.from(image.classList)) {
|
|
2203
|
+
if (className !== 'mdzip-image-loading')
|
|
2204
|
+
host.classList.add(className);
|
|
2205
|
+
}
|
|
2206
|
+
const alignClass = rawHtmlImageAlignClass(image);
|
|
2207
|
+
if (alignClass)
|
|
2208
|
+
host.classList.add(alignClass);
|
|
2209
|
+
const alt = image.getAttribute('alt');
|
|
2210
|
+
host.setAttribute('role', 'group');
|
|
2211
|
+
if (alt)
|
|
2212
|
+
host.setAttribute('aria-label', alt);
|
|
2213
|
+
const width = image.getAttribute('width');
|
|
2214
|
+
if (width && /^\d+(\.\d+)?$/.test(width))
|
|
2215
|
+
host.style.width = `${width}px`;
|
|
2216
|
+
else if (width)
|
|
2217
|
+
host.style.width = width;
|
|
2218
|
+
const shadow = host.attachShadow({ mode: 'open' });
|
|
2219
|
+
const style = doc.createElement('style');
|
|
2220
|
+
style.textContent = INLINE_SVG_SHADOW_CSS + (host.style.width ? 'svg{width:100%}' : '');
|
|
2221
|
+
shadow.append(style, svg.cloneNode(true));
|
|
2222
|
+
this.applyExternalLinkPolicy(shadow);
|
|
2223
|
+
image.replaceWith(host);
|
|
2224
|
+
return host;
|
|
2225
|
+
}
|
|
2122
2226
|
collectPendingImages(roots, context, generation, animateImageHydration) {
|
|
2123
2227
|
if (!this.assetSession) {
|
|
2124
2228
|
return [];
|
|
@@ -2127,9 +2231,12 @@ export class MdzipWorkspaceView {
|
|
|
2127
2231
|
for (const root of roots) {
|
|
2128
2232
|
for (const image of Array.from(root.querySelectorAll('img'))) {
|
|
2129
2233
|
applyRawHtmlImageSizeAttributes(image);
|
|
2130
|
-
const
|
|
2234
|
+
const held = image.getAttribute(PREVIEW_IMAGE_PENDING_ATTRIBUTE);
|
|
2235
|
+
if (held !== null)
|
|
2236
|
+
image.removeAttribute(PREVIEW_IMAGE_PENDING_ATTRIBUTE);
|
|
2237
|
+
const source = held ?? image.getAttribute('src');
|
|
2131
2238
|
// Leave external, protocol-relative, data, and fragment URLs untouched.
|
|
2132
|
-
if (!source ||
|
|
2239
|
+
if (!source || NON_ARCHIVE_IMAGE_SOURCE_RE.test(source)) {
|
|
2133
2240
|
const alignClass = rawHtmlImageAlignClass(image);
|
|
2134
2241
|
if (alignClass) {
|
|
2135
2242
|
image.classList.add(alignClass);
|
|
@@ -2138,7 +2245,14 @@ export class MdzipWorkspaceView {
|
|
|
2138
2245
|
}
|
|
2139
2246
|
// The src is replaced by a blob: URL; Copy as HTML restores it from here.
|
|
2140
2247
|
image.setAttribute(PREVIEW_IMAGE_SOURCE_ATTRIBUTE, source);
|
|
2141
|
-
const
|
|
2248
|
+
const inlineSvg = this.inlineSvgLinksEnabled()
|
|
2249
|
+
? this.assetSession.knownInlineSvg(source, context.currentPath)
|
|
2250
|
+
: null;
|
|
2251
|
+
if (inlineSvg) {
|
|
2252
|
+
this.mountInlineSvg(image, inlineSvg);
|
|
2253
|
+
continue;
|
|
2254
|
+
}
|
|
2255
|
+
const known = inlineSvg === undefined ? undefined : this.assetSession.resolveKnownImage(source, context.currentPath);
|
|
2142
2256
|
if (known) {
|
|
2143
2257
|
if (known.width && known.height && !image.hasAttribute('width') && !image.hasAttribute('height')) {
|
|
2144
2258
|
image.setAttribute('width', String(known.width));
|
|
@@ -2208,6 +2322,26 @@ export class MdzipWorkspaceView {
|
|
|
2208
2322
|
return slot;
|
|
2209
2323
|
};
|
|
2210
2324
|
const hydrate = (image, slot, source) => {
|
|
2325
|
+
if (this.inlineSvgLinksEnabled() && session.knownInlineSvg(source, context.currentPath) !== null) {
|
|
2326
|
+
void session.resolveInlineSvg(source, context.currentPath).then((svg) => {
|
|
2327
|
+
if (generation !== this.previewGeneration || context.signal.aborted) {
|
|
2328
|
+
settle();
|
|
2329
|
+
return;
|
|
2330
|
+
}
|
|
2331
|
+
if (!svg) {
|
|
2332
|
+
hydrateImage(image, slot, source);
|
|
2333
|
+
return;
|
|
2334
|
+
}
|
|
2335
|
+
image.classList.remove('mdzip-image-loading');
|
|
2336
|
+
this.mountInlineSvg(image, svg);
|
|
2337
|
+
this.openImageSlot(slot);
|
|
2338
|
+
settle();
|
|
2339
|
+
});
|
|
2340
|
+
return;
|
|
2341
|
+
}
|
|
2342
|
+
hydrateImage(image, slot, source);
|
|
2343
|
+
};
|
|
2344
|
+
const hydrateImage = (image, slot, source) => {
|
|
2211
2345
|
void session.resolveImage(source, context.currentPath).then((resolved) => {
|
|
2212
2346
|
if (generation !== this.previewGeneration || context.signal.aborted) {
|
|
2213
2347
|
settle();
|
|
@@ -2754,7 +2888,7 @@ export class MdzipWorkspaceView {
|
|
|
2754
2888
|
const doc = this.elPreviewContent.ownerDocument;
|
|
2755
2889
|
const wrapper = doc.createElement('div');
|
|
2756
2890
|
wrapper.className = 'mdzip-chunk';
|
|
2757
|
-
wrapper.
|
|
2891
|
+
wrapper.appendChild(this.inertPreviewFragment(html));
|
|
2758
2892
|
if (beforeNode && beforeNode.parentNode === this.elPreviewContent) {
|
|
2759
2893
|
this.elPreviewContent.insertBefore(wrapper, beforeNode);
|
|
2760
2894
|
}
|
|
@@ -3590,14 +3724,26 @@ export class MdzipWorkspaceView {
|
|
|
3590
3724
|
button.classList.toggle('active', this.colorSchemePreference === preference);
|
|
3591
3725
|
button.setAttribute('aria-checked', String(this.colorSchemePreference === preference));
|
|
3592
3726
|
}
|
|
3727
|
+
const systemIcon = this.systemColorSource?.icon
|
|
3728
|
+
? `<span class="${TOOLBAR_ICON_CLASS} mdzip-host-icon" aria-hidden="true">${this.systemColorSource.icon}</span>`
|
|
3729
|
+
: SYSTEM_THEME_ICON_HTML;
|
|
3730
|
+
const systemItemLabel = this.systemColorSource?.label ?? 'System setting';
|
|
3731
|
+
const systemItemKey = `${systemItemLabel}\n${systemIcon}`;
|
|
3732
|
+
if (this.systemItemKey !== systemItemKey) {
|
|
3733
|
+
this.systemItemKey = systemItemKey;
|
|
3734
|
+
this.themeMenuIconPreference = null;
|
|
3735
|
+
this.elSystemThemeBtn.innerHTML = systemIcon;
|
|
3736
|
+
setTooltip(this.elSystemThemeBtn, systemItemLabel);
|
|
3737
|
+
this.elSystemThemeBtn.setAttribute('aria-label', systemItemLabel);
|
|
3738
|
+
}
|
|
3593
3739
|
if (this.themeMenuIconPreference !== this.colorSchemePreference) {
|
|
3594
3740
|
this.themeMenuIconPreference = this.colorSchemePreference;
|
|
3595
3741
|
this.elThemeMenuIcon.innerHTML = this.colorSchemePreference === 'system'
|
|
3596
|
-
?
|
|
3742
|
+
? systemIcon
|
|
3597
3743
|
: this.colorSchemePreference === 'dark' ? DARK_THEME_ICON_HTML : LIGHT_THEME_ICON_HTML;
|
|
3598
3744
|
}
|
|
3599
3745
|
const schemeLabel = this.colorSchemePreference === 'system'
|
|
3600
|
-
?
|
|
3746
|
+
? `${this.systemColorSource?.label ?? 'System'} (${this.colorScheme})`
|
|
3601
3747
|
: this.colorSchemePreference === 'dark' ? 'Dark' : 'Light';
|
|
3602
3748
|
setTooltip(this.elThemeMenuBtn, `Color scheme: ${schemeLabel}`);
|
|
3603
3749
|
this.elThemeMenuBtn.setAttribute('aria-label', `Color scheme: ${schemeLabel}`);
|
|
@@ -3829,6 +3975,7 @@ export class MdzipWorkspaceView {
|
|
|
3829
3975
|
}
|
|
3830
3976
|
e.preventDefault();
|
|
3831
3977
|
this.selectAllPreview();
|
|
3978
|
+
this.keepPreviewSelectAll();
|
|
3832
3979
|
});
|
|
3833
3980
|
this.elNavBtn.addEventListener('click', () => {
|
|
3834
3981
|
if (!this.controlPolicy.navigation) {
|
|
@@ -4341,6 +4488,15 @@ export class MdzipWorkspaceView {
|
|
|
4341
4488
|
void this.deleteFile(path);
|
|
4342
4489
|
}
|
|
4343
4490
|
});
|
|
4491
|
+
this.elAddFilesInput.addEventListener('change', () => {
|
|
4492
|
+
const files = Array.from(this.elAddFilesInput.files ?? []);
|
|
4493
|
+
const dir = this.pendingAddFilesDir;
|
|
4494
|
+
this.elAddFilesInput.value = '';
|
|
4495
|
+
this.pendingAddFilesDir = null;
|
|
4496
|
+
if (files.length > 0 && dir !== null) {
|
|
4497
|
+
void this.addDroppedFiles(dir, files);
|
|
4498
|
+
}
|
|
4499
|
+
});
|
|
4344
4500
|
this.elReplaceInput.addEventListener('change', () => {
|
|
4345
4501
|
const file = this.elReplaceInput.files?.[0];
|
|
4346
4502
|
const path = this.pendingReplacePath;
|
|
@@ -4366,7 +4522,9 @@ export class MdzipWorkspaceView {
|
|
|
4366
4522
|
this.elPreviewPane.focus({ preventScroll: true });
|
|
4367
4523
|
});
|
|
4368
4524
|
this.elPreviewPane.addEventListener('click', (event) => {
|
|
4369
|
-
|
|
4525
|
+
// The event's path, not its target: clicks inside an inline SVG's shadow root are
|
|
4526
|
+
// retargeted to the host, and SVG links are SVGAElements, not HTMLAnchorElements.
|
|
4527
|
+
const link = event.composedPath().find((node) => node.nodeType === 1 && node.localName === 'a' && node.hasAttribute('href')) ?? null;
|
|
4370
4528
|
const snapshot = this.workspace?.snapshot();
|
|
4371
4529
|
if (!link || !snapshot) {
|
|
4372
4530
|
return;
|
|
@@ -4764,8 +4922,9 @@ export class MdzipWorkspaceView {
|
|
|
4764
4922
|
}
|
|
4765
4923
|
showTooltipForElementNow(element) {
|
|
4766
4924
|
const rect = element.getBoundingClientRect();
|
|
4767
|
-
// A menu item's tip goes beside it, so it doesn't cover the items above
|
|
4768
|
-
|
|
4925
|
+
// A menu item's tip goes beside it, so it doesn't cover the items above:
|
|
4926
|
+
// context menus and the toolbar's dropdowns (color scheme, page width).
|
|
4927
|
+
const placement = element.closest('.nav-context-menu, .theme-popover, .toolbar-popover') ? 'side' : 'above';
|
|
4769
4928
|
this.showTooltip(element.dataset['tooltip'] ?? '', rect.left + rect.width / 2, rect.top, placement, rect);
|
|
4770
4929
|
}
|
|
4771
4930
|
/**
|
|
@@ -5827,6 +5986,31 @@ export class MdzipWorkspaceView {
|
|
|
5827
5986
|
end: range.endContainer, endOffset: range.endOffset
|
|
5828
5987
|
};
|
|
5829
5988
|
}
|
|
5989
|
+
/**
|
|
5990
|
+
* Some hosts run their own Select All after the key event, even when the
|
|
5991
|
+
* editor handled it: a VS Code webview selects the whole page, source pane
|
|
5992
|
+
* included. For a moment after Ctrl+A, a selection that reaches outside
|
|
5993
|
+
* the preview is put back to the preview's own.
|
|
5994
|
+
*/
|
|
5995
|
+
keepPreviewSelectAll() {
|
|
5996
|
+
const doc = this.elPreviewContent.ownerDocument;
|
|
5997
|
+
const stop = () => doc.removeEventListener('selectionchange', onChange);
|
|
5998
|
+
const onChange = () => {
|
|
5999
|
+
const selection = doc.defaultView?.getSelection();
|
|
6000
|
+
if (this.destroyed || !this.previewSelectAll || !selection || selection.rangeCount === 0) {
|
|
6001
|
+
stop();
|
|
6002
|
+
return;
|
|
6003
|
+
}
|
|
6004
|
+
const range = selection.getRangeAt(0);
|
|
6005
|
+
if (range.intersectsNode(this.elPreviewContent) && !this.elPreviewContent.contains(range.commonAncestorContainer)
|
|
6006
|
+
&& range.commonAncestorContainer !== this.elPreviewContent) {
|
|
6007
|
+
stop();
|
|
6008
|
+
this.selectAllPreview();
|
|
6009
|
+
}
|
|
6010
|
+
};
|
|
6011
|
+
doc.addEventListener('selectionchange', onChange);
|
|
6012
|
+
setTimeout(stop, 500);
|
|
6013
|
+
}
|
|
5830
6014
|
/**
|
|
5831
6015
|
* Re-copies a Ctrl+C selection with its images embedded. Best effort: the
|
|
5832
6016
|
* synchronous copy already on the clipboard stays if this fails, and it's
|
|
@@ -6739,11 +6923,27 @@ export class MdzipWorkspaceView {
|
|
|
6739
6923
|
this.colorScheme = next;
|
|
6740
6924
|
this.render();
|
|
6741
6925
|
}
|
|
6742
|
-
/**
|
|
6926
|
+
/**
|
|
6927
|
+
* Sets what the menu's System choice follows: the host theme in `source`
|
|
6928
|
+
* (its current scheme, and optionally the choice's name and icon), or the
|
|
6929
|
+
* operating system's setting again with `null`. Call it whenever the host
|
|
6930
|
+
* theme changes; while System is chosen the preview switches with it,
|
|
6931
|
+
* keeping the rendered preview in place. Like `setColorScheme`, it fires
|
|
6932
|
+
* no change callbacks.
|
|
6933
|
+
*/
|
|
6934
|
+
setSystemColorScheme(source) {
|
|
6935
|
+
this.systemColorSource = source;
|
|
6936
|
+
this.colorScheme = this.resolveColorScheme(this.colorSchemePreference);
|
|
6937
|
+
this.render();
|
|
6938
|
+
}
|
|
6939
|
+
/** The light/dark scheme a choice means right now; `'system'` reads the host theme or the OS setting. */
|
|
6743
6940
|
resolveColorScheme(preference) {
|
|
6744
6941
|
if (preference !== 'system') {
|
|
6745
6942
|
return preference;
|
|
6746
6943
|
}
|
|
6944
|
+
if (this.systemColorSource) {
|
|
6945
|
+
return this.systemColorSource.colorScheme;
|
|
6946
|
+
}
|
|
6747
6947
|
return this.systemColorSchemeQuery?.matches ? 'dark' : 'light';
|
|
6748
6948
|
}
|
|
6749
6949
|
themeMenuItems() {
|
|
@@ -6906,11 +7106,14 @@ export class MdzipWorkspaceView {
|
|
|
6906
7106
|
}
|
|
6907
7107
|
return [
|
|
6908
7108
|
{ action: 'new-file', label: 'New .md File' },
|
|
6909
|
-
{ action: 'new-folder', label: 'New Folder' }
|
|
7109
|
+
{ action: 'new-folder', label: 'New Folder' },
|
|
7110
|
+
null,
|
|
7111
|
+
{ action: 'add-files', label: 'Add Files…' }
|
|
6910
7112
|
];
|
|
6911
7113
|
}
|
|
7114
|
+
const hostItems = this.fileActionItems(target);
|
|
6912
7115
|
if (target.isManifest) {
|
|
6913
|
-
return [{ action: 'download', label: 'Download' }];
|
|
7116
|
+
return [...hostItems, ...(hostItems.length ? [null] : []), { action: 'download', label: 'Download' }];
|
|
6914
7117
|
}
|
|
6915
7118
|
const groups = [];
|
|
6916
7119
|
if (canMutate) {
|
|
@@ -6942,6 +7145,9 @@ export class MdzipWorkspaceView {
|
|
|
6942
7145
|
}
|
|
6943
7146
|
groups.push(editGroup);
|
|
6944
7147
|
}
|
|
7148
|
+
if (hostItems.length > 0) {
|
|
7149
|
+
groups.push(hostItems);
|
|
7150
|
+
}
|
|
6945
7151
|
groups.push([{ action: 'download', label: 'Download' }]);
|
|
6946
7152
|
if (canMutate && !target.isEntryPoint) {
|
|
6947
7153
|
groups.push([{
|
|
@@ -6951,6 +7157,13 @@ export class MdzipWorkspaceView {
|
|
|
6951
7157
|
}
|
|
6952
7158
|
return groups.flatMap((group, index) => index === 0 ? group : [null, ...group]);
|
|
6953
7159
|
}
|
|
7160
|
+
/** Menu items for the host's {@link MdzipFileAction}s that apply to this file. */
|
|
7161
|
+
fileActionItems(target) {
|
|
7162
|
+
const file = fileActionTarget(target);
|
|
7163
|
+
return (this.options.hostFileActions ?? [])
|
|
7164
|
+
.filter((item) => item.appliesTo?.(file) ?? true)
|
|
7165
|
+
.map((item) => ({ action: `${HOST_FILE_ACTION_PREFIX}${item.id}`, label: item.label }));
|
|
7166
|
+
}
|
|
6954
7167
|
async handleNavMenuAction(action) {
|
|
6955
7168
|
const state = this.contextMenuState;
|
|
6956
7169
|
this.contextMenuState = null;
|
|
@@ -6971,6 +7184,12 @@ export class MdzipWorkspaceView {
|
|
|
6971
7184
|
this.openNameDialog({ mode: 'new-folder', dir: target.path, value: 'new-folder' });
|
|
6972
7185
|
}
|
|
6973
7186
|
break;
|
|
7187
|
+
case 'add-files':
|
|
7188
|
+
if (target.kind === 'directory') {
|
|
7189
|
+
this.pendingAddFilesDir = target.path;
|
|
7190
|
+
this.elAddFilesInput.click();
|
|
7191
|
+
}
|
|
7192
|
+
break;
|
|
6974
7193
|
case 'rename':
|
|
6975
7194
|
if (target.kind === 'file') {
|
|
6976
7195
|
this.openNameDialog({ mode: 'rename', oldPath: target.path, value: target.path });
|
|
@@ -7025,6 +7244,11 @@ export class MdzipWorkspaceView {
|
|
|
7025
7244
|
}
|
|
7026
7245
|
break;
|
|
7027
7246
|
default:
|
|
7247
|
+
if (action.startsWith(HOST_FILE_ACTION_PREFIX) && target.kind === 'file') {
|
|
7248
|
+
const id = action.slice(HOST_FILE_ACTION_PREFIX.length);
|
|
7249
|
+
const item = (this.options.hostFileActions ?? []).find((candidate) => candidate.id === id);
|
|
7250
|
+
await item?.run(fileActionTarget(target));
|
|
7251
|
+
}
|
|
7028
7252
|
break;
|
|
7029
7253
|
}
|
|
7030
7254
|
}
|
|
@@ -7277,7 +7501,8 @@ export class MdzipWorkspaceView {
|
|
|
7277
7501
|
}
|
|
7278
7502
|
const internal = this.dragSourcePath !== null
|
|
7279
7503
|
|| (e.dataTransfer?.types.includes('application/x-mdzip-path') ?? false);
|
|
7280
|
-
const external = e.dataTransfer?.types.includes('Files') ?? false
|
|
7504
|
+
const external = (e.dataTransfer?.types.includes('Files') ?? false)
|
|
7505
|
+
|| (this.options.resolveDroppedUris !== undefined && droppedUriListType(e.dataTransfer) !== null);
|
|
7281
7506
|
if (!internal && !external) {
|
|
7282
7507
|
return;
|
|
7283
7508
|
}
|
|
@@ -7312,6 +7537,15 @@ export class MdzipWorkspaceView {
|
|
|
7312
7537
|
if (files && files.length > 0) {
|
|
7313
7538
|
e.preventDefault();
|
|
7314
7539
|
void this.addDroppedFiles(targetDir, Array.from(files));
|
|
7540
|
+
return;
|
|
7541
|
+
}
|
|
7542
|
+
const uriListType = droppedUriListType(e.dataTransfer);
|
|
7543
|
+
const resolveUris = this.options.resolveDroppedUris;
|
|
7544
|
+
if (uriListType && resolveUris) {
|
|
7545
|
+
e.preventDefault();
|
|
7546
|
+
const uris = parseUriList(e.dataTransfer?.getData(uriListType) ?? '');
|
|
7547
|
+
if (uris.length > 0)
|
|
7548
|
+
void this.addDroppedUris(targetDir, uris, resolveUris);
|
|
7315
7549
|
}
|
|
7316
7550
|
});
|
|
7317
7551
|
}
|
|
@@ -7337,6 +7571,19 @@ export class MdzipWorkspaceView {
|
|
|
7337
7571
|
this.options.onFailed?.(error);
|
|
7338
7572
|
}
|
|
7339
7573
|
}
|
|
7574
|
+
async addDroppedUris(targetDir, uris, resolveUris) {
|
|
7575
|
+
try {
|
|
7576
|
+
const resolved = await resolveUris(uris);
|
|
7577
|
+
// `File` here is an imported icon; take the browser's class from the page's window.
|
|
7578
|
+
const FileClass = this.elPreviewContent.ownerDocument.defaultView?.File ?? globalThis.File;
|
|
7579
|
+
const files = resolved.map(({ name, bytes }) => new FileClass([bytes], name));
|
|
7580
|
+
if (files.length > 0)
|
|
7581
|
+
await this.addDroppedFiles(targetDir, files);
|
|
7582
|
+
}
|
|
7583
|
+
catch (error) {
|
|
7584
|
+
this.options.onFailed?.(error);
|
|
7585
|
+
}
|
|
7586
|
+
}
|
|
7340
7587
|
async addDroppedFiles(targetDir, files) {
|
|
7341
7588
|
const workspace = this.workspace;
|
|
7342
7589
|
if (!workspace) {
|
|
@@ -8038,6 +8285,7 @@ const SHELL_HTML = `
|
|
|
8038
8285
|
</div>
|
|
8039
8286
|
|
|
8040
8287
|
<input type="file" data-ref="replace-input" hidden />
|
|
8288
|
+
<input type="file" data-ref="add-files-input" multiple hidden />
|
|
8041
8289
|
|
|
8042
8290
|
<div class="nav-context-menu" data-ref="nav-menu" hidden role="menu"></div>
|
|
8043
8291
|
|