Skip to content

Commit

Permalink
images: remove via context menu, re-order
Browse files Browse the repository at this point in the history
  • Loading branch information
mhuebert committed Jan 3, 2024
1 parent f7825f2 commit e1b0e7c
Show file tree
Hide file tree
Showing 5 changed files with 146 additions and 116 deletions.
14 changes: 4 additions & 10 deletions src/sb/app/entity/data.cljc
Expand Up @@ -119,18 +119,12 @@
[ctx e a v]
(save-attributes! ctx e {a v}))

(defn save-field [?field]
(when-let [{:as ?persisted-field :keys [db/id attribute]} (io/ancestor-by ?field :field/persisted?)]
(io/try-submit+ ?persisted-field
(save-attribute! nil id attribute @?persisted-field))))

(defn maybe-save-field
[?field]
(let [value @?field]
(when (and (io/closest ?field :field/persisted?)
(not= value (persisted-value ?field)))
(io/try-submit+ ?field
(save-field ?field)))))
(when-let [{:as ?persisted-field :keys [db/id attribute]} (io/ancestor-by ?field :field/persisted?)]
(when (not= @?field (persisted-value ?field))
(io/try-submit+ ?persisted-field
(save-attribute! nil id attribute @?persisted-field)))))

(defn reverse-attr [a]
(keyword (namespace a) (str "_" (name a))))
Expand Down
91 changes: 6 additions & 85 deletions src/sb/app/field/admin_ui.cljc
Expand Up @@ -18,87 +18,8 @@
[yawn.hooks :as h]
[yawn.view :as v]))

(defn element-center-y [el]
#?(:cljs
(j/let [^js {:keys [y height]} (j/call el :getBoundingClientRect)]
(+ y (/ height 2)))))

(defn re-order [xs source side destination]
{:post [(= (count %) (count xs))]}
(let [out (reduce (fn [out x]
(if (= x destination)
(into out (case side :before [source destination]
:after [destination source]))
(conj out x)))
[]
(remove #{source} xs))]
(when-not (= (count out) (count xs))
(throw (ex-info "re-order failed, destination not found" {:source source :destination destination})))
out))

(defn orderable-props
[?child]
#?(:cljs
(let [?parent (io/parent ?child)
group (goog/getUid ?parent)
id (:sym ?child)
on-move (fn [{:keys [source side destination]}]
(io/swap-many! ?parent re-order
(get ?parent source)
side
(get ?parent destination))
(entity.data/save-field ?child))
transfer-data (fn [e data]
(j/call-in e [:dataTransfer :setData] (str group)
(pr-str data)))

receive-data (fn [e]
(try
(ui/read-string (j/call-in e [:dataTransfer :getData] (str group)))
(catch js/Error e nil)))
data-matches? (fn [e]
(some #{(str group)} (j/get-in e [:dataTransfer :types])))
[active-drag set-drag!] (h/use-state nil)
[active-drop set-drop!] (h/use-state nil)
!should-drag? (h/use-ref false)]
{:drag-handle-props {:on-mouse-down #(reset! !should-drag? true)
:on-mouse-up #(reset! !should-drag? false)}
:drag-subject-props {:draggable true
:data-dragging active-drag
:data-dropping active-drop
:on-drag-over (j/fn [^js {:as e :keys [clientY currentTarget]}]
(j/call e :preventDefault)
(when (data-matches? e)
(set-drop! (if (< clientY (element-center-y currentTarget))
:before
:after))))
:on-drag-leave (fn [^js e]
(j/call e :preventDefault)
(set-drop! nil))
:on-drop (fn [^js e]
(.preventDefault e)
(set-drop! nil)
(when-let [source (receive-data e)]
(on-move {:destination id
:source source
:side active-drop})))
:on-drag-end (fn [^js e]
(set-drag! nil))
:on-drag-start (fn [^js e]
(if @!should-drag?
(do
(set-drag! true)
(transfer-data e id))
(.preventDefault e)))}
:drop-indicator (when active-drop
(v/x [:div.absolute.bg-focus-accent
{:class ["h-[4px] z-[99] inset-x-0 rounded"
(case active-drop
:before "top-[-2px]"
:after "bottom-[-2px]" nil)]}]))})))

(ui/defview show-option [{:as ?option :syms [?label ?value ?color]}]
(let [{:keys [drag-handle-props drag-subject-props drop-indicator]} (orderable-props ?option)]
(let [{:keys [drag-handle-props drag-subject-props drop-indicator]} (ui/orderable-props ?option {:axis :y})]
[:div.flex.gap-2.items-center.group.relative.-ml-6.py-1
(merge {:key @?value}
drag-subject-props)
Expand Down Expand Up @@ -129,7 +50,7 @@
:confirm-text (t :tr/remove)
:confirm-fn (fn []
(io/remove-many! ?option)
(p/do (entity.data/save-field ?option)
(p/do (entity.data/maybe-save-field ?option)
(radix/close-alert!)))}))}
(t :tr/remove)]]}]]))

Expand All @@ -147,7 +68,7 @@
'?label @?new
'?color "#ffffff"})
(io/try-submit+ ?new
(p/let [result (entity.data/save-field ?options)]
(p/let [result (entity.data/maybe-save-field ?options)]
(reset! ?new (:init ?new))
result)))}
[field.ui/text-field ?new {:placeholder "Option label" :field/wrapper-class "flex-auto"}]
Expand Down Expand Up @@ -186,7 +107,7 @@
:confirm-text (t :tr/remove)
:confirm-fn (fn []
(io/remove-many! ?field)
(entity.data/save-field ?field))})}
(entity.data/maybe-save-field ?field))})}
[:div.w-5.h-5.rounded.flex.items-center.justify-center.text-destructive [icons/trash "w-4 h-4"]]
(t :tr/remove)]]]]))

Expand All @@ -198,7 +119,7 @@
{:keys [icon]} (data/field-types @?type)
{:keys [drag-handle-props
drag-subject-props
drop-indicator]} (orderable-props ?field)]
drop-indicator]} (ui/orderable-props ?field {:axis :y})]
[:div.flex-v.relative.border-b
;; label row
[:div.flex.gap-3.p-3.items-stretch.relative.cursor-default.relative.group
Expand Down Expand Up @@ -278,7 +199,7 @@
(prn [:client2 (last @?fields)])
(expand! (:field/id @?new-field))
(reset! !new-field nil)
(entity.data/save-field ?fields)))}
(entity.data/maybe-save-field ?fields)))}
[:div.h-10.flex.items-center [(:icon (data/field-types @?type)) "icon-lg text-gray-700 mx-2"]]
[field.ui/text-field ?label {:field/label false
:ref !autofocus-ref
Expand Down
52 changes: 36 additions & 16 deletions src/sb/app/field/ui.cljc
Expand Up @@ -337,11 +337,40 @@
;; put messages in a popover
(form.ui/show-field-messages ?image-list)]))

(ui/defview images-field [?images {:field/keys [label can-edit?]}]
(let [?current (h/use-state (first ?images))]
(ui/defview image-thumbnail
{:key (fn [_ ?image] (:entity/id @?image))}
[{:keys [!?current ?images field/can-edit?]} {:as ?image :syms [?id]}]
(let [url (asset.ui/asset-src @?id :card)
{:keys [drag-handle-props
drag-subject-props
drop-indicator
dragging
dropping]} (ui/orderable-props ?image {:axis :x})
current? (= @!?current ?image)]
[radix/context-menu
{:key url
:trigger [:div.relative.w-16.h-16.rounded.overflow-hidden.bg-gray-50.transition-all
(v/merge-props {:class [(when current? "outline outline-2 outline-black")
(when dragging "opacity-20")
(case dropping
:before "ml-1"
:after "-ml-1"
nil)]
:on-click #(reset! !?current ?image)}
drag-handle-props
drag-subject-props)
[:div.absolute.inset-0.bg-black.opacity-10.z-1]
[:div.absolute.inset-0.z-2.bg-contain {:style {:background-image (asset.ui/css-url url)}}]]
:items [[radix/context-menu-item {:on-select (fn []
(io/remove-many! ?image)
(entity.data/maybe-save-field ?images))}
"Delete"]]}]))

(ui/defview images-field [?images {:as props :field/keys [label can-edit?]}]
(let [!?current (h/use-state (first ?images))]
[:div.field-wrapper
(form.ui/show-label ?images label)
(when-let [{:syms [?id]} @?current]
(when-let [{:syms [?id]} @!?current]
(let [[url loading?] (ui/use-last-loaded (asset.ui/asset-src @?id :avatar))]
[:div.relative {:key url}
(when loading? [icons/loading "w-4 h-4 text-txt/60 absolute top-2 right-2"])
Expand All @@ -350,19 +379,10 @@
;; thumbnails
[:div.flex.gap-2.flex-wrap
(when can-edit? [:div.relative.h-16.w-16.flex-none [add-image-button ?images]])
(for [{:as ?image :syms [?id]} ?images
:let [url (asset.ui/asset-src @?id :card)
current? (= ?image @?current)]]
[radix/context-menu [:div.relative.w-16.h-16.rounded.overflow-hidden.bg-gray-50
{:class (when current? "outline outline-2 outline-black")
:on-click #(reset! ?current ?image)
:key url}
[:div.absolute.inset-0.bg-black.opacity-10.z-1]
[:div.absolute.inset-0.z-2.bg-contain {:style {:background-image (asset.ui/css-url url)}}]]
{:items [[radix/context-menu-item {:on-select (fn []
(io/remove-many! ?image)
(entity.data/maybe-save-field ?images))}
"Delete"]]}])]]))
(->> ?images
(map (partial image-thumbnail
(merge props {:!?current !?current
:?images ?images}))))]]))

(ui/defview link-list-field [?links {:field/keys [label]}]
[:div.field-wrapper
Expand Down
3 changes: 2 additions & 1 deletion src/sb/app/views/radix.cljc
Expand Up @@ -217,7 +217,8 @@
(def context-menu-item (v/from-element :el.text-sm.flex.items-center.outline-none.user-select-none.rounded.px-2.py-1 ContextMenu/Item
{:class "data-[highlighted]:bg-gray-100"}))

(v/defview context-menu [trigger {:keys [id items] :or {id (str ::context-menu)}}]
(v/defview context-menu [{:keys [trigger
items]}]
[:el ContextMenu/Root
[:el ContextMenu/Trigger (v/x trigger)]
[:el.bg-white.rounded.overflow-hidden.p-1.shadow-md.min-w-32 ContextMenu/Content {:style {:z-index 20}}
Expand Down
102 changes: 98 additions & 4 deletions src/sb/app/views/ui.cljs
Expand Up @@ -9,20 +9,20 @@
[cljs.reader :as edn]
[clojure.pprint]
[clojure.string :as str]
[inside-out.forms :as io]
[inside-out.macros]
[promesa.core :as p]
[re-db.api :as db]
[re-db.react]
[sb.app.asset.ui :as asset.ui]
[sb.app.entity.data :as entity.data]
[sb.client.sanitize :as sanitize]
[sb.i18n]
[sb.icons :as icons]
[sb.routing :as routing]
[shadow.cljs.modern :refer [defclass]]
[taoensso.tempura :as tempura]
[yawn.hooks :as h]
[yawn.view :as v]
[sb.i18n :as i18n]))
[yawn.view :as v]))

(defn dev? [] (= "dev" (db/get :env/config :env)))

Expand Down Expand Up @@ -301,4 +301,98 @@
(.preventDefault e)
(f e)))

(defn pprint [x] (clojure.pprint/pprint x))
(defn pprint [x] (clojure.pprint/pprint x))

(defn element-center-y [el]
(j/let [^js {:keys [y height]} (j/call el :getBoundingClientRect)]
(+ y (/ height 2))))
(defn element-center-x [el]
(j/let [^js {:keys [x width]} (j/call el :getBoundingClientRect)]
(+ x (/ width 2))))

(defn re-order [xs source side destination]
{:post [(= (count %) (count xs))]}
(let [out (reduce (fn [out x]
(if (= x destination)
(into out (case side :before [source destination]
:after [destination source]))
(conj out x)))
[]
(remove #{source} xs))]
(when-not (= (count out) (count xs))
(throw (ex-info "re-order failed, destination not found" {:source source :destination destination})))
out))

(defn orderable-props
[?child {:keys [axis] :or {axis :y}}]
(let [?parent (io/parent ?child)
group (goog/getUid ?parent)
id (:sym ?child)
on-move (fn [{:keys [source side destination]}]
(io/swap-many-children! ?parent re-order
(get ?parent source)
side
(get ?parent destination))
(entity.data/maybe-save-field ?child))
transfer-data (fn [e data]
(j/call-in e [:dataTransfer :setData] (str group)
(pr-str data)))

receive-data (fn [e]
(try
(read-string (j/call-in e [:dataTransfer :getData] (str group)))
(catch js/Error e nil)))
data-matches? (fn [e]
(some #{(str group)} (j/get-in e [:dataTransfer :types])))
[active-drag set-drag!] (h/use-state nil)
[active-drop set-drop!] (h/use-state nil)
!should-drag? (h/use-ref false)]
{:drag-handle-props {:on-mouse-down #(reset! !should-drag? true)
:on-mouse-up #(reset! !should-drag? false)}
:drag-subject-props {:draggable true
:data-dragging active-drag
:data-dropping active-drop
:on-drag-over (j/fn [^js {:as e :keys [clientX
clientY
currentTarget]}]
(j/call e :preventDefault)
(when (data-matches? e)
(set-drop! (if (= ?child (last ?parent))
(if (case axis
:y (< clientY (element-center-y currentTarget))
:x (< clientX (element-center-x currentTarget)))
:before
:after)
:before))))
:on-drag-leave (fn [^js e]
(j/call e :preventDefault)
(set-drop! nil))
:on-drop (fn [^js e]
(.preventDefault e)
(set-drop! nil)
(when-let [source (receive-data e)]
(on-move {:destination id
:source source
:side active-drop})))
:on-drag-end (fn [^js e]
(set-drag! nil))
:on-drag-start (fn [^js e]
(if @!should-drag?
(do
(set-drag! true)
(transfer-data e id))
(.preventDefault e)))}
:dragging active-drag
:dropping active-drop
:drop-indicator (when active-drop
(case axis
:y (v/x [:div.absolute.bg-focus-accent
{:class ["h-[4px] z-[99] inset-x-0 rounded"
(case active-drop
:before "top-[-2px]"
:after "bottom-[-2px]" nil)]}])
:x (v/x [:div.absolute.bg-focus-accent
{:class ["w-[4px] z-[99] inset-y-0 rounded"
(case active-drop
:before "left-[-2px]"
:after "right-[-2px]" nil)]}])))}))

0 comments on commit e1b0e7c

Please sign in to comment.