@@ -78,48 +78,152 @@ export default {
7878 return this .addAnnos ? ` <div @contextmenu.prevent.stop="showCtxMenu($event)">${ text} </div>` : ` <div>${ text} </div>`
7979 }
8080
81+ // Sort entities by start_ind, then by end_ind (longer first for same start)
82+ // Preserve original index for click handlers
83+ const sortedEnts = this .ents .map ((ent , origIdx ) => ({ ent, origIdx })).sort ((a , b ) => {
84+ if (a .ent .start_ind !== b .ent .start_ind ) {
85+ return a .ent .start_ind - b .ent .start_ind
86+ }
87+ return b .ent .end_ind - a .ent .end_ind // Longer spans first when same start
88+ })
89+
8190 const taskHighlightDefault = ' highlight-task-default'
82- let formattedText = ' '
83- let start = 0
8491 let timeout = 0
85- for (let i = 0 ; i < this .ents .length ; i++ ) {
86- // highlight the span with default
87- let highlightText = this .text .slice (this .ents [i].start_ind , this .ents [i].end_ind )
8892
93+ // Create events for start and end of each annotation
94+ const events = []
95+ sortedEnts .forEach ((entData , i ) => {
96+ const ent = entData .ent
97+ const origIdx = entData .origIdx
98+ events .push ({ pos: ent .start_ind , type: ' start' , entIndex: i, origIndex: origIdx, ent: ent })
99+ events .push ({ pos: ent .end_ind , type: ' end' , entIndex: i, origIndex: origIdx, ent: ent })
100+ })
101+ events .sort ((a , b ) => {
102+ if (a .pos !== b .pos ) {
103+ return a .pos - b .pos
104+ }
105+ // At same position, process starts before ends
106+ if (a .type !== b .type ) {
107+ return a .type === ' start' ? - 1 : 1
108+ }
109+ return 0
110+ })
111+
112+ let formattedText = ' '
113+ let currentPos = 0
114+ const activeEnts = [] // Stack of active entities (ordered by when they were opened)
115+
116+ // Helper function to get style class for an entity
117+ const getStyleClass = (ent , origIndex ) => {
89118 let styleClass = taskHighlightDefault
90- if (this . ents [i] .assignedValues [this .taskName ] !== null ) {
91- let btnIndex = this .taskValues .indexOf (this . ents [i] .assignedValues [this .taskName ])
119+ if (ent .assignedValues [this .taskName ] !== null ) {
120+ let btnIndex = this .taskValues .indexOf (ent .assignedValues [this .taskName ])
92121 styleClass = ` highlight-task-${ btnIndex} `
93122 }
94123
95- if (this . ents [i] .id === this .currentRelStartEnt .id ) {
124+ if (ent .id === this .currentRelStartEnt .id ) {
96125 styleClass += ' current-rel-start'
97- } else if (this . ents [i] .id === this .currentRelEndEnt .id ) {
126+ } else if (ent .id === this .currentRelEndEnt .id ) {
98127 styleClass += ' current-rel-end'
99128 }
100129
101- styleClass = this .ents [i] === this .currentEnt ? ` ${ styleClass} highlight-task-selected` : styleClass
102- timeout = this .ents [i] === this .currentEnt && i === 0 ? 500 : timeout
130+ if (ent === this .currentEnt ) {
131+ styleClass += ' highlight-task-selected'
132+ timeout = origIndex === 0 ? 500 : timeout
133+ }
134+ return styleClass
135+ }
136+
137+ // Helper function to build opening span tag
138+ const buildOpenSpan = (ent , origIndex ) => {
139+ const styleClass = getStyleClass (ent, origIndex)
140+ return ` <span @click.stop="selectEnt(${ origIndex} )" class="${ styleClass} ">`
141+ }
103142
143+ // Helper function to build closing span tag with optional remove button
144+ const buildCloseSpan = (ent , origIndex , isInnermost ) => {
104145 let removeButtonEl = ' '
105- if (this . ents [i] .manually_created ) {
106- removeButtonEl = ` <font-awesome-icon icon="times" class="remove-new-anno" @click="removeNewAnno(${ i } )"></font-awesome-icon>`
146+ if (isInnermost && ent .manually_created ) {
147+ removeButtonEl = ` <font-awesome-icon icon="times" class="remove-new-anno" @click.stop ="removeNewAnno(${ origIndex } )"></font-awesome-icon>`
107148 }
108- let spanText = ` <span @click="selectEnt(${ i} )" class="${ styleClass} ">${ _ .escape (highlightText)}${ removeButtonEl} </span>`
109-
110- let precedingText = _ .escape (this .text .slice (start, this .ents [i].start_ind ))
111- precedingText = precedingText .length !== 0 ? precedingText : ' '
112- start = this .ents [i].end_ind
113- formattedText += precedingText + spanText
114- if (i === this .ents .length - 1 ) {
115- formattedText += this .text .slice (start, this .text .length )
149+ return ` ${ removeButtonEl} </span>`
150+ }
151+
152+ for (const event of events) {
153+ // Handle start events first (before adding text)
154+ if (event .type === ' start' ) {
155+ // Add any text up to this point
156+ if (event .pos > currentPos) {
157+ const textSegment = this .text .slice (currentPos, event .pos )
158+ if (textSegment .length > 0 ) {
159+ formattedText += _ .escape (textSegment)
160+ }
161+ currentPos = event .pos
162+ }
163+ // Open the span for this annotation
164+ formattedText += buildOpenSpan (event .ent , event .origIndex )
165+ activeEnts .push ({ entIndex: event .entIndex , origIndex: event .origIndex , ent: event .ent })
166+ } else if (event .type === ' end' ) {
167+ // Close the span (in reverse order to maintain nesting)
168+ const index = activeEnts .findIndex (ae => ae .entIndex === event .entIndex )
169+ if (index !== - 1 ) {
170+ // If this is not the innermost span, we need to handle overlapping text
171+ if (index < activeEnts .length - 1 ) {
172+ // Add text up to the end position while all spans are still active
173+ // This text is inside all active spans including this one
174+ if (event .pos > currentPos) {
175+ const textSegment = this .text .slice (currentPos, event .pos )
176+ if (textSegment .length > 0 ) {
177+ formattedText += _ .escape (textSegment)
178+ }
179+ currentPos = event .pos
180+ }
181+ // Close all inner spans (from innermost to the one after this)
182+ // Don't add remove buttons here - these are temporary closes for nesting
183+ // We'll add remove buttons only when we reach the actual end position
184+ for (let j = activeEnts .length - 1 ; j > index; j-- ) {
185+ const innerData = activeEnts[j]
186+ // Always pass false here - these are temporary closes, not final ends
187+ formattedText += buildCloseSpan (innerData .ent , innerData .origIndex , false )
188+ }
189+ // Close this span (temporary close, no remove button)
190+ formattedText += buildCloseSpan (event .ent , event .origIndex , false )
191+ // Reopen inner spans (in the same order) so text after this position is inside them
192+ for (let j = index + 1 ; j < activeEnts .length ; j++ ) {
193+ const innerData = activeEnts[j]
194+ formattedText += buildOpenSpan (innerData .ent , innerData .origIndex )
195+ }
196+ } else {
197+ // This is the innermost span at its final end position
198+ // Add text then close it with remove button if needed
199+ if (event .pos > currentPos) {
200+ const textSegment = this .text .slice (currentPos, event .pos )
201+ if (textSegment .length > 0 ) {
202+ formattedText += _ .escape (textSegment)
203+ }
204+ currentPos = event .pos
205+ }
206+ // Only add remove button when closing at the actual end position
207+ formattedText += buildCloseSpan (event .ent , event .origIndex , true )
208+ }
209+ activeEnts .splice (index, 1 )
210+ }
116211 }
117212 }
118213
119- // escape '<' '>' that may be interpreted as start/end tags, escape inserted span tags.
120- // formattedText = formattedText
121- // .replace(/<(?!\/?span|font-awesome-icon)/g, '<')
122- // .replace(/(?<!<span @click="selectEnt\(\d\d?\d?\d?\)".*"|\/span)>/g, '>')
214+ // Add remaining text after all events
215+ if (currentPos < this .text .length ) {
216+ const textSegment = this .text .slice (currentPos)
217+ if (textSegment .length > 0 ) {
218+ formattedText += _ .escape (textSegment)
219+ }
220+ // Close any remaining active spans (in reverse order)
221+ for (let j = activeEnts .length - 1 ; j >= 0 ; j-- ) {
222+ const activeData = activeEnts[j]
223+ const isInnermost = j === activeEnts .length - 1
224+ formattedText += buildCloseSpan (activeData .ent , activeData .origIndex , isInnermost)
225+ }
226+ }
123227
124228 formattedText = this .addAnnos ? ` <div @contextmenu.prevent.stop="showCtxMenu($event)">${ formattedText} </div>` : ` <div>${ formattedText} </div>`
125229 this .scrollIntoView (timeout)
@@ -238,18 +342,64 @@ export default {
238342 box-shadow : 0px -2px 3px 2px rgba (0 , 0 , 0 , 0.2 );
239343 padding : 25px ;
240344 white-space : pre-wrap ;
345+ line-height : 1.6 ; // Base line height for normal text
346+
347+ // Increase line height when there are 3 or more nested underlines
348+ // to prevent underlines from overlapping with next line
349+ [class ^= " highlight-task-" ] [class ^= " highlight-task-" ] [class ^= " highlight-task-" ] {
350+ line-height : 2.2 ; // Increased line height for 3+ levels of nesting
351+ padding-bottom : 4px ; // Extra padding to push next line down
352+ display : inline-block ; // Ensure padding applies
353+ }
354+
355+ // Also handle when default is deeply nested
356+ .highlight-task-default [class ^= " highlight-task-" ] [class ^= " highlight-task-" ] {
357+ line-height : 2.2 ;
358+ padding-bottom : 4px ;
359+ display : inline-block ;
360+ }
241361}
242362
243363.highlight-task-default {
244- background : lightgrey ;
245- border : 1px solid lightgrey ;
246- border-radius : 3px ;
364+ text-decoration : underline ;
365+ text-decoration-color : lightgrey ;
366+ text-decoration-thickness : 3px ;
367+ text-underline-offset : 3px ; // Moved down 1px to avoid descender breaks
247368 cursor : pointer ;
369+
370+ // Stack underlines when nested - each nested level gets a larger offset with clear spacing
371+ [class ^= " highlight-task-" ] {
372+ text-underline-offset : 7px ; // Second level underline (4px spacing from first, moved down 1px)
373+ }
374+
375+ [class ^= " highlight-task-" ] [class ^= " highlight-task-" ] {
376+ text-underline-offset : 11px ; // Third level underline (4px spacing from second, moved down 1px)
377+ // Increase line height for 3+ levels to prevent overlap with next line
378+ line-height : 2.2 ;
379+ padding-bottom : 4px ;
380+ display : inline-block ;
381+ }
382+
383+ [class ^= " highlight-task-" ] [class ^= " highlight-task-" ] [class ^= " highlight-task-" ] {
384+ text-underline-offset : 15px ; // Fourth level underline (4px spacing from third, moved down 1px)
385+ // Further increase line height for 4+ levels
386+ line-height : 2.4 ;
387+ padding-bottom : 6px ;
388+ display : inline-block ;
389+ }
248390}
249391
250392.highlight-task-selected {
251- font-weight : bold ;
252- font-size : 1.15rem ;
393+ // Background highlight is applied via the specific highlight-task-{i} class
394+ // This ensures the background color matches the state color
395+ text-decoration-thickness : 4px ;
396+ }
397+
398+ // Selected state for default (unvalidated) annotations
399+ .highlight-task-default.highlight-task-selected {
400+ background-color : rgba (211 , 211 , 211 , 0.3 ); // Light grey background for selected default
401+ padding : 1px 2px ;
402+ border-radius : 2px ;
253403}
254404
255405.current-rel-start {
0 commit comments