/*!
* jQuery Enhanced Splitter Plugin
* Main ECMAScript File
* Version 1.2.3
*
* https://github.com/hiltonjanfield/jquery.enhsplitter
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see .
*/
(function ($) {
var splitterCount = 0;
var splitters = [];
var currentSplitter = null; // Reference to current splitter during events.
var dragStartPosition = null;
var disableClick = false;
$.fn.enhsplitter = function (options, arg1) {
var data = this.data('splitter');
if (data) {
if (typeof options === 'string') {
currentSplitter = data;
if (options == 'move') {
currentSplitter.setPosition(currentSplitter.translatePosition(arg1));
currentSplitter = null;
return this;
} else if (options == 'refresh') {
if (!currentSplitter.refresh()) {
currentSplitter.setPosition(currentSplitter.currentPosition);
}
currentSplitter = null;
return this;
} else if (options == 'reset') {
currentSplitter.setPosition(currentSplitter.translatePosition(currentSplitter.settings.position));
currentSplitter = null;
return this;
} else if (options == 'collapse') {
currentSplitter.data('savedPosition', currentSplitter.currentPosition);
if (currentSplitter.settings.collapseNormal) {
currentSplitter.setPosition(currentSplitter.settings.leftMinSize);
} else {
currentSplitter.setPosition(currentSplitter.containerSize - currentSplitter.settings.rightMinSize);
}
currentSplitter = null;
return this;
} else if (options == 'uncollapse') {
var saved = currentSplitter.data('savedPosition');
currentSplitter.setPosition(saved || currentSplitter.settings.position);
currentSplitter.data('savedPosition', null);
currentSplitter = null;
return this;
} else if (options == 'visible') {
currentSplitter.settings.invisible = !arg1;
var splitterBar = currentSplitter.children('.splitter_bar');
if (currentSplitter.settings.invisible) {
splitterBar.addClass('splitter-invisible');
currentSplitter.splitterSize = 0;
currentSplitter.splitterSizeHalf = 0;
} else {
splitterBar.removeClass('splitter-invisible');
currentSplitter.splitterSize = (currentSplitter.settings.vertical ? splitterBar.outerWidth() : splitterBar.outerHeight());
currentSplitter.splitterSizeHalf = (currentSplitter.settings.vertical ? splitterBar.outerWidth() / 2 : splitterBar.outerHeight() / 2);
}
currentSplitter.setPosition(currentSplitter.currentPosition);
currentSplitter = null;
return this;
} else if (options == 'handle') {
splitterBar = currentSplitter.children('.splitter_bar');
if (!arg1) {
arg1 = 'none';
}
$.each(splitterBar[0].classList, function (k, v) {
if (v.indexOf('splitter-handle-') !== -1) {
splitterBar.removeClass(v);
}
});
splitterBar.addClass('splitter-handle-' + arg1);
currentSplitter = null;
return this;
} else if (options == 'remove') {
currentSplitter.destroy();
currentSplitter = null;
return true;
}
return false;
}
return data;
}
var settings = $.extend({}, $.fn.enhsplitter.defaults, options);
var id = splitterCount++;
var self;
var panelOne;
var panelTwo;
var splitter;
var handle;
// Wrap the existing child elements in invisible panels. These prevent unknown CSS from messing with the
// positioning code - padding, borders, etc. easily break the splitter.
panelOne = $('
')
.append(this.children().first().detach())
.prependTo(this);
panelTwo = $('')
.append(panelOne.next().detach())
.insertAfter(panelOne);
if (settings.vertical) {
this.addClass('splitter_container splitter-vertical');
} else {
this.addClass('splitter_container splitter-horizontal');
}
// Check for an empty container height (happens when height on the parent has not been set), and fix.
// Some weirdness has sometimes resulted in panel heights of 0.33333px, hence the < 1 check rather than == 0.
if (!settings.height && this.height() < 1) {
settings.height = '10em';
}
if (settings.height) {
this.css('height', settings.height);
}
var containerSize = settings.vertical ? this.width() : this.height();
if (typeof settings.minSize !== 'undefined') { settings.leftMinSize = settings.rightMinSize = settings.minSize; }
if (typeof settings.maxSize !== 'undefined') { settings.leftMaxSize = settings.rightMaxSize = settings.maxSize; }
// If left/right not defined, check for top/bottom and use them instead. (if all defined for some reason, left/right get precedence)
settings.leftMinSize = ((settings.leftMinSize === null) && settings.topMinSize) ? settings.topMinSize : settings.leftMinSize;
settings.leftMaxSize = ((settings.leftMaxSize === null) && settings.topMaxSize) ? settings.topMaxSize : settings.leftMaxSize;
settings.rightMinSize = ((settings.rightMinSize === null) && settings.bottomMinSize) ? settings.bottomMinSize : settings.rightMinSize;
settings.rightMaxSize = ((settings.rightMaxSize === null) && settings.bottomMaxSize) ? settings.bottomMaxSize : settings.rightMaxSize;
// Verify ranges.
settings.leftMinSize = (settings.leftMinSize < 0 ? 0 : (settings.leftMinSize > containerSize ? containerSize : settings.leftMinSize));
settings.leftMaxSize = (settings.leftMaxSize < 0 ? 0 : (settings.leftMaxSize > containerSize ? containerSize : settings.leftMaxSize));
settings.rightMinSize = (settings.rightMinSize < 0 ? 0 : (settings.rightMinSize > containerSize ? containerSize : settings.rightMinSize));
settings.rightMaxSize = (settings.rightMaxSize < 0 ? 0 : (settings.rightMaxSize > containerSize ? containerSize : settings.rightMaxSize));
// Verify minimum sizes.
var totalSize = settings.leftMinSize + settings.rightMinSize;
if (totalSize > containerSize) {
// User has set the left and right minimums too high. Proportionally reduce them.
settings.leftMinSize = containerSize * (settings.leftMinSize / totalSize);
settings.rightMinSize = containerSize * (settings.rightMinSize / totalSize);
}
settings.collapseNormal = !(settings.collapse == 'right' || settings.collapse == 'down');
settings.collapsable = !(settings.collapse == 'none');
if (!settings.collapsable) {
this.addClass('splitter-handle-disabled');
}
if (settings.fixed) {
this.addClass('splitter-fixed');
}
splitter = $('')
.insertAfter(panelOne);
handle = $('')
.appendTo(splitter);
if (settings.invisible) {
splitter.addClass('splitter-invisible');
}
// Option to override CSS for width. Useful in conjunction with {invisible: true} or {handle: none}.
if (settings.splitterSize) {
splitter.css(settings.vertical ? 'width' : 'height', settings.splitterSize);
}
self = $.extend(this, {
currentPosition: 0,
containerSize: containerSize,
splitterSize: settings.invisible ? 0 : (settings.vertical ? splitter.outerWidth() : splitter.outerHeight()),
splitterSizeHalf: settings.invisible ? 0 : (settings.vertical ? splitter.outerWidth() / 2 : splitter.outerHeight() / 2),
refresh: function () {
var newSize = self.settings.vertical ? self.width() : self.height();
if (self.containerSize != newSize) {
self.containerSize = newSize;
self.setPosition(self.currentPosition);
return true;
}
return false;
},
setPosition: function (newPos) {
if (newPos <= settings.leftMinSize) {
newPos = settings.leftMinSize;
} else if (newPos >= self.containerSize - settings.rightMinSize - self.splitterSize) {
newPos = self.containerSize - settings.rightMinSize - self.splitterSize;
}
if (settings.leftMaxSize !== null && newPos >= settings.leftMaxSize) {
newPos = settings.leftMaxSize;
} else if (settings.rightMaxSize !== null && newPos <= self.containerSize - settings.rightMaxSize - self.splitterSize) {
newPos = self.containerSize - settings.rightMaxSize - self.splitterSize;
}
self.currentPosition = newPos;
if (self.settings.vertical) {
panelOne.outerWidth(newPos);
panelTwo.outerWidth(self.containerSize - newPos - self.splitterSize);
splitter.css('left', self.settings.invisible ? newPos - self.splitterSizeHalf : newPos);
} else {
panelOne.outerHeight(newPos);
panelTwo.outerHeight(self.containerSize - newPos - self.splitterSize);
splitter.css('top', self.settings.invisible ? newPos - self.splitterSizeHalf : newPos);
}
return self;
},
translatePosition: function (position) {
//TODO: Consider replacing this with a more robust function that can accept any CSS value, such as Length.js at https://github.com/heygrady/Units
// Currently valid parameter examples: 500, '500', '500px', '50%', 12.34, '12.34', '12.34px', '12.34%'
if (typeof position === 'number') {
return position;
} else if (typeof position === 'string') {
var match = position.match(/^([0-9\.]+)(px|%)?$/);
if (match) {
if (match[2] && match[2] == '%') {
var splitter = currentSplitter || self;
return (splitter.containerSize * +match[1]) / 100;
}
return +match[1]; // assume pixels for ANY suffix except '%', or lack thereof.
} else {
throw 'Invalid parameter: self.translatePosition(' + position + ') - bad string (only numbers allowed, with optional suffixes "px" or "%")';
}
} else {
throw 'Invalid parameter: self.translatePosition(' + position + ') - bad type (only string/number allowed)';
}
},
destroy: function () {
self.removeClass('splitter_container');
panelOne.before(panelOne.children().first().detach()).remove();
panelTwo.before(panelTwo.children().first().detach()).remove();
splitters.splice(id, 1);
splitterCount--;
splitter.remove();
var not_null = false;
for (var i = splitters.length; i--;) {
if (splitters[i] !== null) {
not_null = true;
break;
}
}
//remove document events when no splitters
if (!not_null) {
$(document.documentElement).off('.splitter');
$(window).off('.splitter');
self.data('splitter', null);
splitters = [];
splitterCount = 0;
}
}
});
// If this is the first splitter, set up our events.
if (splitters.length == 0) {
$(window)
.on('resize.splitter', function () {
$.each(splitters, function (i, splitter) {
splitter.refresh();
});
});
$(document.documentElement)
.on('click.splitter', '.splitter_handle', function (e) {
// Prevent clicks if the user started dragging too much.
// Some (all?) browsers fire the click event even after the bar has been dragged hundreds of pixels.
if (disableClick) {
return disableClick = false;
}
currentSplitter = $(this).closest('.splitter_container').data('splitter');
if (currentSplitter.settings.collapsable) {
if (currentSplitter.data('savedPosition')) {
// Saved position found; restore.
currentSplitter.setPosition(currentSplitter.data('savedPosition'));
currentSplitter.data('savedPosition', null);
} else {
// Save current position and collapse.
currentSplitter.data('savedPosition', currentSplitter.currentPosition);
if (currentSplitter.settings.collapseNormal) {
currentSplitter.setPosition(currentSplitter.settings.leftMinSize);
} else {
currentSplitter.setPosition(currentSplitter.containerSize - currentSplitter.settings.rightMinSize);
}
}
currentSplitter.find('.splitter_panel').trigger('resize.splitter');
e.preventDefault();
$('.splitter_mask').remove();
currentSplitter.settings.onDrag(e, currentSplitter);
}
currentSplitter.removeClass('splitter-active');
currentSplitter = null;
})
.on('mousedown.splitter', '.splitter_handle', function (e) {
e.preventDefault();
if (currentSplitter === null) {
$(this).closest('.splitter_bar').trigger('mousedown');
}
// Two separate comparisons on purpose. .trigger() may or may not set currentSplitter.
if (currentSplitter !== null) {
dragStartPosition = (currentSplitter.settings.vertical) ? e.pageX : e.pageY;
}
})
.on('mousedown.splitter touchstart.splitter', '.splitter_container > .splitter_bar', function (e) {
e.preventDefault();
currentSplitter = $(this).closest('.splitter_container').data('splitter');
if (currentSplitter.settings.fixed) {
currentSplitter = null;
} else {
currentSplitter.addClass('splitter-active');
$('').css('cursor', currentSplitter.children().eq(1).css('cursor')).insertAfter(currentSplitter);
currentSplitter.settings.onDragStart(e, currentSplitter);
}
})
.on('mouseup.splitter touchend.splitter touchleave.splitter touchcancel.splitter', '.splitter_mask, .splitter_container > .splitter_bar', function (e) {
if (currentSplitter) {
e.preventDefault();
dragStartPosition = null;
// If the slider is dropped near it's collapsed position, set a saved position back to its
// original start position so the collapse handle works at least somewhat properly.
if (!currentSplitter.data('savedPosition')) {
if (currentSplitter.settings.collapseNormal) {
if (currentSplitter.currentPosition <= (currentSplitter.settings.leftMinSize + 5)) {
currentSplitter.data('savedPosition', self.translatePosition(currentSplitter.settings.position));
disableClick = false;
}
} else {
if (currentSplitter.currentPosition >= (currentSplitter.containerSize - currentSplitter.settings.rightMinSize - currentSplitter.splitterSize - 5)) {
currentSplitter.data('savedPosition', self.translatePosition(currentSplitter.settings.position));
disableClick = false;
}
}
}
$('.splitter_mask').remove();
currentSplitter.settings.onDragEnd(e, currentSplitter);
currentSplitter.removeClass('splitter-active');
currentSplitter = null;
}
})
.on('mousemove.splitter touchmove.splitter', '.splitter_mask, .splitter_bar', function (e) {
if (currentSplitter !== null) {
currentSplitter.data('savedPosition', null);
var position = (currentSplitter.settings.vertical) ? e.pageX : e.pageY;
if (e.originalEvent && e.originalEvent.changedTouches) {
position = (currentSplitter.settings.vertical) ? e.originalEvent.changedTouches[0].pageX : e.originalEvent.changedTouches[0].pageY;
}
// If the user started the drag with a mousedown on the handle, give it a 5-pixel delay.
if (dragStartPosition !== null) {
if (position > (dragStartPosition + 5) || position < (dragStartPosition - 5)) {
dragStartPosition = null;
disableClick = true;
} else {
e.preventDefault();
return false;
}
}
if (currentSplitter.settings.vertical) {
currentSplitter.setPosition(position - currentSplitter.offset().left - currentSplitter.splitterSize + 1);
} else {
currentSplitter.setPosition(position - currentSplitter.offset().top - currentSplitter.splitterSize + 1);
}
e.preventDefault();
currentSplitter.settings.onDrag(e, currentSplitter);
}
}
);
}
self.settings = settings;
// Set the initial position of the splitter.
self.setPosition(self.translatePosition(settings.position));
self.data('splitter', self);
splitters.push(self);
return self;
};
$.fn.enhsplitter.defaults = {
vertical: true,
position: '50%',
leftMinSize: 100,
leftMaxSize: null,
rightMinSize: 100,
rightMaxSize: null,
invisible: false,
handle: 'default',
fixed: false,
collapse: 'left',
height: null,
splitterSize: null,
onDragStart: $.noop,
onDragEnd: $.noop,
onDrag: $.noop
};
})
(jQuery);