vitepress-plugin-filelist 0.6.2 → 0.7.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 +25 -2
- package/dist/client/browser/index.d.ts +25 -1
- package/dist/client/browser/index.js +425 -112
- package/dist/client/ssr/index.d.ts +25 -1
- package/dist/client/ssr/index.js +244 -14
- package/dist/client/style.css +95 -1
- package/package.json +8 -2
|
@@ -92,6 +92,24 @@ interface FileListLabels {
|
|
|
92
92
|
unlockFolder: string;
|
|
93
93
|
/** @deprecated Network failures now append `corsHint`. */
|
|
94
94
|
networkHint: string;
|
|
95
|
+
/** Deep search: the row at the end of a filtered list. `{count}` where noted */
|
|
96
|
+
deepHint: string;
|
|
97
|
+
deepSearch: string;
|
|
98
|
+
deepRunning: string;
|
|
99
|
+
deepStop: string;
|
|
100
|
+
deepDone: string;
|
|
101
|
+
deepCapped: string;
|
|
102
|
+
deepContinue: string;
|
|
103
|
+
deepSkipped: string;
|
|
104
|
+
deepFailed: string;
|
|
105
|
+
/** Markdown / HTML preview: the source / rendered switch. */
|
|
106
|
+
viewMode: string;
|
|
107
|
+
viewSource: string;
|
|
108
|
+
viewRendered: string;
|
|
109
|
+
/** Above a rendered HTML file. */
|
|
110
|
+
htmlSandboxNote: string;
|
|
111
|
+
/** Stands in for a Markdown image with a relative path. `{alt}` */
|
|
112
|
+
imageOmitted: string;
|
|
95
113
|
/** Title of the dialog when a batch ZIP fails (the reason is the text). */
|
|
96
114
|
batchFailedTitle: string;
|
|
97
115
|
batchFailedHint: string;
|
|
@@ -156,6 +174,11 @@ interface FileListProps {
|
|
|
156
174
|
* everything. Data is still loaded in full; only rendering is limited.
|
|
157
175
|
*/
|
|
158
176
|
pageSize?: number;
|
|
177
|
+
/**
|
|
178
|
+
* Deep search loads at most this many folders per click (“Continue” goes on).
|
|
179
|
+
* Defaults to `500`.
|
|
180
|
+
*/
|
|
181
|
+
deepSearchLimit?: number;
|
|
159
182
|
/**
|
|
160
183
|
* Browsers that must build the ZIP in memory (Firefox, Safari) ask first above
|
|
161
184
|
* this many MB. Defaults to 500 (200 on touch devices); `0` never asks.
|
|
@@ -192,6 +215,7 @@ declare const _default: import("vue").DefineComponent<FileListProps, {}, {}, {},
|
|
|
192
215
|
title: string;
|
|
193
216
|
rememberPassword: boolean;
|
|
194
217
|
pageSize: number;
|
|
218
|
+
deepSearchLimit: number;
|
|
195
219
|
locale: FileListLocale;
|
|
196
220
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
197
221
|
declare global {
|
|
@@ -310,7 +334,7 @@ export declare function previewKind(name: string): PreviewKind;
|
|
|
310
334
|
export declare function totalSize(items: Array<{
|
|
311
335
|
size?: number;
|
|
312
336
|
}>): number;
|
|
313
|
-
export declare function compareNodes(a: SortableNode, b: SortableNode, key: SortKey, ascending?: boolean): number;
|
|
337
|
+
export declare function compareNodes(a: SortableNode, b: SortableNode, key: SortKey, ascending?: boolean, locale?: string): number;
|
|
314
338
|
interface IframePreview {
|
|
315
339
|
label: string;
|
|
316
340
|
scheme: string;
|
package/dist/client/ssr/index.js
CHANGED
|
@@ -179,12 +179,29 @@ function previewKind(name) {
|
|
|
179
179
|
function totalSize(items) {
|
|
180
180
|
return items.reduce((sum, item) => sum + (Number.isFinite(item.size) && (item.size ?? 0) > 0 ? item.size : 0), 0);
|
|
181
181
|
}
|
|
182
|
-
|
|
182
|
+
const collators = /* @__PURE__ */ new Map();
|
|
183
|
+
/**
|
|
184
|
+
* Name order for a UI locale: pinyin for Chinese, alphabetical for English,
|
|
185
|
+
* with numbers in natural order (“2” before “10”) either way.
|
|
186
|
+
*/
|
|
187
|
+
function collatorFor(locale) {
|
|
188
|
+
const tag = locale.toLowerCase().startsWith("zh") ? "zh-Hans-CN" : "en";
|
|
189
|
+
let collator = collators.get(tag);
|
|
190
|
+
if (!collator) {
|
|
191
|
+
collator = new Intl.Collator(tag, {
|
|
192
|
+
numeric: true,
|
|
193
|
+
sensitivity: "base"
|
|
194
|
+
});
|
|
195
|
+
collators.set(tag, collator);
|
|
196
|
+
}
|
|
197
|
+
return collator;
|
|
198
|
+
}
|
|
199
|
+
function compareNodes(a, b, key, ascending = true, locale = "zh-CN") {
|
|
183
200
|
if (a.is_dir !== b.is_dir) return a.is_dir ? -1 : 1;
|
|
184
201
|
let result = 0;
|
|
185
202
|
if (key === "size") result = a.size - b.size;
|
|
186
203
|
else if (key === "modified") result = new Date(a.modified).getTime() - new Date(b.modified).getTime();
|
|
187
|
-
else result = a.name.
|
|
204
|
+
else result = collatorFor(locale).compare(a.name, b.name) || (a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
188
205
|
return ascending ? result : -result;
|
|
189
206
|
}
|
|
190
207
|
/**
|
|
@@ -852,6 +869,20 @@ const fileListLocales = {
|
|
|
852
869
|
forgetPassword: "忘记",
|
|
853
870
|
unlockFolder: "输入 {name} 的密码",
|
|
854
871
|
networkHint: "可能被跨域拦截:请检查 OpenList 的 cors.allow_origins,或 OpenList-Worker 的 ALLOW_URLS。",
|
|
872
|
+
deepHint: "只筛选了已加载的内容",
|
|
873
|
+
deepSearch: "深度搜索",
|
|
874
|
+
deepRunning: "正在深度搜索 · 已查找 {count} 个文件夹",
|
|
875
|
+
deepStop: "停止",
|
|
876
|
+
deepDone: "已搜索全部 {count} 个文件夹",
|
|
877
|
+
deepCapped: "已查找 {count} 个文件夹,还有未搜索的子目录",
|
|
878
|
+
deepContinue: "继续",
|
|
879
|
+
deepSkipped: "跳过 {count} 个加密文件夹",
|
|
880
|
+
deepFailed: "{count} 个文件夹读取失败",
|
|
881
|
+
viewMode: "显示方式",
|
|
882
|
+
viewSource: "源码",
|
|
883
|
+
viewRendered: "渲染",
|
|
884
|
+
htmlSandboxNote: "已禁用脚本;网页引用的相对路径资源不会加载",
|
|
885
|
+
imageOmitted: "[图片:{alt}]",
|
|
855
886
|
batchFailedTitle: "打包失败",
|
|
856
887
|
batchFailedHint: "可以减少勾选的文件后重试。",
|
|
857
888
|
downloadStarted: "已交给浏览器下载:{name}",
|
|
@@ -932,6 +963,20 @@ const fileListLocales = {
|
|
|
932
963
|
forgetPassword: "Forget",
|
|
933
964
|
unlockFolder: "Enter the password for {name}",
|
|
934
965
|
networkHint: "Possibly blocked by CORS: check cors.allow_origins in OpenList, or ALLOW_URLS in OpenList-Worker.",
|
|
966
|
+
deepHint: "Only loaded folders are filtered",
|
|
967
|
+
deepSearch: "Deep search",
|
|
968
|
+
deepRunning: "Searching · {count} folders checked",
|
|
969
|
+
deepStop: "Stop",
|
|
970
|
+
deepDone: "Searched all {count} folders",
|
|
971
|
+
deepCapped: "{count} folders checked; some sub-folders remain",
|
|
972
|
+
deepContinue: "Continue",
|
|
973
|
+
deepSkipped: "{count} locked folders skipped",
|
|
974
|
+
deepFailed: "{count} folders could not be read",
|
|
975
|
+
viewMode: "View",
|
|
976
|
+
viewSource: "Source",
|
|
977
|
+
viewRendered: "Rendered",
|
|
978
|
+
htmlSandboxNote: "Scripts are disabled; resources at relative paths do not load",
|
|
979
|
+
imageOmitted: "[Image: {alt}]",
|
|
935
980
|
batchFailedTitle: "Could not create the ZIP",
|
|
936
981
|
batchFailedHint: "Try again with fewer files selected.",
|
|
937
982
|
downloadStarted: "Downloading {name} in your browser",
|
|
@@ -960,6 +1005,45 @@ function resolveLabels(locale = "zh-CN", overrides) {
|
|
|
960
1005
|
...overrides
|
|
961
1006
|
};
|
|
962
1007
|
}
|
|
1008
|
+
// src/client/markdown.ts
|
|
1009
|
+
/**
|
|
1010
|
+
* Markdown → safe HTML for the preview dialog. Loaded on demand, so pages that
|
|
1011
|
+
* never render Markdown do not download markdown-it or DOMPurify.
|
|
1012
|
+
*
|
|
1013
|
+
* - Raw HTML in the file is allowed, then sanitised (no scripts, handlers, iframes...)
|
|
1014
|
+
* - Only absolute http(s) / mailto links stay clickable, opening in a new tab;
|
|
1015
|
+
* relative links (to other files in the share) become plain text
|
|
1016
|
+
* - Images with a relative source are replaced by their alt text, because the
|
|
1017
|
+
* share's relative paths do not resolve on the documentation site
|
|
1018
|
+
*/
|
|
1019
|
+
async function renderMarkdown(source, imageLabel) {
|
|
1020
|
+
const [{ default: MarkdownIt }, { default: DOMPurify }] = await Promise.all([import("markdown-it"), import("dompurify")]);
|
|
1021
|
+
const probe = DOMPurify.isSupported ? DOMPurify.sanitize("<img src=\"x\" onerror=\"1\"><script>1<\/script>") : "<script";
|
|
1022
|
+
if (/<script|onerror/i.test(probe)) throw new Error("HTML sanitiser unavailable");
|
|
1023
|
+
const html = new MarkdownIt({
|
|
1024
|
+
html: true,
|
|
1025
|
+
linkify: true
|
|
1026
|
+
}).render(source);
|
|
1027
|
+
const template = document.createElement("template");
|
|
1028
|
+
template.innerHTML = DOMPurify.sanitize(html);
|
|
1029
|
+
const fragment = template.content;
|
|
1030
|
+
for (const link of fragment.querySelectorAll("a")) {
|
|
1031
|
+
const href = link.getAttribute("href") ?? "";
|
|
1032
|
+
if (/^(https?:|mailto:)/i.test(href)) {
|
|
1033
|
+
link.setAttribute("target", "_blank");
|
|
1034
|
+
link.setAttribute("rel", "noopener noreferrer");
|
|
1035
|
+
} else link.replaceWith(...link.childNodes);
|
|
1036
|
+
}
|
|
1037
|
+
for (const image of fragment.querySelectorAll("img")) {
|
|
1038
|
+
const src = image.getAttribute("src") ?? "";
|
|
1039
|
+
if (/^(https?:|data:image\/)/i.test(src)) continue;
|
|
1040
|
+
const placeholder = document.createElement("span");
|
|
1041
|
+
placeholder.className = "fl-md-image";
|
|
1042
|
+
placeholder.textContent = imageLabel(image.getAttribute("alt") || src.split("/").pop() || "");
|
|
1043
|
+
image.replaceWith(placeholder);
|
|
1044
|
+
}
|
|
1045
|
+
return template.innerHTML;
|
|
1046
|
+
}
|
|
963
1047
|
// src/client/PreviewDialog.vue
|
|
964
1048
|
const _sfc_main$1 = /*@__PURE__*/ defineComponent({
|
|
965
1049
|
__name: "PreviewDialog",
|
|
@@ -994,7 +1078,9 @@ const _sfc_main$1 = /*@__PURE__*/ defineComponent({
|
|
|
994
1078
|
/**
|
|
995
1079
|
* File preview in a native <dialog>, without leaving the page.
|
|
996
1080
|
*
|
|
997
|
-
* image / video / audio: native elements; text: the first `maxTextBytes
|
|
1081
|
+
* image / video / audio: native elements; text: the first `maxTextBytes`,
|
|
1082
|
+
* with Markdown and HTML switchable between source (default) and rendered
|
|
1083
|
+
* (Markdown sanitised in VitePress' doc style, HTML in a script-less sandbox);
|
|
998
1084
|
* through a Range request; pdf: the first matching OpenList `iframe_previews`
|
|
999
1085
|
* viewer, else the browser's reader through a Blob. Anything else (Office
|
|
1000
1086
|
* included), failures, oversized PDFs and demo mode: file info + download.
|
|
@@ -1012,8 +1098,18 @@ const _sfc_main$1 = /*@__PURE__*/ defineComponent({
|
|
|
1012
1098
|
const blobUrl = ref("");
|
|
1013
1099
|
const rotation = ref(0);
|
|
1014
1100
|
const actualSize = ref(false);
|
|
1101
|
+
/** Markdown / HTML: show the source or render it. Kept while the page is open. */
|
|
1102
|
+
const view = ref("source");
|
|
1103
|
+
const renderedHtml = ref("");
|
|
1015
1104
|
let controller;
|
|
1016
1105
|
let loadSeq = 0;
|
|
1106
|
+
/** Text files that can also be rendered. */
|
|
1107
|
+
const renderAs = computed(() => {
|
|
1108
|
+
const name = props.file?.name ?? "";
|
|
1109
|
+
if (/\.(md|markdown)$/i.test(name)) return "markdown";
|
|
1110
|
+
if (/\.html?$/i.test(name)) return "html";
|
|
1111
|
+
return null;
|
|
1112
|
+
});
|
|
1017
1113
|
const tooLarge = computed(() => kind.value === "pdf" && !props.viewer && (props.file?.size ?? 0) > props.maxPdfBytes);
|
|
1018
1114
|
const mode = computed(() => {
|
|
1019
1115
|
if (props.demo) return "demo";
|
|
@@ -1023,6 +1119,7 @@ const _sfc_main$1 = /*@__PURE__*/ defineComponent({
|
|
|
1023
1119
|
return kind.value;
|
|
1024
1120
|
});
|
|
1025
1121
|
const busy = computed(() => !props.demo && kind.value !== "none" && !props.error && !bodyFailed.value && (props.resolving || loadingBody.value || !props.src && !tooLarge.value));
|
|
1122
|
+
const canRender = computed(() => !!renderAs.value && mode.value === "text" && !busy.value);
|
|
1026
1123
|
const infoMessage = computed(() => {
|
|
1027
1124
|
const file = props.file;
|
|
1028
1125
|
if (!file) return "";
|
|
@@ -1043,6 +1140,7 @@ const _sfc_main$1 = /*@__PURE__*/ defineComponent({
|
|
|
1043
1140
|
loadingBody.value = false;
|
|
1044
1141
|
bodyFailed.value = false;
|
|
1045
1142
|
textContent.value = "";
|
|
1143
|
+
renderedHtml.value = "";
|
|
1046
1144
|
truncated.value = false;
|
|
1047
1145
|
rotation.value = 0;
|
|
1048
1146
|
actualSize.value = false;
|
|
@@ -1124,6 +1222,20 @@ const _sfc_main$1 = /*@__PURE__*/ defineComponent({
|
|
|
1124
1222
|
props.error,
|
|
1125
1223
|
props.demo
|
|
1126
1224
|
], () => void loadBody(), { immediate: true });
|
|
1225
|
+
watch(() => [
|
|
1226
|
+
view.value,
|
|
1227
|
+
renderAs.value,
|
|
1228
|
+
textContent.value
|
|
1229
|
+
], async ([current, as, text]) => {
|
|
1230
|
+
if (current !== "rendered" || as !== "markdown" || !text) return;
|
|
1231
|
+
const seq = loadSeq;
|
|
1232
|
+
try {
|
|
1233
|
+
const html = await renderMarkdown(text, (alt) => t("imageOmitted", { alt }));
|
|
1234
|
+
if (seq === loadSeq) renderedHtml.value = html;
|
|
1235
|
+
} catch {
|
|
1236
|
+
if (seq === loadSeq) view.value = "source";
|
|
1237
|
+
}
|
|
1238
|
+
});
|
|
1127
1239
|
watch(() => props.open, (open) => {
|
|
1128
1240
|
const el = dialog.value;
|
|
1129
1241
|
if (!el) return;
|
|
@@ -1145,7 +1257,7 @@ const _sfc_main$1 = /*@__PURE__*/ defineComponent({
|
|
|
1145
1257
|
}, _attrs))}>`);
|
|
1146
1258
|
if (__props.open && __props.file) {
|
|
1147
1259
|
_push(`<div class="fl-preview-panel"><header class="fl-preview-head"><span class="fl-type"${ssrRenderAttr("data-kind", unref(fileKind)(__props.file.name))} aria-hidden="true">${ssrInterpolate(unref(extOf)(__props.file.name) || "FILE")}</span><div class="fl-preview-title"><span class="fl-preview-name">${ssrInterpolate(__props.file.name)}</span><span class="fl-preview-meta">${ssrInterpolate(unref(formatSize)(__props.file.size))} · ${ssrInterpolate(unref(formatDate)(__props.file.modified))}</span></div>`);
|
|
1148
|
-
if (__props.total > 1 || mode.value === "image" && !busy.value) {
|
|
1260
|
+
if (__props.total > 1 || mode.value === "image" && !busy.value || canRender.value) {
|
|
1149
1261
|
_push(`<div class="fl-preview-tools fl-preview-nav">`);
|
|
1150
1262
|
if (__props.total > 1) {
|
|
1151
1263
|
_push(`<!--[--><button class="fl-preview-tool" type="button"${ssrRenderAttr("aria-label", t("previous"))}${ssrRenderAttr("title", t("previous"))}>`);
|
|
@@ -1173,6 +1285,13 @@ const _sfc_main$1 = /*@__PURE__*/ defineComponent({
|
|
|
1173
1285
|
}, null, _parent));
|
|
1174
1286
|
_push(`</button><!--]-->`);
|
|
1175
1287
|
} else _push(`<!---->`);
|
|
1288
|
+
if (canRender.value) {
|
|
1289
|
+
_push(`<span class="fl-preview-view" role="group"${ssrRenderAttr("aria-label", t("viewMode"))}><!--[-->`);
|
|
1290
|
+
ssrRenderList(["source", "rendered"], (option) => {
|
|
1291
|
+
_push(`<button class="fl-preview-view-btn" type="button"${ssrRenderAttr("aria-pressed", view.value === option)}>${ssrInterpolate(t(option === "source" ? "viewSource" : "viewRendered"))}</button>`);
|
|
1292
|
+
});
|
|
1293
|
+
_push(`<!--]--></span>`);
|
|
1294
|
+
} else _push(`<!---->`);
|
|
1176
1295
|
_push(`</div>`);
|
|
1177
1296
|
} else _push(`<!---->`);
|
|
1178
1297
|
_push(`<div class="fl-preview-tools fl-preview-actions">`);
|
|
@@ -1195,10 +1314,17 @@ const _sfc_main$1 = /*@__PURE__*/ defineComponent({
|
|
|
1195
1314
|
else if (mode.value === "video") _push(`<video class="fl-preview-video"${ssrRenderAttr("src", __props.src)} controls playsinline preload="metadata"></video>`);
|
|
1196
1315
|
else if (mode.value === "audio") _push(`<audio class="fl-preview-audio"${ssrRenderAttr("src", __props.src)} controls preload="metadata"></audio>`);
|
|
1197
1316
|
else if (mode.value === "text") {
|
|
1198
|
-
_push(`<div class="fl-preview-text">`);
|
|
1317
|
+
_push(`<div class="${ssrRenderClass([{ "fl-preview-rendered": renderAs.value && view.value === "rendered" }, "fl-preview-text"])}">`);
|
|
1199
1318
|
if (truncated.value) _push(`<p class="fl-preview-note">${ssrInterpolate(t("textTruncated", { size: unref(formatSize)(__props.maxTextBytes) }))}</p>`);
|
|
1200
1319
|
else _push(`<!---->`);
|
|
1201
|
-
|
|
1320
|
+
if (renderAs.value === "markdown" && view.value === "rendered") {
|
|
1321
|
+
_push(`<!--[--><!-- sanitised by renderMarkdown (DOMPurify) -->`);
|
|
1322
|
+
if (renderedHtml.value) _push(`<div class="vp-doc fl-md">${renderedHtml.value ?? ""}</div>`);
|
|
1323
|
+
else _push(`<div class="fl-preview-status" role="status">${ssrInterpolate(t("loading"))}</div>`);
|
|
1324
|
+
_push(`<!--]-->`);
|
|
1325
|
+
} else if (renderAs.value === "html" && view.value === "rendered") _push(`<!--[--><p class="fl-preview-note">${ssrInterpolate(t("htmlSandboxNote"))}</p><!-- empty sandbox: no scripts, no forms, no popups, an opaque origin --><iframe class="fl-preview-html" sandbox="" referrerpolicy="no-referrer"${ssrRenderAttr("srcdoc", textContent.value)}${ssrRenderAttr("title", __props.file.name)}></iframe><!--]-->`);
|
|
1326
|
+
else _push(`<pre>${ssrInterpolate(textContent.value)}</pre>`);
|
|
1327
|
+
_push(`</div>`);
|
|
1202
1328
|
} else if (mode.value === "pdf") _push(`<iframe class="fl-preview-frame"${ssrRenderAttr("src", __props.viewer || blobUrl.value)}${ssrRenderAttr("title", __props.file.name)}></iframe>`);
|
|
1203
1329
|
else {
|
|
1204
1330
|
_push(`<div class="fl-preview-info"><span class="fl-type fl-preview-badge"${ssrRenderAttr("data-kind", unref(fileKind)(__props.file.name))} aria-hidden="true">${ssrInterpolate(unref(extOf)(__props.file.name) || "FILE")}</span><p class="fl-preview-message">${ssrInterpolate(infoMessage.value)}</p>`);
|
|
@@ -1247,9 +1373,9 @@ function toNodes(items, parent, password) {
|
|
|
1247
1373
|
zipPath: [...parent.zipPath, item.name]
|
|
1248
1374
|
}));
|
|
1249
1375
|
}
|
|
1250
|
-
function sortTree(nodes, key, ascending) {
|
|
1251
|
-
nodes.sort((a, b) => compareNodes(a, b, key, ascending));
|
|
1252
|
-
for (const node of nodes) if (node.children.length) sortTree(node.children, key, ascending);
|
|
1376
|
+
function sortTree(nodes, key, ascending, locale) {
|
|
1377
|
+
nodes.sort((a, b) => compareNodes(a, b, key, ascending, locale));
|
|
1378
|
+
for (const node of nodes) if (node.children.length) sortTree(node.children, key, ascending, locale);
|
|
1253
1379
|
}
|
|
1254
1380
|
/** Loaded files at or under a node. */
|
|
1255
1381
|
function descendantFiles(node) {
|
|
@@ -1436,6 +1562,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
1436
1562
|
default: true
|
|
1437
1563
|
},
|
|
1438
1564
|
pageSize: { default: 200 },
|
|
1565
|
+
deepSearchLimit: { default: 500 },
|
|
1439
1566
|
zipWarnSize: {},
|
|
1440
1567
|
zipMaxSize: {},
|
|
1441
1568
|
locale: { default: "zh-CN" },
|
|
@@ -1500,7 +1627,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
1500
1627
|
return isNetworkError(error) ? `${message} ${t("corsHint")}` : message;
|
|
1501
1628
|
}
|
|
1502
1629
|
function sortNodes(nodes) {
|
|
1503
|
-
sortTree(nodes, sortKey.value, sortAsc.value);
|
|
1630
|
+
sortTree(nodes, sortKey.value, sortAsc.value, props.locale);
|
|
1504
1631
|
}
|
|
1505
1632
|
function focusUnlock(path) {
|
|
1506
1633
|
nextTick(() => {
|
|
@@ -1532,7 +1659,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
1532
1659
|
* Load a folder's children. A password error locks the folder instead of
|
|
1533
1660
|
* toasting; `prompt` also shows its same-row input (and focuses it).
|
|
1534
1661
|
*/
|
|
1535
|
-
async function loadChildren(node, prompt = false) {
|
|
1662
|
+
async function loadChildren(node, prompt = false, quiet = false) {
|
|
1536
1663
|
if (node.loaded) return true;
|
|
1537
1664
|
if (node.lock) {
|
|
1538
1665
|
if (prompt) startPrompt(node, prompt === "focus");
|
|
@@ -1548,7 +1675,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
1548
1675
|
node.expanded = false;
|
|
1549
1676
|
node.lock = newLock(false, passwords.dropStale(rememberedAt) ? "stale" : "");
|
|
1550
1677
|
if (prompt) startPrompt(node, prompt === "focus");
|
|
1551
|
-
} else showToast(failureMessage("folderFailed", error));
|
|
1678
|
+
} else if (!quiet) showToast(failureMessage("folderFailed", error));
|
|
1552
1679
|
return false;
|
|
1553
1680
|
} finally {
|
|
1554
1681
|
node.loading = false;
|
|
@@ -1635,6 +1762,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
1635
1762
|
}
|
|
1636
1763
|
/** First load, or a retry after an error: shows the loading state. */
|
|
1637
1764
|
async function loadRoot() {
|
|
1765
|
+
stopDeepSearch();
|
|
1638
1766
|
refreshSeq += 1;
|
|
1639
1767
|
refreshing.value = false;
|
|
1640
1768
|
loadingRoot.value = true;
|
|
@@ -1711,6 +1839,88 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
1711
1839
|
return out;
|
|
1712
1840
|
});
|
|
1713
1841
|
const visible = computed(() => rows.value.filter((row) => !isMore(row)));
|
|
1842
|
+
/** A name split around the filter keyword, for highlighting. */
|
|
1843
|
+
function nameParts(name) {
|
|
1844
|
+
const query = keyword.value.trim().toLowerCase();
|
|
1845
|
+
if (!query) return [{
|
|
1846
|
+
text: name,
|
|
1847
|
+
hit: false
|
|
1848
|
+
}];
|
|
1849
|
+
const parts = [];
|
|
1850
|
+
const lower = name.toLowerCase();
|
|
1851
|
+
let from = 0;
|
|
1852
|
+
for (let at = lower.indexOf(query); at !== -1; at = lower.indexOf(query, from)) {
|
|
1853
|
+
if (at > from) parts.push({
|
|
1854
|
+
text: name.slice(from, at),
|
|
1855
|
+
hit: false
|
|
1856
|
+
});
|
|
1857
|
+
parts.push({
|
|
1858
|
+
text: name.slice(at, at + query.length),
|
|
1859
|
+
hit: true
|
|
1860
|
+
});
|
|
1861
|
+
from = at + query.length;
|
|
1862
|
+
}
|
|
1863
|
+
if (from < name.length) parts.push({
|
|
1864
|
+
text: name.slice(from),
|
|
1865
|
+
hit: false
|
|
1866
|
+
});
|
|
1867
|
+
return parts;
|
|
1868
|
+
}
|
|
1869
|
+
/**
|
|
1870
|
+
* The filter only sees loaded folders. Deep search loads the rest under the
|
|
1871
|
+
* mounted paths (four at a time, `deepSearchLimit` per run) so matches appear
|
|
1872
|
+
* in place, in their folders, like any other row. Locked folders are skipped.
|
|
1873
|
+
*/
|
|
1874
|
+
const deep = reactive({
|
|
1875
|
+
running: false,
|
|
1876
|
+
searched: 0,
|
|
1877
|
+
skipped: 0,
|
|
1878
|
+
failed: 0,
|
|
1879
|
+
capped: false,
|
|
1880
|
+
done: false
|
|
1881
|
+
});
|
|
1882
|
+
let deepSeq = 0;
|
|
1883
|
+
/** Folders that are known but not loaded yet (locked ones excluded). */
|
|
1884
|
+
function unloadedFolders() {
|
|
1885
|
+
const out = [];
|
|
1886
|
+
const walk = (nodes) => {
|
|
1887
|
+
for (const node of nodes) {
|
|
1888
|
+
if (!node.is_dir || node.lock) continue;
|
|
1889
|
+
if (!node.loaded) out.push(node);
|
|
1890
|
+
else walk(node.children);
|
|
1891
|
+
}
|
|
1892
|
+
};
|
|
1893
|
+
walk(roots.value);
|
|
1894
|
+
return out;
|
|
1895
|
+
}
|
|
1896
|
+
const hasUnloaded = computed(() => unloadedFolders().length > 0);
|
|
1897
|
+
const deepRow = computed(() => {
|
|
1898
|
+
if (!keyword.value.trim()) return null;
|
|
1899
|
+
if (deep.running) return "running";
|
|
1900
|
+
if (deep.capped) return "capped";
|
|
1901
|
+
if (hasUnloaded.value) return "idle";
|
|
1902
|
+
return deep.done ? "done" : null;
|
|
1903
|
+
});
|
|
1904
|
+
function stopDeepSearch() {
|
|
1905
|
+
deepSeq += 1;
|
|
1906
|
+
deep.running = false;
|
|
1907
|
+
}
|
|
1908
|
+
watch(keyword, (value) => {
|
|
1909
|
+
if (value.trim()) return;
|
|
1910
|
+
stopDeepSearch();
|
|
1911
|
+
Object.assign(deep, {
|
|
1912
|
+
searched: 0,
|
|
1913
|
+
skipped: 0,
|
|
1914
|
+
failed: 0,
|
|
1915
|
+
capped: false,
|
|
1916
|
+
done: false
|
|
1917
|
+
});
|
|
1918
|
+
});
|
|
1919
|
+
const deepSummary = computed(() => [
|
|
1920
|
+
deepRow.value === "capped" ? t("deepCapped", { count: deep.searched }) : t("deepDone", { count: deep.searched }),
|
|
1921
|
+
deep.skipped ? t("deepSkipped", { count: deep.skipped }) : "",
|
|
1922
|
+
deep.failed ? t("deepFailed", { count: deep.failed }) : ""
|
|
1923
|
+
].filter(Boolean).join(" · "));
|
|
1714
1924
|
const stats = computed(() => folderStats(roots.value, selected.value));
|
|
1715
1925
|
const noStats = {
|
|
1716
1926
|
files: 0,
|
|
@@ -2031,7 +2241,14 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
2031
2241
|
} else _push(`<span class="fl-arrow-placeholder" aria-hidden="true"></span>`);
|
|
2032
2242
|
if (node.is_dir) _push(`<svg class="fl-folder-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path></svg>`);
|
|
2033
2243
|
else _push(`<span class="fl-type"${ssrRenderAttr("data-kind", unref(fileKind)(node.name))} aria-hidden="true">${ssrInterpolate(unref(extOf)(node.name) || "FILE")}</span>`);
|
|
2034
|
-
_push(`<span class="fl-label"><span class="${ssrRenderClass([{ "fl-name-dir": node.is_dir }, "fl-name"])}"
|
|
2244
|
+
_push(`<span class="fl-label"><span class="${ssrRenderClass([{ "fl-name-dir": node.is_dir }, "fl-name"])}"><!--[-->`);
|
|
2245
|
+
ssrRenderList(nameParts(node.name), (part, at) => {
|
|
2246
|
+
_push(`<!--[-->`);
|
|
2247
|
+
if (part.hit) _push(`<mark class="fl-hit">${ssrInterpolate(part.text)}</mark>`);
|
|
2248
|
+
else _push(`<!--[-->${ssrInterpolate(part.text)}<!--]-->`);
|
|
2249
|
+
_push(`<!--]-->`);
|
|
2250
|
+
});
|
|
2251
|
+
_push(`<!--]--></span><span class="fl-meta">`);
|
|
2035
2252
|
if (!node.is_dir) _push(`<!--[-->${ssrInterpolate(unref(formatSize)(node.size))} · ${ssrInterpolate(unref(formatDate)(node.modified))}<!--]-->`);
|
|
2036
2253
|
else if (node.loaded) _push(`<!--[-->${ssrInterpolate(t("folderItems", { count: statsOf(node).files }))} · ${ssrInterpolate(unref(formatSize)(statsOf(node).bytes))}<!--]-->`);
|
|
2037
2254
|
else _push(`<!---->`);
|
|
@@ -2103,7 +2320,20 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
2103
2320
|
}
|
|
2104
2321
|
_push(`<!--]-->`);
|
|
2105
2322
|
});
|
|
2106
|
-
_push(`<!--]
|
|
2323
|
+
_push(`<!--]-->`);
|
|
2324
|
+
if (deepRow.value) {
|
|
2325
|
+
_push(`<div class="fl-row fl-deep" role="status"><span class="fl-cell fl-cell-check"></span><span class="fl-cell fl-more-cell"><span class="fl-indent" aria-hidden="true"></span>`);
|
|
2326
|
+
if (deepRow.value === "running") _push(`<!--[--><span class="fl-more-count">${ssrInterpolate(t("deepRunning", { count: deep.searched }))}</span><button class="fl-more-btn fl-more-all" type="button">${ssrInterpolate(t("deepStop"))}</button><span class="fl-deep-bar" aria-hidden="true"></span><!--]-->`);
|
|
2327
|
+
else if (deepRow.value === "idle") _push(`<!--[--><span class="fl-more-count">${ssrInterpolate(deep.searched ? deepSummary.value : t("deepHint"))}</span><button class="fl-more-btn" type="button">${ssrInterpolate(t("deepSearch"))}</button><!--]-->`);
|
|
2328
|
+
else {
|
|
2329
|
+
_push(`<!--[--><span class="fl-more-count">${ssrInterpolate(deepSummary.value)}</span>`);
|
|
2330
|
+
if (deepRow.value === "capped") _push(`<button class="fl-more-btn" type="button">${ssrInterpolate(t("deepContinue"))}</button>`);
|
|
2331
|
+
else _push(`<!---->`);
|
|
2332
|
+
_push(`<!--]-->`);
|
|
2333
|
+
}
|
|
2334
|
+
_push(`</span></div>`);
|
|
2335
|
+
} else _push(`<!---->`);
|
|
2336
|
+
_push(`<div class="fl-foot"><span>${ssrInterpolate(t("loaded", { count: allFiles.value.length }))} · ${ssrInterpolate(t("totalSize", { size: unref(formatSize)(unref(totalSize)(allFiles.value)) }))}</span></div>`);
|
|
2107
2337
|
if (toast.value) _push(`<div class="fl-toast" role="status">${ssrInterpolate(toast.value)}</div>`);
|
|
2108
2338
|
else _push(`<!---->`);
|
|
2109
2339
|
_push(ssrRenderComponent(_sfc_main$3, {
|
package/dist/client/style.css
CHANGED
|
@@ -824,7 +824,8 @@
|
|
|
824
824
|
|
|
825
825
|
/* ---------------- long folders ---------------- */
|
|
826
826
|
|
|
827
|
-
.fl-more
|
|
827
|
+
.fl-more,
|
|
828
|
+
.fl-deep {
|
|
828
829
|
background: var(--vp-c-bg-soft);
|
|
829
830
|
}
|
|
830
831
|
|
|
@@ -868,6 +869,37 @@
|
|
|
868
869
|
border-color: transparent;
|
|
869
870
|
}
|
|
870
871
|
|
|
872
|
+
/* ---------------- filter and deep search ---------------- */
|
|
873
|
+
|
|
874
|
+
.fl-hit {
|
|
875
|
+
padding: 0 1px;
|
|
876
|
+
color: inherit;
|
|
877
|
+
background: var(--fl-hit-bg, rgb(250 204 21 / 32%));
|
|
878
|
+
border-radius: 3px;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
.fl-deep {
|
|
882
|
+
position: relative;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
.fl-deep-bar {
|
|
886
|
+
position: absolute;
|
|
887
|
+
bottom: -1px;
|
|
888
|
+
left: 0;
|
|
889
|
+
width: 30%;
|
|
890
|
+
height: 2px;
|
|
891
|
+
background: var(--vp-c-brand-1);
|
|
892
|
+
animation: fl-progress 1.2s ease-in-out infinite;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
@media (prefers-reduced-motion: reduce) {
|
|
896
|
+
.fl-deep-bar {
|
|
897
|
+
width: 100%;
|
|
898
|
+
animation: none;
|
|
899
|
+
opacity: 0.6;
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
871
903
|
/* ---------------- confirm dialog ---------------- */
|
|
872
904
|
|
|
873
905
|
.fl-confirm {
|
|
@@ -1316,6 +1348,62 @@
|
|
|
1316
1348
|
background: var(--vp-c-default-soft);
|
|
1317
1349
|
}
|
|
1318
1350
|
|
|
1351
|
+
/* Markdown / HTML: source or rendered */
|
|
1352
|
+
.fl-preview-view {
|
|
1353
|
+
display: inline-flex;
|
|
1354
|
+
padding: 2px;
|
|
1355
|
+
margin-left: 6px;
|
|
1356
|
+
background: var(--vp-c-default-soft);
|
|
1357
|
+
border-radius: 8px;
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
.fl-preview-view-btn {
|
|
1361
|
+
height: 28px;
|
|
1362
|
+
padding: 0 10px;
|
|
1363
|
+
font-size: 12px;
|
|
1364
|
+
color: var(--vp-c-text-2);
|
|
1365
|
+
cursor: pointer;
|
|
1366
|
+
background: transparent;
|
|
1367
|
+
border: 0;
|
|
1368
|
+
border-radius: 6px;
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
.fl-preview-view-btn[aria-pressed='true'] {
|
|
1372
|
+
font-weight: 600;
|
|
1373
|
+
color: var(--vp-c-text-1);
|
|
1374
|
+
background: var(--vp-c-bg);
|
|
1375
|
+
box-shadow: 0 1px 2px rgb(0 0 0 / 12%);
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
.fl-preview-rendered {
|
|
1379
|
+
display: flex;
|
|
1380
|
+
flex-direction: column;
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
.fl-preview .fl-md {
|
|
1384
|
+
box-sizing: border-box;
|
|
1385
|
+
max-width: 860px;
|
|
1386
|
+
padding: 16px 24px 32px;
|
|
1387
|
+
margin: 0 auto;
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
.fl-md-image {
|
|
1391
|
+
display: inline-block;
|
|
1392
|
+
padding: 2px 8px;
|
|
1393
|
+
font-size: 12px;
|
|
1394
|
+
color: var(--vp-c-text-2);
|
|
1395
|
+
border: 1px dashed var(--vp-c-divider);
|
|
1396
|
+
border-radius: 6px;
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
.fl-preview-html {
|
|
1400
|
+
flex: 1 1 auto;
|
|
1401
|
+
width: 100%;
|
|
1402
|
+
min-height: 0;
|
|
1403
|
+
background: #fff;
|
|
1404
|
+
border: 0;
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1319
1407
|
.fl-preview-count {
|
|
1320
1408
|
min-width: 44px;
|
|
1321
1409
|
font-size: 12px;
|
|
@@ -1487,6 +1575,12 @@
|
|
|
1487
1575
|
min-width: 36px;
|
|
1488
1576
|
}
|
|
1489
1577
|
|
|
1578
|
+
.fl-preview-view-btn {
|
|
1579
|
+
height: 36px;
|
|
1580
|
+
padding: 0 14px;
|
|
1581
|
+
font-size: 14px;
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1490
1584
|
.fl-preview .fl-preview-head .fl-type {
|
|
1491
1585
|
display: none;
|
|
1492
1586
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vitepress-plugin-filelist",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"packageManager": "pnpm@11.25.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "OpenList file browser for VitePress: tree view, batch ZIP download, i18n, password and token support.",
|
|
@@ -42,7 +42,9 @@
|
|
|
42
42
|
"typecheck": "vue-tsc --noEmit",
|
|
43
43
|
"test": "vitest run",
|
|
44
44
|
"test:watch": "vitest",
|
|
45
|
-
"pack:check": "npm pack --dry-run"
|
|
45
|
+
"pack:check": "npm pack --dry-run",
|
|
46
|
+
"test:e2e": "playwright test",
|
|
47
|
+
"e2e:site": "pnpm run build && pnpm --dir demo install --force && pnpm --dir demo run build:e2e"
|
|
46
48
|
},
|
|
47
49
|
"peerDependencies": {
|
|
48
50
|
"vitepress": "^1.0.0 || ^2.0.0-alpha.0",
|
|
@@ -50,9 +52,13 @@
|
|
|
50
52
|
},
|
|
51
53
|
"dependencies": {
|
|
52
54
|
"client-zip": "^2.5.1",
|
|
55
|
+
"dompurify": "^3.4.17",
|
|
56
|
+
"markdown-it": "^14.3.3",
|
|
53
57
|
"vitepress-tuck": "^0.12.2"
|
|
54
58
|
},
|
|
55
59
|
"devDependencies": {
|
|
60
|
+
"@playwright/test": "1.63.0",
|
|
61
|
+
"@types/markdown-it": "^14.2.0",
|
|
56
62
|
"@types/node": "^22.10.0",
|
|
57
63
|
"@vue/test-utils": "^2.5.1",
|
|
58
64
|
"happy-dom": "^21.0.4",
|