20
20
/** @typedef {import("../annotation_storage.js").AnnotationStorage } AnnotationStorage */
21
21
/** @typedef {import("../../web/interfaces").IL10n } IL10n */
22
22
23
+ import { AnnotationEditorType , shadow } from "../../shared/util.js" ;
23
24
import { bindEvents , KeyboardManager } from "./tools.js" ;
24
- import { AnnotationEditorType } from "../../shared/util .js" ;
25
+ import { binarySearchFirstItem } from "../display_utils .js" ;
25
26
import { FreeTextEditor } from "./freetext.js" ;
26
27
import { InkEditor } from "./ink.js" ;
27
28
@@ -50,8 +51,14 @@ class AnnotationEditorLayer {
50
51
51
52
#isCleaningUp = false ;
52
53
54
+ #textLayerMap = new WeakMap ( ) ;
55
+
56
+ #textNodes = new Map ( ) ;
57
+
53
58
#uiManager;
54
59
60
+ #waitingEditors = new Set ( ) ;
61
+
55
62
static _initialized = false ;
56
63
57
64
static _keyboardManager = new KeyboardManager ( [
@@ -88,6 +95,7 @@ class AnnotationEditorLayer {
88
95
if ( ! AnnotationEditorLayer . _initialized ) {
89
96
AnnotationEditorLayer . _initialized = true ;
90
97
FreeTextEditor . initialize ( options . l10n ) ;
98
+ InkEditor . initialize ( options . l10n ) ;
91
99
92
100
options . uiManager . registerEditorTypes ( [ FreeTextEditor , InkEditor ] ) ;
93
101
}
@@ -98,11 +106,40 @@ class AnnotationEditorLayer {
98
106
this . #boundClick = this . click . bind ( this ) ;
99
107
this . #boundMousedown = this . mousedown . bind ( this ) ;
100
108
101
- for ( const editor of this . #uiManager. getEditors ( options . pageIndex ) ) {
102
- this . add ( editor ) ;
109
+ this . #uiManager. addLayer ( this ) ;
110
+ }
111
+
112
+ get textLayerElements ( ) {
113
+ // When zooming the text layer is removed from the DOM and sometimes
114
+ // it's rebuilt hence the nodes are no longer valid.
115
+
116
+ const textLayer = this . div . parentNode
117
+ . getElementsByClassName ( "textLayer" )
118
+ . item ( 0 ) ;
119
+
120
+ if ( ! textLayer ) {
121
+ return shadow ( this , "textLayerElements" , null ) ;
103
122
}
104
123
105
- this . #uiManager. addLayer ( this ) ;
124
+ let textChildren = this . #textLayerMap. get ( textLayer ) ;
125
+ if ( textChildren ) {
126
+ return textChildren ;
127
+ }
128
+
129
+ textChildren = textLayer . querySelectorAll ( `span[role="presentation"]` ) ;
130
+ if ( textChildren . length === 0 ) {
131
+ return shadow ( this , "textLayerElements" , null ) ;
132
+ }
133
+
134
+ textChildren = Array . from ( textChildren ) ;
135
+ textChildren . sort ( AnnotationEditorLayer . #compareElementPositions) ;
136
+ this . #textLayerMap. set ( textLayer , textChildren ) ;
137
+
138
+ return textChildren ;
139
+ }
140
+
141
+ get #hasTextLayer( ) {
142
+ return ! ! this . div . parentNode . querySelector ( ".textLayer .endOfContent" ) ;
106
143
}
107
144
108
145
/**
@@ -230,13 +267,19 @@ class AnnotationEditorLayer {
230
267
*/
231
268
enable ( ) {
232
269
this . div . style . pointerEvents = "auto" ;
270
+ for ( const editor of this . #editors. values ( ) ) {
271
+ editor . enableEditing ( ) ;
272
+ }
233
273
}
234
274
235
275
/**
236
276
* Disable editor creation.
237
277
*/
238
278
disable ( ) {
239
279
this . div . style . pointerEvents = "none" ;
280
+ for ( const editor of this . #editors. values ( ) ) {
281
+ editor . disableEditing ( ) ;
282
+ }
240
283
}
241
284
242
285
/**
@@ -276,6 +319,7 @@ class AnnotationEditorLayer {
276
319
277
320
detach ( editor ) {
278
321
this . #editors. delete ( editor . id ) ;
322
+ this . removePointerInTextLayer ( editor ) ;
279
323
}
280
324
281
325
/**
@@ -311,19 +355,160 @@ class AnnotationEditorLayer {
311
355
}
312
356
313
357
if ( this . #uiManager. isActive ( editor ) ) {
314
- editor . parent . setActiveEditor ( null ) ;
358
+ editor . parent ? .setActiveEditor ( null ) ;
315
359
}
316
360
317
361
this . attach ( editor ) ;
318
362
editor . pageIndex = this . pageIndex ;
319
- editor . parent . detach ( editor ) ;
363
+ editor . parent ? .detach ( editor ) ;
320
364
editor . parent = this ;
321
365
if ( editor . div && editor . isAttachedToDOM ) {
322
366
editor . div . remove ( ) ;
323
367
this . div . append ( editor . div ) ;
324
368
}
325
369
}
326
370
371
+ /**
372
+ * Compare the positions of two elements, it must correspond to
373
+ * the visual ordering.
374
+ *
375
+ * @param {HTMLElement } e1
376
+ * @param {HTMLElement } e2
377
+ * @returns {number }
378
+ */
379
+ static #compareElementPositions( e1 , e2 ) {
380
+ const rect1 = e1 . getBoundingClientRect ( ) ;
381
+ const rect2 = e2 . getBoundingClientRect ( ) ;
382
+
383
+ if ( rect1 . y + rect1 . height <= rect2 . y ) {
384
+ return - 1 ;
385
+ }
386
+
387
+ if ( rect2 . y + rect2 . height <= rect1 . y ) {
388
+ return + 1 ;
389
+ }
390
+
391
+ const centerX1 = rect1 . x + rect1 . width / 2 ;
392
+ const centerX2 = rect2 . x + rect2 . width / 2 ;
393
+
394
+ return centerX1 - centerX2 ;
395
+ }
396
+
397
+ /**
398
+ * Function called when the text layer has finished rendering.
399
+ */
400
+ onTextLayerRendered ( ) {
401
+ this . #textNodes. clear ( ) ;
402
+ for ( const editor of this . #waitingEditors) {
403
+ if ( editor . isAttachedToDOM ) {
404
+ this . addPointerInTextLayer ( editor ) ;
405
+ }
406
+ }
407
+ this . #waitingEditors. clear ( ) ;
408
+ }
409
+
410
+ /**
411
+ * Remove an aria-owns id from a node in the text layer.
412
+ * @param {AnnotationEditor } editor
413
+ */
414
+ removePointerInTextLayer ( editor ) {
415
+ if ( ! this . #hasTextLayer) {
416
+ this . #waitingEditors. delete ( editor ) ;
417
+ return ;
418
+ }
419
+
420
+ const { id } = editor ;
421
+ const node = this . #textNodes. get ( id ) ;
422
+ if ( ! node ) {
423
+ return ;
424
+ }
425
+
426
+ this . #textNodes. delete ( id ) ;
427
+ let owns = node . getAttribute ( "aria-owns" ) ;
428
+ if ( owns ?. includes ( id ) ) {
429
+ owns = owns
430
+ . split ( " " )
431
+ . filter ( x => x !== id )
432
+ . join ( " " ) ;
433
+ if ( owns ) {
434
+ node . setAttribute ( "aria-owns" , owns ) ;
435
+ } else {
436
+ node . removeAttribute ( "aria-owns" ) ;
437
+ node . setAttribute ( "role" , "presentation" ) ;
438
+ }
439
+ }
440
+ }
441
+
442
+ /**
443
+ * Find the text node which is the nearest and add an aria-owns attribute
444
+ * in order to correctly position this editor in the text flow.
445
+ * @param {AnnotationEditor } editor
446
+ */
447
+ addPointerInTextLayer ( editor ) {
448
+ if ( ! this . #hasTextLayer) {
449
+ // The text layer needs to be there, so we postpone the association.
450
+ this . #waitingEditors. add ( editor ) ;
451
+ return ;
452
+ }
453
+
454
+ this . removePointerInTextLayer ( editor ) ;
455
+
456
+ const children = this . textLayerElements ;
457
+ if ( ! children ) {
458
+ return ;
459
+ }
460
+ const { contentDiv } = editor ;
461
+ const id = editor . getIdForTextLayer ( ) ;
462
+
463
+ const index = binarySearchFirstItem (
464
+ children ,
465
+ node =>
466
+ AnnotationEditorLayer . #compareElementPositions( contentDiv , node ) < 0
467
+ ) ;
468
+ const node = children [ Math . max ( 0 , index - 1 ) ] ;
469
+ const owns = node . getAttribute ( "aria-owns" ) ;
470
+ if ( ! owns ?. includes ( id ) ) {
471
+ node . setAttribute ( "aria-owns" , owns ? `${ owns } ${ id } ` : id ) ;
472
+ }
473
+ node . removeAttribute ( "role" ) ;
474
+
475
+ this . #textNodes. set ( id , node ) ;
476
+ }
477
+
478
+ /**
479
+ * Move a div in the DOM in order to respect the visual order.
480
+ * @param {HTMLDivElement } div
481
+ */
482
+ moveDivInDOM ( editor ) {
483
+ this . addPointerInTextLayer ( editor ) ;
484
+
485
+ const { div, contentDiv } = editor ;
486
+ if ( ! this . div . hasChildNodes ( ) ) {
487
+ this . div . append ( div ) ;
488
+ return ;
489
+ }
490
+
491
+ const children = Array . from ( this . div . childNodes ) . filter (
492
+ node => node !== div
493
+ ) ;
494
+
495
+ if ( children . length === 0 ) {
496
+ return ;
497
+ }
498
+
499
+ const index = binarySearchFirstItem (
500
+ children ,
501
+ node =>
502
+ AnnotationEditorLayer . #compareElementPositions( contentDiv , node ) < 0
503
+ ) ;
504
+
505
+ if ( index === 0 ) {
506
+ children [ 0 ] . before ( div ) ;
507
+ } else {
508
+ children [ index - 1 ] . after ( div ) ;
509
+ }
510
+ }
511
+
327
512
/**
328
513
* Add a new editor in the current view.
329
514
* @param {AnnotationEditor } editor
@@ -340,6 +525,7 @@ class AnnotationEditorLayer {
340
525
editor . isAttachedToDOM = true ;
341
526
}
342
527
528
+ this . moveDivInDOM ( editor ) ;
343
529
editor . onceAdded ( ) ;
344
530
}
345
531
@@ -493,6 +679,8 @@ class AnnotationEditorLayer {
493
679
const endY = event . clientY - rect . y ;
494
680
495
681
editor . translate ( endX - editor . startX , endY - editor . startY ) ;
682
+ this . moveDivInDOM ( editor ) ;
683
+ editor . div . focus ( ) ;
496
684
}
497
685
498
686
/**
@@ -517,13 +705,20 @@ class AnnotationEditorLayer {
517
705
* Destroy the main editor.
518
706
*/
519
707
destroy ( ) {
708
+ if ( this . #uiManager. getActive ( ) ?. parent === this ) {
709
+ this . #uiManager. setActiveEditor ( null ) ;
710
+ }
711
+
520
712
for ( const editor of this . #editors. values ( ) ) {
713
+ this . removePointerInTextLayer ( editor ) ;
521
714
editor . isAttachedToDOM = false ;
522
715
editor . div . remove ( ) ;
523
716
editor . parent = null ;
524
- this . div = null ;
525
717
}
718
+ this . #textNodes. clear ( ) ;
719
+ this . div = null ;
526
720
this . #editors. clear ( ) ;
721
+ this . #waitingEditors. clear ( ) ;
527
722
this . #uiManager. removeLayer ( this ) ;
528
723
}
529
724
@@ -548,6 +743,9 @@ class AnnotationEditorLayer {
548
743
this . viewport = parameters . viewport ;
549
744
bindEvents ( this , this . div , [ "dragover" , "drop" , "keydown" ] ) ;
550
745
this . setDimensions ( ) ;
746
+ for ( const editor of this . #uiManager. getEditors ( this . pageIndex ) ) {
747
+ this . add ( editor ) ;
748
+ }
551
749
this . updateMode ( ) ;
552
750
}
553
751
0 commit comments