# 自訂元件

> Web SDK 提供了多種方式來整合自訂元件，使你能夠擴充並客製化 Web SDK 的功能。本指南將介紹幾種常用的方法。

- Human documentation: [https://docs.univer.ai/zh-TW/guides/slides/ui/components](https://docs.univer.ai/zh-TW/guides/slides/ui/components)

- Agent Markdown: [https://docs.univer.ai/zh-TW/guides/slides/ui/components.md](https://docs.univer.ai/zh-TW/guides/slides/ui/components.md)

- Requested language: `zh-TW`

- Content language: `zh-TW`

- Documentation version: `1.2.0`

- Source: [slides/ui/components.zh-TW.mdx](https://github.com/dream-num/documentation/blob/dev/content/guides/slides/ui/components.zh-TW.mdx)

---

Univer 並不會直接將元件作為參數傳遞給任何渲染函數，你需要透過 Facade API 將元件註冊到 Univer 中之後，才能在各個掛載點使用它們。

如果你還不瞭解如何獲取 Facade API 的實例，可以參考 [Facade API](https://docs.univer.ai/zh-TW/guides/slides/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 Components。
* `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 元件時，需要確保安裝並註冊 `@univerjs/ui-adapter-vue3` 的 `UniverVue3AdapterPlugin` 插件：

#### 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 已經不再維護，Univer 暫時沒有提供 Vue 2.x 的 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'

/**
 * 允許 Univer 使用 Vue 2 元件作為 UI 元件的插件。
 */
@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 ++]
```

使用時需要指定 `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 標準，並安裝和註冊 `@univerjs/ui-adapter-web-component` 的 `UniverWebComponentAdapterPlugin` 插件：

#### 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',
})
```

## 使用自訂元件

透過以下方法，你可以靈活地在 Univer 中整合各種自訂元件，從而增強和定制 Univer 的功能。

> [!WARNING: 注意事項]
> 1. 在使用這些方法時，請確保 Univer 已經完成渲染。 2. 對於需要註冊的元件，請確保在使用前已正確註冊。 3. 使用
>    `dispose()` 方法來清理和移除添加的元件，以避免記憶體洩漏。

### 添加自訂選單項目

頂部選單列（Ribbon）和右鍵選單（Context Menu）都可以加入自訂元件。請從自己的外掛提供這些選單項目，在使用前註冊元件，並隨外掛一起釋放相關註冊。

### 替換內建元件

> [!ERROR: 警告]
> 替換內建元件可能會導致一些功能無法正常工作，請在充分閱讀原始碼和文件後自行尋找可替換的元件並謹慎操作。

`univerAPI.registerComponent` 預設註冊覆寫元件（`override: true`）。無論外掛在呼叫前或呼叫後註冊內建預設元件，覆寫元件都優先生效。每個 `componentKey` 同時只能有一個覆寫元件，再次註冊前須先釋放上一個註冊。呼叫回傳值的 `dispose()` 後，會恢復已註冊的內建預設元件。傳入 `{ override: false }` 則註冊預設元件。

請在對應檢視首次開啟前註冊覆寫元件；變更註冊不會自動重新呈現已掛載的檢視。

例如簡單地替換內建的 ColorPicker 元件：

```tsx
// 此程式碼僅可在 UI 層面替換內建 ColorPicker 元件，無法替代其功能實現
univerAPI.registerComponent('UI_COLOR_PICKER_COMPONENT', () => <input type="color" />)
```

### 作為內容元件添加到……

#### 在側邊欄中使用

使用 `univerAPI.openSidebar` 方法可以在 Univer 介面中打開一個包含自訂元件的側邊欄。

```tsx
// [!code word:MyCustomSidebarComponent]
// 你應該在合適的時機（比如渲染完成）註冊元件
univerAPI.registerComponent('MyCustomSidebarComponent', () => <div>Hello Univer!</div>)

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

// 稍後關閉側邊欄
sidebar.dispose()
```

參考： [`univerAPI.openSidebar`](https://docs.univer.ai/zh-TW/reference/facade/univer.md#opensidebar)

#### 在對話框中使用

使用 `univerAPI.openDialog` 方法可以打開一個包含自訂元件的對話框。

```tsx
// [!code word:MyCustomDialogComponent]
// 你應該在合適的時機（比如渲染完成）註冊元件
univerAPI.registerComponent('MyCustomDialogComponent', () => <div>Hello Univer!</div>)

const dialog = univerAPI.openDialog({
  id: 'unique-dialog-id', // 對話框的唯一識別符
  draggable: true,
  width: 300,
  title: { title: 'My Dialog' },
  children: {
    label: 'MyCustomDialogComponent',
  },
  destroyOnClose: true,
  preservePositionOnDestroy: true,
  onClose: () => {},
})

// 稍後關閉對話框
dialog.dispose()
```

參考： [`univerAPI.openDialog`](https://docs.univer.ai/zh-TW/reference/facade/univer.md#opendialog)
