Skip to content

Commit

Permalink
UBER-187: Inline attachments
Browse files Browse the repository at this point in the history
* UBER-187: Support of inline attachments.
* UBER-250: Paste with Command + V
* Fix Create issue attachments duplication.
* Use same attachments preview for Edit issue

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
  • Loading branch information
haiodo committed May 26, 2023
1 parent 9d037b7 commit e34e9d9
Show file tree
Hide file tree
Showing 15 changed files with 276 additions and 29 deletions.
5 changes: 4 additions & 1 deletion packages/text-editor/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@
"CategoryColumn": "Columns",
"Table": "Table",
"InsertTable": "Insert table",
"TableOptions": "Customize table"
"TableOptions": "Customize table",
"Width": "Width",
"Height": "Height",
"Unset": "Unset"
}
}
5 changes: 4 additions & 1 deletion packages/text-editor/lang/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@
"CategoryColumn": "Колонки",
"Table": "Таблица",
"InsertTable": "Добавить таблицу",
"TableOptions": "Настроить таблицу"
"TableOptions": "Настроить таблицу",
"Width": "Щирина",
"Height": "Высота",
"Unset": "Убрать"
}
}
4 changes: 2 additions & 2 deletions packages/text-editor/src/components/StyledTextBox.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import { Completion } from '../Completion'
import textEditorPlugin from '../plugin'
import StyledTextEditor from './StyledTextEditor.svelte'
import { completionConfig } from './extensions'
import { completionConfig, imagePlugin } from './extensions'
export let label: IntlString | undefined = undefined
export let content: string
Expand Down Expand Up @@ -161,7 +161,7 @@
{enableFormatting}
{autofocus}
{isScrollable}
extensions={enableBackReferences ? [completionPlugin] : []}
extensions={enableBackReferences ? [completionPlugin, imagePlugin] : [imagePlugin]}
bind:content={rawValue}
bind:this={textEditor}
on:attach
Expand Down
6 changes: 6 additions & 0 deletions packages/text-editor/src/components/extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import Link from '@tiptap/extension-link'
import { CompletionOptions } from '../Completion'
import MentionList from './MentionList.svelte'
import { SvelteRenderer } from './SvelteRenderer'
import { ImageRef } from './imageExt'

export const tableExtensions = [
Table.configure({
Expand Down Expand Up @@ -166,3 +167,8 @@ export const completionConfig: Partial<CompletionOptions> = {
}
}
}

/**
* @public
*/
export const imagePlugin = ImageRef.configure({ inline: false, HTMLAttributes: {} })
225 changes: 225 additions & 0 deletions packages/text-editor/src/components/imageExt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
import { getEmbeddedLabel } from '@hcengineering/platform'
import { getFileUrl } from '@hcengineering/presentation'
import { Action, Menu, getEventPositionElement, showPopup } from '@hcengineering/ui'
import { Node, createNodeFromContent, mergeAttributes, nodeInputRule } from '@tiptap/core'
import { Plugin, PluginKey } from 'prosemirror-state'
import plugin from '../plugin'

export interface ImageOptions {
inline: boolean
HTMLAttributes: Record<string, any>

showPreview?: (event: MouseEvent, fileId: string) => void
}

declare module '@tiptap/core' {
interface Commands<ReturnType> {
image: {
/**
* Add an image
*/
setImage: (options: { src: string, alt?: string, title?: string }) => ReturnType
}
}
}

export const inputRegex = /(?:^|\s)(!\[(.+|:?)]\((\S+)(?:(?:\s+)["'](\S+)["'])?\))$/

export const ImageRef = Node.create<ImageOptions>({
name: 'image',

addOptions () {
return {
inline: false,
HTMLAttributes: {}
}
},

inline () {
return this.options.inline
},

group () {
return this.options.inline ? 'inline' : 'block'
},

draggable: true,
selectable: true,

addAttributes () {
return {
fileid: {
default: null,
parseHTML: (element) => element.getAttribute('file-id'),
renderHTML: (attributes) => {
// eslint-disable-next-line
if (!attributes.fileid) {
return {}
}

return {
'file-id': attributes.fileid
}
}
},
width: {
default: null
},
height: {
default: null
}
}
},

parseHTML () {
return [
{
tag: `img[data-type="${this.name}"]`
}
]
},

renderHTML ({ HTMLAttributes }) {
const merged = mergeAttributes(
{
'data-type': this.name
},
this.options.HTMLAttributes,
HTMLAttributes
)
merged.src = getFileUrl(merged['file-id'], 'full')
merged.class = 'textEditorImage'
return ['img', merged]
},

addCommands () {
return {
setImage:
(options) =>
({ commands }) => {
return commands.insertContent({
type: this.name,
attrs: options
})
}
}
},

addInputRules () {
return [
nodeInputRule({
find: inputRegex,
type: this.type,
getAttributes: (match) => {
const [, , alt, src, title] = match

return { src, alt, title }
}
})
]
},
addProseMirrorPlugins () {
return [
new Plugin({
key: new PluginKey('handle-image-paste'),
props: {
handleDrop (view, event, slice) {
const uris = (event.dataTransfer?.getData('text/uri-list') ?? '')
.split('\r\n')
.filter((it) => !it.startsWith('#'))
let result = false
for (const uri of uris) {
if (uri !== '') {
const pos = view.posAtCoords({ left: event.x, top: event.y })

const url = new URL(uri)
if (url.hostname !== location.hostname) {
return
}

const _file = (url.searchParams.get('file') ?? '').split('/').join('')

if (_file.trim().length === 0) {
return
}
const content = createNodeFromContent(
`<img data-type='image' width='25%' file-id='${_file}'></img>`,
view.state.schema,
{
parseOptions: {
preserveWhitespace: 'full'
}
}
)
event.preventDefault()
event.stopPropagation()
view.dispatch(view.state.tr.insert(pos?.pos ?? 0, content))
result = true
}
}
return result
},
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%', '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%', '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
}
}
})
]
}
})
5 changes: 4 additions & 1 deletion packages/text-editor/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ export default plugin(textEditorId, {
CategoryRow: '' as IntlString,
CategoryColumn: '' as IntlString,
Table: '' as IntlString,
TableOptions: '' as IntlString
TableOptions: '' as IntlString,
Width: '' as IntlString,
Height: '' as IntlString,
Unset: '' as IntlString
}
})
4 changes: 4 additions & 0 deletions packages/theme/styles/_text-editor.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.textEditorImage {
cursor: pointer;
object-fit: contain;
}
1 change: 1 addition & 0 deletions packages/theme/styles/global.scss
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
@import "./panel.scss";
@import "./prose.scss";
@import "./button.scss";
@import "./_text-editor.scss";

@font-face {
font-family: 'IBM Plex Sans';
Expand Down
27 changes: 15 additions & 12 deletions packages/ui/src/components/DropdownLabelsPopup.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
export let items: DropdownTextItem[]
export let selected: DropdownTextItem['id'] | DropdownTextItem['id'][] | undefined = undefined
export let multiselect: boolean = false
export let enableSearch = true
let search: string = ''
const dispatch = createEventDispatcher()
Expand Down Expand Up @@ -91,18 +92,20 @@
dispatch('changeContent')
}}
>
<div class="header">
<EditWithIcon
icon={IconSearch}
size={'large'}
width={'100%'}
focus={!$deviceOptionsStore.isMobile}
bind:value={search}
{placeholder}
{placeholderParam}
on:change
/>
</div>
{#if enableSearch}
<div class="header">
<EditWithIcon
icon={IconSearch}
size={'large'}
width={'100%'}
focus={!$deviceOptionsStore.isMobile}
bind:value={search}
{placeholder}
{placeholderParam}
on:change
/>
</div>
{/if}
<div class="scroll">
<div class="box">
<ListView bind:this={list} count={objects.length} bind:selection>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@
bind:this={attachments[i]}
alwaysEdit
showButtons
fakeAttach={withoutAttach ? 'hidden' : i < items.length - 1 ? 'fake' : 'normal'}
enableAttachments={!withoutAttach}
bind:content={item.content}
placeholder={textEditorPlugin.string.EditorPlaceholder}
{objectId}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@
class="flex-center icon"
class:svg={value.type === 'image/svg+xml'}
class:image={isImage(value.type)}
style:background-image={isImage(value.type) ? `url(${getFileUrl(value.file)})` : 'none'}
style:background-image={isImage(value.type) ? `url(${getFileUrl(value.file, 'large')})` : 'none'}
>
{#if !isImage(value.type)}{iconLabel(value.name)}{/if}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
dispatch('open', popupInfo.id)
}}
>
<img src={getFileUrl(value.file)} alt={value.name} />
<img src={getFileUrl(value.file, 'large')} alt={value.name} />
<div class="actions conner">
<AttachmentActions attachment={value} {isSaved} />
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@
{focusIndex}
enableBackReferences={true}
bind:this={descriptionBox}
useAttachmentPreview={true}
useAttachmentPreview={false}
objectId={object._id}
_class={object._class}
space={object.space}
Expand Down
Loading

0 comments on commit e34e9d9

Please sign in to comment.