Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
seleniumhq
GitHub Repository: seleniumhq/selenium
Path: blob/trunk/third_party/closure/goog/editor/node.js
2868 views
1
// Copyright 2005 The Closure Library Authors. All Rights Reserved.
2
//
3
// Licensed under the Apache License, Version 2.0 (the "License");
4
// you may not use this file except in compliance with the License.
5
// You may obtain a copy of the License at
6
//
7
// http://www.apache.org/licenses/LICENSE-2.0
8
//
9
// Unless required by applicable law or agreed to in writing, software
10
// distributed under the License is distributed on an "AS-IS" BASIS,
11
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
// See the License for the specific language governing permissions and
13
// limitations under the License.
14
15
/**
16
* @fileoverview Utilties for working with DOM nodes related to rich text
17
* editing. Many of these are not general enough to go into goog.dom.
18
*
19
* @author [email protected] (Nick Santos)
20
*/
21
22
goog.provide('goog.editor.node');
23
24
goog.require('goog.dom');
25
goog.require('goog.dom.NodeType');
26
goog.require('goog.dom.TagName');
27
goog.require('goog.dom.iter.ChildIterator');
28
goog.require('goog.dom.iter.SiblingIterator');
29
goog.require('goog.iter');
30
goog.require('goog.object');
31
goog.require('goog.string');
32
goog.require('goog.string.Unicode');
33
goog.require('goog.userAgent');
34
35
36
/**
37
* Names of all block-level tags
38
* @type {Object}
39
* @private
40
*/
41
goog.editor.node.BLOCK_TAG_NAMES_ = goog.object.createSet(
42
goog.dom.TagName.ADDRESS, goog.dom.TagName.ARTICLE, goog.dom.TagName.ASIDE,
43
goog.dom.TagName.BLOCKQUOTE, goog.dom.TagName.BODY,
44
goog.dom.TagName.CAPTION, goog.dom.TagName.CENTER, goog.dom.TagName.COL,
45
goog.dom.TagName.COLGROUP, goog.dom.TagName.DETAILS, goog.dom.TagName.DIR,
46
goog.dom.TagName.DIV, goog.dom.TagName.DL, goog.dom.TagName.DD,
47
goog.dom.TagName.DT, goog.dom.TagName.FIELDSET, goog.dom.TagName.FIGCAPTION,
48
goog.dom.TagName.FIGURE, goog.dom.TagName.FOOTER, goog.dom.TagName.FORM,
49
goog.dom.TagName.H1, goog.dom.TagName.H2, goog.dom.TagName.H3,
50
goog.dom.TagName.H4, goog.dom.TagName.H5, goog.dom.TagName.H6,
51
goog.dom.TagName.HEADER, goog.dom.TagName.HGROUP, goog.dom.TagName.HR,
52
goog.dom.TagName.ISINDEX, goog.dom.TagName.OL, goog.dom.TagName.LI,
53
goog.dom.TagName.MAP, goog.dom.TagName.MENU, goog.dom.TagName.NAV,
54
goog.dom.TagName.OPTGROUP, goog.dom.TagName.OPTION, goog.dom.TagName.P,
55
goog.dom.TagName.PRE, goog.dom.TagName.SECTION, goog.dom.TagName.SUMMARY,
56
goog.dom.TagName.TABLE, goog.dom.TagName.TBODY, goog.dom.TagName.TD,
57
goog.dom.TagName.TFOOT, goog.dom.TagName.TH, goog.dom.TagName.THEAD,
58
goog.dom.TagName.TR, goog.dom.TagName.UL);
59
60
61
/**
62
* Names of tags that have intrinsic content.
63
* TODO(robbyw): What about object, br, input, textarea, button, isindex,
64
* hr, keygen, select, table, tr, td?
65
* @type {Object}
66
* @private
67
*/
68
goog.editor.node.NON_EMPTY_TAGS_ = goog.object.createSet(
69
goog.dom.TagName.IMG, goog.dom.TagName.IFRAME, goog.dom.TagName.EMBED);
70
71
72
/**
73
* Check if the node is in a standards mode document.
74
* @param {Node} node The node to test.
75
* @return {boolean} Whether the node is in a standards mode document.
76
*/
77
goog.editor.node.isStandardsMode = function(node) {
78
return goog.dom.getDomHelper(node).isCss1CompatMode();
79
};
80
81
82
/**
83
* Get the right-most non-ignorable leaf node of the given node.
84
* @param {Node} parent The parent ndoe.
85
* @return {Node} The right-most non-ignorable leaf node.
86
*/
87
goog.editor.node.getRightMostLeaf = function(parent) {
88
var temp;
89
while (temp = goog.editor.node.getLastChild(parent)) {
90
parent = temp;
91
}
92
return parent;
93
};
94
95
96
/**
97
* Get the left-most non-ignorable leaf node of the given node.
98
* @param {Node} parent The parent ndoe.
99
* @return {Node} The left-most non-ignorable leaf node.
100
*/
101
goog.editor.node.getLeftMostLeaf = function(parent) {
102
var temp;
103
while (temp = goog.editor.node.getFirstChild(parent)) {
104
parent = temp;
105
}
106
return parent;
107
};
108
109
110
/**
111
* Version of firstChild that skips nodes that are entirely
112
* whitespace and comments.
113
* @param {Node} parent The reference node.
114
* @return {Node} The first child of sibling that is important according to
115
* goog.editor.node.isImportant, or null if no such node exists.
116
*/
117
goog.editor.node.getFirstChild = function(parent) {
118
return goog.editor.node.getChildHelper_(parent, false);
119
};
120
121
122
/**
123
* Version of lastChild that skips nodes that are entirely whitespace or
124
* comments. (Normally lastChild is a property of all DOM nodes that gives the
125
* last of the nodes contained directly in the reference node.)
126
* @param {Node} parent The reference node.
127
* @return {Node} The last child of sibling that is important according to
128
* goog.editor.node.isImportant, or null if no such node exists.
129
*/
130
goog.editor.node.getLastChild = function(parent) {
131
return goog.editor.node.getChildHelper_(parent, true);
132
};
133
134
135
/**
136
* Version of previoussibling that skips nodes that are entirely
137
* whitespace or comments. (Normally previousSibling is a property
138
* of all DOM nodes that gives the sibling node, the node that is
139
* a child of the same parent, that occurs immediately before the
140
* reference node.)
141
* @param {Node} sibling The reference node.
142
* @return {Node} The closest previous sibling to sibling that is
143
* important according to goog.editor.node.isImportant, or null if no such
144
* node exists.
145
*/
146
goog.editor.node.getPreviousSibling = function(sibling) {
147
return /** @type {Node} */ (
148
goog.editor.node.getFirstValue_(
149
goog.iter.filter(
150
new goog.dom.iter.SiblingIterator(sibling, false, true),
151
goog.editor.node.isImportant)));
152
};
153
154
155
/**
156
* Version of nextSibling that skips nodes that are entirely whitespace or
157
* comments.
158
* @param {Node} sibling The reference node.
159
* @return {Node} The closest next sibling to sibling that is important
160
* according to goog.editor.node.isImportant, or null if no
161
* such node exists.
162
*/
163
goog.editor.node.getNextSibling = function(sibling) {
164
return /** @type {Node} */ (
165
goog.editor.node.getFirstValue_(
166
goog.iter.filter(
167
new goog.dom.iter.SiblingIterator(sibling),
168
goog.editor.node.isImportant)));
169
};
170
171
172
/**
173
* Internal helper for lastChild/firstChild that skips nodes that are entirely
174
* whitespace or comments.
175
* @param {Node} parent The reference node.
176
* @param {boolean} isReversed Whether children should be traversed forward
177
* or backward.
178
* @return {Node} The first/last child of sibling that is important according
179
* to goog.editor.node.isImportant, or null if no such node exists.
180
* @private
181
*/
182
goog.editor.node.getChildHelper_ = function(parent, isReversed) {
183
return (!parent || parent.nodeType != goog.dom.NodeType.ELEMENT) ?
184
null :
185
/** @type {Node} */ (
186
goog.editor.node.getFirstValue_(
187
goog.iter.filter(
188
new goog.dom.iter.ChildIterator(
189
/** @type {!Element} */ (parent), isReversed),
190
goog.editor.node.isImportant)));
191
};
192
193
194
/**
195
* Utility function that returns the first value from an iterator or null if
196
* the iterator is empty.
197
* @param {goog.iter.Iterator} iterator The iterator to get a value from.
198
* @return {*} The first value from the iterator.
199
* @private
200
*/
201
goog.editor.node.getFirstValue_ = function(iterator) {
202
203
try {
204
return iterator.next();
205
} catch (e) {
206
return null;
207
}
208
};
209
210
211
/**
212
* Determine if a node should be returned by the iterator functions.
213
* @param {Node} node An object implementing the DOM1 Node interface.
214
* @return {boolean} Whether the node is an element, or a text node that
215
* is not all whitespace.
216
*/
217
goog.editor.node.isImportant = function(node) {
218
// Return true if the node is not either a TextNode or an ElementNode.
219
return node.nodeType == goog.dom.NodeType.ELEMENT ||
220
node.nodeType == goog.dom.NodeType.TEXT &&
221
!goog.editor.node.isAllNonNbspWhiteSpace(node);
222
};
223
224
225
/**
226
* Determine whether a node's text content is entirely whitespace.
227
* @param {Node} textNode A node implementing the CharacterData interface (i.e.,
228
* a Text, Comment, or CDATASection node.
229
* @return {boolean} Whether the text content of node is whitespace,
230
* otherwise false.
231
*/
232
goog.editor.node.isAllNonNbspWhiteSpace = function(textNode) {
233
return goog.string.isBreakingWhitespace(textNode.nodeValue);
234
};
235
236
237
/**
238
* Returns true if the node contains only whitespace and is not and does not
239
* contain any images, iframes or embed tags.
240
* @param {Node} node The node to check.
241
* @param {boolean=} opt_prohibitSingleNbsp By default, this function treats a
242
* single nbsp as empty. Set this to true to treat this case as non-empty.
243
* @return {boolean} Whether the node contains only whitespace.
244
*/
245
goog.editor.node.isEmpty = function(node, opt_prohibitSingleNbsp) {
246
var nodeData = goog.dom.getRawTextContent(node);
247
248
if (node.getElementsByTagName) {
249
node = /** @type {!Element} */ (node);
250
for (var tag in goog.editor.node.NON_EMPTY_TAGS_) {
251
if (node.tagName == tag || node.getElementsByTagName(tag).length > 0) {
252
return false;
253
}
254
}
255
}
256
return (!opt_prohibitSingleNbsp && nodeData == goog.string.Unicode.NBSP) ||
257
goog.string.isBreakingWhitespace(nodeData);
258
};
259
260
261
/**
262
* Returns the length of the text in node if it is a text node, or the number
263
* of children of the node, if it is an element. Useful for range-manipulation
264
* code where you need to know the offset for the right side of the node.
265
* @param {Node} node The node to get the length of.
266
* @return {number} The length of the node.
267
*/
268
goog.editor.node.getLength = function(node) {
269
return node.length || node.childNodes.length;
270
};
271
272
273
/**
274
* Search child nodes using a predicate function and return the first node that
275
* satisfies the condition.
276
* @param {Node} parent The parent node to search.
277
* @param {function(Node):boolean} hasProperty A function that takes a child
278
* node as a parameter and returns true if it meets the criteria.
279
* @return {?number} The index of the node found, or null if no node is found.
280
*/
281
goog.editor.node.findInChildren = function(parent, hasProperty) {
282
for (var i = 0, len = parent.childNodes.length; i < len; i++) {
283
if (hasProperty(parent.childNodes[i])) {
284
return i;
285
}
286
}
287
return null;
288
};
289
290
291
/**
292
* Search ancestor nodes using a predicate function and returns the topmost
293
* ancestor in the chain of consecutive ancestors that satisfies the condition.
294
*
295
* @param {Node} node The node whose ancestors have to be searched.
296
* @param {function(Node): boolean} hasProperty A function that takes a parent
297
* node as a parameter and returns true if it meets the criteria.
298
* @return {Node} The topmost ancestor or null if no ancestor satisfies the
299
* predicate function.
300
*/
301
goog.editor.node.findHighestMatchingAncestor = function(node, hasProperty) {
302
var parent = node.parentNode;
303
var ancestor = null;
304
while (parent && hasProperty(parent)) {
305
ancestor = parent;
306
parent = parent.parentNode;
307
}
308
return ancestor;
309
};
310
311
312
/**
313
* Checks if node is a block-level html element. The <tt>display</tt> css
314
* property is ignored.
315
* @param {Node} node The node to test.
316
* @return {boolean} Whether the node is a block-level node.
317
*/
318
goog.editor.node.isBlockTag = function(node) {
319
return !!goog.editor.node.BLOCK_TAG_NAMES_[
320
/** @type {!Element} */ (node).tagName];
321
};
322
323
324
/**
325
* Skips siblings of a node that are empty text nodes.
326
* @param {Node} node A node. May be null.
327
* @return {Node} The node or the first sibling of the node that is not an
328
* empty text node. May be null.
329
*/
330
goog.editor.node.skipEmptyTextNodes = function(node) {
331
while (node && node.nodeType == goog.dom.NodeType.TEXT && !node.nodeValue) {
332
node = node.nextSibling;
333
}
334
return node;
335
};
336
337
338
/**
339
* Checks if an element is a top-level editable container (meaning that
340
* it itself is not editable, but all its child nodes are editable).
341
* @param {Node} element The element to test.
342
* @return {boolean} Whether the element is a top-level editable container.
343
*/
344
goog.editor.node.isEditableContainer = function(element) {
345
return element.getAttribute && element.getAttribute('g_editable') == 'true';
346
};
347
348
349
/**
350
* Checks if a node is inside an editable container.
351
* @param {Node} node The node to test.
352
* @return {boolean} Whether the node is in an editable container.
353
*/
354
goog.editor.node.isEditable = function(node) {
355
return !!goog.dom.getAncestor(node, goog.editor.node.isEditableContainer);
356
};
357
358
359
/**
360
* Finds the top-most DOM node inside an editable field that is an ancestor
361
* (or self) of a given DOM node and meets the specified criteria.
362
* @param {Node} node The DOM node where the search starts.
363
* @param {function(Node) : boolean} criteria A function that takes a DOM node
364
* as a parameter and returns a boolean to indicate whether the node meets
365
* the criteria or not.
366
* @return {Node} The DOM node if found, or null.
367
*/
368
goog.editor.node.findTopMostEditableAncestor = function(node, criteria) {
369
var targetNode = null;
370
while (node && !goog.editor.node.isEditableContainer(node)) {
371
if (criteria(node)) {
372
targetNode = node;
373
}
374
node = node.parentNode;
375
}
376
return targetNode;
377
};
378
379
380
/**
381
* Splits off a subtree.
382
* @param {!Node} currentNode The starting splitting point.
383
* @param {Node=} opt_secondHalf The initial leftmost leaf the new subtree.
384
* If null, siblings after currentNode will be placed in the subtree, but
385
* no additional node will be.
386
* @param {Node=} opt_root The top of the tree where splitting stops at.
387
* @return {!Node} The new subtree.
388
*/
389
goog.editor.node.splitDomTreeAt = function(
390
currentNode, opt_secondHalf, opt_root) {
391
var parent;
392
while (currentNode != opt_root && (parent = currentNode.parentNode)) {
393
opt_secondHalf = goog.editor.node.getSecondHalfOfNode_(
394
parent, currentNode, opt_secondHalf);
395
currentNode = parent;
396
}
397
return /** @type {!Node} */ (opt_secondHalf);
398
};
399
400
401
/**
402
* Creates a clone of node, moving all children after startNode to it.
403
* When firstChild is not null or undefined, it is also appended to the clone
404
* as the first child.
405
* @param {!Node} node The node to clone.
406
* @param {!Node} startNode All siblings after this node will be moved to the
407
* clone.
408
* @param {Node|undefined} firstChild The first child of the new cloned element.
409
* @return {!Node} The cloned node that now contains the children after
410
* startNode.
411
* @private
412
*/
413
goog.editor.node.getSecondHalfOfNode_ = function(node, startNode, firstChild) {
414
var secondHalf = /** @type {!Node} */ (node.cloneNode(false));
415
while (startNode.nextSibling) {
416
goog.dom.appendChild(secondHalf, startNode.nextSibling);
417
}
418
if (firstChild) {
419
secondHalf.insertBefore(firstChild, secondHalf.firstChild);
420
}
421
return secondHalf;
422
};
423
424
425
/**
426
* Appends all of oldNode's children to newNode. This removes all children from
427
* oldNode and appends them to newNode. oldNode is left with no children.
428
* @param {!Node} newNode Node to transfer children to.
429
* @param {Node} oldNode Node to transfer children from.
430
* @deprecated Use goog.dom.append directly instead.
431
*/
432
goog.editor.node.transferChildren = function(newNode, oldNode) {
433
goog.dom.append(newNode, oldNode.childNodes);
434
};
435
436
437
/**
438
* Replaces the innerHTML of a node.
439
*
440
* IE has serious problems if you try to set innerHTML of an editable node with
441
* any selection. Early versions of IE tear up the old internal tree storage, to
442
* help avoid ref-counting loops. But this sometimes leaves the selection object
443
* in a bad state and leads to segfaults.
444
*
445
* Removing the nodes first prevents IE from tearing them up. This is not
446
* strictly necessary in nodes that do not have the selection. You should always
447
* use this function when setting innerHTML inside of a field.
448
*
449
* @param {Node} node A node.
450
* @param {string} html The innerHTML to set on the node.
451
*/
452
goog.editor.node.replaceInnerHtml = function(node, html) {
453
// Only do this IE. On gecko, we use element change events, and don't
454
// want to trigger spurious events.
455
if (goog.userAgent.IE) {
456
goog.dom.removeChildren(node);
457
}
458
node.innerHTML = html;
459
};
460
461