mirror of
https://github.com/hcengineering/platform.git
synced 2024-12-23 03:22:19 +03:00
UBERF-5964: Insert items menu in editor (#5341)
Signed-off-by: Alexey Zinoviev <alexey.zinoviev@xored.com>
This commit is contained in:
parent
b34b70feeb
commit
0e15e78e44
@ -47,6 +47,8 @@
|
||||
"TableOptions": "Customize table",
|
||||
"Width": "Width",
|
||||
"Height": "Height",
|
||||
"Unset": "Unset"
|
||||
"Unset": "Unset",
|
||||
"Image": "Image",
|
||||
"SeparatorLine": "Separator line"
|
||||
}
|
||||
}
|
||||
|
@ -45,6 +45,8 @@
|
||||
"TableOptions": "Personalizar tabla",
|
||||
"Width": "Ancho",
|
||||
"Height": "Alto",
|
||||
"Unset": "Anular"
|
||||
"Unset": "Anular",
|
||||
"Image": "Imagen",
|
||||
"SeparatorLine": "Línea de separación"
|
||||
}
|
||||
}
|
@ -45,6 +45,8 @@
|
||||
"TableOptions": "Personalizar tabela",
|
||||
"Width": "Largura",
|
||||
"Height": "Altura",
|
||||
"Unset": "Anular"
|
||||
"Unset": "Anular",
|
||||
"Image": "Imagem",
|
||||
"SeparatorLine": "linha separadora"
|
||||
}
|
||||
}
|
@ -47,6 +47,8 @@
|
||||
"TableOptions": "Настроить таблицу",
|
||||
"Width": "Ширина",
|
||||
"Height": "Высота",
|
||||
"Unset": "Убрать"
|
||||
"Unset": "Убрать",
|
||||
"Image": "Изображение",
|
||||
"SeparatorLine": "Разделительная линия"
|
||||
}
|
||||
}
|
||||
|
@ -119,6 +119,7 @@
|
||||
{readonly}
|
||||
field={key.key}
|
||||
canEmbedFiles={false}
|
||||
withSideMenu={false}
|
||||
on:focus
|
||||
on:blur
|
||||
on:update
|
||||
|
@ -18,9 +18,20 @@
|
||||
import { type Class, type CollaborativeDoc, type Doc, type Ref } from '@hcengineering/core'
|
||||
import { type DocumentId, type PlatformDocumentId } from '@hcengineering/collaborator-client'
|
||||
import { IntlString, getMetadata, translate } from '@hcengineering/platform'
|
||||
import presentation from '@hcengineering/presentation'
|
||||
import { markupToJSON } from '@hcengineering/text'
|
||||
import { AnySvelteComponent, Button, IconSize, Loading, ThrottledCaller, themeStore } from '@hcengineering/ui'
|
||||
import presentation, { getFileUrl, getImageSize } from '@hcengineering/presentation'
|
||||
import view from '@hcengineering/view'
|
||||
import {
|
||||
AnySvelteComponent,
|
||||
Button,
|
||||
IconSize,
|
||||
Loading,
|
||||
PopupAlignment,
|
||||
getEventPositionElement,
|
||||
getPopupPositionElement,
|
||||
ThrottledCaller,
|
||||
themeStore
|
||||
} from '@hcengineering/ui'
|
||||
import { AnyExtension, Editor, FocusPosition, mergeAttributes } from '@tiptap/core'
|
||||
import Collaboration, { isChangeOrigin } from '@tiptap/extension-collaboration'
|
||||
import CollaborationCursor from '@tiptap/extension-collaboration-cursor'
|
||||
@ -45,6 +56,7 @@
|
||||
TextFormatCategory,
|
||||
TextNodeAction
|
||||
} from '../types'
|
||||
import { addTableHandler } from '../utils'
|
||||
|
||||
import CollaborationUsers from './CollaborationUsers.svelte'
|
||||
import ImageStyleToolbar from './ImageStyleToolbar.svelte'
|
||||
@ -55,9 +67,11 @@
|
||||
import { ImageExtension } from './extension/imageExt'
|
||||
import { type FileAttachFunction } from './extension/types'
|
||||
import { FileExtension } from './extension/fileExt'
|
||||
import { LeftMenuExtension } from './extension/leftMenu'
|
||||
import { InlineCommandsExtension } from './extension/inlineCommands'
|
||||
import { InlinePopupExtension } from './extension/inlinePopup'
|
||||
import { InlineStyleToolbarExtension } from './extension/inlineStyleToolbar'
|
||||
import { completionConfig } from './extensions'
|
||||
import { completionConfig, inlineCommandsConfig } from './extensions'
|
||||
|
||||
export let collaborativeDoc: CollaborativeDoc
|
||||
export let initialCollaborativeDoc: CollaborativeDoc | undefined = undefined
|
||||
@ -98,6 +112,8 @@
|
||||
export let canShowPopups = true
|
||||
export let canEmbedFiles = true
|
||||
export let canEmbedImages = true
|
||||
export let withSideMenu = true
|
||||
export let withInlineCommands = true
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@ -166,13 +182,33 @@
|
||||
insertTemplate: (name, markup) => {
|
||||
editor?.commands.insertContent(markupToJSON(markup))
|
||||
},
|
||||
insertTable: (options: { rows?: number, cols?: number, withHeaderRow?: boolean }) => {
|
||||
editor?.commands.insertTable(options)
|
||||
},
|
||||
insertCodeBlock: () => {
|
||||
editor?.commands.insertContent(
|
||||
{
|
||||
type: 'codeBlock',
|
||||
content: [{ type: 'text', text: ' ' }]
|
||||
},
|
||||
{
|
||||
updateSelection: false
|
||||
}
|
||||
)
|
||||
},
|
||||
insertContent: (content) => {
|
||||
editor?.commands.insertContent(content)
|
||||
},
|
||||
insertSeparatorLine: () => {
|
||||
editor?.commands.setHorizontalRule()
|
||||
},
|
||||
focus: () => {
|
||||
focus()
|
||||
}
|
||||
}
|
||||
|
||||
function handleAction (a: RefAction, evt?: Event): void {
|
||||
a.action(evt?.target as HTMLElement, editorHandler)
|
||||
function handleAction (a: RefAction, evt?: MouseEvent): void {
|
||||
a.action(evt?.target as HTMLElement, editorHandler, evt)
|
||||
}
|
||||
|
||||
$: commandHandler = textEditorCommandHandler(editor)
|
||||
@ -258,6 +294,123 @@
|
||||
})
|
||||
)
|
||||
}
|
||||
if (withSideMenu) {
|
||||
optionalExtensions.push(
|
||||
LeftMenuExtension.configure({
|
||||
width: 20,
|
||||
height: 20,
|
||||
marginX: 8,
|
||||
className: 'tiptap-left-menu',
|
||||
icon: view.icon.Add,
|
||||
iconProps: {
|
||||
className: 'svg-tiny',
|
||||
fill: 'currentColor'
|
||||
},
|
||||
items: [
|
||||
...(canEmbedImages ? [{ id: 'image', label: textEditorPlugin.string.Image, icon: view.icon.Image }] : []),
|
||||
{ id: 'table', label: textEditorPlugin.string.Table, icon: view.icon.Table2 },
|
||||
{ id: 'code-block', label: textEditorPlugin.string.CodeBlock, icon: view.icon.CodeBlock },
|
||||
{ id: 'separator-line', label: textEditorPlugin.string.SeparatorLine, icon: view.icon.SeparatorLine }
|
||||
],
|
||||
handleSelect: handleLeftMenuClick
|
||||
})
|
||||
)
|
||||
}
|
||||
if (withInlineCommands) {
|
||||
optionalExtensions.push(
|
||||
InlineCommandsExtension.configure(
|
||||
inlineCommandsConfig(handleLeftMenuClick, attachFile === undefined || !canEmbedImages ? ['image'] : [])
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
let inputImage: HTMLInputElement
|
||||
|
||||
export function handleAttachImage (): void {
|
||||
inputImage.click()
|
||||
}
|
||||
|
||||
async function createInlineImage (file: File): Promise<void> {
|
||||
if (!file.type.startsWith('image/') || attachFile === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const attached = await attachFile(file)
|
||||
if (attached === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const size = await getImageSize(
|
||||
file,
|
||||
getFileUrl(attached.file, 'full', getMetadata(presentation.metadata.UploadURL))
|
||||
)
|
||||
|
||||
editor.commands.insertContent(
|
||||
{
|
||||
type: 'image',
|
||||
attrs: {
|
||||
'file-id': attached.file,
|
||||
width: Math.round(size.width / size.pixelRatio)
|
||||
}
|
||||
},
|
||||
{
|
||||
updateSelection: false
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async function fileSelected (): Promise<void> {
|
||||
if (readonly) return
|
||||
const list = inputImage.files
|
||||
if (list === null || list.length === 0) return
|
||||
for (let index = 0; index < list.length; index++) {
|
||||
const file = list.item(index)
|
||||
if (file !== null) {
|
||||
await createInlineImage(file)
|
||||
}
|
||||
}
|
||||
inputImage.value = ''
|
||||
}
|
||||
|
||||
async function handleLeftMenuClick (id: string, pos: number, targetItem?: MouseEvent | HTMLElement): Promise<void> {
|
||||
editor.commands.focus(pos, { scrollIntoView: false })
|
||||
|
||||
switch (id) {
|
||||
case 'image':
|
||||
handleAttachImage()
|
||||
break
|
||||
case 'table': {
|
||||
let position: PopupAlignment | undefined = undefined
|
||||
if (targetItem !== undefined) {
|
||||
position =
|
||||
targetItem instanceof MouseEvent ? getEventPositionElement(targetItem) : getPopupPositionElement(targetItem)
|
||||
}
|
||||
|
||||
// We need to trigger it asynchronously in order for the editor to finish its focus event
|
||||
// Otherwise, it hoggs the focus from the popup and keyboard navigation doesn't work
|
||||
setTimeout(() => {
|
||||
addTableHandler(editor.commands.insertTable, position)
|
||||
})
|
||||
break
|
||||
}
|
||||
case 'code-block':
|
||||
// For some reason .setCodeBlock doesnt work in our case
|
||||
editor.commands.insertContent(
|
||||
{
|
||||
type: 'codeBlock',
|
||||
content: [{ type: 'text', text: ' ' }]
|
||||
},
|
||||
{
|
||||
updateSelection: false
|
||||
}
|
||||
)
|
||||
editor.commands.focus(pos, { scrollIntoView: false })
|
||||
break
|
||||
case 'separator-line':
|
||||
editor.commands.setHorizontalRule()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const throttle = new ThrottledCaller(100)
|
||||
@ -353,6 +506,16 @@
|
||||
})
|
||||
</script>
|
||||
|
||||
<input
|
||||
bind:this={inputImage}
|
||||
multiple
|
||||
type="file"
|
||||
name="file"
|
||||
id="imageInput"
|
||||
accept="image/*"
|
||||
style="display: none"
|
||||
on:change={fileSelected}
|
||||
/>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
@ -445,6 +608,25 @@
|
||||
align-items: flex-start;
|
||||
min-height: 1.25rem;
|
||||
background-color: transparent;
|
||||
|
||||
:global(.tiptap-left-menu) {
|
||||
color: var(--theme-trans-color);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 20%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--theme-button-hovered);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: var(--theme-button-pressed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.hidden {
|
||||
|
137
packages/text-editor/src/components/InlineCommandsList.svelte
Normal file
137
packages/text-editor/src/components/InlineCommandsList.svelte
Normal file
@ -0,0 +1,137 @@
|
||||
<!--
|
||||
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
||||
// Copyright © 2021 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 {
|
||||
showPopup,
|
||||
resizeObserver,
|
||||
deviceOptionsStore as deviceInfo,
|
||||
PopupResult,
|
||||
SelectPopup
|
||||
} from '@hcengineering/ui'
|
||||
import { onDestroy, onMount } from 'svelte'
|
||||
import DummyPopup from './DummyPopup.svelte'
|
||||
|
||||
export let items: any[]
|
||||
export let clientRect: () => ClientRect
|
||||
export let command: (props: any) => void
|
||||
export let close: () => void
|
||||
|
||||
let popup: HTMLDivElement
|
||||
let dummyPopup: PopupResult
|
||||
let menuPopup: SelectPopup
|
||||
|
||||
onMount(() => {
|
||||
dummyPopup = showPopup(
|
||||
DummyPopup,
|
||||
{},
|
||||
undefined,
|
||||
() => {
|
||||
close()
|
||||
command(null)
|
||||
},
|
||||
() => {},
|
||||
{ overlay: false, category: '' }
|
||||
)
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
dummyPopup.close()
|
||||
})
|
||||
|
||||
export function onKeyDown (ev: KeyboardEvent): boolean {
|
||||
return menuPopup?.onKeydown(ev)
|
||||
}
|
||||
|
||||
export function done (): void {}
|
||||
|
||||
function updateStyle (): void {
|
||||
const rect = clientRect()
|
||||
const wDoc = $deviceInfo.docWidth
|
||||
const hDoc = $deviceInfo.docHeight
|
||||
let tempStyle = ''
|
||||
if (rect.top < hDoc - rect.bottom) {
|
||||
// 20rem - 1.75rem
|
||||
const maxH: number = hDoc - rect.bottom - 40 >= 480 ? 480 : hDoc - rect.bottom - 40
|
||||
tempStyle = `top: calc(${rect.bottom}px + .75rem); max-height: ${maxH}px; `
|
||||
} else {
|
||||
const maxH: number = rect.top - 40 >= 480 ? 480 : rect.top - 40
|
||||
tempStyle = `bottom: calc(${hDoc - rect.top}px + .75rem); max-height: ${maxH}px; `
|
||||
}
|
||||
if (rect.left + wPopup > wDoc - 16) {
|
||||
// 30rem - 1.75rem
|
||||
tempStyle += 'right: 1rem;'
|
||||
} else {
|
||||
tempStyle += `left: ${rect.left}px;`
|
||||
}
|
||||
style = tempStyle
|
||||
}
|
||||
|
||||
let style = 'visibility: hidden'
|
||||
$: if (popup !== undefined && popup !== null) {
|
||||
updateStyle()
|
||||
}
|
||||
|
||||
let wPopup: number = 0
|
||||
|
||||
function handleSelected (id: any): void {
|
||||
command({ id })
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window
|
||||
on:resize={() => {
|
||||
updateStyle()
|
||||
}}
|
||||
/>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class="overlay"
|
||||
on:click={() => {
|
||||
close()
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
bind:this={popup}
|
||||
class="inline-commands"
|
||||
{style}
|
||||
use:resizeObserver={(element) => {
|
||||
wPopup = element.clientWidth
|
||||
updateStyle()
|
||||
}}
|
||||
>
|
||||
<SelectPopup bind:this={menuPopup} value={items} onSelect={handleSelected} />
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
z-index: 1999;
|
||||
}
|
||||
|
||||
.inline-commands {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
z-index: 10001;
|
||||
}
|
||||
</style>
|
@ -77,6 +77,18 @@
|
||||
insertTemplate: (name, markup) => {
|
||||
textEditor?.insertMarkup(markup)
|
||||
},
|
||||
insertTable (options: { rows?: number, cols?: number, withHeaderRow?: boolean }) {
|
||||
textEditor?.insertTable(options)
|
||||
},
|
||||
insertCodeBlock: () => {
|
||||
textEditor?.insertCodeBlock()
|
||||
},
|
||||
insertSeparatorLine: () => {
|
||||
textEditor?.insertSeparatorLine()
|
||||
},
|
||||
insertContent: (content) => {
|
||||
textEditor?.insertContent(content)
|
||||
},
|
||||
focus: () => {
|
||||
textEditor?.focus()
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Markup } from '@hcengineering/core'
|
||||
import { IntlString, getMetadata } from '@hcengineering/platform'
|
||||
import presentation, { MessageViewer } from '@hcengineering/presentation'
|
||||
import presentation, { MessageViewer, getFileUrl, getImageSize } from '@hcengineering/presentation'
|
||||
import {
|
||||
ActionIcon,
|
||||
ButtonSize,
|
||||
@ -9,7 +9,10 @@
|
||||
IconClose,
|
||||
IconEdit,
|
||||
Label,
|
||||
PopupAlignment,
|
||||
ShowMore,
|
||||
getEventPositionElement,
|
||||
getPopupPositionElement,
|
||||
registerFocus,
|
||||
resizeObserver
|
||||
} from '@hcengineering/ui'
|
||||
@ -20,12 +23,14 @@
|
||||
import textEditorPlugin from '../plugin'
|
||||
import StyledTextEditor from './StyledTextEditor.svelte'
|
||||
|
||||
import { completionConfig } from './extensions'
|
||||
import { completionConfig, inlineCommandsConfig } from './extensions'
|
||||
import { EmojiExtension } from './extension/emoji'
|
||||
import { FocusExtension } from './extension/focus'
|
||||
import { ImageExtension } from './extension/imageExt'
|
||||
import { InlineCommandsExtension } from './extension/inlineCommands'
|
||||
import { type FileAttachFunction } from './extension/types'
|
||||
import { RefAction } from '../types'
|
||||
import { addTableHandler } from '../utils'
|
||||
|
||||
export let label: IntlString | undefined = undefined
|
||||
export let content: Markup
|
||||
@ -44,6 +49,7 @@
|
||||
export let autofocus = false
|
||||
export let enableBackReferences: boolean = false
|
||||
export let enableEmojiReplace: boolean = true
|
||||
export let enableInlineCommands: boolean = true
|
||||
export let isScrollable: boolean = true
|
||||
export let boundary: HTMLElement | undefined = undefined
|
||||
|
||||
@ -191,12 +197,102 @@
|
||||
extensions.push(EmojiExtension.configure())
|
||||
}
|
||||
|
||||
if (enableInlineCommands) {
|
||||
extensions.push(
|
||||
InlineCommandsExtension.configure(
|
||||
inlineCommandsConfig(handleCommandSelected, attachFile === undefined ? ['image'] : [])
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return extensions
|
||||
}
|
||||
|
||||
async function handleCommandSelected (id: string, pos: number, targetItem?: MouseEvent | HTMLElement): Promise<void> {
|
||||
switch (id) {
|
||||
case 'image':
|
||||
handleAttachImage()
|
||||
break
|
||||
case 'table': {
|
||||
let position: PopupAlignment | undefined = undefined
|
||||
if (targetItem !== undefined) {
|
||||
position =
|
||||
targetItem instanceof MouseEvent ? getEventPositionElement(targetItem) : getPopupPositionElement(targetItem)
|
||||
}
|
||||
|
||||
addTableHandler(textEditor.editorHandler.insertTable, position)
|
||||
break
|
||||
}
|
||||
case 'code-block':
|
||||
textEditor.editorHandler.insertCodeBlock(pos)
|
||||
|
||||
break
|
||||
case 'separator-line':
|
||||
textEditor.editorHandler.insertSeparatorLine()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
let inputImage: HTMLInputElement
|
||||
|
||||
export function handleAttachImage (): void {
|
||||
inputImage.click()
|
||||
}
|
||||
|
||||
async function createInlineImage (file: File): Promise<void> {
|
||||
if (!file.type.startsWith('image/') || attachFile === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const attached = await attachFile(file)
|
||||
if (attached === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const size = await getImageSize(
|
||||
file,
|
||||
getFileUrl(attached.file, 'full', getMetadata(presentation.metadata.UploadURL))
|
||||
)
|
||||
|
||||
textEditor.editorHandler.insertContent(
|
||||
{
|
||||
type: 'image',
|
||||
attrs: {
|
||||
'file-id': attached.file,
|
||||
width: Math.round(size.width / size.pixelRatio)
|
||||
}
|
||||
},
|
||||
{
|
||||
updateSelection: false
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async function fileSelected (): Promise<void> {
|
||||
const list = inputImage.files
|
||||
if (list === null || list.length === 0) return
|
||||
for (let index = 0; index < list.length; index++) {
|
||||
const file = list.item(index)
|
||||
if (file !== null) {
|
||||
await createInlineImage(file)
|
||||
}
|
||||
}
|
||||
inputImage.value = ''
|
||||
}
|
||||
|
||||
const extensions = configureExtensions()
|
||||
</script>
|
||||
|
||||
<input
|
||||
bind:this={inputImage}
|
||||
multiple
|
||||
type="file"
|
||||
name="file"
|
||||
id="imageInput"
|
||||
accept="image/*"
|
||||
style="display: none"
|
||||
on:change={fileSelected}
|
||||
/>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
|
@ -83,7 +83,7 @@
|
||||
? 'max-content'
|
||||
: maxHeight
|
||||
|
||||
const editorHandler: TextEditorHandler = {
|
||||
export const editorHandler: TextEditorHandler = {
|
||||
insertText: (text) => {
|
||||
textEditor?.insertText(text)
|
||||
},
|
||||
@ -94,6 +94,18 @@
|
||||
textEditor?.insertMarkup(markup)
|
||||
dispatch('template', name)
|
||||
},
|
||||
insertTable (options: { rows?: number, cols?: number, withHeaderRow?: boolean }) {
|
||||
textEditor?.insertTable(options)
|
||||
},
|
||||
insertCodeBlock: (pos?: number) => {
|
||||
textEditor?.insertCodeBlock(pos)
|
||||
},
|
||||
insertSeparatorLine: () => {
|
||||
textEditor?.insertSeparatorLine()
|
||||
},
|
||||
insertContent: (value, options) => {
|
||||
textEditor?.insertContent(value, options)
|
||||
},
|
||||
focus: () => {
|
||||
textEditor?.focus()
|
||||
}
|
||||
|
@ -19,7 +19,7 @@
|
||||
import { EmptyMarkup, getMarkup, markupToJSON } from '@hcengineering/text'
|
||||
import { themeStore } from '@hcengineering/ui'
|
||||
|
||||
import { AnyExtension, Editor, FocusPosition, mergeAttributes } from '@tiptap/core'
|
||||
import { AnyExtension, Content, Editor, FocusPosition, mergeAttributes } from '@tiptap/core'
|
||||
import Placeholder from '@tiptap/extension-placeholder'
|
||||
import { createEventDispatcher, onDestroy, onMount } from 'svelte'
|
||||
|
||||
@ -34,6 +34,9 @@
|
||||
import { InlineStyleToolbarExtension } from './extension/inlineStyleToolbar'
|
||||
import { SubmitExtension } from './extension/submit'
|
||||
import { EditorKit } from '../kits/editor-kit'
|
||||
import { getFileUrl, getImageSize } from '@hcengineering/presentation'
|
||||
import { FileAttachFunction } from './extension/types'
|
||||
import { ParseOptions } from '@tiptap/pm/model'
|
||||
|
||||
export let content: Markup = ''
|
||||
export let placeholder: IntlString = textEditorPlugin.string.EditorPlaceholder
|
||||
@ -43,6 +46,7 @@
|
||||
export let editorAttributes: Record<string, string> = {}
|
||||
export let boundary: HTMLElement | undefined = undefined
|
||||
export let autofocus: FocusPosition = false
|
||||
export let attachFile: FileAttachFunction | undefined = undefined
|
||||
|
||||
let element: HTMLElement
|
||||
let editor: Editor
|
||||
@ -85,6 +89,36 @@
|
||||
export function insertText (text: string): void {
|
||||
editor.commands.insertContent(text)
|
||||
}
|
||||
export function insertTable (options: { rows?: number, cols?: number, withHeaderRow?: boolean }) {
|
||||
editor.commands.insertTable(options)
|
||||
}
|
||||
export function insertCodeBlock (pos?: number): void {
|
||||
editor.commands.insertContent(
|
||||
{
|
||||
type: 'codeBlock',
|
||||
content: [{ type: 'text', text: ' ' }]
|
||||
},
|
||||
{
|
||||
updateSelection: false
|
||||
}
|
||||
)
|
||||
|
||||
if (pos !== undefined) {
|
||||
editor.commands.focus(pos, { scrollIntoView: false })
|
||||
}
|
||||
}
|
||||
export function insertSeparatorLine (): void {
|
||||
editor.commands.setHorizontalRule()
|
||||
}
|
||||
export function insertContent (
|
||||
value: Content,
|
||||
options?: {
|
||||
parseOptions?: ParseOptions
|
||||
updateSelection?: boolean
|
||||
}
|
||||
): void {
|
||||
editor.commands.insertContent(value, options)
|
||||
}
|
||||
|
||||
export function insertMarkup (markup: Markup): void {
|
||||
editor.commands.insertContent(markupToJSON(markup))
|
||||
|
@ -15,13 +15,11 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { getEmbeddedLabel } from '@hcengineering/platform'
|
||||
import presentation from '@hcengineering/presentation'
|
||||
import { getEventPositionElement, IconSize, SelectPopup, showPopup } from '@hcengineering/ui'
|
||||
import { Editor } from '@tiptap/core'
|
||||
import { Level } from '@tiptap/extension-heading'
|
||||
import textEditorPlugin from '../plugin'
|
||||
import { TextFormatCategory, TextNodeAction } from '../types'
|
||||
import { mInsertTable } from './extensions'
|
||||
import Bold from './icons/Bold.svelte'
|
||||
import Code from './icons/Code.svelte'
|
||||
import CodeBlock from './icons/CodeBlock.svelte'
|
||||
@ -79,38 +77,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
function insertTable (event: MouseEvent): void {
|
||||
showPopup(
|
||||
SelectPopup,
|
||||
{
|
||||
value: [
|
||||
{ id: '#delete', label: presentation.string.Remove },
|
||||
...mInsertTable.map((it) => ({ id: it.label, text: it.label }))
|
||||
]
|
||||
},
|
||||
getEventPositionElement(event),
|
||||
(val) => {
|
||||
if (val !== undefined) {
|
||||
if (val === '#delete') {
|
||||
textEditor.commands.deleteTable()
|
||||
dispatch('focus')
|
||||
return
|
||||
}
|
||||
const tab = mInsertTable.find((it) => it.label === val)
|
||||
if (tab !== undefined) {
|
||||
textEditor.commands.insertTable({
|
||||
cols: tab.cols,
|
||||
rows: tab.rows,
|
||||
withHeaderRow: tab.header
|
||||
})
|
||||
|
||||
dispatch('focus')
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function tableOptions (event: MouseEvent): void {
|
||||
const ops = [
|
||||
{
|
||||
@ -200,6 +166,9 @@
|
||||
|
||||
{#if textEditor}
|
||||
{#each textFormatCategories as category, index}
|
||||
{#if index > 0 && (category !== TextFormatCategory.Table || textEditor.isActive('table'))}
|
||||
<div class="buttons-divider" />
|
||||
{/if}
|
||||
{#if category === TextFormatCategory.Heading}
|
||||
<StyleButton
|
||||
icon={Header1}
|
||||
@ -305,14 +274,6 @@
|
||||
/>
|
||||
{/if}
|
||||
{#if category === TextFormatCategory.Table}
|
||||
<StyleButton
|
||||
icon={IconTable}
|
||||
iconProps={{ style: 'table' }}
|
||||
size={formatButtonSize}
|
||||
selected={textEditor.isActive('table')}
|
||||
on:click={insertTable}
|
||||
showTooltip={{ label: textEditorPlugin.string.InsertTable }}
|
||||
/>
|
||||
{#if textEditor.isActive('table')}
|
||||
<StyleButton
|
||||
icon={IconTable}
|
||||
@ -323,9 +284,6 @@
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if index < textFormatCategories.length - 1}
|
||||
<div class="buttons-divider" />
|
||||
{/if}
|
||||
{/each}
|
||||
{#if textFormatCategories.length > 0 && textNodeActions.length > 0}
|
||||
<div class="buttons-divider" />
|
||||
|
@ -0,0 +1,48 @@
|
||||
//
|
||||
// Copyright © 2024 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 { Extension } from '@tiptap/core'
|
||||
import Suggestion, { type SuggestionOptions } from './suggestion'
|
||||
import { PluginKey } from '@tiptap/pm/state'
|
||||
|
||||
export interface InlineCommandsOptions {
|
||||
suggestion: Omit<SuggestionOptions, 'editor'>
|
||||
}
|
||||
|
||||
/*
|
||||
* @public
|
||||
*/
|
||||
export const InlineCommandsExtension = Extension.create<InlineCommandsOptions>({
|
||||
name: 'inline-commands',
|
||||
|
||||
addOptions () {
|
||||
return {
|
||||
suggestion: {
|
||||
char: '/',
|
||||
startOfLine: true
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
addProseMirrorPlugins () {
|
||||
return [
|
||||
Suggestion({
|
||||
pluginKey: new PluginKey('inline-commands'),
|
||||
editor: this.editor,
|
||||
...this.options.suggestion
|
||||
})
|
||||
]
|
||||
}
|
||||
})
|
192
packages/text-editor/src/components/extension/leftMenu.ts
Normal file
192
packages/text-editor/src/components/extension/leftMenu.ts
Normal file
@ -0,0 +1,192 @@
|
||||
//
|
||||
// Copyright © 2024 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 { Extension } from '@tiptap/core'
|
||||
import { Plugin, PluginKey } from '@tiptap/pm/state'
|
||||
import { type EditorView } from '@tiptap/pm/view'
|
||||
import { type Asset, getMetadata } from '@hcengineering/platform'
|
||||
import { SelectPopup, getEventPositionElement, showPopup } from '@hcengineering/ui'
|
||||
|
||||
export interface LeftMenuOptions {
|
||||
width: number
|
||||
height: number
|
||||
marginX: number
|
||||
className: string
|
||||
icon: Asset
|
||||
iconProps: any
|
||||
items: Array<{ id: string, label: string, icon: Asset }>
|
||||
handleSelect: (id: string, pos: number, event: MouseEvent) => Promise<void>
|
||||
}
|
||||
|
||||
function nodeDOMAtCoords (coords: { x: number, y: number }): Element | undefined {
|
||||
return document
|
||||
.elementsFromPoint(coords.x, coords.y)
|
||||
.find((elem: Element) => elem.parentElement?.matches?.('.ProseMirror') === true)
|
||||
}
|
||||
|
||||
function posAtLeftMenuElement (view: EditorView, leftMenuElement: HTMLElement, offsetX: number): number {
|
||||
const rect = leftMenuElement.getBoundingClientRect()
|
||||
|
||||
const position = view.posAtCoords({
|
||||
left: rect.left + offsetX,
|
||||
top: rect.top + rect.height / 2
|
||||
})
|
||||
|
||||
if (position === null) {
|
||||
return -1
|
||||
}
|
||||
|
||||
return position.inside >= 0 ? position.inside : position.pos
|
||||
}
|
||||
|
||||
function LeftMenu (options: LeftMenuOptions): Plugin {
|
||||
let leftMenuElement: HTMLElement | null = null
|
||||
const offsetX = options.width + options.marginX
|
||||
|
||||
function hideLeftMenu (): void {
|
||||
if (leftMenuElement !== null) {
|
||||
leftMenuElement.classList.add('hidden')
|
||||
}
|
||||
}
|
||||
|
||||
function showLeftMenu (): void {
|
||||
if (leftMenuElement !== null) {
|
||||
leftMenuElement.classList.remove('hidden')
|
||||
}
|
||||
}
|
||||
|
||||
return new Plugin({
|
||||
key: new PluginKey('left-menu'),
|
||||
view: (view) => {
|
||||
leftMenuElement = document.createElement('div')
|
||||
leftMenuElement.classList.add(options.className) // Style externally with CSS
|
||||
leftMenuElement.style.position = 'absolute'
|
||||
|
||||
const svgNs = 'http://www.w3.org/2000/svg'
|
||||
const icon = document.createElementNS(svgNs, 'svg')
|
||||
const { className: iconClassName, ...restIconProps } = options.iconProps
|
||||
icon.classList.add(iconClassName)
|
||||
Object.entries(restIconProps).forEach(([key, value]) => {
|
||||
icon.setAttribute(key, value as string)
|
||||
})
|
||||
|
||||
const use = document.createElementNS(svgNs, 'use')
|
||||
const href = getMetadata(options.icon)
|
||||
|
||||
if (href !== undefined) {
|
||||
use.setAttributeNS('http://www.w3.org/1999/xlink', 'href', href)
|
||||
}
|
||||
|
||||
icon.appendChild(use)
|
||||
leftMenuElement.appendChild(icon)
|
||||
|
||||
leftMenuElement.addEventListener('mousedown', (e) => {
|
||||
// Prevent default in order for the popup to take focus for keyboard events
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
showPopup(
|
||||
SelectPopup,
|
||||
{
|
||||
value: options.items
|
||||
},
|
||||
getEventPositionElement(e),
|
||||
(val) => {
|
||||
if (leftMenuElement === null) return
|
||||
const pos = posAtLeftMenuElement(view, leftMenuElement, offsetX)
|
||||
|
||||
void options.handleSelect(val, pos, e)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
view?.dom?.parentElement?.appendChild(leftMenuElement)
|
||||
|
||||
return {
|
||||
destroy: () => {
|
||||
leftMenuElement?.remove?.()
|
||||
leftMenuElement = null
|
||||
}
|
||||
}
|
||||
},
|
||||
props: {
|
||||
handleDOMEvents: {
|
||||
mousemove: (view, event) => {
|
||||
if (!view.editable) {
|
||||
return
|
||||
}
|
||||
|
||||
const node = nodeDOMAtCoords({
|
||||
x: event.clientX + offsetX,
|
||||
y: event.clientY
|
||||
})
|
||||
|
||||
if (!(node instanceof HTMLElement)) {
|
||||
hideLeftMenu()
|
||||
return
|
||||
}
|
||||
|
||||
const parent = node?.parentElement
|
||||
if (!(parent instanceof HTMLElement)) {
|
||||
hideLeftMenu()
|
||||
return
|
||||
}
|
||||
|
||||
const compStyle = window.getComputedStyle(node)
|
||||
const lineHeight = parseInt(compStyle.lineHeight, 10)
|
||||
const paddingTop = parseInt(compStyle.paddingTop, 10)
|
||||
|
||||
// For some reason the offsetTop value for all elements is shifted by the first element's margin
|
||||
// so taking it into account here
|
||||
let firstMargin = 0
|
||||
const firstChild = parent.firstChild
|
||||
if (firstChild !== null) {
|
||||
const firstChildCompStyle = window.getComputedStyle(firstChild as HTMLElement)
|
||||
firstMargin = parseInt(firstChildCompStyle.marginTop, 10)
|
||||
}
|
||||
|
||||
const left = -offsetX
|
||||
let top = node.offsetTop
|
||||
top += (lineHeight - options.height) / 2
|
||||
top += paddingTop
|
||||
top += firstMargin
|
||||
|
||||
if (leftMenuElement === null) return
|
||||
|
||||
leftMenuElement.style.left = `${left}px`
|
||||
leftMenuElement.style.top = `${top}px`
|
||||
|
||||
showLeftMenu()
|
||||
},
|
||||
keydown: () => {
|
||||
hideLeftMenu()
|
||||
},
|
||||
mousewheel: () => {
|
||||
hideLeftMenu()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
* @public
|
||||
*/
|
||||
export const LeftMenuExtension = Extension.create<LeftMenuOptions>({
|
||||
name: 'leftMenu',
|
||||
|
||||
addProseMirrorPlugins () {
|
||||
return [LeftMenu(this.options)]
|
||||
}
|
||||
})
|
@ -13,10 +13,15 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import view from '@hcengineering/view'
|
||||
import { type Editor, type Range } from '@tiptap/core'
|
||||
|
||||
import { type CompletionOptions } from '../Completion'
|
||||
import MentionList from './MentionList.svelte'
|
||||
import { SvelteRenderer } from './node-view'
|
||||
import type { SuggestionKeyDownProps, SuggestionProps } from './extension/suggestion'
|
||||
import InlineCommandsList from './InlineCommandsList.svelte'
|
||||
import plugin from '../plugin'
|
||||
|
||||
export const mInsertTable = [
|
||||
{
|
||||
@ -123,3 +128,66 @@ export const completionConfig: Partial<CompletionOptions> = {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const inlineCommandsIds = ['image', 'table', 'code-block', 'separator-line'] as const
|
||||
export type InlineCommandId = (typeof inlineCommandsIds)[number]
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export function inlineCommandsConfig (
|
||||
handleSelect: (id: string, pos: number, targetItem?: MouseEvent | HTMLElement) => Promise<void>,
|
||||
excludedCommands: InlineCommandId[] = []
|
||||
): Partial<CompletionOptions> {
|
||||
return {
|
||||
suggestion: {
|
||||
items: () => {
|
||||
return [
|
||||
{ id: 'image', label: plugin.string.Image, icon: view.icon.Image },
|
||||
{ id: 'table', label: plugin.string.Table, icon: view.icon.Table2 },
|
||||
{ id: 'code-block', label: plugin.string.CodeBlock, icon: view.icon.CodeBlock },
|
||||
{ id: 'separator-line', label: plugin.string.SeparatorLine, icon: view.icon.SeparatorLine }
|
||||
].filter(({ id }) => !excludedCommands.includes(id as InlineCommandId))
|
||||
},
|
||||
command: ({ editor, range, props }: { editor: Editor, range: Range, props: any }) => {
|
||||
editor.commands.deleteRange(range)
|
||||
|
||||
if (props?.id != null) {
|
||||
const { node } = editor.view.domAtPos(range.from)
|
||||
const targetElement = node instanceof HTMLElement ? node : undefined
|
||||
|
||||
void handleSelect(props.id, range.from, targetElement)
|
||||
}
|
||||
},
|
||||
render: () => {
|
||||
let component: any
|
||||
|
||||
return {
|
||||
onStart: (props: SuggestionProps) => {
|
||||
component = new SvelteRenderer(InlineCommandsList, {
|
||||
element: document.body,
|
||||
props: {
|
||||
...props,
|
||||
close: () => {
|
||||
component.destroy()
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
onUpdate (props: SuggestionProps) {
|
||||
component.updateProps(props)
|
||||
},
|
||||
onKeyDown (props: SuggestionKeyDownProps) {
|
||||
if (props.event.key === 'Escape') {
|
||||
props.event.stopPropagation()
|
||||
}
|
||||
return component.onKeyDown(props)
|
||||
},
|
||||
onExit () {
|
||||
component.destroy()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
14
packages/text-editor/src/components/icons/Table.svelte
Normal file
14
packages/text-editor/src/components/icons/Table.svelte
Normal file
@ -0,0 +1,14 @@
|
||||
<script lang="ts">
|
||||
export let size: 'small' | 'medium' | 'large'
|
||||
export let fill: string = 'currentColor'
|
||||
</script>
|
||||
|
||||
<svg class="svg-{size}" {fill} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18">
|
||||
<g transform="translate(2,3)">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M0 2C0 0.895431 0.895431 0 2 0H12C13.1046 0 14 0.895431 14 2V10C14 11.1046 13.1046 12 12 12H2C0.895431 12 0 11.1046 0 10V2ZM5.5 1H8.5V5.5H5.5V1ZM4.5 5.5V1H2C1.44772 1 1 1.44772 1 2V5.5H4.5ZM1 6.5V10C1 10.5523 1.44772 11 2 11H4.5V6.5H1ZM5.5 6.5H8.5V11H5.5V6.5ZM9.5 6.5V11H12C12.5523 11 13 10.5523 13 10V6.5H9.5ZM13 5.5V2C13 1.44772 12.5523 1 12 1H9.5V5.5H13Z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
@ -34,6 +34,7 @@ export { default as StyledTextEditor } from './components/StyledTextEditor.svelt
|
||||
export { default as TextEditor } from './components/TextEditor.svelte'
|
||||
export { default as TextEditorStyleToolbar } from './components/TextEditorStyleToolbar.svelte'
|
||||
export { default as AttachIcon } from './components/icons/Attach.svelte'
|
||||
export { default as TableIcon } from './components/icons/Table.svelte'
|
||||
export { default as TableOfContents } from './components/toc/TableOfContents.svelte'
|
||||
export * from './components/node-view'
|
||||
export { default } from './plugin'
|
||||
|
@ -78,6 +78,8 @@ export default plugin(textEditorId, {
|
||||
TableOptions: '' as IntlString,
|
||||
Width: '' as IntlString,
|
||||
Height: '' as IntlString,
|
||||
Unset: '' as IntlString
|
||||
Unset: '' as IntlString,
|
||||
Image: '' as IntlString,
|
||||
SeparatorLine: '' as IntlString
|
||||
}
|
||||
})
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { type Asset, type IntlString, type Resource } from '@hcengineering/platform'
|
||||
import { type Account, type Doc, type Markup, type Ref } from '@hcengineering/core'
|
||||
import type { AnySvelteComponent } from '@hcengineering/ui'
|
||||
import { type Editor, type SingleCommands } from '@tiptap/core'
|
||||
import { type RelativePosition } from 'yjs'
|
||||
import { type Content, type Editor, type SingleCommands } from '@tiptap/core'
|
||||
import { type ParseOptions } from '@tiptap/pm/model'
|
||||
|
||||
/**
|
||||
* @public
|
||||
@ -11,12 +12,22 @@ export interface TextEditorHandler {
|
||||
insertText: (html: string) => void
|
||||
insertMarkup: (markup: Markup) => void
|
||||
insertTemplate: (name: string, markup: string) => void
|
||||
insertTable: (options: { rows?: number, cols?: number, withHeaderRow?: boolean }) => void
|
||||
insertCodeBlock: (pos?: number) => void
|
||||
insertSeparatorLine: () => void
|
||||
insertContent: (
|
||||
value: Content,
|
||||
options?: {
|
||||
parseOptions?: ParseOptions
|
||||
updateSelection?: boolean
|
||||
}
|
||||
) => void
|
||||
focus: () => void
|
||||
}
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type RefInputAction = (element: HTMLElement, editor: TextEditorHandler) => void
|
||||
export type RefInputAction = (element: HTMLElement, editor: TextEditorHandler, event?: MouseEvent) => void
|
||||
/**
|
||||
* A contribution to reference input control, to allow to add more actions to it.
|
||||
* @public
|
||||
|
@ -14,6 +14,9 @@
|
||||
//
|
||||
|
||||
import { type Attribute } from '@tiptap/core'
|
||||
import { showPopup, SelectPopup, type PopupAlignment } from '@hcengineering/ui'
|
||||
|
||||
import { mInsertTable } from './components/extensions'
|
||||
|
||||
export function getDataAttribute (
|
||||
name: string,
|
||||
@ -37,3 +40,28 @@ export function getDataAttribute (
|
||||
...(options ?? {})
|
||||
}
|
||||
}
|
||||
|
||||
export async function addTableHandler (
|
||||
insertTable: (options: { rows?: number, cols?: number, withHeaderRow?: boolean }) => void,
|
||||
alignment?: PopupAlignment
|
||||
): Promise<void> {
|
||||
showPopup(
|
||||
SelectPopup,
|
||||
{
|
||||
value: mInsertTable.map((it) => ({ id: it.label, text: it.label }))
|
||||
},
|
||||
alignment ?? 'center',
|
||||
(val) => {
|
||||
if (val !== undefined) {
|
||||
const tab = mInsertTable.find((it) => it.label === val)
|
||||
if (tab !== undefined) {
|
||||
insertTable({
|
||||
cols: tab.cols,
|
||||
rows: tab.rows,
|
||||
withHeaderRow: tab.header
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -901,6 +901,10 @@ a.no-line {
|
||||
&.disabled { pointer-events: none; }
|
||||
}
|
||||
|
||||
.hidden {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.lines-limit-2, .lines-limit-4 {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
|
@ -33,7 +33,7 @@
|
||||
export let value: SelectPopupValueType[]
|
||||
export let width: 'medium' | 'large' | 'full' = 'medium'
|
||||
export let size: 'small' | 'medium' | 'large' = 'small'
|
||||
export let onSelect: ((value: SelectPopupValueType['id']) => void) | undefined = undefined
|
||||
export let onSelect: ((value: SelectPopupValueType['id'], event?: Event) => void) | undefined = undefined
|
||||
export let showShadow: boolean = true
|
||||
export let embedded: boolean = false
|
||||
export let loading = false
|
||||
@ -59,27 +59,32 @@
|
||||
}
|
||||
}
|
||||
|
||||
function onKeydown (key: KeyboardEvent): void {
|
||||
export function onKeydown (key: KeyboardEvent): boolean {
|
||||
if (key.code === 'Tab') {
|
||||
dispatch('close')
|
||||
key.preventDefault()
|
||||
key.stopPropagation()
|
||||
return true
|
||||
}
|
||||
if (key.code === 'ArrowUp') {
|
||||
key.stopPropagation()
|
||||
key.preventDefault()
|
||||
list.select(selection - 1)
|
||||
return true
|
||||
}
|
||||
if (key.code === 'ArrowDown') {
|
||||
key.stopPropagation()
|
||||
key.preventDefault()
|
||||
list.select(selection + 1)
|
||||
return true
|
||||
}
|
||||
if (key.code === 'Enter') {
|
||||
key.preventDefault()
|
||||
key.stopPropagation()
|
||||
sendSelect(value[selection].id)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
const manager = createFocusManager()
|
||||
|
||||
|
@ -18,13 +18,21 @@
|
||||
import { Account, Doc, Ref, generateId } from '@hcengineering/core'
|
||||
import { IntlString, getResource, setPlatformStatus, unknownError } from '@hcengineering/platform'
|
||||
import { KeyedAttribute, createQuery, getClient } from '@hcengineering/presentation'
|
||||
import textEditor, { AttachIcon, CollaborativeAttributeBox, RefAction } from '@hcengineering/text-editor'
|
||||
import { AnySvelteComponent, navigate } from '@hcengineering/ui'
|
||||
import view from '@hcengineering/view'
|
||||
import { getCollaborationUser, getObjectLinkFragment } from '@hcengineering/view-resources'
|
||||
import textEditor, {
|
||||
AttachIcon,
|
||||
CollaborativeAttributeBox,
|
||||
RefAction,
|
||||
TableIcon,
|
||||
TextEditorHandler,
|
||||
addTableHandler
|
||||
} from '@hcengineering/text-editor'
|
||||
import { AnySvelteComponent, getEventPositionElement, getPopupPositionElement, navigate } from '@hcengineering/ui'
|
||||
import view from '@hcengineering/view'
|
||||
import { defaultRefActions, getModelRefActions } from '@hcengineering/text-editor/src/components/editor/actions'
|
||||
|
||||
import AttachmentsGrid from './AttachmentsGrid.svelte'
|
||||
import { uploadFile } from '../utils'
|
||||
import { defaultRefActions, getModelRefActions } from '@hcengineering/text-editor/src/components/editor/actions'
|
||||
|
||||
export let object: Doc
|
||||
export let key: KeyedAttribute
|
||||
@ -58,6 +66,12 @@
|
||||
icon: AttachIcon,
|
||||
action: handleAttach,
|
||||
order: 1001
|
||||
},
|
||||
{
|
||||
label: textEditor.string.Table,
|
||||
icon: TableIcon,
|
||||
action: handleTable,
|
||||
order: 1501
|
||||
}
|
||||
]
|
||||
} else {
|
||||
@ -94,6 +108,12 @@
|
||||
return editor?.isFocused() ?? false
|
||||
}
|
||||
|
||||
export function handleTable (element: HTMLElement, editorHandler: TextEditorHandler, event?: MouseEvent): void {
|
||||
const position = event !== undefined ? getEventPositionElement(event) : getPopupPositionElement(element)
|
||||
|
||||
addTableHandler(editorHandler.insertTable, position)
|
||||
}
|
||||
|
||||
export function handleAttach (): void {
|
||||
inputFile.click()
|
||||
}
|
||||
|
@ -119,4 +119,26 @@
|
||||
<path d="M14 0.00012207C11.2311 0.00012207 8.52431 0.821208 6.22202 2.35955C3.91973 3.89789 2.12532 6.08439 1.06569 8.64255C0.00606596 11.2007 -0.271181 14.0157 0.269012 16.7314C0.809205 19.4471 2.14258 21.9417 4.10051 23.8996C6.05845 25.8576 8.55301 27.1909 11.2687 27.7311C13.9845 28.2713 16.7994 27.9941 19.3576 26.9344C21.9157 25.8748 24.1022 24.0804 25.6406 21.7781C27.1789 19.4758 28 16.7691 28 14.0001C28 10.2871 26.525 6.72614 23.8995 4.10063C21.274 1.47512 17.713 0.00012207 14 0.00012207ZM14 26.0001C11.6266 26.0001 9.30655 25.2963 7.33316 23.9778C5.35977 22.6592 3.8217 20.785 2.91345 18.5923C2.0052 16.3996 1.76756 13.9868 2.23058 11.659C2.69361 9.33127 3.83649 7.19307 5.51472 5.51484C7.19296 3.83661 9.33115 2.69372 11.6589 2.2307C13.9867 1.76768 16.3995 2.00532 18.5922 2.91357C20.7849 3.82182 22.6591 5.35989 23.9776 7.33328C25.2962 9.30667 26 11.6267 26 14.0001C26 17.1827 24.7357 20.235 22.4853 22.4854C20.2348 24.7358 17.1826 26.0001 14 26.0001Z" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.7071 9.29326C21.0976 9.68378 21.0976 10.3169 20.7071 10.7075L12.7071 18.7075C12.3166 19.098 11.6834 19.098 11.2929 18.7075L7.29289 14.7075C6.90237 14.3169 6.90237 13.6838 7.29289 13.2933C7.68342 12.9027 8.31658 12.9027 8.70711 13.2933L12 16.5862L19.2929 9.29326C19.6834 8.90274 20.3166 8.90274 20.7071 9.29326Z" />
|
||||
</symbol>
|
||||
<symbol id="add" viewBox="0 0 10 10">
|
||||
<path d="M5.5 4.5V0.5C5.5 0.223858 5.27614 0 5 0C4.72386 0 4.5 0.223858 4.5 0.5V4.5H0.5C0.223858 4.5 0 4.72386 0 5C0 5.27614 0.223858 5.5 0.5 5.5H4.5V9.5C4.5 9.77614 4.72386 10 5 10C5.27614 10 5.5 9.77614 5.5 9.5V5.5H9.5C9.77614 5.5 10 5.27614 10 5C10 4.72386 9.77614 4.5 9.5 4.5H5.5Z" />
|
||||
</symbol>
|
||||
|
||||
<symbol id="image" viewBox="0 0 12 12">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.33335 4.7472C8.08668 4.91203 7.79667 5 7.5 5C7.10218 5 6.72064 4.84197 6.43934 4.56066C6.15804 4.27936 6 3.89783 6 3.5C6 3.20333 6.08797 2.91332 6.2528 2.66665C6.41762 2.41997 6.65189 2.22771 6.92597 2.11418C7.20006 2.00065 7.50166 1.97094 7.79264 2.02882C8.08361 2.0867 8.35088 2.22956 8.56066 2.43934C8.77044 2.64912 8.9133 2.91639 8.97118 3.20737C9.02906 3.49834 8.99935 3.79994 8.88582 4.07403C8.77229 4.34811 8.58003 4.58238 8.33335 4.7472ZM7.77779 3.08427C7.69556 3.02933 7.59889 3 7.5 3C7.36739 3 7.24021 3.05268 7.14645 3.14645C7.05268 3.24022 7 3.36739 7 3.5C7 3.59889 7.02932 3.69556 7.08426 3.77779C7.13921 3.86001 7.2173 3.9241 7.30866 3.96194C7.40002 3.99978 7.50056 4.00969 7.59755 3.99039C7.69454 3.9711 7.78363 3.92348 7.85355 3.85355C7.92348 3.78363 7.9711 3.69454 7.99039 3.59755C8.00969 3.50056 7.99978 3.40002 7.96194 3.30866C7.9241 3.2173 7.86001 3.13921 7.77779 3.08427Z" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 2C0 0.895431 0.89543 0 2 0H10C11.1046 0 12 0.89543 12 2V10C12 11.1046 11.1046 12 10 12H2C0.895431 12 0 11.1046 0 10V2ZM2 1C1.44772 1 1 1.44772 1 2V6.58574L2.79285 4.79289C3.18337 4.40237 3.81654 4.40237 4.20706 4.79289L6.99995 7.58579L7.79285 6.79289C8.18337 6.40237 8.81653 6.40237 9.20706 6.79289L11 8.58583V2C11 1.44772 10.5523 1 10 1H2ZM1 10V7.99995L3.49995 5.5L6.29285 8.29289C6.68337 8.68342 7.31654 8.68342 7.70706 8.29289L8.49995 7.5L11 10C11 10.5523 10.5523 11 10 11H2C1.44772 11 1 10.5523 1 10Z" />
|
||||
</symbol>
|
||||
<symbol id="table2" viewBox="0 0 14 12">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 2C0 0.895431 0.895431 0 2 0H12C13.1046 0 14 0.895431 14 2V10C14 11.1046 13.1046 12 12 12H2C0.895431 12 0 11.1046 0 10V2ZM5.5 1H8.5V5.5H5.5V1ZM4.5 5.5V1H2C1.44772 1 1 1.44772 1 2V5.5H4.5ZM1 6.5V10C1 10.5523 1.44772 11 2 11H4.5V6.5H1ZM5.5 6.5H8.5V11H5.5V6.5ZM9.5 6.5V11H12C12.5523 11 13 10.5523 13 10V6.5H9.5ZM13 5.5V2C13 1.44772 12.5523 1 12 1H9.5V5.5H13Z" />
|
||||
</symbol>
|
||||
<symbol id="code-block" viewBox="0 0 14 11">
|
||||
<path d="M5.5 0H6.5L5 6H4L5.5 0Z" />
|
||||
<path d="M0.5 3L3 5.5L3.70711 4.79289L1.91421 3L3.70711 1.20711L3 0.5L0.5 3Z" />
|
||||
<path d="M9.99995 3L7.49995 0.5L6.79285 1.20711L8.58574 3L6.79285 4.79289L7.49995 5.5L9.99995 3Z" />
|
||||
<path d="M3 9C3 9.55228 3.44772 10 4 10H12C12.5523 10 13 9.55228 13 9V3C13 2.44772 12.5523 2 12 2H11.5C11.2239 2 11 1.77614 11 1.5C11 1.22386 11.2239 1 11.5 1H12C13.1046 1 14 1.89543 14 3V9C14 10.1046 13.1046 11 12 11H4C2.89543 11 2 10.1046 2 9V7.5C2 7.22386 2.22386 7 2.5 7C2.77614 7 3 7.22386 3 7.5V9Z" />
|
||||
</symbol>
|
||||
<symbol id="separator-line" viewBox="0 0 14 12">
|
||||
<path d="M11 1H3C2.44772 1 2 1.44772 2 2V3C2 3.27614 1.77614 3.5 1.5 3.5C1.22386 3.5 1 3.27614 1 3V2C1 0.895431 1.89543 0 3 0H11C12.1046 0 13 0.895431 13 2V3C13 3.27614 12.7761 3.5 12.5 3.5C12.2239 3.5 12 3.27614 12 3V2C12 1.44772 11.5523 1 11 1Z" />
|
||||
<path d="M1 9C1 8.72386 1.22386 8.5 1.5 8.5C1.77614 8.5 2 8.72386 2 9V10C2 10.5523 2.44772 11 3 11H11C11.5523 11 12 10.5523 12 10V9C12 8.72386 12.2239 8.5 12.5 8.5C12.7761 8.5 13 8.72386 13 9V10C13 11.1046 12.1046 12 11 12H3C1.89543 12 1 11.1046 1 10V9Z" />
|
||||
<path d="M14 6C14 5.72386 13.7761 5.5 13.5 5.5H0.5C0.223858 5.5 0 5.72386 0 6C0 6.27614 0.223857 6.5 0.5 6.5H13.5C13.7761 6.5 14 6.27614 14 6Z" />
|
||||
</symbol>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 37 KiB |
@ -44,5 +44,10 @@ loadMetadata(view.icon, {
|
||||
Star: `${icons}#star`,
|
||||
Eye: `${icons}#eye`,
|
||||
EyeCrossed: `${icons}#eye-crossed`,
|
||||
CheckCircle: `${icons}#check-circle`
|
||||
CheckCircle: `${icons}#check-circle`,
|
||||
Add: `${icons}#add`,
|
||||
Image: `${icons}#image`,
|
||||
Table2: `${icons}#table2`,
|
||||
CodeBlock: `${icons}#code-block`,
|
||||
SeparatorLine: `${icons}#separator-line`
|
||||
})
|
||||
|
@ -222,7 +222,12 @@ const view = plugin(viewId, {
|
||||
Star: '' as Asset,
|
||||
Eye: '' as Asset,
|
||||
EyeCrossed: '' as Asset,
|
||||
CheckCircle: '' as Asset
|
||||
CheckCircle: '' as Asset,
|
||||
Add: '' as Asset,
|
||||
Image: '' as Asset,
|
||||
Table2: '' as Asset,
|
||||
CodeBlock: '' as Asset,
|
||||
SeparatorLine: '' as Asset
|
||||
},
|
||||
category: {
|
||||
General: '' as Ref<ActionCategory>,
|
||||
|
@ -37,7 +37,7 @@ export class PlanningPage extends CalendarPage {
|
||||
this.page = page
|
||||
this.pageHeader = page.locator('div[class*="navigator"] div[class*="header"]', { hasText: 'Planning' })
|
||||
this.buttonCreateNewToDo = page.locator('div[class*="toDos-container"] button.button')
|
||||
this.inputPopupCreateTitle = page.locator('div.popup input')
|
||||
this.inputPopupCreateTitle = page.locator('div.popup input[type="text"]')
|
||||
this.inputPopupCreateDescription = page.locator('div.popup div.tiptap')
|
||||
this.inputPanelCreateDescription = page.locator('div.hulyModal-container div.tiptap')
|
||||
this.buttonPopupCreateDueDate = page.locator(
|
||||
|
@ -19,7 +19,7 @@ export class VacancyDetailsPage extends CommonRecruitingPage {
|
||||
this.inputDescription = page.locator('div[class*="full"] div.tiptap')
|
||||
this.buttonInputDescription = page.locator('button > span', { hasText: 'Description' })
|
||||
this.buttonInputLocation = page.locator('button > span', { hasText: 'Location' })
|
||||
this.inputAttachFile = page.locator('div[class*="full"] input[name="file"]')
|
||||
this.inputAttachFile = page.locator('div[class*="full"] input[name="file"]#fileInput')
|
||||
this.buttonInputCompany = page.locator('button > div', { hasText: 'Company' })
|
||||
this.buttonInputDueDate = page.locator('button > div', { hasText: 'Due date' })
|
||||
this.buttonDatePopupSave = page.locator('div.popup button[type="submit"]')
|
||||
|
@ -63,7 +63,7 @@ export class IssuesPage extends CommonTrackerPage {
|
||||
'form[id="tracker:string:NewIssue"] div#milestone-editor button'
|
||||
)
|
||||
this.buttonPopupCreateNewIssueDuedate = page.locator('form[id="tracker:string:NewIssue"] div#duedate-editor button')
|
||||
this.inputPopupCreateNewIssueFile = page.locator('form[id="tracker:string:NewIssue"] input[type="file"]')
|
||||
this.inputPopupCreateNewIssueFile = page.locator('form[id="tracker:string:NewIssue"] input[type="file"]#file')
|
||||
this.textPopupCreateNewIssueFile = page.locator('div[class*="attachments"] > div[class*="attachment"]')
|
||||
this.buttonCreateIssue = page.locator('button > span', { hasText: 'Create issue' })
|
||||
this.inputSearch = page.locator('input[placeholder="Search"]')
|
||||
|
@ -19,7 +19,7 @@ export class TemplatePage extends CommonTrackerPage {
|
||||
super(page)
|
||||
this.page = page
|
||||
this.buttonNewTemplate = page.locator('button > span', { hasText: 'Template' })
|
||||
this.inputIssueTitle = page.locator('form[id$="NewProcess"] input')
|
||||
this.inputIssueTitle = page.locator('form[id$="NewProcess"] input[type="text"]')
|
||||
this.inputIssueDescription = page.locator('form[id$="NewProcess"] div.tiptap')
|
||||
this.buttonPopupCreateNewTemplatePriority = page.locator(
|
||||
'form[id$="NewProcess"] div.antiCard-pool > button:first-child'
|
||||
|
Loading…
Reference in New Issue
Block a user