diff --git a/packages/remirror__core-utils/src/command-utils.ts b/packages/remirror__core-utils/src/command-utils.ts index 4d16092a76..5543e43d3a 100644 --- a/packages/remirror__core-utils/src/command-utils.ts +++ b/packages/remirror__core-utils/src/command-utils.ts @@ -308,6 +308,21 @@ export function isChrome(minVersion = 0): boolean { return parsedAgent ? Number.parseInt(assertGet(parsedAgent, 2), 10) >= minVersion : false; } +/** + * Taken from https://stackoverflow.com/a/4900484 + * + * Check that the browser is safari. Supports passing a minimum version to check + * that it is a greater than or equal to this version. + */ +export function isSafari(minVersion = 0): boolean { + const isMac = navigator.userAgent.match(/Mac/); + const parsedAgent = navigator.userAgent.match(/Safari\/(\d+)\./); + + return isMac && !isChrome() && parsedAgent + ? Number.parseInt(assertGet(parsedAgent, 2), 10) >= minVersion + : false; +} + /** * Checks the selection for the current state and updates the active transaction * to a selection that is consistent with the initial selection. diff --git a/packages/remirror__core-utils/src/index.ts b/packages/remirror__core-utils/src/index.ts index 05d55dee51..2368c92513 100644 --- a/packages/remirror__core-utils/src/index.ts +++ b/packages/remirror__core-utils/src/index.ts @@ -6,6 +6,7 @@ export type { } from './command-utils'; export { isChrome, + isSafari, lift, preserveSelection, removeMark, diff --git a/packages/remirror__core/src/builtins/keymap-extension.ts b/packages/remirror__core/src/builtins/keymap-extension.ts index 3eb0996c17..519a8f92ee 100644 --- a/packages/remirror__core/src/builtins/keymap-extension.ts +++ b/packages/remirror__core/src/builtins/keymap-extension.ts @@ -603,8 +603,8 @@ export type ShortcutMap = Record; export const DEFAULT_SHORTCUTS: ShortcutMap = { [NamedShortcut.Copy]: 'Mod-c', [NamedShortcut.Cut]: 'Mod-x', - [NamedShortcut.Paste]: 'Mod-p', - [NamedShortcut.PastePlain]: 'Mod-Shift-p', + [NamedShortcut.Paste]: 'Mod-v', + [NamedShortcut.PastePlain]: 'Mod-Shift-v', [NamedShortcut.SelectAll]: 'Mod-a', [NamedShortcut.Undo]: 'Mod-z', [NamedShortcut.Redo]: environment.isMac ? 'Shift-Mod-z' : 'Mod-y', diff --git a/packages/remirror__extension-callout/src/callout-extension.ts b/packages/remirror__extension-callout/src/callout-extension.ts index 2d55668acb..5feae419a8 100644 --- a/packages/remirror__extension-callout/src/callout-extension.ts +++ b/packages/remirror__extension-callout/src/callout-extension.ts @@ -25,7 +25,7 @@ import { TextSelection } from '@remirror/pm/state'; import { EditorView } from '@remirror/pm/view'; import { ExtensionCalloutTheme } from '@remirror/theme'; -import type { CalloutAttributes, CalloutOptions } from './callout-types'; +import type { CalloutExtensionAttributes, CalloutOptions } from './callout-types'; import { dataAttributeEmoji, dataAttributeType, @@ -164,7 +164,7 @@ export class CalloutExtension extends NodeExtension { * ``` */ @command(toggleCalloutOptions) - toggleCallout(attributes: CalloutAttributes = {}): CommandFunction { + toggleCallout(attributes: CalloutExtensionAttributes = {}): CommandFunction { return toggleWrap(this.type, attributes); } @@ -179,7 +179,7 @@ export class CalloutExtension extends NodeExtension { * ``` */ @command(toggleCalloutOptions) - updateCallout(attributes: CalloutAttributes, pos?: number): CommandFunction { + updateCallout(attributes: CalloutExtensionAttributes, pos?: number): CommandFunction { return updateNodeAttributes(this.type)(attributes, pos); } diff --git a/packages/remirror__extension-callout/src/callout-types.ts b/packages/remirror__extension-callout/src/callout-types.ts index 105f4e45ed..2f9a724352 100644 --- a/packages/remirror__extension-callout/src/callout-types.ts +++ b/packages/remirror__extension-callout/src/callout-types.ts @@ -35,7 +35,7 @@ export interface CalloutOptions { renderEmoji?: (node: ProsemirrorNode, view: EditorView, getPos: () => number) => HTMLElement; } -export interface CalloutAttributes extends ProsemirrorAttributes { +export interface CalloutExtensionAttributes extends ProsemirrorAttributes { /** * The type of callout, for instance `info`, `warning`, `error`, `success` or `blank`. * diff --git a/packages/remirror__extension-callout/src/callout-utils.ts b/packages/remirror__extension-callout/src/callout-utils.ts index 6d9667680a..8242f97cb0 100644 --- a/packages/remirror__extension-callout/src/callout-utils.ts +++ b/packages/remirror__extension-callout/src/callout-utils.ts @@ -11,7 +11,7 @@ import { } from '@remirror/core'; import { ExtensionCalloutMessages } from '@remirror/messages'; -import type { CalloutAttributes } from './callout-types'; +import type { CalloutExtensionAttributes } from './callout-types'; export const dataAttributeType = 'data-callout-type'; @@ -21,9 +21,9 @@ export const dataAttributeEmoji = 'data-callout-emoji'; * Check that the attributes exist and are valid for the codeBlock * updateAttributes. */ -export function isValidCalloutAttributes( +export function isValidCalloutExtensionAttributes( attributes: ProsemirrorAttributes, -): attributes is CalloutAttributes { +): attributes is CalloutExtensionAttributes { if (attributes && isObject(attributes)) { const attributesChecklist = Object.entries(attributes).map(([key, value]) => { switch (key) { @@ -49,9 +49,9 @@ export function isValidCalloutAttributes( * This is used to update the type of the callout. */ export function updateNodeAttributes(type: NodeType) { - return (attributes: CalloutAttributes, pos?: number): CommandFunction => + return (attributes: CalloutExtensionAttributes, pos?: number): CommandFunction => ({ state: { tr, selection, doc }, dispatch }) => { - if (!isValidCalloutAttributes(attributes)) { + if (!isValidCalloutExtensionAttributes(attributes)) { throw new Error('Invalid attrs passed to the updateAttributes method'); } @@ -79,7 +79,7 @@ const { DESCRIPTION, LABEL } = ExtensionCalloutMessages; export const toggleCalloutOptions: Remirror.CommandDecoratorOptions = { icon: ({ attrs }) => { - switch (attrs?.type as CalloutAttributes['type']) { + switch (attrs?.type as CalloutExtensionAttributes['type']) { case 'error': return 'closeCircleLine'; case 'success': diff --git a/packages/remirror__extension-callout/src/index.ts b/packages/remirror__extension-callout/src/index.ts index 5974707dbf..2cb27ab71f 100644 --- a/packages/remirror__extension-callout/src/index.ts +++ b/packages/remirror__extension-callout/src/index.ts @@ -1 +1,2 @@ export * from './callout-extension'; +export type { CalloutExtensionAttributes, CalloutOptions } from './callout-types'; diff --git a/packages/remirror__extension-code-block/src/code-block-extension.ts b/packages/remirror__extension-code-block/src/code-block-extension.ts index 42a45a583a..de0fdba38b 100644 --- a/packages/remirror__extension-code-block/src/code-block-extension.ts +++ b/packages/remirror__extension-code-block/src/code-block-extension.ts @@ -255,7 +255,7 @@ export class CodeBlockExtension extends NodeExtension { * remove the code block altogether. */ @command(toggleCodeBlockOptions) - toggleCodeBlock(attributes: Partial): CommandFunction { + toggleCodeBlock(attributes: Partial = {}): CommandFunction { return toggleBlockItem({ type: this.type, toggleType: this.options.toggleName, diff --git a/packages/remirror__extension-code-block/src/code-block-utils.ts b/packages/remirror__extension-code-block/src/code-block-utils.ts index 3ebe69d2d2..ec5f621b7d 100644 --- a/packages/remirror__extension-code-block/src/code-block-utils.ts +++ b/packages/remirror__extension-code-block/src/code-block-utils.ts @@ -22,7 +22,7 @@ import { range, TextProps, } from '@remirror/core'; -import { ExtensionCodeMessages } from '@remirror/messages'; +import { ExtensionCodeBlockMessages } from '@remirror/messages'; import { TextSelection } from '@remirror/pm/state'; import { Decoration } from '@remirror/pm/view'; @@ -343,7 +343,7 @@ export function getLanguageFromDom(codeElement: HTMLElement): string | undefined ); } -const { DESCRIPTION, LABEL } = ExtensionCodeMessages; +const { DESCRIPTION, LABEL } = ExtensionCodeBlockMessages; export const toggleCodeBlockOptions: Remirror.CommandDecoratorOptions = { icon: 'bracesLine', description: ({ t }) => t(DESCRIPTION), diff --git a/packages/remirror__extension-history/src/history-extension.ts b/packages/remirror__extension-history/src/history-extension.ts index 74877e674f..513b9a37df 100644 --- a/packages/remirror__extension-history/src/history-extension.ts +++ b/packages/remirror__extension-history/src/history-extension.ts @@ -7,6 +7,8 @@ import { environment, extension, Handler, + Helper, + helper, isFunction, keyBinding, KeyBindingProps, @@ -20,7 +22,7 @@ import { Static, } from '@remirror/core'; import { ExtensionHistoryMessages as Messages } from '@remirror/messages'; -import { history, redo, undo } from '@remirror/pm/history'; +import { history, redo, redoDepth, undo, undoDepth } from '@remirror/pm/history'; export interface HistoryOptions { /** @@ -197,6 +199,22 @@ export class HistoryExtension extends PlainExtension { redo(): NonChainableCommandFunction { return nonChainable(this.wrapMethod(redo, this.options.onRedo)); } + + /** + * Returns the amount of undoable events available from the current state, or provide a custom state. + */ + @helper() + undoDepth(state: EditorState = this.store.getState()): Helper { + return undoDepth(state); + } + + /** + * Returns the amount of redoable events available from the current state, or provide a custom state. + */ + @helper() + redoDepth(state: EditorState = this.store.getState()): Helper { + return redoDepth(state); + } } declare global { diff --git a/packages/remirror__extension-list/src/task-list-extension.ts b/packages/remirror__extension-list/src/task-list-extension.ts index 92cd0df400..2429ee34bd 100644 --- a/packages/remirror__extension-list/src/task-list-extension.ts +++ b/packages/remirror__extension-list/src/task-list-extension.ts @@ -55,7 +55,7 @@ export class TaskListExtension extends NodeExtension { /** * Toggle the task list for the current selection. */ - @command({ icon: 'listCheck', label: ({ t }) => t(Messages.TASK_LIST_LABEL) }) + @command({ icon: 'checkboxMultipleLine', label: ({ t }) => t(Messages.TASK_LIST_LABEL) }) toggleTaskList(): CommandFunction { return toggleList(this.type, assertGet(this.store.schema.nodes, 'taskListItem')); } diff --git a/packages/remirror__i18n/src/en/messages.po b/packages/remirror__i18n/src/en/messages.po index 44552070e7..b595f3c52e 100644 --- a/packages/remirror__i18n/src/en/messages.po +++ b/packages/remirror__i18n/src/en/messages.po @@ -273,7 +273,7 @@ msgstr "Bulleted list" #: packages/remirror__messages/src/extension-callout-messages.ts:14 msgid "extension.command.toggle-callout.description" msgstr "" -"{level, select, info {Create an information callout block}\n" +"{type, select, info {Create an information callout block}\n" "warning {Create a warning callout block}\n" "error {Create an error callout block}\n" "success {Create a success callout block}\n" @@ -283,7 +283,7 @@ msgstr "" #: packages/remirror__messages/src/extension-callout-messages.ts:4 msgid "extension.command.toggle-callout.label" msgstr "" -"{level, select, info {Information Callout}\n" +"{type, select, info {Information Callout}\n" "warning {Warning Callout}\n" "error {Error Callout}\n" "success {Success Callout}\n" diff --git a/packages/remirror__i18n/src/en/messages.ts b/packages/remirror__i18n/src/en/messages.ts index 2c93c2491d..3ea1b6d74e 100644 --- a/packages/remirror__i18n/src/en/messages.ts +++ b/packages/remirror__i18n/src/en/messages.ts @@ -67,7 +67,7 @@ 'extension.command.toggle-bullet-list.description': 'Bulleted list', 'extension.command.toggle-callout.description': [ [ - 'level', + 'type', 'select', { info: 'Create an information callout block', @@ -80,7 +80,7 @@ ], 'extension.command.toggle-callout.label': [ [ - 'level', + 'type', 'select', { info: 'Information Callout', diff --git a/packages/remirror__icons/src/all-icons.ts b/packages/remirror__icons/src/all-icons.ts index 9505ca80ef..ce20f70a0d 100644 --- a/packages/remirror__icons/src/all-icons.ts +++ b/packages/remirror__icons/src/all-icons.ts @@ -4992,21 +4992,6 @@ export const checkboxMultipleFill: IconTree[] = [ }, ]; -/** - * The icon for `checkbox-multiple-line.svg` created by [RemixIcons](https://remixicons.com). - * ![Checkbox Multiple Line](https://cdn.jsdelivr.net/npm/remixicon@2.5.0/icons/System/checkbox-multiple-line.svg) - */ -export const checkboxMultipleLine: IconTree[] = [ - { tag: 'path', attr: { fill: 'none', d: 'M0 0h24v24H0z' } }, - { - tag: 'path', - attr: { - fillRule: 'nonzero', - d: 'M7 7V3a1 1 0 0 1 1-1h13a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-4v3.993c0 .556-.449 1.007-1.007 1.007H3.007A1.006 1.006 0 0 1 2 20.993l.003-12.986C2.003 7.451 2.452 7 3.01 7H7zm2 0h6.993C16.549 7 17 7.449 17 8.007V15h3V4H9v3zm6 2H4.003L4 20h11V9zm-6.497 9l-3.536-3.536 1.414-1.414 2.122 2.122 4.242-4.243 1.414 1.414L8.503 18z', - }, - }, -]; - /** * The icon for `china-railway-fill.svg` created by [RemixIcons](https://remixicons.com). * ![China Railway Fill](https://cdn.jsdelivr.net/npm/remixicon@2.5.0/icons/Map/china-railway-fill.svg) diff --git a/packages/remirror__icons/src/core-icons.ts b/packages/remirror__icons/src/core-icons.ts index b3ac5c4ce3..ac2d9e8bb1 100644 --- a/packages/remirror__icons/src/core-icons.ts +++ b/packages/remirror__icons/src/core-icons.ts @@ -303,6 +303,21 @@ export const checkboxCircleLine: IconTree[] = [ }, ]; +/** + * The icon for `checkbox-multiple-line.svg` created by [RemixIcons](https://remixicons.com). + * ![Checkbox Multiple Line](https://cdn.jsdelivr.net/npm/remixicon@2.5.0/icons/System/checkbox-multiple-line.svg) + */ +export const checkboxMultipleLine: IconTree[] = [ + { tag: 'path', attr: { fill: 'none', d: 'M0 0h24v24H0z' } }, + { + tag: 'path', + attr: { + fillRule: 'nonzero', + d: 'M7 7V3a1 1 0 0 1 1-1h13a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-4v3.993c0 .556-.449 1.007-1.007 1.007H3.007A1.006 1.006 0 0 1 2 20.993l.003-12.986C2.003 7.451 2.452 7 3.01 7H7zm2 0h6.993C16.549 7 17 7.449 17 8.007V15h3V4H9v3zm6 2H4.003L4 20h11V9zm-6.497 9l-3.536-3.536 1.414-1.414 2.122 2.122 4.242-4.243 1.414 1.414L8.503 18z', + }, + }, +]; + /** * The icon for `clipboard-fill.svg` created by [RemixIcons](https://remixicons.com). * ![Clipboard Fill](https://cdn.jsdelivr.net/npm/remixicon@2.5.0/icons/Document/clipboard-fill.svg) diff --git a/packages/remirror__react-components/package.json b/packages/remirror__react-components/package.json index 44c038baa4..b13eb85f49 100644 --- a/packages/remirror__react-components/package.json +++ b/packages/remirror__react-components/package.json @@ -38,7 +38,10 @@ ], "dependencies": { "@babel/runtime": "^7.13.10", + "@emotion/react": "^11.9.0", + "@emotion/styled": "^11.3.0", "@lingui/core": "^3.14.0", + "@mui/material": "^5.8.5", "@popperjs/core": "^2.9.2", "@remirror/core": "^2.0.0-beta.16", "@remirror/extension-positioner": "^2.0.0-beta.16", @@ -55,11 +58,7 @@ "create-context-state": "^2.0.0-beta.15", "match-sorter": "^6.3.0", "multishift": "^2.0.0-beta.16", - "react-color": "^2.19.3", - "react-popper": "^2.2.5", - "reakit": "^1.3.11", - "reakit-system-palette": "^0.14.2", - "reakit-utils": "^0.15.2" + "react-color": "^2.19.3" }, "devDependencies": { "@remirror/pm": "^2.0.0-beta.16", diff --git a/packages/remirror__react-components/src/button-groups/basic-formatting-button-group.tsx b/packages/remirror__react-components/src/button-groups/basic-formatting-button-group.tsx new file mode 100644 index 0000000000..f6376bd3b9 --- /dev/null +++ b/packages/remirror__react-components/src/button-groups/basic-formatting-button-group.tsx @@ -0,0 +1,19 @@ +import React, { FC, ReactNode } from 'react'; + +import { ToggleBoldButton, ToggleItalicButton, ToggleUnderlineButton } from '../buttons'; +import { CommandButtonGroup } from './command-button-group'; + +export interface BasicFormattingButtonGroupProps { + children?: ReactNode | ReactNode[]; +} + +export const BasicFormattingButtonGroup: FC = ({ children }) => { + return ( + + + + + {children} + + ); +}; diff --git a/packages/remirror__react-components/src/button-groups/callout-type-button-group.tsx b/packages/remirror__react-components/src/button-groups/callout-type-button-group.tsx new file mode 100644 index 0000000000..6f1177bdc8 --- /dev/null +++ b/packages/remirror__react-components/src/button-groups/callout-type-button-group.tsx @@ -0,0 +1,25 @@ +import React, { FC, ReactNode } from 'react'; + +import { ToggleCalloutButton } from '../buttons'; +import { CommandButtonGroup } from './command-button-group'; + +export interface CalloutTypeButtonGroupProps { + children?: ReactNode | ReactNode[]; +} + +const INFO_CALLOUT = { type: 'info' }; +const WARNING_CALLOUT = { type: 'warning' }; +const ERROR_CALLOUT = { type: 'error' }; +const SUCCESS_CALLOUT = { type: 'success' }; + +export const CalloutTypeButtonGroup: FC = ({ children }) => { + return ( + + + + + + {children} + + ); +}; diff --git a/packages/remirror__react-components/src/button-groups/command-button-group.tsx b/packages/remirror__react-components/src/button-groups/command-button-group.tsx new file mode 100644 index 0000000000..53e4fd9a20 --- /dev/null +++ b/packages/remirror__react-components/src/button-groups/command-button-group.tsx @@ -0,0 +1,21 @@ +import { Box, BoxProps } from '@mui/material'; +import React, { FC, ReactNode } from 'react'; + +export interface CommandButtonGroupProps extends Omit { + children: ReactNode | ReactNode[]; +} + +export const CommandButtonGroup: FC = (props) => { + return ( + + ); +}; diff --git a/packages/remirror__react-components/src/button-groups/data-transfer-button-group.tsx b/packages/remirror__react-components/src/button-groups/data-transfer-button-group.tsx new file mode 100644 index 0000000000..c4d8d7ba10 --- /dev/null +++ b/packages/remirror__react-components/src/button-groups/data-transfer-button-group.tsx @@ -0,0 +1,19 @@ +import React, { FC, ReactNode } from 'react'; + +import { CopyButton, CutButton, PasteButton } from '../buttons'; +import { CommandButtonGroup } from './command-button-group'; + +export interface DataTransferButtonGroupProps { + children?: ReactNode | ReactNode[]; +} + +export const DataTransferButtonGroup: FC = ({ children }) => { + return ( + + + + + {children} + + ); +}; diff --git a/packages/remirror__react-components/src/button-groups/formatting-button-group.tsx b/packages/remirror__react-components/src/button-groups/formatting-button-group.tsx new file mode 100644 index 0000000000..90fc6a5115 --- /dev/null +++ b/packages/remirror__react-components/src/button-groups/formatting-button-group.tsx @@ -0,0 +1,27 @@ +import React, { FC, ReactNode } from 'react'; + +import { + ToggleBoldButton, + ToggleCodeButton, + ToggleItalicButton, + ToggleStrikeButton, + ToggleUnderlineButton, +} from '../buttons'; +import { CommandButtonGroup } from './command-button-group'; + +export interface FormattingButtonGroupProps { + children?: ReactNode | ReactNode[]; +} + +export const FormattingButtonGroup: FC = ({ children }) => { + return ( + + + + + + + {children} + + ); +}; diff --git a/packages/remirror__react-components/src/button-groups/heading-level-button-group.tsx b/packages/remirror__react-components/src/button-groups/heading-level-button-group.tsx new file mode 100644 index 0000000000..56b73b99a4 --- /dev/null +++ b/packages/remirror__react-components/src/button-groups/heading-level-button-group.tsx @@ -0,0 +1,40 @@ +import React, { FC, ReactNode } from 'react'; + +import { DropdownButton, ToggleHeadingButton } from '../buttons'; +import { ToggleHeadingMenuItem } from '../menus'; +import { CommandButtonGroup } from './command-button-group'; + +export interface HeadingLevelButtonGroupProps { + showAll?: boolean; + children?: ReactNode | ReactNode[]; +} + +const LEVEL_1 = { level: 1 }; +const LEVEL_2 = { level: 2 }; +const LEVEL_3 = { level: 3 }; +const LEVEL_4 = { level: 4 }; +const LEVEL_5 = { level: 5 }; +const LEVEL_6 = { level: 6 }; + +export const HeadingLevelButtonGroup: FC = ({ + showAll = false, + children, +}) => { + return ( + + + + {!showAll ? ( + + ) : ( + + + + + + + )} + {children} + + ); +}; diff --git a/packages/remirror__react-components/src/button-groups/history-button-group.tsx b/packages/remirror__react-components/src/button-groups/history-button-group.tsx new file mode 100644 index 0000000000..e0d55dd803 --- /dev/null +++ b/packages/remirror__react-components/src/button-groups/history-button-group.tsx @@ -0,0 +1,18 @@ +import React, { FC, ReactNode } from 'react'; + +import { RedoButton, UndoButton } from '../buttons'; +import { CommandButtonGroup } from './command-button-group'; + +export interface HistoryButtonGroupProps { + children?: ReactNode | ReactNode[]; +} + +export const HistoryButtonGroup: FC = ({ children }) => { + return ( + + + + {children} + + ); +}; diff --git a/packages/remirror__react-components/src/button-groups/index.ts b/packages/remirror__react-components/src/button-groups/index.ts new file mode 100644 index 0000000000..1353766a05 --- /dev/null +++ b/packages/remirror__react-components/src/button-groups/index.ts @@ -0,0 +1,8 @@ +export * from './basic-formatting-button-group'; +export * from './callout-type-button-group'; +export * from './command-button-group'; +export * from './data-transfer-button-group'; +export * from './formatting-button-group'; +export * from './heading-level-button-group'; +export * from './history-button-group'; +export * from './list-button-group'; diff --git a/packages/remirror__react-components/src/button-groups/list-button-group.tsx b/packages/remirror__react-components/src/button-groups/list-button-group.tsx new file mode 100644 index 0000000000..893d09d56d --- /dev/null +++ b/packages/remirror__react-components/src/button-groups/list-button-group.tsx @@ -0,0 +1,19 @@ +import React, { FC, ReactNode } from 'react'; + +import { ToggleBulletListButton, ToggleOrderedListButton, ToggleTaskListButton } from '../buttons'; +import { CommandButtonGroup } from './command-button-group'; + +export interface ListButtonGroupProps { + children?: ReactNode | ReactNode[]; +} + +export const ListButtonGroup: FC = ({ children }) => { + return ( + + + + + {children} + + ); +}; diff --git a/packages/remirror__react-components/src/buttons/command-button.tsx b/packages/remirror__react-components/src/buttons/command-button.tsx new file mode 100644 index 0000000000..0e66d3f612 --- /dev/null +++ b/packages/remirror__react-components/src/buttons/command-button.tsx @@ -0,0 +1,109 @@ +import { ToggleButton, ToggleButtonProps, Tooltip } from '@mui/material'; +import React, { FC, MouseEvent, MouseEventHandler, ReactNode, useCallback } from 'react'; +import { CoreIcon, isString } from '@remirror/core'; + +import { Icon } from '../icons'; +import { useCommandOptionValues, UseCommandOptionValuesParams } from '../use-command-option-values'; + +interface ButtonIconProps { + icon: CoreIcon | JSX.Element | null; +} + +const ButtonIcon: FC = ({ icon }) => { + if (isString(icon)) { + return ; + } + + return icon; +}; + +export interface CommandButtonProps + extends Omit, + Omit { + active?: UseCommandOptionValuesParams['active']; + 'aria-label'?: string; + label?: NonNullable; + commandName: string; + displayShortcut?: boolean; + onSelect: () => void; + icon?: CoreIcon | JSX.Element; + attrs?: UseCommandOptionValuesParams['attrs']; +} + +export const CommandButton: FC = ({ + commandName, + active = false, + enabled, + attrs, + onSelect, + onChange, + icon, + displayShortcut = true, + 'aria-label': ariaLabel, + label, + ...rest +}) => { + const handleChange = useCallback( + (e: MouseEvent, value: any) => { + onSelect(); + onChange?.(e, value); + }, + [onSelect, onChange], + ); + + const handleMouseDown: MouseEventHandler = useCallback((e) => { + e.preventDefault(); + }, []); + + const commandOptions = useCommandOptionValues({ commandName, active, enabled, attrs }); + + let fallbackIcon = null; + + if (commandOptions.icon) { + fallbackIcon = isString(commandOptions.icon) ? commandOptions.icon : commandOptions.icon.name; + } + + const labelText = ariaLabel ?? commandOptions.label ?? ''; + const tooltipText = label ?? labelText; + const shortcutText = + displayShortcut && commandOptions.shortcut ? ` (${commandOptions.shortcut})` : ''; + + return ( + + + + + + ); +}; diff --git a/packages/remirror__react-components/src/buttons/copy-button.tsx b/packages/remirror__react-components/src/buttons/copy-button.tsx new file mode 100644 index 0000000000..196ec9022b --- /dev/null +++ b/packages/remirror__react-components/src/buttons/copy-button.tsx @@ -0,0 +1,31 @@ +import React, { FC, useCallback } from 'react'; +import { useCommands, useCurrentSelection } from '@remirror/react-core'; + +import { CommandButton, CommandButtonProps } from './command-button'; + +export interface CopyButtonProps + extends Omit {} + +export const CopyButton: FC = (props) => { + const { copy } = useCommands(); + // Force component update on selection change + useCurrentSelection(); + + const handleSelect = useCallback(() => { + if (copy.enabled()) { + copy(); + } + }, [copy]); + + const enabled = copy.enabled(); + + return ( + + ); +}; diff --git a/packages/remirror__react-components/src/buttons/create-table-button.tsx b/packages/remirror__react-components/src/buttons/create-table-button.tsx new file mode 100644 index 0000000000..88ef8f638e --- /dev/null +++ b/packages/remirror__react-components/src/buttons/create-table-button.tsx @@ -0,0 +1,30 @@ +import React, { FC, useCallback } from 'react'; +import { useActive, useCommands } from '@remirror/react-core'; + +import { CommandButton, CommandButtonProps } from './command-button'; + +export interface CreateTableButtonProps + extends Omit {} + +export const CreateTableButton: FC = (props) => { + const { createTable } = useCommands(); + + const handleSelect = useCallback(() => { + if (createTable.enabled()) { + createTable(); + } + }, [createTable]); + + const active = useActive().table(); + const enabled = createTable.enabled(); + + return ( + + ); +}; diff --git a/packages/remirror__react-components/src/buttons/cut-button.tsx b/packages/remirror__react-components/src/buttons/cut-button.tsx new file mode 100644 index 0000000000..ed64dcba84 --- /dev/null +++ b/packages/remirror__react-components/src/buttons/cut-button.tsx @@ -0,0 +1,31 @@ +import React, { FC, useCallback } from 'react'; +import { useCommands, useCurrentSelection } from '@remirror/react-core'; + +import { CommandButton, CommandButtonProps } from './command-button'; + +export interface CutButtonProps + extends Omit {} + +export const CutButton: FC = (props) => { + const { cut } = useCommands(); + // Force component update on selection change + useCurrentSelection(); + + const handleSelect = useCallback(() => { + if (cut.enabled()) { + cut(); + } + }, [cut]); + + const enabled = cut.enabled(); + + return ( + + ); +}; diff --git a/packages/remirror__react-components/src/buttons/dropdown-button.tsx b/packages/remirror__react-components/src/buttons/dropdown-button.tsx new file mode 100644 index 0000000000..047df9c1ef --- /dev/null +++ b/packages/remirror__react-components/src/buttons/dropdown-button.tsx @@ -0,0 +1,77 @@ +import { IconButton, Menu, MenuProps, Tooltip } from '@mui/material'; +import React, { FC, MouseEventHandler, ReactNode, useCallback, useRef, useState } from 'react'; +import { uniqueId } from '@remirror/core'; + +import { Icon } from '../icons'; + +export interface DropdownButtonProps extends Omit { + 'aria-label': string; + label?: NonNullable; + icon?: JSX.Element; +} + +export const DropdownButton: FC = ({ + label, + 'aria-label': ariaLabel, + icon, + children, + onClose, + ...rest +}) => { + const id = useRef(uniqueId()); + const [anchorEl, setAnchorEl] = useState(null); + const open = Boolean(anchorEl); + + const handleMouseDown: MouseEventHandler = useCallback((e) => { + e.preventDefault(); + }, []); + + const handleClick: MouseEventHandler = useCallback((event) => { + setAnchorEl(event.currentTarget); + }, []); + + const handleClose: MenuProps['onClose'] = useCallback( + (e: Event, reason: 'backdropClick' | 'escapeKeyDown') => { + setAnchorEl(null); + onClose?.(e, reason); + }, + [onClose], + ); + + return ( + <> + + ({ + border: `1px solid ${theme.palette.divider}`, + borderRadius: `${theme.shape.borderRadius}px`, + padding: '6px 12px', + '&:not(:first-of-type)': { + marginLeft: '-1px', + borderLeft: '1px solid transparent', + borderTopLeftRadius: 0, + borderBottomLeftRadius: 0, + }, + '&:not(:last-of-type)': { + borderTopRightRadius: 0, + borderBottomRightRadius: 0, + }, + })} + > + {icon} + + + + + {children} + + + ); +}; diff --git a/packages/remirror__react-components/src/buttons/index.ts b/packages/remirror__react-components/src/buttons/index.ts new file mode 100644 index 0000000000..58484178c3 --- /dev/null +++ b/packages/remirror__react-components/src/buttons/index.ts @@ -0,0 +1,20 @@ +export * from './command-button'; +export * from './copy-button'; +export * from './create-table-button'; +export * from './cut-button'; +export * from './dropdown-button'; +export * from './paste-button'; +export * from './redo-button'; +export * from './toggle-blockquote-button'; +export * from './toggle-bold-button'; +export * from './toggle-bullet-list-button'; +export * from './toggle-callout-button'; +export * from './toggle-code-block-button'; +export * from './toggle-code-button'; +export * from './toggle-heading-button'; +export * from './toggle-italic-button'; +export * from './toggle-ordered-list-button'; +export * from './toggle-strike-button'; +export * from './toggle-task-list-button'; +export * from './toggle-underline-button'; +export * from './undo-button'; diff --git a/packages/remirror__react-components/src/buttons/paste-button.tsx b/packages/remirror__react-components/src/buttons/paste-button.tsx new file mode 100644 index 0000000000..d710534d58 --- /dev/null +++ b/packages/remirror__react-components/src/buttons/paste-button.tsx @@ -0,0 +1,31 @@ +import React, { FC, useCallback } from 'react'; +import { useCommands, useEditorState } from '@remirror/react-core'; + +import { CommandButton, CommandButtonProps } from './command-button'; + +export interface PasteButtonProps + extends Omit {} + +export const PasteButton: FC = (props) => { + const { paste } = useCommands(); + // Force component update on state change + useEditorState(); + + const handleSelect = useCallback(() => { + if (paste.enabled()) { + paste(); + } + }, [paste]); + + const enabled = paste.enabled(); + + return ( + + ); +}; diff --git a/packages/remirror__react-components/src/buttons/redo-button.tsx b/packages/remirror__react-components/src/buttons/redo-button.tsx new file mode 100644 index 0000000000..c94280e670 --- /dev/null +++ b/packages/remirror__react-components/src/buttons/redo-button.tsx @@ -0,0 +1,30 @@ +import React, { FC, useCallback } from 'react'; +import { useCommands, useHelpers } from '@remirror/react-core'; + +import { CommandButton, CommandButtonProps } from './command-button'; + +export interface RedoButtonProps + extends Omit {} + +export const RedoButton: FC = (props) => { + const { redo } = useCommands(); + const { redoDepth } = useHelpers(true); + + const handleSelect = useCallback(() => { + if (redo.enabled()) { + redo(); + } + }, [redo]); + + const enabled = redoDepth() > 0; + + return ( + + ); +}; diff --git a/packages/remirror__react-components/src/buttons/toggle-blockquote-button.tsx b/packages/remirror__react-components/src/buttons/toggle-blockquote-button.tsx new file mode 100644 index 0000000000..e559514d17 --- /dev/null +++ b/packages/remirror__react-components/src/buttons/toggle-blockquote-button.tsx @@ -0,0 +1,30 @@ +import React, { FC, useCallback } from 'react'; +import { useActive, useCommands } from '@remirror/react-core'; + +import { CommandButton, CommandButtonProps } from './command-button'; + +export interface ToggleBlockquoteButtonProps + extends Omit {} + +export const ToggleBlockquoteButton: FC = (props) => { + const { toggleBlockquote } = useCommands(); + + const handleSelect = useCallback(() => { + if (toggleBlockquote.enabled()) { + toggleBlockquote(); + } + }, [toggleBlockquote]); + + const active = useActive().blockquote(); + const enabled = toggleBlockquote.enabled(); + + return ( + + ); +}; diff --git a/packages/remirror__react-components/src/buttons/toggle-bold-button.tsx b/packages/remirror__react-components/src/buttons/toggle-bold-button.tsx new file mode 100644 index 0000000000..d6749bb7e4 --- /dev/null +++ b/packages/remirror__react-components/src/buttons/toggle-bold-button.tsx @@ -0,0 +1,30 @@ +import React, { FC, useCallback } from 'react'; +import { useActive, useCommands } from '@remirror/react-core'; + +import { CommandButton, CommandButtonProps } from './command-button'; + +export interface ToggleBoldButtonProps + extends Omit {} + +export const ToggleBoldButton: FC = (props) => { + const { toggleBold } = useCommands(); + + const handleSelect = useCallback(() => { + if (toggleBold.enabled()) { + toggleBold(); + } + }, [toggleBold]); + + const active = useActive().bold(); + const enabled = toggleBold.enabled(); + + return ( + + ); +}; diff --git a/packages/remirror__react-components/src/buttons/toggle-bullet-list-button.tsx b/packages/remirror__react-components/src/buttons/toggle-bullet-list-button.tsx new file mode 100644 index 0000000000..2c2ec43828 --- /dev/null +++ b/packages/remirror__react-components/src/buttons/toggle-bullet-list-button.tsx @@ -0,0 +1,30 @@ +import React, { FC, useCallback } from 'react'; +import { useActive, useCommands } from '@remirror/react-core'; + +import { CommandButton, CommandButtonProps } from './command-button'; + +export interface ToggleBulletListButtonProps + extends Omit {} + +export const ToggleBulletListButton: FC = (props) => { + const { toggleBulletList } = useCommands(); + + const handleSelect = useCallback(() => { + if (toggleBulletList.enabled()) { + toggleBulletList(); + } + }, [toggleBulletList]); + + const active = useActive().bulletList(); + const enabled = toggleBulletList.enabled(); + + return ( + + ); +}; diff --git a/packages/remirror__react-components/src/buttons/toggle-callout-button.tsx b/packages/remirror__react-components/src/buttons/toggle-callout-button.tsx new file mode 100644 index 0000000000..bde409cb6e --- /dev/null +++ b/packages/remirror__react-components/src/buttons/toggle-callout-button.tsx @@ -0,0 +1,34 @@ +import React, { FC, useCallback } from 'react'; +import { ProsemirrorAttributes } from '@remirror/core'; +import { useActive, useCommands } from '@remirror/react-core'; + +import { CommandButton, CommandButtonProps } from './command-button'; + +export interface ToggleCalloutButtonProps + extends Omit { + attrs?: Partial; +} + +export const ToggleCalloutButton: FC = ({ attrs = {}, ...rest }) => { + const { toggleCallout } = useCommands(); + + const handleSelect = useCallback(() => { + if (toggleCallout.enabled(attrs)) { + toggleCallout(attrs); + } + }, [toggleCallout, attrs]); + + const active = useActive().callout(attrs); + const enabled = toggleCallout.enabled(attrs); + + return ( + + ); +}; diff --git a/packages/remirror__react-components/src/buttons/toggle-code-block-button.tsx b/packages/remirror__react-components/src/buttons/toggle-code-block-button.tsx new file mode 100644 index 0000000000..a8347be2b9 --- /dev/null +++ b/packages/remirror__react-components/src/buttons/toggle-code-block-button.tsx @@ -0,0 +1,34 @@ +import React, { FC, useCallback } from 'react'; +import { ProsemirrorAttributes } from '@remirror/core'; +import { useActive, useCommands } from '@remirror/react-core'; + +import { CommandButton, CommandButtonProps } from './command-button'; + +export interface ToggleCodeBlockButtonProps + extends Omit { + attrs?: Partial; +} + +export const ToggleCodeBlockButton: FC = ({ attrs = {}, ...rest }) => { + const { toggleCodeBlock } = useCommands(); + + const handleSelect = useCallback(() => { + if (toggleCodeBlock.enabled(attrs)) { + toggleCodeBlock(attrs); + } + }, [toggleCodeBlock, attrs]); + + const active = useActive().codeBlock(); + const enabled = toggleCodeBlock.enabled(attrs); + + return ( + + ); +}; diff --git a/packages/remirror__react-components/src/buttons/toggle-code-button.tsx b/packages/remirror__react-components/src/buttons/toggle-code-button.tsx new file mode 100644 index 0000000000..bff51dd930 --- /dev/null +++ b/packages/remirror__react-components/src/buttons/toggle-code-button.tsx @@ -0,0 +1,30 @@ +import React, { FC, useCallback } from 'react'; +import { useActive, useCommands } from '@remirror/react-core'; + +import { CommandButton, CommandButtonProps } from './command-button'; + +export interface ToggleCodeButtonProps + extends Omit {} + +export const ToggleCodeButton: FC = (props) => { + const { toggleCode } = useCommands(); + + const handleSelect = useCallback(() => { + if (toggleCode.enabled()) { + toggleCode(); + } + }, [toggleCode]); + + const active = useActive().code(); + const enabled = toggleCode.enabled(); + + return ( + + ); +}; diff --git a/packages/remirror__react-components/src/buttons/toggle-heading-button.tsx b/packages/remirror__react-components/src/buttons/toggle-heading-button.tsx new file mode 100644 index 0000000000..b54a901d58 --- /dev/null +++ b/packages/remirror__react-components/src/buttons/toggle-heading-button.tsx @@ -0,0 +1,34 @@ +import React, { FC, useCallback } from 'react'; +import { ProsemirrorAttributes } from '@remirror/core'; +import { useActive, useCommands } from '@remirror/react-core'; + +import { CommandButton, CommandButtonProps } from './command-button'; + +export interface ToggleHeadingButtonProps + extends Omit { + attrs?: Partial; +} + +export const ToggleHeadingButton: FC = ({ attrs, ...rest }) => { + const { toggleHeading } = useCommands(); + + const handleSelect = useCallback(() => { + if (toggleHeading.enabled(attrs)) { + toggleHeading(attrs); + } + }, [toggleHeading, attrs]); + + const active = useActive().heading(attrs); + const enabled = toggleHeading.enabled(attrs); + + return ( + + ); +}; diff --git a/packages/remirror__react-components/src/buttons/toggle-italic-button.tsx b/packages/remirror__react-components/src/buttons/toggle-italic-button.tsx new file mode 100644 index 0000000000..b22d84597b --- /dev/null +++ b/packages/remirror__react-components/src/buttons/toggle-italic-button.tsx @@ -0,0 +1,30 @@ +import React, { FC, useCallback } from 'react'; +import { useActive, useCommands } from '@remirror/react-core'; + +import { CommandButton, CommandButtonProps } from './command-button'; + +export interface ToggleItalicButtonProps + extends Omit {} + +export const ToggleItalicButton: FC = (props) => { + const { toggleItalic } = useCommands(); + + const handleSelect = useCallback(() => { + if (toggleItalic.enabled()) { + toggleItalic(); + } + }, [toggleItalic]); + + const active = useActive().italic(); + const enabled = toggleItalic.enabled(); + + return ( + + ); +}; diff --git a/packages/remirror__react-components/src/buttons/toggle-ordered-list-button.tsx b/packages/remirror__react-components/src/buttons/toggle-ordered-list-button.tsx new file mode 100644 index 0000000000..6a6e6623ef --- /dev/null +++ b/packages/remirror__react-components/src/buttons/toggle-ordered-list-button.tsx @@ -0,0 +1,30 @@ +import React, { FC, useCallback } from 'react'; +import { useActive, useCommands } from '@remirror/react-core'; + +import { CommandButton, CommandButtonProps } from './command-button'; + +export interface ToggleOrderedListButtonProps + extends Omit {} + +export const ToggleOrderedListButton: FC = (props) => { + const { toggleOrderedList } = useCommands(); + + const handleSelect = useCallback(() => { + if (toggleOrderedList.enabled()) { + toggleOrderedList(); + } + }, [toggleOrderedList]); + + const active = useActive().orderedList(); + const enabled = toggleOrderedList.enabled(); + + return ( + + ); +}; diff --git a/packages/remirror__react-components/src/buttons/toggle-strike-button.tsx b/packages/remirror__react-components/src/buttons/toggle-strike-button.tsx new file mode 100644 index 0000000000..16b69901a0 --- /dev/null +++ b/packages/remirror__react-components/src/buttons/toggle-strike-button.tsx @@ -0,0 +1,30 @@ +import React, { FC, useCallback } from 'react'; +import { useActive, useCommands } from '@remirror/react-core'; + +import { CommandButton, CommandButtonProps } from './command-button'; + +export interface ToggleStrikeButtonProps + extends Omit {} + +export const ToggleStrikeButton: FC = (props) => { + const { toggleStrike } = useCommands(); + + const handleSelect = useCallback(() => { + if (toggleStrike.enabled()) { + toggleStrike(); + } + }, [toggleStrike]); + + const active = useActive().strike(); + const enabled = toggleStrike.enabled(); + + return ( + + ); +}; diff --git a/packages/remirror__react-components/src/buttons/toggle-task-list-button.tsx b/packages/remirror__react-components/src/buttons/toggle-task-list-button.tsx new file mode 100644 index 0000000000..bb90e91d33 --- /dev/null +++ b/packages/remirror__react-components/src/buttons/toggle-task-list-button.tsx @@ -0,0 +1,30 @@ +import React, { FC, useCallback } from 'react'; +import { useActive, useCommands } from '@remirror/react-core'; + +import { CommandButton, CommandButtonProps } from './command-button'; + +export interface ToggleTaskListButtonProps + extends Omit {} + +export const ToggleTaskListButton: FC = (props) => { + const { toggleTaskList } = useCommands(); + + const handleSelect = useCallback(() => { + if (toggleTaskList.enabled()) { + toggleTaskList(); + } + }, [toggleTaskList]); + + const active = useActive().taskList(); + const enabled = toggleTaskList.enabled(); + + return ( + + ); +}; diff --git a/packages/remirror__react-components/src/buttons/toggle-underline-button.tsx b/packages/remirror__react-components/src/buttons/toggle-underline-button.tsx new file mode 100644 index 0000000000..799e3978a0 --- /dev/null +++ b/packages/remirror__react-components/src/buttons/toggle-underline-button.tsx @@ -0,0 +1,30 @@ +import React, { FC, useCallback } from 'react'; +import { useActive, useCommands } from '@remirror/react-core'; + +import { CommandButton, CommandButtonProps } from './command-button'; + +export interface ToggleUnderlineButtonProps + extends Omit {} + +export const ToggleUnderlineButton: FC = (props) => { + const { toggleUnderline } = useCommands(); + + const handleSelect = useCallback(() => { + if (toggleUnderline.enabled()) { + toggleUnderline(); + } + }, [toggleUnderline]); + + const active = useActive().underline(); + const enabled = toggleUnderline.enabled(); + + return ( + + ); +}; diff --git a/packages/remirror__react-components/src/buttons/undo-button.tsx b/packages/remirror__react-components/src/buttons/undo-button.tsx new file mode 100644 index 0000000000..101b039065 --- /dev/null +++ b/packages/remirror__react-components/src/buttons/undo-button.tsx @@ -0,0 +1,30 @@ +import React, { FC, useCallback } from 'react'; +import { useCommands, useHelpers } from '@remirror/react-core'; + +import { CommandButton, CommandButtonProps } from './command-button'; + +export interface UndoButtonProps + extends Omit {} + +export const UndoButton: FC = (props) => { + const { undo } = useCommands(); + const { undoDepth } = useHelpers(true); + + const handleSelect = useCallback(() => { + if (undo.enabled()) { + undo(); + } + }, [undo]); + + const enabled = undoDepth() > 0; + + return ( + + ); +}; diff --git a/packages/remirror__react-components/src/components/color-picker-component.tsx b/packages/remirror__react-components/src/components/color-picker-component.tsx deleted file mode 100644 index 540f0c50ba..0000000000 --- a/packages/remirror__react-components/src/components/color-picker-component.tsx +++ /dev/null @@ -1,165 +0,0 @@ -/** - * @module - * - * A color picker component which is used for menus. - */ -import type { FC, PropsWithChildren } from 'react'; -import React, { useCallback } from 'react'; -import { - Composite, - CompositeGroup, - CompositeItem, - CompositeStateReturn, - Tooltip, - TooltipReference, - useCompositeState, - useTooltipState, -} from 'reakit'; -import { cx } from '@remirror/core'; -import { HuePalette, Palette, palette as defaultPalette } from '@remirror/extension-text-color'; -import { useI18n } from '@remirror/react-core'; -import { ComponentsTheme } from '@remirror/theme'; - -export interface ColorPickerProps { - /** - * The currently selected color if active. - */ - selectedColor?: string; - - /** - * Called when a color is selected. - */ - onSelect: (color: string) => void; - - /** - * The color palette to use. - */ - palette?: Palette; -} - -export const ColorPickerComponent = (props: ColorPickerProps): JSX.Element => { - const { palette = defaultPalette, onSelect, selectedColor } = props; - const { t } = useI18n(); - const compositeState = useCompositeState({ loop: true }); - const { black, transparent, white, hues } = palette(t); - - return ( - - - - - - - {Object.values(hues).map((hueMap, index) => ( - - ))} - - ); -}; - -interface ColorPickerHue extends HuePalette, CompositeStateProps { - selectedColor?: string; - /** - * Called when a color is selected. - */ - onSelect: (color: string) => void; -} - -const ColorPickerHue = (props: ColorPickerHue) => { - const { label, hues, compositeState, selectedColor, onSelect } = props; - - return ( - <> - {label} - - {hues.map((hue, index) => ( - - ))} - - - ); -}; - -const Grid: FC> = (props) => { - return ( - - {props.children} - - ); -}; - -interface CompositeStateProps { - compositeState: CompositeStateReturn; -} - -const GridRow: FC> = (props) => { - return ( - - {props.children} - - ); -}; - -interface GridCellProps extends CompositeStateProps { - /** - * Called when a color is selected. - */ - onSelect: (color: string) => void; - className?: string; - color: string; - label: string; - selected: boolean; -} - -const GridCell = (props: GridCellProps) => { - const { color, label, selected, compositeState, onSelect, className } = props; - const tooltipState = useTooltipState({ gutter: 5 }); - const onClick = useCallback(() => onSelect(color), [color, onSelect]); - - return ( - <> - - - - {label} - - ); -}; diff --git a/packages/remirror__react-components/src/components/command-icon-component.tsx b/packages/remirror__react-components/src/components/command-icon-component.tsx deleted file mode 100644 index 69d6cbfaec..0000000000 --- a/packages/remirror__react-components/src/components/command-icon-component.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import React from 'react'; -import { Box } from 'reakit'; -import { CommandUiIcon, isString } from '@remirror/core'; -import { CoreIcon } from '@remirror/icons'; - -import { Icon } from '../icons'; - -interface CommandIconProps { - icon: CoreIcon | CommandUiIcon; - wrapperClass?: string; -} - -export const CommandIconComponent = (props: CommandIconProps): JSX.Element => { - const { icon, wrapperClass } = props; - - if (isString(icon)) { - return ; - } - - // Render when the icon is a complex icon. - return ( - - {icon.sup && icon.sup} - - {icon.sub && icon.sub} - - ); -}; diff --git a/packages/remirror__react-components/src/components/dialog-component.tsx b/packages/remirror__react-components/src/components/dialog-component.tsx deleted file mode 100644 index 9eced1028e..0000000000 --- a/packages/remirror__react-components/src/components/dialog-component.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import React, { FC, PropsWithChildren, useEffect } from 'react'; -import { Dialog, DialogBackdrop, useDialogState } from 'reakit'; - -import { useTheme, UseThemeProps } from '../providers'; -import { usePrevious } from './use-previous'; - -interface ControlledDialogProps { - visible: boolean; - onUpdate: (visible: boolean) => void; - backdrop?: boolean; -} - -/** - * A controlled version of the Reakit `Dialog` component. - */ -export const ControlledDialogComponent: FC> = (props) => { - const { visible, children, backdrop = false, onUpdate } = props; - const previousVisible = usePrevious(visible); - const dialogState = useDialogState({ visible, modal: false }); - const themeProps = useTheme({}); - - useEffect(() => { - if (visible !== previousVisible) { - if (visible) { - dialogState.show(); - } else { - dialogState.hide(); - } - - return; - } - - if (visible !== dialogState.visible) { - onUpdate(dialogState.visible); - } - }, [onUpdate, previousVisible, visible, dialogState]); - - const dialog = ( - // - - {children} - - // - ); - - return backdrop ? {dialog} : dialog; -}; - -const Themed: FC> = (props) => { - const { children, ...themeProps } = props; - const { style, className } = useTheme(themeProps); - - return ( -
- {children} -
- ); -}; diff --git a/packages/remirror__react-components/src/components/index.ts b/packages/remirror__react-components/src/components/index.ts deleted file mode 100644 index 4da7d5cdcf..0000000000 --- a/packages/remirror__react-components/src/components/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export * from './color-picker-component'; -export * from './command-icon-component'; -export * from './dialog-component'; -export * from './emoji-popup-component'; -export * from './mention-atom-popup-component'; -export * from './table-size-editor-component'; diff --git a/packages/remirror__react-components/src/components/table-size-editor-component.tsx b/packages/remirror__react-components/src/components/table-size-editor-component.tsx deleted file mode 100644 index d46e57ffdf..0000000000 --- a/packages/remirror__react-components/src/components/table-size-editor-component.tsx +++ /dev/null @@ -1,257 +0,0 @@ -/** - * @module - * - * A grid size component for creating a table of a specific size into the - * editor. - */ -import type { ComponentType, MouseEvent as ReactMouseEvent } from 'react'; -import React, { createRef, Dispatch, SetStateAction, useEffect, useRef, useState } from 'react'; -import { clamp, cx } from '@remirror/core'; -import { Rect } from '@remirror/extension-positioner'; -import { ComponentsTheme } from '@remirror/theme'; - -/** - * The component used to insert a table into the editor of a given size. - */ -export const TableSizeEditorComponent = ({ - MetricsComponent, - ...props -}: TableSizeEditorProps): JSX.Element => { - const [state, setState] = useState({ columns: 1, rows: 1 }); - const instance = useRef(new TableSizeRef({ props, state, setState })); - instance.current.props = props; - instance.current.state = state; - - useEffect(() => { - return instance.current.unmount; - }, []); - - let rows = Math.max(5, state.rows); - let columns = Math.max(5, state.columns); - - if (rows === state.rows) { - rows = Math.min(MAX_SIZE, rows + 1); - } - - if (columns === state.columns) { - columns = Math.min(MAX_SIZE, columns + 1); - } - - const cells = []; - let ii = 0; - let y = 0; - let width = 0; - let height = 0; - - while (ii < rows) { - y += GUTTER_SIZE; - let jj = 0; - let x = 0; - - while (jj < columns) { - x += GUTTER_SIZE; - const selected = ii < rows && jj < columns; - cells.push( - , - ); - x += CELL_SIZE; - width = x + GUTTER_SIZE; - jj++; - } - - y += CELL_SIZE; - height = y + GUTTER_SIZE; - ii++; - } - - const metrics = MetricsComponent ? ( - - ) : ( - `${rows} x ${columns}` - ); - - return ( -
-
- {cells} -
-
{metrics}
-
- ); -}; - -const GUTTER_SIZE = 5; -const CELL_SIZE = 16; -const MAX_SIZE = 20; - -interface TableSizeEditorCellProps { - x: number; - y: number; - selected: boolean; -} - -const TableSizeEditorCell = (props: TableSizeEditorCellProps): JSX.Element => { - return ( -
- ); -}; - -interface SelectTableSizeState { - columns: number; - rows: number; -} - -type SelectTableSizeCallback = (selected: SelectTableSizeState) => void; - -export interface TableSizeEditorProps { - /** - * Called when a cell is selected. - */ - onSelect: SelectTableSizeCallback; - - /** - * Create a custom display for the metric component. - */ - MetricsComponent?: ComponentType; -} - -interface TableSizeRefProps { - props: TableSizeEditorProps; - state: SelectTableSizeState; - setState: Dispatch>; -} - -class TableSizeRef { - #targetX = 0; - #targetY = 0; - #mouseX = 0; - #mouseY = 0; - #animationFrame = 0; - #entered = false; - - readonly ref = createRef(); - readonly setState: Dispatch>; - - props: TableSizeEditorProps; - state: SelectTableSizeState; - - constructor({ props, state, setState }: TableSizeRefProps) { - this.props = props; - this.state = state; - this.setState = setState; - } - - unmount(): void { - if (this.#entered) { - document.removeEventListener('mousemove', this.onMouseMove, true); - } - - if (this.#animationFrame) { - cancelAnimationFrame(this.#animationFrame); - } - } - - readonly onMouseEnter = (event: ReactMouseEvent): void => { - const node = event.currentTarget; - - if (node instanceof HTMLElement) { - const rect = fromHtmlElement(node); - const mouseX = Math.round(event.clientX); - const mouseY = Math.round(event.clientY); - this.#targetX = rect.x; - this.#targetY = rect.y; - this.#mouseX = mouseX; - this.#mouseY = mouseY; - - if (!this.#entered) { - this.#entered = true; - document.addEventListener('mousemove', this.onMouseMove, true); - } - } - }; - - private readonly onMouseMove = (event: MouseEvent): void => { - if (this.ref.current) { - const elementRect = htmlElementToRect(this.ref.current); - const mouseRect = fromXY(event.screenX, event.screenY, 10); - - if (isIntersected(elementRect, mouseRect, 50)) { - // This prevents `Reakit` from collapsing the editor`. - event.preventDefault(); - event.stopImmediatePropagation(); - } - } - - const mouseX = Math.round(event.clientX); - const mouseY = Math.round(event.clientY); - - if (mouseX !== this.#mouseX || mouseY !== this.#mouseY) { - this.#mouseX = mouseX; - this.#mouseY = mouseY; - this.#animationFrame && cancelAnimationFrame(this.#animationFrame); - this.#animationFrame = requestAnimationFrame(this.updateGridSize); - } - }; - - readonly onMouseDown = (event: ReactMouseEvent): void => { - event.preventDefault(); - this.props.onSelect(this.state); - }; - - private readonly updateGridSize = (): void => { - this.#animationFrame = 0; - const x = this.#mouseX - this.#targetX; - const y = this.#mouseY - this.#targetY; - const rows = clamp({ min: 1, max: MAX_SIZE, value: Math.ceil(y / (CELL_SIZE + GUTTER_SIZE)) }); - const columns = clamp({ - min: 1, - max: MAX_SIZE, - value: Math.ceil(x / (CELL_SIZE + GUTTER_SIZE)), - }); - - if (this.state.rows !== rows || this.state.columns !== columns) { - this.setState({ rows: rows, columns: columns }); - } - }; -} - -function fromHtmlElement(element: HTMLElement): Rect { - const display = document.defaultView?.getComputedStyle(element).display; - - if (display === 'contents' && element.children.length === 1) { - // el has no layout at all, use its children instead. - return fromHtmlElement(element.children[0] as HTMLElement); - } - - return htmlElementToRect(element); -} - -function htmlElementToRect(element: HTMLElement): Rect { - const rect = element.getBoundingClientRect(); - - return { x: rect.left, y: rect.top, width: rect.width, height: rect.height }; -} - -function isIntersected(r1: Rect, r2: Rect, padding = 0): boolean { - return !( - r2.x - padding > r1.x + r1.width + padding || - r2.x + r2.width + padding < r1.x - padding || - r2.y - padding > r1.y + r1.height + padding || - r2.y + r2.height + padding < r1.y - padding - ); -} - -function fromXY(x: number, y: number, padding = 0): Rect { - return { x: x - padding, y: y - padding, width: padding * 2, height: padding * 2 }; -} diff --git a/packages/remirror__react-components/src/components/use-previous.ts b/packages/remirror__react-components/src/components/use-previous.ts deleted file mode 100644 index 1bf9b64ec8..0000000000 --- a/packages/remirror__react-components/src/components/use-previous.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { useRef } from 'react'; - -import { useIsomorphicLayoutEffect } from './use-isomorphic-layout-effect'; - -export function usePrevious(value: T): T | undefined { - const ref = useRef(); - useIsomorphicLayoutEffect(() => { - ref.current = value; - }); - return ref.current; -} diff --git a/packages/remirror__react-components/src/floating-menu.tsx b/packages/remirror__react-components/src/floating-menu.tsx index db335fea4f..0ebdfc40b0 100644 --- a/packages/remirror__react-components/src/floating-menu.tsx +++ b/packages/remirror__react-components/src/floating-menu.tsx @@ -1,31 +1,15 @@ import { Placement } from '@popperjs/core'; -import { matchSorter } from 'match-sorter'; import React, { FC, PropsWithChildren, ReactChild, Ref, useMemo } from 'react'; import { createPortal } from 'react-dom'; -import { useMenuState } from 'reakit'; -import { cx, Except } from '@remirror/core'; +import { cx } from '@remirror/core'; import type { PositionerParam } from '@remirror/extension-positioner'; import { getPositioner } from '@remirror/extension-positioner'; import { useHelpers } from '@remirror/react-core'; -import { - useEditorFocus, - UseEditorFocusProps, - usePositioner, - useSuggest, -} from '@remirror/react-hooks'; +import { useEditorFocus, UseEditorFocusProps, usePositioner } from '@remirror/react-hooks'; import { ComponentsTheme, ExtensionPositionerTheme } from '@remirror/theme'; import { composeRefs } from './commonjs-packages/seznam-compose-react-refs'; -import { useIsomorphicLayoutEffect } from './components/use-isomorphic-layout-effect'; -import { MenuComponent } from './menu'; -import { - ComponentItem, - MenuActionItemUnion, - MenuCommandPaneItem, - MenuPaneItem, - ToolbarItem, -} from './react-component-types'; -import { Toolbar } from './toolbar'; +import { useIsomorphicLayoutEffect } from './use-isomorphic-layout-effect'; import { usePopper } from './use-popper'; interface BaseFloatingPositioner extends UseEditorFocusProps { @@ -175,40 +159,6 @@ export const FloatingWrapper: FC> = ( ); }; -interface FloatingToolbarProps extends Except, FloatingWrapperProps {} - -export const FloatingToolbar = (props: FloatingToolbarProps): JSX.Element => { - const { - placement, - positioner, - animated = 200, - animatedClass = ComponentsTheme.ANIMATED_POPOVER, - containerClass, - blurOnInactive, - enabled, - floatingLabel, - ignoredElements, - ...toolbarProps - } = props; - const floatingWrapperProps = { - placement, - positioner, - animated, - animatedClass, - containerClass, - blurOnInactive, - enabled, - floatingLabel, - ignoredElements, - }; - - return ( - - - - ); -}; - export interface PositionerComponentProps { children: ReactChild; } @@ -223,53 +173,53 @@ export const PositionerPortal: FC = (props) => { return createPortal(<>{props.children}, container); }; -interface FloatingActionsMenuProps extends Partial { - actions: MenuActionItemUnion[]; -} +// interface FloatingActionsMenuProps extends Partial { +// actions: MenuActionItemUnion[]; +// } /** * Respond to user queries in the editor. */ -export const FloatingActionsMenu = (props: FloatingActionsMenuProps): JSX.Element => { - const { - actions, - animated = false, - placement = 'right-end', - positioner = 'nearestWord', - blurOnInactive, - ignoredElements, - enabled = true, - ...floatingWrapperProps - } = props; - const { change } = useSuggest({ char: '/', name: 'actions-dropdown', matchOffset: 0 }); - const query = change?.query.full; - const menuState = useMenuState({ unstable_virtual: true, wrap: true, loop: true }); - - const items = ( - query - ? matchSorter(actions, query, { - keys: ['tags', 'description', (item) => item.description?.replace(/\W/g, '') ?? ''], - threshold: matchSorter.rankings.CONTAINS, - }) - : actions - ).map((item) => - item.type === ComponentItem.MenuAction - ? { ...item, type: ComponentItem.MenuPane } - : { ...item, type: ComponentItem.MenuCommandPane }, - ); - - return ( - -
- - - ); -}; +// export const FloatingActionsMenu = (props: FloatingActionsMenuProps): JSX.Element => { +// const { +// actions, +// animated = false, +// placement = 'right-end', +// positioner = 'nearestWord', +// blurOnInactive, +// ignoredElements, +// enabled = true, +// ...floatingWrapperProps +// } = props; +// const { change } = useSuggest({ char: '/', name: 'actions-dropdown', matchOffset: 0 }); +// const query = change?.query.full; +// const menuState = useMenuState({ unstable_virtual: true, wrap: true, loop: true }); +// +// const items = ( +// query +// ? matchSorter(actions, query, { +// keys: ['tags', 'description', (item) => item.description?.replace(/\W/g, '') ?? ''], +// threshold: matchSorter.rankings.CONTAINS, +// }) +// : actions +// ).map((item) => +// item.type === ComponentItem.MenuAction +// ? { ...item, type: ComponentItem.MenuPane } +// : { ...item, type: ComponentItem.MenuCommandPane }, +// ); +// +// return ( +// +//
+// +// +// ); +// }; diff --git a/packages/remirror__react-components/src/icons/all.ts b/packages/remirror__react-components/src/icons/all.ts index bb775ccee2..dea6647817 100644 --- a/packages/remirror__react-components/src/icons/all.ts +++ b/packages/remirror__react-components/src/icons/all.ts @@ -359,7 +359,6 @@ import { checkboxMultipleBlankFill, checkboxMultipleBlankLine, checkboxMultipleFill, - checkboxMultipleLine, checkDoubleFill, checkDoubleLine, checkFill, @@ -5059,14 +5058,6 @@ export const CheckboxMultipleFillIcon: IconType = (props) => { return GenIcon(checkboxMultipleFill)(props); }; -/** - * The react component for the `checkbox-multiple-line.svg` icon created by [RemixIcons](https://remixicons.com). - * ![Checkbox Multiple Line Icon](https://cdn.jsdelivr.net/npm/remixicon@2.5.0/icons/System/checkbox-multiple-line.svg) - */ -export const CheckboxMultipleLineIcon: IconType = (props) => { - return GenIcon(checkboxMultipleLine)(props); -}; - /** * The react component for the `china-railway-fill.svg` icon created by [RemixIcons](https://remixicons.com). * ![China Railway Fill Icon](https://cdn.jsdelivr.net/npm/remixicon@2.5.0/icons/Map/china-railway-fill.svg) diff --git a/packages/remirror__react-components/src/icons/core.ts b/packages/remirror__react-components/src/icons/core.ts index a33a4ebc5d..03f389a519 100644 --- a/packages/remirror__react-components/src/icons/core.ts +++ b/packages/remirror__react-components/src/icons/core.ts @@ -27,6 +27,7 @@ import { bringToFront, chatNewLine, checkboxCircleLine, + checkboxMultipleLine, clipboardFill, clipboardLine, closeCircleLine, @@ -358,6 +359,14 @@ export const CheckboxCircleLineIcon: IconType = (props) => { return GenIcon(checkboxCircleLine)(props); }; +/** + * The react component for the `checkbox-multiple-line.svg` icon created by [RemixIcons](https://remixicons.com). + * ![Checkbox Multiple Line Icon](https://cdn.jsdelivr.net/npm/remixicon@2.5.0/icons/System/checkbox-multiple-line.svg) + */ +export const CheckboxMultipleLineIcon: IconType = (props) => { + return GenIcon(checkboxMultipleLine)(props); +}; + /** * The react component for the `clipboard-fill.svg` icon created by [RemixIcons](https://remixicons.com). * ![Clipboard Fill Icon](https://cdn.jsdelivr.net/npm/remixicon@2.5.0/icons/Document/clipboard-fill.svg) diff --git a/packages/remirror__react-components/src/index.ts b/packages/remirror__react-components/src/index.ts index fe20da92b1..601c532672 100644 --- a/packages/remirror__react-components/src/index.ts +++ b/packages/remirror__react-components/src/index.ts @@ -1,9 +1,8 @@ -export * from './components'; +export * from './button-groups'; +export * from './buttons'; export * from './floating-menu'; export * from './icons/icons-base'; -export * from './menu'; +export * from './menus'; +export * from './popups'; export * from './providers'; -export * from './react-component-types'; export * from './toolbar'; -export type { ButtonProps } from 'reakit'; -export { Button } from 'reakit'; diff --git a/packages/remirror__react-components/src/menu/index.tsx b/packages/remirror__react-components/src/menu/index.tsx deleted file mode 100644 index 8f482e9912..0000000000 --- a/packages/remirror__react-components/src/menu/index.tsx +++ /dev/null @@ -1,422 +0,0 @@ -/** - * @module - * - * The builtin menus. - */ -import type { MouseEvent as ReactMouseEvent, ReactNode } from 'react'; -import React, { forwardRef, useEffect, useRef } from 'react'; -import type { MenuBarStateReturn, MenuStateReturn } from 'reakit'; -import { - Box, - Menu as ReakitMenu, - MenuBar as ReakitMenuBar, - MenuButton as ReakitMenuButton, - MenuGroup as ReakitMenuGroup, - MenuItem as ReakitMenuItem, - MenuSeparator, - Tooltip, - TooltipReference, - useMenuBarState, - useMenuState, - useTooltipState, -} from 'reakit'; -import type { AnyExtension, CommandDecoratorMessageProps } from '@remirror/core'; -import { cx, ErrorConstant, includes, invariant, isString } from '@remirror/core'; -import { - useActive, - useChainedCommands, - useCommands, - useHelpers, - useI18n, - useRemirrorContext, -} from '@remirror/react-core'; -import { ComponentsTheme } from '@remirror/theme'; - -import { CommandIconComponent } from '../components'; -import { useTheme } from '../providers'; -import { - ComponentItem, - MenuBarGroupItem, - MenuBarItem, - MenuCommandPaneItem, - MenuDropdownItem, - MenuGroupItem, - MenuPaneItem, - MenuSeparatorItem, -} from '../react-component-types'; -import { - getCommandOptionValue, - getShortcutString, - getUiShortcutString, -} from '../react-component-utils'; - -interface MenuGroupProps extends BaseMenuProps { - item: MenuGroupItem; -} - -const MenuGroup = (props: MenuGroupProps) => { - const { menuState, item: group } = props; - const { items, label, role, separator } = group; - const startSeparator = includes(['start', 'both'], separator) && ; - const endSeparator = includes(['end', 'both'], separator) && ; - - return ( - <> - {startSeparator} - - {items.map((item, index) => { - switch (item.type) { - case ComponentItem.MenuPane: - return ; - - case ComponentItem.MenuCommandPane: - return ; - - case ComponentItem.MenuDropdown: - return ; - } - })} - - {endSeparator} - - ); -}; - -interface MenuPaneProps extends BaseMenuBarProps { - item: MenuPaneItem; -} - -const MenuPane = (props: MenuPaneProps): JSX.Element => { - return ; -}; - -interface MenuCommandPaneProps extends BaseMenuBarProps { - item: MenuCommandPaneItem; - autoFocus?: boolean; - role?: 'radio' | 'checkbox'; -} - -const MenuCommandPane = (props: MenuCommandPaneProps): JSX.Element => { - // Gather all the hooks used for this component. - const context = useRemirrorContext(); - const { menuState, item, role } = props; - const commands = useCommands(); - const chain = useChainedCommands(); - const { getCommandOptions } = useHelpers(); - const { t } = useI18n(); - const autoFocus = item.refocusEditor ?? props.autoFocus ?? false; - - // Will cause the editor to rerender for each state update. - const active = useActive(); - const { commandName, attrs, displayShortcut = true } = item; - const options = getCommandOptions(commandName); - - if (!options) { - return Not found: {commandName}; - } - - const enabled = commands[commandName]?.isEnabled(attrs) ?? false; - const isActive = active[options.name]?.(attrs) ?? false; - const commandProps: CommandDecoratorMessageProps = { active: isActive, attrs, enabled, t }; - const label = getCommandOptionValue(options.label, commandProps); - const icon = getCommandOptionValue(options.icon, commandProps); - const shortcutString = - displayShortcut && options.shortcut - ? `${getShortcutString(getUiShortcutString(options.shortcut, attrs ?? {}), { t })}` - : undefined; - const onClick = (event: ReactMouseEvent) => { - if (item.onClick) { - item.onClick(event, context); - return; - } - - if (options.disableChaining) { - commands[commandName]?.(attrs); - return; - } - - const command = chain[commandName]?.(attrs); - - if (autoFocus) { - command?.focus?.(); - } - - command?.run(); - }; - - return ( - - {icon && ( - - - - )} - - {label} - - {shortcutString && ( - - {shortcutString} - - )} - - ); -}; - -// type MenuDropdownProps = SubMenuDropdownProps | MenuBarDropdownProps; - -interface MenuDropdownProps extends BaseMenuBarProps { - item: MenuDropdownItem; -} - -const MenuDropdown = (props: MenuDropdownProps): JSX.Element => { - const { menuState, item } = props; - const { items, label, menuLabel } = item; - - return ( - - ); -}; - -interface DropdownComponentProps - extends Omit, - Partial { - menuItems: Array< - MenuPaneItem | MenuSeparatorItem | MenuCommandPaneItem | MenuGroupItem | MenuDropdownItem - >; -} - -/** - * A dropdown menu which can be composed together with other dropdown menus. - */ -export const DropdownComponent = forwardRef( - (props, ref) => { - const menuState = useMenuState(); - const { menuItems, label: Label, menuLabel, icon, ...rest } = props; - - let labelElement: ReactNode | undefined; - let label: string | undefined = menuLabel; - - if (isString(Label)) { - label ??= Label; - } else if (Label != null) { - labelElement =