/** A zero dependency button web component that sends a msg or a document event when clicked.
* Contains relevant data from data-*, topic and payload attributes (or properties),
* includes a _meta object showing whether any modifier keys were used, the element id/name.
*
* See ./docs/button-send.md for detailed documentation on installation and use.
*
* Version: See the class code
*
**/
/** Copyright (c) 2022-2024 Julian Knight (Totally Information)
* https://it.knightnet.org.uk, https://github.com/TotallyInformation
*
* Licensed under the Apache License, Version 2.0 (the 'License');
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an 'AS IS' BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
/** ToDo:
* - Add variants (see simple-card)
* - Allow colour and background colour to be controlled with standard names (e.g. info, warning, error, etc)
* - Create a unique identifier to use when id/name not specified.
* - Allow std pre-formatted msg from uibuilder to change the attribs/props
* - Allow std pre-formatted msg from uibuilder to add a new button
* - Consider moving from `click` to `pointerdown`/`up`. taking note of `setPointerCapture` to detect when up is outside of the down
* target so it can be ignored if desired.
* - Also add processing for multi-click (detail property of click), contextMenu, auxclick, dblclick
*/
import TiBaseComponent from '../libs/ti-base-component'
/** Only use a template if you want to isolate the code and CSS */
const template = document.createElement('template')
template.innerHTML = /*html*/`
<style>
button, input[type="button" i] {
margin-top: .5em;
margin-bottom: .5em;
display: inline-flex;
align-items: center;
justify-content: center;
border: none;
padding: .5rem 1rem;
text-decoration: none;
background-color: var(--info);
color: var(--text2);
font-family: inherit;
font-size: 1rem;
line-height: 1.4;
cursor: pointer;
text-align: center;
transition: background 250ms ease-in-out, transform 150ms ease;
-webkit-appearance: none;
-moz-appearance: none;
border-radius: 8px;
/* box-shadow: 0 3px 5px rgb(var(--uib-color-fg), 0.5); */
box-shadow: inset 2px 2px 3px rgba(255,255,255, .3),
inset -2px -2px 3px rgba(0,0,0, .3);
}
button:hover, input[type="button" i]:hover {
background-color: rgb(var(--uib-color-info), .5);
}
button:focus, input[type="button" i]:focus {
outline: 1px solid rgb(var(--uib-color-fg));
outline-offset: -4px;
}
button:active, input[type="button" i]:active {
transform: scale(0.97);
}
</style>
<button type="button">
<span><slot></slot></span>
</button>
`
/** Namespace
* @namespace Beta
*/
/** A Zero dependency button web component that sends a msg or a document event when clicked.
* Contains relevant data from data-*, topic and payload attributes (or properties),
* includes a _meta object showing whether any modifier keys were used, the element id/name
*
* @class
* @augments TiBaseComponent
* @description Define a new zero dependency custom web component ECMA module that can be used as an HTML tag
*
* @element button-send
* @memberOf Beta
* METHODS FROM BASE:
* @method config Update runtime configuration, return complete config
* @method createShadowSelectors Creates the jQuery-like $ and $$ methods
* @method deepAssign Object deep merger
* @method doInheritStyles If requested, add link to an external style sheet
* @method ensureId Adds a unique ID to the tag if no ID defined.
* @method _uibMsgHandler Not yet in use
* @method _event(name,data) Standardised custom event dispatcher
* OTHER METHODS:
* None
* @fires button-send:connected - When an instance of the component is attached to the DOM. `evt.details` contains the details of the element.
* @fires button-send:ready - Alias for connected. The instance can handle property & attribute changes
* @fires button-send:disconnected - When an instance of the component is removed from the DOM. `evt.details` contains the details of the element.
* @fires button-send:attribChanged - When a watched attribute changes. `evt.details` contains the details of the change.
* @fires button-send:click - Document object event. evt.details contains the data
* @fires uibuilder.send {function} - Sends a msg back to Node-RED if uibuilder available. topic, payload and _meta props may all be set.
* NOTE that listeners can be attached either to the `document` or to the specific element instance.
* Standard watched attributes (common across all my components):
* @property {string|boolean} inherit-style - Optional. Load external styles into component (only useful if using template). If present but empty, will default to './index.css'. Optionally give a URL to load.
* @property {string} name - Optional. HTML name attribute. Included in output _meta prop.
* Other watched attributes:
* @property {string} topic - Optional. Topic string to use. Mostly for node-red messages
* @property {string} payload - Optional. Payload string. Mostly for node-red messages. For non-string payload, see props below
* Standard props (common across all my components):
* @property {boolean} uib True if UIBUILDER for Node-RED is loaded. In base class
* @property {function(string): Element} $ jQuery-like shadow dom selector. In base class
* @property {function(string): NodeList} $$ jQuery-like shadow dom multi-selector. In base class
* @property {number} _iCount The component version string (date updated). In base class
* @property {object} opts This components controllable options - get/set using the `config()` method. In base class
*
* @property {string} version Static. The component version string (date updated). Also has a getter that returns component and base version strings.
* Other props:
* @property {any|string} payload - Can be an attribute or property. If used as property, must not use payload attribute in html, aAllows any data to be attached to payload. As an attribute, allows a string only.
* By default, all attributes are also created as properties
* @slot default - Button label. Allows text, inline and most block tags to be included (unlike the standard button tag which only allows inline tags).
* @csspart button - Uses the uib-styles.css uibuilder master for variables where available.
* See https://github.com/runem/web-component-analyzer?tab=readme-ov-file#-how-to-document-your-components-using-jsdoc
*/
class ButtonSend extends TiBaseComponent {
/** Component version */
static componentVersion = '2024-10-06'
sendEvents = true
/** The topic to include in the output
* @type {string|undefined} */
topic
/** The payload to include in the output
* @type {any} */
payload
/** Standard _ui object to include in msgs */
_ui = {
type: 'button-send',
event: undefined,
id: undefined,
name: undefined,
// data: undefined, // All of the data-* attributes as an object
}
/** The output msg @type {object} */
_msg = {}
/** Makes HTML attribute change watched
* @returns {Array<string>} List of all of the html attribs (props) listened to
*/
static get observedAttributes() {
return [
// Standard watched attributes:
'inherit-style', 'name',
// Other watched attributes:
'topic', 'payload',
]
}
/** NB: Attributes not available here - use connectedCallback to reference */
constructor() {
super()
// Only attach the shadow dom if code and style isolation is needed - comment out if shadow dom not required
this._construct(template.content.cloneNode(true))
// const mydata = { ...this.dataset }
/** The output msg @type {object} */
this._setMsg('component load')
// if ( this.uib && this.sendEvents ) window.uibuilder.send({_ui: {...this._ui}})
} // --- end of constructor --- //
/** Runs when an instance is added to the DOM */
connectedCallback() {
this._connect() // Keep at start.
/** Listen for the button click */
this.addEventListener('click', this.handleClick)
/** Instance registration event @type {CustomEvent} */
// this._setMsg('instance load')
// document.dispatchEvent( new CustomEvent('button-send:instanceAdded', {'detail': this._msg._ui}) )
// if ( window.uibuilder && this.sendEvents ) window.uibuilder.send({_ui: {...this._ui}})
this._ready() // Keep at end. Let everyone know that a new instance of the component has been connected & is ready
}
/** Runs when an instance is removed from the DOM */
disconnectedCallback() {
this.removeEventListener('click', this.handleClick)
this._disconnect() // Keep at end.
}
/** Runs when an observed attribute changes - Note: values are always strings
* @param {string} attrib Name of watched attribute that has changed
* @param {string} oldVal The previous attribute value
* @param {string} newVal The new attribute value
*/
attributeChangedCallback(attrib, oldVal, newVal) {
/** Optionally ignore attrib changes until instance is fully connected
* Otherwise this can fire BEFORE everthing is fully connected.
*/
// if (!this.connected) return
// Don't bother if the new value same as old
if ( oldVal === newVal ) return
// Create a property from the value - WARN: Be careful with name clashes
this[attrib] = newVal
// Add other dynamic attribute processing here.
// If attribute processing doesn't need to be dynamic, process in connectedCallback as that happens earlier in the lifecycle
this._setMsg('attribute change')
if ( window['uibuilder'] && this.sendEvents ) { window['uibuilder'].send( {
payload: { name: attrib, oldVal: oldVal, newVal: newVal },
_ui: { ...this._ui }
} ) }
// Keep at end. Let everyone know that an attribute has changed for this instance of the component
this._event('attribChanged', { attribute: attrib, newVal: newVal, oldVal: oldVal })
}
_setMsg(evtName) {
const mydata = { ...this.dataset }
this._msg.topic = this.topic
this._msg.payload = this.payload ? this.payload : mydata
this._msg._ui = { ...this._ui }
if (evtName) this._msg._ui.event = evtName
if ( this.id !== '') this._msg._ui.id = this.id
const n = this.getAttribute('name')
if ( n !== null ) this._msg._ui.name = n
// this._msg._ui.data = mydata // All of the data-* attributes as an object
}
/** fn to run when the button is clicked
* @param {PointerEvent} evt The event object
*/
handleClick(evt) {
const uibLib = window?.['uibuilder']
evt.preventDefault()
this._setMsg('click')
const _ui = this._msg._ui
const target = /** @type {Element} */ (evt.currentTarget)
// Get target properties - only shows custom props not element default ones
const props = {}
const ignoreProps = ['name', 'sendEvents', 'payload', '$', '_ui', '_msg']
Object.keys(target).forEach( key => {
if ( !ignoreProps.includes(key) ) props[key] = target[key]
})
const ignoreAttribs = ['class', 'id', 'name']
const attribs = Object.assign({},
...Array.from(target.attributes,
( { name, value } ) => {
if ( !ignoreAttribs.includes(name) ) {
return ({ [name]: value })
}
return undefined
}
)
)
_ui.slotText = target.textContent !== '' ? target.textContent?.substring(0, 255) : undefined
_ui.props = props
_ui.attribs = attribs
// @ts-ignore
_ui.dataset = {...target.dataset}
_ui.classes = Array.from(target.classList)
_ui.altKey = evt.altKey
_ui.ctrlKey = evt.ctrlKey
_ui.shiftKey = evt.shiftKey
_ui.metaKey = evt.metaKey
_ui.pointerType = evt.pointerType
_ui.nodeName = target.nodeName
if ( uibLib ) {
_ui.clientId = uibLib.clientId
_ui.pageName = uibLib.pageName
_ui.tabId = uibLib.tabId
}
/** Output a custom document event `button-send:click`, data is in evt.details */
document.dispatchEvent( new CustomEvent(`${this.localName}:click`, {
bubbles: true,
composed: true,
'detail': this._msg
}) )
/** Send a message to uibuilder with the output data */
if (uibLib) uibLib.send(this._msg)
// if (uibLib) uibLib.eventSend(evt)
// else console.debug('[ButtonSend:handleClick] uibuilder not available, cannot send')
}
} // ---- End of class definition ---- //
// Make the class the default export so it can be used elsewhere
export default ButtonSend
/** Self register the class to global
* Enables new data lists to be dynamically added via JS
* and lets the static methods be called
*/
window['ButtonSend'] = ButtonSend
// Add the class as a new Custom Element to the window object
customElements.define('button-send', ButtonSend)