import type { DocumentField, FieldApplyResult, FieldChange } from '../domain/types';
|
import { applyFieldsCommand, cloneRepeatRowCommand, inspectFieldsCommand, removeRepeatRowCommand } from './commands';
|
import type { ApplyResult, AscGlobal, InspectResult, RepeatCloneResult } from './types';
|
|
export interface EditorControlEvent {
|
InternalId?: unknown;
|
Tag?: unknown;
|
}
|
|
interface LifecycleCallbacks {
|
onControlChanged: (control: EditorControlEvent) => void;
|
onControlFocused: (control: EditorControlEvent) => void;
|
onReady: () => void;
|
onThemeChanged: (theme: unknown) => void;
|
}
|
|
export interface OnlyOfficeBridge {
|
applyFields(changes: FieldChange[]): Promise<FieldApplyResult[]>;
|
appendRepeatRow(groupCode: string, rowId: string, beforeRowId?: string): Promise<string[]>;
|
bindLifecycle(callbacks: LifecycleCallbacks): () => void;
|
getRuntimeOptions(): unknown;
|
inspectFields(): Promise<DocumentField[]>;
|
removeRepeatRow(groupCode: string, rowId: string): Promise<string[]>;
|
selectControl(internalId: string): Promise<void>;
|
setFillOnlyMode(enabled: boolean): Promise<void>;
|
}
|
|
function parseResult<T>(raw: unknown): T {
|
const result: unknown = typeof raw === 'string' ? JSON.parse(raw) : raw;
|
if (typeof result !== 'object' || result === null || Array.isArray(result) || !('ok' in result) || typeof result.ok !== 'boolean') {
|
throw new Error('编辑器命令返回无效结果');
|
}
|
return result as T;
|
}
|
|
export function createOnlyOfficeBridge(asc: AscGlobal): OnlyOfficeBridge {
|
let fillOnlyMode = false;
|
let documentCommandQueue: Promise<void> = Promise.resolve();
|
|
function executeMethod(name: string, args: unknown[] = []): Promise<unknown> {
|
return new Promise((resolve, reject) => {
|
try {
|
asc.plugin.executeMethod(name, args, resolve);
|
} catch (error) {
|
reject(error);
|
}
|
});
|
}
|
|
function enqueueDocumentCommand<T>(operation: () => Promise<T>): Promise<T> {
|
const result = documentCommandQueue.then(operation, operation);
|
documentCommandQueue = result.then(() => undefined, () => undefined);
|
return result;
|
}
|
|
async function applyFillOnlyRestriction(): Promise<void> {
|
try {
|
await executeMethod('SetEditingRestrictions', ['forms']);
|
} catch (error) {
|
try {
|
await executeMethod('SetEditingRestrictions', ['readOnly']);
|
} catch {
|
// Preserve the first failure; read-only is a best-effort fail-closed fallback.
|
}
|
throw error;
|
}
|
}
|
|
function callCommandRaw<T>(command: () => string, scopeKey: string, payload: unknown): Promise<T> {
|
return new Promise((resolve, reject) => {
|
asc.scope[scopeKey] = payload;
|
try {
|
asc.plugin.callCommand(command, false, true, (raw) => {
|
try { resolve(parseResult<T>(raw)); } catch (error) { reject(error); }
|
finally { delete asc.scope[scopeKey]; }
|
});
|
} catch (error) {
|
delete asc.scope[scopeKey];
|
reject(error);
|
}
|
});
|
}
|
|
function callCommand<T>(command: () => string, scopeKey: string, payload: unknown): Promise<T> {
|
if (!fillOnlyMode) return callCommandRaw<T>(command, scopeKey, payload);
|
return enqueueDocumentCommand(async () => {
|
await executeMethod('SetEditingRestrictions', ['none']);
|
try {
|
return await callCommandRaw<T>(command, scopeKey, payload);
|
} finally {
|
await applyFillOnlyRestriction();
|
}
|
});
|
}
|
|
async function inspectFields(): Promise<DocumentField[]> {
|
const result = await callCommand<InspectResult>(inspectFieldsCommand, 'elnRecordFillInspect', {});
|
if (!result.ok) throw new Error(result.error || '读取内容控件失败');
|
return result.fields;
|
}
|
|
async function applyFields(changes: FieldChange[]): Promise<FieldApplyResult[]> {
|
const result = await callCommand<ApplyResult>(applyFieldsCommand, 'elnRecordFillChanges', changes);
|
if (!result.ok) throw new Error(result.error || '应用字段失败');
|
return result.results;
|
}
|
|
async function appendRepeatRow(groupCode: string, rowId: string, beforeRowId?: string): Promise<string[]> {
|
const result = await callCommand<RepeatCloneResult>(cloneRepeatRowCommand, 'elnRecordFillRepeatRow', {
|
...(beforeRowId ? { beforeRowId } : {}), groupCode, rowId,
|
});
|
if (!result.ok || !Array.isArray(result.tags)) throw new Error(result.error || '新增文档重复行失败');
|
return result.tags;
|
}
|
|
async function removeRepeatRow(groupCode: string, rowId: string): Promise<string[]> {
|
const result = await callCommand<RepeatCloneResult>(removeRepeatRowCommand, 'elnRecordFillRepeatRow', { groupCode, rowId });
|
if (!result.ok || !Array.isArray(result.tags)) throw new Error(result.error || '删除文档重复行失败');
|
return result.tags;
|
}
|
|
async function selectControl(internalId: string): Promise<void> {
|
if (!internalId) throw new Error('字段控件缺少内部 ID');
|
if (await executeMethod('SelectContentControl', [internalId]) === false) {
|
throw new Error('编辑器未能定位字段');
|
}
|
}
|
|
function bindLifecycle(callbacks: LifecycleCallbacks): () => void {
|
const originalInit = asc.plugin.init;
|
const originalTheme = asc.plugin.onThemeChanged;
|
const changeEvent = 'onChangeContentControl';
|
const focusEvent = 'onFocusContentControl';
|
const onControlChanged = (value: unknown) => callbacks.onControlChanged(
|
typeof value === 'object' && value !== null ? value as EditorControlEvent : {},
|
);
|
const onControlFocused = (value: unknown) => callbacks.onControlFocused(
|
typeof value === 'object' && value !== null ? value as EditorControlEvent : {},
|
);
|
let eventsAttached = false;
|
const onReady = () => {
|
if (!eventsAttached) {
|
asc.plugin.attachEditorEvent?.(changeEvent, onControlChanged);
|
asc.plugin.attachEditorEvent?.(focusEvent, onControlFocused);
|
eventsAttached = true;
|
}
|
callbacks.onReady();
|
};
|
asc.plugin.init = onReady;
|
asc.plugin.onThemeChanged = (theme) => { asc.plugin.onThemeChangedBase(theme); callbacks.onThemeChanged(theme); };
|
return () => {
|
if (asc.plugin.init === onReady) asc.plugin.init = originalInit;
|
asc.plugin.onThemeChanged = originalTheme;
|
if (eventsAttached) {
|
asc.plugin.detachEditorEvent?.(changeEvent);
|
asc.plugin.detachEditorEvent?.(focusEvent);
|
}
|
};
|
}
|
|
return {
|
applyFields,
|
appendRepeatRow,
|
bindLifecycle,
|
getRuntimeOptions: () => asc.plugin.info?.options,
|
inspectFields,
|
removeRepeatRow,
|
selectControl,
|
setFillOnlyMode: (enabled) => enqueueDocumentCommand(async () => {
|
fillOnlyMode = enabled;
|
if (enabled) await applyFillOnlyRestriction();
|
else await executeMethod('SetEditingRestrictions', ['none']);
|
}),
|
};
|
}
|