Dynamic CSS3 prefix user agent detection - javascript

Is there a better way then using jQuery.browser, or equivalents, for determining css 3 prefixes (-moz, -webkit, etc), as it is disencouraged? Since the css is dynamic (the user can do anything with it on runtime), css hacks and style tag hacks can't be considered.

I don't see the issue with using the navigator.userAgent to determine if you need to cater for Webkit / Gecko CSS3 prefixes. Or better yet, just stick with CSS2 until CSS3 becomes a W3C Recommendation.
The reason use of the navigator object is discouraged is because it was used over Object detection when (java)scripting for different browsers, your situation is one where it is fine to use user agent detection, because your'e specifically targeting certain quirks with different rendering engines.
Edit:
Picking up from where cy left off, you can use javascript object detection to detect whether a prefix is used, I made some quick code to do so:
window.onload = function ()
{
CSS3 = {
supported: false,
prefix: ""
};
if (typeof(document.body.style.borderRadius) != 'undefined') {
CSS3.supported = true;
CSS3.prefix = "";
} else if (typeof(document.body.style.MozBorderRadius) != 'undefined') {
CSS3.supported = true;
CSS3.prefix = "-moz-";
} else if (typeof(document.body.style.webkitBorderRadius) != 'undefined') {
CSS3.supported = true;
CSS3.prefix = "-webkit-";
}
if (CSS3.supported)
if (CSS3.prefix == "")
alert("CSS3 is supported in this browser with no prefix required.");
else
alert("CSS3 is supported in this browser with the prefix: '"+CSS3.prefix+"'.");
else
alert("CSS3 is NOT supported in this browser.");
};
Remember to watch out for strange quirks such as -moz-opacity which is only supported in older versions of Firefox but has now been deprecated in favour of opacity, while it still uses the -moz- prefix for other new CSS3 styles.

Array.prototype.slice.call(
document.defaultView.getComputedStyle(document.body, "")
)
.join("")
.match(/(?:-(moz|webkit|ms|khtml)-)/);
Will return an array with two elements. One with dashes and one without dashes, both lowercase, for your convenience.
Array.prototype.slice.call(
document.defaultView.getComputedStyle(document.body, "")
);
Without the browser check will return an array of nearly all the css properties the browser understands. Since it's computed style it won't display shorthand versions, but otherwise I think it gets all of them. It's a quick hop skip and a jump to auto detect whatever you need as only vendor prefixed stuff starts with a dash.
IE9, Chrome, Safari, FF. Opera won't let you slice CSSStyleDeclaration for you can still use the same getComputedStyle code and loop through the properties or test for a specific one. Opera also wanted to be the odd man out and not report the vendor prefix dasherized. Thanks Opera.
Object.keys(CSSStyleDeclaration.prototype)
Works in IE9 and FF and reports the TitleCased (JavaScript) version of the vendor property names. Doesn't work in WebKit as the prototype only reports the methods.
Here's an interesting and very dangerous function I just wrote along these lines:
(function(vp,np){
Object.keys(this).filter(function(p){return vp=vp||p.match(/^(Moz|ms)/)}).forEach(function(op){
this.__defineGetter__(np=op.replace(vp[0], ""), function() { return this[op] });
this.__defineSetter__(np, function(val) { this[op] = val.toString() });
}, this);
}).call(CSSStyleDeclaration.prototype);
I didn't test anything Konquerer.

It's adding in another library, but would Modernizr work for you? It adds CSS classes to the <html> tag that can tell you what the browser supports.
It does muddy up the code a bit, but can certainly be helpful in appropriate situations.

Speculatively: Yes. You can try adding a vendor prefix css rule (that's what they're called), and then test to see if that rule exists. Those vendor-specific rules won't be added to the DOM in browsers in which they're not supported in some cases.
For example, if you try adding a -moz rule in webkit, it won't add to the DOM, and thus jQuery won't be able to detect it.
so,
$('#blah').css('-moz-border-radius','1px');
$('#blah').css('-moz-border-radius') //null in Chrome
Conversely,
$('#blah').css('-webkit-border-radius','1px');
$('#blah').css('-webkit-border-radius'); //returns "" in Chrome
This method works in WebKit browsers; I'm testing to see if it works in others. Pending.
Edit: Sadly, this isn't working in Firefox or Opera, which just returns "" no matter compatibility. Thinking of ways to do this cross-browser...
Final Edit: Andrew Dunn's answer does this in a way that works (at least in FF and Webkit, which is better than my method).

I use ternary operator to have it only in 1 line. If it's not webkit nor gecko, I'll just use the standard property. If it has no support, who really cares then?
var prefix = ('webkitAnimation' in document.body.style) ? '-webkit-' : ('MozAnimation' in document.body.style? '-moz-' : '');
Basically I found Animation is one of the properties never changed. As soon as the browser starts supporting the Draft / Candidate Recommendation of a CSS3 property, it drops the prefix on JS side. So you will need to be careful and take in mind that, before copy-pasting.

Related

Looping through arrays with JavaScript on Firefox issue [duplicate]

I have some JavaScript code that works in IE containing the following:
myElement.innerText = "foo";
However, it seems that the 'innerText' property does not work in Firefox. Is there some Firefox equivalent? Or is there a more generic, cross browser property that can be used?
Update: I wrote a blog post detailing all the differences much better.
Firefox uses W3C standard Node::textContent, but its behavior differs "slightly" from that of MSHTML's proprietary innerText (copied by Opera as well, some time ago, among dozens of other MSHTML features).
First of all, textContent whitespace representation is different from innerText one. Second, and more importantly, textContent includes all of SCRIPT tag contents, whereas innerText doesn't.
Just to make things more entertaining, Opera - besides implementing standard textContent - decided to also add MSHTML's innerText but changed it to act as textContent - i.e. including SCRIPT contents (in fact, textContent and innerText in Opera seem to produce identical results, probably being just aliased to each other).
textContent is part of Node interface, whereas innerText is part of HTMLElement. This, for example, means that you can "retrieve" textContent but not innerText from text nodes:
var el = document.createElement('p');
var textNode = document.createTextNode('x');
el.textContent; // ""
el.innerText; // ""
textNode.textContent; // "x"
textNode.innerText; // undefined
Finally, Safari 2.x also has buggy innerText implementation. In Safari, innerText functions properly only if an element is
neither hidden (via style.display == "none") nor orphaned from the document. Otherwise, innerText results in an empty string.
I was playing with textContent abstraction (to work around these deficiencies), but it turned out to be rather complex.
You best bet is to first define your exact requirements and follow from there. It is often possible to simply strip tags off of innerHTML of an element, rather than deal with all of the possible textContent/innerText deviations.
Another possibility, of course, is to walk the DOM tree and collect text nodes recursively.
Firefox uses the W3C-compliant textContent property.
I'd guess Safari and Opera also support this property.
If you only need to set text content and not retrieve, here's a trivial DOM version you can use on any browser; it doesn't require either the IE innerText extension or the DOM Level 3 Core textContent property.
function setTextContent(element, text) {
while (element.firstChild!==null)
element.removeChild(element.firstChild); // remove all existing content
element.appendChild(document.createTextNode(text));
}
jQuery provides a .text() method that can be used in any browser. For example:
$('#myElement').text("Foo");
As per Prakash K's answer Firefox does not support the innerText property. So you can simply test whether the user agent supports this property and proceed accordingly as below:
function changeText(elem, changeVal) {
if (typeof elem.textContent !== "undefined") {
elem.textContent = changeVal;
} else {
elem.innerText = changeVal;
}
}
A really simple line of Javascript can get the "non-taggy" text in all main browsers...
var myElement = document.getElementById('anyElementId');
var myText = (myElement.innerText || myElement.textContent);
Note that the Element::innerText property will not contain the text which has been hidden by CSS style "display:none" in Google Chrome (as well it will drop the content that has been masked by other CSS technics (including font-size:0, color:transparent, and a few other similar effects that cause the text not to be rendered in any visible way).
Other CSS properties are also considered :
First the "display:" style of inner elements is parsed to determine if it delimits a block content (such as "display:block" which is the default of HTML block elements in the browser's builtin stylesheet, and whose behavior as not been overriden by your own CSS style); if so a newline will be inserted in the value of the innerText property. This won't happen with the textContent property.
The CSS properties that generate inline contents will also be considered : for example the inline element <br \> that generates an inline newline will also generate an newline in the value of innerText.
The "display:inline" style causes no newline either in textContent or innerText.
The "display:table" style generates newlines around the table and between table rows, but"display:table-cell" will generate a tabulation character.
The "position:absolute" property (used with display:block or display:inline, it does not matter) will also cause a line break to be inserted.
Some browsers will also include a single space separation between spans
But Element::textContent will still contain ALL contents of inner text elements independantly of the applied CSS even if they are invisible. And no extra newlines or whitespaces will be generated in textContent, which just ignores all styles and the structure and inline/block or positioned types of inner elements.
A copy/paste operation using mouse selection will discard the hidden text in the plain-text format that is put in the clipboard, so it won't contain everything in the textContent, but only what is within innerText (after whitespace/newline generation as above).
Both properties are then supported in Google Chrome, but their content may then be different. Older browsers still included in innetText everything like what textContent now contains (but their behavior in relation with then generation of whitespaces/newlines was inconsistant).
jQuery will solve these inconsistencies between browsers using the ".text()" method added to the parsed elements it returns via a $() query. Internally, it solves the difficulties by looking into the HTML DOM, working only with the "node" level. So it will return something looking more like the standard textContent.
The caveat is that that this jQuery method will not insert any extra spaces or line breaks that may be visible on screen caused by subelements (like <br />) of the content.
If you design some scripts for accessibility and your stylesheet is parsed for non-aural rendering, such as plugins used to communicate with a Braille reader, this tool should use the textContent if it must include the specific punctuation signs that are added in spans styled with "display:none" and that are typically included in pages (for example for superscripts/subscripts), otherwise the innerText will be very confusive on the Braille reader.
Texts hidden by CSS tricks are now typically ignored by major search engines (that will also parse the CSS of your HTML pages, and will also ignore texts that are not in contrasting colors on the background) using an HTML/CSS parser and the DOM property "innerText" exactly like in modern visual browsers (at least this invisible content will not be indexed so hidden text cannot be used as a trick to force the inclusion of some keywords in the page to check its content) ; but this hidden text will be stil displayed in the result page (if the page was still qualified from the index to be included in results), using the "textContent" property instead of the full HTML to strip the extra styles and scripts.
IF you assign some plain-text in any one of these two properties, this will overwrite the inner markup and styles applied to it (only the assigned element will keep its type, attributes and styles), so both properties will then contain the same content. However, some browsers will now no longer honor the write to innerText, and will only let you overwrite the textContent property (you cannot insert HTML markup when writing to these properties, as HTML special characters will be properly encoded using numeric character references to appear literally, if you then read the innerHTML property after the assignment of innerText or textContent.
myElement.innerText = myElement.textContent = "foo";
Edit (thanks to Mark Amery for the comment below): Only do it this way if you know beyond a reasonable doubt that no code will be relying on checking the existence of these properties, like (for example) jQuery does. But if you are using jQuery, you would probably just use the "text" function and do $('#myElement').text('foo') as some other answers show.
innerText has been added to Firefox and should be available in the FF45 release: https://bugzilla.mozilla.org/show_bug.cgi?id=264412
A draft spec has been written and is expected to be incorporated into the HTML living standard in the future: http://rocallahan.github.io/innerText-spec/, https://github.com/whatwg/html/issues/465
Note that currently the Firefox, Chrome and IE implementations are all incompatible. Going forward, we can probably expect Firefox, Chrome and Edge to converge while old IE remains incompatible.
See also: https://github.com/whatwg/compat/issues/5
This has been my experience with innerText, textContent, innerHTML, and value:
// elem.innerText = changeVal; // works on ie but not on ff or ch
// elem.setAttribute("innerText", changeVal); // works on ie but not ff or ch
// elem.textContent = changeVal; // works on ie but not ff or ch
// elem.setAttribute("textContent", changeVal); // does not work on ie ff or ch
// elem.innerHTML = changeVal; // ie causes error - doesn't work in ff or ch
// elem.setAttribute("innerHTML", changeVal); //ie causes error doesn't work in ff or ch
elem.value = changeVal; // works in ie and ff -- see note 2 on ch
// elem.setAttribute("value", changeVal); // ie works; see note 1 on ff and note 2 on ch
ie = internet explorer, ff = firefox, ch = google chrome.
note 1: ff works until after value is deleted with backspace - see note by Ray Vega above.
note 2: works somewhat in chrome - after update it is unchanged then you click away and click back into the field and the value appears.
The best of the lot is elem.value = changeVal; which I did not comment out above.
As in 2016 from Firefox v45, innerText works on firefox, take a look at its support: http://caniuse.com/#search=innerText
If you want it to work on previous versions of Firefox, you can use textContent, which has better support on Firefox but worse on older IE versions: http://caniuse.com/#search=textContent
What about something like this?
//$elem is the jQuery object passed along.
var $currentText = $elem.context.firstChild.data.toUpperCase();
** I needed to make mine uppercase.
Just reposting from comments under the original post. innerHTML works in all browsers. Thanks stefita.
myElement.innerHTML = "foo";
found this here:
<!--[if lte IE 8]>
<script type="text/javascript">
if (Object.defineProperty && Object.getOwnPropertyDescriptor &&
!Object.getOwnPropertyDescriptor(Element.prototype, "textContent").get)
(function() {
var innerText = Object.getOwnPropertyDescriptor(Element.prototype, "innerText");
Object.defineProperty(Element.prototype, "textContent",
{ // It won't work if you just drop in innerText.get
// and innerText.set or the whole descriptor.
get : function() {
return innerText.get.call(this)
},
set : function(x) {
return innerText.set.call(this, x)
}
}
);
})();
</script>
<![endif]-->
It's also possible to emulate innerText behavior in other browsers:
if (((typeof window.HTMLElement) !== "undefined") && ((typeof HTMLElement.prototype.__defineGetter__) !== "undefined")) {
HTMLElement.prototype.__defineGetter__("innerText", function () {
if (this.textContent) {
return this.textContent;
} else {
var r = this.ownerDocument.createRange();
r.selectNodeContents(this);
return r.toString();
}
});
HTMLElement.prototype.__defineSetter__("innerText", function (str) {
if (this.textContent) {
this.textContent = str;
} else {
this.innerHTML = str.replace(/&/g, '&').replace(/>/g, '>').replace(/</g, '<').replace(/\n/g, "<br />\n");
}
});
}

Making a short alias for document.querySelectorAll

I'm running document.querySelectorAll() frequently, and would like a short alias for it.
var queryAll = document.querySelectorAll
queryAll('body')
TypeError: Illegal invocation
Doesn't work. Whereas:
document.querySelectorAll('body')
Still does. How can I make the alias work?
This seems to work:
const queryAll = document.querySelectorAll.bind(document);
bind returns a new function which works identically to the querySelectorAll function, where the value of this inside the querySelectorAll method is bound to the document object.
The bind function is only supported in IE9+ (and all the other browsers) - https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Function/bind
Update: In fact you could create shortcuts to a whole range of document methods like this:
const query = document.querySelector.bind(document);
const queryAll = document.querySelectorAll.bind(document);
const fromId = document.getElementById.bind(document);
const fromClass = document.getElementsByClassName.bind(document);
const fromTag = document.getElementsByTagName.bind(document);
A common answer is to use $ and $$ for querySelector and querySelectorAll. This alias mimics jQuery's one.
Example:
const $ = document.querySelector.bind(document)
const $$ = document.querySelectorAll.bind(document)
$('div').style.color = 'blue'
$$('div').forEach(div => div.style.background = 'orange')
div {
margin: 2px;
}
<div>
test
</div>
<section>
<div>
hello
</div>
<div>
foo
</div>
</section>
The JavaScript interpreter throws an error because querySelectorAll() should be invoked in document context.
The same error is thrown when you are trying to call console.log() aliased.
So you need to wrap it like this:
function x(selector) {
return document.querySelectorAll(selector);
}
My solution covers the four following use cases:
document.querySelector(...)
document.querySelectorAll(...)
element.querySelector(...)
element.querySelectorAll(...)
The code:
let doc=document,
qsa=(s,o=doc)=>o.querySelectorAll(s),
qs=(s,o=doc)=>o.querySelector(s);
In terms of parameters, the selector s is required, but the container element object o is optional.
Usage:
qs("div"): Queries the whole document for the first div, returns that element
qsa("div"): Queries the whole document for all divs, returns a nodeList of all those elements
qs("div", myContainer): Queries just within the myContainer element for the first div, returns that element
qsa("div", myContainer): Queries just within the myContainer element for all divs, returns a nodeList of all those elements
To make the code slightly shorter (but not quite as efficient), the qs code could be written as follows:
let qs=(s,o=doc)=>qsa(s,o)[0];
The code above uses ES6 features (let, arrow functions and default parameter values). An ES5 equivalent is:
var doc=document,
qsa=function(s,o){return(o||doc).querySelectorAll(s);},
qs=function(s,o){return(o||doc).querySelector(s);};
or the equivalent shorter but less efficient ES5 version of qs:
var qs=function(s,o){return qsa(s,o)[0];};
Below is a working demo. To ensure it works on all browsers, it uses the ES5 version, but if you're going to use this idea, remember that the ES6 version is shorter:
var doc = document;
var qs=function(s,o){return(o||doc).querySelector(s);},
qsa=function(s,o){return(o||doc).querySelectorAll(s);}
var show=function(s){doc.body.appendChild(doc.createElement("p")).innerHTML=s;}
// ____demo____ _____long equivalent______ __check return___ _expect__
// | | | | | | | |
let one = qs("div"); /* doc.querySelector ("#one") */ show(one .id ); // "one"
let oneN = qs("div",one); /* one.querySelector ("div") */ show(oneN .id ); // "oneNested"
let many = qsa("div"); /* doc.querySelectorAll("div") */ show(many .length); // 3
let manyN = qsa("div",one); /* one.querySelectorAll("div") */ show(manyN.length); // 2
<h3>Expecting "one", "oneNested", 3 and 2...</h3>
<div id="one">
<div id="oneNested"></div>
<div></div>
</div>
This would work, you need to invoke the alias using call() or apply() with the appropriate context.
func.call(context, arg1, arg2, ...)
func.apply(context, [args])
var x = document.querySelectorAll;
x.call(document, 'body');
x.apply(document, ['body']);
I took #David Muller's approach and one-lined it using a lambda
let $ = (selector) => document.querySelector(selector);
let $all = (selector) => document.querySelectorAll(selector);
Example:
$('body');
// <body>...</body>
function x(expr)
{
return document.querySelectorAll(expr);
}
If you don't care about supporting ancient, awful browsers that nobody should be using anymore, then you can just do this:
const $ = (sel, parent = document) => parent.querySelector(sel);
const $$ = (sel, parent = document) => Array.from(parent.querySelectorAll(sel));
Here's some examples of usage:
// find specific element by id
console.log($("#someid"));
// find every element by class, within other element
// NOTE: This is a contrived example to demonstrate the parent search feature.
// If you don't already have the parent in a JavaScript variable, you should
// query via $$("#someparent .someclass") for better performance instead.
console.log($$(".someclass", $("#someparent")));
// find every h1 element
console.log($$("h1"));
// find every h1 element, within other element
console.log($$("h1", $("#exampleparent")));
// alternative way of finding every h1 element within other element
console.log($$("#exampleparent h1"));
// example of finding an element and then checking if it contains another element
console.log($("#exampleparent").contains($("#otherelement")));
// example of finding a bunch of elements and then further filtering them by criteria
// that aren't supported by pure CSS, such as their text content
// NOTE: There WAS a ":contains(text)" selector in CSS3 but it was deprecated from the
// spec because it violated the separation between stylesheets and text content, and you
// can't rely on that CSS selector, which is why you should never use it and should
// instead re-implement it yourself like we do in this example.
// ALSO NOTE: This is just a demonstration of .filter(). I don't recommend using
// "textContent" as a filter. If you need to find specific elements, use their way
// more reliable id/class to find them instead of some arbitrary text content search.
console.log($$("#exampleparent h1").filter(el => el.textContent === "Hello World!"));
The functions I provided use a ton of modern JS features:
const to make sure the function variable can't be overwritten.
functions defined as an arrow function aka lambda: (args) => code with implied return statement
default parameters (not supported by browsers before the year 2016)
no {} or return, since those can be skipped if there's just 1 statement in the function body.
The modern function Array.from() is used, which converts the querySelectorAll result (which is always a NodeList, or empty NodeList), into an Array, which is basically what every developer wants. Because that's how you get access to .filter() and other Array functions that allow you to process the discovered nodes further, using clean, short code. And Array.from() creates a shallow copy of all elements which means that it's blazingly fast (it just copies the memory references/pointers to each Node DOM element from the original NodeList). It's a major API enhancer.
If you care about ancient browsers, you can still use my functions but use Babel to convert the modern JS to old ES5 when you release your website.
My suggestion would be to write your entire site in ES6 or higher and then use Babel if you care about visitors from Windows XP and other dead operating systems, or just random people who haven't updated their browsers in 5+ years.
But I wouldn't recommend using Babel. Stop worrying about people who have old browsers. It is their problem, not yours.
The modern "app" world is incredibly deeply based on web browsers and JavaScript and modern CSS, and most of your visitors these days have modern, auto-updated browsers. You basically can't live modern life without a modern browser, since so many websites demand it now.
In my opinion, the days of expecting web designers to waste their time and sanity trying to make a site work on browsers from 1993 are over. It's time the laziest customers/visitors update their old browsers instead. It's not difficult. Even old and dead operating systems usually have ways to install new versions of browsers on them. And people who don't have an updated browser are only a tiny fraction of a percent these days.
For example, the Bootstrap framework, the world's most popular framework for mobile/responsive sites, only cares about supporting the 2 most recent major versions of all major browsers (at least 0.5% market share). Here's their list as of this moment:
0.5% market share or higher
last 2 major versions only
not a dead browser (not discontinued)
Chrome >= 60
Firefox >= 60
Firefox ESR
iOS >= 12
Safari >= 12
not Explorer <= 11 (meaning no versions of Internet Explorer at all)
And I completely agree with this. I was a web developer in the early 2000s and it was absolute hell. Being expected to make it work on some random, stupid user's ancient browser was hell. It took all the fun out of web development. It made me hate and quit web development. Because 90% of my time was wasted on browser compatibility. That's not how life should be. It's not your fault that some customers/visitors are lazy. And these days, visitors have no more excuses to stay lazy.
Instead, you should only target users who have modern browsers. Which is basically everybody these days. There is no excuse for anyone to use an old browser. And if they use an old browser, your site should show a big, fat banner saying "Please, join the modern world for your own sake. Download a new browser. How are you even able to live your normal life with such an old browser? Are you a time traveler from caveman times?".
People have no excuses to have old browsers anymore:
Linux: Ships with the latest versions of Firefox by default.
Mac: Ships with Safari, which is a modern browser. But some older macOS versions won't get newer versions of Safari, and older machines often can't install the latest macOS version either. Well, tough luck for those visitors. They are gonna have trouble on more than just your website. It's up to them to install a modern browser (such as Chrome), which they are able to do even on old versions of macOS. So they have no excuses. Don't waste your life catering to people on very old, buggy Safari versions.
Windows: Windows 10 ships with Edge, which is based on Chromium and is as compatible with websites as Chrome is. People have no excuse to have an old browser. And most Windows users use Chrome. As for very old, discontinued versions of Windows (XP, Vista, 7, 8), well, we yet again arrive at the same question as before: Do you care about 0.0000001% stupid visitors who use a dead OS and an old Internet Explorer version? The whole freaking web will be broken for them anyway, so who cares if your site is broken for them too? They should stop being lazy and just upgrade their OS to Windows 10, or at least install Chrome or Firefox on their current OS. They have no excuses.
iOS: If you're stuck on a super old iOS device, then you can't use the modern web. Tough luck. A lot of the web is gonna be broken for you. Get a new device. Even frameworks like Bootstrap, the world's #1 mobile web framework, doesn't support iOS 11 or earlier. It's not our problem. It's the cheapskate visitor's problem if they still hang onto such an old device. They can literally get a newer iOS device second-hand for almost no money at all and fix all of their problems with visiting the modern web. And they'll need to buy that anyway since most apps (even banking/important apps) require modern iOS versions.
Android: The browser is independently updated from the OS, and can even be sideloaded, so even if you're stuck on old Android versions, you have access to modern browsers. So you have no excuses.
Most people these days have browsers that are completely up-to-date and auto-updated. That's the fact.
So yeah... the days of website designers suffering through hell just for catering to old browsers are over. Therefore I suggest that people use ES6 and CSS3 for their websites, to make web designing a joy for the first time.
Hope you enjoy the ES6 functions I provided!
Here is my take on it. If the selector has multiple matches, return like querySelectorAll. If ony one match is found return like querySelector.
function $(selector) {
let all = document.querySelectorAll(selector);
if(all.length == 1) return all[0];
return all;
}
let one = $('#my-single-element');
let many = $('#multiple-elements li');
2019 update
Today I made a new take on the problem. In this version you can also use a base like this:
let base = document.querySelectorAll('ul');
$$('li'); // All li
$$('li', base); // All li within ul
Functions
function $(selector, base = null) {
base = (base === null) ? document : base;
return base.querySelector(selector);
}
function $$(selector, base = null) {
base = (base === null) ? document : base;
return base.querySelectorAll(selector);
}
The highly invasive version:
<script>
for(const c of [HTMLDocument, Element, DocumentFragment]) {
c.prototype.$ = c.prototype.querySelector;
c.prototype.$$ = c.prototype.querySelectorAll;
}
const $ = document.$.bind(document); // For inline events
const $$ = document.$$.bind(document);
window.$ = $; // For JS files
window.$$ = $$;
</script>
So you can chain $('nav').$$('a') like jQuery allows (I think). Since ShadowRoot extends DocumentFragment you can even inspect (open) Shadow DOMs:
const styles = $('x-button').shadowRoot.$$('style');
function $(selector, base = null) {
base = (base === null) ? document : base;
return base.querySelector(selector);
}
function $$(selector, base = null) {
base = (base === null) ? document : base;
return base.querySelectorAll(selector);
}
Why not simplier ??? :
let $ = (selector, base = document) => {
let elements = base.querySelectorAll(selector);
return (elements.length == 1) ? elements[0] : elements;
}
you dont need to add any dot before class name you can modify this also by using getEle by class or id tag etc
function $(selector){
var dot = ".";
var newSelector = dot.concat(selector);
var Check = document.querySelectorAll(newSelector);
if(Check && Check.length>0 && Check.length < 2){
return document.querySelector(newSelector);
}
else {
return document.querySelectorAll(newSelector);
}
}
$("answercell")[0].remove();

XPath queries in IE use zero-based indexes but the W3C spec is one-based. How should I handle the difference?

The Problem
I am converting a relatively large piece of Javascript that currently only works on Internet Explorer in order to make it work on the other browsers as well. Since the code uses XPath extensively we made a little compatibility function to make things easier
function selectNodes(xmlDoc, xpath){
if('selectNodes' in xmlDoc){
//use IE logic
}else{
//use W3C's document.evaluate
}
}
This is mostly working fine but we just came across the limitation that positions in IE are zero-based but in the W3C model used by the other browsers they are one-based. This means that to get the first element we need to do //books[0] in IE and //books[1] in the other browsers.
My proposed solution
The first thought was using a regex to add one to all indexes that appear in the queries if we are using the document.evaluate version:
function addOne(n){ return 1 + parseInt(nStr, 10); }
xpath = xpath.replace(
/\[\s*(\d+)\s*\]/g,
function(_, nStr){ return '[' + addOne(nStr) + ']'; }
);
My question
Is this regex based solution reasonably safe?
Are there any places it will convert something it should not?
Are there any places where it will not convert something it should?
For example, it would fail to replace the index in //books[position()=1] but since IE doesn't appear to support position() and our code is not using that I think this particular case would not be a problem.
Considerations
I downloaded Sarissa to see if they have a way to solve this but after looking at the source code apparently they don't?
I want to add one to the W3C version instead of subtracting one in the IE version to ease my conversion effort.
In the end
We decided to rewrite the code to use proper XPath in IE too by setting the selection language
xmlDoc.setProperty("SelectionLanguage", "XPath");
we just came across the limitation that positions in IE are zero-based
but in the W3C model used by the other browsers they are one-based.
This means that to get the first element we need to do //books[0] in
IE and //books[1] in the other browsers.
Before doing any XPath selection, specify:
xmlDoc.setProperty("SelectionLanguage", "XPath");
MSXML3 uses a dialect of XSLT/XPath that was in use before XSLT and XPath became W3C Recommendations. The default is "XSLPattern" and this is what you see as behavior.
Read more on this topic here:
http://msdn.microsoft.com/en-us/library/windows/desktop/ms754679(v=vs.85).aspx
Why not modify the original expressions, so that this:
var expr = "books[1]";
...becomes:
var expr = "books[" + index(1) + "]";
...where index is defined as (pseudocode):
function index(i) {
return isIE ? (i - 1) : i;
}

document.evaluate - Cross browser?

I have been looking for a CSS selector function other than Sizzle and I have come across this function.
function SparkEn(xpath,root) {
xpath = xpath
.replace(/((^|\|)\s*)([^/|\s]+)/g,'$2.//$3')
.replace(/\.([\w-]+)(?!([^\]]*]))/g, '[#class="$1" or #class$=" $1" or #class^="$1 " or #class~=" $1 "]')
.replace(/#([\w-]+)/g, '[#id="$1"]')
.replace(/\/\[/g,'/*[');
str = '(#\\w+|"[^"]*"|\'[^\']*\')';
xpath = xpath
.replace(new RegExp(str+'\\s*~=\\s*'+str,'g'), 'contains($1,$2)')
.replace(new RegExp(str+'\\s*\\^=\\s*'+str,'g'), 'starts-with($1,$2)')
.replace(new RegExp(str+'\\s*\\$=\\s*'+str,'g'), 'substring($1,string-length($1)-string-length($2)+1)=$2');
var got = document.evaluate(xpath, root||document, null, 5, null);
var result=[];
while (next = got.iterateNext())
result.push(next);
return result;
}
I just feel like it is too good to be true, is this a firefox only function (xpath?) or is it slow? Basically why would I use Sizzle over this?
I believe no stable version of IE supports document.evaluate, so you're limited to every other browser. It's not slow since it's a native implementation of XPath.
Sizzle is useful because it uses the native support browsers offer when available (such as document.getElementsByClassName), but falls back to doing it itself when unavailable (IE). It's also used by jQuery and Prototype, so it's heavily, heavily tested and is unlikely to give you any trouble. Sizzle is also heavily speed-tested and optimized (they have a whole speed test suite), which is more work you don't have to do.
I'd say go with jQuery, Prototype, or just Sizzle by itself unless you are doing something incredibly performance-sensitive (which, honestly, is probably an indicator that you've structured your application poorly).
I just have found http://sourceforge.net/projects/js-xpath/, which claims to be
an implementation of DOM Level 3 XPath for Internet Explorer 5+
See their implementation at http://nchc.dl.sourceforge.net/project/js-xpath/js-xpath/1.0.0/xpath.js
It is a DOM3 W3C Working Group Note: http://www.w3.org/TR/2004/NOTE-DOM-Level-3-XPath-20040226/xpath.html#XPathEvaluator-evaluate
Implementation status: https://developer.mozilla.org/en-US/docs/Web/API/document.evaluate#Browser_compatibility Today only not in IE 10 on latest stable desktop browsers.

Can you test for browser support for -moz-linear-gradient?

I would like to use feature detection to tell whether the user's version of Firefox supports the CSS style value -moz-linear-gradient. (This was added in Gecko 1.9.2. Version 3.6 of Firefox uses this.)
I can't use document.body.style.mozLinearGradient (or something similar) because -moz-linear-gradient is not a style property but a style value.
Does anyone know how to test for this without using version numbers?
I'm not sure how, but Modernizr (a nice little feature-detection script) appears to do it.
I guess you could create an (offscreen?) element, set that as it's style, and then poke around in the DOM to see if the browser successfully applied it?
Just assign it as style value and check afterwards if it is there.
Kickoff example:
function supportsMozLinearGradient() {
var element = document.getElementsByTagName('script')[0]; // Just grab an "invisible" element.
var oldstyle = element.style.background; // Backup old style.
try {
element.style.background = '-moz-linear-gradient(top, black, white)';
} catch(e) {
// Ignore failures.
}
var supports = element.style.background.indexOf('-moz-linear-gradient') > -1; // Did it accept?
element.style.background = oldstyle; // Restore old style.
return supports;
}
You should check for -moz-background-size (which was introduced in Firefox v3.6). The inference won't be picked up by other browsers since the property is prefixed.
if ('MozBackgroundSize' in document.body.style)
This is how MooTools detects Gecko (Firefox) engine (I'm "paraphrasing" slightly)
gecko = (!document.getBoxObjectFor && window.mozInnerScreenX == null) ? false : ((document.getElementsByClassName) ? 19 : 18)
So if it's FF it'll return 19 or 18, I believe 19 is 3.x and 18 is 2.x
And apparently FF3.6 stopped supporting document.getBoxObjectFor, so to detect 3.6 I basically do
isFF36 = gecko && !document.getBoxObjectFor
Works like a charm from a few tests I did.
If you're not using MooTools you can probably combine the two into one statement that would return something like false or 'ff' or 'f36' but I'm too lazy to work through that logic :)

Categories