/** * 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.dom.client.Node; import org.waveprotocol.wave.client.common.util.DomHelper; import org.waveprotocol.wave.common.logging.LoggerBundle; import org.waveprotocol.wave.model.document.util.FocusedPointRange; import org.waveprotocol.wave.model.document.util.Point; import org.waveprotocol.wave.model.document.util.PointRange; /** * Firefox/Webkit common implementation * * @author danilatos@google.com (Daniel Danilatos) */ public class SelectionImplW3C extends SelectionImpl { /** * Shorthand selection debug logger */ @SuppressWarnings("hiding") static LoggerBundle logger = NativeSelectionUtil.LOG; // NOTE(danilatos): This doesn't handle multiple ranges. Not a big worry // for now anyway. private FocusedPointRange<Node> savedSelection; /** * Constructor */ SelectionImplW3C() { logger.trace().log("Constructed SelectionImplW3C"); } /** * Fast implementation to check if there is a selection or not. * @return true if there is a selection */ @Override boolean selectionExists() { return nativeCheckSelectionExists(); } private static native boolean nativeCheckSelectionExists() /*-{ var selection = $wnd.getSelection(); return selection.rangeCount > 0; }-*/; @Override FocusedPointRange<Node> get() { SelectionW3CNative selection = SelectionW3CNative.getSelectionGuarded(); if (selection == null) { // NOTE(danilatos): This seems to happen very rarely with FF. // I wonder what the reproducible cause is. return null; } Node anchorNode = selection.anchorNode(); Node focusNode = selection.focusNode(); return focusNode == null ? null : constructRange( anchorNode, selection.anchorOffset(), focusNode, selection.focusOffset()); } @Override PointRange<Node> getOrdered() { JsRange jsRange = getSelectionRange(); return toOrderedPointRange(jsRange); } @Override boolean isOrdered() { JsRange jsRange = getSelectionRange(); if (jsRange == null) { return true; } SelectionW3CNative selection = SelectionW3CNative.getSelectionGuarded(); Node anchorNode = selection.anchorNode(); int anchorOffset = selection.anchorOffset(); boolean ret = anchorNode == jsRange.startContainer() && anchorOffset == jsRange.startOffset(); // check that if the anchor doesn't match the start, then the focus does. assert ret || focusIsAtStart(selection, jsRange); return ret; } boolean focusIsAtStart(SelectionW3CNative selection, JsRange jsRange) { Node focusNode = selection.focusNode(); int focusOffset = selection.focusOffset(); return focusNode == jsRange.startContainer() && focusOffset == jsRange.startOffset(); } /** * Gets the native JsRange for the current selection. */ JsRange getSelectionRange() { SelectionW3CNative selection = SelectionW3CNative.getSelectionGuarded(); return selection != null && selection.rangeCount() > 0 ? selection.getRangeAt(0) : null; } /** * Construct a point range from a js range */ public static PointRange<Node> toOrderedPointRange(JsRange jsRange) { if (jsRange == null) { return null; } return new PointRange<Node>( DomHelper.nodeOffsetToNodeletPoint(jsRange.startContainer(), jsRange.startOffset()), DomHelper.nodeOffsetToNodeletPoint(jsRange.endContainer(), jsRange.endOffset())); } @Override void set(Point<Node> anchor, Point<Node> focus) { int anchorOffset; int focusOffset; JsRange range = JsRange.create(); Node anchorNode = anchor.getContainer(); Node focusNode = focus.getContainer(); assert anchorNode != null : "Anchor node must not be null."; assert focusNode != null : "Focus node must not be null."; if (anchor.isInTextNode()) { anchorOffset = anchor.getTextOffset(); } else if (anchor.getNodeAfter() == null) { anchorOffset = anchorNode.getChildCount(); } else { anchorNode = anchor.getNodeAfter().getParentElement(); range.setStartBefore(anchor.getNodeAfter()); anchorOffset = range.startOffset(); } if (focus.isInTextNode()) { focusOffset = focus.getTextOffset(); } else { Node focusNodeAfter = focus.getNodeAfter(); if (focusNodeAfter == null) { focusOffset = focusNode.getChildCount(); } else { focusNode = focusNodeAfter.getParentElement(); assert focusNode != null : "focus node must not be null"; range.setStartBefore(focusNodeAfter); focusOffset = range.startOffset(); } } SelectionW3CNative.getSelectionUnsafe().setAnchorAndFocus( anchorNode, anchorOffset, focusNode, focusOffset); } @Override void set(Point<Node> focus) { int anchorOffset; int focusOffset; JsRange range = JsRange.create(); Node focusNode = focus.getContainer(); if (focus.isInTextNode()) { focusOffset = focus.getTextOffset(); } else if (focus.getNodeAfter() == null) { focusOffset = focusNode.getChildCount(); } else { range.setStartBefore(focus.getNodeAfter()); focusOffset = range.startOffset(); } SelectionW3CNative.getSelectionUnsafe().setCaret(focusNode, focusOffset); } /** * Clears selection */ @Override void clear() { SelectionW3CNative.getSelectionUnsafe().removeAllRanges(); } /** * Factory method for JSNI use * * @param anchorNode * @param anchorOffset * @param focusNode * @param focusOffset * @return new Range object */ private static FocusedPointRange<Node> constructRange( Node anchorNode, int anchorOffset, Node focusNode, int focusOffset) { return new FocusedPointRange<Node>( DomHelper.nodeOffsetToNodeletPoint(anchorNode, anchorOffset), DomHelper.nodeOffsetToNodeletPoint(focusNode, focusOffset)); } @Override void saveSelection() { savedSelection = get(); } @Override void restoreSelection() { if (savedSelection != null) { set(savedSelection.getAnchor(), savedSelection.getFocus()); } } }