App.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408
  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 sortLayout(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. const getMaxContainerHeight = (layout, elementHeight) => {
  195. const height = (layout[layout.length - 1].GridY + layout[layout.length - 1].h) * (30 + 10) + 10
  196. return height
  197. }
  198. class DraggerLayout extends React.Component {
  199. constructor(props) {
  200. super(props)
  201. this.onDrag = this.onDrag.bind(this)
  202. this.onDragStart = this.onDragStart.bind(this)
  203. this.onDragEnd = this.onDragEnd.bind(this)
  204. }
  205. static PropTypes = {
  206. /**外部属性 */
  207. layout: PropTypes.array,
  208. col: PropTypes.number,
  209. width: PropTypes.number,
  210. /**每个元素的最小高度 */
  211. rowHeight: PropTypes.number,
  212. padding: PropTypes.number,
  213. }
  214. state = {
  215. GridXMoving: 0,
  216. GridYMoving: 0,
  217. wMoving: 0,
  218. hMoving: 0,
  219. placeholderShow: false,
  220. placeholderMoving: false,
  221. layout: MapLayoutTostate(this.props.layout, this.props.children),
  222. containerHeight: 500
  223. }
  224. onDragStart(bundles) {
  225. const { GridX, GridY, w, h, UniqueKey } = bundles
  226. const newlayout = syncLayout(this.state.layout, UniqueKey, GridX, GridY, true)
  227. this.setState({
  228. GridXMoving: GridX,
  229. GridYMoving: GridY,
  230. wMoving: w,
  231. hMoving: h,
  232. placeholderShow: true,
  233. placeholderMoving: true,
  234. layout: newlayout,
  235. })
  236. }
  237. onDrag(layoutItem, key) {
  238. const { GridX, GridY } = layoutItem
  239. const moving = GridY - this.state.GridYMoving
  240. const newLayout = layoutCheck(this.state.layout, layoutItem, key, key/*用户移动方块的key */, moving)
  241. const compactedLayout = compactLayout(newLayout)
  242. for (let i = 0; i < compactedLayout.length; i++) {
  243. if (key === compactedLayout[i].key) {
  244. /**
  245. * 特殊点:当我们移动元素的时候,元素在layout中的位置不断改变
  246. * 但是当isUserMove=true的时候,鼠标拖拽的元素不会随着位图变化而变化
  247. * 但是实际layout中的位置还是会改变
  248. * (isUserMove=true用于解除placeholder和元素的绑定)
  249. */
  250. compactedLayout[i].isUserMove = true
  251. layoutItem.GridX = compactedLayout[i].GridX
  252. layoutItem.GridY = compactedLayout[i].GridY
  253. break
  254. }
  255. }
  256. this.setState({
  257. GridXMoving: layoutItem.GridX,
  258. GridYMoving: layoutItem.GridY,
  259. layout: compactedLayout,
  260. containerHeight: getMaxContainerHeight(compactedLayout)
  261. })
  262. }
  263. onDragEnd(key) {
  264. const compactedLayout = compactLayout(this.state.layout)
  265. this.setState({
  266. placeholderShow: false,
  267. layout: compactedLayout,
  268. containerHeight: getMaxContainerHeight(compactedLayout)
  269. })
  270. }
  271. placeholder() {
  272. if (!this.state.placeholderShow) return null
  273. const { col, width, padding, rowHeight } = this.props
  274. const { GridXMoving, GridYMoving, wMoving, hMoving, placeholderMoving } = this.state
  275. return (
  276. <GridItem
  277. col={col}
  278. containerWidth={width}
  279. containerPadding={padding}
  280. rowHeight={rowHeight}
  281. GridX={GridXMoving}
  282. GridY={GridYMoving}
  283. w={wMoving}
  284. h={hMoving}
  285. style={{ background: '#a31', zIndex: -1, transition: ' all .15s' }}
  286. isUserMove={!placeholderMoving}
  287. >
  288. </GridItem >
  289. )
  290. }
  291. componentDidMount() {
  292. let that = this
  293. setTimeout(function () {
  294. let layout = correctLayout(that.state.layout)
  295. const compacted = compactLayout(layout);
  296. that.setState({
  297. layout: compacted,
  298. containerHeight: getMaxContainerHeight(compacted)
  299. })
  300. }, 1);
  301. }
  302. getGridItem(child, index) {
  303. const { layout } = this.state
  304. const { col, width, padding, rowHeight } = this.props
  305. const renderItem = layoutItemForkey(layout, child.key)
  306. return (
  307. <GridItem
  308. col={col}
  309. containerWidth={width}
  310. containerPadding={padding}
  311. rowHeight={rowHeight}
  312. GridX={renderItem.GridX}
  313. GridY={renderItem.GridY}
  314. w={renderItem.w}
  315. h={renderItem.h}
  316. onDrag={this.onDrag}
  317. onDragStart={this.onDragStart}
  318. onDragEnd={this.onDragEnd}
  319. index={index}
  320. isUserMove={renderItem.isUserMove}
  321. style={{ background: '#329' }}
  322. UniqueKey={child.key}
  323. >
  324. {child}
  325. </GridItem >
  326. )
  327. }
  328. render() {
  329. const { layout, col, width, padding, rowHeight } = this.props
  330. return (
  331. <div
  332. className='DraggerLayout'
  333. style={{ left: 100, width: this.props.width, height: this.state.containerHeight, border: '1px solid black' }}
  334. >
  335. {React.Children.map(this.props.children,
  336. (child, index) => this.getGridItem(child, index)
  337. )}
  338. {this.placeholder()}
  339. </div>
  340. )
  341. }
  342. }
  343. export default class LayoutDemo extends React.Component {
  344. render() {
  345. const layout = [{
  346. GridX: 0, GridY: 0, w: 5, h: 5
  347. }, {
  348. GridX: 0, GridY: 0, w: 3, h: 3
  349. }, {
  350. GridX: 0, GridY: 0, w: 3, h: 3
  351. }, {
  352. GridX: 0, GridY: 0, w: 3, h: 3
  353. }, {
  354. GridX: 3, GridY: 8, w: 3, h: 3
  355. }, {
  356. GridX: 3, GridY: 8, w: 3, h: 3
  357. }, {
  358. GridX: 3, GridY: 8, w: 3, h: 3
  359. }, {
  360. GridX: 3, GridY: 8, w: 3, h: 3
  361. }]
  362. return (
  363. <DraggerLayout layout={layout} width={document.body.clientWidth} col={12}>
  364. {layout.map((el, index) => {
  365. return (<div key={index} >{index}</div>)
  366. })}
  367. </DraggerLayout>
  368. )
  369. }
  370. }