/*! * ==================================================== * Hot Box UI - v1.0.15 - 2017-05-05 * https://github.com/fex-team/hotbox * GitHub: https://github.com/fex-team/hotbox.git * Copyright (c) 2017 Baidu FEX; Licensed BSD * ==================================================== */ (function () { var _p = { r: function(index) { if (_p[index].inited) { return _p[index].value; } if (typeof _p[index].value === "function") { var module = { exports: {} }, returnValue = _p[index].value(null, module.exports, module); _p[index].inited = true; _p[index].value = returnValue; if (returnValue !== undefined) { return returnValue; } else { for (var key in module.exports) { if (module.exports.hasOwnProperty(key)) { _p[index].inited = true; _p[index].value = module.exports; return module.exports; } } } } else { _p[index].inited = true; return _p[index].value; } } }; //src/expose.js _p[0] = { value: function(require, exports, module) { module.exports = window.HotBox = _p.r(1); } }; //src/hotbox.js _p[1] = { value: function(require, exports, module) { var key = _p.r(2); var KeyControl = _p.r(3); /**** 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)); } if (abs(dy) > 2) { possible.push(dy > 0 ? "down" : "up"); possible.push(ds + abs(dx)); } 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; } }; //src/key.js _p[2] = { value: function(require, exports, module) { var keymap = _p.r(4); var CTRL_MASK = 4096; var ALT_MASK = 8192; var SHIFT_MASK = 16384; function hash(unknown) { if (typeof unknown == "string") { return hashKeyExpression(unknown); } return hashKeyEvent(unknown); } function is(a, b) { return a && b && hash(a) == hash(b); } exports.hash = hash; exports.is = is; function hashKeyEvent(keyEvent) { var hashCode = 0; if (keyEvent.ctrlKey || keyEvent.metaKey) { hashCode |= CTRL_MASK; } if (keyEvent.altKey) { hashCode |= ALT_MASK; } if (keyEvent.shiftKey) { hashCode |= SHIFT_MASK; } // Shift, Control, Alt KeyCode ignored. if ([ 16, 17, 18, 91 ].indexOf(keyEvent.keyCode) == -1) { hashCode |= keyEvent.keyCode; } return hashCode; } function hashKeyExpression(keyExpression) { var hashCode = 0; keyExpression.toLowerCase().split(/\s*\+\s*/).forEach(function(name) { switch (name) { case "ctrl": case "cmd": hashCode |= CTRL_MASK; break; case "alt": hashCode |= ALT_MASK; break; case "shift": hashCode |= SHIFT_MASK; break; default: hashCode |= keymap[name]; } }); return hashCode; } } }; //src/keycontrol.js _p[3] = { value: function(require, exports, module) { var key = _p.r(2); var FOCUS_CLASS = "hotbox-focus"; var RECEIVER_CLASS = "hotbox-key-receiver"; function KeyControl(hotbox) { var _this = this; var _receiver; var _actived = true; var _receiverIsSelfCreated = false; var $container = hotbox.$container; _createReceiver(); _bindReceiver(); _bindContainer(); _active(); function _createReceiver() { _receiver = document.createElement("input"); _receiver.classList.add(RECEIVER_CLASS); $container.appendChild(_receiver); _receiverIsSelfCreated = true; } function _bindReceiver() { _receiver.onkeyup = _handle; _receiver.onkeypress = _handle; _receiver.onkeydown = _handle; _receiver.onfocus = _active; _receiver.onblur = _deactive; if (_receiverIsSelfCreated) { _receiver.oninput = function(e) { _receiver.value = null; }; } } function _bindContainer() { $container.onmousedown = function(e) { _active(); e.preventDefault(); }; } function _handle(keyEvent) { if (!_actived) return; hotbox.dispatch(keyEvent); } function _active() { _receiver.select(); _receiver.focus(); _actived = true; $container.classList.add(FOCUS_CLASS); } function _deactive() { _receiver.blur(); _actived = false; $container.classList.remove(FOCUS_CLASS); } this.handle = _handle; this.active = _active; this.deactive = _deactive; } module.exports = KeyControl; } }; //src/keymap.js _p[4] = { value: function(require, exports, module) { var keymap = { Shift: 16, Control: 17, Alt: 18, CapsLock: 20, BackSpace: 8, Tab: 9, Enter: 13, Esc: 27, Space: 32, PageUp: 33, PageDown: 34, End: 35, Home: 36, Insert: 45, Left: 37, Up: 38, Right: 39, Down: 40, Direction: { 37: 1, 38: 1, 39: 1, 40: 1 }, Delete: 46, NumLock: 144, Cmd: 91, CmdFF: 224, F1: 112, F2: 113, F3: 114, F4: 115, F5: 116, F6: 117, F7: 118, F8: 119, F9: 120, F10: 121, F11: 122, F12: 123, "`": 192, "=": 187, "-": 189, "/": 191, ".": 190 }; // 小写适配 for (var key in keymap) { if (keymap.hasOwnProperty(key)) { keymap[key.toLowerCase()] = keymap[key]; } } var aKeyCode = 65; var aCharCode = "a".charCodeAt(0); // letters "abcdefghijklmnopqrstuvwxyz".split("").forEach(function(letter) { keymap[letter] = aKeyCode + (letter.charCodeAt(0) - aCharCode); }); // numbers var n = 9; do { keymap[n.toString()] = n + 48; } while (n--); module.exports = keymap; } }; var moduleMapping = { expose: 0 }; function use(name) { _p.r([ moduleMapping[name] ]); } use('expose'); })();