Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
710b350
refactor(links): move linkBubble plugin to plugins/links
max-nextcloud Mar 15, 2024
e6049b0
refactor(links): pass editor via options to LinkBubblePluginView
max-nextcloud Mar 15, 2024
d871277
refactor(links): untangle LinkBubblePluginView from editor
max-nextcloud Mar 15, 2024
e4340f8
refactor(links): use prosemirror tr and state to track clicks
max-nextcloud Mar 17, 2024
90b99be
refactor(links): move click handling into link bubble plugin
max-nextcloud Mar 17, 2024
356509b
refactor(links): only hand link mark to updateTooltip
max-nextcloud Mar 17, 2024
e30c78c
refactor(links): introduce setActiveLink function
max-nextcloud Mar 18, 2024
d956fdd
fix(links): also update if active was unset
max-nextcloud Mar 18, 2024
e02c5ad
refactor(links): operate on state with `linkNodeFromSelection`
max-nextcloud Mar 18, 2024
c1e4bd8
refactor(links): move linkNodeFromSelection into helper
max-nextcloud Mar 18, 2024
96097aa
refactor(links): handle selection changes in plugin
max-nextcloud Mar 18, 2024
4a3165b
refactor(links): use hideLinkBubble command for esc
max-nextcloud Mar 19, 2024
ed923f4
fix(links): simplify updateTooltip and handle active null
max-nextcloud Mar 19, 2024
4bc7586
fix(links): remove special handling for clicked and focus
max-nextcloud Mar 19, 2024
7fe10f9
fix(links): handle esc in DOMEvents rather than prop
max-nextcloud Mar 19, 2024
d79b67c
fix(links) handle linkNodeFromSelection returning false
max-nextcloud Mar 19, 2024
4f5277e
enh(links): close link bubble when opening preview toggle
max-nextcloud Mar 19, 2024
d582f38
fix(lint): add missing jsdoc comments to new files
max-nextcloud Mar 19, 2024
322a752
docs(preview): improve jsdocs for markdownit preview plugin
max-nextcloud Mar 19, 2024
8e78cb7
refactor(links): only get mark from selection
max-nextcloud Mar 20, 2024
385b246
refactor(links): get active link from selection
max-nextcloud Mar 20, 2024
c2fd1e5
fix(links): show bubble with cursor on the end of the link
max-nextcloud Mar 20, 2024
fb0a8ce
tests(links): ignore link bubble when reading link text
max-nextcloud Mar 20, 2024
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
Prev Previous commit
Next Next commit
refactor(links): introduce setActiveLink function
Also rename `clicked` state to `active`.
We will also use it for activating via keypress

Signed-off-by: Max <[email protected]>
  • Loading branch information
max-nextcloud committed Mar 20, 2024
commit e30c78c3398dd249368dabcaaee7fe9e62a33227
8 changes: 1 addition & 7 deletions src/extensions/LinkBubble.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,9 @@ import { linkBubble } from '../plugins/links.js'
const LinkBubble = Extension.create({
name: 'linkViewBubble',

addOptions() {
return {
pluginKey: 'linkViewBubble',
}
},

addProseMirrorPlugins() {
return [
linkBubble(this.options.pluginKey, {
linkBubble({
editor: this.editor,
parent: this.editor.contentComponent,
}),
Expand Down
20 changes: 10 additions & 10 deletions src/plugins/LinkBubblePluginView.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,19 +69,19 @@ class LinkBubblePluginView {
}

update(view, oldState) {
const clicked = this.clickedChanged(view, oldState)
if (clicked) {
this.updateFromClick(view, clicked)
const active = this.activeChanged(view, oldState)
if (active) {
this.updateFromClick(view, active)
} else if (this.selectionUpdated(view, oldState)) {
this.updateFromSelection(view)
}
}

clickedChanged(view, oldState) {
const { clicked } = this.plugin.getState(view.state)
const { clicked: oldClicked } = this.plugin.getState(oldState)
if (clicked !== oldClicked) {
return clicked
activeChanged(view, oldState) {
const { active } = this.plugin.getState(view.state)
const { active: oldActive } = this.plugin.getState(oldState)
if (active !== oldActive) {
return active
}
}

Expand Down Expand Up @@ -117,8 +117,8 @@ class LinkBubblePluginView {
this.updateTooltip(view, shouldShow, mark, nodeStart)
}, 250)

updateFromClick(view, clicked) {
this.updateTooltip(this.view, !!clicked.mark, clicked.mark, clicked.nodeStart)
updateFromClick(view, active) {
this.updateTooltip(this.view, !!active.mark, active.mark, active.nodeStart)
}

updateTooltip(view, shouldShow, mark, nodeStart) {
Expand Down
41 changes: 29 additions & 12 deletions src/plugins/links.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,36 @@
import { Plugin, PluginKey } from '@tiptap/pm/state'
import LinkBubblePluginView from './LinkBubblePluginView.js'

export function linkBubble(pluginKey, options) {
// Commands

/* Set resolved to be the active element (if it has a link mark)
*
* @params {ResolvedPos} resolved position of the action
*/
export const setActiveLink = (resolved) => (state, dispatch) => {
const mark = resolved.marks()
.find(m => m.type.name === 'link')
if (!mark) {
return false
}
const nodeStart = resolved.pos - resolved.textOffset
const active = { mark, nodeStart }
if (dispatch) {
dispatch(state.tr.setMeta(linkBubbleKey, { active }))
}
return true
}

export const linkBubbleKey = new PluginKey('linkBubble')
export function linkBubble(options) {
const linkBubblePlugin = new Plugin({
key: new PluginKey(pluginKey),
key: linkBubbleKey,
state: {
init: () => ({ clicked: null }),
init: () => ({ active: null }),
apply: (tr, cur) => {
const meta = tr.getMeta(linkBubblePlugin)
if (meta?.clicked) {
return { clicked: meta.clicked }
const meta = tr.getMeta(linkBubbleKey)
if (meta?.active) {
return { active: meta.active }
} else {
return cur
}
Expand All @@ -55,13 +76,9 @@ export function linkBubble(pluginKey, options) {
|| event.metaKey) {
return false
}
const { dispatch, state } = view
const { state, dispatch } = view
const resolved = state.doc.resolve(pos)
const mark = resolved.marks()
.find(m => m.type.name === 'link')
const nodeStart = resolved.pos - resolved.textOffset
const clicked = { mark, nodeStart }
dispatch(state.tr.setMeta(linkBubblePlugin, { clicked }))
setActiveLink(resolved)(state, dispatch, view)
},
},

Expand Down
49 changes: 41 additions & 8 deletions src/tests/plugins/linkBubble.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
*
*/

import { linkBubble } from '../../plugins/links.js'
import { linkBubble, setActiveLink } from '../../plugins/links.js'
import { Plugin, EditorState } from '@tiptap/pm/state'
import { schema } from '@tiptap/pm/schema-basic'

Expand All @@ -32,23 +32,56 @@ describe('linkBubble prosemirror plugin', () => {
})

test('usage as plugin', () => {
const plugin = new linkBubble('linkBubble')
const plugin = new linkBubble()
const state = createState({ plugins: [ plugin ] })
expect(state.plugins).toContain(plugin)
expect(plugin.getState(state)).toEqual({"clicked": null})
expect(plugin.getState(state)).toEqual({"active": null})
})

test('updates plugin state clicked on transaction', () => {
const plugin = new linkBubble('linkBubble')
test('updates plugin state active on transaction', () => {
const plugin = new linkBubble()
const state = createState({ plugins: [ plugin ] })
const dummy = { was: 'clicked' }
const tr = state.tr.setMeta(plugin, { clicked: dummy })
const dummy = { was: 'active' }
const tr = state.tr.setMeta(plugin, { active: dummy })
const after = state.apply(tr)
expect(plugin.getState(after)).toEqual({"clicked": dummy})
expect(plugin.getState(after)).toEqual({"active": dummy})
})

test('setActiveLink requires a link mark', () => {
const noMarks = { marks: () => [] }
expect(setActiveLink(noMarks)(null, null)).toBe(false)
const otherMark = { marks: () => [{type: {name: 'other'}}] }
expect(setActiveLink(otherMark)(null, null)).toBe(false)
const mark = { marks: () => [{type: {name: 'link'}}] }
expect(setActiveLink(mark)(null, null)).toBe(true)
})

test('setActiveLink extracts the link mark', () => {
const plugin = new linkBubble()
const state = createState({ plugins: [ plugin ] })
const flow = createFlow(state)
const mark = { type: { name: 'link' } }
const resolved = { marks: () => [mark] }
setActiveLink(resolved)(flow.state, flow.dispatch)
expect(plugin.getState(flow.state).active.mark)
.toEqual(mark)
})

})

// simulate the data flow in prosemirror
function createFlow(initialState) {
let state = initialState
return {
get state() {
return state
},
dispatch: tr => {
state = state.apply(tr)
},
}
}

function createState(options = {}) {
return EditorState.create({
schema,
Expand Down