stimulus-pdf-viewer-rails 0.1.0 → 0.2.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +6 -1
- data/app/assets/javascripts/stimulus-pdf-viewer.esm.js +546 -194
- data/app/assets/javascripts/stimulus-pdf-viewer.js +548 -194
- data/app/assets/stylesheets/stimulus-pdf-viewer.scss +24 -235
- data/lib/stimulus_pdf_viewer/rails/engine.rb +0 -1
- data/lib/stimulus_pdf_viewer/rails/version.rb +2 -2
- data/lib/tasks/assets.rake +0 -22
- metadata +2 -6
- data/app/assets/images/stimulus-pdf-viewer/cursor-editorFreeHighlight.svg +0 -6
- data/app/assets/images/stimulus-pdf-viewer/cursor-editorInk.svg +0 -4
- data/app/assets/images/stimulus-pdf-viewer/cursor-editorNote.svg +0 -8
- data/app/assets/images/stimulus-pdf-viewer/cursor-editorTextHighlight.svg +0 -8
|
@@ -1250,6 +1250,264 @@ class CoreViewer {
|
|
|
1250
1250
|
}
|
|
1251
1251
|
}
|
|
1252
1252
|
|
|
1253
|
+
/**
|
|
1254
|
+
* Base class for annotation storage implementations.
|
|
1255
|
+
*
|
|
1256
|
+
* Subclasses must implement all methods to provide persistence for annotations.
|
|
1257
|
+
* The AnnotationManager delegates all storage operations to a store instance.
|
|
1258
|
+
*
|
|
1259
|
+
* @example
|
|
1260
|
+
* class MyCustomStore extends AnnotationStore {
|
|
1261
|
+
* async load() { return fetch('/my-api/annotations').then(r => r.json()) }
|
|
1262
|
+
* async create(data) { ... }
|
|
1263
|
+
* // ... etc
|
|
1264
|
+
* }
|
|
1265
|
+
*/
|
|
1266
|
+
class AnnotationStore {
|
|
1267
|
+
/**
|
|
1268
|
+
* Load all annotations.
|
|
1269
|
+
* @returns {Promise<Array>} Array of annotation objects
|
|
1270
|
+
*/
|
|
1271
|
+
async load() {
|
|
1272
|
+
throw new Error("AnnotationStore.load() not implemented")
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
/**
|
|
1276
|
+
* Create a new annotation.
|
|
1277
|
+
* @param {Object} data - Annotation data (without id)
|
|
1278
|
+
* @returns {Promise<Object>} Created annotation with server-assigned id
|
|
1279
|
+
*/
|
|
1280
|
+
async create(data) {
|
|
1281
|
+
throw new Error("AnnotationStore.create() not implemented")
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
/**
|
|
1285
|
+
* Update an existing annotation.
|
|
1286
|
+
* @param {string|number} id - Annotation id
|
|
1287
|
+
* @param {Object} data - Fields to update
|
|
1288
|
+
* @returns {Promise<Object>} Updated annotation
|
|
1289
|
+
*/
|
|
1290
|
+
async update(id, data) {
|
|
1291
|
+
throw new Error("AnnotationStore.update() not implemented")
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
/**
|
|
1295
|
+
* Delete an annotation.
|
|
1296
|
+
* @param {string|number} id - Annotation id
|
|
1297
|
+
* @returns {Promise<Object>} Deleted annotation
|
|
1298
|
+
*/
|
|
1299
|
+
async delete(id) {
|
|
1300
|
+
throw new Error("AnnotationStore.delete() not implemented")
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
/**
|
|
1304
|
+
* Restore a soft-deleted annotation.
|
|
1305
|
+
* @param {string|number} id - Annotation id
|
|
1306
|
+
* @returns {Promise<Object>} Restored annotation
|
|
1307
|
+
*/
|
|
1308
|
+
async restore(id) {
|
|
1309
|
+
throw new Error("AnnotationStore.restore() not implemented")
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
/**
|
|
1314
|
+
* REST API annotation store with configurable URL patterns.
|
|
1315
|
+
*
|
|
1316
|
+
* By default, uses Rails-style REST conventions:
|
|
1317
|
+
* - GET {baseUrl}.json - load all
|
|
1318
|
+
* - POST {baseUrl} - create
|
|
1319
|
+
* - PATCH {baseUrl}/{id} - update
|
|
1320
|
+
* - DELETE {baseUrl}/{id} - delete
|
|
1321
|
+
* - PATCH {baseUrl}/{id}/restore - restore
|
|
1322
|
+
*
|
|
1323
|
+
* URL patterns can be customized via function options:
|
|
1324
|
+
*
|
|
1325
|
+
* @example
|
|
1326
|
+
* // Rails default (just provide baseUrl)
|
|
1327
|
+
* new RestAnnotationStore({ baseUrl: '/documents/123/annotations' })
|
|
1328
|
+
*
|
|
1329
|
+
* @example
|
|
1330
|
+
* // Custom URL patterns
|
|
1331
|
+
* new RestAnnotationStore({
|
|
1332
|
+
* baseUrl: '/api/annotations',
|
|
1333
|
+
* loadUrl: () => '/api/annotations', // no .json suffix
|
|
1334
|
+
* updateUrl: (id) => `/api/annotations/${id}/edit`
|
|
1335
|
+
* })
|
|
1336
|
+
*
|
|
1337
|
+
* @example
|
|
1338
|
+
* // Fully custom URLs with closures
|
|
1339
|
+
* const docId = 123
|
|
1340
|
+
* new RestAnnotationStore({
|
|
1341
|
+
* loadUrl: () => `/api/v2/documents/${docId}/annotations`,
|
|
1342
|
+
* createUrl: () => `/api/v2/documents/${docId}/annotations`,
|
|
1343
|
+
* updateUrl: (id) => `/api/v2/annotations/${id}`,
|
|
1344
|
+
* deleteUrl: (id) => `/api/v2/annotations/${id}`,
|
|
1345
|
+
* restoreUrl: (id) => `/api/v2/annotations/${id}/restore`
|
|
1346
|
+
* })
|
|
1347
|
+
*/
|
|
1348
|
+
class RestAnnotationStore extends AnnotationStore {
|
|
1349
|
+
/**
|
|
1350
|
+
* @param {Object} options
|
|
1351
|
+
* @param {string} [options.baseUrl] - Base URL for Rails-style defaults
|
|
1352
|
+
* @param {Function} [options.loadUrl] - () => string - URL for loading annotations
|
|
1353
|
+
* @param {Function} [options.createUrl] - () => string - URL for creating annotations
|
|
1354
|
+
* @param {Function} [options.updateUrl] - (id) => string - URL for updating annotations
|
|
1355
|
+
* @param {Function} [options.deleteUrl] - (id) => string - URL for deleting annotations
|
|
1356
|
+
* @param {Function} [options.restoreUrl] - (id) => string - URL for restoring annotations
|
|
1357
|
+
*/
|
|
1358
|
+
constructor(options = {}) {
|
|
1359
|
+
super();
|
|
1360
|
+
this.baseUrl = options.baseUrl;
|
|
1361
|
+
|
|
1362
|
+
// Function-based URL builders with Rails-style defaults
|
|
1363
|
+
this.getLoadUrl = options.loadUrl || (() => `${this.baseUrl}.json`);
|
|
1364
|
+
this.getCreateUrl = options.createUrl || (() => this.baseUrl);
|
|
1365
|
+
this.getUpdateUrl = options.updateUrl || ((id) => `${this.baseUrl}/${id}`);
|
|
1366
|
+
this.getDeleteUrl = options.deleteUrl || ((id) => `${this.baseUrl}/${id}`);
|
|
1367
|
+
this.getRestoreUrl = options.restoreUrl || ((id) => `${this.baseUrl}/${id}/restore`);
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
async load() {
|
|
1371
|
+
const request = new FetchRequest("get", this.getLoadUrl());
|
|
1372
|
+
const response = await request.perform();
|
|
1373
|
+
|
|
1374
|
+
if (response.ok) {
|
|
1375
|
+
return await response.json
|
|
1376
|
+
} else {
|
|
1377
|
+
throw new Error("Failed to load annotations")
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
async create(data) {
|
|
1382
|
+
const request = new FetchRequest("post", this.getCreateUrl(), {
|
|
1383
|
+
body: JSON.stringify({ annotation: data }),
|
|
1384
|
+
contentType: "application/json",
|
|
1385
|
+
responseKind: "json"
|
|
1386
|
+
});
|
|
1387
|
+
|
|
1388
|
+
const response = await request.perform();
|
|
1389
|
+
|
|
1390
|
+
if (response.ok) {
|
|
1391
|
+
return await response.json
|
|
1392
|
+
} else {
|
|
1393
|
+
throw new Error("Failed to create annotation")
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
async update(id, data) {
|
|
1398
|
+
const request = new FetchRequest("patch", this.getUpdateUrl(id), {
|
|
1399
|
+
body: JSON.stringify({ annotation: data }),
|
|
1400
|
+
contentType: "application/json",
|
|
1401
|
+
responseKind: "json"
|
|
1402
|
+
});
|
|
1403
|
+
|
|
1404
|
+
const response = await request.perform();
|
|
1405
|
+
|
|
1406
|
+
if (response.ok) {
|
|
1407
|
+
return await response.json
|
|
1408
|
+
} else {
|
|
1409
|
+
throw new Error("Failed to update annotation")
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
async delete(id) {
|
|
1414
|
+
const request = new FetchRequest("delete", this.getDeleteUrl(id), {
|
|
1415
|
+
responseKind: "json"
|
|
1416
|
+
});
|
|
1417
|
+
|
|
1418
|
+
const response = await request.perform();
|
|
1419
|
+
|
|
1420
|
+
if (response.ok) {
|
|
1421
|
+
return await response.json
|
|
1422
|
+
} else {
|
|
1423
|
+
throw new Error("Failed to delete annotation")
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
async restore(id) {
|
|
1428
|
+
const request = new FetchRequest("patch", this.getRestoreUrl(id), {
|
|
1429
|
+
responseKind: "json"
|
|
1430
|
+
});
|
|
1431
|
+
|
|
1432
|
+
const response = await request.perform();
|
|
1433
|
+
|
|
1434
|
+
if (response.ok) {
|
|
1435
|
+
return await response.json
|
|
1436
|
+
} else {
|
|
1437
|
+
throw new Error("Failed to restore annotation")
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
/**
|
|
1443
|
+
* In-memory annotation store for development and demo purposes.
|
|
1444
|
+
*
|
|
1445
|
+
* Annotations are stored in memory only and lost on page refresh.
|
|
1446
|
+
* Useful for:
|
|
1447
|
+
* - Local development without a backend
|
|
1448
|
+
* - Demo/preview modes
|
|
1449
|
+
* - Testing
|
|
1450
|
+
*
|
|
1451
|
+
* @example
|
|
1452
|
+
* new MemoryAnnotationStore()
|
|
1453
|
+
*/
|
|
1454
|
+
class MemoryAnnotationStore extends AnnotationStore {
|
|
1455
|
+
constructor() {
|
|
1456
|
+
super();
|
|
1457
|
+
this._annotations = [];
|
|
1458
|
+
this._nextId = 1;
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
async load() {
|
|
1462
|
+
return [...this._annotations]
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
async create(data) {
|
|
1466
|
+
const annotation = {
|
|
1467
|
+
...data,
|
|
1468
|
+
id: `local-${this._nextId++}`,
|
|
1469
|
+
created_at: new Date().toISOString(),
|
|
1470
|
+
updated_at: new Date().toISOString()
|
|
1471
|
+
};
|
|
1472
|
+
|
|
1473
|
+
this._annotations.push(annotation);
|
|
1474
|
+
return annotation
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
async update(id, data) {
|
|
1478
|
+
const index = this._annotations.findIndex(a => a.id === id);
|
|
1479
|
+
if (index === -1) {
|
|
1480
|
+
throw new Error("Annotation not found")
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
const annotation = {
|
|
1484
|
+
...this._annotations[index],
|
|
1485
|
+
...data,
|
|
1486
|
+
id, // Preserve original id
|
|
1487
|
+
updated_at: new Date().toISOString()
|
|
1488
|
+
};
|
|
1489
|
+
|
|
1490
|
+
this._annotations[index] = annotation;
|
|
1491
|
+
return annotation
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
async delete(id) {
|
|
1495
|
+
const index = this._annotations.findIndex(a => a.id === id);
|
|
1496
|
+
if (index === -1) {
|
|
1497
|
+
throw new Error("Annotation not found")
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
const [annotation] = this._annotations.splice(index, 1);
|
|
1501
|
+
return annotation
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
async restore(id) {
|
|
1505
|
+
// Memory store doesn't support soft-delete/restore
|
|
1506
|
+
console.warn("MemoryAnnotationStore.restore() is not supported");
|
|
1507
|
+
return null
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1253
1511
|
// Custom event types for error handling
|
|
1254
1512
|
const AnnotationErrorType = {
|
|
1255
1513
|
LOAD_FAILED: "load_failed",
|
|
@@ -1260,13 +1518,31 @@ const AnnotationErrorType = {
|
|
|
1260
1518
|
};
|
|
1261
1519
|
|
|
1262
1520
|
class AnnotationManager {
|
|
1521
|
+
/**
|
|
1522
|
+
* @param {Object} options
|
|
1523
|
+
* @param {AnnotationStore} [options.store] - Custom store implementation
|
|
1524
|
+
* @param {string} [options.annotationsUrl] - Base URL for REST store (creates RestAnnotationStore)
|
|
1525
|
+
* @param {number} [options.documentId] - Document ID
|
|
1526
|
+
* @param {Function} [options.onAnnotationCreated] - Callback when annotation created
|
|
1527
|
+
* @param {Function} [options.onAnnotationUpdated] - Callback when annotation updated
|
|
1528
|
+
* @param {Function} [options.onAnnotationDeleted] - Callback when annotation deleted
|
|
1529
|
+
* @param {Element} [options.eventTarget] - Element for dispatching error events
|
|
1530
|
+
*/
|
|
1263
1531
|
constructor(options = {}) {
|
|
1264
|
-
this.annotationsUrl = options.annotationsUrl;
|
|
1265
1532
|
this.documentId = options.documentId;
|
|
1266
1533
|
this.onAnnotationCreated = options.onAnnotationCreated;
|
|
1267
1534
|
this.onAnnotationUpdated = options.onAnnotationUpdated;
|
|
1268
1535
|
this.onAnnotationDeleted = options.onAnnotationDeleted;
|
|
1269
|
-
this.eventTarget = options.eventTarget;
|
|
1536
|
+
this.eventTarget = options.eventTarget;
|
|
1537
|
+
|
|
1538
|
+
// Determine store: explicit > REST URL > memory
|
|
1539
|
+
if (options.store) {
|
|
1540
|
+
this.store = options.store;
|
|
1541
|
+
} else if (options.annotationsUrl) {
|
|
1542
|
+
this.store = new RestAnnotationStore({ baseUrl: options.annotationsUrl });
|
|
1543
|
+
} else {
|
|
1544
|
+
this.store = new MemoryAnnotationStore();
|
|
1545
|
+
}
|
|
1270
1546
|
|
|
1271
1547
|
this.annotations = new Map(); // id -> annotation
|
|
1272
1548
|
this.annotationsByPage = new Map(); // pageNumber -> [annotations]
|
|
@@ -1291,15 +1567,8 @@ class AnnotationManager {
|
|
|
1291
1567
|
|
|
1292
1568
|
async loadAnnotations() {
|
|
1293
1569
|
try {
|
|
1294
|
-
const
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
if (response.ok) {
|
|
1298
|
-
const data = await response.json;
|
|
1299
|
-
this._processAnnotations(data);
|
|
1300
|
-
} else {
|
|
1301
|
-
throw new Error("Server returned an error")
|
|
1302
|
-
}
|
|
1570
|
+
const annotations = await this.store.load();
|
|
1571
|
+
this._processAnnotations(annotations);
|
|
1303
1572
|
} catch (error) {
|
|
1304
1573
|
console.error("Failed to load annotations:", error);
|
|
1305
1574
|
this._dispatchError(AnnotationErrorType.LOAD_FAILED, "Failed to load annotations", error);
|
|
@@ -1335,26 +1604,14 @@ class AnnotationManager {
|
|
|
1335
1604
|
|
|
1336
1605
|
async createAnnotation(data) {
|
|
1337
1606
|
try {
|
|
1338
|
-
const
|
|
1339
|
-
|
|
1340
|
-
contentType: "application/json",
|
|
1341
|
-
responseKind: "json"
|
|
1342
|
-
});
|
|
1343
|
-
|
|
1344
|
-
const response = await request.perform();
|
|
1345
|
-
|
|
1346
|
-
if (response.ok) {
|
|
1347
|
-
const annotation = await response.json;
|
|
1348
|
-
this._addAnnotation(annotation);
|
|
1349
|
-
|
|
1350
|
-
if (this.onAnnotationCreated) {
|
|
1351
|
-
this.onAnnotationCreated(annotation);
|
|
1352
|
-
}
|
|
1607
|
+
const annotation = await this.store.create(data);
|
|
1608
|
+
this._addAnnotation(annotation);
|
|
1353
1609
|
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
throw new Error("Failed to create annotation")
|
|
1610
|
+
if (this.onAnnotationCreated) {
|
|
1611
|
+
this.onAnnotationCreated(annotation);
|
|
1357
1612
|
}
|
|
1613
|
+
|
|
1614
|
+
return annotation
|
|
1358
1615
|
} catch (error) {
|
|
1359
1616
|
console.error("Failed to create annotation:", error);
|
|
1360
1617
|
this._dispatchError(AnnotationErrorType.CREATE_FAILED, "Failed to save annotation", error);
|
|
@@ -1364,26 +1621,14 @@ class AnnotationManager {
|
|
|
1364
1621
|
|
|
1365
1622
|
async updateAnnotation(id, data) {
|
|
1366
1623
|
try {
|
|
1367
|
-
const
|
|
1368
|
-
|
|
1369
|
-
contentType: "application/json",
|
|
1370
|
-
responseKind: "json"
|
|
1371
|
-
});
|
|
1624
|
+
const annotation = await this.store.update(id, data);
|
|
1625
|
+
this._updateAnnotation(annotation);
|
|
1372
1626
|
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
if (response.ok) {
|
|
1376
|
-
const annotation = await response.json;
|
|
1377
|
-
this._updateAnnotation(annotation);
|
|
1378
|
-
|
|
1379
|
-
if (this.onAnnotationUpdated) {
|
|
1380
|
-
this.onAnnotationUpdated(annotation);
|
|
1381
|
-
}
|
|
1382
|
-
|
|
1383
|
-
return annotation
|
|
1384
|
-
} else {
|
|
1385
|
-
throw new Error("Failed to update annotation")
|
|
1627
|
+
if (this.onAnnotationUpdated) {
|
|
1628
|
+
this.onAnnotationUpdated(annotation);
|
|
1386
1629
|
}
|
|
1630
|
+
|
|
1631
|
+
return annotation
|
|
1387
1632
|
} catch (error) {
|
|
1388
1633
|
console.error("Failed to update annotation:", error);
|
|
1389
1634
|
this._dispatchError(AnnotationErrorType.UPDATE_FAILED, "Failed to update annotation", error);
|
|
@@ -1392,26 +1637,18 @@ class AnnotationManager {
|
|
|
1392
1637
|
}
|
|
1393
1638
|
|
|
1394
1639
|
async deleteAnnotation(id) {
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
if (!annotation) return
|
|
1640
|
+
const existingAnnotation = this.annotations.get(id);
|
|
1641
|
+
if (!existingAnnotation) return
|
|
1398
1642
|
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
const response = await request.perform();
|
|
1403
|
-
|
|
1404
|
-
if (response.ok) {
|
|
1405
|
-
this._removeAnnotation(id);
|
|
1406
|
-
|
|
1407
|
-
if (this.onAnnotationDeleted) {
|
|
1408
|
-
this.onAnnotationDeleted(annotation);
|
|
1409
|
-
}
|
|
1643
|
+
try {
|
|
1644
|
+
const annotation = await this.store.delete(id);
|
|
1645
|
+
this._removeAnnotation(id);
|
|
1410
1646
|
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
throw new Error("Failed to delete annotation")
|
|
1647
|
+
if (this.onAnnotationDeleted) {
|
|
1648
|
+
this.onAnnotationDeleted(existingAnnotation);
|
|
1414
1649
|
}
|
|
1650
|
+
|
|
1651
|
+
return existingAnnotation
|
|
1415
1652
|
} catch (error) {
|
|
1416
1653
|
console.error("Failed to delete annotation:", error);
|
|
1417
1654
|
this._dispatchError(AnnotationErrorType.DELETE_FAILED, "Failed to delete annotation", error);
|
|
@@ -1421,23 +1658,16 @@ class AnnotationManager {
|
|
|
1421
1658
|
|
|
1422
1659
|
async restoreAnnotation(id) {
|
|
1423
1660
|
try {
|
|
1424
|
-
const
|
|
1425
|
-
|
|
1426
|
-
});
|
|
1427
|
-
const response = await request.perform();
|
|
1428
|
-
|
|
1429
|
-
if (response.ok) {
|
|
1430
|
-
const annotation = await response.json;
|
|
1431
|
-
this._addAnnotation(annotation);
|
|
1661
|
+
const annotation = await this.store.restore(id);
|
|
1662
|
+
if (!annotation) return null
|
|
1432
1663
|
|
|
1433
|
-
|
|
1434
|
-
this.onAnnotationCreated(annotation);
|
|
1435
|
-
}
|
|
1664
|
+
this._addAnnotation(annotation);
|
|
1436
1665
|
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
throw new Error("Failed to restore annotation")
|
|
1666
|
+
if (this.onAnnotationCreated) {
|
|
1667
|
+
this.onAnnotationCreated(annotation);
|
|
1440
1668
|
}
|
|
1669
|
+
|
|
1670
|
+
return annotation
|
|
1441
1671
|
} catch (error) {
|
|
1442
1672
|
console.error("Failed to restore annotation:", error);
|
|
1443
1673
|
this._dispatchError(AnnotationErrorType.RESTORE_FAILED, "Failed to restore annotation", error);
|
|
@@ -2110,6 +2340,11 @@ const Icons = {
|
|
|
2110
2340
|
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
|
|
2111
2341
|
</svg>`,
|
|
2112
2342
|
|
|
2343
|
+
// Comment/Speech bubble icon - used in annotation edit toolbar
|
|
2344
|
+
comment: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
2345
|
+
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
|
|
2346
|
+
</svg>`,
|
|
2347
|
+
|
|
2113
2348
|
// Chevron down - used in color pickers, dropdowns
|
|
2114
2349
|
chevronDown: `<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
|
2115
2350
|
<polyline points="6 9 12 15 18 9"/>
|
|
@@ -2244,6 +2479,7 @@ class AnnotationEditToolbar {
|
|
|
2244
2479
|
this.onColorChange = options.onColorChange;
|
|
2245
2480
|
this.onDelete = options.onDelete;
|
|
2246
2481
|
this.onEdit = options.onEdit;
|
|
2482
|
+
this.onComment = options.onComment;
|
|
2247
2483
|
this.onDeselect = options.onDeselect;
|
|
2248
2484
|
this.colors = options.colors || ColorPicker.COLORS.map(c => c.value);
|
|
2249
2485
|
|
|
@@ -2260,6 +2496,9 @@ class AnnotationEditToolbar {
|
|
|
2260
2496
|
this.element.className = "annotation-edit-toolbar hidden";
|
|
2261
2497
|
this.element.innerHTML = `
|
|
2262
2498
|
<div class="toolbar-buttons">
|
|
2499
|
+
<button class="toolbar-btn comment-btn hidden" title="Add Comment (C)">
|
|
2500
|
+
${Icons.comment}
|
|
2501
|
+
</button>
|
|
2263
2502
|
<button class="color-picker-btn" title="Change color" aria-haspopup="true" aria-expanded="false">
|
|
2264
2503
|
<span class="color-swatch"></span>
|
|
2265
2504
|
${Icons.chevronDown}
|
|
@@ -2279,11 +2518,12 @@ class AnnotationEditToolbar {
|
|
|
2279
2518
|
${Icons.delete}
|
|
2280
2519
|
</button>
|
|
2281
2520
|
</div>
|
|
2282
|
-
<div class="toolbar-
|
|
2521
|
+
<div class="toolbar-annotation-content hidden"></div>
|
|
2283
2522
|
`;
|
|
2284
2523
|
|
|
2524
|
+
this.commentBtn = this.element.querySelector(".comment-btn");
|
|
2285
2525
|
this.editBtn = this.element.querySelector(".edit-btn");
|
|
2286
|
-
this.
|
|
2526
|
+
this.annotationContent = this.element.querySelector(".toolbar-annotation-content");
|
|
2287
2527
|
}
|
|
2288
2528
|
|
|
2289
2529
|
_setupEventListeners() {
|
|
@@ -2304,7 +2544,15 @@ class AnnotationEditToolbar {
|
|
|
2304
2544
|
});
|
|
2305
2545
|
});
|
|
2306
2546
|
|
|
2307
|
-
//
|
|
2547
|
+
// Comment button (for highlight/underline/ink annotations)
|
|
2548
|
+
this.commentBtn.addEventListener("click", (e) => {
|
|
2549
|
+
e.stopPropagation();
|
|
2550
|
+
if (this.currentAnnotation && this.onComment) {
|
|
2551
|
+
this.onComment(this.currentAnnotation);
|
|
2552
|
+
}
|
|
2553
|
+
});
|
|
2554
|
+
|
|
2555
|
+
// Edit button (for notes)
|
|
2308
2556
|
this.editBtn.addEventListener("click", (e) => {
|
|
2309
2557
|
e.stopPropagation();
|
|
2310
2558
|
if (this.currentAnnotation && this.onEdit) {
|
|
@@ -2357,6 +2605,13 @@ class AnnotationEditToolbar {
|
|
|
2357
2605
|
e.preventDefault();
|
|
2358
2606
|
this.onEdit(this.currentAnnotation);
|
|
2359
2607
|
}
|
|
2608
|
+
} else if (e.key === "c" || e.key === "C") {
|
|
2609
|
+
// Comment shortcut for highlight/underline/ink annotations
|
|
2610
|
+
const supportsComment = ["highlight", "line", "ink"].includes(this.currentAnnotation?.annotation_type);
|
|
2611
|
+
if (supportsComment && this.onComment) {
|
|
2612
|
+
e.preventDefault();
|
|
2613
|
+
this.onComment(this.currentAnnotation);
|
|
2614
|
+
}
|
|
2360
2615
|
}
|
|
2361
2616
|
});
|
|
2362
2617
|
}
|
|
@@ -2412,15 +2667,26 @@ class AnnotationEditToolbar {
|
|
|
2412
2667
|
const color = annotation.color || ColorPicker.DEFAULT_HIGHLIGHT_COLOR;
|
|
2413
2668
|
this._updateSelectedColor(color);
|
|
2414
2669
|
|
|
2415
|
-
// Show/hide
|
|
2670
|
+
// Show/hide buttons based on annotation type
|
|
2416
2671
|
const isNote = annotation.annotation_type === "note";
|
|
2672
|
+
const supportsComment = ["highlight", "line", "ink"].includes(annotation.annotation_type);
|
|
2673
|
+
|
|
2674
|
+
// Comment button for highlight/underline/ink, edit button for notes
|
|
2675
|
+
this.commentBtn.classList.toggle("hidden", !supportsComment);
|
|
2417
2676
|
this.editBtn.classList.toggle("hidden", !isNote);
|
|
2418
2677
|
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2678
|
+
// Update comment button title based on whether contents exists
|
|
2679
|
+
if (supportsComment) {
|
|
2680
|
+
const hasComment = annotation.contents && annotation.contents.trim();
|
|
2681
|
+
this.commentBtn.title = hasComment ? "Edit Comment (C)" : "Add Comment (C)";
|
|
2682
|
+
}
|
|
2683
|
+
|
|
2684
|
+
// Show contents for any annotation type that has it
|
|
2685
|
+
if (annotation.contents) {
|
|
2686
|
+
this.annotationContent.textContent = annotation.contents;
|
|
2687
|
+
this.annotationContent.classList.remove("hidden");
|
|
2422
2688
|
} else {
|
|
2423
|
-
this.
|
|
2689
|
+
this.annotationContent.classList.add("hidden");
|
|
2424
2690
|
}
|
|
2425
2691
|
|
|
2426
2692
|
// Determine if toolbar should flip above the annotation
|
|
@@ -2441,9 +2707,9 @@ class AnnotationEditToolbar {
|
|
|
2441
2707
|
this.element.classList.add("hidden");
|
|
2442
2708
|
this.currentAnnotation = null;
|
|
2443
2709
|
|
|
2444
|
-
// Clear
|
|
2445
|
-
this.
|
|
2446
|
-
this.
|
|
2710
|
+
// Clear annotation content
|
|
2711
|
+
this.annotationContent.textContent = "";
|
|
2712
|
+
this.annotationContent.classList.add("hidden");
|
|
2447
2713
|
|
|
2448
2714
|
// Remove from parent when hidden
|
|
2449
2715
|
if (this.element.parentNode) {
|
|
@@ -3137,10 +3403,10 @@ const ANNOTATION_ICONS = {
|
|
|
3137
3403
|
};
|
|
3138
3404
|
|
|
3139
3405
|
class AnnotationSidebar {
|
|
3140
|
-
constructor({ container, annotationManager, onAnnotationClick }) {
|
|
3141
|
-
this.container = container;
|
|
3406
|
+
constructor({ element, itemTemplate, container, annotationManager, onAnnotationClick }) {
|
|
3142
3407
|
this.annotationManager = annotationManager;
|
|
3143
3408
|
this.onAnnotationClick = onAnnotationClick;
|
|
3409
|
+
this.itemTemplate = itemTemplate; // Optional <template> element for custom list items
|
|
3144
3410
|
|
|
3145
3411
|
this.isOpen = false;
|
|
3146
3412
|
this.sidebarWidth = SIDEBAR_DEFAULT_WIDTH;
|
|
@@ -3148,7 +3414,30 @@ class AnnotationSidebar {
|
|
|
3148
3414
|
this.filterType = FilterType.ALL;
|
|
3149
3415
|
this.selectedAnnotationId = null;
|
|
3150
3416
|
|
|
3151
|
-
|
|
3417
|
+
if (element) {
|
|
3418
|
+
// User provided HTML - find elements via data attributes
|
|
3419
|
+
this.element = element;
|
|
3420
|
+
this.container = element.parentElement;
|
|
3421
|
+
this.listContainer = element.querySelector('[data-role="list"]');
|
|
3422
|
+
this.header = element.querySelector('.pdf-sidebar-header');
|
|
3423
|
+
this.emptyState = element.querySelector('[data-role="empty-state"]');
|
|
3424
|
+
this.sortControls = element.querySelector('[data-role="sort-controls"]');
|
|
3425
|
+
this.filterControls = element.querySelector('[data-role="filter-controls"]');
|
|
3426
|
+
this.resizer = element.querySelector('[data-role="resizer"]');
|
|
3427
|
+
|
|
3428
|
+
// Read initial width from CSS variable if set
|
|
3429
|
+
const currentWidth = element.style.getPropertyValue('--sidebar-width');
|
|
3430
|
+
if (currentWidth) {
|
|
3431
|
+
this.sidebarWidth = parseInt(currentWidth, 10) || SIDEBAR_DEFAULT_WIDTH;
|
|
3432
|
+
} else {
|
|
3433
|
+
element.style.setProperty('--sidebar-width', `${this.sidebarWidth}px`);
|
|
3434
|
+
}
|
|
3435
|
+
} else {
|
|
3436
|
+
// Fallback - create default HTML (existing behavior)
|
|
3437
|
+
this.container = container;
|
|
3438
|
+
this._createElements();
|
|
3439
|
+
}
|
|
3440
|
+
|
|
3152
3441
|
this._setupEventListeners();
|
|
3153
3442
|
}
|
|
3154
3443
|
|
|
@@ -3231,27 +3520,35 @@ class AnnotationSidebar {
|
|
|
3231
3520
|
}
|
|
3232
3521
|
|
|
3233
3522
|
_setupEventListeners() {
|
|
3234
|
-
// Close button
|
|
3235
|
-
const closeBtn = this.header
|
|
3236
|
-
|
|
3523
|
+
// Close button - support both user HTML (data-action="close") and auto-generated (.pdf-sidebar-close)
|
|
3524
|
+
const closeBtn = this.header?.querySelector('[data-action="close"]') ||
|
|
3525
|
+
this.header?.querySelector(".pdf-sidebar-close");
|
|
3526
|
+
if (closeBtn) {
|
|
3527
|
+
closeBtn.addEventListener("click", () => this.close());
|
|
3528
|
+
}
|
|
3237
3529
|
|
|
3238
3530
|
// Sort buttons
|
|
3239
|
-
this.sortControls
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3531
|
+
if (this.sortControls) {
|
|
3532
|
+
this.sortControls.addEventListener("click", (e) => {
|
|
3533
|
+
const btn = e.target.closest("[data-sort]") || e.target.closest(".sort-btn");
|
|
3534
|
+
if (btn && btn.dataset.sort) {
|
|
3535
|
+
this.sortMode = btn.dataset.sort;
|
|
3536
|
+
this.sortControls.querySelectorAll("[data-sort], .sort-btn").forEach(b => b.classList.remove("active"));
|
|
3537
|
+
btn.classList.add("active");
|
|
3538
|
+
this._refreshList();
|
|
3539
|
+
}
|
|
3540
|
+
});
|
|
3541
|
+
}
|
|
3248
3542
|
|
|
3249
|
-
// Filter select
|
|
3250
|
-
const filterSelect = this.filterControls
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3543
|
+
// Filter select - support both user HTML (data-action="filter") and auto-generated (.annotation-filter-select)
|
|
3544
|
+
const filterSelect = this.filterControls?.querySelector('[data-action="filter"]') ||
|
|
3545
|
+
this.filterControls?.querySelector(".annotation-filter-select");
|
|
3546
|
+
if (filterSelect) {
|
|
3547
|
+
filterSelect.addEventListener("change", (e) => {
|
|
3548
|
+
this.filterType = e.target.value;
|
|
3549
|
+
this._refreshList();
|
|
3550
|
+
});
|
|
3551
|
+
}
|
|
3255
3552
|
|
|
3256
3553
|
// Sidebar resizing
|
|
3257
3554
|
this._setupResizer();
|
|
@@ -3263,6 +3560,8 @@ class AnnotationSidebar {
|
|
|
3263
3560
|
}
|
|
3264
3561
|
|
|
3265
3562
|
_setupResizer() {
|
|
3563
|
+
if (!this.resizer) return
|
|
3564
|
+
|
|
3266
3565
|
let startX, startWidth;
|
|
3267
3566
|
|
|
3268
3567
|
const onMouseMove = (e) => {
|
|
@@ -3346,16 +3645,19 @@ class AnnotationSidebar {
|
|
|
3346
3645
|
// Clear and rebuild list
|
|
3347
3646
|
this.listContainer.innerHTML = "";
|
|
3348
3647
|
|
|
3349
|
-
// Update count badge
|
|
3350
|
-
const countBadge = this.header
|
|
3351
|
-
|
|
3648
|
+
// Update count badge - support both user HTML (data-role="count") and auto-generated
|
|
3649
|
+
const countBadge = this.header?.querySelector('[data-role="count"]') ||
|
|
3650
|
+
this.header?.querySelector(".annotation-count-badge");
|
|
3651
|
+
if (countBadge) {
|
|
3652
|
+
countBadge.textContent = annotations.length;
|
|
3653
|
+
}
|
|
3352
3654
|
|
|
3353
3655
|
// Show empty state or list
|
|
3354
3656
|
if (annotations.length === 0) {
|
|
3355
|
-
this.emptyState
|
|
3657
|
+
this.emptyState?.classList.add("visible");
|
|
3356
3658
|
this.listContainer.classList.add("empty");
|
|
3357
3659
|
} else {
|
|
3358
|
-
this.emptyState
|
|
3660
|
+
this.emptyState?.classList.remove("visible");
|
|
3359
3661
|
this.listContainer.classList.remove("empty");
|
|
3360
3662
|
|
|
3361
3663
|
for (const annotation of annotations) {
|
|
@@ -3424,40 +3726,71 @@ class AnnotationSidebar {
|
|
|
3424
3726
|
}
|
|
3425
3727
|
|
|
3426
3728
|
_createListItem(annotation) {
|
|
3427
|
-
|
|
3428
|
-
item.className = "annotation-list-item";
|
|
3429
|
-
item.dataset.annotationId = annotation.id;
|
|
3430
|
-
item.tabIndex = 0;
|
|
3729
|
+
let item;
|
|
3431
3730
|
|
|
3432
|
-
if (this.
|
|
3433
|
-
|
|
3434
|
-
|
|
3731
|
+
if (this.itemTemplate) {
|
|
3732
|
+
// Clone user's template and populate data-field elements
|
|
3733
|
+
item = this.itemTemplate.content.firstElementChild.cloneNode(true);
|
|
3734
|
+
item.dataset.annotationId = annotation.id;
|
|
3435
3735
|
|
|
3436
|
-
|
|
3437
|
-
|
|
3736
|
+
// Ensure tabIndex for keyboard navigation
|
|
3737
|
+
if (!item.hasAttribute("tabindex")) {
|
|
3738
|
+
item.tabIndex = 0;
|
|
3739
|
+
}
|
|
3438
3740
|
|
|
3439
|
-
|
|
3440
|
-
|
|
3741
|
+
// Determine display values
|
|
3742
|
+
const { icon, label, typeLabel } = this._getAnnotationDisplay(annotation);
|
|
3743
|
+
const timestamp = this._formatTimestamp(annotation.created_at);
|
|
3744
|
+
|
|
3745
|
+
// Populate data-field elements
|
|
3746
|
+
this._setField(item, "icon", icon, annotation.color);
|
|
3747
|
+
this._setField(item, "label", this._escapeHtml(label));
|
|
3748
|
+
this._setField(item, "type", typeLabel);
|
|
3749
|
+
this._setField(item, "page", `Page ${annotation.page}`);
|
|
3750
|
+
this._setField(item, "time", timestamp);
|
|
3751
|
+
|
|
3752
|
+
// Also set data attributes for user's Stimulus controllers
|
|
3753
|
+
item.dataset.annotationType = annotation.annotation_type;
|
|
3754
|
+
item.dataset.annotationPage = annotation.page;
|
|
3755
|
+
item.dataset.annotationColor = annotation.color || "";
|
|
3756
|
+
} else {
|
|
3757
|
+
// Fallback - existing innerHTML approach
|
|
3758
|
+
item = document.createElement("div");
|
|
3759
|
+
item.className = "annotation-list-item";
|
|
3760
|
+
item.dataset.annotationId = annotation.id;
|
|
3761
|
+
item.tabIndex = 0;
|
|
3441
3762
|
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
<span class="annotation-item-page">Page ${annotation.page}</span>
|
|
3452
|
-
<span class="annotation-item-separator">•</span>
|
|
3453
|
-
<span class="annotation-item-time">${timestamp}</span>
|
|
3763
|
+
// Determine icon and label based on type
|
|
3764
|
+
const { icon, label, typeLabel } = this._getAnnotationDisplay(annotation);
|
|
3765
|
+
|
|
3766
|
+
// Format timestamp
|
|
3767
|
+
const timestamp = this._formatTimestamp(annotation.created_at);
|
|
3768
|
+
|
|
3769
|
+
item.innerHTML = `
|
|
3770
|
+
<div class="annotation-item-icon" style="color: ${annotation.color || '#666'}">
|
|
3771
|
+
${icon}
|
|
3454
3772
|
</div>
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3773
|
+
<div class="annotation-item-content">
|
|
3774
|
+
<div class="annotation-item-label">${this._escapeHtml(label)}</div>
|
|
3775
|
+
<div class="annotation-item-meta">
|
|
3776
|
+
<span class="annotation-item-type">${typeLabel}</span>
|
|
3777
|
+
<span class="annotation-item-separator">•</span>
|
|
3778
|
+
<span class="annotation-item-page">Page ${annotation.page}</span>
|
|
3779
|
+
<span class="annotation-item-separator">•</span>
|
|
3780
|
+
<span class="annotation-item-time">${timestamp}</span>
|
|
3781
|
+
</div>
|
|
3782
|
+
</div>
|
|
3783
|
+
<div class="annotation-item-hover">
|
|
3784
|
+
<span>Jump</span>
|
|
3785
|
+
${Icons.chevronRight}
|
|
3786
|
+
</div>
|
|
3787
|
+
`;
|
|
3788
|
+
}
|
|
3789
|
+
|
|
3790
|
+
// Selection state
|
|
3791
|
+
if (this.selectedAnnotationId === annotation.id) {
|
|
3792
|
+
item.classList.add("selected");
|
|
3793
|
+
}
|
|
3461
3794
|
|
|
3462
3795
|
// Click handler
|
|
3463
3796
|
item.addEventListener("click", () => {
|
|
@@ -3470,6 +3803,23 @@ class AnnotationSidebar {
|
|
|
3470
3803
|
return item
|
|
3471
3804
|
}
|
|
3472
3805
|
|
|
3806
|
+
/**
|
|
3807
|
+
* Set a field value in a template-cloned element
|
|
3808
|
+
* @param {HTMLElement} element - The cloned template element
|
|
3809
|
+
* @param {string} fieldName - The data-field name to find
|
|
3810
|
+
* @param {string} value - The value to set (can include HTML for icons)
|
|
3811
|
+
* @param {string} color - Optional color to apply
|
|
3812
|
+
*/
|
|
3813
|
+
_setField(element, fieldName, value, color) {
|
|
3814
|
+
const field = element.querySelector(`[data-field="${fieldName}"]`);
|
|
3815
|
+
if (field) {
|
|
3816
|
+
field.innerHTML = value;
|
|
3817
|
+
if (color && fieldName === "icon") {
|
|
3818
|
+
field.style.color = color;
|
|
3819
|
+
}
|
|
3820
|
+
}
|
|
3821
|
+
}
|
|
3822
|
+
|
|
3473
3823
|
_getAnnotationDisplay(annotation) {
|
|
3474
3824
|
const type = annotation.annotation_type;
|
|
3475
3825
|
let icon, label, typeLabel;
|
|
@@ -4844,15 +5194,25 @@ class CoordinateTransformer {
|
|
|
4844
5194
|
for (let i = 0; i < result.length; i++) {
|
|
4845
5195
|
const existing = result[i];
|
|
4846
5196
|
|
|
4847
|
-
//
|
|
4848
|
-
const
|
|
5197
|
+
// Calculate vertical overlap amount
|
|
5198
|
+
const overlapTop = Math.max(rect.top, existing.top);
|
|
5199
|
+
const overlapBottom = Math.min(rect.bottom, existing.bottom);
|
|
5200
|
+
const overlapHeight = Math.max(0, overlapBottom - overlapTop);
|
|
5201
|
+
|
|
5202
|
+
// Require significant vertical overlap (at least 50% of smaller rect's height)
|
|
5203
|
+
// This prevents merging rects from different lines that only slightly overlap
|
|
5204
|
+
const rectHeight = rect.bottom - rect.top;
|
|
5205
|
+
const existingHeight = existing.bottom - existing.top;
|
|
5206
|
+
const minHeight = Math.min(rectHeight, existingHeight);
|
|
5207
|
+
const significantVerticalOverlap = overlapHeight > minHeight * 0.5;
|
|
5208
|
+
|
|
4849
5209
|
const horizontalOverlap = rect.left < existing.right && rect.right > existing.left;
|
|
4850
5210
|
|
|
4851
5211
|
// Also merge if they're adjacent horizontally on same line
|
|
4852
5212
|
const sameLine = Math.abs(rect.top - existing.top) < 3 && Math.abs(rect.bottom - existing.bottom) < 3;
|
|
4853
5213
|
const horizontallyAdjacent = Math.abs(rect.left - existing.right) < 2 || Math.abs(existing.left - rect.right) < 2;
|
|
4854
5214
|
|
|
4855
|
-
if ((
|
|
5215
|
+
if ((significantVerticalOverlap && horizontalOverlap) || (sameLine && horizontallyAdjacent)) {
|
|
4856
5216
|
// Merge: extend existing rect to encompass both
|
|
4857
5217
|
result[i] = {
|
|
4858
5218
|
left: Math.min(existing.left, rect.left),
|
|
@@ -5817,12 +6177,12 @@ class NoteTool extends BaseTool {
|
|
|
5817
6177
|
});
|
|
5818
6178
|
}
|
|
5819
6179
|
|
|
5820
|
-
// Method to edit an existing note
|
|
6180
|
+
// Method to edit an existing note or add/edit a comment on other annotation types
|
|
5821
6181
|
editNote(annotation) {
|
|
5822
6182
|
// Store currently focused element for restoration on close
|
|
5823
6183
|
this._previousFocusElement = document.activeElement;
|
|
5824
6184
|
|
|
5825
|
-
// Get the position of the
|
|
6185
|
+
// Get the position of the annotation on screen
|
|
5826
6186
|
const pageContainer = this.viewer.getPageContainer(annotation.page);
|
|
5827
6187
|
if (!pageContainer) return
|
|
5828
6188
|
|
|
@@ -5834,10 +6194,20 @@ class NoteTool extends BaseTool {
|
|
|
5834
6194
|
// Store the annotation being edited
|
|
5835
6195
|
this.editingAnnotation = annotation;
|
|
5836
6196
|
|
|
5837
|
-
|
|
6197
|
+
// Determine dialog title based on annotation type and whether contents exists
|
|
6198
|
+
const isNote = annotation.annotation_type === "note";
|
|
6199
|
+
const hasContents = annotation.contents && annotation.contents.trim();
|
|
6200
|
+
let dialogTitle;
|
|
6201
|
+
if (isNote) {
|
|
6202
|
+
dialogTitle = "Edit Note";
|
|
6203
|
+
} else {
|
|
6204
|
+
dialogTitle = hasContents ? "Edit Comment" : "Add Comment";
|
|
6205
|
+
}
|
|
6206
|
+
|
|
6207
|
+
this._showEditDialog(rect.left + x, rect.top + y, annotation.contents, dialogTitle);
|
|
5838
6208
|
}
|
|
5839
6209
|
|
|
5840
|
-
_showEditDialog(x, y, existingText) {
|
|
6210
|
+
_showEditDialog(x, y, existingText, title = "Edit Note") {
|
|
5841
6211
|
// Remove any existing dialog (but keep editingAnnotation)
|
|
5842
6212
|
this._removeDialog();
|
|
5843
6213
|
|
|
@@ -5846,7 +6216,7 @@ class NoteTool extends BaseTool {
|
|
|
5846
6216
|
this.noteDialog.className = "note-dialog";
|
|
5847
6217
|
this.noteDialog.innerHTML = `
|
|
5848
6218
|
<div class="note-dialog-header">
|
|
5849
|
-
<span
|
|
6219
|
+
<span>${title}</span>
|
|
5850
6220
|
<button class="note-dialog-close" aria-label="Close">
|
|
5851
6221
|
${Icons.close}
|
|
5852
6222
|
</button>
|
|
@@ -6328,7 +6698,9 @@ class PdfViewer {
|
|
|
6328
6698
|
this._setupViewerEvents();
|
|
6329
6699
|
|
|
6330
6700
|
// Annotation manager for CRUD operations
|
|
6701
|
+
// Accepts custom store, falls back to REST store if URL provided, else memory store
|
|
6331
6702
|
this.annotationManager = new AnnotationManager({
|
|
6703
|
+
store: this.options.annotationStore,
|
|
6332
6704
|
annotationsUrl: this.annotationsUrl,
|
|
6333
6705
|
documentId: this.documentId,
|
|
6334
6706
|
eventTarget: this.container, // For dispatching error events
|
|
@@ -6354,6 +6726,7 @@ class PdfViewer {
|
|
|
6354
6726
|
onColorChange: this._onAnnotationColorChange.bind(this),
|
|
6355
6727
|
onDelete: this._onAnnotationDelete.bind(this),
|
|
6356
6728
|
onEdit: this._onAnnotationEdit.bind(this),
|
|
6729
|
+
onComment: this._onAnnotationComment.bind(this),
|
|
6357
6730
|
onDeselect: this._deselectAnnotation.bind(this)
|
|
6358
6731
|
});
|
|
6359
6732
|
|
|
@@ -6374,9 +6747,14 @@ class PdfViewer {
|
|
|
6374
6747
|
onPageClick: (pageNumber) => this.viewer.goToPage(pageNumber)
|
|
6375
6748
|
});
|
|
6376
6749
|
|
|
6377
|
-
// Annotation sidebar
|
|
6750
|
+
// Annotation sidebar - check for user-defined element, fallback to auto-generated
|
|
6751
|
+
const annotationSidebarEl = this.container.querySelector('[data-pdf-sidebar="annotations"]');
|
|
6752
|
+
const annotationItemTemplate = this.container.querySelector('[data-pdf-template="annotation-item"]');
|
|
6753
|
+
|
|
6378
6754
|
this.annotationSidebar = new AnnotationSidebar({
|
|
6379
|
-
|
|
6755
|
+
element: annotationSidebarEl, // null if not provided (triggers fallback)
|
|
6756
|
+
itemTemplate: annotationItemTemplate, // null if not provided (uses innerHTML)
|
|
6757
|
+
container: this.bodyContainer, // Used for fallback
|
|
6380
6758
|
annotationManager: this.annotationManager,
|
|
6381
6759
|
onAnnotationClick: (annotationId) => this._scrollToAnnotationWithFlash(annotationId)
|
|
6382
6760
|
});
|
|
@@ -6538,7 +6916,7 @@ class PdfViewer {
|
|
|
6538
6916
|
await this.thumbnailSidebar.setDocument(this.viewer.pdfDocument);
|
|
6539
6917
|
}
|
|
6540
6918
|
|
|
6541
|
-
// Load existing annotations
|
|
6919
|
+
// Load existing annotations from store
|
|
6542
6920
|
await this.annotationManager.loadAnnotations();
|
|
6543
6921
|
|
|
6544
6922
|
// Render annotations on all rendered pages
|
|
@@ -6757,6 +7135,14 @@ class PdfViewer {
|
|
|
6757
7135
|
}
|
|
6758
7136
|
}
|
|
6759
7137
|
|
|
7138
|
+
_onAnnotationComment(annotation) {
|
|
7139
|
+
// For highlight/underline/ink, use the note tool's edit dialog to edit contents
|
|
7140
|
+
const supportsComment = ["highlight", "line", "ink"].includes(annotation.annotation_type);
|
|
7141
|
+
if (supportsComment) {
|
|
7142
|
+
this.tools[ToolMode.NOTE].editNote(annotation);
|
|
7143
|
+
}
|
|
7144
|
+
}
|
|
7145
|
+
|
|
6760
7146
|
async _onAnnotationDelete(annotation) {
|
|
6761
7147
|
await this.annotationManager.deleteAnnotation(annotation.id);
|
|
6762
7148
|
}
|
|
@@ -7570,7 +7956,8 @@ class pdf_viewer_controller extends Controller {
|
|
|
7570
7956
|
documentId: String,
|
|
7571
7957
|
trackingUrl: String,
|
|
7572
7958
|
initialPage: Number,
|
|
7573
|
-
initialAnnotation: String
|
|
7959
|
+
initialAnnotation: String,
|
|
7960
|
+
autoHeight: { type: Boolean, default: true }
|
|
7574
7961
|
}
|
|
7575
7962
|
|
|
7576
7963
|
initialize() {
|
|
@@ -8036,8 +8423,8 @@ class pdf_viewer_controller extends Controller {
|
|
|
8036
8423
|
|
|
8037
8424
|
setViewportHeight() {
|
|
8038
8425
|
requestAnimationFrame(() => {
|
|
8039
|
-
// Skip if
|
|
8040
|
-
if (this.
|
|
8426
|
+
// Skip if autoHeight is disabled (container height managed by consuming application)
|
|
8427
|
+
if (!this.autoHeightValue) {
|
|
8041
8428
|
return
|
|
8042
8429
|
}
|
|
8043
8430
|
|
|
@@ -8124,40 +8511,5 @@ class pdf_download_controller extends Controller {
|
|
|
8124
8511
|
}
|
|
8125
8512
|
}
|
|
8126
8513
|
|
|
8127
|
-
|
|
8128
|
-
static targets = ["container", "toggle"]
|
|
8129
|
-
|
|
8130
|
-
connect() {
|
|
8131
|
-
this.isSyncing = false;
|
|
8132
|
-
}
|
|
8133
|
-
|
|
8134
|
-
sync(event) {
|
|
8135
|
-
if (this.isSyncing) return
|
|
8136
|
-
if (!this.toggleTarget.checked) return
|
|
8137
|
-
|
|
8138
|
-
const master = event.currentTarget;
|
|
8139
|
-
const slave = this.containerTargets.find(target => target !== master);
|
|
8140
|
-
|
|
8141
|
-
if (!slave) return
|
|
8142
|
-
|
|
8143
|
-
this.isSyncing = true;
|
|
8144
|
-
|
|
8145
|
-
// Calculate percentage-based scroll to account for potential
|
|
8146
|
-
// differences in zoom levels or page counts
|
|
8147
|
-
const scrollPercentageY = master.scrollTop / (master.scrollHeight - master.clientHeight);
|
|
8148
|
-
const scrollPercentageX = master.scrollLeft / (master.scrollWidth - master.clientWidth);
|
|
8149
|
-
|
|
8150
|
-
window.requestAnimationFrame(() => {
|
|
8151
|
-
slave.scrollTop = scrollPercentageY * (slave.scrollHeight - slave.clientHeight);
|
|
8152
|
-
slave.scrollLeft = scrollPercentageX * (slave.scrollWidth - slave.clientWidth);
|
|
8153
|
-
|
|
8154
|
-
// Reset the lock after the browser paint
|
|
8155
|
-
window.requestAnimationFrame(() => {
|
|
8156
|
-
this.isSyncing = false;
|
|
8157
|
-
});
|
|
8158
|
-
});
|
|
8159
|
-
}
|
|
8160
|
-
}
|
|
8161
|
-
|
|
8162
|
-
export { CoreViewer, pdf_download_controller as PdfDownloadController, pdf_sync_scroll_controller as PdfSyncScrollController, PdfViewer, pdf_viewer_controller as PdfViewerController, ToolMode, ViewerEvents };
|
|
8514
|
+
export { AnnotationStore, CoreViewer, MemoryAnnotationStore, pdf_download_controller as PdfDownloadController, PdfViewer, pdf_viewer_controller as PdfViewerController, RestAnnotationStore, ToolMode, ViewerEvents };
|
|
8163
8515
|
//# sourceMappingURL=stimulus-pdf-viewer.esm.js.map
|