(function() {
/**
* @typedef {Object} Help4.control2.Select.Data
* @property {string} id - id
* @property {string} text - text
* @property {string} [icon] - icon -> not yet supported
* @property {string} [css] - CSS class
*/
/**
* @typedef {Help4.control2.Control.Params} Help4.control2.Select.Params
* @property {Array<Help4.control2.Select.Data>} - [data = []] - data that can be selected
* @property {?string} - value - selected value
* @property {?string} [placeholder = ''] - placeholder text
* @property {?syncOpen} [syncOpen = true] - flag to sync open
*/
/**
* A control to offer selection to users
* @augments Help4.control2.Control
* @property {Help4.control2.Select.Data[]} data - data that can be selected
* @property {?string} value - selected value
* @property {?string} placeholder - placeholder text
* @property {?boolean} syncOpen - flag to sync open
* @property {?Help4.control2.bubble.Bubble} _bubble - bubble control
* @property {?Help4.control2.button.Button} _button
* @property {Object} _outsideClick - outside click event handler
*/
Help4.control2.Select = class extends Help4.control2.Control {
/**
* @override
* @param {Help4.control2.Select.Params} [params]
* @param {Help4.jscore.ControlBase.Params} [derived]
*/
constructor(params, derived) {
const {jscore: {ControlBase}, Localization} = Help4;
const T = ControlBase.TYPES;
const TT = ControlBase.TEXT_TYPES;
super(params, {
params: {
data: {type: T.array, mandatory: true},
value: {type: T.string_null, init: null},
placeholder: {type: T.string, init: Localization.getText('placeholder.select'), readonly: true},
syncOpen: {type: T.boolean, init: true, readonly: true},
},
config: {
css: 'control-select'
},
statics: {
_bubble: {},
_button: {},
_outsideClick: {destroy: false}
},
texts: {
text: TT.innerText
},
derived
});
}
/**
* @override
* @returns {Help4.control2.Select}
*/
focus() {
this.getDom('-field').focus();
return this;
}
/** @override */
_onBeforeDestroy() {
const dom = this.getDom('-val');
if (dom) dom.onclick = null;
if (this._outsideClick) {
_observeOutsideClick.call(this, false);
this._outsideClick = null;
}
super._onBeforeDestroy();
}
/**
* @override
* @param {HTMLElement} dom - control DOM
*/
_onDomCreated(dom) {
super._onDomCreated(dom);
const {control2: {button: {APPEARANCES, Button}}} = Help4;
const {ariaLabel, placeholder, id, syncOpen, value} = this;
const open = () => {
if (this._bubble) {
_closeBubble.call(this);
} else {
this._fireEvent({type: 'beforeopen'});
if (syncOpen) _openBubble.call(this);
}
}
const labelClick = (event) => {
// triggers outside click event closing the list. so stop it from bubbling up
event.stopPropagation();
this._fireEvent({type: 'labelclick'});
open();
}
const onHotkey = (event) => { // toggle hotkeys on enter or space/or
const key = Help4.service.HotkeyService.getKey(event);
key === 'enter' && labelClick(event);
}
const wrapper = this._createElement('div', {
id: '-field',
css: 'field',
tabIndex: 0, // only the wrapper is focusable
role: 'combobox',
ariaExpanded: false,
ariaHaspopup: 'listbox',
onclick: labelClick,
ariaLabel
});
wrapper.onkeydown = onHotkey;
this._createElement('span', {
id: '-val',
css: 'select inner',
dom: wrapper,
text: value || placeholder,
tabIndex: -1
});
this._button = /** @type {Help4.control2.button.Button} */ this._createControl(Button, {
id: `${id}-btn`,
dom: wrapper,
appearance: APPEARANCES.input,
icon: 'down',
css: 'icon input',
tabIndex: -1
});
// wrapper click is handling the click for button
}
/**
* @override
* @param {Help4.jscore.ControlBase.PropertyChangeEvent} event - the change event
*/
_applyPropertyToDom({name, value, oldValue}) {
if (name !== 'value') {
return super._applyPropertyToDom({name, value, oldValue});
}
const {Element} = Help4;
const {data, placeholder} = this;
const text = data.find(({id}) => id === value)?.text;
const inner = this.getDom('-val');
text
? Element.removeClass(inner, 'placeholder')
: Element.addClass(inner, 'placeholder');
inner.innerHTML = text || placeholder;
}
}
/**
* @memberof Help4.control2.Select#
* @private
*/
function _closeBubble() {
if (!this._bubble) return;
_observeOutsideClick.call(this, false);
const {Element} = Help4;
this._button.active = false;
this._destroyControl('_bubble');
Element.removeClass(this.getDom(), 'open');
Element.setAttribute(this.getDom('-field'), {ariaExpanded: false});
this.focus();
}
/**
* @memberof Help4.control2.Select#
* @returns {Object[]}
* @private
*/
function _getBubbleData() {
const {data, id, value} = this;
const bubbleId = `${id}-bub-`;
const bubbleData = [];
for (const [index, item] of data.entries()) {
const {id, css} = item;
const iCss = css ? css + ' ' : '';
bubbleData.push(Help4.extendObject({
_metadata: {id},
controlType: 'button.Button',
id: bubbleId + index,
css: iCss + 'option',
role: 'option'
}, item, false));
}
return bubbleData;
}
/**
* @memberof Help4.control2.Select#
* @private
*/
function _openBubble() {
if (this._bubble) return;
const {id, value, rtl, mobile, language, _button} = this;
const data = _getBubbleData.call(this);
const controller = Help4.getController();
const bubble = this._bubble = controller.getService('bubble').add({
id: `${id}-bub`,
css: 'select',
controlType: 'bubble.Container',
containerType: 'container.Select',
dom: controller.getDom(),
size: 'xs',
appearance: 'select',
data,
value: value,
visible: true,
tabIndex: -1,
rtl,
mobile,
autoFocus: true,
language,
onselect: _onBubbleClick.bind(this)
}, 'control');
const dom = this.getDom();
const align = () => { // XRAY-1670: XRAY-3520
if (!this.isDestroyed() && bubble && !bubble.isDestroyed()) {
bubble.applySelectMaxHeight(dom.getBoundingClientRect());
setTimeout(align, 100);
}
};
align();
const {Element} = Help4;
Element.addClass('open');
Element.setAttribute(this.getDom('-field'), {ariaExpanded: true});
_button.active = true;
_observeOutsideClick.call(this, true);
}
/**
* @memberof Help4.control2.Select#
* @param {string} eventType
* @param {Help4.control.container.Container} bubble
* @param {string} id
* @private
*/
function _onBubbleClick(eventType, bubble, id) {
const value = bubble.get(id).getMetadata('id');
this.value = value;
this._fireEvent({type: 'change', value});
// closes itself via outside click, while select is control2 but bubble is control
}
/**
* @memberof Help4.control2.Select#
* @param {boolean} observe
* @private
*/
function _observeOutsideClick(observe) {
const {Event} = Help4;
if (observe) {
Event.observe(window, {
manual: true,
eventType: 'click',
callback: this._outsideClick = event => {
if (!(event = Event.normalize(event))) return;
if (!this.getDom().contains(event.target)) _closeBubble.call(this);
}
});
} else {
const {_outsideClick} = this;
if (_outsideClick) {
Event.stopObserving(window, {
manual: true,
eventType: 'click',
callback: _outsideClick
});
delete this._outsideClick;
}
}
}
})();