diff --git a/inception/inception-html-apache-annotator-editor/src/main/ts/src/apache-annotator/ApacheAnnotatorEditor.scss b/inception/inception-html-apache-annotator-editor/src/main/ts/src/apache-annotator/ApacheAnnotatorEditor.scss
index a8afa8b72a..e9347d9cb1 100644
--- a/inception/inception-html-apache-annotator-editor/src/main/ts/src/apache-annotator/ApacheAnnotatorEditor.scss
+++ b/inception/inception-html-apache-annotator-editor/src/main/ts/src/apache-annotator/ApacheAnnotatorEditor.scss
@@ -90,7 +90,7 @@ html > body {
* Annotation highlight.
*/
.iaa-highlighted {
- transition: filter var(--anim-fast);
+ /* transition: filter var(--anim-fast); -- slow for large documents, so we disable it here */
cursor: pointer;
background-color: var(--iaa-background-color);
box-sizing: border-box;
diff --git a/inception/inception-html-apache-annotator-editor/src/main/ts/src/apache-annotator/ApacheAnnotatorVisualizer.ts b/inception/inception-html-apache-annotator-editor/src/main/ts/src/apache-annotator/ApacheAnnotatorVisualizer.ts
index b414ac986e..d255635f18 100644
--- a/inception/inception-html-apache-annotator-editor/src/main/ts/src/apache-annotator/ApacheAnnotatorVisualizer.ts
+++ b/inception/inception-html-apache-annotator-editor/src/main/ts/src/apache-annotator/ApacheAnnotatorVisualizer.ts
@@ -39,7 +39,7 @@ export class ApacheAnnotatorVisualizer {
private tracker: ViewportTracker
private showInlineLabels = false
private showEmptyHighlights = false
- private observer: IntersectionObserver
+
private sectionSelector: string
private sectionAnnotationVisualizer: SectionAnnotationVisualizer
private sectionAnnotationCreator: SectionAnnotationCreator
@@ -47,8 +47,12 @@ export class ApacheAnnotatorVisualizer {
private data? : AnnotatedText
- private removeTransientMarkers: (() => void)[] = []
- private removeTransientMarkersTimeout: number | undefined = undefined
+ private scrolling = false
+ private lastScrollTop: number | undefined = undefined
+ private removeScrollMarkers: (() => void)[] = []
+ private removeScrollMarkersTimeout: number | undefined = undefined
+ private removePingMarkers: (() => void)[] = []
+ private removePingMarkersTimeout: number | undefined = undefined
private alpha = '55'
@@ -129,6 +133,11 @@ export class ApacheAnnotatorVisualizer {
}
loadAnnotations (): void {
+ // scrollTo uses a timeout to work around the problem that the browser does not always properly
+ // scroll to the target element. We want to avoid loading annotations while scrolling is still
+ // in progress. Once scrolling is complete, we should get triggered by the ViewportTracker.
+ if (this.scrolling) return
+
const options: DiamLoadAnnotationsOptions = {
range: this.tracker.currentRange,
includeText: false,
@@ -146,7 +155,8 @@ export class ApacheAnnotatorVisualizer {
}
private renderAnnotations (doc: AnnotatedText): void {
- const startTime = new Date().getTime()
+ console.log(`Client-side starting`)
+ const startTime = performance.now()
this.clearHighlights()
this.resizer.hide()
@@ -181,8 +191,8 @@ export class ApacheAnnotatorVisualizer {
this.renderSelectedRelationEndpointHighlights(doc)
}
- const endTime = new Date().getTime()
- console.log(`Client-side rendering took ${Math.abs(endTime - startTime)}ms`)
+ const endTime = performance.now()
+ console.log(`Client-side rendering took ${endTime - startTime}ms`)
}
private renderVerticalSelectionMarker (doc: AnnotatedText) {
@@ -262,17 +272,17 @@ export class ApacheAnnotatorVisualizer {
* Some highlights may only contain whitepace. This method removes such highlights.
*/
private removeWhitepaceOnlyHighlights (selector: string = '.iaa-highlighted') {
- let candidates = this.root.querySelectorAll(selector)
+ const start = performance.now();
+ const candidates = this.root.querySelectorAll(selector)
console.log(`Found ${candidates.length} elements matching [${selector}] to remove whitespace-only highlights`)
- let start = performance.now();
candidates.forEach(e => {
if (!e.classList.contains('iaa-zero-width') && !e.textContent?.trim()) {
e.after(...e.childNodes)
e.remove()
}
})
- let end = performance.now();
- console.log(`Time taken: ${end - start} milliseconds`)
+ const end = performance.now();
+ console.log(`Removing whitespace only highlights took ${end - start}ms`)
}
private postProcessHighlights () {
@@ -373,14 +383,14 @@ export class ApacheAnnotatorVisualizer {
if (viewportBegin <= begin && end <= viewportEnd) {
// Quick and easy if the annotation fits entirely into the visible viewport
- const startTime = new Date().getTime()
+ const startTime = performance.now()
this.renderHighlight(span, begin, end, attributes)
- const endTime = new Date().getTime()
+ const endTime = performance.now()
// console.debug(`Rendering span with size ${end - begin} took ${Math.abs(endTime - startTime)}ms`)
} else {
// Try optimizing for long spans to improve rendering performance
let fragmentCount = 0
- const startTime = new Date().getTime()
+ const startTime = performance.now()
const coreBegin = Math.max(begin, viewportBegin)
const coreEnd = Math.min(end, viewportEnd)
@@ -399,7 +409,7 @@ export class ApacheAnnotatorVisualizer {
this.renderHighlight(span, end, end, attributes)
fragmentCount++
}
- const endTime = new Date().getTime()
+ const endTime = performance.now()
// console.debug(`Rendering span with size ${end - begin} took ${Math.abs(endTime - startTime)}ms (${fragmentCount} fragments)`)
}
}
@@ -419,21 +429,56 @@ export class ApacheAnnotatorVisualizer {
this.toCleanUp.add(highlightText(range, 'mark', attributes))
}
+ private clearScrollMarkers () {
+ if (this.removeScrollMarkersTimeout) {
+ window.cancelIdleCallback(this.removeScrollMarkersTimeout)
+ this.removeScrollMarkersTimeout = undefined
+ this.removeScrollMarkers.forEach(remove => remove())
+ this.removeScrollMarkers = []
+ }
+ }
+
+ private renderPingMarkers(pingRanges?: Offsets[]) {
+ if (!pingRanges) return
+
+ console.log('Rendering ping markers')
+
+ for (const pingOffset of pingRanges || []) {
+ const pingRange = offsetToRange(this.root, pingOffset[0], pingOffset[1])
+ if (pingRange) {
+ this.removePingMarkers.push(this.safeHighlightText(pingRange, 'mark', { class: 'iaa-ping-marker' }))
+ }
+ }
+
+ this.removeWhitepaceOnlyHighlights('.iaa-ping-marker')
+ this.removeSpuriousZeroWidthHighlights()
+
+ if (this.removePingMarkers.length > 0) {
+ this.removePingMarkersTimeout = window.setTimeout(() => this.clearPingMarkers(), 2000)
+ }
+ }
+
+ private clearPingMarkers () {
+ console.log('Clearing ping markers');
+
+ if (this.removePingMarkersTimeout) {
+ window.clearTimeout(this.removePingMarkersTimeout)
+ this.removePingMarkersTimeout = undefined
+ this.removePingMarkers.forEach(remove => remove())
+ this.removePingMarkers = []
+ }
+ }
+
scrollTo (args: { offset: number, position?: string, pingRanges?: Offsets[] }): void {
const range = offsetToRange(this.root, args.offset, args.offset)
if (!range) return
- window.clearTimeout(this.removeTransientMarkersTimeout)
- this.removeTransientMarkers.forEach(remove => remove())
- this.root.normalize() // https://github.com/apache/incubator-annotator/issues/120
+ this.clearScrollMarkers()
+ this.clearPingMarkers()
+ // Add scroll marker
const removeScrollMarker = highlightText(range, 'mark', { id: 'iaa-scroll-marker' })
- this.removeTransientMarkers = [removeScrollMarker]
- for (const pingOffset of args.pingRanges || []) {
- const pingRange = offsetToRange(this.root, pingOffset[0], pingOffset[1])
- if (!pingRange) continue
- this.removeTransientMarkers.push(highlightText(pingRange, 'mark', { class: 'iaa-ping-marker' }))
- }
+ this.removeScrollMarkers = [removeScrollMarker]
if (!this.showEmptyHighlights) {
this.removeWhitepaceOnlyHighlights('.iaa-ping-marker')
@@ -466,17 +511,38 @@ export class ApacheAnnotatorVisualizer {
// markers are still there.
var scrollIntoViewFunc = () => {
finalScrollTarget.scrollIntoView({ behavior: 'auto', block: 'start', inline: 'nearest' })
- if (this.removeTransientMarkers.length > 0) window.setTimeout(scrollIntoViewFunc, 100)
+ if (this.removePingMarkers.length > 0) window.setTimeout(scrollIntoViewFunc, 100)
+
+ if (this.root instanceof HTMLElement) {
+ if (this.root.scrollTop === this.lastScrollTop) {
+ this.scrollToComplete(args.pingRanges)
+ }
+ else {
+ this.lastScrollTop = this.root.scrollTop
+ }
+ }
}
+ this.scrolling = true
+ this.sectionAnnotationVisualizer.suspend()
+ this.sectionAnnotationCreator.suspend()
window.setTimeout(scrollIntoViewFunc, 100)
}
- this.removeTransientMarkersTimeout = window.setTimeout(() => {
- this.removeTransientMarkers.forEach(remove => remove())
- this.removeTransientMarkers = []
- this.root.normalize() // https://github.com/apache/incubator-annotator/issues/120
- }, 2000)
+ this.removeScrollMarkersTimeout = window.requestIdleCallback(() => this.scrollToComplete(args.pingRanges), { timeout: 2000 })
+ }
+
+ private scrollToComplete(pingRanges?: Offsets[]) {
+ console.log('Scrolling complete')
+
+ this.clearScrollMarkers()
+ // this.renderPingMarkers(pingRanges)
+ this.root.normalize() // https://github.com/apache/incubator-annotator/issues/120
+
+ this.scrolling = false
+ this.sectionAnnotationCreator.resume()
+ this.sectionAnnotationVisualizer.resume()
+ this.lastScrollTop = undefined
}
private clearHighlights (): void {
@@ -486,20 +552,19 @@ export class ApacheAnnotatorVisualizer {
return
}
- const startTime = new Date().getTime()
+ const startTime = performance.now()
const highlightCount = this.toCleanUp.size
this.toCleanUp.forEach(cleanup => cleanup())
this.toCleanUp.clear()
this.root.normalize() // https://github.com/apache/incubator-annotator/issues/120
- const endTime = new Date().getTime()
+ const endTime = performance.now()
console.log(`Cleaning up ${highlightCount} annotations and normalizing DOM took ${Math.abs(endTime - startTime)}ms`)
}
destroy (): void {
- if (this.observer) {
- this.observer.disconnect()
- }
-
+ this.sectionAnnotationCreator.destroy()
+ this.sectionAnnotationVisualizer.destroy()
+ this.tracker.disconnect()
this.clearHighlights()
}
}
diff --git a/inception/inception-html-apache-annotator-editor/src/main/ts/src/apache-annotator/SectionAnnotationCreator.ts b/inception/inception-html-apache-annotator-editor/src/main/ts/src/apache-annotator/SectionAnnotationCreator.ts
index 20da46b39c..4071b73721 100644
--- a/inception/inception-html-apache-annotator-editor/src/main/ts/src/apache-annotator/SectionAnnotationCreator.ts
+++ b/inception/inception-html-apache-annotator-editor/src/main/ts/src/apache-annotator/SectionAnnotationCreator.ts
@@ -17,12 +17,17 @@
*/
import './SectionAnnotationCreator.scss'
import { AnnotatedText, calculateEndOffset, calculateStartOffset, DiamAjax } from "@inception-project/inception-js-api"
+import { getScrollY } from './SectionAnnotationVisualizer'
export class SectionAnnotationCreator {
private sectionSelector: string
private ajax: DiamAjax
private root: Element
+
private observer: IntersectionObserver
+ private observerDebounceTimeout: number | undefined
+ private suspended = false
+
private _previewFrame: HTMLIFrameElement | undefined
private previewRenderTimeout: number | undefined
private previewScrollTimeout: number | undefined
@@ -38,6 +43,20 @@ export class SectionAnnotationCreator {
}
}
+ public suspend() {
+ this.suspended = true
+ }
+
+ public resume() {
+ this.suspended = false
+ }
+
+ public destroy() {
+ this.observer.disconnect()
+ this.root.querySelectorAll('.iaa-section-control').forEach(e => e.remove())
+ this.hidePreviewFrame()
+ }
+
private initializeSectionTypeAttributes() {
this.root.querySelectorAll(this.sectionSelector).forEach((e, i) => {
e.setAttribute('data-iaa-section-type', e.localName)
@@ -62,9 +81,10 @@ export class SectionAnnotationCreator {
}
private ensureVisibility() {
- const rootRect = this.root.getBoundingClientRect()
- const scrollY = (this.root.scrollTop || 0) - rootRect.top
+ const scrollY = getScrollY(this.root)
const panels = Array.from(this.root.querySelectorAll('.iaa-section-control') || [])
+
+ const panelsTops = new Map()
for (const panel of (panels as HTMLElement[])) {
const sectionId = panel.getAttribute('data-iaa-applies-to')
const section = this.root.querySelector(`[id="${sectionId}"]`)
@@ -73,30 +93,42 @@ export class SectionAnnotationCreator {
continue
}
const sectionRect = section.getBoundingClientRect()
- panel.style.top = `${sectionRect.top + scrollY}px`
+ panelsTops.set(panel, sectionRect.top)
+ }
+
+ // Update the position of the panels all at once to avoid layout thrashing
+ for (const [panel, top] of panelsTops) {
+ panel.style.top = `${top + scrollY}px`
}
}
private handleIntersect(entries: IntersectionObserverEntry[], observer: IntersectionObserver): void {
- const rootRect = this.root.getBoundingClientRect()
- const scrollY = (this.root.scrollTop || 0) - rootRect.top
-
- for (const entry of entries) {
- const sectionId = entry.target.id
- const sectionRect = entry.boundingClientRect
- let panel = this.root.querySelector(`.iaa-section-control[data-iaa-applies-to="${sectionId}"]`) as HTMLElement
-
- if (entry.isIntersecting && !panel) {
- panel = this.createControl()
- panel.setAttribute('data-iaa-applies-to', sectionId)
- panel.style.top = `${sectionRect.top + scrollY}px`
- this.root.appendChild(panel)
- }
+ if (this.observerDebounceTimeout) {
+ window.cancelIdleCallback(this.observerDebounceTimeout)
+ this.observerDebounceTimeout = undefined
+ }
- if (!entry.isIntersecting && panel) {
- panel.remove()
+ this.observerDebounceTimeout = window.requestIdleCallback(() => {
+ const rootRect = this.root.getBoundingClientRect()
+ const scrollY = (this.root.scrollTop || 0) - rootRect.top
+
+ for (const entry of entries) {
+ const sectionId = entry.target.id
+ const sectionRect = entry.boundingClientRect
+ let panel = this.root.querySelector(`.iaa-section-control[data-iaa-applies-to="${sectionId}"]`) as HTMLElement
+
+ if (entry.isIntersecting && !panel) {
+ panel = this.createControl()
+ panel.setAttribute('data-iaa-applies-to', sectionId)
+ panel.style.top = `${sectionRect.top + scrollY}px`
+ this.root.appendChild(panel)
+ }
+
+ if (!entry.isIntersecting && panel) {
+ panel.remove()
+ }
}
- }
+ }, { timeout: 100 })
}
private createControl(): HTMLElement {
diff --git a/inception/inception-html-apache-annotator-editor/src/main/ts/src/apache-annotator/SectionAnnotationVisualizer.ts b/inception/inception-html-apache-annotator-editor/src/main/ts/src/apache-annotator/SectionAnnotationVisualizer.ts
index 8d99e0b2f1..5208dbd117 100644
--- a/inception/inception-html-apache-annotator-editor/src/main/ts/src/apache-annotator/SectionAnnotationVisualizer.ts
+++ b/inception/inception-html-apache-annotator-editor/src/main/ts/src/apache-annotator/SectionAnnotationVisualizer.ts
@@ -15,6 +15,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import { ApacheAnnotatorEditor } from './ApacheAnnotatorEditor'
+import { ApacheAnnotatorVisualizer } from './ApacheAnnotatorVisualizer'
import './SectionAnnotationVisualizer.scss'
import { AnnotatedText, bgToFgColor, DiamAjax, Span, VID } from "@inception-project/inception-js-api"
@@ -22,6 +24,7 @@ export class SectionAnnotationVisualizer {
private sectionSelector: string
private ajax: DiamAjax
private root: Element
+ private suspended = false
public constructor(root: Element, ajax: DiamAjax, sectionSelector: string) {
this.root = root
@@ -31,15 +34,33 @@ export class SectionAnnotationVisualizer {
if (this.sectionSelector) {
const root = this.root.closest('.i7n-wrapper') || this.root
// on scrolling the window, we need to ensure that the panels stay visible
- root.addEventListener('scroll', () => this.ensurePanelVisibility())
+ root.addEventListener('scroll', () => {
+ if (!this.suspended) {
+ this.ensurePanelVisibility('scroll')
+ }
+ })
}
}
+ public suspend() {
+ this.suspended = true
+ }
+
+ public resume() {
+ this.suspended = false
+ this.ensurePanelVisibility('resume')
+ }
+
+ public destroy() {
+ this.clear()
+ }
+
+
render(doc: AnnotatedText) {
if (this.sectionSelector) {
this.clear()
this.renderSectionGroups(doc)
- this.ensurePanelVisibility()
+ this.ensurePanelVisibility('render')
}
}
@@ -50,67 +71,111 @@ export class SectionAnnotationVisualizer {
}
}
- private ensurePanelVisibility() {
- const panels = Array.from(this.root.parentNode?.querySelectorAll('.iaa-visible-annotations-panel') || [])
+ private ensurePanelVisibility(reason: string) {
+ performance.mark('start-ensure-panel-visibility')
+ performance.mark('start-ensure-panel-visibility-init')
+ const panels = Array.from(this.root.parentNode?.querySelectorAll('.iaa-visible-annotations-panel') || [])
const root = this.root.closest('.i7n-wrapper') || this.root
- const rootRect = root.getBoundingClientRect()
- const scrollY = (root.scrollTop || 0) - rootRect.top
+ // const rootRect = root.getBoundingClientRect()
+ // const scrollY = (root.scrollTop || 0) - rootRect.top
+ const scrollY = getScrollY(root)
+ let rootTop = getTop(root)
+ let lastSectionPanelBottom = rootTop
+ performance.mark('end-ensure-panel-visibility-init')
+ performance.measure('SectionAnnotationVisualizer.ensurePanelVisibility.init', 'start-ensure-panel-visibility-init', 'end-ensure-panel-visibility-init')
- let lastSectionPanelBottom = rootRect.top
+ performance.mark('start-get-section-spacers')
+ const sectionSpacersMap = new Map()
+ const spacerRectMap = new Map()
+ const sectionRectMap = new Map()
+ const spacers = this.root.querySelectorAll('.iaa-visible-annotations-panel-spacer')
+ spacers.forEach(spacer => {
+ const sectionId = spacer.getAttribute('data-iaa-applies-to')
+ if (sectionId) {
+ var section = this.root.ownerDocument.getElementById(sectionId)
+ if (section) {
+ sectionSpacersMap.set(sectionId, spacer)
+ spacerRectMap.set(sectionId, spacer.getBoundingClientRect())
+ sectionRectMap.set(sectionId, section.getBoundingClientRect())
+ }
+ }
+ });
+ performance.mark('end-get-section-spacers')
+ performance.measure('SectionAnnotationVisualizer.getSectionSpacers', 'start-get-section-spacers', 'end-get-section-spacers')
+
+ performance.mark('start-render-section-panels')
for (const panel of (panels as HTMLElement[])) {
const sectionId = panel.getAttribute('data-iaa-applies-to')
- const spacer = this.root.querySelector(`.iaa-visible-annotations-panel-spacer[data-iaa-applies-to="${sectionId}"]`)
+ if (!sectionId) {
+ console.warn(`Panel has no 'data-iaa-applies-to' attribute`, panel)
+ continue
+ }
+
+ const spacer = sectionSpacersMap.get(sectionId)
if (!spacer) {
console.warn(`No spacer found for section [${sectionId}]`)
continue
}
- const section = this.root.querySelector(`[id="${sectionId}"]`)
+
+ const section = this.root.ownerDocument.getElementById(sectionId)
if (!section) {
console.warn(`Cannot find element for section [${sectionId}]`)
continue
}
- // Fit the panels to the spacers
- const sectionRect = section.getBoundingClientRect()
- const spacerRect = spacer.getBoundingClientRect() // Dimensions same as panel
-
- const sectionLeavingViewport = sectionRect.bottom - spacerRect.height < rootRect.top
- // console.log(`Leaving viewport = ${sectionLeavingViewport}`)
- if (sectionLeavingViewport) {
- const hiddenUnderHigherLevelPanel = lastSectionPanelBottom && (sectionRect.bottom + rootRect.top - spacerRect.height) < lastSectionPanelBottom
- if (hiddenUnderHigherLevelPanel) {
- // If there is already a higher-level panel stacked then we snap the panel back to its
- // spacer immediately
- panel.style.position = 'fixed'
- panel.style.top = `${spacerRect.top}px`
+ performance.mark(`start-render-section-panel-${sectionId}`)
+ try {
+ // Fit the panels to the spacers
+ const sectionRect = sectionRectMap.get(sectionId)
+ const spacerRect = spacerRectMap.get(sectionId) // Dimensions same as panel
+
+ const sectionLeavingViewport = sectionRect.bottom - spacerRect.height < rootTop
+ // console.log(`Leaving viewport = ${sectionLeavingViewport}`)
+ if (sectionLeavingViewport) {
+ const hiddenUnderHigherLevelPanel = lastSectionPanelBottom && (sectionRect.bottom + rootTop - spacerRect.height) < lastSectionPanelBottom
+ if (hiddenUnderHigherLevelPanel) {
+ // If there is already a higher-level panel stacked then we snap the panel back to its
+ // spacer immediately
+ panel.style.position = 'fixed'
+ panel.style.top = `${spacerRect.top}px`
+ }
+ else {
+ // Otherwise, we move the panel along with the bottom of the section
+ panel.style.position = 'fixed'
+ panel.style.top = `${sectionRect.bottom - spacerRect.height}px`
+ }
+ continue
}
- else {
- // Otherwise, we move the panel along with the bottom of the section
+
+ const shouldKeepPanelVisibleAtTop = spacerRect.top < lastSectionPanelBottom && !(sectionRect.bottom < lastSectionPanelBottom)
+ if (shouldKeepPanelVisibleAtTop) {
+ // Keep the panel at the top of the viewport if the spacer is above the viewport
+ // and the section is still visible
panel.style.position = 'fixed'
- panel.style.top = `${sectionRect.bottom - spacerRect.height}px`
+ panel.style.top = `${lastSectionPanelBottom}px`
+ lastSectionPanelBottom += spacerRect.height
+ continue
}
- continue
- }
- const shouldKeepPanelVisibleAtTop = spacerRect.top < lastSectionPanelBottom && !(sectionRect.bottom < lastSectionPanelBottom)
- if (shouldKeepPanelVisibleAtTop) {
- // Keep the panel at the top of the viewport if the spacer is above the viewport
- // and the section is still visible
- panel.style.position = 'fixed'
- panel.style.top = `${lastSectionPanelBottom}px`
- lastSectionPanelBottom = panel.getBoundingClientRect().bottom
- continue
+ // Otherwise, keep the panel at the same position as the spacer
+ panel.style.position = 'absolute'
+ panel.style.top = `${spacerRect.top + scrollY}px`
+ }
+ finally {
+ performance.mark(`end-render-section-panel-${sectionId}`)
+ performance.measure(`SectionAnnotationVisualizer.renderSectionPanel-${sectionId}`, `start-render-section-panel-${sectionId}`, `end-render-section-panel-${sectionId}`)
}
-
- // Otherwise, keep the panel at the same position as the spacer
- panel.style.position = 'absolute'
- panel.style.top = `${spacerRect.top + scrollY}px`
}
+ performance.mark('end-render-section-panels')
+ performance.measure('SectionAnnotationVisualizer.renderSectionPanels', 'start-render-section-panels', 'end-render-section-panels')
if (root instanceof HTMLElement) {
- root.style.scrollPaddingTop = `${lastSectionPanelBottom - rootRect.top}px`
+ root.style.scrollPaddingTop = `${lastSectionPanelBottom - rootTop}px`
}
+
+ performance.mark('end-ensure-panel-visibility')
+ performance.measure(`SectionAnnotationVisualizer.ensurePanelVisibility (${reason})`, 'start-ensure-panel-visibility', 'end-ensure-panel-visibility')
}
private renderSectionGroups(doc: AnnotatedText) {
@@ -130,6 +195,7 @@ export class SectionAnnotationVisualizer {
}
// Create an annotation panel for each section
+ performance.mark('start-create-annotation-panels')
const annotationPanelsBySectionElement = new Map()
const annotationPanelsByVid = new Map()
for (const [vid, sectionElement] of sectionElements) {
@@ -148,8 +214,11 @@ export class SectionAnnotationVisualizer {
annotationPanelsByVid.set(vid, panel)
}
+ performance.mark('end-create-annotation-panels')
+ performance.measure('SectionAnnotationVisualizer.createAnnotationPanels', 'start-create-annotation-panels', 'end-create-annotation-panels')
- // Render the annotations for each section
+ // Render the section panels
+ performance.mark('start-render-section-panels')
for (const vid of highlightsByVid.keys()) {
const panel = annotationPanelsByVid.get(vid)
if (!panel) continue
@@ -159,13 +228,14 @@ export class SectionAnnotationVisualizer {
panel.appendChild(this.createAnnotationPanelItem(span, selectedAnnotationVids))
}
}
-
- // Fit the panels to the sections
- const panels = Array.from(this.root.parentNode?.querySelectorAll('.iaa-visible-annotations-panel') || [])
- const toProcess: {panel: HTMLElement, spacer: HTMLElement, section: HTMLElement}[] = []
+ performance.mark('end-render-section-panels')
+ performance.measure('SectionAnnotationVisualizer.renderSectionPanels', 'start-render-section-panels', 'end-render-section-panels')
// Prepare the spacers without changing the DOM so layout due to getBoundingClientRect() is not
// triggered repeatedly
+ performance.mark('start-create-spacers')
+ const toProcess: {panel: HTMLElement, spacer: HTMLElement, section: HTMLElement}[] = []
+ const panels = Array.from(this.root.parentNode?.querySelectorAll('.iaa-visible-annotations-panel') || [])
for (const panel of (panels as HTMLElement[])) {
const appliesTo = panel.getAttribute('data-iaa-applies-to')
if (!appliesTo) continue
@@ -173,6 +243,7 @@ export class SectionAnnotationVisualizer {
const section = document.getElementById(appliesTo) as HTMLElement
if (!section) continue
+ performance.mark(`start-create-spacer-${appliesTo}`)
// The spacer reserves space for the panel in the document layout. The actual panel
// will then float over the spacer when possible but be adjusted such that it remains
// visible even if the spacer starts moving out of the screen
@@ -181,13 +252,20 @@ export class SectionAnnotationVisualizer {
spacer.classList.add('iaa-visible-annotations-panel-spacer')
spacer.style.height = `${panel.getBoundingClientRect().height}px`
toProcess.push({panel, spacer, section});
+ performance.mark(`end-create-spacer-${appliesTo}`)
+ performance.measure(`SectionAnnotationVisualizer.createSpacer-${appliesTo}`, `start-create-spacer-${appliesTo}`, `end-create-spacer-${appliesTo}`)
}
+ performance.mark('end-create-spacers')
+ performance.measure('SectionAnnotationVisualizer.createSpacers', 'start-create-spacers', 'end-create-spacers')
// Add the spacers to the DOM all at once without triggering a re-layout in between
+ performance.mark('start-insert-spacers')
for (const parts of toProcess) {
const section = parts.section
section.parentElement?.insertBefore(parts.spacer, section)
}
+ performance.mark('end-insert-spacers')
+ performance.measure('SectionAnnotationVisualizer.insertSpacers', 'start-insert-spacers', 'end-insert-spacers')
}
private groupHighlightsByVid(spans: NodeListOf) {
@@ -278,4 +356,18 @@ export class SectionAnnotationVisualizer {
console.error('Parent element of root element not found - cannot add visible annotations panel')
}
}
-}
\ No newline at end of file
+}
+
+export function getTop(root) {
+ if (root instanceof HTMLElement) {
+ return (root.offsetTop || 0)
+ } else {
+ const rootRect = root.getBoundingClientRect()
+ return rootRect.top
+ }
+}
+
+export function getScrollY(root) {
+ return (root.scrollTop || 0) - getTop(root)
+}
+