UBERF-5964: Insert items menu in editor (#5341)

Signed-off-by: Alexey Zinoviev <alexey.zinoviev@xored.com>
This commit is contained in:
Alexey Zinoviev 2024-04-15 17:47:59 +04:00 committed by GitHub
parent b34b70feeb
commit 0e15e78e44
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 938 additions and 73 deletions

View File

@ -47,6 +47,8 @@
"TableOptions": "Customize table",
"Width": "Width",
"Height": "Height",
"Unset": "Unset"
"Unset": "Unset",
"Image": "Image",
"SeparatorLine": "Separator line"
}
}

View File

@ -45,6 +45,8 @@
"TableOptions": "Personalizar tabla",
"Width": "Ancho",
"Height": "Alto",
"Unset": "Anular"
"Unset": "Anular",
"Image": "Imagen",
"SeparatorLine": "Línea de separación"
}
}

View File

@ -45,6 +45,8 @@
"TableOptions": "Personalizar tabela",
"Width": "Largura",
"Height": "Altura",
"Unset": "Anular"
"Unset": "Anular",
"Image": "Imagem",
"SeparatorLine": "linha separadora"
}
}

View File

@ -47,6 +47,8 @@
"TableOptions": "Настроить таблицу",
"Width": "Ширина",
"Height": "Высота",
"Unset": "Убрать"
"Unset": "Убрать",
"Image": "Изображение",
"SeparatorLine": "Разделительная линия"
}
}

View File

@ -119,6 +119,7 @@
{readonly}
field={key.key}
canEmbedFiles={false}
withSideMenu={false}
on:focus
on:blur
on:update

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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)]
}
})

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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