@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@typebot.io/js",
|
||||
"version": "0.0.45",
|
||||
"version": "0.0.46",
|
||||
"description": "Javascript library to display typebots on your website",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
|
@ -297,3 +297,40 @@ textarea {
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
|
||||
.typebot-picture-button {
|
||||
color: var(--typebot-button-color);
|
||||
background-color: var(--typebot-button-bg-color);
|
||||
border-radius: var(--typebot-border-radius);
|
||||
transition: all 0.3s ease;
|
||||
width: 236px;
|
||||
}
|
||||
|
||||
.typebot-picture-button > img,
|
||||
.typebot-selectable-picture > img {
|
||||
border-radius: var(--typebot-border-radius) var(--typebot-border-radius) 0 0;
|
||||
min-width: 200px;
|
||||
width: 100%;
|
||||
max-height: 200px;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.typebot-selectable-picture {
|
||||
border: 1px solid rgba(var(--typebot-button-bg-color-rgb), 0.25);
|
||||
border-radius: var(--typebot-border-radius);
|
||||
color: var(--typebot-container-color);
|
||||
background-color: rgba(var(--typebot-button-bg-color-rgb), 0.08);
|
||||
transition: all 0.3s ease;
|
||||
width: 236px;
|
||||
}
|
||||
|
||||
.typebot-selectable-picture:hover {
|
||||
background-color: rgba(var(--typebot-button-bg-color-rgb), 0.12);
|
||||
border-color: rgba(var(--typebot-button-bg-color-rgb), 0.3);
|
||||
}
|
||||
|
||||
.typebot-selectable-picture.selected {
|
||||
background-color: rgba(var(--typebot-button-bg-color-rgb), 0.18);
|
||||
border-color: rgba(var(--typebot-button-bg-color-rgb), 0.35);
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ import type {
|
||||
TextInputBlock,
|
||||
Theme,
|
||||
UrlInputBlock,
|
||||
PictureChoiceBlock,
|
||||
} from '@typebot.io/schemas'
|
||||
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/enums'
|
||||
import { GuestBubble } from './bubbles/GuestBubble'
|
||||
@ -30,8 +31,8 @@ import { isMobile } from '@/utils/isMobileSignal'
|
||||
import { PaymentForm } from '@/features/blocks/inputs/payment'
|
||||
import { MultipleChoicesForm } from '@/features/blocks/inputs/buttons/components/MultipleChoicesForm'
|
||||
import { Buttons } from '@/features/blocks/inputs/buttons/components/Buttons'
|
||||
import { SearchableButtons } from '@/features/blocks/inputs/buttons/components/SearchableButtons'
|
||||
import { SearchableMultipleChoicesForm } from '@/features/blocks/inputs/buttons/components/SearchableMultipleChoicesForm'
|
||||
import { SinglePictureChoice } from '@/features/blocks/inputs/pictureChoice/SinglePictureChoice'
|
||||
import { MultiplePictureChoice } from '@/features/blocks/inputs/pictureChoice/MultiplePictureChoice'
|
||||
|
||||
type Props = {
|
||||
block: NonNullable<ChatReply['input']>
|
||||
@ -165,42 +166,40 @@ const Input = (props: {
|
||||
{(block) => (
|
||||
<Switch>
|
||||
<Match when={!block.options.isMultipleChoice}>
|
||||
<Switch>
|
||||
<Match when={block.options.isSearchable}>
|
||||
<SearchableButtons
|
||||
inputIndex={props.inputIndex}
|
||||
defaultItems={block.items}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
</Match>
|
||||
<Match when={!block.options.isSearchable}>
|
||||
<Buttons
|
||||
inputIndex={props.inputIndex}
|
||||
items={block.items}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
</Match>
|
||||
</Switch>
|
||||
<Buttons
|
||||
inputIndex={props.inputIndex}
|
||||
defaultItems={block.items}
|
||||
options={block.options}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
</Match>
|
||||
<Match when={block.options.isMultipleChoice}>
|
||||
<Switch>
|
||||
<Match when={block.options.isSearchable}>
|
||||
<SearchableMultipleChoicesForm
|
||||
inputIndex={props.inputIndex}
|
||||
defaultItems={block.items}
|
||||
options={block.options}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
</Match>
|
||||
<Match when={!block.options.isSearchable}>
|
||||
<MultipleChoicesForm
|
||||
inputIndex={props.inputIndex}
|
||||
items={block.items}
|
||||
options={block.options}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
</Match>
|
||||
</Switch>
|
||||
<MultipleChoicesForm
|
||||
inputIndex={props.inputIndex}
|
||||
defaultItems={block.items}
|
||||
options={block.options}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
</Match>
|
||||
</Switch>
|
||||
)}
|
||||
</Match>
|
||||
<Match when={isPictureChoiceBlock(props.block)} keyed>
|
||||
{(block) => (
|
||||
<Switch>
|
||||
<Match when={!block.options.isMultipleChoice}>
|
||||
<SinglePictureChoice
|
||||
defaultItems={block.items}
|
||||
options={block.options}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
</Match>
|
||||
<Match when={block.options.isMultipleChoice}>
|
||||
<MultiplePictureChoice
|
||||
defaultItems={block.items}
|
||||
options={block.options}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
</Match>
|
||||
</Switch>
|
||||
)}
|
||||
@ -240,3 +239,8 @@ const isButtonsBlock = (
|
||||
block: ChatReply['input']
|
||||
): ChoiceInputBlock | undefined =>
|
||||
block?.type === InputBlockType.CHOICE ? block : undefined
|
||||
|
||||
const isPictureChoiceBlock = (
|
||||
block: ChatReply['input']
|
||||
): PictureChoiceBlock | undefined =>
|
||||
block?.type === InputBlockType.PICTURE_CHOICE ? block : undefined
|
||||
|
@ -1,41 +1,82 @@
|
||||
import { Button } from '@/components/Button'
|
||||
import { SearchInput } from '@/components/inputs/SearchInput'
|
||||
import { InputSubmitContent } from '@/types'
|
||||
import { isMobile } from '@/utils/isMobileSignal'
|
||||
import type { ChoiceInputBlock } from '@typebot.io/schemas'
|
||||
import { For } from 'solid-js'
|
||||
import { defaultChoiceInputOptions } from '@typebot.io/schemas/features/blocks/inputs/choice'
|
||||
import { For, Show, createSignal, onMount } from 'solid-js'
|
||||
|
||||
type Props = {
|
||||
inputIndex: number
|
||||
items: ChoiceInputBlock['items']
|
||||
defaultItems: ChoiceInputBlock['items']
|
||||
options: ChoiceInputBlock['options']
|
||||
onSubmit: (value: InputSubmitContent) => void
|
||||
}
|
||||
|
||||
export const Buttons = (props: Props) => {
|
||||
let inputRef: HTMLInputElement | undefined
|
||||
const [filteredItems, setFilteredItems] = createSignal(props.defaultItems)
|
||||
|
||||
onMount(() => {
|
||||
if (!isMobile() && inputRef) inputRef.focus()
|
||||
})
|
||||
|
||||
// eslint-disable-next-line solid/reactivity
|
||||
const handleClick = (itemIndex: number) => () =>
|
||||
props.onSubmit({ value: props.items[itemIndex].content ?? '' })
|
||||
props.onSubmit({ value: filteredItems()[itemIndex].content ?? '' })
|
||||
|
||||
const filterItems = (inputValue: string) => {
|
||||
setFilteredItems(
|
||||
props.defaultItems.filter((item) =>
|
||||
item.content?.toLowerCase().includes((inputValue ?? '').toLowerCase())
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="flex flex-wrap justify-end gap-2">
|
||||
<For each={props.items}>
|
||||
{(item, index) => (
|
||||
<span class={'relative' + (isMobile() ? ' w-full' : '')}>
|
||||
<Button
|
||||
on:click={handleClick(index())}
|
||||
data-itemid={item.id}
|
||||
class="w-full"
|
||||
>
|
||||
{item.content}
|
||||
</Button>
|
||||
{props.inputIndex === 0 && props.items.length === 1 && (
|
||||
<span class="flex h-3 w-3 absolute top-0 right-0 -mt-1 -mr-1 ping">
|
||||
<span class="animate-ping absolute inline-flex h-full w-full rounded-full brightness-200 opacity-75" />
|
||||
<span class="relative inline-flex rounded-full h-3 w-3 brightness-150" />
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</For>
|
||||
<div class="flex flex-col gap-2 w-full">
|
||||
<Show when={props.options.isSearchable}>
|
||||
<div class="flex items-end typebot-input w-full">
|
||||
<SearchInput
|
||||
ref={inputRef}
|
||||
onInput={filterItems}
|
||||
placeholder={
|
||||
props.options.searchInputPlaceholder ??
|
||||
defaultChoiceInputOptions.searchInputPlaceholder
|
||||
}
|
||||
onClear={() => setFilteredItems(props.defaultItems)}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<div
|
||||
class={
|
||||
'flex flex-wrap justify-end gap-2' +
|
||||
(props.options.isSearchable
|
||||
? ' overflow-y-scroll max-h-80 rounded-md hide-scrollbar'
|
||||
: '')
|
||||
}
|
||||
>
|
||||
<For each={filteredItems()}>
|
||||
{(item, index) => (
|
||||
<span class={'relative' + (isMobile() ? ' w-full' : '')}>
|
||||
<Button
|
||||
on:click={handleClick(index())}
|
||||
data-itemid={item.id}
|
||||
class="w-full"
|
||||
>
|
||||
{item.content}
|
||||
</Button>
|
||||
{props.inputIndex === 0 && props.defaultItems.length === 1 && (
|
||||
<span class="flex h-3 w-3 absolute top-0 right-0 -mt-1 -mr-1 ping">
|
||||
<span class="animate-ping absolute inline-flex h-full w-full rounded-full brightness-200 opacity-75" />
|
||||
<span class="relative inline-flex rounded-full h-3 w-3 brightness-150" />
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -3,12 +3,17 @@ import { Show } from 'solid-js'
|
||||
|
||||
type Props = {
|
||||
isChecked: boolean
|
||||
class?: string
|
||||
}
|
||||
|
||||
export const Checkbox = (props: Props) => {
|
||||
return (
|
||||
<div
|
||||
class={'w-4 h-4 typebot-checkbox' + (props.isChecked ? ' checked' : '')}
|
||||
class={
|
||||
'w-4 h-4 typebot-checkbox' +
|
||||
(props.isChecked ? ' checked' : '') +
|
||||
(props.class ? ` ${props.class}` : '')
|
||||
}
|
||||
>
|
||||
<Show when={props.isChecked}>
|
||||
<CheckIcon />
|
||||
|
@ -1,58 +1,98 @@
|
||||
import { SendButton } from '@/components/SendButton'
|
||||
import { InputSubmitContent } from '@/types'
|
||||
import { isMobile } from '@/utils/isMobileSignal'
|
||||
import type { ChoiceInputBlock } from '@typebot.io/schemas'
|
||||
import { createSignal, For } from 'solid-js'
|
||||
import { ChoiceInputBlock } from '@typebot.io/schemas'
|
||||
import { createSignal, For, onMount, Show } from 'solid-js'
|
||||
import { Checkbox } from './Checkbox'
|
||||
import { SearchInput } from '@/components/inputs/SearchInput'
|
||||
import { defaultChoiceInputOptions } from '@typebot.io/schemas/features/blocks/inputs/choice'
|
||||
|
||||
type Props = {
|
||||
inputIndex: number
|
||||
items: ChoiceInputBlock['items']
|
||||
defaultItems: ChoiceInputBlock['items']
|
||||
options: ChoiceInputBlock['options']
|
||||
onSubmit: (value: InputSubmitContent) => void
|
||||
}
|
||||
|
||||
export const MultipleChoicesForm = (props: Props) => {
|
||||
const [selectedIndices, setSelectedIndices] = createSignal<number[]>([])
|
||||
let inputRef: HTMLInputElement | undefined
|
||||
const [filteredItems, setFilteredItems] = createSignal(props.defaultItems)
|
||||
const [selectedItemIds, setSelectedItemIds] = createSignal<string[]>([])
|
||||
|
||||
const handleClick = (itemIndex: number) => {
|
||||
toggleSelectedItemIndex(itemIndex)
|
||||
onMount(() => {
|
||||
if (!isMobile() && inputRef) inputRef.focus()
|
||||
})
|
||||
|
||||
const handleClick = (itemId: string) => {
|
||||
toggleSelectedItemId(itemId)
|
||||
}
|
||||
|
||||
const toggleSelectedItemIndex = (itemIndex: number) => {
|
||||
const existingIndex = selectedIndices().indexOf(itemIndex)
|
||||
const toggleSelectedItemId = (itemId: string) => {
|
||||
const existingIndex = selectedItemIds().indexOf(itemId)
|
||||
if (existingIndex !== -1) {
|
||||
setSelectedIndices((selectedIndices) =>
|
||||
selectedIndices.filter((index) => index !== itemIndex)
|
||||
setSelectedItemIds((selectedItemIds) =>
|
||||
selectedItemIds.filter((selectedItemId) => selectedItemId !== itemId)
|
||||
)
|
||||
} else {
|
||||
setSelectedIndices((selectedIndices) => [...selectedIndices, itemIndex])
|
||||
setSelectedItemIds((selectedIndices) => [...selectedIndices, itemId])
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = () =>
|
||||
props.onSubmit({
|
||||
value: selectedIndices()
|
||||
.map((itemIndex) => props.items[itemIndex].content)
|
||||
value: selectedItemIds()
|
||||
.map(
|
||||
(selectedItemId) =>
|
||||
props.defaultItems.find((item) => item.id === selectedItemId)
|
||||
?.content
|
||||
)
|
||||
.join(', '),
|
||||
})
|
||||
|
||||
const filterItems = (inputValue: string) => {
|
||||
setFilteredItems(
|
||||
props.defaultItems.filter((item) =>
|
||||
item.content?.toLowerCase().includes((inputValue ?? '').toLowerCase())
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<form class="flex flex-col items-end gap-2" onSubmit={handleSubmit}>
|
||||
<div class="flex flex-wrap justify-end gap-2">
|
||||
<For each={props.items}>
|
||||
{(item, index) => (
|
||||
<form class="flex flex-col items-end gap-2 w-full" onSubmit={handleSubmit}>
|
||||
<Show when={props.options.isSearchable}>
|
||||
<div class="flex items-end typebot-input w-full">
|
||||
<SearchInput
|
||||
ref={inputRef}
|
||||
onInput={filterItems}
|
||||
placeholder={
|
||||
props.options.searchInputPlaceholder ??
|
||||
defaultChoiceInputOptions.searchInputPlaceholder
|
||||
}
|
||||
onClear={() => setFilteredItems(props.defaultItems)}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
<div
|
||||
class={
|
||||
'flex flex-wrap justify-end gap-2' +
|
||||
(props.options.isSearchable
|
||||
? ' overflow-y-scroll max-h-80 rounded-md hide-scrollbar'
|
||||
: '')
|
||||
}
|
||||
>
|
||||
<For each={filteredItems()}>
|
||||
{(item) => (
|
||||
<span class={'relative' + (isMobile() ? ' w-full' : '')}>
|
||||
<div
|
||||
role="checkbox"
|
||||
aria-checked={selectedIndices().some(
|
||||
(selectedIndex) => selectedIndex === index()
|
||||
aria-checked={selectedItemIds().some(
|
||||
(selectedItemId) => selectedItemId === item.id
|
||||
)}
|
||||
on:click={() => handleClick(index())}
|
||||
on:click={() => handleClick(item.id)}
|
||||
class={
|
||||
'w-full py-2 px-4 font-semibold focus:outline-none cursor-pointer select-none typebot-selectable' +
|
||||
(selectedIndices().some(
|
||||
(selectedIndex) => selectedIndex === index()
|
||||
(selectedItemIds().some(
|
||||
(selectedItemId) => selectedItemId === item.id
|
||||
)
|
||||
? ' selected'
|
||||
: '')
|
||||
@ -61,8 +101,8 @@ export const MultipleChoicesForm = (props: Props) => {
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<Checkbox
|
||||
isChecked={selectedIndices().some(
|
||||
(selectedIndex) => selectedIndex === index()
|
||||
isChecked={selectedItemIds().some(
|
||||
(selectedItemId) => selectedItemId === item.id
|
||||
)}
|
||||
/>
|
||||
<span>{item.content}</span>
|
||||
@ -71,8 +111,38 @@ export const MultipleChoicesForm = (props: Props) => {
|
||||
</span>
|
||||
)}
|
||||
</For>
|
||||
<For
|
||||
each={selectedItemIds().filter((selectedItemId) =>
|
||||
filteredItems().every((item) => item.id !== selectedItemId)
|
||||
)}
|
||||
>
|
||||
{(selectedItemId) => (
|
||||
<span class={'relative' + (isMobile() ? ' w-full' : '')}>
|
||||
<div
|
||||
role="checkbox"
|
||||
aria-checked
|
||||
on:click={() => handleClick(selectedItemId)}
|
||||
class={
|
||||
'w-full py-2 px-4 font-semibold focus:outline-none cursor-pointer select-none typebot-selectable selected'
|
||||
}
|
||||
data-itemid={selectedItemId}
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<Checkbox isChecked />
|
||||
<span>
|
||||
{
|
||||
props.defaultItems.find(
|
||||
(item) => item.id === selectedItemId
|
||||
)?.content
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
{selectedIndices().length > 0 && (
|
||||
{selectedItemIds().length > 0 && (
|
||||
<SendButton disableIcon>
|
||||
{props.options?.buttonLabel ?? 'Send'}
|
||||
</SendButton>
|
||||
|
@ -1,68 +0,0 @@
|
||||
import { Button } from '@/components/Button'
|
||||
import { SearchInput } from '@/components/inputs/SearchInput'
|
||||
import { InputSubmitContent } from '@/types'
|
||||
import { isMobile } from '@/utils/isMobileSignal'
|
||||
import type { ChoiceInputBlock } from '@typebot.io/schemas'
|
||||
import { For, createSignal, onMount } from 'solid-js'
|
||||
|
||||
type Props = {
|
||||
inputIndex: number
|
||||
defaultItems: ChoiceInputBlock['items']
|
||||
onSubmit: (value: InputSubmitContent) => void
|
||||
}
|
||||
|
||||
export const SearchableButtons = (props: Props) => {
|
||||
let inputRef: HTMLInputElement | undefined
|
||||
const [filteredItems, setFilteredItems] = createSignal(props.defaultItems)
|
||||
|
||||
onMount(() => {
|
||||
if (!isMobile() && inputRef) inputRef.focus()
|
||||
})
|
||||
|
||||
// eslint-disable-next-line solid/reactivity
|
||||
const handleClick = (itemIndex: number) => () =>
|
||||
props.onSubmit({ value: filteredItems()[itemIndex].content ?? '' })
|
||||
|
||||
const filterItems = (inputValue: string) => {
|
||||
setFilteredItems(
|
||||
props.defaultItems.filter((item) =>
|
||||
item.content?.toLowerCase().includes((inputValue ?? '').toLowerCase())
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="flex flex-col gap-2 w-full">
|
||||
<div class="flex items-end typebot-input w-full">
|
||||
<SearchInput
|
||||
ref={inputRef}
|
||||
onInput={filterItems}
|
||||
placeholder="Filter the options..."
|
||||
onClear={() => setFilteredItems(props.defaultItems)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap justify-end gap-2 overflow-y-scroll max-h-80 rounded-md hide-scrollbar">
|
||||
<For each={filteredItems()}>
|
||||
{(item, index) => (
|
||||
<span class={'relative' + (isMobile() ? ' w-full' : '')}>
|
||||
<Button
|
||||
on:click={handleClick(index())}
|
||||
data-itemid={item.id}
|
||||
class="w-full"
|
||||
>
|
||||
{item.content}
|
||||
</Button>
|
||||
{props.inputIndex === 0 && props.defaultItems.length === 1 && (
|
||||
<span class="flex h-3 w-3 absolute top-0 right-0 -mt-1 -mr-1 ping">
|
||||
<span class="animate-ping absolute inline-flex h-full w-full rounded-full brightness-200 opacity-75" />
|
||||
<span class="relative inline-flex rounded-full h-3 w-3 brightness-150" />
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,132 +0,0 @@
|
||||
import { SendButton } from '@/components/SendButton'
|
||||
import { InputSubmitContent } from '@/types'
|
||||
import { isMobile } from '@/utils/isMobileSignal'
|
||||
import type { ChoiceInputBlock } from '@typebot.io/schemas'
|
||||
import { createSignal, For } from 'solid-js'
|
||||
import { Checkbox } from './Checkbox'
|
||||
import { SearchInput } from '@/components/inputs/SearchInput'
|
||||
|
||||
type Props = {
|
||||
inputIndex: number
|
||||
defaultItems: ChoiceInputBlock['items']
|
||||
options: ChoiceInputBlock['options']
|
||||
onSubmit: (value: InputSubmitContent) => void
|
||||
}
|
||||
|
||||
export const SearchableMultipleChoicesForm = (props: Props) => {
|
||||
let inputRef: HTMLInputElement | undefined
|
||||
const [filteredItems, setFilteredItems] = createSignal(props.defaultItems)
|
||||
const [selectedItemIds, setSelectedItemIds] = createSignal<string[]>([])
|
||||
|
||||
const handleClick = (itemId: string) => {
|
||||
toggleSelectedItemId(itemId)
|
||||
}
|
||||
|
||||
const toggleSelectedItemId = (itemId: string) => {
|
||||
const existingIndex = selectedItemIds().indexOf(itemId)
|
||||
if (existingIndex !== -1) {
|
||||
setSelectedItemIds((selectedItemIds) =>
|
||||
selectedItemIds.filter((selectedItemId) => selectedItemId !== itemId)
|
||||
)
|
||||
} else {
|
||||
setSelectedItemIds((selectedIndices) => [...selectedIndices, itemId])
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = () =>
|
||||
props.onSubmit({
|
||||
value: props.defaultItems
|
||||
.filter((item) => selectedItemIds().includes(item.id))
|
||||
.map((item) => item.content)
|
||||
.join(', '),
|
||||
})
|
||||
|
||||
const filterItems = (inputValue: string) => {
|
||||
setFilteredItems(
|
||||
props.defaultItems.filter((item) =>
|
||||
item.content?.toLowerCase().includes((inputValue ?? '').toLowerCase())
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<form class="flex flex-col items-end gap-2 w-full" onSubmit={handleSubmit}>
|
||||
<div class="flex items-end typebot-input w-full">
|
||||
<SearchInput
|
||||
ref={inputRef}
|
||||
onInput={filterItems}
|
||||
placeholder="Filter the options..."
|
||||
onClear={() => setFilteredItems(props.defaultItems)}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-wrap justify-end gap-2 overflow-y-scroll max-h-80 rounded-md hide-scrollbar">
|
||||
<For each={filteredItems()}>
|
||||
{(item) => (
|
||||
<span class={'relative' + (isMobile() ? ' w-full' : '')}>
|
||||
<div
|
||||
role="checkbox"
|
||||
aria-checked={selectedItemIds().some(
|
||||
(selectedItemId) => selectedItemId === item.id
|
||||
)}
|
||||
on:click={() => handleClick(item.id)}
|
||||
class={
|
||||
'w-full py-2 px-4 font-semibold focus:outline-none cursor-pointer select-none typebot-selectable' +
|
||||
(selectedItemIds().some(
|
||||
(selectedItemId) => selectedItemId === item.id
|
||||
)
|
||||
? ' selected'
|
||||
: '')
|
||||
}
|
||||
data-itemid={item.id}
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<Checkbox
|
||||
isChecked={selectedItemIds().some(
|
||||
(selectedItemId) => selectedItemId === item.id
|
||||
)}
|
||||
/>
|
||||
<span>{item.content}</span>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
)}
|
||||
</For>
|
||||
<For
|
||||
each={selectedItemIds().filter((selectedItemId) =>
|
||||
filteredItems().every((item) => item.id !== selectedItemId)
|
||||
)}
|
||||
>
|
||||
{(selectedItemId) => (
|
||||
<span class={'relative' + (isMobile() ? ' w-full' : '')}>
|
||||
<div
|
||||
role="checkbox"
|
||||
aria-checked
|
||||
on:click={() => handleClick(selectedItemId)}
|
||||
class={
|
||||
'w-full py-2 px-4 font-semibold focus:outline-none cursor-pointer select-none typebot-selectable selected'
|
||||
}
|
||||
data-itemid={selectedItemId}
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<Checkbox isChecked />
|
||||
<span>
|
||||
{
|
||||
props.defaultItems.find(
|
||||
(item) => item.id === selectedItemId
|
||||
)?.content
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
{selectedItemIds().length > 0 && (
|
||||
<SendButton disableIcon>
|
||||
{props.options?.buttonLabel ?? 'Send'}
|
||||
</SendButton>
|
||||
)}
|
||||
</form>
|
||||
)
|
||||
}
|
@ -0,0 +1,215 @@
|
||||
import { InputSubmitContent } from '@/types'
|
||||
import {
|
||||
PictureChoiceBlock,
|
||||
defaultPictureChoiceOptions,
|
||||
} from '@typebot.io/schemas/features/blocks/inputs/pictureChoice'
|
||||
import { For, Show, createSignal, onMount } from 'solid-js'
|
||||
import { Checkbox } from '../buttons/components/Checkbox'
|
||||
import { SendButton } from '@/components'
|
||||
import { isDefined, isEmpty } from '@typebot.io/lib'
|
||||
import { SearchInput } from '@/components/inputs/SearchInput'
|
||||
import { isMobile } from '@/utils/isMobileSignal'
|
||||
|
||||
type Props = {
|
||||
defaultItems: PictureChoiceBlock['items']
|
||||
options: PictureChoiceBlock['options']
|
||||
onSubmit: (value: InputSubmitContent) => void
|
||||
}
|
||||
|
||||
export const MultiplePictureChoice = (props: Props) => {
|
||||
let inputRef: HTMLInputElement | undefined
|
||||
const [filteredItems, setFilteredItems] = createSignal(props.defaultItems)
|
||||
const [selectedItemIds, setSelectedItemIds] = createSignal<string[]>([])
|
||||
|
||||
onMount(() => {
|
||||
if (!isMobile() && inputRef) inputRef.focus()
|
||||
})
|
||||
|
||||
const handleClick = (itemId: string) => {
|
||||
toggleSelectedItemId(itemId)
|
||||
}
|
||||
|
||||
const toggleSelectedItemId = (itemId: string) => {
|
||||
const existingIndex = selectedItemIds().indexOf(itemId)
|
||||
if (existingIndex !== -1) {
|
||||
setSelectedItemIds((selectedItemIds) =>
|
||||
selectedItemIds.filter((selectedItemId) => selectedItemId !== itemId)
|
||||
)
|
||||
} else {
|
||||
setSelectedItemIds((selectedIndices) => [...selectedIndices, itemId])
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = () =>
|
||||
props.onSubmit({
|
||||
value: selectedItemIds()
|
||||
.map((selectedItemId) => {
|
||||
const item = props.defaultItems.find(
|
||||
(item) => item.id === selectedItemId
|
||||
)
|
||||
return item?.title ?? item?.pictureSrc
|
||||
})
|
||||
.join(', '),
|
||||
})
|
||||
|
||||
const filterItems = (inputValue: string) => {
|
||||
setFilteredItems(
|
||||
props.defaultItems.filter(
|
||||
(item) =>
|
||||
item.title
|
||||
?.toLowerCase()
|
||||
.includes((inputValue ?? '').toLowerCase()) ||
|
||||
item.description
|
||||
?.toLowerCase()
|
||||
.includes((inputValue ?? '').toLowerCase())
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<form class="flex flex-col gap-2 w-full items-end" onSubmit={handleSubmit}>
|
||||
<Show when={props.options.isSearchable}>
|
||||
<div class="flex items-end typebot-input w-full">
|
||||
<SearchInput
|
||||
ref={inputRef}
|
||||
onInput={filterItems}
|
||||
placeholder={
|
||||
props.options.searchInputPlaceholder ??
|
||||
defaultPictureChoiceOptions.searchInputPlaceholder
|
||||
}
|
||||
onClear={() => setFilteredItems(props.defaultItems)}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
<div
|
||||
class={
|
||||
'flex flex-wrap justify-end gap-2' +
|
||||
(props.options.isSearchable
|
||||
? ' overflow-y-scroll max-h-[464px] rounded-md hide-scrollbar'
|
||||
: '')
|
||||
}
|
||||
>
|
||||
<For each={filteredItems()}>
|
||||
{(item, index) => (
|
||||
<div
|
||||
role="checkbox"
|
||||
aria-checked={selectedItemIds().some(
|
||||
(selectedItemId) => selectedItemId === item.id
|
||||
)}
|
||||
on:click={() => handleClick(item.id)}
|
||||
class={
|
||||
'flex flex-col focus:outline-none cursor-pointer select-none typebot-selectable-picture' +
|
||||
(selectedItemIds().some(
|
||||
(selectedItemId) => selectedItemId === item.id
|
||||
)
|
||||
? ' selected'
|
||||
: '')
|
||||
}
|
||||
data-itemid={item.id}
|
||||
>
|
||||
<img
|
||||
src={item.pictureSrc}
|
||||
alt={item.title ?? `Picture ${index() + 1}`}
|
||||
elementtiming={`Picture choice ${index() + 1}`}
|
||||
fetchpriority={'high'}
|
||||
/>
|
||||
<div
|
||||
class={
|
||||
'flex gap-3 py-2 flex-shrink-0' +
|
||||
(isEmpty(item.title) && isEmpty(item.description)
|
||||
? ' justify-center'
|
||||
: ' pl-4')
|
||||
}
|
||||
>
|
||||
<Checkbox
|
||||
isChecked={selectedItemIds().some(
|
||||
(selectedItemId) => selectedItemId === item.id
|
||||
)}
|
||||
class={item.title || item.description ? 'mt-1' : undefined}
|
||||
/>
|
||||
<Show when={item.title || item.description}>
|
||||
<div class="flex flex-col gap-1 ">
|
||||
<Show when={item.title}>
|
||||
<span class="font-semibold">{item.title}</span>
|
||||
</Show>
|
||||
<Show when={item.description}>
|
||||
<span class="text-sm">{item.description}</span>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
<For
|
||||
each={selectedItemIds()
|
||||
.filter((selectedItemId) =>
|
||||
filteredItems().every((item) => item.id !== selectedItemId)
|
||||
)
|
||||
.map((selectedItemId) =>
|
||||
props.defaultItems.find((item) => item.id === selectedItemId)
|
||||
)
|
||||
.filter(isDefined)}
|
||||
>
|
||||
{(selectedItem, index) => (
|
||||
<div
|
||||
role="checkbox"
|
||||
aria-checked
|
||||
on:click={() => handleClick(selectedItem.id)}
|
||||
class={
|
||||
'flex flex-col focus:outline-none cursor-pointer select-none typebot-selectable-picture selected'
|
||||
}
|
||||
data-itemid={selectedItem.id}
|
||||
>
|
||||
<img
|
||||
src={
|
||||
props.defaultItems.find((item) => item.id === selectedItem.id)
|
||||
?.pictureSrc
|
||||
}
|
||||
alt={selectedItem.title ?? `Selected picture ${index() + 1}`}
|
||||
elementtiming={`Selected picture choice ${index() + 1}`}
|
||||
fetchpriority={'high'}
|
||||
/>
|
||||
<div
|
||||
class={
|
||||
'flex gap-3 py-2 flex-shrink-0' +
|
||||
(isEmpty(selectedItem.title) &&
|
||||
isEmpty(selectedItem.description)
|
||||
? ' justify-center'
|
||||
: ' pl-4')
|
||||
}
|
||||
>
|
||||
<Checkbox
|
||||
isChecked={selectedItemIds().some(
|
||||
(selectedItemId) => selectedItemId === selectedItem.id
|
||||
)}
|
||||
class={
|
||||
selectedItem.title || selectedItem.description
|
||||
? 'mt-1'
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<Show when={selectedItem.title || selectedItem.description}>
|
||||
<div class="flex flex-col gap-1 ">
|
||||
<Show when={selectedItem.title}>
|
||||
<span class="font-semibold">{selectedItem.title}</span>
|
||||
</Show>
|
||||
<Show when={selectedItem.description}>
|
||||
<span class="text-sm">{selectedItem.description}</span>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
{selectedItemIds().length > 0 && (
|
||||
<SendButton disableIcon>
|
||||
{props.options?.buttonLabel ??
|
||||
defaultPictureChoiceOptions.buttonLabel}
|
||||
</SendButton>
|
||||
)}
|
||||
</form>
|
||||
)
|
||||
}
|
@ -0,0 +1,93 @@
|
||||
import { SearchInput } from '@/components/inputs/SearchInput'
|
||||
import { InputSubmitContent } from '@/types'
|
||||
import { isMobile } from '@/utils/isMobileSignal'
|
||||
import { PictureChoiceBlock } from '@typebot.io/schemas/features/blocks/inputs/pictureChoice'
|
||||
import { For, Show, createSignal, onMount } from 'solid-js'
|
||||
|
||||
type Props = {
|
||||
defaultItems: PictureChoiceBlock['items']
|
||||
options: PictureChoiceBlock['options']
|
||||
onSubmit: (value: InputSubmitContent) => void
|
||||
}
|
||||
|
||||
export const SinglePictureChoice = (props: Props) => {
|
||||
let inputRef: HTMLInputElement | undefined
|
||||
const [filteredItems, setFilteredItems] = createSignal(props.defaultItems)
|
||||
|
||||
onMount(() => {
|
||||
if (!isMobile() && inputRef) inputRef.focus()
|
||||
})
|
||||
|
||||
// eslint-disable-next-line solid/reactivity
|
||||
const handleClick = (itemIndex: number) => () => {
|
||||
const pictureSrc = filteredItems()[itemIndex].pictureSrc
|
||||
if (!pictureSrc) return
|
||||
return props.onSubmit({
|
||||
value: filteredItems()[itemIndex].title ?? pictureSrc,
|
||||
})
|
||||
}
|
||||
|
||||
const filterItems = (inputValue: string) => {
|
||||
setFilteredItems(
|
||||
props.defaultItems.filter(
|
||||
(item) =>
|
||||
item.title
|
||||
?.toLowerCase()
|
||||
.includes((inputValue ?? '').toLowerCase()) ||
|
||||
item.description
|
||||
?.toLowerCase()
|
||||
.includes((inputValue ?? '').toLowerCase())
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="flex flex-col gap-2 w-full">
|
||||
<Show when={props.options.isSearchable}>
|
||||
<div class="flex items-end typebot-input w-full">
|
||||
<SearchInput
|
||||
ref={inputRef}
|
||||
onInput={filterItems}
|
||||
placeholder={props.options.searchInputPlaceholder ?? ''}
|
||||
onClear={() => setFilteredItems(props.defaultItems)}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
<div
|
||||
class={
|
||||
'gap-2 flex flex-wrap justify-end' +
|
||||
(props.options.isSearchable
|
||||
? ' overflow-y-scroll max-h-[464px] rounded-md hide-scrollbar'
|
||||
: '')
|
||||
}
|
||||
>
|
||||
<For each={filteredItems()}>
|
||||
{(item, index) => (
|
||||
<button
|
||||
// eslint-disable-next-line solid/reactivity
|
||||
on:click={handleClick(index())}
|
||||
data-itemid={item.id}
|
||||
class="flex flex-col typebot-picture-button focus:outline-none filter hover:brightness-90 active:brightness-75"
|
||||
>
|
||||
<img
|
||||
src={item.pictureSrc}
|
||||
alt={item.title ?? `Picture ${index() + 1}`}
|
||||
elementtiming={`Picture choice ${index() + 1}`}
|
||||
fetchpriority={'high'}
|
||||
/>
|
||||
<div
|
||||
class={
|
||||
'flex flex-col gap-1 py-2 flex-shrink-0 px-4 w-full' +
|
||||
(item.description ? ' items-start' : '')
|
||||
}
|
||||
>
|
||||
<span class="font-semibold">{item.title}</span>
|
||||
<span class="text-sm">{item.description}</span>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@typebot.io/react",
|
||||
"version": "0.0.45",
|
||||
"version": "0.0.46",
|
||||
"description": "React library to display typebots on your website",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
|
@ -3,6 +3,7 @@ import {
|
||||
BubbleBlockType,
|
||||
ComparisonOperators,
|
||||
InputBlockType,
|
||||
ItemType,
|
||||
LogicalOperator,
|
||||
LogicBlockType,
|
||||
StartTypebot,
|
||||
@ -292,7 +293,7 @@ export const leadGenerationTypebot: StartTypebot = {
|
||||
{
|
||||
id: 'clckrlksq00113b6sz8naxdwx',
|
||||
blockId: 'clckrlksq00103b6s3exi90al',
|
||||
type: 1,
|
||||
type: ItemType.CONDITION,
|
||||
content: {
|
||||
comparisons: [
|
||||
{
|
||||
@ -323,7 +324,7 @@ export const leadGenerationTypebot: StartTypebot = {
|
||||
{
|
||||
id: 'clckrm1zr001a3b6s1hlfm2jh',
|
||||
blockId: 'clckrm1zr00193b6szpz37plc',
|
||||
type: 1,
|
||||
type: ItemType.CONDITION,
|
||||
content: {
|
||||
comparisons: [
|
||||
{
|
||||
|
Reference in New Issue
Block a user