# Пользовательские компоненты

> Web SDK предлагает несколько способов интеграции пользовательских компонентов, с помощью которых можно расширять и настраивать его функциональность. В этом руководстве рассматриваются наиболее распространённые способы.

- Human documentation: [https://docs.univer.ai/ru-RU/guides/sheets/ui/components](https://docs.univer.ai/ru-RU/guides/sheets/ui/components)

- Agent Markdown: [https://docs.univer.ai/ru-RU/guides/sheets/ui/components.md](https://docs.univer.ai/ru-RU/guides/sheets/ui/components.md)

- Requested language: `ru-RU`

- Content language: `ru-RU`

- Documentation version: `1.2.0`

- Source: [sheets/ui/components.ru-RU.mdx](https://github.com/dream-num/documentation/blob/dev/content/guides/sheets/ui/components.ru-RU.mdx)

---

Univer не передаёт компоненты напрямую в функции отрисовки в качестве параметров. Чтобы использовать компоненты в различных точках монтирования, сначала зарегистрируйте их в Univer через Facade API.

Если вы не знаете, как получить экземпляр Facade API, обратитесь к разделу [Facade API](https://docs.univer.ai/ru-RU/guides/sheets/getting-started/facade.md).

## Регистрация пользовательских компонентов

```typescript
// [!code word:componentKey]
// [!code word:CustomComponent]
// [!code word:options]
univerAPI.registerComponent(componentKey, CustomComponent, options)
```

Для регистрации пользовательских компонентов используйте метод `univerAPI.registerComponent`. Он принимает три параметра:

* `componentKey`: уникальный идентификатор компонента, по которому к нему можно обращаться в Univer.
* `CustomComponent`: реализация компонента — это может быть компонент React, Vue или Web Component.
* `options`: необязательные параметры конфигурации, с помощью которых можно указать фреймворк, от которого зависит компонент, и другие связанные настройки.

### Компоненты React

Для регистрации компонента React дополнительная настройка не требуется — достаточно убедиться, что это корректный компонент React. Простой пример:

```tsx
function ReactComponent(props: Record<string, any>) {
  return <div>Hello Univer!</div>
}

univerAPI.registerComponent('MyReactComponent', ReactComponent)
```

### Компоненты Vue

#### Vue 3.x

Чтобы зарегистрировать компоненты Vue, установите и зарегистрируйте плагин `UniverVue3AdapterPlugin` из пакета `@univerjs/ui-adapter-vue3`:

#### npm

```bash
npm install @univerjs/ui-adapter-vue3
```

#### pnpm

```bash
pnpm add @univerjs/ui-adapter-vue3
```

#### yarn

```bash
yarn add @univerjs/ui-adapter-vue3
```

#### bun

```bash
bun add @univerjs/ui-adapter-vue3
```

```typescript
import { UniverVue3AdapterPlugin } from '@univerjs/ui-adapter-vue3'

univer.registerPlugin(UniverVue3AdapterPlugin)
```

При регистрации компонентов Vue необходимо задать для параметра `framework` значение `'vue3'`:

```tsx
const Vue3Component = defineComponent({
  setup(props) {
    return () => <div>Hello Univer!</div>
  },
})

univerAPI.registerComponent('MyVue3Component', Vue3Component, {
  // [!code word:vue3]
  framework: 'vue3',
})
```

#### Vue 2.x

Поскольку поддержка Vue 2.x прекращена (EOL) и эта версия больше не сопровождается, сейчас Univer не предоставляет для нее плагин UI-адаптера. Однако вы можете реализовать адаптер Vue 2.x с помощью пользовательского плагина. Ниже приведен пример такого плагина:

```typescript title="ui-adapter-vue2.ts"
import { DependentOn, Inject, Injector, Plugin } from '@univerjs/core'
import { ComponentManager, UniverUIPlugin } from '@univerjs/ui'
import Vue from 'vue'

/**
 * The plugin that allows Univer to use Vue 2 components as UI components.
 */
@DependentOn(UniverUIPlugin)
export class UniverVue2AdapterPlugin extends Plugin {
  static override pluginName = 'UNIVER_UI_VUE2_ADAPTER_PLUGIN'

  constructor(
    private readonly _config = {},
    @Inject(Injector) protected readonly _injector: Injector,
    @Inject(ComponentManager) protected readonly _componentManager: ComponentManager,
  ) {
    super()
  }

  override onStarting(): void {
    const { createElement, useEffect, useRef } = this._componentManager.reactUtils

    this._componentManager.setHandler('vue2', (component: any) => {
      return (props: Record<string, any>) =>
        createElement(VueComponentWrapper, {
          component,
          props: Object.keys(props).reduce<Record<string, any>>((acc, key) => {
            if (key !== 'key') {
              acc[key] = props[key]
            }
            return acc
          }, {}),
          reactUtils: { createElement, useEffect, useRef },
        })
    })
  }
}

export function VueComponentWrapper(options: {
  component: any
  props: Record<string, any>
  reactUtils: typeof ComponentManager.prototype.reactUtils
}) {
  const { component, props, reactUtils } = options
  const { createElement, useEffect, useRef } = reactUtils

  const domRef = useRef<HTMLDivElement>(null)

  useEffect(() => {
    if (!domRef.current) return

    const Constructor = Vue.extend(component)

    const instance = new Constructor({
      data: props,
    })
    instance.$mount()

    domRef.current.appendChild(instance.$el)

    return () => {
      instance.$destroy()
    }
  }, [props])

  return createElement('div', { ref: domRef })
}
```

Затем зарегистрируйте его:

```typescript
import { UniverVue2AdapterPlugin } from './ui-adapter-vue2' // [!code ++]

univer.registerPlugin(UniverUIPlugin)
univer.registerPlugin(UniverVue2AdapterPlugin) // [!code ++]
```

При регистрации компонентов Vue 2.x необходимо задать для параметра `framework` значение `'vue2'`:

```tsx
const Vue2Component = Vue.component('MyVue2Component', {
  template: '<div>Hello, Univer!</div>',
})

univerAPI.registerComponent('MyVue2Component', Vue2Component, {
  // [!code word:vue2]
  framework: 'vue2',
})
```

### Web Components

Чтобы зарегистрировать Web Components, убедитесь, что компонент соответствует стандарту Web Components, а затем установите и зарегистрируйте плагин `UniverWebComponentAdapterPlugin` из пакета `@univerjs/ui-adapter-web-component`:

#### npm

```bash
npm install @univerjs/ui-adapter-web-component
```

#### pnpm

```bash
pnpm add @univerjs/ui-adapter-web-component
```

#### yarn

```bash
yarn add @univerjs/ui-adapter-web-component
```

#### bun

```bash
bun add @univerjs/ui-adapter-web-component
```

```typescript
import { UniverWebComponentAdapterPlugin } from '@univerjs/ui-adapter-web-component'

univer.registerPlugin(UniverWebComponentAdapterPlugin)
```

При регистрации Web Components необходимо задать для параметра `framework` значение `'web-component'`:

```tsx
class WebComponent extends HTMLElement {
  constructor() {
    super()
    const shadow = this.attachShadow({ mode: 'open' })
    const div = document.createElement('div')
    div.textContent = 'Hello Univer!'
    shadow.appendChild(div)
  }
}

univerAPI.registerComponent('my-web-component', WebComponent, {
  // [!code word:web-component]
  framework: 'web-component',
})
```

### Angular

Преобразуйте компонент Angular в пользовательский элемент через `createCustomElement` из `@angular/elements`, затем зарегистрируйте его через адаптер Web Component в Univer. Компонент можно использовать в боковой панели или диалоге.

Добавьте `@angular/elements` той же версии, что и `@angular/core`, и `@univerjs/ui-adapter-web-component` той же версии, что и остальные пакеты Univer.

Вызовите эту функцию после инициализации Univer. В компоненте-хозяине получите Angular-инжектор через `private readonly injector = inject(Injector)` и вызовите `registerAngularPanel(univer, univerAPI, this.injector)`. Импортируйте `inject` и `Injector` из `@angular/core` в этом компоненте.

```typescript title="angular-panel.ts"
import type { Injector } from '@angular/core'
import type { FUniver, Univer } from '@univerjs/presets'
import { Component } from '@angular/core'
import { createCustomElement } from '@angular/elements'
import { UniverWebComponentAdapterPlugin } from '@univerjs/ui-adapter-web-component'

@Component({
  selector: 'app-angular-panel',
  standalone: true,
  template: '<p>Hello from Angular!</p>',
})
export class AngularPanelComponent {}

export function registerAngularPanel(univer: Univer, univerAPI: FUniver, injector: Injector) {
  const element = createCustomElement(AngularPanelComponent, { injector })

  univer.registerPlugin(UniverWebComponentAdapterPlugin)
  return univerAPI.registerComponent('univer-angular-panel', element, {
    framework: 'web-component',
  })
}
```

После отрисовки редактора используйте зарегистрированный ключ как содержимое боковой панели:

```typescript
const sidebar = univerAPI.openSidebar({
  header: { title: 'Angular' },
  children: { label: 'univer-angular-panel' },
  width: 360,
})
```

Сохраните дескрипторы регистрации и боковой панели и вызовите их методы `dispose()`, когда они больше не нужны. Регистрируйте элемент один раз на приложение. Имя должно содержать дефис и отличаться от selector компонента Angular.

Angular Elements преобразует входы в свойства элемента, а выходы — в события DOM `CustomEvent`. Адаптер Univer передаёт свойства, но не подписывается автоматически на выходы Angular: подключите нужные события явно.

## Использование пользовательских компонентов

С помощью следующих методов можно гибко интегрировать в Univer различные пользовательские компоненты, расширяя и настраивая его возможности.

> [!WARNING: Внимание]
> 1. Перед использованием этих методов убедитесь, что Univer завершил отрисовку. 2. Если компонент требуется
>    зарегистрировать, перед использованием убедитесь, что регистрация выполнена правильно. 3. Используйте метод
>    `dispose()`, чтобы очищать и удалять добавленные компоненты и тем самым избегать утечек памяти.

### Добавление пользовательских пунктов меню

Пользовательские компоненты можно добавлять как на верхнюю панель меню (Ribbon), так и в контекстное меню (Context Menu). Для этого необходимо создать пользовательский плагин. Чтобы вам было проще начать, мы подготовили [рекомендации по добавлению пользовательских пунктов меню](/blog/custom-plugin).

### Замена встроенных компонентов

> [!ERROR: Предупреждение]
> Замена встроенных компонентов может нарушить работу некоторых функций. Прежде чем заменять компоненты, внимательно
> изучите исходный код и документацию и действуйте осторожно.

`univerAPI.registerComponent` по умолчанию регистрирует переопределение (`override: true`). Оно имеет приоритет независимо от порядка регистрации стандартного компонента плагином. Для каждого `componentKey` допустимо только одно активное переопределение; перед повторной регистрацией освободите предыдущее. Метод `dispose()` возвращённого объекта восстанавливает стандартный компонент, если он зарегистрирован. Параметр `{ override: false }` регистрирует стандартный компонент.

Регистрируйте переопределение до первого открытия представления. Изменение регистрации не перерисовывает уже смонтированные представления автоматически.

Публичные точки входа пакетов Sheets экспортируют ключи для переопределения встроенных компонентов:

| Компонент               | Экспортируемый ключ        | Пакет                                        |
| ----------------------- | -------------------------- | -------------------------------------------- |
| Определённые имена      | `DEFINED_NAME_CONTAINER`   | `@univerjs/sheets-ui`                        |
| Другие функции          | `MORE_FUNCTIONS_COMPONENT` | `@univerjs/sheets-formula-ui`                |
| Проверка данных         | `DATA_VALIDATION_PANEL`    | `@univerjs/sheets-data-validation-ui`        |
| Условное форматирование | `CF_PANEL_KEY`             | `@univerjs/sheets-conditional-formatting-ui` |

Например, встроенный компонент ColorPicker можно заменить следующим образом:

```tsx
// Following code only replaces the UI of the built-in ColorPicker component, not its functionality
univerAPI.registerComponent('UI_COLOR_PICKER_COMPONENT', () => <input type="color" />)
```

### Использование в качестве компонентов содержимого

#### На боковой панели

Метод `univerAPI.openSidebar` позволяет открыть в интерфейсе Univer боковую панель с пользовательским компонентом.

```tsx
// [!code word:MyCustomSidebarComponent]
// You should register the component at an appropriate time (e.g., after Univer has loaded)
univerAPI.registerComponent('MyCustomSidebarComponent', () => <div>Hello Univer!</div>)

const sidebar = univerAPI.openSidebar({
  header: { title: 'My Sidebar' },
  children: { label: 'MyCustomSidebarComponent' },
  onClose: () => {
    console.log('close')
  },
  width: 360,
})

// Later close the sidebar
sidebar.dispose()
```

Справочник: [`univerAPI.openSidebar`](https://docs.univer.ai/ru-RU/reference/facade/univer.md#opensidebar)

#### В диалоговом окне

Метод `univerAPI.openDialog` позволяет открыть диалоговое окно с пользовательским компонентом.

```tsx
// [!code word:MyCustomDialogComponent]
// You should register the component at an appropriate time (e.g., after Univer has loaded)
univerAPI.registerComponent('MyCustomDialogComponent', () => <div>Hello Univer!</div>)

const dialog = univerAPI.openDialog({
  id: 'unique-dialog-id', // The unique identifier for the dialog
  draggable: true,
  width: 300,
  title: { title: 'My Dialog' },
  children: {
    label: 'MyCustomDialogComponent',
  },
  destroyOnClose: true,
  preservePositionOnDestroy: true,
  onClose: () => {},
})

// Later close the dialog
dialog.dispose()
```

Справочник: [`univerAPI.openDialog`](https://docs.univer.ai/ru-RU/reference/facade/univer.md#opendialog)

#### Добавление всплывающего окна к ячейкам

Метод `FRange.attachPopup` позволяет добавить пользовательское всплывающее окно к определённому диапазону ячеек.

* Всплывающее окно — это временный DOM-элемент, прикреплённый к ячейке. Обычно он служит для отображения временной информации о состоянии и не поддерживает постоянное хранение данных.
* Метод [`FRange.attachPopup(popup: IFCanvasPopup)`](https://docs.univer.ai/ru-RU/reference/facade/range.md#attachpopup) позволяет добавить пользовательское всплывающее окно к определённому диапазону ячеек.
* Всплывающее окно прикрепляется к краям ячейки и автоматически меняет положение и ориентацию, если оказывается перекрыто.

```tsx
// [!code word:MyCustomPopupComponent]
// You should register the component at an appropriate time (e.g., after Univer has loaded)
univerAPI.registerComponent(
  'MyCustomPopupComponent',
  () => <div>Hello Univer!</div>,
)

const fWorkbook = univerAPI.getActiveWorkbook()
const fWorksheet = fWorkbook.getActiveSheet()
const fRange = fWorksheet.getRange('A1:J10')

// Attach the popup to the first cell of the range
// If `disposable` is null, it indicates that the popup was not added successfully
const disposable = fRange.attachPopup({
  componentKey: 'MyCustomPopupComponent',
})

// Remove the popup
disposable.dispose()
```

#### Добавление предупреждения

Метод `FRange.attachAlertPopup` позволяет добавить всплывающее предупреждение к начальной ячейке указанного диапазона.

```typescript
const fWorkbook = univerAPI.getActiveWorkbook()
const fWorksheet = fWorkbook.getActiveSheet()
const fRange = fWorksheet.getRange('A1:B2')

const disposable = fRange.attachAlertPopup({
  key: 'unique-alert-key', // Unique identifier
  title: '这是一个警告！',
  message: '这是一个警告！',
  width: 300,
  height: 200,
  // 0: Information
  // 1: Warning
  // 2: Error
  type: 0,
})

// Remove the alert later
disposable.dispose()
```

#### Добавление плавающего DOM-элемента на лист

Метод `FWorksheet.addFloatDomToPosition` позволяет добавить на лист плавающий DOM-элемент.

* Перед использованием этого метода установите плагин `@univerjs/sheets-drawing-ui` или пресет `@univerjs/preset-sheets-drawing`.
* Плавающий DOM-элемент — это компонент, который располагается поверх листа, поддерживает перетаскивание и постоянное хранение данных.
* Вызывайте этот метод после завершения отрисовки Univer.
* `componentKey`: уникальный идентификатор компонента, по которому на него можно ссылаться в Univer.
* [Полное описание параметров](https://github.com/dream-num/univer/blob/dev/packages/sheets-drawing-ui/src/facade/f-worksheet.ts#L32)

```tsx
// [!code word:MyCustomFloatDOMComponent]
// You should register the component at an appropriate time (e.g., after Univer has loaded)
univerAPI.registerComponent(
  'MyCustomFloatDOMComponent',
  ({ data }) => (
    <div>
      Hello
      {data?.label}
      !
    </div>
  ),
)
const fWorkbook = univerAPI.getActiveWorkbook()
const fWorksheet = fWorkbook.getActiveSheet()

// Add a floating DOM to the worksheet
// If `disposable` is null, it indicates that the floating DOM was not added successfully
const disposable = fWorksheet.addFloatDomToPosition({
  componentKey: 'MyCustomFloatDOMComponent',
  initPosition: {
    startX: 100,
    endX: 200,
    startY: 100,
    endY: 200,
  },

  // The data for the component
  data: {
    label: 'Univer',
  },
})

// Remove the floating DOM
disposable.dispose()
```
