UNPKG

listine

Version:

Angular virtual scroll component supporting variable item heights.

207 lines 28.1 kB
import { Component, Input, ViewChild, ElementRef, ChangeDetectionStrategy, ViewChildren, EventEmitter, Output, } from "@angular/core"; import { CommonModule } from "@angular/common"; import * as i0 from "@angular/core"; import * as i1 from "@angular/common"; /** * `s73-variable-virtual-scroll` * * A lightweight, customizable virtual scroll component that supports variable item heights. * Efficiently renders only visible items to improve performance for large lists. * * ## Inputs: * - `items`: List of data items to display * - `viewportHeight`: Height of the scrollable container (default: 400px) * - `buffer`: Number of extra items rendered above and below the viewport for smooth scrolling (default: 5) * - `itemTemplate`: Angular template for rendering each item * - `initialItemHeight`: Default item height before measurement (default: 50px) * * ## Outputs: * - `scrollEmitter`: Emits scroll position on every scroll event */ export class ListineVirtualScrollModule { cdr; ngZone; /** List of all items to render */ items = []; /** Height of the scrollable viewport in pixels */ viewportHeight = 400; /** Number of extra items to render above and below the viewport */ buffer = 5; /** Template reference for rendering each item */ itemTemplate; /** Flag to track if panel is open */ panelOpen = false; /** Initial estimated height of each item before actual measurement */ initialItemHeight = 50; /** Emits scroll position whenever user scrolls */ scrollEmitter = new EventEmitter(); /** Emits when user scrolls to the end of the list */ scrollToEnd = new EventEmitter(); /** Reference to the scrolling container */ scrollerRef; /** QueryList of rendered item elements (used for height measurement) */ itemElements; /** Stores measured heights of items */ itemHeights = []; /** Stores calculated top offset for each item */ itemTops = []; /** Total height of all items (used to simulate full scrollable area) */ totalContentHeight = 0; /** Index of the first visible item (including buffer) */ visibleStart = 0; /** Index of the last visible item (including buffer) */ visibleEnd = 0; /** Items currently visible in the viewport */ visibleItems = []; /** ResizeObserver to watch for item height changes */ resizeObserver = new ResizeObserver(() => { this.ngZone.run(() => { this.measureItemHeights(); }); }); /** Previous scroll position to detect actual scrolling */ previousScrollTop = 0; constructor(cdr, ngZone) { this.cdr = cdr; this.ngZone = ngZone; } /** Initialize item heights on component initialization */ ngOnInit() { this.initializeHeights(); } /** * Respond to changes in input bindings (mainly the `items` array) * @param changes - input property changes */ ngOnChanges(changes) { if (changes["items"]) { this.items = changes["items"].currentValue; this.initializeHeights(); setTimeout(() => { this.calculateHeights(); this.onScroll(); }); } if (changes["panelOpen"] && changes["panelOpen"]?.currentValue) { this.panelOpen = changes["panelOpen"]?.currentValue; this.onScroll(); } } /** * Called after view initialization. Triggers initial height calculation. */ ngAfterViewInit() { setTimeout(() => { this.calculateHeights(); this.onScroll(); }); } /** * Disconnects ResizeObserver on component destroy */ ngOnDestroy() { this.resizeObserver.disconnect(); } /** Initializes item height and position tracking arrays */ initializeHeights() { this.itemHeights = this.items.map(() => this.initialItemHeight); this.updateItemTops(); } /** Recalculates item top positions and content height */ calculateHeights() { this.updateItemTops(); } /** Updates top positions of all items based on their current heights */ updateItemTops() { this.itemTops = []; let top = 0; for (const height of this.itemHeights) { this.itemTops.push(top); top += height; } this.totalContentHeight = top; } /** * Returns the top position of an item by index * @param index - index of the item */ getItemTop(index) { return this.itemTops[index] || 0; } /** Scroll event handler: calculates which items should be visible */ onScroll() { const scrollTop = this.scrollerRef.nativeElement.scrollTop; const viewportBottom = scrollTop + this.viewportHeight; let start = 0; while (start < this.items.length && this.getItemTop(start + 1) < scrollTop) { start++; } let end = start; while (end < this.items.length && this.getItemTop(end) < viewportBottom) { end++; } this.visibleStart = Math.max(0, start - this.buffer); this.visibleEnd = Math.min(this.items.length, end + this.buffer); this.updateVisibleItems(); this.scrollEmitter.emit(scrollTop); // Check if scrolled to the end const scrollHeight = this.scrollerRef.nativeElement.scrollHeight; const clientHeight = this.scrollerRef.nativeElement.clientHeight; const isAtEnd = scrollTop + clientHeight >= scrollHeight - 1; if (isAtEnd && scrollTop > this.previousScrollTop && scrollTop > 0) { this.scrollToEnd.emit(); } this.previousScrollTop = scrollTop; } /** Updates the list of items to be rendered based on scroll position */ updateVisibleItems() { this.visibleItems = this.items.slice(this.visibleStart, this.visibleEnd); // Wait for DOM render before measuring heights setTimeout(() => { this.measureItemHeights(); }); } /** Measures actual heights of rendered DOM elements and updates tracking */ measureItemHeights() { this.itemElements.forEach((el, idx) => { const index = this.visibleStart + idx; const height = el.nativeElement.offsetHeight; if (this.itemHeights[index] !== height) { this.itemHeights[index] = height; this.resizeObserver.observe(el.nativeElement); } }); this.updateItemTops(); this.cdr.markForCheck(); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ListineVirtualScrollModule, deps: [{ token: i0.ChangeDetectorRef }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: ListineVirtualScrollModule, isStandalone: true, selector: "listine-variable-virtual-scroll", inputs: { items: "items", viewportHeight: "viewportHeight", buffer: "buffer", itemTemplate: "itemTemplate", panelOpen: "panelOpen", initialItemHeight: "initialItemHeight" }, outputs: { scrollEmitter: "scrollEmitter", scrollToEnd: "scrollToEnd" }, viewQueries: [{ propertyName: "scrollerRef", first: true, predicate: ["scroller"], descendants: true, static: true }, { propertyName: "itemElements", predicate: ["itemElement"], descendants: true, read: ElementRef }], usesOnChanges: true, ngImport: i0, template: "<div\n #scroller\n class=\"scroll-container\"\n (scroll)=\"onScroll()\"\n [style.height.px]=\"viewportHeight\"\n>\n <div\n class=\"total-height\"\n [style.height.px]=\"totalContentHeight - viewportHeight\"\n >\n <ng-container *ngFor=\"let item of visibleItems; let i = index\">\n <div\n #itemElement\n class=\"item\"\n [style.transform]=\"'translateY(' + getItemTop(visibleStart + i) + 'px)'\"\n >\n <ng-container\n *ngTemplateOutlet=\"itemTemplate; context: { $implicit: item }\"\n ></ng-container>\n </div>\n </ng-container>\n </div>\n</div>\n", styles: [".scroll-container{overflow-y:auto;position:relative;width:100%;background:#fff}.total-height{position:relative;width:100%}.item{position:absolute;width:100%;box-sizing:border-box;will-change:transform}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ListineVirtualScrollModule, decorators: [{ type: Component, args: [{ selector: "listine-variable-virtual-scroll", standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div\n #scroller\n class=\"scroll-container\"\n (scroll)=\"onScroll()\"\n [style.height.px]=\"viewportHeight\"\n>\n <div\n class=\"total-height\"\n [style.height.px]=\"totalContentHeight - viewportHeight\"\n >\n <ng-container *ngFor=\"let item of visibleItems; let i = index\">\n <div\n #itemElement\n class=\"item\"\n [style.transform]=\"'translateY(' + getItemTop(visibleStart + i) + 'px)'\"\n >\n <ng-container\n *ngTemplateOutlet=\"itemTemplate; context: { $implicit: item }\"\n ></ng-container>\n </div>\n </ng-container>\n </div>\n</div>\n", styles: [".scroll-container{overflow-y:auto;position:relative;width:100%;background:#fff}.total-height{position:relative;width:100%}.item{position:absolute;width:100%;box-sizing:border-box;will-change:transform}\n"] }] }], ctorParameters: () => [{ type: i0.ChangeDetectorRef }, { type: i0.NgZone }], propDecorators: { items: [{ type: Input }], viewportHeight: [{ type: Input }], buffer: [{ type: Input }], itemTemplate: [{ type: Input }], panelOpen: [{ type: Input }], initialItemHeight: [{ type: Input }], scrollEmitter: [{ type: Output }], scrollToEnd: [{ type: Output }], scrollerRef: [{ type: ViewChild, args: ["scroller", { static: true }] }], itemElements: [{ type: ViewChildren, args: ["itemElement", { read: ElementRef }] }] } }); //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibGlzdGluZS12aXJ0dWFsLXNjcm9sbC5jb21wb25lbnQuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi8uLi8uLi9wcm9qZWN0cy9saXN0aW5lL3NyYy9saWIvbGlzdHRpbmUtdmlydHVhbC92YXJpYWJsZS12aXJ0dWFsLXNjcm9sbC9saXN0aW5lLXZpcnR1YWwtc2Nyb2xsLmNvbXBvbmVudC50cyIsIi4uLy4uLy4uLy4uLy4uLy4uL3Byb2plY3RzL2xpc3RpbmUvc3JjL2xpYi9saXN0dGluZS12aXJ0dWFsL3ZhcmlhYmxlLXZpcnR1YWwtc2Nyb2xsL2xpc3RpbmUtdmlydHVhbC1zY3JvbGwuY29tcG9uZW50Lmh0bWwiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUNMLFNBQVMsRUFDVCxLQUFLLEVBRUwsU0FBUyxFQUNULFVBQVUsRUFFVix1QkFBdUIsRUFFdkIsWUFBWSxFQUVaLFlBQVksRUFDWixNQUFNLEdBTVAsTUFBTSxlQUFlLENBQUM7QUFDdkIsT0FBTyxFQUFFLFlBQVksRUFBRSxNQUFNLGlCQUFpQixDQUFDOzs7QUFFL0M7Ozs7Ozs7Ozs7Ozs7OztHQWVHO0FBU0gsTUFBTSxPQUFPLDBCQUEwQjtJQThEakI7SUFBZ0M7SUEzRHBELGtDQUFrQztJQUN6QixLQUFLLEdBQVUsRUFBRSxDQUFDO0lBRTNCLGtEQUFrRDtJQUN6QyxjQUFjLEdBQUcsR0FBRyxDQUFDO0lBRTlCLG1FQUFtRTtJQUMxRCxNQUFNLEdBQUcsQ0FBQyxDQUFDO0lBRXBCLGlEQUFpRDtJQUN4QyxZQUFZLENBQW1CO0lBRXhDLHFDQUFxQztJQUM1QixTQUFTLEdBQVksS0FBSyxDQUFDO0lBRXBDLHNFQUFzRTtJQUM3RCxpQkFBaUIsR0FBRyxFQUFFLENBQUM7SUFFaEMsa0RBQWtEO0lBQ3hDLGFBQWEsR0FBRyxJQUFJLFlBQVksRUFBTyxDQUFDO0lBRWxELHFEQUFxRDtJQUMzQyxXQUFXLEdBQUcsSUFBSSxZQUFZLEVBQVEsQ0FBQztJ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YUFBYSxDQUFDLENBQUM7YUFDL0M7UUFDSCxDQUFDLENBQUMsQ0FBQztRQUNILElBQUksQ0FBQyxjQUFjLEVBQUUsQ0FBQztRQUN0QixJQUFJLENBQUMsR0FBRyxDQUFDLFlBQVksRUFBRSxDQUFDO0lBQzFCLENBQUM7d0dBaE1VLDBCQUEwQjs0RkFBMUIsMEJBQTBCLHFnQkErQkEsVUFBVSxrREM1RWpELGluQkF1QkEsb1FEaUJZLFlBQVk7OzRGQUtYLDBCQUEwQjtrQkFSdEMsU0FBUzsrQkFDRSxpQ0FBaUMsY0FDL0IsSUFBSSxXQUNQLENBQUMsWUFBWSxDQUFDLG1CQUdOLHVCQUF1QixDQUFDLE1BQU07MkdBTXRDLEtBQUs7c0JBQWIsS0FBSztnQkFHRyxjQUFjO3NCQUF0QixLQUFLO2dCQUdHLE1BQU07c0JBQWQsS0FBSztnQkFHRyxZQUFZO3NCQUFwQixLQUFLO2dCQUdHLFNBQVM7c0JBQWpCLEtBQUs7Z0JBR0csaUJBQWlCO3NCQUF6QixLQUFLO2dCQUdJLGFBQWE7c0JBQXRCLE1BQU07Z0JBR0csV0FBVztzQkFBcEIsTUFBTTtnQkFHa0MsV0FBVztzQkFBbkQsU0FBUzt1QkFBQyxVQUFVLEVBQUUsRUFBRSxNQUFNLEVBQUUsSUFBSSxFQUFFO2dCQUl2QyxZQUFZO3NCQURYLFlBQVk7dUJBQUMsYUFBYSxFQUFFLEVBQUUsSUFBSSxFQUFFLFVBQVUsRUFBRSIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7XG4gIENvbXBvbmVudCxcbiAgSW5wdXQsXG4gIFRlbXBsYXRlUmVmLFxuICBWaWV3Q2hpbGQsXG4gIEVsZW1lbnRSZWYsXG4gIEFmdGVyVmlld0luaXQsXG4gIENoYW5nZURldGVjdGlvblN0cmF0ZWd5LFxuICBDaGFuZ2VEZXRlY3RvclJlZixcbiAgVmlld0NoaWxkcmVuLFxuICBRdWVyeUxpc3QsXG4gIEV2ZW50RW1pdHRlcixcbiAgT3V0cHV0LFxuICBPbkNoYW5nZXMsXG4gIFNpbXBsZUNoYW5nZXMsXG4gIE9uSW5pdCxcbiAgT25EZXN0cm95LFxuICBOZ1pvbmUsXG59IGZyb20gXCJAYW5ndWxhci9jb3JlXCI7XG5pbXBvcnQgeyBDb21tb25Nb2R1bGUgfSBmcm9tIFwiQGFuZ3VsYXIvY29tbW9uXCI7XG5cbi8qKlxuICogYHM3My12YXJpYWJsZS12aXJ0dWFsLXNjcm9sbGBcbiAqXG4gKiBBIGxpZ2h0d2VpZ2h0LCBjdXN0b21pemFibGUgdmlydHVhbCBzY3JvbGwgY29tcG9uZW50IHRoYXQgc3VwcG9ydHMgdmFyaWFibGUgaXRlbSBoZWlnaHRzLlxuICogRWZmaWNpZW50bHkgcmVuZGVycyBvbmx5IHZpc2libGUgaXRlbXMgdG8gaW1wcm92ZSBwZXJmb3JtYW5jZSBmb3IgbGFyZ2UgbGlzdHMuXG4gKlxuICogIyMgSW5wdXRzOlxuICogLSBgaXRlbXNgOiBMaXN0IG9mIGRhdGEgaXRlbXMgdG8gZGlzcGxheVxuICogLSBgdmlld3BvcnRIZWlnaHRgOiBIZWlnaHQgb2YgdGhlIHNjcm9sbGFibGUgY29udGFpbmVyIChkZWZhdWx0OiA0MDBweClcbiAqIC0gYGJ1ZmZlcmA6IE51bWJlciBvZiBleHRyYSBpdGVtcyByZW5kZXJlZCBhYm92ZSBhbmQgYmVsb3cgdGhlIHZpZXdwb3J0IGZvciBzbW9vdGggc2Nyb2xsaW5nIChkZWZhdWx0OiA1KVxuICogLSBgaXRlbVRlbXBsYXRlYDogQW5ndWxhciB0ZW1wbGF0ZSBmb3IgcmVuZGVyaW5nIGVhY2ggaXRlbVxuICogLSBgaW5pdGlhbEl0ZW1IZWlnaHRgOiBEZWZhdWx0IGl0ZW0gaGVpZ2h0IGJlZm9yZSBtZWFzdXJlbWVudCAoZGVmYXVsdDogNTBweClcbiAqXG4gKiAjIyBPdXRwdXRzOlxuICogLSBgc2Nyb2xsRW1pdHRlcmA6IEVtaXRzIHNjcm9sbCBwb3NpdGlvbiBvbiBldmVyeSBzY3JvbGwgZXZlbnRcbiAqL1xuQENvbXBvbmVudCh7XG4gIHNlbGVjdG9yOiBcImxpc3RpbmUtdmFyaWFibGUtdmlydHVhbC1zY3JvbGxcIixcbiAgc3RhbmRhbG9uZTogdHJ1ZSxcbiAgaW1wb3J0czogW0NvbW1vbk1vZHVsZV0sXG4gIHRlbXBsYXRlVXJsOiBcIi4vbGlzdGluZS12aXJ0dWFsLXNjcm9sbC5jb21wb25lbnQuaHRtbFwiLFxuICBzdHlsZVVybHM6IFtcIi4vbGlzdGluZS12aXJ0dWFsLXNjcm9sbC5jb21wb25lbnQuc2Nzc1wiXSxcbiAgY2hhbmdlRGV0ZWN0aW9uOiBDaGFuZ2VEZXRlY3Rpb25TdHJhdGVneS5PblB1c2gsXG59KVxuZXhwb3J0IGNsYXNzIExpc3RpbmVWaXJ0dWFsU2Nyb2xsTW9kdWxlXG4gIGltcGxlbWVudHMgT25Jbml0LCBBZnRlclZpZXdJbml0LCBPbkNoYW5nZXMsIE9uRGVzdHJveVxue1xuICAvKiogTGlzdCBvZiBhbGwgaXRlbXMgdG8gcmVuZGVyICovXG4gIEBJbnB1dCgpIGl0ZW1zOiBhbnlbXSA9IFtdO1xuXG4gIC8qKiBIZWlnaHQgb2YgdGhlIHNjcm9sbGFibGUgdmlld3BvcnQgaW4gcGl4ZWxzICovXG4gIEBJbnB1dCgpIHZpZXdwb3J0SGVpZ2h0ID0gNDAwO1xuXG4gIC8qKiBOdW1iZXIgb2YgZXh0cmEgaXRlbXMgdG8gcmVuZGVyIGFib3ZlIGFuZCBiZWxvdyB0aGUgdmlld3BvcnQgKi9cbiAgQElucHV0KCkgYnVmZmVyID0gNTtcblxuICAvKiogVGVtcGxhdGUgcmVmZXJlbmNlIGZvciByZW5kZXJpbmcgZWFjaCBpdGVtICovXG4gIEBJbnB1dCgpIGl0ZW1UZW1wbGF0ZTogVGVtcGxhdGVSZWY8YW55PjtcblxuICAvKiogRmxhZyB0byB0cmFjayBpZiBwYW5lbCBpcyBvcGVuICovXG4gIEBJbnB1dCgpIHBhbmVsT3BlbjogYm9vbGVhbiA9IGZhbHNlO1xuXG4gIC8qKiBJbml0aWFsIGVzdGltYXRlZCBoZWlnaHQgb2YgZWFjaCBpdGVtIGJlZm9yZSBhY3R1YWwgbWVhc3VyZW1lbnQgKi9cbiAgQElucHV0KCkgaW5pdGlhbEl0ZW1IZWlnaHQgPSA1MDtcblxuICAvKiogRW1pdHMgc2Nyb2xsIHBvc2l0aW9uIHdoZW5ldmVyIHVzZXIgc2Nyb2xscyAqL1xuICBAT3V0cHV0KCkgc2Nyb2xsRW1pdHRlciA9IG5ldyBFdmVudEVtaXR0ZXI8YW55PigpO1xuXG4gIC8qKiBFbWl0cyB3aGVuIHVzZXIgc2Nyb2xscyB0byB0aGUgZW5kIG9mIHRoZSBsaXN0ICovXG4gIEBPdXRwdXQoKSBzY3JvbGxUb0VuZCA9IG5ldyBFdmVudEVtaXR0ZXI8dm9pZD4oKTtcblxuICAvKiogUmVmZXJlbmNlIHRvIHRoZSBzY3JvbGxpbmcgY29udGFpbmVyICovXG4gIEBWaWV3Q2hpbGQoXCJzY3JvbGxlclwiLCB7IHN0YXRpYzogdHJ1ZSB9KSBzY3JvbGxlclJlZiE6IEVsZW1lbnRSZWY7XG5cbiAgLyoqIFF1ZXJ5TGlzdCBvZiByZW5kZXJlZCBpdGVtIGVsZW1lbnRzICh1c2VkIGZvciBoZWlnaHQgbWVhc3VyZW1lbnQpICovXG4gIEBWaWV3Q2hpbGRyZW4oXCJpdGVtRWxlbWVudFwiLCB7IHJlYWQ6IEVsZW1lbnRSZWYgfSlcbiAgaXRlbUVsZW1lbnRzITogUXVlcnlMaXN0PEVsZW1lbnRSZWY+O1xuXG4gIC8qKiBTdG9yZXMgbWVhc3VyZWQgaGVpZ2h0cyBvZiBpdGVtcyAqL1xuICBpdGVtSGVpZ2h0czogbnVtYmVyW10gPSBbXTtcblxuICAvKiogU3RvcmVzIGNhbGN1bGF0ZWQgdG9wIG9mZnNldCBmb3IgZWFjaCBpdGVtICovXG4gIGl0ZW1Ub3BzOiBudW1iZXJbXSA9IFtdO1xuXG4gIC8qKiBUb3RhbCBoZWlnaHQgb2YgYWxsIGl0ZW1zICh1c2VkIHRvIHNpbXVsYXRlIGZ1bGwgc2Nyb2xsYWJsZSBhcmVhKSAqL1xuICB0b3RhbENvbnRlbnRIZWlnaHQgPSAwO1xuXG4gIC8qKiBJbmRleCBvZiB0aGUgZmlyc3QgdmlzaWJsZSBpdGVtIChpbmNsdWRpbmcgYnVmZmVyKSAqL1xuICB2aXNpYmxlU3RhcnQgPSAwO1xuXG4gIC8qKiBJbmRleCBvZiB0aGUgbGFzdCB2aXNpYmxlIGl0ZW0gKGluY2x1ZGluZyBidWZmZXIpICovXG4gIHZpc2libGVFbmQgPSAwO1xuXG4gIC8qKiBJdGVtcyBjdXJyZW50bHkgdmlzaWJsZSBpbiB0aGUgdmlld3BvcnQgKi9cbiAgdmlzaWJsZUl0ZW1zOiBhbnlbXSA9IFtdO1xuXG4gIC8qKiBSZXNpemVPYnNlcnZlciB0byB3YXRjaCBmb3IgaXRlbSBoZWlnaHQgY2hhbmdlcyAqL1xuICBwcml2YXRlIHJlc2l6ZU9ic2VydmVyID0gbmV3IFJlc2l6ZU9ic2VydmVyKCgpID0+IHtcbiAgICB0aGlzLm5nWm9uZS5ydW4oKCkgPT4ge1xuICAgICAgdGhpcy5tZWFzdXJlSXRlbUhlaWdodHMoKTtcbiAgICB9KTtcbiAgfSk7XG5cbiAgLyoqIFByZXZpb3VzIHNjcm9sbCBwb3NpdGlvbiB0byBkZXRlY3QgYWN0dWFsIHNjcm9sbGluZyAqL1xuICBwcml2YXRlIHByZXZpb3VzU2Nyb2xsVG9wID0gMDtcblxuICBjb25zdHJ1Y3Rvcihwcml2YXRlIGNkcjogQ2hhbmdlRGV0ZWN0b3JSZWYsIHByaXZhdGUgbmdab25lOiBOZ1pvbmUpIHt9XG5cbiAgLyoqIEluaXRpYWxpemUgaXRlbSBoZWlnaHRzIG9uIGNvbXBvbmVudCBpbml0aWFsaXphdGlvbiAqL1xuICBuZ09uSW5pdCgpOiB2b2lkIHtcbiAgICB0aGlzLmluaXRpYWxpemVIZWlnaHRzKCk7XG4gIH1cblxuICAvKipcbiAgICogUmVzcG9uZCB0byBjaGFuZ2VzIGluIGlucHV0IGJpbmRpbmdzIChtYWlubHkgdGhlIGBpdGVtc2AgYXJyYXkpXG4gICAqIEBwYXJhbSBjaGFuZ2VzIC0gaW5wdXQgcHJvcGVydHkgY2hhbmdlc1xuICAgKi9cbiAgbmdPbkNoYW5nZXMoY2hhbmdlczogU2ltcGxlQ2hhbmdlcyk6IHZvaWQge1xuICAgIGlmIChjaGFuZ2VzW1wiaXRlbXNcIl0pIHtcbiAgICAgIHRoaXMuaXRlbXMgPSBjaGFuZ2VzW1wiaXRlbXNcIl0uY3VycmVudFZhbHVlO1xuICAgICAgdGhpcy5pbml0aWFsaXplSGVpZ2h0cygpO1xuXG4gICAgICBzZXRUaW1lb3V0KCgpID0+IHtcbiAgICAgICAgdGhpcy5jYWxjdWxhdGVIZWlnaHRzKCk7XG4gICAgICAgIHRoaXMub25TY3JvbGwoKTtcbiAgICAgIH0pO1xuICAgIH1cbiAgICBpZiAoY2hhbmdlc1tcInBhbmVsT3BlblwiXSAmJiBjaGFuZ2VzW1wicGFuZWxPcGVuXCJdPy5jdXJyZW50VmFsdWUpIHtcbiAgICAgIHRoaXMucGFuZWxPcGVuID0gY2hhbmdlc1tcInBhbmVsT3BlblwiXT8uY3VycmVudFZhbHVlO1xuICAgICAgdGhpcy5vblNjcm9sbCgpO1xuICAgIH1cbiAgfVxuXG4gIC8qKlxuICAgKiBDYWxsZWQgYWZ0ZXIgdmlldyBpbml0aWFsaXphdGlvbi4gVHJpZ2dlcnMgaW5pdGlhbCBoZWlnaHQgY2FsY3VsYXRpb24uXG4gICAqL1xuICBuZ0FmdGVyVmlld0luaXQoKSB7XG4gICAgc2V0VGltZW91dCgoKSA9PiB7XG4gICAgICB0aGlzLmNhbGN1bGF0ZUhlaWdodHMoKTtcbiAgICAgIHRoaXMub25TY3JvbGwoKTtcbiAgICB9KTtcbiAgfVxuXG4gIC8qKlxuICAgKiBEaXNjb25uZWN0cyBSZXNpemVPYnNlcnZlciBvbiBjb21wb25lbnQgZGVzdHJveVxuICAgKi9cbiAgbmdPbkRlc3Ryb3koKTogdm9pZCB7XG4gICAgdGhpcy5yZXNpemVPYnNlcnZlci5kaXNjb25uZWN0KCk7XG4gIH1cblxuICAvKiogSW5pdGlhbGl6ZXMgaXRlbSBoZWlnaHQgYW5kIHBvc2l0aW9uIHRyYWNraW5nIGFycmF5cyAqL1xuICBpbml0aWFsaXplSGVpZ2h0cygpIHtcbiAgICB0aGlzLml0ZW1IZWlnaHRzID0gdGhpcy5pdGVtcy5tYXAoKCkgPT4gdGhpcy5pbml0aWFsSXRlbUhlaWdodCk7XG4gICAgdGhpcy51cGRhdGVJdGVtVG9wcygpO1xuICB9XG5cbiAgLyoqIFJlY2FsY3VsYXRlcyBpdGVtIHRvcCBwb3NpdGlvbnMgYW5kIGNvbnRlbnQgaGVpZ2h0ICovXG4gIGNhbGN1bGF0ZUhlaWdodHMoKSB7XG4gICAgdGhpcy51cGRhdGVJdGVtVG9wcygpO1xuICB9XG5cbiAgLyoqIFVwZGF0ZXMgdG9wIHBvc2l0aW9ucyBvZiBhbGwgaXRlbXMgYmFzZWQgb24gdGhlaXIgY3VycmVudCBoZWlnaHRzICovXG4gIHVwZGF0ZUl0ZW1Ub3BzKCkge1xuICAgIHRoaXMuaXRlbVRvcHMgPSBbXTtcbiAgICBsZXQgdG9wID0gMDtcbiAgICBmb3IgKGNvbnN0IGhlaWdodCBvZiB0aGlzLml0ZW1IZWlnaHRzKSB7XG4gICAgICB0aGlzLml0ZW1Ub3BzLnB1c2godG9wKTtcbiAgICAgIHRvcCArPSBoZWlnaHQ7XG4gICAgfVxuICAgIHRoaXMudG90YWxDb250ZW50SGVpZ2h0ID0gdG9wO1xuICB9XG5cbiAgLyoqXG4gICAqIFJldHVybnMgdGhlIHRvcCBwb3NpdGlvbiBvZiBhbiBpdGVtIGJ5IGluZGV4XG4gICAqIEBwYXJhbSBpbmRleCAtIGluZGV4IG9mIHRoZSBpdGVtXG4gICAqL1xuICBnZXRJdGVtVG9wKGluZGV4OiBudW1iZXIpOiBudW1iZXIge1xuICAgIHJldHVybiB0aGlzLml0ZW1Ub3BzW2luZGV4XSB8fCAwO1xuICB9XG5cbiAgLyoqIFNjcm9sbCBldmVudCBoYW5kbGVyOiBjYWxjdWxhdGVzIHdoaWNoIGl0ZW1zIHNob3VsZCBiZSB2aXNpYmxlICovXG4gIG9uU2Nyb2xsKCkge1xuICAgIGNvbnN0IHNjcm9sbFRvcCA9IHRoaXMuc2Nyb2xsZXJSZWYubmF0aXZlRWxlbWVudC5zY3JvbGxUb3A7XG4gICAgY29uc3Qgdmlld3BvcnRCb3R0b20gPSBzY3JvbGxUb3AgKyB0aGlzLnZpZXdwb3J0SGVpZ2h0O1xuXG4gICAgbGV0IHN0YXJ0ID0gMDtcbiAgICB3aGlsZSAoXG4gICAgICBzdGFydCA8IHRoaXMuaXRlbXMubGVuZ3RoICYmXG4gICAgICB0aGlzLmdldEl0ZW1Ub3Aoc3RhcnQgKyAxKSA8IHNjcm9sbFRvcFxuICAgICkge1xuICAgICAgc3RhcnQrKztcbiAgICB9XG5cbiAgICBsZXQgZW5kID0gc3RhcnQ7XG4gICAgd2hpbGUgKGVuZCA8IHRoaXMuaXRlbXMubGVuZ3RoICYmIHRoaXMuZ2V0SXRlbVRvcChlbmQpIDwgdmlld3BvcnRCb3R0b20pIHtcbiAgICAgIGVuZCsrO1xuICAgIH1cbiAgICB0aGlzLnZpc2libGVTdGFydCA9IE1hdGgubWF4KDAsIHN0YXJ0IC0gdGhpcy5idWZmZXIpO1xuICAgIHRoaXMudmlzaWJsZUVuZCA9IE1hdGgubWluKHRoaXMuaXRlbXMubGVuZ3RoLCBlbmQgKyB0aGlzLmJ1ZmZlcik7XG4gICAgdGhpcy51cGRhdGVWaXNpYmxlSXRlbXMoKTtcbiAgICB0aGlzLnNjcm9sbEVtaXR0ZXIuZW1pdChzY3JvbGxUb3ApO1xuXG4gICAgLy8gQ2hlY2sgaWYgc2Nyb2xsZWQgdG8gdGhlIGVuZFxuICAgIGNvbnN0IHNjcm9sbEhlaWdodCA9IHRoaXMuc2Nyb2xsZXJSZWYubmF0aXZlRWxlbWVudC5zY3JvbGxIZWlnaHQ7XG4gICAgY29uc3QgY2xpZW50SGVpZ2h0ID0gdGhpcy5zY3JvbGxlclJlZi5uYXRpdmVFbGVtZW50LmNsaWVudEhlaWdodDtcbiAgICBjb25zdCBpc0F0RW5kID0gc2Nyb2xsVG9wICsgY2xpZW50SGVpZ2h0ID49IHNjcm9sbEhlaWdodCAtIDE7XG5cbiAgICBpZiAoaXNBdEVuZCAmJiBzY3JvbGxUb3AgPiB0aGlzLnByZXZpb3VzU2Nyb2xsVG9wICYmIHNjcm9sbFRvcCA+IDApIHtcbiAgICAgIHRoaXMuc2Nyb2xsVG9FbmQuZW1pdCgpO1xuICAgIH1cblxuICAgIHRoaXMucHJldmlvdXNTY3JvbGxUb3AgPSBzY3JvbGxUb3A7XG4gIH1cblxuICAvKiogVXBkYXRlcyB0aGUgbGlzdCBvZiBpdGVtcyB0byBiZSByZW5kZXJlZCBiYXNlZCBvbiBzY3JvbGwgcG9zaXRpb24gKi9cbiAgdXBkYXRlVmlzaWJsZUl0ZW1zKCkge1xuICAgIHRoaXMudmlzaWJsZUl0ZW1zID0gdGhpcy5pdGVtcy5zbGljZSh0aGlzLnZpc2libGVTdGFydCwgdGhpcy52aXNpYmxlRW5kKTtcblxuICAgIC8vIFdhaXQgZm9yIERPTSByZW5kZXIgYmVmb3JlIG1lYXN1cmluZyBoZWlnaHRzXG4gICAgc2V0VGltZW91dCgoKSA9PiB7XG4gICAgICB0aGlzLm1lYXN1cmVJdGVtSGVpZ2h0cygpO1xuICAgIH0pO1xuICB9XG5cbiAgLyoqIE1lYXN1cmVzIGFjdHVhbCBoZWlnaHRzIG9mIHJlbmRlcmVkIERPTSBlbGVtZW50cyBhbmQgdXBkYXRlcyB0cmFja2luZyAqL1xuICBtZWFzdXJlSXRlbUhlaWdodHMoKSB7XG4gICAgdGhpcy5pdGVtRWxlbWVudHMuZm9yRWFjaCgoZWwsIGlkeCkgPT4ge1xuICAgICAgY29uc3QgaW5kZXggPSB0aGlzLnZpc2libGVTdGFydCArIGlkeDtcbiAgICAgIGNvbnN0IGhlaWdodCA9IGVsLm5hdGl2ZUVsZW1lbnQub2Zmc2V0SGVpZ2h0O1xuICAgICAgaWYgKHRoaXMuaXRlbUhlaWdodHNbaW5kZXhdICE9PSBoZWlnaHQpIHtcbiAgICAgICAgdGhpcy5pdGVtSGVpZ2h0c1tpbmRleF0gPSBoZWlnaHQ7XG4gICAgICAgIHRoaXMucmVzaXplT2JzZXJ2ZXIub2JzZXJ2ZShlbC5uYXRpdmVFbGVtZW50KTtcbiAgICAgIH1cbiAgICB9KTtcbiAgICB0aGlzLnVwZGF0ZUl0ZW1Ub3BzKCk7XG4gICAgdGhpcy5jZHIubWFya0ZvckNoZWNrKCk7XG4gIH1cbn1cbiIsIjxkaXZcbiAgI3Njcm9sbGVyXG4gIGNsYXNzPVwic2Nyb2xsLWNvbnRhaW5lclwiXG4gIChzY3JvbGwpPVwib25TY3JvbGwoKVwiXG4gIFtzdHlsZS5oZWlnaHQucHhdPVwidmlld3BvcnRIZWlnaHRcIlxuPlxuICA8ZGl2XG4gICAgY2xhc3M9XCJ0b3RhbC1oZWlnaHRcIlxuICAgIFtzdHlsZS5oZWlnaHQucHhdPVwidG90YWxDb250ZW50SGVpZ2h0IC0gdmlld3BvcnRIZWlnaHRcIlxuICA+XG4gICAgPG5nLWNvbnRhaW5lciAqbmdGb3I9XCJsZXQgaXRlbSBvZiB2aXNpYmxlSXRlbXM7IGxldCBpID0gaW5kZXhcIj5cbiAgICAgIDxkaXZcbiAgICAgICAgI2l0ZW1FbGVtZW50XG4gICAgICAgIGNsYXNzPVwiaXRlbVwiXG4gICAgICAgIFtzdHlsZS50cmFuc2Zvcm1dPVwiJ3RyYW5zbGF0ZVkoJyArIGdldEl0ZW1Ub3AodmlzaWJsZVN0YXJ0ICsgaSkgKyAncHgpJ1wiXG4gICAgICA+XG4gICAgICAgIDxuZy1jb250YWluZXJcbiAgICAgICAgICAqbmdUZW1wbGF0ZU91dGxldD1cIml0ZW1UZW1wbGF0ZTsgY29udGV4dDogeyAkaW1wbGljaXQ6IGl0ZW0gfVwiXG4gICAgICAgID48L25nLWNvbnRhaW5lcj5cbiAgICAgIDwvZGl2PlxuICAgIDwvbmctY29udGFpbmVyPlxuICA8L2Rpdj5cbjwvZGl2PlxuIl19