0%

angular 實作類似 acejump easymotion 功能

 

最近因為要離職的關係, 才發現離上一篇已經快兩個月, 好久沒更新 blog 了!
想當初是因為長官的知遇之恩才續留, 沒想到最後還是這種結果, 整個心情超難過的 哀~
要是能重來, 去年錄取保哥時就該直接過去, 錯失保哥還真的滿可惜的! 也不會有後續這些遲早引爆的事件發生

離開前把一直想在 angular 上面做類似 acejump or easymotion 的功能給完成下, 不過我覺得用 slickback 這個詞更貼切
之前有用純 js 在自己 blog 搞過一把, 這次就獻給 angular 說不定這是職涯最後一把用 angular 了, 就當最後的致敬吧!

html 的部分大概長這樣, 需要注意的是 span 這個部分
他應該要每一個字使用一個 span, 這樣才可以每敲一個字下去變成紅字, 否則會整坨都紅字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<div
#dtags
class="slickback-tag"
[ngStyle]="{display: isFirstCharInCmdArray('d') ? '' : 'none'}">
<span
*ngFor="
let text of indexToSpan(i, 'd');
let textIndex = index
"
[ngStyle]="{
color: textInCmdArray(
indexToSpan(i, 'd').join(''),
textIndex
)
? 'red'
: 'black'
}"
>{{ text }}</span>
</div>

css 則是用絕對定位這老朋友即可搞定

1
2
3
4
5
6
7
8
9
10
11
.slickback-tag {
position: absolute;
left: 0;
top: 0;
background-color: #89cf07;
font-weight: bold;
font-size: 10px;
color: black;
padding: 2px;
border-radius: 2px;
}

當使用者敲擊第一個字如果是以下列表時表示會觸發執行跳躍的事件, 將這些字 push 進去 cmdArray 裡面

e => edit
a => add
d => delete

接著如果命令的字大於一個時則判斷是否敲入 0123456789
如果正確的話一樣 pushkey, 反之則設定 cmdArray = [] 清除

isNeedAnotherKeyTyping 這個函數則是用來判斷 a1 ... a10 a11 這類兩個字開頭一樣的情況, 如果不需要的話就執行跳躍

回頭看到 防禦式 的部分, 如果使用者敲 e1 a1 d1 這類有可能兩個字相同的部分, 並且明確敲下 Enter 則直接執行

1
2
3
4
5
6
7
8
9
10
//防禦式, 如果需要執行第二個 key 時直接按下 Enter 則直接執行 ex: e1 a1 d1
if (keyPressed === 'Enter' && this.needAnotherKey) {
if (this.typingTimeout) {
clearTimeout(this.typingTimeout);
}
console.log('needAnotherKey', this.needAnotherKey);

this.slickbackClick();
return;
}

最後看到 setTimeout 時間如果在 2.5 秒內都沒動作的話, 就執行 slickbackClick 讓貼在 span 上的按鈕觸發 click 即可

full code

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
cmdArray = new Array<string>();
@ViewChildren('etags')
etags?: QueryList<ElementRef>;

@ViewChildren('atags')
atags?: QueryList<ElementRef>;

@ViewChildren('dtags')
dtags?: QueryList<ElementRef>;

typingTimeout: any;
typingKeyDelayTime = 2500;
needAnotherKey = false;
@HostListener('document:keydown', ['$event'])
onKeyDown(event: KeyboardEvent): void {
// console.log(`KeyDown: ${event.key}`);

let keyPressed = event.key;

//防禦式, 如果需要執行第二個 key 時直接按下 Enter 則直接執行 ex: e1 a1 d1
if (keyPressed === 'Enter' && this.needAnotherKey) {
if (this.typingTimeout) {
clearTimeout(this.typingTimeout);
}
console.log('needAnotherKey', this.needAnotherKey);

this.slickbackClick();
return;
}


//正常情況
if (this.cmdArray.length > 0) {
if ('0123456789'.includes(keyPressed)) {
this.cmdArray.push(keyPressed);
} else {
//萬一中間按下其他 key 則終止執行動作
this.cmdArray = [];
}
} else {

if (this.isViewMode) {
//編輯
if (keyPressed === 'e') this.cmdArray.push('e');

if (keyPressed === 'a') this.cmdArray.push('a');

if (keyPressed === 'd') this.cmdArray.push('d');
} else {
//新增
if (keyPressed === 'a') this.cmdArray.push('a');

//刪除
if (keyPressed === 'd') this.cmdArray.push('d');
}
}

this.needAnotherKey = this.isNeedAnotherKeyTyping();
if (this.needAnotherKey) return;

if (this.typingTimeout) {
clearTimeout(this.typingTimeout);
}

//3 秒內按下的話就執行 slickback
this.typingTimeout = setTimeout(() => {
this.slickbackClick();
}, this.typingKeyDelayTime);
}

//快捷跳躍
slickbackClick() {
if (this.cmdArray.length > 1) {
console.log('於 1 秒按下');
console.log('exe', this.cmdArray);
let str = this.cmdArray.join('');
console.log('str', str);
let index = parseInt(str.substring(1));

let arr;

if (this.isViewMode) {
if (str[0] === 'e') arr = this.etags?.toArray();
} else {
if (str[0] === 'a') arr = this.atags?.toArray();
if (str[0] === 'd') arr = this.dtags?.toArray();
}

if (arr && arr.length > 0 && index < arr.length)
arr![index].nativeElement.parentElement.click();

//已經執行命令所以清空
this.cmdArray = [];
}
}

//判斷是否需要輸入其他 key, ex:a1 ... a10 a11 這時就需要
isNeedAnotherKeyTyping() {
if (this.cmdArray.length > 1) {
console.log('於 1 秒按下');
console.log('exe', this.cmdArray);
let str = this.cmdArray.join('');
console.log('str', str);
let index = parseInt(str.substring(1));
let sindex = str.substring(1);

let arr;

if (this.isViewMode) {
if (str[0] === 'e') arr = this.etags?.toArray();
} else {
if (str[0] === 'a') arr = this.atags?.toArray();
if (str[0] === 'd') arr = this.dtags?.toArray();
}

if (!arr || arr.length === 0) return false;
let indexAsStrings = arr?.map((_, index) => index.toString());

console.log('sindex', sindex);
let needDelay = indexAsStrings?.some(
(value) => value.startsWith(sindex) && value !== sindex
);

// console.log('indexAsStrings', indexAsStrings);

// console.log('needDelay', needDelay);

if (!needDelay) {
// console.log('arr', arr);
if (arr && arr?.length > 0 && index < arr.length)
arr![index].nativeElement.parentElement.click();

//已經執行命令所以清空
this.cmdArray = [];
return false;
}

return true;
}

return false;
}

//把 index 轉為一個一個的 span
indexToSpan(index: number, key: string) {
let arr = key + index.toString();
let result = arr.split('');
return result;
}

textInCmdArray(arr: string, index: number) {
if (this.cmdArray.length === 0) return false;

for (let i = 0; i < index + 1; i++) {
if (arr[i] !== this.cmdArray[i]) return false;
}
return true;
}

isFirstCharInCmdArray(key: string) {
if (this.cmdArray.length >= 1) {
if (this.cmdArray[0] === key) return true;
}

return false;
}

//global 當點選按鈕時, 取消 slickback
@HostListener('document:click', ['$event'])
onClick(event: MouseEvent) {
const target = event.target as HTMLElement;
if (target && target.tagName.toLowerCase() === 'button') {
// 如果點擊的是按鈕,觸發事件
console.log('button click cancel slickback cmd');
this.cmdArray = [];
this.needAnotherKey = false;
}
}
關閉