mirror of
https://github.com/heyarne/airsonic-ui.git
synced 2026-05-07 02:33:39 +02:00
Implement reusable dropdown in bulma.dropdown
This commit is contained in:
parent
99759c919b
commit
bb700b4b66
6 changed files with 179 additions and 29 deletions
15
src/cljs/airsonic_ui/components/current_queue/subs.cljs
Normal file
15
src/cljs/airsonic_ui/components/current_queue/subs.cljs
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
(ns airsonic-ui.components.current-queue.subs
|
||||||
|
(:require [re-frame.core :as rf]))
|
||||||
|
|
||||||
|
(defn queue-info [playlist]
|
||||||
|
{:count (count playlist)
|
||||||
|
:duration
|
||||||
|
(reduce (fn [acc [_ item]]
|
||||||
|
(+ acc (:duration item))) 0 (:items playlist))})
|
||||||
|
|
||||||
|
(println "registering the sub")
|
||||||
|
|
||||||
|
(rf/reg-sub
|
||||||
|
:current-queue/info
|
||||||
|
:<- [:audio/current-playlist]
|
||||||
|
queue-info)
|
||||||
|
|
@ -3,9 +3,11 @@
|
||||||
[reagent.core :as r]
|
[reagent.core :as r]
|
||||||
["react-sortable-hoc" :refer [SortableHandle]]
|
["react-sortable-hoc" :refer [SortableHandle]]
|
||||||
[bulma.icon :refer [icon]]
|
[bulma.icon :refer [icon]]
|
||||||
|
[bulma.dropdown.views :refer [dropdown]]
|
||||||
[airsonic-ui.helpers :as helpers]
|
[airsonic-ui.helpers :as helpers]
|
||||||
[airsonic-ui.components.sortable.views :as sortable]
|
[airsonic-ui.components.sortable.views :as sortable]
|
||||||
[airsonic-ui.routes :as routes]))
|
[airsonic-ui.routes :as routes]
|
||||||
|
[airsonic-ui.components.current-queue.subs]))
|
||||||
|
|
||||||
(def SortHandle
|
(def SortHandle
|
||||||
(SortableHandle.
|
(SortableHandle.
|
||||||
|
|
@ -14,24 +16,14 @@
|
||||||
;; to return React elements from the component.
|
;; to return React elements from the component.
|
||||||
(fn []
|
(fn []
|
||||||
(r/as-element [:span.is-size-7.has-text-grey-lighter
|
(r/as-element [:span.is-size-7.has-text-grey-lighter
|
||||||
[icon :elevator]]))))
|
[icon :elevator]]))))
|
||||||
|
|
||||||
(defn song-actions []
|
(defn song-actions []
|
||||||
(let [controls-id (str "song-actions-" (random-uuid))
|
;; TODO: Implement both of these
|
||||||
is-active? (r/atom false)]
|
[dropdown {:items [{:label "Remove from queue"
|
||||||
(fn []
|
:event []}
|
||||||
[(if @is-active? :div.dropdown.is-right.is-active :div.dropdown.is-right)
|
{:label "Go to source"
|
||||||
[:div.dropdown-trigger
|
:event []}]}])
|
||||||
[:span.is-small.button {:aria-haspopup "true"
|
|
||||||
:aria-controls controls-id
|
|
||||||
:on-click #(swap! is-active? not)}
|
|
||||||
[icon :ellipses]]]
|
|
||||||
[:div.dropdown-menu {:id controls-id, :role "menu"}
|
|
||||||
[:div.dropdown-content
|
|
||||||
;; TODO: Implement removal
|
|
||||||
[:a.dropdown-item {:href "#"} "Remove from queue"]
|
|
||||||
;; TODO: Implement "Go to source"
|
|
||||||
[:a.dropdown-item {:href "#"} "Go to source"]]]])))
|
|
||||||
|
|
||||||
(defn artist-link [{id :artistId, artist :artist}]
|
(defn artist-link [{id :artistId, artist :artist}]
|
||||||
(if id
|
(if id
|
||||||
|
|
@ -44,14 +36,14 @@
|
||||||
:on-click (helpers/muted-dispatch [:audio-player/set-current-song idx])}
|
:on-click (helpers/muted-dispatch [:audio-player/set-current-song idx])}
|
||||||
(:title song)])
|
(:title song)])
|
||||||
|
|
||||||
(defn song-table [{:keys [songs current-song]}]
|
(defn song-table [{:keys [songs current-song-idx]}]
|
||||||
[:table.song-listing-table.table.is-fullwidth
|
[:table.song-listing-table.table.is-fullwidth
|
||||||
[:thead>tr
|
[:thead>tr
|
||||||
[:td.is-narrow]
|
[:td.is-narrow]
|
||||||
[:td.song-artist "Artist"]
|
[:td.song-artist "Artist"]
|
||||||
[:td.song-title "Title"]
|
[:td.song-title "Title"]
|
||||||
[:td.song-duration "Duration"]
|
[:td.song-duration "Duration"]
|
||||||
[:td.is-narrow]]
|
[:td.song-actions.is-narrow]]
|
||||||
[sortable/sortable-component
|
[sortable/sortable-component
|
||||||
{:items songs
|
{:items songs
|
||||||
:container [:tbody]
|
:container [:tbody]
|
||||||
|
|
@ -59,7 +51,7 @@
|
||||||
|
|
||||||
:render-item
|
:render-item
|
||||||
(fn [{[idx song] :value}]
|
(fn [{[idx song] :value}]
|
||||||
[(if (= (:id song) (:id current-song)) :tr.is-playing :tr)
|
[(if (= idx current-song-idx) :tr.is-playing :tr)
|
||||||
[:td.sortable-handle.is-narrow [:> SortHandle]]
|
[:td.sortable-handle.is-narrow [:> SortHandle]]
|
||||||
[:td.song-artist [artist-link song]]
|
[:td.song-artist [artist-link song]]
|
||||||
[:td.song-title [song-link song idx]]
|
[:td.song-title [song-link song idx]]
|
||||||
|
|
@ -68,16 +60,32 @@
|
||||||
|
|
||||||
:on-sort-end
|
:on-sort-end
|
||||||
(fn [{:keys [old-idx new-idx]}]
|
(fn [{:keys [old-idx new-idx]}]
|
||||||
(println "moving from" old-idx "to" new-idx)
|
|
||||||
(dispatch [:audio-player/move-song old-idx new-idx]))}]])
|
(dispatch [:audio-player/move-song old-idx new-idx]))}]])
|
||||||
|
|
||||||
|
(defn collection-info [{:keys [playlist-info]}]
|
||||||
|
[:ul.is-smaller.collection-info
|
||||||
|
[:li [icon :audio-spectrum] (str (:count playlist-info)
|
||||||
|
(if (pos? (:count playlist-info))
|
||||||
|
" tracks"
|
||||||
|
" track"))]
|
||||||
|
[:li [icon :clock] (helpers/format-duration (:duration playlist-info))]])
|
||||||
|
|
||||||
|
(defn playlist [props]
|
||||||
|
[:div
|
||||||
|
[collection-info props]
|
||||||
|
[song-table {:songs (get-in props [:current-playlist :items])
|
||||||
|
:current-song-idx (get-in props [:current-playlist :current-idx])}]])
|
||||||
|
|
||||||
|
(defn empty-playlist []
|
||||||
|
[:p "You are currently not playing anything. Use the search or go to your "
|
||||||
|
[:a {:href (routes/url-for ::routes/library)} "Library"] " to start playing some music."])
|
||||||
|
|
||||||
(defn current-queue []
|
(defn current-queue []
|
||||||
[:section.section>div.container
|
(let [current-playlist @(subscribe [:audio/current-playlist])
|
||||||
[:h1.title "Current Queue"]
|
playlist-info @(subscribe [:current-queue/info])]
|
||||||
(let [current-playlist @(subscribe [:audio/current-playlist])
|
[:section.section>div.container
|
||||||
current-song @(subscribe [:audio/current-song])]
|
[:h1.title "Current Queue"]
|
||||||
(if (empty? current-playlist)
|
(if (empty? current-playlist)
|
||||||
[:p "You are currently not playing anything. Use the search or go to your "
|
[empty-playlist]
|
||||||
[:a {:href (routes/url-for ::routes/library)} "Library"] " to start playing some music."]
|
[playlist {:current-playlist current-playlist
|
||||||
[song-table {:songs (:items current-playlist)
|
:playlist-info playlist-info}])]))
|
||||||
:current-song current-song}]))])
|
|
||||||
|
|
|
||||||
20
src/cljs/bulma/dropdown/events.cljs
Normal file
20
src/cljs/bulma/dropdown/events.cljs
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
(ns bulma.dropdown.events
|
||||||
|
(:require [re-frame.core :as rf]))
|
||||||
|
|
||||||
|
(defn show-dropdown [db [_ dropdown-id]]
|
||||||
|
(assoc-in db [:bulma :visible-dropdown] dropdown-id))
|
||||||
|
|
||||||
|
(rf/reg-event-db ::show show-dropdown)
|
||||||
|
|
||||||
|
(defn hide-dropdown [db _]
|
||||||
|
(update db :bulma dissoc :visible-dropdown))
|
||||||
|
|
||||||
|
(rf/reg-event-db ::hide hide-dropdown)
|
||||||
|
|
||||||
|
(defn toggle-dropdown [db [_ dropdown-id]]
|
||||||
|
(let [visible-dropdown (get-in db [:bulma :visible-dropdown])]
|
||||||
|
(if (= visible-dropdown dropdown-id)
|
||||||
|
(hide-dropdown db [::hide])
|
||||||
|
(show-dropdown db [::show dropdown-id]))))
|
||||||
|
|
||||||
|
(rf/reg-event-db ::toggle toggle-dropdown)
|
||||||
22
src/cljs/bulma/dropdown/subs.cljs
Normal file
22
src/cljs/bulma/dropdown/subs.cljs
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
(ns bulma.dropdown.subs
|
||||||
|
(:require [re-frame.core :as rf]))
|
||||||
|
|
||||||
|
;; NOTE: This is almost the same as bulma.modal.subs
|
||||||
|
;; Maybe we can provide some abstraction that covers both, but maybe we shouldn't
|
||||||
|
|
||||||
|
(defn visible-dropdown
|
||||||
|
"Gives us the ID of the currently visible dropdown"
|
||||||
|
[db _]
|
||||||
|
(get-in db [:bulma :visible-dropdown]))
|
||||||
|
|
||||||
|
(rf/reg-sub ::visible-dropdown visible-dropdown)
|
||||||
|
|
||||||
|
(defn visible?
|
||||||
|
"Predicate to check the visibility of a single modal"
|
||||||
|
[visible-dropdown [_ dropdown-id]]
|
||||||
|
(= visible-dropdown dropdown-id))
|
||||||
|
|
||||||
|
(rf/reg-sub
|
||||||
|
::visible?
|
||||||
|
:<- [::visible-dropdown]
|
||||||
|
visible?)
|
||||||
45
src/cljs/bulma/dropdown/views.cljs
Normal file
45
src/cljs/bulma/dropdown/views.cljs
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
(ns bulma.dropdown.views
|
||||||
|
(:require [re-frame.core :refer [dispatch subscribe]]
|
||||||
|
[reagent.core :as r]
|
||||||
|
[bulma.icon :refer [icon]]
|
||||||
|
[bulma.dropdown.events :as ev]
|
||||||
|
[bulma.dropdown.subs :as sub]))
|
||||||
|
|
||||||
|
;; there's "muted-dispatch" in airsonic-ui.helpers which does the same thing
|
||||||
|
;; it's not used here because all the bulma.*-components should work independently
|
||||||
|
|
||||||
|
(defn muted-dispatch [event-vector]
|
||||||
|
(fn [e]
|
||||||
|
(.preventDefault e)
|
||||||
|
(dispatch event-vector)))
|
||||||
|
|
||||||
|
(defn generate-id []
|
||||||
|
(str "bulma-dropdown-" (random-uuid)))
|
||||||
|
|
||||||
|
(defn click-overlay
|
||||||
|
[]
|
||||||
|
[:div {:style {:position "fixed"
|
||||||
|
:z-index 19 ;; <- 20 is the z-index of .dropdown-menu
|
||||||
|
:top 0
|
||||||
|
:left 0
|
||||||
|
:bottom 0
|
||||||
|
:right 0}
|
||||||
|
:on-click #(dispatch [::ev/hide])}])
|
||||||
|
|
||||||
|
(defn dropdown [{:keys [items]}]
|
||||||
|
(let [dropdown-id (generate-id)]
|
||||||
|
(fn []
|
||||||
|
(let [visible? @(subscribe [::sub/visible? dropdown-id])]
|
||||||
|
[(if visible? :div.dropdown.is-right.is-active :div.dropdown.is-right)
|
||||||
|
(when visible? [click-overlay])
|
||||||
|
[:div.dropdown-trigger
|
||||||
|
[:span.is-small.button {:aria-haspopup "true"
|
||||||
|
:aria-controls dropdown-id
|
||||||
|
:on-click #(dispatch [::ev/toggle dropdown-id])}
|
||||||
|
[icon :ellipses]]]
|
||||||
|
[:div.dropdown-menu {:id dropdown-id, :role "menu"}
|
||||||
|
[:div.dropdown-content
|
||||||
|
(for [[idx {:keys [label event]}] (map-indexed vector items)]
|
||||||
|
^{:key (str dropdown-id "-" idx)}
|
||||||
|
[:a.dropdown-item {:href "#"
|
||||||
|
:on-click (muted-dispatch event)} label])]]]))))
|
||||||
40
test/cljs/bulma/dropdown_test.cljs
Normal file
40
test/cljs/bulma/dropdown_test.cljs
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
(ns bulma.dropdown-test
|
||||||
|
(:require [cljs.test :refer-macros [deftest testing is]]
|
||||||
|
[bulma.dropdown.subs :as sub]
|
||||||
|
[bulma.dropdown.events :as ev]))
|
||||||
|
|
||||||
|
;; NOTE: Here as well; this code is very much like the modal code
|
||||||
|
;; Not sure whether to explicitly duplicate it or provide some smarter
|
||||||
|
;; abstraction that's harder to understand at first sight
|
||||||
|
|
||||||
|
(enable-console-print!)
|
||||||
|
|
||||||
|
(deftest bulma-dropdowns
|
||||||
|
(testing "Should create a collection of dropdowns if there is none"
|
||||||
|
(let [new-db (ev/show-dropdown {} [::ev/show :some-dropdown-id])]
|
||||||
|
(is (= :some-dropdown-id (sub/visible-dropdown new-db [::sub/visible-dropdown])))))
|
||||||
|
(testing "Should hide other dropdowns when displaying a new one"
|
||||||
|
(let [dropdown-ids [:some-id-1 :some-id-2 :some-id-3]
|
||||||
|
new-db (reduce (fn [db dropdown-id]
|
||||||
|
(ev/show-dropdown db [::ev/show dropdown-id]))
|
||||||
|
{} dropdown-ids)]
|
||||||
|
(is (= :some-id-3 (sub/visible-dropdown new-db [::sub/visible-dropdown])))))
|
||||||
|
(testing "Should remove a dropdown from the collection when we hide it"
|
||||||
|
(let [dropdown-ids [:some-id-1 :some-id-2 :some-id-3]
|
||||||
|
new-db (-> (reduce (fn [db dropdown-id]
|
||||||
|
(ev/show-dropdown db [::ev/show dropdown-id]))
|
||||||
|
{} dropdown-ids)
|
||||||
|
(ev/hide-dropdown [::ev/hide]))]
|
||||||
|
(is (not (some? (sub/visible-dropdown new-db [::sub/visible-dropdown]))))))
|
||||||
|
(testing "Should tell us about the visibility of a dropdown with a predicate"
|
||||||
|
(is (true? (-> (ev/show-dropdown {} [::ev/show :getting-repetitive])
|
||||||
|
(sub/visible-dropdown [::sub/visible-dropdown])
|
||||||
|
(sub/visible? [::sub/visible? :getting-repetitive])))))
|
||||||
|
(testing "Dropdown toggling"
|
||||||
|
(is (true? (-> (ev/toggle-dropdown {} [::ev/toggle :some-generic-dropdown])
|
||||||
|
(sub/visible-dropdown [::sub/visible-dropdown])
|
||||||
|
(sub/visible? [::sub/visible? :some-generic-dropdown]))))
|
||||||
|
(is (not (true? (-> (ev/toggle-dropdown {} [::ev/toggle :some-generic-dropdown])
|
||||||
|
(ev/toggle-dropdown [::ev/toggle :some-generic-dropdown])
|
||||||
|
(sub/visible-dropdown [::sub/visible-dropdown])
|
||||||
|
(sub/visible? [::sub/visible? :some-generic-dropdown])))))))
|
||||||
Loading…
Add table
Add a link
Reference in a new issue