Skip to content

Commit

Permalink
Add rich text content type
Browse files Browse the repository at this point in the history
The editor is a very basic one. More features will be implemented later.
  • Loading branch information
arthur-fontaine committed Aug 28, 2023
1 parent 7321bf2 commit 41d6672
Show file tree
Hide file tree
Showing 6 changed files with 749 additions and 11 deletions.
4 changes: 4 additions & 0 deletions packages/octent/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-tabs": "^1.0.4",
"@tiptap/extension-underline": "^2.1.7",
"@tiptap/pm": "^2.1.7",
"@tiptap/react": "^2.1.7",
"@tiptap/starter-kit": "^2.1.7",
"class-variance-authority": "^0.6.0",
"clsx": "^1.2.1",
"isomorphic-git": "^1.24.0",
Expand Down
20 changes: 12 additions & 8 deletions packages/octent/src/lib/utils/content-explorer/content-explorer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,25 @@ import { createUpdateCollection } from './functions/create-update-collection'
import { createUpdateContent } from './functions/create-update-content'

export const AVAILABLE_FIELD_TYPES = [
'string', 'number', 'boolean',
'string', 'number', 'boolean', 'rich-text',
] as const

export interface Field {
name: string
type: typeof AVAILABLE_FIELD_TYPES[number]
}

type InferFieldType<T extends Field['type']> = T extends 'string'
? string
: T extends 'number'
? number
: T extends 'boolean'
? boolean
: never
type InferFieldType<T extends Field['type']> = (
(T extends 'string'
? string
: (T extends 'number'
? number
: (T extends 'boolean'
? boolean
: (T extends 'rich-text'
? string
: never))))
)
type InferField<T extends Field> = {
[K in T['name']]: InferFieldType<T['type']>
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import { Underline as TipTapUnderline } from '@tiptap/extension-underline'
import {
type ChainedCommands, type Editor, type CanCommands,
EditorProvider, useCurrentEditor,
} from '@tiptap/react'
import { StarterKit as TipTapStarterKit } from '@tiptap/starter-kit'
import {
type LucideIcon,
BoldIcon, ItalicIcon, UnderlineIcon, ListOrderedIcon, ListIcon,
} from 'lucide-react'
import React from 'react'

import { Separator } from '@/components/ui/separator'
import { cn } from '@/lib/utils'

import { ContentInputProperties } from './content-input'

const TIPTAP_EXTENSIONS = [
TipTapStarterKit.configure({
bulletList: {
keepMarks: true,
keepAttributes: false,
},
orderedList: {
keepMarks: true,
keepAttributes: false,
},
}),
TipTapUnderline,
]


/**
* @param properties The properties of the content input.
* @returns The content input string.
*/
export function ContentInputRichText(properties: ContentInputProperties) {
return (
<div
className='flex flex-col w-full rounded-md border border-input
bg-background text-sm h-60 [&>*:last-child]:flex-1'
>
<EditorProvider
extensions={TIPTAP_EXTENSIONS}
content={properties.defaultValue as string}
slotBefore={<MenuBar />}
onUpdate={function ({ editor }) {
console.log(editor.getHTML())
}}
editorProps={{
attributes: {
class: 'px-3 py-2 ring-offset-background rounded-md h-full ' +
'prose-focus:outline-none prose-focus:ring-2 ' +
'prose-focus:ring-ring prose-focus:ring-offset-2 ' +
'disabled:cursor-not-allowed disabled:opacity-50 ' +
'[&_ul]:list-disc [&_ol]:list-decimal [&_ul]:list-inside ' +
'[&_ol]:list-inside [&_li>*]:inline',
},
}}
>
</EditorProvider>
</div>
)
}

const MenuBar = function () {
const { editor } = useCurrentEditor()

if (!editor) {
return null
}

return (
<div className='flex flex-row flex-wrap gap-2 bg-input/50 px-3 py-2'>
<MenuItem
icon={BoldIcon}
action_name='toggleBold'
is_active={'bold'}
/>
<MenuItem
icon={ItalicIcon}
action_name='toggleItalic'
is_active={'italic'}
/>
<MenuItem
icon={UnderlineIcon}
action_name='toggleUnderline'
is_active='underline'
/>

<Separator orientation='vertical' className='h-auto' />

<MenuItem
icon={ListOrderedIcon}
action_name='toggleOrderedList'
is_active='orderedList'
/>
<MenuItem
icon={ListIcon}
action_name='toggleBulletList'
is_active='bulletList'
/>
</div>
)
}

interface MenuItemProperties<A extends keyof ChainedCommands> {
icon: LucideIcon
action_name: A
action_arguments?: Parameters<ChainedCommands[A]>
is_active: Parameters<Editor['isActive']>[0]
}

const MenuItem = function <A extends keyof ChainedCommands>(
// eslint-disable-next-line functional/prefer-immutable-types
properties: MenuItemProperties<A>,
) {
const { editor } = useCurrentEditor()

if (editor === null) {
return null
}

const action_name = properties.action_name
const action_arguments = properties.action_arguments ?? []

// eslint-disable-next-line functional/prefer-immutable-types
const getEditorChain = function (editor: Editor | CanCommands) {
// eslint-disable-next-line lines-around-comment
// @ts-expect-error -- TS doesn't like the spread operator here.
const chain = editor.chain().focus()[action_name](...action_arguments)

if (typeof chain === 'boolean') {
return undefined
}

return chain
}

return <button
onClick={function () {
return getEditorChain(editor)?.run()
}}
disabled={
(function () {
return !getEditorChain(editor.can())?.run()
})()
}
className={
cn(
editor.isActive(properties.is_active)
? 'bg-input'
: '',
'p-1 rounded-md hover:bg-input',
)
}
>
<properties.icon className='w-5 h-5' />
</button>
}

Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { Field } from '@/lib/utils/content-explorer/content-explorer'

import { ContentInputBoolean } from './content-input-boolean'
import { ContentInputNumber } from './content-input-number'
import { ContentInputRichText } from './content-input-rich-text'
import { ContentInputString } from './content-input-string'

// eslint-disable-next-line @typescript-eslint/naming-convention
Expand All @@ -29,5 +30,8 @@ export function ContentInput(properties: ContentInputProperties) {
case 'boolean': {
return <ContentInputBoolean {...properties} />
}
case 'rich-text': {
return <ContentInputRichText {...properties} />
}
}
}
9 changes: 8 additions & 1 deletion packages/octent/tailwind.config.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
const plugin = require("tailwindcss/plugin")

/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: ["class"],
Expand Down Expand Up @@ -72,5 +74,10 @@ module.exports = {
},
},
},
plugins: [require("tailwindcss-animate")],
plugins: [
require("tailwindcss-animate"),
plugin(function ({ addVariant }) {
addVariant("prose-focus", "&.ProseMirror-focused")
}),
],
}
Loading

0 comments on commit 41d6672

Please sign in to comment.