xmlplus 組件設計系列之八 - 分隔框(DividedBox)

分隔框(DividedBox)是一種佈局類組件,能夠分爲兩種,其中一種叫水平分隔框(HDividedBox),另外一種叫垂直分隔框(VDividedBox)。水平分隔框會將其子級分爲兩列,而垂直分隔框則會將其子級分爲兩行。列與列之間以及行與行之間通常都會有一條能夠拖動的用以改變子級組件大小的分隔條。下面僅以垂直分隔框爲例來介紹此類組件是如何設計以及實現的。css

成品組件用例

按照以往的設計經驗,咱們能夠先寫出想像中的成品組件用例,這將有助於咱們後續的進一步的設計與實現。垂直分隔框既然是佈局類的組件,那麼它也必定是一個容器,該容器包含了上述咱們提到的三種子級組件。爲了使用方便,咱們不該該把分隔框也寫進去,分隔框應該由組件內部實現的。通過分析,咱們獲得下面的一個應用示例:html

// 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。不過要注意,這些比例分配指的是對排除分隔條所佔用空間後剩餘空間的比例分配。app

設計與實現

如今讓咱們把注意力轉移到組件的內部。咱們先大體地肯定組件基本的組成。直觀地看,垂直分隔框顯示包含三個組件部分:上子框部分、分隔條以及下子框部分。因而咱們暫時能夠獲得下面的視圖項部分:ide

// 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 組件做爲子級的垂直分隔框的應用實例。拖動分隔條,看會出現什麼結果。this

// 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>`
}
相關文章
相關標籤/搜索