@nonoim/editor 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +7 -0
- package/README.md +36 -0
- package/dist/editor.cjs +1 -0
- package/dist/editor.css +1 -0
- package/dist/editor.js +382 -0
- package/package.json +31 -0
- package/src/index.d.ts +8 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 nonoim
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, subject to including this notice.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED.
|
package/README.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# @nonoim/editor
|
|
2
|
+
|
|
3
|
+
Lightweight Vue 3 visual Markdown editor built with official Tiptap packages. Markdown remains the `v-model` contract.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @nonoim/editor
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```vue
|
|
12
|
+
<script setup>
|
|
13
|
+
import { ref } from 'vue';
|
|
14
|
+
import { NonoEditor } from '@nonoim/editor';
|
|
15
|
+
import '@nonoim/editor/style.css';
|
|
16
|
+
const content = ref('## Hello');
|
|
17
|
+
</script>
|
|
18
|
+
<template><NonoEditor v-model="content" locale="zh" /></template>
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Images
|
|
22
|
+
|
|
23
|
+
Provide `uploadImages(files, onProgress)` and return `{ url, alt }[]`:
|
|
24
|
+
|
|
25
|
+
```js
|
|
26
|
+
const uploadImages = async (files, onProgress) => files.map(async (file) => {
|
|
27
|
+
onProgress({ file, progress: 0 });
|
|
28
|
+
const url = await uploadToYourStorage(file);
|
|
29
|
+
onProgress({ file, progress: 100 });
|
|
30
|
+
return { url, alt: file.name };
|
|
31
|
+
});
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Storage, notifications, authentication and autosave stay in the host application. Events: `update:modelValue`, `warning`, `upload-complete`, `upload-error`.
|
|
35
|
+
|
|
36
|
+
MIT licensed.
|
package/dist/editor.cjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const e=require("vue"),z=require("@tiptap/vue-3"),Q=require("@tiptap/starter-kit"),J=require("@tiptap/extension-image"),W=require("@tiptap/extension-table"),X=require("@tiptap/markdown"),Y=require("@tiptap/extensions"),y=d=>/<!--[^]*?-->|<\/?[A-Za-z][A-Za-z0-9-]*(?:\s[^>]*)?\s*\/?>/m.test(d||""),ee=["aria-label"],te={key:0,class:"nono-rich-editor__group"},oe=["disabled","title"],ne=["disabled","title"],le={key:1,class:"nono-rich-editor__group"},ae=["aria-label","value"],re={value:"paragraph"},ie={value:"heading-2"},se={value:"heading-3"},ue={value:"heading-4"},ce={key:2,class:"nono-rich-editor__group"},de=["title"],me=["title"],pe=["title"],ve={key:3,class:"nono-rich-editor__group nono-rich-editor__group--menu"},ge=["aria-label"],be=["disabled"],he={class:"nono-rich-editor__table-grid"},_e=["disabled"],fe=["disabled"],ke=["disabled"],ye=["disabled"],Ve=["disabled"],Ee={key:4,class:"nono-rich-editor__group"},Ne=["title"],Ce=["title"],Se=["title"],Be=["title"],we={key:5,class:"nono-rich-editor__group"},Te=["accept"],xe={class:"nono-rich-editor__group"},De=["aria-pressed"],Me={key:1,class:"nono-rich-editor__notice",role:"status"},ze={key:2,class:"nono-rich-editor__upload-status"},$e={class:"nono-rich-editor__progress"},Le={key:0},Ae={key:3,class:"nono-rich-editor__fallback"},Ie=["value","placeholder"],Ue=["value","placeholder"],He={key:6,class:"nono-rich-editor__footer"},Re={key:0,class:"nono-rich-editor__help"},qe="image/jpeg,image/png,image/webp,image/gif,image/svg+xml",B={__name:"NonoEditor",props:{modelValue:{type:String,default:""},placeholder:{type:String,default:""},rows:{type:Number,default:10},help:{type:String,default:""},fill:{type:Boolean,default:!1},locale:{type:String,default:"en"},uploadImages:{type:Function,default:null},imageAccept:{type:String,default:qe},maxImageSize:{type:Number,default:10*1024*1024}},emits:["update:modelValue","warning","upload-complete","upload-error"],setup(d,{emit:$}){const c=d,b=$,n=(o,t,l)=>{let u=/^zh(?:-|$)/i.test(c.locale)&&t||o;return l?u.replace(/\{(\w+)\}/g,(m,v)=>l[v]==null?"":String(l[v])):u},a=e.shallowRef(null),V=e.ref(""),_=e.ref(!1),h=e.ref(0),C=e.ref(null),r=e.ref(!1),p=e.ref(c.modelValue||""),f=e.ref(null),i=e.ref(null),w=e.computed(()=>i.value?.status==="uploading"),L=e.computed(()=>r.value&&y(p.value)),T=e.computed(()=>c.fill?"640px":`${Math.max(420,c.rows*32)}px`),A=e.computed(()=>(h.value,r.value?p.value.length:a.value?.getText()?.length||0)),I=e.computed(()=>(h.value,a.value?.can().undo()||!1)),U=e.computed(()=>(h.value,a.value?.can().redo()||!1)),H=e.computed(()=>{h.value;for(const o of[2,3,4])if(a.value?.isActive("heading",{level:o}))return`heading-${o}`;return"paragraph"}),E=()=>{h.value+=1},s=(o,t)=>(h.value,a.value?.isActive(o,t)||!1),g=o=>{const t=a.value?.chain().focus();t&&typeof t[o]=="function"&&t[o]().run()},S=()=>{f.value&&(f.value.open=!1)},k=o=>{g(o),S()},R=()=>{a.value?.chain().focus().insertTable({rows:3,cols:3,withHeaderRow:!0}).run(),S()},N=o=>g(`toggle${o[0].toUpperCase()}${o.slice(1)}`),q=o=>{const t=a.value?.chain().focus();t&&(o.target.value==="paragraph"?t.setParagraph().run():t.setHeading({level:Number(o.target.value.split("-")[1])}).run())},F=()=>{if(!a.value||typeof window>"u")return;const o=window.prompt(n("Enter a URL. Leave blank to remove the link.","请输入链接地址,留空可移除链接。"),a.value.getAttributes("link").href||"");if(o===null)return;const t=a.value.chain().focus().extendMarkRange("link");o.trim()?t.setLink({href:o.trim()}).run():t.unsetLink().run()},x=o=>{p.value=o,C.value=o,o!==c.modelValue&&b("update:modelValue",o)},P=()=>{if(a.value){if(!r.value){p.value=a.value.getMarkdown(),r.value=!0;return}if(y(p.value)){b("warning",n("Remove the HTML before switching to visual editing.","请先移除 HTML,再切换到可视化编辑。"));return}a.value.commands.setContent(p.value||"",{contentType:"markdown",emitUpdate:!1}),r.value=!1,E(),a.value.commands.focus()}},j=e.computed(()=>i.value?.progress>=95?n("Processing...","处理中..."):n("Uploading... {progress}%","上传中... {progress}%",{progress:i.value?.progress||0})),K=e.computed(()=>i.value?.status==="uploading"?`${i.value.progress}%`:i.value?.status==="success"?n("Complete","完成"):n("Failed","失败")),O=o=>!new Set(c.imageAccept.split(",").map(l=>l.trim())).has(o.type)&&!/\.(png|jpe?g|gif|webp|svg)$/i.test(o.name||"")?n("Unsupported image format.","不支持该图片格式。"):o.size>c.maxImageSize?n("File exceeds the size limit.","图片超过大小限制。"):"",D=(o,t)=>{const l=typeof o=="number"?{progress:o}:o||{};i.value={name:l.name||l.file?.name||t?.name||n("Images","图片"),progress:Math.max(0,Math.min(100,Number(l.progress)||0)),status:l.status||"uploading",error:""}},Z=o=>{const t=o.flatMap(({url:l,alt:u})=>[{type:"image",attrs:{src:l,alt:u||"image"}},{type:"paragraph"}]);t.length&&a.value?.chain().focus().insertContent(t).run()},G=async o=>{const t=Array.from(o.target.files||[]);if(o.target.value="",!t.length||!c.uploadImages)return;const l=[];for(const u of t){const m=O(u);if(m){const v=new Error(m);i.value={name:u.name,progress:0,status:"error",error:m},b("upload-error",v)}else l.push(u)}if(l.length){D(0,l[0]);try{const u=await c.uploadImages(l,v=>D(v,l[0])),m=Array.isArray(u)?u.filter(v=>v?.url):[];if(!m.length)throw new Error(n("The upload returned no image URLs.","上传结果中没有可用的图片地址。"));Z(m),i.value={name:n("{count} images","{count} 张图片",{count:m.length}),progress:100,status:"success",error:""},b("upload-complete",m)}catch(u){i.value={name:i.value?.name||l[0].name,progress:i.value?.progress||0,status:"error",error:u?.message||String(u)},b("upload-error",u)}}},M=o=>{f.value?.open&&!f.value.contains(o.target)&&S()};return e.watch(()=>c.modelValue,o=>{if(o!==C.value&&(p.value=o||""),!(!a.value||o===C.value)){if(y(o)){r.value=!0;return}r.value||a.value.commands.setContent(o||"",{contentType:"markdown",emitUpdate:!1}),E()}}),e.onMounted(()=>{document.addEventListener("pointerdown",M);try{r.value=y(c.modelValue),a.value=new z.Editor({extensions:[Q.configure({heading:{levels:[1,2,3,4,5,6]}}),J.configure({allowBase64:!1}),W.TableKit,Y.Placeholder.configure({placeholder:c.placeholder||n("Start writing...","开始输入正文…")}),X.Markdown],content:c.modelValue||"",contentType:"markdown",editorProps:{attributes:{class:"nono-rich-editor__prosemirror","aria-label":c.placeholder||n("Rich text editor","富文本编辑器")}},onUpdate:({editor:o})=>{x(o.getMarkdown()),E()},onSelectionUpdate:E,onFocus:()=>{_.value=!0},onBlur:()=>{_.value=!1}})}catch(o){V.value=n("The rich text editor could not start.","富文本编辑器未能启动。"),console.error("Failed to initialize editor",o)}}),e.onBeforeUnmount(()=>{document.removeEventListener("pointerdown",M),a.value?.destroy(),a.value=null}),(o,t)=>(e.openBlock(),e.createElementBlock("div",{class:e.normalizeClass(["nono-rich-editor",{"nono-rich-editor--fill":d.fill}])},[e.createElementVNode("div",{class:e.normalizeClass(["nono-rich-editor__shell",{"is-focused":_.value}])},[a.value?(e.openBlock(),e.createElementBlock("div",{key:0,class:"nono-rich-editor__toolbar",role:"toolbar","aria-label":n("Formatting tools","格式工具")},[r.value?e.createCommentVNode("",!0):(e.openBlock(),e.createElementBlock("div",te,[e.createElementVNode("button",{type:"button",class:"nono-rich-editor__button",disabled:!I.value,title:n("Undo","撤销"),onClick:t[0]||(t[0]=l=>g("undo"))},[t[18]||(t[18]=e.createTextVNode("↶ ",-1)),e.createElementVNode("span",null,e.toDisplayString(n("Undo","撤销")),1)],8,oe),e.createElementVNode("button",{type:"button",class:"nono-rich-editor__button",disabled:!U.value,title:n("Redo","重做"),onClick:t[1]||(t[1]=l=>g("redo"))},[t[19]||(t[19]=e.createTextVNode("↷ ",-1)),e.createElementVNode("span",null,e.toDisplayString(n("Redo","重做")),1)],8,ne)])),r.value?e.createCommentVNode("",!0):(e.openBlock(),e.createElementBlock("div",le,[e.createElementVNode("select",{class:"nono-rich-editor__select","aria-label":n("Text style","文字样式"),value:H.value,onChange:q},[e.createElementVNode("option",re,e.toDisplayString(n("Paragraph","正文")),1),e.createElementVNode("option",ie,e.toDisplayString(n("Heading 2","标题 2")),1),e.createElementVNode("option",se,e.toDisplayString(n("Heading 3","标题 3")),1),e.createElementVNode("option",ue,e.toDisplayString(n("Heading 4","标题 4")),1)],40,ae)])),r.value?e.createCommentVNode("",!0):(e.openBlock(),e.createElementBlock("div",ce,[e.createElementVNode("button",{type:"button",class:e.normalizeClass(["nono-rich-editor__icon is-bold",{"is-active":s("bold")}]),title:n("Bold","粗体"),onClick:t[2]||(t[2]=l=>N("bold"))},"B",10,de),e.createElementVNode("button",{type:"button",class:e.normalizeClass(["nono-rich-editor__icon is-italic",{"is-active":s("italic")}]),title:n("Italic","斜体"),onClick:t[3]||(t[3]=l=>N("italic"))},"I",10,me),e.createElementVNode("button",{type:"button",class:e.normalizeClass(["nono-rich-editor__icon is-strike",{"is-active":s("strike")}]),title:n("Strikethrough","删除线"),onClick:t[4]||(t[4]=l=>N("strike"))},"S",10,pe)])),r.value?e.createCommentVNode("",!0):(e.openBlock(),e.createElementBlock("div",ve,[e.createElementVNode("details",{ref_key:"tableMenu",ref:f,class:"nono-rich-editor__table-menu"},[e.createElementVNode("summary",{class:e.normalizeClass(["nono-rich-editor__button",{"is-active":s("table")}])},[t[20]||(t[20]=e.createTextVNode("▦ ",-1)),e.createElementVNode("span",null,e.toDisplayString(n("Table","表格")),1),t[21]||(t[21]=e.createTextVNode("⌄",-1))],2),e.createElementVNode("div",{class:"nono-rich-editor__table-panel",role:"menu","aria-label":n("Table tools","表格工具")},[e.createElementVNode("button",{type:"button",class:"nono-rich-editor__table-action is-wide",disabled:s("table"),onClick:R},"▦ "+e.toDisplayString(n("Insert 3 × 3 table","插入 3 × 3 表格")),9,be),t[22]||(t[22]=e.createElementVNode("div",{class:"nono-rich-editor__divider"},null,-1)),e.createElementVNode("div",he,[e.createElementVNode("button",{type:"button",class:"nono-rich-editor__table-action",disabled:!s("table"),onClick:t[5]||(t[5]=l=>k("addRowAfter"))},"+ "+e.toDisplayString(n("Add row","增加行")),9,_e),e.createElementVNode("button",{type:"button",class:"nono-rich-editor__table-action",disabled:!s("table"),onClick:t[6]||(t[6]=l=>k("deleteRow"))},"- "+e.toDisplayString(n("Delete row","删除行")),9,fe),e.createElementVNode("button",{type:"button",class:"nono-rich-editor__table-action",disabled:!s("table"),onClick:t[7]||(t[7]=l=>k("addColumnAfter"))},"+ "+e.toDisplayString(n("Add column","增加列")),9,ke),e.createElementVNode("button",{type:"button",class:"nono-rich-editor__table-action",disabled:!s("table"),onClick:t[8]||(t[8]=l=>k("deleteColumn"))},"- "+e.toDisplayString(n("Delete column","删除列")),9,ye)]),e.createElementVNode("button",{type:"button",class:"nono-rich-editor__table-action is-wide is-danger",disabled:!s("table"),onClick:t[9]||(t[9]=l=>k("deleteTable"))},"× "+e.toDisplayString(n("Delete table","删除整个表格")),9,Ve)],8,ge)],512)])),r.value?e.createCommentVNode("",!0):(e.openBlock(),e.createElementBlock("div",Ee,[e.createElementVNode("button",{type:"button",class:e.normalizeClass(["nono-rich-editor__icon",{"is-active":s("bulletList")}]),title:n("Bullet list","项目列表"),onClick:t[10]||(t[10]=l=>g("toggleBulletList"))},"•",10,Ne),e.createElementVNode("button",{type:"button",class:e.normalizeClass(["nono-rich-editor__icon is-small",{"is-active":s("orderedList")}]),title:n("Numbered list","编号列表"),onClick:t[11]||(t[11]=l=>g("toggleOrderedList"))},"1.",10,Ce),e.createElementVNode("button",{type:"button",class:e.normalizeClass(["nono-rich-editor__icon",{"is-active":s("blockquote")}]),title:n("Quote","引用"),onClick:t[12]||(t[12]=l=>g("toggleBlockquote"))},"“",10,Se),e.createElementVNode("button",{type:"button",class:e.normalizeClass(["nono-rich-editor__icon is-code",{"is-active":s("code")}]),title:n("Inline code","行内代码"),onClick:t[13]||(t[13]=l=>N("code"))},"</>",10,Be)])),r.value?e.createCommentVNode("",!0):(e.openBlock(),e.createElementBlock("div",we,[e.createElementVNode("button",{type:"button",class:e.normalizeClass(["nono-rich-editor__button",{"is-active":s("link")}]),onClick:F},[t[23]||(t[23]=e.createTextVNode("↗ ",-1)),e.createElementVNode("span",null,e.toDisplayString(n("Link","链接")),1)],2),d.uploadImages?(e.openBlock(),e.createElementBlock("label",{key:0,class:e.normalizeClass(["nono-rich-editor__button nono-rich-editor__upload",{"is-disabled":w.value}])},[t[24]||(t[24]=e.createTextVNode("↑ ",-1)),e.createElementVNode("span",null,e.toDisplayString(w.value?j.value:n("Upload","上传")),1),e.createElementVNode("input",{type:"file",accept:d.imageAccept,multiple:"",onChange:G},null,40,Te)],2)):e.createCommentVNode("",!0)])),e.createElementVNode("div",xe,[e.createElementVNode("button",{type:"button",class:e.normalizeClass(["nono-rich-editor__button",{"is-active":r.value}]),"aria-pressed":r.value,onClick:P},[t[25]||(t[25]=e.createTextVNode("MD ",-1)),e.createElementVNode("span",null,e.toDisplayString(r.value?n("Visual","可视化"):n("Source","源码")),1)],10,De)])],8,ee)):e.createCommentVNode("",!0),L.value?(e.openBlock(),e.createElementBlock("div",Me,e.toDisplayString(n("This content contains HTML and is kept in source mode to prevent formatting loss.","此内容包含 HTML,为避免格式丢失,已使用源码模式。移除 HTML 后才能切换到可视化编辑。")),1)):e.createCommentVNode("",!0),i.value?(e.openBlock(),e.createElementBlock("div",ze,[e.createElementVNode("div",null,[e.createElementVNode("strong",null,e.toDisplayString(i.value.name),1),e.createElementVNode("span",{class:e.normalizeClass(`is-${i.value.status}`)},e.toDisplayString(K.value),3)]),e.createElementVNode("div",$e,[e.createElementVNode("i",{class:e.normalizeClass(`is-${i.value.status}`),style:e.normalizeStyle({width:`${i.value.progress}%`})},null,6)]),i.value.error?(e.openBlock(),e.createElementBlock("p",Le,e.toDisplayString(i.value.error),1)):e.createCommentVNode("",!0)])):e.createCommentVNode("",!0),V.value?(e.openBlock(),e.createElementBlock("div",Ae,[e.createElementVNode("textarea",{value:d.modelValue,placeholder:d.placeholder,onInput:t[14]||(t[14]=l=>b("update:modelValue",l.target.value))},null,40,Ie),e.createElementVNode("p",null,e.toDisplayString(V.value),1)])):r.value?(e.openBlock(),e.createElementBlock("textarea",{key:4,value:p.value,placeholder:d.placeholder,class:"nono-rich-editor__source",style:e.normalizeStyle({"--nono-editor-min-height":T.value}),spellcheck:"false",onInput:t[15]||(t[15]=l=>x(l.target.value)),onFocus:t[16]||(t[16]=l=>_.value=!0),onBlur:t[17]||(t[17]=l=>_.value=!1)},null,44,Ue)):(e.openBlock(),e.createBlock(e.unref(z.EditorContent),{key:5,editor:a.value,class:"nono-rich-editor__content",style:e.normalizeStyle({"--nono-editor-min-height":T.value})},null,8,["editor","style"])),a.value&&!V.value?(e.openBlock(),e.createElementBlock("div",He,[e.createElementVNode("span",null,e.toDisplayString(n("{count} characters","{count} 个字符",{count:A.value})),1),e.createElementVNode("span",null,e.toDisplayString(r.value?n("Markdown source","Markdown 源码"):n("Markdown compatible","兼容 Markdown")),1)])):e.createCommentVNode("",!0)],2),d.help?(e.openBlock(),e.createElementBlock("p",Re,e.toDisplayString(d.help),1)):e.createCommentVNode("",!0)],2))}};exports.NonoEditor=B;exports.NonoMarkdownEditor=B;exports.containsRawHtml=y;exports.default=B;
|
package/dist/editor.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.nono-rich-editor{--nono-editor-primary:#2856c5;--nono-editor-border:#cbd5e1;--nono-editor-heading:#0f172a;--nono-editor-text:#1e293b;--nono-editor-muted:#64748b;display:flex;min-width:0;flex-direction:column;gap:.5rem;color:var(--nono-editor-text);font-family:ui-sans-serif,system-ui,sans-serif}.nono-rich-editor *{box-sizing:border-box}.nono-rich-editor--fill{min-height:0;flex:1}.nono-rich-editor__shell{overflow:hidden;border:1px solid var(--nono-editor-border);border-radius:12px;background:#fff}.nono-rich-editor__shell.is-focused{border-color:var(--nono-editor-primary);box-shadow:0 0 0 3px color-mix(in srgb,var(--nono-editor-primary) 14%,transparent)}.nono-rich-editor__toolbar{position:sticky;top:0;z-index:5;display:flex;min-height:52px;flex-wrap:wrap;align-items:center;gap:6px;border-bottom:1px solid var(--nono-editor-border);background:#f8fafcf7;padding:7px 9px;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}.nono-rich-editor__group{display:flex;align-items:center;gap:3px;border-right:1px solid var(--nono-editor-border);padding-right:7px}.nono-rich-editor__group--menu{position:relative}.nono-rich-editor__group:last-child{border:0;padding:0}.nono-rich-editor button,.nono-rich-editor select,.nono-rich-editor textarea{font:inherit}.nono-rich-editor__button,.nono-rich-editor__icon{display:inline-flex;min-height:36px;align-items:center;justify-content:center;gap:6px;border:1px solid transparent;border-radius:7px;color:#334155;background:transparent;cursor:pointer}.nono-rich-editor__button{padding:0 10px;font-size:12px;font-weight:650}.nono-rich-editor__icon{width:36px;padding:0;font-size:16px}.nono-rich-editor__icon.is-bold{font-weight:800}.nono-rich-editor__icon.is-italic{font-style:italic}.nono-rich-editor__icon.is-strike{text-decoration:line-through}.nono-rich-editor__icon.is-small{font-size:13px}.nono-rich-editor__icon.is-code{font-family:ui-monospace,monospace;font-size:12px}.nono-rich-editor__button:hover,.nono-rich-editor__icon:hover,.nono-rich-editor .is-active{border-color:var(--nono-editor-border);background:#fff;color:var(--nono-editor-primary)}.nono-rich-editor button:focus-visible,.nono-rich-editor select:focus-visible{outline:2px solid color-mix(in srgb,var(--nono-editor-primary) 35%,transparent);outline-offset:1px}.nono-rich-editor button:disabled,.nono-rich-editor .is-disabled{cursor:not-allowed;opacity:.35}.nono-rich-editor__select{min-height:36px;width:116px;border:1px solid var(--nono-editor-border);border-radius:7px;background:#fff;padding:0 8px;color:#334155;font-size:12px;font-weight:650}.nono-rich-editor__table-menu{position:relative}.nono-rich-editor__table-menu summary{list-style:none;cursor:pointer}.nono-rich-editor__table-menu summary::-webkit-details-marker{display:none}.nono-rich-editor__table-panel{position:absolute;top:calc(100% + 8px);left:0;z-index:20;width:224px;border:1px solid var(--nono-editor-border);border-radius:10px;background:#fff;padding:6px;box-shadow:0 14px 32px #1e293b29}.nono-rich-editor__table-grid{display:grid;grid-template-columns:1fr 1fr;gap:3px}.nono-rich-editor__table-action{display:flex;min-height:34px;align-items:center;gap:7px;border:0;border-radius:6px;background:transparent;padding:0 9px;color:#334155;cursor:pointer;font-size:12px;font-weight:600;text-align:left}.nono-rich-editor__table-action:hover:not(:disabled){background:#f1f5f9;color:var(--nono-editor-primary)}.nono-rich-editor__table-action.is-wide{width:100%}.nono-rich-editor__table-action.is-danger{margin-top:3px;color:#dc2626}.nono-rich-editor__divider{height:1px;margin:5px 3px;background:#e2e8f0}.nono-rich-editor__upload{cursor:pointer}.nono-rich-editor__upload input{display:none}.nono-rich-editor__notice{border-bottom:1px solid #fde68a;background:#fffbeb;padding:8px 12px;color:#78350f;font-size:12px}.nono-rich-editor__upload-status{border-bottom:1px solid var(--nono-editor-border);padding:8px 12px;background:#fff;font-size:12px}.nono-rich-editor__upload-status>div:first-child{display:flex;justify-content:space-between;gap:12px}.nono-rich-editor__upload-status .is-success{color:#047857}.nono-rich-editor__upload-status .is-error,.nono-rich-editor__upload-status p{color:#dc2626}.nono-rich-editor__progress{height:6px;overflow:hidden;margin-top:6px;border-radius:99px;background:#f1f5f9}.nono-rich-editor__progress i{display:block;height:100%;background:var(--nono-editor-primary)}.nono-rich-editor__progress i.is-success{background:#059669}.nono-rich-editor__progress i.is-error{background:#ef4444}.nono-rich-editor__fallback{padding:16px}.nono-rich-editor__fallback textarea{min-height:420px;width:100%;border:1px solid var(--nono-editor-border);padding:12px}.nono-rich-editor__fallback p{color:#dc2626;font-size:12px}.nono-rich-editor__source{display:block;width:100%;min-height:var(--nono-editor-min-height);resize:vertical;border:0;background:#fff;padding:28px clamp(18px,4vw,52px) 48px;color:var(--nono-editor-text);font-family:ui-monospace,monospace;font-size:14px;line-height:1.7;outline:0}.nono-rich-editor__content{background:#fff}.nono-rich-editor__content .ProseMirror{min-height:var(--nono-editor-min-height);padding:28px clamp(18px,4vw,52px) 48px;color:var(--nono-editor-text);font-size:16px;line-height:1.75;outline:0}.nono-rich-editor__content .ProseMirror>*{max-width:100%}.nono-rich-editor__content p{margin:0 0 .85em}.nono-rich-editor__content p.is-editor-empty:first-child:before{float:left;height:0;color:#94a3b8;content:attr(data-placeholder)}.nono-rich-editor__content h2,.nono-rich-editor__content h3,.nono-rich-editor__content h4{color:var(--nono-editor-heading);line-height:1.25}.nono-rich-editor__content h2{margin:1.35em 0 .6em;font-size:1.65em}.nono-rich-editor__content h3{margin:1.2em 0 .5em;font-size:1.35em}.nono-rich-editor__content h4{margin:1.1em 0 .45em;font-size:1.15em}.nono-rich-editor__content ul,.nono-rich-editor__content ol{margin:.8em 0;padding-left:1.6em}.nono-rich-editor__content ul{list-style:disc}.nono-rich-editor__content ol{list-style:decimal}.nono-rich-editor__content blockquote{margin:1em 0;border-left:3px solid #94a3b8;padding:.2em 0 .2em 1em;color:var(--nono-editor-muted)}.nono-rich-editor__content code{border-radius:4px;background:#f1f5f9;padding:.15em .35em;font-family:ui-monospace,monospace}.nono-rich-editor__content pre{overflow-x:auto;border-radius:8px;background:#0f172a;padding:14px;color:#e2e8f0}.nono-rich-editor__content pre code{background:transparent;padding:0}.nono-rich-editor__content a{color:var(--nono-editor-primary)}.nono-rich-editor__content img{display:block;max-width:100%;height:auto;margin:1.2em auto;border-radius:8px}.nono-rich-editor__content .tableWrapper{margin:1.2em 0;overflow-x:auto}.nono-rich-editor__content table{width:100%;border-collapse:collapse;table-layout:fixed}.nono-rich-editor__content th,.nono-rich-editor__content td{position:relative;min-width:90px;border:1px solid var(--nono-editor-border);padding:9px 11px;vertical-align:top}.nono-rich-editor__content th{background:#f1f5f9}.nono-rich-editor__content .selectedCell:after{position:absolute;inset:0;background:color-mix(in srgb,var(--nono-editor-primary) 12%,transparent);content:"";pointer-events:none}.nono-rich-editor__footer{display:flex;min-height:34px;align-items:center;justify-content:space-between;border-top:1px solid var(--nono-editor-border);background:#f8fafc;padding:0 12px;color:#94a3b8;font-size:11px}.nono-rich-editor__help{margin:0;color:var(--nono-editor-muted);font-size:12px}@media(max-width:720px){.nono-rich-editor__toolbar{position:static}.nono-rich-editor__button span{display:none}.nono-rich-editor__button{width:36px;padding:0}.nono-rich-editor__content .ProseMirror{padding:20px 16px 36px}}
|
package/dist/editor.js
ADDED
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
import { shallowRef as oe, ref as k, computed as _, watch as ne, onMounted as le, onBeforeUnmount as ae, openBlock as u, createElementBlock as d, normalizeClass as v, createElementVNode as n, createTextVNode as C, toDisplayString as i, createCommentVNode as m, normalizeStyle as V, createBlock as ie, unref as se } from "vue";
|
|
2
|
+
import { Editor as re, EditorContent as ue } from "@tiptap/vue-3";
|
|
3
|
+
import de from "@tiptap/starter-kit";
|
|
4
|
+
import ce from "@tiptap/extension-image";
|
|
5
|
+
import { TableKit as pe } from "@tiptap/extension-table";
|
|
6
|
+
import { Markdown as ve } from "@tiptap/markdown";
|
|
7
|
+
import { Placeholder as ge } from "@tiptap/extensions";
|
|
8
|
+
const B = (h) => /<!--[^]*?-->|<\/?[A-Za-z][A-Za-z0-9-]*(?:\s[^>]*)?\s*\/?>/m.test(h || ""), me = ["aria-label"], he = {
|
|
9
|
+
key: 0,
|
|
10
|
+
class: "nono-rich-editor__group"
|
|
11
|
+
}, be = ["disabled", "title"], _e = ["disabled", "title"], fe = {
|
|
12
|
+
key: 1,
|
|
13
|
+
class: "nono-rich-editor__group"
|
|
14
|
+
}, ye = ["aria-label", "value"], ke = { value: "paragraph" }, we = { value: "heading-2" }, Ce = { value: "heading-3" }, $e = { value: "heading-4" }, Me = {
|
|
15
|
+
key: 2,
|
|
16
|
+
class: "nono-rich-editor__group"
|
|
17
|
+
}, Te = ["title"], xe = ["title"], Se = ["title"], Le = {
|
|
18
|
+
key: 3,
|
|
19
|
+
class: "nono-rich-editor__group nono-rich-editor__group--menu"
|
|
20
|
+
}, Ae = ["aria-label"], Ie = ["disabled"], Be = { class: "nono-rich-editor__table-grid" }, Ee = ["disabled"], Ue = ["disabled"], Ve = ["disabled"], He = ["disabled"], Re = ["disabled"], Ne = {
|
|
21
|
+
key: 4,
|
|
22
|
+
class: "nono-rich-editor__group"
|
|
23
|
+
}, ze = ["title"], Fe = ["title"], Pe = ["title"], De = ["title"], je = {
|
|
24
|
+
key: 5,
|
|
25
|
+
class: "nono-rich-editor__group"
|
|
26
|
+
}, qe = ["accept"], Ke = { class: "nono-rich-editor__group" }, Ze = ["aria-pressed"], Ge = {
|
|
27
|
+
key: 1,
|
|
28
|
+
class: "nono-rich-editor__notice",
|
|
29
|
+
role: "status"
|
|
30
|
+
}, Oe = {
|
|
31
|
+
key: 2,
|
|
32
|
+
class: "nono-rich-editor__upload-status"
|
|
33
|
+
}, Qe = { class: "nono-rich-editor__progress" }, Je = { key: 0 }, We = {
|
|
34
|
+
key: 3,
|
|
35
|
+
class: "nono-rich-editor__fallback"
|
|
36
|
+
}, Xe = ["value", "placeholder"], Ye = ["value", "placeholder"], et = {
|
|
37
|
+
key: 6,
|
|
38
|
+
class: "nono-rich-editor__footer"
|
|
39
|
+
}, tt = {
|
|
40
|
+
key: 0,
|
|
41
|
+
class: "nono-rich-editor__help"
|
|
42
|
+
}, ot = "image/jpeg,image/png,image/webp,image/gif,image/svg+xml", dt = {
|
|
43
|
+
__name: "NonoEditor",
|
|
44
|
+
props: { modelValue: { type: String, default: "" }, placeholder: { type: String, default: "" }, rows: { type: Number, default: 10 }, help: { type: String, default: "" }, fill: { type: Boolean, default: !1 }, locale: { type: String, default: "en" }, uploadImages: { type: Function, default: null }, imageAccept: { type: String, default: ot }, maxImageSize: { type: Number, default: 10 * 1024 * 1024 } },
|
|
45
|
+
emits: ["update:modelValue", "warning", "upload-complete", "upload-error"],
|
|
46
|
+
setup(h, { emit: P }) {
|
|
47
|
+
const g = h, $ = P, o = (t, e, l) => {
|
|
48
|
+
let p = /^zh(?:-|$)/i.test(g.locale) && e || t;
|
|
49
|
+
return l ? p.replace(/\{(\w+)\}/g, (b, y) => l[y] == null ? "" : String(l[y])) : p;
|
|
50
|
+
}, a = oe(null), L = k(""), T = k(!1), M = k(0), E = k(null), s = k(!1), f = k(g.modelValue || ""), x = k(null), r = k(null), H = _(() => r.value?.status === "uploading"), D = _(() => s.value && B(f.value)), R = _(() => g.fill ? "640px" : `${Math.max(420, g.rows * 32)}px`), j = _(() => (M.value, s.value ? f.value.length : a.value?.getText()?.length || 0)), q = _(() => (M.value, a.value?.can().undo() || !1)), K = _(() => (M.value, a.value?.can().redo() || !1)), Z = _(() => {
|
|
51
|
+
M.value;
|
|
52
|
+
for (const t of [2, 3, 4]) if (a.value?.isActive("heading", { level: t })) return `heading-${t}`;
|
|
53
|
+
return "paragraph";
|
|
54
|
+
}), A = () => {
|
|
55
|
+
M.value += 1;
|
|
56
|
+
}, c = (t, e) => (M.value, a.value?.isActive(t, e) || !1), w = (t) => {
|
|
57
|
+
const e = a.value?.chain().focus();
|
|
58
|
+
e && typeof e[t] == "function" && e[t]().run();
|
|
59
|
+
}, U = () => {
|
|
60
|
+
x.value && (x.value.open = !1);
|
|
61
|
+
}, S = (t) => {
|
|
62
|
+
w(t), U();
|
|
63
|
+
}, G = () => {
|
|
64
|
+
a.value?.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: !0 }).run(), U();
|
|
65
|
+
}, I = (t) => w(`toggle${t[0].toUpperCase()}${t.slice(1)}`), O = (t) => {
|
|
66
|
+
const e = a.value?.chain().focus();
|
|
67
|
+
e && (t.target.value === "paragraph" ? e.setParagraph().run() : e.setHeading({ level: Number(t.target.value.split("-")[1]) }).run());
|
|
68
|
+
}, Q = () => {
|
|
69
|
+
if (!a.value || typeof window > "u") return;
|
|
70
|
+
const t = window.prompt(o("Enter a URL. Leave blank to remove the link.", "请输入链接地址,留空可移除链接。"), a.value.getAttributes("link").href || "");
|
|
71
|
+
if (t === null) return;
|
|
72
|
+
const e = a.value.chain().focus().extendMarkRange("link");
|
|
73
|
+
t.trim() ? e.setLink({ href: t.trim() }).run() : e.unsetLink().run();
|
|
74
|
+
}, N = (t) => {
|
|
75
|
+
f.value = t, E.value = t, t !== g.modelValue && $("update:modelValue", t);
|
|
76
|
+
}, J = () => {
|
|
77
|
+
if (a.value) {
|
|
78
|
+
if (!s.value) {
|
|
79
|
+
f.value = a.value.getMarkdown(), s.value = !0;
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (B(f.value)) {
|
|
83
|
+
$("warning", o("Remove the HTML before switching to visual editing.", "请先移除 HTML,再切换到可视化编辑。"));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
a.value.commands.setContent(f.value || "", { contentType: "markdown", emitUpdate: !1 }), s.value = !1, A(), a.value.commands.focus();
|
|
87
|
+
}
|
|
88
|
+
}, W = _(() => r.value?.progress >= 95 ? o("Processing...", "处理中...") : o("Uploading... {progress}%", "上传中... {progress}%", { progress: r.value?.progress || 0 })), X = _(() => r.value?.status === "uploading" ? `${r.value.progress}%` : r.value?.status === "success" ? o("Complete", "完成") : o("Failed", "失败")), Y = (t) => !new Set(g.imageAccept.split(",").map((l) => l.trim())).has(t.type) && !/\.(png|jpe?g|gif|webp|svg)$/i.test(t.name || "") ? o("Unsupported image format.", "不支持该图片格式。") : t.size > g.maxImageSize ? o("File exceeds the size limit.", "图片超过大小限制。") : "", z = (t, e) => {
|
|
89
|
+
const l = typeof t == "number" ? { progress: t } : t || {};
|
|
90
|
+
r.value = { name: l.name || l.file?.name || e?.name || o("Images", "图片"), progress: Math.max(0, Math.min(100, Number(l.progress) || 0)), status: l.status || "uploading", error: "" };
|
|
91
|
+
}, ee = (t) => {
|
|
92
|
+
const e = t.flatMap(({ url: l, alt: p }) => [{ type: "image", attrs: { src: l, alt: p || "image" } }, { type: "paragraph" }]);
|
|
93
|
+
e.length && a.value?.chain().focus().insertContent(e).run();
|
|
94
|
+
}, te = async (t) => {
|
|
95
|
+
const e = Array.from(t.target.files || []);
|
|
96
|
+
if (t.target.value = "", !e.length || !g.uploadImages) return;
|
|
97
|
+
const l = [];
|
|
98
|
+
for (const p of e) {
|
|
99
|
+
const b = Y(p);
|
|
100
|
+
if (b) {
|
|
101
|
+
const y = new Error(b);
|
|
102
|
+
r.value = { name: p.name, progress: 0, status: "error", error: b }, $("upload-error", y);
|
|
103
|
+
} else l.push(p);
|
|
104
|
+
}
|
|
105
|
+
if (l.length) {
|
|
106
|
+
z(0, l[0]);
|
|
107
|
+
try {
|
|
108
|
+
const p = await g.uploadImages(l, (y) => z(y, l[0])), b = Array.isArray(p) ? p.filter((y) => y?.url) : [];
|
|
109
|
+
if (!b.length) throw new Error(o("The upload returned no image URLs.", "上传结果中没有可用的图片地址。"));
|
|
110
|
+
ee(b), r.value = { name: o("{count} images", "{count} 张图片", { count: b.length }), progress: 100, status: "success", error: "" }, $("upload-complete", b);
|
|
111
|
+
} catch (p) {
|
|
112
|
+
r.value = { name: r.value?.name || l[0].name, progress: r.value?.progress || 0, status: "error", error: p?.message || String(p) }, $("upload-error", p);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}, F = (t) => {
|
|
116
|
+
x.value?.open && !x.value.contains(t.target) && U();
|
|
117
|
+
};
|
|
118
|
+
return ne(() => g.modelValue, (t) => {
|
|
119
|
+
if (t !== E.value && (f.value = t || ""), !(!a.value || t === E.value)) {
|
|
120
|
+
if (B(t)) {
|
|
121
|
+
s.value = !0;
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
s.value || a.value.commands.setContent(t || "", { contentType: "markdown", emitUpdate: !1 }), A();
|
|
125
|
+
}
|
|
126
|
+
}), le(() => {
|
|
127
|
+
document.addEventListener("pointerdown", F);
|
|
128
|
+
try {
|
|
129
|
+
s.value = B(g.modelValue), a.value = new re({ extensions: [de.configure({ heading: { levels: [1, 2, 3, 4, 5, 6] } }), ce.configure({ allowBase64: !1 }), pe, ge.configure({ placeholder: g.placeholder || o("Start writing...", "开始输入正文…") }), ve], content: g.modelValue || "", contentType: "markdown", editorProps: { attributes: { class: "nono-rich-editor__prosemirror", "aria-label": g.placeholder || o("Rich text editor", "富文本编辑器") } }, onUpdate: ({ editor: t }) => {
|
|
130
|
+
N(t.getMarkdown()), A();
|
|
131
|
+
}, onSelectionUpdate: A, onFocus: () => {
|
|
132
|
+
T.value = !0;
|
|
133
|
+
}, onBlur: () => {
|
|
134
|
+
T.value = !1;
|
|
135
|
+
} });
|
|
136
|
+
} catch (t) {
|
|
137
|
+
L.value = o("The rich text editor could not start.", "富文本编辑器未能启动。"), console.error("Failed to initialize editor", t);
|
|
138
|
+
}
|
|
139
|
+
}), ae(() => {
|
|
140
|
+
document.removeEventListener("pointerdown", F), a.value?.destroy(), a.value = null;
|
|
141
|
+
}), (t, e) => (u(), d("div", {
|
|
142
|
+
class: v(["nono-rich-editor", { "nono-rich-editor--fill": h.fill }])
|
|
143
|
+
}, [
|
|
144
|
+
n("div", {
|
|
145
|
+
class: v(["nono-rich-editor__shell", { "is-focused": T.value }])
|
|
146
|
+
}, [
|
|
147
|
+
a.value ? (u(), d("div", {
|
|
148
|
+
key: 0,
|
|
149
|
+
class: "nono-rich-editor__toolbar",
|
|
150
|
+
role: "toolbar",
|
|
151
|
+
"aria-label": o("Formatting tools", "格式工具")
|
|
152
|
+
}, [
|
|
153
|
+
s.value ? m("", !0) : (u(), d("div", he, [
|
|
154
|
+
n("button", {
|
|
155
|
+
type: "button",
|
|
156
|
+
class: "nono-rich-editor__button",
|
|
157
|
+
disabled: !q.value,
|
|
158
|
+
title: o("Undo", "撤销"),
|
|
159
|
+
onClick: e[0] || (e[0] = (l) => w("undo"))
|
|
160
|
+
}, [
|
|
161
|
+
e[18] || (e[18] = C("↶ ", -1)),
|
|
162
|
+
n("span", null, i(o("Undo", "撤销")), 1)
|
|
163
|
+
], 8, be),
|
|
164
|
+
n("button", {
|
|
165
|
+
type: "button",
|
|
166
|
+
class: "nono-rich-editor__button",
|
|
167
|
+
disabled: !K.value,
|
|
168
|
+
title: o("Redo", "重做"),
|
|
169
|
+
onClick: e[1] || (e[1] = (l) => w("redo"))
|
|
170
|
+
}, [
|
|
171
|
+
e[19] || (e[19] = C("↷ ", -1)),
|
|
172
|
+
n("span", null, i(o("Redo", "重做")), 1)
|
|
173
|
+
], 8, _e)
|
|
174
|
+
])),
|
|
175
|
+
s.value ? m("", !0) : (u(), d("div", fe, [
|
|
176
|
+
n("select", {
|
|
177
|
+
class: "nono-rich-editor__select",
|
|
178
|
+
"aria-label": o("Text style", "文字样式"),
|
|
179
|
+
value: Z.value,
|
|
180
|
+
onChange: O
|
|
181
|
+
}, [
|
|
182
|
+
n("option", ke, i(o("Paragraph", "正文")), 1),
|
|
183
|
+
n("option", we, i(o("Heading 2", "标题 2")), 1),
|
|
184
|
+
n("option", Ce, i(o("Heading 3", "标题 3")), 1),
|
|
185
|
+
n("option", $e, i(o("Heading 4", "标题 4")), 1)
|
|
186
|
+
], 40, ye)
|
|
187
|
+
])),
|
|
188
|
+
s.value ? m("", !0) : (u(), d("div", Me, [
|
|
189
|
+
n("button", {
|
|
190
|
+
type: "button",
|
|
191
|
+
class: v(["nono-rich-editor__icon is-bold", { "is-active": c("bold") }]),
|
|
192
|
+
title: o("Bold", "粗体"),
|
|
193
|
+
onClick: e[2] || (e[2] = (l) => I("bold"))
|
|
194
|
+
}, "B", 10, Te),
|
|
195
|
+
n("button", {
|
|
196
|
+
type: "button",
|
|
197
|
+
class: v(["nono-rich-editor__icon is-italic", { "is-active": c("italic") }]),
|
|
198
|
+
title: o("Italic", "斜体"),
|
|
199
|
+
onClick: e[3] || (e[3] = (l) => I("italic"))
|
|
200
|
+
}, "I", 10, xe),
|
|
201
|
+
n("button", {
|
|
202
|
+
type: "button",
|
|
203
|
+
class: v(["nono-rich-editor__icon is-strike", { "is-active": c("strike") }]),
|
|
204
|
+
title: o("Strikethrough", "删除线"),
|
|
205
|
+
onClick: e[4] || (e[4] = (l) => I("strike"))
|
|
206
|
+
}, "S", 10, Se)
|
|
207
|
+
])),
|
|
208
|
+
s.value ? m("", !0) : (u(), d("div", Le, [
|
|
209
|
+
n("details", {
|
|
210
|
+
ref_key: "tableMenu",
|
|
211
|
+
ref: x,
|
|
212
|
+
class: "nono-rich-editor__table-menu"
|
|
213
|
+
}, [
|
|
214
|
+
n("summary", {
|
|
215
|
+
class: v(["nono-rich-editor__button", { "is-active": c("table") }])
|
|
216
|
+
}, [
|
|
217
|
+
e[20] || (e[20] = C("▦ ", -1)),
|
|
218
|
+
n("span", null, i(o("Table", "表格")), 1),
|
|
219
|
+
e[21] || (e[21] = C("⌄", -1))
|
|
220
|
+
], 2),
|
|
221
|
+
n("div", {
|
|
222
|
+
class: "nono-rich-editor__table-panel",
|
|
223
|
+
role: "menu",
|
|
224
|
+
"aria-label": o("Table tools", "表格工具")
|
|
225
|
+
}, [
|
|
226
|
+
n("button", {
|
|
227
|
+
type: "button",
|
|
228
|
+
class: "nono-rich-editor__table-action is-wide",
|
|
229
|
+
disabled: c("table"),
|
|
230
|
+
onClick: G
|
|
231
|
+
}, "▦ " + i(o("Insert 3 × 3 table", "插入 3 × 3 表格")), 9, Ie),
|
|
232
|
+
e[22] || (e[22] = n("div", { class: "nono-rich-editor__divider" }, null, -1)),
|
|
233
|
+
n("div", Be, [
|
|
234
|
+
n("button", {
|
|
235
|
+
type: "button",
|
|
236
|
+
class: "nono-rich-editor__table-action",
|
|
237
|
+
disabled: !c("table"),
|
|
238
|
+
onClick: e[5] || (e[5] = (l) => S("addRowAfter"))
|
|
239
|
+
}, "+ " + i(o("Add row", "增加行")), 9, Ee),
|
|
240
|
+
n("button", {
|
|
241
|
+
type: "button",
|
|
242
|
+
class: "nono-rich-editor__table-action",
|
|
243
|
+
disabled: !c("table"),
|
|
244
|
+
onClick: e[6] || (e[6] = (l) => S("deleteRow"))
|
|
245
|
+
}, "- " + i(o("Delete row", "删除行")), 9, Ue),
|
|
246
|
+
n("button", {
|
|
247
|
+
type: "button",
|
|
248
|
+
class: "nono-rich-editor__table-action",
|
|
249
|
+
disabled: !c("table"),
|
|
250
|
+
onClick: e[7] || (e[7] = (l) => S("addColumnAfter"))
|
|
251
|
+
}, "+ " + i(o("Add column", "增加列")), 9, Ve),
|
|
252
|
+
n("button", {
|
|
253
|
+
type: "button",
|
|
254
|
+
class: "nono-rich-editor__table-action",
|
|
255
|
+
disabled: !c("table"),
|
|
256
|
+
onClick: e[8] || (e[8] = (l) => S("deleteColumn"))
|
|
257
|
+
}, "- " + i(o("Delete column", "删除列")), 9, He)
|
|
258
|
+
]),
|
|
259
|
+
n("button", {
|
|
260
|
+
type: "button",
|
|
261
|
+
class: "nono-rich-editor__table-action is-wide is-danger",
|
|
262
|
+
disabled: !c("table"),
|
|
263
|
+
onClick: e[9] || (e[9] = (l) => S("deleteTable"))
|
|
264
|
+
}, "× " + i(o("Delete table", "删除整个表格")), 9, Re)
|
|
265
|
+
], 8, Ae)
|
|
266
|
+
], 512)
|
|
267
|
+
])),
|
|
268
|
+
s.value ? m("", !0) : (u(), d("div", Ne, [
|
|
269
|
+
n("button", {
|
|
270
|
+
type: "button",
|
|
271
|
+
class: v(["nono-rich-editor__icon", { "is-active": c("bulletList") }]),
|
|
272
|
+
title: o("Bullet list", "项目列表"),
|
|
273
|
+
onClick: e[10] || (e[10] = (l) => w("toggleBulletList"))
|
|
274
|
+
}, "•", 10, ze),
|
|
275
|
+
n("button", {
|
|
276
|
+
type: "button",
|
|
277
|
+
class: v(["nono-rich-editor__icon is-small", { "is-active": c("orderedList") }]),
|
|
278
|
+
title: o("Numbered list", "编号列表"),
|
|
279
|
+
onClick: e[11] || (e[11] = (l) => w("toggleOrderedList"))
|
|
280
|
+
}, "1.", 10, Fe),
|
|
281
|
+
n("button", {
|
|
282
|
+
type: "button",
|
|
283
|
+
class: v(["nono-rich-editor__icon", { "is-active": c("blockquote") }]),
|
|
284
|
+
title: o("Quote", "引用"),
|
|
285
|
+
onClick: e[12] || (e[12] = (l) => w("toggleBlockquote"))
|
|
286
|
+
}, "“", 10, Pe),
|
|
287
|
+
n("button", {
|
|
288
|
+
type: "button",
|
|
289
|
+
class: v(["nono-rich-editor__icon is-code", { "is-active": c("code") }]),
|
|
290
|
+
title: o("Inline code", "行内代码"),
|
|
291
|
+
onClick: e[13] || (e[13] = (l) => I("code"))
|
|
292
|
+
}, "</>", 10, De)
|
|
293
|
+
])),
|
|
294
|
+
s.value ? m("", !0) : (u(), d("div", je, [
|
|
295
|
+
n("button", {
|
|
296
|
+
type: "button",
|
|
297
|
+
class: v(["nono-rich-editor__button", { "is-active": c("link") }]),
|
|
298
|
+
onClick: Q
|
|
299
|
+
}, [
|
|
300
|
+
e[23] || (e[23] = C("↗ ", -1)),
|
|
301
|
+
n("span", null, i(o("Link", "链接")), 1)
|
|
302
|
+
], 2),
|
|
303
|
+
h.uploadImages ? (u(), d("label", {
|
|
304
|
+
key: 0,
|
|
305
|
+
class: v(["nono-rich-editor__button nono-rich-editor__upload", { "is-disabled": H.value }])
|
|
306
|
+
}, [
|
|
307
|
+
e[24] || (e[24] = C("↑ ", -1)),
|
|
308
|
+
n("span", null, i(H.value ? W.value : o("Upload", "上传")), 1),
|
|
309
|
+
n("input", {
|
|
310
|
+
type: "file",
|
|
311
|
+
accept: h.imageAccept,
|
|
312
|
+
multiple: "",
|
|
313
|
+
onChange: te
|
|
314
|
+
}, null, 40, qe)
|
|
315
|
+
], 2)) : m("", !0)
|
|
316
|
+
])),
|
|
317
|
+
n("div", Ke, [
|
|
318
|
+
n("button", {
|
|
319
|
+
type: "button",
|
|
320
|
+
class: v(["nono-rich-editor__button", { "is-active": s.value }]),
|
|
321
|
+
"aria-pressed": s.value,
|
|
322
|
+
onClick: J
|
|
323
|
+
}, [
|
|
324
|
+
e[25] || (e[25] = C("MD ", -1)),
|
|
325
|
+
n("span", null, i(s.value ? o("Visual", "可视化") : o("Source", "源码")), 1)
|
|
326
|
+
], 10, Ze)
|
|
327
|
+
])
|
|
328
|
+
], 8, me)) : m("", !0),
|
|
329
|
+
D.value ? (u(), d("div", Ge, i(o("This content contains HTML and is kept in source mode to prevent formatting loss.", "此内容包含 HTML,为避免格式丢失,已使用源码模式。移除 HTML 后才能切换到可视化编辑。")), 1)) : m("", !0),
|
|
330
|
+
r.value ? (u(), d("div", Oe, [
|
|
331
|
+
n("div", null, [
|
|
332
|
+
n("strong", null, i(r.value.name), 1),
|
|
333
|
+
n("span", {
|
|
334
|
+
class: v(`is-${r.value.status}`)
|
|
335
|
+
}, i(X.value), 3)
|
|
336
|
+
]),
|
|
337
|
+
n("div", Qe, [
|
|
338
|
+
n("i", {
|
|
339
|
+
class: v(`is-${r.value.status}`),
|
|
340
|
+
style: V({ width: `${r.value.progress}%` })
|
|
341
|
+
}, null, 6)
|
|
342
|
+
]),
|
|
343
|
+
r.value.error ? (u(), d("p", Je, i(r.value.error), 1)) : m("", !0)
|
|
344
|
+
])) : m("", !0),
|
|
345
|
+
L.value ? (u(), d("div", We, [
|
|
346
|
+
n("textarea", {
|
|
347
|
+
value: h.modelValue,
|
|
348
|
+
placeholder: h.placeholder,
|
|
349
|
+
onInput: e[14] || (e[14] = (l) => $("update:modelValue", l.target.value))
|
|
350
|
+
}, null, 40, Xe),
|
|
351
|
+
n("p", null, i(L.value), 1)
|
|
352
|
+
])) : s.value ? (u(), d("textarea", {
|
|
353
|
+
key: 4,
|
|
354
|
+
value: f.value,
|
|
355
|
+
placeholder: h.placeholder,
|
|
356
|
+
class: "nono-rich-editor__source",
|
|
357
|
+
style: V({ "--nono-editor-min-height": R.value }),
|
|
358
|
+
spellcheck: "false",
|
|
359
|
+
onInput: e[15] || (e[15] = (l) => N(l.target.value)),
|
|
360
|
+
onFocus: e[16] || (e[16] = (l) => T.value = !0),
|
|
361
|
+
onBlur: e[17] || (e[17] = (l) => T.value = !1)
|
|
362
|
+
}, null, 44, Ye)) : (u(), ie(se(ue), {
|
|
363
|
+
key: 5,
|
|
364
|
+
editor: a.value,
|
|
365
|
+
class: "nono-rich-editor__content",
|
|
366
|
+
style: V({ "--nono-editor-min-height": R.value })
|
|
367
|
+
}, null, 8, ["editor", "style"])),
|
|
368
|
+
a.value && !L.value ? (u(), d("div", et, [
|
|
369
|
+
n("span", null, i(o("{count} characters", "{count} 个字符", { count: j.value })), 1),
|
|
370
|
+
n("span", null, i(s.value ? o("Markdown source", "Markdown 源码") : o("Markdown compatible", "兼容 Markdown")), 1)
|
|
371
|
+
])) : m("", !0)
|
|
372
|
+
], 2),
|
|
373
|
+
h.help ? (u(), d("p", tt, i(h.help), 1)) : m("", !0)
|
|
374
|
+
], 2));
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
export {
|
|
378
|
+
dt as NonoEditor,
|
|
379
|
+
dt as NonoMarkdownEditor,
|
|
380
|
+
B as containsRawHtml,
|
|
381
|
+
dt as default
|
|
382
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nonoim/editor",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A lightweight Vue 3 visual Markdown editor built on Tiptap.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"main": "./dist/editor.cjs",
|
|
8
|
+
"module": "./dist/editor.js",
|
|
9
|
+
"types": "./src/index.d.ts",
|
|
10
|
+
"files": ["dist", "src/index.d.ts", "README.md", "LICENSE"],
|
|
11
|
+
"exports": {
|
|
12
|
+
".": { "types": "./src/index.d.ts", "import": "./dist/editor.js", "require": "./dist/editor.cjs" },
|
|
13
|
+
"./style.css": "./dist/editor.css"
|
|
14
|
+
},
|
|
15
|
+
"sideEffects": ["**/*.css"],
|
|
16
|
+
"scripts": { "build": "vite build", "test": "node --test", "prepack": "npm test && npm run build" },
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@tiptap/core": "3.28.0",
|
|
19
|
+
"@tiptap/extension-image": "3.28.0",
|
|
20
|
+
"@tiptap/extension-table": "3.28.0",
|
|
21
|
+
"@tiptap/extensions": "3.28.0",
|
|
22
|
+
"@tiptap/markdown": "3.28.0",
|
|
23
|
+
"@tiptap/pm": "3.28.0",
|
|
24
|
+
"@tiptap/starter-kit": "3.28.0",
|
|
25
|
+
"@tiptap/vue-3": "3.28.0"
|
|
26
|
+
},
|
|
27
|
+
"peerDependencies": { "vue": "^3.5.0" },
|
|
28
|
+
"devDependencies": { "@vitejs/plugin-vue": "^6.0.3", "vite": "^7.3.1", "vue": "^3.5.39" },
|
|
29
|
+
"publishConfig": { "access": "public" },
|
|
30
|
+
"engines": { "node": ">=20.19.0" }
|
|
31
|
+
}
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { DefineComponent } from 'vue';
|
|
2
|
+
export type EditorImage = { url: string; alt?: string };
|
|
3
|
+
export type UploadProgress = { file?: File; name?: string; progress?: number; status?: string } | number;
|
|
4
|
+
export type UploadImages = (files: File[], onProgress: (progress: UploadProgress) => void) => Promise<EditorImage[]>;
|
|
5
|
+
export const NonoEditor: DefineComponent<{ modelValue?: string; placeholder?: string; rows?: number; help?: string; fill?: boolean; locale?: string; uploadImages?: UploadImages | null; imageAccept?: string; maxImageSize?: number }>;
|
|
6
|
+
export const NonoMarkdownEditor: typeof NonoEditor;
|
|
7
|
+
export function containsRawHtml(value?: string): boolean;
|
|
8
|
+
export default NonoEditor;
|