init AiEditor
This commit is contained in:
commit
de431f2870
|
@ -0,0 +1,19 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
env: { browser: true, es2020: true },
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:react-hooks/recommended',
|
||||
],
|
||||
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['react-refresh'],
|
||||
rules: {
|
||||
"@typescript-eslint/no-explicit-any": ["off"],
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# ai config
|
||||
demos/xinghuo.ts
|
|
@ -0,0 +1,19 @@
|
|||
|
||||
import {AiEditor} from "../src/core/AiEditor.ts";
|
||||
import {config} from "./xinghuo";
|
||||
|
||||
new AiEditor({
|
||||
element:"#zEditor",
|
||||
placeHolder:"点击输入内容...",
|
||||
content:'通过这里,快速感觉 JPress 的功能和强大,这一切,都试开源、免费、可商用的!',
|
||||
ai:{
|
||||
model:{
|
||||
xinghuo:{
|
||||
...config
|
||||
}
|
||||
}
|
||||
},
|
||||
onMentionQuery:(query:string)=>{
|
||||
return [query];
|
||||
}
|
||||
})
|
|
@ -0,0 +1,14 @@
|
|||
|
||||
|
||||
|
||||
```javascript
|
||||
ai:{
|
||||
model:{
|
||||
xinghuo:{
|
||||
appId:"****",
|
||||
apiKey:"****",
|
||||
apiSecret:"****"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
|
@ -0,0 +1,23 @@
|
|||
# 图片上传配置
|
||||
|
||||
## 文件上传配置
|
||||
|
||||
- uploadUrl: 上传文件地址
|
||||
- uploadHeaders: 上传文件 http 头
|
||||
- uploader: 自定义上传函数
|
||||
|
||||
## 服务器响应
|
||||
|
||||
```json
|
||||
{
|
||||
"errorCode": 0,
|
||||
"data": {
|
||||
"href": "http://your-domain.com/attachment.zip",
|
||||
"fileName": "文件名称"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 图片菜单配置
|
||||
|
||||
- customMenuInvoke:自定义菜单点击事件
|
|
@ -0,0 +1,23 @@
|
|||
# 字体配置
|
||||
|
||||
## 字体配置
|
||||
|
||||
```javascript
|
||||
{
|
||||
fontFamily:{
|
||||
values:[
|
||||
{name: "默认字体", value: ""},
|
||||
{name: "宋体", value: "SimSun"},
|
||||
{name: "仿宋", value: "FangSong"},
|
||||
{name: "黑体", value: "SimHei"},
|
||||
{name: "楷体", value: "KaiTi"},
|
||||
{name: "微软雅黑", value: "Microsoft YaHei"},
|
||||
{name: "方正仿宋简体_GBK", value: "FangSong_GB2312"},
|
||||
{name: "Arial", value: "Arial"},
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
# 字号配置
|
||||
|
||||
## 字号配置
|
||||
|
||||
```javascript
|
||||
{
|
||||
fontSize:{
|
||||
values:[
|
||||
{name: "初号", value: 56},
|
||||
{name: "小初", value: 48},
|
||||
{name: "一号", value: 34.7},
|
||||
{name: "小一", value: 32},
|
||||
{name: "二号", value: 29.3},
|
||||
{name: "小二", value: 24},
|
||||
{name: "三号", value: 21.3},
|
||||
{name: "小三", value: 20},
|
||||
{name: "四号", value: 18.7},
|
||||
{name: "小四", value: 16},
|
||||
{name: "五号", value: 14},
|
||||
{name: "小五", value: 12},
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
# 图片上传配置
|
||||
|
||||
## 文件上传配置
|
||||
|
||||
- uploadUrl: 上传文件地址
|
||||
- uploadHeaders: 上传文件 http 头
|
||||
- uploader: 自定义上传函数
|
||||
|
||||
## 服务器响应
|
||||
|
||||
```json
|
||||
{
|
||||
"errorCode": 0,
|
||||
"data": {
|
||||
"src": "http://your-domain.com/image.jpg",
|
||||
"alt": "图片 alt"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 图片菜单配置
|
||||
|
||||
- customMenuInvoke:自定义菜单点击事件
|
|
@ -0,0 +1,37 @@
|
|||
|
||||
|
||||
|
||||
|
||||
```javascript
|
||||
onMentionQuery:(query:string)=>{
|
||||
return [
|
||||
'Lea Thompson',
|
||||
'Cyndi Lauper',
|
||||
'Tom Cruise',
|
||||
'Madonna',
|
||||
'Jerry Hall',
|
||||
'Joan Collins',
|
||||
'Winona Ryder',
|
||||
'Christina Applegate',
|
||||
'Alyssa Milano',
|
||||
'Molly Ringwald',
|
||||
'Ally Sheedy',
|
||||
'Debbie Harry',
|
||||
'Olivia Newton-John',
|
||||
'Elton John',
|
||||
'Michael J. Fox',
|
||||
'Axl Rose',
|
||||
'Emilio Estevez',
|
||||
'Ralph Macchio',
|
||||
'Rob Lowe',
|
||||
'Jennifer Grey',
|
||||
'Mickey Rourke',
|
||||
'John Cusack',
|
||||
'Matthew Broderick',
|
||||
'Justine Bateman',
|
||||
'Lisa Bonet',
|
||||
]
|
||||
.filter(item => item.toLowerCase().startsWith(query.toLowerCase()))
|
||||
.slice(0, 5)
|
||||
}
|
||||
```
|
|
@ -0,0 +1,23 @@
|
|||
# 图片上传配置
|
||||
|
||||
## 文件上传配置
|
||||
|
||||
- uploadUrl: 上传文件地址
|
||||
- uploadHeaders: 上传文件 http 头
|
||||
- uploader: 自定义上传函数
|
||||
|
||||
## 服务器响应
|
||||
|
||||
```json
|
||||
{
|
||||
"errorCode": 0,
|
||||
"data": {
|
||||
"src": "http://your-domain.com/video.mp4",
|
||||
"poster": "视频缩略图"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 图片菜单配置
|
||||
|
||||
- customMenuInvoke:自定义菜单点击事件
|
|
@ -0,0 +1,37 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>AiEditor Demo</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<script>
|
||||
let isDark = false;
|
||||
function dark(){
|
||||
if (!isDark){
|
||||
document.body.style.background="#1a1b1e"
|
||||
document.querySelector("#title").style.color="#eee"
|
||||
document.querySelector("#zEditor").classList.remove("aie-theme-light");
|
||||
document.querySelector("#zEditor").classList.add("aie-theme-dark");
|
||||
}else {
|
||||
document.body.style.background=""
|
||||
document.querySelector("#title").style.color=""
|
||||
document.querySelector("#zEditor").classList.remove("aie-theme-dark");
|
||||
document.querySelector("#zEditor").classList.add("aie-theme-light");
|
||||
}
|
||||
isDark = !isDark;
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<div style="padding: 10px 20px;font-size: 30px" id="title">
|
||||
AiEditor,一个面向 AI 的下一代富文本编辑器(开源)。
|
||||
<button onclick="dark()">切换主题</button>
|
||||
</div>
|
||||
|
||||
<div id="zEditor" style="height: 650px; width: 960px; margin: 20px"></div>
|
||||
<script type="module" src="/demos/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,48 @@
|
|||
{
|
||||
"name": "aieditor",
|
||||
"private": true,
|
||||
"version": "1.0.0-beta.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"devDependencies": {
|
||||
"less": "^4.2.0",
|
||||
"typescript": "^5.0.2",
|
||||
"vite": "^4.4.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tiptap/core": "^2.1.12",
|
||||
"@tiptap/extension-bubble-menu": "^2.1.12",
|
||||
"@tiptap/extension-character-count": "^2.1.12",
|
||||
"@tiptap/extension-code-block-lowlight": "^2.1.12",
|
||||
"@tiptap/extension-color": "^2.1.12",
|
||||
"@tiptap/extension-font-family": "^2.1.12",
|
||||
"@tiptap/extension-gapcursor": "^2.1.12",
|
||||
"@tiptap/extension-highlight": "^2.1.12",
|
||||
"@tiptap/extension-image": "^2.1.12",
|
||||
"@tiptap/extension-link": "^2.1.12",
|
||||
"@tiptap/extension-mention": "^2.1.12",
|
||||
"@tiptap/extension-placeholder": "^2.1.12",
|
||||
"@tiptap/extension-subscript": "^2.1.12",
|
||||
"@tiptap/extension-superscript": "^2.1.12",
|
||||
"@tiptap/extension-table": "^2.1.12",
|
||||
"@tiptap/extension-table-cell": "^2.1.12",
|
||||
"@tiptap/extension-table-header": "^2.1.12",
|
||||
"@tiptap/extension-table-row": "^2.1.12",
|
||||
"@tiptap/extension-task-item": "^2.1.12",
|
||||
"@tiptap/extension-task-list": "^2.1.12",
|
||||
"@tiptap/extension-text-align": "^2.1.12",
|
||||
"@tiptap/extension-text-style": "^2.1.12",
|
||||
"@tiptap/extension-underline": "^2.1.12",
|
||||
"@tiptap/pm": "^2.1.12",
|
||||
"@tiptap/starter-kit": "^2.1.12",
|
||||
"@tiptap/suggestion": "^2.1.12",
|
||||
"crypto-js": "^4.2.0",
|
||||
"lowlight": "^3.1.0",
|
||||
"node-html-parser": "^6.1.11",
|
||||
"tippy.js": "^6.3.7"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
# AiEditor
|
||||
|
||||
AiEditor 是一个面向 AI 的下一代富文本编辑器。
|
||||
|
||||
## 特征
|
||||
|
||||
- [x] 基础功能:标题、正文、字体、字号、加粗、斜体、下划线、删除线、链接、行内代码、上标、下标、分割线、引用、打印、最大化编辑
|
||||
- [x] 增强功能:撤回、重做、格式刷、橡皮擦、待办事项、字体颜色、背景颜色、Emoji 表情、对齐方式、行高、有(无)序列表、段落缩进、强制换行
|
||||
- [x] 图片:点击上传、粘贴上传、拖拽到编辑器上传、自定义对齐方式
|
||||
- [x] 视频:点击上传、粘贴上传、拖拽到编辑器上传
|
||||
- [x] 附件:点击上传、粘贴上传、拖拽到编辑器上传
|
||||
- [x] 代码:行内代码、代码块、自定义选择代码语言
|
||||
- [x] 表格:插入表格、左增右增、左减右减、上增下增、上减下减、合并单元格、解除合并
|
||||
- [x] A I:AI 续写、AI 优化、AI 校对、AI 翻译
|
||||
- [x] 主题:亮色主题、暗色主题
|
||||
|
||||
## 待完善(计划中...)
|
||||
|
||||
- [ ] 完善自动化测试
|
||||
- [ ] 国际化
|
||||
- [ ] AI 插入图片
|
||||
- [ ] AI 图生图(AI 图片优化)
|
||||
- [ ] AI 一键排版
|
||||
- [ ] 自定义 AI 菜单及其 Prompts
|
||||
- [ ] 进一步强化增贴功能
|
||||
- [ ] 上传视频自动获取缩略图
|
||||
- [ ] WORD 导入、导出
|
||||
- [ ] PDF 导出、PDF 预览
|
||||
- [ ] 团队协作
|
||||
- [ ] 类腾讯文档 UI 风格
|
||||
- [ ] 类 Notion 拖拽功能
|
||||
- [ ] 文心一言、ChatGPT 等其他大模型对接
|
|
@ -0,0 +1,48 @@
|
|||
export class AbstractWebSocket {
|
||||
url: string;
|
||||
webSocket?: WebSocket;
|
||||
isOpen: boolean = false;
|
||||
text?: string;
|
||||
|
||||
constructor(url: string) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
start(text: string) {
|
||||
this.text = text;
|
||||
this.webSocket = new WebSocket(this.url);
|
||||
this.webSocket.onopen = (e) => this.onOpen(e)
|
||||
this.webSocket.onmessage = (e) => this.onMessage(e)
|
||||
this.webSocket.onclose = (e) => this.onClose(e)
|
||||
this.webSocket.onerror = (e) => this.onError(e)
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (this.webSocket) {
|
||||
this.webSocket.close();
|
||||
this.webSocket = void 0;
|
||||
}
|
||||
}
|
||||
|
||||
send(message: string) {
|
||||
if (this.webSocket && this.isOpen) {
|
||||
this.webSocket.send(message);
|
||||
}
|
||||
}
|
||||
|
||||
protected onOpen(_: Event) {
|
||||
this.isOpen = true;
|
||||
this.send(this.text!);
|
||||
}
|
||||
|
||||
protected onMessage(_: MessageEvent) {
|
||||
}
|
||||
|
||||
protected onClose(_: CloseEvent) {
|
||||
this.isOpen = false;
|
||||
}
|
||||
|
||||
protected onError(_: Event) {
|
||||
this.isOpen = false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import {Editor} from "@tiptap/core";
|
||||
|
||||
export interface AiModel {
|
||||
|
||||
start: (seletedText: string, prompt: string, editor: Editor) => void,
|
||||
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
import {AiEditorOptions} from "../core/AiEditor.ts";
|
||||
import {AiModel} from "./AiModel.ts";
|
||||
import {XingHuoModel} from "./xinghuo/XingHuoModel.ts";
|
||||
|
||||
export class AiModelFactory {
|
||||
|
||||
static models:Record<string, AiModel> = {};
|
||||
|
||||
static create(modelName: string, options: AiEditorOptions): AiModel | null {
|
||||
let model = this.models[modelName];
|
||||
if (model) return model;
|
||||
|
||||
if (modelName === "xinghuo"){
|
||||
model = new XingHuoModel(options);
|
||||
}
|
||||
|
||||
if (model){
|
||||
this.models[modelName] = model;
|
||||
}
|
||||
return model;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
import {AiModel} from "../AiModel.ts";
|
||||
import {AiEditorOptions} from "../../core/AiEditor.ts";
|
||||
import {Editor} from "@tiptap/core";
|
||||
import {XingHuoSocket} from "./XingHuoSocket.ts";
|
||||
|
||||
export class XingHuoModel implements AiModel {
|
||||
|
||||
appId: string;
|
||||
apiKey: string;
|
||||
apiSecret: string;
|
||||
version: string;
|
||||
|
||||
constructor(options: AiEditorOptions) {
|
||||
const {appId, apiKey, apiSecret, version} = options.ai?.model.xinghuo!;
|
||||
this.appId = appId;
|
||||
this.apiKey = apiKey;
|
||||
this.apiSecret = apiSecret;
|
||||
this.version = version || "v3.1";
|
||||
}
|
||||
|
||||
start(seletedText: string, prompt: string, editor: Editor): void {
|
||||
const socket = new XingHuoSocket(this.appId, this.apiKey, this.apiSecret, this.version, editor);
|
||||
socket.start(`"${seletedText}"\n${prompt}`)
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
import {AbstractWebSocket} from "../AbstractWebSocket.ts";
|
||||
import {Editor} from "@tiptap/core";
|
||||
|
||||
// @ts-ignore
|
||||
import hmacSHA256 from 'crypto-js/hmac-sha256';
|
||||
// @ts-ignore
|
||||
import Base64 from 'crypto-js/enc-base64';
|
||||
import {uuid} from "../../util/uuid.ts";
|
||||
|
||||
export class XingHuoSocket extends AbstractWebSocket {
|
||||
|
||||
appId: string;
|
||||
version: string;
|
||||
editor: Editor;
|
||||
|
||||
|
||||
constructor(appId: string, apiKey: string, apiSecret: string, version: string, editor: Editor) {
|
||||
super(XingHuoSocket.createUrl(apiKey, apiSecret, version));
|
||||
this.appId = appId;
|
||||
this.version = version;
|
||||
this.editor = editor;
|
||||
}
|
||||
|
||||
static createUrl(apiKey: string, apiSecret: string, version: string): string {
|
||||
const date = new Date().toUTCString().replace("GMT", "+0000");
|
||||
let header = "host: spark-api.xf-yun.com\n"
|
||||
header += "date: " + date + "\n"
|
||||
header += `GET /${version}/chat HTTP/1.1`
|
||||
const hmacSHA = hmacSHA256(header, apiSecret);
|
||||
const base64 = Base64.stringify(hmacSHA)
|
||||
const authorization_origin = `api_key="${apiKey}", algorithm="hmac-sha256", headers="host date request-line", signature="${base64}"`
|
||||
const authorization = btoa(authorization_origin);
|
||||
return `ws://spark-api.xf-yun.com/${version}/chat?authorization=${authorization}&date=${encodeURIComponent(date)}&host=spark-api.xf-yun.com`
|
||||
}
|
||||
|
||||
getDomain() {
|
||||
switch (this.version) {
|
||||
case "v3.1":
|
||||
return "generalv3"
|
||||
case "v2.1":
|
||||
return "generalv2"
|
||||
default:
|
||||
return "general"
|
||||
}
|
||||
}
|
||||
|
||||
send(message: string) {
|
||||
const object = {
|
||||
"header": {
|
||||
"app_id": this.appId,
|
||||
"uid": uuid(),
|
||||
},
|
||||
"parameter": {
|
||||
"chat": {
|
||||
"domain": this.getDomain(),
|
||||
"temperature": 0.5,
|
||||
"max_tokens": 2048,
|
||||
}
|
||||
},
|
||||
"payload": {
|
||||
"message": {
|
||||
"text": [
|
||||
// {"role": "user", "content": "你会做什么"}
|
||||
] as any[]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object.payload.message.text.push(
|
||||
{role: "user", content: message}
|
||||
)
|
||||
|
||||
super.send(JSON.stringify(object));
|
||||
}
|
||||
|
||||
|
||||
protected onMessage(e: MessageEvent) {
|
||||
const data = e.data;
|
||||
// message data format https://www.xfyun.cn/doc/spark/Web.html#_1-%E6%8E%A5%E5%8F%A3%E8%AF%B4%E6%98%8E
|
||||
const message = JSON.parse(data) as any;
|
||||
const text = message.payload.choices.text[0].content as string;
|
||||
|
||||
if (text) {
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const c = text.charAt(i);
|
||||
if ((i == 0 || i == text.length - 1) && c === '"') continue
|
||||
if (c === "\n") this.editor.commands.insertContent("<br/>");
|
||||
else this.editor.commands.insertContent(c);
|
||||
}
|
||||
|
||||
this.editor.commands.scrollIntoView();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
import {MenuButtonOptions} from "../components/Header.ts";
|
||||
|
||||
export default [
|
||||
|
||||
|
||||
|
||||
|
||||
] as MenuButtonOptions[];
|
|
@ -0,0 +1,71 @@
|
|||
import tippy, {Instance, Placement} from "tippy.js";
|
||||
|
||||
|
||||
export class Popover {
|
||||
|
||||
tippyInstance?: Instance;
|
||||
content?: string;
|
||||
onConfirmClickFunc?: (instance: Instance)=>void;
|
||||
onShowFunc?: (instance: Instance)=>void;
|
||||
|
||||
|
||||
setContent(content:string){
|
||||
this.content =content;
|
||||
}
|
||||
|
||||
onConfirmClick(onConfirmClick:(instance:Instance)=>void){
|
||||
this.onConfirmClickFunc = onConfirmClick;
|
||||
}
|
||||
|
||||
onShow(onShow:(instance:Instance)=>void){
|
||||
this.onShowFunc = onShow;
|
||||
}
|
||||
|
||||
setTrigger(triggerEl: HTMLElement,placement:Placement = "bottom") {
|
||||
this.tippyInstance = tippy(triggerEl, {
|
||||
content: this.createContentElement(),
|
||||
appendTo: triggerEl.closest(".aie-container")!,
|
||||
placement: placement,
|
||||
trigger: 'click',
|
||||
interactive: true,
|
||||
arrow: false,
|
||||
onShow:(_)=>{this.onShowFunc && this.onShowFunc(_)}
|
||||
})
|
||||
}
|
||||
|
||||
createContentElement() {
|
||||
const template = `
|
||||
<div class="aie-popover">
|
||||
<div class="aie-popover-header">
|
||||
<svg class="aie-popover-header-close" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12.0007 10.5865L16.9504 5.63672L18.3646 7.05093L13.4149 12.0007L18.3646 16.9504L16.9504 18.3646L12.0007 13.4149L7.05093 18.3646L5.63672 16.9504L10.5865 12.0007L5.63672 7.05093L7.05093 5.63672L12.0007 10.5865Z"></path></svg>
|
||||
</div>
|
||||
<div class="aie-popover-content">${this.content}</div>
|
||||
<div class="aie-popover-footer">
|
||||
<button class="aie-popover-footer-confirm" type="button">确定</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const container = document.createElement("div");
|
||||
container.innerHTML = template;
|
||||
|
||||
container.querySelector(".aie-popover-header-close")!
|
||||
.addEventListener("click", () => {
|
||||
this.tippyInstance?.hide();
|
||||
})
|
||||
|
||||
container.querySelector(".aie-popover-footer-confirm")!
|
||||
.addEventListener("click", () => {
|
||||
this.onConfirmClickFunc && this.onConfirmClickFunc(this.tippyInstance!);
|
||||
this.tippyInstance!.hide();
|
||||
})
|
||||
|
||||
return container;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
export const Svgs = {
|
||||
check:`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"></path><path d="M10.0007 15.1709L19.1931 5.97852L20.6073 7.39273L10.0007 17.9993L3.63672 11.6354L5.05093 10.2212L10.0007 15.1709Z"></path></svg>`,
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
import {AiEditorOptions, AiEditorEvent} from "../core/AiEditor.ts";
|
||||
import {Editor, EditorEvents} from "@tiptap/core";
|
||||
|
||||
export type BubbleMenuItem = {
|
||||
id: string,
|
||||
svg: string,
|
||||
}
|
||||
|
||||
export abstract class AbstractBubbleMenu extends HTMLElement implements AiEditorEvent{
|
||||
|
||||
editor?: Editor;
|
||||
items: BubbleMenuItem[] = [];
|
||||
|
||||
protected constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
|
||||
connectedCallback() {
|
||||
this.innerHTML = `
|
||||
<div class="aie-bubble-menu">
|
||||
${this.items!.map((item) => {
|
||||
return `
|
||||
<div class="aie-bubble-menu-item" id="${item.id}">${item.svg}</div>
|
||||
`
|
||||
}).join('')}
|
||||
</div>
|
||||
`;
|
||||
|
||||
this.querySelector("div")!.addEventListener("click", (e) => {
|
||||
this.items.forEach((item) => {
|
||||
const target = (e.target as any).closest(`#${item.id}`);
|
||||
if (target) this.onItemClick(item.id);
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
onCreate(props: EditorEvents['create'], _: AiEditorOptions){
|
||||
this.editor = props.editor
|
||||
}
|
||||
|
||||
abstract onItemClick(id: string): void;
|
||||
|
||||
abstract onTransaction(props: EditorEvents['transaction']):void
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,133 @@
|
|||
import {AbstractMenuButton} from "./AbstractMenuButton.ts";
|
||||
import tippy from "tippy.js";
|
||||
|
||||
|
||||
const colors = [
|
||||
'ffffff', '000000', 'e9d989', '2972f4', '609eec', 'de3c36', 'a1d533', '7334c5', '27b5d9', 'ff8926',
|
||||
'f2f2f2', '7f7f7f', 'ddd9c3', 'c6d9f0', 'dbe5f1', 'f2dcdb', 'ebf1dd', 'e5e0ec', 'dbeef3', 'fdeada',
|
||||
'd8d8d8', '595959', 'c4bd97', '8db3e2', 'b8cce4', 'e5b9b7', 'd7e3bc', 'ccc1d9', 'b7dde8', 'fbd5b5',
|
||||
'bfbfbf', '3f3f3f', '938953', '548dd4', '95b3d7', 'd99694', 'c3d69b', 'b2a2c7', '92cddc', 'fac08f',
|
||||
'a5a5a5', '262626', '494429', '17365d', '366092', '953734', '76923c', '5f497a', '31859b', 'e36c09',
|
||||
'6e6e6e', '0c0c0c', '1d1b10', '0f243e', '244061', '632423', '4f6128', '3f3151', '205867', '974806'],
|
||||
standardColors = ['c00000', 'ff0000', 'ffc000', 'ffff00', '92d050', '00b050', '00b0f0', '0070c0', '002060', '7030a0'];
|
||||
|
||||
export class AbstractColorsMenuButton extends AbstractMenuButton {
|
||||
|
||||
historyColors: string[] = [];
|
||||
|
||||
iconSvg?:string;
|
||||
|
||||
menuColorEL?:HTMLDivElement;
|
||||
|
||||
onColorItemClick?:(color:string)=>void;
|
||||
|
||||
onDefaultColorClick?:()=>void;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.template = `
|
||||
<div style="width: 36px;height: 18px;display: flex">
|
||||
<div style="width: 18px;height: 18px" id="btn">
|
||||
<div style="height: 15px;width: 15px;padding:0 1.5px;line-height: 18px">
|
||||
${this.iconSvg}
|
||||
</div>
|
||||
<div style="width: 18px;height: 3px;background: #333" id="menuColorEL"></div>
|
||||
</div>
|
||||
<div style="width: 18px;height: 18px" id="dropdown">
|
||||
<!-- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 16L6 10H18L12 16Z"></path></svg>-->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"></path><path d="M12 14L8 10H16L12 14Z"></path></svg>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
super.connectedCallback();
|
||||
|
||||
|
||||
this.querySelector("#btn")!.addEventListener("click", () => {
|
||||
this.onColorItemClick!(this.historyColors.length >0 ? this.historyColors[0] : '#ccc')
|
||||
});
|
||||
|
||||
this.menuColorEL = this.querySelector("#menuColorEL")!;
|
||||
|
||||
tippy(this.querySelector("#dropdown")!, {
|
||||
content: this.createMenuElement(),
|
||||
placement: 'bottom',
|
||||
trigger: 'click',
|
||||
interactive: true,
|
||||
})
|
||||
}
|
||||
|
||||
createMenuElement() {
|
||||
const div = document.createElement("div");
|
||||
div.style.height = "278px"
|
||||
div.style.width = "250px"
|
||||
div.classList.add("aie-dropdown-container")
|
||||
div.innerHTML = `
|
||||
<div class="color-panel">
|
||||
<div class="color-panel-default-button" id="defaultColor">默认</div>
|
||||
<div style="display: flex;flex-wrap: wrap;padding-top: 5px">
|
||||
${colors.map((color, index) => {
|
||||
return `<div class="color-item" data-color="#${color}" style="width: 18px;height:18px;margin:1px;padding:1px;border:1px solid #${index == 0 ? 'efefef' : color};background: #${color}"></div>`
|
||||
}).join(" ")
|
||||
}
|
||||
</div>
|
||||
<div class="color-panel-title">标准色</div>
|
||||
<div style="display: flex;flex-wrap: wrap;">
|
||||
${standardColors.map((color) => {
|
||||
return `<div class="color-item" data-color="#${color}" style="width: 18px;height:18px;margin:1px;padding:1px;border:1px solid #${color};background: #${color}"></div>`
|
||||
}).join(" ")
|
||||
}
|
||||
</div>
|
||||
<div class="color-panel-title">最近使用</div>
|
||||
<div style="display: flex;flex-wrap: wrap;" id="history-colors">
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
div.querySelector("#defaultColor")!.addEventListener("click",()=>{
|
||||
this.onDefaultColorClick!();
|
||||
})
|
||||
|
||||
div.querySelectorAll(".color-item").forEach((element) => {
|
||||
element.addEventListener("click", () => {
|
||||
const color = element.getAttribute("data-color");
|
||||
this.historyColors.unshift(color!)
|
||||
if (this.historyColors.length > 7){
|
||||
this.historyColors = this.historyColors.slice(0,7);
|
||||
}
|
||||
div.querySelector("#history-colors")!.innerHTML = `
|
||||
${this.historyColors.map((color) => {
|
||||
return `<div class="history-color-item" data-color="${color}" style="width: 22px;height: 23px;margin: 1px;background: ${color}"></div>`
|
||||
}).join(" ")
|
||||
}
|
||||
`;
|
||||
this.menuColorEL!.style.background = color as string;
|
||||
this.onColorItemClick!(color!);
|
||||
})
|
||||
element.addEventListener("mouseover",()=>{
|
||||
(element as HTMLDivElement).style.border="solid 1px #999"
|
||||
})
|
||||
element.addEventListener("mouseout",()=>{
|
||||
let color = element.getAttribute("data-color");
|
||||
if(color === '#ffffff') color='#efefef';
|
||||
(element as HTMLDivElement).style.border=`solid 1px ${color}`
|
||||
})
|
||||
})
|
||||
|
||||
div.querySelector("#history-colors")!.addEventListener("click",(e)=>{
|
||||
const target:HTMLDivElement = (e.target as any).closest('.history-color-item'); // Or any other selector.
|
||||
if (target){
|
||||
let color = target.getAttribute("data-color");
|
||||
this.menuColorEL!.style.background = color as string;
|
||||
this.onColorItemClick!(color!);
|
||||
}
|
||||
});
|
||||
|
||||
return div;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
import {AbstractMenuButton} from "./AbstractMenuButton.ts";
|
||||
import tippy, {Instance} from "tippy.js";
|
||||
import {Editor, EditorEvents} from "@tiptap/core";
|
||||
|
||||
export abstract class AbstractDropdownMenuButton<T> extends AbstractMenuButton {
|
||||
tippyInstance?: Instance;
|
||||
tippyEl?: HTMLDivElement;
|
||||
textEl?: HTMLDivElement;
|
||||
menuData: T[] = [];
|
||||
menuTextWidth: string = "40px";
|
||||
defaultMenuIndex: number = 0;
|
||||
refreshMenuText:boolean = true;
|
||||
width: string = "48px";
|
||||
dropDivWith: string = "100px";
|
||||
dropDivHeight: string = "260px";
|
||||
|
||||
renderTemplate() {
|
||||
this.template = `
|
||||
<div style="width: ${this.width};">
|
||||
<div style="display: flex" id="tippy">
|
||||
<span style="line-height: 18px;font-size: 14px;text-align:center;overflow: hidden; width: ${this.menuTextWidth}" id="text">
|
||||
${this.onMenuTextRender(this.defaultMenuIndex)}
|
||||
</span>
|
||||
<div style="width: 18px;height: 18px;display: inline-block">
|
||||
<!-- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 16L6 10H18L12 16Z"></path></svg>-->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"></path><path d="M12 14L8 10H16L12 14Z"></path></svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.renderTemplate();
|
||||
super.connectedCallback();
|
||||
|
||||
this.textEl = this.querySelector("#text") as HTMLDivElement;
|
||||
this.tippyInstance = tippy(this.querySelector("#tippy")!, {
|
||||
content: this.createMenuElement(),
|
||||
appendTo: this.closest(".aie-container")!,
|
||||
placement: 'bottom',
|
||||
trigger: 'click',
|
||||
interactive: true,
|
||||
arrow: false,
|
||||
})
|
||||
}
|
||||
|
||||
createMenuElement() {
|
||||
const div = document.createElement("div");
|
||||
div.style.height = this.dropDivHeight;
|
||||
div.style.width = this.dropDivWith;
|
||||
div.classList.add("aie-dropdown-container");
|
||||
|
||||
for (let i = 0; i < this.menuData.length; i++) {
|
||||
const item = document.createElement("div");
|
||||
item.classList.add("aie-dropdown-item");
|
||||
item.innerHTML = `
|
||||
<div class="red-dot-container" id="item${i}"><div class="${i == 0 ? "red-dot" : ""}"></div></div>
|
||||
<div class="text">${this.onDropdownItemRender(i)}</div>
|
||||
`
|
||||
item.addEventListener("click", () => {
|
||||
this.onDropdownItemClick(i);
|
||||
this.tippyInstance!.hide()
|
||||
});
|
||||
div.appendChild(item)
|
||||
}
|
||||
this.tippyEl = div;
|
||||
return div;
|
||||
}
|
||||
|
||||
|
||||
onTransaction(event: EditorEvents["transaction"]) {
|
||||
const redDot = this.tippyEl!.querySelector(".red-dot");
|
||||
if (redDot) {
|
||||
redDot.classList.remove("red-dot")
|
||||
}
|
||||
for (let index = 0; index < this.menuData.length; index++) {
|
||||
if (this.onDropdownActive(event.editor, index)) {
|
||||
this.tippyEl!.querySelector(`#item${index}`)!.children[0].classList.add("red-dot")
|
||||
if (this.refreshMenuText){
|
||||
const menuTextResult = this.onMenuTextRender(index);
|
||||
if (typeof menuTextResult === "string") {
|
||||
this.textEl!.innerHTML = menuTextResult;
|
||||
} else {
|
||||
this.textEl?.removeChild(this.textEl?.firstChild!);
|
||||
this.textEl?.appendChild(menuTextResult as Node);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abstract onDropdownActive(editor: Editor, index: number): boolean;
|
||||
|
||||
abstract onMenuTextRender(index: number): Element | string;
|
||||
|
||||
abstract onDropdownItemRender(index: number): Element | string;
|
||||
|
||||
abstract onDropdownItemClick(index: number): void;
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
import {AiEditorOptions, AiEditorEvent} from "../core/AiEditor.ts";
|
||||
import {Editor, EditorEvents} from "@tiptap/core";
|
||||
// @ts-ignore
|
||||
import {ChainedCommands} from "@tiptap/core/dist/packages/core/src/types";
|
||||
|
||||
export class AbstractMenuButton extends HTMLElement implements AiEditorEvent {
|
||||
|
||||
template: string = '';
|
||||
editor?: Editor;
|
||||
options?: AiEditorOptions;
|
||||
|
||||
protected constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
protected registerClickListener() {
|
||||
this.addEventListener("click", () => {
|
||||
const chain = this.editor?.chain();
|
||||
this.onClick(chain);
|
||||
chain?.run();
|
||||
})
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.innerHTML = this.template;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
onClick(commands: ChainedCommands) {
|
||||
//do nothing
|
||||
}
|
||||
|
||||
onCreate(props: EditorEvents["create"], options: AiEditorOptions): void {
|
||||
this.editor = props.editor;
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
onTransaction(event: EditorEvents["transaction"]): void {
|
||||
const htmlDivElement = this.querySelector("div");
|
||||
if (!htmlDivElement) return;
|
||||
if (this.onActive(event.editor)) {
|
||||
htmlDivElement.classList.add("active")
|
||||
} else {
|
||||
htmlDivElement.classList.remove("active")
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
onActive(editor: Editor): boolean {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
import {AiEditorOptions, AiEditorEvent} from "../core/AiEditor.ts";
|
||||
import {EditorEvents} from "@tiptap/core";
|
||||
|
||||
|
||||
export class Footer extends HTMLElement implements AiEditorEvent {
|
||||
count: number = 0
|
||||
// shadow:ShadowRoot;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
// this. shadow = this.attachShadow({
|
||||
// mode:"closed"
|
||||
// });
|
||||
|
||||
}
|
||||
|
||||
updateCharacters() {
|
||||
this.innerHTML = `
|
||||
<div>
|
||||
Powered by AiEditor, Characters: ${this.count}
|
||||
</div>
|
||||
`;
|
||||
// this.querySelector("div")!
|
||||
// .innerHTML = `
|
||||
// Powered by AiEditor, Characters: ${this.count}
|
||||
// `;
|
||||
}
|
||||
|
||||
onCreate(props: EditorEvents["create"], _: AiEditorOptions): void {
|
||||
this.count = props.editor.storage.characterCount.characters()
|
||||
this.updateCharacters()
|
||||
}
|
||||
|
||||
onTransaction(props: EditorEvents["transaction"]): void {
|
||||
const newCount = props.editor.storage.characterCount.characters();
|
||||
if (newCount != this.count) {
|
||||
this.count = newCount
|
||||
this.updateCharacters()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
import {AiEditorOptions, AiEditorEvent} from "../core/AiEditor.ts";
|
||||
import {EditorEvents} from "@tiptap/core";
|
||||
import {Undo} from "./menus/Undo";
|
||||
import {AbstractMenuButton} from "./AbstractMenuButton.ts";
|
||||
import {Redo} from "./menus/Redo";
|
||||
import {Title} from "./menus/Title";
|
||||
import {FontFamily} from "./menus/FontFamily";
|
||||
import {FontSize} from "./menus/FontSize";
|
||||
import {Bold} from "./menus/Bold";
|
||||
import {Italic} from "./menus/Italic";
|
||||
import {Underline} from "./menus/Underline";
|
||||
import {Strike} from "./menus/Strike";
|
||||
import {Subscript} from "./menus/Subscript";
|
||||
import {Superscript} from "./menus/Superscript";
|
||||
import {Highlight} from "./menus/Highlight";
|
||||
import {FontColor} from "./menus/FontColor";
|
||||
import {Divider} from "./menus/Divider";
|
||||
import {BulletList} from "./menus/BulletList";
|
||||
import {OrderedList} from "./menus/OrderedList";
|
||||
import {IndentDecrease} from "./menus/IndentDecrease";
|
||||
import {IndentIncrease} from "./menus/IndentIncrease";
|
||||
import {Align} from "./menus/Align";
|
||||
import {Link} from "./menus/Link";
|
||||
import {Todo} from "./menus/Todo";
|
||||
import {LineHeight} from "./menus/LineHeight";
|
||||
import {Quote} from "./menus/Quote";
|
||||
import {Image} from "./menus/Image";
|
||||
import {Video} from "./menus/Video";
|
||||
import {Code} from "./menus/Code";
|
||||
import {CodeBlock} from "./menus/CodeBlock";
|
||||
import {Eraser} from "./menus/Eraser";
|
||||
import {Hr} from "./menus/Hr";
|
||||
import {Table} from "./menus/Table";
|
||||
import {Break} from "./menus/Break";
|
||||
import {Attachment} from "./menus/Attachment";
|
||||
import {Fullscreen} from "./menus/Fullscreen";
|
||||
import {Printer} from "./menus/Printer";
|
||||
import {Emoji} from "./menus/Emoji";
|
||||
import {Painter} from "./menus/Painter";
|
||||
import {Ai} from "./menus/Ai.ts";
|
||||
|
||||
window.customElements.define('aie-undo', Undo);
|
||||
window.customElements.define('aie-redo', Redo);
|
||||
window.customElements.define('aie-brush', Painter);
|
||||
window.customElements.define('aie-eraser', Eraser);
|
||||
window.customElements.define('aie-title', Title);
|
||||
window.customElements.define('aie-font-family', FontFamily);
|
||||
window.customElements.define('aie-font-size', FontSize);
|
||||
window.customElements.define('aie-bold', Bold);
|
||||
window.customElements.define('aie-italic', Italic);
|
||||
window.customElements.define('aie-underline', Underline);
|
||||
window.customElements.define('aie-strike', Strike);
|
||||
window.customElements.define('aie-link', Link);
|
||||
window.customElements.define('aie-code', Code);
|
||||
window.customElements.define('aie-subscript', Subscript);
|
||||
window.customElements.define('aie-superscript', Superscript);
|
||||
window.customElements.define('aie-highlight', Highlight);
|
||||
window.customElements.define('aie-font-color', FontColor);
|
||||
window.customElements.define('aie-divider', Divider);
|
||||
window.customElements.define('aie-bullet-list', BulletList);
|
||||
window.customElements.define('aie-ordered-list', OrderedList);
|
||||
window.customElements.define('aie-indent-decrease', IndentDecrease);
|
||||
window.customElements.define('aie-indent-increase', IndentIncrease);
|
||||
window.customElements.define('aie-align', Align);
|
||||
window.customElements.define('aie-todo', Todo);
|
||||
window.customElements.define('aie-line-height', LineHeight);
|
||||
window.customElements.define('aie-break', Break);
|
||||
window.customElements.define('aie-quote', Quote);
|
||||
window.customElements.define('aie-image', Image);
|
||||
window.customElements.define('aie-video', Video);
|
||||
window.customElements.define('aie-code-block', CodeBlock);
|
||||
window.customElements.define('aie-hr', Hr);
|
||||
window.customElements.define('aie-table', Table);
|
||||
window.customElements.define('aie-attachment', Attachment);
|
||||
window.customElements.define('aie-fullscreen', Fullscreen);
|
||||
window.customElements.define('aie-printer', Printer);
|
||||
window.customElements.define('aie-emoji', Emoji);
|
||||
window.customElements.define('aie-ai', Ai);
|
||||
|
||||
export type MenuButtonOptions = {
|
||||
key: string,
|
||||
title: string,
|
||||
svg: string,
|
||||
}
|
||||
|
||||
|
||||
export class Header extends HTMLElement implements AiEditorEvent {
|
||||
// template:string;
|
||||
menuButtons: AbstractMenuButton[] = [];
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
const divElement = document.createElement("div");
|
||||
for (let menuButton of this.menuButtons) {
|
||||
divElement.appendChild(menuButton);
|
||||
}
|
||||
divElement.style.display = "flex"
|
||||
divElement.style.flexWrap = "wrap"
|
||||
this.appendChild(divElement)
|
||||
}
|
||||
|
||||
onCreate(event: EditorEvents["create"], options: AiEditorOptions): void {
|
||||
if (!options.toolbarKeys || options.toolbarKeys.length == 0) {
|
||||
// menuDefs.
|
||||
}
|
||||
|
||||
options.toolbarKeys = ["undo", "redo", "brush", "eraser", "divider", "title", "font-family", "font-size", "divider", "bold", "italic", "underline"
|
||||
, "strike", "link", "code", "subscript", "superscript", "hr", "todo", "emoji", "divider", "highlight", "font-color", "divider"
|
||||
, "align", "line-height", "divider", "bullet-list", "ordered-list", "indent-decrease", "indent-increase", "break", "divider"
|
||||
, "image", "video", "attachment", "quote", "code-block", "table", "divider", "printer", "fullscreen","ai"
|
||||
]
|
||||
|
||||
for (let toolbarKey of options.toolbarKeys) {
|
||||
const menuButton = document.createElement("aie-" + toolbarKey) as AbstractMenuButton;
|
||||
menuButton.classList.add("aie-menu-item")
|
||||
menuButton.onCreate(event, options);
|
||||
this.menuButtons.push(menuButton);
|
||||
}
|
||||
}
|
||||
|
||||
onTransaction(event: EditorEvents["transaction"]): void {
|
||||
for (let menuButton of this.menuButtons) {
|
||||
menuButton.onTransaction(event);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
import {AbstractBubbleMenu} from "../AbstractBubbleMenu.ts";
|
||||
import {EditorEvents} from "@tiptap/core";
|
||||
|
||||
export class ImageBubbleMenu extends AbstractBubbleMenu {
|
||||
constructor() {
|
||||
super();
|
||||
this.items = [
|
||||
{
|
||||
id: "left",
|
||||
svg: "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path d=\"M3 4H21V6H3V4ZM3 19H17V21H3V19ZM3 14H21V16H3V14ZM3 9H17V11H3V9Z\"></path></svg>",
|
||||
},
|
||||
{
|
||||
id: "center",
|
||||
svg: "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path d=\"M3 4H21V6H3V4ZM5 19H19V21H5V19ZM3 14H21V16H3V14ZM5 9H19V11H5V9Z\"></path></svg>",
|
||||
},
|
||||
{
|
||||
id: "right",
|
||||
svg: "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path d=\"M3 4H21V6H3V4ZM7 19H21V21H7V19ZM3 14H21V16H3V14ZM7 9H21V11H7V9Z\"></path></svg>"
|
||||
},
|
||||
{
|
||||
id: "delete",
|
||||
svg: "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path d=\"M7 4V2H17V4H22V6H20V21C20 21.5523 19.5523 22 19 22H5C4.44772 22 4 21.5523 4 21V6H2V4H7ZM6 6V20H18V6H6ZM9 9H11V17H9V9ZM13 9H15V17H13V9Z\"></path></svg>"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
onItemClick(id: string): void {
|
||||
if (id != "delete"){
|
||||
const attrs = this.editor?.getAttributes("image")!;
|
||||
attrs.align = id;
|
||||
this.editor?.chain().setImage(attrs as any).run();
|
||||
}else {
|
||||
this.editor?.commands.deleteSelection();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
onTransaction(_: EditorEvents["transaction"]): void {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
import {AbstractBubbleMenu} from "../AbstractBubbleMenu.ts";
|
||||
import {Popover} from "../../commons/Popover.ts";
|
||||
import {EditorEvents} from "@tiptap/core";
|
||||
|
||||
|
||||
export class LinkBubbleMenu extends AbstractBubbleMenu {
|
||||
constructor() {
|
||||
super();
|
||||
this.items = [
|
||||
{
|
||||
id: "edit",
|
||||
svg: "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path d=\"M6.41421 15.89L16.5563 5.74786L15.1421 4.33365L5 14.4758V15.89H6.41421ZM7.24264 17.89H3V13.6474L14.435 2.21233C14.8256 1.8218 15.4587 1.8218 15.8492 2.21233L18.6777 5.04075C19.0682 5.43128 19.0682 6.06444 18.6777 6.45497L7.24264 17.89ZM3 19.89H21V21.89H3V19.89Z\"></path></svg>",
|
||||
},
|
||||
{
|
||||
id: "unlink",
|
||||
svg: "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path d=\"M17 17H22V19H19V22H17V17ZM7 7H2V5H5V2H7V7ZM18.364 15.5355L16.9497 14.1213L18.364 12.7071C20.3166 10.7545 20.3166 7.58866 18.364 5.63604C16.4113 3.68342 13.2455 3.68342 11.2929 5.63604L9.87868 7.05025L8.46447 5.63604L9.87868 4.22183C12.6123 1.48816 17.0445 1.48816 19.7782 4.22183C22.5118 6.9555 22.5118 11.3877 19.7782 14.1213L18.364 15.5355ZM15.5355 18.364L14.1213 19.7782C11.3877 22.5118 6.9555 22.5118 4.22183 19.7782C1.48816 17.0445 1.48816 12.6123 4.22183 9.87868L5.63604 8.46447L7.05025 9.87868L5.63604 11.2929C3.68342 13.2455 3.68342 16.4113 5.63604 18.364C7.58866 20.3166 10.7545 20.3166 12.7071 18.364L14.1213 16.9497L15.5355 18.364ZM14.8284 7.75736L16.2426 9.17157L9.17157 16.2426L7.75736 14.8284L14.8284 7.75736Z\"></path></svg>",
|
||||
},
|
||||
{
|
||||
id: "visit",
|
||||
svg: "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path d=\"M10 6V8H5V19H16V14H18V20C18 20.5523 17.5523 21 17 21H4C3.44772 21 3 20.5523 3 20V7C3 6.44772 3.44772 6 4 6H10ZM21 3V11H19L18.9999 6.413L11.2071 14.2071L9.79289 12.7929L17.5849 5H13V3H21Z\"></path></svg>",
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
const popover = new Popover();
|
||||
popover.setContent(`
|
||||
<div style="width: 250px">
|
||||
链接地址
|
||||
</div>
|
||||
<div style="width: 250px">
|
||||
<input type="text" id="href" style="width: 240px">
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 10px">
|
||||
打开方式
|
||||
</div>
|
||||
<div>
|
||||
<select id="target" style="width: 250px">
|
||||
<option value="">默认</option>
|
||||
<option value="_blank">新窗口</option>
|
||||
</select>
|
||||
</div>
|
||||
`);
|
||||
|
||||
popover.onConfirmClick((instance) => {
|
||||
const href = (instance.popper.querySelector("#href") as HTMLInputElement).value;
|
||||
if (href.trim() === "") {
|
||||
this.editor?.chain().focus().extendMarkRange('link')
|
||||
.unsetLink()
|
||||
.run()
|
||||
return;
|
||||
}
|
||||
|
||||
let target: string | null = (instance.popper.querySelector("#target") as HTMLInputElement).value;
|
||||
if (target.trim() === "") {
|
||||
target = null;
|
||||
}
|
||||
|
||||
this.editor?.chain().focus().extendMarkRange("link")
|
||||
.setLink({
|
||||
href,
|
||||
target,
|
||||
rel: null,
|
||||
}).run()
|
||||
});
|
||||
|
||||
|
||||
popover.onShow((instance) => {
|
||||
const attrs = this.editor?.getAttributes("link");
|
||||
if (attrs && attrs.href) {
|
||||
(instance.popper.querySelector("#href") as HTMLInputElement).value = attrs.href;
|
||||
}
|
||||
if (attrs && attrs.target) {
|
||||
(instance.popper.querySelector("#target") as HTMLInputElement).value = attrs.target;
|
||||
}
|
||||
})
|
||||
|
||||
popover.setTrigger(this.querySelector("div")!, "left");
|
||||
}
|
||||
|
||||
onItemClick(id: string): void {
|
||||
// alert("id:" + id)
|
||||
if (id === "unlink"){
|
||||
this.editor?.chain().focus().unsetLink().run();
|
||||
}else if (id === "visit"){
|
||||
window.open(this.editor?.getAttributes("link").href,"_blank")
|
||||
}
|
||||
}
|
||||
|
||||
onTransaction(_: EditorEvents["transaction"]): void {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,177 @@
|
|||
import {AbstractBubbleMenu} from "../AbstractBubbleMenu.ts";
|
||||
import {EditorEvents} from "@tiptap/core";
|
||||
import {CellSelection, TableMap} from '@tiptap/pm/tables';
|
||||
import {EditorView} from "@tiptap/pm/view";
|
||||
|
||||
export class TableBubbleMenu extends AbstractBubbleMenu {
|
||||
constructor() {
|
||||
super();
|
||||
this.items = [
|
||||
{
|
||||
id: "insert-column-left",
|
||||
svg: "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" d=\"M0 0h24v24H0z\"></path><path d=\"M20 3C20.5523 3 21 3.44772 21 4V20C21 20.5523 20.5523 21 20 21H14C13.4477 21 13 20.5523 13 20V4C13 3.44772 13.4477 3 14 3H20ZM19 5H15V19H19V5ZM6 7C8.76142 7 11 9.23858 11 12C11 14.7614 8.76142 17 6 17C3.23858 17 1 14.7614 1 12C1 9.23858 3.23858 7 6 7ZM7 9H5V10.999L3 11V13L5 12.999V15H7V12.999L9 13V11L7 10.999V9Z\"></path></svg>",
|
||||
},
|
||||
{
|
||||
id: "insert-column-right",
|
||||
svg: "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" d=\"M0 0h24v24H0z\"></path><path d=\"M10 3C10.5523 3 11 3.44772 11 4V20C11 20.5523 10.5523 21 10 21H4C3.44772 21 3 20.5523 3 20V4C3 3.44772 3.44772 3 4 3H10ZM9 5H5V19H9V5ZM18 7C20.7614 7 23 9.23858 23 12C23 14.7614 20.7614 17 18 17C15.2386 17 13 14.7614 13 12C13 9.23858 15.2386 7 18 7ZM19 9H17V10.999L15 11V13L17 12.999V15H19V12.999L21 13V11L19 10.999V9Z\"></path></svg>",
|
||||
},
|
||||
{
|
||||
id: "insert-row-top",
|
||||
svg: "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" d=\"M0 0h24v24H0z\"></path><path d=\"M20 13C20.5523 13 21 13.4477 21 14V20C21 20.5523 20.5523 21 20 21H4C3.44772 21 3 20.5523 3 20V14C3 13.4477 3.44772 13 4 13H20ZM19 15H5V19H19V15ZM12 1C14.7614 1 17 3.23858 17 6C17 8.76142 14.7614 11 12 11C9.23858 11 7 8.76142 7 6C7 3.23858 9.23858 1 12 1ZM13 3H11V4.999L9 5V7L11 6.999V9H13V6.999L15 7V5L13 4.999V3Z\"></path></svg>"
|
||||
},
|
||||
{
|
||||
id: "insert-row-bottom",
|
||||
svg: "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" d=\"M0 0h24v24H0z\"></path><path d=\"M12 13C14.7614 13 17 15.2386 17 18C17 20.7614 14.7614 23 12 23C9.23858 23 7 20.7614 7 18C7 15.2386 9.23858 13 12 13ZM13 15H11V16.999L9 17V19L11 18.999V21H13V18.999L15 19V17L13 16.999V15ZM20 3C20.5523 3 21 3.44772 21 4V10C21 10.5523 20.5523 11 20 11H4C3.44772 11 3 10.5523 3 10V4C3 3.44772 3.44772 3 4 3H20ZM5 5V9H19V5H5Z\"></path></svg>"
|
||||
},
|
||||
{
|
||||
id: "delete-column",
|
||||
svg: "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" d=\"M0 0h24v24H0z\"></path><path d=\"M12 3C12.5523 3 13 3.44772 13 4L12.9998 11.9998C13.8355 11.372 14.8743 11 16 11C18.7614 11 21 13.2386 21 16C21 18.7614 18.7614 21 16 21C14.9681 21 14.0092 20.6874 13.2129 20.1518L13 20C13 20.5523 12.5523 21 12 21H6C5.44772 21 5 20.5523 5 20V4C5 3.44772 5.44772 3 6 3H12ZM11 5H7V19H11V5ZM19 15H13V17H19V15Z\"></path></svg>"
|
||||
},
|
||||
{
|
||||
id: "delete-row",
|
||||
svg: "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" d=\"M0 0h24v24H0z\"></path><path d=\"M20 5C20.5523 5 21 5.44772 21 6V12C21 12.5523 20.5523 13 20 13C20.628 13.8355 21 14.8743 21 16C21 18.7614 18.7614 21 16 21C13.2386 21 11 18.7614 11 16C11 14.8743 11.372 13.8355 11.9998 12.9998L4 13C3.44772 13 3 12.5523 3 12V6C3 5.44772 3.44772 5 4 5H20ZM13 15V17H19V15H13ZM19 7H5V11H19V7Z\"></path></svg>"
|
||||
},
|
||||
{
|
||||
id: "merge-cells-horizontal",
|
||||
svg: "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" d=\"M0 0h24v24H0z\"></path><path d=\"M20 3C20.5523 3 21 3.44772 21 4V20C21 20.5523 20.5523 21 20 21H4C3.44772 21 3 20.5523 3 20V4C3 3.44772 3.44772 3 4 3H20ZM11 5H5V10.999H7V9L10 12L7 15V13H5V19H11V17H13V19H19V13H17V15L14 12L17 9V10.999H19V5H13V7H11V5ZM13 13V15H11V13H13ZM13 9V11H11V9H13Z\"></path></svg>"
|
||||
},
|
||||
{
|
||||
id: "merge-cells-vertical",
|
||||
svg: "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" d=\"M0 0h24v24H0z\"></path><path d=\"M21 20C21 20.5523 20.5523 21 20 21H4C3.44772 21 3 20.5523 3 20V4C3 3.44772 3.44772 3 4 3H20C20.5523 3 21 3.44772 21 4V20ZM19 11V5H13.001V7H15L12 10L9 7H11V5H5V11H7V13H5V19H11V17H9L12 14L15 17H13.001V19H19V13H17V11H19ZM11 13H9V11H11V13ZM15 13H13V11H15V13Z\"></path></svg>"
|
||||
},
|
||||
{
|
||||
id: "split-cells-horizontal",
|
||||
svg: "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" d=\"M0 0h24v24H0z\"></path><path d=\"M20 3C20.5523 3 21 3.44772 21 4V20C21 20.5523 20.5523 21 20 21H4C3.44772 21 3 20.5523 3 20V4C3 3.44772 3.44772 3 4 3H20ZM11 5H5V19H11V15H13V19H19V5H13V9H11V5ZM15 9L18 12L15 15V13H9V15L6 12L9 9V11H15V9Z\"></path></svg>"
|
||||
},
|
||||
{
|
||||
id: "split-cells-vertical",
|
||||
svg: "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" d=\"M0 0h24v24H0z\"></path><path d=\"M20 3C20.5523 3 21 3.44772 21 4V20C21 20.5523 20.5523 21 20 21H4C3.44772 21 3 20.5523 3 20V4C3 3.44772 3.44772 3 4 3H20ZM19 5H5V10.999L9 11V13H5V19H19V13H15V11L19 10.999V5ZM12 6L15 9H13V15H15L12 18L9 15H11V9H9L12 6Z\"></path></svg>"
|
||||
},
|
||||
{
|
||||
id: "delete",
|
||||
svg: "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path d=\"M7 4V2H17V4H22V6H20V21C20 21.5523 19.5523 22 19 22H5C4.44772 22 4 21.5523 4 21V6H2V4H7ZM6 6V20H18V6H6ZM9 9H11V17H9V9ZM13 9H15V17H13V9Z\"></path></svg>"
|
||||
}
|
||||
// {
|
||||
// id: "insert-row-bottom",
|
||||
// svg: ""
|
||||
// }
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
onItemClick(id: string): void {
|
||||
if (id === "insert-column-left") {
|
||||
this.editor?.chain().focus().addColumnBefore().run()
|
||||
} else if (id === "insert-column-right") {
|
||||
this.editor?.chain().focus().addColumnAfter().run()
|
||||
} else if (id === "insert-row-top") {
|
||||
this.editor?.chain().focus().addRowBefore().run()
|
||||
} else if (id === "insert-row-bottom") {
|
||||
this.editor?.chain().focus().addRowAfter().run()
|
||||
} else if (id === "delete-column") {
|
||||
this.editor?.chain().focus().deleteColumn().run()
|
||||
} else if (id === "delete-row") {
|
||||
this.editor?.chain().focus().deleteRow().run()
|
||||
} else if (id === "merge-cells-horizontal" || id === "merge-cells-vertical") {
|
||||
this.editor?.chain().focus().mergeCells().run()
|
||||
} else if (id === "split-cells-horizontal" || id === "split-cells-vertical") {
|
||||
this.editor?.chain().focus().splitCell().run()
|
||||
} else if (id === "delete") {
|
||||
this.editor?.chain().focus().deleteTable().run()
|
||||
}
|
||||
}
|
||||
|
||||
show(ids: string[]) {
|
||||
if (!ids || ids.length == 0) {
|
||||
this.style.display = "none"
|
||||
} else {
|
||||
this.style.display = ""
|
||||
}
|
||||
this.querySelectorAll(".aie-bubble-menu-item").forEach((el) => {
|
||||
(el as HTMLElement).style.display = "none";
|
||||
})
|
||||
|
||||
ids.forEach((id) => {
|
||||
const div = this.querySelector(`#${id}`) as HTMLElement;
|
||||
div.style.display = "";
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
onTransaction(props: EditorEvents["transaction"]) {
|
||||
if (!props.editor.isActive("table")) {
|
||||
return;
|
||||
}
|
||||
const {state: {selection}, view} = props.editor;
|
||||
console.log("selection: ", selection)
|
||||
if (selection instanceof CellSelection) {
|
||||
if (this.isOneCellSelected(selection)) {
|
||||
const showIds = ["insert-column-left", "insert-column-right", "delete-column", "insert-row-top", "insert-row-bottom", "delete-row"];
|
||||
if (props.editor.can().splitCell()){
|
||||
const nodeDOM = view.nodeDOM(selection.$anchorCell.pos) as HTMLTableRowElement;
|
||||
const colspan = nodeDOM.getAttribute("colspan");
|
||||
const rowspan = nodeDOM.getAttribute("rowspan");
|
||||
if (colspan && Number(colspan) > 1) {
|
||||
showIds.push("split-cells-horizontal")
|
||||
} else if (rowspan && Number(rowspan) > 1) {
|
||||
showIds.push("split-cells-vertical")
|
||||
}
|
||||
}
|
||||
this.show(showIds)
|
||||
} else if (this.isAllTableSelected(selection)) {
|
||||
this.show(["delete"])
|
||||
} else if (this.isColumnSelected(selection, view)) {
|
||||
this.show(["insert-column-left", "insert-column-right", "delete-column", "merge-cells-vertical"])
|
||||
} else if (this.isRowSelected(selection, view)) {
|
||||
this.show(["insert-row-top", "insert-row-bottom", "delete-row", "merge-cells-horizontal"])
|
||||
} else {
|
||||
this.show(["merge-cells-horizontal"])
|
||||
}
|
||||
} else {
|
||||
this.show(["insert-column-left", "insert-column-right", "delete-column", "insert-row-top", "insert-row-bottom", "delete-row"])
|
||||
}
|
||||
}
|
||||
|
||||
isAllTableSelected(selection: CellSelection): boolean {
|
||||
const map = TableMap.get(selection.$anchorCell.node(-1));
|
||||
const cells = map.cellsInRect({
|
||||
top:0,
|
||||
left:0,
|
||||
right:map.width,
|
||||
bottom:map.height
|
||||
});
|
||||
return selection.ranges.length ==cells.length;
|
||||
}
|
||||
|
||||
isOneCellSelected(selection: CellSelection): boolean {
|
||||
return selection.ranges.length == 1;
|
||||
}
|
||||
|
||||
isColumnSelected(selection: CellSelection, view: EditorView): boolean {
|
||||
let left: number = -1;
|
||||
for (let range of selection.ranges) {
|
||||
if (left == -1) {
|
||||
left = view.coordsAtPos(range.$from.pos).left;
|
||||
} else if (left != view.coordsAtPos(range.$from.pos).left) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
isRowSelected(selection: CellSelection, view: EditorView): boolean {
|
||||
let bottom: number = -1;
|
||||
for (let range of selection.ranges) {
|
||||
if (bottom == -1) {
|
||||
bottom = view.coordsAtPos(range.$from.pos).bottom;
|
||||
} else if (bottom != view.coordsAtPos(range.$from.pos).bottom) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
import {AbstractDropdownMenuButton} from "../AbstractDropdownMenuButton.ts";
|
||||
import {Editor, EditorEvents} from "@tiptap/core";
|
||||
import {AiEditorOptions, AiMenu} from "../../core/AiEditor.ts";
|
||||
import {AiModelFactory} from "../../ai/AiModelFactory.ts";
|
||||
|
||||
|
||||
const aiMenus: AiMenu[] = [
|
||||
{
|
||||
icon: `<div style="width:18px;height: 18px;"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"></path><path d="M4 18.9997H20V13.9997H22V19.9997C22 20.552 21.5523 20.9997 21 20.9997H3C2.44772 20.9997 2 20.552 2 19.9997V13.9997H4V18.9997ZM16.1716 6.9997L12.2218 3.04996L13.636 1.63574L20 7.9997L13.636 14.3637L12.2218 12.9495L16.1716 8.9997H5V6.9997H16.1716Z"></path></svg></div>`,
|
||||
name: "AI 续写",
|
||||
prompt: "请帮我继续扩展一些这段话的内容",
|
||||
text: "focusBefore",
|
||||
model: "xinghuo",
|
||||
},
|
||||
{
|
||||
icon: `<div style="width:18px;height: 18px;"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"></path><path d="M15 5.25C16.7949 5.25 18.25 3.79493 18.25 2H19.75C19.75 3.79493 21.2051 5.25 23 5.25V6.75C21.2051 6.75 19.75 8.20507 19.75 10H18.25C18.25 8.20507 16.7949 6.75 15 6.75V5.25ZM4 7C4 5.89543 4.89543 5 6 5H13V3H6C3.79086 3 2 4.79086 2 7V17C2 19.2091 3.79086 21 6 21H18C20.2091 21 22 19.2091 22 17V12H20V17C20 18.1046 19.1046 19 18 19H6C4.89543 19 4 18.1046 4 17V7Z"></path></svg></div>`,
|
||||
name: "AI 优化",
|
||||
prompt: "请帮我优化一下这段文字的内容,并返回结果",
|
||||
text: "selected",
|
||||
model: "xinghuo",
|
||||
},
|
||||
{
|
||||
icon: `<div style="width:18px;height: 18px;"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"></path><path d="M17.934 3.0359L19.666 4.0359L18.531 6H21V8H19V12H21V14H19V21H17V14L13.9157 14.0004C13.5914 16.8623 12.3522 19.3936 10.5466 21.1933L8.98361 19.9233C10.5031 18.4847 11.5801 16.4008 11.9008 14.0009L10 14V12L12 11.999V8H10V6H12.467L11.334 4.0359L13.066 3.0359L14.777 6H16.221L17.934 3.0359ZM5 13.803L3 14.339V12.268L5 11.732V8H3V6H5V3H7V6H9V8H7V11.197L9 10.661V12.731L7 13.267V18.5C7 19.8807 5.88071 21 4.5 21H3V19H4.5C4.74546 19 4.94961 18.8231 4.99194 18.5899L5 18.5V13.803ZM17 8H14V12H17V8Z"></path></svg></div>`,
|
||||
name: "AI 校对",
|
||||
prompt: "请帮我找出这段话的错别字,把错别字修改后,并返回结果,不要解释或其他多余的内容",
|
||||
text: "selected",
|
||||
model: "xinghuo",
|
||||
},
|
||||
{
|
||||
icon: `<div style="width:18px;height: 18px;"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"></path><path d="M5 15V17C5 18.0544 5.81588 18.9182 6.85074 18.9945L7 19H10V21H7C4.79086 21 3 19.2091 3 17V15H5ZM18 10L22.4 21H20.245L19.044 18H14.954L13.755 21H11.601L16 10H18ZM17 12.8852L15.753 16H18.245L17 12.8852ZM8 2V4H12V11H8V14H6V11H2V4H6V2H8ZM17 3C19.2091 3 21 4.79086 21 7V9H19V7C19 5.89543 18.1046 5 17 5H14V3H17ZM6 6H4V9H6V6ZM10 6H8V9H10V6Z"></path></svg></div>`,
|
||||
name: "AI 翻译",
|
||||
prompt: "请帮我把这段内容翻译为英语,直接返回英语结果",
|
||||
text: "selected",
|
||||
model: "xinghuo",
|
||||
},
|
||||
]
|
||||
|
||||
export class Ai extends AbstractDropdownMenuButton<AiMenu> {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.dropDivHeight = "120px"
|
||||
this.dropDivWith = "94px"
|
||||
this.width = "36px"
|
||||
this.menuTextWidth = "20px"
|
||||
}
|
||||
|
||||
onCreate(_: EditorEvents["create"], options: AiEditorOptions) {
|
||||
super.onCreate(_, options);
|
||||
this.menuData = options.ai?.menus || aiMenus;
|
||||
}
|
||||
|
||||
|
||||
renderTemplate() {
|
||||
this.template = `
|
||||
<div style="width: ${this.width};">
|
||||
<div id="tippy" class="menu-ai" id="text">
|
||||
<span> AI </span>
|
||||
<div style="width: 18px;height: 18px;display: inline-block" >
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"></path><path d="M12 14L8 10H16L12 14Z"></path></svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
|
||||
createMenuElement() {
|
||||
const div = document.createElement("div");
|
||||
div.style.height = this.dropDivHeight;
|
||||
div.style.width = this.dropDivWith;
|
||||
div.classList.add("aie-dropdown-container");
|
||||
|
||||
for (let i = 0; i < this.menuData.length; i++) {
|
||||
const item = document.createElement("div");
|
||||
item.classList.add("aie-dropdown-item");
|
||||
item.innerHTML = `
|
||||
<div class="text" style="display: flex;padding-left: 5px">${this.onDropdownItemRender(i)}</div>
|
||||
`
|
||||
item.addEventListener("click", () => {
|
||||
this.onDropdownItemClick(i);
|
||||
this.tippyInstance!.hide()
|
||||
});
|
||||
div.appendChild(item)
|
||||
}
|
||||
this.tippyEl = div;
|
||||
return div;
|
||||
}
|
||||
|
||||
onTransaction(_: EditorEvents["transaction"]) {
|
||||
//do nothing
|
||||
}
|
||||
|
||||
onDropdownActive(_editor: Editor, _index: number): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
getSelectedText(text: "selected" | "focusBefore") {
|
||||
if (text === "selected") {
|
||||
const {selection, doc} = this.editor!.state
|
||||
return doc.textBetween(selection.from, selection.to);
|
||||
} else {
|
||||
return this.editor!.state.selection.$head.parent.textContent;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
onDropdownItemClick(index: number): void {
|
||||
const aiMenu = this.menuData[index];
|
||||
const selectedText = this.getSelectedText(aiMenu.text);
|
||||
|
||||
if (selectedText) {
|
||||
const aiModel = AiModelFactory.create(aiMenu.model, this.options!);
|
||||
if (aiModel) {
|
||||
aiModel?.start(selectedText, aiMenu.prompt, this.editor!);
|
||||
} else {
|
||||
console.error("Ai model config error.")
|
||||
}
|
||||
} else {
|
||||
console.error("Can not get selected text.")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
onDropdownItemRender(index: number): Element | string {
|
||||
return this.menuData[index].icon + `<div style="margin-left: 10px">${this.menuData[index].name}</div>`;
|
||||
}
|
||||
|
||||
onMenuTextRender(index: number): Element | string {
|
||||
return this.menuData[index].icon;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
import {AbstractDropdownMenuButton} from "../AbstractDropdownMenuButton.ts";
|
||||
import {Editor} from "@tiptap/core";
|
||||
|
||||
type AlignInfo = {
|
||||
icon:string,
|
||||
title:string,
|
||||
value:string,
|
||||
}
|
||||
|
||||
const alignInfos:AlignInfo[] = [
|
||||
{
|
||||
icon:`<div style="width:18px;height: 18px;"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M3 4H21V6H3V4ZM3 19H17V21H3V19ZM3 14H21V16H3V14ZM3 9H17V11H3V9Z"></path></svg></div>`,
|
||||
title:"居左对齐",
|
||||
value:"left"
|
||||
},
|
||||
{
|
||||
icon:`<div style="width:18px;height: 18px;"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M3 4H21V6H3V4ZM5 19H19V21H5V19ZM3 14H21V16H3V14ZM5 9H19V11H5V9Z"></path></svg></div>`,
|
||||
title:"居中对齐",
|
||||
value:"center"
|
||||
},
|
||||
{
|
||||
icon:`<div style="width:18px;height: 18px;"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M3 4H21V6H3V4ZM7 19H21V21H7V19ZM3 14H21V16H3V14ZM7 9H21V11H7V9Z"></path></svg></div>`,
|
||||
title:"居右对齐",
|
||||
value:"right"
|
||||
},
|
||||
{
|
||||
icon:`<div style="width:18px;height: 18px;"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M3 4H21V6H3V4ZM3 19H21V21H3V19ZM3 14H21V16H3V14ZM3 9H21V11H3V9Z"></path></svg></div>`,
|
||||
title:"两端对齐",
|
||||
value:"justify"
|
||||
},
|
||||
]
|
||||
export class Align extends AbstractDropdownMenuButton<AlignInfo> {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.menuData = alignInfos;
|
||||
this.dropDivHeight = "112px"
|
||||
this.dropDivWith = "60px"
|
||||
this.width="36px"
|
||||
this.menuTextWidth="20px"
|
||||
}
|
||||
|
||||
onDropdownActive(editor: Editor, index: number): boolean {
|
||||
return editor.isActive( {textAlign: this.menuData[index].value });
|
||||
}
|
||||
|
||||
onDropdownItemClick(index: number): void {
|
||||
this.editor!.chain().focus()
|
||||
.setTextAlign(this.menuData[index].value)
|
||||
.run()
|
||||
}
|
||||
|
||||
onDropdownItemRender(index: number): Element | string {
|
||||
return this.menuData[index].icon;
|
||||
}
|
||||
|
||||
onMenuTextRender(index: number): Element | string {
|
||||
return this.menuData[index].icon;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
import {AbstractMenuButton} from "../AbstractMenuButton.ts";
|
||||
|
||||
export class Attachment extends AbstractMenuButton {
|
||||
|
||||
fileInput?: HTMLInputElement;
|
||||
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const template = `
|
||||
<div>
|
||||
<input type="file" style="display: none">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M14.8287 7.7574L9.1718 13.4143C8.78127 13.8048 8.78127 14.4379 9.1718 14.8285C9.56232 15.219 10.1955 15.219 10.586 14.8285L16.2429 9.17161C17.4144 8.00004 17.4144 6.10055 16.2429 4.92897C15.0713 3.7574 13.1718 3.7574 12.0002 4.92897L6.34337 10.5858C4.39075 12.5384 4.39075 15.7043 6.34337 17.6569C8.29599 19.6095 11.4618 19.6095 13.4144 17.6569L19.0713 12L20.4855 13.4143L14.8287 19.0711C12.095 21.8048 7.66283 21.8048 4.92916 19.0711C2.19549 16.3374 2.19549 11.9053 4.92916 9.17161L10.586 3.51476C12.5386 1.56214 15.7045 1.56214 17.6571 3.51476C19.6097 5.46738 19.6097 8.63321 17.6571 10.5858L12.0002 16.2427C10.8287 17.4143 8.92916 17.4143 7.75759 16.2427C6.58601 15.0711 6.58601 13.1716 7.75759 12L13.4144 6.34319L14.8287 7.7574Z"></path></svg>
|
||||
</div>
|
||||
`;
|
||||
this.template = template;
|
||||
this.registerClickListener();
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (this.options?.attachment?.customMenuInvoke) {
|
||||
this.querySelector("input")!.remove();
|
||||
} else {
|
||||
this.fileInput = this.querySelector("input") as HTMLInputElement;
|
||||
this.fileInput!.addEventListener("change", () => {
|
||||
const files = this.fileInput?.files;
|
||||
if (files && files.length > 0) {
|
||||
for (let file of files) {
|
||||
this.editor?.commands.uploadAttachment(file);
|
||||
}
|
||||
}
|
||||
(this.fileInput as any).value = "";
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// @ts-ignore
|
||||
onClick(commands) {
|
||||
if (this.options?.attachment?.customMenuInvoke) {
|
||||
this.options.attachment.customMenuInvoke(this.editor!);
|
||||
} else {
|
||||
this.fileInput?.click();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
import {AbstractMenuButton} from "../AbstractMenuButton.ts";
|
||||
import {Editor} from "@tiptap/core";
|
||||
|
||||
export class Bold extends AbstractMenuButton {
|
||||
constructor() {
|
||||
super();
|
||||
const template = `
|
||||
<div>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M8 11H12.5C13.8807 11 15 9.88071 15 8.5C15 7.11929 13.8807 6 12.5 6H8V11ZM18 15.5C18 17.9853 15.9853 20 13.5 20H6V4H12.5C14.9853 4 17 6.01472 17 8.5C17 9.70431 16.5269 10.7981 15.7564 11.6058C17.0979 12.3847 18 13.837 18 15.5ZM8 13V18H13.5C14.8807 18 16 16.8807 16 15.5C16 14.1193 14.8807 13 13.5 13H8Z"></path></svg>
|
||||
</div>
|
||||
`;
|
||||
this.template = template;
|
||||
this.registerClickListener();
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
onClick(commands) {
|
||||
commands.toggleBold();
|
||||
}
|
||||
|
||||
onActive(editor: Editor): boolean {
|
||||
return editor.isActive("bold")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import {AbstractMenuButton} from "../AbstractMenuButton.ts";
|
||||
|
||||
export class Break extends AbstractMenuButton {
|
||||
constructor() {
|
||||
super();
|
||||
const template = `
|
||||
<div>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M15 18H16.5C17.8807 18 19 16.8807 19 15.5C19 14.1193 17.8807 13 16.5 13H3V11H16.5C18.9853 11 21 13.0147 21 15.5C21 17.9853 18.9853 20 16.5 20H15V22L11 19L15 16V18ZM3 4H21V6H3V4ZM9 18V20H3V18H9Z"></path></svg>
|
||||
</div>
|
||||
`;
|
||||
this.template = template;
|
||||
this.registerClickListener();
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
onClick(commands) {
|
||||
commands.setHardBreak();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
import {AbstractMenuButton} from "../AbstractMenuButton.ts";
|
||||
import {Editor} from "@tiptap/core";
|
||||
|
||||
export class BulletList extends AbstractMenuButton {
|
||||
constructor() {
|
||||
super();
|
||||
const template = `
|
||||
<div>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M8 4H21V6H8V4ZM3 3.5H6V6.5H3V3.5ZM3 10.5H6V13.5H3V10.5ZM3 17.5H6V20.5H3V17.5ZM8 11H21V13H8V11ZM8 18H21V20H8V18Z"></path></svg>
|
||||
</div>
|
||||
`;
|
||||
this.template = template;
|
||||
this.registerClickListener();
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
onClick(commands) {
|
||||
commands.toggleBulletList();
|
||||
}
|
||||
|
||||
onActive(editor: Editor): boolean {
|
||||
return editor.isActive("bulletList")
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
import {AbstractMenuButton} from "../AbstractMenuButton.ts";
|
||||
import {Editor} from "@tiptap/core";
|
||||
|
||||
export class Code extends AbstractMenuButton {
|
||||
constructor() {
|
||||
super();
|
||||
const template = `
|
||||
<div>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M23 11.9998L15.9289 19.0708L14.5147 17.6566L20.1716 11.9998L14.5147 6.34292L15.9289 4.92871L23 11.9998ZM3.82843 11.9998L9.48528 17.6566L8.07107 19.0708L1 11.9998L8.07107 4.92871L9.48528 6.34292L3.82843 11.9998Z"></path></svg>
|
||||
</div>
|
||||
`;
|
||||
this.template = template;
|
||||
this.registerClickListener();
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
onClick(commands) {
|
||||
commands.toggleCode();
|
||||
}
|
||||
|
||||
onActive(editor: Editor): boolean {
|
||||
return editor.isActive("code")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
import {AbstractMenuButton} from "../AbstractMenuButton.ts";
|
||||
import {Editor} from "@tiptap/core";
|
||||
|
||||
export class CodeBlock extends AbstractMenuButton {
|
||||
constructor() {
|
||||
super();
|
||||
const template = `
|
||||
<div>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M3 3H21C21.5523 3 22 3.44772 22 4V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V4C2 3.44772 2.44772 3 3 3ZM4 5V19H20V5H4ZM20 12L16.4645 15.5355L15.0503 14.1213L17.1716 12L15.0503 9.87868L16.4645 8.46447L20 12ZM6.82843 12L8.94975 14.1213L7.53553 15.5355L4 12L7.53553 8.46447L8.94975 9.87868L6.82843 12ZM11.2443 17H9.11597L12.7557 7H14.884L11.2443 17Z"></path></svg>
|
||||
</div>
|
||||
`;
|
||||
this.template = template;
|
||||
this.registerClickListener();
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
onClick(commands) {
|
||||
commands.toggleCodeBlock();
|
||||
}
|
||||
|
||||
onActive(editor: Editor): boolean {
|
||||
return editor.isActive("codeBlock")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
import {AbstractMenuButton} from "../AbstractMenuButton.ts";
|
||||
|
||||
export class Divider extends AbstractMenuButton {
|
||||
constructor() {
|
||||
super();
|
||||
const template = `
|
||||
<div no-hover style="width: 1px;height: 20px; display: flex">
|
||||
<div class="aie-menu-divider" />
|
||||
</div>
|
||||
`;
|
||||
this.template = template;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
import {AbstractMenuButton} from "../AbstractMenuButton.ts";
|
||||
import tippy from "tippy.js";
|
||||
|
||||
|
||||
const emojis = ['😀','😃','😄','😁','😆','😅','🤣','😂','🙂','🙃','😉','😊','😇','🥰','😍','🤩','😘','😗','😚','😙','🥲','😋','😛','😜','🤪','😝'
|
||||
,'🤑','🤗','🤭','🤫','🤔','🤐','🤨','😐','😑','😶','😶🌫','😏','😒','🙄','😬','😮💨','🤥','😌','😔','😪','🤤','😴','😷'
|
||||
,'🤒','🤕','🤢','🤮','🤧','🥵','🥶','🥴','😵','😵💫','🤯','🤠','🥳','🥸','😎','🤓','🧐','😕','😟','🙁','😮','😲','😳'
|
||||
,'🥺','😦','😧','😨','😰','😥','😢','😭','😱','😖','😣','😞','😓','😩','😫','🥱','😤','😡','😠','🤬','😈','👿','💀','☠️','💩','🤡','👹','👺'
|
||||
,'👻','👽','👾','🤖','😺','😸','😹','😻','😼','😽','🙀','😿','😾','🙈','🙉','🙊','💌','💘','💝','💖','💗','💓','💞','💕','💟','❣️','💔'
|
||||
,'💋','💯','💢','💥','💫','💦','💨','💤'];
|
||||
export class Emoji extends AbstractMenuButton {
|
||||
constructor() {
|
||||
super();
|
||||
const template = `
|
||||
<div id="xxx">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22ZM12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20ZM8 13H16C16 15.2091 14.2091 17 12 17C9.79086 17 8 15.2091 8 13ZM8 11C7.17157 11 6.5 10.3284 6.5 9.5C6.5 8.67157 7.17157 8 8 8C8.82843 8 9.5 8.67157 9.5 9.5C9.5 10.3284 8.82843 11 8 11ZM16 11C15.1716 11 14.5 10.3284 14.5 9.5C14.5 8.67157 15.1716 8 16 8C16.8284 8 17.5 8.67157 17.5 9.5C17.5 10.3284 16.8284 11 16 11Z"></path></svg>
|
||||
</div>
|
||||
`;
|
||||
this.template = template;
|
||||
}
|
||||
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
tippy(this.querySelector("#xxx")!, {
|
||||
content: this.createMenuElement(),
|
||||
appendTo: this.closest(".aie-container")!,
|
||||
placement: 'bottom',
|
||||
trigger: 'click',
|
||||
interactive: true,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
createMenuElement() {
|
||||
const div = document.createElement("div");
|
||||
div.style.height = "220px"
|
||||
div.style.width = "400px"
|
||||
div.classList.add("aie-dropdown-container")
|
||||
div.innerHTML = `
|
||||
<div style="margin: 5px">
|
||||
<div class="emoji-cells">
|
||||
${emojis.map((emoji) => {
|
||||
return `<div class="emoji-cell">${emoji}</div>`;
|
||||
}).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
div.querySelector(".emoji-cells")!.addEventListener("click",(e)=>{
|
||||
const target:HTMLDivElement = (e.target as any).closest('.emoji-cell');
|
||||
if (target){
|
||||
this.editor?.commands.insertContent(target.innerHTML)
|
||||
}
|
||||
});
|
||||
|
||||
return div;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import {AbstractMenuButton} from "../AbstractMenuButton.ts";
|
||||
|
||||
export class Eraser extends AbstractMenuButton {
|
||||
constructor() {
|
||||
super();
|
||||
const template = `
|
||||
<div style="height: 16px">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13.9999 18.9967H20.9999V20.9967H11.9999L8.00229 20.9992L1.51457 14.5115C1.12405 14.1209 1.12405 13.4878 1.51457 13.0972L12.1212 2.49065C12.5117 2.10012 13.1449 2.10012 13.5354 2.49065L21.3136 10.2688C21.7041 10.6593 21.7041 11.2925 21.3136 11.683L13.9999 18.9967ZM15.6567 14.5115L19.1922 10.9759L12.8283 4.61197L9.29275 8.1475L15.6567 14.5115Z"></path></svg>
|
||||
</div>
|
||||
`;
|
||||
this.template = template;
|
||||
this.registerClickListener();
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
onClick(commands) {
|
||||
commands.unsetAllMarks();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import {AbstractColorsMenuButton} from "../AbstractColorsMenuButton.ts";
|
||||
import {Editor} from "@tiptap/core";
|
||||
|
||||
export class FontColor extends AbstractColorsMenuButton {
|
||||
constructor() {
|
||||
super();
|
||||
this.iconSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M5.55446 22H3.40039L11.0004 3H13.0004L20.6004 22H18.4463L16.0463 16H7.95446L5.55446 22ZM8.75446 14H15.2463L12.0004 5.88517L8.75446 14Z"></path></svg>`;
|
||||
this.onDefaultColorClick = () => {
|
||||
this.editor?.chain().focus().unsetColor().run()
|
||||
}
|
||||
this.onColorItemClick = (color) => {
|
||||
this.editor?.chain().focus().setColor(color).run()
|
||||
}
|
||||
}
|
||||
|
||||
onActive(editor: Editor): boolean {
|
||||
return editor.isActive("textStyle");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
import {AbstractDropdownMenuButton} from "../AbstractDropdownMenuButton.ts";
|
||||
import {Editor, EditorEvents} from "@tiptap/core";
|
||||
import {AiEditorOptions, NameAndValue} from "../../core/AiEditor.ts";
|
||||
|
||||
|
||||
const fontFamilies: NameAndValue[] = [
|
||||
{name: "默认字体", value: ""},
|
||||
{name: "宋体", value: "SimSun"},
|
||||
{name: "仿宋", value: "FangSong"},
|
||||
{name: "黑体", value: "SimHei"},
|
||||
{name: "楷体", value: "KaiTi"},
|
||||
{name: "微软雅黑", value: "Microsoft YaHei"},
|
||||
{name: "方正仿宋简体_GBK", value: "FangSong_GB2312"},
|
||||
{name: "Arial", value: "Arial"},
|
||||
]
|
||||
|
||||
export class FontFamily extends AbstractDropdownMenuButton<NameAndValue> {
|
||||
constructor() {
|
||||
super();
|
||||
this.width = "80px"
|
||||
this.menuTextWidth = "60px"
|
||||
this.dropDivWith = "150px"
|
||||
}
|
||||
|
||||
onCreate(_: EditorEvents["create"], options: AiEditorOptions) {
|
||||
super.onCreate(_, options);
|
||||
this.menuData = options.fontFamily?.values || fontFamilies;
|
||||
}
|
||||
|
||||
onDropdownActive(editor: Editor, index: number): boolean {
|
||||
return editor.isActive('textStyle', {fontFamily: this.menuData[index].value});
|
||||
}
|
||||
|
||||
onDropdownItemClick(index: number): void {
|
||||
this.editor!.chain()
|
||||
.setFontFamily(this.menuData[index].value)
|
||||
.run()
|
||||
}
|
||||
|
||||
onDropdownItemRender(index: number): Element | string {
|
||||
return this.menuData[index].name;
|
||||
}
|
||||
|
||||
onMenuTextRender(index: number): Element | string {
|
||||
return this.menuData[index].name;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
import {Editor, EditorEvents} from "@tiptap/core";
|
||||
import {AbstractDropdownMenuButton} from "../AbstractDropdownMenuButton.ts";
|
||||
import {AiEditorOptions, NameAndValue} from "../../core/AiEditor.ts";
|
||||
|
||||
|
||||
const fontSizes: NameAndValue[] = [
|
||||
{name: "初号", value: 56},
|
||||
{name: "小初", value: 48},
|
||||
{name: "一号", value: 34.7},
|
||||
{name: "小一", value: 32},
|
||||
{name: "二号", value: 29.3},
|
||||
{name: "小二", value: 24},
|
||||
{name: "三号", value: 21.3},
|
||||
{name: "小三", value: 20},
|
||||
{name: "四号", value: 18.7},
|
||||
{name: "小四", value: 16},
|
||||
{name: "五号", value: 14},
|
||||
{name: "小五", value: 12},
|
||||
{name: "9", value: 9},
|
||||
{name: "10", value: 10},
|
||||
{name: "11", value: 11},
|
||||
{name: "12", value: 12},
|
||||
{name: "14", value: 14},
|
||||
{name: "18", value: 18},
|
||||
{name: "20", value: 20},
|
||||
{name: "22", value: 22},
|
||||
{name: "24", value: 24},
|
||||
{name: "26", value: 26},
|
||||
{name: "28", value: 28},
|
||||
{name: "30", value: 30},
|
||||
{name: "36", value: 36},
|
||||
{name: "42", value: 42},
|
||||
{name: "48", value: 48},
|
||||
{name: "56", value: 56},
|
||||
{name: "72", value: 72},
|
||||
]
|
||||
|
||||
export class FontSize extends AbstractDropdownMenuButton<NameAndValue> {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
onCreate(_: EditorEvents["create"], options: AiEditorOptions) {
|
||||
super.onCreate(_, options);
|
||||
this.menuData = options.fontSize?.values || fontSizes;
|
||||
}
|
||||
|
||||
onDropdownActive(editor: Editor, index: number): boolean {
|
||||
return editor.isActive('textStyle', {fontSize: `${this.menuData[index].value}px`});
|
||||
}
|
||||
|
||||
onDropdownItemClick(index: number): void {
|
||||
this.editor?.chain().focus()
|
||||
.setFontSize(`${this.menuData[index].value}px`)
|
||||
.run()
|
||||
}
|
||||
|
||||
onDropdownItemRender(index: number): Element | string {
|
||||
return this.menuData[index].name;
|
||||
}
|
||||
|
||||
onMenuTextRender(index: number): Element | string {
|
||||
return this.menuData[index].name;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
import {AbstractMenuButton} from "../AbstractMenuButton.ts";
|
||||
|
||||
export class Fullscreen extends AbstractMenuButton {
|
||||
fullscreenSvg = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path d=\"M8 3V5H4V9H2V3H8ZM2 21V15H4V19H8V21H2ZM22 21H16V19H20V15H22V21ZM22 9H20V5H16V3H22V9Z\"></path></svg>";
|
||||
fullscreenExitSvg = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path d=\"M18 7H22V9H16V3H18V7ZM8 9H2V7H6V3H8V9ZM18 17V21H16V15H22V17H18ZM8 15V21H6V17H2V15H8Z\"></path></svg>";
|
||||
isFullscreen: boolean = false;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const template = `
|
||||
<div>
|
||||
${this.fullscreenSvg}
|
||||
</div>
|
||||
`;
|
||||
this.template = template;
|
||||
this.registerClickListener();
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
onClick(commands) {
|
||||
const container = this.closest(".aie-container") as HTMLDivElement;
|
||||
if (!this.isFullscreen) {
|
||||
container.style.height = "calc(100vh - 2px)"
|
||||
container.style.width = "calc(100% - 2px)"
|
||||
container.style.background = "#ffffff"
|
||||
container.style.position = "absolute"
|
||||
container.style.top = "0"
|
||||
container.style.left = "0"
|
||||
container.style.zIndex = "9999"
|
||||
} else {
|
||||
container.style.height = "100%"
|
||||
container.style.width = ""
|
||||
container.style.background = ""
|
||||
container.style.position = ""
|
||||
container.style.top = ""
|
||||
container.style.left = ""
|
||||
container.style.zIndex = ""
|
||||
}
|
||||
this.isFullscreen = !this.isFullscreen
|
||||
this.querySelector("div")!.innerHTML = this.isFullscreen ? this.fullscreenExitSvg : this.fullscreenSvg;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
import {AbstractColorsMenuButton} from "../AbstractColorsMenuButton.ts";
|
||||
import {Editor} from "@tiptap/core";
|
||||
|
||||
|
||||
export class Highlight extends AbstractColorsMenuButton {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.iconSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M15.2427 4.51138L8.50547 11.2486L7.79836 13.3699L6.7574 14.4109L9.58583 17.2393L10.6268 16.1983L12.7481 15.4912L19.4853 8.75402L15.2427 4.51138ZM21.6066 8.04692C21.9972 8.43744 21.9972 9.0706 21.6066 9.46113L13.8285 17.2393L11.7071 17.9464L10.2929 19.3606C9.90241 19.7511 9.26925 19.7511 8.87872 19.3606L4.63608 15.118C4.24556 14.7275 4.24556 14.0943 4.63608 13.7038L6.0503 12.2896L6.7574 10.1682L14.5356 2.39006C14.9261 1.99954 15.5593 1.99954 15.9498 2.39006L21.6066 8.04692ZM15.2427 7.33981L16.6569 8.75402L11.7071 13.7038L10.2929 12.2896L15.2427 7.33981ZM4.28253 16.8858L7.11096 19.7142L5.69674 21.1284L1.4541 19.7142L4.28253 16.8858Z"></path></svg>`
|
||||
|
||||
this.onDefaultColorClick = ()=>{
|
||||
this.editor?.chain().focus().unsetHighlight().run()
|
||||
}
|
||||
this.onColorItemClick = (color) => {
|
||||
this.editor?.chain().focus().setHighlight({color}).run()
|
||||
}
|
||||
}
|
||||
|
||||
onActive(editor: Editor): boolean {
|
||||
return editor.isActive("highlight");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import {AbstractMenuButton} from "../AbstractMenuButton.ts";
|
||||
|
||||
export class Hr extends AbstractMenuButton {
|
||||
constructor() {
|
||||
super();
|
||||
const template = `
|
||||
<div>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M2 11H4V13H2V11ZM6 11H18V13H6V11ZM20 11H22V13H20V11Z"></path></svg>
|
||||
</div>
|
||||
`;
|
||||
this.template = template;
|
||||
this.registerClickListener();
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
onClick(commands) {
|
||||
commands.setHorizontalRule();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
import {AbstractMenuButton} from "../AbstractMenuButton.ts";
|
||||
|
||||
export class Image extends AbstractMenuButton {
|
||||
|
||||
fileInput?: HTMLInputElement;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const template = `
|
||||
<div>
|
||||
<input type="file" accept="image/*" style="display: none">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M2.9918 21C2.44405 21 2 20.5551 2 20.0066V3.9934C2 3.44476 2.45531 3 2.9918 3H21.0082C21.556 3 22 3.44495 22 3.9934V20.0066C22 20.5552 21.5447 21 21.0082 21H2.9918ZM20 15V5H4V19L14 9L20 15ZM20 17.8284L14 11.8284L6.82843 19H20V17.8284ZM8 11C6.89543 11 6 10.1046 6 9C6 7.89543 6.89543 7 8 7C9.10457 7 10 7.89543 10 9C10 10.1046 9.10457 11 8 11Z"></path></svg>
|
||||
</div>
|
||||
`;
|
||||
this.template = template;
|
||||
this.registerClickListener();
|
||||
}
|
||||
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (this.options?.image?.customMenuInvoke) {
|
||||
this.querySelector("input")!.remove();
|
||||
} else {
|
||||
this.fileInput = this.querySelector("input") as HTMLInputElement;
|
||||
this.fileInput!.addEventListener("change", () => {
|
||||
const files = this.fileInput?.files;
|
||||
if (files && files.length > 0) {
|
||||
for (let file of files) {
|
||||
this.editor?.commands.uploadImage(file);
|
||||
}
|
||||
}
|
||||
(this.fileInput as any).value = "";
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// @ts-ignore
|
||||
onClick(commands) {
|
||||
if (this.options?.image?.customMenuInvoke) {
|
||||
this.options.image.customMenuInvoke(this.editor!);
|
||||
} else {
|
||||
this.fileInput?.click();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import {AbstractMenuButton} from "../AbstractMenuButton.ts";
|
||||
|
||||
export class IndentDecrease extends AbstractMenuButton {
|
||||
constructor() {
|
||||
super();
|
||||
const template = `
|
||||
<div>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M3 4H21V6H3V4ZM3 19H21V21H3V19ZM11 14H21V16H11V14ZM11 9H21V11H11V9ZM3 12.5L7 9V16L3 12.5Z"></path></svg>
|
||||
</div>
|
||||
`;
|
||||
this.template = template;
|
||||
this.registerClickListener();
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
onClick(commands) {
|
||||
commands.outdent();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import {AbstractMenuButton} from "../AbstractMenuButton.ts";
|
||||
|
||||
export class IndentIncrease extends AbstractMenuButton {
|
||||
constructor() {
|
||||
super();
|
||||
const template = `
|
||||
<div>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M3 4H21V6H3V4ZM3 19H21V21H3V19ZM11 14H21V16H11V14ZM11 9H21V11H11V9ZM7 12.5L3 16V9L7 12.5Z"></path></svg>
|
||||
</div>
|
||||
`;
|
||||
this.template = template;
|
||||
this.registerClickListener();
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
onClick(commands) {
|
||||
commands.indent();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
import {AbstractMenuButton} from "../AbstractMenuButton.ts";
|
||||
import {Editor} from "@tiptap/core";
|
||||
|
||||
export class Italic extends AbstractMenuButton {
|
||||
constructor() {
|
||||
super();
|
||||
const template = `
|
||||
<div>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M15 20H7V18H9.92661L12.0425 6H9V4H17V6H14.0734L11.9575 18H15V20Z"></path></svg>
|
||||
</div>
|
||||
`;
|
||||
this.template = template;
|
||||
this.registerClickListener();
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
onClick(commands) {
|
||||
commands.toggleItalic();
|
||||
}
|
||||
|
||||
onActive(editor: Editor): boolean {
|
||||
return editor.isActive("italic")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
import {Editor} from "@tiptap/core";
|
||||
import {AbstractDropdownMenuButton} from "../AbstractDropdownMenuButton.ts";
|
||||
|
||||
|
||||
const titles = ["1.0", "1.25", "1.5", "2.0", "2.5", "3.0"];
|
||||
|
||||
export class LineHeight extends AbstractDropdownMenuButton<string> {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.menuData = titles;
|
||||
this.refreshMenuText = false;
|
||||
this.dropDivHeight = "180px"
|
||||
this.dropDivWith = "60px"
|
||||
this.width="36px"
|
||||
this.menuTextWidth="20px"
|
||||
}
|
||||
|
||||
onDropdownActive(editor: Editor, index: number): boolean {
|
||||
if (index == 0) {
|
||||
return editor.isActive("paragraph")
|
||||
}
|
||||
return editor.isActive("heading", {level: index});
|
||||
}
|
||||
|
||||
onDropdownItemClick(index: number): void {
|
||||
const lineHeight = `${(Number(this.menuData[index]) * 100).toFixed(0)}%`;
|
||||
this.editor!.chain().setLineHeight(lineHeight).run();
|
||||
}
|
||||
|
||||
onDropdownItemRender(index: number): Element | string {
|
||||
return this.menuData[index];
|
||||
}
|
||||
|
||||
onMenuTextRender(_: number): Element | string {
|
||||
return `
|
||||
<div style="width:18px;height: 18px;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M11 4H21V6H11V4ZM6 7V11H4V7H1L5 3L9 7H6ZM6 17H9L5 21L1 17H4V13H6V17ZM11 18H21V20H11V18ZM9 11H21V13H9V11Z"></path></svg>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
import {AbstractMenuButton} from "../AbstractMenuButton.ts";
|
||||
import {Popover} from "../../commons/Popover.ts";
|
||||
|
||||
export class Link extends AbstractMenuButton {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const template = `
|
||||
<div>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M17.6572 14.8282L16.2429 13.414L17.6572 11.9998C19.2193 10.4377 19.2193 7.90506 17.6572 6.34296C16.0951 4.78086 13.5624 4.78086 12.0003 6.34296L10.5861 7.75717L9.17188 6.34296L10.5861 4.92875C12.9292 2.5856 16.7282 2.5856 19.0714 4.92875C21.4145 7.27189 21.4145 11.0709 19.0714 13.414L17.6572 14.8282ZM14.8287 17.6567L13.4145 19.0709C11.0714 21.414 7.27238 21.414 4.92923 19.0709C2.58609 16.7277 2.58609 12.9287 4.92923 10.5856L6.34345 9.17139L7.75766 10.5856L6.34345 11.9998C4.78135 13.5619 4.78135 16.0946 6.34345 17.6567C7.90555 19.2188 10.4382 19.2188 12.0003 17.6567L13.4145 16.2425L14.8287 17.6567ZM14.8287 7.75717L16.2429 9.17139L9.17188 16.2425L7.75766 14.8282L14.8287 7.75717Z"></path></svg>
|
||||
</div>
|
||||
`;
|
||||
this.template = template;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
const popover = new Popover();
|
||||
popover.setContent(`
|
||||
<div style="width: 250px">
|
||||
链接地址
|
||||
</div>
|
||||
<div style="width: 250px">
|
||||
<input type="text" id="href" style="width: 240px">
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 10px">
|
||||
打开方式
|
||||
</div>
|
||||
<div>
|
||||
<select id="target" style="width: 250px">
|
||||
<option value="">默认</option>
|
||||
<option value="_blank">新窗口</option>
|
||||
</select>
|
||||
</div>
|
||||
`);
|
||||
|
||||
|
||||
popover.onConfirmClick((instance) => {
|
||||
const href = (instance.popper.querySelector("#href") as HTMLInputElement).value;
|
||||
if (href.trim() === "") {
|
||||
this.editor?.chain().focus().extendMarkRange('link')
|
||||
.unsetLink()
|
||||
.run()
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
let target: string | null = (instance.popper.querySelector("#target") as HTMLInputElement).value;
|
||||
if (target.trim() === "") {
|
||||
target = null;
|
||||
}
|
||||
|
||||
this.editor?.chain().focus().extendMarkRange("link")
|
||||
.setLink({
|
||||
href,
|
||||
target,
|
||||
rel: null,
|
||||
}).run()
|
||||
});
|
||||
|
||||
popover.onShow((instance) => {
|
||||
const attrs = this.editor?.getAttributes("link");
|
||||
console.log("onShow: ",attrs && attrs.href)
|
||||
if (attrs && attrs.href) {
|
||||
(instance.popper.querySelector("#href") as HTMLInputElement).value = attrs.href;
|
||||
}
|
||||
if (attrs && attrs.target) {
|
||||
(instance.popper.querySelector("#target") as HTMLInputElement).value = attrs.target;
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
popover.setTrigger(this.querySelector("div")!, "bottom");
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
import {AbstractMenuButton} from "../AbstractMenuButton.ts";
|
||||
import {Editor} from "@tiptap/core";
|
||||
|
||||
export class OrderedList extends AbstractMenuButton {
|
||||
constructor() {
|
||||
super();
|
||||
const template = `
|
||||
<div>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M8 4H21V6H8V4ZM5 3V6H6V7H3V6H4V4H3V3H5ZM3 14V11.5H5V11H3V10H6V12.5H4V13H6V14H3ZM5 19.5H3V18.5H5V18H3V17H6V21H3V20H5V19.5ZM8 11H21V13H8V11ZM8 18H21V20H8V18Z"></path></svg>
|
||||
</div>
|
||||
`;
|
||||
this.template = template;
|
||||
this.registerClickListener();
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
onClick(commands) {
|
||||
commands.toggleOrderedList();
|
||||
}
|
||||
|
||||
onActive(editor: Editor): boolean {
|
||||
return editor.isActive("orderedList")
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
import {AbstractMenuButton} from "../AbstractMenuButton.ts";
|
||||
|
||||
export class Painter extends AbstractMenuButton {
|
||||
|
||||
svg:string = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M5 4.99658V7.99658H19V4.99658H5ZM4 2.99658H20C20.5523 2.99658 21 3.4443 21 3.99658V8.99658C21 9.54887 20.5523 9.99658 20 9.99658H4C3.44772 9.99658 3 9.54887 3 8.99658V3.99658C3 3.4443 3.44772 2.99658 4 2.99658ZM6 11.9966H12C12.5523 11.9966 13 12.4443 13 12.9966V15.9966H14V21.9966H10V15.9966H11V13.9966H5C4.44772 13.9966 4 13.5489 4 12.9966V10.9966H6V11.9966ZM17.7322 13.7288L19.5 11.961L21.2678 13.7288C22.2441 14.7051 22.2441 16.288 21.2678 17.2643C20.2915 18.2407 18.7085 18.2407 17.7322 17.2643C16.7559 16.288 16.7559 14.7051 17.7322 13.7288Z"></path></svg>`
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const template = `
|
||||
<div style="height: 16px">
|
||||
${this.svg}
|
||||
</div>
|
||||
`;
|
||||
this.template = template;
|
||||
this.registerClickListener();
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
onClick(commands) {
|
||||
commands.setPainter(this.editor?.state.selection.$head.marks())
|
||||
// console.log( htmlElement.style.cursor)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
import {AbstractMenuButton} from "../AbstractMenuButton.ts";
|
||||
|
||||
export class Printer extends AbstractMenuButton {
|
||||
constructor() {
|
||||
super();
|
||||
const template = `
|
||||
<div>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M17 2C17.5523 2 18 2.44772 18 3V7H21C21.5523 7 22 7.44772 22 8V18C22 18.5523 21.5523 19 21 19H18V21C18 21.5523 17.5523 22 17 22H7C6.44772 22 6 21.5523 6 21V19H3C2.44772 19 2 18.5523 2 18V8C2 7.44772 2.44772 7 3 7H6V3C6 2.44772 6.44772 2 7 2H17ZM16 17H8V20H16V17ZM20 9H4V17H6V16C6 15.4477 6.44772 15 7 15H17C17.5523 15 18 15.4477 18 16V17H20V9ZM8 10V12H5V10H8ZM16 4H8V7H16V4Z"></path></svg>
|
||||
</div>
|
||||
`;
|
||||
this.template = template;
|
||||
this.registerClickListener();
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
onClick(commands) {
|
||||
|
||||
const html = this.closest(".aie-container")!.querySelector(".aie-content")!.innerHTML;
|
||||
|
||||
const style :string = Array.from(document.querySelectorAll('style, link'))
|
||||
.map((el) => el.outerHTML).join('');
|
||||
|
||||
const content: string = style + html;
|
||||
|
||||
const iframe: HTMLIFrameElement = document.createElement('iframe');
|
||||
iframe.id = 'aie-print-iframe';
|
||||
iframe.setAttribute('style', 'position: absolute; width: 0; height: 0; top: -10px; left: -10px;');
|
||||
document.body.appendChild(iframe);
|
||||
|
||||
const frameWindow = iframe.contentWindow;
|
||||
const frameDoc = iframe.contentDocument || (iframe.contentWindow && iframe.contentWindow.document);
|
||||
|
||||
if (frameDoc) {
|
||||
frameDoc.open();
|
||||
frameDoc.write(content);
|
||||
frameDoc.close();
|
||||
}
|
||||
|
||||
if (frameWindow) {
|
||||
iframe.onload = function() {
|
||||
try {
|
||||
setTimeout(() => {
|
||||
frameWindow.focus();
|
||||
try {
|
||||
if (!frameWindow.document.execCommand('print', false)) {
|
||||
frameWindow.print();
|
||||
}
|
||||
} catch (e) {
|
||||
frameWindow.print();
|
||||
}
|
||||
frameWindow.close();
|
||||
}, 10);
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
setTimeout(function() {
|
||||
document.body.removeChild(iframe);
|
||||
}, 100);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import {AbstractMenuButton} from "../AbstractMenuButton.ts";
|
||||
|
||||
export class Quote extends AbstractMenuButton {
|
||||
constructor() {
|
||||
super();
|
||||
const template = `
|
||||
<div>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M4.58341 17.3211C3.55316 16.2275 3 15 3 13.0104C3 9.51092 5.45651 6.37372 9.03059 4.82324L9.92328 6.20085C6.58804 8.00545 5.93618 10.3461 5.67564 11.8221C6.21263 11.5444 6.91558 11.4467 7.60471 11.5106C9.40908 11.6778 10.8312 13.1591 10.8312 15C10.8312 16.933 9.26416 18.5 7.33116 18.5C6.2581 18.5 5.23196 18.0096 4.58341 17.3211ZM14.5834 17.3211C13.5532 16.2275 13 15 13 13.0104C13 9.51092 15.4565 6.37372 19.0306 4.82324L19.9233 6.20085C16.588 8.00545 15.9362 10.3461 15.6756 11.8221C16.2126 11.5444 16.9156 11.4467 17.6047 11.5106C19.4091 11.6778 20.8312 13.1591 20.8312 15C20.8312 16.933 19.2642 18.5 17.3312 18.5C16.2581 18.5 15.232 18.0096 14.5834 17.3211Z"></path></svg>
|
||||
</div>
|
||||
`;
|
||||
this.template = template;
|
||||
this.registerClickListener();
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
onClick(commands) {
|
||||
commands.toggleBlockquote();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import {AbstractMenuButton} from "../AbstractMenuButton.ts";
|
||||
|
||||
export class Redo extends AbstractMenuButton {
|
||||
constructor() {
|
||||
super();
|
||||
const template = `
|
||||
<div style="height: 16px">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M18.1716 6.99955H11C7.68629 6.99955 5 9.68584 5 12.9996C5 16.3133 7.68629 18.9996 11 18.9996H20V20.9996H11C6.58172 20.9996 3 17.4178 3 12.9996C3 8.58127 6.58172 4.99955 11 4.99955H18.1716L15.636 2.46402L17.0503 1.0498L22 5.99955L17.0503 10.9493L15.636 9.53509L18.1716 6.99955Z"></path></svg>
|
||||
</div>
|
||||
`;
|
||||
this.template = template;
|
||||
this.registerClickListener();
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
onClick(commands) {
|
||||
commands.redo();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
import {AbstractMenuButton} from "../AbstractMenuButton.ts";
|
||||
import {Editor} from "@tiptap/core";
|
||||
|
||||
export class Strike extends AbstractMenuButton {
|
||||
constructor() {
|
||||
super();
|
||||
const template = `
|
||||
<div>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M17.1538 14C17.3846 14.5161 17.5 15.0893 17.5 15.7196C17.5 17.0625 16.9762 18.1116 15.9286 18.867C14.8809 19.6223 13.4335 20 11.5862 20C9.94674 20 8.32335 19.6185 6.71592 18.8555V16.6009C8.23538 17.4783 9.7908 17.917 11.3822 17.917C13.9333 17.917 15.2128 17.1846 15.2208 15.7196C15.2208 15.0939 15.0049 14.5598 14.5731 14.1173C14.5339 14.0772 14.4939 14.0381 14.4531 14H3V12H21V14H17.1538ZM13.076 11H7.62908C7.4566 10.8433 7.29616 10.6692 7.14776 10.4778C6.71592 9.92084 6.5 9.24559 6.5 8.45207C6.5 7.21602 6.96583 6.165 7.89749 5.299C8.82916 4.43299 10.2706 4 12.2219 4C13.6934 4 15.1009 4.32808 16.4444 4.98426V7.13591C15.2448 6.44921 13.9293 6.10587 12.4978 6.10587C10.0187 6.10587 8.77917 6.88793 8.77917 8.45207C8.77917 8.87172 8.99709 9.23796 9.43293 9.55079C9.86878 9.86362 10.4066 10.1135 11.0463 10.3004C11.6665 10.4816 12.3431 10.7148 13.076 11H13.076Z"></path></svg>
|
||||
</div>
|
||||
`;
|
||||
this.template = template;
|
||||
this.registerClickListener();
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
onClick(commands) {
|
||||
commands.toggleStrike();
|
||||
}
|
||||
|
||||
onActive(editor: Editor): boolean {
|
||||
return editor.isActive("strike")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
import {AbstractMenuButton} from "../AbstractMenuButton.ts";
|
||||
import {Editor} from "@tiptap/core";
|
||||
|
||||
export class Subscript extends AbstractMenuButton {
|
||||
constructor() {
|
||||
super();
|
||||
const template = `
|
||||
<div>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M5.59567 4L10.5 9.92831L15.4043 4H18L11.7978 11.4971L18 18.9943V19H15.4091L10.5 13.0659L5.59092 19H3V18.9943L9.20216 11.4971L3 4H5.59567ZM21.8 16C21.8 15.5582 21.4418 15.2 21 15.2C20.5582 15.2 20.2 15.5582 20.2 16C20.2 16.0762 20.2107 16.15 20.2306 16.2198L19.0765 16.5496C19.0267 16.375 19 16.1906 19 16C19 14.8954 19.8954 14 21 14C22.1046 14 23 14.8954 23 16C23 16.5727 22.7593 17.0892 22.3735 17.4538L20.7441 19H23V20H19V19L21.5507 16.5803C21.7042 16.4345 21.8 16.2284 21.8 16Z"></path></svg>
|
||||
</div>
|
||||
`;
|
||||
this.template = template;
|
||||
this.registerClickListener();
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
onClick(commands) {
|
||||
commands.toggleSubscript();
|
||||
}
|
||||
|
||||
onActive(editor: Editor): boolean {
|
||||
return editor.isActive("subscript")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
import {AbstractMenuButton} from "../AbstractMenuButton.ts";
|
||||
import {Editor} from "@tiptap/core";
|
||||
|
||||
export class Superscript extends AbstractMenuButton {
|
||||
constructor() {
|
||||
super();
|
||||
const template = `
|
||||
<div>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M5.59567 5L10.5 10.9283L15.4043 5H18L11.7978 12.4971L18 19.9943V20H15.4091L10.5 14.0659L5.59092 20H3V19.9943L9.20216 12.4971L3 5H5.59567ZM21.5507 6.5803C21.7042 6.43453 21.8 6.22845 21.8 6C21.8 5.55817 21.4418 5.2 21 5.2C20.5582 5.2 20.2 5.55817 20.2 6C20.2 6.07624 20.2107 6.14999 20.2306 6.21983L19.0765 6.54958C19.0267 6.37497 19 6.1906 19 6C19 4.89543 19.8954 4 21 4C22.1046 4 23 4.89543 23 6C23 6.57273 22.7593 7.08923 22.3735 7.45384L20.7441 9H23V10H19V9L21.5507 6.5803V6.5803Z"></path></svg>
|
||||
</div>
|
||||
`;
|
||||
this.template = template;
|
||||
this.registerClickListener();
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
onClick(commands) {
|
||||
commands.toggleSuperscript();
|
||||
}
|
||||
|
||||
onActive(editor: Editor): boolean {
|
||||
return editor.isActive("superscript")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
import {AbstractMenuButton} from "../AbstractMenuButton.ts";
|
||||
import tippy, {Instance} from "tippy.js";
|
||||
|
||||
export class Table extends AbstractMenuButton {
|
||||
|
||||
instance?:Instance;
|
||||
constructor() {
|
||||
super();
|
||||
const template = `
|
||||
<div id="xxx">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M14 10H10V14H14V10ZM16 10V14H19V10H16ZM14 19V16H10V19H14ZM16 19H19V16H16V19ZM14 5H10V8H14V5ZM16 5V8H19V5H16ZM8 10H5V14H8V10ZM8 19V16H5V19H8ZM8 5H5V8H8V5ZM4 3H20C20.5523 3 21 3.44772 21 4V20C21 20.5523 20.5523 21 20 21H4C3.44772 21 3 20.5523 3 20V4C3 3.44772 3.44772 3 4 3Z"></path></svg>
|
||||
</div>
|
||||
`;
|
||||
this.template = template;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
this.instance = tippy(this.querySelector("#xxx")!, {
|
||||
content: this.createMenuElement(),
|
||||
appendTo: this.closest(".aie-container")!,
|
||||
placement: 'bottom',
|
||||
trigger: 'click',
|
||||
interactive: true,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
createMenuElement() {
|
||||
const div = document.createElement("div");
|
||||
div.style.height = "250px"
|
||||
div.style.width = "250px"
|
||||
div.style.background = "#fff"
|
||||
div.style.border = "1px solid #ccc"
|
||||
div.innerHTML = `
|
||||
<div style="margin: 5px">
|
||||
<div style="padding: 5px 0;font-size: 14px;color: #666">插入表格</div>
|
||||
<div style="display: flex;flex-wrap: wrap;width: 240px;height: 200px" id="table-cells">
|
||||
${[...Array(8).keys()].map((_, i) => {
|
||||
return [...Array(10).keys()].map((_, j) => {
|
||||
return `<div data-i="${i}" data-j="${j}" class="table-cell" style="width: 20px;height: 20px;margin:1px;border: solid 1px #ccc"></div>`;
|
||||
}).join('');
|
||||
}).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const tableCells = div.querySelector("#table-cells")!;
|
||||
tableCells.addEventListener("click", (e) => {
|
||||
const target: HTMLDivElement = (e.target as any).closest('.table-cell');
|
||||
if (target) {
|
||||
let i = target.getAttribute("data-i");
|
||||
let j = target.getAttribute("data-j");
|
||||
this.editor?.commands.insertTable({rows: Number(i) + 1, cols: Number(j) + 1, withHeaderRow: true})
|
||||
this.instance?.hide()
|
||||
}
|
||||
});
|
||||
|
||||
tableCells.addEventListener("mouseover", (e) => {
|
||||
const target: HTMLDivElement = (e.target as any).closest('.table-cell');
|
||||
if (target) {
|
||||
let targetI = Number(target.getAttribute("data-i"));
|
||||
let targetJ = Number(target.getAttribute("data-j"));
|
||||
const nodeList = tableCells.querySelectorAll("div");
|
||||
nodeList.forEach((element) => {
|
||||
let i = Number(element.getAttribute("data-i"));
|
||||
let j = Number(element.getAttribute("data-j"));
|
||||
if (i <= targetI && j <= targetJ) {
|
||||
element.style.border = "solid 1px #000"
|
||||
} else {
|
||||
element.style.border = "solid 1px #ccc"
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
tableCells.addEventListener("mouseleave", () => {
|
||||
const nodeList = tableCells.querySelectorAll("div");
|
||||
nodeList.forEach((element) => {
|
||||
element.style.border = "solid 1px #ccc"
|
||||
})
|
||||
})
|
||||
|
||||
return div;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
import {Editor} from "@tiptap/core";
|
||||
import {AbstractDropdownMenuButton} from "../AbstractDropdownMenuButton.ts";
|
||||
|
||||
const titles = ["正文", "标题 1", "标题 2", "标题 3", "标题 4", "标题 5", "标题 6"];
|
||||
export class Title extends AbstractDropdownMenuButton<string> {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.menuData = titles;
|
||||
this.dropDivHeight = "265px";
|
||||
this.dropDivWith = "150px";
|
||||
|
||||
}
|
||||
|
||||
onDropdownActive(editor: Editor, index: number): boolean {
|
||||
if (index == 0) {
|
||||
return editor.isActive("paragraph")
|
||||
}
|
||||
return editor.isActive("heading", {level: index});
|
||||
}
|
||||
|
||||
onDropdownItemClick(index: number): void {
|
||||
if (index == 0) {
|
||||
this.editor!.chain().setParagraph().run()
|
||||
} else {
|
||||
this.editor!.chain().setHeading({level: index as any}).run();
|
||||
}
|
||||
}
|
||||
|
||||
onDropdownItemRender(index: number): Element | string {
|
||||
if (index == 0) return this.menuData[index];
|
||||
return `<h${index}>${this.menuData[index]}</h${index}>`;
|
||||
}
|
||||
|
||||
onMenuTextRender(index: number): Element | string {
|
||||
return this.menuData[index];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
import {AbstractMenuButton} from "../AbstractMenuButton.ts";
|
||||
import {Editor} from "@tiptap/core";
|
||||
|
||||
export class Todo extends AbstractMenuButton {
|
||||
constructor() {
|
||||
super();
|
||||
const template = `
|
||||
<div>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M4 3H20C20.5523 3 21 3.44772 21 4V20C21 20.5523 20.5523 21 20 21H4C3.44772 21 3 20.5523 3 20V4C3 3.44772 3.44772 3 4 3ZM5 5V19H19V5H5ZM11.0026 16L6.75999 11.7574L8.17421 10.3431L11.0026 13.1716L16.6595 7.51472L18.0737 8.92893L11.0026 16Z"></path></svg>
|
||||
</div>
|
||||
`;
|
||||
this.template = template;
|
||||
this.registerClickListener();
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
onClick(commands) {
|
||||
commands.toggleTaskList();
|
||||
}
|
||||
|
||||
onActive(editor: Editor): boolean {
|
||||
return editor.isActive("taskList")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
import {AbstractMenuButton} from "../AbstractMenuButton.ts";
|
||||
import {Editor} from "@tiptap/core";
|
||||
|
||||
export class Underline extends AbstractMenuButton {
|
||||
constructor() {
|
||||
super();
|
||||
const template = `
|
||||
<div>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M8 3V12C8 14.2091 9.79086 16 12 16C14.2091 16 16 14.2091 16 12V3H18V12C18 15.3137 15.3137 18 12 18C8.68629 18 6 15.3137 6 12V3H8ZM4 20H20V22H4V20Z"></path></svg>
|
||||
</div>
|
||||
`;
|
||||
this.template = template;
|
||||
this.registerClickListener();
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
onClick(commands) {
|
||||
commands.toggleUnderline();
|
||||
}
|
||||
|
||||
onActive(editor: Editor): boolean {
|
||||
return editor.isActive("underline")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import {AbstractMenuButton} from "../AbstractMenuButton.ts";
|
||||
|
||||
export class Undo extends AbstractMenuButton {
|
||||
constructor() {
|
||||
super();
|
||||
const template = `
|
||||
<div style="height: 16px">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M5.82843 6.99955L8.36396 9.53509L6.94975 10.9493L2 5.99955L6.94975 1.0498L8.36396 2.46402L5.82843 4.99955H13C17.4183 4.99955 21 8.58127 21 12.9996C21 17.4178 17.4183 20.9996 13 20.9996H4V18.9996H13C16.3137 18.9996 19 16.3133 19 12.9996C19 9.68584 16.3137 6.99955 13 6.99955H5.82843Z"></path></svg>
|
||||
</div>
|
||||
`;
|
||||
this.template = template;
|
||||
this.registerClickListener();
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
onClick(commands) {
|
||||
commands.undo();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
import {AbstractMenuButton} from "../AbstractMenuButton.ts";
|
||||
|
||||
export class Video extends AbstractMenuButton {
|
||||
|
||||
fileInput?: HTMLInputElement;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const template = `
|
||||
<div>
|
||||
<input type="file" accept="video/*" style="display: none">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M3 3.9934C3 3.44476 3.44495 3 3.9934 3H20.0066C20.5552 3 21 3.44495 21 3.9934V20.0066C21 20.5552 20.5551 21 20.0066 21H3.9934C3.44476 21 3 20.5551 3 20.0066V3.9934ZM5 5V19H19V5H5ZM10.6219 8.41459L15.5008 11.6672C15.6846 11.7897 15.7343 12.0381 15.6117 12.2219C15.5824 12.2658 15.5447 12.3035 15.5008 12.3328L10.6219 15.5854C10.4381 15.708 10.1897 15.6583 10.0672 15.4745C10.0234 15.4088 10 15.3316 10 15.2526V8.74741C10 8.52649 10.1791 8.34741 10.4 8.34741C10.479 8.34741 10.5562 8.37078 10.6219 8.41459Z"></path></svg>
|
||||
</div>
|
||||
`;
|
||||
this.template = template;
|
||||
this.registerClickListener();
|
||||
}
|
||||
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (this.options?.video?.customMenuInvoke) {
|
||||
this.querySelector("input")!.remove();
|
||||
} else {
|
||||
this.fileInput = this.querySelector("input") as HTMLInputElement;
|
||||
this.fileInput!.addEventListener("change", () => {
|
||||
const files = this.fileInput?.files;
|
||||
if (files && files.length > 0) {
|
||||
for (let file of files) {
|
||||
this.editor?.commands.uploadVideo(file);
|
||||
}
|
||||
}
|
||||
(this.fileInput as any).value = "";
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// @ts-ignore
|
||||
onClick(commands) {
|
||||
if (this.options?.video?.customMenuInvoke) {
|
||||
this.options.video.customMenuInvoke(this.editor!);
|
||||
} else {
|
||||
this.fileInput?.click();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,163 @@
|
|||
import {Editor, Editor as Tiptap, EditorEvents} from "@tiptap/core";
|
||||
|
||||
import {Header} from "../components/Header.ts";
|
||||
import {Footer} from "../components/Footer.ts";
|
||||
|
||||
import {getExtensions} from "./getExtensions.ts";
|
||||
|
||||
import "../styles"
|
||||
|
||||
window.customElements.define('aie-menus', Header);
|
||||
window.customElements.define('aie-footer', Footer);
|
||||
|
||||
export interface NameAndValue {
|
||||
name: string,
|
||||
value: any;
|
||||
}
|
||||
|
||||
export interface AiMenu {
|
||||
icon: string,
|
||||
name: string,
|
||||
prompt: string,
|
||||
text: "selected" | "focusBefore",
|
||||
model: string,
|
||||
}
|
||||
|
||||
export interface AiEditorEvent {
|
||||
onCreate: (props: EditorEvents['create'], options: AiEditorOptions) => void
|
||||
onTransaction: (props: EditorEvents['transaction']) => void
|
||||
}
|
||||
|
||||
|
||||
export type AiEditorOptions = {
|
||||
element: string | Element,
|
||||
content?: string,
|
||||
placeHolder?: string,
|
||||
theme?: "light" | "dark",
|
||||
cbName?: string,
|
||||
cbUrl?: string
|
||||
onMentionQuery?: (query: string) => any[] | Promise<any[]>,
|
||||
toolbarKeys?: string[],
|
||||
uploader?: (file: File, uploadUrl: string, headers: Record<string, any>, formName: string) => Promise<Record<string, any>>,
|
||||
image?: {
|
||||
customMenuInvoke?: (editor: Editor) => void;
|
||||
uploadUrl?: string,
|
||||
uploadHeaders?: Record<string, any>,
|
||||
uploader?: (file: File, uploadUrl: string, headers: Record<string, any>, formName: string) => Promise<Record<string, any>>,
|
||||
},
|
||||
video?: {
|
||||
customMenuInvoke?: (editor: Editor) => void;
|
||||
uploadUrl?: string,
|
||||
uploadHeaders?: Record<string, any>,
|
||||
uploader?: (file: File, uploadUrl: string, headers: Record<string, any>, formName: string) => Promise<Record<string, any>>,
|
||||
},
|
||||
attachment?: {
|
||||
customMenuInvoke?: (editor: Editor) => void;
|
||||
uploadUrl?: string,
|
||||
uploadHeaders?: Record<string, any>,
|
||||
uploader?: (file: File, uploadUrl: string, headers: Record<string, any>, formName: string) => Promise<Record<string, any>>,
|
||||
},
|
||||
fontFamily?: {
|
||||
values: NameAndValue[]
|
||||
},
|
||||
fontSize?: {
|
||||
values: NameAndValue[]
|
||||
},
|
||||
ai?: {
|
||||
model: {
|
||||
xinghuo?: {
|
||||
appId: string,
|
||||
apiKey: string,
|
||||
apiSecret: string,
|
||||
version?: string,
|
||||
}
|
||||
},
|
||||
menus?: AiMenu[],
|
||||
}
|
||||
}
|
||||
|
||||
const defaultOptions: Partial<AiEditorOptions> = {
|
||||
theme: "light",
|
||||
placeHolder: "请输入",
|
||||
}
|
||||
|
||||
export class AiEditor {
|
||||
|
||||
tiptap: Tiptap;
|
||||
|
||||
container: HTMLDivElement;
|
||||
|
||||
menus: Header;
|
||||
|
||||
footer: Footer;
|
||||
|
||||
options: AiEditorOptions;
|
||||
|
||||
eventComponents: AiEditorEvent[] = [];
|
||||
|
||||
constructor(options: AiEditorOptions) {
|
||||
this.options = Object.assign(defaultOptions, options);
|
||||
|
||||
const rootEl = typeof this.options.element === "string"
|
||||
? document.querySelector(this.options.element) as Element : this.options.element;
|
||||
|
||||
//set the editor theme class
|
||||
rootEl.classList.add(`aie-theme-${options.theme}`);
|
||||
|
||||
this.container = document.createElement("div");
|
||||
this.container.classList.add("aie-container");
|
||||
|
||||
rootEl.appendChild(this.container);
|
||||
|
||||
|
||||
const mainEl = document.createElement("div");
|
||||
mainEl.style.flexGrow = "1";
|
||||
mainEl.style.overflow = "auto";
|
||||
|
||||
this.menus = document.createElement("aie-menus") as Header;
|
||||
this.footer = document.createElement("aie-footer") as Footer;
|
||||
|
||||
this.eventComponents.push(this.menus);
|
||||
this.eventComponents.push(this.footer);
|
||||
|
||||
this.tiptap = new Tiptap({
|
||||
element: mainEl,
|
||||
content: options.content,
|
||||
extensions: getExtensions(this, options),
|
||||
onCreate: (props) => this.onCreate(props, mainEl),
|
||||
onTransaction: (props) => this.onTransaction(props),
|
||||
onDestroy: () => this.onDestroy,
|
||||
editorProps: {
|
||||
attributes: {
|
||||
class: "aie-content"
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
// @ts-ignore
|
||||
window.editor = this.tiptap;
|
||||
}
|
||||
|
||||
onCreate(props: EditorEvents['create'], mainEl: Element) {
|
||||
this.tiptap.view.dom.style.height = "calc(100% - 20px)"
|
||||
this.eventComponents.forEach((zEvent) => {
|
||||
zEvent.onCreate && zEvent.onCreate(props, this.options);
|
||||
});
|
||||
|
||||
this.container.appendChild(this.menus);
|
||||
this.container.appendChild(mainEl);
|
||||
this.container.appendChild(this.footer);
|
||||
}
|
||||
|
||||
onTransaction(props: EditorEvents['transaction']) {
|
||||
this.eventComponents.forEach((zEvent) => {
|
||||
zEvent.onTransaction && zEvent.onTransaction(props);
|
||||
});
|
||||
}
|
||||
|
||||
onDestroy() {
|
||||
while (this.container.firstChild) {
|
||||
this.container.firstChild.remove();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,124 @@
|
|||
import {Extensions, posToDOMRect} from "@tiptap/core";
|
||||
import {AiEditor} from "./AiEditor.ts";
|
||||
import {BubbleMenu} from "@tiptap/extension-bubble-menu";
|
||||
|
||||
|
||||
import {LinkBubbleMenu} from "../components/bubbles/LinkBubbleMenu.ts";
|
||||
import {AbstractBubbleMenu} from "../components/AbstractBubbleMenu.ts";
|
||||
import {ImageBubbleMenu} from "../components/bubbles/ImageBubbleMenu.ts";
|
||||
import {TableBubbleMenu} from "../components/bubbles/TableBubbleMenu.ts";
|
||||
|
||||
// import { CellSelection } from '@tiptap/pm/tables';
|
||||
// export function isCellSelection(value: unknown): value is CellSelection {
|
||||
// return value instanceof CellSelection
|
||||
// }
|
||||
|
||||
|
||||
// export declare function isCellSelection(value: unknown): value is CellSelection;
|
||||
|
||||
|
||||
// import {isCellSelection} from "@tiptap/extension-table";
|
||||
// import {Popover} from "../components/Popover";
|
||||
|
||||
// import {Plugin} from "tippy.js"
|
||||
|
||||
window.customElements.define('aie-bubble-link', LinkBubbleMenu);
|
||||
window.customElements.define('aie-bubble-image', ImageBubbleMenu);
|
||||
window.customElements.define('aie-bubble-table', TableBubbleMenu);
|
||||
|
||||
|
||||
const createLinkBubbleMenu = (zEditor: AiEditor) => {
|
||||
|
||||
const menuEl = document.createElement("aie-bubble-link") as AbstractBubbleMenu;
|
||||
zEditor.eventComponents.push(menuEl);
|
||||
|
||||
return BubbleMenu.configure({
|
||||
pluginKey: 'linkBubble',
|
||||
element: menuEl,
|
||||
tippyOptions: {
|
||||
placement: 'bottom-end',
|
||||
},
|
||||
shouldShow: ({editor}) => {
|
||||
return editor.isActive("link")
|
||||
}
|
||||
})
|
||||
}
|
||||
const createImageBubbleMenu = (zEditor: AiEditor) => {
|
||||
|
||||
const menuEl = document.createElement("aie-bubble-image") as AbstractBubbleMenu;
|
||||
zEditor.eventComponents.push(menuEl);
|
||||
|
||||
|
||||
return BubbleMenu.configure({
|
||||
pluginKey: 'imageBubble',
|
||||
element: menuEl,
|
||||
tippyOptions: {
|
||||
appendTo: zEditor.container,
|
||||
placement: 'top-start',
|
||||
getReferenceClientRect: (() => {
|
||||
const {ranges} = zEditor.tiptap.state.selection
|
||||
const from = Math.min(...ranges.map(range => range.$from.pos))
|
||||
const to = Math.max(...ranges.map(range => range.$to.pos))
|
||||
const view = zEditor.tiptap.view;
|
||||
|
||||
let node = view.nodeDOM(from) as HTMLElement
|
||||
const imageEl = node.querySelector("img") as HTMLImageElement;
|
||||
|
||||
const domRect = posToDOMRect(view, from, to);
|
||||
const imgRect = imageEl.getBoundingClientRect();
|
||||
return {
|
||||
...domRect,
|
||||
left: imgRect.left + imgRect.width * 0.25
|
||||
}
|
||||
})
|
||||
},
|
||||
shouldShow: ({editor}) => {
|
||||
return editor.isActive("image")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
const createTableBubbleMenu = (zEditor: AiEditor) => {
|
||||
|
||||
const menuEl = document.createElement("aie-bubble-table") as AbstractBubbleMenu;
|
||||
zEditor.eventComponents.push(menuEl);
|
||||
|
||||
return BubbleMenu.configure({
|
||||
pluginKey: 'tableBubble',
|
||||
element: menuEl,
|
||||
tippyOptions: {
|
||||
placement: 'top',
|
||||
getReferenceClientRect: (() => {
|
||||
const selection = zEditor.tiptap.state.selection;
|
||||
const {ranges} = selection
|
||||
const from = Math.min(...ranges.map(range => range.$from.pos))
|
||||
const to = Math.max(...ranges.map(range => range.$to.pos))
|
||||
const view = zEditor.tiptap.view;
|
||||
|
||||
const domRect = posToDOMRect(view, from, to);
|
||||
const tablePos = zEditor.tiptap.state.selection.$from.posAtIndex(0, 1);
|
||||
const coordsAtPos = zEditor.tiptap.view.coordsAtPos(tablePos);
|
||||
|
||||
return {
|
||||
...domRect,
|
||||
top: coordsAtPos.top
|
||||
};
|
||||
})
|
||||
},
|
||||
shouldShow: ({editor}) => {
|
||||
return editor.isActive("table")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
export const getBubbleMenus = (zEditor: AiEditor): Extensions => {
|
||||
const bubbleMenus: Extensions = [];
|
||||
|
||||
bubbleMenus.push(createLinkBubbleMenu(zEditor))
|
||||
bubbleMenus.push(createImageBubbleMenu(zEditor))
|
||||
bubbleMenus.push(createTableBubbleMenu(zEditor))
|
||||
|
||||
return bubbleMenus;
|
||||
}
|
|
@ -0,0 +1,122 @@
|
|||
import {Extensions} from "@tiptap/core";
|
||||
import StarterKit from "@tiptap/starter-kit";
|
||||
import {Underline} from "@tiptap/extension-underline";
|
||||
import {TextStyle} from "@tiptap/extension-text-style";
|
||||
import {FontFamily} from "@tiptap/extension-font-family";
|
||||
import {AttachmentExt} from "../extensions/AttachmentExt.ts";
|
||||
import {PainterExt} from "../extensions/PainterExt.ts";
|
||||
import {Highlight} from "@tiptap/extension-highlight";
|
||||
import {Color} from "@tiptap/extension-color";
|
||||
import {FontSizeExt} from "../extensions/FontSizeExt.ts";
|
||||
import {LineHeightExt} from "../extensions/LineHeightExt.ts";
|
||||
import {TextAlign} from "@tiptap/extension-text-align";
|
||||
import {IndentExt} from "../extensions/IndentExt.ts";
|
||||
import {ImageExt} from "../extensions/ImageExt.ts";
|
||||
import {Table} from "@tiptap/extension-table";
|
||||
import {TableRow} from "@tiptap/extension-table-row";
|
||||
import {TableHeader} from "@tiptap/extension-table-header";
|
||||
import {TableCell} from "@tiptap/extension-table-cell";
|
||||
import {CharacterCount} from "@tiptap/extension-character-count";
|
||||
import {Link} from "@tiptap/extension-link";
|
||||
import {Superscript} from "@tiptap/extension-superscript";
|
||||
import {Subscript} from "@tiptap/extension-subscript";
|
||||
import {TaskList} from "@tiptap/extension-task-list";
|
||||
import {TaskItem} from "@tiptap/extension-task-item";
|
||||
import {CodeBlockExt} from "../extensions/CodeBlockExt.ts";
|
||||
import {common, createLowlight} from "lowlight";
|
||||
import {VideoExt} from "../extensions/VideoExt.ts";
|
||||
import {IFrameExt} from "../extensions/IFrameExt.ts";
|
||||
import {getBubbleMenus} from "./getBubbleMenus.ts";
|
||||
import {Placeholder} from "@tiptap/extension-placeholder";
|
||||
// import {HocuspocusProvider} from "@hocuspocus/provider";
|
||||
// import {Collaboration} from "@tiptap/extension-collaboration";
|
||||
import {createMention} from "../extensions/MentionExt.ts";
|
||||
import {AiEditor, AiEditorOptions} from "./AiEditor.ts";
|
||||
|
||||
export const getExtensions = (editor: AiEditor, options: AiEditorOptions): Extensions => {
|
||||
// the Collaboration extension comes with its own history handling
|
||||
const ret: Extensions = options.cbName && options.cbUrl ? [StarterKit.configure({
|
||||
history: false,
|
||||
codeBlock: false,
|
||||
})] : [StarterKit.configure({
|
||||
codeBlock: false
|
||||
})];
|
||||
|
||||
{
|
||||
//push default extensions
|
||||
ret.push(Underline, TextStyle, FontFamily,
|
||||
AttachmentExt.configure({
|
||||
uploadUrl: options.video?.uploadUrl,
|
||||
uploadHeaders: options.video?.uploadHeaders,
|
||||
uploader: options.video?.uploader || options.uploader,
|
||||
}),
|
||||
PainterExt,
|
||||
Highlight.configure({
|
||||
multicolor: true
|
||||
}),
|
||||
Color, FontSizeExt, LineHeightExt,
|
||||
TextAlign.configure({
|
||||
types: ['heading', 'paragraph'],
|
||||
}),
|
||||
IndentExt,
|
||||
ImageExt.configure({
|
||||
allowBase64: true,
|
||||
uploadUrl: options.image?.uploadUrl,
|
||||
uploadHeaders: options.image?.uploadHeaders,
|
||||
uploader: options.image?.uploader || options.uploader,
|
||||
}),
|
||||
Table.configure({
|
||||
resizable: true,
|
||||
lastColumnResizable: true,
|
||||
allowTableNodeSelection: true,
|
||||
}),
|
||||
TableRow,
|
||||
TableHeader,
|
||||
TableCell,
|
||||
CharacterCount,
|
||||
Link.configure({
|
||||
openOnClick: false,
|
||||
}),
|
||||
Superscript,
|
||||
Subscript,
|
||||
TaskList,
|
||||
TaskItem.configure({
|
||||
nested: true,
|
||||
}),
|
||||
CodeBlockExt.configure({
|
||||
lowlight: createLowlight(common),
|
||||
defaultLanguage: 'plaintext',
|
||||
languageClassPrefix: 'language-',
|
||||
}),
|
||||
VideoExt.configure({
|
||||
uploadUrl: options.video?.uploadUrl,
|
||||
uploadHeaders: options.video?.uploadHeaders,
|
||||
uploader: options.video?.uploader || options.uploader,
|
||||
}),
|
||||
IFrameExt,
|
||||
...getBubbleMenus(editor),
|
||||
)
|
||||
}
|
||||
|
||||
if (options.placeHolder) {
|
||||
ret.push(Placeholder.configure({
|
||||
placeholder: options.placeHolder,
|
||||
}))
|
||||
}
|
||||
|
||||
// if (options.cbName && options.cbUrl) {
|
||||
// const provider = new HocuspocusProvider({
|
||||
// url: options.cbUrl,
|
||||
// name: options.cbName,
|
||||
// })
|
||||
// ret.push(Collaboration.configure({
|
||||
// document: provider.document,
|
||||
// }))
|
||||
// }
|
||||
|
||||
if (options.onMentionQuery) {
|
||||
ret.push(createMention(options.onMentionQuery))
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
|
@ -0,0 +1,188 @@
|
|||
// import { Extension} from '@tiptap/core'
|
||||
//
|
||||
// import Suggestion, {SuggestionOptions, SuggestionProps} from '@tiptap/suggestion'
|
||||
//
|
||||
//
|
||||
//
|
||||
// import tippy, {Instance} from "tippy.js";
|
||||
// import {XingHuoSocket} from "../ai/xinghuo/XingHuoSocket.ts";
|
||||
//
|
||||
// export type AiCommandOptions = {
|
||||
// HTMLAttributes?: Record<string, any>
|
||||
// suggestion: Omit<SuggestionOptions, 'editor'>
|
||||
// }
|
||||
//
|
||||
// export const AiCommandExt = Extension.create<AiCommandOptions>({
|
||||
// name: 'aiCommand',
|
||||
//
|
||||
// addOptions() {
|
||||
// return {
|
||||
// suggestion: {
|
||||
// char: '/',
|
||||
// command: ({editor, range, props}) => {
|
||||
// props.id.command({editor, range})
|
||||
// },
|
||||
// items: ({query}) => {
|
||||
// return [
|
||||
// {
|
||||
// title: 'AI 续写',
|
||||
// command: ({editor, range}) => {
|
||||
//
|
||||
// // const editor = e as Editor;
|
||||
// // editor.state.selection.
|
||||
//
|
||||
// // const textSelection = TextSelection.create(editor.state.doc,range.to,range.from);
|
||||
// // const slice = textSelection.content();
|
||||
//
|
||||
// const commandText = editor.state.doc.textBetween(range.from,range.to);
|
||||
// console.log("commandText: ",commandText)
|
||||
//
|
||||
// editor
|
||||
// .chain()
|
||||
// .focus()
|
||||
// .deleteRange(range)
|
||||
// .run()
|
||||
//
|
||||
// const textContent = editor.state.selection.$head.parent.textContent;
|
||||
// const ct = `"${textContent}" 请帮我继续扩展一些这段话的内容`
|
||||
//
|
||||
// const APISecret = "****"
|
||||
// const APIKey = "***"
|
||||
//
|
||||
// const xhSocket = new XingHuoSocket("****",APISecret, APIKey,"v3.1", editor);
|
||||
// xhSocket.start(ct);
|
||||
//
|
||||
//
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// title: 'AI 优化(改写)',
|
||||
// command: ({editor, range}) => {
|
||||
// editor
|
||||
// .chain()
|
||||
// .focus()
|
||||
// .deleteRange(range)
|
||||
// .setNode('heading', {level: 2})
|
||||
// .run()
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// title: 'AI 校对(发现错别字)',
|
||||
// command: ({editor, range}) => {
|
||||
// editor
|
||||
// .chain()
|
||||
// .focus()
|
||||
// .deleteRange(range)
|
||||
// .setMark('bold')
|
||||
// .run()
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// title: 'AI 翻译',
|
||||
// command: ({editor, range}) => {
|
||||
// editor
|
||||
// .chain()
|
||||
// .focus()
|
||||
// .deleteRange(range)
|
||||
// .setMark('italic')
|
||||
// .run()
|
||||
// },
|
||||
// },
|
||||
// ];//.filter(item => item.title.toLowerCase().startsWith(query.toLowerCase())).slice(0, 10)
|
||||
// },
|
||||
//
|
||||
//
|
||||
// render: () => {
|
||||
//
|
||||
// let element: HTMLDivElement;
|
||||
// let popup: Instance[];
|
||||
// let selectIndex: number = 0;
|
||||
// let suggestionProps: SuggestionProps;
|
||||
// const updateElement = () => {
|
||||
// element.innerHTML = `
|
||||
// <div class="items">
|
||||
// ${suggestionProps.items.map((item: any, index) => {
|
||||
// return `<button class="item ${index === selectIndex ? 'item-selected' : ''}">${item.title}</button>`
|
||||
// }).join("")}
|
||||
// </div>
|
||||
// `
|
||||
// }
|
||||
//
|
||||
// return {
|
||||
//
|
||||
// onStart: (props: SuggestionProps) => {
|
||||
//
|
||||
// element = document.createElement("div") as HTMLDivElement;
|
||||
// element.classList.add("suggestion")
|
||||
// suggestionProps = props;
|
||||
//
|
||||
// if (!props.clientRect) {
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// updateElement();
|
||||
//
|
||||
// // @ts-ignore
|
||||
// popup = tippy('body', {
|
||||
// getReferenceClientRect: props.clientRect,
|
||||
// appendTo: () => props.editor.view.dom.closest(".aie-container"),
|
||||
// content: element,
|
||||
// showOnCreate: true,
|
||||
// interactive: true,
|
||||
// allowHTML: true,
|
||||
// trigger: 'manual',
|
||||
// placement: 'left-start',
|
||||
// })
|
||||
// },
|
||||
//
|
||||
// onUpdate(props) {
|
||||
// suggestionProps = props;
|
||||
//
|
||||
// if (!props.clientRect) {
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// popup[0].setProps({
|
||||
// getReferenceClientRect: props.clientRect as any,
|
||||
// })
|
||||
// },
|
||||
//
|
||||
// onKeyDown(props) {
|
||||
// if (props.event.key === 'Escape') {
|
||||
// popup[0].hide();
|
||||
// return true;
|
||||
// } else if (props.event.key === "ArrowUp") {
|
||||
// selectIndex = (selectIndex + suggestionProps.items.length - 1) % suggestionProps.items.length
|
||||
// updateElement();
|
||||
// return true;
|
||||
// } else if (props.event.key === "ArrowDown") {
|
||||
// selectIndex = (selectIndex + 1) % suggestionProps.items.length
|
||||
// updateElement();
|
||||
// return true;
|
||||
// } else if (props.event.key === "Enter") {
|
||||
// const item = suggestionProps.items[selectIndex];
|
||||
// if (item) suggestionProps.command({id: item})
|
||||
// return true;
|
||||
// }
|
||||
// return false;
|
||||
// },
|
||||
//
|
||||
// onExit() {
|
||||
// popup[0].destroy()
|
||||
// element.remove()
|
||||
// },
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
// }
|
||||
// },
|
||||
//
|
||||
// addProseMirrorPlugins() {
|
||||
// return [
|
||||
// Suggestion({
|
||||
// editor: this.editor,
|
||||
// ...this.options.suggestion,
|
||||
// }),
|
||||
// ]
|
||||
// },
|
||||
// })
|
|
@ -0,0 +1,146 @@
|
|||
import {Plugin, PluginKey} from 'prosemirror-state'
|
||||
import {uuid} from "../util/uuid.ts";
|
||||
import {uploadFile} from "../util/uploadFile.ts";
|
||||
import {DecorationSet} from "prosemirror-view";
|
||||
import {createAttachmentDecoration} from "../util/decorations.ts";
|
||||
import {Extension} from "@tiptap/core";
|
||||
|
||||
export interface AttachmentOptions {
|
||||
HTMLAttributes: Record<string, any>,
|
||||
uploadUrl?: string,
|
||||
uploadHeaders: Record<string, any>,
|
||||
uploader?: (file: File, uploadUrl: string, headers: Record<string, any>, formName: string) => Promise<Record<string, any>>,
|
||||
}
|
||||
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
attachment: {
|
||||
uploadAttachment: (file: File) => ReturnType,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export type AttachmentAction = {
|
||||
type: "add" | "remove";
|
||||
id: string;
|
||||
pos: number;
|
||||
text: string,
|
||||
}
|
||||
|
||||
const key = new PluginKey("aie-attachment-plugin");
|
||||
const actionKey = "attachment_action";
|
||||
|
||||
export const AttachmentExt = Extension.create<AttachmentOptions>({
|
||||
name: 'attachment',
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
uploadUrl: "",
|
||||
uploadHeaders: {},
|
||||
HTMLAttributes: {},
|
||||
}
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
return {
|
||||
uploadAttachment: (file: File) => () => {
|
||||
const id = uuid();
|
||||
const {state: {tr}, view, schema} = this.editor!
|
||||
if (!tr.selection.empty) tr.deleteSelection();
|
||||
|
||||
view.dispatch(tr.setMeta(actionKey, {
|
||||
type: "add",
|
||||
id,
|
||||
pos: tr.selection.from,
|
||||
text: file.name,
|
||||
}));
|
||||
|
||||
if (this.options.uploadUrl) {
|
||||
const uploader = this.options.uploader || uploadFile;
|
||||
uploader(file, this.options.uploadUrl, this.options.uploadHeaders, "attachment")
|
||||
.then(json => {
|
||||
if (json.errorCode === 0 && json.data && json.data.href) {
|
||||
const decorations = key.getState(this.editor.state) as DecorationSet;
|
||||
let found = decorations.find(void 0, void 0, spec => spec.id == id)
|
||||
const fileName = json.data.fileName || file.name;
|
||||
view.dispatch(view.state.tr
|
||||
.insertText(` ${fileName} `, found[0].from)
|
||||
.addMark(found[0].from + 1, fileName.length + found[0].from + 1, schema.marks.link.create({
|
||||
href: json.href,
|
||||
}))
|
||||
.setMeta(actionKey, {type: "remove", id}));
|
||||
} else {
|
||||
view.dispatch(tr.setMeta(actionKey, {type: "remove", id}));
|
||||
}
|
||||
}).catch(() => {
|
||||
view.dispatch(tr.setMeta(actionKey, {type: "remove", id}));
|
||||
})
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
addProseMirrorPlugins() {
|
||||
const editor = this.editor;
|
||||
return [
|
||||
new Plugin({
|
||||
key: key,
|
||||
state: {
|
||||
init: () => DecorationSet.empty,
|
||||
apply: (tr, set) => {
|
||||
|
||||
const action = tr.getMeta(actionKey) as AttachmentAction;
|
||||
|
||||
// update decorations position
|
||||
set = set.map(tr.mapping, tr.doc);
|
||||
|
||||
// add decoration
|
||||
if (action && action.type === "add") {
|
||||
set = set.add(tr.doc, [createAttachmentDecoration(action)]);
|
||||
}
|
||||
// remove decoration
|
||||
else if (action && action.type === "remove") {
|
||||
set = set.remove(set.find(undefined, undefined,
|
||||
spec => spec.id == action.id));
|
||||
}
|
||||
return set;
|
||||
}
|
||||
},
|
||||
|
||||
props: {
|
||||
decorations(state) {
|
||||
return this.getState(state);
|
||||
},
|
||||
handleDOMEvents: {
|
||||
drop(_, event) {
|
||||
const hasFiles = event.dataTransfer &&
|
||||
event.dataTransfer.files &&
|
||||
event.dataTransfer.files.length
|
||||
|
||||
if (!hasFiles) return false
|
||||
|
||||
const attachments = Array
|
||||
.from(event.dataTransfer.files)
|
||||
.filter(file => !(/video/i).test(file.type) && !(/image/i).test(file.type))
|
||||
|
||||
event.preventDefault()
|
||||
|
||||
attachments.forEach(attachment => {
|
||||
editor.commands.uploadAttachment(attachment);
|
||||
})
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
]
|
||||
},
|
||||
|
||||
|
||||
})
|
|
@ -0,0 +1,372 @@
|
|||
import {CodeBlockLowlight, CodeBlockLowlightOptions} from "@tiptap/extension-code-block-lowlight";
|
||||
import tippy from "tippy.js";
|
||||
import {isActive} from "../util/isActive.ts";
|
||||
import {TextSelection} from "prosemirror-state";
|
||||
import {textblockTypeInputRule} from "../util/textblockTypeInputRule.ts";
|
||||
import { Selection } from '@tiptap/pm/state';
|
||||
import { Node } from '@tiptap/pm/model';
|
||||
|
||||
|
||||
export type LanguageItem = {
|
||||
name: string;
|
||||
value: string;
|
||||
alias?: string[];
|
||||
};
|
||||
|
||||
export const languages = [
|
||||
{ name: 'Plain Text', value: 'plaintext', alias: ['text', 'txt'] },
|
||||
{ name: 'Bash', value: 'bash', alias: ['sh'] },
|
||||
{ name: 'BASIC', value: 'basic', alias: [] },
|
||||
{ name: 'C', value: 'c', alias: ['h'] },
|
||||
{ name: 'Clojure', value: 'clojure', alias: ['clj', 'edn'] },
|
||||
{ name: 'CMake', value: 'cmake', alias: ['cmake.in'] },
|
||||
{
|
||||
name: 'CoffeeScript',
|
||||
value: 'coffeescript',
|
||||
alias: ['coffee', 'cson', 'iced'],
|
||||
},
|
||||
{
|
||||
name: 'C++',
|
||||
value: 'cpp',
|
||||
alias: ['cc', 'c++', 'h++', 'hpp', 'hh', 'hxx', 'cxx'],
|
||||
},
|
||||
{ name: 'C#', value: 'csharp', alias: ['cs', 'c#'] },
|
||||
{ name: 'CSS', value: 'css', alias: [] },
|
||||
{ name: 'Dart', value: 'dart', alias: [] },
|
||||
{ name: 'Delphi', value: 'delphi', alias: ['dpr', 'dfm', 'pas', 'pascal'] },
|
||||
{ name: 'Dockerfile', value: 'dockerfile', alias: ['docker'] },
|
||||
{ name: 'Erlang', value: 'erlang', alias: ['erl'] },
|
||||
{ name: 'Go', value: 'go', alias: ['golang'] },
|
||||
{ name: 'GraphQL', value: 'graphql', alias: ['gql'] },
|
||||
{ name: 'Groovy', value: 'groovy', alias: [] },
|
||||
{ name: 'Java', value: 'java', alias: ['jsp'] },
|
||||
{
|
||||
name: 'JavaScript',
|
||||
value: 'javascript',
|
||||
alias: ['js', 'jsx', 'mjs', 'cjs'],
|
||||
},
|
||||
{ name: 'JSON', value: 'json', alias: [] },
|
||||
{ name: 'Kotlin', value: 'kotlin', alias: ['kt', 'kts'] },
|
||||
{ name: 'Lua', value: 'lua', alias: [] },
|
||||
{ name: 'Makefile', value: 'makefile', alias: ['mk', 'mak', 'make'] },
|
||||
{ name: 'Markdown', value: 'markdown', alias: ['md', 'mkdown', 'mkd'] },
|
||||
{ name: 'Matlab', value: 'matlab', alias: [] },
|
||||
{
|
||||
name: 'Objective-C',
|
||||
value: 'objectivec',
|
||||
alias: ['mm', 'objc', 'obj-c', 'obj-c++', 'objective-c++'],
|
||||
},
|
||||
{ name: 'PHP', value: 'php', alias: [] },
|
||||
{ name: 'Properties', value: 'properties', alias: [] },
|
||||
{ name: 'Python', value: 'python', alias: ['py', 'gyp', 'ipython'] },
|
||||
{
|
||||
name: 'Ruby',
|
||||
value: 'ruby',
|
||||
alias: ['rb', 'gemspec', 'podspec', 'thor', 'irb'],
|
||||
},
|
||||
{ name: 'Rust', value: 'rust', alias: ['rs'] },
|
||||
{ name: 'Scala', value: 'scala', alias: [] },
|
||||
{ name: 'SCSS', value: 'scss', alias: [] },
|
||||
{ name: 'Shell', value: 'shell', alias: ['console', 'shellsession'] },
|
||||
{ name: 'SQL', value: 'sql', alias: [] },
|
||||
{ name: 'Swift', value: 'swift', alias: [] },
|
||||
{ name: 'TypeScript', value: 'typescript', alias: ['ts', 'tsx'] },
|
||||
{ name: 'WebAssembly', value: 'wasm', alias: [] },
|
||||
{
|
||||
name: 'HTML, XML',
|
||||
value: 'xml',
|
||||
alias: [
|
||||
'html',
|
||||
'xhtml',
|
||||
'rss',
|
||||
'atom',
|
||||
'xjb',
|
||||
'xsd',
|
||||
'xsl',
|
||||
'plist',
|
||||
'wsf',
|
||||
'svg',
|
||||
],
|
||||
},
|
||||
{ name: 'YAML', value: 'yaml', alias: ['yml'] },
|
||||
] as LanguageItem[];
|
||||
|
||||
|
||||
export const getLanguageByValueOrAlias = (
|
||||
valueOrAlias: string
|
||||
): LanguageItem | null => {
|
||||
if (!valueOrAlias) {
|
||||
return null;
|
||||
}
|
||||
const v = valueOrAlias.toLocaleLowerCase();
|
||||
const language = languages.find(
|
||||
(language) => language.value === v || (language.alias && language.alias.includes(v))
|
||||
);
|
||||
return language!;
|
||||
};
|
||||
|
||||
export const getLanguageByValue = (value: string): LanguageItem | null => {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
return languages.find((language) => language.value === value)!;
|
||||
};
|
||||
|
||||
export function getSelectedLineRange(
|
||||
selection: Selection,
|
||||
codeBlockNode: Node
|
||||
) {
|
||||
const { $from, from, to } = selection;
|
||||
const text = codeBlockNode.textContent || '';
|
||||
const lines = text.split('\n');
|
||||
|
||||
const lineLastIndexMap = lines.reduce((acc, line, index) => {
|
||||
acc[index] = (acc[index - 1] || 0) + line.length + (index === 0 ? 0 : 1);
|
||||
return acc;
|
||||
}, {} as { [key: number]: number });
|
||||
|
||||
const selectedTextStart = $from.parentOffset;
|
||||
const selectedTextEnd = $from.parentOffset + to - from;
|
||||
const lineKeys = Object.keys(lineLastIndexMap) as unknown as number[];
|
||||
const selectedLineStart: number | undefined = lineKeys.find(
|
||||
(index) => lineLastIndexMap[index] >= selectedTextStart
|
||||
);
|
||||
const selectedLineEnd: number | undefined = lineKeys.find(
|
||||
(index) => lineLastIndexMap[index] >= selectedTextEnd
|
||||
);
|
||||
return {
|
||||
start: selectedLineStart,
|
||||
end: selectedLineEnd,
|
||||
};
|
||||
}
|
||||
|
||||
export const backtickInputRegex = /^[`·]{3}([a-z]+)?[\s\n]$/;
|
||||
export const tildeInputRegex = /^[~~]{3}([a-z]+)?[\s\n]$/;
|
||||
|
||||
export interface MyCodeBlockLowlightOptions extends CodeBlockLowlightOptions {
|
||||
lowlight: any,
|
||||
defaultLanguage: string | null | undefined,
|
||||
languages:LanguageItem[]
|
||||
}
|
||||
|
||||
export const CodeBlockExt = CodeBlockLowlight.extend<MyCodeBlockLowlightOptions>({
|
||||
addOptions() {
|
||||
return {
|
||||
...this.parent?.(),
|
||||
lowlight: {},
|
||||
defaultLanguage: null,
|
||||
languages,
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
addCommands() {
|
||||
return {
|
||||
...this.parent?.(),
|
||||
toggleCodeBlock:
|
||||
(attributes) =>
|
||||
({ commands, editor, chain }) => {
|
||||
const { state } = editor;
|
||||
const { from, to } = state.selection;
|
||||
|
||||
// 如果选中范围是连续段落,则合并后转成一个 codeBlock
|
||||
if (!isActive(state, this.name) && !state.selection.empty) {
|
||||
let isSelectConsecutiveParagraphs = true;
|
||||
const textArr: string[] = [];
|
||||
state.doc.nodesBetween(from, to, (node, pos) => {
|
||||
if (node.isInline) {
|
||||
return false;
|
||||
}
|
||||
if (node.type.name !== 'paragraph') {
|
||||
if (pos + 1 <= from && pos + node.nodeSize - 1 >= to) {
|
||||
// 不要返回 false, 否则会中断遍历子节点
|
||||
return;
|
||||
} else {
|
||||
isSelectConsecutiveParagraphs = false;
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
const selectedText = (node.textContent || '').slice(
|
||||
pos + 1 > from ? 0 : from - pos - 1,
|
||||
pos + node.nodeSize - 1 < to
|
||||
? node.nodeSize - 1
|
||||
: to - pos - 1
|
||||
);
|
||||
textArr.push(selectedText || '');
|
||||
}
|
||||
});
|
||||
// 仅处理选择连续多个段落的情况
|
||||
if (isSelectConsecutiveParagraphs && textArr.length > 1) {
|
||||
return chain()
|
||||
.command(({ state, tr }) => {
|
||||
tr.replaceRangeWith(
|
||||
from,
|
||||
to,
|
||||
this.type.create(
|
||||
attributes,
|
||||
state.schema.text(textArr.join('\n'))
|
||||
)
|
||||
);
|
||||
return true;
|
||||
})
|
||||
.setTextSelection({
|
||||
from: from + 2,
|
||||
to: from + 2,
|
||||
})
|
||||
.run();
|
||||
}
|
||||
}
|
||||
|
||||
return commands.toggleNode(this.name, 'paragraph', attributes);
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
addKeyboardShortcuts() {
|
||||
return {
|
||||
...this.parent?.(),
|
||||
Tab: ({ editor }) => {
|
||||
const { state, view } = editor;
|
||||
if (!isActive(state, this.name)) {
|
||||
return false;
|
||||
}
|
||||
const { selection, tr } = state;
|
||||
const tab = ' ';
|
||||
if (selection.empty) {
|
||||
view.dispatch(tr.insertText(tab));
|
||||
} else {
|
||||
const { $from, from, to } = selection;
|
||||
const node = $from.node(); // code block node
|
||||
if (node.type !== this.type) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { start: selectedLineStart, end: selectedLineEnd } =
|
||||
getSelectedLineRange(selection, node);
|
||||
|
||||
//replace tab string
|
||||
if (selectedLineStart === undefined || selectedLineEnd === undefined) {
|
||||
view.dispatch(tr.replaceSelectionWith(state.schema.text(tab)));
|
||||
return true;
|
||||
}
|
||||
|
||||
const text = node.textContent || '';
|
||||
const lines = text.split('\n');
|
||||
const newLines = lines.map((line, index) => {
|
||||
if (
|
||||
index >= selectedLineStart &&
|
||||
index <= selectedLineEnd &&
|
||||
line
|
||||
) {
|
||||
return tab + line;
|
||||
}
|
||||
return line;
|
||||
});
|
||||
const codeBlockTextNode = $from.node(1);
|
||||
const codeBlockTextNodeStart = $from.start(1);
|
||||
tr.replaceWith(
|
||||
codeBlockTextNodeStart,
|
||||
codeBlockTextNodeStart + codeBlockTextNode.nodeSize - 2,
|
||||
state.schema.text(newLines.join('\n'))
|
||||
);
|
||||
tr.setSelection(
|
||||
TextSelection.between(
|
||||
tr.doc.resolve(from + tab.length),
|
||||
tr.doc.resolve(
|
||||
to + (selectedLineEnd - selectedLineStart + 1) * tab.length
|
||||
)
|
||||
)
|
||||
);
|
||||
view.dispatch(tr);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
addInputRules() {
|
||||
return [
|
||||
textblockTypeInputRule({
|
||||
find: backtickInputRegex,
|
||||
type: this.type,
|
||||
getAttributes: (match) => ({
|
||||
language:
|
||||
getLanguageByValueOrAlias(match[1])?.value ||
|
||||
this.options.defaultLanguage,
|
||||
})
|
||||
}),
|
||||
textblockTypeInputRule({
|
||||
find: tildeInputRegex,
|
||||
type: this.type,
|
||||
getAttributes: (match) => ({
|
||||
language:
|
||||
getLanguageByValueOrAlias(match[1])?.value ||
|
||||
this.options.defaultLanguage,
|
||||
})
|
||||
}),
|
||||
];
|
||||
},
|
||||
|
||||
addNodeView() {
|
||||
return (e) => {
|
||||
const container = document.createElement('div')
|
||||
container.classList.add("aie-codeblock-wrapper")
|
||||
const {language} = e.node.attrs;
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="aie-codeblock-tools" contenteditable="false">
|
||||
<div class="aie-codeblock-tools-lang" contenteditable="false"><span>${language || this.options.defaultLanguage}</span><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 16L6 10H18L12 16Z"></path></svg></div>
|
||||
</div>
|
||||
<pre class="hljs"><code></code></pre>
|
||||
`
|
||||
|
||||
const createEL = ()=>{
|
||||
const div = document.createElement("div") as HTMLDivElement;
|
||||
div.classList.add("aie-codeblock-langs")
|
||||
div.innerHTML = `
|
||||
${this.options.languages.map((lang) => {
|
||||
return `<div class="aie-codeblock-langs-item" data-item="${lang.value}">${lang.name}</div>`
|
||||
}).join("")}`
|
||||
|
||||
div.addEventListener("click",(event)=>{
|
||||
const target: HTMLDivElement = (event.target as HTMLElement).closest('.aie-codeblock-langs-item')!;
|
||||
if (target) {
|
||||
const language = target.getAttribute("data-item")!;
|
||||
e.editor.chain().setCodeBlock({language}).run();
|
||||
}
|
||||
});
|
||||
|
||||
return div;
|
||||
}
|
||||
|
||||
|
||||
const instance = tippy(container.querySelector(".aie-codeblock-tools-lang")!, {
|
||||
content: createEL(),
|
||||
appendTo: e.editor.view.dom.closest(".aie-container")!,
|
||||
placement: 'bottom-end',
|
||||
trigger: 'click',
|
||||
interactive: true,
|
||||
arrow: false,
|
||||
aria: {
|
||||
content: null,
|
||||
expanded: false,
|
||||
},
|
||||
});
|
||||
|
||||
console.log("addNodeView init")
|
||||
|
||||
|
||||
return {
|
||||
dom: container,
|
||||
contentDOM: container.querySelector("code")!,
|
||||
destroy: () => {
|
||||
console.log("addNodeView destroy")
|
||||
instance.destroy();
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
|
||||
})
|
|
@ -0,0 +1,74 @@
|
|||
import '@tiptap/extension-text-style'
|
||||
|
||||
import { Extension } from '@tiptap/core'
|
||||
|
||||
export type FontSizeOptions = {
|
||||
types: string[],
|
||||
}
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
fontSize: {
|
||||
/**
|
||||
* Set the font family
|
||||
*/
|
||||
setFontSize: (fontFamily: string) => ReturnType,
|
||||
/**
|
||||
* Unset the font family
|
||||
*/
|
||||
unsetFontSize: () => ReturnType,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const FontSizeExt = Extension.create<FontSizeOptions>({
|
||||
name: 'fontSize',
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
types: ['textStyle'],
|
||||
}
|
||||
},
|
||||
|
||||
onBeforeCreate(){
|
||||
console.log("onBeforeCreate")
|
||||
},
|
||||
|
||||
addGlobalAttributes() {
|
||||
return [
|
||||
{
|
||||
types: this.options.types,
|
||||
attributes: {
|
||||
fontSize: {
|
||||
default: null,
|
||||
parseHTML: element => element.style.fontSize?.replace(/['"]+/g, ''),
|
||||
renderHTML: attributes => {
|
||||
if (!attributes.fontSize) {
|
||||
return {}
|
||||
}
|
||||
return {
|
||||
style: `font-size: ${attributes.fontSize}`,
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
return {
|
||||
setFontSize: fontSize => ({ chain }) => {
|
||||
return chain()
|
||||
.setMark('textStyle', { fontSize })
|
||||
.run()
|
||||
},
|
||||
unsetFontSize: () => ({ chain }) => {
|
||||
return chain()
|
||||
.setMark('textStyle', { fontSize: null })
|
||||
.removeEmptyTextStyle()
|
||||
.run()
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
|
@ -0,0 +1,75 @@
|
|||
import { Node } from '@tiptap/core'
|
||||
|
||||
export interface IframeOptions {
|
||||
allowFullscreen: boolean,
|
||||
HTMLAttributes: {
|
||||
[key: string]: any
|
||||
},
|
||||
}
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
iframe: {
|
||||
setIframe: (options: { src: string }) => ReturnType,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const IFrameExt = Node.create<IframeOptions>({
|
||||
name: 'iframe',
|
||||
|
||||
group: 'block',
|
||||
|
||||
atom: true,
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
allowFullscreen: true,
|
||||
HTMLAttributes: {
|
||||
class: 'iframe-wrapper',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
addAttributes() {
|
||||
return {
|
||||
src: {
|
||||
default: null,
|
||||
},
|
||||
width:{
|
||||
default:"100%",
|
||||
},
|
||||
frameborder: {
|
||||
default: 0,
|
||||
},
|
||||
allowfullscreen: {
|
||||
default: this.options.allowFullscreen,
|
||||
parseHTML: () => this.options.allowFullscreen,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
return [{
|
||||
tag: 'iframe',
|
||||
}]
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ['div', this.options.HTMLAttributes, ['iframe', HTMLAttributes]]
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
return {
|
||||
setIframe: (options: { src: string }) => ({ tr, dispatch }) => {
|
||||
const { selection } = tr
|
||||
const node = this.type.create(options)
|
||||
|
||||
if (dispatch) {
|
||||
tr.replaceRangeWith(selection.from, selection.to, node)
|
||||
}
|
||||
return true
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
|
@ -0,0 +1,231 @@
|
|||
import {
|
||||
Image
|
||||
} from '@tiptap/extension-image';
|
||||
import {mergeAttributes} from "@tiptap/core";
|
||||
import {resize} from "../util/resize";
|
||||
import {Plugin, PluginKey} from "@tiptap/pm/state";
|
||||
import {DecorationSet} from "prosemirror-view";
|
||||
import {TextSelection} from "prosemirror-state";
|
||||
import {uuid} from "../util/uuid.ts";
|
||||
import {uploadFile} from "../util/uploadFile.ts";
|
||||
import {createMediaDecoration} from "../util/decorations.ts";
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
Image: {
|
||||
uploadImage: (file: File) => ReturnType;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export interface ImageOptions {
|
||||
inline: boolean,
|
||||
allowBase64: boolean,
|
||||
HTMLAttributes: Record<string, any>,
|
||||
uploadUrl?: string,
|
||||
uploadHeaders: Record<string, any>,
|
||||
uploader?: (file: File, uploadUrl: string, headers: Record<string, any>, formName: string) => Promise<Record<string, any>>,
|
||||
}
|
||||
|
||||
export type ImageAction = {
|
||||
type: "add" | "remove";
|
||||
id: string;
|
||||
pos: number;
|
||||
}
|
||||
|
||||
const key = new PluginKey("aie-image-plugin");
|
||||
const actionKey = "image_action";
|
||||
export const ImageExt = Image.extend<ImageOptions>({
|
||||
|
||||
name: "image",
|
||||
draggable: true,
|
||||
selectable: true,
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
...this.parent?.(),
|
||||
uploadUrl: "",
|
||||
uploadHeaders: {},
|
||||
uploader: void 0,
|
||||
}
|
||||
},
|
||||
|
||||
allowGapCursor() {
|
||||
return !this.options.inline;
|
||||
},
|
||||
|
||||
addAttributes() {
|
||||
return {
|
||||
src: {
|
||||
default: '',
|
||||
parseHTML: (element) => `${element.getAttribute('src') ?? ''}`,
|
||||
},
|
||||
alt: {
|
||||
default: '',
|
||||
},
|
||||
title: {
|
||||
default: '',
|
||||
},
|
||||
width: {
|
||||
default: 350,
|
||||
},
|
||||
height: {
|
||||
default: 'auto',
|
||||
},
|
||||
align: {
|
||||
default: 'left',
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: this.options.allowBase64
|
||||
? 'img[src]'
|
||||
: 'img[src]:not([src^="data:"])',
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
renderHTML({HTMLAttributes}) {
|
||||
return [
|
||||
'img',
|
||||
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
|
||||
];
|
||||
},
|
||||
|
||||
|
||||
addCommands() {
|
||||
return {
|
||||
...this.parent?.(),
|
||||
|
||||
uploadImage: (file: File) => () => {
|
||||
const id = uuid();
|
||||
const {state: {tr}, view, schema} = this.editor!
|
||||
if (!tr.selection.empty) tr.deleteSelection();
|
||||
|
||||
view.dispatch(tr.setMeta(actionKey, {
|
||||
type: "add",
|
||||
id,
|
||||
pos: tr.selection.from,
|
||||
}));
|
||||
|
||||
if (this.options.uploadUrl) {
|
||||
const uploader = this.options.uploader || uploadFile;
|
||||
uploader(file, this.options.uploadUrl, this.options.uploadHeaders, "image")
|
||||
.then(json => {
|
||||
if (json.errorCode === 0 && json.data && json.data.src) {
|
||||
const decorations = key.getState(this.editor.state) as DecorationSet;
|
||||
let found = decorations.find(void 0, void 0, spec => spec.id == id)
|
||||
view.dispatch(view.state.tr
|
||||
.insert(found[0].from, schema.nodes.image.create({
|
||||
src: json.data.src,
|
||||
alt: json.data.alt,
|
||||
}))
|
||||
.setMeta(actionKey, {type: "remove", id}));
|
||||
} else {
|
||||
view.dispatch(tr.setMeta(actionKey, {type: "remove", id}));
|
||||
}
|
||||
}).catch(() => {
|
||||
view.dispatch(tr.setMeta(actionKey, {type: "remove", id}));
|
||||
})
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
addNodeView() {
|
||||
return (e) => {
|
||||
const container = document.createElement('div')
|
||||
const {src, width, height, align} = e.node.attrs;
|
||||
container.classList.add(`align-${align}`)
|
||||
container.innerHTML = `
|
||||
<div class="aie-resize-wrapper">
|
||||
<div class="aie-resize">
|
||||
<div class="aie-resize-btn-top-left" data-position="left" draggable="true"></div>
|
||||
<div class="aie-resize-btn-top-right" data-position="right" draggable="true"></div>
|
||||
<div class="aie-resize-btn-bottom-left" data-position="left" draggable="true"></div>
|
||||
<div class="aie-resize-btn-bottom-right" data-position="right" draggable="true"></div>
|
||||
</div>
|
||||
<img src="${src}" style="width: ${width}px; height: ${height}" class="align-${align} resize-obj">
|
||||
</div>
|
||||
`
|
||||
resize(container, e.editor.view.dom, (attrs) => e.editor.commands.updateAttributes("image", attrs));
|
||||
|
||||
return {
|
||||
dom: container,
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
addProseMirrorPlugins() {
|
||||
const editor = this.editor;
|
||||
return [
|
||||
new Plugin({
|
||||
key: key,
|
||||
state: {
|
||||
init: () => DecorationSet.empty,
|
||||
apply: (tr, set) => {
|
||||
|
||||
const action = tr.getMeta(actionKey) as ImageAction;
|
||||
|
||||
// update decorations position
|
||||
set = set.map(tr.mapping, tr.doc);
|
||||
|
||||
// add decoration
|
||||
if (action && action.type === "add") {
|
||||
set = set.add(tr.doc, [createMediaDecoration(action)]);
|
||||
}
|
||||
// remove decoration
|
||||
else if (action && action.type === "remove") {
|
||||
set = set.remove(set.find(undefined, undefined,
|
||||
spec => spec.id == action.id));
|
||||
}
|
||||
return set;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
props: {
|
||||
decorations(state) {
|
||||
return this.getState(state);
|
||||
},
|
||||
|
||||
handleDOMEvents: {
|
||||
drop(view, event) {
|
||||
const hasFiles = event.dataTransfer &&
|
||||
event.dataTransfer.files &&
|
||||
event.dataTransfer.files.length
|
||||
|
||||
if (!hasFiles) return false
|
||||
|
||||
const images = Array
|
||||
.from(event.dataTransfer.files)
|
||||
.filter(file => (/image/i).test(file.type))
|
||||
|
||||
if (images.length === 0) return false
|
||||
|
||||
event.preventDefault()
|
||||
|
||||
const {state: {tr, doc}, dispatch} = view
|
||||
const coordinates = view.posAtCoords({left: event.clientX, top: event.clientY})
|
||||
dispatch(tr.setSelection(TextSelection.create(doc, coordinates!.pos)).scrollIntoView())
|
||||
|
||||
images.forEach(image => {
|
||||
editor.commands.uploadImage(image);
|
||||
})
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
]
|
||||
},
|
||||
|
||||
}
|
||||
);
|
|
@ -0,0 +1,126 @@
|
|||
import { Command, Extension } from '@tiptap/core';
|
||||
import { AllSelection, TextSelection, Transaction } from 'prosemirror-state';
|
||||
|
||||
export interface IndentOptions {
|
||||
types: string[];
|
||||
minLevel: number;
|
||||
maxLevel: number;
|
||||
}
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
indent: {
|
||||
indent: () => ReturnType;
|
||||
outdent: () => ReturnType;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const IndentExt = Extension.create<IndentOptions>({
|
||||
name: 'indent',
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
types: ['listItem', 'paragraph'],
|
||||
minLevel: 0,
|
||||
maxLevel: 8,
|
||||
}
|
||||
},
|
||||
|
||||
addGlobalAttributes() {
|
||||
return [
|
||||
{
|
||||
types: this.options.types,
|
||||
attributes: {
|
||||
indent: {
|
||||
default:0,
|
||||
parseHTML: element => {
|
||||
const level = Number(element.getAttribute('data-indent'));
|
||||
return level && level > this.options.minLevel ? level : null;
|
||||
},
|
||||
|
||||
renderHTML: attributes => {
|
||||
// return attributes?.indent > this.options.minLevel ? { 'data-indent': attributes.indent } : null;
|
||||
if (!attributes.indent){
|
||||
return {}
|
||||
}
|
||||
|
||||
return {
|
||||
style: `text-indent: ${attributes?.indent * 10}px`,
|
||||
"data-indent":attributes?.indent,
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
const setNodeIndentMarkup = (tr: Transaction, pos: number, delta: number): Transaction => {
|
||||
const node = tr?.doc?.nodeAt(pos);
|
||||
|
||||
if (node) {
|
||||
const nextLevel = (node.attrs.indent || 0) + delta;
|
||||
const { minLevel, maxLevel } = this.options;
|
||||
const indent = nextLevel < minLevel ? minLevel : nextLevel > maxLevel ? maxLevel : nextLevel;
|
||||
|
||||
if (indent !== node.attrs.indent) {
|
||||
const { indent: oldIndent, ...currentAttrs } = node.attrs;
|
||||
const nodeAttrs = indent > minLevel ? { ...currentAttrs, indent } : currentAttrs;
|
||||
return tr.setNodeMarkup(pos, node.type, nodeAttrs, node.marks);
|
||||
}
|
||||
}
|
||||
return tr;
|
||||
};
|
||||
|
||||
const updateIndentLevel = (tr: Transaction, delta: number): Transaction => {
|
||||
const { doc, selection } = tr;
|
||||
|
||||
if (doc && selection && (selection instanceof TextSelection || selection instanceof AllSelection)) {
|
||||
const { from, to } = selection;
|
||||
doc.nodesBetween(from, to, (node, pos) => {
|
||||
if (this.options.types.includes(node.type.name)) {
|
||||
tr = setNodeIndentMarkup(tr, pos, delta);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
return tr;
|
||||
};
|
||||
const applyIndent: (direction: number) => () => Command =
|
||||
direction =>
|
||||
() =>
|
||||
({ tr, state, dispatch }) => {
|
||||
const { selection } = state;
|
||||
tr = tr.setSelection(selection);
|
||||
tr = updateIndentLevel(tr, direction);
|
||||
|
||||
if (tr.docChanged) {
|
||||
dispatch?.(tr);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
return {
|
||||
indent: applyIndent(1),
|
||||
outdent: applyIndent(-1),
|
||||
};
|
||||
},
|
||||
|
||||
addKeyboardShortcuts() {
|
||||
return {
|
||||
Tab: () => {
|
||||
return this.editor.commands.indent();
|
||||
},
|
||||
'Shift-Tab': () => {
|
||||
return this.editor.commands.outdent();
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
|
@ -0,0 +1,80 @@
|
|||
import {Extension} from "@tiptap/core";
|
||||
|
||||
export interface LineHeightOptions {
|
||||
types: string[];
|
||||
heights: string[];
|
||||
defaultHeight: string;
|
||||
}
|
||||
|
||||
declare module "@tiptap/core" {
|
||||
interface Commands<ReturnType> {
|
||||
lineHeight: {
|
||||
/**
|
||||
* Set the line height attribute
|
||||
*/
|
||||
setLineHeight: (height: string) => ReturnType;
|
||||
/**
|
||||
* Unset the text align attribute
|
||||
*/
|
||||
unsetLineHeight: () => ReturnType;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const LineHeightExt = Extension.create<LineHeightOptions>({
|
||||
name: "lineHeight",
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
types: ["heading", "paragraph"],
|
||||
heights: ["100%", "125%", "150%", "200%", "250%", "300%"],
|
||||
defaultHeight: "100%",
|
||||
};
|
||||
},
|
||||
|
||||
addGlobalAttributes() {
|
||||
return [
|
||||
{
|
||||
types: this.options.types,
|
||||
attributes: {
|
||||
lineHeight: {
|
||||
default: this.options.defaultHeight,
|
||||
parseHTML: (element) =>
|
||||
element.style.lineHeight || this.options.defaultHeight,
|
||||
renderHTML: (attributes) => {
|
||||
if (attributes.lineHeight === this.options.defaultHeight) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {style: `line-height: ${attributes.lineHeight}`};
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
return {
|
||||
setLineHeight:
|
||||
(height: string) =>
|
||||
({commands}) => {
|
||||
if (!this.options.heights.includes(height)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.options.types.every((type) =>
|
||||
commands.updateAttributes(type, {lineHeight: height})
|
||||
);
|
||||
},
|
||||
|
||||
unsetLineHeight:
|
||||
() =>
|
||||
({commands}) => {
|
||||
return this.options.types.every((type) =>
|
||||
commands.resetAttributes(type, "lineHeight")
|
||||
);
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
|
@ -0,0 +1,104 @@
|
|||
import {Mention} from "@tiptap/extension-mention";
|
||||
import tippy, {Instance} from "tippy.js";
|
||||
import {SuggestionProps} from "@tiptap/suggestion";
|
||||
|
||||
export const createMention = (onMentionLoad: (query: string) => any[] | Promise<any[]>) => {
|
||||
return Mention.configure({
|
||||
HTMLAttributes: {
|
||||
class: 'mention',
|
||||
},
|
||||
suggestion: {
|
||||
items: ({query}) => {
|
||||
return onMentionLoad(query)
|
||||
},
|
||||
|
||||
render: () => {
|
||||
let element: HTMLDivElement;
|
||||
let popup: Instance[];
|
||||
let selectIndex: number = 0;
|
||||
let suggestionProps: SuggestionProps;
|
||||
|
||||
return {
|
||||
onStart: (props: SuggestionProps) => {
|
||||
|
||||
element = document.createElement("div") as HTMLDivElement;
|
||||
element.style.width = "200px"
|
||||
element.style.height = "200px"
|
||||
element.style.border = "solid 1px #ccc"
|
||||
element.style.background = "antiquewhite"
|
||||
|
||||
element.innerHTML = `
|
||||
<div className="items">
|
||||
${props.items.map((item: any) => {
|
||||
return `<button>${item}</button>`
|
||||
}).join("")}
|
||||
</div>
|
||||
`
|
||||
|
||||
console.log("onStart: ", props)
|
||||
if (!props.clientRect) {
|
||||
return
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
popup = tippy('body', {
|
||||
getReferenceClientRect: props.clientRect,
|
||||
appendTo: () => document.body,
|
||||
content: element,
|
||||
showOnCreate: true,
|
||||
interactive: true,
|
||||
allowHTML: true,
|
||||
trigger: 'manual',
|
||||
placement: 'bottom-start',
|
||||
})
|
||||
|
||||
suggestionProps = props;
|
||||
},
|
||||
|
||||
onUpdate(props) {
|
||||
suggestionProps = props;
|
||||
|
||||
element.innerHTML = `
|
||||
<div className="items">
|
||||
${props.items.map((item: any) => {
|
||||
return `<button>${item}</button>`
|
||||
}).join("")}
|
||||
</div>
|
||||
`
|
||||
|
||||
if (!props.clientRect) {
|
||||
return
|
||||
}
|
||||
|
||||
popup[0].setProps({
|
||||
getReferenceClientRect: props.clientRect as any,
|
||||
})
|
||||
},
|
||||
|
||||
onKeyDown(props) {
|
||||
if (props.event.key === 'Escape') {
|
||||
popup[0].hide();
|
||||
return true;
|
||||
} else if (props.event.key === "ArrowUp") {
|
||||
selectIndex = (selectIndex + suggestionProps.items.length - 1) % suggestionProps.items.length
|
||||
return true;
|
||||
} else if (props.event.key === "ArrowDown") {
|
||||
selectIndex = (selectIndex + 1) % suggestionProps.items.length
|
||||
return true;
|
||||
} else if (props.event.key === "Enter") {
|
||||
const item = suggestionProps.items[selectIndex];
|
||||
if (item) suggestionProps.command({id: item})
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
onExit() {
|
||||
popup[0].destroy()
|
||||
element.remove();
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
import {Plugin} from 'prosemirror-state'
|
||||
import {Extension} from "@tiptap/core";
|
||||
import {PluginKey} from "@tiptap/pm/state";
|
||||
import {Mark} from "prosemirror-model";
|
||||
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
painter: {
|
||||
setPainter: (marks: Mark[]) => ReturnType,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export type PainterAction = {
|
||||
type: "start" | "end";
|
||||
marks: Mark[];
|
||||
}
|
||||
|
||||
export const PainterExt = Extension.create({
|
||||
name: 'painter',
|
||||
|
||||
|
||||
addCommands() {
|
||||
return {
|
||||
setPainter: (marks: Mark[]) => ({view: {dispatch, state: {tr}, dom}}) => {
|
||||
dom.style.cursor = "context-menu"
|
||||
dispatch(tr.setMeta("painterAction", {type: "start", marks}))
|
||||
return true;
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
addProseMirrorPlugins() {
|
||||
return [
|
||||
new Plugin({
|
||||
key: new PluginKey("aie-painter"),
|
||||
state: {
|
||||
init: () => [] as Mark[],
|
||||
apply: (tr, set) => {
|
||||
const action = tr.getMeta("painterAction") as PainterAction;
|
||||
if (action && action.type === "start") {
|
||||
set = action.marks
|
||||
} else if (action && action.type === "end") {
|
||||
set = [];
|
||||
}
|
||||
return set;
|
||||
}
|
||||
},
|
||||
props: {
|
||||
handleDOMEvents: {
|
||||
mousedown(view, _) {
|
||||
const marks = this.getState(view.state) as Mark[];
|
||||
if (!marks || marks.length == 0) {
|
||||
return false;
|
||||
}
|
||||
const mouseup = () => {
|
||||
document.removeEventListener("mouseup", mouseup);
|
||||
|
||||
let {dispatch, state: {tr, selection}, dom} = view;
|
||||
dom.style.cursor = ""
|
||||
|
||||
tr = tr.removeMark(selection.from, selection.to);
|
||||
for (let mark of marks) {
|
||||
if (mark.type.name != "link") {
|
||||
tr = tr.addMark(selection.from, selection.to, mark);
|
||||
}
|
||||
}
|
||||
|
||||
dispatch(tr.setMeta("painterAction", {type: "end"}))
|
||||
}
|
||||
|
||||
document.addEventListener("mouseup", mouseup)
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
]
|
||||
},
|
||||
|
||||
|
||||
})
|
|
@ -0,0 +1,229 @@
|
|||
import {Node, nodeInputRule} from '@tiptap/core'
|
||||
import {Plugin, PluginKey, TextSelection} from 'prosemirror-state'
|
||||
import {resize} from "../util/resize";
|
||||
import {uuid} from "../util/uuid.ts";
|
||||
import {uploadFile} from "../util/uploadFile.ts";
|
||||
import {DecorationSet} from "prosemirror-view";
|
||||
import {createMediaDecoration} from "../util/decorations.ts";
|
||||
|
||||
export interface VideoOptions {
|
||||
HTMLAttributes: Record<string, any>,
|
||||
uploadUrl?: string,
|
||||
uploadHeaders: Record<string, any>,
|
||||
uploader?: (file: File, uploadUrl: string, headers: Record<string, any>, formName: string) => Promise<Record<string, any>>,
|
||||
}
|
||||
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
video: {
|
||||
setVideo: (src: string) => ReturnType,
|
||||
toggleVideo: (src: string) => ReturnType,
|
||||
uploadVideo: (file: File) => ReturnType,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const VIDEO_INPUT_REGEX = /!\[(.+|:?)]\((\S+)(?:(?:\s+)["'](\S+)["'])?\)/
|
||||
|
||||
export type VideoAction = {
|
||||
type: "add" | "remove";
|
||||
id: string;
|
||||
pos: number;
|
||||
}
|
||||
|
||||
const key = new PluginKey("aie-video-plugin");
|
||||
const actionKey = "video_action";
|
||||
|
||||
export const VideoExt = Node.create<VideoOptions>({
|
||||
name: 'video',
|
||||
group: "block",
|
||||
|
||||
addAttributes() {
|
||||
return {
|
||||
src: {
|
||||
default: null,
|
||||
parseHTML: (el) => el.getAttribute('src'),
|
||||
renderHTML: (attrs) => ({src: attrs.src}),
|
||||
},
|
||||
poster: {
|
||||
default: null,
|
||||
parseHTML: (el) => el.getAttribute('poster'),
|
||||
renderHTML: (attrs) => ({poster: attrs.poster}),
|
||||
},
|
||||
width: {
|
||||
default: 350,
|
||||
},
|
||||
controls: {
|
||||
default: true,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: 'video',
|
||||
getAttrs: el => ({
|
||||
src: (el as HTMLVideoElement).getAttribute('src'),
|
||||
poster: (el as HTMLVideoElement).getAttribute('poster'),
|
||||
}),
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
renderHTML({HTMLAttributes}) {
|
||||
return [
|
||||
'video',
|
||||
{controls: 'true', ...HTMLAttributes, src: null},
|
||||
['source', {src: HTMLAttributes.src}]
|
||||
]
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
return {
|
||||
setVideo: (src: string) => ({commands}) => commands.insertContent(`<video controls="true" style="width: 100%" src="${src}" />`),
|
||||
toggleVideo: () => ({commands}) => commands.toggleNode(this.name, 'paragraph'),
|
||||
uploadVideo: (file: File) => () => {
|
||||
const id = uuid();
|
||||
const {state: {tr}, view, schema} = this.editor!
|
||||
if (!tr.selection.empty) tr.deleteSelection();
|
||||
|
||||
view.dispatch(tr.setMeta(actionKey, {
|
||||
type: "add",
|
||||
id,
|
||||
pos: tr.selection.from,
|
||||
}));
|
||||
|
||||
if (this.options.uploadUrl) {
|
||||
const uploader = this.options.uploader || uploadFile;
|
||||
uploader(file, this.options.uploadUrl, this.options.uploadHeaders, "video")
|
||||
.then(json => {
|
||||
if (json.errorCode === 0 && json.data && json.data.src) {
|
||||
const decorations = key.getState(this.editor.state) as DecorationSet;
|
||||
let found = decorations.find(void 0, void 0, spec => spec.id == id)
|
||||
view.dispatch(view.state.tr
|
||||
.insert(found[0].from, schema.nodes.video.create({
|
||||
src: json.data.src,
|
||||
poster: json.data.poster,
|
||||
}))
|
||||
.setMeta(actionKey, {type: "remove", id}));
|
||||
} else {
|
||||
view.dispatch(tr.setMeta(actionKey, {type: "remove", id}));
|
||||
}
|
||||
}).catch(() => {
|
||||
view.dispatch(tr.setMeta(actionKey, {type: "remove", id}));
|
||||
})
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
addNodeView() {
|
||||
return (e) => {
|
||||
const container = document.createElement('div')
|
||||
const {src, width, align} = e.node.attrs;
|
||||
container.classList.add(`align-${align}`)
|
||||
container.innerHTML = `
|
||||
<div class="aie-resize-wrapper">
|
||||
<div class="aie-resize">
|
||||
<div class="aie-resize-btn-top-left" data-position="left" draggable="true"></div>
|
||||
<div class="aie-resize-btn-top-right" data-position="right" draggable="true"></div>
|
||||
<div class="aie-resize-btn-bottom-left" data-position="left" draggable="true"></div>
|
||||
<div class="aie-resize-btn-bottom-right" data-position="right" draggable="true"></div>
|
||||
</div>
|
||||
<video controls="true" width="${width}" class="resize-obj">
|
||||
<source src="${src}">
|
||||
</video>
|
||||
</div>
|
||||
`
|
||||
resize(container, e.editor.view.dom, (attrs) => e.editor.commands.updateAttributes("video", attrs));
|
||||
return {
|
||||
dom: container,
|
||||
// contentDOM: container.firstChild,
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
addInputRules() {
|
||||
return [
|
||||
nodeInputRule({
|
||||
find: VIDEO_INPUT_REGEX,
|
||||
type: this.type,
|
||||
getAttributes: (match) => {
|
||||
const [, , src] = match
|
||||
|
||||
return {src}
|
||||
},
|
||||
})
|
||||
]
|
||||
},
|
||||
|
||||
|
||||
addProseMirrorPlugins() {
|
||||
const editor = this.editor;
|
||||
return [
|
||||
new Plugin({
|
||||
key: key,
|
||||
state: {
|
||||
init: () => DecorationSet.empty,
|
||||
apply: (tr, set) => {
|
||||
|
||||
const action = tr.getMeta(actionKey) as VideoAction;
|
||||
|
||||
// update decorations position
|
||||
set = set.map(tr.mapping, tr.doc);
|
||||
|
||||
// add decoration
|
||||
if (action && action.type === "add") {
|
||||
set = set.add(tr.doc, [createMediaDecoration(action)]);
|
||||
}
|
||||
// remove decoration
|
||||
else if (action && action.type === "remove") {
|
||||
set = set.remove(set.find(undefined, undefined,
|
||||
spec => spec.id == action.id));
|
||||
}
|
||||
return set;
|
||||
}
|
||||
},
|
||||
|
||||
props: {
|
||||
decorations(state) {
|
||||
return this.getState(state);
|
||||
},
|
||||
handleDOMEvents: {
|
||||
drop(view, event) {
|
||||
const hasFiles = event.dataTransfer &&
|
||||
event.dataTransfer.files &&
|
||||
event.dataTransfer.files.length
|
||||
|
||||
if (!hasFiles) return false
|
||||
|
||||
const videos = Array
|
||||
.from(event.dataTransfer.files)
|
||||
.filter(file => (/video/i).test(file.type))
|
||||
|
||||
if (videos.length === 0) return false
|
||||
|
||||
event.preventDefault()
|
||||
|
||||
const {state: {tr, doc}, dispatch} = view
|
||||
const coordinates = view.posAtCoords({left: event.clientX, top: event.clientY})
|
||||
dispatch(tr.setSelection(TextSelection.create(doc, coordinates!.pos)).scrollIntoView())
|
||||
|
||||
videos.forEach(video => {
|
||||
editor.commands.uploadVideo(video);
|
||||
})
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
]
|
||||
},
|
||||
|
||||
|
||||
})
|
|
@ -0,0 +1,92 @@
|
|||
// import tippy from 'tippy.js'
|
||||
// import {SuggestionProps} from "@tiptap/suggestion/dist/packages/suggestion/src/suggestion";
|
||||
//
|
||||
//
|
||||
// export default {
|
||||
// items: ({ query }) => {
|
||||
// console.log("query: ",query)
|
||||
// return [
|
||||
// 'Lea Thompson',
|
||||
// 'Cyndi Lauper',
|
||||
// 'Tom Cruise',
|
||||
// 'Madonna',
|
||||
// 'Jerry Hall',
|
||||
// 'Joan Collins',
|
||||
// ].filter(item => item.toLowerCase().startsWith(query.toLowerCase()))
|
||||
// .slice(0, 5)
|
||||
// },
|
||||
//
|
||||
// render: () => {
|
||||
// let component,popup;
|
||||
//
|
||||
// return {
|
||||
// onStart: (props:SuggestionProps) => {
|
||||
// // component = new ReactRenderer(MentionList, {
|
||||
// // props,
|
||||
// // editor: props.editor,
|
||||
// // })
|
||||
//
|
||||
// component = document.createElement("div") as HTMLDivElement;
|
||||
// component.style.width = "200px"
|
||||
// component.style.height = "200px"
|
||||
// component.style.bottom = "solid 1px #ccc"
|
||||
// component.style.background = "antiquewhite"
|
||||
// component.innerHTML = `
|
||||
// <div className="items">
|
||||
// ${props.items.forEach((item:any) => {
|
||||
// `<button>${item}</button>`
|
||||
// }).join("")}
|
||||
// </div>
|
||||
// `
|
||||
//
|
||||
// // Document.
|
||||
// console.log("onStart: ",props)
|
||||
// if (!props.clientRect) {
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// popup = tippy('body', {
|
||||
// getReferenceClientRect: props.clientRect,
|
||||
// appendTo: () => document.body,
|
||||
// content: `<div style="width: 200px;height: 200px;border: solid 1px #ccc;background: antiquewhite"></div>`,
|
||||
// showOnCreate: true,
|
||||
// interactive: true,
|
||||
// allowHTML:true,
|
||||
// trigger: 'manual',
|
||||
// placement: 'bottom-start',
|
||||
// })
|
||||
// },
|
||||
//
|
||||
// onUpdate(props) {
|
||||
// console.log("onUpdate: ",props)
|
||||
// // component.updateProps(props)
|
||||
// //
|
||||
// // if (!props.clientRect) {
|
||||
// // return
|
||||
// // }
|
||||
// //
|
||||
// // popup[0].setProps({
|
||||
// // getReferenceClientRect: props.clientRect,
|
||||
// // })
|
||||
// },
|
||||
//
|
||||
// onKeyDown(props) {
|
||||
// console.log("onKeyDown: ",props)
|
||||
// return true;
|
||||
// // if (props.event.key === 'Escape') {
|
||||
// // popup[0].hide()
|
||||
// //
|
||||
// // return true
|
||||
// // }
|
||||
// //
|
||||
// // return component.ref?.onKeyDown(props)
|
||||
// },
|
||||
//
|
||||
// onExit() {
|
||||
// console.log("onExit: ")
|
||||
// popup[0].destroy()
|
||||
// // component.destroy()
|
||||
// },
|
||||
// }
|
||||
// },
|
||||
// }
|
|
@ -0,0 +1,2 @@
|
|||
export * from './styles';
|
||||
export * from './core/AiEditor.ts';
|
|
@ -0,0 +1,184 @@
|
|||
|
||||
.aie-content {
|
||||
overflow: auto;
|
||||
padding: 10px;
|
||||
margin: 0;
|
||||
color: var(--aie-content-color);
|
||||
|
||||
& > :first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
ul, ol {
|
||||
display: block;
|
||||
margin-block-start: 1em;
|
||||
margin-block-end: 1em;
|
||||
margin-inline-start: 0px;
|
||||
margin-inline-end: 0px;
|
||||
padding-inline-start: 20px;
|
||||
}
|
||||
|
||||
li {
|
||||
p {
|
||||
padding: 0;
|
||||
margin: 5px 0;
|
||||
}
|
||||
}
|
||||
|
||||
ul[data-type="taskList"] {
|
||||
display: block;
|
||||
margin-block-start: 1em;
|
||||
margin-block-end: 1em;
|
||||
margin-inline-start: 0px;
|
||||
margin-inline-end: 0px;
|
||||
padding-inline-start: 0px;
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
p {
|
||||
padding-left: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 16px 0;
|
||||
padding: 4px 0 4px 8px;
|
||||
border-left: 2px solid var(--aie-blockquote-border-color);
|
||||
background: var(--aie-blockquote-bg-color);
|
||||
|
||||
p {
|
||||
color: var(--aie-blockquote-text-color);
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
border: 1px solid #efefef;
|
||||
}
|
||||
|
||||
code {
|
||||
border-radius: 3px;
|
||||
padding: 3px 6px;
|
||||
background: var(--aie-pre-bg-color);
|
||||
}
|
||||
|
||||
|
||||
pre {
|
||||
margin: 0;
|
||||
padding: 0 10px 10px 10px;
|
||||
border-radius: 5px;
|
||||
background: var(--aie-pre-bg-color);
|
||||
overflow: auto;
|
||||
|
||||
code {
|
||||
background: var(--aie-pre-bg-color);
|
||||
}
|
||||
}
|
||||
|
||||
h1 {
|
||||
padding-top: 24px;
|
||||
letter-spacing: -.02em;
|
||||
line-height: 40px;
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
padding-top: 24px;
|
||||
letter-spacing: -.02em;
|
||||
line-height: 32px;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 32px 0 0;
|
||||
letter-spacing: -.01em;
|
||||
line-height: 28px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
table-layout: fixed;
|
||||
width: 100%;
|
||||
|
||||
td,
|
||||
th {
|
||||
border: 2px solid #ced4da;
|
||||
box-sizing: border-box;
|
||||
min-width: 1em;
|
||||
padding: 3px 5px;
|
||||
position: relative;
|
||||
vertical-align: top;
|
||||
|
||||
> * {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: #f1f3f5;
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.selectedCell:after {
|
||||
background: rgba(200, 200, 255, 0.4);
|
||||
content: "";
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.column-resize-handle {
|
||||
background-color: #adf;
|
||||
bottom: -2px;
|
||||
position: absolute;
|
||||
right: -2px;
|
||||
pointer-events: none;
|
||||
top: 0;
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.align-left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.align-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.align-center {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.tableWrapper {
|
||||
padding: 1rem 0;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.resize-cursor {
|
||||
cursor: ew-resize;
|
||||
cursor: col-resize;
|
||||
}
|
|
@ -0,0 +1,475 @@
|
|||
|
||||
.aie-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
border: 1px solid;
|
||||
border-color: var(--aie-container-border);
|
||||
|
||||
aie-menus {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
color: var(--aie-menus-text-color);
|
||||
background: var(--aie-menus-bg-color);
|
||||
z-index: 1;
|
||||
|
||||
& > div {
|
||||
border-bottom: 1px solid var(--aie-container-border);
|
||||
}
|
||||
|
||||
svg {
|
||||
fill: var(--aie-menus-svg-color);
|
||||
}
|
||||
|
||||
.menu-ai {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
background: var(--aie-menus-ai-bg-color);
|
||||
color: var(--aie-menus-ai-color);
|
||||
border-radius: 3px;
|
||||
width: 30px;
|
||||
padding: 0 1px 0 5px;
|
||||
|
||||
svg {
|
||||
fill: var(--aie-menus-ai-color);
|
||||
}
|
||||
}
|
||||
|
||||
.aie-menu-divider {
|
||||
background: var(--aie-menus-divider-color);
|
||||
width: 1px;
|
||||
height: 16px;
|
||||
margin: auto
|
||||
}
|
||||
|
||||
.aie-menu-item {
|
||||
height: 25px;
|
||||
padding: 5px 1px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.active{
|
||||
background: var(--aie-menus-item-hover-color);
|
||||
}
|
||||
}
|
||||
|
||||
.aie-menu-item > div {
|
||||
height: 18px;
|
||||
padding: 5px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.aie-menu-item > div:hover:not([no-hover]) {
|
||||
background: var(--aie-menus-item-hover-color);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.aie-content:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
|
||||
.suggestion {
|
||||
background: #fff;
|
||||
border: 1px solid #ccc;
|
||||
height: 200px;
|
||||
width: 200px;
|
||||
|
||||
.items {
|
||||
.item {
|
||||
display: block;
|
||||
width: 190px;
|
||||
height: 30px;
|
||||
margin: 10px 5px;
|
||||
background: #ffffff;
|
||||
border: 1px solid #dfdfdf;
|
||||
text-align: left;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.item-selected {
|
||||
border: 1px solid #333;
|
||||
background: #f1f3f5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.aie-bubble-menu {
|
||||
height: 30px;
|
||||
background: #fff;
|
||||
border: solid 1px #ccc;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 5px;
|
||||
|
||||
&-item {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin: 5px;
|
||||
padding: 5px;
|
||||
|
||||
svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
&-item:hover {
|
||||
background: #efefef;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.aie-dropdown-container {
|
||||
background: var(--aie-dropdown-container-bg-color);
|
||||
border: solid 1px var(--aie-dropdown-container-border-color);
|
||||
overflow: auto;
|
||||
box-shadow: 0 2px 5px 2px rgba(0, 0, 0, 0.1);
|
||||
padding: 5px 0;
|
||||
color: var(--aie-menus-text-color);
|
||||
|
||||
svg {
|
||||
fill: var(--aie-menus-text-color);
|
||||
}
|
||||
|
||||
.aie-dropdown-item {
|
||||
display: flex;
|
||||
padding: 3px 0;
|
||||
|
||||
.text {
|
||||
padding: 2px 0;
|
||||
overflow: hidden;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.7em;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.6em;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 1.4em;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1.3em;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
}
|
||||
|
||||
.red-dot-container {
|
||||
display: flex;
|
||||
width: 20px;
|
||||
|
||||
.red-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50rem;
|
||||
background: red;
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--aie-dropdown-container-item-hover-color);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.emoji-cells {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
margin-top: -10px;
|
||||
|
||||
.emoji-cell {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin: 2px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
//background: #cccccc;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.color-panel {
|
||||
margin: 0 5px;
|
||||
&-title {
|
||||
padding-top: 5px;font-size: 14px;
|
||||
color: var(--aie-dropdown-color-panel-title-color);
|
||||
}
|
||||
&-default-button {
|
||||
text-align: center;
|
||||
border: 1px solid var(--aie-dropdown-color-panel-button-border-color);;
|
||||
color: var(--aie-dropdown-color-panel-title-color);
|
||||
line-height: 24px;
|
||||
font-size: 14px
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.aie-resize-wrapper {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
|
||||
img {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&:hover .aie-resize {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.aie-resize {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
left: -2px;
|
||||
border: 2px solid #609eec;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
div {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
div::after {
|
||||
content: "";
|
||||
display: block;
|
||||
margin: auto;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: #609eec;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.aie-resize-btn-top-left {
|
||||
left: -10px;
|
||||
top: -10px;
|
||||
cursor: nwse-resize;
|
||||
}
|
||||
|
||||
.aie-resize-btn-top-right {
|
||||
right: -10px;
|
||||
top: -10px;
|
||||
cursor: nesw-resize;
|
||||
}
|
||||
|
||||
.aie-resize-btn-bottom-left {
|
||||
left: -10px;
|
||||
bottom: -10px;
|
||||
cursor: nesw-resize;
|
||||
}
|
||||
|
||||
.aie-resize-btn-bottom-right {
|
||||
right: -10px;
|
||||
bottom: -10px;
|
||||
cursor: nwse-resize;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.aie-loader-placeholder {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
background: #efefef;
|
||||
display: flex;
|
||||
|
||||
svg {
|
||||
margin: auto;
|
||||
width: 80px;
|
||||
fill: #cccccc;
|
||||
animation: rotate 2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.aie-codeblock-wrapper {
|
||||
background: var(--aie-pre-bg-color);
|
||||
word-wrap: normal;
|
||||
white-space: normal;
|
||||
padding-top: 5px;
|
||||
border-radius: 5px;
|
||||
|
||||
&:hover .aie-codeblock-tools {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.aie-codeblock-tools {
|
||||
display: flex;
|
||||
//float: right;
|
||||
div {
|
||||
color: var(--aie-text-color);
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
&-lang {
|
||||
margin-left: auto;
|
||||
|
||||
svg {
|
||||
fill: var(--aie-text-color);
|
||||
margin: 2px;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
&-autowrap {
|
||||
margin: 0 10px;
|
||||
|
||||
svg {
|
||||
fill: var(--aie-text-color);
|
||||
margin: 2px;
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.aie-codeblock-langs {
|
||||
background: var(--aie-dropdown-container-bg-color);
|
||||
width: 150px;
|
||||
height: 300px;
|
||||
overflow: auto;
|
||||
border: solid 1px var(--aie-dropdown-container-border-color);
|
||||
color: var(--aie-menus-text-color);
|
||||
&-item {
|
||||
padding: 5px;
|
||||
font-size: 14px;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background: var(--aie-dropdown-container-item-hover-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
aie-footer {
|
||||
> div {
|
||||
border-top: solid 1px var(--aie-container-border);
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
font-size: 14px;
|
||||
text-align: right;
|
||||
color: #666;
|
||||
padding-right: 10px
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.aie-theme-light{
|
||||
@import (less) '../../node_modules/highlight.js/styles/atom-one-light.css';
|
||||
}
|
||||
.aie-theme-dark{
|
||||
@import (less) '../../node_modules/highlight.js/styles/atom-one-dark.css';
|
||||
}
|
||||
|
||||
|
||||
|
||||
.aie-popover {
|
||||
min-width: 100px;
|
||||
min-height: 100px;
|
||||
background: #fff;
|
||||
border: solid 1px #cccccc;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&-header {
|
||||
height: 20px;
|
||||
display: flex;
|
||||
padding: 5px 5px 0;
|
||||
|
||||
&-close {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&-content {
|
||||
flex-grow: 1;
|
||||
padding: 0 10px 10px 10px;
|
||||
|
||||
> div {
|
||||
color: #333333;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
input, select {
|
||||
border: solid 1px #ccc;
|
||||
height: 25px;
|
||||
padding-inline: 5px;
|
||||
|
||||
&:focus-visible {
|
||||
outline: 0px;
|
||||
border: solid 1px #999999;
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
height: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
&-footer {
|
||||
display: flex;
|
||||
padding: 10px;
|
||||
|
||||
&-confirm {
|
||||
margin-left: auto;
|
||||
background: #ffffff;
|
||||
border: solid 1px #ccc;
|
||||
padding: 5px 10px;
|
||||
font-size: 14px;
|
||||
|
||||
&:hover {
|
||||
border-color: #999999;
|
||||
color: #333333;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
import './variable.less';
|
||||
import './aieditor.less';
|
||||
import './aiecontent.less';
|
|
@ -0,0 +1,52 @@
|
|||
|
||||
|
||||
:root,
|
||||
:root .aie-theme-light {
|
||||
--aie-text-color: #333;
|
||||
--aie-theme-color-rgb: 44, 126, 248;
|
||||
--aie-theme-bg-color: rgba(44, 126, 248, 0.1);
|
||||
--aie-container-border:#ccc;
|
||||
--aie-content-color:#333;
|
||||
--aie-pre-bg-color:#fafafa;
|
||||
--aie-blockquote-bg-color:#fafafa;
|
||||
--aie-blockquote-border-color:#e3e3e3;
|
||||
--aie-blockquote-text-color:#888888;
|
||||
--aie-dropdown-container-bg-color:#ffffff;
|
||||
--aie-dropdown-container-border-color:#e3e3e3;
|
||||
--aie-dropdown-container-item-hover-color: #efefef;
|
||||
--aie-dropdown-color-panel-title-color: #666;
|
||||
--aie-dropdown-color-panel-button-border-color:#e3e3e3;
|
||||
--aie-menus-text-color:var(--aie-content-color);
|
||||
--aie-menus-bg-color:#ffffff;
|
||||
--aie-menus-svg-color:#353535;
|
||||
--aie-menus-item-hover-color:#eee;
|
||||
--aie-menus-divider-color:#eaeaea;
|
||||
--aie-menus-ai-bg-color:var(--aie-menus-svg-color);
|
||||
--aie-menus-ai-color:#ffffff;
|
||||
}
|
||||
|
||||
|
||||
//the dark theme variables
|
||||
:root .aie-theme-dark {
|
||||
--aie-text-color: #eee;
|
||||
--aie-theme-color-rgb: 44, 126, 248;
|
||||
--aie-theme-bg-color: rgba(44, 126, 248, 0.1);
|
||||
--aie-container-border:#333;
|
||||
--aie-content-color:#eee;
|
||||
--aie-pre-bg-color:#282c34;
|
||||
--aie-blockquote-bg-color:#fafafa;
|
||||
--aie-blockquote-border-color:#e3e3e3;
|
||||
--aie-blockquote-text-color:#888888;
|
||||
--aie-dropdown-container-bg-color: #505050;
|
||||
--aie-dropdown-container-border-color: #606060;
|
||||
--aie-dropdown-container-item-hover-color: #696969;
|
||||
--aie-dropdown-color-panel-title-color: #ccc;
|
||||
--aie-dropdown-color-panel-button-border-color: #606060;
|
||||
--aie-menus-text-color:var(--aie-content-color);
|
||||
--aie-menus-bg-color:#1a1b1e;
|
||||
--aie-menus-svg-color: #cccccc;
|
||||
--aie-menus-item-hover-color:#333;
|
||||
--aie-menus-divider-color:#2c2c2c;
|
||||
--aie-menus-ai-bg-color:var(--aie-menus-svg-color);
|
||||
--aie-menus-ai-color:#000;
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
import {Decoration} from "prosemirror-view";
|
||||
|
||||
export const createMediaDecoration = (action: { pos: number, id: string }) => {
|
||||
const placeholder = document.createElement("div");
|
||||
placeholder.classList.add("aie-loader-placeholder");
|
||||
placeholder.innerHTML = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"></path><path d="M12 3C16.9706 3 21 7.02944 21 12H19C19 8.13401 15.866 5 12 5V3Z"></path></svg>
|
||||
`
|
||||
return Decoration.widget(action.pos, placeholder, {id: action.id});
|
||||
}
|
||||
|
||||
export const createAttachmentDecoration = (action: { pos: number, id: string ,text:string}) => {
|
||||
const placeholder = document.createElement("div");
|
||||
placeholder.classList.add("aie-loader-placeholder");
|
||||
placeholder.style.height = "20px"
|
||||
placeholder.style.display="inline-block"
|
||||
placeholder.innerHTML = `
|
||||
<svg style="width: 16px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"></path><path d="M12 3C16.9706 3 21 7.02944 21 12H19C19 8.13401 15.866 5 12 5V3Z"></path></svg>
|
||||
${action.text}
|
||||
`
|
||||
return Decoration.widget(action.pos, placeholder, {id: action.id});
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
import {findParentNode, isNodeActive} from '@tiptap/core';
|
||||
import type {EditorState} from '@tiptap/pm/state';
|
||||
import {isMarkActive} from './isMarkActive';
|
||||
import {NodeType} from "@tiptap/pm/model";
|
||||
|
||||
function isListType(type: NodeType): boolean {
|
||||
return !!type.spec.group?.split(' ').includes('list');
|
||||
}
|
||||
|
||||
export function isActive(
|
||||
state: EditorState,
|
||||
name: string | null,
|
||||
attributes: Record<string, any> = {}
|
||||
): boolean {
|
||||
if (!name) {
|
||||
return (
|
||||
isNodeActive(state, null, attributes) ||
|
||||
isMarkActive(state, null, attributes)
|
||||
);
|
||||
}
|
||||
const type = state.schema.nodes[name];
|
||||
if (type) {
|
||||
const listTypeFlag = isListType(type);
|
||||
if (listTypeFlag) {
|
||||
const parentList = findParentNode((node) => isListType(node.type))(
|
||||
state.selection
|
||||
);
|
||||
return !!(parentList && parentList.node.type.name === name);
|
||||
}
|
||||
return isNodeActive(state, name, attributes);
|
||||
}
|
||||
|
||||
if (state.schema.marks[name]) {
|
||||
return isMarkActive(state, name, attributes);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
import { getMarkType, objectIncludes } from '@tiptap/core';
|
||||
import type { MarkRange } from '@tiptap/core';
|
||||
import type { MarkType } from '@tiptap/pm/model';
|
||||
import type { EditorState } from '@tiptap/pm/state';
|
||||
|
||||
export function isMarkActive(
|
||||
state: EditorState,
|
||||
typeOrName: MarkType | string | null,
|
||||
attributes: Record<string, any> = {}
|
||||
): boolean {
|
||||
const { empty, ranges, from, to } = state.selection;
|
||||
const type = typeOrName ? getMarkType(typeOrName, state.schema) : null;
|
||||
|
||||
if (empty) {
|
||||
return !!(state.storedMarks || state.selection.$from.marks())
|
||||
.filter((mark) => {
|
||||
if (!type) {
|
||||
return true;
|
||||
}
|
||||
return type.name === mark.type.name;
|
||||
})
|
||||
.find((mark) =>
|
||||
objectIncludes(mark.attrs, attributes, { strict: false })
|
||||
);
|
||||
}
|
||||
|
||||
let selectionFrom = from;
|
||||
let selectionTo = to;
|
||||
const markRanges: MarkRange[] = [];
|
||||
|
||||
ranges.forEach(({ $from, $to }) => {
|
||||
const from = $from.pos;
|
||||
const to = $to.pos;
|
||||
|
||||
selectionFrom = Math.min(selectionFrom, from);
|
||||
selectionTo = Math.max(selectionFrom, to);
|
||||
|
||||
state.doc.nodesBetween(from, to, (node, pos) => {
|
||||
if (!node.isText && !node.marks.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const relativeFrom = Math.max(from, pos);
|
||||
const relativeTo = Math.min(to, pos + node.nodeSize);
|
||||
|
||||
markRanges.push(
|
||||
...node.marks.map((mark) => ({
|
||||
mark,
|
||||
from: relativeFrom,
|
||||
to: relativeTo,
|
||||
}))
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
const selectionRange = selectionTo - selectionFrom;
|
||||
|
||||
if (selectionRange === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// calculate range of matched mark
|
||||
const matchedRange = markRanges
|
||||
.filter((markRange) => {
|
||||
if (!type) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return type.name === markRange.mark.type.name;
|
||||
})
|
||||
.filter((markRange) =>
|
||||
objectIncludes(markRange.mark.attrs, attributes, { strict: false })
|
||||
)
|
||||
.reduce((sum, markRange) => sum + markRange.to - markRange.from, 0);
|
||||
|
||||
// calculate range of marks that excludes the searched mark
|
||||
// for example `code` doesn’t allow any other marks
|
||||
const excludedRange = markRanges
|
||||
.filter((markRange) => {
|
||||
if (!type) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return markRange.mark.type !== type && markRange.mark.type.excludes(type);
|
||||
})
|
||||
.reduce((sum, markRange) => sum + markRange.to - markRange.from, 0);
|
||||
|
||||
// we only include the result of `excludedRange`
|
||||
// if there is a match at all
|
||||
const range = matchedRange > 0 ? matchedRange + excludedRange : matchedRange;
|
||||
|
||||
return range >= selectionRange;
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
export const uploadFile = (file: File, _uploadUrl: string, _headers: Record<string, any>, _formName: string): Promise<Record<string, any>> => {
|
||||
let reader = new FileReader;
|
||||
return new Promise((accept, fail) => {
|
||||
reader.onload = () => accept({src:reader.result as string});
|
||||
reader.onerror = () => fail(reader.error);
|
||||
// Some extra delay to make the asynchronicity visible
|
||||
setTimeout(() => reader.readAsDataURL(file), 1500);
|
||||
})
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
export const resize = (container: HTMLDivElement
|
||||
, root: HTMLElement
|
||||
, updateAttrs: (data: any) => void) => {
|
||||
|
||||
const imgRef = container.querySelector(".resize-obj") as HTMLElement,
|
||||
minWidth = 10;
|
||||
|
||||
let startX: number, imageWidth: number, startPosition: string,maxWidth:number;
|
||||
|
||||
|
||||
const onMousedown = (e: any) => {
|
||||
e.preventDefault();
|
||||
|
||||
root.addEventListener('mousemove', onMousemove);
|
||||
root.addEventListener('mouseup', onMouseup);
|
||||
root.addEventListener('mouseleave', onMouseup);
|
||||
|
||||
startX = e.clientX;
|
||||
imageWidth = Number(imgRef.getAttribute("data-with")) || imgRef.clientWidth;
|
||||
startPosition = e.target.getAttribute("data-position");
|
||||
maxWidth = (root.clientWidth - 100);
|
||||
};
|
||||
|
||||
|
||||
const onMousemove = (event: MouseEvent) => {
|
||||
|
||||
const distanceX = event.clientX - startX;
|
||||
if (distanceX == 0) return;
|
||||
|
||||
// debugger
|
||||
const zoomIn = startPosition === "right" ? distanceX > 0 : distanceX < 0;
|
||||
|
||||
let newWidth = imageWidth + Math.abs(distanceX) * (zoomIn ? 1 : -1);
|
||||
|
||||
if (newWidth >= maxWidth) {
|
||||
newWidth = maxWidth;
|
||||
}
|
||||
if (newWidth < minWidth) {
|
||||
newWidth = minWidth;
|
||||
}
|
||||
|
||||
//及时修改 image 节点宽度,再拖动结束后再通知渲染视图
|
||||
imgRef.style.width = `${newWidth}px`;
|
||||
imgRef.setAttribute("data-width", newWidth.toString())
|
||||
}
|
||||
|
||||
|
||||
const onMouseup = () => {
|
||||
root.removeEventListener('mousemove', onMousemove);
|
||||
root.removeEventListener('mouseup', onMouseup);
|
||||
root.removeEventListener('mouseleave', onMouseup);
|
||||
|
||||
const attrs = {width: Number(imgRef.getAttribute("data-width"))};
|
||||
updateAttrs(attrs)
|
||||
};
|
||||
|
||||
|
||||
for (let child of container.querySelector(".aie-resize")!.children) {
|
||||
child.addEventListener("mousedown", onMousedown)
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
import {
|
||||
callOrReturn,
|
||||
ExtendedRegExpMatchArray,
|
||||
InputRule,
|
||||
InputRuleFinder,
|
||||
} from '@tiptap/core';
|
||||
import { NodeType } from '@tiptap/pm/model';
|
||||
|
||||
export function textblockTypeInputRule(config: {
|
||||
find: InputRuleFinder;
|
||||
type: NodeType;
|
||||
getAttributes?:
|
||||
| Record<string, any>
|
||||
| ((match: ExtendedRegExpMatchArray) => Record<string, any>)
|
||||
| false
|
||||
| null;
|
||||
}) {
|
||||
return new InputRule({
|
||||
find: config.find,
|
||||
handler: ({ state, range, match, commands }) => {
|
||||
const $start = state.doc.resolve(range.from);
|
||||
const attributes =
|
||||
callOrReturn(config.getAttributes, undefined, match) || {};
|
||||
|
||||
if (
|
||||
!$start
|
||||
.node(-1)
|
||||
.canReplaceWith($start.index(-1), $start.indexAfter(-1), config.type)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
state.tr
|
||||
.delete(range.from, range.to)
|
||||
.setBlockType(range.from, range.from, config.type, attributes);
|
||||
|
||||
// invoke focus command
|
||||
setTimeout(() => commands.focus(true), 0);
|
||||
},
|
||||
});
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
export const uploadFile = (file: File, uploadUrl: string, headers: Record<string, any>, formName: string): Promise<Record<string, any>> => {
|
||||
const formData = new FormData();
|
||||
formData.append(formName, file);
|
||||
return new Promise((resolve, reject) => {
|
||||
fetch(uploadUrl, {
|
||||
method: "post",
|
||||
headers: {'Accept': 'application/json', ...headers},
|
||||
body: formData,
|
||||
}).then((resp) => resp.json())
|
||||
.then(json => {
|
||||
resolve(json);
|
||||
}).catch((error) => {
|
||||
reject(error);
|
||||
})
|
||||
});
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
export const uuid = () => {
|
||||
return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, (c: any) =>
|
||||
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
|
||||
);
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
Loading…
Reference in New Issue