/******/ (() => { // webpackBootstrap
/******/ var __webpack_modules__ = ({
/***/ "./assets/src/js/admin/statistics/api.js"
/*!***********************************************!*\
!*** ./assets/src/js/admin/statistics/api.js ***!
\***********************************************/
(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ getStatsConfig: () => (/* binding */ getStatsConfig),
/* harmony export */ getStatsI18n: () => (/* binding */ getStatsI18n),
/* harmony export */ lpStatsFetch: () => (/* binding */ lpStatsFetch)
/* harmony export */ });
/* harmony import */ var lpAssetsJsPath_utils_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! lpAssetsJsPath/utils.js */ "./assets/src/js/utils.js");
/* harmony import */ var _state_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./state.js */ "./assets/src/js/admin/statistics/state.js");
/**
* Statistics dashboard fetch wrapper + escaping helper.
*
* Every statistics request goes through lpStatsFetch so the localized globals
* (lpDataAdmin for REST root/nonce, lpAdminStatisticSettings for config) are
* read in exactly one file, and lpFetchAPI's blind spots are normalized here:
* it never rejects on HTTP error codes, so anything but status 'success'
* is routed to the error callback.
*
* @since 4.4.2
* @version 1.0.0
*/
const getStatsConfig = () => window.lpAdminStatisticSettings || {};
const getStatsI18n = (key, fallback = '') => {
const {
i18n = {}
} = getStatsConfig();
return i18n[key] || fallback;
};
/**
* Fetch a statistics endpoint with the global filter state applied.
*
* @param {string} endpoint Route below the statistics namespace, e.g. 'filter-options'.
* @param {Object} extraArgs Query args merged over the state (tab-specific params).
* @param {Object} functions { before, success, error, completed } — success only
* fires on status 'success'; error receives an Error.
*/
const lpStatsFetch = (endpoint, extraArgs = {}, functions = {}) => {
const lpDataAdmin = window.lpDataAdmin || {};
const restNamespace = getStatsConfig().restNamespace || 'lp/v1/statistics';
const url = lpAssetsJsPath_utils_js__WEBPACK_IMPORTED_MODULE_0__.lpAddQueryArgs(`${lpDataAdmin.lp_rest_url || '/wp-json/'}${restNamespace}/${endpoint}`, {
..._state_js__WEBPACK_IMPORTED_MODULE_1__.lpStatsState.get(),
...extraArgs
});
const onError = 'function' === typeof functions.error ? functions.error : err => console.error('LP Statistics:', err);
lpAssetsJsPath_utils_js__WEBPACK_IMPORTED_MODULE_0__.lpFetchAPI(url, {
headers: {
'X-WP-Nonce': lpDataAdmin.nonce || ''
}
}, {
...functions,
success: response => {
if (response && 'success' === response.status) {
// Broadcast the server-resolved range so the filter bar can
// reconcile its toggle label ( fixes the past-midnight case ).
const range = response.data && response.data.range;
if (range && range.label) {
document.dispatchEvent(new CustomEvent(_state_js__WEBPACK_IMPORTED_MODULE_1__.LP_STATS_RANGE_RESOLVED, {
detail: range
}));
}
if ('function' === typeof functions.success) {
functions.success(response);
}
} else {
onError(new Error(response && response.message || getStatsI18n('loadError', 'Request failed.')));
}
},
error: onError
});
};
/***/ },
/***/ "./assets/src/js/admin/statistics/chart.js"
/*!*************************************************!*\
!*** ./assets/src/js/admin/statistics/chart.js ***!
\*************************************************/
(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ granularityLabelFormatter: () => (/* binding */ granularityLabelFormatter),
/* harmony export */ intlFormat: () => (/* binding */ intlFormat),
/* harmony export */ renderLineChart: () => (/* binding */ renderLineChart)
/* harmony export */ });
/* harmony import */ var chart_js_auto__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! chart.js/auto */ "./node_modules/chart.js/auto/auto.js");
/* harmony import */ var _api_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./api.js */ "./assets/src/js/admin/statistics/api.js");
/**
* Line chart renderer wrapping Chart.js — single or dual-axis.
*
* Pure renderer: no fetching, no state mutation. Reuses an existing chart
* instance ( Chart.getChart ) instead of recreating, like the legacy
* initStatisticChart did.
*
* @since 4.4.2
* @version 1.0.0
*/
const DEFAULT_COLORS = ['#2271b1', '#00a32a'];
const EMPTY_STATE_CLASS = 'lp-stats-chart-empty';
/**
* Show/hide the empty state next to the canvas.
*
* @param {Element} canvas
* @param {boolean} show
*/
const toggleEmptyState = (canvas, show) => {
const wrapper = canvas.parentElement;
if (!wrapper) {
return;
}
let elEmpty = wrapper.querySelector(`.${EMPTY_STATE_CLASS}`);
if (show && !elEmpty) {
elEmpty = document.createElement('p');
elEmpty.className = EMPTY_STATE_CLASS;
elEmpty.textContent = (0,_api_js__WEBPACK_IMPORTED_MODULE_1__.getStatsI18n)('noData', 'No data for this period.');
wrapper.appendChild(elEmpty);
}
if (elEmpty) {
elEmpty.style.display = show ? '' : 'none';
}
canvas.style.display = show ? 'none' : 'block';
};
/**
* One-off locale date formatting. Fine for a handful of dates ( labels, a
* custom-range caption ); for per-label chart axes build a single formatter
* and reuse it instead ( see granularityLabelFormatter ).
*
* @param {Date} date
* @param {Object} options Intl.DateTimeFormat options.
* @return {string}
*/
const intlFormat = (date, options) => new Intl.DateTimeFormat(undefined, options).format(date);
/**
* Do all 'Y-m-d' day labels fall inside a single calendar month?
* When they cross a month boundary the axis must show the month, otherwise
* "30, 1, 2" is ambiguous.
*
* @param {Array} labels
* @return {boolean}
*/
const daysWithinOneMonth = labels => {
const months = labels.map(label => String(label).slice(0, 7));
return months.every(m => m === months[0]);
};
/**
* Axis label formatter for a chart payload's `granularity` marker
* ( PeriodRange->granularity, set server-side by PeriodResolver ):
*
* - hour int 0–23 → "14h"
* - day 'Y-m-d' → "Tue 14" ( ≤ 7 points, single month ) / "Jul 14"
* - month 'mm-YYYY' → "Jul 26"-style short month + 2-digit year
*
* The returned closure captures a single Intl formatter ( built once here, not
* per label ), so a 90-point chart formats against one instance. Unparsable
* labels pass through untouched — never throws.
*
* @param {string} granularity Marker from the payload.
* @param {Array} labels Full label set ( picks the day format density ).
* @return {Function|null} ( label ) => string, or null for unknown markers.
*/
const granularityLabelFormatter = (granularity, labels = []) => {
switch (granularity) {
case 'hour':
return label => `${label}h`;
case 'day':
{
// Weekday reads best for a short, single-month range; anything
// crossing a month shows the month so labels like "Jun 30 / Jul 1"
// stay unambiguous.
const options = labels.length <= 7 && daysWithinOneMonth(labels) ? {
weekday: 'short',
day: 'numeric'
} : {
month: 'short',
day: 'numeric'
};
const fmt = new Intl.DateTimeFormat(undefined, options);
return label => {
const date = new Date(`${label}T00:00:00`);
return isNaN(date.getTime()) ? String(label) : fmt.format(date);
};
}
case 'month':
{
// Labels are 'mm-YYYY'.
const fmt = new Intl.DateTimeFormat(undefined, {
month: 'short',
year: '2-digit'
});
return label => {
const parts = String(label).split('-');
const month = parseInt(parts[0], 10);
if (2 === parts.length && month >= 1 && month <= 12) {
return fmt.format(new Date(parseInt(parts[1], 10), month - 1, 1));
}
return String(label);
};
}
default:
// Unknown markers render as-is; Chart.js stringifies them for the axis.
return null;
}
};
/**
* Render (or update) a line chart.
*
* @param {string} canvasSelector e.g. '#net-sales-chart-content'.
* @param {Object} chartData { labels, datasets: [ { label, data, color, yAxisID } ], xLabel,
* granularity? — enables the shared axis label formatter }.
* @param {Object} config { yCurrency?: boolean (default true when 2 datasets),
* formatLabel?: ( label, index ) => string — overrides granularity }.
* @return {Chart|null} Chart instance, or null when canvas missing / no data.
*/
const renderLineChart = (canvasSelector, chartData = {}, config = {}) => {
var _config$yCurrency;
const canvas = document.querySelector(canvasSelector);
if (!canvas) {
return null;
}
const {
datasets = [],
xLabel = '',
granularity = ''
} = chartData;
let {
labels = []
} = chartData;
const hasData = datasets.length > 0 && datasets.some(dataset => (dataset.data || []).length > 0);
if (!hasData) {
const existing = chart_js_auto__WEBPACK_IMPORTED_MODULE_0__["default"].getChart(canvas);
if (existing) {
existing.destroy();
}
toggleEmptyState(canvas, true);
return null;
}
toggleEmptyState(canvas, false);
const formatLabel = 'function' === typeof config.formatLabel ? config.formatLabel : granularityLabelFormatter(granularity, labels);
if (formatLabel) {
labels = labels.map((label, index) => formatLabel(label, index));
}
const isDual = datasets.length > 1;
const yCurrency = (_config$yCurrency = config.yCurrency) !== null && _config$yCurrency !== void 0 ? _config$yCurrency : isDual;
const currencySymbol = (0,_api_js__WEBPACK_IMPORTED_MODULE_1__.getStatsConfig)().currencySymbol || '';
const chartDatasets = datasets.map((dataset, index) => {
const color = dataset.color || DEFAULT_COLORS[index % DEFAULT_COLORS.length];
return {
label: dataset.label || '',
data: dataset.data || [],
borderColor: color,
backgroundColor: color,
borderWidth: 2,
yAxisID: dataset.yAxisID || (isDual && index > 0 ? 'y1' : 'y')
};
});
const scales = {
y: {
min: 0,
position: 'left',
ticks: yCurrency ? {
callback: value => currencySymbol + value
} : {}
},
x: {
title: {
display: !!xLabel,
text: xLabel,
align: 'end'
}
}
};
if (isDual) {
scales.y1 = {
min: 0,
position: 'right',
grid: {
drawOnChartArea: false
},
ticks: {
precision: 0
}
};
}
const existing = chart_js_auto__WEBPACK_IMPORTED_MODULE_0__["default"].getChart(canvas);
if (existing) {
// Axis set changed (1 ↔ 2 lines) is easier rebuilt than migrated.
if (existing.data.datasets.length !== chartDatasets.length) {
existing.destroy();
} else {
existing.data.labels = labels;
chartDatasets.forEach((dataset, index) => {
existing.data.datasets[index].data = dataset.data;
existing.data.datasets[index].label = dataset.label;
});
existing.config.options.scales.x.title.text = xLabel;
existing.update();
return existing;
}
}
return new chart_js_auto__WEBPACK_IMPORTED_MODULE_0__["default"](canvas, {
type: 'line',
data: {
labels,
datasets: chartDatasets
},
options: {
responsive: true,
maintainAspectRatio: false,
aspectRatio: 0.8,
plugins: {
legend: {
display: isDual
}
},
scales
}
});
};
/***/ },
/***/ "./assets/src/js/admin/statistics/csv.js"
/*!***********************************************!*\
!*** ./assets/src/js/admin/statistics/csv.js ***!
\***********************************************/
(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ buildCsvFilename: () => (/* binding */ buildCsvFilename),
/* harmony export */ exportCsv: () => (/* binding */ exportCsv)
/* harmony export */ });
/* harmony import */ var _state_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./state.js */ "./assets/src/js/admin/statistics/state.js");
/**
* Client-side CSV export ( Blob + BOM ).
*
* RFC-4180 escaping plus a CSV-injection guard: values starting with
* = + - @ get a leading apostrophe so Excel never executes them.
*
* @since 4.4.2
* @version 1.0.0
*/
const sanitizeSegment = segment => {
const clean = String(segment !== null && segment !== void 0 ? segment : '').toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/^-+|-+$/g, '');
return clean || 'data';
};
/**
* `learnpress-{tab}-{table}-{filtertype}.csv`, all segments sanitized.
*
* @param {string} tab
* @param {string} table
* @return {string} Filename.
*/
const buildCsvFilename = (tab, table) => {
const {
filtertype
} = _state_js__WEBPACK_IMPORTED_MODULE_0__.lpStatsState.get();
return `learnpress-${sanitizeSegment(tab)}-${sanitizeSegment(table)}-${sanitizeSegment(filtertype)}.csv`;
};
const escapeCell = value => {
let str = null == value ? '' : String(value);
if (/^[=+\-@]/.test(str)) {
str = `'${str}`;
}
if (/[",\n\r]/.test(str)) {
str = `"${str.replace(/"/g, '""')}"`;
}
return str;
};
/**
* Build and download a CSV from a data-table handle.
*
* @param {string} filename Full filename (see buildCsvFilename).
* @param {Array} columns Column definitions ({ key, label, csv? }).
* @param {Array} rows Row objects.
*/
const exportCsv = (filename, columns = [], rows = []) => {
if (!columns.length) {
return;
}
const lines = [columns.map(column => escapeCell(column.label)).join(',')];
rows.forEach(row => {
lines.push(columns.map(column => {
const value = 'function' === typeof column.csv ? column.csv(row) : row[column.key];
return escapeCell(value);
}).join(','));
});
// BOM keeps Excel reading UTF-8 (Vietnamese titles etc.).
const blob = new Blob(['\u{FEFF}' + lines.join('\r\n')], {
type: 'text/csv;charset=utf-8;'
});
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
};
/***/ },
/***/ "./assets/src/js/admin/statistics/data-table.js"
/*!******************************************************!*\
!*** ./assets/src/js/admin/statistics/data-table.js ***!
\******************************************************/
(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ renderDataTable: () => (/* binding */ renderDataTable)
/* harmony export */ });
/* harmony import */ var _api_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./api.js */ "./assets/src/js/admin/statistics/api.js");
/**
* Data table renderer — createElement/textContent only, no innerHTML.
*
* Emits the plugin's shared table markup ( .lp-table-wrap > table.lp-list-table,
* per TableListTemplate ) so the dashboard widgets match every other LearnPress
* table. The extra lp-stats-table class carries the stats-only behaviours
* ( row hover/highlight, clickable performance rows, empty state ).
*
* Column definition:
* { key, label,
* format?: ( value, row ) => string|Node // Node for links etc.
* badge?: ( row ) => 'green'|'yellow'|'red'|'' // wraps the cell value
* csv?: ( row ) => string // plain value for CSV export (Nodes can't export)
* }
*
* @since 4.4.2
* @version 1.1.0
*/
/**
* @param {Element} elContainer Container emptied and refilled.
* @param {Array} columns Column definitions.
* @param {Array} rows Row objects keyed by column key.
* @param {Object} options { emptyText?: string }.
* @return {Object} { columns, rows } handle for csv/modal reuse.
*/
const renderDataTable = (elContainer, columns = [], rows = [], options = {}) => {
if (!elContainer) {
return {
columns,
rows
};
}
elContainer.textContent = '';
const wrap = document.createElement('div');
wrap.className = 'lp-table-wrap';
const table = document.createElement('table');
table.className = 'lp-list-table lp-stats-table';
const thead = document.createElement('thead');
const headRow = document.createElement('tr');
columns.forEach(column => {
var _column$label;
const th = document.createElement('th');
th.textContent = (_column$label = column.label) !== null && _column$label !== void 0 ? _column$label : '';
headRow.appendChild(th);
});
thead.appendChild(headRow);
table.appendChild(thead);
const tbody = document.createElement('tbody');
if (!rows.length) {
const tr = document.createElement('tr');
const td = document.createElement('td');
td.colSpan = columns.length || 1;
td.className = 'lp-stats-table__empty';
td.textContent = options.emptyText || (0,_api_js__WEBPACK_IMPORTED_MODULE_0__.getStatsI18n)('noData', 'No data for this period.');
tr.appendChild(td);
tbody.appendChild(tr);
} else {
rows.forEach(row => {
const tr = document.createElement('tr');
columns.forEach(column => {
const td = document.createElement('td');
const raw = row[column.key];
const output = 'function' === typeof column.format ? column.format(raw, row) : raw;
let cellNode;
if (output instanceof Node) {
cellNode = output;
} else {
cellNode = document.createTextNode(null == output ? '' : String(output));
}
const badgeColor = 'function' === typeof column.badge ? column.badge(row) : '';
if (badgeColor) {
const badge = document.createElement('span');
badge.className = `lp-badge lp-badge--${badgeColor}`;
badge.appendChild(cellNode);
td.appendChild(badge);
} else {
td.appendChild(cellNode);
}
tr.appendChild(td);
});
tbody.appendChild(tr);
});
}
table.appendChild(tbody);
wrap.appendChild(table);
elContainer.appendChild(wrap);
return {
columns,
rows
};
};
/***/ },
/***/ "./assets/src/js/admin/statistics/filter-bar.js"
/*!******************************************************!*\
!*** ./assets/src/js/admin/statistics/filter-bar.js ***!
\******************************************************/
(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ LpStatsFilterBar: () => (/* binding */ LpStatsFilterBar),
/* harmony export */ lpStatsFilterBar: () => (/* binding */ lpStatsFilterBar)
/* harmony export */ });
/* harmony import */ var lpAssetsJsPath_utils_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! lpAssetsJsPath/utils.js */ "./assets/src/js/utils.js");
/* harmony import */ var _state_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./state.js */ "./assets/src/js/admin/statistics/state.js");
/* harmony import */ var _api_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./api.js */ "./assets/src/js/admin/statistics/api.js");
/* harmony import */ var _chart_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./chart.js */ "./assets/src/js/admin/statistics/chart.js");
/**
* Statistics dashboard global filter bar.
*
* WC-style date-range dropdown ( Presets/Custom tabs + Compare to ) +
* instructor/category scope selects + CSV export trigger.
*
* Preset and compare selections apply immediately; the Custom tab applies on
* Update. Mutates state only through lpStatsState.set(); tab modules listen
* for the filter-changed event and never talk to this class directly.
*
* The toggle label is derived from state, not set imperatively per handler:
* LP_STATS_FILTER_CHANGED paints an optimistic label the instant the filter
* moves, and LP_STATS_RANGE_RESOLVED ( echoed by every stats payload ) then
* reconciles it to the server-resolved label — which is what keeps a panel
* left open past midnight from showing a stale "to date" range.
*
* Preset range labels come pre-resolved from the server ( dateRange.presets in
* lpAdminStatisticSettings ) — the only client-side date formatting is the
* custom range, via Intl.
*
* @since 4.4.2
* @version 2.1.0
*/
class LpStatsFilterBar {
static selectors = {
elContainer: '.lp-statistics-filter-bar',
elDaterange: '.lp-stats-daterange',
elToggle: '.lp-stats-daterange__toggle',
elToggleLabel: '.lp-stats-daterange__label',
elPanel: '.lp-stats-daterange__panel',
elTab: '.lp-stats-daterange__tab',
elTabpanel: '.lp-stats-daterange__tabpanel',
elPresetRadio: 'input[name="lp-stats-preset"]',
elCompareRadio: 'input[name="lp-stats-compare"]',
elCustomFrom: '.lp-stats-daterange__from',
elCustomTo: '.lp-stats-daterange__to',
elBtnUpdate: '.lp-stats-daterange__update',
elSelectInstructor: '.lp-stats-filter-instructor',
elSelectCategory: '.lp-stats-filter-category',
elBtnExport: '.lp-stats-export-csv'
};
init() {
this.elContainer = document.querySelector(LpStatsFilterBar.selectors.elContainer);
if (!this.elContainer) {
return;
}
this.loadFilterOptions();
this.events();
}
events() {
if (LpStatsFilterBar._loadedEvents) {
return;
}
LpStatsFilterBar._loadedEvents = this;
lpAssetsJsPath_utils_js__WEBPACK_IMPORTED_MODULE_0__.eventHandlers('click', [{
selector: LpStatsFilterBar.selectors.elToggle,
class: this,
callBack: this.togglePanel.name
}, {
selector: LpStatsFilterBar.selectors.elTab,
class: this,
callBack: this.switchTab.name
},
// Presets commit on a real pointer click. Chromium also fires a click
// on arrow-key radio navigation, but keyboard-synthesized clicks carry
// detail 0 — changePreset ignores those so arrows browse; keyboard
// commit is the Enter/Space keydown handler below. ( Committing on
// 'change' would apply + close + refetch on every arrow keystroke. )
{
selector: LpStatsFilterBar.selectors.elPresetRadio,
class: this,
callBack: this.changePreset.name
}, {
selector: LpStatsFilterBar.selectors.elBtnUpdate,
class: this,
callBack: this.applyCustomRange.name
}, {
selector: LpStatsFilterBar.selectors.elBtnExport,
class: this,
callBack: this.exportCsv.name
}]);
// Keyboard commit for the browsed preset ( Enter or Space on the focused
// radio ); arrow keys move the selection without committing.
lpAssetsJsPath_utils_js__WEBPACK_IMPORTED_MODULE_0__.eventHandlers('keydown', [{
selector: LpStatsFilterBar.selectors.elPresetRadio,
class: this,
callBack: this.changePreset.name,
conditionBeforeCallBack: args => 'Enter' === args.e.key || ' ' === args.e.key
}]);
lpAssetsJsPath_utils_js__WEBPACK_IMPORTED_MODULE_0__.eventHandlers('change', [{
selector: LpStatsFilterBar.selectors.elCompareRadio,
class: this,
callBack: this.changeCompare.name
}, {
selector: LpStatsFilterBar.selectors.elSelectInstructor,
class: this,
callBack: this.changeScope.name
}, {
selector: LpStatsFilterBar.selectors.elSelectCategory,
class: this,
callBack: this.changeScope.name
}]);
// Outside click / Esc close the popover.
document.addEventListener('click', event => {
if (this.isPanelOpen() && !event.target.closest(LpStatsFilterBar.selectors.elDaterange)) {
this.closePanel();
}
});
document.addEventListener('keydown', event => {
if ('Escape' === event.key && this.isPanelOpen()) {
this.closePanel(true);
}
});
// Toggle label follows state: an optimistic label the moment the filter
// moves, then the authoritative server label when the payload lands.
document.addEventListener(_state_js__WEBPACK_IMPORTED_MODULE_1__.LP_STATS_FILTER_CHANGED, event => {
this.setToggleLabel(this.labelForState(event.detail));
});
document.addEventListener(_state_js__WEBPACK_IMPORTED_MODULE_1__.LP_STATS_RANGE_RESOLVED, event => {
this.setToggleLabel(event.detail?.label);
});
}
// Popover open/close + tabs.
panel() {
return this.elContainer.querySelector(LpStatsFilterBar.selectors.elPanel);
}
isPanelOpen() {
const elPanel = this.panel();
return !!elPanel && !elPanel.hidden;
}
togglePanel(args) {
const btn = args.target.closest(LpStatsFilterBar.selectors.elToggle);
if (!btn || !this.elContainer.contains(btn)) {
return;
}
const elPanel = this.panel();
if (!elPanel) {
// Toggle rendered without its panel ( template override ) — no-op,
// like closePanel(), instead of throwing on a null deref.
return;
}
if (!elPanel.hidden) {
this.closePanel();
return;
}
elPanel.hidden = false;
btn.setAttribute('aria-expanded', 'true');
// Focus-trap-lite: land on the checked preset ( or the active tab ).
const checked = elPanel.querySelector(`${LpStatsFilterBar.selectors.elPresetRadio}:checked`);
const fallback = elPanel.querySelector(`${LpStatsFilterBar.selectors.elTab}.active`);
(checked && checked.offsetParent ? checked : fallback)?.focus();
}
/**
* @param {boolean} refocus Return focus to the toggle ( Esc ), not on outside click.
*/
closePanel(refocus = false) {
const elPanel = this.panel();
if (!elPanel) {
return;
}
elPanel.hidden = true;
const elToggle = this.elContainer.querySelector(LpStatsFilterBar.selectors.elToggle);
elToggle?.setAttribute('aria-expanded', 'false');
if (refocus) {
elToggle?.focus();
}
}
switchTab(args) {
const btn = args.target.closest(LpStatsFilterBar.selectors.elTab);
if (!btn || !this.elContainer.contains(btn)) {
return;
}
const tab = btn.dataset.tab;
this.elContainer.querySelectorAll(LpStatsFilterBar.selectors.elTab).forEach(el => {
const active = el === btn;
el.classList.toggle('active', active);
el.setAttribute('aria-selected', active ? 'true' : 'false');
});
this.elContainer.querySelectorAll(LpStatsFilterBar.selectors.elTabpanel).forEach(el => {
el.hidden = el.dataset.tabpanel !== tab;
});
}
// Selection → state.
changePreset(args) {
const radio = args.target.closest(LpStatsFilterBar.selectors.elPresetRadio);
if (!radio || !this.elContainer.contains(radio)) {
return;
}
// A click with detail 0 is keyboard-synthesized ( arrow navigation, or
// Space ) — let the user browse; the Enter/Space keydown binding is what
// commits from the keyboard. Real pointer clicks have detail >= 1.
if ('click' === args.e.type && !args.e.detail) {
return;
}
// Label updates via the filter-changed listener ( derived from state ).
this.closePanel(true);
_state_js__WEBPACK_IMPORTED_MODULE_1__.lpStatsState.set({
filtertype: radio.value,
date: ''
});
}
changeCompare(args) {
const radio = args.target.closest(LpStatsFilterBar.selectors.elCompareRadio);
if (!radio || !this.elContainer.contains(radio)) {
return;
}
// Popover stays open: compare is a modifier, not a range choice.
_state_js__WEBPACK_IMPORTED_MODULE_1__.lpStatsState.set({
compare: radio.value
});
}
applyCustomRange(args) {
const btn = args.target.closest(LpStatsFilterBar.selectors.elBtnUpdate);
if (!btn || !this.elContainer.contains(btn)) {
return;
}
const from = this.elContainer.querySelector(LpStatsFilterBar.selectors.elCustomFrom)?.value;
const to = this.elContainer.querySelector(LpStatsFilterBar.selectors.elCustomTo)?.value;
if (!from || !to) {
return;
}
// Uncheck any preset — the window is now the custom pair.
this.elContainer.querySelectorAll(LpStatsFilterBar.selectors.elPresetRadio).forEach(el => {
el.checked = false;
});
const [start, end] = [from, to].sort();
// Label updates via the filter-changed listener ( derived from state ).
this.closePanel(true);
_state_js__WEBPACK_IMPORTED_MODULE_1__.lpStatsState.set({
filtertype: 'custom',
date: `${start}+${end}`
});
}
// Toggle label.
setToggleLabel(label) {
const elLabel = this.elContainer.querySelector(LpStatsFilterBar.selectors.elToggleLabel);
if (elLabel && label) {
elLabel.textContent = label;
}
}
/**
* Optimistic toggle label for the current filter state — a preset's
* server-resolved label, or "Custom (range)" for a custom window. The
* authoritative label arrives later via LP_STATS_RANGE_RESOLVED.
*
* @param {Object} filters { filtertype, date } from lpStatsState.
*/
labelForState({
filtertype,
date
} = {}) {
if ('custom' === filtertype && date) {
const [start, end] = date.split('+');
if (start && end) {
return `${(0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('custom', 'Custom')} (${this.customRangeLabel(start, end)})`;
}
}
return this.presetLabel(filtertype);
}
/**
* "Month to date (Jul 1 – 14)" from the server-resolved preset table.
*
* @param {string} value Preset id.
*/
presetLabel(value) {
const {
presets = []
} = (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsConfig)().dateRange || {};
const preset = presets.find(entry => entry.value === value);
if (!preset) {
return value;
}
return preset.rangeLabel ? `${preset.name} (${preset.rangeLabel})` : preset.name;
}
/**
* Locale-formatted custom range, densest unambiguous form
* ( "Jul 1 – 14", "Apr 1 – Jun 30", "Dec 29, 2025 – Jan 4, 2026" ).
*
* @param {string} start 'Y-m-d'.
* @param {string} end 'Y-m-d'.
*/
customRangeLabel(start, end) {
const dateFrom = new Date(`${start}T00:00:00`);
const dateTo = new Date(`${end}T00:00:00`);
if (isNaN(dateFrom.getTime()) || isNaN(dateTo.getTime())) {
return `${start} – ${end}`;
}
const sameYear = dateFrom.getFullYear() === dateTo.getFullYear();
const sameMonth = sameYear && dateFrom.getMonth() === dateTo.getMonth();
if (sameMonth) {
const startPart = (0,_chart_js__WEBPACK_IMPORTED_MODULE_3__.intlFormat)(dateFrom, {
month: 'short',
day: 'numeric'
});
return start === end ? startPart : `${startPart} – ${(0,_chart_js__WEBPACK_IMPORTED_MODULE_3__.intlFormat)(dateTo, {
day: 'numeric'
})}`;
}
if (sameYear) {
const options = {
month: 'short',
day: 'numeric'
};
return `${(0,_chart_js__WEBPACK_IMPORTED_MODULE_3__.intlFormat)(dateFrom, options)} – ${(0,_chart_js__WEBPACK_IMPORTED_MODULE_3__.intlFormat)(dateTo, options)}`;
}
const options = {
month: 'short',
day: 'numeric',
year: 'numeric'
};
return `${(0,_chart_js__WEBPACK_IMPORTED_MODULE_3__.intlFormat)(dateFrom, options)} – ${(0,_chart_js__WEBPACK_IMPORTED_MODULE_3__.intlFormat)(dateTo, options)}`;
}
// Scope selects + export ( unchanged behavior ).
/**
* Populate the two scope selects from the filter-options endpoint,
* then restore any deep-linked selection already held by the state.
*/
loadFilterOptions() {
(0,_api_js__WEBPACK_IMPORTED_MODULE_2__.lpStatsFetch)('filter-options', {}, {
success: response => {
const {
instructors = [],
categories = []
} = response.data || {};
this.fillSelect(LpStatsFilterBar.selectors.elSelectInstructor, instructors, _state_js__WEBPACK_IMPORTED_MODULE_1__.lpStatsState.get().instructor_id);
this.fillSelect(LpStatsFilterBar.selectors.elSelectCategory, categories, _state_js__WEBPACK_IMPORTED_MODULE_1__.lpStatsState.get().category_id);
}
});
}
/**
* Append { id, name } options — createElement + textContent only,
* names are user-controlled.
*
* @param {string} selector Select element selector inside the bar.
* @param {Array} items [ { id, name } ].
* @param {number} selected Id to preselect (deep link), 0 for "All".
*/
fillSelect(selector, items, selected = 0) {
const elSelect = this.elContainer.querySelector(selector);
if (!elSelect) {
return;
}
items.forEach(item => {
const option = document.createElement('option');
option.value = item.id;
option.textContent = item.name;
elSelect.appendChild(option);
});
if (selected) {
elSelect.value = String(selected);
// Unknown deep-link id → back to "All", state follows the visible truth.
if (elSelect.value !== String(selected)) {
elSelect.value = '0';
}
}
}
changeScope(args) {
const elSelect = args.target.closest('select');
if (!elSelect || !this.elContainer.contains(elSelect)) {
return;
}
const elInstructor = this.elContainer.querySelector(LpStatsFilterBar.selectors.elSelectInstructor);
const elCategory = this.elContainer.querySelector(LpStatsFilterBar.selectors.elSelectCategory);
_state_js__WEBPACK_IMPORTED_MODULE_1__.lpStatsState.set({
instructor_id: parseInt(elInstructor?.value, 10) || 0,
category_id: parseInt(elCategory?.value, 10) || 0
});
}
exportCsv(args) {
const btn = args.target.closest(LpStatsFilterBar.selectors.elBtnExport);
if (!btn || !this.elContainer.contains(btn)) {
return;
}
document.dispatchEvent(new CustomEvent(_state_js__WEBPACK_IMPORTED_MODULE_1__.LP_STATS_EXPORT_CSV));
}
}
const lpStatsFilterBar = new LpStatsFilterBar();
/***/ },
/***/ "./assets/src/js/admin/statistics/kpi.js"
/*!***********************************************!*\
!*** ./assets/src/js/admin/statistics/kpi.js ***!
\***********************************************/
(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ renderKpi: () => (/* binding */ renderKpi)
/* harmony export */ });
/**
* KPI card renderer — pure DOM fill, no fetch.
*
* Payload shape comes from PeriodHelper::kpi_payload() on the server:
* { value, prev_value, change_pct } plus optional client extras
* { formatted, subline }. change_pct null → delta hidden (no wrong deltas).
*
* @since 4.4.2
* @version 1.0.0
*/
/**
* @param {Element} elCard The .lp-kpi-card root.
* @param {Object} payload KPI payload.
*/
const renderKpi = (elCard, payload = {}) => {
if (!elCard) {
return;
}
const elValue = elCard.querySelector('.lp-kpi-value');
const elDelta = elCard.querySelector('.lp-kpi-delta');
const elSubline = elCard.querySelector('.lp-kpi-subline');
if (elValue) {
var _payload$formatted;
const value = (_payload$formatted = payload.formatted) !== null && _payload$formatted !== void 0 ? _payload$formatted : payload.value;
elValue.textContent = null == value || '' === value ? '–' : String(value);
}
if (elDelta) {
elDelta.classList.remove('is-up', 'is-down');
if ('number' === typeof payload.change_pct) {
const isUp = payload.change_pct >= 0;
elDelta.classList.add(isUp ? 'is-up' : 'is-down');
elDelta.textContent = `${isUp ? '▲' : '▼'} ${Math.abs(payload.change_pct)}%`;
} else {
elDelta.textContent = '';
}
}
if (elSubline) {
var _payload$subline;
elSubline.textContent = (_payload$subline = payload.subline) !== null && _payload$subline !== void 0 ? _payload$subline : '';
}
};
/***/ },
/***/ "./assets/src/js/admin/statistics/report-modal.js"
/*!********************************************************!*\
!*** ./assets/src/js/admin/statistics/report-modal.js ***!
\********************************************************/
(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ LpStatsReportModal: () => (/* binding */ LpStatsReportModal),
/* harmony export */ lpStatsReportModal: () => (/* binding */ lpStatsReportModal)
/* harmony export */ });
/* harmony import */ var sweetalert2__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! sweetalert2 */ "./node_modules/sweetalert2/dist/sweetalert2.all.js");
/* harmony import */ var sweetalert2__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(sweetalert2__WEBPACK_IMPORTED_MODULE_0__);
/* harmony import */ var lpAssetsJsPath_utils_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! lpAssetsJsPath/utils.js */ "./assets/src/js/utils.js");
/* harmony import */ var _state_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./state.js */ "./assets/src/js/admin/statistics/state.js");
/* harmony import */ var _api_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./api.js */ "./assets/src/js/admin/statistics/api.js");
/**
* Report popup controller — SweetAlert2 shell over a server-rendered table.
*
* The table is built in PHP ( AdminStatisticsReportTable ) via TableListTemplate
* and delivered through TemplateAJAX: open() injects the popup body, points the
* .lp-target at the requested report + current filters, and triggers loadAJAX to
* fetch it. Pagination is handled by loadAJAX.js ( .page-numbers ). Search
* re-queries the server ( debounced, resets to page 1 ); export asks the server
* for the full CSV and downloads it.
*
* @since 4.4.2
* @version 3.0.0
*/
class LpStatsReportModal {
static selectors = {
template: '#lp-tmpl-stats-report-modal',
elContainer: '.lp-stats-report-modal',
elSearch: '.lp-stats-report-modal__search',
elExport: '.lp-stats-report-modal__export',
elTarget: '.lp-target'
};
constructor() {
this.title = '';
this.tableId = '';
}
init() {
this.events();
}
events() {
if (LpStatsReportModal._loadedEvents) {
return;
}
LpStatsReportModal._loadedEvents = this;
// Debounced ONCE here — never create a debounce inside a handler.
this.debouncedSearch = lpAssetsJsPath_utils_js__WEBPACK_IMPORTED_MODULE_1__.debounce(() => this.applySearch(), 400);
lpAssetsJsPath_utils_js__WEBPACK_IMPORTED_MODULE_1__.eventHandlers('click', [{
selector: LpStatsReportModal.selectors.elExport,
class: this,
callBack: this.exportCsv.name
}]);
lpAssetsJsPath_utils_js__WEBPACK_IMPORTED_MODULE_1__.eventHandlers('input', [{
selector: LpStatsReportModal.selectors.elSearch,
class: this,
callBack: this.onSearchInput.name
}]);
}
/**
* @return {Object|null} window.lpAJAXG when it exposes the API we need.
*/
getAjaxHandle() {
const handle = window.lpAJAXG;
if (!handle || 'function' !== typeof handle.getDataSetCurrent || 'function' !== typeof handle.setDataSetCurrent || 'function' !== typeof handle.fetchAJAX || 'function' !== typeof handle.showHideLoading) {
return null;
}
return handle;
}
getModalPopup() {
return (sweetalert2__WEBPACK_IMPORTED_MODULE_0___default().getPopup) ? sweetalert2__WEBPACK_IMPORTED_MODULE_0___default().getPopup() : null;
}
getModalContent() {
const popup = this.getModalPopup();
if (!popup) {
return null;
}
return popup.querySelector(LpStatsReportModal.selectors.elContainer);
}
getTarget() {
const content = this.getModalContent();
return content ? content.querySelector(LpStatsReportModal.selectors.elTarget) : null;
}
getModalHtml() {
const template = document.querySelector(LpStatsReportModal.selectors.template);
return template ? template.innerHTML : '';
}
isOpen() {
return !!this.getModalContent();
}
/**
* @param {Object} report { report, title, tableId?, orderStatus? }
* - report: server report slug ( e.g. 'top_courses' ).
* - orderStatus: cancelled/failed deep-link for the exceptions report.
*/
open(report = {}) {
const modalHtml = this.getModalHtml();
if (!modalHtml || !report.report) {
return;
}
this.init();
this.title = report.title || '';
this.tableId = report.tableId || report.report;
sweetalert2__WEBPACK_IMPORTED_MODULE_0___default().fire({
title: this.title,
html: modalHtml,
// Large by default; the custom class lets the SCSS push it (near) full size.
width: '100%',
customClass: {
popup: 'lp-stats-report-popup'
},
showConfirmButton: false,
showCloseButton: true,
didOpen: () => this.loadReport(report)
});
}
close() {
sweetalert2__WEBPACK_IMPORTED_MODULE_0___default().close();
}
/**
* Seed the .lp-target with report + current filters and fetch page 1.
*
* @param {Object} report
*/
loadReport(report) {
const target = this.getTarget();
const handle = this.getAjaxHandle();
if (!target || !handle) {
if (target) {
target.innerHTML = (0,_api_js__WEBPACK_IMPORTED_MODULE_3__.getStatsI18n)('loadError', 'Request failed.');
}
return;
}
const dataSend = handle.getDataSetCurrent(target);
dataSend.args = {
...(dataSend.args || {}),
..._state_js__WEBPACK_IMPORTED_MODULE_2__.lpStatsState.get(),
report: report.report,
search: '',
paged: 1,
// Report-specific args ( e.g. instructor_id ) win over the global filters.
...(report.args || {})
};
if (report.orderStatus) {
dataSend.args.order_status = report.orderStatus;
}
handle.setDataSetCurrent(target, dataSend);
this.reloadTarget(target, dataSend);
}
onSearchInput() {
this.debouncedSearch();
}
applySearch() {
const content = this.getModalContent();
const target = this.getTarget();
const handle = this.getAjaxHandle();
if (!content || !target || !handle) {
return;
}
const elSearch = content.querySelector(LpStatsReportModal.selectors.elSearch);
const dataSend = handle.getDataSetCurrent(target);
dataSend.args = dataSend.args || {};
dataSend.args.search = (elSearch?.value || '').trim();
dataSend.args.paged = 1;
handle.setDataSetCurrent(target, dataSend);
this.reloadTarget(target, dataSend);
}
/**
* Loading indicator + AJAX fetch, swapping the target's innerHTML.
*
* @param {Element} target
* @param {Object} dataSend
*/
reloadTarget(target, dataSend) {
const handle = this.getAjaxHandle();
if (!handle) {
return;
}
handle.showHideLoading(target, 1);
handle.fetchAJAX(dataSend, {
success: response => {
const {
status,
message,
data
} = response;
if ('success' === status) {
target.innerHTML = data.content || '';
} else {
target.innerHTML = message || (0,_api_js__WEBPACK_IMPORTED_MODULE_3__.getStatsI18n)('loadError', 'Request failed.');
}
},
error: err => {
// eslint-disable-next-line no-console
console.error('LP Statistics report:', err);
},
completed: () => handle.showHideLoading(target, 0)
});
}
/**
* Ask the server for the full ( capped ) CSV and download it.
*/
exportCsv(args) {
const content = this.getModalContent();
const target = this.getTarget();
const handle = this.getAjaxHandle();
if (!content || !target || !handle) {
return;
}
const btn = args?.target?.closest(LpStatsReportModal.selectors.elExport);
if (!btn || btn.classList.contains('loading')) {
return;
}
// Clone the current request but hit the CSV callback.
const current = handle.getDataSetCurrent(target);
const dataSend = {
...current,
args: {
...(current.args || {})
},
callback: {
...(current.callback || {}),
method: 'render_report_csv'
}
};
lpAssetsJsPath_utils_js__WEBPACK_IMPORTED_MODULE_1__.lpSetLoadingEl(btn, 1);
handle.fetchAJAX(dataSend, {
success: response => {
const {
status,
data
} = response;
if ('success' === status && data && data.csv) {
this.download(data.filename || 'learnpress-report.csv', data.csv);
}
},
error: err => {
// eslint-disable-next-line no-console
console.error('LP Statistics export:', err);
},
completed: () => lpAssetsJsPath_utils_js__WEBPACK_IMPORTED_MODULE_1__.lpSetLoadingEl(btn, 0)
});
}
/**
* @param {string} filename
* @param {string} csv
*/
download(filename, csv) {
// BOM keeps Excel reading UTF-8 (Vietnamese titles etc.).
const blob = new Blob(['\u{FEFF}' + csv], {
type: 'text/csv;charset=utf-8;'
});
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}
}
const lpStatsReportModal = new LpStatsReportModal();
/***/ },
/***/ "./assets/src/js/admin/statistics/state.js"
/*!*************************************************!*\
!*** ./assets/src/js/admin/statistics/state.js ***!
\*************************************************/
(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ LP_STATS_EXPORT_CSV: () => (/* binding */ LP_STATS_EXPORT_CSV),
/* harmony export */ LP_STATS_FILTER_CHANGED: () => (/* binding */ LP_STATS_FILTER_CHANGED),
/* harmony export */ LP_STATS_RANGE_RESOLVED: () => (/* binding */ LP_STATS_RANGE_RESOLVED),
/* harmony export */ LpStatsState: () => (/* binding */ LpStatsState),
/* harmony export */ lpStatsState: () => (/* binding */ lpStatsState)
/* harmony export */ });
/**
* Statistics dashboard shared filter state.
*
* The singleton is the ONLY mutation path: modules call lpStatsState.set()
* and every tab module re-renders on the 'lp-stats:filter-changed' event.
* Tab-specific deep-link params (e.g. order_status) are read by their own
* tab module — only the four global filters live here.
*
* On every set() the five filters are written back to the URL (replaceState,
* no history spam) so the current view is always copy/paste shareable; other
* query params (page, tab, order_status, …) are preserved untouched.
*
* @since 4.4.2
* @version 1.2.0
*/
const LP_STATS_FILTER_CHANGED = 'lp-stats:filter-changed';
const LP_STATS_EXPORT_CSV = 'lp-stats:export-csv';
// Server-resolved range echoed by a stats payload ( data.range ). Carries the
// authoritative toggle label so the filter bar can reconcile its optimistic one.
const LP_STATS_RANGE_RESOLVED = 'lp-stats:range-resolved';
const COMPARE_DEFAULT = 'previous_period';
class LpStatsState {
constructor() {
const params = new URL(window.location.href).searchParams;
this.filters = {
filtertype: params.get('filtertype') || 'today',
date: params.get('date') || '',
compare: 'previous_year' === params.get('compare') ? 'previous_year' : COMPARE_DEFAULT,
instructor_id: parseInt(params.get('instructor_id'), 10) || 0,
category_id: parseInt(params.get('category_id'), 10) || 0
};
}
get() {
return {
...this.filters
};
}
set(partial = {}) {
this.filters = {
...this.filters,
...partial
};
this.syncUrl();
document.dispatchEvent(new CustomEvent(LP_STATS_FILTER_CHANGED, {
detail: this.get()
}));
}
/**
* Reflect the current filters in the URL without pushing a history entry.
* Defaults (today / empty date / id 0) are dropped to keep URLs clean;
* unrelated params (page, tab, order_status, …) are left as-is.
*/
syncUrl() {
if (!window.history || typeof window.history.replaceState !== 'function') {
return;
}
const url = new URL(window.location.href);
const params = url.searchParams;
const {
filtertype,
date,
compare,
instructor_id: instructorId,
category_id: categoryId
} = this.filters;
this.writeParam(params, 'filtertype', filtertype && filtertype !== 'today' ? filtertype : '');
this.writeParam(params, 'date', date);
this.writeParam(params, 'compare', compare && compare !== COMPARE_DEFAULT ? compare : '');
this.writeParam(params, 'instructor_id', instructorId > 0 ? String(instructorId) : '');
this.writeParam(params, 'category_id', categoryId > 0 ? String(categoryId) : '');
window.history.replaceState(null, '', url.toString());
}
/**
* Set the param when a truthy value is given, otherwise remove it.
*/
writeParam(params, key, value) {
if (value) {
params.set(key, value);
} else {
params.delete(key);
}
}
}
const lpStatsState = new LpStatsState();
/***/ },
/***/ "./assets/src/js/admin/statistics/tab-courses.js"
/*!*******************************************************!*\
!*** ./assets/src/js/admin/statistics/tab-courses.js ***!
\*******************************************************/
(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ LpStatsTabCourses: () => (/* binding */ LpStatsTabCourses),
/* harmony export */ lpStatsTabCourses: () => (/* binding */ lpStatsTabCourses)
/* harmony export */ });
/* harmony import */ var lpAssetsJsPath_utils_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! lpAssetsJsPath/utils.js */ "./assets/src/js/utils.js");
/* harmony import */ var _state_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./state.js */ "./assets/src/js/admin/statistics/state.js");
/* harmony import */ var _api_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./api.js */ "./assets/src/js/admin/statistics/api.js");
/* harmony import */ var _kpi_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./kpi.js */ "./assets/src/js/admin/statistics/kpi.js");
/* harmony import */ var _chart_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./chart.js */ "./assets/src/js/admin/statistics/chart.js");
/* harmony import */ var _data_table_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./data-table.js */ "./assets/src/js/admin/statistics/data-table.js");
/* harmony import */ var _report_modal_js__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ./report-modal.js */ "./assets/src/js/admin/statistics/report-modal.js");
/* harmony import */ var _csv_js__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ./csv.js */ "./assets/src/js/admin/statistics/csv.js");
/**
* Courses tab module.
*
* Fetches the `dashboard` payload and renders KPIs, course performance,
* published-courses chart, health checks, inventory, popups and CSV export.
*
* @since 4.4.2
* @version 1.0.0
*/
const sprintfLite = (template, value) => String(template).replace(/%[ds]/, String(value)).replace(/%%/g, '%');
class LpStatsTabCourses {
static selectors = {
elContainer: '.lp-stats-tab-courses',
elChartCanvas: '#course-chart-content',
elTablePerformance: '.lp-stats-table-course-performance',
elTableInventory: '.lp-stats-table-content-inventory',
elBtnViewAllPerformance: '.lp-stats-view-all-performance',
elPerformanceRow: '.lp-stats-course-performance-row',
elHealthCheckCount: '.lp-stats-health-check__count',
elSkeleton: '.lp-skeleton-animation'
};
static kpiCards = {
published: '.lp-kpi-published',
pending_review: '.lp-kpi-pending-review',
future: '.lp-kpi-future',
enrollments: '.lp-kpi-enrollments',
avg_completion: '.lp-kpi-avg-completion',
courses_without_enrollment: '.lp-kpi-courses-without-enrollment'
};
constructor() {
this.elContainer = null;
this.isRequesting = false;
this.pendingReload = false;
this.tables = {};
}
init() {
this.elContainer = document.querySelector(LpStatsTabCourses.selectors.elContainer);
if (!this.elContainer) {
return;
}
this.events();
this.loadData();
}
events() {
if (LpStatsTabCourses._loadedEvents) {
return;
}
LpStatsTabCourses._loadedEvents = this;
lpAssetsJsPath_utils_js__WEBPACK_IMPORTED_MODULE_0__.eventHandlers('click', [{
selector: LpStatsTabCourses.selectors.elBtnViewAllPerformance,
class: this,
callBack: this.viewAllPerformance.name
}, {
selector: LpStatsTabCourses.selectors.elPerformanceRow,
class: this,
callBack: this.openCourseEdit.name
}]);
document.addEventListener(_state_js__WEBPACK_IMPORTED_MODULE_1__.LP_STATS_FILTER_CHANGED, () => this.loadData());
document.addEventListener(_state_js__WEBPACK_IMPORTED_MODULE_1__.LP_STATS_EXPORT_CSV, () => this.exportTables());
}
toggleSkeletons(show) {
this.elContainer.querySelectorAll(LpStatsTabCourses.selectors.elSkeleton).forEach(el => {
el.style.display = show ? 'block' : 'none';
});
}
loadData() {
if (this.isRequesting) {
this.pendingReload = true;
return;
}
this.isRequesting = true;
(0,_api_js__WEBPACK_IMPORTED_MODULE_2__.lpStatsFetch)('course-statistics', {}, {
before: () => this.toggleSkeletons(true),
success: response => this.render(response.data),
error: err => {
console.error('LP Statistics courses:', err);
this.render(null);
},
completed: () => {
this.toggleSkeletons(false);
this.isRequesting = false;
if (this.pendingReload) {
this.pendingReload = false;
this.loadData();
}
}
});
}
render(data) {
if (!data?.dashboard) {
console.error('LP Statistics courses: dashboard payload missing.');
data = {
chart_data: {},
dashboard: {}
};
}
const dashboard = data.dashboard || {};
this.renderKpis(dashboard.kpis || {});
this.renderTables(dashboard);
// Prefer the scoped chart from the dashboard payload so instructor/category
// changes redraw the chart; fall back to the legacy unscoped series.
this.renderChart(dashboard.chart || data.chart_data || {});
this.renderHealthChecks(dashboard.health_checks || {});
}
renderKpis(kpis) {
Object.entries(LpStatsTabCourses.kpiCards).forEach(([key, selector]) => {
const elCard = this.elContainer.querySelector(selector);
const payload = {
...(kpis[key] || {})
};
if ('published' === key) {
var _payload$added_in_per;
payload.subline = sprintfLite((0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('addedThisPeriod', '%d added this period'), (_payload$added_in_per = payload.added_in_period) !== null && _payload$added_in_per !== void 0 ? _payload$added_in_per : 0);
}
if ('pending_review' === key) {
payload.subline = (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('needsInstructorAction', 'Needs instructor action');
}
if ('future' === key) {
payload.subline = (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('scheduledReleases', 'Scheduled releases');
}
if ('avg_completion' === key) {
var _ref, _payload$target;
if ('number' === typeof payload.value) {
payload.formatted = `${payload.value}%`;
}
payload.subline = sprintfLite((0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('targetPercent', 'Target: %s%%'), (_ref = (_payload$target = payload.target) !== null && _payload$target !== void 0 ? _payload$target : (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsConfig)().completionTarget) !== null && _ref !== void 0 ? _ref : 0);
}
(0,_kpi_js__WEBPACK_IMPORTED_MODULE_3__.renderKpi)(elCard, payload);
if ('avg_completion' === key) {
this.renderCompletionProgress(elCard, payload);
}
});
}
renderCompletionProgress(elCard, payload) {
const elBar = elCard?.querySelector('.lp-kpi-progress__bar');
if (!elBar) {
return;
}
const value = Number(payload.value || 0);
const target = Number(payload.target || (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsConfig)().completionTarget || 0);
const width = target > 0 ? Math.min(100, value / target * 100) : 0;
elBar.style.width = `${width}%`;
}
renderChart(chartData) {
(0,_chart_js__WEBPACK_IMPORTED_MODULE_4__.renderLineChart)(LpStatsTabCourses.selectors.elChartCanvas, {
labels: chartData.labels || [],
datasets: [{
label: chartData.line_label || (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('publishedCourses', 'Published courses'),
data: chartData.data || [],
yAxisID: 'y'
}],
xLabel: chartData.x_label || '',
granularity: chartData.granularity || ''
}, {
yCurrency: false
});
}
completionBadge(rate) {
var _completionBadge$gree, _completionBadge$yell;
if (null == rate) {
return '';
}
const {
completionBadge = {}
} = (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsConfig)();
const green = (_completionBadge$gree = completionBadge.green) !== null && _completionBadge$gree !== void 0 ? _completionBadge$gree : 60;
const yellow = (_completionBadge$yell = completionBadge.yellow) !== null && _completionBadge$yell !== void 0 ? _completionBadge$yell : 40;
if (rate >= green) {
return 'green';
}
return rate >= yellow ? 'yellow' : 'red';
}
performanceColumns() {
return [{
key: 'name',
label: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('course', 'Course')
}, {
key: 'instructor',
label: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('instructor', 'Instructor')
}, {
key: 'revenue_formatted',
label: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('revenue', 'Revenue'),
csv: row => row.revenue
}, {
key: 'enrollments',
label: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('enrollments', 'Enrollments')
}, {
key: 'completion_rate',
label: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('completion', 'Completion'),
format: value => null == value ? '-' : `${value}%`,
badge: row => this.completionBadge(row.completion_rate)
}];
}
inventoryLabel(key) {
const labels = {
courses: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('courses', 'Courses'),
lessons: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('lessons', 'Lessons'),
quizzes: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('quizzes', 'Quizzes'),
assignments: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('assignments', 'Assignments')
};
return labels[key] || String(key || '');
}
inventoryStatusLabel(key) {
const labels = {
publish: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('published', 'Published'),
pending: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('pending', 'Pending'),
future: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('future', 'Future'),
draft: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('drafts', 'Drafts'),
total: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('total', 'Total')
};
return labels[key] || String(key || '');
}
inventoryRows(inventory = {}) {
return Object.entries(inventory).map(([type, counts]) => ({
type,
label: this.inventoryLabel(type),
...(counts || {})
}));
}
inventoryColumns(rows = []) {
const preferred = ['publish', 'pending', 'future', 'draft', 'total'];
const statusKeys = preferred.filter(key => rows.some(row => Object.prototype.hasOwnProperty.call(row, key)));
return [{
key: 'label',
label: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('content', 'Content')
}, ...statusKeys.map(key => ({
key,
label: this.inventoryStatusLabel(key),
csv: row => row[key]
}))];
}
renderTables(dashboard) {
const performanceRows = dashboard.performance || [];
const performanceHandle = (0,_data_table_js__WEBPACK_IMPORTED_MODULE_5__.renderDataTable)(this.elContainer.querySelector(LpStatsTabCourses.selectors.elTablePerformance), this.performanceColumns(), performanceRows, {
emptyText: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('noCoursePerformance', 'No course performance data in this period.')
});
this.tables.performance = performanceHandle;
this.decoratePerformanceRows(performanceRows);
const inventoryRows = this.inventoryRows(dashboard.inventory || {});
this.tables.inventory = (0,_data_table_js__WEBPACK_IMPORTED_MODULE_5__.renderDataTable)(this.elContainer.querySelector(LpStatsTabCourses.selectors.elTableInventory), this.inventoryColumns(inventoryRows), inventoryRows);
}
decoratePerformanceRows(rows = []) {
const tableRows = this.elContainer.querySelectorAll(`${LpStatsTabCourses.selectors.elTablePerformance} tbody tr`);
rows.forEach((row, index) => {
const tableRow = tableRows[index];
if (tableRow && row.edit_link) {
tableRow.classList.add('lp-stats-course-performance-row');
tableRow.dataset.editLink = row.edit_link;
}
});
}
renderHealthChecks(healthChecks) {
this.elContainer.querySelectorAll(LpStatsTabCourses.selectors.elHealthCheckCount).forEach(elCount => {
var _healthChecks$check;
const check = elCount.dataset.check;
elCount.textContent = String((_healthChecks$check = healthChecks[check]) !== null && _healthChecks$check !== void 0 ? _healthChecks$check : 0);
});
}
openCourseEdit(args) {
const row = args.target.closest(LpStatsTabCourses.selectors.elPerformanceRow);
if (!row || !this.elContainer.contains(row)) {
return;
}
const editLink = row.dataset.editLink || '';
const adminUrl = (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsConfig)().adminUrl || '';
if (editLink && adminUrl && editLink.startsWith(adminUrl)) {
window.location.href = editLink;
}
}
viewAllPerformance(args) {
const btn = args.target.closest(LpStatsTabCourses.selectors.elBtnViewAllPerformance);
if (!btn || !this.elContainer.contains(btn)) {
return;
}
_report_modal_js__WEBPACK_IMPORTED_MODULE_6__.lpStatsReportModal.open({
report: 'course_performance',
title: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('coursePerformance', 'Course performance'),
tableId: 'course-performance'
});
}
exportTables() {
Object.entries(this.tables).forEach(([tableId, handle]) => {
if (handle && handle.rows.length) {
(0,_csv_js__WEBPACK_IMPORTED_MODULE_7__.exportCsv)((0,_csv_js__WEBPACK_IMPORTED_MODULE_7__.buildCsvFilename)('courses', tableId), handle.columns, handle.rows);
}
});
}
}
const lpStatsTabCourses = new LpStatsTabCourses();
/***/ },
/***/ "./assets/src/js/admin/statistics/tab-instructors.js"
/*!***********************************************************!*\
!*** ./assets/src/js/admin/statistics/tab-instructors.js ***!
\***********************************************************/
(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ LpStatsTabInstructors: () => (/* binding */ LpStatsTabInstructors),
/* harmony export */ lpStatsTabInstructors: () => (/* binding */ lpStatsTabInstructors)
/* harmony export */ });
/* harmony import */ var lpAssetsJsPath_utils_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! lpAssetsJsPath/utils.js */ "./assets/src/js/utils.js");
/* harmony import */ var _state_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./state.js */ "./assets/src/js/admin/statistics/state.js");
/* harmony import */ var _api_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./api.js */ "./assets/src/js/admin/statistics/api.js");
/* harmony import */ var _kpi_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./kpi.js */ "./assets/src/js/admin/statistics/kpi.js");
/* harmony import */ var _chart_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./chart.js */ "./assets/src/js/admin/statistics/chart.js");
/* harmony import */ var _data_table_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./data-table.js */ "./assets/src/js/admin/statistics/data-table.js");
/* harmony import */ var _report_modal_js__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ./report-modal.js */ "./assets/src/js/admin/statistics/report-modal.js");
/* harmony import */ var _csv_js__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ./csv.js */ "./assets/src/js/admin/statistics/csv.js");
/**
* Instructors tab module.
*
* Fetches the `dashboard` payload and renders KPIs, the operations widget,
* instructor performance + course watchlist tables, per-instructor report
* popup, and CSV export.
*
* @since 4.4.2
* @version 1.0.0
*/
const sprintfLite = (template, value) => String(template).replace(/%[ds]/, String(value)).replace(/%%/g, '%');
class LpStatsTabInstructors {
static selectors = {
elContainer: '.lp-stats-tab-instructors',
elChartCanvas: '#instructor-chart-content',
elTablePerformance: '.lp-stats-table-instructor-performance',
elTableWatchlist: '.lp-stats-table-instructor-watchlist',
elBtnViewAllInstructors: '.lp-stats-view-all-instructors',
elPerformanceRow: '.lp-stats-instructor-performance-row',
elOperationsRow: '.lp-stats-operations__row',
elSkeleton: '.lp-skeleton-animation'
};
static kpiCards = {
active_instructors: '.lp-kpi-active-instructors',
instructor_revenue: '.lp-kpi-instructor-revenue',
courses_managed: '.lp-kpi-courses-managed',
students_reached: '.lp-kpi-students-reached',
avg_completion: '.lp-kpi-avg-completion',
needs_review: '.lp-kpi-needs-review'
};
constructor() {
this.elContainer = null;
this.isRequesting = false;
this.pendingReload = false;
this.tables = {};
}
init() {
this.elContainer = document.querySelector(LpStatsTabInstructors.selectors.elContainer);
if (!this.elContainer) {
return;
}
this.events();
this.loadData();
}
events() {
if (LpStatsTabInstructors._loadedEvents) {
return;
}
LpStatsTabInstructors._loadedEvents = this;
lpAssetsJsPath_utils_js__WEBPACK_IMPORTED_MODULE_0__.eventHandlers('click', [{
selector: LpStatsTabInstructors.selectors.elBtnViewAllInstructors,
class: this,
callBack: this.viewAllInstructors.name
}, {
selector: LpStatsTabInstructors.selectors.elPerformanceRow,
class: this,
callBack: this.openInstructorReport.name
}]);
document.addEventListener(_state_js__WEBPACK_IMPORTED_MODULE_1__.LP_STATS_FILTER_CHANGED, () => this.loadData());
document.addEventListener(_state_js__WEBPACK_IMPORTED_MODULE_1__.LP_STATS_EXPORT_CSV, () => this.exportTables());
}
toggleSkeletons(show) {
this.elContainer.querySelectorAll(LpStatsTabInstructors.selectors.elSkeleton).forEach(el => {
el.style.display = show ? 'block' : 'none';
});
}
loadData() {
if (this.isRequesting) {
this.pendingReload = true;
return;
}
this.isRequesting = true;
(0,_api_js__WEBPACK_IMPORTED_MODULE_2__.lpStatsFetch)('instructor-statistics', {}, {
before: () => this.toggleSkeletons(true),
success: response => this.render(response.data),
error: err => {
console.error('LP Statistics instructors:', err);
this.render(null);
},
completed: () => {
this.toggleSkeletons(false);
this.isRequesting = false;
if (this.pendingReload) {
this.pendingReload = false;
this.loadData();
}
}
});
}
render(data) {
if (!data?.dashboard) {
console.error('LP Statistics instructors: dashboard payload missing.');
data = {
chart_data: {},
dashboard: {}
};
}
const dashboard = data.dashboard || {};
this.renderKpis(dashboard.kpis || {});
this.renderChart(data.chart_data || {});
this.renderOperations(dashboard.operations || {});
this.renderTables(dashboard);
}
renderKpis(kpis) {
Object.entries(LpStatsTabInstructors.kpiCards).forEach(([key, selector]) => {
const elCard = this.elContainer.querySelector(selector);
const payload = {
...(kpis[key] || {})
};
if ('active_instructors' === key) {
var _payload$total;
payload.subline = sprintfLite((0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('ofTotalInstructors', '%d total'), (_payload$total = payload.total) !== null && _payload$total !== void 0 ? _payload$total : 0);
}
if ('instructor_revenue' === key && null != payload.contribution_pct) {
payload.subline = sprintfLite((0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('ofNetSales', '%s%% of net sales'), payload.contribution_pct);
}
if ('avg_completion' === key && 'number' === typeof payload.value) {
payload.formatted = `${payload.value}%`;
}
(0,_kpi_js__WEBPACK_IMPORTED_MODULE_3__.renderKpi)(elCard, payload);
});
}
renderChart(chartData) {
(0,_chart_js__WEBPACK_IMPORTED_MODULE_4__.renderLineChart)(LpStatsTabInstructors.selectors.elChartCanvas, {
labels: chartData.labels || [],
datasets: [{
label: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('revenue', 'Revenue'),
data: chartData.revenue || [],
yAxisID: 'y'
}, {
label: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('enrollments', 'Enrollments'),
data: chartData.enrollments || [],
yAxisID: 'y1'
}],
xLabel: chartData.x_label || '',
granularity: chartData.granularity || ''
});
}
renderOperations(operations) {
this.elContainer.querySelectorAll(LpStatsTabInstructors.selectors.elOperationsRow).forEach(elRow => {
const op = elRow.dataset.op;
const elValue = elRow.querySelector('.lp-stats-operations__value');
const elName = elRow.querySelector('.lp-stats-operations__name');
const data = operations[op];
if (elName) {
elName.textContent = '';
}
if (null == data) {
if (elValue) {
elValue.textContent = '–';
}
return;
}
// Scalar operations (counts) vs highlight objects ({ name, value }).
if ('object' === typeof data) {
if (elValue) {
var _data$value_formatted, _data$value;
elValue.textContent = (_data$value_formatted = data.value_formatted) !== null && _data$value_formatted !== void 0 ? _data$value_formatted : 'number' === typeof data.value && op === 'top_completion' ? `${data.value}%` : String((_data$value = data.value) !== null && _data$value !== void 0 ? _data$value : '');
}
if (elName) {
elName.textContent = data.name || '';
}
} else if (elValue) {
elValue.textContent = String(data);
}
});
}
completionBadge(rate) {
var _completionBadge$gree, _completionBadge$yell;
if (null == rate) {
return '';
}
const {
completionBadge = {}
} = (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsConfig)();
const green = (_completionBadge$gree = completionBadge.green) !== null && _completionBadge$gree !== void 0 ? _completionBadge$gree : 60;
const yellow = (_completionBadge$yell = completionBadge.yellow) !== null && _completionBadge$yell !== void 0 ? _completionBadge$yell : 40;
if (rate >= green) {
return 'green';
}
return rate >= yellow ? 'yellow' : 'red';
}
riskLabel(slug) {
const labels = {
high: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('riskHigh', 'High'),
medium: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('riskMedium', 'Medium'),
healthy: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('riskHealthy', 'Healthy')
};
return labels[slug] || String(slug || '');
}
actionLabel(slug) {
const {
watchlistActions = {}
} = (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsConfig)().i18n || {};
return watchlistActions[slug] || String(slug || '');
}
performanceColumns() {
return [{
key: 'name',
label: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('instructor', 'Instructor')
}, {
key: 'courses',
label: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('courses', 'Courses')
}, {
key: 'students',
label: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('students', 'Students')
}, {
key: 'revenue_formatted',
label: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('revenue', 'Revenue'),
csv: row => row.revenue
}, {
key: 'avg_completion',
label: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('completion', 'Completion'),
format: value => null == value ? '–' : `${value}%`,
badge: row => this.completionBadge(row.avg_completion)
}];
}
watchlistColumns() {
return [{
key: 'name',
label: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('course', 'Course')
}, {
key: 'instructor',
label: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('instructor', 'Instructor')
}, {
key: 'completion_rate',
label: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('completion', 'Completion'),
format: value => null == value ? '–' : `${value}%`
}, {
key: 'risk',
label: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('risk', 'Risk'),
// Emoji lives in CSS pseudo-content on .lp-risk--{slug}; text stays clean.
format: value => {
const span = document.createElement('span');
span.className = `lp-badge lp-risk lp-risk--${value}`;
span.textContent = this.riskLabel(value);
return span;
},
csv: row => this.riskLabel(row.risk)
}, {
key: 'action',
label: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('actionRequired', 'Action required'),
format: value => this.actionLabel(value),
csv: row => this.actionLabel(row.action)
}];
}
renderTables(dashboard) {
const performanceRows = dashboard.performance || [];
this.tables.performance = (0,_data_table_js__WEBPACK_IMPORTED_MODULE_5__.renderDataTable)(this.elContainer.querySelector(LpStatsTabInstructors.selectors.elTablePerformance), this.performanceColumns(), performanceRows, {
emptyText: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('noInstructorData', 'No instructor data in this period.')
});
this.decoratePerformanceRows(performanceRows);
this.tables.watchlist = (0,_data_table_js__WEBPACK_IMPORTED_MODULE_5__.renderDataTable)(this.elContainer.querySelector(LpStatsTabInstructors.selectors.elTableWatchlist), this.watchlistColumns(), dashboard.watchlist || []);
}
decoratePerformanceRows(rows = []) {
const scopedInstructor = _state_js__WEBPACK_IMPORTED_MODULE_1__.lpStatsState.get().instructor_id;
const tableRows = this.elContainer.querySelectorAll(`${LpStatsTabInstructors.selectors.elTablePerformance} tbody tr`);
rows.forEach((row, index) => {
const tableRow = tableRows[index];
if (!tableRow || !row.instructor_id) {
return;
}
tableRow.classList.add('lp-stats-instructor-performance-row');
tableRow.dataset.instructorId = String(row.instructor_id);
tableRow.dataset.instructorName = row.name || '';
if (scopedInstructor && scopedInstructor === row.instructor_id) {
tableRow.classList.add('is-highlighted');
}
});
}
openInstructorReport(args) {
const row = args.target.closest(LpStatsTabInstructors.selectors.elPerformanceRow);
if (!row || !this.elContainer.contains(row)) {
return;
}
const instructorId = parseInt(row.dataset.instructorId, 10) || 0;
if (instructorId <= 0) {
return;
}
const instructorName = row.dataset.instructorName || (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('instructorReport', 'Instructor report');
_report_modal_js__WEBPACK_IMPORTED_MODULE_6__.lpStatsReportModal.open({
report: 'instructor_report',
title: instructorName,
tableId: `instructor-${instructorId}`,
args: {
instructor_id: instructorId
}
});
}
viewAllInstructors(args) {
const btn = args.target.closest(LpStatsTabInstructors.selectors.elBtnViewAllInstructors);
if (!btn || !this.elContainer.contains(btn)) {
return;
}
_report_modal_js__WEBPACK_IMPORTED_MODULE_6__.lpStatsReportModal.open({
report: 'instructor_performance',
title: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('instructorPerformance', 'Instructor performance'),
tableId: 'instructor-performance'
});
}
exportTables() {
Object.entries(this.tables).forEach(([tableId, handle]) => {
if (handle && handle.rows.length) {
(0,_csv_js__WEBPACK_IMPORTED_MODULE_7__.exportCsv)((0,_csv_js__WEBPACK_IMPORTED_MODULE_7__.buildCsvFilename)('instructors', tableId), handle.columns, handle.rows);
}
});
}
}
const lpStatsTabInstructors = new LpStatsTabInstructors();
/***/ },
/***/ "./assets/src/js/admin/statistics/tab-orders.js"
/*!******************************************************!*\
!*** ./assets/src/js/admin/statistics/tab-orders.js ***!
\******************************************************/
(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ LpStatsTabOrders: () => (/* binding */ LpStatsTabOrders),
/* harmony export */ lpStatsTabOrders: () => (/* binding */ lpStatsTabOrders)
/* harmony export */ });
/* harmony import */ var lpAssetsJsPath_utils_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! lpAssetsJsPath/utils.js */ "./assets/src/js/utils.js");
/* harmony import */ var _state_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./state.js */ "./assets/src/js/admin/statistics/state.js");
/* harmony import */ var _api_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./api.js */ "./assets/src/js/admin/statistics/api.js");
/* harmony import */ var _kpi_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./kpi.js */ "./assets/src/js/admin/statistics/kpi.js");
/* harmony import */ var _chart_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./chart.js */ "./assets/src/js/admin/statistics/chart.js");
/* harmony import */ var _data_table_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./data-table.js */ "./assets/src/js/admin/statistics/data-table.js");
/* harmony import */ var _report_modal_js__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ./report-modal.js */ "./assets/src/js/admin/statistics/report-modal.js");
/* harmony import */ var _csv_js__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ./csv.js */ "./assets/src/js/admin/statistics/csv.js");
/**
* Orders tab module.
*
* Fetches the `dashboard` payload and renders KPIs, completed-orders chart,
* top sold courses, recent exceptions, popups and CSV export.
*
* @since 4.4.2
* @version 1.0.0
*/
const sprintfLite = (template, value) => String(template).replace(/%[ds]/, String(value)).replace(/%%/g, '%');
class LpStatsTabOrders {
static selectors = {
elContainer: '.lp-stats-tab-orders',
elChartCanvas: '#orders-chart-content',
elTableTopSold: '.lp-stats-table-top-sold-courses',
elTableExceptions: '.lp-stats-table-order-exceptions',
elBtnViewAllTopSold: '.lp-stats-view-all-top-sold',
elBtnViewAllExceptions: '.lp-stats-view-all-exceptions',
elPaymentHealthRow: '.lp-stats-payment-health__row',
elSkeleton: '.lp-skeleton-animation'
};
static kpiCards = {
net_sales: '.lp-kpi-net-sales',
completed_orders: '.lp-kpi-completed-orders',
processing: '.lp-kpi-processing',
pending: '.lp-kpi-pending',
cancelled_failed: '.lp-kpi-cancelled-failed',
paid_courses_sold: '.lp-kpi-paid-courses-sold'
};
static orderStatusAllowlist = ['completed', 'processing', 'pending', 'cancelled', 'failed'];
constructor() {
this.elContainer = null;
this.isRequesting = false;
this.pendingReload = false;
this.tables = {};
this.orderStatusFilter = '';
}
init() {
this.elContainer = document.querySelector(LpStatsTabOrders.selectors.elContainer);
if (!this.elContainer) {
return;
}
this.orderStatusFilter = this.readOrderStatus();
this.events();
this.loadData();
}
events() {
if (LpStatsTabOrders._loadedEvents) {
return;
}
LpStatsTabOrders._loadedEvents = this;
lpAssetsJsPath_utils_js__WEBPACK_IMPORTED_MODULE_0__.eventHandlers('click', [{
selector: LpStatsTabOrders.selectors.elBtnViewAllTopSold,
class: this,
callBack: this.viewAllTopSold.name
}, {
selector: LpStatsTabOrders.selectors.elBtnViewAllExceptions,
class: this,
callBack: this.viewAllExceptions.name
}]);
document.addEventListener(_state_js__WEBPACK_IMPORTED_MODULE_1__.LP_STATS_FILTER_CHANGED, () => this.loadData());
document.addEventListener(_state_js__WEBPACK_IMPORTED_MODULE_1__.LP_STATS_EXPORT_CSV, () => this.exportTables());
}
readOrderStatus() {
const status = new URL(window.location.href).searchParams.get('order_status');
return LpStatsTabOrders.orderStatusAllowlist.includes(status) ? status : '';
}
toggleSkeletons(show) {
this.elContainer.querySelectorAll(LpStatsTabOrders.selectors.elSkeleton).forEach(el => {
el.style.display = show ? 'block' : 'none';
});
}
loadData() {
if (this.isRequesting) {
this.pendingReload = true;
return;
}
this.isRequesting = true;
(0,_api_js__WEBPACK_IMPORTED_MODULE_2__.lpStatsFetch)('order-statistics', {}, {
before: () => this.toggleSkeletons(true),
success: response => this.render(response.data),
error: err => {
console.error('LP Statistics orders:', err);
this.render(null);
},
completed: () => {
this.toggleSkeletons(false);
this.isRequesting = false;
if (this.pendingReload) {
this.pendingReload = false;
this.loadData();
}
}
});
}
render(data) {
if (!data?.dashboard) {
console.error('LP Statistics orders: dashboard payload missing.');
data = {
chart_data: {},
dashboard: {}
};
}
const dashboard = data.dashboard || {};
this.renderKpis(dashboard.kpis || {});
this.renderChart(data.chart_data || {});
this.renderPaymentHealth(dashboard.order_health || {});
this.renderTables(dashboard);
this.highlightStatus();
}
renderKpis(kpis) {
Object.entries(LpStatsTabOrders.kpiCards).forEach(([key, selector]) => {
const elCard = this.elContainer.querySelector(selector);
const payload = {
...(kpis[key] || {})
};
if ('completed_orders' === key && payload.aov_formatted) {
payload.subline = sprintfLite((0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('aov', 'Avg. order value: %s'), payload.aov_formatted);
}
if ('processing' === key) {
payload.subline = (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('needsFulfillmentReview', 'Needs fulfillment review');
}
if ('pending' === key) {
payload.subline = (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('awaitingPayment', 'Awaiting payment');
}
if ('cancelled_failed' === key && null != payload.rate_pct) {
payload.subline = sprintfLite((0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('exceptionRate', '%s%% of all orders'), payload.rate_pct);
}
(0,_kpi_js__WEBPACK_IMPORTED_MODULE_3__.renderKpi)(elCard, payload);
});
}
renderPaymentHealth(orderHealth) {
this.elContainer.querySelectorAll(LpStatsTabOrders.selectors.elPaymentHealthRow).forEach(elRow => {
const status = elRow.dataset.status;
const elCount = elRow.querySelector('.lp-stats-payment-health__count');
if (elCount) {
var _orderHealth$status;
elCount.textContent = String((_orderHealth$status = orderHealth[status]) !== null && _orderHealth$status !== void 0 ? _orderHealth$status : 0);
}
});
}
renderChart(chartData) {
(0,_chart_js__WEBPACK_IMPORTED_MODULE_4__.renderLineChart)(LpStatsTabOrders.selectors.elChartCanvas, {
labels: chartData.labels || [],
datasets: [{
label: chartData.line_label || (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('orders', 'Orders'),
data: chartData.data || [],
yAxisID: 'y'
}],
xLabel: chartData.x_label || '',
granularity: chartData.granularity || ''
}, {
yCurrency: false
});
}
statusLabel(slug) {
const labels = {
healthy: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('healthy', 'Healthy'),
watch_completion: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('watchCompletion', 'Watch completion'),
high_failed_quizzes: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('highFailedQuizzes', 'High failed quizzes')
};
return labels[slug] || String(slug || '');
}
statusBadge(slug) {
if ('high_failed_quizzes' === slug) {
return 'red';
}
if ('watch_completion' === slug) {
return 'yellow';
}
return 'green';
}
severityLabel(severity) {
const labels = {
high: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('high', 'High'),
medium: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('medium', 'Medium'),
low: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('low', 'Low')
};
return labels[severity] || String(severity || '');
}
severityBadge(severity) {
if ('high' === severity) {
return 'red';
}
if ('medium' === severity) {
return 'yellow';
}
return 'grey';
}
topSoldColumns() {
return [{
key: 'name',
label: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('course', 'Course')
}, {
key: 'revenue_formatted',
label: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('revenue', 'Revenue'),
csv: row => row.revenue
}, {
key: 'orders',
label: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('orders', 'Orders')
}, {
key: 'aov_formatted',
label: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('aovShort', 'AOV'),
csv: row => row.aov
}, {
key: 'status_label',
label: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('status', 'Status'),
format: value => this.statusLabel(value),
badge: row => this.statusBadge(row.status_label)
}];
}
exceptionColumns() {
return [{
key: 'order_id',
label: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('orderId', 'Order ID'),
format: (value, row) => {
if (!row.edit_link) {
return value;
}
const link = document.createElement('a');
link.href = row.edit_link;
link.textContent = `#${value}`;
return link;
},
csv: row => row.order_id
}, {
key: 'student',
label: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('student', 'Student')
}, {
key: 'course',
label: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('course', 'Course')
}, {
key: 'issue',
label: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('issue', 'Issue')
}, {
key: 'date',
label: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('date', 'Date')
}, {
key: 'severity',
label: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('severity', 'Severity'),
format: value => this.severityLabel(value),
badge: row => this.severityBadge(row.severity)
}];
}
filterExceptions(rows = []) {
if (!['cancelled', 'failed'].includes(this.orderStatusFilter)) {
return rows;
}
return rows.filter(row => row.status === this.orderStatusFilter);
}
renderTables(dashboard) {
this.tables['top-sold-courses'] = (0,_data_table_js__WEBPACK_IMPORTED_MODULE_5__.renderDataTable)(this.elContainer.querySelector(LpStatsTabOrders.selectors.elTableTopSold), this.topSoldColumns(), dashboard.top_sold_courses || []);
const exceptionRows = this.filterExceptions(dashboard.exceptions || []);
const exceptionHandle = (0,_data_table_js__WEBPACK_IMPORTED_MODULE_5__.renderDataTable)(this.elContainer.querySelector(LpStatsTabOrders.selectors.elTableExceptions), this.exceptionColumns(), exceptionRows, {
emptyText: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('noOrderExceptions', 'No failed or cancelled orders in this period.')
});
this.tables.exceptions = {
...exceptionHandle,
rows: exceptionRows,
allRows: dashboard.exceptions || []
};
}
highlightStatus() {
Object.values(LpStatsTabOrders.kpiCards).forEach(selector => {
const elCard = this.elContainer.querySelector(selector);
if (elCard) {
elCard.classList.remove('is-highlighted');
}
});
const statusMap = {
completed: 'completed_orders',
processing: 'processing',
pending: 'pending',
cancelled: 'cancelled_failed',
failed: 'cancelled_failed'
};
const kpiKey = statusMap[this.orderStatusFilter];
const selector = kpiKey ? LpStatsTabOrders.kpiCards[kpiKey] : '';
const elCard = selector ? this.elContainer.querySelector(selector) : null;
if (elCard) {
elCard.classList.add('is-highlighted');
}
}
viewAllTopSold(args) {
const btn = args.target.closest(LpStatsTabOrders.selectors.elBtnViewAllTopSold);
if (!btn || !this.elContainer.contains(btn)) {
return;
}
_report_modal_js__WEBPACK_IMPORTED_MODULE_6__.lpStatsReportModal.open({
report: 'top_sold_courses',
title: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('topSoldCourses', 'Top sold courses'),
tableId: 'top-sold-courses'
});
}
viewAllExceptions(args) {
const btn = args.target.closest(LpStatsTabOrders.selectors.elBtnViewAllExceptions);
if (!btn || !this.elContainer.contains(btn)) {
return;
}
// The cancelled/failed deep-link is pushed to the server so pagination
// totals match the rows shown ( no more client-side filterExceptions ).
_report_modal_js__WEBPACK_IMPORTED_MODULE_6__.lpStatsReportModal.open({
report: 'exceptions',
title: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('orderExceptions', 'Recent order exceptions'),
tableId: 'exceptions',
orderStatus: ['cancelled', 'failed'].includes(this.orderStatusFilter) ? this.orderStatusFilter : ''
});
}
exportTables() {
Object.entries(this.tables).forEach(([tableId, handle]) => {
if (handle && handle.rows.length) {
(0,_csv_js__WEBPACK_IMPORTED_MODULE_7__.exportCsv)((0,_csv_js__WEBPACK_IMPORTED_MODULE_7__.buildCsvFilename)('orders', tableId), handle.columns, handle.rows);
}
});
}
}
const lpStatsTabOrders = new LpStatsTabOrders();
/***/ },
/***/ "./assets/src/js/admin/statistics/tab-overview.js"
/*!********************************************************!*\
!*** ./assets/src/js/admin/statistics/tab-overview.js ***!
\********************************************************/
(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ LpStatsTabOverview: () => (/* binding */ LpStatsTabOverview),
/* harmony export */ lpStatsTabOverview: () => (/* binding */ lpStatsTabOverview)
/* harmony export */ });
/* harmony import */ var lpAssetsJsPath_utils_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! lpAssetsJsPath/utils.js */ "./assets/src/js/utils.js");
/* harmony import */ var _state_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./state.js */ "./assets/src/js/admin/statistics/state.js");
/* harmony import */ var _api_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./api.js */ "./assets/src/js/admin/statistics/api.js");
/* harmony import */ var _kpi_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./kpi.js */ "./assets/src/js/admin/statistics/kpi.js");
/* harmony import */ var _chart_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./chart.js */ "./assets/src/js/admin/statistics/chart.js");
/* harmony import */ var _data_table_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./data-table.js */ "./assets/src/js/admin/statistics/data-table.js");
/* harmony import */ var _report_modal_js__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ./report-modal.js */ "./assets/src/js/admin/statistics/report-modal.js");
/* harmony import */ var _csv_js__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ./csv.js */ "./assets/src/js/admin/statistics/csv.js");
/**
* Overview tab module — fetches the `dashboard` payload and renders
* KPIs, dual-line chart, funnel, tables, order health and health checks.
*
* Listens to lp-stats:filter-changed; never mutates state itself.
*
* @since 4.4.2
* @version 1.0.0
*/
const sprintfLite = (template, value) => String(template).replace(/%[ds]/, String(value)).replace(/%%/g, '%');
class LpStatsTabOverview {
static selectors = {
elContainer: '.lp-stats-tab-overview',
elChartCanvas: '#net-sales-chart-content',
elFunnelStep: '.lp-stats-funnel__step',
elTableTopCourses: '.lp-stats-table-top-courses',
elTableInstructors: '.lp-stats-table-instructors',
elBtnViewAllCourses: '.lp-stats-view-all-courses',
elOrderHealthBox: '.lp-stats-order-health .lp-stats-health-box',
elHealthCheckCount: '.lp-stats-health-check__count',
elSkeleton: '.lp-skeleton-animation'
};
static kpiCards = {
net_sales: '.lp-kpi-net-sales',
completed_orders: '.lp-kpi-completed-orders',
enrollments: '.lp-kpi-enrollments',
completion_rate: '.lp-kpi-completion-rate',
active_learners: '.lp-kpi-active-learners',
failed_orders: '.lp-kpi-failed-orders'
};
constructor() {
this.elContainer = null;
this.isRequesting = false;
this.pendingReload = false;
this.tables = {};
}
init() {
this.elContainer = document.querySelector(LpStatsTabOverview.selectors.elContainer);
if (!this.elContainer) {
return;
}
this.events();
this.loadData();
}
events() {
if (LpStatsTabOverview._loadedEvents) {
return;
}
LpStatsTabOverview._loadedEvents = this;
lpAssetsJsPath_utils_js__WEBPACK_IMPORTED_MODULE_0__.eventHandlers('click', [{
selector: LpStatsTabOverview.selectors.elBtnViewAllCourses,
class: this,
callBack: this.viewAllCourses.name
}]);
document.addEventListener(_state_js__WEBPACK_IMPORTED_MODULE_1__.LP_STATS_FILTER_CHANGED, () => this.loadData());
document.addEventListener(_state_js__WEBPACK_IMPORTED_MODULE_1__.LP_STATS_EXPORT_CSV, () => this.exportTables());
}
toggleSkeletons(show) {
this.elContainer.querySelectorAll(LpStatsTabOverview.selectors.elSkeleton).forEach(el => {
el.style.display = show ? 'block' : 'none';
});
}
loadData() {
if (this.isRequesting) {
// Latest filter wins: re-run once the in-flight request finishes.
this.pendingReload = true;
return;
}
this.isRequesting = true;
(0,_api_js__WEBPACK_IMPORTED_MODULE_2__.lpStatsFetch)('overviews-statistics', {}, {
before: () => this.toggleSkeletons(true),
success: response => this.render(response.data?.dashboard),
error: err => {
console.error('LP Statistics overview:', err);
this.render(null);
},
completed: () => {
this.toggleSkeletons(false);
this.isRequesting = false;
if (this.pendingReload) {
this.pendingReload = false;
this.loadData();
}
}
});
}
/**
* @param {Object|null} dashboard `dashboard` key of the response; null/missing
* renders empty states, never throws.
*/
render(dashboard) {
if (!dashboard) {
console.error('LP Statistics overview: dashboard payload missing.');
dashboard = {};
}
this.renderKpis(dashboard.kpis || {});
this.renderChart(dashboard.chart || {});
this.renderFunnel(dashboard.funnel || {});
this.renderTables(dashboard);
this.renderOrderHealth(dashboard.order_health || {});
this.renderHealthChecks(dashboard.health_checks || {});
}
renderKpis(kpis) {
const i18n = (key, fallback) => (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)(key, fallback);
Object.entries(LpStatsTabOverview.kpiCards).forEach(([key, selector]) => {
const elCard = this.elContainer.querySelector(selector);
const payload = {
...(kpis[key] || {})
};
if ('completed_orders' === key && payload.aov_formatted) {
payload.subline = sprintfLite(i18n('aov', 'Avg. order value: %s'), payload.aov_formatted);
}
if ('completion_rate' === key) {
var _payload$courses_belo;
if ('number' === typeof payload.value) {
payload.formatted = `${payload.value}%`;
}
payload.subline = sprintfLite(i18n('belowTarget', '%d below completion target'), (_payload$courses_belo = payload.courses_below_target) !== null && _payload$courses_belo !== void 0 ? _payload$courses_belo : 0);
}
if ('failed_orders' === key && null != payload.fail_rate_pct) {
payload.subline = sprintfLite(i18n('failRate', '%s%% of all orders'), payload.fail_rate_pct);
}
(0,_kpi_js__WEBPACK_IMPORTED_MODULE_3__.renderKpi)(elCard, payload);
});
}
renderChart(chart) {
(0,_chart_js__WEBPACK_IMPORTED_MODULE_4__.renderLineChart)(LpStatsTabOverview.selectors.elChartCanvas, {
labels: chart.labels || [],
datasets: [{
label: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('revenue', 'Revenue'),
data: chart.revenue || [],
yAxisID: 'y'
}, {
label: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('enrollments', 'Enrollments'),
data: chart.enrollments || [],
yAxisID: 'y1'
}],
xLabel: chart.x_label || '',
granularity: chart.granularity || ''
});
}
renderFunnel(funnel) {
const steps = ['registered', 'enrolled', 'started', 'completed'];
let previous = null;
steps.forEach(step => {
var _funnel$step;
const elStep = this.elContainer.querySelector(`${LpStatsTabOverview.selectors.elFunnelStep}[data-step="${step}"]`);
if (!elStep) {
return;
}
const count = Number((_funnel$step = funnel[step]) !== null && _funnel$step !== void 0 ? _funnel$step : 0);
const base = null === previous ? count : previous;
const width = base > 0 ? Math.min(100, count / base * 100) : 0;
elStep.querySelector('.lp-stats-funnel__count').textContent = String(count);
elStep.querySelector('.lp-stats-funnel__bar').style.width = `${width}%`;
previous = count;
});
}
completionBadge(rate) {
var _completionBadge$gree, _completionBadge$yell;
if (null == rate) {
return '';
}
const {
completionBadge = {}
} = (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsConfig)();
const green = (_completionBadge$gree = completionBadge.green) !== null && _completionBadge$gree !== void 0 ? _completionBadge$gree : 60;
const yellow = (_completionBadge$yell = completionBadge.yellow) !== null && _completionBadge$yell !== void 0 ? _completionBadge$yell : 40;
if (rate >= green) {
return 'green';
}
return rate >= yellow ? 'yellow' : 'red';
}
topCoursesColumns() {
return [{
key: 'course_name',
label: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('course', 'Course')
}, {
key: 'revenue_formatted',
label: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('revenue', 'Revenue'),
csv: row => row.revenue
}, {
key: 'order_count',
label: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('orders', 'Orders')
}, {
key: 'enrolled',
label: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('enrolled', 'Enrolled')
}, {
key: 'completion_rate',
label: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('completion', 'Completion'),
format: value => null == value ? '–' : `${value}%`,
badge: row => this.completionBadge(row.completion_rate)
}];
}
instructorColumns() {
return [{
key: 'instructor_name',
label: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('instructor', 'Instructor')
}, {
key: 'course_count',
label: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('courses', 'Courses')
}, {
key: 'revenue_formatted',
label: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('revenue', 'Revenue'),
csv: row => row.revenue
}, {
key: 'enrolled',
label: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('enrolled', 'Enrolled')
}, {
key: 'completion_rate',
label: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('completion', 'Completion'),
format: value => null == value ? '–' : `${value}%`,
badge: row => this.completionBadge(row.completion_rate)
}];
}
renderTables(dashboard) {
this.tables['top-courses'] = (0,_data_table_js__WEBPACK_IMPORTED_MODULE_5__.renderDataTable)(this.elContainer.querySelector(LpStatsTabOverview.selectors.elTableTopCourses), this.topCoursesColumns(), dashboard.top_courses || []);
this.tables.instructors = (0,_data_table_js__WEBPACK_IMPORTED_MODULE_5__.renderDataTable)(this.elContainer.querySelector(LpStatsTabOverview.selectors.elTableInstructors), this.instructorColumns(), dashboard.instructor_summary || []);
}
renderOrderHealth(orderHealth) {
this.elContainer.querySelectorAll(LpStatsTabOverview.selectors.elOrderHealthBox).forEach(elBox => {
const status = elBox.dataset.status;
const elCount = elBox.querySelector('.lp-stats-health-box__count');
if (elCount) {
var _orderHealth$status;
elCount.textContent = String((_orderHealth$status = orderHealth[status]) !== null && _orderHealth$status !== void 0 ? _orderHealth$status : 0);
}
});
}
renderHealthChecks(healthChecks) {
this.elContainer.querySelectorAll(LpStatsTabOverview.selectors.elHealthCheckCount).forEach(elCount => {
var _healthChecks$check;
const check = elCount.dataset.check;
elCount.textContent = String((_healthChecks$check = healthChecks[check]) !== null && _healthChecks$check !== void 0 ? _healthChecks$check : 0);
});
}
viewAllCourses(args) {
const btn = args.target.closest(LpStatsTabOverview.selectors.elBtnViewAllCourses);
if (!btn || !this.elContainer.contains(btn)) {
return;
}
_report_modal_js__WEBPACK_IMPORTED_MODULE_6__.lpStatsReportModal.open({
report: 'top_courses',
title: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('topCourses', 'Top courses'),
tableId: 'top-courses'
});
}
exportTables() {
Object.entries(this.tables).forEach(([tableId, handle]) => {
if (handle && handle.rows.length) {
(0,_csv_js__WEBPACK_IMPORTED_MODULE_7__.exportCsv)((0,_csv_js__WEBPACK_IMPORTED_MODULE_7__.buildCsvFilename)('overview', tableId), handle.columns, handle.rows);
}
});
}
}
const lpStatsTabOverview = new LpStatsTabOverview();
/***/ },
/***/ "./assets/src/js/admin/statistics/tab-users.js"
/*!*****************************************************!*\
!*** ./assets/src/js/admin/statistics/tab-users.js ***!
\*****************************************************/
(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ LpStatsTabUsers: () => (/* binding */ LpStatsTabUsers),
/* harmony export */ lpStatsTabUsers: () => (/* binding */ lpStatsTabUsers)
/* harmony export */ });
/* harmony import */ var lpAssetsJsPath_utils_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! lpAssetsJsPath/utils.js */ "./assets/src/js/utils.js");
/* harmony import */ var _state_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./state.js */ "./assets/src/js/admin/statistics/state.js");
/* harmony import */ var _api_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./api.js */ "./assets/src/js/admin/statistics/api.js");
/* harmony import */ var _kpi_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./kpi.js */ "./assets/src/js/admin/statistics/kpi.js");
/* harmony import */ var _chart_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./chart.js */ "./assets/src/js/admin/statistics/chart.js");
/* harmony import */ var _data_table_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./data-table.js */ "./assets/src/js/admin/statistics/data-table.js");
/* harmony import */ var _report_modal_js__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ./report-modal.js */ "./assets/src/js/admin/statistics/report-modal.js");
/* harmony import */ var _csv_js__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ./csv.js */ "./assets/src/js/admin/statistics/csv.js");
/**
* Users tab module.
*
* Fetches the `dashboard` payload and renders KPIs, registered-users chart,
* 5-step funnel (incl. failed), Top Students and Top Courses by Students
* tables, popups and CSV export.
*
* @since 4.4.2
* @version 1.0.0
*/
const sprintfLite = (template, value) => String(template).replace(/%[ds]/, String(value)).replace(/%%/g, '%');
class LpStatsTabUsers {
static selectors = {
elContainer: '.lp-stats-tab-users',
elChartCanvas: '#user-chart-content',
elFunnelStep: '.lp-stats-funnel__step',
elTableTopStudents: '.lp-stats-table-top-students',
elTableCoursesByStudents: '.lp-stats-table-courses-by-students',
elBtnViewAllStudents: '.lp-stats-view-all-students',
elBtnViewAllCourses: '.lp-stats-view-all-courses-by-students',
elSkeleton: '.lp-skeleton-animation'
};
static kpiCards = {
users_activated: '.lp-kpi-users-activated',
students: '.lp-kpi-students',
instructors: '.lp-kpi-instructors',
not_started: '.lp-kpi-not-started',
in_progress: '.lp-kpi-in-progress',
finished: '.lp-kpi-finished'
};
constructor() {
this.elContainer = null;
this.isRequesting = false;
this.pendingReload = false;
this.tables = {};
}
init() {
this.elContainer = document.querySelector(LpStatsTabUsers.selectors.elContainer);
if (!this.elContainer) {
return;
}
this.events();
this.loadData();
}
events() {
if (LpStatsTabUsers._loadedEvents) {
return;
}
LpStatsTabUsers._loadedEvents = this;
lpAssetsJsPath_utils_js__WEBPACK_IMPORTED_MODULE_0__.eventHandlers('click', [{
selector: LpStatsTabUsers.selectors.elBtnViewAllStudents,
class: this,
callBack: this.viewAllStudents.name
}, {
selector: LpStatsTabUsers.selectors.elBtnViewAllCourses,
class: this,
callBack: this.viewAllCoursesByStudents.name
}]);
document.addEventListener(_state_js__WEBPACK_IMPORTED_MODULE_1__.LP_STATS_FILTER_CHANGED, () => this.loadData());
document.addEventListener(_state_js__WEBPACK_IMPORTED_MODULE_1__.LP_STATS_EXPORT_CSV, () => this.exportTables());
}
toggleSkeletons(show) {
this.elContainer.querySelectorAll(LpStatsTabUsers.selectors.elSkeleton).forEach(el => {
el.style.display = show ? 'block' : 'none';
});
}
loadData() {
if (this.isRequesting) {
this.pendingReload = true;
return;
}
this.isRequesting = true;
(0,_api_js__WEBPACK_IMPORTED_MODULE_2__.lpStatsFetch)('user-statistics', {}, {
before: () => this.toggleSkeletons(true),
success: response => this.render(response.data),
error: err => {
console.error('LP Statistics users:', err);
this.render(null);
},
completed: () => {
this.toggleSkeletons(false);
this.isRequesting = false;
if (this.pendingReload) {
this.pendingReload = false;
this.loadData();
}
}
});
}
render(data) {
if (!data?.dashboard) {
console.error('LP Statistics users: dashboard payload missing.');
data = {
chart_data: {},
dashboard: {}
};
}
const dashboard = data.dashboard || {};
this.renderKpis(dashboard.kpis || {});
this.renderChart(data.chart_data || {});
this.renderFunnel(dashboard.funnel || {});
this.renderTables(dashboard);
}
renderKpis(kpis) {
Object.entries(LpStatsTabUsers.kpiCards).forEach(([key, selector]) => {
const elCard = this.elContainer.querySelector(selector);
const payload = {
...(kpis[key] || {})
};
if ('users_activated' === key) {
var _payload$new_in_perio;
payload.subline = sprintfLite((0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('newThisPeriod', '+%d this period'), (_payload$new_in_perio = payload.new_in_period) !== null && _payload$new_in_perio !== void 0 ? _payload$new_in_perio : 0);
}
if ('students' === key) {
var _payload$active_in_pe;
payload.subline = sprintfLite((0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('activeInPeriod', '%d active in this period'), (_payload$active_in_pe = payload.active_in_period) !== null && _payload$active_in_pe !== void 0 ? _payload$active_in_pe : 0);
}
if ('instructors' === key) {
var _payload$active_in_pe2, _payload$value;
payload.subline = `${(_payload$active_in_pe2 = payload.active_in_period) !== null && _payload$active_in_pe2 !== void 0 ? _payload$active_in_pe2 : 0}/${(_payload$value = payload.value) !== null && _payload$value !== void 0 ? _payload$value : 0} ${(0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('activeThisPeriod', 'active this period')}`;
}
if ('not_started' === key) {
payload.subline = (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('afterEnrollment', 'After enrollment');
}
if ('in_progress' === key) {
payload.subline = (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('currentLearners', 'Current learners');
}
if ('finished' === key && null != payload.completion_rate) {
payload.subline = sprintfLite((0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('completionRateSub', '%s%% completion rate'), payload.completion_rate);
}
(0,_kpi_js__WEBPACK_IMPORTED_MODULE_3__.renderKpi)(elCard, payload);
});
}
renderChart(chartData) {
(0,_chart_js__WEBPACK_IMPORTED_MODULE_4__.renderLineChart)(LpStatsTabUsers.selectors.elChartCanvas, {
labels: chartData.labels || [],
datasets: [{
label: chartData.line_label || (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('registeredUsers', 'Registered users'),
data: chartData.data || [],
yAxisID: 'y'
}],
xLabel: chartData.x_label || '',
granularity: chartData.granularity || ''
}, {
yCurrency: false
});
}
renderFunnel(funnel) {
const steps = ['registered', 'enrolled', 'started', 'completed', 'failed'];
let previous = null;
steps.forEach(step => {
var _funnel$step;
const elStep = this.elContainer.querySelector(`${LpStatsTabUsers.selectors.elFunnelStep}[data-step="${step}"]`);
if (!elStep) {
return;
}
const count = Number((_funnel$step = funnel[step]) !== null && _funnel$step !== void 0 ? _funnel$step : 0);
// 'failed' is an annotation on 'started', not the next narrowing step.
let base = previous;
if ('failed' === step) {
var _funnel$started;
base = Number((_funnel$started = funnel.started) !== null && _funnel$started !== void 0 ? _funnel$started : 0);
} else if (null === previous) {
base = count;
}
const width = base > 0 ? Math.min(100, count / base * 100) : 0;
elStep.querySelector('.lp-stats-funnel__count').textContent = String(count);
elStep.querySelector('.lp-stats-funnel__bar').style.width = `${width}%`;
if ('failed' !== step) {
previous = count;
}
});
}
completionBadge(rate) {
var _completionBadge$gree, _completionBadge$yell;
if (null == rate) {
return '';
}
const {
completionBadge = {}
} = (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsConfig)();
const green = (_completionBadge$gree = completionBadge.green) !== null && _completionBadge$gree !== void 0 ? _completionBadge$gree : 60;
const yellow = (_completionBadge$yell = completionBadge.yellow) !== null && _completionBadge$yell !== void 0 ? _completionBadge$yell : 40;
if (rate >= green) {
return 'green';
}
return rate >= yellow ? 'yellow' : 'red';
}
studentStatusLabel(slug) {
const labels = {
active: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('statusActive', 'Active'),
at_risk: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('statusAtRisk', 'At risk'),
idle: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('statusIdle', 'Idle')
};
return labels[slug] || String(slug || '');
}
studentStatusBadge(slug) {
const badges = {
active: 'green',
at_risk: 'yellow',
idle: 'grey'
};
return badges[slug] || '';
}
formatLastActive(value) {
if (!value) {
return '—';
}
const date = new Date(String(value).replace(' ', 'T'));
if (isNaN(date.getTime())) {
return '—';
}
try {
const days = Math.round((date.getTime() - Date.now()) / 86400000);
return new Intl.RelativeTimeFormat(undefined, {
numeric: 'auto'
}).format(days, 'day');
} catch {
return date.toLocaleDateString();
}
}
/**
* @param {boolean} withScore avg_score column only when the payload carries scores.
*/
topStudentsColumns(withScore = true) {
const columns = [{
key: 'name',
label: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('student', 'Student')
}, {
key: 'enrolled',
label: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('enrolled', 'Enrolled')
}, {
key: 'completed',
label: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('completedLabel', 'Completed')
}];
if (withScore) {
columns.push({
key: 'avg_score',
label: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('avgScore', 'Quiz pass rate'),
format: value => null == value ? '—' : `${value}%`
});
}
columns.push({
key: 'last_active',
label: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('lastActive', 'Last active'),
format: value => this.formatLastActive(value),
csv: row => row.last_active || ''
}, {
key: 'status',
label: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('status', 'Status'),
format: value => this.studentStatusLabel(value),
badge: row => this.studentStatusBadge(row.status),
csv: row => row.status
});
return columns;
}
coursesByStudentsColumns() {
return [{
key: 'name',
label: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('course', 'Course')
}, {
key: 'enrolled',
label: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('enrolled', 'Enrolled')
}, {
key: 'started',
label: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('startedLabel', 'Started')
}, {
key: 'completed',
label: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('completedLabel', 'Completed')
}, {
key: 'completion_rate',
label: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('completion', 'Completion'),
format: value => null == value ? '—' : `${value}%`,
badge: row => this.completionBadge(row.completion_rate)
}, {
key: 'active_7d',
label: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('activeLast7dShort', 'Active 7d')
}];
}
/**
* avg_score is null when quiz data is unavailable — hide the whole column.
*
* @param {Array} rows
*/
hasScores(rows = []) {
return rows.some(row => null != row.avg_score);
}
renderTables(dashboard) {
const students = dashboard.top_students || [];
this.tables['top-students'] = (0,_data_table_js__WEBPACK_IMPORTED_MODULE_5__.renderDataTable)(this.elContainer.querySelector(LpStatsTabUsers.selectors.elTableTopStudents), this.topStudentsColumns(this.hasScores(students)), students);
this.tables['courses-by-students'] = (0,_data_table_js__WEBPACK_IMPORTED_MODULE_5__.renderDataTable)(this.elContainer.querySelector(LpStatsTabUsers.selectors.elTableCoursesByStudents), this.coursesByStudentsColumns(), dashboard.top_courses_by_students || []);
}
viewAllStudents(args) {
const btn = args.target.closest(LpStatsTabUsers.selectors.elBtnViewAllStudents);
if (!btn || !this.elContainer.contains(btn)) {
return;
}
_report_modal_js__WEBPACK_IMPORTED_MODULE_6__.lpStatsReportModal.open({
report: 'top_students',
title: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('topStudents', 'Top students'),
tableId: 'top-students'
});
}
viewAllCoursesByStudents(args) {
const btn = args.target.closest(LpStatsTabUsers.selectors.elBtnViewAllCourses);
if (!btn || !this.elContainer.contains(btn)) {
return;
}
_report_modal_js__WEBPACK_IMPORTED_MODULE_6__.lpStatsReportModal.open({
report: 'courses_by_students',
title: (0,_api_js__WEBPACK_IMPORTED_MODULE_2__.getStatsI18n)('topCoursesByStudents', 'Top courses by students'),
tableId: 'courses-by-students'
});
}
exportTables() {
Object.entries(this.tables).forEach(([tableId, handle]) => {
if (handle && handle.rows.length) {
(0,_csv_js__WEBPACK_IMPORTED_MODULE_7__.exportCsv)((0,_csv_js__WEBPACK_IMPORTED_MODULE_7__.buildCsvFilename)('users', tableId), handle.columns, handle.rows);
}
});
}
}
const lpStatsTabUsers = new LpStatsTabUsers();
/***/ },
/***/ "./assets/src/js/utils.js"
/*!********************************!*\
!*** ./assets/src/js/utils.js ***!
\********************************/
(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ debounce: () => (/* binding */ debounce),
/* harmony export */ eventHandlers: () => (/* binding */ eventHandlers),
/* harmony export */ fullScreenView: () => (/* binding */ fullScreenView),
/* harmony export */ getDataOfForm: () => (/* binding */ getDataOfForm),
/* harmony export */ getFieldKeysOfForm: () => (/* binding */ getFieldKeysOfForm),
/* harmony export */ listenElementCreated: () => (/* binding */ listenElementCreated),
/* harmony export */ listenElementViewed: () => (/* binding */ listenElementViewed),
/* harmony export */ lpAddQueryArgs: () => (/* binding */ lpAddQueryArgs),
/* harmony export */ lpAjaxParseJsonOld: () => (/* binding */ lpAjaxParseJsonOld),
/* harmony export */ lpClassName: () => (/* binding */ lpClassName),
/* harmony export */ lpFetchAPI: () => (/* binding */ lpFetchAPI),
/* harmony export */ lpGetCurrentURLNoParam: () => (/* binding */ lpGetCurrentURLNoParam),
/* harmony export */ lpOnElementReady: () => (/* binding */ lpOnElementReady),
/* harmony export */ lpSetLoadingEl: () => (/* binding */ lpSetLoadingEl),
/* harmony export */ lpShowHideEl: () => (/* binding */ lpShowHideEl),
/* harmony export */ mergeDataWithDatForm: () => (/* binding */ mergeDataWithDatForm),
/* harmony export */ toggleCollapse: () => (/* binding */ toggleCollapse),
/* harmony export */ toggleEnable: () => (/* binding */ toggleEnable)
/* harmony export */ });
/**
* Utils functions
*
* @param url
* @param data
* @param functions
* @since 4.2.5.1
* @version 1.0.7
*/
const lpClassName = {
hidden: 'lp-hidden',
loading: 'loading',
elCollapse: 'lp-collapse',
elSectionToggle: '.lp-section-toggle',
elTriggerToggle: '.lp-trigger-toggle',
elBtnFullScreen: '.lp-btn-full-screen-view',
elFullScreen: 'lp-full-screen-view',
elBtnFullScreenClose: 'lp-full-screen-view__close'
};
const lpFetchAPI = (url, data = {}, functions = {}) => {
if ('function' === typeof functions.before) {
functions.before();
}
fetch(url, {
method: 'GET',
...data
}).then(response => response.json()).then(response => {
if ('function' === typeof functions.success) {
functions.success(response);
}
}).catch(err => {
if ('function' === typeof functions.error) {
functions.error(err);
}
}).finally(() => {
if ('function' === typeof functions.completed) {
functions.completed();
}
});
};
/**
* Get current URL without params.
*
* @since 4.2.5.1
*/
const lpGetCurrentURLNoParam = () => {
let currentUrl = window.location.href;
const hasParams = currentUrl.includes('?');
if (hasParams) {
currentUrl = currentUrl.split('?')[0];
}
return currentUrl;
};
const lpAddQueryArgs = (endpoint, args) => {
const url = new URL(endpoint);
Object.keys(args).forEach(arg => {
url.searchParams.set(arg, args[arg]);
});
return url;
};
/**
* Listen element viewed.
*
* @param el
* @param callback
* @since 4.2.5.8
*/
const listenElementViewed = (el, callback) => {
const observerSeeItem = new IntersectionObserver(function (entries) {
for (const entry of entries) {
if (entry.isIntersecting) {
callback(entry);
}
}
});
observerSeeItem.observe(el);
};
/**
* Listen element created.
*
* @param callback
* @since 4.2.5.8
*/
const listenElementCreated = callback => {
const observerCreateItem = new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) {
if (mutation.addedNodes) {
mutation.addedNodes.forEach(function (node) {
if (node.nodeType === 1) {
callback(node);
}
});
}
});
});
observerCreateItem.observe(document, {
childList: true,
subtree: true
});
// End.
};
/**
* Listen element created.
*
* @param selector
* @param callback
* @since 4.2.7.1
*/
const lpOnElementReady = (selector, callback) => {
const element = document.querySelector(selector);
if (element) {
callback(element);
return;
}
const observer = new MutationObserver((mutations, obs) => {
const element = document.querySelector(selector);
if (element) {
obs.disconnect();
callback(element);
}
});
observer.observe(document.documentElement, {
childList: true,
subtree: true
});
};
// Parse JSON from string with content include LP_AJAX_START.
const lpAjaxParseJsonOld = data => {
if (typeof data !== 'string') {
return data;
}
const m = String.raw({
raw: data
}).match(/<-- LP_AJAX_START -->(.*)<-- LP_AJAX_END -->/s);
try {
if (m) {
data = JSON.parse(m[1].replace(/(?:\r\n|\r|\n)/g, ''));
} else {
data = JSON.parse(data);
}
} catch (e) {
data = {};
}
return data;
};
// status 0: hide, 1: show
const lpShowHideEl = (el, status = 0) => {
if (!el) {
return;
}
if (!status) {
el.classList.add(lpClassName.hidden);
} else {
el.classList.remove(lpClassName.hidden);
}
};
// status 0: hide, 1: show
const lpSetLoadingEl = (el, status) => {
if (!el) {
return;
}
if (!status) {
el.classList.remove(lpClassName.loading);
} else {
el.classList.add(lpClassName.loading);
}
};
// Toggle collapse section
const toggleCollapse = (e, target, elTriggerClassName = '', elsExclude = [], callback) => {
if (!elTriggerClassName) {
elTriggerClassName = lpClassName.elTriggerToggle;
}
// Exclude elements, which should not trigger the collapse toggle
if (elsExclude && elsExclude.length > 0) {
for (const elExclude of elsExclude) {
if (target.closest(elExclude)) {
return;
}
}
}
const elTrigger = target.closest(elTriggerClassName);
if (!elTrigger) {
return;
}
//console.log( 'elTrigger', elTrigger );
const elSectionToggle = elTrigger.closest(`${lpClassName.elSectionToggle}`);
if (!elSectionToggle) {
return;
}
elSectionToggle.classList.toggle(`${lpClassName.elCollapse}`);
if ('function' === typeof callback) {
callback(elSectionToggle);
}
};
// Get data of form
const getDataOfForm = form => {
const dataSend = {};
const formData = new FormData(form);
for (const pair of formData.entries()) {
const key = pair[0];
const value = formData.getAll(key);
if (!dataSend.hasOwnProperty(key)) {
// Convert value array to string.
dataSend[key] = value.join(',');
}
}
return dataSend;
};
// Get field keys of form
const getFieldKeysOfForm = form => {
const keys = [];
const elements = form.elements;
for (let i = 0; i < elements.length; i++) {
const name = elements[i].name;
if (name && !keys.includes(name)) {
keys.push(name);
}
}
return keys;
};
// Merge data handle with data form.
const mergeDataWithDatForm = (elForm, dataHandle) => {
const dataForm = getDataOfForm(elForm);
const keys = getFieldKeysOfForm(elForm);
keys.forEach(key => {
if (!dataForm.hasOwnProperty(key)) {
delete dataHandle[key];
} else if (dataForm[key][0] === '') {
delete dataForm[key];
delete dataHandle[key];
}
});
dataHandle = {
...dataHandle,
...dataForm
};
return dataHandle;
};
/**
* Event trigger
* For each list of event handlers, listen event on document.
*
* eventName: 'click', 'change', ...
* eventHandlers = [ { selector: '.lp-button', callBack: function(){}, class: object } ]
*
* @param eventName
* @param eventHandlers
*/
const eventHandlers = (eventName, eventHandlers) => {
document.addEventListener(eventName, e => {
const target = e.target;
let args = {
e,
target
};
eventHandlers.forEach(eventHandler => {
args = {
...args,
...eventHandler
};
//console.log( args );
// Check condition before call back
if (eventHandler.conditionBeforeCallBack) {
if (eventHandler.conditionBeforeCallBack(args) !== true) {
return;
}
}
// Special check for keydown event with checkIsEventEnter = true
if (eventName === 'keydown' && eventHandler.checkIsEventEnter) {
if (e.key !== 'Enter') {
return;
}
}
if (target.closest(eventHandler.selector)) {
if (eventHandler.class) {
// Call method of class, function callBack will understand exactly {this} is class object.
eventHandler.class[eventHandler.callBack](args);
} else {
// For send args is objected, {this} is eventHandler object, not class object.
eventHandler.callBack(args);
}
}
});
});
};
/**
* Debounce - delays function execution until after `wait` ms of inactivity.
*
* Each call resets the timer. Only the last call in a burst executes.
*
* USE CASES:
* - Search inputs, form validation, window resize
* - Multiple elements need independent timers
* - When you need to call with different arguments
*
* EXAMPLES:
* const debouncedSearch = debounce( (query) => fetchResults(query), 300 );
* searchInput.addEventListener('input', (e) => debouncedSearch(e.target.value));
*
* const debouncedResize = debounce( recalculateLayout, 250 );
* window.addEventListener('resize', debouncedResize);
*
* ⚠️ Create ONCE outside event handlers, not inside.
*
* @param {Function} func - Function to debounce (can be anonymous)
* @param {number} wait - Milliseconds to wait (default: 500)
* @return {Function} Debounced wrapper function
* @since 4.3.7
* @version 1.0.0
*/
const debounce = (func, wait = 500) => {
let timer;
return args => {
clearTimeout(timer);
timer = setTimeout(() => func(args), wait);
};
};
/**
* Initialize lp-toggle-enable components.
*
* Finds all `.lp-toggle-enable` elements and wires up toggle behavior.
* Reads initial state from `data-enabled` attribute ("true"/"false").
* Calls `data-on-toggle` callback (if provided via options) on state change.
*
* HTML structure:
*
*
* @param {string} selector CSS selector for toggle elements (default: '.lp-toggle-enable')
* @param {Function} onToggle Optional callback( el, isEnabled ) called on state change
* @since 4.4.5
* @version 1.0.0
*/
window.lpToggleEnableInit = 0;
const toggleEnable = (onToggle = null) => {
if (window.lpToggleEnableInit) {
return;
}
window.lpToggleEnableInit = 1;
const selector = '.lp-toggle-enable';
const updateUI = (toggle, isEnabled) => {
toggle.classList.toggle('is-enabled', isEnabled);
const input = toggle.querySelector('.lp-toggle-enable__input');
if (input) {
input.checked = isEnabled;
input.value = isEnabled ? '1' : '0';
}
};
// Delegate click handling via eventHandlers.
eventHandlers('click', [{
selector,
callBack: args => {
const {
e,
target
} = args;
const toggle = target.closest(selector);
if (!toggle || toggle.classList.contains('is-disabled')) {
return;
}
e.preventDefault();
const isEnabled = !toggle.classList.contains('is-enabled');
updateUI(toggle, isEnabled);
if ('function' === typeof onToggle) {
onToggle(toggle, isEnabled);
}
}
}]);
};
/**
* Initialize custom fullscreen view buttons.
*
* Delegates clicks on `.lp-btn-full-screen-view` buttons to
* `lpToggleFullscreenView`. Reads the `data-target` attribute to find the
* target element. Falls back to the button's parent element when
* `data-target` is not provided.
*
* @since 4.4.5
* @version 1.0.0
*/
window.lpFullScreenViewInit = 0;
const fullScreenView = () => {
if (window.lpFullScreenViewInit) {
return;
}
window.lpFullScreenViewInit = 1;
let lastScrollY = 0;
const lpToggleFullscreenView = (elTarget, elBtnFullScreen = null) => {
const isFullscreen = elTarget.classList.contains(lpClassName.elFullScreen);
if (isFullscreen) {
elTarget.classList.remove(lpClassName.elFullScreen);
document.documentElement.classList.remove('lp-full-screen-active');
window.scrollTo(0, lastScrollY);
} else {
lastScrollY = window.scrollY;
elTarget.classList.add(lpClassName.elFullScreen);
document.documentElement.classList.add('lp-full-screen-active');
}
if (!isFullscreen) {
if (!elTarget.querySelector(`.${lpClassName.elBtnFullScreenClose}`)) {
const closeButton = document.createElement('button');
closeButton.type = 'button';
closeButton.className = lpClassName.elBtnFullScreenClose;
closeButton.setAttribute('aria-label', 'Close');
closeButton.innerHTML = lpData.i18n.closeButtonFullScreen || 'Close ×';
closeButton.addEventListener('click', e => {
e.preventDefault();
lpToggleFullscreenView(elTarget);
});
elTarget.appendChild(closeButton);
}
} else {
const closeButton = elTarget.querySelector(`.${lpClassName.elBtnFullScreenClose}`);
if (closeButton) {
closeButton.remove();
}
}
};
eventHandlers('click', [{
selector: lpClassName.elBtnFullScreen,
callBack: args => {
const {
e,
target
} = args;
const elBtnFullScreen = target.closest(lpClassName.elBtnFullScreen);
if (!elBtnFullScreen) {
console.log('No full screen button found');
return;
}
e.preventDefault();
let elTarget = null;
const targetSelector = elBtnFullScreen.dataset.targetFullscreen;
console.log(targetSelector);
if (targetSelector) {
elTarget = document.querySelector(targetSelector);
}
if (!elTarget) {
console.log('No target element found');
return;
}
lpToggleFullscreenView(elTarget, elBtnFullScreen);
}
}]);
};
/***/ },
/***/ "./node_modules/sweetalert2/dist/sweetalert2.all.js"
/*!**********************************************************!*\
!*** ./node_modules/sweetalert2/dist/sweetalert2.all.js ***!
\**********************************************************/
(module) {
/*!
* sweetalert2 v11.26.17
* Released under the MIT License.
*/
(function (global, factory) {
true ? module.exports = factory() :
0;
})(this, (function () { 'use strict';
function _assertClassBrand(e, t, n) {
if ("function" == typeof e ? e === t : e.has(t)) return arguments.length < 3 ? t : n;
throw new TypeError("Private element is not present on this object");
}
function _checkPrivateRedeclaration(e, t) {
if (t.has(e)) throw new TypeError("Cannot initialize the same private elements twice on an object");
}
function _classPrivateFieldGet2(s, a) {
return s.get(_assertClassBrand(s, a));
}
function _classPrivateFieldInitSpec(e, t, a) {
_checkPrivateRedeclaration(e, t), t.set(e, a);
}
function _classPrivateFieldSet2(s, a, r) {
return s.set(_assertClassBrand(s, a), r), r;
}
const RESTORE_FOCUS_TIMEOUT = 100;
/** @type {GlobalState} */
const globalState = {};
const focusPreviousActiveElement = () => {
if (globalState.previousActiveElement instanceof HTMLElement) {
globalState.previousActiveElement.focus();
globalState.previousActiveElement = null;
} else if (document.body) {
document.body.focus();
}
};
/**
* Restore previous active (focused) element
*
* @param {boolean} returnFocus
* @returns {Promise}
*/
const restoreActiveElement = returnFocus => {
return new Promise(resolve => {
if (!returnFocus) {
return resolve();
}
const x = window.scrollX;
const y = window.scrollY;
globalState.restoreFocusTimeout = setTimeout(() => {
focusPreviousActiveElement();
resolve();
}, RESTORE_FOCUS_TIMEOUT); // issues/900
window.scrollTo(x, y);
});
};
const swalPrefix = 'swal2-';
/**
* @typedef {Record} SwalClasses
*/
/**
* @typedef {'success' | 'warning' | 'info' | 'question' | 'error'} SwalIcon
* @typedef {Record} SwalIcons
*/
/** @type {SwalClass[]} */
const classNames = ['container', 'shown', 'height-auto', 'iosfix', 'popup', 'modal', 'no-backdrop', 'no-transition', 'toast', 'toast-shown', 'show', 'hide', 'close', 'title', 'html-container', 'actions', 'confirm', 'deny', 'cancel', 'footer', 'icon', 'icon-content', 'image', 'input', 'file', 'range', 'select', 'radio', 'checkbox', 'label', 'textarea', 'inputerror', 'input-label', 'validation-message', 'progress-steps', 'active-progress-step', 'progress-step', 'progress-step-line', 'loader', 'loading', 'styled', 'top', 'top-start', 'top-end', 'top-left', 'top-right', 'center', 'center-start', 'center-end', 'center-left', 'center-right', 'bottom', 'bottom-start', 'bottom-end', 'bottom-left', 'bottom-right', 'grow-row', 'grow-column', 'grow-fullscreen', 'rtl', 'timer-progress-bar', 'timer-progress-bar-container', 'scrollbar-measure', 'icon-success', 'icon-warning', 'icon-info', 'icon-question', 'icon-error', 'draggable', 'dragging'];
const swalClasses = classNames.reduce((acc, className) => {
acc[className] = swalPrefix + className;
return acc;
}, /** @type {SwalClasses} */{});
/** @type {SwalIcon[]} */
const icons = ['success', 'warning', 'info', 'question', 'error'];
const iconTypes = icons.reduce((acc, icon) => {
acc[icon] = swalPrefix + icon;
return acc;
}, /** @type {SwalIcons} */{});
const consolePrefix = 'SweetAlert2:';
/**
* Capitalize the first letter of a string
*
* @param {string} str
* @returns {string}
*/
const capitalizeFirstLetter = str => str.charAt(0).toUpperCase() + str.slice(1);
/**
* Standardize console warnings
*
* @param {string | string[]} message
*/
const warn = message => {
console.warn(`${consolePrefix} ${typeof message === 'object' ? message.join(' ') : message}`);
};
/**
* Standardize console errors
*
* @param {string} message
*/
const error = message => {
console.error(`${consolePrefix} ${message}`);
};
/**
* Private global state for `warnOnce`
*
* @type {string[]}
* @private
*/
const previousWarnOnceMessages = [];
/**
* Show a console warning, but only if it hasn't already been shown
*
* @param {string} message
*/
const warnOnce = message => {
if (!previousWarnOnceMessages.includes(message)) {
previousWarnOnceMessages.push(message);
warn(message);
}
};
/**
* Show a one-time console warning about deprecated params/methods
*
* @param {string} deprecatedParam
* @param {string?} useInstead
*/
const warnAboutDeprecation = (deprecatedParam, useInstead = null) => {
warnOnce(`"${deprecatedParam}" is deprecated and will be removed in the next major release.${useInstead ? ` Use "${useInstead}" instead.` : ''}`);
};
/**
* If `arg` is a function, call it (with no arguments or context) and return the result.
* Otherwise, just pass the value through
*
* @param {(() => *) | *} arg
* @returns {*}
*/
const callIfFunction = arg => typeof arg === 'function' ? arg() : arg;
/**
* @param {*} arg
* @returns {boolean}
*/
const hasToPromiseFn = arg => arg && typeof arg.toPromise === 'function';
/**
* @param {*} arg
* @returns {Promise<*>}
*/
const asPromise = arg => hasToPromiseFn(arg) ? arg.toPromise() : Promise.resolve(arg);
/**
* @param {*} arg
* @returns {boolean}
*/
const isPromise = arg => arg && Promise.resolve(arg) === arg;
/**
* Gets the popup container which contains the backdrop and the popup itself.
*
* @returns {HTMLElement | null}
*/
const getContainer = () => document.body.querySelector(`.${swalClasses.container}`);
/**
* @param {string} selectorString
* @returns {HTMLElement | null}
*/
const elementBySelector = selectorString => {
const container = getContainer();
return container ? container.querySelector(selectorString) : null;
};
/**
* @param {string} className
* @returns {HTMLElement | null}
*/
const elementByClass = className => {
return elementBySelector(`.${className}`);
};
/**
* @returns {HTMLElement | null}
*/
const getPopup = () => elementByClass(swalClasses.popup);
/**
* @returns {HTMLElement | null}
*/
const getIcon = () => elementByClass(swalClasses.icon);
/**
* @returns {HTMLElement | null}
*/
const getIconContent = () => elementByClass(swalClasses['icon-content']);
/**
* @returns {HTMLElement | null}
*/
const getTitle = () => elementByClass(swalClasses.title);
/**
* @returns {HTMLElement | null}
*/
const getHtmlContainer = () => elementByClass(swalClasses['html-container']);
/**
* @returns {HTMLElement | null}
*/
const getImage = () => elementByClass(swalClasses.image);
/**
* @returns {HTMLElement | null}
*/
const getProgressSteps = () => elementByClass(swalClasses['progress-steps']);
/**
* @returns {HTMLElement | null}
*/
const getValidationMessage = () => elementByClass(swalClasses['validation-message']);
/**
* @returns {HTMLButtonElement | null}
*/
const getConfirmButton = () => (/** @type {HTMLButtonElement} */elementBySelector(`.${swalClasses.actions} .${swalClasses.confirm}`));
/**
* @returns {HTMLButtonElement | null}
*/
const getCancelButton = () => (/** @type {HTMLButtonElement} */elementBySelector(`.${swalClasses.actions} .${swalClasses.cancel}`));
/**
* @returns {HTMLButtonElement | null}
*/
const getDenyButton = () => (/** @type {HTMLButtonElement} */elementBySelector(`.${swalClasses.actions} .${swalClasses.deny}`));
/**
* @returns {HTMLElement | null}
*/
const getInputLabel = () => elementByClass(swalClasses['input-label']);
/**
* @returns {HTMLElement | null}
*/
const getLoader = () => elementBySelector(`.${swalClasses.loader}`);
/**
* @returns {HTMLElement | null}
*/
const getActions = () => elementByClass(swalClasses.actions);
/**
* @returns {HTMLElement | null}
*/
const getFooter = () => elementByClass(swalClasses.footer);
/**
* @returns {HTMLElement | null}
*/
const getTimerProgressBar = () => elementByClass(swalClasses['timer-progress-bar']);
/**
* @returns {HTMLElement | null}
*/
const getCloseButton = () => elementByClass(swalClasses.close);
// https://github.com/jkup/focusable/blob/master/index.js
const focusable = `
a[href],
area[href],
input:not([disabled]),
select:not([disabled]),
textarea:not([disabled]),
button:not([disabled]),
iframe,
object,
embed,
[tabindex="0"],
[contenteditable],
audio[controls],
video[controls],
summary
`;
/**
* @returns {HTMLElement[]}
*/
const getFocusableElements = () => {
const popup = getPopup();
if (!popup) {
return [];
}
/** @type {NodeListOf} */
const focusableElementsWithTabindex = popup.querySelectorAll('[tabindex]:not([tabindex="-1"]):not([tabindex="0"])');
const focusableElementsWithTabindexSorted = Array.from(focusableElementsWithTabindex)
// sort according to tabindex
.sort((a, b) => {
const tabindexA = parseInt(a.getAttribute('tabindex') || '0');
const tabindexB = parseInt(b.getAttribute('tabindex') || '0');
if (tabindexA > tabindexB) {
return 1;
} else if (tabindexA < tabindexB) {
return -1;
}
return 0;
});
/** @type {NodeListOf} */
const otherFocusableElements = popup.querySelectorAll(focusable);
const otherFocusableElementsFiltered = Array.from(otherFocusableElements).filter(el => el.getAttribute('tabindex') !== '-1');
return [...new Set(focusableElementsWithTabindexSorted.concat(otherFocusableElementsFiltered))].filter(el => isVisible$1(el));
};
/**
* @returns {boolean}
*/
const isModal = () => {
return hasClass(document.body, swalClasses.shown) && !hasClass(document.body, swalClasses['toast-shown']) && !hasClass(document.body, swalClasses['no-backdrop']);
};
/**
* @returns {boolean}
*/
const isToast = () => {
const popup = getPopup();
if (!popup) {
return false;
}
return hasClass(popup, swalClasses.toast);
};
/**
* @returns {boolean}
*/
const isLoading = () => {
const popup = getPopup();
if (!popup) {
return false;
}
return popup.hasAttribute('data-loading');
};
/**
* Securely set innerHTML of an element
* https://github.com/sweetalert2/sweetalert2/issues/1926
*
* @param {HTMLElement} elem
* @param {string} html
*/
const setInnerHtml = (elem, html) => {
elem.textContent = '';
if (html) {
const parser = new DOMParser();
const parsed = parser.parseFromString(html, `text/html`);
const head = parsed.querySelector('head');
if (head) {
Array.from(head.childNodes).forEach(child => {
elem.appendChild(child);
});
}
const body = parsed.querySelector('body');
if (body) {
Array.from(body.childNodes).forEach(child => {
if (child instanceof HTMLVideoElement || child instanceof HTMLAudioElement) {
elem.appendChild(child.cloneNode(true)); // https://github.com/sweetalert2/sweetalert2/issues/2507
} else {
elem.appendChild(child);
}
});
}
}
};
/**
* @param {HTMLElement} elem
* @param {string} className
* @returns {boolean}
*/
const hasClass = (elem, className) => {
if (!className) {
return false;
}
const classList = className.split(/\s+/);
for (let i = 0; i < classList.length; i++) {
if (!elem.classList.contains(classList[i])) {
return false;
}
}
return true;
};
/**
* @param {HTMLElement} elem
* @param {SweetAlertOptions} params
*/
const removeCustomClasses = (elem, params) => {
Array.from(elem.classList).forEach(className => {
if (!Object.values(swalClasses).includes(className) && !Object.values(iconTypes).includes(className) && !Object.values(params.showClass || {}).includes(className)) {
elem.classList.remove(className);
}
});
};
/**
* @param {HTMLElement} elem
* @param {SweetAlertOptions} params
* @param {string} className
*/
const applyCustomClass = (elem, params, className) => {
removeCustomClasses(elem, params);
if (!params.customClass) {
return;
}
const customClass = params.customClass[(/** @type {keyof SweetAlertCustomClass} */className)];
if (!customClass) {
return;
}
if (typeof customClass !== 'string' && !customClass.forEach) {
warn(`Invalid type of customClass.${className}! Expected string or iterable object, got "${typeof customClass}"`);
return;
}
addClass(elem, customClass);
};
/**
* @param {HTMLElement} popup
* @param {import('./renderers/renderInput').InputClass | SweetAlertInput} inputClass
* @returns {HTMLInputElement | null}
*/
const getInput$1 = (popup, inputClass) => {
if (!inputClass) {
return null;
}
switch (inputClass) {
case 'select':
case 'textarea':
case 'file':
return popup.querySelector(`.${swalClasses.popup} > .${swalClasses[inputClass]}`);
case 'checkbox':
return popup.querySelector(`.${swalClasses.popup} > .${swalClasses.checkbox} input`);
case 'radio':
return popup.querySelector(`.${swalClasses.popup} > .${swalClasses.radio} input:checked`) || popup.querySelector(`.${swalClasses.popup} > .${swalClasses.radio} input:first-child`);
case 'range':
return popup.querySelector(`.${swalClasses.popup} > .${swalClasses.range} input`);
default:
return popup.querySelector(`.${swalClasses.popup} > .${swalClasses.input}`);
}
};
/**
* @param {HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement} input
*/
const focusInput = input => {
input.focus();
// place cursor at end of text in text input
if (input.type !== 'file') {
// http://stackoverflow.com/a/2345915
const val = input.value;
input.value = '';
input.value = val;
}
};
/**
* @param {HTMLElement | HTMLElement[] | null} target
* @param {string | string[] | readonly string[] | undefined} classList
* @param {boolean} condition
*/
const toggleClass = (target, classList, condition) => {
if (!target || !classList) {
return;
}
if (typeof classList === 'string') {
classList = classList.split(/\s+/).filter(Boolean);
}
classList.forEach(className => {
if (Array.isArray(target)) {
target.forEach(elem => {
if (condition) {
elem.classList.add(className);
} else {
elem.classList.remove(className);
}
});
} else {
if (condition) {
target.classList.add(className);
} else {
target.classList.remove(className);
}
}
});
};
/**
* @param {HTMLElement | HTMLElement[] | null} target
* @param {string | string[] | readonly string[] | undefined} classList
*/
const addClass = (target, classList) => {
toggleClass(target, classList, true);
};
/**
* @param {HTMLElement | HTMLElement[] | null} target
* @param {string | string[] | readonly string[] | undefined} classList
*/
const removeClass = (target, classList) => {
toggleClass(target, classList, false);
};
/**
* Get direct child of an element by class name
*
* @param {HTMLElement} elem
* @param {string} className
* @returns {HTMLElement | undefined}
*/
const getDirectChildByClass = (elem, className) => {
const children = Array.from(elem.children);
for (let i = 0; i < children.length; i++) {
const child = children[i];
if (child instanceof HTMLElement && hasClass(child, className)) {
return child;
}
}
};
/**
* @param {HTMLElement} elem
* @param {string} property
* @param {string | number | null | undefined} value
*/
const applyNumericalStyle = (elem, property, value) => {
if (value === `${parseInt(`${value}`)}`) {
value = parseInt(value);
}
if (value || parseInt(`${value}`) === 0) {
elem.style.setProperty(property, typeof value === 'number' ? `${value}px` : (/** @type {string} */value));
} else {
elem.style.removeProperty(property);
}
};
/**
* @param {HTMLElement | null} elem
* @param {string} display
*/
const show = (elem, display = 'flex') => {
if (!elem) {
return;
}
elem.style.display = display;
};
/**
* @param {HTMLElement | null} elem
*/
const hide = elem => {
if (!elem) {
return;
}
elem.style.display = 'none';
};
/**
* @param {HTMLElement | null} elem
* @param {string} display
*/
const showWhenInnerHtmlPresent = (elem, display = 'block') => {
if (!elem) {
return;
}
new MutationObserver(() => {
toggle(elem, elem.innerHTML, display);
}).observe(elem, {
childList: true,
subtree: true
});
};
/**
* @param {HTMLElement} parent
* @param {string} selector
* @param {string} property
* @param {string} value
*/
const setStyle = (parent, selector, property, value) => {
/** @type {HTMLElement | null} */
const el = parent.querySelector(selector);
if (el) {
el.style.setProperty(property, value);
}
};
/**
* @param {HTMLElement} elem
* @param {boolean | string | null | undefined} condition
* @param {string} display
*/
const toggle = (elem, condition, display = 'flex') => {
if (condition) {
show(elem, display);
} else {
hide(elem);
}
};
/**
* borrowed from jquery $(elem).is(':visible') implementation
*
* @param {HTMLElement | null} elem
* @returns {boolean}
*/
const isVisible$1 = elem => Boolean(elem && (elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length));
/**
* @returns {boolean}
*/
const allButtonsAreHidden = () => !isVisible$1(getConfirmButton()) && !isVisible$1(getDenyButton()) && !isVisible$1(getCancelButton());
/**
* @param {HTMLElement} elem
* @returns {boolean}
*/
const isScrollable = elem => Boolean(elem.scrollHeight > elem.clientHeight);
/**
* @param {HTMLElement} element
* @param {HTMLElement} stopElement
* @returns {boolean}
*/
const selfOrParentIsScrollable = (element, stopElement) => {
let parent = /** @type {HTMLElement | null} */element;
while (parent && parent !== stopElement) {
if (isScrollable(parent)) {
return true;
}
parent = parent.parentElement;
}
return false;
};
/**
* borrowed from https://stackoverflow.com/a/46352119
*
* @param {HTMLElement} elem
* @returns {boolean}
*/
const hasCssAnimation = elem => {
const style = window.getComputedStyle(elem);
const animDuration = parseFloat(style.getPropertyValue('animation-duration') || '0');
const transDuration = parseFloat(style.getPropertyValue('transition-duration') || '0');
return animDuration > 0 || transDuration > 0;
};
/**
* @param {number} timer
* @param {boolean} reset
*/
const animateTimerProgressBar = (timer, reset = false) => {
const timerProgressBar = getTimerProgressBar();
if (!timerProgressBar) {
return;
}
if (isVisible$1(timerProgressBar)) {
if (reset) {
timerProgressBar.style.transition = 'none';
timerProgressBar.style.width = '100%';
}
setTimeout(() => {
timerProgressBar.style.transition = `width ${timer / 1000}s linear`;
timerProgressBar.style.width = '0%';
}, 10);
}
};
const stopTimerProgressBar = () => {
const timerProgressBar = getTimerProgressBar();
if (!timerProgressBar) {
return;
}
const timerProgressBarWidth = parseInt(window.getComputedStyle(timerProgressBar).width);
timerProgressBar.style.removeProperty('transition');
timerProgressBar.style.width = '100%';
const timerProgressBarFullWidth = parseInt(window.getComputedStyle(timerProgressBar).width);
const timerProgressBarPercent = timerProgressBarWidth / timerProgressBarFullWidth * 100;
timerProgressBar.style.width = `${timerProgressBarPercent}%`;
};
/**
* Detect Node env
*
* @returns {boolean}
*/
const isNodeEnv = () => typeof window === 'undefined' || typeof document === 'undefined';
const sweetHTML = `
`.replace(/(^|\n)\s*/g, '');
/**
* @returns {boolean}
*/
const resetOldContainer = () => {
const oldContainer = getContainer();
if (!oldContainer) {
return false;
}
oldContainer.remove();
removeClass([document.documentElement, document.body], [swalClasses['no-backdrop'], swalClasses['toast-shown'],
// @ts-ignore: 'has-column' is not defined in swalClasses but may be set dynamically
swalClasses['has-column']]);
return true;
};
const resetValidationMessage$1 = () => {
if (globalState.currentInstance) {
globalState.currentInstance.resetValidationMessage();
}
};
const addInputChangeListeners = () => {
const popup = getPopup();
if (!popup) {
return;
}
const input = getDirectChildByClass(popup, swalClasses.input);
const file = getDirectChildByClass(popup, swalClasses.file);
/** @type {HTMLInputElement | null} */
const range = popup.querySelector(`.${swalClasses.range} input`);
/** @type {HTMLOutputElement | null} */
const rangeOutput = popup.querySelector(`.${swalClasses.range} output`);
const select = getDirectChildByClass(popup, swalClasses.select);
/** @type {HTMLInputElement | null} */
const checkbox = popup.querySelector(`.${swalClasses.checkbox} input`);
const textarea = getDirectChildByClass(popup, swalClasses.textarea);
if (input) {
input.oninput = resetValidationMessage$1;
}
if (file) {
file.onchange = resetValidationMessage$1;
}
if (select) {
select.onchange = resetValidationMessage$1;
}
if (checkbox) {
checkbox.onchange = resetValidationMessage$1;
}
if (textarea) {
textarea.oninput = resetValidationMessage$1;
}
if (range && rangeOutput) {
range.oninput = () => {
resetValidationMessage$1();
rangeOutput.value = range.value;
};
range.onchange = () => {
resetValidationMessage$1();
rangeOutput.value = range.value;
};
}
};
/**
* @param {string | HTMLElement} target
* @returns {HTMLElement}
*/
const getTarget = target => {
if (typeof target === 'string') {
const element = document.querySelector(target);
if (!element) {
throw new Error(`Target element "${target}" not found`);
}
return /** @type {HTMLElement} */element;
}
return target;
};
/**
* @param {SweetAlertOptions} params
*/
const setupAccessibility = params => {
const popup = getPopup();
if (!popup) {
return;
}
popup.setAttribute('role', params.toast ? 'alert' : 'dialog');
popup.setAttribute('aria-live', params.toast ? 'polite' : 'assertive');
if (!params.toast) {
popup.setAttribute('aria-modal', 'true');
}
};
/**
* @param {HTMLElement} targetElement
*/
const setupRTL = targetElement => {
if (window.getComputedStyle(targetElement).direction === 'rtl') {
addClass(getContainer(), swalClasses.rtl);
globalState.isRTL = true;
}
};
/**
* Add modal + backdrop to DOM
*
* @param {SweetAlertOptions} params
*/
const init = params => {
// Clean up the old popup container if it exists
const oldContainerExisted = resetOldContainer();
if (isNodeEnv()) {
error('SweetAlert2 requires document to initialize');
return;
}
const container = document.createElement('div');
container.className = swalClasses.container;
if (oldContainerExisted) {
addClass(container, swalClasses['no-transition']);
}
setInnerHtml(container, sweetHTML);
container.dataset['swal2Theme'] = params.theme;
const targetElement = getTarget(params.target || 'body');
targetElement.appendChild(container);
if (params.topLayer) {
container.setAttribute('popover', '');
container.showPopover();
}
setupAccessibility(params);
setupRTL(targetElement);
addInputChangeListeners();
};
/**
* @param {HTMLElement | object | string} param
* @param {HTMLElement} target
*/
const parseHtmlToContainer = (param, target) => {
// DOM element
if (param instanceof HTMLElement) {
target.appendChild(param);
}
// Object
else if (typeof param === 'object') {
handleObject(param, target);
}
// Plain string
else if (param) {
setInnerHtml(target, param);
}
};
/**
* @param {object} param
* @param {HTMLElement} target
*/
const handleObject = (param, target) => {
// JQuery element(s)
if ('jquery' in param) {
handleJqueryElem(target, param);
}
// For other objects use their string representation
else {
setInnerHtml(target, param.toString());
}
};
/**
* @param {HTMLElement} target
* @param {any} elem
*/
const handleJqueryElem = (target, elem) => {
target.textContent = '';
if (0 in elem) {
for (let i = 0; i in elem; i++) {
target.appendChild(elem[i].cloneNode(true));
}
} else {
target.appendChild(elem.cloneNode(true));
}
};
/**
* @param {SweetAlert} instance
* @param {SweetAlertOptions} params
*/
const renderActions = (instance, params) => {
const actions = getActions();
const loader = getLoader();
if (!actions || !loader) {
return;
}
// Actions (buttons) wrapper
if (!params.showConfirmButton && !params.showDenyButton && !params.showCancelButton) {
hide(actions);
} else {
show(actions);
}
// Custom class
applyCustomClass(actions, params, 'actions');
// Render all the buttons
renderButtons(actions, loader, params);
// Loader
setInnerHtml(loader, params.loaderHtml || '');
applyCustomClass(loader, params, 'loader');
};
/**
* @param {HTMLElement} actions
* @param {HTMLElement} loader
* @param {SweetAlertOptions} params
*/
function renderButtons(actions, loader, params) {
const confirmButton = getConfirmButton();
const denyButton = getDenyButton();
const cancelButton = getCancelButton();
if (!confirmButton || !denyButton || !cancelButton) {
return;
}
// Render buttons
renderButton(confirmButton, 'confirm', params);
renderButton(denyButton, 'deny', params);
renderButton(cancelButton, 'cancel', params);
handleButtonsStyling(confirmButton, denyButton, cancelButton, params);
if (params.reverseButtons) {
if (params.toast) {
actions.insertBefore(cancelButton, confirmButton);
actions.insertBefore(denyButton, confirmButton);
} else {
actions.insertBefore(cancelButton, loader);
actions.insertBefore(denyButton, loader);
actions.insertBefore(confirmButton, loader);
}
}
}
/**
* @param {HTMLElement} confirmButton
* @param {HTMLElement} denyButton
* @param {HTMLElement} cancelButton
* @param {SweetAlertOptions} params
*/
function handleButtonsStyling(confirmButton, denyButton, cancelButton, params) {
if (!params.buttonsStyling) {
removeClass([confirmButton, denyButton, cancelButton], swalClasses.styled);
return;
}
addClass([confirmButton, denyButton, cancelButton], swalClasses.styled);
// Apply custom background colors to action buttons
if (params.confirmButtonColor) {
confirmButton.style.setProperty('--swal2-confirm-button-background-color', params.confirmButtonColor);
}
if (params.denyButtonColor) {
denyButton.style.setProperty('--swal2-deny-button-background-color', params.denyButtonColor);
}
if (params.cancelButtonColor) {
cancelButton.style.setProperty('--swal2-cancel-button-background-color', params.cancelButtonColor);
}
// Apply the outline color to action buttons
applyOutlineColor(confirmButton);
applyOutlineColor(denyButton);
applyOutlineColor(cancelButton);
}
/**
* @param {HTMLElement} button
*/
function applyOutlineColor(button) {
const buttonStyle = window.getComputedStyle(button);
if (buttonStyle.getPropertyValue('--swal2-action-button-focus-box-shadow')) {
// If the button already has a custom outline color, no need to change it
return;
}
const outlineColor = buttonStyle.backgroundColor.replace(/rgba?\((\d+), (\d+), (\d+).*/, 'rgba($1, $2, $3, 0.5)');
button.style.setProperty('--swal2-action-button-focus-box-shadow', buttonStyle.getPropertyValue('--swal2-outline').replace(/ rgba\(.*/, ` ${outlineColor}`));
}
/**
* @param {HTMLElement} button
* @param {'confirm' | 'deny' | 'cancel'} buttonType
* @param {SweetAlertOptions} params
*/
function renderButton(button, buttonType, params) {
const buttonName = /** @type {'Confirm' | 'Deny' | 'Cancel'} */capitalizeFirstLetter(buttonType);
toggle(button, params[`show${buttonName}Button`], 'inline-block');
setInnerHtml(button, params[`${buttonType}ButtonText`] || ''); // Set caption text
button.setAttribute('aria-label', params[`${buttonType}ButtonAriaLabel`] || ''); // ARIA label
// Add buttons custom classes
button.className = swalClasses[buttonType];
applyCustomClass(button, params, `${buttonType}Button`);
}
/**
* @param {SweetAlert} instance
* @param {SweetAlertOptions} params
*/
const renderCloseButton = (instance, params) => {
const closeButton = getCloseButton();
if (!closeButton) {
return;
}
setInnerHtml(closeButton, params.closeButtonHtml || '');
// Custom class
applyCustomClass(closeButton, params, 'closeButton');
toggle(closeButton, params.showCloseButton);
closeButton.setAttribute('aria-label', params.closeButtonAriaLabel || '');
};
/**
* @param {SweetAlert} instance
* @param {SweetAlertOptions} params
*/
const renderContainer = (instance, params) => {
const container = getContainer();
if (!container) {
return;
}
handleBackdropParam(container, params.backdrop);
handlePositionParam(container, params.position);
handleGrowParam(container, params.grow);
// Custom class
applyCustomClass(container, params, 'container');
};
/**
* @param {HTMLElement} container
* @param {SweetAlertOptions['backdrop']} backdrop
*/
function handleBackdropParam(container, backdrop) {
if (typeof backdrop === 'string') {
container.style.background = backdrop;
} else if (!backdrop) {
addClass([document.documentElement, document.body], swalClasses['no-backdrop']);
}
}
/**
* @param {HTMLElement} container
* @param {SweetAlertOptions['position']} position
*/
function handlePositionParam(container, position) {
if (!position) {
return;
}
if (position in swalClasses) {
addClass(container, swalClasses[position]);
} else {
warn('The "position" parameter is not valid, defaulting to "center"');
addClass(container, swalClasses.center);
}
}
/**
* @param {HTMLElement} container
* @param {SweetAlertOptions['grow']} grow
*/
function handleGrowParam(container, grow) {
if (!grow) {
return;
}
addClass(container, swalClasses[`grow-${grow}`]);
}
/**
* This module contains `WeakMap`s for each effectively-"private property" that a `Swal` has.
* For example, to set the private property "foo" of `this` to "bar", you can `privateProps.foo.set(this, 'bar')`
* This is the approach that Babel will probably take to implement private methods/fields
* https://github.com/tc39/proposal-private-methods
* https://github.com/babel/babel/pull/7555
* Once we have the changes from that PR in Babel, and our core class fits reasonable in *one module*
* then we can use that language feature.
*/
var privateProps = {
innerParams: new WeakMap(),
domCache: new WeakMap()
};
///
/** @type {InputClass[]} */
const inputClasses = ['input', 'file', 'range', 'select', 'radio', 'checkbox', 'textarea'];
/**
* @param {SweetAlert} instance
* @param {SweetAlertOptions} params
*/
const renderInput = (instance, params) => {
const popup = getPopup();
if (!popup) {
return;
}
const innerParams = privateProps.innerParams.get(instance);
const rerender = !innerParams || params.input !== innerParams.input;
inputClasses.forEach(inputClass => {
const inputContainer = getDirectChildByClass(popup, swalClasses[inputClass]);
if (!inputContainer) {
return;
}
// set attributes
setAttributes(inputClass, params.inputAttributes);
// set class
inputContainer.className = swalClasses[inputClass];
if (rerender) {
hide(inputContainer);
}
});
if (params.input) {
if (rerender) {
showInput(params);
}
// set custom class
setCustomClass(params);
}
};
/**
* @param {SweetAlertOptions} params
*/
const showInput = params => {
if (!params.input) {
return;
}
if (!renderInputType[params.input]) {
error(`Unexpected type of input! Expected ${Object.keys(renderInputType).join(' | ')}, got "${params.input}"`);
return;
}
const inputContainer = getInputContainer(params.input);
if (!inputContainer) {
return;
}
const input = renderInputType[params.input](inputContainer, params);
show(inputContainer);
// input autofocus
if (params.inputAutoFocus) {
setTimeout(() => {
focusInput(input);
});
}
};
/**
* @param {HTMLInputElement} input
*/
const removeAttributes = input => {
for (let i = 0; i < input.attributes.length; i++) {
const attrName = input.attributes[i].name;
if (!['id', 'type', 'value', 'style'].includes(attrName)) {
input.removeAttribute(attrName);
}
}
};
/**
* @param {InputClass} inputClass
* @param {SweetAlertOptions['inputAttributes']} inputAttributes
*/
const setAttributes = (inputClass, inputAttributes) => {
const popup = getPopup();
if (!popup) {
return;
}
const input = getInput$1(popup, inputClass);
if (!input) {
return;
}
removeAttributes(input);
for (const attr in inputAttributes) {
input.setAttribute(attr, inputAttributes[attr]);
}
};
/**
* @param {SweetAlertOptions} params
*/
const setCustomClass = params => {
if (!params.input) {
return;
}
const inputContainer = getInputContainer(params.input);
if (inputContainer) {
applyCustomClass(inputContainer, params, 'input');
}
};
/**
* @param {HTMLInputElement | HTMLTextAreaElement} input
* @param {SweetAlertOptions} params
*/
const setInputPlaceholder = (input, params) => {
if (!input.placeholder && params.inputPlaceholder) {
input.placeholder = params.inputPlaceholder;
}
};
/**
* @param {Input} input
* @param {Input} prependTo
* @param {SweetAlertOptions} params
*/
const setInputLabel = (input, prependTo, params) => {
if (params.inputLabel) {
const label = document.createElement('label');
const labelClass = swalClasses['input-label'];
label.setAttribute('for', input.id);
label.className = labelClass;
if (typeof params.customClass === 'object') {
addClass(label, params.customClass.inputLabel);
}
label.innerText = params.inputLabel;
prependTo.insertAdjacentElement('beforebegin', label);
}
};
/**
* @param {SweetAlertInput} inputType
* @returns {HTMLElement | undefined}
*/
const getInputContainer = inputType => {
const popup = getPopup();
if (!popup) {
return;
}
return getDirectChildByClass(popup, swalClasses[(/** @type {SwalClass} */inputType)] || swalClasses.input);
};
/**
* @param {HTMLInputElement | HTMLOutputElement | HTMLTextAreaElement} input
* @param {SweetAlertOptions['inputValue']} inputValue
*/
const checkAndSetInputValue = (input, inputValue) => {
if (['string', 'number'].includes(typeof inputValue)) {
input.value = `${inputValue}`;
} else if (!isPromise(inputValue)) {
warn(`Unexpected type of inputValue! Expected "string", "number" or "Promise", got "${typeof inputValue}"`);
}
};
/** @type {Record Input>} */
const renderInputType = {};
/**
* @param {Input | HTMLElement} input
* @param {SweetAlertOptions} params
* @returns {Input}
*/
renderInputType.text = renderInputType.email = renderInputType.password = renderInputType.number = renderInputType.tel = renderInputType.url = renderInputType.search = renderInputType.date = renderInputType['datetime-local'] = renderInputType.time = renderInputType.week = renderInputType.month = /** @type {(input: Input | HTMLElement, params: SweetAlertOptions) => Input} */
(input, params) => {
const inputElement = /** @type {HTMLInputElement} */input;
checkAndSetInputValue(inputElement, params.inputValue);
setInputLabel(inputElement, inputElement, params);
setInputPlaceholder(inputElement, params);
inputElement.type = /** @type {string} */params.input;
return inputElement;
};
/**
* @param {Input | HTMLElement} input
* @param {SweetAlertOptions} params
* @returns {Input}
*/
renderInputType.file = (input, params) => {
const inputElement = /** @type {HTMLInputElement} */input;
setInputLabel(inputElement, inputElement, params);
setInputPlaceholder(inputElement, params);
return inputElement;
};
/**
* @param {Input | HTMLElement} range
* @param {SweetAlertOptions} params
* @returns {Input}
*/
renderInputType.range = (range, params) => {
const rangeContainer = /** @type {HTMLElement} */range;
const rangeInput = rangeContainer.querySelector('input');
const rangeOutput = rangeContainer.querySelector('output');
if (rangeInput) {
checkAndSetInputValue(rangeInput, params.inputValue);
rangeInput.type = /** @type {string} */params.input;
setInputLabel(rangeInput, /** @type {Input} */range, params);
}
if (rangeOutput) {
checkAndSetInputValue(rangeOutput, params.inputValue);
}
return /** @type {Input} */range;
};
/**
* @param {Input | HTMLElement} select
* @param {SweetAlertOptions} params
* @returns {Input}
*/
renderInputType.select = (select, params) => {
const selectElement = /** @type {HTMLSelectElement} */select;
selectElement.textContent = '';
if (params.inputPlaceholder) {
const placeholder = document.createElement('option');
setInnerHtml(placeholder, params.inputPlaceholder);
placeholder.value = '';
placeholder.disabled = true;
placeholder.selected = true;
selectElement.appendChild(placeholder);
}
setInputLabel(selectElement, selectElement, params);
return selectElement;
};
/**
* @param {Input | HTMLElement} radio
* @returns {Input}
*/
renderInputType.radio = radio => {
const radioElement = /** @type {HTMLElement} */radio;
radioElement.textContent = '';
return /** @type {Input} */radio;
};
/**
* @param {Input | HTMLElement} checkboxContainer
* @param {SweetAlertOptions} params
* @returns {Input}
*/
renderInputType.checkbox = (checkboxContainer, params) => {
const popup = getPopup();
if (!popup) {
throw new Error('Popup not found');
}
const checkbox = getInput$1(popup, 'checkbox');
if (!checkbox) {
throw new Error('Checkbox input not found');
}
checkbox.value = '1';
checkbox.checked = Boolean(params.inputValue);
const containerElement = /** @type {HTMLElement} */checkboxContainer;
const label = containerElement.querySelector('span');
if (label) {
const placeholderOrLabel = params.inputPlaceholder || params.inputLabel;
if (placeholderOrLabel) {
setInnerHtml(label, placeholderOrLabel);
}
}
return checkbox;
};
/**
* @param {Input | HTMLElement} textarea
* @param {SweetAlertOptions} params
* @returns {Input}
*/
renderInputType.textarea = (textarea, params) => {
const textareaElement = /** @type {HTMLTextAreaElement} */textarea;
checkAndSetInputValue(textareaElement, params.inputValue);
setInputPlaceholder(textareaElement, params);
setInputLabel(textareaElement, textareaElement, params);
/**
* @param {HTMLElement} el
* @returns {number}
*/
const getMargin = el => parseInt(window.getComputedStyle(el).marginLeft) + parseInt(window.getComputedStyle(el).marginRight);
// https://github.com/sweetalert2/sweetalert2/issues/2291
setTimeout(() => {
// https://github.com/sweetalert2/sweetalert2/issues/1699
if ('MutationObserver' in window) {
const popup = getPopup();
if (!popup) {
return;
}
const initialPopupWidth = parseInt(window.getComputedStyle(popup).width);
const textareaResizeHandler = () => {
// check if texarea is still in document (i.e. popup wasn't closed in the meantime)
if (!document.body.contains(textareaElement)) {
return;
}
const textareaWidth = textareaElement.offsetWidth + getMargin(textareaElement);
const popupElement = getPopup();
if (popupElement) {
if (textareaWidth > initialPopupWidth) {
popupElement.style.width = `${textareaWidth}px`;
} else {
applyNumericalStyle(popupElement, 'width', params.width);
}
}
};
new MutationObserver(textareaResizeHandler).observe(textareaElement, {
attributes: true,
attributeFilter: ['style']
});
}
});
return textareaElement;
};
/**
* @param {SweetAlert} instance
* @param {SweetAlertOptions} params
*/
const renderContent = (instance, params) => {
const htmlContainer = getHtmlContainer();
if (!htmlContainer) {
return;
}
showWhenInnerHtmlPresent(htmlContainer);
applyCustomClass(htmlContainer, params, 'htmlContainer');
// Content as HTML
if (params.html) {
parseHtmlToContainer(params.html, htmlContainer);
show(htmlContainer, 'block');
}
// Content as plain text
else if (params.text) {
htmlContainer.textContent = params.text;
show(htmlContainer, 'block');
}
// No content
else {
hide(htmlContainer);
}
renderInput(instance, params);
};
/**
* @param {SweetAlert} instance
* @param {SweetAlertOptions} params
*/
const renderFooter = (instance, params) => {
const footer = getFooter();
if (!footer) {
return;
}
showWhenInnerHtmlPresent(footer);
toggle(footer, Boolean(params.footer), 'block');
if (params.footer) {
parseHtmlToContainer(params.footer, footer);
}
// Custom class
applyCustomClass(footer, params, 'footer');
};
/**
* @param {SweetAlert} instance
* @param {SweetAlertOptions} params
*/
const renderIcon = (instance, params) => {
const innerParams = privateProps.innerParams.get(instance);
const icon = getIcon();
if (!icon) {
return;
}
// if the given icon already rendered, apply the styling without re-rendering the icon
if (innerParams && params.icon === innerParams.icon) {
// Custom or default content
setContent(icon, params);
applyStyles(icon, params);
return;
}
if (!params.icon && !params.iconHtml) {
hide(icon);
return;
}
if (params.icon && Object.keys(iconTypes).indexOf(params.icon) === -1) {
error(`Unknown icon! Expected "success", "error", "warning", "info" or "question", got "${params.icon}"`);
hide(icon);
return;
}
show(icon);
// Custom or default content
setContent(icon, params);
applyStyles(icon, params);
// Animate icon
addClass(icon, params.showClass && params.showClass.icon);
// Re-adjust the success icon on system theme change
const colorSchemeQueryList = window.matchMedia('(prefers-color-scheme: dark)');
colorSchemeQueryList.addEventListener('change', adjustSuccessIconBackgroundColor);
};
/**
* @param {HTMLElement} icon
* @param {SweetAlertOptions} params
*/
const applyStyles = (icon, params) => {
for (const [iconType, iconClassName] of Object.entries(iconTypes)) {
if (params.icon !== iconType) {
removeClass(icon, iconClassName);
}
}
addClass(icon, params.icon && iconTypes[params.icon]);
// Icon color
setColor(icon, params);
// Success icon background color
adjustSuccessIconBackgroundColor();
// Custom class
applyCustomClass(icon, params, 'icon');
};
// Adjust success icon background color to match the popup background color
const adjustSuccessIconBackgroundColor = () => {
const popup = getPopup();
if (!popup) {
return;
}
const popupBackgroundColor = window.getComputedStyle(popup).getPropertyValue('background-color');
/** @type {NodeListOf} */
const successIconParts = popup.querySelectorAll('[class^=swal2-success-circular-line], .swal2-success-fix');
for (let i = 0; i < successIconParts.length; i++) {
successIconParts[i].style.backgroundColor = popupBackgroundColor;
}
};
/**
*
* @param {SweetAlertOptions} params
* @returns {string}
*/
const successIconHtml = params => `
${params.animation ? '' : ''}
${params.animation ? '' : ''}
${params.animation ? '' : ''}
`;
const errorIconHtml = `
`;
/**
* @param {HTMLElement} icon
* @param {SweetAlertOptions} params
*/
const setContent = (icon, params) => {
if (!params.icon && !params.iconHtml) {
return;
}
let oldContent = icon.innerHTML;
let newContent = '';
if (params.iconHtml) {
newContent = iconContent(params.iconHtml);
} else if (params.icon === 'success') {
newContent = successIconHtml(params);
oldContent = oldContent.replace(/ style=".*?"/g, ''); // undo adjustSuccessIconBackgroundColor()
} else if (params.icon === 'error') {
newContent = errorIconHtml;
} else if (params.icon) {
const defaultIconHtml = {
question: '?',
warning: '!',
info: 'i'
};
newContent = iconContent(defaultIconHtml[params.icon]);
}
if (oldContent.trim() !== newContent.trim()) {
setInnerHtml(icon, newContent);
}
};
/**
* @param {HTMLElement} icon
* @param {SweetAlertOptions} params
*/
const setColor = (icon, params) => {
if (!params.iconColor) {
return;
}
icon.style.color = params.iconColor;
icon.style.borderColor = params.iconColor;
for (const sel of ['.swal2-success-line-tip', '.swal2-success-line-long', '.swal2-x-mark-line-left', '.swal2-x-mark-line-right']) {
setStyle(icon, sel, 'background-color', params.iconColor);
}
setStyle(icon, '.swal2-success-ring', 'border-color', params.iconColor);
};
/**
* @param {string} content
* @returns {string}
*/
const iconContent = content => `${content}
`;
/**
* @param {SweetAlert} instance
* @param {SweetAlertOptions} params
*/
const renderImage = (instance, params) => {
const image = getImage();
if (!image) {
return;
}
if (!params.imageUrl) {
hide(image);
return;
}
show(image, '');
// Src, alt
image.setAttribute('src', params.imageUrl);
image.setAttribute('alt', params.imageAlt || '');
// Width, height
applyNumericalStyle(image, 'width', params.imageWidth);
applyNumericalStyle(image, 'height', params.imageHeight);
// Class
image.className = swalClasses.image;
applyCustomClass(image, params, 'image');
};
let dragging = false;
let mousedownX = 0;
let mousedownY = 0;
let initialX = 0;
let initialY = 0;
/**
* @param {HTMLElement} popup
*/
const addDraggableListeners = popup => {
popup.addEventListener('mousedown', down);
document.body.addEventListener('mousemove', move);
popup.addEventListener('mouseup', up);
popup.addEventListener('touchstart', down);
document.body.addEventListener('touchmove', move);
popup.addEventListener('touchend', up);
};
/**
* @param {HTMLElement} popup
*/
const removeDraggableListeners = popup => {
popup.removeEventListener('mousedown', down);
document.body.removeEventListener('mousemove', move);
popup.removeEventListener('mouseup', up);
popup.removeEventListener('touchstart', down);
document.body.removeEventListener('touchmove', move);
popup.removeEventListener('touchend', up);
};
/**
* @param {MouseEvent | TouchEvent} event
*/
const down = event => {
const popup = getPopup();
if (!popup) {
return;
}
const icon = getIcon();
if (event.target === popup || icon && icon.contains(/** @type {HTMLElement} */event.target)) {
dragging = true;
const clientXY = getClientXY(event);
mousedownX = clientXY.clientX;
mousedownY = clientXY.clientY;
initialX = parseInt(popup.style.insetInlineStart) || 0;
initialY = parseInt(popup.style.insetBlockStart) || 0;
addClass(popup, 'swal2-dragging');
}
};
/**
* @param {MouseEvent | TouchEvent} event
*/
const move = event => {
const popup = getPopup();
if (!popup) {
return;
}
if (dragging) {
let {
clientX,
clientY
} = getClientXY(event);
const deltaX = clientX - mousedownX;
// In RTL mode, negate the horizontal delta since insetInlineStart refers to the right edge
popup.style.insetInlineStart = `${initialX + (globalState.isRTL ? -deltaX : deltaX)}px`;
popup.style.insetBlockStart = `${initialY + (clientY - mousedownY)}px`;
}
};
const up = () => {
const popup = getPopup();
dragging = false;
removeClass(popup, 'swal2-dragging');
};
/**
* @param {MouseEvent | TouchEvent} event
* @returns {{ clientX: number, clientY: number }}
*/
const getClientXY = event => {
let clientX = 0,
clientY = 0;
if (event.type.startsWith('mouse')) {
clientX = /** @type {MouseEvent} */event.clientX;
clientY = /** @type {MouseEvent} */event.clientY;
} else if (event.type.startsWith('touch')) {
clientX = /** @type {TouchEvent} */event.touches[0].clientX;
clientY = /** @type {TouchEvent} */event.touches[0].clientY;
}
return {
clientX,
clientY
};
};
/**
* @param {SweetAlert} instance
* @param {SweetAlertOptions} params
*/
const renderPopup = (instance, params) => {
const container = getContainer();
const popup = getPopup();
if (!container || !popup) {
return;
}
// Width
// https://github.com/sweetalert2/sweetalert2/issues/2170
if (params.toast) {
applyNumericalStyle(container, 'width', params.width);
popup.style.width = '100%';
const loader = getLoader();
if (loader) {
popup.insertBefore(loader, getIcon());
}
} else {
applyNumericalStyle(popup, 'width', params.width);
}
// Padding
applyNumericalStyle(popup, 'padding', params.padding);
// Color
if (params.color) {
popup.style.color = params.color;
}
// Background
if (params.background) {
popup.style.background = params.background;
}
hide(getValidationMessage());
// Classes
addClasses$1(popup, params);
if (params.draggable && !params.toast) {
addClass(popup, swalClasses.draggable);
addDraggableListeners(popup);
} else {
removeClass(popup, swalClasses.draggable);
removeDraggableListeners(popup);
}
};
/**
* @param {HTMLElement} popup
* @param {SweetAlertOptions} params
*/
const addClasses$1 = (popup, params) => {
const showClass = params.showClass || {};
// Default Class + showClass when updating Swal.update({})
popup.className = `${swalClasses.popup} ${isVisible$1(popup) ? showClass.popup : ''}`;
if (params.toast) {
addClass([document.documentElement, document.body], swalClasses['toast-shown']);
addClass(popup, swalClasses.toast);
} else {
addClass(popup, swalClasses.modal);
}
// Custom class
applyCustomClass(popup, params, 'popup');
// TODO: remove in the next major
if (typeof params.customClass === 'string') {
addClass(popup, params.customClass);
}
// Icon class (#1842)
if (params.icon) {
addClass(popup, swalClasses[`icon-${params.icon}`]);
}
};
/**
* @param {SweetAlert} instance
* @param {SweetAlertOptions} params
*/
const renderProgressSteps = (instance, params) => {
const progressStepsContainer = getProgressSteps();
if (!progressStepsContainer) {
return;
}
const {
progressSteps,
currentProgressStep
} = params;
if (!progressSteps || progressSteps.length === 0 || currentProgressStep === undefined) {
hide(progressStepsContainer);
return;
}
show(progressStepsContainer);
progressStepsContainer.textContent = '';
if (currentProgressStep >= progressSteps.length) {
warn('Invalid currentProgressStep parameter, it should be less than progressSteps.length ' + '(currentProgressStep like JS arrays starts from 0)');
}
progressSteps.forEach((step, index) => {
const stepEl = createStepElement(step);
progressStepsContainer.appendChild(stepEl);
if (index === currentProgressStep) {
addClass(stepEl, swalClasses['active-progress-step']);
}
if (index !== progressSteps.length - 1) {
const lineEl = createLineElement(params);
progressStepsContainer.appendChild(lineEl);
}
});
};
/**
* @param {string} step
* @returns {HTMLLIElement}
*/
const createStepElement = step => {
const stepEl = document.createElement('li');
addClass(stepEl, swalClasses['progress-step']);
setInnerHtml(stepEl, step);
return stepEl;
};
/**
* @param {SweetAlertOptions} params
* @returns {HTMLLIElement}
*/
const createLineElement = params => {
const lineEl = document.createElement('li');
addClass(lineEl, swalClasses['progress-step-line']);
if (params.progressStepsDistance) {
applyNumericalStyle(lineEl, 'width', params.progressStepsDistance);
}
return lineEl;
};
/**
* @param {SweetAlert} instance
* @param {SweetAlertOptions} params
*/
const renderTitle = (instance, params) => {
const title = getTitle();
if (!title) {
return;
}
showWhenInnerHtmlPresent(title);
toggle(title, Boolean(params.title || params.titleText), 'block');
if (params.title) {
parseHtmlToContainer(params.title, title);
}
if (params.titleText) {
title.innerText = params.titleText;
}
// Custom class
applyCustomClass(title, params, 'title');
};
/**
* @param {SweetAlert} instance
* @param {SweetAlertOptions} params
*/
const render = (instance, params) => {
var _globalState$eventEmi;
renderPopup(instance, params);
renderContainer(instance, params);
renderProgressSteps(instance, params);
renderIcon(instance, params);
renderImage(instance, params);
renderTitle(instance, params);
renderCloseButton(instance, params);
renderContent(instance, params);
renderActions(instance, params);
renderFooter(instance, params);
const popup = getPopup();
if (typeof params.didRender === 'function' && popup) {
params.didRender(popup);
}
(_globalState$eventEmi = globalState.eventEmitter) === null || _globalState$eventEmi === void 0 || _globalState$eventEmi.emit('didRender', popup);
};
/*
* Global function to determine if SweetAlert2 popup is shown
*/
const isVisible = () => {
return isVisible$1(getPopup());
};
/*
* Global function to click 'Confirm' button
*/
const clickConfirm = () => {
var _dom$getConfirmButton;
return (_dom$getConfirmButton = getConfirmButton()) === null || _dom$getConfirmButton === void 0 ? void 0 : _dom$getConfirmButton.click();
};
/*
* Global function to click 'Deny' button
*/
const clickDeny = () => {
var _dom$getDenyButton;
return (_dom$getDenyButton = getDenyButton()) === null || _dom$getDenyButton === void 0 ? void 0 : _dom$getDenyButton.click();
};
/*
* Global function to click 'Cancel' button
*/
const clickCancel = () => {
var _dom$getCancelButton;
return (_dom$getCancelButton = getCancelButton()) === null || _dom$getCancelButton === void 0 ? void 0 : _dom$getCancelButton.click();
};
/** @type {Record} */
const DismissReason = Object.freeze({
cancel: 'cancel',
backdrop: 'backdrop',
close: 'close',
esc: 'esc',
timer: 'timer'
});
/**
* @param {GlobalState} globalState
*/
const removeKeydownHandler = globalState => {
if (globalState.keydownTarget && globalState.keydownHandlerAdded && globalState.keydownHandler) {
const handler = /** @type {EventListenerOrEventListenerObject} */ /** @type {unknown} */globalState.keydownHandler;
globalState.keydownTarget.removeEventListener('keydown', handler, {
capture: globalState.keydownListenerCapture
});
globalState.keydownHandlerAdded = false;
}
};
/**
* @param {GlobalState} globalState
* @param {SweetAlertOptions} innerParams
* @param {(dismiss: DismissReason) => void} dismissWith
*/
const addKeydownHandler = (globalState, innerParams, dismissWith) => {
removeKeydownHandler(globalState);
if (!innerParams.toast) {
/** @type {(this: HTMLElement, event: KeyboardEvent) => void} */
const handler = e => keydownHandler(innerParams, e, dismissWith);
globalState.keydownHandler = handler;
const target = innerParams.keydownListenerCapture ? window : getPopup();
if (target) {
globalState.keydownTarget = target;
globalState.keydownListenerCapture = innerParams.keydownListenerCapture;
const eventHandler = /** @type {EventListenerOrEventListenerObject} */ /** @type {unknown} */handler;
globalState.keydownTarget.addEventListener('keydown', eventHandler, {
capture: globalState.keydownListenerCapture
});
globalState.keydownHandlerAdded = true;
}
}
};
/**
* @param {number} index
* @param {number} increment
*/
const setFocus = (index, increment) => {
var _dom$getPopup;
const focusableElements = getFocusableElements();
// search for visible elements and select the next possible match
if (focusableElements.length) {
index = index + increment;
// shift + tab when .swal2-popup is focused
if (index === -2) {
index = focusableElements.length - 1;
}
// rollover to first item
if (index === focusableElements.length) {
index = 0;
// go to last item
} else if (index === -1) {
index = focusableElements.length - 1;
}
focusableElements[index].focus();
return;
}
// no visible focusable elements, focus the popup
(_dom$getPopup = getPopup()) === null || _dom$getPopup === void 0 || _dom$getPopup.focus();
};
const arrowKeysNextButton = ['ArrowRight', 'ArrowDown'];
const arrowKeysPreviousButton = ['ArrowLeft', 'ArrowUp'];
/**
* @param {SweetAlertOptions} innerParams
* @param {KeyboardEvent} event
* @param {(dismiss: DismissReason) => void} dismissWith
*/
const keydownHandler = (innerParams, event, dismissWith) => {
if (!innerParams) {
return; // This instance has already been destroyed
}
// Ignore keydown during IME composition
// https://developer.mozilla.org/en-US/docs/Web/API/Document/keydown_event#ignoring_keydown_during_ime_composition
// https://github.com/sweetalert2/sweetalert2/issues/720
// https://github.com/sweetalert2/sweetalert2/issues/2406
if (event.isComposing || event.keyCode === 229) {
return;
}
if (innerParams.stopKeydownPropagation) {
event.stopPropagation();
}
// ENTER
if (event.key === 'Enter') {
handleEnter(event, innerParams);
}
// TAB
else if (event.key === 'Tab') {
handleTab(event);
}
// ARROWS - switch focus between buttons
else if ([...arrowKeysNextButton, ...arrowKeysPreviousButton].includes(event.key)) {
handleArrows(event.key);
}
// ESC
else if (event.key === 'Escape') {
handleEsc(event, innerParams, dismissWith);
}
};
/**
* @param {KeyboardEvent} event
* @param {SweetAlertOptions} innerParams
*/
const handleEnter = (event, innerParams) => {
// https://github.com/sweetalert2/sweetalert2/issues/2386
if (!callIfFunction(innerParams.allowEnterKey)) {
return;
}
const popup = getPopup();
if (!popup || !innerParams.input) {
return;
}
const input = getInput$1(popup, innerParams.input);
if (event.target && input && event.target instanceof HTMLElement && event.target.outerHTML === input.outerHTML) {
if (['textarea', 'file'].includes(innerParams.input)) {
return; // do not submit
}
clickConfirm();
event.preventDefault();
}
};
/**
* @param {KeyboardEvent} event
*/
const handleTab = event => {
const targetElement = event.target;
const focusableElements = getFocusableElements();
let btnIndex = -1;
for (let i = 0; i < focusableElements.length; i++) {
if (targetElement === focusableElements[i]) {
btnIndex = i;
break;
}
}
// Cycle to the next button
if (!event.shiftKey) {
setFocus(btnIndex, 1);
}
// Cycle to the prev button
else {
setFocus(btnIndex, -1);
}
event.stopPropagation();
event.preventDefault();
};
/**
* @param {string} key
*/
const handleArrows = key => {
const actions = getActions();
const confirmButton = getConfirmButton();
const denyButton = getDenyButton();
const cancelButton = getCancelButton();
if (!actions || !confirmButton || !denyButton || !cancelButton) {
return;
}
/** @type HTMLElement[] */
const buttons = [confirmButton, denyButton, cancelButton];
if (document.activeElement instanceof HTMLElement && !buttons.includes(document.activeElement)) {
return;
}
const sibling = arrowKeysNextButton.includes(key) ? 'nextElementSibling' : 'previousElementSibling';
let buttonToFocus = document.activeElement;
if (!buttonToFocus) {
return;
}
for (let i = 0; i < actions.children.length; i++) {
buttonToFocus = buttonToFocus[sibling];
if (!buttonToFocus) {
return;
}
if (buttonToFocus instanceof HTMLButtonElement && isVisible$1(buttonToFocus)) {
break;
}
}
if (buttonToFocus instanceof HTMLButtonElement) {
buttonToFocus.focus();
}
};
/**
* @param {KeyboardEvent} event
* @param {SweetAlertOptions} innerParams
* @param {(dismiss: DismissReason) => void} dismissWith
*/
const handleEsc = (event, innerParams, dismissWith) => {
event.preventDefault();
if (callIfFunction(innerParams.allowEscapeKey)) {
dismissWith(DismissReason.esc);
}
};
/**
* This module contains `WeakMap`s for each effectively-"private property" that a `Swal` has.
* For example, to set the private property "foo" of `this` to "bar", you can `privateProps.foo.set(this, 'bar')`
* This is the approach that Babel will probably take to implement private methods/fields
* https://github.com/tc39/proposal-private-methods
* https://github.com/babel/babel/pull/7555
* Once we have the changes from that PR in Babel, and our core class fits reasonable in *one module*
* then we can use that language feature.
*/
var privateMethods = {
swalPromiseResolve: new WeakMap(),
swalPromiseReject: new WeakMap()
};
// From https://developer.paciellogroup.com/blog/2018/06/the-current-state-of-modal-dialog-accessibility/
// Adding aria-hidden="true" to elements outside of the active modal dialog ensures that
// elements not within the active modal dialog will not be surfaced if a user opens a screen
// reader’s list of elements (headings, form controls, landmarks, etc.) in the document.
const setAriaHidden = () => {
const container = getContainer();
const bodyChildren = Array.from(document.body.children);
bodyChildren.forEach(el => {
if (el.contains(container)) {
return;
}
if (el.hasAttribute('aria-hidden')) {
el.setAttribute('data-previous-aria-hidden', el.getAttribute('aria-hidden') || '');
}
el.setAttribute('aria-hidden', 'true');
});
};
const unsetAriaHidden = () => {
const bodyChildren = Array.from(document.body.children);
bodyChildren.forEach(el => {
if (el.hasAttribute('data-previous-aria-hidden')) {
el.setAttribute('aria-hidden', el.getAttribute('data-previous-aria-hidden') || '');
el.removeAttribute('data-previous-aria-hidden');
} else {
el.removeAttribute('aria-hidden');
}
});
};
// @ts-ignore
const isSafariOrIOS = typeof window !== 'undefined' && Boolean(window.GestureEvent); // true for Safari desktop + all iOS browsers https://stackoverflow.com/a/70585394
/**
* Fix iOS scrolling
* http://stackoverflow.com/q/39626302
*/
const iOSfix = () => {
if (isSafariOrIOS && !hasClass(document.body, swalClasses.iosfix)) {
const offset = document.body.scrollTop;
document.body.style.top = `${offset * -1}px`;
addClass(document.body, swalClasses.iosfix);
lockBodyScroll();
}
};
/**
* https://github.com/sweetalert2/sweetalert2/issues/1246
*/
const lockBodyScroll = () => {
const container = getContainer();
if (!container) {
return;
}
/** @type {boolean} */
let preventTouchMove;
/**
* @param {TouchEvent} event
*/
container.ontouchstart = event => {
preventTouchMove = shouldPreventTouchMove(event);
};
/**
* @param {TouchEvent} event
*/
container.ontouchmove = event => {
if (preventTouchMove) {
event.preventDefault();
event.stopPropagation();
}
};
};
/**
* @param {TouchEvent} event
* @returns {boolean}
*/
const shouldPreventTouchMove = event => {
const target = event.target;
const container = getContainer();
const htmlContainer = getHtmlContainer();
if (!container || !htmlContainer) {
return false;
}
if (isStylus(event) || isZoom(event)) {
return false;
}
if (target === container) {
return true;
}
if (!isScrollable(container) && target instanceof HTMLElement && !selfOrParentIsScrollable(target, htmlContainer) &&
// #2823
target.tagName !== 'INPUT' &&
// #1603
target.tagName !== 'TEXTAREA' &&
// #2266
!(isScrollable(htmlContainer) &&
// #1944
htmlContainer.contains(target))) {
return true;
}
return false;
};
/**
* https://github.com/sweetalert2/sweetalert2/issues/1786
*
* @param {TouchEvent} event
* @returns {boolean}
*/
const isStylus = event => {
return Boolean(event.touches && event.touches.length &&
// @ts-ignore - touchType is not a standard property
event.touches[0].touchType === 'stylus');
};
/**
* https://github.com/sweetalert2/sweetalert2/issues/1891
*
* @param {TouchEvent} event
* @returns {boolean}
*/
const isZoom = event => {
return event.touches && event.touches.length > 1;
};
const undoIOSfix = () => {
if (hasClass(document.body, swalClasses.iosfix)) {
const offset = parseInt(document.body.style.top, 10);
removeClass(document.body, swalClasses.iosfix);
document.body.style.top = '';
document.body.scrollTop = offset * -1;
}
};
/**
* Measure scrollbar width for padding body during modal show/hide
* https://github.com/twbs/bootstrap/blob/master/js/src/modal.js
*
* @returns {number}
*/
const measureScrollbar = () => {
const scrollDiv = document.createElement('div');
scrollDiv.className = swalClasses['scrollbar-measure'];
document.body.appendChild(scrollDiv);
const scrollbarWidth = scrollDiv.getBoundingClientRect().width - scrollDiv.clientWidth;
document.body.removeChild(scrollDiv);
return scrollbarWidth;
};
/**
* Remember state in cases where opening and handling a modal will fiddle with it.
* @type {number | null}
*/
let previousBodyPadding = null;
/**
* @param {string} initialBodyOverflow
*/
const replaceScrollbarWithPadding = initialBodyOverflow => {
// for queues, do not do this more than once
if (previousBodyPadding !== null) {
return;
}
// if the body has overflow
if (document.body.scrollHeight > window.innerHeight || initialBodyOverflow === 'scroll' // https://github.com/sweetalert2/sweetalert2/issues/2663
) {
// add padding so the content doesn't shift after removal of scrollbar
previousBodyPadding = parseInt(window.getComputedStyle(document.body).getPropertyValue('padding-right'));
document.body.style.paddingRight = `${previousBodyPadding + measureScrollbar()}px`;
}
};
const undoReplaceScrollbarWithPadding = () => {
if (previousBodyPadding !== null) {
document.body.style.paddingRight = `${previousBodyPadding}px`;
previousBodyPadding = null;
}
};
/**
* @param {SweetAlert} instance
* @param {HTMLElement} container
* @param {boolean} returnFocus
* @param {(() => void) | undefined} didClose
*/
function removePopupAndResetState(instance, container, returnFocus, didClose) {
if (isToast()) {
triggerDidCloseAndDispose(instance, didClose);
} else {
restoreActiveElement(returnFocus).then(() => triggerDidCloseAndDispose(instance, didClose));
removeKeydownHandler(globalState);
}
// workaround for https://github.com/sweetalert2/sweetalert2/issues/2088
// for some reason removing the container in Safari will scroll the document to bottom
if (isSafariOrIOS) {
container.setAttribute('style', 'display:none !important');
container.removeAttribute('class');
container.innerHTML = '';
} else {
container.remove();
}
if (isModal()) {
undoReplaceScrollbarWithPadding();
undoIOSfix();
unsetAriaHidden();
}
removeBodyClasses();
}
/**
* Remove SweetAlert2 classes from body
*/
function removeBodyClasses() {
removeClass([document.documentElement, document.body], [swalClasses.shown, swalClasses['height-auto'], swalClasses['no-backdrop'], swalClasses['toast-shown']]);
}
/**
* Instance method to close sweetAlert
*
* @param {SweetAlertResult | undefined} resolveValue
* @this {SweetAlert}
*/
function close(resolveValue) {
resolveValue = prepareResolveValue(resolveValue);
const swalPromiseResolve = privateMethods.swalPromiseResolve.get(this);
const didClose = triggerClosePopup(this);
if (this.isAwaitingPromise) {
// A swal awaiting for a promise (after a click on Confirm or Deny) cannot be dismissed anymore #2335
if (!resolveValue.isDismissed) {
handleAwaitingPromise(this);
swalPromiseResolve(resolveValue);
}
} else if (didClose) {
// Resolve Swal promise
swalPromiseResolve(resolveValue);
}
}
/**
* @param {SweetAlert} instance
* @returns {boolean}
*/
const triggerClosePopup = instance => {
const popup = getPopup();
if (!popup) {
return false;
}
const innerParams = privateProps.innerParams.get(instance);
if (!innerParams || hasClass(popup, innerParams.hideClass.popup)) {
return false;
}
removeClass(popup, innerParams.showClass.popup);
addClass(popup, innerParams.hideClass.popup);
const backdrop = getContainer();
removeClass(backdrop, innerParams.showClass.backdrop);
addClass(backdrop, innerParams.hideClass.backdrop);
handlePopupAnimation(instance, popup, innerParams);
return true;
};
/**
* @param {Error | string} error
* @this {SweetAlert}
*/
function rejectPromise(error) {
const rejectPromise = privateMethods.swalPromiseReject.get(this);
handleAwaitingPromise(this);
if (rejectPromise) {
// Reject Swal promise
rejectPromise(error);
}
}
/**
* @param {SweetAlert} instance
*/
const handleAwaitingPromise = instance => {
if (instance.isAwaitingPromise) {
// @ts-ignore
delete instance.isAwaitingPromise;
// The instance might have been previously partly destroyed, we must resume the destroy process in this case #2335
if (!privateProps.innerParams.get(instance)) {
instance._destroy();
}
}
};
/**
* @param {SweetAlertResult | undefined} resolveValue
* @returns {SweetAlertResult}
*/
const prepareResolveValue = resolveValue => {
// When user calls Swal.close()
if (typeof resolveValue === 'undefined') {
return {
isConfirmed: false,
isDenied: false,
isDismissed: true
};
}
return Object.assign({
isConfirmed: false,
isDenied: false,
isDismissed: false
}, resolveValue);
};
/**
* @param {SweetAlert} instance
* @param {HTMLElement} popup
* @param {SweetAlertOptions} innerParams
*/
const handlePopupAnimation = (instance, popup, innerParams) => {
var _globalState$eventEmi;
const container = getContainer();
// If animation is supported, animate
const animationIsSupported = hasCssAnimation(popup);
if (typeof innerParams.willClose === 'function') {
innerParams.willClose(popup);
}
(_globalState$eventEmi = globalState.eventEmitter) === null || _globalState$eventEmi === void 0 || _globalState$eventEmi.emit('willClose', popup);
if (animationIsSupported && container) {
animatePopup(instance, popup, container, Boolean(innerParams.returnFocus), innerParams.didClose);
} else if (container) {
// Otherwise, remove immediately
removePopupAndResetState(instance, container, Boolean(innerParams.returnFocus), innerParams.didClose);
}
};
/**
* @param {SweetAlert} instance
* @param {HTMLElement} popup
* @param {HTMLElement} container
* @param {boolean} returnFocus
* @param {(() => void) | undefined} didClose
*/
const animatePopup = (instance, popup, container, returnFocus, didClose) => {
globalState.swalCloseEventFinishedCallback = removePopupAndResetState.bind(null, instance, container, returnFocus, didClose);
/**
* @param {AnimationEvent | TransitionEvent} e
*/
const swalCloseAnimationFinished = function (e) {
if (e.target === popup) {
var _globalState$swalClos;
(_globalState$swalClos = globalState.swalCloseEventFinishedCallback) === null || _globalState$swalClos === void 0 || _globalState$swalClos.call(globalState);
delete globalState.swalCloseEventFinishedCallback;
popup.removeEventListener('animationend', swalCloseAnimationFinished);
popup.removeEventListener('transitionend', swalCloseAnimationFinished);
}
};
popup.addEventListener('animationend', swalCloseAnimationFinished);
popup.addEventListener('transitionend', swalCloseAnimationFinished);
};
/**
* @param {SweetAlert} instance
* @param {(() => void) | undefined} didClose
*/
const triggerDidCloseAndDispose = (instance, didClose) => {
setTimeout(() => {
var _globalState$eventEmi2;
if (typeof didClose === 'function') {
didClose.bind(instance.params)();
}
(_globalState$eventEmi2 = globalState.eventEmitter) === null || _globalState$eventEmi2 === void 0 || _globalState$eventEmi2.emit('didClose');
// instance might have been destroyed already
if (instance._destroy) {
instance._destroy();
}
});
};
/**
* Shows loader (spinner), this is useful with AJAX requests.
* By default the loader be shown instead of the "Confirm" button.
*
* @param {HTMLButtonElement | null} [buttonToReplace]
*/
const showLoading = buttonToReplace => {
let popup = getPopup();
if (!popup) {
new Swal();
}
popup = getPopup();
if (!popup) {
return;
}
const loader = getLoader();
if (isToast()) {
hide(getIcon());
} else {
replaceButton(popup, buttonToReplace);
}
show(loader);
popup.setAttribute('data-loading', 'true');
popup.setAttribute('aria-busy', 'true');
popup.focus();
};
/**
* @param {HTMLElement} popup
* @param {HTMLButtonElement | null} [buttonToReplace]
*/
const replaceButton = (popup, buttonToReplace) => {
const actions = getActions();
const loader = getLoader();
if (!actions || !loader) {
return;
}
if (!buttonToReplace && isVisible$1(getConfirmButton())) {
buttonToReplace = getConfirmButton();
}
show(actions);
if (buttonToReplace) {
hide(buttonToReplace);
loader.setAttribute('data-button-to-replace', buttonToReplace.className);
actions.insertBefore(loader, buttonToReplace);
}
addClass([popup, actions], swalClasses.loading);
};
/**
* @param {SweetAlert} instance
* @param {SweetAlertOptions} params
*/
const handleInputOptionsAndValue = (instance, params) => {
if (params.input === 'select' || params.input === 'radio') {
handleInputOptions(instance, params);
} else if (['text', 'email', 'number', 'tel', 'textarea'].some(i => i === params.input) && (hasToPromiseFn(params.inputValue) || isPromise(params.inputValue))) {
showLoading(getConfirmButton());
handleInputValue(instance, params);
}
};
/**
* @param {SweetAlert} instance
* @param {SweetAlertOptions} innerParams
* @returns {SweetAlertInputValue}
*/
const getInputValue = (instance, innerParams) => {
const input = instance.getInput();
if (!input) {
return null;
}
switch (innerParams.input) {
case 'checkbox':
return getCheckboxValue(input);
case 'radio':
return getRadioValue(input);
case 'file':
return getFileValue(input);
default:
return innerParams.inputAutoTrim ? input.value.trim() : input.value;
}
};
/**
* @param {HTMLInputElement} input
* @returns {number}
*/
const getCheckboxValue = input => input.checked ? 1 : 0;
/**
* @param {HTMLInputElement} input
* @returns {string | null}
*/
const getRadioValue = input => input.checked ? input.value : null;
/**
* @param {HTMLInputElement} input
* @returns {FileList | File | null}
*/
const getFileValue = input => input.files && input.files.length ? input.getAttribute('multiple') !== null ? input.files : input.files[0] : null;
/**
* @param {SweetAlert} instance
* @param {SweetAlertOptions} params
*/
const handleInputOptions = (instance, params) => {
const popup = getPopup();
if (!popup) {
return;
}
/**
* @param {*} inputOptions
*/
const processInputOptions = inputOptions => {
if (params.input === 'select') {
populateSelectOptions(popup, formatInputOptions(inputOptions), params);
} else if (params.input === 'radio') {
populateRadioOptions(popup, formatInputOptions(inputOptions), params);
}
};
if (hasToPromiseFn(params.inputOptions) || isPromise(params.inputOptions)) {
showLoading(getConfirmButton());
asPromise(params.inputOptions).then(inputOptions => {
instance.hideLoading();
processInputOptions(inputOptions);
});
} else if (typeof params.inputOptions === 'object') {
processInputOptions(params.inputOptions);
} else {
error(`Unexpected type of inputOptions! Expected object, Map or Promise, got ${typeof params.inputOptions}`);
}
};
/**
* @param {SweetAlert} instance
* @param {SweetAlertOptions} params
*/
const handleInputValue = (instance, params) => {
const input = instance.getInput();
if (!input) {
return;
}
hide(input);
asPromise(params.inputValue).then(inputValue => {
input.value = params.input === 'number' ? `${parseFloat(inputValue) || 0}` : `${inputValue}`;
show(input);
input.focus();
instance.hideLoading();
}).catch(err => {
error(`Error in inputValue promise: ${err}`);
input.value = '';
show(input);
input.focus();
instance.hideLoading();
});
};
/**
* @param {HTMLElement} popup
* @param {InputOptionFlattened[]} inputOptions
* @param {SweetAlertOptions} params
*/
function populateSelectOptions(popup, inputOptions, params) {
const select = getDirectChildByClass(popup, swalClasses.select);
if (!select) {
return;
}
/**
* @param {HTMLElement} parent
* @param {string} optionLabel
* @param {string} optionValue
*/
const renderOption = (parent, optionLabel, optionValue) => {
const option = document.createElement('option');
option.value = optionValue;
setInnerHtml(option, optionLabel);
option.selected = isSelected(optionValue, params.inputValue);
parent.appendChild(option);
};
inputOptions.forEach(inputOption => {
const optionValue = inputOption[0];
const optionLabel = inputOption[1];
//