feat: add AI command suggestions extension for Tiptap editor
- Implemented Commands plugin to handle command suggestions triggered by '@' character. - Created suggestion configuration to filter and render command items. - Added various command items including date, time, formatting, templates, and task management commands. - Introduced CSS styles for the Tiptap editor to enhance UI. - Configured TypeScript settings and Vite build for the project.
This commit is contained in:
55
.cnb.yml
Normal file
55
.cnb.yml
Normal file
@@ -0,0 +1,55 @@
|
||||
# .cnb.yml
|
||||
$:
|
||||
vscode:
|
||||
- docker:
|
||||
image: docker.cnb.cool/kevisual/dev-env:latest
|
||||
services:
|
||||
- vscode
|
||||
- docker
|
||||
imports: https://cnb.cool/kevisual/env/-/blob/main/env.yml
|
||||
# 开发环境启动后会执行的任务
|
||||
# stages:
|
||||
# - name: pnpm install
|
||||
# script: pnpm install
|
||||
|
||||
|
||||
main:
|
||||
web_trigger_sync_to_gitea:
|
||||
- services:
|
||||
- docker
|
||||
imports:
|
||||
- https://cnb.cool/kevisual/env/-/blob/main/env.yml
|
||||
stages:
|
||||
- name: 'show username'
|
||||
script: echo "GITEA_USERNAME is ${GITEA_USERNAME} and GITEA_PASSWORD is ${GITEA_PASSWORD}"
|
||||
- name: sync to gitea
|
||||
image: tencentcom/git-sync
|
||||
settings:
|
||||
target_url: https://git.xiongxiao.me/kevisual/cnb.git
|
||||
auth_type: https
|
||||
username: "oauth2"
|
||||
password: ${GITEA_TOKEN}
|
||||
git_user: "abearxiong"
|
||||
git_email: "xiongxiao@xiongxiao.me"
|
||||
sync_mode: rebase
|
||||
branch: main
|
||||
|
||||
|
||||
|
||||
"**":
|
||||
web_trigger_test:
|
||||
- stages:
|
||||
- name: 执行任务
|
||||
script: echo "job"
|
||||
|
||||
# .cnb/web_trigger.yml
|
||||
branch:
|
||||
# 如下按钮在分支名以 release 开头的分支详情页面显示
|
||||
- reg: "^main"
|
||||
buttons:
|
||||
- name: 同步代码到gitea
|
||||
desc: 同步代码到gitea
|
||||
event: web_trigger_sync_to_gitea
|
||||
- name: 同步gitea代码到当前仓库
|
||||
desc: 同步gitea代码到当前仓库
|
||||
event: web_trigger_sync_from_gitea
|
||||
15
.gitignore
vendored
Normal file
15
.gitignore
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
.env
|
||||
!.env*development
|
||||
|
||||
node_modules
|
||||
|
||||
dist
|
||||
pack-dist
|
||||
|
||||
.DS_Store
|
||||
|
||||
.pnpm-store
|
||||
|
||||
.vite
|
||||
|
||||
.astro
|
||||
15
bun.config.ts
Normal file
15
bun.config.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { build } from 'bun';
|
||||
|
||||
await build({
|
||||
entrypoints: ["./src/lib.ts"],
|
||||
outdir: './dist',
|
||||
target: 'browser',
|
||||
format: 'esm',
|
||||
naming: {
|
||||
entry: 'app.js',
|
||||
},
|
||||
minify: false,
|
||||
sourcemap: false,
|
||||
});
|
||||
|
||||
console.log('✅ Build complete: dist/app.js');
|
||||
20
index.html
Normal file
20
index.html
Normal file
@@ -0,0 +1,20 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Light Code</title>
|
||||
<!-- <link href="./dist/kv-md.css" rel="stylesheet" /> -->
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="./src/main.tsx"></script>
|
||||
<!-- <kv-md-editor id="editor"></kv-md-editor> -->
|
||||
<!-- <script type="module">
|
||||
import './dist/kv-md.js';
|
||||
</script> -->
|
||||
</body>
|
||||
|
||||
</html>
|
||||
25
kevisual.json
Normal file
25
kevisual.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"metadata": {
|
||||
"name": "kevisual",
|
||||
"share": "public"
|
||||
},
|
||||
"registry": "https://kevisual.cn/root/ai/kevisual/frontend/simple-lit-vite",
|
||||
"clone": {
|
||||
".": {
|
||||
"enabled": true
|
||||
}
|
||||
},
|
||||
"syncd": [
|
||||
{
|
||||
"files": [
|
||||
"**/*"
|
||||
],
|
||||
"registry": ""
|
||||
}
|
||||
],
|
||||
"sync": {
|
||||
".gitignore": {
|
||||
"url": "/gitignore.txt"
|
||||
}
|
||||
}
|
||||
}
|
||||
2
mod.ts
Normal file
2
mod.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
import './src/index.css'
|
||||
export * from './src/md-editor.ts';
|
||||
67
package.json
Normal file
67
package.json
Normal file
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"name": "@kevisual/kv-md",
|
||||
"version": "0.0.1",
|
||||
"description": "",
|
||||
"basename": "/root/kv-md",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"build:lib": "vite build --config vite.config.lib.ts",
|
||||
"prepub": "rm -rf ./dist && rimraf ./pack-dist && pnpm run build:test",
|
||||
"pub": "envision deploy ./dist -k kv-md -v 0.0.1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "abearxiong <xiongxiao@xiongxiao.me> (https://www.xiongxiao.me)",
|
||||
"license": "MIT",
|
||||
"packageManager": "pnpm@10.25.0",
|
||||
"type": "module",
|
||||
"files": [
|
||||
"dist",
|
||||
"mod.ts",
|
||||
"src"
|
||||
],
|
||||
"dependencies": {
|
||||
"@kevisual/app": "^0.0.1",
|
||||
"@kevisual/context": "^0.0.4",
|
||||
"@tiptap/core": "^3.13.0",
|
||||
"@tiptap/extension-code-block-lowlight": "^3.13.0",
|
||||
"@tiptap/extension-dropcursor": "^3.13.0",
|
||||
"@tiptap/extension-highlight": "^3.13.0",
|
||||
"@tiptap/extension-placeholder": "^3.13.0",
|
||||
"@tiptap/extension-typography": "^3.13.0",
|
||||
"@tiptap/pm": "^3.13.0",
|
||||
"@tiptap/starter-kit": "^3.13.0",
|
||||
"@tiptap/suggestion": "^3.13.0",
|
||||
"dayjs": "^1.11.19",
|
||||
"highlight.js": "^11.11.1",
|
||||
"lit-html": "^3.3.1",
|
||||
"lowlight": "^3.3.0",
|
||||
"nanoid": "^5.1.6",
|
||||
"react": "^19.2.1",
|
||||
"react-dom": "^19.2.1",
|
||||
"tiptap-markdown": "^0.9.0",
|
||||
"zustand": "^5.0.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@kevisual/query": "0.0.31",
|
||||
"@kevisual/types": "^0.0.10",
|
||||
"@tailwindcss/vite": "^4.1.17",
|
||||
"@types/bun": "^1.3.4",
|
||||
"@types/node": "^24.10.2",
|
||||
"@types/react": "^19.2.7",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^5.1.2",
|
||||
"dotenv": "^17.2.3",
|
||||
"tailwindcss": "^4.1.17",
|
||||
"vite": "^7.2.7"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"exports": {
|
||||
".": "./dist/kv-md.js",
|
||||
"./kv-md.js": "./dist/kv-md.js",
|
||||
"./kv-md.css": "./dist/kv-md.css",
|
||||
"./mod.ts": "./mod.ts"
|
||||
}
|
||||
}
|
||||
2637
pnpm-lock.yaml
generated
Normal file
2637
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
15
readme.md
Normal file
15
readme.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# simple-lit-vite
|
||||
|
||||
这是一个使用 Vite 构建的简单 Lit 项目模板。您可以使用此模板快速启动一个新的 Lit 项目,并利用 AI 助手生成代码片段以加速开发过程。少用git。
|
||||
|
||||
## cli
|
||||
|
||||
```sh
|
||||
ev sync clone -i https://kevisual.cn/root/ai/kevisual/frontend/simple-lit-vite/kevisual.json
|
||||
```
|
||||
|
||||
## 同步命令
|
||||
```sh
|
||||
ev sync list
|
||||
ev sync upload
|
||||
```
|
||||
71
src/app.tsx
Normal file
71
src/app.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { TextEditor, TextEditorProps } from './tiptap/editor.ts';
|
||||
const markdown = `
|
||||
# 欢迎使用 KeVisual Markdown 编辑器!
|
||||
这是一个基于 Tiptap 和 React 构建的富文本编辑器,支持 Markdown 语法。
|
||||
|
||||
## 功能特点
|
||||
- 实时预览
|
||||
- 丰富的文本格式化选项
|
||||
- 支持图片和链接插入
|
||||
- 自定义快捷键
|
||||
|
||||
## 快速开始
|
||||
只需在编辑区域输入 Markdown 语法,即可实时看到格式化后的内容。
|
||||
|
||||
**示例:**
|
||||
\`\`\`markdown
|
||||
# 这是一个标题
|
||||
**加粗文本**
|
||||
*斜体文本*
|
||||
- 列表项 1
|
||||
- 列表项 2
|
||||
\`\`\`
|
||||
|
||||
享受写作的乐趣吧!
|
||||
|
||||
ts code
|
||||
|
||||
\`\`\`typescript
|
||||
function greet(name: string): string {
|
||||
return \`Hello, \${name}!\`;
|
||||
}
|
||||
\`\`\`
|
||||
`
|
||||
const init = () => {
|
||||
const editor = new TextEditor();
|
||||
const editorContainer = document.getElementById('editor') as HTMLDivElement;
|
||||
|
||||
const opts: TextEditorProps = {
|
||||
markdown,
|
||||
placeholder: '输入内容...',
|
||||
onUpdateHtml: (html) => {
|
||||
console.log('Content changed:', html);
|
||||
},
|
||||
};
|
||||
editor.createEditor(editorContainer, opts);
|
||||
return editor;
|
||||
}
|
||||
export const App = () => {
|
||||
// useEffect(() => {
|
||||
// const editor = init();
|
||||
// return () => {
|
||||
// editor.destroy();
|
||||
// };
|
||||
// }, []);
|
||||
return (
|
||||
<div>
|
||||
<h1 className="bg-red-200">Hello Vite + React Simple vite</h1>
|
||||
<div className='card tiptap'>
|
||||
<button type='button' onClick={() => alert('Button clicked!')}>
|
||||
Click me
|
||||
</button>
|
||||
</div>
|
||||
<kv-md-editor markdown={markdown} placeholder='请输入内容...'>
|
||||
</kv-md-editor>
|
||||
|
||||
{/* <kv-template></kv-template> */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
2
src/index.css
Normal file
2
src/index.css
Normal file
@@ -0,0 +1,2 @@
|
||||
@import "tailwindcss";
|
||||
@import "./tiptap/tiptap.css";
|
||||
49
src/lib.ts
Normal file
49
src/lib.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { render } from 'lit-html';
|
||||
import { html, TemplateResult } from 'lit-html';
|
||||
|
||||
class KvTemplate extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.render();
|
||||
}
|
||||
|
||||
private render(): void {
|
||||
const content: TemplateResult = html`
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
padding: 16px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 8px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
.container {
|
||||
font-family: Arial, sans-serif;
|
||||
text-align: center;
|
||||
}
|
||||
h1 {
|
||||
color: #333;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
p {
|
||||
color: #666;
|
||||
line-height: 1.5;
|
||||
}
|
||||
</style>
|
||||
<div class="container">
|
||||
<h1>KV Template Component</h1>
|
||||
<p>This is a sample custom element using Lit-HTML.</p>
|
||||
<p>Current time: ${new Date().toLocaleString()}</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
render(content, this.shadowRoot!);
|
||||
}
|
||||
}
|
||||
|
||||
// Define the custom element globally
|
||||
customElements.define('kv-template', KvTemplate);
|
||||
7
src/main.tsx
Normal file
7
src/main.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import './lib.ts'
|
||||
import { App } from './app.tsx';
|
||||
import './index.css';
|
||||
import './md-editor.ts';
|
||||
|
||||
createRoot(document.getElementById('root')!).render(<App />);
|
||||
124
src/md-editor.ts
Normal file
124
src/md-editor.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import { render } from 'lit-html';
|
||||
import { html, TemplateResult } from 'lit-html';
|
||||
import { TextEditor, TextEditorProps } from './tiptap/editor.ts';
|
||||
|
||||
class KvMdEditor extends HTMLElement {
|
||||
private editor: TextEditor;
|
||||
private editorContainer?: HTMLDivElement;
|
||||
private _markdown: string = '';
|
||||
private _placeholder: string = '输入内容...';
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
this.editor = new TextEditor();
|
||||
}
|
||||
|
||||
static get observedAttributes() {
|
||||
return ['markdown', 'placeholder'];
|
||||
}
|
||||
getId() {
|
||||
return this.id;
|
||||
}
|
||||
attributeChangedCallback(name: string, oldValue: string, newValue: string) {
|
||||
if (oldValue === newValue) return;
|
||||
|
||||
switch (name) {
|
||||
case 'markdown':
|
||||
this._markdown = newValue || '';
|
||||
if (this.editorContainer) {
|
||||
this.editor.setContent(this._markdown);
|
||||
}
|
||||
break;
|
||||
case 'placeholder':
|
||||
this._placeholder = newValue || this._placeholder;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.render();
|
||||
this.initEditor();
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
this.editor.destroy();
|
||||
}
|
||||
|
||||
private initEditor(): void {
|
||||
if (!this.editorContainer) return;
|
||||
|
||||
const opts: TextEditorProps = {
|
||||
markdown: this._markdown,
|
||||
placeholder: this._placeholder,
|
||||
onUpdateHtml: (html) => {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('content-change', {
|
||||
detail: { html, text: this.editor.getContent() },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
})
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
this.editor.createEditor(this.editorContainer, opts);
|
||||
}
|
||||
|
||||
private render(): void {
|
||||
const content: TemplateResult = html`
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
<slot></slot>
|
||||
`;
|
||||
|
||||
render(content, this.shadowRoot!);
|
||||
let id = this.getId();
|
||||
if(!id) {
|
||||
id = `kv-md-editor-${Math.random().toString(36).substr(2, 9)}`;
|
||||
this.id = id;
|
||||
}
|
||||
const inlineId = `${id}-inline`;
|
||||
let editor = this.shadowRoot!.querySelector(`#${inlineId}`)!;
|
||||
if(!editor) {
|
||||
editor = document.createElement('div');
|
||||
editor.id = inlineId;
|
||||
const host = document.querySelector(`#${id}`);
|
||||
host!.appendChild(editor);
|
||||
}
|
||||
this.editorContainer = editor as HTMLDivElement;
|
||||
}
|
||||
|
||||
// 公开的 API 方法
|
||||
public getHtml(): string {
|
||||
return this.editor.getHtml() || '';
|
||||
}
|
||||
|
||||
public getContent(): string {
|
||||
return this.editor.getContent() || '';
|
||||
}
|
||||
|
||||
public setContent(content: string): void {
|
||||
this.editor.setContent(content);
|
||||
}
|
||||
|
||||
public focus(): void {
|
||||
this.editor.foucus();
|
||||
}
|
||||
}
|
||||
|
||||
// Define the custom element globally
|
||||
customElements.define('kv-md-editor', KvMdEditor);
|
||||
|
||||
export {
|
||||
TextEditor,
|
||||
}
|
||||
|
||||
export type {
|
||||
TextEditorProps
|
||||
}
|
||||
112
src/tiptap/components/CommandsList.tsx
Normal file
112
src/tiptap/components/CommandsList.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
import React, { forwardRef, useEffect, useImperativeHandle, useState } from 'react';
|
||||
import { CommandItem } from '../extensions/suggestions/commands';
|
||||
|
||||
interface CommandsListProps {
|
||||
items: CommandItem[];
|
||||
command: (props: { content: string }) => void;
|
||||
}
|
||||
|
||||
export const CommandsList = forwardRef((props: CommandsListProps, ref) => {
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
|
||||
const selectItem = (index: number) => {
|
||||
const item = props.items[index];
|
||||
|
||||
if (item) {
|
||||
props.command({ content: item.content });
|
||||
}
|
||||
};
|
||||
|
||||
const upHandler = () => {
|
||||
setSelectedIndex((selectedIndex + props.items.length - 1) % props.items.length);
|
||||
};
|
||||
|
||||
const downHandler = () => {
|
||||
setSelectedIndex((selectedIndex + 1) % props.items.length);
|
||||
};
|
||||
|
||||
const enterHandler = () => {
|
||||
selectItem(selectedIndex);
|
||||
};
|
||||
|
||||
useEffect(() => setSelectedIndex(0), [props.items]);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
onKeyDown: ({ event }: { event: KeyboardEvent }) => {
|
||||
if (event.key === 'ArrowUp') {
|
||||
upHandler();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (event.key === 'ArrowDown') {
|
||||
downHandler();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (event.key === 'Enter') {
|
||||
enterHandler();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
}));
|
||||
|
||||
// Scroll to selected item when it changes
|
||||
useEffect(() => {
|
||||
const element = document.getElementById(`command-item-${selectedIndex}`);
|
||||
if (element) {
|
||||
element.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
||||
}
|
||||
}, [selectedIndex]);
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-md shadow-lg border border-gray-200 overflow-hidden" style={{ width: '350px', maxHeight: '80vh' }}>
|
||||
<div className="p-2 bg-gray-50 border-b border-gray-200 sticky top-0 z-10">
|
||||
<div className="text-sm font-medium text-gray-700">Commands ({props.items.length})</div>
|
||||
<div className="text-xs text-gray-500">Type to filter commands</div>
|
||||
</div>
|
||||
|
||||
<div className="max-h-72 overflow-y-auto">
|
||||
{props.items.length ? (
|
||||
props.items.map((item, index) => (
|
||||
<button
|
||||
id={`command-item-${index}`}
|
||||
key={index}
|
||||
className={`block w-full text-left px-4 py-2 text-sm transition-colors ${
|
||||
index === selectedIndex ? 'bg-blue-100 border-l-4 border-blue-500' : 'border-l-4 border-transparent'
|
||||
} hover:bg-gray-50`}
|
||||
onClick={() => selectItem(index)}
|
||||
>
|
||||
<div className="font-medium flex items-center">
|
||||
!{item.title}
|
||||
{index === selectedIndex && (
|
||||
<span className="ml-2 text-xs bg-blue-500 text-white px-2 py-0.5 rounded">
|
||||
Press Enter to select
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-gray-500 text-xs">{item.description}</div>
|
||||
</button>
|
||||
))
|
||||
) : (
|
||||
<div className="px-4 py-2 text-sm text-gray-500">No results</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-50 px-3 py-2 text-xs text-gray-500 border-t flex justify-between items-center sticky bottom-0 z-10">
|
||||
<span className="inline-flex items-center">
|
||||
<kbd className="px-2 py-1 bg-white rounded border border-gray-300 shadow-sm mr-1">↑</kbd>
|
||||
<kbd className="px-2 py-1 bg-white rounded border border-gray-300 shadow-sm">↓</kbd>
|
||||
<span className="ml-1">to navigate</span>
|
||||
</span>
|
||||
<span className="inline-flex items-center">
|
||||
<kbd className="px-2 py-1 bg-white rounded border border-gray-300 shadow-sm">Enter</kbd>
|
||||
<span className="ml-1">to select</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
CommandsList.displayName = 'CommandsList';
|
||||
42
src/tiptap/components/ReactRenderer.tsx
Normal file
42
src/tiptap/components/ReactRenderer.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
export class ReactRenderer {
|
||||
component: any;
|
||||
element: HTMLElement;
|
||||
ref: React.RefObject<any>;
|
||||
props: any;
|
||||
editor: any;
|
||||
root: any;
|
||||
|
||||
constructor(component: any, { props, editor }: any) {
|
||||
this.component = component;
|
||||
this.element = document.createElement('div');
|
||||
this.ref = React.createRef();
|
||||
this.props = {
|
||||
...props,
|
||||
ref: this.ref,
|
||||
};
|
||||
this.editor = editor;
|
||||
this.root = createRoot(this.element);
|
||||
this.render();
|
||||
}
|
||||
|
||||
updateProps(props: any) {
|
||||
this.props = {
|
||||
...this.props,
|
||||
...props,
|
||||
};
|
||||
this.render();
|
||||
}
|
||||
|
||||
render() {
|
||||
this.root.render(React.createElement(this.component, this.props));
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.root.unmount();
|
||||
}
|
||||
}
|
||||
|
||||
export default ReactRenderer;
|
||||
173
src/tiptap/editor.ts
Normal file
173
src/tiptap/editor.ts
Normal file
@@ -0,0 +1,173 @@
|
||||
import { Editor } from '@tiptap/core';
|
||||
import StarterKit from '@tiptap/starter-kit';
|
||||
import Highlight from '@tiptap/extension-highlight';
|
||||
import Typography from '@tiptap/extension-typography';
|
||||
import { Markdown } from 'tiptap-markdown';
|
||||
import Dropcursor from '@tiptap/extension-dropcursor';
|
||||
// import Gapcursor from '@tiptap/extension-gapcursor';
|
||||
|
||||
import Placeholder from '@tiptap/extension-placeholder';
|
||||
import { Commands, getSuggestionItems, CommandItem } from './extensions/suggestions';
|
||||
// import { DragHandle } from './extensions/dragHandle';
|
||||
import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight';
|
||||
import { all, createLowlight } from 'lowlight';
|
||||
import 'highlight.js/styles/github.css';
|
||||
// 根据需要引入的语言支持
|
||||
import js from 'highlight.js/lib/languages/javascript';
|
||||
import ts from 'highlight.js/lib/languages/typescript';
|
||||
import html from 'highlight.js/lib/languages/xml';
|
||||
import css from 'highlight.js/lib/languages/css';
|
||||
import markdown from 'highlight.js/lib/languages/markdown';
|
||||
// import './editor.css';
|
||||
|
||||
const lowlight = createLowlight(all);
|
||||
|
||||
// you can also register individual languages
|
||||
lowlight.register('html', html);
|
||||
lowlight.register('css', css);
|
||||
lowlight.register('js', js);
|
||||
lowlight.register('ts', ts);
|
||||
lowlight.register('markdown', markdown);
|
||||
|
||||
export type TextEditorProps = {
|
||||
markdown?: string;
|
||||
html?: string;
|
||||
items?: CommandItem[];
|
||||
placeholder?: string;
|
||||
onUpdateHtml?: (html: string) => void; //
|
||||
};
|
||||
export class TextEditor {
|
||||
private editor?: Editor;
|
||||
private opts?: TextEditorProps;
|
||||
private element?: HTMLElement;
|
||||
private isInitialSetup: boolean = true;
|
||||
|
||||
constructor() { }
|
||||
createEditor(el: HTMLElement, opts?: TextEditorProps) {
|
||||
if (this.editor) {
|
||||
this.destroy();
|
||||
}
|
||||
this.opts = opts;
|
||||
this.element = el;
|
||||
const html = opts?.markdown || opts?.html || '';
|
||||
const items = opts?.items || getSuggestionItems();
|
||||
const placeholder = opts?.placeholder || 'Type @ to see commands (e.g., @today, @list @test )...';
|
||||
// const suggestionConfig = createSuggestionConfig(items);
|
||||
this.isInitialSetup = true;
|
||||
this.editor = new Editor({
|
||||
element: el, // 指定编辑器容器
|
||||
editable: true,
|
||||
extensions: [
|
||||
StarterKit.configure({
|
||||
codeBlock: false,
|
||||
// 禁用内置的拖拽功能,使用我们的自定义拖拽
|
||||
dropcursor: false,
|
||||
}), // 使用 StarterKit 包含基础功能
|
||||
Highlight,
|
||||
Dropcursor.configure({
|
||||
color: '#3b82f6', // 设置拖拽指示器颜色
|
||||
width: 2, // 设置拖拽指示器宽度
|
||||
}),
|
||||
// Gapcursor, // 允许在难以选择的位置放置光标
|
||||
// DragHandle,
|
||||
Placeholder.configure({
|
||||
placeholder,
|
||||
}),
|
||||
Typography,
|
||||
Markdown,
|
||||
CodeBlockLowlight.extend({
|
||||
addKeyboardShortcuts() {
|
||||
return {
|
||||
Tab: () => {
|
||||
const { state, dispatch } = this.editor.view;
|
||||
const { tr, selection } = state;
|
||||
const { from, to } = selection;
|
||||
|
||||
// 插入4个空格的缩进
|
||||
dispatch(tr.insertText(' ', from, to));
|
||||
return true;
|
||||
},
|
||||
'Shift-Tab': () => {
|
||||
const { state, dispatch } = this.editor.view;
|
||||
const { tr, selection } = state;
|
||||
const { from, to } = selection;
|
||||
|
||||
// 获取当前选中的文本
|
||||
const selectedText = state.doc.textBetween(from, to, '\n');
|
||||
|
||||
// 取消缩进:移除前面的4个空格
|
||||
const unindentedText = selectedText.replace(/^ {1,4}/gm, '');
|
||||
dispatch(tr.insertText(unindentedText, from, to));
|
||||
return true;
|
||||
},
|
||||
};
|
||||
},
|
||||
}).configure({
|
||||
lowlight,
|
||||
}),
|
||||
// Commands.configure({
|
||||
// suggestion: suggestionConfig,
|
||||
// }),
|
||||
],
|
||||
content: html, // 初始化内容,
|
||||
onUpdate: () => {
|
||||
if (this.isInitialSetup) {
|
||||
this.isInitialSetup = false;
|
||||
return;
|
||||
}
|
||||
if (this.opts?.onUpdateHtml) {
|
||||
this.opts.onUpdateHtml(this.editor?.getHTML() || '');
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
init(el: HTMLElement, opts?: TextEditorProps) {
|
||||
return this.createEditor(el, opts);
|
||||
}
|
||||
updateSugestionConfig(items: CommandItem[]) {
|
||||
if (!this.element) return;
|
||||
const element = this.element;
|
||||
if (this.editor) {
|
||||
const content = this.editor.getHTML(); // Save current content
|
||||
const opts = { ...this.opts, html: content, items };
|
||||
this.createEditor(element, opts); // Recreate the editor with the new config
|
||||
}
|
||||
}
|
||||
setContent(html: string, emitUpdate?: boolean) {
|
||||
this.editor?.commands.setContent(html, { emitUpdate });
|
||||
}
|
||||
/**
|
||||
* before set options ,you should has element and editor
|
||||
* @param opts
|
||||
*/
|
||||
setOptions(opts: { markdown?: string; html?: string; items?: CommandItem[]; onUpdateHtml?: (html: string) => void }) {
|
||||
this.opts = { ...this.opts, ...opts };
|
||||
this.createEditor(this.element!, this.opts!);
|
||||
}
|
||||
getHtml() {
|
||||
return this.editor?.getHTML();
|
||||
}
|
||||
getContent() {
|
||||
return this.editor?.getText();
|
||||
}
|
||||
foucus() {
|
||||
this.editor?.view?.focus?.();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.editor?.destroy();
|
||||
this.editor = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* use example
|
||||
* const textEditor = new TextEditor();
|
||||
textEditor.createEditor(document.getElementById('editor')!, {
|
||||
markdown: '# Hello World',
|
||||
onUpdateHtml: (html) => {
|
||||
console.log('Updated HTML:', html);
|
||||
},
|
||||
});
|
||||
*/
|
||||
179
src/tiptap/extensions/dragHandle.ts
Normal file
179
src/tiptap/extensions/dragHandle.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
import { Extension } from '@tiptap/core';
|
||||
import { Plugin, PluginKey } from '@tiptap/pm/state';
|
||||
import { TextSelection } from '@tiptap/pm/state';
|
||||
|
||||
function setupDraggableElements(editor: any) {
|
||||
if (editor?.view?.dom) {
|
||||
const blockElements = editor.view.dom.querySelectorAll('p, h1, h2, h3, h4, h5, h6, li, blockquote, pre');
|
||||
blockElements.forEach((element: HTMLElement) => {
|
||||
element.draggable = true;
|
||||
element.classList.add('draggable-block');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const DragHandle = Extension.create({
|
||||
name: 'dragHandle',
|
||||
|
||||
addProseMirrorPlugins() {
|
||||
return [
|
||||
new Plugin({
|
||||
key: new PluginKey('dragHandle'),
|
||||
props: {
|
||||
handleDOMEvents: {
|
||||
dragstart: (view, event) => {
|
||||
const target = event.target as HTMLElement;
|
||||
|
||||
// 查找最近的块级元素
|
||||
let blockElement = target;
|
||||
while (blockElement && blockElement !== view.dom) {
|
||||
if (['P', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'LI', 'BLOCKQUOTE', 'PRE'].includes(blockElement.nodeName)) {
|
||||
break;
|
||||
}
|
||||
blockElement = blockElement.parentElement as HTMLElement;
|
||||
}
|
||||
|
||||
if (!blockElement || blockElement === view.dom) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 获取块元素在文档中的位置并选中整个节点
|
||||
try {
|
||||
const pos = view.posAtDOM(blockElement, 0);
|
||||
if (pos !== null && pos >= 0) {
|
||||
const resolvedPos = view.state.doc.resolve(pos);
|
||||
|
||||
// 查找包含此位置的块级节点
|
||||
let node = resolvedPos.parent;
|
||||
let from = resolvedPos.start();
|
||||
let to = resolvedPos.end();
|
||||
|
||||
// 如果当前不是块级节点,向上查找
|
||||
let depth = resolvedPos.depth;
|
||||
while (node && !node.isBlock && depth > 0) {
|
||||
depth--;
|
||||
const parentPos = view.state.doc.resolve(resolvedPos.before(depth + 1));
|
||||
node = parentPos.parent;
|
||||
from = parentPos.start();
|
||||
to = parentPos.end();
|
||||
}
|
||||
|
||||
if (node && node.isBlock) {
|
||||
// 选中整个块级节点
|
||||
const tr = view.state.tr.setSelection(
|
||||
TextSelection.create(view.state.doc, from, to)
|
||||
);
|
||||
view.dispatch(tr);
|
||||
|
||||
console.log('Selected node:', { from, to, type: node.type.name });
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error selecting node:', error);
|
||||
}
|
||||
|
||||
// 设置拖拽数据类型标识
|
||||
event.dataTransfer?.setData('application/x-tiptap-drag', 'true');
|
||||
event.dataTransfer?.setData('text/html', blockElement.outerHTML);
|
||||
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.effectAllowed = 'move';
|
||||
}
|
||||
|
||||
blockElement.classList.add('dragging');
|
||||
|
||||
console.log('Drag started for:', blockElement.nodeName);
|
||||
return false;
|
||||
},
|
||||
|
||||
dragover: (view, event) => {
|
||||
event.preventDefault();
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.dropEffect = 'move';
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
drop: (view, event) => {
|
||||
event.preventDefault();
|
||||
|
||||
// 检查是否是我们的拖拽
|
||||
if (!event.dataTransfer?.getData('application/x-tiptap-drag')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const coordinates = view.posAtCoords({
|
||||
left: event.clientX,
|
||||
top: event.clientY,
|
||||
});
|
||||
|
||||
if (!coordinates) {
|
||||
console.log('No coordinates found');
|
||||
return true;
|
||||
}
|
||||
|
||||
// 使用内置的拖拽处理
|
||||
const slice = view.state.selection.content();
|
||||
const { tr } = view.state;
|
||||
|
||||
// 删除原来的内容
|
||||
tr.deleteSelection();
|
||||
|
||||
// 在新位置插入
|
||||
let insertPos = coordinates.pos;
|
||||
if (insertPos > view.state.selection.from) {
|
||||
insertPos -= (view.state.selection.to - view.state.selection.from);
|
||||
}
|
||||
|
||||
tr.insert(insertPos, slice.content);
|
||||
|
||||
view.dispatch(tr);
|
||||
|
||||
console.log('Drop completed at position:', insertPos);
|
||||
return true;
|
||||
},
|
||||
|
||||
dragend: (view, event) => {
|
||||
// 清理拖拽样式
|
||||
const draggingElements = view.dom.querySelectorAll('.dragging');
|
||||
draggingElements.forEach((el) => {
|
||||
el.classList.remove('dragging');
|
||||
});
|
||||
return false;
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
];
|
||||
},
|
||||
|
||||
onUpdate() {
|
||||
setupDraggableElements(this.editor);
|
||||
},
|
||||
|
||||
onCreate() {
|
||||
setTimeout(() => {
|
||||
setupDraggableElements(this.editor);
|
||||
}, 100);
|
||||
|
||||
// 监听内容变化
|
||||
if (this.editor?.view?.dom) {
|
||||
const observer = new MutationObserver(() => {
|
||||
setupDraggableElements(this.editor);
|
||||
});
|
||||
|
||||
observer.observe(this.editor.view.dom, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
|
||||
(this as any)._observer = observer;
|
||||
}
|
||||
},
|
||||
|
||||
onDestroy() {
|
||||
if ((this as any)._observer) {
|
||||
(this as any)._observer.disconnect();
|
||||
}
|
||||
},
|
||||
});
|
||||
40
src/tiptap/extensions/suggestions/commands.ts
Normal file
40
src/tiptap/extensions/suggestions/commands.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { Extension } from '@tiptap/core';
|
||||
import Suggestion from '@tiptap/suggestion';
|
||||
import { PluginKey } from '@tiptap/pm/state';
|
||||
|
||||
export const CommandsPluginKey = new PluginKey('commands');
|
||||
|
||||
export interface CommandItem {
|
||||
title: string;
|
||||
description: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export const Commands = Extension.create({
|
||||
name: 'ai-commands',
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
suggestion: {
|
||||
char: '@',
|
||||
command: ({ editor, range, props }: any) => {
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.deleteRange(range)
|
||||
.insertContent(props.content)
|
||||
.run();
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
addProseMirrorPlugins() {
|
||||
return [
|
||||
Suggestion({
|
||||
editor: this.editor,
|
||||
...this.options.suggestion,
|
||||
}),
|
||||
];
|
||||
},
|
||||
});
|
||||
3
src/tiptap/extensions/suggestions/index.ts
Normal file
3
src/tiptap/extensions/suggestions/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './commands';
|
||||
// export * from './suggestionConfig';
|
||||
export * from './suggestionItems';
|
||||
121
src/tiptap/extensions/suggestions/suggestionConfig.ts
Normal file
121
src/tiptap/extensions/suggestions/suggestionConfig.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import { CommandItem } from './commands';
|
||||
import { CommandsList } from '../../components/CommandsList';
|
||||
import ReactRenderer from '../../components/ReactRenderer';
|
||||
|
||||
export const createSuggestionConfig = (items: CommandItem[]) => {
|
||||
return {
|
||||
items: ({ query }: { query: string }) => {
|
||||
return items.filter(item => item.title.toLowerCase().startsWith(query.toLowerCase()));
|
||||
},
|
||||
render: () => {
|
||||
let component: ReactRenderer | null = null;
|
||||
let popup: HTMLElement | null = null;
|
||||
|
||||
const calculatePosition = (view: any, from: number) => {
|
||||
const coords = view.coordsAtPos(from);
|
||||
const editorRect = view.dom.getBoundingClientRect();
|
||||
const popupRect = popup?.getBoundingClientRect();
|
||||
|
||||
if (!popup || !popupRect) return { left: coords.left, top: coords.bottom + 10 };
|
||||
|
||||
// Default position below the cursor
|
||||
let left = coords.left;
|
||||
let top = coords.bottom + 10;
|
||||
|
||||
// Check if we're near the bottom of the viewport
|
||||
const viewportHeight = window.innerHeight;
|
||||
const bottomSpace = viewportHeight - coords.bottom;
|
||||
const popupHeight = popupRect.height;
|
||||
|
||||
// If there's not enough space below, position above
|
||||
if (bottomSpace < popupHeight + 10 && coords.top > popupHeight + 10) {
|
||||
top = coords.top - popupHeight - 10;
|
||||
}
|
||||
|
||||
// Check if we're near the right edge of the viewport
|
||||
const viewportWidth = window.innerWidth;
|
||||
const rightSpace = viewportWidth - coords.left;
|
||||
const popupWidth = popupRect.width;
|
||||
|
||||
// If there's not enough space to the right, align right edge
|
||||
if (rightSpace < popupWidth) {
|
||||
left = Math.max(10, viewportWidth - popupWidth - 10);
|
||||
}
|
||||
|
||||
// Ensure popup stays within editor bounds horizontally if possible
|
||||
if (left < editorRect.left) {
|
||||
left = editorRect.left;
|
||||
}
|
||||
|
||||
return { left, top };
|
||||
};
|
||||
|
||||
return {
|
||||
onStart: (props: any) => {
|
||||
component = new ReactRenderer(CommandsList, {
|
||||
props,
|
||||
editor: props.editor,
|
||||
});
|
||||
|
||||
popup = document.createElement('div');
|
||||
popup.className = 'commands-popup';
|
||||
popup.style.position = 'fixed'; // Use fixed instead of absolute for better viewport positioning
|
||||
popup.style.zIndex = '9999';
|
||||
document.body.appendChild(popup);
|
||||
|
||||
popup.appendChild(component.element);
|
||||
|
||||
// Initial position
|
||||
const { view } = props.editor;
|
||||
const { from } = props.range;
|
||||
|
||||
// Set initial position to get popup dimensions
|
||||
popup.style.left = '0px';
|
||||
popup.style.top = '0px';
|
||||
|
||||
// Calculate proper position after the popup is rendered
|
||||
setTimeout(() => {
|
||||
if (!popup) return;
|
||||
const { left, top } = calculatePosition(view, from);
|
||||
popup.style.left = `${left}px`;
|
||||
popup.style.top = `${top}px`;
|
||||
}, 0);
|
||||
},
|
||||
onUpdate: (props: any) => {
|
||||
if (!component) return;
|
||||
|
||||
component.updateProps(props);
|
||||
|
||||
if (!popup) return;
|
||||
|
||||
// Update position
|
||||
const { view } = props.editor;
|
||||
const { from } = props.range;
|
||||
const { left, top } = calculatePosition(view, from);
|
||||
|
||||
popup.style.left = `${left}px`;
|
||||
popup.style.top = `${top}px`;
|
||||
},
|
||||
onKeyDown: (props: any) => {
|
||||
if (props.event.key === 'Escape') {
|
||||
if (popup) popup.remove();
|
||||
if (component) component.destroy();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (component && component.ref && component.ref.current) {
|
||||
return component.ref.current.onKeyDown(props);
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
onExit: () => {
|
||||
if (popup) popup.remove();
|
||||
if (component) component.destroy();
|
||||
component = null;
|
||||
popup = null;
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
};
|
||||
203
src/tiptap/extensions/suggestions/suggestionItems.ts
Normal file
203
src/tiptap/extensions/suggestions/suggestionItems.ts
Normal file
@@ -0,0 +1,203 @@
|
||||
import { CommandItem } from './commands';
|
||||
|
||||
export const getSuggestionItems = (): CommandItem[] => {
|
||||
// Basic commands
|
||||
const basicCommands = [
|
||||
{
|
||||
title: 'today',
|
||||
description: 'Insert today\'s date',
|
||||
content: new Date().toLocaleDateString(),
|
||||
},
|
||||
{
|
||||
title: 'now',
|
||||
description: 'Insert current time',
|
||||
content: new Date().toLocaleTimeString(),
|
||||
},
|
||||
{
|
||||
title: 'datetime',
|
||||
description: 'Insert current date and time',
|
||||
content: new Date().toLocaleString(),
|
||||
},
|
||||
{
|
||||
title: 'list',
|
||||
description: 'Insert a bullet list',
|
||||
content: '<ul><li>Item 1</li><li>Item 2</li><li>Item 3</li></ul>',
|
||||
},
|
||||
{
|
||||
title: 'numbered',
|
||||
description: 'Insert a numbered list',
|
||||
content: '<ol><li>First item</li><li>Second item</li><li>Third item</li></ol>',
|
||||
},
|
||||
{
|
||||
title: 'good',
|
||||
description: 'Insert a positive message',
|
||||
content: 'Great job! Keep up the good work! 👍',
|
||||
},
|
||||
{
|
||||
title: 'meeting',
|
||||
description: 'Insert meeting template',
|
||||
content: '<h3>Meeting Notes</h3><p><strong>Date:</strong> ' + new Date().toLocaleDateString() + '</p><p><strong>Attendees:</strong></p><ul><li>Person 1</li><li>Person 2</li></ul><p><strong>Agenda:</strong></p><ol><li>Topic 1</li><li>Topic 2</li></ol><p><strong>Action Items:</strong></p><ul><li>[ ] Task 1</li><li>[ ] Task 2</li></ul>',
|
||||
},
|
||||
{
|
||||
title: 'signature',
|
||||
description: 'Insert your signature',
|
||||
content: '<p>Best regards,<br>Your Name<br>your.email@example.com</p>',
|
||||
},
|
||||
];
|
||||
|
||||
// Text formatting commands
|
||||
const formattingCommands = [
|
||||
{
|
||||
title: 'h1',
|
||||
description: 'Insert heading 1',
|
||||
content: '<h1>Heading 1</h1>',
|
||||
},
|
||||
{
|
||||
title: 'h2',
|
||||
description: 'Insert heading 2',
|
||||
content: '<h2>Heading 2</h2>',
|
||||
},
|
||||
{
|
||||
title: 'h3',
|
||||
description: 'Insert heading 3',
|
||||
content: '<h3>Heading 3</h3>',
|
||||
},
|
||||
{
|
||||
title: 'quote',
|
||||
description: 'Insert blockquote',
|
||||
content: '<blockquote>This is a quote</blockquote>',
|
||||
},
|
||||
{
|
||||
title: 'code',
|
||||
description: 'Insert code block',
|
||||
content: '<pre><code>// Your code here\nconsole.log("Hello world");</code></pre>',
|
||||
},
|
||||
{
|
||||
title: 'bold',
|
||||
description: 'Insert bold text',
|
||||
content: '<strong>Bold text</strong>',
|
||||
},
|
||||
{
|
||||
title: 'italic',
|
||||
description: 'Insert italic text',
|
||||
content: '<em>Italic text</em>',
|
||||
},
|
||||
{
|
||||
title: 'underline',
|
||||
description: 'Insert underlined text',
|
||||
content: '<u>Underlined text</u>',
|
||||
},
|
||||
{
|
||||
title: 'strike',
|
||||
description: 'Insert strikethrough text',
|
||||
content: '<s>Strikethrough text</s>',
|
||||
},
|
||||
{
|
||||
title: 'highlight',
|
||||
description: 'Insert highlighted text',
|
||||
content: '<mark>Highlighted text</mark>',
|
||||
},
|
||||
];
|
||||
|
||||
// Template commands
|
||||
const templateCommands = [
|
||||
{
|
||||
title: 'email',
|
||||
description: 'Insert email template',
|
||||
content: '<p>Subject: [Your Subject]</p><p>Dear [Name],</p><p>I hope this email finds you well.</p><p>[Your message here]</p><p>Thank you for your time and consideration.</p><p>Best regards,<br>Your Name</p>',
|
||||
},
|
||||
{
|
||||
title: 'letter',
|
||||
description: 'Insert formal letter template',
|
||||
content: '<p>[Your Name]<br>[Your Address]<br>[City, State ZIP]<br>[Your Email]<br>[Your Phone]</p><p>[Date]</p><p>[Recipient Name]<br>[Recipient Title]<br>[Company Name]<br>[Street Address]<br>[City, State ZIP]</p><p>Dear [Recipient Name],</p><p>[Letter content]</p><p>Sincerely,</p><p>[Your Name]</p>',
|
||||
},
|
||||
{
|
||||
title: 'report',
|
||||
description: 'Insert report template',
|
||||
content: '<h1>Report Title</h1><p><strong>Date:</strong> ' + new Date().toLocaleDateString() + '</p><p><strong>Author:</strong> Your Name</p><h2>Executive Summary</h2><p>[Brief summary of the report]</p><h2>Introduction</h2><p>[Introduction text]</p><h2>Findings</h2><p>[Detailed findings]</p><h2>Conclusion</h2><p>[Conclusion text]</p><h2>Recommendations</h2><p>[Recommendations]</p>',
|
||||
},
|
||||
{
|
||||
title: 'proposal',
|
||||
description: 'Insert proposal template',
|
||||
content: '<h1>Project Proposal</h1><p><strong>Date:</strong> ' + new Date().toLocaleDateString() + '</p><p><strong>Prepared by:</strong> Your Name</p><h2>Project Overview</h2><p>[Brief description of the project]</p><h2>Objectives</h2><ul><li>[Objective 1]</li><li>[Objective 2]</li></ul><h2>Scope of Work</h2><p>[Detailed scope]</p><h2>Timeline</h2><p>[Project timeline]</p><h2>Budget</h2><p>[Budget details]</p>',
|
||||
},
|
||||
{
|
||||
title: 'invoice',
|
||||
description: 'Insert invoice template',
|
||||
content: '<h1>INVOICE</h1><p><strong>Invoice #:</strong> [Number]</p><p><strong>Date:</strong> ' + new Date().toLocaleDateString() + '</p><p><strong>Due Date:</strong> [Due Date]</p><div><strong>From:</strong><br>[Your Name/Company]<br>[Your Address]<br>[Your Contact Info]</div><div><strong>To:</strong><br>[Client Name/Company]<br>[Client Address]</div><table style="width:100%; border-collapse: collapse;"><tr style="border-bottom: 1px solid #ddd;"><th style="text-align:left; padding: 8px;">Description</th><th style="text-align:right; padding: 8px;">Amount</th></tr><tr style="border-bottom: 1px solid #ddd;"><td style="padding: 8px;">[Item/Service Description]</td><td style="text-align:right; padding: 8px;">[Amount]</td></tr><tr><td style="text-align:right; padding: 8px;"><strong>Total</strong></td><td style="text-align:right; padding: 8px;"><strong>[Total Amount]</strong></td></tr></table><p><strong>Payment Terms:</strong> [Terms]</p><p><strong>Payment Method:</strong> [Method]</p>',
|
||||
},
|
||||
];
|
||||
|
||||
// Task management commands
|
||||
const taskCommands = [
|
||||
{
|
||||
title: 'todo',
|
||||
description: 'Insert todo list',
|
||||
content: '<h3>To-Do List</h3><ul><li>[ ] Task 1</li><li>[ ] Task 2</li><li>[ ] Task 3</li></ul>',
|
||||
},
|
||||
{
|
||||
title: 'checklist',
|
||||
description: 'Insert checklist',
|
||||
content: '<h3>Checklist</h3><ul><li>[ ] Item 1</li><li>[ ] Item 2</li><li>[ ] Item 3</li></ul>',
|
||||
},
|
||||
{
|
||||
title: 'progress',
|
||||
description: 'Insert progress tracker',
|
||||
content: '<h3>Project Progress</h3><ul><li>[x] Planning - Complete</li><li>[x] Research - Complete</li><li>[ ] Implementation - In Progress</li><li>[ ] Testing</li><li>[ ] Deployment</li></ul>',
|
||||
},
|
||||
{
|
||||
title: 'timeline',
|
||||
description: 'Insert project timeline',
|
||||
content: '<h3>Project Timeline</h3><ul><li><strong>Week 1:</strong> Planning and Research</li><li><strong>Week 2-3:</strong> Design and Development</li><li><strong>Week 4:</strong> Testing</li><li><strong>Week 5:</strong> Deployment</li></ul>',
|
||||
},
|
||||
{
|
||||
title: 'goals',
|
||||
description: 'Insert goals list',
|
||||
content: '<h3>Goals</h3><ol><li>Short-term goal 1</li><li>Short-term goal 2</li><li>Long-term goal 1</li><li>Long-term goal 2</li></ol>',
|
||||
},
|
||||
];
|
||||
|
||||
// Table commands
|
||||
const tableCommands = [
|
||||
{
|
||||
title: 'table2x2',
|
||||
description: 'Insert 2x2 table',
|
||||
content: '<table style="width:100%; border-collapse: collapse;"><tr style="border-bottom: 1px solid #ddd;"><th style="border: 1px solid #ddd; padding: 8px;">Header 1</th><th style="border: 1px solid #ddd; padding: 8px;">Header 2</th></tr><tr><td style="border: 1px solid #ddd; padding: 8px;">Row 1, Cell 1</td><td style="border: 1px solid #ddd; padding: 8px;">Row 1, Cell 2</td></tr><tr><td style="border: 1px solid #ddd; padding: 8px;">Row 2, Cell 1</td><td style="border: 1px solid #ddd; padding: 8px;">Row 2, Cell 2</td></tr></table>',
|
||||
},
|
||||
{
|
||||
title: 'table3x3',
|
||||
description: 'Insert 3x3 table',
|
||||
content: '<table style="width:100%; border-collapse: collapse;"><tr style="border-bottom: 1px solid #ddd;"><th style="border: 1px solid #ddd; padding: 8px;">Header 1</th><th style="border: 1px solid #ddd; padding: 8px;">Header 2</th><th style="border: 1px solid #ddd; padding: 8px;">Header 3</th></tr><tr><td style="border: 1px solid #ddd; padding: 8px;">Row 1, Cell 1</td><td style="border: 1px solid #ddd; padding: 8px;">Row 1, Cell 2</td><td style="border: 1px solid #ddd; padding: 8px;">Row 1, Cell 3</td></tr><tr><td style="border: 1px solid #ddd; padding: 8px;">Row 2, Cell 1</td><td style="border: 1px solid #ddd; padding: 8px;">Row 2, Cell 2</td><td style="border: 1px solid #ddd; padding: 8px;">Row 2, Cell 3</td></tr><tr><td style="border: 1px solid #ddd; padding: 8px;">Row 3, Cell 1</td><td style="border: 1px solid #ddd; padding: 8px;">Row 3, Cell 2</td><td style="border: 1px solid #ddd; padding: 8px;">Row 3, Cell 3</td></tr></table>',
|
||||
},
|
||||
{
|
||||
title: 'schedule',
|
||||
description: 'Insert schedule table',
|
||||
content: '<table style="width:100%; border-collapse: collapse;"><tr style="border-bottom: 1px solid #ddd;"><th style="border: 1px solid #ddd; padding: 8px;">Time</th><th style="border: 1px solid #ddd; padding: 8px;">Monday</th><th style="border: 1px solid #ddd; padding: 8px;">Tuesday</th><th style="border: 1px solid #ddd; padding: 8px;">Wednesday</th><th style="border: 1px solid #ddd; padding: 8px;">Thursday</th><th style="border: 1px solid #ddd; padding: 8px;">Friday</th></tr><tr><td style="border: 1px solid #ddd; padding: 8px;">9:00 AM</td><td style="border: 1px solid #ddd; padding: 8px;"></td><td style="border: 1px solid #ddd; padding: 8px;"></td><td style="border: 1px solid #ddd; padding: 8px;"></td><td style="border: 1px solid #ddd; padding: 8px;"></td><td style="border: 1px solid #ddd; padding: 8px;"></td></tr><tr><td style="border: 1px solid #ddd; padding: 8px;">10:00 AM</td><td style="border: 1px solid #ddd; padding: 8px;"></td><td style="border: 1px solid #ddd; padding: 8px;"></td><td style="border: 1px solid #ddd; padding: 8px;"></td><td style="border: 1px solid #ddd; padding: 8px;"></td><td style="border: 1px solid #ddd; padding: 8px;"></td></tr><tr><td style="border: 1px solid #ddd; padding: 8px;">11:00 AM</td><td style="border: 1px solid #ddd; padding: 8px;"></td><td style="border: 1px solid #ddd; padding: 8px;"></td><td style="border: 1px solid #ddd; padding: 8px;"></td><td style="border: 1px solid #ddd; padding: 8px;"></td><td style="border: 1px solid #ddd; padding: 8px;"></td></tr></table>',
|
||||
},
|
||||
{
|
||||
title: 'comparison',
|
||||
description: 'Insert comparison table',
|
||||
content: '<table style="width:100%; border-collapse: collapse;"><tr style="border-bottom: 1px solid #ddd;"><th style="border: 1px solid #ddd; padding: 8px;">Feature</th><th style="border: 1px solid #ddd; padding: 8px;">Option A</th><th style="border: 1px solid #ddd; padding: 8px;">Option B</th><th style="border: 1px solid #ddd; padding: 8px;">Option C</th></tr><tr><td style="border: 1px solid #ddd; padding: 8px;">Feature 1</td><td style="border: 1px solid #ddd; padding: 8px;">✓</td><td style="border: 1px solid #ddd; padding: 8px;">✓</td><td style="border: 1px solid #ddd; padding: 8px;">✓</td></tr><tr><td style="border: 1px solid #ddd; padding: 8px;">Feature 2</td><td style="border: 1px solid #ddd; padding: 8px;">✓</td><td style="border: 1px solid #ddd; padding: 8px;">✗</td><td style="border: 1px solid #ddd; padding: 8px;">✓</td></tr><tr><td style="border: 1px solid #ddd; padding: 8px;">Feature 3</td><td style="border: 1px solid #ddd; padding: 8px;">✗</td><td style="border: 1px solid #ddd; padding: 8px;">✓</td><td style="border: 1px solid #ddd; padding: 8px;">✓</td></tr><tr><td style="border: 1px solid #ddd; padding: 8px;">Price</td><td style="border: 1px solid #ddd; padding: 8px;">$</td><td style="border: 1px solid #ddd; padding: 8px;">$$</td><td style="border: 1px solid #ddd; padding: 8px;">$$$</td></tr></table>',
|
||||
},
|
||||
];
|
||||
|
||||
// Additional commands to reach 100 total
|
||||
const additionalCommands = Array.from({ length: 100 - (basicCommands.length + formattingCommands.length + templateCommands.length + taskCommands.length + tableCommands.length) }, (_, i) => {
|
||||
const index = i + 1;
|
||||
return {
|
||||
title: `command${index}`,
|
||||
description: `Example command ${index}`,
|
||||
content: `<p>This is example command ${index}</p>`,
|
||||
};
|
||||
});
|
||||
|
||||
// Combine all command categories
|
||||
return [
|
||||
...basicCommands,
|
||||
...formattingCommands,
|
||||
...templateCommands,
|
||||
...taskCommands,
|
||||
...tableCommands,
|
||||
...additionalCommands,
|
||||
];
|
||||
};
|
||||
203
src/tiptap/extensions/suggestions/suggestions.ts
Normal file
203
src/tiptap/extensions/suggestions/suggestions.ts
Normal file
@@ -0,0 +1,203 @@
|
||||
import { CommandItem } from './commands';
|
||||
|
||||
export const getSuggestionItems = (): CommandItem[] => {
|
||||
// Basic commands
|
||||
const basicCommands = [
|
||||
{
|
||||
title: 'today',
|
||||
description: 'Insert today\'s date',
|
||||
content: new Date().toLocaleDateString(),
|
||||
},
|
||||
{
|
||||
title: 'now',
|
||||
description: 'Insert current time',
|
||||
content: new Date().toLocaleTimeString(),
|
||||
},
|
||||
{
|
||||
title: 'datetime',
|
||||
description: 'Insert current date and time',
|
||||
content: new Date().toLocaleString(),
|
||||
},
|
||||
{
|
||||
title: 'list',
|
||||
description: 'Insert a bullet list',
|
||||
content: '<ul><li>Item 1</li><li>Item 2</li><li>Item 3</li></ul>',
|
||||
},
|
||||
{
|
||||
title: 'numbered',
|
||||
description: 'Insert a numbered list',
|
||||
content: '<ol><li>First item</li><li>Second item</li><li>Third item</li></ol>',
|
||||
},
|
||||
{
|
||||
title: 'good',
|
||||
description: 'Insert a positive message',
|
||||
content: 'Great job! Keep up the good work! 👍',
|
||||
},
|
||||
{
|
||||
title: 'meeting',
|
||||
description: 'Insert meeting template',
|
||||
content: '<h3>Meeting Notes</h3><p><strong>Date:</strong> ' + new Date().toLocaleDateString() + '</p><p><strong>Attendees:</strong></p><ul><li>Person 1</li><li>Person 2</li></ul><p><strong>Agenda:</strong></p><ol><li>Topic 1</li><li>Topic 2</li></ol><p><strong>Action Items:</strong></p><ul><li>[ ] Task 1</li><li>[ ] Task 2</li></ul>',
|
||||
},
|
||||
{
|
||||
title: 'signature',
|
||||
description: 'Insert your signature',
|
||||
content: '<p>Best regards,<br>Your Name<br>your.email@example.com</p>',
|
||||
},
|
||||
];
|
||||
|
||||
// Text formatting commands
|
||||
const formattingCommands = [
|
||||
{
|
||||
title: 'h1',
|
||||
description: 'Insert heading 1',
|
||||
content: '<h1>Heading 1</h1>',
|
||||
},
|
||||
{
|
||||
title: 'h2',
|
||||
description: 'Insert heading 2',
|
||||
content: '<h2>Heading 2</h2>',
|
||||
},
|
||||
{
|
||||
title: 'h3',
|
||||
description: 'Insert heading 3',
|
||||
content: '<h3>Heading 3</h3>',
|
||||
},
|
||||
{
|
||||
title: 'quote',
|
||||
description: 'Insert blockquote',
|
||||
content: '<blockquote>This is a quote</blockquote>',
|
||||
},
|
||||
{
|
||||
title: 'code',
|
||||
description: 'Insert code block',
|
||||
content: '<pre><code>// Your code here\nconsole.log("Hello world");</code></pre>',
|
||||
},
|
||||
{
|
||||
title: 'bold',
|
||||
description: 'Insert bold text',
|
||||
content: '<strong>Bold text</strong>',
|
||||
},
|
||||
{
|
||||
title: 'italic',
|
||||
description: 'Insert italic text',
|
||||
content: '<em>Italic text</em>',
|
||||
},
|
||||
{
|
||||
title: 'underline',
|
||||
description: 'Insert underlined text',
|
||||
content: '<u>Underlined text</u>',
|
||||
},
|
||||
{
|
||||
title: 'strike',
|
||||
description: 'Insert strikethrough text',
|
||||
content: '<s>Strikethrough text</s>',
|
||||
},
|
||||
{
|
||||
title: 'highlight',
|
||||
description: 'Insert highlighted text',
|
||||
content: '<mark>Highlighted text</mark>',
|
||||
},
|
||||
];
|
||||
|
||||
// Template commands
|
||||
const templateCommands = [
|
||||
{
|
||||
title: 'email',
|
||||
description: 'Insert email template',
|
||||
content: '<p>Subject: [Your Subject]</p><p>Dear [Name],</p><p>I hope this email finds you well.</p><p>[Your message here]</p><p>Thank you for your time and consideration.</p><p>Best regards,<br>Your Name</p>',
|
||||
},
|
||||
{
|
||||
title: 'letter',
|
||||
description: 'Insert formal letter template',
|
||||
content: '<p>[Your Name]<br>[Your Address]<br>[City, State ZIP]<br>[Your Email]<br>[Your Phone]</p><p>[Date]</p><p>[Recipient Name]<br>[Recipient Title]<br>[Company Name]<br>[Street Address]<br>[City, State ZIP]</p><p>Dear [Recipient Name],</p><p>[Letter content]</p><p>Sincerely,</p><p>[Your Name]</p>',
|
||||
},
|
||||
{
|
||||
title: 'report',
|
||||
description: 'Insert report template',
|
||||
content: '<h1>Report Title</h1><p><strong>Date:</strong> ' + new Date().toLocaleDateString() + '</p><p><strong>Author:</strong> Your Name</p><h2>Executive Summary</h2><p>[Brief summary of the report]</p><h2>Introduction</h2><p>[Introduction text]</p><h2>Findings</h2><p>[Detailed findings]</p><h2>Conclusion</h2><p>[Conclusion text]</p><h2>Recommendations</h2><p>[Recommendations]</p>',
|
||||
},
|
||||
{
|
||||
title: 'proposal',
|
||||
description: 'Insert proposal template',
|
||||
content: '<h1>Project Proposal</h1><p><strong>Date:</strong> ' + new Date().toLocaleDateString() + '</p><p><strong>Prepared by:</strong> Your Name</p><h2>Project Overview</h2><p>[Brief description of the project]</p><h2>Objectives</h2><ul><li>[Objective 1]</li><li>[Objective 2]</li></ul><h2>Scope of Work</h2><p>[Detailed scope]</p><h2>Timeline</h2><p>[Project timeline]</p><h2>Budget</h2><p>[Budget details]</p>',
|
||||
},
|
||||
{
|
||||
title: 'invoice',
|
||||
description: 'Insert invoice template',
|
||||
content: '<h1>INVOICE</h1><p><strong>Invoice #:</strong> [Number]</p><p><strong>Date:</strong> ' + new Date().toLocaleDateString() + '</p><p><strong>Due Date:</strong> [Due Date]</p><div><strong>From:</strong><br>[Your Name/Company]<br>[Your Address]<br>[Your Contact Info]</div><div><strong>To:</strong><br>[Client Name/Company]<br>[Client Address]</div><table style="width:100%; border-collapse: collapse;"><tr style="border-bottom: 1px solid #ddd;"><th style="text-align:left; padding: 8px;">Description</th><th style="text-align:right; padding: 8px;">Amount</th></tr><tr style="border-bottom: 1px solid #ddd;"><td style="padding: 8px;">[Item/Service Description]</td><td style="text-align:right; padding: 8px;">[Amount]</td></tr><tr><td style="text-align:right; padding: 8px;"><strong>Total</strong></td><td style="text-align:right; padding: 8px;"><strong>[Total Amount]</strong></td></tr></table><p><strong>Payment Terms:</strong> [Terms]</p><p><strong>Payment Method:</strong> [Method]</p>',
|
||||
},
|
||||
];
|
||||
|
||||
// Task management commands
|
||||
const taskCommands = [
|
||||
{
|
||||
title: 'todo',
|
||||
description: 'Insert todo list',
|
||||
content: '<h3>To-Do List</h3><ul><li>[ ] Task 1</li><li>[ ] Task 2</li><li>[ ] Task 3</li></ul>',
|
||||
},
|
||||
{
|
||||
title: 'checklist',
|
||||
description: 'Insert checklist',
|
||||
content: '<h3>Checklist</h3><ul><li>[ ] Item 1</li><li>[ ] Item 2</li><li>[ ] Item 3</li></ul>',
|
||||
},
|
||||
{
|
||||
title: 'progress',
|
||||
description: 'Insert progress tracker',
|
||||
content: '<h3>Project Progress</h3><ul><li>[x] Planning - Complete</li><li>[x] Research - Complete</li><li>[ ] Implementation - In Progress</li><li>[ ] Testing</li><li>[ ] Deployment</li></ul>',
|
||||
},
|
||||
{
|
||||
title: 'timeline',
|
||||
description: 'Insert project timeline',
|
||||
content: '<h3>Project Timeline</h3><ul><li><strong>Week 1:</strong> Planning and Research</li><li><strong>Week 2-3:</strong> Design and Development</li><li><strong>Week 4:</strong> Testing</li><li><strong>Week 5:</strong> Deployment</li></ul>',
|
||||
},
|
||||
{
|
||||
title: 'goals',
|
||||
description: 'Insert goals list',
|
||||
content: '<h3>Goals</h3><ol><li>Short-term goal 1</li><li>Short-term goal 2</li><li>Long-term goal 1</li><li>Long-term goal 2</li></ol>',
|
||||
},
|
||||
];
|
||||
|
||||
// Table commands
|
||||
const tableCommands = [
|
||||
{
|
||||
title: 'table2x2',
|
||||
description: 'Insert 2x2 table',
|
||||
content: '<table style="width:100%; border-collapse: collapse;"><tr style="border-bottom: 1px solid #ddd;"><th style="border: 1px solid #ddd; padding: 8px;">Header 1</th><th style="border: 1px solid #ddd; padding: 8px;">Header 2</th></tr><tr><td style="border: 1px solid #ddd; padding: 8px;">Row 1, Cell 1</td><td style="border: 1px solid #ddd; padding: 8px;">Row 1, Cell 2</td></tr><tr><td style="border: 1px solid #ddd; padding: 8px;">Row 2, Cell 1</td><td style="border: 1px solid #ddd; padding: 8px;">Row 2, Cell 2</td></tr></table>',
|
||||
},
|
||||
{
|
||||
title: 'table3x3',
|
||||
description: 'Insert 3x3 table',
|
||||
content: '<table style="width:100%; border-collapse: collapse;"><tr style="border-bottom: 1px solid #ddd;"><th style="border: 1px solid #ddd; padding: 8px;">Header 1</th><th style="border: 1px solid #ddd; padding: 8px;">Header 2</th><th style="border: 1px solid #ddd; padding: 8px;">Header 3</th></tr><tr><td style="border: 1px solid #ddd; padding: 8px;">Row 1, Cell 1</td><td style="border: 1px solid #ddd; padding: 8px;">Row 1, Cell 2</td><td style="border: 1px solid #ddd; padding: 8px;">Row 1, Cell 3</td></tr><tr><td style="border: 1px solid #ddd; padding: 8px;">Row 2, Cell 1</td><td style="border: 1px solid #ddd; padding: 8px;">Row 2, Cell 2</td><td style="border: 1px solid #ddd; padding: 8px;">Row 2, Cell 3</td></tr><tr><td style="border: 1px solid #ddd; padding: 8px;">Row 3, Cell 1</td><td style="border: 1px solid #ddd; padding: 8px;">Row 3, Cell 2</td><td style="border: 1px solid #ddd; padding: 8px;">Row 3, Cell 3</td></tr></table>',
|
||||
},
|
||||
{
|
||||
title: 'schedule',
|
||||
description: 'Insert schedule table',
|
||||
content: '<table style="width:100%; border-collapse: collapse;"><tr style="border-bottom: 1px solid #ddd;"><th style="border: 1px solid #ddd; padding: 8px;">Time</th><th style="border: 1px solid #ddd; padding: 8px;">Monday</th><th style="border: 1px solid #ddd; padding: 8px;">Tuesday</th><th style="border: 1px solid #ddd; padding: 8px;">Wednesday</th><th style="border: 1px solid #ddd; padding: 8px;">Thursday</th><th style="border: 1px solid #ddd; padding: 8px;">Friday</th></tr><tr><td style="border: 1px solid #ddd; padding: 8px;">9:00 AM</td><td style="border: 1px solid #ddd; padding: 8px;"></td><td style="border: 1px solid #ddd; padding: 8px;"></td><td style="border: 1px solid #ddd; padding: 8px;"></td><td style="border: 1px solid #ddd; padding: 8px;"></td><td style="border: 1px solid #ddd; padding: 8px;"></td></tr><tr><td style="border: 1px solid #ddd; padding: 8px;">10:00 AM</td><td style="border: 1px solid #ddd; padding: 8px;"></td><td style="border: 1px solid #ddd; padding: 8px;"></td><td style="border: 1px solid #ddd; padding: 8px;"></td><td style="border: 1px solid #ddd; padding: 8px;"></td><td style="border: 1px solid #ddd; padding: 8px;"></td></tr><tr><td style="border: 1px solid #ddd; padding: 8px;">11:00 AM</td><td style="border: 1px solid #ddd; padding: 8px;"></td><td style="border: 1px solid #ddd; padding: 8px;"></td><td style="border: 1px solid #ddd; padding: 8px;"></td><td style="border: 1px solid #ddd; padding: 8px;"></td><td style="border: 1px solid #ddd; padding: 8px;"></td></tr></table>',
|
||||
},
|
||||
{
|
||||
title: 'comparison',
|
||||
description: 'Insert comparison table',
|
||||
content: '<table style="width:100%; border-collapse: collapse;"><tr style="border-bottom: 1px solid #ddd;"><th style="border: 1px solid #ddd; padding: 8px;">Feature</th><th style="border: 1px solid #ddd; padding: 8px;">Option A</th><th style="border: 1px solid #ddd; padding: 8px;">Option B</th><th style="border: 1px solid #ddd; padding: 8px;">Option C</th></tr><tr><td style="border: 1px solid #ddd; padding: 8px;">Feature 1</td><td style="border: 1px solid #ddd; padding: 8px;">✓</td><td style="border: 1px solid #ddd; padding: 8px;">✓</td><td style="border: 1px solid #ddd; padding: 8px;">✓</td></tr><tr><td style="border: 1px solid #ddd; padding: 8px;">Feature 2</td><td style="border: 1px solid #ddd; padding: 8px;">✓</td><td style="border: 1px solid #ddd; padding: 8px;">✗</td><td style="border: 1px solid #ddd; padding: 8px;">✓</td></tr><tr><td style="border: 1px solid #ddd; padding: 8px;">Feature 3</td><td style="border: 1px solid #ddd; padding: 8px;">✗</td><td style="border: 1px solid #ddd; padding: 8px;">✓</td><td style="border: 1px solid #ddd; padding: 8px;">✓</td></tr><tr><td style="border: 1px solid #ddd; padding: 8px;">Price</td><td style="border: 1px solid #ddd; padding: 8px;">$</td><td style="border: 1px solid #ddd; padding: 8px;">$$</td><td style="border: 1px solid #ddd; padding: 8px;">$$$</td></tr></table>',
|
||||
},
|
||||
];
|
||||
|
||||
// Additional commands to reach 100 total
|
||||
const additionalCommands = Array.from({ length: 100 - (basicCommands.length + formattingCommands.length + templateCommands.length + taskCommands.length + tableCommands.length) }, (_, i) => {
|
||||
const index = i + 1;
|
||||
return {
|
||||
title: `command${index}`,
|
||||
description: `Example command ${index}`,
|
||||
content: `<p>This is example command ${index}</p>`,
|
||||
};
|
||||
});
|
||||
|
||||
// Combine all command categories
|
||||
return [
|
||||
...basicCommands,
|
||||
...formattingCommands,
|
||||
...templateCommands,
|
||||
...taskCommands,
|
||||
...tableCommands,
|
||||
...additionalCommands,
|
||||
];
|
||||
};
|
||||
126
src/tiptap/tiptap.css
Normal file
126
src/tiptap/tiptap.css
Normal file
@@ -0,0 +1,126 @@
|
||||
:root {
|
||||
--purple-light: #e0e0ff; /* 默认浅紫色背景 */
|
||||
--black: #000000; /* 默认黑色 */
|
||||
--white: #ffffff; /* 默认白色 */
|
||||
--gray-3: #d3d3d3; /* 默认灰色3 */
|
||||
--gray-2: #e5e5e5; /* 默认灰色2 */
|
||||
}
|
||||
.ProseMirror-focused.tiptap {
|
||||
outline: 1px solid #999;
|
||||
}
|
||||
.tiptap-preview {
|
||||
.tiptap {
|
||||
margin: 0;
|
||||
padding: 0.5rem;
|
||||
border: unset;
|
||||
}
|
||||
}
|
||||
.tiptap {
|
||||
/* margin: 0.5rem 1rem; */
|
||||
margin: 0;
|
||||
padding: 0.5rem;
|
||||
border-radius: 5px;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
/* Basic editor styles */
|
||||
.tiptap:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
/* List styles */
|
||||
.tiptap ul,
|
||||
.tiptap ol {
|
||||
padding: 0 1rem;
|
||||
margin: 1.25rem 1rem 1.25rem 0.4rem;
|
||||
}
|
||||
|
||||
.tiptap ul li p,
|
||||
.tiptap ol li p {
|
||||
margin-top: 0.25em;
|
||||
margin-bottom: 0.25em;
|
||||
}
|
||||
|
||||
/* Heading styles */
|
||||
.tiptap h1,
|
||||
.tiptap h2,
|
||||
.tiptap h3,
|
||||
.tiptap h4,
|
||||
.tiptap h5,
|
||||
.tiptap h6 {
|
||||
line-height: 1.1;
|
||||
margin-top: 2.5rem;
|
||||
text-wrap: pretty;
|
||||
}
|
||||
|
||||
.tiptap h1,
|
||||
.tiptap h2 {
|
||||
/* margin-top: 3.5rem; */
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.tiptap h1 {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.tiptap h2 {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tiptap h3 {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.tiptap h4,
|
||||
.tiptap h5,
|
||||
.tiptap h6 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* Code and preformatted text styles */
|
||||
.tiptap code {
|
||||
background-color: var(--purple-light);
|
||||
border-radius: 0.4rem;
|
||||
color: var(--black);
|
||||
font-size: 0.85rem;
|
||||
padding: 0.25em 0.3em;
|
||||
}
|
||||
|
||||
.tiptap pre {
|
||||
border: 1px solid #ccc;
|
||||
/* background: var(--black); */
|
||||
border-radius: 0.5rem;
|
||||
/* color: var(--white); */
|
||||
font-family: 'JetBrainsMono', monospace;
|
||||
margin: 1.5rem 0;
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
|
||||
.tiptap pre code {
|
||||
background: none;
|
||||
color: inherit;
|
||||
font-size: 0.8rem;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.tiptap mark {
|
||||
background-color: #faf594;
|
||||
border-radius: 0.4rem;
|
||||
box-decoration-break: clone;
|
||||
padding: 0.1rem 0.3rem;
|
||||
}
|
||||
|
||||
.tiptap blockquote {
|
||||
border-left: 3px solid var(--gray-3);
|
||||
margin: 1.5rem 0;
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
.tiptap hr {
|
||||
border: none;
|
||||
border-top: 1px solid var(--gray-2);
|
||||
margin: 2rem 0;
|
||||
}
|
||||
20
tsconfig.json
Normal file
20
tsconfig.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"extends": "@kevisual/types/json/frontend.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"strict": false,
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
],
|
||||
"@/agent": [
|
||||
"./src/agent"
|
||||
]
|
||||
},
|
||||
},
|
||||
"include": [
|
||||
"src/**/*",
|
||||
"agent/**/*",
|
||||
"typings.d.ts"
|
||||
],
|
||||
}
|
||||
15
typings.d.ts
vendored
Normal file
15
typings.d.ts
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { DetailedHTMLProps, HTMLAttributes } from 'react';
|
||||
|
||||
interface KvMdEditorProps extends HTMLAttributes<HTMLElement> {
|
||||
markdown?: string;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
declare module 'react' {
|
||||
namespace JSX {
|
||||
interface IntrinsicElements {
|
||||
'kv-template': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
|
||||
'kv-md-editor': DetailedHTMLProps<KvMdEditorProps, HTMLElement>;
|
||||
}
|
||||
}
|
||||
}
|
||||
27
vite.config.lib.ts
Normal file
27
vite.config.lib.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import path from 'path';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
const plugins = [react(), tailwindcss()];
|
||||
|
||||
/**
|
||||
* @see https://vitejs.dev/config/
|
||||
*/
|
||||
export default defineConfig({
|
||||
plugins,
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
},
|
||||
},
|
||||
build: {
|
||||
lib: {
|
||||
entry: path.resolve(__dirname, 'mod.ts'),
|
||||
name: 'KvMd',
|
||||
formats: ['es'],
|
||||
fileName: () => `kv-md.js`,
|
||||
},
|
||||
outDir: 'dist',
|
||||
emptyOutDir: true,
|
||||
},
|
||||
});
|
||||
44
vite.config.ts
Normal file
44
vite.config.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import path from 'path';
|
||||
import pkgs from './package.json';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import dotenv from 'dotenv';
|
||||
dotenv.config();
|
||||
const isDev = process.env.NODE_ENV === 'development';
|
||||
const plugins = [react(), tailwindcss()];
|
||||
|
||||
let target = process.env.VITE_API_URL || 'http://localhost:51015';
|
||||
const apiProxy = { target: target, changeOrigin: true, ws: true, rewriteWsOrigin: true, secure: false, cookieDomainRewrite: 'localhost' };
|
||||
let proxy = {
|
||||
'/root/': apiProxy,
|
||||
'/api': apiProxy,
|
||||
'/client': apiProxy,
|
||||
};
|
||||
const ENV_BASE_NAME = process.env.BASE_NAME;
|
||||
|
||||
const _basename = ENV_BASE_NAME || pkgs.basename;
|
||||
const basename = isDev ? undefined : `${_basename}`;
|
||||
/**
|
||||
* @see https://vitejs.dev/config/
|
||||
*/
|
||||
export default defineConfig(() => {
|
||||
return {
|
||||
plugins,
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
},
|
||||
},
|
||||
base: basename,
|
||||
define: {
|
||||
BASE_NAME: JSON.stringify(basename),
|
||||
BUILD_TIME: JSON.stringify(new Date().toISOString()),
|
||||
},
|
||||
server: {
|
||||
port: 7008,
|
||||
host: '0.0.0.0',
|
||||
proxy,
|
||||
},
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user