diff --git a/plugins/talk-plugin-rich-text/client/components/rte/RTE.js b/plugins/talk-plugin-rich-text/client/components/rte/RTE.js index 655ebfc1d..e919a86ef 100644 --- a/plugins/talk-plugin-rich-text/client/components/rte/RTE.js +++ b/plugins/talk-plugin-rich-text/client/components/rte/RTE.js @@ -126,6 +126,17 @@ class RTE extends React.Component { }); }; + // Allow buttons to handle shortcuts. + handleShortcut = e => { + let handled = false; + this.forEachButton(b => { + if (!handled) { + handled = !!(b.onShortcut && b.onShortcut(e)); + } + }); + return handled; + }; + // Called when Enter was pressed without shift. // Traverses from bottom to top and calling // button handlers and stops when one has handled this event. @@ -209,6 +220,13 @@ class RTE extends React.Component { return false; } + if (e.metaKey || e.ctrlKey) { + if (this.handleShortcut(e)) { + e.preventDefault(); + return false; + } + } + // Newlines Or Special Enter Behaviors. if (e.key === 'Enter') { if (!e.shiftKey && this.handleSpecialEnter()) { diff --git a/plugins/talk-plugin-rich-text/client/components/rte/buttons/Blockquote.js b/plugins/talk-plugin-rich-text/client/components/rte/buttons/Blockquote.js index 5fb6dbf63..201260fcd 100644 --- a/plugins/talk-plugin-rich-text/client/components/rte/buttons/Blockquote.js +++ b/plugins/talk-plugin-rich-text/client/components/rte/buttons/Blockquote.js @@ -4,7 +4,7 @@ import { insertNewLineAfterNode, insertNodes, getSelectedNodesExpanded, - outdentNode, + outdentBlock, selectEndOfNode, indentNodes, } from '../lib/dom'; @@ -12,7 +12,7 @@ import { function execCommand() { const bq = findIntersecting('BLOCKQUOTE', this.container); if (bq) { - outdentNode(bq, true); + outdentBlock(bq, true); } else { // Expanded selection means we always select whole lines. const selectedNodes = getSelectedNodesExpanded(); diff --git a/plugins/talk-plugin-rich-text/client/components/rte/buttons/Bold.js b/plugins/talk-plugin-rich-text/client/components/rte/buttons/Bold.js index 53bdfe71b..8f5b55de5 100644 --- a/plugins/talk-plugin-rich-text/client/components/rte/buttons/Bold.js +++ b/plugins/talk-plugin-rich-text/client/components/rte/buttons/Bold.js @@ -25,8 +25,16 @@ function isDisabled() { this.container ); } +function onShortcut(e) { + if (e.key === 'b') { + if (!isDisabled.apply(this)) { + execCommand.apply(this); + } + return true; + } +} -const Bold = createToggle(execCommand, { isActive, isDisabled }); +const Bold = createToggle(execCommand, { isActive, isDisabled, onShortcut }); Bold.defaultProps = { children: 'Bold', diff --git a/plugins/talk-plugin-rich-text/client/components/rte/buttons/Italic.js b/plugins/talk-plugin-rich-text/client/components/rte/buttons/Italic.js index 64786d5b9..d35f08da0 100644 --- a/plugins/talk-plugin-rich-text/client/components/rte/buttons/Italic.js +++ b/plugins/talk-plugin-rich-text/client/components/rte/buttons/Italic.js @@ -23,8 +23,16 @@ function isDisabled() { this.container ); } +function onShortcut(e) { + if (e.key === 'i') { + if (!isDisabled.apply(this)) { + execCommand.apply(this); + } + return true; + } +} -const Italic = createToggle(execCommand, { isActive, isDisabled }); +const Italic = createToggle(execCommand, { isActive, isDisabled, onShortcut }); Italic.defaultProps = { children: 'Italic', diff --git a/plugins/talk-plugin-rich-text/client/components/rte/factories/createToggle.js b/plugins/talk-plugin-rich-text/client/components/rte/factories/createToggle.js index 6e1ca5b71..028757222 100644 --- a/plugins/talk-plugin-rich-text/client/components/rte/factories/createToggle.js +++ b/plugins/talk-plugin-rich-text/client/components/rte/factories/createToggle.js @@ -8,7 +8,7 @@ import Button from '../components/Button'; */ const createToggle = ( execCommand, - { onEnter, isActive = () => false, isDisabled = () => false } = {} + { onEnter, onShortcut, isActive = () => false, isDisabled = () => false } = {} ) => { class Toggle extends React.Component { state = { @@ -20,6 +20,8 @@ const createToggle = ( isActive = () => isActive.apply(this.props.api); isDisabled = () => isDisabled.apply(this.props.api); onEnter = (...args) => onEnter && onEnter.apply(this.props.api, args); + onShortcut = (...args) => + onShortcut && onShortcut.apply(this.props.api, args); unmounted = false; componentWillUnmount() { diff --git a/plugins/talk-plugin-rich-text/client/components/rte/lib/dom.js b/plugins/talk-plugin-rich-text/client/components/rte/lib/dom.js index c2bfb4372..a5fbec19e 100644 --- a/plugins/talk-plugin-rich-text/client/components/rte/lib/dom.js +++ b/plugins/talk-plugin-rich-text/client/components/rte/lib/dom.js @@ -498,7 +498,7 @@ export function getSelectedChildren(ancestor) { /** * Removes node and assimilate its children with the parent. */ -export function outdentNode(node, changeSelection) { +export function outdentBlock(node, changeSelection) { // Save previous range. const selectionWasInside = isSelectionInside(node); const { @@ -517,6 +517,7 @@ export function outdentNode(node, changeSelection) { const needLineAfter = node.nextSibling && !isBlockElement(node.nextSibling) && + node.lastChild && !isBlockElement(node.lastChild); const needLineBefore = node.previousSibling &&