Skip to content

Commit

Permalink
images: improved drag and drop
Browse files Browse the repository at this point in the history
  • Loading branch information
mhuebert committed Jan 3, 2024
1 parent e1b0e7c commit f9abb08
Show file tree
Hide file tree
Showing 4 changed files with 123 additions and 113 deletions.
54 changes: 29 additions & 25 deletions src/sb/app/field/admin_ui.cljc
Expand Up @@ -18,8 +18,8 @@
[yawn.hooks :as h]
[yawn.view :as v]))

(ui/defview show-option [{:as ?option :syms [?label ?value ?color]}]
(let [{:keys [drag-handle-props drag-subject-props drop-indicator]} (ui/orderable-props ?option {:axis :y})]
(ui/defview show-option [use-order {:as ?option :syms [?label ?value ?color]}]
(let [{:keys [drag-handle-props drag-subject-props drop-indicator]} (use-order ?option)]
[:div.flex.gap-2.items-center.group.relative.-ml-6.py-1
(merge {:key @?value}
drag-subject-props)
Expand Down Expand Up @@ -55,25 +55,26 @@
(t :tr/remove)]]}]]))

(ui/defview options-editor [?options]
[:div.col-span-2.flex-v.gap-3
[:label.field-label (t :tr/options)]
(when (:loading? ?options)
[:div.loading-bar.absolute.h-1.top-0.left-0.right-0])
(into [:div.flex-v]
(map show-option ?options))
(let [?new (h/use-memo #(io/field :init ""))]
[:form.flex.gap-2 {:on-submit (fn [^js e]
(.preventDefault e)
(io/add-many! ?options {'?value (str (random-uuid))
'?label @?new
'?color "#ffffff"})
(io/try-submit+ ?new
(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"}]
[:div.btn.bg-white.px-3.py-1.shadow "Add Option"]])
#_[ui/pprinted @?options]])
(let [use-order (ui/use-orderable-parent ?options {:axis :y})]
[:div.col-span-2.flex-v.gap-3
[:label.field-label (t :tr/options)]
(when (:loading? ?options)
[:div.loading-bar.absolute.h-1.top-0.left-0.right-0])
(into [:div.flex-v]
(map (partial show-option use-order) ?options))
(let [?new (h/use-memo #(io/field :init ""))]
[:form.flex.gap-2 {:on-submit (fn [^js e]
(.preventDefault e)
(io/add-many! ?options {'?value (str (random-uuid))
'?label @?new
'?color "#ffffff"})
(io/try-submit+ ?new
(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"}]
[:div.btn.bg-white.px-3.py-1.shadow "Add Option"]])
#_[ui/pprinted @?options]]))

(ui/defview field-row-detail [{:as ?field :syms [?label
?hint
Expand Down Expand Up @@ -114,12 +115,13 @@
(ui/defview field-row
{:key (fn [{:syms [?id]} _] @?id)}
[?field {:keys [expanded?
toggle-expand!]}]
toggle-expand!
use-order]}]
(let [{:syms [?type ?label]} ?field
{:keys [icon]} (data/field-types @?type)
{:keys [drag-handle-props
drag-subject-props
drop-indicator]} (ui/orderable-props ?field {:axis :y})]
drop-indicator]} (use-order ?field)]
[: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 @@ -162,7 +164,8 @@
(ui/defview fields-editor [{:as ?fields :keys [field/label]} props]
(let [!new-field (h/use-state nil)
!autofocus-ref (ui/use-autofocus-ref)
[expanded expand!] (h/use-state nil)]
[expanded expand!] (h/use-state nil)
use-order (ui/use-orderable-parent ?fields {:axis :y})]
[:div.field-wrapper {:class "labels-semibold"}
[:label.field-label {:class "flex items-center"}
label
Expand All @@ -184,7 +187,8 @@
(->> ?fields
(map (fn [{:as ?field :syms [?id]}]
(field-row ?field
{:expanded? (= expanded @?id)
{:use-order use-order
:expanded? (= expanded @?id)
:toggle-expand! #(expand! (fn [old]
(u/guard @?id (partial not= old))))})))
doall)]
Expand Down
42 changes: 20 additions & 22 deletions src/sb/app/field/ui.cljc
Expand Up @@ -337,52 +337,50 @@
;; put messages in a popover
(form.ui/show-field-messages ?image-list)]))


(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 [!?current ?images use-order 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})
dropping]} (use-order ?image)
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)}}]]
:trigger (v/x [:div.relative.transition-all
(v/merge-props {:class (when (= dropping :before) "pl-4")
:on-click #(reset! !?current ?image)}
drag-handle-props
drag-subject-props)
[:img.object-contain.h-16.w-16.rounded.overflow-hidden.bg-gray-50.transition-all
{:src url
:class [(when dragging "opacity-20 w-0")
(when current? "outline outline-2 outline-black")]}]])
: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))]
(let [!?current (h/use-state (first ?images))
use-order (ui/use-orderable-parent ?images {:axis :x})]
[:div.field-wrapper
(form.ui/show-label ?images label)
(when-let [{:syms [?id]} @!?current]
(let [[url loading?] (ui/use-last-loaded (asset.ui/asset-src @?id :avatar))]
[:div.relative {:key url}
(let [[url loading?] (ui/use-last-loaded (asset.ui/asset-src @?id :card))]
[:div.relative.flex.items-center.justify-center {:key url}
(when loading? [icons/loading "w-4 h-4 text-txt/60 absolute top-2 right-2"])
[:div.inset-0.bg-black.absolute.opacity-10]
[:img {:src url}]]))
;; thumbnails
[:div.flex.gap-2.flex-wrap
(when can-edit? [:div.relative.h-16.w-16.flex-none [add-image-button ?images]])
(->> ?images
(map (partial image-thumbnail
(merge props {:!?current !?current
:?images ?images}))))]]))
(merge props {:use-order use-order
:!?current !?current
:?images ?images}))))]]))

(ui/defview link-list-field [?links {:field/keys [label]}]
[:div.field-wrapper
Expand Down
2 changes: 1 addition & 1 deletion src/sb/app/views/radix.cljc
Expand Up @@ -214,7 +214,7 @@
[:el.accordion-content accordion/Content
(v/x content)]])))]))

(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
(def context-menu-item (v/from-element :el.text-sm.flex.items-center.outline-none.user-select-none.rounded.px-2.py-1.cursor-default ContextMenu/Item
{:class "data-[highlighted]:bg-gray-100"}))

(v/defview context-menu [{:keys [trigger
Expand Down
138 changes: 73 additions & 65 deletions src/sb/app/views/ui.cljs
Expand Up @@ -323,76 +323,84 @@
(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))
(defn use-orderable-parent
[?parent {:keys [axis] :or {axis :y}}]
(let [group (goog/getUid ?parent)
transfer-data (fn [e data]
(j/call-in e [:dataTransfer :setData] (str group)
(pr-str data)))
(j/call-in e [:dataTransfer :setData]
(str group)
(pr-str data))
(j/assoc-in! e [:dataTransfer :effectAllowed] "move")
)

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)]}])))}))
[drag-id set-drag!] (h/use-state nil)
[[drop-id drop-type] set-drop!] (h/use-state nil)]
(fn [?child]
(let [id (:sym ?child)
drop-type (when (= drop-id id) drop-type)
!should-drag? (h/use-ref false)
dragging? (= drag-id id)
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))]
{:drag-handle-props {:on-mouse-down #(reset! !should-drag? true)
:on-mouse-up #(reset! !should-drag? false)}
:drag-subject-props {:draggable true
:data-dragging dragging?
:data-dropping (some? drop-type)
:on-drag-over (j/fn [^js {:as e :keys [clientX
clientY
currentTarget]}]
(j/call e :preventDefault)
(.persist e)
(when (data-matches? e)
(set-drop!
(cond (= drag-id id) nil
(= ?child (last ?parent)) (if (case axis
:y (< clientY (element-center-y currentTarget))
:x (< clientX (element-center-x currentTarget)))
[id :before]
[id :after])
:else [id :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)]
(when-not (= source id)
(on-move {:destination id
:source source
:side drop-type}))))
:on-drag-end (fn [^js e]
(set-drag! nil))
:on-drag-start (fn [^js e]
(if @!should-drag?
(do
(set-drag! id)
(transfer-data e id))
(.preventDefault e)))}
:dragging dragging?
:dropping drop-type
:drop-indicator (when drop-type
(case axis
:y (v/x [:div.absolute.bg-focus-accent
{:class ["h-[4px] z-[99] inset-x-0 rounded"
(case drop-type
: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 drop-type
:before "left-[-2px]"
:after "right-[-2px]" nil)]}])))}))))

0 comments on commit f9abb08

Please sign in to comment.