Skip to content
Merged
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
46 changes: 28 additions & 18 deletions packages/core/src/extensions/Blocks/PreviousBlockTypePlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,9 @@ import {
} from "@tiptap/core";
import { Plugin, PluginKey } from "prosemirror-state";
import { Decoration, DecorationSet } from "prosemirror-view";
import BlockAttributes from "./BlockAttributes";

const PLUGIN_KEY = new PluginKey(`previous-blocks`);

// Inserts "prev-" string into an HTML attribute name with a "data-" prefix, e.g. "data-depth" -> "data-prev-depth".
// Assumes "data-" prefix is in the attribute name.
const insertPrev = (attr: string) => attr.slice(0, 5) + "prev-" + attr.slice(5);

/**
* This plugin tracks transformation of Block node attributes, so we can support CSS transitions.
*
Expand Down Expand Up @@ -75,7 +70,6 @@ export const PreviousBlockTypePlugin = () => {

changes.forEach(() => {
const oldNodes = findChildren(oldState.doc, (node) => node.attrs.id);

const oldNodesById = new Map(
oldNodes.map((node) => [node.node.attrs.id, node])
);
Expand All @@ -84,28 +78,38 @@ export const PreviousBlockTypePlugin = () => {

for (let node of newNodes) {
const oldNode = oldNodesById.get(node.node.attrs.id);
if (oldNode) {
const oldContentNode = oldNode?.node.firstChild;
const newContentNode = node.node.firstChild;
if (oldNode && oldContentNode && newContentNode) {
const newAttrs = {
listType: node.node.attrs.listType,
blockColor: node.node.attrs.blockColor,
blockStyle: node.node.attrs.blockStyle,
headingType: node.node.attrs.headingType,
// listType: node.node.attrs.listType,
Comment thread
YousefED marked this conversation as resolved.
level: newContentNode.attrs.level,
type: newContentNode.type.name,
depth: newState.doc.resolve(node.pos).depth,
};

const oldAttrs = {
listType: oldNode.node.attrs.listType,
blockColor: oldNode.node.attrs.blockColor,
blockStyle: oldNode.node.attrs.blockStyle,
headingType: oldNode.node.attrs.headingType,
// listType: oldNode.node.attrs.listType,
level: oldContentNode.attrs.level,
type: oldContentNode.type.name,
depth: oldState.doc.resolve(oldNode.pos).depth,
};

if (
JSON.stringify(oldAttrs) !== JSON.stringify(newAttrs) // TODO: faster deep equal?
) {
(oldAttrs as any).depthChange = oldAttrs.depth - newAttrs.depth;
(oldAttrs as any)["depth-change"] =
oldAttrs.depth - newAttrs.depth;
prev.prevBlockAttrs[node.node.attrs.id] = oldAttrs;

// for debugging:
// console.log(
// "previousBlockTypePlugin changes detected, oldAttrs",
// oldAttrs,
// "new",
// newAttrs
// );

prev.needsUpdate = true;
}
}
Expand Down Expand Up @@ -135,9 +139,15 @@ export const PreviousBlockTypePlugin = () => {

const decorationAttributes: any = {};
for (let [nodeAttr, val] of Object.entries(prevAttrs)) {
decorationAttributes[insertPrev(BlockAttributes[nodeAttr])] =
val || "none";
decorationAttributes["data-prev-" + nodeAttr] = val || "none";
}

// for debugging:
// console.log(
// "previousBlockTypePlugin committing decorations",
// decorationAttributes
// );

const decoration = Decoration.node(pos, pos + node.nodeSize, {
...decorationAttributes,
});
Expand Down
70 changes: 45 additions & 25 deletions packages/core/src/extensions/Blocks/nodes/Block.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,22 @@ BASIC STYLES
}

.blockContent {
font-size: 1em;
padding: 3px 0;
transition: font-size 0.2s;
/* display: inline-block; */
}

/* reset styles, they will be set on blockContent */
.blockContent p,
.blockContent h1,
.blockContent h2,
.blockContent h3 {
margin: 0;
padding: 0;
font-size: inherit;
}

/*
NESTED BLOCKS
*/
Expand All @@ -54,7 +65,7 @@ NESTED BLOCKS
height: 0;
}

/* NESTED BLOCK ANIMATIONS */
/* NESTED BLOCK ANIMATIONS (change in indent) */

[data-prev-depth-change="1"] {
--x: 1;
Expand Down Expand Up @@ -97,31 +108,39 @@ NESTED BLOCKS
}

/* HEADINGS*/
.blockOuter[data-prev-heading-type="1"] > .block > div:first-child,
.blockOuter[data-heading-type="1"]:not([data-prev-heading-type])
> .block
> div:first-child {
font-size: 3em;
font-weight: bold;
[data-level="1"] {
--level: 3em;
}
[data-level="2"] {
--level: 2em;
}
[data-level="3"] {
--level: 1.3em;
}

.blockOuter[data-prev-heading-type="2"] > .block > div:first-child,
.blockOuter[data-heading-type="2"]:not([data-prev-heading-type])
> .block
> div:first-child {
font-size: 2em;
[data-prev-level="1"] {
--prev-level: 3em;
}
[data-prev-level="2"] {
--prev-level: 2em;
}
[data-prev-level="3"] {
--prev-level: 1.3em;
}

.blockOuter[data-prev-type="headingContent"] > .block > .blockContent {
font-size: var(--prev-level);
font-weight: bold;
}

.blockOuter[data-prev-heading-type="3"] > .block > div:first-child,
.blockOuter[data-heading-type="3"]:not([data-prev-heading-type])
.blockOuter:not([data-prev-type])
> .block
> div:first-child {
font-size: 1.3em;
> .blockContent[data-content-type="headingContent"] {
font-size: var(--level);
font-weight: bold;
}

/* LISTS */
/* LISTS (TODO) */

.block > div:first-child::before {
content: "";
Expand Down Expand Up @@ -191,12 +210,12 @@ NESTED BLOCKS

/* PLACEHOLDERS*/

.blockContent > div {
.blockContent > :first-child {
display: inline;
}

.blockContent.isEmpty div::before,
.blockContent.isFilter div::before {
.blockContent.isEmpty > :first-child:before,
.blockContent.isFilter > :first-child:before {
/*float: left; */
content: "";
color: #aeb8c2;
Expand All @@ -209,18 +228,19 @@ NESTED BLOCKS

/* TODO: would be nicer if defined from code */

.blockContent.isEmpty.hasAnchor div::before {
.blockContent.isEmpty.hasAnchor > :first-child:before {
content: "Enter text or type '/' for commands";
}

.blockContent.isFilter.hasAnchor div::before {
.blockContent.isFilter.hasAnchor > :first-child:before {
content: "Type to filter";
}

[data-heading-type] > .blockContent.isEmpty div::before {
.blockContent[data-content-type="headingContent"].isEmpty
> :first-child::before {
content: "Heading";
}

[data-list-type] > .blockContent.isEmpty div::before {
.blockContent[data-content-type="listContent"] > :first-child div::before {
content: "List";
}
}
37 changes: 26 additions & 11 deletions packages/core/src/extensions/Blocks/nodes/Block.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { mergeAttributes, Node } from "@tiptap/core";
import { TextSelection } from "prosemirror-state";
import { Slice } from "prosemirror-model";
import { PreviousBlockTypePlugin } from "../PreviousBlockTypePlugin";
import { getBlockInfoFromPos } from "../helpers/getBlockInfoFromPos";
import { TextSelection } from "prosemirror-state";
import BlockAttributes from "../BlockAttributes";
import { getBlockInfoFromPos } from "../helpers/getBlockInfoFromPos";
import { PreviousBlockTypePlugin } from "../PreviousBlockTypePlugin";
import styles from "./Block.module.css";
import { HeadingContentAttributes } from "./HeadingContent";
import { ListItemContentAttributes } from "./ListItemContent";
import styles from "./Block.module.css";

export interface IBlock {
HTMLAttributes: Record<string, any>;
Expand Down Expand Up @@ -109,6 +109,7 @@ export const Block = Node.create<IBlock>({
[
"div",
mergeAttributes(attrs, {
// TODO: maybe remove html attributes from inner block
class: styles.block,
"data-node-type": this.name,
}),
Expand All @@ -134,7 +135,9 @@ export const Block = Node.create<IBlock>({
(posInBlock) =>
({ state }) => {
const blockInfo = getBlockInfoFromPos(state.doc, posInBlock);
if (blockInfo === undefined) return false;
if (blockInfo === undefined) {
return false;
}

const { startPos, endPos } = blockInfo;

Expand Down Expand Up @@ -166,7 +169,9 @@ export const Block = Node.create<IBlock>({
state.doc,
posBetweenBlocks + 1
);
if (nextBlockInfo === undefined) return false;
if (nextBlockInfo === undefined) {
return false;
}

const { node, contentNode, startPos, endPos, depth } = nextBlockInfo;

Expand All @@ -186,13 +191,17 @@ export const Block = Node.create<IBlock>({

let prevBlockEndPos = posBetweenBlocks - 1;
let prevBlockInfo = getBlockInfoFromPos(state.doc, prevBlockEndPos);
if (prevBlockInfo === undefined) return false;
if (prevBlockInfo === undefined) {
return false;
}

// Finds the nearest previous block, prioritizing higher nesting levels.
while (prevBlockInfo.numChildBlocks > 0) {
prevBlockEndPos--;
prevBlockInfo = getBlockInfoFromPos(state.doc, prevBlockEndPos);
if (prevBlockInfo === undefined) return false;
if (prevBlockInfo === undefined) {
return false;
}
}

// Deletes next block and adds its text content to the nearest previous block.
Expand All @@ -212,7 +221,9 @@ export const Block = Node.create<IBlock>({
(posInBlock, keepType) =>
({ state }) => {
const blockInfo = getBlockInfoFromPos(state.doc, posInBlock);
if (blockInfo === undefined) return false;
if (blockInfo === undefined) {
return false;
}

const { startPos, endPos, depth } = blockInfo;

Expand Down Expand Up @@ -259,7 +270,9 @@ export const Block = Node.create<IBlock>({
(posInBlock, type, attributes) =>
({ state }) => {
const blockInfo = getBlockInfoFromPos(state.doc, posInBlock);
if (blockInfo === undefined) return false;
if (blockInfo === undefined) {
return false;
}

const { startPos, endPos } = blockInfo;

Expand All @@ -278,7 +291,9 @@ export const Block = Node.create<IBlock>({
(posInBlock, type, attributes) =>
({ state, chain }) => {
const blockInfo = getBlockInfoFromPos(state.doc, posInBlock);
if (blockInfo === undefined) return false;
if (blockInfo === undefined) {
return false;
}

const { node, startPos, endPos } = blockInfo;

Expand Down
42 changes: 16 additions & 26 deletions packages/core/src/extensions/Blocks/nodes/HeadingContent.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { InputRule, Node, NodeViewRendererProps } from "@tiptap/core";
import { InputRule, mergeAttributes, Node } from "@tiptap/core";
import styles from "./Block.module.css";

export type HeadingContentAttributes = {
Expand All @@ -10,28 +10,18 @@ export const HeadingContent = Node.create({
group: "blockContent",
content: "inline*",

addNodeView() {
return (props: NodeViewRendererProps) => {
const element = document.createElement("div");
element.setAttribute("data-node-type", "block-content");
element.setAttribute("data-content-type", this.name);
element.className = styles.blockContent;

const editableElement = document.createElement(
"h" + props.HTMLAttributes["level"]
);
element.appendChild(editableElement);

return {
dom: element,
contentDOM: editableElement,
};
};
},

addAttributes() {
return {
level: { default: "1" },
level: {
default: "1",
// instead of "level" attributes, use "data-level"
parseHTML: (element) => element.getAttribute("data-level"),
renderHTML: (attributes) => {
return {
"data-level": attributes.level,
};
},
},
};
},

Expand Down Expand Up @@ -71,15 +61,15 @@ export const HeadingContent = Node.create({
];
},

renderHTML({ HTMLAttributes }) {
renderHTML({ node, HTMLAttributes }) {
return [
"div",
{
"data-node-type": "block-content",
mergeAttributes(HTMLAttributes, {
"data-node-type": "block-content", // TODO: only for testing? if so, rename to data-test-*?
"data-content-type": this.name,
class: styles.blockContent,
},
["h" + HTMLAttributes["level"], 0],
}),
["h" + node.attrs.level, 0],
];
},
});
Loading