mirror of
https://github.com/edufeed-org/edufeed-web.git
synced 2025-12-10 08:44:39 +00:00
Add Search Option to Concept Schemes
This commit is contained in:
parent
a834ba5acf
commit
09514ad038
2 changed files with 115 additions and 56 deletions
|
|
@ -1887,15 +1887,6 @@ html {
|
||||||
line-height: 1.25rem;
|
line-height: 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-title {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
font-size: 1.25rem;
|
|
||||||
line-height: 1.75rem;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card.image-full :where(figure) {
|
.card.image-full :where(figure) {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-radius: inherit;
|
border-radius: inherit;
|
||||||
|
|
@ -2804,10 +2795,6 @@ details.collapse summary::-webkit-details-marker {
|
||||||
line-height: 1.5rem;
|
line-height: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-normal .card-title {
|
|
||||||
margin-bottom: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.join.join-vertical > :where(*:not(:first-child)):is(.btn) {
|
.join.join-vertical > :where(*:not(:first-child)):is(.btn) {
|
||||||
margin-top: calc(var(--border-btn) * -1);
|
margin-top: calc(var(--border-btn) * -1);
|
||||||
}
|
}
|
||||||
|
|
@ -3429,6 +3416,11 @@ details.collapse summary::-webkit-details-marker {
|
||||||
background-color: var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));
|
background-color: var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bg-orange-400 {
|
||||||
|
--tw-bg-opacity: 1;
|
||||||
|
background-color: rgb(251 146 60 / var(--tw-bg-opacity));
|
||||||
|
}
|
||||||
|
|
||||||
.bg-primary {
|
.bg-primary {
|
||||||
--tw-bg-opacity: 1;
|
--tw-bg-opacity: 1;
|
||||||
background-color: var(--fallback-p,oklch(var(--p)/var(--tw-bg-opacity)));
|
background-color: var(--fallback-p,oklch(var(--p)/var(--tw-bg-opacity)));
|
||||||
|
|
@ -3458,21 +3450,6 @@ details.collapse summary::-webkit-details-marker {
|
||||||
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
|
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.bg-orange-500 {
|
|
||||||
--tw-bg-opacity: 1;
|
|
||||||
background-color: rgb(249 115 22 / var(--tw-bg-opacity));
|
|
||||||
}
|
|
||||||
|
|
||||||
.bg-orange-200 {
|
|
||||||
--tw-bg-opacity: 1;
|
|
||||||
background-color: rgb(254 215 170 / var(--tw-bg-opacity));
|
|
||||||
}
|
|
||||||
|
|
||||||
.bg-orange-400 {
|
|
||||||
--tw-bg-opacity: 1;
|
|
||||||
background-color: rgb(251 146 60 / var(--tw-bg-opacity));
|
|
||||||
}
|
|
||||||
|
|
||||||
.object-contain {
|
.object-contain {
|
||||||
-o-object-fit: contain;
|
-o-object-fit: contain;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
|
|
@ -3500,10 +3477,6 @@ details.collapse summary::-webkit-details-marker {
|
||||||
padding-bottom: 1rem;
|
padding-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pl-2 {
|
|
||||||
padding-left: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-2xl {
|
.text-2xl {
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
line-height: 2rem;
|
line-height: 2rem;
|
||||||
|
|
@ -3571,11 +3544,6 @@ details.collapse summary::-webkit-details-marker {
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hover\:bg-base-200:hover {
|
|
||||||
--tw-bg-opacity: 1;
|
|
||||||
background-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)));
|
|
||||||
}
|
|
||||||
|
|
||||||
.hover\:bg-blue-600:hover {
|
.hover\:bg-blue-600:hover {
|
||||||
--tw-bg-opacity: 1;
|
--tw-bg-opacity: 1;
|
||||||
background-color: rgb(37 99 235 / var(--tw-bg-opacity));
|
background-color: rgb(37 99 235 / var(--tw-bg-opacity));
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
(ns ied.views.add
|
(ns ied.views.add
|
||||||
(:require [re-frame.core :as re-frame]
|
(:require [re-frame.core :as re-frame]
|
||||||
|
[clojure.string :as str]
|
||||||
[reagent.core :as reagent]
|
[reagent.core :as reagent]
|
||||||
[ied.subs :as subs]
|
[ied.subs :as subs]
|
||||||
[ied.events :as events]
|
[ied.events :as events]
|
||||||
|
|
@ -86,26 +87,111 @@
|
||||||
:class "checkbox checkbox-warning"
|
:class "checkbox checkbox-warning"
|
||||||
:on-change (fn [] (re-frame/dispatch [::events/toggle-concept [concept field]]))}])
|
:on-change (fn [] (re-frame/dispatch [::events/toggle-concept [concept field]]))}])
|
||||||
|
|
||||||
|
(defn highlight-match
|
||||||
|
"Wraps the matching part of the text in bold markers, preserving the original capitalization.
|
||||||
|
Arguments:
|
||||||
|
- `text`: The full text (string).
|
||||||
|
- `query`: The search term (string).
|
||||||
|
Returns:
|
||||||
|
- A Hiccup structure with the matching part bolded, or the original text if no match."
|
||||||
|
[text query]
|
||||||
|
(if (and text query)
|
||||||
|
(let [lower-text (str/lower-case text)
|
||||||
|
lower-query (str/lower-case query)
|
||||||
|
index (str/index-of lower-text lower-query)]
|
||||||
|
(if index
|
||||||
|
[:span
|
||||||
|
(subs text 0 index)
|
||||||
|
[:span {:class "font-bold"} (subs text index (+ index (count query)))]
|
||||||
|
(subs text (+ index (count query)))]
|
||||||
|
text))
|
||||||
|
text))
|
||||||
|
|
||||||
(defn concept-label-component
|
(defn concept-label-component
|
||||||
[[concept field]]
|
[[concept field search-input]]
|
||||||
(fn []
|
(fn []
|
||||||
(let [toggled-concepts @(re-frame/subscribe [::subs/toggled-concepts])
|
(let [toggled-concepts @(re-frame/subscribe [::subs/toggled-concepts])
|
||||||
toggled (some #(= (:id %) (:id concept)) toggled-concepts)]
|
toggled (some #(= (:id %) (:id concept)) toggled-concepts)
|
||||||
|
prefLabel (highlight-match (-> concept :prefLabel :de) search-input)]
|
||||||
[:li
|
[:li
|
||||||
(if-let [narrower (:narrower concept)]
|
(if-let [narrower (:narrower concept)]
|
||||||
[:details {:open false}
|
[:details {:open false}
|
||||||
[:summary {:class (when toggled "bg-orange-400 text-black")}
|
[:summary {:class (when toggled "bg-orange-400 text-black")}
|
||||||
[concept-checkbox concept field toggled]
|
[concept-checkbox concept field toggled]
|
||||||
(-> concept :prefLabel :de)]
|
prefLabel]
|
||||||
[:ul {:tabindex "0"}
|
[:ul {:tabindex "0"}
|
||||||
(for [child narrower]
|
(for [child narrower]
|
||||||
^{:key (:id child)} [concept-label-component [child field]])]]
|
^{:key (:id child)} [concept-label-component [child field]])]]
|
||||||
[:a {:class (when toggled "bg-orange-400 text-black")}
|
[:a {:class (when toggled "bg-orange-400 text-black")
|
||||||
|
:on-click (fn [] (re-frame/dispatch [::events/toggle-concept [concept field]]))}
|
||||||
[concept-checkbox concept field toggled]
|
[concept-checkbox concept field toggled]
|
||||||
[:p {:on-click (fn [] (re-frame/dispatch [::events/toggle-concept [concept field]])) } (-> concept :prefLabel :de)]])])))
|
[:p
|
||||||
|
|
||||||
|
prefLabel]])])))
|
||||||
|
|
||||||
|
(defn get-nested
|
||||||
|
"Retrieve all values from nested paths in a node.
|
||||||
|
Arguments:
|
||||||
|
- `node`: The map representing a node.
|
||||||
|
- `paths`: A sequence of keyword paths to retrieve values from the node."
|
||||||
|
[node paths]
|
||||||
|
(map #(get-in node %) paths))
|
||||||
|
|
||||||
|
(defn match-node?
|
||||||
|
"Checks if the query matches any field value in the provided paths.
|
||||||
|
Arguments:
|
||||||
|
- `node`: The map representing a node.
|
||||||
|
- `query`: The search term (string).
|
||||||
|
- `paths`: A sequence of keyword paths to check in the node."
|
||||||
|
[node query paths]
|
||||||
|
(some #(str/includes? (str/lower-case (str %)) (str/lower-case query)) (get-nested node paths)))
|
||||||
|
|
||||||
|
(defn search-concepts
|
||||||
|
"Recursively search for matches in `hasTopConcept` and `narrower`.
|
||||||
|
Arguments:
|
||||||
|
- `cs`: The Concept Scheme
|
||||||
|
- `query`: The search term (string).
|
||||||
|
- `paths`: A sequence of keyword paths to check in each concept node.
|
||||||
|
Returns:
|
||||||
|
- A map with the same structure as `cs`, containing only matching concepts
|
||||||
|
and their parents."
|
||||||
|
[cs query paths]
|
||||||
|
(letfn [(traverse [concept]
|
||||||
|
(let [children (:narrower concept)
|
||||||
|
matched-children (keep traverse children)
|
||||||
|
node-matches (match-node? concept query paths)]
|
||||||
|
(when (or node-matches (seq matched-children))
|
||||||
|
(-> concept
|
||||||
|
(assoc :narrower (when (seq matched-children) matched-children))))))]
|
||||||
|
(if-let [filtered-concepts (keep traverse (:hasTopConcept cs))]
|
||||||
|
(assoc cs :hasTopConcept filtered-concepts)
|
||||||
|
nil)))
|
||||||
|
|
||||||
|
(comment
|
||||||
|
(def data {:id "http://w3id.org/openeduhub/vocabs/locationType/"
|
||||||
|
:type "ConceptScheme"
|
||||||
|
:title {:de "Typ eines Ortes, einer Einrichtung oder Organisation"
|
||||||
|
:en "Type of place, institution or organisation"}
|
||||||
|
:hasTopConcept [{:id "http://w3id.org/openeduhub/vocabs/locationType/bdc2d9f1-bef6-4bf4-844d-4efc1c99ddbe"
|
||||||
|
:prefLabel {:de "Lernort"}
|
||||||
|
:narrower [{:id "http://w3id.org/openeduhub/vocabs/locationType/0d08a1e9-09d4-4024-9b5c-7e4028e28ce5"
|
||||||
|
:prefLabel {:de "Kindergarten/-betreuung"}}
|
||||||
|
{:id "http://w3id.org/openeduhub/vocabs/locationType/1d0965c1-4a3e-4228-a600-bfa1d267e4d9"
|
||||||
|
:prefLabel {:de "Schule"}}
|
||||||
|
{:id "http://w3id.org/openeduhub/vocabs/locationType/729b6724-1dd6-476e-b871-44383ac11ef3"
|
||||||
|
:prefLabel {:de "berufsbildende Schule"}}]}
|
||||||
|
{:id "http://test.com/"
|
||||||
|
:prefLabel {:de "Test"}}]})
|
||||||
|
|
||||||
|
(def fields [[:prefLabel :de] [:prefLabel :en]])
|
||||||
|
(def query "Test")
|
||||||
|
(println (pr-str (search-concepts data query fields))))
|
||||||
|
|
||||||
(defn skos-concept-scheme-multiselect-component
|
(defn skos-concept-scheme-multiselect-component
|
||||||
[[cs field field-title]]
|
[[cs field field-title]]
|
||||||
|
(let [search-input (reagent/atom "")]
|
||||||
|
(fn []
|
||||||
|
(let [filtered-cs (search-concepts cs @search-input [[:prefLabel :de]])] ;; TODO needs to be otherwise configured for multi language stuff
|
||||||
[:div
|
[:div
|
||||||
{:class "dropdown w-full"
|
{:class "dropdown w-full"
|
||||||
:key (:id cs)}
|
:key (:id cs)}
|
||||||
|
|
@ -115,11 +201,16 @@
|
||||||
:class "btn m-1 grow w-full"}
|
:class "btn m-1 grow w-full"}
|
||||||
field-title]
|
field-title]
|
||||||
[:div {:class "dropdown-content z-[1]"}
|
[:div {:class "dropdown-content z-[1]"}
|
||||||
|
|
||||||
[:ul {:class "menu bg-base-200 rounded-box w-96 "
|
[:ul {:class "menu bg-base-200 rounded-box w-96 "
|
||||||
:tabindex "0"}
|
:tabindex "0"}
|
||||||
|
[:input {:class "input"
|
||||||
|
:type "text"
|
||||||
|
:on-change (fn [e]
|
||||||
|
(reset! search-input (-> e .-target .-value)))}]
|
||||||
(doall
|
(doall
|
||||||
(for [concept (:hasTopConcept cs)]
|
(for [concept (:hasTopConcept filtered-cs)]
|
||||||
^{:key (:id concept)} [concept-label-component [concept field]]))]]])
|
^{:key (str @search-input (:id concept))} [concept-label-component [concept field @search-input]]))]]]))))
|
||||||
|
|
||||||
(defn array-fields-component [selected-md-scheme field field-title]
|
(defn array-fields-component [selected-md-scheme field field-title]
|
||||||
(let [array-items-type (get-in selected-md-scheme [field :items :type])
|
(let [array-items-type (get-in selected-md-scheme [field :items :type])
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue