define(function(require, exports, module) { var key = require('./key'); var KeyControl = require('./keycontrol'); /**** Dom Utils ****/ function createElement(name) { return document.createElement(name); } function setElementAttribute(element, name, value) { element.setAttribute(name, value); } function getElementAttribute(element, name) { return element.getAttribute(name); } function addElementClass(element, name) { element.classList.add(name); } function removeElementClass(element, name) { element.classList.remove(name); } function appendChild(parent, child) { parent.appendChild(child); } /*******************/ var IDLE = HotBox.STATE_IDLE = 'idle'; var div = 'div'; /** * Simple Formatter */ function format(template, args) { if (typeof(args) != 'object') { args = [].slice.apply(arguments, 1); } return String(template).replace(/\{(\w+)\}/g, function(match, name) { return args[name] || match; }); } /** * Hot Box Class */ function HotBox($container) { if (typeof($container) == 'string') { $container = document.querySelector($container); } if (!$container || !($container instanceof HTMLElement)) { throw new Error('No container or not invalid container for hot box'); } // 创建 HotBox Dom 解构 var $hotBox = createElement(div); addElementClass($hotBox, 'hotbox'); appendChild($container, $hotBox); // 保存 Dom 解构和父容器 this.$element = $hotBox; this.$container = $container; // 标示是否是输入法状态 this.isIME = false; /** * @Desc: 增加一个browser用于判断浏览器类型,方便解决兼容性问题 * @Editor: Naixor * @Date: 2015.09.14 */ this.browser = { sg: /se[\s\S]+metasr/.test(navigator.userAgent.toLowerCase()) }; /* * added by zhangbobell * 2015.09.22 * 增加父状态机,以解决在父 FSM 下状态控制的问题,最好的解决办法是增加一个函数队列 * 将其中的函数一起执行。//TODO * */ this._parentFSM = {}; // 记录位置 this.position = {}; // 已定义的状态(string => HotBoxState) var _states = {}; // 主状态(HotBoxState) var _mainState = null; // 当前状态(HotBoxState) var _currentState = IDLE; // 当前状态堆栈 var _stateStack = []; // 实例引用 var _this = this; var _controler; /** * Controller: { * constructor(hotbox: HotBox), * active: () => void * } */ function _control(Controller) { if (_controler) { _controler.active(); return; } Controller = Controller || KeyControl; _controler = new Controller(_this); _controler.active(); $hotBox.onmousedown = function(e) { e.stopPropagation(); e.preventDefault(); }; return _this; } function _dispatchKey(e) { var type = e.type.toLowerCase(); e.keyHash = key.hash(e); e.isKey = function(keyExpression) { if (!keyExpression) return false; var expressions = keyExpression.split(/\s*\|\s*/); while(expressions.length) { if (e.keyHash == key.hash(expressions.shift())) return true; } return false; }; e[type] = true; // Boot: keyup and activeKey pressed on IDLE, active main state. if (e.keyup && _this.activeKey && e.isKey(_this.activeKey) && _currentState == IDLE && _mainState) { _activeState('main', { x: $container.clientWidth / 2, y: $container.clientHeight / 2 }); return; } var handleState = _currentState == IDLE ? _mainState : _currentState; if (handleState) { var handleResult = handleState.handleKeyEvent(e); if (typeof(_this.onkeyevent) == 'function') { e.handleResult = handleResult; _this.onkeyevent(e, handleResult); } return handleResult; } return null; } function _addState(name) { if (!name) return _currentState; if (name == IDLE) { throw new Error('Can not define or use the `idle` state.'); } _states[name] = _states[name] || new HotBoxState(this, name); if (name == 'main') { _mainState = _states[name]; } return _states[name]; } function _activeState(name, position) { _this.position = position; // 回到 IDLE if (name == IDLE) { if (_currentState != IDLE) { _stateStack.shift().deactive(); _stateStack = []; } _currentState = IDLE; } // 回退一个状态 else if (name == 'back') { if (_currentState != IDLE) { _currentState.deactive(); _stateStack.shift(); _currentState = _stateStack[0]; if (_currentState) { _currentState.active(); } else { _currentState = 'idle'; } } } // 切换到具体状态 else { if (_currentState != IDLE) { _currentState.deactive(); } var newState = _states[name]; _stateStack.unshift(newState); if (typeof(_this.position) == 'function') { position = _this.position(position); } newState.active(position); _currentState = newState; } } function setParentFSM(fsm) { _this._parentFSM = fsm; } function getParentFSM() { return _this._parentFSM; } this.control = _control; this.state = _addState; this.active = _activeState; this.dispatch = _dispatchKey; this.setParentFSM = setParentFSM; this.getParentFSM = getParentFSM; this.activeKey = 'space'; this.actionKey = 'space'; } /** * 表示热盒某个状态,包含这些状态需要的 Dom 对象 */ function HotBoxState(hotBox, stateName) { var BUTTON_SELECTED_CLASS = 'selected'; var BUTTON_PRESSED_CLASS = 'pressed'; var STATE_ACTIVE_CLASS = 'active'; // 状态容器 var $state = createElement(div); // 四种可见的按钮容器 var $center = createElement(div); var $ring = createElement(div); var $ringShape = createElement('div'); var $top = createElement(div); var $bottom = createElement(div); // 添加 CSS 类 addElementClass($state, 'state'); addElementClass($state, stateName); addElementClass($center, 'center'); addElementClass($ring, 'ring'); addElementClass($ringShape, 'ring-shape'); addElementClass($top, 'top'); addElementClass($bottom, 'bottom'); // 摆放容器 appendChild(hotBox.$element, $state); appendChild($state, $ringShape); appendChild($state, $center); appendChild($state, $ring); appendChild($state, $top); appendChild($state, $bottom); // 记住状态名称 this.name = stateName; // 五种按钮:中心,圆环,上栏,下栏,幕后 var buttons = { center: null, ring: [], top: [], bottom: [], behind: [] }; var allButtons = []; var selectedButton = null; var pressedButton = null; var stateActived = false; // 布局,添加按钮后,标记需要布局 var needLayout = true; function layout() { var radius = buttons.ring.length * 15; layoutRing(radius); layoutTop(radius); layoutBottom(radius); indexPosition(); needLayout = false; function layoutRing(radius) { var ring = buttons.ring; var step = 2 * Math.PI / ring.length; if (buttons.center) { buttons.center.indexedPosition = [0, 0]; } $ringShape.style.marginLeft = $ringShape.style.marginTop = -radius + 'px'; $ringShape.style.width = $ringShape.style.height = (radius + radius) + 'px'; var $button, angle, x, y; for (var i = 0; i < ring.length; i++) { $button = ring[i].$button; angle = step * i - Math.PI / 2; x = radius * Math.cos(angle); y = radius * Math.sin(angle); ring[i].indexedPosition = [x, y]; $button.style.left = x + 'px'; $button.style.top = y + 'px'; } } function layoutTop(radius) { var xOffset = -$top.clientWidth / 2; var yOffset = -radius * 2 - $top.clientHeight / 2; $top.style.marginLeft = xOffset + 'px'; $top.style.marginTop = yOffset + 'px'; buttons.top.forEach(function(topButton) { var $button = topButton.$button; topButton.indexedPosition = [xOffset + $button.offsetLeft + $button.clientWidth / 2, yOffset]; }); } function layoutBottom(radius) { var xOffset = -$bottom.clientWidth / 2; var yOffset = radius * 2 - $bottom.clientHeight / 2; $bottom.style.marginLeft = xOffset + 'px'; $bottom.style.marginTop = yOffset + 'px'; buttons.bottom.forEach(function(bottomButton) { var $button = bottomButton.$button; bottomButton.indexedPosition = [xOffset + $button.offsetLeft + $button.clientWidth / 2, yOffset]; }); } function indexPosition() { var positionedButtons = allButtons.filter(function(button) { return button.indexedPosition; }); positionedButtons.forEach(findNeightbour); function findNeightbour(button) { var neighbor = {}; var coef = 0; var minCoef = {}; var homePosition = button.indexedPosition; var candidatePosition, dx, dy, ds; var possible, dir; var abs = Math.abs; positionedButtons.forEach(function(candidate) { if (button == candidate) return; candidatePosition = candidate.indexedPosition; possible = []; dx = candidatePosition[0] - homePosition[0]; dy = candidatePosition[1] - homePosition[1]; ds = Math.sqrt(dx * dx + dy * dy); if (abs(dx) > 2) { possible.push(dx > 0 ? 'right' : 'left'); possible.push(ds + abs(dy)); // coef for right/left neighbor } if (abs(dy) > 2) { possible.push(dy > 0 ? 'down' : 'up'); possible.push(ds + abs(dx)); // coef for up/down neighbor } while (possible.length) { dir = possible.shift(); coef = possible.shift(); if (!neighbor[dir] || coef < minCoef[dir]) { neighbor[dir] = candidate; minCoef[dir] = coef; } } }); button.neighbor = neighbor; } } } function alwaysEnable() { return true; } // 为状态创建按钮 function createButton(option) { var $button = createElement(div); addElementClass($button, 'button'); var render = option.render || defaultButtonRender; $button.innerHTML = render(format, option); switch (option.position) { case 'center': appendChild($center, $button); break; case 'ring': appendChild($ring, $button); break; case 'top': appendChild($top, $button); break; case 'bottom': appendChild($bottom, $button); break; } return { action: option.action, enable: option.enable || alwaysEnable, beforeShow: option.beforeShow, key: option.key, next: option.next, label: option.label, data: option.data || null, $button: $button }; } // 默认按钮渲染 function defaultButtonRender(format, option) { return format('{label}{key}', { label: option.label, key: option.key && option.key.split('|')[0] }); } // 为当前状态添加按钮 this.button = function(option) { var button = createButton(option); if (option.position == 'center') { buttons.center = button; } else if (buttons[option.position]) { buttons[option.position].push(button); } allButtons.push(button); needLayout = true; }; function activeState(position) { position = position || { x: hotBox.$container.clientWidth / 2, y: hotBox.$container.clientHeight / 2 }; if (position) { $state.style.left = position.x + 'px'; $state.style.top = position.y + 'px'; } allButtons.forEach(function(button) { var $button = button.$button; if ($button) { $button.classList[button.enable() ? 'add' : 'remove']('enabled'); } if (button.beforeShow) { button.beforeShow(); } }); addElementClass($state, STATE_ACTIVE_CLASS); if (needLayout) { layout(); } if (!selectedButton) { select(buttons.center || buttons.ring[0] || buttons.top[0] || buttons.bottom[0]); } stateActived = true; } function deactiveState() { removeElementClass($state, STATE_ACTIVE_CLASS); select(null); stateActived = false; } // 激活当前状态 this.active = activeState; // 反激活当前状态 this.deactive = deactiveState; function press(button) { if (pressedButton && pressedButton.$button) { removeElementClass(pressedButton.$button, BUTTON_PRESSED_CLASS); } pressedButton = button; if (pressedButton && pressedButton.$button) { addElementClass(pressedButton.$button, BUTTON_PRESSED_CLASS); } } function select(button) { if (selectedButton && selectedButton.$button) { if (selectedButton.$button) { removeElementClass(selectedButton.$button, BUTTON_SELECTED_CLASS); } } selectedButton = button; if (selectedButton && selectedButton.$button) { addElementClass(selectedButton.$button, BUTTON_SELECTED_CLASS); } } $state.onmouseup = function(e) { if (e.button) return; var target = e.target; while (target && target != $state) { if (target.classList.contains('button')) { allButtons.forEach(function(button) { if (button.$button == target) { execute(button); } }); } target = target.parentNode; } }; this.handleKeyEvent = function(e) { var handleResult = null; /** * @Desc: 搜狗浏览器下esc只触发keyup,因此做兼容性处理 * @Editor: Naixor * @Date: 2015.09.14 */ if (hotBox.browser.sg) { if (e.isKey('esc')) { if (pressedButton) { // 若存在已经按下的按钮,则取消操作 if (!e.isKey(pressedButton.key)) { // the button is not esc press(null); } } else { hotBox.active('back', hotBox.position); } return 'back'; }; }; if (e.keydown || (hotBox.isIME && e.keyup)) { allButtons.forEach(function(button) { if (button.enable() && e.isKey(button.key)) { if (stateActived || hotBox.hintDeactiveMainState) { select(button); press(button); handleResult = 'buttonpress'; // 如果是 keyup 事件触发的,因为没有后续的按键事件,所以就直接执行 if(e.keyup) { execute(button); handleResult = 'execute'; return handleResult; } } else { execute(button); handleResult = 'execute'; } e.preventDefault(); e.stopPropagation(); if (!stateActived && hotBox.hintDeactiveMainState) { hotBox.active(stateName, hotBox.position); } } }); if (stateActived) { if (e.isKey('esc')) { if (pressedButton) { // 若存在已经按下的按钮,则取消操作 if (!e.isKey(pressedButton.key)) { // the button is not esc press(null); } } else { hotBox.active('back', hotBox.position); } return 'back'; } ['up', 'down', 'left', 'right'].forEach(function(dir) { if (!e.isKey(dir)) return; if (!selectedButton) { select(buttons.center || buttons.ring[0] || buttons.top[0] || buttons.bottom[0]); return; } var neighbor = selectedButton.neighbor[dir]; while (neighbor && !neighbor.enable()) { neighbor = neighbor.neighbor[dir]; } if (neighbor) { select(neighbor); } handleResult = 'navigate'; }); // 若是由 keyup 触发的,则直接执行选中的按钮 if (e.isKey('space') && e.keyup) { execute(selectedButton); e.preventDefault(); e.stopPropagation(); handleResult = 'execute'; } else if (e.isKey('space') && selectedButton) { press(selectedButton); handleResult = 'buttonpress'; } else if (pressedButton && pressedButton != selectedButton) { press(null); handleResult = 'selectcancel'; } } } else if (e.keyup && (stateActived || !hotBox.hintDeactiveMainState)) { if (pressedButton) { if (e.isKey('space') && selectedButton == pressedButton || e.isKey(pressedButton.key)) { execute(pressedButton); e.preventDefault(); e.stopPropagation(); handleResult = 'execute'; } } } /* * Add by zhangbobell 2015.09.06 * 增加了下面这一个判断因为 safari 下开启输入法后,所有的 keydown 的 keycode 都为 229, * 只能以 keyup 的 keycode 进行判断 * */ hotBox.isIME = (e.keyCode == 229 && e.keydown); return handleResult; }; function execute(button) { if (button) { if (!button.enable || button.enable()) { if (button.action) button.action(button); hotBox.active(button.next || IDLE, hotBox.position); } press(null); select(null); } } } module.exports = HotBox; });