-
-
Notifications
You must be signed in to change notification settings - Fork 2.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Embedding image in a figure element with a figcaption #573
Comments
These nodes should work for the most part, but you're going to have to modify them to do what you want to achieve:
Figure.js import { Node } from 'tiptap'
import { Plugin } from 'prosemirror-state'
export default class Figure extends Node {
get name() {
return "figure";
}
get schema() {
return {
content: "image figcaption",
group: "block",
parseDOM: [{tag: "figure"}],
toDOM: node => ["figure", 0],
};
}
// Deletes the entire figure node if imageNode is empty.
get plugins() {
return [
new Plugin ({
appendTransaction: (transactions, oldState, newState) => {
const tr = newState.tr
let modified = false;
// TO-DO: Iterate through transactions instead of descendants.
newState.doc.descendants((node, pos, parent) => {
if (node.type.name != "figure") return;
let imageNode = node.firstChild;
if (imageNode.attrs.src == imageNode.type.defaultAttrs.src &&
imageNode.attrs.alt == imageNode.type.defaultAttrs.alt)
{
tr.deleteRange(pos, pos + node.nodeSize);
modified = true;
}
})
if (modified) return tr;
}
})
];
}
} Image.js import { Node, Plugin } from "tiptap"
import { fs, Path, Buffer } from "filer"
export default class Image extends Node {
get name() {
return "image"
}
get schema() {
return {
inline: false,
attrs: {src: {default: ""}, alt: {default: ""}},
parseDOM: [{tag: "img", getAttrs: dom => ({src: dom.src, alt: dom.alt})}],
toDOM: node => ["img", node.attrs],
}
}
commands({ type }) {
return attrs => (state, dispatch) => {
const { selection } = state
const position = selection.$cursor ? selection.$cursor.pos : selection.$to.pos
const node = type.create(attrs)
const transaction = state.tr.insert(position, node)
dispatch(transaction)
}
}
get plugins() {
return [
new Plugin({
props: {
handleDOMEvents: {
drop(view, event) {
if (!event.dataTransfer || !event.dataTransfer.files || !event.dataTransfer.files.length)
return
const images = Array.from(event.dataTransfer.files).filter(file => {
return (/image/i).test(file.type)
})
if (images.length == 0) return
event.preventDefault()
const { schema } = view.state
const coordinates = view.posAtCoords({ left: event.clientX, top: event.clientY })
images.forEach(image => {
// This is where you would upload image to a server or a filesystem,
// then create an imageNode.
const node = schema.nodes.image.create({src: XXX, alt: YYY})
const tr = view.state.tr.insert(coordinates.pos, node)
view.dispatch(tr)
})
},
},
},
}),
]
}
} Figcaption.js import {Node} from 'tiptap'
export default class Figcaption extends Node {
get name() {
return "figcaption"
}
get schema() {
return {
content: "inline*",
group: "figure",
// TO-DO: get image alt if figcaption not found using prosemirror-model
parseDOM: [{tag: "figcaption"}],
toDOM: node => ["figcaption", 0],
}
}
} |
Thanks a lot @BrianHung! I will give this a try. |
Hey, we are creating our editor using tiptap. Made a custom block for Image. We will going to open source it soon. For Image
|
@BrianHung |
You would have to modify the |
@BrianHung
|
I haven't checked this, but try this out // figure.js
commands({ type, schema }) {
return attrs => (state, dispatch) => {
const { tr, selection } = state
const pos = selection.$cursor ? selection.$cursor.pos : selection.$to.pos
const node = type.createAndFill(attrs, schema.text(attrs.caption));
dispatch(tr.insert(pos, node));
}
} I made two modifications.
|
@BrianHung And according to the ProseMirror doc, |
Hey @neelay92, I've been giving this a try today and this is what I ended up with: commands({ type, schema }) {
return attrs => (state, dispatch) => {
const { tr, selection } = state
const pos = selection.$cursor ? selection.$cursor.pos : selection.$to.pos
const node = type.create(null, [
schema.nodes.image.create({
alt: attrs.caption,
src: attrs.src,
}),
schema.nodes.figcaption.create(
null,
attrs.caption ? schema.text(attrs.caption) : 'default caption'
),
])
dispatch(tr.insert(pos, node))
}
} I still have no idea why |
So I used code from #573 (comment) and it worked well. However what I am still struggling with is this part:
Here's my Figcaption.js: import { Node } from 'tiptap'
export default class Figcaption extends Node {
get name() {
return "figcaption"
}
get schema() {
return {
content: "inline*",
group: "figure",
attrs: { attribution: { default: "" } },
parseDOM: [
{ tag: "img", getAttrs: dom => ({ attribution: dom.dataset.attribution }) },
{ tag: "figcaption" }
],
toDOM: node => [
"figcaption",
{
contenteditable: node.attrs.attribution ? false : true,
},
node.attrs.attribution || "Add attribution here"
],
}
}
} But |
@portikM Try using hakla's method #573 (comment) of initializing the figcaption within the figure creation. I would also try expanding the getAttrs function to be more debug-able: getAttrs: dom => {
console.log(dom); // check if dom.dataset exists
return {attribution: dom.dataset.attribution}
} |
@BrianHung that makes sense, yes. But when I try to |
@BrianHung Thank you for the code, it helped me a lot. I ended up using |
Edit: Was related to the div I wrapped around the |
hey can i get a final code?, i still not understand @BrianHung |
Hi @BrianHung your implementation of Thank said, I'm struggling to implement ways to let the user leave Do you happen to have an idea of how to go about this, pointing in the right direction would be helpful. |
@vinlim If there's no selectable content below the figcaption or figure, I would handle it like you're exiting a codeblock: pressing "Enter" or "Shift+Enter" should create a new paragraph below. For codeblocks, this is done using To adapt it to figcaptions, simply replace the if condition that checks you're in a codeblock You would then use it within figcaption or figure like keys({ type }) {
return {
'Shift-Enter': exitFigcaption(type),
}
} Edit: Tiptap also has a useful plugin, @Tukizz The code I originally posted above is the code I'm using. If you don't understand, I would recommend groking through ProseMirror's documentation, https://prosemirror.net/docs/ref/, which tiptap builds upon. Reading through that has been extremely useful in building custom tiptap nodes. Best of luck! |
@BrianHung Can you elaborate more on that? I can't get mine to work: keys({ type }) {
return {
'Enter': (state, dispatch) => {
let {$head, $anchor} = state.selection
if ($head.parent.type.name !== 'figcaption' || !$head.sameParent($anchor)) return false
let above = $head.node(-1), after = $head.indexAfter(-1)
if (!above.canReplaceWith(after, after, type)) return false
if (dispatch) {
let pos = $head.after(), tr = state.tr.replaceWith(pos, pos, type.createAndFill())
tr.setSelection(Selection.near(tr.doc.resolve(pos), 1))
dispatch(tr.scrollIntoView())
}
return true
},
}
} |
I managed to get everything to work the way I want it when editing the document. However, when I load the stored HTML from database, the attributes and value never reach This is the HTML snippet for the Figure element I have stored as JSON in Db: <figure style=\"width: 300px; margin: 0 auto;\">
<img src=\"https://placekitten.com/600/200\" alt=\"Cat\">
<figcaption>Zat</figcaption>
</figure> If I inspect what gets outputted to DOM on load, I get this: <figure style="width: 300px; margin: 0 auto;">
<img contenteditable="false">
<figcaption><br></figcaption>
</figure> This is the current schema part of Figure.js: get schema() {
return {
content: "image_block figcaption",
group: "block",
inline: false,
attrs: {
width: { default: null }
},
parseDOM: [{
tag: "figure",
getAttrs: dom => {
return {
width: dom.style.width
}
}
}],
toDOM(node) {
let styleString = ''
if (node.attrs.width) {
styleString = 'width: ' + node.attrs.width + '; margin: 0 auto;'
}
return ["figure", { style: styleString }, 0]
}
};
} If I remove Has anyone gotten this to work when loading the value dynamically? |
What is your image_block code looks like? |
This is the entire ImageBlock.js: import { Node, Plugin } from "tiptap"
export default class ImageBlock extends Node {
get name() {
return "image_block"
}
get schema() {
return {
inline: false,
group: 'figure',
attrs: {
src: { default: null },
alt: { default: null }
},
parseDOM: [{
tag: "img[src]",
getAttrs: dom => {
console.log('src: ' + dom.getAttribute('src'))
return {
src: dom.getAttribute("src"),
alt: dom.getAttribute("alt")
}
}
}],
toDOM(node) {
console.log(node.attrs)
return ["img", node.attrs]
},
}
}
commands({ type }) {
return attrs => (state, dispatch) => {
const { selection } = state
const position = selection.$cursor ? selection.$cursor.pos : selection.$to.pos
const node = type.create(attrs)
const transaction = state.tr.insert(position, node)
dispatch(transaction)
}
}
} |
I don't have the code anymore but I believe I used a |
That wasn't it @tyuwan , thanks for trying to help though. I actually did manage to get it to work by storing the document to database as JSON instead of HTML. Maybe ProseMirror only supports it's own JSON data as input value? Weird that all other elements render fine when loading the editor with HTML input. |
Sorry, I misunderstood. Yea, you would have to store it in JSON. We used the JSON to render the HTML(with nicer format) on the fly server-side and cached it. |
If it only works with JSON, the |
Many times. You can see my I also didn't think that the format should matter when storing data. PM should be able to do a 1-to-1 conversion between them. I just don't understand what exactly is the mechanism that passes attributes to node children in ProseMirror to be able to debug this. Also ProseMirror seems fail silently in some cases instead of raising errors, which makes debugging difficult. Anyway, while it would be easier in my use case to just store HTML as that's the representation I'm using in the end, I can manage storing it in JSON. It would be great if this could be properly written as an npm module by @BrianHung or someone, as mentioned in #819 . Although if you're busy coding 2.0, the efforts might be in better use there. |
@BrianHung ping! I still don't understand how to leave the figcaption to enter a new paragraph. Or can I treat figure as a single non editable node? Then I could just render the figure using vue |
@Zzombiee2361 Sorry for the late response. I looked back at my figure node: using the baseKeymap that comes with Also this is my update import Node from './Node'
import { Plugin } from 'prosemirror-state'
import type { Node as PMNode, NodeSpec } from "prosemirror-model";
/**
* https://discuss.prosemirror.net/t/figure-and-editable-caption/462/5
*/
export default class Figure extends Node {
get name() {
return "figure";
}
get schema(): NodeSpec {
return {
attrs: {src: {}, alt: {default: null}, title: {default: null}},
group: "block",
content: "inline*",
parseDOM: [{
tag: "figure",
contentElement: "figcaption",
getAttrs(dom: HTMLElement) {
const img = dom.querySelector("img")
console.log("figure", dom, { src: img.getAttribute("src"), alt: img.getAttribute("alt"), title: img.getAttribute("title") })
return { src: img.getAttribute("src"), alt: img.getAttribute("alt"), title: img.getAttribute("title") }
}
}],
toDOM(node: PMNode) {
let {src, alt, title} = node.attrs;
return ["figure", ["img", {src, alt, title}], ["figcaption", 0]]
}
};
}
} And updated import Node from "./Node"
import type { EditorView } from "prosemirror-view"
import { Plugin } from "prosemirror-state"
import type { Node as PMNode, NodeSpec } from "prosemirror-model";
/**
* https://github.com/ProseMirror/prosemirror-schema-basic/blob/master/src/schema-basic.js
*/
export default class Image extends Node {
get name() {
return "image"
}
get schema(): NodeSpec {
return {
attrs: {src: {}, alt: {default: null}, title: {default: null}},
inline: false,
group: "block",
draggable: true,
parseDOM: [{tag: "img[src]", getAttrs(dom: HTMLImageElement) {
console.log("image", dom, { src: dom.getAttribute("src"), alt: dom.getAttribute("alt"), title: dom.getAttribute("title") })
return { src: dom.getAttribute("src"), alt: dom.getAttribute("alt"), title: dom.getAttribute("title") }
}}],
toDOM(node: PMNode) { let {src, alt, title} = node.attrs; return ["img", {src, alt, title}] }
}
}
commands({nodeType}) {
return attrs => (state, dispatch) => {
const { selection } = state
const position = selection.$cursor ? selection.$cursor.pos : selection.$to.pos
const node = nodeType.create(attrs)
const transaction = state.tr.insert(position, node)
dispatch(transaction)
}
}
} Instead of making a I'm planning to write some additional keymaps to convert between figures and images as well: for example, if backspace is pressed while caption is already empty, then figures should be converted into an image. Re:
The above should help. To initialize the content for the figcaption, which is now part of the figure node, we can use the contentElement field. This is a sketch of what you can do with that: tag: "figure",
contentElement: (dom: HTMLElement) => {
let figcaption = dom.querySelector("figcaption"); // dom is the figure node we parsed
if (figcaption) return figcaption;
else {
let img = dom.querySelector("img"); // assuming image exists
let figcaption = document.createElement("figcaption")
figcaption.innerHTML = img.getAttribute("alt")
return figcaption;
}
} If you want to keep the current schema you have, then use content: "image figcaption",
parseDOM: [{
tag: "figure",
getContent(dom: HTMLElement, schema) {
let img = dom.querySelector("img")
const imageNode = schema.nodes.image.create({ src: img.getAttribute("src"), alt: img.getAttribute("alt"), title: img.getAttribute("title") })
let figcaption = dom.querySelector("figcaption)
let captionContent = figcaption.textContent || img.getAttribute("alt") || img.getAttribute("title);
const figcaptionNode = schema.nodes.figcaption.create(null, schema.text(captionContent);
return schema.nodes.figure.create(null, [imageNode, figcaptionNode ]);
}
}], @arokanto @tyuwan pinging you two as well, as this updated |
I'm struggling with how to do this:
The things I want to achieve here is to:
I guess it involves creating a schema for <figure> but I have a hard time understanding how schemas work in general and with nested elements in particular.
Has anyone done something similar to this and/or have some example code to show? I would really appreciate any help!
The text was updated successfully, but these errors were encountered: