Closed
Bug 1258085
Opened 9 years ago
Closed 9 years ago
Pressing backspace after <br> in paragraph mispositions caret at beginning of line
Categories
(Core :: DOM: Editor, defect)
Core
DOM: Editor
Tracking
()
RESOLVED
FIXED
mozilla48
Tracking | Status | |
---|---|---|
firefox48 | --- | fixed |
People
(Reporter: jorgk-bmo, Assigned: ayg)
References
(Blocks 1 open bug)
Details
(Whiteboard: btpp-active)
Attachments
(4 files, 3 obsolete files)
(deleted),
text/html
|
Details | |
(deleted),
patch
|
Details | Diff | Splinter Review | |
(deleted),
text/html
|
Details | |
(deleted),
patch
|
masayuki
:
review+
|
Details | Diff | Splinter Review |
Pressing backspace after paragraph loses caret and also mispositions caret to beginning of preceding paragraph.
See enclosed example.
Note: Works OK in Chrome.
Reporter | ||
Comment 1•9 years ago
|
||
I'm moving this to Core::Layout since it doesn't see to have anything to do with the editor. The editor is fine, but the caret is misplaced. Simpler test case and debug patch coming.
Component: Editor → Layout
Summary: Pressing backspace after paragraph loses caret and also mispositions caret to beginning of preceding paragraph → Pressing backspace after <br> in paragraph mispositions caret at beginning of line
Reporter | ||
Comment 2•9 years ago
|
||
Click, backspace and bad caret position.
Attachment #8732486 -
Attachment is obsolete: true
Reporter | ||
Comment 3•9 years ago
|
||
As far as I can see, the editor has worked OK, but the Collapse() shows the caret in the wrong location.
Note that it all depends on the \n after the <br>:
Not working:
====
<p style="color:red;">Position behind the x on the next line, press backspace<br>
x</p>
====
Working:
<p style="color:red;">Position behind the x on the next line, press backspace<br>x</p>
====
Reporter | ||
Comment 4•9 years ago
|
||
Gentlemen, this bug doesn't seem to be in the unowned editor, neither the unowned selection. It simply seems to be a layout problem. So perhaps I can get someone interested.
I'm happy to help, but I have no idea were I would start looking.
The Collapse() call from the editor is clearly: Position at offset 1 in a text node containing a newline. That doesn't happen. Instead it draws the caret somewhere else sort of at half height. You can already see that it's confused.
NI from David since he's the owner of layout and also NI from Mats who's been working right in Collapse() very recently.
Flags: needinfo?(mats)
Flags: needinfo?(dbaron)
Reporter | ||
Comment 5•9 years ago
|
||
Or maybe Ehsan is also appropriate since he's recently reviewed work in Collapse().
Sorry about the NI Spam, hard to tell who looks after this area.
Flags: needinfo?(ehsan)
Reporter | ||
Updated•9 years ago
|
Comment 6•9 years ago
|
||
The caret code lives in layout/base/nsCaret.cpp, with some "caret hints" managed by
by the the selection code in layout/generic/nsFrameSelection.h and layout/generic/nsSelection.cpp.
It looks like you've found the relevant Editor code already.
I'm not convinced this isn't an Editor bug (yet). If it's not Editor then it's likely Selection
(and the bug is in one of the files mentioned above).
What was the output from your debug code?
Component: Layout → Editor
Flags: needinfo?(mozilla)
Flags: needinfo?(mats)
Flags: needinfo?(dbaron)
Reporter | ||
Comment 7•9 years ago
|
||
As I said in comment #4. It dumps out a text node that contains a \n and the offset is 1. Do you need to see it or do you believe me?
Flags: needinfo?(mozilla)
Reporter | ||
Comment 8•9 years ago
|
||
OK, seeing is believing, here you go:
Text@13D67830 flags=[07000208] ranges:1 primaryframe=13D6A9F8 refcount=28<\u000a>
===== Offset 1
Right, Selection::Collapse() is in layout/generic/nsSelection.cpp, but that's part of Layout, no? Or is that the unwanted unowned orphan?
Comment 9•9 years ago
|
||
Oh, I missed that in comment 4, sorry. Can you check if the Selection has a range
and what its nodes/offsets are after the text node is removed and where it's removed?
> layout/generic/nsSelection.cpp, but that's part of Layout, no?
No, that file belongs to Core/Selection. It really should be moved under
dom/base/ where nsRange lives (it's an historical artifact that it was
created under layout/)
Reporter | ||
Comment 10•9 years ago
|
||
I'm not sure I understand the question.
When you click after the x, you're most likely in the text node with a collapsed selection, or maybe behind the paragraph, but you get moved into the text so you have something to delete.
The text deleting happens here:
https://dxr.mozilla.org/mozilla-central/source/editor/libeditor/DeleteTextTxn.cpp#59
Right after, the program sets a new collapsed selection and that ain't working.
What else should I check?
(Let's take Ehsan out of the loop.)
Flags: needinfo?(ehsan)
Comment 11•9 years ago
|
||
OK, the "x" was already removed a few lines earlier. So yeah, the remaining text node
(after the <br>) is now "\n" and collapsing at offset 1 seems correct to me.
If you dump the frame tree before and after deleting the "x" (give -layoutdebug on
the command line when you start Firefox) you can see that the text frame goes from:
Text(2)"\nx"@7f6b4ca25c70 {0,1140,540,1140} ...
to
Text(2)"\n"@7f6b4ca25c70 {0,1140,0,0} ...
(those numbers are x,y,width,height)
so the caret is positioned correctly it's just that the frame has zero height
so the baseline is wrong and that's why it's got the wrong y-offset.
I think Ehsan has been dabbling with this problem in the past...
Flags: needinfo?(ehsan)
Reporter | ||
Comment 12•9 years ago
|
||
(if you put this into Core::Editor, no one will look at it ;-()
Component: Editor → Layout
Comment 13•9 years ago
|
||
Bug 389321 is the one I was thinking about that have some background info on caret
positioning that might help. So the patches in that bug, and comment 6 / comment 11
in this bug should be enough to debug this and fix it I think.
Reporter | ||
Comment 14•9 years ago
|
||
Well, Ehsan, Robert and David worked on bug 389321.
So who should be looking into this? I'm already looking into editor problems (currently bug 1257363), so it would be great if someone else could fix the layout problems. Layout is not unowned, unlike editor and selection.
Flags: needinfo?(dbaron)
Comment 15•9 years ago
|
||
I have seen issues related to text frames without anything other than collapsible whitespace having the wrong baseline in the past, but I couldn't find the bug I was thinking about with a quick search. Bug 904846 may be related. I don't remember much more concrete details, but Mats' analysis in comment 11 seems to be on the right path. Someone needs to debug this further.
Flags: needinfo?(ehsan)
Comment 17•9 years ago
|
||
FWIW we've had the underlying bug for years now, this is not a new issue.
Comment 18•9 years ago
|
||
This testcase shows the state of layout following the character deletion. I've added a yellow background to the p for clarity.
This testcase appears to be displayed interoperably across browsers; the <br> at the end of the p element does not add anything to its height.
Comment 19•9 years ago
|
||
I don't believe this is a layout bug as such. The layout of the document is what's required for Web compatibility.
Unfortunately, layout on the Web wasn't really designed with editing in mind. There are many cases where you can place a caret at a position that currently doesn't take up space, but if you start typing, then suddenly things will start taking up space. The solutions to these situations generally involve making the editor do somewhat non-obvious things that don't match the user's editing inputs. (You might suggest doing editing-dependent layout instead, such as having layout produce different results when the caret is present at a location. However, I don't know of a way to do this without yielding user interface that jumps around in horrible and incomprehensible ways.)
In this case, the editor allows the creation of one of those situations through character deletion. Chrome's editor, on the other hand, inserts a *second* <br> in this situation (when the character is deleted) to avoid entering such a layout situation. (You can see that if you have Chrome's inspector open while following the steps to reproduce.) So if you want things to work like Chrome, that's an Editor bug. That also seems like the most reasonable path forward.
Component: Layout → Editor
Flags: needinfo?(dbaron)
Comment 20•9 years ago
|
||
One final note: making any change here is somewhat risky in terms of Web-compat, since there are a number of JS editing libraries that work around various browser bugs, and which may be assuming that Gecko behaves differently from Chromium in this case. It is worth investigating whether that's the case if we're going to try changing the editing behavior, and if it is, trying to get those libraries updated as needed.
Reporter | ||
Updated•9 years ago
|
Flags: needinfo?(bugs)
Assignee | ||
Comment 21•9 years ago
|
||
The problem is very simple: when you delete the "x", the <br> becomes collapsed. The deletion code needs to check for this and add an additional <br> to prevent the original one from collapsing. The editing spec does this in a lot of cases already, and so do we, e.g.:
<div contenteditable>x<br>yz</div>
If you backspace the "yz", we insert a <br>. Actually, if you type text after the "z", we also insert a <br>. But if you backspace the "z", we don't insert a <br>.
The lazy fix would be to insert a <br> here even if you only backspace the "z". This would leave useless <br>s lying around, but we already do that anyway. The cleaner approach would be to check if the preceding <br> is collapsed and only insert an extra <br> if we collapsed a previously non-collapsed <br>, and conversely to remove a <br> that previously was needed to stop another <br> from collapsing and now is not needed. This is what Chrome does, and what the editing spec tries to do, but it's much more complicated and error-prone, so probably best to be lazy.
Assignee | ||
Updated•9 years ago
|
Assignee: nobody → ayg
Status: NEW → ASSIGNED
Assignee | ||
Comment 22•9 years ago
|
||
Try run from previous patch version with one mochitest failure: https://treeherder.mozilla.org/#/jobs?repo=try&revision=989715b32764
New mochitest-only try run with updated patch: https://treeherder.mozilla.org/#/jobs?repo=try&revision=bf5f7f92d8b6
This is based on the whole gigantic series of bug 1193762 and its dependencies, which hopefully should be ready for checkin soon!
Attachment #8742389 -
Flags: review?(masayuki)
Assignee | ||
Comment 23•9 years ago
|
||
Attachment #8742389 -
Attachment is obsolete: true
Attachment #8742389 -
Flags: review?(masayuki)
Attachment #8742444 -
Flags: review?(masayuki)
Updated•9 years ago
|
Whiteboard: btpp-active
Comment 24•9 years ago
|
||
Comment on attachment 8742444 [details] [diff] [review]
Simple patch v2, with test fix
>+ {
>+ nsAutoTrackDOMPoint startTracker(mHTMLEditor->mRangeUpdater,
>+ address_of(startNode), &startOffset);
>+ nsAutoTrackDOMPoint endTracker(mHTMLEditor->mRangeUpdater,
>+ address_of(endNode), &endOffset);
>+
>+ HandleEmptyText(*startNode);
>+ HandleEmptyText(*endNode);
>+ }
Well, I want some comments at start of this block for explaining why we need to do this.
>+/**
>+ * If aNode is a text node that contains only collapsed whitespace, delete it.
>+ * It doesn't serve any useful purpose, and we don't want it to confuse code
>+ * that doesn't correctly skip over it.
>+ *
>+ * If deleting the node fails (like if it's not editable), the caller should
>+ * proceed as usual, so don't return any errors.
>+ */
>+void
>+nsHTMLEditRules::HandleEmptyText(nsINode& aNode)
>+{
>+ if (!aNode.GetAsText()) {
>+ return;
>+ }
>+ bool empty;
>+ nsresult res = mHTMLEditor->IsVisTextNode(aNode.AsContent(), &empty, false);
>+ NS_ENSURE_SUCCESS_VOID(res);
>+ if (empty) {
>+ mHTMLEditor->DeleteNode(&aNode);
>+ }
>+}
How about |DeleteNodeIfEmptyTextNode()|?
>diff --git a/editor/libeditor/tests/test_bug1258085.html b/editor/libeditor/tests/test_bug1258085.html
>new file mode 100644
>index 0000000..4884d95
>--- /dev/null
>+++ b/editor/libeditor/tests/test_bug1258085.html
>@@ -0,0 +1,54 @@
>+<!DOCTYPE html>
>+<title>Test for Bug 1186799</title>
>+<script src="/tests/SimpleTest/SimpleTest.js"></script>
>+<script src="/tests/SimpleTest/EventUtils.js"></script>
>+<link rel="stylesheet" href="/tests/SimpleTest/test.css">
>+<div contenteditable></div>
>+<script>
>+var div = document.querySelector("div");
>+
>+function reset() {
>+ div.innerHTML = "x<br> y";
>+ div.focus();
>+ synthesizeKey("VK_DOWN", {});
>+}
>+
>+function checks(msg) {
>+ is(div.innerHTML, "x<br><br>",
>+ msg + ": Should add a second <br> to prevent collapse of first");
>+ is(div.childNodes.length, 3, msg + ": No empty text nodes allowed");
>+ ok(getSelection().isCollapsed, msg + ": Selection must be collapsed");
>+ is(getSelection().focusNode, div, msg + ": Focus must be in div");
>+ is(getSelection().focusOffset, 2,
>+ msg + ": Focus must be between the two <br>s");
>+}
>+
>+SimpleTest.waitForExplicitFinish();
>+SimpleTest.waitForFocus(function() {
>+ // Put selection after the "y" and backspace
>+ reset();
>+ synthesizeKey("VK_RIGHT", {});
>+ synthesizeKey("VK_BACK_SPACE", {});
>+ checks("Collapsed backspace");
>+
>+ // Now do the same with delete
>+ reset();
>+ synthesizeKey("VK_DELETE", {});
>+ checks("Collapsed delete");
>+
>+ // Forward selection
>+ reset();
>+ synthesizeKey("VK_RIGHT", {shiftKey: true});
>+ synthesizeKey("VK_BACK_SPACE", {});
>+ checks("Forward-selected backspace");
>+
>+ // Backward selection
>+ reset();
>+ synthesizeKey("VK_RIGHT", {});
>+ synthesizeKey("VK_LEFT", {shiftKey: true});
>+ synthesizeKey("VK_BACK_SPACE", {});
>+ checks("Backward-selected backspace");
>+
>+ SimpleTest.finish();
>+});
>+</script>
I wonder, how does this work with "white-space: pre-wrap;"? If whitespace won't be collapsed, the text node shouldn't be removed in this case, isn't it? If so, I hope you add the testcases for that.
Attachment #8742444 -
Flags: review?(masayuki)
Assignee | ||
Comment 25•9 years ago
|
||
I didn't think it was necessary to do a new try run, since there were no behavior changes.
(In reply to Masayuki Nakano [:masayuki] (Mozilla Japan) from comment #24)
> >+ {
> >+ nsAutoTrackDOMPoint startTracker(mHTMLEditor->mRangeUpdater,
> >+ address_of(startNode), &startOffset);
> >+ nsAutoTrackDOMPoint endTracker(mHTMLEditor->mRangeUpdater,
> >+ address_of(endNode), &endOffset);
> >+
> >+ HandleEmptyText(*startNode);
> >+ HandleEmptyText(*endNode);
> >+ }
>
> Well, I want some comments at start of this block for explaining why we need
> to do this.
I added a comment, but I'm not sure why it's not obvious, so let me know if you want a different comment. It's needed if the start/end nodes of the deletion lie in text nodes, and after the deletion only collapsed whitespace is left.
> How about |DeleteNodeIfEmptyTextNode()|?
Good idea. I went with "Collapsed" instead of "Empty" because "Empty" sounds like zero-length to me.
> I wonder, how does this work with "white-space: pre-wrap;"? If whitespace
> won't be collapsed, the text node shouldn't be removed in this case, isn't
> it? If so, I hope you add the testcases for that.
Good catch! Fortunately it does work.
Attachment #8742444 -
Attachment is obsolete: true
Attachment #8742751 -
Flags: review?(masayuki)
Updated•9 years ago
|
Attachment #8742751 -
Flags: review?(masayuki) → review+
Comment 26•9 years ago
|
||
(In reply to :Aryeh Gregor (working until May 8) from comment #25)
> Created attachment 8742751 [details] [diff] [review]
> Patch v3
>
> I didn't think it was necessary to do a new try run, since there were no
> behavior changes.
>
> (In reply to Masayuki Nakano [:masayuki] (Mozilla Japan) from comment #24)
> > >+ {
> > >+ nsAutoTrackDOMPoint startTracker(mHTMLEditor->mRangeUpdater,
> > >+ address_of(startNode), &startOffset);
> > >+ nsAutoTrackDOMPoint endTracker(mHTMLEditor->mRangeUpdater,
> > >+ address_of(endNode), &endOffset);
> > >+
> > >+ HandleEmptyText(*startNode);
> > >+ HandleEmptyText(*endNode);
> > >+ }
> >
> > Well, I want some comments at start of this block for explaining why we need
> > to do this.
>
> I added a comment, but I'm not sure why it's not obvious, so let me know if
> you want a different comment. It's needed if the start/end nodes of the
> deletion lie in text nodes, and after the deletion only collapsed whitespace
> is left.
It was unclear the code looks like that might removing the selection start and/or end node but how selection range is managed and the new range.
Comment 27•9 years ago
|
||
Comment 28•9 years ago
|
||
bugherder |
Status: ASSIGNED → RESOLVED
Closed: 9 years ago
Resolution: --- → FIXED
Target Milestone: --- → mozilla48
Comment 29•9 years ago
|
||
This bug was landed in THUNDERBIRD_45_VERBRANCH
https://hg.mozilla.org/releases/mozilla-esr45/rev/70c9bb7161ca
Reporter | ||
Comment 30•9 years ago
|
||
I'm not sure why/how I became the author of this patch. This is Aryeh Gregor's work. I just adapted the patch for ESR 45.
Comment 31•8 years ago
|
||
Pushed to a release branch on mozilla-beta for Thunderbird 47.0b1
You need to log in
before you can comment on or make changes to this bug.
Description
•