You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

284 lines
6.6 KiB
JavaScript

/**
* Abstract widget.
*/
class Widget {
/** @type {string} */
id;
/** @type {string} */
type;
/** @type {Renderer} */
renderer;
/** @type {object} */
data;
/**
* @param {object} data
* @param {Renderer} renderer
*/
constructor(data, renderer) {
this.id = data.id;
this.type = data.type;
this.data = data;
this.renderer = renderer;
}
/**
* Build HTML tree from widget.
*
* Should be overriden.
* @returns {HTMLElement | null}
*/
build() {
return null;
}
/**
* Handle update.
*
* Should be overriden.
* @param {object} data
*/
update(data) {
for (const [k, v] of Object.entries(data))
this.data[k] = v;
}
/**
* Handle previous set (with ack) was timed out.
*
* Should be overriden.
*/
_handleSetError(err) {}
/**
* Handle ack for previous set.
*
* Should be overriden.
*/
_handleAck() {}
/**
* Handle renderer closing.
*
* Should be overriden to stop timers (if any).
*/
close() {}
/**
* Set widget value.
*
* Should be called from subclass.
* @param {any} value
* @param {boolean} ack
* @returns {Promise<undefined>}
*/
set(value, ack = true) {
this.renderer._set(this, value, ack);
}
/**
* Register an UI file to load.
*
* Should be called from subclass.
* @param {string} path
* @param {string} type
* @param {(string) => undefined} callback
*/
addFile(path, type, callback) {
this.renderer._addFile(this, path, type, callback);
}
_handleFileProgress(perc) {}
_handleFileError(err) {}
_handleFileLoaded(res) {}
}
/**
* Widget with container.
*/
class BaseWidget extends Widget {
/** @type {HTMLDivElement} */
#root;
/** @type {HTMLDivElement} */
#inner;
/** @type {HTMLDivElement} */
#cont;
/** @type {HTMLSpanElement} */
#hint;
/** @type {HTMLSpanElement} */
#label;
/** @type {HTMLSpanElement} */
#plabel;
/** @type {HTMLSpanElement} */
#suffix;
/** @type {HTMLDivElement} */
#container;
/**
* @param {object} data
* @param {Renderer} renderer
*/
constructor(data, renderer) {
super(data, renderer);
this.#root = createElement(this, {
type: 'div',
class: 'widget_main',
style: {
width: data.wwidth_t + '%',
}
});
this.#inner = createElement(this, {
type: 'div',
class: 'widget_inner'
});
this.#root.append(this.#inner);
this.#cont = createElement(this, {
type: 'div',
class: 'widget_label'
});
this.#inner.append(this.#cont);
this.#hint = createElement(this, {
type: 'span',
class: 'whint',
text: '?',
style: {
display: 'none',
},
also($hint) {
$hint.addEventListener('click', () => asyncAlert($hint.title));
}
});
this.#cont.append(this.#hint);
this.#label = createElement(this, {
type: 'span',
text: data.type.toUpperCase(),
});
this.#cont.append(this.#label);
this.#plabel = createElement(this, {
type: 'span',
});
this.#cont.append(this.#plabel);
this.#suffix = createElement(this, {
type: 'span',
class: 'wsuffix',
});
this.#cont.append(this.#suffix);
this.#container = createElement(this, {
type: 'div',
class: 'widget_body',
style: {
minHeight: data.wheight && data.wheight > 25 ? data.wheight + 'px' : '',
}
});
this.#inner.append(this.#container);
}
build() {
return this.#root;
}
/**
* Initialize widget layout.
*
* Should be called from subclass constructor.
* @param {object[]} obj
*/
makeLayout(...obj) {
this.#container.replaceChildren(...obj.map(o => createElement(this, o)));
}
/**
* Handle update.
*
* Subclass should override this method and call super.update(data) from it.
* @param {object} data
*/
update(data) {
super.update(data);
if ('label' in data) {
this.#label.textContent = data.label.length ? data.label : this.type.toUpperCase();
}
if ('suffix' in data) {
this.#suffix.textContent = data.suffix;
}
if ('nolabel' in data) {
if (data.nolabel) this.#cont.classList.add('wnolabel');
else this.#cont.classList.remove('wnolabel');
}
if ('square' in data) {
if (data.square) this.#root.classList.add('wsquare');
else this.#root.classList.remove('wsquare');
}
if ('notab' in data) {
if (data.notab) this.#inner.classList.add('widget_notab');
else this.#inner.classList.remove('widget_notab');
}
if ('disable' in data) {
if (data.disable) this.#container.classList.add('widget_dsbl');
else this.#container.classList.remove('widget_dsbl');
}
if ('hint' in data) {
const htext = 'name: ' + this.id + '\n' + (data.hint ?? '');
this.#label.title = htext;
this.#hint.title = htext;
this.#hint.style.display = (data.hint && data.hint.length) ? 'inline-block' : 'none';
}
}
disable(el, disable) {
if (disable) {
el.setAttribute('disabled', '1');
el.classList.add('disable');
} else { // null/undefined/0/false
el.removeAttribute('disabled');
el.classList.remove('disable');
}
}
align(align) {
this.#container.style.justifyContent = ["flex-start", "center", "flex-end"][Number(align ?? 1)];
}
setPlabel(text = null) {
this.#plabel.textContent = text ?? '';
}
setSuffix(text = null) {
this.#suffix.textContent = text ?? '';
}
_handleSetError(err){
this.setPlabel("[ERR]");
showPopupError(`Widget ${this.id}: ` + getError(err));
}
_handleAck() {
this.setPlabel();
}
_handleFileProgress(perc) {
this.setPlabel(`[${perc}%]`);
}
_handleFileError(err) {
this.setPlabel("[ERR]");
showPopupError(`Widget ${this.id}: ` + getError(err));
}
_handleFileLoaded() {
this.setPlabel();
}
}