<|start_filename|>src/cljs/re_dnd_demo/db.cljs<|end_filename|>
(ns re-dnd-demo.db)

(def default-db
  {})


<|start_filename|>resources/public/css/demo.css<|end_filename|>
body {
   font-family: Verdana, sans-serif;
}

.draggable {
   border: 1px solid red;
   width: 250px;
   height: 100px;
   text-align: center;
   padding: 20px;
   cursor: move;
   position: relative;
}

.dropped-element {
  position: relative;
   cursor: move;
}

.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 {
   border: 1px solid green;
   width: 270px;
   height: 800px;
   text-align: center;
   padding: 20px;
   box-sizing: border-box;
   overflow-y: auto;
}

.drop-zone.highlight {
   border: 3px solid green;
}

.drop-marker {
   width: 100%;
   height: 4px;
   margin-bottom: 5px;
   background-color: orange;
}

.container {
   width: 800px;
   margin: 0 auto;
   border: 1px solid #ededed;
}

.box {
   width: 100%;
   height: 60px;
   margin-bottom: 5px;
   padding-top: 30px;
   color: white;
}

.clear {
  clear: both;
}

.red-box {
   background-color: #d62a3b;
}

.blue-box {
   background-color: #486fad;
}

.draggable {
   margin-bottom: 10px;
}

.btn {
   margin: 20px;
   padding: 20px;
   background-color: #ededed;
   border: 1px solid #ccc;
}


<|start_filename|>test/cljs/re_dnd/runner.cljs<|end_filename|>
(ns re-dnd.runner
    (:require [doo.runner :refer-macros [doo-tests]]
              [re-dnd.core-test]))

(doo-tests 're-dnd.core-test)


<|start_filename|>src/cljs/re_dnd_demo/views.cljs<|end_filename|>
(ns re-dnd-demo.views
  (:require [fipp.clojure :refer [pprint]]
            [re-dnd-demo.events :as h]
            [re-dnd.events :as dnd]
            [re-dnd.views :as dndv]
            [re-frame.core :as rf]
            [reagent.core :as r]
            [taoensso.timbre :as timbre
             :refer-macros (log  trace  debug  info  warn  error  fatal  report
                                 logf tracef debugf infof warnf errorf fatalf reportf
                                 spy get-env log-env)]))

(rf/reg-sub :db
            (fn [db] db))

(defn debug-panel
  "pretty prints data in a nice box on the screen."
  [s]
  (let [collapsed (r/atom false)]
    (fn [s]
      [:div.debug-window-wrap
       [:div {:on-click #(swap! collapsed not)
              :style {:cursor :pointer
                      :padding "10px"
                      :border "1px solid #ccc"}}
        [:div.clear
         [:div.pull-right [:b "Debug window "]]
         [:div (if @collapsed "▷ expand" "▽ collapse")]]]
       (when-not @collapsed
         [:pre (with-out-str (pprint s))])])))

;;this should have its own file, custom_events
(defmethod dndv/dropped-widget
  :my-drop-marker
  [{:keys [type id]}]
  [:div.drop-marker])

(defmethod dndv/drag-handle
  :my-drop-marker
  [{:keys [type id]}]
  [:div])

(defmethod dndv/dropped-widget
  :bluebox
  [{:keys [type id]}]
  [:div.box.blue-box
   (str type ", " id)])

(defmethod dndv/drag-handle
  :bluebox
  [{:keys [type id]}]
  [:div "bluedraghandlee"])

(defmethod dndv/dropped-widget
  :redbox
  [{:keys [type id]}]
  [:div.box.red-box
   (str type ", " id)])

(defmethod dndv/drag-handle
  :redbox
  [{:keys [type id]}]
  [:div "reddraghandle"])

(defn main-panel
  []
  (let [drag-box-state (rf/subscribe [:dnd/drag-box])
        db             (rf/subscribe [:db])
        last-id        (r/atom 1)]
    (rf/dispatch [:dnd/initialize-drop-zone
                  :drop-zone-1
                  {:drop-dispatch          [:my-drop-dispatch]
                   :three-part-drag-handle true
                   :drop-marker            :my-drop-marker}
                  ;;initial elements can be put here
                  [{:type :bluebox
                    :id   (keyword (str "drop-zone-element-" 0))}
                   {:type :redbox
                    :id   (keyword (str "drop-zone-element-" 1))}]])
    (fn []
      [:div.container
       {:style {:height "1400px"}}
       [:div {:style {:position :absolute
                      :border   "1px solid black"
                      :top      "400px"}}
        #_(when @drag-box-state
            [dndv/drag-box]) ;;this thing follows the mouse, and takes over the draggable's size
        [dndv/drag-box]
        [:div
         [:p "Drag draggables to the drop-zone to the right, or re-order dropped elements in the drop-zone"]
         #_[debug-panel @db]
         [:button.btn.btn-primary
          {:on-click #(do
                        (swap! last-id inc)
                        (rf/dispatch [:dnd/add-drop-zone-element
                                      :drop-zone-1
                                      {:type (if (odd? @last-id) :redbox :bluebox)
                                       :id   (keyword (str "drop-zone-element-" @last-id))}]))}
          "Add element to dropzone programmatically"]
         [:div.clear]
         [:div {:style {:float :left}}
          [dndv/draggable :draggable1 [:span "draggable1"]]
          [dndv/draggable :draggable2 [:span "draggable2"]]
          [dndv/draggable :draggable3 [:span "draggable3"]]]
         [:div {:style {:float :right}}
          [dndv/drop-zone :drop-zone-1
           [:div "Drop zone"]]]]]
       [:div.clear]

       #_[debug-panel @db]])))


<|start_filename|>resources/public/css/style.css<|end_filename|>
.draggable, dropped-element {
   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;
}


<|start_filename|>src/cljs/re_dnd/core.cljs<|end_filename|>
(ns re-dnd.core
  (:require [reagent.core :as reagent]
            [re-frame.core :as re-frame]
            [re-dnd.events]
            [re-dnd.subs]
            [re-dnd.views]
            [re-dnd.config]))


<|start_filename|>src/cljs/re_dnd_demo/events.cljs<|end_filename|>
(ns re-dnd-demo.events
  (:require [re-dnd-demo.db :as db]
            [re-frame.core :as rf]
            [reagent.core :as r]
            [taoensso.timbre :as timbre
             :refer-macros (log  trace  debug  info  warn  error  fatal  report
                                 logf tracef debugf infof warnf errorf fatalf reportf
                                 spy get-env log-env)]))

(rf/reg-event-db
 :initialize-db
 (fn  [_ _]
   db/default-db))

(def last-id (r/atom 0))

(rf/reg-event-fx
 :my-drop-dispatch
 (fn [{db :db}
      [_
       [source-drop-zone-id source-element-id]
       [drop-zone-id dropped-element-id dropped-position]]]
   ;;position = index in the list of dropped elements
   (debug "source:" source-drop-zone-id source-element-id
          " target:" drop-zone-id dropped-element-id dropped-position)
   (swap! last-id inc)
   {:db       db
    :dispatch
    (if (= source-drop-zone-id drop-zone-id)
      [:dnd/move-drop-zone-element drop-zone-id source-element-id dropped-position]

      [:dnd/add-drop-zone-element
       drop-zone-id
       {:id   (keyword (str (name source-element-id) "-dropped-" @last-id))
        :type (if (odd? @last-id )
                :bluebox
                :redbox)}
       dropped-position])}))


<|start_filename|>src/clj/re_dnd/core.clj<|end_filename|>
(ns re-dnd.core
  (:require [taoensso.timbre :as timbre]))

(def log-config
  {:level          (keyword (env :log-level))
   :ns-whitelist   [] #_ []
   :ns-blacklist   [] #_ ["taoensso.*"]
   :middleware     []
   :timestamp-opts timbre/default-timestamp-opts
   :output-fn      timbre/default-output-fn
   :appenders      {:debug (appenders/println-appender
                            {:level  :debug
                             :stream :auto})}})


(timbre/set-config! log-config)


<|start_filename|>src/cljs/re_dnd/subs.cljs<|end_filename|>
(ns re-dnd.subs
  (:require-macros [reagent.ratom :refer [reaction]])
  (:require [re-dnd.events :as h]
            [re-frame.core :as re-frame]
            [taoensso.timbre :as timbre
             :refer-macros (log  trace  debug  info  warn  error  fatal  report
                                 logf tracef debugf infof warnf errorf fatalf reportf
                                 spy get-env log-env)]))

(re-frame/reg-sub
 :dnd/position
 (fn [db [_ id]]
   (get-in db [:dnd/state :draggables id :position])))

(defn get-dz-element
 [db dz-id draggable-id]
 (->>
  (get-in db [:dnd/state :drop-zones dz-id])
  (filter (comp (partial = draggable-id) :id))
  first))

(defn drag-box-parent-bounding-rect
  "DOM lookup, returns {:x 0 :y 0} if not found"
  []
  (let [e (.getElementById js/document "drag-box" )
        p (when e
            (some-> e
                    .-parentNode
                    .-parentNode
                    h/bounding-rect))]
    (or  {:left 0 :top 0})))

(re-frame/reg-sub
 :dnd/drag-box
 (fn [db _]
   (let [[drop-zone-id draggable-id] (h/find-first-dragging-element db)
                   ]
     (if (and drop-zone-id draggable-id)
       (-> (get-dz-element db drop-zone-id draggable-id)
           :position)
       ;;else
       (when draggable-id
         (get-in db [:dnd/state :draggables draggable-id :position]))))))

(re-frame/reg-sub
 :dnd/db
 (fn [db _]
   db))

(re-frame/reg-sub
 :dnd/drag-status
 (fn [db [_ id drop-zone-id]]
   (if drop-zone-id
     (:status (get-dz-element db drop-zone-id id))
     (get-in db [:dnd/state :draggables id :status]))))

(re-frame/reg-sub
 :dnd/draggable-overlaps?
 (fn [db [_ id]]
   ;;returns true if the position of :drag-box overlaps the drop-zone with the supplied id.
   (let [drop-zone (h/bounding-rect (.getElementById
                                     js/document
                                     (str "drop-zone-" (name id))))
         drag-box  (h/bounding-rect (.getElementById
                                     js/document
                                     "drag-box"))
         collides? (h/collides? drop-zone drag-box)]
     collides?)))

(re-frame/reg-sub
 :dnd/mouse-position
 (fn [db _]
   (get-in db [:dnd/state :mouse-position])))

(defn calculate-drop-zone-collisions
  "Returns a list of tuples of [<dropzone-id> <index>]"
  [dropzone-elements]
  (let [drag-box (h/bounding-rect (.getElementById js/document "drag-box"))]
    (->> dropzone-elements
         (map-indexed (fn [i e]
                        [(inc i) e]))
         (filter
          (fn [[i e]]
            (if (not (:id e))
              false
              (let [dropped-rect (h/bounding-rect
                                  (.getElementById
                                   js/document
                                   (str "dropped-element-" (name (:id e)))))]
                (h/collides? dropped-rect drag-box)))))
         (map (fn [[i e]]
                [(:id e)
                 (if  (= 1 i)
                   ;; is top of dragbox higher than dropped rec?
                   ;; then return index 0, 1 otherwise, to
                   ;; allow special case of moving to top
                   (let [dropped-rect (h/bounding-rect
                                       (.getElementById
                                        js/document
                                        (str "dropped-element-" (name (:id e)))))]
                     (if (<= (:top drag-box) (:top dropped-rect))
                       0 1))
                   i
                   )])))))

(re-frame/reg-sub
 :dnd/dropped-item-overlap-id
 (fn [[_ id] _]
   ;; we need this to trigger a re-run of this sub when the mouse moves.
   ;; these derived signal graphs only rerun if signal changes,
   ;; but draggable-overlaps? can become stable
   ;; we do need that one for improved efficiency
   [(re-frame/subscribe [:dnd/mouse-position])
    (re-frame/subscribe [:dnd/draggable-overlaps? id])
    (re-frame/subscribe [:dnd/dropped-elements id])])
 (fn [[mouse-pos draggable-overlaps? dropzone-elements]]
   ;;returns true if the position of :drag-box overlaps the dragtarget of the supplied id.
   (if-not draggable-overlaps?
     nil
     ;;else ,we have overlap, calculate with which of the dropped elements we collide, and return the first one if there's more than one
     (-> dropzone-elements
         calculate-drop-zone-collisions
         ffirst))))

(re-frame/reg-sub
 :dnd/dragged-element
 (fn [db _]
   (let [draggable-being-dragged   (->>
                                    db
                                    :dnd/state
                                    :draggables
                                    vals
                                    (filter #(= :dragging (:status %)))
                                    first)
         droppeditem-being-dragged (->>
                                    db
                                    :dnd/state
                                    :drop-zones
                                    vals
                                    flatten
                                    (filter #(= :dragging (:status %)))
                                    first)]
     (or draggable-being-dragged droppeditem-being-dragged))))

(re-frame/reg-sub
 :dnd/dragdrop-options
 (fn [db [_ id]]
   (get-in db [:dnd/state :drop-zone-options id])))

(re-frame/reg-sub
 :dnd/dropped-elements-with-drop-marker
 (fn [[_ id] _]
   [(re-frame/subscribe [:dnd/dragged-element])
    (re-frame/subscribe [:dnd/dragdrop-options id])
    (re-frame/subscribe [:dnd/dropped-item-overlap-id id])
    (re-frame/subscribe [:dnd/draggable-overlaps? id])
    (re-frame/subscribe [:dnd/dropped-elements id])])
 (fn [[dragged-element options overlap-id overlap-dropzone? dropzone-elements]]
   (if (and (or overlap-dropzone? overlap-id) dragged-element)
     ;;we have overlap, and there is dragging going on, insert the separator in there.
     (let [dm          (:drop-marker options)
           sep         (if dm
                         {:type dm
                          :id   dm}
                         {:type :dnd/drop-marker
                          :id   :dnd/drop-marker})
           parts       (partition-by #(= overlap-id (:id %)) dropzone-elements)
           dragbox     (h/bounding-rect (.getElementById js/document "drag-box"))
           overlap-elt (when overlap-id
                         (h/bounding-rect
                          (.getElementById
                           js/document
                           (str "dropped-element-" (name overlap-id)))))
           sz          (count parts)]
       (flatten
        (case sz
          0 [sep]
          1 [(first parts) sep]
          2 (if (= overlap-id (-> parts ffirst :id))
              (if (<= (:top dragbox) (:top overlap-elt ))
                [sep (first parts) (last parts)]
                [(first parts) sep (last parts)])
              ;;else, put sep at the back
              [(first parts) (last parts) sep])
          3 [(first parts) (second parts) sep (last parts)])))
     ;;else no dragging going on, return the elements
     dropzone-elements)))

(re-frame/reg-sub
 :dnd/get-colliding-drop-zone-and-index
 ;; Returns collisions of currently dragged elements with drop-zone(s)
 ;; Returns a map from drop-zone-id
 :<- [:dnd/mouse-position]
 :<- [:dnd/drop-zones]
 :<- [:dnd/dragged-element]
 (fn [[mouse drop-zones dragged-element]]
   (into {}
         (comp
          (map
           (fn [[dz-id dz]]
             (let [drag-box (h/bounding-rect (.getElementById js/document "drag-box"))
                   dz-box   (h/bounding-rect (.getElementById js/document (str "drop-zone-" (name dz-id))))
                   k        (when (h/collides? drag-box dz-box)
                              dz-id)]
               [k (calculate-drop-zone-collisions dz)])))
          (remove
           (fn [[k _]]
             (nil? k))))
         drop-zones)))

(re-frame/reg-sub
 :dnd/drop-zones
 (fn [db _]
   (get-in db [:dnd/state :drop-zones])))

(re-frame/reg-sub
 :dnd/dropped-elements
 (fn [db [_ id]]
   (get-in db [:dnd/state :drop-zones id])))


<|start_filename|>src/cljs/re_dnd/views.cljs<|end_filename|>
(ns re-dnd.views
  (:require [goog.dom :as dom]
            [goog.style :as style]
            [re-dnd.events :as dnd]
            [re-frame.core :as rf]
            [reagent.core :as r]
            [taoensso.timbre :as timbre
             :refer-macros (log  trace  debug  info  warn  error  fatal  report
                                 logf tracef debugf infof warnf errorf fatalf reportf
                                 spy get-env log-env)]))

(defmulti dropped-widget
  (fn [{:keys [type id]}] type))

(defmethod dropped-widget
  :dnd/drop-marker
  [{:keys [type id]}]
  [:div.drop-marker])

(defmulti drag-handle
  (fn [{:keys [type id]}] type))

(defmethod drag-handle
  :dnd/drop-marker
  [{:keys [type id]}]
  [:div.drop-marker])

(defmethod drag-handle
  :default
  [{:keys [type id]}]
  [:div.drop-marker])

(defn start-drag-fn
  [id drop-zone-id e e2]
  (let  [p (or (dom/getAncestorByClass (.-target e) "dropped-element")
               (dom/getAncestorByClass (.-target e) "draggable"))
         bounds (style/getBounds p)]
    (rf/dispatch [:dnd/start-drag id
                  drop-zone-id
                  (.-left bounds)
                  (.-top bounds)
                  (.-width bounds)
                  (.-height bounds)])))

(defn hover-fn
  [elt-id drop-zone-id hover-in?]
  (rf/dispatch [:dnd/hover elt-id drop-zone-id hover-in?]))

(defn draggable
  ([id]
   [draggable id nil])
  ([id body]
   (let [drag-status (rf/subscribe [:dnd/drag-status id nil])]
     (fn [id body]
       [:div.draggable
        {:on-mouse-down (partial start-drag-fn id nil)
         :on-mouse-over (partial hover-fn id nil true)
         :on-mouse-out (partial hover-fn id nil false)}
        (when (= :hover @drag-status)
          [:div.drag-mask
           {:style {:width "100%"
                    :height "100%"}}])
        (when body
          body)]))))

(defn reorder-fn
  [drop-zone-id dropped-element-id e]
  (rf/dispatch [:dnd/reorder-drop drop-zone-id dropped-element-id]))

(defn dropped-element
  [id de]
  (let [drag-status (rf/subscribe [:dnd/drag-status (:id de) id])
        options     (rf/subscribe [:dnd/dragdrop-options id])]
    (fn [id de]
      [:div.dropped-element.row
       {:id (str "dropped-element-" (name (:id de)))}
       #_(when (= :hover @drag-status)
          [:div.drag-mask
           {:style {:width  "100%"
                    :height "100%"}}])

       [:div.drag-handle.col-md-1
        (when-not (:three-part-drag-handle @options)
          {:on-mouse-over (partial hover-fn (:id de) id true)
           :on-mouse-out  (partial hover-fn (:id de) id false)
           :on-mouse-down (partial start-drag-fn (:id de) id)
           :on-mouse-up   (partial reorder-fn id (:id de))})
        (when (:three-part-drag-handle @options)
          [:i.zmdi.zmdi-caret-up
           {:on-click #(rf/dispatch [:dnd/move-up id (:id de)])}])
        (when (:three-part-drag-handle @options)
          [:i.zmdi.zmdi-menu
           {:on-mouse-over (partial hover-fn (:id de) id true)
            :on-mouse-out  (partial hover-fn (:id de) id false)
            :on-mouse-down (partial start-drag-fn (:id de) id)
            :on-mouse-up   (partial reorder-fn (:id de) id)}])
        (when (:three-part-drag-handle @options)
          [:i.zmdi.zmdi-caret-down
           {:on-click #(rf/dispatch [:dnd/move-down id (:id de)])}])
        [drag-handle de]]
       [:div.dropped-element-body.col-md-11
        ^{:key (:id de)#_(hash de)} ;;force rerender everytime `de` changes. since it's an multimethod, this might otherwise fail sometimes.
        [dropped-widget de]]
       [:div {:style {:clear :both}}]])))

(defn drop-zone
  ([id]
   [drop-zone id nil])
  ([id body]
   (let [dropped-elements (rf/subscribe [:dnd/dropped-elements-with-drop-marker id])
         overlaps?        (rf/subscribe [:dnd/draggable-overlaps? id])]
     (fn [id body]
       [:div.drop-zone
        {:id        (str "drop-zone-" (name id))
         :className (if @overlaps? "highlight" "")}
        (map (fn [de]
               ^{:key (:id de)}
               [dropped-element id de])
             @dropped-elements)
        (when body body)]))))

(defn drag-box
  "Box floating around following the mouse"
  []
  (let [s (rf/subscribe [:dnd/drag-box])]
    (fn []
      (let [{:keys [width height x y] :as drag-box} @s]
        [:div#drag-box.drag-box
         {:style {:display (if (nil? drag-box) "none")
                  :width  (str width "px")
                  :height (str height "px")
                  :top    (str y "px")
                  :left   (str x "px")}}]))))
