xmlplus组件设计系列之分隔框(DividedBox)(8)

分隔框(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。这里有详尽的入门文档可供参考。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • xmlplus组件设计系列之树(Tree)(9)

    树形组件是一种具有层级结构的组件,广泛应用于各种场景.本章会实现一个简单的树形组件,尽管功能有限,但你可以通过扩展它来实现自己所需要的树形组件. 数据源 树形组件的数据源可以是 JSON 格式的数据对象,也可以是具有 XML 结构的数据或者是其它的具有层级结构的数据.本章将采用具有如下 JSON 格式的数据对象. var data = { name: 'My Tree', children: [ { name: 'hello' }, { name: 'world' }, { name: 'chi

  • xmlplus组件设计系列之路由(ViewStack)(7)

    在浏览器端,对路由的理解一般是根据不同的 URL 完成页面的切换.在服务器端,则是根据不同的 URL 请求回馈相关的页面.在本章,我们广义的组件路由的定义:根据接收到的不同命令,组件对象呈现出不同的子级页面.在这里将介绍与路由相关的一个组件,即视图栈 ViewStack. 视图栈初步 该组件在<文档>部分的最后一个章节<延迟实例化>已经出现过了.这里将对一些细节部分进行解读.下面再次给出该组件的源码. ViewStack: { xml: "<div id='view

  • 值得分享和收藏的xmlplus组件学习教程

    xmlplus 介绍 xmlplus 是一个设计非常独特 JavaScript 框架,用于快速开发前后端项目. 基于组件设计 在 xmlplus 中,组件是基本的构造块.评价组件设计好坏的一个重要标准是封装度.基于 xmlplus 设计的组件具有极高的封装度.下面是一个简单的组件示例: Widget: { css: "#widget{ color: red; }", xml: `<h1 id='widget'>default</h1>`, fun: functi

  • xmlplus组件设计系列之网格(DataGrid)(10)

    这一章我们要实现是一个网格组件,该组件除了最基本的数据展示功能外,还提供排序以及数据过滤功能. 数据源 为了测试我们即将编写好网格组件,我们采用如下格式的数据源.此数据源包含两部分的内容,分别是表头数据集和表体数据集.网格组件实例最终的列数由表头数据集的长度决定. var data = { gridColumns: ['name', 'power'], gridData: [ { name: 'Chuck Norris', power: Infinity }, { name: 'Bruce Le

  • xmlplus组件设计系列之分隔框(DividedBox)(8)

    分隔框(DividedBox)是一种布局类组件,可以分为两类,其中一类叫水平分隔框(HDividedBox),另一类叫垂直分隔框(VDividedBox).水平分隔框会将其子级分为两列,而垂直分隔框则会将其子级分为两行.列与列之间以及行与行之间一般都会有一条可以拖动的用以改变子级组件大小的分隔条.下面仅以垂直分隔框为例来介绍此类组件是如何设计以及实现的. 成品组件用例 按照以往的设计经验,我们可以先写出想像中的成品组件用例,这将有助于我们后续的进一步的设计与实现.垂直分隔框既然是布局类的组件,那

  • xmlplus组件设计系列之文本框(TextBox)(3)

    文本框是页面中最常用的输入组件,它的默认使用方式如下: <input type='text'/> 当然,这里的 `type='text' 可以略去不写.大部分情况下,使用默认的文本框作为输入组件是没什么问题的,但在具体的项目中,难免会有功能扩展的需求.这里仅以如何增加文本框数据的格式化输入输出能力为例说明如何扩展原生的文本框组件.除了本章的内容,你也可以参考官方文档中的 参数映射 一章. 目标组件的功能分析 对于原生的文本框,我们获取到的值是文本类型的,就像下面的示例所展示的: Example

  • xmlplus组件设计系列之选项卡(Tabbar)(5)

    这一章将设计一个选项卡组件,选项卡组件在手持设备上用的比较多,下面是一个示意图: 选项卡组成 在具体实现之前,想像一下目标组件是如何使用的,对于设计会有莫大的帮助.通过观察,可以将选项卡组件分为容器部分和子项部分,正如下面的 XML 结构所展示的. <Tabbar id="tabbar"> <TabItem id="home" label="首页"/> <TabItem id="setting"

  • xmlplus组件设计系列之按钮(2)

    除了图标以外,按钮也许是最简单的组件了,现在来看看如何定义按钮组件. 使用原生按钮组件 在 xmlplus 中,HTML 元素也以组件的方式存在.所以,你可以直接通过使用 button 标签或者 input 标签来使用按钮组件.如下示例所示: Example: { xml: "<div id='example'>\ <button>Default</button>\ <input type='submit'>Primary</input>

  • xmlplus组件设计系列之下拉刷新(PullRefresh)(6)

    "下拉刷新"由著名设计师 Loren Brichter 设计,并应用于 Twitter 第三方应用 Tweetie 中.2010年4月,Twitter 收购 Tweetie 开发商 Atebits 后,该专利归 Twitter 所有.这一章我们就来看看如何实现一个简单的下拉刷新组件. 目标组件分析 和前面在设计组件时的做法一样,我们先想想看最终的成品组件是如何使用的,这需要点想像力.下拉刷新组件看成一个容器组件是合理的,用户可以对容器的内容进行下拉操作.如果用户完成了完整的下拉触发操作

  • xmlplus组件设计系列之列表(4)

    列表组件是极其常用的一类组件,是许多视图组件系统的必须包含的.列表可以做的很简单,只显示简洁的内容.列表也可以做的很复杂,用于展示非常丰富的内容. 组成元素 列表离不开列表项以及包含列表项的容器.下面是最简单的列表组件,它包含一个列表项组件 Item 以及一个列表项容器组件 List. Item: { xml: "<li id='item'/>" }, List: { xml: "<ul id='list'/>" } 此列表组件尽管简单,但所

  • xmlplus组件设计系列之图标(ICON)(1)

    网页上使用的图标分可为三种:文件图标.字体图标和 SVG 图标.对于文件图标,下面仅以 PNG 格式来说明. PNG 图标 对于 PNG 图标的引用,有两种方式.一种是直接由 HTML 元素 img 的 src 属性给出.下面是一个简单的示例. Icon: { css: "#icon { width: 68px; height: 68px; }", xml: "<img id='icon'/>", fun: function (sys, items, o

随机推荐