Files
Component-Builder/media/js/admin_fields_relations.js
T
2026-03-24 13:37:04 +00:00

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