mirror of
https://github.com/hcengineering/platform.git
synced 2025-04-23 08:48:01 +00:00
UBER-1118 Wiki images improvements (#3887)
Signed-off-by: Alexander Onnikov <alexander.onnikov@xored.com>
This commit is contained in:
parent
25086b8348
commit
f64901cfd5
@ -15,7 +15,7 @@
|
||||
<script lang="ts">
|
||||
import { Doc } from '@hcengineering/core'
|
||||
import { Button, Dialog, PopupOptions } from '@hcengineering/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { createEventDispatcher, onMount } from 'svelte'
|
||||
import presentation from '..'
|
||||
import { getFileUrl } from '../utils'
|
||||
import Download from './icons/Download.svelte'
|
||||
@ -27,6 +27,7 @@
|
||||
export let popupOptions: PopupOptions
|
||||
export let value: Doc
|
||||
export let showIcon = true
|
||||
export let fullSize = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
// let imgView: 'img-horizontal-fit' | 'img-vertical-fit' | 'img-original-fit' = 'img-vertical-fit'
|
||||
@ -36,9 +37,11 @@
|
||||
const ext = parts[parts.length - 1]
|
||||
return ext.substring(0, 4).toUpperCase()
|
||||
}
|
||||
// onMount(() => {
|
||||
// dispatch('fullsize')
|
||||
// })
|
||||
onMount(() => {
|
||||
if (fullSize) {
|
||||
dispatch('fullsize')
|
||||
}
|
||||
})
|
||||
let download: HTMLAnchorElement
|
||||
</script>
|
||||
|
||||
|
@ -240,7 +240,7 @@
|
||||
$: updateEditor(editor, field, comparedVersion)
|
||||
$: if (editor) dispatch('editor', editor)
|
||||
|
||||
const tippyOptions = {
|
||||
$: tippyOptions = {
|
||||
zIndex: 100000,
|
||||
popperOptions: {
|
||||
modifiers: [
|
||||
@ -287,7 +287,10 @@
|
||||
InlinePopupExtension.configure({
|
||||
pluginKey: 'show-image-actions-popup',
|
||||
element: imageToolbarElement,
|
||||
tippyOptions,
|
||||
tippyOptions: {
|
||||
...tippyOptions,
|
||||
appendTo: () => boundary ?? element
|
||||
},
|
||||
shouldShow: () => {
|
||||
if (!visible && !readonly) {
|
||||
return false
|
||||
|
@ -39,16 +39,22 @@
|
||||
|
||||
function openImage () {
|
||||
const attributes = textEditor.getAttributes('image')
|
||||
const fileId = attributes['file-id']
|
||||
const fileId = attributes['file-id'] ?? attributes.src
|
||||
const fileName = attributes.alt ?? ''
|
||||
showPopup(PDFViewer, { file: fileId, name: fileName, contentType: 'image/*', showIcon: false }, 'centered', () => {
|
||||
dispatch('focus')
|
||||
})
|
||||
showPopup(
|
||||
PDFViewer,
|
||||
{ file: fileId, name: fileName, contentType: 'image/*', fullSize: true, showIcon: false },
|
||||
'centered',
|
||||
() => {
|
||||
dispatch('focus')
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function openOriginalImage () {
|
||||
const attributes = textEditor.getAttributes('image')
|
||||
const url = getFileUrl(attributes['file-id'], 'full')
|
||||
const fileId = attributes['file-id'] ?? attributes.src
|
||||
const url = getFileUrl(fileId, 'full')
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
|
||||
@ -64,21 +70,7 @@
|
||||
}
|
||||
})
|
||||
|
||||
const actions = [
|
||||
{
|
||||
id: '#imageOpen',
|
||||
icon: IconScaleOut,
|
||||
label: plugin.string.ViewImage,
|
||||
action: openImage
|
||||
},
|
||||
{
|
||||
id: '#imageOriginal',
|
||||
icon: IconExpand,
|
||||
label: plugin.string.ViewOriginal,
|
||||
action: openOriginalImage
|
||||
},
|
||||
...widthActions
|
||||
]
|
||||
const actions = [...widthActions]
|
||||
|
||||
showPopup(
|
||||
SelectPopup,
|
||||
@ -123,6 +115,19 @@
|
||||
on:click={getImageAlignmentToggler('right')}
|
||||
/>
|
||||
<div class="buttons-divider" />
|
||||
<StyleButton
|
||||
icon={IconScaleOut}
|
||||
size={formatButtonSize}
|
||||
on:click={openImage}
|
||||
showTooltip={{ label: plugin.string.ViewImage }}
|
||||
/>
|
||||
<StyleButton
|
||||
icon={IconExpand}
|
||||
size={formatButtonSize}
|
||||
on:click={openOriginalImage}
|
||||
showTooltip={{ label: plugin.string.ViewOriginal }}
|
||||
/>
|
||||
<div class="buttons-divider" />
|
||||
<StyleButton
|
||||
icon={IconMoreH}
|
||||
size={formatButtonSize}
|
||||
|
@ -83,7 +83,7 @@
|
||||
let textToolbarElement: HTMLElement
|
||||
let imageToolbarElement: HTMLElement
|
||||
|
||||
const tippyOptions = {
|
||||
$: tippyOptions = {
|
||||
zIndex: 100000,
|
||||
popperOptions: {
|
||||
modifiers: [
|
||||
@ -166,7 +166,10 @@
|
||||
InlinePopupExtension.configure({
|
||||
pluginKey: 'show-image-actions-popup',
|
||||
element: imageToolbarElement,
|
||||
tippyOptions,
|
||||
tippyOptions: {
|
||||
...tippyOptions,
|
||||
appendTo: () => boundary ?? element
|
||||
},
|
||||
shouldShow: () => editor?.isActive('image')
|
||||
})
|
||||
],
|
||||
|
@ -1,8 +1,22 @@
|
||||
//
|
||||
// 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 { getMetadata } from '@hcengineering/platform'
|
||||
import presentation, { getFileUrl } from '@hcengineering/presentation'
|
||||
import { IconSize, getIconSize2x } from '@hcengineering/ui'
|
||||
import { Node, createNodeFromContent, mergeAttributes, nodeInputRule } from '@tiptap/core'
|
||||
import { Fragment, Node as ProseMirrorNode } from '@tiptap/pm/model'
|
||||
import presentation, { PDFViewer, getFileUrl } from '@hcengineering/presentation'
|
||||
import { IconSize, getIconSize2x, showPopup } from '@hcengineering/ui'
|
||||
import { Node, mergeAttributes, nodeInputRule } from '@tiptap/core'
|
||||
import { Node as ProseMirrorNode } from '@tiptap/pm/model'
|
||||
import { Plugin, PluginKey } from '@tiptap/pm/state'
|
||||
import { EditorView } from '@tiptap/pm/view'
|
||||
import { getDataAttribute } from '../../utils'
|
||||
@ -12,6 +26,11 @@ import { getDataAttribute } from '../../utils'
|
||||
*/
|
||||
export type FileAttachFunction = (file: File) => Promise<{ file: string, type: string } | undefined>
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type ImageAlignment = 'center' | 'left' | 'right'
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@ -25,7 +44,7 @@ export interface ImageOptions {
|
||||
}
|
||||
|
||||
export interface ImageAlignmentOptions {
|
||||
align?: 'center' | 'left' | 'right'
|
||||
align?: ImageAlignment
|
||||
}
|
||||
|
||||
export interface ImageSizeOptions {
|
||||
@ -129,7 +148,7 @@ export const ImageExtension = Node.create<ImageOptions>({
|
||||
const divAttributes = {
|
||||
class: 'text-editor-image-container',
|
||||
'data-type': this.name,
|
||||
'data-align': node.attrs.align ?? 'center'
|
||||
'data-align': node.attrs.align
|
||||
}
|
||||
|
||||
const imgAttributes = mergeAttributes(
|
||||
@ -244,20 +263,10 @@ export const ImageExtension = Node.create<ImageOptions>({
|
||||
const ctype = dataTransfer.getData('application/contentType')
|
||||
const type = getType(ctype ?? 'other')
|
||||
|
||||
let content: ProseMirrorNode | Fragment | undefined
|
||||
if (type === 'image') {
|
||||
content = createNodeFromContent(
|
||||
`<img data-type='image' width='75%' file-id='${_file}'></img>`,
|
||||
view.state.schema,
|
||||
{
|
||||
parseOptions: {
|
||||
preserveWhitespace: 'full'
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
if (content !== undefined) {
|
||||
view.dispatch(view.state.tr.insert(pos?.pos ?? 0, content))
|
||||
const node = view.state.schema.nodes.image.create({ 'file-id': _file })
|
||||
const transaction = view.state.tr.insert(pos?.pos ?? 0, node)
|
||||
view.dispatch(transaction)
|
||||
result = true
|
||||
}
|
||||
}
|
||||
@ -275,16 +284,9 @@ export const ImageExtension = Node.create<ImageOptions>({
|
||||
void opt.attachFile(file).then((id) => {
|
||||
if (id !== undefined) {
|
||||
if (id.type.includes('image')) {
|
||||
const content = createNodeFromContent(
|
||||
`<img data-type='image' width='75%' file-id='${id.file}'></img>`,
|
||||
view.state.schema,
|
||||
{
|
||||
parseOptions: {
|
||||
preserveWhitespace: 'full'
|
||||
}
|
||||
}
|
||||
)
|
||||
view.dispatch(view.state.tr.insert(pos?.pos ?? 0, content))
|
||||
const node = view.state.schema.nodes.image.create({ 'file-id': id.file })
|
||||
const transaction = view.state.tr.insert(pos?.pos ?? 0, node)
|
||||
view.dispatch(transaction)
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -315,6 +317,26 @@ export const ImageExtension = Node.create<ImageOptions>({
|
||||
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 !== 'image') {
|
||||
return
|
||||
}
|
||||
|
||||
const fileId = node.attrs['file-id'] ?? node.attrs.src
|
||||
const fileName = node.attrs.alt ?? ''
|
||||
|
||||
showPopup(
|
||||
PDFViewer,
|
||||
{
|
||||
file: fileId,
|
||||
name: fileName,
|
||||
contentType: 'image/*',
|
||||
fullSize: true,
|
||||
showIcon: false
|
||||
},
|
||||
'centered'
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Editor, Extension, isTextSelection } from '@tiptap/core'
|
||||
import { Extension, isTextSelection } from '@tiptap/core'
|
||||
import { BubbleMenuOptions } from '@tiptap/extension-bubble-menu'
|
||||
import { Plugin, PluginKey } from 'prosemirror-state'
|
||||
import { InlinePopupExtension } from './inlinePopup'
|
||||
@ -9,39 +9,23 @@ export type InlineStyleToolbarOptions = BubbleMenuOptions & {
|
||||
}
|
||||
|
||||
export interface InlineStyleToolbarStorage {
|
||||
isShown: boolean
|
||||
}
|
||||
|
||||
const handleFocus = (editor: Editor, options: InlineStyleToolbarOptions, storage: InlineStyleToolbarStorage): void => {
|
||||
if (!options.isSupported()) {
|
||||
return
|
||||
}
|
||||
|
||||
if (editor.isEmpty) {
|
||||
return
|
||||
}
|
||||
|
||||
if (options.isSelectionOnly?.() === true && editor.view.state.selection.empty) {
|
||||
return
|
||||
}
|
||||
|
||||
storage.isShown = true
|
||||
canShowWithoutSelection: boolean
|
||||
}
|
||||
|
||||
export const InlineStyleToolbarExtension = Extension.create<InlineStyleToolbarOptions, InlineStyleToolbarStorage>({
|
||||
pluginKey: new PluginKey('inline-style-toolbar'),
|
||||
addProseMirrorPlugins () {
|
||||
const options = this.options
|
||||
const storage = this.storage
|
||||
const editor = this.editor
|
||||
|
||||
const plugins = [
|
||||
...(this.parent?.() ?? []),
|
||||
new Plugin({
|
||||
key: new PluginKey('inline-style-toolbar-click-plugin'),
|
||||
props: {
|
||||
handleClick () {
|
||||
handleFocus(editor, options, storage)
|
||||
handleClickOn (view, pos, node, nodePos, event, direct) {
|
||||
if (direct) {
|
||||
storage.canShowWithoutSelection = node.type.name !== 'image'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -51,7 +35,7 @@ export const InlineStyleToolbarExtension = Extension.create<InlineStyleToolbarOp
|
||||
},
|
||||
addStorage () {
|
||||
return {
|
||||
isShown: false
|
||||
canShowWithoutSelection: false
|
||||
}
|
||||
},
|
||||
addExtensions () {
|
||||
@ -69,10 +53,6 @@ export const InlineStyleToolbarExtension = Extension.create<InlineStyleToolbarOp
|
||||
return false
|
||||
}
|
||||
|
||||
if (this.storage.isShown) {
|
||||
return true
|
||||
}
|
||||
|
||||
// For some reason shouldShow might be called after dismount and
|
||||
// after destroing the editor. We should handle this just no to have
|
||||
// any errors in runtime
|
||||
@ -93,30 +73,25 @@ export const InlineStyleToolbarExtension = Extension.create<InlineStyleToolbarOp
|
||||
const { doc, selection } = state
|
||||
const { empty } = selection
|
||||
|
||||
const textSelection = isTextSelection(state.selection)
|
||||
|
||||
// Sometime check for `empty` is not enough.
|
||||
// Doubleclick an empty paragraph returns a node size of 2.
|
||||
// So we check also for an empty text size.
|
||||
const isEmptyTextBlock = doc.textBetween(from, to).length === 0 && isTextSelection(state.selection)
|
||||
const isEmptyTextBlock = doc.textBetween(from, to).length === 0 && textSelection
|
||||
if (empty || isEmptyTextBlock) {
|
||||
return false
|
||||
return this.storage.canShowWithoutSelection
|
||||
}
|
||||
|
||||
if (editor.isActive('image')) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
return textSelection
|
||||
}
|
||||
})
|
||||
]
|
||||
},
|
||||
onFocus () {
|
||||
handleFocus(this.editor, this.options, this.storage)
|
||||
},
|
||||
onSelectionUpdate () {
|
||||
this.storage.isShown = false
|
||||
this.storage.canShowWithoutSelection = false
|
||||
},
|
||||
onUpdate () {
|
||||
this.storage.isShown = false
|
||||
this.storage.canShowWithoutSelection = false
|
||||
}
|
||||
})
|
||||
|
Loading…
Reference in New Issue
Block a user