Skip to content

Commit

Permalink
feat(plugin-mention): add mention plugin
Browse files Browse the repository at this point in the history
* feat(plugin-mention): add mention plugin

* feat(changeset): add changeset for mention plugin patch

* chore: update yarn.lock
  • Loading branch information
cycleccc authored Feb 7, 2025
1 parent a82fa34 commit 140d22c
Show file tree
Hide file tree
Showing 18 changed files with 555 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .changeset/wise-ducks-grin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@wangeditor-next/plugin-mention': patch
---

feat(plugin-mention): add mention plugin
Empty file.
90 changes: 90 additions & 0 deletions packages/plugin-mention/README-en.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# wangEditor mention plugin

[中文文档](./README.md)

## Introduction

[wangeditor-next](https://github.com/cycleccc/wangEditor-next) mention plugin, like `@James`.

![](./_img/demo.png)

## Installation

```shell
yarn add @wangeditor-next/plugin-mention
```

## Usage

[Vue demo source code](https://github.com/wangfupeng1988/vue2-wangeditor-demo/blob/master/src/components/MyEditorWithMention.vue)

### Use in editor

```ts
import { IDomEditor, Boot, IEditorConfig } from '@wangeditor-next/editor'
import mentionModule, { MentionElement } from '@wangeditor-next/plugin-mention'

// Register
// You should register this before create editor, and register only once (not repeatedly).
Boot.registerModule(mentionModule)

// Show your modal
function showModal(editor: IDomEditor) {
// Get cursor's position info, to set modal position
const domSelection = document.getSelection()
const domRange = domSelection.getRangeAt(0)
if (domRange == null) return
const selectionRect = domRange.getBoundingClientRect()

// Get editor container's position info, maybe help to get right modal position
const containerRect = editor.getEditableContainer().getBoundingClientRect()

// Show your modal, and set position
// PS: You must implement the modal yourself, use <div> or Vue React component


// Insert mention node when emit some event.
function insertMention() {
const mentionNode: MentionElement = {
type: 'mention', // must be 'mention'
value: 'James', // text
info: { x: 1, y: 2 }, // extended info
children: [{ text: '' }], // must have an empty text node in children
}

editor.restoreSelection()
editor.deleteBackward('character') // delete '@'
editor.insertNode(mentionNode)
editor.move(1) // move curser
}
}

// hide your modal
function hideModal(editor: IDomEditor) {
// hide your modal
}

// editor config
const editorConfig: Partial<IEditorConfig> = {
EXTEND_CONF: {
mentionConfig: {
showModal, // required
hideModal, // required
},
},

// others...
}

// Then create editor and toolbar, you will use `editorConfig`
```

### Render HTML

You will get a mention's HTML format like this. You need to `decodeURIComponent` the value of `data-info`.

```html
<span data-w-e-type="mention" data-w-e-is-void data-w-e-is-inline data-value="James" data-info="%7B%22x%22%3A10%7D">@James</span>
```


87 changes: 87 additions & 0 deletions packages/plugin-mention/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# wangEditor mention 插件

[English Documentation](./README-en.md)

## 介绍

[wangeditor-next](https://github.com/cycleccc/wangEditor-next) mention 插件,如 `@张三`

![](./_img/demo.png)

## 安装

```shell
yarn add @wangeditor-next/plugin-mention
```

## 使用

[Vue 示例源码](https://github.com/wangfupeng1988/vue2-wangeditor-demo/blob/master/src/components/MyEditorWithMention.vue)

### 注册到编辑器

```ts
import { IDomEditor, Boot, IEditorConfig } from '@wangeditor-next/editor'
import mentionModule, { MentionElement } from '@wangeditor-next/plugin-mention'

// 注册。要在创建编辑器之前注册,且只能注册一次,不可重复注册。
Boot.registerModule(mentionModule)

// 显示弹框
function showModal(editor: IDomEditor) {
// 获取光标位置,定位 modal
const domSelection = document.getSelection()
const domRange = domSelection.getRangeAt(0)
if (domRange == null) return
const selectionRect = domRange.getBoundingClientRect()

// 获取编辑区域 DOM 节点的位置,以辅助定位
const containerRect = editor.getEditableContainer().getBoundingClientRect()

// 显示 modal 弹框,并定位
// PS:modal 需要自定义,如 <div> 或 Vue React 组件


// 当触发某事件(如点击一个按钮)时,插入 mention 节点
function insertMention() {
const mentionNode: MentionElement = {
type: 'mention', // 必须是 'mention'
value: '张三', // 文本
info: { x: 1, y: 2 }, // 其他信息,自定义
children: [{ text: '' }], // 必须有一个空 text 作为 children
}

editor.restoreSelection() // 恢复选区
editor.deleteBackward('character') // 删除 '@'
editor.insertNode(mentionNode) // 插入 mention
editor.move(1) // 移动光标
}
}

// 隐藏弹框
function hideModal(editor: IDomEditor) {
// 隐藏 modal
}

// 编辑器配置
const editorConfig: Partial<IEditorConfig> = {
EXTEND_CONF: {
mentionConfig: {
showModal, // 必须
hideModal, // 必须
},
},

// 其他...
}

// 创建创建和工具栏,会用到 editorConfig 。具体查看 wangEditor 文档
```

### 显示 HTML

mention 节点返回的 HTML 格式如下,其中 `data-info` 的值需要 `decodeURIComponent` 解析。

```html
<span data-w-e-type="mention" data-w-e-is-void data-w-e-is-inline data-value="张三" data-info="%7B%22x%22%3A10%7D">@张三</span>
```
Binary file added packages/plugin-mention/_img/demo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
47 changes: 47 additions & 0 deletions packages/plugin-mention/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
{
"name": "@wangeditor-next/plugin-mention",
"version": "1.0.0",
"description": "wangEditor mention plugin",
"author": "cycleccc <[email protected]>",
"type": "module",
"homepage": "https://github.com/wangeditor-next/wangEditor-next#readme",
"license": "MIT",
"types": "dist/plugin-mention/src/index.d.ts",
"main": "dist/index.js",
"module": "dist/index.mjs",
"exports": {
".": {
"types": "./dist/plugin-mention/src/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.js"
},
"./dist/css/style.css": "./dist/css/style.css"
},
"directories": {
"lib": "dist"
},
"files": [
"dist"
],
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "git+https://github.com/wangeditor-next/wangEditor-next.git"
},
"scripts": {
"dev": "cross-env NODE_ENV=development rollup -c rollup.config.js",
"dev-watch": "cross-env NODE_ENV=development rollup -c rollup.config.js -w",
"build": "cross-env NODE_ENV=production rollup -c rollup.config.js",
"dev-size-stats": "cross-env NODE_ENV=development:size_stats rollup -c rollup.config.js",
"size-stats": "cross-env NODE_ENV=production:size_stats rollup -c rollup.config.js"
},
"bugs": {
"url": "https://github.com/wangeditor-next/wangeditor-next/issues"
},
"peerDependencies": {
"@wangeditor-next/editor": "5.6.31",
"snabbdom": "^3.1.0"
}
}
31 changes: 31 additions & 0 deletions packages/plugin-mention/rollup.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { createRollupConfig } from '@wangeditor-next-shared/rollup-config'

import pkg from './package.json' assert { type: 'json' }

const name = 'WangEditorMentionPlugin'

const configList = []

// esm
const esmConf = createRollupConfig({
output: {
file: pkg.module,
format: 'esm',
name,
},
})

configList.push(esmConf)

// umd
const umdConf = createRollupConfig({
output: {
file: pkg.main,
format: 'umd',
name,
},
})

configList.push(umdConf)

export default configList
11 changes: 11 additions & 0 deletions packages/plugin-mention/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/**
* @description src entry
* @author wangfupeng
*/

import module from './module/index'

export * from './module/custom-types'
export * from './module/interface'

export default module
14 changes: 14 additions & 0 deletions packages/plugin-mention/src/module/custom-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* @description mention element
* @author wangfupeng
*/

type EmptyText = {
text: ''
}
export type MentionElement = {
type: 'mention'
value: string
info: any
children: EmptyText[] // void 元素必须有一个空 text
}
24 changes: 24 additions & 0 deletions packages/plugin-mention/src/module/elem-to-html.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* @description elem to html
* @author wangfupeng
*/

import { SlateElement } from '@wangeditor-next/editor'

import { MentionElement } from './custom-types'

// 生成 html 的函数
function mentionToHtml(elem: SlateElement, _childrenHtml: string): string {
const { value = '', info = {} } = elem as MentionElement
const infoStr = encodeURIComponent(JSON.stringify(info))

return `<span data-w-e-type="mention" data-w-e-is-void data-w-e-is-inline data-value="${value}" data-info="${infoStr}">@${value}</span>`
}

// 配置
const conf = {
type: 'mention', // 节点 type ,重要!!!
elemToHtml: mentionToHtml,
}

export default conf
20 changes: 20 additions & 0 deletions packages/plugin-mention/src/module/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* @description mention module entry
* @author wangfupeng
*/

import { IModuleConf } from '@wangeditor-next/editor'

import elemToHtmlConf from './elem-to-html'
import parseHtmlConf from './parse-elem-html'
import withMention from './plugin'
import renderElemConf from './render-elem'

const module: Partial<IModuleConf> = {
editorPlugin: withMention,
renderElems: [renderElemConf],
elemsToHtml: [elemToHtmlConf],
parseElemsHtml: [parseHtmlConf],
}

export default module
13 changes: 13 additions & 0 deletions packages/plugin-mention/src/module/interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* @description interface
* @author wangfupeng
*/

import { IDomEditor } from '@wangeditor-next/editor'

export interface IExtendConfig {
mentionConfig: {
showModal: (editor: IDomEditor) => void
hideModal: (editor: IDomEditor) => void
}
}
41 changes: 41 additions & 0 deletions packages/plugin-mention/src/module/parse-elem-html.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* @description parse elem html
* @author wangfupeng
*/

import { IDomEditor, SlateDescendant, SlateElement } from '@wangeditor-next/editor'

import { DOMElement } from '../utils/dom'
import { MentionElement } from './custom-types'

function parseHtml(
elem: DOMElement,
_children: SlateDescendant[],
_editor: IDomEditor,
): SlateElement {
// elem HTML 结构 <span data-w-e-type="mention" data-w-e-is-void data-w-e-is-inline data-value="张三" data-info="xxx">@张三</span>

const value = elem.getAttribute('data-value') || ''
const rawInfo = decodeURIComponent(elem.getAttribute('data-info') || '')
let info: any

try {
info = JSON.parse(rawInfo)
} catch (ex) {
info = rawInfo
}

return {
type: 'mention',
value,
info,
children: [{ text: '' }], // void node 必须有一个空白 text
} as MentionElement
}

const parseHtmlConf = {
selector: 'span[data-w-e-type="mention"]',
parseElemHtml: parseHtml,
}

export default parseHtmlConf
Loading

0 comments on commit 140d22c

Please sign in to comment.