diff --git a/package-lock.json b/package-lock.json index bb85c1f458..ac64aac1d7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1912,62 +1912,6 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, - "node_modules/@codemirror/autocomplete": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.7.1.tgz", - "integrity": "sha512-hSxf9S0uB+GV+gBsjY1FZNo53e1FFdzPceRfCfD1gWOnV6o21GfB5J5Wg9G/4h76XZMPrF0A6OCK/Rz5+V1egg==", - "dependencies": { - "@codemirror/language": "^6.0.0", - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.6.0", - "@lezer/common": "^1.0.0" - }, - "peerDependencies": { - "@codemirror/language": "^6.0.0", - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0", - "@lezer/common": "^1.0.0" - } - }, - "node_modules/@codemirror/commands": { - "version": "6.2.4", - "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.2.4.tgz", - "integrity": "sha512-42lmDqVH0ttfilLShReLXsDfASKLXzfyC36bzwcqzox9PlHulMcsUOfHXNo2X2aFMVNUoQ7j+d4q5bnfseYoOA==", - "dependencies": { - "@codemirror/language": "^6.0.0", - "@codemirror/state": "^6.2.0", - "@codemirror/view": "^6.0.0", - "@lezer/common": "^1.0.0" - } - }, - "node_modules/@codemirror/language": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.7.0.tgz", - "integrity": "sha512-4SMwe6Fwn57klCUsVN0y4/h/iWT+XIXFEmop2lIHHuWO0ubjCrF3suqSZLyOQlznxkNnNbOOfKe5HQbQGCAmTg==", - "dependencies": { - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0", - "@lezer/common": "^1.0.0", - "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.0.0", - "style-mod": "^4.0.0" - } - }, - "node_modules/@codemirror/state": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.2.1.tgz", - "integrity": "sha512-RupHSZ8+OjNT38zU9fKH2sv+Dnlr8Eb8sl4NOnnqz95mCFTZUaiRP8Xv5MeeaG0px2b8Bnfe7YGwCV3nsBhbuw==" - }, - "node_modules/@codemirror/view": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.12.0.tgz", - "integrity": "sha512-xNHvbJBc2v8JuEcIGOck6EUGShpP+TYGCEMVEVQMYxbFXfMhYnoF3znxB/2GgeKR0nrxBs+nhBupiTYQqCp2kw==", - "dependencies": { - "@codemirror/state": "^6.1.4", - "style-mod": "^4.0.0", - "w3c-keyname": "^2.2.4" - } - }, "node_modules/@emotion/is-prop-valid": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.1.tgz", @@ -3037,27 +2981,6 @@ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==" }, - "node_modules/@lezer/common": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.0.2.tgz", - "integrity": "sha512-SVgiGtMnMnW3ActR8SXgsDhw7a0w0ChHSYAyAUxxrOiJ1OqYWEKk/xJd84tTSPo1mo6DXLObAJALNnd0Hrv7Ng==" - }, - "node_modules/@lezer/highlight": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.1.6.tgz", - "integrity": "sha512-cmSJYa2us+r3SePpRCjN5ymCqCPv+zyXmDl0ciWtVaNiORT/MxM7ZgOMQZADD0o51qOaOg24qc/zBViOIwAjJg==", - "dependencies": { - "@lezer/common": "^1.0.0" - } - }, - "node_modules/@lezer/lr": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.3.5.tgz", - "integrity": "sha512-Kye0rxYBi+OdToLUN2tQfeH5VIrpESC6XznuvxmIxbO1lz6M1C90vkjMNYoX1SfbUcuvoPXvLYsBquZ//77zVQ==", - "dependencies": { - "@lezer/common": "^1.0.0" - } - }, "node_modules/@messageformat/fluent": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@messageformat/fluent/-/fluent-0.4.1.tgz", @@ -12059,11 +11982,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/style-mod": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.0.3.tgz", - "integrity": "sha512-78Jv8kYJdjbvRwwijtCevYADfsI0lGzYJe4mMFdceO8l75DFFDoqBhR1jVDicDRRaX4//g1u9wKeo+ztc2h1Rw==" - }, "node_modules/style-to-js": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.1.tgz", @@ -12688,11 +12606,6 @@ "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz", "integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==" }, - "node_modules/w3c-keyname": { - "version": "2.2.7", - "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.7.tgz", - "integrity": "sha512-XB8aa62d4rrVfoZYQaYNy3fy+z4nrfy2ooea3/0BnBzXW0tSdZ+lRgjzBZhk0La0H6h8fVyYCxx/qkQcAIuvfg==" - }, "node_modules/w3c-xmlserializer": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", @@ -13031,16 +12944,10 @@ }, "translate": { "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", "@fluent/syntax": "^0.19.0", - "@lezer/highlight": "^1.1.6", "@messageformat/fluent": "^0.4.1", "@reduxjs/toolkit": "^1.6.1", "classnames": "^2.3.1", diff --git a/translate/.eslintrc.js b/translate/.eslintrc.js index 946aa041c5..c53b62c4b8 100644 --- a/translate/.eslintrc.js +++ b/translate/.eslintrc.js @@ -19,7 +19,6 @@ module.exports = { '@typescript-eslint/no-empty-function': 0, '@typescript-eslint/no-explicit-any': 0, '@typescript-eslint/no-inferrable-types': 0, - '@typescript-eslint/no-unused-vars': ['error', { varsIgnorePattern: '^_' }], '@typescript-eslint/prefer-as-const': 0, 'import/no-default-export': 'error', }, diff --git a/translate/package.json b/translate/package.json index 426127c0ad..95fce8e8b2 100644 --- a/translate/package.json +++ b/translate/package.json @@ -2,16 +2,10 @@ "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", "@fluent/syntax": "^0.19.0", - "@lezer/highlight": "^1.1.6", "@messageformat/fluent": "^0.4.1", "@reduxjs/toolkit": "^1.6.1", "classnames": "^2.3.1", diff --git a/translate/rollup.config.mjs b/translate/rollup.config.mjs index 4e242af95b..d60e8e6764 100644 --- a/translate/rollup.config.mjs +++ b/translate/rollup.config.mjs @@ -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', format: 'iife' }, + output: { file: 'dist/translate.js' }, treeshake: 'recommended', diff --git a/translate/src/context/Editor.test.js b/translate/src/context/Editor.test.js index 6a85900ac1..b3f418f0f8 100644 --- a/translate/src/context/Editor.test.js +++ b/translate/src/context/Editor.test.js @@ -6,12 +6,7 @@ import { act } from 'react-dom/test-utils'; import { createReduxStore, mountComponentWithStore } from '~/test/store'; import { editMessageEntry, parseEntry } from '~/utils/message'; -import { - EditorActions, - EditorData, - EditorProvider, - EditorResult, -} from './Editor'; +import { EditorActions, EditorData, EditorProvider } from './Editor'; import { EntityView, EntityViewProvider } from './EntityView'; import { Locale } from './Locale'; import { Location, LocationProvider } from './Location'; @@ -73,64 +68,37 @@ function mountSpy(Spy, format, translation) { describe('', () => { it('provides a simple non-Fluent value', () => { - let editor, result; + let editor; const Spy = () => { editor = useContext(EditorData); - result = useContext(EditorResult); return null; }; mountSpy(Spy, 'simple', 'message'); expect(editor).toMatchObject({ sourceView: false, - initial: { - id: 'key', - value: { pattern: { body: [{ type: 'text', value: 'message' }] } }, - }, - fields: [ - { - id: '', - keys: [], - labels: [], - name: '', - handle: { current: { value: 'message' } }, - }, - ], + initial: [{ id: '', keys: [], labels: [], name: '', value: 'message' }], + value: [{ id: '', keys: [], labels: [], name: '', value: 'message' }], }); - expect(result).toMatchObject([{ name: '', keys: [], value: 'message' }]); }); it('provides a simple Fluent value', () => { - let editor, result; + let editor; const Spy = () => { editor = useContext(EditorData); - result = useContext(EditorResult); return null; }; mountSpy(Spy, 'ftl', 'key = message'); expect(editor).toMatchObject({ sourceView: false, - initial: { - id: 'key', - value: { pattern: { body: [{ type: 'text', value: 'message' }] } }, - }, - fields: [ - { - id: '', - keys: [], - labels: [], - name: '', - handle: { current: { value: 'message' } }, - }, - ], + initial: [{ id: '', keys: [], labels: [], name: '', value: 'message' }], + value: [{ id: '', keys: [], labels: [], name: '', value: 'message' }], }); - expect(result).toMatchObject([{ name: '', keys: [], value: 'message' }]); }); it('provides a rich Fluent value', () => { - let editor, result; + let editor; const Spy = () => { editor = useContext(EditorData); - result = useContext(EditorResult); return null; }; const source = ftl` @@ -143,23 +111,14 @@ describe('', () => { `; mountSpy(Spy, 'ftl', source); - const entry = parseEntry(source); - const fields = editMessageEntry(parseEntry(source)).map((field) => ({ - ...field, - handle: { current: { value: field.handle.current.value } }, - })); - expect(editor).toMatchObject({ sourceView: false, initial: entry, fields }); - expect(result).toMatchObject([ - { name: '', keys: [{ type: 'nmtoken', value: 'one' }], value: 'ONE' }, - { name: '', keys: [{ type: '*', value: 'other' }], value: 'OTHER' }, - ]); + const value = editMessageEntry(parseEntry(source)); + expect(editor).toMatchObject({ sourceView: false, initial: value, value }); }); it('provides a forced source Fluent value', () => { - let editor, result; + let editor; const Spy = () => { editor = useContext(EditorData); - result = useContext(EditorResult); return null; }; const source = '## comment\n'; @@ -167,28 +126,19 @@ describe('', () => { expect(editor).toMatchObject({ sourceView: true, - initial: { - id: 'key', - value: { pattern: { body: [{ type: 'text', value: '## comment\n' }] } }, - }, - fields: [ - { - handle: { current: { value: '## comment' } }, - id: '', - keys: [], - labels: [], - name: '', - }, + initial: [ + { id: '', keys: [], labels: [], name: '', value: '## comment' }, ], + value: [{ id: '', keys: [], labels: [], name: '', value: '## comment' }], }); - expect(result).toMatchObject([{ name: '', keys: [], value: '## comment' }]); }); it('updates state on entity and plural form changes', () => { - let editor, result, location, entity; + let editor; + let location; + let entity; const Spy = () => { editor = useContext(EditorData); - result = useContext(EditorResult); location = useContext(Location); entity = useContext(EntityView); return null; @@ -199,27 +149,24 @@ describe('', () => { wrapper.update(); expect(editor).toMatchObject({ - initial: { - value: { pattern: { body: [{ type: 'text', value: 'one' }] } }, - }, - fields: [{ handle: { current: { value: 'one' } } }], + sourceView: false, + initial: [{ id: '', keys: [], labels: [], name: '', value: 'one' }], + value: [{ id: '', keys: [], labels: [], name: '', value: 'one' }], }); - expect(result).toMatchObject([{ value: 'one' }]); act(() => entity.setPluralForm(1)); wrapper.update(); expect(editor).toMatchObject({ - initial: { - value: { pattern: { body: [{ type: 'text', value: 'other' }] } }, - }, - fields: [{ handle: { current: { value: 'other' } } }], + sourceView: false, + initial: [{ id: '', keys: [], labels: [], name: '', value: 'other' }], + value: [{ id: '', keys: [], labels: [], name: '', value: 'other' }], }); - expect(result).toMatchObject([{ value: 'other' }]); }); it('clears a rich Fluent value', () => { - let editor, actions; + let editor; + let actions; const Spy = () => { editor = useContext(EditorData); actions = useContext(EditorActions); @@ -237,36 +184,41 @@ describe('', () => { wrapper.update(); expect(editor).toMatchObject({ + fields: [{ current: null }, { current: null }], sourceView: false, - fields: [ + value: [ { - handle: { current: { value: '' } }, - id: '|one', keys: [{ type: 'nmtoken', value: 'one' }], labels: [{ label: 'one', plural: true }], name: '', + value: '', }, { - handle: { current: { value: '' } }, - id: '|other', keys: [{ type: '*', value: 'other' }], labels: [{ label: 'other', plural: true }], name: '', + value: '', }, ], }); }); it('sets editor from history', () => { - let editor, result, actions; + let editor; + let actions; const Spy = () => { editor = useContext(EditorData); - result = useContext(EditorResult); actions = useContext(EditorActions); return null; }; const wrapper = mountSpy(Spy, 'ftl', `key = VALUE\n`); + expect(editor).toMatchObject({ + fields: [{ current: null }], + sourceView: false, + value: [{ keys: [], labels: [], name: '', value: 'VALUE' }], + }); + const source = ftl` key = { $var -> @@ -278,35 +230,30 @@ describe('', () => { wrapper.update(); expect(editor).toMatchObject({ + fields: [{ current: null }, { current: null }], sourceView: false, - fields: [ + value: [ { - handle: { current: { value: 'ONE' } }, - id: '|one', keys: [{ type: 'nmtoken', value: 'one' }], labels: [{ label: 'one', plural: true }], name: '', + value: 'ONE', }, { - handle: { current: { value: 'OTHER' } }, - id: '|other', keys: [{ type: '*', value: 'other' }], labels: [{ label: 'other', plural: true }], name: '', + value: 'OTHER', }, ], }); - expect(result).toMatchObject([ - { keys: [{ type: 'nmtoken', value: 'one' }], name: '', value: 'ONE' }, - { keys: [{ type: '*', value: 'other' }], name: '', value: 'OTHER' }, - ]); }); it('toggles Fluent source view', () => { - let editor, result, actions; + let editor; + let actions; const Spy = () => { editor = useContext(EditorData); - result = useContext(EditorResult); actions = useContext(EditorActions); return null; }; @@ -322,26 +269,31 @@ describe('', () => { wrapper.update(); expect(editor).toMatchObject({ + fields: [{ current: null }], sourceView: true, - fields: [ - { - handle: { current: { value: source } }, - id: '', - keys: [], - labels: [], - name: '', - }, - ], + value: [{ keys: [], labels: [], name: '', value: source }], }); - expect(result).toMatchObject([{ keys: [], name: '', value: source }]); act(() => actions.toggleSourceView()); wrapper.update(); - expect(editor).toMatchObject({ fields: [{}, {}], sourceView: false }); - expect(result).toMatchObject([ - { keys: [{ type: 'nmtoken', value: 'one' }], name: '', value: 'ONE' }, - { keys: [{ type: '*', value: 'other' }], name: '', value: 'OTHER' }, - ]); + expect(editor).toMatchObject({ + fields: [{ current: null }, { current: null }], + sourceView: false, + value: [ + { + keys: [{ type: 'nmtoken', value: 'one' }], + labels: [{ label: 'one', plural: true }], + name: '', + value: 'ONE', + }, + { + keys: [{ type: '*', value: 'other' }], + labels: [{ label: 'other', plural: true }], + name: '', + value: 'OTHER', + }, + ], + }); }); }); diff --git a/translate/src/context/Editor.tsx b/translate/src/context/Editor.tsx index 54d0419269..28a423d36d 100644 --- a/translate/src/context/Editor.tsx +++ b/translate/src/context/Editor.tsx @@ -8,12 +8,11 @@ import React, { } from 'react'; import type { SourceType } from '~/api/machinery'; -import { useReadonlyEditor } from '~/hooks/useReadonlyEditor'; import { useTranslationStatus } from '~/modules/entities/useTranslationStatus'; +import { useReadonlyEditor } from '~/hooks/useReadonlyEditor'; import { buildMessageEntry, editMessageEntry, - editSource, requiresSourceView, getEmptyMessageEntry, MessageEntry, @@ -26,16 +25,9 @@ import { EntityView, useActiveTranslation } from './EntityView'; import { FailedChecksData } from './FailedChecksData'; import { Locale } from './Locale'; import { MachineryTranslations } from './MachineryTranslations'; -import { UnsavedActions } from './UnsavedChanges'; - -export type EditFieldHandle = { - get value(): string; - focus(): void; - setSelection(text: string): void; - setValue(text: string): void; -}; +import { UnsavedActions, UnsavedChanges } from './UnsavedChanges'; -export type EditorField = { +export type EditorMessage = Array<{ /** An identifier for this field */ id: string; @@ -47,33 +39,82 @@ export type EditorField = { labels: Array<{ label: string; plural: boolean }>; - handle: React.MutableRefObject; -}; - -export type EditorData = Readonly<{ /** - * Should match `useContext(EntityView).pk`. - * If it doesn't, the entity has changed but data isn't updated yet. + * A flattened representation of a single message pattern, + * which may contain syntactic representations of placeholders. */ - pk: number; + value: string; +}>; + +function editSource(source: string | MessageEntry) { + const value = + typeof source === 'string' ? source : serializeEntry('ftl', source); + return [{ id: '', name: '', keys: [], labels: [], value: value.trim() }]; +} + +/** + * Creates a copy of `base` with an entry matching `id` updated to `value`. + * + * @param id If empty, matches first entry of `base`. + * If set, a path split by `|` characters. + */ +function setEditorMessage( + base: EditorMessage, + id: string | null | undefined, + value: string, +): EditorMessage { + let set = false; + return base.map((field) => { + if (!set && (!id || field.id === id)) { + set = true; + return { ...field, value }; + } else { + return field; + } + }); +} + +function parseEntryFromFluentSource(base: MessageEntry, source: string) { + const entry = parseEntry(source); + if (entry) { + entry.id = base.id; + } + return entry; +} + +/** + * Create a new MessageEntry with a simple string pattern `value`, + * using `id` as its identifier. + */ +const createSimpleMessageEntry = (id: string, value: string): MessageEntry => ({ + id, + value: { + type: 'message', + declarations: [], + pattern: { body: [{ type: 'text', value }] }, + }, +}); +export type EditorData = Readonly<{ /** Is a request to send a new translation running? */ busy: boolean; /** Used to reconstruct edited messages */ entry: MessageEntry; - /** Input fields for the value being edited */ - fields: EditorField[]; + /** Editor input components */ + fields: Array< + React.MutableRefObject + >; /** - * The current or most recent field with focus; + * Index in `fields` of the current or most recent field with focus; * used as the target of machinery replacements. */ - focusField: React.MutableRefObject; + focusField: React.MutableRefObject; /** Used for detecting unsaved changes */ - initial: MessageEntry; + initial: EditorMessage; machinery: { manual: boolean; @@ -82,20 +123,9 @@ export type EditorData = Readonly<{ } | null; sourceView: boolean; -}>; - -export type EditorResult = Array<{ - /** Attribute name, or empty for the value */ - name: string; - - /** Selector keys, or empty array for single-pattern messages */ - keys: Variant['keys']; - /** - * A flattened representation of a single message pattern, - * which may contain syntactic representations of placeholders. - */ - value: string; + /** The current value being edited */ + value: EditorMessage; }>; export type EditorActions = { @@ -106,6 +136,9 @@ export type EditorActions = { /** If `format: 'ftl'`, must be called with the source of a full entry */ setEditorFromHistory(value: string): void; + /** For `view: 'rich'`, if `value` is a string, sets the value of the active input */ + setEditorFromInput(value: string | EditorMessage): void; + /** @param manual Set `true` when value set due to direct user action */ setEditorFromHelpers( value: string, @@ -115,42 +148,18 @@ export type EditorActions = { setEditorSelection(content: string): void; - /** Set the result value of the active input */ - setResultFromInput(idx: number, value: string): void; - toggleSourceView(): void; }; -function parseEntryFromFluentSource(base: MessageEntry, source: string) { - const entry = parseEntry(source); - if (entry) { - entry.id = base.id; - } - return entry; -} - -/** - * Create a new MessageEntry with a simple string pattern `value`, - * using `id` as its identifier. - */ -const createSimpleMessageEntry = (id: string, value: string): MessageEntry => ({ - id, - value: { - type: 'message', - declarations: [], - pattern: { body: [{ type: 'text', value }] }, - }, -}); - const initEditorData: EditorData = { - pk: 0, busy: false, entry: { id: '', value: null, attributes: new Map() }, - focusField: { current: null }, - initial: { id: '', value: null, attributes: new Map() }, - machinery: null, fields: [], + focusField: { current: 0 }, + initial: [], + machinery: null, sourceView: false, + value: [], }; const initEditorActions: EditorActions = { @@ -158,22 +167,14 @@ const initEditorActions: EditorActions = { setEditorBusy: () => {}, setEditorFromHelpers: () => {}, setEditorFromHistory: () => {}, + setEditorFromInput: () => {}, setEditorSelection: () => {}, - setResultFromInput: () => {}, toggleSourceView: () => {}, }; export const EditorData = createContext(initEditorData); -export const EditorResult = createContext([]); export const EditorActions = createContext(initEditorActions); -const buildResult = (message: EditorField[]): EditorResult => - message.map(({ handle, keys, name }) => ({ - name, - keys, - value: handle.current.value, - })); - export function EditorProvider({ children }: { children: React.ReactElement }) { const locale = useContext(Locale); const { entity } = useContext(EntityView); @@ -181,44 +182,37 @@ export function EditorProvider({ children }: { children: React.ReactElement }) { const readonly = useReadonlyEditor(); const machinery = useContext(MachineryTranslations); const { setUnsavedChanges } = useContext(UnsavedActions); + const { exist } = useContext(UnsavedChanges); const { resetFailedChecks } = useContext(FailedChecksData); - const [state, setState] = useState(initEditorData); - const [result, setResult] = useState([]); + const [state, setState] = useState(initEditorData); const actions = useMemo(() => { if (readonly) { return initEditorActions; } return { - clearEditor() { - setState((state) => { - for (const field of state.fields) { - field.handle.current.setValue(''); - } - return state; - }); - }, + clearEditor: () => + setState((prev) => { + const empty = prev.value.map((field) => ({ ...field, value: '' })); + return { ...prev, value: empty }; + }), setEditorBusy: (busy) => setState((prev) => (busy === prev.busy ? prev : { ...prev, busy })), setEditorFromHelpers: (str, sources, manual) => setState((prev) => { - const { fields, focusField, sourceView } = prev; - const field = focusField.current ?? fields[0]; - field.handle.current.setValue(str); - let next = fields.slice(); + const { fields, focusField, sourceView, value } = prev; + const input = fields[focusField.current]?.current; + let next = setEditorMessage(value, input?.id, str); if (sourceView) { - const result = buildResult(next); - next = editSource(buildMessageEntry(prev.entry, result)); - focusField.current = next[0]; - setResult(result); + next = editSource(buildMessageEntry(prev.entry, next)); } return { ...prev, machinery: { manual, translation: str, sources }, - fields: next, + value: next, }; }), @@ -231,62 +225,80 @@ export function EditorProvider({ children }: { children: React.ReactElement }) { next.entry = entry; } if (entry && !requiresSourceView(entry)) { - next.fields = prev.sourceView + next.value = prev.sourceView ? editSource(entry) : editMessageEntry(entry); } else { - next.fields = editSource(str); + next.value = editSource(str); next.sourceView = true; } + next.fields = next.value.map(() => ({ current: null })); } else { - next.fields = editMessageEntry(prev.initial); - next.fields[0].handle.current.setValue(str); + next.value = setEditorMessage(prev.initial, null, str); } - next.focusField.current = next.fields[0]; - setResult(buildResult(next.fields)); return next; }), - setEditorSelection: (content) => - setState((state) => { - const { fields, focusField } = state; - const field = focusField.current ?? fields[0]; - field.handle.current.setSelection(content); - return state; + setEditorFromInput: (input) => + setState((prev) => { + if (typeof input === 'string') { + const { fields, focusField, value } = prev; + const field = fields[focusField.current]?.current; + const next = setEditorMessage(value, field?.id, input); + return { ...prev, value: next }; + } else { + return { ...prev, value: input }; + } }), - setResultFromInput: (idx, value) => - setResult((prev) => { - if (prev.length > idx) { - const res = prev.slice(); - res[idx] = { ...res[idx], value }; - return res; + setEditorSelection: (content) => + setState((prev) => { + const { fields, focusField, sourceView, value } = prev; + let next: EditorMessage; + const input = fields[focusField.current]?.current; + if (input) { + input.setRangeText( + content, + input.selectionStart ?? 0, // never actually null for or