new SplitPanel implementation

This commit is contained in:
rolux 2014-08-21 12:14:30 +02:00
parent 9001a45aa1
commit 835c4c92b7
2 changed files with 249 additions and 311 deletions

View file

@ -8,6 +8,7 @@ Ox.Resizebar <f> Resizebar
elements <a|[]> Elements of the bar
orientation <s|horizontal> Orientation ('horizontal' or 'vertical')
panel <o|null> Panel object
resizeable <b|true> If true, can be resetted to default or original size
resizeable <b|true> If true, can be resized
resize <a|[]> Array of sizes
size <n|0> Default size
@ -21,12 +22,12 @@ Ox.Resizebar = function(options, self) {
var that = Ox.Element({}, self)
.defaults({
collapsed: false,
collapsible: true,
collapsible: false,
defaultSize: null,
edge: 'left',
elements: [],
orientation: 'horizontal',
parent: null,
resizable: true,
resettable: false,
resizable: false,
resize: [],
size: 0,
tooltip: false
@ -35,98 +36,42 @@ Ox.Resizebar = function(options, self) {
.update({
collapsed: function() {
that.css({cursor: getCursor()});
self.$tooltip && self.$tooltip.options({title: getTitle()});
self.$tooltip && self.$tooltip.options({title: getTooltipTitle()});
}
})
.addClass('OxResizebar Ox' + Ox.toTitleCase(self.options.orientation))
.bindEvent({
// singleclick: toggle,
// doubleclick: reset,
anyclick: toggle,
dragstart: dragstart,
drag: drag,
dragend: dragend,
mousedown: function() {
triggerEvents('resizestart');
},
mouseup: function() {
triggerEvents('resizeend');
}
})
.append($('<div>').addClass('OxSpace'))
.append($('<div>').addClass('OxLine'))
.append($('<div>').addClass('OxSpace'));
.bindEvent(Ox.extend({
dragstart: onDragstart,
drag: onDrag,
dragpause: onDragpause,
dragend: onDragend
}, self.options.resettable ? {
doubleclick: reset,
singleclick: toggle
} : {
anyclick: toggle
}))
.append(Ox.$('<div>').addClass('OxSpace'))
.append(Ox.$('<div>').addClass('OxLine'))
.append(Ox.$('<div>').addClass('OxSpace'));
if (self.options.tooltip) {
if (Ox.isString(self.options.tooltip)) {
// FIXME: Use Ox.Element's tooltip
self.$tooltip = Ox.Tooltip({title: getTitle()});
self.$tooltip = Ox.Tooltip({title: getTooltipTitle()});
that.on({
mouseenter: self.$tooltip.show,
mouseleave: self.$tooltip.hide
});
}
Ox.extend(self, {
clientXY: self.options.orientation == 'horizontal' ? 'clientY' : 'clientX',
dimensions: Ox.UI.DIMENSIONS[self.options.orientation], // fixme: should orientation be the opposite orientation here?
edges: Ox.UI.EDGES[self.options.orientation],
isLeftOrTop: self.options.edge == 'left' || self.options.edge == 'top'
});
self.clientXY = self.options.orientation == 'horizontal'
? 'clientY' : 'clientX';
self.dimensions = Ox.UI.DIMENSIONS[self.options.orientation];
self.edges = Ox.UI.EDGES[self.options.orientation];
self.isLeftOrTop = self.options.edge == 'left' || self.options.edge == 'top';
that.css({cursor: getCursor()});
function dragstart(data) {
if (self.options.resizable && !self.options.collapsed) {
Ox.$body.addClass('OxDragging');
self.drag = {
startPos: data[self.clientXY],
startSize: self.options.size
}
}
}
function drag(data) {
if (self.options.resizable && !self.options.collapsed) {
var d = data[self.clientXY] - self.drag.startPos,
size = self.options.size;
self.options.size = Ox.limit(
self.drag.startSize + d * (self.isLeftOrTop ? 1 : -1),
self.options.resize[0],
self.options.resize[self.options.resize.length - 1]
);
Ox.forEach(self.options.resize, function(v) {
if (self.options.size >= v - 8 && self.options.size <= v + 8) {
self.options.size = v;
return false; // break
}
});
if (self.options.size != size) {
that.css(self.edges[self.isLeftOrTop ? 2 : 3], self.options.size + 'px');
// fixme: send {size: x}, not x
if (self.isLeftOrTop) {
self.options.elements[0]
.css(self.dimensions[1], self.options.size + 'px');
self.options.elements[1]
.css(self.edges[2], (self.options.size + 1) + 'px');
} else {
self.options.elements[0]
.css(self.edges[3], (self.options.size + 1) + 'px');
self.options.elements[1]
.css(self.dimensions[1], self.options.size + 'px');
}
triggerEvents('resize');
self.options.parent.updateSize(self.isLeftOrTop ? 0 : 1, self.options.size); // fixme: listen to event instead?
}
}
}
function dragend() {
if (self.options.resizable && !self.options.collapsed) {
Ox.$body.removeClass('OxDragging');
self.options.size != self.drag.startSize && triggerEvents('resizeend');
}
}
function getCursor() {
var cursor = '';
if (self.options.collapsed) {
@ -146,7 +91,7 @@ Ox.Resizebar = function(options, self) {
return cursor + '-resize';
}
function getTitle() {
function getTooltipTitle() {
var title = '';
if (self.options.collapsed) {
title = Ox._('Click to show');
@ -156,54 +101,94 @@ Ox.Resizebar = function(options, self) {
}
if (self.options.collapsible) {
title = title
? Ox._('{0} or click to hide', [title])
? Ox._('{0}{1} click to hide', [
title, self.options.resettable ? ',' : ' or'
])
: Ox._('Click to hide');
}
if (self.options.resettable) {
title += ' or doubleclick to reset'
}
if (title && Ox.isString(self.options.tooltip)) {
}
if (title && self.options.tooltip) {
title += ' ' + self.options.tooltip;
}
return title;
}
function onDragstart(data) {
if (self.options.resizable && !self.options.collapsed) {
Ox.$body.addClass('OxDragging');
self.drag = {
startPos: data[self.clientXY],
startSize: self.options.size
}
}
that.triggerEvent('resizestart', {size: self.options.size});
}
function onDrag(data) {
if (self.options.resizable && !self.options.collapsed) {
var delta = data[self.clientXY] - self.drag.startPos,
size = self.options.size;
self.options.size = Ox.limit(
self.drag.startSize + delta * (self.isLeftOrTop ? 1 : -1),
self.options.resize[0],
self.options.resize[self.options.resize.length - 1]
);
Ox.forEach(self.options.resize, function(value) {
if (
self.options.size >= value - 8
&& self.options.size <= value + 8
) {
self.options.size = value;
return false; // break
}
});
if (self.options.size != size) {
that.css(
self.edges[self.isLeftOrTop ? 2 : 3],
self.options.size + 'px'
)
.triggerEvent('resize', {size: self.options.size});
}
}
}
function onDragpause() {
if (self.options.resizable && !self.options.collapsed) {
if (self.options.size != self.drag.startSize) {
that.triggerEvent('resizepause', {size: self.options.size});
}
}
}
function onDragend() {
if (self.options.resizable && !self.options.collapsed) {
Ox.$body.removeClass('OxDragging');
if (self.options.size != self.drag.startSize) {
that.triggerEvent('resizeend', {size: self.options.size});
}
}
}
function reset() {
if (self.options.resizable && !self.options.collapsed) {
// fixme: silly, pass an option
self.options.parent.reset(
self.isLeftOrTop ? 0
: self.options.parent.options('elements').length - 1
);
that.triggerEvent('reset');
}
}
function toggle() {
if (self.options.collapsible) {
// fixme: silly, pass an option
self.options.parent.toggle(
self.isLeftOrTop ? 0
: self.options.parent.options('elements').length - 1
);
self.options.collapsed = !self.options.collapsed;
that.css({cursor: getCursor()});
self.$tooltip && self.$tooltip.hide(function() {
self.$tooltip.options({title: getTitle()});
self.$tooltip.options({title: getTooltipTitle()});
});
that.triggerEvent('toggle', {collapsed: self.options.collapsed});
}
}
function triggerEvents(event) {
self.options.elements[0].triggerEvent(event, {
size: self.isLeftOrTop
? self.options.size
: self.options.elements[0][self.dimensions[1]]()
});
self.options.elements[1].triggerEvent(event, {
size: self.isLeftOrTop
? self.options.elements[1][self.dimensions[1]]()
: self.options.size
});
}
return that;
};

View file

@ -6,10 +6,14 @@ Ox.SplitPanel <f> SpliPanel Object
elements <[o]|[]> Array of two or three element objects
collapsible <b|false> If true, can be collapsed (if outer element)
collapsed <b|false> If true, is collapsed (if collapsible)
defaultSize <n|s|auto> Default size in px (restorable via reset)
defaultSize <n|s|"auto"> Default size in px (restorable via reset)
element <o> Any Ox.Element
If any element is collapsible or resizable, all elements must
have an id.
resettable <b|false> If true, can be resetted (if outer element)
Note that reset fires on doubleclick, and if the element is also
collapsible, toggle now fires on singleclick, no longer on click.
Singleclick happens 250 ms later.
resizable <b|false> If true, can be resized (if outer element)
resize <[n]|[]> Min size, optional snappy points, and max size
size <n|s|"auto"> Size in px (one element must be "auto")
@ -19,15 +23,16 @@ Ox.SplitPanel <f> SpliPanel Object
([options[, self]]) -> <o:Ox.Element> SpliPanel Object
resize <!> resize
Fires on resize, on both elements being resized
resizeend <!> resizeend
Fires on resize, on both elements being resized
resizepause <!> resizepause
Fires on resize, on both elements being resized
toggle <!> toggle
Fires on collapse or expand, on the element being toggled
@*/
Ox.SplitPanel = function(options, self) {
// fixme: doubleclick on resizebar should reset to initial size
// (but anyclick would become singleclick, i.e. less responsive)
self = self || {};
var that = Ox.Element({}, self)
.defaults({
@ -37,83 +42,77 @@ Ox.SplitPanel = function(options, self) {
.options(options || {})
.addClass('OxSplitPanel');
Ox.extend(self, {
dimensions: Ox.UI.DIMENSIONS[self.options.orientation],
edges: Ox.UI.EDGES[self.options.orientation],
defaultSize: self.options.elements.map(function(element) {
self.defaultSize = self.options.elements.map(function(element) {
return !Ox.isUndefined(element.defaultSize)
? element.defaultSize : element.size;
}),
length: self.options.elements.length,
resizebarElements: [],
$resizebars: []
});
self.dimensions = Ox.UI.DIMENSIONS[self.options.orientation];
self.edges = Ox.UI.EDGES[self.options.orientation];
self.initialized = false;
self.length = self.options.elements.length;
// create elements
// fixme: attach to self instead?
that.$elements = Ox.map(self.options.elements, function(element, i) {
self.options.elements[i] = Ox.extend({
self.$elements = [];
self.$resizebars = [];
self.options.elements.forEach(function(element, index) {
var elementIndices = index == 0 ? [0, 1] : index == 1 ? [1, 0] : [2, 1],
resizebarIndex = self.$resizebars.length;
self.options.elements[index] = Ox.extend({
collapsible: false,
collapsed: false,
defaultSize: 'auto',
resettable: false,
resizable: false,
resize: [],
size: 'auto'
size: 'auto',
tooltip: false
}, element);
// top and bottom (horizontal) or left and right (vertical)
self.edges.slice(2).forEach(function(edge) {
element.element.css(edge, (parseInt(element.element.css(edge)) || 0) + 'px');
if (!element.element.css(edge)) {
element.element.css(edge, 0);
}
});
return element.element;
});
// create resizebars
self.options.elements.forEach(function(element, i) {
var index = i == 0 ? 0 : 1;
that.$elements[i].appendTo(that.$element); // fixme: that.$content
if (element.collapsed) {
// left/right (horizontal) or top/bottom (vertical)
that.css(self.edges[index == 0 ? 0 : 1], -element.size + 'px');
}
self.$elements[index] = element.element.appendTo(that);
if (element.collapsible || element.resizable) {
self.resizebarElements[index] = i < 2 ? [0, 1] : [1, 2];
self.$resizebars[index] = Ox.Resizebar({
self.$resizebars[resizebarIndex] = Ox.Resizebar({
collapsed: element.collapsed,
collapsible: element.collapsible,
edge: self.edges[index],
elements: [
that.$elements[self.resizebarElements[index][0]],
that.$elements[self.resizebarElements[index][1]]
],
id: element.element.options('id'),
edge: self.edges[index == 0 ? 0 : 1],
orientation: self.options.orientation == 'horizontal'
? 'vertical' : 'horizontal',
parent: that, // fixme: that.$content
resettable: element.resettable,
resizable: element.resizable,
resize: element.resize,
size: element.size,
tooltip: element.tooltip
});
self.$resizebars[index][
i == 0 ? 'insertAfter' : 'insertBefore'
](that.$elements[i]);
tooltip: element.tooltip === true ? '' : element.tooltip
})
.bindEvent({
reset: function() {
that.resetElement(index);
},
resize: function(data) {
onResize(elementIndices, data.size);
triggerEvents(elementIndices, 'resize', data);
},
resizepause: function(data) {
triggerEvents(elementIndices, 'resizepause', data);
},
resizeend: function(data) {
triggerEvents(elementIndices, 'resizeend', data);
},
toggle: function(data) {
that.toggleElement(index);
}
})
[index == 0 ? 'insertAfter' : 'insertBefore'](self.$elements[index]);
}
});
self.options.elements.forEach(function(element, i) {
element.collapsed && that.css(
// left/right (horizontal) or top/bottom (vertical)
self.edges[i == 0 ? 0 : 1], -element.size + 'px'
);
});
setSizes(true);
function getIndexById(id) {
var index = -1;
Ox.forEach(self.options.elements, function(element, i) {
if (element.element.options('id') == id) {
index = i;
return false; // break
}
});
return index;
}
setSizes();
function getSize(index) {
var element = self.options.elements[index];
@ -125,41 +124,50 @@ Ox.SplitPanel = function(options, self) {
return getSize(index) * !element.collapsed;
}
function setSizes(init, animate) {
function onResize(elementIndices, size) {
var dimension = self.dimensions[0],
edge = self.edges[elementIndices[0] == 0 ? 0 : 1];
self.options.elements[elementIndices[0]].size = size;
elementIndices.forEach(function(elementIndex, index) {
self.$elements[elementIndex].css(
index == 0 ? dimension : edge,
index == 0 ? size : size + 1
);
});
}
function setSizes(animate) {
// will animate if animate is truthy and call animate if it's a function
self.options.elements.forEach(function(element, i) {
// fixme: maybe we can add a conditional here, since init
// is about elements that are collapsed splitpanels
var css = {},
self.options.elements.forEach(function(element, index) {
var $resizebar,
css = {},
edges = self.edges.slice(0, 2).map(function(edge) {
// left/right (horizontal) or top/bottom (vertical)
return init && parseInt(that.$elements[i].css(edge)) || 0;
var value = parseInt(self.$elements[index].css(edge));
return !self.initialized && value || 0;
});
if (element.size != 'auto') {
// width (horizontal) or height (vertical)
css[self.dimensions[0]] = element.size + 'px';
}
if (i == 0) {
if (index == 0) {
// left (horizontal) or top (vertical)
css[self.edges[0]] = edges[0] + 'px';
// right (horizontal) or bottom (vertical)
if (element.size == 'auto') {
css[self.edges[1]] = getSize(1)
+ (
css[self.edges[1]] = getSize(1) + (
self.length == 3 ? getVisibleSize(2) : 0
) + 'px';
}
} else if (i == 1) {
} else if (index == 1) {
// left (horizontal) or top (vertical)
if (self.options.elements[0].size != 'auto') {
css[self.edges[0]] = edges[0] + getSize(0) + 'px'
css[self.edges[0]] = edges[0] + getSize(0) + 'px';
} else {
css[self.edges[0]] = 'auto'; // fixme: why is this needed?
}
// right (horizontal) or bottom (vertical)
css[self.edges[1]] = (
self.length == 3 ? getSize(2) : 0
) + 'px';
css[self.edges[1]] = (self.length == 3 ? getSize(2) : 0) + 'px';
} else {
// left (horizontal) or top (vertical)
if (element.size == 'auto') {
@ -171,117 +179,74 @@ Ox.SplitPanel = function(options, self) {
css[self.edges[1]] = edges[1] + 'px';
}
if (animate) {
that.$elements[i].animate(css, 250, function() {
i == 0 && Ox.isFunction(animate) && animate();
self.$elements[index].animate(css, 250, function() {
index == 0 && Ox.isFunction(animate) && animate();
});
} else {
that.$elements[i].css(css);
self.$elements[index].css(css);
}
if (element.collapsible || element.resizable) {
css = {};
$resizebar = self.$resizebars[
index < 2 ? 0 : self.$resizebars.length - 1
];
// left or right (horizontal) or top or bottom (vertical)
css[self.edges[i == 0 ? 0 : 1]] = element.size + 'px'
css = Ox.extend(
{}, self.edges[index == 0 ? 0 : 1], element.size + 'px'
);
if (animate) {
self.$resizebars[i == 0 ? 0 : 1].animate(css, 250);
$resizebar.animate(css, 250);
} else {
self.$resizebars[i == 0 ? 0 : 1].css(css);
$resizebar.css(css);
}
self.$resizebars[i == 0 ? 0 : 1].options({size: element.size});
$resizebar.options({size: element.size});
}
});
self.initialized = true;
}
function triggerEvents(elementIndices, event, data) {
elementIndices.forEach(function(elementIndex, index) {
var $element = self.$elements[elementIndex],
size = index == 0 ? data.size : $element[self.dimensions[0]]();
$element.triggerEvent(event, {size: size});
});
}
/*@
getSize <f> get size of panel
(id) -> <i> id or index of element, returns size
id <s|i> The element's id or index
isCollapsed <f> Tests if an outer element is collapsed
(index) -> <b> True if collapsed
index <i> The element's index
@*/
// fixme: what is this? there is that.size()
that.getSize = function(id) {
var index = Ox.isNumber(id) ? id : getIndexById(id),
element = self.options.elements[index];
return element.element[self.dimensions[0]]() * !that.isCollapsed(index);
};
/*@
isCollapsed <f> panel collapsed state
(id) -> <b> id or index of element, returns collapsed state
id <i> The element's id or index
@*/
that.isCollapsed = function(id) {
var index = Ox.isNumber(id) ? id : getIndexById(id);
that.isCollapsed = function(index) {
return self.options.elements[index].collapsed;
};
/*@
replaceElement <f> Replace panel element
(id, element) -> <f> replace element
id <s|n> The element's id or index
replaceElement <f> Replaces an element
(index, element) -> <f> replace element
index <n> The element's index
element <o> New element
@*/
that.replaceElement = function(id, element) {
// one can pass index instead of id
var index = Ox.isNumber(id) ? id : getIndexById(id);
that.replaceElement = function(index, element) {
// top and bottom (horizontal) or left and right (vertical)
self.edges.slice(2).forEach(function(edge) {
element.css(edge, (parseInt(element.css(edge)) || 0) + 'px');
});
that.$elements[index] = element;
self.$elements[index] = element;
self.options.elements[index].element.replaceWith(
self.options.elements[index].element = element
);
setSizes();
self.$resizebars.forEach(function($resizebar, i) {
$resizebar.options({
elements: [
that.$elements[self.resizebarElements[i][0]],
that.$elements[self.resizebarElements[i][1]]
]
});
});
return that;
};
/*@
replaceElements <f> replace panel elements
(elements) -> <f> replace elements
elements <a> array of new elements
resetElement <f> Resets an outer element to its initial size
@*/
that.replaceElements = function(elements) {
elements.forEach(function(element, i) {
if (Ox.isNumber(element.size)) {
that.size(i, element.size);
if (element.collapsible || element.resizable) {
self.$resizebars[i == 0 ? 0 : 1].options({
collapsible: element.collapsible,
resizable: element.resizable,
size: element.size
});
}
}
that.replace(i, element.element);
});
self.options.elements = elements;
self.$resizebars.forEach(function($resizebar, i) {
$resizebar.options({
elements: [
that.$elements[self.resizebarElements[i][0]],
that.$elements[self.resizebarElements[i][1]]
]
});
});
return that;
};
/*@
reset <f> Reset an outer element to its initial size
@*/
that.reset = function(id) {
// one can pass index instead of id
var index = Ox.isNumber(id) ? id : getIndexById(id),
element = self.options.elements[index];
that.resetElement = function(index) {
var element = self.options.elements[index];
element.size = self.defaultSize[index];
setSizes(false, function() {
setSizes(function() {
element.element.triggerEvent('resize', {
size: element.size
});
@ -290,72 +255,60 @@ Ox.SplitPanel = function(options, self) {
size: element.element[self.dimensions[0]]()
});
});
return that;
};
/*@
size <f> Get or set size of an element
(id) -> <i> Returns size
(id, size) -> <o> Sets size, returns SplitPanel
(id, size, callback) -> <o> Sets size with animation, returns SplitPanel
id <i> The element's id or index
(index) -> <i> Returns size
(index, size) -> <o> Sets size, returns SplitPanel
(index, size, callback) -> <o> Sets size with animation, returns SplitPanel
index <i> The element's index
size <i> New size, in px
callback <b|f> Callback function (passing true animates w/o callback)
@*/
that.size = function(id, size, callback) {
// one can pass index instead of id
var index = Ox.isNumber(id) ? id : getIndexById(id),
element = self.options.elements[index];
that.resizeElement = that.size = function(index, size, callback) {
var element = self.options.elements[index];
if (arguments.length == 1) {
return element.element[self.dimensions[0]]() * !that.isCollapsed(index);
return element.element[self.dimensions[0]]()
* !that.isCollapsed(index);
} else {
element.size = size;
setSizes(false, callback);
setSizes(callback);
return that;
}
};
/*@
toggle <f> Toggles collapsed state of an outer element
(id) -> <o> The SplitPanel
id <s|i> The element's id or index
toggleElement <f> Toggles collapsed state of an outer element
(index) -> <o> The SplitPanel
index <s|i> The element's index
@*/
// FIXME: 'toggle' is reserved by jQuery
that.toggle = function(id) {
// one can pass index instead of id
that.toggleElement = function(index) {
if (self.toggling) {
return false;
return that;
}
var index = Ox.isNumber(id) ? id : getIndexById(id),
element = self.options.elements[index],
value = parseInt(that.css(self.edges[index == 0 ? 0 : 1]), 10)
+ element.element[self.dimensions[0]]() * (element.collapsed ? 1 : -1),
animate = {};
var element = self.options.elements[index],
value = parseInt(that.css(self.edges[index == 0 ? 0 : 1]))
+ element.element[self.dimensions[0]]()
* (element.collapsed ? 1 : -1),
animate = Ox.extend({}, self.edges[index == 0 ? 0 : 1], value);
self.toggling = true;
animate[self.edges[index == 0 ? 0 : 1]] = value;
that.animate(animate, 250, function() {
element.collapsed = !element.collapsed;
element.element.triggerEvent('toggle', {
collapsed: element.collapsed
});
self.$resizebars[index == 0 ? 0 : 1].options({collapsed: element.collapsed});
self.$resizebars[index < 2 ? 0 : self.$resizebars.length - 1].options({
collapsed: element.collapsed
});
element = self.options.elements[index == 0 ? 1 : index - 1];
element.element.triggerEvent('resize', {
size: element.element[self.dimensions[0]]()
});
self.toggling = false;
});
};
/*@
updateSize <f> update size of element
(index, size) -> <o> update size of element
index <i> index of element
size <n> new size
@*/
that.updateSize = function(index, size) {
// this is called from resizebar
index = index == 0 ? 0 : self.options.elements.length - 1; // fixme: silly that 0 or 1 is passed, and not index
self.options.elements[index].size = size;
return that;
};
return that;