feat: Added image alignment options and preference in Super notes (#2903)
This commit is contained in:
@@ -43,6 +43,7 @@ export const PrefDefaults = {
|
|||||||
[PrefKey.SuperNoteExportEmbedBehavior]: 'reference',
|
[PrefKey.SuperNoteExportEmbedBehavior]: 'reference',
|
||||||
[PrefKey.SuperNoteExportUseMDFrontmatter]: true,
|
[PrefKey.SuperNoteExportUseMDFrontmatter]: true,
|
||||||
[PrefKey.SuperNoteExportPDFPageSize]: 'A4',
|
[PrefKey.SuperNoteExportPDFPageSize]: 'A4',
|
||||||
|
[PrefKey.SuperNoteImageAlignment]: 'left',
|
||||||
[PrefKey.SystemViewPreferences]: {},
|
[PrefKey.SystemViewPreferences]: {},
|
||||||
[PrefKey.AuthenticatorNames]: '',
|
[PrefKey.AuthenticatorNames]: '',
|
||||||
[PrefKey.ComponentPreferences]: {},
|
[PrefKey.ComponentPreferences]: {},
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ export enum PrefKey {
|
|||||||
SuperNoteExportEmbedBehavior = 'superNoteExportEmbedBehavior',
|
SuperNoteExportEmbedBehavior = 'superNoteExportEmbedBehavior',
|
||||||
SuperNoteExportUseMDFrontmatter = 'superNoteExportUseMDFrontmatter',
|
SuperNoteExportUseMDFrontmatter = 'superNoteExportUseMDFrontmatter',
|
||||||
SuperNoteExportPDFPageSize = 'superNoteExportPDFPageSize',
|
SuperNoteExportPDFPageSize = 'superNoteExportPDFPageSize',
|
||||||
|
SuperNoteImageAlignment = 'superNoteImageAlignment',
|
||||||
AuthenticatorNames = 'authenticatorNames',
|
AuthenticatorNames = 'authenticatorNames',
|
||||||
PaneGesturesEnabled = 'paneGesturesEnabled',
|
PaneGesturesEnabled = 'paneGesturesEnabled',
|
||||||
ComponentPreferences = 'componentPreferences',
|
ComponentPreferences = 'componentPreferences',
|
||||||
@@ -101,4 +102,5 @@ export type PrefValue = {
|
|||||||
[PrefKey.AddImportsToTag]: boolean
|
[PrefKey.AddImportsToTag]: boolean
|
||||||
[PrefKey.AlwaysCreateNewTagForImports]: boolean
|
[PrefKey.AlwaysCreateNewTagForImports]: boolean
|
||||||
[PrefKey.ExistingTagForImports]: string | undefined
|
[PrefKey.ExistingTagForImports]: string | undefined
|
||||||
|
[PrefKey.SuperNoteImageAlignment]: 'left' | 'center' | 'right'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { FunctionComponent, MouseEventHandler } from 'react'
|
import { ComponentPropsWithoutRef, ForwardedRef, forwardRef, MouseEventHandler } from 'react'
|
||||||
import Icon from '@/Components/Icon/Icon'
|
import Icon from '@/Components/Icon/Icon'
|
||||||
import { IconType } from '@standardnotes/snjs'
|
import { IconType } from '@standardnotes/snjs'
|
||||||
|
|
||||||
type Props = {
|
interface Props extends ComponentPropsWithoutRef<'button'> {
|
||||||
onClick: MouseEventHandler<HTMLButtonElement>
|
onClick: MouseEventHandler<HTMLButtonElement>
|
||||||
className?: string
|
className?: string
|
||||||
icon: IconType
|
icon: IconType
|
||||||
@@ -12,32 +12,31 @@ type Props = {
|
|||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const IconButton: FunctionComponent<Props> = ({
|
const IconButton = forwardRef(
|
||||||
onClick,
|
(
|
||||||
className = '',
|
{ onClick, className = '', icon, title, focusable, iconClassName = '', disabled = false, ...rest }: Props,
|
||||||
icon,
|
ref: ForwardedRef<HTMLButtonElement>,
|
||||||
title,
|
) => {
|
||||||
focusable,
|
const click: MouseEventHandler<HTMLButtonElement> = (e) => {
|
||||||
iconClassName = '',
|
e.preventDefault()
|
||||||
disabled = false,
|
onClick(e)
|
||||||
}) => {
|
}
|
||||||
const click: MouseEventHandler<HTMLButtonElement> = (e) => {
|
const focusableClass = focusable ? '' : 'focus:shadow-none'
|
||||||
e.preventDefault()
|
return (
|
||||||
onClick(e)
|
<button
|
||||||
}
|
{...rest}
|
||||||
const focusableClass = focusable ? '' : 'focus:shadow-none'
|
type="button"
|
||||||
return (
|
title={title}
|
||||||
<button
|
className={`no-border flex cursor-pointer flex-row items-center bg-transparent ${focusableClass} ${className}`}
|
||||||
type="button"
|
onClick={click}
|
||||||
title={title}
|
disabled={disabled}
|
||||||
className={`no-border flex cursor-pointer flex-row items-center bg-transparent ${focusableClass} ${className}`}
|
aria-label={title}
|
||||||
onClick={click}
|
ref={ref}
|
||||||
disabled={disabled}
|
>
|
||||||
aria-label={title}
|
<Icon type={icon} className={iconClassName} />
|
||||||
>
|
</button>
|
||||||
<Icon type={icon} className={iconClassName} />
|
)
|
||||||
</button>
|
},
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
export default IconButton
|
export default IconButton
|
||||||
|
|||||||
@@ -13,15 +13,23 @@ import { isFileTypePreviewable } from './isFilePreviewable'
|
|||||||
import PreviewComponent from './PreviewComponent'
|
import PreviewComponent from './PreviewComponent'
|
||||||
import Button from '../Button/Button'
|
import Button from '../Button/Button'
|
||||||
import { ProtectedIllustration } from '@standardnotes/icons'
|
import { ProtectedIllustration } from '@standardnotes/icons'
|
||||||
import { ImageZoomLevelProps } from './ImageZoomLevelProps'
|
import { OptionalSuperEmbeddedImageProps } from './OptionalSuperEmbeddedImageProps'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
application: WebApplication
|
application: WebApplication
|
||||||
file: FileItem
|
file: FileItem
|
||||||
isEmbeddedInSuper?: boolean
|
isEmbeddedInSuper?: boolean
|
||||||
} & ImageZoomLevelProps
|
} & OptionalSuperEmbeddedImageProps
|
||||||
|
|
||||||
const FilePreview = ({ file, application, isEmbeddedInSuper = false, imageZoomLevel, setImageZoomLevel }: Props) => {
|
const FilePreview = ({
|
||||||
|
file,
|
||||||
|
application,
|
||||||
|
isEmbeddedInSuper = false,
|
||||||
|
imageZoomLevel,
|
||||||
|
setImageZoomLevel,
|
||||||
|
alignment,
|
||||||
|
changeAlignment,
|
||||||
|
}: Props) => {
|
||||||
const [isAuthorized, setIsAuthorized] = useState(application.isAuthorizedToRenderItem(file))
|
const [isAuthorized, setIsAuthorized] = useState(application.isAuthorizedToRenderItem(file))
|
||||||
|
|
||||||
const isFilePreviewable = useMemo(() => {
|
const isFilePreviewable = useMemo(() => {
|
||||||
@@ -137,6 +145,8 @@ const FilePreview = ({ file, application, isEmbeddedInSuper = false, imageZoomLe
|
|||||||
isEmbeddedInSuper={isEmbeddedInSuper}
|
isEmbeddedInSuper={isEmbeddedInSuper}
|
||||||
imageZoomLevel={imageZoomLevel}
|
imageZoomLevel={imageZoomLevel}
|
||||||
setImageZoomLevel={setImageZoomLevel}
|
setImageZoomLevel={setImageZoomLevel}
|
||||||
|
alignment={alignment}
|
||||||
|
changeAlignment={changeAlignment}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<FilePreviewError
|
<FilePreviewError
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { classNames, IconType } from '@standardnotes/snjs'
|
||||||
|
import IconButton from '@/Components/Button/IconButton'
|
||||||
|
import StyledTooltip from '@/Components/StyledTooltip/StyledTooltip'
|
||||||
|
import { ElementFormatType } from 'lexical'
|
||||||
|
|
||||||
|
export function getCSSValueFromAlignment(format: ElementFormatType) {
|
||||||
|
switch (format) {
|
||||||
|
case 'start':
|
||||||
|
case 'left':
|
||||||
|
return 'start'
|
||||||
|
case 'right':
|
||||||
|
case 'end':
|
||||||
|
return 'end'
|
||||||
|
default:
|
||||||
|
return 'center'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const Options = [
|
||||||
|
{
|
||||||
|
alignment: 'left',
|
||||||
|
label: 'Left align',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
alignment: 'center',
|
||||||
|
label: 'Center align',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
alignment: 'right',
|
||||||
|
label: 'Right align',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export function ImageAlignmentOptions({
|
||||||
|
alignment: currentAlignment,
|
||||||
|
changeAlignment,
|
||||||
|
}: {
|
||||||
|
alignment: ElementFormatType
|
||||||
|
changeAlignment: (format: ElementFormatType) => void
|
||||||
|
}) {
|
||||||
|
return Options.map(({ alignment, label }) => (
|
||||||
|
<StyledTooltip label={label} key={alignment}>
|
||||||
|
<IconButton
|
||||||
|
className={classNames(
|
||||||
|
alignment === currentAlignment && '!bg-info text-info-contrast',
|
||||||
|
'rounded p-1 hover:bg-contrast',
|
||||||
|
)}
|
||||||
|
icon={`format-align-${alignment}` as IconType}
|
||||||
|
title={label}
|
||||||
|
focusable={true}
|
||||||
|
onClick={(e) => {
|
||||||
|
// the preventDefault and stopPropagation for these events are required
|
||||||
|
// so that the keyboard doesn't jump when you select another option
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
changeAlignment(alignment as ElementFormatType)
|
||||||
|
}}
|
||||||
|
onMouseDown={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</StyledTooltip>
|
||||||
|
))
|
||||||
|
}
|
||||||
@@ -1,12 +1,16 @@
|
|||||||
import { classNames, IconType } from '@standardnotes/snjs'
|
import { IconType, PrefKey } from '@standardnotes/snjs'
|
||||||
import { FunctionComponent, useCallback, useEffect, useState } from 'react'
|
import { FunctionComponent, useCallback, useEffect, useState } from 'react'
|
||||||
import IconButton from '../Button/IconButton'
|
import IconButton from '@/Components/Button/IconButton'
|
||||||
import { ImageZoomLevelProps } from './ImageZoomLevelProps'
|
import { OptionalSuperEmbeddedImageProps } from './OptionalSuperEmbeddedImageProps'
|
||||||
|
import usePreference from '@/Hooks/usePreference'
|
||||||
|
import { getCSSValueFromAlignment, ImageAlignmentOptions } from './ImageAlignmentOptions'
|
||||||
|
import { ElementIds } from '../../Constants/ElementIDs'
|
||||||
|
import { getOverflows } from '@/Components/Popover/Utils/Collisions'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
objectUrl: string
|
objectUrl: string
|
||||||
isEmbeddedInSuper: boolean
|
isEmbeddedInSuper: boolean
|
||||||
} & ImageZoomLevelProps
|
} & OptionalSuperEmbeddedImageProps
|
||||||
|
|
||||||
const MinimumZoomPercent = 10
|
const MinimumZoomPercent = 10
|
||||||
const DefaultZoomPercent = 100
|
const DefaultZoomPercent = 100
|
||||||
@@ -19,6 +23,8 @@ const ImagePreview: FunctionComponent<Props> = ({
|
|||||||
isEmbeddedInSuper,
|
isEmbeddedInSuper,
|
||||||
imageZoomLevel,
|
imageZoomLevel,
|
||||||
setImageZoomLevel,
|
setImageZoomLevel,
|
||||||
|
alignment,
|
||||||
|
changeAlignment,
|
||||||
}) => {
|
}) => {
|
||||||
const [imageWidth, setImageWidth] = useState(0)
|
const [imageWidth, setImageWidth] = useState(0)
|
||||||
const [imageHeight, setImageHeight] = useState<number>(0)
|
const [imageHeight, setImageHeight] = useState<number>(0)
|
||||||
@@ -48,8 +54,96 @@ const ImagePreview: FunctionComponent<Props> = ({
|
|||||||
|
|
||||||
const widthIfEmbedded = imageWidth * (imageZoomPercent / PercentageDivisor)
|
const widthIfEmbedded = imageWidth * (imageZoomPercent / PercentageDivisor)
|
||||||
|
|
||||||
|
const imageResizer = (
|
||||||
|
<>
|
||||||
|
<span className="mr-1.5">{isEmbeddedInSuper ? 'Size' : 'Zoom'}:</span>
|
||||||
|
<IconButton
|
||||||
|
className="rounded p-1 hover:bg-contrast"
|
||||||
|
icon={'subtract' as IconType}
|
||||||
|
title={isEmbeddedInSuper ? 'Decrease size' : 'Zoom Out'}
|
||||||
|
focusable={true}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
const newPercent = imageZoomPercent - ZoomPercentModifier
|
||||||
|
if (newPercent >= ZoomPercentModifier) {
|
||||||
|
setImageZoom(newPercent)
|
||||||
|
} else {
|
||||||
|
setImageZoom(imageZoomPercent)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onMouseDown={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{isZoomInputVisible ? (
|
||||||
|
<div className="mx-2">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className="w-10 bg-default text-center"
|
||||||
|
defaultValue={imageZoomPercent}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
event.stopPropagation()
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
const value = parseInt(event.currentTarget.value)
|
||||||
|
if (value >= MinimumZoomPercent && value <= MaximumZoomPercent) {
|
||||||
|
setImageZoom(value)
|
||||||
|
}
|
||||||
|
setIsZoomInputVisible(false)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onBlur={(event) => {
|
||||||
|
setIsZoomInputVisible(false)
|
||||||
|
const value = parseInt(event.currentTarget.value)
|
||||||
|
if (value >= MinimumZoomPercent && value <= MaximumZoomPercent) {
|
||||||
|
setImageZoom(value)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
%
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
className="mx-1 rounded px-1.5 py-1 hover:bg-contrast"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
setIsZoomInputVisible((visible) => !visible)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{imageZoomPercent}%
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<IconButton
|
||||||
|
className="rounded p-1 hover:bg-contrast"
|
||||||
|
icon="add"
|
||||||
|
title={isEmbeddedInSuper ? 'Increase size' : 'Zoom In'}
|
||||||
|
focusable={true}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
setImageZoom(imageZoomPercent + ZoomPercentModifier)
|
||||||
|
}}
|
||||||
|
onMouseDown={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
|
||||||
|
const defaultSuperImageAlignment = usePreference(PrefKey.SuperNoteImageAlignment)
|
||||||
|
const finalAlignment = alignment || defaultSuperImageAlignment
|
||||||
|
const justifyContent = isEmbeddedInSuper ? getCSSValueFromAlignment(finalAlignment) : 'center'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="group relative flex h-full min-h-0 w-full items-center justify-center">
|
<div
|
||||||
|
className="group relative flex h-full min-h-0 w-full items-center"
|
||||||
|
style={{ justifyContent }}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className="relative flex h-full w-full items-center justify-center overflow-auto"
|
className="relative flex h-full w-full items-center justify-center overflow-auto"
|
||||||
style={{
|
style={{
|
||||||
@@ -78,71 +172,37 @@ const ImagePreview: FunctionComponent<Props> = ({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
{!isEmbeddedInSuper && (
|
||||||
className={classNames(
|
<div className="absolute bottom-6 left-1/2 flex -translate-x-1/2 items-center rounded border border-solid border-border bg-default px-3 py-1">
|
||||||
isEmbeddedInSuper ? 'hidden focus-within:flex group-hover:flex' : '',
|
{imageResizer}
|
||||||
'absolute bottom-6 left-1/2 flex -translate-x-1/2 items-center rounded border border-solid border-border bg-default px-3 py-1',
|
</div>
|
||||||
)}
|
)}
|
||||||
>
|
{isEmbeddedInSuper && (
|
||||||
<span className="mr-1.5">{isEmbeddedInSuper ? 'Size' : 'Zoom'}:</span>
|
<div
|
||||||
<IconButton
|
className="invisible absolute bottom-full left-1/2 z-10 w-max -translate-x-1/2 px-1 pb-1 focus-within:visible group-hover:visible [.embedBlockFocused_&]:visible"
|
||||||
className="rounded p-1 hover:bg-contrast"
|
ref={(popover) => {
|
||||||
icon={'subtract' as IconType}
|
const editorRoot = document.getElementById(ElementIds.SuperEditorContent)
|
||||||
title={isEmbeddedInSuper ? 'Decrease size' : 'Zoom Out'}
|
if (!popover || !editorRoot) {
|
||||||
focusable={true}
|
return
|
||||||
onClick={() => {
|
}
|
||||||
const newPercent = imageZoomPercent - ZoomPercentModifier
|
const editorRootRect = editorRoot.getBoundingClientRect()
|
||||||
if (newPercent >= ZoomPercentModifier) {
|
const popoverRect = popover.getBoundingClientRect()
|
||||||
setImageZoom(newPercent)
|
const overflows = getOverflows(popoverRect, editorRootRect)
|
||||||
} else {
|
if (overflows.top > 0) {
|
||||||
setImageZoom(imageZoomPercent)
|
popover.style.setProperty('--tw-translate-y', `${overflows.top}px`)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
{isZoomInputVisible ? (
|
<div className="flex divide-x divide-border rounded border border-border bg-default">
|
||||||
<div className="mx-2">
|
{changeAlignment && (
|
||||||
<input
|
<div className="flex items-center gap-1 px-1 py-0.5">
|
||||||
type="number"
|
<ImageAlignmentOptions alignment={finalAlignment} changeAlignment={changeAlignment} />
|
||||||
className="w-10 bg-default text-center"
|
</div>
|
||||||
defaultValue={imageZoomPercent}
|
)}
|
||||||
onKeyDown={(event) => {
|
<div className="flex items-center px-2 py-0.5 text-sm">{imageResizer}</div>
|
||||||
event.stopPropagation()
|
|
||||||
if (event.key === 'Enter') {
|
|
||||||
const value = parseInt(event.currentTarget.value)
|
|
||||||
if (value >= MinimumZoomPercent && value <= MaximumZoomPercent) {
|
|
||||||
setImageZoom(value)
|
|
||||||
}
|
|
||||||
setIsZoomInputVisible(false)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onBlur={(event) => {
|
|
||||||
setIsZoomInputVisible(false)
|
|
||||||
const value = parseInt(event.currentTarget.value)
|
|
||||||
if (value >= MinimumZoomPercent && value <= MaximumZoomPercent) {
|
|
||||||
setImageZoom(value)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
%
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
</div>
|
||||||
<button
|
)}
|
||||||
className="mx-1 rounded px-1.5 py-1 hover:bg-contrast"
|
|
||||||
onClick={() => setIsZoomInputVisible((visible) => !visible)}
|
|
||||||
>
|
|
||||||
{imageZoomPercent}%
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<IconButton
|
|
||||||
className="rounded p-1 hover:bg-contrast"
|
|
||||||
icon="add"
|
|
||||||
title={isEmbeddedInSuper ? 'Increase size' : 'Zoom In'}
|
|
||||||
focusable={true}
|
|
||||||
onClick={() => {
|
|
||||||
setImageZoom(imageZoomPercent + ZoomPercentModifier)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
export type ImageZoomLevelProps = {
|
|
||||||
imageZoomLevel?: number
|
|
||||||
setImageZoomLevel?: (zoomLevel: number) => void
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { ElementFormatType } from 'lexical'
|
||||||
|
|
||||||
|
export type OptionalSuperEmbeddedImageProps = {
|
||||||
|
imageZoomLevel?: number
|
||||||
|
setImageZoomLevel?: (zoomLevel: number) => void
|
||||||
|
alignment?: ElementFormatType | null
|
||||||
|
changeAlignment?: (alignment: ElementFormatType) => void
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@ import { FunctionComponent, useCallback, useEffect, useMemo, useRef } from 'reac
|
|||||||
import Button from '../Button/Button'
|
import Button from '../Button/Button'
|
||||||
import { createObjectURLWithRef } from './CreateObjectURLWithRef'
|
import { createObjectURLWithRef } from './CreateObjectURLWithRef'
|
||||||
import ImagePreview from './ImagePreview'
|
import ImagePreview from './ImagePreview'
|
||||||
import { ImageZoomLevelProps } from './ImageZoomLevelProps'
|
import { OptionalSuperEmbeddedImageProps } from './OptionalSuperEmbeddedImageProps'
|
||||||
import { PreviewableTextFileTypes, RequiresNativeFilePreview } from './isFilePreviewable'
|
import { PreviewableTextFileTypes, RequiresNativeFilePreview } from './isFilePreviewable'
|
||||||
import TextPreview from './TextPreview'
|
import TextPreview from './TextPreview'
|
||||||
import { parseFileName, sanitizeFileName } from '@standardnotes/utils'
|
import { parseFileName, sanitizeFileName } from '@standardnotes/utils'
|
||||||
@@ -16,7 +16,7 @@ type Props = {
|
|||||||
file: FileItem
|
file: FileItem
|
||||||
bytes: Uint8Array
|
bytes: Uint8Array
|
||||||
isEmbeddedInSuper: boolean
|
isEmbeddedInSuper: boolean
|
||||||
} & ImageZoomLevelProps
|
} & OptionalSuperEmbeddedImageProps
|
||||||
|
|
||||||
const PreviewComponent: FunctionComponent<Props> = ({
|
const PreviewComponent: FunctionComponent<Props> = ({
|
||||||
application,
|
application,
|
||||||
@@ -25,6 +25,8 @@ const PreviewComponent: FunctionComponent<Props> = ({
|
|||||||
isEmbeddedInSuper,
|
isEmbeddedInSuper,
|
||||||
imageZoomLevel,
|
imageZoomLevel,
|
||||||
setImageZoomLevel,
|
setImageZoomLevel,
|
||||||
|
alignment,
|
||||||
|
changeAlignment,
|
||||||
}) => {
|
}) => {
|
||||||
const objectUrlRef = useRef<string>()
|
const objectUrlRef = useRef<string>()
|
||||||
|
|
||||||
@@ -84,6 +86,8 @@ const PreviewComponent: FunctionComponent<Props> = ({
|
|||||||
isEmbeddedInSuper={isEmbeddedInSuper}
|
isEmbeddedInSuper={isEmbeddedInSuper}
|
||||||
imageZoomLevel={imageZoomLevel}
|
imageZoomLevel={imageZoomLevel}
|
||||||
setImageZoomLevel={setImageZoomLevel}
|
setImageZoomLevel={setImageZoomLevel}
|
||||||
|
alignment={alignment}
|
||||||
|
changeAlignment={changeAlignment}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import { PrefKey, Platform } from '@standardnotes/snjs'
|
import { PrefKey, Platform, PrefValue } from '@standardnotes/snjs'
|
||||||
import { Subtitle, Text, Title } from '@/Components/Preferences/PreferencesComponents/Content'
|
import { Subtitle, Text, Title } from '@/Components/Preferences/PreferencesComponents/Content'
|
||||||
import { WebApplication } from '@/Application/WebApplication'
|
import { WebApplication } from '@/Application/WebApplication'
|
||||||
import { FunctionComponent, useState } from 'react'
|
import { FunctionComponent, useMemo, useState } from 'react'
|
||||||
import HorizontalSeparator from '@/Components/Shared/HorizontalSeparator'
|
import HorizontalSeparator from '@/Components/Shared/HorizontalSeparator'
|
||||||
import Switch from '@/Components/Switch/Switch'
|
import Switch from '@/Components/Switch/Switch'
|
||||||
import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup'
|
import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup'
|
||||||
import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment'
|
import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment'
|
||||||
import usePreference from '@/Hooks/usePreference'
|
import usePreference from '@/Hooks/usePreference'
|
||||||
|
import Dropdown from '@/Components/Dropdown/Dropdown'
|
||||||
|
import { DropdownItem } from '@/Components/Dropdown/DropdownItem'
|
||||||
import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery'
|
import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -27,6 +29,27 @@ const Defaults: FunctionComponent<Props> = ({ application }) => {
|
|||||||
const addNoteToParentFolders = usePreference(PrefKey.NoteAddToParentFolders)
|
const addNoteToParentFolders = usePreference(PrefKey.NoteAddToParentFolders)
|
||||||
|
|
||||||
const alwaysShowSuperToolbar = usePreference(PrefKey.AlwaysShowSuperToolbar)
|
const alwaysShowSuperToolbar = usePreference(PrefKey.AlwaysShowSuperToolbar)
|
||||||
|
const defaultSuperImageAlignment = usePreference(PrefKey.SuperNoteImageAlignment)
|
||||||
|
const imageAlignmentOptions = useMemo(
|
||||||
|
(): DropdownItem[] => [
|
||||||
|
{
|
||||||
|
icon: 'format-align-left',
|
||||||
|
label: 'Left align',
|
||||||
|
value: 'left',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'format-align-center',
|
||||||
|
label: 'Center align',
|
||||||
|
value: 'center',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'format-align-right',
|
||||||
|
label: 'Right align',
|
||||||
|
value: 'right',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
|
||||||
const toggleSpellcheck = () => {
|
const toggleSpellcheck = () => {
|
||||||
application.toggleGlobalSpellcheck().catch(console.error)
|
application.toggleGlobalSpellcheck().catch(console.error)
|
||||||
@@ -79,23 +102,46 @@ const Defaults: FunctionComponent<Props> = ({ application }) => {
|
|||||||
</div>
|
</div>
|
||||||
<HorizontalSeparator classes="my-4" />
|
<HorizontalSeparator classes="my-4" />
|
||||||
{!isMobile && (
|
{!isMobile && (
|
||||||
<div className="flex justify-between gap-2 md:items-center">
|
<>
|
||||||
<div className="flex flex-col">
|
<div className="flex justify-between gap-2 md:items-center">
|
||||||
<Subtitle>Use always-visible toolbar in Super notes</Subtitle>
|
<div className="flex flex-col">
|
||||||
<Text>
|
<Subtitle>Use always-visible toolbar in Super notes</Subtitle>
|
||||||
When enabled, the Super toolbar will always be shown at the top of the note. It can be temporarily
|
<Text>
|
||||||
toggled using Cmd/Ctrl+Shift+K. When disabled, the Super toolbar will only be shown as a floating
|
When enabled, the Super toolbar will always be shown at the top of the note. It can be temporarily
|
||||||
toolbar when text is selected.
|
toggled using Cmd/Ctrl+Shift+K. When disabled, the Super toolbar will only be shown as a floating
|
||||||
</Text>
|
toolbar when text is selected.
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
onChange={() => {
|
||||||
|
application
|
||||||
|
.setPreference(PrefKey.AlwaysShowSuperToolbar, !alwaysShowSuperToolbar)
|
||||||
|
.catch(console.error)
|
||||||
|
}}
|
||||||
|
checked={alwaysShowSuperToolbar}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
<HorizontalSeparator classes="my-4" />
|
||||||
onChange={() => {
|
</>
|
||||||
application.setPreference(PrefKey.AlwaysShowSuperToolbar, !alwaysShowSuperToolbar).catch(console.error)
|
)}
|
||||||
|
<div>
|
||||||
|
<Subtitle>Default image alignment in Super notes</Subtitle>
|
||||||
|
<div className="mt-2">
|
||||||
|
<Dropdown
|
||||||
|
label="Default image alignment in super notes"
|
||||||
|
items={imageAlignmentOptions}
|
||||||
|
value={defaultSuperImageAlignment}
|
||||||
|
onChange={(alignment) => {
|
||||||
|
application
|
||||||
|
.setPreference(
|
||||||
|
PrefKey.SuperNoteImageAlignment,
|
||||||
|
alignment as PrefValue[PrefKey.SuperNoteImageAlignment],
|
||||||
|
)
|
||||||
|
.catch(console.error)
|
||||||
}}
|
}}
|
||||||
checked={alwaysShowSuperToolbar}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</PreferencesSegment>
|
</PreferencesSegment>
|
||||||
</PreferencesGroup>
|
</PreferencesGroup>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ const BlocksEditorTheme: EditorThemeClasses = {
|
|||||||
},
|
},
|
||||||
embedBlock: {
|
embedBlock: {
|
||||||
base: 'Lexical__embedBlock',
|
base: 'Lexical__embedBlock',
|
||||||
focus: 'Lexical__embedBlockFocus',
|
focus: 'embedBlockFocused',
|
||||||
},
|
},
|
||||||
hashtag: 'Lexical__hashtag',
|
hashtag: 'Lexical__hashtag',
|
||||||
heading: {
|
heading: {
|
||||||
|
|||||||
@@ -253,7 +253,7 @@
|
|||||||
.Lexical__embedBlock {
|
.Lexical__embedBlock {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
.Lexical__embedBlockFocus {
|
.embedBlockFocused {
|
||||||
outline: 2px solid var(--sn-stylekit-info-color);
|
outline: 2px solid var(--sn-stylekit-info-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
import { BlockWithAlignableContents } from '@lexical/react/LexicalBlockWithAlignableContents'
|
import { BlockWithAlignableContents } from '@lexical/react/LexicalBlockWithAlignableContents'
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { $getNodeByKey, CLICK_COMMAND, COMMAND_PRIORITY_LOW, ElementFormatType, NodeKey } from 'lexical'
|
import {
|
||||||
|
$getNodeByKey,
|
||||||
|
CLICK_COMMAND,
|
||||||
|
COMMAND_PRIORITY_LOW,
|
||||||
|
ElementFormatType,
|
||||||
|
NodeKey,
|
||||||
|
SKIP_DOM_SELECTION_TAG,
|
||||||
|
} from 'lexical'
|
||||||
import { useApplication } from '@/Components/ApplicationProvider'
|
import { useApplication } from '@/Components/ApplicationProvider'
|
||||||
import FilePreview from '@/Components/FilePreview/FilePreview'
|
import FilePreview from '@/Components/FilePreview/FilePreview'
|
||||||
import { FileItem } from '@standardnotes/snjs'
|
import { FileItem } from '@standardnotes/snjs'
|
||||||
@@ -16,13 +23,22 @@ export type FileComponentProps = Readonly<{
|
|||||||
focus: string
|
focus: string
|
||||||
}>
|
}>
|
||||||
format: ElementFormatType | null
|
format: ElementFormatType | null
|
||||||
|
setFormat: (format: ElementFormatType) => void
|
||||||
nodeKey: NodeKey
|
nodeKey: NodeKey
|
||||||
fileUuid: string
|
fileUuid: string
|
||||||
zoomLevel: number
|
zoomLevel: number
|
||||||
setZoomLevel: (zoomLevel: number) => void
|
setZoomLevel: (zoomLevel: number) => void
|
||||||
}>
|
}>
|
||||||
|
|
||||||
function FileComponent({ className, format, nodeKey, fileUuid, zoomLevel, setZoomLevel }: FileComponentProps) {
|
function FileComponent({
|
||||||
|
className,
|
||||||
|
format,
|
||||||
|
setFormat,
|
||||||
|
nodeKey,
|
||||||
|
fileUuid,
|
||||||
|
zoomLevel,
|
||||||
|
setZoomLevel,
|
||||||
|
}: FileComponentProps) {
|
||||||
const application = useApplication()
|
const application = useApplication()
|
||||||
const [editor] = useLexicalComposerContext()
|
const [editor] = useLexicalComposerContext()
|
||||||
const [file, setFile] = useState(() => application.items.findItem<FileItem>(fileUuid))
|
const [file, setFile] = useState(() => application.items.findItem<FileItem>(fileUuid))
|
||||||
@@ -71,6 +87,19 @@ function FileComponent({ className, format, nodeKey, fileUuid, zoomLevel, setZoo
|
|||||||
[editor, setZoomLevel],
|
[editor, setZoomLevel],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const changeAlignment = useCallback(
|
||||||
|
(alignment: ElementFormatType) =>
|
||||||
|
editor.update(
|
||||||
|
() => {
|
||||||
|
setFormat(alignment)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: SKIP_DOM_SELECTION_TAG,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
[editor, setFormat],
|
||||||
|
)
|
||||||
|
|
||||||
const [isSelected, setSelected] = useLexicalNodeSelection(nodeKey)
|
const [isSelected, setSelected] = useLexicalNodeSelection(nodeKey)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -147,6 +176,8 @@ function FileComponent({ className, format, nodeKey, fileUuid, zoomLevel, setZoo
|
|||||||
application={application}
|
application={application}
|
||||||
imageZoomLevel={zoomLevel}
|
imageZoomLevel={zoomLevel}
|
||||||
setImageZoomLevel={setImageZoomLevel}
|
setImageZoomLevel={setImageZoomLevel}
|
||||||
|
alignment={format}
|
||||||
|
changeAlignment={changeAlignment}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ export class FileNode extends DecoratorBlockNode implements ItemNodeInterface {
|
|||||||
<FileComponent
|
<FileComponent
|
||||||
className={className}
|
className={className}
|
||||||
format={this.__format}
|
format={this.__format}
|
||||||
|
setFormat={this.setFormat.bind(this)}
|
||||||
nodeKey={this.getKey()}
|
nodeKey={this.getKey()}
|
||||||
fileUuid={this.__id}
|
fileUuid={this.__id}
|
||||||
zoomLevel={this.__zoomLevel}
|
zoomLevel={this.__zoomLevel}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { BlockWithAlignableContents } from '@lexical/react/LexicalBlockWithAlignableContents'
|
import { BlockWithAlignableContents } from '@lexical/react/LexicalBlockWithAlignableContents'
|
||||||
import { Platform, classNames } from '@standardnotes/snjs'
|
import { Platform, PrefKey, classNames } from '@standardnotes/snjs'
|
||||||
import { ElementFormatType, NodeKey } from 'lexical'
|
import { ElementFormatType, NodeKey } from 'lexical'
|
||||||
import { InlineFileNode } from './InlineFileNode'
|
import { InlineFileNode } from './InlineFileNode'
|
||||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||||
@@ -9,6 +9,9 @@ import { $createFileNode } from '../EncryptedFilePlugin/Nodes/FileUtils'
|
|||||||
import { isIOS } from '@standardnotes/ui-services'
|
import { isIOS } from '@standardnotes/ui-services'
|
||||||
import Icon from '@/Components/Icon/Icon'
|
import Icon from '@/Components/Icon/Icon'
|
||||||
import Spinner from '@/Components/Spinner/Spinner'
|
import Spinner from '@/Components/Spinner/Spinner'
|
||||||
|
import usePreference from '@/Hooks/usePreference'
|
||||||
|
import { getCSSValueFromAlignment, ImageAlignmentOptions } from '@/Components/FilePreview/ImageAlignmentOptions'
|
||||||
|
import { getOverflows } from '@/Components/Popover/Utils/Collisions'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
fileName: string | undefined
|
fileName: string | undefined
|
||||||
@@ -19,11 +22,12 @@ type Props = {
|
|||||||
focus: string
|
focus: string
|
||||||
}>
|
}>
|
||||||
format: ElementFormatType | null
|
format: ElementFormatType | null
|
||||||
|
setFormat: (format: ElementFormatType) => void
|
||||||
node: InlineFileNode
|
node: InlineFileNode
|
||||||
nodeKey: NodeKey
|
nodeKey: NodeKey
|
||||||
}
|
}
|
||||||
|
|
||||||
const InlineFileComponent = ({ className, src, mimeType, fileName, format, node, nodeKey }: Props) => {
|
const InlineFileComponent = ({ className, src, mimeType, fileName, format, setFormat, node, nodeKey }: Props) => {
|
||||||
const application = useApplication()
|
const application = useApplication()
|
||||||
const [editor] = useLexicalComposerContext()
|
const [editor] = useLexicalComposerContext()
|
||||||
|
|
||||||
@@ -57,11 +61,49 @@ const InlineFileComponent = ({ className, src, mimeType, fileName, format, node,
|
|||||||
|
|
||||||
const isPDF = mimeType === 'application/pdf'
|
const isPDF = mimeType === 'application/pdf'
|
||||||
|
|
||||||
|
const defaultSuperImageAlignment = usePreference(PrefKey.SuperNoteImageAlignment)
|
||||||
|
const finalAlignment = format || defaultSuperImageAlignment
|
||||||
|
const alignItems: 'start' | 'center' | 'end' = getCSSValueFromAlignment(finalAlignment)
|
||||||
|
const changeAlignment = useCallback(
|
||||||
|
(format: ElementFormatType) => {
|
||||||
|
editor.update(() => {
|
||||||
|
setFormat(format)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[editor, setFormat],
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BlockWithAlignableContents className={className} format={format} nodeKey={nodeKey}>
|
<BlockWithAlignableContents className={className} format={format} nodeKey={nodeKey}>
|
||||||
{mimeType.startsWith('image') ? (
|
{mimeType.startsWith('image') ? (
|
||||||
<div className="relative flex min-h-[2rem] flex-col items-center gap-2.5">
|
<div
|
||||||
|
className="group relative flex min-h-[2rem] flex-col gap-2.5"
|
||||||
|
style={{ alignItems }}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
}}
|
||||||
|
>
|
||||||
<img alt={fileName} src={src} />
|
<img alt={fileName} src={src} />
|
||||||
|
<div
|
||||||
|
className="invisible absolute bottom-full left-1/2 z-10 w-max -translate-x-1/2 px-1 pb-1 focus-within:visible group-hover:visible [.embedBlockFocused_&]:visible"
|
||||||
|
ref={(popover) => {
|
||||||
|
const editorRoot = editor.getRootElement()
|
||||||
|
if (!popover || !editorRoot) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const editorRootRect = editorRoot.getBoundingClientRect()
|
||||||
|
const popoverRect = popover.getBoundingClientRect()
|
||||||
|
const overflows = getOverflows(popoverRect, editorRootRect)
|
||||||
|
if (overflows.top > 0) {
|
||||||
|
popover.style.setProperty('--tw-translate-y', `${overflows.top}px`)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex gap-1 rounded border border-border bg-default px-1 py-0.5">
|
||||||
|
<ImageAlignmentOptions alignment={finalAlignment} changeAlignment={changeAlignment} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : mimeType.startsWith('video') ? (
|
) : mimeType.startsWith('video') ? (
|
||||||
<video className="h-full w-full" controls autoPlay>
|
<video className="h-full w-full" controls autoPlay>
|
||||||
|
|||||||
@@ -172,6 +172,7 @@ export class InlineFileNode extends DecoratorBlockNode {
|
|||||||
<InlineFileComponent
|
<InlineFileComponent
|
||||||
className={className}
|
className={className}
|
||||||
format={this.__format}
|
format={this.__format}
|
||||||
|
setFormat={this.setFormat.bind(this)}
|
||||||
node={this}
|
node={this}
|
||||||
nodeKey={this.getKey()}
|
nodeKey={this.getKey()}
|
||||||
src={this.__src}
|
src={this.__src}
|
||||||
|
|||||||
@@ -4,13 +4,16 @@ import Spinner from '@/Components/Spinner/Spinner'
|
|||||||
import { isDesktopApplication } from '@/Utils'
|
import { isDesktopApplication } from '@/Utils'
|
||||||
import { BlockWithAlignableContents } from '@lexical/react/LexicalBlockWithAlignableContents'
|
import { BlockWithAlignableContents } from '@lexical/react/LexicalBlockWithAlignableContents'
|
||||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||||
import { classNames, Platform } from '@standardnotes/snjs'
|
import { classNames, Platform, PrefKey } from '@standardnotes/snjs'
|
||||||
import { $getNodeByKey, CLICK_COMMAND, COMMAND_PRIORITY_LOW, ElementFormatType, NodeKey } from 'lexical'
|
import { $getNodeByKey, CLICK_COMMAND, COMMAND_PRIORITY_LOW, ElementFormatType, NodeKey } from 'lexical'
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
import { $createFileNode } from '../EncryptedFilePlugin/Nodes/FileUtils'
|
import { $createFileNode } from '../EncryptedFilePlugin/Nodes/FileUtils'
|
||||||
import { RemoteImageNode } from './RemoteImageNode'
|
import { RemoteImageNode } from './RemoteImageNode'
|
||||||
import { isIOS } from '@standardnotes/ui-services'
|
import { isIOS } from '@standardnotes/ui-services'
|
||||||
import { useLexicalNodeSelection } from '@lexical/react/useLexicalNodeSelection'
|
import { useLexicalNodeSelection } from '@lexical/react/useLexicalNodeSelection'
|
||||||
|
import usePreference from '@/Hooks/usePreference'
|
||||||
|
import { getCSSValueFromAlignment, ImageAlignmentOptions } from '@/Components/FilePreview/ImageAlignmentOptions'
|
||||||
|
import { getOverflows } from '@/Components/Popover/Utils/Collisions'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
src: string
|
src: string
|
||||||
@@ -21,10 +24,11 @@ type Props = {
|
|||||||
focus: string
|
focus: string
|
||||||
}>
|
}>
|
||||||
format: ElementFormatType | null
|
format: ElementFormatType | null
|
||||||
|
setFormat: (format: ElementFormatType) => void
|
||||||
nodeKey: NodeKey
|
nodeKey: NodeKey
|
||||||
}
|
}
|
||||||
|
|
||||||
const RemoteImageComponent = ({ className, src, alt, node, format, nodeKey }: Props) => {
|
const RemoteImageComponent = ({ className, src, alt, node, format, nodeKey, setFormat }: Props) => {
|
||||||
const application = useApplication()
|
const application = useApplication()
|
||||||
const [editor] = useLexicalComposerContext()
|
const [editor] = useLexicalComposerContext()
|
||||||
|
|
||||||
@@ -91,9 +95,30 @@ const RemoteImageComponent = ({ className, src, alt, node, format, nodeKey }: Pr
|
|||||||
)
|
)
|
||||||
}, [editor, isSelected, nodeKey, setSelected])
|
}, [editor, isSelected, nodeKey, setSelected])
|
||||||
|
|
||||||
|
const changeAlignment = useCallback(
|
||||||
|
(format: ElementFormatType) => {
|
||||||
|
editor.update(() => {
|
||||||
|
setFormat(format)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[editor, setFormat],
|
||||||
|
)
|
||||||
|
|
||||||
|
const defaultSuperImageAlignment = usePreference(PrefKey.SuperNoteImageAlignment)
|
||||||
|
const finalAlignment = format || defaultSuperImageAlignment
|
||||||
|
const alignItems: 'start' | 'center' | 'end' = getCSSValueFromAlignment(finalAlignment)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BlockWithAlignableContents className={className} format={format} nodeKey={nodeKey}>
|
<BlockWithAlignableContents className={className} format={format} nodeKey={nodeKey}>
|
||||||
<div ref={ref} className="relative flex min-h-[2rem] flex-col items-center gap-2.5">
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className="group relative flex min-h-[2rem] flex-col gap-2.5"
|
||||||
|
style={{ alignItems }}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
}}
|
||||||
|
>
|
||||||
<img
|
<img
|
||||||
alt={alt}
|
alt={alt}
|
||||||
src={src}
|
src={src}
|
||||||
@@ -101,6 +126,25 @@ const RemoteImageComponent = ({ className, src, alt, node, format, nodeKey }: Pr
|
|||||||
setDidImageLoad(true)
|
setDidImageLoad(true)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<div
|
||||||
|
className="invisible absolute bottom-full left-1/2 z-10 w-max -translate-x-1/2 px-1 pb-1 focus-within:visible group-hover:visible [.embedBlockFocused_&]:visible"
|
||||||
|
ref={(popover) => {
|
||||||
|
const editorRoot = editor.getRootElement()
|
||||||
|
if (!popover || !editorRoot) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const editorRootRect = editorRoot.getBoundingClientRect()
|
||||||
|
const popoverRect = popover.getBoundingClientRect()
|
||||||
|
const overflows = getOverflows(popoverRect, editorRootRect)
|
||||||
|
if (overflows.top > 0) {
|
||||||
|
popover.style.setProperty('--tw-translate-y', `${overflows.top}px`)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex gap-1 rounded border border-border bg-default px-1 py-0.5">
|
||||||
|
<ImageAlignmentOptions alignment={finalAlignment} changeAlignment={changeAlignment} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{didImageLoad && canShowSaveButton && (
|
{didImageLoad && canShowSaveButton && (
|
||||||
<button
|
<button
|
||||||
className={classNames(
|
className={classNames(
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ export class RemoteImageNode extends DecoratorBlockNode {
|
|||||||
<RemoteImageComponent
|
<RemoteImageComponent
|
||||||
className={className}
|
className={className}
|
||||||
format={this.__format}
|
format={this.__format}
|
||||||
|
setFormat={this.setFormat.bind(this)}
|
||||||
nodeKey={this.getKey()}
|
nodeKey={this.getKey()}
|
||||||
node={this}
|
node={this}
|
||||||
src={this.__src}
|
src={this.__src}
|
||||||
|
|||||||
@@ -15,4 +15,5 @@ export const ElementIds = {
|
|||||||
SearchBar: 'search-bar',
|
SearchBar: 'search-bar',
|
||||||
ConflictResolutionButton: 'conflict-resolution-button',
|
ConflictResolutionButton: 'conflict-resolution-button',
|
||||||
SuperEditor: 'super-editor',
|
SuperEditor: 'super-editor',
|
||||||
|
SuperEditorContent: 'super-editor-content',
|
||||||
} as const
|
} as const
|
||||||
|
|||||||
Reference in New Issue
Block a user