78
common/resources/client/text-editor/menubar/align-buttons.tsx
Executable file
78
common/resources/client/text-editor/menubar/align-buttons.tsx
Executable file
@@ -0,0 +1,78 @@
|
||||
import clsx from 'clsx';
|
||||
import {ComponentType} from 'react';
|
||||
import {FormatAlignLeftIcon} from '../../icons/material/FormatAlignLeft';
|
||||
import {FormatAlignCenterIcon} from '../../icons/material/FormatAlignCenter';
|
||||
import {FormatAlignRightIcon} from '../../icons/material/FormatAlignRight';
|
||||
import {FormatAlignJustifyIcon} from '../../icons/material/FormatAlignJustify';
|
||||
import {MenubarButtonProps} from './menubar-button-props';
|
||||
import {IconButton} from '../../ui/buttons/icon-button';
|
||||
import {
|
||||
Menu,
|
||||
MenuItem,
|
||||
MenuTrigger,
|
||||
} from '../../ui/navigation/menu/menu-trigger';
|
||||
import {Trans} from '../../i18n/trans';
|
||||
import {message} from '../../i18n/message';
|
||||
|
||||
const iconMap = {
|
||||
left: {
|
||||
icon: FormatAlignLeftIcon,
|
||||
label: message('Align left'),
|
||||
},
|
||||
center: {
|
||||
icon: FormatAlignCenterIcon,
|
||||
label: message('Align center'),
|
||||
},
|
||||
right: {
|
||||
icon: FormatAlignRightIcon,
|
||||
label: message('Align right'),
|
||||
},
|
||||
justify: {
|
||||
icon: FormatAlignJustifyIcon,
|
||||
label: message('Justify'),
|
||||
},
|
||||
};
|
||||
|
||||
export function AlignButtons({editor, size}: MenubarButtonProps) {
|
||||
const activeKey = (Object.keys(iconMap).find(key => {
|
||||
return editor.isActive({textAlign: key});
|
||||
}) || 'left') as keyof typeof iconMap;
|
||||
const ActiveIcon: ComponentType = activeKey
|
||||
? iconMap[activeKey].icon
|
||||
: iconMap.left.icon;
|
||||
|
||||
return (
|
||||
<MenuTrigger
|
||||
floatingWidth="auto"
|
||||
selectionMode="single"
|
||||
selectedValue={activeKey}
|
||||
onSelectionChange={key => {
|
||||
editor.commands.focus();
|
||||
editor.commands.setTextAlign(key as string);
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
size={size}
|
||||
color={activeKey ? 'primary' : null}
|
||||
className={clsx('flex-shrink-0')}
|
||||
>
|
||||
<ActiveIcon />
|
||||
</IconButton>
|
||||
<Menu>
|
||||
{Object.entries(iconMap).map(([name, config]) => {
|
||||
const Icon = config.icon;
|
||||
return (
|
||||
<MenuItem
|
||||
key={name}
|
||||
value={name}
|
||||
startIcon={<Icon size="md" />}
|
||||
capitalizeFirst
|
||||
>
|
||||
<Trans message={config.label.message} />
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</Menu>
|
||||
</MenuTrigger>
|
||||
);
|
||||
}
|
||||
23
common/resources/client/text-editor/menubar/clear-format-button.tsx
Executable file
23
common/resources/client/text-editor/menubar/clear-format-button.tsx
Executable file
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import {FormatClearIcon} from '../../icons/material/FormatClear';
|
||||
import {IconButton} from '../../ui/buttons/icon-button';
|
||||
import {MenubarButtonProps} from './menubar-button-props';
|
||||
import {Tooltip} from '@common/ui/tooltip/tooltip';
|
||||
import {Trans} from '@common/i18n/trans';
|
||||
|
||||
export function ClearFormatButton({editor, size}: MenubarButtonProps) {
|
||||
return (
|
||||
<Tooltip label={<Trans message="Clear formatting" />}>
|
||||
<IconButton
|
||||
className={clsx('flex-shrink-0')}
|
||||
size={size}
|
||||
onClick={() => {
|
||||
editor.chain().focus().clearNodes().unsetAllMarks().run();
|
||||
}}
|
||||
>
|
||||
<FormatClearIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
47
common/resources/client/text-editor/menubar/code-block-menu-trigger.tsx
Executable file
47
common/resources/client/text-editor/menubar/code-block-menu-trigger.tsx
Executable file
@@ -0,0 +1,47 @@
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import {IconButton} from '../../ui/buttons/icon-button';
|
||||
import {CodeIcon} from '../../icons/material/Code';
|
||||
import {MenubarButtonProps} from './menubar-button-props';
|
||||
import {
|
||||
Menu,
|
||||
MenuItem,
|
||||
MenuTrigger,
|
||||
} from '../../ui/navigation/menu/menu-trigger';
|
||||
import {Tooltip} from '@common/ui/tooltip/tooltip';
|
||||
import {Trans} from '@common/i18n/trans';
|
||||
|
||||
export function CodeBlockMenuTrigger({editor, size}: MenubarButtonProps) {
|
||||
const language = editor.getAttributes('codeBlock').language || '';
|
||||
return (
|
||||
<MenuTrigger
|
||||
selectionMode="single"
|
||||
selectedValue={language}
|
||||
onSelectionChange={key => {
|
||||
editor.commands.toggleCodeBlock({language: key as string});
|
||||
}}
|
||||
>
|
||||
<Tooltip label={<Trans message="Codeblock" />}>
|
||||
<IconButton
|
||||
className={clsx('flex-shrink-0')}
|
||||
size={size}
|
||||
color={language ? 'primary' : null}
|
||||
>
|
||||
<CodeIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Menu>
|
||||
<MenuItem value="html">HTML</MenuItem>
|
||||
<MenuItem value="javascript">JavaScript</MenuItem>
|
||||
<MenuItem value="css">CSS</MenuItem>
|
||||
<MenuItem value="php">PHP</MenuItem>
|
||||
<MenuItem value="shell">Shell</MenuItem>
|
||||
<MenuItem value="bash">Bash</MenuItem>
|
||||
<MenuItem value="ruby">Ruby</MenuItem>
|
||||
<MenuItem value="python">Python</MenuItem>
|
||||
<MenuItem value="java">Java</MenuItem>
|
||||
<MenuItem value="c++">C++</MenuItem>
|
||||
</Menu>
|
||||
</MenuTrigger>
|
||||
);
|
||||
}
|
||||
55
common/resources/client/text-editor/menubar/color-buttons.tsx
Executable file
55
common/resources/client/text-editor/menubar/color-buttons.tsx
Executable file
@@ -0,0 +1,55 @@
|
||||
import React, {Fragment, useState} from 'react';
|
||||
import clsx from 'clsx';
|
||||
import {FormatColorTextIcon} from '../../icons/material/FormatColorText';
|
||||
import {ColorPickerDialog} from '../../ui/color-picker/color-picker-dialog';
|
||||
import {MenubarButtonProps} from './menubar-button-props';
|
||||
import {IconButton} from '../../ui/buttons/icon-button';
|
||||
import {FormatColorFillIcon} from '../../icons/material/FormatColorFill';
|
||||
import {DialogTrigger} from '../../ui/overlays/dialog/dialog-trigger';
|
||||
|
||||
export function ColorButtons({editor, size}: MenubarButtonProps) {
|
||||
const [dialog, setDialog] = useState<'text' | 'bg' | false>(false);
|
||||
const textActive = editor.getAttributes('textStyle').color;
|
||||
const backgroundActive = editor.getAttributes('textStyle').backgroundColor;
|
||||
return (
|
||||
<Fragment>
|
||||
<span className={clsx('flex-shrink-0 whitespace-nowrap')}>
|
||||
<IconButton
|
||||
size={size}
|
||||
color={textActive ? 'primary' : null}
|
||||
onClick={() => {
|
||||
setDialog('text');
|
||||
}}
|
||||
>
|
||||
<FormatColorTextIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size={size}
|
||||
color={backgroundActive ? 'primary' : null}
|
||||
onClick={() => {
|
||||
setDialog('bg');
|
||||
}}
|
||||
>
|
||||
<FormatColorFillIcon />
|
||||
</IconButton>
|
||||
</span>
|
||||
<DialogTrigger
|
||||
defaultValue={dialog === 'text' ? '#000000' : '#FFFFFF'}
|
||||
type="modal"
|
||||
isOpen={!!dialog}
|
||||
onClose={newValue => {
|
||||
if (newValue) {
|
||||
if (dialog === 'text') {
|
||||
editor.commands.setColor(newValue);
|
||||
} else {
|
||||
editor.commands.setBackgroundColor(newValue);
|
||||
}
|
||||
}
|
||||
setDialog(false);
|
||||
}}
|
||||
>
|
||||
<ColorPickerDialog />
|
||||
</DialogTrigger>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
3
common/resources/client/text-editor/menubar/divider.tsx
Executable file
3
common/resources/client/text-editor/menubar/divider.tsx
Executable file
@@ -0,0 +1,3 @@
|
||||
export function Divider() {
|
||||
return <div className="self-stretch mx-4 w-1 bg-divider flex-shrink-0" />;
|
||||
}
|
||||
52
common/resources/client/text-editor/menubar/font-style-buttons.tsx
Executable file
52
common/resources/client/text-editor/menubar/font-style-buttons.tsx
Executable file
@@ -0,0 +1,52 @@
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import {IconButton} from '../../ui/buttons/icon-button';
|
||||
import {FormatBoldIcon} from '../../icons/material/FormatBold';
|
||||
import {FormatItalicIcon} from '../../icons/material/FormatItalic';
|
||||
import {FormatUnderlinedIcon} from '../../icons/material/FormatUnderlined';
|
||||
import {MenubarButtonProps} from './menubar-button-props';
|
||||
import {Tooltip} from '@common/ui/tooltip/tooltip';
|
||||
import {Trans} from '@common/i18n/trans';
|
||||
|
||||
export function FontStyleButtons({editor, size}: MenubarButtonProps) {
|
||||
return (
|
||||
<span className={clsx('flex-shrink-0 whitespace-nowrap')}>
|
||||
<Tooltip label={<Trans message="Bold" />}>
|
||||
<IconButton
|
||||
size={size}
|
||||
color={editor.isActive('bold') ? 'primary' : null}
|
||||
onClick={() => {
|
||||
editor.commands.focus();
|
||||
editor.commands.toggleBold();
|
||||
}}
|
||||
>
|
||||
<FormatBoldIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip label={<Trans message="Italic" />}>
|
||||
<IconButton
|
||||
size={size}
|
||||
color={editor.isActive('italic') ? 'primary' : null}
|
||||
onClick={() => {
|
||||
editor.commands.focus();
|
||||
editor.commands.toggleItalic();
|
||||
}}
|
||||
>
|
||||
<FormatItalicIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip label={<Trans message="Underline" />}>
|
||||
<IconButton
|
||||
size={size}
|
||||
color={editor.isActive('underline') ? 'primary' : null}
|
||||
onClick={() => {
|
||||
editor.commands.focus();
|
||||
editor.commands.toggleUnderline();
|
||||
}}
|
||||
>
|
||||
<FormatUnderlinedIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
106
common/resources/client/text-editor/menubar/format-menu-trigger.tsx
Executable file
106
common/resources/client/text-editor/menubar/format-menu-trigger.tsx
Executable file
@@ -0,0 +1,106 @@
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import {Button} from '../../ui/buttons/button';
|
||||
import {KeyboardArrowDownIcon} from '../../icons/material/KeyboardArrowDown';
|
||||
import {Keyboard} from '../../ui/keyboard/keyboard';
|
||||
import {MenubarButtonProps} from './menubar-button-props';
|
||||
import {
|
||||
Menu,
|
||||
MenuItem,
|
||||
MenuTrigger,
|
||||
} from '../../ui/navigation/menu/menu-trigger';
|
||||
import {Trans} from '../../i18n/trans';
|
||||
|
||||
type Level = 1 | 2 | 3 | 4;
|
||||
|
||||
export function FormatMenuTrigger({editor, size}: MenubarButtonProps) {
|
||||
return (
|
||||
<MenuTrigger
|
||||
floatingMinWidth="w-256"
|
||||
onItemSelected={key => {
|
||||
editor.commands.focus();
|
||||
if (typeof key === 'string' && key.startsWith('h')) {
|
||||
editor.commands.toggleHeading({
|
||||
level: parseInt(key.replace('h', '')) as Level,
|
||||
});
|
||||
} else if (key === 'code') {
|
||||
editor.commands.toggleCode();
|
||||
} else if (key === 'strike') {
|
||||
editor.commands.toggleStrike();
|
||||
} else if (key === 'super') {
|
||||
editor.commands.toggleSuperscript();
|
||||
} else if (key === 'sub') {
|
||||
editor.commands.toggleSubscript();
|
||||
} else if (key === 'blockquote') {
|
||||
editor.commands.toggleBlockquote();
|
||||
} else if (key === 'paragraph') {
|
||||
editor.commands.setParagraph();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
className={clsx('flex-shrink-0')}
|
||||
variant="text"
|
||||
size={size}
|
||||
endIcon={<KeyboardArrowDownIcon />}
|
||||
>
|
||||
<Trans message="Format" />
|
||||
</Button>
|
||||
<Menu>
|
||||
<MenuItem value="h1" endSection={<Keyboard modifier>Alt+1</Keyboard>}>
|
||||
<Trans message="Heading :number" values={{number: 1}} />
|
||||
</MenuItem>
|
||||
<MenuItem value="h2" endSection={<Keyboard modifier>Alt+2</Keyboard>}>
|
||||
<Trans message="Heading :number" values={{number: 2}} />
|
||||
</MenuItem>
|
||||
<MenuItem value="h3" endSection={<Keyboard modifier>Alt+3</Keyboard>}>
|
||||
<Trans message="Heading :number" values={{number: 3}} />
|
||||
</MenuItem>
|
||||
<MenuItem value="h4" endSection={<Keyboard modifier>Alt+4</Keyboard>}>
|
||||
<Trans message="Heading :number" values={{number: 4}} />
|
||||
</MenuItem>
|
||||
<MenuItem value="code" endSection={<Keyboard modifier>E</Keyboard>}>
|
||||
<Trans message="Code" />
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
value="strike"
|
||||
endSection={<Keyboard modifier>Shift+X</Keyboard>}
|
||||
>
|
||||
<Trans message="Strikethrough" />
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
value="super"
|
||||
endSection={
|
||||
<Keyboard modifier separator=" ">
|
||||
.
|
||||
</Keyboard>
|
||||
}
|
||||
>
|
||||
<Trans message="Superscript" />
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
value="sub"
|
||||
endSection={
|
||||
<Keyboard modifier separator=" ">
|
||||
,
|
||||
</Keyboard>
|
||||
}
|
||||
>
|
||||
<Trans message="Subscript" />
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
value="blockquote"
|
||||
endSection={<Keyboard modifier>Shift+B</Keyboard>}
|
||||
>
|
||||
<Trans message="Blockquote" />
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
value="paragraph"
|
||||
endSection={<Keyboard modifier>Alt+0</Keyboard>}
|
||||
>
|
||||
<Trans message="Paragraph" />
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</MenuTrigger>
|
||||
);
|
||||
}
|
||||
32
common/resources/client/text-editor/menubar/history-buttons.tsx
Executable file
32
common/resources/client/text-editor/menubar/history-buttons.tsx
Executable file
@@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
import {IconButton} from '../../ui/buttons/icon-button';
|
||||
import {UndoIcon} from '../../icons/material/Undo';
|
||||
import {RedoIcon} from '../../icons/material/Redo';
|
||||
import {MenubarButtonProps} from './menubar-button-props';
|
||||
|
||||
export function HistoryButtons({editor}: MenubarButtonProps) {
|
||||
return (
|
||||
<span>
|
||||
<IconButton
|
||||
size="md"
|
||||
disabled={!editor.can().undo()}
|
||||
onClick={() => {
|
||||
editor.commands.focus();
|
||||
editor.commands.undo();
|
||||
}}
|
||||
>
|
||||
<UndoIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size="md"
|
||||
disabled={!editor.can().redo()}
|
||||
onClick={() => {
|
||||
editor.commands.focus();
|
||||
editor.commands.redo();
|
||||
}}
|
||||
>
|
||||
<RedoIcon />
|
||||
</IconButton>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
52
common/resources/client/text-editor/menubar/image-button.tsx
Executable file
52
common/resources/client/text-editor/menubar/image-button.tsx
Executable file
@@ -0,0 +1,52 @@
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import {IconButton} from '../../ui/buttons/icon-button';
|
||||
import {ImageIcon} from '../../icons/material/Image';
|
||||
import {MenubarButtonProps} from './menubar-button-props';
|
||||
import {useActiveUpload} from '../../uploads/uploader/use-active-upload';
|
||||
import {UploadInputType} from '../../uploads/types/upload-input-config';
|
||||
import {Disk} from '../../uploads/types/backend-metadata';
|
||||
import {Tooltip} from '@common/ui/tooltip/tooltip';
|
||||
import {Trans} from '@common/i18n/trans';
|
||||
|
||||
const TwoMB = 2097152;
|
||||
|
||||
interface Props extends MenubarButtonProps {
|
||||
diskPrefix?: string;
|
||||
}
|
||||
|
||||
export function ImageButton({editor, size, diskPrefix = 'page_media'}: Props) {
|
||||
const {selectAndUploadFile} = useActiveUpload();
|
||||
|
||||
const handleUpload = () => {
|
||||
selectAndUploadFile({
|
||||
showToastOnRestrictionFail: true,
|
||||
restrictions: {
|
||||
allowedFileTypes: [UploadInputType.image],
|
||||
maxFileSize: TwoMB,
|
||||
},
|
||||
metadata: {
|
||||
diskPrefix: diskPrefix,
|
||||
disk: Disk.public,
|
||||
},
|
||||
onSuccess: entry => {
|
||||
editor.commands.focus();
|
||||
editor.commands.setImage({
|
||||
src: entry.url,
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Tooltip label={<Trans message="Insert image" />}>
|
||||
<IconButton
|
||||
size={size}
|
||||
onClick={handleUpload}
|
||||
className={clsx('flex-shrink-0')}
|
||||
>
|
||||
<ImageIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
37
common/resources/client/text-editor/menubar/indent-buttons.tsx
Executable file
37
common/resources/client/text-editor/menubar/indent-buttons.tsx
Executable file
@@ -0,0 +1,37 @@
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import {IconButton} from '../../ui/buttons/icon-button';
|
||||
import {FormatIndentDecreaseIcon} from '../../icons/material/FormatIndentDecrease';
|
||||
import {FormatIndentIncreaseIcon} from '../../icons/material/FormatIndentIncrease';
|
||||
import {MenubarButtonProps} from './menubar-button-props';
|
||||
import {Tooltip} from '@common/ui/tooltip/tooltip';
|
||||
import {Trans} from '@common/i18n/trans';
|
||||
|
||||
export function IndentButtons({editor, size}: MenubarButtonProps) {
|
||||
return (
|
||||
<span className={clsx('flex-shrink-0', 'whitespace-nowrap')}>
|
||||
<Tooltip label={<Trans message="Decrease indent" />}>
|
||||
<IconButton
|
||||
size={size}
|
||||
onClick={() => {
|
||||
editor.commands.focus();
|
||||
editor.commands.outdent();
|
||||
}}
|
||||
>
|
||||
<FormatIndentDecreaseIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip label={<Trans message="Increase indent" />}>
|
||||
<IconButton
|
||||
size={size}
|
||||
onClick={() => {
|
||||
editor.commands.focus();
|
||||
editor.commands.indent();
|
||||
}}
|
||||
>
|
||||
<FormatIndentIncreaseIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
128
common/resources/client/text-editor/menubar/insert-menu-trigger.tsx
Executable file
128
common/resources/client/text-editor/menubar/insert-menu-trigger.tsx
Executable file
@@ -0,0 +1,128 @@
|
||||
import React, {useState} from 'react';
|
||||
import {useForm} from 'react-hook-form';
|
||||
import clsx from 'clsx';
|
||||
import {HorizontalRuleIcon} from '../../icons/material/HorizontalRule';
|
||||
import {PriorityHighIcon} from '../../icons/material/PriorityHigh';
|
||||
import {WarningIcon} from '../../icons/material/Warning';
|
||||
import {NoteIcon} from '../../icons/material/Note';
|
||||
import {MenubarButtonProps} from './menubar-button-props';
|
||||
import {IconButton} from '../../ui/buttons/icon-button';
|
||||
import {MoreVertIcon} from '../../icons/material/MoreVert';
|
||||
import {SmartDisplayIcon} from '../../icons/material/SmartDisplay';
|
||||
import {Form} from '../../ui/forms/form';
|
||||
import {FormTextField} from '../../ui/forms/input-field/text-field/text-field';
|
||||
import {DialogFooter} from '../../ui/overlays/dialog/dialog-footer';
|
||||
import {Button} from '../../ui/buttons/button';
|
||||
import {
|
||||
Menu,
|
||||
MenuItem,
|
||||
MenuTrigger,
|
||||
} from '../../ui/navigation/menu/menu-trigger';
|
||||
import {DialogTrigger} from '../../ui/overlays/dialog/dialog-trigger';
|
||||
import {useDialogContext} from '../../ui/overlays/dialog/dialog-context';
|
||||
import {Dialog} from '../../ui/overlays/dialog/dialog';
|
||||
import {DialogHeader} from '../../ui/overlays/dialog/dialog-header';
|
||||
import {DialogBody} from '../../ui/overlays/dialog/dialog-body';
|
||||
import {Trans} from '../../i18n/trans';
|
||||
|
||||
export function InsertMenuTrigger({editor, size}: MenubarButtonProps) {
|
||||
const [dialog, setDialog] = useState<'embed' | false>(false);
|
||||
return (
|
||||
<>
|
||||
<MenuTrigger
|
||||
onItemSelected={key => {
|
||||
if (key === 'hr') {
|
||||
editor.commands.focus();
|
||||
editor.commands.setHorizontalRule();
|
||||
} else if (key === 'embed') {
|
||||
setDialog('embed');
|
||||
} else {
|
||||
editor.commands.focus();
|
||||
editor.commands.addInfo({type: key as any});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
variant="text"
|
||||
size={size}
|
||||
className={clsx('flex-shrink-0')}
|
||||
>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
<Menu>
|
||||
<MenuItem value="hr" startIcon={<HorizontalRuleIcon />}>
|
||||
<Trans message="Horizontal rule" />
|
||||
</MenuItem>
|
||||
<MenuItem value="embed" startIcon={<SmartDisplayIcon />}>
|
||||
<Trans message="Embed" />
|
||||
</MenuItem>
|
||||
<MenuItem value="important" startIcon={<PriorityHighIcon />}>
|
||||
<Trans message="Important" />
|
||||
</MenuItem>
|
||||
<MenuItem value="warning" startIcon={<WarningIcon />}>
|
||||
<Trans message="Warning" />
|
||||
</MenuItem>
|
||||
<MenuItem value="success" startIcon={<NoteIcon />}>
|
||||
<Trans message="Note" />
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</MenuTrigger>
|
||||
<DialogTrigger
|
||||
type="modal"
|
||||
isOpen={!!dialog}
|
||||
onClose={() => {
|
||||
setDialog(false);
|
||||
}}
|
||||
>
|
||||
<EmbedDialog editor={editor} />
|
||||
</DialogTrigger>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function EmbedDialog({editor}: MenubarButtonProps) {
|
||||
const previousSrc = editor.getAttributes('embed').src;
|
||||
const form = useForm<{src: string}>({
|
||||
defaultValues: {src: previousSrc},
|
||||
});
|
||||
const {formId, close} = useDialogContext();
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogHeader>
|
||||
<Trans message="Insert link" />
|
||||
</DialogHeader>
|
||||
<DialogBody>
|
||||
<Form
|
||||
form={form}
|
||||
id={formId}
|
||||
onSubmit={value => {
|
||||
editor.commands.setEmbed(value);
|
||||
close();
|
||||
}}
|
||||
>
|
||||
<FormTextField
|
||||
name="src"
|
||||
label={<Trans message="Embed URL" />}
|
||||
autoFocus
|
||||
type="url"
|
||||
required
|
||||
/>
|
||||
</Form>
|
||||
</DialogBody>
|
||||
<DialogFooter>
|
||||
<Button onClick={close} variant="text">
|
||||
<Trans message="Cancel" />
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
form={formId}
|
||||
disabled={!form.formState.isValid}
|
||||
variant="flat"
|
||||
color="primary"
|
||||
>
|
||||
<Trans message="Add" />
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
102
common/resources/client/text-editor/menubar/link-button.tsx
Executable file
102
common/resources/client/text-editor/menubar/link-button.tsx
Executable file
@@ -0,0 +1,102 @@
|
||||
import {useForm} from 'react-hook-form';
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import {Form} from '../../ui/forms/form';
|
||||
import {FormTextField} from '../../ui/forms/input-field/text-field/text-field';
|
||||
import {DialogFooter} from '../../ui/overlays/dialog/dialog-footer';
|
||||
import {Button} from '../../ui/buttons/button';
|
||||
import {IconButton} from '../../ui/buttons/icon-button';
|
||||
import {LinkIcon} from '../../icons/material/Link';
|
||||
import {MenubarButtonProps} from './menubar-button-props';
|
||||
import {DialogTrigger} from '../../ui/overlays/dialog/dialog-trigger';
|
||||
import {FormSelect, Option} from '../../ui/forms/select/select';
|
||||
import {useDialogContext} from '../../ui/overlays/dialog/dialog-context';
|
||||
import {Dialog} from '../../ui/overlays/dialog/dialog';
|
||||
import {DialogHeader} from '../../ui/overlays/dialog/dialog-header';
|
||||
import {DialogBody} from '../../ui/overlays/dialog/dialog-body';
|
||||
import {Trans} from '../../i18n/trans';
|
||||
import {Tooltip} from '@common/ui/tooltip/tooltip';
|
||||
import {insertLinkIntoTextEditor} from '@common/text-editor/insert-link-into-text-editor';
|
||||
|
||||
interface FormValue {
|
||||
href: string;
|
||||
target?: string;
|
||||
text?: string;
|
||||
}
|
||||
|
||||
export function LinkButton({editor, size}: MenubarButtonProps) {
|
||||
return (
|
||||
<DialogTrigger type="modal">
|
||||
<Tooltip label={<Trans message="Insert link" />}>
|
||||
<IconButton size={size} className={clsx('flex-shrink-0')}>
|
||||
<LinkIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<LinkDialog editor={editor} />
|
||||
</DialogTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
function LinkDialog({editor}: MenubarButtonProps) {
|
||||
const previousUrl = editor.getAttributes('link').href;
|
||||
const previousText = editor.state.doc.textBetween(
|
||||
editor.state.selection.from,
|
||||
editor.state.selection.to,
|
||||
'',
|
||||
);
|
||||
|
||||
const form = useForm<FormValue>({
|
||||
defaultValues: {href: previousUrl, text: previousText, target: '_blank'},
|
||||
});
|
||||
const {formId, close} = useDialogContext();
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogHeader>
|
||||
<Trans message="Insert link" />
|
||||
</DialogHeader>
|
||||
<DialogBody>
|
||||
<Form
|
||||
form={form}
|
||||
id={formId}
|
||||
onSubmit={value => {
|
||||
insertLinkIntoTextEditor(editor, value);
|
||||
close();
|
||||
}}
|
||||
>
|
||||
<FormTextField
|
||||
name="href"
|
||||
label={<Trans message="URL" />}
|
||||
autoFocus
|
||||
type="url"
|
||||
className="mb-20"
|
||||
/>
|
||||
<FormTextField
|
||||
name="text"
|
||||
label={<Trans message="Text to display" />}
|
||||
className="mb-20"
|
||||
/>
|
||||
<FormSelect
|
||||
selectionMode="single"
|
||||
name="target"
|
||||
label={<Trans message="Open link in..." />}
|
||||
>
|
||||
<Option value="_self">
|
||||
<Trans message="Current window" />
|
||||
</Option>
|
||||
<Option value="_blank">
|
||||
<Trans message="New window" />
|
||||
</Option>
|
||||
</FormSelect>
|
||||
</Form>
|
||||
</DialogBody>
|
||||
<DialogFooter>
|
||||
<Button onClick={close} variant="text">
|
||||
<Trans message="Cancel" />
|
||||
</Button>
|
||||
<Button type="submit" form={formId} variant="flat" color="primary">
|
||||
<Trans message="Save" />
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
41
common/resources/client/text-editor/menubar/list-buttons.tsx
Executable file
41
common/resources/client/text-editor/menubar/list-buttons.tsx
Executable file
@@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import {IconButton} from '../../ui/buttons/icon-button';
|
||||
import {FormatListBulletedIcon} from '../../icons/material/FormatListBulleted';
|
||||
import {FormatListNumberedIcon} from '../../icons/material/FormatListNumbered';
|
||||
import {MenubarButtonProps} from './menubar-button-props';
|
||||
import {Tooltip} from '@common/ui/tooltip/tooltip';
|
||||
import {Trans} from '@common/i18n/trans';
|
||||
|
||||
export function ListButtons({editor, size}: MenubarButtonProps) {
|
||||
const bulletActive = editor.isActive('bulletList');
|
||||
const orderedActive = editor.isActive('orderedList');
|
||||
return (
|
||||
<span className={clsx('flex-shrink-0', 'whitespace-nowrap')}>
|
||||
<Tooltip label={<Trans message="Bulleted list" />}>
|
||||
<IconButton
|
||||
size={size}
|
||||
color={bulletActive ? 'primary' : null}
|
||||
onClick={() => {
|
||||
editor.commands.focus();
|
||||
editor.commands.toggleBulletList();
|
||||
}}
|
||||
>
|
||||
<FormatListBulletedIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip label={<Trans message="Numbered list" />}>
|
||||
<IconButton
|
||||
size={size}
|
||||
color={orderedActive ? 'primary' : null}
|
||||
onClick={() => {
|
||||
editor.commands.focus();
|
||||
editor.commands.toggleOrderedList();
|
||||
}}
|
||||
>
|
||||
<FormatListNumberedIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
7
common/resources/client/text-editor/menubar/menubar-button-props.ts
Executable file
7
common/resources/client/text-editor/menubar/menubar-button-props.ts
Executable file
@@ -0,0 +1,7 @@
|
||||
import type {Editor} from '@tiptap/react';
|
||||
import {ButtonSize} from '../../ui/buttons/button-size';
|
||||
|
||||
export interface MenubarButtonProps {
|
||||
editor: Editor;
|
||||
size?: ButtonSize;
|
||||
}
|
||||
31
common/resources/client/text-editor/menubar/mode-button.tsx
Executable file
31
common/resources/client/text-editor/menubar/mode-button.tsx
Executable file
@@ -0,0 +1,31 @@
|
||||
import {Button} from '../../ui/buttons/button';
|
||||
import {CodeIcon} from '../../icons/material/Code';
|
||||
import {Trans} from '../../i18n/trans';
|
||||
import {DialogTrigger} from '../../ui/overlays/dialog/dialog-trigger';
|
||||
import {AceDialog} from '../../ace-editor/ace-dialog';
|
||||
import {Editor} from '@tiptap/react';
|
||||
import React from 'react';
|
||||
|
||||
interface ModeButtonProps {
|
||||
editor: Editor;
|
||||
}
|
||||
export function ModeButton({editor}: ModeButtonProps) {
|
||||
return (
|
||||
<DialogTrigger
|
||||
type="modal"
|
||||
onClose={newValue => {
|
||||
if (newValue != null) {
|
||||
editor?.commands.setContent(newValue);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Button variant="text" startIcon={<CodeIcon />}>
|
||||
<Trans message="Source" />
|
||||
</Button>
|
||||
<AceDialog
|
||||
title={<Trans message="Source code" />}
|
||||
defaultValue={editor.getHTML()}
|
||||
/>
|
||||
</DialogTrigger>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user