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.SuperNoteExportUseMDFrontmatter]: true,
|
||||
[PrefKey.SuperNoteExportPDFPageSize]: 'A4',
|
||||
[PrefKey.SuperNoteImageAlignment]: 'left',
|
||||
[PrefKey.SystemViewPreferences]: {},
|
||||
[PrefKey.AuthenticatorNames]: '',
|
||||
[PrefKey.ComponentPreferences]: {},
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 { 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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -38,7 +38,7 @@ const BlocksEditorTheme: EditorThemeClasses = {
|
||||
},
|
||||
embedBlock: {
|
||||
base: 'Lexical__embedBlock',
|
||||
focus: 'Lexical__embedBlockFocus',
|
||||
focus: 'embedBlockFocused',
|
||||
},
|
||||
hashtag: 'Lexical__hashtag',
|
||||
heading: {
|
||||
|
||||
@@ -253,7 +253,7 @@
|
||||
.Lexical__embedBlock {
|
||||
user-select: none;
|
||||
}
|
||||
.Lexical__embedBlockFocus {
|
||||
.embedBlockFocused {
|
||||
outline: 2px solid var(--sn-stylekit-info-color);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -15,4 +15,5 @@ export const ElementIds = {
|
||||
SearchBar: 'search-bar',
|
||||
ConflictResolutionButton: 'conflict-resolution-button',
|
||||
SuperEditor: 'super-editor',
|
||||
SuperEditorContent: 'super-editor-content',
|
||||
} as const
|
||||
|
||||
Reference in New Issue
Block a user