Files
integreat/src/clj/auto_ap/ssr/components/inputs.clj

228 lines
9.8 KiB
Clojure

(ns auto-ap.ssr.components.inputs
(:require
[hiccup2.core :as hiccup]
[auto-ap.ssr.hiccup-helper :as hh]
[clojure.string :as str]
[auto-ap.ssr.svg :as svg]
[auto-ap.ssr.hx :as hx]))
(def default-input-classes
["bg-gray-50" "border" "text-sm" "rounded-lg" "" "block"
"p-2.5" "border-gray-300" "text-gray-900" "focus:ring-blue-500" "focus:border-blue-500"
"dark:bg-gray-700" "dark:border-gray-600" "dark:placeholder-gray-400" "dark:text-white"
"dark:focus:ring-blue-500" "dark:focus:border-blue-500"
"group-[.has-error]:bg-red-50" "group-[.has-error]:border-red-500" "group-[.has-error]:text-red-900"
"group-[.has-error]:placeholder-red-700" "group-[.has-error]:focus:ring-red-500"
"group-[.has-error]:dark:bg-gray-700" "group-[.has-error]:focus:border-red-500"
"group-[.has-error]:dark:text-red-500" "group-[.has-error]:dark:placeholder-red-500"
"group-[.has-error]:dark:border-red-500"])
(defn select- [params & children]
(into
[:select (-> params
(dissoc :allow-blank? :value :options)
(update
:class (fnil hh/add-class "") default-input-classes))
(cond->>
(map (fn [[k v]]
[:option {:value k :selected (= k (:value params))} v])
(:options params))
(:allow-blank? params) (conj [:option {:value "" :selected (not (:value params))} ""]))]
children))
(defn typeahead- [params]
[:select (-> params
(dissoc :url)
(dissoc :value)
(dissoc :value-fn)
(dissoc :content-fn))
(for [value (if (:multiple params)
(:value params)
[(:value params)])
:when ((:value-fn params first) value)]
[:option {:value ((:value-fn params first) value) :selected true} ((:content-fn params second) value)])
[:script {:lang "javascript"}
(hiccup/raw (format "
(function () {
var element = document.getElementById('%s');
var c = new Choices(element, {removeItems: true, removeItemButton:true, searchFloor: 3, searchPlaceholderValue: '%s'});
let baseUrl = '%s';
element.addEventListener('search', function (e) {
let fullUrl = baseUrl + (baseUrl.includes(\"?\") ? \"&\" : \"?\") + \"q=\" + e.detail.value;
let data = fetch(fullUrl)
.then(res => res.json())
.then(data => {
c.setChoices(data, 'value', 'label', true)
});
});
element.addEventListener('choice', function (e) {
c.clearChoices();
})
})();
"
(:id params)
(:placeholder params)
(:url params)
))]])
(defn typeahead-2- [params]
[:div {:x-data (hx/json {:open false
:baseUrl (if (str/includes? (:url params) "?")
(str (:url params) "&q=")
(str (:url params) "?q="))
:value {:value ((:value-fn params first) (:value params)) :label ((:content-fn params second) (:value params))}
:search ""
:active -1
:elements (if ((:value-fn params first) (:value params))
[{:value ((:value-fn params first) (:value params)) :label ((:content-fn params second) (:value params))}]
[])})
:x-modelable "value.value"
:x-model (:x-model params)
:class "relative"}
[:a {:class (-> (hh/add-class (or (:class params) "") default-input-classes)
(hh/add-class "cursor-pointer"))
"@click.prevent" "open = !open;"
"@keydown.enter.prevent.stop" "open = !open;"
"@keydown.down.prevent.stop" "open = true;"
"@keydown.backspace" "value = {value: '', label: '' }"
:tabindex 0
:x-init (:x-init params)}
[:input (-> params
(dissoc :class)
(dissoc :value-fn)
(dissoc :content-fn)
(dissoc :placeholder)
(dissoc :x-model)
(assoc
"x-ref" "hidden"
:type "hidden"
":value" "value.value"
:x-init (hiccup/raw (str "$watch('value', v => $dispatch('change')); "))))]
[:div.flex.w-full.justify-items-stretch
[:span.flex-grow.text-left {"x-text" "value.label"}]
[:div {:class "w-3 h-3 m-1 inline ml-1 justify-self-end text-gray-500 self-center"}
svg/drop-down]]]
[:ul.dropdown-contents {:class "absolute bg-gray-50 dark:bg-gray-600 rounded-lg shadow-lg py-1 w-max z-10 mt-1"
"@keydown.escape" "open = false; value = {value: '', label: '' }"
"x-transition:enter" "ease-[cubic-bezier(.3,2.3,.6,1)] duration-200"
"x-transition:enter-start" "!opacity-0 !mt-0"
"x-transition:enter-end" "!opacity-1 !mt-1"
"x-transition:leave" "ease-out duration-200"
"x-transition:leave-start" "!opacity-1 !mt-1"
"x-transition:leave-end" "!opacity-0 !mt-0"
"x-show " "open"
"x-trap" "open"
"@click.outside" "open=false; console.log('is this ihid')"}
[:input {:type "text" :class (hh/add-class (or (:class params) "") default-input-classes)
"x-model" "search"
"placeholder" (:placeholder params)
"@keydown.down.prevent" "active ++; active = active >= elements.length - 1 ? elements.length - 1 : active"
"@keydown.up.prevent" "active --; active = active < 0 ? 0 : active"
"@keydown.enter.prevent" "open = false; value = elements.length > 0 ? $data.elements[active] : {'value': '', label: ''}; console.log('are we here')"
"x-init" "$watch('search', s => { if($el.value.length > 2) {fetch(baseUrl + s).then(data=>data.json()).then(data => {elements = data; active=-1}) }})"}]
[:div.dropdown-options
[:template {:x-for "(element, index) in elements"}
[:li [:a {:class "px-4 py-2 flex gap-2 items-center outline-0 focus:bg-neutral-100 hover:bg-neutral-100 whitespace-nowrap [&.active]:bg-primary-300 [&.active]:dark:bg-primary-700 text-gray-800 dark:text-gray-100"
:href "#"
":class" "active == index ? 'active' : ''"
"@mouseover" "active = index"
"@mouseout" "active = -1"
"@click.prevent" "value = element; open=false; "
"x-html" "element.label"}]]]
[:template {:x-if "elements.length == 0"}
[:li {:class "px-4 py-2 flex gap-2 items-center outline-0 focus:bg-neutral-100 hover:bg-neutral-100 whitespace-nowrap [&.active]:bg-primary-500 text-gray-800 dark:text-gray-100 text-xs "}
"No results found"]]]]])
(defn use-size [size]
(if (= :small size)
(str " " "text-xs p-2")
(str " " "text-sm p-2.25")))
(defn text-input- [{:keys [size error?] :as params}]
[:input
(-> params
(dissoc :error?)
(assoc :type "text")
(update
:class (fnil hh/add-class "") default-input-classes)
(update :class #(str % (use-size size))))])
(defn money-input- [{:keys [size] :as params}]
[:input
(-> params
(update :class (fnil hh/add-class "") default-input-classes)
(update :class hh/add-class "appearance-none text-right")
(update :class #(str % (use-size size)))
(assoc :type "number"
:step "0.01")
(dissoc :size))])
(defn int-input- [{:keys [size] :as params}]
[:input
(-> params
(update :class (fnil hh/add-class "") default-input-classes)
(update :class hh/add-class "appearance-none text-right")
(update :class #(str % (use-size size)))
(assoc :type "number"
:step "1")
(dissoc :size))])
(defn date-input- [{:keys [size] :as params}]
[:div
[:input
(-> params
(update :class (fnil hh/add-class "") default-input-classes)
(assoc :type "text")
(assoc "_" (hiccup/raw "init initDatepicker(me)"))
(assoc "hx-on" (hiccup/raw "changeDate: htmx.trigger(this, \"change\")
htmx:beforeCleanupElement: this.dp.destroy()"))
(update :class #(str % (use-size size)))
(dissoc :size))]])
(defn field-errors- [{:keys [source key]} & rest]
(let [errors (:errors (cond-> (meta source)
key (get key)))]
[:p.mt-2.text-xs.text-red-600.dark:text-red-500.h-4 (str/join ", " errors)]))
(defn field- [params & rest]
[:div (-> params
(update :class #(hh/add-class (or % "") "group" )))
(when (:label params)
[:label {:class "block mb-2 text-sm font-medium text-gray-900 dark:text-white"} (:label params)])
rest
(when (:error-source params)
(field-errors- {:source (:error-source params)
:key (:error-key params)}))])
(defn errors- [{:keys [errors]}]
[:p.mt-2.text-xs.text-red-600.dark:text-red-500.h-4
(when (sequential? errors)
(str/join ", " (filter string? errors)))])
(defn form-errors- [{:keys [errors]}]
[:div#form-errors (when errors
[:span.error-content
(errors- {:errors errors})])])
(defn validated-field- [params & rest]
(field- (cond-> params
true (dissoc :errors)
(sequential? (:errors params)) (update :class #(hh/add-class (or % "") "has-error")))
rest
(errors- {:errors (:errors params)})))
(defn hidden- [{:keys [name value]}]
[:input {:type "hidden" :value value :name name}])