Skip to content

Commit

Permalink
Enable undo/redo for markdown editor, apart from firefox (#3582)
Browse files Browse the repository at this point in the history
* Enable undo/redo for markdown editor, apart from firefox

* Add test-and-retry logic to insertText to account for ability to focus the text area before brute forcing
  • Loading branch information
chandlerprall authored Jun 15, 2020
1 parent d0bf6ac commit dc8fee5
Show file tree
Hide file tree
Showing 3 changed files with 114 additions and 101 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -153,35 +153,39 @@ exports[`EuiMarkdownEditor is rendered 1`] = `
</div>
</div>
<div
class="euiMarkdownEditor__dropZone"
style="display:block"
>
<textarea
class="euiMarkdownEditor__textArea"
id="editorId"
rows="6"
style="height:calc(118px"
/>
<button
class="euiMarkdownEditor__dropZone__button"
<div
class="euiMarkdownEditor__dropZone"
>
<div
aria-hidden="true"
class="euiMarkdownEditor__dropZone__icon"
data-euiicon-type="paperClip"
<textarea
class="euiMarkdownEditor__textArea"
id="editorId"
rows="6"
style="height:calc(118px"
/>
<div
class="euiMarkdownEditor__dropZone__text"
<button
class="euiMarkdownEditor__dropZone__button"
>
Attach files by dragging & dropping or by clicking this area
</div>
</button>
<input
autocomplete="off"
multiple=""
style="display:none"
tabindex="-1"
type="file"
/>
<div
aria-hidden="true"
class="euiMarkdownEditor__dropZone__icon"
data-euiicon-type="paperClip"
/>
<div
class="euiMarkdownEditor__dropZone__text"
>
Attach files by dragging & dropping or by clicking this area
</div>
</button>
<input
autocomplete="off"
multiple=""
style="display:none"
tabindex="-1"
type="file"
/>
</div>
</div>
</div>
`;
75 changes: 42 additions & 33 deletions src/components/markdown_editor/markdown_actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,54 +227,63 @@ function wordSelectionEnd(text: string, i: number, multiline: boolean): number {
return index;
}

let canInsertText: boolean | null = null;

/**
* Note that we're using the native HTMLTextAreaElement.set() method to play nicely with
* React's synthetic event system. We fallback to a brute force way of doing it if the
* above doesn't work. Although all modern browsers, including IE, seem to be fine:
* https://hustle.bizongo.in/simulate-react-on-change-on-controlled-components-baa336920e04
*/
const MAX_TRIES = 10;
const TRY_TIMEOUT = 10 /*ms*/;
export function insertText(
textarea: HTMLTextAreaElement,
{ text, selectionStart, selectionEnd }: SelectionRange
) {
const originalSelectionStart = textarea.selectionStart;
const before = textarea.value.slice(0, originalSelectionStart);
const after = textarea.value.slice(textarea.selectionEnd);
const inputEvent = new Event('input', { bubbles: true });

if (canInsertText === null || canInsertText === true) {
canInsertText = true;

const nativeInputValueSetter =
// @ts-ignore
Object.getOwnPropertyDescriptor(
// configuration modal/dialog will continue intercepting focus in Safari
// need to wait until the textarea can receive focus
let tries = 0;

const insertText = () => {
const insertResult = document.execCommand('insertText', false, text);

if (insertResult === false) {
/**
* Fallback for Firefox; this kills undo/redo but at least updates the value
*
* Note that we're using the native HTMLTextAreaElement.set() method to play nicely with
* React's synthetic event system.
* https://hustle.bizongo.in/simulate-react-on-change-on-controlled-components-baa336920e04
*/
const inputEvent = new Event('input', { bubbles: true });
const nativeInputValueSetter =
// @ts-ignore
window.HTMLTextAreaElement.prototype,
'value'
).set;
try {
Object.getOwnPropertyDescriptor(
// @ts-ignore
window.HTMLTextAreaElement.prototype,
'value'
).set;
// @ts-ignore
nativeInputValueSetter.call(textarea, before + text + after);

textarea.dispatchEvent(inputEvent);
} catch (error) {
canInsertText = false;
}
}

if (!canInsertText) {
// If calling [HTMLTextAreaElement.set()] fails, just brute-force it
textarea.value = before + text + after;
textarea.dispatchEvent(inputEvent);
}
if (selectionStart != null && selectionEnd != null) {
textarea.setSelectionRange(selectionStart, selectionEnd);
} else {
textarea.setSelectionRange(originalSelectionStart, textarea.selectionEnd);
}
};

const focusTextarea = () => {
textarea.focus();
if (document.activeElement === textarea) {
insertText();
} else if (++tries === MAX_TRIES) {
insertText();
} else {
setTimeout(focusTextarea, TRY_TIMEOUT);
}
};

if (selectionStart != null && selectionEnd != null) {
textarea.setSelectionRange(selectionStart, selectionEnd);
} else {
textarea.setSelectionRange(originalSelectionStart, textarea.selectionEnd);
}
focusTextarea();
}

function styleSelectedText(
Expand Down
86 changes: 43 additions & 43 deletions src/components/markdown_editor/markdown_editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -228,56 +228,56 @@ export const EuiMarkdownEditor: FunctionComponent<EuiMarkdownEditorProps> = ({
uiPlugins={uiPlugins}
/>

{isPreviewing ? (
{isPreviewing && (
<div
className="euiMarkdownEditor__preview"
style={{ height: `${height}px` }}>
<EuiMarkdownFormat processor={processor}>{value}</EuiMarkdownFormat>
</div>
) : (
<>
<EuiMarkdownEditorDropZone>
<EuiMarkdownEditorTextArea
ref={setTextareaRef}
height={height}
id={editorId}
onChange={e => onChange(e.target.value)}
value={value}
/>
</EuiMarkdownEditorDropZone>
{textareaRef && pluginEditorPlugin && (
<EuiOverlayMask>
<EuiModal onClose={() => setPluginEditorPlugin(undefined)}>
{createElement(pluginEditorPlugin.editor!, {
node:
)}
{/* Toggle the editor's display instead of unmounting to retain its undo/redo history */}
<div style={{ display: isPreviewing ? 'none' : 'block' }}>
<EuiMarkdownEditorDropZone>
<EuiMarkdownEditorTextArea
ref={setTextareaRef}
height={height}
id={editorId}
onChange={e => onChange(e.target.value)}
value={value}
/>
</EuiMarkdownEditorDropZone>
{textareaRef && pluginEditorPlugin && (
<EuiOverlayMask>
<EuiModal onClose={() => setPluginEditorPlugin(undefined)}>
{createElement(pluginEditorPlugin.editor!, {
node:
selectedNode &&
selectedNode.type === pluginEditorPlugin.name
? selectedNode
: null,
onCancel: () => setPluginEditorPlugin(undefined),
onSave: markdown => {
if (
selectedNode &&
selectedNode.type === pluginEditorPlugin.name
? selectedNode
: null,
onCancel: () => setPluginEditorPlugin(undefined),
onSave: markdown => {
if (
selectedNode &&
selectedNode.type === pluginEditorPlugin.name
) {
textareaRef.setSelectionRange(
selectedNode.position.start.offset,
selectedNode.position.end.offset
);
}
insertText(textareaRef, {
text: markdown,
selectionStart: undefined,
selectionEnd: undefined,
});
setPluginEditorPlugin(undefined);
},
})}
</EuiModal>
</EuiOverlayMask>
)}
</>
)}
) {
textareaRef.setSelectionRange(
selectedNode.position.start.offset,
selectedNode.position.end.offset
);
}
insertText(textareaRef, {
text: markdown,
selectionStart: undefined,
selectionEnd: undefined,
});
setPluginEditorPlugin(undefined);
},
})}
</EuiModal>
</EuiOverlayMask>
)}
</div>
</div>
</EuiMarkdownContext.Provider>
);
Expand Down

0 comments on commit dc8fee5

Please sign in to comment.