')
56 | .append($('| ').addClass('hometype-help-box-commands-key').text(key + ' :'))
57 | .append($(' | ').addClass('hometype-help-box-commands-command').text(command))
58 | );
59 | });
60 | });
61 |
62 | this.box = box;
63 |
64 | $(document).ready(function() {
65 | box.appendTo($('body'));
66 | });
67 | };
68 |
69 | HometypeHelpBox.prototype.show = function() {
70 | // Calculate position.
71 | var scrollTop = Viewport.getScrollPosition().top;
72 |
73 | // Place command box to calculated position and show it.
74 | this.box.css({
75 | top: scrollTop * 1.2,
76 | left: HELP_BOX_MARGIN
77 | }).fadeIn(300);
78 |
79 | this.box.fadeIn(300);
80 | };
81 |
82 | HometypeHelpBox.prototype.hide = function() {
83 | this.box.hide();
84 | };
85 |
86 | var HelpBox = new HometypeHelpBox();
87 |
--------------------------------------------------------------------------------
/js/key/map.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2013 Kengo Tateishi (@tkengo)
3 | * Licensed under MIT license.
4 | * http://www.opensource.org/licenses/mit-license.php
5 | *
6 | * Manage key and command mapping.
7 | */
8 |
9 | /**
10 | * Key mapping managing object.
11 | * Mapping information is saved to this object.
12 | *
13 | * Hometype has below modes.
14 | * normal mode
15 | * hint mode
16 | * visual mode
17 | * command mode
18 | */
19 | var KeyMap = {
20 | };
21 | var _map = {
22 | normal: {},
23 | insert: {},
24 | hint: {},
25 | visual: {},
26 | command: {},
27 | help: {}
28 | }
29 |
30 | /**
31 | * Assign key to command in specified mode.
32 | *
33 | * @param string mode The mode that assign key
34 | * @param string key Assigned key
35 | * @param string command Command that assigned in key
36 | */
37 | KeyMap.assign = function(mode, key, command) {
38 | _map[mode][key] = command;
39 | };
40 |
41 | /**
42 | * Assign key to command in normal mode.
43 | *
44 | * @param string key Assigned key
45 | * @param string command Command that assigned in key
46 | */
47 | KeyMap.nmap = function(key, command) {
48 | KeyMap.assign(ModeList.NORMAL_MODE, key, command);
49 | };
50 |
51 | /**
52 | * Assign key to command in insert mode.
53 | *
54 | * @param string key Assigned key
55 | * @param string command Command that assigned in key
56 | */
57 | KeyMap.imap = function(key, command) {
58 | KeyMap.assign(ModeList.INSERT_MODE, key, command);
59 | };
60 |
61 | /**
62 | * Assign key to command in hint mode.
63 | *
64 | * @param string key Assigned key
65 | * @param string command Command that assigned in key
66 | */
67 | KeyMap.fmap = function(key, command) {
68 | KeyMap.assign(ModeList.HINT_MODE, key, command);
69 | };
70 |
71 | /**
72 | * Assign key to command in visual mode.
73 | *
74 | * @param string key Assigned key
75 | * @param string command Command that assigned in key
76 | */
77 | KeyMap.vmap = function(key, command) {
78 | KeyMap.assign(ModeList.VISUAL_MODE, key, command);
79 | };
80 |
81 | /**
82 | * Assign key to command in command mode.
83 | *
84 | * @param string key Assigned key
85 | * @param string command Command that assigned in key
86 | */
87 | KeyMap.cmap = function(key, command) {
88 | KeyMap.assign(ModeList.COMMAND_MODE, key, command);
89 | };
90 |
91 | /**
92 | * Assign key to command in help mode.
93 | *
94 | * @param string key Assigned key
95 | * @param string command Command that assigned in key
96 | */
97 | KeyMap.hmap = function(key, command) {
98 | KeyMap.assign(ModeList.HELP_MODE, key, command);
99 | };
100 |
101 | /**
102 | * Get the command that is assigned specified key in specified mode.
103 | *
104 | * @param string mode The mode.
105 | * @param string key Assigned key.
106 | * @return function Command name.
107 | */
108 | KeyMap.command = function(mode, key) {
109 | return Command[_map[mode][key]];
110 | };
111 |
112 | KeyMap.assignedCommands = function() {
113 | return _map;
114 | };
115 |
116 | /**
117 | * Clear all key binding.
118 | */
119 | KeyMap.clear = function() {
120 | $.each(_map, function(key, map) {
121 | _map[key] = {};
122 | });
123 | };
124 |
--------------------------------------------------------------------------------
/lib/utility.js:
--------------------------------------------------------------------------------
1 | var Utility = {};
2 |
3 | Utility.clickElement = function(element, commandKey) {
4 | commandKey = commandKey || false;
5 |
6 | var event = document.createEvent('MouseEvents');
7 | event.initMouseEvent('click', true, true, window, 1, 0, 0, 0, 0, false, false, false, commandKey, 0, null);
8 | element.get(0).dispatchEvent(event);
9 | };
10 |
11 | /**
12 | * Open the url in the current tab.
13 | *
14 | * @param string url Target url.
15 | */
16 | Utility.openUrl = function(url) {
17 | var params = { url: url };
18 | chrome.runtime.sendMessage({ command: 'openUrl', params: params });
19 | };
20 |
21 | /**
22 | * Open the url in a new window.
23 | *
24 | * @param string url Target url.
25 | */
26 | Utility.openUrlInNewWindow = function(url) {
27 | var params = {
28 | url: url,
29 | newWindow: true
30 | };
31 | chrome.runtime.sendMessage({ command: 'openUrl', params: params });
32 | };
33 |
34 | /**
35 | * Check if a word is included in specified properties in target object.
36 | *
37 | * @param Object target Target object.
38 | * @param string word Search word.
39 | * @param array properties Searched properties.
40 | * @param boolean ignoreCase Search by ignore case if this argument is true.
41 | * @return boolean Return true if a word is included in specified properties in target object.
42 | */
43 | Utility.includedInProperties = function(target, word, properties, ignoreCase) {
44 | ignoreCase = ignoreCase || true;
45 |
46 | if (ignoreCase) {
47 | word = word.toLowerCase();
48 | }
49 |
50 | var included = false;
51 |
52 | for (var i in properties) {
53 | var propertyValue = target[properties[i]];
54 | if (ignoreCase) {
55 | propertyValue = propertyValue.toLowerCase();
56 | }
57 |
58 | if (propertyValue.indexOf(word) > -1) {
59 | included = true;
60 | break;
61 | }
62 | }
63 |
64 | return included;
65 | };
66 |
67 | /**
68 | * Do zero padding for passed number.
69 | *
70 | * @param integer number zero padding target.
71 | * @param integer length length.
72 | * @return string zero padded number.
73 | */
74 | Utility.pad = function(number, length) {
75 | var str = '' + number;
76 | while (str.length < length) {
77 | str = '0' + str;
78 | }
79 |
80 | return str;
81 | };
82 |
83 | /**
84 | * Check if a value is included in an array.
85 | *
86 | * @param array array A target array.
87 | * @param mixed value Check value.
88 | * @return boolean Return true if the value is included in the array.
89 | */
90 | Utility.inArray = function(array, value) {
91 | for (var i = 0; i < array.length; i++) {
92 | if (array[i] === value) {
93 | return true;
94 | }
95 | }
96 |
97 | return false;
98 | };
99 |
100 | /**
101 | * Collect a hash key and return it.
102 | *
103 | * @param array arrayOfHash A target array that has a hash.
104 | * @param string key A key you want to collect.
105 | * @return array Collected value list.
106 | */
107 | Utility.collect = function(arrayOfHash, key) {
108 | var items = [];
109 |
110 | for (var i = 0; i < arrayOfHash.length; i++) {
111 | items.push(arrayOfHash[i][key]);
112 | }
113 |
114 | return items;
115 | };
116 |
--------------------------------------------------------------------------------
/js/hint/element_collection.js:
--------------------------------------------------------------------------------
1 | var HintElementCollection = function(hintTheme, target) {
2 | this.originalElements = target;
3 | this.hintTheme = hintTheme;
4 |
5 | this.createHintsFrom(target);
6 | };
7 |
8 | HintElementCollection.prototype.createHintsFrom = function(target) {
9 | this.elements = [];
10 | this.hintKeys = [];
11 |
12 | var keyAl = HintKeyFactory.create(target.length);
13 | var parent = document.createElement('div');
14 |
15 | for (var i = 0; i < target.length; i++) {
16 | var key = keyAl.pop();
17 | var element = new HintElement(target[i], i, key, this.hintTheme);
18 |
19 | this.elements.push(element);
20 | this.hintKeys.push({ index: i, key: key });
21 | parent.appendChild(element.getTipElement());
22 | }
23 |
24 | document.documentElement.appendChild(parent);
25 |
26 | return this.elements;
27 | };
28 |
29 | HintElementCollection.prototype.getElements = function() {
30 | return this.elements;
31 | };
32 |
33 | HintElementCollection.prototype.getMatchedElements = function(key) {
34 | var results = [];
35 |
36 | for (var i in this.hintKeys) {
37 | var hintKey = this.hintKeys[i];
38 | if (hintKey.key.indexOf(key) == 0) {
39 | results.push(this.elements[hintKey.index]);
40 | }
41 | }
42 |
43 | return results;
44 | };
45 |
46 | HintElementCollection.prototype.hideUnmatchedElements = function(key) {
47 | for (var i = 0; i < this.hintKeys.length; i++) {
48 | var hintKey = this.hintKeys[i];
49 | var element = this.elements[hintKey.index];
50 | if (hintKey.key.indexOf(key) != 0) {
51 | element.removeHintTip();
52 | } else {
53 | element.setPushed();
54 | }
55 | }
56 | };
57 |
58 | HintElementCollection.prototype.regenerateHintsBy = function(text) {
59 | var homedics = new Homedics(text);
60 | var originalElements = this.originalElements;
61 | var regenerateElements = [];
62 | var matches = [];
63 |
64 | this.removeAllHint();
65 |
66 | if (text == '') {
67 | regenerateElements = originalElements;
68 | } else {
69 | for (var i = 0; i < originalElements.length; i++) {
70 | var element = originalElements[i];
71 | var result = homedics.match(element.innerText.trim().toLowerCase());
72 |
73 | if (result.matched) {
74 | regenerateElements.push(element);
75 | matches.push(result.matches);
76 | }
77 | }
78 |
79 | if (regenerateElements.length > 0) {
80 | regenerateElements[0].className = regenerateElements[0].className + ' hometype-hit-a-hint-head-area';
81 | }
82 | }
83 |
84 | regenerateElements = this.createHintsFrom(regenerateElements);
85 | for (var i = 0; i < matches.length; i++) {
86 | regenerateElements[i].highlight(matches[i]);
87 | }
88 |
89 | return regenerateElements;
90 | };
91 |
92 | HintElementCollection.prototype.getFilteringMatchedElement = function() {
93 | for (var i = 0; i < this.elements.length; i++) {
94 | var element = this.elements[i].getElement();
95 | if (element.className.indexOf('hometype-hit-a-hint-head-area') > -1) {
96 | return element;
97 | }
98 | }
99 |
100 | return null;
101 | };
102 |
103 | HintElementCollection.prototype.removeAllHint = function() {
104 | for (var i = 0; i < this.elements.length; i++) {
105 | this.elements[i].removeHintTip();
106 | }
107 | };
108 |
--------------------------------------------------------------------------------
/_locales/ja/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "basic_options": {
3 | "message": "基本設定"
4 | },
5 | "key_bindings": {
6 | "message": "キーバインド"
7 | },
8 | "basic_config": {
9 | "message": "基本"
10 | },
11 | "command_interval": {
12 | "message": "インターバル"
13 | },
14 | "amount_of_scrolling": {
15 | "message": "スクロール量"
16 | },
17 | "hint_config": {
18 | "message": "ヒント"
19 | },
20 | "key_algorithm": {
21 | "message": "アルゴリズム"
22 | },
23 | "standard": {
24 | "message": "スタンダード"
25 | },
26 | "comfortable": {
27 | "message": "コンフォータブル"
28 | },
29 | "custom": {
30 | "message": "カスタム"
31 | },
32 | "letter_type": {
33 | "message": "文字種別"
34 | },
35 | "hint_auto_confirm": {
36 | "message": "リンク確定時に自動的に遷移する"
37 | },
38 | "lowercase": {
39 | "message": "小文字"
40 | },
41 | "uppercase": {
42 | "message": "大文字"
43 | },
44 | "ignore_urls": {
45 | "message": "無視URL"
46 | },
47 | "save": {
48 | "message": "保存"
49 | },
50 |
51 | "desc_for_command_interval": {
52 | "message": "このオプションはコマンドが確定するまでの時間を指定します。キーを押下した後にコマンドが一意に決まらない場合にこの時間だけ待って、この時間が過ぎてから初めてコマンドが確定されます。"
53 | },
54 | "desc_for_amount_of_scrolling": {
55 | "message": "scrollDown または scrollUp が呼ばれた時のスクロール量を指定します。"
56 | },
57 | "desc_for_key_algorithm": {
58 | "message": "
59 | ヒントモード時にヒントキーを生成するアルゴリズムを選択できます。
60 |
61 | - 1. スタンダード
62 | - jfhkgyuiopqwertnmzxcvblasd の文字の中から、先頭から1文字ずつ取り出した文字を組み合わせてヒントキーを生成します。
63 | - 2. コンフォータブル
64 | - 出来るだけ片手でキーを押下できるようにヒントキーを生成します。
65 | - 3. カスタム
66 | - ヒントキーに現れる文字を自分で指定します。必ず2つ以上のキーを指定して下さい。
67 |
68 | "
69 | },
70 | "desc_for_letter_type": {
71 | "message": "
72 | ヒントキーの文字種別を選択できます。
73 |
74 | - 1. 小文字
75 | - 生成されるヒントキーは小文字になります。
76 | - 2. 大文字
77 | - 生成されるヒントキーは大文字になります。
78 |
79 | "
80 | },
81 | "desc_for_hint_auto_confirm": {
82 | "message": "このオプションにチェックをいれると、ヒントモード中に要素をテキスト検索してマッチした要素が1つに絞られた場合、自動的にその要素をクリックして次の画面へ遷移するようになります。"
83 | },
84 | "desc_for_ignore_urls": {
85 | "message": "Hometypeを無効にしたいURLを改行区切りで指定することができます。正規表現を使えます。"
86 | },
87 | "desc_for_key_bindings": {
88 | "message": "
89 | 以下のような形式でカスタムキーバインドを設定することができます。
90 |
91 | map-type assigned-keys executed-commands [--options]
92 |
93 | 例えば以下の様にキーバインドを設定できます。
94 |
95 | nmap j scrollDown
96 | vmap <C-c> enterNormalMode
97 | imap Jf scrollDown followLink
98 |
99 |
100 | - map-type
101 | - nmap, cmap, imap, vmap, fmapを指定できます。これらはそれぞれ ノーマルモード, コマンドモード, インサートモード, ビジュアルモード, ヒントモード でのキーバインド設定となります。
102 | - assigned-keys
103 | - コマンドに対応するキーを指定します。2文字以上の複数キーも指定できます。
104 | - executed-commands
105 | - 実行するコマンドを指定します。指定したいオプションがあればここで一緒に指定します。2つ以上の複数のコマンドも指定できます。コマンド名の最初に @ をつけると、最後に実行したコマンドのオプションが引き継がれて実行されます。
106 |
107 | Hometypeに実装されているコマンドはコマンドリファレンスを参考にしてください。
108 | "
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/lib/jquery.extend.js:
--------------------------------------------------------------------------------
1 | (function($) {
2 | // TODO: Refactor. There is an overlapped code with this in viewport.js.
3 | var scrollingArea = function() {
4 | if ('scrollingElement' in document) {
5 | return document.scrollingElement;
6 | // Fallback for legacy browsers
7 | } else if (navigator.userAgent.indexOf('WebKit') != -1) {
8 | return document.body;
9 | }
10 |
11 | return document.documentElement;
12 | }();
13 |
14 | $.fn.screenCenter = function() {
15 | return this.each(function() {
16 | var windowWidth = window.innerWidth;
17 | var windowHeight = window.innerHeight;
18 | var elementWidth = $(this).outerWidth();
19 | var elementHeight = $(this).outerHeight();
20 | var screenOffsetTop = scrollingArea.scrollTop;
21 |
22 | var top = screenOffsetTop + (windowHeight / 2) - (elementHeight / 2);
23 | var left = (windowWidth / 2) - (elementWidth / 2);
24 |
25 | $(this).css({
26 | position: 'absolute',
27 | top: top,
28 | left: left
29 | });
30 | });
31 | };
32 |
33 | $.fn.isInner = function(target) {
34 | if (!target) {
35 | return false;
36 | }
37 |
38 | var targetElementOffset = target.offset();
39 | var srcElementOffset = $(this).offset();
40 |
41 | var outerBoxPosition = {};
42 | outerBoxPosition.top = targetElementOffset.top;
43 | outerBoxPosition.left = targetElementOffset.left;
44 | outerBoxPosition.bottom = outerBoxPosition.top + target.height();
45 | outerBoxPosition.right = outerBoxPosition.left + target.width();
46 |
47 | return outerBoxPosition.top <= srcElementOffset.top &&
48 | outerBoxPosition.left <= srcElementOffset.left &&
49 | outerBoxPosition.bottom >= srcElementOffset.top &&
50 | outerBoxPosition.right >= srcElementOffset.left;
51 | };
52 |
53 | $.fn.tag = function() {
54 | return this.get(0).tagName.toLowerCase();
55 | };
56 |
57 | $.extend($.expr.filters, {
58 | clickable: function(element) {
59 | return (/^(select|a|area)$/i.test(element.nodeName) || $.css(element, 'cursor') === 'pointer') &&
60 | element.innerHTML !== '';
61 | },
62 | submittable: function(element) {
63 | return element.nodeName.toLowerCase() === 'input' &&
64 | /^(submit|reset|image|radio)$/.test(element.type);
65 | },
66 | visualable: function(element) {
67 | return /^(div|section|th|td|header)$/i.test(element.nodeName);
68 | },
69 | insertable: function(element) {
70 | var isInput = element.nodeName.toLowerCase() === 'input' && /^(text|password|tel)$/.test(element.type);
71 | var isTextarea = element.nodeName.toLowerCase() === 'textarea'
72 | var isContentEditable = element.getAttribute('contenteditable') === 'true'
73 |
74 | return isInput || isTextarea || isContentEditable;
75 | },
76 | screen: function(element) {
77 | var docElem = element.ownerDocument.documentElement;
78 |
79 | if (!jQuery.contains(docElem, element)) {
80 | return false;
81 | }
82 |
83 | var box = element.getBoundingClientRect();
84 | var screenOffsetTop = scrollingArea.scrollTop;
85 | var screenOffsetBottom = screenOffsetTop + window.innerHeight;
86 | var elementOffsetTop = box.top + window.pageYOffset - docElem.clientTop;
87 | var elementOffsetBottom = elementOffsetTop + element.offsetHeight;
88 |
89 | return $.expr.filters.visible(element) &&
90 | $.css(element, 'visibility') !== 'hidden' &&
91 | elementOffsetBottom >= screenOffsetTop &&
92 | screenOffsetBottom >= elementOffsetTop;
93 | }
94 | });
95 | })(jQuery);
96 |
--------------------------------------------------------------------------------
/js/filter.js:
--------------------------------------------------------------------------------
1 | function filterClosedTabs(tabs, text)
2 | {
3 | var homedics = new Homedics(text);
4 | var list = [];
5 |
6 | for (var i = 0; i < tabs.length; i++) {
7 | var tab = tabs[i];
8 |
9 | if (tab) {
10 | var urlMatched = homedics.match(tab.url);
11 | var titleMatched = homedics.match(tab.title);
12 |
13 | if (text == '' || urlMatched.matched || titleMatched.matched) {
14 | list.push({
15 | text: tab.title + '(' + tab.url + ')',
16 | url: tab.url,
17 | tabId: tab.id,
18 | highlights: (urlMatched.matches || []).concat(titleMatched.matches || [])
19 | });
20 | }
21 | }
22 | }
23 |
24 | return list;
25 | }
26 |
27 | function filterBookmarks(bookmarks, text)
28 | {
29 | var homedics = new Homedics(text);
30 | var list = [];
31 |
32 | for (var i = 0; i < bookmarks.length; i++) {
33 | var bookmark = bookmarks[i];
34 | var urlMatched = homedics.match(bookmark.url);
35 | var titleMatched = homedics.match(bookmark.title);
36 |
37 | if (text == '' || urlMatched.matched || titleMatched.matched) {
38 | list.push({
39 | text: bookmark.title + '(' + bookmark.url + ')',
40 | url: bookmark.url,
41 | icon: bookmark.faviconDataUrl,
42 | highlights: (urlMatched.matches || []).concat(titleMatched.matches || [])
43 | });
44 | }
45 | };
46 |
47 | return list;
48 | }
49 |
50 | function filterHistories(histories, text)
51 | {
52 | var homedics = new Homedics(text);
53 | var list = [];
54 |
55 | for (var i = histories.length - 1; i > -1; i--) {
56 | var history = histories[i];
57 | var urlMatched = homedics.match(history.url);
58 | var titleMatched = homedics.match(history.title);
59 |
60 | if (text == '' || urlMatched.matched || titleMatched.matched) {
61 | list.push({
62 | text: history.title + '(' + history.url + ')',
63 | url: history.url,
64 | icon: history.faviconDataUrl,
65 | highlights: (urlMatched.matches || []).concat(titleMatched.matches || [])
66 | });
67 | }
68 | }
69 |
70 | return list;
71 | }
72 |
73 | function filterApplications(apps, text)
74 | {
75 | var homedics = new Homedics(text);
76 | var list = [];
77 |
78 | for (var i = 0; i < apps.length; i++) {
79 | var app = apps[i];
80 | var nameMatched = homedics.match(app.name);
81 | var urlMatched = homedics.match(app.appLaunchUrl);
82 |
83 | if (text == '' || urlMatched.matched || nameMatched.matched) {
84 | list.push({
85 | text: app.appLaunchUrl ? app.name + '(' + app.appLaunchUrl + ')' : app.name,
86 | url: app.appLaunchUrl,
87 | id: app.id,
88 | icon: app.faviconDataUrl,
89 | highlights: (urlMatched.matches || []).concat(nameMatched.matches || [])
90 | });
91 | }
92 | }
93 |
94 | return list;
95 | }
96 |
97 | function filterTabs(tabs, text)
98 | {
99 | var homedics = new Homedics(text);
100 | var list = [];
101 |
102 | for (var i = 0; i < tabs.length; i++) {
103 | var tab = tabs[i];
104 | var urlMatched = homedics.match(tab.url);
105 | var titleMatched = homedics.match(tab.title);
106 |
107 | if (text == '' || urlMatched.matched || titleMatched.matched) {
108 | var char = '' + Opt.tab_selection_hint_keys.charAt(i) + ' ';
109 | list.push({
110 | escape: false,
111 | text: ' - ' + char + Dom.escapeHTML(tab.title + '(' + tab.url + ')'),
112 | url: tab.url,
113 | icon: tab.faviconDataUrl,
114 | id: tab.id,
115 | highlights: (urlMatched.matches || []).concat(titleMatched.matches || [])
116 | });
117 | }
118 | }
119 |
120 | return list;
121 | }
122 |
--------------------------------------------------------------------------------
/options/main.js:
--------------------------------------------------------------------------------
1 | $(document).ready(function() {
2 | translate();
3 | HometypeOptions.getInstance().load(load);
4 |
5 | $('.js-help').click(showHelp);
6 | $('.js-menu-section').click(switchSection);
7 | $('#save').click(saveOptions);
8 | $(':radio[name="hint_key_algorithm"]').click(controlCustomHintKeyField);
9 | });
10 |
11 | function translate()
12 | {
13 | $('.js-translate').each(function() {
14 | var el = $(this);
15 | el.html(chrome.i18n.getMessage(el.data('translate')));
16 | });
17 | }
18 |
19 | function load(options)
20 | {
21 | $.each(options, function(key, value) {
22 | var element = $("input[name='" + key + "']");
23 | if (element.length > 0) {
24 | if (element.is(':radio')) {
25 | element.val([ value ]);
26 | } else if (element.is(':checkbox')) {
27 | element.prop('checked', value);
28 | } else {
29 | element.val(value);
30 | }
31 | }
32 | });
33 |
34 | controlCustomHintKeyField();
35 |
36 | var bindList = '';
37 | $.each(options.key_bind, function(map, bind) {
38 | $.each(bind, function(key, command) {
39 | bindList += map + ' ' + key + ' ' + command + "\r\n";
40 | });
41 | bindList += "\r\n";
42 | });
43 | $('#key_bind_list').val(bindList);
44 |
45 | var ignoreList = '';
46 | $.each(options.ignore_urls, function(index, url) {
47 | ignoreList += url + "\r\n";
48 | });
49 | $('#ignore_url_list').val(ignoreList);
50 | }
51 |
52 | function showHelp()
53 | {
54 | var name = $(this).data('name');
55 | var title = $('#title-for-' + name).text();
56 | var desc = $('#desc-for-' + name).html();
57 | var oldDesc = $($('#description-area-content').children()[0]);
58 | var newDesc = $('').html(desc);
59 |
60 | $('#description-area-content').append(newDesc);
61 | $('#description-area-title').text(title);
62 | $('#description-area').show();
63 | oldDesc.animate({ height: 0 }, 500, function() {
64 | $(this).remove();
65 | $('#description-area-content').animate({ height: newDesc.outerHeight() }, 1000);
66 | });
67 | }
68 |
69 | function switchSection()
70 | {
71 | var ref = $(this).data('ref');
72 |
73 | $('.js-menu-section').removeClass('active-menu-section');
74 | $(this).addClass('active-menu-section');
75 | $('.js-option-panel').hide();
76 | $('#' + ref).show();
77 | }
78 |
79 | function saveOptions()
80 | {
81 | var params = {};
82 |
83 | $(':text, :radio:checked').each(function() {
84 | params[$(this).attr('name')] = $(this).val();
85 | });
86 | $(':checkbox').each(function() {
87 | params[$(this).attr('name')] = $(this).prop('checked');
88 | });
89 |
90 | var bindList = $('#key_bind_list').val();
91 | params.key_bind = {};
92 | $.each(bindList.split(/\r\n|\r|\n/), function(index, line) {
93 | if (line == '') {
94 | return true;
95 | }
96 |
97 | value = line.split(' ');
98 | if (!params.key_bind[value[0]]) {
99 | params.key_bind[value[0]] = {};
100 | }
101 |
102 | params.key_bind[value[0]][value[1]] = value[2];
103 | });
104 |
105 | var ignoreList = $('#ignore_url_list').val();
106 | params.ignore_urls = [];
107 | $.each(ignoreList.split(/\r\n|\r|\n/), function(index, line) {
108 | if (line == '') {
109 | return true;
110 | }
111 |
112 | params.ignore_urls.push(line);
113 | });
114 |
115 | chrome.storage.sync.set({ 'options': params }, function() {
116 | $('#status').text('Saved!');
117 | });
118 | }
119 |
120 | function controlCustomHintKeyField()
121 | {
122 | var el = $('#custom_hint_keys');
123 | var checked = !$('#hint_key_custom').prop('checked');
124 | el.prop('disabled', checked);
125 |
126 | if (checked) {
127 | el.addClass('disabled');
128 | } else {
129 | el.removeClass('disabled');
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/js/main.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2013 Kengo Tateishi (@tkengo)
3 | * Licensed under MIT license.
4 | * http://www.opensource.org/licenses/mit-license.php
5 | *
6 | * Entry point in Hometype.
7 | */
8 |
9 | // Load options and initialize Hometype
10 | HometypeOptions.getInstance().load(initialize);
11 | HometypeOptions.getInstance().onLoaded(bindCommand);
12 |
13 | /**
14 | * Initialize Hometype
15 | *
16 | * 1. Check whether if the current url is matched in ignore url list.
17 | * 2. Bind a command.
18 | * 3. Register event onProcess in KyeSequence object.
19 | */
20 | function initialize(options)
21 | {
22 | // If current url is included in ignore url list, do nothing.
23 | if (isIgnored(options.ignore_urls)) {
24 | return;
25 | }
26 |
27 | bindCommand(options);
28 |
29 | // Set an event listener to the key sequence object when options have loaded.
30 | var key = new KeySequence();
31 | key.onProcess(function (e, sequence, stack, currentKey) {
32 | // Adjust the current mode before key processing.
33 | adjustCurrentMode();
34 |
35 | var processor = Mode.getProcessor();
36 | var delegation = 'onKeyDown' in processor;
37 | var executer = new Executer(Mode.getCurrentMode(), sequence);
38 | var stopPropagation = (executer.hasCandidates() || delegation) && !Dom.isEditable(document.activeElement);
39 |
40 | // Execute a command and reset key sequence.
41 | // Delegate process to the processor of the current mode if a command was not found.
42 | if (executer.noCandidate()) {
43 | if (delegation && processor.onKeyDown(stack, currentKey, e)) {
44 | this.reset();
45 | }
46 | } else if (executer.execute()) {
47 | this.reset();
48 | }
49 |
50 | if (stopPropagation) {
51 | e.stopPropagation();
52 | e.preventDefault();
53 | }
54 | });
55 |
56 | $(document).ready(function() {
57 | chrome.runtime.sendMessage({ command: 'getContinuousState' }, function(status) {
58 | if (status) {
59 | new Executer('followLink --continuous').execute();
60 | }
61 | });
62 | });
63 | }
64 |
65 | /**
66 | * Adjust the current mode.
67 | *
68 | * 1. If active element is editable and the current mode is the normal mode,
69 | * change the current mode to the insert mode.
70 | * 2. If active element is not editable and the current mode is the insert mode,
71 | * change the current mode to the normal mode.
72 | */
73 | function adjustCurrentMode()
74 | {
75 | var isEditable = Dom.isEditable(document.activeElement);
76 | if (isEditable && Mode.isNormalMode()) {
77 | Mode.changeMode(ModeList.INSERT_MODE);
78 | }
79 | if (!isEditable && Mode.isInsertMode()) {
80 | Mode.changeMode(ModeList.NORMAL_MODE);
81 | }
82 | }
83 |
84 | /**
85 | * bind a command to a key.
86 | */
87 | function bindCommand(options)
88 | {
89 | KeyMap.clear();
90 |
91 | $.each($.extend(true, {}, options.default_key_bind, options.key_bind), function(map, bind) {
92 | $.each(bind, function(key, command) {
93 | KeyMap[map](key, command);
94 | });
95 | });
96 | }
97 |
98 | /**
99 | * Check ignore urls
100 | */
101 | function isIgnored(ignore_urls)
102 | {
103 | var currentUrl = window.location.href.replace(/\/$/, '');
104 | for (var i in ignore_urls) {
105 | var ignoreUrl = ignore_urls[i];
106 |
107 | if (ignoreUrl.substr(0, 1) == '"' && ignoreUrl.substr(-1, 1) == '"') {
108 | if (currentUrl == ignoreUrl.replace(/"/g, '').replace(/\/$/, '')) {
109 | return true;
110 | }
111 | }
112 | else {
113 | ignoreUrl = ignoreUrl.replace(/\*/g, '.*');
114 | var regexp = new RegExp(ignoreUrl);
115 |
116 | if (regexp.test(currentUrl)) {
117 | return true;
118 | }
119 | }
120 | }
121 | return false;
122 | }
123 |
--------------------------------------------------------------------------------
/options/main.css:
--------------------------------------------------------------------------------
1 | html, body, input, h1, h2 {
2 | margin:0;
3 | padding:0;
4 | font-family: 'Open Sans', sans-serif;
5 | }
6 |
7 | body {
8 | font-size: 14px;
9 | color: #666;
10 | }
11 | h1 {
12 | font-size: 20px;
13 | }
14 | hr {
15 | border-style: dashed;
16 | border-width: 0 0 1px 0;
17 | border-color: #ccc;
18 | }
19 | code, .code {
20 | padding:1px 5px;
21 | font-size:12px;
22 | font-weight:normal;
23 | font-family:Consolas, "Liberation Mono", Courier, monospace;
24 | border:1px solid #ddd;
25 | background-color:#f8f8f8;
26 | border-radius:3px;
27 | line-height:150%;
28 | }
29 | a {
30 | text-decoration: none;
31 | }
32 | a, a:visited {
33 | color: #4183c4;
34 | }
35 | a:hover {
36 | text-decoration: underline;
37 | }
38 |
39 | .container {
40 | width:1020px;
41 | margin:20px 20px;
42 | }
43 |
44 | .description-area {
45 | float:right;
46 | width:500px;
47 | margin:10px 0 0 0;
48 | display:none;
49 | background-color:#fff;
50 | }
51 | .description-area-inner {
52 | border:solid 1px #ccc;
53 | position:relative;
54 | padding:15px 10px 10px 10px;
55 | border-radius:4px;
56 | }
57 | .description-area-title {
58 | position:absolute;
59 | top:-10px;
60 | background-color:#fff;
61 | padding:0 10px;
62 | font-weight: 700;
63 | }
64 | .description-area-content {
65 | line-height:160%;
66 | overflow:hidden;
67 | }
68 |
69 | .category {
70 | border-style: dashed;
71 | border-width: 0 0 1px 0;
72 | border-color: #ccc;
73 | width: 500px;
74 | padding: 10px 0;
75 | }
76 | .category:last-child {
77 | border-width: 0;
78 | }
79 | .category-header {
80 | width: 100px;
81 | font-size: 16px;
82 | font-weight: 700;
83 | float: left;
84 | }
85 | .category-content {
86 | margin: 0 0 0 100px;
87 | }
88 |
89 | .section {
90 | font-size:18px;
91 | }
92 |
93 | .menu-section {
94 | background-color:#fff;
95 | color:#444;
96 | height:36px;
97 | }
98 | .menu-section ul {
99 | list-style:none;
100 | margin:0;
101 | padding:0;
102 | }
103 | .menu-section li {
104 | list-style:none;
105 | float:left;
106 | width:150px;
107 | height:26px;
108 | line-height:26px;
109 | text-align:center;
110 | display:block;
111 | cursor:pointer;
112 | padding:5px;
113 |
114 | }
115 | .active-menu-section {
116 | box-shadow: #525252 0px -2px inset;
117 | }
118 |
119 | .header {
120 | background-color: #62a609;
121 | color: #f7e8ad;
122 | font-family: 'Open Sans', sans-serif;
123 | padding: 12px;
124 | }
125 | .header h1 {
126 | font-weight:100;
127 | }
128 |
129 | input, textarea {
130 | border: solid 1px #aaa;
131 | padding: 4px;
132 | border-radius: 3px;
133 | }
134 | textarea {
135 | font-family: Monaco, Consolas, 'Courier New', Courier, monospace;
136 | font-size: 10px;
137 | }
138 |
139 | dt {
140 | float: left;
141 | width: 160px;
142 | position: relative;
143 | }
144 | dd {
145 | margin-left: 160px;
146 | }
147 | dt, dd {
148 | line-height: 25px;
149 | margin-bottom: 10px;
150 | }
151 | .help-wrapper {
152 | position: relative;
153 | }
154 | .help {
155 | width: 16px;
156 | height: 16px;
157 | position: absolute;
158 | margin-left: 3px;
159 | font-size: 12px;
160 | top: -4px;
161 | color: #018b06;
162 | font-weight: 400;
163 | cursor: pointer;
164 | }
165 | .multi-selection {
166 | border-style: solid;
167 | border-width: 0 0 1px 0;
168 | border-color: #ccc;
169 | }
170 | .option-description {
171 | display: none;
172 | }
173 | .disabled {
174 | color: #bbb;
175 | background-color: #fafafa;
176 | }
177 |
178 | #key_bind_list {
179 | width: 396px;
180 | height: 551px;
181 | }
182 |
183 | #ignore_url_list {
184 | width: 396px;
185 | height: 100px;
186 | }
187 |
188 | #key-bindings {
189 | display: none;
190 | }
191 |
192 | .clearfix {
193 | *zoom: 1;
194 |
195 | &:before, &:after {
196 | content: " ";
197 | display: table;
198 | }
199 |
200 | &:after {
201 | clear: both;
202 | }
203 | }
204 |
--------------------------------------------------------------------------------
/js/background/tab.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2013 Kengo Tateishi (@tkengo)
3 | * Licensed under MIT license.
4 | * http://www.opensource.org/licenses/mit-license.php
5 | *
6 | * Managed tabs.
7 | */
8 | var HometypeTab = function() {
9 | this.tabs = {};
10 | this.history = new HometypeHistory();
11 | this.closedTabStacks = JSON.parse(localStorage.getItem('closedTabs')) || [];
12 |
13 | this.loadAllTabs();
14 | chrome.tabs.onCreated.addListener($.proxy(this.createAction, this));
15 | chrome.tabs.onRemoved.addListener($.proxy(this.removeAction, this));
16 | chrome.tabs.onUpdated.addListener($.proxy(this.updateAction, this));
17 | };
18 |
19 | /**
20 | * Load all tabs of current window for this object.
21 | */
22 | HometypeTab.prototype.loadAllTabs = function() {
23 | var context = this;
24 | chrome.windows.getCurrent({ populate: true }, function(window) {
25 | for (var i in window.tabs) {
26 | var tab = window.tabs[i];
27 | context.tabs[tab.id] = tab;
28 | }
29 | });
30 | };
31 |
32 | /**
33 | * The method that is invoked when a new tab is created.
34 | * The tab is saved to this object, and set its first history.
35 | *
36 | * @param integer tab A new tab object.
37 | */
38 | HometypeTab.prototype.createAction = function(tab) {
39 | this.tabs[tab.id] = tab;
40 | this.history.set(tab.id, [ { url: tab.url, title: tab.title } ]);
41 | };
42 |
43 | /**
44 | * The method that is invoked when a tab is removed.
45 | * The tab is saved to this object as closed tab, and remove
46 | * from its history. Last, it is deleted from this object.
47 | *
48 | * @param integer tab Removed tab id.
49 | */
50 | HometypeTab.prototype.removeAction = function(tabId) {
51 | this.closedTabStacks.unshift(this.tabs[tabId]);
52 | if (this.closedTabStacks.length > 20) {
53 | this.closedTabStacks.pop();
54 | }
55 | localStorage.setItem('closedTabs', JSON.stringify(this.closedTabStacks));
56 |
57 | this.history.remove(tabId);
58 | delete this.tabs[tabId];
59 | };
60 |
61 | /**
62 | * The method that is invoked when a tab is updated.
63 | *
64 | * @param integer tabId Updated tab id.
65 | * @param object changeInfo Lists the changes to the state of the tab that was updated.
66 | * @param integer tab Updated tab.
67 | */
68 | HometypeTab.prototype.updateAction = function(tabId, changeInfo, tab) {
69 | this.tabs[tabId] = tab;
70 |
71 | var history = this.history.get(tabId);
72 | if (changeInfo.url) {
73 | this.history.push(tabId, changeInfo.url);
74 | }
75 | else {
76 | this.history.update(tabId, tab.title);
77 | }
78 | };
79 |
80 | /**
81 | * Get histories.
82 | *
83 | * @param integer tabId The tab's id you want to get histories.
84 | * @return array tab's histories.
85 | */
86 | HometypeTab.prototype.getHistories = function(tabId) {
87 | return this.history.get(tabId);
88 | };
89 |
90 | /**
91 | * Open recent closed tab.
92 | *
93 | * @param integer tabId The tab's id
94 | */
95 | HometypeTab.prototype.openClosedTab = function(tabId) {
96 | if (this.closedTabStacks.length == 0) {
97 | return;
98 | }
99 |
100 | var tab = null;
101 | if (tabId) {
102 | for (var i in this.closedTabStacks) {
103 | tab = this.closedTabStacks[i];
104 | if (tab.id == tabId) {
105 | this.closedTabStacks.splice(i, 1);
106 | break;
107 | }
108 | }
109 | }
110 | else {
111 | tab = this.closedTabStacks.shift();
112 | }
113 | localStorage.setItem('closedTabs', JSON.stringify(this.closedTabStacks));
114 |
115 | var params = {
116 | windowId: tab.windowId,
117 | index: tab.index,
118 | url: tab.url,
119 | pinned: tab.pinned,
120 | openerTabId: tab.openerTabId
121 | };
122 | chrome.tabs.create(params);
123 | };
124 |
125 | /**
126 | * Get closed tab list.
127 | *
128 | * @return array closed tab list.
129 | */
130 | HometypeTab.prototype.getClosedTabList = function() {
131 | return this.closedTabStacks;
132 | };
133 |
134 | var Tab = new HometypeTab();
135 |
--------------------------------------------------------------------------------
/js/executer.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2013 Kengo Tateishi (@tkengo)
3 | * Licensed under MIT license.
4 | * http://www.opensource.org/licenses/mit-license.php
5 | *
6 | * Hometype command executer.
7 | */
8 |
9 | /**
10 | * Create Executer instance with a command name or a binding key, and then
11 | * invoke 'execute' method to execute the command.
12 | *
13 | * How to specify a command:
14 | * 1. Only command name 'commandName'
15 | * 2. With arguments 'commandName --hoge --moge'
16 | * 3. Take over the option from previous execution '@commandName'
17 | */
18 | var Executer = (function() {
19 | /**
20 | * Store option of previous execution to this property.
21 | */
22 | var previousOptions = {};
23 |
24 | /**
25 | * Constructor.
26 | *
27 | * If arguments are passed the current mode and key, Executer searches
28 | * a command from related them, and set a command candidate.
29 | *
30 | * If arguments are passed only a command name, Executer set it to
31 | * a command candidate.
32 | *
33 | * And 'execute' method in this object will invoke frmo a command candidate.
34 | * If it has only one candidate, the command is executed. Otherwise, not.
35 | *
36 | * @param string mode The current mode or a command name.
37 | * @param string key Key.
38 | */
39 | var constructor = function(mode, key) {
40 | if (!key) {
41 | key = '__undefined__';
42 | var command = mode;
43 | }
44 |
45 | this.key = key;
46 | this.candidates = [];
47 |
48 | if (key == '__undefined__') {
49 | this.candidates.push({ command: command, key: key });
50 | } else {
51 | var commands = KeyMap.assignedCommands();
52 | var maps = commands[mode];
53 | for (keyMap in maps) {
54 | if (keyMap.indexOf(key) == 0) {
55 | this.candidates.push({ command: maps[keyMap], key: keyMap });
56 | }
57 | }
58 | }
59 | };
60 |
61 | /**
62 | * Store or get a previous options.
63 | *
64 | * If args is omitted, get options.
65 | * If args is passed, set options.
66 | */
67 | constructor.previousOptions = function(command, args) {
68 | if (args) {
69 | previousOptions[command] = args || {};
70 | } else {
71 | return previousOptions[command] || {};
72 | }
73 | };
74 |
75 | return constructor;
76 | })();
77 |
78 | /**
79 | * Check if whether this object has a command that should be executed.
80 | */
81 | Executer.prototype.noCandidate = function() {
82 | return this.candidates.length == 0;
83 | };
84 | Executer.prototype.hasCandidates = function() {
85 | return !this.noCandidate();
86 | };
87 |
88 | /**
89 | * Check if whether a command that should be executed is fixed.
90 | */
91 | Executer.prototype.fixedCandidate = function() {
92 | return this.candidates.length == 1 && this.candidates[0].key == this.key;
93 | };
94 |
95 | /**
96 | * Execute a command.
97 | */
98 | Executer.prototype.execute = function() {
99 | if (!this.fixedCandidate()) {
100 | return false;
101 | }
102 |
103 | var commands = [],
104 | map = this.candidates[0].command.split(' ');
105 |
106 | for (var i = 0; i < map.length; i++) {
107 | if (map[i].substr(0, 2) != '--') {
108 | var command = map[i];
109 | var previouse = false;
110 | if (command.substr(0, 1) == '@') {
111 | previouse = true;
112 | command = command.substr(1);
113 | }
114 |
115 | if (Command[command]) {
116 | commands.push({ command: command, args: {}, previouse: previouse });
117 | } else {
118 | return false;
119 | }
120 | } else {
121 | commands[commands.length - 1].args[map[i].replace('--', '')] = true;
122 | }
123 | }
124 |
125 | for (var i = 0; i < commands.length; i++) {
126 | var command = commands[i].command,
127 | args = commands[i].previouse ? Executer.previousOptions(command) : commands[i].args;
128 |
129 | Command[command](args);
130 |
131 | Executer.previousOptions(command, args);
132 | }
133 |
134 | return true;
135 | };
136 |
--------------------------------------------------------------------------------
/js/key/sequence.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Manage key.
3 | *
4 | * Register callback method to onProcess in KeySequence class, and
5 | * it is called every keyboard push with below parameters.
6 | *
7 | * function callback(e, sequence, stack, key);
8 | * e : Keydown event.
9 | * sequence : A string that concat pushed key from keyboard in specified time.
10 | * At first, if 'a' was pushed, this argument is 'a'.
11 | * Second, if 'b' was pushed in specified time, this argument is 'ab'.
12 | * Third, if 'c' was pushed out of specified time, this argument is 'c'.
13 | * stack : A string that concat pushed key from keyboard.
14 | * Difference of stack between sequence is that this argument is not reset
15 | * in specified time. If you want to reset this argument, you can call
16 | * 'reset' method.
17 | * key : A pushed key character.
18 | */
19 | var KeySequence = function() {
20 | this.keySequece = '';
21 | this.keyStack = '';
22 | this.callbacks = [];
23 | this.resetkeySequeceTimerId = -1;
24 |
25 | var _this = this;
26 | document.addEventListener('keydown', function(e) {
27 | _this.processor(e);
28 | }, true);
29 | };
30 |
31 | /**
32 | * Register callback method called when key was pushed.
33 | *
34 | * @param function callback Callback method.
35 | */
36 | KeySequence.prototype.onProcess = function(callback) {
37 | this.callbacks.push(callback);
38 | };
39 |
40 | /**
41 | * Process key pushing.
42 | */
43 | KeySequence.prototype.processor = function(e) {
44 | // Return if pushed key is only meta key.
45 | var id = e.key;
46 | if (id == 'Control' || id == 'Shift' || id == 'Alt' || id == 'Meta') {
47 | return false;
48 | }
49 |
50 | // Reset a key input waiting timer.
51 | this.resetTimerForResetKeySequence();
52 |
53 | // Add pushed key to key sequence.
54 | var key = this.getKeyChar(e);
55 | this.keySequece += key;
56 | this.keyStack += key;
57 |
58 | // Invoke callback method.
59 | for (var i in this.callbacks) {
60 | var callback = this.callbacks[i];
61 | if (typeof callback == 'function') {
62 | callback.call(this, e, this.keySequece, this.keyStack, key);
63 | }
64 | }
65 |
66 | this.setTimerForResetKeySequence(Opt.command_interval);
67 | };
68 |
69 | /**
70 | * Get a string that is translated for key mapping. For example:
71 | * If 'a' was pushed with Ctrl, return ' '
72 | * If 'a' was pushed with Shift, return 'A'
73 | * If 'a' was pushed with Command, return ''
74 | *
75 | * @return string A key mapping string.
76 | */
77 | KeySequence.prototype.getKeyChar = function(e) {
78 | var key = e.key;
79 | switch (key) {
80 | case 'BackSpace': return '';
81 | case 'Tab': return '';
82 | case 'Cancel': return '';
83 | case 'Escape': return '';
84 | case ' ': return '';
85 | case 'Delete': return '';
86 | }
87 |
88 | if (e.shiftKey) {
89 | key = key.toUpperCase();
90 | }
91 |
92 | if (e.metaKey) {
93 | key = '';
94 | }
95 | else if (e.ctrlKey) {
96 | key = '';
97 | }
98 |
99 | return key;
100 | };
101 |
102 | /**
103 | * Reset key sequence.
104 | */
105 | KeySequence.prototype.reset = function() {
106 | this.keySequece = '';
107 | this.keyStack = '';
108 | this.resetTimerForResetKeySequence();
109 | };
110 |
111 | /**
112 | * Set a timer to wait a next key.
113 | *
114 | * Invoke callback method to confirm key push if there is no key push between
115 | * argument 'interval' time.
116 | *
117 | * @param integer interval Interval time.
118 | */
119 | KeySequence.prototype.setTimerForResetKeySequence = function(interval) {
120 | this.resetkeySequeceTimerId = setTimeout($.proxy(function() {
121 | this.keySequece = '';
122 | this.resetTimerForResetKeySequence();
123 | }, this), interval);
124 | };
125 |
126 | /**
127 | * Reset a timer.
128 | */
129 | KeySequence.prototype.resetTimerForResetKeySequence = function() {
130 | clearTimeout(this.resetkeySequeceTimerId);
131 | };
132 |
--------------------------------------------------------------------------------
/js/hint/element.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2013 Kengo Tateishi (@tkengo)
3 | * Licensed under MIT license.
4 | * http://www.opensource.org/licenses/mit-license.php
5 | *
6 | * Hint element.
7 | */
8 |
9 | /**
10 | * Constructor
11 | *
12 | * @param element srcElement A source element of this hint element.
13 | * @param integer index Hint element index.
14 | * @param array key Hint key.
15 | * @param string hintTheme Hint theme.
16 | */
17 | var HintElement = function(srcElement, index, key, hintTheme) {
18 | this.className = 'hometype-hit-a-hint-' + hintTheme;
19 | this.index = index;
20 | this.key = key;
21 | this.matchedIndex = 0;
22 |
23 | this.srcElement = srcElement;
24 | this.srcElement.className ?
25 | this.srcElement.className += ' ' + this.className + '-area' :
26 | this.srcElement.className = this.className + '-area';
27 |
28 | this.tipElement = this.createTipElement();
29 | };
30 |
31 | /**
32 | * Create a hint tip element.
33 | * Calculate a hint tip position to show in left side of a source element.
34 | */
35 | HintElement.prototype.createTipElement = function() {
36 | var top = 0, left = 0;
37 | var rect = this.srcElement.getClientRects()[0];
38 |
39 | if (this.srcElement.tagName == 'AREA') {
40 | // Get a position from coords attribute if an element is a clickable map.
41 | var coords = this.srcElement.coords.split(',');
42 | rect = this.srcElement.parentNode.getClientRects()[0];
43 |
44 | top = rect.top + parseInt(coords[1]);
45 | left = rect.left + parseInt(coords[0]);
46 | }
47 | else {
48 | // Usually get a position from an element offset.
49 | top = rect.top;
50 | left = rect.left;
51 | }
52 |
53 | // Correct an element position if it is out of display.
54 | if (top < 0) {
55 | top = 0;
56 | }
57 | if (left < 0) {
58 | left = 0;
59 | }
60 |
61 | // Set hint keys to a hint tip element.
62 | var tipHtml = '';
63 | for (var i in this.key) {
64 | tipHtml += '' + this.key[i] + '';
65 | }
66 | this.elementId = 'hometype-hit-a-hint-element-' + this.key;
67 |
68 | var tip = document.createElement('div');
69 | tip.className = this.className + ' hometype-hit-a-hint-base';
70 | tip.clickableItem = this.srcElement;
71 | tip.style.left = left + window.scrollX + 'px';
72 | tip.style.top = top + window.scrollY + 'px';
73 | tip.rect = rect;
74 | tip.innerHTML = tipHtml;
75 | tip.id = this.elementId;
76 |
77 | return tip;
78 | };
79 |
80 | /**
81 | * Get the source element.
82 | */
83 | HintElement.prototype.getElement = function() {
84 | return this.srcElement;
85 | };
86 |
87 | /**
88 | * Get the tip element.
89 | * Use this if it is not still rendered to DOM.
90 | */
91 | HintElement.prototype.getTipElement = function() {
92 | return this.tipElement;
93 | };
94 |
95 | /**
96 | * Get the hint keys.
97 | */
98 | HintElement.prototype.getKey = function() {
99 | return this.key;
100 | };
101 |
102 | /**
103 | * Pushed first hint key.
104 | */
105 | HintElement.prototype.setPushed = function() {
106 | this.getTipElement().children[this.matchedIndex++].className = 'hometype-hit-a-hint-pushed';
107 | };
108 |
109 | HintElement.prototype.highlight = function(targets) {
110 | if (typeof targets == 'string') {
111 | targets = [ targets ];
112 | }
113 |
114 | for (var i = 0; i < targets.length; i++) {
115 | Dom.highlight(this.getElement(), targets[i], { ignoreCase: true });
116 | }
117 | };
118 |
119 | HintElement.prototype.removeHighlight = function() {
120 | Dom.removeHighlight(this.getElement());
121 | };
122 |
123 | /**
124 | * Remove hint tip element.
125 | */
126 | HintElement.prototype.removeHintTip = function() {
127 | var element = this.getElement();
128 |
129 | element.className = element.className.replace(this.className + '-area', '');
130 | element.className = element.className.replace('hometype-hit-a-hint-head-area', '');
131 |
132 | var tip = this.getTipElement();
133 | if (tip.parentNode) {
134 | tip.parentNode.removeChild(tip);
135 | }
136 |
137 | this.removeHighlight();
138 | };
139 |
--------------------------------------------------------------------------------
/js/viewport.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2013 Kengo Tateishi (@tkengo)
3 | * Licensed under MIT license.
4 | * http://www.opensource.org/licenses/mit-license.php
5 | *
6 | * Screen object.
7 | */
8 | var HometypeScreen = function() {
9 | };
10 |
11 | /**
12 | * Get the top DOM element which can be scrolled.
13 | * Referred to https://dev.opera.com/articles/fixing-the-scrolltop-bug/
14 | *
15 | * @return (HTMLElement | null) document.scrollingElement or document.body. It depends on an environment.
16 | */
17 | HometypeScreen.prototype.scrollingArea = function() {
18 | if ('scrollingElement' in document) {
19 | return document.scrollingElement;
20 | // Fallback for legacy browsers
21 | } else if (navigator.userAgent.indexOf('WebKit') != -1) {
22 | return document.body;
23 | }
24 |
25 | return document.documentElement;
26 | }();
27 |
28 | /**
29 | * Get the current scroll position.
30 | *
31 | * @return Object A hash that has top and left key.
32 | */
33 | HometypeScreen.prototype.getScrollPosition = function() {
34 | return {
35 | top: this.scrollingArea.scrollTop,
36 | left: this.scrollingArea.scrollLeft
37 | };
38 | };
39 |
40 | /**
41 | * Get the current window size.
42 | *
43 | * @return Object A hash that has width and height key.
44 | */
45 | HometypeScreen.prototype.getWindowSize = function() {
46 | return {
47 | width: window.innerWidth,
48 | height: window.innerHeight
49 | };
50 | };
51 |
52 | /**
53 | * Get the current document size.
54 | *
55 | * @return Object A hash that has width and height key.
56 | */
57 | HometypeScreen.prototype.getDocumentSize = function() {
58 | return {
59 | width: $(document).width(),
60 | height: $(document).height()
61 | };
62 | };
63 |
64 | /**
65 | * Scroll to specified position.
66 | *
67 | * @param integer x horizontal position.
68 | * @param integer y vertical position.
69 | */
70 | HometypeScreen.prototype.scrollTo = function(x, y) {
71 | this.scrollingArea.scrollTop = y;
72 | this.scrollingArea.scrollLeft = x;
73 | };
74 |
75 | /**
76 | * Scroll to specified position for vertical direction.
77 | *
78 | * @param integer value Scroll amount.
79 | */
80 | HometypeScreen.prototype.scrollVertical = function(value) {
81 | var pos = this.getScrollPosition();
82 | this.scrollTo(pos.left, pos.top + value);
83 | };
84 |
85 | /**
86 | * Scroll to specified position for horizontal direction.
87 | *
88 | * @param integer value Scroll amount.
89 | */
90 | HometypeScreen.prototype.scrollHorizontal = function(value) {
91 | var pos = this.getScrollPosition();
92 | this.scrollTo(pos.left - value, pos.top);
93 | };
94 |
95 | HometypeScreen.prototype.setContentEditable = function(element) {
96 | element.attr('contenteditable', true);
97 | element.attr('data-hometype-not-insert-mode', 'true');
98 | element.attr('data-hometype-contenteditable', 'true');
99 | $('').addClass('hometype-contenteditable').css({
100 | width: element.innerWidth() + 10,
101 | height: element.innerHeight() + 10,
102 | top: element.offset().top - 5,
103 | left: element.offset().left - 5
104 | }).appendTo($('body')).click(function() {
105 | element.focus();
106 | });
107 | };
108 |
109 | HometypeScreen.prototype.resetContentEditable = function() {
110 | var element = this.getCurrentContentEditable();
111 | element.removeAttr('contenteditable');
112 | element.removeAttr('data-hometype-not-insert-mode');
113 | element.removeAttr('data-hometype-contenteditable');
114 | $('.hometype-contenteditable').remove();
115 | $(document.activeElement).blur();
116 | };
117 |
118 | HometypeScreen.prototype.getCurrentContentEditable = function() {
119 | return $('[data-hometype-contenteditable=true]');
120 | };
121 |
122 | /**
123 | * Add link element to DOM.
124 | *
125 | * @param string url link url.
126 | * @param element parant An element that is parent for a link element.
127 | * Add link element into body element if omit an argument.
128 | */
129 | HometypeScreen.prototype.createLink = function(url, parent) {
130 | var parent = $(parent || 'body');
131 | return $(' ').attr('href', url).appendTo(parent);
132 | };
133 |
134 | var Viewport = new HometypeScreen();
135 |
--------------------------------------------------------------------------------
/spec/support/jasmine-2.0.0/jasmine.css:
--------------------------------------------------------------------------------
1 | body { background-color: #eeeeee; padding: 0; margin: 5px; overflow-y: scroll; }
2 |
3 | .html-reporter { font-size: 11px; font-family: Monaco, "Lucida Console", monospace; line-height: 14px; color: #333333; }
4 | .html-reporter a { text-decoration: none; }
5 | .html-reporter a:hover { text-decoration: underline; }
6 | .html-reporter p, .html-reporter h1, .html-reporter h2, .html-reporter h3, .html-reporter h4, .html-reporter h5, .html-reporter h6 { margin: 0; line-height: 14px; }
7 | .html-reporter .banner, .html-reporter .symbol-summary, .html-reporter .summary, .html-reporter .result-message, .html-reporter .spec .description, .html-reporter .spec-detail .description, .html-reporter .alert .bar, .html-reporter .stack-trace { padding-left: 9px; padding-right: 9px; }
8 | .html-reporter .banner .version { margin-left: 14px; }
9 | .html-reporter #jasmine_content { position: fixed; right: 100%; }
10 | .html-reporter .version { color: #aaaaaa; }
11 | .html-reporter .banner { margin-top: 14px; }
12 | .html-reporter .duration { color: #aaaaaa; float: right; }
13 | .html-reporter .symbol-summary { overflow: hidden; *zoom: 1; margin: 14px 0; }
14 | .html-reporter .symbol-summary li { display: inline-block; height: 8px; width: 14px; font-size: 16px; }
15 | .html-reporter .symbol-summary li.passed { font-size: 14px; }
16 | .html-reporter .symbol-summary li.passed:before { color: #5e7d00; content: "\02022"; }
17 | .html-reporter .symbol-summary li.failed { line-height: 9px; }
18 | .html-reporter .symbol-summary li.failed:before { color: #b03911; content: "x"; font-weight: bold; margin-left: -1px; }
19 | .html-reporter .symbol-summary li.disabled { font-size: 14px; }
20 | .html-reporter .symbol-summary li.disabled:before { color: #bababa; content: "\02022"; }
21 | .html-reporter .symbol-summary li.pending { line-height: 17px; }
22 | .html-reporter .symbol-summary li.pending:before { color: #ba9d37; content: "*"; }
23 | .html-reporter .exceptions { color: #fff; float: right; margin-top: 5px; margin-right: 5px; }
24 | .html-reporter .bar { line-height: 28px; font-size: 14px; display: block; color: #eee; }
25 | .html-reporter .bar.failed { background-color: #b03911; }
26 | .html-reporter .bar.passed { background-color: #a6b779; }
27 | .html-reporter .bar.skipped { background-color: #bababa; }
28 | .html-reporter .bar.menu { background-color: #fff; color: #aaaaaa; }
29 | .html-reporter .bar.menu a { color: #333333; }
30 | .html-reporter .bar a { color: white; }
31 | .html-reporter.spec-list .bar.menu.failure-list, .html-reporter.spec-list .results .failures { display: none; }
32 | .html-reporter.failure-list .bar.menu.spec-list, .html-reporter.failure-list .summary { display: none; }
33 | .html-reporter .running-alert { background-color: #666666; }
34 | .html-reporter .results { margin-top: 14px; }
35 | .html-reporter.showDetails .summaryMenuItem { font-weight: normal; text-decoration: inherit; }
36 | .html-reporter.showDetails .summaryMenuItem:hover { text-decoration: underline; }
37 | .html-reporter.showDetails .detailsMenuItem { font-weight: bold; text-decoration: underline; }
38 | .html-reporter.showDetails .summary { display: none; }
39 | .html-reporter.showDetails #details { display: block; }
40 | .html-reporter .summaryMenuItem { font-weight: bold; text-decoration: underline; }
41 | .html-reporter .summary { margin-top: 14px; }
42 | .html-reporter .summary ul { list-style-type: none; margin-left: 14px; padding-top: 0; padding-left: 0; }
43 | .html-reporter .summary ul.suite { margin-top: 7px; margin-bottom: 7px; }
44 | .html-reporter .summary li.passed a { color: #5e7d00; }
45 | .html-reporter .summary li.failed a { color: #b03911; }
46 | .html-reporter .summary li.pending a { color: #ba9d37; }
47 | .html-reporter .description + .suite { margin-top: 0; }
48 | .html-reporter .suite { margin-top: 14px; }
49 | .html-reporter .suite a { color: #333333; }
50 | .html-reporter .failures .spec-detail { margin-bottom: 28px; }
51 | .html-reporter .failures .spec-detail .description { background-color: #b03911; }
52 | .html-reporter .failures .spec-detail .description a { color: white; }
53 | .html-reporter .result-message { padding-top: 14px; color: #333333; white-space: pre; }
54 | .html-reporter .result-message span.result { display: block; }
55 | .html-reporter .stack-trace { margin: 5px 0 0 0; max-height: 224px; overflow: auto; line-height: 18px; color: #666666; border: 1px solid #ddd; background: white; white-space: pre; }
56 |
--------------------------------------------------------------------------------
/js/processor/command.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2013 Kengo Tateishi (@tkengo)
3 | * Licensed under MIT license.
4 | * http://www.opensource.org/licenses/mit-license.php
5 | *
6 | * Command mode processor.
7 | * Show command box under the window if enter the command mode.
8 | *
9 | * This processor has below events, and set event listener.
10 | * - onUpdateBoxText Fire up when command box contents is updated.
11 | * If this event listener returns an array, it is shown on a command box
12 | * as candidate list.
13 | * - onEnter Fire up when the enter key was pushed in a command box.
14 | * These event listeners is reset when leave the command mode.
15 | */
16 | var CommandModeProcessor = function() {
17 | this.updateBoxTextCallback = null;
18 | this.enterCallback = null;
19 | this.notifyLeaveModeCallback = null;
20 |
21 | this.commandBox = new HometypeCommandBox();
22 | };
23 |
24 | /**
25 | * Callback method that invoke when enter the command mode.
26 | */
27 | CommandModeProcessor.prototype.notifyEnterMode = function() {
28 | this.commandBox.show();
29 | };
30 |
31 | /**
32 | * Callback method that invoke when leave the command mode.
33 | */
34 | CommandModeProcessor.prototype.notifyLeaveMode = function() {
35 | this.updateBoxTextCallback = null;
36 | this.enterCallback = null;
37 |
38 | if (this.notifyLeaveModeCallback) {
39 | this.notifyLeaveModeCallback();
40 | this.notifyLeaveModeCallback = null;
41 | }
42 |
43 | this.commandBox.hide();
44 | };
45 |
46 | /**
47 | * Register callback that invokes when Ht leaves from the command mode.
48 | *
49 | * @param function notifyLeaveModeCallback Callback method.
50 | */
51 | CommandModeProcessor.prototype.onNotifyLeaveMode = function(notifyLeaveModeCallback) {
52 | this.notifyLeaveModeCallback = notifyLeaveModeCallback;
53 | };
54 |
55 | /**
56 | * Key processing.
57 | *
58 | * @param string stack key stack.
59 | * @param string currentKey pushed key.
60 | * @param KeyboradEvent e event.
61 | */
62 | CommandModeProcessor.prototype.onKeyDown = function(stack, currentKey, e) {
63 | if (currentKey == 'Enter') {
64 | var text = this.commandBox.getText();
65 | var result = true;
66 | if (this.enterCallback) {
67 | result = this.enterCallback(text, this.commandBox.getSelected());
68 | }
69 |
70 | if (result !== false) {
71 | this.commandBox.hide();
72 | Mode.changeMode(ModeList.NORMAL_MODE);
73 | }
74 |
75 | if (!this.enterCallback) {
76 | new Executer(text).execute();
77 | }
78 |
79 | return result;
80 | }
81 |
82 | if (this.updateBoxTextCallback) {
83 | // Wait a moment, then invoke a callback, because this key event is invalid
84 | // if processor don't spread it over command box.
85 | setTimeout($.proxy(function() {
86 | this.updateCandidate();
87 | }, this), 10);
88 | }
89 |
90 | return true;
91 | };
92 |
93 | /**
94 | * Update candidate.
95 | */
96 | CommandModeProcessor.prototype.updateCandidate = function() {
97 | var result = this.updateBoxTextCallback(this.commandBox.getText());
98 |
99 | // If a result of callback method is array, set it as candidate,
100 | // and then show candidates list.
101 | if (result instanceof Array) {
102 | this.commandBox.setCandidate(result);
103 | this.commandBox.showCandidate();
104 | }
105 | };
106 |
107 | /**
108 | * Set a callback method that invoke when command box contents is updated.
109 | *
110 | * @param function callback Callback method.
111 | * @param boolean immediately Execute once registered callback method if true.
112 | */
113 | CommandModeProcessor.prototype.onUpdateBoxText = function(callback, immediately) {
114 | this.updateBoxTextCallback = callback;
115 |
116 | if (immediately) {
117 | this.updateCandidate();
118 | }
119 | };
120 |
121 | /**
122 | * Set a callback method that invoke when enter key was pushed in command box.
123 | *
124 | * @param function callback Callback method.
125 | */
126 | CommandModeProcessor.prototype.onEnter = function(callback) {
127 | this.enterCallback = callback;
128 | };
129 |
130 | /**
131 | * Get the command box.
132 | *
133 | * @return HometypeCommandBox The command box object.
134 | */
135 | CommandModeProcessor.prototype.getCommandBox = function() {
136 | return this.commandBox;
137 | };
138 |
--------------------------------------------------------------------------------
/_locales/en/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "basic_options": {
3 | "message": "Basic Options"
4 | },
5 | "key_bindings": {
6 | "message": "Key Bindings"
7 | },
8 | "basic_config": {
9 | "message": "Basic"
10 | },
11 | "command_interval": {
12 | "message": "Command Interval"
13 | },
14 | "amount_of_scrolling": {
15 | "message": "Amount of Scrolling"
16 | },
17 | "hint_config": {
18 | "message": "Hint"
19 | },
20 | "key_algorithm": {
21 | "message": "Key Algorithm"
22 | },
23 | "standard": {
24 | "message": "Standard"
25 | },
26 | "comfortable": {
27 | "message": "Comfortable"
28 | },
29 | "custom": {
30 | "message": "Custom"
31 | },
32 | "letter_type": {
33 | "message": "Letter Type"
34 | },
35 | "hint_auto_confirm": {
36 | "message": "Follow the link if it was confirmed"
37 | },
38 | "lowercase": {
39 | "message": "lowercase"
40 | },
41 | "uppercase": {
42 | "message": "UPPERCASE"
43 | },
44 | "ignore_urls": {
45 | "message": "Ignore Urls"
46 | },
47 | "save": {
48 | "message": "Save"
49 | },
50 |
51 | "desc_for_command_interval": {
52 | "message": "This option value is the time till a command is confirmed. It is available only if there aren't a confirmed command when you typed keys."
53 | },
54 | "desc_for_amount_of_scrolling": {
55 | "message": "This is used when scrollDown and scrollUp is called."
56 | },
57 | "desc_for_key_algorithm": {
58 | "message": "
59 | Select an algorithm that generates hint keys in the hint mode.
60 |
61 | - 1. Standard
62 | - Characters used as hint keys in this algorithm is jfhkgyuiopqwertnmzxcvblasd. Hint keys in the hint mode are generated by fetching a character from it.
63 | - 2. Comfortable
64 | - This algorithm tries to generate hint keys as possible as type with only one hand. In most case, this is usable more than Standard Algorithm.
65 | - 3. Custom
66 | - Specify hint keys by yourself. You must at least specify two keys.
67 |
68 | "
69 | },
70 | "desc_for_letter_type": {
71 | "message": "
72 | Select an hint letter type.
73 |
74 | - 1. lowercase
75 | - Hint keys generated by the algorithm is converted to lowercase
76 | - 2. UPPERCASE
77 | - Hint keys generated by the algorithm is converted to UPPERCASE
78 |
79 | "
80 | },
81 | "desc_for_hint_auto_confirm": {
82 | "message": "If this option is available, if there is only one element when you search with text in the hint mode, the element will be clicked and move to a next page."
83 | },
84 | "desc_for_ignore_urls": {
85 | "message": "You can specify url at each lines using regular expression to make Hometype unavailable."
86 | },
87 | "desc_for_key_bindings": {
88 | "message": "
89 | You can customize key bindings to specify following format:
90 |
91 | map-type assigned-keys executed-commands [--options]
92 |
93 | For example,
94 |
95 |
96 | nmap j scrollDown
97 | vmap <C-c> enterNormalMode
98 | imap Jf scrollDown followLink
99 |
100 |
101 |
102 | - map-type
103 | - You can specify nmap, cmap, imap, vmap, fmap. Each types correspond to key bindings in the normal mode, command mode, insert mode, visual mode, hint mode.
104 | - assigned-keys
105 | - You can specify keys to invoke a command you want to execute. Multi keys is available.
106 | - executed-commands
107 | - You can specify commands you want to executed with its options. Multi commands also is available. If you give @ character to the head of the command, Hometype takes over its options that was executed previous when the command will be executed.
108 |
109 | To get more information for commands please see Hometype command reference
110 | "
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/js/homedics.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2013 Kengo Tateishi (@tkengo)
3 | * Licensed under MIT license.
4 | * http://www.opensource.org/licenses/mit-license.php
5 | *
6 | * Manage Hometype dictionaries.
7 | */
8 |
9 | /**
10 | * You can pass a romaji string to this constructor.
11 | * Then, Homedics searches candidates from SKK dictionaries
12 | * and builds a regular expression.
13 | * You can check a string whether it matches with words in the
14 | * dictionaries by invoking 'match' method.
15 | *
16 | * Dictionary files is placed in the dicts/ directory and separated
17 | * by a head letter of words.
18 | *
19 | * @param string romaji A checking target string.
20 | */
21 | var Homedics = function(romaji) {
22 | this.romaji = romaji;
23 | this.regexp = this.buildRegexp(romaji);
24 | };
25 |
26 | Homedics.alphabetDict = '';
27 | Homedics.dict = '';
28 | Homedics.letter = '';
29 |
30 | /**
31 | * Dictionary letters array.
32 | */
33 | Homedics.dictLetters = [
34 | 'a', 'i', 'u', 'e', 'o', 'k', 's', 't', 'n', 'h', 'm', 'y',
35 | 'r', 'w', 'g', 'z', 'd', 'b', 'p', 'j', 'c', 'q', 'f'
36 | ];
37 |
38 | /**
39 | * Build regular expression for dictionary matching.
40 | *
41 | * @param string romaji
42 | * @return RegExp
43 | */
44 | Homedics.prototype.buildRegexp = function(romaji) {
45 | if (romaji == '') {
46 | return null;
47 | }
48 |
49 | var dict = this.loadAlphabetDict();
50 | var regexp = [ romaji ];
51 | var candidate = [ romaji ];
52 | var patterns = [];
53 | var m, characters, words;
54 |
55 | if (Utility.inArray([ 'a', 'i', 'u', 'e', 'o' ], romaji) || romaji.length > 1) {
56 | candidate = candidate.concat(Jp.getHiraganaCandidates(romaji));
57 | dict += "\n" + this.loadDict(romaji.charAt(0));
58 | }
59 | regexp.push(candidate.join('|'));
60 | regexp.push(Jp.toKatakana(candidate.join('|')));
61 |
62 | var dictPattern = new RegExp('^(' + candidate.join('|') + ').*:(.*)$', 'gm');
63 | while (m = dictPattern.exec(dict)) {
64 | patterns = patterns.concat(m[2].split(' '));
65 | }
66 | patterns = patterns.sort().join("\n").replace(/^(.+)$(\n^\1.+$)+/gm, '$1');
67 |
68 | if (characters = patterns.match(/^.$/gm)) {
69 | regexp.push('[' + characters.join('').replace(/(.)(\1)+/g, '$1') + ']');
70 | }
71 | if (words = patterns.match(/^..+$/gm)) {
72 | regexp.push(words.join('|'));
73 | }
74 |
75 | return new RegExp(regexp.join('|'), 'ig');
76 | };
77 |
78 | /**
79 | * Load a dictionary from dicts/*.ml with ajax from
80 | * web_accessible_resources.
81 | *
82 | * @param string letter A head letter of a dictionary you want to load.
83 | * @return string The content of a dictionary.
84 | */
85 | Homedics.prototype.loadDict = function(letter) {
86 | if (Homedics.dictLetters.indexOf(letter) == -1) {
87 | return '';
88 | }
89 |
90 | if (Homedics.letter == letter && Homedics.dict != '') {
91 | return Homedics.dict;
92 | }
93 | Homedics.letter = letter;
94 |
95 | var xhr = new XMLHttpRequest()
96 | var dictUrl = chrome.extension.getURL('dicts/' + letter + '.ml');
97 |
98 | xhr.open('GET', dictUrl, false);
99 | xhr.send();
100 | return Homedics.dict = xhr.responseText;
101 | };
102 |
103 | /**
104 | * Load an alphabet dictionary from dicts/alphabet.ml with ajax from
105 | * web_accessible_resources.
106 | *
107 | * @return string The content of an alphabet dictionary.
108 | */
109 | Homedics.prototype.loadAlphabetDict = function() {
110 | if (Homedics.alphabetDict != '') {
111 | return Homedics.alphabetDict;
112 | }
113 |
114 | var xhr = new XMLHttpRequest()
115 | var dictUrl = chrome.extension.getURL('dicts/alphabet.ml');
116 |
117 | xhr.open('GET', dictUrl, false);
118 | xhr.send();
119 | return Homedics.alphabetDict = xhr.responseText;
120 | };
121 |
122 | /**
123 | * Check whether the target string is matched with words
124 | * in a dictionary.
125 | *
126 | * @param string target
127 | * @return object A object value that has following key:
128 | * match: true if the target string is matched, otherwise false.
129 | * head: true if the target string is matched in the its head, otherwise false.
130 | */
131 | Homedics.prototype.match = function(target) {
132 | var position = -1;
133 | var matches = target.match(this.regexp);
134 |
135 | if (matches) {
136 | matches = $.unique(matches);
137 | for (var i = 0; i < matches.length; i++) {
138 | if ((position = target.indexOf(matches[i])) == 0) {
139 | break;
140 | }
141 | }
142 | }
143 |
144 | return {
145 | matched: !!matches,
146 | matches: matches || [],
147 | head: position == 0
148 | };
149 | };
150 |
--------------------------------------------------------------------------------
/spec/support/jasmine-2.0.0/console.js:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (c) 2008-2013 Pivotal Labs
3 |
4 | Permission is hereby granted, free of charge, to any person obtaining
5 | a copy of this software and associated documentation files (the
6 | "Software"), to deal in the Software without restriction, including
7 | without limitation the rights to use, copy, modify, merge, publish,
8 | distribute, sublicense, and/or sell copies of the Software, and to
9 | permit persons to whom the Software is furnished to do so, subject to
10 | the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be
13 | included in all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22 | */
23 | function getJasmineRequireObj() {
24 | if (typeof module !== "undefined" && module.exports) {
25 | return exports;
26 | } else {
27 | window.jasmineRequire = window.jasmineRequire || {};
28 | return window.jasmineRequire;
29 | }
30 | }
31 |
32 | getJasmineRequireObj().console = function(jRequire, j$) {
33 | j$.ConsoleReporter = jRequire.ConsoleReporter();
34 | };
35 |
36 | getJasmineRequireObj().ConsoleReporter = function() {
37 |
38 | var noopTimer = {
39 | start: function(){},
40 | elapsed: function(){ return 0; }
41 | };
42 |
43 | function ConsoleReporter(options) {
44 | var print = options.print,
45 | showColors = options.showColors || false,
46 | onComplete = options.onComplete || function() {},
47 | timer = options.timer || noopTimer,
48 | specCount,
49 | failureCount,
50 | failedSpecs = [],
51 | pendingCount,
52 | ansi = {
53 | green: '\x1B[32m',
54 | red: '\x1B[31m',
55 | yellow: '\x1B[33m',
56 | none: '\x1B[0m'
57 | };
58 |
59 | this.jasmineStarted = function() {
60 | specCount = 0;
61 | failureCount = 0;
62 | pendingCount = 0;
63 | print("Started");
64 | printNewline();
65 | timer.start();
66 | };
67 |
68 | this.jasmineDone = function() {
69 | printNewline();
70 | for (var i = 0; i < failedSpecs.length; i++) {
71 | specFailureDetails(failedSpecs[i]);
72 | }
73 |
74 | printNewline();
75 | var specCounts = specCount + " " + plural("spec", specCount) + ", " +
76 | failureCount + " " + plural("failure", failureCount);
77 |
78 | if (pendingCount) {
79 | specCounts += ", " + pendingCount + " pending " + plural("spec", pendingCount);
80 | }
81 |
82 | print(specCounts);
83 |
84 | printNewline();
85 | var seconds = timer.elapsed() / 1000;
86 | print("Finished in " + seconds + " " + plural("second", seconds));
87 |
88 | printNewline();
89 |
90 | onComplete(failureCount === 0);
91 | };
92 |
93 | this.specDone = function(result) {
94 | specCount++;
95 |
96 | if (result.status == "pending") {
97 | pendingCount++;
98 | print(colored("yellow", "*"));
99 | return;
100 | }
101 |
102 | if (result.status == "passed") {
103 | print(colored("green", '.'));
104 | return;
105 | }
106 |
107 | if (result.status == "failed") {
108 | failureCount++;
109 | failedSpecs.push(result);
110 | print(colored("red", 'F'));
111 | }
112 | };
113 |
114 | return this;
115 |
116 | function printNewline() {
117 | print("\n");
118 | }
119 |
120 | function colored(color, str) {
121 | return showColors ? (ansi[color] + str + ansi.none) : str;
122 | }
123 |
124 | function plural(str, count) {
125 | return count == 1 ? str : str + "s";
126 | }
127 |
128 | function repeat(thing, times) {
129 | var arr = [];
130 | for (var i = 0; i < times; i++) {
131 | arr.push(thing);
132 | }
133 | return arr;
134 | }
135 |
136 | function indent(str, spaces) {
137 | var lines = (str || '').split("\n");
138 | var newArr = [];
139 | for (var i = 0; i < lines.length; i++) {
140 | newArr.push(repeat(" ", spaces).join("") + lines[i]);
141 | }
142 | return newArr.join("\n");
143 | }
144 |
145 | function specFailureDetails(result) {
146 | printNewline();
147 | print(result.fullName);
148 |
149 | for (var i = 0; i < result.failedExpectations.length; i++) {
150 | var failedExpectation = result.failedExpectations[i];
151 | printNewline();
152 | print(indent(failedExpectation.stack, 2));
153 | }
154 |
155 | printNewline();
156 | }
157 | }
158 |
159 | return ConsoleReporter;
160 | };
161 |
--------------------------------------------------------------------------------
/spec/js/executer_spec.js:
--------------------------------------------------------------------------------
1 | describe('Executer', function() {
2 | var executer;
3 |
4 | beforeEach(function() {
5 | Command.testCommand = function() { };
6 | spyOn(Command, 'testCommand');
7 |
8 | KeyMap.nmap('a', 'testCommand');
9 | KeyMap.nmap('ba', 'testCommand');
10 | KeyMap.nmap('bb', 'testCommand');
11 | KeyMap.nmap('c', '@testCommand');
12 | });
13 |
14 | describe('was passed a mode and key to arguments', function() {
15 | beforeEach(function() {
16 | executer = new Executer(ModeList.NORMAL_MODE, 'a');
17 | });
18 |
19 | it('should be fix a candidate', function() {
20 | expect(executer.fixedCandidate()).toBe(true);
21 | expect(executer.noCandidate()).toBe(false);
22 | });
23 |
24 | it('should invoke a command', function() {
25 | expect(executer.execute()).toBe(true);
26 | expect(Command.testCommand).toHaveBeenCalled();
27 | });
28 | });
29 |
30 | describe('was passed a mode and key to arguments with previous option', function() {
31 | beforeEach(function() {
32 | executer = new Executer(ModeList.NORMAL_MODE, 'c');
33 | });
34 |
35 | it('should be fix a candidate', function() {
36 | expect(executer.fixedCandidate()).toBe(true);
37 | expect(executer.noCandidate()).toBe(false);
38 | });
39 |
40 | it('should invoke a command', function() {
41 | expect(executer.execute()).toBe(true);
42 | expect(Command.testCommand).toHaveBeenCalled();
43 | });
44 | });
45 |
46 | describe('was passed non fixed candidate value to arguments', function() {
47 | beforeEach(function() {
48 | executer = new Executer(ModeList.NORMAL_MODE, 'b');
49 | });
50 |
51 | it('should be fix a candidate', function() {
52 | expect(executer.fixedCandidate()).toBe(false);
53 | expect(executer.noCandidate()).toBe(false);
54 | });
55 |
56 | it('should invoke a command', function() {
57 | expect(executer.execute()).toBe(false);
58 | });
59 | });
60 |
61 | describe('was passed fixed candidate value to arguments', function() {
62 | beforeEach(function() {
63 | executer = new Executer(ModeList.NORMAL_MODE, 'ba');
64 | });
65 |
66 | it('should be fix a candidate', function() {
67 | expect(executer.fixedCandidate()).toBe(true);
68 | expect(executer.noCandidate()).toBe(false);
69 | });
70 |
71 | it('should invoke a command', function() {
72 | expect(executer.execute()).toBe(true);
73 | expect(Command.testCommand).toHaveBeenCalled();
74 | });
75 | });
76 |
77 | describe('was passed no candidate value to arguments', function() {
78 | beforeEach(function() {
79 | executer = new Executer(ModeList.NORMAL_MODE, 'baa');
80 | });
81 |
82 | it('should be fix a candidate', function() {
83 | expect(executer.fixedCandidate()).toBe(false);
84 | expect(executer.noCandidate()).toBe(true);
85 | });
86 |
87 | it('should invoke a command', function() {
88 | expect(executer.execute()).toBe(false);
89 | });
90 | });
91 |
92 | describe('was passed a command name to arguments', function() {
93 | beforeEach(function() {
94 | executer = new Executer('testCommand');
95 | });
96 |
97 | it('should be fix a candidate', function() {
98 | expect(executer.fixedCandidate()).toBe(true);
99 | expect(executer.noCandidate()).toBe(false);
100 | });
101 |
102 | it('should invoke a command', function() {
103 | expect(executer.execute()).toBe(true);
104 | expect(Command.testCommand).toHaveBeenCalled();
105 | });
106 | });
107 |
108 | describe('was passed a command name to arguments with previous option', function() {
109 | beforeEach(function() {
110 | executer = new Executer('@testCommand');
111 | });
112 |
113 | it('should be fix a candidate', function() {
114 | expect(executer.fixedCandidate()).toBe(true);
115 | expect(executer.noCandidate()).toBe(false);
116 | });
117 |
118 | it('should invoke a command', function() {
119 | expect(executer.execute()).toBe(true);
120 | expect(Command.testCommand).toHaveBeenCalled();
121 | });
122 | });
123 |
124 | describe('was passed a command name with options to arguments', function() {
125 | beforeEach(function() {
126 | executer = new Executer('testCommand --option1 --option2');
127 | });
128 |
129 | it('should be fix a command', function() {
130 | expect(executer.fixedCandidate()).toBe(true);
131 | expect(executer.noCandidate()).toBe(false);
132 | });
133 |
134 | it('should invoke a command with options', function() {
135 | var option = { option1: true, option2: true };
136 | expect(executer.execute()).toBe(true);
137 | expect(Command.testCommand).toHaveBeenCalledWith(option);
138 | });
139 | });
140 |
141 | describe('was passed nonexistent command name to arguments', function() {
142 | beforeEach(function() {
143 | executer = new Executer('nonexistentCommand');
144 | });
145 |
146 | it('should be fix a command', function() {
147 | expect(executer.fixedCandidate()).toBe(true);
148 | expect(executer.noCandidate()).toBe(false);
149 | });
150 |
151 | it('should not invoke a command', function() {
152 | expect(executer.execute()).toBe(false);
153 | });
154 | });
155 | });
156 |
--------------------------------------------------------------------------------
/js/mode.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2013 Kengo Tateishi (@tkengo)
3 | * Licensed under MIT license.
4 | * http://www.opensource.org/licenses/mit-license.php
5 | *
6 | * Manage mode.
7 | */
8 |
9 | /**
10 | * Mode list.
11 | */
12 | var ModeList = {
13 | NORMAL_MODE: 'normal',
14 | INSERT_MODE: 'insert',
15 | HINT_MODE: 'hint',
16 | VISUAL_MODE: 'visual',
17 | COMMAND_MODE: 'command',
18 | HELP_MODE: 'help'
19 | };
20 |
21 | /**
22 | * Constructor.
23 | */
24 | var HometypeMode = function() {
25 | this.mode = ModeList.NORMAL_MODE;
26 |
27 | this.callbacks = [];
28 |
29 | this.modeProcessors = {};
30 | this.modeProcessors[ModeList.NORMAL_MODE] = new NoopProcessor();
31 | this.modeProcessors[ModeList.INSERT_MODE] = new InsertModeProcessor();
32 | this.modeProcessors[ModeList.HINT_MODE] = new HintModeProcessor();
33 | this.modeProcessors[ModeList.VISUAL_MODE] = new VisualModeProcessor();
34 | this.modeProcessors[ModeList.COMMAND_MODE] = new CommandModeProcessor();
35 | this.modeProcessors[ModeList.HELP_MODE] = new HelpModeProcessor();
36 |
37 | this.lockMode = false;
38 | };
39 |
40 | /**
41 | * Get the current mode.
42 | *
43 | * @return string The current mode.
44 | */
45 | HometypeMode.prototype.getCurrentMode = function() {
46 | return this.mode;
47 | };
48 |
49 | /**
50 | * Change the current mode to specified mode,
51 | * and call event handler registered by onModeChange.
52 | *
53 | * @param string modeName A mode name
54 | * @return Object The current mode processor
55 | */
56 | HometypeMode.prototype.changeMode = function(modeName) {
57 | if (this.lockMode) {
58 | return false;
59 | }
60 |
61 | // Nothing happens if mode didn't change.
62 | if (this.mode == modeName) {
63 | return this.getProcessor(this.mode);
64 | }
65 |
66 | var oldMode = this.mode;
67 | this.mode = modeName;
68 |
69 | // call callbacks
70 | for (var key in this.callbacks) {
71 | var callback = this.callbacks[key];
72 | if (typeof callback === 'function') {
73 | callback.call(callback, this.mode, oldMode);
74 | }
75 | }
76 |
77 | // Notify the current mode processor that mode has changed.
78 | var oldProcessor = this.getProcessor(oldMode);
79 | var currentProcessor = this.getProcessor(this.mode);
80 | if (typeof oldProcessor.notifyLeaveMode == 'function') {
81 | oldProcessor.notifyLeaveMode();
82 | }
83 | if (typeof currentProcessor.notifyEnterMode == 'function') {
84 | currentProcessor.notifyEnterMode();
85 | }
86 |
87 | return currentProcessor;
88 | };
89 |
90 | /**
91 | * Enter the hint mode.
92 | * Specify hint targets and hint theme.
93 | *
94 | * @param string theme hint theme.
95 | * @param array targets hint targets.
96 | * @return Object hint mode processor.
97 | */
98 | HometypeMode.prototype.enterHintMode = function(theme, targets) {
99 | var processor = this.changeMode(ModeList.HINT_MODE);
100 | processor.createHints(theme, targets);
101 | return processor;
102 | };
103 |
104 | /**
105 | * Enter the visual mode.
106 | * Specify visual mode target.
107 | *
108 | * @param jQueryElement element visual mode target.
109 | * @return Object visual mode processor.
110 | */
111 | HometypeMode.prototype.enterVisualMode = function(element) {
112 | Viewport.setContentEditable(element);
113 | element.focus().click();
114 | return Mode.changeMode(ModeList.VISUAL_MODE);
115 | };
116 |
117 | /**
118 | * Get the processor specified mode by argument.
119 | * Return current mode processor if omit an argument.
120 | *
121 | * @param ModeList mode mode
122 | * @return Object processor
123 | */
124 | HometypeMode.prototype.getProcessor = function(mode) {
125 | mode = mode || this.mode;
126 |
127 | return this.modeProcessors[mode];
128 | };
129 |
130 | /**
131 | * Check if whether the current mode is the normal mode.
132 | *
133 | * @return boolean Return true if the current mode is the normal mode.
134 | */
135 | HometypeMode.prototype.isNormalMode = function() {
136 | return this.getCurrentMode() == ModeList.NORMAL_MODE;
137 | };
138 |
139 | /**
140 | * Check if whether the current mode is the insert mode.
141 | *
142 | * @return boolean Return true if the current mode is the insert mode.
143 | */
144 | HometypeMode.prototype.isInsertMode = function() {
145 | return this.getCurrentMode() == ModeList.INSERT_MODE;
146 | };
147 |
148 | /**
149 | * Register an event which is called when mode was changed.
150 | * Callback method has two arguments.
151 | * newMode is the mode after changed.
152 | * oldMode is the mode before changed.
153 | *
154 | * @param function callback Callback method.
155 | */
156 | HometypeMode.prototype.onModeChange = function(callback) {
157 | this.callbacks.push(callback);
158 | };
159 |
160 | /**
161 | * Lock changing the mode.
162 | * Mode is able to be changed to any other mode If this method was called
163 | * before changeMode method is called.
164 | */
165 | HometypeMode.prototype.lock = function() {
166 | this.lockMode = true;
167 | };
168 |
169 | /**
170 | * Release mode locking status.
171 | */
172 | HometypeMode.prototype.release = function() {
173 | this.lockMode = false;
174 | };
175 |
176 | /**
177 | * Check if whether mode was locked.
178 | *
179 | * @return boolean Return true if mode was locked, otherwise false.
180 | */
181 | HometypeMode.prototype.isLock = function() {
182 | return this.lockMode;
183 | };
184 |
185 | var Mode = new HometypeMode();
186 |
--------------------------------------------------------------------------------
/js/processor/hint.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2013 Kengo Tateishi (@tkengo)
3 | * Licensed under MIT license.
4 | * http://www.opensource.org/licenses/mit-license.php
5 | *
6 | * Hint mode processor.
7 | */
8 | var HintModeProcessor = function() {
9 | this.chooseElementCallback = null;
10 | this.notifyLeaveModeCallback = null;
11 | this.hintElements = null;
12 | this.commandBox = null;
13 | this.headElements = [];
14 | this.searching = false;
15 | this.extendAction = null;
16 | };
17 |
18 | /**
19 | * Callback method that invoke when enter the hint mode.
20 | */
21 | HintModeProcessor.prototype.notifyEnterMode = function() {
22 | if (!this.commandBox) {
23 | this.commandBox = new HometypeCommandBox('SearchHints');
24 | }
25 | };
26 |
27 | /**
28 | * Callback method that invoke when leave the hint mode.
29 | */
30 | HintModeProcessor.prototype.notifyLeaveMode = function() {
31 | this.chooseElementCallback = null;
32 | this.hintElements.removeAllHint();
33 | this.searching = false;
34 | this.extendAction = null;
35 |
36 | if (this.commandBox) {
37 | this.commandBox.hide();
38 | }
39 |
40 | if (this.notifyLeaveModeCallback) {
41 | this.notifyLeaveModeCallback();
42 | this.notifyLeaveModeCallback = null;
43 | }
44 | };
45 |
46 | /**
47 | * Key processing.
48 | *
49 | * @param string stack key stack.
50 | * @param string currentKey pushed key.
51 | * @param KeyboradEvent e event.
52 | */
53 | HintModeProcessor.prototype.onKeyDown = function(stack, currentKey, e) {
54 | // Enter the extend hint mode.
55 | if (!this.searching && (currentKey == ';' || this.commandBox.getHeaderText() == ';')) {
56 | if (currentKey == ';') {
57 | this.commandBox.setHeaderText(';').show();
58 | return true;
59 | } else if (ActionMap[currentKey]) {
60 | this.extendAction = ActionMap[currentKey];
61 | this.commandBox.setHeaderText(this.extendAction).setText('');
62 | document.activeElement.blur();
63 | return true;
64 | } else {
65 | this.commandBox.setHeaderText(this.getExtendAction());
66 | }
67 | }
68 |
69 | // Get elements matched hint key.
70 | var elements = this.hintElements.getMatchedElements(stack);
71 | if (elements.length == 0) {
72 | // Search hint texts if there is no element matched hint key.
73 | this.startSearching(currentKey);
74 | return true;
75 | }
76 |
77 | e.stopPropagation();
78 | e.preventDefault();
79 |
80 | if (elements.length == 1 && elements[0].getKey() == stack) {
81 | this.confirm(elements[0].getElement());
82 | return true;
83 | } else {
84 | this.hintElements.hideUnmatchedElements(stack);
85 | return false;
86 | }
87 | };
88 |
89 | HintModeProcessor.prototype.getExtendAction = function() {
90 | return this.extendAction || ActionMap.default;
91 | };
92 |
93 | /**
94 | * Confirm an element and invoke a callback method.
95 | * Return normal mode if the callback method returned false.
96 | *
97 | * @param DOMElement element A confirmed element.
98 | */
99 | HintModeProcessor.prototype.confirm = function(element) {
100 | // Invoke a callback method if an element is confirmed.
101 | if (this.chooseElementCallback && this.chooseElementCallback.call(this, element) !== false) {
102 | // Return normal mode if only callback didn't return false.
103 | Mode.changeMode(ModeList.NORMAL_MODE);
104 | }
105 | };
106 |
107 | /**
108 | * Create hints for source elements and show it.
109 | *
110 | * @param array elements Source elements.
111 | */
112 | HintModeProcessor.prototype.createHints = function(theme, elements) {
113 | if (this.hintElements) {
114 | this.hintElements.removeAllHint();
115 | }
116 |
117 | this.hintElements = new HintElementCollection(theme, elements);
118 | };
119 |
120 | /**
121 | * Set a callback method that is invoked when a hint is confirmed.
122 | *
123 | * @param function chooseElementCallback Callback method.
124 | */
125 | HintModeProcessor.prototype.onChooseElement = function(chooseElementCallback) {
126 | this.chooseElementCallback = chooseElementCallback;
127 | };
128 |
129 | /**
130 | * Register callback that invokes when Ht leaves from the hint mode.
131 | *
132 | * @param function notifyLeaveModeCallback Callback method.
133 | */
134 | HintModeProcessor.prototype.onNotifyLeaveMode = function(notifyLeaveModeCallback) {
135 | this.notifyLeaveModeCallback = notifyLeaveModeCallback;
136 | };
137 |
138 | /**
139 | * Start searching in the hint mode.
140 | *
141 | * @param string currentKey
142 | */
143 | HintModeProcessor.prototype.startSearching = function(currentKey) {
144 | if (!this.searching) {
145 | this.commandBox.setHeaderText(this.getExtendAction());
146 | this.searching = true;
147 | }
148 |
149 | if (!this.commandBox.isFocused()) {
150 | this.commandBox.show().setText(currentKey);
151 | }
152 |
153 | var context = this;
154 | setTimeout(function() {
155 | context.searchHints(context.commandBox.getText(), currentKey);
156 | }, 10);
157 | };
158 |
159 | /**
160 | * Search texts in all hints and regenerate hints only matched by a search text.
161 | * An element will be confirmed if there is only one regenerated hint.
162 | *
163 | * @param string text A search text.
164 | * @param string currentkey
165 | */
166 | HintModeProcessor.prototype.searchHints = function(text, currentKey) {
167 | var regenerateElements = this.hintElements.regenerateHintsBy(text.toLowerCase());
168 | if (regenerateElements.length == 1 && Opt.hint_auto_confirm) {
169 | this.confirm(regenerateElements[0].getElement());
170 | } else {
171 | if (currentKey == 'Enter') {
172 | var matchedElement = this.hintElements.getFilteringMatchedElement();
173 | if (matchedElement) {
174 | this.confirm(matchedElement);
175 | }
176 | }
177 | }
178 | };
179 |
--------------------------------------------------------------------------------
/lib/jp.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Library for japanese character Romaji, Hiragana, Katakana.
3 | */
4 | var Jp = {};
5 |
6 | /**
7 | * Conversion map of alphabet to hiragana.
8 | */
9 | Jp.conversionMap = {
10 | 'a': 'あ', 'i': 'い', 'u': 'う', 'e': 'え', 'o': 'お',
11 | 'k': { 'a': 'か', 'i': 'き', 'u': 'く', 'e': 'け', 'o': 'こ',
12 | 'y': { 'a': 'きゃ', 'i': 'きぃ', 'u': 'きゅ', 'e': 'きぇ', 'o': 'きょ' }
13 | },
14 | 's': { 'a': 'さ', 'i': 'し', 'u': 'す', 'e': 'せ', 'o': 'そ',
15 | 'y': { 'a': 'しゃ', 'i': 'しぃ', 'u': 'しゅ', 'e': 'しぇ', 'o': 'しょ' },
16 | 'h': { 'a': 'しゃ', 'i': 'し', 'u': 'しゅ', 'e': 'しぇ', 'o': 'しょ' }
17 | },
18 | 't': { 'a': 'た', 'i': 'ち', 'u': 'つ', 'e': 'て', 'o': 'と',
19 | 'y': { 'a': 'ちゃ', 'i': 'ちぃ', 'u': 'ちゅ', 'e': 'ちぇ', 'o': 'ちょ' },
20 | 'h': { 'a': 'てゃ', 'i': 'てぃ', 'u': 'てゅ', 'e': 'てぇ', 'o': 'てょ' }
21 | },
22 | 'n': { 'a': 'な', 'i': 'に', 'u': 'ぬ', 'e': 'ね', 'o': 'の',
23 | 'y': { 'a': 'にゃ', 'i': 'にぃ', 'u': 'にゅ', 'e': 'にぇ', 'o': 'にょ' },
24 | 'n': 'ん',
25 | },
26 | 'h': { 'a': 'は', 'i': 'ひ', 'u': 'ふ', 'e': 'へ', 'o': 'ほ',
27 | 'y': { 'a': 'ひゃ', 'i': 'ひぃ', 'u': 'ひゅ', 'e': 'ひぇ', 'o': 'ひょ' }
28 | },
29 | 'm': { 'a': 'ま', 'i': 'み', 'u': 'む', 'e': 'め', 'o': 'も',
30 | 'y': { 'a': 'みゃ', 'i': 'みぃ', 'u': 'みゅ', 'e': 'みぇ', 'o': 'みょ' }
31 | },
32 | 'y': { 'a': 'や', 'u': 'ゆ', 'o': 'よ' },
33 | 'r': { 'a': 'ら', 'i': 'り', 'u': 'る', 'e': 'れ', 'o': 'ろ',
34 | 'y': { 'a': 'りゃ', 'i': 'りぃ', 'u': 'りゅ', 'e': 'りぇ', 'o': 'りょ' }
35 | },
36 | 'w': { 'a': 'わ', 'i': 'うぃ', 'u': 'う', 'e': 'うぇ', 'o': 'を' },
37 | 'g': { 'a': 'が', 'i': 'ぎ', 'u': 'ぐ', 'e': 'げ', 'o': 'ご',
38 | 'y': { 'a': 'ぎゃ', 'i': 'ぎぃ', 'u': 'ぎゅ', 'e': 'ぎぇ', 'o': 'ぎょ' }
39 | },
40 | 'z': { 'a': 'ざ', 'i': 'じ', 'u': 'ず', 'e': 'ぜ', 'o': 'ぞ',
41 | 'y': { 'a': 'じゃ', 'i': 'じぃ', 'u': 'じゅ', 'e': 'じぇ', 'o': 'じょ' }
42 | },
43 | 'j': { 'a': 'じゃ', 'i': 'じ', 'u': 'じゅ', 'e': 'じぇ', 'o': 'じょ' },
44 | 'd': { 'a': 'だ', 'i': 'ぢ', 'u': 'づ', 'e': 'で', 'o': 'ど',
45 | 'y': { 'a': 'ぢゃ', 'i': 'ぢぃ', 'u': 'ぢゅ', 'e': 'ぢぇ', 'o': 'ぢょ' },
46 | 'h': { 'a': 'でゃ', 'i': 'でぃ', 'u': 'でゅ', 'e': 'でぇ', 'o': 'でょ' }
47 | },
48 | 'b': { 'a': 'ば', 'i': 'び', 'u': 'ぶ', 'e': 'べ', 'o': 'ぼ',
49 | 'y': { 'a': 'びゃ', 'i': 'びぃ', 'u': 'びゅ', 'e': 'びぇ', 'o': 'びょ' }
50 | },
51 | 'p': { 'a': 'ぱ', 'i': 'ぴ', 'u': 'ぷ', 'e': 'ぺ', 'o': 'ぽ',
52 | 'y': { 'a': 'ぴゃ', 'i': 'ぴぃ', 'u': 'ぴゅ', 'e': 'ぴぇ', 'o': 'ぴょ' }
53 | },
54 | 'c': { 'a': 'か', 'i': 'し', 'u': 'く', 'e': 'せ', 'o': 'こ',
55 | 'y': { 'a': 'ちゃ', 'i': 'ちぃ', 'u': 'ちゅ', 'e': 'ちぇ', 'o': 'ちょ' },
56 | 'h': { 'a': 'ちゃ', 'i': 'ち', 'u': 'ちゅ', 'e': 'ちぇ', 'o': 'ちょ' }
57 | },
58 | 'q': { 'a': 'くぁ', 'i': 'くぃ', 'u': 'く', 'e': 'くぇ', 'o': 'くぉ' },
59 | 'f': { 'a': 'ふぁ', 'i': 'ふぃ', 'u': 'ふ', 'e': 'ふぇ', 'o': 'ふぉ' },
60 | 'l': { 'a': 'ぁ', 'i': 'ぃ', 'u': 'ぅ', 'e': 'ぇ', 'o': 'ぉ' },
61 | 'x': { 'a': 'ぁ', 'i': 'ぃ', 'u': 'ぅ', 'e': 'ぇ', 'o': 'ぉ' },
62 | 'v': { 'a': 'ヴぁ', 'i': 'ヴぃ', 'u': 'ヴ', 'e': 'ヴぇ', 'o': 'ヴぉ' }
63 | };
64 |
65 | /**
66 | * Get hiragana candidates.
67 | *
68 | * For example:
69 | * If romaji is 'sar', then return 'さら', 'さり', 'さる', 'され', 'さろ'
70 | * If romaji is 'kak', then return 'かか', 'かき', 'かく', 'かけ', 'かこ'
71 | *
72 | * @param string romaji A romaji string.
73 | * @return array Hiragana candidates.
74 | */
75 | Jp.getHiraganaCandidates = function(romaji) {
76 | var hiragana = Jp.romaji2hiragana(romaji);
77 | var halfRoman, candidate = [];
78 |
79 | var e = /([ywjqflxv]|[knhmrgzbp]y?|[stdc][hy]?)$/;
80 | if (halfRoman = hiragana.match(e)) {
81 | hiragana = hiragana.replace(e, '');
82 |
83 | var converted = Jp.convertHalfRomanToHiragana(halfRoman[0]);
84 | for (var i = 0; i < converted.length; i++) {
85 | candidate.push(hiragana + converted[i]);
86 | }
87 | } else {
88 | candidate.push(hiragana);
89 | }
90 |
91 | return candidate;
92 | };
93 |
94 | /**
95 | * Convert hiragana to katakana.
96 | *
97 | * @param string target A string that converts from hiragana to katakana.
98 | */
99 | Jp.toKatakana = function(hiragana) {
100 | return hiragana.replace(/[ぁ-ん]/g, function(s) {
101 | return String.fromCharCode(s.charCodeAt(0) + 0x60);
102 | });
103 | };
104 |
105 | /**
106 | * Convert half romaji to hiragana.
107 | *
108 | * For example:
109 | * If romaji is 'a', then return 'あ'
110 | * If romaji is 'k', then return 'か', 'き', 'く', 'け', 'こ', 'きゃ', 'きぃ', 'きゅ', 'きぇ', 'きょ'
111 | * If romaji is 'ky', then return 'きゃ', 'きぃ', 'きゅ', 'きぇ', 'きょ'
112 | *
113 | * @param string romaji A romaji string.
114 | * @return array Hiragana candidates.
115 | */
116 | Jp.convertHalfRomanToHiragana = function(romaji) {
117 | var candidate = [];
118 |
119 | if (romaji.length == 1) {
120 | var map = Jp.conversionMap[romaji];
121 | if (typeof map == 'object') {
122 | for (var i in map) {
123 | if (typeof map[i] == 'object') {
124 | for (var j in map[i]) {
125 | candidate.push(map[i][j]);
126 | }
127 | } else {
128 | candidate.push(map[i]);
129 | }
130 | }
131 | } else {
132 | candidate.push(map);
133 | }
134 | } else if (romaji.length == 2) {
135 | var map = Jp.conversionMap[romaji.charAt(0)][romaji.charAt(1)];
136 | for (var i in map) {
137 | candidate.push(map[i]);
138 | }
139 | }
140 |
141 | return candidate;
142 | };
143 |
144 | /**
145 | * Convert a romaji string to a hiragana.
146 | *
147 | * @param string romaji A string you want to convert.
148 | */
149 | Jp.romaji2hiragana = function(romaji) {
150 | var hiragana = '';
151 |
152 | for (var i = 0; i < romaji.length; i++) {
153 | var str = romaji.charAt(i),
154 | map = Jp.conversionMap[str];
155 |
156 | while (true) {
157 | if (typeof map == 'string') {
158 | break;
159 | } else {
160 | var c = romaji.charAt(++i);
161 | if (!map || !(map = map[c])) {
162 | i--;
163 | break;
164 | }
165 | str += c;
166 | }
167 | }
168 |
169 | if (map) {
170 | hiragana += map;
171 | } else if (str == romaji.charAt(i + 1)) {
172 | hiragana += 'っ';
173 | } else if (str == 'n') {
174 | hiragana += 'ん';
175 | } else {
176 | hiragana += str;
177 | }
178 | }
179 |
180 | return hiragana;
181 | };
182 |
--------------------------------------------------------------------------------
/css/main.css:
--------------------------------------------------------------------------------
1 | .hometype-hit-a-hint-base {
2 | position:absolute;
3 | padding:2px;
4 | font-size:12px;
5 | color:#333;
6 | z-index:1000001;
7 | line-height:100%;
8 | font-family:'Osaka-Mono';
9 | }
10 | .hometype-hit-a-hint-base span {
11 | font-family:'Osaka-Mono';
12 | }
13 | .hometype-hit-a-hint-border {
14 | border:dashed 1px #900;
15 | position:absolute;
16 | z-index:1000000;
17 | }
18 | .hometype-hit-a-hint-yellow {
19 | background-color:#fe0;
20 | }
21 | .hometype-hit-a-hint-yellow-area {
22 | background-color:#FDFFC3;
23 | }
24 | .hometype-hit-a-hint-blue {
25 | background-color:#81b9ff;
26 | }
27 | .hometype-hit-a-hint-blue-area {
28 | background-color:#c5e3ff;
29 | }
30 | .hometype-hit-a-hint-green {
31 | background-color:#B2FFC7;
32 | }
33 | .hometype-hit-a-hint-green-area {
34 | background-color:#E2FFE3;
35 | }
36 | .hometype-hit-a-hint-red {
37 | background-color:#ff8bb9;
38 | }
39 | .hometype-hit-a-hint-head-area, .hometype-hit-a-hint-red-area {
40 | background-color:#ffd3e0;
41 | }
42 | .hometype-hit-a-hint-pushed {
43 | color:red;
44 | }
45 |
46 | .hometype-matched-text, .hometype-char-text {
47 | background-color:#fc3;
48 | color:#f22;
49 | }
50 | .hometype-char-text {
51 | padding: 0 4px;
52 | }
53 |
54 | .hometype-help-box {
55 | position:absolute;
56 | z-index:1000000;
57 | display:none;
58 | opacity:0.6;
59 | background-color: black;
60 | color: white;
61 | }
62 | .hometype-help-box-wrap {
63 | display: -webkit-box;
64 | }
65 | .hometype-help-box-commands-key {
66 | text-align: right;
67 | color: #dd0;
68 | }
69 | .hometype-command-box {
70 | position:absolute;
71 | z-index:1000002;
72 | display:none;
73 | height:34px;
74 | padding:0;
75 | margin:0;
76 | width:100%;
77 | height:22px;
78 | border-style:solid;
79 | border-width:1px 0 0 0;
80 | border-color:#aaa;
81 | box-sizing:content-box !important;
82 | background-color:#fff !important;
83 | }
84 | .hometype-command-box-header {
85 | float:left !important;
86 | height:22px !important;
87 | line-height:22px !important;
88 | font-size:14px !important;
89 | padding:1px 5px 0px 5px !important;
90 | color:#f7e8ad !important;
91 | background-color:#62a609 !important;
92 | font-family:'Osaka-Mono' !important;
93 | box-sizing:border-box !important;
94 | }
95 | .hometype-command-box-content {
96 | background-color:transparent !important;
97 | overflow:hidden !important;
98 | height:100% !important;
99 | box-sizing:content-box !important;
100 | }
101 | .hometype-command-box-triangle {
102 | float:left !important;
103 | width:0 !important;
104 | height:0 !important;
105 | border-top: 11px solid transparent !important;
106 | border-bottom: 11px solid transparent !important;
107 | border-left: 11px solid #62a609 !important;
108 | border-right: 2px solid transparent !important;
109 | }
110 | .hometype-command-box input {
111 | width:100% !important;
112 | height:100% !important;
113 | border-width:0 !important;
114 | border:0 solid #fff !important;
115 | margin:0 !important;
116 | padding:3px !important;
117 | height:22px !important;
118 | font-size:14px !important;
119 | background-color:transparent !important;
120 | font-family:'Osaka-Mono' !important;
121 | outline:none !important;
122 | box-sizing:border-box !important;
123 | min-height:22px !important;
124 | border-radius:0 !important;
125 | display:block !important;
126 | color:#333 !important;
127 | box-shadow:0 !important;
128 | }
129 | .hometype-command-box-candidate-area {
130 | border-style:solid;
131 | border-width:1px 0 0 0;
132 | border-color:#aaa;
133 | background-color:#fff;
134 | position:absolute;
135 | z-index:1000002;
136 | display:none;
137 | padding:0;
138 | box-sizing:border-box;
139 | font-size:12px !important;
140 | }
141 | .hometype-command-box-candidate-area div {
142 | border-style:dotted;
143 | border-width:0 0 1px 0;
144 | border-color:#aaa;
145 | background-color:transparent;
146 | color:#333;
147 | text-align:left;
148 | font-size:11px;
149 | font-family:'Osaka-Mono';
150 | padding:2px;
151 | height:20px;
152 | width:100%;
153 | word-break:break-all;
154 | word-wrap:break-word;
155 | overflow:hidden;
156 | box-sizing:border-box;
157 | line-height: 20px;
158 | }
159 | .hometype-command-box-candidate-area div.selected {
160 | color:#444;
161 | background-color:#ffa;
162 | }
163 | .hometype-command-box-candidate-area div.hometype-no-candidate {
164 | color:#d00;
165 | background-color:#ffa;
166 | }
167 | .hometype-command-box-icon {
168 | float:left !important;
169 | margin:0 4px 0 0 !important;
170 | display:block !important;
171 | }
172 |
173 | .hometype-select-box {
174 | width:400px;
175 | height:600px;
176 | z-index:10000;
177 | background-color:#fff;
178 | overflow:auto;
179 | border-radius:4px;
180 | border:solid 1px #ccc;
181 | padding:10px;
182 | }
183 | .hometype-select-box ul, .hometype-select-box li {
184 | list-style:none;
185 | }
186 | .hometype-select-box li {
187 | float:left;
188 | line-height:200%;
189 | font-size:14px;
190 | margin:5px;
191 | padding:0 10px;
192 | border:solid 1px #999;
193 | border-radius:4px;
194 | cursor:pointer;
195 | background: linear-gradient(to bottom, #ffffff 0%,#f3f3f3 50%,#ededed 51%,#ffffff 100%);
196 | }
197 |
198 | .hometype-contenteditable {
199 | border:solid 1px #f00;
200 | position:absolute;
201 | z-index:1000000;
202 | background-color:transparent;
203 | cursor:text;
204 | }
205 |
206 | .hometype-current-mode {
207 | position:fixed;
208 | z-index:1000002;
209 | top:6px;
210 | right:6px;
211 | font-family:'Osaka-Mono';
212 | background: linear-gradient(to bottom, #fefcea 0%,#f1da36 100%);
213 | box-shadow:0 0 2px 2px rgba(200,200,200,0.2) inset;
214 | padding:2px 6px;
215 | border-radius:4px;
216 | border:solid 1px #cecece;
217 | font-size:14px;
218 | color:#222;
219 | }
220 |
221 | .hometype-tab-list-box {
222 | position: absolute;
223 | width: 900px;
224 | background-color: #fff !important;;
225 | text-align: left;
226 | opacity: 0.9;
227 | padding: 4px 10px;
228 | border-radius: 4px;
229 | z-index: 1000000 !important;
230 | border: solid 1px #888 !important;
231 | font-size: 14px !important;
232 | }
233 | .hometype-tab-element {
234 | float: left;
235 | width: 190px;
236 | height: 30px;
237 | line-height: 30px;
238 | overflow: hidden;
239 | text-overflow: ellipsis;
240 | white-space: nowrap;
241 | position: relative;
242 | margin-right: 10px;
243 | font-family:'Osaka-Mono' !important;
244 | box-sizing: border-box !important;
245 | }
246 | .hometype-tab-element-hint-key {
247 | color: #f00;
248 | }
249 | .hometype-tab-element img {
250 | position: absolute;
251 | top: 7px;
252 | left: 0;
253 | }
254 |
--------------------------------------------------------------------------------
/spec/support/jasmine-2.0.0/boot.js:
--------------------------------------------------------------------------------
1 | /**
2 | Starting with version 2.0, this file "boots" Jasmine, performing all of the necessary initialization before executing the loaded environment and all of a project's specs. This file should be loaded after `jasmine.js`, but before any project source files or spec files are loaded. Thus this file can also be used to customize Jasmine for a project.
3 |
4 | If a project is using Jasmine via the standalone distribution, this file can be customized directly. If a project is using Jasmine via the [Ruby gem][jasmine-gem], this file can be copied into the support directory via `jasmine copy_boot_js`. Other environments (e.g., Python) will have different mechanisms.
5 |
6 | The location of `boot.js` can be specified and/or overridden in `jasmine.yml`.
7 |
8 | [jasmine-gem]: http://github.com/pivotal/jasmine-gem
9 | */
10 |
11 | (function() {
12 |
13 | /**
14 | * ## Require & Instantiate
15 | *
16 | * Require Jasmine's core files. Specifically, this requires and attaches all of Jasmine's code to the `jasmine` reference.
17 | */
18 | window.jasmine = jasmineRequire.core(jasmineRequire);
19 |
20 | /**
21 | * Since this is being run in a browser and the results should populate to an HTML page, require the HTML-specific Jasmine code, injecting the same reference.
22 | */
23 | jasmineRequire.html(jasmine);
24 |
25 | /**
26 | * Create the Jasmine environment. This is used to run all specs in a project.
27 | */
28 | var env = jasmine.getEnv();
29 |
30 | /**
31 | * ## The Global Interface
32 | *
33 | * Build up the functions that will be exposed as the Jasmine public interface. A project can customize, rename or alias any of these functions as desired, provided the implementation remains unchanged.
34 | */
35 | var jasmineInterface = {
36 | describe: function(description, specDefinitions) {
37 | return env.describe(description, specDefinitions);
38 | },
39 |
40 | xdescribe: function(description, specDefinitions) {
41 | return env.xdescribe(description, specDefinitions);
42 | },
43 |
44 | it: function(desc, func) {
45 | return env.it(desc, func);
46 | },
47 |
48 | xit: function(desc, func) {
49 | return env.xit(desc, func);
50 | },
51 |
52 | beforeEach: function(beforeEachFunction) {
53 | return env.beforeEach(beforeEachFunction);
54 | },
55 |
56 | afterEach: function(afterEachFunction) {
57 | return env.afterEach(afterEachFunction);
58 | },
59 |
60 | expect: function(actual) {
61 | return env.expect(actual);
62 | },
63 |
64 | pending: function() {
65 | return env.pending();
66 | },
67 |
68 | spyOn: function(obj, methodName) {
69 | return env.spyOn(obj, methodName);
70 | },
71 |
72 | jsApiReporter: new jasmine.JsApiReporter({
73 | timer: new jasmine.Timer()
74 | })
75 | };
76 |
77 | /**
78 | * Add all of the Jasmine global/public interface to the proper global, so a project can use the public interface directly. For example, calling `describe` in specs instead of `jasmine.getEnv().describe`.
79 | */
80 | if (typeof window == "undefined" && typeof exports == "object") {
81 | extend(exports, jasmineInterface);
82 | } else {
83 | extend(window, jasmineInterface);
84 | }
85 |
86 | /**
87 | * Expose the interface for adding custom equality testers.
88 | */
89 | jasmine.addCustomEqualityTester = function(tester) {
90 | env.addCustomEqualityTester(tester);
91 | };
92 |
93 | /**
94 | * Expose the interface for adding custom expectation matchers
95 | */
96 | jasmine.addMatchers = function(matchers) {
97 | return env.addMatchers(matchers);
98 | };
99 |
100 | /**
101 | * Expose the mock interface for the JavaScript timeout functions
102 | */
103 | jasmine.clock = function() {
104 | return env.clock;
105 | };
106 |
107 | /**
108 | * ## Runner Parameters
109 | *
110 | * More browser specific code - wrap the query string in an object and to allow for getting/setting parameters from the runner user interface.
111 | */
112 |
113 | var queryString = new jasmine.QueryString({
114 | getWindowLocation: function() { return window.location; }
115 | });
116 |
117 | var catchingExceptions = queryString.getParam("catch");
118 | env.catchExceptions(typeof catchingExceptions === "undefined" ? true : catchingExceptions);
119 |
120 | /**
121 | * ## Reporters
122 | * The `HtmlReporter` builds all of the HTML UI for the runner page. This reporter paints the dots, stars, and x's for specs, as well as all spec names and all failures (if any).
123 | */
124 | var htmlReporter = new jasmine.HtmlReporter({
125 | env: env,
126 | onRaiseExceptionsClick: function() { queryString.setParam("catch", !env.catchingExceptions()); },
127 | getContainer: function() { return document.body; },
128 | createElement: function() { return document.createElement.apply(document, arguments); },
129 | createTextNode: function() { return document.createTextNode.apply(document, arguments); },
130 | timer: new jasmine.Timer()
131 | });
132 |
133 | /**
134 | * The `jsApiReporter` also receives spec results, and is used by any environment that needs to extract the results from JavaScript.
135 | */
136 | env.addReporter(jasmineInterface.jsApiReporter);
137 | env.addReporter(htmlReporter);
138 |
139 | /**
140 | * Filter which specs will be run by matching the start of the full name against the `spec` query param.
141 | */
142 | var specFilter = new jasmine.HtmlSpecFilter({
143 | filterString: function() { return queryString.getParam("spec"); }
144 | });
145 |
146 | env.specFilter = function(spec) {
147 | return specFilter.matches(spec.getFullName());
148 | };
149 |
150 | /**
151 | * Setting up timing functions to be able to be overridden. Certain browsers (Safari, IE 8, phantomjs) require this hack.
152 | */
153 | window.setTimeout = window.setTimeout;
154 | window.setInterval = window.setInterval;
155 | window.clearTimeout = window.clearTimeout;
156 | window.clearInterval = window.clearInterval;
157 |
158 | /**
159 | * ## Execution
160 | *
161 | * Replace the browser window's `onload`, ensure it's called, and then run all of the loaded specs. This includes initializing the `HtmlReporter` instance and then executing the loaded Jasmine environment. All of this will happen after all of the specs are loaded.
162 | */
163 | var currentWindowOnload = window.onload;
164 |
165 | window.onload = function() {
166 | if (currentWindowOnload) {
167 | currentWindowOnload();
168 | }
169 | htmlReporter.initialize();
170 | env.execute();
171 | };
172 |
173 | /**
174 | * Helper function for readability above.
175 | */
176 | function extend(destination, source) {
177 | for (var property in source) destination[property] = source[property];
178 | return destination;
179 | }
180 |
181 | }());
182 |
--------------------------------------------------------------------------------
/js/box/command.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2013 Kengo Tateishi (@tkengo)
3 | * Licensed under MIT license.
4 | * http://www.opensource.org/licenses/mit-license.php
5 | *
6 | * Manage command box that is shown bottom of the display when enter the command mode.
7 | */
8 | var CANDIDATE_AREA_HEIGHT = 180;
9 | var COMMAND_BOX_MARGIN = 0;
10 | var CANDIDATE_MAX_COUNT = 20;
11 |
12 | /**
13 | * Constructor
14 | */
15 | var HometypeCommandBox = function(header) {
16 | var windowWidth = Viewport.getWindowSize().width;
17 |
18 | // Create command box elements.
19 | var box = $(' ') .addClass('hometype-command-box');
20 | var header = $(' ') .addClass('hometype-command-box-header')
21 | .text(header || 'Command')
22 | .appendTo(box);
23 | var triangle = $(' ') .addClass('hometype-command-box-triangle')
24 | .appendTo(box);
25 | var content = $(' ') .addClass('hometype-command-box-content')
26 | .appendTo(box);
27 | var text = $(' ').attr('type', 'text')
28 | .appendTo(content);
29 | var candidate = $(' ') .addClass('hometype-command-box-candidate-area')
30 | .width(windowWidth)
31 | .height(CANDIDATE_AREA_HEIGHT)
32 |
33 | this.list = [];
34 | this.box = box;
35 | this.header = header;
36 | this.text = text;
37 | this.candidate = candidate;
38 |
39 | // Append created elements to body
40 | $(document).ready($.proxy(function() {
41 | this.box.appendTo($('body'));
42 | this.candidate.appendTo($('body'));
43 | }, this));
44 | };
45 |
46 | /**
47 | * Show command box.
48 | */
49 | HometypeCommandBox.prototype.show = function() {
50 | // Calculate position.
51 | var scrollTop = Viewport.getWindowSize().height + Viewport.getScrollPosition().top;
52 |
53 | // Place command box to calculated position and show it.
54 | this.box.css({
55 | top: scrollTop - (this.box.outerHeight() + COMMAND_BOX_MARGIN * 3),
56 | left: COMMAND_BOX_MARGIN
57 | }).show();
58 |
59 | this.text.focus();
60 |
61 | return this;
62 | };
63 |
64 | /**
65 | * Hide command box.
66 | */
67 | HometypeCommandBox.prototype.hide = function() {
68 | this.box.hide();
69 | this.candidate.hide();
70 | $('div', this.candidate).remove();
71 | this.text.val('').blur();
72 | };
73 |
74 | /**
75 | * Show candidate list.
76 | */
77 | HometypeCommandBox.prototype.showCandidate = function() {
78 | if (!this.candidate.is(':visible')) {
79 | this.recalculateAndSetPosition();
80 | this.candidate.hide().show();
81 | }
82 | };
83 |
84 | /**
85 | * Recalculate position of candidate list element and move its.
86 | */
87 | HometypeCommandBox.prototype.recalculateAndSetPosition = function() {
88 | // Show at out of window because size of an element can't be retrieved.
89 | this.candidate.css({ top: -9999, left: -9999 }).show();
90 |
91 | // Get the scroll position.
92 | var scrollTop = Viewport.getWindowSize().height + Viewport.getScrollPosition().top;
93 |
94 | // Set candidate list size and position.
95 | var children = this.candidate.children();
96 | this.candidate.height($(children[0]).outerHeight() * children.length);
97 | this.candidate.css({
98 | top: scrollTop - this.candidate.height() - this.box.outerHeight() - COMMAND_BOX_MARGIN * 4,
99 | left: COMMAND_BOX_MARGIN
100 | });
101 | };
102 |
103 | /**
104 | * Set candidate list.
105 | *
106 | * @param array list Candidate list.
107 | */
108 | HometypeCommandBox.prototype.setCandidate = function(list) {
109 | $('div', this.candidate).remove();
110 |
111 | this.list = list;
112 |
113 | if (list.length == 0) {
114 | var div = $(' ').html('nothing').addClass('hometype-no-candidate');
115 | this.candidate.append(div);
116 | }
117 |
118 | for (var i = 0; i < list.length; i++) {
119 | var item = list[i];
120 | var text = typeof item == 'string' ? item : item.text;
121 |
122 | if (typeof item.escape == 'undefined') {
123 | item.escape = true;
124 | }
125 | var div = $(' ').html(item.escape ? Dom.escapeHTML(text) : text).attr('data-index', i);
126 |
127 | // if (this.text.val() != '') {
128 | // Dom.highlight(div.get(0), this.text.val(), { ignoreCase: true });
129 | // }
130 | for (var j = 0; item.highlights && j < item.highlights.length; j++) {
131 | Dom.highlight(div.get(0), item.highlights[j], { ignoreCase: true });
132 | }
133 |
134 | if (item.icon) {
135 | var icon = $(' ![]() ').attr('src', item.icon).attr('width', '16').addClass('hometype-command-box-icon');
136 | icon.prependTo(div);
137 | }
138 |
139 | // Selected first item.
140 | if (i == 0) {
141 | div.addClass('selected');
142 | }
143 | this.candidate.append(div);
144 |
145 | // Break if item count was over max count.
146 | if (i == CANDIDATE_MAX_COUNT) {
147 | break;
148 | }
149 | }
150 |
151 | if (this.candidate.is(':visible')) {
152 | this.recalculateAndSetPosition();
153 | }
154 | };
155 |
156 | /**
157 | * Select next candidate.
158 | */
159 | HometypeCommandBox.prototype.selectNext = function() {
160 | var div = $('div.selected', this.candidate);
161 | div.removeClass('selected');
162 | var next = div.next();
163 | if (next.length > 0) {
164 | next.addClass('selected');
165 | }
166 | else {
167 | $('div:first', this.candidate).addClass('selected');
168 | }
169 | };
170 |
171 | /**
172 | * Select previous candidate.
173 | */
174 | HometypeCommandBox.prototype.selectPrev = function() {
175 | var div = $('div.selected', this.candidate);
176 | div.removeClass('selected');
177 | var prev = div.prev();
178 | if (prev.length > 0) {
179 | prev.addClass('selected');
180 | }
181 | else {
182 | $('div:last', this.candidate).addClass('selected');
183 | }
184 | };
185 |
186 | /**
187 | * Get a selected candidate.
188 | */
189 | HometypeCommandBox.prototype.getSelected = function() {
190 | var div = $('div.selected', this.candidate);
191 | return this.list[div.attr('data-index')];
192 | };
193 |
194 | /**
195 | * Get a text in the command box.
196 | */
197 | HometypeCommandBox.prototype.getText = function() {
198 | return this.text.val();
199 | };
200 |
201 | /**
202 | * Set a text to the command box.
203 | */
204 | HometypeCommandBox.prototype.setText = function(text) {
205 | this.text.val(text);
206 | };
207 |
208 | /**
209 | * Set a text to the header of the command box.
210 | */
211 | HometypeCommandBox.prototype.setHeaderText = function(text) {
212 | this.header.text(text);
213 | return this;
214 | };
215 |
216 | /**
217 | * Get a text in the header of the command box.
218 | */
219 | HometypeCommandBox.prototype.getHeaderText = function() {
220 | return this.header.text();
221 | };
222 |
223 | /**
224 | * Check if text box has focus.
225 | */
226 | HometypeCommandBox.prototype.isFocused = function() {
227 | return document.activeElement == this.text.get(0);
228 | };
229 |
--------------------------------------------------------------------------------
/options/options.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Hometype Option
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
20 |
23 |
24 |
25 |
31 |
46 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 | Hint Characters
95 |
96 | Hint characters is used as a hint tip key when Hometype shifts the hint mode. This is CASE SENSITIVE.
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 | Key Bindings
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
--------------------------------------------------------------------------------
/lib/dom.js:
--------------------------------------------------------------------------------
1 | var Dom = {};
2 |
3 | /**
4 | * Get clickable and insertable and selectable xpath.
5 | * For hint mode.
6 | */
7 | Dom.clickableAndInsertableXPath = function() {
8 | return [
9 | "//a", "//textarea", "//button", "//select", "//area",
10 | "//input[not(@type='hidden' or @disabled or @readonly)]",
11 | "//*[@onclick or @onmousedown or @tabindex or @role='button' or @role='tab' or @role='link' or @contenteditable='true' or @contenteditable='']"
12 | ].join(' | ');
13 | };
14 |
15 | /**
16 | * Get visualable xpath.
17 | * For visual mode.
18 | */
19 | Dom.visualableXPath = function() {
20 | return [
21 | "//code", "//div", "//td", "//th", "//li", "//section", "//h1", "//h2", "//h3", "//h4"
22 | ].join(' | ');
23 | };
24 |
25 | /**
26 | * Search all visible elements in xpath.
27 | *
28 | * @param string xpath
29 | * @return array Elements.
30 | */
31 | Dom.searchVisibleElementsFrom = function(xpath) {
32 | var results = document.evaluate(xpath, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
33 | var targets = [];
34 |
35 | for (var i = 0; i < results.snapshotLength; i++) {
36 | var targetElement = results.snapshotItem(i);
37 | var inspectionElement = targetElement;
38 |
39 | if (targetElement.tagName == 'AREA') {
40 | inspectionElement = Dom.searchParentImageOf(targetElement);
41 | }
42 |
43 | if (!inspectionElement ||
44 | !Dom.isVisibleInDisplay(inspectionElement) ||
45 | !Dom.stayInOverflowHiddenBox(inspectionElement)) {
46 | continue;
47 | }
48 |
49 | targets.push(targetElement);
50 | }
51 |
52 | return targets;
53 | };
54 |
55 | /**
56 | * Check if whether an element is visible in the display.
57 | *
58 | * @param Element element a target element.
59 | * @return boolean return true if it is visible and otherwise, return false.
60 | */
61 | Dom.isVisibleInDisplay = function(element) {
62 | var visibleInOffset = element.offsetWidth > 0 && element.offsetHeight > 0;
63 | if (!visibleInOffset) {
64 | // If the element has floated child, it is visible even if it has zero size
65 | // box bounding.
66 | for (var i = 0; i < element.childNodes.length; i++) {
67 | var child = element.childNodes[i];
68 | if (child && child.nodeName != '#document' && child.nodeName != '#text' && child.nodeName != '#comment') {
69 | var style = window.getComputedStyle(child, null);
70 | if (style && style.getPropertyValue('float') != 'none' && Dom.isVisibleInDisplay(child)) {
71 | return true;
72 | }
73 | }
74 | }
75 |
76 | return false;
77 | }
78 |
79 | var rect = element.getClientRects()[0] || {};
80 | var visibleInPosition = rect.bottom > 0 && rect.top < window.innerHeight &&
81 | rect.right > 0 && rect.left < window.innerWidth;
82 | if (!visibleInPosition) {
83 | return false;
84 | }
85 |
86 | var computedStyle = window.getComputedStyle(element, null);
87 | var visibleInStyle = computedStyle.getPropertyValue('visibility') == 'visible' &&
88 | computedStyle.getPropertyValue('display') != 'none' &&
89 | computedStyle.getPropertyValue('opacity') != '100';
90 | if (!visibleInStyle) {
91 | return false;
92 | }
93 |
94 | return true;
95 | };
96 |
97 | /**
98 | * Check if whether an element is put to inner of the other element.
99 | *
100 | * @param Element outerElement
101 | * @param Element innerElement
102 | * @return boolean return true if innerElement is put to inner of outerElement
103 | * and otherwise, return false.
104 | */
105 | Dom.isInner = function(outerElement, innerElement) {
106 | var outerRect = outerElement.getBoundingClientRect();
107 | var innerRect = innerElement.getBoundingClientRect();
108 |
109 | return outerRect.top <= innerRect.top &&
110 | outerRect.left <= innerRect.left &&
111 | outerRect.bottom >= innerRect.top &&
112 | outerRect.right >= innerRect.left;
113 | };
114 |
115 | Dom.stayInOverflowHiddenBox = function(element) {
116 | var parentNode = element;
117 | var stayInBox = true;
118 | var fixedElement = undefined;
119 |
120 | while (parentNode = parentNode.parentNode) {
121 | if (!parentNode || parentNode.nodeName == '#document' || parentNode.nodeName == '#text') {
122 | break;
123 | }
124 | computedStyle = window.getComputedStyle(parentNode, null);
125 | if (!computedStyle) {
126 | break;
127 | }
128 |
129 | if (computedStyle.getPropertyValue('position') == 'fixed') {
130 | fixedElement = parentNode;
131 | }
132 |
133 | if (computedStyle.getPropertyValue('overflow') == 'hidden') {
134 | var parentNodeRect = parentNode.getBoundingClientRect();
135 | var elementRect = element.getBoundingClientRect();
136 | stayInBox = fixedElement ||
137 | (parentNodeRect.top <= elementRect.top &&
138 | parentNodeRect.left <= elementRect.left &&
139 | parentNodeRect.bottom >= elementRect.top &&
140 | parentNodeRect.right >= elementRect.left);
141 | break;
142 | }
143 | }
144 |
145 | return stayInBox;
146 | };
147 |
148 | /**
149 | * Search parent image of specified area element
150 | *
151 | * @param Element element An area tag element you want to search a parent image element.
152 | * @return Element If a parent element is found return it, otherwise undefined.
153 | */
154 | Dom.searchParentImageOf = function(element) {
155 | var map = element.parentNode;
156 | var xpath = "//img[@usemap='#" + map.name + "']";
157 |
158 | var results = document.evaluate(xpath, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
159 | if (results.snapshotLength > 0) {
160 | return results.snapshotItem(0);
161 | }
162 |
163 | return undefined;
164 | };
165 |
166 | /**
167 | * Check if whether an element is editable.
168 | *
169 | * @param Element element A target element.
170 | * @return boolean Return true if an element is editable, otherwise false.
171 | */
172 | Dom.isEditable = function(element) {
173 | var tagName = element.tagName.toLowerCase();
174 | var editableType = [
175 | 'date', 'datetime', 'datetime-local', 'email', 'month', 'number', 'password',
176 | 'search', 'tel', 'text', 'time', 'url', 'week'
177 | ];
178 |
179 | return element.isContentEditable ||
180 | tagName == 'textarea' ||
181 | (tagName == 'input' && Utility.inArray(editableType, element.type));
182 | };
183 |
184 | /**
185 | * Do HTML escape for a text.
186 | *
187 | * @param string text A target text.
188 | */
189 | Dom.escapeHTML = function(text) {
190 | var pre = document.createElement('pre');
191 | pre.appendChild(document.createTextNode(text));
192 | return pre.innerHTML;
193 | };
194 |
195 | Dom.highlight = function(element, text, option) {
196 | option = option || { ignoreCase: false };
197 |
198 | if (element.nodeType == 3) {
199 | var pos = -1;
200 | if (option.ignoreCase) {
201 | pos = element.data.toLowerCase().indexOf(text.toLowerCase());
202 | } else {
203 | pos = element.data.indexOf(text);
204 | }
205 |
206 | if (pos > -1) {
207 | var matchedNode = element.splitText(pos);
208 | var highlightedNode = document.createElement('span');
209 |
210 | matchedNode.splitText(text.length);
211 | highlightedNode.className = 'hometype-matched-text';
212 | highlightedNode.appendChild(document.createTextNode(matchedNode.data));
213 | element.parentNode.replaceChild(highlightedNode, matchedNode);
214 | return true;
215 | }
216 |
217 | return false;
218 | } else if (element.nodeType == 1 &&
219 | element.childNodes &&
220 | element.className.indexOf('hometype-matched-text') == -1 &&
221 | !element.tagName.match(/(script|style)/i)) {
222 | var nodes = element.childNodes;
223 | for (var i = 0; i < nodes.length; i++) {
224 | if (Dom.highlight(nodes[i], text, option)) {
225 | i++;
226 | }
227 | }
228 | }
229 | };
230 |
231 | Dom.removeHighlight = function(element) {
232 | var results = document.evaluate(".//span[@class='hometype-matched-text']", element, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
233 | for (var i = 0; i < results.snapshotLength; i++) {
234 | var item = results.snapshotItem(i);
235 | var parentNode = item.parentNode;
236 |
237 | parentNode.replaceChild(item.firstChild, item);
238 | parentNode.normalize();
239 | }
240 | };
241 |
--------------------------------------------------------------------------------
/spec/js/homedics_spec.js:
--------------------------------------------------------------------------------
1 | describe('Homedics', function() {
2 | beforeEach(function() {
3 | jasmine.Ajax.install();
4 | });
5 |
6 | afterEach(function() {
7 | jasmine.Ajax.uninstall();
8 | });
9 |
10 | var homedics;
11 | var dict = 'そん:損 存 遜 村 尊 孫 巽 樽';
12 | var alDict = 'my:マイ';
13 | var dictLetter = 's';
14 | var dictUrl = chrome.extension.getURL('dicts/' + dictLetter + '.ml');
15 | var alDictUrl = chrome.extension.getURL('dicts/alphabet.ml');
16 |
17 | beforeEach(function() {
18 | jasmine.Ajax.stubRequest(dictUrl).andReturn({ 'responseText': dict });
19 | jasmine.Ajax.stubRequest(alDictUrl).andReturn({ 'responseText': alDict });
20 | });
21 |
22 | describe('loading of a dictionary', function() {
23 | beforeEach(function() {
24 | Homedics.dict = '';
25 | homedics = new Homedics('');
26 | });
27 |
28 | it('should return a dictionary content', function() {
29 | expect(homedics.loadDict(dictLetter)).toBe(dict);
30 | });
31 |
32 | it('should cache a dictionary content', function() {
33 | expect(homedics.loadDict(dictLetter)).toBe(dict);
34 | jasmine.Ajax.stubRequest(dictUrl).andReturn({ 'responseText': '' });
35 | expect(homedics.loadDict(dictLetter)).toBe(dict);
36 | });
37 |
38 | context('when dict letter does not exist', function() {
39 | var dictLetter = 'x';
40 |
41 | it('should return empty string', function() {
42 | expect(homedics.loadDict(dictLetter)).toBe('');
43 | });
44 | });
45 | });
46 |
47 | describe('matching', function() {
48 | context('when text is one letter', function() {
49 | beforeEach(function() {
50 | homedics = new Homedics('s');
51 | });
52 |
53 | it('should match alphabet', function() {
54 | expect(homedics.match('superstar')).toEqual({ matched: true, matches: [ 's' ], head: true });
55 | expect(homedics.match('lockstar')).toEqual({ matched: true, matches: [ 's' ], head: false });
56 | });
57 |
58 | it('should match hiragana', function() {
59 | expect(homedics.match('さ')) .toEqual({ matched: false, matches: [], head: false });
60 | expect(homedics.match('し')) .toEqual({ matched: false, matches: [], head: false });
61 | expect(homedics.match('す')) .toEqual({ matched: false, matches: [], head: false });
62 | expect(homedics.match('せ')) .toEqual({ matched: false, matches: [], head: false });
63 | expect(homedics.match('そ')) .toEqual({ matched: false, matches: [], head: false });
64 | expect(homedics.match('しゃ')).toEqual({ matched: false, matches: [], head: false });
65 | expect(homedics.match('しぃ')).toEqual({ matched: false, matches: [], head: false });
66 | expect(homedics.match('しゅ')).toEqual({ matched: false, matches: [], head: false });
67 | expect(homedics.match('しぇ')).toEqual({ matched: false, matches: [], head: false });
68 | expect(homedics.match('しょ')).toEqual({ matched: false, matches: [], head: false });
69 | expect(homedics.match('あ')) .toEqual({ matched: false, matches: [], head: false });
70 |
71 | expect(homedics.match('今度すしを食べたい')).toEqual({ matched: false, matches: [], head: false });
72 | });
73 |
74 | it('should match katakana', function() {
75 | expect(homedics.match('サ')) .toEqual({ matched: false, matches: [], head: false });
76 | expect(homedics.match('シ')) .toEqual({ matched: false, matches: [], head: false });
77 | expect(homedics.match('ス')) .toEqual({ matched: false, matches: [], head: false });
78 | expect(homedics.match('セ')) .toEqual({ matched: false, matches: [], head: false });
79 | expect(homedics.match('ソ')) .toEqual({ matched: false, matches: [], head: false });
80 | expect(homedics.match('シャ')).toEqual({ matched: false, matches: [], head: false });
81 | expect(homedics.match('シィ')).toEqual({ matched: false, matches: [], head: false });
82 | expect(homedics.match('シュ')).toEqual({ matched: false, matches: [], head: false });
83 | expect(homedics.match('シェ')).toEqual({ matched: false, matches: [], head: false });
84 | expect(homedics.match('ショ')).toEqual({ matched: false, matches: [], head: false });
85 | expect(homedics.match('ア')) .toEqual({ matched: false, matches: [], head: false });
86 |
87 | expect(homedics.match('あのスーパースター')).toEqual({ matched: false, matches: [], head: false });
88 | });
89 |
90 | it('should match kanji in dictionary', function() {
91 | expect(homedics.match('損')).toEqual({ matched: false, matches: [], head: false });
92 | expect(homedics.match('猿')).toEqual({ matched: false, matches: [], head: false });
93 |
94 | expect(homedics.match('とても損だ')).toEqual({ matched: false, matches: [], head: false });
95 | });
96 | });
97 |
98 | context('when text is two letters', function() {
99 | beforeEach(function() {
100 | homedics = new Homedics('so');
101 | });
102 |
103 | it('should match alphabet', function() {
104 | expect(homedics.match('source')).toEqual({ matched: true, matches: [ 'so' ], head: true });
105 | expect(homedics.match('open source')).toEqual({ matched: true, matches: [ 'so' ], head: false });
106 | });
107 |
108 | it("should match hiragana 'so'", function() {
109 | expect(homedics.match('さ')) .toEqual({ matched: false, matches: [], head: false });
110 | expect(homedics.match('し')) .toEqual({ matched: false, matches: [], head: false });
111 | expect(homedics.match('す')) .toEqual({ matched: false, matches: [], head: false });
112 | expect(homedics.match('せ')) .toEqual({ matched: false, matches: [], head: false });
113 | expect(homedics.match('そ')) .toEqual({ matched: true, matches: [ 'そ' ], head: true });
114 | expect(homedics.match('しゃ')).toEqual({ matched: false, matches: [], head: false });
115 | expect(homedics.match('しぃ')).toEqual({ matched: false, matches: [], head: false });
116 | expect(homedics.match('しゅ')).toEqual({ matched: false, matches: [], head: false });
117 | expect(homedics.match('しぇ')).toEqual({ matched: false, matches: [], head: false });
118 | expect(homedics.match('しょ')).toEqual({ matched: false, matches: [], head: false });
119 |
120 | expect(homedics.match('冷たいそうめん')).toEqual({ matched: true, matches: [ 'そ' ], head: false });
121 | });
122 |
123 | it("should match katakana 'so'", function() {
124 | expect(homedics.match('サ')) .toEqual({ matched: false, matches: [], head: false });
125 | expect(homedics.match('シ')) .toEqual({ matched: false, matches: [], head: false });
126 | expect(homedics.match('ス')) .toEqual({ matched: false, matches: [], head: false });
127 | expect(homedics.match('セ')) .toEqual({ matched: false, matches: [], head: false });
128 | expect(homedics.match('ソ')) .toEqual({ matched: true, matches: [ 'ソ' ], head: true });
129 | expect(homedics.match('シャ')).toEqual({ matched: false, matches: [], head: false });
130 | expect(homedics.match('シィ')).toEqual({ matched: false, matches: [], head: false });
131 | expect(homedics.match('シュ')).toEqual({ matched: false, matches: [], head: false });
132 | expect(homedics.match('シェ')).toEqual({ matched: false, matches: [], head: false });
133 | expect(homedics.match('ショ')).toEqual({ matched: false, matches: [], head: false });
134 |
135 | expect(homedics.match('冷たいソーメン')).toEqual({ matched: true, matches: [ 'ソ' ], head: false });
136 | });
137 |
138 | it('should match kanji in dictionary', function() {
139 | expect(homedics.match('損')).toEqual({ matched: true, matches: [ '損' ], head: true });
140 | expect(homedics.match('猿')).toEqual({ matched: false, matches: [], head: false });
141 |
142 | expect(homedics.match('とても損だ')).toEqual({ matched: true, matches: [ '損' ], head: false });
143 | });
144 | });
145 |
146 | context('when text is in an alphabet dictionary', function() {
147 | beforeEach(function() {
148 | homedics = new Homedics('my');
149 | });
150 |
151 | it("should match 'マイ'", function() {
152 | expect(homedics.match('マイ・オークション')).toEqual({ matched: true, matches: [ 'マイ' ], head: true });
153 | expect(homedics.match('念願のマイホーム')). toEqual({ matched: true, matches: [ 'マイ' ], head: false });
154 | });
155 | });
156 | });
157 | });
158 |
--------------------------------------------------------------------------------
/js/background/script.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2013 Kengo Tateishi (@tkengo)
3 | * Licensed under MIT license.
4 | * http://www.opensource.org/licenses/mit-license.php
5 | *
6 | * Event page script.
7 | * Define commands used through the chrome runtime.
8 | */
9 | var RuntimeCommand = {};
10 |
11 | /**
12 | * ------------------------------------
13 | * OnMessage callback runtime methods.
14 | * ------------------------------------
15 | */
16 |
17 | /**
18 | * Close a tab.
19 | */
20 | RuntimeCommand.closeTab = function(sender, params, response) {
21 | chrome.tabs.remove(sender.tab.id, null);
22 | };
23 |
24 | /**
25 | * Open a url in the current tab or a new tab.
26 | */
27 | RuntimeCommand.openUrl = function(sender, params, response) {
28 | if (params.newWindow) {
29 | chrome.windows.create({ url: params.url, focused: true });
30 | } else if (params.newTab) {
31 | chrome.tabs.create({ url: params.url, active: true });
32 | } else {
33 | chrome.tabs.update({ url: params.url });
34 | }
35 | };
36 |
37 | /**
38 | * Create a new tab.
39 | */
40 | RuntimeCommand.createTab = function(sender, params) {
41 | chrome.tabs.create({ url: params.url, active: true });
42 | };
43 |
44 | /**
45 | * Move the current tab to the left tab.
46 | */
47 | RuntimeCommand.moveLeftTab = function(sender) {
48 | chrome.tabs.query({ currentWindow: true }, function(tabs) {
49 | var index;
50 | if (sender.tab.index == 0) {
51 | index = tabs.length - 1;
52 | }
53 | else {
54 | index = sender.tab.index - 1;
55 | }
56 | chrome.tabs.update(tabs[index].id, { active: true });
57 | });
58 | };
59 |
60 | /**
61 | * Move the current tab to the right tab.
62 | */
63 | RuntimeCommand.moveRightTab = function(sender) {
64 | chrome.tabs.query({ currentWindow: true }, function(tabs) {
65 | var index;
66 | if (sender.tab.index == tabs.length - 1) {
67 | index = 0;
68 | }
69 | else {
70 | index = sender.tab.index + 1;
71 | }
72 | chrome.tabs.update(tabs[index].id, { active: true });
73 | });
74 | };
75 |
76 | /**
77 | * Restore a tab that have specified tab id.
78 | */
79 | RuntimeCommand.restoreTab = function(sender, tabId) {
80 | Tab.openClosedTab(tabId);
81 | };
82 |
83 | /**
84 | * Get the closed tab list.
85 | */
86 | RuntimeCommand.closedTabList = function(sender, params, sendResponse) {
87 | sendResponse(Tab.getClosedTabList());
88 | };
89 |
90 | /**
91 | * Select a tab.
92 | */
93 | RuntimeCommand.selectTab = function(sender, tabId, sendResponse) {
94 | chrome.tabs.update(tabId, { active: true, highlighted: true });
95 | };
96 |
97 | /**
98 | * Set continuous option.
99 | *
100 | * If continuous is true in local storage, Hometype immediately enters the hint
101 | * mode after document ready. So you can follow a link continuous.
102 | */
103 | RuntimeCommand.enterContinuousMode = function(sender, params, sendResponse) {
104 | localStorage.setItem('continuous_tab_id', sender.tab.id);
105 | sendResponse(true);
106 | };
107 | /**
108 | * Unset continuous option.
109 | */
110 | RuntimeCommand.leaveContinuousMode = function(sender, params, sendResponse) {
111 | localStorage.removeItem('continuous_tab_id');
112 | sendResponse(false);
113 | };
114 | /**
115 | * Get continuous state.
116 | */
117 | RuntimeCommand.getContinuousState = function(sender, params, sendResponse) {
118 | sendResponse(localStorage.getItem('continuous_tab_id') == sender.tab.id);
119 | };
120 |
121 | /**
122 | * Set a tab hint key to the document title.
123 | */
124 | RuntimeCommand.setTitleForAllTabs = function(sender, params, sendResponse) {
125 | chrome.tabs.query({ currentWindow: true }, function(tabs) {
126 | for (var i = 0; i < tabs.length; i++) {
127 | var code = "document.title = '[" + params.tab_selection_hint_keys.charAt(i) + "]' + document.title";
128 | chrome.tabs.executeScript(tabs[i].id, { code: code });
129 | }
130 | });
131 | };
132 |
133 | /**
134 | * Reset the document title.
135 | */
136 | RuntimeCommand.resetTitleForAllTabs = function(sender, params, sendResponse) {
137 | chrome.tabs.query({ currentWindow: true }, function(tabs) {
138 | for (var i = 0; i < tabs.length; i++) {
139 | var tab = tabs[i];
140 | if (!tab.url.match(/^chrome.*:\/\//)) {
141 | var code = "document.title = document.title.replace(/^\\[[0-9a-z]\\]/i, '')";
142 | chrome.tabs.executeScript(tab.id, { code: code });
143 | }
144 | }
145 | });
146 | };
147 |
148 | /**
149 | * Launch an application.
150 | */
151 | RuntimeCommand.launchApplication = function(sender, params, sendResponse) {
152 | chrome.management.launchApp(params);
153 | };
154 |
155 | /**
156 | * Copy a text to the clipboard.
157 | */
158 | RuntimeCommand.copyToClipboard = function(sender, params, sendResponse) {
159 | var textArea = document.createElement('textarea');
160 | textArea.style.cssText = 'position:absolute;top:-9999px';
161 |
162 | document.body.appendChild(textArea);
163 |
164 | textArea.value = params;
165 | textArea.select();
166 | document.execCommand('copy');
167 |
168 | document.body.removeChild(textArea);
169 | };
170 |
171 | /**
172 | * Toggle the pin state in the current tab.
173 | */
174 | RuntimeCommand.togglePin = function(sender, params, sendResponse) {
175 | chrome.tabs.get(sender.tab.id, function(tab) {
176 | chrome.tabs.update(sender.tab.id, {
177 | pinned: !tab.pinned
178 | });
179 | });
180 | };
181 |
182 | /**
183 | * ------------------------------------
184 | * OnConnect callback runtime methods.
185 | * ------------------------------------
186 | */
187 |
188 | /**
189 | * Load the history list.
190 | */
191 | RuntimeCommand.loadHistories = function(port) {
192 | var histories = Tab.getHistories(port.sender.tab.id);
193 | convertFaviconsToDataURL(Utility.collect(histories, 'url'), function(results) {
194 | for (var i = 0; i < results.length; i++) {
195 | histories[i].faviconDataUrl = results[i];
196 | }
197 | port.postMessage(histories);
198 | });
199 | };
200 |
201 | /**
202 | * Load tabs.
203 | */
204 | RuntimeCommand.loadTabs = function(port) {
205 | port.onMessage.addListener(function() {
206 | chrome.tabs.query({ currentWindow: true }, function(tabs) {
207 | convertFaviconsToDataURL(Utility.collect(tabs, 'url'), function(results) {
208 | for (var i = 0; i < results.length; i++) {
209 | tabs[i].faviconDataUrl = results[i];
210 | }
211 | port.postMessage(tabs);
212 | });
213 | });
214 | });
215 | };
216 |
217 | /**
218 | * Get the bookmark list.
219 | */
220 | RuntimeCommand.loadBookmarks = function(port) {
221 | port.onMessage.addListener(function() {
222 | chrome.bookmarks.getSubTree('1', function(tree) {
223 | var bookmarks = [];
224 | var find = function(node) {
225 | if (node.children) {
226 | for (var i in node.children) {
227 | if (node.id != '1') {
228 | node.children[i].title = node.title + '/' + node.children[i].title;
229 | }
230 | find(node.children[i]);
231 | }
232 | }
233 | if (node.url) {
234 | bookmarks.push(node);
235 | }
236 | };
237 | find(tree[0]);
238 |
239 | convertFaviconsToDataURL(Utility.collect(bookmarks, 'url'), function(results) {
240 | for (var i = 0; i < results.length; i++) {
241 | bookmarks[i].faviconDataUrl = results[i];
242 | }
243 | port.postMessage(bookmarks);
244 | });
245 | });
246 | });
247 | };
248 |
249 | /**
250 | * Get a list of applications.
251 | */
252 | RuntimeCommand.loadApplications = function(port) {
253 | port.onMessage.addListener(function() {
254 | chrome.management.getAll(function(items) {
255 | var apps = [];
256 | for (var i = 0; i < items.length; i++) {
257 | var item = items[i];
258 | if (item.type.indexOf('_app') > -1) {
259 | apps.push(item);
260 | }
261 | }
262 |
263 | convertFaviconsToDataURL(Utility.collect(apps, 'appLaunchUrl'), function(results) {
264 | for (var i = 0; i < results.length; i++) {
265 | apps[i].faviconDataUrl = results[i];
266 | }
267 | port.postMessage(apps);
268 | });
269 | });
270 | });
271 | };
272 |
273 | (function() {
274 | /**
275 | * This is invoked when called chrome.runtime.sendMessage in content script.
276 | *
277 | * @param object message Message from content script. This should include 'command' key
278 | * to determine invoked runtime method. If this has 'params'
279 | * key, it is passed to the runtime method as a second argument.
280 | * @param object sender The message sender.
281 | * @param function sendResponse The callback method.
282 | */
283 | var messageCallback = function(message, sender, sendResponse) {
284 | var command = RuntimeCommand[message.command];
285 | if (command) {
286 | command.call(command, sender, message.params, sendResponse);
287 | }
288 | };
289 | var connectCallback = function(port) {
290 | var command = RuntimeCommand[port.name];
291 | if (command) {
292 | command.call(command, port);
293 | }
294 | }
295 |
296 | chrome.runtime.onMessage.addListener(messageCallback);
297 | chrome.runtime.onConnect.addListener(connectCallback);
298 | })();
299 |
--------------------------------------------------------------------------------
/js/command.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2013 Kengo Tateishi (@tkengo)
3 | * Licensed under MIT license.
4 | * http://www.opensource.org/licenses/mit-license.php
5 | *
6 | * Define command used in Hometype.
7 | */
8 | var Command = {};
9 |
10 | /**
11 | * Noop command.
12 | */
13 | Command.noop = function() { };
14 |
15 | /**
16 | * Scroll down.
17 | */
18 | Command.scrollDown = function() {
19 | Viewport.scrollVertical(parseInt(Opt.scroll_amount));
20 | };
21 |
22 | /**
23 | * Scroll up.
24 | */
25 | Command.scrollUp = function() {
26 | Viewport.scrollVertical(-parseInt(Opt.scroll_amount));
27 | };
28 |
29 | /**
30 | * Scroll down half display.
31 | */
32 | Command.scrollDownHalf = function() {
33 | Viewport.scrollVertical(Viewport.getWindowSize().height / 2);
34 | };
35 |
36 | /**
37 | * Scroll up half display.
38 | */
39 | Command.scrollUpHalf = function() {
40 | Viewport.scrollVertical(-Viewport.getWindowSize().height / 2);
41 | };
42 |
43 | /**
44 | * Scroll down display.
45 | */
46 | Command.scrollDownPage = function() {
47 | Viewport.scrollVertical(Viewport.getWindowSize().height);
48 | };
49 |
50 | /**
51 | * Scroll up display.
52 | */
53 | Command.scrollUpPage = function() {
54 | Viewport.scrollVertical(-Viewport.getWindowSize().height);
55 | };
56 |
57 | /**
58 | * Scroll to document top.
59 | */
60 | Command.scrollToTop = function() {
61 | Viewport.scrollTo(0, 0);
62 | };
63 |
64 | /**
65 | * Scroll to document bottom.
66 | */
67 | Command.scrollToBottom = function() {
68 | Viewport.scrollTo(0, Viewport.getDocumentSize().height);
69 | };
70 |
71 | /**
72 | * Close the current tab.
73 | */
74 | Command.closeTab = function() {
75 | chrome.runtime.sendMessage({ command: 'closeTab' });
76 | };
77 |
78 | /**
79 | * Move to left tab.
80 | */
81 | Command.moveLeftTab = function() {
82 | chrome.runtime.sendMessage({ command: 'moveLeftTab' });
83 | };
84 |
85 | /**
86 | * Move to right tab.
87 | */
88 | Command.moveRightTab = function() {
89 | chrome.runtime.sendMessage({ command: 'moveRightTab' });
90 | };
91 |
92 | /**
93 | * Select next candidate in a command box.
94 | */
95 | Command.selectNextCandidate = function() {
96 | Mode.getProcessor(ModeList.COMMAND_MODE).getCommandBox().selectNext();
97 | };
98 |
99 | /**
100 | * Select previous candidate in a command box.
101 | */
102 | Command.selectPrevCandidate = function() {
103 | Mode.getProcessor(ModeList.COMMAND_MODE).getCommandBox().selectPrev();
104 | };
105 |
106 | /**
107 | * Focus out from an active element.
108 | */
109 | Command.cancelInsertMode = function() {
110 | $(document.activeElement).blur();
111 | Mode.release();
112 | Mode.changeMode(ModeList.NORMAL_MODE);
113 | };
114 |
115 | /**
116 | * Back history.
117 | */
118 | Command.backHistory = function() {
119 | window.history.back();
120 | };
121 |
122 | /**
123 | * Foward history.
124 | */
125 | Command.forwardHistory = function() {
126 | window.history.forward();
127 | };
128 |
129 | /**
130 | * Restore closed tabs.
131 | */
132 | Command.restoreTab = function() {
133 | chrome.runtime.sendMessage({ command: 'restoreTab' });
134 | };
135 |
136 | /**
137 | * Focus first element in window.
138 | */
139 | Command.focusFirstInput = function() {
140 | $(':insertable:screen:first').focus();
141 | };
142 |
143 | /**
144 | * Focus last element in window.
145 | */
146 | Command.focusLastInput = function() {
147 | $(':insertable:screen:last').focus();
148 | };
149 |
150 | /**
151 | * Yank a url.
152 | */
153 | Command.yankUrl = function() {
154 | chrome.runtime.sendMessage({ command: 'copyToClipboard', params: window.location.href });
155 | };
156 |
157 | /**
158 | * Toggle the pin state in the current tab.
159 | */
160 | Command.togglePin = function() {
161 | chrome.runtime.sendMessage({ command: 'togglePin' });
162 | };
163 |
164 | /**
165 | * Enter the tab selection mode.
166 | */
167 | Command.selectTab = function() {
168 | var processor = Mode.changeMode(ModeList.COMMAND_MODE);
169 | var commandBox = processor.getCommandBox().setHeaderText('Tabs');
170 | var port = chrome.runtime.connect({ name: 'loadTabs' });
171 | var selectTab = function(id) {
172 | Mode.changeMode(ModeList.NORMAL_MODE);
173 | chrome.runtime.sendMessage({ command: 'selectTab', params: id });
174 | };
175 |
176 | port.onMessage.addListener(function(tabs) {
177 | processor.onNotifyLeaveMode(function() {
178 | chrome.runtime.sendMessage({ command: 'resetTitleForAllTabs' });
179 | });
180 | processor.onEnter(function(text, selected) {
181 | selectTab(selected.id);
182 | });
183 | processor.onUpdateBoxText(function(text) {
184 | var index = -1;
185 | if (text) {
186 | index = Opt.tab_selection_hint_keys.indexOf(text.charAt(text.length - 1));
187 | }
188 |
189 | if (index > -1 && tabs[index]) {
190 | selectTab(tabs[index].id);
191 | } else {
192 | return filterTabs(tabs, text);
193 | }
194 | });
195 |
196 | chrome.runtime.sendMessage({
197 | command: 'setTitleForAllTabs',
198 | params: { tab_selection_hint_keys: Opt.tab_selection_hint_keys }
199 | });
200 |
201 | commandBox.setCandidate(filterTabs(tabs, ''));
202 | commandBox.showCandidate();
203 |
204 | port.disconnect();
205 | });
206 | port.postMessage();
207 | };
208 |
209 | /**
210 | * Search a tab from closed tab list.
211 | */
212 | Command.searchClosedTabs = function() {
213 | var processor = Mode.changeMode(ModeList.COMMAND_MODE);
214 | processor.getCommandBox().setHeaderText('ClosedTabs');
215 | processor.onEnter(function(text, selected) {
216 | chrome.runtime.sendMessage({ command: 'restoreTab', params: selected.tabId });
217 | });
218 |
219 | chrome.runtime.sendMessage({ command: 'closedTabList' }, function(closedTabs) {
220 | processor.onUpdateBoxText(function(text) {
221 | return filterClosedTabs(closedTabs, text);
222 | }, true);
223 | });
224 | };
225 |
226 | /**
227 | * Search a bookmark from chrome bookmark list.
228 | */
229 | Command.searchBookmarks = function(option) {
230 | var newTab = option.new || false;
231 |
232 | var processor = Mode.changeMode(ModeList.COMMAND_MODE);
233 | processor.getCommandBox().setHeaderText('Bookmarks');
234 | processor.onEnter(function(text, selected) {
235 | if (newTab) {
236 | chrome.runtime.sendMessage({ command: 'createTab', params: { url: selected.url } });
237 | } else {
238 | window.location.href = selected.url;
239 | }
240 | });
241 |
242 | var port = chrome.runtime.connect({ name: 'loadBookmarks' });
243 | port.onMessage.addListener(function(bookmarks) {
244 | processor.onUpdateBoxText(function(text) {
245 | port.disconnect();
246 | return filterBookmarks(bookmarks, text);
247 | });
248 | });
249 | port.postMessage();
250 | };
251 |
252 | /**
253 | * Search a history.
254 | */
255 | Command.searchHistories = function() {
256 | var port = chrome.runtime.connect({ name: 'loadHistories' });
257 | var processor = Mode.changeMode(ModeList.COMMAND_MODE);
258 | var commandBox = processor.getCommandBox().setHeaderText('Histories');
259 |
260 | port.onMessage.addListener(function(histories) {
261 | commandBox.setCandidate(filterHistories(histories, ''));
262 | commandBox.showCandidate();
263 |
264 | processor.onUpdateBoxText(function(text) {
265 | return filterHistories(histories, text);
266 | }, true);
267 | processor.onEnter(function(text, selected) {
268 | Utility.openUrl(selected.url);
269 | });
270 | });
271 | };
272 |
273 | /**
274 | * Search applications.
275 | */
276 | Command.searchApplications = function() {
277 | var processor = Mode.changeMode(ModeList.COMMAND_MODE);
278 | var port = chrome.runtime.connect({ name: 'loadApplications' });
279 | var commandBox = processor.getCommandBox().setHeaderText('Applications');
280 |
281 | processor.onEnter(function(text, selected) {
282 | chrome.runtime.sendMessage({ command: 'launchApplication', params: selected.id });
283 | });
284 |
285 | port.onMessage.addListener(function(apps) {
286 | processor.onUpdateBoxText(function(text) {
287 | return filterApplications(apps, text);
288 | });
289 | commandBox.setCandidate(filterApplications(apps, ''));
290 | commandBox.showCandidate();
291 | port.disconnect();
292 | });
293 | port.postMessage();
294 | };
295 |
296 | /**
297 | * Enter the visual mode.
298 | */
299 | Command.enterVisualMode = function() {
300 | var targets = Dom.searchVisibleElementsFrom(Dom.visualableXPath());
301 | if (targets.length > 0) {
302 | var processor = Mode.enterHintMode('red', targets);
303 | processor.onChooseElement(function(element) {
304 | Mode.enterVisualMode(element);
305 | return false;
306 | });
307 | }
308 | };
309 |
310 | /**
311 | * Enter the hint mode. Hint targets are clickable and form elements.
312 | */
313 | Command.followLink = function(option) {
314 | // Collect hint source targets.
315 | var targets = Dom.searchVisibleElementsFrom(Dom.clickableAndInsertableXPath());
316 | var newTab = option.new || false;
317 | var theme = newTab ? 'blue' : 'yellow';
318 |
319 | // Do nothing if there are not targets or the current mode is the insert mode.
320 | if (targets.length == 0 || Mode.isInsertMode()) {
321 | return;
322 | }
323 |
324 | // Sometimes, browser may have an active editable element in the hint mode,
325 | // So Ht shifts the current mode to the insert mode if it have ones.
326 | if (Dom.isEditable(document.activeElement)) {
327 | Mode.changeMode(ModeList.INSERT_MODE);
328 | return;
329 | }
330 |
331 | // Set continuous state in background if continuous option is true.
332 | if (option.continuous) {
333 | chrome.runtime.sendMessage({ command: 'enterContinuousMode' });
334 | }
335 |
336 | // enter the hint mode, and register event listener to handle choosen element.
337 | var processor = Mode.enterHintMode(theme, targets);
338 | processor.onChooseElement(function(element) {
339 | return HintAction.run(this.getExtendAction(), element, processor, option);
340 | });
341 | };
342 |
343 | /**
344 | * Enter the insert mode.
345 | */
346 | Command.enterInsertMode = function() {
347 | Mode.changeMode(ModeList.INSERT_MODE);
348 | Mode.lock();
349 | };
350 |
351 | /**
352 | * Enter the command mode.
353 | */
354 | Command.executeCommand = function() {
355 | var processor = Mode.changeMode(ModeList.COMMAND_MODE);
356 | processor.getCommandBox().setHeaderText(':');
357 | };
358 |
359 | /**
360 | * Enter the normal mode.
361 | */
362 | Command.enterNormalMode = function() {
363 | Mode.changeMode(ModeList.NORMAL_MODE);
364 | chrome.runtime.sendMessage({ command: 'leaveContinuousMode' });
365 | };
366 |
367 | Command.showAssignedCommands = function() {
368 | Mode.changeMode(ModeList.HELP_MODE);
369 | };
370 |
--------------------------------------------------------------------------------
/spec/support/jasmine-ajax.js:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | Jasmine-Ajax : a set of helpers for testing AJAX requests under the Jasmine
4 | BDD framework for JavaScript.
5 |
6 | http://github.com/pivotal/jasmine-ajax
7 |
8 | Jasmine Home page: http://pivotal.github.com/jasmine
9 |
10 | Copyright (c) 2008-2013 Pivotal Labs
11 |
12 | Permission is hereby granted, free of charge, to any person obtaining
13 | a copy of this software and associated documentation files (the
14 | "Software"), to deal in the Software without restriction, including
15 | without limitation the rights to use, copy, modify, merge, publish,
16 | distribute, sublicense, and/or sell copies of the Software, and to
17 | permit persons to whom the Software is furnished to do so, subject to
18 | the following conditions:
19 |
20 | The above copyright notice and this permission notice shall be
21 | included in all copies or substantial portions of the Software.
22 |
23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
24 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
25 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
26 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
27 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
28 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
29 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
30 |
31 | */
32 |
33 | (function() {
34 | function extend(destination, source, propertiesToSkip) {
35 | propertiesToSkip = propertiesToSkip || [];
36 | for (var property in source) {
37 | if (!arrayContains(propertiesToSkip, property)) {
38 | destination[property] = source[property];
39 | }
40 | }
41 | return destination;
42 | }
43 |
44 | function arrayContains(arr, item) {
45 | for (var i = 0; i < arr.length; i++) {
46 | if (arr[i] === item) {
47 | return true;
48 | }
49 | }
50 | return false;
51 | }
52 |
53 | function MockAjax(global) {
54 | var requestTracker = new RequestTracker(),
55 | stubTracker = new StubTracker(),
56 | paramParser = new ParamParser(),
57 | realAjaxFunction = global.XMLHttpRequest,
58 | mockAjaxFunction = fakeRequest(requestTracker, stubTracker, paramParser);
59 |
60 | this.install = function() {
61 | global.XMLHttpRequest = mockAjaxFunction;
62 | };
63 |
64 | this.uninstall = function() {
65 | global.XMLHttpRequest = realAjaxFunction;
66 |
67 | this.stubs.reset();
68 | this.requests.reset();
69 | paramParser.reset();
70 | };
71 |
72 | this.stubRequest = function(url, data) {
73 | var stub = new RequestStub(url, data);
74 | stubTracker.addStub(stub);
75 | return stub;
76 | };
77 |
78 | this.withMock = function(closure) {
79 | this.install();
80 | try {
81 | closure();
82 | } finally {
83 | this.uninstall();
84 | }
85 | };
86 |
87 | this.addCustomParamParser = function(parser) {
88 | paramParser.add(parser);
89 | };
90 |
91 | this.requests = requestTracker;
92 | this.stubs = stubTracker;
93 | }
94 |
95 | function StubTracker() {
96 | var stubs = [];
97 |
98 | this.addStub = function(stub) {
99 | stubs.push(stub);
100 | };
101 |
102 | this.reset = function() {
103 | stubs = [];
104 | };
105 |
106 | this.findStub = function(url, data) {
107 | for (var i = stubs.length - 1; i >= 0; i--) {
108 | var stub = stubs[i];
109 | if (stub.matches(url, data)) {
110 | return stub;
111 | }
112 | }
113 | };
114 | }
115 |
116 | function ParamParser() {
117 | var defaults = [
118 | {
119 | test: function(xhr) {
120 | return /^application\/json/.test(xhr.contentType());
121 | },
122 | parse: function jsonParser(paramString) {
123 | return JSON.parse(paramString);
124 | }
125 | },
126 | {
127 | test: function(xhr) {
128 | return true;
129 | },
130 | parse: function naiveParser(paramString) {
131 | var data = {};
132 | var params = paramString.split('&');
133 |
134 | for (var i = 0; i < params.length; ++i) {
135 | var kv = params[i].replace(/\+/g, ' ').split('=');
136 | var key = decodeURIComponent(kv[0]);
137 | data[key] = data[key] || [];
138 | data[key].push(decodeURIComponent(kv[1]));
139 | }
140 | return data;
141 | }
142 | }
143 | ];
144 | var paramParsers = [];
145 |
146 | this.add = function(parser) {
147 | paramParsers.unshift(parser);
148 | };
149 |
150 | this.findParser = function(xhr) {
151 | for(var i in paramParsers) {
152 | var parser = paramParsers[i];
153 | if (parser.test(xhr)) {
154 | return parser;
155 | }
156 | }
157 | };
158 |
159 | this.reset = function() {
160 | paramParsers = [];
161 | for(var i in defaults) {
162 | paramParsers.push(defaults[i]);
163 | }
164 | };
165 |
166 | this.reset();
167 | }
168 |
169 | function fakeRequest(requestTracker, stubTracker, paramParser) {
170 | function FakeXMLHttpRequest() {
171 | requestTracker.track(this);
172 | this.requestHeaders = {};
173 | }
174 |
175 | var iePropertiesThatCannotBeCopied = ['responseBody', 'responseText', 'responseXML', 'status', 'statusText', 'responseTimeout'];
176 | extend(FakeXMLHttpRequest.prototype, new window.XMLHttpRequest(), iePropertiesThatCannotBeCopied);
177 | extend(FakeXMLHttpRequest.prototype, {
178 | open: function() {
179 | this.method = arguments[0];
180 | this.url = arguments[1];
181 | this.username = arguments[3];
182 | this.password = arguments[4];
183 | this.readyState = 1;
184 | this.onreadystatechange();
185 | },
186 |
187 | setRequestHeader: function(header, value) {
188 | this.requestHeaders[header] = value;
189 | },
190 |
191 | abort: function() {
192 | this.readyState = 0;
193 | this.status = 0;
194 | this.statusText = "abort";
195 | this.onreadystatechange();
196 | },
197 |
198 | readyState: 0,
199 |
200 | onload: function() {
201 | },
202 |
203 | onreadystatechange: function(isTimeout) {
204 | },
205 |
206 | status: null,
207 |
208 | send: function(data) {
209 | this.params = data;
210 | this.readyState = 2;
211 | this.onreadystatechange();
212 |
213 | var stub = stubTracker.findStub(this.url, data);
214 | if (stub) {
215 | this.response(stub);
216 | }
217 | },
218 |
219 | contentType: function() {
220 | for (var header in this.requestHeaders) {
221 | if (header.toLowerCase() === 'content-type') {
222 | return this.requestHeaders[header];
223 | }
224 | }
225 | },
226 |
227 | data: function() {
228 | if (!this.params) {
229 | return {};
230 | }
231 |
232 | return paramParser.findParser(this).parse(this.params);
233 | },
234 |
235 | getResponseHeader: function(name) {
236 | return this.responseHeaders[name];
237 | },
238 |
239 | getAllResponseHeaders: function() {
240 | var responseHeaders = [];
241 | for (var i in this.responseHeaders) {
242 | if (this.responseHeaders.hasOwnProperty(i)) {
243 | responseHeaders.push(i + ': ' + this.responseHeaders[i]);
244 | }
245 | }
246 | return responseHeaders.join('\r\n');
247 | },
248 |
249 | responseText: null,
250 |
251 | response: function(response) {
252 | this.status = response.status;
253 | this.statusText = response.statusText || "";
254 | this.responseText = response.responseText || "";
255 | this.readyState = 4;
256 | this.responseHeaders = response.responseHeaders ||
257 | {"Content-Type": response.contentType || "application/json" };
258 |
259 | this.onload();
260 | this.onreadystatechange();
261 | },
262 |
263 | responseTimeout: function() {
264 | this.readyState = 4;
265 | jasmine.clock().tick(30000);
266 | this.onreadystatechange('timeout');
267 | }
268 | });
269 |
270 | return FakeXMLHttpRequest;
271 | }
272 |
273 | function RequestTracker() {
274 | var requests = [];
275 |
276 | this.track = function(request) {
277 | requests.push(request);
278 | };
279 |
280 | this.first = function() {
281 | return requests[0];
282 | };
283 |
284 | this.count = function() {
285 | return requests.length;
286 | };
287 |
288 | this.reset = function() {
289 | requests = [];
290 | };
291 |
292 | this.mostRecent = function() {
293 | return requests[requests.length - 1];
294 | };
295 |
296 | this.at = function(index) {
297 | return requests[index];
298 | };
299 |
300 | this.filter = function(url_to_match) {
301 | if (requests.length == 0) return [];
302 | var matching_requests = [];
303 |
304 | for (var i = 0; i < requests.length; i++) {
305 | if (url_to_match instanceof RegExp &&
306 | url_to_match.test(requests[i].url)) {
307 | matching_requests.push(requests[i]);
308 | } else if (url_to_match instanceof Function &&
309 | url_to_match(requests[i])) {
310 | matching_requests.push(requests[i]);
311 | } else {
312 | if (requests[i].url == url_to_match) {
313 | matching_requests.push(requests[i]);
314 | }
315 | }
316 | }
317 |
318 | return matching_requests;
319 | };
320 | }
321 |
322 | function RequestStub(url, stubData) {
323 | var normalizeQuery = function(query) {
324 | return query ? query.split('&').sort().join('&') : undefined;
325 | };
326 |
327 | if (url instanceof RegExp) {
328 | this.url = url;
329 | this.query = undefined;
330 | } else {
331 | var split = url.split('?');
332 | this.url = split[0];
333 | this.query = split.length > 1 ? normalizeQuery(split[1]) : undefined;
334 | }
335 |
336 | this.data = normalizeQuery(stubData);
337 |
338 | this.andReturn = function(options) {
339 | this.status = options.status || 200;
340 |
341 | this.contentType = options.contentType;
342 | this.responseText = options.responseText;
343 | };
344 |
345 | this.matches = function(fullUrl, data) {
346 | var matches = false;
347 | fullUrl = fullUrl.toString();
348 | if (this.url instanceof RegExp) {
349 | matches = this.url.test(fullUrl);
350 | } else {
351 | var urlSplit = fullUrl.split('?'),
352 | url = urlSplit[0],
353 | query = urlSplit[1];
354 | matches = this.url === url && this.query === normalizeQuery(query);
355 | }
356 | return matches && (!this.data || this.data === normalizeQuery(data));
357 | };
358 | }
359 |
360 | if (typeof window === "undefined" && typeof exports === "object") {
361 | exports.MockAjax = MockAjax;
362 | jasmine.Ajax = new MockAjax(exports);
363 | } else {
364 | window.MockAjax = MockAjax;
365 | jasmine.Ajax = new MockAjax(window);
366 | }
367 | }());
368 |
--------------------------------------------------------------------------------
|