@bbki.ng/bb-msg-history 0.11.1 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/component.d.ts +3 -0
- package/dist/component.js +49 -6
- package/dist/const/styles.js +5 -0
- package/package.json +1 -1
- package/src/component.ts +62 -7
- package/src/const/styles.ts +5 -0
package/dist/component.d.ts
CHANGED
|
@@ -5,6 +5,9 @@ export declare class BBMsgHistory extends HTMLElement {
|
|
|
5
5
|
private _lastAuthor;
|
|
6
6
|
private _lastGroupTimestamp;
|
|
7
7
|
private _scrollButtonVisible;
|
|
8
|
+
private _userHasScrolledManually;
|
|
9
|
+
private _isProgrammaticScroll;
|
|
10
|
+
private _lastScrollTop;
|
|
8
11
|
static get observedAttributes(): string[];
|
|
9
12
|
constructor();
|
|
10
13
|
attributeChangedCallback(name: string): void;
|
package/dist/component.js
CHANGED
|
@@ -6,17 +6,20 @@ import { buildMessageRowHtml, setupTooltipForElement } from './utils/message-bui
|
|
|
6
6
|
import { buildScrollButtonHtml } from './utils/scroll-button.js';
|
|
7
7
|
export class BBMsgHistory extends HTMLElement {
|
|
8
8
|
static get observedAttributes() {
|
|
9
|
-
return ['theme', 'loading', 'hide-scroll-bar', 'infinite'];
|
|
9
|
+
return ['theme', 'loading', 'hide-scroll-bar', 'infinite', 'hide-scroll-button'];
|
|
10
10
|
}
|
|
11
11
|
constructor() {
|
|
12
12
|
super();
|
|
13
13
|
this._userAuthors = new Map();
|
|
14
14
|
this._lastAuthor = '';
|
|
15
15
|
this._scrollButtonVisible = false;
|
|
16
|
+
this._userHasScrolledManually = false;
|
|
17
|
+
this._isProgrammaticScroll = false;
|
|
18
|
+
this._lastScrollTop = 0;
|
|
16
19
|
this.attachShadow({ mode: 'open' });
|
|
17
20
|
}
|
|
18
21
|
attributeChangedCallback(name) {
|
|
19
|
-
if (name === 'theme' || name === 'loading' || name === 'hide-scroll-bar' || name === 'infinite') {
|
|
22
|
+
if (name === 'theme' || name === 'loading' || name === 'hide-scroll-bar' || name === 'infinite' || name === 'hide-scroll-button') {
|
|
20
23
|
this.render();
|
|
21
24
|
}
|
|
22
25
|
}
|
|
@@ -115,15 +118,27 @@ export class BBMsgHistory extends HTMLElement {
|
|
|
115
118
|
}
|
|
116
119
|
// Smooth scroll to bottom (skip in infinite mode)
|
|
117
120
|
if (!this.hasAttribute('infinite')) {
|
|
121
|
+
// Mark this as a programmatic scroll so the scroll handler ignores it
|
|
122
|
+
this._isProgrammaticScroll = true;
|
|
118
123
|
container.scrollTo({
|
|
119
124
|
top: container.scrollHeight,
|
|
120
125
|
behavior: 'smooth',
|
|
121
126
|
});
|
|
127
|
+
// Reset the flag after smooth scroll animation completes (~300ms)
|
|
128
|
+
setTimeout(() => {
|
|
129
|
+
this._isProgrammaticScroll = false;
|
|
130
|
+
}, 300);
|
|
122
131
|
// Hide scroll button since we're scrolling to bottom
|
|
123
132
|
const scrollButton = this.shadowRoot.querySelector('.scroll-to-bottom');
|
|
124
133
|
if (scrollButton && this._scrollButtonVisible) {
|
|
125
134
|
this._scrollButtonVisible = false;
|
|
126
135
|
scrollButton.classList.remove('visible');
|
|
136
|
+
// Dispatch hide event
|
|
137
|
+
this.dispatchEvent(new CustomEvent('bb-scrollbuttonhide', {
|
|
138
|
+
bubbles: true,
|
|
139
|
+
composed: true,
|
|
140
|
+
detail: { visible: false }
|
|
141
|
+
}));
|
|
127
142
|
}
|
|
128
143
|
}
|
|
129
144
|
}
|
|
@@ -224,12 +239,13 @@ export class BBMsgHistory extends HTMLElement {
|
|
|
224
239
|
<div class="loading-spinner"></div>
|
|
225
240
|
</div>`
|
|
226
241
|
: '';
|
|
242
|
+
const hideScrollButton = this.hasAttribute('hide-scroll-button');
|
|
227
243
|
this.shadowRoot.innerHTML = `
|
|
228
244
|
<style>${MAIN_STYLES}${LOADING_STYLES}</style>
|
|
229
245
|
<div class="history" role="log" aria-live="polite" aria-label="Message history">
|
|
230
246
|
${messagesHtml}
|
|
231
247
|
</div>
|
|
232
|
-
${buildScrollButtonHtml()}
|
|
248
|
+
${hideScrollButton ? '' : buildScrollButtonHtml()}
|
|
233
249
|
${loadingOverlay}
|
|
234
250
|
`;
|
|
235
251
|
this._setupAfterRender();
|
|
@@ -267,10 +283,18 @@ export class BBMsgHistory extends HTMLElement {
|
|
|
267
283
|
_setupAfterRender() {
|
|
268
284
|
requestAnimationFrame(() => {
|
|
269
285
|
const container = this.shadowRoot.querySelector('.history');
|
|
270
|
-
const
|
|
286
|
+
const hideScrollButton = this.hasAttribute('hide-scroll-button');
|
|
287
|
+
const scrollButton = hideScrollButton
|
|
288
|
+
? null
|
|
289
|
+
: this.shadowRoot.querySelector('.scroll-to-bottom');
|
|
271
290
|
const isInfinite = this.hasAttribute('infinite');
|
|
272
|
-
if (container && !isInfinite) {
|
|
291
|
+
if (container && !isInfinite && !hideScrollButton) {
|
|
292
|
+
// Mark as programmatic scroll to prevent triggering user scroll detection
|
|
293
|
+
this._isProgrammaticScroll = true;
|
|
273
294
|
container.scrollTop = container.scrollHeight;
|
|
295
|
+
requestAnimationFrame(() => {
|
|
296
|
+
this._isProgrammaticScroll = false;
|
|
297
|
+
});
|
|
274
298
|
this._setupScrollTracking(container, scrollButton, { skipInitialCheck: true });
|
|
275
299
|
}
|
|
276
300
|
if (scrollButton && !isInfinite) {
|
|
@@ -306,15 +330,34 @@ export class BBMsgHistory extends HTMLElement {
|
|
|
306
330
|
}
|
|
307
331
|
_setupScrollTracking(container, button, options) {
|
|
308
332
|
const checkScrollPosition = () => {
|
|
333
|
+
// Ignore programmatic scrolls - they don't indicate user intent
|
|
334
|
+
if (this._isProgrammaticScroll)
|
|
335
|
+
return;
|
|
336
|
+
// Mark that user has manually scrolled
|
|
337
|
+
if (!this._userHasScrolledManually) {
|
|
338
|
+
this._userHasScrolledManually = true;
|
|
339
|
+
}
|
|
340
|
+
const currentScrollTop = container.scrollTop;
|
|
341
|
+
const isScrollingUp = currentScrollTop < this._lastScrollTop;
|
|
342
|
+
this._lastScrollTop = currentScrollTop;
|
|
309
343
|
const threshold = 50; // pixels from bottom
|
|
310
344
|
const isAtBottom = container.scrollHeight - container.scrollTop - container.clientHeight < threshold;
|
|
311
345
|
const hasOverflow = container.scrollHeight > container.clientHeight;
|
|
312
|
-
|
|
346
|
+
// Only show button when: user has scrolled, is not at bottom, is scrolling up
|
|
347
|
+
const shouldShow = !isAtBottom && hasOverflow && this._userHasScrolledManually && isScrollingUp;
|
|
313
348
|
if (shouldShow !== this._scrollButtonVisible) {
|
|
314
349
|
this._scrollButtonVisible = shouldShow;
|
|
315
350
|
button.classList.toggle('visible', shouldShow);
|
|
351
|
+
// Dispatch custom event
|
|
352
|
+
this.dispatchEvent(new CustomEvent(shouldShow ? 'bb-scrollbuttonshow' : 'bb-scrollbuttonhide', {
|
|
353
|
+
bubbles: true,
|
|
354
|
+
composed: true,
|
|
355
|
+
detail: { visible: shouldShow }
|
|
356
|
+
}));
|
|
316
357
|
}
|
|
317
358
|
};
|
|
359
|
+
// Initialize last scroll position
|
|
360
|
+
this._lastScrollTop = container.scrollTop;
|
|
318
361
|
// Check initial state unless skipped
|
|
319
362
|
if (!options?.skipInitialCheck) {
|
|
320
363
|
checkScrollPosition();
|
package/dist/const/styles.js
CHANGED
|
@@ -71,6 +71,11 @@ export const MAIN_STYLES = `
|
|
|
71
71
|
display: none;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
/* Hide scroll button when explicitly disabled */
|
|
75
|
+
:host([hide-scroll-button]) .scroll-to-bottom {
|
|
76
|
+
display: none;
|
|
77
|
+
}
|
|
78
|
+
|
|
74
79
|
/* Scroll to bottom button */
|
|
75
80
|
.scroll-to-bottom {
|
|
76
81
|
position: absolute;
|
package/package.json
CHANGED
package/src/component.ts
CHANGED
|
@@ -12,9 +12,12 @@ export class BBMsgHistory extends HTMLElement {
|
|
|
12
12
|
private _lastAuthor = '';
|
|
13
13
|
private _lastGroupTimestamp: string | undefined;
|
|
14
14
|
private _scrollButtonVisible = false;
|
|
15
|
+
private _userHasScrolledManually = false;
|
|
16
|
+
private _isProgrammaticScroll = false;
|
|
17
|
+
private _lastScrollTop = 0;
|
|
15
18
|
|
|
16
19
|
static get observedAttributes() {
|
|
17
|
-
return ['theme', 'loading', 'hide-scroll-bar', 'infinite'];
|
|
20
|
+
return ['theme', 'loading', 'hide-scroll-bar', 'infinite', 'hide-scroll-button'];
|
|
18
21
|
}
|
|
19
22
|
|
|
20
23
|
constructor() {
|
|
@@ -23,7 +26,7 @@ export class BBMsgHistory extends HTMLElement {
|
|
|
23
26
|
}
|
|
24
27
|
|
|
25
28
|
attributeChangedCallback(name: string) {
|
|
26
|
-
if (name === 'theme' || name === 'loading' || name === 'hide-scroll-bar' || name === 'infinite') {
|
|
29
|
+
if (name === 'theme' || name === 'loading' || name === 'hide-scroll-bar' || name === 'infinite' || name === 'hide-scroll-button') {
|
|
27
30
|
this.render();
|
|
28
31
|
}
|
|
29
32
|
}
|
|
@@ -149,16 +152,33 @@ export class BBMsgHistory extends HTMLElement {
|
|
|
149
152
|
|
|
150
153
|
// Smooth scroll to bottom (skip in infinite mode)
|
|
151
154
|
if (!this.hasAttribute('infinite')) {
|
|
155
|
+
// Mark this as a programmatic scroll so the scroll handler ignores it
|
|
156
|
+
this._isProgrammaticScroll = true;
|
|
157
|
+
|
|
152
158
|
container.scrollTo({
|
|
153
159
|
top: container.scrollHeight,
|
|
154
160
|
behavior: 'smooth',
|
|
155
161
|
});
|
|
156
162
|
|
|
163
|
+
// Reset the flag after smooth scroll animation completes (~300ms)
|
|
164
|
+
setTimeout(() => {
|
|
165
|
+
this._isProgrammaticScroll = false;
|
|
166
|
+
}, 300);
|
|
167
|
+
|
|
157
168
|
// Hide scroll button since we're scrolling to bottom
|
|
158
169
|
const scrollButton = this.shadowRoot!.querySelector('.scroll-to-bottom') as HTMLButtonElement;
|
|
159
170
|
if (scrollButton && this._scrollButtonVisible) {
|
|
160
171
|
this._scrollButtonVisible = false;
|
|
161
172
|
scrollButton.classList.remove('visible');
|
|
173
|
+
|
|
174
|
+
// Dispatch hide event
|
|
175
|
+
this.dispatchEvent(
|
|
176
|
+
new CustomEvent('bb-scrollbuttonhide', {
|
|
177
|
+
bubbles: true,
|
|
178
|
+
composed: true,
|
|
179
|
+
detail: { visible: false }
|
|
180
|
+
})
|
|
181
|
+
);
|
|
162
182
|
}
|
|
163
183
|
}
|
|
164
184
|
}
|
|
@@ -282,12 +302,14 @@ export class BBMsgHistory extends HTMLElement {
|
|
|
282
302
|
</div>`
|
|
283
303
|
: '';
|
|
284
304
|
|
|
305
|
+
const hideScrollButton = this.hasAttribute('hide-scroll-button');
|
|
306
|
+
|
|
285
307
|
this.shadowRoot!.innerHTML = `
|
|
286
308
|
<style>${MAIN_STYLES}${LOADING_STYLES}</style>
|
|
287
309
|
<div class="history" role="log" aria-live="polite" aria-label="Message history">
|
|
288
310
|
${messagesHtml}
|
|
289
311
|
</div>
|
|
290
|
-
${buildScrollButtonHtml()}
|
|
312
|
+
${hideScrollButton ? '' : buildScrollButtonHtml()}
|
|
291
313
|
${loadingOverlay}
|
|
292
314
|
`;
|
|
293
315
|
|
|
@@ -334,12 +356,20 @@ export class BBMsgHistory extends HTMLElement {
|
|
|
334
356
|
private _setupAfterRender(): void {
|
|
335
357
|
requestAnimationFrame(() => {
|
|
336
358
|
const container = this.shadowRoot!.querySelector('.history') as HTMLElement;
|
|
337
|
-
const
|
|
359
|
+
const hideScrollButton = this.hasAttribute('hide-scroll-button');
|
|
360
|
+
const scrollButton = hideScrollButton
|
|
361
|
+
? null
|
|
362
|
+
: (this.shadowRoot!.querySelector('.scroll-to-bottom') as HTMLButtonElement);
|
|
338
363
|
const isInfinite = this.hasAttribute('infinite');
|
|
339
364
|
|
|
340
|
-
if (container && !isInfinite) {
|
|
365
|
+
if (container && !isInfinite && !hideScrollButton) {
|
|
366
|
+
// Mark as programmatic scroll to prevent triggering user scroll detection
|
|
367
|
+
this._isProgrammaticScroll = true;
|
|
341
368
|
container.scrollTop = container.scrollHeight;
|
|
342
|
-
|
|
369
|
+
requestAnimationFrame(() => {
|
|
370
|
+
this._isProgrammaticScroll = false;
|
|
371
|
+
});
|
|
372
|
+
this._setupScrollTracking(container, scrollButton!, { skipInitialCheck: true });
|
|
343
373
|
}
|
|
344
374
|
|
|
345
375
|
if (scrollButton && !isInfinite) {
|
|
@@ -382,18 +412,43 @@ export class BBMsgHistory extends HTMLElement {
|
|
|
382
412
|
options?: { skipInitialCheck?: boolean }
|
|
383
413
|
): void {
|
|
384
414
|
const checkScrollPosition = () => {
|
|
415
|
+
// Ignore programmatic scrolls - they don't indicate user intent
|
|
416
|
+
if (this._isProgrammaticScroll) return;
|
|
417
|
+
|
|
418
|
+
// Mark that user has manually scrolled
|
|
419
|
+
if (!this._userHasScrolledManually) {
|
|
420
|
+
this._userHasScrolledManually = true;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const currentScrollTop = container.scrollTop;
|
|
424
|
+
const isScrollingUp = currentScrollTop < this._lastScrollTop;
|
|
425
|
+
this._lastScrollTop = currentScrollTop;
|
|
426
|
+
|
|
385
427
|
const threshold = 50; // pixels from bottom
|
|
386
428
|
const isAtBottom =
|
|
387
429
|
container.scrollHeight - container.scrollTop - container.clientHeight < threshold;
|
|
388
430
|
const hasOverflow = container.scrollHeight > container.clientHeight;
|
|
389
|
-
|
|
431
|
+
// Only show button when: user has scrolled, is not at bottom, is scrolling up
|
|
432
|
+
const shouldShow = !isAtBottom && hasOverflow && this._userHasScrolledManually && isScrollingUp;
|
|
390
433
|
|
|
391
434
|
if (shouldShow !== this._scrollButtonVisible) {
|
|
392
435
|
this._scrollButtonVisible = shouldShow;
|
|
393
436
|
button.classList.toggle('visible', shouldShow);
|
|
437
|
+
|
|
438
|
+
// Dispatch custom event
|
|
439
|
+
this.dispatchEvent(
|
|
440
|
+
new CustomEvent(shouldShow ? 'bb-scrollbuttonshow' : 'bb-scrollbuttonhide', {
|
|
441
|
+
bubbles: true,
|
|
442
|
+
composed: true,
|
|
443
|
+
detail: { visible: shouldShow }
|
|
444
|
+
})
|
|
445
|
+
);
|
|
394
446
|
}
|
|
395
447
|
};
|
|
396
448
|
|
|
449
|
+
// Initialize last scroll position
|
|
450
|
+
this._lastScrollTop = container.scrollTop;
|
|
451
|
+
|
|
397
452
|
// Check initial state unless skipped
|
|
398
453
|
if (!options?.skipInitialCheck) {
|
|
399
454
|
checkScrollPosition();
|
package/src/const/styles.ts
CHANGED
|
@@ -72,6 +72,11 @@ export const MAIN_STYLES = `
|
|
|
72
72
|
display: none;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
/* Hide scroll button when explicitly disabled */
|
|
76
|
+
:host([hide-scroll-button]) .scroll-to-bottom {
|
|
77
|
+
display: none;
|
|
78
|
+
}
|
|
79
|
+
|
|
75
80
|
/* Scroll to bottom button */
|
|
76
81
|
.scroll-to-bottom {
|
|
77
82
|
position: absolute;
|