Skip to content

Commit

Permalink
Replace editor <textarea> with CodeMirror v6
Browse files Browse the repository at this point in the history
  • Loading branch information
eemeli committed May 30, 2023
1 parent ea082d6 commit 57a7927
Show file tree
Hide file tree
Showing 19 changed files with 644 additions and 400 deletions.
92 changes: 92 additions & 0 deletions package-lock.json

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

5 changes: 5 additions & 0 deletions translate/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@
"name": "translate",
"private": true,
"dependencies": {
"@codemirror/autocomplete": "^6.7.1",
"@codemirror/commands": "^6.2.4",
"@codemirror/language": "^6.7.0",
"@codemirror/state": "^6.2.1",
"@codemirror/view": "^6.12.0",
"@fluent/bundle": "^0.18.0",
"@fluent/langneg": "^0.7.0",
"@fluent/react": "^0.15.1",
Expand Down
2 changes: 1 addition & 1 deletion translate/rollup.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import css from 'rollup-plugin-css-only';
/** @type {import('rollup').RollupOptions} */
const config = {
input: 'src/index.tsx',
output: { file: 'dist/translate.js' },
output: { file: 'dist/translate.js', format: 'iife' },

treeshake: 'recommended',

Expand Down
19 changes: 6 additions & 13 deletions translate/src/context/Editor.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -219,10 +219,9 @@ describe('<EditorProvider>', () => {
});

it('clears a rich Fluent value', () => {
let editor, result, actions;
let editor, actions;
const Spy = () => {
editor = useContext(EditorData);
result = useContext(EditorResult);
actions = useContext(EditorActions);
return null;
};
Expand Down Expand Up @@ -256,10 +255,6 @@ describe('<EditorProvider>', () => {
},
],
});
expect(result).toMatchObject([
{ name: '', keys: [{ type: 'nmtoken', value: 'one' }], value: '' },
{ name: '', keys: [{ type: '*', value: 'other' }], value: '' },
]);
});

it('sets editor from history', () => {
Expand All @@ -272,8 +267,6 @@ describe('<EditorProvider>', () => {
};
const wrapper = mountSpy(Spy, 'ftl', `key = VALUE\n`);

expect(result).toMatchObject([{ name: '', keys: [], value: 'VALUE' }]);

const source = ftl`
key =
{ $var ->
Expand Down Expand Up @@ -304,8 +297,8 @@ describe('<EditorProvider>', () => {
],
});
expect(result).toMatchObject([
{ name: '', keys: [{ type: 'nmtoken', value: 'one' }], value: 'ONE' },
{ name: '', keys: [{ type: '*', value: 'other' }], value: 'OTHER' },
{ keys: [{ type: 'nmtoken', value: 'one' }], name: '', value: 'ONE' },
{ keys: [{ type: '*', value: 'other' }], name: '', value: 'OTHER' },
]);
});

Expand Down Expand Up @@ -340,15 +333,15 @@ describe('<EditorProvider>', () => {
},
],
});
expect(result).toMatchObject([{ name: '', keys: [], value: source }]);
expect(result).toMatchObject([{ keys: [], name: '', value: source }]);

act(() => actions.toggleSourceView());
wrapper.update();

expect(editor).toMatchObject({ fields: [{}, {}], sourceView: false });
expect(result).toMatchObject([
{ name: '', keys: [{ type: 'nmtoken', value: 'one' }], value: 'ONE' },
{ name: '', keys: [{ type: '*', value: 'other' }], value: 'OTHER' },
{ keys: [{ type: 'nmtoken', value: 'one' }], name: '', value: 'ONE' },
{ keys: [{ type: '*', value: 'other' }], name: '', value: 'OTHER' },
]);
});
});
53 changes: 30 additions & 23 deletions translate/src/context/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ import { UnsavedActions } from './UnsavedChanges';

export type EditFieldHandle = {
get value(): string;
set value(next: string);
focus(): void;
setSelection(content: string): void;
setSelection(text: string): void;
setValue(text: string): void;
};

export type EditorField = {
Expand All @@ -51,6 +51,12 @@ export type EditorField = {
};

export type EditorData = Readonly<{
/**
* Should match `useContext(EntityView).pk`.
* If it doesn't, the entity has changed but data isn't updated yet.
*/
pk: number;

/** Is a request to send a new translation running? */
busy: boolean;

Expand Down Expand Up @@ -100,9 +106,6 @@ export type EditorActions = {
/** If `format: 'ftl'`, must be called with the source of a full entry */
setEditorFromHistory(value: string): void;

/** Set the result value of the active input */
setEditorFromInput(idx: number, value: string): void;

/** @param manual Set `true` when value set due to direct user action */
setEditorFromHelpers(
value: string,
Expand All @@ -112,6 +115,9 @@ export type EditorActions = {

setEditorSelection(content: string): void;

/** Set the result value of the active input */
setResultFromInput(idx: number, value: string): void;

toggleSourceView(): void;
};

Expand All @@ -137,6 +143,7 @@ const createSimpleMessageEntry = (id: string, value: string): MessageEntry => ({
});

const initEditorData: EditorData = {
pk: 0,
busy: false,
entry: { id: '', value: null, attributes: new Map() },
focusField: { current: null },
Expand All @@ -151,8 +158,8 @@ const initEditorActions: EditorActions = {
setEditorBusy: () => {},
setEditorFromHelpers: () => {},
setEditorFromHistory: () => {},
setEditorFromInput: () => {},
setEditorSelection: () => {},
setResultFromInput: () => {},
toggleSourceView: () => {},
};

Expand Down Expand Up @@ -188,13 +195,10 @@ export function EditorProvider({ children }: { children: React.ReactElement }) {
clearEditor() {
setState((state) => {
for (const field of state.fields) {
field.handle.current.value = '';
field.handle.current.setValue('');
}
return state;
});
setResult((result) =>
result.map(({ name, keys }) => ({ name, keys, value: '' })),
);
},

setEditorBusy: (busy) =>
Expand All @@ -204,15 +208,14 @@ export function EditorProvider({ children }: { children: React.ReactElement }) {
setState((prev) => {
const { fields, focusField, sourceView } = prev;
const field = focusField.current ?? fields[0];
field.handle.current.value = str;
field.handle.current.setValue(str);
let next = fields.slice();
let result = buildResult(next);
if (sourceView) {
const result = buildResult(next);
next = editSource(buildMessageEntry(prev.entry, result));
focusField.current = next[0];
result = buildResult(next);
setResult(result);
}
setResult(result);
return {
...prev,
machinery: { manual, translation: str, sources },
Expand All @@ -238,29 +241,32 @@ export function EditorProvider({ children }: { children: React.ReactElement }) {
}
} else {
next.fields = editMessageEntry(prev.initial);
next.fields[0].handle.current.value = str;
next.fields[0].handle.current.setValue(str);
}
next.focusField.current = next.fields[0];
setResult(buildResult(next.fields));
return next;
}),

setEditorFromInput: (idx, value) =>
setResult((prev) => {
const res = prev.slice();
res[idx] = { ...res[idx], value };
return res;
}),

setEditorSelection: (content) =>
setState((state) => {
const { fields, focusField } = state;
const field = focusField.current ?? fields[0];
field.handle.current.setSelection(content);
setResult(buildResult(fields));
return state;
}),

setResultFromInput: (idx, value) =>
setResult((prev) => {
if (prev.length > idx) {
const res = prev.slice();
res[idx] = { ...res[idx], value };
return res;
} else {
return prev;
}
}),

toggleSourceView: () =>
setState((prev) => {
if (prev.sourceView) {
Expand Down Expand Up @@ -325,6 +331,7 @@ export function EditorProvider({ children }: { children: React.ReactElement }) {
const fields = sourceView ? editSource(source) : editMessageEntry(entry);

setState(() => ({
pk: entity.pk,
busy: false,
entry,
fields,
Expand Down
11 changes: 6 additions & 5 deletions translate/src/context/UnsavedChanges.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ export type UnsavedChanges = Readonly<{
}>;

export type UnsavedActions = {
/**
* The `callback` is called as `setTimout(callback)`
* to avoid an occasional React complaint about
* updating one component while rendering a different component.
*/
checkUnsavedChanges(callback: () => void): void;

/**
Expand Down Expand Up @@ -50,7 +55,7 @@ export function UnsavedChangesProvider({
if (prev.check()) {
return { check: () => true, onIgnore: callback };
} else {
callback();
setTimeout(callback);
return prev;
}
}),
Expand All @@ -60,11 +65,7 @@ export function UnsavedChangesProvider({
const { onIgnore } = prev;
if (onIgnore) {
if (ignore) {
// Needs to happen after the return to avoid an occasional React
// complaint about updating one component while rendering a
// different component.
setTimeout(onIgnore);

return { check: () => false, onIgnore: null };
}
return { ...prev, onIgnore: null };
Expand Down
Loading

0 comments on commit 57a7927

Please sign in to comment.