Lexical: Add media preview
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
parent
b3f9edd41e
commit
41ee08cd14
3 changed files with 52 additions and 21 deletions
|
@ -1,13 +1,13 @@
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { List as ImmutableList } from 'immutable';
|
import { List as ImmutableList } from 'immutable';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
import { spring } from 'react-motion';
|
import { spring } from 'react-motion';
|
||||||
|
|
||||||
import { openModal } from 'soapbox/actions/modals';
|
import { openModal } from 'soapbox/actions/modals';
|
||||||
import Blurhash from 'soapbox/components/blurhash';
|
import Blurhash from 'soapbox/components/blurhash';
|
||||||
import Icon from 'soapbox/components/icon';
|
import Icon from 'soapbox/components/icon';
|
||||||
import IconButton from 'soapbox/components/icon-button';
|
import { IconButton } from 'soapbox/components/ui';
|
||||||
import Motion from 'soapbox/features/ui/util/optional-motion';
|
import Motion from 'soapbox/features/ui/util/optional-motion';
|
||||||
import { useAppDispatch } from 'soapbox/hooks';
|
import { useAppDispatch } from 'soapbox/hooks';
|
||||||
import { Attachment } from 'soapbox/types/entities';
|
import { Attachment } from 'soapbox/types/entities';
|
||||||
|
@ -57,6 +57,7 @@ export const MIMETYPE_ICONS: Record<string, string> = {
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
description: { id: 'upload_form.description', defaultMessage: 'Describe for the visually impaired' },
|
description: { id: 'upload_form.description', defaultMessage: 'Describe for the visually impaired' },
|
||||||
delete: { id: 'upload_form.undo', defaultMessage: 'Delete' },
|
delete: { id: 'upload_form.undo', defaultMessage: 'Delete' },
|
||||||
|
preview: { id: 'upload_form.preview', defaultMessage: 'Preview' },
|
||||||
});
|
});
|
||||||
|
|
||||||
interface IUpload {
|
interface IUpload {
|
||||||
|
@ -159,20 +160,24 @@ const Upload: React.FC<IUpload> = ({
|
||||||
backgroundPosition: typeof x === 'number' && typeof y === 'number' ? `${x}% ${y}%` : undefined }}
|
backgroundPosition: typeof x === 'number' && typeof y === 'number' ? `${x}% ${y}%` : undefined }}
|
||||||
>
|
>
|
||||||
<div className={clsx('compose-form__upload__actions', { active })}>
|
<div className={clsx('compose-form__upload__actions', { active })}>
|
||||||
{onDelete && (
|
|
||||||
<IconButton
|
|
||||||
onClick={handleUndoClick}
|
|
||||||
src={require('@tabler/icons/x.svg')}
|
|
||||||
text={<FormattedMessage id='upload_form.undo' defaultMessage='Delete' />}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Only display the "Preview" button for a valid attachment with a URL */}
|
|
||||||
{(withPreview && mediaType !== 'unknown' && Boolean(media.url)) && (
|
{(withPreview && mediaType !== 'unknown' && Boolean(media.url)) && (
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={handleOpenModal}
|
onClick={handleOpenModal}
|
||||||
src={require('@tabler/icons/zoom-in.svg')}
|
src={require('@tabler/icons/zoom-in.svg')}
|
||||||
text={<FormattedMessage id='upload_form.preview' defaultMessage='Preview' />}
|
theme='dark'
|
||||||
|
className='hover:scale-105 hover:bg-gray-900'
|
||||||
|
iconClassName='h-5 w-5'
|
||||||
|
title={intl.formatMessage(messages.preview)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{onDelete && (
|
||||||
|
<IconButton
|
||||||
|
onClick={handleUndoClick}
|
||||||
|
src={require('@tabler/icons/x.svg')}
|
||||||
|
theme='dark'
|
||||||
|
className='hover:scale-105 hover:bg-gray-900'
|
||||||
|
iconClassName='h-5 w-5'
|
||||||
|
title={intl.formatMessage(messages.delete)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -11,6 +11,7 @@ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext
|
||||||
import { useLexicalNodeSelection } from '@lexical/react/useLexicalNodeSelection';
|
import { useLexicalNodeSelection } from '@lexical/react/useLexicalNodeSelection';
|
||||||
import { mergeRegister } from '@lexical/utils';
|
import { mergeRegister } from '@lexical/utils';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
import { List as ImmutableList } from 'immutable';
|
||||||
import {
|
import {
|
||||||
$getNodeByKey,
|
$getNodeByKey,
|
||||||
$getSelection,
|
$getSelection,
|
||||||
|
@ -28,7 +29,10 @@ import {
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Suspense, useCallback, useEffect, useRef, useState } from 'react';
|
import { Suspense, useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { IconButton } from 'soapbox/components/ui';
|
import { openModal } from 'soapbox/actions/modals';
|
||||||
|
import { HStack, IconButton } from 'soapbox/components/ui';
|
||||||
|
import { useAppDispatch } from 'soapbox/hooks';
|
||||||
|
import { normalizeAttachment } from 'soapbox/normalizers';
|
||||||
|
|
||||||
import { $isImageNode } from './image-node';
|
import { $isImageNode } from './image-node';
|
||||||
|
|
||||||
|
@ -40,6 +44,7 @@ import type {
|
||||||
RangeSelection,
|
RangeSelection,
|
||||||
} from 'lexical';
|
} from 'lexical';
|
||||||
|
|
||||||
|
|
||||||
const imageCache = new Set();
|
const imageCache = new Set();
|
||||||
|
|
||||||
const useSuspenseImage = (src: string) => {
|
const useSuspenseImage = (src: string) => {
|
||||||
|
@ -87,6 +92,8 @@ const ImageComponent = ({
|
||||||
nodeKey: NodeKey
|
nodeKey: NodeKey
|
||||||
src: string
|
src: string
|
||||||
}): JSX.Element => {
|
}): JSX.Element => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const imageRef = useRef<null | HTMLImageElement>(null);
|
const imageRef = useRef<null | HTMLImageElement>(null);
|
||||||
const buttonRef = useRef<HTMLButtonElement | null>(null);
|
const buttonRef = useRef<HTMLButtonElement | null>(null);
|
||||||
const [isSelected, setSelected, clearSelection] =
|
const [isSelected, setSelected, clearSelection] =
|
||||||
|
@ -109,6 +116,16 @@ const ImageComponent = ({
|
||||||
[nodeKey],
|
[nodeKey],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const previewImage = () => {
|
||||||
|
const image = normalizeAttachment({
|
||||||
|
type: 'image',
|
||||||
|
url: src,
|
||||||
|
altText,
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatch(openModal('MEDIA', { media: ImmutableList.of(image), index: 0 }));
|
||||||
|
};
|
||||||
|
|
||||||
const onDelete = useCallback(
|
const onDelete = useCallback(
|
||||||
(payload: KeyboardEvent) => {
|
(payload: KeyboardEvent) => {
|
||||||
if (isSelected && $isNodeSelection($getSelection())) {
|
if (isSelected && $isNodeSelection($getSelection())) {
|
||||||
|
@ -248,13 +265,22 @@ const ImageComponent = ({
|
||||||
<Suspense fallback={null}>
|
<Suspense fallback={null}>
|
||||||
<>
|
<>
|
||||||
<div className='relative' draggable={draggable}>
|
<div className='relative' draggable={draggable}>
|
||||||
<IconButton
|
<HStack className='absolute right-2 top-2 z-10' space={2}>
|
||||||
onClick={deleteNode}
|
<IconButton
|
||||||
src={require('@tabler/icons/x.svg')}
|
onClick={previewImage}
|
||||||
theme='dark'
|
src={require('@tabler/icons/zoom-in.svg')}
|
||||||
className='absolute right-2 top-2 z-10 hover:scale-105 hover:bg-gray-900'
|
theme='dark'
|
||||||
iconClassName='h-5 w-5'
|
className='!p-1.5 hover:scale-105 hover:bg-gray-900'
|
||||||
/>
|
iconClassName='h-5 w-5'
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
onClick={deleteNode}
|
||||||
|
src={require('@tabler/icons/x.svg')}
|
||||||
|
theme='dark'
|
||||||
|
className='!p-1.5 hover:scale-105 hover:bg-gray-900'
|
||||||
|
iconClassName='h-5 w-5'
|
||||||
|
/>
|
||||||
|
</HStack>
|
||||||
<LazyImage
|
<LazyImage
|
||||||
className={
|
className={
|
||||||
clsx('cursor-default', {
|
clsx('cursor-default', {
|
||||||
|
|
|
@ -50,7 +50,7 @@
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
&__actions {
|
&__actions {
|
||||||
@apply bg-gradient-to-b from-gray-900/80 via-gray-900/50 to-transparent flex items-start justify-between opacity-0 transition-opacity duration-100 ease-linear;
|
@apply p-2 bg-gradient-to-b from-gray-900/80 via-gray-900/50 to-transparent flex items-start gap-2 justify-end opacity-0 transition-opacity duration-100 ease-linear;
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
@apply opacity-100;
|
@apply opacity-100;
|
||||||
|
|
Loading…
Reference in a new issue