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