Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/plugins/CoreEnroller/templates/element/spa-field.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
<div class="field">
<div class="field-name">
<div class="field-title">
<?= $label ?>
<?= $this->Form->label($formArguments['fieldName'], $label) ?>
<?php if($isRequired):?>
<?= $this->element('form/requiredSpan') ?>
<?php endif;?>
Expand Down
10 changes: 9 additions & 1 deletion app/src/View/Helper/FieldHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -264,14 +264,22 @@ public function constructSPAField(string $element, string $vueElementName): stri

if(!empty($matchesId[0][1]) && !empty($matchesName[0][1])) {
$vueElementProperties = [
'htmlId' => $matchesId[0][1] . '-picker',
// htmlId matches CakePHP's label 'for' attribute (e.g. 'field-3') so clicking
// the visible label natively focuses the interactive AutoComplete search input.
'htmlId' => $matchesId[0][1],
'fieldName' => $matchesName[0][1],
'containerClasses' => $matchesClass[0][1],
'type' => 'field',
// we want the label to be an empty string to hide the default label introduced by the module.
'label' => ''
];

// Parse the Required attribute
$regexRequired = '/\brequired(=["\']?required["\']?)?/i';
if(preg_match($regexRequired, $element)) {
$vueElementProperties['isRequired'] = true;
}

if (isset($matchesValue[0][1])) {
$vueElementProperties['inputValue'] = $matchesValue[0][1];
}
Expand Down
29 changes: 22 additions & 7 deletions app/templates/element/peopleAutocomplete.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,15 @@
// Used by the SearchFilter Configuration
$personType = $personType ?? 'person';
$htmlId = $htmlId ?? 'person-id-picker';

// DOM ID and Form Name separation:
// - Display Label: Assigned for="field-3" (matching $htmlId).
// - Interactive AutoComplete Input: Assigned id="field-3" (via $htmlId). Clicking the visible label natively
// focuses the interactive AutoComplete input, satisfying browser accessibility standards with zero ambiguity.
// - Hidden Submission Input: Assigned id="field-3-target" (via $targetId).
// Result: Every ID in the DOM is globally unique.
$targetId = $targetId ?? ($htmlId === $fieldName ? $fieldName . '-target' : $fieldName);
$isRequired = $isRequired ?? $vv_field_arguments["fieldOptions"]["required"] ?? $vv_field_arguments["required"] ?? false;
// Does it have a value already. Default or stored
// CAKEPHP automatically generates a select element if the value is an integer. This is not helpful here.
$inputValue = $inputValue ?? $vv_field_arguments["fieldOptions"]["default"] ?? $vv_field_arguments["fieldOptions"]["value"] ?? '';
Expand Down Expand Up @@ -87,12 +96,14 @@

$searchPeople = $this->request->getAttribute('webroot') . 'api/ajax/v2/people/pick?' . $targetKey . '=' . $targetValue;

// This is the actual field that will be submitted.
print $this->Form->control($fieldName, [
'id' => $fieldName,
// Form Submission name Attributes:
// - Hidden Submission Input: Rendered as <input type="hidden" name="field-3" id="field-3-target" value="...">
// This is the actual field that will be submitted on form POST.
// - Interactive AutoComplete Input: Rendered without a native form name="field-3" attribute (it uses internal
// Vue props and dataName), preventing duplicate/conflicting payload submissions.
print $this->Form->hidden($fieldName, [
'id' => $targetId,
'value' => '',
'type' => 'text',
'class' => 'visually-hidden'
]
);
}
Expand Down Expand Up @@ -129,7 +140,8 @@
return {
autocompleteOptions: {
label: '<?= $label ?>',
fieldName: '<?= $fieldName ?>', // This property hold the form input id
fieldName: '<?= $fieldName ?>', // This property holds the form input name
targetId: '<?= $targetId ?>', // This property holds the hidden input id
type: '<?= $type ?>',
personType: '<?= $personType ?>',
// XXX probably should be set by config, note the backend (ApiV2Controller::pick)
Expand All @@ -143,7 +155,10 @@
//name: '<?php //= $htmlId ?>//',
dataName: '<?= $htmlId ?>',
// This is not translated to data-personid but to datapersonid.
dataPersonid: '<?= $inputValue ?>'
dataPersonid: '<?= $inputValue ?>',
<?php if(!empty($isRequired)): ?>
required: true,
<?php endif; ?>
},
personRecord: <?= json_encode($personRecord ?? [])?>,
canvasUrl: '<?= $canvasUrl ?? '' ?>'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -190,8 +190,11 @@ export default {
if(['search', 'field'].includes(this.options.type)) {
this.options.inputProps.dataPersonid = this.person.value;
// Update the field that will be submitted
let inputElement = document.getElementById(this.options.fieldName)
inputElement.value = this.person.value;
const targetElementId = this.options.targetId || this.options.fieldName;
let inputElement = document.getElementById(targetElementId);
if(inputElement) {
inputElement.value = this.person.value;
}

} else {
// The picker is stand-alone, and should render the configured page in a modal on @item-select
Expand All @@ -211,8 +214,11 @@ export default {
},
onClear(ev) {
// The user cleared the input field - so clear the real field (and the model).
let inputElement = document.getElementById(this.options.fieldName)
inputElement.value = null;
const targetElementId = this.options.targetId || this.options.fieldName;
let inputElement = document.getElementById(targetElementId);
if(inputElement) {
inputElement.value = null;
}
this.person = null;
},
onListNavigate(ev) {
Expand Down Expand Up @@ -262,11 +268,14 @@ export default {
mounted() {
if(this.options.inputValue != undefined
&& this.options.inputValue != ''
&& this.options.inputProps.dataName.endsWith('person-id-picker')) {
&& (this.options.inputProps.dataName.endsWith('person-id') || this.options.inputProps.dataName.endsWith('person-id-picker'))) {
this.person = `${this.options.personRecord.primary_name.given} ${this.options.personRecord.primary_name.family} (ID: ${this.options.personRecord.id})`;
this.personUrl = `${this.options.canvasUrl}`;
let inputElement = document.getElementById(this.options.fieldName)
inputElement.value = this.options.personRecord.id;
const targetElementId = this.options.targetId || this.options.fieldName;
let inputElement = document.getElementById(targetElementId);
if(inputElement) {
inputElement.value = this.options.personRecord.id;
}
}
},
computed: {
Expand Down Expand Up @@ -384,7 +393,7 @@ export default {
</a>
</div>
<div v-if="this.options.type == 'field'" class="field-desc field-autocomplete-desc">
<span class="material-symbols-outlined">info</span>
<span class="material-symbols-outlined" aria-hidden="true">info</span>
<span>{{ this.txt['operation.autocomplete.people.field.desc'] }}</span>
</div>
</div>
Expand Down