mirror of
https://github.com/penpot/penpot.git
synced 2025-05-25 21:56:13 +02:00
✨ Make the multi-input more generic
This commit is contained in:
parent
a1c3789ec2
commit
cfe657d853
6 changed files with 173 additions and 148 deletions
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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 [])
|
||||||
|
value (mf/use-state "")
|
||||||
|
result (hooks/use-equal-memo @items)
|
||||||
|
|
||||||
|
empty? (and (str/empty? @value)
|
||||||
|
(zero? (count @items)))
|
||||||
|
|
||||||
|
klass (str (get props :class) " "
|
||||||
|
(dom/classnames
|
||||||
|
:focus @focus?
|
||||||
|
:valid (and touched? (not error))
|
||||||
|
:invalid (and touched? error)
|
||||||
|
:empty empty?
|
||||||
|
:custom-multi-input true
|
||||||
|
:custom-input true))
|
||||||
|
|
||||||
|
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]
|
(fn [items]
|
||||||
(if (= "" single-mail-value)
|
(let [value (str/join " " (map :text items))]
|
||||||
(str/join "," (keys items))
|
(fm/update-input-value! form input-name value))))
|
||||||
(str/join "," (conj (keys items) single-mail-value))))
|
|
||||||
|
|
||||||
update-multi-input
|
on-key-up
|
||||||
(fn [all]
|
(mf/use-fn
|
||||||
(fm/on-input-change form multi-input-name all true)
|
(mf/deps @value)
|
||||||
|
(fn [event]
|
||||||
(if (= "" all)
|
(cond
|
||||||
|
(or (kbd/enter? event)
|
||||||
|
(kbd/comma? event))
|
||||||
(do
|
(do
|
||||||
(dom/add-class! single-input-element "empty")
|
(dom/prevent-default event)
|
||||||
(dom/add-class! hint-element "hidden"))
|
(dom/stop-propagation event)
|
||||||
(do
|
(let [val (cond-> @value trim str/trim)]
|
||||||
(dom/remove-class! single-input-element "empty")
|
(reset! value "")
|
||||||
(dom/remove-class! hint-element "hidden")))
|
(swap! items conj-dedup {:text val :valid (valid-item-fn val)})))
|
||||||
|
|
||||||
(dom/focus! single-input-element))
|
(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!
|
||||||
|
(mf/use-fn
|
||||||
(fn [item]
|
(fn [item]
|
||||||
(swap! items
|
(swap! items #(into #{} (remove (fn [x] (= x item))) %))))]
|
||||||
(fn [items]
|
|
||||||
(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}]]))
|
|
||||||
|
|
|
@ -127,21 +127,16 @@
|
||||||
(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")}]]]]))
|
||||||
|
|
|
@ -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 [_]
|
||||||
|
|
|
@ -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))
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue