Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 51 additions & 1 deletion core/target.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,13 +112,39 @@ exports.Target = class Target extends Montage {
* https://developer.mozilla.org/en-US/docs/Web/API/Event/composedPath
* @property {Array<Target>}
*/
composedPath: {value: undefined}
composedPath: { value: undefined },

/**
* Map storing the last timestamp when each interaction type was processed
* @type {Map<string, number>}
* @private
*/
__debouncedInteractionTimestamps: { value: null },

/**
* Map of threshold durations (in milliseconds) for each interaction type
* @type {Map<string, number>}
* @private
*/
__debouncedInteractionThresholds: { value: null },
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@marchant if you come up with a better name

});
}

get _debouncedInteractionTimestamps() {
if (!this.__debouncedInteractionTimestamps) {
this.__debouncedInteractionTimestamps = new Map();
}

return this.__debouncedInteractionTimestamps;
}

get _debouncedInteractionThresholds() {
if (!this.__debouncedInteractionThresholds) {
this.__debouncedInteractionThresholds = new Map();
}

return this.__debouncedInteractionThresholds;
}

/**
* Whether or not this is the activeTarget
Expand All @@ -133,7 +159,31 @@ exports.Target = class Target extends Montage {
return this === defaultEventManager.activeTarget;
}

registerInteractionForDebounce(interactionKey, threshold) {
this._debouncedInteractionThresholds.set(interactionKey, threshold);
}

unregisterInteractionForDebounce(interactionKey) {
this._debouncedInteractionThresholds.delete(interactionKey);
this._debouncedInteractionTimestamps.delete(interactionKey);
}

shouldPreventInteraction(interactionKey) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@marchant if you come up with a better name

if (!this._debouncedInteractionThresholds.has(interactionKey)) return false;

const currentTime = Date.now();
const lastTime = this._debouncedInteractionTimestamps.get(interactionKey) || 0;
const threshold = this._debouncedInteractionThresholds.get(interactionKey) || 0;
const delta = currentTime - lastTime;

// Check if we need to debounce
if (lastTime > 0 && delta < threshold) return true;

// Update timestamp for this interaction type
this._debouncedInteractionTimestamps.set(interactionKey, currentTime);

return false;
}

/**
* Ask this target to surrender its activeTarget status.
Expand Down
55 changes: 53 additions & 2 deletions ui/button.mod/button.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,50 @@ const Button = (exports.Button = class Button extends Control {

static VisualPosition = VisualPosition;

/**
* How long to wait (in milliseconds) between allowing press events
* @type {number}
*/
_debounceThreshold = 300;

/**
* Whether debouncing is enabled for press events
* @type {boolean}
*/
_debounceEnabled = false;

get debounceEnabled() {
return this._debounceEnabled;
}

set debounceEnabled(enabled) {
enabled = !!enabled;

if (enabled !== this._debounceEnabled) {
this._debounceEnabled = enabled;
this._updateActionDebounceRegistration();
}
}

get debounceThreshold() {
return this._debounceThreshold;
}

set debounceThreshold(threshold) {
if (typeof threshold === "number" && threshold >= 0 && this._debounceThreshold !== threshold) {
this._debounceThreshold = threshold;
this._updateActionDebounceRegistration();
}
}

_updateActionDebounceRegistration() {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@marchant if you come up with a better name

if (this.debounceEnabled ) {
this.registerInteractionForDebounce("action", this.debounceThreshold);
} else {
this.unregisterInteractionForDebounce("action");
}
}

// <---- Properties ---->

_visualPosition = VisualPosition.start;
Expand Down Expand Up @@ -275,7 +319,10 @@ const Button = (exports.Button = class Button extends Control {

if (identifier === "space" || identifier === "enter") {
this.active = false;
this.dispatchActionEvent();

if (!this.shouldPreventInteraction("action")) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@marchant I tried to add it to the press composer and the key composers and it worked fine only when the source was the same composer.
Meaning, an interaction from the press composer and the key composer during the debouncing window will cause 2 action events. So, I added the logic to Target and it's managed by the button itself. I also have a version working for the press composer and the key composer but that would be for another PR.

this.dispatchActionEvent();
}
}
}

Expand All @@ -300,7 +347,11 @@ const Button = (exports.Button = class Button extends Control {
handlePress(mutableEvent) {
if (!this._promise) {
this.active = false;
this.dispatchActionEvent(event.details);

if (!this.shouldPreventInteraction("action")) {
this.dispatchActionEvent(event.details);
}

this._removeEventListeners();
}
}
Expand Down
18 changes: 18 additions & 0 deletions ui/button.mod/teach/ui/main.mod/main.html
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,22 @@
"element": {"#": "text"},
"value": {"<-": "@owner.message || 'please hit a button'"}
}
},
"debounceButton": {
"prototype": "mod/ui/button.mod",
"values": {
"element": {"#": "debounceButton"},
"debounceEnabled": true,
"label": "Debounced Button",
"debounceThreshold": 1000
}
},
"debounceText": {
"prototype": "mod/ui/text.mod",
"values": {
"element": {"#": "debounceText"},
"value": {"<-": "@owner.debounceMessage"}
}
}
}
</script>
Expand All @@ -84,6 +100,8 @@
<span data-mod-id="text" class="infoText"></span>
<br />
<button>Bare Button</button>
<button data-mod-id="debounceButton" class="button"></button>
<span data-mod-id="debounceText" class="infoText"></span>
</section>
</div>
</body>
Expand Down
8 changes: 8 additions & 0 deletions ui/button.mod/teach/ui/main.mod/main.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
const { Component } = require("mod/ui/component");

exports.Main = class Main extends Component {
debounceCounter = 0;

message = null;

handleAction(event) {
Expand All @@ -11,6 +13,12 @@ exports.Main = class Main extends Component {
this.message = `${event.target.identifier} button has been clicked (long action)`;
}

handleDebounceButtonAction(event) {
this.debounceCounter++;
const id = event.target.identifier;
this.debounceMessage = `${id} button has been clicked (debounce) ${this.debounceCounter} times`;
}

async handlePromiseButtonAction(_) {
this.message = "First Promise is pending resolution. Wait 2 seconds...";

Expand Down