View Javadoc
1   /*
2    * Copyright (c) 2002-2026 Gargoyle Software Inc.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * https://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed under the License is distributed on an "AS IS" BASIS,
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12   * See the License for the specific language governing permissions and
13   * limitations under the License.
14   */
15  package org.htmlunit.html;
16  
17  import static org.htmlunit.javascript.host.event.KeyboardEvent.DOM_VK_ADD;
18  import static org.htmlunit.javascript.host.event.KeyboardEvent.DOM_VK_BACK_SPACE;
19  import static org.htmlunit.javascript.host.event.KeyboardEvent.DOM_VK_DECIMAL;
20  import static org.htmlunit.javascript.host.event.KeyboardEvent.DOM_VK_DELETE;
21  import static org.htmlunit.javascript.host.event.KeyboardEvent.DOM_VK_DIVIDE;
22  import static org.htmlunit.javascript.host.event.KeyboardEvent.DOM_VK_DOWN;
23  import static org.htmlunit.javascript.host.event.KeyboardEvent.DOM_VK_END;
24  import static org.htmlunit.javascript.host.event.KeyboardEvent.DOM_VK_EQUALS;
25  import static org.htmlunit.javascript.host.event.KeyboardEvent.DOM_VK_HOME;
26  import static org.htmlunit.javascript.host.event.KeyboardEvent.DOM_VK_LEFT;
27  import static org.htmlunit.javascript.host.event.KeyboardEvent.DOM_VK_MULTIPLY;
28  import static org.htmlunit.javascript.host.event.KeyboardEvent.DOM_VK_NUMPAD0;
29  import static org.htmlunit.javascript.host.event.KeyboardEvent.DOM_VK_NUMPAD9;
30  import static org.htmlunit.javascript.host.event.KeyboardEvent.DOM_VK_RIGHT;
31  import static org.htmlunit.javascript.host.event.KeyboardEvent.DOM_VK_SEMICOLON;
32  import static org.htmlunit.javascript.host.event.KeyboardEvent.DOM_VK_SEPARATOR;
33  import static org.htmlunit.javascript.host.event.KeyboardEvent.DOM_VK_SPACE;
34  import static org.htmlunit.javascript.host.event.KeyboardEvent.DOM_VK_SUBTRACT;
35  import static org.htmlunit.javascript.host.event.KeyboardEvent.DOM_VK_UP;
36  
37  import java.io.Serializable;
38  import java.util.HashMap;
39  import java.util.Map;
40  
41  import org.htmlunit.ClipboardHandler;
42  import org.htmlunit.html.impl.SelectionDelegate;
43  
44  /**
45   * The processor for {@link HtmlElement#doType(char, boolean)}
46   * and {@link HtmlElement#doType(int, boolean)}.
47   *
48   * @author Marc Guillemot
49   * @author Ronald Brill
50   * @author Ahmed Ashour
51   */
52  class DoTypeProcessor implements Serializable {
53  
54      private static final Map<Integer, Character> SPECIAL_KEYS_MAP_ = new HashMap<>();
55  
56      /**
57       * Either {@link HtmlElement} or {@link DomText}.
58       */
59      private final DomNode domNode_;
60  
61      static {
62          SPECIAL_KEYS_MAP_.put(DOM_VK_ADD, '+');
63          SPECIAL_KEYS_MAP_.put(DOM_VK_DECIMAL, '.');
64          SPECIAL_KEYS_MAP_.put(DOM_VK_DIVIDE, '/');
65          SPECIAL_KEYS_MAP_.put(DOM_VK_EQUALS, '=');
66          SPECIAL_KEYS_MAP_.put(DOM_VK_MULTIPLY, '*');
67          SPECIAL_KEYS_MAP_.put(DOM_VK_SEMICOLON, ';');
68          SPECIAL_KEYS_MAP_.put(DOM_VK_SEPARATOR, ',');
69          SPECIAL_KEYS_MAP_.put(DOM_VK_SPACE, ' ');
70          SPECIAL_KEYS_MAP_.put(DOM_VK_SUBTRACT, '-');
71  
72          for (int i = DOM_VK_NUMPAD0; i <= DOM_VK_NUMPAD9; i++) {
73              SPECIAL_KEYS_MAP_.put(i, (char) ('0' + i - DOM_VK_NUMPAD0));
74          }
75      }
76  
77      DoTypeProcessor(final DomNode domNode) {
78          domNode_ = domNode;
79      }
80  
81      void doType(final String currentValue, final SelectionDelegate selectionDelegate,
82              final char c, final HtmlElement element, final boolean lastType) {
83  
84          int selectionStart = selectionDelegate.getSelectionStart();
85          selectionStart = Math.max(0, Math.min(selectionStart, currentValue.length()));
86  
87          int selectionEnd = selectionDelegate.getSelectionEnd();
88          selectionEnd = Math.max(selectionStart, Math.min(selectionEnd, currentValue.length()));
89  
90          final StringBuilder newValue = new StringBuilder(currentValue);
91          if (c == '\b') {
92              if (selectionStart > 0) {
93                  newValue.deleteCharAt(selectionStart - 1);
94                  selectionStart--;
95                  selectionEnd--;
96              }
97          }
98          else if (acceptChar(c)) {
99              final boolean ctrlKey = element.isCtrlPressed();
100             if (ctrlKey && (c == 'C' || c == 'c')) {
101                 final ClipboardHandler clipboardHandler = element.getPage().getWebClient().getClipboardHandler();
102                 if (clipboardHandler != null) {
103                     final String content = newValue.substring(selectionStart, selectionEnd);
104                     clipboardHandler.setClipboardContent(content);
105                 }
106             }
107             else if (ctrlKey && (c == 'V' || c == 'v')) {
108                 final ClipboardHandler clipboardHandler = element.getPage().getWebClient().getClipboardHandler();
109                 if (clipboardHandler != null) {
110                     final String content = clipboardHandler.getClipboardContent();
111                     add(newValue, content, selectionStart, selectionEnd);
112                     selectionStart += content.length();
113                     selectionEnd = selectionStart;
114                 }
115             }
116             else if (ctrlKey && (c == 'X' || c == 'x')) {
117                 final ClipboardHandler clipboardHandler = element.getPage().getWebClient().getClipboardHandler();
118                 if (clipboardHandler != null) {
119                     final String content = newValue.substring(selectionStart, selectionEnd);
120                     clipboardHandler.setClipboardContent(content);
121                     newValue.delete(selectionStart, selectionEnd);
122                     selectionEnd = selectionStart;
123                 }
124             }
125             else if (ctrlKey && (c == 'A' || c == 'a')) {
126                 selectionStart = 0;
127                 selectionEnd = newValue.length();
128             }
129             else {
130                 add(newValue, c, selectionStart, selectionEnd);
131                 selectionStart++;
132                 selectionEnd = selectionStart;
133             }
134         }
135 
136         typeDone(newValue.toString(), lastType);
137 
138         selectionDelegate.setSelectionStart(selectionStart);
139         selectionDelegate.setSelectionEnd(selectionEnd);
140     }
141 
142     private static void add(final StringBuilder newValue, final char c, final int selectionStart,
143             final int selectionEnd) {
144         if (selectionStart == newValue.length()) {
145             newValue.append(c);
146         }
147         else {
148             newValue.replace(selectionStart, selectionEnd, Character.toString(c));
149         }
150     }
151 
152     private static void add(final StringBuilder newValue, final String string, final int selectionStart,
153             final int selectionEnd) {
154         if (selectionStart == newValue.length()) {
155             newValue.append(string);
156         }
157         else {
158             newValue.replace(selectionStart, selectionEnd, string);
159         }
160     }
161 
162     private void typeDone(final String newValue, final boolean notifyAttributeChangeListeners) {
163         if (domNode_ instanceof DomText text) {
164             text.setData(newValue);
165         }
166         else {
167             ((HtmlElement) domNode_).typeDone(newValue, notifyAttributeChangeListeners);
168         }
169     }
170 
171     private boolean acceptChar(final char ch) {
172         if (domNode_ instanceof DomText text) {
173             return text.acceptChar(ch);
174         }
175         return ((HtmlElement) domNode_).acceptChar(ch);
176     }
177 
178     void doType(final String currentValue, final SelectionDelegate selectionDelegate,
179             final int keyCode, final HtmlElement element, final boolean lastType) {
180 
181         int selectionStart = selectionDelegate.getSelectionStart();
182         selectionStart = Math.max(0, Math.min(selectionStart, currentValue.length()));
183 
184         int selectionEnd = selectionDelegate.getSelectionEnd();
185         selectionEnd = Math.max(selectionStart, Math.min(selectionEnd, currentValue.length()));
186 
187         final Character ch = SPECIAL_KEYS_MAP_.get(keyCode);
188         if (ch != null) {
189             doType(currentValue, selectionDelegate, ch, element, lastType);
190             return;
191         }
192 
193         final StringBuilder newValue = new StringBuilder(currentValue);
194         switch (keyCode) {
195             case DOM_VK_BACK_SPACE:
196                 if (selectionEnd != selectionStart) {
197                     newValue.delete(selectionStart, selectionEnd);
198                     selectionEnd = selectionStart;
199                     break;
200                 }
201 
202                 if (selectionStart > 0) {
203                     if (element.isCtrlPressed()) {
204                         int targetStart = selectionStart;
205                         while (targetStart > 0 && Character.isWhitespace(newValue.charAt(targetStart - 1))) {
206                             targetStart--;
207                         }
208                         while (targetStart > 0 && !Character.isWhitespace(newValue.charAt(targetStart - 1))) {
209                             targetStart--;
210                         }
211                         newValue.delete(targetStart, selectionStart);
212                         selectionStart = targetStart;
213                     }
214                     else {
215                         newValue.deleteCharAt(selectionStart - 1);
216                         selectionStart--;
217                     }
218                 }
219                 break;
220 
221             case DOM_VK_LEFT:
222                 if (element.isCtrlPressed()) {
223                     int targetPos = selectionStart;
224                     // 1. Skip whitespace to the left of caret
225                     while (targetPos > 0 && Character.isWhitespace(newValue.charAt(targetPos - 1))) {
226                         targetPos--;
227                     }
228                     // 2. Skip word characters to the left
229                     while (targetPos > 0 && !Character.isWhitespace(newValue.charAt(targetPos - 1))) {
230                         targetPos--;
231                     }
232                     selectionStart = targetPos;
233                 }
234                 else if (element.isShiftPressed()) {
235                     if (selectionStart > 0) {
236                         selectionStart--;
237                     }
238                 }
239                 else if (selectionStart > 0) {
240                     selectionStart--;
241                 }
242                 break;
243 
244             case DOM_VK_RIGHT:
245                 if (element.isCtrlPressed()) {
246                     int targetPos = selectionStart;
247                     // 1. Skip word characters to the right
248                     while (targetPos < newValue.length() && !Character.isWhitespace(newValue.charAt(targetPos))) {
249                         targetPos++;
250                     }
251                     // 2. Skip whitespace to the right
252                     while (targetPos < newValue.length() && Character.isWhitespace(newValue.charAt(targetPos))) {
253                         targetPos++;
254                     }
255                     if (element.isShiftPressed()) {
256                         selectionEnd = targetPos;
257                     }
258                     else {
259                         selectionStart = targetPos;
260                     }
261                 }
262                 else if (element.isShiftPressed()) {
263                     if (selectionEnd < newValue.length()) {
264                         selectionEnd++;
265                     }
266                 }
267                 else if (selectionStart < newValue.length()) {
268                     selectionStart++;
269                 }
270                 break;
271 
272             case DOM_VK_UP:
273                 final int lastLfUp = currentValue.lastIndexOf('\n', selectionStart - 1);
274                 final int lastCrUp = currentValue.lastIndexOf('\r', selectionStart - 1);
275                 final int lastBreakUp = Math.max(lastLfUp, lastCrUp);
276 
277                 if (lastBreakUp == -1) {
278                     selectionStart = 0;
279                 }
280                 else {
281                     final int currentLineStart = lastBreakUp + 1;
282                     final int column = selectionStart - currentLineStart;
283 
284                     final int prevLineEnd = (lastBreakUp > 0 && newValue.charAt(lastBreakUp) == '\n'
285                             && newValue.charAt(lastBreakUp - 1) == '\r') ? lastBreakUp - 1 : lastBreakUp;
286 
287                     final int prevLf = currentValue.lastIndexOf('\n', prevLineEnd - 1);
288                     final int prevCr = currentValue.lastIndexOf('\r', prevLineEnd - 1);
289                     final int prevBreak = Math.max(prevLf, prevCr);
290 
291                     final int prevLineStart = (prevBreak == -1) ? 0 : prevBreak + 1;
292                     final int prevLineLength = prevLineEnd - prevLineStart;
293 
294                     selectionStart = prevLineStart + Math.min(column, prevLineLength);
295                 }
296                 break;
297 
298             case DOM_VK_DOWN:
299                 final int lastLfDown = currentValue.lastIndexOf('\n', selectionStart - 1);
300                 final int lastCrDown = currentValue.lastIndexOf('\r', selectionStart - 1);
301                 final int lastBreakDown = Math.max(lastLfDown, lastCrDown);
302                 final int currentLineStartDown = (lastBreakDown == -1) ? 0 : lastBreakDown + 1;
303                 final int columnDown = selectionStart - currentLineStartDown;
304 
305                 final int nextLfDown = currentValue.indexOf('\n', selectionStart);
306                 final int nextCrDown = currentValue.indexOf('\r', selectionStart);
307                 final int nextBreakDown;
308                 if (nextLfDown == -1) {
309                     nextBreakDown = nextCrDown;
310                 }
311                 else if (nextCrDown == -1) {
312                     nextBreakDown = nextLfDown;
313                 }
314                 else {
315                     nextBreakDown = Math.min(nextLfDown, nextCrDown);
316                 }
317 
318                 final int targetDown;
319                 if (nextBreakDown == -1) {
320                     targetDown = newValue.length();
321                 }
322                 else {
323                     int nextLineStart = nextBreakDown + 1;
324                     if (newValue.charAt(nextBreakDown) == '\r' && nextLineStart < newValue.length()
325                             && newValue.charAt(nextLineStart) == '\n') {
326                         nextLineStart++;
327                     }
328 
329                     final int followingLf = currentValue.indexOf('\n', nextLineStart);
330                     final int followingCr = currentValue.indexOf('\r', nextLineStart);
331                     final int followingBreak;
332                     if (followingLf == -1) {
333                         followingBreak = followingCr;
334                     }
335                     else if (followingCr == -1) {
336                         followingBreak = followingLf;
337                     }
338                     else {
339                         followingBreak = Math.min(followingLf, followingCr);
340                     }
341 
342                     final int nextLineEnd = (followingBreak == -1) ? newValue.length() : followingBreak;
343                     final int nextLineLength = nextLineEnd - nextLineStart;
344 
345                     targetDown = nextLineStart + Math.min(columnDown, nextLineLength);
346                 }
347 
348                 if (element.isShiftPressed()) {
349                     selectionEnd = targetDown;
350                 }
351                 else {
352                     selectionStart = targetDown;
353                 }
354                 break;
355 
356             case DOM_VK_HOME:
357                 final int targetHome;
358                 if (element.isCtrlPressed()) {
359                     targetHome = 0;
360                 }
361                 else {
362                     final int lastLf = currentValue.lastIndexOf('\n', selectionStart - 1);
363                     final int lastCr = currentValue.lastIndexOf('\r', selectionStart - 1);
364                     // Use Math.max to find the closest line break before the caret,
365                     // so the line start is positioned right after it
366                     final int lastBreak = Math.max(lastLf, lastCr);
367                     targetHome = (lastBreak == -1) ? 0 : lastBreak + 1;
368                 }
369 
370                 if (element.isShiftPressed()) {
371                     selectionStart = targetHome;
372                 }
373                 else {
374                     selectionStart = targetHome;
375                     selectionEnd = targetHome;
376                 }
377                 break;
378 
379             case DOM_VK_END:
380                 int targetEnd;
381                 if (element.isCtrlPressed()) {
382                     targetEnd = newValue.length();
383                 }
384                 else {
385                     final int nextLf = currentValue.indexOf('\n', selectionStart);
386                     final int nextCr = currentValue.indexOf('\r', selectionStart);
387                     if (nextLf == -1) {
388                         targetEnd = nextCr;
389                     }
390                     else if (nextCr == -1) {
391                         targetEnd = nextLf;
392                     }
393                     else {
394                         targetEnd = Math.min(nextLf, nextCr);
395                     }
396                     if (targetEnd == -1) {
397                         targetEnd = newValue.length();
398                     }
399                 }
400 
401                 if (element.isShiftPressed()) {
402                     selectionEnd = targetEnd;
403                 }
404                 else {
405                     selectionStart = targetEnd;
406                 }
407                 break;
408 
409             case DOM_VK_DELETE:
410                 if (selectionEnd != selectionStart) {
411                     newValue.delete(selectionStart, selectionEnd);
412                     selectionEnd = selectionStart;
413                     break;
414                 }
415 
416                 if (selectionStart < newValue.length()) {
417                     if (element.isCtrlPressed()) {
418                         int delTargetEnd = selectionStart;
419                         // 1. Skip initial whitespace at caret (if caret is on a space)
420                         while (delTargetEnd < newValue.length()
421                                 && Character.isWhitespace(newValue.charAt(delTargetEnd))) {
422                             delTargetEnd++;
423                         }
424                         // 2. Skip word characters
425                         while (delTargetEnd < newValue.length()
426                                 && !Character.isWhitespace(newValue.charAt(delTargetEnd))) {
427                             delTargetEnd++;
428                         }
429                         // 3. Skip trailing whitespace after the word
430                         while (delTargetEnd < newValue.length()
431                                 && Character.isWhitespace(newValue.charAt(delTargetEnd))) {
432                             delTargetEnd++;
433                         }
434                         newValue.delete(selectionStart, delTargetEnd);
435                     }
436                     else {
437                         newValue.deleteCharAt(selectionStart);
438                     }
439                 }
440                 break;
441 
442             default:
443                 return;
444         }
445 
446         if (!element.isShiftPressed()) {
447             selectionEnd = selectionStart;
448         }
449 
450         typeDone(newValue.toString(), lastType);
451 
452         selectionDelegate.setSelectionStart(selectionStart);
453         selectionDelegate.setSelectionEnd(selectionEnd);
454     }
455 }