Linter
This commit is contained in:
parent
b3979008ae
commit
cc6eba7df1
16 changed files with 260 additions and 237 deletions
|
|
@ -119,9 +119,7 @@
|
||||||
onmouseenter={() => (hoveredIndex = index)}
|
onmouseenter={() => (hoveredIndex = index)}
|
||||||
onmouseleave={() => (hoveredIndex = null)}
|
onmouseleave={() => (hoveredIndex = null)}
|
||||||
>
|
>
|
||||||
<item.icon
|
<item.icon class="nav-icon {hoveredIndex === index ? 'animate' : ''}" />
|
||||||
class="nav-icon {hoveredIndex === index ? 'animate' : ''}"
|
|
||||||
/>
|
|
||||||
<span>{item.text}</span>
|
<span>{item.text}</span>
|
||||||
</a>
|
</a>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
|
||||||
|
|
@ -252,7 +252,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle link context menu
|
// Handle link context menu
|
||||||
const handleShowLinkContextMenu = (pos: number, url: string, coords: { x: number, y: number }) => {
|
const handleShowLinkContextMenu = (
|
||||||
|
pos: number,
|
||||||
|
url: string,
|
||||||
|
coords: { x: number; y: number }
|
||||||
|
) => {
|
||||||
if (!editor) return
|
if (!editor) return
|
||||||
|
|
||||||
linkContextMenuPosition = { x: coords.x, y: coords.y + 5 }
|
linkContextMenuPosition = { x: coords.x, y: coords.y + 5 }
|
||||||
|
|
@ -325,7 +329,13 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (showTextStyleDropdown || showMediaDropdown || showUrlConvertDropdown || showLinkContextMenu || showLinkEditDialog) {
|
if (
|
||||||
|
showTextStyleDropdown ||
|
||||||
|
showMediaDropdown ||
|
||||||
|
showUrlConvertDropdown ||
|
||||||
|
showLinkContextMenu ||
|
||||||
|
showLinkEditDialog
|
||||||
|
) {
|
||||||
document.addEventListener('click', handleClickOutside)
|
document.addEventListener('click', handleClickOutside)
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('click', handleClickOutside)
|
document.removeEventListener('click', handleClickOutside)
|
||||||
|
|
@ -484,9 +494,9 @@
|
||||||
// Dismiss URL convert dropdown if user types
|
// Dismiss URL convert dropdown if user types
|
||||||
if (showUrlConvertDropdown && transaction.docChanged) {
|
if (showUrlConvertDropdown && transaction.docChanged) {
|
||||||
// Check if the change is actual typing (not just cursor movement)
|
// Check if the change is actual typing (not just cursor movement)
|
||||||
const hasTextChange = transaction.steps.some(step =>
|
const hasTextChange = transaction.steps.some(
|
||||||
step.toJSON().stepType === 'replace' ||
|
(step) =>
|
||||||
step.toJSON().stepType === 'replaceAround'
|
step.toJSON().stepType === 'replace' || step.toJSON().stepType === 'replaceAround'
|
||||||
)
|
)
|
||||||
if (hasTextChange) {
|
if (hasTextChange) {
|
||||||
showUrlConvertDropdown = false
|
showUrlConvertDropdown = false
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { Extension } from '@tiptap/core'
|
||||||
import { Plugin, PluginKey } from '@tiptap/pm/state'
|
import { Plugin, PluginKey } from '@tiptap/pm/state'
|
||||||
|
|
||||||
export interface LinkContextMenuOptions {
|
export interface LinkContextMenuOptions {
|
||||||
onShowContextMenu?: (pos: number, url: string, coords: { x: number, y: number }) => void
|
onShowContextMenu?: (pos: number, url: string, coords: { x: number; y: number }) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LinkContextMenu = Extension.create<LinkContextMenuOptions>({
|
export const LinkContextMenu = Extension.create<LinkContextMenuOptions>({
|
||||||
|
|
@ -30,7 +30,7 @@ export const LinkContextMenu = Extension.create<LinkContextMenuOptions>({
|
||||||
|
|
||||||
const $pos = state.doc.resolve(pos.pos)
|
const $pos = state.doc.resolve(pos.pos)
|
||||||
const marks = $pos.marks()
|
const marks = $pos.marks()
|
||||||
const linkMark = marks.find(mark => mark.type.name === 'link')
|
const linkMark = marks.find((mark) => mark.type.name === 'link')
|
||||||
|
|
||||||
if (linkMark && linkMark.attrs.href) {
|
if (linkMark && linkMark.attrs.href) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,10 @@ export const UrlEmbed = Node.create<UrlEmbedOptions>({
|
||||||
},
|
},
|
||||||
|
|
||||||
renderHTML({ HTMLAttributes }) {
|
renderHTML({ HTMLAttributes }) {
|
||||||
return ['div', mergeAttributes({ 'data-url-embed': '' }, this.options.HTMLAttributes, HTMLAttributes)]
|
return [
|
||||||
|
'div',
|
||||||
|
mergeAttributes({ 'data-url-embed': '' }, this.options.HTMLAttributes, HTMLAttributes)
|
||||||
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
addCommands() {
|
addCommands() {
|
||||||
|
|
@ -106,7 +109,7 @@ export const UrlEmbed = Node.create<UrlEmbedOptions>({
|
||||||
// Find the link mark at the given position
|
// Find the link mark at the given position
|
||||||
const $pos = doc.resolve(pos)
|
const $pos = doc.resolve(pos)
|
||||||
const marks = $pos.marks()
|
const marks = $pos.marks()
|
||||||
const linkMark = marks.find(mark => mark.type.name === 'link')
|
const linkMark = marks.find((mark) => mark.type.name === 'link')
|
||||||
|
|
||||||
if (!linkMark) return false
|
if (!linkMark) return false
|
||||||
|
|
||||||
|
|
@ -119,14 +122,20 @@ export const UrlEmbed = Node.create<UrlEmbedOptions>({
|
||||||
|
|
||||||
// Walk backwards to find the start
|
// Walk backwards to find the start
|
||||||
doc.nodesBetween(Math.max(0, pos - 300), pos, (node, nodePos) => {
|
doc.nodesBetween(Math.max(0, pos - 300), pos, (node, nodePos) => {
|
||||||
if (node.isText && node.marks.some(m => m.type.name === 'link' && m.attrs.href === url)) {
|
if (
|
||||||
|
node.isText &&
|
||||||
|
node.marks.some((m) => m.type.name === 'link' && m.attrs.href === url)
|
||||||
|
) {
|
||||||
from = nodePos
|
from = nodePos
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Walk forwards to find the end
|
// Walk forwards to find the end
|
||||||
doc.nodesBetween(pos, Math.min(doc.content.size, pos + 300), (node, nodePos) => {
|
doc.nodesBetween(pos, Math.min(doc.content.size, pos + 300), (node, nodePos) => {
|
||||||
if (node.isText && node.marks.some(m => m.type.name === 'link' && m.attrs.href === url)) {
|
if (
|
||||||
|
node.isText &&
|
||||||
|
node.marks.some((m) => m.type.name === 'link' && m.attrs.href === url)
|
||||||
|
) {
|
||||||
to = nodePos + node.nodeSize
|
to = nodePos + node.nodeSize
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -179,7 +188,8 @@ export const UrlEmbed = Node.create<UrlEmbedOptions>({
|
||||||
// Check if it's a plain text paste
|
// Check if it's a plain text paste
|
||||||
if (text && !html) {
|
if (text && !html) {
|
||||||
// Simple URL regex check
|
// Simple URL regex check
|
||||||
const urlRegex = /^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$/
|
const urlRegex =
|
||||||
|
/^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$/
|
||||||
|
|
||||||
if (urlRegex.test(text.trim())) {
|
if (urlRegex.test(text.trim())) {
|
||||||
// It's a URL, let it paste as a link naturally (don't prevent default)
|
// It's a URL, let it paste as a link naturally (don't prevent default)
|
||||||
|
|
@ -199,20 +209,32 @@ export const UrlEmbed = Node.create<UrlEmbedOptions>({
|
||||||
let linkEnd = -1
|
let linkEnd = -1
|
||||||
|
|
||||||
// Search for the link in a reasonable range
|
// Search for the link in a reasonable range
|
||||||
for (let pos = beforePos; pos < Math.min(doc.content.size, beforePos + pastedUrl.length + 10); pos++) {
|
for (
|
||||||
|
let pos = beforePos;
|
||||||
|
pos < Math.min(doc.content.size, beforePos + pastedUrl.length + 10);
|
||||||
|
pos++
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
const $pos = doc.resolve(pos)
|
const $pos = doc.resolve(pos)
|
||||||
const marks = $pos.marks()
|
const marks = $pos.marks()
|
||||||
const linkMark = marks.find(m => m.type.name === 'link' && m.attrs.href === pastedUrl)
|
const linkMark = marks.find(
|
||||||
|
(m) => m.type.name === 'link' && m.attrs.href === pastedUrl
|
||||||
|
)
|
||||||
|
|
||||||
if (linkMark) {
|
if (linkMark) {
|
||||||
// Found the link, now find its boundaries
|
// Found the link, now find its boundaries
|
||||||
linkStart = pos
|
linkStart = pos
|
||||||
|
|
||||||
// Find the end of the link
|
// Find the end of the link
|
||||||
for (let endPos = pos; endPos < Math.min(doc.content.size, pos + pastedUrl.length + 5); endPos++) {
|
for (
|
||||||
|
let endPos = pos;
|
||||||
|
endPos < Math.min(doc.content.size, pos + pastedUrl.length + 5);
|
||||||
|
endPos++
|
||||||
|
) {
|
||||||
const $endPos = doc.resolve(endPos)
|
const $endPos = doc.resolve(endPos)
|
||||||
const hasLink = $endPos.marks().some(m => m.type.name === 'link' && m.attrs.href === pastedUrl)
|
const hasLink = $endPos
|
||||||
|
.marks()
|
||||||
|
.some((m) => m.type.name === 'link' && m.attrs.href === pastedUrl)
|
||||||
if (hasLink) {
|
if (hasLink) {
|
||||||
linkEnd = endPos + 1
|
linkEnd = endPos + 1
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,17 @@
|
||||||
onDismiss: () => void
|
onDismiss: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
let { x, y, url, onConvertToLink, onCopyLink, onRefresh, onOpenLink, onRemove, onDismiss }: Props = $props()
|
let {
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
url,
|
||||||
|
onConvertToLink,
|
||||||
|
onCopyLink,
|
||||||
|
onRefresh,
|
||||||
|
onOpenLink,
|
||||||
|
onRemove,
|
||||||
|
onDismiss
|
||||||
|
}: Props = $props()
|
||||||
|
|
||||||
let dropdown: HTMLDivElement
|
let dropdown: HTMLDivElement
|
||||||
|
|
||||||
|
|
@ -52,27 +62,17 @@
|
||||||
<div class="menu-url">{url}</div>
|
<div class="menu-url">{url}</div>
|
||||||
<div class="menu-divider"></div>
|
<div class="menu-divider"></div>
|
||||||
|
|
||||||
<button class="menu-item" onclick={onOpenLink}>
|
<button class="menu-item" onclick={onOpenLink}> Open link </button>
|
||||||
Open link
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button class="menu-item" onclick={onCopyLink}>
|
<button class="menu-item" onclick={onCopyLink}> Copy link </button>
|
||||||
Copy link
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button class="menu-item" onclick={onRefresh}>
|
<button class="menu-item" onclick={onRefresh}> Refresh preview </button>
|
||||||
Refresh preview
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button class="menu-item" onclick={onConvertToLink}>
|
<button class="menu-item" onclick={onConvertToLink}> Convert to link </button>
|
||||||
Convert to link
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div class="menu-divider"></div>
|
<div class="menu-divider"></div>
|
||||||
|
|
||||||
<button class="menu-item danger" onclick={onRemove}>
|
<button class="menu-item danger" onclick={onRemove}> Remove card </button>
|
||||||
Remove card
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,17 @@
|
||||||
onDismiss: () => void
|
onDismiss: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
let { x, y, url, onConvertToCard, onEditLink, onCopyLink, onRemoveLink, onOpenLink, onDismiss }: Props = $props()
|
let {
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
url,
|
||||||
|
onConvertToCard,
|
||||||
|
onEditLink,
|
||||||
|
onCopyLink,
|
||||||
|
onRemoveLink,
|
||||||
|
onOpenLink,
|
||||||
|
onDismiss
|
||||||
|
}: Props = $props()
|
||||||
|
|
||||||
let dropdown: HTMLDivElement
|
let dropdown: HTMLDivElement
|
||||||
|
|
||||||
|
|
@ -52,27 +62,17 @@
|
||||||
<div class="menu-url">{url}</div>
|
<div class="menu-url">{url}</div>
|
||||||
<div class="menu-divider"></div>
|
<div class="menu-divider"></div>
|
||||||
|
|
||||||
<button class="menu-item" onclick={onOpenLink}>
|
<button class="menu-item" onclick={onOpenLink}> Open link </button>
|
||||||
Open link
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button class="menu-item" onclick={onEditLink}>
|
<button class="menu-item" onclick={onEditLink}> Edit link </button>
|
||||||
Edit link
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button class="menu-item" onclick={onCopyLink}>
|
<button class="menu-item" onclick={onCopyLink}> Copy link </button>
|
||||||
Copy link
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button class="menu-item" onclick={onConvertToCard}>
|
<button class="menu-item" onclick={onConvertToCard}> Convert to card </button>
|
||||||
Convert to card
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div class="menu-divider"></div>
|
<div class="menu-divider"></div>
|
||||||
|
|
||||||
<button class="menu-item danger" onclick={onRemoveLink}>
|
<button class="menu-item danger" onclick={onRemoveLink}> Remove link </button>
|
||||||
Remove link
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|
|
||||||
|
|
@ -79,19 +79,10 @@
|
||||||
class:invalid={urlInput && !isValid}
|
class:invalid={urlInput && !isValid}
|
||||||
/>
|
/>
|
||||||
<div class="dialog-actions">
|
<div class="dialog-actions">
|
||||||
<button
|
<button class="action-button save" onclick={handleSave} disabled={!isValid} title="Save">
|
||||||
class="action-button save"
|
|
||||||
onclick={handleSave}
|
|
||||||
disabled={!isValid}
|
|
||||||
title="Save"
|
|
||||||
>
|
|
||||||
<Check />
|
<Check />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button class="action-button cancel" onclick={onCancel} title="Cancel">
|
||||||
class="action-button cancel"
|
|
||||||
onclick={onCancel}
|
|
||||||
title="Cancel"
|
|
||||||
>
|
|
||||||
<X />
|
<X />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -53,9 +53,7 @@
|
||||||
transition:fly={{ y: -10, duration: 200 }}
|
transition:fly={{ y: -10, duration: 200 }}
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
>
|
>
|
||||||
<button class="convert-button" onclick={handleConvert}>
|
<button class="convert-button" onclick={handleConvert}> Convert to card </button>
|
||||||
Convert to card
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|
|
||||||
|
|
@ -33,25 +33,22 @@
|
||||||
editor
|
editor
|
||||||
.chain()
|
.chain()
|
||||||
.focus()
|
.focus()
|
||||||
.insertContentAt(
|
.insertContentAt({ from: pos, to: pos + node.nodeSize }, [
|
||||||
{ from: pos, to: pos + node.nodeSize },
|
{
|
||||||
[
|
type: 'urlEmbed',
|
||||||
{
|
attrs: {
|
||||||
type: 'urlEmbed',
|
url: url,
|
||||||
attrs: {
|
title: metadata.title,
|
||||||
url: url,
|
description: metadata.description,
|
||||||
title: metadata.title,
|
image: metadata.image,
|
||||||
description: metadata.description,
|
favicon: metadata.favicon,
|
||||||
image: metadata.image,
|
siteName: metadata.siteName
|
||||||
favicon: metadata.favicon,
|
|
||||||
siteName: metadata.siteName
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'paragraph'
|
|
||||||
}
|
}
|
||||||
]
|
},
|
||||||
)
|
{
|
||||||
|
type: 'paragraph'
|
||||||
|
}
|
||||||
|
])
|
||||||
.run()
|
.run()
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -126,7 +123,13 @@
|
||||||
<AlertCircle class="placeholder-icon" />
|
<AlertCircle class="placeholder-icon" />
|
||||||
<div class="error-content">
|
<div class="error-content">
|
||||||
<span class="placeholder-text">{errorMessage}</span>
|
<span class="placeholder-text">{errorMessage}</span>
|
||||||
<button onclick={() => { showInput = true; error = false; }} class="retry-button">
|
<button
|
||||||
|
onclick={() => {
|
||||||
|
showInput = true
|
||||||
|
error = false
|
||||||
|
}}
|
||||||
|
class="retry-button"
|
||||||
|
>
|
||||||
Try another URL
|
Try another URL
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -201,7 +201,8 @@ function renderTiptapContent(doc: any): string {
|
||||||
embedHtml += '<div class="youtube-embed-wrapper">'
|
embedHtml += '<div class="youtube-embed-wrapper">'
|
||||||
embedHtml += `<iframe src="https://www.youtube.com/embed/${videoId}" `
|
embedHtml += `<iframe src="https://www.youtube.com/embed/${videoId}" `
|
||||||
embedHtml += 'frameborder="0" '
|
embedHtml += 'frameborder="0" '
|
||||||
embedHtml += 'allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" '
|
embedHtml +=
|
||||||
|
'allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" '
|
||||||
embedHtml += 'allowfullscreen>'
|
embedHtml += 'allowfullscreen>'
|
||||||
embedHtml += '</iframe>'
|
embedHtml += '</iframe>'
|
||||||
embedHtml += '</div>'
|
embedHtml += '</div>'
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue