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) +} +