mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-28 04:25:03 +02:00
EQMS-885 Refactor image and file extensions (#5584)
Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>
This commit is contained in:
@@ -64,9 +64,9 @@
|
||||
import { noSelectionRender, renderCursor } from './editor/collaboration'
|
||||
import { defaultEditorAttributes } from './editor/editorProps'
|
||||
import { EmojiExtension } from './extension/emoji'
|
||||
import { ImageExtension } from './extension/imageExt'
|
||||
import { ImageUploadExtension } from './extension/imageUploadExt'
|
||||
import { type FileAttachFunction } from './extension/types'
|
||||
import { FileExtension } from './extension/fileExt'
|
||||
import { FileUploadExtension } from './extension/fileUploadExt'
|
||||
import { LeftMenuExtension } from './extension/leftMenu'
|
||||
import { InlineCommandsExtension } from './extension/inlineCommands'
|
||||
import { InlinePopupExtension } from './extension/inlinePopup'
|
||||
@@ -279,16 +279,14 @@
|
||||
if (attachFile !== undefined) {
|
||||
if (canEmbedFiles) {
|
||||
optionalExtensions.push(
|
||||
FileExtension.configure({
|
||||
inline: true,
|
||||
FileUploadExtension.configure({
|
||||
attachFile
|
||||
})
|
||||
)
|
||||
}
|
||||
if (canEmbedImages) {
|
||||
optionalExtensions.push(
|
||||
ImageExtension.configure({
|
||||
inline: true,
|
||||
ImageUploadExtension.configure({
|
||||
attachFile,
|
||||
uploadUrl: getMetadata(presentation.metadata.UploadURL)
|
||||
})
|
||||
|
||||
@@ -19,13 +19,10 @@
|
||||
import { Plugin, PluginKey } from '@tiptap/pm/state'
|
||||
import { DecorationSet } from '@tiptap/pm/view'
|
||||
import { onDestroy, onMount } from 'svelte'
|
||||
import { getMetadata } from '@hcengineering/platform'
|
||||
import presentation from '@hcengineering/presentation'
|
||||
import { MarkupNode, ReferenceNode, jsonToPmNode } from '@hcengineering/text'
|
||||
|
||||
import { calculateDecorations } from './diff/decorations'
|
||||
import { defaultEditorAttributes } from './editor/editorProps'
|
||||
import { ImageExtension } from './extension/imageExt'
|
||||
import { EditorKit } from '../kits/editor-kit'
|
||||
|
||||
export let content: MarkupNode
|
||||
@@ -82,14 +79,7 @@
|
||||
element,
|
||||
content,
|
||||
editable: false,
|
||||
extensions: [
|
||||
EditorKit,
|
||||
ReferenceNode,
|
||||
ImageExtension.configure({
|
||||
uploadUrl: getMetadata(presentation.metadata.UploadURL)
|
||||
}),
|
||||
DecorationExtension
|
||||
],
|
||||
extensions: [EditorKit, ReferenceNode, DecorationExtension],
|
||||
onTransaction: () => {
|
||||
// force re-render so `editor.isActive` works as expected
|
||||
editor = editor
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
import { completionConfig, inlineCommandsConfig } from './extensions'
|
||||
import { EmojiExtension } from './extension/emoji'
|
||||
import { FocusExtension } from './extension/focus'
|
||||
import { ImageExtension } from './extension/imageExt'
|
||||
import { ImageUploadExtension } from './extension/imageUploadExt'
|
||||
import { InlineCommandsExtension } from './extension/inlineCommands'
|
||||
import { type FileAttachFunction } from './extension/types'
|
||||
import { RefAction } from '../types'
|
||||
@@ -175,9 +175,7 @@
|
||||
}
|
||||
|
||||
function configureExtensions (): AnyExtension[] {
|
||||
const imagePlugin = ImageExtension.configure({
|
||||
inline: true,
|
||||
HTMLAttributes: {},
|
||||
const imageUploadPlugin = ImageUploadExtension.configure({
|
||||
attachFile,
|
||||
uploadUrl: getMetadata(presentation.metadata.UploadURL)
|
||||
})
|
||||
@@ -194,7 +192,7 @@
|
||||
extensions.push(completionPlugin)
|
||||
}
|
||||
extensions.push(
|
||||
imagePlugin,
|
||||
imageUploadPlugin,
|
||||
FocusExtension.configure({ onCanBlur: (value: boolean) => (canBlur = value), onFocus: handleFocus })
|
||||
)
|
||||
if (enableEmojiReplace) {
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
|
||||
import { AnyExtension, Content, Editor, FocusPosition, mergeAttributes } from '@tiptap/core'
|
||||
import Placeholder from '@tiptap/extension-placeholder'
|
||||
import { ParseOptions } from '@tiptap/pm/model'
|
||||
import { createEventDispatcher, onDestroy, onMount } from 'svelte'
|
||||
|
||||
import { deleteAttachment } from '../command/deleteAttachment'
|
||||
@@ -34,8 +35,6 @@
|
||||
import { InlineStyleToolbarExtension } from './extension/inlineStyleToolbar'
|
||||
import { SubmitExtension } from './extension/submit'
|
||||
import { EditorKit } from '../kits/editor-kit'
|
||||
import { FileAttachFunction } from './extension/types'
|
||||
import { ParseOptions } from '@tiptap/pm/model'
|
||||
|
||||
export let content: Markup = EmptyMarkup
|
||||
export let placeholder: IntlString = textEditorPlugin.string.EditorPlaceholder
|
||||
@@ -45,7 +44,6 @@
|
||||
export let editorAttributes: Record<string, string> = {}
|
||||
export let boundary: HTMLElement | undefined = undefined
|
||||
export let autofocus: FocusPosition = false
|
||||
export let attachFile: FileAttachFunction | undefined = undefined
|
||||
|
||||
let element: HTMLElement
|
||||
let editor: Editor
|
||||
|
||||
@@ -16,11 +16,8 @@ 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 =
|
||||
@@ -33,7 +30,6 @@ const imageIcon =
|
||||
*/
|
||||
export interface FileOptions extends FileNodeOptions {
|
||||
attachFile?: FileAttachFunction
|
||||
reportNode?: (id: string, node: ProseMirrorNode) => void
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -84,7 +80,6 @@ export const FileExtension = FileNode.extend<FileOptions>({
|
||||
let href: string = ''
|
||||
if (id != null) {
|
||||
href = getFileUrl(id, 'full', fileName)
|
||||
this.options.reportNode?.(id, node)
|
||||
}
|
||||
const linkAttributes = {
|
||||
class: 'file-name',
|
||||
@@ -115,74 +110,15 @@ export const FileExtension = FileNode.extend<FileOptions>({
|
||||
},
|
||||
|
||||
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'),
|
||||
key: new PluginKey('handle-file-open'),
|
||||
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) {
|
||||
if (node.type.name !== FileExtension.name) {
|
||||
return
|
||||
}
|
||||
|
||||
const fileId = node.attrs['file-id'] ?? ''
|
||||
if (fileId === '') return
|
||||
const fileName = node.attrs['data-file-name'] ?? ''
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
//
|
||||
// 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 { Extension } from '@tiptap/core'
|
||||
import { Plugin, PluginKey } from '@tiptap/pm/state'
|
||||
import { type EditorView } from '@tiptap/pm/view'
|
||||
|
||||
import { type FileAttachFunction } from './types'
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface FileUploadOptions {
|
||||
attachFile?: FileAttachFunction
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const FileUploadExtension = Extension.create<FileUploadOptions>({
|
||||
name: 'file-upload-ext',
|
||||
|
||||
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 && !file.type.startsWith('image/')) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
]
|
||||
}
|
||||
})
|
||||
@@ -12,16 +12,12 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
import { PDFViewer, getImageSize } from '@hcengineering/presentation'
|
||||
import { PDFViewer } from '@hcengineering/presentation'
|
||||
import { showPopup } from '@hcengineering/ui'
|
||||
import { ImageNode, type ImageOptions as ImageNodeOptions } from '@hcengineering/text'
|
||||
import { type IconSize, getIconSize2x, showPopup } from '@hcengineering/ui'
|
||||
import { type IconSize, getIconSize2x } from '@hcengineering/ui'
|
||||
import { mergeAttributes, 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 { setPlatformStatus, unknownError } from '@hcengineering/platform'
|
||||
|
||||
import { type FileAttachFunction } from './types'
|
||||
|
||||
/**
|
||||
* @public
|
||||
@@ -32,8 +28,6 @@ export type ImageAlignment = 'center' | 'left' | 'right'
|
||||
* @public
|
||||
*/
|
||||
export interface ImageOptions extends ImageNodeOptions {
|
||||
attachFile?: FileAttachFunction
|
||||
reportNode?: (id: string, node: ProseMirrorNode) => void
|
||||
uploadUrl: string
|
||||
}
|
||||
|
||||
@@ -70,16 +64,8 @@ declare module '@tiptap/core' {
|
||||
*/
|
||||
export const inputRegex = /(?:^|\s)(!\[(.+|:?)]\((\S+)(?:(?:\s+)["'](\S+)["'])?\))$/
|
||||
|
||||
function getType (type: string): 'image' | 'other' {
|
||||
if (type.startsWith('image/')) {
|
||||
return 'image'
|
||||
}
|
||||
|
||||
return 'other'
|
||||
}
|
||||
|
||||
// This is a simplified version of getFileUrl from presentation plugin, which we cannot use
|
||||
function getFileUrl (fileId: string, size: IconSize = 'full', uploadUrl: string): string {
|
||||
export function getFileUrl (fileId: string, size: IconSize = 'full', uploadUrl: string): string {
|
||||
return `${uploadUrl}?file=${fileId}&size=${size as string}`
|
||||
}
|
||||
|
||||
@@ -149,7 +135,6 @@ export const ImageExtension = ImageNode.extend<ImageOptions>({
|
||||
}
|
||||
imgAttributes.class = 'text-editor-image'
|
||||
imgAttributes.contentEditable = false
|
||||
this.options.reportNode?.(id, node)
|
||||
}
|
||||
|
||||
return ['div', divAttributes, ['img', imgAttributes]]
|
||||
@@ -203,90 +188,12 @@ export const ImageExtension = ImageNode.extend<ImageOptions>({
|
||||
},
|
||||
|
||||
addProseMirrorPlugins () {
|
||||
const attachFile = this.options.attachFile
|
||||
const uploadUrl = this.options.uploadUrl
|
||||
|
||||
function handleDrop (
|
||||
view: EditorView,
|
||||
pos: { pos: number, inside: number } | null,
|
||||
dataTransfer: DataTransfer
|
||||
): any {
|
||||
const uris = (dataTransfer.getData('text/uri-list') ?? '').split('\r\n').filter((it) => !it.startsWith('#'))
|
||||
let result = false
|
||||
for (const uri of uris) {
|
||||
if (uri !== '') {
|
||||
const url = new URL(uri)
|
||||
if (uploadUrl === undefined || !url.href.includes(uploadUrl)) {
|
||||
continue
|
||||
}
|
||||
|
||||
const _file = (url.searchParams.get('file') ?? '').split('/').join('')
|
||||
|
||||
if (_file.trim().length === 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
const ctype = dataTransfer.getData('application/contentType')
|
||||
const type = getType(ctype ?? 'other')
|
||||
|
||||
if (type === 'image') {
|
||||
const node = view.state.schema.nodes.image.create({
|
||||
'file-id': _file,
|
||||
src: getFileUrl(_file, 'full', uploadUrl)
|
||||
})
|
||||
const transaction = view.state.tr.insert(pos?.pos ?? 0, node)
|
||||
view.dispatch(transaction)
|
||||
result = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if (result) {
|
||||
return result
|
||||
}
|
||||
|
||||
const files = dataTransfer?.files
|
||||
if (files !== undefined && attachFile !== undefined) {
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files.item(i)
|
||||
if (file != null) {
|
||||
result = true
|
||||
void handleImageUpload(file, view, pos, attachFile, uploadUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
return [
|
||||
new Plugin({
|
||||
key: new PluginKey('handle-image-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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
new Plugin({
|
||||
key: new PluginKey('handle-image-open'),
|
||||
props: {
|
||||
handleDoubleClickOn (view, pos, node, nodePos, event) {
|
||||
if (node.type.name !== 'image') {
|
||||
if (node.type.name !== ImageExtension.name) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -310,37 +217,3 @@ export const ImageExtension = ImageNode.extend<ImageOptions>({
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
async function handleImageUpload (
|
||||
file: File,
|
||||
view: EditorView,
|
||||
pos: { pos: number, inside: number } | null,
|
||||
attachFile: FileAttachFunction,
|
||||
uploadUrl: string
|
||||
): Promise<void> {
|
||||
const attached = await attachFile(file)
|
||||
|
||||
if (attached === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!attached.type.includes('image')) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const url = getFileUrl(attached.file, 'full', uploadUrl)
|
||||
const size = await getImageSize(file, url)
|
||||
const node = view.state.schema.nodes.image.create({
|
||||
'file-id': attached.file,
|
||||
src: url,
|
||||
width: Math.round(size.width / size.pixelRatio)
|
||||
})
|
||||
|
||||
const transaction = view.state.tr.insert(pos?.pos ?? 0, node)
|
||||
|
||||
view.dispatch(transaction)
|
||||
} catch (e) {
|
||||
void setPlatformStatus(unknownError(e))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
//
|
||||
// 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 { getImageSize } from '@hcengineering/presentation'
|
||||
import { Extension } from '@tiptap/core'
|
||||
import { Plugin, PluginKey } from '@tiptap/pm/state'
|
||||
import { type EditorView } from '@tiptap/pm/view'
|
||||
import { setPlatformStatus, unknownError } from '@hcengineering/platform'
|
||||
|
||||
import { getFileUrl } from './imageExt'
|
||||
import { type FileAttachFunction } from './types'
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface ImageUploadOptions {
|
||||
attachFile?: FileAttachFunction
|
||||
uploadUrl: string
|
||||
}
|
||||
|
||||
function getType (type: string): 'image' | 'other' {
|
||||
if (type.startsWith('image/')) {
|
||||
return 'image'
|
||||
}
|
||||
|
||||
return 'other'
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const ImageUploadExtension = Extension.create<ImageUploadOptions>({
|
||||
addOptions () {
|
||||
return {
|
||||
uploadUrl: ''
|
||||
}
|
||||
},
|
||||
|
||||
addProseMirrorPlugins () {
|
||||
const attachFile = this.options.attachFile
|
||||
const uploadUrl = this.options.uploadUrl
|
||||
|
||||
function handleDrop (
|
||||
view: EditorView,
|
||||
pos: { pos: number, inside: number } | null,
|
||||
dataTransfer: DataTransfer
|
||||
): any {
|
||||
const uris = (dataTransfer.getData('text/uri-list') ?? '').split('\r\n').filter((it) => !it.startsWith('#'))
|
||||
let result = false
|
||||
for (const uri of uris) {
|
||||
if (uri !== '') {
|
||||
const url = new URL(uri)
|
||||
if (uploadUrl === undefined || !url.href.includes(uploadUrl)) {
|
||||
continue
|
||||
}
|
||||
|
||||
const _file = (url.searchParams.get('file') ?? '').split('/').join('')
|
||||
|
||||
if (_file.trim().length === 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
const ctype = dataTransfer.getData('application/contentType')
|
||||
const type = getType(ctype ?? 'other')
|
||||
|
||||
if (type === 'image') {
|
||||
const node = view.state.schema.nodes.image.create({
|
||||
'file-id': _file,
|
||||
src: getFileUrl(_file, 'full', uploadUrl)
|
||||
})
|
||||
const transaction = view.state.tr.insert(pos?.pos ?? 0, node)
|
||||
view.dispatch(transaction)
|
||||
result = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if (result) {
|
||||
return result
|
||||
}
|
||||
|
||||
const files = dataTransfer?.files
|
||||
if (files !== undefined && attachFile !== undefined) {
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files.item(i)
|
||||
if (file != null && file.type.startsWith('image/')) {
|
||||
result = true
|
||||
void handleImageUpload(file, view, pos, attachFile, uploadUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
return [
|
||||
new Plugin({
|
||||
key: new PluginKey('handle-image-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)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
async function handleImageUpload (
|
||||
file: File,
|
||||
view: EditorView,
|
||||
pos: { pos: number, inside: number } | null,
|
||||
attachFile: FileAttachFunction,
|
||||
uploadUrl: string
|
||||
): Promise<void> {
|
||||
const attached = await attachFile(file)
|
||||
|
||||
if (attached === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!attached.type.includes('image')) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const url = getFileUrl(attached.file, 'full', uploadUrl)
|
||||
const size = await getImageSize(file, url)
|
||||
const node = view.state.schema.nodes.image.create({
|
||||
'file-id': attached.file,
|
||||
src: url,
|
||||
width: Math.round(size.width / size.pixelRatio)
|
||||
})
|
||||
|
||||
const transaction = view.state.tr.insert(pos?.pos ?? 0, node)
|
||||
|
||||
view.dispatch(transaction)
|
||||
} catch (e) {
|
||||
void setPlatformStatus(unknownError(e))
|
||||
}
|
||||
}
|
||||
@@ -73,6 +73,7 @@ export {
|
||||
export { InlinePopupExtension } from './components/extension/inlinePopup'
|
||||
export { InlineStyleToolbarExtension, type InlineStyleToolbarOptions } from './components/extension/inlineStyleToolbar'
|
||||
export { ImageExtension, type ImageOptions } from './components/extension/imageExt'
|
||||
export { ImageUploadExtension, type ImageUploadOptions } from './components/extension/imageUploadExt'
|
||||
export { TodoItemExtension, TodoListExtension } from './components/extension/todo'
|
||||
|
||||
export * from './command/deleteAttachment'
|
||||
|
||||
@@ -22,10 +22,14 @@ import TaskList from '@tiptap/extension-task-list'
|
||||
|
||||
import { DefaultKit, type DefaultKitOptions } from './default-kit'
|
||||
|
||||
import { getMetadata } from '@hcengineering/platform'
|
||||
import presentation from '@hcengineering/presentation'
|
||||
import { CodeBlockExtension, codeBlockOptions } from '@hcengineering/text'
|
||||
import { CodemarkExtension } from '../components/extension/codemark'
|
||||
import { NodeUuidExtension } from '../components/extension/nodeUuid'
|
||||
import { Table, TableCell, TableRow } from '../components/extension/table'
|
||||
import { type ImageOptions, ImageExtension } from '../components/extension/imageExt'
|
||||
import { type FileOptions, FileExtension } from '../components/extension/fileExt'
|
||||
|
||||
const headingLevels: Level[] = [1, 2, 3]
|
||||
|
||||
@@ -53,6 +57,8 @@ export const taskListExtensions = [
|
||||
|
||||
export interface EditorKitOptions extends DefaultKitOptions {
|
||||
history?: false
|
||||
file?: Partial<FileOptions> | false
|
||||
image?: Partial<ImageOptions> | false
|
||||
}
|
||||
|
||||
export const EditorKit = Extension.create<EditorKitOptions>({
|
||||
@@ -86,7 +92,24 @@ export const EditorKit = Extension.create<EditorKitOptions>({
|
||||
]
|
||||
}),
|
||||
NodeUuidExtension,
|
||||
...tableExtensions
|
||||
...tableExtensions,
|
||||
...(this.options.file !== false
|
||||
? [
|
||||
FileExtension.configure({
|
||||
inline: true,
|
||||
...this.options.file
|
||||
})
|
||||
]
|
||||
: []),
|
||||
...(this.options.image !== false
|
||||
? [
|
||||
ImageExtension.configure({
|
||||
inline: true,
|
||||
uploadUrl: getMetadata(presentation.metadata.UploadURL) ?? '',
|
||||
...this.options.image
|
||||
})
|
||||
]
|
||||
: [])
|
||||
// ...taskListExtensions // Disable since tasks are not working properly now.
|
||||
]
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
import {
|
||||
CollaboratorEditor,
|
||||
HeadingsExtension,
|
||||
ImageOptions,
|
||||
ImageUploadOptions,
|
||||
SvelteNodeViewRenderer,
|
||||
TodoItemExtension,
|
||||
TodoListExtension
|
||||
@@ -37,7 +37,7 @@
|
||||
export let object: Document
|
||||
export let readonly = false
|
||||
export let boundary: HTMLElement | undefined = undefined
|
||||
export let attachFile: ImageOptions['attachFile'] | undefined = undefined
|
||||
export let attachFile: ImageUploadOptions['attachFile'] | undefined = undefined
|
||||
export let focusIndex = -1
|
||||
export let overflow: 'auto' | 'none' = 'none'
|
||||
export let editorAttributes: Record<string, string> = {}
|
||||
|
||||
Reference in New Issue
Block a user