splitter - 调整特定节点的大小

splitter - resize specific node

如何在拖动拆分器时调整 xul window 中特定节点的大小? 由于 xul window.

的复杂性,无法使用 resizebefore/resizeafter 属性

我试过在拆分器上使用 ondrag 事件,但它根本没有触发。 ondragstart 事件正常触发,我可以使用 event.offsetY 来捕获分离器移动了多少像素。 使用该值,我可以将它添加到需要元素的高度,这工作正常,但不幸的是,每个拖动会话仅触发一次此事件。

有什么想法吗?

谢谢。

用于测试的示例。由于我原来的 xul 的复杂性,我无法改变 xul 结构(用户可以隐藏和更改行的顺序),所以可能只有 javascript 解决方案是可行的:

<?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>

没有为 <splitter> 调整大小指定特定节点的常用方法。

与 XUL 中的所有调整大小一样,意图 是您应该能够对 XUL 进行编码,这样您就可以 UI 调整布局大小,或者它的内部部分自动使用 <splitter> 元素,无需让 JavaScript 监听事件并执行调整大小。但是,您当然可以让 JavaScript 执行 <splitter> 调整大小。你通常会这样做,当你正在做一些复杂的事情时,你有 运行 进入 <splitter> 实现中的一个错误,你只是发现它比微调你的 XUL 更容易使用股票功能,或者如果您只想要编写自己的代码所提供的完整控制。关键是 <splitter> 和底层系统 应该 为您执行全部调整大小。

但是,<splitter> 元素确实有很大的局限性和一些错误,这可能会导致您需要编写自己的调整大小代码。这些限制包括:

  • flex 属性 过载。它用于控制 object 的初始放置方式、window 调整大小时它们的大小调整方式以及所有 <splitters> 调整它们大小的方式。您很可能希望在每种情况下发生不同的事情。
  • 股票 <splitter> 代码中存在错误。我观察到至少有几个不同的,包括一些明确声明为不灵活的元素仍然调整大小的地方。 IIRC,这些似乎主要是在尝试使用容器内的 <splitter> 来更改该容器的 object 超大尺寸时。
  • 无法明确指定(例如通过 ID)<splitter> 要调整大小的元素。

[我在想更多的限制,但我现在不记得了。]

如果您要使用 JavaScript 进行自己的处理,您似乎需要通过监听 mouse events. The movement of a <splitter> does not appear to fire drag events 来完全实现功能。我假设这是因为移动 <splitter> 不被认为是 "drag-and-drop" 动作的一部分(即你实际上并没有拿起它并将它放在放置目标上)。虽然我希望能够听到拖动事件,但很明显它们没有触发。

对我来说,<splitters> 中缺少的最重要的功能是无法通过 ID 指定要调整大小的两个元素。显然,从你的问题标题来看,很明显,这也是你发现明显缺乏的东西。

添加指定 ID 到 <splitter>:

以下代码实现并提供了使用 <splitter> 元素的示例,这些元素在 XUL 的 resizebeforeresizeafter 属性中指定要调整大小的元素的 ID .

为了在特定的 <splitter> 上使用它,您需要调用 public 函数之一来使用 <splitter>' 注册 <splitter> s ID 或 <splitter> 元素。例如,示例 XUL 中的两个 <spliter> 元素(根据您问题中的代码进行了一些修改)注册为:

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);
        }
    };
})();

XUL 示例(根据问题修改):

<?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>

这个例子看起来像:

[注意:虽然代码被编写为同时适用于垂直和水平 <splitters>,但我在上面的示例中仅使用垂直 <splitters> 对其进行了测试。]

正常使用<splitter>(不监听事件):
您最初在问题中使用的示例比您现在使用的示例复杂得多。完全可以使用严格的 XUL 对其进行编码,以使 <splitter> 能够按照您要求的方式运行。

有多种方法(其中许多以各种组合相互作用)可用于控制通过 <splitter> 元素调整哪些 object 或 object 的大小,或总体布局的一般调整大小。其中包括使用 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).

您似乎错过的一件事是 flex 属性可以是其他值,而不仅仅是 10。该数值用于按比例指定相对于受调整大小影响的其他元素(是由于 <splitter> 或容器元素的调整大小)在特定元素上完成的调整大小(例如 <window><box><vbox><hbox> 等)其中包括您感兴趣的元素。

反复试验:
要在特定布局中准确获得您想要的功能,您可能需要进行一些试验和错误。您可能会发现 XUL 原型制作工具 XUL Explorer 对此有所帮助,具体取决于您在做什么。例如,如果您的代码动态构建 XUL,那么 XUL Explorer 就没有那么大的帮助。豪呃,即使在动态构建我的 XUL 布局时,我也使用 XUL Explorer 快速查看我正在构建的 XUL 的变化 look/behave.

你的(原)具体例子:
[注意:以下内容基于问题中包含的第一个示例。该示例远没有现在问题中的示例复杂。特别是,它在容器内没有 <splitter>(新示例中的 <grid>),需要在该容器外调整元素的大小。]

对于您的具体示例,您描述的行为可以通过将绿色 <vbox> 上的 flex 值设置为相对于其他元素较大的值来实现。

与许多 UI 问题一样,很难用语言表达您希望发生的一切。例如,在本例中,您没有为其他 <vbox> 元素指定起始大小。为了显示更多 <splitter> 发生的情况并在绿色 <vbox> 上使用不同的 flex 值,我为 starting/default height 添加了一个其他 <vbox> 个元素。这将导致这些元素从该高度开始,然后仅在绿色 <vbox> sh运行k 达到其最小高度后才从该高度缩小到它们的最小高度。

注意:您正在使用 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。如果您愿意,这样做可以更轻松地以编程方式进行更改。鉴于这是示例代码,您可能只有 in-lined,以便不必还包含 CSS 文件。

代码:

<?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>

使用 <splitter> 调整大小时的效果: