分隔框(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。這里有詳盡的入門文檔可供參考。
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持億速云。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。