aboutsummaryrefslogtreecommitdiff
path: root/contexts/data/lib/closure-library/closure/goog/editor/plugins/blockquote.js
blob: d73719c8137994cc6ce606e6b466e799992e2dc2 (plain)
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
// Copyright 2008 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
 * @fileoverview goog.editor plugin to handle splitting block quotes.
 *
 */

goog.provide('goog.editor.plugins.Blockquote');

goog.require('goog.debug.Logger');
goog.require('goog.dom');
goog.require('goog.dom.NodeType');
goog.require('goog.dom.TagName');
goog.require('goog.dom.classes');
goog.require('goog.editor.BrowserFeature');
goog.require('goog.editor.Command');
goog.require('goog.editor.Plugin');
goog.require('goog.editor.node');
goog.require('goog.functions');



/**
 * Plugin to handle splitting block quotes.  This plugin does nothing on its
 * own and should be used in conjunction with EnterHandler or one of its
 * subclasses.
 * @param {boolean} requiresClassNameToSplit Whether to split only blockquotes
 *     that have the given classname.
 * @param {string=} opt_className The classname to apply to generated
 *     blockquotes.  Defaults to 'tr_bq'.
 * @constructor
 * @extends {goog.editor.Plugin}
 */
goog.editor.plugins.Blockquote = function(requiresClassNameToSplit,
    opt_className) {
  goog.editor.Plugin.call(this);

  /**
   * Whether we only split blockquotes that have {@link classname}, or whether
   * all blockquote tags should be split on enter.
   * @type {boolean}
   * @private
   */
  this.requiresClassNameToSplit_ = requiresClassNameToSplit;

  /**
   * Classname to put on blockquotes that are generated via the toolbar for
   * blockquote, so that we can internally distinguish these from blockquotes
   * that are used for indentation.  This classname can be over-ridden by
   * clients for styling or other purposes.
   * @type {string}
   * @private
   */
  this.className_ = opt_className || goog.getCssName('tr_bq');
};
goog.inherits(goog.editor.plugins.Blockquote, goog.editor.Plugin);


/**
 * Command implemented by this plugin.
 * @type {string}
 */
goog.editor.plugins.Blockquote.SPLIT_COMMAND = '+splitBlockquote';


/**
 * Class ID used to identify this plugin.
 * @type {string}
 */
goog.editor.plugins.Blockquote.CLASS_ID = 'Blockquote';


/**
 * Logging object.
 * @type {goog.debug.Logger}
 * @protected
 * @override
 */
goog.editor.plugins.Blockquote.prototype.logger =
    goog.debug.Logger.getLogger('goog.editor.plugins.Blockquote');


/** @override */
goog.editor.plugins.Blockquote.prototype.getTrogClassId = function() {
  return goog.editor.plugins.Blockquote.CLASS_ID;
};


/**
 * Since our exec command is always called from elsewhere, we make it silent.
 * @override
 */
goog.editor.plugins.Blockquote.prototype.isSilentCommand = goog.functions.TRUE;


/**
 * Checks if a node is a blockquote node.  If isAlreadySetup is set, it also
 * makes sure the node has the blockquote classname applied.  Otherwise, it
 * ensures that the blockquote does not already have the classname applied.
 * @param {Node} node DOM node to check.
 * @param {boolean} isAlreadySetup True to enforce that the classname must be
 *                  set in order for it to count as a blockquote, false to
 *                  enforce that the classname must not be set in order for
 *                  it to count as a blockquote.
 * @param {boolean} requiresClassNameToSplit Whether only blockquotes with the
 *     class name should be split.
 * @param {string} className The official blockquote class name.
 * @return {boolean} Whether node is a blockquote and if isAlreadySetup is
 *    true, then whether this is a setup blockquote.
 * @deprecated Use {@link #isSplittableBlockquote},
 *     {@link #isSetupBlockquote}, or {@link #isUnsetupBlockquote} instead
 *     since this has confusing behavior.
 */
goog.editor.plugins.Blockquote.isBlockquote = function(node, isAlreadySetup,
    requiresClassNameToSplit, className) {
  if (node.tagName != goog.dom.TagName.BLOCKQUOTE) {
    return false;
  }
  if (!requiresClassNameToSplit) {
    return isAlreadySetup;
  }
  var hasClassName = goog.dom.classes.has(/** @type {Element} */ (node),
      className);
  return isAlreadySetup ? hasClassName : !hasClassName;
};


/**
 * Checks if a node is a blockquote which can be split. A splittable blockquote
 * meets the following criteria:
 * <ol>
 *   <li>Node is a blockquote element</li>
 *   <li>Node has the blockquote classname if the classname is required to
 *       split</li>
 * </ol>
 *
 * @param {Node} node DOM node in question.
 * @return {boolean} Whether the node is a splittable blockquote.
 */
goog.editor.plugins.Blockquote.prototype.isSplittableBlockquote =
    function(node) {
  if (node.tagName != goog.dom.TagName.BLOCKQUOTE) {
    return false;
  }

  if (!this.requiresClassNameToSplit_) {
    return true;
  }

  return goog.dom.classes.has(node, this.className_);
};


/**
 * Checks if a node is a blockquote element which has been setup.
 * @param {Node} node DOM node to check.
 * @return {boolean} Whether the node is a blockquote with the required class
 *     name applied.
 */
goog.editor.plugins.Blockquote.prototype.isSetupBlockquote =
    function(node) {
  return node.tagName == goog.dom.TagName.BLOCKQUOTE &&
      goog.dom.classes.has(node, this.className_);
};


/**
 * Checks if a node is a blockquote element which has not been setup yet.
 * @param {Node} node DOM node to check.
 * @return {boolean} Whether the node is a blockquote without the required
 *     class name applied.
 */
goog.editor.plugins.Blockquote.prototype.isUnsetupBlockquote =
    function(node) {
  return node.tagName == goog.dom.TagName.BLOCKQUOTE &&
      !this.isSetupBlockquote(node);
};


/**
 * Gets the class name required for setup blockquotes.
 * @return {string} The blockquote class name.
 */
goog.editor.plugins.Blockquote.prototype.getBlockquoteClassName = function() {
  return this.className_;
};


/**
 * Helper routine which walks up the tree to find the topmost
 * ancestor with only a single child. The ancestor node or the original
 * node (if no ancestor was found) is then removed from the DOM.
 *
 * @param {Node} node The node whose ancestors have to be searched.
 * @param {Node} root The root node to stop the search at.
 * @private
 */
goog.editor.plugins.Blockquote.findAndRemoveSingleChildAncestor_ = function(
    node, root) {
  var predicateFunc = function(parentNode) {
    return parentNode != root && parentNode.childNodes.length == 1;
  }
  var ancestor = goog.editor.node.findHighestMatchingAncestor(node,
      predicateFunc);
  if (!ancestor) {
    ancestor = node;
  }
  goog.dom.removeNode(ancestor);
};


/**
 * Remove every nodes from the DOM tree that are all white space nodes.
 * @param {Array.<Node>} nodes Nodes to be checked.
 * @private
 */
goog.editor.plugins.Blockquote.removeAllWhiteSpaceNodes_ = function(nodes) {
  for (var i = 0; i < nodes.length; ++i) {
    if (goog.editor.node.isEmpty(nodes[i], true)) {
      goog.dom.removeNode(nodes[i]);
    }
  }
};


/** @override */
goog.editor.plugins.Blockquote.prototype.isSupportedCommand = function(
    command) {
  return command == goog.editor.plugins.Blockquote.SPLIT_COMMAND;
};


/**
 * Splits a quoted region if any.  To be called on a key press event.  When this
 * function returns true, the event that caused it to be called should be
 * canceled.
 * @param {string} command The command to execute.
 * @param {...*} var_args Single additional argument representing the
 *     current cursor position.  In IE, it is a single node.  In any other
 *     browser, it is an object with a {@code node} key and an {@code offset}
 *     key.
 * @return {boolean|undefined} Boolean true when the quoted region has been
 *     split, false or undefined otherwise.
 * @override
 */
goog.editor.plugins.Blockquote.prototype.execCommandInternal = function(
    command, var_args) {
  var pos = arguments[1];
  if (command == goog.editor.plugins.Blockquote.SPLIT_COMMAND && pos &&
      (this.className_ || !this.requiresClassNameToSplit_)) {
    return goog.editor.BrowserFeature.HAS_W3C_RANGES ?
        this.splitQuotedBlockW3C_(pos) :
        this.splitQuotedBlockIE_(/** @type {Node} */ (pos));
  }
};


/**
 * Version of splitQuotedBlock_ that uses W3C ranges.
 * @param {Object} anchorPos The current cursor position.
 * @return {boolean} Whether the blockquote was split.
 * @private
 */
goog.editor.plugins.Blockquote.prototype.splitQuotedBlockW3C_ =
    function(anchorPos) {
  var cursorNode = anchorPos.node;
  var quoteNode = goog.editor.node.findTopMostEditableAncestor(
      cursorNode.parentNode, goog.bind(this.isSplittableBlockquote, this));

  var secondHalf, textNodeToRemove;
  var insertTextNode = false;
  // There are two special conditions that we account for here.
  //
  // 1. Whenever the cursor is after (one<BR>|) or just before a BR element
  //    (one|<BR>) and the user presses enter, the second quoted block starts
  //    with a BR which appears to the user as an extra newline. This stems
  //    from the fact that we create two text nodes as our split boundaries
  //    and the BR becomes a part of the second half because of this.
  //
  // 2. When the cursor is at the end of a text node with no siblings and
  //    the user presses enter, the second blockquote might contain a
  //    empty subtree that ends in a 0 length text node. We account for that
  //    as a post-splitting operation.
  if (quoteNode) {

    // selection is in a line that has text in it
    if (cursorNode.nodeType == goog.dom.NodeType.TEXT) {
      if (anchorPos.offset == cursorNode.length) {
        var siblingNode = cursorNode.nextSibling;

        // This accounts for the condition where the cursor appears at the
        // end of a text node and right before the BR eg: one|<BR>. We ensure
        // that we split on the BR in that case.
        if (siblingNode && siblingNode.tagName == goog.dom.TagName.BR) {
          cursorNode = siblingNode;
          // This might be null but splitDomTreeAt accounts for the null case.
          secondHalf = siblingNode.nextSibling;
        } else {
          textNodeToRemove = cursorNode.splitText(anchorPos.offset);
          secondHalf = textNodeToRemove;
        }
      } else {
        secondHalf = cursorNode.splitText(anchorPos.offset);
      }
    } else if (cursorNode.tagName == goog.dom.TagName.BR) {
      // This might be null but splitDomTreeAt accounts for the null case.
      secondHalf = cursorNode.nextSibling;
    } else {
      // The selection is in a line that is empty, with more than 1 level
      // of quote.
      insertTextNode = true;
    }
  } else {
    // Check if current node is a quote node.
    // This will happen if user clicks in an empty line in the quote,
    // when there is 1 level of quote.
    if (this.isSetupBlockquote(cursorNode)) {
      quoteNode = cursorNode;
      insertTextNode = true;
    }
  }

  if (insertTextNode) {
    // Create two empty text nodes to split between.
    cursorNode = this.insertEmptyTextNodeBeforeRange_();
    secondHalf = this.insertEmptyTextNodeBeforeRange_();
  }

  if (!quoteNode) {
    return false;
  }

  secondHalf = goog.editor.node.splitDomTreeAt(cursorNode, secondHalf,
      quoteNode);
  goog.dom.insertSiblingAfter(secondHalf, quoteNode);

  // Set the insertion point.
  var dh = this.getFieldDomHelper();
  var tagToInsert =
      this.getFieldObject().queryCommandValue(
          goog.editor.Command.DEFAULT_TAG) ||
          goog.dom.TagName.DIV;
  var container = dh.createElement(/** @type {string} */ (tagToInsert));
  container.innerHTML = '&nbsp;';  // Prevent the div from collapsing.
  quoteNode.parentNode.insertBefore(container, secondHalf);
  dh.getWindow().getSelection().collapse(container, 0);

  // We need to account for the condition where the second blockquote
  // might contain an empty DOM tree. This arises from trying to split
  // at the end of an empty text node. We resolve this by walking up the tree
  // till we either reach the blockquote or till we hit a node with more
  // than one child. The resulting node is then removed from the DOM.
  if (textNodeToRemove) {
    goog.editor.plugins.Blockquote.findAndRemoveSingleChildAncestor_(
        textNodeToRemove, secondHalf);
  }

  goog.editor.plugins.Blockquote.removeAllWhiteSpaceNodes_(
      [quoteNode, secondHalf]);
  return true;
};


/**
 * Inserts an empty text node before the field's range.
 * @return {!Node} The empty text node.
 * @private
 */
goog.editor.plugins.Blockquote.prototype.insertEmptyTextNodeBeforeRange_ =
    function() {
  var range = this.getFieldObject().getRange();
  var node = this.getFieldDomHelper().createTextNode('');
  range.insertNode(node, true);
  return node;
};


/**
 * IE version of splitQuotedBlock_.
 * @param {Node} splitNode The current cursor position.
 * @return {boolean} Whether the blockquote was split.
 * @private
 */
goog.editor.plugins.Blockquote.prototype.splitQuotedBlockIE_ =
    function(splitNode) {
  var dh = this.getFieldDomHelper();
  var quoteNode = goog.editor.node.findTopMostEditableAncestor(
      splitNode.parentNode, goog.bind(this.isSplittableBlockquote, this));

  if (!quoteNode) {
    return false;
  }

  var clone = splitNode.cloneNode(false);

  // Whenever the cursor is just before a BR element (one|<BR>) and the user
  // presses enter, the second quoted block starts with a BR which appears
  // to the user as an extra newline. This stems from the fact that the
  // dummy span that we create (splitNode) occurs before the BR and we split
  // on that.
  if (splitNode.nextSibling &&
      splitNode.nextSibling.tagName == goog.dom.TagName.BR) {
    splitNode = splitNode.nextSibling;
  }
  var secondHalf = goog.editor.node.splitDomTreeAt(splitNode, clone, quoteNode);
  goog.dom.insertSiblingAfter(secondHalf, quoteNode);

  // Set insertion point.
  var tagToInsert =
      this.getFieldObject().queryCommandValue(
          goog.editor.Command.DEFAULT_TAG) ||
          goog.dom.TagName.DIV;
  var div = dh.createElement(/** @type {string} */ (tagToInsert));
  quoteNode.parentNode.insertBefore(div, secondHalf);

  // The div needs non-whitespace contents in order for the insertion point
  // to get correctly inserted.
  div.innerHTML = '&nbsp;';

  // Moving the range 1 char isn't enough when you have markup.
  // This moves the range to the end of the nbsp.
  var range = dh.getDocument().selection.createRange();
  range.moveToElementText(splitNode);
  range.move('character', 2);
  range.select();

  // Remove the no-longer-necessary nbsp.
  div.innerHTML = '';

  // Clear the original selection.
  range.pasteHTML('');

  // We need to remove clone from the DOM but just removing clone alone will
  // not suffice. Let's assume we have the following DOM structure and the
  // cursor is placed after the first numbered list item "one".
  //
  // <blockquote class="gmail-quote">
  //   <div><div>a</div><ol><li>one|</li></ol></div>
  //   <div>b</div>
  // </blockquote>
  //
  // After pressing enter, we have the following structure.
  //
  // <blockquote class="gmail-quote">
  //   <div><div>a</div><ol><li>one|</li></ol></div>
  // </blockquote>
  // <div>&nbsp;</div>
  // <blockquote class="gmail-quote">
  //   <div><ol><li><span id=""></span></li></ol></div>
  //   <div>b</div>
  // </blockquote>
  //
  // The clone is contained in a subtree which should be removed. This stems
  // from the fact that we invoke splitDomTreeAt with the dummy span
  // as the starting splitting point and this results in the empty subtree
  // <div><ol><li><span id=""></span></li></ol></div>.
  //
  // We resolve this by walking up the tree till we either reach the
  // blockquote or till we hit a node with more than one child. The resulting
  // node is then removed from the DOM.
  goog.editor.plugins.Blockquote.findAndRemoveSingleChildAncestor_(
      clone, secondHalf);

  goog.editor.plugins.Blockquote.removeAllWhiteSpaceNodes_(
      [quoteNode, secondHalf]);
  return true;
};