Path: blob/trunk/third_party/closure/goog/editor/plugins/firststrong.js
2868 views
// Copyright 2012 The Closure Library Authors. All Rights Reserved.1//2// Licensed under the Apache License, Version 2.0 (the "License");3// you may not use this file except in compliance with the License.4// You may obtain a copy of the License at5//6// http://www.apache.org/licenses/LICENSE-2.07//8// Unless required by applicable law or agreed to in writing, software9// distributed under the License is distributed on an "AS-IS" BASIS,10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.11// See the License for the specific language governing permissions and12// limitations under the License.1314/**15* @fileoverview A plugin to enable the First Strong Bidi algorithm. The First16* Strong algorithm as a heuristic used to automatically set paragraph direction17* depending on its content.18*19* In the documentation below, a 'paragraph' is the local element which we20* evaluate as a whole for purposes of determining directionality. It may be a21* 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 original24* First Strong algorithm defined in Unicode25* @see http://www.unicode.org/reports/tr9/26* The central difference from the original First Strong algorithm is that this27* implementation decides the paragraph direction based on the first strong28* character that is <em>typed</em> into the paragraph, regardless of its29* location in the paragraph, as opposed to the original algorithm where it is30* the first character in the paragraph <em>by location</em>, regardless of31* whether other strong characters already appear in the paragraph, further its32* start.33*34* <em>Please note</em> that this plugin does not perform the direction change35* itself. Rather, it fires editor commands upon the key up event when a36* direction change needs to be performed; {@code goog.editor.Command.DIR_RTL}37* or {@code goog.editor.Command.DIR_RTL}.38*39*/4041goog.provide('goog.editor.plugins.FirstStrong');4243goog.require('goog.dom.NodeType');44goog.require('goog.dom.TagIterator');45goog.require('goog.dom.TagName');46goog.require('goog.editor.Command');47goog.require('goog.editor.Field');48goog.require('goog.editor.Plugin');49goog.require('goog.editor.node');50goog.require('goog.editor.range');51goog.require('goog.i18n.bidi');52goog.require('goog.i18n.uChar');53goog.require('goog.iter');54goog.require('goog.userAgent');55565758/**59* First Strong plugin.60* @constructor61* @extends {goog.editor.Plugin}62* @final63*/64goog.editor.plugins.FirstStrong = function() {65goog.editor.plugins.FirstStrong.base(this, 'constructor');6667/**68* Indicates whether or not the cursor is in a paragraph we have not yet69* finished evaluating for directionality. This is set to true whenever the70* cursor is moved, and set to false after seeing a strong character in the71* paragraph the cursor is currently in.72*73* @type {boolean}74* @private75*/76this.isNewBlock_ = true;7778/**79* Indicates whether or not the current paragraph the cursor is in should be80* set to Right-To-Left directionality.81*82* @type {boolean}83* @private84*/85this.switchToRtl_ = false;8687/**88* Indicates whether or not the current paragraph the cursor is in should be89* set to Left-To-Right directionality.90*91* @type {boolean}92* @private93*/94this.switchToLtr_ = false;95};96goog.inherits(goog.editor.plugins.FirstStrong, goog.editor.Plugin);979899/** @override */100goog.editor.plugins.FirstStrong.prototype.getTrogClassId = function() {101return 'FirstStrong';102};103104105/** @override */106goog.editor.plugins.FirstStrong.prototype.queryCommandValue = function(107command) {108return false;109};110111112/** @override */113goog.editor.plugins.FirstStrong.prototype.handleSelectionChange = function(114e, node) {115this.isNewBlock_ = true;116return false;117};118119120/**121* The name of the attribute which records the input text.122*123* @type {string}124* @const125*/126goog.editor.plugins.FirstStrong.INPUT_ATTRIBUTE = 'fs-input';127128129/** @override */130goog.editor.plugins.FirstStrong.prototype.handleKeyPress = function(e) {131if (goog.editor.Field.SELECTION_CHANGE_KEYCODES[e.keyCode]) {132// Key triggered selection change event (e.g. on ENTER) is throttled and a133// later LTR/RTL strong keypress may come before it. Need to capture it.134this.isNewBlock_ = true;135return false; // A selection-changing key is not LTR/RTL strong.136}137if (!this.isNewBlock_) {138return false; // We've already determined this paragraph's direction.139}140// Ignore non-character key press events.141if (e.ctrlKey || e.metaKey) {142return false;143}144var newInput = goog.i18n.uChar.fromCharCode(e.charCode);145146// IME's may return 0 for the charCode, which is a legitimate, non-Strong147// charCode, or they may return an illegal charCode (for which newInput will148// be false).149if (!newInput || !e.charCode) {150var browserEvent = e.getBrowserEvent();151if (browserEvent) {152if (goog.userAgent.IE && browserEvent['getAttribute']) {153newInput = browserEvent['getAttribute'](154goog.editor.plugins.FirstStrong.INPUT_ATTRIBUTE);155} else {156newInput =157browserEvent[goog.editor.plugins.FirstStrong.INPUT_ATTRIBUTE];158}159}160}161162if (!newInput) {163return false; // Unrecognized key.164}165166var isLtr = goog.i18n.bidi.isLtrChar(newInput);167var isRtl = !isLtr && goog.i18n.bidi.isRtlChar(newInput);168if (!isLtr && !isRtl) {169return false; // This character cannot change anything (it is not Strong).170}171// This character is Strongly LTR or Strongly RTL. We might switch direction172// on it now, but in any case we do not need to check any more characters in173// this paragraph after it.174this.isNewBlock_ = false;175176// Are there no Strong characters already in the paragraph?177if (this.isNeutralBlock_()) {178this.switchToRtl_ = isRtl;179this.switchToLtr_ = isLtr;180}181return false;182};183184185/**186* Calls the flip directionality commands. This is done here so things go into187* the redo-undo stack at the expected order; fist enter the input, then flip188* directionality.189* @override190*/191goog.editor.plugins.FirstStrong.prototype.handleKeyUp = function(e) {192if (this.switchToRtl_) {193var field = this.getFieldObject();194field.dispatchChange(true);195field.execCommand(goog.editor.Command.DIR_RTL);196this.switchToRtl_ = false;197} else if (this.switchToLtr_) {198var field = this.getFieldObject();199field.dispatchChange(true);200field.execCommand(goog.editor.Command.DIR_LTR);201this.switchToLtr_ = false;202}203return false;204};205206207/**208* @return {Element} The lowest Block element ancestor of the node where the209* next character will be placed.210* @private211*/212goog.editor.plugins.FirstStrong.prototype.getBlockAncestor_ = function() {213var start = this.getFieldObject().getRange().getStartNode();214// Go up in the DOM until we reach a Block element.215while (!goog.editor.plugins.FirstStrong.isBlock_(start)) {216start = start.parentNode;217}218return /** @type {Element} */ (start);219};220221222/**223* @return {boolean} Whether the paragraph where the next character will be224* entered contains only non-Strong characters.225* @private226*/227goog.editor.plugins.FirstStrong.prototype.isNeutralBlock_ = function() {228var root = this.getBlockAncestor_();229// The exact node with the cursor location. Simply calling getStartNode() on230// the range only returns the containing block node.231var cursor =232goog.editor.range.getDeepEndPoint(this.getFieldObject().getRange(), false)233.node;234235// In FireFox the BR tag also represents a change in paragraph if not inside a236// list. So we need special handling to only look at the sub-block between237// BR elements.238var blockFunction = (goog.userAgent.GECKO && !this.isList_(root)) ?239goog.editor.plugins.FirstStrong.isGeckoBlock_ :240goog.editor.plugins.FirstStrong.isBlock_;241var paragraph = this.getTextAround_(root, cursor, blockFunction);242// Not using {@code goog.i18n.bidi.isNeutralText} as it contains additional,243// unwanted checks to the content.244return !goog.i18n.bidi.hasAnyLtr(paragraph) &&245!goog.i18n.bidi.hasAnyRtl(paragraph);246};247248249/**250* Checks if an element is a list element ('UL' or 'OL').251*252* @param {Element} element The element to test.253* @return {boolean} Whether the element is a list element ('UL' or 'OL').254* @private255*/256goog.editor.plugins.FirstStrong.prototype.isList_ = function(element) {257if (!element) {258return false;259}260var tagName = element.tagName;261return tagName == goog.dom.TagName.UL || tagName == goog.dom.TagName.OL;262};263264265/**266* Returns the text within the local paragraph around the cursor.267* Notice that for GECKO a BR represents a pargraph change despite not being a268* block element.269*270* @param {Element} root The first block element ancestor of the node the cursor271* is in.272* @param {Node} cursorLocation Node where the cursor currently is, marking the273* paragraph whose text we will return.274* @param {function(Node): boolean} isParagraphBoundary The function to275* determine if a node represents the start or end of the paragraph.276* @return {string} the text in the paragraph around the cursor location.277* @private278*/279goog.editor.plugins.FirstStrong.prototype.getTextAround_ = function(280root, cursorLocation, isParagraphBoundary) {281// The buffer where we're collecting the text.282var buffer = [];283// Have we reached the cursor yet, or are we still before it?284var pastCursorLocation = false;285286if (root && cursorLocation) {287goog.iter.some(new goog.dom.TagIterator(root), function(node) {288if (node == cursorLocation) {289pastCursorLocation = true;290} else if (isParagraphBoundary(node)) {291if (pastCursorLocation) {292// This is the end of the paragraph containing the cursor. We're done.293return true;294} else {295// All we collected so far does not count; it was in a previous296// paragraph that did not contain the cursor.297buffer = [];298}299}300if (node.nodeType == goog.dom.NodeType.TEXT) {301buffer.push(node.nodeValue);302}303return false; // Keep going.304});305}306return buffer.join('');307};308309310/**311* @param {Node} node Node to check.312* @return {boolean} Does the given node represent a Block element? Notice we do313* not consider list items as Block elements in the algorithm.314* @private315*/316goog.editor.plugins.FirstStrong.isBlock_ = function(node) {317return !!node && goog.editor.node.isBlockTag(node) &&318/** @type {!Element} */ (node).tagName != goog.dom.TagName.LI;319};320321322/**323* @param {Node} node Node to check.324* @return {boolean} Does the given node represent a Block element from the325* point of view of FireFox? Notice we do not consider list items as Block326* elements in the algorithm.327* @private328*/329goog.editor.plugins.FirstStrong.isGeckoBlock_ = function(node) {330return !!node &&331(/** @type {!Element} */ (node).tagName == goog.dom.TagName.BR ||332goog.editor.plugins.FirstStrong.isBlock_(node));333};334335336