Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions foundations/core/packages/text/src/kits/common-kit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export const CommonKitFactory = (e: ExtensionFactory) =>
blockquote: e(Blockquote, { HTMLAttributes: { class: 'proseBlockQuote' } }),
link: e(Link.extend({ inclusive: false }), {
openOnClick: false,
protocols: ['ref'],
HTMLAttributes: { class: 'cursor-pointer', rel: 'noopener noreferrer', target: '_blank' }
}),
textAlign: e(TextAlign, {
Expand Down
1 change: 1 addition & 0 deletions plugins/text-editor-resources/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@
"@hcengineering/chunter": "workspace:^0.7.0",
"@tiptap/extension-text-align": "~2.11.0",
"@hcengineering/workbench": "workspace:^0.7.0",
"@hcengineering/document": "workspace:^0.7.0",
"@hcengineering/drive": "workspace:^0.7.0",
"@hcengineering/time": "workspace:^0.7.0",
"@hcengineering/rank": "workspace:^0.7.17"
Expand Down
157 changes: 140 additions & 17 deletions plugins/text-editor-resources/src/components/LinkPopup.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -13,33 +13,156 @@
// limitations under the License.
-->
<script lang="ts">
import { createEventDispatcher } from 'svelte'
import { type Ref, SortingOrder } from '@hcengineering/core'
import { createEventDispatcher, onDestroy } from 'svelte'
import textEditor from '@hcengineering/text-editor'
import { getEmbeddedLabel } from '@hcengineering/platform'
import { Card } from '@hcengineering/presentation'
import { EditBox } from '@hcengineering/ui'
import presentation, { Card, getClient } from '@hcengineering/presentation'
import { EditBox, Label, ListView } from '@hcengineering/ui'
import { buildReferenceUrl } from './extension/reference'
import document, { type Document } from '@hcengineering/document'

export let link = ''

const dispatch = createEventDispatcher()
const linkPlaceholder = getEmbeddedLabel('http://my.link.net')
const client = getClient()
const linkPlaceholder = getEmbeddedLabel('URL or document name')

let items: Document[] = []
let list: ListView
let selection = 0
let searchQuery = ''
let debounceTimer: any

function isUrl (text: string): boolean {
if (text.length === 0) return false
return text.includes('://') || text.startsWith('http') || text.startsWith('www.')
}

function save (): void {
dispatch('update', link)
}

$: canSave = link === '' || URL.canParse(link)
function selectItem (doc: Document): void {
const refUrl = buildReferenceUrl({
id: doc._id,
objectclass: doc._class,
label: doc.title
})
if (refUrl !== undefined) {
link = refUrl
}
}

async function doSearch (query: string): Promise<void> {
if (query.length === 0 || isUrl(query)) {
items = []
return
}
try {
const r = await client.findAll(
document.class.Document,
{ title: { $like: `%${query}%` } },
{ limit: 10, sort: { title: SortingOrder.Ascending } }
)
if (query === searchQuery) {
items = r
selection = 0
}
} catch (e) {
console.error('LinkPopup search error:', e)
items = []
}
}

function onInput (value: string): void {
searchQuery = value
clearTimeout(debounceTimer)
if (value.length === 0 || isUrl(value) || value.startsWith('ref://')) {
items = []
return
}
debounceTimer = setTimeout(() => {
void doSearch(value)
}, 200)
}

$: onInput(link)

onDestroy(() => {
clearTimeout(debounceTimer)
})

function handleKeydown (event: KeyboardEvent): void {
if (items.length === 0) return
if (event.key === 'ArrowDown') {
event.preventDefault()
event.stopPropagation()
selection = Math.min(selection + 1, items.length - 1)
list?.select(selection)
} else if (event.key === 'ArrowUp') {
event.preventDefault()
event.stopPropagation()
selection = Math.max(selection - 1, 0)
list?.select(selection)
} else if (event.key === 'Enter') {
event.preventDefault()
event.stopPropagation()
selectItem(items[selection])
}
}

$: canSave = link === '' || link.startsWith('ref://') || URL.canParse(link)
</script>

<Card
label={textEditor.string.Link}
okLabel={textEditor.string.Save}
okAction={save}
{canSave}
on:close={() => {
dispatch('close')
}}
on:changeContent
>
<EditBox placeholder={linkPlaceholder} bind:value={link} autoFocus />
</Card>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div on:keydown|capture={handleKeydown}>
<Card
label={textEditor.string.Link}
okLabel={textEditor.string.Save}
okAction={save}
{canSave}
on:close={() => {
dispatch('close')
}}
on:changeContent
>
<EditBox placeholder={linkPlaceholder} bind:value={link} autoFocus />
{#if items.length > 0}
<div class="searchResults">
<ListView bind:this={list} bind:selection count={items.length}>
<svelte:fragment slot="item" let:item={num}>
{@const item = items[num]}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="ap-menuItem withComp h-8"
style="padding-left: 0.75rem; display: flex; align-items: center;"
on:click={() => {
selectItem(item)
}}
>
<span class="overflow-label">{item.title}</span>
</div>
</svelte:fragment>
</ListView>
</div>
{:else if searchQuery.length > 0 && !isUrl(searchQuery) && !searchQuery.startsWith('ref://')}
<div class="noResults"><Label label={presentation.string.NoResults} /></div>
{/if}
</Card>
</div>

<style lang="scss">
.searchResults {
max-height: 15rem;
overflow-y: auto;
margin-top: 0.5rem;
}

.noResults {
display: flex;
padding: 0.25rem 0;
color: var(--theme-dark-color);
}
</style>
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,14 @@
// limitations under the License.
//

import { getResource } from '@hcengineering/platform'
import { showPopup } from '@hcengineering/ui'
import viewPlugin from '@hcengineering/view'
import { Extension } from '@tiptap/core'
import { type MarkType } from '@tiptap/pm/model'
import { Plugin, PluginKey } from '@tiptap/pm/state'
import LinkPopup from '../../LinkPopup.svelte'
import { parseReferenceUrl } from '../reference'

export const LinkKeymapExtension = Extension.create<any>({
name: 'linkUtils',
Expand Down Expand Up @@ -60,7 +63,17 @@ export function LinkClickHandlerPlugin (options: LinkClickHandlerPluginOptions):
const $pos = view.state.doc.resolve(pos)
const link = options.type.isInSet($pos.marks())
if (typeof link?.attrs.href === 'string') {
window.open(link.attrs.href, link.attrs.target)
const href = link.attrs.href
if (href.startsWith('ref://')) {
const ref = parseReferenceUrl(href)
if (ref !== undefined) {
void getResource(viewPlugin.function.OpenDocument).then((openDoc) => {
void openDoc?.(ref.objectclass, ref.id)
})
return true
}
}
window.open(href, link.attrs.target)
return true
}

Expand Down
Loading