+2006-07-06 Justin Garcia <justin.garcia@apple.com>
+
+ Reviewed by levi
+
+ <rdar://problem/4609184>
+ Mail crashes at WebCore::Selection::adjustForEditableContent
+ <rdar://problem/4609140>
+ Crash at WebCore::Range::compareBoundaryPoints
+
+ * editing/selection/mixed-editability-1-expected.checksum: Added.
+ * editing/selection/mixed-editability-1-expected.png: Added.
+ * editing/selection/mixed-editability-1-expected.txt: Added.
+ * editing/selection/mixed-editability-1.html: Added.
+ * editing/selection/mixed-editability-2-expected.checksum: Added.
+ * editing/selection/mixed-editability-2-expected.png: Added.
+ * editing/selection/mixed-editability-2-expected.txt: Added.
+ * editing/selection/mixed-editability-2.html: Added.
+
2006-07-06 Anders Carlsson <acarlsson@apple.com>
Reviewed by Adele.
--- /dev/null
+3dca5749d73aa0bd06f5da211a83cd8f
\ No newline at end of file
--- /dev/null
+EDITING DELEGATE: shouldBeginEditingInDOMRange:range from 0 of BODY > HTML > #document to 4 of BODY > HTML > #document
+EDITING DELEGATE: webViewDidBeginEditing:WebViewDidBeginEditingNotification
+EDITING DELEGATE: shouldChangeSelectedDOMRange:(null) toDOMRange:range from 1 of #text > SPAN > BODY > HTML > #document to 1 of #text > SPAN > BODY > HTML > #document affinity:NSSelectionAffinityDownstream stillSelecting:FALSE
+EDITING DELEGATE: webViewDidChangeSelection:WebViewDidChangeSelectionNotification
+EDITING DELEGATE: shouldChangeSelectedDOMRange:range from 1 of #text > SPAN > BODY > HTML > #document to 1 of #text > SPAN > BODY > HTML > #document toDOMRange:range from 0 of #text > SPAN > BODY > HTML > #document to 2 of #text > SPAN > BODY > HTML > #document affinity:NSSelectionAffinityDownstream stillSelecting:FALSE
+EDITING DELEGATE: webViewDidChangeSelection:WebViewDidChangeSelectionNotification
+layer at (0,0) size 800x600
+ RenderView at (0,0) size 800x600
+layer at (0,0) size 800x600
+ RenderBlock {HTML} at (0,0) size 800x600
+ RenderBody {BODY} at (8,8) size 784x584
+ RenderInline {SPAN} at (0,0) size 20x18
+ RenderText {#text} at (0,0) size 20x18
+ text run at (0,0) width 20: "On"
+ RenderText {#text} at (20,0) size 784x36
+ text run at (20,0) width 464: "ly the first two letters of the first word in this sentence should be selected. "
+ text run at (484,0) width 300: "To run this test manually, double click between "
+ text run at (0,18) width 351: "the first two letters of the first word of the first sentence."
+ RenderText {#text} at (0,0) size 0x0
+selection start: position 0 of child 0 {#text} of child 0 {SPAN} of child 0 {BODY} of child 0 {HTML} of document
+selection end: position 2 of child 0 {#text} of child 0 {SPAN} of child 0 {BODY} of child 0 {HTML} of document
--- /dev/null
+
+<body contenteditable="true"><span id="start" contenteditable="false">On</span>ly the first two letters of the first word in this sentence should be selected. To run this test manually, double click between the first two letters of the first word of the first sentence.</body>
+
+<script>
+if (window.layoutTestController) {
+ var s = window.getSelection();
+
+ var start = document.getElementById("start");
+
+ var x, y;
+
+ x = start.offsetParent.offsetTop + start.offsetLeft + start.offsetWidth / 2;
+ y = start.offsetParent.offsetTop + start.offsetTop + start.offsetHeight / 2;
+
+ eventSender.mouseMoveTo(x, y);
+ eventSender.mouseDown();
+ eventSender.mouseUp();
+ eventSender.mouseDown();
+ eventSender.mouseUp();
+}
+</script>
\ No newline at end of file
--- /dev/null
+3c823324cbf8c851a2ede3c9d3456890
\ No newline at end of file
--- /dev/null
+EDITING DELEGATE: webViewDidChangeSelection:WebViewDidChangeSelectionNotification
+layer at (0,0) size 800x600
+ RenderView at (0,0) size 800x600
+layer at (0,0) size 800x600
+ RenderBlock {HTML} at (0,0) size 800x600
+ RenderBody {BODY} at (8,8) size 784x584
+ RenderBlock {DIV} at (0,0) size 784x18
+ RenderText {#text} at (0,0) size 57x18
+ text run at (0,0) width 57: "OnlyThe"
+ RenderInline {SPAN} at (0,0) size 32x18
+ RenderText {#text} at (57,0) size 32x18
+ text run at (57,0) width 32: "Bold"
+ RenderText {#text} at (89,0) size 74x18
+ text run at (89,0) width 74: "TextShould"
+ RenderInline {SPAN} at (0,0) size 18x18
+ RenderText {#text} at (163,0) size 18x18
+ text run at (163,0) width 18: "Be"
+ RenderText {#text} at (181,0) size 53x18
+ text run at (181,0) width 53: "Selected"
+selection start: position 0 of child 0 {#text} of child 1 {SPAN} of child 1 {DIV} of child 0 {BODY} of child 0 {HTML} of document
+selection end: position 4 of child 0 {#text} of child 1 {SPAN} of child 1 {DIV} of child 0 {BODY} of child 0 {HTML} of document
--- /dev/null
+<body>
+<div contenteditable="true">
+OnlyThe<span id="base" style="font-weight:bold;" contenteditable="false">Bold</span>TextShould<span id="extent">Be</span>Selected
+</div>
+<script>
+var s = window.getSelection();
+var base = document.getElementById("base");
+var extent = document.getElementById("extent");
+s.setBaseAndExtent(base, 0, extent, 0);
+</script>
+</body>
\ No newline at end of file
+2006-07-06 Justin Garcia <justin.garcia@apple.com>
+
+ Reviewed by levi
+
+ <rdar://problem/4609184>
+ Mail crashes at WebCore::Selection::adjustForEditableContent
+ <rdar://problem/4609140>
+ Crash at WebCore::Range::compareBoundaryPoints
+
+ * editing/Selection.cpp:
+ (WebCore::Selection::adjustForEditableContent):
+ * editing/VisiblePosition.cpp:
+ (WebCore::VisiblePosition::next): Moved code into a helper function.
+ (WebCore::VisiblePosition::previous): Ditto.
+ * editing/htmlediting.cpp:
+ (WebCore::comparePositions): Moved from Selection.cpp.
+ (WebCore::lowestEditableAncestor): Added. Returns rootEditableElement
+ for a node in editable content and the rootEditableElement for the first
+ ancestor that's editable for a node in non-editable content.
+ (WebCore::firstEditablePositionAfterPositionInRoot):
+ (WebCore::lastEditablePositionBeforePositionInRoot):
+ * editing/htmlediting.h:
+
2006-07-06 Anders Carlsson <acarlsson@apple.com>
Reviewed by Adele.
return true;
}
-// Compare two positions, taking into account the possibility that one or both
-// could be inside a shadow tree. Only works for non-null values.
-static int comparePositions(const Position& a, const Position& b)
-{
- Node* nodeA = a.node();
- ASSERT(nodeA);
- Node* nodeB = b.node();
- ASSERT(nodeB);
- int offsetA = a.offset();
- int offsetB = b.offset();
-
- Node* shadowAncestorA = nodeA->shadowAncestorNode();
- if (shadowAncestorA == nodeA)
- shadowAncestorA = 0;
- Node* shadowAncestorB = nodeB->shadowAncestorNode();
- if (shadowAncestorB == nodeB)
- shadowAncestorB = 0;
-
- if (shadowAncestorA != shadowAncestorB) {
- if (shadowAncestorA) {
- nodeA = shadowAncestorA;
- offsetA = 0;
- }
- if (shadowAncestorB) {
- nodeB = shadowAncestorB;
- offsetB = 0;
- }
- }
-
- return Range::compareBoundaryPoints(nodeA, offsetA, nodeB, offsetB);
-}
-
void Selection::validate()
{
// Move the selection to rendered positions, if possible.
Node *startRoot = m_start.node()->rootEditableElement();
Node *endRoot = m_end.node()->rootEditableElement();
+ Node* baseEditableAncestor = lowestEditableAncestor(m_base.node());
+
// The base, start and end are all in the same region. No adjustment necessary.
if (baseRoot == startRoot && baseRoot == endRoot)
return;
- // The selection is based in an editable area. Keep both sides from reaching outside that area.
+ // The selection is based in editable content.
if (baseRoot) {
- // If the start is outside the base's editable root, cap it at the start of that editable root.
- if (baseRoot != startRoot) {
- VisiblePosition first(Position(baseRoot, 0));
+ // If the start is outside the base's editable root, cap it at the start of that root.
+ // If the start is in non-editable content that is inside the base's editable root, put it
+ // at the first editable position after start inside the base's editable root.
+ if (startRoot != baseRoot) {
+ VisiblePosition first = firstEditablePositionAfterPositionInRoot(m_start, baseRoot);
m_start = first.deepEquivalent();
}
- // If the end is outside the base's editable root, cap it at the end of that editable root.
- if (baseRoot != endRoot) {
- VisiblePosition last(Position(baseRoot, maxDeepOffset(baseRoot)));
+ // If the end is outside the base's editable root, cap it at the end of that root.
+ // If the end is in non-editable content that is inside the base's root, put it
+ // at the last editable position before the end inside the base's root.
+ if (endRoot != baseRoot) {
+ VisiblePosition last = lastEditablePositionBeforePositionInRoot(m_end, baseRoot);
m_end = last.deepEquivalent();
}
- // The selection is based outside editable content. Keep both sides from reaching into editable content.
+ // The selection is based in non-editable content.
} else {
- // The selection ends in editable content, move backward until non-editable content is reached.
- if (endRoot) {
- VisiblePosition previous;
- do {
- endRoot = endRoot->shadowAncestorNode();
- previous = VisiblePosition(Position(endRoot, 0)).previous();
- endRoot = previous.deepEquivalent().node()->rootEditableElement();
- } while (endRoot);
+ // FIXME: Non-editable pieces inside editable content should be atomic, in the same way that editable
+ // pieces in non-editable content are atomic.
+
+ // The selection ends in editable content or non-editable content inside a different editable ancestor,
+ // move backward until non-editable content inside the same lowest editable ancestor is reached.
+ Node* endEditableAncestor = lowestEditableAncestor(m_end.node());
+ if (endRoot || endEditableAncestor != baseEditableAncestor) {
+ Node* node = m_end.node();
+ VisiblePosition previous(Position(node, maxDeepOffset(node)));
+ while (node && !(lowestEditableAncestor(node) == baseEditableAncestor && !node->isContentEditable() && previous.isNotNull())) {
+ node = node->traversePreviousNode();
+ previous = VisiblePosition(Position(node, maxDeepOffset(node)));
+ }
- ASSERT(!previous.isNull());
+ if (previous.isNull()) {
+ ASSERT_NOT_REACHED();
+ m_base = Position();
+ m_extent = Position();
+ validate();
+ return;
+ }
m_end = previous.deepEquivalent();
}
- // The selection starts in editable content, move forward until non-editable content is reached.
- if (startRoot) {
- VisiblePosition next;
- do {
- startRoot = startRoot->shadowAncestorNode();
- next = VisiblePosition(Position(startRoot, maxDeepOffset(startRoot))).next();
- startRoot = next.deepEquivalent().node()->rootEditableElement();
- } while (startRoot);
+
+ // The selection ends in editable content or non-editable content inside a different editable ancestor,
+ // move backward until non-editable content inside the same lowest editable ancestor is reached.
+ Node* startEditableAncestor = lowestEditableAncestor(m_start.node());
+ if (startRoot || startEditableAncestor != baseEditableAncestor) {
+ Node* node = m_start.node();
+ VisiblePosition next(Position(node, 0));
+ while (node && !(lowestEditableAncestor(node) == baseEditableAncestor && !node->isContentEditable() && next.isNotNull())) {
+ node = node->traverseNextNode();
+ next = VisiblePosition(Position(node, 0));
+ }
- ASSERT(!next.isNull());
+ if (next.isNull()) {
+ ASSERT_NOT_REACHED();
+ m_base = Position();
+ m_extent = Position();
+ validate();
+ return;
+ }
m_start = next.deepEquivalent();
}
}
// Correct the extent if necessary.
- if (baseRoot != m_extent.node()->rootEditableElement())
+ if (baseEditableAncestor != lowestEditableAncestor(m_extent.node()))
m_extent = m_baseIsFirst ? m_end : m_start;
}
if (highestEditableRoot(next.deepEquivalent().node()) == highestRoot)
return next;
-
- Position p = next.deepEquivalent();
- Node* node = p.node();
- Node* child = node->childNode(p.offset());
- node = child ? child : node->traverseNextSibling(highestRoot);
-
- while (node && !node->isContentEditable())
- node = node->traverseNextNode(highestRoot);
- if (!node)
- return VisiblePosition();
-
- return VisiblePosition(Position(node, 0));
+ return firstEditablePositionAfterPositionInRoot(next.deepEquivalent(), highestRoot);
}
VisiblePosition VisiblePosition::previous(bool stayInEditableContent) const
if (highestEditableRoot(prev.deepEquivalent().node()) == highestRoot)
return prev;
- Position p = prev.deepEquivalent();
- Node* node = p.node();
- Node* child = node->firstChild() && p.offset() > 1 ? node->childNode(p.offset() - 1) : 0;
- node = child ? child : node->traversePreviousNode(highestRoot);
-
- while (node && !node->isContentEditable())
- node = node->traversePreviousNodePostOrder(highestRoot);
-
- if (!node)
- return VisiblePosition();
-
- return VisiblePosition(Position(node, maxDeepOffset(node)));
+ return lastEditablePositionBeforePositionInRoot(prev.deepEquivalent(), highestRoot);
}
Position VisiblePosition::previousVisiblePosition(const Position& pos)
#include "HTMLNames.h"
#include "RenderObject.h"
#include "RegularExpression.h"
+#include "Range.h"
using namespace std;
!node->isTextNode();
}
+// Compare two positions, taking into account the possibility that one or both
+// could be inside a shadow tree. Only works for non-null values.
+int comparePositions(const Position& a, const Position& b)
+{
+ Node* nodeA = a.node();
+ ASSERT(nodeA);
+ Node* nodeB = b.node();
+ ASSERT(nodeB);
+ int offsetA = a.offset();
+ int offsetB = b.offset();
+
+ Node* shadowAncestorA = nodeA->shadowAncestorNode();
+ if (shadowAncestorA == nodeA)
+ shadowAncestorA = 0;
+ Node* shadowAncestorB = nodeB->shadowAncestorNode();
+ if (shadowAncestorB == nodeB)
+ shadowAncestorB = 0;
+
+ if (shadowAncestorA != shadowAncestorB) {
+ if (shadowAncestorA) {
+ nodeA = shadowAncestorA;
+ offsetA = 0;
+ }
+ if (shadowAncestorB) {
+ nodeB = shadowAncestorB;
+ offsetB = 0;
+ }
+ }
+
+ return Range::compareBoundaryPoints(nodeA, offsetA, nodeB, offsetB);
+}
+
Node* highestEditableRoot(Node* node)
{
if (!node)
return highestRoot;
}
+Node* lowestEditableAncestor(Node* node)
+{
+ if (!node)
+ return 0;
+
+ Node *lowestRoot = 0;
+ while (node) {
+ if (node->isContentEditable())
+ return node->rootEditableElement();
+ if (node->hasTagName(bodyTag))
+ break;
+ node = node->parentNode();
+ }
+
+ return lowestRoot;
+}
+
+VisiblePosition firstEditablePositionAfterPositionInRoot(const Position& position, Node* highestRoot)
+{
+ if (comparePositions(position, Position(highestRoot, 0)) == -1)
+ return VisiblePosition(Position(highestRoot, 0));
+
+ Node* node = position.node();
+ Node* child = node->childNode(position.offset());
+ node = child ? child : node->traverseNextSibling(highestRoot);
+
+ while (node && !node->isContentEditable())
+ node = node->traverseNextNode(highestRoot);
+
+ if (!node)
+ return VisiblePosition();
+
+ return VisiblePosition(Position(node, 0));
+}
+
+VisiblePosition lastEditablePositionBeforePositionInRoot(const Position& position, Node* highestRoot)
+{
+ if (comparePositions(position, Position(highestRoot, maxDeepOffset(highestRoot))) == 1)
+ return VisiblePosition(Position(highestRoot, maxDeepOffset(highestRoot)));
+
+ Node* node = position.node();
+ Node* child = node->firstChild() && position.offset() > 1 ? node->childNode(position.offset() - 1) : 0;
+ node = child ? child : node->traversePreviousNode(highestRoot);
+
+ while (node && !node->isContentEditable())
+ node = node->traversePreviousNodePostOrder(highestRoot);
+
+ if (!node)
+ return VisiblePosition();
+
+ return VisiblePosition(Position(node, maxDeepOffset(node)));
+}
+
// antidote for maxDeepOffset()
Position rangeCompliantEquivalent(const Position& pos)
{
bool editingIgnoresContent(const Node*);
bool canHaveChildrenForEditing(const Node*);
Node* highestEditableRoot(Node*);
+VisiblePosition firstEditablePositionAfterPositionInRoot(const Position&, Node*);
+VisiblePosition lastEditablePositionBeforePositionInRoot(const Position&, Node*);
+int comparePositions(const Position&, const Position&);
+Node* lowestEditableAncestor(Node*);
void rebalanceWhitespaceInTextNode(Node*, unsigned start, unsigned length);
const String& nonBreakingSpaceString();