mirror of
https://github.com/dart-lang/sdk
synced 2024-07-21 02:14:37 +00:00
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:
parent
97c9523e7a
commit
3c1426ea86
17
site/try/bugs/single_line_delete.dart
Normal file
17
site/try/bugs/single_line_delete.dart
Normal 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);
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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}.');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
|
57
tests/try/firefox.applescript
Normal file
57
tests/try/firefox.applescript
Normal 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
|
|
@ -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);
|
||||
|
|
144
tests/try/safari.applescript
Normal file
144
tests/try/safari.applescript
Normal 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"
|
Loading…
Reference in a new issue