Stability update addressing a post-release field issue. #1262. Fix a security vulnerability in custom code redirect URL validation. Add an alert to the Compiler view when no Components exist. #1263.
1860 lines
58 KiB
JavaScript
1860 lines
58 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
|
|
*/
|
|
|
|
// Some Global Values
|
|
jform_vvvvwbgvws_required = false;
|
|
jform_vvvvwbhvwt_required = false;
|
|
jform_vvvvwbivwu_required = false;
|
|
jform_vvvvwbjvwv_required = false;
|
|
jform_vvvvwbmvww_required = false;
|
|
jform_vvvvwbmvwx_required = false;
|
|
|
|
// Initial Script
|
|
document.addEventListener('DOMContentLoaded', function()
|
|
{
|
|
var datalenght_vvvvwbg = jQuery("#jform_datalenght").val();
|
|
vvvvwbg(datalenght_vvvvwbg);
|
|
|
|
var datadefault_vvvvwbh = jQuery("#jform_datadefault").val();
|
|
vvvvwbh(datadefault_vvvvwbh);
|
|
|
|
var datatype_vvvvwbi = jQuery("#jform_datatype").val();
|
|
vvvvwbi(datatype_vvvvwbi);
|
|
|
|
var datatype_vvvvwbj = jQuery("#jform_datatype").val();
|
|
vvvvwbj(datatype_vvvvwbj);
|
|
|
|
var store_vvvvwbm = jQuery("#jform_store").val();
|
|
vvvvwbm(store_vvvvwbm);
|
|
|
|
var add_css_view_vvvvwbn = jQuery("#jform_add_css_view input[type='radio']:checked").val();
|
|
vvvvwbn(add_css_view_vvvvwbn);
|
|
|
|
var add_css_views_vvvvwbo = jQuery("#jform_add_css_views input[type='radio']:checked").val();
|
|
vvvvwbo(add_css_views_vvvvwbo);
|
|
|
|
var add_javascript_view_footer_vvvvwbp = jQuery("#jform_add_javascript_view_footer input[type='radio']:checked").val();
|
|
vvvvwbp(add_javascript_view_footer_vvvvwbp);
|
|
|
|
var add_javascript_views_footer_vvvvwbq = jQuery("#jform_add_javascript_views_footer input[type='radio']:checked").val();
|
|
vvvvwbq(add_javascript_views_footer_vvvvwbq);
|
|
});
|
|
|
|
// the vvvvwbg function
|
|
function vvvvwbg(datalenght_vvvvwbg)
|
|
{
|
|
if (isSet(datalenght_vvvvwbg) && datalenght_vvvvwbg.constructor !== Array)
|
|
{
|
|
var temp_vvvvwbg = datalenght_vvvvwbg;
|
|
var datalenght_vvvvwbg = [];
|
|
datalenght_vvvvwbg.push(temp_vvvvwbg);
|
|
}
|
|
else if (!isSet(datalenght_vvvvwbg))
|
|
{
|
|
var datalenght_vvvvwbg = [];
|
|
}
|
|
var datalenght = datalenght_vvvvwbg.some(datalenght_vvvvwbg_SomeFunc);
|
|
|
|
|
|
// set this function logic
|
|
if (datalenght)
|
|
{
|
|
jQuery('#jform_datalenght_other').closest('.control-group').show();
|
|
// add required attribute to datalenght_other field
|
|
if (jform_vvvvwbgvws_required)
|
|
{
|
|
updateFieldRequired('datalenght_other',0);
|
|
jQuery('#jform_datalenght_other').prop('required','required');
|
|
jQuery('#jform_datalenght_other').attr('aria-required',true);
|
|
jQuery('#jform_datalenght_other').addClass('required');
|
|
jform_vvvvwbgvws_required = false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
jQuery('#jform_datalenght_other').closest('.control-group').hide();
|
|
// remove required attribute from datalenght_other field
|
|
if (!jform_vvvvwbgvws_required)
|
|
{
|
|
updateFieldRequired('datalenght_other',1);
|
|
jQuery('#jform_datalenght_other').removeAttr('required');
|
|
jQuery('#jform_datalenght_other').removeAttr('aria-required');
|
|
jQuery('#jform_datalenght_other').removeClass('required');
|
|
jform_vvvvwbgvws_required = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// the vvvvwbg Some function
|
|
function datalenght_vvvvwbg_SomeFunc(datalenght_vvvvwbg)
|
|
{
|
|
// set the function logic
|
|
if (datalenght_vvvvwbg == 'Other')
|
|
{
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// the vvvvwbh function
|
|
function vvvvwbh(datadefault_vvvvwbh)
|
|
{
|
|
if (isSet(datadefault_vvvvwbh) && datadefault_vvvvwbh.constructor !== Array)
|
|
{
|
|
var temp_vvvvwbh = datadefault_vvvvwbh;
|
|
var datadefault_vvvvwbh = [];
|
|
datadefault_vvvvwbh.push(temp_vvvvwbh);
|
|
}
|
|
else if (!isSet(datadefault_vvvvwbh))
|
|
{
|
|
var datadefault_vvvvwbh = [];
|
|
}
|
|
var datadefault = datadefault_vvvvwbh.some(datadefault_vvvvwbh_SomeFunc);
|
|
|
|
|
|
// set this function logic
|
|
if (datadefault)
|
|
{
|
|
jQuery('#jform_datadefault_other').closest('.control-group').show();
|
|
// add required attribute to datadefault_other field
|
|
if (jform_vvvvwbhvwt_required)
|
|
{
|
|
updateFieldRequired('datadefault_other',0);
|
|
jQuery('#jform_datadefault_other').prop('required','required');
|
|
jQuery('#jform_datadefault_other').attr('aria-required',true);
|
|
jQuery('#jform_datadefault_other').addClass('required');
|
|
jform_vvvvwbhvwt_required = false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
jQuery('#jform_datadefault_other').closest('.control-group').hide();
|
|
// remove required attribute from datadefault_other field
|
|
if (!jform_vvvvwbhvwt_required)
|
|
{
|
|
updateFieldRequired('datadefault_other',1);
|
|
jQuery('#jform_datadefault_other').removeAttr('required');
|
|
jQuery('#jform_datadefault_other').removeAttr('aria-required');
|
|
jQuery('#jform_datadefault_other').removeClass('required');
|
|
jform_vvvvwbhvwt_required = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// the vvvvwbh Some function
|
|
function datadefault_vvvvwbh_SomeFunc(datadefault_vvvvwbh)
|
|
{
|
|
// set the function logic
|
|
if (datadefault_vvvvwbh == 'Other')
|
|
{
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// the vvvvwbi function
|
|
function vvvvwbi(datatype_vvvvwbi)
|
|
{
|
|
if (isSet(datatype_vvvvwbi) && datatype_vvvvwbi.constructor !== Array)
|
|
{
|
|
var temp_vvvvwbi = datatype_vvvvwbi;
|
|
var datatype_vvvvwbi = [];
|
|
datatype_vvvvwbi.push(temp_vvvvwbi);
|
|
}
|
|
else if (!isSet(datatype_vvvvwbi))
|
|
{
|
|
var datatype_vvvvwbi = [];
|
|
}
|
|
var datatype = datatype_vvvvwbi.some(datatype_vvvvwbi_SomeFunc);
|
|
|
|
|
|
// set this function logic
|
|
if (datatype)
|
|
{
|
|
jQuery('#jform_datadefault').closest('.control-group').show();
|
|
jQuery('#jform_indexes').closest('.control-group').show();
|
|
// add required attribute to indexes field
|
|
if (jform_vvvvwbivwu_required)
|
|
{
|
|
updateFieldRequired('indexes',0);
|
|
jQuery('#jform_indexes').prop('required','required');
|
|
jQuery('#jform_indexes').attr('aria-required',true);
|
|
jQuery('#jform_indexes').addClass('required');
|
|
jform_vvvvwbivwu_required = false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
jQuery('#jform_datadefault').closest('.control-group').hide();
|
|
jQuery('#jform_indexes').closest('.control-group').hide();
|
|
// remove required attribute from indexes field
|
|
if (!jform_vvvvwbivwu_required)
|
|
{
|
|
updateFieldRequired('indexes',1);
|
|
jQuery('#jform_indexes').removeAttr('required');
|
|
jQuery('#jform_indexes').removeAttr('aria-required');
|
|
jQuery('#jform_indexes').removeClass('required');
|
|
jform_vvvvwbivwu_required = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// the vvvvwbi Some function
|
|
function datatype_vvvvwbi_SomeFunc(datatype_vvvvwbi)
|
|
{
|
|
// set the function logic
|
|
if (datatype_vvvvwbi == 'CHAR' || datatype_vvvvwbi == 'VARCHAR' || datatype_vvvvwbi == 'DATETIME' || datatype_vvvvwbi == 'DATE' || datatype_vvvvwbi == 'TIME' || datatype_vvvvwbi == 'INT' || datatype_vvvvwbi == 'TINYINT' || datatype_vvvvwbi == 'BIGINT' || datatype_vvvvwbi == 'FLOAT' || datatype_vvvvwbi == 'DECIMAL' || datatype_vvvvwbi == 'DOUBLE')
|
|
{
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// the vvvvwbj function
|
|
function vvvvwbj(datatype_vvvvwbj)
|
|
{
|
|
if (isSet(datatype_vvvvwbj) && datatype_vvvvwbj.constructor !== Array)
|
|
{
|
|
var temp_vvvvwbj = datatype_vvvvwbj;
|
|
var datatype_vvvvwbj = [];
|
|
datatype_vvvvwbj.push(temp_vvvvwbj);
|
|
}
|
|
else if (!isSet(datatype_vvvvwbj))
|
|
{
|
|
var datatype_vvvvwbj = [];
|
|
}
|
|
var datatype = datatype_vvvvwbj.some(datatype_vvvvwbj_SomeFunc);
|
|
|
|
|
|
// set this function logic
|
|
if (datatype)
|
|
{
|
|
jQuery('#jform_datalenght').closest('.control-group').show();
|
|
// add required attribute to datalenght field
|
|
if (jform_vvvvwbjvwv_required)
|
|
{
|
|
updateFieldRequired('datalenght',0);
|
|
jQuery('#jform_datalenght').prop('required','required');
|
|
jQuery('#jform_datalenght').attr('aria-required',true);
|
|
jQuery('#jform_datalenght').addClass('required');
|
|
jform_vvvvwbjvwv_required = false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
jQuery('#jform_datalenght').closest('.control-group').hide();
|
|
// remove required attribute from datalenght field
|
|
if (!jform_vvvvwbjvwv_required)
|
|
{
|
|
updateFieldRequired('datalenght',1);
|
|
jQuery('#jform_datalenght').removeAttr('required');
|
|
jQuery('#jform_datalenght').removeAttr('aria-required');
|
|
jQuery('#jform_datalenght').removeClass('required');
|
|
jform_vvvvwbjvwv_required = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// the vvvvwbj Some function
|
|
function datatype_vvvvwbj_SomeFunc(datatype_vvvvwbj)
|
|
{
|
|
// set the function logic
|
|
if (datatype_vvvvwbj == 'CHAR' || datatype_vvvvwbj == 'VARCHAR' || datatype_vvvvwbj == 'INT' || datatype_vvvvwbj == 'TINYINT' || datatype_vvvvwbj == 'BIGINT' || datatype_vvvvwbj == 'FLOAT' || datatype_vvvvwbj == 'DECIMAL' || datatype_vvvvwbj == 'DOUBLE')
|
|
{
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// the vvvvwbm function
|
|
function vvvvwbm(store_vvvvwbm)
|
|
{
|
|
if (isSet(store_vvvvwbm) && store_vvvvwbm.constructor !== Array)
|
|
{
|
|
var temp_vvvvwbm = store_vvvvwbm;
|
|
var store_vvvvwbm = [];
|
|
store_vvvvwbm.push(temp_vvvvwbm);
|
|
}
|
|
else if (!isSet(store_vvvvwbm))
|
|
{
|
|
var store_vvvvwbm = [];
|
|
}
|
|
var store = store_vvvvwbm.some(store_vvvvwbm_SomeFunc);
|
|
|
|
|
|
// set this function logic
|
|
if (store)
|
|
{
|
|
jQuery('#jform_initiator_on_get_model').closest('.control-group').show();
|
|
jQuery('#jform_initiator_on_save_model').closest('.control-group').show();
|
|
jQuery('.note_expert_field_save_mode').closest('.control-group').show();
|
|
jQuery('#jform_on_get_model_field').closest('.control-group').show();
|
|
// add required attribute to on_get_model_field field
|
|
if (jform_vvvvwbmvww_required)
|
|
{
|
|
updateFieldRequired('on_get_model_field',0);
|
|
jQuery('#jform_on_get_model_field').prop('required','required');
|
|
jQuery('#jform_on_get_model_field').attr('aria-required',true);
|
|
jQuery('#jform_on_get_model_field').addClass('required');
|
|
jform_vvvvwbmvww_required = false;
|
|
}
|
|
jQuery('#jform_on_save_model_field').closest('.control-group').show();
|
|
// add required attribute to on_save_model_field field
|
|
if (jform_vvvvwbmvwx_required)
|
|
{
|
|
updateFieldRequired('on_save_model_field',0);
|
|
jQuery('#jform_on_save_model_field').prop('required','required');
|
|
jQuery('#jform_on_save_model_field').attr('aria-required',true);
|
|
jQuery('#jform_on_save_model_field').addClass('required');
|
|
jform_vvvvwbmvwx_required = false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
jQuery('#jform_initiator_on_get_model').closest('.control-group').hide();
|
|
jQuery('#jform_initiator_on_save_model').closest('.control-group').hide();
|
|
jQuery('.note_expert_field_save_mode').closest('.control-group').hide();
|
|
jQuery('#jform_on_get_model_field').closest('.control-group').hide();
|
|
// remove required attribute from on_get_model_field field
|
|
if (!jform_vvvvwbmvww_required)
|
|
{
|
|
updateFieldRequired('on_get_model_field',1);
|
|
jQuery('#jform_on_get_model_field').removeAttr('required');
|
|
jQuery('#jform_on_get_model_field').removeAttr('aria-required');
|
|
jQuery('#jform_on_get_model_field').removeClass('required');
|
|
jform_vvvvwbmvww_required = true;
|
|
}
|
|
jQuery('#jform_on_save_model_field').closest('.control-group').hide();
|
|
// remove required attribute from on_save_model_field field
|
|
if (!jform_vvvvwbmvwx_required)
|
|
{
|
|
updateFieldRequired('on_save_model_field',1);
|
|
jQuery('#jform_on_save_model_field').removeAttr('required');
|
|
jQuery('#jform_on_save_model_field').removeAttr('aria-required');
|
|
jQuery('#jform_on_save_model_field').removeClass('required');
|
|
jform_vvvvwbmvwx_required = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// the vvvvwbm Some function
|
|
function store_vvvvwbm_SomeFunc(store_vvvvwbm)
|
|
{
|
|
// set the function logic
|
|
if (store_vvvvwbm == 6)
|
|
{
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// the vvvvwbn function
|
|
function vvvvwbn(add_css_view_vvvvwbn)
|
|
{
|
|
// set the function logic
|
|
if (add_css_view_vvvvwbn == 1)
|
|
{
|
|
jQuery('#jform_css_view-lbl').closest('.control-group').show();
|
|
}
|
|
else
|
|
{
|
|
jQuery('#jform_css_view-lbl').closest('.control-group').hide();
|
|
}
|
|
}
|
|
|
|
// the vvvvwbo function
|
|
function vvvvwbo(add_css_views_vvvvwbo)
|
|
{
|
|
// set the function logic
|
|
if (add_css_views_vvvvwbo == 1)
|
|
{
|
|
jQuery('#jform_css_views-lbl').closest('.control-group').show();
|
|
}
|
|
else
|
|
{
|
|
jQuery('#jform_css_views-lbl').closest('.control-group').hide();
|
|
}
|
|
}
|
|
|
|
// the vvvvwbp function
|
|
function vvvvwbp(add_javascript_view_footer_vvvvwbp)
|
|
{
|
|
// set the function logic
|
|
if (add_javascript_view_footer_vvvvwbp == 1)
|
|
{
|
|
jQuery('#jform_javascript_view_footer-lbl').closest('.control-group').show();
|
|
}
|
|
else
|
|
{
|
|
jQuery('#jform_javascript_view_footer-lbl').closest('.control-group').hide();
|
|
}
|
|
}
|
|
|
|
// the vvvvwbq function
|
|
function vvvvwbq(add_javascript_views_footer_vvvvwbq)
|
|
{
|
|
// set the function logic
|
|
if (add_javascript_views_footer_vvvvwbq == 1)
|
|
{
|
|
jQuery('#jform_javascript_views_footer-lbl').closest('.control-group').show();
|
|
}
|
|
else
|
|
{
|
|
jQuery('#jform_javascript_views_footer-lbl').closest('.control-group').hide();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update the "not required" field list by adding or removing a field name.
|
|
*
|
|
* Mirrors the original jQuery logic exactly but uses pure JavaScript.
|
|
*
|
|
* @param {string} name The field name to add or remove.
|
|
* @param {number} status 1 to add as not required, 0 to remove.
|
|
*
|
|
* @return {void}
|
|
* @since 3.1.3
|
|
*/
|
|
function updateFieldRequired(name, status) {
|
|
// Check if #jform_not_required exists
|
|
const notRequiredField = document.getElementById('jform_not_required');
|
|
if (!notRequiredField) {
|
|
return;
|
|
}
|
|
|
|
// Split the comma-separated list into an array
|
|
let not_required = notRequiredField.value ? notRequiredField.value.split(',') : [];
|
|
|
|
// Add or remove the field name from the list
|
|
if (status == 1) {
|
|
not_required.push(name);
|
|
} else {
|
|
not_required = removeFieldFromNotRequired(not_required, name);
|
|
}
|
|
|
|
// Clean and deduplicate the list
|
|
const fixedList = fixNotRequiredArray(not_required);
|
|
|
|
// Write back the updated comma-separated list
|
|
notRequiredField.value = fixedList.toString();
|
|
}
|
|
|
|
/**
|
|
* Remove a specific field name from the "not required" array.
|
|
*
|
|
* @param {Array<string>} array The list of not-required field names.
|
|
* @param {string} what The field name to remove.
|
|
*
|
|
* @return {Array<string>} The updated array.
|
|
* @since 3.1.3
|
|
*/
|
|
function removeFieldFromNotRequired(array, what) {
|
|
return array.filter(function (element) {
|
|
return element !== what;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Deduplicate and clean a "not required" array.
|
|
*
|
|
* @param {Array<string>} array The array to fix.
|
|
*
|
|
* @return {Array<string>} A cleaned, unique array.
|
|
* @since 3.1.3
|
|
*/
|
|
function fixNotRequiredArray(array) {
|
|
const seen = {};
|
|
return removeEmptyFromNotRequiredArray(array).filter(function (item) {
|
|
return seen.hasOwnProperty(item) ? false : (seen[item] = true);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Remove empty or invalid entries from a "not required" array.
|
|
*
|
|
* Also removes the literal '一_一' token (legacy quirk preserved for compatibility).
|
|
*
|
|
* @param {Array<string>} array The array to process.
|
|
*
|
|
* @return {Array<string>} The cleaned array.
|
|
* @since 3.1.3
|
|
*/
|
|
function removeEmptyFromNotRequiredArray(array) {
|
|
return array.filter(function (el) {
|
|
return el && el.length > 0 && el !== '一_一';
|
|
});
|
|
}
|
|
|
|
// the isSet function
|
|
function isSet(val)
|
|
{
|
|
if ((val != undefined) && (val != null) && 0 !== val.length){
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
|
|
// set properties the options
|
|
var propertiesArray = {};
|
|
var propertyIdRemoved;
|
|
|
|
// the options row id key
|
|
var rowIdKey = 'properties';
|
|
|
|
/**
|
|
* Initialize field property and validation UI logic after DOM is ready.
|
|
*
|
|
* This listener replaces jQuery(document).ready() with a pure JavaScript equivalent.
|
|
* It initializes field-type properties, linked fields, validation rules,
|
|
* and dynamic UI behaviors immediately after Joomla's form is rendered.
|
|
*
|
|
* @return {void}
|
|
* @since 3.1.3
|
|
*/
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
try {
|
|
// --- Get the current field type value and text ---
|
|
const fieldTypeSelect = document.querySelector('#jform_fieldtype');
|
|
const fieldtype = fieldTypeSelect?.value ?? '';
|
|
const fieldText = fieldTypeSelect?.options[fieldTypeSelect.selectedIndex]?.text?.toLowerCase() ?? '';
|
|
|
|
// --- Load field type properties dynamically ---
|
|
if (typeof getFieldTypeProperties === 'function') {
|
|
getFieldTypeProperties(fieldtype, false);
|
|
}
|
|
|
|
// --- Load linked details if available ---
|
|
if (typeof getLinked === 'function') {
|
|
getLinked();
|
|
}
|
|
|
|
// --- Load and render validation rules table ---
|
|
if (typeof getValidationRulesTable === 'function') {
|
|
getValidationRulesTable();
|
|
}
|
|
|
|
// --- Initialize add button for validation rules ---
|
|
if (typeof addButton === 'function') {
|
|
addButton('validation_rule', 'validation_rules_header', 2);
|
|
}
|
|
|
|
// --- Check if database input fields should be shown or hidden ---
|
|
if (typeof dbChecker === 'function') {
|
|
dbChecker(fieldText);
|
|
}
|
|
|
|
// --- Load and prepare edit custom code buttons ---
|
|
if (typeof getEditCustomCodeButtons === 'function') {
|
|
getEditCustomCodeButtons();
|
|
}
|
|
|
|
console.debug('[Init] Field property/validation logic initialized successfully.');
|
|
} catch (error) {
|
|
console.error('[Init] Initialization error:', error);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Handle field property selection and dynamically update related description/value fields.
|
|
*
|
|
* This function checks for duplicate property selections, resets invalid ones,
|
|
* updates dependent fields, and fetches the property description and value
|
|
* from the server via getFieldPropertyDesc_server().
|
|
*
|
|
* @param {HTMLElement} field The select field element triggering the change.
|
|
* @param {string} targetForm The form context ("properties" or another form name).
|
|
*
|
|
* @return {Promise<void>} Resolves when updates and server fetch are complete.
|
|
* @since 3.1.3
|
|
*/
|
|
async function getFieldPropertyDesc(field, targetForm) {
|
|
if (!field) {
|
|
console.debug('[getFieldPropertyDesc] Missing field element.');
|
|
return;
|
|
}
|
|
|
|
// Get the ID and property value
|
|
const id = field.id;
|
|
const property = field.value ?? '';
|
|
|
|
// Split the ID into parts (e.g. field__desc)
|
|
const target = id.split('__');
|
|
|
|
// Check for duplicate properties
|
|
if (typeof propertyIsSet === 'function' && propertyIsSet(property, id, targetForm)) {
|
|
// Reset the selection
|
|
unselectChoicesFieldValue(`#${id}`);
|
|
|
|
// Show Joomla dialog warning and auto-close
|
|
const message = Joomla?.Text?._('Property already selected, try another.')
|
|
|| 'Property already selected, try another.';
|
|
|
|
// Ensure Joomla renderMessages API exists
|
|
if (typeof Joomla !== 'undefined' && typeof Joomla.renderMessages === 'function') {
|
|
Joomla.renderMessages({
|
|
warning: [message]
|
|
});
|
|
// Auto-scroll to top so message is visible
|
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
}
|
|
// Final fallback to console
|
|
else {
|
|
console.warn(message);
|
|
}
|
|
|
|
// Reset dependent description/value fields
|
|
const descField = document.querySelector(`#${target[0]}__desc`);
|
|
const valueField = document.querySelector(`#${target[0]}__value`);
|
|
if (descField) descField.value = '';
|
|
if (valueField) valueField.value = '';
|
|
|
|
return;
|
|
}
|
|
|
|
// Trigger property refresh logic
|
|
if (typeof propertyDynamicSet === 'function') {
|
|
propertyDynamicSet();
|
|
}
|
|
|
|
// Determine field type (context: 'properties' or 'extra')
|
|
let fieldtype = 'extra';
|
|
if (targetForm === 'properties') {
|
|
const fieldTypeSelect = document.querySelector('#jform_fieldtype');
|
|
if (fieldTypeSelect) {
|
|
fieldtype = fieldTypeSelect.value ?? 'extra';
|
|
}
|
|
}
|
|
|
|
// Fetch property description and value from the server
|
|
const result = await getFieldPropertyDesc_server(fieldtype, property);
|
|
|
|
// Update dependent fields based on the result
|
|
const descField = document.querySelector(`#${target[0]}__desc`);
|
|
const valueField = document.querySelector(`#${target[0]}__value`);
|
|
|
|
if (result && (result.desc || result.value)) {
|
|
if (descField) descField.value = result.desc ?? '';
|
|
if (valueField) valueField.value = result.value ?? '';
|
|
} else {
|
|
if (descField) descField.value = Joomla.Text._('COM_COMPONENTBUILDER_SELECT_A_PROPERTY');
|
|
if (valueField) valueField.value = '';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check whether a given property has already been selected in another field.
|
|
*
|
|
* This function iterates through all property select fields (up to 70 by default)
|
|
* within the given target form, comparing their current values against the provided
|
|
* property value. If a duplicate is found, it returns true; otherwise false.
|
|
*
|
|
* @param {string} prop The property value to check for duplicates.
|
|
* @param {string} id The current field's ID to exclude from the check.
|
|
* @param {string} targetForm The name prefix of the form group (e.g., "properties").
|
|
*
|
|
* @return {boolean} True if the property is already selected elsewhere, otherwise false.
|
|
* @since 3.1.3
|
|
*/
|
|
function propertyIsSet(prop, id, targetForm) {
|
|
// Validate input
|
|
if (!prop || !id || !targetForm) {
|
|
console.debug('[propertyIsSet] Missing required parameters:', { prop, id, targetForm });
|
|
return false;
|
|
}
|
|
|
|
// Loop through possible fields (max 70 rows by convention)
|
|
for (let i = 0; i < 70; i++) {
|
|
// Construct the expected field ID pattern
|
|
const id_check = `${targetForm}_${targetForm}${i}__name`;
|
|
const field = document.getElementById(id_check);
|
|
|
|
// Skip if not found or if this is the same field being checked
|
|
if (!field || id_check === id) {
|
|
continue;
|
|
}
|
|
|
|
// Get the currently selected value
|
|
const selectedOption = field.options[field.selectedIndex];
|
|
const tmp = selectedOption ? selectedOption.value : '';
|
|
|
|
// If the same property is already selected, return true
|
|
if (tmp === prop) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// No duplicates found
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Load and initialize the field type properties dynamically.
|
|
*
|
|
* This method retrieves subform and database property information for a given
|
|
* field type, updates the Joomla form dynamically, and initializes new form elements.
|
|
* It reproduces the full behavior of the legacy jQuery implementation using pure JavaScript.
|
|
*
|
|
* @param {string|number} fieldtype The field type identifier or ID.
|
|
* @param {boolean} db Whether to load database defaults if present.
|
|
*
|
|
* @return {void}
|
|
* @since 3.1.3
|
|
*/
|
|
function getFieldTypeProperties(fieldtype, db) {
|
|
getCodeFrom_server(fieldtype, 'type', 'type', 'fieldTypeProperties')
|
|
.then(function (result) {
|
|
// Ensure a valid response with subform data
|
|
if (!result || !result.subform) {
|
|
return;
|
|
}
|
|
|
|
// Store the property options list globally
|
|
propertiesArray = result.nameListOptions;
|
|
|
|
// --- Remove any previous dynamically added property forms ---
|
|
document.querySelectorAll('.prop_removal').forEach(el => el.remove());
|
|
|
|
// --- Remove "select field type" notices if they exist ---
|
|
document.querySelectorAll('.note_select_field_type').forEach(el => {
|
|
const group = el.closest('.control-group');
|
|
if (group) group.remove();
|
|
});
|
|
|
|
// --- Locate the control group for filter information ---
|
|
const noteInfo = document.querySelector('.note_filter_information');
|
|
const targetGroup = noteInfo ? noteInfo.closest('.control-group') : null;
|
|
|
|
// --- Prepend the new content in the correct order ---
|
|
if (targetGroup) {
|
|
// Prepend the "extra" HTML block
|
|
if (result.extra) {
|
|
targetGroup.insertAdjacentHTML('afterbegin', result.extra);
|
|
}
|
|
|
|
// Prepend any textarea fields returned by the AJAX response
|
|
if (result.textarea && Array.isArray(result.textarea)) {
|
|
for (const tField of result.textarea) {
|
|
targetGroup.insertAdjacentHTML('afterbegin', tField);
|
|
}
|
|
}
|
|
|
|
// Finally, prepend the subform structure itself
|
|
targetGroup.insertAdjacentHTML('afterbegin', result.subform);
|
|
}
|
|
|
|
// --- Initialize row watcher if available ---
|
|
if (typeof rowWatcher === 'function') {
|
|
rowWatcher();
|
|
}
|
|
|
|
// --- Trigger "update" events on all subform-repeatable elements ---
|
|
document.querySelectorAll('div.subform-repeatable').forEach(el => {
|
|
el.dispatchEvent(new Event('update', { bubbles: true }));
|
|
});
|
|
|
|
// --- Reapply property dynamic list restrictions if available ---
|
|
if (typeof propertyDynamicSet === 'function') {
|
|
propertyDynamicSet();
|
|
}
|
|
|
|
// --- Remove any old help block and re-render updated help info ---
|
|
document.querySelectorAll('#help').forEach(el => el.remove());
|
|
|
|
const helpNote = document.querySelector('.helpNote');
|
|
if (helpNote) {
|
|
const helpDiv = document.createElement('div');
|
|
helpDiv.id = 'help';
|
|
helpDiv.innerHTML = `${result.description}<br />${result.values_description}`;
|
|
helpNote.appendChild(helpDiv);
|
|
}
|
|
|
|
// --- If requested, load database-related default values ---
|
|
if (db && result.database) {
|
|
const dbData = result.database;
|
|
|
|
// Update datatype
|
|
if (notEmpty(dbData?.datatype)) {
|
|
setChoicesFieldValue('#jform_datatype', dbData.datatype);
|
|
updateFieldRequired('datatype', 0);
|
|
} else {
|
|
unselectChoicesFieldValue('#jform_datatype');
|
|
}
|
|
|
|
// Update datalenght
|
|
if (notEmpty(dbData?.datalenght)) {
|
|
setChoicesFieldValue('#jform_datalenght', dbData.datalenght);
|
|
updateFieldRequired('datalenght', 0);
|
|
if (dbData.datalenght === 'Other' && notEmpty(dbData?.datalenght_other)) {
|
|
setChoicesFieldValue('#jform_datalenght_other', dbData.datalenght_other);
|
|
updateFieldRequired('datalenght_other', 0);
|
|
}
|
|
} else {
|
|
unselectChoicesFieldValue('#jform_datalenght');
|
|
unselectChoicesFieldValue('#jform_datalenght_other');
|
|
updateFieldRequired('datalenght', 1);
|
|
updateFieldRequired('datalenght_other', 1);
|
|
}
|
|
|
|
// Update datadefault
|
|
if (notEmpty(dbData?.datadefault)) {
|
|
setChoicesFieldValue('#jform_datadefault', dbData.datadefault);
|
|
updateFieldRequired('datadefault', 0);
|
|
if (dbData.datadefault === 'Other') {
|
|
setChoicesFieldValue('#jform_datadefault_other', dbData.datadefault_other);
|
|
updateFieldRequired('datadefault_other', 0);
|
|
} else {
|
|
updateFieldRequired('datadefault_other', 1);
|
|
}
|
|
} else {
|
|
unselectChoicesFieldValue('#jform_datadefault');
|
|
unselectChoicesFieldValue('#jform_datadefault_other');
|
|
updateFieldRequired('datadefault', 1);
|
|
updateFieldRequired('datadefault_other', 1);
|
|
}
|
|
|
|
// Update indexes
|
|
if (notEmpty(dbData?.indexes)) {
|
|
setChoicesFieldValue('#jform_indexes', dbData.indexes);
|
|
// updateFieldRequired('indexes', 0);
|
|
} else {
|
|
// unselectChoicesFieldValue('#jform_indexes');
|
|
}
|
|
|
|
// Update store
|
|
if (notEmpty(dbData?.store)) {
|
|
setChoicesFieldValue('#jform_store', dbData.store);
|
|
// updateFieldRequired('store', 0);
|
|
} else {
|
|
// unselectChoicesFieldValue('#jform_store');
|
|
}
|
|
} else if (db) {
|
|
// Reset datatype
|
|
// unselectChoicesFieldValue('#jform_datatype');
|
|
|
|
// Reset datalenght
|
|
// unselectChoicesFieldValue('#jform_datalenght');
|
|
// updateFieldRequired('datalenght', 1);
|
|
// unselectChoicesFieldValue('#jform_datalenght_other');
|
|
// updateFieldRequired('datalenght_other', 1);
|
|
|
|
// Reset datadefault
|
|
// unselectChoicesFieldValue('#jform_datadefault');
|
|
// updateFieldRequired('datadefault', 1);
|
|
// unselectChoicesFieldValue('#jform_datadefault_other');
|
|
// updateFieldRequired('datadefault_other', 1);
|
|
|
|
// Reset indexes
|
|
// unselectChoicesFieldValue('#jform_indexes');
|
|
// updateFieldRequired('indexes', 1);
|
|
|
|
// Reset store
|
|
// unselectChoicesFieldValue('#jform_store');
|
|
// updateFieldRequired('store', 1);
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('[getFieldTypeProperties] Error:', error);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Determine whether a given value should be considered "non-empty" or "present".
|
|
*
|
|
* This function returns true for:
|
|
* - Positive numbers (greater than 0)
|
|
* - Non-empty strings
|
|
* - Non-empty arrays
|
|
* - Any object with a numeric `.length` property greater than 0
|
|
*
|
|
* It safely ignores and returns false for:
|
|
* - 0, null, undefined, NaN, false
|
|
* - Empty strings ('')
|
|
* - Empty arrays ([])
|
|
* - Objects without a `length` property
|
|
*
|
|
* @param {*} value The value to evaluate.
|
|
*
|
|
* @return {boolean} True if the value is considered non-empty, false otherwise.
|
|
* @since 5.1.3
|
|
*/
|
|
function notEmpty(value) {
|
|
// Handle numeric values explicitly
|
|
if (typeof value === 'number') {
|
|
return value > 0 && Number.isFinite(value);
|
|
}
|
|
|
|
// Handle objects with a length property (string, array, NodeList, etc.)
|
|
if (typeof value?.length === 'number') {
|
|
return value.length > 0;
|
|
}
|
|
|
|
// Everything else (null, undefined, {}, false, etc.) is considered empty
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Watch for Joomla subform row additions and removals.
|
|
*
|
|
* This listener keeps property data consistent by re-running
|
|
* propertyDynamicSet() whenever a subform row is added or removed.
|
|
*
|
|
* On row removal, the removed field's ID is also extracted
|
|
* to update the dynamic property list accordingly.
|
|
*
|
|
* @return {void}
|
|
* @since 3.1.3
|
|
*/
|
|
function rowWatcher() {
|
|
// Listen for subform row removals
|
|
document.addEventListener('subform-row-remove', function (event) {
|
|
try {
|
|
// Joomla 5 provides the removed row in event.detail.row
|
|
const row = event.detail?.row || null;
|
|
|
|
if (row) {
|
|
// Find the .field_list_name_options element and get its ID
|
|
const field = row.querySelector('.field_list_name_options');
|
|
if (field && field.id) {
|
|
propertyIdRemoved = field.id;
|
|
}
|
|
}
|
|
|
|
// Recalculate property options
|
|
if (typeof propertyDynamicSet === 'function') {
|
|
propertyDynamicSet();
|
|
}
|
|
} catch (error) {
|
|
console.error('[rowWatcher] subform-row-remove error:', error);
|
|
}
|
|
});
|
|
|
|
// Listen for subform row additions
|
|
document.addEventListener('subform-row-add', function (event) {
|
|
try {
|
|
// Recalculate property options after a new row is added
|
|
if (typeof propertyDynamicSet === 'function') {
|
|
propertyDynamicSet();
|
|
}
|
|
} catch (error) {
|
|
console.error('[rowWatcher] subform-row-add error:', error);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Dynamically rebuild the available property selection lists.
|
|
*
|
|
* This function synchronizes all property select fields on the page
|
|
* to prevent duplicate selections. It rebuilds the available options list
|
|
* for each field based on what is already selected elsewhere.
|
|
*
|
|
* @return {void}
|
|
* @since 3.1.3
|
|
*/
|
|
function propertyDynamicSet() {
|
|
// Reset global trackers
|
|
propertiesAvailable = {};
|
|
propertiesSelectedArray = {};
|
|
propertiesTrackerArray = {};
|
|
|
|
// Check up to 70 potential property rows (same as legacy logic)
|
|
for (let i = 0; i < 70; i++) {
|
|
// Build field ID (example: properties_properties0__name)
|
|
const id_check = `${rowIdKey}_${rowIdKey}${i}__name`;
|
|
const field = document.getElementById(id_check);
|
|
|
|
// Ensure field exists and isn't the one just removed
|
|
if (field && propertyIdRemoved !== id_check) {
|
|
// Get selected option key and label
|
|
const selectedOption = field.options[field.selectedIndex];
|
|
if (selectedOption) {
|
|
const key = selectedOption.value;
|
|
const text = selectedOption.text;
|
|
|
|
// Track selected and used properties
|
|
propertiesSelectedArray[key] = text;
|
|
propertiesTrackerArray[id_check] = key;
|
|
|
|
// Clear all existing options
|
|
while (field.options.length > 0) {
|
|
field.remove(0);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Build available property options (those not yet selected)
|
|
for (const prop in propertiesArray) {
|
|
if (Object.prototype.hasOwnProperty.call(propertiesArray, prop) && !propertiesSelectedArray.hasOwnProperty(prop)) {
|
|
propertiesAvailable[prop] = propertiesArray[prop];
|
|
}
|
|
}
|
|
|
|
// Rebuild the options list for each tracked select field
|
|
for (const tId in propertiesTrackerArray) {
|
|
if (Object.prototype.hasOwnProperty.call(propertiesTrackerArray, tId)) {
|
|
const tKey = propertiesTrackerArray[tId];
|
|
const selectEl = document.getElementById(tId);
|
|
|
|
if (selectEl) {
|
|
// Reinsert the previously selected option
|
|
const selectedOption = document.createElement('option');
|
|
selectedOption.value = tKey;
|
|
selectedOption.textContent = propertiesSelectedArray[tKey] ?? '';
|
|
selectEl.appendChild(selectedOption);
|
|
|
|
// Add all available properties
|
|
for (const aKey in propertiesAvailable) {
|
|
if (Object.prototype.hasOwnProperty.call(propertiesAvailable, aKey)) {
|
|
const option = document.createElement('option');
|
|
option.value = aKey;
|
|
option.textContent = propertiesAvailable[aKey];
|
|
selectEl.appendChild(option);
|
|
}
|
|
}
|
|
|
|
// Restore selected value and refresh Choices UI
|
|
setChoicesFieldValue(`#${tId}`, tKey, false);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fetch the field property description from the server.
|
|
*
|
|
* This method replaces the legacy jQuery.ajax() call with a modern Fetch API request.
|
|
* It validates inputs, builds a secure request URL using Joomla's JRouter,
|
|
* and returns the JSON response from the server. If an error occurs, it logs
|
|
* a descriptive message and returns null to ensure safe promise resolution.
|
|
*
|
|
* @param {string|number} fieldtype The field type ID or name to request the description for.
|
|
* @param {string} property The property name to request.
|
|
*
|
|
* @return {Promise<object|null>} A Promise resolving to the JSON response object or null on error.
|
|
* @since 3.1.3
|
|
*/
|
|
function getFieldPropertyDesc_server(fieldtype, property) {
|
|
// Validate token
|
|
if (typeof token !== 'string' || token.length === 0) {
|
|
console.warn('[getFieldPropertyDesc_server] Missing or invalid token.');
|
|
return Promise.resolve(null);
|
|
}
|
|
|
|
// Validate fieldtype
|
|
let validFieldtype = false;
|
|
if (typeof fieldtype === 'number' && fieldtype > 0) {
|
|
validFieldtype = true;
|
|
} else if (typeof fieldtype === 'string' && fieldtype.trim().length > 0) {
|
|
validFieldtype = true;
|
|
}
|
|
|
|
if (!validFieldtype) {
|
|
console.debug('[getFieldPropertyDesc_server] Empty fieldtype provided:', fieldtype);
|
|
return Promise.resolve(null);
|
|
}
|
|
|
|
// Validate property
|
|
if (typeof property !== 'string' || property.trim().length === 0) {
|
|
console.debug('[getFieldPropertyDesc_server] Empty property provided.');
|
|
return Promise.resolve(null);
|
|
}
|
|
|
|
// Build URL and query
|
|
const baseUrl = 'index.php';
|
|
const params = new URLSearchParams({
|
|
option: 'com_componentbuilder',
|
|
task: 'ajax.getFieldPropertyDesc',
|
|
format: 'json',
|
|
raw: 'true',
|
|
[token]: '1',
|
|
fieldtype: encodeURIComponent(fieldtype),
|
|
property: encodeURIComponent(property)
|
|
});
|
|
if (vastDevMod) params.append('vdm', vastDevMod);
|
|
|
|
// Final request URL
|
|
const requestUrl = JRouter(`${baseUrl}?${params.toString()}`);
|
|
|
|
// Perform GET request via Fetch
|
|
return fetch(requestUrl, {
|
|
method: 'GET',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
}
|
|
})
|
|
.then((response) => {
|
|
if (!response.ok) {
|
|
throw new Error(`[getFieldPropertyDesc_server] Network response was not ok: ${response.status}`);
|
|
}
|
|
return response.json();
|
|
})
|
|
.then((data) => data)
|
|
.catch((error) => {
|
|
console.error('[getFieldPropertyDesc_server] Fetch operation failed:', error);
|
|
return null;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Manage database field visibility and validation based on the field type.
|
|
*
|
|
* When the field type is "note" or "spacer", all database settings are hidden
|
|
* and cleared. Otherwise, the relevant database fields are shown and required.
|
|
*
|
|
* @param {string} type The field type to evaluate.
|
|
*
|
|
* @return {void}
|
|
* @since 3.1.3
|
|
*/
|
|
function dbChecker(type) {
|
|
if (type === 'note' || type === 'spacer') {
|
|
// Reset database-related select fields
|
|
unselectChoicesFieldValue('#jform_datatype');
|
|
unselectChoicesFieldValue('#jform_datalenght');
|
|
unselectChoicesFieldValue('#jform_datadefault');
|
|
unselectChoicesFieldValue('#jform_indexes');
|
|
unselectChoicesFieldValue('#jform_store');
|
|
|
|
// Datatype field group
|
|
const datatypeLbl = document.querySelector('#jform_datatype-lbl');
|
|
const datatype = document.querySelector('#jform_datatype');
|
|
if (datatypeLbl) datatypeLbl.closest('.control-group').style.display = 'none';
|
|
if (datatype) datatype.closest('.control-group').style.display = 'none';
|
|
updateFieldRequired('datatype', 1);
|
|
if (datatype) {
|
|
datatype.removeAttribute('required');
|
|
datatype.removeAttribute('aria-required');
|
|
datatype.classList.remove('required');
|
|
}
|
|
|
|
// Null switch field group
|
|
const nullLbl = document.querySelector('#jform_null_switch-lbl');
|
|
const nullSwitch = document.querySelector('#jform_null_switch');
|
|
if (nullLbl) nullLbl.closest('.control-group').style.display = 'none';
|
|
if (nullSwitch) nullSwitch.closest('.control-group').style.display = 'none';
|
|
updateFieldRequired('null_switch', 1);
|
|
if (nullSwitch) {
|
|
nullSwitch.removeAttribute('required');
|
|
nullSwitch.removeAttribute('aria-required');
|
|
nullSwitch.classList.remove('required');
|
|
}
|
|
|
|
// Store field group
|
|
const storeLbl = document.querySelector('#jform_store-lbl');
|
|
const store = document.querySelector('#jform_store');
|
|
if (storeLbl) storeLbl.closest('.control-group').style.display = 'none';
|
|
if (store) store.closest('.control-group').style.display = 'none';
|
|
updateFieldRequired('store', 1);
|
|
if (store) {
|
|
store.removeAttribute('required');
|
|
store.removeAttribute('aria-required');
|
|
store.classList.remove('required');
|
|
}
|
|
|
|
// Show/hide notices
|
|
document.querySelectorAll('.note_no_database_settings_needed').forEach(el => {
|
|
const group = el.closest('.control-group');
|
|
if (group) group.style.display = '';
|
|
});
|
|
document.querySelectorAll('.note_database_settings_needed').forEach(el => {
|
|
const group = el.closest('.control-group');
|
|
if (group) group.style.display = 'none';
|
|
});
|
|
|
|
} else {
|
|
// Datatype field group
|
|
const datatypeLbl = document.querySelector('#jform_datatype-lbl');
|
|
const datatype = document.querySelector('#jform_datatype');
|
|
if (datatypeLbl) datatypeLbl.closest('.control-group').style.display = '';
|
|
if (datatype) datatype.closest('.control-group').style.display = '';
|
|
updateFieldRequired('datatype', 0);
|
|
if (datatype) {
|
|
datatype.setAttribute('required', 'required');
|
|
datatype.setAttribute('aria-required', 'true');
|
|
datatype.classList.add('required');
|
|
}
|
|
|
|
// Null switch field group
|
|
// const nullLbl = document.querySelector('#jform_null_switch-lbl');
|
|
// const nullSwitch = document.querySelector('#jform_null_switch');
|
|
// if (nullLbl) nullLbl.closest('.control-group').style.display = '';
|
|
// if (nullSwitch) nullSwitch.closest('.control-group').style.display = '';
|
|
// updateFieldRequired('null_switch', 0);
|
|
// if (nullSwitch) {
|
|
// nullSwitch.setAttribute('required', 'required');
|
|
// nullSwitch.setAttribute('aria-required', 'true');
|
|
// nullSwitch.classList.add('required');
|
|
// }
|
|
|
|
// Store field group
|
|
// const storeLbl = document.querySelector('#jform_store-lbl');
|
|
// const store = document.querySelector('#jform_store');
|
|
// if (storeLbl) storeLbl.closest('.control-group').style.display = '';
|
|
// if (store) store.closest('.control-group').style.display = '';
|
|
// updateFieldRequired('store', 0);
|
|
// if (store) {
|
|
// store.setAttribute('required', 'required');
|
|
// store.setAttribute('aria-required', 'true');
|
|
// store.classList.add('required');
|
|
// }
|
|
|
|
// Update database-related selects to defaults (0 or empty) if needed
|
|
// unselectChoicesFieldValue('#jform_indexes');
|
|
// unselectChoicesFieldValue('#jform_store');
|
|
|
|
// Show/hide notices
|
|
document.querySelectorAll('.note_no_database_settings_needed').forEach(el => {
|
|
const group = el.closest('.control-group');
|
|
if (group) group.style.display = 'none';
|
|
});
|
|
document.querySelectorAll('.note_database_settings_needed').forEach(el => {
|
|
const group = el.closest('.control-group');
|
|
if (group) group.style.display = '';
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fetch and display the validation rules table from the server.
|
|
*
|
|
* This method requests the validation rules HTML from the server using
|
|
* getCodeFrom_server(), then injects it into the #display_validation_rules
|
|
* container. It is functionally identical to the original jQuery version
|
|
* but rewritten in pure, modern JavaScript.
|
|
*
|
|
* @return {Promise<void>} Resolves when the validation rules are loaded and rendered.
|
|
* @since 3.1.3
|
|
*/
|
|
async function getValidationRulesTable() {
|
|
try {
|
|
// Request validation rules HTML from the server
|
|
const result = await getCodeFrom_server(1, 'type', 'type', 'getValidationRulesTable');
|
|
|
|
// Inject result into target container if present
|
|
if (result) {
|
|
const target = document.getElementById('display_validation_rules');
|
|
if (target) {
|
|
target.innerHTML = result;
|
|
} else {
|
|
console.warn('[getValidationRulesTable] Target element #display_validation_rules not found.');
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('[getValidationRulesTable] Failed to fetch validation rules table:', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fetch data from the server with validated parameters.
|
|
*
|
|
* @param {number|string} id The record ID (integer > 0 or string > 30 chars)
|
|
* @param {string} type The type value to send
|
|
* @param {string} typeName The type parameter name (e.g. "type" or "context")
|
|
* @param {string} callingName The AJAX task name (e.g. "getCode")
|
|
* @global {string} token The CSRF token name or key
|
|
* @global {string} vastDevMod The developer key or mode flag (optional)
|
|
*
|
|
* @return {Promise<object|null>} Returns parsed JSON data or null on failure
|
|
* @since 3.1.2
|
|
*/
|
|
async function getCodeFrom_server(id, type, typeName, callingName) {
|
|
try {
|
|
// --- Validation ---
|
|
if (!getCodeFrom_isValidId(id)) {
|
|
console.debug('[getCodeFrom_server] Invalid ID provided:', id);
|
|
return null;
|
|
}
|
|
if (typeof type !== 'string' || !type.trim()) {
|
|
console.debug('[getCodeFrom_server] Invalid type provided:', type);
|
|
return null;
|
|
}
|
|
if (typeof typeName !== 'string' || !typeName.trim()) {
|
|
console.debug('[getCodeFrom_server] Invalid typeName provided:', typeName);
|
|
return null;
|
|
}
|
|
if (typeof callingName !== 'string' || !callingName.trim()) {
|
|
console.debug('[getCodeFrom_server] Invalid callingName provided:', callingName);
|
|
return null;
|
|
}
|
|
if (typeof token !== 'string' || !token.trim()) {
|
|
console.debug('[getCodeFrom_server] Missing security token.');
|
|
return null;
|
|
}
|
|
|
|
// --- Construct URL safely ---
|
|
const baseUrl = 'index.php';
|
|
const params = new URLSearchParams({
|
|
option: 'com_componentbuilder',
|
|
task: `ajax.${callingName}`,
|
|
format: 'json',
|
|
raw: 'true',
|
|
[token]: '1',
|
|
[typeName]: type,
|
|
id: id
|
|
});
|
|
if (vastDevMod) params.append('vdm', vastDevMod);
|
|
|
|
const fullUrl = JRouter(`${baseUrl}?${params.toString()}`);
|
|
|
|
// --- Execute request ---
|
|
const response = await fetch(fullUrl, {
|
|
method: 'GET',
|
|
headers: {
|
|
'Accept': 'application/json',
|
|
'Content-Type': 'application/json'
|
|
},
|
|
cache: 'no-store',
|
|
credentials: 'same-origin'
|
|
});
|
|
|
|
// --- Validate HTTP response ---
|
|
if (!response.ok) {
|
|
console.error(`[getCodeFromServer] Server responded with status ${response.status}: ${response.statusText}`);
|
|
return null;
|
|
}
|
|
|
|
// --- Parse JSON response ---
|
|
const data = await response.json();
|
|
return data ?? null;
|
|
|
|
} catch (error) {
|
|
console.error('[getCodeFromServer] Fetch operation failed:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate if the given ID is acceptable.
|
|
*
|
|
* @param {number|string} id The ID value to validate.
|
|
* @return {boolean} True if valid, false otherwise.
|
|
* @since 3.1.2
|
|
*/
|
|
function getCodeFrom_isValidId(id) {
|
|
if (typeof id === 'number') {
|
|
return Number.isInteger(id) && id > 0;
|
|
}
|
|
if (typeof id === 'string') {
|
|
return id.trim().length > 30;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function getLinked() {
|
|
getCodeFrom_server(1, 'type', 'type', 'getLinked').then(function(result) {
|
|
if (result.error) {
|
|
console.error(result.error);
|
|
} else if (result) {
|
|
document.getElementById('display_linked_to').innerHTML = result;
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Fetches button data from the server.
|
|
*
|
|
* @param {string} type The button type identifier.
|
|
* @param {number} size The button size indicator (default: 1).
|
|
* @global {string} token The CSRF token key.
|
|
* @global {string} vastDevMod Developer mode flag (optional).
|
|
*
|
|
* @return {Promise<object|null>} Returns JSON data or null on failure.
|
|
* @since 3.1.3
|
|
*/
|
|
async function addButton_server(type, size = 1) {
|
|
try {
|
|
// --- Validate input ---
|
|
if (typeof type !== 'string' || !type.trim()) {
|
|
console.error('[addButton_server] Invalid type provided:', type);
|
|
return null;
|
|
}
|
|
if (typeof token !== 'string' || !token.trim()) {
|
|
console.error('[addButton_server] Missing CSRF token.');
|
|
return null;
|
|
}
|
|
|
|
// --- Build URL and query ---
|
|
const baseUrl = 'index.php';
|
|
const params = new URLSearchParams({
|
|
option: 'com_componentbuilder',
|
|
task: 'ajax.getButton',
|
|
format: 'json',
|
|
raw: 'true',
|
|
[token]: '1',
|
|
type: type,
|
|
size: size
|
|
});
|
|
if (vastDevMod) params.append('vdm', vastDevMod);
|
|
|
|
const fullUrl = JRouter(`${baseUrl}?${params.toString()}`);
|
|
|
|
// --- Fetch the data ---
|
|
const response = await fetch(fullUrl, {
|
|
method: 'GET',
|
|
headers: {
|
|
'Accept': 'application/json',
|
|
'Content-Type': 'application/json'
|
|
},
|
|
cache: 'no-store',
|
|
credentials: 'same-origin'
|
|
});
|
|
|
|
if (!response.ok) {
|
|
console.error(`[addButton_server] Server responded with ${response.status}: ${response.statusText}`);
|
|
return null;
|
|
}
|
|
|
|
const data = await response.json();
|
|
return data ?? null;
|
|
} catch (error) {
|
|
console.error('[addButton_server] Fetch failed:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handles button rendering into the DOM.
|
|
*
|
|
* @param {string} type The button type identifier.
|
|
* @param {string} where The target element ID or selector.
|
|
* @param {number} size Optional button size (default: 1).
|
|
* @global {string} token The CSRF token key.
|
|
* @global {string} vastDevMod Developer mode flag (optional).
|
|
*
|
|
* @return {Promise<void>}
|
|
* @since 3.1.3
|
|
*/
|
|
async function addButton(type, where, size) {
|
|
const result = await addButton_server(type, size);
|
|
|
|
if (!result) {
|
|
console.warn('[addButton] No button data returned from server.');
|
|
return;
|
|
}
|
|
|
|
const target = document.querySelector(size === 2 ? `#${where}` : `#jform_${where}`);
|
|
if (!target) {
|
|
console.error('[addButton] Target element not found:', where);
|
|
return;
|
|
}
|
|
|
|
if (size === 2) {
|
|
target.innerHTML = result;
|
|
} else {
|
|
addData(result, target);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Insert a new HTML element or markup right after
|
|
* the closest `.control-group` ancestor of the target element.
|
|
*
|
|
* Equivalent to:
|
|
* jQuery(result).insertAfter(jQuery(where).closest('.control-group'));
|
|
*
|
|
* @param {string|HTMLElement} result The HTML markup or DOM element to insert.
|
|
* @param {string} where A CSS selector or HTMLElement used to locate the `.control-group` ancestor.
|
|
*
|
|
* @return {void}
|
|
* @since 3.1.3
|
|
*/
|
|
function addData(result, where) {
|
|
try {
|
|
// Resolve the base element (can be selector or element)
|
|
const baseElement = typeof where === 'string'
|
|
? document.querySelector(where)
|
|
: where;
|
|
|
|
if (!baseElement) {
|
|
console.error('[addData] Target element not found:', where);
|
|
return;
|
|
}
|
|
|
|
// Find the closest .control-group ancestor
|
|
const controlGroup = baseElement.closest('.control-group');
|
|
if (!controlGroup || !controlGroup.parentNode) {
|
|
console.error('[addData] No .control-group ancestor found for:', where);
|
|
return;
|
|
}
|
|
|
|
// Create or reuse element to insert
|
|
let nodeToInsert;
|
|
if (typeof result === 'string') {
|
|
// Create a DOM node directly from HTML
|
|
const wrapper = document.createElement('div');
|
|
wrapper.innerHTML = result.trim();
|
|
nodeToInsert = wrapper.firstElementChild;
|
|
} else if (result instanceof HTMLElement) {
|
|
nodeToInsert = result;
|
|
} else {
|
|
console.error('[addData] Invalid result type:', result);
|
|
return;
|
|
}
|
|
|
|
// Insert the node after the .control-group element
|
|
controlGroup.parentNode.insertBefore(nodeToInsert, controlGroup.nextSibling);
|
|
} catch (error) {
|
|
console.error('[addData] Error inserting element:', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set a field value and keep the visible UI (Choices/Joomla Fancy Select) in sync.
|
|
*
|
|
* - Skips Choices/Joomla fancy select logic for plain text/number/email/url inputs.
|
|
* - Only updates the value if it actually changed.
|
|
* - Avoids duplicate change events that can cause recursion.
|
|
*
|
|
* @param {string} selector CSS selector for the field element.
|
|
* @param {any} value The value to assign to the field.
|
|
* @param {bool} forceBubbles
|
|
*
|
|
* @return {void}
|
|
* @since 5.1.3
|
|
*/
|
|
function setChoicesFieldValue(selector, value, forceBubbles = true) {
|
|
const field = document.querySelector(selector);
|
|
if (!field) {
|
|
return;
|
|
}
|
|
|
|
// Compute normalized new value and current value
|
|
const newVal = value == null ? '' : String(value);
|
|
const curVal = field.value == null ? '' : String(field.value);
|
|
|
|
// Skip work if nothing changed (prevents unnecessary events/loops)
|
|
const valueChanged = newVal !== curVal;
|
|
isPlainInput = isPlainInputField(field);
|
|
|
|
// For plain inputs: set only if changed, then emit a single change
|
|
if (isPlainInput) {
|
|
if (valueChanged) {
|
|
field.value = newVal;
|
|
// Fire a single change to notify observers; no need for input here
|
|
field.dispatchEvent(new Event('change', { bubbles: true }));
|
|
}
|
|
return;
|
|
}
|
|
|
|
// For selects (or other widgets) ensure Choices/Joomla Fancy Select is ready
|
|
const choicesInstance = ensureChoicesInitialization(field);
|
|
|
|
// Update only if value actually changed
|
|
if (valueChanged) {
|
|
// Update the native value first
|
|
field.value = newVal;
|
|
|
|
// If we have a Choices/Joomla instance, update its UI.
|
|
// These often emit their own change event; no need to manually dispatch another.
|
|
if (choicesInstance) {
|
|
try {
|
|
// Preferred API
|
|
choicesInstance.setChoiceByValue(newVal);
|
|
} catch (err) {
|
|
// Fallback for older APIs
|
|
if (typeof choicesInstance.setValue === 'function') {
|
|
choicesInstance.setValue(newVal);
|
|
}
|
|
}
|
|
if (!forceBubbles) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
// No widget present: dispatch a single change event.
|
|
field.dispatchEvent(new Event('change', { bubbles: true }));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Unselect the current value from a Joomla Fancy Select / Choices.js field
|
|
* without removing or clearing its available options.
|
|
*
|
|
* - Keeps all existing options intact.
|
|
* - Ensures an empty option ("Select an option") exists.
|
|
* - Visually and logically deselects the current choice.
|
|
* - Works with both Joomla.FieldChoices and plain Choices.js.
|
|
*
|
|
* @param {string} selector CSS selector for the field element.
|
|
*
|
|
* @return {void}
|
|
* @since 5.1.5
|
|
*/
|
|
function unselectChoicesFieldValue(selector) {
|
|
const field = document.querySelector(selector);
|
|
if (!field) {
|
|
console.warn('unselectChoicesFieldValue: field not found ->', selector);
|
|
return;
|
|
}
|
|
|
|
// Compute normalized new value and current value
|
|
const newVal = '';
|
|
const curVal = field.value == null ? '' : String(field.value);
|
|
|
|
// Skip work if nothing changed (prevents unnecessary events/loops)
|
|
const valueChanged = newVal !== curVal;
|
|
isPlainInput = isPlainInputField(field);
|
|
|
|
// Update only if value actually changed
|
|
if (!valueChanged) {
|
|
return;
|
|
}
|
|
|
|
// For plain inputs: set only if changed, then emit a single change
|
|
if (isPlainInput) {
|
|
if (valueChanged) {
|
|
field.value = newVal;
|
|
// Fire a single change to notify observers; no need for input here
|
|
field.dispatchEvent(new Event('change', { bubbles: true }));
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Ensure an empty option exists
|
|
let emptyOption = field.querySelector('option[value=""]');
|
|
if (!emptyOption) {
|
|
emptyOption = document.createElement('option');
|
|
emptyOption.value = '';
|
|
emptyOption.textContent = 'Select an option';
|
|
field.insertBefore(emptyOption, field.firstChild);
|
|
}
|
|
|
|
// Initialize or fetch existing Choices/Fancy Select instance
|
|
const choicesInstance = ensureChoicesInitialization(field);
|
|
|
|
// Set the field to the empty option (unselect)
|
|
field.value = emptyOption.value;
|
|
|
|
// Update Choices/Fancy Select visually without clearing the available choices
|
|
if (choicesInstance) {
|
|
try {
|
|
// Try the standard method first
|
|
if (typeof choicesInstance.setChoiceByValue === 'function') {
|
|
choicesInstance.setChoiceByValue(emptyOption.value);
|
|
}
|
|
// Fallback for older APIs
|
|
else if (typeof choicesInstance.setValue === 'function') {
|
|
choicesInstance.setValue([emptyOption.value]);
|
|
}
|
|
// Some Joomla Fancy Selects may use a selectElement property
|
|
else if (choicesInstance.passedElement?.element) {
|
|
choicesInstance.passedElement.element.value = emptyOption.value;
|
|
}
|
|
} catch (e) {
|
|
console.warn('unselectChoicesFieldValue: failed to update UI', e);
|
|
}
|
|
}
|
|
|
|
// Fire a proper change event to keep observers in sync
|
|
field.dispatchEvent(new Event('change', { bubbles: true }));
|
|
}
|
|
|
|
/**
|
|
* Ensure a Joomla Fancy Select or Choices.js instance is initialized for a given field.
|
|
*
|
|
* This function checks whether the element already has a Choices/Fancy Select instance.
|
|
* If not, it attempts to initialize one using Joomla.FieldChoices or a Choices.js fallback.
|
|
*
|
|
* @param {HTMLElement} element The field element to initialize.
|
|
*
|
|
* @return {object|null} The initialized Choices/Fancy Select instance or null on failure.
|
|
* @since 5.1.3
|
|
*/
|
|
function ensureChoicesInitialization(element) {
|
|
if (!element) {
|
|
return null;
|
|
}
|
|
|
|
// Detect existing wrapper or instance
|
|
const fancyWrapper = element.closest('joomla-field-fancy-select');
|
|
const existingChoices =
|
|
element.choices ||
|
|
(fancyWrapper && fancyWrapper.choicesInstance) ||
|
|
(window.Joomla?.FieldChoices?.instances && Joomla.FieldChoices.instances[element.id]) ||
|
|
null;
|
|
|
|
// Return if already initialized
|
|
if (existingChoices) {
|
|
return existingChoices;
|
|
}
|
|
|
|
// Attempt Joomla FieldChoices initialization
|
|
if (window.Joomla?.FieldChoices?.init) {
|
|
try {
|
|
const instance = Joomla.FieldChoices.init(element);
|
|
return instance || null;
|
|
} catch (e) {
|
|
console.warn('ensureChoicesInitialization: Joomla FieldChoices.init failed', e);
|
|
}
|
|
}
|
|
|
|
// Fallback: manually initialize Choices.js
|
|
if (typeof Choices !== 'undefined') {
|
|
try {
|
|
const instance = new Choices(element, { shouldSort: false, searchEnabled: true });
|
|
element.choices = instance;
|
|
return instance;
|
|
} catch (e) {
|
|
console.warn('ensureChoicesInitialization: Choices fallback failed', e);
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Detects whether an element is a plain input (not a select/Choices).
|
|
*
|
|
* @param {HTMLElement} field
|
|
* @return {boolean}
|
|
* @since 5.1.3
|
|
*/
|
|
function isPlainInputField(field) {
|
|
const tagName = field.tagName.toLowerCase();
|
|
const type = (field.getAttribute('type') || '').toLowerCase();
|
|
return tagName === 'input' && ['text', 'number', 'email', 'url'].includes(type);
|
|
}
|