91334a46b5
Co-authored-by: Cursor <cursoragent@cursor.com>
481 lines
19 KiB
JavaScript
481 lines
19 KiB
JavaScript
/**
|
|
* MeetLet mod form participants — page-local only (not AMD).
|
|
*/
|
|
require(['core/str', 'core/notification', 'core/templates', 'core/ajax'], function(Str, Notification, Templates, Ajax) {
|
|
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();
|
|
});
|