blob: e2a51a74c9c4fe886284fe3b6e8325c04d887d96 [file] [log] [blame]
/*
* Copyright 2010 Google Inc.
*
* 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.
*/
package com.google.gwt.user.client.ui;
import com.google.gwt.dom.client.Element;
import com.google.gwt.i18n.client.BidiUtils;
import com.google.gwt.i18n.client.HasDirection.Direction;
import com.google.gwt.i18n.shared.BidiFormatter;
import com.google.gwt.i18n.shared.DirectionEstimator;
import com.google.gwt.i18n.shared.HasDirectionEstimator;
import com.google.gwt.i18n.shared.WordCountDirectionEstimator;
import com.google.gwt.safehtml.shared.SafeHtml;
import com.google.gwt.safehtml.shared.annotations.IsSafeHtml;
import com.google.gwt.safehtml.shared.annotations.SuppressIsSafeHtmlCastCheck;
/**
* A helper class for displaying bidi (i.e. potentially opposite-direction) text
* or HTML in an element.
* Note: this class assumes that callers perform all their text/html and
* direction manipulations through it alone.
*/
public class DirectionalTextHelper implements HasDirectionEstimator {
/**
* A default direction estimator instance.
*/
public static final DirectionEstimator DEFAULT_DIRECTION_ESTIMATOR =
WordCountDirectionEstimator.get();
/**
* The DirectionEstimator object.
*/
private DirectionEstimator directionEstimator;
/**
* The target element.
*/
private final Element element;
/**
* The initial direction of the element.
*/
private Direction initialElementDir;
/**
* Whether direction was explicitly set on the last {@code setTextOrHtml}
* call. If so, {@link #setDirectionEstimator} will refrain from modifying the
* direction until {@link #setTextOrHtml} is called without specifying an
* explicit direction.
*/
private boolean isDirectionExplicitlySet;
/**
* Whether the element is inline (e.g. a <span> element, but not a block
* element like <div>).
* This is needed because direction is handled differently for inline elements
* and for non-inline elements.
*/
private final boolean isElementInline;
/**
* Whether the element contains a nested <span> element used to
* indicate the content's direction.
* <p>
* The element itself is used for this purpose when it is a block element
* (i.e. !isElementInline), but doing so on an inline element often results in
* garbling what follows it. Thus, when the element is inline, a nested
* &lt;span&gt; must be used to carry the content's direction, with an LRM or
* RLM character afterwards to prevent the garbling.
*/
private boolean isSpanWrapped;
/**
* The direction of the element's content.
* Note: this may not match the direction attribute of the element itself.
* See
* {@link #setText(String, com.google.gwt.i18n.client.HasDirection.Direction, boolean)
* setText(String, Direction, boolean)}
* for details.
*/
private Direction textDir;
/**
* @param element The widget's element holding text.
* @param isElementInline Whether the element is an inline element.
*/
public DirectionalTextHelper(Element element, boolean isElementInline) {
this.element = element;
this.isElementInline = isElementInline;
isSpanWrapped = false;
this.initialElementDir = BidiUtils.getDirectionOnElement(element);
textDir = initialElementDir;
// setDirectionEstimator shouldn't refresh appearance of initial empty text.
isDirectionExplicitlySet = true;
}
public DirectionEstimator getDirectionEstimator() {
return directionEstimator;
}
public Direction getTextDirection() {
return textDir;
}
/**
* Get the inner text of the element, taking the inner span wrap into
* consideration, if needed.
*
* @return the text
*/
public String getText() {
return getTextOrHtml(false /* isHtml */);
}
/**
* Get the inner html of the element, taking the inner span wrap into
* consideration, if needed.
*
* @return the html
*/
public String getHtml() {
return getTextOrHtml(true /* isHtml */);
}
/**
* Get the inner text or html of the element, taking the inner span wrap into consideration, if
* needed. Prefer using {@link #getText} or {@link #getHtml} instead of this method.
*
* @param isHtml true to get the inner html, false to get the inner text
* @return the text or html
*/
public String getTextOrHtml(boolean isHtml) {
Element elem = isSpanWrapped ? element.getFirstChildElement() : element;
return isHtml ? elem.getInnerHTML() : elem.getInnerText();
}
/**
* Provides implementation for HasDirection's method setDirection (normally
* deprecated), dealing with backwards compatibility issues.
* @deprecated
*/
@Deprecated
@SuppressIsSafeHtmlCastCheck
public void setDirection(Direction direction) {
BidiUtils.setDirectionOnElement(element, direction);
initialElementDir = direction;
/*
* For backwards compatibility, assure there's no span wrap, and update the
* content direction.
*/
setInnerTextOrHtml(getHtml(), true); // TODO: mXSS?
isSpanWrapped = false;
textDir = initialElementDir;
isDirectionExplicitlySet = true;
}
/**
* See note at
* {@link #setDirectionEstimator(com.google.gwt.i18n.shared.DirectionEstimator)}.
*/
public void setDirectionEstimator(boolean enabled) {
setDirectionEstimator(enabled ? DEFAULT_DIRECTION_ESTIMATOR : null);
}
/**
* Note: if the element already has non-empty content, this will update
* its direction according to the new estimator's result. This may cause
* flicker, and thus should be avoided; DirectionEstimator should be set
* before the element has any content.
*/
@SuppressIsSafeHtmlCastCheck
public void setDirectionEstimator(DirectionEstimator directionEstimator) {
this.directionEstimator = directionEstimator;
/*
* Refresh appearance unless direction was explicitly set on last
* setTextOrHtml call.
*/
if (!isDirectionExplicitlySet) {
setHtml(getHtml()); // TODO: mXSS
}
}
/**
* Sets the element's content to the given value (plain text).
* If direction estimation is off, the direction is verified to match the
* element's initial direction. Otherwise, the direction is affected as
* described at
* {@link #setText(String, com.google.gwt.i18n.client.HasDirection.Direction)
* setText(String, Direction)}.
*
* @param content the element's new content
*/
@SuppressIsSafeHtmlCastCheck
public void setText(String content) {
setTextOrHtml(content, false /* isHtml */);
}
/**
* Sets the element's content to the given value (html).
* If direction estimation is off, the direction is verified to match the
* element's initial direction. Otherwise, the direction is affected as
* described at
* {@link #setHtml(String, com.google.gwt.i18n.client.HasDirection.Direction)
* setHtml(String, Direction)}.
*
* @param content the element's new content
*/
public void setHtml(SafeHtml content) {
setHtml(content.asString());
}
/**
* Sets the element's content to the given value (html).
* If direction estimation is off, the direction is verified to match the
* element's initial direction. Otherwise, the direction is affected as
* described at
* {@link #setHtml(String, com.google.gwt.i18n.client.HasDirection.Direction)
* setHtml(String, Direction)}.
*
* @param content the element's new content
*/
public void setHtml(@IsSafeHtml String content) {
setTextOrHtml(content, true /* isHtml */);
}
/**
* Sets the element's content to the given value (either plain text or HTML).
* Prefer using {@link #setText(String) setText} or {@link #setHtml(String) setHtml} instead of
* this method.
*
* @param content the element's new content
* @param isHtml whether the content is HTML
*/
public void setTextOrHtml(@IsSafeHtml String content, boolean isHtml) {
if (directionEstimator == null) {
isSpanWrapped = false;
setInnerTextOrHtml(content, isHtml);
/*
* Preserves the initial direction of the element. This is different from
* passing the direction parameter explicitly as DEFAULT, which forces the
* element to inherit the direction from its parent.
*/
if (textDir != initialElementDir) {
textDir = initialElementDir;
BidiUtils.setDirectionOnElement(element, initialElementDir);
}
} else {
setTextOrHtml(content, directionEstimator.estimateDirection(content,
isHtml), isHtml);
}
isDirectionExplicitlySet = false;
}
/**
* Sets the element's content to the given value (plain text), applying the
* given direction.
* <p>
* Implementation details:
* <ul>
* <li> If the element is a block element, sets its dir attribute according
* to the given direction.
* <li> Otherwise (i.e. the element is inline), the direction is set using a
* nested &lt;span dir=...&gt; element which holds the content of the element.
* This nested span may be followed by a zero-width Unicode direction
* character (LRM or RLM). This manipulation is necessary to prevent garbling
* in case the direction of the element is opposite to the direction of its
* context. See {@link com.google.gwt.i18n.shared.BidiFormatter} for more
* details.
* </ul>
*
* @param content the element's new content
* @param dir the content's direction
*/
@SuppressIsSafeHtmlCastCheck
public void setText(String content, Direction dir) {
setTextOrHtml(content, dir, false /* isHtml */);
}
/**
* Sets the element's content to the given value (html), applying the given
* direction.
* <p>
* Implementation details:
* <ul>
* <li> If the element is a block element, sets its dir attribute according
* to the given direction.
* <li> Otherwise (i.e. the element is inline), the direction is set using a
* nested &lt;span dir=...&gt; element which holds the content of the element.
* This nested span may be followed by a zero-width Unicode direction
* character (LRM or RLM). This manipulation is necessary to prevent garbling
* in case the direction of the element is opposite to the direction of its
* context. See {@link com.google.gwt.i18n.shared.BidiFormatter} for more
* details.
* </ul>
*
* @param content the element's new content
* @param dir the content's direction
*/
public void setHtml(SafeHtml content, Direction dir) {
setHtml(content.asString(), dir);
}
/**
* Sets the element's content to the given value (html), applying the given
* direction.
* <p>
* Implementation details:
* <ul>
* <li> If the element is a block element, sets its dir attribute according
* to the given direction.
* <li> Otherwise (i.e. the element is inline), the direction is set using a
* nested &lt;span dir=...&gt; element which holds the content of the element.
* This nested span may be followed by a zero-width Unicode direction
* character (LRM or RLM). This manipulation is necessary to prevent garbling
* in case the direction of the element is opposite to the direction of its
* context. See {@link com.google.gwt.i18n.shared.BidiFormatter} for more
* details.
* </ul>
*
* @param content the element's new content
* @param dir the content's direction
*/
public void setHtml(@IsSafeHtml String content, Direction dir) {
setTextOrHtml(content, dir, true /* isHtml */);
}
/**
* Sets the element's content to the given value (either plain text or HTML),
* applying the given direction. Prefer using
* {@link #setText(String, com.google.gwt.i18n.client.HasDirection.Direction) setText} or
* {@link #setHtml(String, com.google.gwt.i18n.client.HasDirection.Direction) setHtml}
* instead of this method.
*
* @param content the element's new content
* @param dir the content's direction
* @param isHtml whether the content is HTML
*/
public void setTextOrHtml(@IsSafeHtml String content, Direction dir, boolean isHtml) {
textDir = dir;
// Set the text and the direction.
if (isElementInline) {
isSpanWrapped = true;
element.setInnerHTML(BidiFormatter.getInstanceForCurrentLocale(
true /* alwaysSpan */).spanWrapWithKnownDir(dir, content, isHtml));
} else {
isSpanWrapped = false;
BidiUtils.setDirectionOnElement(element, dir);
setInnerTextOrHtml(content, isHtml);
}
isDirectionExplicitlySet = true;
}
private void setInnerTextOrHtml(@IsSafeHtml String content, boolean isHtml) {
if (isHtml) {
element.setInnerHTML(content);
} else {
element.setInnerText(content);
}
}
}