@squiz/formatted-text-editor 1.67.1 → 1.68.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/cypress/e2e/bold.spec.cy.ts +2 -2
  3. package/lib/Editor/Editor.js +4 -1
  4. package/lib/EditorToolbar/Tools/Bold/BoldButton.js +2 -1
  5. package/lib/EditorToolbar/Tools/ClearFormatting/ClearFormattingButton.js +2 -1
  6. package/lib/EditorToolbar/Tools/Image/ImageButton.js +2 -1
  7. package/lib/EditorToolbar/Tools/Italic/ItalicButton.js +2 -1
  8. package/lib/EditorToolbar/Tools/Link/LinkButton.js +2 -1
  9. package/lib/EditorToolbar/Tools/Link/RemoveLinkButton.js +2 -1
  10. package/lib/EditorToolbar/Tools/Redo/RedoButton.js +2 -1
  11. package/lib/EditorToolbar/Tools/Underline/UnderlineButton.js +2 -1
  12. package/lib/EditorToolbar/Tools/Undo/UndoButton.js +2 -1
  13. package/lib/utils/getShortcutSymbol.d.ts +1 -0
  14. package/lib/utils/getShortcutSymbol.js +8 -0
  15. package/package.json +2 -2
  16. package/src/Editor/Editor.spec.tsx +8 -8
  17. package/src/Editor/Editor.tsx +6 -1
  18. package/src/EditorToolbar/FloatingToolbar.spec.tsx +5 -5
  19. package/src/EditorToolbar/Tools/Bold/BoldButton.spec.tsx +3 -3
  20. package/src/EditorToolbar/Tools/Bold/BoldButton.tsx +2 -1
  21. package/src/EditorToolbar/Tools/ClearFormatting/ClearFormattingButton.spec.tsx +2 -2
  22. package/src/EditorToolbar/Tools/ClearFormatting/ClearFormattingButton.tsx +4 -1
  23. package/src/EditorToolbar/Tools/Image/ImageButton.spec.tsx +1 -1
  24. package/src/EditorToolbar/Tools/Image/ImageButton.tsx +2 -1
  25. package/src/EditorToolbar/Tools/Italic/ItalicButton.spec.tsx +3 -3
  26. package/src/EditorToolbar/Tools/Italic/ItalicButton.tsx +2 -1
  27. package/src/EditorToolbar/Tools/Link/LinkButton.spec.tsx +1 -1
  28. package/src/EditorToolbar/Tools/Link/LinkButton.tsx +2 -1
  29. package/src/EditorToolbar/Tools/Link/RemoveLinkButton.spec.tsx +5 -5
  30. package/src/EditorToolbar/Tools/Link/RemoveLinkButton.tsx +2 -1
  31. package/src/EditorToolbar/Tools/Redo/RedoButton.spec.tsx +6 -6
  32. package/src/EditorToolbar/Tools/Redo/RedoButton.tsx +2 -1
  33. package/src/EditorToolbar/Tools/Underline/Underline.spec.tsx +3 -3
  34. package/src/EditorToolbar/Tools/Underline/UnderlineButton.tsx +2 -1
  35. package/src/EditorToolbar/Tools/Undo/UndoButton.spec.tsx +5 -5
  36. package/src/EditorToolbar/Tools/Undo/UndoButton.tsx +2 -1
  37. package/src/utils/getShortcutSymbol.spec.ts +27 -0
  38. package/src/utils/getShortcutSymbol.ts +4 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Change Log
2
2
 
3
+ ## 1.68.1
4
+
5
+ ### Patch Changes
6
+
7
+ - e2010f7: Fixed pasting into FTE from Microsoft office
8
+
9
+ ## 1.68.0
10
+
11
+ ### Minor Changes
12
+
13
+ - 4d3dbba: Updated FTE shortcuts to change tooltips depending on OS
14
+
3
15
  ## 1.67.1
4
16
 
5
17
  ### Patch Changes
@@ -7,11 +7,11 @@ describe('The formatted text editor renders', () => {
7
7
  .type('It was the best of times it was the worst of times.')
8
8
  .setSelection('best of times');
9
9
 
10
- cy.findAllByRole('button', { name: 'Bold (cmd+B)' }).first().click();
10
+ cy.findAllByRole('button', { name: 'Bold (Ctrl+B)' }).first().click();
11
11
 
12
12
  cy.get('strong').should('include.text', 'best of times');
13
13
 
14
- cy.findAllByRole('button', { name: 'Bold (cmd+B)' }).first().click();
14
+ cy.findAllByRole('button', { name: 'Bold (Ctrl+B)' }).first().click();
15
15
 
16
16
  cy.get('strong').should('not.exist');
17
17
  });
@@ -38,7 +38,10 @@ const WrappedEditor = () => {
38
38
  const preventImagePaste = (0, react_1.useCallback)((event) => {
39
39
  const { clipboardData } = event;
40
40
  const pastedData = clipboardData?.files[0];
41
- if (pastedData?.type && pastedData?.type.startsWith('image/')) {
41
+ if (pastedData?.type &&
42
+ pastedData?.type.startsWith('image/') &&
43
+ // Still allow paste of any text that came through (Word, etc)
44
+ !clipboardData?.types.includes('text/plain')) {
42
45
  event.preventDefault();
43
46
  }
44
47
  // Allow other paste event handlers to be run.
@@ -7,6 +7,7 @@ const react_1 = __importDefault(require("react"));
7
7
  const react_2 = require("@remirror/react");
8
8
  const Button_1 = __importDefault(require("../../../ui/Button/Button"));
9
9
  const FormatBoldRounded_1 = __importDefault(require("@mui/icons-material/FormatBoldRounded"));
10
+ const getShortcutSymbol_1 = require("../../../utils/getShortcutSymbol");
10
11
  const BoldButton = () => {
11
12
  const { toggleBold } = (0, react_2.useCommands)();
12
13
  const chain = (0, react_2.useChainedCommands)();
@@ -17,6 +18,6 @@ const BoldButton = () => {
17
18
  chain.toggleBold().focus().run();
18
19
  }
19
20
  };
20
- return (react_1.default.createElement(Button_1.default, { handleOnClick: handleSelect, isDisabled: !enabled, isActive: active.bold(), icon: react_1.default.createElement(FormatBoldRounded_1.default, null), label: "Bold (cmd+B)" }));
21
+ return (react_1.default.createElement(Button_1.default, { handleOnClick: handleSelect, isDisabled: !enabled, isActive: active.bold(), icon: react_1.default.createElement(FormatBoldRounded_1.default, null), label: `Bold (${(0, getShortcutSymbol_1.getShortcutSymbol)()}+B)` }));
21
22
  };
22
23
  exports.default = BoldButton;
@@ -31,6 +31,7 @@ const react_2 = require("@remirror/react");
31
31
  const react_components_1 = require("@remirror/react-components");
32
32
  const FormatClearRounded_1 = __importDefault(require("@mui/icons-material/FormatClearRounded"));
33
33
  const Button_1 = __importDefault(require("../../../ui/Button/Button"));
34
+ const getShortcutSymbol_1 = require("../../../utils/getShortcutSymbol");
34
35
  const ClearFormattingButton = () => {
35
36
  const { clearFormatting } = (0, react_2.useCommands)();
36
37
  const { selection } = (0, react_2.useEditorState)();
@@ -51,6 +52,6 @@ const ClearFormattingButton = () => {
51
52
  (0, react_2.useKeymap)('Mod-\\', handleShortcut);
52
53
  return (react_1.default.createElement(react_1.default.Fragment, null,
53
54
  react_1.default.createElement(react_components_1.VerticalDivider, null),
54
- react_1.default.createElement(Button_1.default, { handleOnClick: handleSelect, isDisabled: false, isActive: false, icon: react_1.default.createElement(FormatClearRounded_1.default, null), label: `${contentSelected ? 'Clear formatting from selection' : 'Clear all formatting'} (cmd+\\)` })));
55
+ react_1.default.createElement(Button_1.default, { handleOnClick: handleSelect, isDisabled: false, isActive: false, icon: react_1.default.createElement(FormatClearRounded_1.default, null), label: `${contentSelected ? 'Clear formatting from selection' : 'Clear all formatting'} (${(0, getShortcutSymbol_1.getShortcutSymbol)()}+\\)` })));
55
56
  };
56
57
  exports.default = ClearFormattingButton;
@@ -32,6 +32,7 @@ const ImageRounded_1 = __importDefault(require("@mui/icons-material/ImageRounded
32
32
  const ImageModal_1 = __importDefault(require("./ImageModal"));
33
33
  const Button_1 = __importDefault(require("../../../ui/Button/Button"));
34
34
  const Extensions_1 = require("../../../Extensions/Extensions");
35
+ const getShortcutSymbol_1 = require("../../../utils/getShortcutSymbol");
35
36
  const ImageButton = ({ inPopover = false }) => {
36
37
  const [showModal, setShowModal] = (0, react_1.useState)(false);
37
38
  const { insertImage, insertAssetImage } = (0, react_2.useCommands)();
@@ -69,7 +70,7 @@ const ImageButton = ({ inPopover = false }) => {
69
70
  (0, react_2.useKeymap)('Mod-l', disabled ? () => false : handleShortcut);
70
71
  }
71
72
  return (react_1.default.createElement(react_1.default.Fragment, null,
72
- react_1.default.createElement(Button_1.default, { handleOnClick: handleClick, isActive: active.image() || active.assetImage(), icon: react_1.default.createElement(ImageRounded_1.default, null), label: "Image (cmd+L)", isDisabled: disabled }),
73
+ react_1.default.createElement(Button_1.default, { handleOnClick: handleClick, isActive: active.image() || active.assetImage(), icon: react_1.default.createElement(ImageRounded_1.default, null), label: `Image (${(0, getShortcutSymbol_1.getShortcutSymbol)()}+L)`, isDisabled: disabled }),
73
74
  showModal && react_1.default.createElement(ImageModal_1.default, { onCancel: () => setShowModal(false), onSubmit: handleSubmit })));
74
75
  };
75
76
  exports.default = ImageButton;
@@ -7,6 +7,7 @@ const react_1 = __importDefault(require("react"));
7
7
  const react_2 = require("@remirror/react");
8
8
  const Button_1 = __importDefault(require("../../../ui/Button/Button"));
9
9
  const FormatItalicRounded_1 = __importDefault(require("@mui/icons-material/FormatItalicRounded"));
10
+ const getShortcutSymbol_1 = require("../../../utils/getShortcutSymbol");
10
11
  const ItalicButton = () => {
11
12
  const { toggleItalic } = (0, react_2.useCommands)();
12
13
  const chain = (0, react_2.useChainedCommands)();
@@ -17,6 +18,6 @@ const ItalicButton = () => {
17
18
  chain.toggleItalic().focus().run();
18
19
  }
19
20
  };
20
- return (react_1.default.createElement(Button_1.default, { handleOnClick: handleSelect, isDisabled: !enabled, isActive: active.italic(), icon: react_1.default.createElement(FormatItalicRounded_1.default, null), label: "Italic (cmd+I)" }));
21
+ return (react_1.default.createElement(Button_1.default, { handleOnClick: handleSelect, isDisabled: !enabled, isActive: active.italic(), icon: react_1.default.createElement(FormatItalicRounded_1.default, null), label: `Italic (${(0, getShortcutSymbol_1.getShortcutSymbol)()}+I)` }));
21
22
  };
22
23
  exports.default = ItalicButton;
@@ -32,6 +32,7 @@ const LinkModal_1 = __importDefault(require("./LinkModal"));
32
32
  const Button_1 = __importDefault(require("../../../ui/Button/Button"));
33
33
  const react_2 = require("@remirror/react");
34
34
  const Extensions_1 = require("../../../Extensions/Extensions");
35
+ const getShortcutSymbol_1 = require("../../../utils/getShortcutSymbol");
35
36
  const LinkButton = ({ inPopover = false }) => {
36
37
  const [showModal, setShowModal] = (0, react_1.useState)(false);
37
38
  const { updateLink, updateAssetLink } = (0, react_2.useCommands)();
@@ -64,7 +65,7 @@ const LinkButton = ({ inPopover = false }) => {
64
65
  (0, react_2.useKeymap)('Mod-k', disabled ? () => true : handleShortcut);
65
66
  }
66
67
  return (react_1.default.createElement(react_1.default.Fragment, null,
67
- react_1.default.createElement(Button_1.default, { handleOnClick: handleClick, isActive: active.link() || active.assetLink(), icon: react_1.default.createElement(InsertLinkRounded_1.default, null), label: "Link (cmd+K)", isDisabled: disabled }),
68
+ react_1.default.createElement(Button_1.default, { handleOnClick: handleClick, isActive: active.link() || active.assetLink(), icon: react_1.default.createElement(InsertLinkRounded_1.default, null), label: `Link (${(0, getShortcutSymbol_1.getShortcutSymbol)()}+K)`, isDisabled: disabled }),
68
69
  showModal && react_1.default.createElement(LinkModal_1.default, { onCancel: () => setShowModal(false), onSubmit: handleSubmit })));
69
70
  };
70
71
  exports.default = LinkButton;
@@ -30,6 +30,7 @@ const react_1 = __importStar(require("react"));
30
30
  const react_2 = require("@remirror/react");
31
31
  const Button_1 = __importDefault(require("../../../ui/Button/Button"));
32
32
  const LinkOff_1 = __importDefault(require("@mui/icons-material/LinkOff"));
33
+ const getShortcutSymbol_1 = require("../../../utils/getShortcutSymbol");
33
34
  const RemoveLinkButton = ({ inPopover = false }) => {
34
35
  const chain = (0, react_2.useChainedCommands)();
35
36
  const active = (0, react_2.useActive)();
@@ -48,6 +49,6 @@ const RemoveLinkButton = ({ inPopover = false }) => {
48
49
  // disable the shortcut if the button is disabled
49
50
  (0, react_2.useKeymap)('Shift-Mod-k', disabled ? () => true : handleShortcut);
50
51
  }
51
- return (react_1.default.createElement(Button_1.default, { handleOnClick: handleClick, isActive: false, isDisabled: disabled, icon: react_1.default.createElement(LinkOff_1.default, null), label: "Remove link (shift+cmd+K)" }));
52
+ return (react_1.default.createElement(Button_1.default, { handleOnClick: handleClick, isActive: false, isDisabled: disabled, icon: react_1.default.createElement(LinkOff_1.default, null), label: `Remove link (Shift+${(0, getShortcutSymbol_1.getShortcutSymbol)()}+K)` }));
52
53
  };
53
54
  exports.default = RemoveLinkButton;
@@ -7,6 +7,7 @@ const react_1 = __importDefault(require("react"));
7
7
  const react_2 = require("@remirror/react");
8
8
  const Button_1 = __importDefault(require("../../../ui/Button/Button"));
9
9
  const RedoRounded_1 = __importDefault(require("@mui/icons-material/RedoRounded"));
10
+ const getShortcutSymbol_1 = require("../../../utils/getShortcutSymbol");
10
11
  const RedoButton = () => {
11
12
  const { redo } = (0, react_2.useCommands)();
12
13
  const { redoDepth } = (0, react_2.useHelpers)(true);
@@ -16,6 +17,6 @@ const RedoButton = () => {
16
17
  }
17
18
  };
18
19
  const enabled = redoDepth() > 0;
19
- return (react_1.default.createElement(Button_1.default, { handleOnClick: handleSelect, isDisabled: !enabled, isActive: false, icon: react_1.default.createElement(RedoRounded_1.default, null), label: "Redo (shift+cmd+Z)" }));
20
+ return (react_1.default.createElement(Button_1.default, { handleOnClick: handleSelect, isDisabled: !enabled, isActive: false, icon: react_1.default.createElement(RedoRounded_1.default, null), label: `Redo (Shift+${(0, getShortcutSymbol_1.getShortcutSymbol)()}+Z)` }));
20
21
  };
21
22
  exports.default = RedoButton;
@@ -7,6 +7,7 @@ const react_1 = __importDefault(require("react"));
7
7
  const react_2 = require("@remirror/react");
8
8
  const Button_1 = __importDefault(require("../../../ui/Button/Button"));
9
9
  const FormatUnderlinedRounded_1 = __importDefault(require("@mui/icons-material/FormatUnderlinedRounded"));
10
+ const getShortcutSymbol_1 = require("../../../utils/getShortcutSymbol");
10
11
  const UnderlineButton = () => {
11
12
  const { toggleUnderline } = (0, react_2.useCommands)();
12
13
  const chain = (0, react_2.useChainedCommands)();
@@ -17,6 +18,6 @@ const UnderlineButton = () => {
17
18
  chain.toggleUnderline().focus().run();
18
19
  }
19
20
  };
20
- return (react_1.default.createElement(Button_1.default, { handleOnClick: handleSelect, isDisabled: !enabled, isActive: active.underline(), icon: react_1.default.createElement(FormatUnderlinedRounded_1.default, null), label: "Underline (cmd+U)" }));
21
+ return (react_1.default.createElement(Button_1.default, { handleOnClick: handleSelect, isDisabled: !enabled, isActive: active.underline(), icon: react_1.default.createElement(FormatUnderlinedRounded_1.default, null), label: `Underline (${(0, getShortcutSymbol_1.getShortcutSymbol)()}+U)` }));
21
22
  };
22
23
  exports.default = UnderlineButton;
@@ -7,6 +7,7 @@ const react_1 = __importDefault(require("react"));
7
7
  const react_2 = require("@remirror/react");
8
8
  const Button_1 = __importDefault(require("../../../ui/Button/Button"));
9
9
  const UndoRounded_1 = __importDefault(require("@mui/icons-material/UndoRounded"));
10
+ const getShortcutSymbol_1 = require("../../../utils/getShortcutSymbol");
10
11
  const UndoButton = () => {
11
12
  const { undo } = (0, react_2.useCommands)();
12
13
  const { undoDepth } = (0, react_2.useHelpers)(true);
@@ -16,6 +17,6 @@ const UndoButton = () => {
16
17
  }
17
18
  };
18
19
  const enabled = undoDepth() > 0;
19
- return (react_1.default.createElement(Button_1.default, { handleOnClick: handleSelect, isDisabled: !enabled, isActive: false, icon: react_1.default.createElement(UndoRounded_1.default, null), label: "Undo (cmd+Z)" }));
20
+ return (react_1.default.createElement(Button_1.default, { handleOnClick: handleSelect, isDisabled: !enabled, isActive: false, icon: react_1.default.createElement(UndoRounded_1.default, null), label: `Undo (${(0, getShortcutSymbol_1.getShortcutSymbol)()}+Z)` }));
20
21
  };
21
22
  exports.default = UndoButton;
@@ -0,0 +1 @@
1
+ export declare const getShortcutSymbol: () => string;
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getShortcutSymbol = void 0;
4
+ const getShortcutSymbol = () => {
5
+ // If we can detect Mac then we return "⌘" otherwise default to "Ctrl"
6
+ return window.navigator.userAgent.toLowerCase().indexOf('mac') > -1 ? '⌘' : 'Ctrl';
7
+ };
8
+ exports.getShortcutSymbol = getShortcutSymbol;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@squiz/formatted-text-editor",
3
- "version": "1.67.1",
3
+ "version": "1.68.1",
4
4
  "main": "lib/index.js",
5
5
  "types": "lib/index.d.ts",
6
6
  "private": false,
@@ -35,7 +35,7 @@
35
35
  "@testing-library/jest-dom": "5.16.5",
36
36
  "@testing-library/react": "14.0.0",
37
37
  "@testing-library/user-event": "14.4.3",
38
- "@types/node": "20.11.24",
38
+ "@types/node": "20.12.4",
39
39
  "@types/react": "^18.2.45",
40
40
  "@types/react-dom": "^18.2.18",
41
41
  "@vitejs/plugin-react": "3.0.0",
@@ -23,17 +23,17 @@ describe('Formatted text editor', () => {
23
23
 
24
24
  it('Renders the bold button', () => {
25
25
  render(<Editor />);
26
- expect(screen.getByRole('button', { name: 'Bold (cmd+B)' })).toBeInTheDocument();
26
+ expect(screen.getByRole('button', { name: 'Bold (Ctrl+B)' })).toBeInTheDocument();
27
27
  });
28
28
 
29
29
  it('Renders the italic button', () => {
30
30
  render(<Editor />);
31
- expect(screen.getByRole('button', { name: 'Italic (cmd+I)' })).toBeInTheDocument();
31
+ expect(screen.getByRole('button', { name: 'Italic (Ctrl+I)' })).toBeInTheDocument();
32
32
  });
33
33
 
34
34
  it('Renders the underline button', () => {
35
35
  render(<Editor />);
36
- expect(screen.getByRole('button', { name: 'Underline (cmd+U)' })).toBeInTheDocument();
36
+ expect(screen.getByRole('button', { name: 'Underline (Ctrl+U)' })).toBeInTheDocument();
37
37
  });
38
38
 
39
39
  it('Renders the align left button', () => {
@@ -296,9 +296,9 @@ describe('Formatted text editor', () => {
296
296
 
297
297
  it('Should not display the toolbar if is not editable', () => {
298
298
  render(<Editor editable={false} />);
299
- expect(screen.queryByRole('button', { name: 'Bold (cmd+B)' })).not.toBeInTheDocument();
300
- expect(screen.queryByRole('button', { name: 'Italic (cmd+I)' })).not.toBeInTheDocument();
301
- expect(screen.queryByRole('button', { name: 'Underline (cmd+U)' })).not.toBeInTheDocument();
299
+ expect(screen.queryByRole('button', { name: 'Bold (Ctrl+B)' })).not.toBeInTheDocument();
300
+ expect(screen.queryByRole('button', { name: 'Italic (Ctrl+I)' })).not.toBeInTheDocument();
301
+ expect(screen.queryByRole('button', { name: 'Underline (Ctrl+U)' })).not.toBeInTheDocument();
302
302
  });
303
303
 
304
304
  it('Should not display the floating toolbar if is not editable', async () => {
@@ -415,7 +415,7 @@ describe('Formatted text editor', () => {
415
415
  </ResourceBrowserContext.Provider>,
416
416
  );
417
417
 
418
- await act(() => fireEvent.click(screen.getByRole('button', { name: 'Link (cmd+K)' })));
418
+ await act(() => fireEvent.click(screen.getByRole('button', { name: 'Link (Ctrl+K)' })));
419
419
  await act(() => fireEvent.click(screen.getByText('Choose asset')));
420
420
  await act(() => fireEvent.click(screen.getByRole('button', { name: 'Drill down to My Matrix instance children' })));
421
421
  await act(() => fireEvent.click(screen.getByRole('button', { name: 'Drill down to Folder 1 children' })));
@@ -426,7 +426,7 @@ describe('Formatted text editor', () => {
426
426
  // Toolbar (Link button) should be mounted.
427
427
  // Toolbar should be shown (.show-toolbar class).
428
428
  expect(screen.getByRole('button', { name: /My image/ })).toBeInTheDocument();
429
- expect(within(document.body).getByRole('button', { name: 'Link (cmd+K)', hidden: true })).toBeInTheDocument();
429
+ expect(within(document.body).getByRole('button', { name: 'Link (Ctrl+K)', hidden: true })).toBeInTheDocument();
430
430
  expect(document.querySelector('.show-toolbar')).toBeInTheDocument();
431
431
  });
432
432
  });
@@ -25,7 +25,12 @@ const WrappedEditor = () => {
25
25
  const preventImagePaste = useCallback((event) => {
26
26
  const { clipboardData } = event;
27
27
  const pastedData = clipboardData?.files[0];
28
- if (pastedData?.type && pastedData?.type.startsWith('image/')) {
28
+ if (
29
+ pastedData?.type &&
30
+ pastedData?.type.startsWith('image/') &&
31
+ // Still allow paste of any text that came through (Word, etc)
32
+ !clipboardData?.types.includes('text/plain')
33
+ ) {
29
34
  event.preventDefault();
30
35
  }
31
36
 
@@ -9,11 +9,11 @@ describe('FloatingToolbar', () => {
9
9
 
10
10
  it.each([
11
11
  ['Nothing selected', 3, 3, []],
12
- ['Regular text selected', 3, 4, ['Bold (cmd+B)', 'Italic (cmd+I)', 'Underline (cmd+U)', 'Link (cmd+K)']],
13
- ['Regular text + link selected', 3, 17, ['Bold (cmd+B)', 'Italic (cmd+I)', 'Underline (cmd+U)']],
12
+ ['Regular text selected', 3, 4, ['Bold (Ctrl+B)', 'Italic (Ctrl+I)', 'Underline (Ctrl+U)', 'Link (Ctrl+K)']],
13
+ ['Regular text + link selected', 3, 17, ['Bold (Ctrl+B)', 'Italic (Ctrl+I)', 'Underline (Ctrl+U)']],
14
14
  ['Nothing selected, positioned directly on the left of a link', 12, 12, []],
15
15
  ['Nothing selected, positioned directly on the right of a link', 19, 19, []],
16
- ['Nothing selected, positioned within a link', 13, 13, ['Link (cmd+K)', 'Remove link (shift+cmd+K)']],
16
+ ['Nothing selected, positioned within a link', 13, 13, ['Link (Ctrl+K)', 'Remove link (Shift+Ctrl+K)']],
17
17
  ])(
18
18
  'Renders formatting buttons when text is selected - %s',
19
19
  async (description: string, from: number, to: number, expectedButtons: string[]) => {
@@ -29,8 +29,8 @@ describe('FloatingToolbar', () => {
29
29
  );
30
30
 
31
31
  it.each([
32
- ['Image selected', 1, ['Image (cmd+L)']],
33
- ['Asset image selected', 2, ['Image (cmd+L)']],
32
+ ['Image selected', 1, ['Image (Ctrl+L)']],
33
+ ['Asset image selected', 2, ['Image (Ctrl+L)']],
34
34
  ])(
35
35
  'Renders formatting buttons when node is selected - %s',
36
36
  async (description: string, pos: number, expectedButtons: string[]) => {
@@ -6,13 +6,13 @@ import Editor from '../../../Editor/Editor';
6
6
  describe('Bold button', () => {
7
7
  it('Renders the bold button', () => {
8
8
  render(<Editor />);
9
- expect(screen.getByRole('button', { name: 'Bold (cmd+B)' })).toBeInTheDocument();
9
+ expect(screen.getByRole('button', { name: 'Bold (Ctrl+B)' })).toBeInTheDocument();
10
10
  });
11
11
 
12
12
  it('Activates the button if clicked', () => {
13
13
  render(<Editor />);
14
- expect(screen.getByRole('button', { name: 'Bold (cmd+B)' }).classList.contains('squiz-fte-btn')).toBeTruthy();
15
- const bold = screen.getByRole('button', { name: 'Bold (cmd+B)' });
14
+ expect(screen.getByRole('button', { name: 'Bold (Ctrl+B)' }).classList.contains('squiz-fte-btn')).toBeTruthy();
15
+ const bold = screen.getByRole('button', { name: 'Bold (Ctrl+B)' });
16
16
  fireEvent.click(bold);
17
17
  expect(bold.classList.contains('squiz-fte-btn--is-active')).toBeTruthy();
18
18
  });
@@ -3,6 +3,7 @@ import { useCommands, useActive, useChainedCommands } from '@remirror/react';
3
3
  import { BoldExtension } from '@remirror/extension-bold';
4
4
  import Button from '../../../ui/Button/Button';
5
5
  import FormatBoldRoundedIcon from '@mui/icons-material/FormatBoldRounded';
6
+ import { getShortcutSymbol } from '../../../utils/getShortcutSymbol';
6
7
 
7
8
  const BoldButton = () => {
8
9
  const { toggleBold } = useCommands();
@@ -22,7 +23,7 @@ const BoldButton = () => {
22
23
  isDisabled={!enabled}
23
24
  isActive={active.bold()}
24
25
  icon={<FormatBoldRoundedIcon />}
25
- label="Bold (cmd+B)"
26
+ label={`Bold (${getShortcutSymbol()}+B)`}
26
27
  />
27
28
  );
28
29
  };
@@ -7,7 +7,7 @@ import ClearFormattingButton from './ClearFormattingButton';
7
7
  describe('Clear formatting button', () => {
8
8
  it('Renders the clear formatting button', async () => {
9
9
  await renderWithEditor(<ClearFormattingButton />, { content: 'Some nonsense content here' });
10
- expect(screen.getByRole('button', { name: 'Clear all formatting (cmd+\\)' })).toBeInTheDocument();
10
+ expect(screen.getByRole('button', { name: 'Clear all formatting (Ctrl+\\)' })).toBeInTheDocument();
11
11
  });
12
12
 
13
13
  it('Clears the formatting from editor content after clicking button', async () => {
@@ -15,7 +15,7 @@ describe('Clear formatting button', () => {
15
15
  content: '<p>Hello <strong>Mr Bean</strong></p>',
16
16
  });
17
17
 
18
- const clearFormatting = screen.getByRole('button', { name: 'Clear all formatting (cmd+\\)' });
18
+ const clearFormatting = screen.getByRole('button', { name: 'Clear all formatting (Ctrl+\\)' });
19
19
  fireEvent.click(clearFormatting);
20
20
 
21
21
  expect(getHtmlContent()).toBe('<p style="">Hello Mr Bean</p>');
@@ -4,6 +4,7 @@ import { VerticalDivider } from '@remirror/react-components';
4
4
  import FormatClearRoundedIcon from '@mui/icons-material/FormatClearRounded';
5
5
  import { ClearFormattingExtension } from '../../../Extensions/ClearFormattingExtension/ClearFormattingExtension';
6
6
  import Button from '../../../ui/Button/Button';
7
+ import { getShortcutSymbol } from '../../../utils/getShortcutSymbol';
7
8
 
8
9
  const ClearFormattingButton = () => {
9
10
  const { clearFormatting } = useCommands<ClearFormattingExtension>();
@@ -36,7 +37,9 @@ const ClearFormattingButton = () => {
36
37
  isDisabled={false}
37
38
  isActive={false}
38
39
  icon={<FormatClearRoundedIcon />}
39
- label={`${contentSelected ? 'Clear formatting from selection' : 'Clear all formatting'} (cmd+\\)`}
40
+ label={`${
41
+ contentSelected ? 'Clear formatting from selection' : 'Clear all formatting'
42
+ } (${getShortcutSymbol()}+\\)`}
40
43
  />
41
44
  </>
42
45
  );
@@ -10,7 +10,7 @@ jest.mock('react-image-size');
10
10
 
11
11
  describe('ImageButton', () => {
12
12
  const openModal = async () => {
13
- fireEvent.click(screen.getByRole('button', { name: 'Image (cmd+L)' }));
13
+ fireEvent.click(screen.getByRole('button', { name: 'Image (Ctrl+L)' }));
14
14
  await screen.findByRole('button', { name: 'Apply' });
15
15
  fireEvent.click(screen.getByRole('button', { name: 'From URL' }));
16
16
  };
@@ -8,6 +8,7 @@ import { ImageExtension } from '../../../Extensions/ImageExtension/ImageExtensio
8
8
  import { NodeName } from '../../../Extensions/Extensions';
9
9
  import { AssetImageExtension } from '../../../Extensions/ImageExtension/AssetImageExtension';
10
10
  import { CodeBlockExtension } from 'remirror/dist-types/extensions';
11
+ import { getShortcutSymbol } from '../../../utils/getShortcutSymbol';
11
12
 
12
13
  type ImageButtonProps = {
13
14
  inPopover?: boolean;
@@ -60,7 +61,7 @@ const ImageButton = ({ inPopover = false }: ImageButtonProps) => {
60
61
  handleOnClick={handleClick}
61
62
  isActive={active.image() || active.assetImage()}
62
63
  icon={<ImageRoundedIcon />}
63
- label="Image (cmd+L)"
64
+ label={`Image (${getShortcutSymbol()}+L)`}
64
65
  isDisabled={disabled}
65
66
  />
66
67
  {showModal && <ImageModal onCancel={() => setShowModal(false)} onSubmit={handleSubmit} />}
@@ -6,13 +6,13 @@ import Editor from '../../../Editor/Editor';
6
6
  describe('Italic button', () => {
7
7
  it('Renders the italic button', () => {
8
8
  render(<Editor />);
9
- expect(screen.getByRole('button', { name: 'Italic (cmd+I)' })).toBeInTheDocument();
9
+ expect(screen.getByRole('button', { name: 'Italic (Ctrl+I)' })).toBeInTheDocument();
10
10
  });
11
11
 
12
12
  it('Activates the button if clicked', () => {
13
13
  render(<Editor />);
14
- expect(screen.getByRole('button', { name: 'Italic (cmd+I)' }).classList.contains('squiz-fte-btn')).toBeTruthy();
15
- const italic = screen.getByRole('button', { name: 'Italic (cmd+I)' });
14
+ expect(screen.getByRole('button', { name: 'Italic (Ctrl+I)' }).classList.contains('squiz-fte-btn')).toBeTruthy();
15
+ const italic = screen.getByRole('button', { name: 'Italic (Ctrl+I)' });
16
16
  fireEvent.click(italic);
17
17
  expect(italic.classList.contains('squiz-fte-btn--is-active')).toBeTruthy();
18
18
  });
@@ -3,6 +3,7 @@ import { useCommands, useActive, useChainedCommands } from '@remirror/react';
3
3
  import { ItalicExtension } from '@remirror/extension-italic';
4
4
  import Button from '../../../ui/Button/Button';
5
5
  import FormatItalicRoundedIcon from '@mui/icons-material/FormatItalicRounded';
6
+ import { getShortcutSymbol } from '../../../utils/getShortcutSymbol';
6
7
 
7
8
  const ItalicButton = () => {
8
9
  const { toggleItalic } = useCommands();
@@ -22,7 +23,7 @@ const ItalicButton = () => {
22
23
  isDisabled={!enabled}
23
24
  isActive={active.italic()}
24
25
  icon={<FormatItalicRoundedIcon />}
25
- label="Italic (cmd+I)"
26
+ label={`Italic (${getShortcutSymbol()}+I)`}
26
27
  />
27
28
  );
28
29
  };
@@ -6,7 +6,7 @@ import LinkButton from './LinkButton';
6
6
 
7
7
  describe('LinkButton', () => {
8
8
  const openModal = async () => {
9
- fireEvent.click(screen.getByRole('button', { name: 'Link (cmd+K)' }));
9
+ fireEvent.click(screen.getByRole('button', { name: 'Link (Ctrl+K)' }));
10
10
  await screen.findByRole('button', { name: 'Apply' });
11
11
  fireEvent.click(screen.getByRole('button', { name: 'From URL' }));
12
12
  };
@@ -10,6 +10,7 @@ import { AssetLinkExtension } from '../../../Extensions/LinkExtension/AssetLinkE
10
10
  import { MarkName } from '../../../Extensions/Extensions';
11
11
  import { ImageExtension } from '../../../Extensions/ImageExtension/ImageExtension';
12
12
  import { CodeBlockExtension } from 'remirror/dist-types/extensions';
13
+ import { getShortcutSymbol } from '../../../utils/getShortcutSymbol';
13
14
 
14
15
  export type LinkButtonProps = {
15
16
  inPopover?: boolean;
@@ -55,7 +56,7 @@ const LinkButton = ({ inPopover = false }: LinkButtonProps) => {
55
56
  handleOnClick={handleClick}
56
57
  isActive={active.link() || active.assetLink()}
57
58
  icon={<InsertLinkRoundedIcon />}
58
- label="Link (cmd+K)"
59
+ label={`Link (${getShortcutSymbol()}+K)`}
59
60
  isDisabled={disabled}
60
61
  />
61
62
  {showModal && <LinkModal onCancel={() => setShowModal(false)} onSubmit={handleSubmit} />}
@@ -40,7 +40,7 @@ describe('RemoveLinkButton', () => {
40
40
  await act(() => editor.selectText('all'));
41
41
 
42
42
  // remove the links.
43
- fireEvent.click(screen.getByRole('button', { name: 'Remove link (shift+cmd+K)' }));
43
+ fireEvent.click(screen.getByRole('button', { name: 'Remove link (Shift+Ctrl+K)' }));
44
44
 
45
45
  // make sure both types of link have been removed.
46
46
  expect(getJsonContent()).toEqual({
@@ -101,11 +101,11 @@ describe('RemoveLinkButton', () => {
101
101
  });
102
102
 
103
103
  // expect remove button to be disabled
104
- expect(screen.getByRole('button', { name: 'Remove link (shift+cmd+K)' })).toBeDisabled();
104
+ expect(screen.getByRole('button', { name: 'Remove link (Shift+Ctrl+K)' })).toBeDisabled();
105
105
  // jump to the middle of the link.
106
106
  await act(() => editor.selectText(3));
107
107
  // expect remove button to be enabled
108
- expect(screen.getByRole('button', { name: 'Remove link (shift+cmd+K)' })).not.toBeDisabled();
108
+ expect(screen.getByRole('button', { name: 'Remove link (Shift+Ctrl+K)' })).not.toBeDisabled();
109
109
  });
110
110
 
111
111
  it('Enables the Remove link button when asset link text is selected', async () => {
@@ -134,10 +134,10 @@ describe('RemoveLinkButton', () => {
134
134
  });
135
135
 
136
136
  // expect remove button to be disabled
137
- expect(screen.getByRole('button', { name: 'Remove link (shift+cmd+K)' })).toBeDisabled();
137
+ expect(screen.getByRole('button', { name: 'Remove link (Shift+Ctrl+K)' })).toBeDisabled();
138
138
  // jump to the middle of the link.
139
139
  await act(() => editor.selectText(3));
140
140
  // expect remove button to be enabled
141
- expect(screen.getByRole('button', { name: 'Remove link (shift+cmd+K)' })).not.toBeDisabled();
141
+ expect(screen.getByRole('button', { name: 'Remove link (Shift+Ctrl+K)' })).not.toBeDisabled();
142
142
  });
143
143
  });
@@ -5,6 +5,7 @@ import LinkOffIcon from '@mui/icons-material/LinkOff';
5
5
  import { AssetLinkExtension } from '../../../Extensions/LinkExtension/AssetLinkExtension';
6
6
  import { LinkExtension } from '../../../Extensions/LinkExtension/LinkExtension';
7
7
  import { LinkButtonProps } from './LinkButton';
8
+ import { getShortcutSymbol } from '../../../utils/getShortcutSymbol';
8
9
 
9
10
  const RemoveLinkButton = ({ inPopover = false }: LinkButtonProps) => {
10
11
  const chain = useChainedCommands();
@@ -33,7 +34,7 @@ const RemoveLinkButton = ({ inPopover = false }: LinkButtonProps) => {
33
34
  isActive={false}
34
35
  isDisabled={disabled}
35
36
  icon={<LinkOffIcon />}
36
- label="Remove link (shift+cmd+K)"
37
+ label={`Remove link (Shift+${getShortcutSymbol()}+K)`}
37
38
  />
38
39
  );
39
40
  };
@@ -6,12 +6,12 @@ import React from 'react';
6
6
  describe('Redo button', () => {
7
7
  it('Renders the redo button', () => {
8
8
  render(<Editor />);
9
- expect(screen.getByRole('button', { name: 'Redo (shift+cmd+Z)' })).toBeInTheDocument();
9
+ expect(screen.getByRole('button', { name: 'Redo (Shift+Ctrl+Z)' })).toBeInTheDocument();
10
10
  });
11
11
 
12
12
  it('Renders a disabled button if you have not made any changes yet', () => {
13
13
  render(<Editor />);
14
- const redo = screen.getByRole('button', { name: 'Redo (shift+cmd+Z)' });
14
+ const redo = screen.getByRole('button', { name: 'Redo (Shift+Ctrl+Z)' });
15
15
  expect(redo).toBeDisabled();
16
16
  });
17
17
 
@@ -25,12 +25,12 @@ describe('Redo button', () => {
25
25
  expect(baseElement.querySelector('p[data-node-text-align="left"]')).toBeTruthy();
26
26
 
27
27
  // Revert this action
28
- const undo = screen.getByRole('button', { name: 'Undo (cmd+Z)' });
28
+ const undo = screen.getByRole('button', { name: 'Undo (Ctrl+Z)' });
29
29
  fireEvent.click(undo);
30
30
  expect(baseElement.querySelector('p[data-node-text-align="left"]')).toBeFalsy();
31
31
 
32
32
  // Check that this enables the redo button
33
- const redo = screen.getByRole('button', { name: 'Redo (shift+cmd+Z)' });
33
+ const redo = screen.getByRole('button', { name: 'Redo (Shift+Ctrl+Z)' });
34
34
  expect(redo).not.toBeDisabled();
35
35
  });
36
36
 
@@ -44,12 +44,12 @@ describe('Redo button', () => {
44
44
  expect(baseElement.querySelector('p[data-node-text-align="left"]')).toBeTruthy();
45
45
 
46
46
  // Revert this action
47
- const undo = screen.getByRole('button', { name: 'Undo (cmd+Z)' });
47
+ const undo = screen.getByRole('button', { name: 'Undo (Ctrl+Z)' });
48
48
  fireEvent.click(undo);
49
49
  expect(baseElement.querySelector('p[data-node-text-align="left"]')).toBeFalsy();
50
50
 
51
51
  // Check that this enables the redo button
52
- const redo = screen.getByRole('button', { name: 'Redo (shift+cmd+Z)' });
52
+ const redo = screen.getByRole('button', { name: 'Redo (Shift+Ctrl+Z)' });
53
53
  expect(redo).not.toBeDisabled();
54
54
 
55
55
  // Click the redo button and check that this has reverted the previous action
@@ -3,6 +3,7 @@ import { useCommands, useHelpers } from '@remirror/react';
3
3
  import { HistoryExtension } from 'remirror/extensions';
4
4
  import Button from '../../../ui/Button/Button';
5
5
  import RedoRoundedIcon from '@mui/icons-material/RedoRounded';
6
+ import { getShortcutSymbol } from '../../../utils/getShortcutSymbol';
6
7
 
7
8
  const RedoButton = () => {
8
9
  const { redo } = useCommands<HistoryExtension>();
@@ -22,7 +23,7 @@ const RedoButton = () => {
22
23
  isDisabled={!enabled}
23
24
  isActive={false}
24
25
  icon={<RedoRoundedIcon />}
25
- label="Redo (shift+cmd+Z)"
26
+ label={`Redo (Shift+${getShortcutSymbol()}+Z)`}
26
27
  />
27
28
  );
28
29
  };
@@ -6,13 +6,13 @@ import Editor from '../../../Editor/Editor';
6
6
  describe('Underline button', () => {
7
7
  it('Renders the underline button', () => {
8
8
  render(<Editor />);
9
- expect(screen.getByRole('button', { name: 'Underline (cmd+U)' })).toBeInTheDocument();
9
+ expect(screen.getByRole('button', { name: 'Underline (Ctrl+U)' })).toBeInTheDocument();
10
10
  });
11
11
 
12
12
  it('Activates the button if clicked', () => {
13
13
  render(<Editor />);
14
- expect(screen.getByRole('button', { name: 'Underline (cmd+U)' }).classList.contains('squiz-fte-btn')).toBeTruthy();
15
- const underline = screen.getByRole('button', { name: 'Underline (cmd+U)' });
14
+ expect(screen.getByRole('button', { name: 'Underline (Ctrl+U)' }).classList.contains('squiz-fte-btn')).toBeTruthy();
15
+ const underline = screen.getByRole('button', { name: 'Underline (Ctrl+U)' });
16
16
  fireEvent.click(underline);
17
17
  expect(underline.classList.contains('squiz-fte-btn--is-active')).toBeTruthy();
18
18
  });
@@ -3,6 +3,7 @@ import { useCommands, useActive, useChainedCommands } from '@remirror/react';
3
3
  import { UnderlineExtension } from '@remirror/extension-underline';
4
4
  import Button from '../../../ui/Button/Button';
5
5
  import FormatUnderlinedRoundedIcon from '@mui/icons-material/FormatUnderlinedRounded';
6
+ import { getShortcutSymbol } from '../../../utils/getShortcutSymbol';
6
7
 
7
8
  const UnderlineButton = () => {
8
9
  const { toggleUnderline } = useCommands();
@@ -22,7 +23,7 @@ const UnderlineButton = () => {
22
23
  isDisabled={!enabled}
23
24
  isActive={active.underline()}
24
25
  icon={<FormatUnderlinedRoundedIcon />}
25
- label="Underline (cmd+U)"
26
+ label={`Underline (${getShortcutSymbol()}+U)`}
26
27
  />
27
28
  );
28
29
  };
@@ -8,12 +8,12 @@ import UndoButton from './UndoButton';
8
8
  describe('Undo button', () => {
9
9
  it('Renders the undo button', () => {
10
10
  render(<Editor />);
11
- expect(screen.getByRole('button', { name: 'Undo (cmd+Z)' })).toBeInTheDocument();
11
+ expect(screen.getByRole('button', { name: 'Undo (Ctrl+Z)' })).toBeInTheDocument();
12
12
  });
13
13
 
14
14
  it('Renders a disabled button if you have not made any changes yet', () => {
15
15
  render(<Editor />);
16
- const undo = screen.getByRole('button', { name: 'Undo (cmd+Z)' });
16
+ const undo = screen.getByRole('button', { name: 'Undo (Ctrl+Z)' });
17
17
  expect(undo).toBeDisabled();
18
18
  });
19
19
 
@@ -26,7 +26,7 @@ describe('Undo button', () => {
26
26
 
27
27
  fireEvent.click(leftAlignButton);
28
28
  expect(baseElement.querySelector('p[data-node-text-align="left"]')).toBeTruthy();
29
- const undo = screen.getByRole('button', { name: 'Undo (cmd+Z)' });
29
+ const undo = screen.getByRole('button', { name: 'Undo (Ctrl+Z)' });
30
30
  expect(undo).not.toBeDisabled();
31
31
  });
32
32
 
@@ -41,7 +41,7 @@ describe('Undo button', () => {
41
41
  expect(baseElement.querySelector('p[data-node-text-align="left"]')).toBeTruthy();
42
42
 
43
43
  // Check that this enables the undo button
44
- const undo = screen.getByRole('button', { name: 'Undo (cmd+Z)' });
44
+ const undo = screen.getByRole('button', { name: 'Undo (Ctrl+Z)' });
45
45
  expect(undo).not.toBeDisabled();
46
46
 
47
47
  // Click the undo button and check that this has reverted the previous action
@@ -60,7 +60,7 @@ describe('Undo button', () => {
60
60
  content: [{ type: 'text', text: 'Initial content... with some updated content.' }],
61
61
  });
62
62
 
63
- fireEvent.click(screen.getByRole('button', { name: 'Undo (cmd+Z)' }));
63
+ fireEvent.click(screen.getByRole('button', { name: 'Undo (Ctrl+Z)' }));
64
64
  expect(getJsonContent()).toEqual({
65
65
  type: 'paragraph',
66
66
  attrs: expect.any(Object),
@@ -3,6 +3,7 @@ import { useCommands, useHelpers } from '@remirror/react';
3
3
  import { HistoryExtension } from 'remirror/extensions';
4
4
  import Button from '../../../ui/Button/Button';
5
5
  import UndoRoundedIcon from '@mui/icons-material/UndoRounded';
6
+ import { getShortcutSymbol } from '../../../utils/getShortcutSymbol';
6
7
 
7
8
  const UndoButton = () => {
8
9
  const { undo } = useCommands<HistoryExtension>();
@@ -22,7 +23,7 @@ const UndoButton = () => {
22
23
  isDisabled={!enabled}
23
24
  isActive={false}
24
25
  icon={<UndoRoundedIcon />}
25
- label="Undo (cmd+Z)"
26
+ label={`Undo (${getShortcutSymbol()}+Z)`}
26
27
  />
27
28
  );
28
29
  };
@@ -0,0 +1,27 @@
1
+ import { getShortcutSymbol } from './getShortcutSymbol';
2
+
3
+ describe('getShortcutSymbol', () => {
4
+ it('Returns Mac command icon if user-agent is a Mac based user-agent', () => {
5
+ Object.defineProperty(window, `navigator`, {
6
+ value: {
7
+ userAgent:
8
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_2) AppleWebKit/601.3.9 (KHTML, like Gecko) Version/9.0.2 Safari/601.3.9',
9
+ },
10
+ writable: true,
11
+ });
12
+
13
+ expect(getShortcutSymbol()).toBe('⌘');
14
+ });
15
+
16
+ it('Returns Ctrl shortcut if user-agent is not a Mac based user-agent', () => {
17
+ Object.defineProperty(window, `navigator`, {
18
+ value: {
19
+ userAgent:
20
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.246',
21
+ },
22
+ writable: true,
23
+ });
24
+
25
+ expect(getShortcutSymbol()).toBe('Ctrl');
26
+ });
27
+ });
@@ -0,0 +1,4 @@
1
+ export const getShortcutSymbol = (): string => {
2
+ // If we can detect Mac then we return "⌘" otherwise default to "Ctrl"
3
+ return window.navigator.userAgent.toLowerCase().indexOf('mac') > -1 ? '⌘' : 'Ctrl';
4
+ };