implement DOMTokenList stringification
[microdatajs:microdatajs.git] / microdata.js
1 if (!document.getItems) {
2
3 // trick IE into handling these elements properly
4 document.createElement('audio');
5 document.createElement('video');
6 document.createElement('time');
7
8 // emulate Object.defineProperty
9 if (!Object.defineProperty && document.__defineGetter__) {
10     Object.defineProperty = function(obj, prop, desc) {
11         if (desc.get)
12             obj.__defineGetter__(prop, desc.get);
13         if (desc.set)
14             obj.__defineSetter__(prop, desc.set);
15     };
16 }
17
18 // emulate Element.textContent
19 if (typeof document.documentElement.textContent == 'undefined') {
20     Object.defineProperty(Element.prototype, 'textContent',
21         {
22             get: function() {
23                 return this.innerText;
24             },
25             set: function(val) {
26                 this.innerText = val;
27             }
28         });
29 }
30
31 // utility functions
32
33 function splitTokens(s) {
34     if (s && /\S/.test(s))
35         return s.replace(/^\s+|\s+$/g,'').split(/\s+/);
36     return [];
37 }
38
39 function inList(item, list) {
40     for (var i = 0; i < list.length; i++) {
41         if (item == list[i])
42             return true;
43     }
44     return false;
45 }
46
47 // http://ejohn.org/blog/comparing-document-position/
48 function contains(a, b){
49     return a.contains ?
50         a != b && a.contains(b) :
51         !!(a.compareDocumentPosition(b) & 16);
52 }
53
54 function fakeCollection(rootElem, incFilter, recFilter) {
55     var elems = [];
56     elems.item = function(idx){return this[idx];};
57
58     function update(_incFilter, _recFilter) {
59         elems.length = 0;
60         function pushElements(elem) {
61             if (_incFilter(elem))
62                 elems.push(elem);
63             if (!_recFilter || _recFilter(elem)) {
64                 for (var child = elem.firstChild; child; child = child.nextSibling) {
65                     if (child.nodeType == 1) {
66                         pushElements(child);
67                     }
68                 }
69             }
70         }
71         pushElements(rootElem);
72     }
73
74     function updateSimple() {
75         update(incFilter, recFilter);
76     }
77
78     function afterUpdate() {
79         if (typeof elems.__onchange__ == 'function') {
80             elems.__onchange__();
81         }
82     }
83
84     function updateHandler() {
85         updateSimple();
86         afterUpdate();
87     }
88
89     // keep collection up to date if possible
90     if (rootElem.addEventListener) {
91         rootElem.addEventListener('DOMAttrModified', updateHandler, false);
92         rootElem.addEventListener('DOMNodeInserted', updateHandler, false);
93         rootElem.addEventListener('DOMNodeRemoved',
94             function(ev) {
95                 update(function(e){return e != ev.target && incFilter(e);},
96                        function(e){return e != ev.target;});
97                 afterUpdate();
98             }, false);
99     }
100
101     updateSimple();
102
103     return elems;
104 }
105
106 function map(list, func) {
107     var ret = [];
108     for (var i = 0; i < list.length; i++) {
109         ret.push(func(list[i]));
110     }
111     return ret;
112 }
113
114 function filter(list, pred) {
115     var ret = [];
116     for (var i = 0; i < list.length; i++) {
117         if (pred(list[i]))
118             ret.push(list[i]);
119     }
120     return ret;
121 }
122
123 // Element attribute<->property reflection
124
125 function reflectBoolean(attr, prop) {
126     Object.defineProperty(Element.prototype, prop,
127         { get: function () { return this.hasAttribute(attr); },
128           set: function (val) { if (val) this.setAttribute(attr, attr);
129                                 else this.removeAttribute(attr); } });
130 }
131
132 function reflectString(attr, prop) {
133     Object.defineProperty(Element.prototype, prop,
134         { get: function () { return this.getAttribute(attr) || ""; },
135           set: function (val) { this.setAttribute(attr, val); }});
136 }
137
138 function reflectSettableTokenList(attr, prop) {
139     function getProp() {
140         var elem = this;
141         var list = [];
142         list.item = function(index) { return this[index]; };
143
144         function update() {
145             list.length = 0;
146             if (elem.hasAttribute(attr))
147                 list.push.apply(list, splitTokens(elem.getAttribute(attr)));
148         }
149         update();
150
151         function getValue() { return elem.hasAttribute(attr) ? elem.getAttribute(attr) : ''; };
152         function setValue(val) { elem.setAttribute(attr, val); };
153         Object.defineProperty(list, 'value', { get: getValue, set: setValue });
154
155         list.toString = getValue;
156
157         function validate(token) {
158             if (!token)
159                 throw new Error('SYNTAX_ERR');
160             if (/\s/.test(token))
161                 throw new Error('INVALID_CHARACTER_ERR');
162         }
163
164         list.contains = function(token) {
165             validate(token);
166             return inList(token, this);
167         };
168         if (elem.addEventListener)
169             elem.addEventListener('DOMAttrModified', update, false);
170         list.add = function(token) {
171             validate(token);
172             if (!inList(token, this)) {
173                 var attrValue = elem.hasAttribute(attr) ? elem.getAttribute(attr) : '';
174                 if (attrValue.length && attrValue[attrValue.length-1] != ' ')
175                     attrValue += ' ';
176                 attrValue += token;
177                 elem.setAttribute(attr, attrValue);
178             }
179         };
180         list.remove = function(token) {
181             validate(token);
182             var input = elem.hasAttribute(attr) ? elem.getAttribute(attr) : '';
183             var output = '';
184             while (input) {
185                 var m = /^(\s+)?(\S+)(\s+)?/.exec(input);
186                 if (m) {
187                     input = input.substr(m[0].length);
188                     if (m[2] == token) {
189                         output = output.replace(/\s+$/, '');
190                         if (input && output)
191                             output += ' ';
192                     } else {
193                         output += m[0];
194                     }
195                 } else {
196                     output += input;
197                     break;
198                 }
199             }
200             elem.setAttribute(attr, output);
201         };
202         list.toggle = function(token) {
203             validate(token);
204             if (this.contains(token)) {
205                 this.remove(token);
206                 return false;
207             } else {
208                 this.add(token);
209                 return true;
210             }
211         };
212         return list;
213     }
214     function setProp(val) {
215         this.setAttribute(attr, val);
216     }
217     Object.defineProperty(Element.prototype, prop,
218         { get: getProp, set: setProp });
219 }
220
221 reflectBoolean('itemscope', 'itemScope');
222 // FIXME: should be URL?
223 reflectString('itemtype', 'itemType');
224 // FIXME: should be URL?
225 reflectString('itemid', 'itemId');
226 reflectSettableTokenList('itemprop', 'itemProp');
227 // FIXME: should also be DOMSettableTokenList?
228 reflectString('itemref', 'itemRef');
229 // FIXME: only if not browser-implemented
230 reflectString('datetime', 'dateTime');
231
232 function getItemValueProperty(e) {
233     var tag = e.tagName.toUpperCase();
234     if (tag == 'META')
235         return 'content';
236     if (tag == 'AUDIO' || tag == 'EMBED' ||
237         tag == 'IFRAME' || tag == 'IMG' ||
238         tag == 'SOURCE' || tag == 'VIDEO')
239         return 'src';
240     if (tag == 'A' || tag == 'AREA' || tag == 'LINK')
241         return 'href';
242     if (tag == 'OBJECT')
243         return 'data';
244     if (tag == 'TIME' && e.hasAttribute('datetime'))
245         return 'dateTime';
246     return 'textContent';
247 }
248
249 Object.defineProperty(Element.prototype, 'itemValue',
250 {
251     get: function() {
252         if (!this.hasAttribute('itemprop'))
253             return null;
254         if (this.hasAttribute('itemscope'))
255             return this;
256         return this[getItemValueProperty(this)];
257     },
258     set: function(val) {
259         if (!this.hasAttribute('itemprop') || this.hasAttribute('itemscope'))
260             throw new Error('INVALID_ACCESS_ERR');
261         this[getItemValueProperty(this)] = val;
262     }
263 });
264
265 // Element.properties
266
267 Object.defineProperty(Element.prototype, 'properties', { get:
268 function() {
269     // http://www.whatwg.org/specs/web-apps/current-work/multipage/microdata.html#the-properties-of-an-item
270     var itemElem = this;
271
272     var props = [];
273     function updateProperties(elemFilter) {
274         var root = itemElem;
275
276         props.length = 0;
277
278         // when the root isn't a item in the document, match nothing
279         // FIXME: the spec doesn't actually say this.
280         if (!root.itemScope || !contains(document.documentElement, root))
281             return;
282
283         var pending = [];
284         function pushChildren(e) {
285             for (var child = e.lastChild; child; child = child.previousSibling) {
286                 if (child.nodeType == 1 && elemFilter(child)) {
287                     pending.push(child);
288                 }
289             }
290         }
291         pushChildren(root);
292
293         function getScopeNode(e) {
294             var scope = e.parentNode;
295             while (scope && !scope.itemScope)
296                 scope = scope.parentNode;
297             return scope;
298         }
299         var refIds = splitTokens(root.itemRef);
300         idloop: for (var i=0; i<refIds.length; i++) {
301             var candidate = document.getElementById(refIds[i]);
302             if (!candidate || !elemFilter(candidate))
303                 continue;
304             var scope = getScopeNode(candidate);
305             for (var j=0; j<pending.length; j++) {
306                 if (candidate == pending[j])
307                     continue idloop;
308                 if (contains(pending[j], candidate) &&
309                     (pending[j] == scope ||
310                      getScopeNode(pending[j]) == scope))
311                     continue idloop;
312             }
313             pending.push(candidate);
314         }
315
316         // from http://www.quirksmode.org/dom/getElementsByTagNames.html
317         pending.sort(function (a,b){return (a.compareDocumentPosition(b)&6)-3;});
318
319         while (pending.length) {
320             var current = pending.pop();
321             if (current.hasAttribute('itemprop'))
322                 props.push(current);
323             if (!current.hasAttribute('itemscope'))
324                 pushChildren(current);
325         }
326     }
327
328     props.names = [];
329     function updateNames() {
330         props.names.length = 0;
331         for (var i = 0; i < props.length; i++) {
332             for (var j=0; j<props[i].itemProp.length; j++) {
333                 if (!inList(props[i].itemProp[j], props.names))
334                     props.names.push(props[i].itemProp[j]);
335             }
336         }
337     }
338
339     function updatePropertyNodeList(pnl, name) {
340         pnl.length = 0;
341         pnl.values.length = 0;
342         for (var i=0; i<props.length; i++) {
343             if (inList(name, props[i].itemProp)) {
344                 pnl.push(props[i]);
345                 pnl.values.push(props[i].itemValue);
346             }
347         }
348     }
349
350     props.item = function(idx){return this[idx];};
351
352     var pnlCache = {};
353     props.namedItem = function (name) {
354         if (!pnlCache[name]) {
355             // fake PropertyNodeList
356             var pnl = [];
357             pnl.item = function(idx){return this[idx];};
358             pnl.values = [];
359             updatePropertyNodeList(pnl, name);
360             pnlCache[name] = pnl;
361         }
362         return pnlCache[name];
363     };
364
365     function updateHandler(elemFilter) {
366         updateProperties(function(e){return typeof elemFilter != 'function' || elemFilter(e);});
367         updateNames();
368         for (name in pnlCache)
369             updatePropertyNodeList(pnlCache[name], name);
370     }
371
372     // keep collection up to date if possible
373     if (document.documentElement.addEventListener) {
374         document.documentElement.addEventListener('DOMAttrModified', updateHandler, false);
375         document.documentElement.addEventListener('DOMNodeInserted', updateHandler, false);
376         document.documentElement.addEventListener('DOMNodeRemoved',
377             function(ev) {
378                 updateHandler(function(e){return (e != ev.target) && !contains(ev.target, e);});
379             }, false);
380     }
381
382     updateHandler();
383     return props;
384 }});
385
386 // document.getItems
387
388 document.getItems = function(typeNames) {
389     var types = splitTokens(typeNames);
390
391     function isTopLevelItem(e) {
392         return e.hasAttribute('itemscope') &&
393             !e.hasAttribute('itemprop') &&
394             (types.length == 0 ||
395              inList(e.getAttribute('itemtype'), types));
396     }
397
398     return fakeCollection(this.documentElement, isTopLevelItem);
399 };
400     
401 }