diff --git a/src/lib/components/variables/VariableInput.svelte b/src/lib/components/variables/VariableInput.svelte index e0bfdff2c716366438b22c3ee3fe78f3e60f1209..a03e877138ccd4c3d045f13098702c5dc59eb48f 100644 --- a/src/lib/components/variables/VariableInput.svelte +++ b/src/lib/components/variables/VariableInput.svelte @@ -27,6 +27,7 @@ } from "$lib/variables" export let date: string + export let highlight: string[] | undefined = undefined export let inputInstantsByVariableName: { [name: string]: Set<string> } @@ -259,6 +260,21 @@ } } + function highlightKeywords(content: string, keywords: string[] | undefined) { + try { + if (keywords === undefined || keywords.length === 0) { + return content + } + + return content.replace( + new RegExp(`(${keywords.join("|")})`, "gi"), + `<span class="bg-le-jaune">$1</span>`, + ) + } catch (e) { + console.error(`RegExp error ${e}`) + } + } + function isFirstEncounter(name: string, populationId: string): boolean { const encounteredPopulationId = encountered.get(name) if (encounteredPopulationId === undefined) { @@ -308,7 +324,7 @@ <div class="flex items-start justify-between"> <h1 class="flex w-4/5 items-center text-base font-bold"> <span> - {variable.label ?? variable.name} + {@html highlightKeywords(variable.label ?? variable.name, highlight)} </span> <a class="ml-1 text-gray-400 hover:text-gray-800" diff --git a/src/lib/components/variables/VariableReferredInputs.svelte b/src/lib/components/variables/VariableReferredInputs.svelte index 1ebc9097c68ab473dc316eabee435c1ca6833e1f..b90f27ef997fd94a49a0de40f43d3c0cfb1d9ff2 100644 --- a/src/lib/components/variables/VariableReferredInputs.svelte +++ b/src/lib/components/variables/VariableReferredInputs.svelte @@ -2,9 +2,9 @@ import type { Variable } from "@openfisca/json-model" import { getVariableFormula } from "@openfisca/json-model" - import VariableInput from "./VariableInput.svelte" - + import VariableInput from "$lib/components/variables/VariableInput.svelte" import type { Situation } from "$lib/situations" + import { diacritiquesMinuscule } from "$lib/strings" import type { ValuesByCalculationNameByVariableName } from "$lib/variables" export let date: string @@ -19,6 +19,8 @@ export let year: number let openDirectInputs = true + let allInputsQuery = "" + let directInputsQuery = "" $: directVariablesName = new Set( getVariableFormula(variable, date)?.variables ?? [], @@ -29,6 +31,45 @@ ) $: openAllInputs = directInputs.length === 0 + + $: allInputsSearchTerms = parseSearch(allInputsQuery) + + $: directInputsSearchTerms = parseSearch(directInputsQuery) + + function escapeRegex(s: string): string { + return s.replace(/[/\-\\^$*+?.()|[\]{}]/g, "\\$&") + } + + function filter(inputs: Variable[], terms: string[] | undefined) { + if (terms !== undefined && terms.length > 0) { + return inputs.filter((input) => + terms.every((term) => new RegExp(term, "gi").test(input.label)), + ) + } + return inputs + } + + function parseSearch(search: string | undefined) { + let terms = search?.split(/\s+/).filter(Boolean) + + if (terms === undefined) { + return [] + } + + let regexOK = true + try { + terms.forEach((term) => new RegExp(term, "gi").test("")) + } catch (e) { + regexOK = false + } + + return terms.map((term) => + (regexOK ? term : escapeRegex(term)).replace( + new RegExp(Object.keys(diacritiquesMinuscule).join("|"), "gi"), + (m) => diacritiquesMinuscule[m] || m, + ), + ) + } </script> <h2 class="mb-2 ml-4 mr-4 pt-3 text-xl md:mr-0"> @@ -71,14 +112,39 @@ </h2> {#if openDirectInputs} + <div + class="flex items-center gap-1.5 ml-4 overflow-hidden rounded-t-md bg-white border-b-2 md:border-b-4 border-b-[#A6A00C]" + > + <iconify-icon + class="ml-1 md:ml-3 self-center p-1 text-lg text-black" + icon="ri-search-line" + /> + <input + autocomplete="off" + bind:value={directInputsQuery} + class="w-full px-1 py-1.5 md:py-2.5 border-none bg-transparent text-gray-900 placeholder-gray-400 !ring-transparent focus:outline-none 2xl:text-xl" + placeholder="Rechercher des caractéristiques écrites dans la formule du dispositif..." + type="search" + /> + {#if directInputsQuery.length > 0} + <button + class="md:mr-1.5 p-2 rounded-full hover:bg-black hover:bg-opacity-10 active:bg-black active:bg-opacity-20 transition-all duration-200 ease-out-back" + on:click={() => (directInputsQuery = "")} + > + <iconify-icon class="block text-xl" icon="ri-close-line" /> + </button> + {/if} + </div> + <div class="ml-4 mr-4 md:mr-0"> <ul> - {#each directInputs as input} + {#each filter(directInputs, directInputsSearchTerms) as input} <li class="my-4 flex-col items-baseline rounded bg-gray-200 p-1 py-1 text-base text-black md:p-2" > <VariableInput {date} + highlight={directInputsSearchTerms} bind:inputInstantsByVariableName bind:situation {situationIndex} @@ -110,14 +176,39 @@ </h2> {#if openAllInputs} + <div + class="flex items-center gap-1.5 ml-4 overflow-hidden rounded-t-md bg-white border-b-2 md:border-b-4 border-b-[#A6A00C]" + > + <iconify-icon + class="ml-1 md:ml-3 self-center p-1 text-lg text-black" + icon="ri-search-line" + /> + <input + autocomplete="off" + bind:value={allInputsQuery} + class="w-full px-1 py-1.5 md:py-2.5 border-none bg-transparent text-gray-900 placeholder-gray-400 !ring-transparent focus:outline-none 2xl:text-xl" + placeholder="Rechercher des caractéristiques directement influentes..." + type="search" + /> + {#if allInputsQuery.length > 0} + <button + class="md:mr-1.5 p-2 rounded-full hover:bg-black hover:bg-opacity-10 active:bg-black active:bg-opacity-20 transition-all duration-200 ease-out-back" + on:click={() => (allInputsQuery = "")} + > + <iconify-icon class="block text-xl" icon="ri-close-line" /> + </button> + {/if} + </div> + <div class="ml-4 mr-4 md:mr-0"> <ul> - {#each inputs as input} + {#each filter(inputs, allInputsSearchTerms) as input} <li class="my-4 flex-col items-baseline rounded bg-gray-200 p-1 py-1 text-base text-black md:p-2" > <VariableInput {date} + highlight={allInputsSearchTerms} bind:inputInstantsByVariableName bind:situation {situationIndex} diff --git a/src/lib/strings.ts b/src/lib/strings.ts index c7d833d9fce13050f4f2d19360352bc223b290cb..5e981daeb08c7de0fe4a90503fa79280305742e9 100644 --- a/src/lib/strings.ts +++ b/src/lib/strings.ts @@ -1,5 +1,20 @@ import originalSlugify from "slug" +export const diacritiquesMinuscule: { [letter: string]: string } = { + ae: "(ae|æ)", + oe: "(oe|œ)", + a: "(a|â|ä|à)", + c: "(c|ç)", + e: "(e|é|ê|ë|è)", + i: "(i|î|ï)", + o: "(o|ô|ö)", + u: "(u|û|ü|ù)", + y: "(y|ÿ)", + "'": "('|‘|’)", + "‘": "(‘|'|’)", + "’": "(’|'|‘)", +} + const slugifyCharmap = { ...originalSlugify.defaults.charmap, "'": " ", diff --git a/src/lib/variables.ts b/src/lib/variables.ts index c37cca94f33fc22da26c7a81f7ac2eba013758e8..8a6a2b2857945b0617ef2847a5fb135526a20a91 100644 --- a/src/lib/variables.ts +++ b/src/lib/variables.ts @@ -872,10 +872,17 @@ export function getVariableParametersName( } export function* iterVariableInputVariables( - variable: Variable, + variable: Variable | undefined, date: string, encounteredVariablesName: Set<string> = new Set(), ): Generator<Variable, void> { + if (variable === undefined) { + console.error( + `iterVariableInputVariables(undefined, ...): Given variable is undefined`, + ) + return + } + const name = variable.name if (encounteredVariablesName.has(name)) { return