mirror of
https://github.com/hcengineering/platform.git
synced 2024-12-23 03:22:19 +03:00
EZQMS-377: Add file attachments extension to text editor (#4284)
Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>
This commit is contained in:
parent
4fc3a0bd5d
commit
9ef5b8f172
File diff suppressed because it is too large
Load Diff
@ -34,7 +34,8 @@
|
|||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"ts-jest": "^29.1.1",
|
"ts-jest": "^29.1.1",
|
||||||
"@types/jest": "^29.5.5",
|
"@types/jest": "^29.5.5",
|
||||||
"svelte-eslint-parser": "^0.33.1"
|
"svelte-eslint-parser": "^0.33.1",
|
||||||
|
"filesize": "^8.0.3"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hcengineering/presentation": "^0.6.2",
|
"@hcengineering/presentation": "^0.6.2",
|
||||||
|
46
packages/text-editor/src/command/deleteAttachment.ts
Normal file
46
packages/text-editor/src/command/deleteAttachment.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
// Copyright © 2024 Hardcore Engineering Inc.
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
import { findChildren, type RawCommands } from '@tiptap/core'
|
||||||
|
import { FileNode, ImageNode } from '@hcengineering/text'
|
||||||
|
|
||||||
|
declare module '@tiptap/core' {
|
||||||
|
interface Commands<ReturnType> {
|
||||||
|
attachment: {
|
||||||
|
/**
|
||||||
|
* Delete a given attachment.
|
||||||
|
*/
|
||||||
|
deleteAttachment: (id: string) => ReturnType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteAttachment: RawCommands['deleteAttachment'] =
|
||||||
|
(id: string) =>
|
||||||
|
({ tr, dispatch }) => {
|
||||||
|
if (dispatch !== undefined) {
|
||||||
|
const nodeWithPos = findChildren(tr.doc, (node) => {
|
||||||
|
return (
|
||||||
|
(node.type.name === FileNode.name && node.attrs['file-id'] === id) ||
|
||||||
|
(node.type.name === ImageNode.name && node.attrs['file-id'] === id)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
nodeWithPos
|
||||||
|
.sort((a, b) => b.pos - a.pos)
|
||||||
|
.forEach(({ node, pos }) => {
|
||||||
|
tr.delete(pos, pos + node.nodeSize)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
@ -19,7 +19,7 @@
|
|||||||
import { registerFocus } from '@hcengineering/ui'
|
import { registerFocus } from '@hcengineering/ui'
|
||||||
import CollaborativeTextEditor from './CollaborativeTextEditor.svelte'
|
import CollaborativeTextEditor from './CollaborativeTextEditor.svelte'
|
||||||
import { FocusExtension } from './extension/focus'
|
import { FocusExtension } from './extension/focus'
|
||||||
import { FileAttachFunction } from './extension/imageExt'
|
import { type FileAttachFunction } from './extension/types'
|
||||||
import textEditorPlugin from '../plugin'
|
import textEditorPlugin from '../plugin'
|
||||||
import { DocumentId } from '../provider/tiptap'
|
import { DocumentId } from '../provider/tiptap'
|
||||||
import { collaborativeDocumentId, mongodbDocumentId, platformDocumentId } from '../provider/utils'
|
import { collaborativeDocumentId, mongodbDocumentId, platformDocumentId } from '../provider/utils'
|
||||||
@ -41,6 +41,10 @@
|
|||||||
return editor?.isFocused() ?? false
|
return editor?.isFocused() ?? false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function removeAttachment (id: string): void {
|
||||||
|
return editor?.removeAttachment(id)
|
||||||
|
}
|
||||||
|
|
||||||
let editor: CollaborativeTextEditor
|
let editor: CollaborativeTextEditor
|
||||||
|
|
||||||
$: documentId = getDocumentId(object, key)
|
$: documentId = getDocumentId(object, key)
|
||||||
@ -120,6 +124,7 @@
|
|||||||
{boundary}
|
{boundary}
|
||||||
{readonly}
|
{readonly}
|
||||||
field={key.key}
|
field={key.key}
|
||||||
|
canEmbedFiles={false}
|
||||||
on:focus
|
on:focus
|
||||||
on:blur
|
on:blur
|
||||||
on:update
|
on:update
|
||||||
|
@ -25,6 +25,7 @@
|
|||||||
import { createEventDispatcher, getContext, onDestroy, onMount } from 'svelte'
|
import { createEventDispatcher, getContext, onDestroy, onMount } from 'svelte'
|
||||||
import { Doc as YDoc } from 'yjs'
|
import { Doc as YDoc } from 'yjs'
|
||||||
|
|
||||||
|
import { deleteAttachment } from '../command/deleteAttachment'
|
||||||
import { Completion } from '../Completion'
|
import { Completion } from '../Completion'
|
||||||
import { textEditorCommandHandler } from '../commands'
|
import { textEditorCommandHandler } from '../commands'
|
||||||
import { EditorKit } from '../kits/editor-kit'
|
import { EditorKit } from '../kits/editor-kit'
|
||||||
@ -46,7 +47,9 @@
|
|||||||
import { noSelectionRender, renderCursor } from './editor/collaboration'
|
import { noSelectionRender, renderCursor } from './editor/collaboration'
|
||||||
import { defaultEditorAttributes } from './editor/editorProps'
|
import { defaultEditorAttributes } from './editor/editorProps'
|
||||||
import { EmojiExtension } from './extension/emoji'
|
import { EmojiExtension } from './extension/emoji'
|
||||||
import { FileAttachFunction, ImageExtension } from './extension/imageExt'
|
import { ImageExtension } from './extension/imageExt'
|
||||||
|
import { type FileAttachFunction } from './extension/types'
|
||||||
|
import { FileExtension } from './extension/fileExt'
|
||||||
import { InlinePopupExtension } from './extension/inlinePopup'
|
import { InlinePopupExtension } from './extension/inlinePopup'
|
||||||
import { InlineStyleToolbarExtension } from './extension/inlineStyleToolbar'
|
import { InlineStyleToolbarExtension } from './extension/inlineStyleToolbar'
|
||||||
import { completionConfig } from './extensions'
|
import { completionConfig } from './extensions'
|
||||||
@ -82,6 +85,8 @@
|
|||||||
|
|
||||||
export let attachFile: FileAttachFunction | undefined = undefined
|
export let attachFile: FileAttachFunction | undefined = undefined
|
||||||
export let canShowPopups = true
|
export let canShowPopups = true
|
||||||
|
export let canEmbedFiles = true
|
||||||
|
export let canEmbedImages = true
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
@ -150,6 +155,10 @@
|
|||||||
return commandHandler
|
return commandHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function removeAttachment (id: string): void {
|
||||||
|
editor.commands.command(deleteAttachment(id))
|
||||||
|
}
|
||||||
|
|
||||||
export function isEditable (): boolean {
|
export function isEditable (): boolean {
|
||||||
return editor?.isEditable ?? false
|
return editor?.isEditable ?? false
|
||||||
}
|
}
|
||||||
@ -206,13 +215,23 @@
|
|||||||
const optionalExtensions: AnyExtension[] = []
|
const optionalExtensions: AnyExtension[] = []
|
||||||
|
|
||||||
if (attachFile !== undefined) {
|
if (attachFile !== undefined) {
|
||||||
optionalExtensions.push(
|
if (canEmbedFiles) {
|
||||||
ImageExtension.configure({
|
optionalExtensions.push(
|
||||||
inline: true,
|
FileExtension.configure({
|
||||||
attachFile,
|
inline: true,
|
||||||
uploadUrl: getMetadata(presentation.metadata.UploadURL)
|
attachFile
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
if (canEmbedImages) {
|
||||||
|
optionalExtensions.push(
|
||||||
|
ImageExtension.configure({
|
||||||
|
inline: true,
|
||||||
|
attachFile,
|
||||||
|
uploadUrl: getMetadata(presentation.metadata.UploadURL)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
|
@ -25,7 +25,7 @@
|
|||||||
import { TextEditorCommandHandler, TextFormatCategory, TextNodeAction } from '../types'
|
import { TextEditorCommandHandler, TextFormatCategory, TextNodeAction } from '../types'
|
||||||
|
|
||||||
import CollaborativeTextEditor from './CollaborativeTextEditor.svelte'
|
import CollaborativeTextEditor from './CollaborativeTextEditor.svelte'
|
||||||
import { FileAttachFunction } from './extension/imageExt'
|
import { FileAttachFunction } from './extension/types'
|
||||||
import { NodeUuidExtension, nodeElementQuerySelector } from './extension/nodeUuid'
|
import { NodeUuidExtension, nodeElementQuerySelector } from './extension/nodeUuid'
|
||||||
|
|
||||||
export let documentId: DocumentId
|
export let documentId: DocumentId
|
||||||
|
@ -14,7 +14,6 @@
|
|||||||
} from '@hcengineering/ui'
|
} from '@hcengineering/ui'
|
||||||
import { createEventDispatcher } from 'svelte'
|
import { createEventDispatcher } from 'svelte'
|
||||||
import type { AnyExtension } from '@tiptap/core'
|
import type { AnyExtension } from '@tiptap/core'
|
||||||
import { Node as ProseMirrorNode } from '@tiptap/pm/model'
|
|
||||||
|
|
||||||
import { Completion } from '../Completion'
|
import { Completion } from '../Completion'
|
||||||
import textEditorPlugin from '../plugin'
|
import textEditorPlugin from '../plugin'
|
||||||
@ -23,7 +22,8 @@
|
|||||||
import { completionConfig } from './extensions'
|
import { completionConfig } from './extensions'
|
||||||
import { EmojiExtension } from './extension/emoji'
|
import { EmojiExtension } from './extension/emoji'
|
||||||
import { FocusExtension } from './extension/focus'
|
import { FocusExtension } from './extension/focus'
|
||||||
import { ImageExtension, FileAttachFunction } from './extension/imageExt'
|
import { ImageExtension } from './extension/imageExt'
|
||||||
|
import { type FileAttachFunction } from './extension/types'
|
||||||
import { RefAction } from '../types'
|
import { RefAction } from '../types'
|
||||||
|
|
||||||
export let label: IntlString | undefined = undefined
|
export let label: IntlString | undefined = undefined
|
||||||
@ -156,16 +156,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const attachments = new Map<string, ProseMirrorNode>()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
export function removeAttachment (id: string): void {
|
export function removeAttachment (id: string): void {
|
||||||
const nde = attachments.get(id)
|
textEditor.removeAttachment(id)
|
||||||
if (nde !== undefined) {
|
|
||||||
textEditor.removeNode(nde)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function configureExtensions (): AnyExtension[] {
|
function configureExtensions (): AnyExtension[] {
|
||||||
@ -173,9 +168,6 @@
|
|||||||
inline: true,
|
inline: true,
|
||||||
HTMLAttributes: {},
|
HTMLAttributes: {},
|
||||||
attachFile,
|
attachFile,
|
||||||
reportNode: (id, node) => {
|
|
||||||
attachments.set(id, node)
|
|
||||||
},
|
|
||||||
uploadUrl: getMetadata(presentation.metadata.UploadURL)
|
uploadUrl: getMetadata(presentation.metadata.UploadURL)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -15,7 +15,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { createEventDispatcher } from 'svelte'
|
import { createEventDispatcher } from 'svelte'
|
||||||
import { AnyExtension, mergeAttributes } from '@tiptap/core'
|
import { AnyExtension, mergeAttributes } from '@tiptap/core'
|
||||||
import { Node as ProseMirrorNode } from '@tiptap/pm/model'
|
|
||||||
import { IntlString } from '@hcengineering/platform'
|
import { IntlString } from '@hcengineering/platform'
|
||||||
import { Button, ButtonSize, Scroller } from '@hcengineering/ui'
|
import { Button, ButtonSize, Scroller } from '@hcengineering/ui'
|
||||||
import textEditorPlugin from '../plugin'
|
import textEditorPlugin from '../plugin'
|
||||||
@ -131,8 +130,8 @@
|
|||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
export function removeNode (nde: ProseMirrorNode): void {
|
export function removeAttachment (id: string): void {
|
||||||
textEditor?.removeNode(nde)
|
textEditor?.removeAttachment(id)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -19,9 +19,9 @@
|
|||||||
|
|
||||||
import { AnyExtension, Editor, FocusPosition, mergeAttributes } from '@tiptap/core'
|
import { AnyExtension, Editor, FocusPosition, mergeAttributes } from '@tiptap/core'
|
||||||
import Placeholder from '@tiptap/extension-placeholder'
|
import Placeholder from '@tiptap/extension-placeholder'
|
||||||
import { Node as ProseMirrorNode } from '@tiptap/pm/model'
|
|
||||||
import { createEventDispatcher, onDestroy, onMount } from 'svelte'
|
import { createEventDispatcher, onDestroy, onMount } from 'svelte'
|
||||||
|
|
||||||
|
import { deleteAttachment } from '../command/deleteAttachment'
|
||||||
import textEditorPlugin from '../plugin'
|
import textEditorPlugin from '../plugin'
|
||||||
import { TextFormatCategory } from '../types'
|
import { TextFormatCategory } from '../types'
|
||||||
|
|
||||||
@ -184,17 +184,8 @@
|
|||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
export function removeNode (nde: ProseMirrorNode): void {
|
export function removeAttachment (id: string): void {
|
||||||
const deleteOp = (n: ProseMirrorNode, pos: number): void => {
|
editor.commands.command(deleteAttachment(id))
|
||||||
if (nde === n) {
|
|
||||||
// const pos = editor.view.posAtDOM(nde, 0)
|
|
||||||
editor.view.dispatch(editor.view.state.tr.delete(pos, pos + 1))
|
|
||||||
}
|
|
||||||
n.descendants(deleteOp)
|
|
||||||
}
|
|
||||||
editor.view.state.doc.descendants((n, pos) => {
|
|
||||||
deleteOp(n, pos)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleFocus (): void {
|
function handleFocus (): void {
|
||||||
|
208
packages/text-editor/src/components/extension/fileExt.ts
Normal file
208
packages/text-editor/src/components/extension/fileExt.ts
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
//
|
||||||
|
// Copyright © 2023 Hardcore Engineering Inc.
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
//
|
||||||
|
import { getFileUrl, PDFViewer } from '@hcengineering/presentation'
|
||||||
|
import { FileNode, type FileOptions as FileNodeOptions } from '@hcengineering/text'
|
||||||
|
import { showPopup } from '@hcengineering/ui'
|
||||||
|
import { nodeInputRule } from '@tiptap/core'
|
||||||
|
import { type Node as ProseMirrorNode } from '@tiptap/pm/model'
|
||||||
|
import { Plugin, PluginKey } from '@tiptap/pm/state'
|
||||||
|
import { type EditorView } from '@tiptap/pm/view'
|
||||||
|
import filesize from 'filesize'
|
||||||
|
|
||||||
|
import { type FileAttachFunction } from './types'
|
||||||
|
|
||||||
|
const attachIcon =
|
||||||
|
'<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M9.5 1C9.10603 1 8.71593 1.0776 8.35195 1.22836C7.98797 1.37913 7.65726 1.6001 7.37868 1.87868L1.35355 7.90381C1.15829 8.09907 0.841709 8.09907 0.646447 7.90381C0.451184 7.70854 0.451184 7.39196 0.646447 7.1967L6.67157 1.17157C7.04301 0.800138 7.48396 0.5055 7.96927 0.304482C8.45457 0.103463 8.97471 0 9.5 0C10.0253 0 10.5454 0.103463 11.0307 0.304482C11.516 0.505501 11.957 0.800139 12.3284 1.17157C12.6999 1.54301 12.9945 1.98396 13.1955 2.46927C13.3965 2.95457 13.5 3.47471 13.5 4C13.5 4.52529 13.3965 5.04543 13.1955 5.53073C12.9945 6.01604 12.6999 6.45699 12.3284 6.82843L12.3251 6.83173L5.76601 13.2695C5.53423 13.5008 5.25926 13.6844 4.95671 13.8097C4.65339 13.9353 4.3283 14 4 14C3.6717 14 3.34661 13.9353 3.04329 13.8097C2.73998 13.6841 2.46438 13.4999 2.23223 13.2678C2.00009 13.0356 1.81594 12.76 1.6903 12.4567C1.56466 12.1534 1.5 11.8283 1.5 11.5C1.5 11.1717 1.56466 10.8466 1.6903 10.5433C1.81594 10.24 2.00009 9.96438 2.23223 9.73223L8.14645 3.81802C8.34171 3.62276 8.65829 3.62276 8.85355 3.81802C9.04882 4.01328 9.04882 4.32986 8.85355 4.52513L2.93934 10.4393C2.80005 10.5786 2.68956 10.744 2.61418 10.926C2.5388 11.108 2.5 11.303 2.5 11.5C2.5 11.697 2.5388 11.892 2.61418 12.074C2.68956 12.256 2.80005 12.4214 2.93934 12.5607C3.07863 12.6999 3.24399 12.8104 3.42598 12.8858C3.60796 12.9612 3.80302 13 4 13C4.19698 13 4.39204 12.9612 4.57402 12.8858C4.75601 12.8104 4.92137 12.6999 5.06066 12.5607L5.06396 12.5574L11.6229 6.11972C11.9007 5.84148 12.1212 5.51133 12.2716 5.14805C12.4224 4.78407 12.5 4.39397 12.5 4C12.5 3.60603 12.4224 3.21593 12.2716 2.85195C12.1209 2.48797 11.8999 2.15726 11.6213 1.87868C11.3427 1.6001 11.012 1.37913 10.6481 1.22836C10.2841 1.0776 9.89396 1 9.5 1Z" fill="currentColor"/></svg>'
|
||||||
|
const imageIcon =
|
||||||
|
'<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M8.33335 4.7472C8.08668 4.91203 7.79667 5 7.5 5C7.10218 5 6.72064 4.84197 6.43934 4.56066C6.15804 4.27936 6 3.89783 6 3.5C6 3.20333 6.08797 2.91332 6.2528 2.66665C6.41762 2.41997 6.65189 2.22771 6.92597 2.11418C7.20006 2.00065 7.50166 1.97094 7.79264 2.02882C8.08361 2.0867 8.35088 2.22956 8.56066 2.43934C8.77044 2.64912 8.9133 2.91639 8.97118 3.20737C9.02906 3.49834 8.99935 3.79994 8.88582 4.07403C8.77229 4.34811 8.58003 4.58238 8.33335 4.7472ZM7.77779 3.08427C7.69556 3.02933 7.59889 3 7.5 3C7.36739 3 7.24021 3.05268 7.14645 3.14645C7.05268 3.24022 7 3.36739 7 3.5C7 3.59889 7.02932 3.69556 7.08426 3.77779C7.13921 3.86001 7.2173 3.9241 7.30866 3.96194C7.40002 3.99978 7.50056 4.00969 7.59755 3.99039C7.69454 3.9711 7.78363 3.92348 7.85355 3.85355C7.92348 3.78363 7.9711 3.69454 7.99039 3.59755C8.00969 3.50056 7.99978 3.40002 7.96194 3.30866C7.9241 3.2173 7.86001 3.13921 7.77779 3.08427Z" fill="currentColor"/><path fill-rule="evenodd" clip-rule="evenodd" d="M0 2C0 0.895431 0.89543 0 2 0H10C11.1046 0 12 0.89543 12 2V10C12 11.1046 11.1046 12 10 12H2C0.895431 12 0 11.1046 0 10V2ZM2 1C1.44772 1 1 1.44772 1 2V6.58574L2.79285 4.79289C3.18337 4.40237 3.81654 4.40237 4.20706 4.79289L6.99995 7.58579L7.79285 6.79289C8.18337 6.40237 8.81653 6.40237 9.20706 6.79289L11 8.58583V2C11 1.44772 10.5523 1 10 1H2ZM1 10V7.99995L3.49995 5.5L6.29285 8.29289C6.68337 8.68342 7.31654 8.68342 7.70706 8.29289L8.49995 7.5L11 10C11 10.5523 10.5523 11 10 11H2C1.44772 11 1 10.5523 1 10Z" fill="currentColor"/></svg>'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export interface FileOptions extends FileNodeOptions {
|
||||||
|
attachFile?: FileAttachFunction
|
||||||
|
reportNode?: (id: string, node: ProseMirrorNode) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export const inputRegex = /(?:^|\s)(!\[(.+|:?)]\((\S+)(?:(?:\s+)["'](\S+)["'])?\))$/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export const FileExtension = FileNode.extend<FileOptions>({
|
||||||
|
addOptions () {
|
||||||
|
return {
|
||||||
|
inline: false,
|
||||||
|
HTMLAttributes: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
parseHTML () {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
tag: `div[data-type="${this.name}"]`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: 'div[data-file-name]'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: 'div[data-file-size]'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: 'div[data-file-type]'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
priority: 1100,
|
||||||
|
|
||||||
|
renderHTML ({ node, HTMLAttributes }) {
|
||||||
|
const nodeAttributes = {
|
||||||
|
class: 'text-editor-file-container',
|
||||||
|
'data-type': this.name
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = HTMLAttributes['file-id']
|
||||||
|
const fileName = HTMLAttributes['data-file-name']
|
||||||
|
const size = HTMLAttributes['data-file-size']
|
||||||
|
const fileType = HTMLAttributes['data-file-type']
|
||||||
|
let href: string = ''
|
||||||
|
if (id != null) {
|
||||||
|
href = getFileUrl(id, 'full', fileName)
|
||||||
|
this.options.reportNode?.(id, node)
|
||||||
|
}
|
||||||
|
const linkAttributes = {
|
||||||
|
class: 'file-name',
|
||||||
|
href,
|
||||||
|
type: fileType,
|
||||||
|
download: fileName,
|
||||||
|
target: '_blank'
|
||||||
|
}
|
||||||
|
const icon = document.createElement('div')
|
||||||
|
icon.classList.add('icon')
|
||||||
|
icon.innerHTML = fileType.startsWith('image') === true ? imageIcon : attachIcon
|
||||||
|
|
||||||
|
return [
|
||||||
|
'div',
|
||||||
|
nodeAttributes,
|
||||||
|
['div', { class: 'file-name-container' }, icon, ['a', linkAttributes, `${fileName}`]],
|
||||||
|
['div', { class: 'file-size' }, `${filesize(size)}`]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
addInputRules () {
|
||||||
|
return [
|
||||||
|
nodeInputRule({
|
||||||
|
find: inputRegex,
|
||||||
|
type: this.type
|
||||||
|
})
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
addProseMirrorPlugins () {
|
||||||
|
const opt = this.options
|
||||||
|
function handleDrop (
|
||||||
|
view: EditorView,
|
||||||
|
pos: { pos: number, inside: number } | null,
|
||||||
|
dataTransfer: DataTransfer
|
||||||
|
): any {
|
||||||
|
let result = false
|
||||||
|
|
||||||
|
const files = dataTransfer?.files
|
||||||
|
if (files !== undefined && opt.attachFile !== undefined) {
|
||||||
|
let hasNotImageFile: boolean = false
|
||||||
|
for (let i = 0; i < files.length; i++) {
|
||||||
|
const file = files.item(i)
|
||||||
|
if (file != null) {
|
||||||
|
if (!file.type.startsWith('image')) {
|
||||||
|
hasNotImageFile = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!hasNotImageFile) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < files.length; i++) {
|
||||||
|
const file = files.item(i)
|
||||||
|
if (file != null) {
|
||||||
|
result = true
|
||||||
|
void opt.attachFile(file).then((id) => {
|
||||||
|
if (id !== undefined) {
|
||||||
|
const node = view.state.schema.nodes.file.create({
|
||||||
|
'file-id': id.file,
|
||||||
|
'data-file-name': file.name,
|
||||||
|
'data-file-type': file.type,
|
||||||
|
'data-file-size': file.size
|
||||||
|
})
|
||||||
|
const transaction = view.state.tr.insert(pos?.pos ?? 0, node)
|
||||||
|
view.dispatch(transaction)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
new Plugin({
|
||||||
|
key: new PluginKey('handle-file-paste'),
|
||||||
|
props: {
|
||||||
|
handlePaste (view, event) {
|
||||||
|
const dataTransfer = event.clipboardData
|
||||||
|
if (dataTransfer !== null) {
|
||||||
|
const res = handleDrop(view, { pos: view.state.selection.$from.pos, inside: 0 }, dataTransfer)
|
||||||
|
if (res === true) {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleDrop (view, event) {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
const dataTransfer = event.dataTransfer
|
||||||
|
if (dataTransfer !== null) {
|
||||||
|
return handleDrop(view, view.posAtCoords({ left: event.x, top: event.y }), dataTransfer)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleDoubleClickOn (view, pos, node, nodePos, event) {
|
||||||
|
const fileId = node.attrs['file-id'] ?? ''
|
||||||
|
if (fileId === '') return
|
||||||
|
const fileName = node.attrs['data-file-name'] ?? ''
|
||||||
|
const fileType: string = node.attrs['data-file-type'] ?? ''
|
||||||
|
if (!(fileType.startsWith('image/') || fileType === 'text/plain' || fileType === 'application/pdf')) return
|
||||||
|
|
||||||
|
showPopup(
|
||||||
|
PDFViewer,
|
||||||
|
{
|
||||||
|
file: fileId,
|
||||||
|
name: fileName,
|
||||||
|
contentType: fileType,
|
||||||
|
fullSize: true,
|
||||||
|
showIcon: false
|
||||||
|
},
|
||||||
|
'centered'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
@ -21,10 +21,7 @@ import { Plugin, PluginKey } from '@tiptap/pm/state'
|
|||||||
import { type EditorView } from '@tiptap/pm/view'
|
import { type EditorView } from '@tiptap/pm/view'
|
||||||
import { setPlatformStatus, unknownError } from '@hcengineering/platform'
|
import { setPlatformStatus, unknownError } from '@hcengineering/platform'
|
||||||
|
|
||||||
/**
|
import { type FileAttachFunction } from './types'
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
export type FileAttachFunction = (file: File) => Promise<{ file: string, type: string } | undefined>
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
|
19
packages/text-editor/src/components/extension/types.ts
Normal file
19
packages/text-editor/src/components/extension/types.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
//
|
||||||
|
// Copyright © 2023 Hardcore Engineering Inc.
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
//
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export type FileAttachFunction = (file: File) => Promise<{ file: string, type: string } | undefined>
|
@ -64,6 +64,7 @@ export { InlineStyleToolbarExtension, type InlineStyleToolbarOptions } from './c
|
|||||||
export { ImageExtension, type ImageOptions } from './components/extension/imageExt'
|
export { ImageExtension, type ImageOptions } from './components/extension/imageExt'
|
||||||
export { TodoItemExtension, TodoListExtension } from './components/extension/todo'
|
export { TodoItemExtension, TodoListExtension } from './components/extension/todo'
|
||||||
|
|
||||||
|
export * from './command/deleteAttachment'
|
||||||
export {
|
export {
|
||||||
type DocumentId,
|
type DocumentId,
|
||||||
TiptapCollabProvider,
|
TiptapCollabProvider,
|
||||||
|
@ -23,6 +23,7 @@ import TaskItem from '@tiptap/extension-task-item'
|
|||||||
import TaskList from '@tiptap/extension-task-list'
|
import TaskList from '@tiptap/extension-task-list'
|
||||||
|
|
||||||
import { NodeUuid } from '../marks/nodeUuid'
|
import { NodeUuid } from '../marks/nodeUuid'
|
||||||
|
import { FileNode, FileOptions } from '../nodes/file'
|
||||||
import { ImageNode, ImageOptions } from '../nodes/image'
|
import { ImageNode, ImageOptions } from '../nodes/image'
|
||||||
import { ReferenceNode } from '../nodes/reference'
|
import { ReferenceNode } from '../nodes/reference'
|
||||||
import { TodoItemNode, TodoListNode } from '../nodes/todo'
|
import { TodoItemNode, TodoListNode } from '../nodes/todo'
|
||||||
@ -54,13 +55,18 @@ const taskListExtensions = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
export interface ServerKitOptions extends DefaultKitOptions {
|
export interface ServerKitOptions extends DefaultKitOptions {
|
||||||
image: Partial<ImageOptions>
|
file: Partial<FileOptions> | false
|
||||||
|
image: Partial<ImageOptions> | false
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ServerKit = Extension.create<ServerKitOptions>({
|
export const ServerKit = Extension.create<ServerKitOptions>({
|
||||||
name: 'serverKit',
|
name: 'serverKit',
|
||||||
|
|
||||||
addExtensions () {
|
addExtensions () {
|
||||||
|
const fileExtensions = this.options.file !== false ? [FileNode.configure(this.options.file)] : []
|
||||||
|
|
||||||
|
const imageExtensions = this.options.image !== false ? [ImageNode.configure(this.options.image)] : []
|
||||||
|
|
||||||
return [
|
return [
|
||||||
DefaultKit.configure({
|
DefaultKit.configure({
|
||||||
...this.options,
|
...this.options,
|
||||||
@ -70,7 +76,8 @@ export const ServerKit = Extension.create<ServerKitOptions>({
|
|||||||
}),
|
}),
|
||||||
...tableExtensions,
|
...tableExtensions,
|
||||||
...taskListExtensions,
|
...taskListExtensions,
|
||||||
ImageNode.configure(this.options.image),
|
...fileExtensions,
|
||||||
|
...imageExtensions,
|
||||||
TodoItemNode,
|
TodoItemNode,
|
||||||
TodoListNode,
|
TodoListNode,
|
||||||
ReferenceNode,
|
ReferenceNode,
|
||||||
|
114
packages/text/src/nodes/file.ts
Normal file
114
packages/text/src/nodes/file.ts
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
//
|
||||||
|
// Copyright © 2023 Hardcore Engineering Inc.
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
//
|
||||||
|
import { Node } from '@tiptap/core'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export interface FileOptions {
|
||||||
|
inline: boolean
|
||||||
|
HTMLAttributes: Record<string, any>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export const FileNode = Node.create<FileOptions>({
|
||||||
|
name: 'file',
|
||||||
|
|
||||||
|
addOptions () {
|
||||||
|
return {
|
||||||
|
inline: true,
|
||||||
|
HTMLAttributes: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
inline () {
|
||||||
|
return this.options.inline
|
||||||
|
},
|
||||||
|
|
||||||
|
group () {
|
||||||
|
return this.options.inline ? 'inline' : 'block'
|
||||||
|
},
|
||||||
|
|
||||||
|
draggable: true,
|
||||||
|
|
||||||
|
selectable: true,
|
||||||
|
|
||||||
|
addAttributes () {
|
||||||
|
return {
|
||||||
|
'file-id': {
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
'data-file-name': {
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
'data-file-size': {
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
'data-file-type': {
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
'data-file-href': {
|
||||||
|
default: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
parseHTML () {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
tag: `div[data-type="${this.name}"]`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: 'div[data-file-name]'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: 'div[data-file-size]'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: 'div[data-file-type]'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: 'div[data-file-href]'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
renderHTML ({ node, HTMLAttributes }) {
|
||||||
|
const nodeAttributes = {
|
||||||
|
'data-type': this.name
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileName = HTMLAttributes['data-file-name']
|
||||||
|
const size = HTMLAttributes['data-file-size']
|
||||||
|
const fileType = HTMLAttributes['data-file-type']
|
||||||
|
const href = HTMLAttributes['data-file-href']
|
||||||
|
const linkAttributes = {
|
||||||
|
class: 'file-name',
|
||||||
|
href,
|
||||||
|
type: fileType,
|
||||||
|
download: fileName,
|
||||||
|
target: '_blank'
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'div',
|
||||||
|
nodeAttributes,
|
||||||
|
['div', {}, ['a', linkAttributes, `${fileName} (${fileType})`]],
|
||||||
|
['div', {}, `${size}`]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
@ -16,4 +16,5 @@
|
|||||||
export * from './image'
|
export * from './image'
|
||||||
export * from './reference'
|
export * from './reference'
|
||||||
export * from './todo'
|
export * from './todo'
|
||||||
|
export * from './file'
|
||||||
export { getDataAttribute } from './utils'
|
export { getDataAttribute } from './utils'
|
||||||
|
@ -225,6 +225,62 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.text-editor-file-container {
|
||||||
|
background-color: var(--theme-button-default);
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
border: 1px solid var(--theme-button-border);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-weight: 400;
|
||||||
|
width: 22.5rem;
|
||||||
|
height: 2rem;
|
||||||
|
padding: 0 0.25rem;
|
||||||
|
margin: 0.375rem 0;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-name-container {
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
padding: 0 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-name {
|
||||||
|
white-space: nowrap;
|
||||||
|
word-break: break-all;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
line-height: 1.25rem;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
padding: 0 0.25rem;
|
||||||
|
color: var(--theme-content-color);
|
||||||
|
font-weight: 400;
|
||||||
|
text-decoration: none;
|
||||||
|
max-width: 16rem;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-size {
|
||||||
|
font-size: 0.6875rem;
|
||||||
|
color: var(--theme-dark-color);
|
||||||
|
padding: 0 0.25rem;
|
||||||
|
line-height: 1rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.text-editor-image {
|
.text-editor-image {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
@ -238,6 +294,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.text-editor-file-container.ProseMirror-selectednode {
|
||||||
|
box-shadow: 0 0 0 2px var(--text-editor-selected-node-color);
|
||||||
|
border-radius: 0.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
.ProseMirror-gapcursor:after {
|
.ProseMirror-gapcursor:after {
|
||||||
border-top: 1px solid var(--theme-text-primary-color) !important;
|
border-top: 1px solid var(--theme-text-primary-color) !important;
|
||||||
}
|
}
|
||||||
|
@ -203,6 +203,8 @@
|
|||||||
attachment.attachedToClass,
|
attachment.attachedToClass,
|
||||||
'attachments'
|
'attachments'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
await editor.removeAttachment(attachment.file)
|
||||||
}
|
}
|
||||||
|
|
||||||
let progressItems: Ref<Doc>[] = []
|
let progressItems: Ref<Doc>[] = []
|
||||||
|
Loading…
Reference in New Issue
Block a user