Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
32 changes: 25 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,13 @@
//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; ?>
<?php if($type === 'field'): ?>
'aria-describedby': '<?= $htmlId ?>-desc',
<?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,8 +393,8 @@ 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>{{ this.txt['operation.autocomplete.people.field.desc'] }}</span>
<span class="material-symbols-outlined" aria-hidden="true">info</span>
<span :id="this.options.htmlId + '-desc'">{{ this.txt['operation.autocomplete.people.field.desc'] }}</span>
</div>
</div>
`
Expand Down