App.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437
  1. import React from 'react'
  2. import PropTypes from 'prop-types'
  3. import GridItem, { checkInContainer } from './GridItem'
  4. import './style.css'
  5. /**
  6. * 这个函数会有副作用,不是纯函数,会改变item的Gridx和GridY
  7. * @param {*} item
  8. */
  9. const correctItem = (item, col) => {
  10. const { GridX, GridY } = checkInContainer(item.GridX, item.GridY, col, item.w)
  11. item.GridX = GridX;
  12. item.GridY = GridY;
  13. }
  14. const correctLayout = (layout, col) => {
  15. var copy = [...layout];
  16. for (let i = 0; i < layout.length - 1; i++) {
  17. correctItem(copy[i], col)
  18. correctItem(copy[i + 1], col);
  19. if (collision(copy[i], copy[i + 1])) {
  20. copy = layoutCheck(copy, copy[i], copy[i].key, copy[i].key, undefined)
  21. }
  22. }
  23. return copy;
  24. }
  25. /**
  26. * 用key从layout中拿出item
  27. * @param {*} layout 输入进来的布局
  28. * @param {*} key
  29. */
  30. const layoutItemForkey = (layout, key) => {
  31. for (let i = 0, length = layout.length; i < length; i++) {
  32. if (key === layout[i].key) {
  33. return layout[i]
  34. }
  35. }
  36. }
  37. /**
  38. * 初始化的时候调用
  39. * 会把isUserMove和key一起映射到layout中
  40. * 不用用户设置
  41. * @param {*} layout
  42. * @param {*} children
  43. */
  44. const MapLayoutTostate = (layout, children) => {
  45. return layout.map((child, index) => {
  46. let newChild = { ...child, isUserMove: true, key: children[index].key }
  47. return newChild
  48. })
  49. }
  50. /**
  51. * 把用户移动的块,标记为true
  52. * @param {*} layout
  53. * @param {*} key
  54. * @param {*} GridX
  55. * @param {*} GridY
  56. * @param {*} isUserMove
  57. */
  58. const syncLayout = (layout, key, GridX, GridY, isUserMove) => {
  59. const newlayout = layout.map((item) => {
  60. if (item.key === key) {
  61. item.GridX = GridX
  62. item.GridY = GridY
  63. item.isUserMove = isUserMove
  64. return item
  65. }
  66. return item
  67. })
  68. return newlayout
  69. }
  70. const collision = (a, b) => {
  71. if (a.GridX === b.GridX && a.GridY === b.GridY &&
  72. a.w === b.w && a.h === b.h) {
  73. return true
  74. }
  75. if (a.GridX + a.w <= b.GridX) return false
  76. if (a.GridX >= b.GridX + b.w) return false
  77. if (a.GridY + a.h <= b.GridY) return false
  78. if (a.GridY >= b.GridY + b.h) return false
  79. return true
  80. }
  81. const sortLayout = (layout) => {
  82. return [].concat(layout).sort((a, b) => {
  83. if (a.GridY > b.GridY || (a.GridY === b.GridY && a.GridX > b.GridX)) {
  84. return 1
  85. } else if (a.GridY === b.GridY && a.GridX === b.GridX) {
  86. return 0
  87. }
  88. return -1
  89. })
  90. }
  91. /**获取layout中,item第一个碰撞到的物体 */
  92. const getFirstCollison = (layout, item) => {
  93. for (let i = 0, length = layout.length; i < length; i++) {
  94. if (collision(layout[i], item)) {
  95. return layout[i]
  96. }
  97. }
  98. return null
  99. }
  100. /**
  101. * 压缩单个元素,使得每一个元素都会紧挨着边界或者相邻的元素
  102. * @param {*} finishedLayout 压缩完的元素会放进这里来,用来对比之后的每一个元素是否需要压缩
  103. * @param {*} item
  104. */
  105. const compactItem = (finishedLayout, item) => {
  106. const newItem = { ...item }
  107. if (finishedLayout.length === 0) {
  108. return { ...newItem, GridY: 0 }
  109. }
  110. /**
  111. * 类似一个递归调用
  112. */
  113. while (true) {
  114. let FirstCollison = getFirstCollison(finishedLayout, newItem)
  115. if (FirstCollison) {
  116. /**第一次发生碰撞时,就可以返回了 */
  117. newItem.GridY = FirstCollison.GridY + FirstCollison.h
  118. return newItem
  119. }
  120. newItem.GridY--
  121. if (newItem.GridY < 0) return { ...newItem, GridY: 0 }/**碰到边界的时候,返回 */
  122. }
  123. return newItem
  124. }
  125. /**
  126. * 压缩layout,使得每一个元素都会紧挨着边界或者相邻的元素
  127. * @param {*} layout
  128. */
  129. const compactLayout = (layout) => {
  130. let sorted = sortLayout(layout)
  131. const needCompact = Array(layout.length)
  132. const compareList = []
  133. for (let i = 0, length = sorted.length; i < length; i++) {
  134. let finished = compactItem(compareList, sorted[i])
  135. finished.isUserMove = false
  136. compareList.push(finished)
  137. needCompact[i] = finished//用于输出从小到大的位置
  138. }
  139. return needCompact
  140. }
  141. const layoutCheck = (layout, layoutItem, key, fristItemkey, moving) => {
  142. let i = [], movedItem = []/**收集所有移动过的物体 */
  143. let newlayout = layout.map((item, idx) => {
  144. if (item.key !== key) {
  145. if (collision(item, layoutItem)) {
  146. i.push(item.key)
  147. /**
  148. * 这里就是奇迹发生的地方,如果向上移动,那么必须注意的是
  149. * 一格一格的移动,而不是一次性移动
  150. */
  151. let offsetY = item.GridY + 1
  152. /**这一行也非常关键,当向上移动的时候,碰撞的元素必须固定 */
  153. // if (moving < 0 && layoutItem.GridY > 0) offsetY = item.GridY
  154. if (layoutItem.GridY > item.GridY && layoutItem.GridY < item.GridY + item.h) {
  155. /**
  156. * 元素向上移动时,元素的上面空间不足,则不移动这个元素
  157. * 当元素移动到GridY>所要向上交换的元素时,就不会进入这里,直接交换元素
  158. *
  159. */
  160. offsetY = item.GridY
  161. }
  162. /**
  163. * 物体向下移动的时候
  164. */
  165. if (moving > 0) {
  166. if (layoutItem.GridY + layoutItem.h < item.GridY) {
  167. let collision;
  168. let copy = { ...item }
  169. while (true) {
  170. let newLayout = layout.filter((item) => {
  171. if (item.key !== key && (item.key !== copy.key)) {
  172. return item
  173. }
  174. })
  175. collision = getFirstCollison(newLayout, copy)
  176. if (collision) {
  177. offsetY = collision.GridY + collision.h
  178. break
  179. } else {
  180. copy.GridY--
  181. }
  182. if (copy.GridY < 0) {
  183. offsetY = 0
  184. break
  185. }
  186. }
  187. }
  188. }
  189. movedItem.push({ ...item, GridY: offsetY, isUserMove: false })
  190. return { ...item, GridY: offsetY, isUserMove: false }
  191. }
  192. } else if (fristItemkey === key) {
  193. /**永远保持用户移动的块是 isUserMove === true */
  194. return { ...item, GridX: layoutItem.GridX, GridY: layoutItem.GridY, isUserMove: true }
  195. }
  196. return item
  197. })
  198. /** 递归调用,将layout中的所有重叠元素全部移动 */
  199. const length = movedItem.length;
  200. for (let c = 0; c < length; c++) {
  201. newlayout = layoutCheck(newlayout, movedItem[c], i[c], fristItemkey, undefined)
  202. }
  203. return newlayout
  204. }
  205. function quickSort(a) {
  206. return a.length <= 1 ? a : quickSort(a.slice(1).filter(item => item <= a[0])).concat(a[0], quickSort(a.slice(1).filter(item => item > a[0])));
  207. }
  208. const getMaxContainerHeight = (layout, elementHeight = 30, elementMarginBottom = 10) => {
  209. const ar = layout.map((item) => {
  210. return item.GridY + item.h
  211. })
  212. const h = quickSort(ar)[ar.length - 1];
  213. const height = h * (elementHeight + elementMarginBottom) + elementMarginBottom
  214. return height
  215. }
  216. const getDataSet = (children) => {
  217. return children.map((child) => {
  218. return { ...child.props['data-set'], isUserMove: true, key: child.key }
  219. })
  220. }
  221. const stringJoin = (source, join) => {
  222. return source + (join ? ` ${join}` : '')
  223. }
  224. class DraggerLayout extends React.Component {
  225. constructor(props) {
  226. super(props)
  227. this.onDrag = this.onDrag.bind(this)
  228. this.onDragStart = this.onDragStart.bind(this)
  229. this.onDragEnd = this.onDragEnd.bind(this)
  230. const layout = props.layout ?
  231. MapLayoutTostate(props.layout, props.children)
  232. :
  233. getDataSet(props.children);
  234. this.state = {
  235. GridXMoving: 0,
  236. GridYMoving: 0,
  237. wMoving: 0,
  238. hMoving: 0,
  239. placeholderShow: false,
  240. placeholderMoving: false,
  241. layout: layout,
  242. containerHeight: 500
  243. }
  244. }
  245. static PropTypes = {
  246. /**外部属性 */
  247. layout: PropTypes.array,
  248. col: PropTypes.number,
  249. width: PropTypes.number,
  250. /**每个元素的最小高度 */
  251. rowHeight: PropTypes.number,
  252. padding: PropTypes.number,
  253. }
  254. onDragStart(bundles) {
  255. const { GridX, GridY, w, h, UniqueKey } = bundles
  256. const newlayout = syncLayout(this.state.layout, UniqueKey, GridX, GridY, true)
  257. console.log(newlayout)
  258. this.setState({
  259. GridXMoving: GridX,
  260. GridYMoving: GridY,
  261. wMoving: w,
  262. hMoving: h,
  263. placeholderShow: true,
  264. placeholderMoving: true,
  265. layout: newlayout,
  266. })
  267. this.props.onDragStart && this.props.onDragStart({ GridX, GridY })
  268. }
  269. onDrag(layoutItem, key) {
  270. const { GridX, GridY } = layoutItem
  271. const moving = GridY - this.state.GridYMoving
  272. const newLayout = layoutCheck(this.state.layout, layoutItem, key, key/*用户移动方块的key */, moving)
  273. const compactedLayout = compactLayout(newLayout)
  274. for (let i = 0; i < compactedLayout.length; i++) {
  275. if (key === compactedLayout[i].key) {
  276. /**
  277. * 特殊点:当我们移动元素的时候,元素在layout中的位置不断改变
  278. * 但是当isUserMove=true的时候,鼠标拖拽的元素不会随着位图变化而变化
  279. * 但是实际layout中的位置还是会改变
  280. * (isUserMove=true用于解除placeholder和元素的绑定)
  281. */
  282. compactedLayout[i].isUserMove = true
  283. layoutItem.GridX = compactedLayout[i].GridX
  284. layoutItem.GridY = compactedLayout[i].GridY
  285. break
  286. }
  287. }
  288. this.setState({
  289. GridXMoving: layoutItem.GridX,
  290. GridYMoving: layoutItem.GridY,
  291. layout: compactedLayout,
  292. containerHeight: getMaxContainerHeight(compactedLayout, this.props.rowHeight, this.props.margin[1])
  293. })
  294. this.props.onDrag && this.props.onDrag({ GridX, GridY });
  295. }
  296. onDragEnd(key) {
  297. const compactedLayout = compactLayout(this.state.layout)
  298. this.setState({
  299. placeholderShow: false,
  300. layout: compactedLayout,
  301. containerHeight: getMaxContainerHeight(compactedLayout, this.props.rowHeight, this.props.margin[1])
  302. })
  303. this.props.onDragEnd && this.props.onDragEnd();
  304. }
  305. renderPlaceholder() {
  306. if (!this.state.placeholderShow) return null
  307. const { col, width, padding, rowHeight, margin } = this.props
  308. const { GridXMoving, GridYMoving, wMoving, hMoving, placeholderMoving } = this.state
  309. return (
  310. <GridItem
  311. margin={margin}
  312. col={col}
  313. containerWidth={width}
  314. containerPadding={padding}
  315. rowHeight={rowHeight}
  316. GridX={GridXMoving}
  317. GridY={GridYMoving}
  318. w={wMoving}
  319. h={hMoving}
  320. style={{ background: '#a31', zIndex: -1, transition: ' all .15s' }}
  321. isUserMove={!placeholderMoving}
  322. >
  323. </GridItem >
  324. )
  325. }
  326. componentDidMount() {
  327. setTimeout(() => {
  328. let layout = correctLayout(this.state.layout, this.props.col)
  329. const compacted = compactLayout(layout);
  330. this.setState({
  331. layout: compacted,
  332. containerHeight: getMaxContainerHeight(compacted, this.props.rowHeight, this.props.margin[1])
  333. })
  334. }, 1);
  335. }
  336. getGridItem(child, index) {
  337. const { layout } = this.state
  338. const { col, width, padding, rowHeight, margin } = this.props
  339. const renderItem = layoutItemForkey(layout, child.key)
  340. return (
  341. <GridItem
  342. margin={margin}
  343. col={col}
  344. containerWidth={width}
  345. containerPadding={padding}
  346. rowHeight={rowHeight}
  347. GridX={renderItem.GridX}
  348. GridY={renderItem.GridY}
  349. w={renderItem.w}
  350. h={renderItem.h}
  351. onDrag={this.onDrag}
  352. onDragStart={this.onDragStart}
  353. onDragEnd={this.onDragEnd}
  354. index={index}
  355. isUserMove={renderItem.isUserMove}
  356. style={{ background: '#329' }}
  357. UniqueKey={child.key}
  358. >
  359. {child}
  360. </GridItem >
  361. )
  362. }
  363. render() {
  364. const { layout, col, width, padding, rowHeight, className } = this.props;
  365. const { containerHeight } = this.state;
  366. return (
  367. <div
  368. className={stringJoin('DraggerLayout', className)}
  369. style={{ left: 100, width: width, height: containerHeight, border: '1px solid black' }}
  370. >
  371. {React.Children.map(this.props.children,
  372. (child, index) => this.getGridItem(child, index)
  373. )}
  374. {this.renderPlaceholder()}
  375. </div>
  376. )
  377. }
  378. }
  379. export default class LayoutDemo extends React.Component {
  380. render() {
  381. return (
  382. <DraggerLayout width={800} col={4} rowHeight={800 / 12} margin={[5, 5]}>
  383. {['我', '叫', '做', '方', '正'].map((el, index) => {
  384. return (<div key={index} data-set={{ GridX: index*3, GridY: index*2, w: 1, h: 2 }}>{el}</div>)
  385. })}
  386. </DraggerLayout>
  387. )
  388. }
  389. }