在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, // 通過反三角函數計算弧度,再將弧度轉換為角度 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()方法。
具體的代碼我就不在闡述了,思路與前面講述的編輯器的思路差不多。
最后附上程序的所有代碼,供大家參考,有什么問題歡迎留言咨詢。