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