Skip to content
Merged
31 changes: 27 additions & 4 deletions packages/common/core/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,22 @@
// Shared exports
export {assertNonNullable} from './src/shared/checkers/assertNonNullable'
export {convertDataAttrs, cx, merge} from './src/shared/utils'
export {KEYBOARD, DEFAULT_MARKUP, DEFAULT_OVERLAY_TRIGGER} from './src/shared/constants'
export {
convertDataAttrs,
cx,
merge,
resolveOptionSlot,
resolveSlot,
resolveSlotProps,
type SlotName,
} from './src/shared/utils'
export {
KEYBOARD,
DEFAULT_MARKUP,
DEFAULT_OVERLAY_TRIGGER,
DEFAULT_OPTIONS,
type DefaultOption,
type DefaultOverlayConfig,
} from './src/shared/constants'
export type {
OverlayMatch,
EventKey,
Expand All @@ -16,6 +31,7 @@ export type {
GenericAttributes,
CoreSlots,
CoreSlotProps,
DataAttributes,
} from './src/shared/types'

// Parsing exports (modern API)
Expand Down Expand Up @@ -43,7 +59,14 @@ export {findGap, getClosestIndexes} from './src/features/preparsing'
export {toString} from './src/features/parsing'
export {shallow, createNewSpan, deleteMark} from './src/features/text-manipulation'
export {Store, type StoreOptions} from './src/features/store'
export {OverlayController} from './src/features/overlay'
export {
OverlayController,
createMarkFromOverlay,
filterSuggestions,
navigateSuggestions,
type NavigationAction,
type NavigationResult,
} from './src/features/overlay'
export {FocusController} from './src/features/focus'
export {KeyDownController} from './src/features/input'
export {SystemListenerController} from './src/features/events'
Expand All @@ -59,7 +82,7 @@ export {createCoreFeatures} from './src/features/coreFeatures'
export {Lifecycle, type LifecycleOptions} from './src/features/lifecycle'

// Mark Handler
export {MarkHandler, type RefAccessor} from './src/features/mark'
export {MarkHandler, type MarkOptions, type RefAccessor} from './src/features/mark'

// Blocks
export {splitTokensIntoBlocks, reorderBlocks, type Block} from './src/features/blocks'
Expand Down
3 changes: 2 additions & 1 deletion packages/common/core/src/features/mark/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export {MarkHandler, type RefAccessor} from './MarkHandler'
export {MarkHandler, type RefAccessor} from './MarkHandler'
export type {MarkOptions} from './types'
3 changes: 3 additions & 0 deletions packages/common/core/src/features/mark/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface MarkOptions {
controlled?: boolean
}
26 changes: 26 additions & 0 deletions packages/common/core/src/features/overlay/createMarkFromOverlay.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type {OverlayMatch} from '../../shared/types'
import type {MarkToken} from '../parsing'

export function createMarkFromOverlay(match: OverlayMatch, value: string, meta?: string): MarkToken {
return {
type: 'mark',
value,
meta,
content: '',
position: {
start: match.index,
end: match.index + match.span.length,
},
descriptor: {
markup: match.option.markup!,
index: 0,
segments: [],
gapTypes: [],
hasNested: false,
hasTwoValues: false,
segmentGlobalIndices: [],
},
children: [],
nested: undefined,
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export function filterSuggestions(data: string[], search: string): string[] {
const query = search.toLowerCase()
return data.filter(s => s.toLowerCase().indexOf(query) > -1)
}
5 changes: 4 additions & 1 deletion packages/common/core/src/features/overlay/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
export {OverlayController} from './OverlayController'
export {filterSuggestions} from './filterSuggestions'
export {createMarkFromOverlay} from './createMarkFromOverlay'
export {OverlayController} from './OverlayController'
export {navigateSuggestions, type NavigationAction, type NavigationResult} from './suggestionNavigation'
25 changes: 25 additions & 0 deletions packages/common/core/src/features/overlay/suggestionNavigation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import {KEYBOARD} from '../../shared/constants'

export type NavigationAction = 'up' | 'down' | 'select' | 'none'

export interface NavigationResult {
action: NavigationAction
index: number
}

export function navigateSuggestions(key: string, activeIndex: number, length: number): NavigationResult {
if (length === 0) return {action: 'none', index: activeIndex}

const hasActive = !isNaN(activeIndex)

switch (key) {
case KEYBOARD.UP:
return {action: 'up', index: hasActive ? (length + ((activeIndex - 1) % length)) % length : 0}
case KEYBOARD.DOWN:
return {action: 'down', index: hasActive ? (activeIndex + 1) % length : 0}
case KEYBOARD.ENTER:
return hasActive ? {action: 'select', index: activeIndex} : {action: 'none', index: activeIndex}
default:
return {action: 'none', index: activeIndex}
}
}
22 changes: 21 additions & 1 deletion packages/common/core/src/shared/constants.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
import type {Markup} from '../features/parsing/ParserV2/types'
import type {CoreOption} from './types'

export interface DefaultOverlayConfig {
trigger?: string
data?: string[]
}

export interface DefaultOption extends CoreOption {
overlay?: DefaultOverlayConfig
}

export enum KEYBOARD {
// Navigation Keys
Expand Down Expand Up @@ -27,4 +37,14 @@ export enum KEYBOARD {

export const DEFAULT_OVERLAY_TRIGGER = '@'

export const DEFAULT_MARKUP: Markup = '@[__value__](__meta__)'
export const DEFAULT_MARKUP: Markup = '@[__value__](__meta__)'

export const DEFAULT_OPTIONS: DefaultOption[] = [
{
markup: DEFAULT_MARKUP,
overlay: {
trigger: DEFAULT_OVERLAY_TRIGGER,
data: [],
},
},
]
2 changes: 2 additions & 0 deletions packages/common/core/src/shared/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ export type OverlayTrigger = Array<'change' | 'selectionChange'> | 'change' | 's

export type StyleProperties = Record<string, string | number>

export type DataAttributes = Record<`data${Capitalize<string>}`, string | number | boolean | undefined>

export type GenericComponent = unknown
export type GenericElement = unknown
export type GenericAttributes = Record<string, unknown>
Expand Down
4 changes: 3 additions & 1 deletion packages/common/core/src/shared/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export {convertDataAttrs} from './dataAttributes'
export {cx} from './cx'
export {merge} from './merge'
export {merge} from './merge'
export {resolveOptionSlot} from './resolveOptionSlot'
export {resolveSlot, resolveSlotProps, type SlotName} from './resolveSlot'
6 changes: 6 additions & 0 deletions packages/common/core/src/shared/utils/resolveOptionSlot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export function resolveOptionSlot<T extends object>(optionConfig: T | ((base: T) => T) | undefined, baseProps: T): T {
if (optionConfig !== undefined) {
return typeof optionConfig === 'function' ? optionConfig(baseProps) : optionConfig
}
return baseProps ?? {}
}
21 changes: 21 additions & 0 deletions packages/common/core/src/shared/utils/resolveSlot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type {CoreSlotProps, CoreSlots} from '../types'
import {convertDataAttrs} from './dataAttributes'

export type SlotName = 'container' | 'span'

const defaultSlots: Record<SlotName, string> = {
container: 'div',
span: 'span',
}

export function resolveSlot<T = string>(slotName: SlotName, slots: CoreSlots | undefined): T {
return (slots?.[slotName] ?? defaultSlots[slotName]) as T
}

export function resolveSlotProps<T = Record<string, unknown>>(
slotName: SlotName,
slotProps: CoreSlotProps | undefined
): T | undefined {
const props = slotProps?.[slotName]
return props ? (convertDataAttrs(props as Record<string, unknown>) as T) : undefined
}
15 changes: 11 additions & 4 deletions packages/react/markput/src/components/BlockContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import {splitTokensIntoBlocks, reorderBlocks, parseWithParser, type Block} from '@markput/core'
import {
resolveSlot,
resolveSlotProps,
splitTokensIntoBlocks,
reorderBlocks,
parseWithParser,
type Block,
} from '@markput/core'
import type {ElementType} from 'react'
import {memo, useCallback, useMemo, useRef} from 'react'

import {useStore} from '../lib/hooks/useStore'
import {resolveSlot, resolveSlotProps} from '../lib/utils/resolveSlot'
import {useStore} from '../lib/providers/StoreContext'
import {DraggableBlock} from './DraggableBlock'
import {Token} from './Token'

Expand All @@ -19,7 +26,7 @@ export const BlockContainer = memo(() => {
const key = store.key
const refs = store.refs

const ContainerComponent = useMemo(() => resolveSlot('container', slots), [slots])
const ContainerComponent = useMemo(() => resolveSlot<ElementType>('container', slots), [slots])
const containerProps = useMemo(() => resolveSlotProps('container', slotProps), [slotProps])

const blocks = useMemo(() => splitTokensIntoBlocks(tokens), [tokens])
Expand Down
7 changes: 4 additions & 3 deletions packages/react/markput/src/components/Container.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import {resolveSlot, resolveSlotProps} from '@markput/core'
import type {ElementType} from 'react'
import {memo, useMemo} from 'react'

import {useStore} from '../lib/hooks/useStore'
import {resolveSlot, resolveSlotProps} from '../lib/utils/resolveSlot'
import {useStore} from '../lib/providers/StoreContext'
import {Token} from './Token'

export const Container = memo(() => {
Expand All @@ -13,7 +14,7 @@ export const Container = memo(() => {
const style = store.state.style.use()
const key = store.key
const refs = store.refs
const ContainerComponent = useMemo(() => resolveSlot('container', slots), [slots])
const ContainerComponent = useMemo(() => resolveSlot<ElementType>('container', slots), [slots])
const containerProps = useMemo(() => resolveSlotProps('container', slotProps), [slotProps])

return (
Expand Down
4 changes: 2 additions & 2 deletions packages/react/markput/src/components/MarkRenderer.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type {MarkToken} from '@markput/core'

import {useSlot} from '../lib/hooks/useSlot'
import {useStore} from '../lib/hooks/useStore'
import {useToken} from '../lib/providers/TokenProvider'
import {useStore} from '../lib/providers/StoreContext'
import {useToken} from '../lib/providers/TokenContext'
import type {MarkProps} from '../types'
// eslint-disable-next-line import/no-cycle
import {Token} from './Token'
Expand Down
7 changes: 3 additions & 4 deletions packages/react/markput/src/components/MarkedInput.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import type {CoreSlotProps, CoreSlots, MarkputHandler, OverlayTrigger, StyleProperties} from '@markput/core'
import {cx, merge, Store} from '@markput/core'
import {cx, DEFAULT_OPTIONS, merge, Store} from '@markput/core'
import type {ComponentType, CSSProperties, Ref} from 'react'
import {useState} from 'react'

import {DEFAULT_OPTIONS} from '../constants'
import {createUseHook} from '../lib/hooks/createUseHook'
import {useCoreFeatures} from '../lib/hooks/useCoreFeatures'
import {StoreContext} from '../lib/providers/StoreContext'
Expand Down Expand Up @@ -118,9 +117,9 @@ export function MarkedInput<TMarkProps = MarkProps, TOverlayProps = OverlayProps
const ContainerImpl = block ? BlockContainer : Container

return (
<StoreContext.Provider value={store}>
<StoreContext value={store}>
<ContainerImpl />
<OverlayRenderer />
</StoreContext.Provider>
</StoreContext>
)
}
2 changes: 1 addition & 1 deletion packages/react/markput/src/components/OverlayRenderer.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {memo, useMemo} from 'react'

import {useSlot} from '../lib/hooks/useSlot'
import {useStore} from '../lib/hooks/useStore'
import {useStore} from '../lib/providers/StoreContext'
import {Suggestions} from './Suggestions'

export const OverlayRenderer = memo(() => {
Expand Down
33 changes: 12 additions & 21 deletions packages/react/markput/src/components/Suggestions/Suggestions.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import {KEYBOARD} from '@markput/core'
import {filterSuggestions, navigateSuggestions} from '@markput/core'
import type {RefObject} from 'react'
import {useEffect, useMemo, useState} from 'react'

import {useOverlay} from '../../lib/hooks/useOverlay'
import {useStore} from '../../lib/hooks/useStore'
import {useStore} from '../../lib/providers/StoreContext'

import styles from '@markput/core/styles.module.css'

Expand All @@ -12,41 +12,32 @@ export const Suggestions = () => {
const {match, select, style, ref} = useOverlay()
const [active, setActive] = useState(NaN)
const data = match.option.overlay?.data || []
const filtered = useMemo(
() => data.filter(s => s.toLowerCase().indexOf(match.value.toLowerCase()) > -1),
[match.value, data]
)
const filtered = useMemo(() => filterSuggestions(data, match.value), [match.value, data])
const length = filtered.length

useEffect(() => {
const container = store.refs.container
if (!container) return

const handler = (event: KeyboardEvent) => {
switch (event.key) {
case KEYBOARD.UP:
event.preventDefault()
setActive(prev => (isNaN(prev) ? 0 : (length + ((prev - 1) % length)) % length))
break
case KEYBOARD.DOWN:
const result = navigateSuggestions(event.key, active, length)
switch (result.action) {
case 'up':
case 'down':
event.preventDefault()
setActive(prev => (isNaN(prev) ? 0 : (prev + 1) % length))
setActive(result.index)
break
case KEYBOARD.ENTER:
case 'select':
event.preventDefault()
setActive(current => {
if (isNaN(current)) return current
const suggestion = filtered[current]
select({value: suggestion, meta: current.toString()})
return current
})
const suggestion = filtered[result.index]
select({value: suggestion, meta: result.index.toString()})
break
}
}

container.addEventListener('keydown', handler)
return () => container.removeEventListener('keydown', handler)
}, [length, filtered])
}, [length, filtered, active])

if (!filtered.length) return null

Expand Down
9 changes: 5 additions & 4 deletions packages/react/markput/src/components/TextSpan.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import {resolveSlot, resolveSlotProps} from '@markput/core'
import type {ElementType} from 'react'
import {useLayoutEffect, useMemo, useRef} from 'react'

import {useStore} from '../lib/hooks/useStore'
import {useToken} from '../lib/providers/TokenProvider'
import {resolveSlot, resolveSlotProps} from '../lib/utils/resolveSlot'
import {useStore} from '../lib/providers/StoreContext'
import {useToken} from '../lib/providers/TokenContext'

export const TextSpan = () => {
const token = useToken()
Expand All @@ -11,7 +12,7 @@ export const TextSpan = () => {

const slots = store.state.slots.use()
const slotProps = store.state.slotProps.use()
const SpanComponent = useMemo(() => resolveSlot('span', slots), [slots])
const SpanComponent = useMemo(() => resolveSlot<ElementType>('span', slots), [slots])
const spanProps = useMemo(() => resolveSlotProps('span', slotProps), [slotProps])

if (token.type !== 'text') {
Expand Down
Loading