mirror of
https://github.com/hcengineering/platform.git
synced 2024-11-22 21:50:34 +03:00
UBER-1076 Wiki images support (#3864)
Signed-off-by: Alexander Onnikov <alexander.onnikov@xored.com>
This commit is contained in:
parent
e3e2b02d40
commit
435d094c7d
@ -26,6 +26,7 @@
|
||||
export let contentType: string | undefined
|
||||
export let popupOptions: PopupOptions
|
||||
export let value: Doc
|
||||
export let showIcon = true
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
// let imgView: 'img-horizontal-fit' | 'img-vertical-fit' | 'img-original-fit' = 'img-vertical-fit'
|
||||
@ -51,11 +52,13 @@
|
||||
>
|
||||
<svelte:fragment slot="title">
|
||||
<div class="antiTitle icon-wrapper">
|
||||
<div class="wrapped-icon">
|
||||
<div class="flex-center icon">
|
||||
{iconLabel(name)}
|
||||
{#if showIcon}
|
||||
<div class="wrapped-icon">
|
||||
<div class="flex-center icon">
|
||||
{iconLabel(name)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<span class="wrapped-title">{name}</span>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
|
@ -21,6 +21,12 @@
|
||||
"GIF": "GIF",
|
||||
"Mention": "Mention",
|
||||
"Underlined": "Underlined",
|
||||
"AlignCenter": "Align center",
|
||||
"AlignLeft": "Align left",
|
||||
"AlignRight": "Align right",
|
||||
"ViewImage": "View image",
|
||||
"ViewOriginal": "View original",
|
||||
"MoreActions": "More actions",
|
||||
"FullDescription": "Full description",
|
||||
"NoFullDescription": "There are no detailed description",
|
||||
"EnableDiffMode": "Diff mode",
|
||||
|
@ -21,6 +21,12 @@
|
||||
"GIF": "GIF",
|
||||
"Mention": "Упомянуть",
|
||||
"Underlined": "Подчеркнутый",
|
||||
"AlignCenter": "По центру",
|
||||
"AlignLeft": "По левому краю",
|
||||
"AlignRight": "По правому краю",
|
||||
"ViewImage": "Открыть изображение",
|
||||
"ViewOriginal": "Открыть оригинал",
|
||||
"MoreActions": "Дополнительные действия",
|
||||
"FullDescription": "Детальное описание",
|
||||
"NoFullDescription": "Нет детального описания",
|
||||
"EnableDiffMode": "Режим сравнения",
|
||||
|
@ -2,6 +2,7 @@ import { Node, mergeAttributes } from '@tiptap/core'
|
||||
import Suggestion, { SuggestionOptions } from '@tiptap/suggestion'
|
||||
|
||||
import { Plugin, PluginKey } from '@tiptap/pm/state'
|
||||
import { getDataAttribute } from './utils'
|
||||
|
||||
export interface CompletionOptions {
|
||||
HTMLAttributes: Record<string, any>
|
||||
@ -96,49 +97,9 @@ export const Completion = Node.create<CompletionOptions>({
|
||||
|
||||
addAttributes () {
|
||||
return {
|
||||
id: {
|
||||
default: null,
|
||||
parseHTML: (element) => element.getAttribute('data-id'),
|
||||
renderHTML: (attributes) => {
|
||||
// eslint-disable-next-line
|
||||
if (!attributes.id) {
|
||||
return {}
|
||||
}
|
||||
|
||||
return {
|
||||
'data-id': attributes.id
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
label: {
|
||||
default: null,
|
||||
parseHTML: (element) => element.getAttribute('data-label'),
|
||||
renderHTML: (attributes) => {
|
||||
// eslint-disable-next-line
|
||||
if (!attributes.label) {
|
||||
return {}
|
||||
}
|
||||
|
||||
return {
|
||||
'data-label': attributes.label
|
||||
}
|
||||
}
|
||||
},
|
||||
objectclass: {
|
||||
default: null,
|
||||
parseHTML: (element) => element.getAttribute('data-objectclass'),
|
||||
renderHTML: (attributes) => {
|
||||
// eslint-disable-next-line
|
||||
if (!attributes.objectclass) {
|
||||
return {}
|
||||
}
|
||||
|
||||
return {
|
||||
'data-objectclass': attributes.objectclass
|
||||
}
|
||||
}
|
||||
}
|
||||
id: getDataAttribute('id', null),
|
||||
label: getDataAttribute('label', null),
|
||||
objectclass: getDataAttribute('objectclass', null)
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -45,8 +45,11 @@
|
||||
import { noSelectionRender } from './editor/collaboration'
|
||||
import { defaultEditorAttributes } from './editor/editorProps'
|
||||
import { completionConfig, defaultExtensions } from './extensions'
|
||||
import { InlinePopupExtension } from './extension/inlinePopup'
|
||||
import { InlineStyleToolbarExtension } from './extension/inlineStyleToolbar'
|
||||
import { FileAttachFunction, ImageExtension } from './extension/imageExt'
|
||||
import { NodeUuidExtension } from './extension/nodeUuid'
|
||||
import ImageStyleToolbar from './ImageStyleToolbar.svelte'
|
||||
import StyleButton from './StyleButton.svelte'
|
||||
import TextEditorStyleToolbar from './TextEditorStyleToolbar.svelte'
|
||||
|
||||
@ -72,6 +75,8 @@
|
||||
export let onExtensions: () => AnyExtension[] = () => []
|
||||
export let boundary: HTMLElement | undefined = undefined
|
||||
|
||||
export let attachFile: FileAttachFunction | undefined = undefined
|
||||
|
||||
let element: HTMLElement
|
||||
|
||||
const ydoc = (getContext(CollaborationIds.Doc) as Y.Doc | undefined) ?? new Y.Doc()
|
||||
@ -99,7 +104,8 @@
|
||||
const currentUser = getCurrentAccount()
|
||||
|
||||
let editor: Editor
|
||||
let inlineToolbar: HTMLElement
|
||||
let textToolbarElement: HTMLElement
|
||||
let imageToolbarElement: HTMLElement
|
||||
|
||||
let placeHolderStr: string = ''
|
||||
|
||||
@ -234,6 +240,34 @@
|
||||
$: updateEditor(editor, field, comparedVersion)
|
||||
$: if (editor) dispatch('editor', editor)
|
||||
|
||||
const tippyOptions = {
|
||||
zIndex: 100000,
|
||||
popperOptions: {
|
||||
modifiers: [
|
||||
{
|
||||
name: 'preventOverflow',
|
||||
options: {
|
||||
boundary,
|
||||
padding: 8,
|
||||
altAxis: true,
|
||||
tether: false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
const optionalExtensions: AnyExtension[] = []
|
||||
|
||||
if (attachFile !== undefined) {
|
||||
optionalExtensions.push(
|
||||
ImageExtension.configure({
|
||||
inline: true,
|
||||
attachFile
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
ph.then(() => {
|
||||
editor = new Editor({
|
||||
@ -242,27 +276,25 @@
|
||||
editorProps: { attributes: mergeAttributes(defaultEditorAttributes, editorAttributes, { class: 'flex-grow' }) },
|
||||
extensions: [
|
||||
...defaultExtensions,
|
||||
...optionalExtensions,
|
||||
Placeholder.configure({ placeholder: placeHolderStr }),
|
||||
InlineStyleToolbarExtension.configure({
|
||||
tippyOptions: {
|
||||
popperOptions: {
|
||||
modifiers: [
|
||||
{
|
||||
name: 'preventOverflow',
|
||||
options: {
|
||||
boundary,
|
||||
padding: 8,
|
||||
altAxis: true,
|
||||
tether: false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
element: inlineToolbar,
|
||||
tippyOptions,
|
||||
element: textToolbarElement,
|
||||
isSupported: () => !readonly,
|
||||
isSelectionOnly: () => false
|
||||
}),
|
||||
InlinePopupExtension.configure({
|
||||
pluginKey: 'show-image-actions-popup',
|
||||
element: imageToolbarElement,
|
||||
tippyOptions,
|
||||
shouldShow: () => {
|
||||
if (!visible && !readonly) {
|
||||
return false
|
||||
}
|
||||
return editor?.isActive('image')
|
||||
}
|
||||
}),
|
||||
Collaboration.configure({
|
||||
document: ydoc,
|
||||
field
|
||||
@ -331,7 +363,7 @@
|
||||
const { idx, focusManager } = registerFocus(focusIndex, {
|
||||
focus: () => {
|
||||
if (visible) {
|
||||
focus('start')
|
||||
focus()
|
||||
}
|
||||
return visible && element !== null
|
||||
},
|
||||
@ -346,6 +378,10 @@
|
||||
$: if (element) {
|
||||
element.addEventListener('focus', updateFocus, { once: true })
|
||||
}
|
||||
|
||||
function handleFocus () {
|
||||
needFocus = true
|
||||
}
|
||||
</script>
|
||||
|
||||
<slot {editor} />
|
||||
@ -375,7 +411,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="formatPanel buttons-group xsmall-gap mb-4" bind:this={inlineToolbar}>
|
||||
<div class="formatPanel buttons-group xsmall-gap mb-4" bind:this={textToolbarElement}>
|
||||
<TextEditorStyleToolbar
|
||||
textEditor={editor}
|
||||
textFormatCategories={[
|
||||
@ -399,6 +435,10 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="formatPanel buttons-group xsmall-gap mb-4" bind:this={imageToolbarElement}>
|
||||
<ImageStyleToolbar textEditor={editor} formatButtonSize={buttonSize} on:focus={handleFocus} />
|
||||
</div>
|
||||
|
||||
<div class="ref-container" style:overflow>
|
||||
<div class="text-input" class:focusable>
|
||||
<div class="select-text" style="width: 100%;" bind:this={element} />
|
||||
|
133
packages/text-editor/src/components/ImageStyleToolbar.svelte
Normal file
133
packages/text-editor/src/components/ImageStyleToolbar.svelte
Normal file
@ -0,0 +1,133 @@
|
||||
<!--
|
||||
// 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.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { getEmbeddedLabel } from '@hcengineering/platform'
|
||||
import { PDFViewer, getFileUrl } from '@hcengineering/presentation'
|
||||
import { IconExpand, IconMoreH, IconSize, SelectPopup, getEventPositionElement, showPopup } from '@hcengineering/ui'
|
||||
import { Editor } from '@tiptap/core'
|
||||
import IconAlignCenter from './icons/AlignCenter.svelte'
|
||||
import IconAlignLeft from './icons/AlignLeft.svelte'
|
||||
import IconAlignRight from './icons/AlignRight.svelte'
|
||||
import IconScaleOut from './icons/ScaleOut.svelte'
|
||||
import StyleButton from './StyleButton.svelte'
|
||||
import plugin from '../plugin'
|
||||
|
||||
export let formatButtonSize: IconSize = 'small'
|
||||
export let textEditor: Editor
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
function getImageAlignmentToggler (align: 'center' | 'left' | 'right') {
|
||||
return () => {
|
||||
textEditor.commands.setImageAlignment({ align })
|
||||
dispatch('focus')
|
||||
}
|
||||
}
|
||||
|
||||
function openImage () {
|
||||
const attributes = textEditor.getAttributes('image')
|
||||
const fileId = attributes['file-id']
|
||||
const fileName = attributes.alt ?? ''
|
||||
showPopup(PDFViewer, { file: fileId, name: fileName, contentType: 'image/*', showIcon: false }, 'centered', () => {
|
||||
dispatch('focus')
|
||||
})
|
||||
}
|
||||
|
||||
function openOriginalImage () {
|
||||
const attributes = textEditor.getAttributes('image')
|
||||
const url = getFileUrl(attributes['file-id'], 'full')
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
|
||||
function moreOptions (event: MouseEvent) {
|
||||
const widthActions = ['25%', '50%', '75%', '100%', plugin.string.Unset].map((it) => {
|
||||
return {
|
||||
id: `#imageWidth${it}`,
|
||||
label: it === plugin.string.Unset ? it : getEmbeddedLabel(it),
|
||||
action: () => textEditor.commands.setImageSize({ width: it === plugin.string.Unset ? undefined : it }),
|
||||
category: {
|
||||
label: plugin.string.Width
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const actions = [
|
||||
{
|
||||
id: '#imageOpen',
|
||||
icon: IconScaleOut,
|
||||
label: plugin.string.ViewImage,
|
||||
action: openImage
|
||||
},
|
||||
{
|
||||
id: '#imageOriginal',
|
||||
icon: IconExpand,
|
||||
label: plugin.string.ViewOriginal,
|
||||
action: openOriginalImage
|
||||
},
|
||||
...widthActions
|
||||
]
|
||||
|
||||
showPopup(
|
||||
SelectPopup,
|
||||
{
|
||||
value: actions
|
||||
},
|
||||
getEventPositionElement(event),
|
||||
(val) => {
|
||||
if (val !== undefined) {
|
||||
const op = actions.find((it) => it.id === val)
|
||||
if (op) {
|
||||
op.action()
|
||||
dispatch('focus')
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if textEditor}
|
||||
{#if textEditor.isActive('image')}
|
||||
<StyleButton
|
||||
icon={IconAlignLeft}
|
||||
size={formatButtonSize}
|
||||
selected={textEditor.isActive('image', { align: 'left' })}
|
||||
showTooltip={{ label: plugin.string.AlignLeft }}
|
||||
on:click={getImageAlignmentToggler('left')}
|
||||
/>
|
||||
<StyleButton
|
||||
icon={IconAlignCenter}
|
||||
size={formatButtonSize}
|
||||
selected={textEditor.isActive('image', { align: 'center' })}
|
||||
showTooltip={{ label: plugin.string.AlignCenter }}
|
||||
on:click={getImageAlignmentToggler('center')}
|
||||
/>
|
||||
<StyleButton
|
||||
icon={IconAlignRight}
|
||||
size={formatButtonSize}
|
||||
selected={textEditor.isActive('image', { align: 'right' })}
|
||||
showTooltip={{ label: plugin.string.AlignRight }}
|
||||
on:click={getImageAlignmentToggler('right')}
|
||||
/>
|
||||
<div class="buttons-divider" />
|
||||
<StyleButton
|
||||
icon={IconMoreH}
|
||||
size={formatButtonSize}
|
||||
on:click={moreOptions}
|
||||
showTooltip={{ label: plugin.string.MoreActions }}
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
@ -15,6 +15,7 @@
|
||||
} from '@hcengineering/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import type { AnyExtension } from '@tiptap/core'
|
||||
import { Node as ProseMirrorNode } from '@tiptap/pm/model'
|
||||
|
||||
import { Completion } from '../Completion'
|
||||
import textEditorPlugin from '../plugin'
|
||||
@ -23,9 +24,7 @@
|
||||
import { completionConfig } from './extensions'
|
||||
import { EmojiExtension } from './extension/emoji'
|
||||
import { FocusExtension } from './extension/focus'
|
||||
|
||||
import { ImageRef, FileAttachFunction } from './imageExt'
|
||||
import { Node as ProseMirrorNode } from '@tiptap/pm/model'
|
||||
import { ImageExtension, FileAttachFunction } from './extension/imageExt'
|
||||
import { RefAction } from '../types'
|
||||
|
||||
export let label: IntlString | undefined = undefined
|
||||
@ -170,7 +169,7 @@
|
||||
}
|
||||
|
||||
function configureExtensions () {
|
||||
const imagePlugin = ImageRef.configure({
|
||||
const imagePlugin = ImageExtension.configure({
|
||||
inline: true,
|
||||
HTMLAttributes: {},
|
||||
attachFile,
|
||||
|
@ -1,6 +1,6 @@
|
||||
<!--
|
||||
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
||||
// Copyright © 2021 Hardcore Engineering Inc.
|
||||
// Copyright © 2021, 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
|
||||
@ -15,21 +15,23 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { IntlString, translate } from '@hcengineering/platform'
|
||||
import { themeStore } from '@hcengineering/ui'
|
||||
|
||||
import { FocusPosition, mergeAttributes } from '@tiptap/core'
|
||||
import { AnyExtension, Editor, Extension, HTMLContent } from '@tiptap/core'
|
||||
|
||||
import Placeholder from '@tiptap/extension-placeholder'
|
||||
|
||||
import { createEventDispatcher, onDestroy, onMount } from 'svelte'
|
||||
import textEditorPlugin from '../plugin'
|
||||
import { defaultExtensions } from './extensions'
|
||||
import { Node as ProseMirrorNode } from '@tiptap/pm/model'
|
||||
import { themeStore } from '@hcengineering/ui'
|
||||
import TextEditorStyleToolbar from './TextEditorStyleToolbar.svelte'
|
||||
import { createEventDispatcher, onDestroy, onMount } from 'svelte'
|
||||
|
||||
import textEditorPlugin from '../plugin'
|
||||
import { TextFormatCategory } from '../types'
|
||||
import { InlineStyleToolbarExtension } from './extension/inlineStyleToolbar'
|
||||
|
||||
import { defaultEditorAttributes } from './editor/editorProps'
|
||||
import { defaultExtensions } from './extensions'
|
||||
import { InlinePopupExtension } from './extension/inlinePopup'
|
||||
import { InlineStyleToolbarExtension } from './extension/inlineStyleToolbar'
|
||||
import ImageStyleToolbar from './ImageStyleToolbar.svelte'
|
||||
import TextEditorStyleToolbar from './TextEditorStyleToolbar.svelte'
|
||||
|
||||
export let content: string = ''
|
||||
export let placeholder: IntlString = textEditorPlugin.string.EditorPlaceholder
|
||||
@ -78,7 +80,25 @@
|
||||
let needFocus = false
|
||||
let focused = false
|
||||
let posFocus: FocusPosition | undefined = undefined
|
||||
let textEditorToolbar: HTMLElement
|
||||
let textToolbarElement: HTMLElement
|
||||
let imageToolbarElement: HTMLElement
|
||||
|
||||
const tippyOptions = {
|
||||
zIndex: 100000,
|
||||
popperOptions: {
|
||||
modifiers: [
|
||||
{
|
||||
name: 'preventOverflow',
|
||||
options: {
|
||||
boundary,
|
||||
padding: 8,
|
||||
altAxis: true,
|
||||
tether: false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
export function focus (position?: FocusPosition): void {
|
||||
posFocus = position
|
||||
@ -138,25 +158,16 @@
|
||||
Placeholder.configure({ placeholder: placeHolderStr }),
|
||||
...extensions,
|
||||
InlineStyleToolbarExtension.configure({
|
||||
tippyOptions: {
|
||||
zIndex: 100000,
|
||||
popperOptions: {
|
||||
modifiers: [
|
||||
{
|
||||
name: 'preventOverflow',
|
||||
options: {
|
||||
boundary,
|
||||
padding: 8,
|
||||
altAxis: true,
|
||||
tether: false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
element: textEditorToolbar,
|
||||
tippyOptions,
|
||||
element: textToolbarElement,
|
||||
isSupported: () => true,
|
||||
isSelectionOnly: () => false
|
||||
}),
|
||||
InlinePopupExtension.configure({
|
||||
pluginKey: 'show-image-actions-popup',
|
||||
element: imageToolbarElement,
|
||||
tippyOptions,
|
||||
shouldShow: () => editor?.isActive('image')
|
||||
})
|
||||
],
|
||||
parseOptions: {
|
||||
@ -206,7 +217,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="formatPanel buttons-group xsmall-gap mb-4" bind:this={textEditorToolbar}>
|
||||
<div class="formatPanel buttons-group xsmall-gap mb-4" bind:this={textToolbarElement}>
|
||||
<TextEditorStyleToolbar
|
||||
textEditor={editor}
|
||||
{textFormatCategories}
|
||||
@ -215,6 +226,16 @@
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="formatPanel buttons-group xsmall-gap mb-4" bind:this={imageToolbarElement}>
|
||||
<ImageStyleToolbar
|
||||
textEditor={editor}
|
||||
on:focus={() => {
|
||||
needFocus = true
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="select-text" style="width: 100%;" bind:this={element} />
|
||||
|
||||
<style lang="scss">
|
||||
|
@ -1,12 +1,11 @@
|
||||
import { getEmbeddedLabel, getMetadata } from '@hcengineering/platform'
|
||||
import { getMetadata } from '@hcengineering/platform'
|
||||
import presentation, { getFileUrl } from '@hcengineering/presentation'
|
||||
import { Action, IconSize, Menu, getEventPositionElement, getIconSize2x, showPopup } from '@hcengineering/ui'
|
||||
import { IconSize, getIconSize2x } from '@hcengineering/ui'
|
||||
import { Node, createNodeFromContent, mergeAttributes, nodeInputRule } from '@tiptap/core'
|
||||
import { Plugin, PluginKey } from 'prosemirror-state'
|
||||
import plugin from '../plugin'
|
||||
|
||||
import { Fragment, Node as ProseMirrorNode } from '@tiptap/pm/model'
|
||||
import { EditorView } from 'prosemirror-view'
|
||||
import { Plugin, PluginKey } from '@tiptap/pm/state'
|
||||
import { EditorView } from '@tiptap/pm/view'
|
||||
import { getDataAttribute } from '../../utils'
|
||||
|
||||
/**
|
||||
* @public
|
||||
@ -25,6 +24,15 @@ export interface ImageOptions {
|
||||
reportNode?: (id: string, node: ProseMirrorNode) => void
|
||||
}
|
||||
|
||||
export interface ImageAlignmentOptions {
|
||||
align?: 'center' | 'left' | 'right'
|
||||
}
|
||||
|
||||
export interface ImageSizeOptions {
|
||||
height?: number | string
|
||||
width?: number | string
|
||||
}
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
image: {
|
||||
@ -32,6 +40,14 @@ declare module '@tiptap/core' {
|
||||
* Add an image
|
||||
*/
|
||||
setImage: (options: { src: string, alt?: string, title?: string }) => ReturnType
|
||||
/**
|
||||
* Set image alignment
|
||||
*/
|
||||
setImageAlignment: (options: ImageAlignmentOptions) => ReturnType
|
||||
/**
|
||||
* Set image size
|
||||
*/
|
||||
setImageSize: (options: ImageSizeOptions) => ReturnType
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -52,7 +68,7 @@ function getType (type: string): 'image' | 'other' {
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const ImageRef = Node.create<ImageOptions>({
|
||||
export const ImageExtension = Node.create<ImageOptions>({
|
||||
name: 'image',
|
||||
|
||||
addOptions () {
|
||||
@ -71,6 +87,7 @@ export const ImageRef = Node.create<ImageOptions>({
|
||||
},
|
||||
|
||||
draggable: true,
|
||||
|
||||
selectable: true,
|
||||
|
||||
addAttributes () {
|
||||
@ -92,7 +109,8 @@ export const ImageRef = Node.create<ImageOptions>({
|
||||
},
|
||||
title: {
|
||||
default: null
|
||||
}
|
||||
},
|
||||
align: getDataAttribute('align')
|
||||
}
|
||||
},
|
||||
|
||||
@ -100,23 +118,33 @@ export const ImageRef = Node.create<ImageOptions>({
|
||||
return [
|
||||
{
|
||||
tag: `img[data-type="${this.name}"]`
|
||||
},
|
||||
{
|
||||
tag: 'img[src]'
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
renderHTML ({ node, HTMLAttributes }) {
|
||||
const merged = mergeAttributes(
|
||||
const divAttributes = {
|
||||
class: 'text-editor-image-container',
|
||||
'data-type': this.name,
|
||||
'data-align': node.attrs.align ?? 'center'
|
||||
}
|
||||
|
||||
const imgAttributes = mergeAttributes(
|
||||
{
|
||||
'data-type': this.name
|
||||
},
|
||||
this.options.HTMLAttributes,
|
||||
HTMLAttributes
|
||||
)
|
||||
const id = merged['file-id']
|
||||
|
||||
const id = imgAttributes['file-id']
|
||||
if (id != null) {
|
||||
merged.src = getFileUrl(id, 'full')
|
||||
imgAttributes.src = getFileUrl(id, 'full')
|
||||
let width: IconSize | undefined
|
||||
switch (merged.width) {
|
||||
switch (imgAttributes.width) {
|
||||
case '32px':
|
||||
width = 'small'
|
||||
break
|
||||
@ -132,13 +160,15 @@ export const ImageRef = Node.create<ImageOptions>({
|
||||
break
|
||||
}
|
||||
if (width !== undefined) {
|
||||
merged.src = getFileUrl(id, width)
|
||||
merged.srcset = getFileUrl(id, width) + ' 1x,' + getFileUrl(id, getIconSize2x(width)) + ' 2x'
|
||||
imgAttributes.src = getFileUrl(id, width)
|
||||
imgAttributes.srcset = getFileUrl(id, width) + ' 1x,' + getFileUrl(id, getIconSize2x(width)) + ' 2x'
|
||||
}
|
||||
merged.class = 'text-editor-image'
|
||||
imgAttributes.class = 'text-editor-image'
|
||||
imgAttributes.contentEditable = false
|
||||
this.options.reportNode?.(id, node)
|
||||
}
|
||||
return ['img', merged]
|
||||
|
||||
return ['div', divAttributes, ['img', imgAttributes]]
|
||||
},
|
||||
|
||||
addCommands () {
|
||||
@ -150,6 +180,26 @@ export const ImageRef = Node.create<ImageOptions>({
|
||||
type: this.name,
|
||||
attrs: options
|
||||
})
|
||||
},
|
||||
|
||||
setImageAlignment:
|
||||
(options) =>
|
||||
({ chain, tr }) => {
|
||||
const { from } = tr.selection
|
||||
return chain()
|
||||
.updateAttributes(this.name, { ...options })
|
||||
.setNodeSelection(from)
|
||||
.run()
|
||||
},
|
||||
|
||||
setImageSize:
|
||||
(options) =>
|
||||
({ chain, tr }) => {
|
||||
const { from } = tr.selection
|
||||
return chain()
|
||||
.updateAttributes(this.name, { ...options })
|
||||
.setNodeSelection(from)
|
||||
.run()
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -167,6 +217,7 @@ export const ImageRef = Node.create<ImageOptions>({
|
||||
})
|
||||
]
|
||||
},
|
||||
|
||||
addProseMirrorPlugins () {
|
||||
const opt = this.options
|
||||
function handleDrop (
|
||||
@ -246,7 +297,7 @@ export const ImageRef = Node.create<ImageOptions>({
|
||||
new Plugin({
|
||||
key: new PluginKey('handle-image-paste'),
|
||||
props: {
|
||||
handlePaste (view, event, slice) {
|
||||
handlePaste (view, event) {
|
||||
const dataTransfer = event.clipboardData
|
||||
if (dataTransfer !== null) {
|
||||
const res = handleDrop(view, { pos: view.state.selection.$from.pos, inside: 0 }, dataTransfer)
|
||||
@ -257,90 +308,13 @@ export const ImageRef = Node.create<ImageOptions>({
|
||||
return res
|
||||
}
|
||||
},
|
||||
handleDrop (view, event, slice) {
|
||||
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)
|
||||
}
|
||||
},
|
||||
handleClick: (view, pos, event) => {
|
||||
if (event.button !== 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
const node = event.target as unknown as HTMLElement
|
||||
if (node != null) {
|
||||
const fileId = (node as any).attributes['file-id']?.value
|
||||
if (fileId === undefined) {
|
||||
return false
|
||||
}
|
||||
const pos = view.posAtDOM(node, 0)
|
||||
|
||||
const actions: Action[] = [
|
||||
{
|
||||
label: plugin.string.Width,
|
||||
action: async (props, event) => {},
|
||||
component: Menu,
|
||||
props: {
|
||||
actions: [
|
||||
'32px',
|
||||
'64px',
|
||||
'128px',
|
||||
'256px',
|
||||
'512px',
|
||||
'25%',
|
||||
'50%',
|
||||
'75%',
|
||||
'100%',
|
||||
plugin.string.Unset
|
||||
].map((it) => {
|
||||
return {
|
||||
label: it === plugin.string.Unset ? it : getEmbeddedLabel(it),
|
||||
action: async () => {
|
||||
view.dispatch(
|
||||
view.state.tr.setNodeAttribute(pos, 'width', it === plugin.string.Unset ? null : it)
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
label: plugin.string.Height,
|
||||
action: async (props, event) => {},
|
||||
component: Menu,
|
||||
props: {
|
||||
actions: [
|
||||
'32px',
|
||||
'64px',
|
||||
'128px',
|
||||
'256px',
|
||||
'512px',
|
||||
'25%',
|
||||
'50%',
|
||||
'75%',
|
||||
'100%',
|
||||
plugin.string.Unset
|
||||
].map((it) => {
|
||||
return {
|
||||
label: it === plugin.string.Unset ? it : getEmbeddedLabel(it),
|
||||
action: async () => {
|
||||
view.dispatch(
|
||||
view.state.tr.setNodeAttribute(pos, 'height', it === plugin.string.Unset ? null : it)
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
]
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
showPopup(Menu, { actions }, getEventPositionElement(event))
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
})
|
@ -101,6 +101,10 @@ export const InlineStyleToolbarExtension = Extension.create<InlineStyleToolbarOp
|
||||
return false
|
||||
}
|
||||
|
||||
if (editor.isActive('image')) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
13
packages/text-editor/src/components/icons/AlignCenter.svelte
Normal file
13
packages/text-editor/src/components/icons/AlignCenter.svelte
Normal file
@ -0,0 +1,13 @@
|
||||
<script lang="ts">
|
||||
export let size: 'small' | 'medium' | 'large'
|
||||
const fill: string = 'currentColor'
|
||||
</script>
|
||||
|
||||
<svg class="svg-{size}" {fill} viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M15 5C15 4.44772 15.4477 4 16 4C16.5523 4 17 4.44772 17 5V27C17 27.5523 16.5523 28 16 28C15.4477 28 15 27.5523 15 27V24H7C6.44772 24 6 23.5523 6 23C6 22.4477 6.44772 22 7 22H15V17H9C8.44772 17 8 16.5523 8 16C8 15.4477 8.44772 15 9 15H15V10H5C4.44772 10 4 9.55228 4 9C4 8.44772 4.44772 8 5 8H15V5Z"
|
||||
/>
|
||||
<path d="M19 24H25C25.5523 24 26 23.5523 26 23C26 22.4477 25.5523 22 25 22H19V24Z" />
|
||||
<path d="M19 17H22C22.5523 17 23 16.5523 23 16C23 15.4477 22.5523 15 22 15H19V17Z" />
|
||||
<path d="M19 10H27C27.5523 10 28 9.55228 28 9C28 8.44772 27.5523 8 27 8H19V10Z" />
|
||||
</svg>
|
19
packages/text-editor/src/components/icons/AlignLeft.svelte
Normal file
19
packages/text-editor/src/components/icons/AlignLeft.svelte
Normal file
@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
export let size: 'small' | 'medium' | 'large'
|
||||
const fill: string = 'currentColor'
|
||||
</script>
|
||||
|
||||
<svg class="svg-{size}" {fill} viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M5 4C4.44772 4 4 4.44772 4 5V27C4 27.5523 4.44772 28 5 28C5.55228 28 6 27.5523 6 27V5C6 4.44772 5.55228 4 5 4Z"
|
||||
/>
|
||||
<path
|
||||
d="M27 8C27.5523 8 28 8.44772 28 9C28 9.55228 27.5523 10 27 10H11C10.4477 10 10 9.55228 10 9C10 8.44772 10.4477 8 11 8H27Z"
|
||||
/>
|
||||
<path
|
||||
d="M22 15C22.5523 15 23 15.4477 23 16C23 16.5523 22.5523 17 22 17H11C10.4477 17 10 16.5523 10 16C10 15.4477 10.4477 15 11 15H22Z"
|
||||
/>
|
||||
<path
|
||||
d="M28 23C28 22.4477 27.5523 22 27 22H11C10.4477 22 10 22.4477 10 23C10 23.5523 10.4477 24 11 24H27C27.5523 24 28 23.5523 28 23Z"
|
||||
/>
|
||||
</svg>
|
19
packages/text-editor/src/components/icons/AlignRight.svelte
Normal file
19
packages/text-editor/src/components/icons/AlignRight.svelte
Normal file
@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
export let size: 'small' | 'medium' | 'large'
|
||||
const fill: string = 'currentColor'
|
||||
</script>
|
||||
|
||||
<svg class="svg-{size}" {fill} viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M27 4C26.4477 4 26 4.44772 26 5V27C26 27.5523 26.4477 28 27 28C27.5523 28 28 27.5523 28 27V5C28 4.44772 27.5523 4 27 4Z"
|
||||
/>
|
||||
<path
|
||||
d="M5 8C4.44772 8 4 8.44772 4 9C4 9.55228 4.44772 10 5 10H21C21.5523 10 22 9.55228 22 9C22 8.44772 21.5523 8 21 8H5Z"
|
||||
/>
|
||||
<path
|
||||
d="M10 15C9.44772 15 9 15.4477 9 16C9 16.5523 9.44772 17 10 17H21C21.5523 17 22 16.5523 22 16C22 15.4477 21.5523 15 21 15H10Z"
|
||||
/>
|
||||
<path
|
||||
d="M4 23C4 22.4477 4.44772 22 5 22H21C21.5523 22 22 22.4477 22 23C22 23.5523 21.5523 24 21 24H5C4.44772 24 4 23.5523 4 23Z"
|
||||
/>
|
||||
</svg>
|
19
packages/text-editor/src/components/icons/ScaleOut.svelte
Normal file
19
packages/text-editor/src/components/icons/ScaleOut.svelte
Normal file
@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
export let size: 'small' | 'medium' | 'large'
|
||||
const fill: string = 'currentColor'
|
||||
</script>
|
||||
|
||||
<svg class="svg-{size}" {fill} viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M20 4V6H24.586L18.293 12.2929C17.9024 12.6834 17.9024 13.3166 18.293 13.7071C18.6835 14.0976 19.3167 14.0976 19.7072 13.7071L26 7.414V12H28V4H20Z"
|
||||
/>
|
||||
<path
|
||||
d="M13.7073 19.7071C14.0978 19.3166 14.0978 18.6834 13.7073 18.2929C13.3167 17.9024 12.6836 17.9024 12.293 18.2929L6 24.586V20H4V28H12V26H7.414L13.7073 19.7071Z"
|
||||
/>
|
||||
<path
|
||||
d="M12 4V6H7.414L13.707 12.2929C14.0976 12.6834 14.0976 13.3166 13.707 13.7071C13.3165 14.0976 12.6833 14.0976 12.2928 13.7071L6 7.414V12H4V4H12Z"
|
||||
/>
|
||||
<path
|
||||
d="M18.2927 19.7071C17.9022 19.3166 17.9022 18.6834 18.2927 18.2929C18.6833 17.9024 19.3164 17.9024 19.707 18.2929L26 24.586V20H28V28H20V26H24.586L18.2927 19.7071Z"
|
||||
/>
|
||||
</svg>
|
@ -55,5 +55,6 @@ export {
|
||||
type InlineStyleToolbarOptions,
|
||||
type InlineStyleToolbarStorage
|
||||
} from './components/extension/inlineStyleToolbar'
|
||||
export { ImageExtension, type ImageOptions } from './components/extension/imageExt'
|
||||
|
||||
export { textEditorId }
|
||||
|
@ -53,6 +53,13 @@ export default plugin(textEditorId, {
|
||||
NoFullDescription: '' as IntlString,
|
||||
EnableDiffMode: '' as IntlString,
|
||||
|
||||
AlignCenter: '' as IntlString,
|
||||
AlignLeft: '' as IntlString,
|
||||
AlignRight: '' as IntlString,
|
||||
ViewImage: '' as IntlString,
|
||||
ViewOriginal: '' as IntlString,
|
||||
MoreActions: '' as IntlString,
|
||||
|
||||
InsertTable: '' as IntlString,
|
||||
AddColumnBefore: '' as IntlString,
|
||||
AddColumnAfter: '' as IntlString,
|
||||
|
@ -14,6 +14,7 @@
|
||||
//
|
||||
|
||||
import { onStatelessParameters } from '@hocuspocus/provider'
|
||||
import { Attribute } from '@tiptap/core'
|
||||
import * as Y from 'yjs'
|
||||
|
||||
import { TiptapCollabProvider } from './provider'
|
||||
@ -76,3 +77,22 @@ export function copyDocumentContent (
|
||||
const provider = getProvider(documentId, providerData, initialContentId)
|
||||
provider.copyContent(documentId, snapshotId)
|
||||
}
|
||||
|
||||
export function getDataAttribute (name: string, def?: unknown | null): Partial<Attribute> {
|
||||
const dataName = `data-${name}`
|
||||
|
||||
return {
|
||||
default: def,
|
||||
parseHTML: (element) => element.getAttribute(dataName),
|
||||
renderHTML: (attributes) => {
|
||||
// eslint-disable-next-line
|
||||
if (!attributes[name]) {
|
||||
return {}
|
||||
}
|
||||
|
||||
return {
|
||||
[dataName]: attributes[name]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -73,6 +73,8 @@
|
||||
--highlight-red-hover: #ff967e;
|
||||
--highlight-red-press: #f96f50bd;
|
||||
|
||||
--text-editor-selected-node-color: #93CAF3;
|
||||
|
||||
--text-editor-highlighted-node-warning-active-background-color: #F2D7AE;
|
||||
--text-editor-highlighted-node-warning-background-color: #F8EBD7;
|
||||
--text-editor-highlighted-node-warning-border-color: #DE9B35;
|
||||
|
@ -169,11 +169,41 @@
|
||||
}
|
||||
}
|
||||
|
||||
.text-editor-image-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
&[data-align="center"] {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&[data-align="left"] {
|
||||
justify-content: left;
|
||||
}
|
||||
|
||||
&[data-align="right"] {
|
||||
justify-content: right;
|
||||
}
|
||||
}
|
||||
|
||||
.text-editor-image-container {
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.text-editor-image {
|
||||
cursor: pointer;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.ProseMirror-selectednode {
|
||||
img {
|
||||
box-shadow: 0 0 0 2px var(--text-editor-selected-node-color);
|
||||
border-radius: 0.125rem;
|
||||
}
|
||||
}
|
||||
|
||||
.text-editor-highlighted-node-warning {
|
||||
background-color: var(--text-editor-highlighted-node-warning-background-color);
|
||||
border-bottom: 0.0625rem solid var(--text-editor-highlighted-node-warning-border-color);
|
||||
|
Loading…
Reference in New Issue
Block a user