diff --git a/resources/public/css/output.css b/resources/public/css/output.css index 266f085..3a91300 100644 --- a/resources/public/css/output.css +++ b/resources/public/css/output.css @@ -1887,15 +1887,6 @@ html { 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) { overflow: hidden; border-radius: inherit; @@ -2804,10 +2795,6 @@ details.collapse summary::-webkit-details-marker { line-height: 1.5rem; } -.card-normal .card-title { - margin-bottom: 0.75rem; -} - .join.join-vertical > :where(*:not(:first-child)):is(.btn) { 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))); } +.bg-orange-400 { + --tw-bg-opacity: 1; + background-color: rgb(251 146 60 / var(--tw-bg-opacity)); +} + .bg-primary { --tw-bg-opacity: 1; 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)); } -.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 { -o-object-fit: contain; object-fit: contain; @@ -3500,10 +3477,6 @@ details.collapse summary::-webkit-details-marker { padding-bottom: 1rem; } -.pl-2 { - padding-left: 0.5rem; -} - .text-2xl { font-size: 1.5rem; line-height: 2rem; @@ -3571,11 +3544,6 @@ details.collapse summary::-webkit-details-marker { 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 { --tw-bg-opacity: 1; background-color: rgb(37 99 235 / var(--tw-bg-opacity)); diff --git a/src/ied/views/add.cljs b/src/ied/views/add.cljs index 903f0f4..8641404 100644 --- a/src/ied/views/add.cljs +++ b/src/ied/views/add.cljs @@ -1,5 +1,6 @@ (ns ied.views.add (:require [re-frame.core :as re-frame] + [clojure.string :as str] [reagent.core :as reagent] [ied.subs :as subs] [ied.events :as events] @@ -86,40 +87,130 @@ :class "checkbox checkbox-warning" :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 - [[concept field]] + [[concept field search-input]] (fn [] (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 (if-let [narrower (:narrower concept)] [:details {:open false} [:summary {:class (when toggled "bg-orange-400 text-black")} [concept-checkbox concept field toggled] - (-> concept :prefLabel :de)] + prefLabel] [:ul {:tabindex "0"} (for [child narrower] ^{: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] - [: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 [[cs field field-title]] - [:div - {:class "dropdown w-full" - :key (:id cs)} - [:div - {:tabIndex "0" - :role "button" - :class "btn m-1 grow w-full"} - field-title] - [:div {:class "dropdown-content z-[1]"} - [:ul {:class "menu bg-base-200 rounded-box w-96 " - :tabindex "0"} - (doall - (for [concept (:hasTopConcept cs)] - ^{:key (:id concept)} [concept-label-component [concept field]]))]]]) + (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 + {:class "dropdown w-full" + :key (:id cs)} + [:div + {:tabIndex "0" + :role "button" + :class "btn m-1 grow w-full"} + field-title] + [:div {:class "dropdown-content z-[1]"} + + [:ul {:class "menu bg-base-200 rounded-box w-96 " + :tabindex "0"} + [:input {:class "input" + :type "text" + :on-change (fn [e] + (reset! search-input (-> e .-target .-value)))}] + (doall + (for [concept (:hasTopConcept filtered-cs)] + ^{:key (str @search-input (:id concept))} [concept-label-component [concept field @search-input]]))]]])))) (defn array-fields-component [selected-md-scheme field field-title] (let [array-items-type (get-in selected-md-scheme [field :items :type])