App.js 13 KB

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