1+ import { Component } from "./component" ;
2+ import { findTargetNodeAndOffset , hashElement } from "../services/dom" ;
3+ import { el } from "../wysiwyg/utils/dom" ;
4+ import commentIcon from "@icons/comment.svg" ;
5+ import closeIcon from "@icons/close.svg" ;
6+ import { scrollAndHighlightElement } from "../services/util" ;
7+
8+ /**
9+ * Track the close function for the current open marker so it can be closed
10+ * when another is opened so we only show one marker comment thread at one time.
11+ */
12+ let openMarkerClose : Function | null = null ;
13+
14+ export class PageCommentReference extends Component {
15+ protected link : HTMLLinkElement ;
16+ protected reference : string ;
17+ protected markerWrap : HTMLElement | null = null ;
18+
19+ protected viewCommentText : string ;
20+ protected jumpToThreadText : string ;
21+ protected closeText : string ;
22+
23+ setup ( ) {
24+ this . link = this . $el as HTMLLinkElement ;
25+ this . reference = this . $opts . reference ;
26+ this . viewCommentText = this . $opts . viewCommentText ;
27+ this . jumpToThreadText = this . $opts . jumpToThreadText ;
28+ this . closeText = this . $opts . closeText ;
29+
30+ // Show within page display area if seen
31+ const pageContentArea = document . querySelector ( '.page-content' ) ;
32+ if ( pageContentArea instanceof HTMLElement ) {
33+ this . updateMarker ( pageContentArea ) ;
34+ }
35+
36+ // Handle editor view to show on comments toolbox view
37+ window . addEventListener ( 'editor-toolbox-change' , ( event ) => {
38+ const tabName : string = ( event as { detail : { tab : string , open : boolean } } ) . detail . tab ;
39+ const isOpen = ( event as { detail : { tab : string , open : boolean } } ) . detail . open ;
40+ if ( tabName === 'comments' && isOpen ) {
41+ this . showForEditor ( ) ;
42+ } else {
43+ this . hideMarker ( ) ;
44+ }
45+ } ) ;
46+ }
47+
48+ protected showForEditor ( ) {
49+ const contentWrap = document . querySelector ( '.editor-content-wrap' ) ;
50+ if ( contentWrap instanceof HTMLElement ) {
51+ this . updateMarker ( contentWrap ) ;
52+ }
53+
54+ const onChange = ( ) => {
55+ this . hideMarker ( ) ;
56+ setTimeout ( ( ) => {
57+ window . $events . remove ( 'editor-html-change' , onChange ) ;
58+ } , 1 ) ;
59+ } ;
60+
61+ window . $events . listen ( 'editor-html-change' , onChange ) ;
62+ }
63+
64+ protected updateMarker ( contentContainer : HTMLElement ) {
65+ // Reset link and existing marker
66+ this . link . classList . remove ( 'outdated' , 'missing' ) ;
67+ if ( this . markerWrap ) {
68+ this . markerWrap . remove ( ) ;
69+ }
70+
71+ const [ refId , refHash , refRange ] = this . reference . split ( ':' ) ;
72+ const refEl = document . getElementById ( refId ) ;
73+ if ( ! refEl ) {
74+ this . link . classList . add ( 'outdated' , 'missing' ) ;
75+ return ;
76+ }
77+
78+ const refCloneToAssess = refEl . cloneNode ( true ) as HTMLElement ;
79+ const toRemove = refCloneToAssess . querySelectorAll ( '[data-lexical-text]' ) ;
80+ refCloneToAssess . removeAttribute ( 'style' ) ;
81+ for ( const el of toRemove ) {
82+ el . after ( ...el . childNodes ) ;
83+ el . remove ( ) ;
84+ }
85+
86+ const actualHash = hashElement ( refCloneToAssess ) ;
87+ if ( actualHash !== refHash ) {
88+ this . link . classList . add ( 'outdated' ) ;
89+ }
90+
91+ const marker = el ( 'button' , {
92+ type : 'button' ,
93+ class : 'content-comment-marker' ,
94+ title : this . viewCommentText ,
95+ } ) ;
96+ marker . innerHTML = < string > commentIcon ;
97+ marker . addEventListener ( 'click' , event => {
98+ this . showCommentAtMarker ( marker ) ;
99+ } ) ;
100+
101+ this . markerWrap = el ( 'div' , {
102+ class : 'content-comment-highlight' ,
103+ } , [ marker ] ) ;
104+
105+ contentContainer . append ( this . markerWrap ) ;
106+ this . positionMarker ( refEl , refRange ) ;
107+
108+ this . link . href = `#${ refEl . id } ` ;
109+ this . link . addEventListener ( 'click' , ( event : MouseEvent ) => {
110+ event . preventDefault ( ) ;
111+ scrollAndHighlightElement ( refEl ) ;
112+ } ) ;
113+
114+ window . addEventListener ( 'resize' , ( ) => {
115+ this . positionMarker ( refEl , refRange ) ;
116+ } ) ;
117+ }
118+
119+ protected positionMarker ( targetEl : HTMLElement , range : string ) {
120+ if ( ! this . markerWrap ) {
121+ return ;
122+ }
123+
124+ const markerParent = this . markerWrap . parentElement as HTMLElement ;
125+ const parentBounds = markerParent . getBoundingClientRect ( ) ;
126+ let targetBounds = targetEl . getBoundingClientRect ( ) ;
127+ const [ rangeStart , rangeEnd ] = range . split ( '-' ) ;
128+ if ( rangeStart && rangeEnd ) {
129+ const range = new Range ( ) ;
130+ const relStart = findTargetNodeAndOffset ( targetEl , Number ( rangeStart ) ) ;
131+ const relEnd = findTargetNodeAndOffset ( targetEl , Number ( rangeEnd ) ) ;
132+ if ( relStart && relEnd ) {
133+ range . setStart ( relStart . node , relStart . offset ) ;
134+ range . setEnd ( relEnd . node , relEnd . offset ) ;
135+ targetBounds = range . getBoundingClientRect ( ) ;
136+ }
137+ }
138+
139+ const relLeft = targetBounds . left - parentBounds . left ;
140+ const relTop = ( targetBounds . top - parentBounds . top ) + markerParent . scrollTop ;
141+
142+ this . markerWrap . style . left = `${ relLeft } px` ;
143+ this . markerWrap . style . top = `${ relTop } px` ;
144+ this . markerWrap . style . width = `${ targetBounds . width } px` ;
145+ this . markerWrap . style . height = `${ targetBounds . height } px` ;
146+ }
147+
148+ protected hideMarker ( ) {
149+ // Hide marker and close existing marker windows
150+ if ( openMarkerClose ) {
151+ openMarkerClose ( ) ;
152+ }
153+ this . markerWrap ?. remove ( ) ;
154+ }
155+
156+ protected showCommentAtMarker ( marker : HTMLElement ) : void {
157+ // Hide marker and close existing marker windows
158+ if ( openMarkerClose ) {
159+ openMarkerClose ( ) ;
160+ }
161+ marker . hidden = true ;
162+
163+ // Locate relevant comment
164+ const commentBox = this . link . closest ( '.comment-box' ) as HTMLElement ;
165+
166+ // Build comment window
167+ const readClone = ( commentBox . closest ( '.comment-branch' ) as HTMLElement ) . cloneNode ( true ) as HTMLElement ;
168+ const toRemove = readClone . querySelectorAll ( '.actions, form' ) ;
169+ for ( const el of toRemove ) {
170+ el . remove ( ) ;
171+ }
172+
173+ const close = el ( 'button' , { type : 'button' , title : this . closeText } ) ;
174+ close . innerHTML = ( closeIcon as string ) ;
175+ const jump = el ( 'button' , { type : 'button' , 'data-action' : 'jump' } , [ this . jumpToThreadText ] ) ;
176+
177+ const commentWindow = el ( 'div' , {
178+ class : 'content-comment-window'
179+ } , [
180+ el ( 'div' , {
181+ class : 'content-comment-window-actions' ,
182+ } , [ jump , close ] ) ,
183+ el ( 'div' , {
184+ class : 'content-comment-window-content comment-container-compact comment-container-super-compact' ,
185+ } , [ readClone ] ) ,
186+ ] ) ;
187+
188+ marker . parentElement ?. append ( commentWindow ) ;
189+
190+ // Handle interaction within window
191+ const closeAction = ( ) => {
192+ commentWindow . remove ( ) ;
193+ marker . hidden = false ;
194+ window . removeEventListener ( 'click' , windowCloseAction ) ;
195+ openMarkerClose = null ;
196+ } ;
197+
198+ const windowCloseAction = ( event : MouseEvent ) => {
199+ if ( ! ( marker . parentElement as HTMLElement ) . contains ( event . target as HTMLElement ) ) {
200+ closeAction ( ) ;
201+ }
202+ } ;
203+ window . addEventListener ( 'click' , windowCloseAction ) ;
204+
205+ openMarkerClose = closeAction ;
206+ close . addEventListener ( 'click' , closeAction . bind ( this ) ) ;
207+ jump . addEventListener ( 'click' , ( ) => {
208+ closeAction ( ) ;
209+ commentBox . scrollIntoView ( { behavior : 'smooth' } ) ;
210+ const highlightTarget = commentBox . querySelector ( '.header' ) as HTMLElement ;
211+ highlightTarget . classList . add ( 'anim-highlight' ) ;
212+ highlightTarget . addEventListener ( 'animationend' , ( ) => highlightTarget . classList . remove ( 'anim-highlight' ) )
213+ } ) ;
214+
215+ // Position window within bounds
216+ const commentWindowBounds = commentWindow . getBoundingClientRect ( ) ;
217+ const contentBounds = document . querySelector ( '.page-content' ) ?. getBoundingClientRect ( ) ;
218+ if ( contentBounds && commentWindowBounds . right > contentBounds . right ) {
219+ const diff = commentWindowBounds . right - contentBounds . right ;
220+ commentWindow . style . left = `-${ diff } px` ;
221+ }
222+ }
223+ }
0 commit comments