diff --git a/docs/nodes/widgets/ui-button.md b/docs/nodes/widgets/ui-button.md
index d2ac4b1e2..59d7bffb0 100644
--- a/docs/nodes/widgets/ui-button.md
+++ b/docs/nodes/widgets/ui-button.md
@@ -21,6 +21,12 @@ props:
Icon Color:
description: Icon color. If not provided, will have the same color as text / label.
dynamic: true
+ Enable pointerup event:
+ description: Enables the capturing of pointerup events on the button
+ dynamic: false
+ Enable pointerdown event:
+ description: Enables the capturing of pointerdown events on the button
+ dynamic: false
Emulate Button Click: If enabled, any received message will trigger a button click, emitting the relevant payload and topic.
controls:
enabled:
diff --git a/nodes/widgets/locales/en-US/ui_button.html b/nodes/widgets/locales/en-US/ui_button.html
index 27f6a5cd9..ce6f42bde 100644
--- a/nodes/widgets/locales/en-US/ui_button.html
+++ b/nodes/widgets/locales/en-US/ui_button.html
@@ -22,6 +22,10 @@
Properties
If "Icon" is defined, this property controls which side of the "Label" the icon will render on
Label string
The text shown within the button. If not provided, then the button will only render the icon. It supports HTML content
+ Enable pointerdown event bool
+ If enabled, the button will emit a message on pointerdown event with the given payload
+ Enable pointerup event bool
+ If enabled, the button will emit a message on pointerup event with the given payload
Emulate Button Click bool
If enabled, any received message will trigger a button click, emitting the relevant payload and topic
Background Colorstring
diff --git a/nodes/widgets/locales/en-US/ui_button.json b/nodes/widgets/locales/en-US/ui_button.json
index 5e3442eea..12295fc72 100644
--- a/nodes/widgets/locales/en-US/ui_button.json
+++ b/nodes/widgets/locales/en-US/ui_button.json
@@ -23,6 +23,8 @@
"payload": "Payload",
"topic": "Topic",
"emulateClick": "If msg arrives on input, emulate a button click:",
+ "enablePointerup": "Enable pointerup event",
+ "enablePointerdown": "Enable pointerdown event",
"buttonColor": "Background",
"optionalButtonColor": "(Optional) e.g. 'green'/'#a5a5a5'",
"textColor": "Text",
diff --git a/nodes/widgets/ui_button.html b/nodes/widgets/ui_button.html
index 898e94c4f..01673945b 100644
--- a/nodes/widgets/ui_button.html
+++ b/nodes/widgets/ui_button.html
@@ -36,6 +36,16 @@
topicType: { value: 'msg' },
buttonColor: { value: '' },
textColor: { value: '' },
+ iconColor: { value: '' },
+
+ // pointerup/down event support
+ enablePointerdown: { value: false },
+ pointerdownPayload: { value: '', validate: (hasProperty(RED.validators, 'typedInput') ? RED.validators.typedInput('pointerdownPayloadType') : function (v) { return true }) },
+ pointerdownPayloadType: { value: 'str' },
+ enablePointerup: { value: false },
+ pointerupPayload: { value: '', validate: (hasProperty(RED.validators, 'typedInput') ? RED.validators.typedInput('pointerupPayloadType') : function (v) { return true }) },
+ pointerupPayloadType: { value: 'str' },
+
iconColor: { value: '' }
},
inputs: 1,
@@ -83,6 +93,34 @@
types: ['str', 'msg', 'flow', 'global']
})
+ $('#node-input-enablePointerdown').on('change', function () {
+ if (this.checked) {
+ $('.form-row-pointerdown').show()
+ } else {
+ $('.form-row-pointerdown').hide()
+ }
+ })
+
+ $('#node-input-pointerdownPayload').typedInput({
+ default: 'str',
+ typeField: $('#node-input-pointerdownPayloadType'),
+ types: ['str', 'num', 'bool', 'json', 'bin', 'date', 'flow', 'global']
+ });
+
+ $('#node-input-enablePointerup').on('change', function () {
+ if (this.checked) {
+ $('.form-row-pointerup').show()
+ } else {
+ $('.form-row-pointerup').hide()
+ }
+ })
+
+ $('#node-input-pointerupPayload').typedInput({
+ default: 'str',
+ typeField: $('#node-input-pointerdownPayloadType'),
+ types: ['str', 'num', 'bool', 'json', 'bin', 'date', 'flow', 'global']
+ });
+
if (!this.iconPosition) {
$('#node-input-iconPosition').val('left')
}
@@ -151,6 +189,24 @@
-->
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/nodes/widgets/ui_button.js b/nodes/widgets/ui_button.js
index 7e99a6af0..9bb477bc0 100644
--- a/nodes/widgets/ui_button.js
+++ b/nodes/widgets/ui_button.js
@@ -12,10 +12,33 @@ module.exports = function (RED) {
const beforeSend = async function (msg) {
let error = null
+ let payload = null
+ let payloadType = null
- // retrieve the payload we're sending from this button
- let payloadType = config.payloadType
- let payload = config.payload
+ msg._event = msg._event || { type: 'inject' }
+
+ switch (msg._event.type) {
+ case 'pointerup':
+ payload = config.pointerupPayload
+ payloadType = config.pointerupPayloadType
+ break
+ case 'pointerdown':
+ payload = config.pointerdownPayload
+ payloadType = config.pointerdownPayloadType
+ break
+ case 'click':
+ payload = config.payload
+ payloadType = config.payloadType
+ break
+ case 'inject':
+ payload = config.payload
+ payloadType = config.payloadType
+ break
+ default:
+ payload = config.payload
+ payloadType = config.payloadType
+ break
+ }
if (payloadType === 'flow' || payloadType === 'global') {
try {
@@ -43,7 +66,6 @@ module.exports = function (RED) {
}
}
}
-
msg.payload = payload
const updates = msg.ui_update
diff --git a/ui/src/stylesheets/common.css b/ui/src/stylesheets/common.css
index e311feee9..19ea89801 100644
--- a/ui/src/stylesheets/common.css
+++ b/ui/src/stylesheets/common.css
@@ -481,5 +481,6 @@ NB! supresses the gridarea for messages, but those are not in use*/
.v-btn.v-btn--density-default,
.v-btn.v-btn--density-compact,
.v-btn.v-btn--density-comfortable {
+ height: auto;
min-height: var(--widget-row-height);
}
\ No newline at end of file
diff --git a/ui/src/widgets/ui-button/UIButton.vue b/ui/src/widgets/ui-button/UIButton.vue
index 7d413e2a5..ad9f0433b 100644
--- a/ui/src/widgets/ui-button/UIButton.vue
+++ b/ui/src/widgets/ui-button/UIButton.vue
@@ -3,6 +3,9 @@
block variant="flat" :disabled="!state.enabled" :prepend-icon="prependIcon" :append-icon="appendIcon"
:class="{ 'nrdb-ui-button--icon': iconOnly }" :color="buttonColor" :style="{ 'min-width': iconOnly ?? 'auto' }"
@click="action"
+ @pointerdown="pointerdown"
+ @pointerup="pointerup"
+ @pointermove="checkIsPointerOverButton"
>
@@ -26,6 +29,11 @@ export default {
props: { type: Object, default: () => ({}) },
state: { type: Object, default: () => ({}) }
},
+ data () {
+ return {
+ isPointerOverButton: true // Tracks if the pointer is still over the button
+ }
+ },
computed: {
...mapState('data', ['messages']),
prependIcon () {
@@ -62,6 +70,23 @@ export default {
},
methods: {
action ($evt) {
+ if (!this.isPointerOverButton) {
+ return
+ }
+ const evt = {
+ type: $evt.type,
+ clientX: $evt.clientX,
+ clientY: $evt.clientY,
+ bbox: $evt.target.getBoundingClientRect()
+ }
+ const msg = this.messages[this.id] || {}
+ msg._event = evt
+ this.$socket.emit('widget-action', this.id, msg)
+ },
+ pointerdown: function ($evt) {
+ if (!this.props.enablePointerdown) {
+ return
+ }
const evt = {
type: $evt.type,
clientX: $evt.clientX,
@@ -70,8 +95,35 @@ export default {
}
const msg = this.messages[this.id] || {}
msg._event = evt
+ $evt.target.setPointerCapture($evt.pointerId)
this.$socket.emit('widget-action', this.id, msg)
},
+ pointerup: function ($evt) {
+ if (!this.props.enablePointerup) {
+ return
+ }
+ const evt = {
+ type: $evt.type,
+ clientX: $evt.clientX,
+ clientY: $evt.clientY,
+ bbox: $evt.target.getBoundingClientRect()
+ }
+ const msg = this.messages[this.id] || {}
+ msg._event = evt
+ $evt.target.releasePointerCapture($evt.pointerId)
+ this.$socket.emit('widget-action', this.id, msg)
+ },
+ checkIsPointerOverButton: function ($evt) {
+ // Check if pointer is still over the button
+ const buttonRect = $evt.target.getBoundingClientRect()
+ this.isPointerOverButton = (
+ $evt.clientX >= buttonRect.left &&
+ $evt.clientX <= buttonRect.right &&
+ $evt.clientY >= buttonRect.top &&
+ $evt.clientY <= buttonRect.bottom
+ )
+ },
+
makeMdiIcon (icon) {
return 'mdi-' + icon.replace(/^mdi-/, '')
},