split.js 24 KB

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