Revision: 5426
Author:   [email protected]
Date:     Tue May 28 14:58:38 2013
Log:      Implement element.classList.
https://codereview.appspot.com/9731044

element.classList is a DOM feature which allows adding, removing, and
testing for DOM classes on an element without doing string processing
on the space-separated tokens in .className. Add the classList property
and taming for its new list type.

Supporting changes:
* finishArrayLikeClass handles accessor properties on the prototype.
* confidence.amplifying()'s result is constFunc'd rather than def'd
  (not needed here, but spotted as an opportunity).

Fixes <https://code.google.com/p/google-caja/issues/detail?id=1744>.

[email protected]

http://code.google.com/p/google-caja/source/detail?r=5426

Modified:
 /trunk/src/com/google/caja/plugin/domado.js
 /trunk/tests/com/google/caja/plugin/es53-test-domado-dom-guest.html
 /trunk/tests/com/google/caja/plugin/es53-test-scan-guest.js

=======================================
--- /trunk/src/com/google/caja/plugin/domado.js Tue May 28 14:16:48 2013
+++ /trunk/src/com/google/caja/plugin/domado.js Tue May 28 14:58:38 2013
@@ -572,7 +572,7 @@
         setOwn(amplifierMethod, 'toString', cajaVM.constFunc(function() {
           return '[' + typename + ']' + method.toString();
         }));
-        return cajaVM.def(amplifierMethod);
+        return cajaVM.constFunc(amplifierMethod);
       };

       /**
@@ -3038,7 +3038,8 @@
         cajaVM.tamperProof(proto);
         Object.getOwnPropertyNames(proto).forEach(function(prop) {
           if (prop !== 'constructor') {
-            cajaVM.def(proto[prop]);
+ // transitively def the value or getter/setter, whichever exists
+            cajaVM.def(Object.getOwnPropertyDescriptor(proto, prop));
           }
         });
       }
@@ -3396,6 +3397,101 @@
                   historyInsensitiveVirtualizedSelectors));
         }
       }
+
+      /**
+       * DOMTokenList taming.
+       */
+      var TokenListConf = new Confidence('TameDOMTokenList');
+      function TameDOMTokenList(feral, getTransform, setTransform) {
+        feral = makeDOMAccessible(feral);
+        function getItem(i) {
+          return TameDOMTokenList.prototype.item.call(self, i);
+        }
+        function getLength() { return feral.length; }
+ var self = constructArrayLike(this.constructor, getItem, getLength);
+        TokenListConf.confide(self, taming);
+        TokenListConf.amplify(self, function(privates) {
+          privates.feral = feral;
+          privates.getT = getTransform;
+          privates.setT = setTransform;
+        });
+        return self;
+      }
+      registerArrayLikeClass(TameDOMTokenList);
+      Props.define(TameDOMTokenList.prototype, TokenListConf, {
+        length: PT.ro,
+        item: Props.ampMethod(function(privates, i) {
+          var ftoken = privates.feral.item(i);
+          return ftoken === null ? null : privates.getT(ftoken);
+        }),
+        contains: Props.ampMethod(function(privates, ttoken) {
+          var ftoken = privates.setT(String(ttoken));
+          if (ftoken === null) { return false; }
+          return !!privates.feral.contains(ftoken);
+        }),
+        add: Props.ampMethod(function(privates, ttoken) {
+          var ftoken = privates.setT(String(ttoken));
+          if (ftoken === null) { return; }
+          privates.feral.add(ftoken);
+        }),
+        remove: Props.ampMethod(function(privates, ttoken) {
+          var ftoken = privates.setT(String(ttoken));
+          if (ftoken === null) { return; }
+          privates.feral.remove(ftoken);
+        }),
+        toggle: Props.ampMethod(function(privates, ttoken) {
+          var ftoken = privates.setT(String(ttoken));
+          if (ftoken === null) { return false; }
+          return !!privates.feral.toggle(ftoken);
+        }),
+        toString: Props.ampMethod(function(privates) {
+          return privates.feral.toString().replace(/[^ \t\n\r\f]+/g,
+              function(ftoken) {
+            var ttoken = privates.getT(ftoken);
+            if (ttoken === null) { return ''; }
+            return ttoken;
+          });
+        })
+      });
+      finishArrayLikeClass(TameDOMTokenList);
+
+ function TameDOMSettableTokenList(feral, getTransform, setTransform) { + return TameDOMTokenList.call(this, feral, getTransform, setTransform);
+      }
+      registerArrayLikeClass(TameDOMSettableTokenList, TameDOMTokenList);
+      if (elementForFeatureTests.classList && makeDOMAccessible(
+          elementForFeatureTests.classList).value !== undefined) {
+        Props.define(TameDOMSettableTokenList.prototype, TokenListConf, {
+          value: {
+            get: TokenListConf.amplifying(function(privates) {
+              return privates.feral.value.replace(/[^ \t\n\r\f]+/g,
+                    function(s) {
+                var ttoken = privates.getT(s);
+                if (ttoken === null) { return ''; }
+                return ttoken;
+              });
+            }),
+            set: TokenListConf.amplifying(function(privates, val) {
+ privates.feral.value = val.replace(/[^ \t\n\r\f]+/g, function(s) {
+                var ftoken = privates.setT(s);
+                if (ftoken === null) { return ''; }
+                return ftoken;
+              });
+            })
+          }
+        });
+      }
+      finishArrayLikeClass(TameDOMSettableTokenList);
+
+      function tameTokenList(feral, getTransform, setTransform) {
+        makeDOMAccessible(feral);
+        if ('value' in feral) {
+          return new TameDOMSettableTokenList(feral, getTransform,
+              setTransform);
+        } else {
+          return new TameDOMTokenList(feral, getTransform, setTransform);
+        }
+      }

       function makeEventHandlerWrapper(thisNode, listener) {
         domitaModules.ensureValidCallback(listener);
@@ -4261,6 +4357,22 @@
         addEventListener: Props.overridable(true, tameAddEventListener),
removeEventListener: Props.overridable(true, tameRemoveEventListener)
       });
+      if ('classList' in elementForFeatureTests) {
+        Props.define(TameElement.prototype, TameNodeConf, {
+          classList: PT.TameMemoIf(true, 'classList',
+                         nodeAmp(function(privates, feralList) {
+            var element = this;
+            return new TameDOMSettableTokenList(feralList,
+                function classListGetTransform(token) {
+ return virtualizeAttributeValue(html4.atype.CLASSES, token);
+                },
+                function classListSetTransform(token) {
+                  return rewriteAttribute(element.tagName, 'class',
+                      html4.atype.CLASSES, token);
+                });
+          }))
+        });
+      }
       cajaVM.def(TameElement);  // and its prototype

       /**
=======================================
--- /trunk/tests/com/google/caja/plugin/es53-test-domado-dom-guest.html Fri May 24 09:23:04 2013 +++ /trunk/tests/com/google/caja/plugin/es53-test-domado-dom-guest.html Tue May 28 14:58:38 2013
@@ -1727,7 +1727,21 @@
       assertEquals(note + ' className', className, el.className);
assertEquals(note + ' getAttribute', className, el.getAttribute('class'));
       assertTrue(note + ' hasAttribute', el.hasAttribute('class'));
+
+      var classList = el.classList;
+      assertTrue('classList', !!classList);
+      var ourList = className.trim().split(/\s+/);
+      if (ourList.length === 1 && ourList[0] === "") { ourList = []; }
+ for (var i = 0; i < Math.max(classList.length, ourList.length); i++) { + assertEquals(note + ' classList[' + i + ']', ourList[i], classList[i]);
+        assertTrue(note + ' classList.contains ' + i,
+            classList.contains(ourList[i]));
+      }
+      assertTrue(note + ' !classList.contains',
+          !classList.contains('nonexistent'));
     }
+
+    // test modifying classes

     var el = document.getElementById('testClassNames');
     assertEquals('before class added',
@@ -1739,15 +1753,46 @@
         'inline', window.getComputedStyle(el, null).display);
     assertClassIs('testcontainer inline', el);

+    // syntax
+
     el.className = '$-.:;()[]=';
     assertClassIs('$-.:;()[]=', el);

     el.className = '!@{} ok__1';
     assertClassIs('!@{} ok__1', el);

+    // simple case, and classList interface
+
+    var classList = el.classList;
+    assertTrue('one classList', classList === el.classList);
+
     el.setAttribute('class', 'foo');
     assertClassIs('foo', el);

+    classList.add('bar');
+    assertClassIs('foo bar', el);
+
+    classList.remove('foo');
+    assertClassIs('bar', el);
+
+    var r = classList.toggle('foo');
+    assertEquals('toggle add', true, r);
+    assertClassIs('bar foo', el);
+
+    assertEquals('tokenList stringify', 'bar foo', classList.toString());
+
+    r = classList.toggle('bar');
+    assertEquals('toggle remove', false, r);
+    assertClassIs('foo', el);
+
+    if ('value' in classList) {  // in HTML5 spec, not impl in Chrome
+      assertEquals('classList.value', 'foo', classList.value);
+      classList.value = 'bar';
+      assertEquals('className after value set', 'bar', el.className);
+    }
+
+    // rejections
+
     el.className = 'simple';
     assertClassIs('simple', el);

@@ -1772,6 +1817,13 @@
     el.setAttribute('class', '');
     assertClassIs('', el, 'setAttribute class ""');

+    el.className = 'initial';
+    classList.add('bad__');
+    assertClassIs('initial', el, 'classList.add bad')
+    classList.toggle('bad__');
+    assertClassIs('initial', el, 'classList.add bad')
+    assertEquals(false, classList.contains('bad__'));
+
     pass('testClassNames');
   });
 </script>
=======================================
--- /trunk/tests/com/google/caja/plugin/es53-test-scan-guest.js Thu May 23 14:04:29 2013 +++ /trunk/tests/com/google/caja/plugin/es53-test-scan-guest.js Tue May 28 14:58:38 2013
@@ -1428,10 +1428,15 @@
         undefined, null, 'testUniverse', 'not an/id')));
argsByProp('getElementsByTagName', freshResult(genMethod(genElementName))); argsByProp('getElementsByClassName', freshResult(genMethod(genClassName)));
+    argsByProp('addEventListener', argsByProp('removeEventListener',
+ genMethod(genEventName, G.value(function stubL() {}), genBoolean)));
+
+    // NodeList and friends (currently have no exported type)
     argsByProp('item', genMethod(genSmallInteger));
     argsByProp('namedItem', genMethod(genString));
-    argsByProp('addEventListener', argsByProp('removeEventListener',
- genMethod(genEventName, G.value(function stubL() {}), genBoolean)));
+    argsByProp('add', genMethod(genString));
+    argsByProp('remove', genMethod(genString));
+    argsByProp('toggle', genMethod(genString));

     // 2D context (and friends) methods
     var canvas2DProto = CanvasRenderingContext2D.prototype;

--

--- You received this message because you are subscribed to the Google Groups "Google Caja Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
For more options, visit https://groups.google.com/groups/opt_out.


Reply via email to