Skip to content

Commit

Permalink
Fix 'beforeinput' event for content editable divs (closes DevExpress#…
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexKamaev authored and AndreyBelym committed Jan 23, 2020
1 parent 8d76db2 commit 251c9aa
Show file tree
Hide file tree
Showing 2 changed files with 136 additions and 40 deletions.
31 changes: 21 additions & 10 deletions src/client/automation/playback/type/type-text.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,18 @@ function _excludeInvisibleSymbolsFromSelection (selection) {
return selection;
}

// NOTE: https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/beforeinput_event
// The `beforeInput` event is supported only in Chrome-based browsers and Safari
// The order of events differs in Chrome and Safari:
// In Chrome: `beforeinput` occurs before `textInput`
// In Safari: `beforeinput` occurs after `textInput`
function simulateBeforeInput (element, text, needSimulate) {
if (needSimulate)
return eventSimulator.beforeInput(element, text);

return true;
}

// NOTE: Typing can be prevented in Chrome/Edge but can not be prevented in IE11 or Firefox
// Firefox does not support TextInput event
// Safari supports the TextInput event but has a bug: e.data is added to the node value.
Expand Down Expand Up @@ -205,8 +217,14 @@ function _typeTextToContentEditable (element, text) {
if (!startNode || !domUtils.isContentEditableElement(startNode) || !domUtils.isRenderedNode(startNode))
return;

if (!simulateBeforeInput(element, text, browserUtils.isChrome))
return;

beforeContentChanged();

if (needProcessInput)
needProcessInput = simulateBeforeInput(element, text, browserUtils.isSafari);

if (needProcessInput) {
// NOTE: we can type only to the text nodes; for nodes with the 'element-node' type, we use a special behavior
if (domUtils.isElementNode(startNode))
Expand All @@ -225,20 +243,13 @@ function _typeTextToTextEditable (element, text) {
let endSelection = textSelection.getSelectionEnd(element);
const isInputTypeNumber = domUtils.isInputElement(element) && element.type === 'number';

// NOTE: https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/beforeinput_event
// The `beforeInput` event is supported only in Chrome-based browsers and Safari
// The order of events differs in Chrome and Safari:
// In Chrome: `beforeinput` occurs before `textInput`
// In Safari: `beforeinput` occurs after `textInput`
const needProcessTextInput = !browserUtils.isChrome || eventSimulator.beforeInput(element, text);

if (!needProcessTextInput)
if (!simulateBeforeInput(element, text, browserUtils.isChrome))
return;

let needProcessInput = simulateTextInput(element, text);

if (needProcessInput && browserUtils.isSafari)
needProcessInput = eventSimulator.beforeInput(element, text);
if (needProcessInput)
needProcessInput = simulateBeforeInput(element, text, browserUtils.isSafari);

if (!needProcessInput)
return;
Expand Down
145 changes: 115 additions & 30 deletions test/client/fixtures/automation/type-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,36 @@ $(document).ready(function () {
}

if (nativeMethods.WindowInputEvent && !browserUtils.isFirefox) {
const expectedAllEvents = [
{ type: 'beforeinput', data: '1' },
{ type: 'textInput', data: '1' },
{ type: 'beforeinput', data: '2' },
{ type: 'textInput', data: '2' },
{ type: 'beforeinput', data: '3' },
{ type: 'textInput', data: '3' },
];

const expectedAllEventsReversed = [
{ type: 'textInput', data: '1' },
{ type: 'beforeinput', data: '1' },
{ type: 'textInput', data: '2' },
{ type: 'beforeinput', data: '2' },
{ type: 'textInput', data: '3' },
{ type: 'beforeinput', data: '3' }
];

const expectedOnlyBeforeInput = [
{ type: 'beforeinput', data: '1' },
{ type: 'beforeinput', data: '2' },
{ type: 'beforeinput', data: '3' }
];

const expectedOnlyTextInput = [
{ type: 'textInput', data: '1' },
{ type: 'textInput', data: '2' },
{ type: 'textInput', data: '3' }
];

asyncTest('Should fire `beforeInput` event', function () {
const input1 = document.createElement('input');
const input2 = document.createElement('input');
Expand Down Expand Up @@ -553,36 +583,6 @@ $(document).ready(function () {
e.preventDefault();
});

const expectedAllEvents = [
{ type: 'beforeinput', data: '1' },
{ type: 'textInput', data: '1' },
{ type: 'beforeinput', data: '2' },
{ type: 'textInput', data: '2' },
{ type: 'beforeinput', data: '3' },
{ type: 'textInput', data: '3' },
];

const expectedAllEventsReversed = [
{ type: 'textInput', data: '1' },
{ type: 'beforeinput', data: '1' },
{ type: 'textInput', data: '2' },
{ type: 'beforeinput', data: '2' },
{ type: 'textInput', data: '3' },
{ type: 'beforeinput', data: '3' }
];

const expectedOnlyBeforeInput = [
{ type: 'beforeinput', data: '1' },
{ type: 'beforeinput', data: '2' },
{ type: 'beforeinput', data: '3' }
];

const expectedOnlyTextInput = [
{ type: 'textInput', data: '1' },
{ type: 'textInput', data: '2' },
{ type: 'textInput', data: '3' }
];

const automation1 = new TypeAutomation(input1, '123', new TypeOptions());
const automation2 = new TypeAutomation(input2, '123', new TypeOptions());
const automation3 = new TypeAutomation(input3, '123', new TypeOptions());
Expand Down Expand Up @@ -614,6 +614,91 @@ $(document).ready(function () {
start();
});
});

asyncTest('Should fire `beforeInput` event in contenteditable div', function () {
const input1 = document.createElement('div');
const input2 = document.createElement('div');
const input3 = document.createElement('div');

input1.contentEditable = true;
input2.contentEditable = true;
input3.contentEditable = true;

input1.className = TEST_ELEMENT_CLASS;
input2.className = TEST_ELEMENT_CLASS;
input3.className = TEST_ELEMENT_CLASS;

document.body.appendChild(input1);
document.body.appendChild(input2);
document.body.appendChild(input3);

const log1 = [];
const log2 = [];
const log3 = [];

function logEvents (e, log) {
log.push({ type: e.type, data: e.data });
}

input1.addEventListener('beforeinput', function (e) {
logEvents(e, log1);
});

input1.addEventListener('textInput', function (e) {
logEvents(e, log1);
});

input2.addEventListener('beforeinput', function (e) {
logEvents(e, log2);

e.preventDefault();
});

input2.addEventListener('textInput', function (e) {
logEvents(e, log2);
});

input3.addEventListener('beforeinput', function (e) {
logEvents(e, log3);
});

input3.addEventListener('textInput', function (e) {
logEvents(e, log3);

e.preventDefault();
});

const automation1 = new TypeAutomation(input1, '123', new TypeOptions());
const automation2 = new TypeAutomation(input2, '123', new TypeOptions());
const automation3 = new TypeAutomation(input3, '123', new TypeOptions());

return automation1.run()
.then(function () {
return automation2.run();
})
.then(function () {
return automation3.run();
})
.then(function () {
if (browserUtils.isChrome) {
deepEqual(log1, expectedAllEvents);
deepEqual(log2, expectedOnlyBeforeInput);
deepEqual(log3, expectedAllEvents);
}

if (browserUtils.isSafari) {
deepEqual(log1, expectedAllEventsReversed);
deepEqual(log2, expectedAllEventsReversed);
deepEqual(log3, expectedOnlyTextInput);
}

strictEqual(input1.innerText, '123');
strictEqual(input2.innerText, '');
strictEqual(input3.innerText, '');

start();
});
});
}

if (nativeMethods.inputValueSetter) {
Expand Down

0 comments on commit 251c9aa

Please sign in to comment.