%PDF- %PDF-
| Direktori : /www/varak.net/dmarc.varak.net/public/js/ |
| 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;
}
}
}