feat: sticky header on mobile if plaintext note is scrolled (#1645)
This commit is contained in:
@@ -14,7 +14,7 @@ import {
|
|||||||
PayloadEmitSource,
|
PayloadEmitSource,
|
||||||
WebAppEvent,
|
WebAppEvent,
|
||||||
} from '@standardnotes/snjs'
|
} from '@standardnotes/snjs'
|
||||||
import { debounce, isDesktopApplication } from '@/Utils'
|
import { debounce, isDesktopApplication, isIOS } from '@/Utils'
|
||||||
import { EditorEventSource } from '../../Types/EditorEventSource'
|
import { EditorEventSource } from '../../Types/EditorEventSource'
|
||||||
import { confirmDialog, KeyboardModifier, KeyboardKey } from '@standardnotes/ui-services'
|
import { confirmDialog, KeyboardModifier, KeyboardKey } from '@standardnotes/ui-services'
|
||||||
import { STRING_DELETE_PLACEHOLDER_ATTEMPT, STRING_DELETE_LOCKED_ATTEMPT, StringDeleteNote } from '@/Constants/Strings'
|
import { STRING_DELETE_PLACEHOLDER_ATTEMPT, STRING_DELETE_LOCKED_ATTEMPT, StringDeleteNote } from '@/Constants/Strings'
|
||||||
@@ -22,7 +22,6 @@ import { PureComponent } from '@/Components/Abstract/PureComponent'
|
|||||||
import ProtectedItemOverlay from '@/Components/ProtectedItemOverlay/ProtectedItemOverlay'
|
import ProtectedItemOverlay from '@/Components/ProtectedItemOverlay/ProtectedItemOverlay'
|
||||||
import PinNoteButton from '@/Components/PinNoteButton/PinNoteButton'
|
import PinNoteButton from '@/Components/PinNoteButton/PinNoteButton'
|
||||||
import NotesOptionsPanel from '@/Components/NotesOptions/NotesOptionsPanel'
|
import NotesOptionsPanel from '@/Components/NotesOptions/NotesOptionsPanel'
|
||||||
import NoteTagsContainer from '@/Components/NoteTags/NoteTagsContainer'
|
|
||||||
import ComponentView from '@/Components/ComponentView/ComponentView'
|
import ComponentView from '@/Components/ComponentView/ComponentView'
|
||||||
import PanelResizer, { PanelSide, PanelResizeType } from '@/Components/PanelResizer/PanelResizer'
|
import PanelResizer, { PanelSide, PanelResizeType } from '@/Components/PanelResizer/PanelResizer'
|
||||||
import { ElementIds } from '@/Constants/ElementIDs'
|
import { ElementIds } from '@/Constants/ElementIDs'
|
||||||
@@ -41,9 +40,10 @@ import AutoresizingNoteViewTextarea from './AutoresizingTextarea'
|
|||||||
import MobileItemsListButton from '../NoteGroupView/MobileItemsListButton'
|
import MobileItemsListButton from '../NoteGroupView/MobileItemsListButton'
|
||||||
import NoteTagsPanel from '../NoteTags/NoteTagsPanel'
|
import NoteTagsPanel from '../NoteTags/NoteTagsPanel'
|
||||||
|
|
||||||
const MINIMUM_STATUS_DURATION = 400
|
const MinimumStatusDuration = 400
|
||||||
const TEXTAREA_DEBOUNCE = 100
|
const TextareaDebounce = 100
|
||||||
const NOTE_EDITING_DISABLED_TEXT = 'Note editing disabled.'
|
const NoteEditingDisabledText = 'Note editing disabled.'
|
||||||
|
const StickyHeaderScrollThresholdInPx = 20
|
||||||
|
|
||||||
type NoteStatus = {
|
type NoteStatus = {
|
||||||
message?: string
|
message?: string
|
||||||
@@ -81,6 +81,8 @@ type State = {
|
|||||||
leftResizerOffset: number
|
leftResizerOffset: number
|
||||||
rightResizerWidth: number
|
rightResizerWidth: number
|
||||||
rightResizerOffset: number
|
rightResizerOffset: number
|
||||||
|
|
||||||
|
shouldStickyHeader: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
class NoteView extends PureComponent<NoteViewProps, State> {
|
class NoteView extends PureComponent<NoteViewProps, State> {
|
||||||
@@ -112,7 +114,9 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
|||||||
|
|
||||||
this.debounceReloadEditorComponent = debounce(this.debounceReloadEditorComponent.bind(this), 25)
|
this.debounceReloadEditorComponent = debounce(this.debounceReloadEditorComponent.bind(this), 25)
|
||||||
|
|
||||||
this.textAreaChangeDebounceSave = debounce(this.textAreaChangeDebounceSave, TEXTAREA_DEBOUNCE)
|
this.textAreaChangeDebounceSave = debounce(this.textAreaChangeDebounceSave, TextareaDebounce)
|
||||||
|
|
||||||
|
this.handleWindowScroll = debounce(this.handleWindowScroll, 10)
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
availableStackComponents: [],
|
availableStackComponents: [],
|
||||||
@@ -120,7 +124,7 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
|||||||
editorText: '',
|
editorText: '',
|
||||||
editorTitle: '',
|
editorTitle: '',
|
||||||
isDesktop: isDesktopApplication(),
|
isDesktop: isDesktopApplication(),
|
||||||
lockText: NOTE_EDITING_DISABLED_TEXT,
|
lockText: NoteEditingDisabledText,
|
||||||
noteStatus: undefined,
|
noteStatus: undefined,
|
||||||
noteLocked: this.controller.item.locked,
|
noteLocked: this.controller.item.locked,
|
||||||
showLockedIcon: true,
|
showLockedIcon: true,
|
||||||
@@ -133,12 +137,17 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
|||||||
leftResizerOffset: 0,
|
leftResizerOffset: 0,
|
||||||
rightResizerWidth: 0,
|
rightResizerWidth: 0,
|
||||||
rightResizerOffset: 0,
|
rightResizerOffset: 0,
|
||||||
|
shouldStickyHeader: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
this.editorContentRef = createRef<HTMLDivElement>()
|
this.editorContentRef = createRef<HTMLDivElement>()
|
||||||
|
|
||||||
|
window.addEventListener('scroll', this.handleWindowScroll)
|
||||||
}
|
}
|
||||||
|
|
||||||
override deinit() {
|
override deinit() {
|
||||||
|
window.removeEventListener('scroll', this.handleWindowScroll)
|
||||||
|
|
||||||
this.removeComponentStreamObserver?.()
|
this.removeComponentStreamObserver?.()
|
||||||
;(this.removeComponentStreamObserver as unknown) = undefined
|
;(this.removeComponentStreamObserver as unknown) = undefined
|
||||||
|
|
||||||
@@ -522,7 +531,7 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
|||||||
this.setState({
|
this.setState({
|
||||||
noteStatus: status,
|
noteStatus: status,
|
||||||
})
|
})
|
||||||
}, MINIMUM_STATUS_DURATION)
|
}, MinimumStatusDuration)
|
||||||
} else {
|
} else {
|
||||||
this.setState({
|
this.setState({
|
||||||
noteStatus: status,
|
noteStatus: status,
|
||||||
@@ -872,6 +881,12 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleWindowScroll = () => {
|
||||||
|
this.setState({
|
||||||
|
shouldStickyHeader: window.scrollY > StickyHeaderScrollThresholdInPx,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
override render() {
|
override render() {
|
||||||
if (this.state.showProtectedWarning) {
|
if (this.state.showProtectedWarning) {
|
||||||
return (
|
return (
|
||||||
@@ -890,7 +905,7 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
|||||||
<EditingDisabledBanner
|
<EditingDisabledBanner
|
||||||
onMouseLeave={() => {
|
onMouseLeave={() => {
|
||||||
this.setState({
|
this.setState({
|
||||||
lockText: NOTE_EDITING_DISABLED_TEXT,
|
lockText: NoteEditingDisabledText,
|
||||||
showLockedIcon: true,
|
showLockedIcon: true,
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
@@ -907,7 +922,14 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{this.note && (
|
{this.note && (
|
||||||
<div id="editor-title-bar" className="content-title-bar section-title-bar z-editor-title-bar w-full">
|
<div
|
||||||
|
id="editor-title-bar"
|
||||||
|
className={classNames(
|
||||||
|
'content-title-bar section-title-bar z-editor-title-bar w-full bg-default',
|
||||||
|
this.state.shouldStickyHeader && 'fixed top-0',
|
||||||
|
this.state.shouldStickyHeader ? (isIOS() ? 'pt-safe-top' : 'pt-4') : '',
|
||||||
|
)}
|
||||||
|
>
|
||||||
<div className="mb-2 flex flex-wrap items-start justify-between gap-2 md:mb-0 md:flex-nowrap md:gap-0 xl:items-center">
|
<div className="mb-2 flex flex-wrap items-start justify-between gap-2 md:mb-0 md:flex-nowrap md:gap-0 xl:items-center">
|
||||||
<div className={classNames(this.state.noteLocked && 'locked', 'flex flex-grow items-center')}>
|
<div className={classNames(this.state.noteLocked && 'locked', 'flex flex-grow items-center')}>
|
||||||
<MobileItemsListButton />
|
<MobileItemsListButton />
|
||||||
@@ -927,62 +949,62 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row-reverse items-center gap-3 md:flex-col-reverse md:items-end xl:flex-row xl:flex-nowrap xl:items-center">
|
{!this.state.shouldStickyHeader && (
|
||||||
{this.state.noteStatus?.message?.length && (
|
<div className="flex flex-row-reverse items-center gap-3 md:flex-col-reverse md:items-end xl:flex-row xl:flex-nowrap xl:items-center">
|
||||||
<div id="save-status-container" className={'xl:mr-5 xl:max-w-[16ch]'}>
|
{this.state.noteStatus?.message?.length && (
|
||||||
<div id="save-status">
|
<div id="save-status-container" className={'xl:mr-5 xl:max-w-[16ch]'}>
|
||||||
<div
|
<div id="save-status">
|
||||||
className={
|
<div
|
||||||
(this.state.syncTakingTooLong ? 'font-bold text-warning ' : '') +
|
className={
|
||||||
(this.state.saveError ? 'font-bold text-danger ' : '') +
|
(this.state.syncTakingTooLong ? 'font-bold text-warning ' : '') +
|
||||||
'message text-xs'
|
(this.state.saveError ? 'font-bold text-danger ' : '') +
|
||||||
}
|
'message text-xs'
|
||||||
>
|
}
|
||||||
{this.state.noteStatus?.message}
|
>
|
||||||
|
{this.state.noteStatus?.message}
|
||||||
|
</div>
|
||||||
|
{this.state.noteStatus?.desc && (
|
||||||
|
<div className="desc text-xs">{this.state.noteStatus.desc}</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{this.state.noteStatus?.desc && <div className="desc text-xs">{this.state.noteStatus.desc}</div>}
|
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<NoteTagsPanel
|
||||||
|
onClickPreprocessing={this.ensureNoteIsInsertedBeforeUIAction}
|
||||||
|
noteTagsController={this.viewControllerManager.noteTagsController}
|
||||||
|
/>
|
||||||
|
<AttachedFilesButton
|
||||||
|
application={this.application}
|
||||||
|
onClickPreprocessing={this.ensureNoteIsInsertedBeforeUIAction}
|
||||||
|
featuresController={this.viewControllerManager.featuresController}
|
||||||
|
filePreviewModalController={this.viewControllerManager.filePreviewModalController}
|
||||||
|
filesController={this.viewControllerManager.filesController}
|
||||||
|
navigationController={this.viewControllerManager.navigationController}
|
||||||
|
notesController={this.viewControllerManager.notesController}
|
||||||
|
selectionController={this.viewControllerManager.selectionController}
|
||||||
|
/>
|
||||||
|
<ChangeEditorButton
|
||||||
|
application={this.application}
|
||||||
|
viewControllerManager={this.viewControllerManager}
|
||||||
|
onClickPreprocessing={this.ensureNoteIsInsertedBeforeUIAction}
|
||||||
|
/>
|
||||||
|
<PinNoteButton
|
||||||
|
notesController={this.viewControllerManager.notesController}
|
||||||
|
onClickPreprocessing={this.ensureNoteIsInsertedBeforeUIAction}
|
||||||
|
/>
|
||||||
|
<NotesOptionsPanel
|
||||||
|
application={this.application}
|
||||||
|
navigationController={this.viewControllerManager.navigationController}
|
||||||
|
notesController={this.viewControllerManager.notesController}
|
||||||
|
noteTagsController={this.viewControllerManager.noteTagsController}
|
||||||
|
historyModalController={this.viewControllerManager.historyModalController}
|
||||||
|
onClickPreprocessing={this.ensureNoteIsInsertedBeforeUIAction}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<NoteTagsPanel
|
|
||||||
onClickPreprocessing={this.ensureNoteIsInsertedBeforeUIAction}
|
|
||||||
noteTagsController={this.viewControllerManager.noteTagsController}
|
|
||||||
/>
|
|
||||||
<AttachedFilesButton
|
|
||||||
application={this.application}
|
|
||||||
onClickPreprocessing={this.ensureNoteIsInsertedBeforeUIAction}
|
|
||||||
featuresController={this.viewControllerManager.featuresController}
|
|
||||||
filePreviewModalController={this.viewControllerManager.filePreviewModalController}
|
|
||||||
filesController={this.viewControllerManager.filesController}
|
|
||||||
navigationController={this.viewControllerManager.navigationController}
|
|
||||||
notesController={this.viewControllerManager.notesController}
|
|
||||||
selectionController={this.viewControllerManager.selectionController}
|
|
||||||
/>
|
|
||||||
<ChangeEditorButton
|
|
||||||
application={this.application}
|
|
||||||
viewControllerManager={this.viewControllerManager}
|
|
||||||
onClickPreprocessing={this.ensureNoteIsInsertedBeforeUIAction}
|
|
||||||
/>
|
|
||||||
<PinNoteButton
|
|
||||||
notesController={this.viewControllerManager.notesController}
|
|
||||||
onClickPreprocessing={this.ensureNoteIsInsertedBeforeUIAction}
|
|
||||||
/>
|
|
||||||
<NotesOptionsPanel
|
|
||||||
application={this.application}
|
|
||||||
navigationController={this.viewControllerManager.navigationController}
|
|
||||||
notesController={this.viewControllerManager.notesController}
|
|
||||||
noteTagsController={this.viewControllerManager.noteTagsController}
|
|
||||||
historyModalController={this.viewControllerManager.historyModalController}
|
|
||||||
onClickPreprocessing={this.ensureNoteIsInsertedBeforeUIAction}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
<NoteTagsContainer
|
|
||||||
noteTagsController={this.viewControllerManager.noteTagsController}
|
|
||||||
navigationController={this.viewControllerManager.navigationController}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user