mirror of
https://github.com/hcengineering/platform.git
synced 2025-05-29 11:31:32 +00:00
Refactor TextEditor: Move bubble menu toolbar in separate component (#3647)
Signed-off-by: Anna No <anna.no@xored.com>
This commit is contained in:
parent
4bdb5ebcd3
commit
125d77f944
@ -15,59 +15,30 @@
|
||||
//
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { getEmbeddedLabel, IntlString, translate } from '@hcengineering/platform'
|
||||
import { IntlString, translate } from '@hcengineering/platform'
|
||||
|
||||
import { Editor, Extension, HTMLContent } from '@tiptap/core'
|
||||
import Collaboration from '@tiptap/extension-collaboration'
|
||||
import CollaborationCursor from '@tiptap/extension-collaboration-cursor'
|
||||
import { Level } from '@tiptap/extension-heading'
|
||||
import Placeholder from '@tiptap/extension-placeholder'
|
||||
|
||||
import { Plugin, PluginKey, Transaction } from 'prosemirror-state'
|
||||
import { createEventDispatcher, onDestroy, onMount } from 'svelte'
|
||||
|
||||
import { getCurrentAccount, Markup } from '@hcengineering/core'
|
||||
import {
|
||||
getEventPositionElement,
|
||||
getPlatformColorForText,
|
||||
IconObjects,
|
||||
IconSize,
|
||||
SelectPopup,
|
||||
showPopup,
|
||||
themeStore
|
||||
} from '@hcengineering/ui'
|
||||
import { getPlatformColorForText, IconObjects, IconSize, themeStore } from '@hcengineering/ui'
|
||||
import { WebsocketProvider } from 'y-websocket'
|
||||
import * as Y from 'yjs'
|
||||
import StyleButton from './StyleButton.svelte'
|
||||
|
||||
import presentation from '@hcengineering/presentation'
|
||||
|
||||
import { DecorationSet } from 'prosemirror-view'
|
||||
import textEditorPlugin from '../plugin'
|
||||
import { CollaborationIds, FORMAT_MODES, FormatMode } from '../types'
|
||||
import Bold from './icons/Bold.svelte'
|
||||
import Code from './icons/Code.svelte'
|
||||
import CodeBlock from './icons/CodeBlock.svelte'
|
||||
import { CollaborationIds, TextFormatCategory } from '../types'
|
||||
|
||||
import { getContext } from 'svelte'
|
||||
import { calculateDecorations } from './diff/decorations'
|
||||
import { defaultExtensions, headingLevels, mInsertTable } from './extensions'
|
||||
import Header from './icons/Header.svelte'
|
||||
import IconTable from './icons/IconTable.svelte'
|
||||
import Italic from './icons/Italic.svelte'
|
||||
import LinkEl from './icons/Link.svelte'
|
||||
import ListBullet from './icons/ListBullet.svelte'
|
||||
import ListNumber from './icons/ListNumber.svelte'
|
||||
import Quote from './icons/Quote.svelte'
|
||||
import Strikethrough from './icons/Strikethrough.svelte'
|
||||
import AddColAfter from './icons/table/AddColAfter.svelte'
|
||||
import AddColBefore from './icons/table/AddColBefore.svelte'
|
||||
import AddRowAfter from './icons/table/AddRowAfter.svelte'
|
||||
import AddRowBefore from './icons/table/AddRowBefore.svelte'
|
||||
import DeleteCol from './icons/table/DeleteCol.svelte'
|
||||
import DeleteRow from './icons/table/DeleteRow.svelte'
|
||||
import DeleteTable from './icons/table/DeleteTable.svelte'
|
||||
import LinkPopup from './LinkPopup.svelte'
|
||||
import { defaultExtensions } from './extensions'
|
||||
import TextEditorStyleToolbar from './TextEditorStyleToolbar.svelte'
|
||||
|
||||
export let documentId: string
|
||||
export let readonly = false
|
||||
@ -127,50 +98,16 @@
|
||||
editor.commands.insertContent(text as HTMLContent)
|
||||
}
|
||||
|
||||
export function toggleBold () {
|
||||
editor.commands.toggleBold()
|
||||
}
|
||||
export function toggleItalic () {
|
||||
editor.commands.toggleItalic()
|
||||
}
|
||||
export function toggleStrike () {
|
||||
editor.commands.toggleStrike()
|
||||
}
|
||||
|
||||
export function getHTML (): string | undefined {
|
||||
if (editor) {
|
||||
return editor.getHTML()
|
||||
}
|
||||
}
|
||||
|
||||
export function getLink () {
|
||||
return editor.getAttributes('link').href
|
||||
}
|
||||
export function unsetLink () {
|
||||
editor.chain().focus().extendMarkRange('link').unsetLink().run()
|
||||
}
|
||||
export function setLink (link: string) {
|
||||
editor.chain().focus().extendMarkRange('link').setLink({ href: link }).run()
|
||||
}
|
||||
|
||||
export function checkIsSelectionEmpty () {
|
||||
return editor.view.state.selection.empty
|
||||
}
|
||||
export function toggleOrderedList () {
|
||||
editor.commands.toggleOrderedList()
|
||||
}
|
||||
export function toggleBulletList () {
|
||||
editor.commands.toggleBulletList()
|
||||
}
|
||||
export function toggleBlockquote () {
|
||||
editor.commands.toggleBlockquote()
|
||||
}
|
||||
export function toggleCode () {
|
||||
editor.commands.toggleCode()
|
||||
}
|
||||
export function toggleCodeBlock () {
|
||||
editor.commands.toggleCodeBlock()
|
||||
}
|
||||
|
||||
let needFocus = false
|
||||
|
||||
let focused = false
|
||||
@ -266,11 +203,9 @@
|
||||
},
|
||||
onUpdate: (op: { editor: Editor; transaction: Transaction }) => {
|
||||
dispatch('content', editor.getHTML())
|
||||
updateFormattingState()
|
||||
},
|
||||
onSelectionUpdate: () => {
|
||||
dispatch('selection-update')
|
||||
updateFormattingState()
|
||||
}
|
||||
})
|
||||
|
||||
@ -291,192 +226,6 @@
|
||||
}
|
||||
})
|
||||
|
||||
let activeModes = new Set<FormatMode>()
|
||||
let isSelectionEmpty = true
|
||||
|
||||
export function checkIsActive (formatMode: FormatMode) {
|
||||
return editor.isActive(formatMode)
|
||||
}
|
||||
|
||||
let headingLevel = 0
|
||||
|
||||
function updateFormattingState () {
|
||||
activeModes = new Set(FORMAT_MODES.filter(checkIsActive))
|
||||
for (const l of headingLevels) {
|
||||
if (editor.isActive('heading', { level: l })) {
|
||||
headingLevel = l
|
||||
activeModes.add('heading')
|
||||
}
|
||||
}
|
||||
if (!activeModes.has('heading')) {
|
||||
headingLevel = 0
|
||||
}
|
||||
isSelectionEmpty = editor.view.state.selection.empty
|
||||
}
|
||||
|
||||
function getToggler (toggle: () => void) {
|
||||
return () => {
|
||||
toggle()
|
||||
needFocus = true
|
||||
updateFormattingState()
|
||||
}
|
||||
}
|
||||
|
||||
function toggleHeader (event: MouseEvent) {
|
||||
if (activeModes.has('heading')) {
|
||||
editor.commands.toggleHeading({ level: headingLevel as Level })
|
||||
needFocus = true
|
||||
updateFormattingState()
|
||||
} else {
|
||||
showPopup(
|
||||
SelectPopup,
|
||||
{
|
||||
value: Array.from(headingLevels).map((it) => ({ id: it.toString(), text: it.toString() }))
|
||||
},
|
||||
getEventPositionElement(event),
|
||||
(val) => {
|
||||
if (val !== undefined) {
|
||||
editor.commands.toggleHeading({ level: parseInt(val) as Level })
|
||||
needFocus = true
|
||||
updateFormattingState()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function insertTable (event: MouseEvent) {
|
||||
showPopup(
|
||||
SelectPopup,
|
||||
{
|
||||
value: [
|
||||
{ id: '#delete', label: presentation.string.Remove },
|
||||
...mInsertTable.map((it) => ({ id: it.label, text: it.label }))
|
||||
]
|
||||
},
|
||||
getEventPositionElement(event),
|
||||
(val) => {
|
||||
if (val !== undefined) {
|
||||
if (val === '#delete') {
|
||||
editor.commands.deleteTable()
|
||||
needFocus = true
|
||||
updateFormattingState()
|
||||
return
|
||||
}
|
||||
const tab = mInsertTable.find((it) => it.label === val)
|
||||
if (tab) {
|
||||
editor.commands.insertTable({
|
||||
cols: tab.cols,
|
||||
rows: tab.rows,
|
||||
withHeaderRow: tab.header
|
||||
})
|
||||
|
||||
needFocus = true
|
||||
updateFormattingState()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function tableOptions (event: MouseEvent) {
|
||||
const ops = [
|
||||
{
|
||||
id: '#addColumnBefore',
|
||||
icon: AddColBefore,
|
||||
label: textEditorPlugin.string.AddColumnBefore,
|
||||
action: () => editor.commands.addColumnBefore(),
|
||||
category: {
|
||||
label: textEditorPlugin.string.CategoryColumn
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '#addColumnAfter',
|
||||
icon: AddColAfter,
|
||||
label: textEditorPlugin.string.AddColumnAfter,
|
||||
action: () => editor.commands.addColumnAfter(),
|
||||
category: {
|
||||
label: textEditorPlugin.string.CategoryColumn
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
id: '#deleteColumn',
|
||||
icon: DeleteCol,
|
||||
label: textEditorPlugin.string.DeleteColumn,
|
||||
action: () => editor.commands.deleteColumn(),
|
||||
category: {
|
||||
label: textEditorPlugin.string.CategoryColumn
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '#addRowBefore',
|
||||
icon: AddRowBefore,
|
||||
label: textEditorPlugin.string.AddRowBefore,
|
||||
action: () => editor.commands.addRowBefore(),
|
||||
category: {
|
||||
label: textEditorPlugin.string.CategoryRow
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '#addRowAfter',
|
||||
icon: AddRowAfter,
|
||||
label: textEditorPlugin.string.AddRowAfter,
|
||||
action: () => editor.commands.addRowAfter(),
|
||||
category: {
|
||||
label: textEditorPlugin.string.CategoryRow
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '#deleteRow',
|
||||
icon: DeleteRow,
|
||||
label: textEditorPlugin.string.DeleteRow,
|
||||
action: () => editor.commands.deleteRow(),
|
||||
category: {
|
||||
label: textEditorPlugin.string.CategoryRow
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '#deleteTable',
|
||||
icon: DeleteTable,
|
||||
label: textEditorPlugin.string.DeleteTable,
|
||||
action: () => editor.commands.deleteTable(),
|
||||
category: {
|
||||
label: textEditorPlugin.string.Table
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
showPopup(
|
||||
SelectPopup,
|
||||
{
|
||||
value: ops
|
||||
},
|
||||
getEventPositionElement(event),
|
||||
(val) => {
|
||||
if (val !== undefined) {
|
||||
const op = ops.find((it) => it.id === val)
|
||||
if (op) {
|
||||
op.action()
|
||||
needFocus = true
|
||||
updateFormattingState()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async function formatLink (): Promise<void> {
|
||||
const link = editor.getAttributes('link').href
|
||||
|
||||
showPopup(LinkPopup, { link }, undefined, undefined, (newLink) => {
|
||||
if (newLink === '') {
|
||||
editor.chain().focus().extendMarkRange('link').unsetLink().run()
|
||||
} else {
|
||||
editor.chain().focus().extendMarkRange('link').setLink({ href: newLink }).run()
|
||||
}
|
||||
})
|
||||
}
|
||||
let showDiff = true
|
||||
</script>
|
||||
|
||||
@ -485,99 +234,22 @@
|
||||
{#if isFormatting && !readonly}
|
||||
<div class="formatPanelRef formatPanel flex-between clear-mins">
|
||||
<div class="flex-row-center buttons-group xsmall-gap">
|
||||
<StyleButton
|
||||
icon={Header}
|
||||
size={buttonSize}
|
||||
selected={activeModes.has('heading')}
|
||||
showTooltip={{ label: getEmbeddedLabel(`H${headingLevel}`) }}
|
||||
on:click={toggleHeader}
|
||||
<TextEditorStyleToolbar
|
||||
textEditor={editor}
|
||||
textFormatCategories={[
|
||||
TextFormatCategory.Heading,
|
||||
TextFormatCategory.TextDecoration,
|
||||
TextFormatCategory.Link,
|
||||
TextFormatCategory.List,
|
||||
TextFormatCategory.Quote,
|
||||
TextFormatCategory.Code,
|
||||
TextFormatCategory.Table
|
||||
]}
|
||||
formatButtonSize={buttonSize}
|
||||
on:focus={() => {
|
||||
needFocus = true
|
||||
}}
|
||||
/>
|
||||
|
||||
<StyleButton
|
||||
icon={Bold}
|
||||
size={buttonSize}
|
||||
selected={activeModes.has('bold')}
|
||||
showTooltip={{ label: textEditorPlugin.string.Bold }}
|
||||
on:click={getToggler(toggleBold)}
|
||||
/>
|
||||
<StyleButton
|
||||
icon={Italic}
|
||||
size={buttonSize}
|
||||
selected={activeModes.has('italic')}
|
||||
showTooltip={{ label: textEditorPlugin.string.Italic }}
|
||||
on:click={getToggler(toggleItalic)}
|
||||
/>
|
||||
<StyleButton
|
||||
icon={Strikethrough}
|
||||
size={buttonSize}
|
||||
selected={activeModes.has('strike')}
|
||||
showTooltip={{ label: textEditorPlugin.string.Strikethrough }}
|
||||
on:click={getToggler(toggleStrike)}
|
||||
/>
|
||||
<StyleButton
|
||||
icon={LinkEl}
|
||||
size={buttonSize}
|
||||
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={buttonSize}
|
||||
selected={activeModes.has('orderedList')}
|
||||
showTooltip={{ label: textEditorPlugin.string.OrderedList }}
|
||||
on:click={getToggler(toggleOrderedList)}
|
||||
/>
|
||||
<StyleButton
|
||||
icon={ListBullet}
|
||||
size={buttonSize}
|
||||
selected={activeModes.has('bulletList')}
|
||||
showTooltip={{ label: textEditorPlugin.string.BulletedList }}
|
||||
on:click={getToggler(toggleBulletList)}
|
||||
/>
|
||||
<div class="buttons-divider" />
|
||||
<StyleButton
|
||||
icon={Quote}
|
||||
size={buttonSize}
|
||||
selected={activeModes.has('blockquote')}
|
||||
showTooltip={{ label: textEditorPlugin.string.Blockquote }}
|
||||
on:click={getToggler(toggleBlockquote)}
|
||||
/>
|
||||
<div class="buttons-divider" />
|
||||
<StyleButton
|
||||
icon={Code}
|
||||
size={buttonSize}
|
||||
selected={activeModes.has('code')}
|
||||
showTooltip={{ label: textEditorPlugin.string.Code }}
|
||||
on:click={getToggler(toggleCode)}
|
||||
/>
|
||||
<StyleButton
|
||||
icon={CodeBlock}
|
||||
size={buttonSize}
|
||||
selected={activeModes.has('codeBlock')}
|
||||
showTooltip={{ label: textEditorPlugin.string.CodeBlock }}
|
||||
on:click={getToggler(toggleCodeBlock)}
|
||||
/>
|
||||
|
||||
<StyleButton
|
||||
icon={IconTable}
|
||||
iconProps={{ style: 'table' }}
|
||||
size={buttonSize}
|
||||
selected={activeModes.has('table')}
|
||||
on:click={insertTable}
|
||||
showTooltip={{ label: textEditorPlugin.string.InsertTable }}
|
||||
/>
|
||||
{#if activeModes.has('table')}
|
||||
<StyleButton
|
||||
icon={IconTable}
|
||||
iconProps={{ style: 'tableProps' }}
|
||||
size={buttonSize}
|
||||
on:click={tableOptions}
|
||||
showTooltip={{ label: textEditorPlugin.string.TableOptions }}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex-grow" />
|
||||
{#if comparedVersion !== undefined}
|
||||
|
@ -17,7 +17,6 @@
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import {
|
||||
AnySvelteComponent,
|
||||
Button,
|
||||
EmojiPopup,
|
||||
Icon,
|
||||
IconEmoji,
|
||||
@ -32,22 +31,11 @@
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { Completion } from '../Completion'
|
||||
import textEditorPlugin from '../plugin'
|
||||
import { FORMAT_MODES, FormatMode, RefAction, RefInputActionItem, TextEditorHandler } from '../types'
|
||||
import LinkPopup from './LinkPopup.svelte'
|
||||
import { RefAction, RefInputActionItem, TextEditorHandler, TextFormatCategory } from '../types'
|
||||
import TextEditor from './TextEditor.svelte'
|
||||
import { completionConfig } from './extensions'
|
||||
import Attach from './icons/Attach.svelte'
|
||||
import CodeBlock from './icons/CodeBlock.svelte'
|
||||
import ListBullet from './icons/ListBullet.svelte'
|
||||
import ListNumber from './icons/ListNumber.svelte'
|
||||
import Quote from './icons/Quote.svelte'
|
||||
import Bold from './icons/Bold.svelte'
|
||||
import Code from './icons/Code.svelte'
|
||||
import Italic from './icons/Italic.svelte'
|
||||
import Link from './icons/Link.svelte'
|
||||
import RIMention from './icons/RIMention.svelte'
|
||||
import RIStrikethrough from './icons/RIStrikethrough.svelte'
|
||||
import Underline from './icons/Underline.svelte'
|
||||
import Send from './icons/Send.svelte'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
@ -64,10 +52,7 @@
|
||||
const client = getClient()
|
||||
|
||||
let textEditor: TextEditor
|
||||
let textEditorToolbar: HTMLElement
|
||||
|
||||
let activeModes = new Set<FormatMode>()
|
||||
let isSelectionEmpty = true
|
||||
let isEmpty = true
|
||||
|
||||
$: setContent(content)
|
||||
@ -142,34 +127,6 @@
|
||||
a.action(evt?.target as HTMLElement, editorHandler)
|
||||
}
|
||||
|
||||
function updateFormattingState () {
|
||||
if (textEditor?.checkIsActive === undefined) {
|
||||
return
|
||||
}
|
||||
activeModes = new Set(FORMAT_MODES.filter(textEditor.checkIsActive))
|
||||
isSelectionEmpty = textEditor.checkIsSelectionEmpty()
|
||||
}
|
||||
|
||||
function getToggler (toggle: () => void) {
|
||||
return () => {
|
||||
toggle()
|
||||
textEditor.focus()
|
||||
updateFormattingState()
|
||||
}
|
||||
}
|
||||
|
||||
async function formatLink (): Promise<void> {
|
||||
const link = textEditor.getLink()
|
||||
|
||||
showPopup(LinkPopup, { link }, undefined, undefined, (newLink) => {
|
||||
if (newLink === '') {
|
||||
textEditor.unsetLink()
|
||||
} else {
|
||||
textEditor.setLink(newLink)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Focusable control with index
|
||||
let focused = false
|
||||
export let focusIndex = -1
|
||||
@ -198,92 +155,6 @@
|
||||
</script>
|
||||
|
||||
<div class="ref-container">
|
||||
<div class="formatPanel buttons-group xsmall-gap mb-4" class:withoutTopBorder bind:this={textEditorToolbar}>
|
||||
<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={RIStrikethrough}
|
||||
kind={'ghost'}
|
||||
size={'small'}
|
||||
selected={activeModes.has('strike')}
|
||||
showTooltip={{ label: textEditorPlugin.string.Strikethrough }}
|
||||
on:click={getToggler(textEditor.toggleStrike)}
|
||||
/>
|
||||
<Button
|
||||
icon={Underline}
|
||||
kind={'ghost'}
|
||||
size={'small'}
|
||||
selected={activeModes.has('underline')}
|
||||
showTooltip={{ label: textEditorPlugin.string.Underlined }}
|
||||
on:click={getToggler(textEditor.toggleUnderline)}
|
||||
/>
|
||||
<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>
|
||||
<div class="textInput" class:withoutTopBorder>
|
||||
<div class="inputMsg">
|
||||
<TextEditor
|
||||
@ -307,10 +178,15 @@
|
||||
dispatch('focus', focused)
|
||||
}}
|
||||
extensions={[completionPlugin]}
|
||||
on:selection-update={updateFormattingState}
|
||||
on:update
|
||||
placeholder={placeholder ?? textEditorPlugin.string.EditorPlaceholder}
|
||||
{textEditorToolbar}
|
||||
textFormatCategories={[
|
||||
TextFormatCategory.TextDecoration,
|
||||
TextFormatCategory.Link,
|
||||
TextFormatCategory.List,
|
||||
TextFormatCategory.Quote,
|
||||
TextFormatCategory.Code
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
{#if showSend}
|
||||
@ -371,10 +247,6 @@
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.buttons-divider {
|
||||
height: 1rem;
|
||||
max-height: 1rem;
|
||||
}
|
||||
.icon-button {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@ -400,21 +272,6 @@
|
||||
flex-direction: column;
|
||||
min-height: 4.5rem;
|
||||
|
||||
.formatPanelRef {
|
||||
padding: 0.5rem;
|
||||
background-color: var(--theme-comp-header-color);
|
||||
border: 1px solid var(--theme-refinput-divider);
|
||||
border-radius: 0.5rem 0.5rem 0 0;
|
||||
border-bottom: 0;
|
||||
|
||||
&.withoutTopBorder {
|
||||
border-radius: 0;
|
||||
}
|
||||
& + .textInput {
|
||||
border-top: none;
|
||||
}
|
||||
}
|
||||
|
||||
.textInput {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@ -484,14 +341,5 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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>
|
||||
|
@ -13,46 +13,14 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Asset, getEmbeddedLabel, getResource, IntlString } from '@hcengineering/platform'
|
||||
import presentation, { getClient } from '@hcengineering/presentation'
|
||||
import {
|
||||
AnySvelteComponent,
|
||||
EmojiPopup,
|
||||
getEventPositionElement,
|
||||
IconEmoji,
|
||||
IconSize,
|
||||
Scroller,
|
||||
SelectPopup,
|
||||
showPopup
|
||||
} from '@hcengineering/ui'
|
||||
import { Level } from '@tiptap/extension-heading'
|
||||
import { Asset, getResource, IntlString } from '@hcengineering/platform'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { AnySvelteComponent, EmojiPopup, IconEmoji, IconSize, Scroller, showPopup } from '@hcengineering/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import textEditorPlugin from '../plugin'
|
||||
import { FORMAT_MODES, FormatMode, RefInputAction, RefInputActionItem, TextEditorHandler } from '../types'
|
||||
import { headingLevels, mInsertTable } from './extensions'
|
||||
import { RefInputAction, RefInputActionItem, TextEditorHandler, TextFormatCategory } from '../types'
|
||||
import Attach from './icons/Attach.svelte'
|
||||
import CodeBlock from './icons/CodeBlock.svelte'
|
||||
import Header1 from './icons/Header1.svelte'
|
||||
import Header2 from './icons/Header2.svelte'
|
||||
import IconTable from './icons/IconTable.svelte'
|
||||
import ListBullet from './icons/ListBullet.svelte'
|
||||
import ListNumber from './icons/ListNumber.svelte'
|
||||
import Quote from './icons/Quote.svelte'
|
||||
import Bold from './icons/Bold.svelte'
|
||||
import Code from './icons/Code.svelte'
|
||||
import Italic from './icons/Italic.svelte'
|
||||
import Link from './icons/Link.svelte'
|
||||
import RIStrikethrough from './icons/RIStrikethrough.svelte'
|
||||
import Underline from './icons/Underline.svelte'
|
||||
import { AnyExtension } from '@tiptap/core'
|
||||
import AddColAfter from './icons/table/AddColAfter.svelte'
|
||||
import AddColBefore from './icons/table/AddColBefore.svelte'
|
||||
import AddRowAfter from './icons/table/AddRowAfter.svelte'
|
||||
import AddRowBefore from './icons/table/AddRowBefore.svelte'
|
||||
import DeleteCol from './icons/table/DeleteCol.svelte'
|
||||
import DeleteRow from './icons/table/DeleteRow.svelte'
|
||||
import DeleteTable from './icons/table/DeleteTable.svelte'
|
||||
import LinkPopup from './LinkPopup.svelte'
|
||||
import StyleButton from './StyleButton.svelte'
|
||||
import TextEditor from './TextEditor.svelte'
|
||||
import { Node as ProseMirrorNode } from '@tiptap/pm/model'
|
||||
@ -68,14 +36,21 @@
|
||||
export let isScrollable: boolean = true
|
||||
export let focusable: boolean = false
|
||||
export let maxHeight: 'max' | 'card' | 'limited' | string | undefined = undefined
|
||||
export let withoutTopBorder = false
|
||||
export let enableFormatting = false
|
||||
export let autofocus = false
|
||||
export let full = false
|
||||
export let extensions: AnyExtension[] = []
|
||||
export let textFormatCategories: TextFormatCategory[] = [
|
||||
TextFormatCategory.Heading,
|
||||
TextFormatCategory.TextDecoration,
|
||||
TextFormatCategory.Link,
|
||||
TextFormatCategory.List,
|
||||
TextFormatCategory.Quote,
|
||||
TextFormatCategory.Code,
|
||||
TextFormatCategory.Table
|
||||
]
|
||||
|
||||
let textEditor: TextEditor
|
||||
let textEditorToolbar: HTMLElement
|
||||
|
||||
let isEmpty = true
|
||||
let contentHeight: number
|
||||
@ -122,9 +97,6 @@
|
||||
? 'max-content'
|
||||
: maxHeight
|
||||
|
||||
let activeModes = new Set<FormatMode>()
|
||||
let isSelectionEmpty = true
|
||||
|
||||
interface RefAction {
|
||||
label: IntlString
|
||||
icon: Asset | AnySvelteComponent
|
||||
@ -176,43 +148,6 @@
|
||||
actions = defActions.concat(...cont).sort((a, b) => a.order - b.order)
|
||||
})
|
||||
|
||||
function updateFormattingState () {
|
||||
if (textEditor?.checkIsActive === undefined) {
|
||||
return
|
||||
}
|
||||
activeModes = new Set(FORMAT_MODES.filter(textEditor.checkIsActive))
|
||||
for (const l of headingLevels) {
|
||||
if (textEditor.checkIsActive('heading', { level: l })) {
|
||||
activeModes.add('heading')
|
||||
if (l === 1) {
|
||||
activeModes.add('heading1')
|
||||
} else if (l === 2) {
|
||||
activeModes.add('heading2')
|
||||
}
|
||||
}
|
||||
}
|
||||
isSelectionEmpty = textEditor.checkIsSelectionEmpty()
|
||||
}
|
||||
|
||||
function getToggler (toggle: () => void) {
|
||||
return () => {
|
||||
toggle()
|
||||
textEditor.focus()
|
||||
updateFormattingState()
|
||||
}
|
||||
}
|
||||
|
||||
async function formatLink (): Promise<void> {
|
||||
const link = textEditor.getLink()
|
||||
|
||||
showPopup(LinkPopup, { link }, undefined, undefined, (newLink) => {
|
||||
if (newLink === '') {
|
||||
textEditor.unsetLink()
|
||||
} else {
|
||||
textEditor.setLink(newLink)
|
||||
}
|
||||
})
|
||||
}
|
||||
const editorHandler: TextEditorHandler = {
|
||||
insertText: (text) => {
|
||||
textEditor.insertText(text)
|
||||
@ -234,135 +169,6 @@
|
||||
needFocus = false
|
||||
}
|
||||
|
||||
function getHeaderToggler (level: Level) {
|
||||
return () => {
|
||||
textEditor.toggleHeading({ level })
|
||||
needFocus = true
|
||||
updateFormattingState()
|
||||
}
|
||||
}
|
||||
|
||||
function insertTable (event: MouseEvent) {
|
||||
showPopup(
|
||||
SelectPopup,
|
||||
{
|
||||
value: [
|
||||
{ id: '#delete', label: presentation.string.Remove },
|
||||
...mInsertTable.map((it) => ({ id: it.label, text: it.label }))
|
||||
]
|
||||
},
|
||||
getEventPositionElement(event),
|
||||
(val) => {
|
||||
if (val !== undefined) {
|
||||
if (val === '#delete') {
|
||||
textEditor.deleteTable()
|
||||
needFocus = true
|
||||
updateFormattingState()
|
||||
return
|
||||
}
|
||||
const tab = mInsertTable.find((it) => it.label === val)
|
||||
if (tab) {
|
||||
textEditor.insertTable({
|
||||
cols: tab.cols,
|
||||
rows: tab.rows,
|
||||
withHeaderRow: tab.header
|
||||
})
|
||||
|
||||
needFocus = true
|
||||
updateFormattingState()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function tableOptions (event: MouseEvent) {
|
||||
const ops = [
|
||||
{
|
||||
id: '#addColumnBefore',
|
||||
icon: AddColBefore,
|
||||
label: textEditorPlugin.string.AddColumnBefore,
|
||||
action: () => textEditor.addColumnBefore(),
|
||||
category: {
|
||||
label: textEditorPlugin.string.CategoryColumn
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '#addColumnAfter',
|
||||
icon: AddColAfter,
|
||||
label: textEditorPlugin.string.AddColumnAfter,
|
||||
action: () => textEditor.addColumnAfter(),
|
||||
category: {
|
||||
label: textEditorPlugin.string.CategoryColumn
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
id: '#deleteColumn',
|
||||
icon: DeleteCol,
|
||||
label: textEditorPlugin.string.DeleteColumn,
|
||||
action: () => textEditor.deleteColumn(),
|
||||
category: {
|
||||
label: textEditorPlugin.string.CategoryColumn
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '#addRowBefore',
|
||||
icon: AddRowBefore,
|
||||
label: textEditorPlugin.string.AddRowBefore,
|
||||
action: () => textEditor.addRowBefore(),
|
||||
category: {
|
||||
label: textEditorPlugin.string.CategoryRow
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '#addRowAfter',
|
||||
icon: AddRowAfter,
|
||||
label: textEditorPlugin.string.AddRowAfter,
|
||||
action: () => textEditor.addRowAfter(),
|
||||
category: {
|
||||
label: textEditorPlugin.string.CategoryRow
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '#deleteRow',
|
||||
icon: DeleteRow,
|
||||
label: textEditorPlugin.string.DeleteRow,
|
||||
action: () => textEditor.deleteRow(),
|
||||
category: {
|
||||
label: textEditorPlugin.string.CategoryRow
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '#deleteTable',
|
||||
icon: DeleteTable,
|
||||
label: textEditorPlugin.string.DeleteTable,
|
||||
action: () => textEditor.deleteTable(),
|
||||
category: {
|
||||
label: textEditorPlugin.string.Table
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
showPopup(
|
||||
SelectPopup,
|
||||
{
|
||||
value: ops
|
||||
},
|
||||
getEventPositionElement(event),
|
||||
(val) => {
|
||||
if (val !== undefined) {
|
||||
const op = ops.find((it) => it.id === val)
|
||||
if (op) {
|
||||
op.action()
|
||||
needFocus = true
|
||||
updateFormattingState()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const buttonsGap = 'small-gap'
|
||||
|
||||
$: buttonsHeight =
|
||||
@ -387,113 +193,6 @@
|
||||
tabindex="-1"
|
||||
on:click|preventDefault|stopPropagation={() => (needFocus = true)}
|
||||
>
|
||||
<div class="formatPanel buttons-group xsmall-gap mb-4" class:withoutTopBorder bind:this={textEditorToolbar}>
|
||||
<StyleButton
|
||||
icon={Header1}
|
||||
size={formatButtonSize}
|
||||
selected={activeModes.has('heading1')}
|
||||
showTooltip={{ label: getEmbeddedLabel('H1') }}
|
||||
on:click={getHeaderToggler(1)}
|
||||
/>
|
||||
<StyleButton
|
||||
icon={Header2}
|
||||
size={formatButtonSize}
|
||||
selected={activeModes.has('heading2')}
|
||||
showTooltip={{ label: getEmbeddedLabel('H2') }}
|
||||
on:click={getHeaderToggler(2)}
|
||||
/>
|
||||
<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={RIStrikethrough}
|
||||
size={formatButtonSize}
|
||||
selected={activeModes.has('strike')}
|
||||
showTooltip={{ label: textEditorPlugin.string.Strikethrough }}
|
||||
on:click={getToggler(textEditor.toggleStrike)}
|
||||
/>
|
||||
<StyleButton
|
||||
icon={Underline}
|
||||
size={formatButtonSize}
|
||||
selected={activeModes.has('underline')}
|
||||
showTooltip={{ label: textEditorPlugin.string.Underlined }}
|
||||
on:click={getToggler(textEditor.toggleUnderline)}
|
||||
/>
|
||||
<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)}
|
||||
/>
|
||||
<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: 'tableProps' }}
|
||||
size={formatButtonSize}
|
||||
on:click={tableOptions}
|
||||
showTooltip={{ label: textEditorPlugin.string.TableOptions }}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="textInput" class:focusable>
|
||||
<div
|
||||
bind:clientHeight={contentHeight}
|
||||
@ -507,6 +206,7 @@
|
||||
bind:content
|
||||
{placeholder}
|
||||
{extensions}
|
||||
{textFormatCategories}
|
||||
bind:this={textEditor}
|
||||
bind:isEmpty
|
||||
on:value
|
||||
@ -518,8 +218,6 @@
|
||||
on:blur
|
||||
on:focus
|
||||
supportSubmit={false}
|
||||
{textEditorToolbar}
|
||||
on:selection-update={updateFormattingState}
|
||||
/>
|
||||
</Scroller>
|
||||
{:else}
|
||||
@ -527,6 +225,7 @@
|
||||
bind:content
|
||||
{placeholder}
|
||||
{extensions}
|
||||
{textFormatCategories}
|
||||
bind:this={textEditor}
|
||||
bind:isEmpty
|
||||
on:value
|
||||
@ -538,8 +237,6 @@
|
||||
on:blur
|
||||
on:focus
|
||||
supportSubmit={false}
|
||||
{textEditorToolbar}
|
||||
on:selection-update={updateFormattingState}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
@ -628,14 +325,5 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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>
|
||||
|
@ -18,23 +18,24 @@
|
||||
|
||||
import { FocusPosition } from '@tiptap/core'
|
||||
import { AnyExtension, Editor, Extension, HTMLContent, isTextSelection } 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'
|
||||
import { defaultExtensions } from './extensions'
|
||||
import { Node as ProseMirrorNode } from '@tiptap/pm/model'
|
||||
import { themeStore } from '@hcengineering/ui'
|
||||
import TextEditorStyleToolbar from './TextEditorStyleToolbar.svelte'
|
||||
import { TextFormatCategory } from '../types'
|
||||
|
||||
export let content: string = ''
|
||||
export let placeholder: IntlString = textEditorPlugin.string.EditorPlaceholder
|
||||
export let extensions: AnyExtension[] = []
|
||||
export let textFormatCategories: TextFormatCategory[] = []
|
||||
export let supportSubmit = true
|
||||
export let isEmpty = true
|
||||
export let textEditorToolbar: HTMLElement | null = null
|
||||
|
||||
let element: HTMLElement
|
||||
let editor: Editor
|
||||
@ -75,79 +76,6 @@
|
||||
export function insertText (text: string): void {
|
||||
editor.commands.insertContent(text as HTMLContent)
|
||||
}
|
||||
export function checkIsActive (formatMode: FormatMode, attributes?: {} | undefined) {
|
||||
return editor.isActive(formatMode, attributes)
|
||||
}
|
||||
export function toggleBold () {
|
||||
editor.commands.toggleBold()
|
||||
}
|
||||
export function toggleItalic () {
|
||||
editor.commands.toggleItalic()
|
||||
}
|
||||
export function toggleStrike () {
|
||||
editor.commands.toggleStrike()
|
||||
}
|
||||
export function toggleUnderline () {
|
||||
editor.commands.toggleUnderline()
|
||||
}
|
||||
export function getLink () {
|
||||
return editor.getAttributes('link').href
|
||||
}
|
||||
export function unsetLink () {
|
||||
editor.chain().focus().extendMarkRange('link').unsetLink().run()
|
||||
}
|
||||
export function setLink (link: string) {
|
||||
editor.chain().focus().extendMarkRange('link').setLink({ href: link }).run()
|
||||
}
|
||||
export function checkIsSelectionEmpty () {
|
||||
return editor.view.state.selection.empty
|
||||
}
|
||||
export function toggleOrderedList () {
|
||||
editor.commands.toggleOrderedList()
|
||||
}
|
||||
export function toggleBulletList () {
|
||||
editor.commands.toggleBulletList()
|
||||
}
|
||||
export function toggleBlockquote () {
|
||||
editor.commands.toggleBlockquote()
|
||||
}
|
||||
export function toggleCode () {
|
||||
editor.commands.toggleCode()
|
||||
}
|
||||
export function toggleCodeBlock () {
|
||||
editor.commands.toggleCodeBlock()
|
||||
}
|
||||
export function toggleHeading (attributes: { level: Level }) {
|
||||
editor.commands.toggleHeading(attributes)
|
||||
}
|
||||
export function addColumnBefore () {
|
||||
editor.commands.addColumnBefore()
|
||||
}
|
||||
export function addColumnAfter () {
|
||||
editor.commands.addColumnAfter()
|
||||
}
|
||||
export function deleteColumn () {
|
||||
editor.commands.deleteColumn()
|
||||
}
|
||||
export function addRowBefore () {
|
||||
editor.commands.addRowBefore()
|
||||
}
|
||||
export function addRowAfter () {
|
||||
editor.commands.addRowAfter()
|
||||
}
|
||||
export function deleteRow () {
|
||||
editor.commands.deleteRow()
|
||||
}
|
||||
export function deleteTable () {
|
||||
editor.commands.deleteTable()
|
||||
}
|
||||
export function insertTable (options?: { rows?: number; cols?: number; withHeaderRow?: boolean }) {
|
||||
editor.commands.insertTable({
|
||||
cols: options?.cols ?? 2,
|
||||
rows: options?.rows ?? 1,
|
||||
withHeaderRow: options?.withHeaderRow
|
||||
})
|
||||
}
|
||||
|
||||
export function isEmptyContent (): boolean {
|
||||
return isEmpty
|
||||
@ -157,6 +85,7 @@
|
||||
let focused = false
|
||||
let posFocus: FocusPosition | undefined = undefined
|
||||
let showContextMenu = false
|
||||
let textEditorToolbar: HTMLElement
|
||||
|
||||
export function focus (position?: FocusPosition): void {
|
||||
posFocus = position
|
||||
@ -216,7 +145,9 @@
|
||||
...extensions,
|
||||
BubbleMenu.configure({
|
||||
element: textEditorToolbar,
|
||||
|
||||
tippyOptions: {
|
||||
maxWidth: '38rem'
|
||||
},
|
||||
// to override shouldShow behaviour a little
|
||||
// I need to copypaste original function and make a little change
|
||||
// with showContextMenu falg
|
||||
@ -318,6 +249,15 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="formatPanel buttons-group xsmall-gap mb-4" bind:this={textEditorToolbar}>
|
||||
<TextEditorStyleToolbar
|
||||
textEditor={editor}
|
||||
{textFormatCategories}
|
||||
on:focus={() => {
|
||||
needFocus = true
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class="select-text" style="width: 100%;" on:mousedown={onEditorClick} bind:this={element} />
|
||||
|
||||
<style lang="scss" global>
|
||||
@ -363,4 +303,13 @@
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.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>
|
||||
|
@ -0,0 +1,321 @@
|
||||
<!--
|
||||
// Copyright © 2020 Anticrm Platform Contributors.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { getEmbeddedLabel } from '@hcengineering/platform'
|
||||
import presentation from '@hcengineering/presentation'
|
||||
import { getEventPositionElement, IconSize, SelectPopup, showPopup } from '@hcengineering/ui'
|
||||
import { Editor } from '@tiptap/core'
|
||||
import { Level } from '@tiptap/extension-heading'
|
||||
import textEditorPlugin from '../plugin'
|
||||
import { TextFormatCategory } from '../types'
|
||||
import { mInsertTable } from './extensions'
|
||||
import Bold from './icons/Bold.svelte'
|
||||
import Code from './icons/Code.svelte'
|
||||
import CodeBlock from './icons/CodeBlock.svelte'
|
||||
import Header1 from './icons/Header1.svelte'
|
||||
import Header2 from './icons/Header2.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'
|
||||
import RIStrikethrough from './icons/RIStrikethrough.svelte'
|
||||
import Underline from './icons/Underline.svelte'
|
||||
import AddColAfter from './icons/table/AddColAfter.svelte'
|
||||
import AddColBefore from './icons/table/AddColBefore.svelte'
|
||||
import AddRowAfter from './icons/table/AddRowAfter.svelte'
|
||||
import AddRowBefore from './icons/table/AddRowBefore.svelte'
|
||||
import DeleteCol from './icons/table/DeleteCol.svelte'
|
||||
import DeleteRow from './icons/table/DeleteRow.svelte'
|
||||
import DeleteTable from './icons/table/DeleteTable.svelte'
|
||||
import LinkPopup from './LinkPopup.svelte'
|
||||
import StyleButton from './StyleButton.svelte'
|
||||
|
||||
export let formatButtonSize: IconSize = 'small'
|
||||
export let textEditor: Editor
|
||||
export let textFormatCategories: TextFormatCategory[] = []
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
function getToggler (toggle: () => void) {
|
||||
return () => {
|
||||
toggle()
|
||||
dispatch('focus')
|
||||
}
|
||||
}
|
||||
|
||||
async function formatLink (): Promise<void> {
|
||||
const link = textEditor.getAttributes('link').href
|
||||
|
||||
showPopup(LinkPopup, { link }, undefined, undefined, (newLink) => {
|
||||
if (newLink === '') {
|
||||
textEditor.chain().focus().extendMarkRange('link').unsetLink().run()
|
||||
} else {
|
||||
textEditor.chain().focus().extendMarkRange('link').setLink({ href: newLink }).run()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function getHeaderToggler (level: Level) {
|
||||
return () => {
|
||||
textEditor.commands.toggleHeading({ level })
|
||||
dispatch('focus')
|
||||
}
|
||||
}
|
||||
|
||||
function insertTable (event: MouseEvent) {
|
||||
showPopup(
|
||||
SelectPopup,
|
||||
{
|
||||
value: [
|
||||
{ id: '#delete', label: presentation.string.Remove },
|
||||
...mInsertTable.map((it) => ({ id: it.label, text: it.label }))
|
||||
]
|
||||
},
|
||||
getEventPositionElement(event),
|
||||
(val) => {
|
||||
if (val !== undefined) {
|
||||
if (val === '#delete') {
|
||||
textEditor.commands.deleteTable()
|
||||
dispatch('focus')
|
||||
return
|
||||
}
|
||||
const tab = mInsertTable.find((it) => it.label === val)
|
||||
if (tab) {
|
||||
textEditor.commands.insertTable({
|
||||
cols: tab.cols,
|
||||
rows: tab.rows,
|
||||
withHeaderRow: tab.header
|
||||
})
|
||||
|
||||
dispatch('focus')
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function tableOptions (event: MouseEvent) {
|
||||
const ops = [
|
||||
{
|
||||
id: '#addColumnBefore',
|
||||
icon: AddColBefore,
|
||||
label: textEditorPlugin.string.AddColumnBefore,
|
||||
action: () => textEditor.commands.addColumnBefore(),
|
||||
category: {
|
||||
label: textEditorPlugin.string.CategoryColumn
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '#addColumnAfter',
|
||||
icon: AddColAfter,
|
||||
label: textEditorPlugin.string.AddColumnAfter,
|
||||
action: () => textEditor.commands.addColumnAfter(),
|
||||
category: {
|
||||
label: textEditorPlugin.string.CategoryColumn
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
id: '#deleteColumn',
|
||||
icon: DeleteCol,
|
||||
label: textEditorPlugin.string.DeleteColumn,
|
||||
action: () => textEditor.commands.deleteColumn(),
|
||||
category: {
|
||||
label: textEditorPlugin.string.CategoryColumn
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '#addRowBefore',
|
||||
icon: AddRowBefore,
|
||||
label: textEditorPlugin.string.AddRowBefore,
|
||||
action: () => textEditor.commands.addRowBefore(),
|
||||
category: {
|
||||
label: textEditorPlugin.string.CategoryRow
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '#addRowAfter',
|
||||
icon: AddRowAfter,
|
||||
label: textEditorPlugin.string.AddRowAfter,
|
||||
action: () => textEditor.commands.addRowAfter(),
|
||||
category: {
|
||||
label: textEditorPlugin.string.CategoryRow
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '#deleteRow',
|
||||
icon: DeleteRow,
|
||||
label: textEditorPlugin.string.DeleteRow,
|
||||
action: () => textEditor.commands.deleteRow(),
|
||||
category: {
|
||||
label: textEditorPlugin.string.CategoryRow
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '#deleteTable',
|
||||
icon: DeleteTable,
|
||||
label: textEditorPlugin.string.DeleteTable,
|
||||
action: () => textEditor.commands.deleteTable(),
|
||||
category: {
|
||||
label: textEditorPlugin.string.Table
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
showPopup(
|
||||
SelectPopup,
|
||||
{
|
||||
value: ops
|
||||
},
|
||||
getEventPositionElement(event),
|
||||
(val) => {
|
||||
if (val !== undefined) {
|
||||
const op = ops.find((it) => it.id === val)
|
||||
if (op) {
|
||||
op.action()
|
||||
dispatch('focus')
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if textEditor}
|
||||
{#each textFormatCategories as category, index}
|
||||
{#if category === TextFormatCategory.Heading}
|
||||
<StyleButton
|
||||
icon={Header1}
|
||||
size={formatButtonSize}
|
||||
selected={textEditor.isActive('heading', { level: 1 })}
|
||||
showTooltip={{ label: getEmbeddedLabel('H1') }}
|
||||
on:click={getHeaderToggler(1)}
|
||||
/>
|
||||
<StyleButton
|
||||
icon={Header2}
|
||||
size={formatButtonSize}
|
||||
selected={textEditor.isActive('heading', { level: 2 })}
|
||||
showTooltip={{ label: getEmbeddedLabel('H2') }}
|
||||
on:click={getHeaderToggler(2)}
|
||||
/>
|
||||
{/if}
|
||||
{#if category === TextFormatCategory.TextDecoration}
|
||||
<StyleButton
|
||||
icon={Bold}
|
||||
size={formatButtonSize}
|
||||
selected={textEditor.isActive('bold')}
|
||||
showTooltip={{ label: textEditorPlugin.string.Bold }}
|
||||
on:click={getToggler(textEditor.commands.toggleBold)}
|
||||
/>
|
||||
<StyleButton
|
||||
icon={Italic}
|
||||
size={formatButtonSize}
|
||||
selected={textEditor.isActive('italic')}
|
||||
showTooltip={{ label: textEditorPlugin.string.Italic }}
|
||||
on:click={getToggler(textEditor.commands.toggleItalic)}
|
||||
/>
|
||||
<StyleButton
|
||||
icon={RIStrikethrough}
|
||||
size={formatButtonSize}
|
||||
selected={textEditor.isActive('strike')}
|
||||
showTooltip={{ label: textEditorPlugin.string.Strikethrough }}
|
||||
on:click={getToggler(textEditor.commands.toggleStrike)}
|
||||
/>
|
||||
<StyleButton
|
||||
icon={Underline}
|
||||
size={formatButtonSize}
|
||||
selected={textEditor.isActive('underline')}
|
||||
showTooltip={{ label: textEditorPlugin.string.Underlined }}
|
||||
on:click={getToggler(textEditor.commands.toggleUnderline)}
|
||||
/>
|
||||
{/if}
|
||||
{#if category === TextFormatCategory.Link}
|
||||
<StyleButton
|
||||
icon={Link}
|
||||
size={formatButtonSize}
|
||||
selected={textEditor.isActive('link')}
|
||||
disabled={textEditor.view.state.selection.empty && !textEditor.isActive('link')}
|
||||
showTooltip={{ label: textEditorPlugin.string.Link }}
|
||||
on:click={formatLink}
|
||||
/>
|
||||
{/if}
|
||||
{#if category === TextFormatCategory.List}
|
||||
<StyleButton
|
||||
icon={ListNumber}
|
||||
size={formatButtonSize}
|
||||
selected={textEditor.isActive('orderedList')}
|
||||
showTooltip={{ label: textEditorPlugin.string.OrderedList }}
|
||||
on:click={getToggler(textEditor.commands.toggleOrderedList)}
|
||||
/>
|
||||
<StyleButton
|
||||
icon={ListBullet}
|
||||
size={formatButtonSize}
|
||||
selected={textEditor.isActive('bulletList')}
|
||||
showTooltip={{ label: textEditorPlugin.string.BulletedList }}
|
||||
on:click={getToggler(textEditor.commands.toggleBulletList)}
|
||||
/>
|
||||
{/if}
|
||||
{#if category === TextFormatCategory.Quote}
|
||||
<StyleButton
|
||||
icon={Quote}
|
||||
size={formatButtonSize}
|
||||
selected={textEditor.isActive('blockquote')}
|
||||
showTooltip={{ label: textEditorPlugin.string.Blockquote }}
|
||||
on:click={getToggler(textEditor.commands.toggleBlockquote)}
|
||||
/>
|
||||
{/if}
|
||||
{#if category === TextFormatCategory.Code}
|
||||
<StyleButton
|
||||
icon={Code}
|
||||
size={formatButtonSize}
|
||||
selected={textEditor.isActive('code')}
|
||||
showTooltip={{ label: textEditorPlugin.string.Code }}
|
||||
on:click={getToggler(textEditor.commands.toggleCode)}
|
||||
/>
|
||||
<StyleButton
|
||||
icon={CodeBlock}
|
||||
size={formatButtonSize}
|
||||
selected={textEditor.isActive('codeBlock')}
|
||||
showTooltip={{ label: textEditorPlugin.string.CodeBlock }}
|
||||
on:click={getToggler(textEditor.commands.toggleCodeBlock)}
|
||||
/>
|
||||
{/if}
|
||||
{#if category === TextFormatCategory.Table}
|
||||
<StyleButton
|
||||
icon={IconTable}
|
||||
iconProps={{ style: 'table' }}
|
||||
size={formatButtonSize}
|
||||
selected={textEditor.isActive('table')}
|
||||
on:click={insertTable}
|
||||
showTooltip={{ label: textEditorPlugin.string.InsertTable }}
|
||||
/>
|
||||
{#if textEditor.isActive('table')}
|
||||
<StyleButton
|
||||
icon={IconTable}
|
||||
iconProps={{ style: 'tableProps' }}
|
||||
size={formatButtonSize}
|
||||
on:click={tableOptions}
|
||||
showTooltip={{ label: textEditorPlugin.string.TableOptions }}
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if index < textFormatCategories.length - 1}
|
||||
<div class="buttons-divider" />
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
@ -42,7 +42,7 @@ export const taskListExtensions = [
|
||||
})
|
||||
]
|
||||
|
||||
export const headingLevels: Level[] = [1, 2, 3, 4, 5, 6]
|
||||
export const supportedHeadingLevels: Level[] = [1, 2]
|
||||
|
||||
export const defaultExtensions: AnyExtension[] = [
|
||||
StarterKit.configure({
|
||||
@ -60,7 +60,7 @@ export const defaultExtensions: AnyExtension[] = [
|
||||
}
|
||||
},
|
||||
heading: {
|
||||
levels: headingLevels
|
||||
levels: supportedHeadingLevels
|
||||
}
|
||||
}),
|
||||
Highlight.configure({
|
||||
|
@ -30,27 +30,15 @@ export interface RefInputActionItem extends Doc {
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const FORMAT_MODES = [
|
||||
'bold',
|
||||
'italic',
|
||||
'strike',
|
||||
'link',
|
||||
'orderedList',
|
||||
'bulletList',
|
||||
'blockquote',
|
||||
'code',
|
||||
'codeBlock',
|
||||
'heading',
|
||||
'heading1',
|
||||
'heading2',
|
||||
'table',
|
||||
'underline'
|
||||
] as const
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type FormatMode = (typeof FORMAT_MODES)[number]
|
||||
export enum TextFormatCategory {
|
||||
Heading = 'heading',
|
||||
TextDecoration = 'text-decoration', // bold, italic, strike, underline
|
||||
Link = 'link',
|
||||
List = 'list', // orderedList, bulletList,
|
||||
Quote = 'quote', // blockquote
|
||||
Code = 'code', // code, codeBlock
|
||||
Table = 'table'
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
|
@ -2,9 +2,3 @@
|
||||
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: 450px !important;
|
||||
}
|
Loading…
Reference in New Issue
Block a user