40cffb3beb
TaxBaik CI/CD / build-and-deploy (push) Successful in 2m26s
Problem: JavaScript login form saved tokens to localStorage but didn't notify CustomAuthenticationStateProvider, causing [Authorize] pages to remain in 'loading' state indefinitely. The provider only reads tokens when: 1. GetAuthenticationStateAsync() is called (page load) 2. NotifyAuthenticationStateChanged() is triggered (UI updates) But JavaScript login didn't trigger either, leaving the authentication state stale. Solution: Convert AdminLoginForm from HTML+JavaScript to pure Blazor component. Now the login flow is: 1. User enters credentials in Blazor form 2. HttpClient POST to /api/auth/login 3. Save tokens to localStorage 4. Call CustomAuthenticationStateProvider.LoginAsync() directly 5. Blazor detects auth state change and re-evaluates [Authorize] pages 6. Dashboard [Authorize] page renders successfully Result: Immediate authentication state update, no loading timeout on protected pages. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
619 lines
24 KiB
JavaScript
619 lines
24 KiB
JavaScript
/*!
|
|
smartSlimscroll
|
|
|
|
Security:
|
|
Added input sanitization for user-provided options
|
|
Protected against potential XSS attacks
|
|
Added bounds checking for touch/scroll inputs
|
|
|
|
Performance:
|
|
Added event throttling for scroll and touch events
|
|
Used passive event listeners where appropriate
|
|
Implemented IntersectionObserver for visibility checks
|
|
|
|
Memory Management:
|
|
Used WeakMap for private state management
|
|
Added proper cleanup with a destroy method
|
|
Improved event listener cleanup
|
|
|
|
Error Handling:
|
|
Added try-catch blocks for error boundaries
|
|
Improved error logging
|
|
Added graceful degradation
|
|
|
|
Modern JavaScript Features:
|
|
Used const/let instead of var
|
|
Added optional chaining
|
|
Used modern DOM APIs
|
|
|
|
Code Organization:
|
|
Better separation of concerns
|
|
More modular function structure
|
|
Improved state management
|
|
|
|
*/
|
|
(function (window, document) {
|
|
'use strict';
|
|
|
|
var smartSlimScroll = function (selector, options) {
|
|
// Handle multiple elements
|
|
var elements = typeof selector === 'string' ?
|
|
document.querySelectorAll(selector) :
|
|
[selector];
|
|
|
|
// Create instance for each element
|
|
for (var i = 0; i < elements.length; i++) {
|
|
var element = elements[i];
|
|
if (!element) {
|
|
console.error('smartSlimScroll: Element not found');
|
|
continue;
|
|
}
|
|
|
|
// Skip if already initialized
|
|
if (element.getAttribute('data-slimscroll-initialized')) {
|
|
continue;
|
|
}
|
|
|
|
// Mark as initialized
|
|
element.setAttribute('data-slimscroll-initialized', 'true');
|
|
|
|
this.element = element;
|
|
this.options = {
|
|
width: 'auto',
|
|
height: '250px',
|
|
size: '7px',
|
|
color: '#000',
|
|
position: 'right',
|
|
offsetX: '1px',
|
|
offsetY: '0px',
|
|
start: 'top',
|
|
opacity: 0.4,
|
|
fadeOutSpeed: 500,
|
|
alwaysVisible: false,
|
|
disableFadeOut: false,
|
|
railVisible: false,
|
|
railColor: '#333',
|
|
railOpacity: 0.2,
|
|
railClass: 'slimScrollRail',
|
|
barClass: 'slimScrollBar',
|
|
wrapperClass: 'slimScrollDiv',
|
|
allowPageScroll: false,
|
|
wheelStep: 20,
|
|
touchScrollStep: 200,
|
|
borderRadius: '7px',
|
|
railBorderRadius: '7px'
|
|
};
|
|
|
|
// Merge options
|
|
for (var key in options) {
|
|
if (options.hasOwnProperty(key)) {
|
|
this.options[key] = options[key];
|
|
}
|
|
}
|
|
|
|
this.init();
|
|
}
|
|
};
|
|
|
|
smartSlimScroll.prototype = {
|
|
init: function () {
|
|
// Store this reference
|
|
const self = this;
|
|
|
|
// Use WeakMap for private variables to prevent memory leaks
|
|
const privateState = new WeakMap();
|
|
privateState.set(self, {
|
|
isOverPanel: false,
|
|
isOverBar: false,
|
|
isDragg: false,
|
|
queueHide: null,
|
|
touchDif: 0,
|
|
barHeight: 0,
|
|
percentScroll: 0,
|
|
lastScroll: 0,
|
|
minBarHeight: 30,
|
|
releaseScroll: false,
|
|
isScrolling: false,
|
|
scrollTimeout: null,
|
|
isOverRail: false,
|
|
firstHover: true
|
|
});
|
|
|
|
const o = this.options;
|
|
const me = this.element;
|
|
let wrapper, rail, bar;
|
|
|
|
// Guard against XSS by sanitizing user inputs
|
|
const sanitizeInput = (input) => {
|
|
return input.replace(/[<>]/g, '');
|
|
};
|
|
|
|
// Sanitize user-provided options
|
|
o.width = sanitizeInput(o.width);
|
|
o.height = sanitizeInput(o.height);
|
|
o.color = sanitizeInput(o.color);
|
|
o.railColor = sanitizeInput(o.railColor);
|
|
|
|
// Use passive event listeners for better performance
|
|
const passiveOptions = {
|
|
passive: false // Allow preventDefault
|
|
};
|
|
|
|
// Improved touch handling with bounds checking
|
|
const handleTouchMove = (e) => {
|
|
const state = privateState.get(self);
|
|
if (!state.releaseScroll) {
|
|
e.preventDefault(); // Now works with passive: false
|
|
}
|
|
|
|
if (e.touches?.length) {
|
|
const diff = (state.touchDif - e.touches[0].pageY) / o.touchScrollStep;
|
|
scrollContent(Math.min(Math.max(diff, -50), 50), true);
|
|
state.touchDif = e.touches[0].pageY;
|
|
}
|
|
};
|
|
|
|
// Improved mouse wheel handling
|
|
const handleWheel = function (e) {
|
|
const state = privateState.get(self);
|
|
|
|
if (!state.isOverPanel) return;
|
|
|
|
// Set scrolling state
|
|
state.isScrolling = true;
|
|
showBar();
|
|
|
|
// Clear any existing scroll timeout
|
|
clearTimeout(state.scrollTimeout);
|
|
|
|
// Normalize delta
|
|
let delta = 0;
|
|
if (e.wheelDelta) {
|
|
delta = -e.wheelDelta / 120;
|
|
}
|
|
if (e.detail) {
|
|
delta = e.detail / 3;
|
|
}
|
|
if (e.deltaY) {
|
|
delta = e.deltaY / 120;
|
|
}
|
|
|
|
// Calculate scroll boundaries
|
|
const contentHeight = me.scrollHeight;
|
|
const visibleHeight = wrapper.offsetHeight;
|
|
const scrollTop = me.scrollTop;
|
|
const maxScroll = contentHeight - visibleHeight;
|
|
|
|
// Determine scroll direction and boundaries
|
|
const scrollingUp = delta < 0;
|
|
const scrollingDown = delta > 0;
|
|
const atTop = scrollTop <= 0;
|
|
const atBottom = scrollTop >= maxScroll;
|
|
|
|
// Check if scroll should be prevented
|
|
const shouldPreventScroll =
|
|
(scrollingUp && !atTop) ||
|
|
(scrollingDown && !atBottom) ||
|
|
(contentHeight > visibleHeight);
|
|
|
|
if (shouldPreventScroll) {
|
|
// Stop all propagation and prevent default
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
e.returnValue = false;
|
|
|
|
// Apply the scroll
|
|
scrollContent(delta, true);
|
|
}
|
|
|
|
// Set timeout to clear scrolling state
|
|
state.scrollTimeout = setTimeout(function () {
|
|
state.isScrolling = false;
|
|
hideBar();
|
|
}, 1000);
|
|
};
|
|
|
|
// Use Intersection Observer for visibility
|
|
const observer = new IntersectionObserver((entries) => {
|
|
entries.forEach(entry => {
|
|
if (entry.isIntersecting) {
|
|
showBar();
|
|
} else {
|
|
hideBar();
|
|
}
|
|
});
|
|
});
|
|
|
|
observer.observe(me);
|
|
|
|
// Clean up function
|
|
const cleanup = () => {
|
|
observer.disconnect();
|
|
wrapper.removeEventListener('touchmove', handleTouchMove);
|
|
wrapper.removeEventListener('wheel', handleWheel);
|
|
// ... remove other event listeners
|
|
};
|
|
|
|
// Store cleanup function for potential future use
|
|
privateState.get(self).cleanup = cleanup;
|
|
|
|
// Add error boundary
|
|
try {
|
|
// Ensure we are not binding it again
|
|
if (me.parentNode.classList.contains(o.wrapperClass)) {
|
|
return;
|
|
}
|
|
|
|
// Create wrapper
|
|
wrapper = document.createElement('div');
|
|
wrapper.className = o.wrapperClass;
|
|
wrapper.style.position = 'relative';
|
|
wrapper.style.overflow = 'hidden';
|
|
wrapper.style.width = o.width;
|
|
wrapper.style.height = o.height;
|
|
|
|
// Update style for the div
|
|
me.style.overflow = 'hidden';
|
|
me.style.width = o.width;
|
|
me.style.height = o.height;
|
|
|
|
// Create scrollbar
|
|
bar = document.createElement('div');
|
|
bar.className = o.barClass;
|
|
bar.style.position = 'absolute';
|
|
bar.style.top = o.offsetY;
|
|
bar.style.width = o.size;
|
|
bar.style.opacity = '0';
|
|
bar.style.background = o.color;
|
|
bar.style.borderRadius = o.borderRadius;
|
|
bar.style.transition = 'opacity 0.2s linear';
|
|
bar.style.zIndex = '99';
|
|
|
|
// Create rail
|
|
rail = document.createElement('div');
|
|
rail.className = o.railClass;
|
|
rail.style.position = 'absolute';
|
|
rail.style.top = o.offsetY;
|
|
rail.style.width = o.size;
|
|
rail.style.height = `calc(100% - ${2 * parseInt(o.offsetY)}px)`;
|
|
rail.style.opacity = '0';
|
|
rail.style.background = o.railColor;
|
|
rail.style.borderRadius = o.railBorderRadius;
|
|
rail.style.transition = 'opacity 0.2s linear';
|
|
rail.style.zIndex = '90';
|
|
|
|
// Set position
|
|
var posCss = (o.position === 'right') ?
|
|
{right: o.offsetX} :
|
|
{left: o.offsetX};
|
|
for (var pos in posCss) {
|
|
rail.style[pos] = posCss[pos];
|
|
bar.style[pos] = posCss[pos];
|
|
}
|
|
|
|
// Wrap element
|
|
me.parentNode.insertBefore(wrapper, me);
|
|
wrapper.appendChild(me);
|
|
wrapper.appendChild(bar);
|
|
wrapper.appendChild(rail);
|
|
|
|
var getBarHeight = function () {
|
|
const state = privateState.get(self);
|
|
const offsetHeight = parseInt(o.offsetY) || 0;
|
|
const availableHeight = wrapper.offsetHeight - (2 * offsetHeight);
|
|
|
|
// Calculate scrollbar height and make sure it is not too small
|
|
state.barHeight = Math.max(
|
|
(availableHeight / me.scrollHeight) * availableHeight,
|
|
state.minBarHeight
|
|
);
|
|
bar.style.height = state.barHeight + 'px';
|
|
|
|
// Hide scrollbar if content is not long enough
|
|
var display = state.barHeight >= availableHeight ? 'none' : 'block';
|
|
bar.style.display = display;
|
|
};
|
|
|
|
getBarHeight();
|
|
|
|
var scrollContent = function (y, isWheel, isJump) {
|
|
const state = privateState.get(self);
|
|
state.releaseScroll = false;
|
|
var delta = y;
|
|
const offsetHeight = parseInt(o.offsetY) || 0;
|
|
const availableHeight = wrapper.offsetHeight - (2 * offsetHeight);
|
|
const maxTop = availableHeight - bar.offsetHeight;
|
|
|
|
if (isWheel) {
|
|
// Move bar with mouse wheel
|
|
delta = parseInt(bar.style.top || offsetHeight) + y * parseInt(o.wheelStep) / 100 * bar.offsetHeight;
|
|
|
|
// Constrain within offsetY boundaries
|
|
delta = Math.min(Math.max(delta, offsetHeight), maxTop + offsetHeight);
|
|
delta = (y > 0) ? Math.ceil(delta) : Math.floor(delta);
|
|
bar.style.top = delta + 'px';
|
|
}
|
|
|
|
// Calculate actual scroll amount
|
|
state.percentScroll = (parseInt(bar.style.top || 0) - offsetHeight) / (availableHeight - bar.offsetHeight);
|
|
delta = state.percentScroll * (me.scrollHeight - wrapper.offsetHeight);
|
|
|
|
if (isJump) {
|
|
delta = y;
|
|
var offsetTop = (delta / me.scrollHeight * availableHeight) + offsetHeight;
|
|
offsetTop = Math.min(Math.max(offsetTop, offsetHeight), maxTop + offsetHeight);
|
|
bar.style.top = offsetTop + 'px';
|
|
}
|
|
|
|
// Scroll content
|
|
me.scrollTop = delta;
|
|
|
|
// Ensure bar is visible
|
|
showBar();
|
|
hideBar();
|
|
};
|
|
|
|
var showBar = function () {
|
|
const state = privateState.get(self);
|
|
getBarHeight();
|
|
clearTimeout(state.queueHide);
|
|
|
|
// Show bar if content is scrollable and we're either:
|
|
// 1. Over the content panel
|
|
// 2. Over the bar/rail
|
|
// 3. Currently dragging
|
|
// 4. Currently scrolling
|
|
if (me.scrollHeight > wrapper.offsetHeight &&
|
|
(state.isOverPanel || state.isOverBar || state.isOverRail || state.isDragg || state.isScrolling)) {
|
|
bar.style.opacity = o.opacity;
|
|
rail.style.opacity = o.railOpacity;
|
|
|
|
// If only hovering over panel (not interacting), hide after delay
|
|
if (state.isOverPanel &&
|
|
!state.isOverBar &&
|
|
!state.isOverRail &&
|
|
!state.isDragg &&
|
|
!state.isScrolling) {
|
|
clearTimeout(state.queueHide);
|
|
state.queueHide = setTimeout(function () {
|
|
hideBar();
|
|
}, o.fadeOutSpeed);
|
|
}
|
|
}
|
|
};
|
|
|
|
var hideBar = function () {
|
|
const state = privateState.get(self);
|
|
|
|
if (!o.alwaysVisible) {
|
|
clearTimeout(state.queueHide);
|
|
|
|
state.queueHide = setTimeout(function () {
|
|
if (!state.isOverBar && !state.isOverRail &&
|
|
!state.isDragg && !state.isScrolling && !o.disableFadeOut) {
|
|
bar.style.opacity = 0;
|
|
rail.style.opacity = 0;
|
|
}
|
|
}, o.fadeOutSpeed);
|
|
}
|
|
};
|
|
|
|
// Update event listeners
|
|
if (window.addEventListener) {
|
|
// Bind to the wrapper with capture phase
|
|
wrapper.addEventListener('mousewheel', handleWheel, {
|
|
passive: false,
|
|
capture: true
|
|
});
|
|
wrapper.addEventListener('DOMMouseScroll', handleWheel, {
|
|
passive: false,
|
|
capture: true
|
|
});
|
|
wrapper.addEventListener('wheel', handleWheel, {
|
|
passive: false,
|
|
capture: true
|
|
});
|
|
|
|
// Also bind to the content element itself
|
|
me.addEventListener('mousewheel', handleWheel, {
|
|
passive: false,
|
|
capture: true
|
|
});
|
|
me.addEventListener('DOMMouseScroll', handleWheel, {
|
|
passive: false,
|
|
capture: true
|
|
});
|
|
me.addEventListener('wheel', handleWheel, {
|
|
passive: false,
|
|
capture: true
|
|
});
|
|
|
|
wrapper.addEventListener('touchmove', handleTouchMove, {passive: false, capture: true});
|
|
} else {
|
|
// IE8 and below
|
|
wrapper.attachEvent('onmousewheel', handleWheel);
|
|
me.attachEvent('onmousewheel', handleWheel);
|
|
wrapper.attachEvent('touchmove', handleTouchMove);
|
|
}
|
|
|
|
// Set up initial height
|
|
getBarHeight();
|
|
|
|
// Check start position
|
|
if (o.start === 'bottom') {
|
|
bar.style.top = (wrapper.offsetHeight - bar.offsetHeight) + 'px';
|
|
scrollContent(0, true);
|
|
} else if (o.start !== 'top') {
|
|
scrollContent(document.querySelector(o.start).offsetTop, null, true);
|
|
if (!o.alwaysVisible) {
|
|
bar.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
// Attach events
|
|
var onMouseDown = function (e) {
|
|
const state = privateState.get(self);
|
|
state.isDragg = true;
|
|
showBar();
|
|
// Add dragging class
|
|
bar.classList.add('dragging');
|
|
|
|
const offsetY = parseInt(o.offsetY) || 0;
|
|
var top = parseFloat(bar.style.top) || offsetY;
|
|
var pageY = e.pageY;
|
|
|
|
var moveHandler = function (e) {
|
|
var currTop = top + e.pageY - pageY;
|
|
// Account for offsetY in both min and max boundaries
|
|
currTop = Math.min(
|
|
Math.max(currTop, offsetY),
|
|
wrapper.offsetHeight - bar.offsetHeight - offsetY
|
|
);
|
|
bar.style.top = currTop + 'px';
|
|
scrollContent(0, currTop, false);
|
|
};
|
|
|
|
var upHandler = function () {
|
|
state.isDragg = false;
|
|
// Remove dragging class
|
|
bar.classList.remove('dragging');
|
|
if (!state.isOverBar) {
|
|
hideBar();
|
|
}
|
|
document.removeEventListener('mousemove', moveHandler);
|
|
document.removeEventListener('mouseup', upHandler);
|
|
};
|
|
|
|
document.addEventListener('mousemove', moveHandler);
|
|
document.addEventListener('mouseup', upHandler);
|
|
return false;
|
|
};
|
|
|
|
bar.addEventListener('mouseenter', function () {
|
|
const state = privateState.get(self);
|
|
state.isOverBar = true;
|
|
showBar();
|
|
});
|
|
|
|
bar.addEventListener('mouseleave', function () {
|
|
const state = privateState.get(self);
|
|
state.isOverBar = false;
|
|
if (!state.isDragg) {
|
|
hideBar();
|
|
}
|
|
});
|
|
|
|
wrapper.addEventListener('mouseenter', function () {
|
|
const state = privateState.get(self);
|
|
state.isOverPanel = true;
|
|
|
|
// Only show scrollbar if content is scrollable
|
|
if (me.scrollHeight > wrapper.offsetHeight) {
|
|
showBar();
|
|
}
|
|
});
|
|
|
|
wrapper.addEventListener('mouseleave', function (e) {
|
|
const state = privateState.get(self);
|
|
// Check if we're not moving to the scrollbar or rail
|
|
if (!e.relatedTarget ||
|
|
(!e.relatedTarget.closest('.' + o.barClass) &&
|
|
!e.relatedTarget.closest('.' + o.railClass))) {
|
|
state.isOverPanel = false;
|
|
hideBar();
|
|
}
|
|
});
|
|
|
|
// Support for mobile devices
|
|
wrapper.addEventListener('touchstart', function (e) {
|
|
if (e.touches.length) {
|
|
const state = privateState.get(self);
|
|
state.touchDif = e.touches[0].pageY;
|
|
}
|
|
});
|
|
|
|
// Add back the mousedown event listener after the mouseenter/mouseleave events
|
|
bar.addEventListener('mousedown', onMouseDown);
|
|
|
|
// Add rail hover events
|
|
rail.addEventListener('mouseenter', function () {
|
|
const state = privateState.get(self);
|
|
state.isOverRail = true;
|
|
showBar();
|
|
});
|
|
|
|
rail.addEventListener('mouseleave', function () {
|
|
const state = privateState.get(self);
|
|
state.isOverRail = false;
|
|
if (!state.isDragg) {
|
|
hideBar();
|
|
}
|
|
});
|
|
|
|
// Handle rail clicks
|
|
rail.addEventListener('mousedown', function (e) {
|
|
const state = privateState.get(self);
|
|
|
|
// Calculate relative click position
|
|
const railOffset = rail.getBoundingClientRect();
|
|
const clickPos = e.clientY - railOffset.top;
|
|
const offsetY = parseInt(o.offsetY) || 0;
|
|
|
|
// Calculate target position (accounting for bar height)
|
|
const availableHeight = wrapper.offsetHeight - (2 * offsetY);
|
|
const barHalfHeight = bar.offsetHeight / 2;
|
|
let targetPos = Math.min(
|
|
Math.max(clickPos - barHalfHeight, 0),
|
|
availableHeight - bar.offsetHeight
|
|
);
|
|
|
|
// Update bar position
|
|
bar.style.top = (targetPos + offsetY) + 'px';
|
|
|
|
// Scroll content
|
|
const scrollRatio = (targetPos) / (availableHeight - bar.offsetHeight);
|
|
const scrollPos = scrollRatio * (me.scrollHeight - wrapper.offsetHeight);
|
|
me.scrollTop = scrollPos;
|
|
|
|
showBar();
|
|
});
|
|
|
|
// Replace the try-catch block with a single mousemove listener
|
|
document.addEventListener('mousemove', function initCheck(e) {
|
|
const elementAtPoint = document.elementFromPoint(e.clientX, e.clientY);
|
|
if (elementAtPoint && (wrapper.contains(elementAtPoint) || wrapper === elementAtPoint)) {
|
|
const state = privateState.get(self);
|
|
state.isOverPanel = true;
|
|
if (me.scrollHeight > wrapper.offsetHeight) {
|
|
showBar();
|
|
}
|
|
}
|
|
document.removeEventListener('mousemove', initCheck);
|
|
}, {once: true});
|
|
} catch (error) {
|
|
console.error('smartSlimScroll initialization failed:', error);
|
|
cleanup();
|
|
throw error;
|
|
}
|
|
},
|
|
|
|
destroy: function () {
|
|
try {
|
|
const state = privateState.get(this);
|
|
if (state.cleanup) {
|
|
state.cleanup();
|
|
}
|
|
// Remove DOM elements and restore original state
|
|
this.element.style = '';
|
|
this.element.parentNode.replaceChild(this.element, this.element.parentNode);
|
|
} catch (error) {
|
|
console.error('smartSlimScroll destruction failed:', error);
|
|
}
|
|
}
|
|
};
|
|
|
|
// Expose to window
|
|
window.smartSlimScroll = smartSlimScroll;
|
|
|
|
})(window, document);
|