Commit 952c4101 authored by chongz's avatar chongz Committed by Commit bot

[InputEvent] Support |deleteByCut|&|insertFromPaste| with |dataTransfer| field

* Event order: (see July F2F[1] and GitHub discussion[2])
  1. Clipboard API cut
  2. (Clipboard update)
  3. 'beforeinput' InputType=|deleteByCut|
  4. (DOM update)
  5. 'input' InputType=|deleteByCut|

* Canceling 'beforeinput' will only prevent DOM update.

* |dataTransfer| field: (see Editing spec discussion[3])
  1. NULL for |deleteByCut| (JS could always get from selection)
  2. Readonly |dataTransfer| for |insertFromPaste|

[1] Editing TF July F2F:
https://docs.google.com/document/d/1XxIEF0So-kMF5mcJ03Yj0zsYMFRHEgXw1fV1K5FOwuQ/edit#heading=h.l9vlzb1oc68r

[2] GitHub discussion for event order:
https://github.com/w3c/editing/issues/144#issuecomment-240892363

[3] Editing spec discussion:
https://github.com/w3c/editing/issues/144#issuecomment-240259209

Intent to Implement:
https://groups.google.com/a/chromium.org/forum/#!searchin/blink-dev/InputEvent/blink-dev/RrnitB0OElc/rirueVekCwAJ

BUG=639139

Review-Url: https://codereview.chromium.org/2258663003
Cr-Commit-Position: refs/heads/master@{#414659}
parent 595223a9
<!DOCTYPE html>
<html>
<head>
<title>InputEvent: beforeinput shouldn't crash in removed iframe</title>
<script src="../../../resources/testharness.js"></script>
<script src="../../../resources/testharnessreport.js"></script>
</head>
<body>
<script>
test(function() {
assert_not_equals(window.eventSender, undefined, 'This test requires eventSender.');
assert_not_equals(window.testRunner, undefined, 'This test requires testRunner.');
function testBeforeInputCrash(expectedBeforeInputCount, beforeInputTrigger, comments) {
const iframe = document.createElement('iframe');
document.body.appendChild(iframe);
const childDocument = iframe.contentDocument;
childDocument.body.innerHTML = '<p id="editable" contenteditable>Foo</p>';
var actualBeforeInputCount = 0;
const editable = childDocument.getElementById('editable');
editable.addEventListener('beforeinput', event => {
actualBeforeInputCount++;
if (actualBeforeInputCount == expectedBeforeInputCount && iframe.parentNode)
iframe.remove();
});
editable.focus();
beforeInputTrigger(childDocument, editable);
if (iframe.parentNode)
iframe.remove();
assert_equals(actualBeforeInputCount, expectedBeforeInputCount, comments);
}
// Text command.
testBeforeInputCrash(1, () => eventSender.keyDown('a'), 'Testing insertText "a"');
testBeforeInputCrash(1, () => eventSender.keyDown('Enter', ['shiftKey']), 'Testing insertLineBreak');
testBeforeInputCrash(1, () => eventSender.keyDown('Delete'), 'Testing deleteCharacterForward');
// Styling command.
testBeforeInputCrash(1, (childDocument, editable) => {
var selection = childDocument.getSelection();
selection.collapse(editable, 0);
selection.extend(editable, 1);
testRunner.execCommand('bold');
}, 'Testing bold');
// Cut & Paste.
testBeforeInputCrash(1, (childDocument, editable) => {
var selection = childDocument.getSelection();
selection.collapse(editable, 0);
selection.extend(editable, 1);
eventSender.keyDown('Cut');
}, 'Testing deleteByCut');
testBeforeInputCrash(1, () => eventSender.keyDown('Paste'), 'Testing insertFromPaste');
// Undo & Redo.
testBeforeInputCrash(2, () => {
eventSender.keyDown('a');
testRunner.execCommand('undo');
}, 'Testing undo');
testBeforeInputCrash(3, () => {
eventSender.keyDown('a');
testRunner.execCommand('undo');
testRunner.execCommand('redo');
}, 'Testing redo');
}, 'Testing beforeinput in removed iframe');
</script>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<title>InputEvent: beforeinput for Cut and Paste</title>
<script src="../../../resources/testharness.js"></script>
<script src="../../../resources/testharnessreport.js"></script>
</head>
<body>
<p id="editable" contenteditable></p>
<script>
test(function() {
assert_not_equals(window.eventSender, undefined, 'This test requires eventSender.');
var editable = document.getElementById('editable');
var eventOrderRecorder = [];
editable.addEventListener('beforeinput', () => eventOrderRecorder.push('beforeinput'));
editable.addEventListener('input', () => eventOrderRecorder.push('input'));
['cut', 'paste'].forEach(eventType => editable.addEventListener(
eventType, () => eventOrderRecorder.push(eventType)));
function testClipboardEventOrder(command, expectedOrder) {
eventOrderRecorder = [];
eventSender.keyDown(command);
assert_array_equals(eventOrderRecorder, expectedOrder, `Testing ${command} event orders`);
}
// Test Cut and Paste.
editable.innerHTML = 'abc';
var selection = window.getSelection();
selection.collapse(editable, 0);
selection.extend(editable, 1);
testClipboardEventOrder('Cut', ['cut', 'beforeinput', 'input']);
testClipboardEventOrder('Paste', ['paste', 'beforeinput', 'input']);
}, 'Testing Cut and Paste ordering');
test(function() {
assert_not_equals(window.eventSender, undefined, 'This test requires eventSender.');
var editable = document.getElementById('editable');
var lastPlainTextData = "";
var lastHTMLData = "";
editable.addEventListener('beforeinput', event => {
lastPlainTextData = event.dataTransfer ? event.dataTransfer.getData('text/plain') : null;
lastHTMLData = event.dataTransfer ? event.dataTransfer.getData('text/html') : null;
});
function testClipboardDataTransfer(command, expectedPlainText, expectedHTML) {
lastPlainTextData = "";
lastHTMLData = "";
eventSender.keyDown(command);
assert_equals(lastPlainTextData, expectedPlainText, `Testing ${command} getData('text/plain')`);
if (expectedHTML && expectedHTML.test) {
assert_regexp_match(lastHTMLData, expectedHTML, `Testing ${command} getData('text/html')`);
} else {
assert_equals(lastHTMLData, expectedHTML, `Testing ${command} getData('text/html')`);
}
}
// Test Cut and Paste.
editable.innerHTML = '<b>abc</b>';
var selection = window.getSelection();
selection.collapse(editable, 0);
selection.extend(editable, 1);
// Cut has null |dataTransfer|.
testClipboardDataTransfer('Cut', null, null);
// Paste has |dataTransfer| with additional inline styles.
testClipboardDataTransfer('Paste', 'abc', /^<b.*>abc<\/b>$/);
}, 'Testing Cut and Paste dataTransfer');
test(function() {
assert_not_equals(window.eventSender, undefined, 'This test requires eventSender.');
var editable = document.getElementById('editable');
editable.addEventListener('beforeinput', event => {
if (event.inputType == 'deleteByCut') {
// Prevents text deletion but not Clipboard update.
event.preventDefault();
}
});
// Test Cut and Paste.
editable.innerHTML = 'abc';
var selection = window.getSelection();
selection.collapse(editable, 0);
selection.extend(editable, 1);
// The text won't be removed but should still update Clipboard.
eventSender.keyDown('Cut');
selection.collapse(editable, 0);
eventSender.keyDown('Paste');
assert_equals(editable.innerHTML, 'abcabc', 'Testing Cut.preventDefault() and Paste.')
}, 'Testing Cut and Paste preventDefault()');
</script>
</body>
</html>
......@@ -83,8 +83,8 @@ test(function() {
// Copy shouldn't fire 'input'.
testExecCommandInputType('copy', null, NO_INPUT_EVENT_FIRED);
// Cut/Paste should fire 'input'.
testExecCommandInputType('cut', null, 'cut');
testExecCommandInputType('paste', null, 'paste');
testExecCommandInputType('cut', null, 'deleteByCut');
testExecCommandInputType('paste', null, 'insertFromPaste');
}, 'Testing input with execCommand');
</script>
</body>
......
......@@ -3605,6 +3605,7 @@ interface InputDeviceCapabilities
interface InputEvent : UIEvent
attribute @@toStringTag
getter data
getter dataTransfer
getter inputType
getter isComposing
method constructor
......
......@@ -1891,6 +1891,16 @@ DispatchEventResult dispatchBeforeInputEditorCommand(EventTarget* target, InputE
return target->dispatchEvent(beforeInputEvent);
}
DispatchEventResult dispatchBeforeInputDataTransfer(EventTarget* target, InputEvent::InputType inputType, DataTransfer* dataTransfer, const RangeVector* ranges)
{
if (!RuntimeEnabledFeatures::inputEventEnabled())
return DispatchEventResult::NotCanceled;
if (!target)
return DispatchEventResult::NotCanceled;
InputEvent* beforeInputEvent = InputEvent::createBeforeInput(inputType, dataTransfer, InputEvent::EventCancelable::IsCancelable, InputEvent::EventIsComposing::NotComposing, ranges);
return target->dispatchEvent(beforeInputEvent);
}
InputEvent::InputType deletionInputTypeFromTextGranularity(DeleteDirection direction, TextGranularity granularity)
{
using InputType = InputEvent::InputType;
......
......@@ -364,6 +364,7 @@ const String& nonBreakingSpaceString();
DispatchEventResult dispatchBeforeInputInsertText(EventTarget*, const String& data);
DispatchEventResult dispatchBeforeInputFromComposition(EventTarget*, InputEvent::InputType, const String& data, InputEvent::EventCancelable);
DispatchEventResult dispatchBeforeInputEditorCommand(EventTarget*, InputEvent::InputType, const String& data, const RangeVector*);
DispatchEventResult dispatchBeforeInputDataTransfer(EventTarget*, InputEvent::InputType, DataTransfer*, const RangeVector*);
InputEvent::InputType deletionInputTypeFromTextGranularity(DeleteDirection, TextGranularity);
......
......@@ -542,7 +542,7 @@ void Editor::replaceSelectionWithFragment(DocumentFragment* fragment, bool selec
if (matchStyle)
options |= ReplaceSelectionCommand::MatchStyle;
DCHECK(frame().document());
ReplaceSelectionCommand::create(*frame().document(), fragment, options, InputEvent::InputType::Paste)->apply();
ReplaceSelectionCommand::create(*frame().document(), fragment, options, InputEvent::InputType::InsertFromPaste)->apply();
revealSelectionAfterEditingOperation();
}
......@@ -868,7 +868,7 @@ bool Editor::insertParagraphSeparator()
return true;
}
void Editor::cut()
void Editor::cut(EditorCommandSource source)
{
if (tryDHTMLCut())
return; // DHTML did the whole operation
......@@ -884,7 +884,15 @@ void Editor::cut()
} else {
writeSelectionToPasteboard();
}
deleteSelectionWithSmartDelete(canSmartCopyOrDelete(), InputEvent::InputType::Cut);
if (source == CommandFromMenuOrKeyBinding) {
if (dispatchBeforeInputDataTransfer(findEventTargetFromSelection(), InputEvent::InputType::DeleteByCut, nullptr, nullptr) != DispatchEventResult::NotCanceled)
return;
// 'beforeinput' event handler may destroy target frame.
if (m_frame->document()->frame() != m_frame)
return;
}
deleteSelectionWithSmartDelete(canSmartCopyOrDelete(), InputEvent::InputType::DeleteByCut);
}
}
......@@ -906,7 +914,7 @@ void Editor::copy()
}
}
void Editor::paste()
void Editor::paste(EditorCommandSource source)
{
DCHECK(frame().document());
if (tryDHTMLPaste(AllMimeTypes))
......@@ -916,13 +924,29 @@ void Editor::paste()
spellChecker().updateMarkersForWordsAffectedByEditing(false);
ResourceFetcher* loader = frame().document()->fetcher();
ResourceCacheValidationSuppressor validationSuppressor(loader);
if (frame().selection().isContentRichlyEditable())
PasteMode pasteMode = frame().selection().isContentRichlyEditable() ? AllMimeTypes : PlainTextOnly;
if (source == CommandFromMenuOrKeyBinding) {
DataTransfer* dataTransfer = DataTransfer::create(
DataTransfer::CopyAndPaste,
DataTransferReadable,
DataObject::createFromPasteboard(pasteMode));
if (dispatchBeforeInputDataTransfer(findEventTargetFromSelection(), InputEvent::InputType::InsertFromPaste, dataTransfer, nullptr) != DispatchEventResult::NotCanceled)
return;
// 'beforeinput' event handler may destroy target frame.
if (m_frame->document()->frame() != m_frame)
return;
}
if (pasteMode == AllMimeTypes)
pasteWithPasteboard(Pasteboard::generalPasteboard());
else
pasteAsPlainTextWithPasteboard(Pasteboard::generalPasteboard());
}
void Editor::pasteAsPlainText()
void Editor::pasteAsPlainText(EditorCommandSource source)
{
if (tryDHTMLPaste(PlainTextOnly))
return;
......
......@@ -87,10 +87,10 @@ public:
bool canDelete() const;
bool canSmartCopyOrDelete() const;
void cut();
void cut(EditorCommandSource);
void copy();
void paste();
void pasteAsPlainText();
void paste(EditorCommandSource);
void pasteAsPlainText(EditorCommandSource);
void performDelete();
static void countEvent(ExecutionContext*, const Event*);
......
......@@ -190,6 +190,7 @@ bool CompositeEditCommand::apply()
case InputEvent::InputType::InsertText:
case InputEvent::InputType::InsertLineBreak:
case InputEvent::InputType::InsertParagraph:
case InputEvent::InputType::InsertFromPaste:
case InputEvent::InputType::DeleteComposedCharacterForward:
case InputEvent::InputType::DeleteComposedCharacterBackward:
case InputEvent::InputType::DeleteWordBackward:
......@@ -198,10 +199,9 @@ bool CompositeEditCommand::apply()
case InputEvent::InputType::DeleteLineForward:
case InputEvent::InputType::DeleteContentBackward:
case InputEvent::InputType::DeleteContentForward:
case InputEvent::InputType::Paste:
case InputEvent::InputType::DeleteByCut:
case InputEvent::InputType::Drag:
case InputEvent::InputType::SetWritingDirection:
case InputEvent::InputType::Cut:
case InputEvent::InputType::None:
break;
default:
......
......@@ -161,12 +161,7 @@ InputEvent::InputType InputTypeFromCommandType(WebEditingCommandType commandType
return InputType::Undo;
case CommandType::Redo:
return InputType::Redo;
case CommandType::Copy:
return InputType::Copy;
case CommandType::Cut:
return InputType::Cut;
case CommandType::Paste:
return InputType::Paste;
// Cut and Paste will be handled in |Editor::dispatchCPPEvent()|.
// Styling.
case CommandType::Bold:
......@@ -479,7 +474,7 @@ static bool executeCut(LocalFrame& frame, Event*, EditorCommandSource source, co
// |canExecute()|. See also "Copy", and "Paste" command.
if (!canWriteClipboard(frame, source))
return false;
frame.editor().cut();
frame.editor().cut(source);
return true;
}
......@@ -1126,7 +1121,7 @@ static bool executePaste(LocalFrame& frame, Event*, EditorCommandSource source,
// |canExecute()|. See also "Copy", and "Cut" command.
if (!canReadClipboard(frame, source))
return false;
frame.editor().paste();
frame.editor().paste(source);
return true;
}
......@@ -1145,14 +1140,14 @@ static bool executePasteGlobalSelection(LocalFrame& frame, Event*, EditorCommand
bool oldSelectionMode = Pasteboard::generalPasteboard()->isSelectionMode();
Pasteboard::generalPasteboard()->setSelectionMode(true);
frame.editor().paste();
frame.editor().paste(source);
Pasteboard::generalPasteboard()->setSelectionMode(oldSelectionMode);
return true;
}
static bool executePasteAndMatchStyle(LocalFrame& frame, Event*, EditorCommandSource, const String&)
static bool executePasteAndMatchStyle(LocalFrame& frame, Event*, EditorCommandSource source, const String&)
{
frame.editor().pasteAsPlainText();
frame.editor().pasteAsPlainText(source);
return true;
}
......@@ -1886,8 +1881,8 @@ bool Editor::Command::execute(const String& parameter, Event* triggeringEvent) c
}
}
// 'beforeinput' event handler may destroy |frame()|.
if (!m_frame || !frame().document())
// 'beforeinput' event handler may destroy target frame.
if (m_frame->document()->frame() != m_frame)
return false;
frame().document()->updateStyleAndLayoutIgnorePendingStylesheets();
......
......@@ -351,7 +351,7 @@ void SpellChecker::markMisspellingsAfterApplyingCommand(const CompositeEditComma
// Note: Request spell checking for and only for |ReplaceSelectionCommand|s
// created in |Editor::replaceSelectionWithFragment()|.
// TODO(xiaochengh): May also need to do this after dragging crbug.com/298046.
if (cmd.inputType() != InputEvent::InputType::Paste)
if (cmd.inputType() != InputEvent::InputType::InsertFromPaste)
return;
markMisspellingsAfterReplaceSelectionCommand(toReplaceSelectionCommand(cmd));
......
......@@ -24,6 +24,7 @@ const struct {
{ InputEvent::InputType::InsertOrderedList, "insertOrderedList" },
{ InputEvent::InputType::InsertUnorderedList, "insertUnorderedList" },
{ InputEvent::InputType::InsertHorizontalRule, "insertHorizontalRule" },
{ InputEvent::InputType::InsertFromPaste, "insertFromPaste" },
{ InputEvent::InputType::DeleteComposedCharacterForward, "deleteComposedCharacterForward" },
{ InputEvent::InputType::DeleteComposedCharacterBackward, "deleteComposedCharacterBackward" },
{ InputEvent::InputType::DeleteWordBackward, "deleteWordBackward" },
......@@ -32,11 +33,9 @@ const struct {
{ InputEvent::InputType::DeleteLineForward, "deleteLineForward" },
{ InputEvent::InputType::DeleteContentBackward, "deleteContentBackward" },
{ InputEvent::InputType::DeleteContentForward, "deleteContentForward" },
{ InputEvent::InputType::DeleteByCut, "deleteByCut" },
{ InputEvent::InputType::Undo, "undo" },
{ InputEvent::InputType::Redo, "redo" },
{ InputEvent::InputType::Copy, "copy" },
{ InputEvent::InputType::Cut, "cut" },
{ InputEvent::InputType::Paste, "paste" },
{ InputEvent::InputType::Bold, "bold" },
{ InputEvent::InputType::Italic, "italic" },
{ InputEvent::InputType::Underline, "underline" },
......@@ -94,6 +93,8 @@ InputEvent::InputEvent(const AtomicString& type, const InputEventInit& initializ
m_inputType = convertStringToInputType(initializer.inputType());
if (initializer.hasData())
m_data = initializer.data();
if (initializer.hasDataTransfer())
m_dataTransfer = initializer.dataTransfer();
if (initializer.hasIsComposing())
m_isComposing = initializer.isComposing();
if (initializer.hasRanges())
......@@ -118,6 +119,22 @@ InputEvent* InputEvent::createBeforeInput(InputType inputType, const String& dat
return InputEvent::create(EventTypeNames::beforeinput, inputEventInit);
}
/* static */
InputEvent* InputEvent::createBeforeInput(InputType inputType, DataTransfer* dataTransfer, EventCancelable cancelable, EventIsComposing isComposing, const RangeVector* ranges)
{
InputEventInit inputEventInit;
inputEventInit.setBubbles(true);
inputEventInit.setCancelable(cancelable == IsCancelable);
inputEventInit.setInputType(convertInputTypeToString(inputType));
inputEventInit.setDataTransfer(dataTransfer);
inputEventInit.setIsComposing(isComposing == IsComposing);
if (ranges)
inputEventInit.setRanges(*ranges);
return InputEvent::create(EventTypeNames::beforeinput, inputEventInit);
}
/* static */
InputEvent* InputEvent::createInput(InputType inputType, const String& data, EventIsComposing isComposing, const RangeVector* ranges)
{
......@@ -164,6 +181,7 @@ EventDispatchMediator* InputEvent::createMediator()
DEFINE_TRACE(InputEvent)
{
UIEvent::trace(visitor);
visitor->trace(m_dataTransfer);
visitor->trace(m_ranges);
}
......
......@@ -5,6 +5,7 @@
#ifndef InputEvent_h
#define InputEvent_h
#include "core/clipboard/DataTransfer.h"
#include "core/dom/StaticRange.h"
#include "core/events/InputEventInit.h"
#include "core/events/UIEvent.h"
......@@ -30,6 +31,7 @@ public:
InsertOrderedList,
InsertUnorderedList,
InsertHorizontalRule,
InsertFromPaste,
// Deletion.
DeleteComposedCharacterForward,
DeleteComposedCharacterBackward,
......@@ -39,12 +41,10 @@ public:
DeleteLineForward,
DeleteContentBackward,
DeleteContentForward,
DeleteByCut,
// Command.
Undo,
Redo,
Copy,
Cut,
Paste,
// Styling.
Bold,
Italic,
......@@ -88,10 +88,12 @@ public:
};
static InputEvent* createBeforeInput(InputType, const String& data, EventCancelable, EventIsComposing, const RangeVector*);
static InputEvent* createBeforeInput(InputType, DataTransfer*, EventCancelable, EventIsComposing, const RangeVector*);
static InputEvent* createInput(InputType, const String& data, EventIsComposing, const RangeVector*);
String inputType() const;
const String& data() const { return m_data; }
DataTransfer* dataTransfer() const { return m_dataTransfer.get(); }
bool isComposing() const { return m_isComposing; }
// Returns a copy of target ranges during event dispatch, and returns an empty
// vector after dispatch.
......@@ -109,6 +111,7 @@ private:
InputType m_inputType;
String m_data;
Member<DataTransfer> m_dataTransfer;
bool m_isComposing;
// We have to stored |Range| internally and only expose |StaticRange|, please
// see comments in |InputEventDispatchMediator::dispatchEvent()|.
......
......@@ -10,6 +10,7 @@
] interface InputEvent : UIEvent {
readonly attribute DOMString inputType;
readonly attribute DOMString data;
readonly attribute DataTransfer dataTransfer;
readonly attribute boolean isComposing;
sequence<StaticRange> getRanges();
};
......@@ -9,6 +9,7 @@
] dictionary InputEventInit : UIEventInit {
DOMString inputType = "";
DOMString data = "";
DataTransfer? dataTransfer;
boolean isComposing = false;
sequence<Range> ranges = [];
};
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment