xmlplus组件设计系列之分隔框(DividedBox)(8)
作者:qudou
分隔框(DividedBox)是一种布局类组件,可以分为两类,其中一类叫水平分隔框(HDividedBox),另一类叫垂直分隔框(VDividedBox)。水平分隔框会将其子级分为两列,而垂直分隔框则会将其子级分为两行。列与列之间以及行与行之间一般都会有一条可以拖动的用以改变子级组件大小的分隔条。下面仅以垂直分隔框为例来介绍此类组件是如何设计以及实现的。
成品组件用例
按照以往的设计经验,我们可以先写出想像中的成品组件用例,这将有助于我们后续的进一步的设计与实现。垂直分隔框既然是布局类的组件,那么它也一定是一个容器,该容器包含了上述我们提到的三种子级组件。为了使用方便,我们不应该把分隔框也写进去,分隔框应该由组件内部实现的。经过分析,我们得到下面的一个应用示例:
Example1: { css: "#example div { width: 80%; height: 80%; background: #AAA; }", xml: `<VDividedBox id="example"> <div id='top'/> <div id='bottom'/> </VDividedBox>` }
该示例由一垂直分隔框组件包裹着两个 div 元素。这里分别设置两个 div 元素的宽高为父级的 80%,同时设置它们的背景色为灰色,这只是为了方便测试。另外,我们还需要考虑一个子框的初始比例分配问题。我们可以设置默认比例为 50:50,比例最好可以在组件实例化时静态指定,同时提供比例设置的动态接口。于是我们就有了下面的改进用例。
Example2: { css: "#example div { width: 80%; height: 80%; background: #AAA; }", xml: `<VDividedBox id="example" percent='30'> <div id='top'/> <div id='bottom'/> </VDividedBox>`, fun: function (sys, items, opts) { sys.top.on("click", e => sys.example.percent = 50); } }
这个用例在垂直分隔框初始化时设置子框的初始比例分配为 30:70,当用户点击第一子框时,比例分配重新恢复为 50:50。不过要注意,这些比例分配指的是对排除分隔条所占用空间后剩余空间的比例分配。
设计与实现
现在让我们把注意力转移到组件的内部。我们先大致地确定组件基本的组成。直观地看,垂直分隔框显示包含三个组件部分:上子框部分、分隔条以及下子框部分。于是我们暂时可以得到下面的视图项部分:
VDividedBox: { xml: `<div id='hbox'> <div id='top'/> <div id='handle'/> <div id='bottom'/> </div>` }
下一步,确保垂直分隔框组件实例的子级部分被正确地映射到上子框 top 以及下子框 bottom。方法是先让所有的子级元素对象全部被添加到上子框 top 中,然后在函数项中将下子级元素添加到下子框 bottom 中。
VDividedBox: { xml: `<div id='hbox'> <div id='top'/> <div id='handle'/> <div id='bottom'/> </div>`, map: {appendTo: "top" }, fun: function (sys, items, opts) { sys.bottom.elem().appendChild(this.last().elem()); } }
现在让我们来考虑下视图项的样式,对于顶层 div 元素,我们设置其定位方式为相对定位。对于子级的三个元素则设置为绝对定位。另外,把分隔条高度设置为 5px。
VDividedBox: { css: `#hbox { position:relative; width:100%; height:100%; box-sizing: border-box; } #top { top: 0; height: 30%; } #bottom { bottom: 0; height: calc(70% - 5px); } #top,#bottom { left: 0; right: 0; position: absolute; } #handle { height: 5px; width: 100%; position:absolute; left:0; top: 30%; z-index:11; cursor:row-resize; }`, xml: `<div id='hbox'> <div id='top'/> <div id='handle'/> <div id='bottom'/> </div>`, map: {appendTo: "top" }, fun: function (sys, items, opts) { sys.bottom.elem().appendChild(this.last().elem()); } }
最后让我们看看如何响应分隔条的拖动事件,从而更改子框的分配比例。我们需要定义一个改变子框比例的函数,同时侦听分隔条的拖拽事件。下面是我们的一个实现。
VDividedBox: { // 视图项同上 map: { format: {"int": "percent"}, appendTo: "top" }, fun: function (sys, items, opts) { var percent = 50; sys.handle.on("dragstart", function (e) { sys.hbox.on("dragover", dragover); }); sys.hbox.on("dragend", function (e) { e.stopPropagation(); sys.hbox.off("dragover", dragover); }); function dragover(e) { e.preventDefault(); setPercent((e.pageY - sys.hbox.offset().top) / sys.hbox.height() * 100); } function setPercent(value) { sys.handle.css("top", value + "%"); sys.top.css("height", value + "%"); sys.bottom.css("height", "calc(" + (100 - value) + "% - 5px)"); } setPercent(opts.percent || percent); sys.bottom.elem().appendChild(this.last().elem()); return Object.defineProperty({}, "percent", {get: () => {return percent}, set: setPercent}); } }
上述代码的映射项中有一项关于 percent 格式的设置,该设置确保了 percent 为整型数。另外函数项中对子框的比例设定用到了 css3 的 calc 计算函数,改函数在浏览器窗体改变大小时仍然能够起作用。如果你希望兼容更多的浏览器,你需要做更多的工作。另外注意,为了让组件有好的性能表现,只有当用户开始拖拽时,才对事件 dragover 实施侦听。
进一步改进
让我们现在做个小测试,写一个包含两个文本域作为子级的垂直分隔框的应用实例。拖动分隔条,看会出现什么结果。
Example3: { css: `#example textarea { width: 80%; height: 80%; }`, xml: `<VDividedBox id="example"> <textarea id='top'/> <textarea id='bottom'/> </VDividedBox>` }
在这个示例中,有时候分隔条会失灵,子框比例不再随着分隔条位置而出现变化。问题出在文本域对拖拽事件进行了劫持,导致我们我组件内部收不到响应的事件。我们需要做些补丁才行。
VDividedBox: { css: "#hbox { position:relative; width:100%; height:100%; box-sizing: border-box; }\ #top { top: 0; height: 30%; } #bottom { bottom: 0; height: calc(70% - 5px); }\ #top,#bottom { left: 0; right: 0; position: absolute; }\ #handle { height: 5px; width: 100%; position:absolute; left:0; top: 30%; z-index:11; cursor:row-resize; }\ #mask { width: 100%; height: 100%; position: absolute; display: none; z-index: 10; }", xml: "<div id='hbox'>\ <div id='top'/>\ <div id='handle' draggable='true'/>\ <div id='bottom'/>\ <div id='mask'/>\ </div>", map: { format: {"int": "percent"}, appendTo: "top" }, fun: function (sys, items, opts) { var percent = 50; sys.handle.on("dragstart", function (e) { sys.mask.show(); sys.hbox.on("dragover", dragover); }); sys.hbox.on("dragend", function (e) { sys.mask.hide(); e.stopPropagation(); sys.hbox.off("dragover", dragover); }); function dragover(e) { e.preventDefault(); setPercent((e.pageY - sys.hbox.offset().top) / sys.hbox.height() * 100); } function setPercent(value) { sys.handle.css("top", value + "%"); sys.top.css("height", value + "%"); sys.bottom.css("height", "calc(" + (100 - value) + "% - 5px)"); } setPercent(opts.percent || percent); sys.bottom.elem().appendChild(this.last().elem()); return Object.defineProperty({}, "percent", {get: () => {return percent}, set: setPercent}); } }
为了解决问题,我们在组件中引用了额外的 div 元素对象 mask,此元素默认是不显示的。当拖动开始时,它才会覆盖住子框以及分隔条,而拖动一结束,它又隐藏掉。这样就避免了文本域对拖拽事件的劫持。
结合水平分隔框使用
我们有了上述垂直分隔框的设计经验,搞个水平分隔框也就不是什么难事了,这里就不列出来了。这里主要是给出一个综合使用水平分隔框和垂直分隔框的示例。当然的设计之初,我们并没有想到要这么使用。
Example4: { css: `#example div { width: 100%; height: 100%; }`, xml: `<HDividedBox id='example'> <VDividedBox percent='30'> <div/><div/> </VDividedBox> <VDividedBox percent='30'> <div/><div/> </VDividedBox> </HDividedBox>` }
这个示例主要用于展示当分隔框嵌套使用时的表现。该示例包含一个水平分隔框,该水平分隔框又包含两个垂直分隔框,这种布局在不少编辑器中是很常见的,我们这里已经简单高效地把它实现了。
本系列文章基于 xmlplus 框架。如果你对 xmlplus 没有多少了解,可以访问 www.xmlplus.cn。这里有详尽的入门文档可供参考。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。