-
-
Notifications
You must be signed in to change notification settings - Fork 432
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Add new "Change Editor" option to note context menu (#823)
* feat: add editor icon * refactor: remove 'any' type and format * refactor: move NotesOptions and add ChangeEditorOption * refactor: fix type for using regular RefObject<T> * feat: add hide-if-last-child util class * feat: add Change Editor option * feat: make radio btn gray if not checked * fix: accordion menu header and item sizing/spacing * feat: add Escape key to KeyboardKey enum * refactor: Remove Editor Menu * feat: add editor select functionality * refactor: move plain editor name to constant * feat: add premium editors with modal if no subscription refactor: simplify menu group creation * feat: show alert when switching to non-interchangeable editor * fix: change editor menu going out of bounds * feat: increase group header & editor item size * fix: change editor menu close on blur * refactor: Use KeyboardKey enum & remove else statement * feat: add keyboard navigation to change editor menu * fix: editor menu separators * feat: improve change editor menu sizing & spacing * feat: show alert only if editor is not interchangeable * feat: don't show alert when switching to/from plain editor * chore: bump snjs version * feat: temporarily remove change editor alert * feat: dynamically get footer height * refactor: move magic number to const * refactor: move constants to constants file * feat: use const instead of magic number
- Loading branch information
1 parent
6aa926c
commit b932e2a
Showing
21 changed files
with
933 additions
and
385 deletions.
There are no files selected for viewing
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
288 changes: 288 additions & 0 deletions
288
app/assets/javascripts/components/NotesOptions/ChangeEditorOption.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,288 @@ | ||
import { KeyboardKey } from '@/services/ioService'; | ||
import { STRING_EDIT_LOCKED_ATTEMPT } from '@/strings'; | ||
import { WebApplication } from '@/ui_models/application'; | ||
import { AppState } from '@/ui_models/app_state'; | ||
import { | ||
MENU_MARGIN_FROM_APP_BORDER, | ||
MAX_MENU_SIZE_MULTIPLIER, | ||
} from '@/views/constants'; | ||
import { | ||
reloadFont, | ||
transactionForAssociateComponentWithCurrentNote, | ||
transactionForDisassociateComponentWithCurrentNote, | ||
} from '@/views/note_view/note_view'; | ||
import { | ||
Disclosure, | ||
DisclosureButton, | ||
DisclosurePanel, | ||
} from '@reach/disclosure'; | ||
import { | ||
ComponentArea, | ||
ItemMutator, | ||
NoteMutator, | ||
PrefKey, | ||
SNComponent, | ||
SNNote, | ||
TransactionalMutation, | ||
} from '@standardnotes/snjs'; | ||
import { FunctionComponent } from 'preact'; | ||
import { useEffect, useRef, useState } from 'preact/hooks'; | ||
import { Icon, IconType } from '../Icon'; | ||
import { PremiumModalProvider } from '../Premium'; | ||
import { createEditorMenuGroups } from './changeEditor/createEditorMenuGroups'; | ||
import { EditorAccordionMenu } from './changeEditor/EditorAccordionMenu'; | ||
|
||
type ChangeEditorOptionProps = { | ||
appState: AppState; | ||
application: WebApplication; | ||
note: SNNote; | ||
closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void; | ||
}; | ||
|
||
type AccordionMenuGroup<T> = { | ||
icon?: IconType; | ||
iconClassName?: string; | ||
title: string; | ||
items: Array<T>; | ||
}; | ||
|
||
export type EditorMenuItem = { | ||
name: string; | ||
component?: SNComponent; | ||
isPremiumFeature?: boolean; | ||
}; | ||
|
||
export type EditorMenuGroup = AccordionMenuGroup<EditorMenuItem>; | ||
|
||
export const ChangeEditorOption: FunctionComponent<ChangeEditorOptionProps> = ({ | ||
application, | ||
appState, | ||
closeOnBlur, | ||
note, | ||
}) => { | ||
const [changeEditorMenuOpen, setChangeEditorMenuOpen] = useState(false); | ||
const [changeEditorMenuPosition, setChangeEditorMenuPosition] = useState<{ | ||
top?: number | 'auto'; | ||
right?: number | 'auto'; | ||
bottom: number | 'auto'; | ||
left?: number | 'auto'; | ||
}>({ | ||
right: 0, | ||
bottom: 0, | ||
}); | ||
const changeEditorMenuRef = useRef<HTMLDivElement>(null); | ||
const changeEditorButtonRef = useRef<HTMLButtonElement>(null); | ||
const [editors] = useState<SNComponent[]>(() => | ||
application.componentManager | ||
.componentsForArea(ComponentArea.Editor) | ||
.sort((a, b) => { | ||
return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1; | ||
}) | ||
); | ||
const [editorMenuGroups, setEditorMenuGroups] = useState<EditorMenuGroup[]>( | ||
[] | ||
); | ||
const [selectedEditor, setSelectedEditor] = useState(() => | ||
application.componentManager.editorForNote(note) | ||
); | ||
|
||
useEffect(() => { | ||
setEditorMenuGroups(createEditorMenuGroups(editors)); | ||
}, [editors]); | ||
|
||
useEffect(() => { | ||
setSelectedEditor(application.componentManager.editorForNote(note)); | ||
}, [application, note]); | ||
|
||
const toggleChangeEditorMenu = () => { | ||
const defaultFontSize = window.getComputedStyle( | ||
document.documentElement | ||
).fontSize; | ||
const maxChangeEditorMenuSize = | ||
parseFloat(defaultFontSize) * MAX_MENU_SIZE_MULTIPLIER; | ||
const { clientWidth, clientHeight } = document.documentElement; | ||
const buttonRect = changeEditorButtonRef.current?.getBoundingClientRect(); | ||
const buttonParentRect = | ||
changeEditorButtonRef.current?.parentElement?.getBoundingClientRect(); | ||
const footerElementRect = document | ||
.getElementById('footer-bar') | ||
?.getBoundingClientRect(); | ||
const footerHeightInPx = footerElementRect?.height; | ||
|
||
if (buttonRect && buttonParentRect && footerHeightInPx) { | ||
let positionBottom = | ||
clientHeight - buttonRect.bottom - buttonRect.height / 2; | ||
|
||
if (positionBottom < footerHeightInPx) { | ||
positionBottom = footerHeightInPx + MENU_MARGIN_FROM_APP_BORDER; | ||
} | ||
|
||
if (buttonRect.right + maxChangeEditorMenuSize > clientWidth) { | ||
setChangeEditorMenuPosition({ | ||
top: positionBottom - buttonParentRect.height / 2, | ||
right: clientWidth - buttonRect.left, | ||
bottom: 'auto', | ||
}); | ||
} else { | ||
setChangeEditorMenuPosition({ | ||
bottom: positionBottom, | ||
left: buttonRect.right, | ||
}); | ||
} | ||
} | ||
|
||
setChangeEditorMenuOpen(!changeEditorMenuOpen); | ||
}; | ||
|
||
useEffect(() => { | ||
if (changeEditorMenuOpen) { | ||
const defaultFontSize = window.getComputedStyle( | ||
document.documentElement | ||
).fontSize; | ||
const maxChangeEditorMenuSize = | ||
parseFloat(defaultFontSize) * MAX_MENU_SIZE_MULTIPLIER; | ||
const changeEditorMenuBoundingRect = | ||
changeEditorMenuRef.current?.getBoundingClientRect(); | ||
const buttonRect = changeEditorButtonRef.current?.getBoundingClientRect(); | ||
|
||
if (changeEditorMenuBoundingRect && buttonRect) { | ||
if (changeEditorMenuBoundingRect.y < MENU_MARGIN_FROM_APP_BORDER) { | ||
if ( | ||
buttonRect.right + maxChangeEditorMenuSize > | ||
document.documentElement.clientWidth | ||
) { | ||
setChangeEditorMenuPosition({ | ||
...changeEditorMenuPosition, | ||
top: MENU_MARGIN_FROM_APP_BORDER + buttonRect.height, | ||
bottom: 'auto', | ||
}); | ||
} else { | ||
setChangeEditorMenuPosition({ | ||
...changeEditorMenuPosition, | ||
top: MENU_MARGIN_FROM_APP_BORDER, | ||
bottom: 'auto', | ||
}); | ||
} | ||
} | ||
} | ||
} | ||
}, [changeEditorMenuOpen, changeEditorMenuPosition]); | ||
|
||
const selectComponent = async (component: SNComponent | null) => { | ||
if (component) { | ||
if (component.conflictOf) { | ||
application.changeAndSaveItem(component.uuid, (mutator) => { | ||
mutator.conflictOf = undefined; | ||
}); | ||
} | ||
} | ||
|
||
const transactions: TransactionalMutation[] = []; | ||
|
||
if (appState.getActiveNoteController()?.isTemplateNote) { | ||
await appState.getActiveNoteController().insertTemplatedNote(); | ||
} | ||
|
||
if (note.locked) { | ||
application.alertService.alert(STRING_EDIT_LOCKED_ATTEMPT); | ||
return; | ||
} | ||
|
||
if (!component) { | ||
if (!note.prefersPlainEditor) { | ||
transactions.push({ | ||
itemUuid: note.uuid, | ||
mutate: (m: ItemMutator) => { | ||
const noteMutator = m as NoteMutator; | ||
noteMutator.prefersPlainEditor = true; | ||
}, | ||
}); | ||
} | ||
const currentEditor = application.componentManager.editorForNote(note); | ||
if (currentEditor?.isExplicitlyEnabledForItem(note.uuid)) { | ||
transactions.push( | ||
transactionForDisassociateComponentWithCurrentNote( | ||
currentEditor, | ||
note | ||
) | ||
); | ||
} | ||
reloadFont(application.getPreference(PrefKey.EditorMonospaceEnabled)); | ||
} else if (component.area === ComponentArea.Editor) { | ||
const currentEditor = application.componentManager.editorForNote(note); | ||
if (currentEditor && component.uuid !== currentEditor.uuid) { | ||
transactions.push( | ||
transactionForDisassociateComponentWithCurrentNote( | ||
currentEditor, | ||
note | ||
) | ||
); | ||
} | ||
const prefersPlain = note.prefersPlainEditor; | ||
if (prefersPlain) { | ||
transactions.push({ | ||
itemUuid: note.uuid, | ||
mutate: (m: ItemMutator) => { | ||
const noteMutator = m as NoteMutator; | ||
noteMutator.prefersPlainEditor = false; | ||
}, | ||
}); | ||
} | ||
transactions.push( | ||
transactionForAssociateComponentWithCurrentNote(component, note) | ||
); | ||
} | ||
|
||
await application.runTransactionalMutations(transactions); | ||
/** Dirtying can happen above */ | ||
application.sync(); | ||
|
||
setSelectedEditor(application.componentManager.editorForNote(note)); | ||
}; | ||
|
||
return ( | ||
<Disclosure open={changeEditorMenuOpen} onChange={toggleChangeEditorMenu}> | ||
<DisclosureButton | ||
onKeyDown={(event) => { | ||
if (event.key === KeyboardKey.Escape) { | ||
setChangeEditorMenuOpen(false); | ||
} | ||
}} | ||
onBlur={closeOnBlur} | ||
ref={changeEditorButtonRef} | ||
className="sn-dropdown-item justify-between" | ||
> | ||
<div className="flex items-center"> | ||
<Icon type="editor" className="color-neutral mr-2" /> | ||
Change editor | ||
</div> | ||
<Icon type="chevron-right" className="color-neutral" /> | ||
</DisclosureButton> | ||
<DisclosurePanel | ||
ref={changeEditorMenuRef} | ||
onKeyDown={(event) => { | ||
if (event.key === KeyboardKey.Escape) { | ||
setChangeEditorMenuOpen(false); | ||
changeEditorButtonRef.current?.focus(); | ||
} | ||
}} | ||
style={{ | ||
...changeEditorMenuPosition, | ||
position: 'fixed', | ||
}} | ||
className="sn-dropdown flex flex-col py-1 max-h-120 min-w-68 fixed overflow-y-auto" | ||
> | ||
<PremiumModalProvider state={appState.features}> | ||
<EditorAccordionMenu | ||
application={application} | ||
closeOnBlur={closeOnBlur} | ||
groups={editorMenuGroups} | ||
isOpen={changeEditorMenuOpen} | ||
selectComponent={selectComponent} | ||
selectedEditor={selectedEditor} | ||
/> | ||
</PremiumModalProvider> | ||
</DisclosurePanel> | ||
</Disclosure> | ||
); | ||
}; |
Oops, something went wrong.