webui: Improve Chat Messages initial scroll + auto-scroll logic + add lazy loading with transitions to content blocks (#20999)
* refactor: Always use agentic content renderer for Assistant Message * feat: Improve initial scroll + auto-scroll logic + implement fade in action for content blocks * chore: update webui build output
This commit is contained in:
Binary file not shown.
@@ -0,0 +1,54 @@
|
|||||||
|
/**
|
||||||
|
* Svelte action that fades in an element when it enters the viewport.
|
||||||
|
* Uses IntersectionObserver for efficient viewport detection.
|
||||||
|
*
|
||||||
|
* If skipIfVisible is set and the element is already visible in the viewport
|
||||||
|
* when the action attaches (e.g. a markdown block promoted from unstable
|
||||||
|
* during streaming), the fade is skipped entirely to avoid a flash.
|
||||||
|
*/
|
||||||
|
export function fadeInView(
|
||||||
|
node: HTMLElement,
|
||||||
|
options: { duration?: number; y?: number; skipIfVisible?: boolean } = {}
|
||||||
|
) {
|
||||||
|
const { duration = 300, y = 0, skipIfVisible = false } = options;
|
||||||
|
|
||||||
|
if (skipIfVisible) {
|
||||||
|
const rect = node.getBoundingClientRect();
|
||||||
|
const isAlreadyVisible =
|
||||||
|
rect.top < window.innerHeight &&
|
||||||
|
rect.bottom > 0 &&
|
||||||
|
rect.left < window.innerWidth &&
|
||||||
|
rect.right > 0;
|
||||||
|
|
||||||
|
if (isAlreadyVisible) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
node.style.opacity = '0';
|
||||||
|
node.style.transform = `translateY(${y}px)`;
|
||||||
|
node.style.transition = `opacity ${duration}ms ease-out, transform ${duration}ms ease-out`;
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
node.style.opacity = '1';
|
||||||
|
node.style.transform = 'translateY(0)';
|
||||||
|
});
|
||||||
|
observer.disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ threshold: 0.05 }
|
||||||
|
);
|
||||||
|
|
||||||
|
observer.observe(node);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
observer.disconnect();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
+1
-11
@@ -3,14 +3,12 @@
|
|||||||
ChatMessageAgenticContent,
|
ChatMessageAgenticContent,
|
||||||
ChatMessageActions,
|
ChatMessageActions,
|
||||||
ChatMessageStatistics,
|
ChatMessageStatistics,
|
||||||
MarkdownContent,
|
|
||||||
ModelBadge,
|
ModelBadge,
|
||||||
ModelsSelector
|
ModelsSelector
|
||||||
} from '$lib/components/app';
|
} from '$lib/components/app';
|
||||||
import { getMessageEditContext } from '$lib/contexts';
|
import { getMessageEditContext } from '$lib/contexts';
|
||||||
import { useProcessingState } from '$lib/hooks/use-processing-state.svelte';
|
import { useProcessingState } from '$lib/hooks/use-processing-state.svelte';
|
||||||
import { isLoading, isChatStreaming } from '$lib/stores/chat.svelte';
|
import { isLoading, isChatStreaming } from '$lib/stores/chat.svelte';
|
||||||
import { agenticStreamingToolCall } from '$lib/stores/agentic.svelte';
|
|
||||||
import { autoResizeTextarea, copyToClipboard, isIMEComposing } from '$lib/utils';
|
import { autoResizeTextarea, copyToClipboard, isIMEComposing } from '$lib/utils';
|
||||||
import { tick } from 'svelte';
|
import { tick } from 'svelte';
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
@@ -87,13 +85,7 @@
|
|||||||
const hasAgenticMarkers = $derived(
|
const hasAgenticMarkers = $derived(
|
||||||
messageContent?.includes(AGENTIC_TAGS.TOOL_CALL_START) ?? false
|
messageContent?.includes(AGENTIC_TAGS.TOOL_CALL_START) ?? false
|
||||||
);
|
);
|
||||||
const hasStreamingToolCall = $derived(
|
|
||||||
isChatStreaming() && agenticStreamingToolCall(message.convId) !== null
|
|
||||||
);
|
|
||||||
const hasReasoningMarkers = $derived(messageContent?.includes(REASONING_TAGS.START) ?? false);
|
const hasReasoningMarkers = $derived(messageContent?.includes(REASONING_TAGS.START) ?? false);
|
||||||
const isStructuredContent = $derived(
|
|
||||||
hasAgenticMarkers || hasReasoningMarkers || hasStreamingToolCall
|
|
||||||
);
|
|
||||||
const processingState = useProcessingState();
|
const processingState = useProcessingState();
|
||||||
|
|
||||||
let currentConfig = $derived(config());
|
let currentConfig = $derived(config());
|
||||||
@@ -256,15 +248,13 @@
|
|||||||
{:else if message.role === MessageRole.ASSISTANT}
|
{:else if message.role === MessageRole.ASSISTANT}
|
||||||
{#if showRawOutput}
|
{#if showRawOutput}
|
||||||
<pre class="raw-output">{messageContent || ''}</pre>
|
<pre class="raw-output">{messageContent || ''}</pre>
|
||||||
{:else if isStructuredContent}
|
{:else}
|
||||||
<ChatMessageAgenticContent
|
<ChatMessageAgenticContent
|
||||||
content={messageContent || ''}
|
content={messageContent || ''}
|
||||||
isStreaming={isChatStreaming()}
|
isStreaming={isChatStreaming()}
|
||||||
highlightTurns={highlightAgenticTurns}
|
highlightTurns={highlightAgenticTurns}
|
||||||
{message}
|
{message}
|
||||||
/>
|
/>
|
||||||
{:else}
|
|
||||||
<MarkdownContent content={messageContent || ''} attachments={message.extra} />
|
|
||||||
{/if}
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
<div class="text-sm whitespace-pre-wrap">
|
<div class="text-sm whitespace-pre-wrap">
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { fadeInView } from '$lib/actions/fade-in-view.svelte';
|
||||||
import { ChatMessage } from '$lib/components/app';
|
import { ChatMessage } from '$lib/components/app';
|
||||||
import { setChatActionsContext } from '$lib/contexts';
|
import { setChatActionsContext } from '$lib/contexts';
|
||||||
import { MessageRole } from '$lib/enums';
|
import { MessageRole } from '$lib/enums';
|
||||||
@@ -140,13 +141,18 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex h-full flex-col space-y-10 pt-24 {className}" style="height: auto; ">
|
<div
|
||||||
|
class="flex h-full flex-col space-y-10 pt-24 {className}"
|
||||||
|
style="height: auto; min-height: calc(100dvh - 14rem);"
|
||||||
|
>
|
||||||
{#each displayMessages as { message, isLastAssistantMessage, siblingInfo } (message.id)}
|
{#each displayMessages as { message, isLastAssistantMessage, siblingInfo } (message.id)}
|
||||||
|
<div use:fadeInView>
|
||||||
<ChatMessage
|
<ChatMessage
|
||||||
class="mx-auto w-full max-w-[48rem]"
|
class="mx-auto w-full max-w-[48rem]"
|
||||||
{message}
|
{message}
|
||||||
{isLastAssistantMessage}
|
{isLastAssistantMessage}
|
||||||
{siblingInfo}
|
{siblingInfo}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,7 +12,6 @@
|
|||||||
} from '$lib/components/app';
|
} from '$lib/components/app';
|
||||||
import * as Alert from '$lib/components/ui/alert';
|
import * as Alert from '$lib/components/ui/alert';
|
||||||
import * as AlertDialog from '$lib/components/ui/alert-dialog';
|
import * as AlertDialog from '$lib/components/ui/alert-dialog';
|
||||||
import { INITIAL_SCROLL_DELAY } from '$lib/constants';
|
|
||||||
import { KeyboardKey } from '$lib/enums';
|
import { KeyboardKey } from '$lib/enums';
|
||||||
import { createAutoScrollController } from '$lib/hooks/use-auto-scroll.svelte';
|
import { createAutoScrollController } from '$lib/hooks/use-auto-scroll.svelte';
|
||||||
import {
|
import {
|
||||||
@@ -48,7 +47,7 @@
|
|||||||
let showFileErrorDialog = $state(false);
|
let showFileErrorDialog = $state(false);
|
||||||
let uploadedFiles = $state<ChatUploadedFile[]>([]);
|
let uploadedFiles = $state<ChatUploadedFile[]>([]);
|
||||||
|
|
||||||
const autoScroll = createAutoScrollController();
|
const autoScroll = createAutoScrollController({ isColumnReverse: true });
|
||||||
|
|
||||||
let fileErrorData = $state<{
|
let fileErrorData = $state<{
|
||||||
generallyUnsupported: File[];
|
generallyUnsupported: File[];
|
||||||
@@ -310,13 +309,15 @@
|
|||||||
|
|
||||||
afterNavigate(() => {
|
afterNavigate(() => {
|
||||||
if (!disableAutoScroll) {
|
if (!disableAutoScroll) {
|
||||||
setTimeout(() => autoScroll.scrollToBottom('instant'), INITIAL_SCROLL_DELAY);
|
autoScroll.enable();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
|
autoScroll.startObserving();
|
||||||
|
|
||||||
if (!disableAutoScroll) {
|
if (!disableAutoScroll) {
|
||||||
setTimeout(() => autoScroll.scrollToBottom('instant'), INITIAL_SCROLL_DELAY);
|
autoScroll.enable();
|
||||||
}
|
}
|
||||||
|
|
||||||
const pendingDraft = chatStore.consumePendingDraft();
|
const pendingDraft = chatStore.consumePendingDraft();
|
||||||
@@ -333,10 +334,6 @@
|
|||||||
$effect(() => {
|
$effect(() => {
|
||||||
autoScroll.setDisabled(disableAutoScroll);
|
autoScroll.setDisabled(disableAutoScroll);
|
||||||
});
|
});
|
||||||
|
|
||||||
$effect(() => {
|
|
||||||
autoScroll.updateInterval(isCurrentConversationLoading);
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if isDragOver}
|
{#if isDragOver}
|
||||||
@@ -351,7 +348,7 @@
|
|||||||
<div
|
<div
|
||||||
bind:this={chatScrollContainer}
|
bind:this={chatScrollContainer}
|
||||||
aria-label="Chat interface with file drop zone"
|
aria-label="Chat interface with file drop zone"
|
||||||
class="flex h-full flex-col overflow-y-auto px-4 md:px-6"
|
class="flex h-full flex-col-reverse overflow-y-auto px-4 md:px-6"
|
||||||
ondragenter={handleDragEnter}
|
ondragenter={handleDragEnter}
|
||||||
ondragleave={handleDragLeave}
|
ondragleave={handleDragLeave}
|
||||||
ondragover={handleDragOver}
|
ondragover={handleDragOver}
|
||||||
@@ -359,6 +356,7 @@
|
|||||||
onscroll={handleScroll}
|
onscroll={handleScroll}
|
||||||
role="main"
|
role="main"
|
||||||
>
|
>
|
||||||
|
<div class="flex flex-col">
|
||||||
<ChatMessages
|
<ChatMessages
|
||||||
class="mb-16 md:mb-24"
|
class="mb-16 md:mb-24"
|
||||||
messages={activeMessages()}
|
messages={activeMessages()}
|
||||||
@@ -413,6 +411,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{:else if isServerLoading}
|
{:else if isServerLoading}
|
||||||
<!-- Server Loading State -->
|
<!-- Server Loading State -->
|
||||||
<ServerLoadingSplash />
|
<ServerLoadingSplash />
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
import { createAutoScrollController } from '$lib/hooks/use-auto-scroll.svelte';
|
import { createAutoScrollController } from '$lib/hooks/use-auto-scroll.svelte';
|
||||||
import type { DatabaseMessageExtra } from '$lib/types/database';
|
import type { DatabaseMessageExtra } from '$lib/types/database';
|
||||||
import { config } from '$lib/stores/settings.svelte';
|
import { config } from '$lib/stores/settings.svelte';
|
||||||
|
import { fadeInView } from '$lib/actions/fade-in-view.svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
attachments?: DatabaseMessageExtra[];
|
attachments?: DatabaseMessageExtra[];
|
||||||
@@ -598,7 +599,7 @@
|
|||||||
: ''}"
|
: ''}"
|
||||||
>
|
>
|
||||||
{#each renderedBlocks as block (block.id)}
|
{#each renderedBlocks as block (block.id)}
|
||||||
<div class="markdown-block" data-block-id={block.id}>
|
<div class="markdown-block" data-block-id={block.id} use:fadeInView={{ skipIfVisible: true }}>
|
||||||
<!-- eslint-disable-next-line no-at-html-tags -->
|
<!-- eslint-disable-next-line no-at-html-tags -->
|
||||||
{@html block.html}
|
{@html block.html}
|
||||||
</div>
|
</div>
|
||||||
@@ -651,7 +652,6 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.markdown-block,
|
|
||||||
.markdown-block--unstable {
|
.markdown-block--unstable {
|
||||||
display: contents;
|
display: contents;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,2 @@
|
|||||||
export const AUTO_SCROLL_INTERVAL = 100;
|
export const AUTO_SCROLL_INTERVAL = 100;
|
||||||
export const INITIAL_SCROLL_DELAY = 50;
|
|
||||||
export const AUTO_SCROLL_AT_BOTTOM_THRESHOLD = 10;
|
export const AUTO_SCROLL_AT_BOTTOM_THRESHOLD = 10;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { AUTO_SCROLL_AT_BOTTOM_THRESHOLD, AUTO_SCROLL_INTERVAL } from '$lib/constants';
|
import { AUTO_SCROLL_AT_BOTTOM_THRESHOLD, AUTO_SCROLL_INTERVAL } from '$lib/constants';
|
||||||
|
|
||||||
export interface AutoScrollOptions {
|
export interface AutoScrollOptions {
|
||||||
/** Whether auto-scroll is disabled globally (e.g., from settings) */
|
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
isColumnReverse?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -12,6 +12,7 @@ export interface AutoScrollOptions {
|
|||||||
* - Auto-scrolls to bottom during streaming/loading
|
* - Auto-scrolls to bottom during streaming/loading
|
||||||
* - Stops auto-scroll when user manually scrolls up
|
* - Stops auto-scroll when user manually scrolls up
|
||||||
* - Resumes auto-scroll when user scrolls back to bottom
|
* - Resumes auto-scroll when user scrolls back to bottom
|
||||||
|
* - Supports both normal and column-reverse scroll containers
|
||||||
*/
|
*/
|
||||||
export class AutoScrollController {
|
export class AutoScrollController {
|
||||||
private _autoScrollEnabled = $state(true);
|
private _autoScrollEnabled = $state(true);
|
||||||
@@ -21,9 +22,14 @@ export class AutoScrollController {
|
|||||||
private _scrollTimeout: ReturnType<typeof setTimeout> | undefined;
|
private _scrollTimeout: ReturnType<typeof setTimeout> | undefined;
|
||||||
private _container: HTMLElement | undefined;
|
private _container: HTMLElement | undefined;
|
||||||
private _disabled: boolean;
|
private _disabled: boolean;
|
||||||
|
private _isColumnReverse: boolean;
|
||||||
|
private _mutationObserver: MutationObserver | null = null;
|
||||||
|
private _rafPending = false;
|
||||||
|
private _observerEnabled = false;
|
||||||
|
|
||||||
constructor(options: AutoScrollOptions = {}) {
|
constructor(options: AutoScrollOptions = {}) {
|
||||||
this._disabled = options.disabled ?? false;
|
this._disabled = options.disabled ?? false;
|
||||||
|
this._isColumnReverse = options.isColumnReverse ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
get autoScrollEnabled(): boolean {
|
get autoScrollEnabled(): boolean {
|
||||||
@@ -38,7 +44,12 @@ export class AutoScrollController {
|
|||||||
* Binds the controller to a scrollable container element.
|
* Binds the controller to a scrollable container element.
|
||||||
*/
|
*/
|
||||||
setContainer(container: HTMLElement | undefined): void {
|
setContainer(container: HTMLElement | undefined): void {
|
||||||
|
this._doStopObserving();
|
||||||
this._container = container;
|
this._container = container;
|
||||||
|
|
||||||
|
if (this._observerEnabled && container && !this._disabled) {
|
||||||
|
this._doStartObserving();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -49,6 +60,9 @@ export class AutoScrollController {
|
|||||||
if (disabled) {
|
if (disabled) {
|
||||||
this._autoScrollEnabled = false;
|
this._autoScrollEnabled = false;
|
||||||
this.stopInterval();
|
this.stopInterval();
|
||||||
|
this._doStopObserving();
|
||||||
|
} else if (this._observerEnabled && this._container && !this._mutationObserver) {
|
||||||
|
this._doStartObserving();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,10 +73,23 @@ export class AutoScrollController {
|
|||||||
if (this._disabled || !this._container) return;
|
if (this._disabled || !this._container) return;
|
||||||
|
|
||||||
const { scrollTop, scrollHeight, clientHeight } = this._container;
|
const { scrollTop, scrollHeight, clientHeight } = this._container;
|
||||||
const distanceFromBottom = scrollHeight - scrollTop - clientHeight;
|
|
||||||
|
let distanceFromBottom: number;
|
||||||
|
let isScrollingUp: boolean;
|
||||||
|
|
||||||
|
if (this._isColumnReverse) {
|
||||||
|
// column-reverse: scrollTop=0 at bottom, negative when scrolled up
|
||||||
|
distanceFromBottom = Math.abs(scrollTop);
|
||||||
|
isScrollingUp = scrollTop < this._lastScrollTop;
|
||||||
|
} else {
|
||||||
|
// normal: scrollTop=0 at top, increases when scrolled down
|
||||||
|
distanceFromBottom = scrollHeight - clientHeight - scrollTop;
|
||||||
|
isScrollingUp = scrollTop < this._lastScrollTop;
|
||||||
|
}
|
||||||
|
|
||||||
const isAtBottom = distanceFromBottom < AUTO_SCROLL_AT_BOTTOM_THRESHOLD;
|
const isAtBottom = distanceFromBottom < AUTO_SCROLL_AT_BOTTOM_THRESHOLD;
|
||||||
|
|
||||||
if (scrollTop < this._lastScrollTop && !isAtBottom) {
|
if (isScrollingUp && !isAtBottom) {
|
||||||
this._userScrolledUp = true;
|
this._userScrolledUp = true;
|
||||||
this._autoScrollEnabled = false;
|
this._autoScrollEnabled = false;
|
||||||
} else if (isAtBottom && this._userScrolledUp) {
|
} else if (isAtBottom && this._userScrolledUp) {
|
||||||
@@ -90,10 +117,12 @@ export class AutoScrollController {
|
|||||||
scrollToBottom(behavior: ScrollBehavior = 'smooth'): void {
|
scrollToBottom(behavior: ScrollBehavior = 'smooth'): void {
|
||||||
if (this._disabled || !this._container) return;
|
if (this._disabled || !this._container) return;
|
||||||
|
|
||||||
this._container.scrollTo({
|
if (this._isColumnReverse) {
|
||||||
top: this._container.scrollHeight,
|
// column-reverse: scrollTop=0 is the bottom
|
||||||
behavior
|
this._container.scrollTo({ top: 0, behavior });
|
||||||
});
|
} else {
|
||||||
|
this._container.scrollTo({ top: this._container.scrollHeight, behavior });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -150,11 +179,69 @@ export class AutoScrollController {
|
|||||||
*/
|
*/
|
||||||
destroy(): void {
|
destroy(): void {
|
||||||
this.stopInterval();
|
this.stopInterval();
|
||||||
|
this._doStopObserving();
|
||||||
|
|
||||||
if (this._scrollTimeout) {
|
if (this._scrollTimeout) {
|
||||||
clearTimeout(this._scrollTimeout);
|
clearTimeout(this._scrollTimeout);
|
||||||
this._scrollTimeout = undefined;
|
this._scrollTimeout = undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts a MutationObserver on the container that auto-scrolls to bottom
|
||||||
|
* on content changes. More responsive than interval-based polling.
|
||||||
|
*/
|
||||||
|
startObserving(): void {
|
||||||
|
this._observerEnabled = true;
|
||||||
|
|
||||||
|
if (this._container && !this._disabled && !this._mutationObserver) {
|
||||||
|
this._doStartObserving();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops the MutationObserver.
|
||||||
|
*/
|
||||||
|
stopObserving(): void {
|
||||||
|
this._observerEnabled = false;
|
||||||
|
this._doStopObserving();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _doStartObserving(): void {
|
||||||
|
if (!this._container || this._mutationObserver) return;
|
||||||
|
|
||||||
|
const isReverse = this._isColumnReverse;
|
||||||
|
|
||||||
|
this._mutationObserver = new MutationObserver(() => {
|
||||||
|
if (!this._autoScrollEnabled || this._rafPending) return;
|
||||||
|
this._rafPending = true;
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
this._rafPending = false;
|
||||||
|
if (this._autoScrollEnabled && this._container) {
|
||||||
|
if (isReverse) {
|
||||||
|
// column-reverse: scrollTop=0 is the bottom
|
||||||
|
this._container.scrollTop = 0;
|
||||||
|
} else {
|
||||||
|
this._container.scrollTop = this._container.scrollHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this._mutationObserver.observe(this._container, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
characterData: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _doStopObserving(): void {
|
||||||
|
if (this._mutationObserver) {
|
||||||
|
this._mutationObserver.disconnect();
|
||||||
|
this._mutationObserver = null;
|
||||||
|
}
|
||||||
|
this._rafPending = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user