分隔框(DividedBox)是一种布局类组件,可以分为两种,其中一种叫水平分隔框(HDividedBox),另一种叫垂直分隔框(VDividedBox)。水平分隔框会将其子级分为两列,而垂直分隔框则会将其子级分为两行。列与列之间以及行与行之间一般都会有一条可以拖动的用以改变子级组件大小的分隔条。下面仅以垂直分隔框为例来介绍此类组件是如何设计以及实现的。

成品组件用例

按照以往的设计经验,我们可以先写出想像中的成品组件用例,这将有助于我们后续的进一步的设计与实现。垂直分隔框既然是布局类的组件,那么它也一定是一个容器,该容器包含了上述我们提到的三种子级组件。为了使用方便,我们不应该把分隔框也写进去,分隔框应该由组件内部实现的。经过分析,我们得到下面的一个应用示例:

// 08-01
Index: {
css: "#index { width: 640px; height: 480px; box-sizing: border-box; border: 1px solid blue; }\
#top, #bottom { width: 100%; height: 100%; background: #AAA; }",
xml: "<VDividedBox id='index'>\
<div id='top'/>\
<div id='bottom'/>\
</VDividedBox>"
}

该示例由一垂直分隔框组件包裹着两个 div 元素。这里分别设置两个 div 元素的宽高为父级的 100%,同时设置它们的背景色为灰色,这只是为了方便测试。另外,我们还需要考虑一个子框的初始比例分配问题。我们可以设置默认比例为 50:50,比例最好可以在组件实例化时静态指定,同时提供比例设置的动态接口。于是我们就有了下面的改进用例。

// 08-01
Index: {
css: "#index { width: 640px; height: 480px; box-sizing: border-box; border: 1px solid blue; }\
#top, #bottom { width: 100%; height: 100%; background: #AAA; }",
xml: "<VDividedBox id='index' percent='30'>\
<div id='top'/>\
<div id='bottom'/>\
</VDividedBox>",
fun: function (sys, items, opts) {
sys.top.on("click", e => sys.index.percent = 50);
}
}

这个用例在垂直分隔框初始化时设置子框的初始比例分配为 30:70,当用户点击第一子框时,比例分配重新恢复为 50:50。不过要注意,这些比例分配指的是对排除分隔条所占用空间后剩余空间的比例分配。

设计与实现

现在让我们把注意力转移到组件的内部。我们先大致地确定组件基本的组成。直观地看,垂直分隔框显示包含三个组件部分:上子框部分、分隔条以及下子框部分。于是我们暂时可以得到下面的视图项部分:

// 08-01
<div id='hbox'>
<div id='top'/>
<div id='handle'/>
<div id='bottom'/>
</div>`

下一步,确保垂直分隔框组件实例的子级部分被正确地映射到上子框 top 以及下子框 bottom。方法是先让所有的子级元素对象全部被添加到上子框 top 中,然后在函数项中将下子级元素添加到下子框 bottom 中。

// 08-01
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

// 08-01
VDividedBox: {
css: `#vbox { 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='vbox'>
<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());
}
}

最后让我们看看如何响应分隔条的拖动事件,从而更改子框的分配比例。我们需要定义一个改变子框比例的函数,同时侦听分隔条的拖拽事件。下面是我们的一个实现。

// 08-01
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 实施侦听。

进一步改进

上述组件在大部分情况下运作良好,但当我将 CodeMirror 组件整合进去时,出了点小问题。让我们现在做个小测试,写一个包含两个 CodeMirror 组件作为子级的垂直分隔框的应用实例。拖动分隔条,看会出现什么结果。

// 08-02
Index: {
css: "#index { width: 640px; height: 480px; box-sizing: border-box; border: 1px solid #AAA; }",
xml: "<VDividedBox id='index'>\
<Editor id='top'/>\
<Editor id='bottom'/>\
</VDividedBox>"
},
Editor: {
css: `.CodeMirror { height:100%; height: 100%; font-size: 14px; }
.CodeMirror-gutters { border-right: 1px solid %border-color; background: linear-gradient...}
#editor { position: relative; width: 100%; height: 100%; box-sizing: border-box; border: 1px solid #AAA; }`,
map: { nofragment: true },
opt: { lineNumbers: true, indentUnit: 4, mode: "text/html" },
xml: "<div id='editor'/>",
fun: function (sys, items, opts) {
return CodeMirror(sys.editor.elem(), opts);
}
}

如果你运行此示例,会发现分隔条失灵了,拖动分隔条子框比例不再出现变化。问题出在 CodeMirror 组件对象对拖拽事件进行了劫持,导致我们我组件内部收不到响应的事件。我们需要做些补丁才行,下面是改进后的组件:

// 08-03
VDividedBox: {
css: `#vbox { 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='vbox'>\
<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.vbox.on("dragover", dragover);
});
sys.vbox.on("dragend", function (e) {
sys.mask.hide();
e.stopPropagation();
sys.vbox.off("dragover", dragover);
});
function dragover(e) {
e.preventDefault();
setPercent((e.pageY - sys.vbox.offset().top) / sys.vbox.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,此元素默认是不显示的。当拖动开始时,它才显示并覆盖住子框以及分隔条,而拖动一结束,它又隐藏掉。这样就避免了 CodeMirror 组件对象对拖拽事件的劫持。

结合水平分隔框使用

我们有了上述垂直分隔框的设计经验,搞个水平分隔框也就不是什么难事了,这里就不列出来了。这里主要是给出一个综合使用水平分隔框和垂直分隔框的示例。当然,在设计之初,我们并没有想到要这么使用。

// 08-04
Index: {
css: `#index { width: 640px; height: 480px; box-sizing: border-box; border: 1px solid blue; }
#left0, #right0, #left1, #right1 { width: 100%; height: 100%; background: #AAA; }`,
xml: `<HDividedBox id='index'>
<VDividedBox percent='30'>
<div id='left0'/><div id='right0'/>
</VDividedBox>
<VDividedBox percent='30'>
<div id='left1'/><div id='right1'/>
</VDividedBox>
</HDividedBox>`
}

xmlplus 组件设计系列之八 - 分隔框(DividedBox)的更多相关文章

  1. xmlplus 组件设计系列之三 - 文本框

    文本框是页面中最常用的输入组件,它的默认使用方式如下: <input type='text'/> 当然,这里的 `type='text' 可以略去不写.大部分情况下,使用默认的文本框作为输 ...

  2. xmlplus 组件设计系列之零 - xmlplus 简介

    xmlplus 是什么 xmlplus 是博主写的一个 JavaScript 框架,用于快速开发前后端项目. xmlplus 基于组件设计,组件是基本的构造块.评价组件设计好坏的一个重要标准是封装度. ...

  3. xmlplus 组件设计系列之二 - 按钮

    除了图标以外,按钮也许是最简单的组件了,现在来看看如何定义按钮组件. 使用原生按钮组件 在 xmlplus 中,HTML 元素也以组件的方式存在.所以,你可以直接通过使用 button 标签或者 in ...

  4. xmlplus 组件设计系列之一 - 图标

    网页上使用的图标分可为三种:文件图标.字体图标和 SVG 图标.对于文件图标,下面仅以 PNG 格式来说明. PNG 图标 对于 PNG 图标的引用,有两种方式.一种是直接由 HTML 元素 img ...

  5. xmlplus 组件设计系列之五 - 选项卡

    这一章将设计一个选项卡组件,选项卡组件在手持设备上用的比较多,下面是一个示意图: 选项卡组件的分解 在具体实现之前,想像一下目标组件是如何使用的,对于设计会有莫大的帮助.通过观察,可以将选项卡组件分为 ...

  6. xmlplus 组件设计系列之六 - 下拉刷新

    "下拉刷新"由著名设计师 Loren Brichter 设计,并应用于 Twitter 第三方应用 Tweetie 中.2010年4月,Twitter 收购 Tweetie 开发商 ...

  7. xmlplus 组件设计系列之九 - 树(Tree)

    树形组件是一种具有层级结构的组件,广泛应用于各种场景.本章会实现一个简单的树形组件,尽管功能有限,但你可以通过扩展它来实现自己所需要的树形组件. 数据源 树形组件的数据源可以是 JSON 格式的数据对 ...

  8. xmlplus 组件设计系列之十 - 网格(DataGrid)

    这一章我们要实现是一个网格组件,该组件除了最基本的数据展示功能外,还提供排序以及数据过滤功能. 数据源 为了测试我们即将编写好网格组件,我们采用如下格式的数据源.此数据源包含两部分的内容,分别是表头数 ...

  9. xmlplus 组件设计系列之四 - 列表

    列表组件是极其常用的一类组件,是许多视图组件系统的必须包含的.列表可以做的很简单,只显示简洁的内容.列表也可以做的很复杂,用于展示非常丰富的内容. 组成元素 列表离不开列表项以及包含列表项的容器.下面 ...

随机推荐

  1. IE浏览器下使用AJAX登陆接口请求缓存与登陆不了的问题解决

    问题: 在IE浏览器下面,登陆的时候老是登陆不上,但是打开控制台的时候再登陆却能登陆上. 分析: 通过抓包,发现,在不打开控制台的时候,少了一个接口的请求,却返回了改接口的返回信息,但是返回信息并不是 ...

  2. 安装rabbitmq以及集群配置

    前言: (一些有用没用的唠叨,反正看了也不少肉,跳过也没啥) 情况是这样的:虚拟机.CentOS 6.5.免编译包安装rabbitmq集群,可不用连外网. 我原计划是安装在虚拟机上wyt1/wyt2/ ...

  3. Azure Messaging-ServiceBus Messaging消息队列技术系列5-重复消息:at-least-once at-most-once

    上篇博客中,我们用实际的业务场景和代码示例了Azure Messaging-ServiceBus Messaging对复杂对象消息的支持和消息的持久化: Azure Messaging-Service ...

  4. Android四大组件详解

    注:本文主要来自网易的一个博主的文章,经过阅读,总结,故留下文章在此 Android四大基本组件介绍与生命周期 Android四大基本组件分别是Activity,Service服务,Content P ...

  5. GDAL C# 开发环境配置

    http://blog.csdn.net/mygisforum/article/details/22478491

  6. JS中的普通函数和箭头函数

    最近被问到了一个问题: >javaScript 中的箭头函数 (=>) 和普通函数 (function) 有什么区别? 我当时想的就是:这个问题很简单啊~(flag),然后做出了错误的回答 ...

  7. 【翻译】CSS水平和垂直居中的12种方法

    英语原文链接 在CSS中有许多不同的方法能够做到水平和垂直居中,但很难去选择合适的那个.我会向你展示我所看到的所有的方法,帮助你在所面对的情境下选择最棒的那一个. 方法1 此方法将只能垂直居中单行文本 ...

  8. 关于数组和集合的冒泡排序中容易出现的IndexOutOfBoundsException

    数组只能存错一种相同的数据类型,集合只能存储引用数据类型(用泛型),集合的底层就是一个可变的数组. 数组的冒泡排序: public static void arrayMaxPaiXu(int[] ar ...

  9. Spring MVC 处理异常的3种方式

    使用Spring MVC开发的博客网站时,遇到了如何处理业务层抛出的异常的问题,查阅到了spring官方博客-spring MVC中异常的处理,以下将会以登录模块为示例. 愚蠢的处理方式 处理异常遵循 ...

  10. 如何记录selenium自动化测试过程中接口的调用信息

    上一篇博客,我写了python自动化框架的一些知识和粗浅的看法,在上一篇中我也给自己提出一个需求:如果记录在测试过程中接口的调用情况?提出这个需求,我觉得是有意义的.你在测试过程中肯定会遇到一些莫名其 ...