466 lines
25 KiB
Clojure
466 lines
25 KiB
Clojure
(ns auto-ap.ssr.components.inputs
|
|
(:require
|
|
[auto-ap.ssr.components.tags :as tags]
|
|
[auto-ap.ssr.hiccup-helper :as hh]
|
|
[auto-ap.ssr.hx :as hx :refer [js-fn]]
|
|
[auto-ap.ssr.svg :as svg]
|
|
[auto-ap.time :as atime]
|
|
[clojure.string :as str]
|
|
[hiccup2.core :as hiccup]))
|
|
|
|
|
|
(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 indeterminate:bg-gray-300 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 checkbox- [params & rest]
|
|
(if (seq rest)
|
|
[:label {:class "text-sm text-gray-800 dark:text-gray-300 "}
|
|
[:input (merge (dissoc params :indeterminate?)
|
|
{:type "checkbox" :class (hh/add-class default-checkbox-classes (:class params ""))}
|
|
(when (:indeterminate? params)
|
|
{:x-init "$el.indeterminate = true"}))]
|
|
[:span.ml-2
|
|
rest]]
|
|
[:input (merge (dissoc params :indeterminate params)
|
|
{:type "checkbox" :class (hh/add-class default-checkbox-classes (:class params ""))}
|
|
(when (:indeterminate? params)
|
|
{:x-init "$el.indeterminate = true"}))
|
|
]))
|
|
|
|
(defn typeahead- [params]
|
|
[:div.relative {:x-data (hx/json { :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))}
|
|
:tippy nil
|
|
: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))}]
|
|
[])
|
|
})
|
|
:x-modelable "value.value"
|
|
:x-model (:x-model params)}
|
|
(if (:disabled params)
|
|
[:span {:x-text "value.label"}]
|
|
[:a {:class (-> (hh/add-class (or (:class params) "") default-input-classes)
|
|
(hh/add-class "cursor-pointer"))
|
|
"x-tooltip.on.click" "{content: ()=>$refs.dropdown.innerHTML, placement: 'bottom', onMount(i) {htmx.process(i.popper); }, popperOptions: {strategy: 'fixed', modifiers: [{name: 'flip', options: {fallbackPlacements: ['top']}}]}, theme: 'dropdown', allowHTML: true, interactive:true}"
|
|
"@keydown.down.prevent.stop" "tippy.show();"
|
|
"@keydown.backspace" "tippy.hide(); value = {value: '', label: '' }"
|
|
:tabindex 0
|
|
:x-init (str "$nextTick(() => tippy = $el.__x_tippy); " (: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]
|
|
[:div {:x-show "value.warning" }
|
|
(tags/badge- {:class "peer"
|
|
:x-tooltip "value.warning"} "!") ]]])
|
|
|
|
[:template {:x-ref "dropdown"}
|
|
[:ul.dropdown-contents {:class "bg-gray-100 dark:bg-gray-600 ring-1"
|
|
"@keydown.escape" "tippy.hide(); value = {value: '', label: '' }; "
|
|
:x-destroy "if ($refs.input) {$refs.input.focus();}"}
|
|
[:input {:type "text"
|
|
:autofocus true
|
|
: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.stop" "tippy.hide(); value = elements.length > 0 ? $data.elements[active] : {'value': '', label: ''}; $refs.input.focus()"
|
|
"x-init" "$el.focus(); $watch('search', s => { if($el.value.length > 2) {fetch(baseUrl + s).then(data=>data.json()).then(data => {elements = data; active=-1; tippy.popperInstance.update()}) }})"}]
|
|
[: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-500 [&.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; tippy.hide(); $refs.input.focus()"
|
|
"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 multi-typeahead-dropdown- [params]
|
|
[:template {:x-ref "dropdown"}
|
|
[:ul.dropdown-contents {:class "bg-gray-100 dark:bg-gray-600 rounded-lg shadow-2xl w-max z-50 ring-1 p-4"
|
|
"@keydown.escape.prevent" "tippy.hide();"
|
|
:x-destroy "if ($refs.input) {$refs.input.focus();}"}
|
|
[:div {:class (-> "relative"
|
|
#_(hh/replace-wildcard ["rounded" "border"] "border-bottom bg-gray-100 rounded-t-lg w-full"))}
|
|
[:div {:class "absolute inset-y-0 start-0 flex items-center ps-3 pointer-events-none"}
|
|
[:svg {:class "w-4 h-4 text-gray-500 dark:text-gray-400", :aria-hidden "true", :xmlns "http://www.w3.org/2000/svg", :fill "none", :viewbox "0 0 20 20"}
|
|
[:path {:stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "2", :d "m19 19-4-4m0-7A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z"}]]]
|
|
[:input {:type "text"
|
|
:class (-> (:class params)
|
|
(or "")
|
|
(hh/add-class "block w-full p-2 ps-10 text-sm text-gray-900 border border-gray-300 rounded-lg bg-gray-50 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")
|
|
(hh/add-class 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.stop" "if ($data.elements[active]) { if (value.has($data.elements[active].value)) { value.delete($data.elements[active].value) } else {value.add($data.elements[active].value); lookup[$data.elements[active].value] = $data.elements[active].label} } "
|
|
"x-init" " $el.focus(); $watch('search', s => { if($el.value.length > 2) {fetch(baseUrl + s).then(data=>data.json()).then(data => reset_elements(data)) }})"}]]
|
|
[:div.dropdown-options {:class "overflow-hidden divide-y divide-gray-200 "}
|
|
[:template {:x-for "(element, index) in elements"}
|
|
[:li {":style" "index == 0 && 'border: 0 !important;'"}
|
|
[:label {:class "p-3 group rounded 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 [&.implied]:text-gray-500 text-gray-800 dark:text-gray-100 cursor-pointer"
|
|
|
|
|
|
:href "#"
|
|
":class" (hx/json {"active" (hx/js-fn "active==index")
|
|
"implied" (hx/js-fn "all_selected && index != 0")
|
|
} )
|
|
"@mouseover" "active = index"
|
|
"@mouseout" "active = -1"
|
|
"@click.prevent" "toggle(element)"}
|
|
(checkbox- {":checked" "value.has(element.value) || all_selected"
|
|
:class "group-[&.implied]:bg-green-200"
|
|
|
|
})
|
|
#_[:input {:type "checkbox" }]
|
|
[:span {"x-html" "element.label"}]]]]
|
|
[:template {:x-if "elements.length == 0"}
|
|
[:li {:class "px-4 pt-4 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 " "style" "border: 0 !important"}
|
|
"No results found"]]]]])
|
|
|
|
(defn multi-typeahead-selected-pill- [params]
|
|
[:div.flex-grow.flex
|
|
[:template {:x-if "value.size > 0"}
|
|
[:a.bg-blue-100.rounded-full.px-3
|
|
[:span.text-left
|
|
[:span {"x-text" "value.has('all') ? 'All' : value.size"}]
|
|
" selected"]
|
|
|
|
]]
|
|
[:template {:x-if "value.size == 0"}
|
|
[:span.text-left.text-gray-400 "None selected"]]
|
|
[:div {:class "w-4 h-4 ml-2 inline text-gray-500 self-center rounded-full bg-gray-100 text-gray-500"
|
|
"@click.prevent.stop" "value = new Set([]); all_selected=false;"
|
|
:x-show "value.size > 0"}
|
|
svg/x]])
|
|
|
|
|
|
(defn multi-typeahead- [params]
|
|
[:div.relative {:x-data (hx/json {:baseUrl (if (str/includes? (:url params) "?")
|
|
(str (:url params) "&q=")
|
|
(str (:url params) "?q="))
|
|
:reset_elements (js-fn "function(e) {
|
|
this.elements = [{value: 'all', label:'All'}].concat(e);
|
|
this.active = -1
|
|
}")
|
|
:toggle (js-fn "function(e) {
|
|
if (e.value == 'all') {
|
|
if (this.value.size > 0) {
|
|
this.value = new Set([]);
|
|
this.all_selected = false;
|
|
} else {
|
|
this.value = new Set(['all']);
|
|
this.all_selected = true;
|
|
}
|
|
}
|
|
else {
|
|
if (this.all_selected) {
|
|
this.value.delete('all')
|
|
this.all_selected = false;
|
|
|
|
}
|
|
if (this.value.has(e.value)) {
|
|
this.value.delete(e.value)
|
|
} else {
|
|
this.value.add(e.value); this.lookup[e.value] = e.label
|
|
}
|
|
}
|
|
}")
|
|
:all_selected (boolean (= (:value params) :all)),
|
|
:value (cond
|
|
(= :all (:value params))
|
|
["all"]
|
|
|
|
(sequential? (:value params))
|
|
(map (fn [v] ((:value-fn params identity) v))
|
|
(:value params))
|
|
|
|
:else
|
|
[])
|
|
:tippy nil
|
|
:lookup (into {}
|
|
(when (sequential? (:value params))
|
|
(map (fn [v] [((:value-fn params identity) v)
|
|
((:content-fn params identity) v)])
|
|
(:value params))))
|
|
:x-init (str "$watch('value', v => $dispatch('change')); ")
|
|
:search ""
|
|
:active -1
|
|
:elements (cond-> [{:value "all" :label "All"}]
|
|
(sequential? (:value params))
|
|
(into (map (fn [v]
|
|
{:value ((:value-fn params identity) v)
|
|
:label ((:content-fn params identity) v)})
|
|
(:value params))))
|
|
:x-ref "r"})
|
|
;; :x-modelable "value.value" TODO
|
|
;; :x-model (:x-model params) TODO
|
|
:x-init "value=new Set(value || []); "}
|
|
(if (:disabled params)
|
|
[:span {:x-text "value.label"}]
|
|
[:a {:class (-> (hh/add-class (or (:class params) "") default-input-classes)
|
|
(hh/add-class "cursor-pointer"))
|
|
"x-tooltip.on.click.prevent" "{content: ()=>$refs.dropdown.innerHTML, placement: 'bottom', onMount(i) {htmx.process(i.popper); }, popperOptions: {strategy: 'fixed', modifiers: [{name: 'flip', options: {fallbackPlacements: ['top']}}]}, theme: 'dropdown', allowHTML: true, interactive:true}"
|
|
"@keydown.down.prevent.stop" "tippy.show();"
|
|
"@keydown.backspace" "tippy.hide(); value=new Set( []);"
|
|
:tabindex 0
|
|
:x-init (str "$nextTick(() => tippy = $el.__x_tippy); " (:x-init params))
|
|
:x-ref "input"}
|
|
[:template {:x-for "v in Array.from(value.values())"}
|
|
[:input (-> params
|
|
(dissoc :class :value-fn :content-fn :placeholder :x-model)
|
|
(assoc
|
|
:type "hidden"
|
|
"x-bind:value" "v"))]]
|
|
[:template {:x-if "value.size == 0"}
|
|
[:input (-> params
|
|
(dissoc :class :value-fn :content-fn :placeholder :x-model)
|
|
(assoc :type "hidden"
|
|
:value ""
|
|
))]]
|
|
[:div.flex.w-full.justify-items-stretch
|
|
(multi-typeahead-selected-pill- params)
|
|
[:div {:class "w-3 h-3 m-1 inline ml-1 justify-self-end text-gray-500 self-center"}
|
|
svg/drop-down]
|
|
[:div {:x-show "value.warning"
|
|
:x-ref "warning_badge"
|
|
:x-effect "if (value.warning) { $nextTick(()=> warning_badge.update()) }"}
|
|
(tags/badge- {:class "peer"} "!")
|
|
|
|
|
|
[:div {:x-show "value.warning"
|
|
:x-ref "warning_pop"
|
|
:class "hidden peer-hover:block bg-red-50 dark:bg-gray-600 rounded-lg shadow-2xl w-max z-50 p-4"
|
|
:x-text "value.warning"}]]]
|
|
(multi-typeahead-dropdown- params) ])])
|
|
|
|
|
|
(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" :autocomplete "off")
|
|
(update
|
|
:class #(-> ""
|
|
(hh/add-class default-input-classes)
|
|
(hh/add-class %)))
|
|
(update :class #(str % (use-size size))))])
|
|
|
|
(defn text-area- [{:keys [] :as params}]
|
|
[:textarea
|
|
(-> params
|
|
(update :class #(-> ""
|
|
(hh/add-class default-input-classes)
|
|
(hh/add-class %)))) ]
|
|
)
|
|
|
|
(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.shrink {:x-data (hx/json {:value (:value params)})}
|
|
[:input
|
|
(-> params
|
|
(update :class (fnil hh/add-class "") default-input-classes)
|
|
(assoc :x-modelable "value")
|
|
(assoc :type "text")
|
|
(assoc :x-data (hx/json {:dp nil}) )
|
|
(assoc :x-init " dp = initDatepicker($el);")
|
|
(assoc "@htmx:before-history-save" "destroyDatepicker(dp)" )
|
|
(assoc "@htmx:before-cleanup-element" "destroyDatepicker(dp)" )
|
|
(assoc "@change" "value = $event.target.value;")
|
|
(assoc "hx-on" (hiccup/raw "changeDate: htmx.trigger(this, \"change\") "))
|
|
(update :class #(str % (use-size size) " w-full"))
|
|
(dissoc :size))]])
|
|
|
|
(defn multi-calendar-input- [{:keys [size] :as params}]
|
|
(let [value (str/join ", "
|
|
(for [v (:value params)
|
|
:when v]
|
|
(some-> v (atime/unparse-local atime/normal-date))))]
|
|
[:div.shrink {:x-data (hx/json {:value value
|
|
:dp nil })
|
|
:x-modelable "value"
|
|
:x-model (:x-model params) }
|
|
[:template {:x-for "v in value"}
|
|
[:input {:type "hidden" :name (:name params) :x-model "v"}]]
|
|
[:div
|
|
(-> params
|
|
(update :class (fnil hh/add-class "") default-input-classes)
|
|
(assoc :type "text")
|
|
(assoc :value value)
|
|
;; the data-date field has to be bound before the datepicker can be initialized
|
|
(assoc :x-init "$nextTick(() => { dp = initMultiDatepicker($el, value); ;}); ")
|
|
(assoc "x-effect" "if(dp) { dp.setDate(Array.from(value), {clear: true}); } ")
|
|
(assoc ":data-date" "Array.prototype.join.call(value, ', ')")
|
|
(assoc "@htmx:before-history-save" "destroyDatepicker(dp)" )
|
|
(assoc "@htmx:before-cleanup-element" "destroyDatepicker(dp)" )
|
|
(assoc "x-destroy" "destroyDatepicker(dp)")
|
|
(assoc "@change-date.camel" "console.log('date changing'); value = dp.getDate(\"mm/dd/yyyy\");")
|
|
|
|
(update :class #(str % (use-size size) " w-full"))
|
|
(dissoc :size :name :x-model :x-modelable))]]))
|
|
|
|
(defn calendar-input- [{:keys [size] :as params}]
|
|
(let [value (:value params)]
|
|
[:div.shrink {:x-data (hx/json {:value value
|
|
:dp nil })
|
|
:x-modelable "value"
|
|
:x-model (:x-model params) }
|
|
[:input {:type "hidden" :name (:name params) :x-model "value"}]
|
|
[:div
|
|
(-> params
|
|
(update :class (fnil hh/add-class "") default-input-classes)
|
|
(assoc :type "text")
|
|
(assoc :value value)
|
|
;; the data-date field has to be bound before the datepicker can be initialized
|
|
(assoc :x-init "$nextTick(() => { dp = initCalendar($el); ;}); ")
|
|
(assoc "x-effect" "if(dp) { dp.setDate(value); } ")
|
|
(assoc ":data-date" "value")
|
|
(assoc "@htmx:before-history-save" "destroyDatepicker(dp)" )
|
|
(assoc "@htmx:before-cleanup-element" "destroyDatepicker(dp)" )
|
|
(assoc "x-destroy" "destroyDatepicker(dp)")
|
|
(assoc "@change-date.camel" "value = dp.getDate(\"mm/dd/yyyy\");")
|
|
|
|
(update :class #(str % (use-size size) " w-full"))
|
|
(dissoc :size :name :x-model :x-modelable))]]))
|
|
|
|
|
|
|
|
(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 inline-field- [params & rest]
|
|
[:div (-> params
|
|
(update :class #(hh/add-class (or % "") "group flex items-baseline gap-2" )))
|
|
(when (:label params)
|
|
[:label {:class "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 validated-inline-field- [params & rest]
|
|
(inline-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] :as params}]
|
|
[:input (merge {:type "hidden" :value value :name name} params)])
|
|
|
|
|
|
|
|
(defn toggle- [params & children]
|
|
[:label {:class "inline-flex items-center cursor-pointer"}
|
|
[:input (merge {:type "checkbox", :class "sr-only peer"} params)]
|
|
[:div {:class "relative w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600"}]
|
|
[:span {:class "ms-3 text-sm font-medium text-gray-900 dark:text-gray-300"} children]])
|