/
index.html
1413 lines (1300 loc) · 67.6 KB
/
index.html
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
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<html lang="en">
<head>
<title>Browser Detection (and What to Do Instead)</title>
<meta HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=iso-8859-1">
<link href="../../faq.css" rel="stylesheet" type="text/css">
<link href="../notes.css" rel="stylesheet" type="text/css">
<style type="text/css">
.tip {
border: 1px solid #ccc;
padding: 1ex;
background: #fcfcfc;
font-size: 90%;
}
</style>
</head>
<body>
<h1 id="bdTop">Browser Detection (and What to Do Instead)</h1>
<div id="faqNav">
<a href="../../">FAQ</a> > <a href="../">FAQ Notes</a>
</div>
<p>
By Richard Cornford, edited by Garrett Smith
</p>
<ul>
<li><a href="#bdIntro">Introduction</a></li>
<li><a href="#bdValid">Avoiding Structural Differences in the Browser DOMs</a></li>
<li><a href="#bdDif">Browsers Differences</a></li>
<li><a href="#bdFailS">Failed Strategies: Browser Detecting</a>
<ul>
<li><a href="#bdUAS">Assumptions Based on navigator.userAgent</a></li>
<li><a href="#bdOI">Assumptions Based on DOM Objects: Object inference</a></li>
</ul>
</li>
<li><a href="#bdFD">A Strategy That Works: Object/Feature Detecting.</a>
<ul>
<li><a href="#bdGEID">Example 1: IDed Element Retrieval</a></li>
<li><a href="#bdScroll">Example 2: Scroll Values</a></li>
<li><a href="#bdReplace">Example 3: String.prototype.replace</a></li>
</ul>
</li>
<li><a href="#bdDesPb">The Javascript Design Problem</a></li>
</ul>
<h2 id="bdIntro">Introduction</h2>
<p id="bdIntro_1">
Under normal circumstances computer programs are written for a known
environment. The programmer knows what to expect; which APIs will be
available, how they will behave and so on. Java is an ideal example
of this, providing a theoretically consistent set of APIs and language
features across all Java Virtual Machine (JVM) implementations. But
this is also true in most other circumstances. The programmer of C++
for the Windows operating system will know what MFC classes are
available and how to program them. Their expectations will be
rewarded, if they posses the required knowledge.
</p>
<p id="bdIntro_2">
Client side javascript for the Internet, on the other hand, has the
possibly unique problem of having to be authored with no specific
knowledge of the environment in which it will be executed. The
client side environment will usually be a web browser and web
browsers do tend to have many common features (and increasingly
standardised features) but the author cannot know which version of
which browser will be making any HTTP request (or
whether it is a browser at all). It is not even possible to tell in
advance whether the User Agent will be capable of executing
javascript; all of those that can include a user configurable option
to disable it anyway.
</p>
<p id="bdIntro_3">
Javascript authors for the Internet must realise that they are dealing
with the unknown and that any, and all, scripts will fail to execute
somewhere. The challenge is to get the most from your javascript when
it is available but to cope with their failure in a meaningful way.
</p>
<p id="bdIntro_4">
I once read a description of a variant on the game of chess, played
at military academies. Two players sit at separate boards set up with
only their own pieces, out of sight of each other, and a referee
supervises the game. Each player makes their move in turn and the
referee is responsible for informing them how the other's move impacts
on their own pieces and how the other's disposition of pieces impact
on their intended move. The player is informed only when one of their
own pieces is taken, when one of their moves is affected by
interacting with one of their opponents pieces (i.e. a player may want
to move a bishop across the board but the referee may inform them that
their move was stopped four squares early when the bishop took a pawn
from the other side) and when one of their opponents pieces is
sitting on a square adjacent to one of their own.
</p>
<p id="bdIntro_5">
The game is used to teach battlefield strategy. To win a player must
probe and test to deduce his opponent's disposition, while planing and
executing a response that will achieve the desired checkmate. It is
this sort of strategy that needs to be added to the normal programming
problems in order that javascript may cope with its unknown execution
environment, with the significant difference that the strategy, its
tests and all of the paths of execution must be fully planed out before
the code can even starts executing.
</p>
<h2 id="bdValid">Avoiding Structural Differences in the Browser DOMs</h2>
<p id="bdValid_1">
While the point of this article is to introduce techniques for handling
the differences between web browsers and their DOM implementations it
is also possible to avoid some types of differences especially related
to the structure of the DOM that is being scripted.
</p>
<p id="bdValid_2">
If I was asked to recommend one action most likely to promote the
authoring of cross-browser scripts it would be: <strong><em>Start
from a basis of valid <span class="initialism" title="HyperText Mark-up Language"><abbr title="HyperText Mark-up Language"
>HTML</abbr></span></em></strong>.</p>
<p id="bdValid_3">
Browsers presented with invalid <span class="initialism" title="HyperText Mark-up Language"><abbr title="HyperText Mark-up Language">HTML</abbr></span> will usually attempt to error
correct it in order to do the best possible job of displaying it.
Some browsers, particularly IE, are tolerant of all sorts of strange
formulations of mark-up. Valid <span class="initialism" title="HyperText Mark-up Language"><abbr title="HyperText Mark-up Language">HTML</abbr></span> has a tree-like structure, elements
may completely contain others but they cannot overlap, and there are
rules about which elements may appear in which contexts. The DOM that
is to be scripted also has a tree-like structure and there is a very
simple relationship between the tree-like structure of valid <span class="initialism" title="HyperText Mark-up Language"><abbr title="HyperText Mark-up Language">HTML</abbr></span> and
the DOM constructed from it. So any browser presented with valid <span class="initialism" title="HyperText Mark-up Language"><abbr title="HyperText Mark-up Language">HTML</abbr></span>
will be able to directly translate that <span class="initialism" title="HyperText Mark-up Language"><abbr title="HyperText Mark-up Language">HTML</abbr></span> into a corresponding DOM
using well specified rules, resulting in a DOM that is of predictable
and consistent structure on all of the browsers that can build a DOM.
</p>
<p id="bdValid_4">
Invalid <span class="initialism" title="HyperText Mark-up Language"><abbr title="HyperText Mark-up Language">HTML</abbr></span> will not translate as naturally into a DOM, or even a
tree-like structure. If the browser is going to build a DOM with the
source provided it is going to have to apply error correcting rules
and attempt to build the best DOM it can. But the error correcting
rules are not standardised, not even published. So different browsers
have no choice but apply different rules and that directly results in
the building of DOMs with different (and in extremes, radically
different) structures.
</p>
<p id="bdValid_5">
As a result, using invalid <span class="initialism" title="HyperText Mark-up Language"><abbr title="HyperText Mark-up Language">HTML</abbr></span> directly produces differences in the
DOMs produced by different browsers. No matter how good the application
of techniques for dealing with the differences between browsers, it
does not make sense to do anything that will provoke more differences
than are unavoidable.
</p>
<p id="bdValid_6">
The authoring of invalid <span class="initialism" title="HyperText Mark-up Language"><abbr title="HyperText Mark-up Language">HTML</abbr></span>, justified because "It works in
browser XYZ", gives the authors of accompanying scripts the
impression that cross-browser scripting is harder than it is. If they
had started with valid <span class="initialism" title="HyperText Mark-up Language"><abbr title="HyperText Mark-up Language">HTML</abbr></span> they would never have encountered any of
the structural inconsistencies that invalid <span class="initialism" title="HyperText Mark-up Language"><abbr title="HyperText Mark-up Language">HTML</abbr></span> can provoke.
</p>
<h2 id="bdDif">Browsers Differences</h2>
<p id="bdDif_1">
As browsers have evolved they have offered more features to javascript.
Different manufactures have adopted the features of other browsers,
while adding new features, that may in turn have been adopted by (or
will be adopted by) their competitors. Various sets of common features
have emerged and some have been formalised by the W3C into a sequence
of standard DOM specifications. Along the way an increasing number of
javascript capable browsers have emerged. In addition to desktop PC
browsers, javascript capable browsers exist for a whole range of
devices; PDAs, mobile telephones (Microwave ovens, refrigerators).
</p>
<p id="bdDif_2">
Unfortunately it is the case that the browsers on the smaller devices
cannot offer the range of features available to a desktop PC and even
as the technology improves and features are added to the smaller
browsers the problem will not improve as browsers will become available
on a wider range of devices while the desktop PC browsers will continue
to march ahead of them.
</p>
<p id="bdDif_3">
Over the years various strategies have been attempted to tackle this
problem and some have failed miserably.
</p>
<h2 id="bdFailS">Failed Strategies: Browser Detecting</h2>
<h3 id="bdUAS">Assumptions Based on navigator.userAgent</h3>
<p id="bdUAS_1">
One of the most popular strategies for handling the differences between
web browsers was browser detecting based on the User Agent string.
Browsers possessing a <code>navigator</code> object also provide a
property of that object: <code>navigator.userAgent</code> containing a
string that (in theory) identifies the browser. Its application went
something like:-
</p>
<pre id="bdUAS_ex1">
<span class="commentJS">/* Warning: never use this script, or any script based on, or resembling, it.
*/</span>
var userAgent = self.navigator.userAgent;
var appName = self.navigator.appName;
var isOpera = false;
var isOpera5 = false;
var isOpera6p = false;
var isIE = false;
var isIE4 = false;
var isIE5p = false;
var isMozilla1p = false;
var isNet4 = false;
var isNet5p = false;
var operaVersion = 0;
var ieVersion = 0;
var appVersion = self.navigator.appVersion;
var brSet = false;
function brSetup(){
for(var c = 3;c < appVersion.length;c++){
var chr = appVersion.charAt(c);
if(isNaN(chr)){
appVersion = appVersion.substring(0, c);
break;
}
}
if((userAgent.indexOf('webtv') < 0) &&
(userAgent.indexOf('hotjava') < 0)){
if(userAgent.indexOf('Opera') >= 0){
var ind = (userAgent.indexOf('Opera')+6);
if(((ind+1) < userAgent.length)&&(ind >= 6)){
isOpera = true;
var bsVersion = parseInt(userAgent.substring(ind, ind+1));
if(!isNaN(bsVersion)){
operaVersion = bsVersion;
if(operaVersion >= 6){
isOpera6p = true;
}else if(operaVersion >= 5){
isOpera5 = true;
}
}
}
}else if(appName.indexOf('Microsoft Internet Explorer') >= 0){
var ind = (userAgent.indexOf('MSIE')+5);
if(((ind+1) < userAgent.length)&&(ind >= 5)){
isIE = true;
var bsVersion = parseInt(userAgent.substring(ind, ind+1));
if(!isNaN(bsVersion)){
ieVersion = bsVersion;
if(ieVersion >= 5){
isIE5p = true;
}else if(ieVersion >= 4){
isIE4 = true;
}
}
}
}else if(appName.indexOf('Netscape') >= 0){
if((self.navigator.vendor)&&
(self.navigator.vendor.indexOf('Netscape') >= 0)&&
(userAgent.indexOf('Gecko') >= 0)){
isNet5p = true;
}else if((userAgent.indexOf('Netscape') < 0)&&
(userAgent.indexOf('Gecko') >= 0)&&
(appVersion >= 5)){
isMozilla1p = true;
}else if((appVersion < 5)&&
(userAgent.indexOf('compatible') < 0)){
isNet4 = true;
}
}
}
brSet = true;
}
</pre>
<p id="bdUAS_2">
This version also uses some other properties of the
<code>navigator</code> object; <code>appName</code> and
<code>appVersion</code>.
</p>
<p id="bdUAS_3">
Superficially this type of script seems to be saying quite a lot about
what browser is executing the script. Knowing that the
<code>isIE5p</code> variable is boolean <code>true</code> seems to be
a reasonable indicator that the browser in question is Internet
Explorer Version 5 or above and from that all of the available features
on the IE 5+ DOM could be assumed to exist.
</p>
<p id="bdUAS_4">
Unfortunately, if this type of script ever was an effective determiner
of the browser type, it is not now. The first problem is that you cannot
write this type of script to take into account all web browsers. The
script above is only interested in Internet Explorer, Netscape and
(some) Mozilla derived browsers and Opera. Any other browser will not
be identified, and that will include a number of W3C DOM conforming
fully dynamic visual browsers quite capable of delivering on even quite
demanding code.
</p>
<p id="bdUAS_5">
The second problem is that scripts like this one, and server-side
counter-parts (reading the HTTP User Agent header) were used to
<em>exclude</em> browsers that did not fall into a set of browsers
known to the author, regardless of whether those browsers were
capable of displaying the offending site or not.
</p>
<p id="bdUAS_6">
As more browsers were written, their authors discovered that if they
honestly reported their type and version in their User Agent string
they would likely be excluded from sites that they would
otherwise be quite capable of displaying. To get around this problem
browsers began spoofing the more popular versions, sending HTTP User
Agent headers, and reporting <code>navigator.userAgent</code> strings,
that were indistinguishable from, say, IE.
</p>
<p id="bdUAS_7">
As a result, when the above script reports <code>isIE5p</code> as true, it is
possible that the browser that is executing the script is one of
numerous current browsers. Many of those browsers support sufficient
features found on IE5+ to allow most scripts to execute but the
trueness of <code>isIE5p</code> is not a valid indicator that the
browser will support <em>all</em> of the IE 5+ DOM.
</p>
<p id="bdUAS_8">
Now you might decide that a browser that lies about its identity
deserves what it gets (though they started lying in order to make
themselves usable in the face of near-sighted <span class="initialism" title="HyperText Mark-up Language"><abbr title="HyperText Mark-up Language">HTML</abbr></span> and script authors)
but is worth bearing in mind that the IE 5
<code>navigator.userAgent</code> string is:
<code>"Mozilla/4.0 (compatible; MSIE 5.01; Windows NT)"</code>.
IE 5 is in fact spoofing Netscape 4, and Microsoft started to do that
for precisely the same reasons that motivate many current browsers to
send User Agent headers, and report <code>navigator.userAgent</code>
strings that are indistinguishable form those of Microsoft browsers.
</p>
<p id="bdUAS_9">
No browser manufacture wants (or ever has wanted) their browser to be
needlessly excluded from displaying a web site that it is perfectly
capable of handling just because the author of that site does not
know it by name. And to prevent that they have followed Microsoft and
taken action that has rendered the <code>userAgent</code> string (and
the HTTP User Agent header) meaningless.
</p>
<p id="bdUAS_10">
We are now at a point where the contents of the User Agent strings
bear no relationship at all to the capabilities and features of the
browser that is reporting it. The situation has gone so far that a
number of javascript experts have stated that a standard quality test
for an unknown script would include searching the source code of the
script for the string <code>"userAgent"</code> and dismissing
the script out of hand if that string is found.
</p>
<h3 id="bdOI">Assumptions Based on DOM Objects: Object inference</h3>
<p id="bdOI_1">
A second browser detecting strategy uses the objects present in various
browser DOMs and make the assumption that the presence (or absence) of
one or more objects indicates that a browser is a particular type or
version. I quickly found this example of typical code of this type:-
</p>
<pre id="bdOI_ex1">
<span class="commentJS">/* Warning: never use this script, or any script based on, or resembling, it.
*/</span>
var isDOM=(document.getElementById)?true:false;
var isIE4=(document.all&&!isDOM)?true:false;
var isIE5p=(document.all&&isDOM)?true:false;
var isIE=(document.all)?true:false;
var isOP=(window.opera)?true:false;
var isNS4=(document.layers)?true:false;
</pre>
<p id="bdOI_2">
Javascript performs automatic type conversion so when a boolean result
is expected from an expression that evaluates to a non-boolean value
that non-boolean value is (internally) converted to a boolean value
(using the rules defined in the ECMAScript specification) and that
boolean is used as the result.
</p>
<p id="bdOI_3">
Take the first line:-
</p>
<pre id="bdOI_ex2">
var isDOM=(document.getElementById)?true:false;
</pre>
<p id="bdOI_4">
The conditional expression requires that the expression preceding the <code>?</code>
have a boolean result. The <code>document.getElementById</code>
property accessor can resolve as one of two values depending on whether
the <code>getElementById</code> function is supported in the browser in
question. If it is supported then the accessor resolves as a function
object, and is type converted to boolean <code>true</code>. If
<code>getElementById</code> is not supported the accessor resolves as
undefined, and undefined type converts to boolean
<code>false</code>. Thus the expression preceding the question mark
resolves as <code>true</code> or <code>false</code> and based on that
result <code>true</code> or <code>false</code> are assigned to the
variable <code>isDOM</code>.
</p>
<div class="tip">
<h4>Boolean Conversion Tip: !!</h4>
<p id="bdOI_5">
Incidentally, this code is not the optimum method of assigning a boolean
value based on the type converted to boolean result of a property accessor.
It is better to use the javascript NOT operator ( <code>!</code> ) twice
or to pass the object reference as the argument to the <code>Boolean</code>
constructor called as a function. The not operator will type convert its
operand to boolean and then invert it so <code>false</code> becomes
<code>true</code> and <code>true</code> becomes <code>false</code>.
Passing that result as the operand for a second not operator inverts
the boolean again so a reference to a function object results in boolean
<code>true</code> and an undefined reference results in boolean
<code>false</code>. The <code>Boolean</code> constructor called as a
function converts its argument to boolean and returns that value. The
statement would become:-
</p>
<pre id="bdOI_ex3">
var isDOM = !!document.getElementById;
<span class="commentJS">/* - or - */</span>
var isDOM = Boolean(document.getElementById);
</pre>
<p id="bdOI_6">
Which is shorter and faster than the original version and certainly
more direct.
</p>
</div>
<h4>Inductive Generalization Fallacy</h4>
<p id="bdOI_7">
The problem with this type of browser detecting script is that it is
used to make assumptions about the browser's capabilities that are
rarely valid. For example, this <code>isDOM</code> result, based on
the browser's support for <code>document.getElementById</code>, is
often used as the basis for the assumption that the browser has a
fully dynamic DOM with methods such as
<code>document.createElement</code>, <code>replaceChild</code> and
<code>appendChild</code>. Browsers do not live up to that expectation,
some are not that dynamic and while they may implement some of the Core
DOM level 1 methods such as <code>getElementById</code> They do not
necessarily implement large parts of the various DOM standards,
including all of the dynamic <code>Node</code> manipulation methods.
</p>
<p id="bdOI_8">
The result of the <code>isIE5p</code> test is intended to indicate that
the browser is Internet Explorer 5.0 or above. However, Opera 7,
IceBrowser 5.4, Web Browser 2.0 (palm OS), Konquerer, Safari, NetFront,
iCab and others will all produce a <code>true</code> value in
<code>isIE5p</code> because they implement <code>getElementById</code>
and the <code>document.all</code> collection. As a result, code that
assumes that it will have <em>all</em> of the capabilities of IE 5.0+
available to it when <code>isIE5p</code> is <code>true</code> will as
often as not be mistaken.
</p>
<p id="bdOI_9">
This problem applies to all of the tests above with the possible
exception of the <code>window.opera</code> test. I am unaware of a
second browser type that has implemented an <code>opera</code> object
on the window object. But then Opera 7 is a radically different, and
much more dynamic browser that its preceding versions, though they all
possess a <code>window.opera</code> object.
</p>
<p id="bdOI_10">
To get around the problem that multiple browsers implement the same
features (even if they start off unique to one browser) script authors
have attempted to find more discriminating features to test. For
example, the following script extract is intended to work only on IE
5.0+ browsers:-
</p>
<pre id="bdOI_ex4">
var isIE5p = !!window.ActiveXObject;
...
function copyToClip(myString){
if(!isIE5p) return;
window.clipboardData.setData("text",myString);
}
</pre>
<p id="bdOI_11">
The <code>ActiveXObject</code> constructor is intended to be
discriminating of an IE browser. However, this type if script still
does not work. It has placed the competing browser manufacturers in
exactly the same position as they were in when scripts tested the
<code>navigator.userAgent</code> string and excluded them from
accessing a site because they honestly reported that they where not
IE. As a result I already know of one browser that has implemented
a <code>window.ActiveXObject</code> function, it probably is a dummy
and exists in the browsers DOM specifically to defeat the exclusion
of that browser based on tests like the one above.
</p>
<p id="bdOI_12">
The assumptions that the existence of one (or two) feature(s) in a
javascript environment infers the existence of any feature beyond
the ones tested is invalid. It is only used by those ignorant of the
potential for diversity, imitation and the patterns of evolution in
browser DOMs.
</p>
<p id="bdOI_13">
No matter how specifically the objects from which the inferences are
derived are chosen, the technique itself sows the seeds of its own
invalidity, an object that may actually validly be used to infer that
a browser is of a particular type/version today probably will not still
be valid next year. Adding a maintenance burden to a task that already
presupposes an omniscient knowledge of <em>all</em> browser DOMs just
in order to be effectively implemented at present.
</p>
<h2 id="bdFD">A Strategy That Works: Object/Feature Detecting</h2>
<p id="bdFD_1">
The main point of the previous discussion is to convey the idea that it
is impossible to detect exactly which type of browser (or version of
that browser) a script is being executed on. The use that such scripts
have been put to in the past (to exclude browsers from sites that
they probably could have successfully handled) has motivated the
manufactures of browsers to render browser detecting nonviable
as a strategy for dealing with the variations in browser DOMs.
</p>
<p id="bdFD_2">
Fortunately, not being able to identify a web browser type or version
with more accuracy than could be achieved by generating a random number
and then biasing the result by your favourite (meaningless, because
they too are based on browser detecting and suffer exactly the same
unreliability) browser usage statistics, does not need to impact upon
your ability to script web browsers at all. A viable alternative
strategy has been identified and developed to the point where it is
possible to author javascript to be used on web pages without any
interest in the type or version of the browser at all.
</p>
<p id="bdFD_3">
That alternative strategy is known as object or feature detecting. I
prefer to use the term "feature detecting", partly because the
resulting code often needs to test and probe a wider range of
features than just those that could be described as objects, but
mostly because "object detecting" is occasionally
erroneously applied to the object inference style of script described
above.
</p>
<p id="bdFD_4">
Feature detecting seeks to match an attempt to execute as script (or a
part of a script) with the execution environment by seeking to test
features of that environment where the results of the test have a
direct one-to-one relationship with the features that need to be
supported in the environment for the code to successfully execute. It
is the direct one-to-one relationship in the implemented tests that
avoids the need to identify the specific browser because whatever
browser it is it either will support all of the required features or
it will not. That would mean testing the feature itself (to ensure
that it exists on the browser) and possibly aspects of the behaviour
of that feature.
</p>
<p id="bdFD_5">
Taking the previous example that illustrated how the
<code>ActiveXObject</code> constructor might be used as the basis for
a script that inferred the existence of, and ability to use, the
<code>clipboardData</code> feature implemented on window IE. Rather
than inferring the browser's support for the <code>clipboardData</code>
feature from some other unrelated feature it should be fairly obvious
that the feature that should be tested for prior to attempting to write
to the clipboard <em>is</em> the <code>clipboardData</code> object, and
further, that calling the <code>setData</code> method of that object
should necessitate checking that it too is implemented:-
</p>
<pre id="bdFD_ex1">
function copyToClip(myString){
if((typeof clipboardData != 'undefined')&&
(clipboardData.setData)){
clipboardData.setData("text",myString);
}
}
</pre>
<p id="bdFD_6">
In this way the tests that determine whether the
<code>clipboardData.setData</code> method is called have a direct
one-to-one relationship with the browser's support for the feature. It
is not necessary to be interested in whether the browser is the
expected windows IE that is known to implement the feature, or whether
it is some other browser that has decided to copy IE's implementation
and provide the feature itself. If the feature is there (at least to
the required extent) it is used and if it is not there no attempt is
made to use it.
</p>
<p id="bdFD_7">
The above feature detecting tests are done using two operations. The
first employs the <code>typeof</code> operator, which returns a string
depending on the type of its operand. That string is one of
<code>"undefined"</code>, <code>"object"</code>,
<code>"function"</code>, <code>"boolean"</code>
<code>"string"</code> and <code>"number"</code>
and the test compares the returned string with the string
<code>"undefined"</code>. The <code>clipboardData</code>
object is not used unless typeof does not return
<code>"undefined"</code>.
</p>
<p id="bdFD_8">
The second test is a type-converting test. The logical AND
(<code>&&</code>) operator internally converts its operands to
boolean in order to make its decision about what value it will return.
If <code>clipboardData.setData</code> is undefined it will type-convert
to boolean <code>false</code>, while if it is an object or a function
the result of the conversion will be boolean <code>true</code>.
</p>
<p id="bdFD_9">
However, that function is not a particularly clever application of
feature detecting because, while it avoids the function throwing errors
in an attempt to execute <code>clipboardData.setData</code> on a browser
thatdoes not support it, it will do nothing on a browser that does not
support it. That is a problem when the user has been presented with a
GUI component that gives them the impression that their interaction
will result in something being written to the clipboard but when they
use it nothing happens. And of course nothing was going to happen if
the browser in use did not support javascript or it had been disabled.
</p>
<p id="bdFD_10">
Ensuring that a script will not attempt to use a feature that is not
supported is not sufficient to address the design challenge of crating
scripts for the Internet. Testing the browser for the features that it
does support makes it practical to handle a spectrum of browser DOMs
but the script design task also involves planning how to handle the
range of possibilities. A range that goes from guaranteed failure to
execute at all on browser that do not support javascript, to full
support for all of the required features.
</p>
<p id="bdFD_11">
You can tell when the browser does not support the
<code>clipboardData</code> feature from the script prior to using it
but the user has no way of knowing why a button that promised them
some action has failed to do anything. So in addition to matching the
script to the browser's ability to execute it, it is also necessary to
match the GUI, and the user's resulting expectations, to what the
script is going to be able to deliver.
</p>
<p id="bdFD_12">
Suppose the <code>copyToClip</code> function was called from an
<code>INPUT</code> element of <code>type="button"</code>
and was intended to copy a company e-mail address into the clipboard,
the <span class="initialism" title="HyperText Mark-up Language"><abbr title="HyperText Mark-up Language">HTML</abbr></span> code for the button might look like:-
</p>
<pre id="bdFD_ex2">
<input type="button"
value="copy our contact e-mail address to your clipboard"
onclick="copyToClip('info@example.com')">
</pre>
<p id="bdFD_13">
We know that that <span class="initialism" title="HyperText Mark-up Language"><abbr title="HyperText Mark-up Language">HTML</abbr></span> will do nothing if javascript is
disabled/unavailable and we know that it will do nothing if the browser
does not support the required features, so one option would be to use a
script to write the button <span class="initialism" title="HyperText Mark-up Language"><abbr title="HyperText Mark-up Language">HTML</abbr></span> into the document in the position in
which the button was wanted when the browser provided the facility:-
</p>
<pre id="bdFD_ex3">
<script type="text/javascript">
if((typeof clipboardData != 'undefined')&&
(clipboardData.setData)&&
(document.write)){
document.write('<input type="button"',
'value="copy our contact e-mail address',
' to your clipboard" onclick="',
'copyToClip(\'info@example.com\')">');
}
</script>
</pre>
<p id="bdFD_14">
Now the user will never see the button unless the browser supports the
required features <em>and</em> javascript is enabled. The user never
gets an expectation that the script will not be able to deliver (at
least that is the theory, it is still possible for the user's browser
configuration to prevent scripts from writing to the clipboard, but
the user might be expected to know how their browser is configured and
understand that the button is not in a position to override it).
</p>
<p id="bdFD_15">
If the <code>copyToClip</code> function is only ever called from
buttons that are written only following the required feature detection
then it can be simplified by the removal of the test from its body as
it would be shielded from generating errors on nonsupporting browsers by
the fact that there would be no way for it to be executed.
</p>
<p id="bdFD_16">
The <code>document.write</code> method is not the only way of adding
GUI components to a web page in a way that can be subject to the
verification of the features that render those components meaningful.
Alternatives include writing to a parent element's
<code>innerHTML</code> property (where supported, see
<a href="/faq/#updateContent">FAQ: How do I modify the content of the current page?</a>), or using the W3C DOM
<code>document.crateElement</code> (or <code>createElementNS</code>)
methods and appending the created element at a suitable location within
the DOM. Either of these two approaches are suited to adding the
components after the page has finished loading, but that can be useful
as some feature testing is not practical before that point. The
approach used can be chosen based on the requirements of the script.
If the script is going to be using the
<code>document.createElement</code> method itself then it is a good
candidate as a method for inserting any required GUI components,
similarly with <code>innerHTML</code>. The <code>document.write</code>
method is universally supported in <span class="initialism" title="HyperText Mark-up Language"><abbr title="HyperText Mark-up Language">HTML</abbr></span> DOMs but is not necessarily
available at all in XHTML DOMs.
</p>
<p id="bdFD_17">
Other ways of handling the possibility that the browser will not
support either javascript or the features required by the script used
is to design the <span class="initialism" title="HyperText Mark-up Language"><abbr title="HyperText Mark-up Language">HTML</abbr></span>/<span class="initialism" title="Cascading Style Sheet"><abbr title="Cascading Style Sheet">CSS</abbr></span> parts of the page so that the script, upon
verifying support for the features it requires, can modify, manipulate
and transform the resulting elements in the DOM. But in the absence of
sufficient script support the unmodified <span class="initialism" title="HyperText Mark-up Language"><abbr title="HyperText Mark-up Language">HTML</abbr></span> presents all of the
required content, navigation, etc.
</p>
<p id="bdFD_18">
This can be particularly significant with things like navigation menus.
One style of design would place the content of the navigation menus,
the URLs and text, in javascript structures such as Arrays. But either
of javascript being disabled/unavailable on the client or the absence
of the features required to support a functional javascript menu would
leave the page without any navigation at all. Generally that would not
be a viable web page, and not that good for search engine placement as
search engine robots do not tend to execute javascript either so they
would be left unable to navigate a site featuring such a menu and so
unable to rank its content for listing.
</p>
<p id="bdFD_19">
A better approach to menu design would have the navigation menu content
defined in the <span class="initialism" title="HyperText Mark-up Language"><abbr title="HyperText Mark-up Language">HTML</abbr></span>, possibly as nested list elements of some sort, and
once the script has ascertained that the browser is capable of executing
it and providing the menu in an interactive form it can modify the <span class="initialism" title="Cascading Style Sheet"><abbr title="Cascading Style Sheet">CSS</abbr></span>
<code>position</code>, <code>display</code> and <code>visibility</code>
properties, move the elements to their desired location, attach event
handlers and generally get on with the task of providing a javascript
menu. And the worst that happens when the browser does not support
scripting or the required features is that the user (and any search
engine robots) finds the navigation in the page as a series of nested
lists containing links. Fully functional, if not quite as impressive as
it could have been had the script been supported. This is termed
"clean degradation" and goes hand in hand with feature
detecting during the process of planing and implementing a browser
script for the Internet.
</p>
<h3 id="bdGEID">Example 1: IDed Element Retrieval</h3>
<p id="bdGEID_1">
An important aspect of feature detecting is that it allows a script to
take advantage of possible fall-back options. Having deduced that a
browser lacks the preferred feature it still may be possible to
achieve the desired goal by using an alternative feature that is know
to exist on some browsers. A common example of this is retrieving an
element reference from the DOM given a string representing the
<code>ID</code> attribute of that element. The preferred method would
be the W3C Core DOM standard <code>document.getElementById</code>
method which is supported on the widest number of browsers. If that
method was not available but the browser happened to support the
<code>document.all</code> collection then it could be used for the
element retrieval as a fall-back option. And for some types of
elements, such as <span class="initialism" title="Cascading Style Sheet"><abbr title="Cascading Style Sheet">CSS</abbr></span> positioned elements on Netscape 4 (where the
<code>document.layers</code> collection may be used to retrieve such
a reference), additional options may be available.
</p>
<pre id="bdGEID_ex1">
function getElementWithId(id){
var obj;
if(document.getElementById){
<span class="commentJS">/* Prefer the widely supported W3C DOM method, if
available:-
*/</span>
obj = document.getElementById(id);
}else if(document.all){
<span class="commentJS">/* Branch to use document.all on document.all only
browsers. Requires that IDs are unique to the page
and do not coincide with NAME attributes on other
elements:-
*/</span>
obj = document.all[id];
}else if(document.layers){
<span class="commentJS">/* Branch to use document.layers, but that will only work for
<span class="initialism" title="Cascading Style Sheet"><abbr title="Cascading Style Sheet">CSS</abbr></span> positioned elements and LAYERs that are not nested. A
recursive method might be used instead to find positioned
elements within positioned elements but most DOM nodes on
document.layers browsers cannot be referenced at all.
*/</span>
obj = document.layers[id];
}
<span class="commentJS">/* If no appropriate/functional element retrieval mechanism
exists on this browser this function returns null:-
*/</span>
return obj||null;
}
</pre>
<p id="bdGEID_2">
Although that function is not very long or complex (without its
comments) it does demonstrate a consequence of one style of
implementation of feature detecting, it repeats the test each time
it is necessary to retrieve an element using its ID. If not too many
elements need retrieving that may not be significant, but if many
elements needed retrieving in rapid succession and performance was
significant then the overhead of performing the feature detection on
each retrieval may add up and impact on the resulting
script.
</p>
<p id="bdGEID_3">
An alternative is to assign one of many functions to a global
<code>getElementWithId</code> variable based on the results of the
feature detecting tests, as the script loads.
</p>
<pre id="bdGEID_ex2">
var getElementWithId;
if(document.getElementById){
<span class="commentJS">/* Prefer the widely supported W3C DOM method, if
available:-
*/</span>
getElementWithId = function(id){
return document.getElementById(id);
}
}else if(document.all){
<span class="commentJS">/* Branch to use document.all on document.all only
browsers. Requires that IDs are unique to the page
and do not coincide with NAME attributes on other
elements:-
*/</span>
getElementWithId = function(id){
return document.all[id];
}
}else if(document.layers){
<span class="commentJS">/* Branch to use document.layers, but that will only work for <span class="initialism" title="Cascading Style Sheet"><abbr title="Cascading Style Sheet">CSS</abbr></span>
positioned elements. This function uses a recursive method
to find positioned elements within positioned elements but most
DOM nodes on document.layers browsers cannot be referenced at
all. This function is expected to be called with only one
argument, exactly like the over versions.
*/</span>
getElementWithId = function(id, baseLayers){
<span class="commentJS">/* If the - baseLayers - parameter is not provided default
its value to the document.layers collection of the main
document:
*/</span>
baseLayers = baseLayers||document.layers;
<span class="commentJS">/* Assign the value of the property of the - baseLayers -
object (possibly defaulted to the document.layers
collection) with the property name corresponding to the
- id - parameter to the local variable - obj:
*/</span>
var obj = baseLayers[id];
<span class="commentJS">/* If - obj - remains undefined (no element existed with the
given - id -) try searching the indexed members of
- baseLayers - to see if any of their layers collections
contain the element with the corresponding - id:
*/</span>
if(!obj){ <span class="commentJS">//Element not found</span>
<span class="commentJS">/* Loop through the indexed members of - baseLayers: */</span>
for(var c = 0;c < baseLayers.length;c++){
if((baseLayers[c])&& <span class="commentJS">//Object at index - c.</span>
(baseLayers[c].document)&& <span class="commentJS">//It has a - document.</span>
<span class="commentJS">/* And a layers collection on that document: */</span>
(baseLayers[c].document.layers)){
<span class="commentJS">/* Recursively call this function passing the - id - as
the first parameter and the layers collection from
within the document found on the layer at index - c
- in - baseLayers - as the second parameter and
assign the result to the local variable - obj:
*/</span>
obj=getElementWithId(id,baseLayers[c].document.layers);
<span class="commentJS">/* If - obj - is now not null then we have found the
required element and can break out of the - for -
loop:
*/</span>
if(obj)break;
}
}
}
<span class="commentJS">/* If - obj - will type-convert to boolean true (it is not null
or undefined) return it, else return null:
*/</span>
return obj||null;
}
}else{
<span class="commentJS">/* No appropriate element retrieval mechanism exists on
this browser. So assign this function as a safe dummy.
Values returned form calls to getElementWithId probably
should be tested to ensure that they are non-null prior
to use anyway so this branch always returns null:-
*/</span>
getElementWithId = function(id){
return null;
}
}
<span class="commentJS">/*
Usage:-
var elRef = getElementWithId("ID_string");
if(elRef){
... //element was found
}else{
... //element could not be found.
}
*/</span>
</pre>
<p id="bdGEID_4">
The feature detecting tests are performed once as the page loads and
one of many function expressions assigned to the
<code>getElementWithId</code> global variable as a result. The
assigned function is the one most capable of retrieving the required
element on the browser in use but it is still necessary to check that
the returned value in not <code>null</code> and plan for the
possibility of failure in the element retrieval process. It is
guaranteed to fail on any browser that does not implement at least one
of the element retrieval mechanisms used as the default function just
returns <code>null</code>.
</p>
<p id="bdGEID_5">
It may not be sufficient to provide a function that does the best job
of element retrieval that can be done on the browser in use. Other
scripts, or parts of the script, may need to know how successful their
attempts to retrieve elements are likely to be. The
<code>getElementWithId</code> version that uses
<code>document.layers</code> cannot retrieve elements that are not <span class="initialism" title="Cascading Style Sheet"><abbr title="Cascading Style Sheet">CSS</abbr></span>
positioned and that may not be good enough for some scripts, while it
may not matter to others. The <code>document.getElementById</code> and
<code>document.all</code> versions should be able to retrieve any
element given its <code>ID</code>. An opting would be to set a couple
of boolean flags to indicate how successful element retrieval can be
expected to be. Defaulting the flags to <code>false</code> and setting
them to <code>true</code> in the branches that assign the various
function expressions. Then if other code is interested in what can be
expected from the <code>getElementWithId</code> function, say in order
to decide how to configure, or default itself, it can check the
pertinent flag.
</p>
<pre id="bdGEID_ex3">
var getElementWithId;
var canGetAnyElement = false; <span class="commentJS">//default any element flag</span>
var canGetCSSPositionedElements = false; <span class="commentJS">//default <span class="initialism" title="Cascading Style Sheet"><abbr title="Cascading Style Sheet">CSS</abbr></span> positioned flag</span>
if(document.getElementById){
canGetAnyElement = true; <span class="commentJS">//set any element flag to true</span>
canGetCSSPositionedElements = true; <span class="commentJS">//set <span class="initialism" title="Cascading Style Sheet"><abbr title="Cascading Style Sheet">CSS</abbr></span> positioned flag true</span>
getElementWithId = ...
}else if(document.all){
canGetAnyElement = true; <span class="commentJS">//set any element flag to true</span>
canGetCSSPositionedElements = true; <span class="commentJS">//set <span class="initialism" title="Cascading Style Sheet"><abbr title="Cascading Style Sheet">CSS</abbr></span> positioned flag true</span>
getElementWithId = ...
}else if(document.layers){
canGetCSSPositionedElements = true; <span class="commentJS">//set <span class="initialism" title="Cascading Style Sheet"><abbr title="Cascading Style Sheet">CSS</abbr></span> positioned flag true</span>
<span class="commentJS">/* The - canGetAnyElement - flag is not set in this branch because
the document.layers collection does not make *all* elements