Fix variable name mismatch for organization in the Network Resolve class. #1291. Fix issue where Admin Field relations using custom code did not automatically include multiple fields as expected. #1293. Fix issue where Admin Field relations did not correctly apply concatenation/glue options. #1294. Refactor Compiler functions to improve maintainability and align with modern PHP coding best practices. Refactor the JCB Dashboard, introducing a modernized layout, improved usability, and full responsiveness across desktop, tablet, and mobile devices. Fix deprecated warnings triggered in the JCB CLI. Fix resolve PHP syntax errors and PHP 8.4 deprecations across codebase. Fix four fatal parse/compile errors. Fix implicit nullable parameter deprecations.
418 lines
11 KiB
JavaScript
418 lines
11 KiB
JavaScript
/**
|
|
* @package Joomla.Component.Builder
|
|
*
|
|
* @created 30th April, 2015
|
|
* @author Llewellyn van der Merwe <https://dev.vdm.io>
|
|
* @git Joomla Component Builder <https://git.vdm.dev/joomla/Component-Builder>
|
|
* @copyright Copyright (C) 2015 Vast Development Method. All rights reserved.
|
|
* @license GNU General Public License version 2 or later; see LICENSE.txt
|
|
*/
|
|
|
|
|
|
|
|
|
|
jQuery(document).ready(function()
|
|
{
|
|
// check and load all the customcode edit buttons
|
|
getEditCustomCodeButtons();
|
|
});
|
|
|
|
/**
|
|
* Track active requests per subform row so stale responses
|
|
* do not overwrite newer values.
|
|
*
|
|
* @type {Object.<string, AbortController>}
|
|
*/
|
|
var codeGlueOptionsControllers = {};
|
|
|
|
/**
|
|
* Check whether a value is set.
|
|
*
|
|
* @param {*} value
|
|
* @returns {boolean}
|
|
*/
|
|
function _isSet(value) {
|
|
return value !== undefined && value !== null && value !== '';
|
|
}
|
|
|
|
/**
|
|
* Get a DOM element by ID.
|
|
*
|
|
* @param {string} id
|
|
* @returns {HTMLElement|null}
|
|
*/
|
|
function getElement(id) {
|
|
return document.getElementById(id);
|
|
}
|
|
|
|
/**
|
|
* Get the subform row prefix from a field element.
|
|
*
|
|
* Expected field ID format:
|
|
* prefix__rowindex__fieldname
|
|
*
|
|
* @param {HTMLElement} field
|
|
* @returns {string}
|
|
*/
|
|
function getSubformRowId(field) {
|
|
if (!field || !field.id || typeof field.id !== 'string') {
|
|
return '';
|
|
}
|
|
|
|
var parts = field.id.split('__');
|
|
|
|
if (parts.length < 3) {
|
|
return '';
|
|
}
|
|
|
|
return parts[0] + '__' + parts[1];
|
|
}
|
|
|
|
/**
|
|
* Get a trimmed field value by ID.
|
|
*
|
|
* @param {string} id
|
|
* @returns {string}
|
|
*/
|
|
function getFieldValue(id) {
|
|
var element = getElement(id);
|
|
|
|
if (!element || element.value === undefined || element.value === null) {
|
|
return '';
|
|
}
|
|
|
|
return String(element.value).trim();
|
|
}
|
|
|
|
/**
|
|
* Extract selected values from a select element.
|
|
*
|
|
* For a multiple select, all selected values are returned.
|
|
* For a normal select, the selected value is returned as a one-item array.
|
|
*
|
|
* @param {HTMLSelectElement|null} selectElement
|
|
* @returns {string[]}
|
|
*/
|
|
function getSelectedValues(selectElement) {
|
|
if (!selectElement || !selectElement.selectedOptions) {
|
|
return [];
|
|
}
|
|
|
|
return Array.from(selectElement.selectedOptions, function(option) {
|
|
return String(option.value || '').trim();
|
|
}).filter(function(value) {
|
|
return value !== '';
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Convert selected join field values into the exact format
|
|
* expected by the PHP server method:
|
|
*
|
|
* - "guid1,guid2,guid3" when values exist
|
|
* - "none" when no values are selected
|
|
*
|
|
* @param {string[]} values
|
|
* @returns {string}
|
|
*/
|
|
function buildJoinfieldsString(values) {
|
|
if (!Array.isArray(values) || values.length === 0) {
|
|
return 'none';
|
|
}
|
|
|
|
return values.join(',');
|
|
}
|
|
|
|
/**
|
|
* Normalize the AJAX response into a string.
|
|
*
|
|
* Your PHP method returns a string or false.
|
|
* Depending on the Joomla AJAX wrapper, the JSON response may contain
|
|
* that value directly or inside a property like result/data/value.
|
|
*
|
|
* @param {*} result
|
|
* @returns {string}
|
|
*/
|
|
function normalizeCodeGlueResponse(result) {
|
|
if (result === false || result === null || result === undefined) {
|
|
return '';
|
|
}
|
|
|
|
if (typeof result === 'string') {
|
|
return result;
|
|
}
|
|
|
|
if (typeof result === 'object') {
|
|
if (_isSet(result.result)) {
|
|
return String(result.result);
|
|
}
|
|
|
|
if (_isSet(result.data)) {
|
|
return String(result.data);
|
|
}
|
|
|
|
if (_isSet(result.value)) {
|
|
return String(result.value);
|
|
}
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* Abort an in-flight request for a given subform row.
|
|
*
|
|
* @param {string} subID
|
|
* @returns {void}
|
|
*/
|
|
function abortCodeGlueOptionsRequest(subID) {
|
|
if (codeGlueOptionsControllers[subID]) {
|
|
codeGlueOptionsControllers[subID].abort();
|
|
delete codeGlueOptionsControllers[subID];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Triggered on subform field change.
|
|
* Reads sibling field values, fetches CodeGlue options,
|
|
* and writes the result into the set field.
|
|
*
|
|
* @param {HTMLElement} field
|
|
* @returns {void}
|
|
*/
|
|
function getCodeGlueOptions(field) {
|
|
var subID = getSubformRowId(field);
|
|
|
|
if (!subID) {
|
|
console.warn('getCodeGlueOptions: could not determine subform row ID.', field);
|
|
return;
|
|
}
|
|
|
|
var setField = getElement(subID + '__set');
|
|
|
|
if (!setField) {
|
|
console.warn('getCodeGlueOptions: target "set" field not found for row:', subID);
|
|
return;
|
|
}
|
|
|
|
var listfield = getFieldValue(subID + '__listfield');
|
|
var type = getFieldValue(subID + '__join_type');
|
|
var area = getFieldValue(subID + '__area');
|
|
var joinfieldsSelect = getElement(subID + '__joinfields');
|
|
var joinfields = buildJoinfieldsString(getSelectedValues(joinfieldsSelect));
|
|
|
|
if (!_isSet(listfield) || !_isSet(type) || !_isSet(area)) {
|
|
abortCodeGlueOptionsRequest(subID);
|
|
setField.value = '';
|
|
return;
|
|
}
|
|
|
|
getCodeGlueOptions_server(subID, listfield, joinfields, type, area)
|
|
.then(function(result) {
|
|
setField.value = normalizeCodeGlueResponse(result);
|
|
})
|
|
.catch(function(error) {
|
|
if (error && error.name === 'AbortError') {
|
|
return;
|
|
}
|
|
|
|
console.error('getCodeGlueOptions failed:', error);
|
|
setField.value = '';
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Fetch CodeGlue options from the server.
|
|
*
|
|
* The PHP side expects:
|
|
* - joinfields = "guid1,guid2,guid3"
|
|
* - or joinfields = "none"
|
|
*
|
|
* @param {string} subID
|
|
* @param {string} listfield
|
|
* @param {string} joinfields
|
|
* @param {string} type
|
|
* @param {string} area
|
|
* @returns {Promise<*>}
|
|
*/
|
|
function getCodeGlueOptions_server(subID, listfield, joinfields, type, area) {
|
|
if (
|
|
typeof token === 'undefined' ||
|
|
!_isSet(token) ||
|
|
!_isSet(listfield) ||
|
|
!_isSet(type) ||
|
|
!_isSet(area)
|
|
) {
|
|
return Promise.resolve(null);
|
|
}
|
|
|
|
var getUrl = JRouter('index.php?option=com_componentbuilder&task=ajax.getCodeGlueOptions&format=json');
|
|
var params = new URLSearchParams();
|
|
|
|
params.append(token, '1');
|
|
params.append('listfield', listfield);
|
|
params.append('joinfields', _isSet(joinfields) ? joinfields : 'none');
|
|
params.append('type', type);
|
|
params.append('area', area);
|
|
|
|
abortCodeGlueOptionsRequest(subID);
|
|
|
|
var controller = new AbortController();
|
|
codeGlueOptionsControllers[subID] = controller;
|
|
|
|
return fetch(getUrl + '&' + params.toString(), {
|
|
method: 'GET',
|
|
headers: {
|
|
'Accept': 'application/json',
|
|
'X-Requested-With': 'XMLHttpRequest'
|
|
},
|
|
credentials: 'same-origin',
|
|
signal: controller.signal
|
|
})
|
|
.then(function(response) {
|
|
if (!response.ok) {
|
|
throw new Error('Server responded with status ' + response.status);
|
|
}
|
|
|
|
return response.json();
|
|
})
|
|
.finally(function() {
|
|
if (codeGlueOptionsControllers[subID] === controller) {
|
|
delete codeGlueOptionsControllers[subID];
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Retrieve the Edit Custom Code buttons from the server.
|
|
*
|
|
* @param {number} id The record ID to load custom code buttons for.
|
|
*
|
|
* @return {Promise<object|null>} Returns JSON object of buttons or null on failure.
|
|
* @since 3.1.3
|
|
*/
|
|
async function getEditCustomCodeButtons_server(id) {
|
|
try {
|
|
// --- Validation ---
|
|
if (typeof token !== 'string' || !token.trim()) {
|
|
console.error('[getEditCustomCodeButtons_server] Missing or invalid CSRF token.');
|
|
return null;
|
|
}
|
|
if (typeof id !== 'number' || id <= 0) {
|
|
console.error('[getEditCustomCodeButtons_server] Invalid ID provided:', id);
|
|
return null;
|
|
}
|
|
if (typeof return_here !== 'string' || !return_here.trim()) {
|
|
console.warn('[getEditCustomCodeButtons_server] "return_here" not set; continuing without it.');
|
|
}
|
|
|
|
// --- Build URL safely ---
|
|
const baseUrl = 'index.php';
|
|
const params = new URLSearchParams({
|
|
option: 'com_componentbuilder',
|
|
task: 'ajax.getEditCustomCodeButtons',
|
|
format: 'json',
|
|
raw: 'true',
|
|
[token]: '1',
|
|
id: id,
|
|
return_here: return_here || ''
|
|
});
|
|
if (typeof vastDevMod === 'string' && vastDevMod.length > 0) {
|
|
params.append('vdm', vastDevMod);
|
|
}
|
|
|
|
const urlWithParams = JRouter(`${baseUrl}?${params.toString()}`);
|
|
|
|
// --- Execute request ---
|
|
const response = await fetch(urlWithParams, {
|
|
method: 'GET',
|
|
headers: {
|
|
'Accept': 'application/json',
|
|
'Content-Type': 'application/json'
|
|
},
|
|
cache: 'no-store',
|
|
credentials: 'same-origin'
|
|
});
|
|
|
|
// --- Handle network errors ---
|
|
if (!response.ok) {
|
|
console.error(`[getEditCustomCodeButtons_server] HTTP ${response.status}: ${response.statusText}`);
|
|
return null;
|
|
}
|
|
|
|
// --- Parse JSON result ---
|
|
const data = await response.json();
|
|
return data ?? null;
|
|
|
|
} catch (error) {
|
|
console.error('[getEditCustomCodeButtons_server] Fetch failed:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Load and inject Edit Custom Code buttons into the DOM.
|
|
*
|
|
* @return {Promise<void>}
|
|
* @since 3.1.3
|
|
*/
|
|
async function getEditCustomCodeButtons() {
|
|
try {
|
|
// --- Get record ID from the form ---
|
|
const idField = document.querySelector('#jform_id');
|
|
if (!idField) {
|
|
console.error('[getEditCustomCodeButtons] #jform_id not found.');
|
|
return;
|
|
}
|
|
|
|
const idValue = parseInt(idField.value, 10);
|
|
if (isNaN(idValue) || idValue <= 0) {
|
|
console.warn('[getEditCustomCodeButtons] Invalid or empty ID; skipping button load.');
|
|
return;
|
|
}
|
|
|
|
// --- Request data from server ---
|
|
const result = await getEditCustomCodeButtons_server(idValue);
|
|
if (!result || typeof result !== 'object') {
|
|
console.warn('[getEditCustomCodeButtons] No result returned or invalid format.');
|
|
return;
|
|
}
|
|
|
|
// --- Inject returned button groups ---
|
|
Object.entries(result).forEach(([field, buttons]) => {
|
|
// Create the container div
|
|
const div = document.createElement('div');
|
|
div.className = 'control-group';
|
|
div.innerHTML = `
|
|
<div class="control-label">
|
|
<label>Add/Edit Customcode</label>
|
|
</div>
|
|
<div class="controls control-customcode-buttons-${field}"></div>
|
|
`;
|
|
|
|
// Find where to insert (before .control-wrapper-{field})
|
|
const insertBeforeElement = document.querySelector(`.control-wrapper-${field}`);
|
|
if (insertBeforeElement && insertBeforeElement.parentNode) {
|
|
insertBeforeElement.parentNode.insertBefore(div, insertBeforeElement);
|
|
}
|
|
|
|
// Append buttons to the new container
|
|
const controlsDiv = div.querySelector(`.control-customcode-buttons-${field}`);
|
|
if (controlsDiv && typeof buttons === 'object') {
|
|
Object.entries(buttons).forEach(([name, buttonHtml]) => {
|
|
if (typeof buttonHtml === 'string') {
|
|
const wrapper = document.createElement('div');
|
|
wrapper.innerHTML = buttonHtml.trim();
|
|
const buttonNode = wrapper.firstElementChild;
|
|
if (buttonNode) {
|
|
controlsDiv.appendChild(buttonNode);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('[getEditCustomCodeButtons] Error rendering buttons:', error);
|
|
}
|
|
}
|