From 988aafce39e13bdb74df38860aa0e45f5f1db885 Mon Sep 17 00:00:00 2001 From: Mohammad Reza Zamani Date: Sun, 20 Sep 2026 12:42:19 +0330 Subject: [PATCH] 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 --- .gitignore | 1 - amd/build/featureaccess_matrix.min.js | 67 ++ amd/build/meetlet_modal_scroll.min.js | 124 +++ amd/build/modform.min.js | 504 ++++++++++++ amd/build/settings_access_reset.min.js | 418 ++++++++++ amd/build/settings_conntest.min.js | 324 ++++++++ amd/build/settings_registration.min.js | 336 ++++++++ amd/build/settings_toggles.min.js | 160 ++++ amd/build/view.min.js | 1052 ++++++++++++++++++++++++ 9 files changed, 2985 insertions(+), 1 deletion(-) create mode 100644 amd/build/featureaccess_matrix.min.js create mode 100644 amd/build/meetlet_modal_scroll.min.js create mode 100644 amd/build/modform.min.js create mode 100644 amd/build/settings_access_reset.min.js create mode 100644 amd/build/settings_conntest.min.js create mode 100644 amd/build/settings_registration.min.js create mode 100644 amd/build/settings_toggles.min.js create mode 100644 amd/build/view.min.js diff --git a/.gitignore b/.gitignore index b6a9f12..544042a 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,6 @@ Desktop.ini .phpunit.result.cache .phpunit.cache/ /coverage/ -/amd/build/ grunt/ npm-debug.log* diff --git a/amd/build/featureaccess_matrix.min.js b/amd/build/featureaccess_matrix.min.js new file mode 100644 index 0000000..a732dcf --- /dev/null +++ b/amd/build/featureaccess_matrix.min.js @@ -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 . + +/** + * @module mod_meetletacbfree/featureaccess_matrix + * @copyright 2026 MeetLet + * @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}; +}); diff --git a/amd/build/meetlet_modal_scroll.min.js b/amd/build/meetlet_modal_scroll.min.js new file mode 100644 index 0000000..c4f7375 --- /dev/null +++ b/amd/build/meetlet_modal_scroll.min.js @@ -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 . + +/** + * @module mod_meetletacbfree/meetlet_modal_scroll + * @copyright 2026 MeetLet + * @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 + }; +}); diff --git a/amd/build/modform.min.js b/amd/build/modform.min.js new file mode 100644 index 0000000..846324b --- /dev/null +++ b/amd/build/modform.min.js @@ -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 . + +/** + * @module mod_meetletacbfree/modform + * @copyright 2026 MeetLet + * @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 + }; +}); diff --git a/amd/build/settings_access_reset.min.js b/amd/build/settings_access_reset.min.js new file mode 100644 index 0000000..a420073 --- /dev/null +++ b/amd/build/settings_access_reset.min.js @@ -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 . + +/** + * @module mod_meetletacbfree/settings_access_reset + * @copyright 2026 MeetLet + * @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}; +}); diff --git a/amd/build/settings_conntest.min.js b/amd/build/settings_conntest.min.js new file mode 100644 index 0000000..657e7f1 --- /dev/null +++ b/amd/build/settings_conntest.min.js @@ -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 . + +/** + * @module mod_meetletacbfree/settings_conntest + * @copyright 2026 MeetLet + * @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 = ''; + + 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 + + '' + + (btn.value || btn.getAttribute('value') || 'Save changes') + + ''; + 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}; +}); diff --git a/amd/build/settings_registration.min.js b/amd/build/settings_registration.min.js new file mode 100644 index 0000000..3d736dc --- /dev/null +++ b/amd/build/settings_registration.min.js @@ -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 . + +/** + * @module mod_meetletacbfree/settings_registration + * @copyright 2026 MeetLet + * @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}; +}); diff --git a/amd/build/settings_toggles.min.js b/amd/build/settings_toggles.min.js new file mode 100644 index 0000000..727ffa4 --- /dev/null +++ b/amd/build/settings_toggles.min.js @@ -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 . + +/** + * @module mod_meetletacbfree/settings_toggles + * @copyright 2026 MeetLet + * @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}; +}); diff --git a/amd/build/view.min.js b/amd/build/view.min.js new file mode 100644 index 0000000..bfc4348 --- /dev/null +++ b/amd/build/view.min.js @@ -0,0 +1,1052 @@ +// 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 . + +/** + * @module mod_meetletacbfree/view + * @copyright 2026 MeetLet + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +define(['core/ajax', 'core/notification', 'core/str'], function(Ajax, Notification, Str) { + function init() { + const SELECTORS = { + config: '[data-meetlet-config]', + app: '.meetlet-app', + tab: '.meetlet-tab', + panel: '[data-panel]', + join: '[data-action="join"]', + offline: '[data-action="makeoffline"]', + launchRecording: '[data-action="launch-recording"]', + launchContent: '[data-action="launch-content"]', + downloadContent: '[data-action="download-content"]', + refresh: '[data-action="refresh-tab"]', + overlay: '[data-join-overlay]', + sessionDetails: '[data-action="session-details"]', + modal: '[data-attendance-modal]', + modalBody: '[data-attendance-modal-body]', + modalTitle: '[data-attendance-modal-title]', + modalClose: '[data-attendance-modal-close]', + }; + + let state = { + cmid: 0, + groupid: 0, + current: 'details', + cache: {}, + joinable: true, + status: 'open', + statusMessage: '', + pollSeconds: 15, + statusTimer: null, + recPollTimer: null, + licenseUsable: true, + }; + + const RECORDINGS_POLL_MS = 60000; + const DOWNLOAD_CONTENT_TIMEOUT_MS = 320000; + let modalLabels = { + name: '', + login: '', + timein: '', + timeout: '', + empty: '', + }; + + const detailsHtml = () => { + const tpl = document.querySelector('[data-meetlet-details-html]'); + return tpl ? tpl.innerHTML : ''; + }; + + const setLoading = (panel, text) => { + panel.innerHTML = '
' + + text + '
'; + }; + + const showDetails = (panel) => { + panel.innerHTML = detailsHtml(); + }; + + const escapeHtml = (value) => { + return String(value) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); + }; + + let floatTipEl = null; + let floatTipTarget = null; + + const ensureFloatTip = () => { + if (floatTipEl && document.body.contains(floatTipEl)) { + return floatTipEl; + } + floatTipEl = document.createElement('div'); + floatTipEl.className = 'meetlet-floattip'; + floatTipEl.setAttribute('role', 'tooltip'); + floatTipEl.hidden = true; + document.body.appendChild(floatTipEl); + return floatTipEl; + }; + + const hideFloatTip = () => { + floatTipTarget = null; + const el = ensureFloatTip(); + el.classList.remove('is-visible', 'is-below'); + el.hidden = true; + el.textContent = ''; + }; + + const placeFloatTip = (target) => { + const text = (target.getAttribute('data-tip') || '').trim(); + if (!text) { + hideFloatTip(); + return; + } + floatTipTarget = target; + const el = ensureFloatTip(); + el.textContent = text; + el.hidden = false; + el.classList.remove('is-visible', 'is-below'); + el.style.left = '0px'; + el.style.top = '0px'; + const tipW = el.offsetWidth; + const tipH = el.offsetHeight; + const r = target.getBoundingClientRect(); + let left = r.left + (r.width / 2) - (tipW / 2); + left = Math.max(8, Math.min(left, window.innerWidth - tipW - 8)); + let top = r.top - tipH - 10; + let below = false; + if (top < 8) { + top = r.bottom + 10; + below = true; + } + if (top + tipH > window.innerHeight - 8 && !below) { + top = Math.max(8, r.bottom + 10); + below = true; + } + el.style.left = left + 'px'; + el.style.top = top + 'px'; + el.classList.toggle('is-below', below); + requestAnimationFrame(() => { + if (floatTipTarget === target) { + el.classList.add('is-visible'); + } + }); + }; + + const bindFloatTips = (root) => { + if (!root || root.dataset.meetletTipsBound === '1') { + return; + } + root.dataset.meetletTipsBound = '1'; + root.addEventListener('pointerover', (e) => { + const tip = e.target.closest('[data-tip]'); + if (!tip || !root.contains(tip)) { + return; + } + placeFloatTip(tip); + }); + root.addEventListener('pointerout', (e) => { + const tip = e.target.closest('[data-tip]'); + if (!tip || !root.contains(tip)) { + return; + } + const next = e.relatedTarget; + if (next && tip.contains(next)) { + return; + } + if (floatTipTarget === tip) { + hideFloatTip(); + } + }); + root.addEventListener('focusin', (e) => { + const tip = e.target.closest('[data-tip]'); + if (tip && root.contains(tip)) { + placeFloatTip(tip); + } + }); + root.addEventListener('focusout', (e) => { + const tip = e.target.closest('[data-tip]'); + if (!tip) { + return; + } + const next = e.relatedTarget; + if (next && tip.contains(next)) { + return; + } + if (floatTipTarget === tip) { + hideFloatTip(); + } + }); + }; + + const modalScroll = () => window.MeetletModalScroll || { + lock: () => {}, + unlock: () => {}, + focusNoScroll: (el) => { + if (el && typeof el.focus === 'function') { + el.focus(); + } + }, + }; + + const closeModal = () => { + const modal = document.querySelector(SELECTORS.modal); + if (!modal) { + return; + } + modal.setAttribute('hidden', 'hidden'); + modal.style.display = 'none'; + modalScroll().unlock(); + }; + + const ensureModalShell = () => { + let modal = document.querySelector(SELECTORS.modal); + if (modal) { + return modal; + } + modal = document.createElement('div'); + modal.className = 'meetlet-modal'; + modal.setAttribute('data-attendance-modal', '1'); + modal.setAttribute('hidden', 'hidden'); + modal.style.display = 'none'; + modal.innerHTML = + '
' + + ''; + document.body.appendChild(modal); + modal.addEventListener('click', (e) => { + if (e.target.closest(SELECTORS.modalClose)) { + closeModal(); + } + }); + return modal; + }; + + const openModal = (title, attendees) => { + const modal = ensureModalShell(); + const titleEl = modal.querySelector(SELECTORS.modalTitle); + const body = modal.querySelector(SELECTORS.modalBody); + if (titleEl) { + titleEl.textContent = title || ''; + } + if (!attendees || !attendees.length) { + body.innerHTML = '
' + escapeHtml(modalLabels.empty) + '
'; + } else { + let rows = ''; + attendees.forEach((row) => { + rows += '' + + '' + escapeHtml(row.name || '') + '' + + '' + escapeHtml(row.login || '') + '' + + '' + escapeHtml(row.timein || '-') + '' + + '' + escapeHtml(row.timeout || '-') + '' + + ''; + }); + body.innerHTML = + '
' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + rows + '
' + escapeHtml(modalLabels.name) + '' + escapeHtml(modalLabels.login) + '' + escapeHtml(modalLabels.timein) + '' + escapeHtml(modalLabels.timeout) + '
'; + } + modalScroll().lock(); + modal.removeAttribute('hidden'); + modal.style.display = 'flex'; + }; + + const readSessionsPayload = (panel) => { + const holder = panel.querySelector('[data-meetlet-sessions-json]'); + if (!holder) { + return []; + } + try { + return JSON.parse(holder.textContent || '[]'); + } catch (e) { + return []; + } + }; + + const setButtonBusy = (btn, busy) => { + if (!btn) { + return; + } + if (btn.tagName !== 'A') { + btn.disabled = !!busy; + } + btn.classList.toggle('is-busy', !!busy); + if (busy) { + btn.setAttribute('aria-busy', 'true'); + if (!btn.querySelector('.meetlet-iconbtn__spinner')) { + const spin = document.createElement('span'); + spin.className = 'meetlet-iconbtn__spinner'; + spin.setAttribute('aria-hidden', 'true'); + btn.appendChild(spin); + } + } else { + btn.removeAttribute('aria-busy'); + const spin = btn.querySelector('.meetlet-iconbtn__spinner'); + if (spin) { + spin.remove(); + } + } + }; + + const parseDownloadFilename = (disposition, fallback) => { + if (!disposition) { + return fallback; + } + const utf8 = disposition.match(/filename\*=UTF-8''([^;\n]+)/i); + if (utf8 && utf8[1]) { + try { + return decodeURIComponent(utf8[1].trim()); + } catch (e) { + return utf8[1].trim(); + } + } + const quoted = disposition.match(/filename="([^"]+)"/i); + if (quoted && quoted[1]) { + return quoted[1].trim(); + } + const plain = disposition.match(/filename=([^;\n]+)/i); + if (plain && plain[1]) { + return plain[1].trim().replace(/^"|"$/g, ''); + } + return fallback; + }; + + const triggerFileDownload = (blob, filename) => { + const url = URL.createObjectURL(blob); + const anchor = document.createElement('a'); + anchor.href = url; + anchor.download = filename || 'download'; + anchor.style.display = 'none'; + document.body.appendChild(anchor); + anchor.click(); + anchor.remove(); + window.setTimeout(() => URL.revokeObjectURL(url), 1000); + }; + + const notifyDownloadError = (err) => { + const isAbort = err && err.name === 'AbortError'; + const key = isAbort ? 'downloadcontenttimeout' : 'downloadcontentfailed'; + return Str.get_string(key, 'meetletacbfree').then((message) => { + Notification.addNotification({message: message, type: 'error'}); + return null; + }).catch(() => { + Notification.addNotification({ + message: isAbort ? 'Download timed out.' : 'Download failed.', + type: 'error', + }); + }); + }; + + const downloadContentFile = (link) => { + const url = link.getAttribute('href'); + const fallbackName = link.getAttribute('download') || 'download'; + if (!url || link.classList.contains('is-busy')) { + return Promise.resolve(); + } + setButtonBusy(link, true); + const controller = new AbortController(); + const timer = window.setTimeout(() => controller.abort(), DOWNLOAD_CONTENT_TIMEOUT_MS); + return fetch(url, { + credentials: 'same-origin', + signal: controller.signal, + }).then((response) => { + if (!response.ok) { + throw new Error('http-' + response.status); + } + const filename = parseDownloadFilename( + response.headers.get('Content-Disposition'), + fallbackName + ); + // Proxy delay is over once headers arrive — re-enable before blob finishes. + setButtonBusy(link, false); + return response.blob().then((blob) => { + if (!blob || blob.size <= 0) { + throw new Error('empty'); + } + triggerFileDownload(blob, filename); + }); + }).catch((err) => { + setButtonBusy(link, false); + return notifyDownloadError(err); + }).finally(() => { + window.clearTimeout(timer); + }); + }; + + const endInlineRename = (wrap, restore) => { + const text = wrap.querySelector('.meetlet-recname__text'); + const input = wrap.querySelector('.meetlet-recname__input'); + const btn = wrap.querySelector('[data-action="inline-rename"]'); + wrap.classList.remove('is-editing'); + if (input) { + input.hidden = true; + if (restore && text) { + input.value = wrap.getAttribute('data-name') || text.textContent || ''; + } + } + if (text) { + text.hidden = false; + } + if (btn) { + btn.hidden = false; + setButtonBusy(btn, false); + } + }; + + const saveInlineRename = (wrap) => { + const input = wrap.querySelector('.meetlet-recname__input'); + const text = wrap.querySelector('.meetlet-recname__text'); + const btn = wrap.querySelector('[data-action="inline-rename"]'); + if (!input || wrap.dataset.saving === '1') { + return; + } + const current = wrap.getAttribute('data-name') || ''; + const name = String(input.value || '').trim(); + if (name === '' || name === current) { + endInlineRename(wrap, true); + return; + } + wrap.dataset.saving = '1'; + if (btn) { + setButtonBusy(btn, true); + } + input.disabled = true; + Ajax.call([{ + methodname: 'mod_meetletacbfree_rename_recording', + args: { + cmid: state.cmid, + recordingscoid: parseInt(wrap.getAttribute('data-recording'), 10), + name: name, + groupid: state.groupid, + }, + }])[0].then((res) => { + wrap.setAttribute('data-name', name); + if (text) { + text.textContent = name; + } + input.value = name; + Notification.addNotification({message: res.message, type: 'success'}); + endInlineRename(wrap, false); + const app = document.querySelector(SELECTORS.app); + const panel = app && app.querySelector(SELECTORS.panel); + if (state.current === 'recordings' && panel) { + state.cache.recordings = panel.innerHTML; + } + }).catch((err) => { + Notification.exception(err); + endInlineRename(wrap, true); + }).finally(() => { + wrap.dataset.saving = '0'; + input.disabled = false; + }); + }; + + const startInlineRename = (wrap) => { + const text = wrap.querySelector('.meetlet-recname__text'); + const input = wrap.querySelector('.meetlet-recname__input'); + const btn = wrap.querySelector('[data-action="inline-rename"]'); + if (!input) { + return; + } + wrap.classList.add('is-editing'); + if (text) { + text.hidden = true; + } + if (btn) { + btn.hidden = true; + } + input.hidden = false; + input.value = wrap.getAttribute('data-name') || (text ? text.textContent : '') || ''; + input.focus(); + input.select(); + const onKey = (ev) => { + if (ev.key === 'Enter') { + ev.preventDefault(); + input.removeEventListener('keydown', onKey); + input.removeEventListener('blur', onBlur); + saveInlineRename(wrap); + } else if (ev.key === 'Escape') { + ev.preventDefault(); + input.removeEventListener('keydown', onKey); + input.removeEventListener('blur', onBlur); + endInlineRename(wrap, true); + } + }; + const onBlur = () => { + input.removeEventListener('keydown', onKey); + input.removeEventListener('blur', onBlur); + saveInlineRename(wrap); + }; + input.addEventListener('keydown', onKey); + input.addEventListener('blur', onBlur); + }; + + const renderCached = (panel, tab) => { + panel.innerHTML = state.cache[tab]; + bindPanelActions(panel); + }; + + const bindPanelActions = (panel) => { + panel.querySelectorAll(SELECTORS.offline).forEach((btn) => { + btn.addEventListener('click', () => { + setButtonBusy(btn, true); + Ajax.call([{ + methodname: 'mod_meetletacbfree_make_offline', + args: { + cmid: state.cmid, + recordingscoid: parseInt(btn.dataset.recording, 10), + groupid: state.groupid, + }, + }])[0].then((res) => { + setButtonBusy(btn, false); + Notification.addNotification({ + message: res.message, + type: res.success ? 'success' : 'warning', + }); + // Force re-fetch recordings after job request. + return loadTab('recordings', true); + }).catch((err) => { + setButtonBusy(btn, false); + const msg = (err && (err.message || err.error)) ? (err.message || err.error) : ''; + if (msg) { + Notification.addNotification({message: msg, type: 'error'}); + } else { + Notification.exception(err); + } + }); + }); + }); + + panel.querySelectorAll(SELECTORS.launchRecording).forEach((btn) => { + btn.addEventListener('click', (e) => { + e.preventDefault(); + const scoid = parseInt(btn.dataset.recording, 10); + const mode = (btn.dataset.mode || 'play').toLowerCase(); + if (!scoid) { + return; + } + setButtonBusy(btn, true); + setOverlay(true, 'recording'); + Ajax.call([{ + methodname: 'mod_meetletacbfree_get_recording_launch_url', + args: { + cmid: state.cmid, + recordingscoid: scoid, + mode: mode, + groupid: state.groupid, + }, + }])[0].then((response) => { + setOverlay(false); + setButtonBusy(btn, false); + handleLaunchResponse(response); + return null; + }).catch((err) => { + setOverlay(false); + setButtonBusy(btn, false); + handleLaunchResponse(null, err); + }); + }); + }); + + panel.querySelectorAll(SELECTORS.launchContent).forEach((btn) => { + btn.addEventListener('click', (e) => { + e.preventDefault(); + const scoid = parseInt(btn.dataset.content, 10); + if (!scoid) { + return; + } + setButtonBusy(btn, true); + setOverlay(true, 'content'); + Ajax.call([{ + methodname: 'mod_meetletacbfree_get_content_launch_url', + args: { + cmid: state.cmid, + contentscoid: scoid, + groupid: state.groupid, + }, + }])[0].then((response) => { + setOverlay(false); + setButtonBusy(btn, false); + handleLaunchResponse(response); + return null; + }).catch((err) => { + setOverlay(false); + setButtonBusy(btn, false); + handleLaunchResponse(null, err); + }); + }); + }); + + panel.querySelectorAll(SELECTORS.downloadContent).forEach((link) => { + link.addEventListener('click', (e) => { + e.preventDefault(); + downloadContentFile(link); + }); + }); + + panel.querySelectorAll('[data-action="inline-rename"]').forEach((btn) => { + btn.addEventListener('click', (e) => { + e.preventDefault(); + e.stopPropagation(); + const wrap = btn.closest('.meetlet-recname'); + if (!wrap || wrap.classList.contains('is-editing')) { + return; + } + startInlineRename(wrap); + }); + }); + + panel.onclick = (e) => { + const refreshBtn = e.target.closest(SELECTORS.refresh); + if (refreshBtn && panel.contains(refreshBtn)) { + e.preventDefault(); + const tab = refreshBtn.getAttribute('data-tab') || state.current; + loadTab(tab, true); + return; + } + const btn = e.target.closest(SELECTORS.sessionDetails); + if (!btn || !panel.contains(btn)) { + return; + } + e.preventDefault(); + const sessions = readSessionsPayload(panel); + const index = parseInt(btn.getAttribute('data-session-index') || '0', 10); + const session = sessions[index] || {title: '', attendees: []}; + openModal(session.title || '', session.attendees || []); + }; + }; + + const loadTab = (tab, force) => { + const app = document.querySelector(SELECTORS.app); + if (!app) { + return Promise.resolve(); + } + const panel = app.querySelector(SELECTORS.panel); + state.current = tab; + app.querySelectorAll(SELECTORS.tab).forEach((btn) => { + const active = btn.dataset.tab === tab; + btn.classList.toggle('is-active', active); + btn.setAttribute('aria-selected', active ? 'true' : 'false'); + }); + + if (tab === 'details') { + showDetails(panel); + return Promise.resolve(); + } + + if (!force && state.cache[tab]) { + renderCached(panel, tab); + if (tab === 'recordings') { + ensureRecordingsPoll(); + } + return Promise.resolve(); + } + + return Str.get_string('loading', 'meetletacbfree').then((loading) => { + setLoading(panel, loading); + return Ajax.call([{ + methodname: 'mod_meetletacbfree_get_tab', + args: {cmid: state.cmid, tab: tab, groupid: state.groupid}, + }])[0]; + }).then((response) => { + state.cache[tab] = response.html; + panel.innerHTML = response.html; + bindPanelActions(panel); + if (tab === 'recordings') { + resetRecordingsPoll(); + } + return null; + }).catch(Notification.exception); + }; + + const pollRecordingsBackground = () => { + if (!state.cache.recordings) { + return; + } + Ajax.call([{ + methodname: 'mod_meetletacbfree_get_tab', + args: {cmid: state.cmid, tab: 'recordings', groupid: state.groupid}, + }])[0].then((response) => { + state.cache.recordings = response.html; + if (state.current !== 'recordings') { + return null; + } + const app = document.querySelector(SELECTORS.app); + const panel = app && app.querySelector(SELECTORS.panel); + if (panel) { + panel.innerHTML = response.html; + bindPanelActions(panel); + } + return null; + }).catch(() => null); + }; + + const stopRecordingsPoll = () => { + if (state.recPollTimer) { + window.clearInterval(state.recPollTimer); + state.recPollTimer = null; + } + }; + + const resetRecordingsPoll = () => { + stopRecordingsPoll(); + state.recPollTimer = window.setInterval(pollRecordingsBackground, RECORDINGS_POLL_MS); + }; + + const ensureRecordingsPoll = () => { + if (!state.recPollTimer && state.cache.recordings) { + resetRecordingsPoll(); + } + }; + + const setOverlay = (show, kind) => { + const overlay = document.querySelector(SELECTORS.overlay); + if (!overlay) { + return; + } + if (show) { + const titleEl = overlay.querySelector('.meetlet-joinoverlay__title'); + const textEl = overlay.querySelector('.meetlet-joinoverlay__text'); + const isRecording = kind === 'recording'; + if (titleEl) { + titleEl.textContent = isRecording + ? (state.strRecordingTitle || titleEl.textContent) + : (state.strJoiningTitle || titleEl.textContent); + } + if (textEl) { + textEl.textContent = isRecording + ? (state.strRecordingText || textEl.textContent) + : (state.strJoiningText || textEl.textContent); + } + overlay.removeAttribute('hidden'); + } else { + overlay.setAttribute('hidden', 'hidden'); + } + }; + + const showGateModal = (title, message) => { + showHelpError({ + message: message || '', + helpcode: '', + detailhtml: '', + }, title); + }; + + const bindCopyCode = (root) => { + if (!root) { + return; + } + root.querySelectorAll('[data-copy-code]').forEach((el) => { + el.style.cursor = 'pointer'; + el.title = el.getAttribute('data-copy-hint') || el.textContent || ''; + el.addEventListener('click', () => { + const code = el.getAttribute('data-copy-code') || el.textContent || ''; + if (!code || !navigator.clipboard) { + return; + } + navigator.clipboard.writeText(code).then(() => { + el.classList.add('is-copied'); + window.setTimeout(() => el.classList.remove('is-copied'), 1200); + }).catch(() => null); + }); + }); + }; + + const showHelpError = (payload, title) => { + const modal = ensureModalShell(); + const titleEl = modal.querySelector(SELECTORS.modalTitle); + const body = modal.querySelector(SELECTORS.modalBody); + if (titleEl) { + titleEl.textContent = title || ''; + } + const code = (payload && (payload.helpcode || payload.code)) || ''; + const message = (payload && payload.message) || ''; + const detail = (payload && payload.detailhtml) || ''; + if (body) { + if (detail) { + body.innerHTML = detail; + } else { + let html = '
'; + html += '

' + escapeHtml(message) + '

'; + if (code) { + html += '
'; + html += ''; + html += '' + escapeHtml(code) + ''; + html += '
'; + } + html += '
'; + body.innerHTML = html; + } + bindCopyCode(body); + } + modalScroll().lock(); + modal.removeAttribute('hidden'); + modal.style.display = 'flex'; + }; + + const handleLaunchResponse = (response, fallbackErr) => { + if (response && response.ok && response.url) { + window.open(response.url, '_blank', 'noopener'); + return; + } + if (response && (response.message || response.detailhtml || response.helpcode)) { + Str.get_string('joindeniedtitle', 'meetletacbfree').then((title) => { + showHelpError(response, title); + return null; + }).catch(() => showHelpError(response, '')); + return; + } + if (fallbackErr) { + const msg = (fallbackErr.message || fallbackErr.error) ? (fallbackErr.message || fallbackErr.error) : ''; + if (msg) { + Str.get_string('joindeniedtitle', 'meetletacbfree').then((title) => { + showHelpError({message: msg}, title); + return null; + }).catch(() => Notification.exception(fallbackErr)); + } else { + Notification.exception(fallbackErr); + } + } + }; + + const applyMeetingStatus = (info) => { + state.joinable = !!info.joinable; + state.status = info.status || state.status; + let message = info.message || ''; + if (!state.joinable && (info.status === 'waitinghost' || info.errorcode === 'waitingforhost')) { + // Prefer the clearer moderator-wait explanation for the join tip. + message = state.strJoinWaiting || message; + } + state.statusMessage = message; + const wrap = document.querySelector('[data-meetlet-status-wrap]'); + const badge = document.querySelector('[data-meetlet-status]'); + const joinBtn = document.querySelector(SELECTORS.join); + const joinWrap = document.querySelector('[data-join-wrap]'); + if (wrap) { + wrap.setAttribute('data-status', state.status); + } + if (badge) { + badge.className = 'meetlet-status meetlet-status--' + state.status; + badge.textContent = info.statuslabel || badge.textContent; + if (!state.joinable && state.statusMessage) { + badge.setAttribute('data-tip', state.statusMessage); + badge.setAttribute('tabindex', '0'); + } else { + badge.removeAttribute('data-tip'); + badge.removeAttribute('tabindex'); + } + } + if (joinBtn) { + joinBtn.classList.toggle('is-disabled', !state.joinable); + if (state.joinable) { + joinBtn.removeAttribute('aria-disabled'); + } else { + joinBtn.setAttribute('aria-disabled', 'true'); + } + } + if (joinWrap) { + joinWrap.classList.toggle('is-blocked', !state.joinable); + joinWrap.removeAttribute('data-tip'); + } + }; + + const CIRC = 2 * Math.PI * 16; + const setRingProgress = (remaining, total) => { + const ring = document.querySelector('[data-status-ring]'); + const count = document.querySelector('[data-status-countdown]'); + if (count) { + count.textContent = String(remaining); + } + if (ring) { + const ratio = total > 0 ? (remaining / total) : 0; + ring.style.strokeDasharray = String(CIRC); + ring.style.strokeDashoffset = String(CIRC * (1 - ratio)); + } + }; + + const pollMeetingStatus = () => { + return Ajax.call([{ + methodname: 'mod_meetletacbfree_get_meeting_status', + args: {cmid: state.cmid, groupid: state.groupid}, + }])[0].then((info) => { + applyMeetingStatus(info); + return info; + }).catch(() => null); + }; + + const startStatusTicker = () => { + if (!state.licenseUsable || state.pollSeconds <= 0) { + return; + } + const total = state.pollSeconds || 15; + let remaining = total; + setRingProgress(remaining, total); + if (state.statusTimer) { + window.clearInterval(state.statusTimer); + } + const tick = () => { + remaining -= 1; + if (remaining <= 0) { + remaining = total; + setRingProgress(remaining, total); + pollMeetingStatus(); + return; + } + setRingProgress(remaining, total); + }; + state.statusTimer = window.setInterval(tick, 1000); + // After first paint / idle: ask Adobe for real room status, then every 15s via ticker. + const kickoff = () => { + pollMeetingStatus().then(() => { + remaining = total; + setRingProgress(remaining, total); + }); + }; + if (window.requestIdleCallback) { + window.requestIdleCallback(kickoff, {timeout: 1200}); + } else { + window.setTimeout(kickoff, 0); + } + }; + + const joinMeeting = () => { + if (!state.joinable) { + Str.get_strings([ + {key: 'joindeniedtitle', component: 'meetletacbfree'}, + ]).then((strings) => { + const msg = (state.status === 'waitinghost' || state.status === 'checking') + ? (state.strJoinWaiting || state.statusMessage || '') + : (state.statusMessage || ''); + showGateModal(strings[0], msg); + return null; + }).catch(() => { + showGateModal('', state.statusMessage || ''); + }); + return; + } + const joinBtn = document.querySelector(SELECTORS.join); + if (joinBtn) { + joinBtn.classList.add('is-busy'); + } + setOverlay(true, 'meeting'); + Ajax.call([{ + methodname: 'mod_meetletacbfree_get_join_url', + args: {cmid: state.cmid, groupid: state.groupid}, + }])[0].then((response) => { + setOverlay(false); + if (joinBtn) { + joinBtn.classList.remove('is-busy'); + } + handleLaunchResponse(response); + return null; + }).catch((err) => { + setOverlay(false); + if (joinBtn) { + joinBtn.classList.remove('is-busy'); + } + handleLaunchResponse(null, err); + pollMeetingStatus(); + }); + }; + + const cfgEl = document.querySelector(SELECTORS.config); + if (!cfgEl) { + return; + } + let config = {}; + try { + config = JSON.parse(cfgEl.getAttribute('data-meetlet-config')); + } catch (e) { + return; + } + + state.cmid = config.cmid; + state.groupid = config.groupid || 0; + state.pollSeconds = parseInt(config.statusPollSeconds || '15', 10) || 15; + state.strJoinWaiting = config.strJoinWaiting || ''; + state.strJoiningTitle = config.strJoiningTitle || ''; + state.strJoiningText = config.strJoiningText || ''; + state.strRecordingTitle = config.strRecordingTitle || ''; + state.strRecordingText = config.strRecordingText || ''; + state.licenseUsable = config.licenseUsable !== false; + if (typeof config.joinable === 'boolean') { + state.joinable = config.joinable; + } + if (config.status) { + state.status = config.status; + } + if (config.statusMessage) { + state.statusMessage = config.statusMessage; + } + modalLabels = { + name: config.strAttendeeName || '', + login: config.strAttendeeLogin || '', + timein: config.strTimeIn || '', + timeout: config.strTimeOut || '', + empty: config.strNoAttendance || '', + }; + + const app = document.querySelector(SELECTORS.app); + if (!app) { + return; + } + + bindFloatTips(app); + window.addEventListener('scroll', () => { + if (floatTipTarget) { + hideFloatTip(); + } + }, true); + window.addEventListener('resize', hideFloatTip); + + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape') { + hideFloatTip(); + closeModal(); + } + }); + + window.addEventListener('pagehide', stopRecordingsPoll); + + app.querySelectorAll(SELECTORS.tab).forEach((btn) => { + btn.addEventListener('click', () => { + loadTab(btn.dataset.tab, false); + }); + }); + const joinBtn = app.querySelector(SELECTORS.join); + if (joinBtn) { + joinBtn.addEventListener('click', joinMeeting); + } + + startStatusTicker(); + loadTab('details', false); + } + + return { + init: init + }; +});