docxodus 12.1.0 → 12.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 +14 -4
- package/dist/editor.bundle.js +137 -23
- package/dist/editor.d.ts +25 -3
- package/dist/editor.d.ts.map +1 -1
- package/dist/editor.js +132 -13
- package/dist/editor.js.map +1 -1
- package/dist/embed.bundle.js +1163 -48
- package/dist/embed.d.ts +30 -1
- package/dist/embed.d.ts.map +1 -1
- package/dist/embed.iife.js +1163 -48
- package/dist/embed.js +124 -13
- package/dist/embed.js.map +1 -1
- package/dist/export-assets.json +33 -33
- package/dist/history-checkpoints.d.ts +48 -0
- package/dist/history-checkpoints.d.ts.map +1 -0
- package/dist/history-checkpoints.js +100 -0
- package/dist/history-checkpoints.js.map +1 -0
- package/dist/history-controls.d.ts +30 -0
- package/dist/history-controls.d.ts.map +1 -0
- package/dist/history-controls.js +333 -0
- package/dist/history-controls.js.map +1 -0
- package/dist/history-indexeddb.d.ts +12 -0
- package/dist/history-indexeddb.d.ts.map +1 -0
- package/dist/history-indexeddb.js +154 -0
- package/dist/history-indexeddb.js.map +1 -0
- package/dist/history.d.ts +265 -0
- package/dist/history.d.ts.map +1 -0
- package/dist/history.js +307 -0
- package/dist/history.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +23 -2
- package/dist/index.js.map +1 -1
- package/dist/ribbon-chrome.d.ts +2 -2
- package/dist/ribbon-chrome.d.ts.map +1 -1
- package/dist/ribbon-chrome.js +6 -4
- package/dist/ribbon-chrome.js.map +1 -1
- package/dist/ribbon.js +42 -9
- package/dist/ribbon.js.map +1 -1
- package/dist/session.js +1 -1
- package/dist/session.js.map +1 -1
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/wasm/_framework/DocumentFormat.OpenXml.Framework.wasm +0 -0
- package/dist/wasm/_framework/DocumentFormat.OpenXml.Framework.wasm.br +0 -0
- package/dist/wasm/_framework/DocumentFormat.OpenXml.wasm +0 -0
- package/dist/wasm/_framework/DocumentFormat.OpenXml.wasm.br +0 -0
- package/dist/wasm/_framework/Docxodus.wasm +0 -0
- package/dist/wasm/_framework/Docxodus.wasm.br +0 -0
- package/dist/wasm/_framework/DocxodusWasm.wasm +0 -0
- package/dist/wasm/_framework/DocxodusWasm.wasm.br +0 -0
- package/dist/wasm/_framework/System.Collections.Concurrent.wasm +0 -0
- package/dist/wasm/_framework/System.Collections.Concurrent.wasm.br +0 -0
- package/dist/wasm/_framework/System.Collections.wasm +0 -0
- package/dist/wasm/_framework/System.Collections.wasm.br +0 -0
- package/dist/wasm/_framework/System.IO.Compression.wasm +0 -0
- package/dist/wasm/_framework/System.IO.Compression.wasm.br +0 -0
- package/dist/wasm/_framework/System.IO.Packaging.wasm +0 -0
- package/dist/wasm/_framework/System.IO.Packaging.wasm.br +0 -0
- package/dist/wasm/_framework/System.Linq.Expressions.wasm +0 -0
- package/dist/wasm/_framework/System.Linq.Expressions.wasm.br +0 -0
- package/dist/wasm/_framework/System.Memory.wasm +0 -0
- package/dist/wasm/_framework/System.Memory.wasm.br +0 -0
- package/dist/wasm/_framework/System.Net.Http.wasm +0 -0
- package/dist/wasm/_framework/System.Net.Http.wasm.br +0 -0
- package/dist/wasm/_framework/System.Private.CoreLib.wasm +0 -0
- package/dist/wasm/_framework/System.Private.CoreLib.wasm.br +0 -0
- package/dist/wasm/_framework/System.Private.Xml.Linq.wasm +0 -0
- package/dist/wasm/_framework/System.Private.Xml.Linq.wasm.br +0 -0
- package/dist/wasm/_framework/System.Private.Xml.wasm +0 -0
- package/dist/wasm/_framework/System.Private.Xml.wasm.br +0 -0
- package/dist/wasm/_framework/System.Runtime.InteropServices.JavaScript.wasm +0 -0
- package/dist/wasm/_framework/System.Runtime.InteropServices.JavaScript.wasm.br +0 -0
- package/dist/wasm/_framework/System.Runtime.wasm +0 -0
- package/dist/wasm/_framework/System.Runtime.wasm.br +0 -0
- package/dist/wasm/_framework/System.Security.Cryptography.wasm +0 -0
- package/dist/wasm/_framework/System.Security.Cryptography.wasm.br +0 -0
- package/dist/wasm/_framework/System.Text.Json.wasm +0 -0
- package/dist/wasm/_framework/System.Text.Json.wasm.br +0 -0
- package/dist/wasm/_framework/System.Text.RegularExpressions.wasm +0 -0
- package/dist/wasm/_framework/System.Text.RegularExpressions.wasm.br +0 -0
- package/dist/wasm/_framework/dotnet.boot.js +21 -21
- package/dist/wasm/_framework/dotnet.boot.js.br +0 -0
- package/dist/wasm/_framework/dotnet.native.wasm +0 -0
- package/dist/wasm/_framework/dotnet.native.wasm.br +0 -0
- package/package.json +6 -3
package/dist/embed.iife.js
CHANGED
|
@@ -141,8 +141,15 @@ var Docxodus = (() => {
|
|
|
141
141
|
DocxDiffFormatComparison: () => DocxDiffFormatComparison,
|
|
142
142
|
DocxDiffRevisionGranularity: () => DocxDiffRevisionGranularity,
|
|
143
143
|
DocxEditor: () => DocxEditor,
|
|
144
|
+
DocxHistoryArchive: () => DocxHistoryArchive,
|
|
145
|
+
DocxHistoryClient: () => DocxHistoryClient,
|
|
146
|
+
DocxHistoryDocument: () => DocxHistoryDocument,
|
|
147
|
+
DocxHistoryError: () => DocxHistoryError,
|
|
148
|
+
DocxHistoryReader: () => DocxHistoryReader,
|
|
144
149
|
DocxSession: () => DocxSession,
|
|
145
150
|
EmptyParagraphMode: () => EmptyParagraphMode,
|
|
151
|
+
HistoryCheckpoints: () => HistoryCheckpoints,
|
|
152
|
+
MAX_HISTORY_ARCHIVE_BYTES: () => MAX_HISTORY_ARCHIVE_BYTES,
|
|
146
153
|
PaginationEngine: () => PaginationEngine,
|
|
147
154
|
PaginationMode: () => PaginationMode,
|
|
148
155
|
PlaceholderKinds: () => PlaceholderKinds,
|
|
@@ -166,6 +173,8 @@ var Docxodus = (() => {
|
|
|
166
173
|
createBlankDocx: () => createBlankDocx,
|
|
167
174
|
createEditor: () => createEditor,
|
|
168
175
|
createExternalAnnotationSet: () => createExternalAnnotationSet,
|
|
176
|
+
createHistoryViewer: () => createHistoryViewer,
|
|
177
|
+
createMemoryHistoryStorage: () => createMemoryHistoryStorage,
|
|
169
178
|
createRibbonEditor: () => createRibbonEditor,
|
|
170
179
|
createUnavailablePageMap: () => createUnavailablePageMap,
|
|
171
180
|
createViewer: () => createViewer,
|
|
@@ -199,15 +208,21 @@ var Docxodus = (() => {
|
|
|
199
208
|
getVersion: () => getVersion,
|
|
200
209
|
getWasmExports: () => getWasmExports,
|
|
201
210
|
hasAnnotations: () => hasAnnotations,
|
|
211
|
+
historyControlError: () => historyControlError,
|
|
202
212
|
initialize: () => initialize,
|
|
213
|
+
installHistoryStorageImports: () => installHistoryStorageImports,
|
|
203
214
|
isDeletion: () => isDeletion,
|
|
204
215
|
isFormatChange: () => isFormatChange,
|
|
205
216
|
isInitialized: () => isInitialized,
|
|
206
217
|
isInsertion: () => isInsertion,
|
|
207
218
|
isMove: () => isMove,
|
|
219
|
+
mountHistoryControls: () => mountHistoryControls,
|
|
208
220
|
mountRibbon: () => mountRibbon,
|
|
209
221
|
navigateToPageCitation: () => navigateToPageCitation,
|
|
222
|
+
openDocxHistory: () => openDocxHistory,
|
|
223
|
+
openDocxHistoryArchive: () => openDocxHistoryArchive,
|
|
210
224
|
openDocxSession: () => openDocxSession2,
|
|
225
|
+
openIndexedDbHistoryStore: () => openIndexedDbHistoryStore,
|
|
211
226
|
paginateHtml: () => paginateHtml,
|
|
212
227
|
parseSectionDimensions: () => parseSectionDimensions,
|
|
213
228
|
projectAnnotationsOntoHtml: () => projectAnnotationsOntoHtml,
|
|
@@ -2161,6 +2176,872 @@ var Docxodus = (() => {
|
|
|
2161
2176
|
return new DocxSession(handle, bridge);
|
|
2162
2177
|
}
|
|
2163
2178
|
|
|
2179
|
+
// src/history.ts
|
|
2180
|
+
var MAX_HISTORY_ARCHIVE_BYTES = 64 * 1024 * 1024;
|
|
2181
|
+
var DocxHistoryError = class extends Error {
|
|
2182
|
+
constructor(code, message) {
|
|
2183
|
+
super(message);
|
|
2184
|
+
this.code = code;
|
|
2185
|
+
this.name = "DocxHistoryError";
|
|
2186
|
+
}
|
|
2187
|
+
};
|
|
2188
|
+
var adapters = /* @__PURE__ */ new Map();
|
|
2189
|
+
var nextAdapter = 0;
|
|
2190
|
+
function adapter(id) {
|
|
2191
|
+
const store = adapters.get(id);
|
|
2192
|
+
if (!store) throw new DocxHistoryError("Closed", "History storage adapter is closed.");
|
|
2193
|
+
return store;
|
|
2194
|
+
}
|
|
2195
|
+
function toBase64(bytes) {
|
|
2196
|
+
let binary = "";
|
|
2197
|
+
for (let offset = 0; offset < bytes.length; offset += 32768)
|
|
2198
|
+
binary += String.fromCharCode(...bytes.subarray(offset, offset + 32768));
|
|
2199
|
+
return btoa(binary);
|
|
2200
|
+
}
|
|
2201
|
+
function fromBase64(encoded) {
|
|
2202
|
+
const binary = atob(encoded);
|
|
2203
|
+
return Uint8Array.from(binary, (c) => c.charCodeAt(0));
|
|
2204
|
+
}
|
|
2205
|
+
function installHistoryStorageImports(setModuleImports) {
|
|
2206
|
+
setModuleImports("docxodus.history", {
|
|
2207
|
+
readBlob: async (id, json) => {
|
|
2208
|
+
const reference = JSON.parse(json);
|
|
2209
|
+
const bytes = await adapter(id).readBlob(reference);
|
|
2210
|
+
if (bytes === null) return "";
|
|
2211
|
+
if (bytes.length !== reference.length) throw new DocxHistoryError("PayloadMismatch", "Stored blob length differs from its reference.");
|
|
2212
|
+
return bytes.length === 0 ? "=" : toBase64(bytes);
|
|
2213
|
+
},
|
|
2214
|
+
putBlob: async (id, json, bytes) => adapter(id).putBlob(JSON.parse(json), bytes.slice()),
|
|
2215
|
+
readHead: async (id, documentId) => JSON.stringify(await adapter(id).readHead(documentId)),
|
|
2216
|
+
advanceHead: async (id, documentId, expected, state) => JSON.stringify(await adapter(id).advanceHead(documentId, JSON.parse(expected), JSON.parse(state))),
|
|
2217
|
+
initializeHead: async (id, documentId, head) => {
|
|
2218
|
+
const storage = adapter(id);
|
|
2219
|
+
if (!storage.initializeHead) throw new DocxHistoryError("InitializationUnsupported", "Storage cannot import an exact head.");
|
|
2220
|
+
return JSON.stringify(await storage.initializeHead(documentId, JSON.parse(head)));
|
|
2221
|
+
}
|
|
2222
|
+
});
|
|
2223
|
+
}
|
|
2224
|
+
var DocxHistoryClient = class {
|
|
2225
|
+
constructor(bridge, storage) {
|
|
2226
|
+
this.bridge = bridge;
|
|
2227
|
+
this.closed = false;
|
|
2228
|
+
if (nextAdapter >= 2147483647) throw new Error("History adapter identifiers exhausted.");
|
|
2229
|
+
this.adapterId = ++nextAdapter;
|
|
2230
|
+
adapters.set(this.adapterId, storage);
|
|
2231
|
+
try {
|
|
2232
|
+
this.handle = storage.initializeHead && bridge.OpenWithInitialization ? bridge.OpenWithInitialization(this.adapterId) : bridge.Open(this.adapterId);
|
|
2233
|
+
} catch (error) {
|
|
2234
|
+
adapters.delete(this.adapterId);
|
|
2235
|
+
throw error;
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
close() {
|
|
2239
|
+
if (this.closed) return;
|
|
2240
|
+
this.bridge.Close(this.handle);
|
|
2241
|
+
adapters.delete(this.adapterId);
|
|
2242
|
+
this.closed = true;
|
|
2243
|
+
}
|
|
2244
|
+
/** Bind an identity without reading, creating a version, or taking ownership of this client. */
|
|
2245
|
+
document(documentId) {
|
|
2246
|
+
return new DocxHistoryDocument(documentId, (operation, fields, bytes) => this.call(operation, documentId, fields, bytes));
|
|
2247
|
+
}
|
|
2248
|
+
async exportHistoryArchive(documentId) {
|
|
2249
|
+
return fromBase64((await this.call("exportArchive", documentId)).bytes);
|
|
2250
|
+
}
|
|
2251
|
+
async importHistoryArchive(bytes) {
|
|
2252
|
+
checkArchiveSize(bytes);
|
|
2253
|
+
return (await this.call("importArchive", "", {}, bytes)).import;
|
|
2254
|
+
}
|
|
2255
|
+
async read(documentId) {
|
|
2256
|
+
return (await this.call("read", documentId)).view;
|
|
2257
|
+
}
|
|
2258
|
+
/** Host-triggered validated metadata tail; first join starts at the latest checkpoint. */
|
|
2259
|
+
async readChangesSince(documentId, after, maxEntriesToScan = 1e4) {
|
|
2260
|
+
return (await this.call("updates", documentId, { expectedHead: after, maxEntriesToScan })).update;
|
|
2261
|
+
}
|
|
2262
|
+
async readOperationsSince(documentId, after, maxEntriesToScan = 1e4) {
|
|
2263
|
+
return (await this.call("operations", documentId, { expectedHead: after, maxEntriesToScan })).operationUpdate;
|
|
2264
|
+
}
|
|
2265
|
+
async getOperation(documentId, operationId) {
|
|
2266
|
+
return (await this.call("getOperation", documentId, { operationId })).operation;
|
|
2267
|
+
}
|
|
2268
|
+
async exportOperationProposal(documentId, operationId) {
|
|
2269
|
+
return fromBase64((await this.call("exportOperationProposal", documentId, { operationId })).bytes);
|
|
2270
|
+
}
|
|
2271
|
+
async compareVersions(documentId, beforeVersionId, afterVersionId) {
|
|
2272
|
+
return fromBase64((await this.call("compare", documentId, { beforeVersionId, afterVersionId })).bytes);
|
|
2273
|
+
}
|
|
2274
|
+
async createVersion(documentId, expectedHead, bytes, metadata, requestId) {
|
|
2275
|
+
return (await this.call("create", documentId, { expectedHead, metadata, requestId }, bytes)).view;
|
|
2276
|
+
}
|
|
2277
|
+
async listVersions(documentId, cursor = null, limit = 25) {
|
|
2278
|
+
return (await this.call("list", documentId, { versionId: cursor, limit })).page;
|
|
2279
|
+
}
|
|
2280
|
+
async getVersion(documentId, versionId) {
|
|
2281
|
+
return (await this.call("get", documentId, { versionId })).version;
|
|
2282
|
+
}
|
|
2283
|
+
async exportVersion(documentId, versionId) {
|
|
2284
|
+
return fromBase64((await this.call("export", documentId, { versionId })).bytes);
|
|
2285
|
+
}
|
|
2286
|
+
async materialize(documentId, sequence, maxEntriesToScan = 1e4) {
|
|
2287
|
+
return fromBase64((await this.call("materialize", documentId, { sequence, maxEntriesToScan })).bytes);
|
|
2288
|
+
}
|
|
2289
|
+
async replay(documentId, sequence, maxEntriesToScan = 1e4) {
|
|
2290
|
+
return fromBase64((await this.call("replay", documentId, { sequence, maxEntriesToScan })).bytes);
|
|
2291
|
+
}
|
|
2292
|
+
async resolveSequenceAtTime(documentId, cutoff, maxEntriesToScan = 1e4) {
|
|
2293
|
+
return (await this.call("resolveTime", documentId, { cutoff, maxEntriesToScan })).sequence;
|
|
2294
|
+
}
|
|
2295
|
+
async restoreVersion(documentId, expectedHead, versionId, metadata, requestId) {
|
|
2296
|
+
return (await this.call("restore", documentId, { expectedHead, versionId, metadata, requestId })).view;
|
|
2297
|
+
}
|
|
2298
|
+
async call(operation, documentId, fields = {}, bytes = new Uint8Array()) {
|
|
2299
|
+
if (this.closed) throw new DocxHistoryError("Closed", "History client is closed.");
|
|
2300
|
+
return parseResult(await this.bridge.Invoke(
|
|
2301
|
+
this.handle,
|
|
2302
|
+
JSON.stringify({ schemaVersion: 1, operation, documentId, ...fields }),
|
|
2303
|
+
bytes
|
|
2304
|
+
));
|
|
2305
|
+
}
|
|
2306
|
+
};
|
|
2307
|
+
var DocxHistoryReader = class {
|
|
2308
|
+
/** @internal Obtain through client.document() or openDocxHistoryArchive(). */
|
|
2309
|
+
constructor(documentId, call) {
|
|
2310
|
+
this.call = call;
|
|
2311
|
+
this.#documentId = documentId;
|
|
2312
|
+
}
|
|
2313
|
+
#documentId;
|
|
2314
|
+
get documentId() {
|
|
2315
|
+
return this.#documentId;
|
|
2316
|
+
}
|
|
2317
|
+
async read() {
|
|
2318
|
+
return (await this.call("read")).view;
|
|
2319
|
+
}
|
|
2320
|
+
async listVersions(cursor = null, limit = 25) {
|
|
2321
|
+
return (await this.call("list", { versionId: cursor, limit })).page;
|
|
2322
|
+
}
|
|
2323
|
+
async getVersion(versionId) {
|
|
2324
|
+
return (await this.call("get", { versionId })).version;
|
|
2325
|
+
}
|
|
2326
|
+
/** Omit versionId for latest; pass an ID to keep a UI operation pinned to a captured version. */
|
|
2327
|
+
async exportDocx(versionId = null) {
|
|
2328
|
+
return fromBase64((await this.call("exportDocx", { versionId })).bytes);
|
|
2329
|
+
}
|
|
2330
|
+
async exportHistoryArchive() {
|
|
2331
|
+
return fromBase64((await this.call("exportArchive")).bytes);
|
|
2332
|
+
}
|
|
2333
|
+
async materialize(sequence, maxEntriesToScan = 1e4) {
|
|
2334
|
+
return fromBase64((await this.call("materialize", { sequence, maxEntriesToScan })).bytes);
|
|
2335
|
+
}
|
|
2336
|
+
async replay(sequence, maxEntriesToScan = 1e4) {
|
|
2337
|
+
return fromBase64((await this.call("replay", { sequence, maxEntriesToScan })).bytes);
|
|
2338
|
+
}
|
|
2339
|
+
async resolveSequenceAtTime(cutoff, maxEntriesToScan = 1e4) {
|
|
2340
|
+
return (await this.call("resolveTime", { cutoff, maxEntriesToScan })).sequence;
|
|
2341
|
+
}
|
|
2342
|
+
async readChangesSince(after, maxEntriesToScan = 1e4) {
|
|
2343
|
+
return (await this.call("updates", { expectedHead: after, maxEntriesToScan })).update;
|
|
2344
|
+
}
|
|
2345
|
+
async readOperationsSince(after, maxEntriesToScan = 1e4) {
|
|
2346
|
+
return (await this.call("operations", { expectedHead: after, maxEntriesToScan })).operationUpdate;
|
|
2347
|
+
}
|
|
2348
|
+
async getOperation(operationId) {
|
|
2349
|
+
return (await this.call("getOperation", { operationId })).operation;
|
|
2350
|
+
}
|
|
2351
|
+
async exportOperationProposal(operationId) {
|
|
2352
|
+
return fromBase64((await this.call("exportOperationProposal", { operationId })).bytes);
|
|
2353
|
+
}
|
|
2354
|
+
/** Redlined DOCX through DocxCompare's existing accepted-input revision policy. */
|
|
2355
|
+
async compareVersions(beforeVersionId, afterVersionId) {
|
|
2356
|
+
return fromBase64((await this.call("compare", { beforeVersionId, afterVersionId })).bytes);
|
|
2357
|
+
}
|
|
2358
|
+
};
|
|
2359
|
+
var DocxHistoryDocument = class extends DocxHistoryReader {
|
|
2360
|
+
async createVersion(expectedHead, bytes, metadata, requestId) {
|
|
2361
|
+
requireRequestId(requestId);
|
|
2362
|
+
return (await this.call("create", { expectedHead, metadata, requestId }, bytes)).view;
|
|
2363
|
+
}
|
|
2364
|
+
async restoreVersion(expectedHead, versionId, metadata, requestId) {
|
|
2365
|
+
requireRequestId(requestId);
|
|
2366
|
+
return (await this.call("restore", { expectedHead, versionId, metadata, requestId })).view;
|
|
2367
|
+
}
|
|
2368
|
+
};
|
|
2369
|
+
var DocxHistoryArchive = class _DocxHistoryArchive extends DocxHistoryReader {
|
|
2370
|
+
constructor(bridge, handle, info) {
|
|
2371
|
+
super(info.documentId, async (operation, fields = {}, bytes = new Uint8Array()) => {
|
|
2372
|
+
if (this.closed) throw new DocxHistoryError("Closed", "History archive is closed.");
|
|
2373
|
+
return parseResult(await bridge.Invoke(
|
|
2374
|
+
handle,
|
|
2375
|
+
JSON.stringify({ schemaVersion: 1, operation, documentId: this.documentId, ...fields }),
|
|
2376
|
+
bytes
|
|
2377
|
+
));
|
|
2378
|
+
});
|
|
2379
|
+
this.bridge = bridge;
|
|
2380
|
+
this.handle = handle;
|
|
2381
|
+
this.info = info;
|
|
2382
|
+
this.closed = false;
|
|
2383
|
+
}
|
|
2384
|
+
/** For custom loaders; normal callers use openDocxHistoryArchive(). */
|
|
2385
|
+
static async open(bridge, bytes) {
|
|
2386
|
+
checkArchiveSize(bytes);
|
|
2387
|
+
if (!bridge.OpenArchive) throw new Error("This WASM build does not include portable history.");
|
|
2388
|
+
const result = parseResult(await bridge.OpenArchive(bytes));
|
|
2389
|
+
return new _DocxHistoryArchive(bridge, result.handle, result.archive);
|
|
2390
|
+
}
|
|
2391
|
+
close() {
|
|
2392
|
+
if (this.closed) return;
|
|
2393
|
+
this.bridge.Close(this.handle);
|
|
2394
|
+
this.closed = true;
|
|
2395
|
+
}
|
|
2396
|
+
};
|
|
2397
|
+
function parseResult(json) {
|
|
2398
|
+
const result = JSON.parse(json);
|
|
2399
|
+
if (!result.success) throw new DocxHistoryError(result.errorCode, result.message);
|
|
2400
|
+
return result;
|
|
2401
|
+
}
|
|
2402
|
+
function checkArchiveSize(bytes) {
|
|
2403
|
+
if (bytes.length > MAX_HISTORY_ARCHIVE_BYTES) throw new DocxHistoryError("ResourceLimit", "History archive exceeds 64 MiB.");
|
|
2404
|
+
}
|
|
2405
|
+
function requireRequestId(requestId) {
|
|
2406
|
+
if (typeof requestId !== "string" || !requestId.trim()) throw new DocxHistoryError("InvalidRequest", "A durable requestId is required.");
|
|
2407
|
+
}
|
|
2408
|
+
function createMemoryHistoryStorage(maxBlobBytes = 256 * 1024 * 1024) {
|
|
2409
|
+
if (!Number.isSafeInteger(maxBlobBytes) || maxBlobBytes <= 0) throw new RangeError("Invalid blob byte limit.");
|
|
2410
|
+
const blobs = /* @__PURE__ */ new Map();
|
|
2411
|
+
const heads = /* @__PURE__ */ new Map();
|
|
2412
|
+
const copy = (value) => JSON.parse(JSON.stringify(value));
|
|
2413
|
+
const key = (reference) => {
|
|
2414
|
+
if (reference.digest.algorithm !== "SHA-256" || !/^[a-f0-9]{64}$/.test(reference.digest.value) || !Number.isSafeInteger(reference.length) || reference.length < 0 || reference.length > maxBlobBytes)
|
|
2415
|
+
throw new DocxHistoryError("InvalidRequest", "Invalid blob reference or byte limit.");
|
|
2416
|
+
return `${reference.digest.value}:${reference.length}`;
|
|
2417
|
+
};
|
|
2418
|
+
const equalReference = (a, b) => a.length === b.length && a.digest.algorithm === b.digest.algorithm && a.digest.value === b.digest.value;
|
|
2419
|
+
const equalHead2 = (a, b) => a === null || b === null ? a === b : a.revision === b.revision && equalReference(a.state, b.state);
|
|
2420
|
+
return {
|
|
2421
|
+
async readBlob(reference) {
|
|
2422
|
+
return blobs.get(key(reference))?.slice() ?? null;
|
|
2423
|
+
},
|
|
2424
|
+
async putBlob(reference, bytes) {
|
|
2425
|
+
const blobKey = key(reference);
|
|
2426
|
+
const captured = bytes.slice();
|
|
2427
|
+
if (captured.length !== reference.length) throw new DocxHistoryError("PayloadMismatch", "Blob length mismatch.");
|
|
2428
|
+
const digest = Array.from(
|
|
2429
|
+
new Uint8Array(await crypto.subtle.digest("SHA-256", captured)),
|
|
2430
|
+
(n) => n.toString(16).padStart(2, "0")
|
|
2431
|
+
).join("");
|
|
2432
|
+
if (digest !== blobKey.split(":")[0]) throw new DocxHistoryError("PayloadMismatch", "Blob digest mismatch.");
|
|
2433
|
+
if (!blobs.has(blobKey)) blobs.set(blobKey, captured);
|
|
2434
|
+
},
|
|
2435
|
+
async readHead(documentId) {
|
|
2436
|
+
return copy(heads.get(documentId) ?? null);
|
|
2437
|
+
},
|
|
2438
|
+
async advanceHead(documentId, expected, state) {
|
|
2439
|
+
key(state);
|
|
2440
|
+
const current = heads.get(documentId) ?? null;
|
|
2441
|
+
if (!equalHead2(current, expected)) return null;
|
|
2442
|
+
const revision = BigInt(current?.revision ?? "0") + 1n;
|
|
2443
|
+
if (revision > 9223372036854775807n) throw new RangeError("History revision exhausted.");
|
|
2444
|
+
const head = { revision: String(revision), state: copy(state) };
|
|
2445
|
+
heads.set(documentId, head);
|
|
2446
|
+
return copy(head);
|
|
2447
|
+
},
|
|
2448
|
+
async initializeHead(documentId, head) {
|
|
2449
|
+
key(head.state);
|
|
2450
|
+
if (typeof head.revision !== "string" || !/^[1-9][0-9]*$/.test(head.revision) || BigInt(head.revision) > 9223372036854775807n)
|
|
2451
|
+
throw new DocxHistoryError("InvalidRequest", "Imported head revision must be a positive Int64 string.");
|
|
2452
|
+
const existing = heads.get(documentId);
|
|
2453
|
+
if (existing) return { initialized: false, head: copy(existing) };
|
|
2454
|
+
const captured = copy(head);
|
|
2455
|
+
heads.set(documentId, captured);
|
|
2456
|
+
return { initialized: true, head: copy(captured) };
|
|
2457
|
+
}
|
|
2458
|
+
};
|
|
2459
|
+
}
|
|
2460
|
+
|
|
2461
|
+
// src/history-checkpoints.ts
|
|
2462
|
+
var HistoryCheckpoints = class _HistoryCheckpoints {
|
|
2463
|
+
constructor(document2, journal) {
|
|
2464
|
+
this.document = document2;
|
|
2465
|
+
this.journal = journal;
|
|
2466
|
+
this.current = null;
|
|
2467
|
+
this.request = null;
|
|
2468
|
+
this.running = false;
|
|
2469
|
+
this.stale = false;
|
|
2470
|
+
}
|
|
2471
|
+
/** Pass an already captured view when opening its exact version in an editor (for example, after import). */
|
|
2472
|
+
static async open(document2, journal, view) {
|
|
2473
|
+
const controls = new _HistoryCheckpoints(document2, journal);
|
|
2474
|
+
controls.request = structuredClone(await journal.read());
|
|
2475
|
+
if (controls.request && controls.request.documentId !== document2.documentId)
|
|
2476
|
+
throw new DocxHistoryError("InvalidRequest", "The pending checkpoint belongs to another document.");
|
|
2477
|
+
controls.current = view === void 0 ? await document2.read() : structuredClone(view);
|
|
2478
|
+
if (controls.current && controls.current.state.documentId !== document2.documentId)
|
|
2479
|
+
throw new DocxHistoryError("InvalidRequest", "The captured view belongs to another document.");
|
|
2480
|
+
return controls;
|
|
2481
|
+
}
|
|
2482
|
+
get view() {
|
|
2483
|
+
return structuredClone(this.current);
|
|
2484
|
+
}
|
|
2485
|
+
get hasPending() {
|
|
2486
|
+
return this.request !== null;
|
|
2487
|
+
}
|
|
2488
|
+
get needsRefresh() {
|
|
2489
|
+
return this.stale;
|
|
2490
|
+
}
|
|
2491
|
+
async refresh() {
|
|
2492
|
+
return this.exclusive(async () => {
|
|
2493
|
+
this.current = await this.document.read();
|
|
2494
|
+
this.stale = false;
|
|
2495
|
+
return this.view;
|
|
2496
|
+
});
|
|
2497
|
+
}
|
|
2498
|
+
async save(bytes, metadata) {
|
|
2499
|
+
return this.start({
|
|
2500
|
+
kind: "save",
|
|
2501
|
+
head: this.current?.head ?? null,
|
|
2502
|
+
bytes,
|
|
2503
|
+
id: crypto.randomUUID(),
|
|
2504
|
+
documentId: this.document.documentId,
|
|
2505
|
+
metadata
|
|
2506
|
+
});
|
|
2507
|
+
}
|
|
2508
|
+
async restore(target, metadata) {
|
|
2509
|
+
if (!this.current) throw new DocxHistoryError("NotFound", "Save a checkpoint before restoring a version.");
|
|
2510
|
+
return this.start({
|
|
2511
|
+
kind: "restore",
|
|
2512
|
+
head: this.current.head,
|
|
2513
|
+
target,
|
|
2514
|
+
id: crypto.randomUUID(),
|
|
2515
|
+
documentId: this.document.documentId,
|
|
2516
|
+
metadata
|
|
2517
|
+
});
|
|
2518
|
+
}
|
|
2519
|
+
async retry() {
|
|
2520
|
+
return this.exclusive(async () => {
|
|
2521
|
+
if (!this.request) throw new DocxHistoryError("InvalidRequest", "There is no pending checkpoint.");
|
|
2522
|
+
return this.publish();
|
|
2523
|
+
});
|
|
2524
|
+
}
|
|
2525
|
+
async start(request) {
|
|
2526
|
+
return this.exclusive(async () => {
|
|
2527
|
+
if (this.request) throw new DocxHistoryError("PendingRequest", "Retry the pending checkpoint first.");
|
|
2528
|
+
if (this.stale) throw new DocxHistoryError("StaleHead", "Refresh history before saving again.");
|
|
2529
|
+
this.request = structuredClone(request);
|
|
2530
|
+
return this.publish();
|
|
2531
|
+
});
|
|
2532
|
+
}
|
|
2533
|
+
async publish() {
|
|
2534
|
+
const intended = this.request;
|
|
2535
|
+
const request = await this.journal.put(structuredClone(intended));
|
|
2536
|
+
this.request = structuredClone(request);
|
|
2537
|
+
if (request.id !== intended.id)
|
|
2538
|
+
throw new DocxHistoryError("PendingRequest", "Another tab has a pending checkpoint. Retry it before saving your draft.");
|
|
2539
|
+
let view;
|
|
2540
|
+
try {
|
|
2541
|
+
view = request.kind === "save" ? await this.document.createVersion(request.head, request.bytes, request.metadata, request.id) : await this.document.restoreVersion(request.head, request.target, request.metadata, request.id);
|
|
2542
|
+
} catch (error) {
|
|
2543
|
+
if (error instanceof DocxHistoryError && error.code === "StaleHead") {
|
|
2544
|
+
this.stale = true;
|
|
2545
|
+
await this.journal.remove(request.id);
|
|
2546
|
+
this.request = null;
|
|
2547
|
+
}
|
|
2548
|
+
throw error;
|
|
2549
|
+
}
|
|
2550
|
+
this.current = view;
|
|
2551
|
+
await this.journal.remove(request.id);
|
|
2552
|
+
this.request = null;
|
|
2553
|
+
return this.view;
|
|
2554
|
+
}
|
|
2555
|
+
async exclusive(action) {
|
|
2556
|
+
if (this.running) throw new DocxHistoryError("Busy", "A history command is still running.");
|
|
2557
|
+
this.running = true;
|
|
2558
|
+
try {
|
|
2559
|
+
return await action();
|
|
2560
|
+
} finally {
|
|
2561
|
+
this.running = false;
|
|
2562
|
+
}
|
|
2563
|
+
}
|
|
2564
|
+
};
|
|
2565
|
+
|
|
2566
|
+
// src/history-indexeddb.ts
|
|
2567
|
+
async function openIndexedDbHistoryStore(name) {
|
|
2568
|
+
const db = await new Promise((resolve, reject) => {
|
|
2569
|
+
let blocked = false;
|
|
2570
|
+
const request = indexedDB.open(name, 1);
|
|
2571
|
+
request.onupgradeneeded = () => {
|
|
2572
|
+
for (const store of ["blobs", "heads", "requests"]) request.result.createObjectStore(store);
|
|
2573
|
+
};
|
|
2574
|
+
request.onsuccess = () => {
|
|
2575
|
+
if (blocked) request.result.close();
|
|
2576
|
+
else resolve(request.result);
|
|
2577
|
+
};
|
|
2578
|
+
request.onerror = () => reject(request.error);
|
|
2579
|
+
request.onblocked = () => {
|
|
2580
|
+
blocked = true;
|
|
2581
|
+
reject(new Error("Close other tabs using this history store, then try again."));
|
|
2582
|
+
};
|
|
2583
|
+
});
|
|
2584
|
+
db.onversionchange = () => db.close();
|
|
2585
|
+
function transaction(name2, mode, action) {
|
|
2586
|
+
return new Promise((resolve, reject) => {
|
|
2587
|
+
const tx = db.transaction(name2, mode);
|
|
2588
|
+
let value;
|
|
2589
|
+
tx.oncomplete = () => resolve(value);
|
|
2590
|
+
tx.onabort = () => reject(tx.error ?? new Error("History storage transaction was aborted."));
|
|
2591
|
+
try {
|
|
2592
|
+
action(tx.objectStore(name2), (result) => {
|
|
2593
|
+
value = result;
|
|
2594
|
+
});
|
|
2595
|
+
} catch (error) {
|
|
2596
|
+
tx.abort();
|
|
2597
|
+
reject(error);
|
|
2598
|
+
}
|
|
2599
|
+
});
|
|
2600
|
+
}
|
|
2601
|
+
const storage = {
|
|
2602
|
+
readBlob(reference) {
|
|
2603
|
+
const key = referenceKey(reference);
|
|
2604
|
+
return transaction("blobs", "readonly", (store, result) => {
|
|
2605
|
+
store.get(key).onsuccess = (event) => result(event.target.result ?? null);
|
|
2606
|
+
});
|
|
2607
|
+
},
|
|
2608
|
+
async putBlob(reference, bytes) {
|
|
2609
|
+
const key = referenceKey(reference);
|
|
2610
|
+
const captured = bytes.slice();
|
|
2611
|
+
if (captured.length !== reference.length) throw new DocxHistoryError("PayloadMismatch", "Stored document length does not match its reference.");
|
|
2612
|
+
const hash = Array.from(
|
|
2613
|
+
new Uint8Array(await crypto.subtle.digest("SHA-256", captured)),
|
|
2614
|
+
(byte) => byte.toString(16).padStart(2, "0")
|
|
2615
|
+
).join("");
|
|
2616
|
+
if (hash !== key.split(":")[0])
|
|
2617
|
+
throw new DocxHistoryError("PayloadMismatch", "Stored document bytes do not match their reference.");
|
|
2618
|
+
await transaction("blobs", "readwrite", (store, result) => {
|
|
2619
|
+
store.put(captured, key);
|
|
2620
|
+
result();
|
|
2621
|
+
});
|
|
2622
|
+
},
|
|
2623
|
+
readHead(documentId) {
|
|
2624
|
+
return transaction("heads", "readonly", (store, result) => {
|
|
2625
|
+
store.get(documentId).onsuccess = (event) => result(event.target.result ?? null);
|
|
2626
|
+
});
|
|
2627
|
+
},
|
|
2628
|
+
advanceHead(documentId, expected, state) {
|
|
2629
|
+
referenceKey(state);
|
|
2630
|
+
const captured = structuredClone({ expected, state });
|
|
2631
|
+
if (expected) validateHead(expected);
|
|
2632
|
+
const revision = BigInt(expected?.revision ?? "0") + 1n;
|
|
2633
|
+
if (revision > 9223372036854775807n) throw new RangeError("History revision exhausted.");
|
|
2634
|
+
return transaction("heads", "readwrite", (store, result) => {
|
|
2635
|
+
store.get(documentId).onsuccess = (event) => {
|
|
2636
|
+
const current = event.target.result ?? null;
|
|
2637
|
+
if (!equalHead(current, captured.expected)) {
|
|
2638
|
+
result(null);
|
|
2639
|
+
return;
|
|
2640
|
+
}
|
|
2641
|
+
const head = { revision: String(revision), state: captured.state };
|
|
2642
|
+
store.put(head, documentId);
|
|
2643
|
+
result(head);
|
|
2644
|
+
};
|
|
2645
|
+
});
|
|
2646
|
+
},
|
|
2647
|
+
initializeHead(documentId, head) {
|
|
2648
|
+
validateHead(head);
|
|
2649
|
+
const captured = structuredClone(head);
|
|
2650
|
+
return transaction("heads", "readwrite", (store, result) => {
|
|
2651
|
+
store.get(documentId).onsuccess = (event) => {
|
|
2652
|
+
const existing = event.target.result;
|
|
2653
|
+
if (existing) {
|
|
2654
|
+
result({ initialized: false, head: existing });
|
|
2655
|
+
return;
|
|
2656
|
+
}
|
|
2657
|
+
store.put(captured, documentId);
|
|
2658
|
+
result({ initialized: true, head: captured });
|
|
2659
|
+
};
|
|
2660
|
+
});
|
|
2661
|
+
}
|
|
2662
|
+
};
|
|
2663
|
+
return {
|
|
2664
|
+
storage,
|
|
2665
|
+
journal(documentId) {
|
|
2666
|
+
return {
|
|
2667
|
+
read: () => transaction("requests", "readonly", (store, result) => {
|
|
2668
|
+
store.get(documentId).onsuccess = (event) => result(event.target.result ?? null);
|
|
2669
|
+
}),
|
|
2670
|
+
put(request) {
|
|
2671
|
+
if (request.documentId !== documentId) throw new Error("Checkpoint document identity does not match.");
|
|
2672
|
+
const captured = structuredClone(request);
|
|
2673
|
+
return transaction("requests", "readwrite", (store, result) => {
|
|
2674
|
+
store.get(documentId).onsuccess = (event) => {
|
|
2675
|
+
const existing = event.target.result;
|
|
2676
|
+
if (existing) {
|
|
2677
|
+
result(existing);
|
|
2678
|
+
return;
|
|
2679
|
+
}
|
|
2680
|
+
store.put(captured, documentId);
|
|
2681
|
+
result(captured);
|
|
2682
|
+
};
|
|
2683
|
+
});
|
|
2684
|
+
},
|
|
2685
|
+
remove(requestId) {
|
|
2686
|
+
return transaction("requests", "readwrite", (store, result) => {
|
|
2687
|
+
store.get(documentId).onsuccess = (event) => {
|
|
2688
|
+
if (event.target.result?.id === requestId) store.delete(documentId);
|
|
2689
|
+
result();
|
|
2690
|
+
};
|
|
2691
|
+
});
|
|
2692
|
+
}
|
|
2693
|
+
};
|
|
2694
|
+
},
|
|
2695
|
+
close: () => db.close()
|
|
2696
|
+
};
|
|
2697
|
+
}
|
|
2698
|
+
function referenceKey(reference) {
|
|
2699
|
+
if (reference.digest.algorithm !== "SHA-256" || !/^[a-f0-9]{64}$/.test(reference.digest.value) || !Number.isSafeInteger(reference.length) || reference.length < 0)
|
|
2700
|
+
throw new DocxHistoryError("InvalidRequest", "Invalid history blob reference.");
|
|
2701
|
+
return `${reference.digest.value}:${reference.length}`;
|
|
2702
|
+
}
|
|
2703
|
+
function validateHead(head) {
|
|
2704
|
+
referenceKey(head.state);
|
|
2705
|
+
if (typeof head.revision !== "string" || !/^[1-9][0-9]*$/.test(head.revision) || BigInt(head.revision) > 9223372036854775807n)
|
|
2706
|
+
throw new DocxHistoryError("InvalidRequest", "Invalid history revision.");
|
|
2707
|
+
}
|
|
2708
|
+
function equalHead(a, b) {
|
|
2709
|
+
return a === null || b === null ? a === b : a.revision === b.revision && a.state.length === b.state.length && a.state.digest.algorithm === b.state.digest.algorithm && a.state.digest.value === b.state.digest.value;
|
|
2710
|
+
}
|
|
2711
|
+
|
|
2712
|
+
// src/history-controls.ts
|
|
2713
|
+
function mountHistoryControls(container, options) {
|
|
2714
|
+
const pageSize = options.pageSize ?? 25;
|
|
2715
|
+
if (!Number.isInteger(pageSize) || pageSize < 1 || pageSize > 100) throw new RangeError("History page size must be 1\u2013100.");
|
|
2716
|
+
if (options.checkpoints && options.checkpoints.document !== options.reader)
|
|
2717
|
+
throw new Error("History controls and checkpoints must use the same document.");
|
|
2718
|
+
return new HistoryPanel(container, options, pageSize);
|
|
2719
|
+
}
|
|
2720
|
+
var HistoryPanel = class {
|
|
2721
|
+
constructor(container, options, pageSize) {
|
|
2722
|
+
this.options = options;
|
|
2723
|
+
this.pageSize = pageSize;
|
|
2724
|
+
this.actions = /* @__PURE__ */ new Map();
|
|
2725
|
+
this.events = new AbortController();
|
|
2726
|
+
this.records = [];
|
|
2727
|
+
this.next = null;
|
|
2728
|
+
this.view = null;
|
|
2729
|
+
this.active = null;
|
|
2730
|
+
this.destroyed = false;
|
|
2731
|
+
const doc = container.ownerDocument;
|
|
2732
|
+
this.element = doc.createElement("section");
|
|
2733
|
+
this.element.className = "dx-history";
|
|
2734
|
+
this.element.setAttribute("aria-label", "Document history");
|
|
2735
|
+
const style = doc.createElement("style");
|
|
2736
|
+
style.textContent = HISTORY_CSS;
|
|
2737
|
+
const title = doc.createElement("h2");
|
|
2738
|
+
title.textContent = "Document history";
|
|
2739
|
+
this.status = doc.createElement("p");
|
|
2740
|
+
this.status.setAttribute("role", "status");
|
|
2741
|
+
this.status.setAttribute("aria-live", "polite");
|
|
2742
|
+
this.fieldset = doc.createElement("fieldset");
|
|
2743
|
+
const legend = doc.createElement("legend");
|
|
2744
|
+
legend.textContent = "Versions and checkpoints";
|
|
2745
|
+
this.fieldset.append(legend);
|
|
2746
|
+
this.element.append(style, title, this.status, this.fieldset);
|
|
2747
|
+
container.append(this.element);
|
|
2748
|
+
const note = doc.createElement("p");
|
|
2749
|
+
note.textContent = options.checkpoints ? "Browse saved versions without changing your draft." : "Read-only history. Preview or download any saved version.";
|
|
2750
|
+
this.fieldset.append(note);
|
|
2751
|
+
this.button("refresh", "Refresh history", () => this.load(true));
|
|
2752
|
+
this.button("latest", "Open latest", async () => this.preview(await options.reader.exportDocx(), "Latest saved version"));
|
|
2753
|
+
this.versions = this.select("Version");
|
|
2754
|
+
this.versions.size = 6;
|
|
2755
|
+
this.detail = doc.createElement("p");
|
|
2756
|
+
this.fieldset.append(this.detail);
|
|
2757
|
+
this.versions.addEventListener("change", () => this.update(), { signal: this.events.signal });
|
|
2758
|
+
this.button("preview", "Preview selected", async () => this.preview(await options.reader.exportDocx(this.selected().id), versionTitle(this.selected())));
|
|
2759
|
+
this.button("download", "Download selected", async () => this.download(await options.reader.exportDocx(this.selected().id), `version-${this.selected().record.sequence}.docx`));
|
|
2760
|
+
this.before = this.select("Compare from");
|
|
2761
|
+
this.before.addEventListener("change", () => this.update(), { signal: this.events.signal });
|
|
2762
|
+
const compareNote = doc.createElement("p");
|
|
2763
|
+
compareNote.textContent = "Compare from this version to the selected version above.";
|
|
2764
|
+
this.fieldset.append(compareNote);
|
|
2765
|
+
this.button("compare", "Compare versions", async () => {
|
|
2766
|
+
const before = this.records[Number(this.before.value)];
|
|
2767
|
+
await this.preview(await options.reader.compareVersions(before.id, this.selected().id), "Comparison with tracked changes");
|
|
2768
|
+
});
|
|
2769
|
+
this.button("more", "Load older versions", async () => {
|
|
2770
|
+
if (this.next) await this.page(this.next, false);
|
|
2771
|
+
});
|
|
2772
|
+
this.author = this.input("Your name", "text", options.author ?? "You");
|
|
2773
|
+
this.label = this.input("Checkpoint name (optional)", "text");
|
|
2774
|
+
this.author.parentElement.hidden = this.label.parentElement.hidden = !options.checkpoints;
|
|
2775
|
+
this.button("save", "Save checkpoint", async () => {
|
|
2776
|
+
const view = await options.checkpoints.save(await options.capture(), this.metadata());
|
|
2777
|
+
await options.onCheckpoint?.(view, "save");
|
|
2778
|
+
await this.load(false);
|
|
2779
|
+
this.label.value = "";
|
|
2780
|
+
this.status.textContent = "Checkpoint saved. Your draft remains open.";
|
|
2781
|
+
});
|
|
2782
|
+
this.button("restore", "Restore selected", async () => {
|
|
2783
|
+
const version = this.selected();
|
|
2784
|
+
if (!doc.defaultView?.confirm(`Restore ${versionTitle(version)} as a new checkpoint?
|
|
2785
|
+
|
|
2786
|
+
Your current draft and all later versions will be kept.`)) {
|
|
2787
|
+
this.status.textContent = "Restore canceled. Your draft and history are unchanged.";
|
|
2788
|
+
return;
|
|
2789
|
+
}
|
|
2790
|
+
const view = await options.checkpoints.restore(version.id, this.metadata());
|
|
2791
|
+
await options.onCheckpoint?.(view, "restore");
|
|
2792
|
+
await this.load(false);
|
|
2793
|
+
this.status.textContent = "Restored as a new checkpoint. Your draft and later versions are kept.";
|
|
2794
|
+
});
|
|
2795
|
+
const restoreNote = doc.createElement("p");
|
|
2796
|
+
restoreNote.hidden = !options.checkpoints;
|
|
2797
|
+
restoreNote.textContent = "Restore creates a new checkpoint. Open latest to preview it; your draft stays open.";
|
|
2798
|
+
this.fieldset.append(restoreNote);
|
|
2799
|
+
this.button("retry", "Retry checkpoint", async () => {
|
|
2800
|
+
const view = await options.checkpoints.retry();
|
|
2801
|
+
await options.onCheckpoint?.(view, "retry");
|
|
2802
|
+
await this.load(false);
|
|
2803
|
+
this.status.textContent = "Checkpoint recovered. Your draft remains open. Refresh history to check for newer versions.";
|
|
2804
|
+
});
|
|
2805
|
+
const sharing = this.disclosure("Download with history");
|
|
2806
|
+
const sharingNote = doc.createElement("p");
|
|
2807
|
+
sharingNote.textContent = "Includes retained drafts and collaboration proposals. Share this file only when you want to include that history. A DOCX download keeps existing Word comments and revisions, without external history.";
|
|
2808
|
+
sharing.append(sharingNote);
|
|
2809
|
+
this.button("archive", "Download .docxhistory", async () => this.download(await options.reader.exportHistoryArchive(), "docxhistory"), sharing);
|
|
2810
|
+
const time = this.disclosure("Find a version by time");
|
|
2811
|
+
const cutoff = this.input("Saved at or before (local time)", "datetime-local", "", time);
|
|
2812
|
+
this.button("time", "Preview at time", async () => {
|
|
2813
|
+
if (!cutoff.value) throw new DocxHistoryError("InvalidRequest", "Choose a date and time first.");
|
|
2814
|
+
const sequence = await options.reader.resolveSequenceAtTime(new Date(cutoff.value).toISOString());
|
|
2815
|
+
await this.preview(await options.reader.materialize(sequence), "Version at selected time");
|
|
2816
|
+
}, time);
|
|
2817
|
+
const activity = this.disclosure("Recorded collaboration");
|
|
2818
|
+
const decisions = doc.createElement("ol");
|
|
2819
|
+
let showOlderActivity = () => {
|
|
2820
|
+
};
|
|
2821
|
+
this.button("activity", "Load activity", async () => {
|
|
2822
|
+
const { operations } = await options.reader.readOperationsSince(null);
|
|
2823
|
+
const resolved = new Set(operations.filter((op) => op.record.status === "accepted").map((op) => op.input.request.resolves?.digest.value));
|
|
2824
|
+
let shown = 0;
|
|
2825
|
+
showOlderActivity = () => {
|
|
2826
|
+
const page = operations.slice(Math.max(0, operations.length - shown - this.pageSize), operations.length - shown).reverse();
|
|
2827
|
+
decisions.append(...page.map((op) => this.activityItem(op, resolved.has(op.id.digest.value))));
|
|
2828
|
+
shown += page.length;
|
|
2829
|
+
this.actions.get("activity-more").hidden = shown >= operations.length;
|
|
2830
|
+
};
|
|
2831
|
+
decisions.replaceChildren();
|
|
2832
|
+
showOlderActivity();
|
|
2833
|
+
this.status.textContent = operations.length ? "Recorded activity loaded." : "No recorded collaboration.";
|
|
2834
|
+
}, activity);
|
|
2835
|
+
activity.append(decisions);
|
|
2836
|
+
this.button("activity-more", "Load older activity", async () => showOlderActivity(), activity);
|
|
2837
|
+
this.actions.get("activity-more").hidden = true;
|
|
2838
|
+
this.ready = this.run("Loading history", () => this.load(false));
|
|
2839
|
+
}
|
|
2840
|
+
refresh() {
|
|
2841
|
+
return this.run("Loading history", () => this.load(true));
|
|
2842
|
+
}
|
|
2843
|
+
async destroy() {
|
|
2844
|
+
this.destroyed = true;
|
|
2845
|
+
this.events.abort();
|
|
2846
|
+
this.element.remove();
|
|
2847
|
+
await this.active?.catch(() => {
|
|
2848
|
+
});
|
|
2849
|
+
}
|
|
2850
|
+
async load(refresh) {
|
|
2851
|
+
this.view = this.options.checkpoints ? refresh ? await this.options.checkpoints.refresh() : this.options.checkpoints.view : await this.options.reader.read();
|
|
2852
|
+
if (this.view) await this.page(this.view.version.id, true);
|
|
2853
|
+
else {
|
|
2854
|
+
this.records = [];
|
|
2855
|
+
this.next = null;
|
|
2856
|
+
this.versions.replaceChildren();
|
|
2857
|
+
this.before.replaceChildren();
|
|
2858
|
+
}
|
|
2859
|
+
this.status.textContent = this.options.checkpoints?.hasPending ? "A checkpoint needs recovery. Retry it before saving more changes." : this.view ? "History loaded. Select a version to preview, download or restore." : "No checkpoints yet. Save your first checkpoint when you are ready.";
|
|
2860
|
+
}
|
|
2861
|
+
async page(cursor, reset) {
|
|
2862
|
+
const page = await this.options.reader.listVersions(cursor, this.pageSize);
|
|
2863
|
+
if (reset) {
|
|
2864
|
+
this.records = [];
|
|
2865
|
+
this.versions.replaceChildren();
|
|
2866
|
+
this.before.replaceChildren();
|
|
2867
|
+
}
|
|
2868
|
+
const start2 = this.records.length;
|
|
2869
|
+
this.records.push(...page.versions);
|
|
2870
|
+
this.next = page.next;
|
|
2871
|
+
for (let index = start2; index < this.records.length; index++) {
|
|
2872
|
+
for (const select of [this.versions, this.before]) {
|
|
2873
|
+
const option = select.ownerDocument.createElement("option");
|
|
2874
|
+
option.value = String(index);
|
|
2875
|
+
option.textContent = option.title = versionTitle(this.records[index]);
|
|
2876
|
+
select.append(option);
|
|
2877
|
+
}
|
|
2878
|
+
}
|
|
2879
|
+
if (reset) {
|
|
2880
|
+
this.versions.value = "0";
|
|
2881
|
+
this.before.value = this.records.length > 1 ? "1" : "0";
|
|
2882
|
+
}
|
|
2883
|
+
}
|
|
2884
|
+
update() {
|
|
2885
|
+
const hasVersion = this.records.length > 0;
|
|
2886
|
+
const pending = this.options.checkpoints?.hasPending ?? false;
|
|
2887
|
+
const stale = this.options.checkpoints?.needsRefresh ?? false;
|
|
2888
|
+
for (const key of ["latest", "preview", "download", "archive", "time", "activity"]) this.actions.get(key).disabled = !hasVersion;
|
|
2889
|
+
this.actions.get("more").hidden = !this.next;
|
|
2890
|
+
this.actions.get("compare").disabled = !hasVersion || this.before.value === this.versions.value;
|
|
2891
|
+
this.actions.get("save").hidden = !this.options.checkpoints || !this.options.capture;
|
|
2892
|
+
this.actions.get("save").disabled = pending || stale;
|
|
2893
|
+
this.actions.get("restore").hidden = !this.options.checkpoints;
|
|
2894
|
+
this.actions.get("restore").disabled = !hasVersion || pending || stale;
|
|
2895
|
+
this.actions.get("retry").hidden = !pending;
|
|
2896
|
+
this.detail.textContent = hasVersion ? [
|
|
2897
|
+
versionTitle(this.selected()),
|
|
2898
|
+
this.selected().record.metadata.message,
|
|
2899
|
+
this.selected().record.restoredFrom ? "Restored from an earlier version." : ""
|
|
2900
|
+
].filter(Boolean).join(" \u2014 ") : "";
|
|
2901
|
+
}
|
|
2902
|
+
selected() {
|
|
2903
|
+
return this.records[Number(this.versions.value)];
|
|
2904
|
+
}
|
|
2905
|
+
activityItem(op, resolved) {
|
|
2906
|
+
const doc = this.element.ownerDocument;
|
|
2907
|
+
const item = doc.createElement("li");
|
|
2908
|
+
const state = op.record.status === "accepted" ? "Accepted" : resolved ? "Conflict resolved" : "Conflict needs review";
|
|
2909
|
+
const label = doc.createElement("p");
|
|
2910
|
+
label.textContent = `${op.input.request.metadata.author} \xB7 ${state} \xB7 ${formatTime(op.input.request.metadata.createdAt)}`;
|
|
2911
|
+
item.append(label);
|
|
2912
|
+
const description = doc.createElement("p");
|
|
2913
|
+
this.commandButton("View decision", async () => {
|
|
2914
|
+
const decision = await this.options.reader.getOperation(op.id);
|
|
2915
|
+
const { kind, metadata } = decision.input.request;
|
|
2916
|
+
description.textContent = [
|
|
2917
|
+
{ text: "Text edit", package: "Document edit", discard: "Discarded proposal" }[kind],
|
|
2918
|
+
metadata.label,
|
|
2919
|
+
metadata.message,
|
|
2920
|
+
decision.record.conflict
|
|
2921
|
+
].filter(Boolean).join(" \u2014 ");
|
|
2922
|
+
}, item);
|
|
2923
|
+
item.append(description);
|
|
2924
|
+
this.commandButton("Download proposal", async () => {
|
|
2925
|
+
await this.download(await this.options.reader.exportOperationProposal(op.id), `proposal-${op.record.revision}.docx`);
|
|
2926
|
+
}, item);
|
|
2927
|
+
return item;
|
|
2928
|
+
}
|
|
2929
|
+
metadata() {
|
|
2930
|
+
return { author: this.author.value.trim() || "You", createdAt: (/* @__PURE__ */ new Date()).toISOString(), label: this.label.value.trim() || void 0 };
|
|
2931
|
+
}
|
|
2932
|
+
async preview(bytes, title) {
|
|
2933
|
+
if (!this.destroyed) await this.options.preview(bytes, title);
|
|
2934
|
+
}
|
|
2935
|
+
async download(bytes, suffix) {
|
|
2936
|
+
if (this.destroyed) return;
|
|
2937
|
+
const stem = (this.options.documentName ?? "document").replace(/\.(docx|docxhistory)$/i, "");
|
|
2938
|
+
const name = suffix === "docxhistory" ? `${stem}.docxhistory` : `${stem}-${suffix}`;
|
|
2939
|
+
if (this.options.download) {
|
|
2940
|
+
await this.options.download(bytes, name);
|
|
2941
|
+
return;
|
|
2942
|
+
}
|
|
2943
|
+
const url = URL.createObjectURL(new Blob([bytes.slice()], { type: suffix === "docxhistory" ? "application/octet-stream" : "application/vnd.openxmlformats-officedocument.wordprocessingml.document" }));
|
|
2944
|
+
const link = this.element.ownerDocument.createElement("a");
|
|
2945
|
+
link.href = url;
|
|
2946
|
+
link.download = name;
|
|
2947
|
+
link.click();
|
|
2948
|
+
setTimeout(() => URL.revokeObjectURL(url), 1e3);
|
|
2949
|
+
}
|
|
2950
|
+
async run(label, action) {
|
|
2951
|
+
if (this.destroyed) throw new DocxHistoryError("Closed", "History controls are closed.");
|
|
2952
|
+
if (this.active) throw new DocxHistoryError("Busy", "A history command is still running.");
|
|
2953
|
+
this.fieldset.disabled = true;
|
|
2954
|
+
this.element.setAttribute("aria-busy", "true");
|
|
2955
|
+
this.status.textContent = `${label}\u2026`;
|
|
2956
|
+
const work = Promise.resolve().then(action);
|
|
2957
|
+
this.active = work;
|
|
2958
|
+
try {
|
|
2959
|
+
await work;
|
|
2960
|
+
if (this.status.textContent === `${label}\u2026`) this.status.textContent = "Ready.";
|
|
2961
|
+
} catch (error) {
|
|
2962
|
+
if (!this.destroyed) this.status.textContent = historyControlError(error, this.options.checkpoints?.hasPending);
|
|
2963
|
+
throw error;
|
|
2964
|
+
} finally {
|
|
2965
|
+
this.active = null;
|
|
2966
|
+
if (!this.destroyed) {
|
|
2967
|
+
this.fieldset.disabled = false;
|
|
2968
|
+
this.element.setAttribute("aria-busy", "false");
|
|
2969
|
+
this.update();
|
|
2970
|
+
}
|
|
2971
|
+
}
|
|
2972
|
+
}
|
|
2973
|
+
button(key, title, action, parent = this.fieldset) {
|
|
2974
|
+
this.actions.set(key, this.commandButton(title, action, parent));
|
|
2975
|
+
}
|
|
2976
|
+
commandButton(title, action, parent) {
|
|
2977
|
+
const button = parent.ownerDocument.createElement("button");
|
|
2978
|
+
button.type = "button";
|
|
2979
|
+
button.textContent = title;
|
|
2980
|
+
button.addEventListener("click", () => {
|
|
2981
|
+
void this.run(title, action).catch(() => {
|
|
2982
|
+
});
|
|
2983
|
+
}, { signal: this.events.signal });
|
|
2984
|
+
parent.append(button);
|
|
2985
|
+
return button;
|
|
2986
|
+
}
|
|
2987
|
+
select(title) {
|
|
2988
|
+
const label = this.fieldset.ownerDocument.createElement("label");
|
|
2989
|
+
label.textContent = title;
|
|
2990
|
+
const select = label.ownerDocument.createElement("select");
|
|
2991
|
+
select.setAttribute("aria-label", title);
|
|
2992
|
+
label.append(select);
|
|
2993
|
+
this.fieldset.append(label);
|
|
2994
|
+
return select;
|
|
2995
|
+
}
|
|
2996
|
+
input(title, type, value = "", parent = this.fieldset) {
|
|
2997
|
+
const label = parent.ownerDocument.createElement("label");
|
|
2998
|
+
label.textContent = title;
|
|
2999
|
+
const input = label.ownerDocument.createElement("input");
|
|
3000
|
+
input.type = type;
|
|
3001
|
+
input.value = value;
|
|
3002
|
+
label.append(input);
|
|
3003
|
+
parent.append(label);
|
|
3004
|
+
return input;
|
|
3005
|
+
}
|
|
3006
|
+
disclosure(title) {
|
|
3007
|
+
const details = this.fieldset.ownerDocument.createElement("details");
|
|
3008
|
+
const summary = details.ownerDocument.createElement("summary");
|
|
3009
|
+
summary.textContent = title;
|
|
3010
|
+
details.append(summary);
|
|
3011
|
+
this.fieldset.append(details);
|
|
3012
|
+
return details;
|
|
3013
|
+
}
|
|
3014
|
+
};
|
|
3015
|
+
function historyControlError(error, pending = false) {
|
|
3016
|
+
const code = error instanceof DocxHistoryError ? error.code : "";
|
|
3017
|
+
if (code === "StaleHead") return "A newer checkpoint exists. Your draft is safe. Refresh history, review the newer version, then save again.";
|
|
3018
|
+
if (code === "ImportConflict") return "A different local history already exists. Open this file read-only to explore it.";
|
|
3019
|
+
if (code === "InitializationUnsupported") return "This storage cannot import history. Open read-only or choose storage that supports importing.";
|
|
3020
|
+
if (pending) return "The checkpoint could not be confirmed. Your draft is safe. Retry checkpoint to recover the original request.";
|
|
3021
|
+
if (code === "ResourceLimit") return "This history file exceeds browser processing limits, which can apply even below 64 MiB. Your document is unchanged.";
|
|
3022
|
+
if (code === "UnsupportedVersion") return "This history file uses an unsupported version. Open it with a newer app. Your document is unchanged.";
|
|
3023
|
+
if (code === "InvalidManifest") return "This history file is damaged or incomplete. Choose another copy. Your document is unchanged.";
|
|
3024
|
+
return `History could not be loaded. Your document is unchanged. ${error instanceof Error ? error.message : "Please try again."}`;
|
|
3025
|
+
}
|
|
3026
|
+
function versionTitle(version) {
|
|
3027
|
+
const { metadata, sequence } = version.record;
|
|
3028
|
+
return `${metadata.label || `Version ${BigInt(sequence) + 1n}`} \xB7 ${metadata.author} \xB7 ${formatTime(metadata.createdAt)}`;
|
|
3029
|
+
}
|
|
3030
|
+
function formatTime(value) {
|
|
3031
|
+
const time = new Date(value);
|
|
3032
|
+
return Number.isNaN(time.getTime()) ? value : time.toLocaleString();
|
|
3033
|
+
}
|
|
3034
|
+
var HISTORY_CSS = `
|
|
3035
|
+
.dx-history{font:14px/1.5 system-ui,sans-serif;color:#203047;background:#fff;border:1px solid #cbd5e1;border-radius:12px;padding:18px;min-width:0}
|
|
3036
|
+
.dx-history *{box-sizing:border-box}.dx-history h2{font-size:20px;margin:0 0 8px}.dx-history p{margin:8px 0;overflow-wrap:anywhere}
|
|
3037
|
+
.dx-history fieldset{border:0;padding:0;margin:0;min-width:0}.dx-history legend{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)}
|
|
3038
|
+
.dx-history label{display:block;font-weight:600;margin:12px 0 6px}.dx-history input,.dx-history select{display:block;width:100%;max-width:100%;margin-top:4px;font:inherit;color:inherit;border:1px solid #94a3b8;border-radius:6px;padding:8px;background:#fff}
|
|
3039
|
+
.dx-history button{min-height:40px;margin:4px 6px 4px 0;padding:7px 12px;border:1px solid #94a3b8;border-radius:6px;background:#f8fafc;color:inherit;font:inherit;cursor:pointer}
|
|
3040
|
+
.dx-history button:disabled{opacity:.5;cursor:wait}.dx-history :focus-visible{outline:3px solid #2563eb;outline-offset:2px}.dx-history [hidden]{display:none}
|
|
3041
|
+
.dx-history details{margin-top:16px;border-top:1px solid #e2e8f0;padding-top:12px}.dx-history summary{cursor:pointer;font-weight:600;padding:4px 0}
|
|
3042
|
+
.dx-history [role=status]{min-height:42px}.dx-history ol{padding-left:22px}.dx-history option{padding:6px}
|
|
3043
|
+
`;
|
|
3044
|
+
|
|
2164
3045
|
// src/page-number-format.ts
|
|
2165
3046
|
var ROMAN_ONES = ["", "i", "ii", "iii", "iv", "v", "vi", "vii", "viii", "ix"];
|
|
2166
3047
|
var ROMAN_TENS = ["", "x", "xx", "xxx", "xl", "l", "lx", "lxx", "lxxx", "xc"];
|
|
@@ -5610,11 +6491,11 @@ var Docxodus = (() => {
|
|
|
5610
6491
|
if (!this.partUriFor(which, "default")) this.seedStory(which, "default");
|
|
5611
6492
|
if (!this.partUriFor(which, kind)) this.seedStory(which, kind);
|
|
5612
6493
|
}
|
|
5613
|
-
const res =
|
|
6494
|
+
const res = parseResult2(this.bridge.EnsureHeaderFooterVisible(this.handle, this.bodyAnchorId, kind));
|
|
5614
6495
|
if (!res.success) return false;
|
|
5615
6496
|
} else {
|
|
5616
6497
|
if (!this.bridge.SetHeaderFooterKindEnabled) return false;
|
|
5617
|
-
const res =
|
|
6498
|
+
const res = parseResult2(this.bridge.SetHeaderFooterKindEnabled(this.handle, this.bodyAnchorId, kind, false));
|
|
5618
6499
|
if (!res.success) return false;
|
|
5619
6500
|
for (const which of ["header", "footer"]) {
|
|
5620
6501
|
if (this.kinds[which] === kind) this.kinds[which] = "default";
|
|
@@ -5639,7 +6520,7 @@ var Docxodus = (() => {
|
|
|
5639
6520
|
* Deliberately a PLAIN field (no `\*` switch), exactly as Word inserts one, so it follows the
|
|
5640
6521
|
* section's page-number format — see {@link setPageNumbering}. */
|
|
5641
6522
|
insertPageNumber(which, anchorId, field) {
|
|
5642
|
-
const res =
|
|
6523
|
+
const res = parseResult2(this.bridge.InsertPageNumberField(this.handle, anchorId, field, ""));
|
|
5643
6524
|
if (!res.success) return false;
|
|
5644
6525
|
this.refresh(which);
|
|
5645
6526
|
return true;
|
|
@@ -5662,7 +6543,7 @@ var Docxodus = (() => {
|
|
|
5662
6543
|
/** Set this section's page numbering (`w:pgNumType`) — Word's *Format Page Numbers…*. */
|
|
5663
6544
|
setPageNumbering(op) {
|
|
5664
6545
|
if (!this.bodyAnchorId) return false;
|
|
5665
|
-
const res =
|
|
6546
|
+
const res = parseResult2(this.bridge.SetPageNumbering(this.handle, this.bodyAnchorId, JSON.stringify(op)));
|
|
5666
6547
|
if (!res.success) return false;
|
|
5667
6548
|
this.reloadSection();
|
|
5668
6549
|
return true;
|
|
@@ -5674,7 +6555,7 @@ var Docxodus = (() => {
|
|
|
5674
6555
|
/** Remove this section's page-numbering start/format. */
|
|
5675
6556
|
clearPageNumbering() {
|
|
5676
6557
|
if (!this.bodyAnchorId) return false;
|
|
5677
|
-
const res =
|
|
6558
|
+
const res = parseResult2(this.bridge.ClearPageNumbering(this.handle, this.bodyAnchorId));
|
|
5678
6559
|
if (!res.success) return false;
|
|
5679
6560
|
this.reloadSection();
|
|
5680
6561
|
return true;
|
|
@@ -5985,7 +6866,7 @@ var Docxodus = (() => {
|
|
|
5985
6866
|
seedStory(which, kind) {
|
|
5986
6867
|
if (!this.bodyAnchorId) return;
|
|
5987
6868
|
const set = which === "header" ? this.bridge.SetHeaderText : this.bridge.SetFooterText;
|
|
5988
|
-
const res =
|
|
6869
|
+
const res = parseResult2(set(this.handle, this.bodyAnchorId, kind, ""));
|
|
5989
6870
|
if (!res.success) return;
|
|
5990
6871
|
this.callbacks.refreshAnchorMap();
|
|
5991
6872
|
this.sectionInfo = this.readSectionInfo(this.bodyAnchorId);
|
|
@@ -6141,7 +7022,7 @@ var Docxodus = (() => {
|
|
|
6141
7022
|
const range = d.caretRangeFromPoint?.(x, y);
|
|
6142
7023
|
return range ? { node: range.startContainer, offset: range.startOffset } : null;
|
|
6143
7024
|
}
|
|
6144
|
-
function
|
|
7025
|
+
function parseResult2(json) {
|
|
6145
7026
|
try {
|
|
6146
7027
|
return JSON.parse(json);
|
|
6147
7028
|
} catch {
|
|
@@ -8374,7 +9255,7 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
|
|
|
8374
9255
|
};
|
|
8375
9256
|
}
|
|
8376
9257
|
var honeyPotFix = makeHoneyPotFix();
|
|
8377
|
-
var
|
|
9258
|
+
var adapter2 = makeAdapter({
|
|
8378
9259
|
typeKey: "element",
|
|
8379
9260
|
defaultDropEffect: "move",
|
|
8380
9261
|
mount: function mount(api2) {
|
|
@@ -8463,8 +9344,8 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
|
|
|
8463
9344
|
},
|
|
8464
9345
|
onPostDispatch: honeyPotFix.getOnPostDispatch()
|
|
8465
9346
|
});
|
|
8466
|
-
var dropTargetForElements =
|
|
8467
|
-
var monitorForElements =
|
|
9347
|
+
var dropTargetForElements = adapter2.dropTarget;
|
|
9348
|
+
var monitorForElements = adapter2.monitor;
|
|
8468
9349
|
function draggable(args) {
|
|
8469
9350
|
if (true) {
|
|
8470
9351
|
if (args.dragHandle && !args.element.contains(args.dragHandle)) {
|
|
@@ -8488,7 +9369,7 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
|
|
|
8488
9369
|
// this is because you *must* have a draggable element to start a drag
|
|
8489
9370
|
// but you _might_ not have any drop targets immediately
|
|
8490
9371
|
// (You might create drop targets async)
|
|
8491
|
-
|
|
9372
|
+
adapter2.registerUsage(),
|
|
8492
9373
|
addToRegistry(args),
|
|
8493
9374
|
addAttribute(args.element, {
|
|
8494
9375
|
attribute: "draggable",
|
|
@@ -9935,22 +10816,55 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
|
|
|
9935
10816
|
const span = trimmedSpan(block, { start: Math.min(start2, end), length: Math.abs(end - start2) });
|
|
9936
10817
|
return span.length > 0 ? span : null;
|
|
9937
10818
|
}
|
|
9938
|
-
function
|
|
9939
|
-
|
|
9940
|
-
|
|
9941
|
-
|
|
9942
|
-
const range = document.createRange();
|
|
10819
|
+
function contentRangeIn(el, start2, length) {
|
|
10820
|
+
if (!el.isConnected) return null;
|
|
10821
|
+
const doc = el.ownerDocument;
|
|
10822
|
+
const range = doc.createRange();
|
|
9943
10823
|
const from = contentPositionIn(el, start2);
|
|
9944
10824
|
const to = contentPositionIn(el, start2 + length);
|
|
9945
10825
|
try {
|
|
9946
10826
|
range.setStart(from.node, from.offset);
|
|
9947
10827
|
range.setEnd(to.node, to.offset);
|
|
9948
10828
|
} catch {
|
|
9949
|
-
return;
|
|
10829
|
+
return null;
|
|
9950
10830
|
}
|
|
10831
|
+
return range;
|
|
10832
|
+
}
|
|
10833
|
+
function selectRange(el, start2, length) {
|
|
10834
|
+
const sel = typeof window !== "undefined" ? window.getSelection() : null;
|
|
10835
|
+
if (!sel || !el.isConnected) return;
|
|
10836
|
+
el.focus();
|
|
10837
|
+
const range = contentRangeIn(el, start2, length);
|
|
10838
|
+
if (!range) return;
|
|
9951
10839
|
sel.removeAllRanges();
|
|
9952
10840
|
sel.addRange(range);
|
|
9953
10841
|
}
|
|
10842
|
+
var FIND_HIGHLIGHT = "docxodus-find";
|
|
10843
|
+
var FIND_HIGHLIGHT_ACTIVE = "docxodus-find-active";
|
|
10844
|
+
var FIND_PAINT_LIMIT = 500;
|
|
10845
|
+
var findHighlightStyledDocuments = /* @__PURE__ */ new WeakSet();
|
|
10846
|
+
var findPaintOwner = null;
|
|
10847
|
+
function ensureFindHighlightStyles(doc) {
|
|
10848
|
+
if (findHighlightStyledDocuments.has(doc)) return;
|
|
10849
|
+
findHighlightStyledDocuments.add(doc);
|
|
10850
|
+
const style = doc.createElement("style");
|
|
10851
|
+
style.dataset.docxodusFindHighlight = "true";
|
|
10852
|
+
style.textContent = `
|
|
10853
|
+
::highlight(${FIND_HIGHLIGHT}) { background-color: rgba(15, 118, 110, .16); }
|
|
10854
|
+
::highlight(${FIND_HIGHLIGHT_ACTIVE}) { background-color: rgba(15, 118, 110, .38); }
|
|
10855
|
+
`;
|
|
10856
|
+
(doc.head ?? doc.documentElement).appendChild(style);
|
|
10857
|
+
}
|
|
10858
|
+
function highlightRegistry() {
|
|
10859
|
+
if (typeof CSS === "undefined") return null;
|
|
10860
|
+
const registry = CSS.highlights;
|
|
10861
|
+
const ctor = globalThis.Highlight;
|
|
10862
|
+
return registry && typeof ctor === "function" ? registry : null;
|
|
10863
|
+
}
|
|
10864
|
+
function makeHighlight(ranges) {
|
|
10865
|
+
const ctor = globalThis.Highlight;
|
|
10866
|
+
return new ctor(...ranges);
|
|
10867
|
+
}
|
|
9954
10868
|
function inBorderWrapper(el) {
|
|
9955
10869
|
const style = el.parentElement?.getAttribute("style") ?? "";
|
|
9956
10870
|
return /border-(top|bottom|left|right):\s*(?!none)[^;]+/i.test(style);
|
|
@@ -10321,6 +11235,7 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
|
|
|
10321
11235
|
document.removeEventListener("mouseup", this.onMouseUp, true);
|
|
10322
11236
|
}
|
|
10323
11237
|
this.clearDragSelection();
|
|
11238
|
+
this.clearFindMatches();
|
|
10324
11239
|
this.teardownBlockDrag();
|
|
10325
11240
|
this.gutter?.dispose();
|
|
10326
11241
|
this.gutter = null;
|
|
@@ -12719,15 +13634,65 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
|
|
|
12719
13634
|
}
|
|
12720
13635
|
return out;
|
|
12721
13636
|
}
|
|
12722
|
-
/**
|
|
13637
|
+
/**
|
|
13638
|
+
* Select a match, focus its block and scroll it into view — the caret lands on the hit, so
|
|
13639
|
+
* typing continues in the document. A find box that is still being typed into wants
|
|
13640
|
+
* `showFindMatches` instead; this is the commit step (closing the bar, jumping in to edit).
|
|
13641
|
+
*/
|
|
12723
13642
|
selectMatch(match) {
|
|
12724
13643
|
if (!match.block.isConnected) return;
|
|
13644
|
+
this.clearFindMatches();
|
|
12725
13645
|
this.activeBlock = match.block;
|
|
12726
13646
|
selectRange(match.block, match.start, match.length);
|
|
12727
13647
|
match.block.scrollIntoView({ block: "center", behavior: "smooth" });
|
|
12728
13648
|
}
|
|
12729
|
-
/**
|
|
12730
|
-
|
|
13649
|
+
/**
|
|
13650
|
+
* Paint `matches` and scroll the one at `activeIndex` into view WITHOUT moving focus or the
|
|
13651
|
+
* caret. This is what a find field calls on every keystroke: the document shows where the hits
|
|
13652
|
+
* are and rides to the current one, while the keyboard stays in the search box.
|
|
13653
|
+
*
|
|
13654
|
+
* Falls back to the document selection where the CSS Custom Highlight API is missing — still
|
|
13655
|
+
* without focusing the block, so the next character typed goes to the search field either way.
|
|
13656
|
+
*/
|
|
13657
|
+
showFindMatches(matches, activeIndex) {
|
|
13658
|
+
const active = matches[activeIndex];
|
|
13659
|
+
this.clearFindMatches();
|
|
13660
|
+
if (!active || !active.block.isConnected) return;
|
|
13661
|
+
this.activeBlock = active.block;
|
|
13662
|
+
const activeRange = contentRangeIn(active.block, active.start, active.length);
|
|
13663
|
+
const registry = highlightRegistry();
|
|
13664
|
+
if (registry && activeRange) {
|
|
13665
|
+
ensureFindHighlightStyles(this.container.ownerDocument);
|
|
13666
|
+
const rest = [];
|
|
13667
|
+
for (let i = 0; i < matches.length && rest.length < FIND_PAINT_LIMIT; i++) {
|
|
13668
|
+
if (i === activeIndex) continue;
|
|
13669
|
+
const range = contentRangeIn(matches[i].block, matches[i].start, matches[i].length);
|
|
13670
|
+
if (range) rest.push(range);
|
|
13671
|
+
}
|
|
13672
|
+
if (rest.length > 0) registry.set(FIND_HIGHLIGHT, makeHighlight(rest));
|
|
13673
|
+
registry.set(FIND_HIGHLIGHT_ACTIVE, makeHighlight([activeRange]));
|
|
13674
|
+
findPaintOwner = this;
|
|
13675
|
+
} else if (activeRange) {
|
|
13676
|
+
const sel = this.container.ownerDocument.defaultView?.getSelection();
|
|
13677
|
+
sel?.removeAllRanges();
|
|
13678
|
+
sel?.addRange(activeRange);
|
|
13679
|
+
}
|
|
13680
|
+
active.block.scrollIntoView({ block: "center", behavior: "smooth" });
|
|
13681
|
+
}
|
|
13682
|
+
/** Drop the find painting (closing the find bar, or committing a match to the caret). */
|
|
13683
|
+
clearFindMatches() {
|
|
13684
|
+
if (findPaintOwner !== this) return;
|
|
13685
|
+
const registry = highlightRegistry();
|
|
13686
|
+
registry?.delete(FIND_HIGHLIGHT);
|
|
13687
|
+
registry?.delete(FIND_HIGHLIGHT_ACTIVE);
|
|
13688
|
+
findPaintOwner = null;
|
|
13689
|
+
}
|
|
13690
|
+
/**
|
|
13691
|
+
* Replace one match's text (formatting of the surrounding run is inherited). `focus: false`
|
|
13692
|
+
* leaves the keyboard where it is — a Replace button pressed from the find bar must not drag
|
|
13693
|
+
* the caret into the document mid-search.
|
|
13694
|
+
*/
|
|
13695
|
+
replaceMatch(match, replacement, options = {}) {
|
|
12731
13696
|
const block = match.block;
|
|
12732
13697
|
if (this.closed || !block.isConnected) return false;
|
|
12733
13698
|
const unid = block.getAttribute("data-anchor");
|
|
@@ -12748,7 +13713,7 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
|
|
|
12748
13713
|
);
|
|
12749
13714
|
if (!res.success) return false;
|
|
12750
13715
|
const fresh = this.swapBlock(block, unid, res.modified?.[0]);
|
|
12751
|
-
if (fresh) selectRange(fresh, span.start, replacement.length);
|
|
13716
|
+
if (fresh && options.focus !== false) selectRange(fresh, span.start, replacement.length);
|
|
12752
13717
|
return true;
|
|
12753
13718
|
}
|
|
12754
13719
|
/** Replace every occurrence; returns how many were replaced. */
|
|
@@ -12767,7 +13732,11 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
|
|
|
12767
13732
|
for (const m of list.slice().sort((a, b) => b.start - a.start)) {
|
|
12768
13733
|
if (!current.isConnected) break;
|
|
12769
13734
|
const before = current;
|
|
12770
|
-
if (this.replaceMatch(
|
|
13735
|
+
if (this.replaceMatch(
|
|
13736
|
+
{ block: current, start: m.start, length: m.length },
|
|
13737
|
+
replacement,
|
|
13738
|
+
{ focus: options.focus }
|
|
13739
|
+
)) {
|
|
12771
13740
|
count++;
|
|
12772
13741
|
current = this.activeBlock && this.activeBlock !== before ? this.activeBlock : current;
|
|
12773
13742
|
}
|
|
@@ -13449,7 +14418,7 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
|
|
|
13449
14418
|
}
|
|
13450
14419
|
|
|
13451
14420
|
// src/ribbon-chrome.ts
|
|
13452
|
-
var RIBBON_STYLE_VERSION = "
|
|
14421
|
+
var RIBBON_STYLE_VERSION = "11";
|
|
13453
14422
|
var RIBBON_STYLE_ATTR = "data-docxodus-ribbon-styles";
|
|
13454
14423
|
var COMMENT_GUTTER_WIDTH = 264;
|
|
13455
14424
|
var RIBBON_CSS = `
|
|
@@ -13882,9 +14851,11 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
|
|
|
13882
14851
|
background: linear-gradient(to top, var(--dxr-desk), transparent);
|
|
13883
14852
|
}
|
|
13884
14853
|
.dxr[data-chrome] .dxr-surface { position: relative; margin: 26px auto; padding: 0 16px 96px; }
|
|
13885
|
-
/*
|
|
13886
|
-
|
|
13887
|
-
.dxr[data-chrome="full"] .dxr-surface[data-comments="on"]
|
|
14854
|
+
/* Reserve the markup area only while a visible comment or draft needs it. The gutter owns
|
|
14855
|
+
its empty/hidden state, so ordinary documents stay centered without disabling comments. */
|
|
14856
|
+
.dxr[data-chrome="full"] .dxr-surface[data-comments="on"]:has(> .docx-comment-gutter:not([data-empty]):not([hidden])) {
|
|
14857
|
+
padding-right: calc(var(--dxr-gutter) + 8px);
|
|
14858
|
+
}
|
|
13888
14859
|
.dxr-surface [contenteditable="true"]:focus {
|
|
13889
14860
|
outline: 2px solid var(--dxr-accent);
|
|
13890
14861
|
outline-offset: 2px;
|
|
@@ -15400,7 +16371,8 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
|
|
|
15400
16371
|
comments: this.options.comments,
|
|
15401
16372
|
commentAuthor: this.author
|
|
15402
16373
|
});
|
|
15403
|
-
this.
|
|
16374
|
+
const saveButton = this.control("save");
|
|
16375
|
+
if (saveButton) saveButton.disabled = false;
|
|
15404
16376
|
this.require("ribbon").setAttribute("aria-disabled", "false");
|
|
15405
16377
|
this.setState("ready");
|
|
15406
16378
|
this.setStatus(`Rendered in ${Math.round(performance.now() - started)} ms`);
|
|
@@ -16035,11 +17007,18 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
|
|
|
16035
17007
|
if (!this.live || !text.value) return;
|
|
16036
17008
|
let count = 0;
|
|
16037
17009
|
this.run("replace all", () => {
|
|
16038
|
-
count = this.live.replaceAll(text.value, replace.value, {
|
|
17010
|
+
count = this.live.replaceAll(text.value, replace.value, {
|
|
17011
|
+
matchCase: matchCase.checked,
|
|
17012
|
+
focus: false
|
|
17013
|
+
});
|
|
16039
17014
|
});
|
|
16040
17015
|
this.setStatus(`Replaced ${count} occurrence${count === 1 ? "" : "s"}`);
|
|
16041
17016
|
this.refreshFind(true);
|
|
16042
17017
|
});
|
|
17018
|
+
for (const id of ["findprev", "findnext", "replaceone", "replaceall"]) {
|
|
17019
|
+
const button = this.control(id);
|
|
17020
|
+
if (button) this.keepSelection(button);
|
|
17021
|
+
}
|
|
16043
17022
|
bar.hidden = true;
|
|
16044
17023
|
}
|
|
16045
17024
|
openFindBar(withReplace) {
|
|
@@ -16050,28 +17029,44 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
|
|
|
16050
17029
|
const text = this.require("findtext");
|
|
16051
17030
|
text.focus();
|
|
16052
17031
|
text.select();
|
|
16053
|
-
this.refreshFind(
|
|
17032
|
+
this.refreshFind(true);
|
|
16054
17033
|
}
|
|
17034
|
+
/**
|
|
17035
|
+
* Close the bar and hand the current hit over to the caret: the document is where the user is
|
|
17036
|
+
* going next, and Word leaves the insertion point on the match it stopped at. Until this runs
|
|
17037
|
+
* the match is only PAINTED (see `showFindMatches`), so the search field keeps the keyboard for
|
|
17038
|
+
* as long as the bar is open.
|
|
17039
|
+
*/
|
|
16055
17040
|
closeFindBar() {
|
|
16056
17041
|
const bar = this.control("findbar");
|
|
16057
17042
|
if (bar) bar.hidden = true;
|
|
17043
|
+
const match = this.findMatches[this.findIndex];
|
|
17044
|
+
if (match) this.live?.selectMatch(match);
|
|
17045
|
+
else this.live?.clearFindMatches();
|
|
16058
17046
|
this.findMatches = [];
|
|
16059
17047
|
this.findIndex = -1;
|
|
16060
17048
|
}
|
|
16061
|
-
|
|
17049
|
+
/**
|
|
17050
|
+
* Re-scan for the current query. `show` paints the hits and rides to the first one — it never
|
|
17051
|
+
* focuses the document, so typing the second character of a query that already matched goes on
|
|
17052
|
+
* refining the search instead of being typed into the document.
|
|
17053
|
+
*/
|
|
17054
|
+
refreshFind(show) {
|
|
16062
17055
|
const text = this.require("findtext");
|
|
16063
17056
|
const matchCase = this.require("findcase").checked;
|
|
16064
17057
|
this.findMatches = this.live && text.value ? this.live.find(text.value, { matchCase }) : [];
|
|
16065
17058
|
this.findIndex = this.findMatches.length > 0 ? 0 : -1;
|
|
16066
17059
|
this.updateFindCount();
|
|
16067
|
-
if (
|
|
17060
|
+
if (!show) return;
|
|
17061
|
+
if (this.findIndex >= 0) this.live?.showFindMatches(this.findMatches, this.findIndex);
|
|
17062
|
+
else this.live?.clearFindMatches();
|
|
16068
17063
|
}
|
|
16069
17064
|
stepFind(direction) {
|
|
16070
17065
|
if (!this.live) return;
|
|
16071
17066
|
if (this.findMatches.length === 0) this.refreshFind(false);
|
|
16072
17067
|
if (this.findMatches.length === 0) return;
|
|
16073
17068
|
this.findIndex = (this.findIndex + direction + this.findMatches.length) % this.findMatches.length;
|
|
16074
|
-
this.live.
|
|
17069
|
+
this.live.showFindMatches(this.findMatches, this.findIndex);
|
|
16075
17070
|
this.updateFindCount();
|
|
16076
17071
|
}
|
|
16077
17072
|
replaceCurrent() {
|
|
@@ -16080,12 +17075,12 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
|
|
|
16080
17075
|
const match = this.findMatches[this.findIndex];
|
|
16081
17076
|
if (!match) return;
|
|
16082
17077
|
const replacement = this.require("replacetext").value;
|
|
16083
|
-
this.run("replace", () => this.live.replaceMatch(match, replacement));
|
|
17078
|
+
this.run("replace", () => this.live.replaceMatch(match, replacement, { focus: false }));
|
|
16084
17079
|
const keep = this.findIndex;
|
|
16085
17080
|
this.refreshFind(false);
|
|
16086
17081
|
if (this.findMatches.length > 0) {
|
|
16087
17082
|
this.findIndex = Math.min(keep, this.findMatches.length - 1);
|
|
16088
|
-
this.live.
|
|
17083
|
+
this.live.showFindMatches(this.findMatches, this.findIndex);
|
|
16089
17084
|
this.updateFindCount();
|
|
16090
17085
|
}
|
|
16091
17086
|
}
|
|
@@ -16517,6 +17512,16 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
|
|
|
16517
17512
|
|
|
16518
17513
|
// src/index.ts
|
|
16519
17514
|
var import_meta = {};
|
|
17515
|
+
function openDocxHistory(storage) {
|
|
17516
|
+
const bridge = ensureInitialized().HistoryBridge;
|
|
17517
|
+
if (!bridge) throw new Error("This WASM build does not include history bindings.");
|
|
17518
|
+
return new DocxHistoryClient(bridge, storage);
|
|
17519
|
+
}
|
|
17520
|
+
function openDocxHistoryArchive(bytes) {
|
|
17521
|
+
const bridge = ensureInitialized().HistoryBridge;
|
|
17522
|
+
if (!bridge) throw new Error("This WASM build does not include history bindings.");
|
|
17523
|
+
return DocxHistoryArchive.open(bridge, bytes);
|
|
17524
|
+
}
|
|
16520
17525
|
function openDocxSession2(bytes, settings) {
|
|
16521
17526
|
const wasm = ensureInitialized();
|
|
16522
17527
|
return openDocxSession(bytes, wasm, settings);
|
|
@@ -16571,14 +17576,16 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
|
|
|
16571
17576
|
/* @vite-ignore */
|
|
16572
17577
|
dotnetPath
|
|
16573
17578
|
);
|
|
16574
|
-
const { getAssemblyExports, getConfig } = await dotnet.withDiagnosticTracing(false).create();
|
|
17579
|
+
const { getAssemblyExports, getConfig, setModuleImports } = await dotnet.withDiagnosticTracing(false).create();
|
|
17580
|
+
installHistoryStorageImports(setModuleImports);
|
|
16575
17581
|
const config = getConfig();
|
|
16576
17582
|
const exports = await getAssemblyExports(config.mainAssemblyName);
|
|
16577
17583
|
wasmExports = {
|
|
16578
17584
|
DocumentConverter: exports.DocxodusWasm.DocumentConverter,
|
|
16579
17585
|
DocumentComparer: exports.DocxodusWasm.DocumentComparer,
|
|
16580
17586
|
DocxDiffBridge: exports.DocxodusWasm.DocxDiffBridge,
|
|
16581
|
-
DocxSessionBridge: exports.DocxodusWasm.DocxSessionBridge
|
|
17587
|
+
DocxSessionBridge: exports.DocxodusWasm.DocxSessionBridge,
|
|
17588
|
+
HistoryBridge: exports.DocxodusWasm.HistoryBridge
|
|
16582
17589
|
};
|
|
16583
17590
|
return true;
|
|
16584
17591
|
} catch {
|
|
@@ -17953,6 +18960,20 @@ ${parsed.documentElement.outerHTML}`;
|
|
|
17953
18960
|
}
|
|
17954
18961
|
throw new Error("Docxodus embed: unsupported document source");
|
|
17955
18962
|
}
|
|
18963
|
+
function scopedDocumentFragment(mount2, html) {
|
|
18964
|
+
const parsed = new DOMParser().parseFromString(html, "text/html");
|
|
18965
|
+
const fragment = document.createDocumentFragment();
|
|
18966
|
+
parsed.querySelectorAll("style").forEach((sourceStyle) => {
|
|
18967
|
+
const style = document.createElement("style");
|
|
18968
|
+
style.textContent = sourceStyle.textContent;
|
|
18969
|
+
scopeStyleElement(style, mount2.selector);
|
|
18970
|
+
fragment.appendChild(style);
|
|
18971
|
+
});
|
|
18972
|
+
const body = document.createElement("template");
|
|
18973
|
+
body.innerHTML = parsed.body.innerHTML;
|
|
18974
|
+
fragment.appendChild(body.content);
|
|
18975
|
+
return fragment;
|
|
18976
|
+
}
|
|
17956
18977
|
async function createViewer(container, source, options = {}) {
|
|
17957
18978
|
const el = resolveContainer2(container);
|
|
17958
18979
|
const { wasmBasePath: wasmBasePath2, ...conversion } = options;
|
|
@@ -17965,18 +18986,7 @@ ${parsed.documentElement.outerHTML}`;
|
|
|
17965
18986
|
renderFootnotesAndEndnotes: true,
|
|
17966
18987
|
...opts
|
|
17967
18988
|
});
|
|
17968
|
-
|
|
17969
|
-
const fragment = document.createDocumentFragment();
|
|
17970
|
-
parsed.querySelectorAll("style").forEach((sourceStyle) => {
|
|
17971
|
-
const style = document.createElement("style");
|
|
17972
|
-
style.textContent = sourceStyle.textContent;
|
|
17973
|
-
scopeStyleElement(style, mount2.selector);
|
|
17974
|
-
fragment.appendChild(style);
|
|
17975
|
-
});
|
|
17976
|
-
const body = document.createElement("template");
|
|
17977
|
-
body.innerHTML = parsed.body.innerHTML;
|
|
17978
|
-
fragment.appendChild(body.content);
|
|
17979
|
-
mount2.root.replaceChildren(fragment);
|
|
18989
|
+
mount2.root.replaceChildren(scopedDocumentFragment(mount2, lastHtml));
|
|
17980
18990
|
};
|
|
17981
18991
|
await render(source, conversion);
|
|
17982
18992
|
return {
|
|
@@ -17990,6 +19000,111 @@ ${parsed.documentElement.outerHTML}`;
|
|
|
17990
19000
|
}
|
|
17991
19001
|
};
|
|
17992
19002
|
}
|
|
19003
|
+
async function createHistoryViewer(container, history, documentId, options = {}) {
|
|
19004
|
+
const el = resolveContainer2(container);
|
|
19005
|
+
const { wasmBasePath: wasmBasePath2, maxEntriesToScan = 1e4, ...conversion } = options;
|
|
19006
|
+
if (!Number.isSafeInteger(maxEntriesToScan) || maxEntriesToScan <= 0 || maxEntriesToScan > 2147483647)
|
|
19007
|
+
throw new RangeError("Invalid history scan budget.");
|
|
19008
|
+
await ensureWasm(wasmBasePath2);
|
|
19009
|
+
const mount2 = createScopedMount(el);
|
|
19010
|
+
let accepted = null;
|
|
19011
|
+
let acceptedBytes = null;
|
|
19012
|
+
let displayedBytes = null;
|
|
19013
|
+
let shownSequence = "0";
|
|
19014
|
+
let following = true;
|
|
19015
|
+
let closed = false;
|
|
19016
|
+
let html = "";
|
|
19017
|
+
let queue = Promise.resolve();
|
|
19018
|
+
const clone = (value) => JSON.parse(JSON.stringify(value));
|
|
19019
|
+
const assertOpen = () => {
|
|
19020
|
+
if (closed) throw new DocxHistoryError("Closed", "History viewer is destroyed.");
|
|
19021
|
+
};
|
|
19022
|
+
const enqueue = (action) => {
|
|
19023
|
+
const result = queue.then(() => {
|
|
19024
|
+
assertOpen();
|
|
19025
|
+
return action();
|
|
19026
|
+
});
|
|
19027
|
+
queue = result.then(() => void 0, () => void 0);
|
|
19028
|
+
return result;
|
|
19029
|
+
};
|
|
19030
|
+
const sameHead = (a, b) => a.revision === b.revision && a.state.length === b.state.length && a.state.digest.algorithm === b.state.digest.algorithm && a.state.digest.value === b.state.digest.value;
|
|
19031
|
+
const paint = async (bytes, sequence) => {
|
|
19032
|
+
const nextHtml = await convertDocxToHtml(bytes, { renderFootnotesAndEndnotes: true, ...conversion });
|
|
19033
|
+
assertOpen();
|
|
19034
|
+
const fragment = scopedDocumentFragment(mount2, nextHtml);
|
|
19035
|
+
mount2.root.replaceChildren(fragment);
|
|
19036
|
+
html = nextHtml;
|
|
19037
|
+
displayedBytes = bytes;
|
|
19038
|
+
shownSequence = sequence;
|
|
19039
|
+
};
|
|
19040
|
+
const refresh = async (resume = false) => {
|
|
19041
|
+
const update = await history.readChangesSince(documentId, accepted?.view.head ?? null, maxEntriesToScan);
|
|
19042
|
+
assertOpen();
|
|
19043
|
+
const duplicate = accepted !== null && sameHead(accepted.view.head, update.view.head);
|
|
19044
|
+
const bytes = duplicate ? acceptedBytes : await history.exportVersion(documentId, update.view.version.id);
|
|
19045
|
+
assertOpen();
|
|
19046
|
+
const wantLive = following || resume;
|
|
19047
|
+
const changedContent = accepted === null || accepted.view.state.snapshot.contentDigest.value !== update.view.state.snapshot.contentDigest.value || accepted.view.state.epoch !== update.view.state.epoch;
|
|
19048
|
+
if (wantLive && (changedContent || !following)) await paint(bytes, update.view.state.sequence);
|
|
19049
|
+
else if (wantLive) {
|
|
19050
|
+
displayedBytes = bytes;
|
|
19051
|
+
shownSequence = update.view.state.sequence;
|
|
19052
|
+
}
|
|
19053
|
+
assertOpen();
|
|
19054
|
+
accepted = update;
|
|
19055
|
+
acceptedBytes = bytes;
|
|
19056
|
+
if (wantLive) following = true;
|
|
19057
|
+
return clone(update);
|
|
19058
|
+
};
|
|
19059
|
+
const showSequence = async (sequence) => {
|
|
19060
|
+
const bytes = await history.materialize(documentId, sequence, maxEntriesToScan);
|
|
19061
|
+
assertOpen();
|
|
19062
|
+
await paint(bytes, sequence);
|
|
19063
|
+
following = false;
|
|
19064
|
+
};
|
|
19065
|
+
const viewer = {
|
|
19066
|
+
element: el,
|
|
19067
|
+
get html() {
|
|
19068
|
+
return html;
|
|
19069
|
+
},
|
|
19070
|
+
get head() {
|
|
19071
|
+
assertOpen();
|
|
19072
|
+
return clone(accepted.view.head);
|
|
19073
|
+
},
|
|
19074
|
+
get sequence() {
|
|
19075
|
+
return shownSequence;
|
|
19076
|
+
},
|
|
19077
|
+
get following() {
|
|
19078
|
+
return following;
|
|
19079
|
+
},
|
|
19080
|
+
refresh: () => enqueue(() => refresh()),
|
|
19081
|
+
showSequence: (sequence) => enqueue(() => showSequence(sequence)),
|
|
19082
|
+
showTime: (cutoff) => enqueue(async () => {
|
|
19083
|
+
const sequence = await history.resolveSequenceAtTime(documentId, cutoff, maxEntriesToScan);
|
|
19084
|
+
assertOpen();
|
|
19085
|
+
await showSequence(sequence);
|
|
19086
|
+
}),
|
|
19087
|
+
resume: () => enqueue(() => refresh(true)),
|
|
19088
|
+
exportDisplayed: () => {
|
|
19089
|
+
assertOpen();
|
|
19090
|
+
return displayedBytes.slice();
|
|
19091
|
+
},
|
|
19092
|
+
destroy: () => {
|
|
19093
|
+
if (closed) return;
|
|
19094
|
+
closed = true;
|
|
19095
|
+
acceptedBytes = null;
|
|
19096
|
+
displayedBytes = null;
|
|
19097
|
+
mount2.root.remove();
|
|
19098
|
+
}
|
|
19099
|
+
};
|
|
19100
|
+
try {
|
|
19101
|
+
await viewer.refresh();
|
|
19102
|
+
return viewer;
|
|
19103
|
+
} catch (error) {
|
|
19104
|
+
viewer.destroy();
|
|
19105
|
+
throw error;
|
|
19106
|
+
}
|
|
19107
|
+
}
|
|
17993
19108
|
async function createEditor(container, source, options = {}) {
|
|
17994
19109
|
const el = resolveContainer2(container);
|
|
17995
19110
|
const { wasmBasePath: wasmBasePath2, ...editorOptions } = options;
|