A simple CSS query selector

N

nick

Hope this gets through, I know some of you guys ignore google groups
posts due to all the spam.

Anyway, I know everyone and their mother has done a query selector
thing, but here's my take on it. I needed something simple and
lightweight for in-house use. I'd like to get some feedback from the
gurus here.

Mostly, I'm wondering if I've made any glaring mistakes that will
prevent this from working in certain browsers/situations. Advice on
coding style and optimization hints are also welcome.

The "select" function takes a CSS selector string. Currently selecting
tag names, class names and IDs is suported. These should work in any
combination supported by CSS. A selector like this should work: "body
#nav a.offsite, #footer .inner a"

///

var DM = DM || {};

(function(){

DM.contains = function(obj, value) {
for (var i=-1, len=obj.length; i<len; i++) {
if (obj===value) return true;
}
return false;
};

DM.unique = function(obj) {
for (var i=0, len=obj.length, r=[]; i<len; i++) {
if (!(DM.contains(r, obj))) r.push(obj);
}
return r;
};

DM.append = function(a, b) {
for (var i=0, len=b.length; i<len; i++) a.push(b);
return a;
},

DM.select = function(selector) {
var groups=selector.split(','), i=-1, group,
found=[], nodes=[document];
while (group=groups[++i]) {
var path=group.split(' '), j=-1, part;
while (part=path[++j]) {
nodes=selectPart(nodes, part)
}
found=DM.unique((DM.append(found, nodes)));
}
return found;
};

function selectPart(nodes, selector) {
var i=-1, symbol, attr, val, buffer='', tokens=[];
while (symbol=selector.charAt(++i)) {
switch (symbol) {
case '#':
if (i) tokens.push(buffer);
buffer='';
tokens.push('id');
break;
case '.':
if (i) tokens.push(buffer);
buffer='';
tokens.push('class');
break;
default :
if (!i) tokens.push('tagName');
buffer+=symbol;
break;
}
}
tokens.push(buffer);

i=-1;
while ((attr=tokens[++i]) && (val=tokens[++i])) {
nodes = byAttr(nodes, attr, val, i-1);
}

return nodes;
}

function getDescendants(nodes, includeSiblings) {
var node, results=[];
for (var i=0, len=nodes.length; i<len; i++) {
node=nodes;
DM.append(results, node.getElementsByTagName('*'));
if (includeSiblings) results.push(node);
}
return results;
}

function byAttr(nodes, attribute, value, includeSiblings) {
var results=[], desc=getDescendants(nodes, includeSiblings);

value=value.toLowerCase();

for (var i=0, len=desc.length; i<len; i++) {
if (((''+desc[attribute]).toLowerCase()==value) ||
((''+desc[attribute+'Name']).toLowerCase()==value)) {
results.push(desc);
}
}
return results;
}

})();

///

P.S. - our company's initials are DM, so I'm "namespacing" things into
an object named DM.
 
N

nick

Comma grouping didn't work. It does now.

var DM = DM || {};

(function(){

// array stuff

DM.contains = function(obj, value) {
for (var i=0, len=obj.length; i<len; i++) {
if (obj===value) return true;
}
return false;
};

DM.unique = function(obj) {
for (var i=0, len=obj.length, r=[]; i<len; i++) {
if (!(DM.contains(r, obj))) r.push(obj);
}
return r;
};

DM.append = function(a, b) {
for (var i=0, len=b.length; i<len; i++) a.push(b);
return a;
};

// string stuff

DM.trim = function(str) {
return (''+str).replace(/^\s\s*/, '').replace(/\s\s*$/, '');
};

// node query

DM.select = function(selector) {
var groups=selector.split(','), i=-1, group, found=[], nodes;
while (group=groups[++i]) {
nodes=[document];
group=DM.trim(group);
var path=group.split(' '), j=-1, part;
console.log(path);
while (part=path[++j]) {
console.log(part);
nodes=selectPart(nodes, part)
console.log(nodes);
}
DM.append(found, nodes);
}
found=DM.unique(found);
return found;
};

function selectPart(nodes, selector) {
var i=-1, symbol, attr, val, buffer='', tokens=[];
while (symbol=selector.charAt(++i)) {
switch (symbol) {
case '#':
if (i) tokens.push(buffer);
buffer='';
tokens.push('id');
break;
case '.':
if (i) tokens.push(buffer);
buffer='';
tokens.push('class');
break;
default :
if (!i) tokens.push('tagName');
buffer+=symbol;
break;
}
}
tokens.push(buffer);

i=-1;
while ((attr=tokens[++i]) && (val=tokens[++i])) {
nodes = byAttr(nodes, attr, val, i-1);
}

return nodes;
}

function getDescendants(nodes, includeSiblings) {
var node, results=[];
for (var i=0, len=nodes.length; i<len; i++) {
node=nodes;
DM.append(results, node.getElementsByTagName('*'));
if (includeSiblings) results.push(node);
}
return results;
}

function byAttr(nodes, attribute, value, includeSiblings) {
var results=[], desc=getDescendants(nodes, includeSiblings);

value=value.toLowerCase();

for (var i=0, len=desc.length; i<len; i++) {
if (((''+desc[attribute]).toLowerCase()==value) ||
((''+desc[attribute+'Name']).toLowerCase()==value)) {
results.push(desc);
}
}
return results;
}

})();
 
J

Jukka K. Korpela

16.8.2011 4:59, nick wrote :
Comma grouping didn't work. It does now.

Maybe you should have checked and tested your code before posting it,
and preferably you should post a URL instead of a bulk of code. All too
often, posted code gets munged one way or another - by the poster, by a
newsreader, by Google Groups, or something else - and people find all
too late that they have been discussing code different from the real code.

Perhaps most importantly, what are you trying to achieve? Reinventing
jQuery?
 
N

nick

Maybe you should have checked and tested your code before posting it,

Probably jumped the gun a little. I kind of half-tested it; I finished
writing it just before posting.
and preferably you should post a URL instead of a bulk of code. All too
often, posted code gets munged one way or another - by the poster, by a
newsreader, by Google Groups, or something else -

It's less than 100 lines, didn't think it was too long to post here.
Took care to shorten lines to around 70 characters. I thought that
would be better received than posting a link to pastebin or similar.
No offense but I'm sure someone would have complained either way ;)
and people find all
too late that they have been discussing code different from the real code..

I'd think this would be just as much of a problem when posting a link
to content that may change. At least this way there's a record of
changes.

Anyway, I don't expect it to change much more, just needed to work out
a few last minute bugs.
Perhaps most importantly, what are you trying to achieve? Reinventing
jQuery?

Not at all. All I really needed was a cross-browser
getElementsByClassName, and it sort of grew into a mini query
selector. I'm just adding on to an in-house utility class we use for
other generic stuff. We don't use jQuery or other GP libraries for
various reasons.

Anyway, I'm more concerned with the content of the script than the
mode of delivery or my particular intended use. It's just a query
selector, not anything close to a full-blown GP library. If anyone
has helpful comments regarding the soundness of this code as a query
selector function, I'd like to hear it. Things like "this won't work
on browser X because Y" or "doing it like this would be faster on most
browsers" or "you have a memory leak here" are more what I'm looking
for.
 
J

Jukka K. Korpela

I kind of half-tested it; I finished
writing it just before posting.

This implies an interesting definition for "half-testing": something you
do in infinitesimally small time between writing and posting. :)
I'd think this would be just as much of a problem when posting a link
to content that may change.

The proper way is to post a URL to a specific version and not change it
during the discussion, and (if needed) another URL for the working version.
Not at all. All I really needed was a cross-browser
getElementsByClassName, and it sort of grew into a mini query
selector.

It sounds like reinventing jQuery. It might be a good idea to rewrite
parts of jQuery, but hardly this way - Usenet groups aren't really a
good forum for collaborative software development, especially now that
people can do such things on web-based forums with loads of nice tools.
Things like "this won't work
on browser X because Y" or "doing it like this would be faster on most
browsers" or "you have a memory leak here" are more what I'm looking
for.

Well, you didn't make it particularly easy to others to test your code.
A URL of a test page would let others immediately test it on their browsers.

For one thing, your code doesn't handle class attributes containing
multiple values, a very common case. DM.select('.bar') only finds
elements with class="bar", not e.g. elements with class="foo bar".
 
J

John Nagle

I want to get the position of a DIV element from inside a Greasemonkey
script. (This is done to detect some strange situations involving
off-page content.) So I write:

Code:
var computedstyle = document.defaultView.getComputedStyle(elt,
null); // get style
var zindex = computedstyle.getPropertyValue("z-index");
var left = computedstyle.getPropertyValue("left");
var top = computedstyle.getPropertyValue("top");
...


This works, but I get back values of "auto" for "left" and "top".

Consulting the Mozilla documentation at
"https://developer.mozilla.org/en/DOM/window.getComputedStyle":

"The returned object actually represents the CSS 2.1 used values, not
the computed values. Originally, CSS 2.0 defined the computed values to
be the "ready to be used" values of properties after cascading and
inheritance, but CSS 2.1 redefined computed values as pre-layout, and
used values as post-layout. The getComputedStyle function returns the
old meaning of computed values, now called used values. There is no DOM
API to get CSS 2.1 computed values."

So I should get "used values", described at

"https://developer.mozilla.org/en/CSS/used_value"

"The used value of any CSS property is the final value of that property
after all calculations have been performed. Used values can be retrieved
by calling window.getComputedStyle. Dimensions (e.g. width, line-height)
are all in pixels, shorthand properties (e.g. background) are consistent
with their component properties (e.g. background-color), display is
consistent with position and float, and every CSS property has a value."

So, according to the documentation, I should get a number, not "auto",
for "top", "left", "right", and "bottom". In practice, they're usually
returning "auto".

Code bug? Documentation bug? Wrong version of documentation?

(Firefox 5.0.1, Windows 7, 64 bit).

John Nagle
 
N

nick

This implies an interesting definition for "half-testing": something you
do in infinitesimally small time between writing and posting. :)

That's actually a pretty accurate summation. ;)

I didn't do any testing between writing and posting, but I tested
things as I was writing.

If "not tested" throws exceptions, crashes your browser, and gets
stuck in infinite loops, and "fully tested" has unit tests and works
flawlessly, then maybe "half-tested" is somewhere in between... the
code runs without error in the most common use cases, but probably
doesn't work as expected in all cases.
The proper way is to post a URL to a specific version and not change it
during the discussion, and (if needed) another URL for the working version.

Sounds reasonable enough, I'll do that next time.
It sounds like reinventing jQuery. It might be a good idea to rewrite
parts of jQuery, but hardly this way - Usenet groups aren't really a
good forum for collaborative software development, especially now that
people can do such things on web-based forums with loads of nice tools.

This seems a bit small of a snippet of code for github or google code,
no? Or is that not what you had in mind?
Well, you didn't make it particularly easy to others to test your code.
A URL of a test page would let others immediately test it on their browsers.

Guilty as charged.
For one thing, your code doesn't handle class attributes containing
multiple values, a very common case. DM.select('.bar') only finds
elements with class="bar", not e.g. elements with class="foo bar".

Ah, thank you very much! A glaring omission on my part. And, I just
remembered that jsbin exists.

http://jsbin.com/oqefaw/edit#javascript,html - This copy supports
class attributes with multiple values.

I want to get the position of a DIV element

Get your own topic. =P
 
T

Thomas 'PointedEars' Lahn

John said:
I want to get the position of a DIV element from inside a Greasemonkey
script. (This is done to detect some strange situations involving
off-page content.) So I write:

It would be helpful if you told the URL of the Web site that you want to
post-process with Greasemonkey, or posted the relevant parts of its source
code if it was not publicly available.
Code:
var computedstyle = document.defaultView.getComputedStyle(elt,
null); // get style
var zindex = computedstyle.getPropertyValue("z-index");
var left = computedstyle.getPropertyValue("left");
var top = computedstyle.getPropertyValue("top");
...

This works, but I get back values of "auto" for "left" and "top".
[…]
So, according to the documentation, I should get a number, not "auto",
for "top", "left", "right", and "bottom". In practice, they're usually
returning "auto".

You should get a *pixel length* ("…px") – which is a _String_ value, not a
Number value – if, and only if, you (or the author of that Web site) have
set a length for that property, as its initial value is "auto", indeed:

<http://www.w3.org/TR/CSS21/visuren.html#propdef-left>

Have you/they?
Code bug? Documentation bug? Wrong version of documentation?

Uncertain. You need to be more precise about the stylesheet(s) and the
element (object) you want to retrieve style information about.

That said, for an element that does not have a `left' property declaration
applying to it, the value ("auto") in Iceweasel 5.0 (Firefox 5.0 for Debian
GNU/Linux) is the same as in Chromium 13.0.x and Opera 11.50, which along
with the definition of the initial value in the Specification indicates that
it is not a code bug.

You should keep in mind that MDN is a wiki, and anyone can write any
nonsense there, although it would hopefully not take long if someone
corrected/reverted it. (I am not saying that what is written there on this
subject is nonsense.)

There is also a fair chance that you misunderstood the getComputedStyle()
method, and wanted to retrieve the values of the proprietary, but – within
the boundaries of different box models – well-supported `offsetLeft' and
`offsetTop' DOM properties instead (which do yield Number values
representing pixel lengths).


PointedEars
 
J

John Nagle

It would be helpful if you told the URL of the Web site that you want to
post-process with Greasemonkey, or posted the relevant parts of its source
code if it was not publicly available.

Thanks. That makes sense.

I'm looking at Google search result pages, which have,
as parts of some search results, a DIV with negative X values.

To see this, use a DOM inspector on a Google search results
page. Go down to an id of "search" to find the search results.
The results are in an OL, and each LI item is one search result.
Continue looking inward until you find an A tag with an "href"
to a non-Google site. Now you're inside a search result DIV.

Now, while in Mozilla's DOM Inspector, look at
View->Object Viewer->Box Model. The A tag, according to
Mozilla, has position values of x=0, y=-2. Looking outward
in the DOM tree, the position values are bogus until the
DIV (class "vsc") just below the LI, which has reasonable
x and y values.

It makes sense (sort of) that "inline" items might not
have valid x and y positions in the box model. But there's
a DIV with class "vspi", which shows a computed style of
display="block", a "left" value of "-8px", and a box
position of (-8,-5). Clicking on that item in DOM Inspector
produces a red box bottom on the displayed page, rather than
a full red outline box, so even DOM Inspector is confused.

My goal here is to be able to find the next valid block
item outward from an A tag, so I can enhance or gray it out
depending on certain site legitimacy criteria. (Remember
that I'm talking about a Greasemoney or Mozilla plug in,
not an ordinary web page.) I want to
do this without depending on details like ids or class names,
so it doesn't have to be modified for each minor page format
change.

I don't need the absolute location of anything; I just need
to find the first DIV that has a valid box model.
Code:
var computedstyle = document.defaultView.getComputedStyle(elt,
null); // get style
var zindex = computedstyle.getPropertyValue("z-index");
var left = computedstyle.getPropertyValue("left");
var top = computedstyle.getPropertyValue("top");
...

This works, but I get back values of "auto" for "left" and "top".
[…]
So, according to the documentation, I should get a number, not "auto",
for "top", "left", "right", and "bottom". In practice, they're usually
returning "auto".

You should get a *pixel length* ("…px") – which is a _String_ value, not a
Number value – if, and only if, you (or the author of that Web site) have
set a length for that property, as its initial value is "auto", indeed:

<http://www.w3.org/TR/CSS21/visuren.html#propdef-left>

Have you/they?
Code bug? Documentation bug? Wrong version of documentation?

Uncertain. You need to be more precise about the stylesheet(s) and the
element (object) you want to retrieve style information about.

That's covered above.
That said, for an element that does not have a `left' property declaration
applying to it, the value ("auto") in Iceweasel 5.0 (Firefox 5.0 for Debian
GNU/Linux) is the same as in Chromium 13.0.x and Opera 11.50, which along
with the definition of the initial value in the Specification indicates that
it is not a code bug.

That's useful to know.
You should keep in mind that MDN is a wiki, and anyone can write any
nonsense there, although it would hopefully not take long if someone
corrected/reverted it. (I am not saying that what is written there on this
subject is nonsense.)

MDN does seem to say that a number should be expected for positions.
That MDN page says this has changed from version to version, and
may be wrong or not current.
There is also a fair chance that you misunderstood the getComputedStyle()
method, and wanted to retrieve the values of the proprietary, but – within
the boundaries of different box models – well-supported `offsetLeft' and
`offsetTop' DOM properties instead (which do yield Number values
representing pixel lengths).

PointedEars

John Nagle
 
J

Jukka K. Korpela

var computedstyle = document.defaultView.getComputedStyle(elt, null); // [...]
This works, but I get back values of "auto" for "left" and "top".

The CSS properties "left" and "top" have been defined so that their
computed value may be "auto", and in browsers getComputedStyle really
returns that value e.g. if you just test with any element without any
CSS settings.

This might be seen as conflicting with the DOM 2 specification, item
http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-OverrideAndComputed'
which refers to a definition of "computed value" at
http://www.w3.org/TR/1998/REC-CSS2-19980512/cascade.html#computed-value
which describes "auto" as a relative value that must be resolved. But
the reference is outdated, as the current CSS specification, CSS 2.1,
describes computed values differently:
http://www.w3.org/TR/CSS21/cascade.html#value-stages

It now by definition depends on the description of an individual CSS
property what its computed value really means.
 
T

Thomas 'PointedEars' Lahn

John said:
Thomas said:
It would be helpful if you told the URL of the Web site that you want to
post-process with Greasemonkey, or posted the relevant parts of its
source code if it was not publicly available.

[…]
I'm looking at Google search result pages, which have,
as parts of some search results, a DIV with negative X values.

To see this, use a DOM inspector on a Google search results
page. Go down to an id of "search" to find the search results.
The results are in an OL, and each LI item is one search result.
Continue looking inward until you find an A tag with an "href"
to a non-Google site. Now you're inside a search result DIV.

It would have been easier if you had me select the button left to the URI in
the DOM Inspector window (or Edit → Select Element By Click), and then click
the first link in a search hit.
Now, while in Mozilla's DOM Inspector, look at
View->Object Viewer->Box Model. The A tag, according to
Mozilla, has position values of x=0, y=-2. Looking outward
in the DOM tree, the position values are bogus until the
DIV (class "vsc") just below the LI, which has reasonable
x and y values.

It makes sense (sort of) that "inline" items might not
have valid x and y positions in the box model. But there's
a DIV with class "vspi", which shows a computed style of
display="block", a "left" value of "-8px", and a box
position of (-8,-5). Clicking on that item in DOM Inspector
produces a red box bottom on the displayed page, rather than
a full red outline box, so even DOM Inspector is confused.

You are mistaken. For some reason, that empty div.vspi is positioned
absolute (`position: absolute') below the other elements on the z-axis
(`z-index: -1'). Its parent, a div.vsc, is positioned relative (`position:
relative') accordingly. As a result, div.vspi is positioned with regard to
the box of div.vsc, while the positioning of the other, not explicitly
positioned elements (i.e. `position: static') follow the normal document
flow.
My goal here is to be able to find the next valid block
item outward from an A tag, so I can enhance or gray it out

So for each `a' element in the `ol' element, that meets your criteria,
traverse the tree up to the div.vsc or a `li' element, and set e.g. its
`opacity' style property to, e.g. "0.5". I do not see the problem. In
fact, formatting might be possible with a simple user stylesheet and no
scripting at all (just not on the `li' element, as CSS has no notion of
child-based formatting).
depending on certain site legitimacy criteria. (Remember
that I'm talking about a Greasemoney or Mozilla plug in,
not an ordinary web page.)

Did you know that there is a WOT (Web of Trust) extension for Firefox?

I want to do this without depending on details like ids or class names,
so it doesn't have to be modified for each minor page format
change.

Traversing up to the `li' element remains.
I don't need the absolute location of anything; I just need
to find the first DIV that has a valid box model.

Your notion that a box model could be invalid, and that would be invalid
here, is bogus. And the box model has little to do with the computed style
or used style. In fact, the DOM Inspector has another dropdown entry for
"Computed Style", which probably refers to the CSS 2.0 definition (as the
DOM Inspector existed before CSS 2.1, too.)
MDN does seem to say that a number should be expected for positions.

Learn to think clearly.
Learn to distinguish – what is, and what seems to be.
-- Surak

The word "number" does not occur in either MDN article to begin with. The
article about the used value is referring to dimension properties, like
`width' and `line-height', which CSS 2.0 computed style is given "in
pixels", meaning a pixel length. A pixel length is a number followed by the
unit of length, "px". In implementations that do not have a pixel length
data type, such as ECMAScript implementations, this value is represented by
a String value, not a Number value.

`left' is no dimension property, and its initial value is `auto'. You would
only get a pixel length for its CSS 2.0 computed value if there was a
stylesheet applying to it to declare a property value different from `auto'.
That MDN page says this has changed from version to version, and
may be wrong or not current.

Only the meaning of "computed style" has changed from one CSS version to the
next. What was called "computed style" in CSS 2.0 is called "used style" in
CSS 2.1. The DOM Level 2 CSS API was created and implemented before CSS 2.1
came to be, so the getComputedStyle() method returns the computed style as
it was understood in CSS 2.0, which is the "used style" in CSS 2.1.
Modifying the API to accomodate the change in CSS definitions would have
broken existing programs, so it was not done. (As a side effect, there is
no way to retrieve the CSS 2.1 computed style.)

BTW: While the OP's reaction shows that he has not yet read the Usenet
primer, you should create a new thread for an entirely new problem/question
next time. And please trim your quotes to the relevant minimum, which
includes not quoting signatures of any kind (unless you refer to them).


HTH

PointedEars
 
J

John Nagle

(flaming deleted)

For some reason, that empty div.vspi is positioned
absolute (`position: absolute') below the other elements on the z-axis
(`z-index: -1').

Right. I hadn't noticed that the item was positioned absolute.
The DIV seems to be redundant. Probably some artifact of the generator
for Google search results pages.

John Nagle
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,769
Messages
2,569,582
Members
45,065
Latest member
OrderGreenAcreCBD

Latest Threads

Top