View Javadoc
1   /*
2    * Copyright (c) 2002-2025 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_END;
23  import static org.htmlunit.javascript.host.event.KeyboardEvent.DOM_VK_EQUALS;
24  import static org.htmlunit.javascript.host.event.KeyboardEvent.DOM_VK_HOME;
25  import static org.htmlunit.javascript.host.event.KeyboardEvent.DOM_VK_LEFT;
26  import static org.htmlunit.javascript.host.event.KeyboardEvent.DOM_VK_MULTIPLY;
27  import static org.htmlunit.javascript.host.event.KeyboardEvent.DOM_VK_NUMPAD0;
28  import static org.htmlunit.javascript.host.event.KeyboardEvent.DOM_VK_NUMPAD9;
29  import static org.htmlunit.javascript.host.event.KeyboardEvent.DOM_VK_RIGHT;
30  import static org.htmlunit.javascript.host.event.KeyboardEvent.DOM_VK_SEMICOLON;
31  import static org.htmlunit.javascript.host.event.KeyboardEvent.DOM_VK_SEPARATOR;
32  import static org.htmlunit.javascript.host.event.KeyboardEvent.DOM_VK_SPACE;
33  import static org.htmlunit.javascript.host.event.KeyboardEvent.DOM_VK_SUBTRACT;
34  
35  import java.io.Serializable;
36  import java.util.HashMap;
37  import java.util.Map;
38  
39  import org.htmlunit.ClipboardHandler;
40  import org.htmlunit.html.impl.SelectionDelegate;
41  
42  /**
43   * The processor for {@link HtmlElement#doType(char, boolean)}
44   * and {@link HtmlElement#doType(int, boolean)}.
45   *
46   * @author Marc Guillemot
47   * @author Ronald Brill
48   * @author Ahmed Ashour
49   */
50  class DoTypeProcessor implements Serializable {
51  
52      private static final Map<Integer, Character> SPECIAL_KEYS_MAP_ = new HashMap<>();
53  
54      /**
55       * Either {@link HtmlElement} or {@link DomText}.
56       */
57      private final DomNode domNode_;
58  
59      static {
60          SPECIAL_KEYS_MAP_.put(DOM_VK_ADD, '+');
61          SPECIAL_KEYS_MAP_.put(DOM_VK_DECIMAL, '.');
62          SPECIAL_KEYS_MAP_.put(DOM_VK_DIVIDE, '/');
63          SPECIAL_KEYS_MAP_.put(DOM_VK_EQUALS, '=');
64          SPECIAL_KEYS_MAP_.put(DOM_VK_MULTIPLY, '*');
65          SPECIAL_KEYS_MAP_.put(DOM_VK_SEMICOLON, ';');
66          SPECIAL_KEYS_MAP_.put(DOM_VK_SEPARATOR, ',');
67          SPECIAL_KEYS_MAP_.put(DOM_VK_SPACE, ' ');
68          SPECIAL_KEYS_MAP_.put(DOM_VK_SUBTRACT, '-');
69  
70          for (int i = DOM_VK_NUMPAD0; i <= DOM_VK_NUMPAD9; i++) {
71              SPECIAL_KEYS_MAP_.put(i, (char) ('0' + i - DOM_VK_NUMPAD0));
72          }
73      }
74  
75      DoTypeProcessor(final DomNode domNode) {
76          domNode_ = domNode;
77      }
78  
79      void doType(final String currentValue, final SelectionDelegate selectionDelegate,
80              final char c, final HtmlElement element, final boolean lastType) {
81  
82          int selectionStart = selectionDelegate.getSelectionStart();
83          selectionStart = Math.max(0, Math.min(selectionStart, currentValue.length()));
84  
85          int selectionEnd = selectionDelegate.getSelectionEnd();
86          selectionEnd = Math.max(selectionStart, Math.min(selectionEnd, currentValue.length()));
87  
88          final StringBuilder newValue = new StringBuilder(currentValue);
89          if (c == '\b') {
90              if (selectionStart > 0) {
91                  newValue.deleteCharAt(selectionStart - 1);
92                  selectionStart--;
93                  selectionEnd--;
94              }
95          }
96          else if (acceptChar(c)) {
97              final boolean ctrlKey = element.isCtrlPressed();
98              if (ctrlKey && (c == 'C' || c == 'c')) {
99                  final ClipboardHandler clipboardHandler = element.getPage().getWebClient().getClipboardHandler();
100                 if (clipboardHandler != null) {
101                     final String content = newValue.substring(selectionStart, selectionEnd);
102                     clipboardHandler.setClipboardContent(content);
103                 }
104             }
105             else if (ctrlKey && (c == 'V' || c == 'v')) {
106                 final ClipboardHandler clipboardHandler = element.getPage().getWebClient().getClipboardHandler();
107                 if (clipboardHandler != null) {
108                     final String content = clipboardHandler.getClipboardContent();
109                     add(newValue, content, selectionStart, selectionEnd);
110                     selectionStart += content.length();
111                     selectionEnd = selectionStart;
112                 }
113             }
114             else if (ctrlKey && (c == 'X' || c == 'x')) {
115                 final ClipboardHandler clipboardHandler = element.getPage().getWebClient().getClipboardHandler();
116                 if (clipboardHandler != null) {
117                     final String content = newValue.substring(selectionStart, selectionEnd);
118                     clipboardHandler.setClipboardContent(content);
119                     newValue.delete(selectionStart, selectionEnd);
120                     selectionEnd = selectionStart;
121                 }
122             }
123             else if (ctrlKey && (c == 'A' || c == 'a')) {
124                 selectionStart = 0;
125                 selectionEnd = newValue.length();
126             }
127             else {
128                 add(newValue, c, selectionStart, selectionEnd);
129                 selectionStart++;
130                 selectionEnd = selectionStart;
131             }
132         }
133 
134         typeDone(newValue.toString(), lastType);
135 
136         selectionDelegate.setSelectionStart(selectionStart);
137         selectionDelegate.setSelectionEnd(selectionEnd);
138     }
139 
140     private static void add(final StringBuilder newValue, final char c, final int selectionStart,
141             final int selectionEnd) {
142         if (selectionStart == newValue.length()) {
143             newValue.append(c);
144         }
145         else {
146             newValue.replace(selectionStart, selectionEnd, Character.toString(c));
147         }
148     }
149 
150     private static void add(final StringBuilder newValue, final String string, final int selectionStart,
151             final int selectionEnd) {
152         if (selectionStart == newValue.length()) {
153             newValue.append(string);
154         }
155         else {
156             newValue.replace(selectionStart, selectionEnd, string);
157         }
158     }
159 
160     private void typeDone(final String newValue, final boolean notifyAttributeChangeListeners) {
161         if (domNode_ instanceof DomText) {
162             ((DomText) domNode_).setData(newValue);
163         }
164         else {
165             ((HtmlElement) domNode_).typeDone(newValue, notifyAttributeChangeListeners);
166         }
167     }
168 
169     private boolean acceptChar(final char ch) {
170         if (domNode_ instanceof DomText) {
171             return ((DomText) domNode_).acceptChar(ch);
172         }
173         return ((HtmlElement) domNode_).acceptChar(ch);
174     }
175 
176     void doType(final String currentValue, final SelectionDelegate selectionDelegate,
177             final int keyCode, final HtmlElement element, final boolean lastType) {
178 
179         int selectionStart = selectionDelegate.getSelectionStart();
180         selectionStart = Math.max(0, Math.min(selectionStart, currentValue.length()));
181 
182         int selectionEnd = selectionDelegate.getSelectionEnd();
183         selectionEnd = Math.max(selectionStart, Math.min(selectionEnd, currentValue.length()));
184 
185         final Character ch = SPECIAL_KEYS_MAP_.get(keyCode);
186         if (ch != null) {
187             doType(currentValue, selectionDelegate, ch, element, lastType);
188             return;
189         }
190 
191         final StringBuilder newValue = new StringBuilder(currentValue);
192         switch (keyCode) {
193             case DOM_VK_BACK_SPACE:
194                 if (selectionStart > 0) {
195                     newValue.deleteCharAt(selectionStart - 1);
196                     selectionStart--;
197                 }
198                 break;
199 
200             case DOM_VK_LEFT:
201                 if (element.isCtrlPressed()) {
202                     while (selectionStart > 0 && newValue.charAt(selectionStart - 1) != ' ') {
203                         selectionStart--;
204                     }
205                 }
206                 else if (selectionStart > 0) {
207                     selectionStart--;
208                 }
209                 break;
210 
211             case DOM_VK_RIGHT:
212                 if (element.isCtrlPressed()) {
213                     if (selectionStart < newValue.length()) {
214                         selectionStart++;
215                     }
216                     while (selectionStart < newValue.length() && newValue.charAt(selectionStart - 1) != ' ') {
217                         selectionStart++;
218                     }
219                 }
220                 else if (element.isShiftPressed()) {
221                     selectionEnd++;
222                 }
223                 else if (selectionStart > 0) {
224                     selectionStart++;
225                 }
226                 break;
227 
228             case DOM_VK_HOME:
229                 selectionStart = 0;
230                 break;
231 
232             case DOM_VK_END:
233                 if (element.isShiftPressed()) {
234                     selectionEnd = newValue.length();
235                 }
236                 else {
237                     selectionStart = newValue.length();
238                 }
239                 break;
240 
241             case DOM_VK_DELETE:
242                 if (selectionEnd == selectionStart) {
243                     selectionEnd++;
244                 }
245                 newValue.delete(selectionStart, selectionEnd);
246                 selectionEnd = selectionStart;
247                 break;
248 
249             default:
250                 return;
251         }
252 
253         if (!element.isShiftPressed()) {
254             selectionEnd = selectionStart;
255         }
256 
257         typeDone(newValue.toString(), lastType);
258 
259         selectionDelegate.setSelectionStart(selectionStart);
260         selectionDelegate.setSelectionEnd(selectionEnd);
261     }
262 }