├── Makefile ├── README.md ├── css └── app.css ├── index.html ├── package.json └── src ├── app.jsx └── quotes.js /Makefile: -------------------------------------------------------------------------------- 1 | 2 | .PHONY: js bundle clean server 3 | .DEFAULT_GOAL := bundle 4 | 5 | BUILD=babel --optional runtime --stage 2 src --out-dir build 6 | BUILD_CSS=postcss --use autoprefixer css/* -d build 7 | 8 | BUNDLE_ARGS=./build/app.js -t babelify -o build/bundle.js --verbose --debug 9 | 10 | css: build/app.css 11 | 12 | js: build/app.js 13 | 14 | bundle: build/bundle.js build/app.css 15 | 16 | watch: build 17 | ($(BUILD) --watch & watchify $(BUNDLE_ARGS) & $(BUILD_CSS) --watch & wait) 18 | 19 | clean: 20 | rm -r build 21 | 22 | server: 23 | browser-sync start --server --files="build/bundle.js, build/app.css" 24 | 25 | # ts: 26 | # tsc --watch --experimentalAsyncFunctions --target ES6 --jsx react app.tsx --outDir build 27 | 28 | 29 | build: 30 | mkdir -p build 31 | 32 | build/app.js: build 33 | $(BUILD) 34 | 35 | build/bundle.js: build/app.js 36 | browserify $(BUNDLE_ARGS) 37 | 38 | build/app.css: build 39 | $(BUILD_CSS) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [Sortable List Demo](https://hayeah.github.io/react-motion-SortableList) 2 | 3 | # Sortable List 4 | 5 | (proof-of-concept!) 6 | 7 | This is a generalization of the react-motion sortable list demo. It supports arbitrary number of items, and each item can have different heights. 8 | 9 | # API 10 | 11 | ``` 12 | 13 | {(key) => ... } 14 | 15 | ``` 16 | 17 | Where data is a map of string to data (i.e. `{[string]: any}`). The data items can be polymorphic. 18 | 19 | Like react-motion, we use the insertion order of the keys to determine the items ordering. 20 | 21 | # How it works 22 | 23 | The sortable list tracks the height of its children, and lay them out vertically one after another. 24 | 25 | Since we know the dimensions and locations of all children, it's easy to animate them using react-motion whenever the order of the children changes. 26 | 27 | On drag, we look at the mouse position and iterates through the list to find an insertion point. Once we know the new ordering, the same code that does the layout animates everything into place. 28 | 29 | Ditto with shuffling items. 30 | 31 | # TODO 32 | 33 | + Can add and remove items. 34 | + Trello-esque scrolling for long list of items. 35 | + React-Native port. 36 | 37 | # Dev Guide 38 | 39 | `npm install` to install dependencies. 40 | 41 | `make` or `make bundle` to build the project. Open index.html. 42 | 43 | `make watch` to continuously build the project. 44 | 45 | `make server` to launch a livereload server. 46 | -------------------------------------------------------------------------------- /css/app.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 0; 3 | margin: 0; 4 | } 5 | 6 | body { 7 | font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif; 8 | font-weight: 300; 9 | color: #4D4F7D; 10 | } 11 | 12 | * { 13 | box-sizing: border-box; 14 | } 15 | 16 | a { 17 | color: #4D4F7D; 18 | text-decoration: underline; 19 | } 20 | 21 | a:hover { 22 | color: #A8A9D1; 23 | } 24 | 25 | .container { 26 | display: flex; 27 | flex-direction: column; 28 | justify-content: center; 29 | align-items: center; 30 | 31 | position: absolute; 32 | top: 0; 33 | left: 0; 34 | right: 0; 35 | bottom: 0; 36 | 37 | background-color: #000445; 38 | } 39 | 40 | .sortable-list { 41 | position: relative; 42 | list-style: none; 43 | padding: 0; 44 | margin: 0; 45 | width: 300px; 46 | } 47 | 48 | .sortable-list li { 49 | width: 100%; 50 | padding: 10px; 51 | margin-bottom: 10px; 52 | border: 1px solid #B9BBFF; 53 | color: #B9BBFF; 54 | } 55 | 56 | h1.shuffle-button { 57 | /*background: rgba(0,0,0,0.5);*/ 58 | padding: 5px 20px; 59 | border: 2px solid #B9BBFF; 60 | color: #B9BBFF; 61 | border-radius: 5px; 62 | } 63 | 64 | h1.shuffle-button:hover { 65 | color: #A8A9D1; 66 | border: 2px solid #A8A9D1; 67 | } 68 | 69 | .colophon { 70 | font-size: 14px; 71 | } 72 | 73 | .colophon a { 74 | font-size: 14px; 75 | margin-right: 0.5em; 76 | } 77 | 78 | .noselect { 79 | -webkit-touch-callout: none; 80 | user-select: none; 81 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | React Motion Sortable List 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "try-react-motion", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "babel-runtime": "^5.8.20", 13 | "lodash": "^3.10.1", 14 | "react": "^0.13.3", 15 | "react-motion": "^0.2.7" 16 | }, 17 | "devDependencies": { 18 | "autoprefixer": "^5.2.0", 19 | "babelify": "^6.1.3", 20 | "browserify": "^11.0.1", 21 | "postcss-cli": "^1.5.0", 22 | "watchify": "^3.3.1" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/app.jsx: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import React from "react/addons"; 4 | import {TransitionSpring,Spring,utils as RMutils} from "react-motion"; 5 | 6 | import _ from "lodash"; 7 | import {pick} from "lodash"; 8 | 9 | const {reorderKeys} = RMutils; 10 | 11 | const {update} = React.addons; 12 | 13 | const QUOTES = require("./quotes"); 14 | 15 | 16 | let Item = React.createClass({ 17 | componentDidMount() { 18 | const dom = this.refs.li.getDOMNode(); 19 | const layout = pick(dom,"offsetWidth","offsetHeight"); 20 | const {id,key,onLayout} = this.props; 21 | onLayout && onLayout(id,layout); 22 | }, 23 | 24 | render() { 25 | const {children,style,onMouseDown} = this.props; 26 | 27 | return ( 28 |
  • {children}
  • 29 | ); 30 | }, 31 | 32 | }); 33 | 34 | let List = React.createClass({ 35 | getInitialState() { 36 | const {items} = this.props; 37 | return { 38 | layouts: {}, 39 | items, 40 | // The key of the current item we are moving. 41 | movingItemKey: null, 42 | movingY: null, 43 | }; 44 | }, 45 | 46 | componentWillReceiveProps(props) { 47 | const {items} = props; 48 | this.setState({items}); 49 | }, 50 | 51 | handleItemLayout(key,layout) { 52 | const {layouts} = this.state; 53 | this.setState(({layouts}) => { 54 | return { 55 | layouts: { 56 | ...layouts, 57 | [key]: layout, 58 | } 59 | }; 60 | }); 61 | }, 62 | 63 | componentDidMount() { 64 | window.addEventListener("mousemove",this.handleMouseMove); 65 | window.addEventListener("mouseup",this.handleMouseUp); 66 | }, 67 | 68 | handleMouseMove(e) { 69 | const {movingItemKey} = this.state; 70 | if(movingItemKey == null) { 71 | return; 72 | } 73 | 74 | let y = this.distanceFromListTop(e.pageY); 75 | 76 | // 1. search through items to find where to insert 77 | const {items,layouts} = this.state; 78 | 79 | let curHeight = 0; 80 | let marginBottom = 10; 81 | 82 | let rowKey; 83 | let keys = Object.keys(items); 84 | for(let i = 0; i < keys.length; i++) { 85 | const key = keys[i]; 86 | const layout = layouts[key]; 87 | curHeight = curHeight + layout.offsetHeight + marginBottom; 88 | if(y < curHeight) { 89 | rowKey = key; 90 | break; 91 | } 92 | } 93 | // Cursor is outside the last item. Use the last item's key. 94 | if(rowKey == null) { 95 | rowKey = keys[keys.length-1]; 96 | } 97 | 98 | this.setState({movingY: y}); 99 | 100 | // 2. swap items if necessary 101 | if(rowKey !== movingItemKey) { 102 | this.setState({ 103 | items: reorderKeys(this.state.items,keys => { 104 | let a, b; 105 | keys.forEach((key,i) => { 106 | // console.log("compare key",key,rowKey,movingItemKey,i); 107 | if(key == rowKey) { 108 | a = i 109 | } 110 | 111 | if(key == movingItemKey) { 112 | b = i 113 | } 114 | }); 115 | 116 | const tmp = keys[a]; 117 | keys[a] = keys[b]; 118 | keys[b] = tmp; 119 | return keys; 120 | }), 121 | }); 122 | 123 | } 124 | }, 125 | 126 | distanceFromListTop(pageY) { 127 | const listDom = this.refs.list.getDOMNode(); 128 | const y = pageY - listDom.offsetTop; 129 | return y; 130 | }, 131 | 132 | handleMouseUp() { 133 | this.setState({ 134 | movingItemKey: null, 135 | movingY: null, 136 | }); 137 | }, 138 | 139 | handleMousedownOnItem(key,{pageY}) { 140 | let y = this.distanceFromListTop(pageY); 141 | this.setState({ 142 | movingItemKey: key, 143 | movingY: y, 144 | }); 145 | }, 146 | 147 | render() { 148 | 149 | const dataRenderer = this.props.children; 150 | if(typeof dataRenderer != 'function') { 151 | throw "must be a function" 152 | } 153 | 154 | const {items,movingItemKey,movingY} = this.state; 155 | 156 | // calculate positions using layout dimensions. 157 | let curHeight = 0; 158 | let marginBottom = 10; 159 | const children = Object.keys(items).map((key) => { 160 | const item = items[key]; 161 | 162 | let layout = this.state.layouts[key]; 163 | 164 | let style; 165 | if(layout) { 166 | style = { 167 | position: 'absolute', 168 | top: {val: curHeight}, 169 | scale: {val: 1}, 170 | opacity: 1, 171 | } 172 | 173 | curHeight = curHeight + layout.offsetHeight + marginBottom; 174 | } else { 175 | style = { 176 | position: 'absolute', 177 | top: {val: 0}, 178 | scale: {val: 1}, 179 | opacity: 1, 180 | } 181 | } 182 | 183 | const isSelected = movingItemKey === key; 184 | 185 | if(isSelected) { 186 | style = { 187 | ...style, 188 | scale: {val: 1.1}, 189 | backgroundColor: '#33366A', 190 | top: { 191 | val: movingY - layout.offsetHeight/2, 192 | config: [] 193 | }, 194 | } 195 | } 196 | 197 | return ( 198 | 203 | { 204 | ({top,scale,backgroundColor}) => { 205 | let style = { 206 | position: 'absolute', 207 | top: 0, 208 | backgroundColor: backgroundColor, 209 | transform: `translate3d(0,${top.val}px,0) scale(${scale.val})`, 210 | // < Safari 8 211 | '-webkit-transform': `translate3d(0,${top.val}px,0) scale(${scale.val})`, 212 | zIndex: isSelected ? 99 : 0, 213 | } 214 | return ( 215 | 219 | {dataRenderer(item)} 220 | 221 | ); 222 | } 223 | } 224 | 225 | 226 | ) 227 | }); 228 | 229 | const contentHeight = curHeight; 230 | 231 | // console.log(items,children); 232 | 233 | return ( 234 | 240 | 241 | ); 242 | } 243 | }); 244 | 245 | const App = React.createClass({ 246 | shuffle() { 247 | console.log("shuffle yo!"); 248 | const {items} = this.state; 249 | 250 | this.setState({ 251 | items: reorderKeys(items,keys => { 252 | var keys2= shuffle(keys); 253 | console.log(keys2); 254 | return keys 255 | }), 256 | }); 257 | }, 258 | 259 | getInitialState() { 260 | let items = {}; 261 | 262 | QUOTES.slice(0,8).forEach((quote,i) => { 263 | // Javascript hash preserves insertion order except for "numeric" keys. 264 | // Add a random prefix to avoid that. 265 | items[`@${i}`] = quote; 266 | }); 267 | 268 | return { 269 | items: items, 270 | } 271 | }, 272 | 273 | render() { 274 | const {items} = this.state; 275 | // console.log(items); 276 | return ( 277 |
    278 |

    React Motion Sortable List

    279 | 280 | {item => {item}} 281 | 282 | 283 |

    Shuffle

    284 | 285 |

    286 | made with react motion. 287 | source. 288 | by @hayeah 289 |

    290 |
    291 | ); 292 | }, 293 | }); 294 | 295 | function shuffle(array) { 296 | var currentIndex = array.length, temporaryValue, randomIndex ; 297 | 298 | // While there remain elements to shuffle... 299 | while (0 !== currentIndex) { 300 | 301 | // Pick a remaining element... 302 | randomIndex = Math.floor(Math.random() * currentIndex); 303 | currentIndex -= 1; 304 | 305 | // And swap it with the current element. 306 | temporaryValue = array[currentIndex]; 307 | array[currentIndex] = array[randomIndex]; 308 | array[randomIndex] = temporaryValue; 309 | } 310 | 311 | return array; 312 | } 313 | 314 | 315 | function isAlphaNumeric(keyCode) { 316 | return (48 <= keyCode && keyCode <= 57) || (65 <= keyCode && keyCode <= 90); 317 | } 318 | 319 | 320 | 321 | React.render(, document.querySelector('#content')); -------------------------------------------------------------------------------- /src/quotes.js: -------------------------------------------------------------------------------- 1 | const quotes = ["I think that God, in creating man, somewhat overestimated his ability.", 2 | "The world is a stage, but the play is badly cast.", 3 | "Always forgive your enemies; nothing annoys them so much.", 4 | "It is absurd to divide people into good and bad. People are either charming or tedious.", 5 | "The only thing to do with good advice is pass it on. It is never any use to oneself.", 6 | "Some cause happiness wherever they go; others whenever they go.", 7 | "What is a cynic? A man who knows the price of everything and the value of nothing.", 8 | "A little sincerity is a dangerous thing, and a great deal of it is absolutely fatal.", 9 | "When I was young I thought that money was the most important thing in life; now that I am old I know that it is.", 10 | "There are only two tragedies in life: one is not getting what one wants, and the other is getting it.", 11 | "Work is the curse of the drinking classes.", 12 | "Anyone who lives within their means suffers from a lack of imagination.", 13 | "True friends stab you in the front.", 14 | "All women become like their mothers. That is their tragedy. No man does. That's his.", 15 | "Fashion is a form of ugliness so intolerable that we have to alter it every six months.", 16 | "There is only one thing in life worse than being talked about, and that is not being talked about.", 17 | "Genius is born—not paid.", 18 | "Morality is simply the attitude we adopt towards people whom we personally dislike.", 19 | "How can a woman be expected to be happy with a man who insists on treating her as if she were a perfectly normal human being?", 20 | "A gentleman is one who never hurts anyone’s feelings unintentionally.", 21 | "My own business always bores me to death; I prefer other people’s.", 22 | "The old believe everything, the middle-aged suspect everything, the young know everything.", 23 | "I like men who have a future and women who have a past.", 24 | "There are two ways of disliking poetry; one way is to dislike it, the other is to read Pope.", 25 | "Quotation is a serviceable substitute for wit."] 26 | 27 | export default quotes; --------------------------------------------------------------------------------