Skip to content

Conversation

@saqimtiaz
Copy link
Member

@saqimtiaz saqimtiaz commented Jan 23, 2026

This PR introduces three major enhancements to the EventCatcher widget:

  1. Optional Pointer Capture Support
    Adds native pointer capture handling to improve pointer event reliability and tracking outside the widget DOM nodes boundaries. When enabled, pointer events such as pointermove, pointerup, and pointercancel are correctly captured and routed to the widget, even if the pointer moves off-screen or outside the element.
    The pointer capture logic supports two modes of event listener attachment for better performance and control.

  2. Widget Enable/Disable Control
    Adds a new attribute to enable or disable the EventCatcher widget at runtime. This allows users to temporarily deactivate event handling without removing the widget or disrupting the DOM structure, enhancing flexibility for dynamic UIs.

  3. Access to a JSON blob containing event properties via the variable eventJSON. The event.detail related variables have been deprecated as they were always brittle and the seem needs can now be met via eventJSON.


User-Facing Attributes

Attribute Default Description
enabled yes Enables or disables the EventCatcher widget. When set to no, all event listeners are removed.
pointerCapture no Enables native pointer capture support (yes, no, dynamic).

Detailed Behavior of pointerCapture

When pointerCapture is dynamic

  • Attaches pointerdown, pointerup and pointercancel event listeners at the appropriate time even if they are not specified in the widget attributes.
  • The pointerdown event listener is attached when the widget is initialized and when a pointerdown occurs, pointer capture is set with setPointerCapture.
  • The widget dynamically attaches the following pointer event listeners for the duration of the pointer capture:
    • pointerup
    • pointercancel
    • pointermove (only if the user has specified it in the widget attributes)
  • When the interaction ends (pointerup or pointercancel), these active listeners are removed, and pointer capture is released.
  • This mode minimizes the number of active event listeners and improves performance by only listening when necessary.

When dynamicPointerListeners is yes

  • The widget attaches all event listeners specified in the widget attributes when it is rendered.
  • No pointer event listeners are added that are not explicitly specified in the widget attributes.
  • If pointerdown has been specified, it initiates pointer capture.
  • If pointerup or pointercancel have been specified, they release pointer capture.

To Do:

  • ensure that we support the use case where no selector attribute has been specified.
  • further testing in FF to verify recent changes are compatible
  • update documentation
  • add examples
  • refactor to avoid deprecated utils functions where possible
    • $tw.utils.domMatchesSelector

@netlify
Copy link

netlify bot commented Jan 23, 2026

Deploy Preview for tiddlywiki-previews ready!

Name Link
🔨 Latest commit 92c41f8
🔍 Latest deploy log https://app.netlify.com/projects/tiddlywiki-previews/deploys/69767e42d1b4670008469dbb
😎 Deploy Preview https://deploy-preview-9609--tiddlywiki-previews.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@github-actions
Copy link

Confirmed: saqimtiaz has already signed the Contributor License Agreement (see contributing.md)

@github-actions
Copy link

github-actions bot commented Jan 23, 2026

📊 Build Size Comparison: empty.html

Branch Size
Base (master) 2448.0 KB
PR 2453.6 KB

Diff: ⬆️ Increase: +5.6 KB


✅ Change Note Status

All change notes are properly formatted and validated!

📝 $:/changenotes/5.4.0/#9609

Type: enhancement | Category: widget
Release: 5.4.0

adds support for pointer capture and enabling/disabling the widget

🔗 #9609

👥 Contributors: saqimtiaz


📖 Change Note Guidelines

Change notes help track and communicate changes effectively. See the full documentation for details.

@saqimtiaz
Copy link
Member Author

@Jermolene seeing as how this is a complete rewrite, should we take this opportunity to switch to using JavaScript classes?

class EventWidget extends Widget { ....

@saqimtiaz saqimtiaz marked this pull request as ready for review January 25, 2026 15:54
@saqimtiaz saqimtiaz moved this to Ready in Planning for v5.4.0 Jan 25, 2026
if(matchSelector && !selectedNode.matches(matchSelector)) {
return null;
}
if(selector) {
Copy link
Contributor

@yaisog yaisog Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we have something like

// Search ancestors for a node that matches the selector
const matchedAncestor = selectedNode.closest(selector);
		        
// Exit if we didn't find the selector, or if we went past domNode
if(!matchedAncestor || !domNode.contains(matchedAncestor)) {
    return false;
}

using closest() instead of the while loop? All rlevant browser should support this from 2017 on.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The while loop is actually problematic when you have a listener like "pointerdown" on a DOM element that changes or disappears due to the click. That can lead to parentNode being null at some point in the traversal, giving an RSoE. In an exemplary case this happens with a Pikaday calendar.

Copy link
Member Author

@saqimtiaz saqimtiaz Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using closest() wont stop at the widget DOM node boundary and could give therefore give false positive results. We can guard against parentNode being null.

This might work but I will need to investigate next week, I am away until Wednesday, Feb 4th.

const matchedAncestor = selectedNode.closest(selector);
selectedNode = (matchedAncestor && domNode.contains(matchedAncestor))
	? matchedAncestor
	: null;

@yaisog
Copy link
Contributor

yaisog commented Jan 28, 2026

Hi @saqimtiaz, we should probably unify the parameter names between this PR and #9528.
I'm using json-message in the MessageCatcherWidget (from e.g. list-event which became list-message). We could change that to messageJSON. It'd be more in line with your eventJSON...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Ready

Development

Successfully merging this pull request may close these issues.

2 participants