import { countTag } from '../domain/field-types';
|
import { parseTag } from '../domain/tags';
|
import type { FieldDefinition } from '../domain/types';
|
import { disableTrackRevisionsCommand, insertTypedControlCommand, listContentControlsCommand, writeTextDefaultCommand } from './commands';
|
import type { AscGlobal, OfficeContentControl, OnlyOfficeCommand } from './types';
|
|
interface CommandResult {
|
controls?: OfficeContentControl[];
|
error?: string;
|
ok: boolean;
|
}
|
|
interface LifecycleCallbacks {
|
onReady: () => void;
|
onThemeChanged: (theme: unknown) => void;
|
}
|
|
interface LifecycleBinding {
|
callbacks: LifecycleCallbacks;
|
}
|
|
interface LifecycleState {
|
bindings: LifecycleBinding[];
|
button: NonNullable<AscGlobal['plugin']['button']>;
|
init: NonNullable<AscGlobal['plugin']['init']>;
|
onThemeChanged: NonNullable<AscGlobal['plugin']['onThemeChanged']>;
|
originalButton: AscGlobal['plugin']['button'];
|
originalInit: AscGlobal['plugin']['init'];
|
originalThemeChanged: AscGlobal['plugin']['onThemeChanged'];
|
}
|
|
export interface OnlyOfficeBridge {
|
bindLifecycle(callbacks: LifecycleCallbacks): () => void;
|
close(): void;
|
deleteControl(internalId: string): Promise<OfficeContentControl[]>;
|
disableTrackRevisions(): Promise<void>;
|
getRuntimeOptions(): unknown;
|
insertField(definition: FieldDefinition): Promise<OfficeContentControl[]>;
|
listControls(): Promise<OfficeContentControl[]>;
|
selectControl(internalId: string): Promise<void>;
|
}
|
|
const lifecycleStates = new WeakMap<AscGlobal, LifecycleState>();
|
|
function toError(error: unknown): Error {
|
return error instanceof Error ? error : new Error(String(error));
|
}
|
|
function isCommandResult(value: unknown): value is CommandResult {
|
return typeof value === 'object' && value !== null && !Array.isArray(value) && 'ok' in value && typeof value.ok === 'boolean';
|
}
|
|
function bindLifecycleHandlers(asc: AscGlobal, callbacks: LifecycleCallbacks): () => void {
|
let state = lifecycleStates.get(asc);
|
|
if (!state) {
|
const bindings: LifecycleBinding[] = [];
|
const currentBinding = () => bindings[bindings.length - 1];
|
state = {
|
bindings,
|
originalInit: asc.plugin.init,
|
originalButton: asc.plugin.button,
|
originalThemeChanged: asc.plugin.onThemeChanged,
|
init: () => currentBinding()?.callbacks.onReady(),
|
button: (id) => {
|
if (id === -1 && currentBinding()) {
|
asc.plugin.executeCommand('close', '');
|
}
|
},
|
onThemeChanged: (theme) => {
|
if (currentBinding()) {
|
asc.plugin.onThemeChangedBase(theme);
|
currentBinding()?.callbacks.onThemeChanged(theme);
|
}
|
},
|
};
|
lifecycleStates.set(asc, state);
|
asc.plugin.init = state.init;
|
asc.plugin.button = state.button;
|
asc.plugin.onThemeChanged = state.onThemeChanged;
|
}
|
|
const binding = { callbacks };
|
state.bindings.push(binding);
|
let active = true;
|
|
return () => {
|
if (!active) {
|
return;
|
}
|
active = false;
|
const index = state.bindings.indexOf(binding);
|
if (index >= 0) {
|
state.bindings.splice(index, 1);
|
}
|
if (state.bindings.length > 0) {
|
return;
|
}
|
|
if (asc.plugin.init === state.init) {
|
asc.plugin.init = state.originalInit;
|
}
|
if (asc.plugin.button === state.button) {
|
asc.plugin.button = state.originalButton;
|
}
|
if (asc.plugin.onThemeChanged === state.onThemeChanged) {
|
asc.plugin.onThemeChanged = state.originalThemeChanged;
|
}
|
if (lifecycleStates.get(asc) === state) {
|
lifecycleStates.delete(asc);
|
}
|
};
|
}
|
|
export function createOnlyOfficeBridge(asc: AscGlobal): OnlyOfficeBridge {
|
function executeMethod(name: string, args: unknown[] = []): Promise<unknown> {
|
return new Promise((resolve, reject) => {
|
try {
|
asc.plugin.executeMethod(name, args, resolve);
|
} catch (error: unknown) {
|
reject(toError(error));
|
}
|
});
|
}
|
|
function executeCommand(command: OnlyOfficeCommand, scopeKey: string, payload: unknown): Promise<CommandResult> {
|
return new Promise((resolve, reject) => {
|
const clearScope = () => {
|
if (asc.scope[scopeKey] === payload) {
|
delete asc.scope[scopeKey];
|
}
|
};
|
|
try {
|
asc.scope[scopeKey] = payload;
|
asc.plugin.callCommand(command, false, true, (raw) => {
|
try {
|
const result: unknown = typeof raw === 'string' ? JSON.parse(raw) : raw;
|
|
if (!isCommandResult(result)) {
|
reject(new Error('编辑器命令返回无效结果'));
|
return;
|
}
|
|
if (!result.ok) {
|
reject(new Error(typeof result.error === 'string' ? result.error : '编辑器命令执行失败'));
|
return;
|
}
|
|
resolve(result);
|
} catch {
|
reject(new Error('编辑器命令返回无效结果'));
|
} finally {
|
clearScope();
|
}
|
});
|
} catch (error: unknown) {
|
clearScope();
|
reject(toError(error));
|
}
|
});
|
}
|
|
async function readControls(errorMessage: string): Promise<OfficeContentControl[]> {
|
const result = await executeCommand(listContentControlsCommand, 'elnTemplateList', {});
|
|
if (!Array.isArray(result.controls)) {
|
throw new Error(errorMessage);
|
}
|
|
return result.controls;
|
}
|
|
function listControls(): Promise<OfficeContentControl[]> {
|
return readControls('编辑器未返回内容控件列表');
|
}
|
|
async function disableTrackRevisions(): Promise<void> {
|
await executeCommand(disableTrackRevisionsCommand, 'elnTemplateReview', {});
|
}
|
|
async function insertField(definition: FieldDefinition): Promise<OfficeContentControl[]> {
|
const before = await listControls();
|
const beforeCount = countTag(before, definition.tag);
|
|
if (beforeCount > 0) {
|
throw new Error('字段 ID 已存在');
|
}
|
|
const alias = definition.alias.trim();
|
if (before.some((control) => parseTag(control.Tag) && (control.Alias || '').trim() === alias)) {
|
throw new Error('字段名称已存在');
|
}
|
|
if (definition.controlType === 'text') {
|
const created = (await executeMethod('AddContentControl', [
|
2,
|
{
|
Tag: definition.tag,
|
Alias: definition.alias,
|
Lock: 3,
|
PlaceHolderText: definition.placeholder,
|
Appearance: 1,
|
Color: definition.color,
|
},
|
])) as OfficeContentControl | null | undefined;
|
|
if (!created?.InternalId) {
|
throw new Error('编辑器未返回新内容控件');
|
}
|
|
if (definition.defaultValue) {
|
try {
|
await executeCommand(writeTextDefaultCommand, 'elnTemplateTextDefault', {
|
tag: definition.tag,
|
value: definition.defaultValue,
|
});
|
} catch (error: unknown) {
|
throw new Error(`控件已创建,但默认值写入失败:${toError(error).message}`);
|
}
|
}
|
} else {
|
const isDropdown = definition.controlType === 'dropdown';
|
await executeCommand(insertTypedControlCommand, 'elnTemplateInsert', {
|
controlType: definition.controlType,
|
tag: definition.tag,
|
alias: definition.alias,
|
placeholder: definition.placeholder,
|
defaultValue: isDropdown ? '' : definition.defaultValue,
|
defaultChecked: isDropdown ? false : definition.defaultChecked,
|
options: isDropdown ? definition.options : [],
|
selectedIndex: isDropdown ? definition.selectedIndex : -1,
|
color: definition.color,
|
});
|
}
|
|
const after = await readControls('插入后无法读取内容控件列表');
|
if (countTag(after, definition.tag) !== beforeCount + 1) {
|
throw new Error('插入后 Tag 计数未增加');
|
}
|
|
return after;
|
}
|
|
async function selectControl(internalId: string): Promise<void> {
|
if (!internalId) {
|
throw new Error('该内容控件没有可用的内部 ID');
|
}
|
|
const controls = await listControls();
|
if (!controls.some((control) => control.InternalId === internalId)) {
|
throw new Error('定位目标已不存在,请刷新字段列表');
|
}
|
|
const result = await executeMethod('SelectContentControl', [internalId]);
|
if (result === false) {
|
throw new Error('编辑器未能定位字段');
|
}
|
}
|
|
async function deleteControl(internalId: string): Promise<OfficeContentControl[]> {
|
if (!internalId) {
|
throw new Error('该内容控件没有可用的内部 ID');
|
}
|
|
const before = await listControls();
|
if (!before.some((control) => control.InternalId === internalId)) {
|
throw new Error('删除目标已不存在,请刷新字段列表');
|
}
|
|
if ((await executeMethod('RemoveContentControl', [internalId])) === false) {
|
throw new Error('编辑器未能移除字段控件');
|
}
|
|
const after = await readControls('解除后无法读取内容控件列表');
|
if (after.some((control) => control.InternalId === internalId)) {
|
throw new Error('解除后字段仍然存在');
|
}
|
return after;
|
}
|
|
function close(): void {
|
asc.plugin.executeCommand('close', '');
|
}
|
|
function getRuntimeOptions(): unknown {
|
return asc.plugin.info?.options;
|
}
|
|
function bindLifecycle(callbacks: LifecycleCallbacks): () => void {
|
return bindLifecycleHandlers(asc, callbacks);
|
}
|
|
return { bindLifecycle, close, deleteControl, disableTrackRevisions, getRuntimeOptions, insertField, listControls, selectControl };
|
}
|