Commit AMD build artifacts required for Moodle Marketplace review.
Stop ignoring amd/build so generated modules ship with the plugin ZIP. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -10,7 +10,6 @@ Desktop.ini
|
|||||||
.phpunit.result.cache
|
.phpunit.result.cache
|
||||||
.phpunit.cache/
|
.phpunit.cache/
|
||||||
/coverage/
|
/coverage/
|
||||||
/amd/build/
|
|
||||||
grunt/
|
grunt/
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
|
|
||||||
|
|||||||
+67
@@ -0,0 +1,67 @@
|
|||||||
|
// This file is part of Moodle - http://moodle.org/
|
||||||
|
//
|
||||||
|
// Moodle is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// Moodle is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @module mod_meetletacbfree/featureaccess_matrix
|
||||||
|
* @copyright 2026 MeetLet <info@meetlet.net>
|
||||||
|
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||||
|
*/
|
||||||
|
|
||||||
|
define([], function() {
|
||||||
|
const bindMatrix = (matrix) => {
|
||||||
|
if (!matrix || matrix.getAttribute('data-access-bound') === '1') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
matrix.setAttribute('data-access-bound', '1');
|
||||||
|
|
||||||
|
const setGroup = (checks, forceOn) => {
|
||||||
|
const list = Array.from(checks).filter((el) => !el.disabled);
|
||||||
|
if (!list.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const turnOn = (typeof forceOn === 'boolean')
|
||||||
|
? forceOn
|
||||||
|
: list.some((el) => !el.checked);
|
||||||
|
list.forEach((el) => {
|
||||||
|
if (el.checked !== turnOn) {
|
||||||
|
el.checked = turnOn;
|
||||||
|
el.dispatchEvent(new Event('change', {bubbles: true}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
matrix.addEventListener('click', (e) => {
|
||||||
|
const btn = e.target.closest('[data-access-toggle]');
|
||||||
|
if (!btn || !matrix.contains(btn) || matrix.classList.contains('is-disabled')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
const mode = btn.getAttribute('data-access-toggle');
|
||||||
|
if (mode === 'row') {
|
||||||
|
const role = btn.getAttribute('data-role');
|
||||||
|
setGroup(matrix.querySelectorAll('.meetlet-access-check[data-role="' + role + '"]'));
|
||||||
|
} else if (mode === 'col') {
|
||||||
|
const feature = btn.getAttribute('data-feature');
|
||||||
|
setGroup(matrix.querySelectorAll('.meetlet-access-check[data-feature="' + feature + '"]'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const init = () => {
|
||||||
|
document.querySelectorAll('[data-meetlet-access-matrix]').forEach(bindMatrix);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {init: init};
|
||||||
|
});
|
||||||
+124
@@ -0,0 +1,124 @@
|
|||||||
|
// This file is part of Moodle - http://moodle.org/
|
||||||
|
//
|
||||||
|
// Moodle is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// Moodle is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @module mod_meetletacbfree/meetlet_modal_scroll
|
||||||
|
* @copyright 2026 MeetLet <info@meetlet.net>
|
||||||
|
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||||
|
*/
|
||||||
|
|
||||||
|
define([], function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var scrollLockY = 0;
|
||||||
|
var guardsActive = false;
|
||||||
|
|
||||||
|
function scrollbarWidth() {
|
||||||
|
return Math.max(0, window.innerWidth - document.documentElement.clientWidth);
|
||||||
|
}
|
||||||
|
|
||||||
|
function focusNoScroll(el) {
|
||||||
|
if (!el || typeof el.focus !== 'function') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
el.focus({preventScroll: true});
|
||||||
|
} catch (e) {
|
||||||
|
el.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreScroll() {
|
||||||
|
if (scrollLockY > 0) {
|
||||||
|
window.scrollTo(0, scrollLockY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onScrollGuard() {
|
||||||
|
if (scrollLockY && window.scrollY !== scrollLockY) {
|
||||||
|
window.scrollTo(0, scrollLockY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMoveGuard(e) {
|
||||||
|
if (e.target.closest && e.target.closest('.meetlet-modal__dialog')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindGuards() {
|
||||||
|
if (guardsActive) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
guardsActive = true;
|
||||||
|
window.addEventListener('scroll', onScrollGuard, true);
|
||||||
|
window.addEventListener('wheel', onMoveGuard, {passive: false, capture: true});
|
||||||
|
window.addEventListener('touchmove', onMoveGuard, {passive: false, capture: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
function unbindGuards() {
|
||||||
|
if (!guardsActive) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
guardsActive = false;
|
||||||
|
window.removeEventListener('scroll', onScrollGuard, true);
|
||||||
|
window.removeEventListener('wheel', onMoveGuard, true);
|
||||||
|
window.removeEventListener('touchmove', onMoveGuard, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function lockPageScroll() {
|
||||||
|
if (document.documentElement.classList.contains('meetlet-modal-open')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
scrollLockY = window.scrollY || document.documentElement.scrollTop || document.body.scrollTop || 0;
|
||||||
|
var pad = scrollbarWidth();
|
||||||
|
if (pad > 0) {
|
||||||
|
document.body.style.paddingRight = pad + 'px';
|
||||||
|
}
|
||||||
|
document.documentElement.classList.add('meetlet-modal-open');
|
||||||
|
restoreScroll();
|
||||||
|
if (typeof window.requestAnimationFrame === 'function') {
|
||||||
|
window.requestAnimationFrame(restoreScroll);
|
||||||
|
}
|
||||||
|
bindGuards();
|
||||||
|
}
|
||||||
|
|
||||||
|
function unlockPageScroll() {
|
||||||
|
if (!document.documentElement.classList.contains('meetlet-modal-open')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
unbindGuards();
|
||||||
|
var y = scrollLockY;
|
||||||
|
document.documentElement.classList.remove('meetlet-modal-open');
|
||||||
|
document.body.style.paddingRight = '';
|
||||||
|
scrollLockY = 0;
|
||||||
|
if (y) {
|
||||||
|
window.scrollTo(0, y);
|
||||||
|
if (typeof window.requestAnimationFrame === 'function') {
|
||||||
|
window.requestAnimationFrame(function() {
|
||||||
|
window.scrollTo(0, y);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
init: function() {},
|
||||||
|
lock: lockPageScroll,
|
||||||
|
unlock: unlockPageScroll,
|
||||||
|
focusNoScroll: focusNoScroll
|
||||||
|
};
|
||||||
|
});
|
||||||
Vendored
+504
@@ -0,0 +1,504 @@
|
|||||||
|
// This file is part of Moodle - http://moodle.org/
|
||||||
|
//
|
||||||
|
// Moodle is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// Moodle is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @module mod_meetletacbfree/modform
|
||||||
|
* @copyright 2026 MeetLet <info@meetlet.net>
|
||||||
|
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||||
|
*/
|
||||||
|
|
||||||
|
define(['core/str', 'core/notification', 'core/templates', 'core/ajax'], function(Str, Notification, Templates, Ajax) {
|
||||||
|
function init() {
|
||||||
|
const ELEMENT_SELECTOR = {
|
||||||
|
participantData: () => document.querySelector('[data-participant-data]'),
|
||||||
|
participantList: () => document.getElementsByName('participants')[0],
|
||||||
|
participantTable: () => document.getElementById('participant_list_table'),
|
||||||
|
participantSelectionType: () => document.getElementsByName('meetlet_participant_selection_type')[0],
|
||||||
|
participantSelection: () => document.getElementsByName('meetlet_participant_selection')[0],
|
||||||
|
participantAddButton: () => document.getElementsByName('meetlet_participant_selection_add')[0],
|
||||||
|
};
|
||||||
|
|
||||||
|
const getParticipantList = () => {
|
||||||
|
const value = ELEMENT_SELECTOR.participantList().value;
|
||||||
|
return value ? JSON.parse(value) : [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const participantListUpdate = (list) => {
|
||||||
|
ELEMENT_SELECTOR.participantList().value = JSON.stringify(list);
|
||||||
|
};
|
||||||
|
|
||||||
|
const participantRemove = (selectionTypeValue, selectionValue) => {
|
||||||
|
const pList = getParticipantList();
|
||||||
|
const id = 'participant_list_tr_' + selectionTypeValue + '-' + selectionValue;
|
||||||
|
const table = ELEMENT_SELECTOR.participantTable();
|
||||||
|
for (let i = 0; i < pList.length; i++) {
|
||||||
|
if (pList[i].selectiontype === selectionTypeValue &&
|
||||||
|
String(pList[i].selectionid) === String(selectionValue)) {
|
||||||
|
pList.splice(i, 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (let i = 0; i < table.rows.length; i++) {
|
||||||
|
if (table.rows[i].id === id) {
|
||||||
|
table.deleteRow(i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
participantListUpdate(pList);
|
||||||
|
};
|
||||||
|
|
||||||
|
const participantListRoleUpdate = (type, id) => {
|
||||||
|
const select = document.querySelector('#participant_list_tr_' + type + '-' + id + ' .participant-select');
|
||||||
|
const pList = getParticipantList();
|
||||||
|
for (let i = 0; i < pList.length; i++) {
|
||||||
|
if (pList[i].selectiontype === type && String(pList[i].selectionid) === String(id)) {
|
||||||
|
pList[i].role = select.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
participantListUpdate(pList);
|
||||||
|
};
|
||||||
|
|
||||||
|
const participantAddToForm = async(selectionTypeValue, selectionValue, selectedRole, canRemove) => {
|
||||||
|
const participantData = JSON.parse(ELEMENT_SELECTOR.participantData().dataset.participantData);
|
||||||
|
const [shost, spresenter, sparticipant] = await Promise.all([
|
||||||
|
Str.get_string('role_host', 'meetletacbfree'),
|
||||||
|
Str.get_string('role_presenter', 'meetletacbfree'),
|
||||||
|
Str.get_string('role_participant', 'meetletacbfree'),
|
||||||
|
]);
|
||||||
|
const roles = {
|
||||||
|
host: {id: 'host', label: shost},
|
||||||
|
presenter: {id: 'presenter', label: spresenter},
|
||||||
|
participant: {id: 'participant', label: sparticipant},
|
||||||
|
};
|
||||||
|
if (roles[selectedRole]) {
|
||||||
|
roles[selectedRole].isselected = true;
|
||||||
|
} else {
|
||||||
|
roles.participant.isselected = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const listTable = document.querySelector('#participant_list_table tbody');
|
||||||
|
const templateContext = {
|
||||||
|
selectiontypevalue: selectionTypeValue,
|
||||||
|
selectionvalue: selectionValue,
|
||||||
|
participanttype: participantData[selectionTypeValue].name,
|
||||||
|
participantvalue: (selectionTypeValue !== 'all')
|
||||||
|
? participantData[selectionTypeValue].children[selectionValue].name : null,
|
||||||
|
roles: Object.values(roles),
|
||||||
|
canRemove: canRemove,
|
||||||
|
};
|
||||||
|
const {html, js} = await Templates.renderForPromise('mod_meetletacbfree/participant_form_add', templateContext);
|
||||||
|
const newNode = Templates.appendNodeContents(listTable, html, js)[0];
|
||||||
|
newNode.querySelector('.participant-select').addEventListener('change', () => {
|
||||||
|
participantListRoleUpdate(selectionTypeValue, selectionValue);
|
||||||
|
});
|
||||||
|
const removeNode = newNode.querySelector('.remove-button');
|
||||||
|
if (removeNode) {
|
||||||
|
removeNode.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
participantRemove(selectionTypeValue, selectionValue);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Notification.exception(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const participantAddFromCurrentSelection = () => {
|
||||||
|
const selectionType = ELEMENT_SELECTOR.participantSelectionType();
|
||||||
|
const selection = ELEMENT_SELECTOR.participantSelection();
|
||||||
|
const pList = getParticipantList();
|
||||||
|
for (let i = 0; i < pList.length; i++) {
|
||||||
|
if (pList[i].selectiontype === selectionType.value &&
|
||||||
|
String(pList[i].selectionid) === String(selection.value)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const role = 'participant';
|
||||||
|
pList.push({
|
||||||
|
selectiontype: selectionType.value,
|
||||||
|
selectionid: selection.value,
|
||||||
|
role: role,
|
||||||
|
});
|
||||||
|
participantAddToForm(selectionType.value, selection.value, role, true);
|
||||||
|
participantListUpdate(pList);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateSelectionFromType = (currentTypeSelect) => {
|
||||||
|
const participantData = JSON.parse(ELEMENT_SELECTOR.participantData().dataset.participantData);
|
||||||
|
const participantSelect = ELEMENT_SELECTOR.participantSelection();
|
||||||
|
while (participantSelect.firstChild) {
|
||||||
|
participantSelect.removeChild(participantSelect.firstChild);
|
||||||
|
}
|
||||||
|
if (currentTypeSelect.value === 'all') {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.text = '---------------';
|
||||||
|
option.value = 'all';
|
||||||
|
participantSelect.add(option);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const options = Object.values(participantData[currentTypeSelect.value].children || {});
|
||||||
|
options.forEach((opt) => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.text = opt.name;
|
||||||
|
option.value = opt.id;
|
||||||
|
participantSelect.add(option);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const participantListInit = () => {
|
||||||
|
const participantData = JSON.parse(ELEMENT_SELECTOR.participantData().dataset.participantData);
|
||||||
|
getParticipantList().forEach((participant) => {
|
||||||
|
if (participant.selectiontype === 'all' ||
|
||||||
|
(participantData[participant.selectiontype] &&
|
||||||
|
typeof participantData[participant.selectiontype].children[participant.selectionid] !== 'undefined')) {
|
||||||
|
participantAddToForm(participant.selectiontype, participant.selectionid, participant.role, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const syncAccessMatrix = () => {
|
||||||
|
// Moodle advcheckbox renders a hidden input with the same name first — use the real checkbox.
|
||||||
|
const override = document.querySelector('#id_featureaccess_override') ||
|
||||||
|
document.querySelector('input[type="checkbox"][name="featureaccess_override"]');
|
||||||
|
const matrix = document.querySelector('.meetlet-access-matrix');
|
||||||
|
if (!override || !matrix) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const enabled = !!override.checked;
|
||||||
|
matrix.classList.toggle('is-disabled', !enabled);
|
||||||
|
matrix.querySelectorAll('input.meetlet-access-check').forEach((el) => {
|
||||||
|
el.disabled = !enabled;
|
||||||
|
});
|
||||||
|
matrix.querySelectorAll('.meetlet-access-toggle').forEach((el) => {
|
||||||
|
el.disabled = !enabled;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const accessOverride = document.querySelector('#id_featureaccess_override') ||
|
||||||
|
document.querySelector('input[type="checkbox"][name="featureaccess_override"]');
|
||||||
|
if (accessOverride) {
|
||||||
|
accessOverride.addEventListener('change', syncAccessMatrix);
|
||||||
|
accessOverride.addEventListener('click', syncAccessMatrix);
|
||||||
|
syncAccessMatrix();
|
||||||
|
}
|
||||||
|
|
||||||
|
const setDatetimeFields = (basename, unix) => {
|
||||||
|
if (!unix) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const d = new Date(unix * 1000);
|
||||||
|
const set = (suffix, value) => {
|
||||||
|
const el = document.querySelector('[name="' + basename + '[' + suffix + ']"]');
|
||||||
|
if (el) {
|
||||||
|
el.value = String(value);
|
||||||
|
el.dispatchEvent(new Event('change', {bubbles: true}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
set('year', d.getFullYear());
|
||||||
|
set('month', d.getMonth() + 1);
|
||||||
|
set('day', d.getDate());
|
||||||
|
set('hour', d.getHours());
|
||||||
|
set('minute', d.getMinutes());
|
||||||
|
};
|
||||||
|
|
||||||
|
const applyExistingMeeting = () => {
|
||||||
|
const picker = document.querySelector('[data-meeting-picker]');
|
||||||
|
const metaEl = document.querySelector('[data-meetings-meta]');
|
||||||
|
const scoInput = document.querySelector('#id_existingscoid') || document.querySelector('input[name="existingscoid"]');
|
||||||
|
const linkmode = document.querySelector('#id_linkmode') || document.querySelector('[name="linkmode"]');
|
||||||
|
const nameInput = document.querySelector('#id_name') || document.querySelector('[name="name"]');
|
||||||
|
if (!picker || !scoInput || !linkmode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggle = picker.querySelector('[data-meeting-toggle]');
|
||||||
|
const panel = picker.querySelector('[data-meeting-panel]');
|
||||||
|
const searchInput = picker.querySelector('[data-meeting-search]');
|
||||||
|
const listEl = picker.querySelector('[data-meeting-list]');
|
||||||
|
const hint = picker.querySelector('[data-meeting-hint]');
|
||||||
|
const labelEl = picker.querySelector('[data-meeting-label]');
|
||||||
|
const readonly = picker.getAttribute('data-readonly') === '1';
|
||||||
|
const strChoose = picker.getAttribute('data-str-choose') || '---';
|
||||||
|
const strLoading = picker.getAttribute('data-str-loading') || '...';
|
||||||
|
const strEmpty = picker.getAttribute('data-str-empty') || '';
|
||||||
|
const courseid = parseInt(picker.getAttribute('data-courseid') || '0', 10);
|
||||||
|
|
||||||
|
let meta = {};
|
||||||
|
try {
|
||||||
|
meta = JSON.parse((metaEl && metaEl.getAttribute('data-meetings-meta')) || '{}');
|
||||||
|
} catch (e) {
|
||||||
|
meta = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
let loadedOnce = false;
|
||||||
|
let searchTimer = null;
|
||||||
|
let requestSeq = 0;
|
||||||
|
|
||||||
|
const setOpen = (open) => {
|
||||||
|
if (!panel || !toggle || readonly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (open) {
|
||||||
|
panel.removeAttribute('hidden');
|
||||||
|
toggle.setAttribute('aria-expanded', 'true');
|
||||||
|
picker.classList.add('is-open');
|
||||||
|
if (searchInput) {
|
||||||
|
searchInput.focus();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
panel.setAttribute('hidden', 'hidden');
|
||||||
|
toggle.setAttribute('aria-expanded', 'false');
|
||||||
|
picker.classList.remove('is-open');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const applySelected = (scoid) => {
|
||||||
|
const info = meta[String(scoid)];
|
||||||
|
if (!info || !nameInput || String(linkmode.value) !== '1') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (info.name) {
|
||||||
|
nameInput.value = info.name;
|
||||||
|
nameInput.dispatchEvent(new Event('input', {bubbles: true}));
|
||||||
|
nameInput.dispatchEvent(new Event('change', {bubbles: true}));
|
||||||
|
}
|
||||||
|
if (info.starttime) {
|
||||||
|
setDatetimeFields('starttime', info.starttime);
|
||||||
|
}
|
||||||
|
if (info.endtime) {
|
||||||
|
const endEnable = document.querySelector('[name="endtimeenable"]');
|
||||||
|
if (endEnable && !endEnable.checked) {
|
||||||
|
endEnable.checked = true;
|
||||||
|
endEnable.dispatchEvent(new Event('change', {bubbles: true}));
|
||||||
|
}
|
||||||
|
setDatetimeFields('endtime', info.endtime);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectMeeting = (m) => {
|
||||||
|
const scoid = String(m.scoid || '0');
|
||||||
|
scoInput.value = scoid;
|
||||||
|
meta[scoid] = {
|
||||||
|
name: m.name || '',
|
||||||
|
urlpath: m.urlpath || '',
|
||||||
|
starttime: m.starttime || 0,
|
||||||
|
endtime: m.endtime || 0,
|
||||||
|
};
|
||||||
|
if (metaEl) {
|
||||||
|
metaEl.setAttribute('data-meetings-meta', JSON.stringify(meta));
|
||||||
|
}
|
||||||
|
if (labelEl) {
|
||||||
|
labelEl.textContent = m.label || m.name || strChoose;
|
||||||
|
}
|
||||||
|
if (listEl) {
|
||||||
|
listEl.querySelectorAll('.meetlet-combobox__option').forEach((btn) => {
|
||||||
|
btn.classList.toggle('is-selected', btn.getAttribute('data-scoid') === scoid);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setOpen(false);
|
||||||
|
applySelected(scoid);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderList = (meetings) => {
|
||||||
|
if (!listEl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const selected = String(scoInput.value || '0');
|
||||||
|
listEl.innerHTML = '';
|
||||||
|
const rows = meetings || [];
|
||||||
|
if (!rows.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
rows.forEach((m) => {
|
||||||
|
const scoid = String(m.scoid);
|
||||||
|
meta[scoid] = {
|
||||||
|
name: m.name || '',
|
||||||
|
urlpath: m.urlpath || '',
|
||||||
|
starttime: m.starttime || 0,
|
||||||
|
endtime: m.endtime || 0,
|
||||||
|
};
|
||||||
|
const li = document.createElement('li');
|
||||||
|
const btn = document.createElement('button');
|
||||||
|
btn.type = 'button';
|
||||||
|
btn.className = 'meetlet-combobox__option' + (scoid === selected ? ' is-selected' : '');
|
||||||
|
btn.setAttribute('data-scoid', scoid);
|
||||||
|
btn.textContent = m.label || m.name || scoid;
|
||||||
|
btn.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
selectMeeting(m);
|
||||||
|
});
|
||||||
|
li.appendChild(btn);
|
||||||
|
listEl.appendChild(li);
|
||||||
|
});
|
||||||
|
if (metaEl) {
|
||||||
|
metaEl.setAttribute('data-meetings-meta', JSON.stringify(meta));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchMeetings = (query) => {
|
||||||
|
if (String(linkmode.value) !== '1' || readonly) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
const seq = ++requestSeq;
|
||||||
|
if (hint) {
|
||||||
|
hint.textContent = strLoading;
|
||||||
|
}
|
||||||
|
return Ajax.call([{
|
||||||
|
methodname: 'mod_meetletacbfree_search_server_meetings',
|
||||||
|
args: {
|
||||||
|
query: query || '',
|
||||||
|
limit: 10,
|
||||||
|
courseid: courseid,
|
||||||
|
},
|
||||||
|
}])[0].then((res) => {
|
||||||
|
if (seq !== requestSeq) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const meetings = (res && res.meetings) ? res.meetings : [];
|
||||||
|
renderList(meetings);
|
||||||
|
if (hint) {
|
||||||
|
hint.textContent = meetings.length ? '' : strEmpty;
|
||||||
|
}
|
||||||
|
loadedOnce = true;
|
||||||
|
return res;
|
||||||
|
}).catch((err) => {
|
||||||
|
if (hint) {
|
||||||
|
hint.textContent = (err && err.message) ? err.message : strEmpty;
|
||||||
|
}
|
||||||
|
Notification.exception(err);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (toggle) {
|
||||||
|
toggle.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (readonly || String(linkmode.value) !== '1') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const willOpen = panel.hasAttribute('hidden');
|
||||||
|
setOpen(willOpen);
|
||||||
|
if (willOpen) {
|
||||||
|
fetchMeetings(searchInput ? String(searchInput.value || '').trim() : '');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchInput) {
|
||||||
|
searchInput.addEventListener('input', () => {
|
||||||
|
window.clearTimeout(searchTimer);
|
||||||
|
searchTimer = window.setTimeout(() => {
|
||||||
|
fetchMeetings(String(searchInput.value || '').trim());
|
||||||
|
}, 350);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('click', (e) => {
|
||||||
|
if (!picker.contains(e.target)) {
|
||||||
|
setOpen(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
linkmode.addEventListener('change', () => {
|
||||||
|
if (String(linkmode.value) === '1') {
|
||||||
|
loadedOnce = false;
|
||||||
|
if (!readonly) {
|
||||||
|
setOpen(true);
|
||||||
|
fetchMeetings('');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setOpen(false);
|
||||||
|
scoInput.value = '0';
|
||||||
|
if (labelEl) {
|
||||||
|
labelEl.textContent = strChoose;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (String(linkmode.value) === '1' && !readonly) {
|
||||||
|
// Prefetch quietly so first open is fast; do not force panel open on edit.
|
||||||
|
fetchMeetings('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
applyExistingMeeting();
|
||||||
|
|
||||||
|
const syncSettingOverrideInputs = () => {
|
||||||
|
document.querySelectorAll('[data-meetlet-setting-radio]').forEach((input) => {
|
||||||
|
if (input.getAttribute('data-meetlet-setting-bound') === '1') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
input.setAttribute('data-meetlet-setting-bound', '1');
|
||||||
|
input.addEventListener('change', () => {
|
||||||
|
const baseName = input.name.replace(/_state$/, '');
|
||||||
|
const hidden = document.querySelector('input[type="hidden"][name="' + baseName + '"]');
|
||||||
|
if (hidden && input.checked) {
|
||||||
|
if (baseName === 'participantsmode') {
|
||||||
|
hidden.value = input.value;
|
||||||
|
} else {
|
||||||
|
hidden.value = input.value === '2' ? '1' : '0';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
syncParticipantOverride();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const participantModeInput = () => document.querySelector('input[name="participantsmode_state"]:checked');
|
||||||
|
|
||||||
|
const syncParticipantOverride = () => {
|
||||||
|
const mode = participantModeInput();
|
||||||
|
const enabled = !mode || mode.value === '1';
|
||||||
|
const container = document.querySelector('#fitem_id_static_participant_list');
|
||||||
|
if (container) {
|
||||||
|
container.classList.toggle('is-disabled', !enabled);
|
||||||
|
}
|
||||||
|
const participantForm = document.querySelector('.meetlet-participant-form') || container;
|
||||||
|
if (participantForm) {
|
||||||
|
participantForm.querySelectorAll('select, button, input.participant-select').forEach((el) => {
|
||||||
|
el.disabled = !enabled;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const table = ELEMENT_SELECTOR.participantTable();
|
||||||
|
if (table) {
|
||||||
|
table.querySelectorAll('select, button').forEach((el) => {
|
||||||
|
el.disabled = !enabled;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
syncSettingOverrideInputs();
|
||||||
|
syncParticipantOverride();
|
||||||
|
|
||||||
|
const typeSelect = ELEMENT_SELECTOR.participantSelectionType();
|
||||||
|
const addButton = ELEMENT_SELECTOR.participantAddButton();
|
||||||
|
if (!typeSelect || !addButton || !ELEMENT_SELECTOR.participantData()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
typeSelect.addEventListener('change', (e) => updateSelectionFromType(e.target));
|
||||||
|
addButton.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
participantAddFromCurrentSelection();
|
||||||
|
});
|
||||||
|
document.querySelectorAll('input[name="participantsmode_state"]').forEach((input) => {
|
||||||
|
input.addEventListener('change', syncParticipantOverride);
|
||||||
|
});
|
||||||
|
updateSelectionFromType(typeSelect);
|
||||||
|
participantListInit();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
init: init
|
||||||
|
};
|
||||||
|
});
|
||||||
+418
@@ -0,0 +1,418 @@
|
|||||||
|
// This file is part of Moodle - http://moodle.org/
|
||||||
|
//
|
||||||
|
// Moodle is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// Moodle is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @module mod_meetletacbfree/settings_access_reset
|
||||||
|
* @copyright 2026 MeetLet <info@meetlet.net>
|
||||||
|
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||||
|
*/
|
||||||
|
|
||||||
|
define(['core/ajax', 'mod_meetletacbfree/meetlet_modal_scroll'], function(Ajax, MeetletModalScroll) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var modalState = {
|
||||||
|
open: false,
|
||||||
|
resolve: null,
|
||||||
|
lastFocus: null,
|
||||||
|
onKey: null,
|
||||||
|
mode: 'confirm',
|
||||||
|
pendingSection: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
function modalScroll() {
|
||||||
|
return MeetletModalScroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
function byId(id) {
|
||||||
|
return document.getElementById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseStrings(root) {
|
||||||
|
if (!root) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return JSON.parse(root.getAttribute('data-strings') || '{}') || {};
|
||||||
|
} catch (e) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sectionContainer(section) {
|
||||||
|
if (section === 'rolemapping') {
|
||||||
|
return document.querySelector('[data-meetlet-rolemapping]');
|
||||||
|
}
|
||||||
|
if (section === 'featureaccess') {
|
||||||
|
return document.querySelector('[data-meetlet-access-matrix]');
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyRoleMatrix(container, matrix) {
|
||||||
|
if (!container || !matrix) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Object.keys(matrix).forEach(function (rowkey) {
|
||||||
|
var row = container.querySelector('[data-role-row="' + rowkey + '"]');
|
||||||
|
if (!row) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var val = matrix[rowkey];
|
||||||
|
var radio = row.querySelector('input[type="radio"][value="' + val + '"]');
|
||||||
|
if (radio) {
|
||||||
|
radio.checked = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyFeatureMatrix(container, matrix) {
|
||||||
|
if (!container || !matrix) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
container.querySelectorAll('.meetlet-access-check').forEach(function (cb) {
|
||||||
|
var role = cb.getAttribute('data-role');
|
||||||
|
var feature = cb.getAttribute('data-feature');
|
||||||
|
var on = matrix[role] && matrix[role][feature];
|
||||||
|
cb.checked = !!on;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderModalMessage(bodyEl, mode, message) {
|
||||||
|
if (!bodyEl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
bodyEl.textContent = '';
|
||||||
|
if (mode === 'busy') {
|
||||||
|
var wrap = document.createElement('div');
|
||||||
|
wrap.className = 'meetlet-modal__busy';
|
||||||
|
var spinner = document.createElement('div');
|
||||||
|
spinner.className = 'meetlet-modal__spinner';
|
||||||
|
spinner.setAttribute('aria-hidden', 'true');
|
||||||
|
var text = document.createElement('p');
|
||||||
|
text.className = 'meetlet-modal__lead';
|
||||||
|
text.textContent = message || '';
|
||||||
|
wrap.appendChild(spinner);
|
||||||
|
wrap.appendChild(text);
|
||||||
|
bodyEl.appendChild(wrap);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var lead = document.createElement('p');
|
||||||
|
lead.className = 'meetlet-modal__lead';
|
||||||
|
lead.textContent = message || '';
|
||||||
|
bodyEl.appendChild(lead);
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeModal(confirmed) {
|
||||||
|
var modal = byId('meetlet-access-reset-modal');
|
||||||
|
if (!modal || !modalState.open) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modal.setAttribute('hidden', 'hidden');
|
||||||
|
modal.style.display = 'none';
|
||||||
|
modal.classList.remove('is-busy');
|
||||||
|
modal.setAttribute('aria-busy', 'false');
|
||||||
|
modalState.open = false;
|
||||||
|
modalScroll().unlock();
|
||||||
|
if (modalState.onKey) {
|
||||||
|
document.removeEventListener('keydown', modalState.onKey, true);
|
||||||
|
modalState.onKey = null;
|
||||||
|
}
|
||||||
|
var resolve = modalState.resolve;
|
||||||
|
modalState.resolve = null;
|
||||||
|
modalState.pendingSection = null;
|
||||||
|
if (resolve) {
|
||||||
|
resolve(!!confirmed);
|
||||||
|
}
|
||||||
|
if (modalState.lastFocus && typeof modalState.lastFocus.focus === 'function') {
|
||||||
|
modalScroll().focusNoScroll(modalState.lastFocus);
|
||||||
|
}
|
||||||
|
modalState.lastFocus = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openModal(opts) {
|
||||||
|
opts = opts || {};
|
||||||
|
var mode = opts.mode === 'alert' ? 'alert' : (opts.mode === 'busy' ? 'busy' : 'confirm');
|
||||||
|
var modal = byId('meetlet-access-reset-modal');
|
||||||
|
if (!modal) {
|
||||||
|
return Promise.resolve(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modalState.open) {
|
||||||
|
if (modalState.mode === 'busy' && mode === 'alert') {
|
||||||
|
modalState.resolve = null;
|
||||||
|
} else {
|
||||||
|
closeModal(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var str = opts.strings || {};
|
||||||
|
var titleEl = byId('meetlet-access-reset-modal-title');
|
||||||
|
var bodyEl = byId('meetlet-access-reset-modal-body');
|
||||||
|
var confirmBtn = byId('meetlet-access-reset-modal-confirm');
|
||||||
|
var cancelBtn = modal.querySelector('[data-meetlet-modal-dismiss].btn');
|
||||||
|
var closeBtn = modal.querySelector('.meetlet-modal__close');
|
||||||
|
|
||||||
|
if (titleEl) {
|
||||||
|
titleEl.textContent = opts.title || '';
|
||||||
|
}
|
||||||
|
renderModalMessage(bodyEl, mode, opts.message || '');
|
||||||
|
modal.classList.toggle('is-busy', mode === 'busy');
|
||||||
|
modal.setAttribute('aria-busy', mode === 'busy' ? 'true' : 'false');
|
||||||
|
|
||||||
|
if (confirmBtn) {
|
||||||
|
confirmBtn.textContent = opts.confirmLabel || (mode === 'alert' ? (str.modal_ok || 'OK') : (str.confirm_reset || 'Reset'));
|
||||||
|
confirmBtn.className = opts.confirmClass || (mode === 'alert' ? 'btn btn-primary' : 'btn btn-outline-danger');
|
||||||
|
confirmBtn.hidden = mode === 'busy';
|
||||||
|
confirmBtn.disabled = mode === 'busy';
|
||||||
|
confirmBtn.style.display = mode === 'busy' ? 'none' : '';
|
||||||
|
}
|
||||||
|
if (cancelBtn) {
|
||||||
|
cancelBtn.textContent = str.modal_cancel || 'Cancel';
|
||||||
|
cancelBtn.hidden = mode !== 'confirm';
|
||||||
|
cancelBtn.style.display = mode !== 'confirm' ? 'none' : '';
|
||||||
|
}
|
||||||
|
if (closeBtn) {
|
||||||
|
if (str.modal_close) {
|
||||||
|
closeBtn.setAttribute('aria-label', str.modal_close);
|
||||||
|
}
|
||||||
|
closeBtn.hidden = mode === 'busy';
|
||||||
|
closeBtn.style.display = mode === 'busy' ? 'none' : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
var dir = document.documentElement.getAttribute('dir') || 'ltr';
|
||||||
|
modal.setAttribute('dir', dir);
|
||||||
|
var dialog = modal.querySelector('.meetlet-modal__dialog');
|
||||||
|
if (dialog) {
|
||||||
|
dialog.setAttribute('dir', dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
modalState.mode = mode;
|
||||||
|
if (!modalState.lastFocus) {
|
||||||
|
modalState.lastFocus = document.activeElement;
|
||||||
|
}
|
||||||
|
modalScroll().lock();
|
||||||
|
modal.removeAttribute('hidden');
|
||||||
|
modal.style.display = 'flex';
|
||||||
|
modalState.open = true;
|
||||||
|
|
||||||
|
if (modalState.onKey) {
|
||||||
|
document.removeEventListener('keydown', modalState.onKey, true);
|
||||||
|
}
|
||||||
|
modalState.onKey = function (e) {
|
||||||
|
if (!modalState.open) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
if (modalState.mode === 'busy') {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
closeModal(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (e.key !== 'Tab' || modalState.mode === 'busy') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var focusable = modal.querySelectorAll(
|
||||||
|
'button:not([disabled]):not([hidden]), [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
||||||
|
);
|
||||||
|
var visible = [];
|
||||||
|
for (var i = 0; i < focusable.length; i++) {
|
||||||
|
if (focusable[i].style.display === 'none') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
visible.push(focusable[i]);
|
||||||
|
}
|
||||||
|
if (!visible.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var first = visible[0];
|
||||||
|
var last = visible[visible.length - 1];
|
||||||
|
if (e.shiftKey && document.activeElement === first) {
|
||||||
|
e.preventDefault();
|
||||||
|
modalScroll().focusNoScroll(last);
|
||||||
|
} else if (!e.shiftKey && document.activeElement === last) {
|
||||||
|
e.preventDefault();
|
||||||
|
modalScroll().focusNoScroll(first);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener('keydown', modalState.onKey, true);
|
||||||
|
|
||||||
|
if (mode !== 'busy' && confirmBtn && !confirmBtn.hidden) {
|
||||||
|
window.setTimeout(function () {
|
||||||
|
modalScroll().focusNoScroll(confirmBtn);
|
||||||
|
}, 20);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode === 'busy') {
|
||||||
|
modalState.resolve = null;
|
||||||
|
return Promise.resolve(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise(function (resolve) {
|
||||||
|
modalState.resolve = resolve;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindModal() {
|
||||||
|
var modal = byId('meetlet-access-reset-modal');
|
||||||
|
if (!modal || modal.dataset.bound === '1') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modal.dataset.bound = '1';
|
||||||
|
modal.addEventListener('click', function (e) {
|
||||||
|
if (modalState.mode === 'busy') {
|
||||||
|
e.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (e.target.closest('[data-meetlet-modal-dismiss]')) {
|
||||||
|
e.preventDefault();
|
||||||
|
closeModal(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
var confirmBtn = byId('meetlet-access-reset-modal-confirm');
|
||||||
|
if (confirmBtn) {
|
||||||
|
confirmBtn.addEventListener('click', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (modalState.mode === 'busy') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
closeModal(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function postReset(root, section, str) {
|
||||||
|
return Ajax.call([{
|
||||||
|
methodname: 'mod_meetletacbfree_reset_access_settings',
|
||||||
|
args: {section: section}
|
||||||
|
}])[0].then(function(data) {
|
||||||
|
if (data && data.matrixjson) {
|
||||||
|
try {
|
||||||
|
data.matrix = JSON.parse(data.matrixjson);
|
||||||
|
} catch (e) {
|
||||||
|
data.matrix = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmCopy(section, str) {
|
||||||
|
if (section === 'rolemapping') {
|
||||||
|
return {
|
||||||
|
title: str.confirm_rolemapping_title || '',
|
||||||
|
message: str.confirm_rolemapping || '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
title: str.confirm_featureaccess_title || '',
|
||||||
|
message: str.confirm_featureaccess || '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function successCopy(section, str, serverMessage) {
|
||||||
|
if (serverMessage) {
|
||||||
|
return serverMessage;
|
||||||
|
}
|
||||||
|
if (section === 'rolemapping') {
|
||||||
|
return str.success_rolemapping || '';
|
||||||
|
}
|
||||||
|
return str.success_featureaccess || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleResetClick(btn, root, str) {
|
||||||
|
var section = btn.getAttribute('data-meetlet-reset-section') || '';
|
||||||
|
if (!section) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var copy = confirmCopy(section, str);
|
||||||
|
openModal({
|
||||||
|
mode: 'confirm',
|
||||||
|
strings: str,
|
||||||
|
title: copy.title,
|
||||||
|
message: copy.message,
|
||||||
|
confirmClass: 'btn btn-outline-danger',
|
||||||
|
}).then(function (confirmed) {
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
openModal({
|
||||||
|
mode: 'busy',
|
||||||
|
strings: str,
|
||||||
|
title: str.busy || '',
|
||||||
|
message: str.busy || '',
|
||||||
|
});
|
||||||
|
btn.disabled = true;
|
||||||
|
postReset(root, section, str).then(function (data) {
|
||||||
|
btn.disabled = false;
|
||||||
|
if (!data || !data.success) {
|
||||||
|
openModal({
|
||||||
|
mode: 'alert',
|
||||||
|
strings: str,
|
||||||
|
title: str.error || 'Error',
|
||||||
|
message: (data && data.error) || str.error || 'Error',
|
||||||
|
confirmClass: 'btn btn-primary',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var container = sectionContainer(section);
|
||||||
|
if (section === 'rolemapping') {
|
||||||
|
applyRoleMatrix(container, data.matrix);
|
||||||
|
} else {
|
||||||
|
applyFeatureMatrix(container, data.matrix);
|
||||||
|
}
|
||||||
|
openModal({
|
||||||
|
mode: 'alert',
|
||||||
|
strings: str,
|
||||||
|
title: copy.title,
|
||||||
|
message: successCopy(section, str, data.message),
|
||||||
|
confirmClass: 'btn btn-primary',
|
||||||
|
});
|
||||||
|
}).catch(function () {
|
||||||
|
btn.disabled = false;
|
||||||
|
openModal({
|
||||||
|
mode: 'alert',
|
||||||
|
strings: str,
|
||||||
|
title: str.error || 'Error',
|
||||||
|
message: str.error || 'Error',
|
||||||
|
confirmClass: 'btn btn-primary',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
var root = byId('meetlet-access-reset-root');
|
||||||
|
if (!root) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var str = parseStrings(root);
|
||||||
|
bindModal();
|
||||||
|
document.addEventListener('click', function (e) {
|
||||||
|
var btn = e.target.closest('[data-meetlet-reset-section]');
|
||||||
|
if (!btn || btn.disabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
handleResetClick(btn, root, str);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {init: init};
|
||||||
|
});
|
||||||
Vendored
+324
@@ -0,0 +1,324 @@
|
|||||||
|
// This file is part of Moodle - http://moodle.org/
|
||||||
|
//
|
||||||
|
// Moodle is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// Moodle is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @module mod_meetletacbfree/settings_conntest
|
||||||
|
* @copyright 2026 MeetLet <info@meetlet.net>
|
||||||
|
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||||
|
*/
|
||||||
|
|
||||||
|
define(['core/ajax'], function(Ajax) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
function ready(fn) {
|
||||||
|
if (document.readyState !== 'loading') {
|
||||||
|
fn();
|
||||||
|
} else {
|
||||||
|
document.addEventListener('DOMContentLoaded', fn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function el(tag, cls) {
|
||||||
|
var node = document.createElement(tag);
|
||||||
|
if (cls) {
|
||||||
|
node.className = cls;
|
||||||
|
}
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
function text(tag, cls, value) {
|
||||||
|
var node = el(tag, cls);
|
||||||
|
node.textContent = value || '';
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseLabels(raw) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(raw || '{}') || {};
|
||||||
|
} catch (e) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setProgress(root, current, total, strings) {
|
||||||
|
var fill = root.querySelector('[data-meetlet-progress-fill]');
|
||||||
|
var label = root.querySelector('[data-meetlet-progress-label]');
|
||||||
|
var pct = total ? Math.round((current / total) * 100) : 0;
|
||||||
|
if (fill) {
|
||||||
|
fill.style.width = pct + '%';
|
||||||
|
}
|
||||||
|
if (label) {
|
||||||
|
label.textContent = strings.progress
|
||||||
|
.replace('{$a->current}', String(current))
|
||||||
|
.replace('{$a->total}', String(total))
|
||||||
|
.replace('{$a->percent}', String(pct));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCard(index, total, step, label, strings) {
|
||||||
|
var wrap = el('div', 'meetlet-conntest__card meetlet-conntest__card--pending');
|
||||||
|
wrap.setAttribute('data-step', step);
|
||||||
|
wrap.setAttribute('data-index', String(index));
|
||||||
|
|
||||||
|
var top = el('div', 'meetlet-conntest__rowtop');
|
||||||
|
top.appendChild(text('span', 'meetlet-conntest__num', String(index + 1)));
|
||||||
|
top.appendChild(el('span', 'meetlet-dot meetlet-dot--pending'));
|
||||||
|
top.appendChild(text('span', 'meetlet-conntest__label', label));
|
||||||
|
top.appendChild(text('span', 'meetlet-pill meetlet-pill--pending', strings.waiting));
|
||||||
|
wrap.appendChild(top);
|
||||||
|
wrap.appendChild(text('div', 'meetlet-conntest__detail meetlet-conntest__card-detail', ''));
|
||||||
|
return wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setCardState(card, state, strings, detail) {
|
||||||
|
card.className = 'meetlet-conntest__card meetlet-conntest__card--' + state;
|
||||||
|
var dot = card.querySelector('.meetlet-dot');
|
||||||
|
var pill = card.querySelector('.meetlet-pill');
|
||||||
|
var detailNode = card.querySelector('.meetlet-conntest__card-detail');
|
||||||
|
if (dot) {
|
||||||
|
dot.className = 'meetlet-dot meetlet-dot--' + state;
|
||||||
|
}
|
||||||
|
if (pill) {
|
||||||
|
pill.className = 'meetlet-pill meetlet-pill--' + state;
|
||||||
|
if (state === 'pending') {
|
||||||
|
pill.textContent = strings.waiting;
|
||||||
|
} else if (state === 'running') {
|
||||||
|
pill.textContent = strings.runningstep;
|
||||||
|
} else if (state === 'ok') {
|
||||||
|
pill.textContent = strings.ok;
|
||||||
|
} else if (state === 'fail') {
|
||||||
|
pill.textContent = strings.fail;
|
||||||
|
} else if (state === 'skipped') {
|
||||||
|
pill.textContent = strings.skipped;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (detailNode) {
|
||||||
|
detailNode.textContent = detail || '';
|
||||||
|
detailNode.style.display = detail ? '' : 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setSubResults(card, subresults, strings) {
|
||||||
|
var wrap = card.querySelector('[data-meetlet-subresults]');
|
||||||
|
if (!wrap) {
|
||||||
|
wrap = el('div', 'meetlet-conntest__subs');
|
||||||
|
wrap.setAttribute('data-meetlet-subresults', '1');
|
||||||
|
card.appendChild(wrap);
|
||||||
|
}
|
||||||
|
wrap.innerHTML = '';
|
||||||
|
if (!subresults || !subresults.length) {
|
||||||
|
wrap.style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
wrap.style.display = '';
|
||||||
|
subresults.forEach(function (sub) {
|
||||||
|
var substate = 'fail';
|
||||||
|
var pillText = strings.fail;
|
||||||
|
if (sub.skipped) {
|
||||||
|
substate = 'skipped';
|
||||||
|
pillText = strings.skipped;
|
||||||
|
} else if (sub.ok) {
|
||||||
|
substate = 'ok';
|
||||||
|
pillText = strings.ok;
|
||||||
|
}
|
||||||
|
var subcard = el('div', 'meetlet-conntest__sub meetlet-conntest__sub--' + substate);
|
||||||
|
var top = el('div', 'meetlet-conntest__rowtop');
|
||||||
|
top.appendChild(el('span', 'meetlet-dot meetlet-dot--' + substate));
|
||||||
|
top.appendChild(text('span', 'meetlet-conntest__sublabel', sub.label || sub.id || ''));
|
||||||
|
top.appendChild(text('span', 'meetlet-pill meetlet-pill--' + substate, pillText));
|
||||||
|
subcard.appendChild(top);
|
||||||
|
if (sub.detail) {
|
||||||
|
subcard.appendChild(text('div', 'meetlet-conntest__detail', sub.detail));
|
||||||
|
}
|
||||||
|
wrap.appendChild(subcard);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function sleep(ms) {
|
||||||
|
return new Promise(function (resolve) {
|
||||||
|
setTimeout(resolve, ms);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
var SAVE_ICON = '<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">'
|
||||||
|
+ '<path fill="currentColor" d="M17 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V7l-4-4zm-5 16a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm3-10H5V5h10v4z"/>'
|
||||||
|
+ '</svg>';
|
||||||
|
|
||||||
|
document.querySelectorAll('#adminsettings .offset-sm-3').forEach(function (el) {
|
||||||
|
if (!el.querySelector('button.btn-primary, input.btn-primary[type="submit"]')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
el.classList.remove('offset-sm-3', 'col-sm-3', 'col-md-3');
|
||||||
|
el.classList.add('col-12', 'meetlet-savebar');
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll(
|
||||||
|
'#adminsettings button.btn-primary[type="submit"],'
|
||||||
|
+ ' #adminsettings input.btn-primary[type="submit"]'
|
||||||
|
).forEach(function (btn) {
|
||||||
|
if (btn.getAttribute('data-meetlet-save-icon') === '1') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
btn.setAttribute('data-meetlet-save-icon', '1');
|
||||||
|
btn.classList.add('meetlet-btn-with-icon');
|
||||||
|
if (btn.tagName === 'INPUT') {
|
||||||
|
var replacement = document.createElement('button');
|
||||||
|
replacement.type = 'submit';
|
||||||
|
replacement.name = btn.name || 'submitbutton';
|
||||||
|
replacement.value = btn.value || '';
|
||||||
|
replacement.id = btn.id || '';
|
||||||
|
replacement.className = btn.className;
|
||||||
|
replacement.setAttribute('data-meetlet-save-icon', '1');
|
||||||
|
replacement.innerHTML = SAVE_ICON
|
||||||
|
+ '<span class="meetlet-btn-with-icon__label">'
|
||||||
|
+ (btn.value || btn.getAttribute('value') || 'Save changes')
|
||||||
|
+ '</span>';
|
||||||
|
btn.parentNode.replaceChild(replacement, btn);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (btn.querySelector('svg')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var label = (btn.textContent || '').trim() || 'Save changes';
|
||||||
|
btn.textContent = '';
|
||||||
|
btn.insertAdjacentHTML('afterbegin', SAVE_ICON);
|
||||||
|
var span = document.createElement('span');
|
||||||
|
span.className = 'meetlet-btn-with-icon__label';
|
||||||
|
span.textContent = label;
|
||||||
|
btn.appendChild(span);
|
||||||
|
});
|
||||||
|
|
||||||
|
var root = document.getElementById('meetlet-acb-conntest');
|
||||||
|
if (!root) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var btn = root.querySelector('[data-meetlet-run-test]');
|
||||||
|
var grid = root.querySelector('[data-meetlet-test-grid]');
|
||||||
|
var status = root.querySelector('[data-meetlet-test-status]');
|
||||||
|
var progressWrap = root.querySelector('[data-meetlet-progress]');
|
||||||
|
if (!btn || !grid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var steps = (root.getAttribute('data-steps') || 'login,folders,user,sso,cleanup')
|
||||||
|
.split(',')
|
||||||
|
.map(function (s) { return s.trim(); })
|
||||||
|
.filter(Boolean);
|
||||||
|
var labels = parseLabels(root.getAttribute('data-step-labels'));
|
||||||
|
var strings = {
|
||||||
|
ok: root.getAttribute('data-str-ok') || 'OK',
|
||||||
|
fail: root.getAttribute('data-str-fail') || 'Fail',
|
||||||
|
running: root.getAttribute('data-str-running') || 'Running…',
|
||||||
|
idle: root.getAttribute('data-str-idle') || '',
|
||||||
|
waiting: root.getAttribute('data-str-waiting') || 'Waiting',
|
||||||
|
runningstep: root.getAttribute('data-str-runningstep') || 'Testing',
|
||||||
|
skipped: root.getAttribute('data-str-skipped') || 'Skipped',
|
||||||
|
progress: root.getAttribute('data-str-progress') || 'Step {$a->current} of {$a->total} ({$a->percent}%)'
|
||||||
|
};
|
||||||
|
|
||||||
|
btn.addEventListener('click', function () {
|
||||||
|
if (btn.disabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
btn.disabled = true;
|
||||||
|
grid.innerHTML = '';
|
||||||
|
if (progressWrap) {
|
||||||
|
progressWrap.hidden = false;
|
||||||
|
}
|
||||||
|
if (status) {
|
||||||
|
status.textContent = strings.running;
|
||||||
|
}
|
||||||
|
|
||||||
|
var cards = {};
|
||||||
|
steps.forEach(function (step, index) {
|
||||||
|
var label = labels[step] || step;
|
||||||
|
var card = buildCard(index, steps.length, step, label, strings);
|
||||||
|
setCardState(card, 'pending', strings, '');
|
||||||
|
grid.appendChild(card);
|
||||||
|
cards[step] = card;
|
||||||
|
});
|
||||||
|
setProgress(root, 0, steps.length, strings);
|
||||||
|
|
||||||
|
(async function () {
|
||||||
|
var skipToCleanup = false;
|
||||||
|
var completed = 0;
|
||||||
|
for (var i = 0; i < steps.length; i++) {
|
||||||
|
var step = steps[i];
|
||||||
|
var card = cards[step];
|
||||||
|
|
||||||
|
if (skipToCleanup && step !== 'cleanup') {
|
||||||
|
setCardState(card, 'skipped', strings, '');
|
||||||
|
completed++;
|
||||||
|
setProgress(root, completed, steps.length, strings);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
setCardState(card, 'running', strings, '');
|
||||||
|
setProgress(root, completed, steps.length, strings);
|
||||||
|
|
||||||
|
var result = await new Promise(function(resolve, reject) {
|
||||||
|
Ajax.call([{
|
||||||
|
methodname: 'mod_meetletacbfree_run_conntest_step',
|
||||||
|
args: {step: step}
|
||||||
|
}])[0].then(resolve).catch(reject);
|
||||||
|
});
|
||||||
|
setCardState(
|
||||||
|
card,
|
||||||
|
result.ok ? 'ok' : 'fail',
|
||||||
|
strings,
|
||||||
|
result.detail || ''
|
||||||
|
);
|
||||||
|
if (result.subresults && result.subresults.length) {
|
||||||
|
setSubResults(card, result.subresults, strings);
|
||||||
|
}
|
||||||
|
completed++;
|
||||||
|
setProgress(root, completed, steps.length, strings);
|
||||||
|
await sleep(220);
|
||||||
|
|
||||||
|
if (result.abort || (result.done && step !== 'cleanup')) {
|
||||||
|
skipToCleanup = true;
|
||||||
|
if (result.done && step !== 'cleanup') {
|
||||||
|
// Mark remaining non-cleanup as skipped, then stop if no cleanup needed.
|
||||||
|
for (var j = i + 1; j < steps.length; j++) {
|
||||||
|
if (steps[j] === 'cleanup') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
setCardState(cards[steps[j]], 'skipped', strings, '');
|
||||||
|
completed++;
|
||||||
|
}
|
||||||
|
// Still run cleanup if present later in loop via skipToCleanup.
|
||||||
|
}
|
||||||
|
if (step === 'cleanup') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setProgress(root, steps.length, steps.length, strings);
|
||||||
|
})().catch(function (err) {
|
||||||
|
var firstPending = grid.querySelector('.meetlet-conntest__card--pending, .meetlet-conntest__card--running');
|
||||||
|
if (firstPending) {
|
||||||
|
setCardState(firstPending, 'fail', strings, String(err && err.message ? err.message : err));
|
||||||
|
}
|
||||||
|
}).finally(function () {
|
||||||
|
btn.disabled = false;
|
||||||
|
if (status) {
|
||||||
|
status.textContent = strings.idle;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {init: init};
|
||||||
|
});
|
||||||
+336
@@ -0,0 +1,336 @@
|
|||||||
|
// This file is part of Moodle - http://moodle.org/
|
||||||
|
//
|
||||||
|
// Moodle is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// Moodle is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @module mod_meetletacbfree/settings_registration
|
||||||
|
* @copyright 2026 MeetLet <info@meetlet.net>
|
||||||
|
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||||
|
*/
|
||||||
|
|
||||||
|
define(['core/ajax', 'mod_meetletacbfree/meetlet_modal_scroll'], function(Ajax, MeetletModalScroll) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
function byId(id) {
|
||||||
|
return document.getElementById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setFlash(el, ok, message) {
|
||||||
|
if (!el) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
el.className = 'alert mt-3 ' + (ok ? 'alert-success' : 'alert-danger');
|
||||||
|
el.textContent = message || '';
|
||||||
|
el.classList.toggle('d-none', !message);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setActiveUi(active) {
|
||||||
|
var inactive = byId('meetlet-registration-actions-inactive');
|
||||||
|
var activeEl = byId('meetlet-registration-actions-active');
|
||||||
|
var product = byId('meetlet-registration-product');
|
||||||
|
if (inactive) {
|
||||||
|
inactive.classList.toggle('d-none', !!active);
|
||||||
|
}
|
||||||
|
if (activeEl) {
|
||||||
|
activeEl.classList.toggle('d-none', !active);
|
||||||
|
}
|
||||||
|
if (product) {
|
||||||
|
product.classList.toggle('d-none', !active);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSummary(summary) {
|
||||||
|
if (!summary) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var badge = byId('meetlet-registration-status-badge');
|
||||||
|
var cfg = byId('meetlet-registration-config');
|
||||||
|
if (badge) {
|
||||||
|
badge.className = 'badge meetlet-license-badge '
|
||||||
|
+ (summary.active ? 'meetlet-license-badge--active' : 'meetlet-license-badge--inactive');
|
||||||
|
badge.textContent = summary.status_label
|
||||||
|
|| (summary.active
|
||||||
|
? (cfg && cfg.dataset.labelActive) || 'Active'
|
||||||
|
: (cfg && cfg.dataset.labelInactive) || 'Inactive');
|
||||||
|
}
|
||||||
|
var domain = byId('meetlet-registration-domain');
|
||||||
|
if (domain && cfg && cfg.dataset.labelDomain) {
|
||||||
|
domain.textContent = cfg.dataset.labelDomain.replace('__V__', summary.domain || '—');
|
||||||
|
}
|
||||||
|
var installation = byId('meetlet-registration-installation');
|
||||||
|
if (installation && cfg && cfg.dataset.labelInstallation) {
|
||||||
|
installation.textContent = cfg.dataset.labelInstallation.replace('__V__', summary.installation_id || '—');
|
||||||
|
}
|
||||||
|
var product = byId('meetlet-registration-product');
|
||||||
|
if (product) {
|
||||||
|
product.textContent = summary.product_label || '';
|
||||||
|
}
|
||||||
|
var err = byId('meetlet-registration-error');
|
||||||
|
if (err) {
|
||||||
|
if (summary.error) {
|
||||||
|
err.textContent = summary.error;
|
||||||
|
err.classList.remove('d-none');
|
||||||
|
} else {
|
||||||
|
err.textContent = '';
|
||||||
|
err.classList.add('d-none');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setActiveUi(!!summary.active);
|
||||||
|
}
|
||||||
|
|
||||||
|
function currentApiUrl(cfg) {
|
||||||
|
var input = cfg && cfg.dataset.apiInput ? document.getElementById(cfg.dataset.apiInput) : null;
|
||||||
|
if (input && input.value) {
|
||||||
|
return input.value.trim();
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
var modalState = {
|
||||||
|
open: false,
|
||||||
|
resolve: null,
|
||||||
|
lastFocus: null,
|
||||||
|
onKey: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
function ensureModalOnBody(modal) {
|
||||||
|
if (modal && modal.parentNode !== document.body) {
|
||||||
|
document.body.appendChild(modal);
|
||||||
|
}
|
||||||
|
return modal;
|
||||||
|
}
|
||||||
|
|
||||||
|
function modalScroll() {
|
||||||
|
return MeetletModalScroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeRegistrationModal(result) {
|
||||||
|
var modal = byId('meetlet-registration-modal');
|
||||||
|
if (!modal) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modal.setAttribute('hidden', 'hidden');
|
||||||
|
modal.style.display = 'none';
|
||||||
|
modalScroll().unlock();
|
||||||
|
if (modalState.onKey) {
|
||||||
|
document.removeEventListener('keydown', modalState.onKey, true);
|
||||||
|
modalState.onKey = null;
|
||||||
|
}
|
||||||
|
modalState.open = false;
|
||||||
|
var resolve = modalState.resolve;
|
||||||
|
modalState.resolve = null;
|
||||||
|
if (modalState.lastFocus) {
|
||||||
|
modalScroll().focusNoScroll(modalState.lastFocus);
|
||||||
|
}
|
||||||
|
modalState.lastFocus = null;
|
||||||
|
if (typeof resolve === 'function') {
|
||||||
|
resolve(!!result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openRegistrationModal(opts) {
|
||||||
|
var modal = ensureModalOnBody(byId('meetlet-registration-modal'));
|
||||||
|
var cfg = byId('meetlet-registration-config');
|
||||||
|
if (!modal) {
|
||||||
|
return Promise.resolve(false);
|
||||||
|
}
|
||||||
|
if (modalState.open) {
|
||||||
|
closeRegistrationModal(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
var title = byId('meetlet-registration-modal-title');
|
||||||
|
var body = byId('meetlet-registration-modal-body');
|
||||||
|
var confirmBtn = byId('meetlet-registration-modal-confirm');
|
||||||
|
var tpl = opts.templateId ? document.getElementById(opts.templateId) : null;
|
||||||
|
if (!title || !body || !confirmBtn) {
|
||||||
|
return Promise.resolve(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
title.textContent = opts.title || '';
|
||||||
|
if (tpl) {
|
||||||
|
body.innerHTML = '';
|
||||||
|
if (tpl.content && tpl.content.cloneNode) {
|
||||||
|
body.appendChild(tpl.content.cloneNode(true));
|
||||||
|
} else {
|
||||||
|
body.innerHTML = tpl.innerHTML;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
body.innerHTML = '';
|
||||||
|
}
|
||||||
|
confirmBtn.textContent = opts.confirmLabel || '';
|
||||||
|
confirmBtn.className = 'btn ' + (opts.danger ? 'btn-outline-danger' : 'btn-primary');
|
||||||
|
|
||||||
|
modalState.lastFocus = document.activeElement;
|
||||||
|
modalScroll().lock();
|
||||||
|
modal.removeAttribute('hidden');
|
||||||
|
modal.style.display = 'flex';
|
||||||
|
modalState.open = true;
|
||||||
|
|
||||||
|
modalState.onKey = function (e) {
|
||||||
|
if (!modalState.open) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
closeRegistrationModal(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (e.key !== 'Tab') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var focusable = modal.querySelectorAll(
|
||||||
|
'button:not([disabled]), [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
||||||
|
);
|
||||||
|
if (!focusable.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var first = focusable[0];
|
||||||
|
var last = focusable[focusable.length - 1];
|
||||||
|
if (e.shiftKey && document.activeElement === first) {
|
||||||
|
e.preventDefault();
|
||||||
|
modalScroll().focusNoScroll(last);
|
||||||
|
} else if (!e.shiftKey && document.activeElement === last) {
|
||||||
|
e.preventDefault();
|
||||||
|
modalScroll().focusNoScroll(first);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener('keydown', modalState.onKey, true);
|
||||||
|
|
||||||
|
window.setTimeout(function () {
|
||||||
|
modalScroll().focusNoScroll(confirmBtn);
|
||||||
|
}, 20);
|
||||||
|
|
||||||
|
return new Promise(function (resolve) {
|
||||||
|
modalState.resolve = resolve;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function postAction(action) {
|
||||||
|
var cfg = byId('meetlet-registration-config');
|
||||||
|
var flash = byId('meetlet-registration-flash');
|
||||||
|
if (!cfg) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setFlash(flash, true, '…');
|
||||||
|
Ajax.call([{
|
||||||
|
methodname: 'mod_meetletacbfree_registration_action',
|
||||||
|
args: {
|
||||||
|
action: action,
|
||||||
|
registration_api_url: currentApiUrl(cfg)
|
||||||
|
}
|
||||||
|
}])[0].then(function(data) {
|
||||||
|
data.summary = {
|
||||||
|
active: !!data.active,
|
||||||
|
status: data.status,
|
||||||
|
status_label: data.status_label,
|
||||||
|
product_label: data.product_label,
|
||||||
|
domain: data.domain,
|
||||||
|
installation_id: data.installation_id,
|
||||||
|
can_deactivate: !!data.can_deactivate
|
||||||
|
};
|
||||||
|
if (data.summary) {
|
||||||
|
renderSummary(data.summary);
|
||||||
|
}
|
||||||
|
if (data.success) {
|
||||||
|
setFlash(flash, true, data.message);
|
||||||
|
if (action === 'activate' || action === 'deactivate') {
|
||||||
|
window.setTimeout(function() {
|
||||||
|
window.location.reload();
|
||||||
|
}, 600);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setFlash(flash, false, data.error || 'Request failed');
|
||||||
|
}
|
||||||
|
}).catch(function(err) {
|
||||||
|
setFlash(flash, false, err && err.message ? err.message : 'Request failed');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindModalChrome() {
|
||||||
|
var modal = byId('meetlet-registration-modal');
|
||||||
|
if (!modal || modal.dataset.bound === '1') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modal.dataset.bound = '1';
|
||||||
|
modal.addEventListener('click', function (e) {
|
||||||
|
if (e.target.closest('[data-meetlet-modal-dismiss]')) {
|
||||||
|
e.preventDefault();
|
||||||
|
closeRegistrationModal(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
var confirmBtn = byId('meetlet-registration-modal-confirm');
|
||||||
|
if (confirmBtn) {
|
||||||
|
confirmBtn.addEventListener('click', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
closeRegistrationModal(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindDeactivateButton(btn) {
|
||||||
|
if (!btn || btn.dataset.bound === '1') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var cfg = byId('meetlet-registration-config');
|
||||||
|
btn.dataset.bound = '1';
|
||||||
|
btn.addEventListener('click', function () {
|
||||||
|
openRegistrationModal({
|
||||||
|
title: (cfg && cfg.dataset.confirmDeactivateTitle) || '',
|
||||||
|
templateId: 'meetlet-registration-tpl-deactivate',
|
||||||
|
confirmLabel: (cfg && cfg.dataset.confirmDeactivateOk) || '',
|
||||||
|
danger: true,
|
||||||
|
}).then(function (ok) {
|
||||||
|
if (ok) {
|
||||||
|
postAction('deactivate');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function bind() {
|
||||||
|
var activate = byId('meetlet-registration-activate');
|
||||||
|
var recheck = byId('meetlet-registration-recheck');
|
||||||
|
var deactivate = byId('meetlet-registration-deactivate');
|
||||||
|
var cfg = byId('meetlet-registration-config');
|
||||||
|
if ((!activate && !recheck) || (activate && activate.dataset.bound === '1')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
bindModalChrome();
|
||||||
|
if (activate) {
|
||||||
|
activate.dataset.bound = '1';
|
||||||
|
activate.addEventListener('click', function () {
|
||||||
|
openRegistrationModal({
|
||||||
|
title: (cfg && cfg.dataset.confirmActivateTitle) || '',
|
||||||
|
templateId: 'meetlet-registration-tpl-activate',
|
||||||
|
confirmLabel: (cfg && cfg.dataset.confirmActivateOk) || '',
|
||||||
|
danger: false,
|
||||||
|
}).then(function (ok) {
|
||||||
|
if (ok) {
|
||||||
|
postAction('activate');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (recheck) {
|
||||||
|
recheck.dataset.bound = '1';
|
||||||
|
recheck.addEventListener('click', function () {
|
||||||
|
postAction('recheck');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
bindDeactivateButton(deactivate);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {init: bind};
|
||||||
|
});
|
||||||
Vendored
+160
@@ -0,0 +1,160 @@
|
|||||||
|
// This file is part of Moodle - http://moodle.org/
|
||||||
|
//
|
||||||
|
// Moodle is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// Moodle is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @module mod_meetletacbfree/settings_toggles
|
||||||
|
* @copyright 2026 MeetLet <info@meetlet.net>
|
||||||
|
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||||
|
*/
|
||||||
|
|
||||||
|
define([], function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
function enhanceInput(input) {
|
||||||
|
if (!input || input.getAttribute('data-meetlet-switch') === '1') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (input.type !== 'checkbox') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
input.setAttribute('data-meetlet-switch', '1');
|
||||||
|
input.classList.add('meetlet-switch__input');
|
||||||
|
|
||||||
|
var compact = input.classList.contains('meetlet-access-check');
|
||||||
|
var switchClass = compact ? 'meetlet-switch meetlet-switch--compact' : 'meetlet-switch';
|
||||||
|
|
||||||
|
var track = document.createElement('span');
|
||||||
|
track.className = 'meetlet-switch__track';
|
||||||
|
track.setAttribute('aria-hidden', 'true');
|
||||||
|
var thumb = document.createElement('span');
|
||||||
|
thumb.className = 'meetlet-switch__thumb';
|
||||||
|
track.appendChild(thumb);
|
||||||
|
|
||||||
|
var parentLabel = input.closest('label');
|
||||||
|
if (parentLabel && parentLabel.contains(input)) {
|
||||||
|
parentLabel.classList.add('meetlet-switch');
|
||||||
|
if (compact) {
|
||||||
|
parentLabel.classList.add('meetlet-switch--compact');
|
||||||
|
}
|
||||||
|
if (input.nextSibling) {
|
||||||
|
parentLabel.insertBefore(track, input.nextSibling);
|
||||||
|
} else {
|
||||||
|
parentLabel.appendChild(track);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var wrap = document.createElement('label');
|
||||||
|
wrap.className = switchClass;
|
||||||
|
if (input.id) {
|
||||||
|
wrap.setAttribute('for', input.id);
|
||||||
|
}
|
||||||
|
input.parentNode.insertBefore(wrap, input);
|
||||||
|
wrap.appendChild(input);
|
||||||
|
wrap.appendChild(track);
|
||||||
|
}
|
||||||
|
|
||||||
|
function run() {
|
||||||
|
var adminRoot = document.querySelector('#adminsettings');
|
||||||
|
if (adminRoot) {
|
||||||
|
adminRoot.querySelectorAll('.form-setting input[type="checkbox"]').forEach(enhanceInput);
|
||||||
|
}
|
||||||
|
document.querySelectorAll('.meetlet-access-check').forEach(enhanceInput);
|
||||||
|
var form = document.getElementById('mform1') || document.querySelector('form.mform');
|
||||||
|
if (form) {
|
||||||
|
form.querySelectorAll(
|
||||||
|
' input[name="autooffline"], input[name="showzip"], input[name="showmp4"],' +
|
||||||
|
' input[name="showmakeoffline"], input[name="showrecordingstab"], input[name="showcontenttab"],' +
|
||||||
|
' input[name="featureaccess_override"], input[name="default_waitforhost"],' +
|
||||||
|
' input[name="waitforhost"], input[name="https"]'
|
||||||
|
).forEach(enhanceInput);
|
||||||
|
}
|
||||||
|
bindHourPicker();
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectedHours(wrap) {
|
||||||
|
var hours = [];
|
||||||
|
wrap.querySelectorAll('.meetlet-hourchip.is-on').forEach(function (btn) {
|
||||||
|
hours.push(parseInt(btn.getAttribute('data-hour'), 10));
|
||||||
|
});
|
||||||
|
hours.sort(function (a, b) { return a - b; });
|
||||||
|
return hours;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyHours(wrap, list) {
|
||||||
|
var set = {};
|
||||||
|
(list || []).forEach(function (h) { set[String(h)] = true; });
|
||||||
|
wrap.querySelectorAll('.meetlet-hourchip').forEach(function (btn) {
|
||||||
|
var on = !!set[btn.getAttribute('data-hour')];
|
||||||
|
btn.classList.toggle('is-on', on);
|
||||||
|
btn.setAttribute('aria-pressed', on ? 'true' : 'false');
|
||||||
|
});
|
||||||
|
var input = wrap.querySelector('[data-meetlet-hours-input]');
|
||||||
|
if (input) {
|
||||||
|
input.value = selectedHours(wrap).join(',');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncHoursEnabled() {
|
||||||
|
var sel = document.querySelector('select[name="s_meetletacbfree_agent_dispatch"]')
|
||||||
|
|| document.getElementById('id_s_meetletacbfree_agent_dispatch');
|
||||||
|
var wrap = document.querySelector('[data-meetlet-hours-wrap]');
|
||||||
|
if (!wrap) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var item = wrap.closest('.form-item');
|
||||||
|
var windowMode = !sel || sel.value === 'window';
|
||||||
|
wrap.classList.toggle('is-dimmed', !windowMode);
|
||||||
|
if (item) {
|
||||||
|
item.classList.toggle('is-dimmed', !windowMode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindHourPicker() {
|
||||||
|
var wrap = document.querySelector('[data-meetlet-hours-wrap]');
|
||||||
|
if (!wrap || wrap.getAttribute('data-bound') === '1') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
wrap.setAttribute('data-bound', '1');
|
||||||
|
wrap.addEventListener('click', function (e) {
|
||||||
|
var chip = e.target.closest('.meetlet-hourchip');
|
||||||
|
if (chip && wrap.contains(chip)) {
|
||||||
|
e.preventDefault();
|
||||||
|
chip.classList.toggle('is-on');
|
||||||
|
chip.setAttribute('aria-pressed', chip.classList.contains('is-on') ? 'true' : 'false');
|
||||||
|
applyHours(wrap, selectedHours(wrap));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var preset = e.target.closest('.meetlet-hourpreset');
|
||||||
|
if (preset && wrap.contains(preset)) {
|
||||||
|
e.preventDefault();
|
||||||
|
var raw = preset.getAttribute('data-hours') || '';
|
||||||
|
var list = raw === '' ? [] : raw.split(',').map(function (x) { return parseInt(x, 10); });
|
||||||
|
applyHours(wrap, list);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
var sel = document.querySelector('select[name="s_meetletacbfree_agent_dispatch"]')
|
||||||
|
|| document.getElementById('id_s_meetletacbfree_agent_dispatch');
|
||||||
|
if (sel) {
|
||||||
|
sel.addEventListener('change', syncHoursEnabled);
|
||||||
|
if (document.documentElement.getAttribute('dir') === 'rtl') {
|
||||||
|
sel.setAttribute('dir', 'rtl');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
syncHoursEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {init: run};
|
||||||
|
});
|
||||||
Vendored
+1052
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user