Sticker Creator
This commit is contained in:
parent
2df1ba6e61
commit
11d47a8eb9
123 changed files with 11287 additions and 1714 deletions
271
sticker-creator/components/StickerFrame.tsx
Normal file
271
sticker-creator/components/StickerFrame.tsx
Normal file
|
@ -0,0 +1,271 @@
|
|||
import * as React from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { SortableHandle } from 'react-sortable-hoc';
|
||||
import { noop } from 'lodash';
|
||||
import {
|
||||
Manager as PopperManager,
|
||||
Popper,
|
||||
Reference as PopperReference,
|
||||
} from 'react-popper';
|
||||
import { AddEmoji } from '../elements/icons';
|
||||
import { DropZone, Props as DropZoneProps } from '../elements/DropZone';
|
||||
import { StickerPreview } from '../elements/StickerPreview';
|
||||
import * as styles from './StickerFrame.scss';
|
||||
import {
|
||||
EmojiPickDataType,
|
||||
EmojiPicker,
|
||||
Props as EmojiPickerProps,
|
||||
} from '../../ts/components/emoji/EmojiPicker';
|
||||
import { Emoji } from '../../ts/components/emoji/Emoji';
|
||||
import { useI18n } from '../util/i18n';
|
||||
|
||||
export type Mode = 'removable' | 'pick-emoji' | 'add';
|
||||
|
||||
export type Props = Partial<
|
||||
Pick<EmojiPickerProps, 'skinTone' | 'onSetSkinTone'>
|
||||
> &
|
||||
Partial<Pick<DropZoneProps, 'onDrop'>> & {
|
||||
readonly id?: string;
|
||||
readonly emojiData?: EmojiPickDataType;
|
||||
readonly image?: string;
|
||||
readonly mode?: Mode;
|
||||
readonly showGuide?: boolean;
|
||||
onPickEmoji?({ id: string, emoji: EmojiPickData }): unknown;
|
||||
onRemove?(id: string): unknown;
|
||||
};
|
||||
|
||||
const spinnerSvg = (
|
||||
<svg width={56} height={56}>
|
||||
<path d="M52.36 14.185A27.872 27.872 0 0156 28c0 15.464-12.536 28-28 28v-2c14.36 0 26-11.64 26-26 0-4.66-1.226-9.033-3.372-12.815l1.732-1z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
const closeSvg = (
|
||||
<svg
|
||||
viewBox="0 0 16 16"
|
||||
width="16px"
|
||||
height="16px"
|
||||
className={styles.closeButtonIcon}
|
||||
>
|
||||
<path d="M13.4 3.3l-.8-.6L8 7.3 3.3 2.7l-.6.6L7.3 8l-4.6 4.6.6.8L8 8.7l4.6 4.7.8-.8L8.7 8z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
const ImageHandle = SortableHandle((props: { src: string }) => (
|
||||
<img className={styles.image} {...props} alt="Sticker" />
|
||||
));
|
||||
|
||||
export const StickerFrame = React.memo(
|
||||
// tslint:disable-next-line max-func-body-length
|
||||
({
|
||||
id,
|
||||
emojiData,
|
||||
image,
|
||||
showGuide,
|
||||
mode,
|
||||
onRemove,
|
||||
onPickEmoji,
|
||||
skinTone,
|
||||
onSetSkinTone,
|
||||
onDrop,
|
||||
}: Props) => {
|
||||
const i18n = useI18n();
|
||||
const [emojiPickerOpen, setEmojiPickerOpen] = React.useState(false);
|
||||
const [
|
||||
emojiPopperRoot,
|
||||
setEmojiPopperRoot,
|
||||
] = React.useState<HTMLElement | null>(null);
|
||||
const [previewActive, setPreviewActive] = React.useState(false);
|
||||
const [
|
||||
previewPopperRoot,
|
||||
setPreviewPopperRoot,
|
||||
] = React.useState<HTMLElement | null>(null);
|
||||
const timerRef = React.useRef<number>();
|
||||
|
||||
const handleToggleEmojiPicker = React.useCallback(
|
||||
() => {
|
||||
setEmojiPickerOpen(open => !open);
|
||||
},
|
||||
[setEmojiPickerOpen]
|
||||
);
|
||||
|
||||
const handlePickEmoji = React.useCallback(
|
||||
(emoji: EmojiPickDataType) => {
|
||||
onPickEmoji({ id, emoji });
|
||||
setEmojiPickerOpen(false);
|
||||
},
|
||||
[id, onPickEmoji, setEmojiPickerOpen]
|
||||
);
|
||||
|
||||
const handleRemove = React.useCallback(
|
||||
() => {
|
||||
onRemove(id);
|
||||
},
|
||||
[onRemove, id]
|
||||
);
|
||||
|
||||
const handleMouseEnter = React.useCallback(
|
||||
() => {
|
||||
window.clearTimeout(timerRef.current);
|
||||
timerRef.current = window.setTimeout(() => {
|
||||
setPreviewActive(true);
|
||||
}, 500);
|
||||
},
|
||||
[timerRef, setPreviewActive]
|
||||
);
|
||||
|
||||
const handleMouseLeave = React.useCallback(
|
||||
() => {
|
||||
clearTimeout(timerRef.current);
|
||||
setPreviewActive(false);
|
||||
},
|
||||
[timerRef, setPreviewActive]
|
||||
);
|
||||
|
||||
React.useEffect(
|
||||
() => () => {
|
||||
clearTimeout(timerRef.current);
|
||||
},
|
||||
[timerRef]
|
||||
);
|
||||
|
||||
// Create popper root and handle outside clicks
|
||||
React.useEffect(
|
||||
() => {
|
||||
if (emojiPickerOpen) {
|
||||
const root = document.createElement('div');
|
||||
setEmojiPopperRoot(root);
|
||||
document.body.appendChild(root);
|
||||
const handleOutsideClick = ({ target }: MouseEvent) => {
|
||||
if (!root.contains(target as Node)) {
|
||||
setEmojiPickerOpen(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener('click', handleOutsideClick);
|
||||
|
||||
return () => {
|
||||
document.body.removeChild(root);
|
||||
document.removeEventListener('click', handleOutsideClick);
|
||||
};
|
||||
}
|
||||
|
||||
return noop;
|
||||
},
|
||||
[emojiPickerOpen, setEmojiPickerOpen, setEmojiPopperRoot]
|
||||
);
|
||||
|
||||
React.useEffect(
|
||||
() => {
|
||||
if (mode !== 'pick-emoji' && image && previewActive) {
|
||||
const root = document.createElement('div');
|
||||
setPreviewPopperRoot(root);
|
||||
document.body.appendChild(root);
|
||||
|
||||
return () => {
|
||||
document.body.removeChild(root);
|
||||
};
|
||||
}
|
||||
|
||||
return noop;
|
||||
},
|
||||
[mode, image, previewActive, setPreviewPopperRoot]
|
||||
);
|
||||
|
||||
const [dragActive, setDragActive] = React.useState<boolean>(false);
|
||||
const containerClass = dragActive ? styles.dragActive : styles.container;
|
||||
|
||||
return (
|
||||
<PopperManager>
|
||||
<PopperReference>
|
||||
{({ ref: rootRef }) => (
|
||||
<div
|
||||
className={containerClass}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
ref={rootRef}
|
||||
>
|
||||
{mode !== 'add' ? (
|
||||
image ? (
|
||||
<ImageHandle src={image} />
|
||||
) : (
|
||||
<div className={styles.spinner}>{spinnerSvg}</div>
|
||||
)
|
||||
) : null}
|
||||
{showGuide && mode !== 'add' ? (
|
||||
<div className={styles.guide} />
|
||||
) : null}
|
||||
{mode === 'add' && onDrop ? (
|
||||
<DropZone
|
||||
onDrop={onDrop}
|
||||
inner={true}
|
||||
onDragActive={setDragActive}
|
||||
/>
|
||||
) : null}
|
||||
{mode === 'removable' ? (
|
||||
<button className={styles.closeButton} onClick={handleRemove}>
|
||||
{closeSvg}
|
||||
</button>
|
||||
) : null}
|
||||
{mode === 'pick-emoji' ? (
|
||||
<PopperManager>
|
||||
<PopperReference>
|
||||
{({ ref }) => (
|
||||
<button
|
||||
ref={ref}
|
||||
className={styles.emojiButton}
|
||||
onClick={handleToggleEmojiPicker}
|
||||
>
|
||||
{emojiData ? (
|
||||
<Emoji {...emojiData} size={24} />
|
||||
) : (
|
||||
<AddEmoji />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</PopperReference>
|
||||
{emojiPickerOpen && emojiPopperRoot
|
||||
? createPortal(
|
||||
<Popper placement="bottom-start">
|
||||
{({ ref, style }) => (
|
||||
<EmojiPicker
|
||||
ref={ref}
|
||||
style={{ ...style, marginTop: '8px' }}
|
||||
i18n={i18n}
|
||||
onPickEmoji={handlePickEmoji}
|
||||
skinTone={skinTone}
|
||||
onSetSkinTone={onSetSkinTone}
|
||||
onClose={handleToggleEmojiPicker}
|
||||
/>
|
||||
)}
|
||||
</Popper>,
|
||||
emojiPopperRoot
|
||||
)
|
||||
: null}
|
||||
</PopperManager>
|
||||
) : null}
|
||||
{mode !== 'pick-emoji' &&
|
||||
image &&
|
||||
previewActive &&
|
||||
previewPopperRoot
|
||||
? createPortal(
|
||||
<Popper placement="bottom">
|
||||
{({ ref, style, arrowProps, placement }) => (
|
||||
<StickerPreview
|
||||
ref={ref}
|
||||
style={style}
|
||||
image={image}
|
||||
arrowProps={arrowProps}
|
||||
placement={placement}
|
||||
/>
|
||||
)}
|
||||
</Popper>,
|
||||
previewPopperRoot
|
||||
)
|
||||
: null}
|
||||
</div>
|
||||
)}
|
||||
</PopperReference>
|
||||
</PopperManager>
|
||||
);
|
||||
}
|
||||
);
|
Loading…
Add table
Add a link
Reference in a new issue