mirror of
https://github.com/hcengineering/platform.git
synced 2025-05-13 02:41:11 +00:00
Cherry Pick of recent changes (#8605)
Signed-off-by: Victor Ilyushchenko <alt13ri@gmail.com>
This commit is contained in:
parent
55d184c99e
commit
330b58d221
@ -56,7 +56,12 @@
|
|||||||
{:else if mark.type === MarkupMarkType.em}
|
{:else if mark.type === MarkupMarkType.em}
|
||||||
<em><slot /></em>
|
<em><slot /></em>
|
||||||
{:else if mark.type === MarkupMarkType.link}
|
{:else if mark.type === MarkupMarkType.link}
|
||||||
<a href={attrs.href} target={attrs.target} on:click|stopPropagation={handleLink} on:contextmenu|stopPropagation>
|
<a
|
||||||
|
href={attrs.href}
|
||||||
|
target={attrs.target ?? '_blank'}
|
||||||
|
on:click|stopPropagation={handleLink}
|
||||||
|
on:contextmenu|stopPropagation
|
||||||
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</a>
|
</a>
|
||||||
{:else if mark.type === MarkupMarkType.strike}
|
{:else if mark.type === MarkupMarkType.strike}
|
||||||
|
@ -135,7 +135,7 @@ export const TextColor = Extension.create<TextColorOptions>({
|
|||||||
unsetTextColor:
|
unsetTextColor:
|
||||||
() =>
|
() =>
|
||||||
({ chain }) => {
|
({ chain }) => {
|
||||||
return chain().setMark('textStyle', { color: null }).removeEmptyTextStyle().run()
|
return chain().unsetMark('textStyle').run()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -92,12 +92,13 @@ export function showPopup (
|
|||||||
overlay: boolean
|
overlay: boolean
|
||||||
fixed?: boolean
|
fixed?: boolean
|
||||||
refId?: string
|
refId?: string
|
||||||
|
id?: string
|
||||||
} = {
|
} = {
|
||||||
category: 'popup',
|
category: 'popup',
|
||||||
overlay: true
|
overlay: true
|
||||||
}
|
}
|
||||||
): PopupResult {
|
): PopupResult {
|
||||||
const id = `${popupId++}`
|
const id = options?.id ?? `${popupId++}`
|
||||||
const closePopupOp = (): void => {
|
const closePopupOp = (): void => {
|
||||||
modalStore.update((popups) => {
|
modalStore.update((popups) => {
|
||||||
const pos = popups.findIndex((p) => (p as CompAndProps).id === id && p.type === 'popup')
|
const pos = popups.findIndex((p) => (p as CompAndProps).id === id && p.type === 'popup')
|
||||||
@ -107,6 +108,7 @@ export function showPopup (
|
|||||||
return popups
|
return popups
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
closePopupOp()
|
||||||
const _element = element instanceof HTMLElement ? getPopupPositionElement(element) : element
|
const _element = element instanceof HTMLElement ? getPopupPositionElement(element) : element
|
||||||
const data: Omit<CompAndProps, 'is'> = {
|
const data: Omit<CompAndProps, 'is'> = {
|
||||||
id,
|
id,
|
||||||
|
@ -872,12 +872,16 @@ export async function getFirstRank (
|
|||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
export function getEffectiveDocUpdate (): DocumentUpdate<ControlledDocument> {
|
export function getEffectiveDocUpdates (): DocumentUpdate<ControlledDocument>[] {
|
||||||
return {
|
return [
|
||||||
|
{
|
||||||
state: DocumentState.Effective,
|
state: DocumentState.Effective,
|
||||||
effectiveDate: Date.now(),
|
effectiveDate: Date.now()
|
||||||
controlledState: undefined
|
},
|
||||||
|
{
|
||||||
|
$unset: { controlledState: true }
|
||||||
}
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -50,7 +50,7 @@ const palette = {
|
|||||||
colorSpec('red', 'bg')
|
colorSpec('red', 'bg')
|
||||||
],
|
],
|
||||||
text: [
|
text: [
|
||||||
{ color: 'var(--theme-text-color-primary)' },
|
{ color: 'var(--theme-text-primary-color)' },
|
||||||
colorSpec('gray'),
|
colorSpec('gray'),
|
||||||
colorSpec('brown'),
|
colorSpec('brown'),
|
||||||
colorSpec('orange'),
|
colorSpec('orange'),
|
||||||
@ -65,7 +65,11 @@ const palette = {
|
|||||||
|
|
||||||
export async function openBackgroundColorOptions (editor: Editor, event: MouseEvent): Promise<void> {
|
export async function openBackgroundColorOptions (editor: Editor, event: MouseEvent): Promise<void> {
|
||||||
await new Promise<void>((resolve) => {
|
await new Promise<void>((resolve) => {
|
||||||
showPopup(ColorPicker, { palette: palette.background }, getEventPositionElement(event), (val) => {
|
showPopup(
|
||||||
|
ColorPicker,
|
||||||
|
{ palette: palette.background, id: 'text-editor-background-color-picker' },
|
||||||
|
getEventPositionElement(event),
|
||||||
|
(val) => {
|
||||||
const color: string | undefined = val?.color
|
const color: string | undefined = val?.color
|
||||||
if (color === undefined) return
|
if (color === undefined) return
|
||||||
|
|
||||||
@ -75,23 +79,41 @@ export async function openBackgroundColorOptions (editor: Editor, event: MouseEv
|
|||||||
editor.commands.setBackgroundColor(color)
|
editor.commands.setBackgroundColor(color)
|
||||||
}
|
}
|
||||||
resolve()
|
resolve()
|
||||||
})
|
},
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
id: 'text-editor-background-color-picker',
|
||||||
|
category: 'popup',
|
||||||
|
overlay: true
|
||||||
|
}
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function openTextColorOptions (editor: Editor, event: MouseEvent): Promise<void> {
|
export async function openTextColorOptions (editor: Editor, event: MouseEvent): Promise<void> {
|
||||||
await new Promise<void>((resolve) => {
|
await new Promise<void>((resolve) => {
|
||||||
showPopup(ColorPicker, { palette: palette.text }, getEventPositionElement(event), (val) => {
|
showPopup(
|
||||||
|
ColorPicker,
|
||||||
|
{ palette: palette.text, letters: true },
|
||||||
|
getEventPositionElement(event),
|
||||||
|
(val) => {
|
||||||
const color: string | undefined = val?.color
|
const color: string | undefined = val?.color
|
||||||
if (color === undefined) return
|
if (color === undefined) return
|
||||||
|
|
||||||
if (color === 'var(--theme-text-color-primary)') {
|
if (color === 'var(--theme-text-primary-color)') {
|
||||||
editor.commands.unsetTextColor()
|
editor.commands.unsetTextColor()
|
||||||
} else {
|
} else {
|
||||||
editor.commands.setTextColor(color)
|
editor.commands.setTextColor(color)
|
||||||
}
|
}
|
||||||
resolve()
|
resolve()
|
||||||
})
|
},
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
id: 'text-editor-text-color-picker',
|
||||||
|
category: 'popup',
|
||||||
|
overlay: true
|
||||||
|
}
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,8 +13,10 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
import { type Editor } from '@tiptap/core'
|
import { isAtStartOfNode, isNodeActive, type Editor } from '@tiptap/core'
|
||||||
import ListKeymap, { type ListKeymapOptions, listHelpers } from '@tiptap/extension-list-keymap'
|
import ListKeymap, { listHelpers, type ListKeymapOptions } from '@tiptap/extension-list-keymap'
|
||||||
|
import { type ResolvedPos, type Node } from '@tiptap/pm/model'
|
||||||
|
import { type EditorState } from '@tiptap/pm/state'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Workaround for the original ListKeymap extension issue that
|
* Workaround for the original ListKeymap extension issue that
|
||||||
@ -33,7 +35,7 @@ export const ListKeymapExtension = ListKeymap.extend<ListKeymapOptions>({
|
|||||||
if (editor.state.schema.nodes[itemName] === undefined) {
|
if (editor.state.schema.nodes[itemName] === undefined) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (listHelpers.handleBackspace(editor, itemName, wrapperNames)) {
|
if (handleListItemBackspace(editor, itemName, wrapperNames)) {
|
||||||
handled = true
|
handled = true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -86,3 +88,144 @@ export const ListKeymapExtension = ListKeymap.extend<ListKeymapOptions>({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const handleListItemBackspace = (editor: Editor, name: string, parentListTypes: string[]): boolean => {
|
||||||
|
// this is required to still handle the undo handling
|
||||||
|
if (editor.commands.undoInputRule()) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the selection is not collapsed
|
||||||
|
// we can rely on the default backspace behavior
|
||||||
|
if (editor.state.selection.from !== editor.state.selection.to) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the current item is NOT inside a list item &
|
||||||
|
// the previous item is a list (orderedList or bulletList)
|
||||||
|
// move the cursor into the list and delete the current item
|
||||||
|
if (!isNodeActive(editor.state, name) && listHelpers.hasListBefore(editor.state, name, parentListTypes)) {
|
||||||
|
const { $anchor } = editor.state.selection
|
||||||
|
|
||||||
|
const $listPos = editor.state.doc.resolve($anchor.before() - 1)
|
||||||
|
|
||||||
|
const listDescendants: Array<{ node: Node, pos: number }> = []
|
||||||
|
|
||||||
|
$listPos.node().descendants((node, pos) => {
|
||||||
|
if (node.type.isInGroup('listItems')) {
|
||||||
|
listDescendants.push({ node, pos })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const lastItem = listDescendants.at(-1)
|
||||||
|
|
||||||
|
if (lastItem === undefined) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const $lastItemPos = editor.state.doc.resolve($listPos.start() + lastItem.pos + 1)
|
||||||
|
|
||||||
|
return editor
|
||||||
|
.chain()
|
||||||
|
.cut({ from: $anchor.start() - 1, to: $anchor.end() + 1 }, $lastItemPos.end())
|
||||||
|
.joinForward()
|
||||||
|
.run()
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the cursor is not inside the current node type
|
||||||
|
// do nothing and proceed
|
||||||
|
if (!isNodeActive(editor.state, name)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const $from = editor.state.selection.$from
|
||||||
|
const parentOffset = $from.depth > 0 ? $from.index($from.depth - 1) : 0
|
||||||
|
|
||||||
|
// if the cursor is not at the start of a node
|
||||||
|
// do nothing and proceed
|
||||||
|
if (!isAtStartOfNode(editor.state) || parentOffset > 0) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const listItemPos = findListItemPos(editor.state)
|
||||||
|
if (listItemPos === null) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const $prev = editor.state.doc.resolve(listItemPos.$pos.pos - 2)
|
||||||
|
const prevNode = $prev.node(listItemPos.depth)
|
||||||
|
|
||||||
|
const previousListItemHasSubList = listItemHasSubList(prevNode)
|
||||||
|
|
||||||
|
if (hasListItemBefore(editor.state)) {
|
||||||
|
// if the previous item is a list item and doesn't have a sublist, join the list items
|
||||||
|
if (!previousListItemHasSubList) {
|
||||||
|
return editor.commands.joinItemBackward()
|
||||||
|
} else {
|
||||||
|
return editor.chain().sinkListItem(name).joinItemBackward().run()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// otherwise in the end, a backspace should
|
||||||
|
// always just lift the list item if
|
||||||
|
// joining / merging is not possible
|
||||||
|
return editor.chain().liftListItem(name).run()
|
||||||
|
}
|
||||||
|
|
||||||
|
const findListItemPos = (state: EditorState): { $pos: ResolvedPos, depth: number } | null => {
|
||||||
|
const { $from } = state.selection
|
||||||
|
|
||||||
|
let currentNode = null
|
||||||
|
let currentDepth = $from.depth
|
||||||
|
let currentPos = $from.pos
|
||||||
|
let targetDepth: number | null = null
|
||||||
|
|
||||||
|
while (currentDepth > 0 && targetDepth === null) {
|
||||||
|
currentNode = $from.node(currentDepth)
|
||||||
|
|
||||||
|
if (currentNode.type.isInGroup('listItems')) {
|
||||||
|
targetDepth = currentDepth
|
||||||
|
} else {
|
||||||
|
currentDepth -= 1
|
||||||
|
currentPos -= 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetDepth === null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return { $pos: state.doc.resolve(currentPos), depth: targetDepth }
|
||||||
|
}
|
||||||
|
|
||||||
|
const listItemHasSubList = (node?: Node): boolean => {
|
||||||
|
if (node === undefined) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
let hasSubList = false
|
||||||
|
|
||||||
|
node.descendants((child) => {
|
||||||
|
if (child.type.isInGroup('listItems')) {
|
||||||
|
hasSubList = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return hasSubList
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasListItemBefore = (state: EditorState): boolean => {
|
||||||
|
const { $anchor } = state.selection
|
||||||
|
|
||||||
|
const $targetPos = state.doc.resolve($anchor.pos - 2)
|
||||||
|
|
||||||
|
if ($targetPos.index() === 0) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!($targetPos.nodeBefore?.type.isInGroup('listItems') ?? false)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
import { Card } from '@hcengineering/presentation'
|
import { Card } from '@hcengineering/presentation'
|
||||||
|
|
||||||
export let palette: Array<{ color: string, preview?: string }> = [{ color: 'transparent' }]
|
export let palette: Array<{ color: string, preview?: string }> = [{ color: 'transparent' }]
|
||||||
|
export let letters: boolean = false
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
@ -32,11 +33,15 @@
|
|||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
<div
|
<div
|
||||||
class="colorBox"
|
class="colorBox"
|
||||||
style:background-color={k.preview ?? k.color}
|
class:letters
|
||||||
|
class:solid={!letters}
|
||||||
|
style:--color={k.preview ?? k.color}
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
handleSubmit(k)
|
handleSubmit(k)
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
|
{#if letters}A{/if}
|
||||||
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -61,6 +66,20 @@
|
|||||||
height: 1.5rem;
|
height: 1.5rem;
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.solid {
|
||||||
|
background-color: var(--color);
|
||||||
box-shadow: var(--text-editor-color-picker-outline) 0px 0px 0px 1px inset;
|
box-shadow: var(--text-editor-color-picker-outline) 0px 0px 0px 1px inset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.letters {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: transparent;
|
||||||
|
border: 1px solid var(--color);
|
||||||
|
color: var(--color);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -9,7 +9,7 @@ import documents, {
|
|||||||
DocumentApprovalRequest,
|
DocumentApprovalRequest,
|
||||||
DocumentState,
|
DocumentState,
|
||||||
DocumentTemplate,
|
DocumentTemplate,
|
||||||
getEffectiveDocUpdate,
|
getEffectiveDocUpdates,
|
||||||
type DocumentRequest,
|
type DocumentRequest,
|
||||||
type DocumentTraining
|
type DocumentTraining
|
||||||
} from '@hcengineering/controlled-documents'
|
} from '@hcengineering/controlled-documents'
|
||||||
@ -54,8 +54,9 @@ async function getDocs (
|
|||||||
return allDocs.filter(predicate)
|
return allDocs.filter(predicate)
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeDocEffective (doc: ControlledDocument, txFactory: TxFactory): Tx {
|
function makeDocEffective (doc: ControlledDocument, txFactory: TxFactory): Tx[] {
|
||||||
return txFactory.createTxUpdateDoc(doc._class, doc.space, doc._id, getEffectiveDocUpdate())
|
const updates = getEffectiveDocUpdates()
|
||||||
|
return updates.map((u) => txFactory.createTxUpdateDoc(doc._class, doc.space, doc._id, u))
|
||||||
}
|
}
|
||||||
|
|
||||||
function archiveDocs (docs: ControlledDocument[], txFactory: TxFactory): Tx[] {
|
function archiveDocs (docs: ControlledDocument[], txFactory: TxFactory): Tx[] {
|
||||||
@ -339,7 +340,7 @@ export async function OnDocPlannedEffectiveDateChanged (
|
|||||||
if (tx.operations.plannedEffectiveDate === 0 && doc.controlledState === ControlledDocumentState.Approved) {
|
if (tx.operations.plannedEffectiveDate === 0 && doc.controlledState === ControlledDocumentState.Approved) {
|
||||||
// Create with not derived tx factory in order for notifications to work
|
// Create with not derived tx factory in order for notifications to work
|
||||||
const factory = new TxFactory(control.txFactory.account)
|
const factory = new TxFactory(control.txFactory.account)
|
||||||
await control.apply(control.ctx, [makeDocEffective(doc, factory)])
|
await control.apply(control.ctx, makeDocEffective(doc, factory))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -364,7 +365,7 @@ export async function OnDocApprovalRequestApproved (
|
|||||||
|
|
||||||
// Create with not derived tx factory in order for notifications to work
|
// Create with not derived tx factory in order for notifications to work
|
||||||
const factory = new TxFactory(control.txFactory.account)
|
const factory = new TxFactory(control.txFactory.account)
|
||||||
await control.apply(control.ctx, [makeDocEffective(doc, factory)])
|
await control.apply(control.ctx, makeDocEffective(doc, factory))
|
||||||
// make doc effective immediately
|
// make doc effective immediately
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
|
Loading…
Reference in New Issue
Block a user