diff --git a/src/cljs/airsonic_ui/components/current_queue/views.cljs b/src/cljs/airsonic_ui/components/current_queue/views.cljs index 4f0e5c0..383933e 100644 --- a/src/cljs/airsonic_ui/components/current_queue/views.cljs +++ b/src/cljs/airsonic_ui/components/current_queue/views.cljs @@ -1,16 +1,53 @@ (ns airsonic-ui.components.current-queue.views (:require [re-frame.core :refer [subscribe]] + [reagent.core :as reagent] + ["react-sortable-hoc" :refer [SortableHandle]] + [airsonic-ui.helpers :as helpers] [airsonic-ui.views.song :as song] [airsonic-ui.components.sortable.views :as sortable] [airsonic-ui.routes :as r])) +(defonce items (reagent/atom [1 2 3 4 5 6 7])) + +(def SortHandle + (SortableHandle. + ;; Alternative to r/reactify-component, which doens't convert props and hiccup, + ;; is to just provide fn as component and use as-element or create-element + ;; to return React elements from the component. + (fn [] + (reagent/as-element [:span {:style {:WebkitTouchCallout "none" + :WebkitUserSelect "none" + :KhtmlUserSelect "none" + :MozUserSelect "none" + ;; NOTE: lowercase "ms" prefix + ;; https://www.andismith.com/blogs/2012/02/modernizr-prefixed/ + :msUserSelect "none" + :userSelect "none"}} + "::"])))) + (defn current-queue [] - [:section.section>div.container - [:h1.title "Current Queue"] - [sortable/sortable-component] - (let [current-queue @(subscribe [:audio/current-queue]) - #_#_ current-song @(subscribe [:audio/current-song])] - (if (some? current-queue) - [song/listing (:items current-queue)] - [:p "You are currently not playing anything. Use the search or go to your " - [:a {:href (r/url-for ::r/library)} "Library"] " to start playing some music."]))]) + (let [is-sortable? (reagent/atom true)] + (fn [] + [:section.section>div.container + [:h1.title "Current Queue"] + [sortable/sortable-component {:container [:table.table.is-fullwidth>tbody] + :items @items + + :render-item + (fn [{:keys [value]}] + [:tr + [:td "Some table cell"] + [:td (str "Value: " value)] + [:td [:a {:on-click #(swap! is-sortable? not)} (str "Is sortable: " @is-sortable?)]] + [:td (when @is-sortable? + [:> SortHandle])]]) + + :on-sort-end + (fn [{:keys [old-idx new-idx]}] + (swap! items helpers/vector-move old-idx new-idx))}] + (let [current-queue @(subscribe [:audio/current-queue]) + #_#_ current-song @(subscribe [:audio/current-song])] + (if (some? current-queue) + [song/listing (:items current-queue)] + [:p "You are currently not playing anything. Use the search or go to your " + [:a {:href (r/url-for ::r/library)} "Library"] " to start playing some music."]))]))) diff --git a/src/cljs/airsonic_ui/components/sortable/views.cljs b/src/cljs/airsonic_ui/components/sortable/views.cljs index d79b711..17fd468 100644 --- a/src/cljs/airsonic_ui/components/sortable/views.cljs +++ b/src/cljs/airsonic_ui/components/sortable/views.cljs @@ -3,58 +3,21 @@ [clojure.string :as str] ["react-sortable-hoc" :refer [SortableHandle SortableElement SortableContainer]])) +;; this code is taken and adapted from https://github.com/reagent-project/reagent/blob/72c95257c13e5de1531e16d1a06da7686041d3f4/examples/react-sortable-hoc/src/example/core.cljs -(defonce state - (r/atom {:items (vec (map #(str "Item " %) (range 1 11))) - :sort-enabled? true})) - -;; this code is taken from https://github.com/reagent-project/reagent/blob/72c95257c13e5de1531e16d1a06da7686041d3f4/examples/react-sortable-hoc/src/example/core.cljs -(def DragHandle - (SortableHandle. - ;; Alternative to r/reactify-component, which doens't convert props and hiccup, - ;; is to just provide fn as component and use as-element or create-element - ;; to return React elements from the component. - (fn [] - (r/as-element [:span {:style {:WebkitTouchCallout "none" - :WebkitUserSelect "none" - :KhtmlUserSelect "none" - :MozUserSelect "none" - ;; NOTE: lowercase "ms" prefix - ;; https://www.andismith.com/blogs/2012/02/modernizr-prefixed/ - :msUserSelect "none" - :userSelect "none"}} - "::"])))) - -(def SortableRow - (SortableElement. - (r/reactify-component (fn [{:keys [value]}] - [:tr - [:td value] - (when (:sort-enabled? @state) - [:td [:> DragHandle]])])))) - -(def SortableTable - (SortableContainer. - (r/reactify-component - (fn [{:keys [items]}] - [:table.table.is-fullwidth>tbody - (for [[idx value] (map-indexed vector items)] - ;; No :> or adapt-react-class here because that would convert value to JS - (r/create-element - SortableRow - #js {:key (str "item-" idx) - :index idx - :value value}))])))) - -(defn vector-move [coll prev-index new-index] - (let [items (into (subvec coll 0 prev-index) - (subvec coll (inc prev-index)))] - (-> (subvec items 0 new-index) - (conj (get coll prev-index)) - (into (subvec items new-index))))) - -(comment - (= [0 2 3 4 1 5] (vector-move [0 1 2 3 4 5] 1 4))) +(defn make-wrapper [{:keys [container render-item]}] + (let [SortableItem (SortableElement. + (r/reactify-component render-item))] + (SortableContainer. + (r/reactify-component + (fn [{:keys [items]}] + (into container + (for [[idx value] (map-indexed vector items)] + (r/create-element + SortableItem + #js {:key (str "item-" idx) + :index idx + :value value})))))))) (defn style-map "Returns a map representing all currently set css styles; this makes sense @@ -97,24 +60,27 @@ (defonce saved-snapshot (atom nil)) -(defn sortable-component [] - (r/create-element - SortableTable - #js {:items (:items @state) - :helperClass "sortable-is-moving" +(defn sortable-component [{:keys [container items render-item on-sort-end]}] + (let [Wrapper (make-wrapper {:container container + :render-item render-item})] + (r/create-element + Wrapper + #js {:items items + :helperClass "sortable-is-moving" - ;; save the style of all of the rows children - :updateBeforeSortStart - (fn [event] - (reset! saved-snapshot (style-snapshot (.-node event)))) + ;; save the style of all of the rows children + :updateBeforeSortStart + (fn [event] + (reset! saved-snapshot (style-snapshot (.-node event)))) + :onSortStart + (fn [_] + ;; the node we get passed as parameter is the original node unfortunately + (restore-snapshot @saved-snapshot (js/document.querySelector "body > tr:last-of-type"))) - :onSortStart - (fn [_] - ;; the node we get passed as parameter is the original node unfortunately - (restore-snapshot @saved-snapshot (js/document.querySelector "body > tr:last-of-type"))) + ;; update the state to reflect the new order + :onSortEnd + (fn [event] + (on-sort-end {:old-idx (.-oldIndex event) + :new-idx (.-newIndex event)})) - ;; update the state to reflect the new order - :onSortEnd - (fn [event] - (swap! state update :items vector-move (.-oldIndex event) (.-newIndex event))) - :useDragHandle true})) + :useDragHandle true}))) diff --git a/src/cljs/airsonic_ui/helpers.cljs b/src/cljs/airsonic_ui/helpers.cljs index 6ed4015..5a64523 100644 --- a/src/cljs/airsonic_ui/helpers.cljs +++ b/src/cljs/airsonic_ui/helpers.cljs @@ -55,3 +55,10 @@ (if brief? (brief-duration hours minutes seconds) (long-duration hours minutes seconds)))) + +(defn vector-move [coll prev-index new-index] + (let [items (into (subvec coll 0 prev-index) + (subvec coll (inc prev-index)))] + (-> (subvec items 0 new-index) + (conj (get coll prev-index)) + (into (subvec items new-index))))) diff --git a/test/cljs/airsonic_ui/helpers_test.cljs b/test/cljs/airsonic_ui/helpers_test.cljs index 42b2212..7069585 100644 --- a/test/cljs/airsonic_ui/helpers_test.cljs +++ b/test/cljs/airsonic_ui/helpers_test.cljs @@ -43,3 +43,9 @@ (is (= "59:00" (helpers/format-duration (* 59 60) :brief? true))) (is (= "01:00" (helpers/format-duration 60 :brief? true))) (is (= "00:05" (helpers/format-duration 5 :brief? true))))) + +(deftest vector-move + (testing "Should correctly move items in a vector" + (is (= [1] (helpers/vector-move [1] 0 0))) + (is (= [2 1] (helpers/vector-move [1 2] 0 1) (helpers/vector-move [1 2] 1 0))) + (is (= [1 4 2 3 5] (helpers/vector-move [1 2 3 4 5] 3 1)))))