Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable undo/redo for markdown editor, apart from firefox #3582

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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