Fix issues that broke editing on browsers without Shadow DOM support.

R=johnniwinther@google.com

Review URL: https://codereview.chromium.org//345553008

git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart@38014 260f80e4-7a28-3924-810f-c04153c831b5
This commit is contained in:
ahe@google.com 2014-07-04 13:53:20 +00:00
parent 97c9523e7a
commit 3c1426ea86
11 changed files with 353 additions and 69 deletions

View file

@ -0,0 +1,17 @@
// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
var greeting = "Hello, World!";
void main() {
// Put cursor at end of previous line. Hit backspace.
// 1. Ensure this triggers a single-line change.
// 2. Ensure the cursor position is correct.
// Then restore the file and place the cursor at the end of the file. Delete
// each character in the file by holding down backspace. Verify that there
// are no exceptions and that the entire buffer is deleted.
// Then restore the file and place the cursor before the semicolon on the
// first line. Hit delete and verify that a character is deleted.
print(greeting);
}

View file

@ -9,6 +9,9 @@ import 'dart:html';
import 'shadow_root.dart' show
setShadowRoot;
import 'editor.dart' show
diagnostic;
class Decoration {
final String color;
final bool bold;
@ -65,11 +68,7 @@ class DiagnosticDecoration extends Decoration {
if (kind == 'error') {
tip = error(message);
}
return element..append(
new AnchorElement()
..classes.add('diagnostic')
..nodes.addAll(nodes)
..append(tip));
return element..append(diagnostic(nodes, tip));
}
}

View file

@ -257,12 +257,15 @@ Decoration getDecoration(Token token) {
return currentTheme.foreground;
}
diagnostic(text, tip) {
if (text is String) {
text = new Text(text);
diagnostic(content, tip) {
if (content is String) {
content = new Text(content);
}
if (content is! List) {
content = [content];
}
return new AnchorElement()
..classes.add('diagnostic')
..append(text)
..append(tip);
..append(tip) // Should be first for better Firefox editing.
..nodes.addAll(content);
}

View file

@ -8,16 +8,21 @@ import 'dart:math' show
max;
import 'dart:html' show
CharacterData,
Element,
Node,
NodeFilter,
ShadowRoot,
Text,
TreeWalker;
import 'selection.dart' show
TrySelection;
import 'shadow_root.dart' show
WALKER_NEXT,
WALKER_SKIP_NODE,
walkNodes;
/// Returns true if [node] is a block element, that is, not inline.
bool isBlockElement(Node node) {
if (node is! Element) return false;
@ -30,24 +35,6 @@ bool isBlockElement(Node node) {
return element.getComputedStyle().display != 'inline';
}
/// Position [walker] at the last predecessor (that is, child of child of
/// child...) of [node]. The next call to walker.nextNode will return the first
/// node after [node].
void skip(Node node, TreeWalker walker) {
if (walker.nextSibling() != null) {
walker.previousNode();
return;
}
for (Node current = walker.nextNode();
current != null;
current = walker.nextNode()) {
if (!node.contains(current)) {
walker.previousNode();
return;
}
}
}
/// Writes the text of [root] to [buffer]. Keeps track of [selection] and
/// returns the new anchorOffset from beginning of [buffer] or -1 if the
/// selection isn't in [root].
@ -56,34 +43,30 @@ int htmlToText(Node root,
TrySelection selection,
{bool treatRootAsInline: false}) {
int selectionOffset = -1;
TreeWalker walker = new TreeWalker(root, NodeFilter.SHOW_ALL);
for (Node node = root; node != null; node = walker.nextNode()) {
walkNodes(root, (Node node) {
if (selection.anchorNode == node) {
selectionOffset = selection.anchorOffset + buffer.length;
}
switch (node.nodeType) {
case Node.CDATA_SECTION_NODE:
case Node.TEXT_NODE:
if (selection.anchorNode == node) {
selectionOffset = selection.anchorOffset + buffer.length;
}
Text text = node;
CharacterData text = node;
buffer.write(text.data.replaceAll('\xA0', ' '));
break;
default:
if (!ShadowRoot.supported &&
node is Element &&
node.getAttribute('try-dart-shadow-root') != null) {
skip(node, walker);
} else if (node.nodeName == 'BR') {
if (node.nodeName == 'BR') {
buffer.write('\n');
} else if (node != root && isBlockElement(node)) {
selectionOffset =
max(selectionOffset, htmlToText(node, buffer, selection));
skip(node, walker);
return WALKER_SKIP_NODE;
}
break;
}
}
return WALKER_NEXT;
});
if (!treatRootAsInline && isBlockElement(root)) {
buffer.write('\n');

View file

@ -236,6 +236,7 @@ class InteractionContext extends InteractionManager {
void onKeyUp(KeyboardEvent event) => state.onKeyUp(event);
void onMutation(List<MutationRecord> mutations, MutationObserver observer) {
workAroundFirefoxBug();
try {
try {
return state.onMutation(mutations, observer);
@ -308,7 +309,6 @@ abstract class InteractionState implements InteractionManager {
void set state(InteractionState newState);
void onStateChanged(InteractionState previous) {
print('State change ${previous.runtimeType} -> ${runtimeType}.');
}
void transitionToInitialState() {
@ -336,10 +336,8 @@ class InitialState extends InteractionState {
void onKeyUp(KeyboardEvent event) {
if (computeHasModifier(event)) {
print('onKeyUp (modified)');
onModifiedKeyUp(event);
} else {
print('onKeyUp (unmodified)');
onUnmodifiedKeyUp(event);
}
}
@ -413,8 +411,6 @@ class InitialState extends InteractionState {
}
void onMutation(List<MutationRecord> mutations, MutationObserver observer) {
print('onMutation');
removeCodeCompletion();
Selection selection = window.getSelection();
@ -456,7 +452,11 @@ class InitialState extends InteractionState {
node.parent.insertAllBefore(nodes, node);
node.remove();
trySelection.adjust(selection);
if (mainEditorPane.contains(trySelection.anchorNode)) {
// Sometimes the anchor node is removed by the above call. This has
// only been observed in Firefox, and is hard to reproduce.
trySelection.adjust(selection);
}
// TODO(ahe): We know almost exactly what has changed. It could be
// more efficient to only communicate what changed.
@ -1206,7 +1206,10 @@ void normalizeMutationRecord(MutationRecord record,
}
normalizedNodes.add(line);
}
if (record.type == "characterData") {
if (record.type == "characterData" && record.target.parent != null) {
// At least Firefox sends a "characterData" record whose target is the
// deleted text node. It also sends a record where "removedNodes" isn't
// empty whose target is the parent (which we are interested in).
normalizedNodes.add(findLine(record.target));
}
}
@ -1259,3 +1262,23 @@ bool isCompilerStageMarker(String message) {
message == "Compiling..." ||
message.startsWith('Compiled ');
}
void workAroundFirefoxBug() {
Selection selection = window.getSelection();
if (!isCollapsed(selection)) return;
Node node = selection.anchorNode;
int offset = selection.anchorOffset;
if (selection.anchorNode is Element && selection.anchorOffset != 0) {
// In some cases, Firefox reports the wrong anchorOffset (always seems to
// be 6) when anchorNode is an Element. Moving the cursor back and forth
// adjusts the anchorOffset.
// Safari can also reach this code, but the offset isn't wrong, just
// inconsistent. After moving the cursor back and forth, Safari will make
// the offset relative to a text node.
selection
..modify('move', 'backward', 'character')
..modify('move', 'forward', 'character');
print('Selection adjusted $node@$offset -> '
'${selection.anchorNode}@${selection.anchorOffset}.');
}
}

View file

@ -6,12 +6,18 @@ library trydart.selection;
import 'dart:html' show
CharacterData,
Element,
Node,
NodeFilter,
Selection,
Text,
TreeWalker;
import 'shadow_root.dart' show
WALKER_NEXT,
WALKER_RETURN,
walkNodes;
import 'decoration.dart';
class TrySelection {
@ -70,18 +76,23 @@ class TrySelection {
if (anchorOffset == -1) return -1;
int offset = 0;
TreeWalker walker = new TreeWalker(root, NodeFilter.SHOW_TEXT);
for (Node node = walker.nextNode();
node != null;
node = walker.nextNode()) {
CharacterData text = node;
if (anchorNode == text) {
return anchorOffset + offset;
bool found = false;
walkNodes(root, (Node node) {
if (anchorNode == node) {
offset += anchorOffset;
found = true;
return WALKER_RETURN;
}
offset += text.data.length;
}
return -1;
switch (node.nodeType) {
case Node.CDATA_SECTION_NODE:
case Node.TEXT_NODE:
CharacterData text = node;
offset += text.data.length;
break;
}
return WALKER_NEXT;
});
return found ? offset : -1;
}
}

View file

@ -12,6 +12,10 @@ import 'selection.dart' show
import 'html_to_text.dart' show
htmlToText;
const int WALKER_NEXT = 0;
const int WALKER_RETURN = 1;
const int WALKER_SKIP_NODE = 2;
void setShadowRoot(Element node, text) {
if (text is String) {
text = new Text(text);
@ -53,3 +57,47 @@ String getText(Element node) {
node, buffer, new TrySelection.empty(node), treatRootAsInline: true);
return '$buffer';
}
/// Position [walker] at the last predecessor (that is, child of child of
/// child...) of [node]. The next call to walker.nextNode will return the first
/// node after [node].
void skip(Node node, TreeWalker walker) {
if (walker.nextSibling() != null) {
walker.previousNode();
return;
}
for (Node current = walker.nextNode();
current != null;
current = walker.nextNode()) {
if (!node.contains(current)) {
walker.previousNode();
return;
}
}
}
/// Call [f] on each node in [root] in same order as [TreeWalker]. Skip any
/// nodes used to implement shadow root polyfill.
void walkNodes(Node root, int f(Node node)) {
TreeWalker walker = new TreeWalker(root, NodeFilter.SHOW_ALL);
for (Node node = root; node != null; node = walker.nextNode()) {
if (!ShadowRoot.supported &&
node is Element &&
node.getAttribute('try-dart-shadow-root') != null) {
skip(node, walker);
}
int action = f(node);
switch (action) {
case WALKER_RETURN:
return;
case WALKER_SKIP_NODE:
skip(node, walker);
break;
case WALKER_NEXT:
break;
default:
throw 'Unexpected action returned from [f]: $action';
}
}
}

View file

@ -49,14 +49,14 @@ void main() {
new TestCase('Clear and presetup the test', () {
clearEditorPaneWithoutNotifications();
mainEditorPane.text = 'var greeting = "Hello, World!\n";';
}, () {
checkLineCount(2);
}, () {
checkLineCount(2);
}),
new TestCase('Test removing a split line', () {
mainEditorPane.nodes.first.nodes.last.remove();
}, () {
checkLineCount(1);
}, () {
checkLineCount(1);
}),
]);
}

View file

@ -0,0 +1,57 @@
-- Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
-- for details. All rights reserved. Use of this source code is governed by a
-- BSD-style license that can be found in the LICENSE file.
tell application "Firefox 29" to activate
delay 3.0
tell application "System Events"
keystroke "n" using command down
delay 1.0
keystroke "l" using command down
keystroke "http://localhost:8080/"
-- Simulate Enter key.
key code 36
delay 10.0
keystroke "l" using command down
-- Simulate Tab key.
key code 48
key code 48
key code 48
key code 48
-- Simulate End key.
key code 119
-- Simulate Home key.
key code 115
-- Simulate Tab key.
key code 48
-- Simulate Cmd-Up.
key code 126 using command down
-- Simulate Down.
key code 125
key code 125
key code 125
key code 125
key code 125
-- Simulate Cmd-Right.
key code 124 using command down
-- Simulate Delete
key code 51
-- Simulate Cmd-Down.
-- key code 125 using command down
end tell

View file

@ -46,9 +46,8 @@ Future runTests() {
String key = keys.current;
print('Checking $key');
queryDiagnosticNodes().forEach((Node node) {
node.parent.insertBefore(
new Text('<DIAGNOSTIC>'), node.parent.firstChild);
node.replaceWith(new Text('</DIAGNOSTIC>'));
node.parent.append(new Text('</DIAGNOSTIC>'));
node.replaceWith(new Text('<DIAGNOSTIC>'));
observer.takeRecords(); // Discard mutations.
});
Expect.stringEquals(tests[key], mainEditorPane.text);

View file

@ -0,0 +1,144 @@
-- Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
-- for details. All rights reserved. Use of this source code is governed by a
-- BSD-style license that can be found in the LICENSE file.
tell application "Safari" to activate
delay 3.0
tell application "System Events"
keystroke "n" using command down
delay 1.0
keystroke "l" using command down
keystroke "http://localhost:8080/"
-- Simulate Enter key.
key code 36
delay 5.0
keystroke "l" using command down
-- Simulate Tab key.
key code 48
key code 48
delay 0.2
-- Simulate Down.
key code 125
delay 0.2
-- Simulate Down.
key code 125
delay 0.2
-- Simulate Enter key.
key code 36
delay 0.2
-- Simulate Tab key.
key code 48
-- Simulate Cmd-Up.
key code 126 using command down
-- Simulate Down.
key code 125
key code 125
key code 125
key code 125
key code 125
-- Simulate Cmd-Right.
key code 124 using command down
-- Simulate Delete
key code 51
delay 0.1
keystroke "a" using command down
delay 0.2
keystroke "c" using command down
delay 0.2
set clipboardData to (the clipboard as text)
if ("main() {" is in (clipboardData as string)) then
error "main() { in clipboardData"
end if
if ("main() " is not in (clipboardData as string)) then
error "main() is not in clipboardData"
end if
keystroke "l" using command down
delay 0.2
keystroke "http://localhost:8080/"
-- Simulate Enter key.
key code 36
delay 5.0
keystroke "l" using command down
-- Simulate Tab key.
key code 48
key code 48
delay 0.2
-- Simulate Down.
key code 125
delay 0.2
-- Simulate Down.
key code 125
delay 0.2
-- Simulate Enter key.
key code 36
delay 0.2
-- Simulate Tab key.
key code 48
-- Simulate Cmd-Down.
key code 125 using command down
repeat 203 times
-- Simulate Delete
key code 51
end repeat
delay 5.0
repeat 64 times
-- Simulate Delete
key code 51
end repeat
delay 0.1
keystroke "a" using command down
delay 0.5
keystroke "c" using command down
delay 0.5
set clipboardData to (the clipboard as text)
if ("/" is not (clipboardData as string)) then
error "/ is not clipboardData"
end if
end tell
tell application "Safari" to quit
display notification "Test passed" with title "Safari test" sound name "Glass"