diff --git a/src/sb/app/field/admin_ui.cljc b/src/sb/app/field/admin_ui.cljc index 0c8952ad..2404ac6e 100644 --- a/src/sb/app/field/admin_ui.cljc +++ b/src/sb/app/field/admin_ui.cljc @@ -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) @@ -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 @@ -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 @@ -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 @@ -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)] diff --git a/src/sb/app/field/ui.cljc b/src/sb/app/field/ui.cljc index 5cd8414b..73a3289b 100644 --- a/src/sb/app/field/ui.cljc +++ b/src/sb/app/field/ui.cljc @@ -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 diff --git a/src/sb/app/views/radix.cljc b/src/sb/app/views/radix.cljc index ba2a80c9..8b4ba5b0 100644 --- a/src/sb/app/views/radix.cljc +++ b/src/sb/app/views/radix.cljc @@ -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 diff --git a/src/sb/app/views/ui.cljs b/src/sb/app/views/ui.cljs index bc62eae4..ab3f3753 100644 --- a/src/sb/app/views/ui.cljs +++ b/src/sb/app/views/ui.cljs @@ -323,20 +323,15 @@ (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 @@ -344,55 +339,68 @@ (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)]}])))})) \ No newline at end of file + [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)]}])))})))) \ No newline at end of file