teratail header banner
teratail header banner
質問するログイン新規登録

回答編集履歴

2

大幅に修正しました

2021/03/05 09:23

投稿

babu_babu_baboo
babu_babu_baboo

スコア616

answer CHANGED
@@ -1,138 +1,245 @@
1
+ 前回の回答があまりにも適当過ぎたので書き直しました。
1
- ちょっと大げさかな
2
+ think49 らの指摘で、summary どに対応しました。
2
3
 
3
4
  ```js
4
- <!DOCTYPE html>
5
- <meta charset="UTF-8">
5
+ <!DOCTYPE html><title></title><meta charset="utf-8">
6
- <title></title>
7
6
  <style>
7
+ a:focus, datalist:focus, input:focus {
8
+ background: rgba(255,0,0,.2);
9
+ }
8
10
  </style>
9
11
 
10
12
  <body>
13
+ <nav>
14
+ <p>最初に[Tab]でフォーカスを移動した場合テストできる<br>
15
+ <a href="#C">ABC</a>
16
+ <a href="#C">DEF</a>
17
+ <a href="#A">GHI</a><br>
18
+ </nav>
19
+
20
+ <form>
21
+ <p>フォーム内のアンカータグを移動する<br>
22
+ <a href="#C">ABC</a>
23
+ <a href="#C">DEF</a>
24
+ <a href="#A">GHI</a>
25
+
26
+ <p>通常の INPUT要素<br>
11
- <form id="hoge">
27
+ 1.<input name="d0" value="">
28
+ 2.<input name="d1" value="">
29
+
30
+ <p>表示されていない要素<br>
12
- <input type="text" name="a"><br>
31
+ 1.<input type="hidden" name="d2">
13
- <input type="text" name="b" style="display:none;"><br>
32
+ 2.<input type="hidden" name="d2" style="display:none;">
14
- <input type="text" name="c"><br>
33
+ 3.<input type="hidden" name="d2" style="visibility:hidden;">
15
- <input type="text" name="d" tabindex="-1"><br>
34
+ 4.<input type="hidden" name="d2" style="opacity: 0;">
35
+
36
+ <p>INPUT[type=radio]要素の場合<br>
37
+ <label>1.<input type="radio" name="d3" value="1">abc</label>
38
+ <label>2.<input type="radio" name="d3" value="2">def</label>
39
+ <label>3.<input type="radio" name="d3" value="3">ghi</label>
40
+
41
+ <p>INPUT[type=checkbox]要素の場合<br>
42
+ <label><input type="checkbox" name="d4" value="1">abc</label>
43
+ <label><input type="checkbox" name="d4" value="2">def</label>
44
+ <label><input type="checkbox" name="d4" value="3">ghi</label><br>
45
+
46
+ <p>SELECT要素<br>
47
+ <select name="d5"><option value="">-- <option value="1">abc <option value="2">def</select>
48
+
49
+ <p>TEXTAREA要素<br>
16
- <input type="text" name="e"><br>
50
+ <textarea name="d6" cols="80" rows="5">
51
+ 改行すると、その行の最初のインデントが継続されます
52
+ 改行せずに移動する場合は[Ctrl]+[Enter]、設定で逆の動作をします
53
+ 改行すると、その行の最初のインデントが継続されます
54
+ </textarea>
55
+
56
+ <p>DATALIST要素にSUMMARYがある場合<br>
57
+ <details>
58
+ <summary>Summary OPEN1</summary>
59
+ <ol><li>abc <li>def <li>ghi</ol>
60
+ </details>
61
+ <details open>
62
+ <summary>Summary OPEN2</summary>
63
+ <ol><li>abc <li>def <li>ghi</ol>
64
+ </details>
65
+
66
+ <p>INPUT[type=button] / BUTTON 要素の場合<br>
67
+ <input type="button" value="フォームの移動を無効にする" onclick="F.disabled=true">
68
+ <input type="button" value="フォームの移動を有効にする" onclick="F.disabled=false">
69
+ </p>
70
+
17
71
  </form>
72
+ <script>
18
73
 
74
+ /*
75
+ [Enter] 要素を次に移動する
76
+ [Shift]+[Enter] 前に移動する
77
+ [Ctrl]+[Enter]: 作用する
78
+ */
19
79
 
20
- <script>
21
80
  class ExEnter {
22
81
 
23
- constructor (root = document.documentElement, option = { }) {
82
+ constructor (root = document.documentElement, option = { }) {
83
+ const
24
- this.root = root;
84
+ filter = this.filter.bind (this),
25
- this.walker = document.createTreeWalker (root, NodeFilter.SHOW_ELEMENT, this.filter, true);
85
+ Nfilter = NodeFilter.SHOW_ELEMENT;
26
- this.option = Object.assign ({ }, this.constructor.defaultOption, option);
27
86
 
28
- //IME日本語入力中で未変換文字があり Enterが押されたことを感知するには
29
- //keypress イベントがが実行されないことを利用する
30
- this.imeFlag = null; //ime
87
+ this.root = root;
88
+ this.walker = root.ownerDocument.createTreeWalker (root, Nfilter, filter, true);
31
- }
89
+ this.option = Object.assign ({ }, this.constructor.defaultOption, option);
32
90
 
91
+ //IME日本語入力中で未変換文字があり Enterが押されたことを感知するには
92
+ //keypress イベントがが実行されないことを利用する
93
+ this.imeFlag = null; //ime
94
+ this.disabled = false; //機能停止用
95
+ }
33
96
 
34
- //ルートの中の最初の要素を返す
35
- get firstElement () {
36
- this.walker.currentNode = this.root;
37
- return this.walker.firstChild ();
38
- }
39
97
 
98
+ //ルートの中の最初の要素を返す
99
+ get firstElement () {
100
+ this.walker.currentNode = this.root;
101
+ return this.walker.firstChild ();
102
+ }
40
103
 
41
- //ルートの最後の要素を返す
42
- get lastElement () {
43
- this.walker.currentNode = this.root;
44
- return this.walker.lastChild ();
45
- }
46
104
 
105
+ //ルートの最後の要素を返す
106
+ get lastElement () {
107
+ this.walker.currentNode = this.root;
108
+ return this.walker.lastChild ();
109
+ }
47
110
 
48
- //要素を移動する
49
- move (target, direction = false) {
50
- const
51
- walker = this.walker,
52
- isLoop = this.option.loop;
53
111
 
112
+ //要素を移動する
54
- this.walker.currentNode = target;
113
+ move (target, direction = false) {
55
- let e = (direction)
114
+ if (this.disabled)
56
- ? walker.previousNode () || (isLoop ? this.lastElement: null)
57
- : walker.nextNode () || (isLoop ? this.firstElement: null);
58
- e && e.focus ();
115
+ return;
59
- }
60
116
 
117
+ const
118
+ walker = this.walker,
119
+ isLoop = this.option.loop;
61
120
 
121
+ walker.currentNode = target;
62
- filter (node) {
122
+ let e = (direction)
63
- if (/^(INPUT|TEXTAREA|SELECT|BUTTON)$/.test (node.tagName))
123
+ ? walker.previousNode () || (isLoop ? this.lastElement: null)
64
- if (! node.disabled && ! node.readOnly && '-1' != node.getAttribute ('tabIndex')) {
124
+ : walker.nextNode () || (isLoop ? this.firstElement: null);
125
+
65
- if (! isHide (node))
126
+ if (e) e.focus ();
66
- return NodeFilter.FILTER_ACCEPT;
67
- }
68
- return NodeFilter.FILTER_SKIP;
69
- //__
70
- function isHide (node) {
71
- for (let e = node, s; e; e = e.parentNode) {
72
- if (e === document.body) break;
73
- s = getComputedStyle (e, null);
74
- if ('none' === s.display
75
- || 'hidden' === s.visibility
76
- || !parseFloat (s.opacity || '')
77
- || !parseInt (s.height || '', 10)
78
- || !parseInt (s.width || '', 10)
79
- ) return true;
80
- }
81
- return false;
82
127
  }
83
- }
84
128
 
85
129
 
130
+ //TreeWalker用のフィルター関数(要.bind(this))
86
- //_____
131
+ filter (node) {
132
+ const
133
+ accept = NodeFilter.FILTER_ACCEPT,
134
+ skip = NodeFilter.FILTER_SKIP;
135
+
136
+ switch (node.nodeName) {
137
+ case 'INPUT' : case 'TEXTAREA' : case 'SELECT' : case 'BUTTON' :
138
+ if (node.disabled) break;
139
+ if (node.readOnly) break;
140
+ if (this.option.tabIndex && ('-1' === node.getAttribute ('tabIndex'))) break;
141
+ if (isHide (node)) break;
142
+ return accept;
87
143
 
88
- //テキストエリアキャレット位置で分割(改行)する
144
+ case 'A' : //href = '#...' で始まるもののみ対象とする
89
- textareaSpliter (e) {
145
+ if (this.option.anchor) {
90
- let
146
+ let href = node.getAttribute ('href');
91
- v = e.value,
147
+ if (href)
92
- a = v.substring (0, e.selectionStart);
148
+ if (! href.indexOf ('#'))
149
+ return accept;
150
+ }
151
+ break;
93
152
 
153
+ case 'SUMMARY' :
154
+ return accept;
155
+ }
156
+ return skip;
157
+
158
+ //__
159
+ //祖先の要素が隠された状態にあるか?
160
+ function isHide (node) {
161
+ const chks = ['display', 'visibility', 'opacity', 'height', 'width'];
162
+
163
+ for (let e = node; e !== document.body; e = e.parentNode) {
164
+ let cs = getComputedStyle (e, null);
165
+ let [d, v, o, h, w] = chks.map (p=> cs.getPropertyValue (p));
166
+ if (
94
- e.value = a + '\n' + v.substr (e.selectionEnd);
167
+ 'none' === d || 'hidden' === v || 0 == parseFloat (o) ||
168
+ ! ('auto' === h || 0 < parseInt (h, 10)) ||
95
- e.selectionStart = e.selectionEnd = a.length + 1;
169
+ ! ('auto' === w || 0 < parseInt (w, 10))
170
+ ) return true;
171
+ }
172
+ return false;
173
+ }
96
174
  }
97
175
 
98
176
 
177
+ //_____
178
+
99
179
  //[CTRL]が押されていれば、適用させる
100
180
  apply (e, sw, prev) {
101
- let r = false; //要素に対して操作を行ったか
181
+ let stay = false, tag = e.nodeName;
102
- switch (e.type) {
182
+
103
- case 'radio': case 'checkbox' :
183
+ if ('TEXTAREA' === tag && !prev) {
104
- if (sw) {
105
- e.checked = !e.checked;
106
- r = this.option.stay;
184
+ if (sw ^ this.option.textarea) {
185
+ this.constructor.insertCRLF (e, this.option.autoIndent);
186
+ stay = true;
107
- }
187
+ }
188
+ }
189
+ else if (sw) {
190
+ stay = this.option.stay;
191
+ switch (tag) {
192
+ case 'SUMMARY' :
193
+ let parent = e.parentNode;
194
+ parent.hasAttribute ('open')
195
+ ? parent.removeAttribute ('open')
196
+ : parent.setAttribute ('open', '');
108
197
  break;
109
- case 'button' : case 'submit' : case 'reset' :
198
+
110
- if (sw) {
111
- let event = document.createEvent ('MouseEvents');
199
+ case 'A' :
112
- event.initEvent("click", false, true);
113
- e.dispatchEvent (event);
200
+ fireEvent (e);
114
- r = this.option.stay;
115
- }
116
201
  break;
202
+
117
- case 'textarea' :
203
+ case 'INPUT' :
204
+ switch (e.type) {
118
- if ((sw ^ this.option.textarea_enter) && !prev) {
205
+ case 'radio': case 'checkbox' :
206
+ e.checked = !e.checked;
207
+ break;
208
+
209
+ case 'button' : case 'submit' : case 'reset' :
119
- this.textareaSpliter (e);
210
+ fireEvent (e);
120
- r =true;
211
+ break;
121
212
  }
122
213
  break;
214
+ }
123
215
  }
216
+ //値が有効か?
217
+ if (this.option.invalidStop && e.checkValidity)
218
+ stay = ! e.checkValidity ();
219
+
124
- return r;
220
+ return stay;
221
+
222
+ //__
223
+
224
+ function fireEvent (e) {
225
+ let event = document.createEvent ('MouseEvents');
226
+ event.initEvent ('click', false, true);
227
+ e.dispatchEvent (event);
228
+ return event;
229
+ }
125
230
  }
126
231
 
127
232
 
128
233
  //_____
129
234
 
235
+ //イベントハンドラ(各typeによって分岐)
130
236
  handleEvent (event) {
131
- this[event.type+ 'Handler'].call (this, event, event.target);
237
+ this[event.type + 'Handler'].call (this, event);
132
238
  }
133
239
 
240
+ //キーアップハンドラ
134
- keyupHandler (event, e) {
241
+ keyupHandler (event) {
135
- let { code, shiftKey, ctrlKey } = event;
242
+ let { code, shiftKey, ctrlKey, target: e } = event;
136
243
  if ('Enter' !== code) return;
137
244
  if (! this.imeFlag) //IMEの動作で未変換中の文字があると判断してスルー
138
245
  return event.preventDefault ();
@@ -141,30 +248,59 @@
141
248
  this.move (e, shiftKey);
142
249
  }
143
250
 
251
+ //キープレスハンドラ
144
- keypressHandler (event, e) {
252
+ keypressHandler (event) {
253
+ let { target: { nodeName: n, type: t }, code } = event;
254
+ if ('Enter' == code)
145
- if (/^(submit|reset|button|textarea)$/.test (e.type))
255
+ if (/^(submit|reset|button|textarea)$/.test (t) || 'SUMMARY' === n || 'A' == n)
146
- event.preventDefault ();
256
+ event.preventDefault ();
147
257
  this.imeFlag = true;//未変換文字がある場合の対処として
148
258
  }
149
259
 
150
260
  //________
151
261
 
152
- static defaultOption = {
153
- stay: false, //true: radio,checkbox,submit,button を作用させた後に自動的に移動する
154
- textarea_enter: false, //true:テキストエリア内では改行する ,false:[Ctrl]+[Enter]で改行
155
- loop: true, //要素を巡回する
262
+ //インスタンスの作成
156
- };
157
-
158
-
159
263
  static create (root = document.documentElement, option = { }) {
160
264
  let obj = new this (root, option);
161
265
  root.addEventListener ('keyup', obj, false);
162
266
  root.addEventListener ('keypress', obj, false);
163
267
  return obj;
164
268
  }
269
+
270
+ //textarea要素に改行を挿入する
271
+ static insertCRLF (t, autoIndent = false, start = t.selectionStart || 0) {
272
+ let
273
+ str = '\n',
274
+ value = t.value,
275
+ first = value.slice (0, start),
276
+ last = value.slice (start).replace (/^([\t\u3000\u0020]+)/, '');
277
+
278
+ if (autoIndent) {
279
+ let spc = /(?:\n|^)([\t\u3000\u0020]+).*$/.exec (first);
280
+ if (spc) str += spc[1];
281
+ }
282
+
283
+ t.value = first + str + last;
284
+ t.selectionStart = t.selectionEnd = start + str.length;
285
+ }
286
+
287
+
288
+
289
+ //初期設定オプション値
290
+ static defaultOption = {
291
+ stay: true, //true: radio,checkbox,submit,button,summary を作用させた後も留まる
292
+ loop: true, //要素を巡回する
293
+ tabIndex: true, //tabIndex="-1" を有効にする
294
+ anchor: true, //アンカータグを有効にする(但し href属性の値が "#"から始まる場合)
295
+ textarea: true, //true:テキストエリア内では改行する, false:次に移動する(改行は、[Ctrl]+[Enter])
296
+ autoIndent: true, //textarea要素内でのオートインデントを行う
297
+ invalidStop: false,//要素の値が不正な場合でも移動する
298
+ };
165
299
  }
166
300
 
301
+ const
302
+ nav = ExEnter.create (document.querySelector ('nav')),
167
- ExEnter.create ();
303
+ F = ExEnter.create (document.querySelector ('form'));
168
304
 
169
305
  </script>
170
306
 

1

汎用性なし

2021/03/05 09:23

投稿

babu_babu_baboo
babu_babu_baboo

スコア616

answer CHANGED
@@ -169,4 +169,28 @@
169
169
  </script>
170
170
 
171
171
 
172
+ ```
173
+
174
+ ```js
175
+ <a href="#"><h3 class="LC20lb"><span>BELIEVE</span></h3></a>
176
+ <a href="#"><h3 class="LC20lb"><span>BELIEVE</span></h3></a>
177
+ <a href="#"><h3 class="LC20lb"><span>BELIEVE</span></h3></a>
178
+ <a href="#"><h3 class="LC20lb"><span>BELIEVE</span></h3></a>
179
+
180
+
181
+ <script>
182
+ const walker = document.createTreeWalker (document.body, NodeFilter.SHOW_ELEMENT, filter, true);
183
+
184
+ function filter (node) {
185
+ return 'A' === node.nodeName
186
+ ? NodeFilter.FILTER_ACCEPT
187
+ : NodeFilter.FILTER_SKIP;
188
+ }
189
+
190
+ function handler ({ ctrlKey, key, target: e }, n) {
191
+ (ctrlKey && 'ArrowDown' === key) &&
192
+ (walker.currentNode = document.activeElement, n = walker.nextNode (), n && n.focus ());
193
+ }
194
+
195
+ document.addEventListener ('keydown', handler, true);
172
196
  ```