擴充HT for Web之HTML5表格組件的Renderer和Editor,html5renderer
在HT for Web提供了一下幾種常用的Editor,分別是:
- slider:拉條
- color picker:顏色選取器
- enum:枚舉類型
- boolean:真假編輯器
- string:普通的文字編輯器
除了這幾種常用編輯器之外,使用者還可以通過繼承ht.widget.BaseItemEditor類來實現自訂編輯器。
而渲染器,在HT for Web提供常用的Renderer有:
- enum:枚舉類型
- color:顏色類型
- boolean:真假渲染器
- text:文本渲染器
和編輯器一樣也可以自訂渲染器,但是方式不太一樣,渲染器是通過定義column中drawCell()方法來自訂單元格展現效果。
今天我們就來實現一把自訂HTML5表格組件的Renderer和Editor,為了更直觀地示範編輯效果,我們正好利用HT for Web強大的HTML5拓撲圖組件
首先來瞧瞧效果:
中,左邊表格的第二列,是定義了一個編輯器,用一個圓盤來表示當前文本的旋轉角度,可以通過拖拉來實現角度變換;表格的第三列,是通過drawCell()方法來繪製單元格內容,中間線標識旋轉角度為零,向左表示文本逆時針旋轉指定角度,向右表示文本順時針旋轉指定角度。
HT for Web的拓撲圖網路節點的文字,簡單修改label.rotation屬性即可實現文字旋轉功能,為了更直觀我特意加上label.background使得網路拓撲圖節點文字具有背景效果。
接下來我們就來看看具體的實現,先來瞭解下渲染器的實現:
{ 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, // 通過反三角Function Compute弧度,再將弧度轉換為角度 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()方法。
具體的代碼我就不在闡述了,思路與前面講述的編輯器的思路差不多。
最後附上程式的所有代碼,供大家參考,有什麼問題歡迎留言諮詢。
TableRendererEditor.zip