From c614cdbff3d1020a6f0b7449b1d7e9c62119722b Mon Sep 17 00:00:00 2001 From: Daniel Ziltener Date: Mon, 1 Jun 2020 06:01:59 +0200 Subject: [PATCH] In the beginning there was darkness --- .gitignore | 26 ++ LICENSE | 15 + README.adoc | 37 ++ deps.edn | 9 + pom.xml | 92 +++++ .../rendering/semantic_ui/autocomplete.cljc | 154 +++++++ .../rad/rendering/semantic_ui/blob_field.cljc | 121 ++++++ .../rendering/semantic_ui/boolean_field.cljc | 34 ++ .../rad/rendering/semantic_ui/components.cljc | 92 +++++ .../rad/rendering/semantic_ui/container.cljc | 85 ++++ .../semantic_ui/controls/action_button.cljc | 40 ++ .../semantic_ui/controls/boolean_control.cljc | 35 ++ .../semantic_ui/controls/control.cljc | 35 ++ .../semantic_ui/controls/instant_inputs.cljc | 87 ++++ .../semantic_ui/controls/pickers.cljc | 72 ++++ .../semantic_ui/controls/text_input.cljc | 59 +++ .../rendering/semantic_ui/decimal_field.cljc | 15 + .../rendering/semantic_ui/entity_picker.cljc | 115 ++++++ .../semantic_ui/enumerated_field.cljc | 79 ++++ .../rad/rendering/semantic_ui/field.cljc | 44 ++ .../rad/rendering/semantic_ui/form.cljc | 391 ++++++++++++++++++ .../rendering/semantic_ui/instant_field.cljc | 34 ++ .../rad/rendering/semantic_ui/int_field.cljc | 6 + .../rad/rendering/semantic_ui/report.cljc | 360 ++++++++++++++++ .../semantic_ui/semantic_ui_controls.cljc | 78 ++++ .../rad/rendering/semantic_ui/text_field.cljc | 67 +++ 26 files changed, 2182 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.adoc create mode 100644 deps.edn create mode 100644 pom.xml create mode 100644 src/main/com/fulcrologic/rad/rendering/semantic_ui/autocomplete.cljc create mode 100644 src/main/com/fulcrologic/rad/rendering/semantic_ui/blob_field.cljc create mode 100644 src/main/com/fulcrologic/rad/rendering/semantic_ui/boolean_field.cljc create mode 100644 src/main/com/fulcrologic/rad/rendering/semantic_ui/components.cljc create mode 100644 src/main/com/fulcrologic/rad/rendering/semantic_ui/container.cljc create mode 100644 src/main/com/fulcrologic/rad/rendering/semantic_ui/controls/action_button.cljc create mode 100644 src/main/com/fulcrologic/rad/rendering/semantic_ui/controls/boolean_control.cljc create mode 100644 src/main/com/fulcrologic/rad/rendering/semantic_ui/controls/control.cljc create mode 100644 src/main/com/fulcrologic/rad/rendering/semantic_ui/controls/instant_inputs.cljc create mode 100644 src/main/com/fulcrologic/rad/rendering/semantic_ui/controls/pickers.cljc create mode 100644 src/main/com/fulcrologic/rad/rendering/semantic_ui/controls/text_input.cljc create mode 100644 src/main/com/fulcrologic/rad/rendering/semantic_ui/decimal_field.cljc create mode 100644 src/main/com/fulcrologic/rad/rendering/semantic_ui/entity_picker.cljc create mode 100644 src/main/com/fulcrologic/rad/rendering/semantic_ui/enumerated_field.cljc create mode 100644 src/main/com/fulcrologic/rad/rendering/semantic_ui/field.cljc create mode 100644 src/main/com/fulcrologic/rad/rendering/semantic_ui/form.cljc create mode 100644 src/main/com/fulcrologic/rad/rendering/semantic_ui/instant_field.cljc create mode 100644 src/main/com/fulcrologic/rad/rendering/semantic_ui/int_field.cljc create mode 100644 src/main/com/fulcrologic/rad/rendering/semantic_ui/report.cljc create mode 100644 src/main/com/fulcrologic/rad/rendering/semantic_ui/semantic_ui_controls.cljc create mode 100644 src/main/com/fulcrologic/rad/rendering/semantic_ui/text_field.cljc diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3497901 --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +*~ +*.iml +*.log +*.sw? +*.swp +*jar +.DS_Store +.cljs_rhino_repl +.idea +.nrepl* +.nrepl-port +.repl +checkouts +classes +compiled +datahub.log* +node_modules +out +resources/public/js +target +.cpcache +.shadow-cljs +.clj-kondo/.cache +DevelopersGuide.html +docs/assets +docs/.asciidoctor diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b944d74 --- /dev/null +++ b/LICENSE @@ -0,0 +1,15 @@ +The MIT License (MIT) +Copyright (c) 2017-2019, Fulcrologic, LLC + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit +persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the +Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.adoc b/README.adoc new file mode 100644 index 0000000..fcb95bd --- /dev/null +++ b/README.adoc @@ -0,0 +1,37 @@ += Fulcro RAD Carbon Rendering Plugin + +WARNING: This library is alpha. + +== Usage + +image:https://img.shields.io/clojars/v/ch.lyrion/fulcro-rad-carbon.svg[link=https://clojars.org/ch.lyrion/fulcro-rad-carbon] + +To use this library simply install the controls on your RAD application: + +[source] +----- +(ns com.example.main + (:require + [com.fulcrologic.rad.application :as rad-app])) + +(defonce app (-> (rad-app/fulcro-rad-app) + (rad-app/install-ui-controls! sui/all-controls))) +----- + +== LICENSE + +The MIT License (MIT) +Copyright (c), Fulcrologic, LLC + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit +persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the +Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/deps.edn b/deps.edn new file mode 100644 index 0000000..d47c362 --- /dev/null +++ b/deps.edn @@ -0,0 +1,9 @@ +{:paths ["src/main"] + + :deps {com.fulcrologic/fulcro-rad {:mvn/version "0.0.9-alpha"} + com.fulcrologic/fulcro-i18n {:mvn/version "0.0.3-alpha"} + ch.lyrion/carbon-wrapper {:mvn/version "1.0.3-SNAPSHOT"} + com.fulcrologic/semantic-ui-wrapper {:mvn/version "1.0.1"} + + org.clojure/clojure {:mvn/version "1.10.1" :scope "provided"} + org.clojure/clojurescript {:mvn/version "1.10.764" :scope "provided"}}} diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..afa4cb9 --- /dev/null +++ b/pom.xml @@ -0,0 +1,92 @@ + + + 4.0.0 + com.fulcrologic + fulcro-rad-semantic-ui + jar + 0.0.10-alpha-SNAPSHOT + + UTF-8 + 1.8 + 1.8 + + Fulcro RAD Semantic UI + A UI plugin for Fulcro RAD that renders to web/electron using Semantic UI CSS + https://github.com/fulcrologic/fulcro-rad-semantic-ui + + + MIT + https://opensource.org/licenses/MIT + + + + https://github.com/fulcrologic/fulcro-rad-semantic-ui + scm:git:git://github.com/fulcrologic/fulcro-rad-semantic-ui.git + scm:git:ssh://git@github.com/fulcrologic/fulcro-rad-semantic-ui.git + HEAD + + + + clojars + Clojars repository + https://clojars.org/repo + + + + src/main + + + src/main + + + + + org.apache.maven.plugins + maven-gpg-plugin + 1.6 + + + sign-artifacts + verify + + sign + + + + + + + + + clojars + https://repo.clojars.org/ + + + + + org.clojure + clojure + 1.10.1 + + + com.fulcrologic + fulcro-rad + 0.0.9-alpha + + + com.fulcrologic + fulcro-i18n + 0.0.3-alpha + + + org.clojure + clojurescript + 1.10.764 + + + com.fulcrologic + semantic-ui-wrapper + 1.0.1 + + + diff --git a/src/main/com/fulcrologic/rad/rendering/semantic_ui/autocomplete.cljc b/src/main/com/fulcrologic/rad/rendering/semantic_ui/autocomplete.cljc new file mode 100644 index 0000000..68aaffc --- /dev/null +++ b/src/main/com/fulcrologic/rad/rendering/semantic_ui/autocomplete.cljc @@ -0,0 +1,154 @@ +(ns com.fulcrologic.rad.rendering.semantic-ui.autocomplete + (:require + #?@(:cljs + [[com.fulcrologic.fulcro.dom :as dom :refer [div label input]] + [goog.object :as gobj] + [cljs.reader :refer [read-string]] + [com.fulcrologic.semantic-ui.modules.dropdown.ui-dropdown :refer [ui-dropdown]]] + :clj + [[com.fulcrologic.fulcro.dom-server :as dom :refer [div label input]]]) + [com.fulcrologic.rad.ids :as ids] + [com.fulcrologic.rad.ui-validation :as validation] + [com.fulcrologic.fulcro.rendering.multiple-roots-renderer :as mroot] + [com.fulcrologic.fulcro.components :as comp :refer [defsc]] + [com.fulcrologic.fulcro.data-fetch :as df] + [com.fulcrologic.fulcro.mutations :as m :refer [defmutation]] + [com.fulcrologic.fulcro.algorithms.merge :as merge] + [com.fulcrologic.rad.options-util :as opts] + [com.fulcrologic.rad.rendering.semantic-ui.components :refer [ui-wrapped-dropdown]] + [com.fulcrologic.rad.attributes :as attr] + [clojure.string :as str] + [taoensso.timbre :as log] + [com.fulcrologic.rad.form :as form] + [com.fulcrologic.fulcro.algorithms.normalized-state :as fns])) + +(defsc AutocompleteQuery [_ _] {:query [:text :value]}) + +(defn to-js [v] + #?(:clj v + :cljs (clj->js v))) + +(defmutation normalize-options [{:keys [source target]}] + (action [{:keys [state]}] + #?(:clj true + :cljs + (let [options (get @state source) + normalized-options (apply array + (map (fn [{:keys [text value]}] + #js {:text text :value (pr-str value)}) options))] + (fns/swap!-> state + (dissoc source) + (assoc-in target normalized-options)))))) + +(defsc AutocompleteField [this {:ui/keys [search-string options] :as props} {:keys [value label onChange + invalid? validation-message + read-only?]}] + {:initLocalState (fn [this] + ;; TASK: props not making it...fix that, or debounce isn't configurable. + (let [{:autocomplete/keys [debounce-ms]} (comp/props this)] + {:load! (opts/debounce + (fn [s] + (let [{id ::autocomplete-id + :autocomplete/keys [search-key]} (comp/props this)] + (df/load! this search-key AutocompleteQuery + {:params {:search-string s} + :post-mutation `normalize-options + :post-mutation-params {:source search-key + :target [::autocomplete-id id :ui/options]}}))) + (or debounce-ms 200))})) + :componentDidMount (fn [this] + (let [{id ::autocomplete-id + :autocomplete/keys [search-key]} (comp/props this) + value (comp/get-computed this :value)] + (when (and search-key value) + (df/load! this search-key AutocompleteQuery + {:params {:only value} + :post-mutation `normalize-options + :post-mutation-params {:source search-key + :target [::autocomplete-id id :ui/options]}})))) + :query [::autocomplete-id :ui/search-string :ui/options :autocomplete/search-key + :autocomplete/debounce-ms :autocomplete/minimum-input] + :ident ::autocomplete-id} + (let [load! (comp/get-state this :load!)] + #?(:clj + (dom/div "") + :cljs + (dom/div :.field {:classes [(when invalid? "error")]} + (dom/label label (when invalid? (str " " validation-message))) + (if read-only? + (gobj/getValueByKeys options 0 "text") + (ui-dropdown #js {:search true + :options (if options options #js []) + :value (pr-str value) + :selection true + :closeOnBlur true + :openOnFocus true + :selectOnBlur true + :selectOnNavigation true + :onSearchChange (fn [_ v] + (let [query (comp/isoget v "searchQuery")] + (load! query))) + :onChange (fn [_ v] + (when onChange + (onChange (some-> (comp/isoget v "value") + read-string))))})))))) + +(def ui-autocomplete-field (comp/computed-factory AutocompleteField {:keyfn ::autocomplete-id})) + +(defmutation gc-autocomplete [{:keys [id]}] + (action [{:keys [state]}] + (when id + (swap! state fns/remove-entity [::autocomplete-id id])))) + +(defsc AutocompleteFieldRoot [this props {:keys [env attribute]}] + {:initLocalState (fn [this] {:field-id (ids/new-uuid)}) + :componentDidMount (fn [this] + (let [id (comp/get-state this :field-id) + {:keys [attribute]} (comp/get-computed this) + {:autocomplete/keys [search-key debounce-ms minimum-input]} (::form/field-options attribute)] + (merge/merge-component! this AutocompleteField {::autocomplete-id id + :autocomplete/search-key search-key + :autocomplete/debounce-ms debounce-ms + :autocomplete/minimum-input minimum-input + :ui/search-string "" + :ui/options #js []})) + (mroot/register-root! this {:initialize? true})) + :shouldComponentUpdate (fn [_ _] true) + :initial-state {::autocomplete-id {}} + :componentWillUnmount (fn [this] + (comp/transact! this [(gc-autocomplete {:id (comp/get-state this :field-id)})]) + (mroot/deregister-root! this)) + :query [::autocomplete-id]} + (let [{:autocomplete/keys [debounce-ms search-key]} (::form/field-options attribute) + k (::attr/qualified-key attribute) + {::form/keys [form-instance]} env + value (-> (comp/props form-instance) (get k)) + id (comp/get-state this :field-id) + label (form/field-label env attribute) + read-only? (form/read-only? form-instance attribute) + invalid? (validation/invalid-attribute-value? env attribute) + validation-message (when invalid? (validation/validation-error-message env attribute)) + field (get-in props [::autocomplete-id id])] + ;; Have to pass the id and debounce early since the merge in mount won't happen until after, which is too late for initial + ;; state + (ui-autocomplete-field (assoc field + ::autocomplete-id id + :autocomplete/search-key search-key + :autocomplete/debounce-ms debounce-ms) + {:value value + :invalid? invalid? + :validation-message validation-message + :label label + :read-only? read-only? + :onChange (fn [normalized-value] + #?(:cljs + (when normalized-value (form/input-changed! env k normalized-value))))}))) + +(def ui-autocomplete-field-root (mroot/floating-root-factory AutocompleteFieldRoot + {:keyfn (fn [props] (-> props :attribute ::attr/qualified-key))})) + +(defn render-autocomplete-field [env {::attr/keys [cardinality] :or {cardinality :one} :as attribute}] + (if (= :many cardinality) + (log/error "Cannot autocomplete to-many attributes with renderer" `render-autocomplete-field) + (ui-autocomplete-field-root {:env env :attribute attribute}))) + diff --git a/src/main/com/fulcrologic/rad/rendering/semantic_ui/blob_field.cljc b/src/main/com/fulcrologic/rad/rendering/semantic_ui/blob_field.cljc new file mode 100644 index 0000000..6bc587b --- /dev/null +++ b/src/main/com/fulcrologic/rad/rendering/semantic_ui/blob_field.cljc @@ -0,0 +1,121 @@ +(ns com.fulcrologic.rad.rendering.semantic-ui.blob-field + (:require + [com.fulcrologic.fulcro.components :as comp :refer [defsc]] + #?@(:cljs [[com.fulcrologic.fulcro.dom :as dom :refer [div input]] + [goog.object :as gobj] + [com.fulcrologic.fulcro.networking.file-upload :as file-upload]] + :clj [[com.fulcrologic.fulcro.dom-server :as dom :refer [div input]]]) + [com.fulcrologic.rad.form :as form] + [com.fulcrologic.rad.attributes :as attr] + [taoensso.timbre :as log] + [com.fulcrologic.rad.ui-validation :as validation] + [com.fulcrologic.rad.blob :as blob] + [com.fulcrologic.rad.options-util :refer [?! narrow-keyword]] + [com.fulcrologic.rad.rendering.semantic-ui.components :refer [ui-wrapped-dropdown]] + [com.fulcrologic.rad.rendering.semantic-ui.field :refer [render-field-factory]] + [com.fulcrologic.fulcro.algorithms.form-state :as fs])) + +(defn evt->js-files [evt] + #?(:cljs + (let [js-file-list (.. evt -target -files)] + (map (fn [file-idx] + (let [js-file (.item js-file-list file-idx) + name (.-name js-file)] + js-file)) + (range (.-length js-file-list)))))) + +#_(defsc ImageUploadField [this + {::form/keys [form-instance] :as env} + {::blob/keys [accept-file-types] + ::attr/keys [qualified-key] :as attribute}] + {:initLocalState (fn [this] + #?(:cljs + {:save-ref (fn [r] (gobj/set this "fileinput" r)) + :on-click (fn [evt] (when-let [i (gobj/get this "fileinput")] + (.click i))) + :on-change (fn [evt] + (let [env (comp/props this) + attribute (comp/get-computed this) + file (-> evt evt->js-files first)] + (blob/upload-file! env attribute file)))}))} + (let [props (comp/props form-instance) + url-key (narrow-keyword qualified-key "url") + current-sha (get props qualified-key) + url (get props url-key) + has-current-value? (seq current-sha) + {:keys [save-ref on-change on-click]} (comp/get-state this) + upload-complete? false + label (form/field-label env attribute) + valid? (and upload-complete? has-current-value?)] + (div :.field {:key (str qualified-key)} + (dom/label label) + (div :.ui.tiny.image + (dom/img {:src url :width "100" + :onClick on-click}) + (dom/input (cond-> {:id (str qualified-key) + :ref save-ref + :style {:position "absolute" + :opacity 0 + :top 0 + :right 0} + :onChange on-change + :type "file"} + accept-file-types (assoc :allow (?! accept-file-types)))))))) + +#_(def ui-image-upload-field (comp/computed-factory ImageUploadField + {:keyfn (fn [props] (some-> props comp/get-computed ::attr/qualified-key))})) + +#_(defn render-image-upload [env attribute] + (ui-image-upload-field env attribute)) + +(defsc FileUploadField [this + {::form/keys [form-instance master-form] :as env} + {::blob/keys [accept-file-types can-change?] + ::attr/keys [qualified-key] :as attribute}] + {:componentDidMount (fn [this] + (comment "TRIGGER UPLOAD IF CONFIG SAYS TO?")) + :initLocalState (fn [this] + #?(:cljs + {:save-ref (fn [r] (gobj/set this "fileinput" r)) + :on-click (fn [evt] (when-let [i (gobj/get this "fileinput")] + (.click i))) + :on-change (fn [evt] + (let [env (comp/props this) + attribute (comp/get-computed this) + file (-> evt evt->js-files first)] + (blob/upload-file! this attribute file {:file-ident []})))}))} + (let [props (comp/props form-instance) + read-only? (or + (form/read-only? master-form attribute) + (form/read-only? form-instance attribute)) + can-change? (if read-only? false (?! can-change? env attribute)) + url-key (blob/url-key qualified-key) + name-key (blob/filename-key qualified-key) + current-sha (get props qualified-key) + url (get props url-key) + filename (get props name-key) + pct (blob/upload-percentage props qualified-key) + has-current-value? (seq current-sha) + {:keys [save-ref on-change on-click]} (comp/get-state this) + dirty? (if read-only? false (fs/dirty? props qualified-key)) + label (form/field-label env attribute) + invalid? (if read-only? false (validation/invalid-attribute-value? env attribute)) + validation-message (when invalid? (validation/validation-error-message env attribute))] + (div :.field {:key (str qualified-key)} + (dom/label label) + (cond + (blob/blob-downloadable? props qualified-key) + (dom/a {:href (str url "?filename=" filename)} "Download") + + (blob/uploading? props qualified-key) + (dom/div :.ui.small.blue.progress + (div :.bar {:style {:transitionDuration "300ms" + :display "block" + :width pct}} + (div :.progress pct))))))) + +(def ui-file-upload-field (comp/computed-factory FileUploadField + {:keyfn (fn [props] (some-> props comp/get-computed ::attr/qualified-key))})) + +(defn render-file-upload [env attribute] + (ui-file-upload-field env attribute)) diff --git a/src/main/com/fulcrologic/rad/rendering/semantic_ui/boolean_field.cljc b/src/main/com/fulcrologic/rad/rendering/semantic_ui/boolean_field.cljc new file mode 100644 index 0000000..d9272d6 --- /dev/null +++ b/src/main/com/fulcrologic/rad/rendering/semantic_ui/boolean_field.cljc @@ -0,0 +1,34 @@ +(ns com.fulcrologic.rad.rendering.semantic-ui.boolean-field + (:require + #?(:cljs + [com.fulcrologic.fulcro.dom :as dom :refer [div label input]] + :clj + [com.fulcrologic.fulcro.dom-server :as dom :refer [div label input]]) + [com.fulcrologic.rad.attributes :as attr] + [com.fulcrologic.fulcro.components :as comp] + [clojure.string :as str] + [com.fulcrologic.rad.form :as form] + [com.fulcrologic.fulcro.dom.events :as evt])) + +(defn render-field [{::form/keys [form-instance] :as env} attribute] + (let [k (::attr/qualified-key attribute) + props (comp/props form-instance) + user-props (form/field-style-config env attribute :input/props) + field-label (form/field-label env attribute) + read-only? (form/read-only? form-instance attribute) + value (get props k false)] + (div :.ui.field {:key (str k)} + (if read-only? + (label field-label " " (if value "Yes" "No")) + (div :.ui.checkbox + (input (merge + {:checked value + :type "checkbox" + :disabled (boolean read-only?) + :onChange (fn [evt] + (let [v (not value)] + (form/input-blur! env k v) + (form/input-changed! env k v)))} + user-props)) + (label field-label)))))) + diff --git a/src/main/com/fulcrologic/rad/rendering/semantic_ui/components.cljc b/src/main/com/fulcrologic/rad/rendering/semantic_ui/components.cljc new file mode 100644 index 0000000..89335ed --- /dev/null +++ b/src/main/com/fulcrologic/rad/rendering/semantic_ui/components.cljc @@ -0,0 +1,92 @@ +(ns com.fulcrologic.rad.rendering.semantic-ui.components + (:require + #?@(:cljs + [[com.fulcrologic.fulcro.dom :as dom :refer [div label input]] + [com.fulcrologic.semantic-ui.modules.dropdown.ui-dropdown :refer [ui-dropdown]]] + :clj + [[com.fulcrologic.fulcro.dom-server :as dom :refer [div label input]]]) + [com.fulcrologic.fulcro.components :as comp :refer [defsc]] + [com.fulcrologic.fulcro.algorithms.transit :as ftransit] + [taoensso.timbre :as log])) + +(defn sui-format->user-format + "Converts transit encoded value(s), used by Semantic UI, into CLJS datastructure." + [{:keys [multiple]} value] + (if multiple + (into [] (map ftransit/transit-str->clj value)) + (ftransit/transit-str->clj value))) + +(defn user-format->sui-format [{:keys [multiple]} value] + "Converts CLJS datastructure into transit encoded string(s), usable by Semantic UI." + (if multiple + (if value + (to-array (map ftransit/transit-clj->str value)) + #js []) + (if (or value (boolean? value)) + (ftransit/transit-clj->str value) + ""))) + +(defn wrapped-onChange + "Wraps userOnChange fn with try/catch and sui-form->user-format conversion." + [props userOnChange] + (fn [_ v] + #?(:cljs + (try + (if (and (.-value v) (seq (.-value v))) + (let [value (sui-format->user-format props (.-value v))] + (when (and value userOnChange) (userOnChange value))) + (userOnChange nil)) + (catch :default e + (log/error e "Unable to read dropdown value " (when v (.-value v)))))))) + +(defsc WrappedDropdown [this {:keys [onChange value multiple] :as props}] + {:initLocalState (fn [this] + #?(:cljs + (let [xform-options (memoize (fn [options] + (clj->js (mapv (fn [{:keys [text value]}] + #js {:text text :value (some-> value (ftransit/transit-clj->str))}) + options)))) + xform-value (fn [multiple? value] + (user-format->sui-format {:multiple multiple?} value))] + {:get-options (fn [props] (xform-options (:options props))) + :format-value (fn [props value] (xform-value (:multiple props) value))})))} + #?(:cljs + (let [{:keys [get-options format-value]} (comp/get-state this) + userOnChange onChange + options (get-options props) + value (format-value props value) + props (merge + {:search true + :selection true + :closeOnBlur true + :openOnFocus true + :selectOnBlur true + :selectOnNavigation true + :multiple (boolean multiple)} + props + {:value value + :options options + :onChange (fn [e v] + (try + (let [string-value (.-value v) + value (if multiple + (mapv #(when (seq %) (ftransit/transit-str->clj %)) string-value) + (when (seq string-value) (ftransit/transit-str->clj string-value)))] + (when userOnChange + (userOnChange value))) + (catch :default e + (log/error "Unable to read dropdown value " e (when v (.-value v))))))})] + (ui-dropdown props)) + :clj + (dom/div :.ui.selection.dropdown + (dom/input {:type "hidden"}) + (dom/i :.dropdown.icon) + (dom/div :.default.text "") + (dom/div :.menu)))) + +(def ui-wrapped-dropdown + "Draw a SUI dropdown with the given props. The arguments are identical to sui/ui-dropdown, but options and onChange + are auto-wrapped so that clojure data (e.g. keywords) can be used for the option :value fields. It also defaults + a number of things (:search, :closeOnBlue, openOnFocus, selectOnBlue, and :selectOnNavigation) to true, but you can" + (comp/factory WrappedDropdown)) + diff --git a/src/main/com/fulcrologic/rad/rendering/semantic_ui/container.cljc b/src/main/com/fulcrologic/rad/rendering/semantic_ui/container.cljc new file mode 100644 index 0000000..b382459 --- /dev/null +++ b/src/main/com/fulcrologic/rad/rendering/semantic_ui/container.cljc @@ -0,0 +1,85 @@ +(ns com.fulcrologic.rad.rendering.semantic-ui.container + (:require + #?@(:cljs + [[com.fulcrologic.fulcro.dom :as dom :refer [div]]] + :clj + [[com.fulcrologic.fulcro.dom-server :as dom :refer [div]]]) + [com.fulcrologic.fulcro.components :as comp] + [com.fulcrologic.rad.container :as container] + [com.fulcrologic.rad.control :as control] + [com.fulcrologic.rad.options-util :refer [?!]] + [com.fulcrologic.rad.rendering.semantic-ui.form :as sui-form] + [taoensso.timbre :as log])) + +(comp/defsc StandardContainerControls [_ {:keys [instance]}] + {:shouldComponentUpdate (fn [_ _ _] true)} + (let [controls (control/component-controls instance) + {:keys [input-layout action-layout]} (control/standard-control-layout instance)] + (div :.ui.top.attached.compact.basic.segment + (dom/h3 :.ui.header + (or (some-> instance comp/component-options ::container/title (?! instance)) "") + (div :.ui.right.floated.buttons + (keep (fn [k] (control/render-control instance k (get controls k))) action-layout))) + (div :.ui.form + (map-indexed + (fn [idx row] + (div {:key idx :className (sui-form/n-fields-string (count row))} + (map #(if-let [c (get controls %)] + (control/render-control instance % c) + (dom/div :.ui.field {:key (str %)} "")) row))) + input-layout))))) + +(let [ui-standard-container-controls (comp/factory StandardContainerControls)] + (defn render-standard-controls [instance] + (ui-standard-container-controls {:instance instance}))) + +(def n-string {0 "zero" + 1 "one" + 2 "two" + 3 "three" + 4 "four" + 5 "five" + 6 "six" + 7 "seven" + 8 "eight" + 9 "nine" + 10 "ten" + 11 "eleven" + 12 "twelve" + 13 "thirteen" + 14 "fourteen" + 15 "fifteen" + 16 "sixteen"}) + +(defn render-container-layout [container-instance] + (let [{::container/keys [children layout]} (comp/component-options container-instance)] + ;; TODO: Custom controls rendering as a separate config? + (let [container-props (comp/props container-instance) + render-cls (fn [id cls] + (let [factory (comp/computed-factory cls) + props (get container-props id {})] + (factory props {::container/controlled? true})))] + (dom/div :.ui.basic.segments + (render-standard-controls container-instance) + (dom/div :.ui.basic.segment + (if layout + (dom/div :.ui.container.centered.grid + (map-indexed + (fn *render-row [idx row] + (let [cols (count row)] + (dom/div :.row {:key idx} + (map + (fn *render-col [entry] + (let [id (if (keyword? entry) entry (:id entry)) + width (or + (and (map? entry) (:width entry)) + (int (/ 16 cols))) + cls (get children id)] + (dom/div {:key id :classes [(when width (str (n-string width) " wide")) "column"]} + (render-cls id cls)))) + row)))) + layout)) + (map + (fn [[id cls]] + (dom/div {:key id} + (render-cls id cls))) (container/id-child-pairs children)))))))) diff --git a/src/main/com/fulcrologic/rad/rendering/semantic_ui/controls/action_button.cljc b/src/main/com/fulcrologic/rad/rendering/semantic_ui/controls/action_button.cljc new file mode 100644 index 0000000..4d7e692 --- /dev/null +++ b/src/main/com/fulcrologic/rad/rendering/semantic_ui/controls/action_button.cljc @@ -0,0 +1,40 @@ +(ns com.fulcrologic.rad.rendering.semantic-ui.controls.action-button + (:require + [com.fulcrologic.rad.report :as report] + [com.fulcrologic.rad.options-util :refer [?!]] + [com.fulcrologic.fulcro.data-fetch :as df] + [com.fulcrologic.rad.control :as control] + #?(:cljs [com.fulcrologic.fulcro.dom :as dom] + :clj [com.fulcrologic.fulcro.dom-server :as dom]) + [com.fulcrologic.fulcro.components :as comp :refer [defsc]] + #?@(:cljs [[ch.lyrion.carbon.button.ui-button :refer [ui-button]]]))) + +(defsc ActionButton [_ {:keys [instance control-key]}] + {:shouldComponentUpdate (fn [_ _ _] true)} + #?(:cljs + (let [controls (control/component-controls instance) + props (comp/props instance) + {:keys [label icon class action disabled? visible?] :as control} (get controls control-key)] + (when control + (let [label (?! label instance) + class (?! class instance) + loading? (df/loading? (get-in props [df/marker-table (comp/get-ident instance)])) + disabled? (or loading? (?! disabled? instance)) + visible? (or (nil? visible?) (?! visible? instance))] + (when visible? + (ui-button + {:id (str control-key) + :className (when loading? "bx--skeleton") + :disabled (boolean disabled?) + :onClick (fn [] (when action (action instance control-key)))} + (when icon (dom/i {:className (str icon " icon")})) + (when label label)) + #_(dom/button :.bx--btn.bx--btn--sm.bx--skeleton + {:key (str control-key) + :classes [(when class class)] + :disabled (boolean disabled?) + :onClick (fn [] (when action (action instance control-key)))} + (when icon (dom/i {:className (str icon " icon")})) + (when label label)))))))) + +(def render-control (comp/factory ActionButton {:keyfn :control-key})) diff --git a/src/main/com/fulcrologic/rad/rendering/semantic_ui/controls/boolean_control.cljc b/src/main/com/fulcrologic/rad/rendering/semantic_ui/controls/boolean_control.cljc new file mode 100644 index 0000000..a2c2208 --- /dev/null +++ b/src/main/com/fulcrologic/rad/rendering/semantic_ui/controls/boolean_control.cljc @@ -0,0 +1,35 @@ +(ns com.fulcrologic.rad.rendering.semantic-ui.controls.boolean-control + (:require + [com.fulcrologic.fulcro.components :as comp :refer [defsc]] + [com.fulcrologic.fulcro.ui-state-machines :as uism] + [com.fulcrologic.rad.options-util :refer [?!]] + [com.fulcrologic.rad.report :as report] + [taoensso.timbre :as log] + #?(:cljs [com.fulcrologic.fulcro.dom :as dom] + :clj [com.fulcrologic.fulcro.dom-server :as dom]) + [com.fulcrologic.rad.control :as control] + #?@(:cljs [[ch.lyrion.carbon.toggle.ui-toggle :refer [ui-toggle]]]))) + +(defsc BooleanControl [_ {:keys [instance control-key]}] + {:shouldComponentUpdate (fn [_ _ _] true)} + (let [controls (control/component-controls instance) + {:keys [label onChange disabled? visible?] :as control} (get controls control-key)] + (if control + (let [label (or (?! label instance)) + disabled? (?! disabled? instance) + visible? (or (nil? visible?) (?! visible? instance)) + value (control/current-value instance control-key)] + (when visible? + #?(:cljs + (ui-toggle + {:id (str (hash control)) + :defaultToggled (boolean value) + :labelA label + :labelB label + :onToggle (fn [_] + (control/set-parameter! instance control-key (not value)) + (when onChange + (onChange instance (not value))))})))) + (log/error "Could not find control definition for " control-key)))) + +(def render-control (comp/factory BooleanControl {:keyfn :control-key})) diff --git a/src/main/com/fulcrologic/rad/rendering/semantic_ui/controls/control.cljc b/src/main/com/fulcrologic/rad/rendering/semantic_ui/controls/control.cljc new file mode 100644 index 0000000..ac6c27c --- /dev/null +++ b/src/main/com/fulcrologic/rad/rendering/semantic_ui/controls/control.cljc @@ -0,0 +1,35 @@ +(ns com.fulcrologic.rad.rendering.semantic-ui.controls.control + (:require + [com.fulcrologic.fulcro.components :as comp :refer [defsc]] + [com.fulcrologic.fulcro.ui-state-machines :as uism] + [com.fulcrologic.rad.options-util :refer [?!]] + [com.fulcrologic.guardrails.core :refer [>defn =>]] + [com.fulcrologic.rad.report :as report] + [com.fulcrologic.rad.control :as control] + #?(:cljs [com.fulcrologic.fulcro.dom :as dom] + :clj [com.fulcrologic.fulcro.dom-server :as dom]) + [taoensso.timbre :as log])) + +(defsc Control [_ {:keys [instance control control-key input-factory] :as report-env}] + {:shouldComponentUpdate (fn [_ _ _] true)} + (let [controls (control/component-controls instance) + {:keys [label onChange disabled? visible? user-props] :as control} (get controls control-key control)] + (if (and input-factory control) + (let [label (or (?! label instance)) + disabled? (?! disabled? instance) + visible? (or (nil? visible?) (?! visible? instance)) + value (control/current-value instance control-key) + onChange (fn [new-value] + (control/set-parameter! instance control-key new-value) + (when onChange + (onChange instance new-value)))] + (when visible? + (dom/div :.ui.field {:key (str control-key)} + (dom/label label) + (input-factory report-env (merge user-props + {:disabled? disabled? + :value value + :onChange onChange}))))) + (log/error "Cannot render control. Missing input factory or control definition.")))) + +(def ui-control (comp/factory Control {:keyfn :control-key})) diff --git a/src/main/com/fulcrologic/rad/rendering/semantic_ui/controls/instant_inputs.cljc b/src/main/com/fulcrologic/rad/rendering/semantic_ui/controls/instant_inputs.cljc new file mode 100644 index 0000000..70e5737 --- /dev/null +++ b/src/main/com/fulcrologic/rad/rendering/semantic_ui/controls/instant_inputs.cljc @@ -0,0 +1,87 @@ +(ns com.fulcrologic.rad.rendering.semantic-ui.controls.instant-inputs + (:require + [clojure.string :as str] + [com.fulcrologic.guardrails.core :refer [>defn => ?]] + [com.fulcrologic.rad.type-support.date-time :as dt] + [com.fulcrologic.rad.rendering.semantic-ui.controls.control :as control] + [cljc.java-time.local-time :as lt] + [com.fulcrologic.fulcro.dom.events :as evt] + #?(:clj [com.fulcrologic.fulcro.dom-server :as dom] + :cljs [com.fulcrologic.fulcro.dom :as dom]) + [cljc.java-time.local-date-time :as ldt] + [cljc.java-time.local-date :as ld] + #?@(:cljs [[ch.lyrion.carbon.date-picker.ui-date-picker :refer [ui-date-picker]] + [ch.lyrion.carbon.date-picker-input.ui-date-picker-input :refer [ui-date-picker-input]]]))) + +(defn ui-date-instant-input [{::keys [default-local-time]} {:keys [value onChange local-time] :as props}] + #?(:cljs + (let [value (dt/inst->html-date (or value (dt/now))) + local-time (or local-time default-local-time)] + (ui-date-picker + {:id (str (hash props) "-picker") + :datePickerType "single" + :dateFormat "Y-m-d" + :value value + :onChange (fn [evt] + (when onChange + (let [date (.toJSON (first evt)) + date-string (first (str/split date "T")) + instant (dt/html-date->inst date-string local-time)] + (onChange instant))))} + (ui-date-picker-input + {:id (str (dt/now) "ui-date-instant-input")}))))) + +(defn ui-ending-date-instant-input + "Display the date the user selects, but control a value that is midnight on the next date. Used for generating ending + instants that can be used for a proper non-inclusive end date." + [_ {:keys [value onChange] :as props}] + #?(:cljs + (let [today (dt/inst->local-datetime (or value (dt/now))) + display-date (ldt/to-local-date (ldt/minus-days today 1)) + value (dt/local-date->html-date-string display-date)] + (ui-date-picker + {:id (str (hash props) "-picker") + :datePickerType "single" + :dateFormat "Y-m-d" + :value value + :onChange (fn [evt] + (when onChange + (let [date (.toJSON (first evt)) + date-string (first (str/split date "T")) + tomorrow (ld/at-time (ld/plus-days (dt/html-date-string->local-date date-string) 1) + lt/midnight) + instant (dt/local-datetime->inst tomorrow)] + (onChange instant))))} + (ui-date-picker-input + {:id (str (hash props) "-picker-input")}))))) + +(defn ui-date-time-instant-input [_ {:keys [disabled? value onChange] :as props}] + (let [value (dt/inst->html-datetime-string (or value (dt/now)))] + (dom/input + (merge props + (cond-> + {:value value + :type "date" + :onChange (fn [evt] + (when onChange + (let [date-time-string (evt/target-value evt) + instant (dt/html-datetime-string->inst date-time-string)] + (onChange instant))))} + disabled? (assoc :readOnly true)))))) + +(defn date-time-control [render-env] + (control/ui-control (assoc render-env :input-factory ui-date-time-instant-input))) + +(defn midnight-on-date-control [render-env] + (control/ui-control (assoc render-env + :input-factory ui-date-instant-input + ::default-local-time lt/midnight))) + +(defn midnight-next-date-control [render-env] + (control/ui-control (assoc render-env + :input-factory ui-ending-date-instant-input))) + +(defn date-at-noon-control [render-env] + (control/ui-control (assoc render-env + ::default-local-time lt/noon + :input-factory ui-date-instant-input))) diff --git a/src/main/com/fulcrologic/rad/rendering/semantic_ui/controls/pickers.cljc b/src/main/com/fulcrologic/rad/rendering/semantic_ui/controls/pickers.cljc new file mode 100644 index 0000000..117b9a3 --- /dev/null +++ b/src/main/com/fulcrologic/rad/rendering/semantic_ui/controls/pickers.cljc @@ -0,0 +1,72 @@ +(ns com.fulcrologic.rad.rendering.semantic-ui.controls.pickers + (:require + [com.fulcrologic.fulcro.components :as comp :refer [defsc]] + [com.fulcrologic.rad.picker-options :as po] + [com.fulcrologic.rad.control :as control] + [com.fulcrologic.rad.rendering.semantic-ui.components :refer [ui-wrapped-dropdown]] + #?(:cljs [ch.lyrion.carbon.combo-box.ui-combo-box :refer [ui-combo-box]]) + [com.fulcrologic.rad.options-util :refer [?!]] + [taoensso.timbre :as log] + #?(:cljs [com.fulcrologic.fulcro.dom :as dom] + :clj [com.fulcrologic.fulcro.dom-server :as dom]))) + +(defsc SimplePicker [_ {:keys [instance control-key]}] + {:shouldComponentUpdate (fn [_ _ _] true) + :componentDidMount (fn [this] + (let [{:keys [instance control-key] :as props} (comp/props this) + controls (control/component-controls instance) + {::po/keys [query-key] :as picker-options} (get controls control-key)] + (when query-key + (po/load-picker-options! instance (comp/react-type instance) props picker-options))))} + #?(:cljs + (let [controls (control/component-controls instance) + props (comp/props instance) + {::po/keys [query-key cache-key] + :keys [label onChange disabled? visible? action placeholder options user-props] :as control} (get controls control-key) + options (or options (get-in props [::po/options-cache (or cache-key query-key) :options]))] + (when control + (let [label (or (?! label instance)) + disabled? (?! disabled? instance) + placeholder (?! placeholder instance) + visible? (or (nil? visible?) (?! visible? instance)) + value (control/current-value instance control-key)] + (when visible? + (dom/div + (ui-combo-box + {:id (or (str control-key) (str (hash options))) + ;;:titleText label + :disabled (or disabled? false) + :placeholder (str placeholder) + :items (or options []) + :itemToString (fn [item] (.-text item)) + :initialSelectedItem (or value "") + :onChange (fn [v] + (let [selectedItem (.-selectedItem v) + selected-item (if (nil? selectedItem) + (or (first (filter #(= "" (:value %)) options)) + {:text "" :value ""}) + {:text (.-text selectedItem) :value (.-value selectedItem)})] + (js/console.log "Event:" v) + (control/set-parameter! instance control-key (:value selected-item)) + (binding [comp/*after-render* true] + (when onChange + (onChange instance v)) + (when action + (action instance)))))})) + #_(dom/div :.ui.field {:key (str control-key)} + (dom/label label) + (ui-wrapped-dropdown (merge + user-props + {:disabled disabled? + :placeholder (str placeholder) + :options options + :value value + :onChange (fn [v] + (control/set-parameter! instance control-key v) + (binding [comp/*after-render* true] + (when onChange + (onChange instance v)) + (when action + (action instance))))}))))))))) + +(def render-control (comp/factory SimplePicker {:keyfn :control-key})) diff --git a/src/main/com/fulcrologic/rad/rendering/semantic_ui/controls/text_input.cljc b/src/main/com/fulcrologic/rad/rendering/semantic_ui/controls/text_input.cljc new file mode 100644 index 0000000..5849976 --- /dev/null +++ b/src/main/com/fulcrologic/rad/rendering/semantic_ui/controls/text_input.cljc @@ -0,0 +1,59 @@ +(ns com.fulcrologic.rad.rendering.semantic-ui.controls.text-input + (:require + [com.fulcrologic.fulcro.components :as comp :refer [defsc]] + [com.fulcrologic.fulcro.ui-state-machines :as uism] + [com.fulcrologic.fulcro.dom.events :as evt] + [com.fulcrologic.rad.control :as control] + [com.fulcrologic.rad.options-util :refer [?! debounce]] + [taoensso.timbre :as log] + #?(:cljs [com.fulcrologic.fulcro.dom :as dom] + :clj [com.fulcrologic.fulcro.dom-server :as dom]) + #?@(:cljs [[ch.lyrion.carbon.text-input.ui-text-input :refer [ui-text-input]]]))) + +(defsc TextControl [this {:keys [instance control-key]}] + {:shouldComponentUpdate (fn [_ _ _] true)} + #?(:cljs + (let [controls (control/component-controls instance) + props (comp/props instance) + {:keys [label onChange icon placeholder disabled? visible?] :as control} (get controls control-key)] + (when control + (let [label (?! label instance) + disabled? (?! disabled? instance) + placeholder (?! placeholder) + visible? (or (nil? visible?) (?! visible? instance)) + chg! #(control/set-parameter! instance control-key (evt/target-value %)) + run! (fn [evt] (let [v (evt/target-value evt)] + (when onChange (onChange instance v)))) + value (control/current-value instance control-key)] + (when visible? + (dom/div + (when icon + (dom/i {:className (str icon " icon")})) + (ui-text-input + {:id (str control-key) + :disabled (boolean disabled?) + :labelText label + :placeholder (str placeholder) + :onChange chg! + :onBlur run! + :onKeyDown (fn [evt] (when (evt/enter? evt) (run! evt))) + :value (str value)})) + #_(dom/div :.ui.field {:key (str control-key)} + (dom/label label) + (if icon + (dom/div :.ui.icon.input + (dom/i {:className (str icon " icon")}) + (dom/input {:readOnly (boolean disabled?) + :placeholder (str placeholder) + :onChange chg! + :onBlur run! + :onKeyDown (fn [evt] (when (evt/enter? evt) (run! evt))) + :value (str value)})) + (dom/input {:readOnly (boolean disabled?) + :placeholder (str placeholder) + :onChange chg! + :onBlur run! + :onKeyDown (fn [evt] (when (evt/enter? evt) (run! evt))) + :value (str value)}))))))))) + +(def render-control (comp/factory TextControl {:keyfn :control-key})) diff --git a/src/main/com/fulcrologic/rad/rendering/semantic_ui/decimal_field.cljc b/src/main/com/fulcrologic/rad/rendering/semantic_ui/decimal_field.cljc new file mode 100644 index 0000000..3c023ed --- /dev/null +++ b/src/main/com/fulcrologic/rad/rendering/semantic_ui/decimal_field.cljc @@ -0,0 +1,15 @@ +(ns com.fulcrologic.rad.rendering.semantic-ui.decimal-field + (:require + [com.fulcrologic.fulcro.components :as comp] + [com.fulcrologic.fulcro.dom.inputs :as inputs] + [clojure.string :as str] + [com.fulcrologic.rad.rendering.semantic-ui.field :refer [render-field-factory]] + [com.fulcrologic.rad.type-support.decimal :as math])) + +(def ui-decimal-input + (comp/factory (inputs/StringBufferedInput ::DecimalInput + {:model->string (fn [n] (if (math/numeric? n) (math/numeric->str n) "")) + :string->model (fn [s] (math/numeric s)) + :string-filter (fn [s] (str/replace s #"[^\d.]" ""))}))) + +(def render-field (render-field-factory {} ui-decimal-input)) diff --git a/src/main/com/fulcrologic/rad/rendering/semantic_ui/entity_picker.cljc b/src/main/com/fulcrologic/rad/rendering/semantic_ui/entity_picker.cljc new file mode 100644 index 0000000..37f8c8e --- /dev/null +++ b/src/main/com/fulcrologic/rad/rendering/semantic_ui/entity_picker.cljc @@ -0,0 +1,115 @@ +(ns com.fulcrologic.rad.rendering.semantic-ui.entity-picker + (:require + #?(:cljs [com.fulcrologic.fulcro.dom :as dom :refer [div h3 button i span]] + :clj [com.fulcrologic.fulcro.dom-server :as dom :refer [div h3 button i span]]) + [com.fulcrologic.rad.rendering.semantic-ui.components :refer [ui-wrapped-dropdown]] + [com.fulcrologic.fulcro.components :as comp :refer [defsc]] + [com.fulcrologic.rad.form :as form] + [com.fulcrologic.rad.attributes :as attr] + [com.fulcrologic.rad.picker-options :as picker-options] + [com.fulcrologic.rad.ui-validation :as validation] + [com.fulcrologic.rad.options-util :refer [?!]] + [taoensso.timbre :as log])) + +(defsc ToOnePicker [this {:keys [env attr]}] + {:componentDidMount (fn [this] + (let [{:keys [env attr]} (comp/props this) + form-instance (::form/form-instance env) + props (comp/props form-instance) + form-class (comp/react-type form-instance)] + (picker-options/load-options! form-instance form-class props attr)))} + (let [{::form/keys [master-form form-instance]} env + {::form/keys [attributes field-options]} (comp/component-options form-instance) + {::attr/keys [qualified-key required?]} attr + field-options (get field-options qualified-key) + target-id-key (first (keep (fn [{k ::attr/qualified-key ::attr/keys [target]}] + (when (= k qualified-key) target)) attributes)) + {::picker-options/keys [cache-key query-key]} (merge attr field-options) + cache-key (or (?! cache-key (comp/react-type form-instance) (comp/props form-instance)) query-key) + cache-key (or cache-key query-key (log/error "Ref field MUST have either a ::picker-options/cache-key or ::picker-options/query-key in attribute " qualified-key)) + props (comp/props form-instance) + options (get-in props [::picker-options/options-cache cache-key :options]) + value [target-id-key (get-in props [qualified-key target-id-key])] + field-label (form/field-label env attr) + read-only? (or (form/read-only? master-form attr) (form/read-only? form-instance attr)) + invalid? (and (not read-only?) (validation/invalid-attribute-value? env attr)) + onSelect (fn [v] + (form/input-changed! env qualified-key v))] + (div :.ui.field {:classes [(when invalid? "error")]} + (dom/label field-label (when invalid? " (Required)")) + (if read-only? + (let [value (first (filter #(= value (:value %)) options))] + (:text value)) + (ui-wrapped-dropdown (cond-> + {:onChange (fn [v] (onSelect v)) + :value value + :clearable (not required?) + :disabled read-only? + :options options})))))) + +(let [ui-to-one-picker (comp/factory ToOnePicker {:keyfn (fn [{:keys [attr]}] (::attr/qualified-key attr))})] + (defn to-one-picker [env attribute] + (ui-to-one-picker {:env env + :attr attribute}))) + +(defsc ToManyPicker [this {:keys [env attr]}] + {:componentDidMount (fn [this] + (let [{:keys [env attr]} (comp/props this) + form-instance (::form/form-instance env) + props (comp/props form-instance) + form-class (comp/react-type form-instance)] + (picker-options/load-options! form-instance form-class props attr)))} + (let [{::form/keys [form-instance]} env + visible? (form/field-visible? form-instance attr)] + (when visible? + (let [{::form/keys [attributes field-options]} (comp/component-options form-instance) + {attr-field-options ::form/field-options + ::attr/keys [qualified-key]} attr + field-options (get field-options qualified-key) + target-id-key (first (keep (fn [{k ::attr/qualified-key ::attr/keys [target]}] + (when (= k qualified-key) target)) attributes)) + {:keys [style] + ::picker-options/keys [cache-key query-key]} (merge attr-field-options field-options) + cache-key (or (?! cache-key (comp/react-type form-instance) (comp/props form-instance)) query-key) + cache-key (or cache-key query-key (log/error "Ref field MUST have either a ::picker-options/cache-key or ::picker-options/query-key in attribute " qualified-key)) + props (comp/props form-instance) + options (get-in props [::picker-options/options-cache cache-key :options]) + current-selection (into #{} + (keep (fn [entity] + (when-let [id (get entity target-id-key)] + [target-id-key id]))) + (get props qualified-key)) + field-label (form/field-label env attr) + invalid? (validation/invalid-attribute-value? env attr) + read-only? (form/read-only? form-instance attr) + validation-message (when invalid? (validation/validation-error-message env attr))] + (div :.ui.field {:classes [(when invalid? "error")]} + (dom/label field-label " " (when invalid? validation-message)) + (div :.ui.middle.aligned.celled.list.big + {:style {:marginTop "0"}} + (if (= style :dropdown) + (ui-wrapped-dropdown + {:value current-selection + :multiple true + :disabled read-only? + :options options + :onChange (fn [v] (form/input-changed! env qualified-key v))}) + (map (fn [{:keys [text value]}] + (let [checked? (contains? current-selection value)] + (div :.item {:key value} + (div :.content {} + (div :.ui.toggle.checkbox {:style {:marginTop "0"}} + (dom/input + {:type "checkbox" + :checked checked? + :onChange #(if-not checked? + (form/input-changed! env qualified-key (vec (conj current-selection value))) + (form/input-changed! env qualified-key (vec (disj current-selection value))))}) + (dom/label text)))))) + options)))))))) + +(def ui-to-many-picker (comp/factory ToManyPicker {:keyfn :id})) +(let [ui-to-many-picker (comp/factory ToManyPicker {:keyfn (fn [{:keys [attr]}] (::attr/qualified-key attr))})] + (defn to-many-picker [env attribute] + (ui-to-many-picker {:env env + :attr attribute}))) diff --git a/src/main/com/fulcrologic/rad/rendering/semantic_ui/enumerated_field.cljc b/src/main/com/fulcrologic/rad/rendering/semantic_ui/enumerated_field.cljc new file mode 100644 index 0000000..11efb11 --- /dev/null +++ b/src/main/com/fulcrologic/rad/rendering/semantic_ui/enumerated_field.cljc @@ -0,0 +1,79 @@ +(ns com.fulcrologic.rad.rendering.semantic-ui.enumerated-field + (:require + #?@(:cljs + [[com.fulcrologic.fulcro.dom :as dom :refer [div label input]] + [cljs.reader :refer [read-string]] + [com.fulcrologic.semantic-ui.modules.dropdown.ui-dropdown :refer [ui-dropdown]]] + :clj + [[com.fulcrologic.fulcro.dom-server :as dom :refer [div label input]]]) + [com.fulcrologic.rad.ui-validation :as validation] + [com.fulcrologic.fulcro.components :as comp :refer [defsc]] + [com.fulcrologic.rad.options-util :refer [?!]] + [com.fulcrologic.rad.rendering.semantic-ui.components :refer [ui-wrapped-dropdown]] + [com.fulcrologic.rad.attributes :as attr] + [clojure.string :as str] + [com.fulcrologic.rad.form :as form])) + +(defn enumerated-options [{::form/keys [form-instance] :as env} {::attr/keys [qualified-key] :as attribute}] + (let [{::attr/keys [enumerated-values]} attribute + enumeration-labels (merge + (::attr/enumerated-labels attribute) + (comp/component-options form-instance ::form/enumerated-labels qualified-key))] + ;; TODO: Sorting should be something users control + (sort-by :text + (mapv (fn [k] + {:text (?! (get enumeration-labels k (name k))) + :value k}) enumerated-values)))) + +(defn- render-to-many [{::form/keys [form-instance] :as env} {::form/keys [field-label] + ::attr/keys [qualified-key] :as attribute}] + (when (form/field-visible? form-instance attribute) + (let [props (comp/props form-instance) + read-only? (form/read-only? form-instance attribute) + options (enumerated-options env attribute) + selected-ids (set (get props qualified-key))] + (div :.ui.field {:key (str qualified-key)} + (label (or field-label (some-> qualified-key name str/capitalize))) + (div :.ui.middle.aligned.celled.list.big {:style {:marginTop "0"}} + (map (fn [{:keys [text value]}] + (let [checked? (contains? selected-ids value)] + (div :.item {:key value} + (div :.content {} + (div :.ui.toggle.checkbox {:style {:marginTop "0"}} + (dom/input + {:type "checkbox" + :checked checked? + :disabled read-only? + :onChange #(let [selection (if-not checked? + (conj (set (or selected-ids #{})) value) + (disj selected-ids value))] + (form/input-changed! env qualified-key selection))}) + (dom/label text)))))) + options)))))) + +(defn- render-to-one [{::form/keys [form-instance] :as env} {::form/keys [field-label] + ::attr/keys [qualified-key] :as attribute}] + (when (form/field-visible? form-instance attribute) + (let [props (comp/props form-instance) + read-only? (form/read-only? form-instance attribute) + invalid? (validation/invalid-attribute-value? env attribute) + user-props (form/field-style-config env attribute :input/props) + options (enumerated-options env attribute) + value (get props qualified-key)] + (div :.ui.field {:key (str qualified-key) :classes [(when invalid? "error")]} + (label (str (or field-label (some-> qualified-key name str/capitalize)) + (when invalid? " (Required)"))) + (if read-only? + (let [value (first (filter #(= value (:value %)) options))] + (:text value)) + (ui-wrapped-dropdown (merge + {:disabled read-only? + :options options + :value value + :onChange (fn [v] (form/input-changed! env qualified-key v))} + user-props))))))) + +(defn render-field [env {::attr/keys [cardinality] :or {cardinality :one} :as attribute}] + (if (= :many cardinality) + (render-to-many env attribute) + (render-to-one env attribute))) diff --git a/src/main/com/fulcrologic/rad/rendering/semantic_ui/field.cljc b/src/main/com/fulcrologic/rad/rendering/semantic_ui/field.cljc new file mode 100644 index 0000000..b7e8d92 --- /dev/null +++ b/src/main/com/fulcrologic/rad/rendering/semantic_ui/field.cljc @@ -0,0 +1,44 @@ +(ns com.fulcrologic.rad.rendering.semantic-ui.field + (:require + [clojure.string :as str] + [com.fulcrologic.fulcro.components :as comp] + [com.fulcrologic.guardrails.core :refer [>defn =>]] + #?(:cljs [com.fulcrologic.fulcro.dom :refer [div label input span]] + :clj [com.fulcrologic.fulcro.dom-server :refer [div label input span]]) + [com.fulcrologic.rad.attributes :as attr] + [com.fulcrologic.fulcro.dom.html-entities :as ent] + [com.fulcrologic.rad.form :as form] + [com.fulcrologic.rad.ui-validation :as validation] + [taoensso.timbre :as log])) + +(defn render-field-factory + "Create a general field factory using the given input factory as the function to call to draw an input." + ([input-factory] + (render-field-factory {} input-factory)) + ([addl-props input-factory] + (fn [{::form/keys [form-instance] :as env} {::attr/keys [type qualified-key] :as attribute}] + (let [props (comp/props form-instance) + value (or (form/computed-value env attribute) + (and attribute (get props qualified-key))) + invalid? (validation/invalid-attribute-value? env attribute) + validation-message (when invalid? (validation/validation-error-message env attribute)) + user-props (form/field-style-config env attribute :input/props) + field-label (form/field-label env attribute) + visible? (form/field-visible? form-instance attribute) + read-only? (form/read-only? form-instance attribute) + addl-props (if read-only? (assoc addl-props :readOnly "readonly") addl-props)] + (when visible? + (div :.ui.field {:key (str qualified-key) + :classes [(when invalid? "error")]} + (label + (or field-label (some-> qualified-key name str/capitalize)) + (when validation-message (str ent/nbsp "(" validation-message ")"))) + (div :.ui.input {:classes [(when read-only? "transparent")]} + (input-factory (merge addl-props + {:value value + :onBlur (fn [v] (form/input-blur! env qualified-key v)) + :onChange (fn [v] (form/input-changed! env qualified-key v))} + user-props))) + #_(when validation-message + (div :.ui.error.message + (str validation-message))))))))) diff --git a/src/main/com/fulcrologic/rad/rendering/semantic_ui/form.cljc b/src/main/com/fulcrologic/rad/rendering/semantic_ui/form.cljc new file mode 100644 index 0000000..9f8d304 --- /dev/null +++ b/src/main/com/fulcrologic/rad/rendering/semantic_ui/form.cljc @@ -0,0 +1,391 @@ +(ns com.fulcrologic.rad.rendering.semantic-ui.form + (:require + [com.fulcrologic.rad.attributes :as attr] + [com.fulcrologic.rad.options-util :refer [?! narrow-keyword]] + [com.fulcrologic.rad.ui-validation :as validation] + [com.fulcrologic.rad.form :as form] + [com.fulcrologic.rad.control :as control] + [com.fulcrologic.rad.blob :as blob] + [com.fulcrologic.fulcro.dom.events :as evt] + [com.fulcrologic.fulcro-i18n.i18n :refer [tr]] + [com.fulcrologic.fulcro.components :as comp :refer [defsc]] + [com.fulcrologic.fulcro.application :as app] + #?(:cljs [com.fulcrologic.fulcro.dom :as dom :refer [div h3 button i span]] + :clj [com.fulcrologic.fulcro.dom-server :as dom :refer [div h3 button i span]]) + [com.fulcrologic.fulcro.dom.html-entities :as ent] + [com.fulcrologic.fulcro.algorithms.form-state :as fs] + [com.fulcrologic.fulcro.algorithms.tempid :as tempid] + [com.fulcrologic.fulcro.algorithms.merge :as merge] + [taoensso.encore :as enc] + [taoensso.timbre :as log])) + +(defn render-to-many [{::form/keys [form-instance] :as env} {k ::attr/qualified-key :as attr} {::form/keys [subforms] :as options}] + (let [{:semantic-ui/keys [add-position] + ::form/keys [ui title can-delete? can-add? added-via-upload?]} (get subforms k) + form-instance-props (comp/props form-instance) + read-only? (form/read-only? form-instance attr) + add? (if read-only? false (?! can-add? form-instance attr)) + delete? (fn [item] (and (not read-only?) (?! can-delete? form-instance item))) + items (get form-instance-props k) + title (?! (or title (some-> ui (comp/component-options ::form/title)) "") form-instance form-instance-props) + invalid? (validation/invalid-attribute-value? env attr) + validation-message (validation/validation-error-message env attr) + add (when (or (nil? add?) add?) + (let [order (if (keyword? add?) add? :append)] + (if (?! added-via-upload? env) + (dom/input {:type "file" + :onChange (fn [evt] + (log/info "UPLOAD FILE!!!") + (let [new-id (tempid/tempid) + js-file (-> evt blob/evt->js-files first) + attributes (comp/component-options ui ::form/attributes) + id-attr (comp/component-options ui ::form/id) + id-key (::attr/qualified-key id-attr) + {::attr/keys [qualified-key] :as sha-attr} (first (filter ::blob/store + attributes)) + target (conj (comp/get-ident form-instance) k) + new-entity (fs/add-form-config ui + {id-key new-id + qualified-key ""})] + (merge/merge-component! form-instance ui new-entity order target) + (blob/upload-file! form-instance sha-attr js-file {:file-ident [id-key new-id]})))}) + (button :.ui.tiny.icon.button + {:onClick (fn [_] + (form/add-child! (assoc env + ::form/order order + ::form/parent-relation k + ::form/parent form-instance + ::form/child-class ui)))} + (i :.plus.icon))))) + ui-factory (comp/computed-factory ui {:keyfn (fn [item] (-> ui (comp/get-ident item) second str))})] + (div :.ui.container {:key (str k)} + (h3 title (span ent/nbsp ent/nbsp) (when (or (nil? add-position) (= :top add-position)) add)) + (when invalid? + (div :.ui.error.message + validation-message)) + (if (seq items) + (div :.ui.segments + (mapv + (fn [props] + (ui-factory props + (merge + env + {::form/parent form-instance + ::form/parent-relation k + ::form/can-delete? (if delete? (delete? props) false)}))) + items)) + (div :.ui.message "None.")) + (when (= :bottom add-position) add)))) + +(defn render-to-one [{::form/keys [form-instance] :as env} {k ::attr/qualified-key :as attr} {::form/keys [subforms] :as options}] + (let [{::form/keys [ui can-delete? title]} (get subforms k) + form-props (comp/props form-instance) + props (get form-props k) + title (?! (or title (some-> ui (comp/component-options ::form/title)) "") form-instance form-props) + ui-factory (comp/computed-factory ui) + invalid? (validation/invalid-attribute-value? env attr) + validation-message (validation/validation-error-message env attr) + std-props {::form/nested? true + ::form/parent form-instance + ::form/parent-relation k + ::form/can-delete? (or + (?! can-delete? form-instance form-props) + false)}] + (cond + props + (div {:key (str k)} + (h3 :.ui.header title) + (when invalid? + (div :.ui.error.message validation-message)) + (ui-factory props (merge env std-props))) + + :else + (div {:key (str k)} + (h3 :.ui.header title) + (button {:onClick (fn [] (form/add-child! (assoc env + ::form/parent-relation k + ::form/parent form-instance + ::form/child-class ui)))} "Create"))))) + +(defn standard-ref-container [env {::attr/keys [cardinality] :as attr} options] + (if (= :many cardinality) + (render-to-many env attr options) + (render-to-one env attr options))) + +(defn render-single-file [{::form/keys [form-instance] :as env} {k ::attr/qualified-key :as attr} {::form/keys [subforms] :as options}] + (let [{::form/keys [ui can-delete?]} (get subforms k) + parent (comp/props form-instance) + form-props (comp/props form-instance) + props (get form-props k) + ui-factory (comp/computed-factory ui) + label (form/field-label env attr) + std-props {::form/nested? true + ::form/parent form-instance + ::form/parent-relation k + ::form/can-delete? (if can-delete? + (can-delete? parent props) + false)}] + (if props + (div :.field {:key (str k)} + (dom/label label) + (ui-factory props (merge env std-props))) + (div {:key (str k)} + (div "Upload??? (TODO)"))))) + +(defsc ManyFiles [this {{::form/keys [form-instance master-form] :as env} :env + {k ::attr/qualified-key :as attr} :attribute + {::form/keys [subforms] :as options} :options}] + {:initLocalState (fn [this] {:input-key (str (rand-int 1000000))})} + (let [{:semantic-ui/keys [add-position] + ::form/keys [ui title can-delete? can-add? sort-children]} (get subforms k) + form-instance-props (comp/props form-instance) + read-only? (or + (form/read-only? master-form attr) + (form/read-only? form-instance attr)) + add? (if read-only? false (?! can-add? form-instance attr)) + delete? (if read-only? false (fn [item] (?! can-delete? form-instance item))) + items (-> form-instance comp/props k + (cond-> + sort-children sort-children)) + title (?! (or title (some-> ui (comp/component-options ::form/title)) "") form-instance form-instance-props) + upload-id (str k "-file-upload") + add (when (or (nil? add?) add?) + (dom/div + (dom/label :.ui.green.button {:htmlFor upload-id} + (dom/i :.ui.plus.icon) + "Add File") + (dom/input {:type "file" + ;; trick: changing the key on change clears the input, so a failed upload can be retried + :key (comp/get-state this :input-key) + :id upload-id + :style {:zIndex -1 + :width "1px" + :height "1px" + :opacity 0} + :onChange (fn [evt] + (let [new-id (tempid/tempid) + js-file (-> evt blob/evt->js-files first) + attributes (comp/component-options ui ::form/attributes) + id-attr (comp/component-options ui ::form/id) + id-key (::attr/qualified-key id-attr) + {::attr/keys [qualified-key] :as sha-attr} (first (filter ::blob/store + attributes)) + target (conj (comp/get-ident form-instance) k) + new-entity (fs/add-form-config ui + {id-key new-id + qualified-key ""})] + (merge/merge-component! form-instance ui new-entity :append target) + (blob/upload-file! form-instance sha-attr js-file {:file-ident [id-key new-id]}) + (comp/set-state! this {:input-key (str (rand-int 1000000))})))}))) + ui-factory (comp/computed-factory ui {:keyfn (fn [item] (-> ui (comp/get-ident item) second str))})] + (div :.ui.basic.segment {:key (str k)} + (dom/h2 :.ui.header title) + (when (or (nil? add-position) (= :top add-position)) add) + (if (seq items) + (div :.ui.very.relaxed.items + (mapv + (fn [props] + (ui-factory props + (merge + env + {::form/parent form-instance + ::form/parent-relation k + ::form/can-delete? (if delete? (?! delete? props) false)}))) + items)) + (div :.ui.message + "None")) + + (when (= :bottom add-position) add)))) + +(def ui-many-files (comp/factory ManyFiles {:keyfn (fn [{:keys [attribute]}] (::attr/qualified-key attribute))})) + +(defn file-ref-container + [env {::attr/keys [cardinality] :as attr} options] + (if (= :many cardinality) + (ui-many-files {:env env :attribute attr :options options}) + (render-single-file env attr options))) + +(defn render-attribute [env attr {::form/keys [subforms] :as options}] + (let [{k ::attr/qualified-key} attr] + (if (contains? subforms k) + (let [render-ref (or (form/ref-container-renderer env attr) standard-ref-container)] + (render-ref env attr options)) + (form/render-field env attr)))) + +(def n-fields-string {1 "one field" + 2 "two fields" + 3 "three fields" + 4 "four fields" + 5 "five fields" + 6 "six fields" + 7 "seven fields"}) + +(def attribute-map (memoize + (fn [attributes] + (reduce + (fn [m {::attr/keys [qualified-key] :as attr}] + (assoc m qualified-key attr)) + {} + attributes)))) + +(defn- render-layout* [env options k->attribute layout] + (when #?(:clj true :cljs goog.DEBUG) + (when-not (and (vector? layout) (every? vector? layout)) + (log/error "::form/layout must be a vector of vectors!"))) + (try + (into [] + (map-indexed + (fn [idx row] + (div {:key idx :className (n-fields-string (count row))} + (mapv (fn [col] + (enc/if-let [_ k->attribute + attr (k->attribute col)] + (render-attribute env attr options) + (if (some-> options ::control/controls (get col)) + (control/render-control (::form/form-instance env) col) + (log/error "Missing attribute (or lookup) for" col)))) + row))) + layout)) + (catch #?(:clj Exception :cljs :default) _))) + +(defn render-layout [env {::form/keys [attributes layout] :as options}] + (let [k->attribute (attribute-map attributes)] + (render-layout* env options k->attribute layout))) + +(defsc TabbedLayout [this env {::form/keys [attributes tabbed-layout] :as options}] + {:initLocalState (fn [this] + (try + {:current-tab 0 + :tab-details (memoize + (fn [attributes tabbed-layout] + (let [k->attr (attribute-map attributes) + tab-labels (filterv string? tabbed-layout) + tab-label->layout (into {} + (map vec) + (partition 2 (mapv first (partition-by string? tabbed-layout))))] + {:k->attr k->attr + :tab-labels tab-labels + :tab-label->layout tab-label->layout})))} + (catch #?(:clj Exception :cljs :default) _ + (log/error "Cannot build tabs for tabbed layout. Check your tabbed-layout options for" (comp/component-name this)))))} + (let [{:keys [tab-details current-tab]} (comp/get-state this) + {:keys [k->attr tab-labels tab-label->layout]} (tab-details attributes tabbed-layout) + active-layout (some->> current-tab + (get tab-labels) + (get tab-label->layout))] + (div {:key (str current-tab)} + (div :.ui.pointing.menu {} + (map-indexed + (fn [idx title] + (dom/a :.item + {:key (str idx) + :onClick #(comp/set-state! this {:current-tab idx}) + :classes [(when (= current-tab idx) "active")]} + title)) tab-labels)) + (div :.ui.segment + (render-layout* env options k->attr active-layout))))) + +(def ui-tabbed-layout (comp/computed-factory TabbedLayout)) + +(declare standard-form-layout-renderer) + +(defsc StandardFormContainer [this {::form/keys [props computed-props form-instance master-form] :as env}] + {:shouldComponentUpdate (fn [_ _ _] true)} + (let [{::form/keys [can-delete?]} computed-props + nested? (not= master-form form-instance) + read-only-form? (or + (?! (comp/component-options form-instance ::form/read-only?) form-instance) + (?! (comp/component-options master-form ::form/read-only?) master-form)) + invalid? (if read-only-form? false (form/invalid? env)) + render-fields (or (form/form-layout-renderer env) standard-form-layout-renderer)] + (when #?(:cljs goog.DEBUG :clj true) + (let [valid? (if read-only-form? true (form/valid? env)) + dirty? (if read-only-form? false (or (:ui/new? props) (fs/dirty? props)))] + (log/debug "Form " (comp/component-name form-instance) " valid? " valid?) + (log/debug "Form " (comp/component-name form-instance) " dirty? " dirty?))) + (if nested? + (div :.ui.segment + (div :.ui.form {:classes [(when invalid? "error")] + :key (str (comp/get-ident form-instance))} + (when can-delete? + (button :.ui.icon.primary.right.floated.button {:disabled (not can-delete?) + :onClick (fn [] + (form/delete-child! env))} + (i :.times.icon))) + (render-fields env))) + (let [{::form/keys [title action-buttons controls]} (comp/component-options form-instance) + title (?! title form-instance props) + action-buttons (if action-buttons action-buttons form/standard-action-buttons)] + (div :.ui.container {:key (str (comp/get-ident form-instance))} + (div :.ui.top.attached.segment + (dom/h3 :.ui.header + title + (div :.ui.right.floated.buttons + (keep #(control/render-control master-form %) action-buttons)))) + (div :.ui.attached.form {:classes [(when invalid? "error")]} + (div :.ui.error.message (tr "The form has errors and cannot be saved.")) + (div :.ui.attached.segment + (render-fields env)))))))) + +(def standard-form-container (comp/factory StandardFormContainer)) + +(defn standard-form-layout-renderer [{::form/keys [form-instance] :as env}] + (let [{::form/keys [attributes layout tabbed-layout] :as options} (comp/component-options form-instance)] + (cond + (vector? layout) (render-layout env options) + (vector? tabbed-layout) (ui-tabbed-layout env options) + :else (mapv (fn [attr] (render-attribute env attr options)) attributes)))) + +(defn- file-icon-renderer* [{::form/keys [form-instance] :as env}] + (let [{::form/keys [attributes] :as options} (comp/component-options form-instance) + attribute (first (filter ::blob/store attributes)) + sha-key (::attr/qualified-key attribute) + file-key (blob/filename-key sha-key) + url-key (blob/url-key sha-key) + props (comp/props form-instance) + filename (get props file-key "File") + dirty? (fs/dirty? props sha-key) + failed? (blob/failed-upload? props sha-key) + invalid? (validation/invalid-attribute-value? env attribute) + pct (blob/upload-percentage props sha-key) + sha (get props sha-key) + url (get props url-key)] + (if (blob/uploading? props sha-key) + (dom/span :.item {:key (str sha)} + (dom/div :.ui.tiny.image + (dom/i :.huge.file.icon) + (dom/div :.ui.active.red.loader {:style {:marginLeft "-10px"}}) + (dom/div :.ui.bottom.attached.blue.progress {:data-percent pct} + (div :.bar {:style {:transitionDuration "300ms" + :width pct}} + (div :.progress "")))) + (div :.middle.aligned.content + filename) + (dom/button :.ui.red.icon.button {:onClick (fn [] + (app/abort! form-instance sha) + (form/delete-child! env))} + (dom/i :.times.icon))) + ((if dirty? dom/span dom/a) :.item + {:target "_blank" + :key (str sha) + :href (str url "?filename=" filename) + :onClick (fn [evt] + #?(:cljs (when-not (or (not (blob/blob-downloadable? props sha-key)) + (js/confirm "View/download?")) + (evt/stop-propagation! evt) + (evt/prevent-default! evt))))} + (dom/div :.ui.tiny.image + (if failed? + (dom/i :.huge.skull.crossbones.icon) + (dom/i :.huge.file.icon))) + (div :.middle.aligned.content + (str filename (cond failed? " (Upload failed. Delete and try again.)" + dirty? " (unsaved)"))) + (dom/button :.ui.red.icon.button {:onClick (fn [evt] + (evt/stop-propagation! evt) + (evt/prevent-default! evt) + (when #?(:clj true :cljs (js/confirm "Permanently Delete File?")) + (form/delete-child! env)))} + (dom/i :.times.icon)))))) + +(defn file-icon-renderer [env] (file-icon-renderer* env)) diff --git a/src/main/com/fulcrologic/rad/rendering/semantic_ui/instant_field.cljc b/src/main/com/fulcrologic/rad/rendering/semantic_ui/instant_field.cljc new file mode 100644 index 0000000..a7ace9c --- /dev/null +++ b/src/main/com/fulcrologic/rad/rendering/semantic_ui/instant_field.cljc @@ -0,0 +1,34 @@ +(ns com.fulcrologic.rad.rendering.semantic-ui.instant-field + (:require + [clojure.string :as str] + [com.fulcrologic.fulcro.components :as comp] + #?(:cljs [com.fulcrologic.fulcro.dom :refer [div label input]] + :clj [com.fulcrologic.fulcro.dom-server :refer [div label input]]) + [com.fulcrologic.fulcro.dom.inputs :as inputs] + [taoensso.timbre :as log] + [com.fulcrologic.rad.type-support.date-time :as datetime] + [com.fulcrologic.rad.rendering.semantic-ui.field :refer [render-field-factory]])) + +(def ui-datetime-input + (comp/factory (inputs/StringBufferedInput ::DateTimeInput + {:model->string (fn [tm] + (if tm + (datetime/inst->html-datetime-string tm) + "")) + :string->model (fn [s] (some-> s (datetime/html-datetime-string->inst)))}))) + +(def ui-date-noon-input + (comp/factory (inputs/StringBufferedInput ::DateTimeInput + {:model->string (fn [tm] + (if tm + (str/replace (datetime/inst->html-datetime-string tm) #"T.*$" "") + "")) + :string->model (fn [s] (some-> s (str "T12:00") (datetime/html-datetime-string->inst)))}))) + +(def render-field + "Uses current timezone and gathers date/time." + (render-field-factory {:type "datetime-local"} ui-datetime-input)) +(def render-date-at-noon-field + "Uses current timezone and gathers a local date but saves it as an instant at noon on that date." + (render-field-factory {:type "date"} ui-date-noon-input)) + diff --git a/src/main/com/fulcrologic/rad/rendering/semantic_ui/int_field.cljc b/src/main/com/fulcrologic/rad/rendering/semantic_ui/int_field.cljc new file mode 100644 index 0000000..834cdca --- /dev/null +++ b/src/main/com/fulcrologic/rad/rendering/semantic_ui/int_field.cljc @@ -0,0 +1,6 @@ +(ns com.fulcrologic.rad.rendering.semantic-ui.int-field + (:require + [com.fulcrologic.fulcro.dom.inputs :as inputs] + [com.fulcrologic.rad.rendering.semantic-ui.field :refer [render-field-factory]])) + +(def render-field (render-field-factory inputs/ui-int-input)) diff --git a/src/main/com/fulcrologic/rad/rendering/semantic_ui/report.cljc b/src/main/com/fulcrologic/rad/rendering/semantic_ui/report.cljc new file mode 100644 index 0000000..f09c0d0 --- /dev/null +++ b/src/main/com/fulcrologic/rad/rendering/semantic_ui/report.cljc @@ -0,0 +1,360 @@ +(ns com.fulcrologic.rad.rendering.semantic-ui.report + (:require + [clojure.string :as str] + [com.fulcrologic.rad.attributes :as attr] + [com.fulcrologic.rad.report :as report] + [com.fulcrologic.rad.control :as control] + [com.fulcrologic.fulcro.components :as comp] + #?@(:cljs + [[com.fulcrologic.fulcro.dom :as dom :refer [div]] + [com.fulcrologic.semantic-ui.addons.pagination.ui-pagination :as sui-pagination] + [ch.lyrion.carbon.data-table-skeleton.ui-data-table-skeleton :refer [ui-data-table-skeleton]] + [ch.lyrion.carbon.data-table.ui-data-table :refer [ui-data-table]] + [ch.lyrion.carbon.data-table.ui-table :refer [ui-table]] + [ch.lyrion.carbon.data-table.ui-table-container :refer [ui-table-container]] + [ch.lyrion.carbon.data-table.ui-table-head :refer [ui-table-head]] + [ch.lyrion.carbon.data-table.ui-table-header :refer [ui-table-header]] + [ch.lyrion.carbon.data-table.ui-table-body :refer [ui-table-body]] + [ch.lyrion.carbon.data-table.ui-table-row :refer [ui-table-row]] + [ch.lyrion.carbon.data-table.ui-table-cell :refer [ui-table-cell]] + [ch.lyrion.carbon.data-table.ui-table-toolbar :refer [ui-table-toolbar]] + [ch.lyrion.carbon.data-table.ui-table-toolbar-action :refer [ui-table-toolbar-action]] + [ch.lyrion.carbon.data-table.ui-table-toolbar-content :refer [ui-table-toolbar-content]] + [ch.lyrion.carbon.link.ui-link :refer [ui-link]] + [ch.lyrion.carbon.overflow-menu.ui-overflow-menu :refer [ui-overflow-menu]] + [ch.lyrion.carbon.overflow-menu-item.ui-overflow-menu-item :refer [ui-overflow-menu-item]]] + :clj + [[com.fulcrologic.fulcro.dom-server :as dom :refer [div]]]) + [com.fulcrologic.fulcro.data-fetch :as df] + [com.fulcrologic.rad.rendering.semantic-ui.form :as sui-form] + [taoensso.timbre :as log] + [com.fulcrologic.rad.options-util :refer [?!]] + [com.fulcrologic.rad.form :as form] + [com.fulcrologic.fulcro.dom.events :as evt])) + +(defn row-action-buttons [report-instance row-props] + (let [{::report/keys [row-actions]} (comp/component-options report-instance)] + (when (seq row-actions) + #?(:cljs + (ui-overflow-menu + {} + (map-indexed + (fn [idx {:keys [label reload? visible? disabled? action]}] + (when (or (nil? visible?) (?! visible? report-instance row-props)) + (ui-overflow-menu-item + {:key idx + :disabled (boolean (?! disabled? report-instance row-props)) + :itemText (?! label report-instance row-props) + :onClick (fn [evt] + (evt/stop-propagation! evt) + (when action + (action report-instance row-props) + (when reload? + (control/run! report-instance))))}))) + row-actions)))))) + +(comp/defsc TableRowLayout [_ {:keys [report-instance props] :as rp}] + {} + #?(:cljs + (let [{::report/keys [columns link links]} (comp/component-options report-instance) + links (or links link) + action-buttons (row-action-buttons report-instance props) + {:keys [highlighted?] + ::report/keys [idx]} (comp/get-computed props)] + (ui-table-row + {:isSelected highlighted? + :onClick (fn [evt] + (evt/stop-propagation! evt) + (report/select-row! report-instance idx))} + (let [trow (mapv + (fn [{::attr/keys [qualified-key] :as column}] + (let [column-classes (report/column-classes report-instance column) + {:keys [edit-form entity-id]} (report/form-link report-instance props qualified-key) + link-fn (get links qualified-key) + label (report/formatted-column-value report-instance props column)] + (ui-table-cell + {:key (str "col-" qualified-key) + :classes [column-classes]} + (cond + edit-form (ui-link {:href "#" + :onClick (fn [evt] + (evt/stop-propagation! evt) + (form/edit! report-instance edit-form entity-id))} + label) + (fn? link-fn) (ui-link {:href "#" + :onClick (fn [evt] + (evt/stop-propagation! evt) + (link-fn report-instance props))} + label) + :else label)))) + columns)] + (if action-buttons + (conj trow + (ui-table-cell {:key "menu" + :width "5em" + :className "bx--table-column-menu"} + action-buttons)) + trow)))))) + +(let [ui-table-row-layout (comp/factory TableRowLayout)] + (defn render-table-row [report-instance row-class row-props] + (ui-table-row-layout {:report-instance report-instance + :row-class row-class + :props row-props}))) + +(comp/defsc ListRowLayout [this {:keys [report-instance props]}] + {} + (let [{::report/keys [columns]} (comp/component-options report-instance)] + (let [header-column (first columns) + description-column (second columns) + {:keys [edit-form entity-id]} (some->> header-column (::attr/qualified-key) (report/form-link report-instance props)) + header-label (some->> header-column (report/formatted-column-value report-instance props)) + description-label (some->> description-column (report/formatted-column-value report-instance props)) + action-buttons (row-action-buttons report-instance props)] + (div :.item + (div :.content + (when action-buttons + (div :.right.floated.content + action-buttons)) + (when header-label + (if edit-form + (dom/a :.header {:onClick (fn [evt] + (evt/stop-propagation! evt) + (form/edit! report-instance edit-form entity-id))} header-label) + (div :.header header-label))) + (when description-label + (div :.description description-label))))))) + +(let [ui-list-row-layout (comp/factory ListRowLayout {:keyfn ::report/idx})] + (defn render-list-row [report-instance row-class row-props] + (ui-list-row-layout {:report-instance report-instance + :row-class row-class + :props row-props}))) + +(comp/defsc StandardReportControls [this {:keys [report-instance] :as env}] + {:shouldComponentUpdate (fn [_ _ _] true)} + #?(:cljs + (let [controls (control/component-controls report-instance) + {:keys [::report/paginate?]} (comp/component-options report-instance) + {:keys [input-layout action-layout]} (control/standard-control-layout report-instance) + {:com.fulcrologic.rad.container/keys [controlled?]} (comp/get-computed report-instance)] + (comp/fragment + (ui-table-toolbar + {} + (flatten [(map-indexed + (fn [idx row] + (div {:key idx :className (sui-form/n-fields-string (count row))} + (keep #(let [control (get controls %)] + (when (or (not controlled?) (:local? control)) + (ui-table-toolbar-content {:key (str (hash %))} (control/render-control report-instance % control)))) + row))) + input-layout) + (ui-table-toolbar-content + {:key (str (hash env) "-toolbar-content")} + (dom/div :.bx--btn-set + (keep (fn [k] + (let [control (get controls k)] + (when (or (not controlled?) (:local? control)) + (control/render-control report-instance k control)))) + action-layout)))])))))) + +(let [ui-standard-report-controls (comp/factory StandardReportControls)] + (defn render-standard-controls [report-instance] + (ui-standard-report-controls {:report-instance report-instance}))) + +(comp/defsc ListReportLayout [this {:keys [report-instance] :as env}] + {:shouldComponentUpdate (fn [_ _ _] true) + :initLocalState (fn [_] {:row-factory (memoize + (fn [cls] + (comp/computed-factory cls + {:keyfn (fn [props] (some-> props (comp/get-computed ::report/idx)))})))})} + (let [{::report/keys [BodyItem]} (comp/component-options report-instance) + render-row ((comp/get-state this :row-factory) BodyItem) + render-controls (report/control-renderer this) + rows (report/current-rows report-instance) + loading? (report/loading? report-instance)] + (div + (when render-controls + (render-controls report-instance)) + (div :.ui.attached.segment + (div :.ui.loader {:classes [(when loading? "active")]}) + (when (seq rows) + (div :.ui.relaxed.divided.list + (map-indexed (fn [idx row] (render-row row {:report-instance report-instance + :row-class BodyItem + ::report/idx idx})) rows))))))) + +(let [ui-list-report-layout (comp/factory ListReportLayout {:keyfn ::report/idx})] + (defn render-list-report-layout [report-instance] + (ui-list-report-layout {:report-instance report-instance}))) + +(defn get-id-key [mapkeys wanted] + ;;(first) + (first + (filter #(= (name wanted) (name %)) + mapkeys))) + +(defn render-standard-table [this {:keys [report-instance]}] + (let [{report-column-headings ::report/column-headings + ::report/keys [columns row-actions BodyItem compare-rows table-class]} (comp/component-options report-instance) + render-row ((comp/get-state this :row-factory) BodyItem) + column-headings (mapv (fn [{::report/keys [column-heading] + ::attr/keys [qualified-key] :as attr}] + {:column attr + :label (or + (?! (get report-column-headings qualified-key) report-instance) + (?! column-heading report-instance) + (some-> qualified-key name str/capitalize) + "")}) + columns) + clj-rows (report/current-rows report-instance) + id-key (get-id-key (keys (first clj-rows)) :id) + props (comp/props report-instance) + sort-params (-> props :ui/parameters ::report/sort) + sortable? (if-not (boolean compare-rows) + (constantly false) + (if-let [sortable-columns (some-> sort-params :sortable-columns set)] + (fn [{::attr/keys [qualified-key]}] (contains? sortable-columns qualified-key)) + (constantly true))) + ascending? (and sortable? (:ascending? sort-params)) + sorting-by (and sortable? (:sort-by sort-params)) + has-row-actions? (seq row-actions)] + #?(:cljs + (ui-table + {:className table-class} + (ui-table-head + {} + (ui-table-row + {} + (let [trow + (into [] + (map-indexed (fn [idx {:keys [label column]}] + (let [sorting (atom (if (= sorting-by (::attr/qualified-key column)) + (if ascending? "ASC" "DESC") + "NONE"))] + (ui-table-header {:key idx + :sortDirection @sorting #_(when (= sorting-by (::attr/qualified-key column)) + (if ascending? "ASC" "DESC")) + :isSortHeader (not= "NONE" @sorting) + :isSortable (sortable? column) + :onClick (fn [evt] + (js/console.log (::attr/qualified-key column)) + (js/console.log "Ascending?" ascending?) + (evt/stop-propagation! evt) + (report/sort-rows! report-instance column) + (reset! sorting (if ascending? "ASC" "DESC")))} + label))) + column-headings))] + (if has-row-actions? + (conj trow (ui-table-header {:key "menu"})) + trow)))) + (when (seq clj-rows) + (ui-table-body + {} (map-indexed + (fn [idx row] + (let [highlighted-row-idx (report/currently-selected-row report-instance)] + (render-row row {:report-instance report-instance + :row-class BodyItem + :highlighted? (= idx highlighted-row-idx) + ::report/idx idx}))) + clj-rows))))))) + +(defn render-rotated-table [_ {:keys [report-instance] :as env}] + (let [{report-column-headings ::report/column-headings + ::report/keys [columns row-actions compare-rows table-class]} (comp/component-options report-instance) + props (comp/props report-instance) + sort-params (-> props :ui/parameters ::report/sort) + sortable? (if-not (boolean compare-rows) + (constantly false) + (if-let [sortable-columns (some-> sort-params :sortable-columns set)] + (fn [{::attr/keys [qualified-key]}] (contains? sortable-columns qualified-key)) + (constantly true))) + ascending? (and sortable? (:ascending? sort-params)) + sorting-by (and sortable? (:sort-by sort-params)) + row-headings (mapv (fn [{::report/keys [column-heading] + ::attr/keys [qualified-key] :as attr}] + (let [label (or + (?! (get report-column-headings qualified-key) report-instance) + (?! column-heading report-instance) + (some-> qualified-key name str/capitalize) + "")] + (if (sortable? attr) + (dom/a {:onClick (fn [evt] + (evt/stop-propagation! evt) + (report/sort-rows! report-instance attr))} + label + (when (= sorting-by (::attr/qualified-key attr)) + (if ascending? + (dom/i :.angle.down.icon) + (dom/i :.angle.up.icon)))) + label))) + columns) + rows (report/current-rows report-instance) + has-row-actions? (seq row-actions)] + (dom/table :.ui.compact.collapsing.definition.selectable.table {:classes [table-class]} + (when (seq rows) + (comp/fragment + (dom/thead + (let [col (first columns)] + (dom/tr {:key "hrow"} + (dom/th + (get row-headings 0)) + (map-indexed + (fn [idx row] + (dom/th {:key idx} + (report/formatted-column-value report-instance row col))) rows) + (when has-row-actions? + (dom/td {:key "actions"} + (row-action-buttons report-instance col)))))) + (dom/tbody + (map-indexed + (fn [idx col] + (dom/tr {:key idx} + (dom/td (get row-headings (inc idx))) + (map-indexed + (fn [idx row] + (dom/td {:key idx :className "right aligned"} + (report/formatted-column-value report-instance row col))) rows) + (when has-row-actions? + (dom/td {:key "actions"} + (row-action-buttons report-instance col))))) + (rest columns)))))))) + +(comp/defsc TableReportLayout [this {:keys [report-instance] :as env}] + {:initLocalState (fn [this] {:row-factory (memoize (fn [cls] (comp/computed-factory cls + {:keyfn (fn [props] + (some-> props (comp/get-computed ::report/idx)))})))}) + :shouldComponentUpdate (fn [_ _ _] true)} + #?(:cljs + (let [{::report/keys [rotate?]} (comp/component-options report-instance) + rotate? (?! rotate? report-instance) + render-controls (report/control-renderer report-instance) + loading? (report/loading? report-instance) + controlled? (comp/get-computed report-instance :com.fulcrologic.rad.container/controlled?) + props (comp/props report-instance) + busy? (:ui/busy? props)] + (div + (ui-data-table-skeleton + {:style {:width "100%" + :position "absolute" + :opacity (if (or busy? loading?) 1.0 0.0) + :top 0 + :left 0}}) + (ui-table-container + {:key (str (hash report-instance)) + :className (if (or busy? loading?) "bx--skeleton") + :style {:width "100%" + :position "absolute" + :top 0 + :left 0 + :zIndex 10} + :title (or (some-> report-instance comp/component-options ::report/title (?! report-instance)) "Report")} + (when render-controls + (render-controls report-instance)) + (if rotate? + (render-rotated-table this env) + (render-standard-table this env))))))) + +(let [ui-table-report-layout (comp/factory TableReportLayout {:keyfn ::report/idx})] + (defn render-table-report-layout [this] + (ui-table-report-layout {:report-instance this}))) diff --git a/src/main/com/fulcrologic/rad/rendering/semantic_ui/semantic_ui_controls.cljc b/src/main/com/fulcrologic/rad/rendering/semantic_ui/semantic_ui_controls.cljc new file mode 100644 index 0000000..eed4d5f --- /dev/null +++ b/src/main/com/fulcrologic/rad/rendering/semantic_ui/semantic_ui_controls.cljc @@ -0,0 +1,78 @@ +(ns com.fulcrologic.rad.rendering.semantic-ui.semantic-ui-controls + "This ns requires all semantic UI renderers for form controls and includes a map that can be installed to + set SUI as the default control set." + (:require + [com.fulcrologic.rad.rendering.semantic-ui.form :as sui-form] + [com.fulcrologic.rad.rendering.semantic-ui.container :as sui-container] + [com.fulcrologic.rad.rendering.semantic-ui.entity-picker :as entity-picker] + [com.fulcrologic.rad.rendering.semantic-ui.report :as sui-report] + [com.fulcrologic.rad.rendering.semantic-ui.boolean-field :as boolean-field] + [com.fulcrologic.rad.rendering.semantic-ui.decimal-field :as decimal-field] + [com.fulcrologic.rad.rendering.semantic-ui.int-field :as int-field] + [com.fulcrologic.rad.rendering.semantic-ui.controls.boolean-control :as boolean-input] + [com.fulcrologic.rad.rendering.semantic-ui.controls.action-button :as action-button] + [com.fulcrologic.rad.rendering.semantic-ui.controls.text-input :as text-input] + [com.fulcrologic.rad.rendering.semantic-ui.controls.instant-inputs :as instant-input] + [com.fulcrologic.rad.rendering.semantic-ui.controls.pickers :as picker-controls] + [com.fulcrologic.rad.rendering.semantic-ui.instant-field :as instant] + [com.fulcrologic.rad.rendering.semantic-ui.enumerated-field :as enumerated-field] + [com.fulcrologic.rad.rendering.semantic-ui.blob-field :as blob-field] + [com.fulcrologic.rad.rendering.semantic-ui.autocomplete :as autocomplete] + [com.fulcrologic.rad.rendering.semantic-ui.text-field :as text-field])) + +(def all-controls + {;; Form-related UI + ;; completely configurable map...element types are malleable as are the styles. Plugins will need to doc where + ;; they vary from the "standard" set. + :com.fulcrologic.rad.form/element->style->layout + {:form-container {:default sui-form/standard-form-container + :file-as-icon sui-form/file-icon-renderer} + :form-body-container {:default sui-form/standard-form-layout-renderer} + :ref-container {:default sui-form/standard-ref-container + :file sui-form/file-ref-container}} + + :com.fulcrologic.rad.form/type->style->control + {:text {:default text-field/render-field} + :enum {:default enumerated-field/render-field + :autocomplete autocomplete/render-autocomplete-field} + :string {:default text-field/render-field + :autocomplete autocomplete/render-autocomplete-field + :viewable-password text-field/render-viewable-password + :password text-field/render-password + :sorted-set text-field/render-dropdown + :com.fulcrologic.rad.blob/file-upload blob-field/render-file-upload} + :int {:default int-field/render-field} + :long {:default int-field/render-field} + :decimal {:default decimal-field/render-field} + :boolean {:default boolean-field/render-field} + :instant {:default instant/render-field + :date-at-noon instant/render-date-at-noon-field} + :ref {:pick-one entity-picker/to-one-picker + :pick-many entity-picker/to-many-picker}} + + ;; Report-related controls + :com.fulcrologic.rad.report/style->layout + {:default sui-report/render-table-report-layout + :list sui-report/render-list-report-layout} + + :com.fulcrologic.rad.report/control-style->control + {:default sui-report/render-standard-controls} + + :com.fulcrologic.rad.report/row-style->row-layout + {:default sui-report/render-table-row + :list sui-report/render-list-row} + + :com.fulcrologic.rad.container/style->layout + {:default sui-container/render-container-layout} + + :com.fulcrologic.rad.control/type->style->control + {:boolean {:toggle boolean-input/render-control + :default boolean-input/render-control} + :string {:default text-input/render-control + :search text-input/render-control} + :instant {:default instant-input/date-time-control + :starting-date instant-input/midnight-on-date-control + :ending-date instant-input/midnight-next-date-control + :date-at-noon instant-input/date-at-noon-control} + :picker {:default picker-controls/render-control} + :button {:default action-button/render-control}}}) diff --git a/src/main/com/fulcrologic/rad/rendering/semantic_ui/text_field.cljc b/src/main/com/fulcrologic/rad/rendering/semantic_ui/text_field.cljc new file mode 100644 index 0000000..fe7958e --- /dev/null +++ b/src/main/com/fulcrologic/rad/rendering/semantic_ui/text_field.cljc @@ -0,0 +1,67 @@ +(ns com.fulcrologic.rad.rendering.semantic-ui.text-field + (:require + [com.fulcrologic.fulcro.components :as comp :refer [defsc]] + #?(:cljs [com.fulcrologic.fulcro.dom :refer [div label input]] + :clj [com.fulcrologic.fulcro.dom-server :refer [div label input]]) + [com.fulcrologic.fulcro.dom.events :as evt] + [com.fulcrologic.rad.form :as form] + [com.fulcrologic.rad.attributes :as attr] + [com.fulcrologic.rad.ui-validation :as validation] + [com.fulcrologic.rad.rendering.semantic-ui.components :refer [ui-wrapped-dropdown]] + [com.fulcrologic.rad.rendering.semantic-ui.field :refer [render-field-factory]])) + +(defn- with-handlers [type {:keys [value onChange onBlur] :as props}] + (assoc props + :value (or value "") + :type type + :onBlur (fn [evt] + (when onBlur + (onBlur (evt/target-value evt)))) + :onChange (fn [evt] + (when onChange + (onChange (evt/target-value evt)))))) + +(defn- text-input [props] (input (with-handlers "text" props))) +(defn- password-input [{:keys [value onChange onBlur] :as props}] (input (with-handlers "password" props))) + +(defsc ViewablePasswordField [this {:keys [value onChange onBlur] :as props}] + {:initLocalState (fn [_] {:hidden? true})} + (let [hidden? (comp/get-state this :hidden?)] + (input (assoc props + :value (if hidden? "*******" (or value "")) + :type "text" + :onBlur (fn [evt] + (comp/set-state! this {:hidden? true}) + (when onBlur + (onBlur (evt/target-value evt)))) + :onFocus (fn [_] (comp/set-state! this {:hidden? false})) + :onChange (fn [evt] + (when onChange + (onChange (evt/target-value evt)))))))) + +(def render-field (render-field-factory text-input)) +(def render-password (render-field-factory password-input)) +(def render-viewable-password (render-field-factory (comp/factory ViewablePasswordField))) + +(defn render-dropdown [{::form/keys [form-instance] :as env} attribute] + (let [{k ::attr/qualified-key + ::attr/keys [required?]} attribute + values (form/field-style-config env attribute :sorted-set/valid-values) + input-props (form/field-style-config env attribute :input/props) + options (mapv (fn [v] {:text v :value v}) values) + props (comp/props form-instance) + value (and attribute (get props k)) + invalid? (not (contains? values value)) + validation-message (when invalid? (validation/validation-error-message env attribute)) + field-label (form/field-label env attribute) + read-only? (form/read-only? form-instance attribute)] + (div :.ui.field {:key (str k)} + (label field-label (when invalid? (str " (" validation-message ")"))) + (ui-wrapped-dropdown + (merge + {:disabled read-only? + :options options + :clearable (not required?) + :value value + :onChange (fn [v] (form/input-changed! env k v))} + input-props)))))