Files
meetletacbfree/amd/build/modform.min.js
T
mrxzamani 988aafce39 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>
2026-09-20 12:42:19 +03:30

505 lines
20 KiB
JavaScript

// 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
};
});