@@ -0,0 +1,208 @@
|
||||
import {message} from '@common/i18n/message';
|
||||
import {BgSelectorTabProps} from '@common/background-selector/bg-selector-tab-props';
|
||||
import {
|
||||
BackgroundSelectorConfig,
|
||||
EditableBackgroundProps,
|
||||
} from '@common/background-selector/background-selector-config';
|
||||
import {Trans} from '@common/i18n/trans';
|
||||
import {RadioGroup} from '@common/ui/forms/radio-group/radio-group';
|
||||
import {Radio} from '@common/ui/forms/radio-group/radio';
|
||||
import {ButtonBase} from '@common/ui/buttons/button-base';
|
||||
import clsx from 'clsx';
|
||||
import {SegmentedRadio} from '@common/ui/forms/segmented-radio-group/segmented-radio';
|
||||
import {SegmentedRadioGroup} from '@common/ui/forms/segmented-radio-group/segmented-radio-group';
|
||||
|
||||
const repeat = [
|
||||
{
|
||||
value: 'no-repeat',
|
||||
label: message("Don't repeat"),
|
||||
},
|
||||
{
|
||||
value: 'repeat-x',
|
||||
label: message('Horizontal'),
|
||||
},
|
||||
{
|
||||
value: 'repeat-y',
|
||||
label: message('Vertical'),
|
||||
},
|
||||
{
|
||||
value: 'repeat',
|
||||
label: message('Both'),
|
||||
},
|
||||
];
|
||||
|
||||
const size = [
|
||||
{
|
||||
value: 'auto',
|
||||
label: message('Auto'),
|
||||
},
|
||||
{
|
||||
value: 'cover',
|
||||
label: message('Stretch to fit'),
|
||||
},
|
||||
{
|
||||
value: 'contain',
|
||||
label: message('Fit image'),
|
||||
},
|
||||
];
|
||||
|
||||
const position = [
|
||||
'left top',
|
||||
'center top',
|
||||
'right top',
|
||||
'left center',
|
||||
'center center',
|
||||
'right center',
|
||||
'left bottom',
|
||||
'center bottom',
|
||||
'right bottom',
|
||||
];
|
||||
|
||||
export function AdvancedBackgroundPositionSelector({
|
||||
value,
|
||||
onChange,
|
||||
}: BgSelectorTabProps<BackgroundSelectorConfig>) {
|
||||
return (
|
||||
<div className="mt-14 border-t pt-14">
|
||||
<div className="flex gap-60">
|
||||
<RepeatSelector value={value} onChange={onChange} />
|
||||
<SizeSelector value={value} onChange={onChange} />
|
||||
<PositionSelector value={value} onChange={onChange} />
|
||||
</div>
|
||||
<SegmentedRadioGroup
|
||||
size="xs"
|
||||
className="mt-20"
|
||||
value={value?.backgroundAttachment ?? 'scroll'}
|
||||
onChange={newValue => {
|
||||
onChange?.({
|
||||
...value!,
|
||||
backgroundAttachment:
|
||||
newValue as EditableBackgroundProps['backgroundAttachment'],
|
||||
});
|
||||
}}
|
||||
>
|
||||
<SegmentedRadio value="fixed">
|
||||
<Trans message="Fixed" />
|
||||
</SegmentedRadio>
|
||||
<SegmentedRadio value="scroll">
|
||||
<Trans message="Not fixed" />
|
||||
</SegmentedRadio>
|
||||
</SegmentedRadioGroup>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RepeatSelector({
|
||||
value,
|
||||
onChange,
|
||||
}: BgSelectorTabProps<BackgroundSelectorConfig>) {
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-10">
|
||||
<Trans message="Repeat" />
|
||||
</div>
|
||||
<RadioGroup orientation="vertical" size="sm" disabled={!value}>
|
||||
{repeat.map(({value: repeatValue, label}) => (
|
||||
<Radio
|
||||
key={repeatValue}
|
||||
value={repeatValue}
|
||||
checked={value?.backgroundRepeat === repeatValue}
|
||||
onChange={() => {
|
||||
onChange?.({
|
||||
...value!,
|
||||
backgroundRepeat:
|
||||
repeatValue as EditableBackgroundProps['backgroundRepeat'],
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Trans {...label} />
|
||||
</Radio>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SizeSelector({
|
||||
value,
|
||||
onChange,
|
||||
}: BgSelectorTabProps<BackgroundSelectorConfig>) {
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-10">
|
||||
<Trans message="Size" />
|
||||
</div>
|
||||
<RadioGroup orientation="vertical" size="sm" disabled={!value}>
|
||||
{size.map(({value: sizeValue, label}) => (
|
||||
<Radio
|
||||
key={sizeValue}
|
||||
value={sizeValue}
|
||||
checked={value?.backgroundSize === sizeValue}
|
||||
onChange={() => {
|
||||
onChange?.({
|
||||
...value!,
|
||||
backgroundSize:
|
||||
sizeValue as EditableBackgroundProps['backgroundSize'],
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Trans {...label} />
|
||||
</Radio>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PositionSelector({
|
||||
value,
|
||||
onChange,
|
||||
}: BgSelectorTabProps<BackgroundSelectorConfig>) {
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-10">
|
||||
<Trans message="Position" />
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-8">
|
||||
{position.map(position => (
|
||||
<PositionSelectorButton
|
||||
disabled={!value}
|
||||
key={position}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
position={position}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface PositionSelectorButtonProps {
|
||||
value: BackgroundSelectorConfig | undefined;
|
||||
onChange: (value: BackgroundSelectorConfig) => void;
|
||||
position: string;
|
||||
disabled: boolean;
|
||||
}
|
||||
function PositionSelectorButton({
|
||||
value,
|
||||
onChange,
|
||||
position,
|
||||
disabled,
|
||||
}: PositionSelectorButtonProps) {
|
||||
return (
|
||||
<ButtonBase
|
||||
disabled={disabled}
|
||||
onClick={() => {
|
||||
onChange({
|
||||
...value!,
|
||||
backgroundPosition: position,
|
||||
});
|
||||
}}
|
||||
className={clsx(
|
||||
'h-26 w-26 rounded border',
|
||||
value?.backgroundPosition === position ? 'bg-primary' : 'bg-alt',
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
import {DialogTrigger} from '@common/ui/overlays/dialog/dialog-trigger';
|
||||
import {Trans} from '@common/i18n/trans';
|
||||
import {UploadIcon} from '@common/icons/material/Upload';
|
||||
import {useForm} from 'react-hook-form';
|
||||
import {useDialogContext} from '@common/ui/overlays/dialog/dialog-context';
|
||||
import {Dialog} from '@common/ui/overlays/dialog/dialog';
|
||||
import {DialogHeader} from '@common/ui/overlays/dialog/dialog-header';
|
||||
import {DialogBody} from '@common/ui/overlays/dialog/dialog-body';
|
||||
import {Form} from '@common/ui/forms/form';
|
||||
import {FileUploadProvider} from '@common/uploads/uploader/file-upload-provider';
|
||||
import {FormImageSelector} from '@common/ui/images/image-selector';
|
||||
import {DialogFooter} from '@common/ui/overlays/dialog/dialog-footer';
|
||||
import {Button} from '@common/ui/buttons/button';
|
||||
import {
|
||||
BaseImageBg,
|
||||
ImageBackgrounds,
|
||||
} from '@common/background-selector/image-backgrounds';
|
||||
import {BackgroundSelectorButton} from '@common/background-selector/background-selector-button';
|
||||
import {cssPropsFromBgConfig} from '@common/background-selector/css-props-from-bg-config';
|
||||
import {SimpleBackgroundPositionSelector} from '@common/background-selector/image-background-tab/simple-background-position-selector';
|
||||
import {BgSelectorTabProps} from '@common/background-selector/bg-selector-tab-props';
|
||||
import {BackgroundSelectorConfig} from '@common/background-selector/background-selector-config';
|
||||
import {AdvancedBackgroundPositionSelector} from '@common/background-selector/image-background-tab/advanced-background-position-selector';
|
||||
import {urlFromBackgroundImage} from '@common/background-selector/bg-config-from-css-props';
|
||||
|
||||
export function ImageBackgroundTab({
|
||||
value,
|
||||
onChange,
|
||||
className,
|
||||
positionSelector,
|
||||
diskPrefix,
|
||||
isInsideDialog,
|
||||
}: BgSelectorTabProps<BackgroundSelectorConfig>) {
|
||||
return (
|
||||
<div>
|
||||
<div className={className}>
|
||||
<CustomImageTrigger
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
diskPrefix={diskPrefix}
|
||||
hideFooter={isInsideDialog}
|
||||
/>
|
||||
{ImageBackgrounds.map(background => (
|
||||
<BackgroundSelectorButton
|
||||
key={background.id}
|
||||
onClick={() =>
|
||||
onChange?.({
|
||||
...BaseImageBg,
|
||||
...background,
|
||||
})
|
||||
}
|
||||
isActive={value?.id === background.id}
|
||||
style={{
|
||||
...cssPropsFromBgConfig(background),
|
||||
backgroundAttachment: 'initial',
|
||||
}}
|
||||
label={<Trans {...background.label} />}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{positionSelector === 'advanced' ? (
|
||||
<AdvancedBackgroundPositionSelector value={value} onChange={onChange} />
|
||||
) : (
|
||||
<SimpleBackgroundPositionSelector value={value} onChange={onChange} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface CustomImageTrigger {
|
||||
value?: BackgroundSelectorConfig;
|
||||
onChange?: (value: BackgroundSelectorConfig | null) => void;
|
||||
diskPrefix?: string;
|
||||
hideFooter?: boolean;
|
||||
}
|
||||
function CustomImageTrigger({
|
||||
value,
|
||||
onChange,
|
||||
diskPrefix,
|
||||
hideFooter,
|
||||
}: CustomImageTrigger) {
|
||||
// only seed form with custom uploaded image
|
||||
value = value?.id === BaseImageBg.id ? value : undefined;
|
||||
return (
|
||||
<DialogTrigger
|
||||
type="popover"
|
||||
onClose={(imageUrl?: string) => {
|
||||
onChange?.(
|
||||
imageUrl
|
||||
? {
|
||||
...BaseImageBg,
|
||||
backgroundImage: `url(${imageUrl})`,
|
||||
}
|
||||
: null,
|
||||
);
|
||||
}}
|
||||
>
|
||||
<BackgroundSelectorButton
|
||||
label={<Trans {...BaseImageBg.label} />}
|
||||
isActive={
|
||||
value?.id === BaseImageBg.id && value?.backgroundImage !== 'none'
|
||||
}
|
||||
className="border-2 border-dashed"
|
||||
style={cssPropsFromBgConfig(value)}
|
||||
>
|
||||
<span className="inline-block rounded bg-black/20 p-10 text-white">
|
||||
<UploadIcon size="lg" />
|
||||
</span>
|
||||
</BackgroundSelectorButton>
|
||||
<CustomImageDialog
|
||||
value={value}
|
||||
diskPrefix={diskPrefix}
|
||||
hideFooter={hideFooter}
|
||||
/>
|
||||
</DialogTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
interface CustomImageDialogProps {
|
||||
value?: BackgroundSelectorConfig;
|
||||
diskPrefix?: string;
|
||||
hideFooter?: boolean;
|
||||
}
|
||||
export function CustomImageDialog({
|
||||
value,
|
||||
diskPrefix,
|
||||
hideFooter,
|
||||
}: CustomImageDialogProps) {
|
||||
const defaultValue =
|
||||
!value?.backgroundImage || !value.backgroundImage.includes('url(')
|
||||
? undefined
|
||||
: urlFromBackgroundImage(value.backgroundImage);
|
||||
const form = useForm<{imageUrl: string}>({
|
||||
defaultValues: {imageUrl: defaultValue},
|
||||
});
|
||||
const {close, formId} = useDialogContext();
|
||||
return (
|
||||
<Dialog size="sm">
|
||||
<DialogHeader>
|
||||
<Trans message="Upload image" />
|
||||
</DialogHeader>
|
||||
<DialogBody>
|
||||
<Form
|
||||
id={formId}
|
||||
form={form}
|
||||
onSubmit={values => close(values.imageUrl)}
|
||||
>
|
||||
<FileUploadProvider>
|
||||
<FormImageSelector
|
||||
autoFocus
|
||||
name="imageUrl"
|
||||
diskPrefix={diskPrefix || 'biolinks'}
|
||||
showRemoveButton
|
||||
onChange={hideFooter ? imageUrl => close(imageUrl) : undefined}
|
||||
/>
|
||||
</FileUploadProvider>
|
||||
</Form>
|
||||
</DialogBody>
|
||||
{!hideFooter && (
|
||||
<DialogFooter>
|
||||
<Button onClick={() => close()}>
|
||||
<Trans message="Cancel" />
|
||||
</Button>
|
||||
<Button variant="flat" color="primary" type="submit" form={formId}>
|
||||
<Trans message="Select" />
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
)}
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import {RadioGroup} from '@common/ui/forms/radio-group/radio-group';
|
||||
import {Radio} from '@common/ui/forms/radio-group/radio';
|
||||
import {Trans} from '@common/i18n/trans';
|
||||
import {MessageDescriptor} from '@common/i18n/message-descriptor';
|
||||
import {message} from '@common/i18n/message';
|
||||
|
||||
import {BgSelectorTabProps} from '@common/background-selector/bg-selector-tab-props';
|
||||
import {BackgroundSelectorConfig} from '@common/background-selector/background-selector-config';
|
||||
|
||||
const BackgroundPositions: Record<
|
||||
'cover' | 'contain' | 'repeat',
|
||||
{
|
||||
label: MessageDescriptor;
|
||||
bgConfig: Partial<BackgroundSelectorConfig>;
|
||||
}
|
||||
> = {
|
||||
cover: {
|
||||
label: message('Stretch to fit'),
|
||||
bgConfig: {
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundSize: 'cover',
|
||||
},
|
||||
},
|
||||
contain: {
|
||||
label: message('Fit image'),
|
||||
bgConfig: {
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundSize: 'contain',
|
||||
backgroundPosition: 'center top',
|
||||
},
|
||||
},
|
||||
repeat: {
|
||||
label: message('Repeat image'),
|
||||
bgConfig: {
|
||||
backgroundRepeat: 'repeat',
|
||||
backgroundSize: undefined,
|
||||
backgroundPosition: 'left top',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export function SimpleBackgroundPositionSelector({
|
||||
value: imageBgValue,
|
||||
onChange,
|
||||
}: BgSelectorTabProps<BackgroundSelectorConfig>) {
|
||||
const selectedPosition = positionKeyFromValue(imageBgValue);
|
||||
return (
|
||||
<div className="mt-20 border-t pt-14">
|
||||
<RadioGroup size="sm" disabled={!imageBgValue}>
|
||||
{Object.entries(BackgroundPositions).map(([key, position]) => (
|
||||
<Radio
|
||||
key={key}
|
||||
name="background-position"
|
||||
value={key}
|
||||
checked={key === selectedPosition}
|
||||
onChange={e => {
|
||||
if (imageBgValue) {
|
||||
onChange?.({
|
||||
...imageBgValue,
|
||||
...position.bgConfig,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Trans {...position.label} />
|
||||
</Radio>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function positionKeyFromValue(
|
||||
value?: BackgroundSelectorConfig,
|
||||
): keyof typeof BackgroundPositions {
|
||||
if (value?.backgroundSize === 'cover') {
|
||||
return 'cover';
|
||||
} else if (value?.backgroundSize === 'contain') {
|
||||
return 'contain';
|
||||
} else {
|
||||
return 'repeat';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user