Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
seleniumhq
GitHub Repository: seleniumhq/selenium
Path: blob/trunk/third_party/closure/goog/editor/plugins/basictextformatter.js
2868 views
1
// Copyright 2006 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 Functions to style text.
17
*
18
* @author [email protected] (Nick Santos)
19
*/
20
21
goog.provide('goog.editor.plugins.BasicTextFormatter');
22
goog.provide('goog.editor.plugins.BasicTextFormatter.COMMAND');
23
24
goog.require('goog.array');
25
goog.require('goog.dom');
26
goog.require('goog.dom.NodeType');
27
goog.require('goog.dom.Range');
28
goog.require('goog.dom.TagName');
29
goog.require('goog.editor.BrowserFeature');
30
goog.require('goog.editor.Command');
31
goog.require('goog.editor.Link');
32
goog.require('goog.editor.Plugin');
33
goog.require('goog.editor.node');
34
goog.require('goog.editor.range');
35
goog.require('goog.editor.style');
36
goog.require('goog.iter');
37
goog.require('goog.iter.StopIteration');
38
goog.require('goog.log');
39
goog.require('goog.object');
40
goog.require('goog.string');
41
goog.require('goog.string.Unicode');
42
goog.require('goog.style');
43
goog.require('goog.ui.editor.messages');
44
goog.require('goog.userAgent');
45
46
47
48
/**
49
* Functions to style text (e.g. underline, make bold, etc.)
50
* @constructor
51
* @extends {goog.editor.Plugin}
52
*/
53
goog.editor.plugins.BasicTextFormatter = function() {
54
goog.editor.Plugin.call(this);
55
};
56
goog.inherits(goog.editor.plugins.BasicTextFormatter, goog.editor.Plugin);
57
58
59
/** @override */
60
goog.editor.plugins.BasicTextFormatter.prototype.getTrogClassId = function() {
61
return 'BTF';
62
};
63
64
65
/**
66
* Logging object.
67
* @type {goog.log.Logger}
68
* @protected
69
* @override
70
*/
71
goog.editor.plugins.BasicTextFormatter.prototype.logger =
72
goog.log.getLogger('goog.editor.plugins.BasicTextFormatter');
73
74
75
/**
76
* Commands implemented by this plugin.
77
* @enum {string}
78
*/
79
goog.editor.plugins.BasicTextFormatter.COMMAND = {
80
LINK: '+link',
81
CREATE_LINK: '+createLink',
82
FORMAT_BLOCK: '+formatBlock',
83
INDENT: '+indent',
84
OUTDENT: '+outdent',
85
STRIKE_THROUGH: '+strikeThrough',
86
HORIZONTAL_RULE: '+insertHorizontalRule',
87
SUBSCRIPT: '+subscript',
88
SUPERSCRIPT: '+superscript',
89
UNDERLINE: '+underline',
90
BOLD: '+bold',
91
ITALIC: '+italic',
92
FONT_SIZE: '+fontSize',
93
FONT_FACE: '+fontName',
94
FONT_COLOR: '+foreColor',
95
BACKGROUND_COLOR: '+backColor',
96
ORDERED_LIST: '+insertOrderedList',
97
UNORDERED_LIST: '+insertUnorderedList',
98
JUSTIFY_CENTER: '+justifyCenter',
99
JUSTIFY_FULL: '+justifyFull',
100
JUSTIFY_RIGHT: '+justifyRight',
101
JUSTIFY_LEFT: '+justifyLeft'
102
};
103
104
105
/**
106
* Inverse map of execCommand strings to
107
* {@link goog.editor.plugins.BasicTextFormatter.COMMAND} constants. Used to
108
* determine whether a string corresponds to a command this plugin
109
* handles in O(1) time.
110
* @type {Object}
111
* @private
112
*/
113
goog.editor.plugins.BasicTextFormatter.SUPPORTED_COMMANDS_ =
114
goog.object.transpose(goog.editor.plugins.BasicTextFormatter.COMMAND);
115
116
117
/**
118
* Whether the string corresponds to a command this plugin handles.
119
* @param {string} command Command string to check.
120
* @return {boolean} Whether the string corresponds to a command
121
* this plugin handles.
122
* @override
123
*/
124
goog.editor.plugins.BasicTextFormatter.prototype.isSupportedCommand = function(
125
command) {
126
// TODO(user): restore this to simple check once table editing
127
// is moved out into its own plugin
128
return command in goog.editor.plugins.BasicTextFormatter.SUPPORTED_COMMANDS_;
129
};
130
131
132
/**
133
* Array of execCommand strings which should be silent.
134
* @type {!Array<goog.editor.plugins.BasicTextFormatter.COMMAND>}
135
* @private
136
*/
137
goog.editor.plugins.BasicTextFormatter.SILENT_COMMANDS_ =
138
[goog.editor.plugins.BasicTextFormatter.COMMAND.CREATE_LINK];
139
140
141
/**
142
* Whether the string corresponds to a command that should be silent.
143
* @override
144
*/
145
goog.editor.plugins.BasicTextFormatter.prototype.isSilentCommand = function(
146
command) {
147
return goog.array.contains(
148
goog.editor.plugins.BasicTextFormatter.SILENT_COMMANDS_, command);
149
};
150
151
152
/**
153
* @return {goog.dom.AbstractRange} The closure range object that wraps the
154
* current user selection.
155
* @private
156
*/
157
goog.editor.plugins.BasicTextFormatter.prototype.getRange_ = function() {
158
return this.getFieldObject().getRange();
159
};
160
161
162
/**
163
* @return {!Document} The document object associated with the currently active
164
* field.
165
* @private
166
*/
167
goog.editor.plugins.BasicTextFormatter.prototype.getDocument_ = function() {
168
return this.getFieldDomHelper().getDocument();
169
};
170
171
172
/**
173
* Execute a user-initiated command.
174
* @param {string} command Command to execute.
175
* @param {...*} var_args For color commands, this
176
* should be the hex color (with the #). For FORMAT_BLOCK, this should be
177
* the goog.editor.plugins.BasicTextFormatter.BLOCK_COMMAND.
178
* It will be unused for other commands.
179
* @return {Object|undefined} The result of the command.
180
* @override
181
*/
182
goog.editor.plugins.BasicTextFormatter.prototype.execCommandInternal = function(
183
command, var_args) {
184
var preserveDir, styleWithCss, needsFormatBlockDiv, hasDummySelection;
185
var result;
186
var opt_arg = arguments[1];
187
188
switch (command) {
189
case goog.editor.plugins.BasicTextFormatter.COMMAND.BACKGROUND_COLOR:
190
// Don't bother for no color selected, color picker is resetting itself.
191
if (!goog.isNull(opt_arg)) {
192
if (goog.editor.BrowserFeature.EATS_EMPTY_BACKGROUND_COLOR) {
193
this.applyBgColorManually_(opt_arg);
194
} else if (goog.userAgent.OPERA) {
195
// backColor will color the block level element instead of
196
// the selected span of text in Opera.
197
this.execCommandHelper_('hiliteColor', opt_arg);
198
} else {
199
this.execCommandHelper_(command, opt_arg);
200
}
201
}
202
break;
203
204
case goog.editor.plugins.BasicTextFormatter.COMMAND.CREATE_LINK:
205
result = this.createLink_(arguments[1], arguments[2], arguments[3]);
206
break;
207
208
case goog.editor.plugins.BasicTextFormatter.COMMAND.LINK:
209
result = this.toggleLink_(opt_arg);
210
break;
211
212
case goog.editor.plugins.BasicTextFormatter.COMMAND.JUSTIFY_CENTER:
213
case goog.editor.plugins.BasicTextFormatter.COMMAND.JUSTIFY_FULL:
214
case goog.editor.plugins.BasicTextFormatter.COMMAND.JUSTIFY_RIGHT:
215
case goog.editor.plugins.BasicTextFormatter.COMMAND.JUSTIFY_LEFT:
216
this.justify_(command);
217
break;
218
219
default:
220
if (goog.userAgent.IE &&
221
command ==
222
goog.editor.plugins.BasicTextFormatter.COMMAND.FORMAT_BLOCK &&
223
opt_arg) {
224
// IE requires that the argument be in the form of an opening
225
// tag, like <h1>, including angle brackets. WebKit will accept
226
// the arguemnt with or without brackets, and Firefox pre-3 supports
227
// only a fixed subset of tags with brackets, and prefers without.
228
// So we only add them IE only.
229
opt_arg = '<' + opt_arg + '>';
230
}
231
232
if (command ==
233
goog.editor.plugins.BasicTextFormatter.COMMAND.FONT_COLOR &&
234
goog.isNull(opt_arg)) {
235
// If we don't have a color, then FONT_COLOR is a no-op.
236
break;
237
}
238
239
switch (command) {
240
case goog.editor.plugins.BasicTextFormatter.COMMAND.INDENT:
241
case goog.editor.plugins.BasicTextFormatter.COMMAND.OUTDENT:
242
if (goog.editor.BrowserFeature.HAS_STYLE_WITH_CSS) {
243
if (goog.userAgent.GECKO) {
244
styleWithCss = true;
245
}
246
if (goog.userAgent.OPERA) {
247
if (command ==
248
goog.editor.plugins.BasicTextFormatter.COMMAND.OUTDENT) {
249
// styleWithCSS actually sets negative margins on <blockquote>
250
// to outdent them. If the command is enabled without
251
// styleWithCSS flipped on, then the caret is in a blockquote so
252
// styleWithCSS must not be used. But if the command is not
253
// enabled, styleWithCSS should be used so that elements such as
254
// a <div> with a margin-left style can still be outdented.
255
// (Opera bug: CORE-21118)
256
styleWithCss =
257
!this.getDocument_().queryCommandEnabled('outdent');
258
} else {
259
// Always use styleWithCSS for indenting. Otherwise, Opera will
260
// make separate <blockquote>s around *each* indented line,
261
// which adds big default <blockquote> margins between each
262
// indented line.
263
styleWithCss = true;
264
}
265
}
266
}
267
// Fall through.
268
269
case goog.editor.plugins.BasicTextFormatter.COMMAND.ORDERED_LIST:
270
case goog.editor.plugins.BasicTextFormatter.COMMAND.UNORDERED_LIST:
271
if (goog.editor.BrowserFeature.LEAVES_P_WHEN_REMOVING_LISTS &&
272
this.queryCommandStateInternal_(this.getDocument_(), command)) {
273
// IE leaves behind P tags when unapplying lists.
274
// If we're not in P-mode, then we want divs
275
// So, unlistify, then convert the Ps into divs.
276
needsFormatBlockDiv =
277
this.getFieldObject().queryCommandValue(
278
goog.editor.Command.DEFAULT_TAG) != goog.dom.TagName.P;
279
} else if (!goog.editor.BrowserFeature.CAN_LISTIFY_BR) {
280
// IE doesn't convert BRed line breaks into separate list items.
281
// So convert the BRs to divs, then do the listify.
282
this.convertBreaksToDivs_();
283
}
284
285
// This fix only works in Gecko.
286
if (goog.userAgent.GECKO &&
287
goog.editor.BrowserFeature.FORGETS_FORMATTING_WHEN_LISTIFYING &&
288
!this.queryCommandValue(command)) {
289
hasDummySelection |= this.beforeInsertListGecko_();
290
}
291
// Fall through to preserveDir block
292
293
case goog.editor.plugins.BasicTextFormatter.COMMAND.FORMAT_BLOCK:
294
// Both FF & IE may lose directionality info. Save/restore it.
295
// TODO(user): Does Safari also need this?
296
// TODO (gmark, jparent): This isn't ideal because it uses a string
297
// literal, so if the plugin name changes, it would break. We need a
298
// better solution. See also other places in code that use
299
// this.getPluginByClassId('Bidi').
300
preserveDir = !!this.getFieldObject().getPluginByClassId('Bidi');
301
break;
302
303
case goog.editor.plugins.BasicTextFormatter.COMMAND.SUBSCRIPT:
304
case goog.editor.plugins.BasicTextFormatter.COMMAND.SUPERSCRIPT:
305
if (goog.editor.BrowserFeature.NESTS_SUBSCRIPT_SUPERSCRIPT) {
306
// This browser nests subscript and superscript when both are
307
// applied, instead of canceling out the first when applying the
308
// second.
309
this.applySubscriptSuperscriptWorkarounds_(command);
310
}
311
break;
312
313
case goog.editor.plugins.BasicTextFormatter.COMMAND.UNDERLINE:
314
case goog.editor.plugins.BasicTextFormatter.COMMAND.BOLD:
315
case goog.editor.plugins.BasicTextFormatter.COMMAND.ITALIC:
316
// If we are applying the formatting, then we want to have
317
// styleWithCSS false so that we generate html tags (like <b>). If we
318
// are unformatting something, we want to have styleWithCSS true so
319
// that we can unformat both html tags and inline styling.
320
// TODO(user): What about WebKit and Opera?
321
styleWithCss = goog.userAgent.GECKO &&
322
goog.editor.BrowserFeature.HAS_STYLE_WITH_CSS &&
323
this.queryCommandValue(command);
324
break;
325
326
case goog.editor.plugins.BasicTextFormatter.COMMAND.FONT_COLOR:
327
case goog.editor.plugins.BasicTextFormatter.COMMAND.FONT_FACE:
328
// It is very expensive in FF (order of magnitude difference) to use
329
// font tags instead of styled spans. Whenever possible,
330
// force FF to use spans.
331
// Font size is very expensive too, but FF always uses font tags,
332
// regardless of which styleWithCSS value you use.
333
styleWithCss = goog.editor.BrowserFeature.HAS_STYLE_WITH_CSS &&
334
goog.userAgent.GECKO;
335
}
336
337
/**
338
* Cases where we just use the default execCommand (in addition
339
* to the above fall-throughs)
340
* goog.editor.plugins.BasicTextFormatter.COMMAND.STRIKE_THROUGH:
341
* goog.editor.plugins.BasicTextFormatter.COMMAND.HORIZONTAL_RULE:
342
* goog.editor.plugins.BasicTextFormatter.COMMAND.SUBSCRIPT:
343
* goog.editor.plugins.BasicTextFormatter.COMMAND.SUPERSCRIPT:
344
* goog.editor.plugins.BasicTextFormatter.COMMAND.UNDERLINE:
345
* goog.editor.plugins.BasicTextFormatter.COMMAND.BOLD:
346
* goog.editor.plugins.BasicTextFormatter.COMMAND.ITALIC:
347
* goog.editor.plugins.BasicTextFormatter.COMMAND.FONT_SIZE:
348
* goog.editor.plugins.BasicTextFormatter.COMMAND.FONT_FACE:
349
*/
350
this.execCommandHelper_(command, opt_arg, preserveDir, !!styleWithCss);
351
352
if (hasDummySelection) {
353
this.getDocument_().execCommand('Delete', false, true);
354
}
355
356
if (needsFormatBlockDiv) {
357
this.getDocument_().execCommand('FormatBlock', false, '<div>');
358
}
359
}
360
// FF loses focus, so we have to set the focus back to the document or the
361
// user can't type after selecting from menu. In IE, focus is set correctly
362
// and resetting it here messes it up.
363
if (goog.userAgent.GECKO && !this.getFieldObject().inModalMode()) {
364
this.focusField_();
365
}
366
return result;
367
};
368
369
370
/**
371
* Focuses on the field.
372
* @private
373
*/
374
goog.editor.plugins.BasicTextFormatter.prototype.focusField_ = function() {
375
this.getFieldDomHelper().getWindow().focus();
376
};
377
378
379
/**
380
* Gets the command value.
381
* @param {string} command The command value to get.
382
* @return {string|boolean|null} The current value of the command in the given
383
* selection. NOTE: This return type list is not documented in MSDN or MDC
384
* and has been constructed from experience. Please update it
385
* if necessary.
386
* @override
387
*/
388
goog.editor.plugins.BasicTextFormatter.prototype.queryCommandValue = function(
389
command) {
390
var styleWithCss;
391
switch (command) {
392
case goog.editor.plugins.BasicTextFormatter.COMMAND.LINK:
393
return this.isNodeInState_(goog.dom.TagName.A);
394
395
case goog.editor.plugins.BasicTextFormatter.COMMAND.JUSTIFY_CENTER:
396
case goog.editor.plugins.BasicTextFormatter.COMMAND.JUSTIFY_FULL:
397
case goog.editor.plugins.BasicTextFormatter.COMMAND.JUSTIFY_RIGHT:
398
case goog.editor.plugins.BasicTextFormatter.COMMAND.JUSTIFY_LEFT:
399
return this.isJustification_(command);
400
401
case goog.editor.plugins.BasicTextFormatter.COMMAND.FORMAT_BLOCK:
402
// TODO(nicksantos): See if we can use queryCommandValue here.
403
return goog.editor.plugins.BasicTextFormatter.getSelectionBlockState_(
404
this.getFieldObject().getRange());
405
406
case goog.editor.plugins.BasicTextFormatter.COMMAND.INDENT:
407
case goog.editor.plugins.BasicTextFormatter.COMMAND.OUTDENT:
408
case goog.editor.plugins.BasicTextFormatter.COMMAND.HORIZONTAL_RULE:
409
// TODO: See if there are reasonable results to return for
410
// these commands.
411
return false;
412
413
case goog.editor.plugins.BasicTextFormatter.COMMAND.FONT_SIZE:
414
case goog.editor.plugins.BasicTextFormatter.COMMAND.FONT_FACE:
415
case goog.editor.plugins.BasicTextFormatter.COMMAND.FONT_COLOR:
416
case goog.editor.plugins.BasicTextFormatter.COMMAND.BACKGROUND_COLOR:
417
// We use queryCommandValue here since we don't just want to know if a
418
// color/fontface/fontsize is applied, we want to know WHICH one it is.
419
return this.queryCommandValueInternal_(
420
this.getDocument_(), command,
421
goog.editor.BrowserFeature.HAS_STYLE_WITH_CSS &&
422
goog.userAgent.GECKO);
423
424
case goog.editor.plugins.BasicTextFormatter.COMMAND.UNDERLINE:
425
case goog.editor.plugins.BasicTextFormatter.COMMAND.BOLD:
426
case goog.editor.plugins.BasicTextFormatter.COMMAND.ITALIC:
427
styleWithCss =
428
goog.editor.BrowserFeature.HAS_STYLE_WITH_CSS && goog.userAgent.GECKO;
429
430
default:
431
/**
432
* goog.editor.plugins.BasicTextFormatter.COMMAND.STRIKE_THROUGH
433
* goog.editor.plugins.BasicTextFormatter.COMMAND.SUBSCRIPT
434
* goog.editor.plugins.BasicTextFormatter.COMMAND.SUPERSCRIPT
435
* goog.editor.plugins.BasicTextFormatter.COMMAND.UNDERLINE
436
* goog.editor.plugins.BasicTextFormatter.COMMAND.BOLD
437
* goog.editor.plugins.BasicTextFormatter.COMMAND.ITALIC
438
* goog.editor.plugins.BasicTextFormatter.COMMAND.ORDERED_LIST
439
* goog.editor.plugins.BasicTextFormatter.COMMAND.UNORDERED_LIST
440
*/
441
// This only works for commands that use the default execCommand
442
return this.queryCommandStateInternal_(
443
this.getDocument_(), command, styleWithCss);
444
}
445
};
446
447
448
/**
449
* @override
450
*/
451
goog.editor.plugins.BasicTextFormatter.prototype.prepareContentsHtml = function(
452
html) {
453
// If the browser collapses empty nodes and the field has only a script
454
// tag in it, then it will collapse this node. Which will mean the user
455
// can't click into it to edit it.
456
if (goog.editor.BrowserFeature.COLLAPSES_EMPTY_NODES &&
457
html.match(/^\s*<script/i)) {
458
html = '&nbsp;' + html;
459
}
460
461
if (goog.editor.BrowserFeature.CONVERT_TO_B_AND_I_TAGS) {
462
// Some browsers (FF) can't undo strong/em in some cases, but can undo b/i!
463
html = html.replace(/<(\/?)strong([^\w])/gi, '<$1b$2');
464
html = html.replace(/<(\/?)em([^\w])/gi, '<$1i$2');
465
}
466
467
return html;
468
};
469
470
471
/**
472
* @override
473
*/
474
goog.editor.plugins.BasicTextFormatter.prototype.cleanContentsDom = function(
475
fieldCopy) {
476
var images = goog.dom.getElementsByTagName(goog.dom.TagName.IMG, fieldCopy);
477
for (var i = 0, image; image = images[i]; i++) {
478
if (goog.editor.BrowserFeature.SHOWS_CUSTOM_ATTRS_IN_INNER_HTML) {
479
// Only need to remove these attributes in IE because
480
// Firefox and Safari don't show custom attributes in the innerHTML.
481
image.removeAttribute('tabIndex');
482
image.removeAttribute('tabIndexSet');
483
goog.removeUid(image);
484
485
// Declare oldTypeIndex for the compiler. The associated plugin may not be
486
// included in the compiled bundle.
487
/** @type {number} */ image.oldTabIndex;
488
489
// oldTabIndex will only be set if
490
// goog.editor.BrowserFeature.TABS_THROUGH_IMAGES is true and we're in
491
// P-on-enter mode.
492
if (image.oldTabIndex) {
493
image.tabIndex = image.oldTabIndex;
494
}
495
}
496
}
497
};
498
499
500
/**
501
* @override
502
*/
503
goog.editor.plugins.BasicTextFormatter.prototype.cleanContentsHtml = function(
504
html) {
505
if (goog.editor.BrowserFeature.MOVES_STYLE_TO_HEAD) {
506
// Safari creates a new <head> element for <style> tags, so prepend their
507
// contents to the output.
508
var heads = this.getFieldObject()
509
.getEditableDomHelper()
510
.getElementsByTagNameAndClass(goog.dom.TagName.HEAD);
511
var stylesHtmlArr = [];
512
513
// i starts at 1 so we don't copy in the original, legitimate <head>.
514
var numHeads = heads.length;
515
for (var i = 1; i < numHeads; ++i) {
516
var styles =
517
goog.dom.getElementsByTagName(goog.dom.TagName.STYLE, heads[i]);
518
var numStyles = styles.length;
519
for (var j = 0; j < numStyles; ++j) {
520
stylesHtmlArr.push(styles[j].outerHTML);
521
}
522
}
523
return stylesHtmlArr.join('') + html;
524
}
525
526
return html;
527
};
528
529
530
/**
531
* @override
532
*/
533
goog.editor.plugins.BasicTextFormatter.prototype.handleKeyboardShortcut =
534
function(e, key, isModifierPressed) {
535
if (!isModifierPressed) {
536
return false;
537
}
538
var command;
539
switch (key) {
540
case 'b': // Ctrl+B
541
command = goog.editor.plugins.BasicTextFormatter.COMMAND.BOLD;
542
break;
543
case 'i': // Ctrl+I
544
command = goog.editor.plugins.BasicTextFormatter.COMMAND.ITALIC;
545
break;
546
case 'u': // Ctrl+U
547
command = goog.editor.plugins.BasicTextFormatter.COMMAND.UNDERLINE;
548
break;
549
case 's': // Ctrl+S
550
// TODO(user): This doesn't belong in here. Clients should handle
551
// this themselves.
552
// Catching control + s prevents the annoying browser save dialog
553
// from appearing.
554
return true;
555
}
556
557
if (command) {
558
this.getFieldObject().execCommand(command);
559
return true;
560
}
561
562
return false;
563
};
564
565
566
// Helpers for execCommand
567
568
569
/**
570
* Regular expression to match BRs in HTML. Saves the BRs' attributes in $1 for
571
* use with replace(). In non-IE browsers, does not match BRs adjacent to an
572
* opening or closing DIV or P tag, since nonrendered BR elements can occur at
573
* the end of block level containers in those browsers' editors.
574
* @type {RegExp}
575
* @private
576
*/
577
goog.editor.plugins.BasicTextFormatter.BR_REGEXP_ = goog.userAgent.IE ?
578
/<br([^\/>]*)\/?>/gi :
579
/<br([^\/>]*)\/?>(?!<\/(div|p)>)/gi;
580
581
582
/**
583
* Convert BRs in the selection to divs.
584
* This is only intended to be used in IE and Opera.
585
* @return {boolean} Whether any BR's were converted.
586
* @private
587
*/
588
goog.editor.plugins.BasicTextFormatter.prototype.convertBreaksToDivs_ =
589
function() {
590
if (!goog.userAgent.IE && !goog.userAgent.OPERA) {
591
// This function is only supported on IE and Opera.
592
return false;
593
}
594
var range = this.getRange_();
595
var parent = range.getContainerElement();
596
var doc = this.getDocument_();
597
var dom = this.getFieldDomHelper();
598
599
goog.editor.plugins.BasicTextFormatter.BR_REGEXP_.lastIndex = 0;
600
// Only mess with the HTML/selection if it contains a BR.
601
if (goog.editor.plugins.BasicTextFormatter.BR_REGEXP_.test(
602
parent.innerHTML)) {
603
// Insert temporary markers to remember the selection.
604
var savedRange = range.saveUsingCarets();
605
606
if (parent.tagName == goog.dom.TagName.P) {
607
// Can't append paragraphs to paragraph tags. Throws an exception in IE.
608
goog.editor.plugins.BasicTextFormatter.convertParagraphToDiv_(
609
parent, true);
610
} else {
611
// Used to do:
612
// IE: <div>foo<br>bar</div> --> <div>foo<p id="temp_br">bar</div>
613
// Opera: <div>foo<br>bar</div> --> <div>foo<p class="temp_br">bar</div>
614
// To fix bug 1939883, now does for both:
615
// <div>foo<br>bar</div> --> <div>foo<p trtempbr="temp_br">bar</div>
616
// TODO(user): Confirm if there's any way to skip this
617
// intermediate step of converting br's to p's before converting those to
618
// div's. The reason may be hidden in CLs 5332866 and 8530601.
619
var attribute = 'trtempbr';
620
var value = 'temp_br';
621
var newHtml = parent.innerHTML.replace(
622
goog.editor.plugins.BasicTextFormatter.BR_REGEXP_,
623
'<p$1 ' + attribute + '="' + value + '">');
624
goog.editor.node.replaceInnerHtml(parent, newHtml);
625
626
var paragraphs = goog.array.toArray(
627
goog.dom.getElementsByTagName(goog.dom.TagName.P, parent));
628
goog.iter.forEach(paragraphs, function(paragraph) {
629
if (paragraph.getAttribute(attribute) == value) {
630
paragraph.removeAttribute(attribute);
631
if (goog.string.isBreakingWhitespace(
632
goog.dom.getTextContent(paragraph))) {
633
// Prevent the empty blocks from collapsing.
634
// A <BR> is preferable because it doesn't result in any text being
635
// added to the "blank" line. In IE, however, it is possible to
636
// place the caret after the <br>, which effectively creates a
637
// visible line break. Because of this, we have to resort to using a
638
// &nbsp; in IE.
639
var child = goog.userAgent.IE ?
640
doc.createTextNode(goog.string.Unicode.NBSP) :
641
dom.createElement(goog.dom.TagName.BR);
642
paragraph.appendChild(child);
643
}
644
goog.editor.plugins.BasicTextFormatter.convertParagraphToDiv_(
645
paragraph);
646
}
647
});
648
}
649
650
// Select the previously selected text so we only listify
651
// the selected portion and maintain the user's selection.
652
savedRange.restore();
653
return true;
654
}
655
656
return false;
657
};
658
659
660
/**
661
* Convert the given paragraph to being a div. This clobbers the
662
* passed-in node!
663
* This is only intended to be used in IE and Opera.
664
* @param {Node} paragraph Paragragh to convert to a div.
665
* @param {boolean=} opt_convertBrs If true, also convert BRs to divs.
666
* @private
667
*/
668
goog.editor.plugins.BasicTextFormatter.convertParagraphToDiv_ = function(
669
paragraph, opt_convertBrs) {
670
if (!goog.userAgent.IE && !goog.userAgent.OPERA) {
671
// This function is only supported on IE and Opera.
672
return;
673
}
674
var outerHTML = paragraph.outerHTML.replace(/<(\/?)p/gi, '<$1div');
675
if (opt_convertBrs) {
676
// IE fills in the closing div tag if it's missing!
677
outerHTML = outerHTML.replace(
678
goog.editor.plugins.BasicTextFormatter.BR_REGEXP_, '</div><div$1>');
679
}
680
if (goog.userAgent.OPERA && !/<\/div>$/i.test(outerHTML)) {
681
// Opera doesn't automatically add the closing tag, so add it if needed.
682
outerHTML += '</div>';
683
}
684
paragraph.outerHTML = outerHTML;
685
};
686
687
688
/**
689
* If this is a goog.editor.plugins.BasicTextFormatter.COMMAND,
690
* convert it to something that we can pass into execCommand,
691
* queryCommandState, etc.
692
*
693
* TODO(user): Consider doing away with the + and converter completely.
694
*
695
* @param {goog.editor.plugins.BasicTextFormatter.COMMAND|string}
696
* command A command key.
697
* @return {string} The equivalent execCommand command.
698
* @private
699
*/
700
goog.editor.plugins.BasicTextFormatter.convertToRealExecCommand_ = function(
701
command) {
702
return command.indexOf('+') == 0 ? command.substring(1) : command;
703
};
704
705
706
/**
707
* Justify the text in the selection.
708
* @param {string} command The type of justification to perform.
709
* @private
710
*/
711
goog.editor.plugins.BasicTextFormatter.prototype.justify_ = function(command) {
712
this.execCommandHelper_(command, null, false, true);
713
// Firefox cannot justify divs. In fact, justifying divs results in removing
714
// the divs and replacing them with brs. So "<div>foo</div><div>bar</div>"
715
// becomes "foo<br>bar" after alignment is applied. However, if you justify
716
// again, then you get "<div style='text-align: right'>foo<br>bar</div>",
717
// which at least looks visually correct. Since justification is (normally)
718
// idempotent, it isn't a problem when the selection does not contain divs to
719
// apply justifcation again.
720
if (goog.userAgent.GECKO) {
721
this.execCommandHelper_(command, null, false, true);
722
}
723
724
// Convert all block elements in the selection to use CSS text-align
725
// instead of the align property. This works better because the align
726
// property is overridden by the CSS text-align property.
727
//
728
// Only for browsers that can't handle this by the styleWithCSS execCommand,
729
// which allows us to specify if we should insert align or text-align.
730
// TODO(user): What about WebKit or Opera?
731
if (!(goog.editor.BrowserFeature.HAS_STYLE_WITH_CSS &&
732
goog.userAgent.GECKO)) {
733
goog.iter.forEach(
734
this.getFieldObject().getRange(),
735
goog.editor.plugins.BasicTextFormatter.convertContainerToTextAlign_);
736
}
737
};
738
739
740
/**
741
* Converts the block element containing the given node to use CSS text-align
742
* instead of the align property.
743
* @param {Node} node The node to convert the container of.
744
* @private
745
*/
746
goog.editor.plugins.BasicTextFormatter.convertContainerToTextAlign_ = function(
747
node) {
748
var container = goog.editor.style.getContainer(node);
749
750
// TODO(user): Fix this so that it doesn't screw up tables.
751
if (container.align) {
752
container.style.textAlign = container.align;
753
container.removeAttribute('align');
754
}
755
};
756
757
758
/**
759
* Perform an execCommand on the active document.
760
* @param {string} command The command to execute.
761
* @param {string|number|boolean|null=} opt_value Optional value.
762
* @param {boolean=} opt_preserveDir Set true to make sure that command does not
763
* change directionality of the selected text (works only if all selected
764
* text has the same directionality, otherwise ignored). Should not be true
765
* if bidi plugin is not loaded.
766
* @param {boolean=} opt_styleWithCss Set to true to ask the browser to use CSS
767
* to perform the execCommand.
768
* @private
769
*/
770
goog.editor.plugins.BasicTextFormatter.prototype.execCommandHelper_ = function(
771
command, opt_value, opt_preserveDir, opt_styleWithCss) {
772
// There is a bug in FF: some commands do not preserve attributes of the
773
// block-level elements they replace.
774
// This (among the rest) leads to loss of directionality information.
775
// For now we use a hack (when opt_preserveDir==true) to avoid this
776
// directionality problem in the simplest cases.
777
// Known affected commands: formatBlock, insertOrderedList,
778
// insertUnorderedList, indent, outdent.
779
// A similar problem occurs in IE when insertOrderedList or
780
// insertUnorderedList remove existing list.
781
var dir = null;
782
if (opt_preserveDir) {
783
dir = this.getFieldObject().queryCommandValue(goog.editor.Command.DIR_RTL) ?
784
'rtl' :
785
this.getFieldObject().queryCommandValue(goog.editor.Command.DIR_LTR) ?
786
'ltr' :
787
null;
788
}
789
790
command =
791
goog.editor.plugins.BasicTextFormatter.convertToRealExecCommand_(command);
792
793
var endDiv, nbsp;
794
if (goog.userAgent.IE) {
795
var ret = this.applyExecCommandIEFixes_(command);
796
endDiv = ret[0];
797
nbsp = ret[1];
798
}
799
800
if (goog.userAgent.WEBKIT) {
801
endDiv = this.applyExecCommandSafariFixes_(command);
802
}
803
804
if (goog.userAgent.GECKO) {
805
this.applyExecCommandGeckoFixes_(command);
806
}
807
808
if (goog.editor.BrowserFeature.DOESNT_OVERRIDE_FONT_SIZE_IN_STYLE_ATTR &&
809
command.toLowerCase() == 'fontsize') {
810
this.removeFontSizeFromStyleAttrs_();
811
}
812
813
var doc = this.getDocument_();
814
if (opt_styleWithCss && goog.editor.BrowserFeature.HAS_STYLE_WITH_CSS) {
815
doc.execCommand('styleWithCSS', false, true);
816
if (goog.userAgent.OPERA) {
817
this.invalidateInlineCss_();
818
}
819
}
820
821
doc.execCommand(command, false, opt_value);
822
if (opt_styleWithCss && goog.editor.BrowserFeature.HAS_STYLE_WITH_CSS) {
823
// If we enabled styleWithCSS, turn it back off.
824
doc.execCommand('styleWithCSS', false, false);
825
}
826
827
if (goog.userAgent.WEBKIT && !goog.userAgent.isVersionOrHigher('526') &&
828
command.toLowerCase() == 'formatblock' && opt_value &&
829
/^[<]?h\d[>]?$/i.test(opt_value)) {
830
this.cleanUpSafariHeadings_();
831
}
832
833
if (/insert(un)?orderedlist/i.test(command)) {
834
// NOTE(user): This doesn't check queryCommandState because it seems to
835
// lie. Also, this runs for insertunorderedlist so that the the list
836
// isn't made up of an <ul> for each <li> - even though it looks the same,
837
// the markup is disgusting.
838
if (goog.userAgent.WEBKIT && !goog.userAgent.isVersionOrHigher(534)) {
839
this.fixSafariLists_();
840
}
841
if (goog.userAgent.IE) {
842
this.fixIELists_();
843
844
if (nbsp) {
845
// Remove the text node, if applicable. Do not try to instead clobber
846
// the contents of the text node if it was added, or the same invalid
847
// node thing as above will happen. The error won't happen here, it
848
// will happen after you hit enter and then do anything that loops
849
// through the dom and tries to read that node.
850
goog.dom.removeNode(nbsp);
851
}
852
}
853
}
854
855
if (endDiv) {
856
// Remove the dummy div.
857
goog.dom.removeNode(endDiv);
858
}
859
860
// Restore directionality if required and only when unambigous (dir!=null).
861
if (dir) {
862
this.getFieldObject().execCommand(dir);
863
}
864
};
865
866
867
/**
868
* Applies a background color to a selection when the browser can't do the job.
869
*
870
* NOTE(nicksantos): If you think this is hacky, you should try applying
871
* background color in Opera. It made me cry.
872
*
873
* @param {string} bgColor backgroundColor from .formatText to .execCommand.
874
* @private
875
*/
876
goog.editor.plugins.BasicTextFormatter.prototype.applyBgColorManually_ =
877
function(bgColor) {
878
var needsSpaceInTextNode = goog.userAgent.GECKO;
879
var range = this.getFieldObject().getRange();
880
var textNode;
881
var parentTag;
882
if (range && range.isCollapsed()) {
883
// Hack to handle Firefox bug:
884
// https://bugzilla.mozilla.org/show_bug.cgi?id=279330
885
// execCommand hiliteColor in Firefox on collapsed selection creates
886
// a font tag onkeypress
887
textNode = this.getFieldDomHelper().createTextNode(
888
needsSpaceInTextNode ? ' ' : '');
889
890
var containerNode = range.getStartNode();
891
// Check if we're inside a tag that contains the cursor and nothing else;
892
// if we are, don't create a dummySpan. Just use this containing tag to
893
// hide the 1-space selection.
894
// If the user sets a background color on a collapsed selection, then sets
895
// another one immediately, we get a span tag with a single empty TextNode.
896
// If the user sets a background color, types, then backspaces, we get a
897
// span tag with nothing inside it (container is the span).
898
parentTag = containerNode.nodeType == goog.dom.NodeType.ELEMENT ?
899
containerNode :
900
containerNode.parentNode;
901
902
if (parentTag.innerHTML == '') {
903
// There's an Element to work with
904
// make the space character invisible using a CSS indent hack
905
parentTag.style.textIndent = '-10000px';
906
parentTag.appendChild(textNode);
907
} else {
908
// No Element to work with; make one
909
// create a span with a space character inside
910
// make the space character invisible using a CSS indent hack
911
parentTag = this.getFieldDomHelper().createDom(
912
goog.dom.TagName.SPAN, {'style': 'text-indent:-10000px'}, textNode);
913
range.replaceContentsWithNode(parentTag);
914
}
915
goog.dom.Range.createFromNodeContents(textNode).select();
916
}
917
918
this.execCommandHelper_('hiliteColor', bgColor, false, true);
919
920
if (textNode) {
921
// eliminate the space if necessary.
922
if (needsSpaceInTextNode) {
923
textNode.data = '';
924
}
925
926
// eliminate the hack.
927
parentTag.style.textIndent = '';
928
// execCommand modified our span so we leave it in place.
929
}
930
};
931
932
933
/**
934
* Toggle link for the current selection:
935
* If selection contains a link, unlink it, return null.
936
* Otherwise, make selection into a link, return the link.
937
* @param {string=} opt_target Target for the link.
938
* @return {goog.editor.Link?} The resulting link, or null if a link was
939
* removed.
940
* @private
941
*/
942
goog.editor.plugins.BasicTextFormatter.prototype.toggleLink_ = function(
943
opt_target) {
944
if (!this.getFieldObject().isSelectionEditable()) {
945
this.focusField_();
946
}
947
948
var range = this.getRange_();
949
// Since we wrap images in links, its possible that the user selected an
950
// image and clicked link, in which case we want to actually use the
951
// image as the selection.
952
var parent = range && range.getContainerElement();
953
var link = /** @type {Element} */ (
954
goog.dom.getAncestorByTagNameAndClass(parent, goog.dom.TagName.A));
955
if (link && goog.editor.node.isEditable(link)) {
956
goog.dom.flattenElement(link);
957
} else {
958
var editableLink = this.createLink_(range, '/', opt_target);
959
if (editableLink) {
960
if (!this.getFieldObject().execCommand(
961
goog.editor.Command.MODAL_LINK_EDITOR, editableLink)) {
962
var url = this.getFieldObject().getAppWindow().prompt(
963
goog.ui.editor.messages.MSG_LINK_TO, 'http://');
964
if (url) {
965
editableLink.setTextAndUrl(editableLink.getCurrentText() || url, url);
966
editableLink.placeCursorRightOf();
967
} else {
968
var savedRange = goog.editor.range.saveUsingNormalizedCarets(
969
goog.dom.Range.createFromNodeContents(editableLink.getAnchor()));
970
editableLink.removeLink();
971
savedRange.restore().select();
972
return null;
973
}
974
}
975
return editableLink;
976
}
977
}
978
return null;
979
};
980
981
982
/**
983
* Create a link out of the current selection. If nothing is selected, insert
984
* a new link. Otherwise, enclose the selection in a link.
985
* @param {goog.dom.AbstractRange} range The closure range object for the
986
* current selection.
987
* @param {string} url The url to link to.
988
* @param {string=} opt_target Target for the link.
989
* @return {goog.editor.Link?} The newly created link, or null if the link
990
* couldn't be created.
991
* @private
992
*/
993
goog.editor.plugins.BasicTextFormatter.prototype.createLink_ = function(
994
range, url, opt_target) {
995
var anchor = null;
996
var anchors = [];
997
var parent = range && range.getContainerElement();
998
// We do not yet support creating links around images. Instead of throwing
999
// lots of js errors, just fail silently.
1000
// TODO(user): Add support for linking images.
1001
if (parent && parent.tagName == goog.dom.TagName.IMG) {
1002
return null;
1003
}
1004
// If range is not present, the editable field doesn't have focus, abort
1005
// creating a link.
1006
if (!range) {
1007
return null;
1008
}
1009
1010
if (range.isCollapsed()) {
1011
var textRange = range.getTextRange(0).getBrowserRangeObject();
1012
if (goog.editor.BrowserFeature.HAS_W3C_RANGES) {
1013
anchor = this.getFieldDomHelper().createElement(goog.dom.TagName.A);
1014
textRange.insertNode(anchor);
1015
} else if (goog.editor.BrowserFeature.HAS_IE_RANGES) {
1016
// TODO: Use goog.dom.AbstractRange's surroundContents
1017
textRange.pasteHTML("<a id='newLink'></a>");
1018
anchor = this.getFieldDomHelper().getElement('newLink');
1019
anchor.removeAttribute('id');
1020
}
1021
} else {
1022
// Create a unique identifier for the link so we can retrieve it later.
1023
// execCommand doesn't return the link to us, and we need a way to find
1024
// the newly created link in the dom, and the url is the only property
1025
// we have control over, so we set that to be unique and then find it.
1026
var uniqueId = goog.string.createUniqueString();
1027
this.execCommandHelper_('CreateLink', uniqueId);
1028
var setHrefAndLink = function(element, index, arr) {
1029
// We can't do straight comparison since the href can contain the
1030
// absolute url.
1031
if (goog.string.endsWith(element.href, uniqueId)) {
1032
anchors.push(element);
1033
}
1034
};
1035
1036
goog.array.forEach(
1037
goog.dom.getElementsByTagName(
1038
goog.dom.TagName.A,
1039
/** @type {!Element} */ (this.getFieldObject().getElement())),
1040
setHrefAndLink);
1041
if (anchors.length) {
1042
anchor = anchors.pop();
1043
}
1044
var isLikelyUrl = function(a, i, anchors) {
1045
return goog.editor.Link.isLikelyUrl(goog.dom.getRawTextContent(a));
1046
};
1047
if (anchors.length && goog.array.every(anchors, isLikelyUrl)) {
1048
for (var i = 0, a; a = anchors[i]; i++) {
1049
goog.editor.Link.createNewLinkFromText(a, opt_target);
1050
}
1051
anchors = null;
1052
}
1053
}
1054
1055
return goog.editor.Link.createNewLink(
1056
/** @type {HTMLAnchorElement} */ (anchor), url, opt_target, anchors);
1057
};
1058
1059
1060
//---------------------------------------------------------------------
1061
// browser fixes
1062
1063
1064
/**
1065
* The following execCommands are "broken" in some way - in IE they allow
1066
* the nodes outside the contentEditable region to get modified (see
1067
* execCommand below for more details).
1068
* @const
1069
* @private
1070
*/
1071
goog.editor.plugins.BasicTextFormatter.brokenExecCommandsIE_ = {
1072
'indent': 1,
1073
'outdent': 1,
1074
'insertOrderedList': 1,
1075
'insertUnorderedList': 1,
1076
'justifyCenter': 1,
1077
'justifyFull': 1,
1078
'justifyRight': 1,
1079
'justifyLeft': 1,
1080
'ltr': 1,
1081
'rtl': 1
1082
};
1083
1084
1085
/**
1086
* When the following commands are executed while the selection is
1087
* inside a blockquote, they hose the blockquote tag in weird and
1088
* unintuitive ways.
1089
* @const
1090
* @private
1091
*/
1092
goog.editor.plugins.BasicTextFormatter.blockquoteHatingCommandsIE_ = {
1093
'insertOrderedList': 1,
1094
'insertUnorderedList': 1
1095
};
1096
1097
1098
/**
1099
* Makes sure that superscript is removed before applying subscript, and vice
1100
* versa. Fixes {@link http://buganizer/issue?id=1173491} .
1101
* @param {goog.editor.plugins.BasicTextFormatter.COMMAND} command The command
1102
* being applied, either SUBSCRIPT or SUPERSCRIPT.
1103
* @private
1104
*/
1105
goog.editor.plugins.BasicTextFormatter.prototype
1106
.applySubscriptSuperscriptWorkarounds_ = function(command) {
1107
if (!this.queryCommandValue(command)) {
1108
// The current selection doesn't currently have the requested
1109
// command, so we are applying it as opposed to removing it.
1110
// (Note that queryCommandValue() will only return true if the
1111
// command is applied to the whole selection, not just part of it.
1112
// In this case it is fine because only if the whole selection has
1113
// the command applied will we be removing it and thus skipping the
1114
// removal of the opposite command.)
1115
var oppositeCommand =
1116
(command == goog.editor.plugins.BasicTextFormatter.COMMAND.SUBSCRIPT ?
1117
goog.editor.plugins.BasicTextFormatter.COMMAND.SUPERSCRIPT :
1118
goog.editor.plugins.BasicTextFormatter.COMMAND.SUBSCRIPT);
1119
var oppositeExecCommand =
1120
goog.editor.plugins.BasicTextFormatter.convertToRealExecCommand_(
1121
oppositeCommand);
1122
// Executing the opposite command on a selection that already has it
1123
// applied will cancel it out. But if the selection only has the
1124
// opposite command applied to a part of it, the browser will
1125
// normalize the selection to have the opposite command applied on
1126
// the whole of it.
1127
if (!this.queryCommandValue(oppositeCommand)) {
1128
// The selection doesn't have the opposite command applied to the
1129
// whole of it, so let's exec the opposite command to normalize
1130
// the selection.
1131
// Note: since we know both subscript and superscript commands
1132
// will boil down to a simple call to the browser's execCommand(),
1133
// for performance reasons we can do that directly instead of
1134
// calling execCommandHelper_(). However this is a potential for
1135
// bugs if the implementation of execCommandHelper_() is changed
1136
// to do something more int eh case of subscript and superscript.
1137
this.getDocument_().execCommand(oppositeExecCommand, false, null);
1138
}
1139
// Now that we know the whole selection has the opposite command
1140
// applied, we exec it a second time to properly remove it.
1141
this.getDocument_().execCommand(oppositeExecCommand, false, null);
1142
}
1143
};
1144
1145
1146
/**
1147
* Removes inline font-size styles from elements fully contained in the
1148
* selection, so the font tags produced by execCommand work properly.
1149
* See {@bug 1286408}.
1150
* @private
1151
*/
1152
goog.editor.plugins.BasicTextFormatter.prototype.removeFontSizeFromStyleAttrs_ =
1153
function() {
1154
// Expand the range so that we consider surrounding tags. E.g. if only the
1155
// text node inside a span is selected, the browser could wrap a font tag
1156
// around the span and leave the selection such that only the text node is
1157
// found when looking inside the range, not the span.
1158
var range = goog.editor.range.expand(
1159
this.getFieldObject().getRange(), this.getFieldObject().getElement());
1160
goog.iter.forEach(goog.iter.filter(range, function(tag, dummy, iter) {
1161
return iter.isStartTag() && range.containsNode(tag);
1162
}), function(node) {
1163
goog.style.setStyle(node, 'font-size', '');
1164
// Gecko doesn't remove empty style tags.
1165
if (goog.userAgent.GECKO && node.style.length == 0 &&
1166
node.getAttribute('style') != null) {
1167
node.removeAttribute('style');
1168
}
1169
});
1170
};
1171
1172
1173
/**
1174
* Apply pre-execCommand fixes for IE.
1175
* @param {string} command The command to execute.
1176
* @return {!Array<Node>} Array of nodes to be removed after the execCommand.
1177
* Will never be longer than 2 elements.
1178
* @private
1179
*/
1180
goog.editor.plugins.BasicTextFormatter.prototype.applyExecCommandIEFixes_ =
1181
function(command) {
1182
// IE has a crazy bug where executing list commands
1183
// around blockquotes cause the blockquotes to get transformed
1184
// into "<OL><OL>" or "<UL><UL>" tags.
1185
var toRemove = [];
1186
var endDiv = null;
1187
var range = this.getRange_();
1188
var dh = this.getFieldDomHelper();
1189
if (command in
1190
goog.editor.plugins.BasicTextFormatter.blockquoteHatingCommandsIE_) {
1191
var parent = range && range.getContainerElement();
1192
if (parent) {
1193
var blockquotes = goog.dom.getElementsByTagNameAndClass(
1194
goog.dom.TagName.BLOCKQUOTE, null, parent);
1195
1196
// If a blockquote contains the selection, the fix is easy:
1197
// add a dummy div to the blockquote that isn't in the current selection.
1198
//
1199
// if the selection contains a blockquote,
1200
// there appears to be no easy way to protect it from getting mangled.
1201
// For now, we're just going to punt on this and try to
1202
// adjust the selection so that IE does something reasonable.
1203
//
1204
// TODO(nicksantos): Find a better fix for this.
1205
var bq;
1206
for (var i = 0; i < blockquotes.length; i++) {
1207
if (range.containsNode(blockquotes[i])) {
1208
bq = blockquotes[i];
1209
break;
1210
}
1211
}
1212
1213
var bqThatNeedsDummyDiv = bq ||
1214
goog.dom.getAncestorByTagNameAndClass(
1215
parent, goog.dom.TagName.BLOCKQUOTE);
1216
if (bqThatNeedsDummyDiv) {
1217
endDiv = dh.createDom(goog.dom.TagName.DIV, {style: 'height:0'});
1218
goog.dom.appendChild(bqThatNeedsDummyDiv, endDiv);
1219
toRemove.push(endDiv);
1220
1221
if (bq) {
1222
range = goog.dom.Range.createFromNodes(bq, 0, endDiv, 0);
1223
} else if (range.containsNode(endDiv)) {
1224
// the selection might be the entire blockquote, and
1225
// it's important that endDiv not be in the selection.
1226
range = goog.dom.Range.createFromNodes(
1227
range.getStartNode(), range.getStartOffset(), endDiv, 0);
1228
}
1229
range.select();
1230
}
1231
}
1232
}
1233
1234
// IE has a crazy bug where certain block execCommands cause it to mess with
1235
// the DOM nodes above the contentEditable element if the selection contains
1236
// or partially contains the last block element in the contentEditable
1237
// element.
1238
// Known commands: Indent, outdent, insertorderedlist, insertunorderedlist,
1239
// Justify (all of them)
1240
1241
// Both of the above are "solved" by appending a dummy div to the field
1242
// before the execCommand and removing it after, but we don't need to do this
1243
// if we've alread added a dummy div somewhere else.
1244
var fieldObject = this.getFieldObject();
1245
if (!fieldObject.usesIframe() && !endDiv) {
1246
if (command in
1247
goog.editor.plugins.BasicTextFormatter.brokenExecCommandsIE_) {
1248
var field = fieldObject.getElement();
1249
1250
// If the field is totally empty, or if the field contains only text nodes
1251
// and the cursor is at the end of the field, then IE stills walks outside
1252
// the contentEditable region and destroys things AND justify will not
1253
// work. This is "solved" by adding a text node into the end of the
1254
// field and moving the cursor before it.
1255
if (range && range.isCollapsed() &&
1256
!goog.dom.getFirstElementChild(field)) {
1257
// The problem only occurs if the selection is at the end of the field.
1258
var selection = range.getTextRange(0).getBrowserRangeObject();
1259
var testRange = selection.duplicate();
1260
testRange.moveToElementText(field);
1261
testRange.collapse(false);
1262
1263
if (testRange.isEqual(selection)) {
1264
// For reasons I really don't understand, if you use a breaking space
1265
// here, either " " or String.fromCharCode(32), this textNode becomes
1266
// corrupted, only after you hit ENTER to split it. It exists in the
1267
// dom in that its parent has it as childNode and the parent's
1268
// innerText is correct, but the node itself throws invalid argument
1269
// errors when you try to access its data, parentNode, nextSibling,
1270
// previousSibling or most other properties. WTF.
1271
var nbsp = dh.createTextNode(goog.string.Unicode.NBSP);
1272
field.appendChild(nbsp);
1273
selection.move('character', 1);
1274
selection.move('character', -1);
1275
selection.select();
1276
toRemove.push(nbsp);
1277
}
1278
}
1279
1280
endDiv = dh.createDom(goog.dom.TagName.DIV, {style: 'height:0'});
1281
goog.dom.appendChild(field, endDiv);
1282
toRemove.push(endDiv);
1283
}
1284
}
1285
1286
return toRemove;
1287
};
1288
1289
1290
/**
1291
* Fix a ridiculous Safari bug: the first letters of new headings
1292
* somehow retain their original font size and weight if multiple lines are
1293
* selected during the execCommand that turns them into headings.
1294
* The solution is to strip these styles which are normally stripped when
1295
* making things headings anyway.
1296
* @private
1297
*/
1298
goog.editor.plugins.BasicTextFormatter.prototype.cleanUpSafariHeadings_ =
1299
function() {
1300
goog.iter.forEach(this.getRange_(), function(node) {
1301
if (node.className == 'Apple-style-span') {
1302
// These shouldn't persist after creating headings via
1303
// a FormatBlock execCommand.
1304
node.style.fontSize = '';
1305
node.style.fontWeight = '';
1306
}
1307
});
1308
};
1309
1310
1311
/**
1312
* Prevent Safari from making each list item be "1" when converting from
1313
* unordered to ordered lists.
1314
* (see https://bugs.webkit.org/show_bug.cgi?id=19539, fixed by 2010-04-21)
1315
* @private
1316
*/
1317
goog.editor.plugins.BasicTextFormatter.prototype.fixSafariLists_ = function() {
1318
var previousList = false;
1319
goog.iter.forEach(this.getRange_(), function(node) {
1320
var tagName = node.tagName;
1321
if (tagName == goog.dom.TagName.UL || tagName == goog.dom.TagName.OL) {
1322
// Don't disturb lists outside of the selection. If this is the first <ul>
1323
// or <ol> in the range, we don't really want to merge the previous list
1324
// into it, since that list isn't in the range.
1325
if (!previousList) {
1326
previousList = true;
1327
return;
1328
}
1329
// The lists must be siblings to be merged; otherwise, indented sublists
1330
// could be broken.
1331
var previousElementSibling = goog.dom.getPreviousElementSibling(node);
1332
if (!previousElementSibling) {
1333
return;
1334
}
1335
// Make sure there isn't text between the two lists before they are merged
1336
var range = node.ownerDocument.createRange();
1337
range.setStartAfter(previousElementSibling);
1338
range.setEndBefore(node);
1339
if (!goog.string.isEmptyOrWhitespace(range.toString())) {
1340
return;
1341
}
1342
// Make sure both are lists of the same type (ordered or unordered)
1343
if (previousElementSibling.nodeName == node.nodeName) {
1344
// We must merge the previous list into this one. Moving around
1345
// the current node will break the iterator, so we can't merge
1346
// this list into the previous one.
1347
while (previousElementSibling.lastChild) {
1348
node.insertBefore(previousElementSibling.lastChild, node.firstChild);
1349
}
1350
previousElementSibling.parentNode.removeChild(previousElementSibling);
1351
}
1352
}
1353
});
1354
};
1355
1356
1357
/**
1358
* Sane "type" attribute values for OL elements
1359
* @private
1360
*/
1361
goog.editor.plugins.BasicTextFormatter.orderedListTypes_ = {
1362
'1': 1,
1363
'a': 1,
1364
'A': 1,
1365
'i': 1,
1366
'I': 1
1367
};
1368
1369
1370
/**
1371
* Sane "type" attribute values for UL elements
1372
* @private
1373
*/
1374
goog.editor.plugins.BasicTextFormatter.unorderedListTypes_ = {
1375
'disc': 1,
1376
'circle': 1,
1377
'square': 1
1378
};
1379
1380
1381
/**
1382
* Changing an OL to a UL (or the other way around) will fail if the list
1383
* has a type attribute (such as "UL type=disc" becoming "OL type=disc", which
1384
* is visually identical). Most browsers will remove the type attribute
1385
* automatically, but IE doesn't. This does it manually.
1386
* @private
1387
*/
1388
goog.editor.plugins.BasicTextFormatter.prototype.fixIELists_ = function() {
1389
// Find the lowest-level <ul> or <ol> that contains the entire range.
1390
var range = this.getRange_();
1391
var container = range && range.getContainer();
1392
while (container &&
1393
/** @type {!Element} */ (container).tagName != goog.dom.TagName.UL &&
1394
/** @type {!Element} */ (container).tagName != goog.dom.TagName.OL) {
1395
container = container.parentNode;
1396
}
1397
if (container) {
1398
// We want the parent node of the list so that we can grab it using
1399
// getElementsByTagName
1400
container = container.parentNode;
1401
}
1402
if (!container) return;
1403
var lists = goog.array.toArray(goog.dom.getElementsByTagName(
1404
goog.dom.TagName.UL, /** @type {!Element} */ (container)));
1405
goog.array.extend(
1406
lists, goog.array.toArray(goog.dom.getElementsByTagName(
1407
goog.dom.TagName.OL, /** @type {!Element} */ (container))));
1408
// Fix the lists
1409
goog.array.forEach(lists, function(node) {
1410
var type = node.type;
1411
if (type) {
1412
var saneTypes =
1413
(node.tagName == goog.dom.TagName.UL ?
1414
goog.editor.plugins.BasicTextFormatter.unorderedListTypes_ :
1415
goog.editor.plugins.BasicTextFormatter.orderedListTypes_);
1416
if (!saneTypes[type]) {
1417
node.type = '';
1418
}
1419
}
1420
});
1421
};
1422
1423
1424
/**
1425
* In WebKit, the following commands will modify the node with
1426
* contentEditable=true if there are no block-level elements.
1427
* @private
1428
*/
1429
goog.editor.plugins.BasicTextFormatter.brokenExecCommandsSafari_ = {
1430
'justifyCenter': 1,
1431
'justifyFull': 1,
1432
'justifyRight': 1,
1433
'justifyLeft': 1,
1434
'formatBlock': 1
1435
};
1436
1437
1438
/**
1439
* In WebKit, the following commands can hang the browser if the selection
1440
* touches the beginning of the field.
1441
* https://bugs.webkit.org/show_bug.cgi?id=19735
1442
* @private
1443
*/
1444
goog.editor.plugins.BasicTextFormatter.hangingExecCommandWebkit_ = {
1445
'insertOrderedList': 1,
1446
'insertUnorderedList': 1
1447
};
1448
1449
1450
/**
1451
* Apply pre-execCommand fixes for Safari.
1452
* @param {string} command The command to execute.
1453
* @return {!Element|undefined} The div added to the field.
1454
* @private
1455
*/
1456
goog.editor.plugins.BasicTextFormatter.prototype.applyExecCommandSafariFixes_ =
1457
function(command) {
1458
// See the comment on brokenExecCommandsSafari_
1459
var div;
1460
if (goog.editor.plugins.BasicTextFormatter
1461
.brokenExecCommandsSafari_[command]) {
1462
// Add a new div at the end of the field.
1463
// Safari knows that it would be wrong to apply text-align to the
1464
// contentEditable element if there are non-empty block nodes in the field,
1465
// because then it would align them too. So in this case, it will
1466
// enclose the current selection in a block node.
1467
div = this.getFieldDomHelper().createDom(
1468
goog.dom.TagName.DIV, {'style': 'height: 0'}, 'x');
1469
goog.dom.appendChild(this.getFieldObject().getElement(), div);
1470
}
1471
1472
if (!goog.userAgent.isVersionOrHigher(534) &&
1473
goog.editor.plugins.BasicTextFormatter
1474
.hangingExecCommandWebkit_[command]) {
1475
// Add a new div at the beginning of the field.
1476
var field = this.getFieldObject().getElement();
1477
div = this.getFieldDomHelper().createDom(
1478
goog.dom.TagName.DIV, {'style': 'height: 0'}, 'x');
1479
field.insertBefore(div, field.firstChild);
1480
}
1481
1482
return div;
1483
};
1484
1485
1486
/**
1487
* Apply pre-execCommand fixes for Gecko.
1488
* @param {string} command The command to execute.
1489
* @private
1490
*/
1491
goog.editor.plugins.BasicTextFormatter.prototype.applyExecCommandGeckoFixes_ =
1492
function(command) {
1493
if (goog.userAgent.isVersionOrHigher('1.9') &&
1494
command.toLowerCase() == 'formatblock') {
1495
// Firefox 3 and above throw a JS error for formatblock if the range is
1496
// a child of the body node. Changing the selection to the BR fixes the
1497
// problem.
1498
// See https://bugzilla.mozilla.org/show_bug.cgi?id=481696
1499
var range = this.getRange_();
1500
var startNode = range.getStartNode();
1501
if (range.isCollapsed() && startNode &&
1502
/** @type {!Element} */ (startNode).tagName == goog.dom.TagName.BODY) {
1503
var startOffset = range.getStartOffset();
1504
var childNode = startNode.childNodes[startOffset];
1505
if (childNode && childNode.tagName == goog.dom.TagName.BR) {
1506
// Change the range using getBrowserRange() because goog.dom.TextRange
1507
// will avoid setting <br>s directly.
1508
// @see goog.dom.TextRange#createFromNodes
1509
var browserRange = range.getBrowserRangeObject();
1510
browserRange.setStart(childNode, 0);
1511
browserRange.setEnd(childNode, 0);
1512
}
1513
}
1514
}
1515
};
1516
1517
1518
/**
1519
* Workaround for Opera bug CORE-23903. Opera sometimes fails to invalidate
1520
* serialized CSS or innerHTML for the DOM after certain execCommands when
1521
* styleWithCSS is on. Toggling an inline style on the elements fixes it.
1522
* @private
1523
*/
1524
goog.editor.plugins.BasicTextFormatter.prototype.invalidateInlineCss_ =
1525
function() {
1526
var ancestors = [];
1527
var ancestor = this.getFieldObject().getRange().getContainerElement();
1528
do {
1529
ancestors.push(ancestor);
1530
} while (ancestor = ancestor.parentNode);
1531
var nodesInSelection = goog.iter.chain(
1532
goog.iter.toIterator(this.getFieldObject().getRange()),
1533
goog.iter.toIterator(ancestors));
1534
var containersInSelection =
1535
goog.iter.filter(nodesInSelection, goog.editor.style.isContainer);
1536
goog.iter.forEach(containersInSelection, function(element) {
1537
var oldOutline = element.style.outline;
1538
element.style.outline = '0px solid red';
1539
element.style.outline = oldOutline;
1540
});
1541
};
1542
1543
1544
/**
1545
* Work around a Gecko bug that causes inserted lists to forget the current
1546
* font. This affects WebKit in the same way and Opera in a slightly different
1547
* way, but this workaround only works in Gecko.
1548
* WebKit bug: https://bugs.webkit.org/show_bug.cgi?id=19653
1549
* Mozilla bug: https://bugzilla.mozilla.org/show_bug.cgi?id=439966
1550
* Opera bug: https://bugs.opera.com/show_bug.cgi?id=340392
1551
* TODO: work around this issue in WebKit and Opera as well.
1552
* @return {boolean} Whether the workaround was applied.
1553
* @private
1554
*/
1555
goog.editor.plugins.BasicTextFormatter.prototype.beforeInsertListGecko_ =
1556
function() {
1557
var tag =
1558
this.getFieldObject().queryCommandValue(goog.editor.Command.DEFAULT_TAG);
1559
if (tag == goog.dom.TagName.P || tag == goog.dom.TagName.DIV) {
1560
return false;
1561
}
1562
1563
// Prevent Firefox from forgetting current formatting
1564
// when creating a list.
1565
// The bug happens with a collapsed selection, but it won't
1566
// happen when text with the desired formatting is selected.
1567
// So, we insert some dummy text, insert the list,
1568
// then remove the dummy text (while preserving its formatting).
1569
// (This formatting bug also affects WebKit, but this fix
1570
// only seems to work in Firefox)
1571
var range = this.getRange_();
1572
if (range.isCollapsed() &&
1573
(range.getContainer().nodeType != goog.dom.NodeType.TEXT)) {
1574
var tempTextNode =
1575
this.getFieldDomHelper().createTextNode(goog.string.Unicode.NBSP);
1576
range.insertNode(tempTextNode, false);
1577
goog.dom.Range.createFromNodeContents(tempTextNode).select();
1578
return true;
1579
}
1580
return false;
1581
};
1582
1583
1584
// Helpers for queryCommandState
1585
1586
1587
/**
1588
* Get the toolbar state for the block-level elements in the given range.
1589
* @param {goog.dom.AbstractRange} range The range to get toolbar state for.
1590
* @return {string?} The selection block state.
1591
* @private
1592
*/
1593
goog.editor.plugins.BasicTextFormatter.getSelectionBlockState_ = function(
1594
range) {
1595
var tagName = null;
1596
goog.iter.forEach(range, function(node, ignore, it) {
1597
if (!it.isEndTag()) {
1598
// Iterate over all containers in the range, checking if they all have the
1599
// same tagName.
1600
var container = goog.editor.style.getContainer(node);
1601
var thisTagName = container.tagName;
1602
tagName = tagName || thisTagName;
1603
1604
if (tagName != thisTagName) {
1605
// If we find a container tag that doesn't match, exit right away.
1606
tagName = null;
1607
throw goog.iter.StopIteration;
1608
}
1609
1610
// Skip the tag.
1611
it.skipTag();
1612
}
1613
});
1614
1615
return tagName;
1616
};
1617
1618
1619
/**
1620
* Hash of suppoted justifications.
1621
* @type {Object}
1622
* @private
1623
*/
1624
goog.editor.plugins.BasicTextFormatter.SUPPORTED_JUSTIFICATIONS_ = {
1625
'center': 1,
1626
'justify': 1,
1627
'right': 1,
1628
'left': 1
1629
};
1630
1631
1632
/**
1633
* Returns true if the current justification matches the justification
1634
* command for the entire selection.
1635
* @param {string} command The justification command to check for.
1636
* @return {boolean} Whether the current justification matches the justification
1637
* command for the entire selection.
1638
* @private
1639
*/
1640
goog.editor.plugins.BasicTextFormatter.prototype.isJustification_ = function(
1641
command) {
1642
var alignment = command.replace('+justify', '').toLowerCase();
1643
if (alignment == 'full') {
1644
alignment = 'justify';
1645
}
1646
var bidiPlugin = this.getFieldObject().getPluginByClassId('Bidi');
1647
if (bidiPlugin) {
1648
// BiDi aware version
1649
1650
// TODO: Since getComputedStyle is not used here, this version may be even
1651
// faster. If profiling confirms that it would be good to use this approach
1652
// in both cases. Otherwise the bidi part should be moved into an
1653
// execCommand so this bidi plugin dependence isn't needed here.
1654
/** @type {Function} */
1655
bidiPlugin.getSelectionAlignment;
1656
return alignment == bidiPlugin.getSelectionAlignment();
1657
} else {
1658
// BiDi unaware version
1659
var range = this.getRange_();
1660
if (!range) {
1661
// When nothing is in the selection then no justification
1662
// command matches.
1663
return false;
1664
}
1665
1666
var parent = range.getContainerElement();
1667
var nodes = goog.array.filter(parent.childNodes, function(node) {
1668
return goog.editor.node.isImportant(node) &&
1669
range.containsNode(node, true);
1670
});
1671
nodes = nodes.length ? nodes : [parent];
1672
1673
for (var i = 0; i < nodes.length; i++) {
1674
var current = nodes[i];
1675
1676
// If any node in the selection is not aligned the way we are checking,
1677
// then the justification command does not match.
1678
var container = goog.editor.style.getContainer(
1679
/** @type {Node} */ (current));
1680
if (alignment !=
1681
goog.editor.plugins.BasicTextFormatter.getNodeJustification_(
1682
container)) {
1683
return false;
1684
}
1685
}
1686
1687
// If all nodes in the selection are aligned the way we are checking,
1688
// the justification command does match.
1689
return true;
1690
}
1691
};
1692
1693
1694
/**
1695
* Determines the justification for a given block-level element.
1696
* @param {Element} element The node to get justification for.
1697
* @return {string} The justification for a given block-level node.
1698
* @private
1699
*/
1700
goog.editor.plugins.BasicTextFormatter.getNodeJustification_ = function(
1701
element) {
1702
var value = goog.style.getComputedTextAlign(element);
1703
// Strip preceding -moz- or -webkit- (@bug 2472589).
1704
value = value.replace(/^-(moz|webkit)-/, '');
1705
1706
// If there is no alignment, try the inline property,
1707
// otherwise assume left aligned.
1708
// TODO: for rtl languages we probably need to assume right.
1709
if (!goog.editor.plugins.BasicTextFormatter
1710
.SUPPORTED_JUSTIFICATIONS_[value]) {
1711
value = element.align || 'left';
1712
}
1713
return /** @type {string} */ (value);
1714
};
1715
1716
1717
/**
1718
* Returns true if a selection contained in the node should set the appropriate
1719
* toolbar state for the given nodeName, e.g. if the node is contained in a
1720
* strong element and nodeName is "strong", then it will return true.
1721
* @param {!goog.dom.TagName} nodeName The type of node to check for.
1722
* @return {boolean} Whether the user's selection is in the given state.
1723
* @private
1724
*/
1725
goog.editor.plugins.BasicTextFormatter.prototype.isNodeInState_ = function(
1726
nodeName) {
1727
var range = this.getRange_();
1728
var node = range && range.getContainerElement();
1729
var ancestor = goog.dom.getAncestorByTagNameAndClass(node, nodeName);
1730
return !!ancestor && goog.editor.node.isEditable(ancestor);
1731
};
1732
1733
1734
/**
1735
* Wrapper for browser's queryCommandState.
1736
* @param {Document|TextRange|Range} queryObject The object to query.
1737
* @param {string} command The command to check.
1738
* @param {boolean=} opt_styleWithCss Set to true to enable styleWithCSS before
1739
* performing the queryCommandState.
1740
* @return {boolean} The command state.
1741
* @private
1742
*/
1743
goog.editor.plugins.BasicTextFormatter.prototype.queryCommandStateInternal_ =
1744
function(queryObject, command, opt_styleWithCss) {
1745
return /** @type {boolean} */ (
1746
this.queryCommandHelper_(true, queryObject, command, opt_styleWithCss));
1747
};
1748
1749
1750
/**
1751
* Wrapper for browser's queryCommandValue.
1752
* @param {Document|TextRange|Range} queryObject The object to query.
1753
* @param {string} command The command to check.
1754
* @param {boolean=} opt_styleWithCss Set to true to enable styleWithCSS before
1755
* performing the queryCommandValue.
1756
* @return {string|boolean|null} The command value.
1757
* @private
1758
*/
1759
goog.editor.plugins.BasicTextFormatter.prototype.queryCommandValueInternal_ =
1760
function(queryObject, command, opt_styleWithCss) {
1761
return this.queryCommandHelper_(
1762
false, queryObject, command, opt_styleWithCss);
1763
};
1764
1765
1766
/**
1767
* Helper function to perform queryCommand(Value|State).
1768
* @param {boolean} isGetQueryCommandState True to use queryCommandState, false
1769
* to use queryCommandValue.
1770
* @param {Document|TextRange|Range} queryObject The object to query.
1771
* @param {string} command The command to check.
1772
* @param {boolean=} opt_styleWithCss Set to true to enable styleWithCSS before
1773
* performing the queryCommand(Value|State).
1774
* @return {string|boolean|null} The command value.
1775
* @private
1776
*/
1777
goog.editor.plugins.BasicTextFormatter.prototype.queryCommandHelper_ = function(
1778
isGetQueryCommandState, queryObject, command, opt_styleWithCss) {
1779
command =
1780
goog.editor.plugins.BasicTextFormatter.convertToRealExecCommand_(command);
1781
if (opt_styleWithCss) {
1782
var doc = this.getDocument_();
1783
// Don't use this.execCommandHelper_ here, as it is more heavyweight
1784
// and inserts a dummy div to protect against comamnds that could step
1785
// outside the editable region, which would cause change event on
1786
// every toolbar update.
1787
doc.execCommand('styleWithCSS', false, true);
1788
}
1789
var ret = isGetQueryCommandState ? queryObject.queryCommandState(command) :
1790
queryObject.queryCommandValue(command);
1791
if (opt_styleWithCss) {
1792
doc.execCommand('styleWithCSS', false, false);
1793
}
1794
return ret;
1795
};
1796
1797