Skip to content

Commit

Permalink
[dsfr-ui] add first steps for dsfr-autocomplete.html
Browse files Browse the repository at this point in the history
  • Loading branch information
ppinette committed Jan 6, 2025
1 parent 0386365 commit c5601f7
Show file tree
Hide file tree
Showing 16 changed files with 23,892 additions and 4,687 deletions.
453 changes: 241 additions & 212 deletions vertigo-ui-dsfr/package-lock.json

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion vertigo-ui-dsfr/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@
"dependencies": {
"@gouvfr/dsfr": "^1.12.1",
"@gouvminint/vue-dsfr": "^7.0.1",
"@trevoreyre/autocomplete": "^3.0.3",
"@trevoreyre/autocomplete-vue": "^3.0.3",
"date-fns": "^4.1.0",
"vue": "^3.4.29"
"vue": "^3.5.13"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.5",
Expand Down
6 changes: 5 additions & 1 deletion vertigo-ui-dsfr/src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,14 @@ import DsfrButtonTooltip from "@/components/DsfrTooltip/DsfrButtonTooltip.vue";
import DsfrLinkTooltip from "@/components/DsfrTooltip/DsfrLinkTooltip.vue";
import DsfrLink from "@/components/DsfrLink/DsfrLink.vue";

import Autocomplete from "@trevoreyre/autocomplete-vue/Autocomplete.vue";

import './utils.css'
import './surcharges.css'

var DSFR = {
install: function (vueApp, options) {
vueApp.use(VueDsfr);
vueApp.use(VueDsfr)

vueApp.component('RouterLink', RouterLink)
vueApp.component('DsfrFacets', DsfrFacets)
Expand All @@ -49,6 +51,8 @@ var DSFR = {
vueApp.component('DsfrButtonTooltip', DsfrButtonTooltip)
vueApp.component('DsfrLinkTooltip', DsfrLinkTooltip)
vueApp.component('DsfrLink', DsfrLink)

vueApp.component('autocomplete', Autocomplete)
},

methods: DsfrMethods,
Expand Down
53 changes: 53 additions & 0 deletions vertigo-ui-dsfr/src/methods.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,59 @@ export default {
} // a label is always a string
});
},
dsfrSearchAutocomplete: function (list, valueField, labelField, componentId, url, minQueryLength, terms) {
if (terms.length < minQueryLength) {
return Promise.resolve([]);
}

return this.$http.post(url, this.objectToFormData({ terms, list, valueField, labelField, CTX: this.$data.vueData.CTX }))
.then((response) => {
return response.data.map((object) => ({
value: object[valueField],
label: object[labelField].toString(), // A label is always a string
}));
})
.catch(() => {
return []; // On error, resolve with an empty array
});
},
dsfrLoadAutocompleteById: function (list, valueField, labelField, componentId, url, objectName, fieldName, rowIndex) {
//Method use when value(id) is set by another way : like Ajax Viewcontext update, other component, ...
//if options already contains the value (id) : we won't reload.
var value
if (rowIndex != null) {
value = this.$data.vueData[objectName][rowIndex][fieldName];
} else {
value = this.$data.vueData[objectName][fieldName];
}

if (Array.isArray(value)) {
value.forEach(element => this.dsfrLoadMissingAutocompleteOption(list, valueField, labelField, componentId, url, element));
} else {
this.dsfrLoadMissingAutocompleteOption(list, valueField, labelField, componentId, url, value);
}

},
dsfrLoadMissingAutocompleteOption: function (list, valueField, labelField, componentId, url, value){
if (!value || (this.$data.componentStates[componentId].options.filter(function (option) { return option.value === value }.bind(this)).length > 0)) {
return
}
this.$data.componentStates[componentId].loading = true;
this.$http.post(url, this.objectToFormData({ value: value, list: list, valueField: valueField, labelField: labelField, CTX: this.$data.vueData.CTX }))
.then(function (response) {
let res = response.data.map(function (object) {
return { value: object[valueField], label: object[labelField].toString() } // a label is always a string
});
this.$data.componentStates[componentId].options = this.$data.componentStates[componentId].options.concat(res);
return this.$data.componentStates[componentId].options;
}.bind(this))
.catch(function (error) {
this.$q.notify(error.response.status + ":" + error.response.statusText);
}.bind(this))
.then(function () {// always executed
this.$data.componentStates[componentId].loading = false;
}.bind(this));
},
dsfrUpdateMenuNavigationActiveState: function () {
this.componentStates?.dsfrHeader?.navItems
.filter(item => item.title)
Expand Down
43 changes: 42 additions & 1 deletion vertigo-ui-dsfr/src/surcharges.css
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,45 @@ td p {

.fr-tabs__tab--active {
background-size: 100% 2px, 1px calc(100% - 1px), 1px calc(100% - 1px), 0 1px;
}
}

/* DSFR Autocomplete */

.autocomplete-result-list {
z-index: 2;
background: #EEEEEE;
border: 1px solid #A0A0A0;
border-radius: 3px;
list-style: none;
padding: 0.2rem;
box-shadow: 2px 2px 10px #F0F0F0;
margin-top: 1px;
}

.autocomplete-result-list li {
padding: .2rem .4rem;
border-radius: 3px;
font-size: 1rem;
font-family: sans-serif;
}

.autocomplete-result:hover, .autocomplete-result[aria-selected=true] {
background: #E0E0E6;
}

[data-position=below] .autocomplete-input[aria-expanded=true] {
border-bottom-color: transparent;
}

[data-position=above] .autocomplete-input[aria-expanded=true] {
border-top-color: transparent;
}

.autocomplete-empty-list {
position: absolute;
z-index: 1;
width: 100%;
top: 100%;
}

/**/
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
<th:block
th:fragment="dsfr-autocomplete-edit(object, field, label, hint, componentId, required, list, valueField, labelField, minQueryLength, noResultLabel, inputUrl, filterUrl, label_attrs, input_attrs)"
vu:alias="dsfr-autocomplete"
vu:selector="${viewMode=='edit' && (readonly==null || !readonly)}"
th:assert="${object} != null and ${field} != null"
th:with="myValueField=${valueField != null ? valueField : model.util.getIdField(list)},
myLabel=${label?:model.util.label(object + '.' + field)},
myLabelField=${labelField != null ? labelField : model.util.getDisplayField(list)},
myRequired=${required != null ? required : model.util.required(object + '.' + field)},
myComponentId=${componentId?:model.util.generateComponentUID('autocomplete', object, field, rowIndex)},
myNoResultLabel=${noResultLabel != null ? noResultLabel : 'Aucun résultat trouvé' },
myInputUrl=${inputUrl != null ? inputUrl : '__@{/autocomplete/_searchByValue}__'},
myFilterUrl=${filterUrl != null ? filterUrl : '__@{/autocomplete/_searchFullText}__'}">

<vu:include-data object="${object}" field="${field}" modifiable/>
<vu:include-data object="${list}" field="${valueField}"/>
<vu:include-data object="${list}" field="${labelField}"/>

<th:block th:attr="objectKey=${model.vContext['componentStates'].addComponentState(myComponentId).addList('options')},
objectKey1=${model.vContext['componentStates'].get(myComponentId).addPrimitive('loading', false)
.addPrimitive('noResult', false)
.addPrimitive('focused', false)
.addPrimitive('field', '')}"/>
<th:block th:if="${rowIndex == null}"
th:with="value=${model.vContext[__${object}__].getTypedValue('__${field}__')},
valueLabel=${value != null ? model.vContext[__${list}__].getById('__${myValueField}__', value)['__${myLabelField}__'] : ''}"
th:attr="objectKey=${model.vContext['componentStates']['__${myComponentId}__']['options'].add( { 'value' : value, 'label': valueLabel})}">
</th:block>
<th:block th:if="${rowIndex != null}" th:each="obj : ${model.vContext[__${object}__]}">
<th:block
th:with="value=${obj.getTypedValue('__${field}__')},
valueLabel=${value != null ? model.vContext[__${list}__].getById('__${myValueField}__', value)['__${myLabelField}__'] : ''}"
th:attr="objectKey=${model.vContext['componentStates']['__${myComponentId}__']['options'].add( { 'value' : value, 'label': valueLabel})}">
</th:block>
</th:block>

<autocomplete th:@vue:mounted="|() => {
let res = dsfrLoadAutocompleteById('${list}', '${myValueField}', '${myLabelField}', '${myComponentId}', '${myInputUrl}','${object}','${field}', ${rowIndex});
$watch(function() {return ${model.util.vueDataKey(object, field, rowIndex)} }, (newValue, oldValue) => {
dsfrLoadAutocompleteById('${list}', '${myValueField}', '${myLabelField}', '${myComponentId}', '${myInputUrl}','${object}','${field}', ${rowIndex});
})
}|"
th::search="|(val) => { return dsfrSearchAutocomplete('${list}', '${myValueField}', '${myLabelField}', '${myComponentId}', '${myFilterUrl}', ${myMinQueryLength}, val)
.then((res) => { componentStates.__${myComponentId}__.noResult = res.length === 0; componentStates.__${myComponentId}__.options = res; return res })
}|"
th::get-result-value="|(res) => { __${model.util.vueDataKey(object, field, rowIndex)}__ = res.value; return res.label } |"
:auto-select="true"
:debounce-time="200"
th:ref="${myComponentId}"
th:attr="__${input_attrs}__"
>
<template
#default="{
rootProps,
inputProps,
inputListeners,
resultListProps,
resultListListeners,
results,
resultProps
}"
>
<div v-bind="rootProps">
<div class="fr-input-group"
th::class="|{ 'fr-input-group--error': hasFieldsError('${object}', '${field}', ${rowIndex}) }|"
>
<label class="fr-label" th:for="${myComponentId}">
<th:block th:text="${myLabel}"></th:block>
<span th:if="${myRequired}" class="required"> *</span>
<span class="fr-hint-text" th:if="${hint ne null}" th:text="${hint}"></span>
</label>

<input class="fr-input"
type="text"
th::class="|{ 'fr-input--error': hasFieldsError('${object}', '${field}', ${rowIndex}) }|"
th:id="${myComponentId}"
th::aria-describedby="| hasFieldsError('${object}', '${field}', ${rowIndex}) ? ${myComponentId} + '-error' : undefined |"
th:required="${myRequired}"
v-bind="inputProps"
@input="(e) => { inputListeners.input(e) }"
th:@blur="|(e) => { inputListeners.blur(e); componentStates.__${myComponentId}__.focused = false }|"
th:@focus="|(e) => { inputListeners.focus(e); componentStates.__${myComponentId}__.focused = true }|"
@keydown="(e) => { inputListeners.keydown(e) }"
>

<ul th:v-if="| componentStates.__${myComponentId}__.noResult && componentStates.__${myComponentId}__.focused && results.length === 0 |"
class="autocomplete-result-list autocomplete-empty-list">
<li class="autocomplete-result" th:text="${noResultLabel}">

</li>
</ul>

<div th:v-if="|hasFieldsError('${object}', '${field}', ${rowIndex})|"
class="fr-messages-group"
role="alert"
aria-live="polite"
>
<p th:id="${myComponentId} + '-error'"
th:v-text="|getErrorMessage('${object}', '${field}', ${rowIndex})|"></p>
</div>
</div>
<input type="hidden"
th::name="${model.util.contextKey(object, field, rowIndex)}"
th:v-bind:value="${model.util.vueDataKey(object, field, rowIndex)}">
<ul v-bind="resultListProps" v-on="resultListListeners">
<li
th:v-for="|(result, index) in componentStates.__${myComponentId}__.options|"
:key="`autocomplete-result-${index}`"
:id="`autocomplete-result-${index}`"
role="option"
class="autocomplete-result"
v-bind="resultProps[index]"
:data-result-index="index"
>
{{ result.label }}
</li>
</ul>
</div>
</template>
</autocomplete>
</th:block>
Loading

0 comments on commit c5601f7

Please sign in to comment.