@embedpdf/plugin-selection 2.14.4 → 2.15.1

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/dist/index.js CHANGED
@@ -809,6 +809,7 @@ const _SelectionPlugin = class _SelectionPlugin extends BasePlugin {
809
809
  getBoundingRectForPage: (p, docId) => selectBoundingRectForPage(this.getDocumentState(getDocId(docId)), p),
810
810
  getBoundingRects: (docId) => selectBoundingRectsForAllPages(this.getDocumentState(getDocId(docId))),
811
811
  getSelectedText: (docId) => this.getSelectedText(getDocId(docId)),
812
+ setSelection: (range, docId) => this.applySelection(getDocId(docId), range),
812
813
  clear: (docId) => this.clearSelection(getDocId(docId)),
813
814
  copyToClipboard: (docId) => this.copyToClipboard(getDocId(docId)),
814
815
  getState: (docId) => this.getDocumentState(getDocId(docId)),
@@ -846,6 +847,7 @@ const _SelectionPlugin = class _SelectionPlugin extends BasePlugin {
846
847
  getBoundingRectForPage: (p) => selectBoundingRectForPage(this.getDocumentState(documentId), p),
847
848
  getBoundingRects: () => selectBoundingRectsForAllPages(this.getDocumentState(documentId)),
848
849
  getSelectedText: () => this.getSelectedText(documentId),
850
+ setSelection: (range) => this.applySelection(documentId, range),
849
851
  clear: () => this.clearSelection(documentId),
850
852
  copyToClipboard: () => this.copyToClipboard(documentId),
851
853
  getState: () => this.getDocumentState(documentId),
@@ -1213,6 +1215,137 @@ const _SelectionPlugin = class _SelectionPlugin extends BasePlugin {
1213
1215
  this.emitMenuPlacement(documentId, null);
1214
1216
  this.notifyAllPages(documentId);
1215
1217
  }
1218
+ /**
1219
+ * Programmatically apply (or restore) a text selection without going through
1220
+ * the pointer drag flow. Useful for restoring a previously persisted selection
1221
+ * or driving selection from code (e.g. search results).
1222
+ *
1223
+ * Geometry for every page spanned by the range is loaded on demand (the data
1224
+ * comes from the WASM engine), so the returned task settles only once the
1225
+ * selection rects have actually been computed.
1226
+ *
1227
+ * Passing `null` is equivalent to calling {@link clearSelection}.
1228
+ *
1229
+ * @returns a task that resolves once the selection has been applied.
1230
+ */
1231
+ applySelection(documentId, range) {
1232
+ if (!this.state.documents[documentId]) {
1233
+ return PdfTaskHelper.reject({
1234
+ code: PdfErrorCode.NotFound,
1235
+ message: `Selection state not found for document: ${documentId}`
1236
+ });
1237
+ }
1238
+ if (range === null) {
1239
+ this.clearSelection(documentId);
1240
+ return PdfTaskHelper.resolve(void 0);
1241
+ }
1242
+ const validationError = this.validateRange(documentId, range);
1243
+ if (validationError) {
1244
+ return PdfTaskHelper.reject(validationError);
1245
+ }
1246
+ const normalized = this.normalizeRange(range);
1247
+ const pages = [];
1248
+ for (let p = normalized.start.page; p <= normalized.end.page; p++) {
1249
+ pages.push(p);
1250
+ }
1251
+ const geoTasks = pages.map((p) => this.getOrLoadGeometry(documentId, p));
1252
+ const result = PdfTaskHelper.create();
1253
+ Task.all(geoTasks).wait(() => {
1254
+ if (!this.state.documents[documentId]) {
1255
+ result.reject({
1256
+ code: PdfErrorCode.NotFound,
1257
+ message: `Selection state not found for document: ${documentId}`
1258
+ });
1259
+ return;
1260
+ }
1261
+ const clamped = this.clampRangeToGeometry(documentId, normalized);
1262
+ if (!clamped) {
1263
+ result.reject({
1264
+ code: PdfErrorCode.NotFound,
1265
+ message: `Cannot apply selection: page(s) ${normalized.start.page}-${normalized.end.page} have no text geometry`
1266
+ });
1267
+ return;
1268
+ }
1269
+ this.selecting.set(documentId, false);
1270
+ this.anchor.set(documentId, void 0);
1271
+ const previouslyHighlighted = Object.keys(this.getDocumentState(documentId).rects).map(
1272
+ Number
1273
+ );
1274
+ this.dispatch(startSelection(documentId));
1275
+ this.dispatch(setSelection(documentId, clamped));
1276
+ this.updateRectsAndSlices(documentId, clamped);
1277
+ this.dispatch(endSelection(documentId));
1278
+ this.selChange$.emit(documentId, clamped);
1279
+ const pagesToNotify = new Set(previouslyHighlighted);
1280
+ for (let p = clamped.start.page; p <= clamped.end.page; p++) {
1281
+ pagesToNotify.add(p);
1282
+ }
1283
+ pagesToNotify.forEach((p) => this.notifyPage(documentId, p));
1284
+ this.recalculateMenuPlacement(documentId);
1285
+ result.resolve(void 0);
1286
+ }, (error) => result.reject(error.reason));
1287
+ return result;
1288
+ }
1289
+ /**
1290
+ * Validate the structure and page bounds of a programmatic selection range.
1291
+ * Returns a rejection reason when the input is invalid, or null when it is
1292
+ * structurally sound. Glyph-index bounds are enforced separately, after the
1293
+ * page geometry has loaded (see {@link clampRangeToGeometry}).
1294
+ */
1295
+ validateRange(documentId, range) {
1296
+ var _a, _b;
1297
+ const isValidPointer = (g) => !!g && typeof g === "object" && Number.isInteger(g.page) && Number.isInteger(g.index);
1298
+ if (!range || typeof range !== "object" || !range.start || !range.end) {
1299
+ return { code: PdfErrorCode.Unknown, message: "Invalid selection range: expected { start, end }" };
1300
+ }
1301
+ if (!isValidPointer(range.start) || !isValidPointer(range.end)) {
1302
+ return {
1303
+ code: PdfErrorCode.Unknown,
1304
+ message: "Invalid selection range: start/end must have integer page and index"
1305
+ };
1306
+ }
1307
+ if (range.start.index < 0 || range.end.index < 0) {
1308
+ return { code: PdfErrorCode.Unknown, message: "Invalid selection range: glyph index cannot be negative" };
1309
+ }
1310
+ const pageCount = ((_b = (_a = this.getCoreDocument(documentId)) == null ? void 0 : _a.document) == null ? void 0 : _b.pageCount) ?? 0;
1311
+ const minPage = Math.min(range.start.page, range.end.page);
1312
+ const maxPage = Math.max(range.start.page, range.end.page);
1313
+ if (minPage < 0 || maxPage >= pageCount) {
1314
+ return {
1315
+ code: PdfErrorCode.NotFound,
1316
+ message: `Invalid selection range: page ${minPage < 0 ? minPage : maxPage} out of bounds [0, ${pageCount})`
1317
+ };
1318
+ }
1319
+ return null;
1320
+ }
1321
+ /**
1322
+ * Clamp the glyph indices of a (normalized) range to the number of glyphs
1323
+ * available on the corresponding pages. Returns null when none of the
1324
+ * spanned pages have any text geometry to select.
1325
+ */
1326
+ clampRangeToGeometry(documentId, range) {
1327
+ const docState = this.getDocumentState(documentId);
1328
+ const lastGlyphIndex = (page) => {
1329
+ const geo = docState.geometry[page];
1330
+ if (!geo || geo.runs.length === 0) return -1;
1331
+ const lastRun = geo.runs[geo.runs.length - 1];
1332
+ return lastRun.charStart + lastRun.glyphs.length - 1;
1333
+ };
1334
+ const startMax = lastGlyphIndex(range.start.page);
1335
+ const endMax = lastGlyphIndex(range.end.page);
1336
+ if (startMax < 0 && endMax < 0) return null;
1337
+ const clampIndex = (index, max) => max < 0 ? 0 : Math.min(Math.max(index, 0), max);
1338
+ return {
1339
+ start: { page: range.start.page, index: clampIndex(range.start.index, startMax) },
1340
+ end: { page: range.end.page, index: clampIndex(range.end.index, endMax) }
1341
+ };
1342
+ }
1343
+ /** Order a range so that `start` never comes after `end`. */
1344
+ normalizeRange(range) {
1345
+ const { start, end } = range;
1346
+ const forward = end.page > start.page || end.page === start.page && end.index >= start.index;
1347
+ return forward ? { start, end } : { start: end, end: start };
1348
+ }
1216
1349
  selectWord(documentId, page, charIndex, modeId) {
1217
1350
  const geo = this.getDocumentState(documentId).geometry[page];
1218
1351
  if (!geo) return;