/* * 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 "HTMLFrameOwnerElement.h" #include "HTMLInputElement.h" #include "HTMLNames.h" #include "HTMLTextAreaElement.h" #include "Node.h" #include "NodeTraversal.h" #include "Range.h" #include "RenderObject.h" #include "RenderText.h" #include "RenderTextControl.h" #include "TextIterator.h" #include "VisiblePosition.h" #include "VisibleSelection.h" #include "VisibleUnits.h" #include "htmlediting.h" #include #include using WTF::Vector; using namespace WebCore; namespace BlackBerry { namespace WebKit { namespace DOMSupport { void visibleTextQuads(const VisibleSelection& selection, Vector& quads) { if (!selection.isRange()) return; // Make sure that both start and end have valid nodes associated otherwise // this can crash. See PR 220628. if (!selection.start().anchorNode() || !selection.end().anchorNode()) return; visibleTextQuads(*(selection.firstRange()), quads, true /* useSelectionHeight */); } void visibleTextQuads(const Range& range, Vector& 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 && node != stopNode; node = NodeTraversal::next(node)) { 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::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(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(element); elementText = inputElement->value(); } else if (element->hasTagName(HTMLNames::textareaTag)) { const HTMLTextAreaElement* inputElement = static_cast(element); elementText = inputElement->value(); } else if (element->isContentEditable()) { RefPtr rangeForNode = rangeOfContents(element); elementText = rangeForNode.get()->text(); } return elementText; } WTF::String webWorksContext(const WebCore::Element* element) { if (!element) return WTF::String(); DEFINE_STATIC_LOCAL(QualifiedName, webworksContextAttr, (nullAtom, "data-webworks-context", nullAtom)); if (element->fastHasAttribute(webworksContextAttr)) return element->fastGetAttribute(webworksContextAttr); return WTF::String(); } 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 = toElement(node); if (!element->isFormControlElement()) return 0; HTMLFormControlElement* formElement = static_cast(element); if (!formElement->isTextFormControl()) return 0; return static_cast(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(element); // The following types have popup's. if (inputElement->isDateField() || inputElement->isDateTimeField() || inputElement->isDateTimeLocalField() || inputElement->isTimeField() || inputElement->isMonthField()) return true; return false; } bool isColorInputField(const Element* element) { if (!element->hasTagName(HTMLNames::inputTag)) return false; const HTMLInputElement* inputElement = static_cast(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 elementSupportsSpellCheck(const Element* element) { return elementAttributeState(element, HTMLNames::spellcheckAttr); } 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(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; } bool elementHasContinuousSpellCheckingEnabled(const PassRefPtr element) { return element && element->document()->frame() && element->document()->frame()->editor()->isContinuousSpellCheckingEnabled(); } // Check if this is an input field that will be focused & require input support. bool isTextBasedContentEditableElement(Element* element) { if (!element) return false; if (element->isReadOnlyNode() || element->isDisabledFormControl()) 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 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 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); } 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) { int leftDistance = 0; int rightDistance = 0; VisibleSelection leftSelection(previousWordPosition(selection.start())); bool leftSelectionIsOnWord = !isWhitespace(leftSelection.visibleStart().characterAfter()) && leftSelection.start().containerNode() == selection.start().containerNode(); if (leftSelectionIsOnWord) { VisibleSelection rangeSelection(endOfWord(leftSelection.start()), selection.visibleStart()); leftDistance = TextIterator::rangeLength(rangeSelection.toNormalizedRange().get()); } VisibleSelection rightSelection = previousWordPosition(nextWordPosition(selection.start())); bool rightSelectionIsOnWord = !isWhitespace(rightSelection.visibleStart().characterAfter()) && rightSelection.start().containerNode() == selection.start().containerNode(); if (rightSelectionIsOnWord) { VisibleSelection rangeSelection = VisibleSelection(rightSelection.visibleStart(), selection.visibleStart()); 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; } int offsetFromStartOfBlock(const VisiblePosition offset) { RefPtr range = makeRange(startOfBlock(offset), offset); if (!range) return -1; return range->text().latin1().length(); } VisibleSelection visibleSelectionForFocusedBlock(Element* element) { int textLength = inputElementText(element).length(); if (DOMSupport::toTextControlElement(element)) { RenderTextControl* textRender = toRenderTextControl(element->renderer()); if (!textRender) return VisibleSelection(); VisiblePosition startPosition = textRender->visiblePositionForIndex(0); VisiblePosition endPosition; if (textLength) endPosition = textRender->visiblePositionForIndex(textLength); else endPosition = startPosition; return VisibleSelection(startPosition, endPosition); } // Must be content editable, generate the range. RefPtr selectionRange = TextIterator::rangeFromLocationAndLength(element, 0, textLength); if (!selectionRange) return VisibleSelection(); if (!textLength) return VisibleSelection(selectionRange->startPosition(), DOWNSTREAM); return VisibleSelection(selectionRange->startPosition(), selectionRange->endPosition()); } // 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); } PassRefPtr trimWhitespaceFromRange(PassRefPtr range) { return trimWhitespaceFromRange(VisiblePosition(range->startPosition()), VisiblePosition(range->endPosition())); } PassRefPtr trimWhitespaceFromRange(VisiblePosition startPosition, VisiblePosition endPosition) { if (startPosition == endPosition || isRangeTextAllWhitespace(startPosition, endPosition)) return 0; while (isWhitespace(startPosition.characterAfter())) startPosition = startPosition.next(); while (isWhitespace(endPosition.characterBefore())) endPosition = endPosition.previous(); return makeRange(startPosition, endPosition); } bool isRangeTextAllWhitespace(VisiblePosition startPosition, VisiblePosition endPosition) { while (isWhitespace(startPosition.characterAfter())) { startPosition = startPosition.next(); if (startPosition == endPosition) return true; } return false; } bool isFixedPositionOrHasFixedPositionAncestor(RenderObject* renderer) { RenderObject* currentRenderer = renderer; while (currentRenderer) { if (currentRenderer->isOutOfFlowPositioned() && currentRenderer->style()->position() == FixedPosition) return true; // Check if the current frame is an iframe. If so, continue checking with the iframe's owner element. if (!currentRenderer->parent() && currentRenderer->isRenderView() && currentRenderer->frame() && currentRenderer->frame()->ownerElement()) { currentRenderer = currentRenderer->frame()->ownerElement()->renderer(); continue; } currentRenderer = currentRenderer->parent(); } return false; } Element* selectionContainerElement(const VisibleSelection& selection) { if (!selection.isRange()) return 0; Node* startContainer = 0; if (selection.firstRange()) startContainer = selection.firstRange()->startContainer(); if (!startContainer) return 0; Element* element = 0; if (startContainer->isInShadowTree()) element = startContainer->shadowHost(); else if (startContainer->isElementNode()) element = toElement(startContainer); else element = startContainer->parentElement(); return element; } BlackBerry::Platform::RequestedHandlePosition elementHandlePositionAttribute(const WebCore::Element* element) { BlackBerry::Platform::RequestedHandlePosition position = BlackBerry::Platform::SmartPlacement; if (!element) return position; DEFINE_STATIC_LOCAL(QualifiedName, qualifiedAttrNameForHandlePosition, (nullAtom, "data-blackberry-text-selection-handle-position", nullAtom)); AtomicString attributeString; if (element->fastHasAttribute(qualifiedAttrNameForHandlePosition)) attributeString = element->fastGetAttribute(qualifiedAttrNameForHandlePosition); if (attributeString.isNull() || attributeString.isEmpty()) return position; if (equalIgnoringCase(attributeString, "above")) position = BlackBerry::Platform::Above; else if (equalIgnoringCase(attributeString, "below")) position = BlackBerry::Platform::Below; return position; } } // DOMSupport } // WebKit } // BlackBerry