View Javadoc
1   /*
2    * Copyright (c) 2002-2017 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    * http://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 com.gargoylesoftware.htmlunit.javascript.host.dom;
16  
17  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.JS_DOMTOKENLIST_CONTAINS_RETURNS_FALSE_FOR_BLANK;
18  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.JS_DOMTOKENLIST_ENHANCED_WHITESPACE_CHARS;
19  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.JS_DOMTOKENLIST_GET_NULL_IF_OUTSIDE;
20  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.JS_DOMTOKENLIST_LENGTH_IGNORES_DUPLICATES;
21  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.JS_DOMTOKENLIST_REMOVE_WHITESPACE_CHARS_ON_ADD;
22  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.JS_DOMTOKENLIST_REMOVE_WHITESPACE_CHARS_ON_EDIT;
23  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.JS_DOMTOKENLIST_REMOVE_WHITESPACE_CHARS_ON_REMOVE;
24  import static com.gargoylesoftware.htmlunit.javascript.configuration.SupportedBrowser.CHROME;
25  import static com.gargoylesoftware.htmlunit.javascript.configuration.SupportedBrowser.EDGE;
26  import static com.gargoylesoftware.htmlunit.javascript.configuration.SupportedBrowser.FF;
27  
28  import java.util.HashSet;
29  
30  import org.apache.commons.lang3.StringUtils;
31  
32  import com.gargoylesoftware.htmlunit.html.DomAttr;
33  import com.gargoylesoftware.htmlunit.html.DomElement;
34  import com.gargoylesoftware.htmlunit.html.DomNode;
35  import com.gargoylesoftware.htmlunit.javascript.SimpleScriptable;
36  import com.gargoylesoftware.htmlunit.javascript.configuration.JsxClass;
37  import com.gargoylesoftware.htmlunit.javascript.configuration.JsxConstructor;
38  import com.gargoylesoftware.htmlunit.javascript.configuration.JsxFunction;
39  import com.gargoylesoftware.htmlunit.javascript.configuration.JsxGetter;
40  
41  import net.sourceforge.htmlunit.corejs.javascript.Context;
42  import net.sourceforge.htmlunit.corejs.javascript.Scriptable;
43  import net.sourceforge.htmlunit.corejs.javascript.Undefined;
44  
45  /**
46   * A JavaScript object for {@code DOMTokenList}.
47   *
48   * @author Ahmed Ashour
49   * @author Ronald Brill
50   * @author Marek Gawlicki
51   */
52  @JsxClass
53  public class DOMTokenList extends SimpleScriptable {
54  
55      private static final String WHITESPACE_CHARS = " \t\r\n\u000C";
56      private static final String WHITESPACE_CHARS_IE_11 = WHITESPACE_CHARS + "\u000B";
57  
58      private String attributeName_;
59  
60      /**
61       * Creates an instance.
62       */
63      @JsxConstructor({CHROME, FF, EDGE})
64      public DOMTokenList() {
65      }
66  
67      /**
68       * Creates an instance.
69       * @param node the node which contains the underlying string
70       * @param attributeName the attribute name of the DomElement of the specified node
71       */
72      public DOMTokenList(final Node node, final String attributeName) {
73          setDomNode(node.getDomNodeOrDie(), false);
74          setParentScope(node.getParentScope());
75          setPrototype(getPrototype(getClass()));
76          attributeName_ = attributeName;
77      }
78  
79      /**
80       * Returns the length property.
81       * @return the length
82       */
83      @JsxGetter
84      public int getLength() {
85          final String value = getDefaultValue(null);
86          final String[] parts = StringUtils.split(value, whitespaceChars());
87          if (getBrowserVersion().hasFeature(JS_DOMTOKENLIST_LENGTH_IGNORES_DUPLICATES)) {
88              final HashSet<String> elements = new HashSet<>(parts.length);
89              for (int i = 0; i < parts.length; i++) {
90                  elements.add(parts[i]);
91              }
92              return elements.size();
93          }
94          return parts.length;
95      }
96  
97      /**
98       * {@inheritDoc}
99       */
100     @Override
101     public String getDefaultValue(final Class<?> hint) {
102         if (getPrototype() == null) {
103             return (String) super.getDefaultValue(hint);
104         }
105         final DomNode node = getDomNodeOrNull();
106         if (node != null) {
107             final DomAttr attr = (DomAttr) node.getAttributes().getNamedItem(attributeName_);
108             if (attr != null) {
109                 String value = attr.getValue();
110                 if (getBrowserVersion().hasFeature(JS_DOMTOKENLIST_REMOVE_WHITESPACE_CHARS_ON_EDIT)) {
111                     value = String.join(" ", StringUtils.split(value, whitespaceChars()));
112                 }
113                 return value;
114             }
115         }
116         return "";
117     }
118 
119     /**
120      * Adds the specified token to the underlying string.
121      * @param token the token to add
122      */
123     @JsxFunction
124     public void add(final String token) {
125         if (StringUtils.isEmpty(token)) {
126             throw Context.reportRuntimeError("Empty imput not allowed");
127         }
128         if (StringUtils.containsAny(token, whitespaceChars())) {
129             throw Context.reportRuntimeError("Empty imput not allowed");
130         }
131 
132         String value = getDefaultValue(null);
133         boolean changed = false;
134         if (position(value, token) < 0) {
135             if (value.length() != 0 && !isWhitespache(value.charAt(value.length() - 1))) {
136                 value = value + " ";
137             }
138             value = value + token;
139             changed = true;
140         }
141         else if (getBrowserVersion().hasFeature(JS_DOMTOKENLIST_REMOVE_WHITESPACE_CHARS_ON_ADD)) {
142             value = String.join(" ", StringUtils.split(value, whitespaceChars()));
143             changed = true;
144         }
145 
146         if (changed) {
147             updateAttribute(value);
148         }
149     }
150 
151     /**
152      * Removes the specified token from the underlying string.
153      * @param token the token to remove
154      */
155     @JsxFunction
156     public void remove(final String token) {
157         if (StringUtils.isEmpty(token)) {
158             throw Context.reportRuntimeError("Empty imput not allowed");
159         }
160         if (StringUtils.containsAny(token, whitespaceChars())) {
161             throw Context.reportRuntimeError("Empty imput not allowed");
162         }
163 
164         String value = getDefaultValue(null);
165         boolean changed = false;
166         int pos = position(value, token);
167         while (pos != -1) {
168             int from = pos;
169             int to = pos + token.length();
170 
171             while (from > 0 && isWhitespache(value.charAt(from - 1))) {
172                 from = from - 1;
173             }
174             while (to < value.length() - 1 && isWhitespache(value.charAt(to))) {
175                 to = to + 1;
176             }
177 
178             final StringBuilder result = new StringBuilder();
179             if (from > 0) {
180                 result.append(value, 0, from);
181                 if (to < value.length()) {
182                     result.append(" ");
183                 }
184             }
185             result.append(value, to, value.length());
186             value = result.toString();
187             changed = true;
188 
189             pos = position(value, token);
190         }
191 
192         if (getBrowserVersion().hasFeature(JS_DOMTOKENLIST_REMOVE_WHITESPACE_CHARS_ON_REMOVE)) {
193             value = String.join(" ", StringUtils.split(value, whitespaceChars()));
194             changed = true;
195         }
196 
197         if (changed) {
198             updateAttribute(value);
199         }
200     }
201 
202     /**
203      * Toggle the token, by adding or removing.
204      * @param token the token to add or remove
205      * @return whether the string now contains the token or not
206      */
207     @JsxFunction
208     public boolean toggle(final String token) {
209         if (contains(token)) {
210             remove(token);
211             return false;
212         }
213         add(token);
214         return true;
215     }
216 
217     /**
218      * Checks if the specified token is contained in the underlying string.
219      * @param token the token to add
220      * @return true if the underlying string contains token, otherwise false
221      */
222     @JsxFunction
223     public boolean contains(final String token) {
224         if (getBrowserVersion().hasFeature(JS_DOMTOKENLIST_CONTAINS_RETURNS_FALSE_FOR_BLANK)
225                 && StringUtils.isBlank(token)) {
226             return false;
227         }
228 
229         if (StringUtils.isEmpty(token)) {
230             throw Context.reportRuntimeError("Empty imput not allowed");
231         }
232         if (StringUtils.containsAny(token, whitespaceChars())) {
233             throw Context.reportRuntimeError("Empty imput not allowed");
234         }
235         return position(getDefaultValue(null), token) > -1;
236     }
237 
238     /**
239      * Returns the item at the specified index.
240      * @param index the index of the item
241      * @return the item
242      */
243     @JsxFunction
244     public Object item(final int index) {
245         if (index < 0) {
246             return null;
247         }
248         final String value = getDefaultValue(null);
249         final String[] values = StringUtils.split(value, whitespaceChars());
250         if (index < values.length) {
251             return values[index];
252         }
253         return null;
254     }
255 
256     /**
257      * {@inheritDoc}
258      */
259     @Override
260     public Object get(final int index, final Scriptable start) {
261         final Object value = item(index);
262         if (value == null && !getBrowserVersion().hasFeature(JS_DOMTOKENLIST_GET_NULL_IF_OUTSIDE)) {
263             return Undefined.instance;
264         }
265         return value;
266     }
267 
268     private void updateAttribute(final String value) {
269         final DomElement domNode = (DomElement) getDomNodeOrDie();
270         DomAttr attr = (DomAttr) domNode.getAttributes().getNamedItem(attributeName_);
271         if (null == attr) {
272             attr = domNode.getPage().createAttribute(attributeName_);
273             domNode.setAttributeNode(attr);
274         }
275         attr.setValue(value);
276     }
277 
278     private int position(final String value, final String token) {
279         final int pos = value.indexOf(token);
280         if (pos < 0) {
281             return -1;
282         }
283 
284         // whitespace before
285         if (pos != 0 && !isWhitespache(value.charAt(pos - 1))) {
286             return -1;
287         }
288 
289         // whitespace after
290         final int end = pos + token.length();
291         if (end != value.length() && !isWhitespache(value.charAt(end))) {
292             return -1;
293         }
294         return pos;
295     }
296 
297     private String whitespaceChars() {
298         if (getBrowserVersion().hasFeature(JS_DOMTOKENLIST_ENHANCED_WHITESPACE_CHARS)) {
299             return WHITESPACE_CHARS_IE_11;
300         }
301         return WHITESPACE_CHARS;
302     }
303 
304     private boolean isWhitespache(final int ch) {
305         return whitespaceChars().indexOf(ch) > -1;
306     }
307 }