(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"]) (def default-checkbox-classes "w-4 h-4 bg-gray-100 border-gray-300 rounded text-primary-600 focus:ring-primary-500 dark:focus:ring-primary-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600") (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] [: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 identity) (:value params)) :label ((:content-fn params identity) (:value params))} :search "" :active -1 :elements (if ((:value-fn params identity) (:value params)) [{:value ((:value-fn params identity) (:value params)) :label ((:content-fn params identity) (:value params))}] []) :popper nil}) :x-modelable "value.value" :x-model (:x-model params) :x-init "popper = Popper.createPopper($refs.input, $refs.dropdown, {placement: 'bottom-start', strategy: 'fixed', modifiers: {name: 'offset', options: {offset: [0, 10]}}})" } [:a {:class (-> (hh/add-class (or (:class params) "") default-input-classes) (hh/add-class "cursor-pointer")) "@click.prevent" "open = !open; popper.update()" "@keydown.enter.prevent.stop" "open = !open; popper.update()" "@keydown.down.prevent.stop" "open = true; popper.update()" "@keydown.backspace" "value = {value: '', label: '' }" :tabindex 0 :x-init (:x-init params) :x-ref "input" } [: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 "bg-gray-100 dark:bg-gray-600 rounded-lg shadow-2xl w-max z-50 ring-1" "x-ref" "dropdown" "@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" "x-transition:enter-end" "!opacity-1" "x-transition:leave" "ease-out duration-200" "x-transition:leave-start" "!opacity-1" "x-transition:leave-end" "!opacity-0" "x-show " "open" "x-trap" "open" "@click.outside" "open=false;"} [:input {:type "text" :class (-> (:class params) (or "") (hh/add-class default-input-classes) (hh/replace-wildcard ["rounded" "border"] "border-bottom bg-gray-100 rounded-t-lg w-full")) "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: ''};" "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 {:class "rounded-b-lg overflow-hidden"} [: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}]) (defn checkbox- [params & rest] [:input (merge params {:type "checkbox" :class (hh/add-class default-checkbox-classes (:class params ""))}) rest])