firststrong.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334
  1. // Copyright 2012 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 A plugin to enable the First Strong Bidi algorithm. The First
  16. * Strong algorithm as a heuristic used to automatically set paragraph direction
  17. * depending on its content.
  18. *
  19. * In the documentation below, a 'paragraph' is the local element which we
  20. * evaluate as a whole for purposes of determining directionality. It may be a
  21. * block-level element (e.g. <div>) or a whole list (e.g. <ul>).
  22. *
  23. * This implementation is based on, but is not identical to, the original
  24. * First Strong algorithm defined in Unicode
  25. * @see http://www.unicode.org/reports/tr9/
  26. * The central difference from the original First Strong algorithm is that this
  27. * implementation decides the paragraph direction based on the first strong
  28. * character that is <em>typed</em> into the paragraph, regardless of its
  29. * location in the paragraph, as opposed to the original algorithm where it is
  30. * the first character in the paragraph <em>by location</em>, regardless of
  31. * whether other strong characters already appear in the paragraph, further its
  32. * start.
  33. *
  34. * <em>Please note</em> that this plugin does not perform the direction change
  35. * itself. Rather, it fires editor commands upon the key up event when a
  36. * direction change needs to be performed; {@code goog.editor.Command.DIR_RTL}
  37. * or {@code goog.editor.Command.DIR_RTL}.
  38. *
  39. */
  40. goog.provide('goog.editor.plugins.FirstStrong');
  41. goog.require('goog.dom.NodeType');
  42. goog.require('goog.dom.TagIterator');
  43. goog.require('goog.dom.TagName');
  44. goog.require('goog.editor.Command');
  45. goog.require('goog.editor.Field');
  46. goog.require('goog.editor.Plugin');
  47. goog.require('goog.editor.node');
  48. goog.require('goog.editor.range');
  49. goog.require('goog.i18n.bidi');
  50. goog.require('goog.i18n.uChar');
  51. goog.require('goog.iter');
  52. goog.require('goog.userAgent');
  53. /**
  54. * First Strong plugin.
  55. * @constructor
  56. * @extends {goog.editor.Plugin}
  57. * @final
  58. */
  59. goog.editor.plugins.FirstStrong = function() {
  60. goog.editor.plugins.FirstStrong.base(this, 'constructor');
  61. /**
  62. * Indicates whether or not the cursor is in a paragraph we have not yet
  63. * finished evaluating for directionality. This is set to true whenever the
  64. * cursor is moved, and set to false after seeing a strong character in the
  65. * paragraph the cursor is currently in.
  66. *
  67. * @type {boolean}
  68. * @private
  69. */
  70. this.isNewBlock_ = true;
  71. /**
  72. * Indicates whether or not the current paragraph the cursor is in should be
  73. * set to Right-To-Left directionality.
  74. *
  75. * @type {boolean}
  76. * @private
  77. */
  78. this.switchToRtl_ = false;
  79. /**
  80. * Indicates whether or not the current paragraph the cursor is in should be
  81. * set to Left-To-Right directionality.
  82. *
  83. * @type {boolean}
  84. * @private
  85. */
  86. this.switchToLtr_ = false;
  87. };
  88. goog.inherits(goog.editor.plugins.FirstStrong, goog.editor.Plugin);
  89. /** @override */
  90. goog.editor.plugins.FirstStrong.prototype.getTrogClassId = function() {
  91. return 'FirstStrong';
  92. };
  93. /** @override */
  94. goog.editor.plugins.FirstStrong.prototype.queryCommandValue = function(
  95. command) {
  96. return false;
  97. };
  98. /** @override */
  99. goog.editor.plugins.FirstStrong.prototype.handleSelectionChange = function(
  100. e, node) {
  101. this.isNewBlock_ = true;
  102. return false;
  103. };
  104. /**
  105. * The name of the attribute which records the input text.
  106. *
  107. * @type {string}
  108. * @const
  109. */
  110. goog.editor.plugins.FirstStrong.INPUT_ATTRIBUTE = 'fs-input';
  111. /** @override */
  112. goog.editor.plugins.FirstStrong.prototype.handleKeyPress = function(e) {
  113. if (goog.editor.Field.SELECTION_CHANGE_KEYCODES[e.keyCode]) {
  114. // Key triggered selection change event (e.g. on ENTER) is throttled and a
  115. // later LTR/RTL strong keypress may come before it. Need to capture it.
  116. this.isNewBlock_ = true;
  117. return false; // A selection-changing key is not LTR/RTL strong.
  118. }
  119. if (!this.isNewBlock_) {
  120. return false; // We've already determined this paragraph's direction.
  121. }
  122. // Ignore non-character key press events.
  123. if (e.ctrlKey || e.metaKey) {
  124. return false;
  125. }
  126. var newInput = goog.i18n.uChar.fromCharCode(e.charCode);
  127. // IME's may return 0 for the charCode, which is a legitimate, non-Strong
  128. // charCode, or they may return an illegal charCode (for which newInput will
  129. // be false).
  130. if (!newInput || !e.charCode) {
  131. var browserEvent = e.getBrowserEvent();
  132. if (browserEvent) {
  133. if (goog.userAgent.IE && browserEvent['getAttribute']) {
  134. newInput = browserEvent['getAttribute'](
  135. goog.editor.plugins.FirstStrong.INPUT_ATTRIBUTE);
  136. } else {
  137. newInput =
  138. browserEvent[goog.editor.plugins.FirstStrong.INPUT_ATTRIBUTE];
  139. }
  140. }
  141. }
  142. if (!newInput) {
  143. return false; // Unrecognized key.
  144. }
  145. var isLtr = goog.i18n.bidi.isLtrChar(newInput);
  146. var isRtl = !isLtr && goog.i18n.bidi.isRtlChar(newInput);
  147. if (!isLtr && !isRtl) {
  148. return false; // This character cannot change anything (it is not Strong).
  149. }
  150. // This character is Strongly LTR or Strongly RTL. We might switch direction
  151. // on it now, but in any case we do not need to check any more characters in
  152. // this paragraph after it.
  153. this.isNewBlock_ = false;
  154. // Are there no Strong characters already in the paragraph?
  155. if (this.isNeutralBlock_()) {
  156. this.switchToRtl_ = isRtl;
  157. this.switchToLtr_ = isLtr;
  158. }
  159. return false;
  160. };
  161. /**
  162. * Calls the flip directionality commands. This is done here so things go into
  163. * the redo-undo stack at the expected order; fist enter the input, then flip
  164. * directionality.
  165. * @override
  166. */
  167. goog.editor.plugins.FirstStrong.prototype.handleKeyUp = function(e) {
  168. if (this.switchToRtl_) {
  169. var field = this.getFieldObject();
  170. field.dispatchChange(true);
  171. field.execCommand(goog.editor.Command.DIR_RTL);
  172. this.switchToRtl_ = false;
  173. } else if (this.switchToLtr_) {
  174. var field = this.getFieldObject();
  175. field.dispatchChange(true);
  176. field.execCommand(goog.editor.Command.DIR_LTR);
  177. this.switchToLtr_ = false;
  178. }
  179. return false;
  180. };
  181. /**
  182. * @return {Element} The lowest Block element ancestor of the node where the
  183. * next character will be placed.
  184. * @private
  185. */
  186. goog.editor.plugins.FirstStrong.prototype.getBlockAncestor_ = function() {
  187. var start = this.getFieldObject().getRange().getStartNode();
  188. // Go up in the DOM until we reach a Block element.
  189. while (!goog.editor.plugins.FirstStrong.isBlock_(start)) {
  190. start = start.parentNode;
  191. }
  192. return /** @type {Element} */ (start);
  193. };
  194. /**
  195. * @return {boolean} Whether the paragraph where the next character will be
  196. * entered contains only non-Strong characters.
  197. * @private
  198. */
  199. goog.editor.plugins.FirstStrong.prototype.isNeutralBlock_ = function() {
  200. var root = this.getBlockAncestor_();
  201. // The exact node with the cursor location. Simply calling getStartNode() on
  202. // the range only returns the containing block node.
  203. var cursor =
  204. goog.editor.range.getDeepEndPoint(this.getFieldObject().getRange(), false)
  205. .node;
  206. // In FireFox the BR tag also represents a change in paragraph if not inside a
  207. // list. So we need special handling to only look at the sub-block between
  208. // BR elements.
  209. var blockFunction = (goog.userAgent.GECKO && !this.isList_(root)) ?
  210. goog.editor.plugins.FirstStrong.isGeckoBlock_ :
  211. goog.editor.plugins.FirstStrong.isBlock_;
  212. var paragraph = this.getTextAround_(root, cursor, blockFunction);
  213. // Not using {@code goog.i18n.bidi.isNeutralText} as it contains additional,
  214. // unwanted checks to the content.
  215. return !goog.i18n.bidi.hasAnyLtr(paragraph) &&
  216. !goog.i18n.bidi.hasAnyRtl(paragraph);
  217. };
  218. /**
  219. * Checks if an element is a list element ('UL' or 'OL').
  220. *
  221. * @param {Element} element The element to test.
  222. * @return {boolean} Whether the element is a list element ('UL' or 'OL').
  223. * @private
  224. */
  225. goog.editor.plugins.FirstStrong.prototype.isList_ = function(element) {
  226. if (!element) {
  227. return false;
  228. }
  229. var tagName = element.tagName;
  230. return tagName == goog.dom.TagName.UL || tagName == goog.dom.TagName.OL;
  231. };
  232. /**
  233. * Returns the text within the local paragraph around the cursor.
  234. * Notice that for GECKO a BR represents a pargraph change despite not being a
  235. * block element.
  236. *
  237. * @param {Element} root The first block element ancestor of the node the cursor
  238. * is in.
  239. * @param {Node} cursorLocation Node where the cursor currently is, marking the
  240. * paragraph whose text we will return.
  241. * @param {function(Node): boolean} isParagraphBoundary The function to
  242. * determine if a node represents the start or end of the paragraph.
  243. * @return {string} the text in the paragraph around the cursor location.
  244. * @private
  245. */
  246. goog.editor.plugins.FirstStrong.prototype.getTextAround_ = function(
  247. root, cursorLocation, isParagraphBoundary) {
  248. // The buffer where we're collecting the text.
  249. var buffer = [];
  250. // Have we reached the cursor yet, or are we still before it?
  251. var pastCursorLocation = false;
  252. if (root && cursorLocation) {
  253. goog.iter.some(new goog.dom.TagIterator(root), function(node) {
  254. if (node == cursorLocation) {
  255. pastCursorLocation = true;
  256. } else if (isParagraphBoundary(node)) {
  257. if (pastCursorLocation) {
  258. // This is the end of the paragraph containing the cursor. We're done.
  259. return true;
  260. } else {
  261. // All we collected so far does not count; it was in a previous
  262. // paragraph that did not contain the cursor.
  263. buffer = [];
  264. }
  265. }
  266. if (node.nodeType == goog.dom.NodeType.TEXT) {
  267. buffer.push(node.nodeValue);
  268. }
  269. return false; // Keep going.
  270. });
  271. }
  272. return buffer.join('');
  273. };
  274. /**
  275. * @param {Node} node Node to check.
  276. * @return {boolean} Does the given node represent a Block element? Notice we do
  277. * not consider list items as Block elements in the algorithm.
  278. * @private
  279. */
  280. goog.editor.plugins.FirstStrong.isBlock_ = function(node) {
  281. return !!node && goog.editor.node.isBlockTag(node) &&
  282. /** @type {!Element} */ (node).tagName != goog.dom.TagName.LI;
  283. };
  284. /**
  285. * @param {Node} node Node to check.
  286. * @return {boolean} Does the given node represent a Block element from the
  287. * point of view of FireFox? Notice we do not consider list items as Block
  288. * elements in the algorithm.
  289. * @private
  290. */
  291. goog.editor.plugins.FirstStrong.isGeckoBlock_ = function(node) {
  292. return !!node &&
  293. (/** @type {!Element} */ (node).tagName == goog.dom.TagName.BR ||
  294. goog.editor.plugins.FirstStrong.isBlock_(node));
  295. };