08aacd83fdb7750f36159d2140b7aedef8f2d7499064e8b48972760a66d5bf65dc99b8709b821dcc1751618085fecf4c29b659ec550daf838e41935486ca4e 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782
  1. <script>
  2. export default {
  3. name: 'splitpanes',
  4. props: {
  5. horizontal: { type: Boolean },
  6. pushOtherPanes: { type: Boolean, default: true },
  7. dblClickSplitter: { type: Boolean, default: true },
  8. rtl: { type: Boolean, default: false }, // Right to left direction.
  9. firstSplitter: { type: Boolean }
  10. },
  11. provide () {
  12. return {
  13. requestUpdate: this.requestUpdate,
  14. onPaneAdd: this.onPaneAdd,
  15. onPaneRemove: this.onPaneRemove,
  16. onPaneClick: this.onPaneClick
  17. }
  18. },
  19. data: () => ({
  20. container: null,
  21. ready: false,
  22. panes: [],
  23. touch: {
  24. mouseDown: false,
  25. dragging: false,
  26. activeSplitter: null
  27. },
  28. splitterTaps: { // Used to detect double click on touch devices.
  29. splitter: null,
  30. timeoutId: null
  31. }
  32. }),
  33. computed: {
  34. panesCount () {
  35. return this.panes.length
  36. },
  37. // Indexed panes by `_uid` of Pane components for fast lookup.
  38. // Every time a pane is destroyed this index is recomputed.
  39. indexedPanes () {
  40. return this.panes.reduce((obj, pane) => (obj[pane.id] = pane) && obj, {})
  41. }
  42. },
  43. methods: {
  44. updatePaneComponents () {
  45. // On update refresh the size of each pane through the registered `update` method (in onPaneAdd).
  46. this.panes.forEach(pane => {
  47. pane.update && pane.update({
  48. // Panes are indexed by Pane component uid, as they might be inserted at different index.
  49. [this.horizontal ? 'height' : 'width']: `${this.indexedPanes[pane.id].size}%`
  50. })
  51. })
  52. },
  53. bindEvents () {
  54. document.addEventListener('mousemove', this.onMouseMove, { passive: false })
  55. document.addEventListener('mouseup', this.onMouseUp)
  56. // Passive: false to prevent scrolling while touch dragging.
  57. if ('ontouchstart' in window) {
  58. document.addEventListener('touchmove', this.onMouseMove, { passive: false })
  59. document.addEventListener('touchend', this.onMouseUp)
  60. }
  61. },
  62. unbindEvents () {
  63. document.removeEventListener('mousemove', this.onMouseMove, { passive: false })
  64. document.removeEventListener('mouseup', this.onMouseUp)
  65. if ('ontouchstart' in window) {
  66. document.removeEventListener('touchmove', this.onMouseMove, { passive: false })
  67. document.removeEventListener('touchend', this.onMouseUp)
  68. }
  69. },
  70. onMouseDown (event, splitterIndex) {
  71. this.bindEvents()
  72. this.touch.mouseDown = true
  73. this.touch.activeSplitter = splitterIndex
  74. },
  75. onMouseMove (event) {
  76. if (this.touch.mouseDown) {
  77. // Prevent scrolling while touch dragging (only works with an active event, eg. passive: false).
  78. event.preventDefault()
  79. this.touch.dragging = true
  80. this.calculatePanesSize(this.getCurrentMouseDrag(event))
  81. this.$emit('resize', this.panes.map(pane => ({ min: pane.min, max: pane.max, size: pane.size })))
  82. }
  83. },
  84. onMouseUp () {
  85. if (this.touch.dragging) {
  86. this.$emit('resized', this.panes.map(pane => ({ min: pane.min, max: pane.max, size: pane.size })))
  87. }
  88. this.touch.mouseDown = false
  89. // Keep dragging flag until click event is finished (click happens immediately after mouseup)
  90. // in order to prevent emitting `splitter-click` event if splitter was dragged.
  91. setTimeout(() => {
  92. this.touch.dragging = false
  93. this.unbindEvents()
  94. }, 100)
  95. },
  96. // If touch device, detect double tap manually (2 taps separated by less than 500ms).
  97. onSplitterClick (event, splitterIndex) {
  98. if ('ontouchstart' in window) {
  99. event.preventDefault()
  100. // Detect splitter double taps if the option is on.
  101. if (this.dblClickSplitter) {
  102. if (this.splitterTaps.splitter === splitterIndex) {
  103. clearTimeout(this.splitterTaps.timeoutId)
  104. this.splitterTaps.timeoutId = null
  105. this.onSplitterDblClick(event, splitterIndex)
  106. this.splitterTaps.splitter = null // Reset for the next tap check.
  107. }
  108. else {
  109. this.splitterTaps.splitter = splitterIndex
  110. this.splitterTaps.timeoutId = setTimeout(() => {
  111. this.splitterTaps.splitter = null
  112. }, 500)
  113. }
  114. }
  115. }
  116. if (!this.touch.dragging) this.$emit('splitter-click', this.panes[splitterIndex])
  117. },
  118. // On splitter dbl click or dbl tap maximize this pane.
  119. onSplitterDblClick (event, splitterIndex) {
  120. let totalMinSizes = 0
  121. this.panes = this.panes.map((pane, i) => {
  122. pane.size = i === splitterIndex ? pane.max : pane.min
  123. if (i !== splitterIndex) totalMinSizes += pane.min
  124. return pane
  125. })
  126. this.panes[splitterIndex].size -= totalMinSizes
  127. this.$emit('pane-maximize', this.panes[splitterIndex])
  128. },
  129. onPaneClick (event, paneId) {
  130. this.$emit('pane-click', this.indexedPanes[paneId])
  131. },
  132. // Get the cursor position relative to the splitpane container.
  133. getCurrentMouseDrag (event) {
  134. const rect = this.container.getBoundingClientRect()
  135. const { clientX, clientY } = ('ontouchstart' in window && event.touches) ? event.touches[0] : event
  136. return {
  137. x: clientX - rect.left,
  138. y: clientY - rect.top
  139. }
  140. },
  141. // Returns the drag percentage of the splitter relative to the 2 panes it's inbetween.
  142. // if the sum of size of the 2 cells is 60%, the dragPercentage range will be 0 to 100% of this 60%.
  143. getCurrentDragPercentage (drag) {
  144. drag = drag[this.horizontal ? 'y' : 'x']
  145. // In the code bellow 'size' refers to 'width' for vertical and 'height' for horizontal layout.
  146. const containerSize = this.container[this.horizontal ? 'clientHeight' : 'clientWidth']
  147. if (this.rtl && !this.horizontal) drag = containerSize - drag
  148. return drag * 100 / containerSize
  149. },
  150. calculatePanesSize (drag) {
  151. const splitterIndex = this.touch.activeSplitter
  152. let sums = {
  153. prevPanesSize: this.sumPrevPanesSize(splitterIndex),
  154. nextPanesSize: this.sumNextPanesSize(splitterIndex),
  155. prevReachedMinPanes: 0,
  156. nextReachedMinPanes: 0
  157. }
  158. const minDrag = 0 + (this.pushOtherPanes ? 0 : sums.prevPanesSize)
  159. const maxDrag = 100 - (this.pushOtherPanes ? 0 : sums.nextPanesSize)
  160. const dragPercentage = Math.max(Math.min(this.getCurrentDragPercentage(drag), maxDrag), minDrag)
  161. // If not pushing other panes, panes to resize are right before and right after splitter.
  162. let panesToResize = [splitterIndex, splitterIndex + 1]
  163. let paneBefore = this.panes[panesToResize[0]] || null
  164. let paneAfter = this.panes[panesToResize[1]] || null
  165. const paneBeforeMaxReached = paneBefore.max < 100 && (dragPercentage >= (paneBefore.max + sums.prevPanesSize))
  166. const paneAfterMaxReached = paneAfter.max < 100 && (dragPercentage <= 100 - (paneAfter.max + this.sumNextPanesSize(splitterIndex + 1)))
  167. // Prevent dragging beyond pane max.
  168. if (paneBeforeMaxReached || paneAfterMaxReached) {
  169. if (paneBeforeMaxReached) {
  170. paneBefore.size = paneBefore.max
  171. paneAfter.size = Math.max(100 - paneBefore.max - sums.prevPanesSize - sums.nextPanesSize, 0)
  172. }
  173. else {
  174. paneBefore.size = Math.max(100 - paneAfter.max - sums.prevPanesSize - this.sumNextPanesSize(splitterIndex + 1), 0)
  175. paneAfter.size = paneAfter.max
  176. }
  177. return
  178. }
  179. // When pushOtherPanes = true, find the closest expanded pane on each side of the splitter.
  180. if (this.pushOtherPanes) {
  181. const vars = this.doPushOtherPanes(sums, dragPercentage)
  182. if (!vars) return // Prevent other calculation.
  183. ({ sums, panesToResize } = vars)
  184. paneBefore = this.panes[panesToResize[0]] || null
  185. paneAfter = this.panes[panesToResize[1]] || null
  186. }
  187. if (paneBefore !== null) {
  188. paneBefore.size = Math.min(Math.max(dragPercentage - sums.prevPanesSize - sums.prevReachedMinPanes, paneBefore.min), paneBefore.max)
  189. }
  190. if (paneAfter !== null) {
  191. paneAfter.size = Math.min(Math.max(100 - dragPercentage - sums.nextPanesSize - sums.nextReachedMinPanes, paneAfter.min), paneAfter.max)
  192. }
  193. },
  194. doPushOtherPanes (sums, dragPercentage) {
  195. const splitterIndex = this.touch.activeSplitter
  196. const panesToResize = [splitterIndex, splitterIndex + 1]
  197. // Pushing Down.
  198. // Going smaller than the current pane min size: take the previous expanded pane.
  199. if (dragPercentage < sums.prevPanesSize + this.panes[panesToResize[0]].min) {
  200. panesToResize[0] = this.findPrevExpandedPane(splitterIndex).index
  201. sums.prevReachedMinPanes = 0
  202. // If pushing a n-2 or less pane, from splitter, then make sure all in between is at min size.
  203. if (panesToResize[0] < splitterIndex) {
  204. this.panes.forEach((pane, i) => {
  205. if (i > panesToResize[0] && i <= splitterIndex) {
  206. pane.size = pane.min
  207. sums.prevReachedMinPanes += pane.min
  208. }
  209. })
  210. }
  211. sums.prevPanesSize = this.sumPrevPanesSize(panesToResize[0])
  212. // If nothing else to push down, cancel dragging.
  213. if (panesToResize[0] === undefined) {
  214. sums.prevReachedMinPanes = 0
  215. this.panes[0].size = this.panes[0].min
  216. this.panes.forEach((pane, i) => {
  217. if (i > 0 && i <= splitterIndex) {
  218. pane.size = pane.min
  219. sums.prevReachedMinPanes += pane.min
  220. }
  221. })
  222. this.panes[panesToResize[1]].size = 100 - sums.prevReachedMinPanes - this.panes[0].min - sums.prevPanesSize - sums.nextPanesSize
  223. return null
  224. }
  225. }
  226. // Pushing Up.
  227. // Pushing up beyond min size is reached: take the next expanded pane.
  228. if (dragPercentage > 100 - sums.nextPanesSize - this.panes[panesToResize[1]].min) {
  229. panesToResize[1] = this.findNextExpandedPane(splitterIndex).index
  230. sums.nextReachedMinPanes = 0
  231. // If pushing a n+2 or more pane, from splitter, then make sure all in between is at min size.
  232. if (panesToResize[1] > splitterIndex + 1) {
  233. this.panes.forEach((pane, i) => {
  234. if (i > splitterIndex && i < panesToResize[1]) {
  235. pane.size = pane.min
  236. sums.nextReachedMinPanes += pane.min
  237. }
  238. })
  239. }
  240. sums.nextPanesSize = this.sumNextPanesSize(panesToResize[1] - 1)
  241. // If nothing else to push up, cancel dragging.
  242. if (panesToResize[1] === undefined) {
  243. sums.nextReachedMinPanes = 0
  244. this.panes[this.panesCount - 1].size = this.panes[this.panesCount - 1].min
  245. this.panes.forEach((pane, i) => {
  246. if (i < this.panesCount - 1 && i >= splitterIndex + 1) {
  247. pane.size = pane.min
  248. sums.nextReachedMinPanes += pane.min
  249. }
  250. })
  251. this.panes[panesToResize[0]].size = 100 - sums.prevPanesSize - sums.nextReachedMinPanes - this.panes[this.panesCount - 1].min - sums.nextPanesSize
  252. return null
  253. }
  254. }
  255. return { sums, panesToResize }
  256. },
  257. sumPrevPanesSize (splitterIndex) {
  258. return this.panes.reduce((total, pane, i) => total + (i < splitterIndex ? pane.size : 0), 0)
  259. },
  260. sumNextPanesSize (splitterIndex) {
  261. return this.panes.reduce((total, pane, i) => total + (i > splitterIndex + 1 ? pane.size : 0), 0)
  262. },
  263. // Return the previous pane from siblings which has a size (width for vert or height for horz) of more than 0.
  264. findPrevExpandedPane (splitterIndex) {
  265. const pane = [...this.panes].reverse().find(p => (p.index < splitterIndex && p.size > p.min))
  266. return pane || {}
  267. },
  268. // Return the next pane from siblings which has a size (width for vert or height for horz) of more than 0.
  269. findNextExpandedPane (splitterIndex) {
  270. const pane = this.panes.find(p => (p.index > splitterIndex + 1 && p.size > p.min))
  271. return pane || {}
  272. },
  273. checkSplitpanesNodes () {
  274. const children = Array.from(this.container.children)
  275. children.forEach(child => {
  276. const isPane = child.classList.contains('splitpanes__pane')
  277. const isSplitter = child.classList.contains('splitpanes__splitter')
  278. // Node is not a Pane or a splitter: remove it.
  279. if (!isPane && !isSplitter) {
  280. child.parentNode.removeChild(child) // el.remove() doesn't work on IE11.
  281. // eslint-disable-next-line no-console
  282. console.warn('Splitpanes: Only <pane> elements are allowed at the root of <splitpanes>. One of your DOM nodes was removed.')
  283. return
  284. }
  285. })
  286. },
  287. addSplitter (paneIndex, nextPaneNode, isVeryFirst = false) {
  288. const splitterIndex = paneIndex - 1
  289. const elm = document.createElement('div')
  290. elm.classList.add('splitpanes__splitter')
  291. if (!isVeryFirst) {
  292. elm.onmousedown = event => this.onMouseDown(event, splitterIndex)
  293. if (typeof window !== 'undefined' && 'ontouchstart' in window) {
  294. elm.ontouchstart = event => this.onMouseDown(event, splitterIndex)
  295. }
  296. elm.onclick = event => this.onSplitterClick(event, splitterIndex + 1)
  297. }
  298. if (this.dblClickSplitter) {
  299. elm.ondblclick = event => this.onSplitterDblClick(event, splitterIndex + 1)
  300. }
  301. nextPaneNode.parentNode.insertBefore(elm, nextPaneNode)
  302. },
  303. removeSplitter (node) {
  304. node.onmousedown = undefined
  305. node.onclick = undefined
  306. node.ondblclick = undefined
  307. node.parentNode.removeChild(node) // el.remove() doesn't work on IE11.
  308. },
  309. redoSplitters () {
  310. const children = Array.from(this.container.children)
  311. children.forEach(el => {
  312. if (el.className.includes('splitpanes__splitter')) this.removeSplitter(el)
  313. })
  314. let paneIndex = 0
  315. children.forEach(el => {
  316. if (el.className.includes('splitpanes__pane')) {
  317. if (!paneIndex && this.firstSplitter) this.addSplitter(paneIndex, el, true)
  318. else if (paneIndex) this.addSplitter(paneIndex, el)
  319. paneIndex++
  320. }
  321. })
  322. },
  323. // Called by Pane component on programmatic resize.
  324. requestUpdate ({ target, ...args }) {
  325. const pane = this.indexedPanes[target._uid]
  326. Object.entries(args).forEach(([key, value]) => pane[key] = value)
  327. },
  328. onPaneAdd (pane) {
  329. // 1. Add pane to array at the same index it was inserted in the <splitpanes> tag.
  330. let index = -1
  331. Array.from(pane.$el.parentNode.children).some(el => {
  332. if (el.className.includes('splitpanes__pane')) index++
  333. return el === pane.$el
  334. })
  335. const min = parseFloat(pane.minSize)
  336. const max = parseFloat(pane.maxSize)
  337. this.panes.splice(index, 0, {
  338. id: pane._uid,
  339. index,
  340. min: isNaN(min) ? 0 : min,
  341. max: isNaN(max) ? 100 : max,
  342. size: pane.size === null ? null : parseFloat(pane.size),
  343. givenSize: pane.size,
  344. update: pane.update
  345. })
  346. // Redo indexes after insertion for other shifted panes.
  347. this.panes.forEach((p, i) => p.index = i)
  348. if (this.ready) {
  349. this.$nextTick(() => {
  350. // 2. Add the splitter.
  351. this.redoSplitters()
  352. // 3. Resize the panes.
  353. this.resetPaneSizes({ addedPane: this.panes[index] })
  354. // 4. Fire `pane-add` event.
  355. this.$emit('pane-add', { index, panes: this.panes.map(pane => ({ min: pane.min, max: pane.max, size: pane.size })) })
  356. })
  357. }
  358. },
  359. onPaneRemove (pane) {
  360. // 1. Remove the pane from array and redo indexes.
  361. const index = this.panes.findIndex(p => p.id === pane._uid)
  362. const removed = this.panes.splice(index, 1)[0]
  363. this.panes.forEach((p, i) => p.index = i)
  364. this.$nextTick(() => {
  365. // 2. Remove the splitter.
  366. this.redoSplitters()
  367. // 3. Resize the panes.
  368. this.resetPaneSizes({ removedPane: { ...removed, index } })
  369. // 4. Fire `pane-remove` event.
  370. this.$emit('pane-remove', { removed, panes: this.panes.map(pane => ({ min: pane.min, max: pane.max, size: pane.size })) })
  371. })
  372. },
  373. resetPaneSizes (changedPanes = {}) {
  374. if (!changedPanes.addedPane && !changedPanes.removedPane) this.initialPanesSizing()
  375. else if (this.panes.some(pane => pane.givenSize !== null || pane.min || pane.max < 100)) this.equalizeAfterAddOrRemove(changedPanes)
  376. else this.equalize()
  377. if (this.ready) this.$emit('resized', this.panes.map(pane => ({ min: pane.min, max: pane.max, size: pane.size })))
  378. },
  379. equalize () {
  380. const equalSpace = 100 / this.panesCount
  381. let leftToAllocate = 0
  382. let ungrowable = []
  383. let unshrinkable = []
  384. this.panes.forEach(pane => {
  385. pane.size = Math.max(Math.min(equalSpace, pane.max), pane.min)
  386. leftToAllocate -= pane.size
  387. if (pane.size >= pane.max) ungrowable.push(pane.id)
  388. if (pane.size <= pane.min) unshrinkable.push(pane.id)
  389. })
  390. if (leftToAllocate > 0.1) this.readjustSizes(leftToAllocate, ungrowable, unshrinkable)
  391. },
  392. initialPanesSizing () {
  393. let equalSpace = 100 / this.panesCount
  394. let leftToAllocate = 100
  395. let ungrowable = []
  396. let unshrinkable = []
  397. let definedSizes = 0
  398. // Check if pre-allocated space is 100%.
  399. this.panes.forEach(pane => {
  400. leftToAllocate -= pane.size
  401. if (pane.size !== null) definedSizes++
  402. if (pane.size >= pane.max) ungrowable.push(pane.id)
  403. if (pane.size <= pane.min) unshrinkable.push(pane.id)
  404. })
  405. // set pane sizes if not set.
  406. let leftToAllocate2 = 100
  407. if (leftToAllocate > 0.1) {
  408. this.panes.forEach(pane => {
  409. if (pane.size === null) {
  410. pane.size = Math.max(Math.min(leftToAllocate / (this.panesCount - definedSizes), pane.max), pane.min)
  411. }
  412. leftToAllocate2 -= pane.size
  413. })
  414. if (leftToAllocate2 > 0.1) this.readjustSizes(leftToAllocate, ungrowable, unshrinkable)
  415. }
  416. },
  417. equalizeAfterAddOrRemove ({ addedPane, removedPane } = {}) {
  418. let equalSpace = 100 / this.panesCount
  419. let leftToAllocate = 0
  420. let ungrowable = []
  421. let unshrinkable = []
  422. if (addedPane && addedPane.givenSize !== null) {
  423. equalSpace = (100 - addedPane.givenSize) / (this.panesCount - 1)
  424. }
  425. // Check if pre-allocated space is 100%.
  426. this.panes.forEach(pane => {
  427. leftToAllocate -= pane.size
  428. if (pane.size >= pane.max) ungrowable.push(pane.id)
  429. if (pane.size <= pane.min) unshrinkable.push(pane.id)
  430. })
  431. if (Math.abs(leftToAllocate) < 0.1) return // Ok.
  432. this.panes.forEach(pane => {
  433. if (addedPane && addedPane.givenSize !== null && addedPane.id === pane.id) {}
  434. else pane.size = Math.max(Math.min(equalSpace, pane.max), pane.min)
  435. leftToAllocate -= pane.size
  436. if (pane.size >= pane.max) ungrowable.push(pane.id)
  437. if (pane.size <= pane.min) unshrinkable.push(pane.id)
  438. })
  439. if (leftToAllocate > 0.1) this.readjustSizes(leftToAllocate, ungrowable, unshrinkable)
  440. },
  441. /* recalculatePaneSizes ({ addedPane, removedPane } = {}) {
  442. let leftToAllocate = 100
  443. let equalSpaceToAllocate = leftToAllocate / this.panesCount
  444. let ungrowable = []
  445. let unshrinkable = []
  446. // When adding a pane with no size, apply min-size if defined otherwise divide another pane
  447. // (next or prev) in 2.
  448. // if (addedPane && addedPane.size === null) {
  449. // if (addedPane.min) addedPane.size = addedPane.min
  450. // else {
  451. // const paneToDivide = this.panes[addedPane.index + 1] || this.panes[addedPane.index - 1]
  452. // if (paneToDivide) {
  453. // // @todo: Dividing that pane in 2 could be incorrect if becoming lower than its min size.
  454. // addedPane.size = paneToDivide.size / 2
  455. // paneToDivide.size /= 2
  456. // }
  457. // }
  458. // }
  459. this.panes.forEach((pane, i) => {
  460. // Added pane - reduce the size of the next pane.
  461. if (addedPane && addedPane.index + 1 === i) {
  462. pane.size = Math.max(Math.min(100 - this.sumPrevPanesSize(i) - this.sumNextPanesSize(i + 1), pane.max), pane.min)
  463. // @todo: if could not allocate correctly, try to allocate in the next pane straight away,
  464. // then still do the second loop if not correct.
  465. }
  466. // Removed pane - increase the size of the next pane.
  467. else if (removedPane && removedPane.index === i) {
  468. pane.size = Math.max(Math.min(100 - this.sumPrevPanesSize(i) - this.sumNextPanesSize(i + 1), pane.max), pane.min)
  469. // @todo: if could not allocate correctly, try to allocate in the next pane straight away,
  470. // then still do the second loop if not correct.
  471. }
  472. // Initial load and on demand recalculation.
  473. else if (!addedPane && !removedPane && pane.size === null) {
  474. pane.size = Math.max(Math.min(equalSpaceToAllocate, pane.max), pane.min)
  475. }
  476. leftToAllocate -= pane.size
  477. if (pane.size >= pane.max) ungrowable.push(pane.id)
  478. if (pane.size <= pane.min) unshrinkable.push(pane.id)
  479. })
  480. // Do one more loop to adjust sizes if still wrong.
  481. // > 0.1: Prevent maths rounding issues due to bytes.
  482. if (Math.abs(leftToAllocate) > 0.1) this.readjustSizes(leftToAllocate, ungrowable, unshrinkable)
  483. }, */
  484. // Second loop to adjust sizes now that we know more about the panes constraints.
  485. readjustSizes (leftToAllocate, ungrowable, unshrinkable) {
  486. let equalSpaceToAllocate
  487. if (leftToAllocate > 0) equalSpaceToAllocate = leftToAllocate / (this.panesCount - ungrowable.length)
  488. else equalSpaceToAllocate = leftToAllocate / (this.panesCount - unshrinkable.length)
  489. this.panes.forEach((pane, i) => {
  490. if (leftToAllocate > 0 && !ungrowable.includes(pane.id)) {
  491. // Need to diff the size before and after to get the exact allocated space.
  492. const newPaneSize = Math.max(Math.min(pane.size + equalSpaceToAllocate, pane.max), pane.min)
  493. const allocated = newPaneSize - pane.size
  494. leftToAllocate -= allocated
  495. pane.size = newPaneSize
  496. }
  497. else if (!unshrinkable.includes(pane.id)) {
  498. // Need to diff the size before and after to get the exact allocated space.
  499. const newPaneSize = Math.max(Math.min(pane.size + equalSpaceToAllocate, pane.max), pane.min)
  500. const allocated = newPaneSize - pane.size
  501. leftToAllocate -= allocated
  502. pane.size = newPaneSize
  503. }
  504. // Update each pane through the registered `update` method.
  505. pane.update({
  506. [this.horizontal ? 'height' : 'width']: `${this.indexedPanes[pane.id].size}%`
  507. })
  508. })
  509. if (Math.abs(leftToAllocate) > 0.1) { // > 0.1: Prevent maths rounding issues due to bytes.
  510. // Don't emit on hot reload when Vue destroys panes.
  511. this.$nextTick(() => {
  512. if (this.ready) {
  513. // eslint-disable-next-line no-console
  514. console.warn('Splitpanes: Could not resize panes correctly due to their constraints.')
  515. }
  516. })
  517. }
  518. }
  519. /* distributeEmptySpace () {
  520. let growablePanes = []
  521. let collapsedPanesCount = 0
  522. let growableAmount = 0 // Total of how much the current panes can grow to fill blank space.
  523. let spaceToDistribute = 100 - this.panes.reduce((sum, pane) => (sum += pane.size) && sum, 0)
  524. // Do a first loop to determine if we can distribute the new blank space between all the
  525. // expandedPanes, without expanding the collapsed ones.
  526. this.panes.forEach(pane => {
  527. if (pane.size < pane.max) growablePanes.push(pane)
  528. if (!pane.size) collapsedPanesCount++
  529. else growableAmount += pane.max - pane.size
  530. })
  531. // If the blank space to distribute is too great for the expanded panes, also expand collapsed ones.
  532. let expandCollapsedPanes = growableAmount < spaceToDistribute
  533. // New space to distribute equally.
  534. let growablePanesCount = (growablePanes.length - (expandCollapsedPanes ? 0 : collapsedPanesCount))
  535. let equalSpaceToDistribute = spaceToDistribute / growablePanesCount
  536. // if (growablePanesCount === 1) equalSpace = 100 / this.panesCount
  537. let spaceLeftToDistribute = spaceToDistribute
  538. // Now add the equalSpaceToDistribute to each pane size accordingly.
  539. growablePanes.forEach(pane => {
  540. if (pane.size < pane.max && (pane.size || (!pane.size && expandCollapsedPanes))) {
  541. const newSize = Math.min(pane.size + equalSpaceToDistribute, pane.max)
  542. let allocatedSpace = (newSize - pane.size)
  543. spaceLeftToDistribute -= allocatedSpace
  544. pane.size = newSize
  545. // If the equalSpaceToDistribute is not fully added to the current pane, distribute the remainder
  546. // to the next panes.
  547. // Also fix decimal issue due to bites - E.g. calculating 8.33 and getting 8.3299999999999
  548. if (equalSpaceToDistribute - allocatedSpace > 0.1) equalSpaceToDistribute = spaceLeftToDistribute / (--growablePanesCount)
  549. }
  550. })
  551. /* Disabled otherwise will show up on hot reload.
  552. // if there is still space to allocate show warning message.
  553. if (this.panesCount && ~~spaceLeftToDistribute) {
  554. // eslint-disable-next-line no-console
  555. console.warn('Splitpanes: Could not distribute all the empty space between panes due to their constraints.')
  556. } *\/
  557. this.$emit('resized', this.panes.map(pane => ({ min: pane.min, max: pane.max, size: pane.size })))
  558. } */
  559. },
  560. watch: {
  561. panes: { // Every time a pane is updated, update the panes accordingly.
  562. deep: true,
  563. immediate: false,
  564. handler () { this.updatePaneComponents() }
  565. },
  566. horizontal () {
  567. this.updatePaneComponents()
  568. },
  569. firstSplitter () {
  570. this.redoSplitters()
  571. },
  572. dblClickSplitter (enable) {
  573. const splitters = [...this.container.querySelectorAll('.splitpanes__splitter')]
  574. splitters.forEach((splitter, i) => {
  575. splitter.ondblclick = enable ? event => this.onSplitterDblClick(event, i) : undefined
  576. })
  577. }
  578. },
  579. beforeDestroy () {
  580. // Prevent emitting console warnings on hot reloading.
  581. this.ready = false
  582. },
  583. mounted () {
  584. this.container = this.$refs.container
  585. this.checkSplitpanesNodes()
  586. this.redoSplitters()
  587. this.resetPaneSizes()
  588. this.$emit('ready')
  589. this.ready = true
  590. },
  591. render (h) {
  592. return h(
  593. 'div',
  594. {
  595. ref: 'container',
  596. class: [
  597. 'splitpanes',
  598. `splitpanes--${this.horizontal ? 'horizontal' : 'vertical'}`,
  599. {
  600. 'splitpanes--dragging': this.touch.dragging
  601. }
  602. ]
  603. },
  604. this.$slots.default
  605. )
  606. }
  607. }
  608. </script>
  609. <style lang="scss">
  610. .splitpanes {
  611. display: flex;
  612. width: 100%;
  613. height: 100%;
  614. &--vertical {flex-direction: row;}
  615. &--horizontal {flex-direction: column;}
  616. &--dragging * {user-select: none;}
  617. &__pane {
  618. width: 100%;
  619. height: 100%;
  620. overflow: hidden;
  621. .splitpanes--vertical & {transition: width 0.2s ease-out;}
  622. .splitpanes--horizontal & {transition: height 0.2s ease-out;}
  623. .splitpanes--dragging & {transition: none;}
  624. }
  625. // Disable default zoom behavior on touch device when double tapping splitter.
  626. &__splitter {touch-action: none;}
  627. &--vertical > .splitpanes__splitter {min-width: 1px;cursor: col-resize;}
  628. &--horizontal > .splitpanes__splitter {min-height: 1px;cursor: row-resize;}
  629. }
  630. .splitpanes.default-theme {
  631. .splitpanes__pane {
  632. background-color: #f2f2f2;
  633. }
  634. .splitpanes__splitter {
  635. background-color: #fff;
  636. box-sizing: border-box;
  637. position: relative;
  638. flex-shrink: 0;
  639. &:before, &:after {
  640. content: "";
  641. position: absolute;
  642. top: 50%;
  643. left: 50%;
  644. background-color: rgba(0, 0, 0, .15);
  645. transition: background-color 0.3s;
  646. }
  647. &:hover:before, &:hover:after {background-color: rgba(0, 0, 0, .25);}
  648. &:first-child {cursor: auto;}
  649. }
  650. }
  651. .default-theme {
  652. &.splitpanes .splitpanes .splitpanes__splitter {
  653. z-index: 1;
  654. }
  655. &.splitpanes--vertical > .splitpanes__splitter,
  656. .splitpanes--vertical > .splitpanes__splitter {
  657. width: 7px;
  658. border-left: 1px solid #eee;
  659. margin-left: -1px;
  660. &:before, &:after {
  661. transform: translateY(-50%);
  662. width: 1px;
  663. height: 30px;
  664. }
  665. &:before {margin-left: -2px;}
  666. &:after {margin-left: 1px;}
  667. }
  668. &.splitpanes--horizontal > .splitpanes__splitter,
  669. .splitpanes--horizontal > .splitpanes__splitter {
  670. height: 7px;
  671. border-top: 1px solid #eee;
  672. margin-top: -1px;
  673. &:before,
  674. &:after {
  675. transform: translateX(-50%);
  676. width: 30px;
  677. height: 1px;
  678. }
  679. &:before {margin-top: -2px;}
  680. &:after {margin-top: 1px;}
  681. }
  682. }
  683. </style>