擴展HT for Web之HTML5表格組件的Renderer和Editor

HT for Web提供了一下幾種經常使用的Editor,分別是:canvas

  • slider:拉條
  • color picker:顏色選擇器
  • enum:枚舉類型
  • boolean:真假編輯器
  • string:普通的文本編輯器

除了這幾種經常使用編輯器以外,用戶還能夠經過繼承ht.widget.BaseItemEditor類來實現自定義編輯器。網絡

而渲染器,在HT for Web提供經常使用的Renderer有:編輯器

  • enum:枚舉類型
  • color:顏色類型
  • boolean:真假渲染器
  • text:文本渲染器

和編輯器同樣也能夠自定義渲染器,可是方式不太同樣,渲染器是經過定義column中drawCell()方法來自定義單元格展示效果。ide

今天咱們就來實現一把自定義HTML5表格組件的Renderer和Editor,爲了更直觀地演示編輯效果,咱們正好利用HT for Web強大的HTML5拓撲圖組件函數

首先來瞧瞧效果:佈局

效果圖中,左邊表格的第二列,是定義了一個編輯器,用一個圓盤來表示當前文本的旋轉角度,能夠經過拖拉來實現角度變換;表格的第三列,是經過drawCell()方法來繪製單元格內容,中間線標識旋轉角度爲零,向左表示文本逆時針旋轉指定角度,向右表示文本順時針旋轉指定角度。this

HT for Web的拓撲圖網絡節點的文字,簡單修改label.rotation屬性便可實現文字旋轉功能,爲了更直觀我特地加上label.background使得網絡拓撲圖節點文字具備背景效果。spa

接下來咱們就來看看具體的實現,先來了解下渲染器的實現:設計

{
    name : 'label.rotation',
    accessType : 'style',
    drawCell : function(g, data, selected, column, x, y, w, h, tableView) {
        var degree = Math.round(data.s('label.rotation') / Math.PI * 180),
                width = Math.abs(w / 360 * degree),
                begin = w / 2,
                rectColor = '#29BB9C',
                fontColor = '#000',
                background = '#F8F0E5';

        if (selected) {
            rectColor = '#F7F283';
            background = '#29BB9C';
        }
        g.beginPath();
        g.fillStyle = background;
        g.fillRect(x, y, w, h);
        g.beginPath();
        if (degree < 0) begin -= width;
        g.fillStyle = rectColor;
        g.fillRect(x + begin, y, width, h);
        g.beginPath();
        g.font = '12px arial, sans-serif';
        g.fillStyle = fontColor;
        g.textAlign = 'center';
        g.textBaseline = 'middle';
        g.fillText(degree, x + w / 2, y + h / 2);
    }
}

 

上面的代碼就是定義表格第三列的代碼,能夠看到除了定義column自身屬性外,還添加了drawCell()方法,經過drawCell()方法傳遞進來的參數,來繪製本身想要的效果。指針

渲染就是這麼簡單,那麼編輯器就沒那麼容易了,在設計自定義編輯器以前,得先來了解下編輯器的基類ht.widget.BaseItemEditor,其代碼以下:

ht.widget.BaseItemEditor = function (data, column, master, editInfo) {    
    this._data = data;
    this._column = column;
    this._master = master;
    this._editInfo = editInfo;
};
ht.Default.def(‘ht.widget.BaseItemEditor’, Object, {
    ms_ac:["data", "column", "master", "editInfo"],
    editBeginning: function() {},
    getView: function() {},
    getValue: function() {},
    setValue: function() {}
});

 

它處理構造函數中初始化類變量外,就定義了幾個接口,讓用戶重載實現相關業務操做邏輯處理。那麼接下來講說這些接口的具體用意:

  • editBeginning:在單元格開始編輯前調用
  • getView:獲取編輯器view,值類型爲DOM元素
  • getValue:獲取編輯器值
  • setValue:設置編輯器值,並作編輯器的頁面初始化操做

在建立一個自定義編輯器的時候,必須實現這些接口,並在不一樣的接口中,作不一樣的操做。

如今咱們來看看旋轉角度的自定義編輯是如何設計的:

1. 按照HT for Web組件的設計慣例,咱們須要建立一個Div做爲view,在view中包含一個canvas元素,組件內容在canvas上繪製;

2. editor須要與用戶有交互,所以,須要在view上添加事件監聽,監聽用戶有可能的操做,在此次的Demo中,咱們但願用戶經過拖拉角度控制盤來控制角度,因此,咱們在view上添加了mousedown、mousemove及mouseup三個事件監聽;

3. 用戶經過拖拉組件能夠改變角度,這個改變是連續的,並且在拖拉的時候有可能鼠標會離開組件區域,要實現離開組件區域也可以正確的改變值,那麼這時候就須要調用HT for Web的startDragging()方法;

以上講述的操做都在構造函數中處理,接下來看看構造函數長什麼樣:

// 類ht.widget.RotationEditor構造函數
ht.widget.RotationEditor = function(data, column, master, editInfo) {
    // 調用父類構造函數初始化參數
    this.getSuperClass().call(this, data, column, master, editInfo);

    var self = this,
        view = self._view = createDiv(1),
        canvas = self._canvas = createCanvas(self._view);
    view.style.boxShadow = '2px 2px 10px #000';

    // 在view上添加mousemove監聽
    view.addEventListener('mousemove', function(e) {
        if (self._state) {
            ht.Default.startDragging(self, e);
        }
    });

    // 在view上添加mousedown監聽
    view.addEventListener('mousedown', function(e) {
        self._state = 1;
        self.handleWindowMouseMove(e);
    });

    // 在view上添加mouseup監聽,作些清理操做
    view.addEventListener('mouseup', function(e) {
        self.clear();
    });
};

 

4. 接下來就是經過def()方法來定義ht.widget.RotationEditor類繼承於ht.widget.BaseItemEditor,並實現父類的方法,代碼以下,在代碼中,我沒有貼出setValue()方法的實現,由於這塊有些複雜,咱們單獨抽出來說解;

ht.Default.def('ht.widget.RotationEditor', ht.widget.BaseItemEditor, {
    editBeginning : function() {
        var self = this,
            editInfo = self.getEditInfo(),
            rect = editInfo.rect;

        // 編輯前再對組件作一次佈局,避免組件寬高計算不到位
        layout(self, rect.x, rect.y, rect.width, rect.width);
    },
    getView : function() {
        return this._view;
    },
    getValue : function() {
        return this._value;
    },
    setValue : function(val) {
       // 設置編輯器值,並作編輯器的頁面初始化操做
    }
});

 

5. 咱們要在setValue()方法中繪製出文章開頭的效果圖上面展示的效果,大體分解了些,能夠分紅如下四步來繪製,固然在繪製以前須要線得到canvas的context對象:

    5.1. 繪製內外圓盤,經過arc()方法繪製兩個間隔10px的同心圓;

    5.2. 繪製值區域,經過結合arc()方法及lineTo()方法繪製一個扇形區域,在經過fill方法填充顏色;

    5.3. 繪製指針,經過lineTo()方法繪製兩個指針;

    5.4. 繪製文本,在繪製文本的時候,不能直接將文本繪製在圓心處,由於圓心處是指針的交匯處,若是直接繪製文本的話,將與指針重疊,這時,經過clearRect()方法來清除文本區域,在經過fillRect()方法將背景填充上去,否則文本區域塊將是透明的,接下來就調用fillText()方法繪製文本。

這些就是組件繪製的全部邏輯,可是有一點必須注意,在繪製完組件後,必須調用下restore()方法,由於在initContext()方法中作了一次save()操做,接下來看看具體實現(代碼有些長);

setValue : function(val) {
    var self = this;
    if (self._value === val) return;

    // 設置組件值
    self._value = val;

    var editInfo = self.getEditInfo(),
        rect = editInfo.rect,
        canvas = self._canvas,
        radius = self._radius = rect.width / 2,
        det = 10,
        border = 2,
        x = radius,
        y = radius;

    // 弧度到角度的轉換
    val = Math.round(val / Math.PI * 180);
    // 設置canvas大小
    setCanvas(canvas, rect.width, rect.width);
    // 獲取畫筆
    var g = initContext(canvas);
    translateAndScale(g, 0, 0, 1);

    // 繪製背景
    g.fillStyle = '#FFF';
    g.fillRect(0, 0, radius * 2, radius * 2);

    // 設置線條顏色及線條寬度
    g.strokeStyle = '#969698';
    g.lineWidth = border;

    // 繪製外圈
    g.beginPath();
    g.arc(x, y, radius - border, 0, Math.PI * 2, true);
    g.stroke();

    // 繪製內圈
    g.beginPath();
    g.arc(x, y, radius - det - border, 0, Math.PI * 2, true);
    g.stroke();

    // 繪製值區域
    var start = -Math.PI / 2,
        end = Math.PI * val / 180 - Math.PI / 2;
    g.beginPath();
    g.fillStyle = 'rgba(255, 0, 0, 0.7)';
    g.arc(x, y, radius - border, end, start, !(val < 0));
    g.lineTo(x, border + det);
    g.arc(x, y, radius - det - border, start, end, val < 0);
    g.closePath();
    // 填充值區域
    g.fill();
    // 繪製值區域末端到圓心的線條
    g.lineTo(x, y);
    g.lineTo(x, det + border);
    g.stroke();

    // 繪製文本
    var font = '12px arial, sans-serif';
    // 計算文本大小
    var textSize = ht.Default.getTextSize(font, '-180');
    // 文本區域
    var textRect = {
        x : x - textSize.width / 2,
        y : y - textSize.height / 2,
        width : textSize.width,
        height : textSize.height
    };
    g.beginPath();
    // 清空文本區域
    g.clearRect(textRect.x, textRect.y, textRect.width, textRect.height);
    g.fillStyle = '#FFF';
    // 補上背景
    g.fillRect(textRect.x, textRect.y, textRect.width, textRect.height);
    // 設置文本樣式
    g.textAlign = 'center';
    g.textBaseline = 'middle';
    g.font = font;
    g.fillStyle = 'black';
    // 繪製文本
    g.fillText(val, x, y);

    // restore()和save()是配對的,在initContext()方法中已經作了save()操做
    g.restore();
}

 

6. 這時候編輯器的設計就大致完成,那麼編輯器該如何用到表格上呢?很簡單,在表格定義列的時候,加上下面兩行代碼就能夠開始使用編輯器了;

editable : true, // 啓動編輯
itemEditor : ‘ht.widget.RotationEditor' // 指點編輯器類

 

7. 在構造函數中,view的mousemove事件調用了startDragging()方法,其實這個方法是有依賴的,它須要組件重載handleWindowMouseMove()及handleWindowMouseUp()兩個方法。緣由很簡單,就如第3點種提到的,用戶在拖拉組件的時候,有可能拖離了組件區域,這時候只能經過window上的mousemove及mouseup兩個事件監聽令用戶繼續操做;

// 監聽window的mousemove事件,在view的mousemove事件中,調用了startDragging()方法,
// 而startDragging()方法中的實質就是觸發window的mousemove事件
// 該方法計算值的變化,並經過setValue()方法來改變值
handleWindowMouseMove : function(e) {
    var rect = this._view.getBoundingClientRect(),
        x = e.x - rect.left,
        y = e.y - rect.top,
        radius = this._radius,
        // 經過反三角函數計算弧度,再將弧度轉換爲角度
        value = Math.round(Math.atan2(y - radius, x - radius) / Math.PI * 180);

    if (value > 90) {
        value = -(180 - value + 90);
    }
    else {
        value = value + 90;
    }
    this.setValue(value / 180 * Math.PI);
},
handleWindowMouseUp : function(e) {
    this.clear();
},
clear : function() {
    // 清楚狀態組件狀態
    delete this._state;
}

加上上面的三個方法,運行代碼能夠發現編輯器能夠正常編輯了。可是隻有在結束編輯後,才能夠在拓撲圖上看到文本旋轉角度變化,若是能夠實時更新拓撲圖上的文本旋轉角度,將會更加直觀些,那麼如今該怎麼辦呢?

8. 自定義編輯器這塊並像其餘已經實現了的編輯器那樣能夠指定編輯器的屬性,自定義編輯器可以指定的就只有一個類名,因此在編輯器上設置參數是沒用的,用戶沒法設置到編輯器中。一個偷巧的方法是在column上作手腳,借鑑其餘編輯器的設計思想,在column上添加一個名字爲_instant的屬性,在代碼中經過該屬性值來判斷是否要當即更新對應的屬性值,所以只須要在setValue()方法中添加以下代碼,就可以實現實時更新屬性值的效果;

// 判斷列對象是否設置了_instant屬性
if (column._instant) {
    var table = self.getMaster();
    table.setValue(self.getData(), column, val);
}

9. 至此,編輯器的設計已經完成,如今來看看具體的用法,下面的代碼是Table中具體的列定義,在列定義中,指定itemEditor屬性值,並設置_instant屬性爲true,就能夠實現編輯器實時更新的效果

{
    accessType : 'style',
    name : 'label.rotation',
    editable : true,
    itemEditor : 'ht.widget.RotationEditor',
    _instant : true,
    formatValue : function(value) {
        return Math.round(value / Math.PI * 180);
    }
}

 

代碼中你會發現定義了一個formatValue()方法,該方法是爲了與編輯器中編輯的值類型一致,都將弧度轉換爲角度。

在表格的第三列中,經過渲染器自定義了單元格樣式,同時我也爲其定義了另一個編輯器,經過左右拖拉單元格來實現角度的變化,這個編輯器的實現與上面談及的編輯器略有不一樣,具體的不一樣之處在於,第三列的編輯器經過HT for Web中定義的ms_listener模塊來添加監聽,讓構造函數與交互分離開,看起來更加清晰明瞭。

介紹下ms_listener模塊,若是類添加了ms_listener模塊,那麼在類中將會多如下兩個方法:

  • addListeners:將類中定義的handle_XXX()方法(XXX表明某個DOM事件名稱,如:mousemove等)做爲相應的事件監聽函數添加到組件的view上;
  • removeListeners:將類中定義的handle_XXX()方法對應的事件從view上移除。

那麼類中如何添加ms_listener模塊呢,只須要在def()方法中類的方法定義上,添加ms_listener:true這行代碼,並在方法定義上添加DOM事件對應的handle函數,再在構造函數中調用類的addListeners()方法。

具體的代碼我就不在闡述了,思路與前面講述的編輯器的思路差很少。

最後附上程序的全部代碼,供你們參考,有什麼問題歡迎留言諮詢。

TabelRendererEditor.zip 

相關文章
相關標籤/搜索