Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

splitter - resize specific node

Tags:

javascript

xul

How can I resize a specific node in xul window when a splitter is dragged? Can't use resizebefore/resizeafter attributes due to complexity of the xul window.

I've tried use ondrag event on splitter, but it's not firing at all. ondragstart event fires fine and I can use event.offsetY to capture how many pixels the splitter moved. Using that value I could add it to the height of need element, which works fine, but unfortunately this event fires only once per drag session.

Any ideas?

Thank you.

An example to test it with. Due to complexity of my original xul I cannot alter the xul structure (user can hide and change order of rows), so probably only javascript solution is viable):

<?xml version="1.0"?>

<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>

<window id="testWindow"
            title="testing resizing element by splitter"
            xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
            style="color: white;"
>
  <vbox id="resizeme" flex="1" style="background-color: yellow; color: black;">
    <hbox flex="1">
      <label value="#1"/>
      <hbox flex="1" align="center" pack="center">
        <label value="Resizable by top and bottom splitter"/>
      </hbox>
    </hbox>
  </vbox>
  <splitter tooltiptext="Top splitter"/>
  <grid flex="1">
    <columns>
        <column/>
        <column flex="1"/>
    </columns>
    <rows>
      <row style="background-color: black;">
        <label value="#2"/>
        <vbox flex="1" pack="center" align="center">
         <label value="Must stay constant size at all times"/>
        </vbox>
      </row>
      <row flex="1" style="background-color: blue;">
        <label value="#3"/>
        <vbox flex="1" pack="center" align="center">
          <label value="Resizable by top splitter only"/>
        </vbox>
      </row>
      <row style="background-color: black;">
        <label value="#4"/>
        <hbox flex="1" pack="center" align="center">
          <label value="Must stay constant size at all times, content must fit"/>
          <button label="blah"/>
        </hbox>
      </row>
      <splitter tooltiptext="Bottom splitter"/>
      <row flex="1" style="background-color: green;">
        <label value="#5"/>
        <vbox flex="1" pack="center" align="center">
        <label value="Resizable by bottom splitter only"/>
        </vbox>
      </row>
      <row style="background-color: black;">
        <label value="#6"/>
        <vbox flex="1" pack="center" align="center">
         <label value="Must stay constant size at all times"/>
        </vbox>
      </row>
    </rows>
  </grid>
</window>
like image 977
vanowm Avatar asked Oct 31 '22 00:10

vanowm


1 Answers

There is no stock way to specify a specific node for a <splitter> to resize.

As with all resizing in XUL, the intent is that you should be able to code your XUL such that you can have the UI resize your layout, or a inner portion of it using <splitter> elements, automatically, without any need for having your JavaScript listening to events and performing the resize. However, you certainly can have your JavaScript perform the <splitter> resizing. You would generally do so when, you are doing something which is complex, you have run into one of the bugs in the <splitter> implementation, you just find it easier than fine tuning your XUL to use the stock functionality, or if you just want the complete control which writing your own code provides. The point is that the <splitter> and the underlying system should be performing the entirety of the resize for you.

However, <splitter> elements do have significant limitations, and a few bugs, which may result in your needing to write your own resizing code. Those limitations include:

  • The flex property is overloaded. It is used to control how objects are initially placed, how they are resized when the window is resized, and how they are resized by all <splitters>. It is quite possible that you want different things to happen in each case.
  • There are bugs in the stock <splitter> code. I have observed at least a couple of different ones, including some where elements which are explicitly declared as not flexible are still resized. IIRC, these appear mostly to be when attempting to use a <splitter> that is within a container to change the size of an object outsize of that container.
  • There is no ability to explicitly specify (e.g. by ID) the elements that the <splitter> is to resize.

[I was thinking about more limitations, but I am not remembering them at the moment.]

If you are going to use JavaScript to do your own processing, it appears you will need to implement the functionality completely by listening to mouse events. The movement of a <splitter> does not appear to fire drag events. I assume that this is because moving a <splitter> is not considered to be part of a "drag-and-drop" action (i.e. you don't actually pick it up and drop it on a drop target). While I expected to be able to listen the drag events, it is clear they are not firing.

To me, the most significant functionality that is lacking in <splitters> is the lack of the ability to specify by ID the two elements which are to be resized. Obviously, from the title of your question, it is clear that this is something you also find to be significantly lacking.

Adding specifying an ID to <splitter>:

The following code implements, and provides an example of using, <splitter> elements which specify the ID of the elements to be resized in the resizebefore and resizeafter attributes within the XUL.

In order to use it on a particular <splitter>, you will need to call one of the public functions to register the <splitter> using either the <splitter>'s ID or the <splitter> element. For example, the two <spliter> elements in the example XUL (somewhat modified from the code in your question) are registered with:

splitterById.registerSplitterById("firstSplitter");
splitterById.registerSplitterById("secondSplitter"); 

splitterById.js:

 /******************************************************************************
 * splitterById                                                                *
 *                                                                             *
 * XUL <splitter> elements which are registered will resize only the two       *
 * specific elements for which the ID is contained in the <splitter>'s         *
 * resizebefore and resizeafter attributes. The orient attribute is used to    *
 * specify if the <splitter> is resizing in the "vertical" or "horizontal"     *
 * orientation. "vertical" is the default.                                     *
 *                                                                             *
 * For a particular <splitter> this is an all or nothing choice.  In other     *
 * words, you _must_ specify both a before and after element (e.g. You can not *
 * mix using an ID on the resizebefore and not on resizeafter with the         *
 * expectation that the after will be resized with the normal <splitter>       *
 * functionality.                                                              *
 *                                                                             *
 * On both elements, the attributes minheight, maxheight, minwidth, and        *
 * maxwidth will be obeyed.  It may be necessary to explicitly set these       *
 * attributes in order to prevent one or the other element from growing or     *
 * shrinking when the other element is prevented from changing size by other   *
 * XUL UI constraints.  For example, an element can not be reduced in size     *
 * beyond the minimum needed to display it. This code does not check for these *
 * other constraints. Thus, setting these attributes, at least the ones        *
 * specifying the minimum height or minimum width will almost always be        *
 * desirable.                                                                  *
 *                                                                             *
 * Public methods:                                                             *
 *   registerSplitterById(id) : registers the <splitter> with that ID          *
 *   registerSplitterByElement(element) : registers the <splitter> element     *
 *   unregisterSplitterById(id) : unregisters the <splitter> with that ID      *
 *   unregisterSplitterByElement(element) : unregisters the <splitter> element *
 *                                                                             *
 ******************************************************************************/

var splitterById = (function(){

    let beforeER = {};
    let afterER = {};
    let splitIsVertical = true;
    let origClientY = -1;
    let origClientX = -1;

    function ElementRec(_el) {
        this.element = _el;
        this.origHeight = getElementHeight(_el);
        this.origWidth = getElementWidth(_el);
        //The .minHeight and .maxHeight attributes/properties
        //  do not appear to be valid when first starting, so don't
        //  get them here.
        //this.minHeight = getMinHeightAsValue(_el);
        //this.maxHeight = getMaxHeightAsValue(_el);
    }
    function getElementHeight(el) {
        //.height can be invalid and does not indicate the actual
        //  height displayed, only the desired height.
        let boundingRec = el.getBoundingClientRect();
        return boundingRec.bottom - boundingRec.top;
    }
    function getElementWidth(el) {
        //.width can be invalid and does not indicate the actual
        //  width displayed, only the desired width.
        let boundingRec = el.getBoundingClientRect();
        return boundingRec.right - boundingRec.left;
    }
    function getMaxHeightAsValue(el) {
        return asValueWithDefault(el.maxHeight,99999999);
    }
    function getMinHeightAsValue(el) {
        return asValueWithDefault(el.minHeight,0);
    }
    function getMaxWidthAsValue(el) {
        return asValueWithDefault(el.maxHeight,99999999);
    }
    function getMinWidthAsValue(el) {
        return asValueWithDefault(el.minHeight,0);
    }
    function asValueWithDefault(value,myDefault) {
        if(value === null || value === "" || value === undefined) {
            value = myDefault;
        }
        //What is returned by the various attributes/properties is
        //  usually text, but not always.
        value++;
        value--;
        return value;
    }
    function storeSplitterStartingValues(el) {
        //Remember if the splitter is vertical or horizontal,
        //  references to the elements being resized and their initial sizes.
        splitIsVertical = true;
        if(el.getAttribute("orient") === "horizontal") {
            splitIsVertical = false;
        }
        beforeER=new ElementRec(document.getElementById(el.getAttribute("resizebefore")));
        afterER=new ElementRec(document.getElementById(el.getAttribute("resizeafter")));
        if(beforeER.element === undefined || afterER.element === undefined) {
            //Did not find one or the other element. We must have both.
            return false;
        }
        return true;
    }
    function mousedownOnSplitter(event) {
        if(event.button != 0) {
            //Only drag with the left button.
            return;
        }
        //Remember the mouse position at the start of the resize.
        origClientY = event.clientY;
        origClientX = event.clientX;
        //Remember what we are acting upon
        if(storeSplitterStartingValues(event.target)) {
            //Start listening to mousemove and mouse up events on the whole document.
            document.addEventListener("mousemove",resizeSplitter,true);
            document.addEventListener("mouseup",endResizeSplitter,true);
        }
    }
    function endResizeSplitter(event) {
        if(event.button != 0) {
            //Only drag with the left button.
            return;
        }
        removeResizeListeners();
    }
    function removeResizeListeners() {
        //Don't listen to document mousemove, mouseup events when not
        //  actively resizing.
        document.removeEventListener("mousemove",resizeSplitter,true);
        document.removeEventListener("mouseup",endResizeSplitter,true);
    }
    function resizeSplitter(event) {
        //Prevent the splitter from acting normally:
        event.preventDefault();
        event.stopPropagation();

        //Get the new size for the before and after elements based on the
        //  mouse position relative to where it was when the mousedown event fired.
        let newBeforeSize = -1;
        let newAfterSize = -1;
        if(splitIsVertical) {
            newBeforeSize = beforeER.origHeight + (event.clientY - origClientY);
            newAfterSize  = afterER.origHeight  - (event.clientY - origClientY);
        } else {
            newBeforeSize = beforeER.origWidth + (event.clientX - origClientX);
            newAfterSize  = afterER.origWidth  - (event.clientX - origClientX);
        }

        //Get any maximum and minimum sizes defined for the elements we are changing.
        //Get these here because they may not have been populated/valid
        //  when the drag was first initiated (i.e. we should have been able
        //  to do this only once when the mousedown event fired, but testing showed
        //  the values are not necessarily valid at that time.
        let beforeMinSize;
        let beforeMaxSize;
        let afterMinSize;
        let afterMaxSize;
        if(splitIsVertical) {
            beforeMinSize = getMinHeightAsValue(beforeER.element);
            beforeMaxSize = getMaxHeightAsValue(beforeER.element);
            afterMinSize  = getMinHeightAsValue(afterER.element);
            afterMaxSize  = getMaxHeightAsValue(afterER.element);
        } else {
            beforeMinSize = getMinWidthAsValue(beforeER.element);
            beforeMaxSize = getMaxWidthAsValue(beforeER.element);
            afterMinSize  = getMinWidthAsValue(afterER.element);
            afterMaxSize  = getMaxWidthAsValue(afterER.element);
        }

        //Apply the limits to sizes we want to change to.
        //These do appear to work better sequentially rather than optimized.
        if(newBeforeSize < beforeMinSize) {
            //Set to beforeMinSize limit if have passed.
            let diff = beforeMinSize - newBeforeSize;
            newBeforeSize += diff;
            newAfterSize -= diff;
        }
        if(newBeforeSize > beforeMaxSize) {
            //Set to beforeMaxSize limit if have passed.
            let diff = beforeMaxSize - newBeforeSize;
            newBeforeSize += diff;
            newAfterSize -= diff;
        }
        if(newAfterSize < afterMinSize) {
            //Set to afterMinSize limit if have passed.
            let diff = afterMinSize - newAfterSize;
            newAfterSize += diff;
            newBeforeSize -= diff;
        }
        if(newAfterSize > afterMaxSize) {
            //Set to afterMaxSize limit if have passed.
            let diff = afterMaxSize - newAfterSize;
            newAfterSize += diff;
            newBeforeSize -= diff;
        }

        //Don't make any changes if we are still violating the limits.
        //There are some pathological cases where we could still be violating
        //  a limit (where limits are set such that it is not possible to have
        //  a valid height).
        if(newBeforeSize < beforeMinSize || newBeforeSize > beforeMaxSize
            || newAfterSize < afterMinSize || newAfterSize > afterMaxSize) {
            return;
        }

        //Make the size changes
        if(splitIsVertical) {
            beforeER.element.height = newBeforeSize;
            afterER.element.height = newAfterSize;
        } else {
            beforeER.element.width = newBeforeSize;
            afterER.element.width = newAfterSize;
        }
    }
    function _registerSplitterById(id) {
        _registerSplitterByElement(document.getElementById(id));
    }
    function _registerSplitterByElement(el) {
        el.addEventListener("mousedown",mousedownOnSplitter,false);
    }
    function _unregisterSplitterById(id) {
        _unregisterSplitterByElement(document.getElementById(id));
    }
    function _unregisterSplitterByElement(el) {
        el.removeEventListener("mousedown",mousedownOnSplitter,false);
        removeResizeListeners();
    }

    return {
        registerSplitterById : function(id) {
            _registerSplitterById(id);
        },
        registerSplitterByElement : function(el) {
            _registerSplitterByElement(el);
        },
        unregisterSplitterById : function(id) {
            _unregisterSplitterById(id);
        },
        unregisterSplitterByElement : function(el) {
            _unregisterSplitterByElement(el);
        }
    };
})();

Example XUL (modified from the question):

<?xml version="1.0"?>
<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
<window id="testWindow"
        title="testing resizing element by splitter"
        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
        style="color: white;"
>
  <vbox id="resizeme" height="120" minheight="30" maxheight="250"
        style="background-color: yellow; color: black;">
     <hbox flex="1">
      <label value="#1"/>
      <hbox flex="1" align="center" pack="center">
        <label id="yellowLabel" value="Resizable by top and bottom splitter"/>
      </hbox>
    </hbox>
  </vbox>
  <splitter id="firstSplitter" tooltiptext="Top splitter" orient="vertical"
            resizebefore="resizeme" resizeafter="blueVbox"/>
  <grid>
    <columns>
        <column/>
        <column flex="1"/>
    </columns>
    <rows>
      <row style="background-color: black;">
        <label value="#2"/>
        <vbox pack="center" align="center">
         <label value="Must stay constant size at all times"/>
        </vbox>
      </row>
      <row id="blueRow" style="background-color: blue;">
        <label value="#3"/>
        <vbox id="blueVbox" height="120" minheight="30" pack="center" align="center">
          <label id="blueLabel" value="Resizable by top splitter only"/>
        </vbox>
      </row>
      <row style="background-color: black;">
        <label value="#4"/>
        <hbox pack="center" align="center">
          <label value="Must stay constant size at all times, content must fit"/>
          <button label="blah"/>
        </hbox>
      </row>
      <splitter id="secondSplitter" tooltiptext="Bottom splitter" orient="vertical"
                resizebefore="resizeme" resizeafter="greenVbox"/>
      <row id="greenRow" style="background-color: green;">
        <label value="#5"/>
        <vbox id="greenVbox" height="120" minheight="30" pack="center" align="center">
        <label id="greenLabel" value="Resizable by bottom splitter only"/>
        </vbox>
      </row>
      <row style="background-color: black;">
        <label value="#6"/>
        <vbox pack="center" align="center">
         <label value="Must stay constant size at all times"/>
        </vbox>
      </row>
    </rows>
  </grid>
<script type="application/x-javascript" src="file://b:/SplitterById.js"/>
<script type="application/javascript">
  <![CDATA[
    splitterById.registerSplitterById("firstSplitter");
    splitterById.registerSplitterById("secondSplitter");
  ]]>
</script>
</window>

This example looks like:
Resizing using splitterById

[Note: While the code is written to work with both vertical and horizontal <splitters>, I have only tested it with the vertical <splitters> in the above example.]

Using <splitter> normally (without listening for events):
The example you initially had in your question was significantly less complex than the example you have now. It was quite possible to code it using strictly XUL to enable the <splitter> to function the way you requested.

There are multiple ways (many of which interact in various combinations) which can be used to control which object, or objects are resized via a <splitter> element, or a general resize of the overall layout. Among other things, these include using the resizebefore and resizeafter attributes of the <splitter> in combination with appropriate values for the flex attribute on the elements in your XUL and potentially including those elements in box, hbox, or vbox elements which are used only to distribute the "flex". In addition, it may be desirable to specify a variety of constraints for each element within the area which is being resized using the various attributes available to an XUL element (additional MDN docs: 1, 2, 3).

One of the things it appears you have missed is that the flex attribute can be other values than just 1 or 0. The numeric value is used to specify proportionally the amount of resizing that is done on a specific element relative to the other elements which are affected by the resize (be that resize due to a <splitter>, or a resize of an container element (e.g. <window>, <box>, <vbox>, <hbox>, etc.) which includes the element in which you are interested).

Trial and error:
To get exactly the functionality you desire in a particular layout you will probably need to perform some trial and error. You may find the XUL prototyping tool XUL Explorer to be helpful in doing so, depending on exactly what you are doing. For instance, if your code dynamically builds your XUL then XUL Explorer is not as much help. However, even when dynamically building my XUL layout, I have used XUL Explorer to quickly see how variations on the XUL I was building would look/behave.

Your (original) specific example:
[Note: The following is based on the first example which was included in the question. That example was significantly less complex than the one which is now in the question. In particular, it did not have a <splitter> inside a container (a <grid> in the new example) which was desired to have resizing elements outside of that container.]

For your specific example, the behavior you describe can be achieved by setting the flex value on the green <vbox> to a large value relative to the other elements.

As with many UI issues it is difficult to say in words everything you desire to have occurring. For example, in this instance, you have specified no starting sizes for the other <vbox> elements. In order to show more of what happens with the <splitter> and using a different value for flex on the green <vbox>, I have included a starting/default height for the other <vbox> elements. This will result in those elements starting at that height and then only shrinking from that down to their minimum height once the green <vbox> has shrunk to its minimum height.

Note: You are using the style attribute with a portion of it being min-height: 30px;. Unless you are going to put this in a CSS class, then it might be better/easier to use the XUL attribute minheight. Doing so would make it easier to change programmatically, should you desire to do so. Given that this is example code, you may have just in-lined that in order to not have to also include a CSS file.

The code:

<?xml version="1.0"?>

<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>

<window id="testWindow"
            title="testing resizing element by splitter"
            xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
>
  <hbox flex="1">
    <vbox flex="1">
      <vbox flex="1" height="80" pack="center" align="center" 
            style="background-color: blue; min-height: 30px; color: white;">
        <label value="this should stay constant size until green element reached its minimum size"/>
      </vbox>
      <vbox id="resizeme" flex="10000" height="80" pack="center" align="center" 
            style="background-color: green; min-height: 30px; color: white;">
        <label value="only this should be resized until it reached minimum size of 30px"/>
      </vbox>
      <vbox flex="1" height="80" pack="center" align="center"
            style="background-color: red; min-height: 30px; color: white;">
        <label value="this should stay constant size until green element reached its minimum size"/>
      </vbox>
    </vbox>
  </hbox>

  <splitter/>
  <vbox flex="1"/>

</window>

What it looks like when using the <splitter> to resize:
Resizing <vbox> elements with flex

like image 76
Makyen Avatar answered Nov 11 '22 06:11

Makyen