Cherry Pick of recent changes (#8605)

Signed-off-by: Victor Ilyushchenko <alt13ri@gmail.com>
This commit is contained in:
Victor Ilyushchenko 2025-04-17 11:40:15 +03:00 committed by GitHub
parent 55d184c99e
commit 330b58d221
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 234 additions and 38 deletions

View File

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

View File

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

View File

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

View File

@ -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 }
} }
]
} }
/** /**

View File

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

View File

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

View File

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

View File

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