/**
* Copyright 2009 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 org.waveprotocol.wave.client.editor.selection.html;
import com.google.gwt.core.client.JavaScriptException;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.Node;
import org.waveprotocol.wave.client.common.util.DomHelper;
import org.waveprotocol.wave.client.common.util.QuirksConstants;
import org.waveprotocol.wave.client.common.util.UserAgent;
import org.waveprotocol.wave.client.common.util.DomHelper.ElementEditability;
/**
* Wrapper for W3C selection.
*
* http://www.whatwg.org/specs/web-apps/current-work/multipage/editing.html#documentSelection
*
*/
public class SelectionW3CNative extends JavaScriptObject {
protected SelectionW3CNative() {
}
/**
* Gets the current selection.
*
* NOTE(patcoleman): **IMPORTANT**
* It is possible the selection nodes will not be within the actual document (e.g. in shadow dom)
* This should only be used when it is known that the selection is in the page's real DOM,
* or when the selection is used to set or clear selection, rather than reading it.
*/
public static native SelectionW3CNative getSelectionUnsafe() /*-{
return $wnd.getSelection();
}-*/;
/**
* Gets the current selection, trying to place it in correspond actual DOM when the selection
* itself is reported as not being in the actual document.
* @return The selection in the page's DOM document, or null if it cannot be calculated.
*/
public static SelectionW3CNative getSelectionGuarded() {
SelectionW3CNative selection = getSelectionUnsafe();
// NOTE(patcoleman) -
// It is possible for the selection to be in a node in the shadow, which
// causes errors whenever you try to read attributes.
if (selection != null && DomHelper.isUnreadable(selection.anchorNode())) {
if (UserAgent.isFirefox()) {
// In firefox, the focus can be practically anywhere, so we give up:
return null;
} else if (UserAgent.isWebkit()) {
// In webkit, the focus should be on the element the shadow dom comes from:
selection.setCaret(NativeSelectionUtil.getActiveElement(), 0);
return selection;
}
return null; // not sure what anything else does, so be safe and assume we're beyond repair.
}
return selection;
}
/**
* Gets the selection range at specified index.
* @param index
*/
public final native JsRange getRangeAt(int index) /*-{
return this.getRangeAt(index);
}-*/;
public final native Node anchorNode() /*-{
return this.anchorNode;
}-*/;
public final native Node focusNode() /*-{
return this.focusNode;
}-*/;
public final native int anchorOffset() /*-{
return this.anchorOffset;
}-*/;
public final native int focusOffset() /*-{
return this.focusOffset;
}-*/;
/**
* Clears the selection.
*/
public final native void removeAllRanges() /*-{
this.removeAllRanges();
}-*/;
/**
* Returns the number of ranges selected.
*/
public final native int rangeCount() /*-{
return this.rangeCount;
}-*/;
/**
* Add a new range to the selection object.
* @param jsRange
*/
public final native void addRange(JsRange jsRange) /*-{
this.addRange(jsRange);
}-*/;
public final void setAnchorAndFocus(Node anchorNode, int anchorOffset,
Node focusNode, int focusOffset) {
if (QuirksConstants.HAS_BASE_AND_EXTENT) {
// NOTE(danilatos): While extend() would also work for webkit,
// we have to use setBaseAndExtent because it appears to reuse
// the existing range, rather than needing to replace it, and
// doing otherwise stuffs up the IME composition state
setBaseAndExtent(anchorNode, anchorOffset, focusNode, focusOffset);
} else {
// We assume that anchor node is in the same focusable area.
// If not, death will ensue.
setFocusElement(focusNode);
// TODO(danilatos): Investigate just using extend() twice - i.e.
// extend to the anchor -> collapse to end -> extend to the focus.
JsRange range = JsRange.create();
range.setStart(anchorNode, anchorOffset);
range.collapse(true);
removeAllRanges();
addRange(range);
if (focusNode != anchorNode || focusOffset != anchorOffset) {
try {
extend(focusNode, focusOffset);
} catch (JavaScriptException e) {
NativeSelectionUtil.LOG.error().logPlainText(
"error extending selection from " + anchorNode + ":" + anchorOffset +
" to " + focusNode + ":" + focusOffset);
removeAllRanges();
range = JsRange.create();
range.setStart(anchorNode, anchorOffset);
range.collapse(true);
range.setEnd(focusNode, focusOffset);
addRange(range);
}
}
}
}
public final void setCaret(Node node, int offset) {
// See notes in setAnchorAndFocus(Node, int, Node, int)
if (QuirksConstants.HAS_BASE_AND_EXTENT) {
setBaseAndExtent(node, offset, node, offset);
} else {
// Required by firefox. Sigh.
setFocusElement(node);
// TODO(danilatos): Investigate just using extend() twice - i.e.
// extend to the anchor -> collapse to end -> extend to the focus.
JsRange range = JsRange.create();
range.setStart(node, offset);
range.collapse(true);
removeAllRanges();
addRange(range);
}
}
/**
* Supported by at least FF and Webkit
*
* Warning: The focus must not be the same as the current anchor, or firefox
* will throw an exception. Also, the currently editable region must have
* focus, or firefox will throw an exception.
*
* @param focusNode
* @param focusOffset
*/
private final native void extend(Node focusNode, int focusOffset) /*-{
this.extend(focusNode, focusOffset);
}-*/;
/**
* Sets the selection to a single range.
* NOTE(patcoleman): ** Not all browsers, guard with QuirksConstants.HAS_BASE_AND_EXTENT **
*/
private final native void setBaseAndExtent(Node anchorNode, int anchorOffset,
Node focusNode, int focusOffset) /*-{
this.setBaseAndExtent(anchorNode, anchorOffset, focusNode, focusOffset);
}-*/;
/**
* Ensure that the rendered content view's DOM has focus
*
* NOTE(patcoleman): Fixes firefox bug that causes invalid selections while
* mutating DOM that doesn't have focus - fixed by finding the next parent element directly
* editable, and forcing this to have the focus.
*/
private static void setFocusElement(Node node) {
if (UserAgent.isFirefox()) {
// adjust to parent if node is a text node
Element toFocus = null;
if (DomHelper.isTextNode(node)) {
toFocus = node.getParentElement();
} else {
toFocus = node.<Element>cast();
}
// traverse up until we have a concretely editable element:
while (toFocus != null &&
DomHelper.getContentEditability(toFocus) != ElementEditability.EDITABLE) {
toFocus = toFocus.getParentElement();
}
// then focus it:
if (toFocus != null) {
DomHelper.focus(toFocus);
}
}
}
}