123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334 |
- // 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) &&
- /** @type {!Element} */ (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 &&
- (/** @type {!Element} */ (node).tagName == goog.dom.TagName.BR ||
- goog.editor.plugins.FirstStrong.isBlock_(node));
- };
|