Skip to content

Commit

Permalink
feat: per-note spellcheck (#815)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
moughxyz committed Jan 14, 2022
1 parent 2b12f7f commit 063c3b2
Show file tree
Hide file tree
Showing 15 changed files with 259 additions and 100 deletions.
3 changes: 3 additions & 0 deletions app/assets/icons/ic-notes-remove.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions app/assets/icons/ic-text.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions app/assets/javascripts/components/Icon.tsx
Expand Up @@ -113,6 +113,7 @@ const ICONS = {
add: AddIcon,
help: HelpIcon,
keyboard: KeyboardIcon,
spellcheck: NotesIcon,
'list-bulleted': ListBulleted,
'link-off': LinkOffIcon,
listed: ListedIcon,
Expand Down
51 changes: 45 additions & 6 deletions app/assets/javascripts/components/NotesOptions.tsx
Expand Up @@ -35,6 +35,8 @@ const DeletePermanentlyButton = ({
</button>
);

const iconClass = 'color-neutral mr-2';

const getWordCount = (text: string) => {
if (text.trim().length === 0) {
return 0;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -171,8 +206,6 @@ export const NotesOptions = observer(

const tagsButtonRef = useRef<HTMLButtonElement>(null);

const iconClass = 'color-neutral mr-2';

useEffect(() => {
if (onSubmenuChange) {
onSubmenuChange(tagsMenuOpen);
Expand Down Expand Up @@ -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}
Expand Down Expand Up @@ -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}
Expand Down
7 changes: 3 additions & 4 deletions app/assets/javascripts/components/Switch.tsx
Expand Up @@ -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}
Expand All @@ -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>
Expand Down
@@ -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,
Expand All @@ -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;
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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>
);
Expand Down
Expand Up @@ -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);
Expand All @@ -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>
Expand All @@ -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>
Expand Down
12 changes: 12 additions & 0 deletions app/assets/javascripts/ui_models/app_state/app_state.ts
Expand Up @@ -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,
Expand Down
17 changes: 17 additions & 0 deletions app/assets/javascripts/ui_models/app_state/notes_state.ts
Expand Up @@ -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);
Expand Down
28 changes: 18 additions & 10 deletions app/assets/javascripts/views/note_view/note_view.ts
Expand Up @@ -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) {
Expand Down Expand Up @@ -694,29 +696,35 @@ export class NoteView extends PureViewCtrl<unknown, EditorState> {
this.application.sync();
}

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 spellcheck = this.application.getPreference(
PrefKey.EditorSpellcheck,
true
);

const marginResizersEnabled = this.application.getPreference(
PrefKey.EditorResizersEnabled,
true
);

if (spellcheck !== this.state.spellcheck) {
await this.setState({ textareaUnloading: true });
await this.setState({ textareaUnloading: false });
this.reloadFont();
}
await this.reloadSpellcheck();

await this.setState({
monospaceFont,
spellcheck,
marginResizersEnabled,
});

Expand Down
8 changes: 4 additions & 4 deletions package.json
Expand Up @@ -27,7 +27,6 @@
"@babel/preset-typescript": "^7.15.0",
"@reach/disclosure": "^0.16.2",
"@reach/visually-hidden": "^0.16.0",
"@standardnotes/components": "1.2.8",
"@svgr/webpack": "^5.5.0",
"@types/angular": "^1.8.3",
"@types/jest": "^27.0.3",
Expand Down Expand Up @@ -86,10 +85,11 @@
"@reach/dialog": "^0.16.2",
"@reach/listbox": "^0.16.2",
"@reach/tooltip": "^0.16.2",
"@standardnotes/features": "1.20.7",
"@standardnotes/components": "1.3.0",
"@standardnotes/features": "1.22.0",
"@standardnotes/settings": "^1.9.0",
"@standardnotes/sncrypto-web": "1.5.3",
"@standardnotes/snjs": "2.35.5",
"@standardnotes/sncrypto-web": "1.6.0",
"@standardnotes/snjs": "2.37.1",
"mobx": "^6.3.5",
"mobx-react-lite": "^3.2.2",
"preact": "^10.5.15",
Expand Down
6 changes: 3 additions & 3 deletions public/components/checksums.json
Expand Up @@ -45,9 +45,9 @@
"binary": "bac85952fbe8af1eac49701da69b811de531fd15b1cfd6bbc90d61d9c785f5d1"
},
"org.standardnotes.plus-editor": {
"version": "1.5.0",
"base64": "f8923ab4a464ee113d47fa164ee7efbc6337e9e538105ae473b00492fb4ec46e",
"binary": "d4645ee7a38eeb1046239ea337333dcb64dc01c81f05e20307696099364b485b"
"version": "1.6.0",
"base64": "ae94bf9621a3863167bead0ea2f3d1bb137c5e1dc65bfd73745113a18d0edb9b",
"binary": "509ba3f2d5d167b05ae9258ac27c48e9ff16820b3beca5600ceab6af8b206a94"
},
"org.standardnotes.simple-markdown-editor": {
"version": "1.4.0",
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions public/components/org.standardnotes.plus-editor/package.json
@@ -1,9 +1,9 @@
{
"name": "sn-plus-editor",
"version": "1.5.0",
"version": "1.6.0",
"description": "A rich text editor for Standard Notes",
"main": "dist/dist.js",
"author": "Standard Notes <hello@standardnotes.org>",
"author": "Standard Notes <hello@standardnotes.com>",
"scripts": {
"lint": "eslint --ext .js .",
"build": "webpack --config webpack.prod.js",
Expand Down

0 comments on commit 063c3b2

Please sign in to comment.