@open-file-viewer/core 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,133 @@
1
+ # @open-file-viewer/core
2
+
3
+ Framework-agnostic browser file preview core for Open File Viewer.
4
+
5
+ Open File Viewer renders files inside your own DOM container instead of opening a new window. It supports images, PDF, Office documents, audio, video, text/code, archives, email, drawings, CAD, 3D and GIS formats through a plugin-based pipeline.
6
+
7
+ - Website: https://open-file-viewer-workspace.void.app
8
+ - GitHub: https://github.com/xushanpei/open-file-viewer
9
+ - npm: https://www.npmjs.com/package/@open-file-viewer/core
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ npm install @open-file-viewer/core
15
+ ```
16
+
17
+ PDF preview requires `pdfjs-dist`:
18
+
19
+ ```bash
20
+ npm install pdfjs-dist
21
+ ```
22
+
23
+ ## Quick Start
24
+
25
+ ```ts
26
+ import {
27
+ createViewer,
28
+ imagePlugin,
29
+ videoPlugin,
30
+ audioPlugin,
31
+ textPlugin,
32
+ pdfPlugin,
33
+ officePlugin,
34
+ archivePlugin,
35
+ emailPlugin,
36
+ drawingPlugin,
37
+ cadPlugin,
38
+ model3dPlugin,
39
+ gisPlugin,
40
+ fallbackPlugin
41
+ } from "@open-file-viewer/core";
42
+ import "@open-file-viewer/core/style.css";
43
+ import pdfWorkerSrc from "pdfjs-dist/build/pdf.worker.mjs?url";
44
+
45
+ const viewer = createViewer({
46
+ container: "#viewer",
47
+ file: fileOrUrl,
48
+ fileName: "contract.pdf",
49
+ width: "100%",
50
+ height: "70vh",
51
+ fit: "contain",
52
+ toolbar: true,
53
+ theme: "auto",
54
+ plugins: [
55
+ imagePlugin(),
56
+ videoPlugin(),
57
+ audioPlugin(),
58
+ textPlugin(),
59
+ pdfPlugin({ workerSrc: pdfWorkerSrc }),
60
+ officePlugin(),
61
+ archivePlugin(),
62
+ emailPlugin(),
63
+ drawingPlugin(),
64
+ cadPlugin(),
65
+ model3dPlugin(),
66
+ gisPlugin(),
67
+ fallbackPlugin()
68
+ ]
69
+ });
70
+
71
+ viewer.resize();
72
+ viewer.destroy();
73
+ ```
74
+
75
+ ## Supported Inputs
76
+
77
+ `createViewer` accepts local files and remote sources:
78
+
79
+ - `File`
80
+ - `Blob`
81
+ - URL string
82
+ - `ArrayBuffer`
83
+ - multiple files through `files`
84
+
85
+ ## Package Notes
86
+
87
+ Import the stylesheet once in your app:
88
+
89
+ ```ts
90
+ import "@open-file-viewer/core/style.css";
91
+ ```
92
+
93
+ React and Vue adapters are available as separate packages:
94
+
95
+ ```bash
96
+ npm install @open-file-viewer/react
97
+ npm install @open-file-viewer/vue
98
+ ```
99
+
100
+ ## Toolbar Customization
101
+
102
+ The toolbar can be configured from simple feature toggles to a fully custom renderer:
103
+
104
+ ```ts
105
+ createViewer({
106
+ container: "#viewer",
107
+ file,
108
+ toolbar: {
109
+ labels: {
110
+ download: "下载",
111
+ fullscreen: "全屏",
112
+ search: "搜索"
113
+ },
114
+ order: ["search", "download", "approve", "fullscreen"],
115
+ actions: [
116
+ {
117
+ id: "approve",
118
+ label: "审批",
119
+ onClick(ctx) {
120
+ openApprovalDialog(ctx.file);
121
+ }
122
+ }
123
+ ]
124
+ },
125
+ plugins
126
+ });
127
+ ```
128
+
129
+ Use `toolbar.render(ctx)` when you need to replace the toolbar completely. The context exposes file metadata, queue navigation, preview commands, download, fullscreen, print and search helpers.
130
+
131
+ ## License
132
+
133
+ MIT
package/dist/index.cjs CHANGED
@@ -735,6 +735,7 @@ function createViewer(options) {
735
735
  file,
736
736
  size: getElementSize(viewport),
737
737
  options: normalizedOptions,
738
+ toolbar: toolbar?.getContext(),
738
739
  setLoading,
739
740
  setError
740
741
  });
@@ -908,18 +909,32 @@ function createToolbar(toolbar, viewport, queue) {
908
909
  element.setAttribute("role", "toolbar");
909
910
  element.setAttribute("aria-label", "File preview toolbar");
910
911
  let file;
912
+ let currentIndex = 0;
913
+ let currentLength = queue.getLength();
911
914
  let queueLabel;
912
915
  let previousButton;
913
916
  let nextButton;
914
917
  const commandButtons = [];
918
+ const customButtons = [];
915
919
  const disposers = [];
916
920
  const search = createSearchController(viewport);
917
921
  let searchInput;
918
922
  let searchCount;
919
- const addButton = (label, title, action, className) => {
923
+ let canRunCommand = (_command) => false;
924
+ const getContext = () => createToolbarContext({
925
+ file,
926
+ index: currentIndex,
927
+ length: currentLength,
928
+ viewport,
929
+ queue,
930
+ element,
931
+ search,
932
+ canCommand: canRunCommand
933
+ });
934
+ const addButton = (label, title, action, className, icon) => {
920
935
  const button = document.createElement("button");
921
936
  button.type = "button";
922
- button.textContent = label;
937
+ setToolbarButtonContent(button, label, icon);
923
938
  button.title = title;
924
939
  button.setAttribute("aria-label", title);
925
940
  if (className) {
@@ -930,68 +945,117 @@ function createToolbar(toolbar, viewport, queue) {
930
945
  disposers.push(() => button.removeEventListener("click", action));
931
946
  return button;
932
947
  };
933
- const addQueueButton = (label, title, action) => {
934
- const button = document.createElement("button");
935
- button.type = "button";
936
- button.textContent = label;
937
- button.title = title;
938
- button.setAttribute("aria-label", title);
939
- const listener = () => {
940
- void action();
941
- };
942
- button.addEventListener("click", listener);
943
- element.append(button);
944
- disposers.push(() => button.removeEventListener("click", listener));
945
- return button;
946
- };
947
- if (queue.getLength() > 1) {
948
- previousButton = addQueueButton("Prev", "Previous file", queue.previous);
949
- nextButton = addQueueButton("Next", "Next file", queue.next);
950
- queueLabel = document.createElement("span");
951
- queueLabel.className = "ofv-toolbar-queue";
952
- element.append(queueLabel);
953
- }
954
- const addCommandButton = (label, title, command) => {
948
+ const addCommandButton = (id, label, title, command) => {
955
949
  const button = addButton(label, title, () => {
956
950
  queue.command(command);
957
- });
951
+ }, void 0, options.icons?.[id]);
958
952
  button.disabled = true;
959
953
  commandButtons.push({ button, command });
960
954
  };
961
- if (options.zoom) {
962
- addCommandButton("-", "Zoom out", "zoom-out");
963
- addCommandButton("+", "Zoom in", "zoom-in");
964
- addCommandButton("100%", "Reset zoom", "zoom-reset");
965
- }
966
- if (options.rotate) {
967
- addCommandButton("Rotate", "Rotate right", "rotate-right");
968
- }
969
- if (options.download !== false) {
970
- addButton("Download", "Download file", () => {
971
- if (file) {
972
- downloadFile(file);
955
+ const renderDefaultAction = (id) => {
956
+ if (!isBuiltInToolbarAction(id)) {
957
+ const customAction = options.actions?.find((action) => action.id === id);
958
+ if (customAction) {
959
+ renderCustomAction(customAction);
973
960
  }
974
- });
975
- }
976
- if (options.fullscreen !== false) {
977
- addButton("Fullscreen", "Open preview fullscreen", () => {
978
- const target = element.parentElement;
979
- void target?.requestFullscreen?.();
980
- });
981
- }
982
- if (options.print) {
983
- addButton("Print", "Print preview", () => {
984
- printPreview(viewport);
985
- });
986
- }
987
- if (options.search !== false) {
961
+ return;
962
+ }
963
+ if (id === "previous" && queue.getLength() > 1) {
964
+ previousButton = addButton(
965
+ getToolbarLabel(options, "previous"),
966
+ getToolbarTitle(options, "previous"),
967
+ () => void queue.previous(),
968
+ void 0,
969
+ options.icons?.previous
970
+ );
971
+ return;
972
+ }
973
+ if (id === "next" && queue.getLength() > 1) {
974
+ nextButton = addButton(
975
+ getToolbarLabel(options, "next"),
976
+ getToolbarTitle(options, "next"),
977
+ () => void queue.next(),
978
+ void 0,
979
+ options.icons?.next
980
+ );
981
+ return;
982
+ }
983
+ if (id === "queue" && queue.getLength() > 1) {
984
+ queueLabel = document.createElement("span");
985
+ queueLabel.className = "ofv-toolbar-queue";
986
+ element.append(queueLabel);
987
+ return;
988
+ }
989
+ if (id === "zoom-out" && options.zoom) {
990
+ addCommandButton(id, getToolbarLabel(options, id), getToolbarTitle(options, id), "zoom-out");
991
+ return;
992
+ }
993
+ if (id === "zoom-in" && options.zoom) {
994
+ addCommandButton(id, getToolbarLabel(options, id), getToolbarTitle(options, id), "zoom-in");
995
+ return;
996
+ }
997
+ if (id === "zoom-reset" && options.zoom) {
998
+ addCommandButton(id, getToolbarLabel(options, id), getToolbarTitle(options, id), "zoom-reset");
999
+ return;
1000
+ }
1001
+ if (id === "rotate-right" && options.rotate) {
1002
+ addCommandButton(id, getToolbarLabel(options, id), getToolbarTitle(options, id), "rotate-right");
1003
+ return;
1004
+ }
1005
+ if (id === "download" && options.download !== false) {
1006
+ addButton(
1007
+ getToolbarLabel(options, id),
1008
+ getToolbarTitle(options, id),
1009
+ () => getContext().download(),
1010
+ void 0,
1011
+ options.icons?.download
1012
+ );
1013
+ return;
1014
+ }
1015
+ if (id === "fullscreen" && options.fullscreen !== false) {
1016
+ addButton(
1017
+ getToolbarLabel(options, id),
1018
+ getToolbarTitle(options, id),
1019
+ () => getContext().fullscreen(),
1020
+ void 0,
1021
+ options.icons?.fullscreen
1022
+ );
1023
+ return;
1024
+ }
1025
+ if (id === "print" && options.print) {
1026
+ addButton(
1027
+ getToolbarLabel(options, id),
1028
+ getToolbarTitle(options, id),
1029
+ () => getContext().print(),
1030
+ void 0,
1031
+ options.icons?.print
1032
+ );
1033
+ return;
1034
+ }
1035
+ if (id === "search" && options.search !== false) {
1036
+ renderSearchControl();
1037
+ return;
1038
+ }
1039
+ };
1040
+ const renderCustomAction = (action) => {
1041
+ const button = addButton(
1042
+ action.label,
1043
+ action.title || action.label,
1044
+ () => void action.onClick(getContext()),
1045
+ action.className,
1046
+ action.icon
1047
+ );
1048
+ button.dataset.ofvToolbarAction = action.id;
1049
+ customButtons.push({ button, action });
1050
+ };
1051
+ const renderSearchControl = () => {
988
1052
  const searchGroup = document.createElement("div");
989
1053
  searchGroup.className = "ofv-toolbar-search";
990
- searchGroup.title = "Search preview text";
1054
+ searchGroup.title = getToolbarTitle(options, "search");
991
1055
  const nextSearchInput = document.createElement("input");
992
1056
  nextSearchInput.type = "search";
993
- nextSearchInput.placeholder = "Search";
994
- nextSearchInput.setAttribute("aria-label", "Search preview text");
1057
+ nextSearchInput.placeholder = getToolbarLabel(options, "search");
1058
+ nextSearchInput.setAttribute("aria-label", getToolbarTitle(options, "search"));
995
1059
  const nextSearchCount = document.createElement("span");
996
1060
  nextSearchCount.className = "ofv-toolbar-search-count";
997
1061
  searchInput = nextSearchInput;
@@ -1004,18 +1068,53 @@ function createToolbar(toolbar, viewport, queue) {
1004
1068
  searchGroup.append(nextSearchInput, nextSearchCount);
1005
1069
  element.append(searchGroup);
1006
1070
  disposers.push(() => nextSearchInput.removeEventListener("input", runSearch));
1007
- }
1071
+ };
1072
+ const renderToolbar = () => {
1073
+ if (options.render) {
1074
+ element.replaceChildren();
1075
+ const customElement = options.render(getContext());
1076
+ if (customElement) {
1077
+ element.append(customElement);
1078
+ }
1079
+ return;
1080
+ }
1081
+ getToolbarOrder(options, queue.getLength()).forEach(renderDefaultAction);
1082
+ getImplicitCustomActions(options).forEach(renderCustomAction);
1083
+ };
1084
+ renderToolbar();
1085
+ const updateCustomButtons = () => {
1086
+ const context = getContext();
1087
+ for (const { button, action } of customButtons) {
1088
+ button.disabled = evaluateToolbarFlag(action.disabled, context);
1089
+ button.hidden = evaluateToolbarFlag(action.hidden, context);
1090
+ }
1091
+ };
1092
+ const resetSearch = () => {
1093
+ search.clear();
1094
+ if (searchInput) {
1095
+ searchInput.value = "";
1096
+ }
1097
+ if (searchCount) {
1098
+ searchCount.textContent = "";
1099
+ }
1100
+ };
1101
+ const refreshCustomRender = () => {
1102
+ if (!options.render) {
1103
+ return;
1104
+ }
1105
+ element.replaceChildren();
1106
+ const customElement = options.render(getContext());
1107
+ if (customElement) {
1108
+ element.append(customElement);
1109
+ }
1110
+ };
1008
1111
  return {
1009
1112
  element,
1010
1113
  update(nextFile, index, length) {
1011
1114
  file = nextFile;
1012
- search.clear();
1013
- if (searchInput) {
1014
- searchInput.value = "";
1015
- }
1016
- if (searchCount) {
1017
- searchCount.textContent = "";
1018
- }
1115
+ currentIndex = index;
1116
+ currentLength = length;
1117
+ resetSearch();
1019
1118
  commandButtons.forEach(({ button }) => {
1020
1119
  button.disabled = true;
1021
1120
  });
@@ -1028,20 +1127,158 @@ function createToolbar(toolbar, viewport, queue) {
1028
1127
  if (nextButton) {
1029
1128
  nextButton.disabled = index >= length - 1;
1030
1129
  }
1130
+ updateCustomButtons();
1131
+ refreshCustomRender();
1031
1132
  },
1032
1133
  setCommandSupport(isSupported) {
1134
+ canRunCommand = isSupported;
1033
1135
  commandButtons.forEach(({ button, command }) => {
1034
1136
  button.disabled = !isSupported(command);
1035
1137
  });
1138
+ updateCustomButtons();
1139
+ refreshCustomRender();
1036
1140
  },
1141
+ getContext,
1037
1142
  destroy() {
1038
1143
  search.clear();
1039
1144
  for (const dispose of disposers) {
1040
1145
  dispose();
1041
1146
  }
1147
+ element.replaceChildren();
1042
1148
  }
1043
1149
  };
1044
1150
  }
1151
+ function createToolbarContext({
1152
+ file,
1153
+ index,
1154
+ length,
1155
+ viewport,
1156
+ queue,
1157
+ element,
1158
+ search,
1159
+ canCommand
1160
+ }) {
1161
+ return {
1162
+ file,
1163
+ index,
1164
+ length,
1165
+ viewport,
1166
+ canPrevious: index > 0,
1167
+ canNext: index < length - 1,
1168
+ async previous() {
1169
+ await queue.previous();
1170
+ },
1171
+ async next() {
1172
+ await queue.next();
1173
+ },
1174
+ command: queue.command,
1175
+ canCommand,
1176
+ download() {
1177
+ if (file) {
1178
+ downloadFile(file);
1179
+ }
1180
+ },
1181
+ fullscreen() {
1182
+ void element.parentElement?.requestFullscreen?.();
1183
+ },
1184
+ print() {
1185
+ printPreview(viewport);
1186
+ },
1187
+ search: search.search,
1188
+ clearSearch: search.clear
1189
+ };
1190
+ }
1191
+ var defaultToolbarLabels = {
1192
+ previous: "Prev",
1193
+ next: "Next",
1194
+ queue: "",
1195
+ "zoom-out": "-",
1196
+ "zoom-in": "+",
1197
+ "zoom-reset": "100%",
1198
+ "rotate-right": "Rotate",
1199
+ download: "Download",
1200
+ fullscreen: "Fullscreen",
1201
+ print: "Print",
1202
+ search: "Search"
1203
+ };
1204
+ var defaultToolbarTitles = {
1205
+ previous: "Previous file",
1206
+ next: "Next file",
1207
+ queue: "Current file position",
1208
+ "zoom-out": "Zoom out",
1209
+ "zoom-in": "Zoom in",
1210
+ "zoom-reset": "Reset zoom",
1211
+ "rotate-right": "Rotate right",
1212
+ download: "Download file",
1213
+ fullscreen: "Open preview fullscreen",
1214
+ print: "Print preview",
1215
+ search: "Search preview text"
1216
+ };
1217
+ function getToolbarLabel(options, id) {
1218
+ return options.labels?.[id] ?? defaultToolbarLabels[id];
1219
+ }
1220
+ function getToolbarTitle(options, id) {
1221
+ return options.titles?.[id] ?? options.labels?.[id] ?? defaultToolbarTitles[id];
1222
+ }
1223
+ function getToolbarOrder(options, queueLength) {
1224
+ if (options.order) {
1225
+ return options.order;
1226
+ }
1227
+ const actions = [];
1228
+ if (queueLength > 1) {
1229
+ actions.push("previous", "next", "queue");
1230
+ }
1231
+ if (options.zoom) {
1232
+ actions.push("zoom-out", "zoom-in", "zoom-reset");
1233
+ }
1234
+ if (options.rotate) {
1235
+ actions.push("rotate-right");
1236
+ }
1237
+ if (options.download !== false) {
1238
+ actions.push("download");
1239
+ }
1240
+ if (options.fullscreen !== false) {
1241
+ actions.push("fullscreen");
1242
+ }
1243
+ if (options.print) {
1244
+ actions.push("print");
1245
+ }
1246
+ if (options.search !== false) {
1247
+ actions.push("search");
1248
+ }
1249
+ return actions;
1250
+ }
1251
+ function getImplicitCustomActions(options) {
1252
+ if (options.order || !options.actions) {
1253
+ return [];
1254
+ }
1255
+ return [...options.actions].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
1256
+ }
1257
+ function evaluateToolbarFlag(value, context) {
1258
+ return typeof value === "function" ? value(context) : Boolean(value);
1259
+ }
1260
+ function setToolbarButtonContent(button, label, icon) {
1261
+ button.replaceChildren();
1262
+ if (!icon) {
1263
+ button.textContent = label;
1264
+ return;
1265
+ }
1266
+ const iconElement = document.createElement("span");
1267
+ iconElement.className = "ofv-toolbar-icon";
1268
+ iconElement.setAttribute("aria-hidden", "true");
1269
+ if (typeof icon === "string") {
1270
+ iconElement.innerHTML = icon;
1271
+ } else {
1272
+ iconElement.append(icon.cloneNode(true));
1273
+ }
1274
+ const labelElement = document.createElement("span");
1275
+ labelElement.className = "ofv-toolbar-label";
1276
+ labelElement.textContent = label;
1277
+ button.append(iconElement, labelElement);
1278
+ }
1279
+ function isBuiltInToolbarAction(id) {
1280
+ return id in defaultToolbarLabels;
1281
+ }
1045
1282
  function createSearchController(root) {
1046
1283
  const markerClass = "ofv-search-match";
1047
1284
  const clear = () => {