-
-
Notifications
You must be signed in to change notification settings - Fork 608
refactor: move all blocks to use the custom blocks API #1904
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
Changes from 7 commits
fcdbf18
e1bb45e
6b10846
92952e7
6d85ef5
ec77940
809ea0a
b46fcc6
5751974
c51767a
97295ce
1ff7474
9918510
1e0e059
fc9e452
a0fc6e2
cda8eb6
55957cd
dba7321
760be84
f934d26
060108a
01478cd
cb8150c
ba57b93
71db2ba
36b9bd9
e41c137
1ddef1b
62be3e2
fda7be0
81c8d20
ff2f039
fdbc419
41b2e43
cf31ed6
a6ea62d
703f86e
cd90895
258520f
dbc0c44
e3e4f5b
fc98fb8
ac0d392
467b33d
f6cb1f9
a0e9443
5e3cad6
069f88f
f4a0316
26b9b1c
356aedc
80963f4
8ef6188
6b7bad8
5dc2544
87e57e3
17398cc
95c493a
41d1449
a92d1e7
422cccb
52a018c
84d50ce
05fbc87
f242a2f
59a7a63
b16a9ac
11f8633
b17a8d8
caf7ffd
b582be8
f9bca88
ce882f1
eb049e2
474897b
b4b56a9
4b9d5d0
3f83c75
89ad4d5
6c0211a
52c7075
4b40c57
fdcfffb
3174b5b
cd788ad
ddd3adf
6e4beff
0246290
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
import { parseAudioElement } from "../../blocks/AudioBlockContent/parseAudioElement.js"; | ||
import { defaultProps } from "../../blocks/defaultProps.js"; | ||
import { parseFigureElement } from "../../blocks/FileBlockContent/helpers/parse/parseFigureElement.js"; | ||
import { createFileBlockWrapper } from "../../blocks/FileBlockContent/helpers/render/createFileBlockWrapper.js"; | ||
import { createFigureWithCaption } from "../../blocks/FileBlockContent/helpers/toExternalHTML/createFigureWithCaption.js"; | ||
import { createLinkWithCaption } from "../../blocks/FileBlockContent/helpers/toExternalHTML/createLinkWithCaption.js"; | ||
import { | ||
createBlockConfig, | ||
createBlockSpec, | ||
} from "../../schema/blocks/playground.js"; | ||
|
||
export const FILE_AUDIO_ICON_SVG = | ||
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M2 16.0001H5.88889L11.1834 20.3319C11.2727 20.405 11.3846 20.4449 11.5 20.4449C11.7761 20.4449 12 20.2211 12 19.9449V4.05519C12 3.93977 11.9601 3.8279 11.887 3.73857C11.7121 3.52485 11.3971 3.49335 11.1834 3.66821L5.88889 8.00007H2C1.44772 8.00007 1 8.44778 1 9.00007V15.0001C1 15.5524 1.44772 16.0001 2 16.0001ZM23 12C23 15.292 21.5539 18.2463 19.2622 20.2622L17.8445 18.8444C19.7758 17.1937 21 14.7398 21 12C21 9.26016 19.7758 6.80629 17.8445 5.15557L19.2622 3.73779C21.5539 5.75368 23 8.70795 23 12ZM18 12C18 10.0883 17.106 8.38548 15.7133 7.28673L14.2842 8.71584C15.3213 9.43855 16 10.64 16 12C16 13.36 15.3213 14.5614 14.2842 15.2841L15.7133 16.7132C17.106 15.6145 18 13.9116 18 12Z"></path></svg>'; | ||
|
||
export interface AudioOptions { | ||
icon?: string; | ||
} | ||
const config = createBlockConfig((_ctx: AudioOptions) => ({ | ||
type: "audio" as const, | ||
propSchema: { | ||
backgroundColor: defaultProps.backgroundColor, | ||
// File name. | ||
name: { | ||
default: "" as const, | ||
}, | ||
// File url. | ||
url: { | ||
default: "" as const, | ||
}, | ||
// File caption. | ||
caption: { | ||
default: "" as const, | ||
}, | ||
|
||
showPreview: { | ||
default: true, | ||
}, | ||
}, | ||
content: "none" as const, | ||
meta: { | ||
fileBlockAccept: ["audio/*"], | ||
}, | ||
})); | ||
|
||
export const definition = createBlockSpec(config).implementation((config) => ({ | ||
parse: (element) => { | ||
if (element.tagName === "AUDIO") { | ||
// Ignore if parent figure has already been parsed. | ||
if (element.closest("figure")) { | ||
return undefined; | ||
} | ||
|
||
return parseAudioElement(element as HTMLAudioElement); | ||
} | ||
|
||
if (element.tagName === "FIGURE") { | ||
const parsedFigure = parseFigureElement(element, "audio"); | ||
if (!parsedFigure) { | ||
return undefined; | ||
} | ||
|
||
const { targetElement, caption } = parsedFigure; | ||
|
||
return { | ||
...parseAudioElement(targetElement as HTMLAudioElement), | ||
caption, | ||
}; | ||
} | ||
|
||
return undefined; | ||
}, | ||
render: (block, editor) => { | ||
const icon = document.createElement("div"); | ||
icon.innerHTML = config.icon ?? FILE_AUDIO_ICON_SVG; | ||
|
||
const audio = document.createElement("audio"); | ||
audio.className = "bn-audio"; | ||
if (editor.resolveFileUrl) { | ||
editor.resolveFileUrl(block.props.url).then((downloadUrl) => { | ||
audio.src = downloadUrl; | ||
}); | ||
} else { | ||
audio.src = block.props.url; | ||
} | ||
audio.controls = true; | ||
audio.contentEditable = "false"; | ||
audio.draggable = false; | ||
|
||
return createFileBlockWrapper( | ||
block, | ||
editor, | ||
{ dom: audio }, | ||
editor.dictionary.file_blocks.audio.add_button_text, | ||
icon.firstElementChild as HTMLElement, | ||
); | ||
}, | ||
toExternalHTML(block) { | ||
if (!block.props.url) { | ||
const div = document.createElement("p"); | ||
div.textContent = "Add audio"; | ||
|
||
return { | ||
dom: div, | ||
}; | ||
} | ||
|
||
let audio; | ||
if (block.props.showPreview) { | ||
audio = document.createElement("audio"); | ||
audio.src = block.props.url; | ||
} else { | ||
audio = document.createElement("a"); | ||
audio.href = block.props.url; | ||
audio.textContent = block.props.name || block.props.url; | ||
} | ||
|
||
if (block.props.caption) { | ||
if (block.props.showPreview) { | ||
return createFigureWithCaption(audio, block.props.caption); | ||
} else { | ||
return createLinkWithCaption(audio, block.props.caption); | ||
} | ||
} | ||
|
||
return { | ||
dom: audio, | ||
}; | ||
}, | ||
runsBefore: ["file"], | ||
})); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
import { updateBlockTr } from "../../api/blockManipulation/commands/updateBlock/updateBlock.js"; | ||
import { getBlockInfoFromTransaction } from "../../api/getBlockInfoFromPos.js"; | ||
import { defaultProps } from "../../blocks/defaultProps.js"; | ||
import { getListItemContent } from "../../blocks/ListItemBlockContent/getListItemContent.js"; | ||
import { | ||
createBlockConfig, | ||
createBlockNoteExtension, | ||
createBlockSpec, | ||
} from "../../schema/blocks/playground.js"; | ||
import { handleEnter } from "../utils/listItemEnterHandler.js"; | ||
|
||
const config = createBlockConfig(() => ({ | ||
type: "bulletListItem" as const, | ||
propSchema: { | ||
...defaultProps, | ||
}, | ||
content: "inline", | ||
})); | ||
|
||
export const definition = createBlockSpec(config).implementation( | ||
() => ({ | ||
parse(element) { | ||
if (element.tagName !== "LI") { | ||
return false; | ||
} | ||
|
||
const parent = element.parentElement; | ||
|
||
if (parent === null) { | ||
return false; | ||
} | ||
|
||
if ( | ||
parent.tagName === "UL" || | ||
(parent.tagName === "DIV" && parent.parentElement?.tagName === "UL") | ||
) { | ||
return {}; | ||
} | ||
|
||
return false; | ||
}, | ||
// As `li` elements can contain multiple paragraphs, we need to merge their contents | ||
// into a single one so that ProseMirror can parse everything correctly. | ||
parseContent: ({ el, schema }) => | ||
getListItemContent(el, schema, "bulletListItem"), | ||
render() { | ||
const div = document.createElement("div"); | ||
// We use a <p> tag, because for <li> tags we'd need a <ul> element to put | ||
// them in to be semantically correct, which we can't have due to the | ||
// schema. | ||
const el = document.createElement("p"); | ||
|
||
div.appendChild(el); | ||
|
||
return { | ||
dom: div, | ||
contentDOM: el, | ||
}; | ||
}, | ||
}), | ||
() => [ | ||
createBlockNoteExtension({ | ||
key: "bullet-list-item-shortcuts", | ||
keyboardShortcuts: { | ||
Enter: ({ editor }) => { | ||
return handleEnter(editor, "bulletListItem"); | ||
}, | ||
"Mod-Shift-8": ({ editor }) => | ||
editor.transact((tr) => { | ||
const blockInfo = getBlockInfoFromTransaction(tr); | ||
|
||
if ( | ||
!blockInfo.isBlockContainer || | ||
blockInfo.blockContent.node.type.spec.content !== "inline*" | ||
) { | ||
return true; | ||
} | ||
|
||
updateBlockTr(tr, blockInfo.bnBlock.beforePos, { | ||
type: "bulletListItem", | ||
props: {}, | ||
}); | ||
return true; | ||
}), | ||
}, | ||
inputRules: [ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. cool that there's a BN-style API for this. Can we do the same for keyboardshortcuts you think? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I thought about what that would look like for keyboard shortcuts, but the problem is that keyboard shortcuts can sort of just do anything. Some usecases I thought of were:
These sorts of cases are too disparate to really "unify" into a sensible API. Fundamental tension between composability & centralization. |
||
{ | ||
find: new RegExp(`^[-+*]\\s$`), | ||
replace() { | ||
return { | ||
type: "bulletListItem", | ||
props: {}, | ||
content: [], | ||
}; | ||
}, | ||
}, | ||
], | ||
}), | ||
], | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What was the reasoning again to define keyboardshortcuts / inputrules in a separate argument +
createBlockNoteExtension
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This introduces keyboard shortcuts & inputrules to the extension API because we didn't have a way of doing them before.
I considered exposing them to both APIs (e.g. a custom block has an input rule API & so does the extension API), but that only added complexity.
Allowing extensions to be included to the block API is useful in that some things may require an extension to function (e.g. code block syntax highlighting, numbered list decorations).