@crashbytes/contentful-richtext-editor 1.0.2 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,12 +4,12 @@ A modern, Tiptap-based rich text editor that's fully compatible with Contentful'
4
4
 
5
5
  ## Features
6
6
 
7
- ✅ **Full Contentful Compatibility** - Seamless conversion between Contentful and Tiptap formats
8
- ✅ **Modern UI** - Clean, intuitive interface matching Contentful's design
9
- ✅ **TypeScript Support** - Complete type safety with Contentful's rich text types
10
- ✅ **Extensible** - Built on Tiptap v2 for easy customization
11
- ✅ **Lightweight** - Tree-shakeable, only import what you need
12
- ✅ **Responsive** - Works on desktop and mobile devices
7
+ - ✅ **Full Contentful Compatibility** - Seamless conversion between Contentful and Tiptap formats
8
+ - ✅ **Modern UI** - Clean, intuitive interface matching Contentful's design
9
+ - ✅ **TypeScript Support** - Complete type safety with Contentful's rich text types
10
+ - ✅ **Extensible** - Built on Tiptap v2 for easy customization
11
+ - ✅ **Lightweight** - Tree-shakeable, only import what you need
12
+ - ✅ **Responsive** - Works on desktop and mobile devices
13
13
 
14
14
  ## Supported Features
15
15
 
@@ -24,22 +24,22 @@ A modern, Tiptap-based rich text editor that's fully compatible with Contentful'
24
24
 
25
25
  ## Installation
26
26
 
27
- ```bash
27
+ ```
28
28
  npm install @crashbytes/contentful-richtext-editor
29
29
  ```
30
30
 
31
31
  ## Basic Usage
32
32
 
33
- ```tsx
33
+ ```javascript
34
34
  import React, { useState } from 'react';
35
35
  import { ContentfulRichTextEditor } from '@crashbytes/contentful-richtext-editor';
36
36
  import '@crashbytes/contentful-richtext-editor/dist/index.css';
37
37
  import { Document } from '@contentful/rich-text-types';
38
38
 
39
39
  function App() {
40
- const [content, setContent] = useState<Document>();
40
+ const [content, setContent] = useState();
41
41
 
42
- const handleChange = (document: Document) => {
42
+ const handleChange = (document) => {
43
43
  setContent(document);
44
44
  console.log('Contentful document:', document);
45
45
  };
@@ -63,7 +63,7 @@ export default App;
63
63
 
64
64
  ### With Contentful Entry/Asset Embedding
65
65
 
66
- ```tsx
66
+ ```javascript
67
67
  import { ContentfulRichTextEditor } from '@crashbytes/contentful-richtext-editor';
68
68
  import '@crashbytes/contentful-richtext-editor/dist/index.css';
69
69
 
@@ -94,7 +94,7 @@ function ContentfulEditor() {
94
94
 
95
95
  ### Customizing Features
96
96
 
97
- ```tsx
97
+ ```javascript
98
98
  <ContentfulRichTextEditor
99
99
  placeholder="Simple editor..."
100
100
  disabledFeatures={['table', 'embed', 'quote']}
@@ -104,9 +104,20 @@ function ContentfulEditor() {
104
104
  />
105
105
  ```
106
106
 
107
+ ### Controlling Available Headings and Formatting
108
+
109
+ ```javascript
110
+ <ContentfulRichTextEditor
111
+ placeholder="Limited editor..."
112
+ availableHeadings={[1, 2, 3]} // Only H1, H2, H3
113
+ availableMarks={['bold', 'italic']} // Only bold and italic, no underline
114
+ onChange={handleChange}
115
+ />
116
+ ```
117
+
107
118
  ### With Initial Content
108
119
 
109
- ```tsx
120
+ ```javascript
110
121
  import { createEmptyDocument } from '@crashbytes/contentful-richtext-editor';
111
122
 
112
123
  const initialContent = {
@@ -149,6 +160,8 @@ const initialContent = {
149
160
  | `className` | `string` | `''` | Additional CSS classes |
150
161
  | `theme` | `'default' \| 'minimal' \| 'contentful'` | `'contentful'` | Visual theme |
151
162
  | `disabledFeatures` | `Array<string>` | `[]` | Features to disable |
163
+ | `availableHeadings` | `Array<1\|2\|3\|4\|5\|6>` | `[1,2,3,4,5,6]` | Which heading levels to show |
164
+ | `availableMarks` | `Array<'bold'\|'italic'\|'underline'>` | `['bold','italic','underline']` | Which text formatting options to show |
152
165
 
153
166
  ### Disabled Features
154
167
 
@@ -166,7 +179,7 @@ You can disable specific features by passing them in the `disabledFeatures` arra
166
179
 
167
180
  ### Utility Functions
168
181
 
169
- ```tsx
182
+ ```javascript
170
183
  import {
171
184
  contentfulToTiptap,
172
185
  tiptapToContentful,
@@ -189,7 +202,7 @@ const emptyDoc = createEmptyDocument();
189
202
 
190
203
  The editor comes with default styles that match Contentful's design. Import the CSS:
191
204
 
192
- ```tsx
205
+ ```javascript
193
206
  import '@crashbytes/contentful-richtext-editor/dist/index.css';
194
207
  ```
195
208
 
@@ -224,7 +237,7 @@ Standard rich text editor appearance with serif fonts.
224
237
 
225
238
  ## Integration with Next.js
226
239
 
227
- ```tsx
240
+ ```javascript
228
241
  // pages/editor.tsx or app/editor/page.tsx
229
242
  import dynamic from 'next/dynamic';
230
243
 
@@ -249,7 +262,7 @@ export default function EditorPage() {
249
262
 
250
263
  This package is written in TypeScript and includes full type definitions. All Contentful rich text types are re-exported for convenience:
251
264
 
252
- ```tsx
265
+ ```javascript
253
266
  import type {
254
267
  Document,
255
268
  Block,
@@ -20,6 +20,10 @@ export interface ContentfulRichTextEditorProps {
20
20
  disabledFeatures?: Array<'bold' | 'italic' | 'underline' | 'link' | 'lists' | 'headings' | 'quote' | 'table' | 'embed'>;
21
21
  /** Custom styling options */
22
22
  theme?: 'default' | 'minimal' | 'contentful';
23
+ /** Which heading levels to make available (1-6) */
24
+ availableHeadings?: Array<1 | 2 | 3 | 4 | 5 | 6>;
25
+ /** Which text formatting marks to make available */
26
+ availableMarks?: Array<'bold' | 'italic' | 'underline'>;
23
27
  }
24
28
  export declare const ContentfulRichTextEditor: React.FC<ContentfulRichTextEditorProps>;
25
29
  export default ContentfulRichTextEditor;
@@ -5,6 +5,8 @@ interface ToolbarProps {
5
5
  onEmbedEntry?: () => void;
6
6
  onEmbedAsset?: () => void;
7
7
  disabledFeatures?: Array<string>;
8
+ availableHeadings?: Array<1 | 2 | 3 | 4 | 5 | 6>;
9
+ availableMarks?: Array<'bold' | 'italic' | 'underline'>;
8
10
  }
9
11
  export declare const ContentfulToolbar: React.FC<ToolbarProps>;
10
12
  export {};
package/dist/index.esm.js CHANGED
@@ -26529,10 +26529,11 @@ const Underline = Mark.create({
26529
26529
  },
26530
26530
  });
26531
26531
 
26532
- const ContentfulToolbar = ({ editor, onEmbedEntry, onEmbedAsset, disabledFeatures = [] }) => {
26532
+ const ContentfulToolbar = ({ editor, onEmbedEntry, onEmbedAsset, disabledFeatures = [], availableHeadings = [1, 2, 3, 4, 5, 6], availableMarks = ['bold', 'italic', 'underline'] }) => {
26533
26533
  const [showLinkInput, setShowLinkInput] = useState(false);
26534
26534
  const [linkUrl, setLinkUrl] = useState('');
26535
26535
  const isDisabled = (feature) => disabledFeatures.includes(feature);
26536
+ const isMarkAvailable = (mark) => availableMarks.includes(mark);
26536
26537
  const handleHeadingChange = (level) => {
26537
26538
  if (level === 0) {
26538
26539
  editor.chain().focus().setParagraph().run();
@@ -26561,14 +26562,15 @@ const ContentfulToolbar = ({ editor, onEmbedEntry, onEmbedAsset, disabledFeature
26561
26562
  editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run();
26562
26563
  };
26563
26564
  const getActiveHeading = () => {
26564
- for (let i = 1; i <= 6; i++) {
26565
- if (editor.isActive('heading', { level: i })) {
26566
- return `Heading ${i}`;
26565
+ for (const level of availableHeadings) {
26566
+ if (editor.isActive('heading', { level })) {
26567
+ return `Heading ${level}`;
26567
26568
  }
26568
26569
  }
26569
26570
  return 'Normal text';
26570
26571
  };
26571
- return (jsxs("div", { className: "contentful-toolbar", children: [jsxs("div", { className: "contentful-toolbar__group", children: [!isDisabled('headings') && (jsxs("select", { className: "contentful-toolbar__select", value: getActiveHeading(), onChange: (e) => {
26572
+ const hasHeadings = !isDisabled('headings') && availableHeadings.length > 0;
26573
+ return (jsxs("div", { className: "contentful-toolbar", children: [jsxs("div", { className: "contentful-toolbar__group", children: [hasHeadings && (jsxs("select", { className: "contentful-toolbar__select", value: getActiveHeading(), onChange: (e) => {
26572
26574
  const value = e.target.value;
26573
26575
  if (value === 'Normal text') {
26574
26576
  handleHeadingChange(0);
@@ -26577,7 +26579,7 @@ const ContentfulToolbar = ({ editor, onEmbedEntry, onEmbedAsset, disabledFeature
26577
26579
  const level = parseInt(value.replace('Heading ', ''));
26578
26580
  handleHeadingChange(level);
26579
26581
  }
26580
- }, children: [jsx("option", { value: "Normal text", children: "Normal text" }), jsx("option", { value: "Heading 1", children: "Heading 1" }), jsx("option", { value: "Heading 2", children: "Heading 2" }), jsx("option", { value: "Heading 3", children: "Heading 3" }), jsx("option", { value: "Heading 4", children: "Heading 4" }), jsx("option", { value: "Heading 5", children: "Heading 5" }), jsx("option", { value: "Heading 6", children: "Heading 6" })] })), jsx("button", { className: "contentful-toolbar__button", onClick: () => editor.chain().focus().undo().run(), disabled: !editor.can().undo(), title: "Undo", children: "\u21B6" }), jsx("button", { className: "contentful-toolbar__button", onClick: () => editor.chain().focus().redo().run(), disabled: !editor.can().redo(), title: "Redo", children: "\u21B7" })] }), jsx("div", { className: "contentful-toolbar__separator" }), jsxs("div", { className: "contentful-toolbar__group", children: [!isDisabled('bold') && (jsx("button", { className: `contentful-toolbar__button ${editor.isActive('bold') ? 'contentful-toolbar__button--active' : ''}`, onClick: () => editor.chain().focus().toggleBold().run(), title: "Bold", children: jsx("strong", { children: "B" }) })), !isDisabled('italic') && (jsx("button", { className: `contentful-toolbar__button ${editor.isActive('italic') ? 'contentful-toolbar__button--active' : ''}`, onClick: () => editor.chain().focus().toggleItalic().run(), title: "Italic", children: jsx("em", { children: "I" }) })), !isDisabled('underline') && (jsx("button", { className: `contentful-toolbar__button ${editor.isActive('underline') ? 'contentful-toolbar__button--active' : ''}`, onClick: () => editor.chain().focus().toggleUnderline().run(), title: "Underline", children: jsx("u", { children: "U" }) })), jsx("button", { className: "contentful-toolbar__button", title: "More formatting options", children: "\u22EF" }), !isDisabled('link') && (jsxs(Fragment$1, { children: [jsx("button", { className: `contentful-toolbar__button ${editor.isActive('link') ? 'contentful-toolbar__button--active' : ''}`, onClick: handleLinkToggle, title: "Link", children: "\uD83D\uDD17" }), showLinkInput && (jsxs("div", { className: "contentful-toolbar__link-input", children: [jsx("input", { type: "url", value: linkUrl, onChange: (e) => setLinkUrl(e.target.value), placeholder: "Enter URL", onKeyDown: (e) => {
26582
+ }, children: [jsx("option", { value: "Normal text", children: "Normal text" }), availableHeadings.map(level => (jsxs("option", { value: `Heading ${level}`, children: ["Heading ", level] }, level)))] })), jsx("button", { className: "contentful-toolbar__button", onClick: () => editor.chain().focus().undo().run(), disabled: !editor.can().undo(), title: "Undo", children: "\u21B6" }), jsx("button", { className: "contentful-toolbar__button", onClick: () => editor.chain().focus().redo().run(), disabled: !editor.can().redo(), title: "Redo", children: "\u21B7" })] }), jsx("div", { className: "contentful-toolbar__separator" }), jsxs("div", { className: "contentful-toolbar__group", children: [!isDisabled('bold') && isMarkAvailable('bold') && (jsx("button", { className: `contentful-toolbar__button ${editor.isActive('bold') ? 'contentful-toolbar__button--active' : ''}`, onClick: () => editor.chain().focus().toggleBold().run(), title: "Bold", children: jsx("strong", { children: "B" }) })), !isDisabled('italic') && isMarkAvailable('italic') && (jsx("button", { className: `contentful-toolbar__button ${editor.isActive('italic') ? 'contentful-toolbar__button--active' : ''}`, onClick: () => editor.chain().focus().toggleItalic().run(), title: "Italic", children: jsx("em", { children: "I" }) })), !isDisabled('underline') && isMarkAvailable('underline') && (jsx("button", { className: `contentful-toolbar__button ${editor.isActive('underline') ? 'contentful-toolbar__button--active' : ''}`, onClick: () => editor.chain().focus().toggleUnderline().run(), title: "Underline", children: jsx("u", { children: "U" }) })), jsx("button", { className: "contentful-toolbar__button", title: "More formatting options", children: "\u22EF" }), !isDisabled('link') && (jsxs(Fragment$1, { children: [jsx("button", { className: `contentful-toolbar__button ${editor.isActive('link') ? 'contentful-toolbar__button--active' : ''}`, onClick: handleLinkToggle, title: "Link", children: "\uD83D\uDD17" }), showLinkInput && (jsxs("div", { className: "contentful-toolbar__link-input", children: [jsx("input", { type: "url", value: linkUrl, onChange: (e) => setLinkUrl(e.target.value), placeholder: "Enter URL", onKeyDown: (e) => {
26581
26583
  if (e.key === 'Enter') {
26582
26584
  handleLinkSubmit();
26583
26585
  }
@@ -27268,59 +27270,63 @@ const createEmptyDocument = () => ({
27268
27270
  ],
27269
27271
  });
27270
27272
 
27271
- const ContentfulRichTextEditor = ({ initialValue, onChange, onEmbedEntry, onEmbedAsset, className = '', readonly = false, placeholder = 'Start writing...', disabledFeatures = [], theme = 'contentful' }) => {
27273
+ const ContentfulRichTextEditor = ({ initialValue, onChange, onEmbedEntry, onEmbedAsset, className = '', readonly = false, placeholder = 'Start writing...', disabledFeatures = [], theme = 'contentful', availableHeadings = [1, 2, 3, 4, 5, 6], availableMarks = ['bold', 'italic', 'underline'] }) => {
27274
+ // Build extensions array based on available features
27275
+ const extensions = [];
27276
+ // Add StarterKit with configuration
27277
+ extensions.push(StarterKit.configure({
27278
+ heading: {
27279
+ levels: availableHeadings,
27280
+ },
27281
+ bold: availableMarks.includes('bold') ? {} : false,
27282
+ italic: availableMarks.includes('italic') ? {} : false,
27283
+ bulletList: {
27284
+ HTMLAttributes: {
27285
+ class: 'contentful-bullet-list',
27286
+ },
27287
+ },
27288
+ orderedList: {
27289
+ HTMLAttributes: {
27290
+ class: 'contentful-ordered-list',
27291
+ },
27292
+ },
27293
+ blockquote: {
27294
+ HTMLAttributes: {
27295
+ class: 'contentful-blockquote',
27296
+ },
27297
+ },
27298
+ }));
27299
+ // Add underline extension only if it's in availableMarks
27300
+ if (availableMarks.includes('underline')) {
27301
+ extensions.push(Underline);
27302
+ }
27303
+ // Add other extensions
27304
+ extensions.push(Link.configure({
27305
+ openOnClick: false,
27306
+ HTMLAttributes: {
27307
+ class: 'contentful-link',
27308
+ rel: 'noopener noreferrer',
27309
+ },
27310
+ }), Table.configure({
27311
+ resizable: true,
27312
+ HTMLAttributes: {
27313
+ class: 'contentful-table',
27314
+ },
27315
+ }), TableRow.configure({
27316
+ HTMLAttributes: {
27317
+ class: 'contentful-table-row',
27318
+ },
27319
+ }), TableHeader.configure({
27320
+ HTMLAttributes: {
27321
+ class: 'contentful-table-header',
27322
+ },
27323
+ }), TableCell.configure({
27324
+ HTMLAttributes: {
27325
+ class: 'contentful-table-cell',
27326
+ },
27327
+ }));
27272
27328
  const editor = useEditor({
27273
- extensions: [
27274
- StarterKit.configure({
27275
- heading: {
27276
- levels: [1, 2, 3, 4, 5, 6],
27277
- },
27278
- bulletList: {
27279
- HTMLAttributes: {
27280
- class: 'contentful-bullet-list',
27281
- },
27282
- },
27283
- orderedList: {
27284
- HTMLAttributes: {
27285
- class: 'contentful-ordered-list',
27286
- },
27287
- },
27288
- blockquote: {
27289
- HTMLAttributes: {
27290
- class: 'contentful-blockquote',
27291
- },
27292
- },
27293
- }),
27294
- Underline,
27295
- Link.configure({
27296
- openOnClick: false,
27297
- HTMLAttributes: {
27298
- class: 'contentful-link',
27299
- rel: 'noopener noreferrer',
27300
- },
27301
- }),
27302
- Table.configure({
27303
- resizable: true,
27304
- HTMLAttributes: {
27305
- class: 'contentful-table',
27306
- },
27307
- }),
27308
- TableRow.configure({
27309
- HTMLAttributes: {
27310
- class: 'contentful-table-row',
27311
- },
27312
- }),
27313
- TableHeader.configure({
27314
- HTMLAttributes: {
27315
- class: 'contentful-table-header',
27316
- },
27317
- }),
27318
- TableCell.configure({
27319
- HTMLAttributes: {
27320
- class: 'contentful-table-cell',
27321
- },
27322
- }),
27323
- ],
27329
+ extensions,
27324
27330
  content: initialValue ? contentfulToTiptap(initialValue) : '',
27325
27331
  editable: !readonly,
27326
27332
  editorProps: {
@@ -27399,7 +27405,7 @@ const ContentfulRichTextEditor = ({ initialValue, onChange, onEmbedEntry, onEmbe
27399
27405
  if (!editor) {
27400
27406
  return (jsx("div", { className: `contentful-editor contentful-editor--loading ${className}`, children: jsx("div", { className: "contentful-editor__loading", children: "Loading editor..." }) }));
27401
27407
  }
27402
- return (jsxs("div", { className: `contentful-editor contentful-editor--${theme} ${className}`, children: [!readonly && (jsx(ContentfulToolbar, { editor: editor, onEmbedEntry: handleEmbedEntry, onEmbedAsset: handleEmbedAsset, disabledFeatures: disabledFeatures })), jsx("div", { className: "contentful-editor__content-wrapper", children: jsx(EditorContent, { editor: editor, className: "contentful-editor__content" }) })] }));
27408
+ return (jsxs("div", { className: `contentful-editor contentful-editor--${theme} ${className}`, children: [!readonly && (jsx(ContentfulToolbar, { editor: editor, onEmbedEntry: handleEmbedEntry, onEmbedAsset: handleEmbedAsset, disabledFeatures: disabledFeatures, availableHeadings: availableHeadings, availableMarks: availableMarks })), jsx("div", { className: "contentful-editor__content-wrapper", children: jsx(EditorContent, { editor: editor, className: "contentful-editor__content" }) })] }));
27403
27409
  };
27404
27410
 
27405
27411
  export { ContentfulRichTextEditor, ContentfulToolbar, contentfulToTiptap, createEmptyDocument, tiptapToContentful, validateContentfulDocument };