feat: Added image alignment options and preference in Super notes (#2903)

This commit is contained in:
Aman Harwara
2025-06-16 16:33:27 +05:30
committed by GitHub
parent 57cb8445fd
commit 7bce025efb
19 changed files with 441 additions and 130 deletions

View File

@@ -43,6 +43,7 @@ export const PrefDefaults = {
[PrefKey.SuperNoteExportEmbedBehavior]: 'reference',
[PrefKey.SuperNoteExportUseMDFrontmatter]: true,
[PrefKey.SuperNoteExportPDFPageSize]: 'A4',
[PrefKey.SuperNoteImageAlignment]: 'left',
[PrefKey.SystemViewPreferences]: {},
[PrefKey.AuthenticatorNames]: '',
[PrefKey.ComponentPreferences]: {},

View File

@@ -36,6 +36,7 @@ export enum PrefKey {
SuperNoteExportEmbedBehavior = 'superNoteExportEmbedBehavior',
SuperNoteExportUseMDFrontmatter = 'superNoteExportUseMDFrontmatter',
SuperNoteExportPDFPageSize = 'superNoteExportPDFPageSize',
SuperNoteImageAlignment = 'superNoteImageAlignment',
AuthenticatorNames = 'authenticatorNames',
PaneGesturesEnabled = 'paneGesturesEnabled',
ComponentPreferences = 'componentPreferences',
@@ -101,4 +102,5 @@ export type PrefValue = {
[PrefKey.AddImportsToTag]: boolean
[PrefKey.AlwaysCreateNewTagForImports]: boolean
[PrefKey.ExistingTagForImports]: string | undefined
[PrefKey.SuperNoteImageAlignment]: 'left' | 'center' | 'right'
}

View File

@@ -1,8 +1,8 @@
import { FunctionComponent, MouseEventHandler } from 'react'
import { ComponentPropsWithoutRef, ForwardedRef, forwardRef, MouseEventHandler } from 'react'
import Icon from '@/Components/Icon/Icon'
import { IconType } from '@standardnotes/snjs'
type Props = {
interface Props extends ComponentPropsWithoutRef<'button'> {
onClick: MouseEventHandler<HTMLButtonElement>
className?: string
icon: IconType
@@ -12,32 +12,31 @@ type Props = {
disabled?: boolean
}
const IconButton: FunctionComponent<Props> = ({
onClick,
className = '',
icon,
title,
focusable,
iconClassName = '',
disabled = false,
}) => {
const click: MouseEventHandler<HTMLButtonElement> = (e) => {
e.preventDefault()
onClick(e)
}
const focusableClass = focusable ? '' : 'focus:shadow-none'
return (
<button
type="button"
title={title}
className={`no-border flex cursor-pointer flex-row items-center bg-transparent ${focusableClass} ${className}`}
onClick={click}
disabled={disabled}
aria-label={title}
>
<Icon type={icon} className={iconClassName} />
</button>
)
}
const IconButton = forwardRef(
(
{ onClick, className = '', icon, title, focusable, iconClassName = '', disabled = false, ...rest }: Props,
ref: ForwardedRef<HTMLButtonElement>,
) => {
const click: MouseEventHandler<HTMLButtonElement> = (e) => {
e.preventDefault()
onClick(e)
}
const focusableClass = focusable ? '' : 'focus:shadow-none'
return (
<button
{...rest}
type="button"
title={title}
className={`no-border flex cursor-pointer flex-row items-center bg-transparent ${focusableClass} ${className}`}
onClick={click}
disabled={disabled}
aria-label={title}
ref={ref}
>
<Icon type={icon} className={iconClassName} />
</button>
)
},
)
export default IconButton

View File

@@ -13,15 +13,23 @@ import { isFileTypePreviewable } from './isFilePreviewable'
import PreviewComponent from './PreviewComponent'
import Button from '../Button/Button'
import { ProtectedIllustration } from '@standardnotes/icons'
import { ImageZoomLevelProps } from './ImageZoomLevelProps'
import { OptionalSuperEmbeddedImageProps } from './OptionalSuperEmbeddedImageProps'
type Props = {
application: WebApplication
file: FileItem
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 isFilePreviewable = useMemo(() => {
@@ -137,6 +145,8 @@ const FilePreview = ({ file, application, isEmbeddedInSuper = false, imageZoomLe
isEmbeddedInSuper={isEmbeddedInSuper}
imageZoomLevel={imageZoomLevel}
setImageZoomLevel={setImageZoomLevel}
alignment={alignment}
changeAlignment={changeAlignment}
/>
) : (
<FilePreviewError

View File

@@ -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>
))
}

View File

@@ -1,12 +1,16 @@
import { classNames, IconType } from '@standardnotes/snjs'
import { IconType, PrefKey } from '@standardnotes/snjs'
import { FunctionComponent, useCallback, useEffect, useState } from 'react'
import IconButton from '../Button/IconButton'
import { ImageZoomLevelProps } from './ImageZoomLevelProps'
import IconButton from '@/Components/Button/IconButton'
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 = {
objectUrl: string
isEmbeddedInSuper: boolean
} & ImageZoomLevelProps
} & OptionalSuperEmbeddedImageProps
const MinimumZoomPercent = 10
const DefaultZoomPercent = 100
@@ -19,6 +23,8 @@ const ImagePreview: FunctionComponent<Props> = ({
isEmbeddedInSuper,
imageZoomLevel,
setImageZoomLevel,
alignment,
changeAlignment,
}) => {
const [imageWidth, setImageWidth] = useState(0)
const [imageHeight, setImageHeight] = useState<number>(0)
@@ -48,8 +54,96 @@ const ImagePreview: FunctionComponent<Props> = ({
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 (
<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
className="relative flex h-full w-full items-center justify-center overflow-auto"
style={{
@@ -78,71 +172,37 @@ const ImagePreview: FunctionComponent<Props> = ({
}}
/>
</div>
<div
className={classNames(
isEmbeddedInSuper ? 'hidden focus-within:flex group-hover:flex' : '',
'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',
)}
>
<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={() => {
const newPercent = imageZoomPercent - ZoomPercentModifier
if (newPercent >= ZoomPercentModifier) {
setImageZoom(newPercent)
} else {
setImageZoom(imageZoomPercent)
{!isEmbeddedInSuper && (
<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">
{imageResizer}
</div>
)}
{isEmbeddedInSuper && (
<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 = document.getElementById(ElementIds.SuperEditorContent)
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`)
}
}}
/>
{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 className="flex divide-x divide-border rounded border border-border bg-default">
{changeAlignment && (
<div className="flex items-center gap-1 px-1 py-0.5">
<ImageAlignmentOptions alignment={finalAlignment} changeAlignment={changeAlignment} />
</div>
)}
<div className="flex items-center px-2 py-0.5 text-sm">{imageResizer}</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>
)
}

View File

@@ -1,4 +0,0 @@
export type ImageZoomLevelProps = {
imageZoomLevel?: number
setImageZoomLevel?: (zoomLevel: number) => void
}

View File

@@ -0,0 +1,8 @@
import { ElementFormatType } from 'lexical'
export type OptionalSuperEmbeddedImageProps = {
imageZoomLevel?: number
setImageZoomLevel?: (zoomLevel: number) => void
alignment?: ElementFormatType | null
changeAlignment?: (alignment: ElementFormatType) => void
}

View File

@@ -5,7 +5,7 @@ import { FunctionComponent, useCallback, useEffect, useMemo, useRef } from 'reac
import Button from '../Button/Button'
import { createObjectURLWithRef } from './CreateObjectURLWithRef'
import ImagePreview from './ImagePreview'
import { ImageZoomLevelProps } from './ImageZoomLevelProps'
import { OptionalSuperEmbeddedImageProps } from './OptionalSuperEmbeddedImageProps'
import { PreviewableTextFileTypes, RequiresNativeFilePreview } from './isFilePreviewable'
import TextPreview from './TextPreview'
import { parseFileName, sanitizeFileName } from '@standardnotes/utils'
@@ -16,7 +16,7 @@ type Props = {
file: FileItem
bytes: Uint8Array
isEmbeddedInSuper: boolean
} & ImageZoomLevelProps
} & OptionalSuperEmbeddedImageProps
const PreviewComponent: FunctionComponent<Props> = ({
application,
@@ -25,6 +25,8 @@ const PreviewComponent: FunctionComponent<Props> = ({
isEmbeddedInSuper,
imageZoomLevel,
setImageZoomLevel,
alignment,
changeAlignment,
}) => {
const objectUrlRef = useRef<string>()
@@ -84,6 +86,8 @@ const PreviewComponent: FunctionComponent<Props> = ({
isEmbeddedInSuper={isEmbeddedInSuper}
imageZoomLevel={imageZoomLevel}
setImageZoomLevel={setImageZoomLevel}
alignment={alignment}
changeAlignment={changeAlignment}
/>
)
}

View File

@@ -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 { WebApplication } from '@/Application/WebApplication'
import { FunctionComponent, useState } from 'react'
import { FunctionComponent, useMemo, useState } from 'react'
import HorizontalSeparator from '@/Components/Shared/HorizontalSeparator'
import Switch from '@/Components/Switch/Switch'
import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup'
import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment'
import usePreference from '@/Hooks/usePreference'
import Dropdown from '@/Components/Dropdown/Dropdown'
import { DropdownItem } from '@/Components/Dropdown/DropdownItem'
import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery'
type Props = {
@@ -27,6 +29,27 @@ const Defaults: FunctionComponent<Props> = ({ application }) => {
const addNoteToParentFolders = usePreference(PrefKey.NoteAddToParentFolders)
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 = () => {
application.toggleGlobalSpellcheck().catch(console.error)
@@ -79,23 +102,46 @@ const Defaults: FunctionComponent<Props> = ({ application }) => {
</div>
<HorizontalSeparator classes="my-4" />
{!isMobile && (
<div className="flex justify-between gap-2 md:items-center">
<div className="flex flex-col">
<Subtitle>Use always-visible toolbar in Super notes</Subtitle>
<Text>
When enabled, the Super toolbar will always be shown at the top of the note. It can be temporarily
toggled using Cmd/Ctrl+Shift+K. When disabled, the Super toolbar will only be shown as a floating
toolbar when text is selected.
</Text>
<>
<div className="flex justify-between gap-2 md:items-center">
<div className="flex flex-col">
<Subtitle>Use always-visible toolbar in Super notes</Subtitle>
<Text>
When enabled, the Super toolbar will always be shown at the top of the note. It can be temporarily
toggled using Cmd/Ctrl+Shift+K. When disabled, the Super toolbar will only be shown as a floating
toolbar when text is selected.
</Text>
</div>
<Switch
onChange={() => {
application
.setPreference(PrefKey.AlwaysShowSuperToolbar, !alwaysShowSuperToolbar)
.catch(console.error)
}}
checked={alwaysShowSuperToolbar}
/>
</div>
<Switch
onChange={() => {
application.setPreference(PrefKey.AlwaysShowSuperToolbar, !alwaysShowSuperToolbar).catch(console.error)
<HorizontalSeparator classes="my-4" />
</>
)}
<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>
</PreferencesSegment>
</PreferencesGroup>
)

View File

@@ -38,7 +38,7 @@ const BlocksEditorTheme: EditorThemeClasses = {
},
embedBlock: {
base: 'Lexical__embedBlock',
focus: 'Lexical__embedBlockFocus',
focus: 'embedBlockFocused',
},
hashtag: 'Lexical__hashtag',
heading: {

View File

@@ -253,7 +253,7 @@
.Lexical__embedBlock {
user-select: none;
}
.Lexical__embedBlockFocus {
.embedBlockFocused {
outline: 2px solid var(--sn-stylekit-info-color);
}

View File

@@ -1,6 +1,13 @@
import { BlockWithAlignableContents } from '@lexical/react/LexicalBlockWithAlignableContents'
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 FilePreview from '@/Components/FilePreview/FilePreview'
import { FileItem } from '@standardnotes/snjs'
@@ -16,13 +23,22 @@ export type FileComponentProps = Readonly<{
focus: string
}>
format: ElementFormatType | null
setFormat: (format: ElementFormatType) => void
nodeKey: NodeKey
fileUuid: string
zoomLevel: number
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 [editor] = useLexicalComposerContext()
const [file, setFile] = useState(() => application.items.findItem<FileItem>(fileUuid))
@@ -71,6 +87,19 @@ function FileComponent({ className, format, nodeKey, fileUuid, zoomLevel, setZoo
[editor, setZoomLevel],
)
const changeAlignment = useCallback(
(alignment: ElementFormatType) =>
editor.update(
() => {
setFormat(alignment)
},
{
tag: SKIP_DOM_SELECTION_TAG,
},
),
[editor, setFormat],
)
const [isSelected, setSelected] = useLexicalNodeSelection(nodeKey)
useEffect(() => {
@@ -147,6 +176,8 @@ function FileComponent({ className, format, nodeKey, fileUuid, zoomLevel, setZoo
application={application}
imageZoomLevel={zoomLevel}
setImageZoomLevel={setImageZoomLevel}
alignment={format}
changeAlignment={changeAlignment}
/>
)}
</div>

View File

@@ -94,6 +94,7 @@ export class FileNode extends DecoratorBlockNode implements ItemNodeInterface {
<FileComponent
className={className}
format={this.__format}
setFormat={this.setFormat.bind(this)}
nodeKey={this.getKey()}
fileUuid={this.__id}
zoomLevel={this.__zoomLevel}

View File

@@ -1,5 +1,5 @@
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 { InlineFileNode } from './InlineFileNode'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
@@ -9,6 +9,9 @@ import { $createFileNode } from '../EncryptedFilePlugin/Nodes/FileUtils'
import { isIOS } from '@standardnotes/ui-services'
import Icon from '@/Components/Icon/Icon'
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 = {
fileName: string | undefined
@@ -19,11 +22,12 @@ type Props = {
focus: string
}>
format: ElementFormatType | null
setFormat: (format: ElementFormatType) => void
node: InlineFileNode
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 [editor] = useLexicalComposerContext()
@@ -57,11 +61,49 @@ const InlineFileComponent = ({ className, src, mimeType, fileName, format, node,
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 (
<BlockWithAlignableContents className={className} format={format} nodeKey={nodeKey}>
{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} />
<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>
) : mimeType.startsWith('video') ? (
<video className="h-full w-full" controls autoPlay>

View File

@@ -172,6 +172,7 @@ export class InlineFileNode extends DecoratorBlockNode {
<InlineFileComponent
className={className}
format={this.__format}
setFormat={this.setFormat.bind(this)}
node={this}
nodeKey={this.getKey()}
src={this.__src}

View File

@@ -4,13 +4,16 @@ import Spinner from '@/Components/Spinner/Spinner'
import { isDesktopApplication } from '@/Utils'
import { BlockWithAlignableContents } from '@lexical/react/LexicalBlockWithAlignableContents'
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 { useCallback, useEffect, useRef, useState } from 'react'
import { $createFileNode } from '../EncryptedFilePlugin/Nodes/FileUtils'
import { RemoteImageNode } from './RemoteImageNode'
import { isIOS } from '@standardnotes/ui-services'
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 = {
src: string
@@ -21,10 +24,11 @@ type Props = {
focus: string
}>
format: ElementFormatType | null
setFormat: (format: ElementFormatType) => void
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 [editor] = useLexicalComposerContext()
@@ -91,9 +95,30 @@ const RemoteImageComponent = ({ className, src, alt, node, format, nodeKey }: Pr
)
}, [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 (
<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
alt={alt}
src={src}
@@ -101,6 +126,25 @@ const RemoteImageComponent = ({ className, src, alt, node, format, nodeKey }: Pr
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 && (
<button
className={classNames(

View File

@@ -94,6 +94,7 @@ export class RemoteImageNode extends DecoratorBlockNode {
<RemoteImageComponent
className={className}
format={this.__format}
setFormat={this.setFormat.bind(this)}
nodeKey={this.getKey()}
node={this}
src={this.__src}

View File

@@ -15,4 +15,5 @@ export const ElementIds = {
SearchBar: 'search-bar',
ConflictResolutionButton: 'conflict-resolution-button',
SuperEditor: 'super-editor',
SuperEditorContent: 'super-editor-content',
} as const