Avoiding general problems

Written on 2005-06-17 00:04 by tarquin. Last modified 2005-09-19 09:19

Use the DOM

Opera is an XHTML capable browser, and tells web pages that it is capable of viewing XHTML pages that are correctly served as application/xhtml+xml (something that Internet Explorer cannot do). As a result, some web pages serve XHTML to Opera using the correct content type. The XHTML standard is a little different to the HTML standard, and causes some other changes to the way scripts can interract with the page. If your script is going to run on these pages, it should be able to cope with these differences, and the DOM is the way to cope with them.

DOM methods should be used in place of innerHTML, no matter how laborious that may seem. If needed, you can use DOMParser to replicate the innerHTML behaviour. Additionally, when creating elements, you should create them with lower case names, reference them using getElementsByTagName with lower case names, and note that the id and class attributes are case sensitive if the document is served as XHTML.

The DOMParser expects to be passed valid a XML or XHTML fragment, and you will need to ensure that it has a root node. This is an example of how you could use it to write some arbitrary HTML into the document (just like with innerHTML):

<span class="js-comment">//this is not allowed (no root node)
//var foo = 'some <b>text</b> string';

//this is also not allowed (invalid XHTML br tag)
//var foo = '<p>some<br>text</p>';

//this is allowed (valid XHTML)</span>
var foo = '<p>some<br />text</p>';

<span class="js-comment">//this is also allowed (valid XML)
//var foo = '<op>some<nurl />text</op>';

//create a DOMParser object</span>
var bar = new DOMParser();

try {

  <span class="js-comment">//convert the string into a DOM tree (treated as a separate document)</span>
  var baz = bar.parseFromString(foo,'application/xhtml+xml');

  <span class="js-comment">//import the tree into the current document</span>
  baz = document.importNode(baz.documentElement,true);

  <span class="js-comment">//add it to the end of the page</span>

} catch(e) {
  alert('String does not appear to be a valid X(HT)ML fragment');

Note that in Opera, innerHTML is a little faster than the equivalent DOM methods, but this is one case where it is better to use the slightly slower technique. Opera should still be easily fast enough for this not to cause problems, especially if you manage to keep the number of reflows to a minimum.

Use Opera's methods

The simplicity of Greasemonkey syntax may be enticing (where the script always runs onload without needing to add an onload event listener), but there are some significant advantages to Opera's normal User JavaScript syntax that makes it much more appropriate for user scripts. With Opera's User JavaScript syntax, the script is run before any scripts on the page have run, and you use event listeners to detect when the page is about to load scripts, run event handlers, or complete loading. If the intention of your script is to patch existing scripts, then you should almost always use opera.defineMagicFunction/Variable or use opera.addEventListener to add a handler that fixes the scripts, and this cannot be done if you use Greasemonkey syntax.

Even if your script does not need to use that syntax yet, you might want to add an enhancement later that does require the Opera syntax. If you start off with Greasemonkey syntax, then when you make the changes, you would need to change the file name of the script to tell Opera that it is not a Greasemonkey script any more. This would then mean having to tell all users of the script to delete the original file, and add the new one, making upgrading harder for all users of your script.

If you start off by using the Opera syntax, you can still run code when the page loads, you can also use all the other great features of Opera's User JavaScript, and making additions later will not require users of your script to do any extra unnecessary work to upgrade. Adding an onload event listener requires very little code (almost nothing considering that Greasemonkey syntax will almost always use an anonymous function anyway). So in general, it is best to use Opera syntax to start with, to save yourself having to change it later.

Prevent possible errors

With so many pages that your script can interact with, it is entirely possible that your script will try to run on a page that it cannot cope with. To ensure that this does not cause problems, your code should check wherever necessary to see if it has actually managed to reference the objects it thinks it has referenced. Do not assume that you will know the exact structure the page will be using just because you know the path to the file. Remember that Opera renders solitary images and text files using a simple HTML structure, and it will also loads scripts on these pages.

var foo = document.getElementsByTagName('h3')[3];
if( foo ) {
  foo.appendChild(document.createTextNode(' 4'));

User JavaScript will also be run within frames and iframes, and is restricted by the same security model as any page scripts. It will not be allowed to reference a frame from another domain. Your script may need to attempt to reference objects in other frames, but will throw security errors if they are from another domain.

If there is the possibility that your script could produce errors, put a try...catch structure around it:

try {
  if( ( self != top ) && ( top.changeData ) ) {
} catch(e) {
  <span class="js-comment">//oops - not allowed</span>

Never rely on sniffing

User JavaScripts on this site are primarily intended for Opera, but a few may work in other browsers, such as Firefox with Greasemonkey, and Internet Explorer with Turnabout. Even if your script is designed only for Opera, there may be cases where you can only run certain parts of it in certain versions of Opera.

Whatever the case, you should never rely on browser or version sniffing. Opera can spoof on several sites, and with ua.ini, it can mask its userAgent identity completely. User JavaScript can also be used to completely remove all other traces of its identity. Any sniffer script would fail under these circumstances. If you need to use certain functionailty in certain versions, check for that functionality:

var XMLReq = new XMLHttpRequest();
if( XMLReq.setRequestHeader ) {

You can also check for entire feature sets using document.implementation.hasFeature, and fall back to other techniques if the feature is not available. For example, you can check for XPath support using if(document.evaluate), but you could also use hasFeature:

if( document.implementation.hasFeature('XPath','3.0') ) {
  ...use XPath...
} else {
  ...use regular DOM methods...

In general, it is better to use checks for the individual methods that you intend to use, as browsers may claim to support a feature, but may not support all the properties and methods of that feature.

If done properly like this, your script should keep working, even if Opera chooses to unexpectedly change its name or version number, or if additional capabilities are added, or faulty behaviours are fixed. Additionally, it would also work in other browsers if they implement the same functionality.

Never rely on bugs

It is a simple fact, all browsers have bugs. Your scripts may need to avoid certain bugs, or may be intended to patch bugs. However, your scripts should never rely on bugs to function correctly (unless their intention is to patch that specific bug).

You should definitely not use one bug to apply a fix for another (and that includes use of CSS hacks). You should also try to limit your use of proprietary extensions, especially those copied from other browsers.

If you are unsure if what you are doing is relying on a bug, read the specifications. The most relevant specifications are the DOM 2, ECMAScript 3, Web Applications 1.0 (including XMLHttpRequest), CSS 2.1, HTML 4.01 and XHTML 1.0. Opera Software ASA also has a list of supported specifications in Opera.