hotbox.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636
  1. define(function(require, exports, module) {
  2. var key = require('./key');
  3. var KeyControl = require('./keycontrol');
  4. /**** Dom Utils ****/
  5. function createElement(name) {
  6. return document.createElement(name);
  7. }
  8. function setElementAttribute(element, name, value) {
  9. element.setAttribute(name, value);
  10. }
  11. function getElementAttribute(element, name) {
  12. return element.getAttribute(name);
  13. }
  14. function addElementClass(element, name) {
  15. element.classList.add(name);
  16. }
  17. function removeElementClass(element, name) {
  18. element.classList.remove(name);
  19. }
  20. function appendChild(parent, child) {
  21. parent.appendChild(child);
  22. }
  23. /*******************/
  24. var IDLE = HotBox.STATE_IDLE = 'idle';
  25. var div = 'div';
  26. /**
  27. * Simple Formatter
  28. */
  29. function format(template, args) {
  30. if (typeof(args) != 'object') {
  31. args = [].slice.apply(arguments, 1);
  32. }
  33. return String(template).replace(/\{(\w+)\}/g, function(match, name) {
  34. return args[name] || match;
  35. });
  36. }
  37. /**
  38. * Hot Box Class
  39. */
  40. function HotBox($container) {
  41. if (typeof($container) == 'string') {
  42. $container = document.querySelector($container);
  43. }
  44. if (!$container || !($container instanceof HTMLElement)) {
  45. throw new Error('No container or not invalid container for hot box');
  46. }
  47. // 创建 HotBox Dom 解构
  48. var $hotBox = createElement(div);
  49. addElementClass($hotBox, 'hotbox');
  50. appendChild($container, $hotBox);
  51. // 保存 Dom 解构和父容器
  52. this.$element = $hotBox;
  53. this.$container = $container;
  54. // 标示是否是输入法状态
  55. this.isIME = false;
  56. /**
  57. * @Desc: 增加一个browser用于判断浏览器类型,方便解决兼容性问题
  58. * @Editor: Naixor
  59. * @Date: 2015.09.14
  60. */
  61. this.browser = {
  62. sg: /se[\s\S]+metasr/.test(navigator.userAgent.toLowerCase())
  63. };
  64. /*
  65. * added by zhangbobell
  66. * 2015.09.22
  67. * 增加父状态机,以解决在父 FSM 下状态控制的问题,最好的解决办法是增加一个函数队列
  68. * 将其中的函数一起执行。//TODO
  69. * */
  70. this._parentFSM = {};
  71. // 记录位置
  72. this.position = {};
  73. // 已定义的状态(string => HotBoxState)
  74. var _states = {};
  75. // 主状态(HotBoxState)
  76. var _mainState = null;
  77. // 当前状态(HotBoxState)
  78. var _currentState = IDLE;
  79. // 当前状态堆栈
  80. var _stateStack = [];
  81. // 实例引用
  82. var _this = this;
  83. var _controler;
  84. /**
  85. * Controller: {
  86. * constructor(hotbox: HotBox),
  87. * active: () => void
  88. * }
  89. */
  90. function _control(Controller) {
  91. if (_controler) {
  92. _controler.active();
  93. return;
  94. }
  95. Controller = Controller || KeyControl;
  96. _controler = new Controller(_this);
  97. _controler.active();
  98. $hotBox.onmousedown = function(e) {
  99. e.stopPropagation();
  100. e.preventDefault();
  101. };
  102. return _this;
  103. }
  104. function _dispatchKey(e) {
  105. var type = e.type.toLowerCase();
  106. e.keyHash = key.hash(e);
  107. e.isKey = function(keyExpression) {
  108. if (!keyExpression) return false;
  109. var expressions = keyExpression.split(/\s*\|\s*/);
  110. while(expressions.length) {
  111. if (e.keyHash == key.hash(expressions.shift())) return true;
  112. }
  113. return false;
  114. };
  115. e[type] = true;
  116. // Boot: keyup and activeKey pressed on IDLE, active main state.
  117. if (e.keyup && _this.activeKey && e.isKey(_this.activeKey) && _currentState == IDLE && _mainState) {
  118. _activeState('main', {
  119. x: $container.clientWidth / 2,
  120. y: $container.clientHeight / 2
  121. });
  122. return;
  123. }
  124. var handleState = _currentState == IDLE ? _mainState : _currentState;
  125. if (handleState) {
  126. var handleResult = handleState.handleKeyEvent(e);
  127. if (typeof(_this.onkeyevent) == 'function') {
  128. e.handleResult = handleResult;
  129. _this.onkeyevent(e, handleResult);
  130. }
  131. return handleResult;
  132. }
  133. return null;
  134. }
  135. function _addState(name) {
  136. if (!name) return _currentState;
  137. if (name == IDLE) {
  138. throw new Error('Can not define or use the `idle` state.');
  139. }
  140. _states[name] = _states[name] || new HotBoxState(this, name);
  141. if (name == 'main') {
  142. _mainState = _states[name];
  143. }
  144. return _states[name];
  145. }
  146. function _activeState(name, position) {
  147. _this.position = position;
  148. // 回到 IDLE
  149. if (name == IDLE) {
  150. if (_currentState != IDLE) {
  151. _stateStack.shift().deactive();
  152. _stateStack = [];
  153. }
  154. _currentState = IDLE;
  155. }
  156. // 回退一个状态
  157. else if (name == 'back') {
  158. if (_currentState != IDLE) {
  159. _currentState.deactive();
  160. _stateStack.shift();
  161. _currentState = _stateStack[0];
  162. if (_currentState) {
  163. _currentState.active();
  164. } else {
  165. _currentState = 'idle';
  166. }
  167. }
  168. }
  169. // 切换到具体状态
  170. else {
  171. if (_currentState != IDLE) {
  172. _currentState.deactive();
  173. }
  174. var newState = _states[name];
  175. _stateStack.unshift(newState);
  176. if (typeof(_this.position) == 'function') {
  177. position = _this.position(position);
  178. }
  179. newState.active(position);
  180. _currentState = newState;
  181. }
  182. }
  183. function setParentFSM(fsm) {
  184. _this._parentFSM = fsm;
  185. }
  186. function getParentFSM() {
  187. return _this._parentFSM;
  188. }
  189. this.control = _control;
  190. this.state = _addState;
  191. this.active = _activeState;
  192. this.dispatch = _dispatchKey;
  193. this.setParentFSM = setParentFSM;
  194. this.getParentFSM = getParentFSM;
  195. this.activeKey = 'space';
  196. this.actionKey = 'space';
  197. }
  198. /**
  199. * 表示热盒某个状态,包含这些状态需要的 Dom 对象
  200. */
  201. function HotBoxState(hotBox, stateName) {
  202. var BUTTON_SELECTED_CLASS = 'selected';
  203. var BUTTON_PRESSED_CLASS = 'pressed';
  204. var STATE_ACTIVE_CLASS = 'active';
  205. // 状态容器
  206. var $state = createElement(div);
  207. // 四种可见的按钮容器
  208. var $center = createElement(div);
  209. var $ring = createElement(div);
  210. var $ringShape = createElement('div');
  211. var $top = createElement(div);
  212. var $bottom = createElement(div);
  213. // 添加 CSS 类
  214. addElementClass($state, 'state');
  215. addElementClass($state, stateName);
  216. addElementClass($center, 'center');
  217. addElementClass($ring, 'ring');
  218. addElementClass($ringShape, 'ring-shape');
  219. addElementClass($top, 'top');
  220. addElementClass($bottom, 'bottom');
  221. // 摆放容器
  222. appendChild(hotBox.$element, $state);
  223. appendChild($state, $ringShape);
  224. appendChild($state, $center);
  225. appendChild($state, $ring);
  226. appendChild($state, $top);
  227. appendChild($state, $bottom);
  228. // 记住状态名称
  229. this.name = stateName;
  230. // 五种按钮:中心,圆环,上栏,下栏,幕后
  231. var buttons = {
  232. center: null,
  233. ring: [],
  234. top: [],
  235. bottom: [],
  236. behind: []
  237. };
  238. var allButtons = [];
  239. var selectedButton = null;
  240. var pressedButton = null;
  241. var stateActived = false;
  242. // 布局,添加按钮后,标记需要布局
  243. var needLayout = true;
  244. function layout() {
  245. var radius = buttons.ring.length * 15;
  246. layoutRing(radius);
  247. layoutTop(radius);
  248. layoutBottom(radius);
  249. indexPosition();
  250. needLayout = false;
  251. function layoutRing(radius) {
  252. var ring = buttons.ring;
  253. var step = 2 * Math.PI / ring.length;
  254. if (buttons.center) {
  255. buttons.center.indexedPosition = [0, 0];
  256. }
  257. $ringShape.style.marginLeft = $ringShape.style.marginTop = -radius + 'px';
  258. $ringShape.style.width = $ringShape.style.height = (radius + radius) + 'px';
  259. var $button, angle, x, y;
  260. for (var i = 0; i < ring.length; i++) {
  261. $button = ring[i].$button;
  262. angle = step * i - Math.PI / 2;
  263. x = radius * Math.cos(angle);
  264. y = radius * Math.sin(angle);
  265. ring[i].indexedPosition = [x, y];
  266. $button.style.left = x + 'px';
  267. $button.style.top = y + 'px';
  268. }
  269. }
  270. function layoutTop(radius) {
  271. var xOffset = -$top.clientWidth / 2;
  272. var yOffset = -radius * 2 - $top.clientHeight / 2;
  273. $top.style.marginLeft = xOffset + 'px';
  274. $top.style.marginTop = yOffset + 'px';
  275. buttons.top.forEach(function(topButton) {
  276. var $button = topButton.$button;
  277. topButton.indexedPosition = [xOffset + $button.offsetLeft + $button.clientWidth / 2, yOffset];
  278. });
  279. }
  280. function layoutBottom(radius) {
  281. var xOffset = -$bottom.clientWidth / 2;
  282. var yOffset = radius * 2 - $bottom.clientHeight / 2;
  283. $bottom.style.marginLeft = xOffset + 'px';
  284. $bottom.style.marginTop = yOffset + 'px';
  285. buttons.bottom.forEach(function(bottomButton) {
  286. var $button = bottomButton.$button;
  287. bottomButton.indexedPosition = [xOffset + $button.offsetLeft + $button.clientWidth / 2, yOffset];
  288. });
  289. }
  290. function indexPosition() {
  291. var positionedButtons = allButtons.filter(function(button) {
  292. return button.indexedPosition;
  293. });
  294. positionedButtons.forEach(findNeightbour);
  295. function findNeightbour(button) {
  296. var neighbor = {};
  297. var coef = 0;
  298. var minCoef = {};
  299. var homePosition = button.indexedPosition;
  300. var candidatePosition, dx, dy, ds;
  301. var possible, dir;
  302. var abs = Math.abs;
  303. positionedButtons.forEach(function(candidate) {
  304. if (button == candidate) return;
  305. candidatePosition = candidate.indexedPosition;
  306. possible = [];
  307. dx = candidatePosition[0] - homePosition[0];
  308. dy = candidatePosition[1] - homePosition[1];
  309. ds = Math.sqrt(dx * dx + dy * dy);
  310. if (abs(dx) > 2) {
  311. possible.push(dx > 0 ? 'right' : 'left');
  312. possible.push(ds + abs(dy)); // coef for right/left neighbor
  313. }
  314. if (abs(dy) > 2) {
  315. possible.push(dy > 0 ? 'down' : 'up');
  316. possible.push(ds + abs(dx)); // coef for up/down neighbor
  317. }
  318. while (possible.length) {
  319. dir = possible.shift();
  320. coef = possible.shift();
  321. if (!neighbor[dir] || coef < minCoef[dir]) {
  322. neighbor[dir] = candidate;
  323. minCoef[dir] = coef;
  324. }
  325. }
  326. });
  327. button.neighbor = neighbor;
  328. }
  329. }
  330. }
  331. function alwaysEnable() {
  332. return true;
  333. }
  334. // 为状态创建按钮
  335. function createButton(option) {
  336. var $button = createElement(div);
  337. addElementClass($button, 'button');
  338. var render = option.render || defaultButtonRender;
  339. $button.innerHTML = render(format, option);
  340. switch (option.position) {
  341. case 'center': appendChild($center, $button); break;
  342. case 'ring': appendChild($ring, $button); break;
  343. case 'top': appendChild($top, $button); break;
  344. case 'bottom': appendChild($bottom, $button); break;
  345. }
  346. return {
  347. action: option.action,
  348. enable: option.enable || alwaysEnable,
  349. beforeShow: option.beforeShow,
  350. key: option.key,
  351. next: option.next,
  352. label: option.label,
  353. data: option.data || null,
  354. $button: $button
  355. };
  356. }
  357. // 默认按钮渲染
  358. function defaultButtonRender(format, option) {
  359. return format('<span class="label">{label}</span><span class="key">{key}</span>', {
  360. label: option.label,
  361. key: option.key && option.key.split('|')[0]
  362. });
  363. }
  364. // 为当前状态添加按钮
  365. this.button = function(option) {
  366. var button = createButton(option);
  367. if (option.position == 'center') {
  368. buttons.center = button;
  369. } else if (buttons[option.position]) {
  370. buttons[option.position].push(button);
  371. }
  372. allButtons.push(button);
  373. needLayout = true;
  374. };
  375. function activeState(position) {
  376. position = position || {
  377. x: hotBox.$container.clientWidth / 2,
  378. y: hotBox.$container.clientHeight / 2
  379. };
  380. if (position) {
  381. $state.style.left = position.x + 'px';
  382. $state.style.top = position.y + 'px';
  383. }
  384. allButtons.forEach(function(button) {
  385. var $button = button.$button;
  386. if ($button) {
  387. $button.classList[button.enable() ? 'add' : 'remove']('enabled');
  388. }
  389. if (button.beforeShow) {
  390. button.beforeShow();
  391. }
  392. });
  393. addElementClass($state, STATE_ACTIVE_CLASS);
  394. if (needLayout) {
  395. layout();
  396. }
  397. if (!selectedButton) {
  398. select(buttons.center || buttons.ring[0] || buttons.top[0] || buttons.bottom[0]);
  399. }
  400. stateActived = true;
  401. }
  402. function deactiveState() {
  403. removeElementClass($state, STATE_ACTIVE_CLASS);
  404. select(null);
  405. stateActived = false;
  406. }
  407. // 激活当前状态
  408. this.active = activeState;
  409. // 反激活当前状态
  410. this.deactive = deactiveState;
  411. function press(button) {
  412. if (pressedButton && pressedButton.$button) {
  413. removeElementClass(pressedButton.$button, BUTTON_PRESSED_CLASS);
  414. }
  415. pressedButton = button;
  416. if (pressedButton && pressedButton.$button) {
  417. addElementClass(pressedButton.$button, BUTTON_PRESSED_CLASS);
  418. }
  419. }
  420. function select(button) {
  421. if (selectedButton && selectedButton.$button) {
  422. if (selectedButton.$button) {
  423. removeElementClass(selectedButton.$button, BUTTON_SELECTED_CLASS);
  424. }
  425. }
  426. selectedButton = button;
  427. if (selectedButton && selectedButton.$button) {
  428. addElementClass(selectedButton.$button, BUTTON_SELECTED_CLASS);
  429. }
  430. }
  431. $state.onmouseup = function(e) {
  432. if (e.button) return;
  433. var target = e.target;
  434. while (target && target != $state) {
  435. if (target.classList.contains('button')) {
  436. allButtons.forEach(function(button) {
  437. if (button.$button == target) {
  438. execute(button);
  439. }
  440. });
  441. }
  442. target = target.parentNode;
  443. }
  444. };
  445. this.handleKeyEvent = function(e) {
  446. var handleResult = null;
  447. /**
  448. * @Desc: 搜狗浏览器下esc只触发keyup,因此做兼容性处理
  449. * @Editor: Naixor
  450. * @Date: 2015.09.14
  451. */
  452. if (hotBox.browser.sg) {
  453. if (e.isKey('esc')) {
  454. if (pressedButton) { // 若存在已经按下的按钮,则取消操作
  455. if (!e.isKey(pressedButton.key)) { // the button is not esc
  456. press(null);
  457. }
  458. } else {
  459. hotBox.active('back', hotBox.position);
  460. }
  461. return 'back';
  462. };
  463. };
  464. if (e.keydown || (hotBox.isIME && e.keyup)) {
  465. allButtons.forEach(function(button) {
  466. if (button.enable() && e.isKey(button.key)) {
  467. if (stateActived || hotBox.hintDeactiveMainState) {
  468. select(button);
  469. press(button);
  470. handleResult = 'buttonpress';
  471. // 如果是 keyup 事件触发的,因为没有后续的按键事件,所以就直接执行
  472. if(e.keyup) {
  473. execute(button);
  474. handleResult = 'execute';
  475. return handleResult;
  476. }
  477. } else {
  478. execute(button);
  479. handleResult = 'execute';
  480. }
  481. e.preventDefault();
  482. e.stopPropagation();
  483. if (!stateActived && hotBox.hintDeactiveMainState) {
  484. hotBox.active(stateName, hotBox.position);
  485. }
  486. }
  487. });
  488. if (stateActived) {
  489. if (e.isKey('esc')) {
  490. if (pressedButton) { // 若存在已经按下的按钮,则取消操作
  491. if (!e.isKey(pressedButton.key)) { // the button is not esc
  492. press(null);
  493. }
  494. } else {
  495. hotBox.active('back', hotBox.position);
  496. }
  497. return 'back';
  498. }
  499. ['up', 'down', 'left', 'right'].forEach(function(dir) {
  500. if (!e.isKey(dir)) return;
  501. if (!selectedButton) {
  502. select(buttons.center || buttons.ring[0] || buttons.top[0] || buttons.bottom[0]);
  503. return;
  504. }
  505. var neighbor = selectedButton.neighbor[dir];
  506. while (neighbor && !neighbor.enable()) {
  507. neighbor = neighbor.neighbor[dir];
  508. }
  509. if (neighbor) {
  510. select(neighbor);
  511. }
  512. handleResult = 'navigate';
  513. });
  514. // 若是由 keyup 触发的,则直接执行选中的按钮
  515. if (e.isKey('space') && e.keyup) {
  516. execute(selectedButton);
  517. e.preventDefault();
  518. e.stopPropagation();
  519. handleResult = 'execute';
  520. } else if (e.isKey('space') && selectedButton) {
  521. press(selectedButton);
  522. handleResult = 'buttonpress';
  523. } else if (pressedButton && pressedButton != selectedButton) {
  524. press(null);
  525. handleResult = 'selectcancel';
  526. }
  527. }
  528. }
  529. else if (e.keyup && (stateActived || !hotBox.hintDeactiveMainState)) {
  530. if (pressedButton) {
  531. if (e.isKey('space') && selectedButton == pressedButton || e.isKey(pressedButton.key)) {
  532. execute(pressedButton);
  533. e.preventDefault();
  534. e.stopPropagation();
  535. handleResult = 'execute';
  536. }
  537. }
  538. }
  539. /*
  540. * Add by zhangbobell 2015.09.06
  541. * 增加了下面这一个判断因为 safari 下开启输入法后,所有的 keydown 的 keycode 都为 229,
  542. * 只能以 keyup 的 keycode 进行判断
  543. * */
  544. hotBox.isIME = (e.keyCode == 229 && e.keydown);
  545. return handleResult;
  546. };
  547. function execute(button) {
  548. if (button) {
  549. if (!button.enable || button.enable()) {
  550. if (button.action) button.action(button);
  551. hotBox.active(button.next || IDLE, hotBox.position);
  552. }
  553. press(null);
  554. select(null);
  555. }
  556. }
  557. }
  558. module.exports = HotBox;
  559. });