GithubHelp home page GithubHelp logo

re-dnd's Introduction

re-dnd

Introduction

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?

Usage

First add this to your project.clj dependencies

Clojars Project

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 http://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])

CSS classes

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;
}

More of the API, events and subscriptions

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])

Development Mode

Start Cider from Emacs:

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 ,".

Run the demo application, which depends on the library:

lein fig

Figwheel will automatically push cljs changes to the browser.

Wait a bit, then browse to http://localhost:9500/demo.html.

Production Build

To compile clojurescript to javascript:

lein build-dev

re-dnd's People

Contributors

kah0ona avatar njordhov avatar russmatney avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

re-dnd's Issues

drag-box element is not positioned correctly if inside a div with position absolute

If the dragbox is placed in the dom inside say a modal, it will render incorrectly placed.

However, simply adding the dragbox to the top of the DOM, ie. just after <body> triggers react re-renders all the time, making everything hang (on Firefox at least).

So the most robust way I think would be to calculate the absolute left/top positions (ie. from boundingRect) of the first parent whose position is relative, and subtract that from the current calculation.

Broken demo

Release 0.1.8 broke the demo, with the drag box not aligned with the mouse moves and failing to disappear on drop.

Test fails with ReferenceError

Running lein doo phantom test once fails with:

;; Testing with Phantom:
ReferenceError: Can't find variable: Map

Happens even after updating to the latest version of phantom by executing:

npm install phantomjs-prebuilt

Initialize with elements

I suggest the :dnd/initialize-drop-zone event handler takes an optional vector of drop zone elements, to simplify adding pre-existing elements to the zone instead of requiring repeat use of :dnd/add-drop-zone-element.

Default drag-handle

Thank you for developing this widget! I used your brand new release today to implement an orderable list.

I have a small suggestion: Provide a catch-all :default for e-dnd.views/drag-handle that covers the typical case. That way, implementing a custom drag-handle method may not be necessary.

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

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.