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:
Mo
2022-01-14 14:32:16 -06:00
committed by GitHub
parent 2b12f7f0e9
commit 063c3b2fee
15 changed files with 265 additions and 106 deletions

View File

@@ -113,6 +113,7 @@ const ICONS = {
add: AddIcon,
help: HelpIcon,
keyboard: KeyboardIcon,
spellcheck: NotesIcon,
'list-bulleted': ListBulleted,
'link-off': LinkOffIcon,
listed: ListedIcon,

View File

@@ -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}

View File

@@ -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>

View File

@@ -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>
);

View File

@@ -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>

View File

@@ -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,

View File

@@ -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);

View File

@@ -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,
});