Is there a way to append html to web component? - javascript

I've a custom js web component:
class MainDiv extends HTMLElement {
constructor() { super(); }
connectedCallback() {
this.innerHTML = '<div id="mydiv"><input type="text" oninput="onInput(event)"/></div>'
}
}
and in main.js I've the function onInput*()
function onInput(event) {
const mainDiv = document.getElementById("mydiv");
const newLabel = document.createElement('span');
newLabel.innerText = `${event.target.value.length}/255`; // output: 5/255...n/255
mainDiv.appendChild(newLabel);
}
If I add a log in the onInput function it prints and does not return any error, but is not updating the webcomponent. Why?

There must be something else at play, your code works:
<script>
function onInput(event) {
const mainDiv = document.getElementById("mydiv");
const newLabel = document.createElement('div');
newLabel.innerText = `${event.target.value.length}/255`; // output: 5/255...n/255
mainDiv.appendChild(newLabel);
}
customElements.define("my-element", class extends HTMLElement {
connectedCallback() {
this.innerHTML = '<div id="mydiv"><input type="text" oninput="onInput(event)"/></div>'
}
});
</script>
<my-element></my-element>
Note
constructor() {
super();
}
Is not required, it says run the constructor from my parent, which is also done when you leave out the constructor in your Component.
Only when you do more in the constructor is it required.

Related

Javascript custom element's oninput function issue

Say, I need to call a function in a custom element, such that when a slider moves in that element the text gets updated(only for that element). The main.js file,
class OninputClassDemo extends HTMLElement {
constructor(){
super();
const shadow = this.attachShadow({mode:'open'});
this.parent = document.createElement('div');
this.slider = document.createElement('input');
this.slider.setAttribute('type','range');
this.slider.setAttribute('min','0');
this.slider.setAttribute('max','99');
this.slider.setAttribute('value','0')
this.text = document.createElement('input');
this.text.setAttribute('type','text');
this.text.setAttribute('value','');
this.parent.appendChild(this.slider);
this.parent.appendChild(this.text);
shadow.appendChild(this.parent);
this.slider.setAttribute('oninput','OninputClassDemo.changeValue()');
}
changeValue = function(){
this.text.setAttribute('value',this.slider.getAttribute('value'));
}
}
window.customElements.define('demo-element',OninputClassDemo);
On the template side,
<!DOCTYPE html>
<html lang="en">
<head>
<script src="main.js"></script>
</head>
<body>
<demo-element></demo-element>
</body>
</html>
The error I am getting is,
OninputClassDemo.changeValue is not a function
at HTMLInputElement.oninput
I do not know how to reference the method for that particular object in this.slider.setAttribute('oninput','WhatShouldIPutHere'),so that the text box for only that object gets changed.
You should be binding event listeners with addEventListener. You should be binding to the method with this, not the class name. Set the value property, do not set an attribute.
class OninputClassDemo extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({
mode: 'open'
});
this.parent = document.createElement('div');
this.slider = document.createElement('input');
this.slider.setAttribute('type', 'range');
this.slider.setAttribute('min', '0');
this.slider.setAttribute('max', '99');
this.slider.setAttribute('value', '0')
this.text = document.createElement('input');
this.text.setAttribute('type', 'text');
this.text.setAttribute('value', '');
this.parent.appendChild(this.slider);
this.parent.appendChild(this.text);
shadow.appendChild(this.parent);
this.slider.addEventListener('input', (event) => this.changeValue());
}
changeValue() {
this.text.value = this.slider.value;
}
}
window.customElements.define('demo-element', OninputClassDemo);
<demo-element></demo-element>
You should write
changeValue() {
this.text.setAttribute('value',this.slider.getAttribute('value'));
}
instead of
changeValue = function(){
this.text.setAttribute('value',this.slider.getAttribute('value'));
}

Uncaught TypeError: xyz is not a function (when calling it from inside string)

Trying to bind the validate() function on blur of input as declared inside render function of class Textbox extends HTMLElement (Part-A), but getting the below error
detail.html:1 Uncaught TypeError: this.validate is not a function
(but the validate() function is accessible when placing outside the call of the function outside string)
Part - A
class HTMLElement {
constructor( ) {
this.value = false;
}
validate() {
console.log('A')
}
}
class Textbox extends HTMLElement {
constructor( ) {
super();
}
render( elem_id, default_value, display_name, placeholder, permission ) {
/*** this.validate() will work here but not inside onblur function ***/
var string = "";
string += "<input type='text' class='form-control' id='"+elem_id+"' value='"+default_value+"' placeholder='" + placeholder + "' onblur='this.validate()' >"
return string;
}
}
Part B
const ELEMENT_ARR = {
"text":Textbox
}
class Elements {
constructor( className, opts ) {
return new ELEMENT_ARR[className](opts);
}
}
function renderForm(){
var ds = {
"fields":[
{"id":"f_name","type":"text","is_mandatory":1,"display_name":"First Name","default":"", "permission":"hidden/editable/readonly","placeholder":"Sample Placeholder 1" },
{"id":"f_name","type":"text","is_mandatory":0,"display_name":"Hellp Name","default":"","permission":"hidden/editable/readonly", "placeholder":"Sample Placeholder 2" }
]
}, ks = JSON.parse(JSON.stringify(ds)), form_str="", elementor;
for (let u = 0 ; u < ks['fields'].length; u++) {
let type = ks['fields'][u].type,
elem_id = ks['fields'][u].id,
is_mandatory = ks['fields'][u].is_mandatory,
default_value = ks['fields'][u].default,
display_name = ks['fields'][u].display_name,
placeholder = ks['fields'][u].placeholder,
permission = ks['fields'][u].permission;
elementor = new Elements( type )
form_str += elementor.render( elem_id, default_value, display_name, placeholder, permission )
}
return form_str;
}
Part - C
window.onload = function(){
document.getElementById('formGenx').innerHTML = renderForm();
};
What possibly wrong I am doing?
The problem is that you're generating a string then appending it to the DOM and only at that point it will be parsed into a DOM element and added to the page. This happens outside the context of your render method and has no link to either it or the object that created it. Moreover, the event handler is just set to the string "this.validate()" which will be executed in the global context.
To retain access to your validate method you have to attach it as a handler at the time render executes. The best way to go about this, in my opinion, is to just avoid the step where you generate a string and programmatically create the element:
document.createElement will create the node
.setAttribute() can add attributes
.classList and .add() any classes you need.
instead of setting the onblur attribute, .addEventListener() to add a proper handler as the direct attribute but note that sometimes you might prefer the attribute.
This will generate the elements, you can then accumulate them in a DocumentFragment before adding them all at once to the DOM which will save you multiple insertions and potential performance hit because of triggering reflow.
Finally, the elements can be added via the normal DOM API but usually .appendChild is enough.
So, we get the following:
class HTMLElement {
constructor( ) {
this.value = false;
}
validate() {
console.log('A')
}
}
class Textbox extends HTMLElement {
constructor( ) {
super();
}
render( elem_id, default_value, display_name, placeholder, permission ) {
/*** programmatically create the input element and set all relevant attributes ***/
var element = document.createElement("input");
element.setAttribute("type", "text");
element.classList.add("form-control");
element.setAttribute("id", elem_id);
element.setAttribute("value", default_value);
element.setAttribute("placeholder", placeholder);
element.addEventListener("blur", () => this.validate())
return element;
}
}
const ELEMENT_ARR = {
"text":Textbox
}
class Elements {
constructor( className, opts ) {
return new ELEMENT_ARR[className](opts);
}
}
function renderForm(){
var ds = {
"fields":[
{"id":"f_name","type":"text","is_mandatory":1,"display_name":"First Name","default":"", "permission":"hidden/editable/readonly","placeholder":"Sample Placeholder 1" },
{"id":"f_name","type":"text","is_mandatory":0,"display_name":"Hellp Name","default":"","permission":"hidden/editable/readonly", "placeholder":"Sample Placeholder 2" }
]
},
ks = JSON.parse(JSON.stringify(ds)),
//just a "container" for the elements.
//can be cahged to document.createElement("form") if needed
form = document.createDocumentFragment(),
elementor;
for (let u = 0 ; u < ks['fields'].length; u++) {
let type = ks['fields'][u].type,
elem_id = ks['fields'][u].id,
is_mandatory = ks['fields'][u].is_mandatory,
default_value = ks['fields'][u].default,
display_name = ks['fields'][u].display_name,
placeholder = ks['fields'][u].placeholder,
permission = ks['fields'][u].permission;
elementor = new Elements( type );
let newElement = elementor.render( elem_id, default_value, display_name, placeholder, permission );
form.appendChild(newElement);//append as DOM elements
}
return form;
}
window.onload = function(){
document.getElementById('formGenx')
.appendChild(renderForm()); //append as DOM element
};
.form-control {
margin: 5px;
border-style: dashed;
border-color: black
}
<div id="formGenx"></div>
The normal JavaScript API tends to be verbose when programmatically creating an element. Libraries like jQuery can actually make it a bit easier at the cost of another dependency, of course. While jQuery is not strictly needed, I'll just demonstrate how programmatic the element creation would work with it for reference:
var element = $("<input>")
.attr("type", "text")
.addClass("form-control")
.id(elem_id)
.val(default_value)
.attr("placeholder", placeholder)
.on("blur", () => this.validate())
return element;

How to add event listener with function callback in html inside a class?

Imagine having a class that generates content on the page. Part of the content should have event listener attached in html such as onclick=function().
How can I make sure to call the function from within the class that constructed the html?
class Container {
constructor(hook) {
this.hook = "#" + hook;
this.addDiv = this.addDiv.bind(this);
this.fireMe = this.fireMe.bind(this);
this.init = this.init.bind(this);
this.init();
}
addDiv() {
const div = `<div onclick="fireMe()">FIRE ME</div>`;
document.querySelector(this.hook).innerHTML = div;
}
fireMe() {
console.log("hello!");
}
init() {
this.addDiv();
}
}
let div = new Container("app");
now getting error that fireMe is undefined (which is right because it is not available in global scope).
I know I can add event listener by rendering the div first and than adding the event listener, but is there a way of adding event listener from within <div> tag to actually reach Container.fireMe() method?
You have to create the element -> something like this
class Container {
constructor (hook) {
this.hook = '#' + hook;
this.addDiv = this.addDiv.bind(this);
this.fireMe = this.fireMe.bind(this);
this.init = this.init.bind(this);
this.init();
}
addDiv () {
const div = document.createElement('div');
div.textContent = 'FIRE ME';
div.addEventListener('click', this.fireMe );
document.querySelector(this.hook).innerHTML = div;
}
fireMe () {
console.log('hello!');
}
init () {
this.addDiv();
}
}
const div = new Container('app');
Never use inline event handlers as there are many reasons to avoid this 20+ year old technique that just will not die.
Instead, use modern, standards-based code with .addEventListener(). If you do this along with making the new HTML using the DOM API, you'll be able to more easily accomplish your goal:
addDiv() {
const div = document.createElement("div");
div.textConent = "FIRE ME";
div.addEventListener("click", this.fireMe);
document.querySelector(this.hook).innerHTML = div;
}
You should create elements use document.createElement() rather than using string
class Container {
constructor(hook) {
this.hook = "#" + hook;
this.addDiv = this.addDiv.bind(this);
this.fireMe = this.fireMe.bind(this);
this.init = this.init.bind(this);
this.init();
}
addDiv(){
const div = document.createElement('div');
div.innerHTML = "Fire Me";
div.addEventListener("click",this.fireMe);
document.querySelector(this.hook).appendChild(div);
}
fireMe() {
console.log("hello!");
}
init() {
this.addDiv();
}
}
let div = new Container("app");

moving element from light DOM to ShadowDOM without disconnectedCallback

I wonder if I am on the right track here
Objective: Need to ensure that all elements end up in the shadowDOM
So the manually created HTML file
<cardts-pile>
<cardts-card>A</cardts-card>
<cardts-card>B</cardts-card>
</cardts-pile>
creates the cards in lightDOM of <cardts-pile>
if I then move them to shadowDOM (ofcourse):
► <cardts-card> is removed from the DOM (triggering disconnectedCallback())
► <cardts-card> is added again (triggering connectedCallback())
[see console.log on Run Code Snipper below]
I have more fancy code in card.connectedCallback()
on 're-connect' it is basically triggering the exact same code again.
Questions
Is it possible to move nodes without DOM changes?
Is there OOTB code to check if an existing <cardts-card> is only being moved,
so connectedCallback knows it doesn't need to run code again.
Should I be doing something different,
making those lightDOM elements end up in shadowDOM immediatly?
customElements.define('cardts-pile', class extends HTMLElement {
constructor(){
super();
this.attachShadow({mode: 'open'}).innerHTML='<slot></slot>';
}
connectedCallback() {
console.log('connect pile');
}
});
customElements.define('cardts-card', class extends HTMLElement {
constructor(){
super();
this.attachShadow({mode: 'open'}).innerHTML='<slot></slot>';
}
connectedCallback() {
console.log('connect card',this.innerText);
if (!this.getRootNode().host) // not in shadowDOM
this.parentNode.shadowRoot.insertBefore(this,null);//or appendChild
}
disconnectedCallback() {
console.log('disconnect card',this.innerText);
}
});
<cardts-pile>
<cardts-card>A</cardts-card>
<cardts-card>B</cardts-card>
</cardts-pile>
Is it possible to move nodes without DOM changes?
No (as far as I know about Shadow DOM).
Is there OOTB code to check if an existing is only being moved?
I would use a boolean flag:
connectedCallback() {
if ( !this.connected )
console.log( 'creation' )
else {
console.log( 'move' )
this.connected = true
}
(or in disconnectedCallack)
customElements.define('cardts-pile', class extends HTMLElement {
constructor(){
super();
this.attachShadow({mode: 'open'}).innerHTML='<slot></slot>';
this.shadowRoot.addEventListener( 'slotchange', ev => {
let node = this.querySelector( 'cardts-card' )
node && this.shadowRoot.append( node )
})
}
connectedCallback() {
console.log('connect pile');
}
});
customElements.define('cardts-card', class extends HTMLElement {
constructor(){
super();
this.attachShadow({mode: 'open'}).innerHTML='<slot></slot>';
}
connectedCallback() {
if ( !this.connected )
console.log( this.innerText + ' created' )
else
console.log( this.innerText + ' moved' )
this.connected = true
}
disconnectedCallback() {
if ( !this.moved )
console.log( 'moving ' + this.innerText );
else
console.log( 'really disconnected' )
this.moved = true
}
});
<cardts-pile>
<cardts-card>A</cardts-card>
<cardts-card>B</cardts-card>
</cardts-pile>
Should I be doing something different?
You could instead define or upgrade <cardts-card> only after the unknown elements are moved, if possible though I don't think it's a good practice unless you can control the whole execution timing, for example with whenDefined() or with ordered HTML and Javascript code:
customElements.define('cardts-pile', Pile)
customElements.whenDefined('cardts-pile').then(() =>
customElements.define('cardts-card', Card)
)
In the example below, you define class Pile before or after class Card (depending on how they are related).
class Card extends HTMLElement {
constructor(){
super()
this.attachShadow({mode: 'open'}).innerHTML='<slot></slot>'
}
connectedCallback() {
console.log(this.innerText + ' connected')
}
disconnectedCallback() {
console.log(this.innerText + ' disconnected')
}
}
class Pile extends HTMLElement {
constructor() {
super()
this.attachShadow({mode: 'open'})
}
connectedCallback() {
console.log('connect pile')
this.shadowRoot.append(...this.querySelectorAll('cardts-card'))
}
}
window.onload = () => customElements.define('cardts-pile', Pile)
customElements.whenDefined('cardts-pile').then(() =>
customElements.define('cardts-card', Card)
)
<cardts-pile>
<cardts-card>A</cardts-card>
<cardts-card>B</cardts-card>
</cardts-pile>

JavaScript ES6 import/export and class extends

I builded a custom element which is a hamburger button and now I'm working on a side nav. In this side nav I want to use my hamburger button so I try to export my HCHamburger class which correspond to my button and import it in my SideNav class. The idea is to animate my button position when the side nav is opened. I try to extends my SideNav class with HCHamburger but I got the following error : Uncaught TypeError: Failed to construct 'HTMLElement': Please use the 'new' operator, this DOM object constructor cannot be called as a function.
My HCHambuger class looks like this :
'use strict';
export default class HCHamburger extends HTMLElement {
get menuButton() {
if (!this._menuButton) {
this._menuButton = this.querySelector('.hamburger-menu');
}
return this._menuButton;
}
get bar() {
if (!this._bar) {
this._bar = this.querySelector('.bar');
}
return this._bar;
}
attachedCallback() {
this.menuButton.addEventListener('click', _ => {
const sideNavContainerEl = document.querySelector('.js-side-nav-container');
this.bar.classList.toggle("animate");
if (sideNavContainerEl.getAttribute('nav-opened') == 'false') {
this.openMenuButton(sideNavContainerEl);
} else {
this.closeMenuButton(sideNavContainerEl);
}
});
}
sayHello() {
console.log('TOTO');
}
openMenuButton(sideNavContainerEl) {
this.style.transform = `translateX(${sideNavContainerEl.offsetWidth}px)`;
}
closeMenuButton(sideNavContainerEl) {
this.style.transform = `translateX(0px)`;
}
}
document.registerElement('hc-hamburger', HCHamburger);
And my SideNav class like this :
'use strict';
import Detabinator from './detabinator.js';
import HCHamburger from './hamburger.js';
class SideNav extends HCHamburger {
constructor () {
super();
this.toggleMenuEl = document.querySelector('.js-menu');
this.showButtonEl = document.querySelector('.js-menu-show');
this.hideButtonEl = document.querySelector('.js-menu-hide');
this.sideNavEl = document.querySelector('.js-side-nav');
this.sideNavContainerEl = document.querySelector('.js-side-nav-container');
// Control whether the container's children can be focused
// Set initial state to inert since the drawer is offscreen
this.detabinator = new Detabinator(this.sideNavContainerEl);
this.detabinator.inert = true;
this.toggleSideNav = this.toggleSideNav.bind(this);
this.showSideNav = this.showSideNav.bind(this);
this.hideSideNav = this.hideSideNav.bind(this);
this.blockClicks = this.blockClicks.bind(this);
this.onTouchStart = this.onTouchStart.bind(this);
this.onTouchMove = this.onTouchMove.bind(this);
this.onTouchEnd = this.onTouchEnd.bind(this);
this.onTransitionEnd = this.onTransitionEnd.bind(this);
this.update = this.update.bind(this);
this.startX = 0;
this.currentX = 0;
this.touchingSideNav = false;
this.supportsPassive = undefined;
this.addEventListeners();
}
// apply passive event listening if it's supported
applyPassive () {
if (this.supportsPassive !== undefined) {
return this.supportsPassive ? {passive: true} : false;
}
// feature detect
let isSupported = false;
try {
document.addEventListener('test', null, {get passive () {
isSupported = true;
}});
} catch (e) { }
this.supportsPassive = isSupported;
return this.applyPassive();
}
addEventListeners () {
this.toggleMenuEl.addEventListener('click', this.toggleSideNav);
this.sideNavEl.addEventListener('click', this.hideSideNav);
this.sideNavContainerEl.addEventListener('click', this.blockClicks);
this.sideNavEl.addEventListener('touchstart', this.onTouchStart, this.applyPassive());
this.sideNavEl.addEventListener('touchmove', this.onTouchMove, this.applyPassive());
this.sideNavEl.addEventListener('touchend', this.onTouchEnd);
}
onTouchStart (evt) {
if (!this.sideNavEl.classList.contains('side-nav--visible'))
return;
this.startX = evt.touches[0].pageX;
this.currentX = this.startX;
this.touchingSideNav = true;
requestAnimationFrame(this.update);
}
onTouchMove (evt) {
if (!this.touchingSideNav)
return;
this.currentX = evt.touches[0].pageX;
const translateX = Math.min(0, this.currentX - this.startX);
if (translateX < 0) {
evt.preventDefault();
}
}
onTouchEnd (evt) {
if (!this.touchingSideNav)
return;
this.touchingSideNav = false;
const translateX = Math.min(0, this.currentX - this.startX);
this.sideNavContainerEl.style.transform = '';
if (translateX < 0) {
this.hideSideNav();
}
}
update () {
if (!this.touchingSideNav)
return;
requestAnimationFrame(this.update);
const translateX = Math.min(0, this.currentX - this.startX);
this.sideNavContainerEl.style.transform = `translateX(${translateX}px)`;
}
blockClicks (evt) {
evt.stopPropagation();
}
onTransitionEnd (evt) {
this.sideNavEl.classList.remove('side-nav--animatable');
this.sideNavEl.removeEventListener('transitionend', this.onTransitionEnd);
}
showSideNav () {
this.sideNavEl.classList.add('side-nav--animatable');
this.sideNavEl.classList.add('side-nav--visible');
this.detabinator.inert = false;
this.sideNavEl.addEventListener('transitionend', this.onTransitionEnd);
}
hideSideNav () {
this.sideNavEl.classList.add('side-nav--animatable');
this.sideNavEl.classList.remove('side-nav--visible');
this.detabinator.inert = true;
this.sideNavEl.addEventListener('transitionend', this.onTransitionEnd);
}
toggleSideNav () {
if (this.sideNavContainerEl.getAttribute('nav-opened') == 'true') {
this.hideSideNav();
this.sideNavContainerEl.setAttribute('nav-opened', 'false');
} else {
this.showSideNav();
this.sideNavContainerEl.setAttribute('nav-opened', 'true');
}
}
}
new SideNav();
I'm using webpack to build my JS code and maybe it's the reason of my issue... I tried different method to import/export but nothing worked.
I thought to just export the method that I needed but it didn't work neither.
Thank's
Fundamentally, there's just a mis-match between the DOM's API and JavaScript's inheritance (at present). You can't do the extends HTMLElement thing on current browsers. You may be able to at some point when the custom elements specification settles down and is widely-implemented in its final form, but not right now.
If you transpile, you'll get the error you have in your question, because the transpiled code tries to do something along these lines:
function MyElement() {
HTMLElement.call(this);
}
var e = new MyElement();
If you don't transpile (requiring ES2015+ support on the browser), you'll likely get a different error:
TypeError: Illegal constructor
class MyElement extends HTMLElement {
}
let e = new MyElement();
You have a couple of options that don't involve inheriting from HTMLElement: Wrapping and prototype augmentation
Wrapping
You have a function that wraps elements. It might create wrappers for individual elements, or sets of elements like jQuery; here's a very simple set example:
// Constructor function creating the wrapper; this one is set-based
// like jQuery, but unlike jQuery requires that you call it via `new`
// (just to keep the example simple).
function Nifty(selectorOrElementOrArray) {
if (!selectorOrElementOrArray) {
this.elements = [];
} else {
if (typeof selectorOrElementOrArray === "string") {
this.elements = Array.prototype.slice.call(
document.querySelectorAll(selectorOrElementOrArray)
);
} else if (Array.isArray(selectorOrElementOrArray)) {
this.elements = selectorOrElementOrArray.slice();
} else {
this.elements = [selectorOrElementOrArray];
}
}
}
Nifty.prototype.addClass = function addClass(cls) {
this.elements.forEach(function(element) {
element.classList.add(cls);
});
};
// Usage
new Nifty(".foo").addClass("test");
new Nifty(".bar").addClass("test2");
.test {
color: green;
}
.test2 {
background-color: yellow;
}
<div id="test">
<span class="bar">bar1</span>
<span class="foo">foo1</span>
<span class="bar">bar2</span>
<span class="foo">foo2</span>
<span class="bar">bar3</span>
</div>
Prototype Augmentation
You can augment HTMLElement.prototype. There are vocal contingents both for and against doing so, the "against" primarily point to the possibility of conflicts if multiple scripts try to add the same properties to it (or if the W3C or WHAT-WG add new properties/methods to it), which is obviously a very real possibility. But if you keep your property names fairly unlikely to be used by others, you can minimize that possibility:
// Add xyzSelect and xyzAddClass to the HTMLElement prototype
Object.defineProperties(HTMLElement.prototype, {
"xyzSelect": {
value: function xyzSelect(selector) {
return Array.prototype.slice.call(this.querySelectorAll(selector));
}
},
"xyzAddClass": {
value: function xyzAddClass(cls) {
return this.classList.add(cls);
}
}
});
// Usage
var test = document.getElementById("test");
test.xyzSelect(".foo").forEach(function(e) { e.xyzAddClass("test"); });
test.xyzSelect(".bar").forEach(function(e) { e.xyzAddClass("test2"); });
.test {
color: green;
}
.test2 {
background-color: yellow;
}
<div id="test">
<span class="bar">bar1</span>
<span class="foo">foo1</span>
<span class="bar">bar2</span>
<span class="foo">foo2</span>
<span class="bar">bar3</span>
</div>
Prototype augmentation works in modern browsers, and also IE8. It didn't work in IE7 and earlier.

Categories