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
|
@@ -1270,6 +1270,264 @@
|
|
|
1270
1270
|
}
|
|
1271
1271
|
}
|
|
1272
1272
|
|
|
1273
|
+
/**
|
|
1274
|
+
* Base class for annotation storage implementations.
|
|
1275
|
+
*
|
|
1276
|
+
* Subclasses must implement all methods to provide persistence for annotations.
|
|
1277
|
+
* The AnnotationManager delegates all storage operations to a store instance.
|
|
1278
|
+
*
|
|
1279
|
+
* @example
|
|
1280
|
+
* class MyCustomStore extends AnnotationStore {
|
|
1281
|
+
* async load() { return fetch('/my-api/annotations').then(r => r.json()) }
|
|
1282
|
+
* async create(data) { ... }
|
|
1283
|
+
* // ... etc
|
|
1284
|
+
* }
|
|
1285
|
+
*/
|
|
1286
|
+
class AnnotationStore {
|
|
1287
|
+
/**
|
|
1288
|
+
* Load all annotations.
|
|
1289
|
+
* @returns {Promise<Array>} Array of annotation objects
|
|
1290
|
+
*/
|
|
1291
|
+
async load() {
|
|
1292
|
+
throw new Error("AnnotationStore.load() not implemented")
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
/**
|
|
1296
|
+
* Create a new annotation.
|
|
1297
|
+
* @param {Object} data - Annotation data (without id)
|
|
1298
|
+
* @returns {Promise<Object>} Created annotation with server-assigned id
|
|
1299
|
+
*/
|
|
1300
|
+
async create(data) {
|
|
1301
|
+
throw new Error("AnnotationStore.create() not implemented")
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
/**
|
|
1305
|
+
* Update an existing annotation.
|
|
1306
|
+
* @param {string|number} id - Annotation id
|
|
1307
|
+
* @param {Object} data - Fields to update
|
|
1308
|
+
* @returns {Promise<Object>} Updated annotation
|
|
1309
|
+
*/
|
|
1310
|
+
async update(id, data) {
|
|
1311
|
+
throw new Error("AnnotationStore.update() not implemented")
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
/**
|
|
1315
|
+
* Delete an annotation.
|
|
1316
|
+
* @param {string|number} id - Annotation id
|
|
1317
|
+
* @returns {Promise<Object>} Deleted annotation
|
|
1318
|
+
*/
|
|
1319
|
+
async delete(id) {
|
|
1320
|
+
throw new Error("AnnotationStore.delete() not implemented")
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
/**
|
|
1324
|
+
* Restore a soft-deleted annotation.
|
|
1325
|
+
* @param {string|number} id - Annotation id
|
|
1326
|
+
* @returns {Promise<Object>} Restored annotation
|
|
1327
|
+
*/
|
|
1328
|
+
async restore(id) {
|
|
1329
|
+
throw new Error("AnnotationStore.restore() not implemented")
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
/**
|
|
1334
|
+
* REST API annotation store with configurable URL patterns.
|
|
1335
|
+
*
|
|
1336
|
+
* By default, uses Rails-style REST conventions:
|
|
1337
|
+
* - GET {baseUrl}.json - load all
|
|
1338
|
+
* - POST {baseUrl} - create
|
|
1339
|
+
* - PATCH {baseUrl}/{id} - update
|
|
1340
|
+
* - DELETE {baseUrl}/{id} - delete
|
|
1341
|
+
* - PATCH {baseUrl}/{id}/restore - restore
|
|
1342
|
+
*
|
|
1343
|
+
* URL patterns can be customized via function options:
|
|
1344
|
+
*
|
|
1345
|
+
* @example
|
|
1346
|
+
* // Rails default (just provide baseUrl)
|
|
1347
|
+
* new RestAnnotationStore({ baseUrl: '/documents/123/annotations' })
|
|
1348
|
+
*
|
|
1349
|
+
* @example
|
|
1350
|
+
* // Custom URL patterns
|
|
1351
|
+
* new RestAnnotationStore({
|
|
1352
|
+
* baseUrl: '/api/annotations',
|
|
1353
|
+
* loadUrl: () => '/api/annotations', // no .json suffix
|
|
1354
|
+
* updateUrl: (id) => `/api/annotations/${id}/edit`
|
|
1355
|
+
* })
|
|
1356
|
+
*
|
|
1357
|
+
* @example
|
|
1358
|
+
* // Fully custom URLs with closures
|
|
1359
|
+
* const docId = 123
|
|
1360
|
+
* new RestAnnotationStore({
|
|
1361
|
+
* loadUrl: () => `/api/v2/documents/${docId}/annotations`,
|
|
1362
|
+
* createUrl: () => `/api/v2/documents/${docId}/annotations`,
|
|
1363
|
+
* updateUrl: (id) => `/api/v2/annotations/${id}`,
|
|
1364
|
+
* deleteUrl: (id) => `/api/v2/annotations/${id}`,
|
|
1365
|
+
* restoreUrl: (id) => `/api/v2/annotations/${id}/restore`
|
|
1366
|
+
* })
|
|
1367
|
+
*/
|
|
1368
|
+
class RestAnnotationStore extends AnnotationStore {
|
|
1369
|
+
/**
|
|
1370
|
+
* @param {Object} options
|
|
1371
|
+
* @param {string} [options.baseUrl] - Base URL for Rails-style defaults
|
|
1372
|
+
* @param {Function} [options.loadUrl] - () => string - URL for loading annotations
|
|
1373
|
+
* @param {Function} [options.createUrl] - () => string - URL for creating annotations
|
|
1374
|
+
* @param {Function} [options.updateUrl] - (id) => string - URL for updating annotations
|
|
1375
|
+
* @param {Function} [options.deleteUrl] - (id) => string - URL for deleting annotations
|
|
1376
|
+
* @param {Function} [options.restoreUrl] - (id) => string - URL for restoring annotations
|
|
1377
|
+
*/
|
|
1378
|
+
constructor(options = {}) {
|
|
1379
|
+
super();
|
|
1380
|
+
this.baseUrl = options.baseUrl;
|
|
1381
|
+
|
|
1382
|
+
// Function-based URL builders with Rails-style defaults
|
|
1383
|
+
this.getLoadUrl = options.loadUrl || (() => `${this.baseUrl}.json`);
|
|
1384
|
+
this.getCreateUrl = options.createUrl || (() => this.baseUrl);
|
|
1385
|
+
this.getUpdateUrl = options.updateUrl || ((id) => `${this.baseUrl}/${id}`);
|
|
1386
|
+
this.getDeleteUrl = options.deleteUrl || ((id) => `${this.baseUrl}/${id}`);
|
|
1387
|
+
this.getRestoreUrl = options.restoreUrl || ((id) => `${this.baseUrl}/${id}/restore`);
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
async load() {
|
|
1391
|
+
const request = new request_js.FetchRequest("get", this.getLoadUrl());
|
|
1392
|
+
const response = await request.perform();
|
|
1393
|
+
|
|
1394
|
+
if (response.ok) {
|
|
1395
|
+
return await response.json
|
|
1396
|
+
} else {
|
|
1397
|
+
throw new Error("Failed to load annotations")
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
async create(data) {
|
|
1402
|
+
const request = new request_js.FetchRequest("post", this.getCreateUrl(), {
|
|
1403
|
+
body: JSON.stringify({ annotation: data }),
|
|
1404
|
+
contentType: "application/json",
|
|
1405
|
+
responseKind: "json"
|
|
1406
|
+
});
|
|
1407
|
+
|
|
1408
|
+
const response = await request.perform();
|
|
1409
|
+
|
|
1410
|
+
if (response.ok) {
|
|
1411
|
+
return await response.json
|
|
1412
|
+
} else {
|
|
1413
|
+
throw new Error("Failed to create annotation")
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
async update(id, data) {
|
|
1418
|
+
const request = new request_js.FetchRequest("patch", this.getUpdateUrl(id), {
|
|
1419
|
+
body: JSON.stringify({ annotation: data }),
|
|
1420
|
+
contentType: "application/json",
|
|
1421
|
+
responseKind: "json"
|
|
1422
|
+
});
|
|
1423
|
+
|
|
1424
|
+
const response = await request.perform();
|
|
1425
|
+
|
|
1426
|
+
if (response.ok) {
|
|
1427
|
+
return await response.json
|
|
1428
|
+
} else {
|
|
1429
|
+
throw new Error("Failed to update annotation")
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
async delete(id) {
|
|
1434
|
+
const request = new request_js.FetchRequest("delete", this.getDeleteUrl(id), {
|
|
1435
|
+
responseKind: "json"
|
|
1436
|
+
});
|
|
1437
|
+
|
|
1438
|
+
const response = await request.perform();
|
|
1439
|
+
|
|
1440
|
+
if (response.ok) {
|
|
1441
|
+
return await response.json
|
|
1442
|
+
} else {
|
|
1443
|
+
throw new Error("Failed to delete annotation")
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
async restore(id) {
|
|
1448
|
+
const request = new request_js.FetchRequest("patch", this.getRestoreUrl(id), {
|
|
1449
|
+
responseKind: "json"
|
|
1450
|
+
});
|
|
1451
|
+
|
|
1452
|
+
const response = await request.perform();
|
|
1453
|
+
|
|
1454
|
+
if (response.ok) {
|
|
1455
|
+
return await response.json
|
|
1456
|
+
} else {
|
|
1457
|
+
throw new Error("Failed to restore annotation")
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
/**
|
|
1463
|
+
* In-memory annotation store for development and demo purposes.
|
|
1464
|
+
*
|
|
1465
|
+
* Annotations are stored in memory only and lost on page refresh.
|
|
1466
|
+
* Useful for:
|
|
1467
|
+
* - Local development without a backend
|
|
1468
|
+
* - Demo/preview modes
|
|
1469
|
+
* - Testing
|
|
1470
|
+
*
|
|
1471
|
+
* @example
|
|
1472
|
+
* new MemoryAnnotationStore()
|
|
1473
|
+
*/
|
|
1474
|
+
class MemoryAnnotationStore extends AnnotationStore {
|
|
1475
|
+
constructor() {
|
|
1476
|
+
super();
|
|
1477
|
+
this._annotations = [];
|
|
1478
|
+
this._nextId = 1;
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
async load() {
|
|
1482
|
+
return [...this._annotations]
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
async create(data) {
|
|
1486
|
+
const annotation = {
|
|
1487
|
+
...data,
|
|
1488
|
+
id: `local-${this._nextId++}`,
|
|
1489
|
+
created_at: new Date().toISOString(),
|
|
1490
|
+
updated_at: new Date().toISOString()
|
|
1491
|
+
};
|
|
1492
|
+
|
|
1493
|
+
this._annotations.push(annotation);
|
|
1494
|
+
return annotation
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
async update(id, data) {
|
|
1498
|
+
const index = this._annotations.findIndex(a => a.id === id);
|
|
1499
|
+
if (index === -1) {
|
|
1500
|
+
throw new Error("Annotation not found")
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
const annotation = {
|
|
1504
|
+
...this._annotations[index],
|
|
1505
|
+
...data,
|
|
1506
|
+
id, // Preserve original id
|
|
1507
|
+
updated_at: new Date().toISOString()
|
|
1508
|
+
};
|
|
1509
|
+
|
|
1510
|
+
this._annotations[index] = annotation;
|
|
1511
|
+
return annotation
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
async delete(id) {
|
|
1515
|
+
const index = this._annotations.findIndex(a => a.id === id);
|
|
1516
|
+
if (index === -1) {
|
|
1517
|
+
throw new Error("Annotation not found")
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
const [annotation] = this._annotations.splice(index, 1);
|
|
1521
|
+
return annotation
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
async restore(id) {
|
|
1525
|
+
// Memory store doesn't support soft-delete/restore
|
|
1526
|
+
console.warn("MemoryAnnotationStore.restore() is not supported");
|
|
1527
|
+
return null
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1273
1531
|
// Custom event types for error handling
|
|
1274
1532
|
const AnnotationErrorType = {
|
|
1275
1533
|
LOAD_FAILED: "load_failed",
|
|
@@ -1280,13 +1538,31 @@
|
|
|
1280
1538
|
};
|
|
1281
1539
|
|
|
1282
1540
|
class AnnotationManager {
|
|
1541
|
+
/**
|
|
1542
|
+
* @param {Object} options
|
|
1543
|
+
* @param {AnnotationStore} [options.store] - Custom store implementation
|
|
1544
|
+
* @param {string} [options.annotationsUrl] - Base URL for REST store (creates RestAnnotationStore)
|
|
1545
|
+
* @param {number} [options.documentId] - Document ID
|
|
1546
|
+
* @param {Function} [options.onAnnotationCreated] - Callback when annotation created
|
|
1547
|
+
* @param {Function} [options.onAnnotationUpdated] - Callback when annotation updated
|
|
1548
|
+
* @param {Function} [options.onAnnotationDeleted] - Callback when annotation deleted
|
|
1549
|
+
* @param {Element} [options.eventTarget] - Element for dispatching error events
|
|
1550
|
+
*/
|
|
1283
1551
|
constructor(options = {}) {
|
|
1284
|
-
this.annotationsUrl = options.annotationsUrl;
|
|
1285
1552
|
this.documentId = options.documentId;
|
|
1286
1553
|
this.onAnnotationCreated = options.onAnnotationCreated;
|
|
1287
1554
|
this.onAnnotationUpdated = options.onAnnotationUpdated;
|
|
1288
1555
|
this.onAnnotationDeleted = options.onAnnotationDeleted;
|
|
1289
|
-
this.eventTarget = options.eventTarget;
|
|
1556
|
+
this.eventTarget = options.eventTarget;
|
|
1557
|
+
|
|
1558
|
+
// Determine store: explicit > REST URL > memory
|
|
1559
|
+
if (options.store) {
|
|
1560
|
+
this.store = options.store;
|
|
1561
|
+
} else if (options.annotationsUrl) {
|
|
1562
|
+
this.store = new RestAnnotationStore({ baseUrl: options.annotationsUrl });
|
|
1563
|
+
} else {
|
|
1564
|
+
this.store = new MemoryAnnotationStore();
|
|
1565
|
+
}
|
|
1290
1566
|
|
|
1291
1567
|
this.annotations = new Map(); // id -> annotation
|
|
1292
1568
|
this.annotationsByPage = new Map(); // pageNumber -> [annotations]
|
|
@@ -1311,15 +1587,8 @@
|
|
|
1311
1587
|
|
|
1312
1588
|
async loadAnnotations() {
|
|
1313
1589
|
try {
|
|
1314
|
-
const
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
if (response.ok) {
|
|
1318
|
-
const data = await response.json;
|
|
1319
|
-
this._processAnnotations(data);
|
|
1320
|
-
} else {
|
|
1321
|
-
throw new Error("Server returned an error")
|
|
1322
|
-
}
|
|
1590
|
+
const annotations = await this.store.load();
|
|
1591
|
+
this._processAnnotations(annotations);
|
|
1323
1592
|
} catch (error) {
|
|
1324
1593
|
console.error("Failed to load annotations:", error);
|
|
1325
1594
|
this._dispatchError(AnnotationErrorType.LOAD_FAILED, "Failed to load annotations", error);
|
|
@@ -1355,26 +1624,14 @@
|
|
|
1355
1624
|
|
|
1356
1625
|
async createAnnotation(data) {
|
|
1357
1626
|
try {
|
|
1358
|
-
const
|
|
1359
|
-
|
|
1360
|
-
contentType: "application/json",
|
|
1361
|
-
responseKind: "json"
|
|
1362
|
-
});
|
|
1363
|
-
|
|
1364
|
-
const response = await request.perform();
|
|
1365
|
-
|
|
1366
|
-
if (response.ok) {
|
|
1367
|
-
const annotation = await response.json;
|
|
1368
|
-
this._addAnnotation(annotation);
|
|
1369
|
-
|
|
1370
|
-
if (this.onAnnotationCreated) {
|
|
1371
|
-
this.onAnnotationCreated(annotation);
|
|
1372
|
-
}
|
|
1627
|
+
const annotation = await this.store.create(data);
|
|
1628
|
+
this._addAnnotation(annotation);
|
|
1373
1629
|
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
throw new Error("Failed to create annotation")
|
|
1630
|
+
if (this.onAnnotationCreated) {
|
|
1631
|
+
this.onAnnotationCreated(annotation);
|
|
1377
1632
|
}
|
|
1633
|
+
|
|
1634
|
+
return annotation
|
|
1378
1635
|
} catch (error) {
|
|
1379
1636
|
console.error("Failed to create annotation:", error);
|
|
1380
1637
|
this._dispatchError(AnnotationErrorType.CREATE_FAILED, "Failed to save annotation", error);
|
|
@@ -1384,26 +1641,14 @@
|
|
|
1384
1641
|
|
|
1385
1642
|
async updateAnnotation(id, data) {
|
|
1386
1643
|
try {
|
|
1387
|
-
const
|
|
1388
|
-
|
|
1389
|
-
contentType: "application/json",
|
|
1390
|
-
responseKind: "json"
|
|
1391
|
-
});
|
|
1644
|
+
const annotation = await this.store.update(id, data);
|
|
1645
|
+
this._updateAnnotation(annotation);
|
|
1392
1646
|
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
if (response.ok) {
|
|
1396
|
-
const annotation = await response.json;
|
|
1397
|
-
this._updateAnnotation(annotation);
|
|
1398
|
-
|
|
1399
|
-
if (this.onAnnotationUpdated) {
|
|
1400
|
-
this.onAnnotationUpdated(annotation);
|
|
1401
|
-
}
|
|
1402
|
-
|
|
1403
|
-
return annotation
|
|
1404
|
-
} else {
|
|
1405
|
-
throw new Error("Failed to update annotation")
|
|
1647
|
+
if (this.onAnnotationUpdated) {
|
|
1648
|
+
this.onAnnotationUpdated(annotation);
|
|
1406
1649
|
}
|
|
1650
|
+
|
|
1651
|
+
return annotation
|
|
1407
1652
|
} catch (error) {
|
|
1408
1653
|
console.error("Failed to update annotation:", error);
|
|
1409
1654
|
this._dispatchError(AnnotationErrorType.UPDATE_FAILED, "Failed to update annotation", error);
|
|
@@ -1412,26 +1657,18 @@
|
|
|
1412
1657
|
}
|
|
1413
1658
|
|
|
1414
1659
|
async deleteAnnotation(id) {
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
if (!annotation) return
|
|
1660
|
+
const existingAnnotation = this.annotations.get(id);
|
|
1661
|
+
if (!existingAnnotation) return
|
|
1418
1662
|
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
const response = await request.perform();
|
|
1423
|
-
|
|
1424
|
-
if (response.ok) {
|
|
1425
|
-
this._removeAnnotation(id);
|
|
1426
|
-
|
|
1427
|
-
if (this.onAnnotationDeleted) {
|
|
1428
|
-
this.onAnnotationDeleted(annotation);
|
|
1429
|
-
}
|
|
1663
|
+
try {
|
|
1664
|
+
const annotation = await this.store.delete(id);
|
|
1665
|
+
this._removeAnnotation(id);
|
|
1430
1666
|
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
throw new Error("Failed to delete annotation")
|
|
1667
|
+
if (this.onAnnotationDeleted) {
|
|
1668
|
+
this.onAnnotationDeleted(existingAnnotation);
|
|
1434
1669
|
}
|
|
1670
|
+
|
|
1671
|
+
return existingAnnotation
|
|
1435
1672
|
} catch (error) {
|
|
1436
1673
|
console.error("Failed to delete annotation:", error);
|
|
1437
1674
|
this._dispatchError(AnnotationErrorType.DELETE_FAILED, "Failed to delete annotation", error);
|
|
@@ -1441,23 +1678,16 @@
|
|
|
1441
1678
|
|
|
1442
1679
|
async restoreAnnotation(id) {
|
|
1443
1680
|
try {
|
|
1444
|
-
const
|
|
1445
|
-
|
|
1446
|
-
});
|
|
1447
|
-
const response = await request.perform();
|
|
1448
|
-
|
|
1449
|
-
if (response.ok) {
|
|
1450
|
-
const annotation = await response.json;
|
|
1451
|
-
this._addAnnotation(annotation);
|
|
1681
|
+
const annotation = await this.store.restore(id);
|
|
1682
|
+
if (!annotation) return null
|
|
1452
1683
|
|
|
1453
|
-
|
|
1454
|
-
this.onAnnotationCreated(annotation);
|
|
1455
|
-
}
|
|
1684
|
+
this._addAnnotation(annotation);
|
|
1456
1685
|
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
throw new Error("Failed to restore annotation")
|
|
1686
|
+
if (this.onAnnotationCreated) {
|
|
1687
|
+
this.onAnnotationCreated(annotation);
|
|
1460
1688
|
}
|
|
1689
|
+
|
|
1690
|
+
return annotation
|
|
1461
1691
|
} catch (error) {
|
|
1462
1692
|
console.error("Failed to restore annotation:", error);
|
|
1463
1693
|
this._dispatchError(AnnotationErrorType.RESTORE_FAILED, "Failed to restore annotation", error);
|
|
@@ -2130,6 +2360,11 @@
|
|
|
2130
2360
|
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
|
|
2131
2361
|
</svg>`,
|
|
2132
2362
|
|
|
2363
|
+
// Comment/Speech bubble icon - used in annotation edit toolbar
|
|
2364
|
+
comment: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
2365
|
+
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
|
|
2366
|
+
</svg>`,
|
|
2367
|
+
|
|
2133
2368
|
// Chevron down - used in color pickers, dropdowns
|
|
2134
2369
|
chevronDown: `<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
|
2135
2370
|
<polyline points="6 9 12 15 18 9"/>
|
|
@@ -2264,6 +2499,7 @@
|
|
|
2264
2499
|
this.onColorChange = options.onColorChange;
|
|
2265
2500
|
this.onDelete = options.onDelete;
|
|
2266
2501
|
this.onEdit = options.onEdit;
|
|
2502
|
+
this.onComment = options.onComment;
|
|
2267
2503
|
this.onDeselect = options.onDeselect;
|
|
2268
2504
|
this.colors = options.colors || ColorPicker.COLORS.map(c => c.value);
|
|
2269
2505
|
|
|
@@ -2280,6 +2516,9 @@
|
|
|
2280
2516
|
this.element.className = "annotation-edit-toolbar hidden";
|
|
2281
2517
|
this.element.innerHTML = `
|
|
2282
2518
|
<div class="toolbar-buttons">
|
|
2519
|
+
<button class="toolbar-btn comment-btn hidden" title="Add Comment (C)">
|
|
2520
|
+
${Icons.comment}
|
|
2521
|
+
</button>
|
|
2283
2522
|
<button class="color-picker-btn" title="Change color" aria-haspopup="true" aria-expanded="false">
|
|
2284
2523
|
<span class="color-swatch"></span>
|
|
2285
2524
|
${Icons.chevronDown}
|
|
@@ -2299,11 +2538,12 @@
|
|
|
2299
2538
|
${Icons.delete}
|
|
2300
2539
|
</button>
|
|
2301
2540
|
</div>
|
|
2302
|
-
<div class="toolbar-
|
|
2541
|
+
<div class="toolbar-annotation-content hidden"></div>
|
|
2303
2542
|
`;
|
|
2304
2543
|
|
|
2544
|
+
this.commentBtn = this.element.querySelector(".comment-btn");
|
|
2305
2545
|
this.editBtn = this.element.querySelector(".edit-btn");
|
|
2306
|
-
this.
|
|
2546
|
+
this.annotationContent = this.element.querySelector(".toolbar-annotation-content");
|
|
2307
2547
|
}
|
|
2308
2548
|
|
|
2309
2549
|
_setupEventListeners() {
|
|
@@ -2324,7 +2564,15 @@
|
|
|
2324
2564
|
});
|
|
2325
2565
|
});
|
|
2326
2566
|
|
|
2327
|
-
//
|
|
2567
|
+
// Comment button (for highlight/underline/ink annotations)
|
|
2568
|
+
this.commentBtn.addEventListener("click", (e) => {
|
|
2569
|
+
e.stopPropagation();
|
|
2570
|
+
if (this.currentAnnotation && this.onComment) {
|
|
2571
|
+
this.onComment(this.currentAnnotation);
|
|
2572
|
+
}
|
|
2573
|
+
});
|
|
2574
|
+
|
|
2575
|
+
// Edit button (for notes)
|
|
2328
2576
|
this.editBtn.addEventListener("click", (e) => {
|
|
2329
2577
|
e.stopPropagation();
|
|
2330
2578
|
if (this.currentAnnotation && this.onEdit) {
|
|
@@ -2377,6 +2625,13 @@
|
|
|
2377
2625
|
e.preventDefault();
|
|
2378
2626
|
this.onEdit(this.currentAnnotation);
|
|
2379
2627
|
}
|
|
2628
|
+
} else if (e.key === "c" || e.key === "C") {
|
|
2629
|
+
// Comment shortcut for highlight/underline/ink annotations
|
|
2630
|
+
const supportsComment = ["highlight", "line", "ink"].includes(this.currentAnnotation?.annotation_type);
|
|
2631
|
+
if (supportsComment && this.onComment) {
|
|
2632
|
+
e.preventDefault();
|
|
2633
|
+
this.onComment(this.currentAnnotation);
|
|
2634
|
+
}
|
|
2380
2635
|
}
|
|
2381
2636
|
});
|
|
2382
2637
|
}
|
|
@@ -2432,15 +2687,26 @@
|
|
|
2432
2687
|
const color = annotation.color || ColorPicker.DEFAULT_HIGHLIGHT_COLOR;
|
|
2433
2688
|
this._updateSelectedColor(color);
|
|
2434
2689
|
|
|
2435
|
-
// Show/hide
|
|
2690
|
+
// Show/hide buttons based on annotation type
|
|
2436
2691
|
const isNote = annotation.annotation_type === "note";
|
|
2692
|
+
const supportsComment = ["highlight", "line", "ink"].includes(annotation.annotation_type);
|
|
2693
|
+
|
|
2694
|
+
// Comment button for highlight/underline/ink, edit button for notes
|
|
2695
|
+
this.commentBtn.classList.toggle("hidden", !supportsComment);
|
|
2437
2696
|
this.editBtn.classList.toggle("hidden", !isNote);
|
|
2438
2697
|
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2698
|
+
// Update comment button title based on whether contents exists
|
|
2699
|
+
if (supportsComment) {
|
|
2700
|
+
const hasComment = annotation.contents && annotation.contents.trim();
|
|
2701
|
+
this.commentBtn.title = hasComment ? "Edit Comment (C)" : "Add Comment (C)";
|
|
2702
|
+
}
|
|
2703
|
+
|
|
2704
|
+
// Show contents for any annotation type that has it
|
|
2705
|
+
if (annotation.contents) {
|
|
2706
|
+
this.annotationContent.textContent = annotation.contents;
|
|
2707
|
+
this.annotationContent.classList.remove("hidden");
|
|
2442
2708
|
} else {
|
|
2443
|
-
this.
|
|
2709
|
+
this.annotationContent.classList.add("hidden");
|
|
2444
2710
|
}
|
|
2445
2711
|
|
|
2446
2712
|
// Determine if toolbar should flip above the annotation
|
|
@@ -2461,9 +2727,9 @@
|
|
|
2461
2727
|
this.element.classList.add("hidden");
|
|
2462
2728
|
this.currentAnnotation = null;
|
|
2463
2729
|
|
|
2464
|
-
// Clear
|
|
2465
|
-
this.
|
|
2466
|
-
this.
|
|
2730
|
+
// Clear annotation content
|
|
2731
|
+
this.annotationContent.textContent = "";
|
|
2732
|
+
this.annotationContent.classList.add("hidden");
|
|
2467
2733
|
|
|
2468
2734
|
// Remove from parent when hidden
|
|
2469
2735
|
if (this.element.parentNode) {
|
|
@@ -3157,10 +3423,10 @@
|
|
|
3157
3423
|
};
|
|
3158
3424
|
|
|
3159
3425
|
class AnnotationSidebar {
|
|
3160
|
-
constructor({ container, annotationManager, onAnnotationClick }) {
|
|
3161
|
-
this.container = container;
|
|
3426
|
+
constructor({ element, itemTemplate, container, annotationManager, onAnnotationClick }) {
|
|
3162
3427
|
this.annotationManager = annotationManager;
|
|
3163
3428
|
this.onAnnotationClick = onAnnotationClick;
|
|
3429
|
+
this.itemTemplate = itemTemplate; // Optional <template> element for custom list items
|
|
3164
3430
|
|
|
3165
3431
|
this.isOpen = false;
|
|
3166
3432
|
this.sidebarWidth = SIDEBAR_DEFAULT_WIDTH;
|
|
@@ -3168,7 +3434,30 @@
|
|
|
3168
3434
|
this.filterType = FilterType.ALL;
|
|
3169
3435
|
this.selectedAnnotationId = null;
|
|
3170
3436
|
|
|
3171
|
-
|
|
3437
|
+
if (element) {
|
|
3438
|
+
// User provided HTML - find elements via data attributes
|
|
3439
|
+
this.element = element;
|
|
3440
|
+
this.container = element.parentElement;
|
|
3441
|
+
this.listContainer = element.querySelector('[data-role="list"]');
|
|
3442
|
+
this.header = element.querySelector('.pdf-sidebar-header');
|
|
3443
|
+
this.emptyState = element.querySelector('[data-role="empty-state"]');
|
|
3444
|
+
this.sortControls = element.querySelector('[data-role="sort-controls"]');
|
|
3445
|
+
this.filterControls = element.querySelector('[data-role="filter-controls"]');
|
|
3446
|
+
this.resizer = element.querySelector('[data-role="resizer"]');
|
|
3447
|
+
|
|
3448
|
+
// Read initial width from CSS variable if set
|
|
3449
|
+
const currentWidth = element.style.getPropertyValue('--sidebar-width');
|
|
3450
|
+
if (currentWidth) {
|
|
3451
|
+
this.sidebarWidth = parseInt(currentWidth, 10) || SIDEBAR_DEFAULT_WIDTH;
|
|
3452
|
+
} else {
|
|
3453
|
+
element.style.setProperty('--sidebar-width', `${this.sidebarWidth}px`);
|
|
3454
|
+
}
|
|
3455
|
+
} else {
|
|
3456
|
+
// Fallback - create default HTML (existing behavior)
|
|
3457
|
+
this.container = container;
|
|
3458
|
+
this._createElements();
|
|
3459
|
+
}
|
|
3460
|
+
|
|
3172
3461
|
this._setupEventListeners();
|
|
3173
3462
|
}
|
|
3174
3463
|
|
|
@@ -3251,27 +3540,35 @@
|
|
|
3251
3540
|
}
|
|
3252
3541
|
|
|
3253
3542
|
_setupEventListeners() {
|
|
3254
|
-
// Close button
|
|
3255
|
-
const closeBtn = this.header
|
|
3256
|
-
|
|
3543
|
+
// Close button - support both user HTML (data-action="close") and auto-generated (.pdf-sidebar-close)
|
|
3544
|
+
const closeBtn = this.header?.querySelector('[data-action="close"]') ||
|
|
3545
|
+
this.header?.querySelector(".pdf-sidebar-close");
|
|
3546
|
+
if (closeBtn) {
|
|
3547
|
+
closeBtn.addEventListener("click", () => this.close());
|
|
3548
|
+
}
|
|
3257
3549
|
|
|
3258
3550
|
// Sort buttons
|
|
3259
|
-
this.sortControls
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3551
|
+
if (this.sortControls) {
|
|
3552
|
+
this.sortControls.addEventListener("click", (e) => {
|
|
3553
|
+
const btn = e.target.closest("[data-sort]") || e.target.closest(".sort-btn");
|
|
3554
|
+
if (btn && btn.dataset.sort) {
|
|
3555
|
+
this.sortMode = btn.dataset.sort;
|
|
3556
|
+
this.sortControls.querySelectorAll("[data-sort], .sort-btn").forEach(b => b.classList.remove("active"));
|
|
3557
|
+
btn.classList.add("active");
|
|
3558
|
+
this._refreshList();
|
|
3559
|
+
}
|
|
3560
|
+
});
|
|
3561
|
+
}
|
|
3268
3562
|
|
|
3269
|
-
// Filter select
|
|
3270
|
-
const filterSelect = this.filterControls
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3563
|
+
// Filter select - support both user HTML (data-action="filter") and auto-generated (.annotation-filter-select)
|
|
3564
|
+
const filterSelect = this.filterControls?.querySelector('[data-action="filter"]') ||
|
|
3565
|
+
this.filterControls?.querySelector(".annotation-filter-select");
|
|
3566
|
+
if (filterSelect) {
|
|
3567
|
+
filterSelect.addEventListener("change", (e) => {
|
|
3568
|
+
this.filterType = e.target.value;
|
|
3569
|
+
this._refreshList();
|
|
3570
|
+
});
|
|
3571
|
+
}
|
|
3275
3572
|
|
|
3276
3573
|
// Sidebar resizing
|
|
3277
3574
|
this._setupResizer();
|
|
@@ -3283,6 +3580,8 @@
|
|
|
3283
3580
|
}
|
|
3284
3581
|
|
|
3285
3582
|
_setupResizer() {
|
|
3583
|
+
if (!this.resizer) return
|
|
3584
|
+
|
|
3286
3585
|
let startX, startWidth;
|
|
3287
3586
|
|
|
3288
3587
|
const onMouseMove = (e) => {
|
|
@@ -3366,16 +3665,19 @@
|
|
|
3366
3665
|
// Clear and rebuild list
|
|
3367
3666
|
this.listContainer.innerHTML = "";
|
|
3368
3667
|
|
|
3369
|
-
// Update count badge
|
|
3370
|
-
const countBadge = this.header
|
|
3371
|
-
|
|
3668
|
+
// Update count badge - support both user HTML (data-role="count") and auto-generated
|
|
3669
|
+
const countBadge = this.header?.querySelector('[data-role="count"]') ||
|
|
3670
|
+
this.header?.querySelector(".annotation-count-badge");
|
|
3671
|
+
if (countBadge) {
|
|
3672
|
+
countBadge.textContent = annotations.length;
|
|
3673
|
+
}
|
|
3372
3674
|
|
|
3373
3675
|
// Show empty state or list
|
|
3374
3676
|
if (annotations.length === 0) {
|
|
3375
|
-
this.emptyState
|
|
3677
|
+
this.emptyState?.classList.add("visible");
|
|
3376
3678
|
this.listContainer.classList.add("empty");
|
|
3377
3679
|
} else {
|
|
3378
|
-
this.emptyState
|
|
3680
|
+
this.emptyState?.classList.remove("visible");
|
|
3379
3681
|
this.listContainer.classList.remove("empty");
|
|
3380
3682
|
|
|
3381
3683
|
for (const annotation of annotations) {
|
|
@@ -3444,40 +3746,71 @@
|
|
|
3444
3746
|
}
|
|
3445
3747
|
|
|
3446
3748
|
_createListItem(annotation) {
|
|
3447
|
-
|
|
3448
|
-
item.className = "annotation-list-item";
|
|
3449
|
-
item.dataset.annotationId = annotation.id;
|
|
3450
|
-
item.tabIndex = 0;
|
|
3749
|
+
let item;
|
|
3451
3750
|
|
|
3452
|
-
if (this.
|
|
3453
|
-
|
|
3454
|
-
|
|
3751
|
+
if (this.itemTemplate) {
|
|
3752
|
+
// Clone user's template and populate data-field elements
|
|
3753
|
+
item = this.itemTemplate.content.firstElementChild.cloneNode(true);
|
|
3754
|
+
item.dataset.annotationId = annotation.id;
|
|
3455
3755
|
|
|
3456
|
-
|
|
3457
|
-
|
|
3756
|
+
// Ensure tabIndex for keyboard navigation
|
|
3757
|
+
if (!item.hasAttribute("tabindex")) {
|
|
3758
|
+
item.tabIndex = 0;
|
|
3759
|
+
}
|
|
3458
3760
|
|
|
3459
|
-
|
|
3460
|
-
|
|
3761
|
+
// Determine display values
|
|
3762
|
+
const { icon, label, typeLabel } = this._getAnnotationDisplay(annotation);
|
|
3763
|
+
const timestamp = this._formatTimestamp(annotation.created_at);
|
|
3764
|
+
|
|
3765
|
+
// Populate data-field elements
|
|
3766
|
+
this._setField(item, "icon", icon, annotation.color);
|
|
3767
|
+
this._setField(item, "label", this._escapeHtml(label));
|
|
3768
|
+
this._setField(item, "type", typeLabel);
|
|
3769
|
+
this._setField(item, "page", `Page ${annotation.page}`);
|
|
3770
|
+
this._setField(item, "time", timestamp);
|
|
3771
|
+
|
|
3772
|
+
// Also set data attributes for user's Stimulus controllers
|
|
3773
|
+
item.dataset.annotationType = annotation.annotation_type;
|
|
3774
|
+
item.dataset.annotationPage = annotation.page;
|
|
3775
|
+
item.dataset.annotationColor = annotation.color || "";
|
|
3776
|
+
} else {
|
|
3777
|
+
// Fallback - existing innerHTML approach
|
|
3778
|
+
item = document.createElement("div");
|
|
3779
|
+
item.className = "annotation-list-item";
|
|
3780
|
+
item.dataset.annotationId = annotation.id;
|
|
3781
|
+
item.tabIndex = 0;
|
|
3461
3782
|
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
<span class="annotation-item-page">Page ${annotation.page}</span>
|
|
3472
|
-
<span class="annotation-item-separator">•</span>
|
|
3473
|
-
<span class="annotation-item-time">${timestamp}</span>
|
|
3783
|
+
// Determine icon and label based on type
|
|
3784
|
+
const { icon, label, typeLabel } = this._getAnnotationDisplay(annotation);
|
|
3785
|
+
|
|
3786
|
+
// Format timestamp
|
|
3787
|
+
const timestamp = this._formatTimestamp(annotation.created_at);
|
|
3788
|
+
|
|
3789
|
+
item.innerHTML = `
|
|
3790
|
+
<div class="annotation-item-icon" style="color: ${annotation.color || '#666'}">
|
|
3791
|
+
${icon}
|
|
3474
3792
|
</div>
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3793
|
+
<div class="annotation-item-content">
|
|
3794
|
+
<div class="annotation-item-label">${this._escapeHtml(label)}</div>
|
|
3795
|
+
<div class="annotation-item-meta">
|
|
3796
|
+
<span class="annotation-item-type">${typeLabel}</span>
|
|
3797
|
+
<span class="annotation-item-separator">•</span>
|
|
3798
|
+
<span class="annotation-item-page">Page ${annotation.page}</span>
|
|
3799
|
+
<span class="annotation-item-separator">•</span>
|
|
3800
|
+
<span class="annotation-item-time">${timestamp}</span>
|
|
3801
|
+
</div>
|
|
3802
|
+
</div>
|
|
3803
|
+
<div class="annotation-item-hover">
|
|
3804
|
+
<span>Jump</span>
|
|
3805
|
+
${Icons.chevronRight}
|
|
3806
|
+
</div>
|
|
3807
|
+
`;
|
|
3808
|
+
}
|
|
3809
|
+
|
|
3810
|
+
// Selection state
|
|
3811
|
+
if (this.selectedAnnotationId === annotation.id) {
|
|
3812
|
+
item.classList.add("selected");
|
|
3813
|
+
}
|
|
3481
3814
|
|
|
3482
3815
|
// Click handler
|
|
3483
3816
|
item.addEventListener("click", () => {
|
|
@@ -3490,6 +3823,23 @@
|
|
|
3490
3823
|
return item
|
|
3491
3824
|
}
|
|
3492
3825
|
|
|
3826
|
+
/**
|
|
3827
|
+
* Set a field value in a template-cloned element
|
|
3828
|
+
* @param {HTMLElement} element - The cloned template element
|
|
3829
|
+
* @param {string} fieldName - The data-field name to find
|
|
3830
|
+
* @param {string} value - The value to set (can include HTML for icons)
|
|
3831
|
+
* @param {string} color - Optional color to apply
|
|
3832
|
+
*/
|
|
3833
|
+
_setField(element, fieldName, value, color) {
|
|
3834
|
+
const field = element.querySelector(`[data-field="${fieldName}"]`);
|
|
3835
|
+
if (field) {
|
|
3836
|
+
field.innerHTML = value;
|
|
3837
|
+
if (color && fieldName === "icon") {
|
|
3838
|
+
field.style.color = color;
|
|
3839
|
+
}
|
|
3840
|
+
}
|
|
3841
|
+
}
|
|
3842
|
+
|
|
3493
3843
|
_getAnnotationDisplay(annotation) {
|
|
3494
3844
|
const type = annotation.annotation_type;
|
|
3495
3845
|
let icon, label, typeLabel;
|
|
@@ -4864,15 +5214,25 @@
|
|
|
4864
5214
|
for (let i = 0; i < result.length; i++) {
|
|
4865
5215
|
const existing = result[i];
|
|
4866
5216
|
|
|
4867
|
-
//
|
|
4868
|
-
const
|
|
5217
|
+
// Calculate vertical overlap amount
|
|
5218
|
+
const overlapTop = Math.max(rect.top, existing.top);
|
|
5219
|
+
const overlapBottom = Math.min(rect.bottom, existing.bottom);
|
|
5220
|
+
const overlapHeight = Math.max(0, overlapBottom - overlapTop);
|
|
5221
|
+
|
|
5222
|
+
// Require significant vertical overlap (at least 50% of smaller rect's height)
|
|
5223
|
+
// This prevents merging rects from different lines that only slightly overlap
|
|
5224
|
+
const rectHeight = rect.bottom - rect.top;
|
|
5225
|
+
const existingHeight = existing.bottom - existing.top;
|
|
5226
|
+
const minHeight = Math.min(rectHeight, existingHeight);
|
|
5227
|
+
const significantVerticalOverlap = overlapHeight > minHeight * 0.5;
|
|
5228
|
+
|
|
4869
5229
|
const horizontalOverlap = rect.left < existing.right && rect.right > existing.left;
|
|
4870
5230
|
|
|
4871
5231
|
// Also merge if they're adjacent horizontally on same line
|
|
4872
5232
|
const sameLine = Math.abs(rect.top - existing.top) < 3 && Math.abs(rect.bottom - existing.bottom) < 3;
|
|
4873
5233
|
const horizontallyAdjacent = Math.abs(rect.left - existing.right) < 2 || Math.abs(existing.left - rect.right) < 2;
|
|
4874
5234
|
|
|
4875
|
-
if ((
|
|
5235
|
+
if ((significantVerticalOverlap && horizontalOverlap) || (sameLine && horizontallyAdjacent)) {
|
|
4876
5236
|
// Merge: extend existing rect to encompass both
|
|
4877
5237
|
result[i] = {
|
|
4878
5238
|
left: Math.min(existing.left, rect.left),
|
|
@@ -5837,12 +6197,12 @@
|
|
|
5837
6197
|
});
|
|
5838
6198
|
}
|
|
5839
6199
|
|
|
5840
|
-
// Method to edit an existing note
|
|
6200
|
+
// Method to edit an existing note or add/edit a comment on other annotation types
|
|
5841
6201
|
editNote(annotation) {
|
|
5842
6202
|
// Store currently focused element for restoration on close
|
|
5843
6203
|
this._previousFocusElement = document.activeElement;
|
|
5844
6204
|
|
|
5845
|
-
// Get the position of the
|
|
6205
|
+
// Get the position of the annotation on screen
|
|
5846
6206
|
const pageContainer = this.viewer.getPageContainer(annotation.page);
|
|
5847
6207
|
if (!pageContainer) return
|
|
5848
6208
|
|
|
@@ -5854,10 +6214,20 @@
|
|
|
5854
6214
|
// Store the annotation being edited
|
|
5855
6215
|
this.editingAnnotation = annotation;
|
|
5856
6216
|
|
|
5857
|
-
|
|
6217
|
+
// Determine dialog title based on annotation type and whether contents exists
|
|
6218
|
+
const isNote = annotation.annotation_type === "note";
|
|
6219
|
+
const hasContents = annotation.contents && annotation.contents.trim();
|
|
6220
|
+
let dialogTitle;
|
|
6221
|
+
if (isNote) {
|
|
6222
|
+
dialogTitle = "Edit Note";
|
|
6223
|
+
} else {
|
|
6224
|
+
dialogTitle = hasContents ? "Edit Comment" : "Add Comment";
|
|
6225
|
+
}
|
|
6226
|
+
|
|
6227
|
+
this._showEditDialog(rect.left + x, rect.top + y, annotation.contents, dialogTitle);
|
|
5858
6228
|
}
|
|
5859
6229
|
|
|
5860
|
-
_showEditDialog(x, y, existingText) {
|
|
6230
|
+
_showEditDialog(x, y, existingText, title = "Edit Note") {
|
|
5861
6231
|
// Remove any existing dialog (but keep editingAnnotation)
|
|
5862
6232
|
this._removeDialog();
|
|
5863
6233
|
|
|
@@ -5866,7 +6236,7 @@
|
|
|
5866
6236
|
this.noteDialog.className = "note-dialog";
|
|
5867
6237
|
this.noteDialog.innerHTML = `
|
|
5868
6238
|
<div class="note-dialog-header">
|
|
5869
|
-
<span
|
|
6239
|
+
<span>${title}</span>
|
|
5870
6240
|
<button class="note-dialog-close" aria-label="Close">
|
|
5871
6241
|
${Icons.close}
|
|
5872
6242
|
</button>
|
|
@@ -6348,7 +6718,9 @@
|
|
|
6348
6718
|
this._setupViewerEvents();
|
|
6349
6719
|
|
|
6350
6720
|
// Annotation manager for CRUD operations
|
|
6721
|
+
// Accepts custom store, falls back to REST store if URL provided, else memory store
|
|
6351
6722
|
this.annotationManager = new AnnotationManager({
|
|
6723
|
+
store: this.options.annotationStore,
|
|
6352
6724
|
annotationsUrl: this.annotationsUrl,
|
|
6353
6725
|
documentId: this.documentId,
|
|
6354
6726
|
eventTarget: this.container, // For dispatching error events
|
|
@@ -6374,6 +6746,7 @@
|
|
|
6374
6746
|
onColorChange: this._onAnnotationColorChange.bind(this),
|
|
6375
6747
|
onDelete: this._onAnnotationDelete.bind(this),
|
|
6376
6748
|
onEdit: this._onAnnotationEdit.bind(this),
|
|
6749
|
+
onComment: this._onAnnotationComment.bind(this),
|
|
6377
6750
|
onDeselect: this._deselectAnnotation.bind(this)
|
|
6378
6751
|
});
|
|
6379
6752
|
|
|
@@ -6394,9 +6767,14 @@
|
|
|
6394
6767
|
onPageClick: (pageNumber) => this.viewer.goToPage(pageNumber)
|
|
6395
6768
|
});
|
|
6396
6769
|
|
|
6397
|
-
// Annotation sidebar
|
|
6770
|
+
// Annotation sidebar - check for user-defined element, fallback to auto-generated
|
|
6771
|
+
const annotationSidebarEl = this.container.querySelector('[data-pdf-sidebar="annotations"]');
|
|
6772
|
+
const annotationItemTemplate = this.container.querySelector('[data-pdf-template="annotation-item"]');
|
|
6773
|
+
|
|
6398
6774
|
this.annotationSidebar = new AnnotationSidebar({
|
|
6399
|
-
|
|
6775
|
+
element: annotationSidebarEl, // null if not provided (triggers fallback)
|
|
6776
|
+
itemTemplate: annotationItemTemplate, // null if not provided (uses innerHTML)
|
|
6777
|
+
container: this.bodyContainer, // Used for fallback
|
|
6400
6778
|
annotationManager: this.annotationManager,
|
|
6401
6779
|
onAnnotationClick: (annotationId) => this._scrollToAnnotationWithFlash(annotationId)
|
|
6402
6780
|
});
|
|
@@ -6558,7 +6936,7 @@
|
|
|
6558
6936
|
await this.thumbnailSidebar.setDocument(this.viewer.pdfDocument);
|
|
6559
6937
|
}
|
|
6560
6938
|
|
|
6561
|
-
// Load existing annotations
|
|
6939
|
+
// Load existing annotations from store
|
|
6562
6940
|
await this.annotationManager.loadAnnotations();
|
|
6563
6941
|
|
|
6564
6942
|
// Render annotations on all rendered pages
|
|
@@ -6777,6 +7155,14 @@
|
|
|
6777
7155
|
}
|
|
6778
7156
|
}
|
|
6779
7157
|
|
|
7158
|
+
_onAnnotationComment(annotation) {
|
|
7159
|
+
// For highlight/underline/ink, use the note tool's edit dialog to edit contents
|
|
7160
|
+
const supportsComment = ["highlight", "line", "ink"].includes(annotation.annotation_type);
|
|
7161
|
+
if (supportsComment) {
|
|
7162
|
+
this.tools[ToolMode.NOTE].editNote(annotation);
|
|
7163
|
+
}
|
|
7164
|
+
}
|
|
7165
|
+
|
|
6780
7166
|
async _onAnnotationDelete(annotation) {
|
|
6781
7167
|
await this.annotationManager.deleteAnnotation(annotation.id);
|
|
6782
7168
|
}
|
|
@@ -7590,7 +7976,8 @@
|
|
|
7590
7976
|
documentId: String,
|
|
7591
7977
|
trackingUrl: String,
|
|
7592
7978
|
initialPage: Number,
|
|
7593
|
-
initialAnnotation: String
|
|
7979
|
+
initialAnnotation: String,
|
|
7980
|
+
autoHeight: { type: Boolean, default: true }
|
|
7594
7981
|
}
|
|
7595
7982
|
|
|
7596
7983
|
initialize() {
|
|
@@ -8056,8 +8443,8 @@
|
|
|
8056
8443
|
|
|
8057
8444
|
setViewportHeight() {
|
|
8058
8445
|
requestAnimationFrame(() => {
|
|
8059
|
-
// Skip if
|
|
8060
|
-
if (this.
|
|
8446
|
+
// Skip if autoHeight is disabled (container height managed by consuming application)
|
|
8447
|
+
if (!this.autoHeightValue) {
|
|
8061
8448
|
return
|
|
8062
8449
|
}
|
|
8063
8450
|
|
|
@@ -8144,46 +8531,13 @@
|
|
|
8144
8531
|
}
|
|
8145
8532
|
}
|
|
8146
8533
|
|
|
8147
|
-
|
|
8148
|
-
static targets = ["container", "toggle"]
|
|
8149
|
-
|
|
8150
|
-
connect() {
|
|
8151
|
-
this.isSyncing = false;
|
|
8152
|
-
}
|
|
8153
|
-
|
|
8154
|
-
sync(event) {
|
|
8155
|
-
if (this.isSyncing) return
|
|
8156
|
-
if (!this.toggleTarget.checked) return
|
|
8157
|
-
|
|
8158
|
-
const master = event.currentTarget;
|
|
8159
|
-
const slave = this.containerTargets.find(target => target !== master);
|
|
8160
|
-
|
|
8161
|
-
if (!slave) return
|
|
8162
|
-
|
|
8163
|
-
this.isSyncing = true;
|
|
8164
|
-
|
|
8165
|
-
// Calculate percentage-based scroll to account for potential
|
|
8166
|
-
// differences in zoom levels or page counts
|
|
8167
|
-
const scrollPercentageY = master.scrollTop / (master.scrollHeight - master.clientHeight);
|
|
8168
|
-
const scrollPercentageX = master.scrollLeft / (master.scrollWidth - master.clientWidth);
|
|
8169
|
-
|
|
8170
|
-
window.requestAnimationFrame(() => {
|
|
8171
|
-
slave.scrollTop = scrollPercentageY * (slave.scrollHeight - slave.clientHeight);
|
|
8172
|
-
slave.scrollLeft = scrollPercentageX * (slave.scrollWidth - slave.clientWidth);
|
|
8173
|
-
|
|
8174
|
-
// Reset the lock after the browser paint
|
|
8175
|
-
window.requestAnimationFrame(() => {
|
|
8176
|
-
this.isSyncing = false;
|
|
8177
|
-
});
|
|
8178
|
-
});
|
|
8179
|
-
}
|
|
8180
|
-
}
|
|
8181
|
-
|
|
8534
|
+
exports.AnnotationStore = AnnotationStore;
|
|
8182
8535
|
exports.CoreViewer = CoreViewer;
|
|
8536
|
+
exports.MemoryAnnotationStore = MemoryAnnotationStore;
|
|
8183
8537
|
exports.PdfDownloadController = pdf_download_controller;
|
|
8184
|
-
exports.PdfSyncScrollController = pdf_sync_scroll_controller;
|
|
8185
8538
|
exports.PdfViewer = PdfViewer;
|
|
8186
8539
|
exports.PdfViewerController = pdf_viewer_controller;
|
|
8540
|
+
exports.RestAnnotationStore = RestAnnotationStore;
|
|
8187
8541
|
exports.ToolMode = ToolMode;
|
|
8188
8542
|
exports.ViewerEvents = ViewerEvents;
|
|
8189
8543
|
|