diff --git a/packages/koenig-lexical/.storybook/main.ts b/packages/koenig-lexical/.storybook/main.ts index 00c4f1604c..d552cce75e 100644 --- a/packages/koenig-lexical/.storybook/main.ts +++ b/packages/koenig-lexical/.storybook/main.ts @@ -1,7 +1,10 @@ import { dirname, join } from "path"; +import { createRequire } from "module"; import { mergeConfig } from 'vite'; import type {StorybookConfig} from '@storybook/react-vite'; +const require = createRequire(import.meta.url); + const config: StorybookConfig = { framework: { name: getAbsolutePath("@storybook/react-vite"), diff --git a/packages/koenig-lexical/demo/DemoApp.jsx b/packages/koenig-lexical/demo/DemoApp.tsx similarity index 61% rename from packages/koenig-lexical/demo/DemoApp.jsx rename to packages/koenig-lexical/demo/DemoApp.tsx index 97376bcf74..f2f8a6f7ff 100644 --- a/packages/koenig-lexical/demo/DemoApp.jsx +++ b/packages/koenig-lexical/demo/DemoApp.tsx @@ -6,19 +6,23 @@ import InitialContentToggle from './components/InitialContentToggle'; import LockIcon from './assets/icons/kg-lock.svg?react'; import React, {useState} from 'react'; import Sidebar from './components/Sidebar'; -import TitleTextBox from './components/TitleTextBox'; +import TitleTextBox, {type TitleTextBoxHandle} from './components/TitleTextBox'; import Watermark from './components/Watermark'; import WordCount from './components/WordCount'; import basicContent from './content/basic-content.json'; import content from './content/content.json'; import emailContent from './content/email-content.json'; import minimalContent from './content/minimal-content.json'; -import {$getRoot, $isDecoratorNode} from 'lexical'; +import {$getRoot, $isDecoratorNode, type Klass, type LexicalNode, type LexicalNodeReplacement} from 'lexical'; import { - BASIC_NODES, BASIC_TRANSFORMERS, EmailEditor, - KoenigComposableEditor, KoenigComposer, KoenigEditor, MINIMAL_NODES, - MINIMAL_TRANSFORMERS, RestrictContentPlugin, TKCountPlugin, WordCountPlugin + BASIC_NODES, BASIC_TRANSFORMERS, BookmarkPlugin, + ButtonPlugin, CallToActionPlugin, CalloutPlugin, CardMenuPlugin, EMAIL_EDITOR_NODES, + EMAIL_TRANSFORMERS, EmEnDashPlugin, EmailCtaPlugin, EmbedPlugin, EmojiPickerPlugin, + HorizontalRulePlugin, HtmlPlugin, ImagePlugin, + KoenigComposableEditor, KoenigComposer, KoenigEditor, KoenigSelectorPlugin, KoenigSnippetPlugin, ListPlugin, MINIMAL_NODES, + MINIMAL_TRANSFORMERS, ProductPlugin, ReplacementStringsPlugin, RestrictContentPlugin, TKCountPlugin, WordCountPlugin } from '../src'; +import {VISIBILITY_SETTINGS} from '../src/utils/visibility'; import {defaultHeaders as defaultUnsplashHeaders} from './utils/unsplashConfig'; import {fetchEmbed} from './utils/fetchEmbed'; import {fileTypes, useFileUpload} from './utils/useFileUpload'; @@ -33,7 +37,7 @@ const WEBSOCKET_ID = params.get('multiplayerId') || '0'; // show deprecated cards by default so they can be tested, unless explicitly hidden // so we can test they are removed from the menu when deprecated/behind a feature flag -function hideDeprecatedCardInMenu(searchParams) { +function hideDeprecatedCardInMenu(searchParams: URLSearchParams) { // allow tests to opt in to hiding deprecated cards if (searchParams.get('hideDeprecatedCards') === 'true') { return true; @@ -62,7 +66,7 @@ const defaultCardConfig = { transistor: false }, // this enables the internal linking feature, can be disabled with `/#/?searchLinks=false` - searchLinks: async (term) => { + searchLinks: async (term: string) => { // default to showing latest posts when search is empty // no delay to simulate posts being pre-loaded in editor if (!term) { @@ -110,7 +114,7 @@ const defaultCardConfig = { } }; -function getDefaultContent({editorType}) { +function getDefaultContent({editorType}: {editorType?: string}) { if (editorType === 'basic') { return basicContent; } else if (editorType === 'minimal') { @@ -121,16 +125,38 @@ function getDefaultContent({editorType}) { return content; } -function getAllowedNodes({editorType}) { +function getAllowedNodes({editorType}: {editorType?: string}): ReadonlyArray | LexicalNodeReplacement> | undefined { if (editorType === 'basic') { - return BASIC_NODES; + return BASIC_NODES as ReadonlyArray | LexicalNodeReplacement>; } else if (editorType === 'minimal') { - return MINIMAL_NODES; + return MINIMAL_NODES as ReadonlyArray | LexicalNodeReplacement>; + } else if (editorType === 'email') { + return EMAIL_EDITOR_NODES as ReadonlyArray | LexicalNodeReplacement>; } return undefined; } -function DemoEditor({editorType, registerAPI, cursorDidExitAtTop, darkMode, setWordCount, setTKCount}) { +interface DemoEditorAPI { + editorInstance: unknown; + editorIsEmpty: () => boolean; + insertParagraphAtTop: (options: {focus: boolean}) => void; + insertParagraphAtBottom: () => void; + focusEditor: (options: {position: string}) => void; + insertFiles: (files: File[]) => void; + serialize: () => string; + [key: string]: unknown; +} + +interface DemoEditorProps { + editorType?: string; + registerAPI: (api: unknown) => void; + cursorDidExitAtTop: () => void; + darkMode: boolean; + setWordCount: (count: number) => void; + setTKCount: (count: number) => void; +} + +function DemoEditor({editorType, registerAPI, cursorDidExitAtTop, darkMode, setWordCount, setTKCount}: DemoEditorProps) { if (editorType === 'basic') { return ( ); + } else if (editorType === 'email') { + return ( + + + + + + + + + + + + + + + + + + + + + ); } return ( @@ -167,7 +221,14 @@ function DemoEditor({editorType, registerAPI, cursorDidExitAtTop, darkMode, setW ); } -function DemoComposer({editorType, isMultiplayer, setWordCount, setTKCount}) { +interface DemoComposerProps { + editorType?: string; + isMultiplayer?: boolean; + setWordCount: (count: number) => void; + setTKCount: (count: number) => void; +} + +function DemoComposer({editorType, isMultiplayer, setWordCount, setTKCount}: DemoComposerProps) { const [searchParams, setSearchParams] = useSearchParams(); const [isSidebarOpen, setIsSidebarOpen] = useState(false); const [sidebarView, setSidebarView] = useState('json'); @@ -195,9 +256,9 @@ function DemoComposer({editorType, isMultiplayer, setWordCount, setTKCount}) { }, [isMultiplayer, contentParam, defaultContent]); const [title, setTitle] = useState(initialContent ? 'Meet the Koenig editor.' : ''); - const [editorAPI, setEditorAPI] = useState(null); - const titleRef = React.useRef(null); - const containerRef = React.useRef(null); + const [editorAPI, setEditorAPI] = useState(null); + const titleRef = React.useRef(null); + const containerRef = React.useRef(null); function openSidebar(view = 'json') { if (isSidebarOpen && sidebarView === view) { @@ -215,27 +276,29 @@ function DemoComposer({editorType, isMultiplayer, setWordCount, setTKCount}) { // mouseup/click event can occur outside of the initially clicked node, in // which case we don't want to then "re-focus" the editor and cause unexpected // selection changes - function maybeSkipFocusEditor(event) { - const clickedOnDecorator = (event.target.closest('[data-lexical-decorator]') !== null) || event.target.hasAttribute('data-lexical-decorator'); - const clickedOnSlashMenu = (event.target.closest('[data-kg-slash-menu]') !== null) || event.target.hasAttribute('data-kg-slash-menu'); - const clickedOnPortal = (event.target.closest('[data-kg-portal]') !== null) || event.target.hasAttribute('data-kg-portal'); + function maybeSkipFocusEditor(event: React.MouseEvent) { + const target = event.target as HTMLElement; + const clickedOnDecorator = (target.closest('[data-lexical-decorator]') !== null) || target.hasAttribute('data-lexical-decorator'); + const clickedOnSlashMenu = (target.closest('[data-kg-slash-menu]') !== null) || target.hasAttribute('data-kg-slash-menu'); + const clickedOnPortal = (target.closest('[data-kg-portal]') !== null) || target.hasAttribute('data-kg-portal'); if (clickedOnDecorator || clickedOnSlashMenu || clickedOnPortal) { skipFocusEditor.current = true; } } - function focusEditor(event) { - const clickedOnDecorator = (event.target.closest('[data-lexical-decorator]') !== null) || event.target.hasAttribute('data-lexical-decorator'); - const clickedOnSlashMenu = (event.target.closest('[data-kg-slash-menu]') !== null) || event.target.hasAttribute('data-kg-slash-menu'); - const clickedOnPortal = (event.target.closest('[data-kg-portal]') !== null) || event.target.hasAttribute('data-kg-portal'); + function focusEditor(event: React.MouseEvent) { + const target = event.target as HTMLElement; + const clickedOnDecorator = (target.closest('[data-lexical-decorator]') !== null) || target.hasAttribute('data-lexical-decorator'); + const clickedOnSlashMenu = (target.closest('[data-kg-slash-menu]') !== null) || target.hasAttribute('data-kg-slash-menu'); + const clickedOnPortal = (target.closest('[data-kg-portal]') !== null) || target.hasAttribute('data-kg-portal'); if (!skipFocusEditor.current && editorAPI && !clickedOnDecorator && !clickedOnSlashMenu && !clickedOnPortal) { - let editor = editorAPI.editorInstance; + const editor = editorAPI.editorInstance as {_rootElement: HTMLElement; getEditorState: () => {read: (fn: () => void) => void}}; // if a mousedown and subsequent mouseup occurs below the editor // canvas, focus the editor and put the cursor at the end of the document - let {bottom} = editor._rootElement.getBoundingClientRect(); + const {bottom} = editor._rootElement.getBoundingClientRect(); if (event.pageY > bottom && event.clientY > bottom) { event.preventDefault(); @@ -261,7 +324,7 @@ function DemoComposer({editorType, isMultiplayer, setWordCount, setTKCount}) { editorAPI.focusEditor({position: 'bottom'}); //scroll to the bottom of the container - containerRef.current.scrollTop = containerRef.current.scrollHeight; + containerRef.current!.scrollTop = containerRef.current!.scrollHeight; } } @@ -278,19 +341,19 @@ function DemoComposer({editorType, isMultiplayer, setWordCount, setTKCount}) { } function saveContent() { - const serializedState = editorAPI.serialize(); + const serializedState = editorAPI!.serialize(); const encodedContent = encodeURIComponent(serializedState); searchParams.set('content', encodedContent); setSearchParams(searchParams); } React.useEffect(() => { - const handleFileDrag = (event) => { + const handleFileDrag = (event: DragEvent) => { event.preventDefault(); }; - const handleFileDrop = (event) => { - if (event.dataTransfer.files.length > 0) { + const handleFileDrop = (event: DragEvent) => { + if (event.dataTransfer && event.dataTransfer.files.length > 0) { event.preventDefault(); editorAPI?.insertFiles(Array.from(event.dataTransfer.files)); } @@ -305,7 +368,7 @@ function DemoComposer({editorType, isMultiplayer, setWordCount, setTKCount}) { }; }, [editorAPI]); - const showTitle = !isMultiplayer && !['basic', 'minimal', 'email'].includes(editorType); + const showTitle = !isMultiplayer && !['basic', 'minimal', 'email'].includes(editorType || ''); const isEmailEditor = editorType === 'email'; const cardConfig = { @@ -323,99 +386,85 @@ function DemoComposer({editorType, isMultiplayer, setWordCount, setTKCount}) { deprecated: { headerV1: hideDeprecatedCardInMenu(searchParams), emailCta: hideDeprecatedCardInMenu(searchParams) - } + }, + ...(isEmailEditor ? { + image: { + ...((defaultCardConfig as Record).image as Record || {}), + allowedWidths: ['regular'] + }, + visibilitySettings: VISIBILITY_SETTINGS.EMAIL_ONLY + } : {}) }; - const fileUploader = {useFileUpload: useFileUpload({isMultiplayer}), fileTypes}; - - // Sidebar uses useLexicalComposerContext so it must be inside a KoenigComposer. - // The email editor manages its own composer, so the sidebar is only available - // for non-email editor types. - const demoChrome = ( - <> - - {!isEmailEditor && ( -
- - -
- )} - - ); - - const demoLayout = (children) => ( -
- { - !isMultiplayer && !isEmailEditor && contentParam !== 'false' - ? - : null - } - -
-
- {showTitle - ? - : null - } - {children} -
-
-
- ); - - // Email editor includes its own KoenigComposer, so it renders outside the shared one - if (isEmailEditor) { - return ( - <> - {demoLayout( - - - - - - )} - {demoChrome} - - ); - } - return ( - {demoLayout( - - )} - {demoChrome} +
+ { + !isMultiplayer + ? + : null + } + +
+
+ {showTitle + ? + : null + } + {editorType === 'email' ? ( + + void} + setTKCount={setTKCount} + setWordCount={setWordCount} + /> + + ) : ( + void} + setTKCount={setTKCount} + setWordCount={setWordCount} + /> + )} +
+
+
+ +
+ + +
); } const MemoizedDemoComposer = React.memo(DemoComposer); -function DemoApp({editorType, isMultiplayer}) { +interface DemoAppProps { + editorType?: string; + isMultiplayer?: boolean; + introContent?: boolean; +} + +function DemoApp({editorType, isMultiplayer}: DemoAppProps) { const [wordCount, setWordCount] = useState(0); const [tkCount, setTKCount] = useState(0); diff --git a/packages/koenig-lexical/demo/HtmlOutputDemo.jsx b/packages/koenig-lexical/demo/HtmlOutputDemo.tsx similarity index 78% rename from packages/koenig-lexical/demo/HtmlOutputDemo.jsx rename to packages/koenig-lexical/demo/HtmlOutputDemo.tsx index 547f9998e3..cce8a121c4 100644 --- a/packages/koenig-lexical/demo/HtmlOutputDemo.jsx +++ b/packages/koenig-lexical/demo/HtmlOutputDemo.tsx @@ -20,9 +20,14 @@ function HtmlOutputDemo() { const [html, setHtml] = useState('

check ghost.org/changelog/markdown/

'); const [sidebarView, setSidebarView] = useState('json'); const [defaultContent] = useState(undefined); - const [editorAPI, setEditorAPI] = useState(null); - const titleRef = React.useRef(null); - const containerRef = React.useRef(null); + const [editorAPI, setEditorAPI] = useState<{ + editorInstance: unknown; + insertParagraphAtBottom: () => void; + focusEditor: (options: {position: string}) => void; + [key: string]: unknown; + } | null>(null); + const titleRef = React.useRef<{focus: () => void} | null>(null); + const containerRef = React.useRef(null); const {snippets, createSnippet, deleteSnippet} = useSnippets(); function openSidebar(view = 'json') { @@ -37,13 +42,14 @@ function HtmlOutputDemo() { titleRef.current?.focus(); } - function focusEditor(event) { - const clickedOnDecorator = (event.target.closest('[data-lexical-decorator]') !== null) || event.target.hasAttribute('data-lexical-decorator'); - const clickedOnSlashMenu = (event.target.closest('[data-kg-slash-menu]') !== null) || event.target.hasAttribute('data-kg-slash-menu'); + function focusEditor(event: React.MouseEvent) { + const target = event.target as HTMLElement; + const clickedOnDecorator = (target.closest('[data-lexical-decorator]') !== null) || target.hasAttribute('data-lexical-decorator'); + const clickedOnSlashMenu = (target.closest('[data-kg-slash-menu]') !== null) || target.hasAttribute('data-kg-slash-menu'); if (editorAPI && !clickedOnDecorator && !clickedOnSlashMenu) { - let editor = editorAPI.editorInstance; - let {bottom} = editor._rootElement.getBoundingClientRect(); + const editor = editorAPI.editorInstance as {_rootElement: HTMLElement; getEditorState: () => {read: (fn: () => void) => void}}; + const {bottom} = editor._rootElement.getBoundingClientRect(); // if a mousedown and subsequent mouseup occurs below the editor // canvas, focus the editor and put the cursor at the end of the document @@ -72,7 +78,7 @@ function HtmlOutputDemo() { editorAPI.focusEditor({position: 'bottom'}); //scroll to the bottom of the container - containerRef.current.scrollTop = containerRef.current.scrollHeight; + containerRef.current!.scrollTop = containerRef.current!.scrollHeight; } } } @@ -93,7 +99,7 @@ function HtmlOutputDemo() {
void} > diff --git a/packages/koenig-lexical/demo/RestrictedContentDemo.jsx b/packages/koenig-lexical/demo/RestrictedContentDemo.tsx similarity index 75% rename from packages/koenig-lexical/demo/RestrictedContentDemo.jsx rename to packages/koenig-lexical/demo/RestrictedContentDemo.tsx index 6002645a4c..77626a6b11 100644 --- a/packages/koenig-lexical/demo/RestrictedContentDemo.jsx +++ b/packages/koenig-lexical/demo/RestrictedContentDemo.tsx @@ -22,15 +22,20 @@ function useQuery() { return React.useMemo(() => new URLSearchParams(search), [search]); } -function RestrictedContentDemo() { - let query = useQuery(); +function RestrictedContentDemo(_props: {paragraphs?: number}) { + const query = useQuery(); const [isSidebarOpen, setIsSidebarOpen] = useState(false); const [sidebarView, setSidebarView] = useState('json'); const [defaultContent] = useState(undefined); - const [editorAPI, setEditorAPI] = useState(null); - const titleRef = React.useRef(null); - const containerRef = React.useRef(null); - const paragraphs = query.get('paragraphs') || 1; + const [editorAPI, setEditorAPI] = useState<{ + editorInstance: unknown; + insertParagraphAtBottom: () => void; + focusEditor: (options: {position: string}) => void; + [key: string]: unknown; + } | null>(null); + const titleRef = React.useRef<{focus: () => void} | null>(null); + const containerRef = React.useRef(null); + const paragraphs = Number(query.get('paragraphs')) || 1; const {snippets, createSnippet, deleteSnippet} = useSnippets(); function openSidebar(view = 'json') { @@ -45,13 +50,14 @@ function RestrictedContentDemo() { titleRef.current?.focus(); } - function focusEditor(event) { - const clickedOnDecorator = (event.target.closest('[data-lexical-decorator]') !== null) || event.target.hasAttribute('data-lexical-decorator'); - const clickedOnSlashMenu = (event.target.closest('[data-kg-slash-menu]') !== null) || event.target.hasAttribute('data-kg-slash-menu'); + function focusEditor(event: React.MouseEvent) { + const target = event.target as HTMLElement; + const clickedOnDecorator = (target.closest('[data-lexical-decorator]') !== null) || target.hasAttribute('data-lexical-decorator'); + const clickedOnSlashMenu = (target.closest('[data-kg-slash-menu]') !== null) || target.hasAttribute('data-kg-slash-menu'); if (editorAPI && !clickedOnDecorator && !clickedOnSlashMenu) { - let editor = editorAPI.editorInstance; - let {bottom} = editor._rootElement.getBoundingClientRect(); + const editor = editorAPI.editorInstance as {_rootElement: HTMLElement; getEditorState: () => {read: (fn: () => void) => void}}; + const {bottom} = editor._rootElement.getBoundingClientRect(); // if a mousedown and subsequent mouseup occurs below the editor // canvas, focus the editor and put the cursor at the end of the document @@ -80,7 +86,7 @@ function RestrictedContentDemo() { editorAPI.focusEditor({position: 'bottom'}); //scroll to the bottom of the container - containerRef.current.scrollTop = containerRef.current.scrollHeight; + containerRef.current!.scrollTop = containerRef.current!.scrollHeight; } } } @@ -99,7 +105,7 @@ function RestrictedContentDemo() {
void} > diff --git a/packages/koenig-lexical/demo/components/DarkModeToggle.jsx b/packages/koenig-lexical/demo/components/DarkModeToggle.tsx similarity index 65% rename from packages/koenig-lexical/demo/components/DarkModeToggle.jsx rename to packages/koenig-lexical/demo/components/DarkModeToggle.tsx index cb39d8eaba..48833e77c8 100644 --- a/packages/koenig-lexical/demo/components/DarkModeToggle.jsx +++ b/packages/koenig-lexical/demo/components/DarkModeToggle.tsx @@ -1,4 +1,9 @@ -const DarkModeToggle = ({darkMode, toggleDarkMode}) => { +interface DarkModeToggleProps { + darkMode: boolean; + toggleDarkMode: () => void; +} + +const DarkModeToggle = ({darkMode, toggleDarkMode}: DarkModeToggleProps) => { return ( <>
); } else if (IndicatorIcon) { + const Icon = IndicatorIcon as React.ComponentType>; indicatorIcon = (
- @@ -99,21 +115,10 @@ export const CardWrapper = React.forwardRef(({ data-kg-card-selected={isSelected} {...props} > - {children} + {children as React.ReactNode}
); }); CardWrapper.displayName = 'CardWrapper'; - -CardWrapper.propTypes = { - isSelected: PropTypes.bool, - isEditing: PropTypes.bool, - cardWidth: PropTypes.oneOf(['regular', 'wide', 'full']), - icon: PropTypes.string, - indicatorPosition: PropTypes.shape({ - left: PropTypes.string, - top: PropTypes.string - }) -}; diff --git a/packages/koenig-lexical/src/components/ui/ColorOptionButtons.jsx b/packages/koenig-lexical/src/components/ui/ColorOptionButtons.tsx similarity index 87% rename from packages/koenig-lexical/src/components/ui/ColorOptionButtons.jsx rename to packages/koenig-lexical/src/components/ui/ColorOptionButtons.tsx index c1794512ce..342dfc2405 100644 --- a/packages/koenig-lexical/src/components/ui/ColorOptionButtons.jsx +++ b/packages/koenig-lexical/src/components/ui/ColorOptionButtons.tsx @@ -4,9 +4,21 @@ import {Tooltip} from './Tooltip'; import {useClickOutside} from '../../hooks/useClickOutside'; import {usePreviousFocus} from '../../hooks/usePreviousFocus'; -export function ColorOptionButtons({buttons = [], selectedName, onClick}) { +interface ColorOptionButton { + label: string; + name: string; + color?: string; +} + +interface ColorOptionButtonsProps { + buttons?: ColorOptionButton[]; + selectedName?: string; + onClick: (name: string) => void; +} + +export function ColorOptionButtons({buttons = [], selectedName, onClick}: ColorOptionButtonsProps) { const [isOpen, setIsOpen] = useState(false); - const componentRef = React.useRef(null); + const componentRef = React.useRef(null); const selectedButton = buttons.find(button => button.name === selectedName); @@ -49,13 +61,13 @@ export function ColorOptionButtons({buttons = [], selectedName, onClick}) { label={label} name={name} selectedName={selectedName} - onClick={(title) => { + onClick={(title: string) => { onClick(title); setIsOpen(false); }} /> : -
  • onClick(name)}> +
  • onClick(name)}> @@ -69,7 +81,16 @@ export function ColorOptionButtons({buttons = [], selectedName, onClick}) { ); } -export function ColorButton({onClick, label, name, color, selectedName}) { +interface ColorButtonProps { + onClick: (name: string) => void; + label: string; + name: string; + color?: string; + selectedName?: string; + [key: string]: unknown; +} + +export function ColorButton({onClick, label, name, color, selectedName}: ColorButtonProps) { const isActive = name === selectedName; const {handleMousedown, handleClick} = usePreviousFocus(onClick, name); diff --git a/packages/koenig-lexical/src/components/ui/ColorPicker.stories.jsx b/packages/koenig-lexical/src/components/ui/ColorPicker.stories.tsx similarity index 98% rename from packages/koenig-lexical/src/components/ui/ColorPicker.stories.jsx rename to packages/koenig-lexical/src/components/ui/ColorPicker.stories.tsx index b6bacf0b41..6ebab91f29 100644 --- a/packages/koenig-lexical/src/components/ui/ColorPicker.stories.jsx +++ b/packages/koenig-lexical/src/components/ui/ColorPicker.stories.tsx @@ -1,3 +1,4 @@ +// @ts-nocheck import {ColorPicker} from './ColorPicker'; const story = { diff --git a/packages/koenig-lexical/src/components/ui/ColorPicker.jsx b/packages/koenig-lexical/src/components/ui/ColorPicker.tsx similarity index 86% rename from packages/koenig-lexical/src/components/ui/ColorPicker.jsx rename to packages/koenig-lexical/src/components/ui/ColorPicker.tsx index e10e658f80..c4b13d66ff 100644 --- a/packages/koenig-lexical/src/components/ui/ColorPicker.jsx +++ b/packages/koenig-lexical/src/components/ui/ColorPicker.tsx @@ -8,11 +8,19 @@ import {Tooltip} from './Tooltip'; import {getAccentColor} from '../../utils/getAccentColor'; import {useClickOutside} from '../../hooks/useClickOutside'; -export function ColorPicker({value, eyedropper, hasTransparentOption, onChange, children}) { +interface ColorPickerProps { + value?: string; + eyedropper?: boolean; + hasTransparentOption?: boolean; + onChange: (color: string) => void; + children?: React.ReactNode; +} + +export function ColorPicker({value, eyedropper, hasTransparentOption, onChange, children}: ColorPickerProps) { // HexColorInput doesn't support adding a ref on the input itself - const inputWrapperRef = useRef(null); + const inputWrapperRef = useRef(null); - const stopPropagation = useCallback((e) => { + const stopPropagation = useCallback((e: React.MouseEvent | React.TouchEvent) => { e.stopPropagation(); const inputElement = inputWrapperRef.current?.querySelector('input'); @@ -46,15 +54,15 @@ export function ColorPicker({value, eyedropper, hasTransparentOption, onChange, document.addEventListener('touchend', stopUsingColorPicker); }, [stopUsingColorPicker]); - const openColorPicker = useCallback((e) => { + const openColorPicker = useCallback((e: React.MouseEvent) => { e.preventDefault(); isUsingColorPicker.current = true; document.body.style.setProperty('pointer-events', 'none'); - const eyeDropper = new window.EyeDropper(); + const eyeDropper = new (window as unknown as {EyeDropper: new () => {open: () => Promise<{sRGBHex: string}>}}).EyeDropper(); eyeDropper.open() - .then(result => onChange(result.sRGBHex)) + .then((result: {sRGBHex: string}) => onChange(result.sRGBHex)) .finally(() => { isUsingColorPicker.current = false; document.body.style.removeProperty('pointer-events'); @@ -73,7 +81,7 @@ export function ColorPicker({value, eyedropper, hasTransparentOption, onChange, hexValue = ''; } - const focusHexInputOnClick = useCallback((e) => { + const focusHexInputOnClick = useCallback(() => { inputWrapperRef.current?.querySelector('input')?.focus(); }, []); @@ -84,7 +92,7 @@ export function ColorPicker({value, eyedropper, hasTransparentOption, onChange,
    # - {eyedropper && !!window.EyeDropper && ( + {eyedropper && !!(window as unknown as {EyeDropper?: unknown}).EyeDropper && ( @@ -85,7 +90,3 @@ export function LinkInput({href, update, cancel}) {
    ); } - -LinkInput.propTypes = { - href: PropTypes.string -}; diff --git a/packages/koenig-lexical/src/components/ui/LinkInputSearchItem.jsx b/packages/koenig-lexical/src/components/ui/LinkInputSearchItem.tsx similarity index 72% rename from packages/koenig-lexical/src/components/ui/LinkInputSearchItem.jsx rename to packages/koenig-lexical/src/components/ui/LinkInputSearchItem.tsx index d8157223f5..f6197b4565 100644 --- a/packages/koenig-lexical/src/components/ui/LinkInputSearchItem.jsx +++ b/packages/koenig-lexical/src/components/ui/LinkInputSearchItem.tsx @@ -1,7 +1,25 @@ import {HighlightedString} from './HighlightedString'; import {InputListItem} from './InputList'; -export function LinkInputSearchItem({dataTestId, item, highlightString, selected, onMouseOver, scrollIntoView, onClick}) { +interface LinkInputSearchItemProps { + dataTestId?: string; + item: { + Icon?: React.ComponentType>; + label: string; + highlight?: boolean; + metaText?: string; + MetaIcon?: React.ComponentType>; + metaIconTitle?: string; + [key: string]: unknown; + }; + highlightString?: string; + selected?: boolean; + onMouseOver?: () => void; + scrollIntoView?: boolean; + onClick?: () => void; +} + +export function LinkInputSearchItem({dataTestId, item, highlightString, selected, onMouseOver, scrollIntoView, onClick}: LinkInputSearchItemProps) { return ( void; + cancel: () => void; +} + +export function LinkInputWithSearch({href, update, cancel}: LinkInputWithSearchProps) { const {cardConfig: {searchLinks}} = React.useContext(KoenigComposerContext); // store the href/query in state so we can update it without affecting the saved editor value const [_href, setHref] = React.useState(href); - const {isSearching, listOptions} = useSearchLinks(_href, searchLinks); + const {isSearching, listOptions} = useSearchLinks(_href || '', searchLinks as (term?: string) => Promise); // add refs for input and container - const containerRef = React.useRef(null); + const containerRef = React.useRef(null); const testId = 'link-input'; @@ -32,13 +37,13 @@ export function LinkInputWithSearch({href, update, cancel}) { // close link input when clicking outside or pressing escape React.useEffect(() => { - const closeOnClickOutside = (event) => { - if (containerRef.current && !containerRef.current.contains(event.target)) { + const closeOnClickOutside = (event: MouseEvent) => { + if (containerRef.current && !containerRef.current.contains(event.target as Node)) { cancel(); } }; - const onEscape = (event) => { + const onEscape = (event: KeyboardEvent) => { if (event.key === 'Escape') { cancel(); } @@ -53,11 +58,11 @@ export function LinkInputWithSearch({href, update, cancel}) { }; }, [cancel]); - const onItemSelected = (item) => { + const onItemSelected = (item: {value: string; type?: string}) => { update(item.value, item.type); }; - const getItem = (item, selected, onMouseOver, scrollIntoView) => { + const getItem = (item: {value: string; label: string; [key: string]: unknown}, selected: boolean, onMouseOver: () => void, scrollIntoView: boolean) => { return ( void} onMouseOver={onMouseOver} /> ); }; - const getGroup = (group, {showSpinner} = {}) => { + const getGroup = (group: unknown, {showSpinner}: {showSpinner?: boolean} = {}) => { return ( - + ); }; @@ -90,16 +95,16 @@ export function LinkInputWithSearch({href, update, cancel}) { placeholder="Search or enter URL to link" value={_href} data-kg-link-input - onChange={(e) => { + onChange={(e: React.ChangeEvent) => { // update local value to allow searching setHref(e.target.value); }} - onKeyDown={(e) => { + onKeyDown={(e: React.KeyboardEvent) => { if (e.key === 'Enter') { // prevent Enter from triggering in the editor and removing text // update the link value in the editor e.preventDefault(); - update(_href); + update(_href || ''); return; } }} @@ -109,11 +114,11 @@ export function LinkInputWithSearch({href, update, cancel}) {
      {isSearching && !listOptions.length && } React.ReactNode} + getItem={getItem as (item: unknown, selected: boolean, onMouseOver: () => void, scrollIntoView: boolean) => React.ReactNode} groups={listOptions} isLoading={isSearching} - onSelect={onItemSelected} + onSelect={onItemSelected as (item: unknown) => void} />
    @@ -121,7 +126,3 @@ export function LinkInputWithSearch({href, update, cancel}) {
  • ); } - -LinkInputWithSearch.propTypes = { - href: PropTypes.string -}; diff --git a/packages/koenig-lexical/src/components/ui/LinkToolbar.stories.jsx b/packages/koenig-lexical/src/components/ui/LinkToolbar.stories.tsx similarity index 96% rename from packages/koenig-lexical/src/components/ui/LinkToolbar.stories.jsx rename to packages/koenig-lexical/src/components/ui/LinkToolbar.stories.tsx index b6e779ac62..4b5356d8b1 100644 --- a/packages/koenig-lexical/src/components/ui/LinkToolbar.stories.jsx +++ b/packages/koenig-lexical/src/components/ui/LinkToolbar.stories.tsx @@ -1,3 +1,4 @@ +// @ts-nocheck import {LinkToolbar} from './LinkToolbar'; const story = { diff --git a/packages/koenig-lexical/src/components/ui/LinkToolbar.jsx b/packages/koenig-lexical/src/components/ui/LinkToolbar.tsx similarity index 83% rename from packages/koenig-lexical/src/components/ui/LinkToolbar.jsx rename to packages/koenig-lexical/src/components/ui/LinkToolbar.tsx index e3aa8c4fe0..11c238973d 100644 --- a/packages/koenig-lexical/src/components/ui/LinkToolbar.jsx +++ b/packages/koenig-lexical/src/components/ui/LinkToolbar.tsx @@ -1,6 +1,12 @@ import {ToolbarMenuItem} from './ToolbarMenu'; -export function LinkToolbar({href, onEdit, onRemove}) { +interface LinkToolbarProps { + href?: string; + onEdit?: () => void; + onRemove?: () => void; +} + +export function LinkToolbar({href, onEdit, onRemove}: LinkToolbarProps) { return (
    {href} diff --git a/packages/koenig-lexical/src/components/ui/MediaPlaceholder.stories.jsx b/packages/koenig-lexical/src/components/ui/MediaPlaceholder.stories.tsx similarity index 99% rename from packages/koenig-lexical/src/components/ui/MediaPlaceholder.stories.jsx rename to packages/koenig-lexical/src/components/ui/MediaPlaceholder.stories.tsx index a14acc19bd..8d58209b62 100644 --- a/packages/koenig-lexical/src/components/ui/MediaPlaceholder.stories.jsx +++ b/packages/koenig-lexical/src/components/ui/MediaPlaceholder.stories.tsx @@ -1,3 +1,4 @@ +// @ts-nocheck import {MediaPlaceholder} from './MediaPlaceholder'; const story = { diff --git a/packages/koenig-lexical/src/components/ui/MediaPlaceholder.jsx b/packages/koenig-lexical/src/components/ui/MediaPlaceholder.tsx similarity index 79% rename from packages/koenig-lexical/src/components/ui/MediaPlaceholder.jsx rename to packages/koenig-lexical/src/components/ui/MediaPlaceholder.tsx index f347e7a689..07f543d073 100644 --- a/packages/koenig-lexical/src/components/ui/MediaPlaceholder.jsx +++ b/packages/koenig-lexical/src/components/ui/MediaPlaceholder.tsx @@ -3,11 +3,11 @@ import FilePlaceholderIcon from '../../assets/icons/kg-file-placeholder.svg?reac import GalleryPlaceholderIcon from '../../assets/icons/kg-gallery-placeholder.svg?react'; import ImgPlaceholderIcon from '../../assets/icons/kg-img-placeholder.svg?react'; import ProductPlaceholderIcon from '../../assets/icons/kg-product-placeholder.svg?react'; -import PropTypes from 'prop-types'; +import React from 'react'; import VideoPlaceholderIcon from '../../assets/icons/kg-video-placeholder.svg?react'; import clsx from 'clsx'; -export const PLACEHOLDER_ICONS = { +export const PLACEHOLDER_ICONS: Record>> = { image: ImgPlaceholderIcon, gallery: GalleryPlaceholderIcon, video: VideoPlaceholderIcon, @@ -16,7 +16,7 @@ export const PLACEHOLDER_ICONS = { product: ProductPlaceholderIcon }; -export const CardText = ({text, type}) => ( +export const CardText = ({text, type}: {text: string; type?: string}) => ( ( ); -const ButtonContents = ({desc, hasErrors}) => { +const ButtonContents = ({desc, hasErrors}: {desc?: string; hasErrors: boolean}) => { if (hasErrors) { return null; } return

    {desc}

    ; }; -const StandardContents = ({desc, hasErrors, icon, size}) => { +const StandardContents = ({desc, hasErrors, icon, size}: {desc?: string; hasErrors: boolean; icon?: string; size?: string}) => { if (size === 'xsmall' && hasErrors) { return null; } - const Icon = PLACEHOLDER_ICONS[icon]; + const Icon = icon ? PLACEHOLDER_ICONS[icon] : null; const iconClasses = clsx( 'shrink-0 opacity-80 transition-all ease-linear hover:scale-105 group-hover:opacity-100', size === 'large' && 'size-20 text-grey', size === 'small' && 'size-14 text-grey', size === 'xsmall' && 'size-5 text-grey-700', - !['large', 'small', 'xsmall'].includes(size) && 'size-16 text-grey', + !['large', 'small', 'xsmall'].includes(size || '') && 'size-16 text-grey', (size === 'xsmall' && desc) && 'mr-3' ); @@ -58,11 +58,27 @@ const StandardContents = ({desc, hasErrors, icon, size}) => { ); return <> - + {Icon && }

    {desc}

    ; }; +interface MediaPlaceholderProps { + desc?: string; + icon?: 'image' | 'gallery' | 'video' | 'audio' | 'file' | 'product'; + filePicker?: () => void; + size?: 'xsmall' | 'small' | 'medium' | 'large'; + type?: 'image' | 'button'; + borderStyle?: 'squared' | 'rounded'; + isDraggedOver?: boolean; + errors?: {message: string}[]; + placeholderRef?: React.Ref; + dataTestId?: string; + errorDataTestId?: string; + multiple?: boolean; + [key: string]: unknown; +} + export function MediaPlaceholder({ desc, icon, @@ -77,7 +93,7 @@ export function MediaPlaceholder({ errorDataTestId = 'media-placeholder-errors', multiple = false, ...props -}) { +}: MediaPlaceholderProps) { const containerClasses = clsx( 'relative flex h-full items-center justify-center', type === 'button' ? 'rounded-lg bg-grey-100' : 'border bg-grey-50', @@ -137,11 +153,3 @@ export function MediaPlaceholder({
    ); } - -MediaPlaceholder.propTypes = { - icon: PropTypes.oneOf(['image', 'gallery', 'video', 'audio', 'file', 'product']), - desc: PropTypes.string, - size: PropTypes.oneOf(['xsmall', 'small', 'medium', 'large']), - type: PropTypes.oneOf(['image', 'button']), - borderStyle: PropTypes.oneOf(['squared', 'rounded']) -}; diff --git a/packages/koenig-lexical/src/components/ui/MediaPlayer.stories.jsx b/packages/koenig-lexical/src/components/ui/MediaPlayer.stories.tsx similarity index 96% rename from packages/koenig-lexical/src/components/ui/MediaPlayer.stories.jsx rename to packages/koenig-lexical/src/components/ui/MediaPlayer.stories.tsx index 293041424d..bd54e1e85d 100644 --- a/packages/koenig-lexical/src/components/ui/MediaPlayer.stories.jsx +++ b/packages/koenig-lexical/src/components/ui/MediaPlayer.stories.tsx @@ -1,3 +1,4 @@ +// @ts-nocheck import {MediaPlayer} from './MediaPlayer'; const story = { diff --git a/packages/koenig-lexical/src/components/ui/MediaPlayer.jsx b/packages/koenig-lexical/src/components/ui/MediaPlayer.tsx similarity index 89% rename from packages/koenig-lexical/src/components/ui/MediaPlayer.jsx rename to packages/koenig-lexical/src/components/ui/MediaPlayer.tsx index d2f90890af..607baee905 100644 --- a/packages/koenig-lexical/src/components/ui/MediaPlayer.jsx +++ b/packages/koenig-lexical/src/components/ui/MediaPlayer.tsx @@ -1,8 +1,14 @@ import PlayIcon from '../../assets/icons/kg-play.svg?react'; -import PropTypes from 'prop-types'; import UnmuteIcon from '../../assets/icons/kg-unmute.svg?react'; -export function MediaPlayer({type, duration, theme, ...args}) { +interface MediaPlayerProps { + type?: string; + duration?: string; + theme?: 'light' | 'dark'; + [key: string]: unknown; +} + +export function MediaPlayer({type: _type, duration, theme, ...args}: MediaPlayerProps) { return (
    @@ -25,7 +31,3 @@ export function MediaPlayer({type, duration, theme, ...args}) {
    ); } - -MediaPlayer.propTypes = { - theme: PropTypes.oneOf(['light', 'dark']) -}; diff --git a/packages/koenig-lexical/src/components/ui/MediaUploader.jsx b/packages/koenig-lexical/src/components/ui/MediaUploader.tsx similarity index 71% rename from packages/koenig-lexical/src/components/ui/MediaUploader.jsx rename to packages/koenig-lexical/src/components/ui/MediaUploader.tsx index 6d98fd1ad1..1e49618d14 100644 --- a/packages/koenig-lexical/src/components/ui/MediaUploader.jsx +++ b/packages/koenig-lexical/src/components/ui/MediaUploader.tsx @@ -1,6 +1,6 @@ import DeleteIcon from '../../assets/icons/kg-trash.svg?react'; import ImageUploadForm from './ImageUploadForm'; -import PropTypes from 'prop-types'; +import React from 'react'; import WandIcon from '../../assets/icons/kg-wand.svg?react'; import clsx from 'clsx'; import {IconButton} from './IconButton'; @@ -9,6 +9,31 @@ import {ProgressBar} from './ProgressBar'; import {openFileSelection} from '../../utils/openFileSelection'; import {useRef} from 'react'; +interface MediaUploaderProps { + className?: string; + imgClassName?: string; + src?: string; + alt?: string; + desc?: string; + icon?: string; + size?: string; + type?: 'image' | 'button'; + borderStyle?: 'squared' | 'rounded'; + backgroundSize?: 'cover' | 'contain'; + mimeTypes?: string[]; + onFileChange: (e: React.ChangeEvent) => void; + dragHandler?: {isDraggedOver?: boolean; setRef?: React.Ref}; + isEditing?: boolean; + isLoading?: boolean; + isPinturaEnabled?: boolean; + openImageEditor?: (opts: unknown) => void; + progress?: number; + errors?: {message: string}[]; + onRemoveMedia?: () => void; + additionalActions?: React.ReactNode; + setFileInputRef?: (el: HTMLInputElement | null) => void; +} + export function MediaUploader({ className, imgClassName, @@ -32,10 +57,10 @@ export function MediaUploader({ onRemoveMedia = () => {}, additionalActions, setFileInputRef -}) { - const fileInputRef = useRef(null); +}: MediaUploaderProps) { + const fileInputRef = useRef(null); - const onFileInputRef = (element) => { + const onFileInputRef = (element: HTMLInputElement | null) => { fileInputRef.current = element; setFileInputRef?.(element); }; @@ -44,7 +69,7 @@ export function MediaUploader({ width: `${progress?.toFixed(0)}%` }; - const onRemove = (e) => { + const onRemove = (e: React.MouseEvent) => { e.stopPropagation(); // prevents card from losing selected state onRemoveMedia(); }; @@ -61,15 +86,14 @@ export function MediaUploader({ errorDataTestId="media-upload-errors" errors={errors} filePicker={() => openFileSelection({fileInputRef})} - icon={icon} + icon={icon as 'image' | 'gallery' | 'video' | 'audio' | 'file' | 'product'} isDraggedOver={dragHandler?.isDraggedOver} placeholderRef={dragHandler?.setRef} - size={size} + size={size as 'xsmall' | 'small' | 'medium' | 'large'} type={type} /> openFileSelection({fileInputRef})} mimeTypes={mimeTypes} onFileChange={onFileChange} /> @@ -79,9 +103,9 @@ export function MediaUploader({ return (
    {src && ( @@ -94,14 +118,14 @@ export function MediaUploader({ {!isLoading && (
    {additionalActions} - { isPinturaEnabled && openImageEditor({ + { isPinturaEnabled && openImageEditor?.({ image: src, - handleSave: (editedImage) => { + handleSave: (editedImage: unknown) => { onFileChange({ target: { - files: [editedImage] + files: [editedImage as File] } - }); + } as unknown as React.ChangeEvent); } })} /> } @@ -119,28 +143,3 @@ export function MediaUploader({
    ); } - -MediaUploader.propTypes = { - additionalActions: PropTypes.node, - alt: PropTypes.string, - backgroundSize: PropTypes.oneOf(['cover', 'contain']), - borderStyle: PropTypes.oneOf(['squared', 'rounded']), - className: PropTypes.string, - desc: PropTypes.string, - dragHandler: PropTypes.shape({isDraggedOver: PropTypes.bool, setRef: PropTypes.func}), - errors: PropTypes.arrayOf(PropTypes.shape({message: PropTypes.string})), - icon: PropTypes.string, - imgClassName: PropTypes.string, - isEditing: PropTypes.bool, - isLoading: PropTypes.bool, - isPinturaEnabled: PropTypes.bool, - mimeTypes: PropTypes.arrayOf(PropTypes.string), - onFileChange: PropTypes.func, - onRemoveMedia: PropTypes.func, - openImageEditor: PropTypes.func, - progress: PropTypes.number, - setFileInputRef: PropTypes.func, - size: PropTypes.string, - src: PropTypes.string, - type: PropTypes.oneOf(['image', 'button']) -}; diff --git a/packages/koenig-lexical/src/components/ui/Modal.stories.jsx b/packages/koenig-lexical/src/components/ui/Modal.stories.tsx similarity index 90% rename from packages/koenig-lexical/src/components/ui/Modal.stories.jsx rename to packages/koenig-lexical/src/components/ui/Modal.stories.tsx index 12ca012997..c5b1a6bd8f 100644 --- a/packages/koenig-lexical/src/components/ui/Modal.stories.jsx +++ b/packages/koenig-lexical/src/components/ui/Modal.stories.tsx @@ -1,3 +1,5 @@ +// @ts-nocheck +import React, {useState} from 'react'; import {Modal} from './Modal'; import {useState} from 'react'; @@ -12,7 +14,7 @@ const story = { }; export default story; -const Template = (args) => { +const Template = (_args) => { const [isOpen, setOpen] = useState(false); const openModal = () => setOpen(true); diff --git a/packages/koenig-lexical/src/components/ui/Modal.jsx b/packages/koenig-lexical/src/components/ui/Modal.tsx similarity index 81% rename from packages/koenig-lexical/src/components/ui/Modal.jsx rename to packages/koenig-lexical/src/components/ui/Modal.tsx index b131f6b373..81b4226899 100644 --- a/packages/koenig-lexical/src/components/ui/Modal.jsx +++ b/packages/koenig-lexical/src/components/ui/Modal.tsx @@ -1,9 +1,15 @@ import CloseIcon from '../../assets/icons/kg-close.svg?react'; import Portal from './Portal'; -import PropTypes from 'prop-types'; +import React from 'react'; -export function Modal({isOpen, onClose, children}) { - const controlByKeys = (event) => { +interface ModalProps { + isOpen?: boolean; + onClose: () => void; + children?: React.ReactNode; +} + +export function Modal({isOpen, onClose, children}: ModalProps) { + const controlByKeys = (event: React.KeyboardEvent) => { event.stopPropagation(); event.preventDefault(); @@ -35,9 +41,3 @@ export function Modal({isOpen, onClose, children}) { ); } - -Modal.propTypes = { - isOpen: PropTypes.bool, - onClose: PropTypes.func, - children: PropTypes.node -}; diff --git a/packages/koenig-lexical/src/components/ui/MultiSelectDropdown.jsx b/packages/koenig-lexical/src/components/ui/MultiSelectDropdown.tsx similarity index 77% rename from packages/koenig-lexical/src/components/ui/MultiSelectDropdown.jsx rename to packages/koenig-lexical/src/components/ui/MultiSelectDropdown.tsx index ab5ea1524a..c5d7f5133e 100644 --- a/packages/koenig-lexical/src/components/ui/MultiSelectDropdown.jsx +++ b/packages/koenig-lexical/src/components/ui/MultiSelectDropdown.tsx @@ -4,7 +4,12 @@ import React from 'react'; import {DropdownContainer} from './DropdownContainer'; import {KeyboardSelection} from './KeyboardSelection'; -function Item({item, selected, onChange}) { +interface DropdownItem { + name: string; + label: React.ReactNode; +} + +function Item({item, selected, onChange}: {item: DropdownItem; selected: boolean; onChange: (item: DropdownItem) => void}) { let selectionClass = ''; if (selected) { @@ -13,7 +18,7 @@ function Item({item, selected, onChange}) { // We use the capture phase of the mouse down event, otherwise the list option will be removed when blurring the input // before calling the click event - const handleOptionMouseDown = (event) => { + const handleOptionMouseDown = (event: React.MouseEvent) => { // Prevent losing focus when clicking an option event.preventDefault(); onChange(item); @@ -33,18 +38,27 @@ function Item({item, selected, onChange}) { ); } -export function MultiSelectDropdown({placeholder = '', items = [], availableItems = [], onChange, dataTestId, allowAdd = true}) { +interface MultiSelectDropdownProps { + placeholder?: string; + items?: string[]; + availableItems?: string[]; + onChange: (items: string[]) => void; + dataTestId?: string; + allowAdd?: boolean; +} + +export function MultiSelectDropdown({placeholder = '', items = [], availableItems = [], onChange, dataTestId, allowAdd = true}: MultiSelectDropdownProps) { const [open, setOpen] = React.useState(false); const [filter, setFilter] = React.useState(''); const [isFocused, setIsFocused] = React.useState(false); - const inputRef = React.useRef(null); + const inputRef = React.useRef(null); - const handleOpen = (event) => { + const handleOpen = (event?: React.MouseEvent) => { setOpen(!open); // For Safari, we need to manually focus the button (doesn't happen by default) - if (!open) { - event.target.focus(); + if (!open && event) { + (event.target as HTMLElement).focus(); } }; @@ -59,7 +73,7 @@ export function MultiSelectDropdown({placeholder = '', items = [], availableItem handleOpen(); }; - const handleSelect = (item) => { + const handleSelect = (item: DropdownItem) => { if (!item.name || items?.includes(item.name)) { return; } @@ -68,7 +82,7 @@ export function MultiSelectDropdown({placeholder = '', items = [], availableItem setFilter(''); }; - const handleDeselect = (event, selectedItem) => { + const handleDeselect = (event: React.MouseEvent, selectedItem: DropdownItem) => { // Prevent losing focus when clicking an option event.preventDefault(); event.stopPropagation(); @@ -76,25 +90,26 @@ export function MultiSelectDropdown({placeholder = '', items = [], availableItem onChange(items.filter(selection => selection !== selectedItem.name)); }; - const handleBackspace = (event) => { + const handleBackspace = (event: React.KeyboardEvent) => { if (event.key === 'Backspace' && !filter) { onChange(items.slice(0, -1)); } }; - const getItem = (item, selected) => { + const getItem = (item: unknown, selected: boolean) => { + const typedItem = item as DropdownItem; return ( - + ); }; const selectedItems = items.map(item => ({name: item, label: item})); - const nonSelectedItems = availableItems.map(item => ({name: item, label: item})).filter( + const nonSelectedItems = availableItems.map(item => ({name: item, label: item as React.ReactNode})).filter( ai => !selectedItems.some(ii => ii.name === ai.name) ); - const filteredItems = nonSelectedItems.filter(item => item.name.toLocaleLowerCase().includes(filter.toLocaleLowerCase())); - let prefixItem = ''; + const filteredItems: DropdownItem[] = nonSelectedItems.filter(item => item.name.toLocaleLowerCase().includes(filter.toLocaleLowerCase())); + const prefixItem = ''; const defaultSelected = filteredItems[0]; if (filter && allowAdd) { @@ -109,8 +124,7 @@ export function MultiSelectDropdown({placeholder = '', items = [], availableItem
    inputRef.current.focus()} + onClick={() => inputRef.current?.focus()} > {selectedItems.map(item => (
    diff --git a/packages/koenig-lexical/src/components/ui/TextInput.tsx b/packages/koenig-lexical/src/components/ui/TextInput.tsx new file mode 100644 index 0000000000..8a67f5dc4a --- /dev/null +++ b/packages/koenig-lexical/src/components/ui/TextInput.tsx @@ -0,0 +1,20 @@ +import React from 'react'; + +interface TextInputProps extends React.InputHTMLAttributes { + value?: string; + onChange: (e: React.ChangeEvent) => void; +} + +export function TextInput({value, onChange, ...args}: TextInputProps) { + const handleOnChange = (e: React.ChangeEvent) => { + onChange(e); + }; + + return ( + + ); +} diff --git a/packages/koenig-lexical/src/components/ui/Toggle.stories.jsx b/packages/koenig-lexical/src/components/ui/Toggle.stories.tsx similarity index 95% rename from packages/koenig-lexical/src/components/ui/Toggle.stories.jsx rename to packages/koenig-lexical/src/components/ui/Toggle.stories.tsx index 8f1f1c2b0e..14658db8d3 100644 --- a/packages/koenig-lexical/src/components/ui/Toggle.stories.jsx +++ b/packages/koenig-lexical/src/components/ui/Toggle.stories.tsx @@ -1,3 +1,4 @@ +// @ts-nocheck import {Toggle} from './Toggle'; const story = { diff --git a/packages/koenig-lexical/src/components/ui/Toggle.jsx b/packages/koenig-lexical/src/components/ui/Toggle.tsx similarity index 74% rename from packages/koenig-lexical/src/components/ui/Toggle.jsx rename to packages/koenig-lexical/src/components/ui/Toggle.tsx index a1ad95040d..d4456a4bb2 100644 --- a/packages/koenig-lexical/src/components/ui/Toggle.jsx +++ b/packages/koenig-lexical/src/components/ui/Toggle.tsx @@ -1,6 +1,12 @@ -import PropTypes from 'prop-types'; +import React from 'react'; -export function Toggle({isChecked, onChange, dataTestId}) { +interface ToggleProps { + isChecked?: boolean; + onChange?: (e: React.ChangeEvent) => void; + dataTestId?: string; +} + +export function Toggle({isChecked, onChange, dataTestId}: ToggleProps) { return (
    ); diff --git a/packages/koenig-lexical/src/components/ui/VisibilitySettings.jsx b/packages/koenig-lexical/src/components/ui/VisibilitySettings.tsx similarity index 74% rename from packages/koenig-lexical/src/components/ui/VisibilitySettings.jsx rename to packages/koenig-lexical/src/components/ui/VisibilitySettings.tsx index 2ca3ca2b7f..d5ed7025c1 100644 --- a/packages/koenig-lexical/src/components/ui/VisibilitySettings.jsx +++ b/packages/koenig-lexical/src/components/ui/VisibilitySettings.tsx @@ -1,6 +1,23 @@ import {ToggleSetting} from './SettingsPanel'; -export function VisibilitySettings({visibilityOptions, toggleVisibility}) { +interface VisibilityToggle { + key: string; + label: string; + checked: boolean; +} + +export interface VisibilityGroup { + key: string; + label: string; + toggles: VisibilityToggle[]; +} + +interface VisibilitySettingsProps { + visibilityOptions: VisibilityGroup[]; + toggleVisibility: (groupKey: string, toggleKey: string, value: boolean) => void; +} + +export function VisibilitySettings({visibilityOptions, toggleVisibility}: VisibilitySettingsProps) { const settingGroups = visibilityOptions.map((group, index) => { const toggles = group.toggles.map((toggle) => { return ( diff --git a/packages/koenig-lexical/src/components/ui/cards/AudioCard.stories.jsx b/packages/koenig-lexical/src/components/ui/cards/AudioCard.stories.tsx similarity index 99% rename from packages/koenig-lexical/src/components/ui/cards/AudioCard.stories.jsx rename to packages/koenig-lexical/src/components/ui/cards/AudioCard.stories.tsx index f7645ad09a..f526839f78 100644 --- a/packages/koenig-lexical/src/components/ui/cards/AudioCard.stories.jsx +++ b/packages/koenig-lexical/src/components/ui/cards/AudioCard.stories.tsx @@ -1,3 +1,4 @@ +// @ts-nocheck import {AudioCard} from './AudioCard'; import {CardWrapper} from './../CardWrapper'; diff --git a/packages/koenig-lexical/src/components/ui/cards/AudioCard.jsx b/packages/koenig-lexical/src/components/ui/cards/AudioCard.tsx similarity index 74% rename from packages/koenig-lexical/src/components/ui/cards/AudioCard.jsx rename to packages/koenig-lexical/src/components/ui/cards/AudioCard.tsx index c982ac5de8..8bd904798e 100644 --- a/packages/koenig-lexical/src/components/ui/cards/AudioCard.jsx +++ b/packages/koenig-lexical/src/components/ui/cards/AudioCard.tsx @@ -1,7 +1,6 @@ import AudioFileIcon from '../../../assets/icons/kg-audio-file.svg?react'; import DeleteIcon from '../../../assets/icons/kg-trash.svg?react'; import FilePlaceholderIcon from '../../../assets/icons/kg-file-placeholder.svg?react'; -import PropTypes from 'prop-types'; import React from 'react'; import {AudioUploadForm} from '../AudioUploadForm'; import {IconButton} from '../IconButton'; @@ -13,7 +12,18 @@ import {ReadOnlyOverlay} from '../ReadOnlyOverlay'; import {TextInput} from '../TextInput'; import {openFileSelection} from '../../../utils/openFileSelection'; -function AudioUploading({progress}) { +interface FileUploader { + isLoading?: boolean; + progress?: number; + errors?: {message: string}[]; +} + +interface DragHandler { + isDraggedOver?: boolean; + setRef?: React.Ref; +} + +function AudioUploading({progress}: {progress?: number}) { const progressStyle = { width: `${progress?.toFixed(0)}%` }; @@ -29,18 +39,26 @@ function AudioUploading({progress}) { ); } +interface EmptyAudioCardProps { + audioUploader: FileUploader; + audioMimeTypes?: string[]; + onFileChange?: (e: React.ChangeEvent) => void; + setFileInputRef: (ref: React.RefObject) => void; + audioDragHandler?: DragHandler; +} + function EmptyAudioCard({ audioUploader, audioMimeTypes, onFileChange, setFileInputRef, audioDragHandler = {} -}) { +}: EmptyAudioCardProps) { const {isLoading: isUploading, progress, errors} = audioUploader; - const fileInputRef = React.useRef(null); + const fileInputRef = React.useRef(null); - const onFileInputRef = (element) => { - fileInputRef.current = element; + const onFileInputRef = (element: HTMLInputElement | null) => { + (fileInputRef as React.MutableRefObject).current = element; setFileInputRef(fileInputRef); }; @@ -63,13 +81,26 @@ function EmptyAudioCard({ fileInputRef={onFileInputRef} filePicker={() => openFileSelection({fileInputRef: fileInputRef})} mimeTypes={audioMimeTypes} - onFileChange={onFileChange} + onFileChange={onFileChange!} /> ); } } +interface AudioThumbnailProps { + mimeTypes?: string[]; + src?: string; + progress?: number; + isUploading?: boolean; + isEditing?: boolean; + setFileInputRef: (ref: React.RefObject) => void; + onFileChange?: (e: React.ChangeEvent) => void; + removeThumbnail?: () => void; + isDraggedOver?: boolean; + errors?: {message: string}[]; +} + function AudioThumbnail({ mimeTypes, src, @@ -81,11 +112,11 @@ function AudioThumbnail({ removeThumbnail, isDraggedOver, errors -}) { - const fileInputRef = React.useRef(null); +}: AudioThumbnailProps) { + const fileInputRef = React.useRef(null); - const onFileInputRef = (element) => { - fileInputRef.current = element; + const onFileInputRef = (element: HTMLInputElement | null) => { + (fileInputRef as React.MutableRefObject).current = element; setFileInputRef(fileInputRef); }; @@ -143,13 +174,28 @@ function AudioThumbnail({ fileInputRef={onFileInputRef} filePicker={() => openFileSelection({fileInputRef: fileInputRef})} mimeTypes={mimeTypes} - onFileChange={onFileChange} + onFileChange={onFileChange!} />
    ); } } +interface PopulatedAudioCardProps { + isEditing?: boolean; + title?: string; + placeholder?: string; + thumbnailUploader: FileUploader; + thumbnailMimeTypes?: string[]; + duration?: number; + updateTitle?: (value: string) => void; + thumbnailSrc?: string; + setFileInputRef: (ref: React.RefObject) => void; + onFileChange?: (e: React.ChangeEvent) => void; + removeThumbnail?: () => void; + thumbnailDragHandler?: DragHandler; +} + function PopulatedAudioCard({ isEditing, title, @@ -163,9 +209,9 @@ function PopulatedAudioCard({ onFileChange, removeThumbnail, thumbnailDragHandler = {} -}) { +}: PopulatedAudioCardProps) { const {isLoading: isUploading, progress, errors} = thumbnailUploader; - const formatDuration = (rawDuration) => { + const formatDuration = (rawDuration: number) => { const minutes = Math.floor(rawDuration / 60); const seconds = Math.floor(rawDuration - (minutes * 60)); const returnedSeconds = seconds < 10 ? `0${seconds}` : `${seconds}`; @@ -173,14 +219,14 @@ function PopulatedAudioCard({ return formattedDuration; }; - const handleChange = (event) => { - updateTitle(event.target.value); + const handleChange = (event: React.ChangeEvent) => { + updateTitle?.(event.target.value); }; return ( <>
    } className="flex rounded-md border border-grey/30 p-2" data-testid="audio-card-populated" > @@ -208,7 +254,7 @@ function PopulatedAudioCard({ onChange={handleChange} /> )} - +
    {!isEditing && } @@ -216,6 +262,26 @@ function PopulatedAudioCard({ ); } +interface AudioCardProps { + src?: string; + thumbnailSrc?: string; + title?: string; + isEditing?: boolean; + updateTitle?: (value: string) => void; + duration?: number; + audioUploader: FileUploader; + audioMimeTypes?: string[]; + thumbnailUploader: FileUploader; + thumbnailMimeTypes?: string[]; + audioFileInputRef?: React.MutableRefObject; + thumbnailFileInputRef?: React.MutableRefObject; + onAudioFileChange?: (e: React.ChangeEvent) => void; + onThumbnailFileChange?: (e: React.ChangeEvent) => void; + audioDragHandler?: DragHandler; + removeThumbnail?: () => void; + thumbnailDragHandler?: DragHandler; +} + export function AudioCard({ src, thumbnailSrc, @@ -234,14 +300,14 @@ export function AudioCard({ audioDragHandler, removeThumbnail, thumbnailDragHandler -}) { - const setAudioFileInputRef = (ref) => { +}: AudioCardProps) { + const setAudioFileInputRef = (ref: React.RefObject) => { if (audioFileInputRef) { audioFileInputRef.current = ref.current; } }; - const setThumbnailFileInputRef = (ref) => { + const setThumbnailFileInputRef = (ref: React.RefObject) => { if (thumbnailFileInputRef) { thumbnailFileInputRef.current = ref.current; } @@ -256,7 +322,6 @@ export function AudioCard({ placeholder='Add a title...' removeThumbnail={removeThumbnail} setFileInputRef={setThumbnailFileInputRef} - setTitle={updateTitle} thumbnailDragHandler={thumbnailDragHandler} thumbnailMimeTypes={thumbnailMimeTypes} thumbnailSrc={thumbnailSrc} @@ -281,64 +346,3 @@ export function AudioCard({ ); } } - -AudioCard.propTypes = { - src: PropTypes.string, - title: PropTypes.string, - isEditing: PropTypes.bool, - updateTitle: PropTypes.func, - duration: PropTypes.number, - thumbnailSrc: PropTypes.string, - audioUploader: PropTypes.object, - audioMimeTypes: PropTypes.array, - thumbnailUploader: PropTypes.object, - thumbnailMimeTypes: PropTypes.array, - audioFileInputRef: PropTypes.object, - thumbnailFileInputRef: PropTypes.object, - onAudioFileChange: PropTypes.func, - onThumbnailFileChange: PropTypes.func, - audioDragHandler: PropTypes.object, - removeThumbnail: PropTypes.func, - thumbnailDragHandler: PropTypes.object -}; - -AudioUploading.propTypes = { - progress: PropTypes.number -}; - -AudioThumbnail.propTypes = { - errors: PropTypes.array, - isDraggedOver: PropTypes.bool, - isEditing: PropTypes.bool, - isUploading: PropTypes.bool, - mimeTypes: PropTypes.array, - progress: PropTypes.number, - removeThumbnail: PropTypes.func, - setFileInputRef: PropTypes.func, - src: PropTypes.string, - onFileChange: PropTypes.func -}; - -PopulatedAudioCard.propTypes = { - duration: PropTypes.number, - errors: PropTypes.array, - isEditing: PropTypes.bool, - placeholder: PropTypes.string, - removeThumbnail: PropTypes.func, - setFileInputRef: PropTypes.func, - thumbnailDragHandler: PropTypes.object, - thumbnailMimeTypes: PropTypes.array, - thumbnailSrc: PropTypes.string, - thumbnailUploader: PropTypes.object, - title: PropTypes.string, - updateTitle: PropTypes.func, - onFileChange: PropTypes.func -}; - -EmptyAudioCard.propTypes = { - audioDragHandler: PropTypes.object, - audioMimeTypes: PropTypes.array, - audioUploader: PropTypes.object, - setFileInputRef: PropTypes.func, - onFileChange: PropTypes.func -}; diff --git a/packages/koenig-lexical/src/components/ui/cards/BookmarkCard.stories.jsx b/packages/koenig-lexical/src/components/ui/cards/BookmarkCard.stories.tsx similarity index 99% rename from packages/koenig-lexical/src/components/ui/cards/BookmarkCard.stories.jsx rename to packages/koenig-lexical/src/components/ui/cards/BookmarkCard.stories.tsx index d7563f46b7..3c07dd4eec 100644 --- a/packages/koenig-lexical/src/components/ui/cards/BookmarkCard.stories.jsx +++ b/packages/koenig-lexical/src/components/ui/cards/BookmarkCard.stories.tsx @@ -1,3 +1,4 @@ +// @ts-nocheck import populateEditor from '../../../utils/storybook/populate-storybook-editor'; import {BookmarkCard} from './BookmarkCard'; import {CardWrapper} from './../CardWrapper'; diff --git a/packages/koenig-lexical/src/components/ui/cards/BookmarkCard.jsx b/packages/koenig-lexical/src/components/ui/cards/BookmarkCard.tsx similarity index 68% rename from packages/koenig-lexical/src/components/ui/cards/BookmarkCard.jsx rename to packages/koenig-lexical/src/components/ui/cards/BookmarkCard.tsx index 6cbd765df8..96cef80417 100644 --- a/packages/koenig-lexical/src/components/ui/cards/BookmarkCard.jsx +++ b/packages/koenig-lexical/src/components/ui/cards/BookmarkCard.tsx @@ -1,8 +1,31 @@ -import PropTypes from 'prop-types'; import React from 'react'; import {CardCaptionEditor} from '../CardCaptionEditor'; import {UrlInput} from '../UrlInput'; import {UrlSearchInput} from '../UrlSearchInput'; +import type {LexicalEditor} from 'lexical'; + +interface BookmarkCardProps { + author?: string; + handleClose?: () => void; + handlePasteAsLink?: (value?: string) => void; + handleRetry?: () => void; + handleUrlChange?: ((e: React.ChangeEvent) => void) | ((value: string) => void); + handleUrlSubmit?: ((e: KeyboardEvent | React.KeyboardEvent) => void) | ((url: string, type?: string) => void); + url?: string; + urlInputValue?: string; + urlPlaceholder?: string; + thumbnail?: string; + title?: string; + description?: string; + icon?: string; + publisher?: string; + captionEditor?: LexicalEditor; + captionEditorInitialState?: string; + isSelected?: boolean; + isLoading?: boolean; + urlError?: boolean; + searchLinks?: (term?: string) => Promise; +} export function BookmarkCard({ author, @@ -25,7 +48,7 @@ export function BookmarkCard({ isLoading, urlError, searchLinks -}) { +}: BookmarkCardProps) { // State to manage thumbnail visibility const [thumbnailVisible, setThumbnailVisible] = React.useState(true); @@ -53,13 +76,15 @@ export function BookmarkCard({ }
    - + {captionEditor && ( + + )} ); } @@ -71,8 +96,8 @@ export function BookmarkCard({ handleClose={handleClose} handlePasteAsLink={handlePasteAsLink} handleRetry={handleRetry} - handleUrlChange={handleUrlChange} - handleUrlSubmit={handleUrlSubmit} + handleUrlChange={handleUrlChange as (value: string) => void} + handleUrlSubmit={handleUrlSubmit as (url: string, type?: string) => void} hasError={urlError} isLoading={isLoading} placeholder={urlPlaceholder} @@ -87,8 +112,8 @@ export function BookmarkCard({ handleClose={handleClose} handlePasteAsLink={handlePasteAsLink} handleRetry={handleRetry} - handleUrlChange={handleUrlChange} - handleUrlSubmit={handleUrlSubmit} + handleUrlChange={handleUrlChange as (e: React.ChangeEvent) => void} + handleUrlSubmit={handleUrlSubmit as (e: KeyboardEvent | React.KeyboardEvent) => void} hasError={urlError} isLoading={isLoading} placeholder={urlPlaceholder} @@ -98,35 +123,12 @@ export function BookmarkCard({ } } -export function BookmarkIcon({src}) { +interface BookmarkIconProps { + src: string; +} + +export function BookmarkIcon({src}: BookmarkIconProps) { return ( ); } - -BookmarkCard.propTypes = { - author: PropTypes.string, - handleClose: PropTypes.func, - handlePasteAsLink: PropTypes.func, - handleRetry: PropTypes.func, - handleUrlChange: PropTypes.func, - handleUrlSubmit: PropTypes.func, - url: PropTypes.string, - urlInputValue: PropTypes.string, - urlPlaceholder: PropTypes.string, - thumbnail: PropTypes.string, - title: PropTypes.string, - description: PropTypes.string, - icon: PropTypes.string, - publisher: PropTypes.string, - captionEditor: PropTypes.object, - captionEditorInitialState: PropTypes.object, - isSelected: PropTypes.bool, - isLoading: PropTypes.bool, - urlError: PropTypes.bool, - searchLinks: PropTypes.func -}; - -BookmarkIcon.propTypes = { - src: PropTypes.string -}; diff --git a/packages/koenig-lexical/src/components/ui/cards/ButtonCard.stories.jsx b/packages/koenig-lexical/src/components/ui/cards/ButtonCard.stories.tsx similarity index 99% rename from packages/koenig-lexical/src/components/ui/cards/ButtonCard.stories.jsx rename to packages/koenig-lexical/src/components/ui/cards/ButtonCard.stories.tsx index 6844c2e3c5..fd391e7f15 100644 --- a/packages/koenig-lexical/src/components/ui/cards/ButtonCard.stories.jsx +++ b/packages/koenig-lexical/src/components/ui/cards/ButtonCard.stories.tsx @@ -1,3 +1,4 @@ +// @ts-nocheck import {ButtonCard} from './ButtonCard'; import {CardWrapper} from './../CardWrapper'; diff --git a/packages/koenig-lexical/src/components/ui/cards/ButtonCard.jsx b/packages/koenig-lexical/src/components/ui/cards/ButtonCard.tsx similarity index 75% rename from packages/koenig-lexical/src/components/ui/cards/ButtonCard.jsx rename to packages/koenig-lexical/src/components/ui/cards/ButtonCard.tsx index 1c85754136..a451eb2674 100644 --- a/packages/koenig-lexical/src/components/ui/cards/ButtonCard.jsx +++ b/packages/koenig-lexical/src/components/ui/cards/ButtonCard.tsx @@ -1,10 +1,21 @@ import CenterAlignIcon from '../../../assets/icons/kg-align-center.svg?react'; import LeftAlignIcon from '../../../assets/icons/kg-align-left.svg?react'; -import PropTypes from 'prop-types'; +import React from 'react'; import {Button} from '../Button'; import {ButtonGroupSetting, InputSetting, InputUrlSetting, SettingsPanel} from '../SettingsPanel'; import {ReadOnlyOverlay} from '../ReadOnlyOverlay'; +interface ButtonCardProps { + alignment?: string; + buttonText?: string; + buttonPlaceholder?: string; + buttonUrl?: string; + handleAlignmentChange?: (name: string) => void; + handleButtonTextChange?: (e: React.ChangeEvent) => void; + handleButtonUrlChange?: (value: string) => void; + isEditing?: boolean; +} + export function ButtonCard({ alignment, buttonText, @@ -14,7 +25,7 @@ export function ButtonCard({ handleButtonTextChange, handleButtonUrlChange, isEditing -}) { +}: ButtonCardProps) { const buttonGroupChildren = [ { label: 'Left', @@ -43,8 +54,8 @@ export function ButtonCard({ )} ); } - -ButtonCard.propTypes = { - alignment: PropTypes.string, - buttonText: PropTypes.string, - buttonPlaceholder: PropTypes.string, - buttonUrl: PropTypes.string, - handleAlignmentChange: PropTypes.func, - handleButtonTextChange: PropTypes.func, - handleButtonUrlChange: PropTypes.func, - handleButtonUrlFocus: PropTypes.func, - handleOptionClick: PropTypes.func, - isEditing: PropTypes.bool, - suggestedUrls: PropTypes.array, - suggestedUrlVisibility: PropTypes.bool -}; diff --git a/packages/koenig-lexical/src/components/ui/cards/CallToActionCard.stories.jsx b/packages/koenig-lexical/src/components/ui/cards/CallToActionCard.stories.tsx similarity index 99% rename from packages/koenig-lexical/src/components/ui/cards/CallToActionCard.stories.jsx rename to packages/koenig-lexical/src/components/ui/cards/CallToActionCard.stories.tsx index 5eb02393bc..d88e54ff8b 100644 --- a/packages/koenig-lexical/src/components/ui/cards/CallToActionCard.stories.jsx +++ b/packages/koenig-lexical/src/components/ui/cards/CallToActionCard.stories.tsx @@ -1,3 +1,4 @@ +// @ts-nocheck import VisibilityIndicatorIcon from '../../../assets/icons/kg-indicator-visibility.svg?react'; import populateEditor from '../../../utils/storybook/populate-storybook-editor.js'; import {BASIC_NODES} from '../../../index.js'; diff --git a/packages/koenig-lexical/src/components/ui/cards/CallToActionCard.jsx b/packages/koenig-lexical/src/components/ui/cards/CallToActionCard.tsx similarity index 87% rename from packages/koenig-lexical/src/components/ui/cards/CallToActionCard.jsx rename to packages/koenig-lexical/src/components/ui/cards/CallToActionCard.tsx index 6a765ecf6c..f498309df6 100644 --- a/packages/koenig-lexical/src/components/ui/cards/CallToActionCard.jsx +++ b/packages/koenig-lexical/src/components/ui/cards/CallToActionCard.tsx @@ -3,7 +3,7 @@ import ImmersiveLayoutIcon from '../../../assets/icons/kg-layout-immersive.svg?r import KoenigNestedEditor from '../../KoenigNestedEditor.jsx'; import LeftAlignIcon from '../../../assets/icons/kg-align-left.svg?react'; import MinimalLayoutIcon from '../../../assets/icons/kg-layout-minimal.svg?react'; -import PropTypes from 'prop-types'; +import React, {useState} from 'react'; import ReplacementStringsPlugin from '../../../plugins/ReplacementStringsPlugin.jsx'; import clsx from 'clsx'; import defaultTheme from '../../../themes/default.js'; @@ -15,7 +15,7 @@ import {RestrictContentPlugin} from '../../../index.js'; import {VisibilitySettings} from '../VisibilitySettings.jsx'; import {getAccentColor} from '../../../utils/getAccentColor.js'; import {textColorForBackgroundColor} from '@tryghost/color-utils'; -import {useState} from 'react'; +import type {LexicalEditor} from 'lexical'; const getTheme = () => ({ ...defaultTheme, @@ -83,6 +83,46 @@ export const callToActionLinkColorPicker = [ } ]; +type CtaColor = 'none' | 'grey' | 'white' | 'blue' | 'green' | 'yellow' | 'red' | 'pink' | 'purple'; + +interface CallToActionCardProps { + alignment?: 'left' | 'center'; + buttonColor?: string; + buttonText?: string; + buttonTextColor?: string; + buttonUrl?: string; + color?: CtaColor; + hasSponsorLabel?: boolean; + htmlEditor?: LexicalEditor; + htmlEditorInitialState?: string; + sponsorLabelHtmlEditor?: LexicalEditor; + sponsorLabelHtmlEditorInitialState?: string; + imageSrc?: string; + isEditing?: boolean; + layout?: 'minimal' | 'immersive'; + showButton?: boolean; + showDividers?: boolean; + visibilityOptions?: import('../VisibilitySettings').VisibilityGroup[]; + handleButtonColor?: (bgColor: string, textColor: string) => void; + handleColorChange?: (name: string) => void; + handleLinkColorChange?: (name: string) => void; + onFileChange?: (e: React.ChangeEvent) => void; + onRemoveMedia?: () => void; + setFileInputRef?: (el: HTMLInputElement | null) => void; + updateAlignment?: (name: string) => void; + updateButtonText?: (e: React.ChangeEvent) => void; + updateButtonUrl?: (value: string) => void; + updateHasSponsorLabel?: (event: React.ChangeEvent) => void; + updateLayout?: (name: string) => void; + updateShowButton?: (event: React.ChangeEvent) => void; + updateShowDividers?: (event: React.ChangeEvent) => void; + toggleVisibility?: (groupKey: string, toggleKey: string, value: boolean) => void; + imageDragHandler?: {isDraggedOver?: boolean; setRef?: React.Ref}; + imageUploader?: {isLoading?: boolean; progress?: number}; + linkColor?: 'text' | 'accent'; + showVisibilitySettings?: boolean; +} + export function CallToActionCard({ alignment = 'left', buttonColor = '', @@ -100,7 +140,7 @@ export function CallToActionCard({ layout = 'immersive', showButton = false, showDividers = true, - visibilityOptions = {}, + visibilityOptions = [], handleButtonColor = () => {}, handleColorChange = () => {}, handleLinkColorChange = () => {}, @@ -116,10 +156,10 @@ export function CallToActionCard({ updateShowDividers = () => {}, toggleVisibility = () => {}, imageDragHandler = {}, - imageUploader = () => {}, + imageUploader = {}, linkColor = 'text', showVisibilitySettings = false -}) { +}: CallToActionCardProps) { const [buttonColorPickerExpanded, setButtonColorPickerExpanded] = useState(false); const {isLoading, progress} = imageUploader || {}; @@ -164,7 +204,7 @@ export function CallToActionCard({ } ]; - const matchingTextColor = (bgColor) => { + const matchingTextColor = (bgColor: string) => { return bgColor === 'transparent' ? '' : textColorForBackgroundColor(bgColor === 'accent' ? getAccentColor() : bgColor).hex(); }; @@ -281,12 +321,12 @@ export function CallToActionCard({ {title: 'Brand color', accent: true} ]} value={buttonColor} - onPickerChange={bgColor => handleButtonColor(bgColor, matchingTextColor(bgColor))} - onSwatchChange={(bgColor) => { + onPickerChange={(bgColor: string) => handleButtonColor(bgColor, matchingTextColor(bgColor))} + onSwatchChange={(bgColor: string) => { handleButtonColor(bgColor, matchingTextColor(bgColor)); setButtonColorPickerExpanded(false); }} - onTogglePicker={(isExpanded) => { + onTogglePicker={(isExpanded: boolean) => { setButtonColorPickerExpanded(isExpanded); }} /> @@ -318,7 +358,7 @@ export function CallToActionCard({ '--cta-link-color': linkColor === 'accent' ? getAccentColor() : 'var(--cta-link-color-text)' - }} + } as React.CSSProperties} > {/* Sponsor label */} @@ -331,7 +371,7 @@ export function CallToActionCard({ autoFocus={true} dataTestId={'sponsor-label-editor'} hasSettingsPanel={true} - initialEditor={sponsorLabelHtmlEditor} + initialEditor={sponsorLabelHtmlEditor!} initialEditorState={sponsorLabelHtmlEditorInitialState} initialTheme={theme} nodes='basic' @@ -378,7 +418,7 @@ export function CallToActionCard({ autoFocus={true} dataTestId={'cta-card-content-editor'} hasSettingsPanel={true} - initialEditor={htmlEditor} + initialEditor={htmlEditor!} initialEditorState={htmlEditorInitialState} initialTheme={theme} nodes='basic' @@ -423,7 +463,7 @@ export function CallToActionCard({ e.preventDefault()} + onMouseDown={(e: React.MouseEvent) => e.preventDefault()} > {{ content: contentSettings, @@ -435,42 +475,3 @@ export function CallToActionCard({ ); } - -CallToActionCard.propTypes = { - alignment: PropTypes.oneOf(['left', 'center']), - buttonText: PropTypes.string, - buttonUrl: PropTypes.string, - buttonColor: PropTypes.string, - buttonTextColor: PropTypes.string, - color: PropTypes.oneOf(['none', 'grey', 'white', 'blue', 'green', 'yellow', 'red', 'pink', 'purple']), - hasSponsorLabel: PropTypes.bool, - imageSrc: PropTypes.string, - isEditing: PropTypes.bool, - layout: PropTypes.oneOf(['minimal', 'immersive']), - showButton: PropTypes.bool, - showDividers: PropTypes.bool, - htmlEditor: PropTypes.object, - htmlEditorInitialState: PropTypes.object, - updateAlignment: PropTypes.func, - updateButtonText: PropTypes.func, - updateButtonUrl: PropTypes.func, - updateHasSponsorLabel: PropTypes.func, - updateShowButton: PropTypes.func, - updateShowDividers: PropTypes.func, - updateLayout: PropTypes.func, - handleColorChange: PropTypes.func, - handleButtonColor: PropTypes.func, - onFileChange: PropTypes.func, - setFileInputRef: PropTypes.func, - onRemoveMedia: PropTypes.func, - sponsorLabelHtmlEditor: PropTypes.object, - sponsorLabelHtmlEditorInitialState: PropTypes.object, - visibilityOptions: PropTypes.array, - toggleVisibility: PropTypes.func, - imageUploadHandler: PropTypes.func, - imageDragHandler: PropTypes.object, - linkColor: PropTypes.oneOf(['text', 'accent']), - handleLinkColorChange: PropTypes.func, - imageUploader: PropTypes.object, - showVisibilitySettings: PropTypes.bool -}; diff --git a/packages/koenig-lexical/src/components/ui/cards/CalloutCard.stories.jsx b/packages/koenig-lexical/src/components/ui/cards/CalloutCard.stories.tsx similarity index 99% rename from packages/koenig-lexical/src/components/ui/cards/CalloutCard.stories.jsx rename to packages/koenig-lexical/src/components/ui/cards/CalloutCard.stories.tsx index 6e6e45c7b5..8d7adb743e 100644 --- a/packages/koenig-lexical/src/components/ui/cards/CalloutCard.stories.jsx +++ b/packages/koenig-lexical/src/components/ui/cards/CalloutCard.stories.tsx @@ -1,3 +1,4 @@ +// @ts-nocheck import populateEditor from '../../../utils/storybook/populate-storybook-editor.js'; import {CalloutCard} from './CalloutCard'; import {CardWrapper} from './../CardWrapper'; diff --git a/packages/koenig-lexical/src/components/ui/cards/CalloutCard.jsx b/packages/koenig-lexical/src/components/ui/cards/CalloutCard.tsx similarity index 83% rename from packages/koenig-lexical/src/components/ui/cards/CalloutCard.jsx rename to packages/koenig-lexical/src/components/ui/cards/CalloutCard.tsx index e00576b1ce..7c7488a515 100644 --- a/packages/koenig-lexical/src/components/ui/cards/CalloutCard.jsx +++ b/packages/koenig-lexical/src/components/ui/cards/CalloutCard.tsx @@ -1,12 +1,14 @@ import EmojiPickerPortal from '../EmojiPickerPortal'; import KoenigComposerContext from '../../../context/KoenigComposerContext.jsx'; import KoenigNestedEditor from '../../KoenigNestedEditor'; -import PropTypes from 'prop-types'; import React from 'react'; import {ColorOptionSetting, SettingsPanel, ToggleSetting} from '../SettingsPanel'; import {ReadOnlyOverlay} from '../ReadOnlyOverlay'; +import type {LexicalEditor} from 'lexical'; -export const CALLOUT_COLORS = { +export type CalloutColor = 'white' | 'grey' | 'blue' | 'green' | 'yellow' | 'red' | 'pink' | 'purple' | 'accent'; + +export const CALLOUT_COLORS: Record = { white: 'bg-transparent border-grey/30', grey: 'bg-grey/10 border-transparent', blue: 'bg-blue/10 border-transparent', @@ -21,7 +23,7 @@ export const CALLOUT_COLORS = { const TEXT_BLACK = 'text-black dark:text-grey-300 caret-black dark:caret-grey-300'; const TEXT_WHITE = 'text-white caret-white'; -export const CALLOUT_TEXT_COLORS = { +export const CALLOUT_TEXT_COLORS: Record = { white: TEXT_BLACK, grey: TEXT_BLACK, blue: TEXT_BLACK, @@ -82,6 +84,22 @@ export const calloutColorPicker = [ } ]; +interface CalloutCardProps { + color?: CalloutColor; + isEditing?: boolean; + setShowEmojiPicker: (show: boolean) => void; + toggleEmoji?: (event: React.ChangeEvent) => void; + hasEmoji?: boolean; + handleColorChange?: (name: string) => void; + changeEmoji?: (emoji: {native: string}) => void; + calloutEmoji?: string; + textEditor?: LexicalEditor; + textEditorInitialState?: string; + nodeKey?: string; + toggleEmojiPicker?: () => void; + showEmojiPicker?: boolean; +} + export function CalloutCard({ color = 'green', isEditing, @@ -90,14 +108,14 @@ export function CalloutCard({ hasEmoji = true, handleColorChange, changeEmoji, - calloutEmoji = '💡', + calloutEmoji = '\u{1F4A1}', textEditor, textEditorInitialState, - nodeKey, + nodeKey: _nodeKey, toggleEmojiPicker, showEmojiPicker -}) { - const emojiButtonRef = React.useRef(null); +}: CalloutCardProps) { + const emojiButtonRef = React.useRef(null); const {darkMode} = React.useContext(KoenigComposerContext); React.useEffect(() => { @@ -134,7 +152,7 @@ export function CalloutCard({ ) : ( @@ -169,22 +187,3 @@ export function CalloutCard({ ); } - -CalloutCard.propTypes = { - color: PropTypes.oneOf(['white', 'grey', 'blue', 'green', 'yellow', 'red', 'pink', 'purple', 'accent']), - text: PropTypes.string, - hasEmoji: PropTypes.bool, - placeholder: PropTypes.string, - isEditing: PropTypes.bool, - updateText: PropTypes.func, - calloutEmoji: PropTypes.string, - setShowEmojiPicker: PropTypes.func, - toggleEmoji: PropTypes.func, - handleColorChange: PropTypes.func, - changeEmoji: PropTypes.func, - textEditor: PropTypes.object, - textEditorInitialState: PropTypes.object, - nodeKey: PropTypes.string, - toggleEmojiPicker: PropTypes.func, - showEmojiPicker: PropTypes.bool -}; diff --git a/packages/koenig-lexical/src/components/ui/cards/CodeBlockCard.jsx b/packages/koenig-lexical/src/components/ui/cards/CodeBlockCard.jsx deleted file mode 100644 index 832f2a1b85..0000000000 --- a/packages/koenig-lexical/src/components/ui/cards/CodeBlockCard.jsx +++ /dev/null @@ -1,140 +0,0 @@ -import CodeMirror from '@uiw/react-codemirror'; -import KoenigComposerContext from '../../../context/KoenigComposerContext'; -import PropTypes from 'prop-types'; -import React from 'react'; -import {CardCaptionEditor} from '../CardCaptionEditor'; -import {css} from '@codemirror/lang-css'; -import {darkBaseExtensions, lightBaseExtensions} from '../../../utils/codemirror-config'; -import {html} from '@codemirror/lang-html'; -import {javascript} from '@codemirror/lang-javascript'; - -const languageMap = new Map([ - ['javascript', javascript], - ['js', javascript], - ['html', html], - ['css', css] -]); - -export function CodeEditor({code, language, updateCode, updateLanguage}) { - const [showLanguage, setShowLanguage] = React.useState(true); - const {darkMode} = React.useContext(KoenigComposerContext); - - // show the language input when the mouse moves - React.useEffect(() => { - const onMouseMove = () => { - setShowLanguage(true); - }; - - window.addEventListener('mousemove', onMouseMove); - - return () => { - window.removeEventListener('mousemove', onMouseMove); - }; - }, []); - - const onChange = React.useCallback((value) => { - setShowLanguage(false); // hide language input whenever the user types in the editor - updateCode(value); - }, [updateCode]); - - const onLanguageChange = React.useCallback((event) => { - updateLanguage(event.target.value); - }, [updateLanguage]); - - const extensions = React.useMemo(() => { - const base = darkMode ? darkBaseExtensions : lightBaseExtensions; - const highlighter = languageMap.get(language?.toLowerCase().trim()); - return highlighter ? [...base, highlighter()] : base; - }, [darkMode, language]); - - return ( -
    - - -
    - ); -} - -export function CodeBlock({code, darkMode, language}) { - const preClass = darkMode - ? `rounded-md border border-grey-950 bg-grey-950 px-2 py-[6px] font-mono text-[1.6rem] leading-9 text-grey-400 whitespace-pre-wrap` - : `rounded-md border border-grey-200 bg-grey-100 px-2 py-[6px] font-mono text-[1.6rem] leading-9 text-grey-900 whitespace-pre-wrap`; - return ( -
    -
    -                
    -                    {code}
    -                
    -            
    -
    - {language} -
    -
    - ); -} - -export function CodeBlockCard({captionEditor, captionEditorInitialState, code, darkMode, isEditing, isSelected, language, updateCode, updateLanguage}) { - if (isEditing) { - return ( - - ); - } else { - return ( - <> - - - - ); - } -} - -CodeEditor.propTypes = { - code: PropTypes.string, - language: PropTypes.string, - updateCode: PropTypes.func, - updateLanguage: PropTypes.func -}; - -CodeBlock.propTypes = { - code: PropTypes.string, - darkMode: PropTypes.bool, - language: PropTypes.string -}; - -CodeBlockCard.propTypes = { - code: PropTypes.string, - darkMode: PropTypes.bool, - language: PropTypes.string, - captionEditor: PropTypes.object, - captionEditorInitialState: PropTypes.object, - isEditing: PropTypes.bool, - isSelected: PropTypes.bool, - updateCode: PropTypes.func, - updateLanguage: PropTypes.func -}; diff --git a/packages/koenig-lexical/src/components/ui/cards/CodeBlockCard.stories.jsx b/packages/koenig-lexical/src/components/ui/cards/CodeBlockCard.stories.tsx similarity index 99% rename from packages/koenig-lexical/src/components/ui/cards/CodeBlockCard.stories.jsx rename to packages/koenig-lexical/src/components/ui/cards/CodeBlockCard.stories.tsx index 950815eda5..426974616b 100644 --- a/packages/koenig-lexical/src/components/ui/cards/CodeBlockCard.stories.jsx +++ b/packages/koenig-lexical/src/components/ui/cards/CodeBlockCard.stories.tsx @@ -1,3 +1,4 @@ +// @ts-nocheck import populateEditor from '../../../utils/storybook/populate-storybook-editor'; import {CardWrapper} from './../CardWrapper'; import {CodeBlockCard} from './CodeBlockCard'; diff --git a/packages/koenig-lexical/src/components/ui/cards/CodeBlockCard.tsx b/packages/koenig-lexical/src/components/ui/cards/CodeBlockCard.tsx new file mode 100644 index 0000000000..7ce4ea8831 --- /dev/null +++ b/packages/koenig-lexical/src/components/ui/cards/CodeBlockCard.tsx @@ -0,0 +1,281 @@ +import CodeMirror from '@uiw/react-codemirror'; +import KoenigComposerContext from '../../../context/KoenigComposerContext'; +import React from 'react'; +import {CardCaptionEditor} from '../CardCaptionEditor'; +import {EditorView, keymap, lineNumbers} from '@codemirror/view'; +import {HighlightStyle, syntaxHighlighting} from '@codemirror/language'; +import {css} from '@codemirror/lang-css'; +import {history, standardKeymap} from '@codemirror/commands'; +import {html} from '@codemirror/lang-html'; +import {javascript} from '@codemirror/lang-javascript'; +import {minimalSetup} from '@uiw/codemirror-extensions-basic-setup'; +import {tags as t} from '@lezer/highlight'; +import type {LexicalEditor} from 'lexical'; + +interface CodeEditorProps { + code?: string; + language?: string; + updateCode?: (value: string) => void; + updateLanguage?: (value: string) => void; + darkMode?: boolean; +} + +export function CodeEditor({code, language, updateCode, updateLanguage}: CodeEditorProps) { + const [showLanguage, setShowLanguage] = React.useState(true); + const {darkMode} = React.useContext(KoenigComposerContext); + + // show the language input when the mouse moves + React.useEffect(() => { + const onMouseMove = () => { + setShowLanguage(true); + }; + + window.addEventListener('mousemove', onMouseMove); + + return () => { + window.removeEventListener('mousemove', onMouseMove); + }; + }, []); + + const onChange = React.useCallback((value: string) => { + setShowLanguage(false); // hide language input whenever the user types in the editor + updateCode?.(value); + }, [updateCode]); + + const onLanguageChange = React.useCallback((event: React.ChangeEvent) => { + updateLanguage?.(event.target.value); + }, [updateLanguage]); + + const editorLightCSS = EditorView.theme({ + '&.cm-editor': { + background: 'transparent' + }, + '&.cm-focused': { + outline: '0' + }, + '&.cm-editor .cm-content': { + padding: '7px 0' + }, + '&.cm-editor .cm-scroller': { + overflow: 'auto' + }, + '&.cm-editor .cm-gutters': { + background: 'none', + border: 'none', + fontFamily: 'Consolas,Liberation Mono,Menlo,Courier,monospace;', + color: '#CED4D9', + lineHeight: '2.25rem' + }, + '&.cm-editor .cm-gutter': { + minHeight: '170px' + }, + '&.cm-editor .cm-lineNumbers': { + padding: '0' + }, + '&.cm-editor .cm-foldGutter': { + width: '0' + }, + '&.cm-editor .cm-line': { + padding: '0 .8rem', + color: '#394047', + fontFamily: 'Consolas,Liberation Mono,Menlo,Courier,monospace;', + fontSize: '1.6rem', + lineHeight: '2.25rem' + }, + '&.cm-editor .cm-activeLine, &.cm-editor .cm-activeLineGutter': { + background: 'none' + }, + '&.cm-editor .cm-cursor, &.cm-editor .cm-dropCursor': { + borderLeft: '1.2px solid black' + } + }); + + const editorDarkCSS = EditorView.theme({ + '&.cm-editor': { + background: 'transparent' + }, + '&.cm-focused': { + outline: '0' + }, + '&.cm-editor .cm-content': { + padding: '7px 0' + }, + '&.cm-editor .cm-scroller': { + overflow: 'auto' + }, + '&.cm-editor .cm-gutters': { + background: 'none', + border: 'none', + fontFamily: 'Consolas,Liberation Mono,Menlo,Courier,monospace;', + color: 'rgb(108, 118, 127);', + lineHeight: '2.25rem' + }, + '&.cm-editor .cm-gutter': { + minHeight: '170px' + }, + '&.cm-editor .cm-lineNumbers': { + padding: '0' + }, + '&.cm-editor .cm-foldGutter': { + width: '0' + }, + '&.cm-editor .cm-line': { + padding: '0 .8rem', + color: 'rgb(210, 215, 218)', + fontFamily: 'Consolas,Liberation Mono,Menlo,Courier,monospace;', + fontSize: '1.6rem', + lineHeight: '2.25rem' + }, + '&.cm-editor .cm-activeLine, &.cm-editor .cm-activeLineGutter': { + background: 'none' + }, + '&.cm-editor .cm-cursor, &.cm-editor .cm-dropCursor': { + borderLeft: '1.2px solid white' + } + + }); + + const editorLightHighlightStyle = HighlightStyle.define([ + {tag: t.keyword, color: '#5A5CAD'}, + {tag: t.atom, color: '#6C8CD5'}, + {tag: t.number, color: '#116644'}, + {tag: t.definition(t.variableName), textDecoration: 'underline'}, + {tag: t.variableName, color: 'black'}, + {tag: t.comment, color: '#0080FF', fontStyle: 'italic', background: 'rgba(0,0,0,.05)'}, + {tag: [t.string, t.special(t.brace)], color: '#183691'}, + {tag: t.meta, color: 'yellow'}, + {tag: t.bracket, color: '#63a35c'}, + {tag: t.tagName, color: '#63a35c'}, + {tag: t.attributeName, color: '#795da3'} + ]); + + const editorDarkHighlightStyle = HighlightStyle.define([ + {tag: t.keyword, color: '#795da3'}, + {tag: t.atom, color: '#6C8CD5'}, + {tag: t.number, color: '#63a35c'}, + {tag: t.definition(t.variableName), textDecoration: 'underline'}, + {tag: t.variableName, color: 'white'}, + {tag: t.comment, color: '#0080FF', fontStyle: 'italic', background: 'rgba(0,0,0,.05)'}, + {tag: [t.string, t.special(t.brace)], color: 'rgb(72, 110, 225)'}, + {tag: t.meta, color: 'yellow'}, + {tag: t.bracket, color: '#63a35c'}, + {tag: t.tagName, color: '#63a35c'}, + {tag: t.attributeName, color: '#795da3'}, + {tag: [t.className, t.propertyName], color: 'rgb(72, 110, 225)'} + ]); + + const editorCSS = darkMode ? editorDarkCSS : editorLightCSS; + const editorHighlightStyle = darkMode ? editorDarkHighlightStyle : editorLightHighlightStyle; + + // Base extensions for the CodeMirror editor + const extensions = [ + EditorView.lineWrapping, // wraps lines that exceed the viewport width + syntaxHighlighting(editorHighlightStyle), // customizes syntax highlighting rules + editorCSS, // customizes general editor appearance (does not include syntax highlighting) + lineNumbers(), // adds line numbers to the gutter + minimalSetup({defaultKeymap: false, history: false}), // disable defaultKeymap to prevent Mod+Enter from inserting new line + keymap.of(standardKeymap), // add back in standardKeymap, which doesn't include Mod+Enter + // adds undo/redo functionality with custom behaviour to make tests faster + history({ + joinToEvent: process.env.NODE_ENV === 'test' ? () => false : undefined + }) + ]; + + // If provided language is supported, add the corresponding extension + const languageMap: Record = { + javascript: javascript, + js: javascript, + html: html, + css: css + }; + const highlighter = languageMap[language?.toLowerCase().trim() || ''] || null; + if (highlighter) { + extensions.push(highlighter()); + } + + return ( +
    + + +
    + ); +} + +interface CodeBlockProps { + code?: string; + darkMode?: boolean; + language?: string; +} + +export function CodeBlock({code, darkMode, language}: CodeBlockProps) { + const preClass = darkMode + ? `rounded-md border border-grey-950 bg-grey-950 px-2 py-[6px] font-mono text-[1.6rem] leading-9 text-grey-400 whitespace-pre-wrap` + : `rounded-md border border-grey-200 bg-grey-100 px-2 py-[6px] font-mono text-[1.6rem] leading-9 text-grey-900 whitespace-pre-wrap`; + return ( +
    +
    +                
    +                    {code}
    +                
    +            
    +
    + {language} +
    +
    + ); +} + +interface CodeBlockCardProps { + captionEditor?: LexicalEditor; + captionEditorInitialState?: string; + code?: string; + darkMode?: boolean; + isEditing?: boolean; + isSelected?: boolean; + language?: string; + updateCode?: (value: string) => void; + updateLanguage?: (value: string) => void; +} + +export function CodeBlockCard({captionEditor, captionEditorInitialState, code, darkMode, isEditing, isSelected, language, updateCode, updateLanguage}: CodeBlockCardProps) { + if (isEditing) { + return ( + + ); + } else { + return ( + <> + + {captionEditor && ( + + )} + + ); + } +} diff --git a/packages/koenig-lexical/src/components/ui/cards/EmailCard.stories.jsx b/packages/koenig-lexical/src/components/ui/cards/EmailCard.stories.tsx similarity index 99% rename from packages/koenig-lexical/src/components/ui/cards/EmailCard.stories.jsx rename to packages/koenig-lexical/src/components/ui/cards/EmailCard.stories.tsx index 4a0a4df174..4d37d9de22 100644 --- a/packages/koenig-lexical/src/components/ui/cards/EmailCard.stories.jsx +++ b/packages/koenig-lexical/src/components/ui/cards/EmailCard.stories.tsx @@ -1,3 +1,4 @@ +// @ts-nocheck import EmailIndicatorIcon from '../../../assets/icons/kg-indicator-email.svg?react'; import populateEditor from '../../../utils/storybook/populate-storybook-editor'; import {BASIC_NODES} from '../../../index.js'; diff --git a/packages/koenig-lexical/src/components/ui/cards/EmailCard.jsx b/packages/koenig-lexical/src/components/ui/cards/EmailCard.tsx similarity index 79% rename from packages/koenig-lexical/src/components/ui/cards/EmailCard.jsx rename to packages/koenig-lexical/src/components/ui/cards/EmailCard.tsx index ca6485b2a8..39d0672496 100644 --- a/packages/koenig-lexical/src/components/ui/cards/EmailCard.jsx +++ b/packages/koenig-lexical/src/components/ui/cards/EmailCard.tsx @@ -1,21 +1,27 @@ import KoenigNestedEditor from '../../KoenigNestedEditor'; -import PropTypes from 'prop-types'; import ReplacementStringsPlugin from '../../../plugins/ReplacementStringsPlugin'; import {CardVisibilityMessage} from '../CardVisibilityMessage.jsx'; import {ReadOnlyOverlay} from '../ReadOnlyOverlay'; +import type {LexicalEditor} from 'lexical'; + +interface EmailCardProps { + htmlEditor?: LexicalEditor; + htmlEditorInitialState?: string; + isEditing?: boolean; +} export function EmailCard({ htmlEditor, htmlEditorInitialState, isEditing = false -}) { +}: EmailCardProps) { return ( <>
    ); } - -EmailCard.propTypes = { - htmlEditor: PropTypes.object, - isEditing: PropTypes.bool, - htmlEditorInitialState: PropTypes.object -}; diff --git a/packages/koenig-lexical/src/components/ui/cards/EmailCtaCard.stories.jsx b/packages/koenig-lexical/src/components/ui/cards/EmailCtaCard.stories.tsx similarity index 99% rename from packages/koenig-lexical/src/components/ui/cards/EmailCtaCard.stories.jsx rename to packages/koenig-lexical/src/components/ui/cards/EmailCtaCard.stories.tsx index 38e1881026..7417306dd3 100644 --- a/packages/koenig-lexical/src/components/ui/cards/EmailCtaCard.stories.jsx +++ b/packages/koenig-lexical/src/components/ui/cards/EmailCtaCard.stories.tsx @@ -1,3 +1,4 @@ +// @ts-nocheck import EmailIndicatorIcon from '../../../assets/icons/kg-indicator-email.svg?react'; import populateEditor from '../../../utils/storybook/populate-storybook-editor.js'; import {BASIC_NODES} from '../../../index.js'; diff --git a/packages/koenig-lexical/src/components/ui/cards/EmailCtaCard.jsx b/packages/koenig-lexical/src/components/ui/cards/EmailCtaCard.tsx similarity index 84% rename from packages/koenig-lexical/src/components/ui/cards/EmailCtaCard.jsx rename to packages/koenig-lexical/src/components/ui/cards/EmailCtaCard.tsx index e8a28f7acd..6a9f48be8f 100644 --- a/packages/koenig-lexical/src/components/ui/cards/EmailCtaCard.jsx +++ b/packages/koenig-lexical/src/components/ui/cards/EmailCtaCard.tsx @@ -1,12 +1,31 @@ import CenterAlignIcon from '../../../assets/icons/kg-align-center.svg?react'; import KoenigNestedEditor from '../../KoenigNestedEditor'; import LeftAlignIcon from '../../../assets/icons/kg-align-left.svg?react'; -import PropTypes from 'prop-types'; +import React from 'react'; import ReplacementStringsPlugin from '../../../plugins/ReplacementStringsPlugin'; import {Button} from '../Button'; import {ButtonGroupSetting, DropdownSetting, InputSetting, InputUrlSetting, SettingsPanel, ToggleSetting} from '../SettingsPanel'; import {CardVisibilityMessage} from '../CardVisibilityMessage.jsx'; import {ReadOnlyOverlay} from '../ReadOnlyOverlay'; +import type {LexicalEditor} from 'lexical'; + +interface EmailCtaCardProps { + alignment?: 'left' | 'center'; + buttonText?: string; + buttonUrl?: string; + handleSegmentChange?: (value: string) => void; + htmlEditor?: LexicalEditor; + htmlEditorInitialState?: string; + isEditing?: boolean; + segment?: 'status:free' | 'status:-free'; + showDividers?: boolean; + showButton?: boolean; + toggleDividers?: (event: React.ChangeEvent) => void; + updateAlignment?: (name: string) => void; + updateShowButton?: (event: React.ChangeEvent) => void; + updateButtonText?: (e: React.ChangeEvent) => void; + updateButtonUrl?: (value: string) => void; +} export function EmailCtaCard({ alignment = 'left', @@ -24,7 +43,7 @@ export function EmailCtaCard({ updateShowButton, updateButtonText, updateButtonUrl -}) { +}: EmailCtaCardProps) { const alignmentOpts = [ { label: 'Left', @@ -48,7 +67,7 @@ export function EmailCtaCard({ name: 'status:-free' }]; - const getVisibilityMessage = (segmentType) => { + const getVisibilityMessage = (segmentType: string) => { switch (segmentType) { case 'status:free': return 'Hidden on website and paid newsletter'; @@ -74,7 +93,7 @@ export function EmailCtaCard({ {/* Alignment settings */} @@ -114,7 +133,7 @@ export function EmailCtaCard({ buttons={alignmentOpts} label='Content alignment' selectedName={alignment} - onClick={updateAlignment} + onClick={updateAlignment!} /> {/* Dividers settings */} @@ -145,7 +164,7 @@ export function EmailCtaCard({ dataTestId="button-url" label='Button URL' value={buttonUrl} - onChange={updateButtonUrl} + onChange={updateButtonUrl!} /> )} @@ -154,22 +173,3 @@ export function EmailCtaCard({ ); } - -EmailCtaCard.propTypes = { - alignment: PropTypes.oneOf(['left', 'center']), - buttonText: PropTypes.string, - buttonUrl: PropTypes.string, - isEditing: PropTypes.bool, - segment: PropTypes.oneOf(['status:free', 'status:-free']), - showButton: PropTypes.bool, - showDividers: PropTypes.bool, - updateAlignment: PropTypes.func, - updateButtonText: PropTypes.func, - updateButtonUrl: PropTypes.func, - updateShowButton: PropTypes.func, - toggleDividers: PropTypes.func, - suggestedUrls: PropTypes.array, - handleSegmentChange: PropTypes.func, - htmlEditor: PropTypes.object, - htmlEditorInitialState: PropTypes.object -}; diff --git a/packages/koenig-lexical/src/components/ui/cards/EmbedCard.stories.jsx b/packages/koenig-lexical/src/components/ui/cards/EmbedCard.stories.tsx similarity index 99% rename from packages/koenig-lexical/src/components/ui/cards/EmbedCard.stories.jsx rename to packages/koenig-lexical/src/components/ui/cards/EmbedCard.stories.tsx index 0403798509..9ea5e99213 100644 --- a/packages/koenig-lexical/src/components/ui/cards/EmbedCard.stories.jsx +++ b/packages/koenig-lexical/src/components/ui/cards/EmbedCard.stories.tsx @@ -1,3 +1,4 @@ +// @ts-nocheck import populateEditor from '../../../utils/storybook/populate-storybook-editor'; import {CardWrapper} from './../CardWrapper'; import {EmbedCard} from './EmbedCard'; diff --git a/packages/koenig-lexical/src/components/ui/cards/EmbedCard.jsx b/packages/koenig-lexical/src/components/ui/cards/EmbedCard.tsx similarity index 60% rename from packages/koenig-lexical/src/components/ui/cards/EmbedCard.jsx rename to packages/koenig-lexical/src/components/ui/cards/EmbedCard.tsx index ce3b12b5dd..f35f71bb7c 100644 --- a/packages/koenig-lexical/src/components/ui/cards/EmbedCard.jsx +++ b/packages/koenig-lexical/src/components/ui/cards/EmbedCard.tsx @@ -1,10 +1,26 @@ import '@tryghost/kg-simplemde/dist/simplemde.min.css'; -import PropTypes from 'prop-types'; import React from 'react'; import {CardCaptionEditor} from '../CardCaptionEditor'; import {UrlInput} from '../UrlInput'; +import type {LexicalEditor} from 'lexical'; + +interface EmbedCardProps { + captionEditor?: LexicalEditor; + captionEditorInitialState?: string; + html?: string; + isSelected?: boolean; + urlInputValue?: string; + urlPlaceholder?: string; + urlError?: boolean; + isLoading?: boolean; + handleUrlChange?: (e: React.ChangeEvent) => void; + handleUrlSubmit: (e: KeyboardEvent | React.KeyboardEvent) => void; + handleRetry?: () => void; + handlePasteAsLink?: (value?: string) => void; + handleClose?: () => void; +} -export function EmbedCard({captionEditor, captionEditorInitialState, html, isSelected, urlInputValue, urlPlaceholder, urlError, isLoading, handleUrlChange, handleUrlSubmit, handleRetry, handlePasteAsLink, handleClose}) { +export function EmbedCard({captionEditor, captionEditorInitialState, html, isSelected, urlInputValue, urlPlaceholder, urlError, isLoading, handleUrlChange, handleUrlSubmit, handleRetry, handlePasteAsLink, handleClose}: EmbedCardProps) { if (html) { return (
    @@ -12,13 +28,15 @@ export function EmbedCard({captionEditor, captionEditorInitialState, html, isSel
    - + {captionEditor && ( + + )}
    ); } @@ -38,12 +56,17 @@ export function EmbedCard({captionEditor, captionEditorInitialState, html, isSel ); } -function EmbedIframe({dataTestId, html}) { - const iframeRef = React.useRef(null); +interface EmbedIframeProps { + dataTestId?: string; + html: string; +} + +function EmbedIframe({dataTestId, html}: EmbedIframeProps) { + const iframeRef = React.useRef(null); const handleResize = () => { // get ratio from nested iframe if present (eg, Vimeo) - const firstElement = iframeRef.current?.contentDocument?.body?.firstChild; + const firstElement = iframeRef.current?.contentDocument?.body?.firstChild as HTMLElement | null; // won't have an iframe if the embed is invalid or fetching if (!firstElement) { @@ -51,20 +74,21 @@ function EmbedIframe({dataTestId, html}) { } if (firstElement.tagName === 'IFRAME') { - const widthAttr = firstElement.getAttribute('width'); - const heightAttr = firstElement.getAttribute('height'); + const iframeElement = firstElement as HTMLIFrameElement; + const widthAttr = iframeElement.getAttribute('width'); + const heightAttr = iframeElement.getAttribute('height'); if (widthAttr && heightAttr && widthAttr.indexOf('%') === -1 && heightAttr.indexOf('%') === -1) { const ratio = parseInt(widthAttr) / parseInt(heightAttr); - const newHeight = iframeRef.current.offsetWidth / ratio; - firstElement.style.height = `${newHeight}px`; - iframeRef.current.style.height = `${newHeight}px`; - firstElement.style.width = '100%'; + const newHeight = iframeRef.current!.offsetWidth / ratio; + iframeElement.style.height = `${newHeight}px`; + iframeRef.current!.style.height = `${newHeight}px`; + iframeElement.style.width = '100%'; return; } if (heightAttr && heightAttr.indexOf('%') === -1) { - iframeRef.current.style.height = `${heightAttr}px`; + iframeRef.current!.style.height = `${heightAttr}px`; return; } } @@ -76,11 +100,11 @@ function EmbedIframe({dataTestId, html}) { return; } - iframeRef.current.style.height = `${scrollHeight}px`; + iframeRef.current!.style.height = `${scrollHeight}px`; }; // register mutation observer to handle changes to iframe content (e.g. twitter embeds loading richer content) - const config = { + const config: MutationObserverInit = { attributes: true, attributeOldValue: false, characterData: true, @@ -91,7 +115,7 @@ function EmbedIframe({dataTestId, html}) { const mutationObserver = new MutationObserver(handleResize); const handleLoad = () => { - const iframeBody = iframeRef.current.contentDocument.body; + const iframeBody = iframeRef.current!.contentDocument!.body; // apply styles iframeBody.style.display = 'flex'; iframeBody.style.margin = '0'; @@ -99,20 +123,20 @@ function EmbedIframe({dataTestId, html}) { // resize first load handleResize(); // start listening to mutations when the iframe content is loaded - mutationObserver.observe(iframeRef.current.contentWindow.document, config); + mutationObserver.observe(iframeRef.current!.contentWindow!.document, config); }; // register listener for window resize events React.useEffect(() => { const resizeObserver = new ResizeObserver(handleResize); - resizeObserver.observe(iframeRef.current); + resizeObserver.observe(iframeRef.current!); // cleanup listener when component unmounts return function cleanup() { resizeObserver.disconnect(); mutationObserver.disconnect(); }; - }, []); + }, []); // eslint-disable-line react-hooks/exhaustive-deps return (