feat: Replaced margin resizers with "Editor width" options. You can set it globally from Preferences > Appearance or per-note from the note options menu (#2324)
This commit is contained in:
@@ -5,6 +5,7 @@ import { DecryptedItem } from '../../Abstract/Item/Implementations/DecryptedItem
|
||||
import { ItemInterface } from '../../Abstract/Item/Interfaces/ItemInterface'
|
||||
import { DecryptedPayloadInterface } from '../../Abstract/Payload/Interfaces/DecryptedPayload'
|
||||
import { NoteContent, NoteContentSpecialized } from './NoteContent'
|
||||
import { EditorLineWidth } from '../UserPrefs'
|
||||
|
||||
export const isNote = (x: ItemInterface): x is SNNote => x.content_type === ContentType.Note
|
||||
|
||||
@@ -15,6 +16,7 @@ export class SNNote extends DecryptedItem<NoteContent> implements NoteContentSpe
|
||||
public readonly preview_plain: string
|
||||
public readonly preview_html: string
|
||||
public readonly spellcheck?: boolean
|
||||
public readonly editorWidth?: EditorLineWidth
|
||||
public readonly noteType?: NoteType
|
||||
public readonly authorizedForListed: boolean
|
||||
|
||||
@@ -30,6 +32,7 @@ export class SNNote extends DecryptedItem<NoteContent> implements NoteContentSpe
|
||||
this.preview_plain = String(this.payload.content.preview_plain || '')
|
||||
this.preview_html = String(this.payload.content.preview_html || '')
|
||||
this.spellcheck = this.payload.content.spellcheck
|
||||
this.editorWidth = this.payload.content.editorWidth
|
||||
this.noteType = this.payload.content.noteType
|
||||
this.editorIdentifier = this.payload.content.editorIdentifier
|
||||
this.authorizedForListed = this.payload.content.authorizedForListed || false
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { FeatureIdentifier, NoteType } from '@standardnotes/features'
|
||||
import { ItemContent } from '../../Abstract/Content/ItemContent'
|
||||
import { EditorLineWidth } from '../UserPrefs'
|
||||
|
||||
export interface NoteContentSpecialized {
|
||||
title: string
|
||||
@@ -8,6 +9,7 @@ export interface NoteContentSpecialized {
|
||||
preview_plain?: string
|
||||
preview_html?: string
|
||||
spellcheck?: boolean
|
||||
editorWidth?: EditorLineWidth
|
||||
noteType?: NoteType
|
||||
editorIdentifier?: FeatureIdentifier | string
|
||||
authorizedForListed?: boolean
|
||||
|
||||
@@ -5,6 +5,7 @@ import { NoteToNoteReference } from '../../Abstract/Reference/NoteToNoteReferenc
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import { ContentReferenceType } from '../../Abstract/Item'
|
||||
import { FeatureIdentifier, NoteType } from '@standardnotes/features'
|
||||
import { EditorLineWidth } from '../UserPrefs'
|
||||
|
||||
export class NoteMutator extends DecryptedItemMutator<NoteContent> {
|
||||
set title(title: string) {
|
||||
@@ -31,6 +32,10 @@ export class NoteMutator extends DecryptedItemMutator<NoteContent> {
|
||||
this.mutableContent.spellcheck = spellcheck
|
||||
}
|
||||
|
||||
set editorWidth(editorWidth: EditorLineWidth) {
|
||||
this.mutableContent.editorWidth = editorWidth
|
||||
}
|
||||
|
||||
set noteType(noteType: NoteType) {
|
||||
this.mutableContent.noteType = noteType
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ export enum PrefKey {
|
||||
EditorSpellcheck = 'spellcheck',
|
||||
EditorResizersEnabled = 'marginResizersEnabled',
|
||||
EditorLineHeight = 'editorLineHeight',
|
||||
EditorLineWidth = 'editorLineWidth',
|
||||
EditorFontSize = 'editorFontSize',
|
||||
SortNotesBy = 'sortBy',
|
||||
SortNotesReverse = 'sortReverse',
|
||||
@@ -65,6 +66,13 @@ export enum EditorLineHeight {
|
||||
Loose = 'Loose',
|
||||
}
|
||||
|
||||
export enum EditorLineWidth {
|
||||
Narrow = 'Narrow',
|
||||
Wide = 'Wide',
|
||||
Dynamic = 'Dynamic',
|
||||
FullWidth = 'FullWidth',
|
||||
}
|
||||
|
||||
export enum EditorFontSize {
|
||||
ExtraSmall = 'ExtraSmall',
|
||||
Small = 'Small',
|
||||
@@ -107,6 +115,7 @@ export type PrefValue = {
|
||||
[PrefKey.NewNoteTitleFormat]: NewNoteTitleFormat
|
||||
[PrefKey.CustomNoteTitleFormat]: string
|
||||
[PrefKey.EditorLineHeight]: EditorLineHeight
|
||||
[PrefKey.EditorLineWidth]: EditorLineWidth
|
||||
[PrefKey.EditorFontSize]: EditorFontSize
|
||||
[PrefKey.UpdateSavingStatusIndicator]: boolean
|
||||
[PrefKey.DarkMode]: boolean
|
||||
|
||||
@@ -37,3 +37,5 @@ export const SUPER_EXPORT_JSON = createKeyboardCommand('SUPER_EXPORT_JSON')
|
||||
export const SUPER_EXPORT_MARKDOWN = createKeyboardCommand('SUPER_EXPORT_MARKDOWN')
|
||||
export const SUPER_EXPORT_HTML = createKeyboardCommand('SUPER_EXPORT_HTML')
|
||||
export const OPEN_PREFERENCES_COMMAND = createKeyboardCommand('OPEN_PREFERENCES_COMMAND')
|
||||
|
||||
export const CHANGE_EDITOR_WIDTH_COMMAND = createKeyboardCommand('CHANGE_EDITOR_WIDTH_COMMAND')
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
SUPER_SEARCH_NEXT_RESULT,
|
||||
SUPER_SEARCH_PREVIOUS_RESULT,
|
||||
SUPER_SEARCH_TOGGLE_REPLACE_MODE,
|
||||
CHANGE_EDITOR_WIDTH_COMMAND,
|
||||
} from './KeyboardCommands'
|
||||
import { KeyboardKey } from './KeyboardKey'
|
||||
import { KeyboardModifier } from './KeyboardModifier'
|
||||
@@ -182,5 +183,11 @@ export function getKeyboardShortcuts(platform: Platform, _environment: Environme
|
||||
modifiers: [primaryModifier],
|
||||
preventDefault: true,
|
||||
},
|
||||
{
|
||||
command: CHANGE_EDITOR_WIDTH_COMMAND,
|
||||
key: 'j',
|
||||
modifiers: [primaryModifier, KeyboardModifier.Shift],
|
||||
preventDefault: true,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ import DotOrgNotice from './DotOrgNotice'
|
||||
import LinkingControllerProvider from '@/Controllers/LinkingControllerProvider'
|
||||
import ImportModal from '../ImportModal/ImportModal'
|
||||
import IosKeyboardClose from '../IosKeyboardClose/IosKeyboardClose'
|
||||
import EditorWidthSelectionModalWrapper from '../EditorWidthSelectionModal/EditorWidthSelectionModal'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
@@ -268,6 +269,7 @@ const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicatio
|
||||
<ToastContainer />
|
||||
<FilePreviewModalWrapper application={application} viewControllerManager={viewControllerManager} />
|
||||
<PermissionsModalWrapper application={application} />
|
||||
<EditorWidthSelectionModalWrapper />
|
||||
<ConfirmDeleteAccountContainer
|
||||
application={application}
|
||||
viewControllerManager={viewControllerManager}
|
||||
|
||||
@@ -4,7 +4,6 @@ import { DropdownItem } from './DropdownItem'
|
||||
import { classNames } from '@standardnotes/snjs'
|
||||
import {
|
||||
Select,
|
||||
SelectArrow,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectPopover,
|
||||
@@ -102,7 +101,7 @@ const Dropdown = ({
|
||||
) : null}
|
||||
<div className="text-base lg:text-sm">{currentItem?.label}</div>
|
||||
</div>
|
||||
<SelectArrow className={classNames('text-passive-1', isExpanded && 'rotate-180')} />
|
||||
<Icon type="chevron-down" size="normal" className={isExpanded ? 'rotate-180' : ''} />
|
||||
</Select>
|
||||
<SelectPopover
|
||||
store={select}
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery'
|
||||
import { classNames, EditorLineWidth, PrefKey, SNNote } from '@standardnotes/snjs'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import Button from '../Button/Button'
|
||||
import Modal, { ModalAction } from '../Modal/Modal'
|
||||
import ModalDialogButtons from '../Modal/ModalDialogButtons'
|
||||
import RadioButtonGroup from '../RadioButtonGroup/RadioButtonGroup'
|
||||
import { EditorMargins, EditorMaxWidths } from './EditorWidths'
|
||||
import { useApplication } from '../ApplicationProvider'
|
||||
import ModalOverlay from '../Modal/ModalOverlay'
|
||||
import { CHANGE_EDITOR_WIDTH_COMMAND, ESCAPE_COMMAND } from '@standardnotes/ui-services'
|
||||
import { PrefDefaults } from '@/Constants/PrefDefaults'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import Switch from '../Switch/Switch'
|
||||
|
||||
const DoubleSidedArrow = ({ className }: { className?: string }) => {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'relative h-[2px] w-full bg-current',
|
||||
'before:absolute before:-left-px before:top-1/2 before:h-0 before:w-0 before:-translate-y-1/2 before:border-r-[6px] before:border-t-[6px] before:border-b-[6px] before:border-current before:border-b-transparent before:border-t-transparent',
|
||||
'after:absolute after:-right-px after:top-1/2 after:h-0 after:w-0 after:-translate-y-1/2 after:border-l-[6px] after:border-t-[6px] after:border-b-[6px] after:border-current after:border-b-transparent after:border-t-transparent',
|
||||
className,
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const EditorWidthSelectionModal = ({
|
||||
initialValue,
|
||||
handleChange,
|
||||
close,
|
||||
note,
|
||||
}: {
|
||||
initialValue: EditorLineWidth
|
||||
handleChange: (value: EditorLineWidth, setGlobally: boolean) => void
|
||||
close: () => void
|
||||
note: SNNote | undefined
|
||||
}) => {
|
||||
const application = useApplication()
|
||||
const isMobileScreen = useMediaQuery(MutuallyExclusiveMediaQueryBreakpoints.sm)
|
||||
|
||||
const [value, setValue] = useState<EditorLineWidth>(() => initialValue)
|
||||
const [setGlobally, setSetGlobally] = useState(false)
|
||||
|
||||
const options = useMemo(
|
||||
() => [
|
||||
{
|
||||
label: 'Narrow',
|
||||
value: EditorLineWidth.Narrow,
|
||||
},
|
||||
{
|
||||
label: 'Wide',
|
||||
value: EditorLineWidth.Wide,
|
||||
},
|
||||
{
|
||||
label: 'Dynamic',
|
||||
value: EditorLineWidth.Dynamic,
|
||||
},
|
||||
{
|
||||
label: 'Full width',
|
||||
value: EditorLineWidth.FullWidth,
|
||||
},
|
||||
],
|
||||
[],
|
||||
)
|
||||
|
||||
const accept = useCallback(() => {
|
||||
handleChange(value, setGlobally)
|
||||
close()
|
||||
}, [close, handleChange, setGlobally, value])
|
||||
|
||||
const actions = useMemo(
|
||||
(): ModalAction[] => [
|
||||
{
|
||||
label: 'Cancel',
|
||||
type: 'cancel',
|
||||
onClick: close,
|
||||
mobileSlot: 'left',
|
||||
},
|
||||
{
|
||||
label: 'Done',
|
||||
type: 'primary',
|
||||
onClick: accept,
|
||||
mobileSlot: 'right',
|
||||
},
|
||||
],
|
||||
[accept, close],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
return application.keyboardService.addCommandHandler({
|
||||
command: ESCAPE_COMMAND,
|
||||
onKeyDown() {
|
||||
close()
|
||||
return
|
||||
},
|
||||
})
|
||||
}, [application.keyboardService, close])
|
||||
|
||||
const DynamicMargin = (
|
||||
<div className="text-center text-sm text-passive-2">
|
||||
<div className={value !== EditorLineWidth.Dynamic ? 'hidden' : ''}>
|
||||
<div className="mb-2">{EditorMargins[value]}</div>
|
||||
<DoubleSidedArrow />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="Set editor width"
|
||||
close={close}
|
||||
customHeader={<></>}
|
||||
customFooter={<></>}
|
||||
disableCustomHeader={isMobileScreen}
|
||||
actions={actions}
|
||||
className={{
|
||||
content: 'select-none md:min-w-[40vw]',
|
||||
description: 'flex min-h-[50vh] flex-col',
|
||||
}}
|
||||
>
|
||||
<div className="flex min-h-0 flex-grow flex-col overflow-hidden rounded bg-passive-5 p-4 pb-0">
|
||||
<div
|
||||
className={classNames(
|
||||
'grid flex-grow grid-cols-[0fr_1fr_0fr] gap-3 rounded rounded-b-none bg-default px-2 pt-4 shadow-[0_1px_4px_rgba(0,0,0,0.12),0_2px_8px_rgba(0,0,0,0.04)] transition-all duration-200 md:px-4',
|
||||
value === EditorLineWidth.Narrow && 'md:grid-cols-[1fr_60%_1fr]',
|
||||
value === EditorLineWidth.Wide && 'md:grid-cols-[1fr_70%_1fr]',
|
||||
value === EditorLineWidth.Dynamic && 'md:grid-cols-[1fr_80%_1fr]',
|
||||
value === EditorLineWidth.FullWidth && 'md:grid-cols-[1fr_95%_1fr]',
|
||||
)}
|
||||
>
|
||||
{DynamicMargin}
|
||||
<div className="flex flex-col text-info">
|
||||
<div className="mb-2 text-center text-sm">
|
||||
{value === EditorLineWidth.Narrow || value === EditorLineWidth.Wide
|
||||
? `Max. ${EditorMaxWidths[value]}`
|
||||
: EditorMaxWidths[value]}
|
||||
</div>
|
||||
<DoubleSidedArrow />
|
||||
<div className="w-full flex-grow bg-[linear-gradient(transparent_50%,var(--sn-stylekit-info-color)_50%)] bg-[length:100%_2.5rem] bg-repeat-y opacity-10" />
|
||||
</div>
|
||||
{DynamicMargin}
|
||||
</div>
|
||||
</div>
|
||||
{!!note && (
|
||||
<div className="border-t border-border bg-default px-4 py-2">
|
||||
<label className="flex items-center gap-2">
|
||||
<Switch checked={setGlobally} onChange={setSetGlobally} />
|
||||
Set globally {note.editorWidth != undefined && '(will not apply to current note)'}
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
<ModalDialogButtons className="justify-center md:justify-between">
|
||||
<RadioButtonGroup items={options} value={value} onChange={(value) => setValue(value as EditorLineWidth)} />
|
||||
<div className="hidden items-center gap-2 md:flex">
|
||||
<Button onClick={close}>Cancel</Button>
|
||||
<Button onClick={accept} primary>
|
||||
Apply
|
||||
</Button>
|
||||
</div>
|
||||
</ModalDialogButtons>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
const EditorWidthSelectionModalWrapper = () => {
|
||||
const application = useApplication()
|
||||
const { notesController } = application.getViewControllerManager()
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [isGlobal, setIsGlobal] = useState(false)
|
||||
|
||||
const note = notesController.selectedNotesCount === 1 && !isGlobal ? notesController.selectedNotes[0] : undefined
|
||||
|
||||
const lineWidth = note
|
||||
? notesController.getEditorWidthForNote(note)
|
||||
: application.getPreference(PrefKey.EditorLineWidth, PrefDefaults[PrefKey.EditorLineWidth])
|
||||
|
||||
const setLineWidth = useCallback(
|
||||
(lineWidth: EditorLineWidth, setGlobally: boolean) => {
|
||||
if (note && !setGlobally) {
|
||||
notesController.setNoteEditorWidth(note, lineWidth).catch(console.error)
|
||||
} else {
|
||||
application.setPreference(PrefKey.EditorLineWidth, lineWidth).catch(console.error)
|
||||
}
|
||||
},
|
||||
[application, note, notesController],
|
||||
)
|
||||
|
||||
const toggle = useCallback(() => {
|
||||
setIsOpen((open) => !open)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
return application.keyboardService.addCommandHandler({
|
||||
command: CHANGE_EDITOR_WIDTH_COMMAND,
|
||||
onKeyDown: (_, data) => {
|
||||
if (typeof data === 'boolean' && data) {
|
||||
setIsGlobal(data)
|
||||
} else {
|
||||
setIsGlobal(false)
|
||||
}
|
||||
toggle()
|
||||
},
|
||||
})
|
||||
}, [application, toggle])
|
||||
|
||||
return (
|
||||
<ModalOverlay isOpen={isOpen}>
|
||||
<EditorWidthSelectionModal initialValue={lineWidth} handleChange={setLineWidth} close={toggle} note={note} />
|
||||
</ModalOverlay>
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(EditorWidthSelectionModalWrapper)
|
||||
@@ -0,0 +1,15 @@
|
||||
import { EditorLineWidth } from '@standardnotes/snjs'
|
||||
|
||||
export const EditorMaxWidths: { [k in EditorLineWidth]: string } = {
|
||||
[EditorLineWidth.Narrow]: '512px',
|
||||
[EditorLineWidth.Wide]: '720px',
|
||||
[EditorLineWidth.Dynamic]: '80%',
|
||||
[EditorLineWidth.FullWidth]: '100%',
|
||||
}
|
||||
|
||||
export const EditorMargins: { [k in EditorLineWidth]: string } = {
|
||||
[EditorLineWidth.Narrow]: 'auto',
|
||||
[EditorLineWidth.Wide]: 'auto',
|
||||
[EditorLineWidth.Dynamic]: '10%',
|
||||
[EditorLineWidth.FullWidth]: '0',
|
||||
}
|
||||
@@ -42,6 +42,7 @@ export const IconNameToSvgMapping = {
|
||||
'hashtag-off': icons.HashtagOffIcon,
|
||||
'keyboard-close': icons.KeyboardCloseIcon,
|
||||
'link-off': icons.LinkOffIcon,
|
||||
'line-width': icons.LineWidthIcon,
|
||||
'list-bulleted': icons.ListBulleted,
|
||||
'list-numbered': icons.ListNumbered,
|
||||
'lock-filled': icons.LockFilledIcon,
|
||||
|
||||
@@ -35,6 +35,7 @@ describe('NoteView', () => {
|
||||
notesController = {} as jest.Mocked<NotesController>
|
||||
notesController.setShowProtectedWarning = jest.fn()
|
||||
notesController.getSpellcheckStateForNote = jest.fn()
|
||||
notesController.getEditorWidthForNote = jest.fn()
|
||||
|
||||
viewControllerManager = {
|
||||
notesController: notesController,
|
||||
|
||||
@@ -2,20 +2,20 @@ import { AbstractComponent } from '@/Components/Abstract/PureComponent'
|
||||
import ChangeEditorButton from '@/Components/ChangeEditor/ChangeEditorButton'
|
||||
import ComponentView from '@/Components/ComponentView/ComponentView'
|
||||
import NotesOptionsPanel from '@/Components/NotesOptions/NotesOptionsPanel'
|
||||
import PanelResizer, { PanelResizeType, PanelSide } from '@/Components/PanelResizer/PanelResizer'
|
||||
import PinNoteButton from '@/Components/PinNoteButton/PinNoteButton'
|
||||
import ProtectedItemOverlay from '@/Components/ProtectedItemOverlay/ProtectedItemOverlay'
|
||||
import { ElementIds } from '@/Constants/ElementIDs'
|
||||
import { PrefDefaults } from '@/Constants/PrefDefaults'
|
||||
import { StringDeleteNote, STRING_DELETE_LOCKED_ATTEMPT, STRING_DELETE_PLACEHOLDER_ATTEMPT } from '@/Constants/Strings'
|
||||
import { log, LoggingDomain } from '@/Logging'
|
||||
import { debounce, isDesktopApplication, isMobileScreen, isTabletOrMobileScreen } from '@/Utils'
|
||||
import { debounce, isDesktopApplication, isMobileScreen } from '@/Utils'
|
||||
import { classNames } from '@standardnotes/utils'
|
||||
import {
|
||||
ApplicationEvent,
|
||||
ComponentArea,
|
||||
ComponentViewerInterface,
|
||||
ContentType,
|
||||
EditorLineWidth,
|
||||
isPayloadSourceInternalChange,
|
||||
isPayloadSourceRetrieved,
|
||||
NoteType,
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
SNNote,
|
||||
} from '@standardnotes/snjs'
|
||||
import { confirmDialog, DELETE_NOTE_KEYBOARD_COMMAND, KeyboardKey } from '@standardnotes/ui-services'
|
||||
import { ChangeEventHandler, createRef, KeyboardEventHandler, RefObject } from 'react'
|
||||
import { ChangeEventHandler, createRef, CSSProperties, KeyboardEventHandler, RefObject } from 'react'
|
||||
import { SuperEditor } from '../SuperEditor/SuperEditor'
|
||||
import IndicatorCircle from '../IndicatorCircle/IndicatorCircle'
|
||||
import LinkedItemBubblesContainer from '../LinkedItems/LinkedItemBubblesContainer'
|
||||
@@ -44,6 +44,7 @@ import {
|
||||
import { SuperEditorContentId } from '../SuperEditor/Constants'
|
||||
import { NoteViewController } from './Controller/NoteViewController'
|
||||
import { PlainEditor, PlainEditorInterface } from './PlainEditor/PlainEditor'
|
||||
import { EditorMargins, EditorMaxWidths } from '../EditorWidthSelectionModal/EditorWidths'
|
||||
|
||||
const MinimumStatusDuration = 400
|
||||
|
||||
@@ -58,7 +59,7 @@ type State = {
|
||||
editorStateDidLoad: boolean
|
||||
editorTitle: string
|
||||
isDesktop?: boolean
|
||||
marginResizersEnabled?: boolean
|
||||
editorLineWidth: EditorLineWidth
|
||||
noteLocked: boolean
|
||||
noteStatus?: NoteStatus
|
||||
saveError?: boolean
|
||||
@@ -68,10 +69,6 @@ type State = {
|
||||
syncTakingTooLong: boolean
|
||||
monospaceFont?: boolean
|
||||
plainEditorFocused?: boolean
|
||||
leftResizerWidth: number
|
||||
leftResizerOffset: number
|
||||
rightResizerWidth: number
|
||||
rightResizerOffset: number
|
||||
|
||||
updateSavingIndicator?: boolean
|
||||
editorFeatureIdentifier?: string
|
||||
@@ -112,6 +109,7 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
||||
availableStackComponents: [],
|
||||
editorStateDidLoad: false,
|
||||
editorTitle: '',
|
||||
editorLineWidth: PrefDefaults[PrefKey.EditorLineWidth],
|
||||
isDesktop: isDesktopApplication(),
|
||||
noteStatus: undefined,
|
||||
noteLocked: this.controller.item.locked,
|
||||
@@ -119,10 +117,6 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
||||
spellcheck: true,
|
||||
stackComponentViewers: [],
|
||||
syncTakingTooLong: false,
|
||||
leftResizerWidth: 0,
|
||||
leftResizerOffset: 0,
|
||||
rightResizerWidth: 0,
|
||||
rightResizerOffset: 0,
|
||||
editorFeatureIdentifier: this.controller.item.editorIdentifier,
|
||||
noteType: this.controller.item.noteType,
|
||||
}
|
||||
@@ -280,6 +274,8 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
||||
|
||||
this.reloadSpellcheck().catch(console.error)
|
||||
|
||||
this.reloadLineWidth()
|
||||
|
||||
const isTemplateNoteInsertedToBeInteractableWithEditor = source === PayloadEmitSource.LocalInserted && note.dirty
|
||||
if (isTemplateNoteInsertedToBeInteractableWithEditor) {
|
||||
return
|
||||
@@ -660,6 +656,14 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
||||
}
|
||||
}
|
||||
|
||||
reloadLineWidth() {
|
||||
const editorLineWidth = this.viewControllerManager.notesController.getEditorWidthForNote(this.note)
|
||||
|
||||
this.setState({
|
||||
editorLineWidth,
|
||||
})
|
||||
}
|
||||
|
||||
async reloadPreferences() {
|
||||
log(LoggingDomain.NoteView, 'Reload preferences')
|
||||
const monospaceFont = this.application.getPreference(
|
||||
@@ -667,10 +671,6 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
||||
PrefDefaults[PrefKey.EditorMonospaceEnabled],
|
||||
)
|
||||
|
||||
const marginResizersEnabled =
|
||||
!isTabletOrMobileScreen() &&
|
||||
this.application.getPreference(PrefKey.EditorResizersEnabled, PrefDefaults[PrefKey.EditorResizersEnabled])
|
||||
|
||||
const updateSavingIndicator = this.application.getPreference(
|
||||
PrefKey.UpdateSavingStatusIndicator,
|
||||
PrefDefaults[PrefKey.UpdateSavingStatusIndicator],
|
||||
@@ -678,30 +678,14 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
||||
|
||||
await this.reloadSpellcheck()
|
||||
|
||||
this.reloadLineWidth()
|
||||
|
||||
this.setState({
|
||||
monospaceFont,
|
||||
marginResizersEnabled,
|
||||
updateSavingIndicator,
|
||||
})
|
||||
|
||||
reloadFont(monospaceFont)
|
||||
|
||||
if (marginResizersEnabled) {
|
||||
const width = this.application.getPreference(PrefKey.EditorWidth, PrefDefaults[PrefKey.EditorWidth])
|
||||
if (width != null) {
|
||||
this.setState({
|
||||
leftResizerWidth: width,
|
||||
rightResizerWidth: width,
|
||||
})
|
||||
}
|
||||
const left = this.application.getPreference(PrefKey.EditorLeft, PrefDefaults[PrefKey.EditorLeft])
|
||||
if (left != null) {
|
||||
this.setState({
|
||||
leftResizerOffset: left,
|
||||
rightResizerOffset: left,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async reloadStackComponents() {
|
||||
@@ -896,24 +880,18 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
||||
|
||||
<div
|
||||
id={ElementIds.EditorContent}
|
||||
className={`${ElementIds.EditorContent} z-editor-content overflow-auto`}
|
||||
className={classNames(
|
||||
ElementIds.EditorContent,
|
||||
'z-editor-content overflow-auto [&>*]:mx-[var(--editor-margin)] [&>*]:max-w-[var(--editor-max-width)]',
|
||||
)}
|
||||
style={
|
||||
{
|
||||
'--editor-margin': EditorMargins[this.state.editorLineWidth],
|
||||
'--editor-max-width': EditorMaxWidths[this.state.editorLineWidth],
|
||||
} as CSSProperties
|
||||
}
|
||||
ref={this.editorContentRef}
|
||||
>
|
||||
{this.state.marginResizersEnabled && this.editorContentRef.current ? (
|
||||
<PanelResizer
|
||||
minWidth={300}
|
||||
hoverable={true}
|
||||
collapsable={false}
|
||||
panel={this.editorContentRef.current}
|
||||
side={PanelSide.Left}
|
||||
type={PanelResizeType.OffsetAndWidth}
|
||||
left={this.state.leftResizerOffset}
|
||||
width={this.state.leftResizerWidth}
|
||||
resizeFinishCallback={this.onPanelResizeFinish}
|
||||
modifyElementWidth={true}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{editorMode === 'component' && this.state.editorComponentViewer && (
|
||||
<div className="component-view flex-grow">
|
||||
<ComponentView
|
||||
@@ -950,21 +928,6 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{this.state.marginResizersEnabled && this.editorContentRef.current ? (
|
||||
<PanelResizer
|
||||
minWidth={300}
|
||||
hoverable={true}
|
||||
collapsable={false}
|
||||
panel={this.editorContentRef.current}
|
||||
side={PanelSide.Right}
|
||||
type={PanelResizeType.OffsetAndWidth}
|
||||
left={this.state.rightResizerOffset}
|
||||
width={this.state.rightResizerWidth}
|
||||
resizeFinishCallback={this.onPanelResizeFinish}
|
||||
modifyElementWidth={true}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div id="editor-pane-component-stack">
|
||||
|
||||
@@ -3,6 +3,7 @@ import { observer } from 'mobx-react-lite'
|
||||
import { useState, useEffect, useMemo, useCallback } from 'react'
|
||||
import { NoteType, Platform, SNNote } from '@standardnotes/snjs'
|
||||
import {
|
||||
CHANGE_EDITOR_WIDTH_COMMAND,
|
||||
OPEN_NOTE_HISTORY_COMMAND,
|
||||
PIN_NOTE_COMMAND,
|
||||
SHOW_HIDDEN_OPTIONS_KEYBOARD_COMMAND,
|
||||
@@ -165,6 +166,14 @@ const NotesOptions = ({
|
||||
commandService.triggerCommand(SUPER_SHOW_MARKDOWN_PREVIEW)
|
||||
}, [commandService])
|
||||
|
||||
const toggleLineWidthModal = useCallback(() => {
|
||||
application.keyboardService.triggerCommand(CHANGE_EDITOR_WIDTH_COMMAND)
|
||||
}, [application.keyboardService])
|
||||
const editorWidthShortcut = useMemo(
|
||||
() => application.keyboardService.keyboardShortcutForCommand(CHANGE_EDITOR_WIDTH_COMMAND),
|
||||
[application],
|
||||
)
|
||||
|
||||
const unauthorized = notes.some((note) => !application.isAuthorizedToRenderItem(note))
|
||||
if (unauthorized) {
|
||||
return <ProtectedUnauthorizedLabel />
|
||||
@@ -186,6 +195,11 @@ const NotesOptions = ({
|
||||
{historyShortcut && <KeyboardShortcutIndicator className="ml-auto" shortcut={historyShortcut} />}
|
||||
</MenuItem>
|
||||
<HorizontalSeparator classes="my-2" />
|
||||
<MenuItem onClick={toggleLineWidthModal}>
|
||||
<Icon type="line-width" className={iconClass} />
|
||||
Editor width
|
||||
{editorWidthShortcut && <KeyboardShortcutIndicator className="ml-auto" shortcut={editorWidthShortcut} />}
|
||||
</MenuItem>
|
||||
</>
|
||||
)}
|
||||
<MenuSwitchButtonItem
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { WebApplication } from '@/Application/WebApplication'
|
||||
import Dropdown from '@/Components/Dropdown/Dropdown'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import HorizontalSeparator from '@/Components/Shared/HorizontalSeparator'
|
||||
import Switch from '@/Components/Switch/Switch'
|
||||
import { PrefDefaults } from '@/Constants/PrefDefaults'
|
||||
import { EditorFontSize, EditorLineHeight, PrefKey } from '@standardnotes/snjs'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { ApplicationEvent, EditorFontSize, EditorLineHeight, EditorLineWidth, PrefKey } from '@standardnotes/snjs'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { Subtitle, Title, Text } from '../../PreferencesComponents/Content'
|
||||
import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup'
|
||||
import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment'
|
||||
import { CHANGE_EDITOR_WIDTH_COMMAND } from '@standardnotes/ui-services'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
@@ -59,28 +61,25 @@ const EditorDefaults = ({ application }: Props) => {
|
||||
[],
|
||||
)
|
||||
|
||||
const [marginResizers, setMarginResizers] = useState(() =>
|
||||
application.getPreference(PrefKey.EditorResizersEnabled, PrefDefaults[PrefKey.EditorResizersEnabled]),
|
||||
const [editorWidth, setEditorWidth] = useState(() =>
|
||||
application.getPreference(PrefKey.EditorLineWidth, PrefDefaults[PrefKey.EditorLineWidth]),
|
||||
)
|
||||
|
||||
const toggleMarginResizers = () => {
|
||||
setMarginResizers(!marginResizers)
|
||||
application.setPreference(PrefKey.EditorResizersEnabled, !marginResizers).catch(console.error)
|
||||
}
|
||||
const toggleEditorWidthModal = useCallback(() => {
|
||||
application.keyboardService.triggerCommand(CHANGE_EDITOR_WIDTH_COMMAND, true)
|
||||
}, [application.keyboardService])
|
||||
|
||||
useEffect(() => {
|
||||
return application.addSingleEventObserver(ApplicationEvent.PreferencesChanged, async () => {
|
||||
setEditorWidth(application.getPreference(PrefKey.EditorLineWidth, PrefDefaults[PrefKey.EditorLineWidth]))
|
||||
})
|
||||
}, [application])
|
||||
|
||||
return (
|
||||
<PreferencesGroup>
|
||||
<PreferencesSegment>
|
||||
<Title>Editor appearance</Title>
|
||||
<div className="mt-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex flex-col">
|
||||
<Subtitle>Margin Resizers</Subtitle>
|
||||
<Text>Allows left and right editor margins to be resized.</Text>
|
||||
</div>
|
||||
<Switch onChange={toggleMarginResizers} checked={marginResizers} />
|
||||
</div>
|
||||
<HorizontalSeparator classes="my-4" />
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex flex-col">
|
||||
<Subtitle>Monospace Font</Subtitle>
|
||||
@@ -114,6 +113,20 @@ const EditorDefaults = ({ application }: Props) => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<HorizontalSeparator classes="my-4" />
|
||||
<div>
|
||||
<Subtitle>Editor width</Subtitle>
|
||||
<Text>Sets the max editor width for all notes</Text>
|
||||
<div className="mt-2">
|
||||
<button
|
||||
className="flex w-full min-w-55 items-center justify-between rounded border border-border bg-default py-1.5 px-3.5 text-left text-base text-foreground md:w-fit lg:text-sm"
|
||||
onClick={toggleEditorWidthModal}
|
||||
>
|
||||
{editorWidth === EditorLineWidth.FullWidth ? 'Full width' : editorWidth}
|
||||
<Icon type="chevron-down" size="normal" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PreferencesSegment>
|
||||
</PreferencesGroup>
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { VisuallyHidden, Radio, RadioGroup, useRadioStore } from '@ariakit/react'
|
||||
import { classNames } from '@standardnotes/utils'
|
||||
|
||||
type Props = {
|
||||
items: { label: string; value: string }[]
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
}
|
||||
|
||||
const RadioButtonGroup = ({ value, items, onChange }: Props) => {
|
||||
const radio = useRadioStore({
|
||||
value,
|
||||
orientation: 'horizontal',
|
||||
setValue(value) {
|
||||
onChange(value as string)
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<RadioGroup store={radio} className="flex divide-x divide-border rounded border border-border">
|
||||
{items.map(({ label, value: itemValue }) => (
|
||||
<label
|
||||
className={classNames(
|
||||
'flex-grow select-none py-1.5 px-3.5 text-center',
|
||||
'first:rounded-tl first:rounded-bl last:rounded-tr last:rounded-br',
|
||||
itemValue === value &&
|
||||
'bg-info-backdrop font-medium text-info ring-1 ring-inset ring-info focus-within:ring-2',
|
||||
)}
|
||||
key={itemValue}
|
||||
>
|
||||
<VisuallyHidden>
|
||||
<Radio value={itemValue} />
|
||||
</VisuallyHidden>
|
||||
{label}
|
||||
</label>
|
||||
))}
|
||||
</RadioGroup>
|
||||
)
|
||||
}
|
||||
|
||||
export default RadioButtonGroup
|
||||
@@ -1,4 +1,11 @@
|
||||
import { PrefKey, CollectionSort, NewNoteTitleFormat, EditorLineHeight, EditorFontSize } from '@standardnotes/models'
|
||||
import {
|
||||
PrefKey,
|
||||
CollectionSort,
|
||||
NewNoteTitleFormat,
|
||||
EditorLineHeight,
|
||||
EditorFontSize,
|
||||
EditorLineWidth,
|
||||
} from '@standardnotes/models'
|
||||
import { FeatureIdentifier } from '@standardnotes/snjs'
|
||||
|
||||
export const PrefDefaults = {
|
||||
@@ -10,6 +17,7 @@ export const PrefDefaults = {
|
||||
[PrefKey.EditorSpellcheck]: true,
|
||||
[PrefKey.EditorResizersEnabled]: false,
|
||||
[PrefKey.EditorLineHeight]: EditorLineHeight.Normal,
|
||||
[PrefKey.EditorLineWidth]: EditorLineWidth.FullWidth,
|
||||
[PrefKey.EditorFontSize]: EditorFontSize.Normal,
|
||||
[PrefKey.SortNotesBy]: CollectionSort.CreatedAt,
|
||||
[PrefKey.SortNotesReverse]: false,
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
InternalEventBus,
|
||||
PrefKey,
|
||||
ApplicationEvent,
|
||||
EditorLineWidth,
|
||||
} from '@standardnotes/snjs'
|
||||
import { makeObservable, observable, action, computed, runInAction } from 'mobx'
|
||||
import { WebApplication } from '../../Application/WebApplication'
|
||||
@@ -358,6 +359,23 @@ export class NotesController extends AbstractViewController implements NotesCont
|
||||
this.application.sync.sync().catch(console.error)
|
||||
}
|
||||
|
||||
getEditorWidthForNote(note: SNNote) {
|
||||
return (
|
||||
note.editorWidth ?? this.application.getPreference(PrefKey.EditorLineWidth, PrefDefaults[PrefKey.EditorLineWidth])
|
||||
)
|
||||
}
|
||||
|
||||
async setNoteEditorWidth(note: SNNote, editorWidth: EditorLineWidth) {
|
||||
await this.application.mutator.changeItem<NoteMutator>(
|
||||
note,
|
||||
(mutator) => {
|
||||
mutator.editorWidth = editorWidth
|
||||
},
|
||||
false,
|
||||
)
|
||||
this.application.sync.sync().catch(console.error)
|
||||
}
|
||||
|
||||
async addTagToSelectedNotes(tag: SNTag): Promise<void> {
|
||||
const selectedNotes = this.getSelectedNotesList()
|
||||
await Promise.all(
|
||||
|
||||
Reference in New Issue
Block a user