Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
seleniumhq
GitHub Repository: seleniumhq/selenium
Path: blob/trunk/third_party/closure/goog/editor/plugins/blockquote.js
2868 views
1
// Copyright 2008 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 goog.editor plugin to handle splitting block quotes.
17
*
18
* @author [email protected] (Robby Walker)
19
*/
20
21
goog.provide('goog.editor.plugins.Blockquote');
22
23
goog.require('goog.dom');
24
goog.require('goog.dom.NodeType');
25
goog.require('goog.dom.TagName');
26
goog.require('goog.dom.classlist');
27
goog.require('goog.editor.BrowserFeature');
28
goog.require('goog.editor.Command');
29
goog.require('goog.editor.Plugin');
30
goog.require('goog.editor.node');
31
goog.require('goog.functions');
32
goog.require('goog.log');
33
34
35
36
/**
37
* Plugin to handle splitting block quotes. This plugin does nothing on its
38
* own and should be used in conjunction with EnterHandler or one of its
39
* subclasses.
40
* @param {boolean} requiresClassNameToSplit Whether to split only blockquotes
41
* that have the given classname.
42
* @param {string=} opt_className The classname to apply to generated
43
* blockquotes. Defaults to 'tr_bq'.
44
* @constructor
45
* @extends {goog.editor.Plugin}
46
* @final
47
*/
48
goog.editor.plugins.Blockquote = function(
49
requiresClassNameToSplit, opt_className) {
50
goog.editor.Plugin.call(this);
51
52
/**
53
* Whether we only split blockquotes that have {@link classname}, or whether
54
* all blockquote tags should be split on enter.
55
* @type {boolean}
56
* @private
57
*/
58
this.requiresClassNameToSplit_ = requiresClassNameToSplit;
59
60
/**
61
* Classname to put on blockquotes that are generated via the toolbar for
62
* blockquote, so that we can internally distinguish these from blockquotes
63
* that are used for indentation. This classname can be over-ridden by
64
* clients for styling or other purposes.
65
* @type {string}
66
* @private
67
*/
68
this.className_ = opt_className || goog.getCssName('tr_bq');
69
};
70
goog.inherits(goog.editor.plugins.Blockquote, goog.editor.Plugin);
71
72
73
/**
74
* Command implemented by this plugin.
75
* @type {string}
76
*/
77
goog.editor.plugins.Blockquote.SPLIT_COMMAND = '+splitBlockquote';
78
79
80
/**
81
* Class ID used to identify this plugin.
82
* @type {string}
83
*/
84
goog.editor.plugins.Blockquote.CLASS_ID = 'Blockquote';
85
86
87
/**
88
* Logging object.
89
* @type {goog.log.Logger}
90
* @protected
91
* @override
92
*/
93
goog.editor.plugins.Blockquote.prototype.logger =
94
goog.log.getLogger('goog.editor.plugins.Blockquote');
95
96
97
/** @override */
98
goog.editor.plugins.Blockquote.prototype.getTrogClassId = function() {
99
return goog.editor.plugins.Blockquote.CLASS_ID;
100
};
101
102
103
/**
104
* Since our exec command is always called from elsewhere, we make it silent.
105
* @override
106
*/
107
goog.editor.plugins.Blockquote.prototype.isSilentCommand = goog.functions.TRUE;
108
109
110
/**
111
* Checks if a node is a blockquote which can be split. A splittable blockquote
112
* meets the following criteria:
113
* <ol>
114
* <li>Node is a blockquote element</li>
115
* <li>Node has the blockquote classname if the classname is required to
116
* split</li>
117
* </ol>
118
*
119
* @param {Node} node DOM node in question.
120
* @return {boolean} Whether the node is a splittable blockquote.
121
*/
122
goog.editor.plugins.Blockquote.prototype.isSplittableBlockquote = function(
123
node) {
124
if (/** @type {!Element} */ (node).tagName != goog.dom.TagName.BLOCKQUOTE) {
125
return false;
126
}
127
128
if (!this.requiresClassNameToSplit_) {
129
return true;
130
}
131
132
return goog.dom.classlist.contains(
133
/** @type {!Element} */ (node), this.className_);
134
};
135
136
137
/**
138
* Checks if a node is a blockquote element which has been setup.
139
* @param {Node} node DOM node to check.
140
* @return {boolean} Whether the node is a blockquote with the required class
141
* name applied.
142
*/
143
goog.editor.plugins.Blockquote.prototype.isSetupBlockquote = function(node) {
144
return /** @type {!Element} */ (node).tagName ==
145
goog.dom.TagName.BLOCKQUOTE &&
146
goog.dom.classlist.contains(
147
/** @type {!Element} */ (node), this.className_);
148
};
149
150
151
/**
152
* Checks if a node is a blockquote element which has not been setup yet.
153
* @param {Node} node DOM node to check.
154
* @return {boolean} Whether the node is a blockquote without the required
155
* class name applied.
156
*/
157
goog.editor.plugins.Blockquote.prototype.isUnsetupBlockquote = function(node) {
158
return /** @type {!Element} */ (node).tagName ==
159
goog.dom.TagName.BLOCKQUOTE &&
160
!this.isSetupBlockquote(node);
161
};
162
163
164
/**
165
* Gets the class name required for setup blockquotes.
166
* @return {string} The blockquote class name.
167
*/
168
goog.editor.plugins.Blockquote.prototype.getBlockquoteClassName = function() {
169
return this.className_;
170
};
171
172
173
/**
174
* Helper routine which walks up the tree to find the topmost
175
* ancestor with only a single child. The ancestor node or the original
176
* node (if no ancestor was found) is then removed from the DOM.
177
*
178
* @param {Node} node The node whose ancestors have to be searched.
179
* @param {Node} root The root node to stop the search at.
180
* @private
181
*/
182
goog.editor.plugins.Blockquote.findAndRemoveSingleChildAncestor_ = function(
183
node, root) {
184
var predicateFunc = function(parentNode) {
185
return parentNode != root && parentNode.childNodes.length == 1;
186
};
187
var ancestor =
188
goog.editor.node.findHighestMatchingAncestor(node, predicateFunc);
189
if (!ancestor) {
190
ancestor = node;
191
}
192
goog.dom.removeNode(ancestor);
193
};
194
195
196
/**
197
* Remove every nodes from the DOM tree that are all white space nodes.
198
* @param {Array<Node>} nodes Nodes to be checked.
199
* @private
200
*/
201
goog.editor.plugins.Blockquote.removeAllWhiteSpaceNodes_ = function(nodes) {
202
for (var i = 0; i < nodes.length; ++i) {
203
if (goog.editor.node.isEmpty(nodes[i], true)) {
204
goog.dom.removeNode(nodes[i]);
205
}
206
}
207
};
208
209
210
/** @override */
211
goog.editor.plugins.Blockquote.prototype.isSupportedCommand = function(
212
command) {
213
return command == goog.editor.plugins.Blockquote.SPLIT_COMMAND;
214
};
215
216
217
/**
218
* Splits a quoted region if any. To be called on a key press event. When this
219
* function returns true, the event that caused it to be called should be
220
* canceled.
221
* @param {string} command The command to execute.
222
* @param {...*} var_args Single additional argument representing the current
223
* cursor position. If BrowserFeature.HAS_W3C_RANGES it is an object with a
224
* {@code node} key and an {@code offset} key. In other cases (legacy IE)
225
* it is a single node.
226
* @return {boolean|undefined} Boolean true when the quoted region has been
227
* split, false or undefined otherwise.
228
* @override
229
*/
230
goog.editor.plugins.Blockquote.prototype.execCommandInternal = function(
231
command, var_args) {
232
var pos = arguments[1];
233
if (command == goog.editor.plugins.Blockquote.SPLIT_COMMAND && pos &&
234
(this.className_ || !this.requiresClassNameToSplit_)) {
235
return goog.editor.BrowserFeature.HAS_W3C_RANGES ?
236
this.splitQuotedBlockW3C_(pos) :
237
this.splitQuotedBlockIE_(/** @type {Node} */ (pos));
238
}
239
};
240
241
242
/**
243
* Version of splitQuotedBlock_ that uses W3C ranges.
244
* @param {Object} anchorPos The current cursor position.
245
* @return {boolean} Whether the blockquote was split.
246
* @private
247
*/
248
goog.editor.plugins.Blockquote.prototype.splitQuotedBlockW3C_ = function(
249
anchorPos) {
250
var cursorNode = anchorPos.node;
251
var quoteNode = goog.editor.node.findTopMostEditableAncestor(
252
cursorNode.parentNode, goog.bind(this.isSplittableBlockquote, this));
253
254
var secondHalf, textNodeToRemove;
255
var insertTextNode = false;
256
// There are two special conditions that we account for here.
257
//
258
// 1. Whenever the cursor is after (one<BR>|) or just before a BR element
259
// (one|<BR>) and the user presses enter, the second quoted block starts
260
// with a BR which appears to the user as an extra newline. This stems
261
// from the fact that we create two text nodes as our split boundaries
262
// and the BR becomes a part of the second half because of this.
263
//
264
// 2. When the cursor is at the end of a text node with no siblings and
265
// the user presses enter, the second blockquote might contain a
266
// empty subtree that ends in a 0 length text node. We account for that
267
// as a post-splitting operation.
268
if (quoteNode) {
269
// selection is in a line that has text in it
270
if (cursorNode.nodeType == goog.dom.NodeType.TEXT) {
271
if (anchorPos.offset == cursorNode.length) {
272
var siblingNode = cursorNode.nextSibling;
273
274
// This accounts for the condition where the cursor appears at the
275
// end of a text node and right before the BR eg: one|<BR>. We ensure
276
// that we split on the BR in that case.
277
if (siblingNode && siblingNode.tagName == goog.dom.TagName.BR) {
278
cursorNode = siblingNode;
279
// This might be null but splitDomTreeAt accounts for the null case.
280
secondHalf = siblingNode.nextSibling;
281
} else {
282
textNodeToRemove = cursorNode.splitText(anchorPos.offset);
283
secondHalf = textNodeToRemove;
284
}
285
} else {
286
secondHalf = cursorNode.splitText(anchorPos.offset);
287
}
288
} else if (cursorNode.tagName == goog.dom.TagName.BR) {
289
// This might be null but splitDomTreeAt accounts for the null case.
290
secondHalf = cursorNode.nextSibling;
291
} else {
292
// The selection is in a line that is empty, with more than 1 level
293
// of quote.
294
insertTextNode = true;
295
}
296
} else {
297
// Check if current node is a quote node.
298
// This will happen if user clicks in an empty line in the quote,
299
// when there is 1 level of quote.
300
if (this.isSetupBlockquote(cursorNode)) {
301
quoteNode = cursorNode;
302
insertTextNode = true;
303
}
304
}
305
306
if (insertTextNode) {
307
// Create two empty text nodes to split between.
308
cursorNode = this.insertEmptyTextNodeBeforeRange_();
309
secondHalf = this.insertEmptyTextNodeBeforeRange_();
310
}
311
312
if (!quoteNode) {
313
return false;
314
}
315
316
secondHalf =
317
goog.editor.node.splitDomTreeAt(cursorNode, secondHalf, quoteNode);
318
goog.dom.insertSiblingAfter(secondHalf, quoteNode);
319
320
// Set the insertion point.
321
var dh = this.getFieldDomHelper();
322
var tagToInsert = this.getFieldObject().queryCommandValue(
323
goog.editor.Command.DEFAULT_TAG) ||
324
goog.dom.TagName.DIV;
325
var container = dh.createElement(/** @type {string} */ (tagToInsert));
326
container.innerHTML = '&nbsp;'; // Prevent the div from collapsing.
327
quoteNode.parentNode.insertBefore(container, secondHalf);
328
dh.getWindow().getSelection().collapse(container, 0);
329
330
// We need to account for the condition where the second blockquote
331
// might contain an empty DOM tree. This arises from trying to split
332
// at the end of an empty text node. We resolve this by walking up the tree
333
// till we either reach the blockquote or till we hit a node with more
334
// than one child. The resulting node is then removed from the DOM.
335
if (textNodeToRemove) {
336
goog.editor.plugins.Blockquote.findAndRemoveSingleChildAncestor_(
337
textNodeToRemove, secondHalf);
338
}
339
340
goog.editor.plugins.Blockquote.removeAllWhiteSpaceNodes_(
341
[quoteNode, secondHalf]);
342
return true;
343
};
344
345
346
/**
347
* Inserts an empty text node before the field's range.
348
* @return {!Node} The empty text node.
349
* @private
350
*/
351
goog.editor.plugins.Blockquote.prototype.insertEmptyTextNodeBeforeRange_ =
352
function() {
353
var range = this.getFieldObject().getRange();
354
var node = this.getFieldDomHelper().createTextNode('');
355
range.insertNode(node, true);
356
return node;
357
};
358
359
360
/**
361
* IE version of splitQuotedBlock_.
362
* @param {Node} splitNode The current cursor position.
363
* @return {boolean} Whether the blockquote was split.
364
* @private
365
*/
366
goog.editor.plugins.Blockquote.prototype.splitQuotedBlockIE_ = function(
367
splitNode) {
368
var dh = this.getFieldDomHelper();
369
var quoteNode = goog.editor.node.findTopMostEditableAncestor(
370
splitNode.parentNode, goog.bind(this.isSplittableBlockquote, this));
371
372
if (!quoteNode) {
373
return false;
374
}
375
376
var clone = splitNode.cloneNode(false);
377
378
// Whenever the cursor is just before a BR element (one|<BR>) and the user
379
// presses enter, the second quoted block starts with a BR which appears
380
// to the user as an extra newline. This stems from the fact that the
381
// dummy span that we create (splitNode) occurs before the BR and we split
382
// on that.
383
if (splitNode.nextSibling &&
384
/** @type {!Element} */ (splitNode.nextSibling).tagName ==
385
goog.dom.TagName.BR) {
386
splitNode = splitNode.nextSibling;
387
}
388
var secondHalf = goog.editor.node.splitDomTreeAt(splitNode, clone, quoteNode);
389
goog.dom.insertSiblingAfter(secondHalf, quoteNode);
390
391
// Set insertion point.
392
var tagToInsert = this.getFieldObject().queryCommandValue(
393
goog.editor.Command.DEFAULT_TAG) ||
394
goog.dom.TagName.DIV;
395
var div = dh.createElement(/** @type {string} */ (tagToInsert));
396
quoteNode.parentNode.insertBefore(div, secondHalf);
397
398
// The div needs non-whitespace contents in order for the insertion point
399
// to get correctly inserted.
400
div.innerHTML = '&nbsp;';
401
402
// Moving the range 1 char isn't enough when you have markup.
403
// This moves the range to the end of the nbsp.
404
var range = dh.getDocument().selection.createRange();
405
range.moveToElementText(splitNode);
406
range.move('character', 2);
407
range.select();
408
409
// Remove the no-longer-necessary nbsp.
410
goog.dom.removeChildren(div);
411
412
// Clear the original selection.
413
range.pasteHTML('');
414
415
// We need to remove clone from the DOM but just removing clone alone will
416
// not suffice. Let's assume we have the following DOM structure and the
417
// cursor is placed after the first numbered list item "one".
418
//
419
// <blockquote class="gmail-quote">
420
// <div><div>a</div><ol><li>one|</li></ol></div>
421
// <div>b</div>
422
// </blockquote>
423
//
424
// After pressing enter, we have the following structure.
425
//
426
// <blockquote class="gmail-quote">
427
// <div><div>a</div><ol><li>one|</li></ol></div>
428
// </blockquote>
429
// <div>&nbsp;</div>
430
// <blockquote class="gmail-quote">
431
// <div><ol><li><span id=""></span></li></ol></div>
432
// <div>b</div>
433
// </blockquote>
434
//
435
// The clone is contained in a subtree which should be removed. This stems
436
// from the fact that we invoke splitDomTreeAt with the dummy span
437
// as the starting splitting point and this results in the empty subtree
438
// <div><ol><li><span id=""></span></li></ol></div>.
439
//
440
// We resolve this by walking up the tree till we either reach the
441
// blockquote or till we hit a node with more than one child. The resulting
442
// node is then removed from the DOM.
443
goog.editor.plugins.Blockquote.findAndRemoveSingleChildAncestor_(
444
clone, secondHalf);
445
446
goog.editor.plugins.Blockquote.removeAllWhiteSpaceNodes_(
447
[quoteNode, secondHalf]);
448
return true;
449
};
450
451