Re-dnd (drag and drop) is a configurable widget for dragging items onto a drop-zone. It is designed to be configurable, so that it looks and behaves like you want it to. Being re-frame based, it also provides a nice API for querying or changing drag/drop state (through subscribe/dispatch).
You can configure what happens upon dropping an element, with sensible defaults in place if you don't.
Let's get started, shall we?
First add this to your project.clj dependencies
re-dnd will store all its state in the local app-db, just as the rest of your re-frame app.
To avoid name clashes, it will store all its state under the key :dnd/state
in the db.
Below the usage is explained through an example. If you clone the library, you can also run the demo, by running lein fig
, and navigating to https://localhost:9500/demo.html.
To make sure everything is initialized properly, require re-dnd.events in your core.cljs, or where ever your entry point is.
(ns your-app.core
(:require
...
[re-dnd.events] ;; make sure the events are registered and inited properly.
[re-dnd.views] ;;make sure the internal components are registered.
[re-dnd.subs] ;; make sure subs are registered
...
))
Now we can create the visual part of the drag and drop. We define a draggable component and a drop-zone.
NOTE: we provide them with id's (normal keys, here :draggable-1 and :drop-zone-1), make sure these keys are unique globally, ie. not shared with other draggables/drop-zones (on the same page). If you would do this, state changes of one draggable/drop-zone are overwriting state of others.
(ns my-project.my-view
(:require
[re-frame.core :as rf]
[reagent.core :as r]
[re-dnd.events :as dnd]
[re-dnd.views :as dndv]))
(defn my-drop-zone
[]
[dndv/drop-zone :drop-zone-1 ;;:drop-zone-1 is a unique identifier
[:div "A custom body for the drop-zone (optional)"]])
(defn my-draggable
[]
[dndv/draggable :draggable-1
[:div "Style me any way you like, i'm draggable."]])
(defn my-panel
[]
(let [;;this state is necesary to determine if we need to show the drag-box
drag-box-state (rf/subscribe [:dnd/drag-box])]
;;It's best not to put this here, but for conciseness it is done here.
;;It should be called when loading the page with the dnd functionality.
;;It prepares the app-db with some initial state.
(rf/dispatch [:dnd/initialize-drop-zone
:drop-zone-1 ;;note, this key is the same as in my-drop-zone above.
;; options
{:drop-dispatch [:my-drop-dispatch] ;;re-frame event handler that will be called when draggable is dropped on a drop-zone
:drop-marker :my-drop-marker ;;multi-method dispatch-value for dnd/dropped-widget
}
])
(fn []
[:div
;; the drag-box widget is a visual that follows the mouse,
;; and takes the size of the draggable currently being dragged
(when @drag-box-state
[dndv/drag-box])
[my-draggable]
[my-drop-zone]])))
Now we have the basic stuff there. You might need some CSS to make it look decent. The point is that the draggable can be dropped on the drop-zone, and then the [:my-drop-dispatch] event will be called. Let's implement this event:
(def last-id (r/atom 0)) ;;or use ie. (str (random-uuid))
(rf/reg-event-fx
:my-drop-dispatch
(fn [{db :db}
[_
;; the callback contains two vectors, of the source and of the target.
;; Note the source-drop-zone-id, it's possible the dropped element actually comes from
;; a drop-zone (ie. re-ordering within the drop-zone). In the example above, we have an external
;; draggable, in which case source-drop-zone-id would be nil.
[source-drop-zone-id source-element-id]
[drop-zone-id dropped-element-id dropped-position]]] ;;position = index in the list of dropped elements
(swap! last-id inc)
{:db db
:dispatch
;;if the source drop-zone and target drop-zone is the same, it means we need to re-order the items
;; (at least, in this example we want that, but what you want is completely up to you :-))
(if (= source-drop-zone-id drop-zone-id)
;;built-in dispatch for re-ordering elements in a drop-zone
[:dnd/move-drop-zone-element drop-zone-id source-element-id dropped-position]
;;Built-in dispatch for adding a drop-zone-element ('dropped-element') in a drop-zone.
;;Our current logic is to just add a new entry to the drop-zone.
;;Your requirement might be different.
[:dnd/add-drop-zone-element
drop-zone-id
{:id (keyword (str (name source-element-id) "-dropped-" @last-id))
;;The type key is the dispatch-value of the dndv/dropped-widget multi-method.
;;thus, by means of multi-methods we can create any component we'd like.
:type (if (odd? @last-id )
:bluebox
:redbox)}
dropped-position])}))
Before dropping an item, a visual is shown indicating where the component will be dropped (ie. between which already existing dropped elements). You can create a custom one like so:
;;this is a multi method implementation fo dndv/dropped-widget.
;;You can create multiple ones, and based on the :type key in the options map of this thing
(defmethod dndv/dropped-widget
:my-drop-marker
[{:keys [type id]}]
[:div.my-drop-marker "Some visual showing when dragged object is hovering over drop zone."])
If you don't supply the :drop-marker key upon initialization of the drop-zone, by default this implementation will be used.
;;we dispatch on the :type key
(defmulti dropped-widget
(fn [{:keys [type id]}] type))
;;default drop-marker, which you could style using CSS.
(defmethod dropped-widget
:dnd/drop-marker
[{:keys [type id]}]
[:div.drop-marker])
Some default components come with CSS classes. The following base CSS should be put into place, and tweaked where you see fit:
.draggable, dropped-element {
cursor: move;
position: relative;
}
.draggable .drag-mask,
.dropped-element .drag-mask {
position: absolute;
top: 0px;
left: 0px;
width: 0px;
height: 0px;
background-color: rgba(0,0,0,0.2);
z-index: 9999;
}
.drop-zone.highlight {
border: 1px solid #000;
}
.drop-marker {
width: 100%;
height: 4px;
margin-bottom: 5px;
background-color: black;
}
.drag-box {
position: fixed;
background-color: rgba(0,0,0,0.2);
z-index: 999;
height: 0px;
width: 0px;
}
Since this is re-frame, all the registered events are available to you. Here are some that could come in handy:
;;Adds it at position/index 2, this parameter is optional, if omitted, element will be added to the back
(rf/dispatch [:dnd/add-drop-zone-element :my-drop-zone-id :my-dropped-element-id 2])
;;Moves it to the new index 2
(rf/dispatch [:dnd/move-drop-zone-element :my-drop-zone-id :my-dropped-element-id 2])
;;Deletes a dropped element from the drop-zone
(rf/dispatch [:dnd/delete-drop-zone-element :my-drop-zone-id :my-dropped-element-id])
Also, there are various subscriptions that might be of use. They're used internally, but some could be useful to you.
;; returns a map of drop-zone-id to a list of elements within the drop-zone that are also colliding, and their positional index in the drop-zone
;; ie. {:my-drop-zone-1 [[:my-dropped-element-1 0] [:my-dropped-element-2 1]] ...}
(rf/subscribe [:dnd/get-colliding-drop-zone-and-index])
;;all drop-zone elements in the drop-zone, returns a list of maps, each map containing at least keys :type and :id,
;; optionally have key :status
(rf/subscribe [:dnd/dropped-elements :my-drop-zone-id])
;;same as above, but on the correct spot, it also returns a map of type {:type :my-drop-marker :id :my-drop-marker},
;;indicating the drop marker. Only shows the drop-marker if there is hover activity of a draggable over the drop-zone.
;;Internally we map over the result of this sub, and dispatch the dndv/dropped-widget multimethod with the record.
(rf/subscribe [:dnd/dropped-elements-with-drop-marker :my-drop-zone-id])
;;lists all registered drop-zones and their state as a map, keyed with drop-zone-id.
(rf/subscribe [:dnd/drop-zones])
;;do we have a colliding draggable over this drop-zone?
(rf/subscribe [:dnd/draggable-overlaps? :my-drop-zone-id])
;;position of the mouse
(rf/subscribe [:dnd/mouse-position])
;;position of the drag-box, when dragging
(rf/subscribe [:dnd/drag-box])
Put this in your Emacs config file:
(setq cider-cljs-lein-repl
"(do (require 'figwheel-sidecar.repl-api)
(figwheel-sidecar.repl-api/start-figwheel!)
(figwheel-sidecar.repl-api/cljs-repl))")
Navigate to a clojurescript file and start a figwheel REPL with cider-jack-in-clojurescript
or (C-c M-J
), or if you use Spacemacs, just press ,"
.
lein fig
Figwheel will automatically push cljs changes to the browser.
Wait a bit, then browse to https://localhost:9500/demo.html.
To compile clojurescript to javascript:
lein build-dev