DOMSupport.cpp   [plain text]


/*
 * Copyright (C) 2011, 2012 Research In Motion Limited. All rights reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */

#include "config.h"
#include "DOMSupport.h"

#include "FloatQuad.h"
#include "Frame.h"
#include "FrameView.h"
#include "HTMLFormElement.h"
#include "HTMLInputElement.h"
#include "HTMLNames.h"
#include "HTMLTextAreaElement.h"
#include "Node.h"
#include "Range.h"
#include "RenderObject.h"
#include "RenderText.h"
#include "RenderTextControl.h"
#include "TextIterator.h"
#include "VisibleSelection.h"
#include "WTFString.h"

#include "htmlediting.h"
#include "visible_units.h"

#include <limits>

using WTF::Vector;

using namespace WebCore;

namespace BlackBerry {
namespace WebKit {
namespace DOMSupport {

void visibleTextQuads(const VisibleSelection& selection, Vector<FloatQuad>& quads)
{
    if (!selection.isRange())
        return;
    ASSERT(selection.firstRange());

    visibleTextQuads(*(selection.firstRange()), quads, true /* useSelectionHeight */);
}

void visibleTextQuads(const Range& range, Vector<FloatQuad>& quads, bool useSelectionHeight)
{
    // Range::textQuads includes hidden text, which we don't want.
    // To work around this, this is a copy of it which skips hidden elements.
    Node* startContainer = range.startContainer();
    Node* endContainer = range.endContainer();

    if (!startContainer || !endContainer)
        return;

    Node* stopNode = range.pastLastNode();
    for (Node* node = range.firstNode(); node != stopNode; node = node->traverseNextNode()) {
        RenderObject* r = node->renderer();
        if (!r || !r->isText())
            continue;

        if (r->style()->visibility() != VISIBLE)
            continue;

        RenderText* renderText = toRenderText(r);
        int startOffset = node == startContainer ? range.startOffset() : 0;
        int endOffset = node == endContainer ? range.endOffset() : std::numeric_limits<int>::max();
        renderText->absoluteQuadsForRange(quads, startOffset, endOffset, useSelectionHeight);
    }
}

bool isTextInputElement(Element* element)
{
    return element->isTextFormControl()
           || element->hasTagName(HTMLNames::textareaTag)
           || element->isContentEditable();
}

bool isPasswordElement(const Element* element)
{
    return element && element->hasTagName(HTMLNames::inputTag)
           && static_cast<const HTMLInputElement*>(element)->isPasswordField();
}

WTF::String inputElementText(Element* element)
{
    if (!element)
        return WTF::String();

    WTF::String elementText;
    if (element->hasTagName(HTMLNames::inputTag)) {
        const HTMLInputElement* inputElement = static_cast<const HTMLInputElement*>(element);
        elementText = inputElement->value();
    } else if (element->hasTagName(HTMLNames::textareaTag)) {
        const HTMLTextAreaElement* inputElement = static_cast<const HTMLTextAreaElement*>(element);
        elementText = inputElement->value();
    } else if (element->isContentEditable()) {
        RefPtr<Range> rangeForNode = rangeOfContents(element);
        elementText = rangeForNode.get()->text();
    }
    return elementText;
}

bool isElementTypePlugin(const Element* element)
{
    if (!element)
        return false;

    if (element->hasTagName(HTMLNames::objectTag)
        || element->hasTagName(HTMLNames::embedTag)
        || element->hasTagName(HTMLNames::appletTag))
        return true;

    return false;
}

HTMLTextFormControlElement* toTextControlElement(Node* node)
{
    if (!(node && node->isElementNode()))
        return 0;

    Element* element = static_cast<Element*>(node);
    if (!element->isFormControlElement())
        return 0;

    HTMLFormControlElement* formElement = static_cast<HTMLFormControlElement*>(element);
    if (!formElement->isTextFormControl())
        return 0;

    return static_cast<HTMLTextFormControlElement*>(formElement);
}

bool isPopupInputField(const Element* element)
{
    return isDateTimeInputField(element) || isColorInputField(element);
}

bool isDateTimeInputField(const Element* element)
{
    if (!element->hasTagName(HTMLNames::inputTag))
        return false;

    const HTMLInputElement* inputElement = static_cast<const HTMLInputElement*>(element);

    // The following types have popup's.
    if (inputElement->isDateControl()
        || inputElement->isDateTimeControl()
        || inputElement->isDateTimeLocalControl()
        || inputElement->isTimeControl()
        || inputElement->isMonthControl())
            return true;

    return false;
}

bool isColorInputField(const Element* element)
{
    if (!element->hasTagName(HTMLNames::inputTag))
        return false;

    const HTMLInputElement* inputElement = static_cast<const HTMLInputElement*>(element);

#if ENABLE(INPUT_TYPE_COLOR)
    if (inputElement->isColorControl())
        return true;
#endif

    return false;
}

// This is a Tristate return to allow us to override name matching when
// the attribute is expressly requested for a field. Default indicates
// that the setting is On which is the default but not expressly requested
// for the element being checked. On indicates that it is directly added
// to the element.
AttributeState elementSupportsAutocorrect(const Element* element)
{
    DEFINE_STATIC_LOCAL(QualifiedName, autocorrectAttr, (nullAtom, "autocorrect", nullAtom));
    return elementAttributeState(element, autocorrectAttr);
}

AttributeState elementSupportsAutocomplete(const Element* element)
{
    return elementAttributeState(element, HTMLNames::autocompleteAttr);
}

AttributeState elementAttributeState(const Element* element, const QualifiedName& attributeName)
{
    // First we check the input item itself. If the attribute is not defined,
    // we check its parent form.
    if (element->fastHasAttribute(attributeName)) {
        AtomicString attributeString = element->fastGetAttribute(attributeName);
        if (equalIgnoringCase(attributeString, "off"))
            return Off;
        if (equalIgnoringCase(attributeString, "on"))
            return On;
        // If we haven't returned, it wasn't set properly. Check the form for an explicit setting
        // because the attribute was provided, but invalid.
    }
    if (element->isFormControlElement()) {
        const HTMLFormControlElement* formElement = static_cast<const HTMLFormControlElement*>(element);
        if (formElement->form() && formElement->form()->fastHasAttribute(attributeName)) {
            AtomicString attributeString = formElement->form()->fastGetAttribute(attributeName);
            if (equalIgnoringCase(attributeString, "off"))
                return Off;
            if (equalIgnoringCase(attributeString, "on"))
                return On;
        }
    }

    return Default;
}

// Check if this is an input field that will be focused & require input support.
bool isTextBasedContentEditableElement(Element* element)
{
    if (!element)
        return false;

    if (element->isReadOnlyFormControl())
        return false;

    if (isPopupInputField(element))
        return false;

    return element->isTextFormControl() || element->isContentEditable();
}

IntRect transformedBoundingBoxForRange(const Range& range)
{
    // Based on Range::boundingBox, which does not handle transforms, and
    // RenderObject::absoluteBoundingBoxRect, which does.
    IntRect result;
    Vector<FloatQuad> quads;
    visibleTextQuads(range, quads);
    const size_t n = quads.size();
    for (size_t i = 0; i < n; ++i)
        result.unite(quads[i].enclosingBoundingBox());

    return result;
}

VisibleSelection visibleSelectionForInputElement(Element* element)
{
    return visibleSelectionForRangeInputElement(element, 0, inputElementText(element).length());
}

VisibleSelection visibleSelectionForRangeInputElement(Element* element, int start, int end)
{
    if (DOMSupport::toTextControlElement(element)) {
        RenderTextControl* textRender = toRenderTextControl(element->renderer());
        if (!textRender)
            return VisibleSelection();

        VisiblePosition startPosition = textRender->visiblePositionForIndex(start);
        VisiblePosition endPosition;
        if (start == end)
            endPosition = startPosition;
        else
            endPosition = textRender->visiblePositionForIndex(end);

        return VisibleSelection(startPosition, endPosition);
    }

    // Must be content editable, generate the range.
    RefPtr<Range> selectionRange = TextIterator::rangeFromLocationAndLength(element, start, end - start);

    if (!selectionRange)
        return VisibleSelection();

    if (start == end)
        return VisibleSelection(selectionRange.get()->startPosition(), DOWNSTREAM);

    VisiblePosition visibleStart(selectionRange->startPosition(), DOWNSTREAM);
    VisiblePosition visibleEnd(selectionRange->endPosition(), SEL_DEFAULT_AFFINITY);

    return VisibleSelection(visibleStart, visibleEnd);
}

Node* DOMContainerNodeForPosition(const Position& position)
{
    Node* nodeAtPos = position.containerNode();
    if (nodeAtPos && nodeAtPos->isInShadowTree())
        nodeAtPos = nodeAtPos->shadowAncestorNode();

    return nodeAtPos;
}

bool isPositionInNode(Node* node, const Position& position)
{
    if (!node)
        return false;

    Node* domNodeAtPos = DOMContainerNodeForPosition(position);
    if (!domNodeAtPos)
        return false;

    int offset = 0;
    if (domNodeAtPos == position.containerNode())
        offset = position.computeOffsetInContainerNode();

    RefPtr<Range> rangeForNode = rangeOfContents(node);
    int ec;

    return rangeForNode->isPointInRange(domNodeAtPos, offset, ec);
}

static bool matchesReservedStringEmail(const AtomicString& string)
{
    return string.contains("email", false /* caseSensitive */);
}

static bool matchesReservedStringUrl(const AtomicString& string)
{
    return equalIgnoringCase("url", string);
}

bool elementIdOrNameIndicatesEmail(const HTMLInputElement* inputElement)
{
    if (!inputElement)
        return false;

    if (matchesReservedStringEmail(inputElement->getIdAttribute()))
        return true;

    if (inputElement->fastHasAttribute(HTMLNames::nameAttr)) {
        if (matchesReservedStringEmail(inputElement->fastGetAttribute(HTMLNames::nameAttr)))
            return true;
    }

    return false;
}

bool elementIdOrNameIndicatesUrl(const HTMLInputElement* inputElement)
{
    if (!inputElement)
        return false;

    if (matchesReservedStringUrl(inputElement->getIdAttribute()))
        return true;

    if (inputElement->fastHasAttribute(HTMLNames::nameAttr)) {
        if (matchesReservedStringUrl(inputElement->fastGetAttribute(HTMLNames::nameAttr)))
            return true;
    }

    return false;
}

static bool matchesReservedStringPreventingAutocomplete(const AtomicString& string)
{
    if (matchesReservedStringEmail(string)
        || matchesReservedStringUrl(string)
        || string.contains("user", false /* caseSensitive */)
        || string.contains("name", false /* caseSensitive */)
        || string.contains("login", false /* caseSensitive */))
        return true;

    return false;
}

// This checks to see if an input element has a name or id attribute set to
// username or email. These are rough checks to avoid major sites that use
// login fields as input type=text and auto correction interfers with.
bool elementIdOrNameIndicatesNoAutocomplete(const Element* element)
{
    if (!element->hasTagName(HTMLNames::inputTag))
        return false;

    AtomicString idAttribute = element->getIdAttribute();
    if (matchesReservedStringPreventingAutocomplete(idAttribute))
        return true;

    if (element->fastHasAttribute(HTMLNames::nameAttr)) {
        AtomicString nameAttribute = element->fastGetAttribute(HTMLNames::nameAttr);
        if (matchesReservedStringPreventingAutocomplete(nameAttribute))
            return true;
    }

    return false;
}

bool elementPatternIndicatesNumber(const HTMLInputElement* inputElement)
{
    return elementPatternMatches("[0-9]", inputElement);
}

bool elementPatternIndicatesHexadecimal(const HTMLInputElement* inputElement)
{
    return elementPatternMatches("[0-9a-fA-F]", inputElement);
}

bool elementPatternMatches(const char* pattern, const HTMLInputElement* inputElement)
{
    WTF::String patternString(pattern);
    if (!inputElement || patternString.isEmpty())
        return false;

    if (inputElement->fastHasAttribute(HTMLNames::patternAttr)) {
        WTF::String patternAttribute = inputElement->fastGetAttribute(HTMLNames::patternAttr);
        if (patternAttribute.startsWith(patternString)) {
            // The pattern is for hexadecimal, make sure nothing else is permitted.

            // Check if it was an exact match.
            if (patternAttribute.length() == patternString.length())
                return true;

            // Check for *
            if (patternAttribute.length() == patternString.length() + 1 && patternAttribute[patternString.length()] == '*')
                return true;

            // Is the regex specifying a character count?
            if (patternAttribute[patternString.length()] != '{' || !patternAttribute.endsWith('}'))
                return false;

            // Make sure the number in the regex is actually a number.
            unsigned count = 0;
            patternString = patternString + "{%d}";
            return (sscanf(patternAttribute.latin1().data(), patternString.latin1().data() + '\0', &count) == 1) && count > 0;
        }
    }
    return false;
}

IntPoint convertPointToFrame(const Frame* sourceFrame, const Frame* targetFrame, const IntPoint& point, const bool clampToTargetFrame)
{
    ASSERT(sourceFrame && targetFrame);
    if (sourceFrame == targetFrame)
        return point;

    ASSERT(sourceFrame->view() && targetFrame->view());
    ASSERT(targetFrame->tree());

    Frame* targetFrameParent = targetFrame->tree()->parent();
    IntRect targetFrameRect = targetFrame->view()->frameRect();
    IntPoint targetPoint = point;

    // Convert the target frame rect to source window content coordinates. This is only required
    // if the parent frame is not the source. If the parent is the source, subframeRect
    // is already in source content coordinates.
    if (targetFrameParent != sourceFrame)
        targetFrameRect = sourceFrame->view()->windowToContents(targetFrameParent->view()->contentsToWindow(targetFrameRect));

    // Requested point is outside of target frame, return InvalidPoint.
    if (clampToTargetFrame && !targetFrameRect.contains(targetPoint))
        targetPoint = IntPoint(targetPoint.x() < targetFrameRect.x() ? targetFrameRect.x() : std::min(targetPoint.x(), targetFrameRect.maxX()),
                targetPoint.y() < targetFrameRect.y() ? targetFrameRect.y() : std::min(targetPoint.y(), targetFrameRect.maxY()));
    else if (!targetFrameRect.contains(targetPoint))
        return InvalidPoint;

    // Adjust the points to be relative to the target.
    return targetFrame->view()->windowToContents(sourceFrame->view()->contentsToWindow(targetPoint));
}

VisibleSelection visibleSelectionForClosestActualWordStart(const VisibleSelection& selection)
{
    // VisibleSelection validation has a special case when the caret is at the end of a paragraph where
    // it selects the paragraph marker. As well, if the position is at the end of a word, it will select
    // only the space between words. We want to select an actual word so we move the selection to
    // the start of the leftmost word if the character after the selection point is whitespace.
    if (selection.selectionType() != VisibleSelection::RangeSelection && isWhitespace(selection.visibleStart().characterAfter())) {
        VisibleSelection leftSelection(previousWordPosition(selection.start()));
        bool leftSelectionIsOnWord = !isWhitespace(leftSelection.visibleStart().characterAfter());

        VisibleSelection rangeSelection(endOfWord(leftSelection.start()), selection.visibleStart());
        int leftDistance = TextIterator::rangeLength(rangeSelection.toNormalizedRange().get());

        VisibleSelection rightSelection(nextWordPosition(selection.start()));
        rightSelection = previousWordPosition(rightSelection.start());
        bool rightSelectionIsOnWord = !isWhitespace(rightSelection.visibleStart().characterAfter());

        rangeSelection = VisibleSelection(rightSelection.visibleStart(), selection.visibleStart());
        int rightDistance = TextIterator::rangeLength(rangeSelection.toNormalizedRange().get());

        // Make sure we found an actual word. If not, return the original selection.
        if (!leftSelectionIsOnWord && !rightSelectionIsOnWord)
            return selection;

        if (!rightSelectionIsOnWord || (leftSelectionIsOnWord && leftDistance < rightDistance)) {
            // Left is closer or right is invalid.
            return leftSelection;
        }

        // Right is closer or equal, or left was invalid.
        return rightSelection;
    }

    // No adjustment required.
    return selection;
}

// This function is copied from WebCore/page/Page.cpp.
Frame* incrementFrame(Frame* curr, bool forward, bool wrapFlag)
{
    return forward
        ? curr->tree()->traverseNextWithWrap(wrapFlag)
        : curr->tree()->traversePreviousWithWrap(wrapFlag);
}

} // DOMSupport
} // WebKit
} // BlackBerry