【问题标题】:splitter - resize specific nodesplitter - 调整特定节点的大小
【发布时间】:2016-07-02 17:43:30
【问题描述】:

拖动拆分器时如何在 xul 窗口中调整特定节点的大小? 由于 xul 窗口的复杂性,无法使用 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>

【问题讨论】:

  • 好的,添加示例。
  • 我的错,把两个例子混在一起了……现在修复了。两个拆分器都应该调整黄色框的大小,但是顶部拆分器应该只影响蓝色行,底部拆分器应该只影响绿色行。旁注,如果打开 xul 文件并将底部拆分器向上移动后会发生奇怪的行为,它会向下跳,意外地调整黑色 #4 行的大小。

标签: javascript xul


【解决方案1】:

没有常规方法可以为 &lt;splitter&gt; 指定特定节点以调整大小。

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

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

  • flex 属性已重载。它用于控制对象最初是如何放置的,在调整窗口大小时如何调整它们的大小,以及它们是如何被所有&lt;splitters&gt; 调整大小的。您很可能希望在每种情况下发生不同的事情。
  • 库存&lt;splitter&gt; 代码中存在错误。我观察到至少有几个不同的,包括一些被明确声明为不灵活的元素仍然被调整大小。 IIRC,这些似乎主要是在尝试使用容器内的&lt;splitter&gt; 来更改超出该容器的对象的大小时。
  • 无法明确指定(例如通过 ID)&lt;splitter&gt; 要调整大小的元素。

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

如果您打算使用 JavaScript 进行自己的处理,您似乎需要通过收听 mouse events 来完全实现该功能。 &lt;splitter&gt; 的移动似乎不会触发 drag events。我认为这是因为移动 &lt;splitter&gt; 不被视为“拖放”操作的一部分(即,您实际上并没有将其拾起并将其放在放置目标上)。虽然我希望能够监听拖动事件,但很明显它们没有触发。

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

&lt;splitter&gt;添加指定ID:

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

为了在特定的&lt;splitter&gt; 上使用它,您需要调用其中一个公共函数来使用&lt;splitter&gt; 的ID 或&lt;splitter&gt; 元素注册&lt;splitter&gt;。例如,示例 XUL 中的两个 &lt;spliter&gt; 元素(根据您问题中的代码进行了一些修改)注册到:

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>

这个例子看起来像:

[注意:虽然编写的代码可以同时使用垂直和水平&lt;splitters&gt;,但在上面的示例中,我只使用垂直&lt;splitters&gt; 对其进行了测试。]

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

有多种方式(其中许多以各种组合进行交互)可用于控制通过&lt;splitter&gt; 元素或整体布局的一般调整大小来调整对象或对象的大小。其中包括使用&lt;splitter&gt;resizebeforeresizeafter 属性以及XUL 中元素的flex 属性的适当值,并可能包括box、@987654329 中的这些元素@ 或 vbox 元素仅用于分发“flex”。此外,可能需要使用各种attributes available to an XUL element(其他 MDN 文档:123)为正在调整大小的区域内的每个元素指定各种约束。

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

反复试验:
要在特定布局中准确获得所需的功能,您可能需要进行一些试验和错误。您可能会发现 XUL 原型设计工具 XUL Explorer 在这样做时会有所帮助,具体取决于您正在做什么。例如,如果您的代码动态构建您的 XUL,那么 XUL Explorer 就没有多大帮助。然而,即使在动态构建我的 XUL 布局时,我也使用 XUL Explorer 快速查看我正在构建的 XUL 的变化的外观/行为。

您的(原始)具体示例:
[注意:以下内容基于问题中包含的第一个示例。该示例远没有现在问题中的示例那么复杂。特别是,它在容器内没有&lt;splitter&gt;(在新示例中为&lt;grid&gt;),这是希望在该容器外调整元素大小的。]

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

与许多 UI 问题一样,很难用语言表达您希望发生的一切。例如,在本例中,您没有为其他 &lt;vbox&gt; 元素指定起始大小。为了显示&lt;splitter&gt; 发生的更多情况并在绿色&lt;vbox&gt; 上为flex 使用不同的值,我为其他&lt;vbox&gt; 元素包含了一个起始/默认height。这将导致这些元素从该高度开始,然后只有在绿色 &lt;vbox&gt; 缩小到其最小高度时才从该高度缩小到其最小高度。

注意:您使用的是style 属性,其中一部分是min-height: 30px;。除非您要将它放在 CSS 类中,否则使用 XUL 属性 minheight 可能会更好/更容易。如果您愿意,这样做会更容易以编程方式进行更改。鉴于这是示例代码,您可能只是将其内联,以便不必同时包含 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>

使用&lt;splitter&gt; 调整大小时的样子:

【讨论】:

  • 感谢您提供如此详细的解释和工作示例!不幸的是,经过一些额外的测试,我的例子还没有完成......我没想到纯 xul 解决方案,所以这个例子被证明过于精简。我已经用更完整的示例更新了我的原始问题,恐怕无法完全在 xul 中修复。
  • @vanowm:我已经大大更新了我的答案。现在包含适用于您问题中当前示例的 JavaScript 代码。事实上,代码实现了一个通用方法,在&lt;splitter&gt; 的XUL 属性中使用ID 来明确指定&lt;splitter&gt; 将调整大小的两个元素。
  • 非常感谢!这是完美的!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2018-08-29
  • 2018-09-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-05-01
  • 1970-01-01
相关资源
最近更新 更多