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-header": "^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 { completionConfig } from './extensions'
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 Italic from './icons/Italic.svelte'
import Link from './icons/Link.svelte'
import ListBullet from './icons/ListBullet.svelte'
import ListNumber from './icons/ListNumber.svelte'
import Quote from './icons/Quote.svelte'
@ -52,8 +48,6 @@
import RIMention from './icons/RIMention.svelte'
import RIStrikethrough from './icons/RIStrikethrough.svelte'
import Send from './icons/Send.svelte'
import Strikethrough from './icons/Strikethrough.svelte'
import TextStyle from './icons/TextStyle.svelte'
const dispatch = createEventDispatcher()
export let content: string = ''
@ -69,7 +63,8 @@
const client = getClient()
let textEditor: TextEditor
let isFormatting = false
let textEditorToolbar: HTMLElement
let activeModes = new Set<FormatMode>()
let isSelectionEmpty = true
let isEmpty = true
@ -90,14 +85,6 @@
},
order: 1001
},
{
label: textEditorPlugin.string.Link,
icon: RILink,
action: () => {
if (!(isSelectionEmpty && !activeModes.has('link'))) formatLink()
},
order: 2000
},
{
label: textEditorPlugin.string.Mention,
icon: RIMention,
@ -121,51 +108,6 @@
)
},
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,87 +197,85 @@
</script>
<div class="ref-container">
{#if isFormatting}
<div class="formatPanelRef buttons-group xsmall-gap" class:withoutTopBorder>
<Button
icon={Bold}
kind={'ghost'}
size={'small'}
selected={activeModes.has('bold')}
showTooltip={{ label: textEditorPlugin.string.Bold }}
on:click={getToggler(textEditor.toggleBold)}
/>
<Button
icon={Italic}
kind={'ghost'}
size={'small'}
selected={activeModes.has('italic')}
showTooltip={{ label: textEditorPlugin.string.Italic }}
on:click={getToggler(textEditor.toggleItalic)}
/>
<Button
icon={Strikethrough}
kind={'ghost'}
size={'small'}
selected={activeModes.has('strike')}
showTooltip={{ label: textEditorPlugin.string.Strikethrough }}
on:click={getToggler(textEditor.toggleStrike)}
/>
<Button
icon={Link}
kind={'ghost'}
size={'small'}
selected={activeModes.has('link')}
disabled={isSelectionEmpty && !activeModes.has('link')}
showTooltip={{ label: textEditorPlugin.string.Link }}
on:click={formatLink}
/>
<div class="buttons-divider" />
<Button
icon={ListNumber}
kind={'ghost'}
size={'small'}
selected={activeModes.has('orderedList')}
showTooltip={{ label: textEditorPlugin.string.OrderedList }}
on:click={getToggler(textEditor.toggleOrderedList)}
/>
<Button
icon={ListBullet}
kind={'ghost'}
size={'small'}
selected={activeModes.has('bulletList')}
showTooltip={{ label: textEditorPlugin.string.BulletedList }}
on:click={getToggler(textEditor.toggleBulletList)}
/>
<div class="buttons-divider" />
<Button
icon={Quote}
kind={'ghost'}
size={'small'}
selected={activeModes.has('blockquote')}
showTooltip={{ label: textEditorPlugin.string.Blockquote }}
on:click={getToggler(textEditor.toggleBlockquote)}
/>
<div class="buttons-divider" />
<Button
icon={Code}
kind={'ghost'}
size={'small'}
selected={activeModes.has('code')}
showTooltip={{ label: textEditorPlugin.string.Code }}
on:click={getToggler(textEditor.toggleCode)}
/>
<Button
icon={CodeBlock}
kind={'ghost'}
size={'small'}
selected={activeModes.has('codeBlock')}
showTooltip={{ label: textEditorPlugin.string.CodeBlock }}
on:click={getToggler(textEditor.toggleCodeBlock)}
/>
</div>
{/if}
<div class="textInput" class:withoutTopBorder={withoutTopBorder || isFormatting}>
<div class="formatPanel buttons-group xsmall-gap mb-4" class:withoutTopBorder bind:this={textEditorToolbar}>
<Button
icon={RIBold}
kind={'ghost'}
size={'small'}
selected={activeModes.has('bold')}
showTooltip={{ label: textEditorPlugin.string.Bold }}
on:click={getToggler(textEditor.toggleBold)}
/>
<Button
icon={RIItalic}
kind={'ghost'}
size={'small'}
selected={activeModes.has('italic')}
showTooltip={{ label: textEditorPlugin.string.Italic }}
on:click={getToggler(textEditor.toggleItalic)}
/>
<Button
icon={RIStrikethrough}
kind={'ghost'}
size={'small'}
selected={activeModes.has('strike')}
showTooltip={{ label: textEditorPlugin.string.Strikethrough }}
on:click={getToggler(textEditor.toggleStrike)}
/>
<Button
icon={RILink}
kind={'ghost'}
size={'small'}
selected={activeModes.has('link')}
disabled={isSelectionEmpty && !activeModes.has('link')}
showTooltip={{ label: textEditorPlugin.string.Link }}
on:click={formatLink}
/>
<div class="buttons-divider" />
<Button
icon={ListNumber}
kind={'ghost'}
size={'small'}
selected={activeModes.has('orderedList')}
showTooltip={{ label: textEditorPlugin.string.OrderedList }}
on:click={getToggler(textEditor.toggleOrderedList)}
/>
<Button
icon={ListBullet}
kind={'ghost'}
size={'small'}
selected={activeModes.has('bulletList')}
showTooltip={{ label: textEditorPlugin.string.BulletedList }}
on:click={getToggler(textEditor.toggleBulletList)}
/>
<div class="buttons-divider" />
<Button
icon={Quote}
kind={'ghost'}
size={'small'}
selected={activeModes.has('blockquote')}
showTooltip={{ label: textEditorPlugin.string.Blockquote }}
on:click={getToggler(textEditor.toggleBlockquote)}
/>
<div class="buttons-divider" />
<Button
icon={RICode}
kind={'ghost'}
size={'small'}
selected={activeModes.has('code')}
showTooltip={{ label: textEditorPlugin.string.Code }}
on:click={getToggler(textEditor.toggleCode)}
/>
<Button
icon={CodeBlock}
kind={'ghost'}
size={'small'}
selected={activeModes.has('codeBlock')}
showTooltip={{ label: textEditorPlugin.string.CodeBlock }}
on:click={getToggler(textEditor.toggleCodeBlock)}
/>
</div>
<div class="textInput" class:withoutTopBorder>
<div class="inputMsg">
<TextEditor
bind:content
@ -361,6 +301,7 @@
on:selection-update={updateFormattingState}
on:update
placeholder={placeholder ?? textEditorPlugin.string.EditorPlaceholder}
{textEditorToolbar}
/>
</div>
{#if showSend}
@ -383,7 +324,7 @@
{/if}
</div>
<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}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<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>

View File

@ -23,9 +23,7 @@
IconSize,
Scroller,
SelectPopup,
showPopup,
deviceOptionsStore as deviceInfo,
checkAdaptiveMatching
showPopup
} from '@hcengineering/ui'
import { Level } from '@tiptap/extension-heading'
import { createEventDispatcher } from 'svelte'
@ -33,13 +31,9 @@
import { FORMAT_MODES, FormatMode, RefInputAction, RefInputActionItem, TextEditorHandler } from '../types'
import { headingLevels, mInsertTable } from './extensions'
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 Header from './icons/Header.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 ListNumber from './icons/ListNumber.svelte'
import Quote from './icons/Quote.svelte'
@ -48,7 +42,6 @@
import RIItalic from './icons/RIItalic.svelte'
import RILink from './icons/RILink.svelte'
import RIStrikethrough from './icons/RIStrikethrough.svelte'
import Strikethrough from './icons/Strikethrough.svelte'
// import RIMention from './icons/RIMention.svelte'
import { AnyExtension } from '@tiptap/core'
import AddColAfter from './icons/table/AddColAfter.svelte'
@ -58,7 +51,6 @@
import DeleteCol from './icons/table/DeleteCol.svelte'
import DeleteRow from './icons/table/DeleteRow.svelte'
import DeleteTable from './icons/table/DeleteTable.svelte'
import TextStyle from './icons/TextStyle.svelte'
import LinkPopup from './LinkPopup.svelte'
import StyleButton from './StyleButton.svelte'
import TextEditor from './TextEditor.svelte'
@ -82,6 +74,8 @@
export let extensions: AnyExtension[] = []
let textEditor: TextEditor
let textEditorToolbar: HTMLElement
let isEmpty = true
let contentHeight: number
@ -127,7 +121,6 @@
? 'max-content'
: maxHeight
let isFormatting = enableFormatting
let activeModes = new Set<FormatMode>()
let isSelectionEmpty = true
@ -147,20 +140,6 @@
},
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,
icon: IconEmoji,
@ -178,51 +157,6 @@
)
},
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
$: buttonsGap = checkAdaptiveMatching(devSize, 'sm') ? 'small-gap' : 'large-gap'
const buttonsGap = 'small-gap'
$: buttonsHeight =
buttonSize === 'large' || buttonSize === 'x-large' || buttonSize === 'full'
? 'h-6 max-h-6'
@ -472,107 +406,104 @@
tabindex="-1"
on:click|preventDefault|stopPropagation={() => (needFocus = true)}
>
{#if isFormatting}
<div class="formatPanel buttons-group xsmall-gap mb-4" class:withoutTopBorder>
<StyleButton
icon={Header}
size={formatButtonSize}
selected={activeModes.has('heading')}
showTooltip={{ label: getEmbeddedLabel(`H${headingLevel}`) }}
on:click={toggleHeader}
/>
<StyleButton
icon={Bold}
size={formatButtonSize}
selected={activeModes.has('bold')}
showTooltip={{ label: textEditorPlugin.string.Bold }}
on:click={getToggler(textEditor.toggleBold)}
/>
<StyleButton
icon={Italic}
size={formatButtonSize}
selected={activeModes.has('italic')}
showTooltip={{ label: textEditorPlugin.string.Italic }}
on:click={getToggler(textEditor.toggleItalic)}
/>
<StyleButton
icon={Strikethrough}
size={formatButtonSize}
selected={activeModes.has('strike')}
showTooltip={{ label: textEditorPlugin.string.Strikethrough }}
on:click={getToggler(textEditor.toggleStrike)}
/>
<StyleButton
icon={Link}
size={formatButtonSize}
selected={activeModes.has('link')}
disabled={isSelectionEmpty && !activeModes.has('link')}
showTooltip={{ label: textEditorPlugin.string.Link }}
on:click={formatLink}
/>
<div class="buttons-divider" />
<StyleButton
icon={ListNumber}
size={formatButtonSize}
selected={activeModes.has('orderedList')}
showTooltip={{ label: textEditorPlugin.string.OrderedList }}
on:click={getToggler(textEditor.toggleOrderedList)}
/>
<StyleButton
icon={ListBullet}
size={formatButtonSize}
selected={activeModes.has('bulletList')}
showTooltip={{ label: textEditorPlugin.string.BulletedList }}
on:click={getToggler(textEditor.toggleBulletList)}
/>
<div class="buttons-divider" />
<StyleButton
icon={Quote}
size={formatButtonSize}
selected={activeModes.has('blockquote')}
showTooltip={{ label: textEditorPlugin.string.Blockquote }}
on:click={getToggler(textEditor.toggleBlockquote)}
/>
<div class="buttons-divider" />
<StyleButton
icon={Code}
size={formatButtonSize}
selected={activeModes.has('code')}
showTooltip={{ label: textEditorPlugin.string.Code }}
on:click={getToggler(textEditor.toggleCode)}
/>
<StyleButton
icon={CodeBlock}
size={formatButtonSize}
selected={activeModes.has('codeBlock')}
showTooltip={{ label: textEditorPlugin.string.CodeBlock }}
on:click={getToggler(textEditor.toggleCodeBlock)}
/>
<div class="formatPanel buttons-group xsmall-gap mb-4" class:withoutTopBorder bind:this={textEditorToolbar}>
<StyleButton
icon={Header}
size={formatButtonSize}
selected={activeModes.has('heading')}
showTooltip={{ label: getEmbeddedLabel(`H${headingLevel}`) }}
on:click={toggleHeader}
/>
<StyleButton
icon={RIBold}
size={formatButtonSize}
selected={activeModes.has('bold')}
showTooltip={{ label: textEditorPlugin.string.Bold }}
on:click={getToggler(textEditor.toggleBold)}
/>
<StyleButton
icon={RIItalic}
size={formatButtonSize}
selected={activeModes.has('italic')}
showTooltip={{ label: textEditorPlugin.string.Italic }}
on:click={getToggler(textEditor.toggleItalic)}
/>
<StyleButton
icon={RIStrikethrough}
size={formatButtonSize}
selected={activeModes.has('strike')}
showTooltip={{ label: textEditorPlugin.string.Strikethrough }}
on:click={getToggler(textEditor.toggleStrike)}
/>
<StyleButton
icon={RILink}
size={formatButtonSize}
selected={activeModes.has('link')}
disabled={isSelectionEmpty && !activeModes.has('link')}
showTooltip={{ label: textEditorPlugin.string.Link }}
on:click={formatLink}
/>
<div class="buttons-divider" />
<StyleButton
icon={ListNumber}
size={formatButtonSize}
selected={activeModes.has('orderedList')}
showTooltip={{ label: textEditorPlugin.string.OrderedList }}
on:click={getToggler(textEditor.toggleOrderedList)}
/>
<StyleButton
icon={ListBullet}
size={formatButtonSize}
selected={activeModes.has('bulletList')}
showTooltip={{ label: textEditorPlugin.string.BulletedList }}
on:click={getToggler(textEditor.toggleBulletList)}
/>
<div class="buttons-divider" />
<StyleButton
icon={Quote}
size={formatButtonSize}
selected={activeModes.has('blockquote')}
showTooltip={{ label: textEditorPlugin.string.Blockquote }}
on:click={getToggler(textEditor.toggleBlockquote)}
/>
<div class="buttons-divider" />
<StyleButton
icon={RICode}
size={formatButtonSize}
selected={activeModes.has('code')}
showTooltip={{ label: textEditorPlugin.string.Code }}
on:click={getToggler(textEditor.toggleCode)}
/>
<StyleButton
icon={CodeBlock}
size={formatButtonSize}
selected={activeModes.has('codeBlock')}
showTooltip={{ label: textEditorPlugin.string.CodeBlock }}
on:click={getToggler(textEditor.toggleCodeBlock)}
/>
<StyleButton
icon={IconTable}
iconProps={{ style: 'table' }}
size={formatButtonSize}
selected={activeModes.has('table')}
on:click={insertTable}
showTooltip={{ label: textEditorPlugin.string.InsertTable }}
/>
{#if activeModes.has('table')}
<StyleButton
icon={IconTable}
iconProps={{ style: 'table' }}
iconProps={{ style: 'tableProps' }}
size={formatButtonSize}
selected={activeModes.has('table')}
on:click={insertTable}
showTooltip={{ label: textEditorPlugin.string.InsertTable }}
on:click={tableOptions}
showTooltip={{ label: textEditorPlugin.string.TableOptions }}
/>
{#if activeModes.has('table')}
<StyleButton
icon={IconTable}
iconProps={{ style: 'tableProps' }}
size={formatButtonSize}
on:click={tableOptions}
showTooltip={{ label: textEditorPlugin.string.TableOptions }}
/>
{/if}
</div>
{/if}
{/if}
</div>
<div class="textInput" class:focusable>
<div
bind:clientHeight={contentHeight}
class="inputMsg"
class="inputMsg showScroll"
class:scrollable={isScrollable}
class:showScroll={contentHeight > 32}
style="--texteditor-maxheight: {varsStyle};"
>
{#if isScrollable}
@ -592,6 +523,7 @@
on:blur
on:focus
supportSubmit={false}
{textEditorToolbar}
on:selection-update={updateFormattingState}
/>
</Scroller>
@ -611,6 +543,7 @@
on:blur
on:focus
supportSubmit={false}
{textEditorToolbar}
on:selection-update={updateFormattingState}
/>
{/if}
@ -672,6 +605,16 @@
}
&:not(.showScroll) {
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 {
background-color: transparent;
@ -690,10 +633,7 @@
}
}
}
&:focus-within .formatPanel {
position: sticky;
top: 1.25rem;
}
.formatPanel {
margin: -0.5rem -0.25rem 0.5rem;
padding: 0.375rem;

View File

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

View File

@ -1,4 +1,10 @@
.textEditorImage {
cursor: pointer;
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;
}