feat(creator-studio): bottom status bar, editor-rendered math/footnote, code language picker, block delete

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
kate522andClaude Opus 5 committed 2026-11-07 00:38:39 +08:00
1 parent 6928127bfa
commit 4a119358ca
11 files changed
+754 -63

No files matched your search

+1 -1
View File
@@ -1 +1 @@
<?php return array('dependencies' => array('react', 'wp-element', 'wp-i18n'), 'version' => 'b411c17006fd7ed53e70');
<?php return array('dependencies' => array('react', 'wp-element', 'wp-i18n'), 'version' => '5aca6e340eab7503b9b9');
+1 -1
View File
@@ -1 +1 @@
.jio-page-submit{background:#f6f6f6}body.jio-page-submit.has-static-nav,body.jio-page-submit.nav-fixed{padding-top:0!important}.jio-page-submit .float-btn,.jio-page-submit .footer,.jio-page-submit .footer-tabbar,.jio-page-submit .header,.jio-page-submit .jio-float-buttons{display:none!important}.jio-page-submit #main-content.container{max-width:none;padding:0}#jio-creator-studio.is-ready{display:block}.jio-creator{--jc-ink:#2b2b2b;--jc-ink-soft:#7d8683;--jc-paper:#f6f6f6;--jc-line:#e6e9e8;--jc-accent:#267665;--jc-accent-strong:#1d5f52;--jc-warn:#b4573f;--jc-bar:64px;-webkit-font-smoothing:antialiased;background:var(--jc-paper);color:var(--jc-ink);font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Noto Sans SC,PingFang SC,Microsoft YaHei,sans-serif;min-height:100vh;min-height:100dvh;padding-top:var(--jc-bar)}.jio-creator *,.jio-creator :after,.jio-creator :before{box-sizing:border-box}.jio-creator button{color:inherit;font:inherit}.jio-creator__bar{align-items:center;background:rgba(250,251,250,.98);border-bottom:1px solid var(--jc-line);box-shadow:0 2px 8px rgba(27,46,39,.035);display:flex;gap:clamp(10px,1.7vw,24px);height:var(--jc-bar);left:0;padding:0 clamp(14px,2.4vw,30px);position:fixed;right:0;top:0;z-index:300}.jio-creator__bar-actions,.jio-creator__bar-left{align-items:center;display:flex;flex:0 0 auto;gap:9px;min-width:0}.jio-creator__bar-left{border-right:1px solid var(--jc-line);padding-right:clamp(10px,1.5vw,20px)}.jio-creator__bar-actions{gap:7px;margin-left:auto}.jio-creator__bar-brand{color:var(--jc-accent-strong);font-family:Songti SC,"Noto Serif SC","Source Han Serif SC",serif;font-size:17px;font-weight:700;letter-spacing:-.04em}.jio-creator__bar-divider{background:var(--jc-line);height:18px;width:1px}.jio-creator__bar-mode{color:var(--jc-ink-soft);font-size:13px;white-space:nowrap}.jio-creator__back{align-items:center;border-radius:7px;color:var(--jc-ink-soft);display:inline-flex;height:30px;justify-content:center;text-decoration:none;width:30px}.jio-creator__back:focus-visible,.jio-creator__back:hover{background:#edf3f0;color:var(--jc-accent);outline:0}.jio-creator__toolbar-tools{align-items:center;display:flex;flex:1 1 auto;gap:7px;min-width:0;overflow-x:auto;scrollbar-width:none}.jio-creator__toolbar-tools::-webkit-scrollbar{display:none}.jio-creator__toolbar-group{align-items:center;display:inline-flex;flex:0 0 auto;gap:2px}.jio-creator__toolbar-divider{background:var(--jc-line);flex:0 0 auto;height:22px;width:1px}.jio-creator__tool-btn{align-items:center;background:transparent;border:0;border-radius:6px;color:#5f6664;cursor:pointer;display:inline-flex;flex:0 0 auto;flex-direction:column;font-size:11px;gap:2px;height:46px;justify-content:center;line-height:1.2;min-width:40px;padding:5px 6px;width:auto}.jio-creator__tool-btn i{font-size:14px;line-height:1}.jio-creator__tool-btn:focus-visible:not(:disabled),.jio-creator__tool-btn:hover:not(:disabled){background:#f0f2f1;color:var(--jc-accent-strong);outline:0}.jio-creator__tool-btn:disabled{color:#c7d0cc;cursor:default}.jio-creator__tool-label{max-width:52px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.jio-creator__format-field{align-items:center;border-radius:6px;cursor:pointer;display:inline-flex;flex:0 0 auto;flex-direction:column;gap:2px;height:46px;justify-content:center;min-width:48px;padding:5px 6px;position:relative;width:auto}.jio-creator__format-field i{font-size:14px;line-height:1}.jio-creator__format-field:focus-within,.jio-creator__format-field:hover{background:#f0f2f1;color:var(--jc-accent-strong)}.jio-creator__format-field:focus-within{outline:2px solid rgba(38,118,101,.35);outline-offset:-2px}.jio-creator__format-select{background:transparent;border:0;color:inherit;cursor:pointer;font:inherit;height:100%;inset:0;opacity:0;padding:0;position:absolute;width:100%}.jio-creator__format-select:disabled{cursor:default}.jio-creator__save-draft{align-items:center;background:#fff;border:1px solid var(--jc-line);border-radius:7px;color:var(--jc-ink-soft);cursor:pointer;display:inline-flex;flex:0 0 auto;font-size:13px;gap:7px;justify-content:center;min-height:34px;padding:0 10px;white-space:nowrap}.jio-creator__save-draft:focus-visible:not(:disabled),.jio-creator__save-draft:hover:not(:disabled){background:#f7faf8;border-color:#b7cdc3;color:var(--jc-accent-strong);outline:0}.jio-creator__save-draft:disabled{cursor:default;opacity:.55}.jio-creator__save{align-items:center;color:var(--jc-ink-soft);display:inline-flex;flex:0 0 auto;font-size:12px;gap:6px;white-space:nowrap}.jio-creator__save>i{color:var(--jc-line);font-size:7px}.jio-creator__save time{font-variant-numeric:tabular-nums;opacity:.7}.jio-creator__save.is-saved>i{color:var(--jc-accent)}.jio-creator__save.is-conflict,.jio-creator__save.is-conflict>i,.jio-creator__save.is-error,.jio-creator__save.is-error>i{color:var(--jc-warn)}.jio-creator__btn,.jio-creator__ghost,.jio-creator__primary{align-items:center;border:1px solid transparent;border-radius:7px;cursor:poLine truncated
.jio-page-submit{background:#f6f6f6}body.jio-page-submit.has-static-nav,body.jio-page-submit.nav-fixed{padding-top:0!important}.jio-page-submit .float-btn,.jio-page-submit .footer,.jio-page-submit .footer-tabbar,.jio-page-submit .header,.jio-page-submit .jio-float-buttons{display:none!important}.jio-page-submit #main-content.container{max-width:none;padding:0}#jio-creator-studio.is-ready{display:block}.jio-creator{--jc-ink:#2b2b2b;--jc-ink-soft:#7d8683;--jc-paper:#f6f6f6;--jc-line:#e6e9e8;--jc-accent:#267665;--jc-accent-strong:#1d5f52;--jc-warn:#b4573f;--jc-bar:64px;--jc-status:52px;-webkit-font-smoothing:antialiased;background:var(--jc-paper);color:var(--jc-ink);font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Noto Sans SC,PingFang SC,Microsoft YaHei,sans-serif;min-height:100vh;min-height:100dvh;padding-bottom:var(--jc-status);padding-top:var(--jc-bar)}.jio-creator *,.jio-creator :after,.jio-creator :before{box-sizing:border-box}.jio-creator button{color:inherit;font:inherit}.jio-creator__bar{align-items:center;background:rgba(250,251,250,.98);border-bottom:1px solid var(--jc-line);box-shadow:0 2px 8px rgba(27,46,39,.035);display:flex;gap:clamp(10px,1.7vw,24px);height:var(--jc-bar);left:0;padding:0 clamp(14px,2.4vw,30px);position:fixed;right:0;top:0;z-index:300}.jio-creator__bar-actions,.jio-creator__bar-left{align-items:center;display:flex;flex:0 0 auto;gap:9px;min-width:0}.jio-creator__bar-left{border-right:1px solid var(--jc-line);padding-right:clamp(10px,1.5vw,20px)}.jio-creator__bar-actions{gap:7px;margin-left:auto}.jio-creator__bar-brand{color:var(--jc-accent-strong);font-family:Songti SC,"Noto Serif SC","Source Han Serif SC",serif;font-size:17px;font-weight:700;letter-spacing:-.04em}.jio-creator__bar-divider{background:var(--jc-line);height:18px;width:1px}.jio-creator__bar-mode{color:var(--jc-ink-soft);font-size:13px;white-space:nowrap}.jio-creator__back{align-items:center;border-radius:7px;color:var(--jc-ink-soft);display:inline-flex;height:30px;justify-content:center;text-decoration:none;width:30px}.jio-creator__back:focus-visible,.jio-creator__back:hover{background:#edf3f0;color:var(--jc-accent);outline:0}.jio-creator__toolbar-tools{align-items:center;display:flex;flex:1 1 auto;gap:7px;min-width:0;overflow-x:auto;scrollbar-width:none}.jio-creator__toolbar-tools::-webkit-scrollbar{display:none}.jio-creator__toolbar-group{align-items:center;display:inline-flex;flex:0 0 auto;gap:2px}.jio-creator__toolbar-divider{background:var(--jc-line);flex:0 0 auto;height:22px;width:1px}.jio-creator__tool-btn{align-items:center;background:transparent;border:0;border-radius:6px;color:#5f6664;cursor:pointer;display:inline-flex;flex:0 0 auto;flex-direction:column;font-size:11px;gap:2px;height:46px;justify-content:center;line-height:1.2;min-width:40px;padding:5px 6px;width:auto}.jio-creator__tool-btn i{font-size:14px;line-height:1}.jio-creator__tool-btn:focus-visible:not(:disabled),.jio-creator__tool-btn:hover:not(:disabled){background:#f0f2f1;color:var(--jc-accent-strong);outline:0}.jio-creator__tool-btn:disabled{color:#c7d0cc;cursor:default}.jio-creator__tool-label{max-width:52px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.jio-creator__format-field{align-items:center;background:transparent;border:0;border-radius:6px;color:#5f6664;cursor:pointer;display:inline-flex;flex:0 0 auto;flex-direction:column;font-size:11px;gap:2px;height:46px;justify-content:center;line-height:1.2;min-width:40px;padding:5px 6px;position:relative;vertical-align:middle;width:auto}.jio-creator__format-field i{font-size:14px;line-height:1}.jio-creator__format-field:focus-within,.jio-creator__format-field:hover{background:#f0f2f1;color:var(--jc-accent-strong)}.jio-creator__format-field:focus-within{outline:2px solid rgba(38,118,101,.35);outline-offset:-2px}.jio-creator__format-select{background:transparent;border:0;color:inherit;cursor:pointer;font:inherit;height:100%;inset:0;opacity:0;padding:0;position:absolute;width:100%}.jio-creator__format-select:disabled{cursor:default}.jio-creator__save-draft{align-items:center;background:#fff;border:1px solid var(--jc-line);border-radius:7px;color:var(--jc-ink-soft);cursor:pointer;display:inline-flex;flex:0 0 auto;font-size:13px;gap:7px;justify-content:center;min-height:34px;padding:0 10px;white-space:nowrap}.jio-creator__save-draft:focus-visible:not(:disabled),.jio-creator__save-draft:hover:not(:disabled){background:#f7faf8;border-color:#b7cdc3;color:var(--jc-accent-strong);outline:0}.jio-creator__save-draft:disabled{cursor:default;opacity:.55}.jio-creator__save{align-items:center;color:var(--jc-ink-soft);display:inline-flex;flex:0 0 auto;font-size:12px;gap:6px;white-space:nowrap}.jio-creator__save>i{color:var(--jc-line);font-size:7px}.jio-creator__save time{font-variant-numeric:tabular-nums;opacity:.7}.jio-creator__save.is-saved>i{color:var(--jc-accent)}.jio-creator__save.is-conflict,.jio-creator__save.is-conflict>i,.jio-creator__save.is-error,.jio-creator__save.is-error>i{color:vaLine truncated
+8 -3
View File
@@ -1,3 +1,8 @@
(()=>{"use strict";const e=window.React,t=window.wp.element,a=window.wp.i18n;function n(e,t){const a=e&&(e.message||e.error)||(401===t?"登录状态已失效,请刷新页面重新登录。":"请求失败,请稍后重试。"),n=new Error(String(a));return n.status=t,n.code=e&&e.code?String(e.code):"",n.data=e,n}async function o(e){try{return await e.json()}catch(e){return null}}async function r(e,t,a){const r=await fetch(e,{method:"POST",credentials:"same-origin",headers:{"Content-Type":"application/json","X-WP-Nonce":a},body:JSON.stringify(t)}),l=await o(r);if(!r.ok)throw n(l,r.status);return l}async function l(e,t,a,r={}){const l=new FormData;l.append("file",t),Object.entries(r).forEach(([e,t])=>l.append(e,t));const i=await fetch(e,{method:"POST",credentials:"same-origin",headers:{"X-WP-Nonce":a},body:l}),c=await o(i);if(!i.ok)throw n(c,i.status);return c}const i=window.jioCreatorStudio||{},c=i.nonce||"";function s(n){const{draft:o,setDraft:r,capabilities:s,limits:u,taxonomy:d,isPublishedEdit:m,onClose:p,onSaveDraft:_,onSubmit:f,privateStorage:b,privateUploads:g,saving:h,captcha:j}=n,[y,E]=(0,t.useState)(""),[v,k]=(0,t.useState)(""),[N,w]=(0,t.useState)(!1),C=(0,t.useRef)(null),S=(0,t.useRef)(null);(0,t.useEffect)(()=>{const e=S.current,t=document.getElementById("jio-creator-captcha");if(!e||!t)return;const a=t.parentElement;return e.appendChild(t),t.removeAttribute("hidden"),()=>{a&&(a.appendChild(t),t.setAttribute("hidden",""))}},[]);const x=(0,t.useMemo)(()=>(o.title||"").trim().length,[o.title])>=Number(u.titleMin||1),q=()=>{const e=y.trim();if(!e)return;const t=e.split(/[,,\n]+/u).map(e=>e.trim()).filter(Boolean);r({tags:Array.from(new Set([...o.tags,...t]))}),E("")};return(0,e.createElement)("div",{className:"jio-creator-drawer",role:"dialog","aria-modal":"true","aria-labelledby":"jio-creator-publish-title"},(0,e.createElement)("button",{type:"button",className:"jio-creator-drawer__backdrop","aria-label":(0,a.__)("关闭","jio"),onClick:p}),(0,e.createElement)("div",{className:"jio-creator-drawer__panel"},(0,e.createElement)("header",{className:"jio-creator-drawer__head"},(0,e.createElement)("h2",{id:"jio-creator-publish-title"},m?(0,a.__)("提交改稿","jio"):(0,a.__)("发布文章","jio")),(0,e.createElement)("button",{type:"button",className:"jio-creator-drawer__close",onClick:p,"aria-label":(0,a.__)("关闭","jio")},(0,e.createElement)("i",{className:"fa fas fa-times","aria-hidden":"true"}))),(0,e.createElement)("div",{className:"jio-creator-drawer__body"},m&&(0,e.createElement)("p",{className:"jio-creator-drawer__note"},(0,a.__)("审核通过前,读者看到的仍然是当前线上版本。","jio")),(0,e.createElement)("section",{className:"jio-creator-field"},(0,e.createElement)("h3",null,(0,a.__)("分类","jio")),(0,e.createElement)("div",{className:"jio-creator-chips"},0===d.categories.length&&(0,e.createElement)("span",{className:"jio-creator-muted"},(0,a.__)("暂无可选分类","jio")),d.categories.map(t=>(0,e.createElement)("label",{key:t.id,className:"jio-creator-chip"+(o.categories.includes(t.id)?" is-active":"")},(0,e.createElement)("input",{type:"checkbox",checked:o.categories.includes(t.id),onChange:()=>(e=>{const t=o.categories.includes(e);r({categories:t?o.categories.filter(t=>t!==e):[...o.categories,e]})})(t.id)}),(0,e.createElement)("span",null,t.name))))),d.tags&&(0,e.createElement)("section",{className:"jio-creator-field"},(0,e.createElement)("h3",null,(0,a.__)("标签","jio")),(0,e.createElement)("div",{className:"jio-creator-chips"},o.tags.map(t=>(0,e.createElement)("span",{key:t,className:"jio-creator-chip is-active"},(0,e.createElement)("span",null,t),(0,e.createElement)("button",{type:"button",onClick:()=>r({tags:o.tags.filter(e=>e!==t)}),"aria-label":(0,a.sprintf)((0,a.__)("移除标签 %s","jio"),t)},(0,e.createElement)("i",{className:"fa fas fa-times","aria-hidden":"true"}))))),(0,e.createElement)("div",{className:"jio-creator-inline"},(0,e.createElement)("input",{type:"text",value:y,placeholder:(0,a.__)("输入标签,逗号分隔","jio"),onChange:e=>E(e.target.value),onKeyDown:e=>{"Enter"===e.key&&(e.preventDefault(),q())}}),(0,e.createElement)("button",{type:"button",onClick:q},(0,a.__)("添加","jio")))),d.customDefs.map(t=>(0,e.createElement)("section",{className:"jio-creator-field",key:t.key},(0,e.createElement)("h3",null,t.name),(0,e.createElement)("div",{className:"jio-creator-chips"},t.values.map(a=>{const n=o.custom[t.key]||[],l=n.includes(a.key);return(0,e.createElement)("label",{key:a.key,className:"jio-creator-chip"+(l?" is-active":"")},(0,e.createElement)("input",{type:"checkbox",checked:l,onChange:()=>r({custom:{...o.custom,[t.key]:l?n.filter(e=>e!==a.key):[...n,a.key]}})}),(0,e.createElement)("span",null,a.name))})))),s.canPay&&(0,e.createElement)("section",{className:"jio-creator-field"},(0,e.createElement)("div",{className:"jio-creator-switch"},(0,e.createElement)("div",null,(0,e.createElement)("h3",null,(0,a.__)("付费内容","jio")),(0,e.Line truncated
(0,a.__)("按站点时区(%s)发布,至少需要晚于当前时间 1 分钟。","jio"),String(m.timezone||"UTC")))))}const g={ready:(0,a.__)("尚未保存","jio"),saving:(0,a.__)("保存中…","jio"),saved:(0,a.__)("已保存","jio"),error:(0,a.__)("保存失败","jio"),conflict:(0,a.__)("内容已在别处更新","jio"),submitting:(0,a.__)("提交中…","jio"),submitted:(0,a.__)("已提交","jio")};function h({value:t}){return(0,e.createElement)("span",{className:`jio-creator__save is-${t.state}`,role:"status","aria-live":"polite"},(0,e.createElement)("i",{className:"fa fas fa-circle","aria-hidden":"true"}),(0,e.createElement)("span",null,g[t.state]||g.ready),"saved"===t.state&&t.at?(0,e.createElement)("time",null,t.at):null)}function j(e){const t=(e.downloads||[]).map(e=>{const t="string"==typeof e.assetKey?e.assetKey:"";if(t)return{type:"protected",asset_key:t,label:"string"==typeof e.label?e.label:""};const a="string"==typeof e.url?e.url.trim():"";return a?{type:"direct",url:a,label:"string"==typeof e.label?e.label:"",extract_code:"string"==typeof e.extractCode?e.extractCode:""}:null}).filter(Boolean);return{enabled:!!e.enabled,fields:{pay_type:e.enabled?t.length>0?"download":"reading":"no",pay_modo:"points"===e.modo?"points":"0",price:"points"===e.modo?"0.00":String(e.price||"0.00"),points_price:"points"===e.modo?Math.max(0,Number(e.points)||0):0,vip1_price:"points"===e.modo?"0.00":String(e.vip1Price||"0.00"),vip2_price:"points"===e.modo?"0.00":String(e.vip2Price||"0.00"),pay_limit:Number(e.payLimit)||0,downloads:t}}}const y=e=>e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;"),E=[{value:"generic",label:(0,a.__)("通用","jio")},{value:"javascript",label:"JavaScript"},{value:"typescript",label:"TypeScript"},{value:"php",label:"PHP"},{value:"python",label:"Python"},{value:"java",label:"Java"},{value:"go",label:"Go"},{value:"rust",label:"Rust"},{value:"csharp",label:"C#"},{value:"cpp",label:"C++"},{value:"sql",label:"SQL"},{value:"json",label:"JSON"},{value:"bash",label:"Shell"},{value:"css",label:"CSS"},{value:"html",label:"HTML"},{value:"markdown",label:"Markdown"}];const v=(e,t,a)=>Math.min(a,Math.max(t,Number.isFinite(e)?Math.round(e):t)),k={table:(0,a.__)("插入表格","jio"),math:(0,a.__)("插入公式","jio"),footnote:(0,a.__)("插入注释","jio")};function N({kind:n,onSubmit:o,onClose:r}){const[l,i]=(0,t.useState)(3),[c,s]=(0,t.useState)(3),[u,d]=(0,t.useState)(!0),[m,p]=(0,t.useState)(!1),[_,f]=(0,t.useState)("a^2 + b^2 = c^2"),[b,g]=(0,t.useState)(!0),[h,j]=(0,t.useState)(""),y=(0,t.useRef)(null),E=(0,t.useRef)(null),N=(0,t.useRef)(null),w=(0,t.useRef)(null);(0,t.useEffect)(()=>{(E.current||N.current||w.current)?.focus();const e=e=>{"Escape"===e.key&&(e.preventDefault(),r())};return document.addEventListener("keydown",e),()=>document.removeEventListener("keydown",e)},[r]);const C=(0,t.useMemo)(()=>"footnote"===n?""!==h.trim():"math"!==n||""!==_.trim(),[n,h,_]);return(0,e.createElement)("div",{className:"jio-creator-dialog",role:"dialog","aria-modal":"true","aria-labelledby":"jio-creator-dialog-title"},(0,e.createElement)("button",{type:"button",className:"jio-creator-dialog__backdrop","aria-label":(0,a.__)("关闭","jio"),onClick:r}),(0,e.createElement)("div",{className:"jio-creator-dialog__panel",ref:y},(0,e.createElement)("header",{className:"jio-creator-dialog__head"},(0,e.createElement)("h2",{id:"jio-creator-dialog-title"},k[n]),(0,e.createElement)("button",{type:"button",className:"jio-creator-dialog__close",onClick:r,"aria-label":(0,a.__)("关闭","jio")},(0,e.createElement)("i",{className:"fa fas fa-times","aria-hidden":"true"}))),(0,e.createElement)("div",{className:"jio-creator-dialog__body"},"table"===n&&(0,e.createElement)(e.Fragment,null,(0,e.createElement)("div",{className:"jio-creator-dialog__row"},(0,e.createElement)("label",null,(0,e.createElement)("span",null,(0,a.__)("行数","jio")),(0,e.createElement)("input",{ref:E,type:"number",min:1,max:20,value:l,onChange:e=>i(Number(e.target.value))})),(0,e.createElement)("label",null,(0,e.createElement)("span",null,(0,a.__)("列数","jio")),(0,e.createElement)("input",{type:"number",min:1,max:10,value:c,onChange:e=>s(Number(e.target.value))}))),(0,e.createElement)("label",{className:"jio-creator-dialog__check"},(0,e.createElement)("input",{type:"checkbox",checked:u,onChange:e=>d(e.target.checked)}),(0,e.createElement)("span",null,(0,a.__)("首行为表头","jio"))),(0,e.createElement)("label",{className:"jio-creator-dialog__check"},(0,e.createElement)("input",{type:"checkbox",checked:m,onChange:e=>p(e.target.checked)}),(0,e.createElement)("span",null,(0,a.__)("隔行变色","jio")))),"math"===n&&(0,e.createElement)(e.Fragment,null,(0,e.createElement)("label",{className:"jio-creator-dialog__field"},(0,e.createElement)("span",null,(0,a.__)("公式","jio")),(0,e.createElement)("textarea",{ref:N,rows:3,value:_,onChange:e=>f(e.target.value),placeholder:"a^2 + b^2 = c^2"})),(0,e.createElement)Line truncated
(0,a.__)("请输入标题(最多 %d 个字)","jio"),Number(H.titleMax)||100),spellCheck:!1,maxLength:Number(H.titleMax)||100})),(0,e.createElement)("div",{className:"jio-creator__editor","data-editor-ready":_?"true":"false"},(0,e.createElement)("textarea",{id:x,defaultValue:n.content,onChange:e=>X(e.target.value),"aria-label":(0,a.__)("文章正文","jio")}))),(0,e.createElement)(b,{featured:n.featured,canEditCover:!!J.canUploadImage,canSchedule:!!J.canSchedule,scheduleEnabled:!!n.schedule.enabled,scheduleAt:n.schedule.at,onFeaturedChange:e=>Q({featured:e}),onScheduleChange:(e,t)=>Q({schedule:{enabled:e,at:t}})}),z&&(0,e.createElement)(N,{kind:z,onSubmit:pe,onClose:()=>U(null)}),D&&(0,e.createElement)(s,{draft:n,setDraft:Q,capabilities:J,limits:H,taxonomy:{categories:w.categories&&w.categories.items||[],tags:!!w.tags?.enabled,customDefs:w.customDefs||[]},isPublishedEdit:V,onClose:()=>P(!1),onSaveDraft:()=>{Z(!1)},onSubmit:e=>{ee(e)},privateStorage:!!w.privateStorage,privateUploads:w.privateUploads||[],captcha:!!w.captcha?.enabled,saving:"saving"===I.state||"submitting"===I.state}))}function I(){const a=document.getElementById("jio-creator-studio");a&&(a.classList.add("is-ready"),(0,t.createRoot)(a).render((0,e.createElement)($,null)))}"loading"===document.readyState?document.addEventListener("DOMContentLoaded",I):I()})();
(()=>{"use strict";const e=window.React,t=window.wp.element,a=window.wp.i18n;function n(e,t){const a=e&&(e.message||e.error)||(401===t?"登录状态已失效,请刷新页面重新登录。":"请求失败,请稍后重试。"),n=new Error(String(a));return n.status=t,n.code=e&&e.code?String(e.code):"",n.data=e,n}async function o(e){try{return await e.json()}catch(e){return null}}async function r(e,t,a){const r=await fetch(e,{method:"POST",credentials:"same-origin",headers:{"Content-Type":"application/json","X-WP-Nonce":a},body:JSON.stringify(t)}),l=await o(r);if(!r.ok)throw n(l,r.status);return l}async function l(e,t,a,r={}){const l=new FormData;l.append("file",t),Object.entries(r).forEach(([e,t])=>l.append(e,t));const i=await fetch(e,{method:"POST",credentials:"same-origin",headers:{"X-WP-Nonce":a},body:l}),c=await o(i);if(!i.ok)throw n(c,i.status);return c}const i=window.jioCreatorStudio||{},c=i.nonce||"";function s(n){const{draft:o,setDraft:r,capabilities:s,limits:d,taxonomy:u,isPublishedEdit:m,onClose:p,onSaveDraft:_,onSubmit:f,privateStorage:b,privateUploads:g,saving:h,captcha:j}=n,[y,E]=(0,t.useState)(""),[v,k]=(0,t.useState)(""),[N,w]=(0,t.useState)(!1),C=(0,t.useRef)(null),S=(0,t.useRef)(null);(0,t.useEffect)(()=>{const e=S.current,t=document.getElementById("jio-creator-captcha");if(!e||!t)return;const a=t.parentElement;return e.appendChild(t),t.removeAttribute("hidden"),()=>{a&&(a.appendChild(t),t.setAttribute("hidden",""))}},[]);const x=(0,t.useMemo)(()=>(o.title||"").trim().length,[o.title])>=Number(d.titleMin||1),M=()=>{const e=y.trim();if(!e)return;const t=e.split(/[,,\n]+/u).map(e=>e.trim()).filter(Boolean);r({tags:Array.from(new Set([...o.tags,...t]))}),E("")};return(0,e.createElement)("div",{className:"jio-creator-drawer",role:"dialog","aria-modal":"true","aria-labelledby":"jio-creator-publish-title"},(0,e.createElement)("button",{type:"button",className:"jio-creator-drawer__backdrop","aria-label":(0,a.__)("关闭","jio"),onClick:p}),(0,e.createElement)("div",{className:"jio-creator-drawer__panel"},(0,e.createElement)("header",{className:"jio-creator-drawer__head"},(0,e.createElement)("h2",{id:"jio-creator-publish-title"},m?(0,a.__)("提交改稿","jio"):(0,a.__)("发布文章","jio")),(0,e.createElement)("button",{type:"button",className:"jio-creator-drawer__close",onClick:p,"aria-label":(0,a.__)("关闭","jio")},(0,e.createElement)("i",{className:"fa fas fa-times","aria-hidden":"true"}))),(0,e.createElement)("div",{className:"jio-creator-drawer__body"},m&&(0,e.createElement)("p",{className:"jio-creator-drawer__note"},(0,a.__)("审核通过前,读者看到的仍然是当前线上版本。","jio")),(0,e.createElement)("section",{className:"jio-creator-field"},(0,e.createElement)("h3",null,(0,a.__)("分类","jio")),(0,e.createElement)("div",{className:"jio-creator-chips"},0===u.categories.length&&(0,e.createElement)("span",{className:"jio-creator-muted"},(0,a.__)("暂无可选分类","jio")),u.categories.map(t=>(0,e.createElement)("label",{key:t.id,className:"jio-creator-chip"+(o.categories.includes(t.id)?" is-active":"")},(0,e.createElement)("input",{type:"checkbox",checked:o.categories.includes(t.id),onChange:()=>(e=>{const t=o.categories.includes(e);r({categories:t?o.categories.filter(t=>t!==e):[...o.categories,e]})})(t.id)}),(0,e.createElement)("span",null,t.name))))),u.tags&&(0,e.createElement)("section",{className:"jio-creator-field"},(0,e.createElement)("h3",null,(0,a.__)("标签","jio")),(0,e.createElement)("div",{className:"jio-creator-chips"},o.tags.map(t=>(0,e.createElement)("span",{key:t,className:"jio-creator-chip is-active"},(0,e.createElement)("span",null,t),(0,e.createElement)("button",{type:"button",onClick:()=>r({tags:o.tags.filter(e=>e!==t)}),"aria-label":(0,a.sprintf)((0,a.__)("移除标签 %s","jio"),t)},(0,e.createElement)("i",{className:"fa fas fa-times","aria-hidden":"true"}))))),(0,e.createElement)("div",{className:"jio-creator-inline"},(0,e.createElement)("input",{type:"text",value:y,placeholder:(0,a.__)("输入标签,逗号分隔","jio"),onChange:e=>E(e.target.value),onKeyDown:e=>{"Enter"===e.key&&(e.preventDefault(),M())}}),(0,e.createElement)("button",{type:"button",onClick:M},(0,a.__)("添加","jio")))),u.customDefs.map(t=>(0,e.createElement)("section",{className:"jio-creator-field",key:t.key},(0,e.createElement)("h3",null,t.name),(0,e.createElement)("div",{className:"jio-creator-chips"},t.values.map(a=>{const n=o.custom[t.key]||[],l=n.includes(a.key);return(0,e.createElement)("label",{key:a.key,className:"jio-creator-chip"+(l?" is-active":"")},(0,e.createElement)("input",{type:"checkbox",checked:l,onChange:()=>r({custom:{...o.custom,[t.key]:l?n.filter(e=>e!==a.key):[...n,a.key]}})}),(0,e.createElement)("span",null,a.name))})))),s.canPay&&(0,e.createElement)("section",{className:"jio-creator-field"},(0,e.createElement)("div",{className:"jio-creator-switch"},(0,e.createElement)("div",null,(0,e.createElement)("h3",null,(0,a.__)("付费内容","jio")),(0,e.Line truncated
(0,a.__)("按站点时区(%s)发布,至少需要晚于当前时间 1 分钟。","jio"),String(m.timezone||"UTC")))))}function g(e){const t=(e.downloads||[]).map(e=>{const t="string"==typeof e.assetKey?e.assetKey:"";if(t)return{type:"protected",asset_key:t,label:"string"==typeof e.label?e.label:""};const a="string"==typeof e.url?e.url.trim():"";return a?{type:"direct",url:a,label:"string"==typeof e.label?e.label:"",extract_code:"string"==typeof e.extractCode?e.extractCode:""}:null}).filter(Boolean);return{enabled:!!e.enabled,fields:{pay_type:e.enabled?t.length>0?"download":"reading":"no",pay_modo:"points"===e.modo?"points":"0",price:"points"===e.modo?"0.00":String(e.price||"0.00"),points_price:"points"===e.modo?Math.max(0,Number(e.points)||0):0,vip1_price:"points"===e.modo?"0.00":String(e.vip1Price||"0.00"),vip2_price:"points"===e.modo?"0.00":String(e.vip2Price||"0.00"),pay_limit:Number(e.payLimit)||0,downloads:t}}}const h=e=>e.replace(/&/g,"&amp;").replace(/"/g,"&quot;").replace(/</g,"&lt;").replace(/>/g,"&gt;");function j(e){if(!e||-1===e.indexOf("data-jio-")&&-1===e.indexOf("jio-code-lang"))return e;const t=document.createElement("div");return t.innerHTML=e,t.querySelectorAll("[data-jio-math-source]").forEach(e=>{const t=e.getAttribute("data-jio-math-source")||"",a="0"!==e.getAttribute("data-jio-math-display"),n=`[jio_math display="${a?"1":"0"}"]${t}[/jio_math]`,o=e.closest("p");a&&o&&o.textContent?.trim()===e.textContent?.trim()?o.textContent=n:e.replaceWith(document.createTextNode(n))}),t.querySelectorAll("[data-jio-footnote]").forEach(e=>{const t=e.getAttribute("data-jio-footnote")||"";e.replaceWith(document.createTextNode(`[jio_footnote]${t}[/jio_footnote]`))}),t.querySelectorAll(".jio-code-lang").forEach(e=>e.remove()),t.innerHTML}function y(e){return e&&-1!==e.indexOf("[jio_")?e.replace(/\[jio_math display="([01])"\]([\s\S]*?)\[\/jio_math\]/g,(e,t,a)=>"0"===t?`<span class="jio-math" data-jio-math-source="${h(a)}" data-jio-math-display="0" contenteditable="false">${E(a)}</span>`:`<p><span class="jio-math jio-math--block" data-jio-math-source="${h(a)}" data-jio-math-display="1" contenteditable="false">${E(a)}</span></p>`).replace(/\[jio_footnote\]([\s\S]*?)\[\/jio_footnote\]/g,(e,t)=>`<sup class="jio-footnote-ref" data-jio-footnote="${h((e=>{const t=document.createElement("textarea");return t.innerHTML=e,t.value})(t))}" contenteditable="false">${t}</sup>`):e}const E=e=>e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/\^\{([^{}]*)\}|\^(\S)/gu,"<sup>$1$2</sup>").replace(/_\{([^{}]*)\}|_(\S)/gu,"<sub>$1$2</sub>"),v={ready:(0,a.__)("尚未保存","jio"),saving:(0,a.__)("保存中…","jio"),saved:(0,a.__)("已保存","jio"),error:(0,a.__)("保存失败","jio"),conflict:(0,a.__)("内容已在别处更新","jio"),submitting:(0,a.__)("提交中…","jio"),submitted:(0,a.__)("已提交","jio")};function k({value:t}){return(0,e.createElement)("span",{className:`jio-creator__save is-${t.state}`,role:"status","aria-live":"polite"},(0,e.createElement)("i",{className:"fa fas fa-circle","aria-hidden":"true"}),(0,e.createElement)("span",null,v[t.state]||v.ready),"saved"===t.state&&t.at?(0,e.createElement)("time",null,t.at):null)}const N=36e5,w=24*N,C=30*w,S=365*w,x={ready:(0,a.__)("草稿","jio"),saving:(0,a.__)("保存中","jio"),saved:(0,a.__)("草稿","jio"),error:(0,a.__)("保存失败","jio"),conflict:(0,a.__)("冲突","jio"),submitting:(0,a.__)("提交中","jio"),submitted:(0,a.__)("已提交","jio")};function M({charCount:t,saveState:n,isPublishedEdit:o,saving:r,onSaveDraft:l,onPreview:i,onPublish:c}){const s=n.at?function(e,t=Date.now()){const n=Date.parse(e);if(!Number.isFinite(n))return"";const o=Math.max(0,t-n);return o<6e4?(0,a.__)("刚刚","jio"):o<N?(0,a.sprintf)(/* translators: %d: minutes */ /* translators: %d: minutes */
(0,a.__)("%d分钟前","jio"),Math.floor(o/6e4)):o<w?(0,a.sprintf)(/* translators: %d: hours */ /* translators: %d: hours */
(0,a.__)("%d小时前","jio"),Math.floor(o/N)):o<C?(0,a.sprintf)(/* translators: %d: days */ /* translators: %d: days */
(0,a.__)("%d天前","jio"),Math.floor(o/w)):o<S?(0,a.sprintf)(/* translators: %d: months */ /* translators: %d: months */
(0,a.__)("%d个月前","jio"),Math.floor(o/C)):(0,a.sprintf)(/* translators: %d: years */ /* translators: %d: years */
(0,a.__)("%d年前","jio"),Math.floor(o/S))}(n.at):"",d=x[n.state]||x.ready;return(0,e.createElement)("footer",{className:"jio-creator__status",role:"contentinfo"},(0,e.createElement)("div",{className:"jio-creator__status-left"},(0,e.createElement)("span",{className:"jio-creator__status-count"},(0,a.__)("字数","jio"),":",t),(0,e.createElement)("span",{className:"jio-creator__status-mode"},(0,a.__)("Markdown 语法输入中","jio"))),(0,e.createElement)("div",{className:"jio-creator__status-meta"},(0,e.createElement)(k,{value:n}),(0,e.createElement)("span",{className:"jio-creator__status-dot","aria-hidden":"true"},"·"),(0,e.createElement)("span",{className:"jio-creator__status-time"},s||d)),(0,e.createElement)("div",{className:"jio-creator__status-actions"},(0,e.createElement)("button",{type:"button",className:"jio-creator__status-ghost",onClick:l,disabled:r},(0,a.__)("保存草稿","jio")),(0,e.createElement)("button",{type:"button",className:"jio-creator__status-ghost",onClick:i,disabled:r},(0,a.__)("预览","jio")),(0,e.createElement)("button",{type:"button",className:"jio-creator__status-primary",onClick:c,disabled:r},o?(0,a.__)("提交改稿","jio"):(0,a.__)("发布","jio"))))}const $=e=>e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;"),q=[{value:"generic",label:(0,a.__)("通用","jio")},{value:"javascript",label:"JavaScript"},{value:"typescript",label:"TypeScript"},{value:"php",label:"PHP"},{value:"python",label:"Python"},{value:"java",label:"Java"},{value:"go",label:"Go"},{value:"rust",label:"Rust"},{value:"csharp",label:"C#"},{value:"cpp",label:"C++"},{value:"sql",label:"SQL"},{value:"json",label:"JSON"},{value:"bash",label:"Shell"},{value:"css",label:"CSS"},{value:"html",label:"HTML"},{value:"markdown",label:"Markdown"}];const L=(e,t,a)=>Math.min(a,Math.max(t,Number.isFinite(e)?Math.round(e):t)),I={table:(0,a.__)("插入表格","jio"),math:(0,a.__)("插入公式","jio"),footnote:(0,a.__)("插入注释","jio")};function R({kind:n,onSubmit:o,onClose:r}){const[l,i]=(0,t.useState)(3),[c,s]=(0,t.useState)(3),[d,u]=(0,t.useState)(!0),[m,p]=(0,t.useState)(!1),[_,f]=(0,t.useState)("a^2 + b^2 = c^2"),[b,g]=(0,t.useState)(!0),[h,j]=(0,t.useState)(""),y=(0,t.useRef)(null),E=(0,t.useRef)(null),v=(0,t.useRef)(null),k=(0,t.useRef)(null);(0,t.useEffect)(()=>{(E.current||v.current||k.current)?.focus();const e=e=>{"Escape"===e.key&&(e.preventDefault(),r())};return document.addEventListener("keydown",e),()=>document.removeEventListener("keydown",e)},[r]);const N=(0,t.useMemo)(()=>"footnote"===n?""!==h.trim():"math"!==n||""!==_.trim(),[n,h,_]);return(0,e.createElement)("div",{className:"jio-creator-dialog",role:"dialog","aria-modal":"true","aria-labelledby":"jio-creator-dialog-title"},(0,e.createElement)("button",{type:"button",className:"jio-creator-dialog__backdrop","aria-label":(0,a.__)("关闭","jio"),onClick:r}),(0,e.createElement)("div",{className:"jio-creator-dialog__panel",ref:y},(0,e.createElement)("header",{className:"jio-creator-dialog__head"},(0,e.createElement)("h2",{id:"jio-creator-dialog-title"},I[n]),(0,e.createElement)("button",{type:"button",className:"jio-creator-dialog__close",onClick:r,"aria-label":(0,a.__)("关闭","jio")},(0,e.createElement)("i",{className:"fa fas fa-times","aria-hidden":"true"}))),(0,e.createElement)("div",{className:"jio-creator-dialog__body"},"table"===n&&(0,e.createElement)(e.Fragment,null,(0,e.createElement)("div",{className:"jio-creator-dialog__row"},(0,e.createElement)("label",null,(0,e.createElement)("span",null,(0,a.__)("行数","jio")),(0,e.createElement)("input",{ref:E,type:"number",min:1,max:20,value:l,onChange:e=>i(Number(e.target.value))})),(0,e.createElement)("label",null,(0,e.createElement)("span",null,(0,a.__)("列数","jio")),(0,e.createElement)("input",{type:"number",min:1,max:10,value:c,onChange:e=>s(Number(e.target.value))}))),(0,e.createElement)("label",{className:"jio-creator-dialog__check"},(0,e.createElement)("input",{type:"checkbox",checked:d,onChange:e=>u(e.target.checked)}),(0,e.createElement)("span",null,(0,a.__)("首行为表头","jio"))),(0,e.createElement)("label",{className:"jio-creator-dialog__check"},(0,e.createElement)("input",{type:"checkbox",checked:m,onChange:e=>p(e.target.checked)}),(0,e.createElement)("span",null,(0,a.__)("隔行变色","jio")))),"math"===n&&(0,e.createElement)(e.Fragment,null,(0,e.createElement)("label",{className:"jio-creator-dialog__field"},(0,e.createElement)("span",null,(0,a.__)("公式","jio")),(0,e.createElement)("textarea",{ref:v,rows:3,value:_,onChange:e=>f(e.target.value),placeholder:"a^2 + b^2 = c^2"})),(0,e.createElement)("p",{className:"jio-creator-muted"},(0,a.__)("支持上标 ^ 与下标 _,例如 x^2 与 a_n;暂不支持完整 LaTeX。","jio")),(0,e.createElement)("label",{className:"jio-creator-dialog__check"},(0,e.createElement)("input",{type:"checkbox",checked:b,onChange:e=>g(e.target.checked)}),(0,e.createElement)("span",null,(0,a.__)("独占一行显示Line truncated
(0,a.__)("请输入标题(最多 %d 个字)","jio"),Number(ee.titleMax)||100),spellCheck:!1,maxLength:Number(ee.titleMax)||100})),(0,e.createElement)("div",{className:"jio-creator__editor","data-editor-ready":_?"true":"false"},(0,e.createElement)("textarea",{id:A,defaultValue:n.content,onChange:e=>oe(e.target.value),"aria-label":(0,a.__)("文章正文","jio")}))),(0,e.createElement)(b,{featured:n.featured,canEditCover:!!Z.canUploadImage,canSchedule:!!Z.canSchedule,scheduleEnabled:!!n.schedule.enabled,scheduleAt:n.schedule.at,onFeaturedChange:e=>ne({featured:e}),onScheduleChange:(e,t)=>ne({schedule:{enabled:e,at:t}})}),null!==I&&(0,e.createElement)("div",{className:"jio-creator-dialog",role:"dialog","aria-modal":"true","aria-labelledby":"jio-code-lang-title"},(0,e.createElement)("button",{type:"button",className:"jio-creator-dialog__backdrop","aria-label":(0,a.__)("关闭","jio"),onClick:()=>H(null)}),(0,e.createElement)("div",{className:"jio-creator-dialog__panel"},(0,e.createElement)("header",{className:"jio-creator-dialog__head"},(0,e.createElement)("h2",{id:"jio-code-lang-title"},(0,a.__)("代码语言","jio")),(0,e.createElement)("button",{type:"button",className:"jio-creator-dialog__close",onClick:()=>H(null),"aria-label":(0,a.__)("关闭","jio")},(0,e.createElement)("i",{className:"fa fas fa-times","aria-hidden":"true"}))),(0,e.createElement)("div",{className:"jio-creator-dialog__body"},(0,e.createElement)("div",{className:"jio-creator__lang-grid"},q.map(t=>(0,e.createElement)("button",{key:t.value,type:"button",className:"jio-creator__lang-item"+(t.value===I?" is-active":""),onClick:()=>{const e=J.current;if(J.current=null,H(null),!e)return;e.setAttribute("data-enlighter-language",t.value);const a=e.querySelector("code");a&&a.setAttribute("data-enlighter-language",t.value),B(i.current),oe(j(i.current.getContent()))}},t.label)))))),x&&(0,e.createElement)(R,{kind:x,onSubmit:ve,onClose:()=>L(null)}),(0,e.createElement)(M,{charCount:ae,saveState:N,isPublishedEdit:te,saving:!_||"saving"===N.state||"submitting"===N.state,onSaveDraft:()=>{ce(!1)},onPreview:()=>{de()},onPublish:()=>S(!0)}),C&&(0,e.createElement)(s,{draft:n,setDraft:ne,capabilities:Z,limits:ee,taxonomy:{categories:D.categories&&D.categories.items||[],tags:!!D.tags?.enabled,customDefs:D.customDefs||[]},isPublishedEdit:te,onClose:()=>S(!1),onSaveDraft:()=>{ce(!1)},onSubmit:e=>{se(e)},privateStorage:!!D.privateStorage,privateUploads:D.privateUploads||[],captcha:!!D.captcha?.enabled,saving:"saving"===N.state||"submitting"===N.state}))}function J(){const a=document.getElementById("jio-creator-studio");a&&(a.classList.add("is-ready"),(0,t.createRoot)(a).render((0,e.createElement)(H,null)))}"loading"===document.readyState?document.addEventListener("DOMContentLoaded",J):J()})();
@@ -0,0 +1,125 @@
/**
* 底部固定状态栏。
*
* 与公众号/知乎一致:左侧是字数与输入法状态,中间是草稿时间,右侧是预览与发布。
* 固定在视口底部,不随正文滚动。
*
* @package Jio
*/
import { __, sprintf } from '@wordpress/i18n';
import { SaveState, type SaveStateValue } from './save-state';
type Props = {
charCount: number;
saveState: SaveStateValue;
isPublishedEdit: boolean;
saving: boolean;
onSaveDraft: () => void;
onPreview: () => void;
onPublish: () => void;
};
const MINUTE = 60 * 1000;
const HOUR = 60 * MINUTE;
const DAY = 24 * HOUR;
const MONTH = 30 * DAY;
const YEAR = 365 * DAY;
/** 相对时间:刚刚 / X分钟前 / X小时前 … 与参考编辑器一致。 */
export function relativeTime(iso: string, now = Date.now()): string {
const at = Date.parse(iso);
if (!Number.isFinite(at)) {
return '';
}
const diff = Math.max(0, now - at);
if (diff < MINUTE) {
return __('刚刚', 'jio');
}
if (diff < HOUR) {
return sprintf(
/* translators: %d: minutes */
__('%d分钟前', 'jio'),
Math.floor(diff / MINUTE)
);
}
if (diff < DAY) {
return sprintf(
/* translators: %d: hours */
__('%d小时前', 'jio'),
Math.floor(diff / HOUR)
);
}
if (diff < MONTH) {
return sprintf(
/* translators: %d: days */
__('%d天前', 'jio'),
Math.floor(diff / DAY)
);
}
if (diff < YEAR) {
return sprintf(
/* translators: %d: months */
__('%d个月前', 'jio'),
Math.floor(diff / MONTH)
);
}
return sprintf(
/* translators: %d: years */
__('%d年前', 'jio'),
Math.floor(diff / YEAR)
);
}
const STATUS_LABELS: Record<SaveStateValue['state'], string> = {
ready: __('草稿', 'jio'),
saving: __('保存中', 'jio'),
saved: __('草稿', 'jio'),
error: __('保存失败', 'jio'),
conflict: __('冲突', 'jio'),
submitting: __('提交中', 'jio'),
submitted: __('已提交', 'jio'),
};
export function StatusBar({
charCount,
saveState,
isPublishedEdit,
saving,
onSaveDraft,
onPreview,
onPublish,
}: Props) {
const savedAt = saveState.at ? relativeTime(saveState.at) : '';
const status = STATUS_LABELS[saveState.state] || STATUS_LABELS.ready;
return (
<footer className="jio-creator__status" role="contentinfo">
<div className="jio-creator__status-left">
<span className="jio-creator__status-count">
{__('字数', 'jio')}:{charCount}
</span>
<span className="jio-creator__status-mode">{__('Markdown 语法输入中', 'jio')}</span>
</div>
<div className="jio-creator__status-meta">
<SaveState value={saveState} />
<span className="jio-creator__status-dot" aria-hidden="true">·</span>
<span className="jio-creator__status-time">{savedAt || status}</span>
</div>
<div className="jio-creator__status-actions">
<button type="button" className="jio-creator__status-ghost" onClick={onSaveDraft} disabled={saving}>
{__('保存草稿', 'jio')}
</button>
<button type="button" className="jio-creator__status-ghost" onClick={onPreview} disabled={saving}>
{__('预览', 'jio')}
</button>
<button type="button" className="jio-creator__status-primary" onClick={onPublish} disabled={saving}>
{isPublishedEdit ? __('提交改稿', 'jio') : __('发布', 'jio')}
</button>
</div>
</footer>
);
}
@@ -42,7 +42,7 @@ img { height: auto; }
line-height: 1.5;
white-space: pre-wrap;
}
.enlighter-pre::before {
.enlighter-pre .jio-code-lang {
position: absolute;
top: 9px;
right: 9px;
@@ -50,10 +50,13 @@ img { height: auto; }
border-radius: 4px;
background: linear-gradient(135deg, #3dcabd 10%, #254bdd 100%);
color: #fff;
content: attr(data-jio-code-label);
font-size: 12px;
line-height: 1.5;
pointer-events: none;
cursor: pointer;
user-select: none;
}
.enlighter-pre .jio-code-lang:hover {
filter: brightness(1.08);
}
/* 引用:与前台主题的 .quote_q 视觉一致。 */
@@ -101,9 +104,21 @@ img { height: auto; }
}
.jio-content-table.is-striped tbody tr:nth-child(odd) td { background: #f7f9f8; }
/* 公式与目录占位。 */
.jio-math { font-family: "Cambria Math", "Latin Modern Math", "Times New Roman", serif; }
.jio-math--block { display: block; margin: 12px 0; text-align: center; }
/* 公式。 */
.jio-math {
font-family: "Cambria Math", "Latin Modern Math", "Times New Roman", serif;
font-size: 1.05em;
white-space: pre-wrap;
}
.jio-math--block {
display: block;
margin: 12px 0;
padding: 10px;
border-radius: 6px;
background: #f8faf9;
text-align: center;
}
.jio-math sup, .jio-math sub { font-size: 0.72em; }
.jio-toc-placeholder {
height: 40px;
margin: 0 0 1.1em;
@@ -117,13 +132,22 @@ img { height: auto; }
}
.jio-toc-placeholder::after { content: '目录将在发布后自动生成'; }
/* 注释上标:编辑器里只显示序号,发布后由服务端渲染脚注列表。 */
.jio-footnote-ref,
.jio-footnote-ref a {
/* 注释:编辑器里显示上标标记,发布后由服务端渲染编号与注释列表。 */
.jio-footnote-ref {
display: inline-block;
max-width: 100%;
padding: 0 6px;
border-radius: 4px;
background: #eef5f2;
color: #267665;
font-size: 0.8em;
text-decoration: none;
font-size: 0.78em;
line-height: 1.6;
vertical-align: super;
cursor: default;
}
.jio-footnote-ref::before {
content: '注 ';
opacity: 0.6;
}
/* 附件链接。 */
+228 -19
View File
@@ -7,19 +7,22 @@
* @package Jio
*/
import { createRoot, useCallback, useEffect, useRef, useState } from '@wordpress/element';
import { createRoot, useCallback, useEffect, useMemo, useRef, useState } from '@wordpress/element';
import { __, sprintf } from '@wordpress/i18n';
import { apiPost } from './services/api';
import { PublishDrawer } from './panels/publish-drawer';
import { EditorToolbar } from './components/editor-toolbar';
import { PublishSettings } from './components/publish-settings';
import { SaveState, type SaveStateValue } from './components/save-state';
import { type SaveStateValue } from './components/save-state';
import { useAutosave } from './hooks/use-autosave';
import { useLocalRecovery } from './hooks/use-local-recovery';
import { normalizePayForRequest } from './utils/pay';
import { EDITOR_CONTENT_STYLE } from './editor-content-style';
import { hydrateBlocks, serializeBlocks } from './utils/serialize';
import { StatusBar } from './components/status-bar';
import {
CODE_LANGUAGES,
codeBlockMarkup,
dividerMarkup,
footnoteMarkup,
@@ -174,6 +177,160 @@ function removeAdjacentBlock(editor: any, event: any): void {
editor.undoManager.add();
}
/**
* 在编辑器里为每个代码块渲染语言角标,并保证回车不会跳出代码区。
*
* 角标是编辑期提示(纯 UI),保存前由 serializeBlocks 移除。
*/
/**
* 在代码块内回车时保持在代码里换行,而不是跳出到普通段落。
*
* TinyMCE 默认把 pre 当成不可持续输入的块,回车会新建段落,导致代码跑到
* 代码块外面。这里手动插入换行并保持光标在 code 内。
*/
/**
* 让 iframe 高度跟随正文增长,避免编辑器内部出现第二根滚动条。
*
* TinyMCE 的 iframe 不会自动撑高;这里测量内容高度并同步容器,
* 页面整体滚动由外层工作台负责。
*/
function syncEditorHeight(editor: any): void {
if (!editor || editor.removed) {
return;
}
const doc = editor.getDoc();
const iframe = editor.getContentAreaContainer()?.querySelector('iframe') as HTMLIFrameElement | null;
if (!doc || !iframe) {
return;
}
const height = Math.max(
320,
doc.documentElement.scrollHeight,
doc.body ? doc.body.scrollHeight : 0
);
iframe.style.height = `${height}px`;
iframe.style.overflow = 'hidden';
}
function keepCaretInsideCode(editor: any, event: any): boolean {
if (Number(event.keyCode) !== 13 || event.shiftKey) {
return false;
}
const pre = editor.dom.getParent(editor.selection.getNode(), 'pre.enlighter-pre');
if (!pre) {
return false;
}
event.preventDefault();
const code = pre.querySelector('code') || pre;
editor.selection.setCursorLocation(code, code.childNodes.length);
editor.insertContent('<br>');
editor.nodeChanged();
return true;
}
/** 可整体删除的块级元素选择器。 */
const BLOCK_SELECTOR = 'table,pre.enlighter-pre,hr,.quote_q,p';
/**
* 给表格 / 代码块 / 引用等块渲染一个浮层删除按钮。
*
* 与 WPS 的块操作一致:鼠标悬停到块上时在右上角出现删除入口,
* 避免只能靠反复按退格键。
*/
function installBlockActions(editor: any): void {
const doc = editor.getDoc();
if (!doc || doc.getElementById('jio-block-actions')) {
return;
}
const bar = doc.createElement('div');
bar.id = 'jio-block-actions';
bar.className = 'jio-creator__block-actions';
bar.innerHTML =
'<button type="button" class="jio-creator__block-action" data-jio-block-delete title="' +
'删除此块' +
'" aria-label="删除此块"><span aria-hidden="true">&#10005;</span></button>';
doc.body.appendChild(bar);
let current: HTMLElement | null = null;
const hide = () => {
current = null;
bar.classList.remove('is-visible');
};
const show = (block: HTMLElement) => {
if (!block || block === doc.body) {
hide();
return;
}
// 空段落没有删除价值,交给退格键处理。
if (block.tagName === 'P' && (block.textContent || '').trim() === '') {
hide();
return;
}
current = block;
const rect = block.getBoundingClientRect();
bar.style.top = `${rect.top + doc.documentElement.scrollTop + 6}px`;
bar.style.left = `${rect.right - 40}px`;
bar.classList.add('is-visible');
};
doc.addEventListener('mouseover', (event: MouseEvent) => {
const block = (event.target as HTMLElement)?.closest?.(BLOCK_SELECTOR) as HTMLElement | null;
if (block && block !== doc.body && !block.classList.contains('jio-code-lang')) {
show(block);
}
});
doc.addEventListener('mouseleave', hide);
bar.addEventListener('mousedown', (event: MouseEvent) => event.preventDefault());
bar.addEventListener('click', (event: MouseEvent) => {
const button = (event.target as HTMLElement)?.closest('[data-jio-block-delete]');
if (!button || !current) {
return;
}
event.preventDefault();
const target = current;
hide();
editor.undoManager.transact(() => {
editor.dom.remove(target);
});
editor.nodeChanged();
editor.undoManager.add();
if (editor.getBody().children.length === 0) {
editor.setContent('<p></p>');
editor.selection.setCursorLocation(editor.getBody().firstChild, 0);
}
syncEditorHeight(editor);
});
}
function syncCodeLabels(editor: any): void {
const root: HTMLElement | null = editor?.getBody?.() ?? null;
if (!root) {
return;
}
root.querySelectorAll<HTMLElement>('pre.enlighter-pre[data-jio-code]').forEach((pre) => {
const existing = pre.querySelector(':scope > .jio-code-lang');
const language = pre.getAttribute('data-enlighter-language') || 'generic';
if (existing) {
existing.setAttribute('data-jio-code-lang', language);
const label = CODE_LANGUAGES.find((item) => item.value === language)?.label || language;
if ((existing.textContent || '') !== label) {
existing.textContent = label;
}
return;
}
const badge = document.createElement('span');
badge.className = 'jio-code-lang';
badge.contentEditable = 'false';
badge.setAttribute('data-jio-code-lang', language);
badge.textContent = CODE_LANGUAGES.find((item) => item.value === language)?.label || language;
pre.insertBefore(badge, pre.firstChild);
});
}
function escapeAttribute(value: string): string {
return value.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
@@ -215,6 +372,8 @@ function Studio(): JSX.Element {
}));
const [drawerOpen, setDrawerOpen] = useState(false);
const [blockDialog, setBlockDialog] = useState<BlockDialogKind | null>(null);
const [codeLanguage, setCodeLanguage] = useState<null | string>(null);
const codeTargetRef = useRef<any>(null);
const [notice, setNotice] = useState<{ tone: 'ok' | 'error'; text: string } | null>(null);
const [recovery, setRecovery] = useState<null | { savedAt: string }>(null);
const dirtyRef = useRef(false);
@@ -231,6 +390,7 @@ function Studio(): JSX.Element {
pointsMax: Number(bootstrap.pay?.pointsMax || 0),
};
const isPublishedEdit = !!bootstrap.isPublished;
const charCount = useMemo(() => Array.from(plainText(draft.content)).length, [draft.content]);
const patchDraft = useCallback((patch: Partial<Draft>) => {
dirtyRef.current = true;
@@ -292,7 +452,7 @@ function Studio(): JSX.Element {
async (autosave = false): Promise<boolean> => {
const editor = editorRef.current;
if (editor && !editor.removed) {
updateContent(editor.getContent());
updateContent(serializeBlocks(editor.getContent()));
}
const current = draftRef.current;
if (!dirtyRef.current && (current.postId > 0 || current.versionId > 0)) {
@@ -468,7 +628,8 @@ function Studio(): JSX.Element {
body_class: 'jio-creator__tinymce',
content_css: bootstrap.creator?.contentCss || '',
content_style: EDITOR_CONTENT_STYLE,
height: 650,
// iframe 高度随后由 autoresize 随正文增长,这里只给初始值。
height: 520,
placeholder: String(bootstrap.limits?.contentPlaceholder || ''),
setup: (editor: any) => {
editorRef.current = editor;
@@ -476,17 +637,24 @@ function Studio(): JSX.Element {
editorReadyRef.current = true;
setEditorReady(true);
if (pendingRestoreContentRef.current !== null) {
editor.setContent(pendingRestoreContentRef.current);
editor.setContent(hydrateBlocks(pendingRestoreContentRef.current));
pendingRestoreContentRef.current = null;
} else {
editor.setContent(hydrateBlocks(editor.getContent()));
}
syncCodeLabels(editor);
syncEditorHeight(editor);
installBlockActions(editor);
updateHistoryAvailability(editor);
updateFormatValue(editor);
});
editor.on('NodeChange', () => updateFormatValue(editor));
// 代码块右上角的语言标签:点击整块即可换语言,等价于公众号的「高亮」入口。
editor.on('input change Undo Redo AddUndo', () => {
updateContent(editor.getContent());
updateContent(serializeBlocks(editor.getContent()));
updateHistoryAvailability(editor);
syncCodeLabels(editor);
syncEditorHeight(editor);
});
editor.on('keydown', (event: any) => {
if ((event.metaKey || event.ctrlKey) && event.keyCode === 83) {
@@ -601,8 +769,9 @@ function Studio(): JSX.Element {
}
editor.insertContent(html);
editor.nodeChanged();
updateContent(editor.getContent());
updateContent(serializeBlocks(editor.getContent()));
updateHistoryAvailability(editor);
syncCodeLabels(editor);
}, [focusEditor]);
// 直接插入、无需参数的小块;需要参数的块打开通用弹窗。
@@ -736,18 +905,6 @@ function Studio(): JSX.Element {
onInsertBlock={insertBlock}
onInsertQuote={insertQuote}
/>
<div className="jio-creator__bar-actions jio-creator__toolbar-extension" data-toolbar-extension aria-label={__('工具扩展区', 'jio')}>
<SaveState value={saveState} />
<button type="button" className="jio-creator__save-draft" onClick={() => void save(false)} disabled={!editorReady}>
<i className="fa fas fa-save" aria-hidden="true" /><span>{__('保存草稿', 'jio')}</span>
</button>
<button type="button" className="jio-creator__ghost jio-creator__preview" onClick={() => void preview()}>
{__('预览', 'jio')}
</button>
<button type="button" className="jio-creator__primary" onClick={() => setDrawerOpen(true)}>
{isPublishedEdit ? __('提交改稿', 'jio') : __('发布', 'jio')}
</button>
</div>
</header>
{recovery && (
@@ -803,6 +960,48 @@ function Studio(): JSX.Element {
onScheduleChange={(enabled, at) => patchDraft({ schedule: { enabled, at } })}
/>
{codeLanguage !== null && (
<div className="jio-creator-dialog" role="dialog" aria-modal="true" aria-labelledby="jio-code-lang-title">
<button type="button" className="jio-creator-dialog__backdrop" aria-label={__('关闭', 'jio')} onClick={() => setCodeLanguage(null)} />
<div className="jio-creator-dialog__panel">
<header className="jio-creator-dialog__head">
<h2 id="jio-code-lang-title">{__('代码语言', 'jio')}</h2>
<button type="button" className="jio-creator-dialog__close" onClick={() => setCodeLanguage(null)} aria-label={__('关闭', 'jio')}>
<i className="fa fas fa-times" aria-hidden="true" />
</button>
</header>
<div className="jio-creator-dialog__body">
<div className="jio-creator__lang-grid">
{CODE_LANGUAGES.map((item) => (
<button
key={item.value}
type="button"
className={`jio-creator__lang-item${item.value === codeLanguage ? ' is-active' : ''}`}
onClick={() => {
const target = codeTargetRef.current;
codeTargetRef.current = null;
setCodeLanguage(null);
if (!target) {
return;
}
target.setAttribute('data-enlighter-language', item.value);
const code = target.querySelector('code');
if (code) {
code.setAttribute('data-enlighter-language', item.value);
}
syncCodeLabels(editorRef.current);
updateContent(serializeBlocks(editorRef.current.getContent()));
}}
>
{item.label}
</button>
))}
</div>
</div>
</div>
</div>
)}
{blockDialog && (
<BlockDialog
kind={blockDialog}
@@ -811,6 +1010,16 @@ function Studio(): JSX.Element {
/>
)}
<StatusBar
charCount={charCount}
saveState={saveState}
isPublishedEdit={isPublishedEdit}
saving={!editorReady || saveState.state === 'saving' || saveState.state === 'submitting'}
onSaveDraft={() => void save(false)}
onPreview={() => void preview()}
onPublish={() => setDrawerOpen(true)}
/>
{drawerOpen && (
<PublishDrawer
draft={draft}
@@ -259,3 +259,203 @@
.is-green .jio-creator__quote-swatch {
background: #23790e;
}
/* ---------- 底部固定状态栏 ---------- */
.jio-creator__status {
position: fixed;
z-index: 310;
right: 0;
bottom: 0;
left: 0;
display: flex;
height: var(--jc-status);
align-items: center;
gap: 16px;
padding: 0 clamp(14px, 2.4vw, 28px) env(safe-area-inset-bottom);
border-top: 1px solid #e6e9e8;
background: rgba(255, 255, 255, 0.97);
color: #7d8683;
font-size: 12px;
box-shadow: 0 -2px 10px rgba(24, 39, 34, 0.04);
}
.jio-creator__status-left,
.jio-creator__status-meta,
.jio-creator__status-actions {
display: flex;
min-width: 0;
align-items: center;
gap: 10px;
}
.jio-creator__status-count {
color: #4f5957;
font-variant-numeric: tabular-nums;
}
.jio-creator__status-mode {
overflow: hidden;
color: #a3aca9;
text-overflow: ellipsis;
white-space: nowrap;
}
.jio-creator__status-meta {
color: #93a09b;
.jio-creator__save {
font-size: 12px;
}
}
.jio-creator__status-dot {
color: #c9d1ce;
}
.jio-creator__status-actions {
margin-left: auto;
gap: 8px;
}
.jio-creator__status-ghost,
.jio-creator__status-primary {
display: inline-flex;
min-height: 32px;
align-items: center;
justify-content: center;
padding: 0 14px;
border-radius: 7px;
cursor: pointer;
font-size: 13px;
white-space: nowrap;
}
.jio-creator__status-ghost {
border: 1px solid #e3e7e5;
background: #fff;
color: #5f6664;
&:hover:not(:disabled),
&:focus-visible:not(:disabled) {
border-color: #b7cdc3;
color: var(--jc-accent-strong);
outline: 0;
}
&:disabled {
opacity: 0.55;
cursor: default;
}
}
.jio-creator__status-primary {
border: 1px solid transparent;
background: var(--jc-accent);
color: #fff;
&:hover:not(:disabled),
&:focus-visible:not(:disabled) {
background: var(--jc-accent-strong);
outline: 0;
}
&:disabled {
opacity: 0.55;
cursor: default;
}
}
/* ---------- 代码语言选择 ---------- */
.jio-creator__lang-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 8px;
}
.jio-creator__lang-item {
padding: 9px 6px;
border: 1px solid #e6e9e8;
border-radius: 8px;
background: #fff;
color: #4f5957;
cursor: pointer;
font-size: 13px;
&:hover,
&:focus-visible {
border-color: #b7cdc3;
color: var(--jc-accent-strong);
outline: 0;
}
&.is-active {
border-color: var(--jc-accent);
background: #eff6f3;
color: var(--jc-accent-strong);
}
}
/* ---------- 块级删除控件(接近 WPS 的「块操作」) ---------- */
.jio-creator__block-actions {
position: absolute;
z-index: 5;
top: 6px;
right: 6px;
display: flex;
gap: 6px;
opacity: 0;
transition: opacity 0.15s ease;
}
.jio-creator__block-actions.is-visible {
opacity: 1;
}
.jio-creator__block-action {
display: inline-flex;
width: 26px;
height: 26px;
align-items: center;
justify-content: center;
border: 1px solid #e3e7e5;
border-radius: 6px;
background: #fff;
color: #7d8683;
cursor: pointer;
font-size: 12px;
&:hover,
&:focus-visible {
border-color: #e6b8ad;
color: #b4573f;
outline: 0;
}
}
@media (max-width: 767px) {
.jio-creator__status {
gap: 8px;
height: 56px;
padding: 0 10px env(safe-area-inset-bottom);
}
.jio-creator__status-mode,
.jio-creator__status-meta {
display: none;
}
.jio-creator__status-ghost {
padding: 0 10px;
}
.jio-creator__status-primary {
padding: 0 12px;
}
.jio-creator__lang-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
+22 -5
View File
@@ -49,11 +49,13 @@ body.jio-page-submit.has-static-nav {
--jc-accent-strong: #{$accent-strong};
--jc-warn: #{$warn};
--jc-bar: 64px;
--jc-status: 52px;
min-height: 100vh;
min-height: 100dvh;
/* 固定顶栏属于工作台 chrome,不随正文一起滚动。 */
/* 固定顶栏 / 底栏属于工作台 chrome,不随正文一起滚动。 */
padding-top: var(--jc-bar);
padding-bottom: var(--jc-status);
background: var(--jc-paper);
color: var(--jc-ink);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;
@@ -217,12 +219,13 @@ body.jio-page-submit.has-static-nav {
white-space: nowrap;
}
/* 标题按钮沿用「图标 + 文字」外观,内部叠一个原生的级别下拉。 */
/* 标题按钮沿用「图标 + 文字」外观,内部叠一个原生的级别下拉。
几何与 .jio-creator__tool-btn 完全一致,否则会在同一行里错位。 */
.jio-creator__format-field {
position: relative;
display: inline-flex;
width: auto;
min-width: 48px;
min-width: 40px;
height: 46px;
flex: 0 0 auto;
align-items: center;
@@ -230,8 +233,14 @@ body.jio-page-submit.has-static-nav {
flex-direction: column;
gap: 2px;
padding: 5px 6px;
border: 0;
border-radius: 6px;
background: transparent;
color: #5f6664;
cursor: pointer;
font-size: 11px;
line-height: 1.2;
vertical-align: middle;
i {
font-size: 14px;
@@ -454,6 +463,7 @@ body.jio-page-submit.has-static-nav {
.jio-creator__editor {
position: relative;
/* 正文随内容增长,滚动交给整个页面,避免出现第二根滚动条。 */
min-height: 52vh;
margin: 22px 0 0;
@@ -489,7 +499,9 @@ body.jio-page-submit.has-static-nav {
.mce-edit-area iframe,
.tox .tox-edit-area__iframe {
/* 自适应高度由脚本同步,这里只给下限,不设内心滚动。 */
min-height: 52vh;
overflow: hidden;
}
textarea {
@@ -735,8 +747,13 @@ body.jio-page-submit.has-static-nav {
}
.jio-creator__format-field {
min-width: 42px;
min-width: 36px;
height: 46px;
padding: 4px 4px;
i {
font-size: 13px;
}
}
.jio-creator__bar-actions .jio-creator__save {
@@ -778,7 +795,7 @@ body.jio-page-submit.has-static-nav {
.jio-creator__settings {
width: calc(100% - 20px);
margin: 12px auto calc(40px + env(safe-area-inset-bottom));
margin: 12px auto calc(24px + env(safe-area-inset-bottom));
padding: 18px;
}
+34 -15
View File
@@ -51,19 +51,26 @@ export function codeLabel(language: string): string {
return CODE_LANGUAGES.find((item) => item.value === language)?.label || language;
}
/** 代码块:语言写在 data 属性里,前台 enlighter 与编辑器标签共用。 */
/** 代码块右上角角标:编辑器内由 React 渲染,这里给出标识供 CSS 定位。 */
export const CODE_LABEL_CLASS = 'jio-code-lang';
/**
* 代码块:语言写在 data 属性里,前台 enlighter 与编辑器角标共用。
*
* `data-jio-code` 标记这是编辑器创建的块,编辑器据此保证回车不跳出代码区、
* 并渲染可点击的语言角标。
*/
export function codeBlockMarkup(language = 'generic', code = ''): string {
const body = code !== '' ? escapeHtml(code) : escapeHtml(__('在此输入代码', 'jio'));
// data-jio-code-label 供编辑器角标直接显示,避免在 CSS 里映射语言标识。
const label = escapeHtml(codeLabel(language));
return (
`<pre class="enlighter-pre" data-enlighter-language="${escapeHtml(language)}" data-jio-code-label="${label}">` +
`<pre class="enlighter-pre" data-jio-code="1" data-enlighter-language="${escapeHtml(language)}">` +
`<code class="gl" data-enlighter-language="${escapeHtml(language)}">` +
body +
'</code></pre><p></p>'
);
}
/** 把代码块的语言写成编辑器里的内联标签(纯文本,保存前由编辑器移除)。 */
export type TableOptions = {
rows: number;
cols: number;
@@ -102,21 +109,33 @@ export function tocMarkup(): string {
return '<p>[jio_toc]</p><p></p>';
}
/** 注释:用短代码落库,服务端生成上标序号与文末注释列表。 */
export function footnoteMarkup(note: string): string {
const text = note.trim() || __('注释内容', 'jio');
// 短代码属性里再放 HTML 会被 kses 改写,这里只允许纯文本内容。
return `<p>[jio_footnote]${escapeHtml(text)}[/jio_footnote]</p>`;
}
export type MathOptions = { formula: string; display: boolean };
/** 公式:用短代码落库,服务端统一做上标 / 下标排版。 */
/** 公式的上标 / 下标排版(与 ContentBlocks::typeset_math 保持一致)。 */
export function typesetMath(raw: string): string {
const escaped = escapeHtml(raw);
return escaped
.replace(/\^\{([^{}]*)\}|\^(\S)/gu, '<sup>$1$2</sup>')
.replace(/_\{([^{}]*)\}|_(\S)/gu, '<sub>$1$2</sub>');
}
/**
* 公式:编辑器里直接显示排版结果,保存前由 `serializeBlocks` 换回短代码。
*/
export function mathMarkup({ formula, display }: MathOptions): string {
const body = escapeHtml(formula.trim());
const body = typesetMath(formula.trim());
const attrs = `data-jio-math-source="${escapeHtml(formula.trim())}" data-jio-math-display="${display ? '1' : '0'}"`;
return display
? `<p>[jio_math display="1"]${body}[/jio_math]</p>`
: `<p>[jio_math display="0"]${body}[/jio_math]</p>`;
? `<p><span class="jio-math jio-math--block" ${attrs} contenteditable="false">${body}</span></p>`
: `<span class="jio-math" ${attrs}>${body}</span>`;
}
/**
* 注释:编辑器里显示上标序号,保存前由 `serializeBlocks` 换回短代码。
*/
export function footnoteMarkup(note: string): string {
const text = note.trim() || __('注释内容', 'jio');
return `<sup class="jio-footnote-ref" data-jio-footnote="${escapeHtml(text)}" contenteditable="false">${escapeHtml(text)}</sup>`;
}
export type QuoteColor = '' | 'qe_wzk_c-red' | 'qe_wzk_lan' | 'qe_wzk_lv';
@@ -0,0 +1,83 @@
/**
* 编辑器内容 ↔ 保存格式的转换。
*
* 编辑器里把公式、注释、代码语言标签渲染成可读的样子(方便写作时看到最终效果),
* 但落库必须回到短代码/纯标记,服务端才能稳定渲染。这里在两个方向上都做一次转换。
*
* @package Jio
*/
const escapeAttr = (value: string): string =>
value.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
const decodeEntities = (value: string): string => {
const area = document.createElement('textarea');
area.innerHTML = value;
return area.value;
};
/**
* 保存前:把编辑器里的可视化块换回短代码 / 纯标记。
*/
export function serializeBlocks(html: string): string {
if (!html || (html.indexOf('data-jio-') === -1 && html.indexOf('jio-code-lang') === -1)) {
return html;
}
const container = document.createElement('div');
container.innerHTML = html;
// 公式
container.querySelectorAll<HTMLElement>('[data-jio-math-source]').forEach((node) => {
const formula = node.getAttribute('data-jio-math-source') || '';
const display = node.getAttribute('data-jio-math-display') !== '0';
const shortcode = `[jio_math display="${display ? '1' : '0'}"]${formula}[/jio_math]`;
const paragraph = node.closest('p');
if (display && paragraph && paragraph.textContent?.trim() === node.textContent?.trim()) {
paragraph.textContent = shortcode;
} else {
node.replaceWith(document.createTextNode(shortcode));
}
});
// 注释
container.querySelectorAll<HTMLElement>('[data-jio-footnote]').forEach((node) => {
const note = node.getAttribute('data-jio-footnote') || '';
node.replaceWith(document.createTextNode(`[jio_footnote]${note}[/jio_footnote]`));
});
// 代码块语言角标只是编辑期提示
container.querySelectorAll('.jio-code-lang').forEach((node) => node.remove());
return container.innerHTML;
}
/**
* 打开已有内容时:把短代码还原成编辑器里的可视化块。
*/
export function hydrateBlocks(html: string): string {
if (!html || html.indexOf('[jio_') === -1) {
return html;
}
return html
.replace(
/\[jio_math display="([01])"\]([\s\S]*?)\[\/jio_math\]/g,
(_match, display: string, formula: string) =>
display === '0'
? `<span class="jio-math" data-jio-math-source="${escapeAttr(formula)}" data-jio-math-display="0" contenteditable="false">${typeset(formula)}</span>`
: `<p><span class="jio-math jio-math--block" data-jio-math-source="${escapeAttr(formula)}" data-jio-math-display="1" contenteditable="false">${typeset(formula)}</span></p>`
)
.replace(
/\[jio_footnote\]([\s\S]*?)\[\/jio_footnote\]/g,
(_match, note: string) =>
`<sup class="jio-footnote-ref" data-jio-footnote="${escapeAttr(decodeEntities(note))}" contenteditable="false">${note}</sup>`
);
}
const typeset = (raw: string): string => {
const escaped = raw.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
return escaped
.replace(/\^\{([^{}]*)\}|\^(\S)/gu, '<sup>$1$2</sup>')
.replace(/_\{([^{}]*)\}|_(\S)/gu, '<sub>$1$2</sub>');
};
@@ -176,14 +176,16 @@ test('fixed top toolbar has TinyMCE formatting, media, and extension actions', (
assert.match(appSource, /onInsertBlock=\{insertBlock\}/);
assert.match(appSource, /onInsertQuote=\{insertQuote\}/);
const markup = read('assets/creator-studio/src/utils/block-markup.ts');
assert.match(markup, /jio_math/);
assert.match(markup, /jio-math/);
// 引用沿用主题的 quote_q 结构与配色类。
assert.match(markup, /quote_q/);
assert.match(markup, /qe_wzk_c-red/);
// 公式与注释用短代码落库,由服务端保证渲染。
assert.doesNotMatch(markup, /E = mc\^2/);
assert.match(markup, /\[jio_math display="1"\]/);
assert.match(markup, /\[jio_footnote\]/);
// 编辑器展示渲染结果,序列化时再换回短代码。
assert.match(markup, /data-jio-math-source/);
assert.match(read('assets/creator-studio/src/utils/serialize.ts'), /\[jio_math display=/);
assert.match(read('assets/creator-studio/src/utils/serialize.ts'), /\[jio_footnote\]/);
assert.match(markup, /\[jio_toc\]/);
// 代码块角标不再输出固定「高亮代码」文案。
assert.doesNotMatch(read('assets/css/frontend/pages/submit-editor.css'), /高亮代码/);
@@ -204,9 +206,17 @@ test('fixed top toolbar has TinyMCE formatting, media, and extension actions', (
// 工具栏按钮统一为「图标在上、文字在下」。
assert.match(editorToolbar, /jio-creator__tool-label/);
assert.match(read('assets/creator-studio/src/styles/studio.scss'), /\.jio-creator__tool-btn\s*\{[\s\S]*?flex-direction:\s*column/);
assert.match(appSource, /保存草稿/);
assert.match(appSource, /<SaveState value=\{saveState\}/);
// 保存 / 预览 / 发布固定在底部状态栏,顶栏只保留工具。
assert.match(appSource, /<StatusBar/);
assert.match(read('assets/creator-studio/src/components/status-bar.tsx'), /保存草稿/);
assert.match(read('assets/creator-studio/src/components/status-bar.tsx'), /预览/);
assert.match(read('assets/creator-studio/src/components/status-bar.tsx'), /字数/);
assert.match(read('assets/creator-studio/src/components/status-bar.tsx'), /Markdown 语法输入中/);
assert.match(read('assets/creator-studio/src/components/status-bar.tsx'), /<SaveState value=\{saveState\}/);
assert.match(appSource, /draftRef\.current\.postId/);
const barStyles = read('assets/creator-studio/src/styles/_block-tools.scss');
assert.match(barStyles, /\.jio-creator__status\s*\{[\s\S]*?position:\s*fixed/);
assert.match(barStyles, /\.jio-creator__status\s*\{[\s\S]*?bottom:\s*0/);
assert.match(editorToolbar, /role="toolbar"/);
assert.match(read('assets/creator-studio/src/styles/studio.scss'), /\.jio-creator__bar\s*\{[^}]*position:\s*fixed/);
assert.match(read('assets/creator-studio/src/styles/studio.scss'), /\.jio-creator\s*\{[\s\S]*?padding-top:\s*var\(--jc-bar\)/);
@@ -289,9 +299,8 @@ test('the writing card has no author strip above the title', () => {
assert.doesNotMatch(read('assets/creator-studio/src/styles/studio.scss'), /jio-creator__author/);
});
test('the writing surface has no character or reading-time footer', () => {
test('character count lives in the fixed bottom bar, not a reading-time footer', () => {
assert.doesNotMatch(appSource, /jio-creator__footer/);
assert.doesNotMatch(appSource, /分钟阅读/);
assert.doesNotMatch(appSource, /charCount/);
assert.doesNotMatch(read('assets/creator-studio/src/styles/studio.scss'), /\.jio-creator__footer/);
assert.match(read('assets/creator-studio/src/components/status-bar.tsx'), /字数/);
});