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;
});