fix itemValue reflection for <time datetime>
[microdatajs:microdatajs.git] / microdata.js
1 // emulate Object.defineProperty
2 if (!Object.defineProperty && document.__defineGetter__) {
3     Object.defineProperty = function(obj, prop, desc) {
4         if (desc.get)
5             obj.__defineGetter__(prop, desc.get);
6         if (desc.set)
7             obj.__defineSetter__(prop, desc.set);
8     };
9 }
10
11 // emulate Element.textContent
12 if (typeof document.documentElement.textContent == 'undefined') {
13     Object.defineProperty(Element.prototype, 'textContent',
14         {
15             get: function() {
16                 return this.innerText;
17             },
18             set: function(val) {
19                 this.innerText = val;
20             }
21         });
22 }
23
24 // utility functions
25
26 function splitTokens(s) {
27     if (s && /\S/.test(s)) return s.split(/\s+/);
28     return [];
29 }
30
31 function inList(item, list) {
32     for (var i = 0; i < list.length; i++) {
33         if (item == list[i])
34             return true;
35     }
36     return false;
37 }
38
39 // http://ejohn.org/blog/comparing-document-position/
40 function contains(a, b){
41     return a.contains ?
42         a != b && a.contains(b) :
43         !!(a.compareDocumentPosition(b) & 16);
44 }
45
46 function fakeCollection(rootElem, incFilter, recFilter) {
47     var elems = [];
48     elems.item = function(idx){return this[idx];};
49
50     function update(_incFilter, _recFilter) {
51         while (elems.length)
52             elems.pop();
53         function pushElements(elem) {
54             if (_incFilter(elem))
55                 elems.push(elem);
56             if (!_recFilter || _recFilter(elem)) {
57                 for (var child = elem.firstChild; child; child = child.nextSibling) {
58                     if (child.nodeType == 1) {
59                         pushElements(child);
60                     }
61                 }
62             }
63         }
64         pushElements(rootElem);
65     }
66
67     function updateSimple() {
68         update(incFilter, recFilter);
69     }
70
71     function afterUpdate() {
72         if (typeof elems.__onchange__ == 'function') {
73             elems.__onchange__();
74         }
75     }
76
77     function updateHandler() {
78         updateSimple();
79         afterUpdate();
80     }
81
82     // keep collection up to date if possible
83     if (rootElem.addEventListener) {
84         rootElem.addEventListener('DOMAttrModified', updateHandler, false);
85         rootElem.addEventListener('DOMNodeInserted', updateHandler, false);
86         rootElem.addEventListener('DOMNodeRemoved',
87             function(ev) {
88                 update(function(e){return e != ev.target && incFilter(e);},
89                        function(e){return e != ev.target;});
90                 afterUpdate();
91             }, false);
92     }
93
94     updateSimple();
95
96     return elems;
97 }
98
99 function map(list, func) {
100     var ret = [];
101     for (var i = 0; i < list.length; i++) {
102         ret.push(func(list[i]));
103     }
104     return ret;
105 }
106
107 function filter(list, pred) {
108     var ret = [];
109     for (var i = 0; i < list.length; i++) {
110         if (pred(list[i]))
111             ret.push(list[i]);
112     }
113     return ret;
114 }
115
116 // Element attribute<->property reflection
117
118 function reflectBoolean(attr, prop) {
119     Object.defineProperty(Element.prototype, prop,
120         { get: function () { return this.hasAttribute(attr); },
121           set: function (val) { if (val) this.setAttribute(attr, attr);
122                                 else this.removeAttribute(attr); } });
123 }
124
125 function reflectString(attr, prop) {
126     Object.defineProperty(Element.prototype, prop,
127         { get: function () { return this.getAttribute(attr) || ""; },
128           set: function (val) { this.setAttribute(attr, val); }});
129 }
130
131 reflectBoolean('itemscope', 'itemScope');
132 reflectString('itemtype', 'itemType');
133 reflectString('itemid', 'itemId');
134 reflectString('itemprop', 'itemProp');
135 reflectString('itemref', 'itemRef');
136 // FIXME: only if not browser-implemented
137 reflectString('datetime', 'dateTime');
138
139 function getItemValueProperty(e) {
140     var tag = e.tagName.toUpperCase();
141     if (tag == 'META')
142         return 'content';
143     if (tag == 'AUDIO' || tag == 'EMBED' ||
144         tag == 'IFRAME' || tag == 'IMG' ||
145         tag == 'SOURCE' || tag == 'VIDEO')
146         return 'src';
147     if (tag == 'A' || tag == 'AREA' || tag == 'LINK')
148         return 'href';
149     if (tag == 'OBJECT')
150         return 'data';
151     if (tag == 'TIME' && e.hasAttribute('datetime'))
152         return 'dateTime';
153     return 'textContent';
154 }
155
156 Object.defineProperty(Element.prototype, 'itemValue',
157 {
158     get: function() {
159         if (!this.hasAttribute('itemprop'))
160             return null;
161         if (this.hasAttribute('itemscope'))
162             return this;
163         return this[getItemValueProperty(this)];
164     },
165     set: function(val) {
166         if (!this.hasAttribute('itemprop') || this.hasAttribute('itemscope'))
167             throw new Error('INVALID_ACCESS_ERR');
168         this[getItemValueProperty(this)] = val;
169     }
170 });
171
172 // Element.properties
173
174 Object.defineProperty(Element.prototype, 'properties', { get:
175 function() {
176     // http://www.whatwg.org/specs/web-apps/current-work/multipage/microdata.html#the-properties-of-an-item
177     var itemElem = this;
178
179     var props = [];
180     function updateProperties(elemFilter) {
181         var root = itemElem;
182
183         props.length = 0;
184
185         // when the root isn't a item in the document, match nothing
186         // FIXME: the spec doesn't actually say this.
187         if (!root.itemScope || !contains(document.documentElement, root))
188             return;
189
190         var pending = [];
191         function pushChildren(e) {
192             for (var child = e.lastChild; child; child = child.previousSibling) {
193                 if (child.nodeType == 1 && elemFilter(child)) {
194                     pending.push(child);
195                 }
196             }
197         }
198         pushChildren(root);
199
200         function getScopeNode(e) {
201             var scope = e.parentNode;
202             while (scope && !scope.itemScope)
203                 scope = scope.parentNode;
204             return scope;
205         }
206         var refIds = splitTokens(root.itemRef);
207         idloop: for (var i=0; i<refIds.length; i++) {
208             var candidate = document.getElementById(refIds[i]);
209             if (!candidate || !elemFilter(candidate))
210                 continue;
211             var scope = getScopeNode(candidate);
212             for (var j=0; j<pending.length; j++) {
213                 if (candidate == pending[j])
214                     continue idloop;
215                 if (contains(pending[j], candidate) &&
216                     (pending[j] == scope ||
217                      getScopeNode(pending[j]) == scope))
218                     continue idloop;
219             }
220             pending.push(candidate);
221         }
222
223         // from http://www.quirksmode.org/dom/getElementsByTagNames.html
224         pending.sort(function (a,b){return (a.compareDocumentPosition(b)&6)-3;});
225
226         while (pending.length) {
227             var current = pending.pop();
228             if (current.hasAttribute('itemprop'))
229                 props.push(current);
230             if (!current.hasAttribute('itemscope'))
231                 pushChildren(current);
232         }
233     }
234
235     props.names = [];
236     function updateNames() {
237         while (props.names.length)
238             props.names.pop();
239         for (var i = 0; i < props.length; i++) {
240             var propNames = splitTokens(props[i].getAttribute('itemprop'));
241             for (var j = 0; j < propNames.length; j++) {
242                 if (!inList(propNames[j], props.names))
243                     props.names.push(propNames[j]);
244             }
245         }
246     }
247
248     function updatePropertyNodeList(pnl, name) {
249         while (pnl.length)
250             pnl.pop();
251         while (pnl.values.length)
252             pnl.values.pop();
253         for (var i=0; i<props.length; i++) {
254             if (inList(name, splitTokens(props[i].getAttribute('itemprop')))) {
255                 pnl.push(props[i]);
256                 pnl.values.push(props[i].itemValue);
257             }
258         }
259     }
260
261     props.item = function(idx){return this[idx];};
262
263     var pnlCache = {};
264     props.namedItem = function (name) {
265         if (!pnlCache[name]) {
266             // fake PropertyNodeList
267             var pnl = [];
268             pnl.item = function(idx){return this[idx];};
269             pnl.values = [];
270             updatePropertyNodeList(pnl, name);
271             pnlCache[name] = pnl;
272         }
273         return pnlCache[name];
274     };
275
276     function updateHandler(elemFilter) {
277         updateProperties(function(e){return typeof elemFilter != 'function' || elemFilter(e);});
278         updateNames();
279         for (name in pnlCache)
280             updatePropertyNodeList(pnlCache[name], name);
281     }
282
283     // keep collection up to date if possible
284     if (document.documentElement.addEventListener) {
285         document.documentElement.addEventListener('DOMAttrModified', updateHandler, false);
286         document.documentElement.addEventListener('DOMNodeInserted', updateHandler, false);
287         document.documentElement.addEventListener('DOMNodeRemoved',
288             function(ev) {
289                 updateHandler(function(e){return (e != ev.target) && !contains(ev.target, e);});
290             }, false);
291     }
292
293     updateHandler();
294     return props;
295 }});
296
297 // document.getItems
298
299 document.getItems = function(typeNames) {
300     var types = splitTokens(typeNames);
301
302     function isTopLevelItem(e) {
303         return e.hasAttribute('itemscope') &&
304             !e.hasAttribute('itemprop') &&
305             (types.length == 0 ||
306              inList(e.getAttribute('itemtype'), types));
307     }
308
309     return fakeCollection(this.documentElement, isTopLevelItem);
310 };