tethys.backend/resources/js/Components/SearchCategoryAutocomplete.vue
Arno Kaimbacher f67b736a88
Some checks failed
CI Pipeline / japa-tests (push) Failing after 1m0s
feat: Enhance dataset management and improve frontend components
- Added preloads 'allowed_extensions_mimetypes' and 'dependent_array_min_length' in adonisrc.ts
- Updated @symfony/webpack-encore from ^4.6.1 to ^5.0.1
- AdminuserController: Implemented pagination for 10 records in index method
- Enabled reviewers to reject datasets to editors with email notifications (DatasetController.ts)
- Submitter DatasetController: Files now loaded in ascending order (sort_order) in edit mode
- file.ts: Removed serialization of fileData due to browser issues
- Modified FileUpload.vue to mark already uploaded files as deleted
- Improved keyword search in SearchCategoryAutocomplete.vue
- Started development on Category.vue for submitters to categorize DDC
- Added new route /dataset/categorize in routes.ts
- Introduced 2 new rules in start/rules: allowed_extensions_mimetypes.ts and dependent_array_min_length.ts
- Performed npm updates
2024-11-29 15:46:26 +01:00

418 lines
20 KiB
Vue

<template>
<div class="relative">
<div class="flex">
<!-- <label for="search-dropdown" class="mb-2 text-sm font-medium text-gray-900 sr-only dark:text-white">Your Email</label> -->
<div class="relative" data-te-dropdown-ref>
<button id="states-button" data-dropdown-toggle="dropdown-states"
class="whitespace-nowrap h-12 z-10 inline-flex items-center py-2.5 px-4 text-sm font-medium text-center text-gray-500 bg-gray-100 border border-gray-300 rounded-l-lg hover:bg-gray-200 focus:ring-4 focus:outline-none focus:ring-gray-100 dark:bg-gray-700 dark:hover:bg-gray-600 dark:focus:ring-gray-700 dark:text-white dark:border-gray-600"
type="button" @click.prevent="showStates">
<!-- <svg aria-hidden="true" class="h-3 mr-2" viewBox="0 0 15 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.5" width="14" height="12" rx="2" fill="white" />
<mask id="mask0_12694_49953" style="mask-type: alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="15" height="12">
<rect x="0.5" width="14" height="12" rx="2" fill="white" />
</mask>
<g mask="url(#mask0_12694_49953)">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M14.5 0H0.5V0.8H14.5V0ZM14.5 1.6H0.5V2.4H14.5V1.6ZM0.5 3.2H14.5V4H0.5V3.2ZM14.5 4.8H0.5V5.6H14.5V4.8ZM0.5 6.4H14.5V7.2H0.5V6.4ZM14.5 8H0.5V8.8H14.5V8ZM0.5 9.6H14.5V10.4H0.5V9.6ZM14.5 11.2H0.5V12H14.5V11.2Z"
fill="#D02F44"
/>
<rect x="0.5" width="6" height="5.6" fill="#46467F" />
<g filter="url(#filter0_d_12694_49953)">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M1.83317 1.20005C1.83317 1.42096 1.68393 1.60005 1.49984 1.60005C1.31574 1.60005 1.1665 1.42096 1.1665 1.20005C1.1665 0.979135 1.31574 0.800049 1.49984 0.800049C1.68393 0.800049 1.83317 0.979135 1.83317 1.20005ZM3.1665 1.20005C3.1665 1.42096 3.01727 1.60005 2.83317 1.60005C2.64908 1.60005 2.49984 1.42096 2.49984 1.20005C2.49984 0.979135 2.64908 0.800049 2.83317 0.800049C3.01727 0.800049 3.1665 0.979135 3.1665 1.20005ZM4.1665 1.60005C4.3506 1.60005 4.49984 1.42096 4.49984 1.20005C4.49984 0.979135 4.3506 0.800049 4.1665 0.800049C3.98241 0.800049 3.83317 0.979135 3.83317 1.20005C3.83317 1.42096 3.98241 1.60005 4.1665 1.60005ZM5.83317 1.20005C5.83317 1.42096 5.68393 1.60005 5.49984 1.60005C5.31574 1.60005 5.1665 1.42096 5.1665 1.20005C5.1665 0.979135 5.31574 0.800049 5.49984 0.800049C5.68393 0.800049 5.83317 0.979135 5.83317 1.20005ZM2.1665 2.40005C2.3506 2.40005 2.49984 2.22096 2.49984 2.00005C2.49984 1.77913 2.3506 1.60005 2.1665 1.60005C1.98241 1.60005 1.83317 1.77913 1.83317 2.00005C1.83317 2.22096 1.98241 2.40005 2.1665 2.40005ZM3.83317 2.00005C3.83317 2.22096 3.68393 2.40005 3.49984 2.40005C3.31574 2.40005 3.1665 2.22096 3.1665 2.00005C3.1665 1.77913 3.31574 1.60005 3.49984 1.60005C3.68393 1.60005 3.83317 1.77913 3.83317 2.00005ZM4.83317 2.40005C5.01726 2.40005 5.1665 2.22096 5.1665 2.00005C5.1665 1.77913 5.01726 1.60005 4.83317 1.60005C4.64908 1.60005 4.49984 1.77913 4.49984 2.00005C4.49984 2.22096 4.64908 2.40005 4.83317 2.40005ZM5.83317 2.80005C5.83317 3.02096 5.68393 3.20005 5.49984 3.20005C5.31574 3.20005 5.1665 3.02096 5.1665 2.80005C5.1665 2.57914 5.31574 2.40005 5.49984 2.40005C5.68393 2.40005 5.83317 2.57914 5.83317 2.80005ZM4.1665 3.20005C4.3506 3.20005 4.49984 3.02096 4.49984 2.80005C4.49984 2.57914 4.3506 2.40005 4.1665 2.40005C3.98241 2.40005 3.83317 2.57914 3.83317 2.80005C3.83317 3.02096 3.98241 3.20005 4.1665 3.20005ZM3.1665 2.80005C3.1665 3.02096 3.01727 3.20005 2.83317 3.20005C2.64908 3.20005 2.49984 3.02096 2.49984 2.80005C2.49984 2.57914 2.64908 2.40005 2.83317 2.40005C3.01727 2.40005 3.1665 2.57914 3.1665 2.80005ZM1.49984 3.20005C1.68393 3.20005 1.83317 3.02096 1.83317 2.80005C1.83317 2.57914 1.68393 2.40005 1.49984 2.40005C1.31574 2.40005 1.1665 2.57914 1.1665 2.80005C1.1665 3.02096 1.31574 3.20005 1.49984 3.20005ZM2.49984 3.60005C2.49984 3.82096 2.3506 4.00005 2.1665 4.00005C1.98241 4.00005 1.83317 3.82096 1.83317 3.60005C1.83317 3.37913 1.98241 3.20005 2.1665 3.20005C2.3506 3.20005 2.49984 3.37913 2.49984 3.60005ZM3.49984 4.00005C3.68393 4.00005 3.83317 3.82096 3.83317 3.60005C3.83317 3.37913 3.68393 3.20005 3.49984 3.20005C3.31574 3.20005 3.1665 3.37913 3.1665 3.60005C3.1665 3.82096 3.31574 4.00005 3.49984 4.00005ZM5.1665 3.60005C5.1665 3.82096 5.01726 4.00005 4.83317 4.00005C4.64908 4.00005 4.49984 3.82096 4.49984 3.60005C4.49984 3.37913 4.64908 3.20005 4.83317 3.20005C5.01726 3.20005 5.1665 3.37913 5.1665 3.60005ZM5.49984 4.80005C5.68393 4.80005 5.83317 4.62096 5.83317 4.40005C5.83317 4.17913 5.68393 4.00005 5.49984 4.00005C5.31574 4.00005 5.1665 4.17913 5.1665 4.40005C5.1665 4.62096 5.31574 4.80005 5.49984 4.80005ZM4.49984 4.40005C4.49984 4.62096 4.3506 4.80005 4.1665 4.80005C3.98241 4.80005 3.83317 4.62096 3.83317 4.40005C3.83317 4.17913 3.98241 4.00005 4.1665 4.00005C4.3506 4.00005 4.49984 4.17913 4.49984 4.40005ZM2.83317 4.80005C3.01727 4.80005 3.1665 4.62096 3.1665 4.40005C3.1665 4.17913 3.01727 4.00005 2.83317 4.00005C2.64908 4.00005 2.49984 4.17913 2.49984 4.40005C2.49984 4.62096 2.64908 4.80005 2.83317 4.80005ZM1.83317 4.40005C1.83317 4.62096 1.68393 4.80005 1.49984 4.80005C1.31574 4.80005 1.1665 4.62096 1.1665 4.40005C1.1665 4.17913 1.31574 4.00005 1.49984 4.00005C1.68393 4.00005 1.83317 4.17913 1.83317 4.40005Z"
fill="url(#paint0_linear_12694_49953)"
/>
</g>
</g>
<defs>
<filter
id="filter0_d_12694_49953"
x="1.1665"
y="0.800049"
width="4.6665"
height="5"
filterUnits="userSpaceOnUse"
color-interpolation-filters="sRGB"
>
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feOffset dy="1" />
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.06 0" />
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_12694_49953" />
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_12694_49953" result="shape" />
</filter>
<linearGradient
id="paint0_linear_12694_49953"
x1="1.1665"
y1="0.800049"
x2="1.1665"
y2="4.80005"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="white" />
<stop offset="1" stop-color="#F0F0F0" />
</linearGradient>
</defs>
</svg> -->
<!-- eng -->
{{ language }}
<svg aria-hidden="true" class="w-4 h-4 ml-1" fill="currentColor" viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clip-rule="evenodd"></path>
</svg>
</button>
<!-- class="w-full overflow-visible z-10 bg-white divide-y divide-gray-100 rounded-lg shadow w-44 dark:bg-gray-700"-->
<div id="dropdown-states" v-show="statesToggle"
class="absolute z-[1000] float-left m-0 min-w-max list-none overflow-hidden rounded-lg border-none bg-white bg-clip-padding text-left text-base shadow-lg dark:bg-neutral-700">
<ul class="py-2 text-sm text-gray-700 dark:text-gray-200" aria-labelledby="states-button">
<li v-for="(item, index) in dropDownStates" :key="index" @click.prevent="setLanguage(item)">
<button type="button"
class="inline-flex w-full px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-600 dark:hover:text-white">
<div class="inline-flex items-center">
<span v-html="item.svg"></span>
{{ item.name }}
</div>
</button>
</li>
</ul>
</div>
</div>
<div class="w-full relative">
<!-- :class="inputElClass" -->
<!-- class="block p-2.5 w-full z-20 text-sm text-gray-900 bg-gray-50 rounded-r-lg border-l-gray-50 border-l-2 border border-gray-300 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-l-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:border-blue-500" -->
<input v-model="computedValue" type="text" :name="props.name" autocomplete="off" :class="inputElClass"
placeholder="Search Keywords..." required @input="handleInput" />
<!-- v-model="data.search" -->
<svg class="w-4 h-4 absolute left-2.5 top-3.5" v-show="computedValue.length < 2"
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
<svg class="w-4 h-4 absolute left-2.5 top-3.5" v-show="computedValue.length >= 2"
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" @click="() => {
computedValue = '';
data.isOpen = false;
}
">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</div>
<slot />
</div>
<ul v-if="data.isOpen"
class="absolute absolute z-[1000] float-left m-0 list-none bg-white dark:bg-slate-800 m-0 max-h-32 overflow-y-auto scroll-smooth min-w-full">
<li class="leading-3 pl-4 py-3 border-b-2 line border-gray-100 relative cursor-pointer hover:bg-yellow-50 hover:text-gray-900"
v-for="(item, index) in data.results" @click.prevent="setResult(item)" :key="index">
<!-- <a href="${BASE}?uri=${a.s.value}&lang=${USER_LANG}"> -->
<strong class="text-sm"> {{ item.title.value }}</strong>
<!-- </a> -->
<br />
<!-- <span class="searchPropTyp">URI: </span>
<span class="searchResultURI text-info"> {{ item.s.value }} </span> -->
<br />
<!-- <p class="searchResultText">{{ createSearchResultsText(item.text.value, searchText) }}</p> -->
</li>
</ul>
</div>
</template>
<script setup lang="ts">
import { reactive, ref, computed, Ref } from 'vue';
import axios from 'axios';
let props = defineProps({
name: {
type: String,
required: false,
default: 'autocomplete',
},
// source: {
// type: [String, Array, Function],
// required: true,
// default: '',
// },
label: {
type: String,
required: false,
default: 'name',
},
responseProperty: {
type: String,
required: false,
default: 'name',
},
placeholder: {
type: String,
default: null,
},
icon: {
type: String,
default: null,
},
modelValue: {
type: String,
default: '',
},
required: Boolean,
borderless: Boolean,
transparent: Boolean,
ctrlKFocus: Boolean,
});
const emit = defineEmits(['update:modelValue', 'subject']);
let computedValue = computed({
get: () => props.modelValue,
set: (value) => {
// props.modelValue = value;
emit('update:modelValue', value);
},
});
const inputElClass = computed(() => {
// class="block p-2.5 w-full z-20 text-sm text-gray-900 bg-gray-50 rounded-r-lg border-l-gray-50 border-l-2 border border-gray-300
// focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-l-gray-700 dark:border-gray-600
// dark:placeholder-gray-400 dark:text-white dark:focus:border-blue-500"
const base = [
'block p-2.5 w-full z-20 text-sm text-gray-900 bg-gray-50 rounded-r-lg',
'dark:placeholder-gray-400 dark:text-white dark:focus:border-blue-500',
'h-12',
props.borderless ? 'border-0' : 'border',
props.transparent ? 'bg-transparent' : 'bg-white dark:bg-slate-800',
// props.isReadOnly ? 'bg-gray-50 dark:bg-slate-600' : 'bg-white dark:bg-slate-800',
];
// if (props.icon) {
base.push('pl-10');
// }
return base;
});
// let search = ref('');
let data = reactive({
// search: search,
isOpen: false,
results: [] as Array<any>,
});
let error = ref('');
// let selectedIndex: Ref<number> = ref(0);
// const ul: Ref<Array<HTMLLIElement>> = ref([]);
let dropDownStates = reactive([
{
name: 'English',
label: 'en',
svg: `
<svg aria-hidden="true" class="h-3.5 w-3.5 rounded-full mr-2" xmlns="http://www.w3.org/2000/svg" id="flag-icons-us" viewBox="0 0 512 512">
<path fill="#bd3d44" d="M0 0h640v480H0"/>
<path stroke="#fff" stroke-width="37" d="M0 55.3h640M0 129h640M0 203h640M0 277h640M0 351h640M0 425h640"/>
<path fill="#192f5d" d="M0 0h364.8v258.5H0"/>
<marker id="a" markerHeight="30" markerWidth="30">
<path fill="#fff" d="m14 0 9 27L0 10h28L5 27z"/>
</marker>
<path fill="none" marker-mid="url(#a)" d="m0 0 16 11h61 61 61 61 60L47 37h61 61 60 61L16 63h61 61 61 61 60L47 89h61 61 60 61L16 115h61 61 61 61 60L47 141h61 61 60 61L16 166h61 61 61 61 60L47 192h61 61 60 61L16 218h61 61 61 61 60L0 0"/>
</svg>`,
},
{
name: 'German',
label: 'de',
svg: `
<svg aria-hidden="true" class="h-3.5 w-3.5 rounded-full mr-2" xmlns="http://www.w3.org/2000/svg" id="flag-icons-at" viewBox="0 0 512 512">
<g fill-rule="evenodd">
<path fill="#fff" d="M640 480H0V0h640z"/>
<path fill="#c8102e" d="M640 480H0V320h640zm0-319.9H0V.1h640z"/>
</g>
</svg>`,
},
]);
let statesToggle: Ref<boolean> = ref(false);
const showStates = () => {
// let map: Map = this.mapService.getMap(this.mapId);
// const bounds: LatLngBoundsExpression = toLatLngBounds(this.southWest, this.northEast);
// map.fitBounds(bounds);
// if (_enabled.value == true) {
// disable();
// } else {
// enable();
// }
statesToggle.value = !statesToggle.value;
};
let language = ref('de');
const setLanguage = (item) => {
language.value = item.label;
// console.log(language.value);
// close selection list
statesToggle.value = false;
// data.search = '';
computedValue.value = '';
data.isOpen = false;
};
const ENDPOINT = 'https://resource.geolba.ac.at/PoolParty/sparql/keyword';
// const USER_LANG = 'de';
// watch(search, async () => {
// await onChange();
// });
async function handleInput(e: Event) {
const target = <HTMLInputElement>e.target;
console.log(target.value);
if (computedValue.value.length >= 2) {
data.isOpen = true;
return await request(ENDPOINT, computedValue.value);
} else {
data.results = [];
data.isOpen = false;
}
}
// async function onChange() {
// if (!data.search) return false;
// // selectedIndex.value = 0;
// if (data.search.length >= 2) {
// data.isOpen = true;
// return await request(ENDPOINT, data.search);
// } else {
// data.results = [];
// data.isOpen = false;
// }
// }
// Function to execute the SPARQL query against the endpoint
async function request(url: string, param: string) {
try {
// Create SPARQL query to search for concepts by keyword
let query = encodeURIComponent(`
PREFIX dcterms:<http://purl.org/dc/terms/>
PREFIX skos:<http://www.w3.org/2004/02/skos/core#>
SELECT DISTINCT ?s ?title ?text
WHERE {
VALUES ?n {"${sparqlEncode(param.toLowerCase())}"}
VALUES ?p { skos:prefLabel skos:altLabel }
?s a skos:Concept; ?p ?lEN .
FILTER((lang(?lEN)="en"))
# New filter added to restrict the URIs to 'ncl/geoera/keyword'
FILTER(regex(str(?s), 'ncl/geoera/keyword'))
OPTIONAL{?s ?p ?l . FILTER(lang(?l)="${language.value}")}
BIND(COALESCE(?l, ?lEN) AS ?L) .
FILTER(regex(?L, ?n, "i"))
# Exclude "(category)" in titles
FILTER(!regex(?L, "\(category\)", "i"))
FILTER(!regex(?L, "\(kategorie\)", "i"))
?s skos:prefLabel ?plEN . FILTER((lang(?plEN)="en"))
OPTIONAL{?s skos:prefLabel ?pl . FILTER(lang(?pl)="${language.value}")}
BIND(COALESCE(?pl, ?plEN) AS ?title)
BIND(CONCAT(STR(?p), "|", STR(?L)) AS ?text)
BIND(IF(?p = skos:prefLabel, 1, 2) AS ?sort)
}
ORDER BY ?sort
LIMIT 100`);
let response = await searchTerm(url + '?query=' + query + '&format=application/json'); // Execute query
error.value = '';
data.results = getResults(response); // Process results and store them
} catch (error) {
error.value = error.message; // Handle any errors in fetching the data
}
}
// async function request(url: string, param: string) {
// try {
// let query = encodeURIComponent(`
// PREFIX skos:<http://www.w3.org/2004/02/skos/core#>
// select distinct (?label as ?title) ?s (strlen(str(?label)) as ?strlen)
// where
// {
// VALUES ?n {"${sparqlEncode(param.toLowerCase())}"}
// ?s skos:prefLabel ?label .
// filter(lang(?label)='${language.value}')
// filter(regex(?label, ?n, "i")) # Case-insensitive regex match
// }
// order by ?label ?strlen
// `);
// let response = await searchTerm(url + '?query=' + query + '&format=application/json');
// error.value = '';
// data.results = getResults(response);
// // // this.results = res.data;
// // // this.loading = false;
// } catch (error) {
// error.value = error.message;
// // this.loading = false;
// }
// }
async function searchTerm(term: string): Promise<any> {
let res = await axios.get(term);
// console.log(res.data);
return res.data; //.response;//.docs;
}
function getResults(response) {
if (Array.isArray(response.results.bindings)) {
return response.results.bindings;
}
return [];
}
function sparqlEncode(str: string) {
var hex, i;
str = str.toLowerCase();
var result = '';
for (i = 0; i < str.length; i++) {
hex = str.charCodeAt(i);
if (hex < 32 || hex > 128) result += '\\u' + ('000' + hex.toString(16)).slice(-4);
else result += str.charAt(i);
}
return result;
}
function setResult(item) {
// search.value = item.title.value;
computedValue.value = item.title.value;
clear();
// this.$emit('person', person);
emit('subject', {
language: language.value,
uri: item.s.value
});
}
function clear() {
// data.search = '';
data.isOpen = false;
data.results = [];
error.value = '';
// this.$emit("clear");
}
</script>