TextEditor: Refactor attachments (#3833)

Signed-off-by: Anna No <anna.no@xored.com>
This commit is contained in:
Anna No 2023-10-16 10:22:15 +07:00 committed by GitHub
parent 72db5745ab
commit b439809138
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 147 additions and 187 deletions

View File

@ -19,11 +19,8 @@
AnySvelteComponent,
Button,
ButtonKind,
EmojiPopup,
IconEmoji,
handler,
registerFocus,
showPopup,
deviceOptionsStore as deviceInfo,
checkAdaptiveMatching
} from '@hcengineering/ui'
@ -35,9 +32,8 @@
import { completionConfig } from './extensions'
import { EmojiExtension } from './extension/emoji'
import { IsEmptyContentExtension } from './extension/isEmptyContent'
import Attach from './icons/Attach.svelte'
import RIMention from './icons/RIMention.svelte'
import Send from './icons/Send.svelte'
import { generateDefaultActions } from './editor/actions'
export let content: string = ''
export let showHeader = false
@ -48,7 +44,7 @@
export let kindSend: ButtonKind = 'ghost'
export let haveAttachment = false
export let placeholder: IntlString | undefined = undefined
export let extraActions: RefAction[] | undefined = undefined
export let extraActions: RefAction[] = []
export let loading: boolean = false
export let focusable: boolean = false
export let boundary: HTMLElement | undefined = undefined
@ -57,7 +53,7 @@
const dispatch = createEventDispatcher()
const buttonSize = 'medium'
let textEditor: TextEditor
let textEditor: TextEditor | undefined = undefined
let isEmpty = true
@ -68,42 +64,22 @@
function setContent (content: string) {
textEditor?.setContent(content)
}
const defActions: RefAction[] = [
{
label: textEditorPlugin.string.Attach,
icon: Attach,
action: () => {
dispatch('attach')
},
order: 1001
},
{
label: textEditorPlugin.string.Mention,
icon: RIMention,
action: () => textEditor.insertText('@'),
order: 3000
},
{
label: textEditorPlugin.string.Emoji,
icon: IconEmoji,
action: (element) => {
showPopup(
EmojiPopup,
{},
element,
(emoji) => {
if (!emoji) return
textEditor.insertText(emoji)
textEditor.focus()
},
() => {}
)
},
order: 4001
}
]
let actions: RefAction[] = []
const editorHandler: TextEditorHandler = {
insertText: (text) => {
textEditor?.insertText(text)
},
insertTemplate: (name, text) => {
textEditor?.insertText(text)
},
focus: () => {
textEditor?.focus()
}
}
let actions: RefAction[] = generateDefaultActions(editorHandler)
.concat(...extraActions)
.sort((a, b) => a.order - b.order)
client.findAll<RefInputActionItem>(textEditorPlugin.class.RefInputActionItem, {}).then(async (res) => {
const cont: RefAction[] = []
for (const r of res) {
@ -114,21 +90,13 @@
action: await getResource(r.action)
})
}
actions = defActions.concat(...cont).sort((a, b) => a.order - b.order)
actions = actions.concat(...cont).sort((a, b) => a.order - b.order)
})
export function submit (): void {
textEditor.submit()
textEditor?.submit()
}
const editorHandler: TextEditorHandler = {
insertText: (text) => {
textEditor.insertText(text)
},
insertTemplate: (name, text) => {
textEditor.insertText(text)
}
}
function handleAction (a: RefAction, evt?: Event): void {
a.action(evt?.target as HTMLElement, editorHandler)
}
@ -138,10 +106,10 @@
export let focusIndex = -1
const { idx, focusManager } = registerFocus(focusIndex, {
focus: () => {
const editable = textEditor?.isEditable()
const editable = textEditor?.isEditable() ?? false
if (editable) {
focused = true
textEditor.focus()
textEditor?.focus()
}
return editable
},
@ -175,7 +143,7 @@
if (!isEmpty || haveAttachment) {
dispatch('message', ev.detail)
content = ''
textEditor.clear()
textEditor?.clear()
}
}}
on:blur={() => {
@ -205,42 +173,28 @@
</div>
{#if showActions || showSend}
<div class="buttons-panel flex-between clear-mins">
{#if showActions}
<div class="buttons-group xsmall-gap">
<div class="buttons-group {shrinkButtons ? 'xxsmall-gap' : 'xsmall-gap'}">
{#if showActions}
{#each actions as a}
<Button
disabled={a.disabled}
icon={a.icon}
iconProps={{ size: buttonSize }}
kind="ghost"
showTooltip={{ label: a.label }}
size={buttonSize}
on:click={handler(a, (a, evt) => handleAction(a, evt))}
on:click={handler(a, (a, evt) => {
if (!a.disabled) {
handleAction(a, evt)
}
})}
/>
{#if a.order % 10 === 1}
<div class="buttons-divider" />
{/if}
{/each}
</div>
{#if extraActions && extraActions.length > 0}
<div class="buttons-group {shrinkButtons ? 'xsmall-gap' : 'small-gap'}">
{#each extraActions as a}
<Button
disabled={a.disabled}
icon={a.icon}
iconProps={{ size: buttonSize }}
kind="ghost"
showTooltip={{ label: a.label }}
size={buttonSize}
on:click={handler(a, (a, evt) => {
if (!a.disabled) {
handleAction(a, evt)
}
})}
/>
{/each}
</div>
{/if}
{/if}
</div>
{#if showSend}
<Button

View File

@ -84,9 +84,6 @@
}}
>
<slot />
<svelte:fragment slot="right">
<slot name="right" />
</svelte:fragment>
</StyledTextEditor>
</div>

View File

@ -26,6 +26,7 @@
import { ImageRef, FileAttachFunction } from './imageExt'
import { Node as ProseMirrorNode } from '@tiptap/pm/model'
import { RefAction } from '../types'
export let label: IntlString | undefined = undefined
export let content: string
@ -33,8 +34,8 @@
export let kind: 'normal' | 'emphasized' | 'indented' = 'normal'
export let alwaysEdit: boolean = false
export let extraActions: RefAction[] = []
export let showButtons: boolean = true
export let hideAttachments: boolean = false
export let buttonSize: ButtonSize = 'medium'
export let formatButtonSize: IconSize = 'small'
export let hideExtraButtons: boolean = false
@ -225,7 +226,6 @@
<StyledTextEditor
{placeholder}
{showButtons}
{hideAttachments}
{buttonSize}
{formatButtonSize}
{maxHeight}
@ -233,10 +233,10 @@
{autofocus}
{isScrollable}
{extensions}
{extraActions}
{boundary}
bind:content={rawValue}
bind:this={textEditor}
on:attach
on:value={(evt) => {
rawValue = evt.detail
if (alwaysEdit) {

View File

@ -16,22 +16,12 @@
import { createEventDispatcher } from 'svelte'
import { AnyExtension, mergeAttributes } from '@tiptap/core'
import { Node as ProseMirrorNode } from '@tiptap/pm/model'
import { Asset, getResource, IntlString } from '@hcengineering/platform'
import { getResource, IntlString } from '@hcengineering/platform'
import { getClient } from '@hcengineering/presentation'
import {
AnySvelteComponent,
Button,
ButtonSize,
EmojiPopup,
IconEmoji,
IconSize,
Scroller,
showPopup
} from '@hcengineering/ui'
import { Button, ButtonSize, IconSize, Scroller } from '@hcengineering/ui'
import textEditorPlugin from '../plugin'
import { RefInputAction, RefInputActionItem, TextEditorHandler, TextFormatCategory } from '../types'
import Attach from './icons/Attach.svelte'
import { RefAction, RefInputActionItem, TextEditorHandler, TextFormatCategory } from '../types'
import { generateDefaultActions } from './editor/actions'
import TextEditor from './TextEditor.svelte'
const dispatch = createEventDispatcher()
@ -39,7 +29,6 @@
export let content: string = ''
export let placeholder: IntlString = textEditorPlugin.string.EditorPlaceholder
export let showButtons: boolean = true
export let hideAttachments: boolean = false
export let buttonSize: ButtonSize = 'medium'
export let formatButtonSize: IconSize = 'small'
export let isScrollable: boolean = true
@ -49,6 +38,7 @@
export let full = false
export let extensions: AnyExtension[] = []
export let editorAttributes: { [name: string]: string } = {}
export let extraActions: RefAction[] = []
export let boundary: HTMLElement | undefined = undefined
export let textFormatCategories: TextFormatCategory[] = [
TextFormatCategory.Heading,
@ -60,30 +50,30 @@
TextFormatCategory.Table
]
let textEditor: TextEditor
let textEditor: TextEditor | undefined = undefined
let contentHeight: number
export function submit (): void {
textEditor.submit()
textEditor?.submit()
}
export function focus (): void {
textEditor.focus()
textEditor?.focus()
}
export function isEditable (): boolean {
return textEditor.isEditable()
return textEditor?.isEditable() ?? false
}
export function setEditable (editable: boolean): void {
textEditor.setEditable(editable)
textEditor?.setEditable(editable)
}
export function getContent (): string {
return content
}
export function setContent (data: string): void {
textEditor.setContent(data)
textEditor?.setContent(data)
}
export function insertText (text: string): void {
textEditor.insertText(text)
textEditor?.insertText(text)
}
$: varsStyle =
@ -95,44 +85,22 @@
? 'max-content'
: maxHeight
interface RefAction {
label: IntlString
icon: Asset | AnySvelteComponent
action: RefInputAction
order: number
hidden?: boolean
}
const defActions: RefAction[] = [
{
label: textEditorPlugin.string.Attach,
icon: Attach,
action: () => {
dispatch('attach')
},
order: 1001
},
{
label: textEditorPlugin.string.Emoji,
icon: IconEmoji,
action: (element) => {
showPopup(
EmojiPopup,
{},
element,
(emoji) => {
if (!emoji) return
textEditor.insertText(emoji)
textEditor.focus()
},
() => {}
)
},
order: 4001
}
]
const client = getClient()
let actions: RefAction[] = []
const editorHandler: TextEditorHandler = {
insertText: (text) => {
textEditor?.insertText(text)
},
insertTemplate: (name, text) => {
textEditor?.insertText(text)
dispatch('template', name)
},
focus: () => {
textEditor?.focus()
}
}
let actions: RefAction[] = generateDefaultActions(editorHandler)
.concat(...extraActions)
.sort((a, b) => a.order - b.order)
client.findAll<RefInputActionItem>(textEditorPlugin.class.RefInputActionItem, {}).then(async (res) => {
const cont: RefAction[] = []
for (const r of res) {
@ -143,7 +111,7 @@
action: await getResource(r.action)
})
}
actions = defActions.concat(...cont).sort((a, b) => a.order - b.order)
actions = actions.concat(...cont).sort((a, b) => a.order - b.order)
})
const mergedEditorAttributes = mergeAttributes(
@ -151,15 +119,6 @@
full ? { class: 'text-editor-view_full-height' } : { class: 'text-editor-view_compact' }
)
const editorHandler: TextEditorHandler = {
insertText: (text) => {
textEditor.insertText(text)
},
insertTemplate: (name, text) => {
textEditor.insertText(text)
dispatch('template', name)
}
}
function handleAction (a: RefAction, evt?: Event): void {
a.action(evt?.target as HTMLElement, editorHandler)
}
@ -185,7 +144,7 @@
* @public
*/
export function removeNode (nde: ProseMirrorNode): void {
textEditor.removeNode(nde)
textEditor?.removeNode(nde)
}
</script>
@ -218,7 +177,7 @@
on:content={(ev) => {
dispatch('message', ev.detail)
content = ''
textEditor.clear()
textEditor?.clear()
}}
on:blur
on:focus
@ -237,7 +196,7 @@
on:content={(ev) => {
dispatch('message', ev.detail)
content = ''
textEditor.clear()
textEditor?.clear()
}}
on:blur
on:focus
@ -250,7 +209,7 @@
{#if showButtons}
<div class="flex-between">
<div class="buttons-group {buttonsGap} mt-3">
{#each actions.filter((it) => it.hidden !== true) as a}
{#each actions as a}
<Button
icon={a.icon}
iconProps={{ size: buttonSize }}
@ -265,11 +224,6 @@
{/each}
<slot />
</div>
{#if $$slots.right}
<div class="buttons-group {buttonsGap} mt-3">
<slot name="right" />
</div>
{/if}
</div>
{/if}
</div>

View File

@ -0,0 +1,36 @@
import { EmojiPopup, IconEmoji, showPopup } from '@hcengineering/ui'
import RiMention from '../icons/RIMention.svelte'
import textEditorPlugin from '../../plugin'
import { RefAction, TextEditorHandler } from '../../types'
export const generateDefaultActions = (editorHandler: TextEditorHandler): RefAction[] => {
return [
{
label: textEditorPlugin.string.Mention,
icon: RiMention,
action: () => editorHandler.insertText('@'),
order: 3000
},
{
label: textEditorPlugin.string.Emoji,
icon: IconEmoji,
action: (element) => {
showPopup(
EmojiPopup,
{},
element,
(emoji) => {
if (emoji === null || emoji === undefined) {
return
}
editorHandler.insertText(emoji)
editorHandler.focus()
},
() => {}
)
},
order: 4001
}
]
}

View File

@ -28,6 +28,7 @@ export { default as StyledTextBox } from './components/StyledTextBox.svelte'
export { default as StyledTextEditor } from './components/StyledTextEditor.svelte'
export { default as TextEditor } from './components/TextEditor.svelte'
export { default as TextEditorStyleToolbar } from './components/TextEditorStyleToolbar.svelte'
export { default as AttachIcon } from './components/icons/Attach.svelte'
export { default } from './plugin'
export * from './types'

View File

@ -8,6 +8,7 @@ import type { AnySvelteComponent } from '@hcengineering/ui'
export interface TextEditorHandler {
insertText: (html: string) => void
insertTemplate: (name: string, html: string) => void
focus: () => void
}
/**
* @public

View File

@ -661,6 +661,7 @@ input.search {
.h-14 { height: 3.5rem; }
.h-16 { height: 4rem; }
.h-18 { height: 4.5rem; }
.h-32 { height: 8rem; }
.h-50 { height: 12.5rem; }
.h-60 { height: 15.0rem; }
.w-min { width: min-content; }

View File

@ -13,16 +13,15 @@
// limitations under the License.
-->
<script lang="ts">
import { createEventDispatcher, onDestroy, tick } from 'svelte'
import { Attachment } from '@hcengineering/attachment'
import { Account, Class, Doc, generateId, IdMap, Ref, Space, toIdMap } from '@hcengineering/core'
import { IntlString, setPlatformStatus, unknownError, Asset } from '@hcengineering/platform'
import { createQuery, DraftController, draftsStore, getClient } from '@hcengineering/presentation'
import { ReferenceInput } from '@hcengineering/text-editor'
import type { RefAction } from '@hcengineering/text-editor'
import textEditor, { AttachIcon, type RefAction, ReferenceInput } from '@hcengineering/text-editor'
import { Loading, type AnySvelteComponent } from '@hcengineering/ui'
import { deleteFile, uploadFile } from '../utils'
import attachment from '../plugin'
import { IntlString, setPlatformStatus, unknownError, Asset } from '@hcengineering/platform'
import { createEventDispatcher, onDestroy, tick } from 'svelte'
import { Account, Class, Doc, generateId, IdMap, Ref, Space, toIdMap } from '@hcengineering/core'
import { Loading, type AnySvelteComponent } from '@hcengineering/ui'
import { Attachment } from '@hcengineering/attachment'
import AttachmentPresenter from './AttachmentPresenter.svelte'
export let objectId: Ref<Doc>
@ -40,7 +39,7 @@
refInput.submit()
}
export let placeholder: IntlString | undefined = undefined
export let extraActions: RefAction[] | undefined = undefined
export let extraActions: RefAction[] = []
export let boundary: HTMLElement | undefined = undefined
let refInput: ReferenceInput
@ -284,20 +283,27 @@
{iconSend}
{labelSend}
{showSend}
showHeader={attachments.size > 0 || progress}
{loading}
{boundary}
extraActions={[
...extraActions,
{
label: textEditor.string.Attach,
icon: AttachIcon,
action: () => {
dispatch('focus')
inputFile.click()
},
order: 1001
}
]}
showHeader={attachments.size > 0 || progress}
haveAttachment={attachments.size > 0}
on:focus
on:blur
on:message={onMessage}
haveAttachment={attachments.size > 0}
on:attach={() => {
dispatch('focus')
inputFile.click()
}}
on:update={onUpdate}
{placeholder}
{extraActions}
>
<div slot="header">
{#if attachments.size || progress}

View File

@ -13,19 +13,18 @@
// limitations under the License.
-->
<script lang="ts">
import { createEventDispatcher, onDestroy } from 'svelte'
import { Attachment } from '@hcengineering/attachment'
import { Account, Class, Doc, generateId, Ref, Space, toIdMap } from '@hcengineering/core'
import { IntlString, setPlatformStatus, unknownError } from '@hcengineering/platform'
import { createQuery, DraftController, draftsStore, getClient } from '@hcengineering/presentation'
import { StyledTextBox } from '@hcengineering/text-editor'
import { ButtonSize, IconSize, updatePopup } from '@hcengineering/ui'
import { createEventDispatcher, onDestroy } from 'svelte'
import textEditor, { AttachIcon, type RefAction, StyledTextBox } from '@hcengineering/text-editor'
import { ButtonSize, IconSize, Loading, updatePopup } from '@hcengineering/ui'
import { ListSelectionProvider, SelectDirection } from '@hcengineering/view-resources'
import attachment from '../plugin'
import { deleteFile, uploadFile } from '../utils'
import AttachmentPresenter from './AttachmentPresenter.svelte'
import AttachmentPreview from './AttachmentPreview.svelte'
import { ListSelectionProvider, SelectDirection } from '@hcengineering/view-resources'
import Loading from '@hcengineering/ui/src/components/Loading.svelte'
export let objectId: Ref<Doc> | undefined = undefined
export let space: Ref<Space> | undefined = undefined
@ -84,7 +83,7 @@
export function setContent (data: string): void {
refInput.setContent(data)
}
export function attach (): void {
export function handleAttach (): void {
inputFile.click()
}
@ -92,6 +91,7 @@
refInput.submit()
}
let refInput: StyledTextBox
let extraActions: RefAction[] = []
let inputFile: HTMLInputElement
let saved = false
@ -346,6 +346,19 @@
values: attachments.size === 0 ? true : attachments
})
$: if (enableAttachments) {
extraActions = [
{
label: textEditor.string.Attach,
icon: AttachIcon,
action: handleAttach,
order: 1001
}
]
} else {
extraActions = []
}
let element: HTMLElement
let progressItems: Ref<Doc>[] = []
</script>
@ -377,7 +390,6 @@
{placeholder}
{alwaysEdit}
{showButtons}
hideAttachments={!enableAttachments}
{buttonSize}
{formatButtonSize}
{maxHeight}
@ -386,14 +398,12 @@
{enableBackReferences}
{isScrollable}
{boundary}
{extraActions}
on:changeSize
on:changeContent
on:blur
on:focus
on:open-document
on:attach={() => {
attach()
}}
attachFile={async (file) => {
return createAttachment(file)
}}