split.js 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587
  1. // The programming goals of Split.js are to deliver readable, understandable and
  2. // maintainable code, while at the same time manually optimizing for tiny minified file size,
  3. // browser compatibility without additional requirements, graceful fallback (IE8 is supported)
  4. // and very few assumptions about the user's page layout.
  5. //
  6. // Make sure all browsers handle this JS library correctly with ES5.
  7. // More information here: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Strict_mode
  8. 'use strict';
  9. // A wrapper function that does a couple things:
  10. //
  11. // 1. Doesn't pollute the global namespace. This is important for a library.
  12. // 2. Allows us to mount the library in different module systems, as well as
  13. // directly in the browser.
  14. (function() {
  15. // Save the global `this` for use later. In this case, since the library only
  16. // runs in the browser, it will refer to `window`. Also, figure out if we're in IE8
  17. // or not. IE8 will still render correctly, but will be static instead of draggable.
  18. //
  19. // Save a couple long function names that are used frequently.
  20. // This optimization saves around 400 bytes.
  21. //
  22. // Set a float fudging global, used when dividing and setting sizes to long floats.
  23. // There's a chance that sometimes the sum of the floats would end up being slightly
  24. // larger than 100%, breaking the layout. The float fudging value is subtracted from
  25. // the percentage size.
  26. var global = this
  27. , isIE8 = global.attachEvent && !global[addEventListener]
  28. , document = global.document
  29. , addEventListener = 'addEventListener'
  30. , removeEventListener = 'removeEventListener'
  31. , getBoundingClientRect = 'getBoundingClientRect'
  32. , FLOAT_FUDGING = 0.5
  33. // This library only needs two helper functions:
  34. //
  35. // The first determines which prefixes of CSS calc we need.
  36. // We only need to do this once on startup, when this anonymous function is called.
  37. //
  38. // Tests -webkit, -moz and -o prefixes. Modified from StackOverflow:
  39. // http://stackoverflow.com/questions/16625140/js-feature-detection-to-detect-the-usage-of-webkit-calc-over-calc/16625167#16625167
  40. , calc = (function () {
  41. var el
  42. , prefixes = ["", "-webkit-", "-moz-", "-o-"]
  43. for (var i = 0; i < prefixes.length; i++) {
  44. el = document.createElement('div')
  45. el.style.cssText = "width:" + prefixes[i] + "calc(9px)"
  46. if (el.style.length) {
  47. return prefixes[i] + "calc"
  48. }
  49. }
  50. })()
  51. // The second helper function allows elements and string selectors to be used
  52. // interchangeably. In either case an element is returned. This allows us to
  53. // do `Split(elem1, elem2)` as well as `Split('#id1', '#id2')`.
  54. , elementOrSelector = function (el) {
  55. if (typeof el === 'string' || el instanceof String) {
  56. return document.querySelector(el)
  57. } else {
  58. return el
  59. }
  60. }
  61. // The main function to initialize a split. Split.js thinks about each pair
  62. // of elements as an independant pair. Dragging the gutter between two elements
  63. // only changes the dimensions of elements in that pair. This is key to understanding
  64. // how the following functions operate, since each function is bound to a pair.
  65. //
  66. // A pair object is shaped like this:
  67. //
  68. // {
  69. // a: DOM element,
  70. // b: DOM element,
  71. // aMin: Number,
  72. // bMin: Number,
  73. // dragging: Boolean,
  74. // parent: DOM element,
  75. // isFirst: Boolean,
  76. // isLast: Boolean,
  77. // direction: 'horizontal' | 'vertical'
  78. // }
  79. //
  80. // The basic sequence:
  81. //
  82. // 1. Set defaults to something sane. `options` doesn't have to be passed at all.
  83. // 2. Initialize a bunch of strings based on the direction we're splitting.
  84. // A lot of the behavior in the rest of the library is paramatized down to
  85. // rely on CSS strings and classes.
  86. // 3. Define the dragging helper functions, and a few helpers to go with them.
  87. // 4. Loop through the elements while pairing them off. Every pair gets an
  88. // `pair` object, a gutter, and special isFirst/isLast properties.
  89. // 5. Actually size the pair elements, insert gutters and attach event listeners.
  90. , Split = function (ids, options) {
  91. var dimension
  92. , i
  93. , clientDimension
  94. , clientAxis
  95. , position
  96. , gutterClass
  97. , paddingA
  98. , paddingB
  99. , pairs = []
  100. // 1. Set defaults to something sane. `options` doesn't have to be passed at all,
  101. // so create an options object if none exists. Pixel values 10, 100 and 30 are
  102. // arbitrary but feel natural.
  103. options = typeof options !== 'undefined' ? options : {}
  104. if (typeof options.gutterSize === 'undefined') options.gutterSize = 10
  105. if (typeof options.minSize === 'undefined') options.minSize = 100
  106. if (typeof options.snapOffset === 'undefined') options.snapOffset = 30
  107. if (typeof options.direction === 'undefined') options.direction = 'horizontal'
  108. if (typeof options.elementStyle === 'undefined') options.elementStyle = function (dimension, size, gutterSize) {
  109. var style = {}
  110. if (typeof size !== 'string' && !(size instanceof String)) {
  111. if (!isIE8) {
  112. style[dimension] = calc + '(' + size + '% - ' + gutterSize + 'px)'
  113. } else {
  114. style[dimension] = size + '%'
  115. }
  116. } else {
  117. style[dimension] = size
  118. }
  119. return style
  120. }
  121. if (typeof options.gutterStyle === 'undefined') options.gutterStyle = function (dimension, gutterSize) {
  122. var style = {}
  123. style[dimension] = gutterSize + 'px'
  124. return style
  125. }
  126. // 2. Initialize a bunch of strings based on the direction we're splitting.
  127. // A lot of the behavior in the rest of the library is paramatized down to
  128. // rely on CSS strings and classes.
  129. if (options.direction == 'horizontal') {
  130. dimension = 'width'
  131. clientDimension = 'clientWidth'
  132. clientAxis = 'clientX'
  133. position = 'left'
  134. gutterClass = 'gutter gutter-horizontal'
  135. paddingA = 'paddingLeft'
  136. paddingB = 'paddingRight'
  137. if (!options.cursor) options.cursor = 'ew-resize'
  138. } else if (options.direction == 'vertical') {
  139. dimension = 'height'
  140. clientDimension = 'clientHeight'
  141. clientAxis = 'clientY'
  142. position = 'top'
  143. gutterClass = 'gutter gutter-vertical'
  144. paddingA = 'paddingTop'
  145. paddingB = 'paddingBottom'
  146. if (!options.cursor) options.cursor = 'ns-resize'
  147. }
  148. // 3. Define the dragging helper functions, and a few helpers to go with them.
  149. // Each helper is bound to a pair object that contains it's metadata. This
  150. // also makes it easy to store references to listeners that that will be
  151. // added and removed.
  152. //
  153. // Even though there are no other functions contained in them, aliasing
  154. // this to self saves 50 bytes or so since it's used so frequently.
  155. //
  156. // The pair object saves metadata like dragging state, position and
  157. // event listener references.
  158. //
  159. // startDragging calls `calculateSizes` to store the inital size in the pair object.
  160. // It also adds event listeners for mouse/touch events,
  161. // and prevents selection while dragging so avoid the selecting text.
  162. var startDragging = function (e) {
  163. // Alias frequently used variables to save space. 200 bytes.
  164. var self = this
  165. , a = self.a
  166. , b = self.b
  167. // Call the onDragStart callback.
  168. if (!self.dragging && options.onDragStart) {
  169. options.onDragStart()
  170. }
  171. // Don't actually drag the element. We emulate that in the drag function.
  172. e.preventDefault()
  173. // Set the dragging property of the pair object.
  174. self.dragging = true
  175. // Create two event listeners bound to the same pair object and store
  176. // them in the pair object.
  177. self.move = drag.bind(self)
  178. self.stop = stopDragging.bind(self)
  179. // All the binding. `window` gets the stop events in case we drag out of the elements.
  180. global[addEventListener]('mouseup', self.stop)
  181. global[addEventListener]('touchend', self.stop)
  182. global[addEventListener]('touchcancel', self.stop)
  183. self.parent[addEventListener]('mousemove', self.move)
  184. self.parent[addEventListener]('touchmove', self.move)
  185. // Disable selection. Disable!
  186. a[addEventListener]('selectstart', noop)
  187. a[addEventListener]('dragstart', noop)
  188. b[addEventListener]('selectstart', noop)
  189. b[addEventListener]('dragstart', noop)
  190. a.style.userSelect = 'none'
  191. a.style.webkitUserSelect = 'none'
  192. a.style.MozUserSelect = 'none'
  193. a.style.pointerEvents = 'none'
  194. b.style.userSelect = 'none'
  195. b.style.webkitUserSelect = 'none'
  196. b.style.MozUserSelect = 'none'
  197. b.style.pointerEvents = 'none'
  198. // Set the cursor, both on the gutter and the parent element.
  199. // Doing only a, b and gutter causes flickering.
  200. self.gutter.style.cursor = options.cursor
  201. self.parent.style.cursor = options.cursor
  202. // Cache the initial sizes of the pair.
  203. calculateSizes.call(self)
  204. }
  205. // stopDragging is very similar to startDragging in reverse.
  206. , stopDragging = function () {
  207. var self = this
  208. , a = self.a
  209. , b = self.b
  210. if (self.dragging && options.onDragEnd) {
  211. options.onDragEnd()
  212. }
  213. self.dragging = false
  214. // Remove the stored event listeners. This is why we store them.
  215. global[removeEventListener]('mouseup', self.stop)
  216. global[removeEventListener]('touchend', self.stop)
  217. global[removeEventListener]('touchcancel', self.stop)
  218. self.parent[removeEventListener]('mousemove', self.move)
  219. self.parent[removeEventListener]('touchmove', self.move)
  220. // Delete them once they are removed. I think this makes a difference
  221. // in memory usage with a lot of splits on one page. But I don't know for sure.
  222. delete self.stop
  223. delete self.move
  224. a[removeEventListener]('selectstart', noop)
  225. a[removeEventListener]('dragstart', noop)
  226. b[removeEventListener]('selectstart', noop)
  227. b[removeEventListener]('dragstart', noop)
  228. a.style.userSelect = ''
  229. a.style.webkitUserSelect = ''
  230. a.style.MozUserSelect = ''
  231. a.style.pointerEvents = ''
  232. b.style.userSelect = ''
  233. b.style.webkitUserSelect = ''
  234. b.style.MozUserSelect = ''
  235. b.style.pointerEvents = ''
  236. self.gutter.style.cursor = ''
  237. self.parent.style.cursor = ''
  238. }
  239. // drag, where all the magic happens. The logic is really quite simple:
  240. //
  241. // 1. Ignore if the pair is not dragging.
  242. // 2. Get the offset of the event.
  243. // 3. Snap offset to min if within snappable range (within min + snapOffset).
  244. // 4. Actually adjust each element in the pair to offset.
  245. //
  246. // ---------------------------------------------------------------------
  247. // | | <- this.aMin || this.bMin -> | |
  248. // | | | <- this.snapOffset || this.snapOffset -> | | |
  249. // | | | || | | |
  250. // | | | || | | |
  251. // ---------------------------------------------------------------------
  252. // | <- this.start this.size -> |
  253. , drag = function (e) {
  254. var offset
  255. if (!this.dragging) return
  256. // Get the offset of the event from the first side of the
  257. // pair `this.start`. Supports touch events, but not multitouch, so only the first
  258. // finger `touches[0]` is counted.
  259. if ('touches' in e) {
  260. offset = e.touches[0][clientAxis] - this.start
  261. } else {
  262. offset = e[clientAxis] - this.start
  263. }
  264. // If within snapOffset of min or max, set offset to min or max.
  265. // snapOffset buffers aMin and bMin, so logic is opposite for both.
  266. // Include the appropriate gutter sizes to prevent overflows.
  267. if (offset <= this.aMin + options.snapOffset + this.aGutterSize) {
  268. offset = this.aMin + this.aGutterSize
  269. } else if (offset >= this.size - (this.bMin + options.snapOffset + this.bGutterSize)) {
  270. offset = this.size - (this.bMin + this.bGutterSize)
  271. }
  272. offset = offset - FLOAT_FUDGING
  273. // Actually adjust the size.
  274. adjust.call(this, offset)
  275. // Call the drag callback continously. Don't do anything too intensive
  276. // in this callback.
  277. if (options.onDrag) {
  278. options.onDrag()
  279. }
  280. }
  281. // Cache some important sizes when drag starts, so we don't have to do that
  282. // continously:
  283. //
  284. // `size`: The total size of the pair. First element + second element + first gutter + second gutter.
  285. // `percentage`: The percentage between 0-100 that the pair occupies in the parent.
  286. // `start`: The leading side of the first element.
  287. //
  288. // ------------------------------------------------ - - - - - - - - - - -
  289. // | aGutterSize -> ||| | |
  290. // | ||| | |
  291. // | ||| | |
  292. // | ||| <- bGutterSize | |
  293. // ------------------------------------------------ - - - - - - - - - - -
  294. // | <- start size -> | parentSize -> |
  295. , calculateSizes = function () {
  296. // Figure out the parent size minus padding.
  297. var computedStyle = global.getComputedStyle(this.parent)
  298. , parentSize = this.parent[clientDimension] - parseFloat(computedStyle[paddingA]) - parseFloat(computedStyle[paddingB])
  299. this.size = this.a[getBoundingClientRect]()[dimension] + this.b[getBoundingClientRect]()[dimension] + this.aGutterSize + this.bGutterSize
  300. this.percentage = Math.min(this.size / parentSize * 100, 100)
  301. this.start = this.a[getBoundingClientRect]()[position]
  302. }
  303. // Actually adjust the size of elements `a` and `b` to `offset` while dragging.
  304. // calc is used to allow calc(percentage + gutterpx) on the whole split instance,
  305. // which allows the viewport to be resized without additional logic.
  306. // Element a's size is the same as offset. b's size is total size - a size.
  307. // Both sizes are calculated from the initial parent percentage, then the gutter size is subtracted.
  308. , adjust = function (offset) {
  309. setElementSize(this.a, (offset / this.size * this.percentage), this.aGutterSize)
  310. setElementSize(this.b, (this.percentage - (offset / this.size * this.percentage)), this.bGutterSize)
  311. }
  312. , setElementSize = function (el, size, gutterSize) {
  313. // Split.js allows setting sizes via numbers (ideally), or if you must,
  314. // by string, like '300px'. This is less than ideal, because it breaks
  315. // the fluid layout that `calc(% - px)` provides. You're on your own if you do that,
  316. // make sure you calculate the gutter size by hand.
  317. var style = options.elementStyle(dimension, size, gutterSize)
  318. , props = Object.keys(style)
  319. for (var i = 0; i < props.length; i++) {
  320. el.style[props[i]] = style[props[i]]
  321. }
  322. }
  323. , setGutterSize = function (gutter, gutterSize) {
  324. var style = options.gutterStyle(dimension, gutterSize)
  325. , props = Object.keys(style)
  326. for (var i = 0; i < props.length; i++) {
  327. gutter.style[props[i]] = style[props[i]]
  328. }
  329. }
  330. // No-op function to prevent default. Used to prevent selection.
  331. , noop = function () { return false }
  332. // All DOM elements in the split should have a common parent. We can grab
  333. // the first elements parent and hope users read the docs because the
  334. // behavior will be whacky otherwise.
  335. , parent = elementOrSelector(ids[0]).parentNode
  336. // Set default options.sizes to equal percentages of the parent element.
  337. if (!options.sizes) {
  338. var percent = 100 / ids.length
  339. options.sizes = []
  340. for (i = 0; i < ids.length; i++) {
  341. options.sizes.push(percent)
  342. }
  343. }
  344. // Standardize minSize to an array if it isn't already. This allows minSize
  345. // to be passed as a number.
  346. if (!Array.isArray(options.minSize)) {
  347. var minSizes = []
  348. for (i = 0; i < ids.length; i++) {
  349. minSizes.push(options.minSize)
  350. }
  351. options.minSize = minSizes
  352. }
  353. // 5. Loop through the elements while pairing them off. Every pair gets a
  354. // `pair` object, a gutter, and isFirst/isLast properties.
  355. //
  356. // Basic logic:
  357. //
  358. // - Starting with the second element `i > 0`, create `pair` objects with
  359. // `a = ids[i - 1]` and `b = ids[i]`
  360. // - Set gutter sizes based on the _pair_ being first/last. The first and last
  361. // pair have gutterSize / 2, since they only have one half gutter, and not two.
  362. // - Create gutter elements and add event listeners.
  363. // - Set the size of the elements, minus the gutter sizes.
  364. //
  365. // -----------------------------------------------------------------------
  366. // | i=0 | i=1 | i=2 | i=3 |
  367. // | | isFirst | | isLast |
  368. // | pair 0 pair 1 pair 2 |
  369. // | | | | |
  370. // -----------------------------------------------------------------------
  371. for (i = 0; i < ids.length; i++) {
  372. var el = elementOrSelector(ids[i])
  373. , isFirstPair = (i == 1)
  374. , isLastPair = (i == ids.length - 1)
  375. , size = options.sizes[i]
  376. , gutterSize = options.gutterSize
  377. , pair
  378. , parentFlexDirection = window.getComputedStyle(parent).flexDirection
  379. , temp
  380. if (i > 0) {
  381. // Create the pair object with it's metadata.
  382. pair = {
  383. a: elementOrSelector(ids[i - 1]),
  384. b: el,
  385. aMin: options.minSize[i - 1],
  386. bMin: options.minSize[i],
  387. dragging: false,
  388. parent: parent,
  389. isFirst: isFirstPair,
  390. isLast: isLastPair,
  391. direction: options.direction
  392. }
  393. // For first and last pairs, first and last gutter width is half.
  394. pair.aGutterSize = options.gutterSize
  395. pair.bGutterSize = options.gutterSize
  396. if (isFirstPair) {
  397. pair.aGutterSize = options.gutterSize / 2
  398. }
  399. if (isLastPair) {
  400. pair.bGutterSize = options.gutterSize / 2
  401. }
  402. // if the parent has a reverse flex-direction, switch the pair elements.
  403. if (parentFlexDirection === 'row-reverse' || parentFlexDirection === 'column-reverse') {
  404. temp = pair.a;
  405. pair.a = pair.b;
  406. pair.b = temp;
  407. }
  408. }
  409. // Determine the size of the current element. IE8 is supported by
  410. // staticly assigning sizes without draggable gutters. Assigns a string
  411. // to `size`.
  412. //
  413. // IE9 and above
  414. if (!isIE8) {
  415. // Create gutter elements for each pair.
  416. if (i > 0) {
  417. var gutter = document.createElement('div')
  418. gutter.className = gutterClass
  419. setGutterSize(gutter, gutterSize)
  420. gutter[addEventListener]('mousedown', startDragging.bind(pair))
  421. gutter[addEventListener]('touchstart', startDragging.bind(pair))
  422. parent.insertBefore(gutter, el)
  423. pair.gutter = gutter
  424. }
  425. // Half-size gutters for first and last elements.
  426. if (i === 0 || i == ids.length - 1) {
  427. gutterSize = options.gutterSize / 2
  428. }
  429. }
  430. // Set the element size to our determined size.
  431. setElementSize(el, size, gutterSize)
  432. if (i > 0) {
  433. var aSize = pair.a[getBoundingClientRect]()[dimension]
  434. , bSize = pair.b[getBoundingClientRect]()[dimension]
  435. if (aSize < pair.aMin) {
  436. pair.aMin = aSize
  437. }
  438. if (bSize < pair.bMin) {
  439. pair.bMin = bSize
  440. }
  441. }
  442. // After the first iteration, and we have a pair object, append it to the
  443. // list of pairs.
  444. if (i > 0) {
  445. pairs.push(pair)
  446. }
  447. }
  448. return {
  449. setSizes: function (sizes) {
  450. for (var i = 0; i < sizes.length; i++) {
  451. if (i > 0) {
  452. var pair = pairs[i - 1]
  453. setElementSize(pair.a, sizes[i - 1], pair.aGutterSize)
  454. setElementSize(pair.b, sizes[i], pair.bGutterSize)
  455. }
  456. }
  457. },
  458. getSizes: function () {
  459. var sizes = []
  460. for (var i = 0; i < pairs.length; i++) {
  461. var pair = pairs[i]
  462. , computedStyle = global.getComputedStyle(pair.parent)
  463. , parentSize = pair.parent[clientDimension] - parseFloat(computedStyle[paddingA]) - parseFloat(computedStyle[paddingB])
  464. sizes.push((pair.a[getBoundingClientRect]()[dimension] + pair.aGutterSize) / parentSize * 100)
  465. if (i === pairs.length - 1) {
  466. sizes.push((pair.b[getBoundingClientRect]()[dimension] + pair.bGutterSize) / parentSize * 100)
  467. }
  468. }
  469. return sizes
  470. },
  471. collapse: function (i) {
  472. var pair
  473. if (i === pairs.length) {
  474. pair = pairs[i - 1]
  475. calculateSizes.call(pair)
  476. adjust.call(pair, pair.size - pair.bGutterSize)
  477. } else {
  478. pair = pairs[i]
  479. calculateSizes.call(pair)
  480. adjust.call(pair, pair.aGutterSize)
  481. }
  482. },
  483. destroy: function () {
  484. for (var i = 0; i < pairs.length; i++) {
  485. pairs[i].parent.removeChild(pairs[i].gutter)
  486. pairs[i].a.style[dimension] = ''
  487. pairs[i].b.style[dimension] = ''
  488. }
  489. }
  490. }
  491. }
  492. // Play nicely with module systems, and the browser too if you include it raw.
  493. if (typeof exports !== 'undefined') {
  494. if (typeof module !== 'undefined' && module.exports) {
  495. exports = module.exports = Split
  496. }
  497. exports.Split = Split
  498. } else {
  499. global.Split = Split
  500. }
  501. // Call our wrapper function with the current global. In this case, `window`.
  502. }).call(window);