熱區,是指在一張圖片上選取一些區域,每一個區域連接到指定的地址。css
所以熱區組件的功能,就是在圖片上設置多個熱區區域並配置相應的數據。html
熱區須要實現的功能爲:git
若是你在公司裏聽到有人在討論 MVVM,那麼話題必定離不開 RegularJS。npm
因此這裏以 RegularJS 爲例對主題進行介紹(但如果用 Vue 在實現上區別其實也不大):bash
MVVM 講究以數據驅動視圖,然而熱區這類場景須要須要大量的 DOM 操做。框架
還好,AngularJS、VueJS 和 RegularJS 等框架都提供了自定義指令(directive,寫法及用法上大同小異),方便開發者對純 DOM 元素進行底層操做。dom
也避免了在節點上大量的掛載 on-
開頭的屬性:ide
<!-- not so good -->
<div on-keydown={this.handleKeyDown($event)}></div>
<!-- better -->
<div r-keydown></div>複製代碼
由上可知,熱區組件重度依賴指令,則代碼結構最終設定爲:函數
src
├── assets
│ ├── directive
│ │ ├── addItem.js 添加熱區指令
│ │ ├── changeSize.js 改變尺寸指令
│ │ ├── dragItem.js 移動位置指令
│ │ ├── resizeImg.js 圖片resize處理指令
│ │ └── ...
│ ├── constant.js 通用常量
│ ├── filter.js 過濾器
│ ├── operations.js 處理邏輯封裝
│ └── util.js 工具函數
├── components
│ ├── modal 數據設置模態窗組件
│ └── zone 熱區區域組件
├── mcss
│ ├── _reset.mcss 限定做用範圍的樣式 reset
│ └── index.mcss 基本樣式
├── index.js 組件入口
└── view.html 組件模板複製代碼
指令通常須要返回一個函數用於指令銷燬工做。
Question: 爲何要返回銷燬函數,而不是經過監聽 $destroy 事件來完成?
Answer: 由於指令的銷燬並不必定伴隨着組件銷燬,指令的生命週期更短,一些語法元素(
if/list/include
)會致使它在組件銷燬以前被重複建立和銷燬。
因爲熱區組件使用了較多的事件監聽,基於上述考慮,熱區操做指令中都返回了用於 事件解綁 的函數。
新增熱區區域、拖拽熱區位置和調整熱區大小指令,都存在三個操做階段:
import { dom } from 'regularjs';
export default function (content) {
dom.on(content, 'mousedown', handleMouseDown);
function handleMouseDown(e) {
// ...
dom.on(window, 'mousemove', handleChange);
dom.on(window, 'mouseup', handleMouseUp);
function handleChange(e) {
// ...
};
function handleMouseUp() {
// ...
dom.off(window, 'mousemove', handleChange);
dom.off(window, 'mouseup', handleMouseUp);
};
}
return () => {
// 解綁 mousedown 事件
dom.off(content, 'mousedown', handleMouseDown);
};
}複製代碼
這裏能夠作些優化:
在 changeSize 時對可拖拽點的事件監聽上,利用「事件委託」+「自定義屬性」減小事件綁定,並統一處理不一樣方位的拖拽點:
如圖所示,熱區區域周圍的八個小方塊就是「拖拽點」。<ul r-changeSize>
<li class="hz-u-square hz-u-square-tl" data-pointer="dealTL"></li>
<li class="hz-u-square hz-u-square-tc" data-pointer="dealTC"></li>
<li class="hz-u-square hz-u-square-tr" data-pointer="dealTR"></li>
<li class="hz-u-square hz-u-square-cl" data-pointer="dealCL"></li>
<li class="hz-u-square hz-u-square-cr" data-pointer="dealCR"></li>
<li class="hz-u-square hz-u-square-bl" data-pointer="dealBL"></li>
<li class="hz-u-square hz-u-square-bc" data-pointer="dealBC"></li>
<li class="hz-u-square hz-u-square-br" data-pointer="dealBR"></li>
</ul>複製代碼
function handleMouseDown(e) {
// 獲取選中節點的自定義屬性值
let pointer = e.target.dataset.pointer;
if(!pointer) {
return;
}
e && e.stopPropagation();
dom.on(window, 'mousemove', handleChange);
dom.on(window, 'mouseup', handleMouseUp);
function handleChange(e) {
e && e.preventDefault();
// 處理選中不一樣拖拽點時的狀況
let styleInfo = operations[pointer](itemInfo, moveX, moveY);
// 邊界值處理
itemInfo = operations.dealEdgeValue(itemInfo, styleInfo, container);
// ...
}
function handleMouseUp() {
// ...
dom.off(window, 'mousemove', handleChange);
dom.off(window, 'mouseup', handleMouseUp);
}
};複製代碼
統一封裝 operations 處理邏輯:
export default {
/** * 改變熱區大小時的邊界狀況處理 * @param {Object} itemInfo 實際使用的熱區模塊數據 * @param {Object} styleInfo 操做中的熱區模塊數據 * @param {Object} container 圖片區域的寬高數據 */
dealEdgeValue(itemInfo, styleInfo, container) {},
/** * 處理不一樣的拖拽點,大寫字母表示含義:T-top,L-left,C-center,R-right,B-bottom * @param {Object} itemInfo * @param {Number} moveX * @param {Number} moveY * @return {Object} 對過程數據進行處理 */
dealTL(itemInfo, moveX, moveY, minLimit = MIN_LIMIT) {},
dealTC(itemInfo, moveX, moveY, minLimit = MIN_LIMIT) {},
dealTR(itemInfo, moveX, moveY, minLimit = MIN_LIMIT) {},
dealCL(itemInfo, moveX, moveY, minLimit = MIN_LIMIT) {},
dealCR(itemInfo, moveX, moveY, minLimit = MIN_LIMIT) {},
dealBL(itemInfo, moveX, moveY, minLimit = MIN_LIMIT) {},
dealBC(itemInfo, moveX, moveY, minLimit = MIN_LIMIT) {},
dealBR(itemInfo, moveX, moveY, minLimit = MIN_LIMIT) {}
};複製代碼
在拖拽移動熱區位置時,使用 translate 替代直接改變 top && left 值避免重繪,實現更流暢的拖拽效果。
// bad
dom.css(elem, {
top: `${moveY}px`,
left: `${moveX}px`
});
// better
dom.css(elem, {
transform: `translate(${moveX}px, ${moveY}px)`
});複製代碼
熱區尺寸單位用 % 取代 px,不一樣屏幕尺寸的用戶都得到更好的熱區操做體驗。
但熱區區域存在最小尺寸限制,須要利用 element-resize-detector 對圖片進行監聽,在圖片尺寸變化時對邊界區域作兼容。
import elementResizeDetectorMaker from 'element-resize-detector';
const erd = elementResizeDetectorMaker();
export default function resizeImg(elem) {
// ...
const resize = _.debounce(() => {
// ...
}, 500);
erd.listenTo(elem, resize);
return () => {
erd.removeListener(elem, resize);
};
};複製代碼
經過計算屬性動態修改設置數據的 hover 位置,確保不超過圖片範圍,以確保信息的正確顯示。
<ul r-style={{top: infoTop, bottom: infoBottom, left: infoLeft, right: infoRight, transform: infoTransform}}></ul>複製代碼
使用透明小方塊加強鼠標從熱區區域移動到設置信息區域的體驗:
(這裏用灰色標識一下小方塊)
雙擊熱區區域彈出信息設置模態框時,利用 r-autofocus 指令自動聚焦:
同時綁定鍵盤監聽事件,監聽 Enter 鍵和 Esc 鍵,加強「確認」和「取消」體驗。