Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/webroot/rsrc/externals/javelin/lib/DOM.js
12242 views
1
/**
2
* @requires javelin-magical-init
3
* javelin-install
4
* javelin-util
5
* javelin-vector
6
* javelin-stratcom
7
* @provides javelin-dom
8
*
9
* @javelin-installs JX.$
10
* @javelin-installs JX.$N
11
* @javelin-installs JX.$H
12
*
13
* @javelin
14
*/
15
16
17
/**
18
* Select an element by its "id" attribute, like ##document.getElementById()##.
19
* For example:
20
*
21
* var node = JX.$('some_id');
22
*
23
* This will select the node with the specified "id" attribute:
24
*
25
* LANG=HTML
26
* <div id="some_id">...</div>
27
*
28
* If the specified node does not exist, @{JX.$()} will throw an exception.
29
*
30
* For other ways to select nodes from the document, see @{JX.DOM.scry()} and
31
* @{JX.DOM.find()}.
32
*
33
* @param string "id" attribute to select from the document.
34
* @return Node Node with the specified "id" attribute.
35
*/
36
JX.$ = function(id) {
37
38
if (__DEV__) {
39
if (!id) {
40
JX.$E('Empty ID passed to JX.$()!');
41
}
42
}
43
44
var node = document.getElementById(id);
45
if (!node || (node.id != id)) {
46
if (__DEV__) {
47
if (node && (node.id != id)) {
48
JX.$E(
49
'JX.$(\''+id+'\'): '+
50
'document.getElementById() returned an element without the '+
51
'correct ID. This usually means that the element you are trying '+
52
'to select is being masked by a form with the same value in its '+
53
'"name" attribute.');
54
}
55
}
56
JX.$E('JX.$(\'' + id + '\') call matched no nodes.');
57
}
58
59
return node;
60
};
61
62
/**
63
* Upcast a string into an HTML object so it is treated as markup instead of
64
* plain text. See @{JX.$N} for discussion of Javelin's security model. Every
65
* time you call this function you potentially open up a security hole. Avoid
66
* its use wherever possible.
67
*
68
* This class intentionally supports only a subset of HTML because many browsers
69
* named "Internet Explorer" have awkward restrictions around what they'll
70
* accept for conversion to document fragments. Alter your datasource to emit
71
* valid HTML within this subset if you run into an unsupported edge case. All
72
* the edge cases are crazy and you should always be reasonably able to emit
73
* a cohesive tag instead of an unappendable fragment.
74
*
75
* You may use @{JX.$H} as a shortcut for creating new JX.HTML instances:
76
*
77
* JX.$N('div', {}, some_html_blob); // Treat as string (safe)
78
* JX.$N('div', {}, JX.$H(some_html_blob)); // Treat as HTML (unsafe!)
79
*
80
* @task build String into HTML
81
* @task nodes HTML into Nodes
82
*/
83
JX.install('HTML', {
84
85
construct : function(str) {
86
if (str instanceof JX.HTML) {
87
this._content = str._content;
88
return;
89
}
90
91
if (__DEV__) {
92
if ((typeof str !== 'string') && (!str || !str.match)) {
93
JX.$E(
94
'new JX.HTML(<empty?>): ' +
95
'call initializes an HTML object with an empty value.');
96
}
97
98
var tags = ['legend', 'thead', 'tbody', 'tfoot', 'column', 'colgroup',
99
'caption', 'tr', 'th', 'td', 'option'];
100
var evil_stuff = new RegExp('^\\s*<(' + tags.join('|') + ')\\b', 'i');
101
var match = str.match(evil_stuff);
102
if (match) {
103
JX.$E(
104
'new JX.HTML("<' + match[1] + '>..."): ' +
105
'call initializes an HTML object with an invalid partial fragment ' +
106
'and can not be converted into DOM nodes. The enclosing tag of an ' +
107
'HTML content string must be appendable to a document fragment. ' +
108
'For example, <table> is allowed but <tr> or <tfoot> are not.');
109
}
110
111
var really_evil = /<script\b/;
112
if (str.match(really_evil)) {
113
JX.$E(
114
'new JX.HTML("...<script>..."): ' +
115
'call initializes an HTML object with an embedded script tag! ' +
116
'Are you crazy?! Do NOT do this!!!');
117
}
118
119
var wont_work = /<object\b/;
120
if (str.match(wont_work)) {
121
JX.$E(
122
'new JX.HTML("...<object>..."): ' +
123
'call initializes an HTML object with an embedded <object> tag. IE ' +
124
'will not do the right thing with this.');
125
}
126
127
// TODO: May need to deny <option> more broadly, see
128
// http://support.microsoft.com/kb/829907 and the whole mess in the
129
// heavy stack. But I seem to have gotten away without cloning into the
130
// documentFragment below, so this may be a nonissue.
131
}
132
133
this._content = str;
134
},
135
136
members : {
137
_content : null,
138
/**
139
* Convert the raw HTML string into a DOM node tree.
140
*
141
* @task nodes
142
* @return DocumentFragment A document fragment which contains the nodes
143
* corresponding to the HTML string you provided.
144
*/
145
getFragment : function() {
146
var wrapper = JX.$N('div');
147
wrapper.innerHTML = this._content;
148
var fragment = document.createDocumentFragment();
149
while (wrapper.firstChild) {
150
// TODO: Do we need to do a bunch of cloning junk here?
151
// See heavy stack. I'm disconnecting the nodes instead; this seems
152
// to work but maybe my test case just isn't extensive enough.
153
fragment.appendChild(wrapper.removeChild(wrapper.firstChild));
154
}
155
return fragment;
156
},
157
158
/**
159
* Convert the raw HTML string into a single DOM node. This only works
160
* if the element has a single top-level element. Otherwise, use
161
* @{method:getFragment} to get a document fragment instead.
162
*
163
* @return Node Single node represented by the object.
164
* @task nodes
165
*/
166
getNode : function() {
167
var fragment = this.getFragment();
168
if (__DEV__) {
169
if (fragment.childNodes.length < 1) {
170
JX.$E('JX.HTML.getNode(): Markup has no root node!');
171
}
172
if (fragment.childNodes.length > 1) {
173
JX.$E('JX.HTML.getNode(): Markup has more than one root node!');
174
}
175
}
176
return fragment.firstChild;
177
}
178
179
}
180
});
181
182
183
/**
184
* Build a new HTML object from a trustworthy string. JX.$H is a shortcut for
185
* creating new JX.HTML instances.
186
*
187
* @task build
188
* @param string A string which you want to be treated as HTML, because you
189
* know it is from a trusted source and any data in it has been
190
* properly escaped.
191
* @return JX.HTML HTML object, suitable for use with @{JX.$N}.
192
*/
193
JX.$H = function(str) {
194
return new JX.HTML(str);
195
};
196
197
198
/**
199
* Create a new DOM node with attributes and content.
200
*
201
* var link = JX.$N('a');
202
*
203
* This creates a new, empty anchor tag without any attributes. The equivalent
204
* markup would be:
205
*
206
* LANG=HTML
207
* <a />
208
*
209
* You can also specify attributes by passing a dictionary:
210
*
211
* JX.$N('a', {name: 'anchor'});
212
*
213
* This is equivalent to:
214
*
215
* LANG=HTML
216
* <a name="anchor" />
217
*
218
* Additionally, you can specify content:
219
*
220
* JX.$N(
221
* 'a',
222
* {href: 'http://www.javelinjs.com'},
223
* 'Visit the Javelin Homepage');
224
*
225
* This is equivalent to:
226
*
227
* LANG=HTML
228
* <a href="http://www.javelinjs.com">Visit the Javelin Homepage</a>
229
*
230
* If you only want to specify content, you can omit the attribute parameter.
231
* That is, these calls are equivalent:
232
*
233
* JX.$N('div', {}, 'Lorem ipsum...'); // No attributes.
234
* JX.$N('div', 'Lorem ipsum...') // Same as above.
235
*
236
* Both are equivalent to:
237
*
238
* LANG=HTML
239
* <div>Lorem ipsum...</div>
240
*
241
* Note that the content is treated as plain text, not HTML. This means it is
242
* safe to use untrusted strings:
243
*
244
* JX.$N('div', '<script src="evil.com" />');
245
*
246
* This is equivalent to:
247
*
248
* LANG=HTML
249
* <div>&lt;script src="evil.com" /&gt;</div>
250
*
251
* That is, the content will be properly escaped and will not create a
252
* vulnerability. If you want to set HTML content, you can use @{JX.HTML}:
253
*
254
* JX.$N('div', JX.$H(some_html));
255
*
256
* **This is potentially unsafe**, so make sure you understand what you're
257
* doing. You should usually avoid passing HTML around in string form. See
258
* @{JX.HTML} for discussion.
259
*
260
* You can create new nodes with a Javelin sigil (and, optionally, metadata) by
261
* providing "sigil" and "meta" keys in the attribute dictionary.
262
*
263
* @param string Tag name, like 'a' or 'div'.
264
* @param dict|string|@{JX.HTML}? Property dictionary, or content if you don't
265
* want to specify any properties.
266
* @param string|@{JX.HTML}? Content string (interpreted as plain text)
267
* or @{JX.HTML} object (interpreted as HTML,
268
* which may be dangerous).
269
* @return Node New node with whatever attributes and
270
* content were specified.
271
*/
272
JX.$N = function(tag, attr, content) {
273
if (typeof content == 'undefined' &&
274
(typeof attr != 'object' || attr instanceof JX.HTML)) {
275
content = attr;
276
attr = {};
277
}
278
279
if (__DEV__) {
280
if (tag.toLowerCase() != tag) {
281
JX.$E(
282
'$N("'+tag+'", ...): '+
283
'tag name must be in lower case; '+
284
'use "'+tag.toLowerCase()+'", not "'+tag+'".');
285
}
286
}
287
288
var node = document.createElement(tag);
289
290
if (attr.style) {
291
JX.copy(node.style, attr.style);
292
delete attr.style;
293
}
294
295
if (attr.sigil) {
296
JX.Stratcom.addSigil(node, attr.sigil);
297
delete attr.sigil;
298
}
299
300
if (attr.meta) {
301
JX.Stratcom.addData(node, attr.meta);
302
delete attr.meta;
303
}
304
305
if (__DEV__) {
306
if (('metadata' in attr) || ('data' in attr)) {
307
JX.$E(
308
'$N(' + tag + ', ...): ' +
309
'use the key "meta" to specify metadata, not "data" or "metadata".');
310
}
311
}
312
313
for (var k in attr) {
314
if (attr[k] === null) {
315
continue;
316
}
317
node[k] = attr[k];
318
}
319
320
if (content) {
321
JX.DOM.setContent(node, content);
322
}
323
return node;
324
};
325
326
327
/**
328
* Query and update the DOM. Everything here is static, this is essentially
329
* a collection of common utility functions.
330
*
331
* @task stratcom Attaching Event Listeners
332
* @task content Changing DOM Content
333
* @task nodes Updating Nodes
334
* @task serialize Serializing Forms
335
* @task test Testing DOM Properties
336
* @task convenience Convenience Methods
337
* @task query Finding Nodes in the DOM
338
* @task view Changing View State
339
*/
340
JX.install('DOM', {
341
statics : {
342
_autoid : 0,
343
_uniqid : 0,
344
_metrics : {},
345
_frameNode: null,
346
_contentNode: null,
347
348
349
/* -( Changing DOM Content )----------------------------------------------- */
350
351
352
/**
353
* Set the content of some node. This uses the same content semantics as
354
* other Javelin content methods, see @{function:JX.$N} for a detailed
355
* explanation. Previous content will be replaced: you can also
356
* @{method:prependContent} or @{method:appendContent}.
357
*
358
* @param Node Node to set content of.
359
* @param mixed Content to set.
360
* @return void
361
* @task content
362
*/
363
setContent : function(node, content) {
364
if (__DEV__) {
365
if (!JX.DOM.isNode(node)) {
366
JX.$E(
367
'JX.DOM.setContent(<yuck>, ...): '+
368
'first argument must be a DOM node.');
369
}
370
}
371
372
while (node.firstChild) {
373
JX.DOM.remove(node.firstChild);
374
}
375
JX.DOM.appendContent(node, content);
376
},
377
378
379
/**
380
* Prepend content to some node. This method uses the same content semantics
381
* as other Javelin methods, see @{function:JX.$N} for an explanation. You
382
* can also @{method:setContent} or @{method:appendContent}.
383
*
384
* @param Node Node to prepend content to.
385
* @param mixed Content to prepend.
386
* @return void
387
* @task content
388
*/
389
prependContent : function(node, content) {
390
if (__DEV__) {
391
if (!JX.DOM.isNode(node)) {
392
JX.$E(
393
'JX.DOM.prependContent(<junk>, ...): '+
394
'first argument must be a DOM node.');
395
}
396
}
397
398
this._insertContent(node, content, this._mechanismPrepend, true);
399
},
400
401
402
/**
403
* Append content to some node. This method uses the same content semantics
404
* as other Javelin methods, see @{function:JX.$N} for an explanation. You
405
* can also @{method:setContent} or @{method:prependContent}.
406
*
407
* @param Node Node to append the content of.
408
* @param mixed Content to append.
409
* @return void
410
* @task content
411
*/
412
appendContent : function(node, content) {
413
if (__DEV__) {
414
if (!JX.DOM.isNode(node)) {
415
JX.$E(
416
'JX.DOM.appendContent(<bleh>, ...): '+
417
'first argument must be a DOM node.');
418
}
419
}
420
421
this._insertContent(node, content, this._mechanismAppend);
422
},
423
424
425
/**
426
* Internal, add content to a node by prepending.
427
*
428
* @param Node Node to prepend content to.
429
* @param Node Node to prepend.
430
* @return void
431
* @task content
432
*/
433
_mechanismPrepend : function(node, content) {
434
node.insertBefore(content, node.firstChild);
435
},
436
437
438
/**
439
* Internal, add content to a node by appending.
440
*
441
* @param Node Node to append content to.
442
* @param Node Node to append.
443
* @task content
444
*/
445
_mechanismAppend : function(node, content) {
446
node.appendChild(content);
447
},
448
449
450
/**
451
* Internal, add content to a node using some specified mechanism.
452
*
453
* @param Node Node to add content to.
454
* @param mixed Content to add.
455
* @param function Callback for actually adding the nodes.
456
* @param bool True if array elements should be passed to the mechanism
457
* in reverse order, i.e. the mechanism prepends nodes.
458
* @return void
459
* @task content
460
*/
461
_insertContent : function(parent, content, mechanism, reverse) {
462
if (JX.isArray(content)) {
463
if (reverse) {
464
content = [].concat(content).reverse();
465
}
466
for (var ii = 0; ii < content.length; ii++) {
467
JX.DOM._insertContent(parent, content[ii], mechanism, reverse);
468
}
469
} else {
470
var type = typeof content;
471
if (content instanceof JX.HTML) {
472
content = content.getFragment();
473
} else if (type == 'string' || type == 'number') {
474
content = document.createTextNode(content);
475
}
476
477
if (__DEV__) {
478
if (content && !content.nodeType) {
479
JX.$E(
480
'JX.DOM._insertContent(<node>, ...): '+
481
'second argument must be a string, a number, ' +
482
'a DOM node or a JX.HTML instance');
483
}
484
}
485
486
content && mechanism(parent, content);
487
}
488
},
489
490
491
/* -( Updating Nodes )----------------------------------------------------- */
492
493
494
/**
495
* Remove a node from its parent, so it is no longer a child of any other
496
* node.
497
*
498
* @param Node Node to remove.
499
* @return Node The node.
500
* @task nodes
501
*/
502
remove : function(node) {
503
node.parentNode && JX.DOM.replace(node, null);
504
return node;
505
},
506
507
508
/**
509
* Replace a node with some other piece of content. This method obeys
510
* Javelin content semantics, see @{function:JX.$N} for an explanation.
511
* You can also @{method:setContent}, @{method:prependContent}, or
512
* @{method:appendContent}.
513
*
514
* @param Node Node to replace.
515
* @param mixed Content to replace it with.
516
* @return Node the original node.
517
* @task nodes
518
*/
519
replace : function(node, replacement) {
520
if (__DEV__) {
521
if (!node.parentNode) {
522
JX.$E(
523
'JX.DOM.replace(<node>, ...): '+
524
'node has no parent node, so it can not be replaced.');
525
}
526
}
527
528
var mechanism;
529
if (node.nextSibling) {
530
mechanism = JX.bind(node.nextSibling, function(parent, content) {
531
parent.insertBefore(content, this);
532
});
533
} else {
534
mechanism = this._mechanismAppend;
535
}
536
var parent = node.parentNode;
537
parent.removeChild(node);
538
this._insertContent(parent, replacement, mechanism);
539
540
return node;
541
},
542
543
544
/* -( Serializing Forms )-------------------------------------------------- */
545
546
547
/**
548
* Converts a form into a list of <name, value> pairs.
549
*
550
* Note: This function explicity does not match for submit inputs as there
551
* could be multiple in a form. It's the caller's obligation to add the
552
* submit input value if desired.
553
*
554
* @param Node The form element to convert into a list of pairs.
555
* @return List A list of <name, value> pairs.
556
* @task serialize
557
*/
558
convertFormToListOfPairs : function(form) {
559
var elements = form.getElementsByTagName('*');
560
var data = [];
561
for (var ii = 0; ii < elements.length; ++ii) {
562
if (!elements[ii].name) {
563
continue;
564
}
565
if (elements[ii].disabled) {
566
continue;
567
}
568
var type = elements[ii].type;
569
var tag = elements[ii].tagName;
570
if ((type in {radio: 1, checkbox: 1} && elements[ii].checked) ||
571
type in {text: 1, hidden: 1, password: 1, email: 1, tel: 1,
572
number: 1} ||
573
tag in {TEXTAREA: 1, SELECT: 1}) {
574
data.push([elements[ii].name, elements[ii].value]);
575
}
576
}
577
return data;
578
},
579
580
581
/**
582
* Converts a form into a dictionary mapping input names to values. This
583
* will overwrite duplicate inputs in an undefined way.
584
*
585
* @param Node The form element to convert into a dictionary.
586
* @return Dict A dictionary of form values.
587
* @task serialize
588
*/
589
convertFormToDictionary : function(form) {
590
var data = {};
591
var pairs = JX.DOM.convertFormToListOfPairs(form);
592
for (var ii = 0; ii < pairs.length; ii++) {
593
data[pairs[ii][0]] = pairs[ii][1];
594
}
595
return data;
596
},
597
598
599
/* -( Testing DOM Properties )--------------------------------------------- */
600
601
602
/**
603
* Test if an object is a valid Node.
604
*
605
* @param wild Something which might be a Node.
606
* @return bool True if the parameter is a DOM node.
607
* @task test
608
*/
609
isNode : function(node) {
610
return !!(node && node.nodeName && (node !== window));
611
},
612
613
614
/**
615
* Test if an object is a node of some specific (or one of several) types.
616
* For example, this tests if the node is an ##<input />##, ##<select />##,
617
* or ##<textarea />##.
618
*
619
* JX.DOM.isType(node, ['input', 'select', 'textarea']);
620
*
621
* @param wild Something which might be a Node.
622
* @param string|list One or more tags which you want to test for.
623
* @return bool True if the object is a node, and it's a node of one
624
* of the provided types.
625
* @task test
626
*/
627
isType : function(node, of_type) {
628
node = ('' + (node.nodeName || '')).toUpperCase();
629
of_type = JX.$AX(of_type);
630
for (var ii = 0; ii < of_type.length; ++ii) {
631
if (of_type[ii].toUpperCase() == node) {
632
return true;
633
}
634
}
635
return false;
636
},
637
638
639
/**
640
* Listen for events occuring beneath a specific node in the DOM. This is
641
* similar to @{JX.Stratcom.listen()}, but allows you to specify some node
642
* which serves as a scope instead of the default scope (the whole document)
643
* which you get if you install using @{JX.Stratcom.listen()} directly. For
644
* example, to listen for clicks on nodes with the sigil 'menu-item' below
645
* the root menu node:
646
*
647
* var the_menu = getReferenceToTheMenuNodeSomehow();
648
* JX.DOM.listen(the_menu, 'click', 'menu-item', function(e) { ... });
649
*
650
* @task stratcom
651
* @param Node The node to listen for events underneath.
652
* @param string|list One or more event types to listen for.
653
* @param list? A path to listen on, or a list of paths.
654
* @param function Callback to invoke when a matching event occurs.
655
* @return object A reference to the installed listener. You can later
656
* remove the listener by calling this object's remove()
657
* method.
658
*/
659
listen : function(node, type, path, callback) {
660
var auto_id = ['autoid:' + JX.DOM._getAutoID(node)];
661
path = JX.$AX(path || []);
662
if (!path.length) {
663
path = auto_id;
664
} else {
665
for (var ii = 0; ii < path.length; ii++) {
666
path[ii] = auto_id.concat(JX.$AX(path[ii]));
667
}
668
}
669
return JX.Stratcom.listen(type, path, callback);
670
},
671
672
673
/**
674
* Invoke a custom event on a node. This method is a companion to
675
* @{method:JX.DOM.listen} and parallels @{method:JX.Stratcom.invoke} in
676
* the same way that method parallels @{method:JX.Stratcom.listen}.
677
*
678
* This method can not be used to invoke native events (like 'click').
679
*
680
* @param Node The node to invoke an event on.
681
* @param string Custom event type.
682
* @param dict Event data.
683
* @return JX.Event The event object which was dispatched to listeners.
684
* The main use of this is to test whether any
685
* listeners prevented the event.
686
*/
687
invoke : function(node, type, data) {
688
if (__DEV__) {
689
if (type in JX.__allowedEvents) {
690
throw new Error(
691
'JX.DOM.invoke(..., "' + type + '", ...): ' +
692
'you cannot invoke with the same type as a native event.');
693
}
694
}
695
return JX.Stratcom.dispatch({
696
target: node,
697
type: type,
698
customData: data
699
});
700
},
701
702
703
uniqID : function(node) {
704
if (!node.getAttribute('id')) {
705
node.setAttribute('id', 'uniqid_'+(++JX.DOM._uniqid));
706
}
707
return node.getAttribute('id');
708
},
709
710
alterClass : function(node, className, add) {
711
if (__DEV__) {
712
if (add !== false && add !== true) {
713
JX.$E(
714
'JX.DOM.alterClass(...): ' +
715
'expects the third parameter to be Boolean: ' +
716
add + ' was provided');
717
}
718
}
719
720
var has = ((' '+node.className+' ').indexOf(' '+className+' ') > -1);
721
if (add && !has) {
722
node.className += ' '+className;
723
} else if (has && !add) {
724
node.className = node.className.replace(
725
new RegExp('(^|\\s)' + className + '(?:\\s|$)', 'g'), ' ').trim();
726
}
727
},
728
729
htmlize : function(str) {
730
return (''+str)
731
.replace(/&/g, '&amp;')
732
.replace(/"/g, '&quot;')
733
.replace(/</g, '&lt;')
734
.replace(/>/g, '&gt;');
735
},
736
737
738
/**
739
* Show one or more elements, by removing their "display" style. This
740
* assumes you have hidden them with @{method:hide}, or explicitly set
741
* the style to `display: none;`.
742
*
743
* @task convenience
744
* @param ... One or more nodes to remove "display" styles from.
745
* @return void
746
*/
747
show : function() {
748
var ii;
749
750
if (__DEV__) {
751
for (ii = 0; ii < arguments.length; ++ii) {
752
if (!arguments[ii]) {
753
JX.$E(
754
'JX.DOM.show(...): ' +
755
'one or more arguments were null or empty.');
756
}
757
}
758
}
759
760
for (ii = 0; ii < arguments.length; ++ii) {
761
arguments[ii].style.display = '';
762
}
763
},
764
765
766
/**
767
* Hide one or more elements, by setting `display: none;` on them. This is
768
* a convenience method. See also @{method:show}.
769
*
770
* @task convenience
771
* @param ... One or more nodes to set "display: none" on.
772
* @return void
773
*/
774
hide : function() {
775
var ii;
776
777
if (__DEV__) {
778
for (ii = 0; ii < arguments.length; ++ii) {
779
if (!arguments[ii]) {
780
JX.$E(
781
'JX.DOM.hide(...): ' +
782
'one or more arguments were null or empty.');
783
}
784
}
785
}
786
787
for (ii = 0; ii < arguments.length; ++ii) {
788
arguments[ii].style.display = 'none';
789
}
790
},
791
792
textMetrics : function(node, pseudoclass, x) {
793
if (!this._metrics[pseudoclass]) {
794
var n = JX.$N(
795
'var',
796
{className: pseudoclass});
797
this._metrics[pseudoclass] = n;
798
}
799
var proxy = this._metrics[pseudoclass];
800
document.body.appendChild(proxy);
801
proxy.style.width = x ? (x+'px') : '';
802
JX.DOM.setContent(
803
proxy,
804
JX.$H(JX.DOM.htmlize(node.value).replace(/\n/g, '<br />')));
805
var metrics = JX.Vector.getDim(proxy);
806
document.body.removeChild(proxy);
807
return metrics;
808
},
809
810
811
/**
812
* Search the document for DOM nodes by providing a root node to look
813
* beneath, a tag name, and (optionally) a sigil. Nodes which match all
814
* specified conditions are returned.
815
*
816
* @task query
817
*
818
* @param Node Root node to search beneath.
819
* @param string Tag name, like 'a' or 'textarea'.
820
* @param string Optionally, a sigil which nodes are required to have.
821
*
822
* @return list List of matching nodes, which may be empty.
823
*/
824
scry : function(root, tagname, sigil) {
825
if (__DEV__) {
826
if (!JX.DOM.isNode(root)) {
827
JX.$E(
828
'JX.DOM.scry(<yuck>, ...): '+
829
'first argument must be a DOM node.');
830
}
831
}
832
833
var nodes = root.getElementsByTagName(tagname);
834
if (!sigil) {
835
return JX.$A(nodes);
836
}
837
var result = [];
838
for (var ii = 0; ii < nodes.length; ii++) {
839
if (JX.Stratcom.hasSigil(nodes[ii], sigil)) {
840
result.push(nodes[ii]);
841
}
842
}
843
return result;
844
},
845
846
847
/**
848
* Select a node uniquely identified by a root, tagname and sigil. This
849
* is similar to JX.DOM.scry() but expects exactly one result.
850
*
851
* @task query
852
*
853
* @param Node Root node to search beneath.
854
* @param string Tag name, like 'a' or 'textarea'.
855
* @param string Optionally, sigil which selected node must have.
856
*
857
* @return Node Node uniquely identified by the criteria.
858
*/
859
find : function(root, tagname, sigil) {
860
if (__DEV__) {
861
if (!JX.DOM.isNode(root)) {
862
JX.$E(
863
'JX.DOM.find(<glop>, "'+tagname+'", "'+sigil+'"): '+
864
'first argument must be a DOM node.');
865
}
866
}
867
868
var result = JX.DOM.scry(root, tagname, sigil);
869
870
if (__DEV__) {
871
if (result.length > 1) {
872
JX.$E(
873
'JX.DOM.find(<node>, "'+tagname+'", "'+sigil+'"): '+
874
'matched more than one node.');
875
}
876
}
877
878
if (!result.length) {
879
JX.$E(
880
'JX.DOM.find(<node>, "' + tagname + '", "' + sigil + '"): ' +
881
'matched no nodes.');
882
}
883
884
return result[0];
885
},
886
887
888
/**
889
* Select a node uniquely identified by an anchor, tagname, and sigil. This
890
* is similar to JX.DOM.find() but walks up the DOM tree instead of down
891
* it.
892
*
893
* @param Node Node to look above.
894
* @param string Optional tag name, like 'a' or 'textarea'.
895
* @param string Optionally, sigil which selected node must have.
896
* @return Node Matching node.
897
*
898
* @task query
899
*/
900
findAbove : function(anchor, tagname, sigil) {
901
if (__DEV__) {
902
if (!JX.DOM.isNode(anchor)) {
903
JX.$E(
904
'JX.DOM.findAbove(<glop>, "' + tagname + '", "' + sigil + '"): ' +
905
'first argument must be a DOM node.');
906
}
907
}
908
909
var result = anchor.parentNode;
910
while (true) {
911
if (!result) {
912
break;
913
}
914
if (!tagname || JX.DOM.isType(result, tagname)) {
915
if (!sigil || JX.Stratcom.hasSigil(result, sigil)) {
916
break;
917
}
918
}
919
result = result.parentNode;
920
}
921
922
if (!result) {
923
JX.$E(
924
'JX.DOM.findAbove(<node>, "' + tagname + '", "' + sigil + '"): ' +
925
'no matching node.');
926
}
927
928
return result;
929
},
930
931
932
/**
933
* Focus a node safely. This is just a convenience wrapper that allows you
934
* to avoid IE's habit of throwing when nearly any focus operation is
935
* invoked.
936
*
937
* @task convenience
938
* @param Node Node to move cursor focus to, if possible.
939
* @return void
940
*/
941
focus : function(node) {
942
try { node.focus(); } catch (lol_ie) {}
943
},
944
945
946
/**
947
* Set specific nodes as content and frame nodes for the document.
948
*
949
* This will cause @{method:scrollTo} and @{method:scrollToPosition} to
950
* affect the given frame node instead of the window. This is useful if the
951
* page content is broken into multiple panels which scroll independently.
952
*
953
* Normally, both nodes are the document body.
954
*
955
* @task view
956
* @param Node Node to set as the scroll frame.
957
* @param Node Node to set as the content frame.
958
* @return void
959
*/
960
setContentFrame: function(frame_node, content_node) {
961
JX.DOM._frameNode = frame_node;
962
JX.DOM._contentNode = content_node;
963
},
964
965
966
/**
967
* Get the current content frame, or `document.body` if one has not been
968
* set.
969
*
970
* @task view
971
* @return Node The node which frames the main page content.
972
* @return void
973
*/
974
getContentFrame: function() {
975
return JX.DOM._contentNode || document.body;
976
},
977
978
/**
979
* Scroll to the position of an element in the document.
980
*
981
* If @{method:setContentFrame} has been used to set a frame, that node is
982
* scrolled.
983
*
984
* @task view
985
* @param Node Node to move document scroll position to, if possible.
986
* @return void
987
*/
988
scrollTo : function(node) {
989
var pos = JX.Vector.getPosWithScroll(node);
990
JX.DOM.scrollToPosition(0, pos.y);
991
},
992
993
/**
994
* Scroll to a specific position in the document.
995
*
996
* If @{method:setContentFrame} has been used to set a frame, that node is
997
* scrolled.
998
*
999
* @task view
1000
* @param int X position, in pixels.
1001
* @param int Y position, in pixels.
1002
* @return void
1003
*/
1004
scrollToPosition: function(x, y) {
1005
var self = JX.DOM;
1006
if (self._frameNode) {
1007
self._frameNode.scrollLeft = x;
1008
self._frameNode.scrollTop = y;
1009
} else {
1010
window.scrollTo(x, y);
1011
}
1012
},
1013
1014
_getAutoID : function(node) {
1015
if (!node.getAttribute('data-autoid')) {
1016
node.setAttribute('data-autoid', 'autoid_'+(++JX.DOM._autoid));
1017
}
1018
return node.getAttribute('data-autoid');
1019
}
1020
}
1021
});
1022
1023