com.gargoylesoftware.htmlunit.javascript.host.dom.TextRange.java Source code

Java tutorial

Introduction

Here is the source code for com.gargoylesoftware.htmlunit.javascript.host.dom.TextRange.java

Source

/*
 * Copyright (c) 2002-2016 Gargoyle Software 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.gargoylesoftware.htmlunit.javascript.host.dom;

import static com.gargoylesoftware.htmlunit.javascript.configuration.BrowserName.IE;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.w3c.dom.ranges.Range;

import com.gargoylesoftware.htmlunit.html.HtmlPage;
import com.gargoylesoftware.htmlunit.html.impl.SelectableTextInput;
import com.gargoylesoftware.htmlunit.html.impl.SimpleRange;
import com.gargoylesoftware.htmlunit.javascript.SimpleScriptable;
import com.gargoylesoftware.htmlunit.javascript.configuration.JsxClass;
import com.gargoylesoftware.htmlunit.javascript.configuration.JsxClasses;
import com.gargoylesoftware.htmlunit.javascript.configuration.JsxFunction;
import com.gargoylesoftware.htmlunit.javascript.configuration.JsxGetter;
import com.gargoylesoftware.htmlunit.javascript.configuration.JsxSetter;
import com.gargoylesoftware.htmlunit.javascript.configuration.WebBrowser;
import com.gargoylesoftware.htmlunit.javascript.host.Window;
import com.gargoylesoftware.htmlunit.javascript.host.html.HTMLElement;

import net.sourceforge.htmlunit.corejs.javascript.Context;
import net.sourceforge.htmlunit.corejs.javascript.Undefined;

/**
 * A JavaScript object for {@code TextRange} (IE only).
 *
 * @see <a href="http://msdn2.microsoft.com/en-us/library/ms535872.aspx">MSDN documentation (1)</a>
 * @see <a href="http://msdn2.microsoft.com/en-us/library/ms533042.aspx">MSDN documentation (2)</a>
 * @author Ahmed Ashour
 * @author Marc Guillemot
 * @author David Gileadi
 */
@JsxClasses({ @JsxClass(browsers = { @WebBrowser(IE) }) })
public class TextRange extends SimpleScriptable {

    private static final Log LOG = LogFactory.getLog(TextRange.class);

    /** The wrapped selection range. */
    private Range range_;

    /**
     * Default constructor used to build the prototype.
     */
    public TextRange() {
        // Empty.
    }

    /**
     * Constructs a text range around the provided element.
     * @param elt the element to wrap
     */
    public TextRange(final HTMLElement elt) {
        range_ = new SimpleRange(elt.getDomNodeOrDie());
    }

    /**
     * Constructs a text range around the provided range.
     * @param range the initial range
     */
    public TextRange(final Range range) {
        range_ = range.cloneRange();
    }

    /**
     * Retrieves the text contained within the range.
     * @return the text contained within the range
     */
    @JsxGetter
    public String getText() {
        return range_.toString();
    }

    /**
     * Sets the text contained within the range.
     * @param text the text contained within the range
     */
    @JsxSetter
    public void setText(final String text) {
        if (range_.getStartContainer() == range_.getEndContainer()
                && range_.getStartContainer() instanceof SelectableTextInput) {
            final SelectableTextInput input = (SelectableTextInput) range_.getStartContainer();
            final String oldValue = input.getText();
            input.setText(oldValue.substring(0, input.getSelectionStart()) + text
                    + oldValue.substring(input.getSelectionEnd()));
        }
    }

    /**
     * Retrieves the HTML fragment contained within the range.
     * @return the HTML fragment contained within the range
     */
    @JsxGetter
    public String getHtmlText() {
        final org.w3c.dom.Node node = range_.getCommonAncestorContainer();
        if (null == node) {
            return "";
        }
        final HTMLElement element = (HTMLElement) getScriptableFor(node);
        return element.getOuterHTML(); // TODO: not quite right, but good enough for now
    }

    /**
     * Duplicates this TextRange instance.
     * @see <a href="http://msdn.microsoft.com/en-us/library/ms536416.aspx">MSDN documentation</a>
     * @return a duplicate of this TextRange instance
     */
    @JsxFunction
    public Object duplicate() {
        final TextRange range = new TextRange(range_.cloneRange());
        range.setParentScope(getParentScope());
        range.setPrototype(getPrototype());
        return range;
    }

    /**
     * Retrieves the parent element for the given text range.
     * The parent element is the element that completely encloses the text in the range.
     * If the text range spans text in more than one element, this method returns the smallest element that encloses
     * all the elements. When you insert text into a range that spans multiple elements, the text is placed in the
     * parent element rather than in any of the contained elements.
     *
     * @see <a href="http://msdn.microsoft.com/en-us/library/ms536654.aspx">MSDN doc</a>
     * @return the parent element object if successful, or null otherwise.
     */
    @JsxFunction
    public Node parentElement() {
        final org.w3c.dom.Node parent = range_.getCommonAncestorContainer();
        if (null == parent) {
            if (null == range_.getStartContainer() || null == range_.getEndContainer()) {
                try {
                    final Window window = (Window) getParentScope();
                    final HtmlPage page = (HtmlPage) window.getDomNodeOrDie();
                    return (Node) getScriptableFor(page.getBody());
                } catch (final Exception e) {
                    // ok bad luck
                }
            }
            return null;
        }
        return (Node) getScriptableFor(parent);
    }

    /**
     * Collapses the range.
     * @param toStart indicates if collapse should be done to the start
     * @see <a href="http://msdn.microsoft.com/en-us/library/ms536371.aspx">MSDN doc</a>
     */
    @JsxFunction
    public void collapse(final boolean toStart) {
        range_.collapse(toStart);
    }

    /**
     * Makes the current range the active selection.
     *
     * @see <a href="http://msdn.microsoft.com/en-us/library/ms536735.aspx">MSDN doc</a>
     */
    @JsxFunction
    public void select() {
        final HtmlPage page = (HtmlPage) getWindow().getDomNodeOrDie();
        page.setSelectionRange(range_);
    }

    /**
     * Changes the start position of the range.
     * @param unit specifies the units to move
     * @param count the number of units to move
     * @return the number of units moved
     */
    @JsxFunction
    public int moveStart(final String unit, final Object count) {
        if (!"character".equals(unit)) {
            LOG.warn("moveStart('" + unit + "') is not yet supported");
            return 0;
        }
        int c = 1;
        if (count != Undefined.instance) {
            c = (int) Context.toNumber(count);
        }
        if (range_.getStartContainer() == range_.getEndContainer()
                && range_.getStartContainer() instanceof SelectableTextInput) {
            final SelectableTextInput input = (SelectableTextInput) range_.getStartContainer();
            c = constrainMoveBy(c, range_.getStartOffset(), input.getText().length());
            range_.setStart(input, range_.getStartOffset() + c);
        }
        return c;
    }

    /**
     * Collapses the given text range and moves the empty range by the given number of units.
     * @param unit specifies the units to move
     * @param count the number of units to move
     * @return the number of units moved
     */
    @JsxFunction
    public int move(final String unit, final Object count) {
        collapse(true);
        return moveStart(unit, count);
    }

    /**
     * Changes the end position of the range.
     * @param unit specifies the units to move
     * @param count the number of units to move
     * @return the number of units moved
     */
    @JsxFunction
    public int moveEnd(final String unit, final Object count) {
        if (!"character".equals(unit)) {
            LOG.warn("moveEnd('" + unit + "') is not yet supported");
            return 0;
        }
        int c = 1;
        if (count != Undefined.instance) {
            c = (int) Context.toNumber(count);
        }
        if (range_.getStartContainer() == range_.getEndContainer()
                && range_.getStartContainer() instanceof SelectableTextInput) {
            final SelectableTextInput input = (SelectableTextInput) range_.getStartContainer();
            c = constrainMoveBy(c, range_.getEndOffset(), input.getText().length());
            range_.setEnd(input, range_.getEndOffset() + c);
        }
        return c;
    }

    /**
     * Moves the text range so that the start and end positions of the range encompass
     * the text in the specified element.
     * @param element the element to move to
     * @see <a href="http://msdn.microsoft.com/en-us/library/ms536630.aspx">MSDN Documentation</a>
     */
    @JsxFunction
    public void moveToElementText(final HTMLElement element) {
        range_.selectNode(element.getDomNodeOrDie());
    }

    /**
     * Indicates if a range is contained in current one.
     * @param other the other range
     * @return {@code true} if <code>other</code> is contained within current range
     * @see <a href="http://msdn.microsoft.com/en-us/library/ms536371.aspx">MSDN doc</a>
     */
    @JsxFunction
    public boolean inRange(final TextRange other) {
        final Range otherRange = other.range_;

        final org.w3c.dom.Node start = range_.getStartContainer();
        final org.w3c.dom.Node otherStart = otherRange.getStartContainer();
        if (otherStart == null) {
            return false;
        }
        final short startComparison = start.compareDocumentPosition(otherStart);
        final boolean startNodeBefore = startComparison == 0
                || (startComparison & Node.DOCUMENT_POSITION_CONTAINS) != 0
                || (startComparison & Node.DOCUMENT_POSITION_PRECEDING) != 0;
        if (startNodeBefore && (start != otherStart || range_.getStartOffset() <= otherRange.getStartOffset())) {
            final org.w3c.dom.Node end = range_.getEndContainer();
            final org.w3c.dom.Node otherEnd = otherRange.getEndContainer();
            final short endComparison = end.compareDocumentPosition(otherEnd);
            final boolean endNodeAfter = endComparison == 0
                    || (endComparison & Node.DOCUMENT_POSITION_CONTAINS) != 0
                    || (endComparison & Node.DOCUMENT_POSITION_FOLLOWING) != 0;
            if (endNodeAfter && (end != otherEnd || range_.getEndOffset() >= otherRange.getEndOffset())) {
                return true;
            }
        }

        return false;
    }

    /**
     * Sets the endpoint of the range based on the endpoint of another range..
     * @param type end point transfer type. One of "StartToEnd", "StartToStart", "EndToStart" and "EndToEnd"
     * @param other the other range
     * @see <a href="http://msdn.microsoft.com/en-us/library/ms536745.aspx">MSDN doc</a>
     */
    @JsxFunction
    public void setEndPoint(final String type, final TextRange other) {
        final Range otherRange = other.range_;

        final org.w3c.dom.Node target;
        final int offset;
        if (type.endsWith("ToStart")) {
            target = otherRange.getStartContainer();
            offset = otherRange.getStartOffset();
        } else {
            target = otherRange.getEndContainer();
            offset = otherRange.getEndOffset();
        }

        if (type.startsWith("Start")) {
            range_.setStart(target, offset);
        } else {
            range_.setEnd(target, offset);
        }
    }

    /**
     * Constrain the given amount to move the range by to the limits of the given current offset and text length.
     * @param moveBy the amount to move by
     * @param current the current index
     * @param textLength the text length
     * @return the moveBy amount constrained to the text length
     */
    protected int constrainMoveBy(int moveBy, final int current, final int textLength) {
        final int to = current + moveBy;
        if (to < 0) {
            moveBy -= to;
        } else if (to >= textLength) {
            moveBy -= to - textLength;
        }
        return moveBy;
    }

    /**
     * Retrieves a bookmark (opaque string) that can be used with {@link #moveToBookmark}
     * to return to the same range.
     * The current implementation return empty string
     * @return the bookmark
     */
    @JsxFunction
    public String getBookmark() {
        return "";
    }

    /**
     * Moves to a bookmark.
     * The current implementation does nothing
     * @param bookmark the bookmark
     * @return {@code false}
     */
    @JsxFunction
    public boolean moveToBookmark(final String bookmark) {
        return false;
    }

    /**
     * Compares an end point of a TextRange object with an end point of another range.
     * @param how how to compare
     * @param sourceRange the other range
     * @return the result
     */
    @JsxFunction
    public int compareEndPoints(final String how, final TextRange sourceRange) {
        final org.w3c.dom.Node start;
        final org.w3c.dom.Node otherStart;
        switch (how) {
        case "StartToEnd":
            start = range_.getStartContainer();
            otherStart = sourceRange.range_.getEndContainer();
            break;

        case "StartToStart":
            start = range_.getStartContainer();
            otherStart = sourceRange.range_.getStartContainer();
            break;

        case "EndToStart":
            start = range_.getEndContainer();
            otherStart = sourceRange.range_.getStartContainer();
            break;

        default:
            start = range_.getEndContainer();
            otherStart = sourceRange.range_.getEndContainer();
            break;
        }
        if (start == null || otherStart == null) {
            return 0;
        }
        return start.compareDocumentPosition(otherStart);
    }
}