Make the multi-input more generic

This commit is contained in:
Andrey Antukh 2022-03-03 14:18:56 +01:00
parent a1c3789ec2
commit cfe657d853
6 changed files with 173 additions and 148 deletions

View file

@ -19,24 +19,7 @@
.custom-input { .custom-input {
width: 314px; width: 314px;
height: 14px;
font-size: 14px;
margin-right: 10px; margin-right: 10px;
input {
padding: 0;
&.empty {
margin-top: 10px;
&::placeholder {
color: $color-gray-20;
opacity: 1;
}
}
&::placeholder {
color: transparent;
}
}
} }
.custom-select { .custom-select {
@ -65,47 +48,6 @@
} }
} }
.invite-member-email-container {
border: 1px solid $color-black;
width: 273px;
margin-right: 10px;
max-height: 300px;
overflow-y: auto;
padding: 0 5px 5px 5px;
}
.invite-member-email-text {
margin-bottom: 5px;
.around {
border: 1px solid $color-gray-20;
padding-left: 5px;
border-radius: 4px;
&.invalid {
border: 1px solid $color-danger;
}
.text {
display: inline-block;
max-width: 85%;
overflow: hidden;
text-overflow: ellipsis;
line-height: 15px;
font-size: 14px;
color: $color-black;
}
.icon {
cursor: pointer;
margin-left: 10px;
margin-right: 5px;
}
}
}
.invite-member-email-input {
width: 95%;
border: 0;
}
svg { svg {
width: 12px; width: 12px;
height: 12px; height: 12px;

View file

@ -227,6 +227,64 @@ textarea {
} }
} }
.custom-multi-input {
border-radius: 2px;
border: 1px solid $color-gray-20;
&.invalid {
label {
color: unset;
}
}
input {
border: 0px;
&.no-padding {
padding-top: 0px;
}
}
.selected-items {
padding-top: 25px;
padding-left: 15px;
display: flex;
flex-wrap: wrap;
}
.selected-item {
// margin-bottom: 5px;
&:not(:last-child) {
margin-right: 3px;
}
.around {
border: 1px solid $color-gray-20;
padding-left: 5px;
border-radius: 4px;
&.invalid {
border: 1px solid $color-danger;
}
.text {
display: inline-block;
max-width: 85%;
overflow: hidden;
text-overflow: ellipsis;
line-height: 15px;
font-size: 14px;
color: $color-black;
}
.icon {
cursor: pointer;
margin-left: 10px;
margin-right: 5px;
}
}
}
}
.custom-select { .custom-select {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View file

@ -7,12 +7,14 @@
(ns app.main.ui.components.forms (ns app.main.ui.components.forms
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.main.ui.hooks :as hooks]
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.forms :as fm] [app.util.forms :as fm]
[app.util.i18n :as i18n :refer [tr]] [app.util.i18n :as i18n :refer [tr]]
[app.util.keyboard :as kbd] [app.util.keyboard :as kbd]
[app.util.object :as obj] [app.util.object :as obj]
[cljs.core :as c]
[clojure.string] [clojure.string]
[cuerdas.core :as str] [cuerdas.core :as str]
[rumext.alpha :as mf])) [rumext.alpha :as mf]))
@ -225,92 +227,112 @@
(on-submit form event))} (on-submit form event))}
children]])) children]]))
(defn- conj-dedup
"A helper that adds item into a vector and removes possible
(mf/defc multi-input-row duplicates. This is not very efficient implementation but is ok for
[{:keys [item, remove-item!, class, invalid-class]}] handling form input that will have a small number of items."
(let [valid (val item) [coll item]
text (key item)] (into [] (distinct) (conj coll item)))
[:div {:class class}
[:span.around {:class (when-not valid invalid-class)}
[:span.text text]
[:span.icon {:on-click #(remove-item! (key item))} i/cross]]]))
(mf/defc multi-input (mf/defc multi-input
[{:keys [form hint class container-class row-class row-invalid-class] :as props}] [{:keys [form label class name trim valid-item-fn] :as props}]
(let [multi-input-name (get props :name) (let [form (or form (mf/use-ctx form-ctx))
single-input-name (keyword (str "single-" (name multi-input-name))) input-name (get props :name)
single-input-element (dom/get-element (name single-input-name)) touched? (get-in @form [:touched input-name])
hint-element (dom/get-element-by-class "hint") error (get-in @form [:errors input-name])
form (or form (mf/use-ctx form-ctx)) focus? (mf/use-state false)
value (get-in @form [:data multi-input-name] "")
single-mail-value (get-in @form [:data single-input-name] "")
items (mf/use-state {})
comma-items items (mf/use-state [])
(fn [items] value (mf/use-state "")
(if (= "" single-mail-value) result (hooks/use-equal-memo @items)
(str/join "," (keys items))
(str/join "," (conj (keys items) single-mail-value))))
update-multi-input empty? (and (str/empty? @value)
(fn [all] (zero? (count @items)))
(fm/on-input-change form multi-input-name all true)
(if (= "" all) klass (str (get props :class) " "
(do (dom/classnames
(dom/add-class! single-input-element "empty") :focus @focus?
(dom/add-class! hint-element "hidden")) :valid (and touched? (not error))
(do :invalid (and touched? error)
(dom/remove-class! single-input-element "empty") :empty empty?
(dom/remove-class! hint-element "hidden"))) :custom-multi-input true
:custom-input true))
(dom/focus! single-input-element)) in-klass (str class " "
(dom/classnames
:no-padding (pos? (count @items))))
on-focus
(mf/use-fn #(reset! focus? true))
on-change
(mf/use-fn
(fn [event]
(let [content (-> event dom/get-target dom/get-input-value)]
(reset! value content))))
update-form!
(mf/use-fn
(mf/deps form)
(fn [items]
(let [value (str/join " " (map :text items))]
(fm/update-input-value! form input-name value))))
on-key-up
(mf/use-fn
(mf/deps @value)
(fn [event]
(cond
(or (kbd/enter? event)
(kbd/comma? event))
(do
(dom/prevent-default event)
(dom/stop-propagation event)
(let [val (cond-> @value trim str/trim)]
(reset! value "")
(swap! items conj-dedup {:text val :valid (valid-item-fn val)})))
(and (kbd/backspace? event)
(str/empty? @value))
(do
(dom/prevent-default event)
(dom/stop-propagation event)
(swap! items (fn [items] (if (c/empty? items) items (pop items))))))))
on-blur
(mf/use-fn
(fn [_]
(reset! focus? false)
(when-not (get-in @form [:touched input-name])
(swap! form assoc-in [:touched input-name] true))))
remove-item! remove-item!
(fn [item] (mf/use-fn
(swap! items (fn [item]
(fn [items] (swap! items #(into #{} (remove (fn [x] (= x item))) %))))]
(let [temp-items (dissoc items item)
all (comma-items temp-items)]
(update-multi-input all)
temp-items))))
add-item! (mf/with-effect [result]
(fn [item valid] (if (every? :valid result)
(swap! items assoc item valid)) (update-form! result)
(update-form! [])))
input-key-down (fn [event] [:div {:class klass}
(let [target (dom/event->target event) (when-let [items (seq @items)]
value (dom/get-value target) [:div.selected-items
valid (and (not (= value "")) (dom/valid? target))] (for [item items]
[:div.selected-item {:key (:text item)}
[:span.around {:class (when-not (:valid item) "invalid")}
[:span.text (:text item)]
[:span.icon {:on-click #(remove-item! item)} i/cross]]])])
(when (kbd/comma? event) [:input {:id (name input-name)
(dom/prevent-default event) :class in-klass
(add-item! value valid) :type "text"
(fm/on-input-change form single-input-name "")))) :auto-focus true
:on-focus on-focus
input-key-up #(update-multi-input (comma-items @items)) :on-blur on-blur
:on-key-up on-key-up
single-props (-> props :value @value
(dissoc :hint :row-class :row-invalid-class :container-class :class) :on-change on-change
(assoc :placeholder (when empty? label)}]
:label hint [:label {:for (name input-name)} label]]))
:name single-input-name
:on-key-down input-key-down
:on-key-up input-key-up
:class (str/join " " [class "empty"])))]
[:div {:class container-class}
(when (string? hint)
[:span.hint.hidden hint])
(for [item @items]
[:& multi-input-row {:item item
:remove-item! remove-item!
:class row-class
:invalid-class row-invalid-class}])
[:& input single-props]
[:input {:id (name multi-input-name)
:read-only true
:type "hidden"
:value value}]]))

View file

@ -101,7 +101,7 @@
(= :profile-is-muted code)) (= :profile-is-muted code))
(st/emit! (dm/error (tr "errors.profile-is-muted")) (st/emit! (dm/error (tr "errors.profile-is-muted"))
(modal/hide)) (modal/hide))
(and (= :validation type) (and (= :validation type)
(or (= :member-is-muted code) (or (= :member-is-muted code)
(= :email-has-permanent-bounces code))) (= :email-has-permanent-bounces code)))
@ -123,25 +123,20 @@
[:& fm/form {:on-submit on-submit :form form} [:& fm/form {:on-submit on-submit :form form}
[:div.title [:div.title
[:span.text (tr "modals.invite-member.title")]] [:span.text (tr "modals.invite-member.title")]]
(when-not (= "" @error-text) (when-not (= "" @error-text)
[:div.error [:div.error
[:span.icon i/msg-error] [:span.icon i/msg-error]
[:span.text @error-text]] [:span.text @error-text]])
)
[:div.form-row [:div.form-row
[:& fm/multi-input {:type "email" [:& fm/multi-input {:type "email"
:name :emails :name :emails
:auto-focus? true :auto-focus? true
:hint (tr "modals.invite-member.emails") :trim true
:class "invite-member-email-input" :valid-item-fn us/parse-email
:container-class "invite-member-email-container" :label (tr "modals.invite-member.emails")}]
:row-class "invite-member-email-text" [:& fm/select {:name :role :options roles}]]
:row-invalid-class "invalid"}]
[:& fm/select {:name :role
:options roles}]]
[:div.action-buttons [:div.action-buttons
[:& fm/submit-button {:label (tr "modals.invite-member-confirm.accept")}]]]])) [:& fm/submit-button {:label (tr "modals.invite-member-confirm.accept")}]]]]))

View file

@ -121,6 +121,13 @@
(assoc-in [:data field] (if trim? (str/trim value) value)) (assoc-in [:data field] (if trim? (str/trim value) value))
(update :errors dissoc field)))))) (update :errors dissoc field))))))
(defn update-input-value!
[form field value]
(swap! form (fn [state]
(-> state
(assoc-in [:data field] value)
(update :errors dissoc field)))))
(defn on-input-blur (defn on-input-blur
[form field] [form field]
(fn [_] (fn [_]

View file

@ -36,6 +36,7 @@
(def ctrlKey? (or (is-key? "Control") (def ctrlKey? (or (is-key? "Control")
(is-key? "Meta"))) (is-key? "Meta")))
(def comma? (is-key? ",")) (def comma? (is-key? ","))
(def backspace? (is-key? "Backspace"))
(defn editing? [e] (defn editing? [e]
(.-editing ^js e)) (.-editing ^js e))