Tables support for Markup editor (#2385)

Signed-off-by: Alexander Platov <sas_lord@mail.ru>
This commit is contained in:
Alexander Platov
2022-11-18 13:45:30 +07:00
committed by GitHub
parent 2cd894e7ad
commit 4878dae539
11 changed files with 366 additions and 119 deletions
@@ -97,6 +97,16 @@
}
}}
/>
{:else if node.nodeName === 'TABLE'}
<table class={node.className}><svelte:self nodes={node.childNodes} /></table>
{:else if node.nodeName === 'TBODY'}
<tbody><svelte:self nodes={node.childNodes} /></tbody>
{:else if node.nodeName === 'TR'}
<tr><svelte:self nodes={node.childNodes} /></tr>
{:else if node.nodeName === 'TH'}
<th><svelte:self nodes={node.childNodes} /></th>
{:else if node.nodeName === 'TD'}
<td><svelte:self nodes={node.childNodes} /></td>
{:else}
Unknown {node.nodeName}
{/if}
@@ -43,7 +43,7 @@
import CodeBlock from './icons/CodeBlock.svelte'
import { calculateDecorations } from './diff/decorations'
import { defaultExtensions, headingLevels } from './extensions'
import { defaultExtensions, headingLevels, mInsertTable } from './extensions'
import Header from './icons/Header.svelte'
import IconTable from './icons/IconTable.svelte'
import Italic from './icons/Italic.svelte'
@@ -316,74 +316,12 @@
}
function insertTable (event: MouseEvent) {
const tables = [
{
label: '2x2',
rows: 2,
cols: 2,
header: false
},
{
label: '3x3',
rows: 3,
cols: 3,
header: false
},
{
label: '2x1',
rows: 2,
cols: 1,
header: false
},
{
label: '5x5',
rows: 5,
cols: 5,
header: false
},
{
label: '1x2',
rows: 1,
cols: 2,
header: false
},
{
label: 'Headed 2x2',
rows: 2,
cols: 2,
header: true
},
{
label: 'Headed 3x3',
rows: 3,
cols: 3,
header: true
},
{
label: 'Headed 2x1',
rows: 2,
cols: 1,
header: true
},
{
label: 'Headed 5x5',
rows: 5,
cols: 5,
header: true
},
{
label: 'Headed 1x2',
rows: 1,
cols: 2,
header: true
}
]
showPopup(
SelectPopup,
{
value: [
{ id: '#delete', label: presentation.string.Remove },
...tables.map((it) => ({ id: it.label, text: it.label }))
...mInsertTable.map((it) => ({ id: it.label, text: it.label }))
]
},
getEventPositionElement(event),
@@ -395,7 +333,7 @@
updateFormattingState()
return
}
const tab = tables.find((it) => it.label === val)
const tab = mInsertTable.find((it) => it.label === val)
if (tab) {
editor.commands.insertTable({
cols: tab.cols,
@@ -22,6 +22,7 @@
export let emphasized: boolean = false
export let alwaysEdit: boolean = false
export let showButtons: boolean = true
export let showAttach: boolean = false
export let buttonSize: IconSize = 'small'
export let hideExtraButtons: boolean = false
export let maxHeight: 'max' | 'card' | 'limited' | string = 'max'
@@ -78,6 +79,7 @@
}
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="antiComponent styled-box clear-mins"
class:emphasized
@@ -98,11 +100,13 @@
<StyledTextEditor
{placeholder}
{showButtons}
{showAttach}
{buttonSize}
{maxHeight}
{focusable}
bind:content={rawValue}
bind:this={textEditor}
on:attach
on:focus={() => {
focused = true
}}
@@ -13,19 +13,29 @@
// limitations under the License.
-->
<script lang="ts">
import { IntlString } from '@hcengineering/platform'
import { IconSize, Scroller, showPopup } from '@hcengineering/ui'
import { IntlString, getEmbeddedLabel } from '@hcengineering/platform'
import {
IconSize,
Scroller,
showPopup,
SelectPopup,
AnySvelteComponent,
getEventPositionElement
} from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import textEditorPlugin from '../plugin'
import EmojiPopup from './EmojiPopup.svelte'
import Emoji from './icons/Emoji.svelte'
import TextStyle from './icons/TextStyle.svelte'
import TextEditor from './TextEditor.svelte'
import { Asset } from '@hcengineering/platform'
import { AnySvelteComponent } from '@hcengineering/ui'
import { FormatMode, FORMAT_MODES, RefInputAction, TextEditorHandler } from '../types'
import { headingLevels, mInsertTable } from './extensions'
import { Level } from '@tiptap/extension-heading'
import presentation from '@hcengineering/presentation'
import Header from './icons/Header.svelte'
import IconTable from './icons/IconTable.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'
@@ -37,12 +47,20 @@
import Strikethrough from './icons/Strikethrough.svelte'
import LinkPopup from './LinkPopup.svelte'
import StyleButton from './StyleButton.svelte'
import AddRowBefore from './icons/table/AddRowBefore.svelte'
import AddRowAfter from './icons/table/AddRowAfter.svelte'
import AddColBefore from './icons/table/AddColBefore.svelte'
import AddColAfter from './icons/table/AddColAfter.svelte'
import DeleteRow from './icons/table/DeleteRow.svelte'
import DeleteCol from './icons/table/DeleteCol.svelte'
import DeleteTable from './icons/table/DeleteTable.svelte'
const dispatch = createEventDispatcher()
export let content: string = ''
export let placeholder: IntlString = textEditorPlugin.string.EditorPlaceholder
export let showButtons: boolean = true
export let showAttach: boolean = false
export let buttonSize: IconSize = 'large'
export let isScrollable: boolean = true
export let focusable: boolean = false
@@ -76,8 +94,19 @@
icon: Asset | AnySvelteComponent
action: RefInputAction
order: number
hidden?: boolean
}
const defActions: RefAction[] = [
let defActions: RefAction[]
$: defActions = [
{
label: textEditorPlugin.string.Attach,
icon: Attach,
action: () => {
dispatch('attach')
},
order: 1000,
hidden: showAttach
},
{
label: textEditorPlugin.string.TextStyle,
icon: TextStyle,
@@ -115,8 +144,21 @@
function updateFormattingState () {
activeModes = new Set(FORMAT_MODES.filter(textEditor.checkIsActive))
for (const l of headingLevels) {
if (textEditor.checkIsActive('heading', { level: l })) {
headingLevel = l
activeModes.add('heading')
}
}
if (!activeModes.has('heading')) {
headingLevel = 0
}
isSelectionEmpty = textEditor.checkIsSelectionEmpty()
}
// function updateFormattingState () {
// activeModes = new Set(FORMAT_MODES.filter(textEditor.checkIsActive))
// isSelectionEmpty = textEditor.checkIsSelectionEmpty()
// }
function getToggler (toggle: () => void) {
return () => {
@@ -145,11 +187,172 @@
function handleAction (a: RefAction, evt?: Event): void {
a.action(evt?.target as HTMLElement, editorHandler)
}
let needFocus = false
const focused = false
$: if (textEditor && needFocus) {
if (!focused) textEditor.focus()
needFocus = false
}
let headingLevel = 0
function toggleHeader (event: MouseEvent) {
if (activeModes.has('heading')) {
textEditor.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) {
textEditor.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') {
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()
}
}
}
)
}
</script>
<div class="ref-container clear-mins">
{#if isFormatting}
<div class="formatPanel buttons-group xsmall-gap mb-4" class:withoutTopBorder>
<StyleButton
icon={Header}
size={buttonSize}
selected={activeModes.has('heading')}
showTooltip={{ label: getEmbeddedLabel(`H${headingLevel}`) }}
on:click={toggleHeader}
/>
<StyleButton
icon={Bold}
size={buttonSize}
@@ -217,6 +420,23 @@
showTooltip={{ label: textEditorPlugin.string.CodeBlock }}
on:click={getToggler(textEditor.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>
{/if}
<div class="textInput" class:focusable>
@@ -259,8 +479,8 @@
</div>
</div>
{#if showButtons}
<div class="buttons">
{#each defActions as a}
<div class="buttons-group xsmall-gap my-4">
{#each defActions.filter((it) => it.hidden === undefined || it.hidden === true) as a}
<StyleButton icon={a.icon} size={buttonSize} on:click={(evt) => handleAction(a, evt)} />
{/each}
<div class="flex-grow">
@@ -315,9 +535,5 @@
}
}
}
.buttons {
display: flex;
align-items: center;
}
}
</style>
@@ -23,6 +23,7 @@
import textEditorPlugin from '../plugin'
import { FormatMode } from '../types'
import { defaultExtensions } from './extensions'
import { Level } from '@tiptap/extension-heading'
export let content: string = ''
export let placeholder: IntlString = textEditorPlugin.string.EditorPlaceholder
@@ -55,8 +56,8 @@
export function insertText (text: string): void {
editor.commands.insertContent(text as HTMLContent)
}
export function checkIsActive (formatMode: FormatMode) {
return editor.isActive(formatMode)
export function checkIsActive (formatMode: FormatMode, attributes?: {} | undefined) {
return editor.isActive(formatMode, attributes)
}
export function toggleBold () {
editor.commands.toggleBold()
@@ -94,9 +95,41 @@
export function toggleCodeBlock () {
editor.commands.toggleCodeBlock()
}
let needFocus = false
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
})
}
let needFocus = false
let focused = false
export function focus (): void {
needFocus = true
}
@@ -164,6 +197,7 @@
content = editor.getHTML()
isEmpty = editor.isEmpty
dispatch('value', content)
dispatch('update', content)
},
onSelectionUpdate: () => dispatch('selection-update')
})
@@ -60,3 +60,66 @@ export const defaultExtensions = [
...tableExtensions,
...taskListExtensions
]
export const mInsertTable = [
{
label: '2x2',
rows: 2,
cols: 2,
header: false
},
{
label: '3x3',
rows: 3,
cols: 3,
header: false
},
{
label: '2x1',
rows: 2,
cols: 1,
header: false
},
{
label: '5x5',
rows: 5,
cols: 5,
header: false
},
{
label: '1x2',
rows: 1,
cols: 2,
header: false
},
{
label: 'Headed 2x2',
rows: 2,
cols: 2,
header: true
},
{
label: 'Headed 3x3',
rows: 3,
cols: 3,
header: true
},
{
label: 'Headed 2x1',
rows: 2,
cols: 1,
header: true
},
{
label: 'Headed 5x5',
rows: 5,
cols: 5,
header: true
},
{
label: 'Headed 1x2',
rows: 1,
cols: 2,
header: true
}
]
+1
View File
@@ -404,6 +404,7 @@ input.search {
.mx-3 { margin: 0 .75rem; }
.mx-10 { margin: 0 2.5rem; }
.mx-auto { margin: 0 auto; }
.my-2 { margin: .5rem 0; }
.my-4 { margin: 1rem 0; }
.pl-1 { padding-left: .25rem; }
+1
View File
@@ -24,6 +24,7 @@ table.proseTable {
td,
th {
min-width: 1rem;
height: 2rem;
border: 1px solid var(--button-border-hover);
padding: .25rem .5rem;
vertical-align: top;
@@ -25,8 +25,6 @@
export let id: string | undefined = undefined
</script>
/** eslint-disable a11y-click-events-have-key-events */
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
{id}
@@ -33,6 +33,7 @@
export let showButtons = false
export let buttonSize: IconSize = 'small'
export let maxHeight: 'max' | 'card' | 'limited' | string = 'max'
export let focusable: boolean = false
export function attach (): void {
inputFile.click()
@@ -199,12 +200,15 @@
{placeholder}
{alwaysEdit}
{showButtons}
showAttach
{buttonSize}
{maxHeight}
{focusable}
on:changeSize
on:attach={() => attach()}
/>
{#if attachments.size}
<div class="flex-row-center list scroll-divider-color mt-1">
<div class="flex-row-center list scroll-divider-color">
{#each Array.from(attachments.values()) as attachment}
<div class="item flex">
<AttachmentPresenter
@@ -223,6 +227,7 @@
<style lang="scss">
.list {
padding: 0.5rem;
min-width: 0;
color: var(--theme-caption-color);
overflow-x: auto;
overflow-y: hidden;
@@ -25,7 +25,6 @@
Button,
EditBox,
getCurrentLocation,
IconAttachment,
IconEdit,
IconMoreH,
Label,
@@ -296,30 +295,19 @@
</div>
{/if}
<EditBox bind:value={title} placeholder={tracker.string.IssueTitlePlaceholder} kind="large-style" />
<div class="flex-between mt-6">
{#key description}
<div class="flex-grow">
<AttachmentStyledBox
bind:this={descriptionBox}
objectId={_id}
_class={tracker.class.Issue}
space={issue.space}
alwaysEdit
showButtons
maxHeight={'card'}
bind:content={description}
placeholder={tracker.string.IssueDescriptionPlaceholder}
/>
</div>
{/key}
<div
class="tool"
on:click={() => {
descriptionBox.attach()
}}
>
<IconAttachment size={'large'} />
</div>
<div class="w-full mt-6">
<AttachmentStyledBox
bind:this={descriptionBox}
objectId={_id}
_class={tracker.class.Issue}
space={issue.space}
alwaysEdit
showButtons
maxHeight={'card'}
focusable
bind:content={description}
placeholder={tracker.string.IssueDescriptionPlaceholder}
/>
</div>
</div>
</Scroller>
@@ -422,15 +410,4 @@
height: 1px;
background-color: var(--divider-color);
}
.tool {
align-self: start;
width: 20px;
height: 20px;
opacity: 0.3;
cursor: pointer;
&:hover {
opacity: 1;
}
}
</style>