From 69fd5eea8861671426e6f49bf2e2f36ea4a9bf81 Mon Sep 17 00:00:00 2001 From: maxwell-jordan Date: Mon, 15 Jun 2026 17:57:59 -0700 Subject: [PATCH 1/5] PressTarget --- ui/press-target.mod/press-target.js | 301 +++++++++++++++++++++++++ ui/press-target.mod/press-target.mjson | 109 +++++++++ 2 files changed, 410 insertions(+) create mode 100644 ui/press-target.mod/press-target.js create mode 100644 ui/press-target.mod/press-target.mjson diff --git a/ui/press-target.mod/press-target.js b/ui/press-target.mod/press-target.js new file mode 100644 index 000000000..ce519c9e4 --- /dev/null +++ b/ui/press-target.mod/press-target.js @@ -0,0 +1,301 @@ + /*global require, exports*/ + +/** + * @module mod/ui/press-target + * @requires mod/ui/component + * @requires mod/composer/press-composer + * @requires collections/map + */ +var Component = require("../component").Component, + PressComposer = require("../../composer/press-composer").PressComposer, + Map = require("core/collections/map"); + +/** + * @class PressTarget + * @extends Component + */ +var PressTarget = exports.PressTarget = Component.specialize( /** @lends PressTarget.prototype # */ { + + /** + * Dispatched when the button is activated through a mouse click, finger + * tap, or when focused and the spacebar is pressed. + * + * @event PressTarget#action + * @property {Map} detail - The detail object as defined in {@link AbstractControl#detail} + */ + + /** + * Dispatched when the button is pressed for a period of time, set by + * {@link PressTarget#holdThreshold}. + * + * @event PressTarget#longAction + * @property {Map} detail - The detail object as defined in {@link PressTarget#detail} + */ + + /** + * @private + * @property {Map} value + * @default null + */ + _detail: { + value: null + }, + + /** + * The data property of the action event. + * + * @returns {Map} + */ + detail: { + get: function () { + if (this._detail === null || this._detail === undefined) { + this._detail = new Map(); + } + return this._detail; + } + }, + + /** + * Creates an action event with custom data. + * + * @function + * @returns {CustomEvent} + */ + createActionEvent: { + value: function () { + var actionEvent = document.createEvent("CustomEvent"), + eventDetail; + + eventDetail = this._detail; + actionEvent.initCustomEvent("action", true, true, eventDetail); + return actionEvent; + } + }, + + /** + * @function + * @fires PressTarget#action + */ + dispatchActionEvent: { + value: function () { + return this.dispatchEvent(this.createActionEvent()); + } + }, + + /** + * Convenience property to toggle enabled state. + */ + disabled: { + get: function () { + return !this.enabled; + }, + set: function (value) { + if (typeof value === "boolean") { + this.enabled = !value; + } + } + }, + + /** + * @constructs + */ + constructor: { + value: function PressTarget() { + this._pressComposer = new PressComposer(); + this.addComposer(this._pressComposer); + this._pressComposer.defineBinding("longPressThreshold", {"<-": "holdThreshold", source: this}); + + //classList management + this.defineBinding("classList.has('mod--disabled')", {"<-": "!enabled"}); + this.defineBinding("classList.has('mod--active')", {"<-": "active"}); + } + }, + + /** + * Enables or disables the Button from user input. When this property is + * set to `false`, the "mod--disabled" CSS style is applied to the + * button's DOM element during the next draw cycle. When set to `true` the + * "mod--disabled" CSS class is removed from the element's class + * list. + * @property {boolean} value + */ + enabled: { + value: true + }, + + /** + * @private + */ + _preventFocus: { + value: false + }, + + /** + * Specifies whether the button should receive focus or not. + * + * @property {boolean} + * @default false + */ + preventFocus: { + get: function () { + return this._preventFocus; + }, + set: function (value) { + this._preventFocus = !!value; + this.needsDraw = true; + } + }, + + acceptsActiveTarget: { + value: function () { + return ! this._preventFocus; + } + }, + + willBecomeActiveTarget: { + value: function (previousActiveTarget) { + + } + }, + + + + /** + * The amount of time in milliseconds the user must press and hold the + * button a `longAction` event is dispatched. The default is 1 second. + * @property {number} value + * @default 1000 + */ + holdThreshold: { + value: 1000 + }, + + /** + * @property {PressComposer} value + * @default null + * @private + */ + _pressComposer: { + value: null + }, + + /** + * @private + */ + _active: { + value: false + }, + + /** + * This property is true when the button is being interacted with, either + * through mouse click or touch event, otherwise false. + * + * @property {boolean} + * @default false + */ + active: { + get: function () { + return this._active; + }, + set: function (value) { + this._active = value; + this.needsDraw = true; + } + }, + + prepareForActivationEvents: { + value: function () { + this._pressComposer.addEventListener("pressStart", this, false); + this._pressComposer.addEventListener("press", this, false); + this._pressComposer.addEventListener("pressCancel", this, false); + } + }, + + // Optimisation + addEventListener: { + value: function (type, listener, useCapture) { + Component.prototype.addEventListener.call(this, type, listener, useCapture); + if (type === "longAction") { + this._pressComposer.addEventListener("longPress", this, false); + } + } + }, + + // Handlers + + /** + * Called when the user starts interacting with the component. + * + * @private + */ + handlePressStart: { + value: function (event) { + this.active = true; + + if (event.touch) { + // Prevent default on touchmove so that if we are inside a scroller, + // it scrolls and not the webpage + document.addEventListener("touchmove", this, false); + } + + if (!this._preventFocus) { + this._element.focus(); + } + } + }, + + /** + * Called when the user has interacted with the button. + * + * @private + */ + handlePress: { + value: function (event) { + this.active = false; + this.dispatchActionEvent(); + document.removeEventListener("touchmove", this, false); + } + }, + + handleKeyup: { + value: function (event) { + // action event on spacebar + if (event.keyCode === 32) { + this.active = false; + this.dispatchActionEvent(); + } + } + }, + + handleLongPress: { + value: function (event) { + // When we fire the "longAction" event we don't want to fire the + // "action" event as well. + this._pressComposer.cancelPress(); + + var longActionEvent = document.createEvent("CustomEvent"); + longActionEvent.initCustomEvent("longAction", true, true, null); + this.dispatchEvent(longActionEvent); + } + }, + + /** + * Called when all interaction is over. + * @private + */ + handlePressCancel: { + value: function (event) { + this.active = false; + document.removeEventListener("touchmove", this, false); + } + }, + + /** + * @private + */ + handleTouchmove: { + value: function (event) { + event.preventDefault(); + } + } +}); diff --git a/ui/press-target.mod/press-target.mjson b/ui/press-target.mod/press-target.mjson new file mode 100644 index 000000000..eae504651 --- /dev/null +++ b/ui/press-target.mod/press-target.mjson @@ -0,0 +1,109 @@ +{ + "objectDescriptor_pressTarget_enabled": { + "prototype": "core/meta/property-descriptor", + "values": { + "name": "enabled", + "objectDescriptor": { + "@": "root" + }, + "valueType": "boolean" + } + }, + "objectDescriptor_pressTarget_preventFocus": { + "prototype": "core/meta/property-descriptor", + "values": { + "name": "preventFocus", + "objectDescriptor": { + "@": "root" + }, + "valueType": "boolean" + } + }, + "objectDescriptor_pressTarget_holdThreshold": { + "prototype": "core/meta/property-descriptor", + "values": { + "name": "holdThreshold", + "objectDescriptor": { + "@": "root" + }, + "valueType": "number" + } + }, + "objectDescriptor_pressTarget_active": { + "prototype": "core/meta/property-descriptor", + "values": { + "name": "active", + "objectDescriptor": { + "@": "root" + }, + "valueType": "boolean" + } + }, + "objectDescriptor_pressTarget_detail": { + "prototype": "core/meta/property-descriptor", + "values": { + "name": "detail", + "objectDescriptor": { + "@": "root" + }, + "valueType": "object", + "readOnly": true, + "collectionValueType": "dict" + } + }, + "objectDescriptor_component_reference": { + "object": "ui/component.mjson" + }, + "root": { + "prototype": "core/meta/module-object-descriptor", + "values": { + "name": "PressTarget", + "parent": { + "@": "objectDescriptor_component_reference" + }, + "propertyDescriptors": [ + { + "@": "objectDescriptor_pressTarget_enabled" + }, + { + "@": "objectDescriptor_pressTarget_preventFocus" + }, + { + "@": "objectDescriptor_pressTarget_holdThreshold" + }, + { + "@": "objectDescriptor_pressTarget_active" + }, + { + "@": "objectDescriptor_pressTarget_detail" + } + ], + "propertyDescriptorGroups": { + "PressTarget": [ + { + "@": "objectDescriptor_pressTarget_enabled" + }, + { + "@": "objectDescriptor_pressTarget_preventFocus" + }, + { + "@": "objectDescriptor_pressTarget_holdThreshold" + }, + { + "@": "objectDescriptor_pressTarget_active" + }, + { + "@": "objectDescriptor_pressTarget_detail" + } + ] + }, + "objectDescriptorModule": { + "%": "ui/press-target.mod/press-target.mjson" + }, + "exportName": "PressTarget", + "module": { + "%": "ui/press-target.mod" + } + } + } +} From a9611de371bb1019c03ed0a2186c074a0f7fa32e Mon Sep 17 00:00:00 2001 From: maxwell-jordan Date: Thu, 18 Jun 2026 10:25:56 -0700 Subject: [PATCH 2/5] ActionTarget --- .../action-target.js} | 32 ++++----------- .../action-target.mjson} | 40 +++++++++---------- ui/button.mod/button.js | 3 +- 3 files changed, 29 insertions(+), 46 deletions(-) rename ui/{press-target.mod/press-target.js => action-target.mod/action-target.js} (86%) rename ui/{press-target.mod/press-target.mjson => action-target.mod/action-target.mjson} (65%) diff --git a/ui/press-target.mod/press-target.js b/ui/action-target.mod/action-target.js similarity index 86% rename from ui/press-target.mod/press-target.js rename to ui/action-target.mod/action-target.js index ce519c9e4..11e0d921b 100644 --- a/ui/press-target.mod/press-target.js +++ b/ui/action-target.mod/action-target.js @@ -1,36 +1,21 @@ /*global require, exports*/ /** - * @module mod/ui/press-target + * @module mod/ui/action-target * @requires mod/ui/component * @requires mod/composer/press-composer * @requires collections/map */ +const { Control } = require("ui/control"); var Component = require("../component").Component, PressComposer = require("../../composer/press-composer").PressComposer, Map = require("core/collections/map"); /** - * @class PressTarget - * @extends Component + * @class ActionTarget + * @extends Control */ -var PressTarget = exports.PressTarget = Component.specialize( /** @lends PressTarget.prototype # */ { - - /** - * Dispatched when the button is activated through a mouse click, finger - * tap, or when focused and the spacebar is pressed. - * - * @event PressTarget#action - * @property {Map} detail - The detail object as defined in {@link AbstractControl#detail} - */ - - /** - * Dispatched when the button is pressed for a period of time, set by - * {@link PressTarget#holdThreshold}. - * - * @event PressTarget#longAction - * @property {Map} detail - The detail object as defined in {@link PressTarget#detail} - */ +var ActionTarget = exports.ActionTarget = Control.specialize( /** @lends ActionTarget.prototype # */ { /** * @private @@ -74,7 +59,7 @@ var PressTarget = exports.PressTarget = Component.specialize( /** @lends PressTa /** * @function - * @fires PressTarget#action + * @fires ActionTarget#action */ dispatchActionEvent: { value: function () { @@ -100,14 +85,11 @@ var PressTarget = exports.PressTarget = Component.specialize( /** @lends PressTa * @constructs */ constructor: { - value: function PressTarget() { + value: function ActionTarget() { this._pressComposer = new PressComposer(); this.addComposer(this._pressComposer); this._pressComposer.defineBinding("longPressThreshold", {"<-": "holdThreshold", source: this}); - //classList management - this.defineBinding("classList.has('mod--disabled')", {"<-": "!enabled"}); - this.defineBinding("classList.has('mod--active')", {"<-": "active"}); } }, diff --git a/ui/press-target.mod/press-target.mjson b/ui/action-target.mod/action-target.mjson similarity index 65% rename from ui/press-target.mod/press-target.mjson rename to ui/action-target.mod/action-target.mjson index eae504651..b4c52fffe 100644 --- a/ui/press-target.mod/press-target.mjson +++ b/ui/action-target.mod/action-target.mjson @@ -1,5 +1,5 @@ { - "objectDescriptor_pressTarget_enabled": { + "objectDescriptor_actionTarget_enabled": { "prototype": "core/meta/property-descriptor", "values": { "name": "enabled", @@ -9,7 +9,7 @@ "valueType": "boolean" } }, - "objectDescriptor_pressTarget_preventFocus": { + "objectDescriptor_actionTarget_preventFocus": { "prototype": "core/meta/property-descriptor", "values": { "name": "preventFocus", @@ -19,7 +19,7 @@ "valueType": "boolean" } }, - "objectDescriptor_pressTarget_holdThreshold": { + "objectDescriptor_actionTarget_holdThreshold": { "prototype": "core/meta/property-descriptor", "values": { "name": "holdThreshold", @@ -29,7 +29,7 @@ "valueType": "number" } }, - "objectDescriptor_pressTarget_active": { + "objectDescriptor_actionTarget_active": { "prototype": "core/meta/property-descriptor", "values": { "name": "active", @@ -39,7 +39,7 @@ "valueType": "boolean" } }, - "objectDescriptor_pressTarget_detail": { + "objectDescriptor_actionTarget_detail": { "prototype": "core/meta/property-descriptor", "values": { "name": "detail", @@ -57,52 +57,52 @@ "root": { "prototype": "core/meta/module-object-descriptor", "values": { - "name": "PressTarget", + "name": "ActionTarget", "parent": { "@": "objectDescriptor_component_reference" }, "propertyDescriptors": [ { - "@": "objectDescriptor_pressTarget_enabled" + "@": "objectDescriptor_actionTarget_enabled" }, { - "@": "objectDescriptor_pressTarget_preventFocus" + "@": "objectDescriptor_actionTarget_preventFocus" }, { - "@": "objectDescriptor_pressTarget_holdThreshold" + "@": "objectDescriptor_actionTarget_holdThreshold" }, { - "@": "objectDescriptor_pressTarget_active" + "@": "objectDescriptor_actionTarget_active" }, { - "@": "objectDescriptor_pressTarget_detail" + "@": "objectDescriptor_actionTarget_detail" } ], "propertyDescriptorGroups": { - "PressTarget": [ + "ActionTarget": [ { - "@": "objectDescriptor_pressTarget_enabled" + "@": "objectDescriptor_actionTarget_enabled" }, { - "@": "objectDescriptor_pressTarget_preventFocus" + "@": "objectDescriptor_actionTarget_preventFocus" }, { - "@": "objectDescriptor_pressTarget_holdThreshold" + "@": "objectDescriptor_actionTarget_holdThreshold" }, { - "@": "objectDescriptor_pressTarget_active" + "@": "objectDescriptor_actionTarget_active" }, { - "@": "objectDescriptor_pressTarget_detail" + "@": "objectDescriptor_actionTarget_detail" } ] }, "objectDescriptorModule": { - "%": "ui/press-target.mod/press-target.mjson" + "%": "ui/action-target.mod/action-target.mjson" }, - "exportName": "PressTarget", + "exportName": "ActionTarget", "module": { - "%": "ui/press-target.mod" + "%": "ui/action-target.mod" } } } diff --git a/ui/button.mod/button.js b/ui/button.mod/button.js index 5f7a01549..10aa06895 100644 --- a/ui/button.mod/button.js +++ b/ui/button.mod/button.js @@ -6,6 +6,7 @@ const { PressComposer } = require("composer/press-composer"); const { KeyComposer } = require("composer/key-composer"); const { Control } = require("ui/control"); const { Montage } = require("core/core"); +const { ActionTarget } = require("ui/action-target.mod/action-target"); // TODO: migrate away from using undefinedGet and undefinedSet @@ -46,7 +47,7 @@ const { Montage } = require("core/core"); * } * */ -const Button = (exports.Button = class Button extends Control { +const Button = (exports.Button = class Button extends ActionTarget { /** @lends module:"mod/ui/native/button.mod".Button# */ // <---- Static ----> From ecda46fdaac2c7f6abdffcea496d456047d36e53 Mon Sep 17 00:00:00 2001 From: maxwell-jordan Date: Thu, 18 Jun 2026 17:11:50 -0700 Subject: [PATCH 3/5] parent update --- ui/button.mod/button.mjson | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ui/button.mod/button.mjson b/ui/button.mod/button.mjson index 4f61169e0..85ad3e06c 100644 --- a/ui/button.mod/button.mjson +++ b/ui/button.mod/button.mjson @@ -141,8 +141,8 @@ "helpKey": "" } }, - "objectDescriptor_control_reference": { - "object": "ui/control.mjson" + "objectDescriptor_action_target_reference": { + "object": "ui/action-target.mod/action-target.mjson" }, "root": { "prototype": "core/meta/module-object-descriptor", @@ -150,7 +150,7 @@ "name": "button", "customPrototype": false, "parent": { - "@": "objectDescriptor_control_reference" + "@": "objectDescriptor_action_target_reference" }, "propertyDescriptors": [ { From cdcacf6398dcd8d98060d84ed0b745155fcf566f Mon Sep 17 00:00:00 2001 From: maxwell-jordan Date: Fri, 26 Jun 2026 12:19:46 -0700 Subject: [PATCH 4/5] reduce redundancies with super --- ui/button.mod/button.js | 71 ++++++----------------------------------- 1 file changed, 9 insertions(+), 62 deletions(-) diff --git a/ui/button.mod/button.js b/ui/button.mod/button.js index 10aa06895..4c77e221c 100644 --- a/ui/button.mod/button.js +++ b/ui/button.mod/button.js @@ -4,7 +4,6 @@ const { VisualOrientation } = require("core/enums/visual-orientation"); const { VisualPosition } = require("core/enums/visual-position"); const { PressComposer } = require("composer/press-composer"); const { KeyComposer } = require("composer/key-composer"); -const { Control } = require("ui/control"); const { Montage } = require("core/core"); const { ActionTarget } = require("ui/action-target.mod/action-target"); @@ -14,7 +13,7 @@ const { ActionTarget } = require("ui/action-target.mod/action-target"); * Wraps a native <button> or <input[type="button"]> HTML element. * The element's standard attributes are exposed as bindable properties. * @class module:"mod/ui/native/button.mod".Button - * @extends module:mod/ui/control.Control + * @extends module:mod/ui/action-target.mod/action-target * @fires action * @fires hold * @@ -257,11 +256,11 @@ const Button = (exports.Button = class Button extends ActionTarget { * @default 1000 */ get holdThreshold() { - return this._pressComposer.longPressThreshold; + return super.holdThreshold; } set holdThreshold(value) { - this._pressComposer.longPressThreshold = value; + super.holdThreshold = value; } __pressComposer = null; @@ -301,27 +300,11 @@ const Button = (exports.Button = class Button extends ActionTarget { * @protected */ prepareForActivationEvents() { - this._pressComposer.addEventListener("pressStart", this, false); + super.prepareForActivationEvents(); this._spaceKeyComposer.addEventListener("keyPress", this, false); this._enterKeyComposer.addEventListener("keyPress", this, false); } - /** - * Override addEventListener for optimization - * @override - * @protected - * @param {String} type - The event type - * @param {Function} listener - The event listener - * @param {boolean} useCapture - The useCapture flag - */ - addEventListener(type, listener, useCapture) { - super.addEventListener(type, listener, useCapture); - - if (type === "longAction") { - this._pressComposer.addEventListener("longPress", this, false); - } - } - // <---- Event Handlers ----> /** @@ -346,8 +329,7 @@ const Button = (exports.Button = class Button extends ActionTarget { */ handlePressStart(mutableEvent) { if (!this._promise) { - this.active = true; - this._addEventListeners(); + super.handlePressStart(mutableEvent); } } @@ -359,9 +341,7 @@ const Button = (exports.Button = class Button extends ActionTarget { */ handlePress(mutableEvent) { if (!this._promise) { - this.active = false; - this.dispatchActionEvent(event.details); - this._removeEventListeners(); + super.handlePress(mutableEvent); } } @@ -373,16 +353,7 @@ const Button = (exports.Button = class Button extends ActionTarget { */ handleLongPress(mutableEvent) { if (!this._promise) { - // When we fire the "hold" event we don't want to fire the - // "action" event as well. - this._pressComposer.cancelPress(); - this._removeEventListeners(); - - const longActionEvent = document.createEvent("CustomEvent"); - - // FIXME: InitCustomEvent is deprecated - longActionEvent.initCustomEvent("longAction", true, true, null); - this.dispatchEvent(longActionEvent); + super.handleLongPress(mutableEvent); } } @@ -392,8 +363,7 @@ const Button = (exports.Button = class Button extends ActionTarget { * @param {MutableEvent} mutableEvent - The event object */ handlePressCancel(mutableEvent) { - this.active = false; - this._removeEventListeners(); + super.handlePressCancel(mutableEvent); } // <---- Life Cycle ----> @@ -436,30 +406,7 @@ const Button = (exports.Button = class Button extends ActionTarget { // <---- Private ----> - /** - * Adds event listeners to the button. - * @private - */ - _addEventListeners() { - this._pressComposer.addEventListener("press", this, false); - this._pressComposer.addEventListener("pressCancel", this, false); - - // FIXME: @benoit: we should maybe have a flag for this kind of event. - // can be tricky with the event delegation for example if we don't add it. - // same issue for: the pressComposer and the translate composer. - this._pressComposer.addEventListener("longPress", this, false); - } - - /** - * Removes event listeners from the button. - * @private - */ - _removeEventListeners() { - this._pressComposer.removeEventListener("press", this, false); - this._pressComposer.removeEventListener("pressCancel", this, false); - this._pressComposer.removeEventListener("longPress", this, false); - } - + /** * Applies the current image position's styling by updating CSS classes * @private From fd5a2a1d30eadf1e49fd72a8a8869b7a714c4b51 Mon Sep 17 00:00:00 2001 From: maxwell-jordan Date: Fri, 26 Jun 2026 15:32:34 -0700 Subject: [PATCH 5/5] migrated functionality upward --- ui/action-target.mod/action-target.js | 156 +++++++++++++++++- ui/button.mod/button.js | 222 -------------------------- 2 files changed, 153 insertions(+), 225 deletions(-) diff --git a/ui/action-target.mod/action-target.js b/ui/action-target.mod/action-target.js index 11e0d921b..7823d047f 100644 --- a/ui/action-target.mod/action-target.js +++ b/ui/action-target.mod/action-target.js @@ -8,6 +8,7 @@ */ const { Control } = require("ui/control"); var Component = require("../component").Component, + KeyComposer = require("../../composer/key-composer").KeyComposer, PressComposer = require("../../composer/press-composer").PressComposer, Map = require("core/collections/map"); @@ -81,15 +82,98 @@ var ActionTarget = exports.ActionTarget = Control.specialize( /** @lends ActionT } }, + debounceOptions: { + value: { + leading: true, + trailing: false + } + }, + + _debounceThreshold: { + value: 300 + }, + + _debounced: { + value: false + }, + + debounceThreshold: { + get: function () { + return this._debounceThreshold; + }, + set: function (value) { + this._debounceThreshold = Number(value); + + if (this._debounced) { + this.dispatchActionEvent = this.debounce( + this.dispatchActionEvent.bind(this), + this._debounceThreshold, + this.debounceOptions + ); + } + } + }, + + debounced: { + get: function () { + return this._debounced; + }, + set: function (value) { + this._debounced = Boolean(value); + + if (this._debounced) { + this.dispatchActionEvent = this.debounce( + this.dispatchActionEvent.bind(this), + this._debounceThreshold, + this.debounceOptions + ); + } else { + this.dispatchActionEvent = this.constructor.prototype.dispatchActionEvent; + } + } + }, + + _promise: { + value: undefined + }, + + promise: { + get: function () { + return this._promise; + }, + set: function (promise) { + var shouldClearPendingState, + currentTrackedPromise; + + if (this._promise === promise) { + return; + } + + shouldClearPendingState = !!this._promise; + this._promise = promise; + + if (promise) { + this.classList.add("mod--pending"); + currentTrackedPromise = promise; + + promise.finally(function () { + if (this._promise === currentTrackedPromise) { + this.classList.remove("mod--pending"); + this._promise = undefined; + } + }.bind(this)); + } else if (shouldClearPendingState) { + this.classList.remove("mod--pending"); + } + } + }, + /** * @constructs */ constructor: { value: function ActionTarget() { - this._pressComposer = new PressComposer(); - this.addComposer(this._pressComposer); this._pressComposer.defineBinding("longPressThreshold", {"<-": "holdThreshold", source: this}); - } }, @@ -157,10 +241,49 @@ var ActionTarget = exports.ActionTarget = Control.specialize( /** @lends ActionT * @default null * @private */ + __pressComposer: { + value: null + }, + _pressComposer: { + get: function () { + if (!this.__pressComposer) { + this.__pressComposer = new PressComposer(); + this.addComposer(this.__pressComposer); + } + + return this.__pressComposer; + } + }, + + __spaceKeyComposer: { + value: null + }, + + _spaceKeyComposer: { + get: function () { + if (!this.__spaceKeyComposer) { + this.__spaceKeyComposer = KeyComposer.createKey(this, "space", "space"); + } + + return this.__spaceKeyComposer; + } + }, + + __enterKeyComposer: { value: null }, + _enterKeyComposer: { + get: function () { + if (!this.__enterKeyComposer) { + this.__enterKeyComposer = KeyComposer.createKey(this, "enter", "enter"); + } + + return this.__enterKeyComposer; + } + }, + /** * @private */ @@ -190,6 +313,8 @@ var ActionTarget = exports.ActionTarget = Control.specialize( /** @lends ActionT this._pressComposer.addEventListener("pressStart", this, false); this._pressComposer.addEventListener("press", this, false); this._pressComposer.addEventListener("pressCancel", this, false); + this._spaceKeyComposer.addEventListener("keyPress", this, false); + this._enterKeyComposer.addEventListener("keyPress", this, false); } }, @@ -212,6 +337,10 @@ var ActionTarget = exports.ActionTarget = Control.specialize( /** @lends ActionT */ handlePressStart: { value: function (event) { + if (this._promise) { + return; + } + this.active = true; if (event.touch) { @@ -233,12 +362,29 @@ var ActionTarget = exports.ActionTarget = Control.specialize( /** @lends ActionT */ handlePress: { value: function (event) { + if (this._promise) { + return; + } + this.active = false; this.dispatchActionEvent(); document.removeEventListener("touchmove", this, false); } }, + handleKeyPress: { + value: function (event) { + if (this._promise) { + return; + } + + if (event.identifier === "space" || event.identifier === "enter") { + this.active = false; + this.dispatchActionEvent(); + } + } + }, + handleKeyup: { value: function (event) { // action event on spacebar @@ -251,6 +397,10 @@ var ActionTarget = exports.ActionTarget = Control.specialize( /** @lends ActionT handleLongPress: { value: function (event) { + if (this._promise) { + return; + } + // When we fire the "longAction" event we don't want to fire the // "action" event as well. this._pressComposer.cancelPress(); diff --git a/ui/button.mod/button.js b/ui/button.mod/button.js index 4c77e221c..997f2a3de 100644 --- a/ui/button.mod/button.js +++ b/ui/button.mod/button.js @@ -2,9 +2,6 @@ const { VisualOrientation } = require("core/enums/visual-orientation"); const { VisualPosition } = require("core/enums/visual-position"); -const { PressComposer } = require("composer/press-composer"); -const { KeyComposer } = require("composer/key-composer"); -const { Montage } = require("core/core"); const { ActionTarget } = require("ui/action-target.mod/action-target"); // TODO: migrate away from using undefinedGet and undefinedSet @@ -57,72 +54,6 @@ const Button = (exports.Button = class Button extends ActionTarget { // <---- Properties ----> - static { - /** - * Options for debouncing the action event. - * Immediately invokes the function and then ignores any calls made - * within the threshold period. - * @type {Object} - * @default { leading: true, trailing: false } - */ - Montage.defineProperties(this.prototype, { - debounceOptions: { - value: { - leading: true, - trailing: false, - }, - }, - - _debounceThreshold: { value: 300 }, // milliseconds - - _debounced: { value: false }, - }); - } - - get debounceThreshold() { - return this._debounceThreshold; - } - - /** - * The debounce threshold in milliseconds. - * @type {number} - * @default 300 - */ - set debounceThreshold(value) { - this._debounceThreshold = Number(value); - - if (this._debounced) { - this.dispatchActionEvent = this.debounce( - this.dispatchActionEvent.bind(this), - this._debounceThreshold, - this.debounceOptions - ); - } - } - - get debounced() { - return this._debounced; - } - - /** - * Indicates whether the action event is debounced. - * @type {boolean} - * @default false - */ - set debounced(value) { - this._debounced = Boolean(value); - - if (this._debounced) { - this.dispatchActionEvent = this.debounce( - this.dispatchActionEvent.bind(this), - this._debounceThreshold, - this.debounceOptions - ); - } else { - this.dispatchActionEvent = Button.prototype.dispatchActionEvent; - } - } - _visualPosition = VisualPosition.start; get visualPosition() { @@ -213,159 +144,6 @@ const Button = (exports.Button = class Button extends ActionTarget { } } - _promise = undefined; - - get promise() { - return this._promise; - } - - set promise(promise) { - // Only proceed if the new promise is different from the current one - if (this._promise === promise) return; - - const shouldClearPendingState = !!this._promise; - this._promise = promise; - - if (promise) { - // Set up pending state when promise is set - this.classList.add("mod--pending"); - - // Store reference to track this specific promise - const currentTrackedPromise = promise; - - // Clear state when promise resolves/rejects - // TODO: we should propably add an error state?... - promise.finally(() => { - // Only clear if this is still the active promise - if (this._promise === currentTrackedPromise) { - this.classList.remove("mod--pending"); - this._promise = undefined; - } - }); - } else if (shouldClearPendingState) { - // Clear pending state when the current promise is set to null - this.classList.remove("mod--pending"); - } - } - - /** - * The amount of time in milliseconds the user must press and hold - * the button a hold event is dispatched. - * The default is 1 second. - * @type {number} - * @default 1000 - */ - get holdThreshold() { - return super.holdThreshold; - } - - set holdThreshold(value) { - super.holdThreshold = value; - } - - __pressComposer = null; - - get _pressComposer() { - if (!this.__pressComposer) { - this.__pressComposer = new PressComposer(); - this.addComposer(this.__pressComposer); - } - - return this.__pressComposer; - } - - __spaceKeyComposer = null; - - get _spaceKeyComposer() { - if (!this.__spaceKeyComposer) { - this.__spaceKeyComposer = KeyComposer.createKey(this, "space", "space"); - } - - return this.__spaceKeyComposer; - } - - __enterKeyComposer = null; - - get _enterKeyComposer() { - if (!this.__enterKeyComposer) { - this.__enterKeyComposer = KeyComposer.createKey(this, "enter", "enter"); - } - - return this.__enterKeyComposer; - } - - /** - * Prepares the component for activation events. - * @override - * @protected - */ - prepareForActivationEvents() { - super.prepareForActivationEvents(); - this._spaceKeyComposer.addEventListener("keyPress", this, false); - this._enterKeyComposer.addEventListener("keyPress", this, false); - } - - // <---- Event Handlers ----> - - /** - * Dispatching the action event on spacebar & enter when the button is focused. - * @param {MutableEvent} mutableEvent - The event object - * @protected - * @fires action - */ - handleKeyPress(mutableEvent) { - const { identifier } = mutableEvent; - - if (identifier === "space" || identifier === "enter") { - this.active = false; - this.dispatchActionEvent(); - } - } - - /** - * Called when the user starts interacting with the component. - * @protected - * @param {MutableEvent} mutableEvent - The event object - */ - handlePressStart(mutableEvent) { - if (!this._promise) { - super.handlePressStart(mutableEvent); - } - } - - /** - * Called when the user has interacted with the button. - * @protected - * @param {MutableEvent} mutableEvent - The event object - * @fires action - */ - handlePress(mutableEvent) { - if (!this._promise) { - super.handlePress(mutableEvent); - } - } - - /** - * Called when the user has interacted with the button for a long time. - * @protected - * @param {MutableEvent} mutableEvent - The event object - * @fires longAction - */ - handleLongPress(mutableEvent) { - if (!this._promise) { - super.handleLongPress(mutableEvent); - } - } - - /** - * Called when all interaction is over. - * @protected - * @param {MutableEvent} mutableEvent - The event object - */ - handlePressCancel(mutableEvent) { - super.handlePressCancel(mutableEvent); - } - // <---- Life Cycle ----> enterDocument(firstDraw) {