@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 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.
@@ -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;
@@ -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;