Signed-off-by: Maxim Karmatskikh <mkarmatskih@gmail.com>
This commit is contained in:
Maksim Karmatskikh 2023-08-10 22:27:43 +06:00 committed by GitHub
parent c628a0a0d1
commit 16d079bb88
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 435 additions and 510 deletions

File diff suppressed because it is too large Load Diff

View File

@ -75,6 +75,7 @@
"@tiptap/extension-table-cell": "^2.0.3", "@tiptap/extension-table-cell": "^2.0.3",
"@tiptap/extension-table-header": "^2.0.3", "@tiptap/extension-table-header": "^2.0.3",
"@tiptap/extension-table-row": "^2.0.3", "@tiptap/extension-table-row": "^2.0.3",
"@tiptap/extension-code": "^2.0.3" "@tiptap/extension-code": "^2.0.3",
"@tiptap/extension-bubble-menu": "^2.0.4"
} }
} }

View File

@ -37,11 +37,7 @@
import TextEditor from './TextEditor.svelte' import TextEditor from './TextEditor.svelte'
import { completionConfig } from './extensions' import { completionConfig } from './extensions'
import Attach from './icons/Attach.svelte' import Attach from './icons/Attach.svelte'
import Bold from './icons/Bold.svelte'
import Code from './icons/Code.svelte'
import CodeBlock from './icons/CodeBlock.svelte' import CodeBlock from './icons/CodeBlock.svelte'
import Italic from './icons/Italic.svelte'
import Link from './icons/Link.svelte'
import ListBullet from './icons/ListBullet.svelte' import ListBullet from './icons/ListBullet.svelte'
import ListNumber from './icons/ListNumber.svelte' import ListNumber from './icons/ListNumber.svelte'
import Quote from './icons/Quote.svelte' import Quote from './icons/Quote.svelte'
@ -52,8 +48,6 @@
import RIMention from './icons/RIMention.svelte' import RIMention from './icons/RIMention.svelte'
import RIStrikethrough from './icons/RIStrikethrough.svelte' import RIStrikethrough from './icons/RIStrikethrough.svelte'
import Send from './icons/Send.svelte' import Send from './icons/Send.svelte'
import Strikethrough from './icons/Strikethrough.svelte'
import TextStyle from './icons/TextStyle.svelte'
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
export let content: string = '' export let content: string = ''
@ -69,7 +63,8 @@
const client = getClient() const client = getClient()
let textEditor: TextEditor let textEditor: TextEditor
let isFormatting = false let textEditorToolbar: HTMLElement
let activeModes = new Set<FormatMode>() let activeModes = new Set<FormatMode>()
let isSelectionEmpty = true let isSelectionEmpty = true
let isEmpty = true let isEmpty = true
@ -90,14 +85,6 @@
}, },
order: 1001 order: 1001
}, },
{
label: textEditorPlugin.string.Link,
icon: RILink,
action: () => {
if (!(isSelectionEmpty && !activeModes.has('link'))) formatLink()
},
order: 2000
},
{ {
label: textEditorPlugin.string.Mention, label: textEditorPlugin.string.Mention,
icon: RIMention, icon: RIMention,
@ -121,51 +108,6 @@
) )
}, },
order: 4001 order: 4001
},
{
label: textEditorPlugin.string.TextStyle,
icon: TextStyle,
action: () => {
isFormatting = !isFormatting
textEditor.focus()
},
order: 6000
},
{
label: textEditorPlugin.string.Bold,
icon: RIBold,
action: () => {
textEditor.toggleBold()
textEditor.focus()
},
order: 6010
},
{
label: textEditorPlugin.string.Italic,
icon: RIItalic,
action: () => {
textEditor.toggleItalic()
textEditor.focus()
},
order: 6020
},
{
label: textEditorPlugin.string.Strikethrough,
icon: RIStrikethrough,
action: () => {
textEditor.toggleStrike()
textEditor.focus()
},
order: 6030
},
{
label: textEditorPlugin.string.Code,
icon: RICode,
action: () => {
textEditor.toggleCode()
textEditor.focus()
},
order: 6040
} }
] ]
@ -255,10 +197,9 @@
</script> </script>
<div class="ref-container"> <div class="ref-container">
{#if isFormatting} <div class="formatPanel buttons-group xsmall-gap mb-4" class:withoutTopBorder bind:this={textEditorToolbar}>
<div class="formatPanelRef buttons-group xsmall-gap" class:withoutTopBorder>
<Button <Button
icon={Bold} icon={RIBold}
kind={'ghost'} kind={'ghost'}
size={'small'} size={'small'}
selected={activeModes.has('bold')} selected={activeModes.has('bold')}
@ -266,7 +207,7 @@
on:click={getToggler(textEditor.toggleBold)} on:click={getToggler(textEditor.toggleBold)}
/> />
<Button <Button
icon={Italic} icon={RIItalic}
kind={'ghost'} kind={'ghost'}
size={'small'} size={'small'}
selected={activeModes.has('italic')} selected={activeModes.has('italic')}
@ -274,7 +215,7 @@
on:click={getToggler(textEditor.toggleItalic)} on:click={getToggler(textEditor.toggleItalic)}
/> />
<Button <Button
icon={Strikethrough} icon={RIStrikethrough}
kind={'ghost'} kind={'ghost'}
size={'small'} size={'small'}
selected={activeModes.has('strike')} selected={activeModes.has('strike')}
@ -282,7 +223,7 @@
on:click={getToggler(textEditor.toggleStrike)} on:click={getToggler(textEditor.toggleStrike)}
/> />
<Button <Button
icon={Link} icon={RILink}
kind={'ghost'} kind={'ghost'}
size={'small'} size={'small'}
selected={activeModes.has('link')} selected={activeModes.has('link')}
@ -318,7 +259,7 @@
/> />
<div class="buttons-divider" /> <div class="buttons-divider" />
<Button <Button
icon={Code} icon={RICode}
kind={'ghost'} kind={'ghost'}
size={'small'} size={'small'}
selected={activeModes.has('code')} selected={activeModes.has('code')}
@ -334,8 +275,7 @@
on:click={getToggler(textEditor.toggleCodeBlock)} on:click={getToggler(textEditor.toggleCodeBlock)}
/> />
</div> </div>
{/if} <div class="textInput" class:withoutTopBorder>
<div class="textInput" class:withoutTopBorder={withoutTopBorder || isFormatting}>
<div class="inputMsg"> <div class="inputMsg">
<TextEditor <TextEditor
bind:content bind:content
@ -361,6 +301,7 @@
on:selection-update={updateFormattingState} on:selection-update={updateFormattingState}
on:update on:update
placeholder={placeholder ?? textEditorPlugin.string.EditorPlaceholder} placeholder={placeholder ?? textEditorPlugin.string.EditorPlaceholder}
{textEditorToolbar}
/> />
</div> </div>
{#if showSend} {#if showSend}
@ -383,7 +324,7 @@
{/if} {/if}
</div> </div>
<div class="flex-between clear-mins" style:margin={'.75rem .75rem 0'}> <div class="flex-between clear-mins" style:margin={'.75rem .75rem 0'}>
<div class="buttons-group {shrinkButtons ? 'medium-gap' : 'large-gap'}"> <div class="buttons-group medium-gap">
{#each actions as a} {#each actions as a}
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-click-events-have-key-events -->
<div <div
@ -534,5 +475,14 @@
} }
} }
} }
.formatPanel {
margin: -0.5rem -0.25rem 0.5rem;
padding: 0.375rem;
background-color: var(--theme-comp-header-color);
border-radius: 0.5rem;
box-shadow: var(--theme-popup-shadow);
z-index: 1;
}
} }
</style> </style>

View File

@ -23,9 +23,7 @@
IconSize, IconSize,
Scroller, Scroller,
SelectPopup, SelectPopup,
showPopup, showPopup
deviceOptionsStore as deviceInfo,
checkAdaptiveMatching
} from '@hcengineering/ui' } from '@hcengineering/ui'
import { Level } from '@tiptap/extension-heading' import { Level } from '@tiptap/extension-heading'
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
@ -33,13 +31,9 @@
import { FORMAT_MODES, FormatMode, RefInputAction, RefInputActionItem, TextEditorHandler } from '../types' import { FORMAT_MODES, FormatMode, RefInputAction, RefInputActionItem, TextEditorHandler } from '../types'
import { headingLevels, mInsertTable } from './extensions' import { headingLevels, mInsertTable } from './extensions'
import Attach from './icons/Attach.svelte' import Attach from './icons/Attach.svelte'
import Bold from './icons/Bold.svelte'
import Code from './icons/Code.svelte'
import CodeBlock from './icons/CodeBlock.svelte' import CodeBlock from './icons/CodeBlock.svelte'
import Header from './icons/Header.svelte' import Header from './icons/Header.svelte'
import IconTable from './icons/IconTable.svelte' import IconTable from './icons/IconTable.svelte'
import Italic from './icons/Italic.svelte'
import Link from './icons/Link.svelte'
import ListBullet from './icons/ListBullet.svelte' import ListBullet from './icons/ListBullet.svelte'
import ListNumber from './icons/ListNumber.svelte' import ListNumber from './icons/ListNumber.svelte'
import Quote from './icons/Quote.svelte' import Quote from './icons/Quote.svelte'
@ -48,7 +42,6 @@
import RIItalic from './icons/RIItalic.svelte' import RIItalic from './icons/RIItalic.svelte'
import RILink from './icons/RILink.svelte' import RILink from './icons/RILink.svelte'
import RIStrikethrough from './icons/RIStrikethrough.svelte' import RIStrikethrough from './icons/RIStrikethrough.svelte'
import Strikethrough from './icons/Strikethrough.svelte'
// import RIMention from './icons/RIMention.svelte' // import RIMention from './icons/RIMention.svelte'
import { AnyExtension } from '@tiptap/core' import { AnyExtension } from '@tiptap/core'
import AddColAfter from './icons/table/AddColAfter.svelte' import AddColAfter from './icons/table/AddColAfter.svelte'
@ -58,7 +51,6 @@
import DeleteCol from './icons/table/DeleteCol.svelte' import DeleteCol from './icons/table/DeleteCol.svelte'
import DeleteRow from './icons/table/DeleteRow.svelte' import DeleteRow from './icons/table/DeleteRow.svelte'
import DeleteTable from './icons/table/DeleteTable.svelte' import DeleteTable from './icons/table/DeleteTable.svelte'
import TextStyle from './icons/TextStyle.svelte'
import LinkPopup from './LinkPopup.svelte' import LinkPopup from './LinkPopup.svelte'
import StyleButton from './StyleButton.svelte' import StyleButton from './StyleButton.svelte'
import TextEditor from './TextEditor.svelte' import TextEditor from './TextEditor.svelte'
@ -82,6 +74,8 @@
export let extensions: AnyExtension[] = [] export let extensions: AnyExtension[] = []
let textEditor: TextEditor let textEditor: TextEditor
let textEditorToolbar: HTMLElement
let isEmpty = true let isEmpty = true
let contentHeight: number let contentHeight: number
@ -127,7 +121,6 @@
? 'max-content' ? 'max-content'
: maxHeight : maxHeight
let isFormatting = enableFormatting
let activeModes = new Set<FormatMode>() let activeModes = new Set<FormatMode>()
let isSelectionEmpty = true let isSelectionEmpty = true
@ -147,20 +140,6 @@
}, },
order: 1001 order: 1001
}, },
{
label: textEditorPlugin.string.Link,
icon: RILink,
action: () => {
if (!(isSelectionEmpty && !activeModes.has('link'))) formatLink()
},
order: 2000
},
// {
// label: textEditorPlugin.string.Mention,
// icon: RIMention,
// action: () => textEditor.insertText('@'),
// order: 3000
// },
{ {
label: textEditorPlugin.string.Emoji, label: textEditorPlugin.string.Emoji,
icon: IconEmoji, icon: IconEmoji,
@ -178,51 +157,6 @@
) )
}, },
order: 4001 order: 4001
},
{
label: textEditorPlugin.string.TextStyle,
icon: TextStyle,
action: () => {
isFormatting = !isFormatting
textEditor.focus()
},
order: 6000
},
{
label: textEditorPlugin.string.Bold,
icon: RIBold,
action: () => {
textEditor.toggleBold()
textEditor.focus()
},
order: 6010
},
{
label: textEditorPlugin.string.Italic,
icon: RIItalic,
action: () => {
textEditor.toggleItalic()
textEditor.focus()
},
order: 6020
},
{
label: textEditorPlugin.string.Strikethrough,
icon: RIStrikethrough,
action: () => {
textEditor.toggleStrike()
textEditor.focus()
},
order: 6030
},
{
label: textEditorPlugin.string.Code,
icon: RICode,
action: () => {
textEditor.toggleCode()
textEditor.focus()
},
order: 6040
} }
] ]
@ -448,8 +382,8 @@
) )
} }
$: devSize = $deviceInfo.size const buttonsGap = 'small-gap'
$: buttonsGap = checkAdaptiveMatching(devSize, 'sm') ? 'small-gap' : 'large-gap'
$: buttonsHeight = $: buttonsHeight =
buttonSize === 'large' || buttonSize === 'x-large' || buttonSize === 'full' buttonSize === 'large' || buttonSize === 'x-large' || buttonSize === 'full'
? 'h-6 max-h-6' ? 'h-6 max-h-6'
@ -472,8 +406,7 @@
tabindex="-1" tabindex="-1"
on:click|preventDefault|stopPropagation={() => (needFocus = true)} on:click|preventDefault|stopPropagation={() => (needFocus = true)}
> >
{#if isFormatting} <div class="formatPanel buttons-group xsmall-gap mb-4" class:withoutTopBorder bind:this={textEditorToolbar}>
<div class="formatPanel buttons-group xsmall-gap mb-4" class:withoutTopBorder>
<StyleButton <StyleButton
icon={Header} icon={Header}
size={formatButtonSize} size={formatButtonSize}
@ -482,28 +415,28 @@
on:click={toggleHeader} on:click={toggleHeader}
/> />
<StyleButton <StyleButton
icon={Bold} icon={RIBold}
size={formatButtonSize} size={formatButtonSize}
selected={activeModes.has('bold')} selected={activeModes.has('bold')}
showTooltip={{ label: textEditorPlugin.string.Bold }} showTooltip={{ label: textEditorPlugin.string.Bold }}
on:click={getToggler(textEditor.toggleBold)} on:click={getToggler(textEditor.toggleBold)}
/> />
<StyleButton <StyleButton
icon={Italic} icon={RIItalic}
size={formatButtonSize} size={formatButtonSize}
selected={activeModes.has('italic')} selected={activeModes.has('italic')}
showTooltip={{ label: textEditorPlugin.string.Italic }} showTooltip={{ label: textEditorPlugin.string.Italic }}
on:click={getToggler(textEditor.toggleItalic)} on:click={getToggler(textEditor.toggleItalic)}
/> />
<StyleButton <StyleButton
icon={Strikethrough} icon={RIStrikethrough}
size={formatButtonSize} size={formatButtonSize}
selected={activeModes.has('strike')} selected={activeModes.has('strike')}
showTooltip={{ label: textEditorPlugin.string.Strikethrough }} showTooltip={{ label: textEditorPlugin.string.Strikethrough }}
on:click={getToggler(textEditor.toggleStrike)} on:click={getToggler(textEditor.toggleStrike)}
/> />
<StyleButton <StyleButton
icon={Link} icon={RILink}
size={formatButtonSize} size={formatButtonSize}
selected={activeModes.has('link')} selected={activeModes.has('link')}
disabled={isSelectionEmpty && !activeModes.has('link')} disabled={isSelectionEmpty && !activeModes.has('link')}
@ -535,7 +468,7 @@
/> />
<div class="buttons-divider" /> <div class="buttons-divider" />
<StyleButton <StyleButton
icon={Code} icon={RICode}
size={formatButtonSize} size={formatButtonSize}
selected={activeModes.has('code')} selected={activeModes.has('code')}
showTooltip={{ label: textEditorPlugin.string.Code }} showTooltip={{ label: textEditorPlugin.string.Code }}
@ -566,13 +499,11 @@
/> />
{/if} {/if}
</div> </div>
{/if}
<div class="textInput" class:focusable> <div class="textInput" class:focusable>
<div <div
bind:clientHeight={contentHeight} bind:clientHeight={contentHeight}
class="inputMsg" class="inputMsg showScroll"
class:scrollable={isScrollable} class:scrollable={isScrollable}
class:showScroll={contentHeight > 32}
style="--texteditor-maxheight: {varsStyle};" style="--texteditor-maxheight: {varsStyle};"
> >
{#if isScrollable} {#if isScrollable}
@ -592,6 +523,7 @@
on:blur on:blur
on:focus on:focus
supportSubmit={false} supportSubmit={false}
{textEditorToolbar}
on:selection-update={updateFormattingState} on:selection-update={updateFormattingState}
/> />
</Scroller> </Scroller>
@ -611,6 +543,7 @@
on:blur on:blur
on:focus on:focus
supportSubmit={false} supportSubmit={false}
{textEditorToolbar}
on:selection-update={updateFormattingState} on:selection-update={updateFormattingState}
/> />
{/if} {/if}
@ -672,6 +605,16 @@
} }
&:not(.showScroll) { &:not(.showScroll) {
overflow-y: hidden; overflow-y: hidden;
/*
showScroll was set only when contentHeight > 32
But this gave a bad behaviour for editor toolbar
in the bubble when there is only one line of text.
I did the testing and figured out that now
we can use showScroll always.
Please refer UBER-555
*/
&::-webkit-scrollbar-thumb { &::-webkit-scrollbar-thumb {
background-color: transparent; background-color: transparent;
@ -690,10 +633,7 @@
} }
} }
} }
&:focus-within .formatPanel {
position: sticky;
top: 1.25rem;
}
.formatPanel { .formatPanel {
margin: -0.5rem -0.25rem 0.5rem; margin: -0.5rem -0.25rem 0.5rem;
padding: 0.375rem; padding: 0.375rem;

View File

@ -20,6 +20,8 @@
import { AnyExtension, Editor, Extension, HTMLContent } from '@tiptap/core' import { AnyExtension, Editor, Extension, HTMLContent } from '@tiptap/core'
import { Level } from '@tiptap/extension-heading' import { Level } from '@tiptap/extension-heading'
import Placeholder from '@tiptap/extension-placeholder' import Placeholder from '@tiptap/extension-placeholder'
import BubbleMenu from '@tiptap/extension-bubble-menu'
import { createEventDispatcher, onDestroy, onMount } from 'svelte' import { createEventDispatcher, onDestroy, onMount } from 'svelte'
import textEditorPlugin from '../plugin' import textEditorPlugin from '../plugin'
import { FormatMode } from '../types' import { FormatMode } from '../types'
@ -32,6 +34,7 @@
export let extensions: AnyExtension[] = [] export let extensions: AnyExtension[] = []
export let supportSubmit = true export let supportSubmit = true
export let isEmpty = true export let isEmpty = true
export let textEditorToolbar: HTMLElement | null = null
let element: HTMLElement let element: HTMLElement
let editor: Editor let editor: Editor
@ -202,7 +205,10 @@
...defaultExtensions, ...defaultExtensions,
...(supportSubmit ? [Handle] : []), // order important ...(supportSubmit ? [Handle] : []), // order important
Placeholder.configure({ placeholder: placeHolderStr }), Placeholder.configure({ placeholder: placeHolderStr }),
...extensions ...extensions,
BubbleMenu.configure({
element: textEditorToolbar
})
], ],
parseOptions: { parseOptions: {
preserveWhitespace: 'full' preserveWhitespace: 'full'

View File

@ -2,3 +2,9 @@
cursor: pointer; cursor: pointer;
object-fit: contain; object-fit: contain;
} }
// need to override editor's bubble max-width
// due to https://github.com/atomiks/tippyjs/issues/451
.tippy-box {
max-width: 370px !important;
}