From 03c9c0804f9942e2bc8430a9c15ecc2866e3295d Mon Sep 17 00:00:00 2001 From: Nathan Gray Date: Fri, 22 Feb 2013 00:25:41 +0000 Subject: [PATCH] Split panel widget. Mostly working, needs some more special case for working with nm - they both want full page --- etemplate/js/et2_widget_split.js | 239 +++++++++++++++ etemplate/js/etemplate2.js | 1 + etemplate/js/widget_browser.js | 2 + etemplate/templates/default/etemplate2.css | 24 ++ phpgwapi/js/jquery/splitter.js | 335 +++++++++++++++++++++ 5 files changed, 601 insertions(+) create mode 100644 etemplate/js/et2_widget_split.js create mode 100644 phpgwapi/js/jquery/splitter.js diff --git a/etemplate/js/et2_widget_split.js b/etemplate/js/et2_widget_split.js new file mode 100644 index 0000000000..96f1066b39 --- /dev/null +++ b/etemplate/js/et2_widget_split.js @@ -0,0 +1,239 @@ +/** + * eGroupWare eTemplate2 - Split panel + * + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package etemplate + * @subpackage api + * @link http://www.egroupware.org + * @author Nathan Gray + * @copyright Nathan Gray 2013 + * @version $Id$ + */ + +"use strict"; + +/*egw:uses + jquery.jquery; + jquery.splitter; + et2_core_baseWidget; +*/ + +/** + * A container widget that accepts 2 children, and puts a resize bar between them. + * + * This is the etemplate2 implementation of the traditional split pane / split panel. + * The split can be horizontal or vertical, and can be limited in size. You can also + * turn on double-click docking to minimize one of the children. + * + * @see http://methvin.com/splitter/ Uses Splitter + */ +var et2_split = et2_DOMWidget.extend([et2_IResizeable], { + + attributes: { + "orientation": { + "name": "Orientation", + "description": "Horizontal or vertical (v or h)", + "default": "v", + "type": "string" + }, + "outline": { + "name": "Outline", + "description": "Use a 'ghosted' copy of the splitbar and does not resize the panes until the mouse button is released. Reduces flickering or unwanted re-layout during resize", + "default": false, + "type": "boolean" + }, + "dock_side": { + "name": "Dock", + "description": "Allow the user to 'Dock' the splitbar to one side of the splitter, essentially hiding one pane and using the entire splitter area for the other pane. One of leftDock, rightDock, topDock, bottomDock.", + "default": et2_no_init, + "type": "string" + }, + "width": { + "default": "100%" + }, + // Not needed + "overflow": {"ignore": true}, + "no_lang": {"ignore": true}, + "rows": {"ignore": true}, + "cols": {"ignore": true} + }, + + init: function() { + this._super.apply(this, arguments); + + + this.div = $j(document.createElement("div")); + + // Create the dynheight component which dynamically scales the inner + // container. + this.dynheight = new et2_dynheight(this.egw().window,this.div, 100); + + // Add something so we can see it - will be replaced if there's children + this.left = $j("
Top / Left
").appendTo(this.div); + this.right = $j("
Bottom / Right
").appendTo(this.div); + }, + + destroy: function() { + // Store current position + if(this.id && this.egw().getAppName()) + { + var size = this.orientation == "v" ? {sizeLeft: this.left.width()} : {sizeTop: this.left.height()}; + this.egw().set_preference(this.egw().getAppName(), 'splitter-size-' + this.id, size); + } + this.div.trigger("destroy"); + this.dynheight.free(); + + // Remove placeholder children + if(this._children.length == 0) + { + this.div.empty(); + } + this.div.remove(); + }, + + /** + * Tap in here to check if we have real children, because all children should be created + * by this point + */ + loadFromXML: function() { + this._super.apply(this, arguments); + if(this._children.length > 0) + { + if(this._children[0]) + { + this.left.detach(); + this.left = $j(this._children[0].getDOMNode(this._children[0])) + .appendTo(this.div); + } + if(this._children[1]) + { + this.right.detach(); + this.right = $j(this._children[1].getDOMNode(this._children[1])) + .appendTo(this.div); + } + } + }, + + doLoadingFinished: function() { + this._super.apply(this, arguments); + + // Use a timeout to give the children a chance to finish + var self = this; + window.setTimeout(function() { + self._init_splitter(); + },1); + return true; + }, + + /** + * Initialize the splitter + * Internal. + */ + _init_splitter: function() { + if(!this.isAttached()) return; + var options = { + type: this.orientation, + dock: this.dock_side, + splitterClass: "et2_split" + }; + + if(this.id) + { + var pref = this.egw().preference('splitter-size-' + this.id, this.egw().getAppName()); + if(pref) + { + options = $j.extend(options, pref); + } + } + + // Avoid double init + if(this.div.hasClass(options.splitterClass)) + { + this.div.trigger("destroy"); + } + this.div.splitter(options); + + // Add icon to splitter bar + var icon = "ui-icon-grip-" + + (this.dock_side ? "solid" : "dotted") + "-" + + (this.orientation == "h" ? "horizontal" : "vertical"); + + $j(document.createElement("div")) + .addClass("ui-icon") + .addClass(icon) + .appendTo(this.left.next()); + }, + + /** + * Implement the et2_IResizable interface to resize + */ + resize: function() { + if(this.dynheight) + { + this.dynheight.update(); + } +console.log(this.div); + }, + + getDOMNode: function() { + return this.div[0]; + }, + + /** + * Set splitter orientation + * + * @param orient String "v" or "h" + */ + set_orientation: function(orient) { + this.orientation = orient; + + this._init_splitter(); + }, + + /** + * Set the side for docking + * + * @param dock String One of leftDock, rightDock, topDock, bottomDock + */ + set_dock_side: function(dock) { + this.dock_side = dock; + + this._init_splitter(); + }, + + /** + * Turn on or off resizing while dragging + * + * @param outline boolean + */ + set_outline: function(outline) { + this.outline = outline; + this._init_splitter(); + }, + + /** + * If the splitter has a dock direction set, dock it. + * Docking requires the dock attribute to be set. + */ + dock: function() { + this.div.trigger("dock"); + }, + + /** + * If the splitter is docked, restore it to previous size + * Docking requires the dock attribute to be set. + */ + undock: function() { + this.div.trigger("undock"); + }, + + /** + * Toggle the splitter's docked state. + * Docking requires the dock attribute to be set. + */ + toggleDock: function() { + this.div.trigger("toggleDock"); + } +}); + +et2_register_widget(et2_split, ["split"]); diff --git a/etemplate/js/etemplate2.js b/etemplate/js/etemplate2.js index d9e940cc06..03a71fb040 100644 --- a/etemplate/js/etemplate2.js +++ b/etemplate/js/etemplate2.js @@ -17,6 +17,7 @@ et2_widget_box; et2_widget_hbox; et2_widget_groupbox; + et2_widget_split; et2_widget_button; et2_widget_color; et2_widget_description; diff --git a/etemplate/js/widget_browser.js b/etemplate/js/widget_browser.js index ff76631b3d..a58d356f7e 100644 --- a/etemplate/js/widget_browser.js +++ b/etemplate/js/widget_browser.js @@ -89,7 +89,9 @@ widget_browser.prototype.select_widget = function(e,f) // Clear previous widget if(this.widget) { + this.et2.widgetContainer.removeChild(this.widget); this.widget.free(); + this.widget = null; } // Get the type of widget diff --git a/etemplate/templates/default/etemplate2.css b/etemplate/templates/default/etemplate2.css index 489e446536..f8351af58a 100644 --- a/etemplate/templates/default/etemplate2.css +++ b/etemplate/templates/default/etemplate2.css @@ -91,6 +91,30 @@ div.et2_hbox>div { margin: 2px 0 2px 0; } +/** + * Splitter widget - split pane + */ +.et2_split { + width: 100%; + min-width: 100px; + min-height: 100px; +} +.splitter-bar-vertical { cursor: ew-resize; width: 5px;} +.splitter-bar-horizontal { cursor: ns-resize; height: 5px;} +/* Hide iframes so moving works */ +.splitter-iframe-hide { display: none;} +.et2_split div.splitter-bar-vertical div.ui-icon { + position: absolute; + margin-left: -5px; + top: 45%; +} +.et2_split div.splitter-bar-horizontal div.ui-icon { + position: absolute; + margin-top: -6px; + left: 47%; +} + + /** * Label widget, and labels for other widgets */ diff --git a/phpgwapi/js/jquery/splitter.js b/phpgwapi/js/jquery/splitter.js new file mode 100644 index 0000000000..25fae08aa7 --- /dev/null +++ b/phpgwapi/js/jquery/splitter.js @@ -0,0 +1,335 @@ +/* + * jQuery.splitter.js - two-pane splitter window plugin + * + * version 1.6 (2010/01/03) + * + * Dual licensed under the MIT and GPL licenses: + * http://www.opensource.org/licenses/mit-license.php + * http://www.gnu.org/licenses/gpl.html + */ + +/** + * The splitter() plugin implements a two-pane resizable splitter window. + * The selected elements in the jQuery object are converted to a splitter; + * each selected element should have two child elements, used for the panes + * of the splitter. The plugin adds a third child element for the splitbar. + * + * For more details see: http://methvin.com/jquery/splitter/ + * + * + * @example $('#MySplitter').splitter(); + * @desc Create a vertical splitter with default settings + * + * @example $('#MySplitter').splitter({type: 'h', accessKey: 'M'}); + * @desc Create a horizontal splitter resizable via Alt+Shift+M + * + * @name splitter + * @type jQuery + * @param Object options Options for the splitter (not required) + * @cat Plugins/Splitter + * @return jQuery + * @author Dave Methvin (dave.methvin@gmail.com) + */ + ;(function($){ + +var splitterCounter = 0; + + $.fn.splitter = function(args){ + args = args || {}; + return this.each(function() { + if ( $(this).is(".splitter") ) // already a splitter + return; + var zombie; // left-behind splitbar for outline resizes + function setBarState(state) { + bar.removeClass(opts.barStateClasses).addClass(state); + } + function startSplitMouse(evt) { + if ( evt.which != 1 ) + return; // left button only + bar.removeClass(opts.barHoverClass); + if ( opts.outline ) { + zombie = zombie || bar.clone(false).insertAfter(A); + bar.removeClass(opts.barDockedClass); + } + setBarState(opts.barActiveClass) + // Safari selects A/B text on a move; iframes capture mouse events so hide them + panes.css("-webkit-user-select", "none").find("iframe").andSelf().filter("iframe").addClass(opts.iframeClass); + A._posSplit = A[0][opts.pxSplit] - evt[opts.eventPos]; + $(document) + .bind("mousemove"+opts.eventNamespace, doSplitMouse) + .bind("mouseup"+opts.eventNamespace, endSplitMouse); + } + function doSplitMouse(evt) { + var pos = A._posSplit+evt[opts.eventPos], + range = Math.max(0, Math.min(pos, splitter._DA - bar._DA)), + limit = Math.max(A._min, splitter._DA - B._max, + Math.min(pos, A._max, splitter._DA - bar._DA - B._min)); + if ( opts.outline ) { + // Let docking splitbar be dragged to the dock position, even if min width applies + if ( (opts.dockPane == A && pos < Math.max(A._min, bar._DA)) || + (opts.dockPane == B && pos > Math.min(pos, A._max, splitter._DA - bar._DA - B._min)) ) { + bar.addClass(opts.barDockedClass).css(opts.origin, range); + } + else { + bar.removeClass(opts.barDockedClass).css(opts.origin, limit); + } + bar._DA = bar[0][opts.pxSplit]; + } else + resplit(pos); + setBarState(pos == limit? opts.barActiveClass : opts.barLimitClass); + } + function endSplitMouse(evt) { + setBarState(opts.barNormalClass); + bar.addClass(opts.barHoverClass); + var pos = A._posSplit+evt[opts.eventPos]; + if ( opts.outline ) { + zombie.remove(); zombie = null; + resplit(pos); + } + panes.css("-webkit-user-select", "text").find("iframe").andSelf().filter("iframe").removeClass(opts.iframeClass); + $(document) + .unbind("mousemove"+opts.eventNamespace+" mouseup"+opts.eventNamespace); + } + function resplit(pos) { + bar._DA = bar[0][opts.pxSplit]; // bar size may change during dock + // Constrain new splitbar position to fit pane size and docking limits + if ( (opts.dockPane == A && pos < Math.max(A._min, bar._DA)) || + (opts.dockPane == B && pos > Math.min(pos, A._max, splitter._DA - bar._DA - B._min)) ) { + bar.addClass(opts.barDockedClass); + bar._DA = bar[0][opts.pxSplit]; + pos = opts.dockPane == A? 0 : splitter._DA - bar._DA; + if ( bar._pos == null ) + bar._pos = A[0][opts.pxSplit]; + } + else { + bar.removeClass(opts.barDockedClass); + bar._DA = bar[0][opts.pxSplit]; + bar._pos = null; + pos = Math.max(A._min, splitter._DA - B._max, + Math.min(pos, A._max, splitter._DA - bar._DA - B._min)); + } + // Resize/position the two panes + bar.css(opts.origin, pos).css(opts.fixed, splitter._DF); + A.css(opts.origin, 0).css(opts.split, pos).css(opts.fixed, splitter._DF); + B.css(opts.origin, pos+bar._DA) + .css(opts.split, splitter._DA-bar._DA-pos).css(opts.fixed, splitter._DF); + // IE fires resize for us; all others pay cash + if ( !$.browser.msie ) + panes.trigger("resize"); + } + function dimSum(jq, dims) { + // Opera returns -1 for missing min/max width, turn into 0 + var sum = 0; + for ( var i=1; i < arguments.length; i++ ) + sum += Math.max(parseInt(jq.css(arguments[i]),10) || 0, 0); + return sum; + } + + // Determine settings based on incoming opts, element classes, and defaults + var vh = (args.splitHorizontal? 'h' : args.splitVertical? 'v' : args.type) || 'v'; + var opts = $.extend({ + // Defaults here allow easy use with ThemeRoller + splitterClass: "splitter ui-widget ui-widget-content", + paneClass: "splitter-pane", + barClass: "splitter-bar", + barNormalClass: "ui-state-default", // splitbar normal + barHoverClass: "ui-state-hover", // splitbar mouse hover + barActiveClass: "ui-state-highlight", // splitbar being moved + barLimitClass: "ui-state-error", // splitbar at limit + iframeClass: "splitter-iframe-hide", // hide iframes during split + eventNamespace: ".splitter"+(++splitterCounter), + pxPerKey: 8, // splitter px moved per keypress + tabIndex: 0, // tab order indicator + accessKey: '' // accessKey for splitbar + },{ + // user can override + v: { // Vertical splitters: + keyLeft: 39, keyRight: 37, cursor: "e-resize", + barStateClass: "splitter-bar-vertical", + barDockedClass: "splitter-bar-vertical-docked" + }, + h: { // Horizontal splitters: + keyTop: 40, keyBottom: 38, cursor: "n-resize", + barStateClass: "splitter-bar-horizontal", + barDockedClass: "splitter-bar-horizontal-docked" + } + }[vh], args, { + // user cannot override + v: { // Vertical splitters: + type: 'v', eventPos: "pageX", origin: "left", + split: "width", pxSplit: "offsetWidth", side1: "Left", side2: "Right", + fixed: "height", pxFixed: "offsetHeight", side3: "Top", side4: "Bottom" + }, + h: { // Horizontal splitters: + type: 'h', eventPos: "pageY", origin: "top", + split: "height", pxSplit: "offsetHeight", side1: "Top", side2: "Bottom", + fixed: "width", pxFixed: "offsetWidth", side3: "Left", side4: "Right" + } + }[vh]); + opts.barStateClasses = [opts.barNormalClass, opts.barHoverClass, opts.barActiveClass, opts.barLimitClass].join(' '); + + // Create jQuery object closures for splitter and both panes + var splitter = $(this).css({position: "relative"}).addClass(opts.splitterClass); + var panes = $(">*", splitter[0]).addClass(opts.paneClass).css({ + position: "absolute", // positioned inside splitter container + "z-index": "1", // splitbar is positioned above + "-moz-outline-style": "none" // don't show dotted outline + }); + var A = $(panes[0]), B = $(panes[1]); // A = left/top, B = right/bottom + opts.dockPane = opts.dock && (/right|bottom/.test(opts.dock)? B:A); + + // Focuser element, provides keyboard support; title is shown by Opera accessKeys + var focuser = $('') + .attr({accessKey: opts.accessKey, tabIndex: opts.tabIndex, title: opts.splitbarClass}) + .bind(($.browser.opera?"click":"focus")+opts.eventNamespace, + function(){ this.focus(); bar.addClass(opts.barActiveClass) }) + .bind("keydown"+opts.eventNamespace, function(e){ + var key = e.which || e.keyCode; + var dir = key==opts["key"+opts.side1]? 1 : key==opts["key"+opts.side2]? -1 : 0; + if ( dir ) + resplit(A[0][opts.pxSplit]+dir*opts.pxPerKey, false); + }) + .bind("blur"+opts.eventNamespace, + function(){ bar.removeClass(opts.barActiveClass) }); + + // Splitbar element + var bar = $('
') + .insertAfter(A).addClass(opts.barClass).addClass(opts.barStateClass) + .append(focuser).attr({unselectable: "on"}) + .css({position: "absolute", "user-select": "none", "-webkit-user-select": "none", + "-khtml-user-select": "none", "-moz-user-select": "none", "z-index": "100"}) + .bind("mousedown"+opts.eventNamespace, startSplitMouse) + .bind("mouseover"+opts.eventNamespace, function(){ + $(this).addClass(opts.barHoverClass); + }) + .bind("mouseout"+opts.eventNamespace, function(){ + $(this).removeClass(opts.barHoverClass); + }); + // Use our cursor unless the style specifies a non-default cursor + if ( /^(auto|default|)$/.test(bar.css("cursor")) ) + bar.css("cursor", opts.cursor); + + // Cache several dimensions for speed, rather than re-querying constantly + // These are saved on the A/B/bar/splitter jQuery vars, which are themselves cached + // DA=dimension adjustable direction, PBF=padding/border fixed, PBA=padding/border adjustable + bar._DA = bar[0][opts.pxSplit]; + splitter._PBF = dimSum(splitter, "border"+opts.side3+"Width", "border"+opts.side4+"Width"); + splitter._PBA = dimSum(splitter, "border"+opts.side1+"Width", "border"+opts.side2+"Width"); + A._pane = opts.side1; + B._pane = opts.side2; + $.each([A,B], function(){ + this._splitter_style = this.style; + this._min = opts["min"+this._pane] || dimSum(this, "min-"+opts.split); + this._max = opts["max"+this._pane] || dimSum(this, "max-"+opts.split) || 9999; + this._init = opts["size"+this._pane]===true ? + parseInt($.curCSS(this[0],opts.split),10) : opts["size"+this._pane]; + }); + + // Determine initial position, get from cookie if specified + var initPos = A._init; + if ( !isNaN(B._init) ) // recalc initial B size as an offset from the top or left side + initPos = splitter[0][opts.pxSplit] - splitter._PBA - B._init - bar._DA; + if ( opts.cookie ) { + if ( !$.cookie ) + alert('jQuery.splitter(): jQuery cookie plugin required'); + initPos = parseInt($.cookie(opts.cookie),10); + $(window).bind("unload"+opts.eventNamespace, function(){ + var state = String(bar.css(opts.origin)); // current location of splitbar + $.cookie(opts.cookie, state, {expires: opts.cookieExpires || 365, + path: opts.cookiePath || document.location.pathname}); + }); + } + if ( isNaN(initPos) ) // King Solomon's algorithm + initPos = Math.round((splitter[0][opts.pxSplit] - splitter._PBA - bar._DA)/2); + + // Resize event propagation and splitter sizing + if ( opts.anchorToWindow ) + opts.resizeTo = window; + if ( opts.resizeTo ) { + splitter._hadjust = dimSum(splitter, "borderTopWidth", "borderBottomWidth", "marginBottom"); + splitter._hmin = Math.max(dimSum(splitter, "minHeight"), 20); + $(window).bind("resize"+opts.eventNamespace, function(){ + var top = splitter.offset().top; + var eh = $(opts.resizeTo).height(); + splitter.css("height", Math.max(eh-top-splitter._hadjust, splitter._hmin)+"px"); + if ( !$.browser.msie ) splitter.trigger("resize"); + }).trigger("resize"+opts.eventNamespace); + } + else if ( opts.resizeToWidth && !$.browser.msie ) { + $(window).bind("resize"+opts.eventNamespace, function(){ + splitter.trigger("resize"); + }); + } + + // Docking support + if ( opts.dock ) { + splitter + .bind("toggleDock"+opts.eventNamespace, function() { + var pw = opts.dockPane[0][opts.pxSplit]; + splitter.trigger(pw?"dock":"undock"); + }) + .bind("dock"+opts.eventNamespace, function(){ + var pw = A[0][opts.pxSplit]; + if ( !pw ) return; + bar._pos = pw; + var x={}; + x[opts.origin] = opts.dockPane==A? 0 : + splitter[0][opts.pxSplit] - splitter._PBA - bar[0][opts.pxSplit]; + bar.animate(x, opts.dockSpeed||1, opts.dockEasing, function(){ + bar.addClass(opts.barDockedClass); + resplit(x[opts.origin]+1); + }); + }) + .bind("undock"+opts.eventNamespace, function(){ + var pw = opts.dockPane[0][opts.pxSplit]; + if ( pw ) return; + var x={}; x[opts.origin]=bar._pos+"px"; + bar.removeClass(opts.barDockedClass) + .animate(x, opts.undockSpeed||opts.dockSpeed||1, opts.undockEasing||opts.dockEasing, function(){ + resplit(bar._pos); + bar._pos = null; + }); + }); + if ( opts.dockKey ) + $('') + .attr({accessKey: opts.dockKey, tabIndex: -1}).appendTo(bar) + .bind($.browser.opera?"click":"focus", function(){ + splitter.trigger("toggleDock"); this.blur(); + }); + bar.bind("dblclick", function(){ splitter.trigger("toggleDock"); }) + } + + + // Resize event handler; triggered immediately to set initial position + splitter + .bind("destroy"+opts.eventNamespace, function(){ + $([window, document]).unbind(opts.eventNamespace); + bar.unbind().remove(); + panes.removeClass(opts.paneClass); + splitter + .removeClass(opts.splitterClass) + .add(panes) + .unbind(opts.eventNamespace) + .attr("style", function(el){ + return this._splitter_style||""; //TODO: save style + }); + splitter = bar = focuser = panes = A = B = opts = args = null; + }) + .bind("resize"+opts.eventNamespace, function(e, size){ + // Custom events bubble in jQuery 1.3; avoid recursion + if ( e.target != this ) return; + // Determine new width/height of splitter container + splitter._DF = splitter[0][opts.pxFixed] - splitter._PBF; + splitter._DA = splitter[0][opts.pxSplit] - splitter._PBA; + // Bail if splitter isn't visible or content isn't there yet + if ( splitter._DF <= 0 || splitter._DA <= 0 ) return; + // Re-divvy the adjustable dimension; maintain size of the preferred pane + resplit(!isNaN(size)? size : (!(opts.sizeRight||opts.sizeBottom)? A[0][opts.pxSplit] : + splitter._DA-B[0][opts.pxSplit]-bar._DA)); + setBarState(opts.barNormalClass); + }) + .trigger("resize" , [initPos]); + }); +}; + +})(jQuery);