Skip to content

Commit

Permalink
add units to extra fields for numbers inputs
Browse files Browse the repository at this point in the history
fix #3518
  • Loading branch information
NicolasCARPi committed Jul 23, 2023
1 parent ecf8745 commit 7341bb6
Show file tree
Hide file tree
Showing 8 changed files with 123 additions and 28 deletions.
8 changes: 7 additions & 1 deletion src/classes/TwigFilters.php
Expand Up @@ -94,7 +94,13 @@ public static function formatMetadata(string $json): string
$value = implode(', ', $value);
}

$final .= sprintf('<li class="list-group-item"><h5 class="mb-0">%s</h5>%s<h6>%s</h6></li>', $field['name'], $description, $value);
$unit = '';
if (!empty($field['unit'])) {
// a space before the unit so if there are no units we don't have a trailing space
$unit = ' ' . $field['unit'];
}

$final .= sprintf('<li class="list-group-item"><h5 class="mb-0">%s</h5>%s<h6>%s%s</h6></li>', $field['name'], $description, $value, $unit);
}
$final .= '</div>';
}
Expand Down
7 changes: 4 additions & 3 deletions src/scss/main.scss
Expand Up @@ -447,9 +447,10 @@ button:disabled {
}

/* required labels/input for extra fields */
#metadataDiv {
/* target the required fields, but only if they are invalid: empty or incorrect */
[required]:invalid {
#metadataDiv,
#newFieldForm {
/* any invalid field will get this red border */
:invalid {
border: 1px solid $lightred;
}

Expand Down
16 changes: 13 additions & 3 deletions src/templates/field-builder-modal.html
Expand Up @@ -49,7 +49,7 @@ <h5 class='modal-title' id='fieldBuilderLabel'><i class='fas fa-fw fa-trowel'></
</select>

<!-- NAME -->
<label for='newFieldKeyInput'>{{ 'Name'|trans }} <span class='smallgray'>({{ 'required'|trans }})</span></label>
<label for='newFieldKeyInput' class='required-label'>{{ 'Name'|trans }} <span class='smallgray'>({{ 'required'|trans }})</span></label>
<input type='text' autocomplete='off' required placeholder='{{ 'Name'|trans }}' id='newFieldKeyInput' class='form-control'>

<!-- DESCRIPTION -->
Expand Down Expand Up @@ -86,8 +86,8 @@ <h5 class='modal-title' id='fieldBuilderLabel'><i class='fas fa-fw fa-trowel'></
<div id='newFieldContentDiv_selectradio' hidden>
<label>{{ 'Choices'|trans }}</label>
<div id='choicesInputDiv'>
<input type='text' class='form-control mb-1 newFieldOption' autocomplete='off'>
<input type='text' class='form-control mb-1 newFieldOption' autocomplete='off'>
<input type='text' class='form-control mb-1' autocomplete='off'>
<input type='text' class='form-control mb-1' autocomplete='off'>
</div>
<button type='button' class='btn btn-secondary btn-sm' data-action='new-field-add-option'>{{ 'Add another choice'|trans }}</button>
<!-- this is only shown for select type -->
Expand All @@ -100,6 +100,16 @@ <h5 class='modal-title' id='fieldBuilderLabel'><i class='fas fa-fw fa-trowel'></
</div>
</div>

<!-- UNITS for NUMBER -->
<div id='newFieldContentDiv_number' hidden>
<label>{{ 'Available units'|trans }}</label>
<div id='unitChoicesInputDiv'>
<input type='text' class='form-control mb-1' autocomplete='off'>
<input type='text' class='form-control mb-1' autocomplete='off'>
</div>
<button type='button' class='btn btn-secondary btn-sm' data-action='new-field-add-option'>{{ 'Add another choice'|trans }}</button>
</div>

<!-- CHECKBOX -->
<div id='newFieldContentDiv_checkbox' hidden>
<label for='newFieldCheckboxDefaultSelect'>{{ 'Default value'|trans }}</label>
Expand Down
66 changes: 57 additions & 9 deletions src/ts/Metadata.class.ts
Expand Up @@ -47,24 +47,40 @@ export class Metadata {
/**
* Only save a single field value after a change
*/
handleEvent(event): Promise<Response> {
handleEvent(event: Event): Promise<Response> | boolean {
const el = event.target as HTMLFormElement;
if (el.reportValidity() === false) {
return false;
}
if (el.dataset.units === '1') {
return this.updateUnit(event);
}
// by default the value is simply the value of the input, which is the event target
let value = event.target.value;
let value = el.value;
// special case for checkboxes
if (event.target.type === 'checkbox') {
value = event.target.checked ? 'on': 'off';
if (el.type === 'checkbox') {
value = el.checked ? 'on': 'off';
}
// special case for multiselect
if (event.target.hasAttribute('multiple')) {
if (el.hasAttribute('multiple')) {
// collect all the selected options, and the value will be an array
value = [...event.target.selectedOptions].map(option => option.value);
value = [...el.selectedOptions].map(option => option.value);
}
const params = {};
params['action'] = Action.UpdateMetadataField;
params[event.target.dataset.field] = value;
params[el.dataset.field] = value;
return this.api.patch(`${this.entity.type}/${this.entity.id}`, params);
}

updateUnit(event: Event): Promise<Response> {
const select = (event.target as HTMLSelectElement);
const value = select.value;
const name = select.parentElement.parentElement.parentElement.querySelector('label').innerText;
return this.read().then(metadata => {
metadata.extra_fields[name].unit = value;
return this.save(metadata as ValidMetadata);
});
}
/**
* Save the whole json at once, coming from json editor save button
*/
Expand Down Expand Up @@ -153,7 +169,11 @@ export class Metadata {
}
} else {
valueEl = document.createElement('div');
valueEl.innerText = properties.value as string;
let value = properties.value as string;
if (properties.unit) {
value += ' ' + properties.unit;
}
valueEl.innerText = value;
// the link is generated with javascript so we can still use innerText and
// not innerHTML with manual "<a href...>" which implicates security considerations
if (properties.type === 'url') {
Expand Down Expand Up @@ -243,6 +263,7 @@ export class Metadata {
// set the callback to the whole class so handleEvent is called and 'this' refers to the class
// not the event in the function called
element.addEventListener('change', this, false);
element.addEventListener('blur', this, false);

// add a prepend button for "Now" for date and time types
if (['time', 'date', 'datetime-local'].includes(element.type)) {
Expand All @@ -262,6 +283,33 @@ export class Metadata {
inputGroupDiv.appendChild(element);
return inputGroupDiv;
}

// UNITS
if (Object.prototype.hasOwnProperty.call(properties, 'units') && properties.units.length > 0) {
const inputGroupDiv = document.createElement('div');
inputGroupDiv.classList.add('input-group');
const appendDiv = document.createElement('div');
appendDiv.classList.add('input-group-append');
const unitsSel = document.createElement('select');
for (const unit of properties.units) {
const optionEl = document.createElement('option');
optionEl.text = unit;
if (properties.unit === unit) {
optionEl.setAttribute('selected', '');
}
unitsSel.add(optionEl);
}
unitsSel.classList.add('form-control', 'brl-none');
// add this so we can differentiat the change event from the main input
unitsSel.dataset.units = '1';
unitsSel.addEventListener('change', this, false);
appendDiv.appendChild(unitsSel);
// input first, then append div
inputGroupDiv.appendChild(element);
inputGroupDiv.appendChild(appendDiv);
return inputGroupDiv;
}

return element;
}

Expand Down Expand Up @@ -424,7 +472,7 @@ export class Metadata {
label.classList.add('py-2');

// add a button to delete the field
const deleteBtn = document.createElement('span');
const deleteBtn = document.createElement('div');
deleteBtn.dataset.action = 'metadata-rm-field';
deleteBtn.classList.add('rounded', 'p-2', 'hl-hover-gray');
const deleteIcon = document.createElement('i');
Expand Down
2 changes: 1 addition & 1 deletion src/ts/edit.ts
Expand Up @@ -288,7 +288,7 @@ document.addEventListener('DOMContentLoaded', () => {
// DELETE EXTRA FIELD
} else if (el.matches('[data-action="metadata-rm-field"]')) {
MetadataC.read().then(metadata => {
const name = el.closest('div').querySelector('label').innerText;
const name = el.parentElement.closest('div').querySelector('label').innerText;
delete metadata.extra_fields[name];
MetadataC.update(metadata as ValidMetadata);
});
Expand Down
31 changes: 21 additions & 10 deletions src/ts/field-builder.ts
Expand Up @@ -20,7 +20,7 @@ document.addEventListener('DOMContentLoaded', () => {
const entity = getEntity();

function toggleContentDiv(key: string) {
const keys = ['classic', 'selectradio', 'checkbox'];
const keys = ['classic', 'selectradio', 'checkbox', 'number'];
document.getElementById('newFieldContentDiv_' + key).toggleAttribute('hidden', false);
// remove the shown one from the list and hide all others
keys.filter(k => k !== key).forEach(k => {
Expand All @@ -32,15 +32,12 @@ document.addEventListener('DOMContentLoaded', () => {
document.getElementById('newFieldTypeSelect').addEventListener('change', event => {
const fieldType = (event.target as HTMLSelectElement).value;
const valueInput = document.getElementById('newFieldValueInput');
// start by hiding this one, which is only shown for select
document.getElementById('newFieldContentDiv_select').toggleAttribute('hidden', true);

switch (fieldType as ExtraFieldInputType) {
case ExtraFieldInputType.Text:
case ExtraFieldInputType.Date:
case ExtraFieldInputType.DateTime:
case ExtraFieldInputType.Email:
case ExtraFieldInputType.Number:
case ExtraFieldInputType.Url:
case ExtraFieldInputType.Time:
valueInput.setAttribute('type', fieldType);
Expand All @@ -53,6 +50,7 @@ document.addEventListener('DOMContentLoaded', () => {
case ExtraFieldInputType.Radio:
toggleContentDiv('selectradio');
break;
case ExtraFieldInputType.Number:
case ExtraFieldInputType.Checkbox:
toggleContentDiv(fieldType);
break;
Expand Down Expand Up @@ -116,13 +114,28 @@ document.addEventListener('DOMContentLoaded', () => {
const field = {};
field['type'] = (document.getElementById('newFieldTypeSelect') as HTMLSelectElement).value;
let fieldValue: string;
if (['text', 'date', 'datetime-local', 'email', 'number', 'time', 'url'].includes(field['type'])) {
if (['text', 'date', 'datetime-local', 'email', 'time', 'url'].includes(field['type'])) {
fieldValue = (document.getElementById('newFieldValueInput') as HTMLInputElement).value;
} else if (['select', 'radio'].includes(field['type'])) {
field['options'] = [];
document.querySelectorAll('.newFieldOption').forEach(opt => field['options'].push((opt as HTMLInputElement).value));
document.getElementById('choicesInputDiv').querySelectorAll('input').forEach(opt => field['options'].push((opt as HTMLInputElement).value));
// just take the first one as selected value
fieldValue = field['options'][0];
} else if (field['type'] === 'number') {
fieldValue = (document.getElementById('newFieldValueInput') as HTMLInputElement).value;
field['units'] = [];
document.getElementById('unitChoicesInputDiv').querySelectorAll('input').forEach(opt => {
const unitValue = (opt as HTMLInputElement).value;
// only add non empty values
if (unitValue) {
field['units'].push(unitValue);
}
});
field['unit'] = '';
// if there is at least one value in "units", add it to "unit"
if (field['units'].length > 0) {
field['unit'] = field['units'][0];
}

} else if (field['type'] === 'checkbox') {
fieldValue = (document.getElementById('newFieldCheckboxDefaultSelect') as HTMLSelectElement).value === 'checked' ? 'on' : '';
Expand Down Expand Up @@ -156,10 +169,8 @@ document.addEventListener('DOMContentLoaded', () => {
// ADD OPTION FOR SELECT OR RADIO
} else if (el.matches('[data-action="new-field-add-option"]')) {
const newInput = document.createElement('input');
newInput.classList.add('form-control');
newInput.classList.add('newFieldOption');
newInput.classList.add('mb-1');
document.getElementById('choicesInputDiv').appendChild(newInput);
newInput.classList.add('form-control', 'mb-1');
el.parentElement.querySelector('div').append(newInput);
// SAVE NEW GROUP
} else if (el.matches('[data-action="save-new-fields-group"]')) {
const nameInput = (document.getElementById('newFieldsGroupKeyInput') as HTMLInputElement);
Expand Down
2 changes: 2 additions & 0 deletions src/ts/metadataInterfaces.ts
Expand Up @@ -37,6 +37,8 @@ export interface ExtraFieldProperties {
description?: string;
allow_multi_values?: boolean;
required?: boolean;
unit?: string;
units?: string[];
}

export interface MetadataElabftw {
Expand Down
19 changes: 18 additions & 1 deletion tests/unit/classes/TwigFiltersTest.php
Expand Up @@ -51,16 +51,33 @@ public function testFormatMetadata(): void
"value": "",
"position": 4
},
"number with unit": {
"type": "number",
"value": 12,
"unit": "kPa"
},
"multi select": {
"type": "select",
"allow_multi_values": true,
"value": ["yep", "yip"],
"options": ["yip", "yap", "yep"]
},
"checked checkbox": {
"type": "checkbox",
"value": "on"
}
}
}';
$expected = '<h4 data-action=\'toggle-next\' class=\'mt-4 d-inline togglable-section-title\'><i class=\'fas fa-caret-down fa-fw mr-2\'></i>Undefined group</h4><div><li class="list-group-item"><h5 class="mb-0">first one</h5><h6>first</h6></li><li class="list-group-item"><h5 class="mb-0">second one</h5><h6>second</h6></li><li class="list-group-item"><h5 class="mb-0">unchecked checkbox</h5><h6><input class="d-block" disabled type="checkbox" ></h6></li><li class="list-group-item"><h5 class="mb-0">url current tab</h5><h6><a href="https://example.com" >https://example.com</a></h6></li><li class="list-group-item"><h5 class="mb-0">url default</h5><h6><a href="https://example.com" target="_blank" rel="noopener">https://example.com</a></h6></li><li class="list-group-item"><h5 class="mb-0">last one</h5><span class="smallgray">last position</span><h6>last content</h6></li><li class="list-group-item"><h5 class="mb-0">checked checkbox</h5><h6><input class="d-block" disabled type="checkbox" checked></h6></li></div>';
$expected = '<h4 data-action=\'toggle-next\' class=\'mt-4 d-inline togglable-section-title\'><i class=\'fas fa-caret-down fa-fw mr-2\'></i>Undefined group</h4><div><li class="list-group-item"><h5 class="mb-0">first one</h5><h6>first</h6></li><li class="list-group-item"><h5 class="mb-0">second one</h5><h6>second</h6></li><li class="list-group-item"><h5 class="mb-0">unchecked checkbox</h5><h6><input class="d-block" disabled type="checkbox" ></h6></li><li class="list-group-item"><h5 class="mb-0">url current tab</h5><h6><a href="https://example.com" >https://example.com</a></h6></li><li class="list-group-item"><h5 class="mb-0">url default</h5><h6><a href="https://example.com" target="_blank" rel="noopener">https://example.com</a></h6></li><li class="list-group-item"><h5 class="mb-0">last one</h5><span class="smallgray">last position</span><h6>last content</h6></li><li class="list-group-item"><h5 class="mb-0">number with unit</h5><h6>12 kPa</h6></li><li class="list-group-item"><h5 class="mb-0">multi select</h5><h6>yep, yip</h6></li><li class="list-group-item"><h5 class="mb-0">checked checkbox</h5><h6><input class="d-block" disabled type="checkbox" checked></h6></li></div>';
$this->assertEquals($expected, TwigFilters::formatMetadata($metadataJson));
}

public function testFormatMetadataEmptyExtrafields(): void
{
$metadata = '{"hello": "friend"}';
$this->assertIsString(TwigFilters::formatMetadata($metadata));
}

public function testDecrypt(): void
{
$secret = 'Section 31';
Expand Down

0 comments on commit 7341bb6

Please sign in to comment.