1 | /* |
||
2 | * jQuery.splitter.js - two-pane splitter window plugin |
||
3 | * |
||
4 | * version 1.6 (2010/01/03) |
||
5 | * |
||
6 | * Dual licensed under the MIT and GPL licenses: |
||
7 | * http://www.opensource.org/licenses/mit-license.php |
||
8 | * http://www.gnu.org/licenses/gpl.html |
||
9 | */ |
||
10 | |||
11 | /** |
||
12 | * The splitter() plugin implements a two-pane resizable splitter window. |
||
13 | * The selected elements in the jQuery object are converted to a splitter; |
||
14 | * each selected element should have two child elements, used for the panes |
||
15 | * of the splitter. The plugin adds a third child element for the splitbar. |
||
16 | * |
||
17 | * For more details see: http://methvin.com/jquery/splitter/ |
||
18 | * |
||
19 | * |
||
20 | * @example $('#MySplitter').splitter(); |
||
21 | * @desc Create a vertical splitter with default settings |
||
22 | * |
||
23 | * @example $('#MySplitter').splitter({type: 'h', accessKey: 'M'}); |
||
24 | * @desc Create a horizontal splitter resizable via Alt+Shift+M |
||
25 | * |
||
26 | * @name splitter |
||
27 | * @type jQuery |
||
28 | * @param Object options Options for the splitter (not required) |
||
29 | * @cat Plugins/Splitter |
||
30 | * @return jQuery |
||
31 | * @author Dave Methvin ([email protected]) |
||
32 | */ |
||
33 | ;(function($){ |
||
34 | |||
35 | var splitterCounter = 0; |
||
36 | |||
37 | $.fn.splitter = function(args){ |
||
38 | args = args || {}; |
||
39 | return this.each(function() { |
||
40 | if ( $(this).is(".splitter") ) // already a splitter |
||
41 | return; |
||
42 | var zombie; // left-behind splitbar for outline resizes |
||
43 | function setBarState(state) { |
||
44 | bar.removeClass(opts.barStateClasses).addClass(state); |
||
45 | } |
||
46 | function startSplitMouse(evt) { |
||
47 | if ( evt.which != 1 ) |
||
48 | return; // left button only |
||
49 | bar.removeClass(opts.barHoverClass); |
||
50 | if ( opts.outline ) { |
||
51 | zombie = zombie || bar.clone(false).insertAfter(A); |
||
52 | bar.removeClass(opts.barDockedClass); |
||
53 | } |
||
54 | setBarState(opts.barActiveClass) |
||
55 | // Safari selects A/B text on a move; iframes capture mouse events so hide them |
||
56 | panes.css("-webkit-user-select", "none").find("iframe").andSelf().filter("iframe").addClass(opts.iframeClass); |
||
57 | A._posSplit = A[0][opts.pxSplit] - evt[opts.eventPos]; |
||
58 | $(document) |
||
59 | .bind("mousemove"+opts.eventNamespace, doSplitMouse) |
||
60 | .bind("mouseup"+opts.eventNamespace, endSplitMouse); |
||
61 | } |
||
62 | function doSplitMouse(evt) { |
||
63 | var pos = A._posSplit+evt[opts.eventPos], |
||
64 | range = Math.max(0, Math.min(pos, splitter._DA - bar._DA)), |
||
65 | limit = Math.max(A._min, splitter._DA - B._max, |
||
66 | Math.min(pos, A._max, splitter._DA - bar._DA - B._min)); |
||
67 | if ( opts.outline ) { |
||
68 | // Let docking splitbar be dragged to the dock position, even if min width applies |
||
69 | if ( (opts.dockPane == A && pos < Math.max(A._min, bar._DA)) || |
||
70 | (opts.dockPane == B && pos > Math.min(pos, A._max, splitter._DA - bar._DA - B._min)) ) { |
||
71 | bar.addClass(opts.barDockedClass).css(opts.origin, range); |
||
72 | } |
||
73 | else { |
||
74 | bar.removeClass(opts.barDockedClass).css(opts.origin, limit); |
||
75 | } |
||
76 | bar._DA = bar[0][opts.pxSplit]; |
||
77 | } else |
||
78 | resplit(pos); |
||
79 | setBarState(pos == limit? opts.barActiveClass : opts.barLimitClass); |
||
80 | } |
||
81 | function endSplitMouse(evt) { |
||
82 | setBarState(opts.barNormalClass); |
||
83 | bar.addClass(opts.barHoverClass); |
||
84 | var pos = A._posSplit+evt[opts.eventPos]; |
||
85 | if ( opts.outline ) { |
||
86 | zombie.remove(); zombie = null; |
||
87 | resplit(pos); |
||
88 | } |
||
89 | panes.css("-webkit-user-select", "text").find("iframe").andSelf().filter("iframe").removeClass(opts.iframeClass); |
||
90 | $(document) |
||
91 | .unbind("mousemove"+opts.eventNamespace+" mouseup"+opts.eventNamespace); |
||
92 | } |
||
93 | function resplit(pos) { |
||
94 | bar._DA = bar[0][opts.pxSplit]; // bar size may change during dock |
||
95 | // Constrain new splitbar position to fit pane size and docking limits |
||
96 | if ( (opts.dockPane == A && pos < Math.max(A._min, bar._DA)) || |
||
97 | (opts.dockPane == B && pos > Math.min(pos, A._max, splitter._DA - bar._DA - B._min)) ) { |
||
98 | bar.addClass(opts.barDockedClass); |
||
99 | bar._DA = bar[0][opts.pxSplit]; |
||
100 | pos = opts.dockPane == A? 0 : splitter._DA - bar._DA; |
||
101 | if ( bar._pos == null ) |
||
102 | bar._pos = A[0][opts.pxSplit]; |
||
103 | } |
||
104 | else { |
||
105 | bar.removeClass(opts.barDockedClass); |
||
106 | bar._DA = bar[0][opts.pxSplit]; |
||
107 | bar._pos = null; |
||
108 | pos = Math.max(A._min, splitter._DA - B._max, |
||
109 | Math.min(pos, A._max, splitter._DA - bar._DA - B._min)); |
||
110 | } |
||
111 | // Resize/position the two panes |
||
112 | bar.css(opts.origin, pos).css(opts.fixed, splitter._DF); |
||
113 | A.css(opts.origin, 0).css(opts.split, pos).css(opts.fixed, splitter._DF); |
||
114 | B.css(opts.origin, pos+bar._DA) |
||
115 | .css(opts.split, splitter._DA-bar._DA-pos).css(opts.fixed, splitter._DF); |
||
116 | panes.trigger("resize"+opts.eventNamespace); |
||
117 | } |
||
118 | function dimSum(jq, dims) { |
||
119 | // Opera returns -1 for missing min/max width, turn into 0 |
||
120 | var sum = 0; |
||
121 | for ( var i=1; i < arguments.length; i++ ) |
||
122 | sum += Math.max(parseInt(jq.css(arguments[i]),10) || 0, 0); |
||
123 | return sum; |
||
124 | } |
||
125 | |||
126 | // Determine settings based on incoming opts, element classes, and defaults |
||
127 | var vh = (args.splitHorizontal? 'h' : args.splitVertical? 'v' : args.type) || 'v'; |
||
128 | var opts = $.extend({ |
||
129 | // Defaults here allow easy use with ThemeRoller |
||
130 | splitterClass: "splitter ui-widget ui-widget-content", |
||
131 | paneClass: "splitter-pane", |
||
132 | barClass: "splitter-bar", |
||
133 | barNormalClass: "ui-state-default", // splitbar normal |
||
134 | barHoverClass: "ui-state-hover", // splitbar mouse hover |
||
135 | barActiveClass: "ui-state-highlight", // splitbar being moved |
||
136 | barLimitClass: "ui-state-error", // splitbar at limit |
||
137 | iframeClass: "splitter-iframe-hide", // hide iframes during split |
||
138 | eventNamespace: ".splitter"+(++splitterCounter), |
||
139 | pxPerKey: 8, // splitter px moved per keypress |
||
140 | tabIndex: 0, // tab order indicator |
||
141 | accessKey: '' // accessKey for splitbar |
||
142 | },{ |
||
143 | // user can override |
||
144 | v: { // Vertical splitters: |
||
145 | keyLeft: 39, keyRight: 37, cursor: "e-resize", |
||
146 | barStateClass: "splitter-bar-vertical", |
||
147 | barDockedClass: "splitter-bar-vertical-docked" |
||
148 | }, |
||
149 | h: { // Horizontal splitters: |
||
150 | keyTop: 40, keyBottom: 38, cursor: "n-resize", |
||
151 | barStateClass: "splitter-bar-horizontal", |
||
152 | barDockedClass: "splitter-bar-horizontal-docked" |
||
153 | } |
||
154 | }[vh], args, { |
||
155 | // user cannot override |
||
156 | v: { // Vertical splitters: |
||
157 | type: 'v', eventPos: "pageX", origin: "left", |
||
158 | split: "width", pxSplit: "offsetWidth", side1: "Left", side2: "Right", |
||
159 | fixed: "height", pxFixed: "offsetHeight", side3: "Top", side4: "Bottom" |
||
160 | }, |
||
161 | h: { // Horizontal splitters: |
||
162 | type: 'h', eventPos: "pageY", origin: "top", |
||
163 | split: "height", pxSplit: "offsetHeight", side1: "Top", side2: "Bottom", |
||
164 | fixed: "width", pxFixed: "offsetWidth", side3: "Left", side4: "Right" |
||
165 | } |
||
166 | }[vh]); |
||
167 | opts.barStateClasses = [opts.barNormalClass, opts.barHoverClass, opts.barActiveClass, opts.barLimitClass].join(' '); |
||
168 | |||
169 | // Create jQuery object closures for splitter and both panes |
||
170 | var splitter = $(this).css({position: "relative"}).addClass(opts.splitterClass); |
||
171 | var panes = $(">*", splitter[0]).addClass(opts.paneClass).css({ |
||
172 | position: "absolute", // positioned inside splitter container |
||
173 | "z-index": "1", // splitbar is positioned above |
||
174 | "-moz-outline-style": "none" // don't show dotted outline |
||
175 | }); |
||
176 | var A = $(panes[0]), B = $(panes[1]); // A = left/top, B = right/bottom |
||
177 | opts.dockPane = opts.dock && (/right|bottom/.test(opts.dock)? B:A); |
||
178 | |||
179 | // Focuser element, provides keyboard support; title is shown by Opera accessKeys |
||
180 | var focuser = $('<a href="javascript:void(0)"></a>') |
||
181 | .attr({accessKey: opts.accessKey, tabIndex: opts.tabIndex, title: opts.splitbarClass}) |
||
182 | .bind(("focus")+opts.eventNamespace, |
||
183 | function(){ this.focus(); bar.addClass(opts.barActiveClass) }) |
||
184 | .bind("keydown"+opts.eventNamespace, function(e){ |
||
185 | var key = e.which || e.keyCode; |
||
186 | var dir = key==opts["key"+opts.side1]? 1 : key==opts["key"+opts.side2]? -1 : 0; |
||
187 | if ( dir ) |
||
188 | resplit(A[0][opts.pxSplit]+dir*opts.pxPerKey, false); |
||
0 ignored issues
–
show
Bug
introduced
by
![]() |
|||
189 | }) |
||
190 | .bind("blur"+opts.eventNamespace, |
||
191 | function(){ bar.removeClass(opts.barActiveClass) }); |
||
192 | |||
193 | // Splitbar element |
||
194 | var bar = $('<div></div>') |
||
195 | .insertAfter(A).addClass(opts.barClass).addClass(opts.barStateClass) |
||
196 | .append(focuser).attr({unselectable: "on"}) |
||
197 | .css({position: "absolute", "user-select": "none", "-webkit-user-select": "none", |
||
198 | "-khtml-user-select": "none", "-moz-user-select": "none", "z-index": "100"}) |
||
199 | .bind("mousedown"+opts.eventNamespace, startSplitMouse) |
||
200 | .bind("mouseover"+opts.eventNamespace, function(){ |
||
201 | $(this).addClass(opts.barHoverClass); |
||
202 | }) |
||
203 | .bind("mouseout"+opts.eventNamespace, function(){ |
||
204 | $(this).removeClass(opts.barHoverClass); |
||
205 | }); |
||
206 | // Use our cursor unless the style specifies a non-default cursor |
||
207 | if ( /^(auto|default|)$/.test(bar.css("cursor")) ) |
||
208 | bar.css("cursor", opts.cursor); |
||
209 | |||
210 | // Cache several dimensions for speed, rather than re-querying constantly |
||
211 | // These are saved on the A/B/bar/splitter jQuery vars, which are themselves cached |
||
212 | // DA=dimension adjustable direction, PBF=padding/border fixed, PBA=padding/border adjustable |
||
213 | bar._DA = bar[0][opts.pxSplit]; |
||
214 | splitter._PBF = dimSum(splitter, "border"+opts.side3+"Width", "border"+opts.side4+"Width"); |
||
215 | splitter._PBA = dimSum(splitter, "border"+opts.side1+"Width", "border"+opts.side2+"Width"); |
||
216 | A._pane = opts.side1; |
||
217 | B._pane = opts.side2; |
||
218 | $.each([A,B], function(){ |
||
219 | this._splitter_style = this.style; |
||
220 | this._min = opts["min"+this._pane] || dimSum(this, "min-"+opts.split); |
||
221 | this._max = opts["max"+this._pane] || dimSum(this, "max-"+opts.split) || 9999; |
||
222 | this._init = opts["size"+this._pane]===true ? |
||
223 | parseInt($.curCSS(this[0],opts.split),10) : opts["size"+this._pane]; |
||
224 | }); |
||
225 | |||
226 | // Determine initial position, get from cookie if specified |
||
227 | var initPos = A._init; |
||
228 | if ( !isNaN(B._init) ) // recalc initial B size as an offset from the top or left side |
||
229 | initPos = splitter[0][opts.pxSplit] - splitter._PBA - B._init - bar._DA; |
||
230 | if ( opts.cookie ) { |
||
231 | if ( !$.cookie ) |
||
232 | alert('jQuery.splitter(): jQuery cookie plugin required'); |
||
0 ignored issues
–
show
Debugging Code
Best Practice
introduced
by
|
|||
233 | initPos = parseInt($.cookie(opts.cookie),10); |
||
234 | $(window).bind("unload"+opts.eventNamespace, function(){ |
||
235 | var state = String(bar.css(opts.origin)); // current location of splitbar |
||
236 | $.cookie(opts.cookie, state, {expires: opts.cookieExpires || 365, |
||
237 | path: opts.cookiePath || document.location.pathname}); |
||
238 | }); |
||
239 | } |
||
240 | if ( isNaN(initPos) ) // King Solomon's algorithm |
||
241 | initPos = Math.round((splitter[0][opts.pxSplit] - splitter._PBA - bar._DA)/2); |
||
242 | |||
243 | // Resize event propagation and splitter sizing |
||
244 | if ( opts.anchorToWindow ) |
||
245 | opts.resizeTo = window; |
||
246 | if ( opts.resizeTo ) { |
||
247 | splitter._hadjust = dimSum(splitter, "borderTopWidth", "borderBottomWidth", "marginBottom"); |
||
248 | splitter._hmin = Math.max(dimSum(splitter, "minHeight"), 20); |
||
249 | $(window).bind("resize"+opts.eventNamespace, function(){ |
||
250 | var top = splitter.offset().top; |
||
251 | var eh = $(opts.resizeTo).height(); |
||
252 | splitter.css("height", Math.max(eh-top-splitter._hadjust, splitter._hmin)+"px"); |
||
253 | splitter.trigger("resize"); |
||
254 | }).trigger("resize"+opts.eventNamespace); |
||
255 | } |
||
256 | else if ( opts.resizeToWidth ) { |
||
257 | $(window).bind("resize"+opts.eventNamespace, function(){ |
||
258 | splitter.trigger("resize"); |
||
259 | }); |
||
260 | } |
||
261 | |||
262 | // Docking support |
||
263 | if ( opts.dock ) { |
||
264 | splitter |
||
265 | .bind("toggleDock"+opts.eventNamespace, function() { |
||
266 | var pw = opts.dockPane[0][opts.pxSplit]; |
||
267 | splitter.trigger(pw?"dock":"undock"); |
||
268 | }) |
||
269 | .bind("dock"+opts.eventNamespace, function(){ |
||
270 | var pw = A[0][opts.pxSplit]; |
||
271 | if ( !pw ) return; |
||
272 | // Don't try to dock twice |
||
273 | if (bar.hasClass("splitter-bar-horizontal-docked") || bar.hasClass("splitter-bar-vertical-docked")) |
||
274 | return; |
||
275 | bar._pos = pw; |
||
276 | var x={}; |
||
277 | x[opts.origin] = opts.dockPane==A? 0 : |
||
278 | splitter[0][opts.pxSplit] - splitter._PBA - bar[0][opts.pxSplit]; |
||
279 | bar.animate(x, opts.dockSpeed||1, opts.dockEasing, function(){ |
||
280 | bar.addClass(opts.barDockedClass); |
||
281 | resplit(x[opts.origin]+1); |
||
282 | }); |
||
283 | }) |
||
284 | .bind("undock"+opts.eventNamespace, function(){ |
||
285 | var pw = opts.dockPane[0][opts.pxSplit]; |
||
286 | if ( pw ) return; |
||
287 | // 20px away is too close, reset to 50% |
||
288 | if(bar._pos == null || Math.abs(bar._pos - splitter._DA - bar._DA) < 20) bar._pos = splitter._DA / 2; |
||
289 | var x={}; x[opts.origin]=bar._pos+"px"; |
||
290 | bar.removeClass(opts.barDockedClass) |
||
291 | .animate(x, opts.undockSpeed||opts.dockSpeed||1, opts.undockEasing||opts.dockEasing, function(){ |
||
292 | resplit(bar._pos); |
||
293 | bar._pos = null; |
||
294 | }); |
||
295 | }); |
||
296 | if ( opts.dockKey ) |
||
297 | $('<a title="'+opts.splitbarClass+' toggle dock" href="javascript:void(0)"></a>') |
||
298 | .attr({accessKey: opts.dockKey, tabIndex: -1}).appendTo(bar) |
||
299 | .bind("focus", function(){ |
||
300 | splitter.trigger("toggleDock"); this.blur(); |
||
301 | }); |
||
302 | bar.bind("dblclick", function(){ splitter.trigger("toggleDock"); }) |
||
303 | } |
||
304 | |||
305 | |||
306 | // Resize event handler; triggered immediately to set initial position |
||
307 | splitter |
||
308 | .bind("destroy"+opts.eventNamespace, function(){ |
||
309 | $([window, document]).unbind(opts.eventNamespace); |
||
310 | bar.unbind().remove(); |
||
311 | panes.removeClass(opts.paneClass); |
||
312 | splitter |
||
313 | .removeClass(opts.splitterClass) |
||
314 | .add(panes) |
||
315 | .unbind(opts.eventNamespace) |
||
316 | .attr("style", function(el){ |
||
317 | return this._splitter_style||""; //TODO: save style |
||
318 | }); |
||
319 | splitter = bar = focuser = panes = A = B = opts = args = null; |
||
320 | }) |
||
321 | .bind("resize"+opts.eventNamespace, function(e, size){ |
||
322 | // Custom events bubble in jQuery 1.3; avoid recursion |
||
323 | if ( e.target != this ) return; |
||
324 | // Determine new width/height of splitter container |
||
325 | splitter._DF = splitter[0][opts.pxFixed] - splitter._PBF; |
||
326 | splitter._DA = splitter[0][opts.pxSplit] - splitter._PBA; |
||
327 | // Bail if splitter isn't visible or content isn't there yet |
||
328 | if ( splitter._DF <= 0 || splitter._DA <= 0 ) return; |
||
329 | // Re-divvy the adjustable dimension; maintain size of the preferred pane |
||
330 | resplit(!isNaN(size)? size : (!(opts.sizeRight||opts.sizeBottom)? A[0][opts.pxSplit] : |
||
331 | splitter._DA-B[0][opts.pxSplit]-bar._DA)); |
||
332 | setBarState(opts.barNormalClass); |
||
333 | }) |
||
334 | .trigger("resize" , [initPos]); |
||
335 | }); |
||
336 | }; |
||
337 | |||
338 | })(jQuery); |
||
339 |