123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636 |
- 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('<span class="label">{label}</span><span class="key">{key}</span>', {
- 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;
- });
|