-
Notifications
You must be signed in to change notification settings - Fork 1
/
omega.js
372 lines (296 loc) · 8.03 KB
/
omega.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
+function () { 'use strict';
/* Omega JavaScript Library
- Evan Myller, with care for a better web.
*/
function _class(body) {
// a simple fake constructor factory.
if (!body) body = {};
// the real thing
function init() {
return body.init.apply(this, arguments);
}
// forces instances' .constructor to be init
body.constructor = init;
init.prototype = body;
return init;
}
function _array(object) {
// convert a iterable object into an array
return Array.prototype.slice.call(object);
}
function _map(array, callback) {
// maps array with callback, returning the processed result
var results = [];
for (var i = -1, l = array.length; ++i < l;)
results.push(callback(array[i]));
return results;
}
function _strip(string, target) {
// remove trailing spaces or specified character from a string
if (!target) target = '\\s';
return string.replace(new RegExp('^['+target+']+|['+target+']+$', 'g'), '');
}
function _merge(array, extension) {
// merge extension into array
Array.prototype.push.apply(array, extension);
}
var Omega = _class({
// identifies Omega objects
__omega__: true,
init: function (object, context) {
if (object && object.__omega__)
return object;
// being called instead of instantiated
if (this.constructor !== Omega)
return new Omega(object, context);
// "initialize" the object as an array
this.length = 0;
// fill the object with a Omega.DOM.QuerySet object
if (typeof object === 'string')
_merge(this, new dom.Query(object, context).results);
// wrap a dom element or the Window object
else
if (object.nodeType && object.nodeName || object === window)
_merge(this, [object]);
},
toString: function () { return '[object Omega]' },
// Trick to make Consoles display an Omega object as an array
splice: function () {}
});
Omega.extend = function (extension) {
// a shortcut to extend the Omega prototype
for (var key in extension)
if (Object.prototype.hasOwnProperty.call(extension, key))
Omega.prototype[key] = extension[key];
};
// Expose Omega main constructor to window scope
if (typeof window !== 'undefined') window.Omega = Omega;
/* The Omega DOM library */
var dom = Omega.DOM = {
Query: _class({
// a rapid element selector based on CSS selectors.
init: function (selector, context) {
var start_time = new Date,
groups = dom.parse_selector(selector),
results = [];
for (var g = -1, group, curr_context; group = groups[++g];) {
curr_context = new Omega(context || document);
for (var p = -1, part; part = group.parts[++p];) {
var elements = [];
// collect context
for (var i = -1, all; curr_context[++i];) {
all = curr_context[i].getElementsByTagName(part.tag_name);
for (var j = -1; all[++j];)
dom.match(all[j], part) && elements.push(all[j]);
}
curr_context = elements;
}
_merge(results, elements);
}
this.results = [];
for (var i = -1; results[++i];)
// clean the result set to avoid duplicates
if (this.results.indexOf(results[i]) === -1)
this.results.push(results[i]);
// records the spent time
this.time = new Date - start_time;
}
}),
parse_selector: function (selector) {
// parse a complete CSS selector into groups of detailed parts
selector = _strip(selector, '\\s,');
var groups = [], group, g = 0,
part, p = 0,
pos = -1, c,
curr_token,
in_quote = false,
// placeholders for values being parsed
attr, pseudo;
while (++pos < selector.length) {
// shortcut to current character
c = selector.charAt(pos);
// create new group
if (!groups[g])
groups[g] = group = {
parts: [],
str: ''
};
// create new part
if (!group.parts[p])
group.parts[p] = part = {
rel: ' ',
tag_name: '',
id: '',
classes: [],
attributes: [],
pseudos: [],
str: ''
};
// toggle "quote state"
if (c === '"') {
in_quote = !in_quote;
continue;
}
// node relationship
else
if (!in_quote && !curr_token && '>+~'.indexOf(c) !== -1) {
part.rel = c;
continue;
}
// handle space
else
if (!in_quote && c === ' ') {
// breaks into a new part
if (curr_token !== 'attr' && part.str) {
p++;
curr_token = undefined;
}
// ignore trailing spaces
continue;
}
// new group
else
if (!in_quote && c === ',') {
p = 0;
g++;
continue;
}
// id
else
if (!in_quote && c === '#')
curr_token = 'id';
// class name
else
if (!in_quote && c === '.') {
curr_token = 'class_name';
part.classes.push('');
}
// attribute
else
if (!in_quote && c === '[') {
// start attribute
curr_token = 'attr';
part.attributes.push(attr = {
name: '',
op: '',
value: ''
});
}
else
if (!in_quote && curr_token === 'attr'
&& '$*=]^~'.indexOf(c) !== -1) {
// end attribute
if (c === ']')
curr_token = undefined;
// attribute operator
else
attr.op += c;
}
// pseudo classes
else
if (!in_quote && c === ':' && curr_token !== 'attr') {
curr_token = 'pseudo';
part.pseudos.push(pseudo = {
name: '',
param: ''
});
}
// append according to switch token, if any
else {
if (curr_token === 'id')
part.id += c;
else
if (curr_token === 'class_name' && part.classes.length)
part.classes[part.classes.length - 1] += c;
else
if (curr_token === 'attr')
attr[attr.op ? 'value' : 'name'] += c;
else
if (curr_token === 'pseudo')
pseudo.name += c;
else
part.tag_name += c;
}
// proceed adding the char
part.str += c;
}
// clean the groups compile the complete selectors strings
for (g = -1; group = groups[++g];) {
for (p = -1; part = group.parts[++p];)
if (part.str)
group.str += (part.rel + ' ' + part.str + ' ');
else
group.parts.splice(p, 1);
group.str = _strip(group.str).replace(/\s{2,}/g, ' ');
}
return groups;
},
match: function (element, token) {
// test an element against a parsed selector
// id
if (token.id && element.getAttribute('id') !== token.id)
return false;
// class name filtering
if (token.classes.length) {
var el_classes = ' ' + element.className + ' ';
for (var i = -1; token.classes[++i];)
if (el_classes.indexOf(token.classes[i]) < 0)
return false;
}
// attribute filtering
for (var i = -1, attr; attr = token.attributes[++i];)
if (
// element doesn't have such attribute
!element.hasAttribute(attr.name)
// element has attribute but it doesn't match
|| attr.op &&
!dom._attr_match[attr.op](
element.getAttribute(attr.name),
attr.value, ' "')
)
return false;
// pseudo classes filtering
for (var i = -1, pseudo; pseudo = token.pseudos[++i];)
if (!dom._pseudo_match[pseudo.name](element, pseudo.param))
return false;
return true;
},
_attr_match: {
// collection of attribute matchers
'=': function (actual_value, test) { // matches exactly
return actual_value === test;
},
'*=': function (actual_value, test) { // contains
return actual_value.indexOf(test) !== -1;
},
'~=': function (actual_value, test) { // contains word
return (' ' + actual_value + ' ').indexOf(' ' + test + ' ') !== -1;
},
'^=': function (actual_value, test) { // starts with
return !actual_value.indexOf(test);
},
'$=': function (actual_value, test) { // ends with
return actual_value.slice(-test.length) === test;
}
},
_pseudo_match: {
'first-child': function (element) {
var first_child = element.parentNode.firstChild;
while (first_child.nodeType !== 1)
first_child = first_child.nextSibling;
return element === first_child;
},
'last-child': function (element) {
var last_child = element.parentNode.lastChild;
while (last_child.nodeType !== 1)
last_child = last_child.previousSibling;
return element === last_child;
},
'only-child': function (element) {
return (
dom._pseudo_match['first-child'](element) &&
dom._pseudo_match['last-child'](element));
}
}
};
}();