11import { Footnote , Footnotes as BaseFootnotes , FootnoteReference } from "tiptap-footnotes" ;
22import { Plugin , PluginKey } from "@tiptap/pm/state" ;
3- import { Extension } from "@tiptap/core" ;
3+ import { Decoration , DecorationSet } from "@tiptap/pm/view" ;
4+ import { Extension , mergeAttributes , NodePos } from "@tiptap/core" ;
45
56export const Footnotes = Extension . create ( {
7+ name : 'footnotesExtension' ,
68 addExtensions ( ) {
79 return [
810 BaseFootnotes ,
911 Footnote . extend ( {
12+ addProseMirrorPlugins ( ) {
13+ const editor = this . editor ;
14+ return [
15+ new Plugin ( {
16+ key : new PluginKey ( 'footnoteDecoration' ) ,
17+ props : {
18+ decorations : ( state ) => {
19+ const decorations : Decoration [ ] = [ ] ;
20+ state . doc . descendants ( ( node , pos ) => {
21+ if ( node . type . name === 'footnote' ) {
22+ const id = node . attrs . id ;
23+ const refId = id ?. startsWith ( 'fn:' ) ? id . replace ( 'fn:' , '' ) : id ;
24+ decorations . push (
25+ Decoration . widget ( pos + 1 , ( ) => {
26+ const sup = document . createElement ( 'sup' ) ;
27+ const a = document . createElement ( 'a' ) ;
28+ a . href = `#fnref:${ refId } ` ;
29+ a . draggable = false ;
30+ a . className = 'underline underline-offset-4 decoration-foreground/20 hover:decoration-foreground select-none' ;
31+ a . textContent = '[^]' ;
32+ sup . className = 'footnote-backref absolute left-[.125em] top-1.5'
33+ sup . appendChild ( a ) ;
34+ return sup ;
35+ } )
36+ ) ;
37+ }
38+ } ) ;
39+ return DecorationSet . create ( state . doc , decorations ) ;
40+ } ,
41+ handleClickOn ( view , pos , node , nodePos , event ) {
42+ if ( ( event . target as HTMLElement ) ?. closest ( '.footnote-backref' ) ) {
43+ event . preventDefault ( ) ;
44+ const id = ( event . target as HTMLElement ) . closest ( '[data-id]' ) . getAttribute ( 'data-id' ) ;
45+ setTimeout ( ( ) => editor . commands . focusFootnoteReference ( id ) ) ;
46+ return true ;
47+ }
48+ } ,
49+ } ,
50+ } ) ,
51+ ] ;
52+ } ,
1053 addCommands ( ) {
1154 return {
1255 focusFootnote : ( id : string ) => ( { editor, chain } ) => {
1356 const matchedFootnote = editor . $node ( "footnote" , {
1457 "data-id" : id ,
1558 } ) ;
59+
1660 if ( matchedFootnote ) {
1761 // sets the text selection to the end of the footnote definition and scroll to it.
1862 chain ( )
@@ -32,6 +76,35 @@ export const Footnotes = Extension.create({
3276 } ) ,
3377 FootnoteReference
3478 . extend ( {
79+ addCommands ( ) {
80+ return {
81+ ...this . parent ( ) ,
82+ focusFootnoteReference : ( id : string ) => ( { editor, chain } ) => {
83+ let matchedFootnoteReference : { from : number } = null ;
84+ editor . state . doc . descendants ( ( node , pos ) => {
85+ if ( node . type . name === 'footnoteReference' && node . attrs [ 'data-id' ] === id ) {
86+ matchedFootnoteReference = {
87+ from : pos ,
88+ }
89+ return false ;
90+ }
91+ } ) ;
92+
93+ if ( matchedFootnoteReference ) {
94+ chain ( )
95+ . focus ( )
96+ . setTextSelection (
97+ matchedFootnoteReference . from ,
98+ )
99+ . run ( ) ;
100+
101+ editor . view . dom . querySelector ( `.footnote-ref[data-id="${ id } "]` ) . scrollIntoView ( { block : 'nearest' } ) ;
102+ return true ;
103+ }
104+ return false ;
105+ } ,
106+ } ;
107+ } ,
35108 addProseMirrorPlugins ( ) {
36109 const editor = this . editor ;
37110 return [
@@ -41,16 +114,17 @@ export const Footnotes = Extension.create({
41114 props : {
42115 handleDOMEvents : {
43116 click ( view , event ) {
44- if ( event . target ?. closest ( '.footnote-ref' ) ) {
117+ if ( ( event . target as HTMLElement ) ?. closest ( '.footnote-ref' ) ) {
45118 event . preventDefault ( ) ;
46119 }
47120 }
48121 } ,
49122 handleClickOn ( view , pos , node , nodePos , event ) {
50- // event.stopImmediatePropagation();
51- const id = node . attrs [ "data-id" ] ;
52- console . log ( event ) ;
53- setTimeout ( ( ) => editor . commands . focusFootnote ( id ) ) ;
123+ if ( node . type . name === 'footnoteReference' ) {
124+ const id = node . attrs [ "data-id" ] ;
125+ setTimeout ( ( ) => editor . commands . focusFootnote ( id ) ) ;
126+ return true ;
127+ }
54128 } ,
55129 } ,
56130 } ) ,
@@ -61,3 +135,16 @@ export const Footnotes = Extension.create({
61135 ] ;
62136 }
63137} )
138+
139+ declare module "@tiptap/core" {
140+ interface Commands < ReturnType > {
141+ extendedFootnoteReference : {
142+ /**
143+ * scrolls to & sets the text selection at the end of the footnote with the given id
144+ * @param id the id of the footote (i.e. the `data-id` attribute value of the footnote)
145+ * @example editor.commands.focusFootnote("a43956c1-1ab8-462f-96e4-be3a4b27fd50")
146+ */
147+ focusFootnoteReference : ( id : string ) => ReturnType ;
148+ } ;
149+ }
150+ }
0 commit comments