feat: preserve response viewer scroll position per tab (#5193)
Co-authored-by: jamesgeorge007 <25279263+jamesgeorge007@users.noreply.github.com> Co-authored-by: nivedin <nivedinp@gmail.com>
This commit is contained in:
parent
deb2c41016
commit
98fa140b55
11 changed files with 381 additions and 11 deletions
|
|
@ -9,7 +9,11 @@
|
|||
/>
|
||||
</template>
|
||||
<template #secondary>
|
||||
<HttpResponse v-model:document="tab.document" :is-embed="false" />
|
||||
<HttpResponse
|
||||
v-model:document="tab.document"
|
||||
:tab-id="tab.id"
|
||||
:is-embed="false"
|
||||
/>
|
||||
</template>
|
||||
</AppPaneLayout>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
v-if="!loading && hasResponse"
|
||||
v-model:document="doc"
|
||||
:is-editable="false"
|
||||
:tab-id="tabId"
|
||||
@save-as-example="saveAsExample"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -40,6 +41,7 @@ const toast = useToast()
|
|||
|
||||
const props = defineProps<{
|
||||
document: HoppRequestDocument
|
||||
tabId: string
|
||||
isEmbed: boolean
|
||||
}>()
|
||||
|
||||
|
|
@ -68,7 +70,6 @@ const loading = computed(() => doc.value.response?.type === "loading")
|
|||
|
||||
const saveAsExample = () => {
|
||||
showSaveResponseName.value = true
|
||||
|
||||
responseName.value = doc.value.request.name
|
||||
}
|
||||
|
||||
|
|
@ -125,14 +126,13 @@ const onSaveAsExample = () => {
|
|||
showSaveResponseName.value = false
|
||||
|
||||
const saveCtx = doc.value.saveContext
|
||||
|
||||
if (!saveCtx) return
|
||||
|
||||
const req = doc.value.request
|
||||
|
||||
if (saveCtx.originLocation === "user-collection") {
|
||||
try {
|
||||
editRESTRequest(saveCtx.folderPath, saveCtx.requestIndex, req)
|
||||
|
||||
toast.success(`${t("response.saved")}`)
|
||||
responseName.value = ""
|
||||
} catch (e) {
|
||||
|
|
@ -152,7 +152,6 @@ const onSaveAsExample = () => {
|
|||
responseName.value = ""
|
||||
} else {
|
||||
doc.value.isDirty = false
|
||||
|
||||
toast.success(`${t("request.saved")}`)
|
||||
responseName.value = ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
v-model:response="doc.response"
|
||||
:is-savable="isSavable"
|
||||
:is-editable="isEditable"
|
||||
:tab-id="props.tabId"
|
||||
@save-as-example="$emit('save-as-example')"
|
||||
/>
|
||||
</HoppSmartTab>
|
||||
|
|
@ -76,6 +77,7 @@ import { ConsoleEntry } from "../console/Panel.vue"
|
|||
const props = defineProps<{
|
||||
document: HoppRequestDocument
|
||||
isEditable: boolean
|
||||
tabId: string
|
||||
isTestRunner?: boolean
|
||||
}>()
|
||||
|
||||
|
|
|
|||
|
|
@ -61,7 +61,11 @@
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-show="!previewEnabled" class="h-full relative flex flex-col flex-1">
|
||||
<div
|
||||
v-show="!previewEnabled"
|
||||
ref="containerRef"
|
||||
class="h-full relative flex flex-col flex-1"
|
||||
>
|
||||
<div ref="htmlResponse" class="absolute inset-0"></div>
|
||||
</div>
|
||||
<iframe
|
||||
|
|
@ -99,6 +103,7 @@ import IconWrapText from "~icons/lucide/wrap-text"
|
|||
import IconSave from "~icons/lucide/save"
|
||||
import { HoppRESTRequestResponse } from "@hoppscotch/data"
|
||||
import { computedAsync } from "@vueuse/core"
|
||||
import { useScrollerRef } from "~/composables/useScrollerRef"
|
||||
|
||||
const t = useI18n()
|
||||
const persistenceService = useService(PersistenceService)
|
||||
|
|
@ -109,8 +114,16 @@ const props = defineProps<{
|
|||
| HoppRESTRequestResponse
|
||||
isSavable: boolean
|
||||
isEditable: boolean
|
||||
tabId: string
|
||||
}>()
|
||||
|
||||
const { containerRef } = useScrollerRef(
|
||||
"HTMLLens",
|
||||
".cm-scroller",
|
||||
undefined, // skip initial
|
||||
`${props.tabId}::html`
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "save-as-example"): void
|
||||
}>()
|
||||
|
|
|
|||
|
|
@ -132,7 +132,10 @@
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="h-full relative overflow-auto flex flex-col flex-1">
|
||||
<div
|
||||
ref="containerRef"
|
||||
class="h-full relative overflow-auto flex flex-col flex-1"
|
||||
>
|
||||
<div ref="jsonResponse" class="absolute inset-0 h-full"></div>
|
||||
</div>
|
||||
<div
|
||||
|
|
@ -274,6 +277,7 @@ import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
|
|||
import { useNestedSetting } from "~/composables/settings"
|
||||
import { toggleNestedSetting } from "~/newstore/settings"
|
||||
import { HoppRESTRequestResponse } from "@hoppscotch/data"
|
||||
import { useScrollerRef } from "~/composables/useScrollerRef"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
|
|
@ -281,8 +285,16 @@ const props = defineProps<{
|
|||
response: HoppRESTResponse | HoppRESTRequestResponse
|
||||
isSavable: boolean
|
||||
isEditable: boolean
|
||||
tabId: string
|
||||
}>()
|
||||
|
||||
const { containerRef } = useScrollerRef(
|
||||
"JSONLens",
|
||||
".cm-scroller",
|
||||
undefined, // skip initial
|
||||
`${props.tabId}::json`
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "save-as-example"): void
|
||||
(e: "update:response", val: HoppRESTRequestResponse | HoppRESTResponse): void
|
||||
|
|
|
|||
|
|
@ -52,7 +52,10 @@
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="h-full relative overflow-auto flex flex-col flex-1">
|
||||
<div
|
||||
ref="containerRef"
|
||||
class="h-full relative overflow-auto flex flex-col flex-1"
|
||||
>
|
||||
<div ref="rawResponse" class="absolute inset-0"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -81,6 +84,7 @@ import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
|
|||
import { useNestedSetting } from "~/composables/settings"
|
||||
import { toggleNestedSetting } from "~/newstore/settings"
|
||||
import { HoppRESTRequestResponse } from "@hoppscotch/data"
|
||||
import { useScrollerRef } from "~/composables/useScrollerRef"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
|
|
@ -90,6 +94,7 @@ const props = defineProps<{
|
|||
| HoppRESTRequestResponse
|
||||
isEditable: boolean
|
||||
isSavable: boolean
|
||||
tabId: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
|
@ -102,6 +107,13 @@ const emit = defineEmits<{
|
|||
(e: "save-as-example"): void
|
||||
}>()
|
||||
|
||||
const { containerRef } = useScrollerRef(
|
||||
"RawLens",
|
||||
".cm-scroller",
|
||||
undefined, // skip initial scrollTop
|
||||
`${props.tabId}::raw` // unique scroll key for RawLens
|
||||
)
|
||||
|
||||
const { responseBodyText } = useResponseBody(props.response)
|
||||
|
||||
const isHttpResponse = computed(() => {
|
||||
|
|
|
|||
|
|
@ -51,8 +51,9 @@
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="h-full">
|
||||
<div ref="xmlResponse" class="flex flex-1 flex-col"></div>
|
||||
|
||||
<div ref="containerRef" class="h-full relative flex flex-col flex-1">
|
||||
<div ref="xmlResponse" class="absolute inset-0"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -80,6 +81,7 @@ import { objFieldMatches } from "~/helpers/functional/object"
|
|||
import { useNestedSetting } from "~/composables/settings"
|
||||
import { toggleNestedSetting } from "~/newstore/settings"
|
||||
import { HoppRESTRequestResponse } from "@hoppscotch/data"
|
||||
import { useScrollerRef } from "~/composables/useScrollerRef"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
|
|
@ -89,8 +91,16 @@ const props = defineProps<{
|
|||
| HoppRESTRequestResponse
|
||||
isEditable: boolean
|
||||
isSavable: boolean
|
||||
tabId: string
|
||||
}>()
|
||||
|
||||
const { containerRef } = useScrollerRef(
|
||||
"XMLLens",
|
||||
".cm-scroller",
|
||||
undefined, // skip initial
|
||||
`${props.tabId}::xml`
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "save-as-example"): void
|
||||
}>()
|
||||
|
|
|
|||
113
packages/hoppscotch-common/src/composables/useScrollerRef.ts
Normal file
113
packages/hoppscotch-common/src/composables/useScrollerRef.ts
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
import { ref, onMounted, onBeforeUnmount } from "vue"
|
||||
import { useService } from "dioc/vue"
|
||||
import { ScrollService } from "~/services/scroll.service"
|
||||
|
||||
/**
|
||||
* A composable used to automatically restore and save scroll position
|
||||
* inside a scrollable element (e.g., .cm-scroller) within a container.
|
||||
*
|
||||
* @param label - Label used in error logging
|
||||
* @param classSelector - CSS selector for the scrollable element
|
||||
* @param initialScrollTop - Optional fallback scroll position
|
||||
* @param scrollKey - Unique key used for saving/restoring scroll state via ScrollService
|
||||
*/
|
||||
export function useScrollerRef(
|
||||
label: string = "Lens",
|
||||
classSelector: string = ".cm-scroller",
|
||||
initialScrollTop?: number,
|
||||
scrollKey?: string
|
||||
) {
|
||||
// Container element ref (typically the root of the scrollable section)
|
||||
const containerRef = ref<HTMLElement | null>(null)
|
||||
|
||||
// Ref for the actual scrollable element inside the container
|
||||
const scrollerRef = ref<HTMLElement | null>(null)
|
||||
|
||||
// Inject the ScrollService to access stored scroll positions
|
||||
const scrollService = useService(ScrollService)
|
||||
|
||||
/**
|
||||
* Utility to wait until the scrollable element is actually scrollable
|
||||
* (i.e., content overflows and scrolling is possible).
|
||||
* Retries for a limited number of times before failing.
|
||||
*/
|
||||
let isUnmounted = false
|
||||
function waitUntilScrollable(
|
||||
maxTries = 60,
|
||||
delay = 16
|
||||
): Promise<HTMLElement> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let tries = 0
|
||||
|
||||
const tryFind = () => {
|
||||
if (isUnmounted) {
|
||||
reject(new Error(`[${label}] Aborted: component unmounted`))
|
||||
return
|
||||
}
|
||||
|
||||
const scroller = containerRef.value?.querySelector(
|
||||
classSelector
|
||||
) as HTMLElement | null
|
||||
|
||||
if (scroller && scroller.scrollHeight > scroller.clientHeight) {
|
||||
resolve(scroller) // Found a scrollable element
|
||||
return
|
||||
}
|
||||
|
||||
tries++
|
||||
if (tries >= maxTries) {
|
||||
reject(
|
||||
new Error(`[${label}] Timeout: ${classSelector} never scrollable`)
|
||||
)
|
||||
} else {
|
||||
setTimeout(tryFind, delay) // Retry after delay
|
||||
}
|
||||
}
|
||||
|
||||
tryFind()
|
||||
})
|
||||
}
|
||||
|
||||
// Scroll event handler to save scroll position
|
||||
let onScroll: (() => void) | null = null
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const scroller = await waitUntilScrollable()
|
||||
scrollerRef.value = scroller
|
||||
|
||||
// Restore scroll position from service (if available)
|
||||
requestAnimationFrame(() => {
|
||||
if (
|
||||
scrollKey &&
|
||||
scrollService.getScrollForKey(scrollKey) !== undefined
|
||||
) {
|
||||
scroller.scrollTop = scrollService.getScrollForKey(scrollKey)!
|
||||
} else if (initialScrollTop !== undefined) {
|
||||
scroller.scrollTop = initialScrollTop
|
||||
}
|
||||
})
|
||||
|
||||
// Register scroll event to update position in ScrollService
|
||||
onScroll = () => {
|
||||
if (scrollKey) {
|
||||
scrollService.setScrollForKey(scrollKey, scroller.scrollTop)
|
||||
}
|
||||
}
|
||||
|
||||
scroller.addEventListener("scroll", onScroll)
|
||||
} catch (error: any) {
|
||||
console.error(`[${label}] Failed to initialize scroller:`, error.message)
|
||||
}
|
||||
})
|
||||
|
||||
// Clean up scroll listener on unmount
|
||||
onBeforeUnmount(() => {
|
||||
isUnmounted = true
|
||||
if (scrollerRef.value && onScroll) {
|
||||
scrollerRef.value.removeEventListener("scroll", onScroll)
|
||||
}
|
||||
})
|
||||
|
||||
return { containerRef, scrollerRef }
|
||||
}
|
||||
|
|
@ -151,6 +151,9 @@ import { cloneDeep } from "lodash-es"
|
|||
import { RESTTabService } from "~/services/tab/rest"
|
||||
import { HoppTab } from "~/services/tab"
|
||||
import { HoppRequestDocument, HoppTabDocument } from "~/helpers/rest/document"
|
||||
import { ScrollService } from "~/services/scroll.service"
|
||||
|
||||
const scrollService = useService(ScrollService)
|
||||
|
||||
const savingRequest = ref(false)
|
||||
const confirmingCloseForTabID = ref<string | null>(null)
|
||||
|
|
@ -249,6 +252,7 @@ const removeTab = (tabID: string) => {
|
|||
if (tabState.document.isDirty) {
|
||||
confirmingCloseForTabID.value = tabID
|
||||
} else {
|
||||
scrollService.cleanupScrollForTab(tabState.id)
|
||||
tabs.closeTab(tabState.id)
|
||||
inspectionService.deleteTabInspectorResult(tabState.id)
|
||||
}
|
||||
|
|
@ -266,6 +270,7 @@ const closeOtherTabsAction = (tabID: string) => {
|
|||
unsavedTabsCount.value = balanceDirtyTabCount
|
||||
exceptedTabID.value = tabID
|
||||
} else {
|
||||
scrollService.cleanupAllScroll(tabID)
|
||||
tabs.closeOtherTabs(tabID)
|
||||
}
|
||||
}
|
||||
|
|
@ -283,7 +288,10 @@ const duplicateTab = (tabID: string) => {
|
|||
}
|
||||
|
||||
const onResolveConfirmCloseAllTabs = () => {
|
||||
if (exceptedTabID.value) tabs.closeOtherTabs(exceptedTabID.value)
|
||||
if (exceptedTabID.value) {
|
||||
scrollService.cleanupAllScroll(exceptedTabID.value)
|
||||
tabs.closeOtherTabs(exceptedTabID.value)
|
||||
}
|
||||
confirmingCloseAllTabs.value = false
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,99 @@
|
|||
import { describe, it, expect, beforeEach } from "vitest"
|
||||
import { TestContainer } from "dioc/testing"
|
||||
import { ScrollService } from "../scroll.service"
|
||||
|
||||
describe("ScrollService", () => {
|
||||
let service: ScrollService
|
||||
|
||||
beforeEach(() => {
|
||||
const container = new TestContainer()
|
||||
service = container.bind(ScrollService)
|
||||
})
|
||||
|
||||
it("should store and retrieve scroll position for tab and suffix", () => {
|
||||
service.setScroll("tab1", "json", 100)
|
||||
|
||||
expect(service.getScroll("tab1", "json")).toBe(100)
|
||||
})
|
||||
|
||||
it("should return undefined for unset scroll position", () => {
|
||||
expect(service.getScroll("tab1", "html")).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should clean up scroll positions for a specific tab", () => {
|
||||
service.setScroll("tab1", "json", 100)
|
||||
service.setScroll("tab1", "html", 200)
|
||||
|
||||
service.cleanupScrollForTab("tab1")
|
||||
|
||||
expect(service.getScroll("tab1", "json")).toBeUndefined()
|
||||
expect(service.getScroll("tab1", "html")).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should clean up all scroll positions", () => {
|
||||
service.setScroll("tab1", "json", 100)
|
||||
service.setScroll("tab2", "html", 200)
|
||||
service.cleanupAllScroll()
|
||||
|
||||
expect(service.getScroll("tab1", "json")).toBeUndefined()
|
||||
expect(service.getScroll("tab2", "html")).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should clean up scroll positions for all tabs except a specific one", () => {
|
||||
service.setScroll("tab1", "json", 100)
|
||||
service.setScroll("tab2", "html", 200)
|
||||
service.cleanupAllScroll("tab1")
|
||||
|
||||
expect(service.getScroll("tab1", "json")).toBe(100)
|
||||
expect(service.getScroll("tab2", "html")).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should store and retrieve scroll position using a custom key", () => {
|
||||
service.setScrollForKey("customKey", 999)
|
||||
|
||||
expect(service.getScrollForKey("customKey")).toBe(999)
|
||||
})
|
||||
|
||||
it("should return undefined for unset custom key", () => {
|
||||
expect(service.getScrollForKey("nonexistentKey")).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should overwrite an existing scroll value", () => {
|
||||
service.setScroll("tab1", "json", 100)
|
||||
service.setScroll("tab1", "json", 300)
|
||||
|
||||
expect(service.getScroll("tab1", "json")).toBe(300)
|
||||
})
|
||||
|
||||
it("custom key and tab+suffix keys do not interfere when keys are different", () => {
|
||||
service.setScroll("tab1", "json", 111)
|
||||
service.setScrollForKey("custom-tab1-json", 999)
|
||||
expect(service.getScroll("tab1", "json")).toBe(111)
|
||||
expect(service.getScrollForKey("custom-tab1-json")).toBe(999)
|
||||
})
|
||||
|
||||
it("cleanupScrollForTab should not remove other tab data", () => {
|
||||
service.setScroll("tab1", "json", 100)
|
||||
service.setScroll("tab2", "json", 200)
|
||||
|
||||
service.cleanupScrollForTab("tab1")
|
||||
|
||||
expect(service.getScroll("tab1", "json")).toBeUndefined()
|
||||
expect(service.getScroll("tab2", "json")).toBe(200)
|
||||
})
|
||||
|
||||
it("should handle empty tabId and suffix gracefully", () => {
|
||||
service.setScroll("", "json", 123)
|
||||
service.setScroll("tab1", "" as any, 456)
|
||||
|
||||
expect(service.getScroll("", "json")).toBe(123)
|
||||
expect(service.getScroll("tab1", "" as any)).toBe(456)
|
||||
})
|
||||
|
||||
it("should overwrite scroll position for custom keys", () => {
|
||||
service.setScrollForKey("key1", 100)
|
||||
service.setScrollForKey("key1", 300)
|
||||
|
||||
expect(service.getScrollForKey("key1")).toBe(300)
|
||||
})
|
||||
})
|
||||
98
packages/hoppscotch-common/src/services/scroll.service.ts
Normal file
98
packages/hoppscotch-common/src/services/scroll.service.ts
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
import { Service } from "dioc"
|
||||
|
||||
/**
|
||||
* Suffix type for different views in the application.
|
||||
* This is used to identify the type of view for which the scroll position is being stored.
|
||||
*/
|
||||
export type Suffix = "json" | "raw" | "html" | "xml" | "preview"
|
||||
|
||||
/**
|
||||
* This service is used to store and manage scroll positions for different tabs and views.
|
||||
* It keeps track of scroll positions using a key-value mapping where each key
|
||||
* is a combination of tab ID and view suffix (like json, raw, html, etc.).
|
||||
*
|
||||
* The scroll data is maintained in-memory and not persisted anywhere.
|
||||
*/
|
||||
export class ScrollService extends Service {
|
||||
public static readonly ID = "SCROLL_SERVICE"
|
||||
|
||||
/**
|
||||
* Internal map to store scroll positions.
|
||||
* The key format is: `${tabId}::${suffix}` or any custom key.
|
||||
*/
|
||||
private scrollMap = new Map<string, number>()
|
||||
|
||||
/**
|
||||
* Set the scroll position for a specific tab and view.
|
||||
* @param tabId ID of the tab (e.g., request ID or panel ID)
|
||||
* @param suffix View identifier (e.g., 'json', 'html', etc.)
|
||||
* @param position Scroll position to store
|
||||
*/
|
||||
public setScroll(tabId: string, suffix: Suffix, position: number) {
|
||||
const key = `${tabId}::${suffix}`
|
||||
this.scrollMap.set(key, position)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the scroll position for a specific tab and view.
|
||||
* @param tabId ID of the tab
|
||||
* @param suffix View identifier
|
||||
* @returns Scroll position if available, otherwise undefined
|
||||
*/
|
||||
public getScroll(tabId: string, suffix: Suffix): number | undefined {
|
||||
const key = `${tabId}::${suffix}`
|
||||
return this.scrollMap.get(key)
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear scroll positions for all suffixes (views) related to a given tab.
|
||||
* @param tabId ID of the tab
|
||||
*/
|
||||
public cleanupScrollForTab(tabId: string) {
|
||||
const keysToDelete = Array.from(this.scrollMap.keys())
|
||||
for (const key of keysToDelete) {
|
||||
const tabKey = key.split("::")[0]
|
||||
if (tabKey === tabId) {
|
||||
this.scrollMap.delete(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all scroll positions from the service.
|
||||
* If no tabId is provided, all scroll positions will be cleared.
|
||||
* @param tabId - ID of the tab not to clear.
|
||||
*/
|
||||
public cleanupAllScroll(tabId?: string) {
|
||||
if (tabId) {
|
||||
const keysToDelete = Array.from(this.scrollMap.keys())
|
||||
for (const key of keysToDelete) {
|
||||
const tabKey = key.split("::")[0]
|
||||
if (tabKey !== tabId) {
|
||||
this.scrollMap.delete(key)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.scrollMap.clear()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set scroll position directly by key (without tabId and suffix).
|
||||
* Useful for custom or global use cases.
|
||||
* @param key Unique identifier for scroll state
|
||||
* @param position Scroll position to store
|
||||
*/
|
||||
public setScrollForKey(key: string, position: number) {
|
||||
this.scrollMap.set(key, position)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get scroll position by key.
|
||||
* @param key Unique identifier for scroll state
|
||||
* @returns Scroll position if available, otherwise undefined
|
||||
*/
|
||||
public getScrollForKey(key: string): number | undefined {
|
||||
return this.scrollMap.get(key)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue