@mdzip/editor 2.2.0 → 2.4.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 +47 -3
- package/dist/asset-cache.d.ts +17 -0
- package/dist/asset-cache.d.ts.map +1 -1
- package/dist/asset-cache.js +61 -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/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 +73 -0
- package/dist/view-css.js.map +1 -1
- package/dist/view.d.ts +80 -3
- package/dist/view.d.ts.map +1 -1
- package/dist/view.js +281 -14
- 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,17 @@ 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;
|
|
695
|
+
// A raw draw.io diagram (.drawio, .dio). The preview can't draw these yet (#59),
|
|
696
|
+
// so an image pointing at one shows a placeholder card instead of a broken
|
|
697
|
+
// image. `.drawio.svg` and `.drawio.png` end in .svg/.png and stay images.
|
|
698
|
+
const DRAWIO_PATH_RE = /\.(?:drawio|dio)$/i;
|
|
699
|
+
const DRAWIO_PLACEHOLDER_CLASS = 'mdzip-drawio-placeholder';
|
|
665
700
|
// Preview chrome the editor adds around rendered content: removed, or
|
|
666
701
|
// unwrapped (children kept), when the selection is copied as HTML.
|
|
667
702
|
const PREVIEW_CHROME_REMOVE_SELECTOR = [
|
|
@@ -670,8 +705,9 @@ const PREVIEW_CHROME_REMOVE_SELECTOR = [
|
|
|
670
705
|
'[data-mdzip-image-edit-affordance]'
|
|
671
706
|
].join(',');
|
|
672
707
|
// Subtrees left out of a plain-text copy: the chrome above, plus markup whose
|
|
673
|
-
// text isn't document text (a drawn diagram's SVG carries its stylesheet
|
|
674
|
-
|
|
708
|
+
// text isn't document text (a drawn diagram's SVG carries its stylesheet, the
|
|
709
|
+
// draw.io placeholder's explanation is the editor's, not the document's).
|
|
710
|
+
const PLAIN_TEXT_SKIP_SELECTOR = `${PREVIEW_CHROME_REMOVE_SELECTOR},style,script,template,svg,button,.${DRAWIO_PLACEHOLDER_CLASS}`;
|
|
675
711
|
/**
|
|
676
712
|
* The text of rendered preview markup for a plain-text copy: the document's
|
|
677
713
|
* own text, without style or script contents, drawn diagrams or the editor's
|
|
@@ -1163,6 +1199,7 @@ export class MdzipWorkspaceView {
|
|
|
1163
1199
|
// matches() does not re-run on every snapshot render of the same entry.
|
|
1164
1200
|
this.entryMatchMissKey = null;
|
|
1165
1201
|
this.entryGeneration = 0;
|
|
1202
|
+
this.pendingAddFilesDir = null;
|
|
1166
1203
|
this.options = options;
|
|
1167
1204
|
this.controlPolicy = resolveMdzipControlPolicy(options.controls);
|
|
1168
1205
|
this.navigationMode = options.navigationMode ?? 'editor';
|
|
@@ -1274,6 +1311,7 @@ export class MdzipWorkspaceView {
|
|
|
1274
1311
|
this.elDeleteDialogText = q('[data-ref="delete-dialog-text"]');
|
|
1275
1312
|
this.elDeleteConfirmBtn = q('[data-ref="delete-confirm-btn"]');
|
|
1276
1313
|
this.elReplaceInput = q('[data-ref="replace-input"]');
|
|
1314
|
+
this.elAddFilesInput = q('[data-ref="add-files-input"]');
|
|
1277
1315
|
this.elTooltip = q('[data-ref="tooltip"]');
|
|
1278
1316
|
this.elCopyToast = q('[data-ref="copy-toast"]');
|
|
1279
1317
|
this.elEmptyState = q('[data-ref="empty-state"]');
|
|
@@ -1745,6 +1783,10 @@ export class MdzipWorkspaceView {
|
|
|
1745
1783
|
this.applyExternalLinkPolicy(this.elPreviewContent);
|
|
1746
1784
|
}
|
|
1747
1785
|
applyExternalLinkPolicy(root) {
|
|
1786
|
+
for (const host of Array.from(root.querySelectorAll('.mdzip-inline-svg'))) {
|
|
1787
|
+
if (host.shadowRoot)
|
|
1788
|
+
this.applyExternalLinkPolicy(host.shadowRoot);
|
|
1789
|
+
}
|
|
1748
1790
|
const policy = this.options.externalLinks;
|
|
1749
1791
|
for (const link of Array.from(root.querySelectorAll('a[href]'))) {
|
|
1750
1792
|
const original = this.externalLinkOriginals.get(link);
|
|
@@ -2086,7 +2128,7 @@ export class MdzipWorkspaceView {
|
|
|
2086
2128
|
* has resolved and had its final `src` assigned (or there are none).
|
|
2087
2129
|
*/
|
|
2088
2130
|
mountProgressivePreview(html, snapshot, context, generation, animateImageHydration) {
|
|
2089
|
-
this.elPreviewContent.
|
|
2131
|
+
this.elPreviewContent.replaceChildren(this.inertPreviewFragment(html));
|
|
2090
2132
|
// Cheap pass over every <img> happens before extensions/code-block
|
|
2091
2133
|
// controls mount, same relative order as always — see
|
|
2092
2134
|
// collectPendingImages for why it's split from the (expensive) slot
|
|
@@ -2105,9 +2147,9 @@ export class MdzipWorkspaceView {
|
|
|
2105
2147
|
}
|
|
2106
2148
|
/**
|
|
2107
2149
|
* Cheap, attribute-only pass over every `<img>` under `roots`: no DOM tree
|
|
2108
|
-
* mutation. Archive-relative sources
|
|
2109
|
-
*
|
|
2110
|
-
*
|
|
2150
|
+
* mutation. Archive-relative sources (already moved off `src` by
|
|
2151
|
+
* {@link inertPreviewFragment}, so the browser never requested them) are
|
|
2152
|
+
* returned for {@link hydrateImages};
|
|
2111
2153
|
* external/data/fragment sources are left untouched (just get their align
|
|
2112
2154
|
* class, if any — they never get a slot). Returns `[]` without touching
|
|
2113
2155
|
* anything when there's no asset session to resolve archive images
|
|
@@ -2124,6 +2166,130 @@ export class MdzipWorkspaceView {
|
|
|
2124
2166
|
* the image, since a fresh `<img>` element never carries over its
|
|
2125
2167
|
* predecessor's already-resolved `src`.
|
|
2126
2168
|
*/
|
|
2169
|
+
/**
|
|
2170
|
+
* Parses preview HTML without loading anything: a `<template>`'s content is
|
|
2171
|
+
* inert, whereas `innerHTML` on any element of a live document (attached or
|
|
2172
|
+
* not) starts fetching every `<img src>` at once, resolved against the host
|
|
2173
|
+
* page's URL. Archive-relative sources move to a holding attribute for
|
|
2174
|
+
* {@link collectPendingImages}; with no asset session to resolve them
|
|
2175
|
+
* against, they're left as they are (that pass then skips them too).
|
|
2176
|
+
*/
|
|
2177
|
+
inertPreviewFragment(html) {
|
|
2178
|
+
const template = this.elPreviewContent.ownerDocument.createElement('template');
|
|
2179
|
+
template.innerHTML = html;
|
|
2180
|
+
if (this.assetSession) {
|
|
2181
|
+
for (const image of Array.from(template.content.querySelectorAll('img[src]'))) {
|
|
2182
|
+
const source = image.getAttribute('src');
|
|
2183
|
+
if (!source || NON_ARCHIVE_IMAGE_SOURCE_RE.test(source))
|
|
2184
|
+
continue;
|
|
2185
|
+
image.setAttribute(PREVIEW_IMAGE_PENDING_ATTRIBUTE, source);
|
|
2186
|
+
image.removeAttribute('src');
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
return template.content;
|
|
2190
|
+
}
|
|
2191
|
+
inlineSvgLinksEnabled() {
|
|
2192
|
+
return this.options.inlineSvgLinks !== false;
|
|
2193
|
+
}
|
|
2194
|
+
/**
|
|
2195
|
+
* Replaces `image` with a shadow-root host holding a copy of the cleaned
|
|
2196
|
+
* `svg` (see `svg-inline.ts`). The host carries the image's alt text,
|
|
2197
|
+
* size, alignment and source path, so the layout matches the `<img>`.
|
|
2198
|
+
*/
|
|
2199
|
+
mountInlineSvg(image, svg) {
|
|
2200
|
+
const doc = this.elPreviewContent.ownerDocument;
|
|
2201
|
+
const host = doc.createElement('span');
|
|
2202
|
+
host.className = 'mdzip-inline-svg';
|
|
2203
|
+
for (const name of [PREVIEW_IMAGE_SOURCE_ATTRIBUTE, 'title']) {
|
|
2204
|
+
const value = image.getAttribute(name);
|
|
2205
|
+
if (value !== null)
|
|
2206
|
+
host.setAttribute(name, value);
|
|
2207
|
+
}
|
|
2208
|
+
for (const className of Array.from(image.classList)) {
|
|
2209
|
+
if (className !== 'mdzip-image-loading')
|
|
2210
|
+
host.classList.add(className);
|
|
2211
|
+
}
|
|
2212
|
+
const alignClass = rawHtmlImageAlignClass(image);
|
|
2213
|
+
if (alignClass)
|
|
2214
|
+
host.classList.add(alignClass);
|
|
2215
|
+
const alt = image.getAttribute('alt');
|
|
2216
|
+
host.setAttribute('role', 'group');
|
|
2217
|
+
if (alt)
|
|
2218
|
+
host.setAttribute('aria-label', alt);
|
|
2219
|
+
const width = image.getAttribute('width');
|
|
2220
|
+
if (width && /^\d+(\.\d+)?$/.test(width))
|
|
2221
|
+
host.style.width = `${width}px`;
|
|
2222
|
+
else if (width)
|
|
2223
|
+
host.style.width = width;
|
|
2224
|
+
const shadow = host.attachShadow({ mode: 'open' });
|
|
2225
|
+
const style = doc.createElement('style');
|
|
2226
|
+
style.textContent = INLINE_SVG_SHADOW_CSS + (host.style.width ? 'svg{width:100%}' : '');
|
|
2227
|
+
shadow.append(style, svg.cloneNode(true));
|
|
2228
|
+
this.applyExternalLinkPolicy(shadow);
|
|
2229
|
+
image.replaceWith(host);
|
|
2230
|
+
return host;
|
|
2231
|
+
}
|
|
2232
|
+
/**
|
|
2233
|
+
* Replaces an image of a raw draw.io diagram (`.drawio`, `.dio`) with a card
|
|
2234
|
+
* saying what it is, why the preview can't draw it yet (#59) and how to show
|
|
2235
|
+
* it anyway, plus a button for each host file action that applies to it
|
|
2236
|
+
* (VS Code's "Open in VS Code Editor" opens it in Draw.io Integration). The
|
|
2237
|
+
* card keeps the image's source and alt text, so copying restores the image.
|
|
2238
|
+
*/
|
|
2239
|
+
mountDrawioPlaceholder(image, path) {
|
|
2240
|
+
const doc = this.elPreviewContent.ownerDocument;
|
|
2241
|
+
const el = (tag, className, text) => {
|
|
2242
|
+
const node = doc.createElement(tag);
|
|
2243
|
+
if (className)
|
|
2244
|
+
node.className = className;
|
|
2245
|
+
if (text !== undefined)
|
|
2246
|
+
node.textContent = text;
|
|
2247
|
+
return node;
|
|
2248
|
+
};
|
|
2249
|
+
const card = el('span', DRAWIO_PLACEHOLDER_CLASS);
|
|
2250
|
+
card.setAttribute('role', 'note');
|
|
2251
|
+
card.setAttribute(PREVIEW_IMAGE_SOURCE_ATTRIBUTE, image.getAttribute(PREVIEW_IMAGE_SOURCE_ATTRIBUTE) ?? path);
|
|
2252
|
+
const alt = image.getAttribute('alt')?.trim() ?? '';
|
|
2253
|
+
if (alt)
|
|
2254
|
+
card.setAttribute('data-mdzip-alt', alt);
|
|
2255
|
+
const fileName = path.split('/').pop() ?? path;
|
|
2256
|
+
const icon = el('span', `${DRAWIO_PLACEHOLDER_CLASS}-icon`);
|
|
2257
|
+
icon.setAttribute('aria-hidden', 'true');
|
|
2258
|
+
icon.innerHTML = '<svg viewBox="0 0 24 24" width="28" height="28" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round">'
|
|
2259
|
+
+ '<rect x="8.5" y="2.5" width="7" height="5" rx="1"/><rect x="2.5" y="16.5" width="7" height="5" rx="1"/>'
|
|
2260
|
+
+ '<rect x="14.5" y="16.5" width="7" height="5" rx="1"/><path d="M12 7.5v4.5M6 16.5V12h12v4.5"/></svg>';
|
|
2261
|
+
const body = el('span', `${DRAWIO_PLACEHOLDER_CLASS}-body`);
|
|
2262
|
+
body.append(el('strong', `${DRAWIO_PLACEHOLDER_CLASS}-title`, alt || fileName), el('span', `${DRAWIO_PLACEHOLDER_CLASS}-file`, `draw.io diagram · ${path}`));
|
|
2263
|
+
const why = el('span', `${DRAWIO_PLACEHOLDER_CLASS}-text`);
|
|
2264
|
+
why.append(doc.createTextNode('This preview can\'t draw '), el('code', '', '.drawio'), doc.createTextNode(' files yet. To show the diagram here, save it from draw.io as an editable SVG ('), el('code', '', '.drawio.svg'), doc.createTextNode(') and use that file for the image instead. You can still open this file in draw.io to see and edit it.'));
|
|
2265
|
+
body.append(why);
|
|
2266
|
+
const target = { path, isMarkdown: false, isImage: false, isManifest: false, isEntryPoint: false };
|
|
2267
|
+
const actions = (this.options.hostFileActions ?? []).filter((item) => item.appliesTo?.(target) ?? true);
|
|
2268
|
+
if (actions.length) {
|
|
2269
|
+
const row = el('span', `${DRAWIO_PLACEHOLDER_CLASS}-actions`);
|
|
2270
|
+
for (const action of actions) {
|
|
2271
|
+
const button = el('button', `${DRAWIO_PLACEHOLDER_CLASS}-action`, action.label);
|
|
2272
|
+
button.type = 'button';
|
|
2273
|
+
button.addEventListener('click', () => {
|
|
2274
|
+
void Promise.resolve()
|
|
2275
|
+
.then(() => action.run(target))
|
|
2276
|
+
.catch((error) => this.options.onFailed?.(error));
|
|
2277
|
+
});
|
|
2278
|
+
row.append(button);
|
|
2279
|
+
}
|
|
2280
|
+
body.append(row);
|
|
2281
|
+
// The same actions are on the file's right-click menu in the contents
|
|
2282
|
+
// pane, when this host shows one.
|
|
2283
|
+
if (this.controlPolicy.navigation) {
|
|
2284
|
+
const hint = el('span', `${DRAWIO_PLACEHOLDER_CLASS}-hint`);
|
|
2285
|
+
hint.append(doc.createTextNode('Or right-click '), el('code', '', fileName), doc.createTextNode(' in the contents pane.'));
|
|
2286
|
+
body.append(hint);
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
card.append(icon, body);
|
|
2290
|
+
image.replaceWith(card);
|
|
2291
|
+
return card;
|
|
2292
|
+
}
|
|
2127
2293
|
collectPendingImages(roots, context, generation, animateImageHydration) {
|
|
2128
2294
|
if (!this.assetSession) {
|
|
2129
2295
|
return [];
|
|
@@ -2132,9 +2298,12 @@ export class MdzipWorkspaceView {
|
|
|
2132
2298
|
for (const root of roots) {
|
|
2133
2299
|
for (const image of Array.from(root.querySelectorAll('img'))) {
|
|
2134
2300
|
applyRawHtmlImageSizeAttributes(image);
|
|
2135
|
-
const
|
|
2301
|
+
const held = image.getAttribute(PREVIEW_IMAGE_PENDING_ATTRIBUTE);
|
|
2302
|
+
if (held !== null)
|
|
2303
|
+
image.removeAttribute(PREVIEW_IMAGE_PENDING_ATTRIBUTE);
|
|
2304
|
+
const source = held ?? image.getAttribute('src');
|
|
2136
2305
|
// Leave external, protocol-relative, data, and fragment URLs untouched.
|
|
2137
|
-
if (!source ||
|
|
2306
|
+
if (!source || NON_ARCHIVE_IMAGE_SOURCE_RE.test(source)) {
|
|
2138
2307
|
const alignClass = rawHtmlImageAlignClass(image);
|
|
2139
2308
|
if (alignClass) {
|
|
2140
2309
|
image.classList.add(alignClass);
|
|
@@ -2143,7 +2312,19 @@ export class MdzipWorkspaceView {
|
|
|
2143
2312
|
}
|
|
2144
2313
|
// The src is replaced by a blob: URL; Copy as HTML restores it from here.
|
|
2145
2314
|
image.setAttribute(PREVIEW_IMAGE_SOURCE_ATTRIBUTE, source);
|
|
2146
|
-
const
|
|
2315
|
+
const drawioPath = this.assetSession.assetPath(source, context.currentPath);
|
|
2316
|
+
if (drawioPath && DRAWIO_PATH_RE.test(drawioPath)) {
|
|
2317
|
+
this.mountDrawioPlaceholder(image, drawioPath);
|
|
2318
|
+
continue;
|
|
2319
|
+
}
|
|
2320
|
+
const inlineSvg = this.inlineSvgLinksEnabled()
|
|
2321
|
+
? this.assetSession.knownInlineSvg(source, context.currentPath)
|
|
2322
|
+
: null;
|
|
2323
|
+
if (inlineSvg) {
|
|
2324
|
+
this.mountInlineSvg(image, inlineSvg);
|
|
2325
|
+
continue;
|
|
2326
|
+
}
|
|
2327
|
+
const known = inlineSvg === undefined ? undefined : this.assetSession.resolveKnownImage(source, context.currentPath);
|
|
2147
2328
|
if (known) {
|
|
2148
2329
|
if (known.width && known.height && !image.hasAttribute('width') && !image.hasAttribute('height')) {
|
|
2149
2330
|
image.setAttribute('width', String(known.width));
|
|
@@ -2213,6 +2394,26 @@ export class MdzipWorkspaceView {
|
|
|
2213
2394
|
return slot;
|
|
2214
2395
|
};
|
|
2215
2396
|
const hydrate = (image, slot, source) => {
|
|
2397
|
+
if (this.inlineSvgLinksEnabled() && session.knownInlineSvg(source, context.currentPath) !== null) {
|
|
2398
|
+
void session.resolveInlineSvg(source, context.currentPath).then((svg) => {
|
|
2399
|
+
if (generation !== this.previewGeneration || context.signal.aborted) {
|
|
2400
|
+
settle();
|
|
2401
|
+
return;
|
|
2402
|
+
}
|
|
2403
|
+
if (!svg) {
|
|
2404
|
+
hydrateImage(image, slot, source);
|
|
2405
|
+
return;
|
|
2406
|
+
}
|
|
2407
|
+
image.classList.remove('mdzip-image-loading');
|
|
2408
|
+
this.mountInlineSvg(image, svg);
|
|
2409
|
+
this.openImageSlot(slot);
|
|
2410
|
+
settle();
|
|
2411
|
+
});
|
|
2412
|
+
return;
|
|
2413
|
+
}
|
|
2414
|
+
hydrateImage(image, slot, source);
|
|
2415
|
+
};
|
|
2416
|
+
const hydrateImage = (image, slot, source) => {
|
|
2216
2417
|
void session.resolveImage(source, context.currentPath).then((resolved) => {
|
|
2217
2418
|
if (generation !== this.previewGeneration || context.signal.aborted) {
|
|
2218
2419
|
settle();
|
|
@@ -2759,7 +2960,7 @@ export class MdzipWorkspaceView {
|
|
|
2759
2960
|
const doc = this.elPreviewContent.ownerDocument;
|
|
2760
2961
|
const wrapper = doc.createElement('div');
|
|
2761
2962
|
wrapper.className = 'mdzip-chunk';
|
|
2762
|
-
wrapper.
|
|
2963
|
+
wrapper.appendChild(this.inertPreviewFragment(html));
|
|
2763
2964
|
if (beforeNode && beforeNode.parentNode === this.elPreviewContent) {
|
|
2764
2965
|
this.elPreviewContent.insertBefore(wrapper, beforeNode);
|
|
2765
2966
|
}
|
|
@@ -4359,6 +4560,15 @@ export class MdzipWorkspaceView {
|
|
|
4359
4560
|
void this.deleteFile(path);
|
|
4360
4561
|
}
|
|
4361
4562
|
});
|
|
4563
|
+
this.elAddFilesInput.addEventListener('change', () => {
|
|
4564
|
+
const files = Array.from(this.elAddFilesInput.files ?? []);
|
|
4565
|
+
const dir = this.pendingAddFilesDir;
|
|
4566
|
+
this.elAddFilesInput.value = '';
|
|
4567
|
+
this.pendingAddFilesDir = null;
|
|
4568
|
+
if (files.length > 0 && dir !== null) {
|
|
4569
|
+
void this.addDroppedFiles(dir, files);
|
|
4570
|
+
}
|
|
4571
|
+
});
|
|
4362
4572
|
this.elReplaceInput.addEventListener('change', () => {
|
|
4363
4573
|
const file = this.elReplaceInput.files?.[0];
|
|
4364
4574
|
const path = this.pendingReplacePath;
|
|
@@ -4384,7 +4594,9 @@ export class MdzipWorkspaceView {
|
|
|
4384
4594
|
this.elPreviewPane.focus({ preventScroll: true });
|
|
4385
4595
|
});
|
|
4386
4596
|
this.elPreviewPane.addEventListener('click', (event) => {
|
|
4387
|
-
|
|
4597
|
+
// The event's path, not its target: clicks inside an inline SVG's shadow root are
|
|
4598
|
+
// retargeted to the host, and SVG links are SVGAElements, not HTMLAnchorElements.
|
|
4599
|
+
const link = event.composedPath().find((node) => node.nodeType === 1 && node.localName === 'a' && node.hasAttribute('href')) ?? null;
|
|
4388
4600
|
const snapshot = this.workspace?.snapshot();
|
|
4389
4601
|
if (!link || !snapshot) {
|
|
4390
4602
|
return;
|
|
@@ -5802,6 +6014,13 @@ export class MdzipWorkspaceView {
|
|
|
5802
6014
|
}
|
|
5803
6015
|
const container = doc.createElement('div');
|
|
5804
6016
|
container.appendChild(content);
|
|
6017
|
+
// A draw.io placeholder copies as the image it stands for.
|
|
6018
|
+
for (const card of Array.from(container.querySelectorAll(`.${DRAWIO_PLACEHOLDER_CLASS}`))) {
|
|
6019
|
+
const image = doc.createElement('img');
|
|
6020
|
+
image.setAttribute(PREVIEW_IMAGE_SOURCE_ATTRIBUTE, card.getAttribute(PREVIEW_IMAGE_SOURCE_ATTRIBUTE) ?? '');
|
|
6021
|
+
image.setAttribute('alt', card.getAttribute('data-mdzip-alt') ?? '');
|
|
6022
|
+
card.replaceWith(image);
|
|
6023
|
+
}
|
|
5805
6024
|
const text = previewPlainText(container);
|
|
5806
6025
|
for (const element of Array.from(container.querySelectorAll(PREVIEW_CHROME_REMOVE_SELECTOR))) {
|
|
5807
6026
|
element.remove();
|
|
@@ -6966,11 +7185,14 @@ export class MdzipWorkspaceView {
|
|
|
6966
7185
|
}
|
|
6967
7186
|
return [
|
|
6968
7187
|
{ action: 'new-file', label: 'New .md File' },
|
|
6969
|
-
{ action: 'new-folder', label: 'New Folder' }
|
|
7188
|
+
{ action: 'new-folder', label: 'New Folder' },
|
|
7189
|
+
null,
|
|
7190
|
+
{ action: 'add-files', label: 'Add Files…' }
|
|
6970
7191
|
];
|
|
6971
7192
|
}
|
|
7193
|
+
const hostItems = this.fileActionItems(target);
|
|
6972
7194
|
if (target.isManifest) {
|
|
6973
|
-
return [{ action: 'download', label: 'Download' }];
|
|
7195
|
+
return [...hostItems, ...(hostItems.length ? [null] : []), { action: 'download', label: 'Download' }];
|
|
6974
7196
|
}
|
|
6975
7197
|
const groups = [];
|
|
6976
7198
|
if (canMutate) {
|
|
@@ -7002,6 +7224,9 @@ export class MdzipWorkspaceView {
|
|
|
7002
7224
|
}
|
|
7003
7225
|
groups.push(editGroup);
|
|
7004
7226
|
}
|
|
7227
|
+
if (hostItems.length > 0) {
|
|
7228
|
+
groups.push(hostItems);
|
|
7229
|
+
}
|
|
7005
7230
|
groups.push([{ action: 'download', label: 'Download' }]);
|
|
7006
7231
|
if (canMutate && !target.isEntryPoint) {
|
|
7007
7232
|
groups.push([{
|
|
@@ -7011,6 +7236,13 @@ export class MdzipWorkspaceView {
|
|
|
7011
7236
|
}
|
|
7012
7237
|
return groups.flatMap((group, index) => index === 0 ? group : [null, ...group]);
|
|
7013
7238
|
}
|
|
7239
|
+
/** Menu items for the host's {@link MdzipFileAction}s that apply to this file. */
|
|
7240
|
+
fileActionItems(target) {
|
|
7241
|
+
const file = fileActionTarget(target);
|
|
7242
|
+
return (this.options.hostFileActions ?? [])
|
|
7243
|
+
.filter((item) => item.appliesTo?.(file) ?? true)
|
|
7244
|
+
.map((item) => ({ action: `${HOST_FILE_ACTION_PREFIX}${item.id}`, label: item.label }));
|
|
7245
|
+
}
|
|
7014
7246
|
async handleNavMenuAction(action) {
|
|
7015
7247
|
const state = this.contextMenuState;
|
|
7016
7248
|
this.contextMenuState = null;
|
|
@@ -7031,6 +7263,12 @@ export class MdzipWorkspaceView {
|
|
|
7031
7263
|
this.openNameDialog({ mode: 'new-folder', dir: target.path, value: 'new-folder' });
|
|
7032
7264
|
}
|
|
7033
7265
|
break;
|
|
7266
|
+
case 'add-files':
|
|
7267
|
+
if (target.kind === 'directory') {
|
|
7268
|
+
this.pendingAddFilesDir = target.path;
|
|
7269
|
+
this.elAddFilesInput.click();
|
|
7270
|
+
}
|
|
7271
|
+
break;
|
|
7034
7272
|
case 'rename':
|
|
7035
7273
|
if (target.kind === 'file') {
|
|
7036
7274
|
this.openNameDialog({ mode: 'rename', oldPath: target.path, value: target.path });
|
|
@@ -7085,6 +7323,11 @@ export class MdzipWorkspaceView {
|
|
|
7085
7323
|
}
|
|
7086
7324
|
break;
|
|
7087
7325
|
default:
|
|
7326
|
+
if (action.startsWith(HOST_FILE_ACTION_PREFIX) && target.kind === 'file') {
|
|
7327
|
+
const id = action.slice(HOST_FILE_ACTION_PREFIX.length);
|
|
7328
|
+
const item = (this.options.hostFileActions ?? []).find((candidate) => candidate.id === id);
|
|
7329
|
+
await item?.run(fileActionTarget(target));
|
|
7330
|
+
}
|
|
7088
7331
|
break;
|
|
7089
7332
|
}
|
|
7090
7333
|
}
|
|
@@ -7337,7 +7580,8 @@ export class MdzipWorkspaceView {
|
|
|
7337
7580
|
}
|
|
7338
7581
|
const internal = this.dragSourcePath !== null
|
|
7339
7582
|
|| (e.dataTransfer?.types.includes('application/x-mdzip-path') ?? false);
|
|
7340
|
-
const external = e.dataTransfer?.types.includes('Files') ?? false
|
|
7583
|
+
const external = (e.dataTransfer?.types.includes('Files') ?? false)
|
|
7584
|
+
|| (this.options.resolveDroppedUris !== undefined && droppedUriListType(e.dataTransfer) !== null);
|
|
7341
7585
|
if (!internal && !external) {
|
|
7342
7586
|
return;
|
|
7343
7587
|
}
|
|
@@ -7372,6 +7616,15 @@ export class MdzipWorkspaceView {
|
|
|
7372
7616
|
if (files && files.length > 0) {
|
|
7373
7617
|
e.preventDefault();
|
|
7374
7618
|
void this.addDroppedFiles(targetDir, Array.from(files));
|
|
7619
|
+
return;
|
|
7620
|
+
}
|
|
7621
|
+
const uriListType = droppedUriListType(e.dataTransfer);
|
|
7622
|
+
const resolveUris = this.options.resolveDroppedUris;
|
|
7623
|
+
if (uriListType && resolveUris) {
|
|
7624
|
+
e.preventDefault();
|
|
7625
|
+
const uris = parseUriList(e.dataTransfer?.getData(uriListType) ?? '');
|
|
7626
|
+
if (uris.length > 0)
|
|
7627
|
+
void this.addDroppedUris(targetDir, uris, resolveUris);
|
|
7375
7628
|
}
|
|
7376
7629
|
});
|
|
7377
7630
|
}
|
|
@@ -7397,6 +7650,19 @@ export class MdzipWorkspaceView {
|
|
|
7397
7650
|
this.options.onFailed?.(error);
|
|
7398
7651
|
}
|
|
7399
7652
|
}
|
|
7653
|
+
async addDroppedUris(targetDir, uris, resolveUris) {
|
|
7654
|
+
try {
|
|
7655
|
+
const resolved = await resolveUris(uris);
|
|
7656
|
+
// `File` here is an imported icon; take the browser's class from the page's window.
|
|
7657
|
+
const FileClass = this.elPreviewContent.ownerDocument.defaultView?.File ?? globalThis.File;
|
|
7658
|
+
const files = resolved.map(({ name, bytes }) => new FileClass([bytes], name));
|
|
7659
|
+
if (files.length > 0)
|
|
7660
|
+
await this.addDroppedFiles(targetDir, files);
|
|
7661
|
+
}
|
|
7662
|
+
catch (error) {
|
|
7663
|
+
this.options.onFailed?.(error);
|
|
7664
|
+
}
|
|
7665
|
+
}
|
|
7400
7666
|
async addDroppedFiles(targetDir, files) {
|
|
7401
7667
|
const workspace = this.workspace;
|
|
7402
7668
|
if (!workspace) {
|
|
@@ -8098,6 +8364,7 @@ const SHELL_HTML = `
|
|
|
8098
8364
|
</div>
|
|
8099
8365
|
|
|
8100
8366
|
<input type="file" data-ref="replace-input" hidden />
|
|
8367
|
+
<input type="file" data-ref="add-files-input" multiple hidden />
|
|
8101
8368
|
|
|
8102
8369
|
<div class="nav-context-menu" data-ref="nav-menu" hidden role="menu"></div>
|
|
8103
8370
|
|