HTMLElement.properties seemingly working as per spec (i.e. a bit strange)
[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
137 function getItemValueProperty(e) {
138     if (e.tagName == 'META')
139         return 'content';
140     if (e.tagName == 'AUDIO' || e.tagName == 'EMBED' ||
141         e.tagName == 'IFRAME' || e.tagName == 'IMG' ||
142         e.tagName == 'SOURCE' || e.tagName == 'VIDEO')
143         return 'src';
144     if (e.tagName == 'A' || e.tagName == 'AREA' || e.tagName == 'LINK')
145         return 'href';
146     if (e.tagName == 'OBJECT')
147         return 'data';
148     if (e.tagName == 'TIME' && e.hasAttribute('datetime'))
149         return 'datetime';
150     return 'textContent';
151 }
152
153 Object.defineProperty(Element.prototype, 'itemValue',
154 {
155     get: function() {
156         if (!this.hasAttribute('itemprop'))
157             return null;
158         if (this.hasAttribute('itemscope'))
159             return this;
160         return this[getItemValueProperty(this)];
161     },
162     set: function(val) {
163         if (!this.hasAttribute('itemprop') || this.hasAttribute('itemscope'))
164             throw new Error('INVALID_ACCESS_ERR');
165         this[getItemValueProperty(this)] = val;
166     }
167 });
168
169 // Element.properties
170
171 Object.defineProperty(Element.prototype, 'properties', { get:
172 function() {
173     // http://www.whatwg.org/specs/web-apps/current-work/multipage/microdata.html#the-properties-of-an-item
174     var itemElem = this;
175
176     var props = [];
177     function updateProperties(elemFilter) {
178         var root = itemElem;
179
180         props.length = 0;
181
182         // when the root isn't a item in the document, match nothing
183         // FIXME: the spec doesn't actually say this.
184         if (!root.itemScope || !contains(document.documentElement, root))
185             return;
186
187         var pending = [];
188         function pushChildren(e) {
189             for (var child = e.lastChild; child; child = child.previousSibling) {
190                 if (child.nodeType == 1 && elemFilter(child)) {
191                     pending.push(child);
192                 }
193             }
194         }
195         pushChildren(root);
196
197         function getScopeNode(e) {
198             var scope = e.parentNode;
199             while (scope && !scope.itemScope)
200                 scope = scope.parentNode;
201             return scope;
202         }
203         var refIds = splitTokens(root.itemRef);
204         idloop: for (var i=0; i<refIds.length; i++) {
205             var candidate = document.getElementById(refIds[i]);
206             if (!candidate || !elemFilter(candidate))
207                 continue;
208             var scope = getScopeNode(candidate);
209             for (var j=0; j<pending.length; j++) {
210                 if (candidate == pending[j])
211                     continue idloop;
212                 if (contains(pending[j], candidate) &&
213                     (pending[j] == scope ||
214                      getScopeNode(pending[j]) == scope))
215                     continue idloop;
216             }
217             pending.push(candidate);
218         }
219
220         // from http://www.quirksmode.org/dom/getElementsByTagNames.html
221         pending.sort(function (a,b){return (a.compareDocumentPosition(b)&6)-3;});
222
223         while (pending.length) {
224             var current = pending.pop();
225             if (current.hasAttribute('itemprop'))
226                 props.push(current);
227             if (!current.hasAttribute('itemscope'))
228                 pushChildren(current);
229         }
230     }
231
232     props.names = [];
233     function updateNames() {
234         while (props.names.length)
235             props.names.pop();
236         for (var i = 0; i < props.length; i++) {
237             var propNames = splitTokens(props[i].getAttribute('itemprop'));
238             for (var j = 0; j < propNames.length; j++) {
239                 if (!inList(propNames[j], props.names))
240                     props.names.push(propNames[j]);
241             }
242         }
243     }
244
245     function updatePropertyNodeList(pnl, name) {
246         while (pnl.length)
247             pnl.pop();
248         while (pnl.values.length)
249             pnl.values.pop();
250         for (var i=0; i<props.length; i++) {
251             if (inList(name, splitTokens(props[i].getAttribute('itemprop')))) {
252                 pnl.push(props[i]);
253                 pnl.values.push(props[i].itemValue);
254             }
255         }
256     }
257
258     props.item = function(idx){return this[idx];};
259
260     var pnlCache = {};
261     props.namedItem = function (name) {
262         if (!pnlCache[name]) {
263             // fake PropertyNodeList
264             var pnl = [];
265             pnl.item = function(idx){return this[idx];};
266             pnl.values = [];
267             updatePropertyNodeList(pnl, name);
268             pnlCache[name] = pnl;
269         }
270         return pnlCache[name];
271     };
272
273     function updateHandler(elemFilter) {
274         updateProperties(function(e){return typeof elemFilter != 'function' || elemFilter(e);});
275         updateNames();
276         for (name in pnlCache)
277             updatePropertyNodeList(pnlCache[name], name);
278     }
279
280     // keep collection up to date if possible
281     if (document.documentElement.addEventListener) {
282         document.documentElement.addEventListener('DOMAttrModified', updateHandler, false);
283         document.documentElement.addEventListener('DOMNodeInserted', updateHandler, false);
284         document.documentElement.addEventListener('DOMNodeRemoved',
285             function(ev) {
286                 updateHandler(function(e){return (e != ev.target) && !contains(ev.target, e);});
287             }, false);
288     }
289
290     updateHandler();
291     return props;
292 }});
293
294 // document.getItems
295
296 document.getItems = function(typeNames) {
297     var types = splitTokens(typeNames);
298
299     function isTopLevelItem(e) {
300         return e.hasAttribute('itemscope') &&
301             !e.hasAttribute('itemprop') &&
302             (types.length == 0 ||
303              inList(e.getAttribute('itemtype'), types));
304     }
305
306     return fakeCollection(this.documentElement, isTopLevelItem);
307 };