basictextformatter.js 65 KB

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