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
3 changes: 2 additions & 1 deletion demo/app/Sharp/Posts/PostForm.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ public function buildFormFields(FieldsContainer $formFields): void
SharpFormEditorField::IFRAME,
SharpFormEditorField::UPLOAD,
SharpFormEditorField::CODE_BLOCK,
SharpFormEditorField::FOOTNOTE,
AuthorEmbed::class,
])
->allowEmbeds([
Expand All @@ -79,7 +80,7 @@ public function buildFormFields(FieldsContainer $formFields): void
])
->allowFullscreen()
->setMaxLength(2000)
->setHeight(300, 0)
->setHeight(400)
)
->addField(
SharpFormTagsField::make('categories', Category::pluck('name', 'id')->toArray())
Expand Down
34 changes: 27 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,12 +78,13 @@
"flexsearch": "^0.8.205",
"leaflet": "^1.9.4",
"lodash": "^4.17.21",
"lucide-vue-next": "^0.542.0",
"lucide-vue-next": "^1.0.0",
"qs": "^6.6.0",
"reka-ui": "^2.6.1",
"sortablejs": "^1.15.2",
"tailwind-merge": "^3.3.0",
"text-clipper": "^1.3.0",
"tiptap-footnotes": "^3.0.1",
"tiptap-markdown": "^0.9.0",
"vue": "^3.5.12",
"vue-sonner": "^1.1.2",
Expand Down
1 change: 1 addition & 0 deletions resources/css/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
@source '../views/**/*.blade.php';
@source '../js/**/*.vue';
@source '../js/components/ui/**/*.ts';
@source '../js/form/components/fields/editor/extensions/Footnotes.ts';

@custom-variant dark (&:is(.dark *));

Expand Down
4 changes: 3 additions & 1 deletion resources/js/form/components/fields/editor/Editor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,7 @@
</template>

<div :class="cn(
'flex-1 grid grid-cols-1 overflow-y-auto overflow-x-clip',
'flex-1 grid grid-cols-1 overflow-y-auto scroll-py-4 overflow-x-clip',
isFullscreen
? 'lg:[scrollbar-gutter:stable]'
: ['min-h-20', {
Expand Down Expand Up @@ -419,10 +419,12 @@
'group/editor content w-full rounded-b-md focus:outline-none px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50',
'[&_.selection-highlight]:bg-[Highlight] [&_.selection-highlight]:py-0.5',
'[&_.ProseMirror-selectednode]:outline-none! [&:focus_.ProseMirror-selectednode]:ring-1 [&_.ProseMirror-selectednode]:ring-primary',
`[&_.footnotes]:before:content-(--footnote-title) [&_.footnotes]:before:text-xs [&_.footnotes]:before:text-muted-foreground [&_.footnotes]:before:block [&_.footnotes]:before:ml-[-1.75em] [&_.footnotes]:before:mb-2 [&_.footnotes]:-mx-3 [&_.footnotes]:pt-2 [&_.footnotes]:pb-2 [&_.footnotes]:pr-3 [&_.footnotes]:pl-[calc(.75rem+1.75em)] [&_.footnotes>li]:relative [&_.footnotes>li]:pl-[1.25em] [&_.footnotes]:border-t [&_.footnote-ref]:underline [&_.footnote-ref]:underline-offset-4 [&_.footnote-ref]:decoration-foreground/20 [&_.footnote-ref]:hover:decoration-foreground [&_.footnote-ref]:before:content-['['] [&_.footnote-ref]:after:content-[']']`,
{
'content-lg max-w-3xl mx-auto py-6 px-4 sm:px-6 text-base min-h-max': isFullscreen,
},
)"
:style="{ '--footnote-title': `'${__('sharp::form.editor.footnotes.title')}'` }"
role="textbox"
/>
</div>
Expand Down
150 changes: 150 additions & 0 deletions resources/js/form/components/fields/editor/extensions/Footnotes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { Footnote, Footnotes as BaseFootnotes, FootnoteReference } from "tiptap-footnotes";
import { Plugin, PluginKey } from "@tiptap/pm/state";
import { Decoration, DecorationSet } from "@tiptap/pm/view";
import { Extension, mergeAttributes, NodePos } from "@tiptap/core";

export const Footnotes = Extension.create({
name: 'footnotesExtension',
addExtensions() {
return [
BaseFootnotes,
Footnote.extend({
addProseMirrorPlugins() {
const editor = this.editor;
return [
new Plugin({
key: new PluginKey('footnoteDecoration'),
props: {
decorations: (state) => {
const decorations: Decoration[] = [];
state.doc.descendants((node, pos) => {
if (node.type.name === 'footnote') {
const id = node.attrs.id;
const refId = id?.startsWith('fn:') ? id.replace('fn:', '') : id;
decorations.push(
Decoration.widget(pos + 1, () => {
const sup = document.createElement('sup');
const a = document.createElement('a');
a.href = `#fnref:${refId}`;
a.draggable = false;
a.className = 'underline underline-offset-4 decoration-foreground/20 hover:decoration-foreground select-none';
a.textContent = '[^]';
sup.className = 'footnote-backref absolute left-[.125em] top-1.5'
sup.appendChild(a);
return sup;
})
);
}
});
return DecorationSet.create(state.doc, decorations);
},
handleClickOn(view, pos, node, nodePos, event) {
if((event.target as HTMLElement)?.closest('.footnote-backref')) {
event.preventDefault();
const id = (event.target as HTMLElement).closest('[data-id]').getAttribute('data-id');
setTimeout(() => editor.commands.focusFootnoteReference(id));
return true;
}
},
},
}),
];
},
addCommands() {
return {
focusFootnote: (id: string) => ({ editor, chain }) => {
const matchedFootnote = editor.$node("footnote", {
"data-id": id,
});

if (matchedFootnote) {
// sets the text selection to the end of the footnote definition and scroll to it.
chain()
.focus()
.setTextSelection(
matchedFootnote.from + matchedFootnote.content.size
)
.run();

matchedFootnote.element.scrollIntoView({ block: 'end' });
return true;
}
return false;
},
};
},
}),
FootnoteReference
.extend({
addCommands() {
return {
...this.parent(),
focusFootnoteReference: (id: string) => ({ editor, chain }) => {
let matchedFootnoteReference: { from: number } = null;
editor.state.doc.descendants((node, pos) => {
if (node.type.name === 'footnoteReference' && node.attrs['data-id'] === id) {
matchedFootnoteReference = {
from: pos,
}
return false;
}
});

if (matchedFootnoteReference) {
chain()
.focus()
.setTextSelection(
matchedFootnoteReference.from,
)
.run();

editor.view.dom.querySelector(`.footnote-ref[data-id="${id}"]`).scrollIntoView({ block: 'nearest' });
return true;
}
return false;
},
};
},
addProseMirrorPlugins() {
const editor = this.editor;
return [
new Plugin({
key: new PluginKey("customFootnoteRefClick"),

props: {
handleDOMEvents: {
click(view, event) {
if((event.target as HTMLElement)?.closest('.footnote-ref')) {
event.preventDefault();
}
}
},
handleClickOn(view, pos, node, nodePos, event) {
if(node.type.name === 'footnoteReference') {
const id = node.attrs["data-id"];
setTimeout(() => editor.commands.focusFootnote(id));
return true;
}
},
},
}),
...this.parent(),
]
},
}),
];
}
})

declare module "@tiptap/core" {
interface Commands<ReturnType> {
extendedFootnoteReference: {
/**
* scrolls to & sets the text selection at the end of the footnote with the given id
* @param id the id of the footote (i.e. the `data-id` attribute value of the footnote)
* @example editor.commands.focusFootnote("a43956c1-1ab8-462f-96e4-be3a4b27fd50")
*/
focusFootnoteReference: (id: string) => ReturnType;
};
}
}
Loading
Loading