%PDF- %PDF-
Mini Shell

Mini Shell

Direktori : /www/varak.net/dmarc.varak.net/public/js/
Upload File :
Create Path :
Current File : //www/varak.net/dmarc.varak.net/public/js/widgets.js

/**
 * dmarc-srg - A php parser, viewer and summary report generator for incoming DMARC reports.
 * Copyright (C) 2020-2025 Aleksey Andreev (liuch)
 *
 * Available at:
 * https://github.com/liuch/dmarc-srg
 *
 * This program is free software: you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation, either version 3 of the License.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of  MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
 * more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * this program.  If not, see <http://www.gnu.org/licenses/>.
 */

class ITable {
	constructor(params) {
		this._element = null;
		this._class = null;
		this._header = null;
		this._status = null;
		this._frames = [];
		this._columns = [];
		this._body = null;
		this._onsort = null;
		this._onclick = null;
		this._onfocus = null;
		this.column_set = 4095;
		if (params) {
			this._class = params.class || null;
			this._onsort = params.onsort || null;
			this._onclick = params.onclick || null;
			this._onfocus = params.onfocus || null;
			this._nodata_text = params.nodata_text || null;
		}
		this._focused = false;
		this._focused_row = null;
		this._selected_rows = [];
	}

	element() {
		if (!this._element) {
			this._element = document.createElement("div");
			if (this._class) this._element.setAttribute("class", this._class);
			this._element.classList.add("table");
			this._element.setAttribute("tabindex", -1);
			this._element.addEventListener("focus", event => {
				this._focused = true;
				this._update_focus();
			}, true);
			this._element.addEventListener("blur", event => {
				this._focused = false;
				this._update_focus();
			}, true);
			let th = this._element.appendChild(document.createElement("div"));
			th.setAttribute("class", "table-header");
			this._header = th.appendChild(document.createElement("div"));
			this._header.setAttribute("class", "table-row");
			this._header.addEventListener("click", event => {
				const col = this.get_column_by_element(event.target);
				if (col && col.is_sortable()) {
					if (this._onsort) this._onsort(col);
				}
			});
			this._fill_columns();
			this._body = this._element.appendChild(document.createElement("div"));
			this._body.setAttribute("class", "table-body");
			this._body.addEventListener("click", event => {
				let row = this._get_row_by_element(event.target);
				if (row) {
					this._set_selected_rows([ row ]);
					if (this._onclick) this._onclick(row);
				}
			});
			this._body.addEventListener("focus", event => {
				let row = this._get_row_by_element(event.target);
				if (row) {
					this._update_focused_row(row);
					if (this._onfocus) this._onfocus(row.element());
				}
			}, true);
			this._body.addEventListener("blur", event => {
				let row = this._get_row_by_element(event.target);
				if (row) row.onfocus(false);
			}, true);
			this._body.addEventListener("keydown", event => {
				let row = null;
				switch (event.code) {
					case "ArrowDown":
						row = this._get_row(this._focused_row !== null && (this._focused_row.id() + 1) || 0);
						break;
					case "ArrowUp":
						if (this._focused_row) {
							let id = this._focused_row.id();
							if (id >= 0) row = this._get_row(id - 1);
						}
						else {
							row = this._get_row(0);
						}
						break;
					case "PageUp":
						if (this._focused_row && this._frames.length > 0) {
							let c_id = this._focused_row.id();
							let f_fr = this._frames[0];
							let f_id = f_fr.first_index();
							if (c_id == f_id) break;
							let s_el = this._get_scroll_element();
							if (s_el) {
								let r_ht = this._focused_row.element().getBoundingClientRect().height;
								let s_ht = s_el.getBoundingClientRect().height;
								let n_id = Math.max(c_id - Math.floor(s_ht / r_ht) - 1, f_id);
								row = this._get_row(n_id);
							}
							else {
								row = f_fr.row(f_id);
							}
						}
						break;
					case "PageDown":
						if (this._focused_row && this._frames.length > 0) {
							let c_id = this._focused_row.id();
							let l_fr = this._frames[this._frames.length - 1];
							let l_id = l_fr.last_index();
							if (c_id == l_id) break;
							let s_el = this._get_scroll_element();
							if (s_el) {
								let r_ht = this._focused_row.element().getBoundingClientRect().height;
								let s_ht = s_el.getBoundingClientRect().height;
								let n_id = Math.min(c_id + Math.floor(s_ht / r_ht) - 1, l_id);
								row = this._get_row(n_id);
							}
							else {
								row = l_fr.row(l_id);
							}
						}
						break;
					case "Home":
						if (this._frames.length > 0) {
							let first_frame = this._frames[0];
							row = first_frame.row(first_frame.first_index());
						}
						break;
					case "End":
						if (this._frames.length > 0) {
							let last_frame = this._frames[this._frames.length - 1];
							row = last_frame.row(last_frame.last_index());
						}
						break;
					case "Enter":
					case "NumpadEnter":
						if (this._onclick && this._focused_row) this._onclick(this._focused_row);
						event.preventDefault();
						return;
				}
				if (row) {
					row.element().focus();
					this._set_selected_rows([ row ]);
					event.preventDefault();
				}
			});
			this._fill_frames();
		}
		return this._element;
	}

	more() {
		return this._frames.length > 0 && this._frames[this._frames.length - 1].more();
	}

	frames_count() {
		return this._frames.length;
	}

	add_column(data) {
		let col = new ITableColumn(data.content, {
			name:     data.name,
			class:    data.class,
			sortable: data.sortable,
			sorted:   data.sorted
		});
		this._columns.push(col);
		if (this._header) {
			const c_idx = this._columns.length - 1;
			if (this.column_set & (1 << c_idx)) this._header.appendChild(col.element());
		}
		return col;
	}

	set_columns_visible(indexes) {
		let col_set = indexes.reduce((res, idx) => {
			res += (1 << idx);
			return res;
		}, 0);
		if (this.column_set !== col_set) {
			this.column_set = col_set;
			if (this._header) this._fill_columns();
			this._frames.forEach(fr => fr.update());
		}
	}

	get_column_by_element(el) {
		el = el && el.closest("div.table-cell");
		if (!el) return null;

		let bmask = 1;
		for (const col of this._columns) {
			if ((this.column_set & bmask) && el === col.element()) return col;
			bmask <<= 1;
		}
	}

	display_status(status, text) {
		if (this._status) {
			this._status.remove();
			if (!status) {
				this._status = null;
				return;
			}
		}

		this.element();
		this._status = document.createElement("div");
		this._status.classList.add("table-row", "colspanned", "noninteractive");
		const tc1 = this._status.appendChild(document.createElement("div"));
		tc1.classList.add("table-cell");
		const tc2 = this._status.appendChild(document.createElement("div"));
		tc2.classList.add("table-cell");
		tc2.textContent = "\u00A0"; // Non breaking space
		if (status === "wait") {
			set_wait_status(tc1);
		} else {
			this._body.replaceChildren();
			if (status === "nodata") {
				tc1.append(text || "No data");
			} else {
				set_error_status(tc1, text);
			}
		}
		this._status.classList.add("nodata");
		this._body.append(this._status);
	}

	last_row_index() {
		let idx = -1;
		if (this._frames.length > 0) {
			idx = this._frames[this._frames.length - 1].last_index();
		}
		return idx;
	}

	add_frame(frame) {
		if (frame.count() === 0) {
			if (this._frames.length === 0)
				this.display_status("nodata", this._nodata_text);
			return
		}

		frame.table = this;
		if (this._frames.length > 0 && this._frames[0].first_index() > frame.last_index()) {
			this._frames.unshift(frame);
			if (this._body)
				this._body.insertBefore(frame.element(), this._body.firstChild);
		}
		else {
			this._frames.push(frame);
			if (this._body)
				this._body.appendChild(frame.element());
		}
	}

	clear() {
		this._frames = [];
		if (this._body) this._body.replaceChildren();
		this._focused_row = null;
		this._selected_rows = [];
	}

	focus() {
		if (!this._focused_row) {
			if (this._frames.length > 0) {
				let fr = this._frames[0];
				this._focused_row = fr.row(fr.first_index());
			}
		}
		if (this._focused_row)
			this._focused_row.element().focus();
	}

	sort(col_name, direction) {
		if (this._frames.length != 1) return;

		for (let i = 0; i < this._columns.length; ++i) {
			const col = this._columns[i];
			if (col.is_sortable() && col.name() === col_name) {
				const fr = this._frames[0];
				fr.sort(i, direction);
				if (this._body) this._body.replaceChildren(fr.element());
				return;
			}
		}
	}

	set_sorted(col_name, direction) {
		for (const col of this._columns) {
			if (!col.is_sortable()) continue;
			if (col.name() !== col_name) {
				col.sort(null);
				continue;
			}

			if (direction === "toggle") {
				switch (col.sorted()) {
					case "ascent":
						direction = "descent";
						break;
					case "descent":
						direction = "ascent";
						break;
					default:
						direction = null;
						break;
				}
			}
			col.sort(direction);
		}
	}

	_fill_columns() {
		let bmask = 1;
		this._header.replaceChildren(...this._columns.reduce((res, col) => {
			if (this.column_set & bmask) {
				res.push(col.element());
			} else {
				col.remove();
			}
			bmask <<= 1;
			return res;
		}, []));
	}

	_fill_frames() {
		this._frames.forEach(fr => this._body.append(fr.element()));
	}

	_get_row(row_id) {
		const fr = this._frames.find(fr => {
			return fr.last_index() >= row_id && fr.first_index() <= row_id;
		});
		return fr && fr.row(row_id) || null;
	}

	_get_row_by_element(el) {
		if (el) {
			const r_el = el.closest("div.table-row");
			if (r_el) {
				const id = parseInt(r_el.dataset.id);
				if (id !== NaN) return this._get_row(id);
			}
		}
		return null;
	}

	_update_focus() {
		if (this._focused)
			this._element.classList.add("focused");
		else
			this._element.classList.remove("focused");
	}

	_update_focused_row(row) {
		if (this._focused_row && row !== this._focused_row) {
			this._focused_row.tabindex(-1);
		}
		this._focused_row = row;
		this._focused_row.tabindex(0);
		this._focused_row.onfocus(true);
	}

	_set_selected_rows(rows) {
		this._selected_rows.forEach(row => row.select(false));
		rows.forEach(row => row.select(true));
		this._selected_rows = rows;
	}

	_get_scroll_element() {
		let t_rect = this._element.getBoundingClientRect();
		let p_elem = this._element.parentElement;
		while (p_elem) {
			let p_rect = p_elem.getBoundingClientRect();
			if (t_rect.top < p_rect.top || t_rect.bottom > p_rect.bottom) {
				return p_elem;
			}
			p_elem = p_elem.parentElement;
		}
	}
}

class ITableFrame {
	constructor(data, pos) {
		this._pos = pos;
		this._more = !!data.more;
		let id = pos;
		this._rows = data.rows.map(function(rd) {
			if (!(rd instanceof ITableRow)) {
				rd = new ITableRow(rd);
			}
			rd.id(id++);
			return rd;
		});
		this.table = null;
	}

	count() {
		return this._rows.length;
	}

	first_index() {
		return this._pos;
	}

	last_index() {
		let cnt = this._rows.length;
		return cnt > 0 ? this._pos + cnt - 1 : null;
	}

	row(id) {
		let idx = id - this._pos;
		if (idx < 0 || idx >= this._rows.length) return null;
		return this._rows[idx];
	}

	more() {
		return this._more;
	}

	element() {
		const fr = document.createDocumentFragment();
		this._rows.forEach(row => {
			row.table = this.table;
			fr.appendChild(row.element());
		});
		return fr;
	}

	update() {
		this._rows.forEach(row => row.update());
	}

	sort(col_idx, direction) {
		let dir = (direction === "ascent" && 1) || (direction === "descent" && 2) || 0;
		if (!dir) return;

		this._rows.sort((a, b) => {
			const c1 = a.cell(col_idx);
			const c2 = b.cell(col_idx);
			return dir === 1 ? this._compare_cells(c2, c1) : this._compare_cells(c1, c2);
		});
		let id = this._pos;
		this._rows.forEach(row => row.id(id++));
	}

	_compare_cells(c1, c2) {
		return c1.value("sort") < c2.value("sort");
	}
}

class ITableRow {
	constructor(data) {
		this._id = -1;
		this._focused = false;
		this._tabindex = -1;
		this._selected = false;
		this._element = null;
		this._class = data.class || null;
		this._userdata = data.userdata || null;
		this._cells = data.cells.map(function(col) {
			if (col instanceof ITableCell) {
				return col;
			}
			let props = null;
			if (col.title || col.class || col.label) {
				props = {
					title: col.title || null,
					class: col.class || null,
					label: col.label || null
				};
			}
			return new ITableCell(col.content, props);
		});
	}

	userdata() {
		return this._userdata;
	}

	element() {
		let row_el = this._element;
		if (!row_el) {
			row_el = document.createElement("div");
			row_el.dataset.id = this._id;
			if (this._class) row_el.setAttribute("class", this._class);
			row_el.classList.add("table-row");
			this._element = row_el;

			row_el.append(...this._get_cell_elements());
			this._update_focus();
			this._update_tabindex();
			this._update_select();
		}
		return row_el;
	}

	update() {
		if (this._element) {
			this._element.replaceChildren(...this._get_cell_elements());
		}
	}

	onfocus(flag) {
		this._focused = flag;
		if (this._element) this._update_focus();
	}

	tabindex(index) {
		if (this._tabindex !== index) {
			this._tabindex = index;
			this._update_tabindex();
		}
	}

	select(flag) {
		this._selected = flag;
		if (this._element) this._update_select();
	}

	id(new_id) {
		if (new_id !== undefined && new_id !== this._id) {
			this._id = new_id;
			if (this._element) this._element.dataset.id = new_id;
		}
		return this._id;
	}

	cell(index) {
		return this._cells[index] || null;
	}

	_update_focus() {
		if (this._focused)
			this._element.classList.add("focused");
		else
			this._element.classList.remove("focused");
	}

	_update_tabindex() {
		this._element.setAttribute("tabindex", this._tabindex);
	}

	_update_select() {
		if (this._selected) {
			this._element.classList.add("selected");
		}
		else {
			this._element.classList.remove("selected");
		}
	}

	_get_cell_elements() {
		const col_set = this.table && this.table.column_set || 4095;
		const res = [];
		let bmask = 1;
		for (const col of this._cells) {
			if (col_set & bmask) {
				res.push(col.element());
			} else {
				col.remove();
			}
			bmask <<= 1;
		}
		return res;
	}
}

class ITableCell {
	constructor(content, props) {
		this._element = null;
		this._content = content;
		if (props) {
			this._title = props.title || null;
			this._class = props.class || null;
			this._label = props.label || null;
		}
	}

	element() {
		if (!this._element) {
			this._element = document.createElement("div");
			if (this._title) this._element.title = this._title;
			if (this._class) this._element.setAttribute("class", this._class);
			if (this._label) this._element.dataset.label = this._label;
			this._element.classList.add("table-cell");
			const content = this.value("dom");
			if (content !== null) this._element.append(content);
		}
		return this._element;
	}

	remove() {
		if (this._element) {
			this._element.remove();
			this._element = null;
		}
	}

	value(target) {
		if (target === "dom" || typeof(this._content) !== "object") {
			return this._content;
		}
		return null;
	}
}

class ITableColumn extends ITableCell {
	constructor(content, props) {
		super(content, props);
		this._name = props.name;
		this._sortable = !!props.sortable;
		this._sorted = props.sorted || null;
	}

	element() {
		if (this._element !== super.element()) {
			this._update_sorted();
		}
		return this._element;
	}

	is_sortable() {
		return this._sortable;
	}

	sort(dir) {
		if (this._sorted !== dir) {
			this._sorted = dir || null;
			if (this._element) {
				this._update_sorted();
			}
		}
	}

	sorted() {
		return this._sorted;
	}

	name() {
		return this._name;
	}

	_update_sorted() {
		if (this._sortable) {
			this._element.classList.add("sortable");
			let c_act = {
				asc: "remove",
				des: "remove"
			};
			if (this._sorted) {
				this._element.classList.add("arrows");
				if (this._sorted === "ascent") {
					c_act["asc"] = "add";
				}
				else if (this._sorted === "descent") {
					c_act["des"] = "add";
				}
			}
			else {
				this._element.classList.remove("arrows");
			}
			for (let key in c_act) {
				this._element.classList[c_act[key]]("sorted-" + key);
			}
		}
	}
}

class Toolbar {
	constructor(label) {
		this._label   = label;
		this._element = null;
		this._items   = [];
		this._spacer  = {}; // Just a unique value
	}

	element() {
		if (!this._element) {
			const el = document.createElement("div");
			el.role = "toolbar";
			el.ariaLabel = this._label;
			let spacer = false
			let first  = true;
			for (const it of this._items) {
				if (it === this._spacer) {
					spacer = true;
					continue;
				}
				if (spacer) {
					it.classList.add("spacer-left");
					spacer = false;
				}
				let te = (it instanceof ToolbarButton) ? it.element() : it;
				el.append(te);
				if (first && te.tabIndex === -1) {
					first = false;
					te.tabIndex = 0;
				}
			}
			el.addEventListener("keydown", event => {
				const target = event.target;
				switch (event.code) {
					case "ArrowLeft":
						this._focusPreviousItem(event.target);
						break;
					case "ArrowRight":
						this._focusNextItem(event.target);
						break;
					case "Home":
						this._focusFirstItem();
						break;
					case "End":
						this._focusLastItem();
						break;
				}
			});
			this._element = el;
		}
		return this._element;
	}

	appendItem(item) {
		this._items.push(item);
		return this;
	}

	appendSpacer() {
		this._items.push(this._spacer);
		return this;
	}

	_focusFirstItem() {
		const first = this._element.firstElementChild;
		if (!first.hasAttribute("tabindex")) {
			this._focusNextItem(first);
			return;
		}
		this._resetTabindexValues();
		first.tabIndex = 0;
		first.focus();
	}

	_focusLastItem() {
		const last = this._element.lastElementChild;
		if (!last.hasAttribute("tabindex")) {
			this._focusPreviousItem(last);
			return;
		}
		this._resetTabindexValues();
		last.tabIndex = 0;
		last.focus();
	}

	_focusNextItem(cItem) {
		let next = cItem;
		while ((next = next.nextElementSibling)) {
			if (next.hasAttribute("tabindex")) {
				this._resetTabindexValues();
				next.tabIndex = 0;
				next.focus();
				break;
			}
		}
	}

	_focusPreviousItem(cItem) {
		let prev = cItem;
		while ((prev = prev.previousElementSibling)) {
			if (prev.hasAttribute("tabindex")) {
				this._resetTabindexValues();
				prev.tabIndex = 0;
				prev.focus();
				break;
			}
		}

	}

	_resetTabindexValues() {
		this._element.querySelectorAll('[tabindex="0"]').forEach(el => {
			el.tabIndex = -1;
		});
	}
}

class ToolbarButton {
	constructor(params) {
		this._element = null;
		this._title   = params.title || null;
		this._content = params.content || null;
		this._onclick = params.onclick || null;
	}

	element() {
		if (!this._element) {
			const el = document.createElement("button");
			el.type = "button";
			el.tabIndex = -1;
			let ce = null
			if (this._content) {
				ce = el.appendChild((typeof(this._content) === "string") && this._getSVG() || this._content);
			}
			if (this._title) {
				const popup = document.createElement("span");
				popup.classList.add("popup-label");
				el.appendChild(popup).textContent = this._title;
				if (ce && ce.nodeName.toUpperCase() === "SVG") ce.setAttribute("aria-hidden", true);
			}
			if (this._onclick) el.addEventListener("click", this._onclick);
			this._element = el;
		}
		return this._element;
	}

	_getSVG() {
		switch (this._content) {
			case "info_icon":
				return this._svgIcon(
					'0 0 16 16',
					'<path d="m8.93 6.588-2.29.287-.082.38.45.083c.294.07.352.176.288.469l-.738 3.468c-.194.897.105 1.319.808 1.319.545 0 1.178-.252 1.465-.598l.088-.416c-.2.176-.492.246-.686.246-.275 0-.375-.193-.304-.533L8.93 6.588zM9 4.5a1 1 0 1 1-2 0 1 1 0 0 1 2 0z"/>'
				);
			case "filter_icon":
				return this._svgIcon(
					'0 1 15 15',
					'<path d="M1.5 1.5A.5.5 0 0 1 2 1h12a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.128.334L10 8.692V13.5a.5.5 0 0 1-.342.474l-3 1A.5.5 0 0 1 6 14.5V8.692L1.628 3.834A.5.5 0 0 1 1.5 3.5zm1 .5v1.308l4.372 4.858A.5.5 0 0 1 7 8.5v5.306l2-.666V8.5a.5.5 0 0 1 .128-.334L13.5 3.308V2z"/>'
				);
			case "columns_icon":
				return this._svgIcon(
					'0 0 17 17',
					'<path d="M0 1.5A1.5 1.5 0 0 1 1.5 0h13A1.5 1.5 0 0 1 16 1.5v13a1.5 1.5 0 0 1-1.5 1.5h-13A1.5 1.5 0 0 1 0 14.5zM1.5 1a.5.5 0 0 0-.5.5v13a.5.5 0 0 0 .5.5H5V1zM10 15V1H6v14zm1 0h3.5a.5.5 0 0 0 .5-.5v-13a.5.5 0 0 0-.5-.5H11z"/>'
				);
		}
	}

	_svgIcon(view_box, html) {
		const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
		svg.setAttribute("focusable", "false");
		svg.setAttribute("fill", "currentColor");
		svg.setAttribute("viewBox", view_box);
		svg.innerHTML = html;
		return svg;
	}
}

class ModalDialog {
	constructor(params) {
		this._params   = params;
		this._element  = null;
		this._title    = null;
		this._messages = null;
		this._alert    = null;
		this._wait     = null;
		this._buttons  = [];
		this._content  = null;
		this._result   = null;
		this._callback = null;
	}

	element() {
		if (!this._element) {
			const ovl = document.createElement("div");
			ovl.classList.add("dialog-overlay", "hidden");
			this._element = ovl;
			const dlg = ovl.appendChild(document.createElement("div"));
			dlg.role = "dialog";
			dlg.ariaModal = true;
			dlg.tabIndex = -1; // To catch keydown events
			dlg.classList.add("dialog");
			const con = dlg.appendChild(document.createElement("div"));
			con.classList.add("container");
			this._title = con.appendChild(document.createElement("div"));
			this._title.classList.add("title");
			{
				const tt = this._title.appendChild(document.createElement("div"));
				tt.classList.add("title-text");
				tt.textContent = this._params.title || "";
				if (this._params.title) dlg.setAriaLabelledBy(tt);
			}
			{
				const cbt = this._title.appendChild(document.createElement("button"));
				cbt.type = "button";
				cbt.ariaLabel = "Close";
				cbt.classList.add("close-btn");
				cbt.textContent = "x";
				this._buttons = [ cbt ];
				cbt.addEventListener("click", event => this.hide());
			}
			const frm = con.appendChild(document.createElement("form"));
			frm.classList.add("vertical-content");
			this._content = frm.appendChild(document.createElement("div"));
			const bdv = frm.appendChild(document.createElement("div"));
			bdv.classList.add("dialog-buttons");
			this._add_buttons(bdv);
			this._gen_content();
			this._element.addEventListener("click", event => {
				if (event.target === event.currentTarget && this._params.overlay_click !== "ignore") {
					this.hide();
				}
			});
			this._element.addEventListener("keydown", event => {
				switch (event.code) {
					case "Tab":
						{
							const els = this._get_focusable_elements();
							const lfe = [ els[0], els[els.length - 1] ];
							switch (lfe.indexOf(event.target)) {
								case -1:
									return;
								case 0:
									if (!event.shiftKey) return;
									lfe[1].focus();
									break;
								case 1:
									if (event.shiftKey) return;
									lfe[0].focus();
									break;
							}
							event.preventDefault();
						}
						break;
					case "Esc":
					case "Escape":
						event.preventDefault();
						this.hide();
						break;
				}
			});
			frm.addEventListener("submit", event => {
				event.preventDefault();
				this._submit();
			});
			frm.addEventListener("reset", event => this._reset());
		}
		return this._element;
	}

	show() {
		this.element();
		this._result = null;
		this._element.classList.remove("hidden");
		this.focus();

		return new Promise((resolve, reject) => {
			this._callback = resolve;
		});
	}

	hide() {
		if (this._element) this._element.classList.add("hidden");
		this._callback && this._callback(this._result);
	}

	focus() {
		this.element();
		const els = this._get_focusable_elements(2);
		switch (els.length) {
			case 2:
				if (els[0].classList.contains("close-btn")) {
					els[1].focus();
					break;
				}
			case 1:
				els[0].focus();
				break;
			default:
				this._element.querySelector('[role="dialog"]').focus();
				break;
		}
	}

	display_status(type, text) {
		if (type && !text) {
			type == "error" && this._alert && this._alert.replaceChildren();
			type == "wait" && this._wait && this._wait.replaceChildren();
		} else {
			this._alert && this._alert.replaceChildren();
			this._wait && this._wait.replaceChildren();
		}
		if (!text) return;

		const t_el = document.createElement("p");
		t_el.textContent = text;
		if (type == "error") {
			if (!this._alert) {
				this._alert = this._make_msg_container();
				this._alert.role = "alert";
			}
			t_el.classList.add("error-message");
			this._alert.append(t_el);
		} else if (type == "wait") {
			if (!this._wait) {
				this._wait = this._make_msg_container();
			}
			t_el.classList.add("wait-message");
			this._wait.append(t_el);
		}
	}

	_make_msg_container() {
		if (!this._messages) {
			this._messages = document.createElement("div");
			const btns = this.element().querySelector("form .dialog-buttons");
			btns.parentElement.insertBefore(this._messages, btns);
		}
		return this._messages.appendChild(document.createElement("div"));
	}

	_add_buttons(container) {
		let bl = this._params.buttons || [];
		bl.forEach(function(bt) {
			let name = null;
			let type = null;
			if (bt == "ok") {
				name = "Ok";
				type = "submit";
			}
			else if (bt == "apply") {
				name = "Apply";
				type = "submit";
			}
			else if (bt == "reset") {
				name = "Reset";
				type = "reset";
			}
			else if (bt == "login") {
				name = "Log in";
				type = "submit";
			}
			else if (bt == "cancel") {
				name = "Cancel";
				type = "close";
			}
			else if (bt == "close") {
				name = "Close";
				type = "close";
			}
			else {
				name = bt;
				type = bt;
			}
			this._add_button(container, name, type);
		}, this);
	}

	_add_button(container, text, type) {
		let btn = document.createElement("button");
		if (type == "close") {
			btn.setAttribute("type", "button");
			btn.addEventListener("click", this.hide.bind(this));
		}
		else {
			btn.setAttribute("type", type);
		}
		btn.appendChild(document.createTextNode(text));
		container.appendChild(btn);
		this._buttons.push(btn);
	}

	_gen_content() {
	}

	_get_focusable_elements(max) {
		const res = [];
		if (!max) max = -1;
		for (const el of this._element.querySelectorAll("input, select, button, a[href], [tabindex]")) {
			const ti = el.tabIndex;
			if (!isNaN(ti) && ti >= 0 && !el.disabled) {
				if (window.getComputedStyle(el, null).display !== "none") {
					res.push(el);
					if (!(--max)) break;
				}
			}
		}
		return res;
	}

	_submit() {
	}

	_reset() {
	}
}

class VerticalDialog extends ModalDialog {
	constructor(params) {
		super(params);
		this._inputs = null;
	}

	_insert_input_row(text, v_el) {
		if (!this._inputs) {
			this._inputs = document.createElement("div");
			this._inputs.classList.add("titled-input");
			this._content.appendChild(this._inputs);
			this._content.classList.add("vertical-content");
		}
		const l_el = document.createElement("label");
		const t_el = document.createElement("span");
		t_el.textContent = text + ": ";
		l_el.appendChild(t_el);
		l_el.appendChild(v_el);
		this._inputs.appendChild(l_el);
	}
}

class AboutDialog extends ModalDialog {
	constructor(params) {
		super({
			title:   "About",
			buttons: [ "ok" ]
		});
		this._authors = params.authors;
		this._documentation = params.documentation;
		this._source_code = params.source_code;
	}

	element() {
		if (!this._element) {
			super.element();
			this._element.children[0].classList.add("about");
			this._content.classList.add("vertical-content");
			this._content.parentElement.classList.add("vertical-content");
		}
		return this._element;
	}

	_gen_content() {
		let header = document.createElement("h2");
		header.appendChild(document.createTextNode(Router.app_name(true)));
		this._content.appendChild(header);

		let cblock = document.createElement("div");
		this._authors.forEach(function(author) {
			let ablock = document.createElement("div");
			ablock.appendChild(document.createTextNode("Copyright © " + author.years + ", "));
			cblock.appendChild(ablock);
			let alink = document.createElement("a");
			alink.setAttribute("href", author.url);
			alink.setAttribute("title", "The author's page");
			alink.setAttribute("target", "_blank");
			alink.appendChild(document.createTextNode(author.name));
			ablock.appendChild(alink);
		});
		this._content.appendChild(cblock);

		let oblock = document.createElement("div");
		oblock.setAttribute("class", "left-titled");
		let add_row = function(title, value) {
			let t_el = document.createElement("span");
			t_el.appendChild(document.createTextNode(title + ": "));
			oblock.appendChild(t_el);
			let v_el = document.createElement("div");
			value.forEach(function(v) {
				if (v_el.children.length > 0) {
					v_el.appendChild(document.createTextNode(", "));
				}
				let a_el = document.createElement("a");
				a_el.setAttribute("href", v.url);
				a_el.setAttribute("title", v.title || v.ancor);
				a_el.setAttribute("target", "_blank");
				a_el.appendChild(document.createTextNode(v.ancor));
				v_el.appendChild(a_el);
			});
			oblock.appendChild(v_el);
		};
		this._content.appendChild(oblock);
		add_row("Documentation", this._documentation);
		add_row("Source code", this._source_code);
		{
			let tl = document.createElement("span");
			tl.appendChild(document.createTextNode("PHP version: "));
			oblock.appendChild(tl);
			let vl = document.createElement("span");
			vl.appendChild(document.createTextNode(Router.php_version || "n/a"));
			oblock.appendChild(vl);
		}

		const lblock = this._content.appendChild(document.createElement("div"));
		lblock.classList.add("text");
		lblock.appendChild(document.createTextNode(
			"This program is free software: you can redistribute it and/or modify it \
under the terms of the GNU General Public License as published by the Free \
Software Foundation, either version 3 of the License."
		));
	}

	_submit() {
		this.hide();
	}
}

class ReportFilterDialog extends ModalDialog {
	constructor(params) {
		params ||= {};
		super({ title: params.title || "Filter settings", buttons: [ "apply", "reset" ] });
		this._data    = params;
		this._content = null;
		let item_list = params.item_list || [];
		this._ui_data = [
			{ name: "domain", title: "Domain" },
			{ name: "month", title: "Month" },
			{ name: "organization", title: "Organization" },
			{ name: "dkim", title: "DKIM result" },
			{ name: "spf", title: "SPF result" },
			{ name: "disposition", title: "Disposition" },
			{ name: "status", title: "Status" }
		].reduce(function(res, item) {
			if (item_list.includes(item.name))
				res.push(item);
			return res;
		}, []);
	}

	show() {
		this._update_ui();
		return super.show();
	}

	_gen_content() {
		let fs = document.createElement("fieldset");
		fs.setAttribute("class", "round-border titled-input");
		let lg = document.createElement("legend");
		lg.appendChild(document.createTextNode("Filter by"));
		fs.appendChild(lg);
		this._ui_data.forEach(function(ud) {
			let el = this._create_select_label(ud.title, fs);
			ud.element = el;
		}, this);
		this._content.appendChild(fs);
		this._content.classList.add("vertical-content");
		if (!this._data.loaded_filters)
			this._fetch_data();
	}

	_create_select_label(text, c_el) {
		let lb = document.createElement("label");
		let sp = document.createElement("span");
		sp.appendChild(document.createTextNode(text + ": "));
		lb.appendChild(sp);
		let sl = document.createElement("select");
		lb.appendChild(sl);
		c_el.appendChild(lb);
		return sl;
	}

	_enable_ui(enable) {
		let list = this._element.querySelector("form").elements;
		for (let i = 0; i < list.length; ++i) {
			list[i].disabled = !enable;
		}
		this.focus();
	}

	_update_ui() {
		this._update_filters();
	}

	_update_filters() {
		let data = this._data.loaded_filters || {};
		let vals = this._data.filter || {};
		this._ui_data.forEach(function(ud) {
			this._update_select_element(ud.element, data[ud.name], vals[ud.name]);
		}, this);
	}

	_update_select_element(sl, d, v) {
		let ao = document.createElement("option");
		ao.setAttribute("value", "");
		ao.setAttribute("selected", "selected");
		ao.appendChild(document.createTextNode("Any"));
		sl.replaceChildren(ao);
		let v2 = "";
		if (d) {
			let op = null;
			d.forEach(function(fs) {
				op = document.createElement("option");
				op.setAttribute("value", fs);
				op.appendChild(document.createTextNode(fs));
				if (fs === v) {
					v2 = v;
				}
				sl.appendChild(op);
			}, this);
		}
		sl.value = v2;
	}

	_submit() {
		let res = {};
		let fdata = {};
		this._ui_data.forEach(function(ud) {
			let el = ud.element;
			let val = el.options[el.selectedIndex].value;
			res[ud.name] = val;
			fdata[ud.name] = val;
		});
		this._data.filter = fdata;
		this._result = res;
		this.hide();
	}

	_fetch_data() {
	}
}

class Multiselect extends HTMLElement {
	constructor() {
		super();
		this._items    = [];
		this._aitems   = [];
		this._values   = new Set();
		this._label    = null;
		this._tags     = null;
		this._more     = null;
		this._search   = null;
		this._select   = null;
		this._listBox  = null;
		this._active   = false;
		this._disabled = false;
		this._focused  = { index: -1, item: null };
	}

	connectedCallback() {
		this._makeSelectButton();
		const iw = document.createElement("div");
		iw.classList.add("multiselect-wrapper");
		this.appendChild(iw).append(this._makeInputElement(), this._select);
		this._makeListbox();

		this._select.setAriaControls(this._listBox);
		this._search.setAriaControls(this._listBox);
		this._select.setAriaLabelledBy(this._listBox);

		this.addEventListener("focusin", event => {
			if (event.target === this._search) {
				this.activate();
			}
		});
		this.addEventListener("focusout", event => {
			if (!this.contains(event.relatedTarget)) {
				this.deactivate();
			}
		});
		this.tabIndex = -1;
		this._disableChanged();
		this._activateSearch();
	}

	static get observedAttributes() {
		return [ "placeholder", "disabled" ];
	}

	attributeChangedCallback(name, oldValue, newValue) {
		switch (name) {
			case "placeholder":
				if (this._search) this._search.placeholder = newValue;
				break;
			case "disabled":
				if (this._disabled !== (newValue !== null)) {
					this._disabled = !this._disabled;
					this._disableChanged();
				}
				break;
		}
	}

	activate() {
		if (!this._active && !this._disabled) {
			this.classList.add("active");
			this._active = true;
			this._select.ariaExpanded = true;
			this._search.ariaExpanded = true;
			this._search.classList.add("active");
			this._displayList(true);
		}
	}

	deactivate() {
		if (this._active) {
			this.classList.remove("active");
			this._active = false;
			this._select.ariaExpanded = false;
			this._search.ariaExpanded = false;
			if (this._values.size) this._search.classList.remove("active");
			this._search.value = "";
			this._displayList(false);
		}
	}

	clear() {
		this._items = [];
		if (this._active) this._updateList();
	}

	appendItem(value, text) {
		this._items.push({ value: value, text: text || ("" + value) });
		if (this._active) this._updateList();
	}

	setValues(data) {
		this._clearResults();
		if (data.length) {
			const items = data.reduce((res, val) => {
				const item = this._items.find(it => it.value === val);
				if (item && !this._values.has(item)) res.push(item);
				return res;
			}, []);
			this._updateResult(items);
		} else if (!this._active) {
			this._activateSearch();
		}
	}

	setLabel(str) {
		this._label = str;
		if (this._listBox) this._listBox.ariaLabel = str;
	}

	getValues() {
		const res = [];
		for (const item of this._values) {
			res.push(item.value);
		}
		return res;
	}

	isEmpty() {
		return !this._values.size;
	}

	get disabled() {
		return this._disabled;
	}

	set disabled(val) {
		if (val) {
			this.setAttribute("disabled", "");
		} else {
			this.removeAttribute("disabled");
		}
	}

	_makeInputElement() {
		const inputEl = document.createElement("div");
		inputEl.classList.add("multiselect-input");
		this._tags = document.createElement("div");
		this._tags.ariaHidden = true;
		this._tags.classList.add("multiselect-tags");
		this._makeSearchBar()
		inputEl.append(this._tags, this._search);
		return inputEl;
	}

	_makeSearchBar() {
		this._search = document.createElement("input");
		this._search.type = "text";
		this._search.role = "textbox";
		this._search.tabIndex = 0;
		this._search.ariaHasPopup = "listbox";
		this._search.ariaExpanded = false;
		this._search.disabled = this._disabled;
		this._search.classList.add("multiselect-search");
		this._search.setAttribute("spellcheck", "false");
		this._search.placeholder = this.getAttribute("placeholder") || ""
		this._search.addEventListener("input", event => {
			this._updateList();
		});
		this._search.addEventListener("keydown", event => {
			let idx = this._focused.index;
			switch (event.code) {
				case "Down":
				case "ArrowDown":
					event.preventDefault();
					if (++idx < this._aitems.length) this._focusItem(idx, true);
					break;
				case "Up":
				case "ArrowUp":
					event.preventDefault();
					if (--idx >= 0) this._focusItem(idx, true);
					break;
				case "Home":
					event.preventDefault();
					if (idx > 0) this._focusItem(0, true);
					break;
				case "End":
					event.preventDefault();
					if (++idx > 0 && idx < this._aitems.length) this._focusItem(this._aitems.length - 1, true);
					break;
				case "Enter":
				case "NumpadEnter":
					if (this._active) {
						event.preventDefault();
						if (this._focused.item) this._updateResult(this._focused.item);
					}
					break;
				case "Esc":
				case "Escape":
					if (this._active) {
						event.preventDefault();
						event.stopPropagation();
						this.deactivate();
					}
					break;
			}
		});
	}

	_makeSelectButton() {
		this._select = document.createElement("div");
		this._select.role = "button";
		this._select.tabIndex = -1;
		this._select.ariaExpanded = false;
		this._select.ariaHasPopup = "listbox";
		if (this._disabled) this._select.ariaDisabled = true;
		this._select.classList.add("multiselect-select");
		this._select.addEventListener("click", event => {
			if (this._active) {
				event.preventDefault();
				this.deactivate();
			}
		});
	}

	_makeListbox() {
		this._listBox = document.createElement("ul");
		this._listBox.role = "listbox";
		this._listBox.tabIndex = -1;
		if (this._label) this._listBox.ariaLabel = this._label;
		this._listBox.ariaMultiSelectable = true;
		this._listBox.classList.add("multiselect-options", "hidden");
		this.append(this._listBox);
		this._listBox.addEventListener("click", event => {
			if (event.target.role === "option") {
				event.preventDefault();
				const item = this._aitems.find(item => event.target === item.element);
				if (item) this._updateResult(item);
				this.deactivate();
			}
		});
	}

	_displayList(visible) {
		if (!visible) {
			this._listBox && this._listBox.classList.add("hidden");
			return;
		}

		this._listBox.classList.remove("hidden");
		this._listBox.scrollTop = 0;
		this._updateList();
	}

	_updateList() {
		this._listBox.replaceChildren();
		this._aitems = [];

		let cnt = 0;
		let txt = this._search.value;
		this._items.forEach(item => {
			if (!item.element) item.element = this._makeOption(item.text, this._values.has(item), false);
			if (item.text.includes(txt)) {
				this._listBox.appendChild(item.element);
				this._aitems.push(item);
				++cnt;
			}
		});

		if (cnt) {
			this._focusItem(0);
		} else {
			this._focused.index = -1;
			this._focused.item = null;
			this._listBox.append(this._makeOption("No items found", null, true));
		}
	}

	_makeTag(item) {
		const tb = document.createElement("i");
		tb.addEventListener("click", event => {
			event.preventDefault();
			if (!this._disabled) {
				this._updateResult(item)
				this._search.focus();
			}
		});
		const el = document.createElement("div");
		el.classList.add("multiselect-tag");
		const sp = el.appendChild(document.createElement("span"));
		sp.tabIndex = -1;
		sp.textContent = item.text
		el.append(tb);
		item.tag = el;
		return el;
	}

	_makeOption(text, selected, nodata) {
		const el = document.createElement("li");
		el.role = "option";
		el.textContent = text;
		if (nodata) {
			el.classList.add("nodata");
		} else {
			el.ariaSelected = selected;
			el.addEventListener("pointerenter", event => this._focusItem(event.target));
		}
		return el;
	}

	_activateSearch() {
		this._search.classList[this._values.size ? "remove" : "add"]("active");
	}

	_focusItem(item, scroll) {
		let idx = -1;
		if (typeof(item) === "number") {
			idx = item;
			item = this._aitems[idx];
		} else {
			idx = this._aitems.findIndex(it => item === it.element);
			item = (idx >= 0) ? this._aitems[idx] : null;
		}
		this._focused.item && this._focused.item.element.classList.remove("focused");
		item.element.classList.add("focused");
		this._focused = { index: idx, item: item };
		this._search.setAttribute("aria-activedescendant", item.element.getId());
		if (scroll) scroll_to_element(item.element, this._listBox);
	}

	_clearResults() {
		this._values.clear();
		this._items.forEach(item => {
			item.tag && item.tag.remove();
			if (item.element) item.element.ariaSelected = false;
		});
		this._more && this._more.remove();
		this.dispatchEvent(new Event("change"));
	}

	_updateResult(items) {
		const MAX_ITEMS = 3;
		if (!Array.isArray(items)) items = [ items ];
		items.forEach(item => {
			if (this._values.delete(item)) {
				if (item.element) item.element.ariaSelected = false;
			} else {
				this._values.add(item);
				if (item.element) item.element.ariaSelected = true;
			}
		});
		this._tags.replaceChildren();
		let cnt = 0;
		for (const vi of this._values) {
			this._tags.append(vi.tag || this._makeTag(vi));
			if (++cnt >= MAX_ITEMS) break;
		}
		if (this._values.size > MAX_ITEMS) {
			if (!this._more) {
				this._more = document.createElement("span");
				this._more.append("and ", "", " more");
			}
			this._more.childNodes[1].textContent = this._values.size - MAX_ITEMS;
			this._tags.append(this._more);
		}
		if (!this._active) this._activateSearch();
		this.dispatchEvent(new Event("change"));
	}

	_disableChanged() {
		if (this._disabled) this.deactivate();
		if (this._search) this._search.disabled = this._disabled;
		if (this._select) this._select.ariaDisabled = this._disabled;
	}
}
customElements.define("multi-select", Multiselect);

class HintButton {
	constructor(params) {
		this._params = params || {};
		this._element = null;
		this._content = null;
	}

	element() {
		if (!this._element) {
			const el = document.createElement("div");
			el.classList.add("hint-block");
			const bt = el.appendChild((new ToolbarButton({ content: "info_icon", title: "Details" })).element());
			bt.tabIndex = 0;
			const ct = el.appendChild(document.createElement("div"));
			ct.tabIndex = -1;
			ct.classList.add("hint-content", "hidden");
			bt.setAriaControls(ct);
			bt.addEventListener("click", event => {
				if (!this._content) {
					switch (typeof(this._params.content)) {
						case "function":
							this._content = this._params.content(this._params.data);
							break;
						case "object":
						case "string":
							this._content = this._params.content;
							break;
					}
					if (this._content) ct.append(this._content);
				}
				if (this._content) {
					ct.classList.remove("hidden");
					ct.focus();
				}
			});
			ct.addEventListener("focusout", event => {
				if (!event.relatedTarget || !ct.contains(event.relatedTarget)) {
					ct.classList.add("hidden");
				}
			});
			ct.addEventListener("keydown", event => {
				switch (event.code) {
					case "Esc":
					case "Escape":
						event.preventDefault();
						bt.focus();
						break;
				}
			});
			this._element = el;
		}
		return this._element;
	}

	reset() {
		if (this._content) {
			this._content.remove();
			this._content = null;
		}
	}
}

class MenuBar {
	constructor(element) {
		this._element = element;
		this._tbtn    = null;
		this._mbar    = null;
		this._focused = false;
	}

	static instance() {
		if (!this._instance) this._instance = new MenuBar(document.getElementById("main-menu-block"));
		return this._instance;
	}

	init() {
		this._tbtn = this._element.querySelector(".toggle-button");
		this._tggl = this._element.querySelector('#main-menu-toggle');
		this._mbar = this._element.querySelector('ul[role="menubar"]');
		this._updateMenu();

		function delayedFocus() {
			this._mbar.addEventListener("transitionend", event => this.focus(), { once: true });
		}

		this._element.addEventListener("focusin", event => {
			this._focused = true;
		});
		this._element.addEventListener("focusout", event => {
			this._focused = false;
			setTimeout(() => {
				if (!this._focused) this._tggl.checked = false;
			}, 0);
		});
		this._tbtn.addEventListener("keydown", event => {
			switch (event.key) {
				case " ":
				case "Enter":
					this._tggl.checked = !this._tggl.checked;
					if (this._tggl.checked) delayedFocus.call(this);
					break;
			}
		});
		this._tbtn.addEventListener("click", delayedFocus.bind(this));
		this._mbar.addEventListener("click", event => {
			const target = event.target;
			switch (target.role) {
				case "menuitem":
					if (target.ariaHasPopup === "true") {
						this._toggleMenu(target);
						this._focusItem(target);
					} else {
						this._tggl.checked = false;
					}
					break;
			}
		});
		this._mbar.addEventListener("keydown", this._onKeydown.bind(this));
		return this;
	}

	element(selector) {
		if (typeof(selector) !== "string") return this._element;
		return this._element.querySelector(selector);
	}

	focus() {
		const that = this;
		function findOldFocus(ul) {
			for (const li of ul.children) {
				if (window.getComputedStyle(li).display !== "none" && !li.classList.contains("disabled")) {
					let item = that._getMenuItem(li);
					if (item.tabIndex === 0) return item;
					if (item.ariaHasPopup) {
						item = findOldFocus(li.children[1]);
						if (item) return item;
					}
				}
			}
		}
		let m = findOldFocus(this._mbar);
		if (!m) m = this._getFirstMenuItem();
		this._focusItem(m);
	}

	updateCurrent() {
		this._mbar.querySelectorAll('[role="menuitem"][aria-current]').forEach(el => {
			el.ariaCurrent = null;
		});
		if (!this._focused) {
			this._mbar.querySelectorAll('[role="menuitem"][aria-expanded="true"]').forEach(el => {
				el.ariaExpanded = false;
			});
		}

		const url = new URL(document.location);
		url.search = "";
		const href = url.toString();
		for (const el of this._mbar.querySelectorAll('a[role="menuitem"]')) {
			if (el.href === href) {
				el.ariaCurrent = "page";
				const pi = this._getMenuItem(el.parentNode.parentNode.parentNode);
				if (pi) pi.ariaExpanded = true;
			}
		}
		this._mbar.tabIndex = -1;
	}

	insertItem(title, href, position) {
		const li = document.createElement("li");
		li.role = "none";
		const ae = li.appendChild(document.createElement("a"));
		ae.role = "menuitem";
		ae.tabIndex = -1;
		ae.href = href;
		ae.textContent = title;
		if (position < 0 || position >= this._mbar.children.length) {
			this._mbar.append(li);
		} else {
			this._mbar.insertBefore(li, this._mbar.children[position]);
		}
		return li;
	}

	_focusItem(item) {
		if (item && item.role !== "menuitem") item = this._getMenuItem(item);
		if (!item) return;

		this._mbar.querySelectorAll('[role="menuitem"][tabindex="0"]').forEach(el => {
			if (el !== item) el.tabIndex = -1;
		});
		item.tabIndex = 0;
		item.focus();
	}

	_focusNextItem(cItem) {
		this._focusItem(this._getNextMenuItem(cItem.parentNode));
	}

	_focusPreviousItem(cItem) {
		this._focusItem(this._getPreviousMenuItem(cItem.parentNode));
	}

	_getMenuItem(li) {
		return li.querySelector('[role="menuitem"]');
	}

	_isItemAvailable(li) {
		return (window.getComputedStyle(li).display !== "none" && !li.classList.contains("disabled"));
	}

	_getFirstMenuItem() {
		for (const li of this._mbar.children) {
			if (this._isItemAvailable(li)) return li;
		}
	}

	_getLastMenuItem() {
		let li = this._mbar.lastElementChild;
		while (li) {
			if (!this._isItemAvailable(li)) return this._getPreviousMenuItem(li);
			if (this._getMenuItem(li).ariaExpanded !== "true") return li;
			li = li.children[1].lastElementChild;
		}
	}

	_getNextMenuItem(li) {
		const ci = this._getMenuItem(li);
		if (ci.ariaExpanded === "true") {
			const first = ci.nextElementSibling.firstElementChild;
			if (this._isItemAvailable(first)) return first;
			return this._getNextMenuItem(first);
		}

		while (true) {
			let next = li.nextElementSibling;
			while (next) {
				if (this._isItemAvailable(next)) return next;
				next = next.nextElementSibling;
			}

			if (li.parentNode === this._mbar) return this._getFirstMenuItem();
			li = li.parentNode.parentNode;
		}
	}

	_getPreviousMenuItem(li) {
		let prev = li.previousElementSibling;
		while (prev) {
			if (!this._isItemAvailable(prev)) return this._getPreviousMenuItem(prev);
			if (this._getMenuItem(prev).ariaExpanded !== "true") return prev;
			prev = prev.children[1].lastElementChild;
		}
		if (li.parentNode === this._mbar) return this._getLastMenuItem();
		return li.parentNode.parentNode;
	}

	_updateMenu() {
		this._mbar.querySelectorAll('[role="menuitem"], [role="menu"]').forEach(el => {
			el.tabIndex = -1;
		});
	}

	_toggleMenu(item) {
		item.ariaExpanded = (item.ariaExpanded !== "true");
	}

	_onKeydown(event) {
		const tg = event.target;
		switch (event.key) {
			case " ":
			case "Enter":
				if (tg.ariaHasPopup === "true") this._toggleMenu(tg);
				break;
			case "Esc":
			case "Escape":
				this._tggl.checked = false;
				this._tbtn.focus();
				break;
			case "Up":
			case "ArrowUp":
				this._focusPreviousItem(tg);
				break;
			case "Down":
			case "ArrowDown":
				this._focusNextItem(tg);
				break;
			case "Home":
				this._focusItem(this._getFirstMenuItem());
				break;
			case "End":
				this._focusItem(this._getLastMenuItem());
				break;
		}
	}
}

Zerion Mini Shell 1.0