/*! Split.js - v1.6.2 */ (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : typeof define === 'function' && define.amd ? define(factory) : (global = global || self, global.Split = factory()); }(this, (function () { 'use strict'; // The programming goals of Split.js are to deliver readable, understandable and // maintainable code, while at the same time manually optimizing for tiny minified file size, // browser compatibility without additional requirements // and very few assumptions about the user's page layout. var global = typeof window !== 'undefined' ? window : null; var ssr = global === null; var document = !ssr ? global.document : undefined; // Save a couple long function names that are used frequently. // This optimization saves around 400 bytes. var addEventListener = 'addEventListener'; var removeEventListener = 'removeEventListener'; var getBoundingClientRect = 'getBoundingClientRect'; var gutterStartDragging = '_a'; var aGutterSize = '_b'; var bGutterSize = '_c'; var HORIZONTAL = 'horizontal'; var NOOP = function () { return false; }; // Helper function determines which prefixes of CSS calc we need. // We only need to do this once on startup, when this anonymous function is called. // // Tests -webkit, -moz and -o prefixes. Modified from StackOverflow: // http://stackoverflow.com/questions/16625140/js-feature-detection-to-detect-the-usage-of-webkit-calc-over-calc/16625167#16625167 var calc = ssr ? 'calc' : ((['', '-webkit-', '-moz-', '-o-'] .filter(function (prefix) { var el = document.createElement('div'); el.style.cssText = "width:" + prefix + "calc(9px)"; return !!el.style.length }) .shift()) + "calc"); // Helper function checks if its argument is a string-like type var isString = function (v) { return typeof v === 'string' || v instanceof String; }; // Helper function allows elements and string selectors to be used // interchangeably. In either case an element is returned. This allows us to // do `Split([elem1, elem2])` as well as `Split(['#id1', '#id2'])`. var elementOrSelector = function (el) { if (isString(el)) { var ele = document.querySelector(el); if (!ele) { throw new Error(("Selector " + el + " did not match a DOM element")) } return ele } return el }; // Helper function gets a property from the properties object, with a default fallback var getOption = function (options, propName, def) { var value = options[propName]; if (value !== undefined) { return value } return def }; var getGutterSize = function (gutterSize, isFirst, isLast, gutterAlign) { if (isFirst) { if (gutterAlign === 'end') { return 0 } if (gutterAlign === 'center') { return gutterSize / 2 } } else if (isLast) { if (gutterAlign === 'start') { return 0 } if (gutterAlign === 'center') { return gutterSize / 2 } } return gutterSize }; // Default options var defaultGutterFn = function (i, gutterDirection) { var gut = document.createElement('div'); gut.className = "gutter gutter-" + gutterDirection; return gut }; var defaultElementStyleFn = function (dim, size, gutSize) { var style = {}; if (!isString(size)) { style[dim] = calc + "(" + size + "% - " + gutSize + "px)"; } else { style[dim] = size; } return style }; var defaultGutterStyleFn = function (dim, gutSize) { var obj; return (( obj = {}, obj[dim] = (gutSize + "px"), obj )); }; // The main function to initialize a split. Split.js thinks about each pair // of elements as an independant pair. Dragging the gutter between two elements // only changes the dimensions of elements in that pair. This is key to understanding // how the following functions operate, since each function is bound to a pair. // // A pair object is shaped like this: // // { // a: DOM element, // b: DOM element, // aMin: Number, // bMin: Number, // dragging: Boolean, // parent: DOM element, // direction: 'horizontal' | 'vertical' // } // // The basic sequence: // // 1. Set defaults to something sane. `options` doesn't have to be passed at all. // 2. Initialize a bunch of strings based on the direction we're splitting. // A lot of the behavior in the rest of the library is paramatized down to // rely on CSS strings and classes. // 3. Define the dragging helper functions, and a few helpers to go with them. // 4. Loop through the elements while pairing them off. Every pair gets an // `pair` object and a gutter. // 5. Actually size the pair elements, insert gutters and attach event listeners. var Split = function (idsOption, options) { if ( options === void 0 ) options = {}; if (ssr) { return {} } var ids = idsOption; var dimension; var clientAxis; var position; var positionEnd; var clientSize; var elements; // Allow HTMLCollection to be used as an argument when supported if (Array.from) { ids = Array.from(ids); } // All DOM elements in the split should have a common parent. We can grab // the first elements parent and hope users read the docs because the // behavior will be whacky otherwise. var firstElement = elementOrSelector(ids[0]); var parent = firstElement.parentNode; var parentStyle = getComputedStyle ? getComputedStyle(parent) : null; var parentFlexDirection = parentStyle ? parentStyle.flexDirection : null; // Set default options.sizes to equal percentages of the parent element. var sizes = getOption(options, 'sizes') || ids.map(function () { return 100 / ids.length; }); // Standardize minSize to an array if it isn't already. This allows minSize // to be passed as a number. var minSize = getOption(options, 'minSize', 100); var minSizes = Array.isArray(minSize) ? minSize : ids.map(function () { return minSize; }); // Get other options var expandToMin = getOption(options, 'expandToMin', false); var gutterSize = getOption(options, 'gutterSize', 10); var gutterAlign = getOption(options, 'gutterAlign', 'center'); var snapOffset = getOption(options, 'snapOffset', 30); var dragInterval = getOption(options, 'dragInterval', 1); var direction = getOption(options, 'direction', HORIZONTAL); var cursor = getOption( options, 'cursor', direction === HORIZONTAL ? 'col-resize' : 'row-resize' ); var gutter = getOption(options, 'gutter', defaultGutterFn); var elementStyle = getOption( options, 'elementStyle', defaultElementStyleFn ); var gutterStyle = getOption(options, 'gutterStyle', defaultGutterStyleFn); // 2. Initialize a bunch of strings based on the direction we're splitting. // A lot of the behavior in the rest of the library is paramatized down to // rely on CSS strings and classes. if (direction === HORIZONTAL) { dimension = 'width'; clientAxis = 'clientX'; position = 'left'; positionEnd = 'right'; clientSize = 'clientWidth'; } else if (direction === 'vertical') { dimension = 'height'; clientAxis = 'clientY'; position = 'top'; positionEnd = 'bottom'; clientSize = 'clientHeight'; } // 3. Define the dragging helper functions, and a few helpers to go with them. // Each helper is bound to a pair object that contains its metadata. This // also makes it easy to store references to listeners that that will be // added and removed. // // Even though there are no other functions contained in them, aliasing // this to self saves 50 bytes or so since it's used so frequently. // // The pair object saves metadata like dragging state, position and // event listener references. function setElementSize(el, size, gutSize, i) { // Split.js allows setting sizes via numbers (ideally), or if you must, // by string, like '300px'. This is less than ideal, because it breaks // the fluid layout that `calc(% - px)` provides. You're on your own if you do that, // make sure you calculate the gutter size by hand. var style = elementStyle(dimension, size, gutSize, i); Object.keys(style).forEach(function (prop) { // eslint-disable-next-line no-param-reassign el.style[prop] = style[prop]; }); } function setGutterSize(gutterElement, gutSize, i) { var style = gutterStyle(dimension, gutSize, i); Object.keys(style).forEach(function (prop) { // eslint-disable-next-line no-param-reassign gutterElement.style[prop] = style[prop]; }); } function getSizes() { return elements.map(function (element) { return element.size; }) } // Supports touch events, but not multitouch, so only the first // finger `touches[0]` is counted. function getMousePosition(e) { if ('touches' in e) { return e.touches[0][clientAxis] } return e[clientAxis] } // Actually adjust the size of elements `a` and `b` to `offset` while dragging. // calc is used to allow calc(percentage + gutterpx) on the whole split instance, // which allows the viewport to be resized without additional logic. // Element a's size is the same as offset. b's size is total size - a size. // Both sizes are calculated from the initial parent percentage, // then the gutter size is subtracted. function adjust(offset) { var a = elements[this.a]; var b = elements[this.b]; var percentage = a.size + b.size; a.size = (offset / this.size) * percentage; b.size = percentage - (offset / this.size) * percentage; setElementSize(a.element, a.size, this[aGutterSize], a.i); setElementSize(b.element, b.size, this[bGutterSize], b.i); } // drag, where all the magic happens. The logic is really quite simple: // // 1. Ignore if the pair is not dragging. // 2. Get the offset of the event. // 3. Snap offset to min if within snappable range (within min + snapOffset). // 4. Actually adjust each element in the pair to offset. // // --------------------------------------------------------------------- // | | <- a.minSize || b.minSize -> | | // | | | <- this.snapOffset || this.snapOffset -> | | | // | | | || | | | // | | | || | | | // --------------------------------------------------------------------- // | <- this.start this.size -> | function drag(e) { var offset; var a = elements[this.a]; var b = elements[this.b]; if (!this.dragging) { return } // Get the offset of the event from the first side of the // pair `this.start`. Then offset by the initial position of the // mouse compared to the gutter size. offset = getMousePosition(e) - this.start + (this[aGutterSize] - this.dragOffset); if (dragInterval > 1) { offset = Math.round(offset / dragInterval) * dragInterval; } // If within snapOffset of min or max, set offset to min or max. // snapOffset buffers a.minSize and b.minSize, so logic is opposite for both. // Include the appropriate gutter sizes to prevent overflows. if (offset <= a.minSize + snapOffset + this[aGutterSize]) { offset = a.minSize + this[aGutterSize]; } else if ( offset >= this.size - (b.minSize + snapOffset + this[bGutterSize]) ) { offset = this.size - (b.minSize + this[bGutterSize]); } // Actually adjust the size. adjust.call(this, offset); // Call the drag callback continously. Don't do anything too intensive // in this callback. getOption(options, 'onDrag', NOOP)(getSizes()); } // Cache some important sizes when drag starts, so we don't have to do that // continously: // // `size`: The total size of the pair. First + second + first gutter + second gutter. // `start`: The leading side of the first element. // // ------------------------------------------------ // | aGutterSize -> ||| | // | ||| | // | ||| | // | ||| <- bGutterSize | // ------------------------------------------------ // | <- start size -> | function calculateSizes() { // Figure out the parent size minus padding. var a = elements[this.a].element; var b = elements[this.b].element; var aBounds = a[getBoundingClientRect](); var bBounds = b[getBoundingClientRect](); this.size = aBounds[dimension] + bBounds[dimension] + this[aGutterSize] + this[bGutterSize]; this.start = aBounds[position]; this.end = aBounds[positionEnd]; } function innerSize(element) { // Return nothing if getComputedStyle is not supported (< IE9) // Or if parent element has no layout yet if (!getComputedStyle) { return null } var computedStyle = getComputedStyle(element); if (!computedStyle) { return null } var size = element[clientSize]; if (size === 0) { return null } if (direction === HORIZONTAL) { size -= parseFloat(computedStyle.paddingLeft) + parseFloat(computedStyle.paddingRight); } else { size -= parseFloat(computedStyle.paddingTop) + parseFloat(computedStyle.paddingBottom); } return size } // When specifying percentage sizes that are less than the computed // size of the element minus the gutter, the lesser percentages must be increased // (and decreased from the other elements) to make space for the pixels // subtracted by the gutters. function trimToMin(sizesToTrim) { // Try to get inner size of parent element. // If it's no supported, return original sizes. var parentSize = innerSize(parent); if (parentSize === null) { return sizesToTrim } if (minSizes.reduce(function (a, b) { return a + b; }, 0) > parentSize) { return sizesToTrim } // Keep track of the excess pixels, the amount of pixels over the desired percentage // Also keep track of the elements with pixels to spare, to decrease after if needed var excessPixels = 0; var toSpare = []; var pixelSizes = sizesToTrim.map(function (size, i) { // Convert requested percentages to pixel sizes var pixelSize = (parentSize * size) / 100; var elementGutterSize = getGutterSize( gutterSize, i === 0, i === sizesToTrim.length - 1, gutterAlign ); var elementMinSize = minSizes[i] + elementGutterSize; // If element is too smal, increase excess pixels by the difference // and mark that it has no pixels to spare if (pixelSize < elementMinSize) { excessPixels += elementMinSize - pixelSize; toSpare.push(0); return elementMinSize } // Otherwise, mark the pixels it has to spare and return it's original size toSpare.push(pixelSize - elementMinSize); return pixelSize }); // If nothing was adjusted, return the original sizes if (excessPixels === 0) { return sizesToTrim } return pixelSizes.map(function (pixelSize, i) { var newPixelSize = pixelSize; // While there's still pixels to take, and there's enough pixels to spare, // take as many as possible up to the total excess pixels if (excessPixels > 0 && toSpare[i] - excessPixels > 0) { var takenPixels = Math.min( excessPixels, toSpare[i] - excessPixels ); // Subtract the amount taken for the next iteration excessPixels -= takenPixels; newPixelSize = pixelSize - takenPixels; } // Return the pixel size adjusted as a percentage return (newPixelSize / parentSize) * 100 }) } // stopDragging is very similar to startDragging in reverse. function stopDragging() { var self = this; var a = elements[self.a].element; var b = elements[self.b].element; if (self.dragging) { getOption(options, 'onDragEnd', NOOP)(getSizes()); } self.dragging = false; // Remove the stored event listeners. This is why we store them. global[removeEventListener]('mouseup', self.stop); global[removeEventListener]('touchend', self.stop); global[removeEventListener]('touchcancel', self.stop); global[removeEventListener]('mousemove', self.move); global[removeEventListener]('touchmove', self.move); // Clear bound function references self.stop = null; self.move = null; a[removeEventListener]('selectstart', NOOP); a[removeEventListener]('dragstart', NOOP); b[removeEventListener]('selectstart', NOOP); b[removeEventListener]('dragstart', NOOP); a.style.userSelect = ''; a.style.webkitUserSelect = ''; a.style.MozUserSelect = ''; a.style.pointerEvents = ''; b.style.userSelect = ''; b.style.webkitUserSelect = ''; b.style.MozUserSelect = ''; b.style.pointerEvents = ''; self.gutter.style.cursor = ''; self.parent.style.cursor = ''; document.body.style.cursor = ''; } // startDragging calls `calculateSizes` to store the inital size in the pair object. // It also adds event listeners for mouse/touch events, // and prevents selection while dragging so avoid the selecting text. function startDragging(e) { // Right-clicking can't start dragging. if ('button' in e && e.button !== 0) { return } // Alias frequently used variables to save space. 200 bytes. var self = this; var a = elements[self.a].element; var b = elements[self.b].element; // Call the onDragStart callback. if (!self.dragging) { getOption(options, 'onDragStart', NOOP)(getSizes()); } // Don't actually drag the element. We emulate that in the drag function. e.preventDefault(); // Set the dragging property of the pair object. self.dragging = true; // Create two event listeners bound to the same pair object and store // them in the pair object. self.move = drag.bind(self); self.stop = stopDragging.bind(self); // All the binding. `window` gets the stop events in case we drag out of the elements. global[addEventListener]('mouseup', self.stop); global[addEventListener]('touchend', self.stop); global[addEventListener]('touchcancel', self.stop); global[addEventListener]('mousemove', self.move); global[addEventListener]('touchmove', self.move); // Disable selection. Disable! a[addEventListener]('selectstart', NOOP); a[addEventListener]('dragstart', NOOP); b[addEventListener]('selectstart', NOOP); b[addEventListener]('dragstart', NOOP); a.style.userSelect = 'none'; a.style.webkitUserSelect = 'none'; a.style.MozUserSelect = 'none'; a.style.pointerEvents = 'none'; b.style.userSelect = 'none'; b.style.webkitUserSelect = 'none'; b.style.MozUserSelect = 'none'; b.style.pointerEvents = 'none'; // Set the cursor at multiple levels self.gutter.style.cursor = cursor; self.parent.style.cursor = cursor; document.body.style.cursor = cursor; // Cache the initial sizes of the pair. calculateSizes.call(self); // Determine the position of the mouse compared to the gutter self.dragOffset = getMousePosition(e) - self.end; } // adjust sizes to ensure percentage is within min size and gutter. sizes = trimToMin(sizes); // 5. Create pair and element objects. Each pair has an index reference to // elements `a` and `b` of the pair (first and second elements). // Loop through the elements while pairing them off. Every pair gets a // `pair` object and a gutter. // // Basic logic: // // - Starting with the second element `i > 0`, create `pair` objects with // `a = i - 1` and `b = i` // - Set gutter sizes based on the _pair_ being first/last. The first and last // pair have gutterSize / 2, since they only have one half gutter, and not two. // - Create gutter elements and add event listeners. // - Set the size of the elements, minus the gutter sizes. // // ----------------------------------------------------------------------- // | i=0 | i=1 | i=2 | i=3 | // | | | | | // | pair 0 pair 1 pair 2 | // | | | | | // ----------------------------------------------------------------------- var pairs = []; elements = ids.map(function (id, i) { // Create the element object. var element = { element: elementOrSelector(id), size: sizes[i], minSize: minSizes[i], i: i, }; var pair; if (i > 0) { // Create the pair object with its metadata. pair = { a: i - 1, b: i, dragging: false, direction: direction, parent: parent, }; pair[aGutterSize] = getGutterSize( gutterSize, i - 1 === 0, false, gutterAlign ); pair[bGutterSize] = getGutterSize( gutterSize, false, i === ids.length - 1, gutterAlign ); // if the parent has a reverse flex-direction, switch the pair elements. if ( parentFlexDirection === 'row-reverse' || parentFlexDirection === 'column-reverse' ) { var temp = pair.a; pair.a = pair.b; pair.b = temp; } } // Determine the size of the current element. IE8 is supported by // staticly assigning sizes without draggable gutters. Assigns a string // to `size`. // // Create gutter elements for each pair. if (i > 0) { var gutterElement = gutter(i, direction, element.element); setGutterSize(gutterElement, gutterSize, i); // Save bound event listener for removal later pair[gutterStartDragging] = startDragging.bind(pair); // Attach bound event listener gutterElement[addEventListener]( 'mousedown', pair[gutterStartDragging] ); gutterElement[addEventListener]( 'touchstart', pair[gutterStartDragging] ); parent.insertBefore(gutterElement, element.element); pair.gutter = gutterElement; } setElementSize( element.element, element.size, getGutterSize( gutterSize, i === 0, i === ids.length - 1, gutterAlign ), i ); // After the first iteration, and we have a pair object, append it to the // list of pairs. if (i > 0) { pairs.push(pair); } return element }); function adjustToMin(element) { var isLast = element.i === pairs.length; var pair = isLast ? pairs[element.i - 1] : pairs[element.i]; calculateSizes.call(pair); var size = isLast ? pair.size - element.minSize - pair[bGutterSize] : element.minSize + pair[aGutterSize]; adjust.call(pair, size); } elements.forEach(function (element) { var computedSize = element.element[getBoundingClientRect]()[dimension]; if (computedSize < element.minSize) { if (expandToMin) { adjustToMin(element); } else { // eslint-disable-next-line no-param-reassign element.minSize = computedSize; } } }); function setSizes(newSizes) { var trimmed = trimToMin(newSizes); trimmed.forEach(function (newSize, i) { if (i > 0) { var pair = pairs[i - 1]; var a = elements[pair.a]; var b = elements[pair.b]; a.size = trimmed[i - 1]; b.size = newSize; setElementSize(a.element, a.size, pair[aGutterSize], a.i); setElementSize(b.element, b.size, pair[bGutterSize], b.i); } }); } function destroy(preserveStyles, preserveGutter) { pairs.forEach(function (pair) { if (preserveGutter !== true) { pair.parent.removeChild(pair.gutter); } else { pair.gutter[removeEventListener]( 'mousedown', pair[gutterStartDragging] ); pair.gutter[removeEventListener]( 'touchstart', pair[gutterStartDragging] ); } if (preserveStyles !== true) { var style = elementStyle( dimension, pair.a.size, pair[aGutterSize] ); Object.keys(style).forEach(function (prop) { elements[pair.a].element.style[prop] = ''; elements[pair.b].element.style[prop] = ''; }); } }); } return { setSizes: setSizes, getSizes: getSizes, collapse: function collapse(i) { adjustToMin(elements[i]); }, destroy: destroy, parent: parent, pairs: pairs, } }; return Split; })));