Skip to content
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

feat: Checkbox/TODO list item block #729

Open
wants to merge 24 commits into
base: main
Choose a base branch
from
Open

Conversation

matthewlipski
Copy link
Collaborator

@matthewlipski matthewlipski commented May 6, 2024

This PR adds a TODO list item style block to the editor. Also includes items for the block type dropdown and slash menu.

Closes #18

TODO: We should merge the releases branch into main first
TODO: Figure out what we should render to external HTML, since there aren't set standards and different platforms do it in different ways.

Copy link

vercel bot commented May 6, 2024

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Updated (UTC)
blocknote ✅ Ready (Inspect) Visit Preview May 22, 2024 6:32pm
blocknote-website ✅ Ready (Inspect) Visit Preview May 22, 2024 6:32pm

@@ -184,6 +184,24 @@ NESTED BLOCKS
gap: 1.2em;
}

/* Checked */
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

makes me think, would it even be possible to use a label for the inline content + checkbox input? That would be the most "semantic" / accessible way, but not sure if that only introduces more complexity in terms of keyboard handling etc

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tested this and while keyboard navigation is no problem, clicking the text (which is now a label) toggles the checkbox, which is default browser behaviour so I don't think there's an easy workaround (assuming the checkbox id and label for attributes match). Also not sure if the semantics are correct actually, since the contentDOM is more like a second input field rather than a label.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right now I'll leave the contentDOM as a div. We can also change it to a label without issues, but we shouldn't set the for attribute.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@matthewlipski you can check how Tiptap handles that in their own extension: https://tiptap.dev/docs/editor/api/nodes/task-item

Clicking the text shouldn't trigger the checkbox, as this can become annoying for the user

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@matthewlipski Is there any way we can use the mantine checkbox with the indigo colorway. I think it looks a lot better than the one in the current preview. Would really appreciate this change. I am currently using a custom schema block that does the todo list with the mantine checkbox item. Here's what it looks like:

Screenshot 2024-05-14 at 10 29 29 PM

Copy link

@aoztanir aoztanir May 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm also happy to paste my custom code even though most if it is a crummy fix to the block not continuing on a new line.

'use client';
import { defaultProps, insertOrUpdateBlock } from '@blocknote/core';
import { createReactBlockSpec } from '@blocknote/react';
import { Box, Checkbox, Flex } from '@mantine/core';
import { IconCheck } from '@tabler/icons-react';
import { useEffect } from 'react';
import CategorySelect from './CategorySelect';
import './styles.css';

export const insertCheckBox = (editor) => ({
  title: 'Todo',
  onItemClick: () => {
    insertOrUpdateBlock(editor, {
      type: 'checkbox',
    });
  },
  aliases: ['checkbox', 'todo', 'list', 'bullets', 'to-do', 'tasks', 'taskboard', 'board'],
  subtext: 'Plan out your tasks using a checkbox todo list',
  icon: <IconCheck size="20" />,
});

export const CheckBoxBlock = createReactBlockSpec(
  {
    type: 'checkbox',
    propSchema: {
      textAlignment: defaultProps.textAlignment,
      textColor: defaultProps.textColor,
      type: {
        default: false,
        values: [true, false],
      },
    },
    content: 'inline',
  },
  {
    render: (props) => {
      useEffect(() => {
        const handleKeyDown = (event) => {
          if (event.key == 'Tab') {
            event.preventDefault();
            return;
          }
          if (event.key === 'Enter') {
            if (props?.editor?.getTextCursorPosition()?.prevBlock?.id != props.block.id) {
              return;
            }

            //!OLD STUFF BELOW
            var thisFunctionalComponentBlock = props.editor.getTextCursorPosition().prevBlock;

            if (thisFunctionalComponentBlock.content.length == 0) {
              props.editor.updateBlock(props.editor.getTextCursorPosition().prevBlock, {
                type: 'paragraph',
              });

              props.editor.setTextCursorPosition(
                props.editor.getTextCursorPosition().prevBlock,
                'start'
              );
              if (props.editor.getTextCursorPosition().nextBlock.content.length == 0) {
                props.editor.removeBlocks([props.editor.getTextCursorPosition().nextBlock]);
              }
              return;
            }

            props.editor.updateBlock(props.editor.getTextCursorPosition().block, {
              type: 'checkbox',
            });
          }
        };

        document.addEventListener('keydown', handleKeyDown);

        return () => {
          document.removeEventListener('keydown', handleKeyDown);
        };
      }, []);

      return (
        <Flex
          align={'center'}
          style={{ textWrap: 'wrap', wordBreak: 'break-word' }}
          gap="xs"
          data-alert-type={props.block.props.type}
        >
          <div contentEditable={false}>
            <Checkbox
              checked={props.block.props.type}
              // mt="1"
              ml="2"
              onChange={(event) =>
                props.editor.updateBlock(props.block, {
                  type: 'checkbox',
                  props: { type: event.currentTarget.checked },
                })
              }
              radius="1"
              color="indigo"
            />
          </div>
          <Box
            className={'inline-content'}
            style={{ textDecoration: props.block.props.type ? 'line-through' : '' }}
            ref={props.contentRef}
          />
        </Flex>
      );
    },
  }
);

@YousefED
Copy link
Collaborator

YousefED commented May 7, 2024

Other todos:
[ ] Placeholder text
[ ] make UI same as notion (strike when completed, etc)

@YousefED
Copy link
Collaborator

[] make sure we can't change checked status when doc is not editable

# Conflicts:
#	docs/package.json
#	lerna.json
#	package-lock.json
#	packages/ariakit/package.json
#	packages/core/package.json
#	packages/core/src/api/testUtil/cases/defaultSchema.ts
#	packages/core/src/blocks/defaultBlocks.ts
#	packages/mantine/package.json
#	packages/react/package.json
#	packages/shadcn/package.json
#	playground/package.json
#	tests/package.json
#	tests/src/end-to-end/ariakit/ariakit.test.ts-snapshots/ariakit-slash-menu-chromium-linux.png
#	tests/src/end-to-end/ariakit/ariakit.test.ts-snapshots/ariakit-slash-menu-firefox-linux.png
#	tests/src/end-to-end/ariakit/ariakit.test.ts-snapshots/ariakit-slash-menu-webkit-linux.png
#	tests/src/end-to-end/shadcn/shadcn.test.ts-snapshots/shadcn-slash-menu-chromium-linux.png
#	tests/src/end-to-end/shadcn/shadcn.test.ts-snapshots/shadcn-slash-menu-firefox-linux.png
#	tests/src/end-to-end/shadcn/shadcn.test.ts-snapshots/shadcn-slash-menu-webkit-linux.png
#	tests/src/end-to-end/theming/theming.test.ts-snapshots/dark-slash-menu-chromium-linux.png
#	tests/src/end-to-end/theming/theming.test.ts-snapshots/dark-slash-menu-firefox-linux.png
#	tests/src/end-to-end/theming/theming.test.ts-snapshots/dark-slash-menu-webkit-linux.png
Copy link
Collaborator

@YousefED YousefED left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

awesome ❤️

@@ -232,6 +228,13 @@ const checkListItemBlockContent = createStronglyTypedTiptapNode({
this.options.domAttributes?.inlineContent || {}
);

if (typeof getPos !== "boolean") {
const label =
"label-" + this.editor.state.doc.resolve(getPos()).node().attrs.id;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you comment (in code) or fix, why we can't just use node.attrs.id? (but need resolve + getpos)

if (typeof getPos === "boolean") {
return;
if (!editor.isEditable) {
checkbox.checked = !checkbox.checked;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this necessary or can we also use preventDefault or return false to prevent the change?

if it's necessary, please comment in code why we need this (it's a typical piece of code that might raise questions / confuse ourselves later!)

@@ -17,6 +17,7 @@ const BulletListItemBlockContent = createStronglyTypedTiptapNode({
name: "bulletListItem",
content: "inline*",
group: "blockContent",
priority: 90,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

still needed?

return {
checked: {
default: false,
// instead of "level" attributes, use "data-level"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fix

.BNSplitBlock(
state.selection.from,
selectionAtBlockStart,
selectionAtBlockStart
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why was this added? (the keepprops system)? (maybe comment as well?)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

TODO items
4 participants