UBER-1118 Wiki images improvements (#3887)

Signed-off-by: Alexander Onnikov <alexander.onnikov@xored.com>
This commit is contained in:
Alexander Onnikov 2023-10-25 23:13:43 +07:00 committed by GitHub
parent 25086b8348
commit f64901cfd5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 107 additions and 96 deletions

View File

@ -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>

View File

@ -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

View File

@ -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}

View File

@ -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')
})
],

View File

@ -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'
)
}
}
})

View File

@ -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
}
})