| // Copyright 2012 The Closure Library Authors. All Rights Reserved. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS-IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| /** |
| * @fileoverview A plugin to enable the First Strong Bidi algorithm. The First |
| * Strong algorithm as a heuristic used to automatically set paragraph direction |
| * depending on its content. |
| * |
| * In the documentation below, a 'paragraph' is the local element which we |
| * evaluate as a whole for purposes of determining directionality. It may be a |
| * block-level element (e.g. <div>) or a whole list (e.g. <ul>). |
| * |
| * This implementation is based on, but is not identical to, the original |
| * First Strong algorithm defined in Unicode |
| * @see http://www.unicode.org/reports/tr9/ |
| * The central difference from the original First Strong algorithm is that this |
| * implementation decides the paragraph direction based on the first strong |
| * character that is <em>typed</em> into the paragraph, regardless of its |
| * location in the paragraph, as opposed to the original algorithm where it is |
| * the first character in the paragraph <em>by location</em>, regardless of |
| * whether other strong characters already appear in the paragraph, further its |
| * start. |
| * |
| * <em>Please note</em> that this plugin does not perform the direction change |
| * itself. Rather, it fires editor commands upon the key up event when a |
| * direction change needs to be performed; {@code goog.editor.Command.DIR_RTL} |
| * or {@code goog.editor.Command.DIR_RTL}. |
| * |
| */ |
| |
| goog.provide('goog.editor.plugins.FirstStrong'); |
| |
| goog.require('goog.dom.NodeType'); |
| goog.require('goog.dom.TagIterator'); |
| goog.require('goog.dom.TagName'); |
| goog.require('goog.editor.Command'); |
| goog.require('goog.editor.Field'); |
| goog.require('goog.editor.Plugin'); |
| goog.require('goog.editor.node'); |
| goog.require('goog.editor.range'); |
| goog.require('goog.i18n.bidi'); |
| goog.require('goog.i18n.uChar'); |
| goog.require('goog.iter'); |
| goog.require('goog.userAgent'); |
| |
| |
| |
| /** |
| * First Strong plugin. |
| * @constructor |
| * @extends {goog.editor.Plugin} |
| * @final |
| */ |
| goog.editor.plugins.FirstStrong = function() { |
| goog.editor.plugins.FirstStrong.base(this, 'constructor'); |
| |
| /** |
| * Indicates whether or not the cursor is in a paragraph we have not yet |
| * finished evaluating for directionality. This is set to true whenever the |
| * cursor is moved, and set to false after seeing a strong character in the |
| * paragraph the cursor is currently in. |
| * |
| * @type {boolean} |
| * @private |
| */ |
| this.isNewBlock_ = true; |
| |
| /** |
| * Indicates whether or not the current paragraph the cursor is in should be |
| * set to Right-To-Left directionality. |
| * |
| * @type {boolean} |
| * @private |
| */ |
| this.switchToRtl_ = false; |
| |
| /** |
| * Indicates whether or not the current paragraph the cursor is in should be |
| * set to Left-To-Right directionality. |
| * |
| * @type {boolean} |
| * @private |
| */ |
| this.switchToLtr_ = false; |
| }; |
| goog.inherits(goog.editor.plugins.FirstStrong, goog.editor.Plugin); |
| |
| |
| /** @override */ |
| goog.editor.plugins.FirstStrong.prototype.getTrogClassId = function() { |
| return 'FirstStrong'; |
| }; |
| |
| |
| /** @override */ |
| goog.editor.plugins.FirstStrong.prototype.queryCommandValue = |
| function(command) { |
| return false; |
| }; |
| |
| |
| /** @override */ |
| goog.editor.plugins.FirstStrong.prototype.handleSelectionChange = |
| function(e, node) { |
| this.isNewBlock_ = true; |
| return false; |
| }; |
| |
| |
| /** |
| * The name of the attribute which records the input text. |
| * |
| * @type {string} |
| * @const |
| */ |
| goog.editor.plugins.FirstStrong.INPUT_ATTRIBUTE = 'fs-input'; |
| |
| |
| /** @override */ |
| goog.editor.plugins.FirstStrong.prototype.handleKeyPress = function(e) { |
| if (goog.editor.Field.SELECTION_CHANGE_KEYCODES[e.keyCode]) { |
| // Key triggered selection change event (e.g. on ENTER) is throttled and a |
| // later LTR/RTL strong keypress may come before it. Need to capture it. |
| this.isNewBlock_ = true; |
| return false; // A selection-changing key is not LTR/RTL strong. |
| } |
| if (!this.isNewBlock_) { |
| return false; // We've already determined this paragraph's direction. |
| } |
| // Ignore non-character key press events. |
| if (e.ctrlKey || e.metaKey) { |
| return false; |
| } |
| var newInput = goog.i18n.uChar.fromCharCode(e.charCode); |
| |
| // IME's may return 0 for the charCode, which is a legitimate, non-Strong |
| // charCode, or they may return an illegal charCode (for which newInput will |
| // be false). |
| if (!newInput || !e.charCode) { |
| var browserEvent = e.getBrowserEvent(); |
| if (browserEvent) { |
| if (goog.userAgent.IE && browserEvent['getAttribute']) { |
| newInput = browserEvent['getAttribute']( |
| goog.editor.plugins.FirstStrong.INPUT_ATTRIBUTE); |
| } else { |
| newInput = browserEvent[ |
| goog.editor.plugins.FirstStrong.INPUT_ATTRIBUTE]; |
| } |
| } |
| } |
| |
| if (!newInput) { |
| return false; // Unrecognized key. |
| } |
| |
| var isLtr = goog.i18n.bidi.isLtrChar(newInput); |
| var isRtl = !isLtr && goog.i18n.bidi.isRtlChar(newInput); |
| if (!isLtr && !isRtl) { |
| return false; // This character cannot change anything (it is not Strong). |
| } |
| // This character is Strongly LTR or Strongly RTL. We might switch direction |
| // on it now, but in any case we do not need to check any more characters in |
| // this paragraph after it. |
| this.isNewBlock_ = false; |
| |
| // Are there no Strong characters already in the paragraph? |
| if (this.isNeutralBlock_()) { |
| this.switchToRtl_ = isRtl; |
| this.switchToLtr_ = isLtr; |
| } |
| return false; |
| }; |
| |
| |
| /** |
| * Calls the flip directionality commands. This is done here so things go into |
| * the redo-undo stack at the expected order; fist enter the input, then flip |
| * directionality. |
| * @override |
| */ |
| goog.editor.plugins.FirstStrong.prototype.handleKeyUp = function(e) { |
| if (this.switchToRtl_) { |
| var field = this.getFieldObject(); |
| field.dispatchChange(true); |
| field.execCommand(goog.editor.Command.DIR_RTL); |
| this.switchToRtl_ = false; |
| } else if (this.switchToLtr_) { |
| var field = this.getFieldObject(); |
| field.dispatchChange(true); |
| field.execCommand(goog.editor.Command.DIR_LTR); |
| this.switchToLtr_ = false; |
| } |
| return false; |
| }; |
| |
| |
| /** |
| * @return {Element} The lowest Block element ancestor of the node where the |
| * next character will be placed. |
| * @private |
| */ |
| goog.editor.plugins.FirstStrong.prototype.getBlockAncestor_ = function() { |
| var start = this.getFieldObject().getRange().getStartNode(); |
| // Go up in the DOM until we reach a Block element. |
| while (!goog.editor.plugins.FirstStrong.isBlock_(start)) { |
| start = start.parentNode; |
| } |
| return /** @type {Element} */ (start); |
| }; |
| |
| |
| /** |
| * @return {boolean} Whether the paragraph where the next character will be |
| * entered contains only non-Strong characters. |
| * @private |
| */ |
| goog.editor.plugins.FirstStrong.prototype.isNeutralBlock_ = function() { |
| var root = this.getBlockAncestor_(); |
| // The exact node with the cursor location. Simply calling getStartNode() on |
| // the range only returns the containing block node. |
| var cursor = goog.editor.range.getDeepEndPoint( |
| this.getFieldObject().getRange(), false).node; |
| |
| // In FireFox the BR tag also represents a change in paragraph if not inside a |
| // list. So we need special handling to only look at the sub-block between |
| // BR elements. |
| var blockFunction = (goog.userAgent.GECKO && |
| !this.isList_(root)) ? |
| goog.editor.plugins.FirstStrong.isGeckoBlock_ : |
| goog.editor.plugins.FirstStrong.isBlock_; |
| var paragraph = this.getTextAround_(root, cursor, |
| blockFunction); |
| // Not using {@code goog.i18n.bidi.isNeutralText} as it contains additional, |
| // unwanted checks to the content. |
| return !goog.i18n.bidi.hasAnyLtr(paragraph) && |
| !goog.i18n.bidi.hasAnyRtl(paragraph); |
| }; |
| |
| |
| /** |
| * Checks if an element is a list element ('UL' or 'OL'). |
| * |
| * @param {Element} element The element to test. |
| * @return {boolean} Whether the element is a list element ('UL' or 'OL'). |
| * @private |
| */ |
| goog.editor.plugins.FirstStrong.prototype.isList_ = function(element) { |
| if (!element) { |
| return false; |
| } |
| var tagName = element.tagName; |
| return tagName == goog.dom.TagName.UL || tagName == goog.dom.TagName.OL; |
| }; |
| |
| |
| /** |
| * Returns the text within the local paragraph around the cursor. |
| * Notice that for GECKO a BR represents a pargraph change despite not being a |
| * block element. |
| * |
| * @param {Element} root The first block element ancestor of the node the cursor |
| * is in. |
| * @param {Node} cursorLocation Node where the cursor currently is, marking the |
| * paragraph whose text we will return. |
| * @param {function(Node): boolean} isParagraphBoundary The function to |
| * determine if a node represents the start or end of the paragraph. |
| * @return {string} the text in the paragraph around the cursor location. |
| * @private |
| */ |
| goog.editor.plugins.FirstStrong.prototype.getTextAround_ = function(root, |
| cursorLocation, isParagraphBoundary) { |
| // The buffer where we're collecting the text. |
| var buffer = []; |
| // Have we reached the cursor yet, or are we still before it? |
| var pastCursorLocation = false; |
| |
| if (root && cursorLocation) { |
| goog.iter.some(new goog.dom.TagIterator(root), function(node) { |
| if (node == cursorLocation) { |
| pastCursorLocation = true; |
| } else if (isParagraphBoundary(node)) { |
| if (pastCursorLocation) { |
| // This is the end of the paragraph containing the cursor. We're done. |
| return true; |
| } else { |
| // All we collected so far does not count; it was in a previous |
| // paragraph that did not contain the cursor. |
| buffer = []; |
| } |
| } |
| if (node.nodeType == goog.dom.NodeType.TEXT) { |
| buffer.push(node.nodeValue); |
| } |
| return false; // Keep going. |
| }); |
| } |
| return buffer.join(''); |
| }; |
| |
| |
| /** |
| * @param {Node} node Node to check. |
| * @return {boolean} Does the given node represent a Block element? Notice we do |
| * not consider list items as Block elements in the algorithm. |
| * @private |
| */ |
| goog.editor.plugins.FirstStrong.isBlock_ = function(node) { |
| return !!node && goog.editor.node.isBlockTag(node) && |
| node.tagName != goog.dom.TagName.LI; |
| }; |
| |
| |
| /** |
| * @param {Node} node Node to check. |
| * @return {boolean} Does the given node represent a Block element from the |
| * point of view of FireFox? Notice we do not consider list items as Block |
| * elements in the algorithm. |
| * @private |
| */ |
| goog.editor.plugins.FirstStrong.isGeckoBlock_ = function(node) { |
| return !!node && (node.tagName == goog.dom.TagName.BR || |
| goog.editor.plugins.FirstStrong.isBlock_(node)); |
| }; |