feat: per-note spellcheck (#815)
* feat: per-note spellcheck control * fix: remove fill from svg * feat: move spellcheck pref into defaults preferences section * fix: use faded css class instead of opacity * feat: plus editor 1.6.0
This commit is contained in:
@@ -113,6 +113,7 @@ const ICONS = {
|
||||
add: AddIcon,
|
||||
help: HelpIcon,
|
||||
keyboard: KeyboardIcon,
|
||||
spellcheck: NotesIcon,
|
||||
'list-bulleted': ListBulleted,
|
||||
'link-off': LinkOffIcon,
|
||||
listed: ListedIcon,
|
||||
|
||||
@@ -35,6 +35,8 @@ const DeletePermanentlyButton = ({
|
||||
</button>
|
||||
);
|
||||
|
||||
const iconClass = 'color-neutral mr-2';
|
||||
|
||||
const getWordCount = (text: string) => {
|
||||
if (text.trim().length === 0) {
|
||||
return 0;
|
||||
@@ -133,6 +135,39 @@ const NoteAttributes: FunctionComponent<{ application: SNApplication, note: SNNo
|
||||
);
|
||||
};
|
||||
|
||||
const SpellcheckOptions: FunctionComponent<{
|
||||
appState: AppState, note: SNNote
|
||||
}> = ({ appState, note }) => {
|
||||
|
||||
const editor = appState.application.componentManager.editorForNote(note);
|
||||
const spellcheckControllable = Boolean(
|
||||
!editor ||
|
||||
appState.application.getFeature(editor.identifier)?.spellcheckControl
|
||||
);
|
||||
const noteSpellcheck = !spellcheckControllable ? true : note ? appState.notes.getSpellcheckStateForNote(note) : undefined;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col px-3 py-1.5">
|
||||
<Switch
|
||||
className="px-0 py-0"
|
||||
checked={noteSpellcheck}
|
||||
disabled={!spellcheckControllable}
|
||||
onChange={() => {
|
||||
appState.notes.toggleGlobalSpellcheckForNote(note);
|
||||
}}
|
||||
>
|
||||
<span className="flex items-center">
|
||||
<Icon type='spellcheck' className={iconClass} />
|
||||
Spellcheck
|
||||
</span>
|
||||
</Switch>
|
||||
{!spellcheckControllable && (
|
||||
<p className="text-xs pt-1.5">Spellcheck cannot be controlled for this editor.</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const NotesOptions = observer(
|
||||
({ application, appState, closeOnBlur, onSubmenuChange }: Props) => {
|
||||
const [tagsMenuOpen, setTagsMenuOpen] = useState(false);
|
||||
@@ -171,8 +206,6 @@ export const NotesOptions = observer(
|
||||
|
||||
const tagsButtonRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const iconClass = 'color-neutral mr-2';
|
||||
|
||||
useEffect(() => {
|
||||
if (onSubmenuChange) {
|
||||
onSubmenuChange(tagsMenuOpen);
|
||||
@@ -350,10 +383,9 @@ export const NotesOptions = observer(
|
||||
>
|
||||
<span
|
||||
className={`whitespace-nowrap overflow-hidden overflow-ellipsis
|
||||
${
|
||||
appState.notes.isTagInSelectedNotes(tag)
|
||||
? 'font-bold'
|
||||
: ''
|
||||
${appState.notes.isTagInSelectedNotes(tag)
|
||||
? 'font-bold'
|
||||
: ''
|
||||
}`}
|
||||
>
|
||||
{tag.title}
|
||||
@@ -484,9 +516,16 @@ export const NotesOptions = observer(
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
{notes.length === 1 ? (
|
||||
<>
|
||||
<div className="min-h-1px my-2 bg-border"></div>
|
||||
|
||||
<SpellcheckOptions appState={appState} note={notes[0]} />
|
||||
|
||||
<div className="min-h-1px my-2 bg-border"></div>
|
||||
|
||||
<NoteAttributes application={application} note={notes[0]} />
|
||||
</>
|
||||
) : null}
|
||||
|
||||
@@ -29,7 +29,7 @@ export const Switch: FunctionalComponent<SwitchProps> = (
|
||||
|
||||
return (
|
||||
<label
|
||||
className={`sn-component flex justify-between items-center cursor-pointer px-3 ${className}`}
|
||||
className={`sn-component flex justify-between items-center cursor-pointer px-3 ${className} ${isDisabled ? 'faded' : ''}`}
|
||||
{...(props.role ? { role: props.role } : {})}
|
||||
>
|
||||
{props.children}
|
||||
@@ -51,9 +51,8 @@ export const Switch: FunctionalComponent<SwitchProps> = (
|
||||
/>
|
||||
<span
|
||||
aria-hidden
|
||||
className={`sn-switch-handle ${
|
||||
checked ? 'sn-switch-handle--right' : ''
|
||||
}`}
|
||||
className={`sn-switch-handle ${checked ? 'sn-switch-handle--right' : ''
|
||||
}`}
|
||||
/>
|
||||
</CustomCheckboxContainer>
|
||||
</label>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Dropdown, DropdownItem } from '@/components/Dropdown';
|
||||
import { IconType } from '@/components/Icon';
|
||||
import { FeatureIdentifier } from '@standardnotes/snjs';
|
||||
import { FeatureIdentifier, PrefKey } from '@standardnotes/snjs';
|
||||
import {
|
||||
PreferencesGroup,
|
||||
PreferencesSegment,
|
||||
@@ -16,6 +16,8 @@ import {
|
||||
} from '@standardnotes/snjs';
|
||||
import { FunctionComponent } from 'preact';
|
||||
import { useEffect, useState } from 'preact/hooks';
|
||||
import { HorizontalSeparator } from '@/components/shared/HorizontalSeparator';
|
||||
import { Switch } from '@/components/Switch';
|
||||
|
||||
type Props = {
|
||||
application: WebApplication;
|
||||
@@ -87,6 +89,15 @@ export const Defaults: FunctionComponent<Props> = ({ application }) => {
|
||||
getDefaultEditor(application)?.package_info?.identifier || 'plain-editor'
|
||||
);
|
||||
|
||||
const [spellcheck, setSpellcheck] = useState(() =>
|
||||
application.getPreference(PrefKey.EditorSpellcheck, true)
|
||||
);
|
||||
|
||||
const toggleSpellcheck = () => {
|
||||
setSpellcheck(!spellcheck);
|
||||
application.getAppState().toggleGlobalSpellcheck();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const editors = application.componentManager
|
||||
.componentsForArea(ComponentArea.Editor)
|
||||
@@ -149,6 +160,17 @@ export const Defaults: FunctionComponent<Props> = ({ application }) => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<HorizontalSeparator classes="mt-5 mb-3" />
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex flex-col">
|
||||
<Subtitle>Spellcheck</Subtitle>
|
||||
<Text>
|
||||
The default spellcheck value for new notes. Spellcheck can be configured per note from the note context menu.
|
||||
Spellcheck may degrade overall typing performance with long notes.
|
||||
</Text>
|
||||
</div>
|
||||
<Switch onChange={toggleSpellcheck} checked={spellcheck} />
|
||||
</div>
|
||||
</PreferencesSegment>
|
||||
</PreferencesGroup>
|
||||
);
|
||||
|
||||
@@ -25,9 +25,6 @@ export const Tools: FunctionalComponent<Props> = observer(
|
||||
const [marginResizers, setMarginResizers] = useState(() =>
|
||||
application.getPreference(PrefKey.EditorResizersEnabled, true)
|
||||
);
|
||||
const [spellcheck, setSpellcheck] = useState(() =>
|
||||
application.getPreference(PrefKey.EditorSpellcheck, true)
|
||||
);
|
||||
|
||||
const toggleMonospaceFont = () => {
|
||||
setMonospaceFont(!monospaceFont);
|
||||
@@ -39,11 +36,6 @@ export const Tools: FunctionalComponent<Props> = observer(
|
||||
application.setPreference(PrefKey.EditorResizersEnabled, !marginResizers);
|
||||
};
|
||||
|
||||
const toggleSpellcheck = () => {
|
||||
setSpellcheck(!spellcheck);
|
||||
application.setPreference(PrefKey.EditorSpellcheck, !spellcheck);
|
||||
};
|
||||
|
||||
return (
|
||||
<PreferencesGroup>
|
||||
<PreferencesSegment>
|
||||
@@ -67,17 +59,6 @@ export const Tools: FunctionalComponent<Props> = observer(
|
||||
checked={marginResizers}
|
||||
/>
|
||||
</div>
|
||||
<HorizontalSeparator classes="mt-5 mb-3" />
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex flex-col">
|
||||
<Subtitle>Spellcheck</Subtitle>
|
||||
<Text>
|
||||
May degrade performance, especially with long notes. This option only controls
|
||||
spellcheck in the Plain Editor.
|
||||
</Text>
|
||||
</div>
|
||||
<Switch onChange={toggleSpellcheck} checked={spellcheck} />
|
||||
</div>
|
||||
</div>
|
||||
</PreferencesSegment>
|
||||
</PreferencesGroup>
|
||||
|
||||
@@ -281,6 +281,18 @@ export class AppState {
|
||||
}
|
||||
}
|
||||
|
||||
isGlobalSpellcheckEnabled(): boolean {
|
||||
return this.application.getPreference(PrefKey.EditorSpellcheck, true);
|
||||
}
|
||||
|
||||
async toggleGlobalSpellcheck() {
|
||||
const currentValue = this.isGlobalSpellcheckEnabled();
|
||||
return this.application.setPreference(
|
||||
PrefKey.EditorSpellcheck,
|
||||
!currentValue
|
||||
);
|
||||
}
|
||||
|
||||
private tagChangedNotifier(): IReactionDisposer {
|
||||
return reaction(
|
||||
() => this.tags.selectedUuid,
|
||||
|
||||
@@ -378,6 +378,23 @@ export class NotesState {
|
||||
this.selectedNotes = {};
|
||||
}
|
||||
|
||||
getSpellcheckStateForNote(note: SNNote) {
|
||||
return note.spellcheck != undefined
|
||||
? note.spellcheck
|
||||
: this.appState.isGlobalSpellcheckEnabled();
|
||||
}
|
||||
|
||||
async toggleGlobalSpellcheckForNote(note: SNNote) {
|
||||
await this.application.changeItem<NoteMutator>(
|
||||
note.uuid,
|
||||
(mutator) => {
|
||||
mutator.toggleSpellcheck();
|
||||
},
|
||||
false
|
||||
);
|
||||
this.application.sync();
|
||||
}
|
||||
|
||||
async addTagToSelectedNotes(tag: SNTag): Promise<void> {
|
||||
const selectedNotes = Object.values(this.selectedNotes);
|
||||
const parentChainTags = this.application.getTagParentChain(tag);
|
||||
|
||||
@@ -201,6 +201,8 @@ export class NoteView extends PureViewCtrl<unknown, EditorState> {
|
||||
this.editorValues.text = note.text;
|
||||
}
|
||||
|
||||
this.reloadSpellcheck();
|
||||
|
||||
const isTemplateNoteInsertedToBeInteractableWithEditor =
|
||||
source === PayloadSource.Constructor && note.dirty;
|
||||
if (isTemplateNoteInsertedToBeInteractableWithEditor) {
|
||||
@@ -694,29 +696,35 @@ export class NoteView extends PureViewCtrl<unknown, EditorState> {
|
||||
this.application.sync();
|
||||
}
|
||||
|
||||
async reloadPreferences() {
|
||||
const monospaceFont = this.application.getPreference(
|
||||
PrefKey.EditorMonospaceEnabled,
|
||||
true
|
||||
);
|
||||
const spellcheck = this.application.getPreference(
|
||||
PrefKey.EditorSpellcheck,
|
||||
true
|
||||
);
|
||||
const marginResizersEnabled = this.application.getPreference(
|
||||
PrefKey.EditorResizersEnabled,
|
||||
true
|
||||
);
|
||||
async reloadSpellcheck() {
|
||||
const spellcheck = this.appState.notes.getSpellcheckStateForNote(this.note);
|
||||
|
||||
if (spellcheck !== this.state.spellcheck) {
|
||||
await this.setState({ textareaUnloading: true });
|
||||
await this.setState({ textareaUnloading: false });
|
||||
this.reloadFont();
|
||||
|
||||
await this.setState({
|
||||
spellcheck,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async reloadPreferences() {
|
||||
const monospaceFont = this.application.getPreference(
|
||||
PrefKey.EditorMonospaceEnabled,
|
||||
true
|
||||
);
|
||||
|
||||
const marginResizersEnabled = this.application.getPreference(
|
||||
PrefKey.EditorResizersEnabled,
|
||||
true
|
||||
);
|
||||
|
||||
await this.reloadSpellcheck();
|
||||
|
||||
await this.setState({
|
||||
monospaceFont,
|
||||
spellcheck,
|
||||
marginResizersEnabled,
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user