0%

js 土炮自己的 chrome vim

img

自己用 vim 來 coding 不知不覺也有幾年的時間了, 在 chrome 上通常都用這個套件 vimium
可是其他人電腦不見得有裝, 導致每次要在別人電腦 debug 又剛好要找自己 blog 資料時總是少了點 fu ~
今天就來自己致敬下 vim, 大致上分為兩部分, 普通移動跟 easymotion
後來發現有些網站 bug bug 的 XD 有空再修
因為放 codepen 怪小怪小的, 難得要放 github repo

普通移動

普通移動相當簡單, 只需要呼叫 window.scrollTo 這個函數就可以搞定
可以設定下 behavior 讓移動起來比較絲滑
這裡如果是在 youtube 上面會發生 document.body.scrollHeight 為零的詭異狀況
所以要用 Math.max 讓取回最大的數值才會正確

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
viGoTop(keyPressed) {
if (keyPressed === "g") {
window.scrollTo({ top: 0, behavior: "smooth" });
}
}

viGoBottom(keyPressed) {
if (keyPressed === "G") {
console.log("document.body.scrollHeight", document.body.scrollHeight);
let h = Math.max(
Math.max(
document.body.scrollHeight,
document.documentElement.scrollHeight
),
Math.max(
document.body.offsetHeight,
document.documentElement.offsetHeight
),
Math.max(
document.body.clientHeight,
document.documentElement.clientHeight
)
);
window.scrollTo({ top: h, behavior: "smooth" });
}
}



viFastDown(keyPressed) {
if (keyPressed === "d") {
this.move(350);
}
}

viDown(keyPressed) {
if (keyPressed === "j") {
this.move(100);
}
}

viFastUp(keyPressed) {
if (keyPressed === "u") {
this.move(-350);
}
}

viUp(keyPressed) {
if (keyPressed === "k") {
this.move(-100);
}
}

move(val) {
var currentPosition =
window.pageYOffset || document.documentElement.scrollTop;
window.scrollTo({
top: currentPosition + val,
behavior: "smooth",
});
}

gg 比較特別要按兩下, 所以可以在事件裡面撰寫以下 code

1
2
3
4
5
6
if (
keyPressed === this.lastKeyPressed &&
currentTime - this.lastKeyPressTime < 300
) {
this.viGoTop(keyPressed);
}

最後因為包成物件, 所以要 bind(this) 才能正確呼叫函數

1
2
3
init() {
document.addEventListener("keydown", this.handleKeyDown.bind(this));
}

Easymotion

Easymotion 比較複雜, 他先用 ABCEILNOPQRSTVWXYZ 排除掉 jkgdu 等特殊字防止出錯
接著將 ABCEILNOPQRSTVWXYZ 進行雙重迴圈配對出兩個字的組合, 大概快 400 個, 超過就放生 XD
holdTags 這個變數則是保存目前塞進去的配對 tag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//可使用移動的字碼
//共 18 個字 排除 vi 會用到的字
this.tagChars = "ABCEILNOPQRSTVWXYZ";
// this.tagChars = "abc";

//目前的 vim 標示字標籤 array
this.holdTags = new Array();

//預先建立兩字組合的字典
this.dict = new Array();
//雙層迴圈灌入所有兩字組合
for (var i = 0; i < this.tagChars.length; i++) {
for (var j = 0; j < this.tagChars.length; j++) {
this.dict.push(this.tagChars[i] + this.tagChars[j]);
}
}

接著看到核心的 createViTags, 當進入 motion 模式時, 這裡因為 f 這個 key保哥條款, 所以改用 F , 需要找到所有 a 標籤
接著跑個迴圈, 當 a 標籤少於 ABCEILNOPQRSTVWXYZ 的話直接放一個 char, 反之就從 dict 找出兩個 char 來放
另外 getBoundingClientRect 這個函數撈出來的值, 還需要加上卷軸數值 window.scrollY window.scrollX 才會對

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
createViTag(text, href, top, left) {
let newDiv = document.createElement("div");
newDiv.classList.add("vim-tag");
newDiv.style.fontFamily = "Arial, sans-serif";
newDiv.style.fontSize = "12px";
newDiv.style.position = "absolute";
newDiv.style.backgroundColor = "#89CF07";
newDiv.style.color = "black";
newDiv.style.padding = "2px";
newDiv.style.borderRadius = "2px";
newDiv.style.zIndex = "999999";

newDiv.textContent = text;
newDiv.dataset.href = href;
newDiv.style.top = top;
newDiv.style.left = left;
return newDiv;
}

createViTags() {
let allTags = document.querySelectorAll("a");
let counter = 0;
for (let tag of allTags) {
let rect = tag.getBoundingClientRect();
let href = tag.href;
//這個距離需要加入卷軸距離才會正確
let top = window.scrollY + rect.top + "px";
let left = window.scrollX + rect.left + "px";
let text = "";
if (allTags.length <= this.tagChars.length) {
text = this.tagChars[counter];
this.holdTags.push(text);
} else {
text = this.dict[counter];
this.holdTags.push(text);
}
let newDiv = this.createViTag(text, href, top, left);
document.body.appendChild(newDiv);
counter++;
}
}

再來是判斷是否一個 char, 若是的話很簡單的讓 window.location.href = tag.dataset.href 即可
這裡要注意到由於 ABCEILNOPQRSTVWXYZ 為大寫所以要呼叫 toLowerCase

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if (
this.currentMode === this.Mode.Motion &&
document.querySelectorAll("a").length <= this.tagChars.length &&
this.tagChars.toLowerCase().includes(keyPressed)
) {
console.log("one char mode");
let allTags = document.querySelectorAll(".vim-tag");

allTags.forEach(function (tag) {
if (tag.textContent.toLowerCase() === keyPressed) {
window.location.href = tag.dataset.href;
}
});

this.toggleNormal();
return;
}

接著是 兩個 char 的狀況, 如果 lastKeyPressed 為空字串的話, 表示敲入第一個字
此時利用 firstCharArray 判斷是否開頭有符合我們 array 保存的第一個字
當有符合的話, 將第一個字以外的內容排除, 並且讓第一個字變成紅色

lastKeyPressed 有東西的話, 則表示已經輸入一個字, 所以判斷第二個字是否正確, ok 的話就跳到該 href 的網址上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
//當 motion 兩個字才走這個模式
if (
this.currentMode === this.Mode.Motion &&
document.querySelectorAll("a").length > this.tagChars.length
) {
console.log("motion lastKeyPressed", this.lastKeyPressed);
console.log("motion current", keyPressed);

if (!this.lastKeyPressed) {
//如果出現字表以外的字則回到 normal
//console.log("firstCharArray", this.firstCharArray());
if (this.firstCharArray().includes(keyPressed) === false) {
this.toggleNormal();
return;
} else {
let allTags = document.querySelectorAll(".vim-tag");
allTags.forEach(function (tag) {
if (tag.textContent[0].toLowerCase() !== keyPressed) {
tag.parentNode.removeChild(tag);
}

//將第一個字變為紅色
if (tag.textContent[0].toLowerCase() === keyPressed) {
tag.innerHTML =
'<span style="color: red;">' +
tag.textContent.charAt(0) +
"</span>" +
tag.textContent.substring(1);
}
});
}
}

//如果有字的話才執行
if (this.lastKeyPressed) {
let chars = this.lastKeyPressed + keyPressed;
console.log("chars", chars);
let allTags = document.querySelectorAll(".vim-tag");
allTags.forEach(function (tag) {
if (tag.textContent.toLowerCase() === chars) {
window.location.href = tag.dataset.href;
}
});
//萬一沒找到切回 Normal
this.toggleNormal();
return;
}
}

fullcode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
class ViNavigation {
constructor() {
this.Mode = {
Normal: "normal",
Motion: "motion",
};

this.lastKeyPressTime = 0;
this.lastKeyPressed = "";
this.currentMode = this.Mode.Normal;

//可使用移動的字碼
//共 18 個字 排除 vi 會用到的字
this.tagChars = "ABCEILNOPQRSTVWXYZ";

//目前的 vim 標示字標籤 array
this.holdTags = new Array();

//預先建立兩字組合的字典
this.dict = new Array();
//雙層迴圈灌入所有兩字組合
for (var i = 0; i < this.tagChars.length; i++) {
for (var j = 0; j < this.tagChars.length; j++) {
this.dict.push(this.tagChars[i] + this.tagChars[j]);
}
}
}

viGoTop(keyPressed) {
if (keyPressed === "g") {
window.scrollTo({ top: 0, behavior: "smooth" });
}
}

viGoBottom(keyPressed) {
if (keyPressed === "G") {
console.log("document.body.scrollHeight", document.body.scrollHeight);
let h = Math.max(
Math.max(
document.body.scrollHeight,
document.documentElement.scrollHeight
),
Math.max(
document.body.offsetHeight,
document.documentElement.offsetHeight
),
Math.max(
document.body.clientHeight,
document.documentElement.clientHeight
)
);
window.scrollTo({ top: h, behavior: "smooth" });
}
}

viFastDown(keyPressed) {
if (keyPressed === "d") {
this.move(350);
}
}

viDown(keyPressed) {
if (keyPressed === "j") {
this.move(100);
}
}

viFastUp(keyPressed) {
if (keyPressed === "u") {
this.move(-350);
}
}

viUp(keyPressed) {
if (keyPressed === "k") {
this.move(-100);
}
}

move(val) {
var currentPosition =
window.pageYOffset || document.documentElement.scrollTop;
window.scrollTo({
top: currentPosition + val,
behavior: "smooth",
});
}

removeViTags() {
let allTags = document.querySelectorAll(".vim-tag");
allTags.forEach(function (tag) {
tag.parentNode.removeChild(tag);
});
}

createViTag(text, href, top, left) {
let newDiv = document.createElement("div");
newDiv.classList.add("vim-tag");
newDiv.style.fontFamily = "Arial, sans-serif";
newDiv.style.fontSize = "12px";
newDiv.style.position = "absolute";
newDiv.style.backgroundColor = "#89CF07";
newDiv.style.color = "black";
newDiv.style.padding = "2px";
newDiv.style.borderRadius = "2px";
newDiv.style.zIndex = "999999";

newDiv.textContent = text;
newDiv.dataset.href = href;
newDiv.style.top = top;
newDiv.style.left = left;
return newDiv;
}

createViTags() {
let allTags = document.querySelectorAll("a");
let counter = 0;
for (let tag of allTags) {
let rect = tag.getBoundingClientRect();
let href = tag.href;
//這個距離需要加入卷軸距離才會正確
let top = window.scrollY + rect.top + "px";
let left = window.scrollX + rect.left + "px";
let text = "";
if (allTags.length <= this.tagChars.length) {
text = this.tagChars[counter];
this.holdTags.push(text);
} else {
text = this.dict[counter];
this.holdTags.push(text);
}
let newDiv = this.createViTag(text, href, top, left);
document.body.appendChild(newDiv);
counter++;
}
}

//找出目前的首字 array
firstCharArray() {
let result = [];
for (let i = 0; i < this.holdTags.length; i++) {
let text = this.holdTags[i];
if (text) {
let theChar = text[0].toLowerCase();
if (result.includes(theChar) === false) {
result.push(theChar);
}
}
}

return result;
}

toggleMotion() {
this.currentMode = this.Mode.Motion;
this.lastKeyPressed = "";
console.log("mode", this.currentMode);
this.createViTags();
}

toggleNormal() {
this.currentMode = this.Mode.Normal;
console.log("mode", this.currentMode);
this.removeViTags();
this.holdTags = [];
this.lastKeyPressed = "";
}

handleKeyDown(event) {
let currentTime = new Date().getTime();
let keyPressed = event.key;

//按下 F 時進入 motion 模式
if (this.currentMode === this.Mode.Normal && keyPressed === "F") {
this.toggleMotion();
return;
}

//按下 esc 跳離 motion 模式回到 normal 模式
if (this.currentMode === this.Mode.Motion && keyPressed === "Escape") {
this.toggleNormal();
return;
}

//當 motion 一個字時才走這模式
if (
this.currentMode === this.Mode.Motion &&
document.querySelectorAll("a").length <= this.tagChars.length &&
this.tagChars.toLowerCase().includes(keyPressed)
) {
console.log("one char mode");
let allTags = document.querySelectorAll(".vim-tag");

allTags.forEach(function (tag) {
if (tag.textContent.toLowerCase() === keyPressed) {
window.location.href = tag.dataset.href;
}
});

this.toggleNormal();
return;
}

//當 motion 兩個字才走這個模式
if (
this.currentMode === this.Mode.Motion &&
document.querySelectorAll("a").length > this.tagChars.length
) {
console.log("motion lastKeyPressed", this.lastKeyPressed);
console.log("motion current", keyPressed);

if (!this.lastKeyPressed) {
//如果出現字表以外的字則回到 normal
//console.log("firstCharArray", this.firstCharArray());
if (this.firstCharArray().includes(keyPressed) === false) {
this.toggleNormal();
return;
} else {
let allTags = document.querySelectorAll(".vim-tag");
allTags.forEach(function (tag) {
if (tag.textContent[0].toLowerCase() !== keyPressed) {
tag.parentNode.removeChild(tag);
}

//將第一個字變為紅色
if (tag.textContent[0].toLowerCase() === keyPressed) {
tag.innerHTML =
'<span style="color: red;">' +
tag.textContent.charAt(0) +
"</span>" +
tag.textContent.substring(1);
}
});
}
}

//如果有字的話才執行
if (this.lastKeyPressed) {
let chars = this.lastKeyPressed + keyPressed;
console.log("chars", chars);
let allTags = document.querySelectorAll(".vim-tag");
allTags.forEach(function (tag) {
if (tag.textContent.toLowerCase() === chars) {
window.location.href = tag.dataset.href;
}
});
//萬一沒找到切回 Normal
this.toggleNormal();
return;
}
}

// 任何模式按兩下的區域
if (
keyPressed === this.lastKeyPressed &&
currentTime - this.lastKeyPressTime < 300
) {
this.viGoTop(keyPressed);
}

// 按一下的區域
this.viGoBottom(keyPressed);
this.viDown(keyPressed);
this.viFastDown(keyPressed);
this.viUp(keyPressed);
this.viFastUp(keyPressed);

this.lastKeyPressTime = currentTime;
this.lastKeyPressed = keyPressed;
}

init() {
document.addEventListener("keydown", this.handleKeyDown.bind(this));
}
}
關閉