From: Alexander Wilms Date: Thu, 26 Jun 2025 21:42:33 +0000 (+0200) Subject: refactor(mermaid-viewer): Bind event handlers to class instance X-Git-Url: http://source.bookstackapp.com/hacks/commitdiff_plain/7300a518bd2c8352b2e6f681a9de78ce5a97f823 refactor(mermaid-viewer): Bind event handlers to class instance This ensures proper addition and removal of event listeners and fixes a memory leak. Also increases the z-index of the notification. --- diff --git a/content/mermaid-viewer/head.html b/content/mermaid-viewer/head.html index 2470231..68962ca 100644 --- a/content/mermaid-viewer/head.html +++ b/content/mermaid-viewer/head.html @@ -70,8 +70,25 @@ this.zoomOutBtn = null; this.zoomResetBtn = null; + // Bind event handlers for proper addition and removal this.boundMouseMoveHandler = this.handleMouseMove.bind(this); this.boundMouseUpHandler = this.handleMouseUp.bind(this); + this.boundToggleInteraction = this.toggleInteraction.bind(this); + this.boundCopyCode = this.copyCode.bind(this); + this.boundZoomIn = () => { + const { clientX, clientY } = this._getViewportCenterClientCoords(); + this.zoom(1, clientX, clientY); + }; + this.boundZoomOut = () => { + const { clientX, clientY } = this._getViewportCenterClientCoords(); + this.zoom(-1, clientX, clientY); + }; + this.boundResetZoom = this.resetZoom.bind(this); + this.boundHandleWheel = this.handleWheel.bind(this); + this.boundHandleMouseDown = this.handleMouseDown.bind(this); + this.boundPreventDefault = e => e.preventDefault(); + this.boundPreventSelect = e => { if (this.isDragging || this.interactionEnabled) e.preventDefault(); }; + this.setupViewer(); this.setupEventListeners(); } @@ -161,52 +178,22 @@ } setupEventListeners() { - this.toggleInteractionBtn.addEventListener('click', () => this.toggleInteraction()); - this.copyCodeBtn.addEventListener('click', () => this.copyCode()); - this.zoomInBtn.addEventListener('click', () => { - const { clientX, clientY } = this._getViewportCenterClientCoords(); - this.zoom(1, clientX, clientY); - }); - this.zoomOutBtn.addEventListener('click', () => { - const { clientX, clientY } = this._getViewportCenterClientCoords(); - this.zoom(-1, clientX, clientY); - }); - this.zoomResetBtn.addEventListener('click', () => this.resetZoom()); - - this.viewport.addEventListener('wheel', (e) => { - if (!this.interactionEnabled) return; - // Prevent default browser scroll/zoom behavior when wheeling over the diagram - e.preventDefault(); - this.content.classList.add(CSS_CLASSES.ZOOMING); - const clientX = e.clientX; - const clientY = e.clientY; - if (e.deltaY > 0) this.zoom(-1, clientX, clientY); - else this.zoom(1, clientX, clientY); - setTimeout(() => this.content.classList.remove(CSS_CLASSES.ZOOMING), ZOOM_ANIMATION_CLASS_TIMEOUT_MS); - }, { passive: false }); + this.toggleInteractionBtn.addEventListener('click', this.boundToggleInteraction); + this.copyCodeBtn.addEventListener('click', this.boundCopyCode); + this.zoomInBtn.addEventListener('click', this.boundZoomIn); + this.zoomOutBtn.addEventListener('click', this.boundZoomOut); + this.zoomResetBtn.addEventListener('click', this.boundResetZoom); - this.viewport.addEventListener('mousedown', (e) => { - if (!this.interactionEnabled || e.button !== 0) return; - e.preventDefault(); - this.isDragging = true; - this.dragStarted = false; - this.startX = e.clientX; - this.startY = e.clientY; - this.dragBaseTranslateX = this.translateX; - this.dragBaseTranslateY = this.translateY; - this.viewport.classList.add(CSS_CLASSES.DRAGGING); - this.viewport.classList.remove(CSS_CLASSES.INTERACTIVE_HOVER); - this.viewport.classList.add(CSS_CLASSES.INTERACTIVE_PAN); - this.content.classList.remove(CSS_CLASSES.ZOOMING); - }); + this.viewport.addEventListener('wheel', this.boundHandleWheel, { passive: false }); + this.viewport.addEventListener('mousedown', this.boundHandleMouseDown); // Listen on document for mousemove to handle dragging outside viewport document.addEventListener('mousemove', this.boundMouseMoveHandler); // Listen on window for mouseup to ensure drag ends even if mouse is released outside window.addEventListener('mouseup', this.boundMouseUpHandler, true); // Use capture phase - this.viewport.addEventListener('contextmenu', (e) => e.preventDefault()); - this.viewport.addEventListener('selectstart', (e) => { if (this.isDragging || this.interactionEnabled) e.preventDefault(); }); + this.viewport.addEventListener('contextmenu', this.boundPreventDefault); + this.viewport.addEventListener('selectstart', this.boundPreventSelect); } toggleInteraction() { @@ -236,6 +223,33 @@ this.content.style.transform = `translate(${this.translateX}px, ${this.translateY}px) scale(${this.scale})`; } + handleWheel(e) { + if (!this.interactionEnabled) return; + // Prevent default browser scroll/zoom behavior when wheeling over the diagram + e.preventDefault(); + this.content.classList.add(CSS_CLASSES.ZOOMING); + const clientX = e.clientX; + const clientY = e.clientY; + if (e.deltaY > 0) this.zoom(-1, clientX, clientY); + else this.zoom(1, clientX, clientY); + setTimeout(() => this.content.classList.remove(CSS_CLASSES.ZOOMING), ZOOM_ANIMATION_CLASS_TIMEOUT_MS); + } + + handleMouseDown(e) { + if (!this.interactionEnabled || e.button !== 0) return; + e.preventDefault(); + this.isDragging = true; + this.dragStarted = false; + this.startX = e.clientX; + this.startY = e.clientY; + this.dragBaseTranslateX = this.translateX; + this.dragBaseTranslateY = this.translateY; + this.viewport.classList.add(CSS_CLASSES.DRAGGING); + this.viewport.classList.remove(CSS_CLASSES.INTERACTIVE_HOVER); + this.viewport.classList.add(CSS_CLASSES.INTERACTIVE_PAN); + this.content.classList.remove(CSS_CLASSES.ZOOMING); + } + handleMouseMove(e) { if (!this.isDragging) return; // e.preventDefault() is called only after dragStarted is true to allow clicks if threshold isn't met. @@ -373,16 +387,16 @@ destroy() { // Remove event listeners specific to this instance - this.toggleInteractionBtn.removeEventListener('click', this.toggleInteraction); // Need to ensure this is the same function reference - this.copyCodeBtn.removeEventListener('click', this.copyCode); - this.zoomInBtn.removeEventListener('click', this.zoom); // These would need bound versions or careful handling - this.zoomOutBtn.removeEventListener('click', this.zoom); - this.zoomResetBtn.removeEventListener('click', this.resetZoom); + this.toggleInteractionBtn.removeEventListener('click', this.boundToggleInteraction); + this.copyCodeBtn.removeEventListener('click', this.boundCopyCode); + this.zoomInBtn.removeEventListener('click', this.boundZoomIn); + this.zoomOutBtn.removeEventListener('click', this.boundZoomOut); + this.zoomResetBtn.removeEventListener('click', this.boundResetZoom); - this.viewport.removeEventListener('wheel', this.handleWheel); // Assuming handleWheel is the actual handler - this.viewport.removeEventListener('mousedown', this.handleMouseDown); // Assuming handleMouseDown - this.viewport.removeEventListener('contextmenu', this.handleContextMenu); - this.viewport.removeEventListener('selectstart', this.handleSelectStart); + this.viewport.removeEventListener('wheel', this.boundHandleWheel, { passive: false }); + this.viewport.removeEventListener('mousedown', this.boundHandleMouseDown); + this.viewport.removeEventListener('contextmenu', this.boundPreventDefault); + this.viewport.removeEventListener('selectstart', this.boundPreventSelect); document.removeEventListener('mousemove', this.boundMouseMoveHandler); window.removeEventListener('mouseup', this.boundMouseUpHandler, true); @@ -546,7 +560,8 @@ border-radius: 6px; transform: translateX(400px); transition: transform 0.3s ease; - z-index: 1000; + z-index: 10000; + /* Increased z-index to appear above site header */ } .mermaid-notification.show {