feat: enhance page block system with custom styling support and add TinyMCE rich editor integration
This commit is contained in:
File diff suppressed because one or more lines are too long
BIN
Binary file not shown.
|
After Width: | Height: | Size: 106 KiB |
+5
File diff suppressed because one or more lines are too long
BIN
Binary file not shown.
+7
File diff suppressed because one or more lines are too long
BIN
Binary file not shown.
BIN
Binary file not shown.
|
After Width: | Height: | Size: 274 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 166 KiB |
+1
File diff suppressed because one or more lines are too long
BIN
Binary file not shown.
+1
File diff suppressed because one or more lines are too long
BIN
Binary file not shown.
+8
@@ -0,0 +1,8 @@
|
||||
(function(){"use strict";try{if(typeof document<"u"){var e=document.createElement("style");e.appendChild(document.createTextNode(".ce-code__textarea{min-height:200px;font-family:Menlo,Monaco,Consolas,Courier New,monospace;color:#41314e;line-height:1.6em;font-size:12px;background:#f8f7fa;border:1px solid #f1f1f4;box-shadow:none;white-space:pre;word-wrap:normal;overflow-x:auto;resize:vertical}")),document.head.appendChild(e)}}catch(o){console.error("vite-plugin-css-injected-by-js",o)}})();
|
||||
(function(i,o){typeof exports=="object"&&typeof module<"u"?module.exports=o():typeof define=="function"&&define.amd?define(o):(i=typeof globalThis<"u"?globalThis:i||self,i.CodeTool=o())})(this,function(){"use strict";function i(h,t){let s="";for(;s!==`
|
||||
`&&t>0;)t=t-1,s=h.substr(t,1);return s===`
|
||||
`&&(t+=1),t}const o='<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 8L5 12L9 16"/><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 8L19 12L15 16"/></svg>';/**
|
||||
* CodeTool for Editor.js
|
||||
* @version 2.0.0
|
||||
* @license MIT
|
||||
*/class l{static get isReadOnlySupported(){return!0}static get enableLineBreaks(){return!0}constructor({data:t,config:e,api:s,readOnly:r}){this.api=s,this.readOnly=r,this.placeholder=this.api.i18n.t(e.placeholder||l.DEFAULT_PLACEHOLDER),this.CSS={baseClass:this.api.styles.block,input:this.api.styles.input,wrapper:"ce-code",textarea:"ce-code__textarea"},this.nodes={holder:null,textarea:null},this.data={code:t.code??""},this.nodes.holder=this.drawView()}render(){return this.nodes.holder}save(t){return{code:t.querySelector("textarea").value}}onPaste(t){const e=t.detail;if("data"in e){const s=e.data;this.data={code:s||""}}}get data(){return this._data}set data(t){this._data=t,this.nodes.textarea&&(this.nodes.textarea.value=t.code)}static get toolbox(){return{icon:o,title:"Code"}}static get DEFAULT_PLACEHOLDER(){return"Enter a code"}static get pasteConfig(){return{tags:["pre"]}}static get sanitize(){return{code:!0}}tabHandler(t){t.stopPropagation(),t.preventDefault();const e=t.target,s=t.shiftKey,r=e.selectionStart,a=e.value,n=" ";let d;if(!s)d=r+n.length,e.value=a.substring(0,r)+n+a.substring(r);else{const c=i(a,r);if(a.substr(c,n.length)!==n)return;e.value=a.substring(0,c)+a.substring(c+n.length),d=r-n.length}e.setSelectionRange(d,d)}drawView(){const t=document.createElement("div"),e=document.createElement("textarea");return t.classList.add(this.CSS.baseClass,this.CSS.wrapper),e.classList.add(this.CSS.textarea,this.CSS.input),e.value=this.data.code,e.placeholder=this.placeholder,this.readOnly&&(e.disabled=!0),t.appendChild(e),e.addEventListener("keydown",s=>{switch(s.code){case"Tab":this.tabHandler(s);break}}),this.nodes.textarea=e,t}}return l});
|
||||
BIN
Binary file not shown.
@@ -0,0 +1,560 @@
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: italic;
|
||||
font-weight: 200;
|
||||
src: url(https://fonts.gstatic.com/s/nunito/v32/XRXX3I6Li01BKofIMNaORs71cA.woff2) format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: italic;
|
||||
font-weight: 200;
|
||||
src: url(https://fonts.gstatic.com/s/nunito/v32/XRXX3I6Li01BKofIMNaHRs71cA.woff2) format('woff2');
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: italic;
|
||||
font-weight: 200;
|
||||
src: url(https://fonts.gstatic.com/s/nunito/v32/XRXX3I6Li01BKofIMNaMRs71cA.woff2) format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: italic;
|
||||
font-weight: 200;
|
||||
src: url(https://fonts.gstatic.com/s/nunito/v32/XRXX3I6Li01BKofIMNaNRs71cA.woff2) format('woff2');
|
||||
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: italic;
|
||||
font-weight: 200;
|
||||
src: url(https://fonts.gstatic.com/s/nunito/v32/XRXX3I6Li01BKofIMNaDRs4.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
src: url(https://fonts.gstatic.com/s/nunito/v32/XRXX3I6Li01BKofIMNaORs71cA.woff2) format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
src: url(https://fonts.gstatic.com/s/nunito/v32/XRXX3I6Li01BKofIMNaHRs71cA.woff2) format('woff2');
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
src: url(https://fonts.gstatic.com/s/nunito/v32/XRXX3I6Li01BKofIMNaMRs71cA.woff2) format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
src: url(https://fonts.gstatic.com/s/nunito/v32/XRXX3I6Li01BKofIMNaNRs71cA.woff2) format('woff2');
|
||||
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
src: url(https://fonts.gstatic.com/s/nunito/v32/XRXX3I6Li01BKofIMNaDRs4.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
src: url(https://fonts.gstatic.com/s/nunito/v32/XRXX3I6Li01BKofIMNaORs71cA.woff2) format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
src: url(https://fonts.gstatic.com/s/nunito/v32/XRXX3I6Li01BKofIMNaHRs71cA.woff2) format('woff2');
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
src: url(https://fonts.gstatic.com/s/nunito/v32/XRXX3I6Li01BKofIMNaMRs71cA.woff2) format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
src: url(https://fonts.gstatic.com/s/nunito/v32/XRXX3I6Li01BKofIMNaNRs71cA.woff2) format('woff2');
|
||||
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
src: url(https://fonts.gstatic.com/s/nunito/v32/XRXX3I6Li01BKofIMNaDRs4.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: italic;
|
||||
font-weight: 600;
|
||||
src: url(https://fonts.gstatic.com/s/nunito/v32/XRXX3I6Li01BKofIMNaORs71cA.woff2) format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: italic;
|
||||
font-weight: 600;
|
||||
src: url(https://fonts.gstatic.com/s/nunito/v32/XRXX3I6Li01BKofIMNaHRs71cA.woff2) format('woff2');
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: italic;
|
||||
font-weight: 600;
|
||||
src: url(https://fonts.gstatic.com/s/nunito/v32/XRXX3I6Li01BKofIMNaMRs71cA.woff2) format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: italic;
|
||||
font-weight: 600;
|
||||
src: url(https://fonts.gstatic.com/s/nunito/v32/XRXX3I6Li01BKofIMNaNRs71cA.woff2) format('woff2');
|
||||
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: italic;
|
||||
font-weight: 600;
|
||||
src: url(https://fonts.gstatic.com/s/nunito/v32/XRXX3I6Li01BKofIMNaDRs4.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
src: url(https://fonts.gstatic.com/s/nunito/v32/XRXX3I6Li01BKofIMNaORs71cA.woff2) format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
src: url(https://fonts.gstatic.com/s/nunito/v32/XRXX3I6Li01BKofIMNaHRs71cA.woff2) format('woff2');
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
src: url(https://fonts.gstatic.com/s/nunito/v32/XRXX3I6Li01BKofIMNaMRs71cA.woff2) format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
src: url(https://fonts.gstatic.com/s/nunito/v32/XRXX3I6Li01BKofIMNaNRs71cA.woff2) format('woff2');
|
||||
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
src: url(https://fonts.gstatic.com/s/nunito/v32/XRXX3I6Li01BKofIMNaDRs4.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: italic;
|
||||
font-weight: 800;
|
||||
src: url(https://fonts.gstatic.com/s/nunito/v32/XRXX3I6Li01BKofIMNaORs71cA.woff2) format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: italic;
|
||||
font-weight: 800;
|
||||
src: url(https://fonts.gstatic.com/s/nunito/v32/XRXX3I6Li01BKofIMNaHRs71cA.woff2) format('woff2');
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: italic;
|
||||
font-weight: 800;
|
||||
src: url(https://fonts.gstatic.com/s/nunito/v32/XRXX3I6Li01BKofIMNaMRs71cA.woff2) format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: italic;
|
||||
font-weight: 800;
|
||||
src: url(https://fonts.gstatic.com/s/nunito/v32/XRXX3I6Li01BKofIMNaNRs71cA.woff2) format('woff2');
|
||||
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: italic;
|
||||
font-weight: 800;
|
||||
src: url(https://fonts.gstatic.com/s/nunito/v32/XRXX3I6Li01BKofIMNaDRs4.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: italic;
|
||||
font-weight: 900;
|
||||
src: url(https://fonts.gstatic.com/s/nunito/v32/XRXX3I6Li01BKofIMNaORs71cA.woff2) format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: italic;
|
||||
font-weight: 900;
|
||||
src: url(https://fonts.gstatic.com/s/nunito/v32/XRXX3I6Li01BKofIMNaHRs71cA.woff2) format('woff2');
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: italic;
|
||||
font-weight: 900;
|
||||
src: url(https://fonts.gstatic.com/s/nunito/v32/XRXX3I6Li01BKofIMNaMRs71cA.woff2) format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: italic;
|
||||
font-weight: 900;
|
||||
src: url(https://fonts.gstatic.com/s/nunito/v32/XRXX3I6Li01BKofIMNaNRs71cA.woff2) format('woff2');
|
||||
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: italic;
|
||||
font-weight: 900;
|
||||
src: url(https://fonts.gstatic.com/s/nunito/v32/XRXX3I6Li01BKofIMNaDRs4.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: normal;
|
||||
font-weight: 200;
|
||||
src: url(https://fonts.gstatic.com/s/nunito/v32/XRXV3I6Li01BKofIOOaBXso.woff2) format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: normal;
|
||||
font-weight: 200;
|
||||
src: url(https://fonts.gstatic.com/s/nunito/v32/XRXV3I6Li01BKofIMeaBXso.woff2) format('woff2');
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: normal;
|
||||
font-weight: 200;
|
||||
src: url(https://fonts.gstatic.com/s/nunito/v32/XRXV3I6Li01BKofIOuaBXso.woff2) format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: normal;
|
||||
font-weight: 200;
|
||||
src: url(https://fonts.gstatic.com/s/nunito/v32/XRXV3I6Li01BKofIO-aBXso.woff2) format('woff2');
|
||||
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: normal;
|
||||
font-weight: 200;
|
||||
src: url(https://fonts.gstatic.com/s/nunito/v32/XRXV3I6Li01BKofINeaB.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src: url(https://fonts.gstatic.com/s/nunito/v32/XRXV3I6Li01BKofIOOaBXso.woff2) format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src: url(https://fonts.gstatic.com/s/nunito/v32/XRXV3I6Li01BKofIMeaBXso.woff2) format('woff2');
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src: url(https://fonts.gstatic.com/s/nunito/v32/XRXV3I6Li01BKofIOuaBXso.woff2) format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src: url(https://fonts.gstatic.com/s/nunito/v32/XRXV3I6Li01BKofIO-aBXso.woff2) format('woff2');
|
||||
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src: url(https://fonts.gstatic.com/s/nunito/v32/XRXV3I6Li01BKofINeaB.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url(https://fonts.gstatic.com/s/nunito/v32/XRXV3I6Li01BKofIOOaBXso.woff2) format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url(https://fonts.gstatic.com/s/nunito/v32/XRXV3I6Li01BKofIMeaBXso.woff2) format('woff2');
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url(https://fonts.gstatic.com/s/nunito/v32/XRXV3I6Li01BKofIOuaBXso.woff2) format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url(https://fonts.gstatic.com/s/nunito/v32/XRXV3I6Li01BKofIO-aBXso.woff2) format('woff2');
|
||||
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url(https://fonts.gstatic.com/s/nunito/v32/XRXV3I6Li01BKofINeaB.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src: url(https://fonts.gstatic.com/s/nunito/v32/XRXV3I6Li01BKofIOOaBXso.woff2) format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src: url(https://fonts.gstatic.com/s/nunito/v32/XRXV3I6Li01BKofIMeaBXso.woff2) format('woff2');
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src: url(https://fonts.gstatic.com/s/nunito/v32/XRXV3I6Li01BKofIOuaBXso.woff2) format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src: url(https://fonts.gstatic.com/s/nunito/v32/XRXV3I6Li01BKofIO-aBXso.woff2) format('woff2');
|
||||
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src: url(https://fonts.gstatic.com/s/nunito/v32/XRXV3I6Li01BKofINeaB.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: url(https://fonts.gstatic.com/s/nunito/v32/XRXV3I6Li01BKofIOOaBXso.woff2) format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: url(https://fonts.gstatic.com/s/nunito/v32/XRXV3I6Li01BKofIMeaBXso.woff2) format('woff2');
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: url(https://fonts.gstatic.com/s/nunito/v32/XRXV3I6Li01BKofIOuaBXso.woff2) format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: url(https://fonts.gstatic.com/s/nunito/v32/XRXV3I6Li01BKofIO-aBXso.woff2) format('woff2');
|
||||
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: url(https://fonts.gstatic.com/s/nunito/v32/XRXV3I6Li01BKofINeaB.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: normal;
|
||||
font-weight: 800;
|
||||
src: url(https://fonts.gstatic.com/s/nunito/v32/XRXV3I6Li01BKofIOOaBXso.woff2) format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: normal;
|
||||
font-weight: 800;
|
||||
src: url(https://fonts.gstatic.com/s/nunito/v32/XRXV3I6Li01BKofIMeaBXso.woff2) format('woff2');
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: normal;
|
||||
font-weight: 800;
|
||||
src: url(https://fonts.gstatic.com/s/nunito/v32/XRXV3I6Li01BKofIOuaBXso.woff2) format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: normal;
|
||||
font-weight: 800;
|
||||
src: url(https://fonts.gstatic.com/s/nunito/v32/XRXV3I6Li01BKofIO-aBXso.woff2) format('woff2');
|
||||
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: normal;
|
||||
font-weight: 800;
|
||||
src: url(https://fonts.gstatic.com/s/nunito/v32/XRXV3I6Li01BKofINeaB.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
src: url(https://fonts.gstatic.com/s/nunito/v32/XRXV3I6Li01BKofIOOaBXso.woff2) format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
src: url(https://fonts.gstatic.com/s/nunito/v32/XRXV3I6Li01BKofIMeaBXso.woff2) format('woff2');
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
src: url(https://fonts.gstatic.com/s/nunito/v32/XRXV3I6Li01BKofIOuaBXso.woff2) format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
src: url(https://fonts.gstatic.com/s/nunito/v32/XRXV3I6Li01BKofIO-aBXso.woff2) format('woff2');
|
||||
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
src: url(https://fonts.gstatic.com/s/nunito/v32/XRXV3I6Li01BKofINeaB.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
BIN
Binary file not shown.
+1788
File diff suppressed because it is too large
Load Diff
+9
@@ -0,0 +1,9 @@
|
||||
(function(){"use strict";try{if(typeof document<"u"){var e=document.createElement("style");e.appendChild(document.createTextNode('.ce-delimiter{line-height:1.6em;width:100%;text-align:center}.ce-delimiter:before{display:inline-block;content:"***";font-size:30px;line-height:65px;height:30px;letter-spacing:.2em}')),document.head.appendChild(e)}}catch(t){console.error("vite-plugin-css-injected-by-js",t)}})();
|
||||
(function(t,i){typeof exports=="object"&&typeof module<"u"?module.exports=i():typeof define=="function"&&define.amd?define(i):(t=typeof globalThis<"u"?globalThis:t||self,t.Delimiter=i())})(this,function(){"use strict";const t='<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><line x1="6" x2="10" y1="12" y2="12" stroke="currentColor" stroke-linecap="round" stroke-width="2"/><line x1="14" x2="18" y1="12" y2="12" stroke="currentColor" stroke-linecap="round" stroke-width="2"/></svg>';/**
|
||||
* Delimiter Block for the Editor.js.
|
||||
*
|
||||
* @author CodeX (team@ifmo.su)
|
||||
* @copyright CodeX 2018
|
||||
* @license The MIT License (MIT)
|
||||
* @version 2.0.0
|
||||
*/class i{static get isReadOnlySupported(){return!0}static get contentless(){return!0}constructor({data:e,config:s,api:r}){this.api=r,this._CSS={block:this.api.styles.block,wrapper:"ce-delimiter"},this._element=this.drawView(),this.data=e}drawView(){let e=document.createElement("div");return e.classList.add(this._CSS.wrapper,this._CSS.block),e}render(){return this._element}save(e){return{}}static get toolbox(){return{icon:t,title:"Delimiter"}}static get pasteConfig(){return{tags:["HR"]}}onPaste(e){this.data={}}}return i});
|
||||
BIN
Binary file not shown.
+777
@@ -0,0 +1,777 @@
|
||||
/**
|
||||
* SIS Vietnam - Block Editor Configuration
|
||||
*
|
||||
* Built on Editor.js (https://editorjs.io)
|
||||
*
|
||||
* PLUGIN ARCHITECTURE:
|
||||
* =====================
|
||||
* This editor uses a plugin registry pattern. The built-in block types
|
||||
* (Header, List, Quote, etc.) are registered by default.
|
||||
*
|
||||
* To add a CUSTOM BLOCK TYPE in the future:
|
||||
*
|
||||
* 1. Create a new JS file in /js/manage/editor-plugins/
|
||||
* Example: /js/manage/editor-plugins/my-custom-block.js
|
||||
*
|
||||
* 2. In that file, register your tool BEFORE the editor initializes:
|
||||
*
|
||||
* window.SISEditorPlugins = window.SISEditorPlugins || {};
|
||||
* window.SISEditorPlugins['myBlock'] = {
|
||||
* class: MyBlockClass, // Your Editor.js Tool class
|
||||
* inlineToolbar: true, // optional
|
||||
* config: { ... } // optional tool-specific config
|
||||
* };
|
||||
*
|
||||
* 3. Load the script in your template BEFORE editor-config.js:
|
||||
* <script src="/js/manage/editor-plugins/my-custom-block.js"></script>
|
||||
*
|
||||
* 4. The editor will automatically pick up all registered plugins.
|
||||
*/
|
||||
|
||||
// Global plugin registry — external plugins register here
|
||||
window.SISEditorPlugins = window.SISEditorPlugins || {};
|
||||
|
||||
/**
|
||||
* Global Media Library Modal Handler
|
||||
* Callable from any Editor.js plugin via window.openSISMediaModal(callback)
|
||||
*/
|
||||
window.openSISMediaModal = function (callback) {
|
||||
if (window.SISMediaPicker && typeof window.SISMediaPicker.open === 'function') {
|
||||
window.SISMediaPicker.open(callback);
|
||||
} else {
|
||||
window.sisMediaSelectCallback = callback;
|
||||
var modalEl = document.getElementById('sisMediaLibraryModal');
|
||||
if (modalEl && typeof $ !== 'undefined' && $.fn && $.fn.modal) {
|
||||
$(modalEl).modal('show');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Dynamic Monaco Editor Script Loader Helper
|
||||
*/
|
||||
window.loadMonacoEditor = function () {
|
||||
if (window.monaco) return Promise.resolve(window.monaco);
|
||||
if (window._monacoLoadingPromise) return window._monacoLoadingPromise;
|
||||
|
||||
window._monacoLoadingPromise = new Promise((resolve, reject) => {
|
||||
if (typeof require !== 'undefined' && require.config) {
|
||||
require(['vs/editor/editor.main'], function () {
|
||||
resolve(window.monaco);
|
||||
});
|
||||
return;
|
||||
}
|
||||
const script = document.createElement('script');
|
||||
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.45.0/min/vs/loader.min.js';
|
||||
script.onload = () => {
|
||||
window.require.config({
|
||||
paths: { vs: 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.45.0/min/vs' }
|
||||
});
|
||||
window.require(['vs/editor/editor.main'], () => {
|
||||
resolve(window.monaco);
|
||||
});
|
||||
};
|
||||
script.onerror = (err) => reject(err);
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
|
||||
return window._monacoLoadingPromise;
|
||||
};
|
||||
|
||||
class SISRawHtmlTool {
|
||||
static get toolbox() {
|
||||
return {
|
||||
title: 'Raw HTML',
|
||||
icon: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="16 18 22 12 16 6"></polyline><polyline points="8 6 2 12 8 18"></polyline></svg>'
|
||||
};
|
||||
}
|
||||
|
||||
constructor({ data, api, readOnly }) {
|
||||
this.api = api;
|
||||
this.readOnly = readOnly;
|
||||
this.data = {
|
||||
html: (data && data.html) ? data.html : '',
|
||||
stretched: !!(data && data.stretched)
|
||||
};
|
||||
this.textarea = null;
|
||||
this.editorInstance = null;
|
||||
}
|
||||
|
||||
render() {
|
||||
const container = document.createElement('div');
|
||||
container.style.width = '100%';
|
||||
container.className = 'ce-raw-html-wrapper mb-3';
|
||||
|
||||
const stretchWrapper = document.createElement('div');
|
||||
stretchWrapper.className = 'custom-control custom-switch mb-2';
|
||||
this.stretchCheck = document.createElement('input');
|
||||
this.stretchCheck.type = 'checkbox';
|
||||
this.stretchCheck.className = 'custom-control-input';
|
||||
this.stretchCheck.id = 'raw_stretch_' + Math.random().toString(36).substring(7);
|
||||
this.stretchCheck.checked = !!(this.data && this.data.stretched);
|
||||
|
||||
const stretchLabel = document.createElement('label');
|
||||
stretchLabel.className = 'custom-control-label small font-weight-bold text-secondary';
|
||||
stretchLabel.htmlFor = this.stretchCheck.id;
|
||||
stretchLabel.innerHTML = '<i class="fas fa-arrows-alt-h"></i> Stretch block to Full Screen Width (Independent Breakout)';
|
||||
|
||||
this.stretchCheck.addEventListener('change', () => {
|
||||
this.data.stretched = this.stretchCheck.checked;
|
||||
});
|
||||
|
||||
stretchWrapper.appendChild(this.stretchCheck);
|
||||
stretchWrapper.appendChild(stretchLabel);
|
||||
container.appendChild(stretchWrapper);
|
||||
|
||||
// Header status bar
|
||||
const headerBar = document.createElement('div');
|
||||
headerBar.className = 'bg-dark text-white p-2 rounded-top d-flex justify-content-between align-items-center small font-weight-bold';
|
||||
headerBar.innerHTML = '<span><i class="fab fa-html5 text-danger"></i> Raw HTML Code (Monaco IDE Editor)</span><span class="badge badge-info font-weight-normal">HTML5</span>';
|
||||
container.appendChild(headerBar);
|
||||
|
||||
// Monaco Container
|
||||
const editorContainer = document.createElement('div');
|
||||
editorContainer.className = 'monaco-editor-box border border-dark rounded-bottom';
|
||||
editorContainer.style.height = '350px';
|
||||
editorContainer.style.width = '100%';
|
||||
container.appendChild(editorContainer);
|
||||
|
||||
// Textarea fallback (hidden when Monaco is active)
|
||||
this.textarea = document.createElement('textarea');
|
||||
this.textarea.className = 'form-control d-none';
|
||||
this.textarea.value = this.data.html || '';
|
||||
container.appendChild(this.textarea);
|
||||
|
||||
if (this.readOnly) {
|
||||
this.stretchCheck.disabled = true;
|
||||
this.textarea.disabled = true;
|
||||
}
|
||||
|
||||
// Initialize Monaco Editor
|
||||
window.loadMonacoEditor().then((monaco) => {
|
||||
if (!editorContainer || !document.body.contains(editorContainer)) return;
|
||||
this.editorInstance = monaco.editor.create(editorContainer, {
|
||||
value: this.data.html || '',
|
||||
language: 'html',
|
||||
theme: 'vs-dark',
|
||||
automaticLayout: true,
|
||||
minimap: { enabled: false },
|
||||
fontSize: 13,
|
||||
lineNumbers: 'on',
|
||||
tabSize: 2,
|
||||
scrollBeyondLastLine: false,
|
||||
readOnly: !!this.readOnly
|
||||
});
|
||||
|
||||
this.editorInstance.onDidChangeModelContent(() => {
|
||||
this.data.html = this.editorInstance.getValue();
|
||||
if (this.textarea) this.textarea.value = this.data.html;
|
||||
});
|
||||
}).catch((err) => {
|
||||
console.warn('[SIS Raw HTML Tool] Monaco Editor CDN fallback to Textarea:', err);
|
||||
this.textarea.classList.remove('d-none');
|
||||
this.textarea.style.minHeight = '300px';
|
||||
this.textarea.style.backgroundColor = '#1e1e24';
|
||||
this.textarea.style.color = '#66d9ef';
|
||||
this.textarea.style.fontFamily = 'monospace';
|
||||
editorContainer.style.display = 'none';
|
||||
|
||||
this.textarea.addEventListener('input', () => {
|
||||
this.data.html = this.textarea.value;
|
||||
});
|
||||
});
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
save() {
|
||||
const htmlVal = this.editorInstance ? this.editorInstance.getValue() : (this.textarea ? this.textarea.value : (this.data.html || ''));
|
||||
return {
|
||||
html: htmlVal,
|
||||
stretched: this.stretchCheck ? this.stretchCheck.checked : !!(this.data && this.data.stretched)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class SISAccordionTool {
|
||||
static get toolbox() {
|
||||
return {
|
||||
title: 'Accordion / Collapse',
|
||||
icon: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 9l6 6 6-6"/></svg>'
|
||||
};
|
||||
}
|
||||
|
||||
constructor({ data, api, readOnly }) {
|
||||
this.api = api;
|
||||
this.readOnly = readOnly;
|
||||
this.data = {
|
||||
title: (data && data.title) ? data.title : '',
|
||||
content: (data && data.content) ? data.content : '',
|
||||
isOpen: !!(data && data.isOpen),
|
||||
stretched: !!(data && data.stretched)
|
||||
};
|
||||
this.titleInput = null;
|
||||
this.contentInput = null;
|
||||
}
|
||||
|
||||
render() {
|
||||
const container = document.createElement('div');
|
||||
container.style.border = '1px solid #d1d3e2';
|
||||
container.style.borderRadius = '6px';
|
||||
container.style.padding = '12px';
|
||||
container.style.background = '#ffffff';
|
||||
container.style.marginBottom = '10px';
|
||||
|
||||
const headerLabel = document.createElement('label');
|
||||
headerLabel.className = 'font-weight-bold text-primary small mb-1';
|
||||
headerLabel.innerHTML = '<i class="fas fa-chevron-circle-down"></i> Accordion Title / Summary';
|
||||
|
||||
this.titleInput = document.createElement('input');
|
||||
this.titleInput.type = 'text';
|
||||
this.titleInput.className = 'form-control mb-2';
|
||||
this.titleInput.placeholder = 'Enter accordion header/title...';
|
||||
this.titleInput.value = this.data.title;
|
||||
|
||||
const bodyLabel = document.createElement('label');
|
||||
bodyLabel.className = 'font-weight-bold text-secondary small mb-1';
|
||||
bodyLabel.innerHTML = '<i class="fas fa-align-left"></i> Accordion Content / Details';
|
||||
|
||||
this.contentInput = document.createElement('textarea');
|
||||
this.contentInput.className = 'form-control';
|
||||
this.contentInput.style.minHeight = '100px';
|
||||
this.contentInput.placeholder = 'Enter body content (HTML or text)...';
|
||||
this.contentInput.value = this.data.content;
|
||||
|
||||
if (this.readOnly) {
|
||||
this.titleInput.disabled = true;
|
||||
this.contentInput.disabled = true;
|
||||
}
|
||||
|
||||
this.titleInput.addEventListener('input', () => {
|
||||
this.data.title = this.titleInput.value;
|
||||
});
|
||||
|
||||
this.contentInput.addEventListener('input', () => {
|
||||
this.data.content = this.contentInput.value;
|
||||
});
|
||||
|
||||
container.appendChild(headerLabel);
|
||||
container.appendChild(this.titleInput);
|
||||
container.appendChild(bodyLabel);
|
||||
container.appendChild(this.contentInput);
|
||||
|
||||
// Open by default toggle
|
||||
const openWrapper = document.createElement('div');
|
||||
openWrapper.className = 'custom-control custom-switch mt-2';
|
||||
this.openCheck = document.createElement('input');
|
||||
this.openCheck.type = 'checkbox';
|
||||
this.openCheck.className = 'custom-control-input';
|
||||
this.openCheck.id = 'acc_open_' + Math.random().toString(36).substring(7);
|
||||
this.openCheck.checked = !!(this.data && this.data.isOpen);
|
||||
|
||||
const openLabel = document.createElement('label');
|
||||
openLabel.className = 'custom-control-label small font-weight-bold text-primary';
|
||||
openLabel.htmlFor = this.openCheck.id;
|
||||
openLabel.innerHTML = '<i class="fas fa-folder-open"></i> Expand / Uncollapse by default after loading page';
|
||||
|
||||
this.openCheck.addEventListener('change', () => {
|
||||
this.data.isOpen = this.openCheck.checked;
|
||||
});
|
||||
|
||||
openWrapper.appendChild(this.openCheck);
|
||||
openWrapper.appendChild(openLabel);
|
||||
container.appendChild(openWrapper);
|
||||
|
||||
// Stretch toggle
|
||||
const stretchWrapper = document.createElement('div');
|
||||
stretchWrapper.className = 'custom-control custom-switch mt-1';
|
||||
this.stretchCheck = document.createElement('input');
|
||||
this.stretchCheck.type = 'checkbox';
|
||||
this.stretchCheck.className = 'custom-control-input';
|
||||
this.stretchCheck.id = 'acc_stretch_' + Math.random().toString(36).substring(7);
|
||||
this.stretchCheck.checked = !!(this.data && this.data.stretched);
|
||||
|
||||
const stretchLabel = document.createElement('label');
|
||||
stretchLabel.className = 'custom-control-label small font-weight-bold text-secondary';
|
||||
stretchLabel.htmlFor = this.stretchCheck.id;
|
||||
stretchLabel.innerHTML = '<i class="fas fa-arrows-alt-h"></i> Stretch block to Full Screen Width (Independent Breakout)';
|
||||
|
||||
this.stretchCheck.addEventListener('change', () => {
|
||||
this.data.stretched = this.stretchCheck.checked;
|
||||
});
|
||||
|
||||
stretchWrapper.appendChild(this.stretchCheck);
|
||||
stretchWrapper.appendChild(stretchLabel);
|
||||
container.appendChild(stretchWrapper);
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
save() {
|
||||
return {
|
||||
title: this.titleInput ? this.titleInput.value : this.data.title,
|
||||
content: this.contentInput ? this.contentInput.value : this.data.content,
|
||||
isOpen: this.openCheck ? this.openCheck.checked : !!(this.data && this.data.isOpen),
|
||||
stretched: this.stretchCheck ? this.stretchCheck.checked : !!(this.data && this.data.stretched)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class SISYouTubeTool {
|
||||
static get toolbox() {
|
||||
return {
|
||||
title: 'YouTube Video',
|
||||
icon: '<svg width="20" height="20" viewBox="0 0 24 24" fill="red"><path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"/></svg>'
|
||||
};
|
||||
}
|
||||
|
||||
constructor({ data, api, readOnly }) {
|
||||
this.api = api;
|
||||
this.readOnly = readOnly;
|
||||
this.data = {
|
||||
url: (data && data.url) ? data.url : '',
|
||||
thumbnailUrl: (data && data.thumbnailUrl) ? data.thumbnailUrl : '',
|
||||
caption: (data && data.caption) ? data.caption : '',
|
||||
stretched: !!(data && data.stretched)
|
||||
};
|
||||
this.urlInput = null;
|
||||
this.thumbnailInput = null;
|
||||
this.captionInput = null;
|
||||
this.previewContainer = null;
|
||||
this.iframe = null;
|
||||
this.isIframeActive = false;
|
||||
}
|
||||
|
||||
extractVideoId(input) {
|
||||
if (!input) return '';
|
||||
var regExp = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/;
|
||||
var match = input.match(regExp);
|
||||
if (match && match[2].length === 11) {
|
||||
return match[2];
|
||||
}
|
||||
if (input.length === 11) {
|
||||
return input;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
getEmbedUrl(videoId) {
|
||||
return videoId ? 'https://www.youtube.com/embed/' + videoId : '';
|
||||
}
|
||||
|
||||
getThumbnailUrl(videoId) {
|
||||
return videoId ? 'https://img.youtube.com/vi/' + videoId + '/hqdefault.jpg' : '';
|
||||
}
|
||||
|
||||
render() {
|
||||
const container = document.createElement('div');
|
||||
container.style.border = '1px solid #e3e6f0';
|
||||
container.style.borderRadius = '8px';
|
||||
container.style.padding = '14px';
|
||||
container.style.background = '#fff';
|
||||
container.style.marginBottom = '12px';
|
||||
|
||||
const label = document.createElement('label');
|
||||
label.className = 'font-weight-bold text-danger small mb-1';
|
||||
label.innerHTML = '<i class="fab fa-youtube"></i> YouTube Video Link / URL / ID';
|
||||
|
||||
this.urlInput = document.createElement('input');
|
||||
this.urlInput.type = 'text';
|
||||
this.urlInput.className = 'form-control mb-2';
|
||||
this.urlInput.placeholder = 'Paste YouTube URL (e.g. https://www.youtube.com/watch?v=sDE4ZZdNOOY)...';
|
||||
this.urlInput.value = this.data.url;
|
||||
|
||||
// Custom Thumbnail Field
|
||||
const thumbLabel = document.createElement('label');
|
||||
thumbLabel.className = 'font-weight-bold text-secondary small mb-1';
|
||||
thumbLabel.innerHTML = '<i class="fas fa-image"></i> Custom Thumbnail Image URL (Optional - Overrides YouTube Cover)';
|
||||
|
||||
this.thumbnailInput = document.createElement('input');
|
||||
this.thumbnailInput.type = 'text';
|
||||
this.thumbnailInput.className = 'form-control form-control-sm mb-2';
|
||||
this.thumbnailInput.placeholder = 'Custom Thumbnail Image URL (leave blank to auto-use YouTube cover)...';
|
||||
this.thumbnailInput.value = this.data.thumbnailUrl;
|
||||
|
||||
// Thumbnail Preview Container
|
||||
this.previewContainer = document.createElement('div');
|
||||
this.previewContainer.className = 'yt-thumbnail-preview';
|
||||
this.previewContainer.style.position = 'relative';
|
||||
this.previewContainer.style.paddingBottom = '56.25%';
|
||||
this.previewContainer.style.height = '0';
|
||||
this.previewContainer.style.overflow = 'hidden';
|
||||
this.previewContainer.style.background = '#111 center/cover no-repeat';
|
||||
this.previewContainer.style.borderRadius = '6px';
|
||||
this.previewContainer.style.marginBottom = '8px';
|
||||
this.previewContainer.style.cursor = 'pointer';
|
||||
this.previewContainer.style.boxShadow = '0 2px 8px rgba(0,0,0,0.15)';
|
||||
|
||||
// Play Button Overlay
|
||||
const playBtn = document.createElement('div');
|
||||
playBtn.className = 'yt-play-button-overlay';
|
||||
playBtn.style.position = 'absolute';
|
||||
playBtn.style.top = '50%';
|
||||
playBtn.style.left = '50%';
|
||||
playBtn.style.transform = 'translate(-50%, -50%)';
|
||||
playBtn.style.transition = 'transform 0.2s ease';
|
||||
playBtn.innerHTML = '<svg width="68" height="48" viewBox="0 0 68 48"><path d="M66.52 7.74c-.78-2.93-2.49-5.41-5.42-6.19C55.79.13 34 0 34 0S12.21.13 6.9 1.55c-2.93.78-4.63 3.26-5.42 6.19C.06 13.05 0 24 0 24s.06 10.95 1.48 16.26c.78 2.93 2.49 5.41 5.42 6.19C12.21 47.87 34 48 34 48s21.79-.13 27.1-1.55c2.93-.78 4.64-3.26 5.42-6.19C67.94 34.95 68 24 68 24s-.06-10.95-1.48-16.26z" fill="#ff0000"/><path d="M45 24L27 14v20z" fill="#ffffff"/></svg>';
|
||||
|
||||
this.previewContainer.appendChild(playBtn);
|
||||
|
||||
// Update Thumbnail Image
|
||||
const updatePreview = () => {
|
||||
const customThumb = this.thumbnailInput.value.trim();
|
||||
const videoId = this.extractVideoId(this.urlInput.value);
|
||||
const thumbUrl = customThumb || this.getThumbnailUrl(videoId);
|
||||
if (thumbUrl) {
|
||||
this.previewContainer.style.backgroundImage = 'url("' + thumbUrl + '")';
|
||||
playBtn.style.display = 'block';
|
||||
} else {
|
||||
this.previewContainer.style.backgroundImage = 'none';
|
||||
this.previewContainer.style.backgroundColor = '#222';
|
||||
}
|
||||
};
|
||||
|
||||
// Click thumbnail to play live video
|
||||
this.previewContainer.addEventListener('click', () => {
|
||||
const videoId = this.extractVideoId(this.urlInput.value);
|
||||
if (videoId && !this.isIframeActive) {
|
||||
this.isIframeActive = true;
|
||||
this.previewContainer.innerHTML = '';
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.style.position = 'absolute';
|
||||
iframe.style.top = '0';
|
||||
iframe.style.left = '0';
|
||||
iframe.style.width = '100%';
|
||||
iframe.style.height = '100%';
|
||||
iframe.style.border = '0';
|
||||
iframe.setAttribute('allow', 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share');
|
||||
iframe.setAttribute('allowfullscreen', 'true');
|
||||
iframe.src = this.getEmbedUrl(videoId) + '?autoplay=1';
|
||||
this.previewContainer.appendChild(iframe);
|
||||
}
|
||||
});
|
||||
|
||||
updatePreview();
|
||||
|
||||
this.captionInput = document.createElement('input');
|
||||
this.captionInput.type = 'text';
|
||||
this.captionInput.className = 'form-control form-control-sm mb-2';
|
||||
this.captionInput.placeholder = 'Video caption (optional)...';
|
||||
this.captionInput.value = this.data.caption;
|
||||
|
||||
if (this.readOnly) {
|
||||
this.urlInput.disabled = true;
|
||||
this.thumbnailInput.disabled = true;
|
||||
this.captionInput.disabled = true;
|
||||
}
|
||||
|
||||
this.urlInput.addEventListener('input', () => {
|
||||
const videoId = this.extractVideoId(this.urlInput.value);
|
||||
this.data.url = this.getEmbedUrl(videoId);
|
||||
this.isIframeActive = false;
|
||||
this.previewContainer.innerHTML = '';
|
||||
this.previewContainer.appendChild(playBtn);
|
||||
updatePreview();
|
||||
});
|
||||
|
||||
this.thumbnailInput.addEventListener('input', () => {
|
||||
this.data.thumbnailUrl = this.thumbnailInput.value.trim();
|
||||
updatePreview();
|
||||
});
|
||||
|
||||
this.captionInput.addEventListener('input', () => {
|
||||
this.data.caption = this.captionInput.value;
|
||||
});
|
||||
|
||||
container.appendChild(label);
|
||||
container.appendChild(this.urlInput);
|
||||
container.appendChild(thumbLabel);
|
||||
container.appendChild(this.thumbnailInput);
|
||||
container.appendChild(this.previewContainer);
|
||||
container.appendChild(this.captionInput);
|
||||
|
||||
const stretchWrapper = document.createElement('div');
|
||||
stretchWrapper.className = 'custom-control custom-switch mt-2';
|
||||
this.stretchCheck = document.createElement('input');
|
||||
this.stretchCheck.type = 'checkbox';
|
||||
this.stretchCheck.className = 'custom-control-input';
|
||||
this.stretchCheck.id = 'yt_stretch_' + Math.random().toString(36).substring(7);
|
||||
this.stretchCheck.checked = !!(this.data && this.data.stretched);
|
||||
|
||||
const stretchLabel = document.createElement('label');
|
||||
stretchLabel.className = 'custom-control-label small font-weight-bold text-secondary';
|
||||
stretchLabel.htmlFor = this.stretchCheck.id;
|
||||
stretchLabel.innerHTML = '<i class="fas fa-arrows-alt-h"></i> Stretch block to Full Screen Width (Independent Breakout)';
|
||||
|
||||
this.stretchCheck.addEventListener('change', () => {
|
||||
this.data.stretched = this.stretchCheck.checked;
|
||||
});
|
||||
|
||||
stretchWrapper.appendChild(this.stretchCheck);
|
||||
stretchWrapper.appendChild(stretchLabel);
|
||||
container.appendChild(stretchWrapper);
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
save() {
|
||||
const videoId = this.extractVideoId(this.urlInput ? this.urlInput.value : this.data.url);
|
||||
return {
|
||||
url: this.getEmbedUrl(videoId) || this.data.url,
|
||||
thumbnailUrl: this.thumbnailInput ? this.thumbnailInput.value.trim() : (this.data.thumbnailUrl || ''),
|
||||
caption: this.captionInput ? this.captionInput.value : this.data.caption,
|
||||
stretched: this.stretchCheck ? this.stretchCheck.checked : !!(this.data && this.data.stretched)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// SISHeroBannerTool extracted to /js/manage/editor-plugins/hero-banner.js
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Initialize the SIS Block Editor on a given holder element.
|
||||
*
|
||||
* @param {string} holderId - The DOM element ID for the editor container
|
||||
* @param {string} hiddenInputId - The DOM element ID for the hidden input storing JSON
|
||||
* @param {object|null} initialData - Pre-existing Editor.js JSON data to load
|
||||
* @param {boolean} [skipSubmitHandler=false] - If true, prevents automatic form submission handling
|
||||
* @returns {EditorJS} The editor instance
|
||||
*/
|
||||
function initSISEditor(holderId, hiddenInputId, initialData, skipSubmitHandler = false) {
|
||||
if (typeof EditorJS === 'undefined') {
|
||||
console.error('[SIS Editor] Editor.js library is not loaded!');
|
||||
return null;
|
||||
}
|
||||
|
||||
// === Register Built-in Tools (Defensive Check for Globals) ===
|
||||
var builtInTools = {};
|
||||
|
||||
if (typeof Header !== 'undefined') {
|
||||
builtInTools.header = {
|
||||
class: Header,
|
||||
inlineToolbar: ['link'],
|
||||
tunes: ['textStyling'],
|
||||
config: {
|
||||
placeholder: 'Header text...',
|
||||
levels: [1, 2, 3, 4, 5, 6],
|
||||
defaultLevel: 2
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
builtInTools.paragraph = {
|
||||
tunes: ['textStyling']
|
||||
};
|
||||
|
||||
if (typeof NestedList !== 'undefined') {
|
||||
builtInTools.list = {
|
||||
class: NestedList,
|
||||
inlineToolbar: true,
|
||||
tunes: ['textStyling'],
|
||||
config: { defaultStyle: 'unordered' }
|
||||
};
|
||||
} else if (typeof List !== 'undefined') {
|
||||
builtInTools.list = {
|
||||
class: List,
|
||||
inlineToolbar: true,
|
||||
tunes: ['textStyling'],
|
||||
config: { defaultStyle: 'unordered' }
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof Quote !== 'undefined') {
|
||||
builtInTools.quote = {
|
||||
class: Quote,
|
||||
inlineToolbar: true,
|
||||
tunes: ['textStyling'],
|
||||
config: {
|
||||
quotePlaceholder: 'Enter a quote',
|
||||
captionPlaceholder: 'Quote\'s author'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof Delimiter !== 'undefined') builtInTools.delimiter = { class: Delimiter };
|
||||
if (typeof Table !== 'undefined') {
|
||||
builtInTools.table = {
|
||||
class: Table,
|
||||
inlineToolbar: true,
|
||||
tunes: ['textStyling'],
|
||||
config: { rows: 2, cols: 3 }
|
||||
};
|
||||
}
|
||||
if (typeof CodeTool !== 'undefined') builtInTools.code = { class: CodeTool };
|
||||
if (typeof Warning !== 'undefined') builtInTools.warning = { class: Warning, inlineToolbar: true, config: { titlePlaceholder: 'Title', messagePlaceholder: 'Message' } };
|
||||
if (typeof Marker !== 'undefined') builtInTools.marker = { class: Marker };
|
||||
if (typeof InlineCode !== 'undefined') builtInTools.inlineCode = { class: InlineCode };
|
||||
if (typeof Underline !== 'undefined') builtInTools.underline = { class: Underline };
|
||||
if (typeof ImageTool !== 'undefined') {
|
||||
builtInTools.image = {
|
||||
class: ImageTool,
|
||||
config: {
|
||||
endpoints: {
|
||||
byFile: '/api/manage/media/upload',
|
||||
byUrl: '/api/manage/media/fetchUrl'
|
||||
},
|
||||
uploader: {
|
||||
uploadByUrl(url) {
|
||||
return Promise.resolve({
|
||||
success: 1,
|
||||
file: { url: url }
|
||||
});
|
||||
},
|
||||
selectFile() {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (window.SISMediaPicker) {
|
||||
SISMediaPicker.open((url, mediaObj, config) => {
|
||||
if (url) {
|
||||
resolve({
|
||||
success: 1,
|
||||
file: {
|
||||
url: url,
|
||||
style: config ? config.style : '',
|
||||
cssClass: config ? config.cssClass : '',
|
||||
alt: config ? config.alt : '',
|
||||
customAttributes: config ? config.customAttributes : '',
|
||||
aspectRatio: config ? config.aspectRatio : ''
|
||||
}
|
||||
});
|
||||
} else {
|
||||
reject('No image selected');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
reject('SISMediaPicker not loaded');
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
field: 'file',
|
||||
types: 'image/*'
|
||||
}
|
||||
};
|
||||
window.SISEditorPlugins.image = builtInTools.image;
|
||||
}
|
||||
if (typeof AttachesTool !== 'undefined') {
|
||||
builtInTools.attaches = {
|
||||
class: AttachesTool,
|
||||
config: { endpoint: '/api/manage/media/upload', field: 'file' }
|
||||
};
|
||||
}
|
||||
|
||||
// Custom SIS Tools (Always present)
|
||||
builtInTools.raw = { class: SISRawHtmlTool };
|
||||
builtInTools.accordion = { class: SISAccordionTool };
|
||||
builtInTools.youtube = { class: SISYouTubeTool };
|
||||
if (typeof SISHeroBannerTool !== 'undefined') {
|
||||
builtInTools.hero = { class: SISHeroBannerTool };
|
||||
}
|
||||
if (typeof SISStickyNavTool !== 'undefined') {
|
||||
builtInTools.stickyNav = { class: SISStickyNavTool };
|
||||
}
|
||||
if (typeof TextStylingTune !== 'undefined') {
|
||||
builtInTools.textStyling = { class: TextStylingTune };
|
||||
}
|
||||
|
||||
// === Merge built-in tools with any registered plugins ===
|
||||
var allTools = Object.assign({}, builtInTools, window.SISEditorPlugins);
|
||||
|
||||
// Apply the textStyling tune to all block tools dynamically
|
||||
for (var toolName in allTools) {
|
||||
if (toolName !== 'textStyling') {
|
||||
if (!allTools[toolName].tunes) {
|
||||
allTools[toolName].tunes = ['textStyling'];
|
||||
} else if (!allTools[toolName].tunes.includes('textStyling')) {
|
||||
allTools[toolName].tunes.push('textStyling');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === Parse initial data ===
|
||||
var editorData = null;
|
||||
if (initialData && typeof initialData === 'string') {
|
||||
try {
|
||||
editorData = JSON.parse(initialData);
|
||||
} catch (e) {
|
||||
console.warn('[SIS Editor] Could not parse initial data as JSON, starting empty.', e);
|
||||
editorData = null;
|
||||
}
|
||||
} else if (initialData && typeof initialData === 'object') {
|
||||
editorData = initialData;
|
||||
}
|
||||
|
||||
// === Create the Editor ===
|
||||
var editor = new EditorJS({
|
||||
holder: holderId,
|
||||
tools: allTools,
|
||||
data: editorData || undefined,
|
||||
placeholder: 'Click here to start writing your page content...',
|
||||
autofocus: false,
|
||||
onReady: function() {
|
||||
console.log('[SIS Editor] Ready. Tools loaded:', Object.keys(allTools));
|
||||
},
|
||||
onChange: function(api, event) {
|
||||
// Auto-save to hidden input on every change
|
||||
api.saver.save().then(function(outputData) {
|
||||
var hiddenInput = document.getElementById(hiddenInputId);
|
||||
if (hiddenInput) {
|
||||
hiddenInput.value = JSON.stringify(outputData);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// === Form submission handler ===
|
||||
// Ensure the latest content is saved before form submit
|
||||
var hiddenInput = document.getElementById(hiddenInputId);
|
||||
var form = hiddenInput ? hiddenInput.closest('form') : document.querySelector('form');
|
||||
if (form && !skipSubmitHandler) {
|
||||
var submitHandler = function(event) {
|
||||
event.preventDefault();
|
||||
editor.save().then(function(outputData) {
|
||||
var hiddenInput = document.getElementById(hiddenInputId);
|
||||
if (hiddenInput) {
|
||||
hiddenInput.value = JSON.stringify(outputData);
|
||||
}
|
||||
// Now submit the form
|
||||
form.removeEventListener('submit', submitHandler);
|
||||
form.submit();
|
||||
}).catch(function(error) {
|
||||
console.error('[SIS Editor] Save failed:', error);
|
||||
});
|
||||
};
|
||||
form.addEventListener('submit', submitHandler);
|
||||
}
|
||||
|
||||
return editor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup IDE Code Editor styling and Tab indentation for custom CSS and JS textareas
|
||||
*/
|
||||
function setupCodeTextareas() {
|
||||
var codeSelectors = '#customCss, #customJs, #postCustomCss, #postCustomJs, textarea[name="customCss"], textarea[name="customJs"]';
|
||||
document.querySelectorAll(codeSelectors).forEach(function(textarea) {
|
||||
if (!textarea || textarea.dataset.codeEditorInit) return;
|
||||
textarea.dataset.codeEditorInit = 'true';
|
||||
|
||||
// Enable Tab key indentation (inserts 2 spaces)
|
||||
textarea.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Tab') {
|
||||
e.preventDefault();
|
||||
var start = this.selectionStart;
|
||||
var end = this.selectionEnd;
|
||||
this.value = this.value.substring(0, start) + ' ' + this.value.substring(end);
|
||||
this.selectionStart = this.selectionEnd = start + 2;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Auto-run code textareas setup on page load
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', setupCodeTextareas);
|
||||
} else {
|
||||
setupCodeTextareas();
|
||||
}
|
||||
+51
File diff suppressed because one or more lines are too long
BIN
Binary file not shown.
+903
@@ -0,0 +1,903 @@
|
||||
/**
|
||||
* Flex block tool for Editor.js.
|
||||
* Allows users to create highly custom Flexbox layouts with alignment, direction, gaps, and custom column items.
|
||||
* Supports instantiating other registered plugins inside columns dynamically.
|
||||
*/
|
||||
class FlexTool {
|
||||
static get toolbox() {
|
||||
return {
|
||||
title: 'Flex Layout',
|
||||
icon: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"></rect><line x1="9" y1="3" x2="9" y2="21"></line><line x1="15" y1="3" x2="15" y2="21"></line></svg>'
|
||||
};
|
||||
}
|
||||
|
||||
constructor({ data, api, readOnly }) {
|
||||
this.api = api;
|
||||
this.readOnly = readOnly;
|
||||
this.data = {
|
||||
cols: parseInt(data.cols) || 2,
|
||||
direction: data.direction || 'row',
|
||||
justify: data.justify || 'start',
|
||||
align: data.align || 'stretch',
|
||||
gap: data.gap || '3', // standard Bootstrap gap level
|
||||
globalClass: data.globalClass || '',
|
||||
globalId: data.globalId || '',
|
||||
globalAttributes: data.globalAttributes || data.attributes || ''
|
||||
};
|
||||
this.activeInstances = {};
|
||||
|
||||
// Load items content dynamically
|
||||
for (let i = 1; i <= 12; i++) {
|
||||
this.data[`type${i}`] = data[`type${i}`] || 'html';
|
||||
this.data[`col${i}`] = data[`col${i}`] || '';
|
||||
this.data[`imgUrl${i}`] = data[`imgUrl${i}`] || '';
|
||||
this.data[`ytUrl${i}`] = data[`ytUrl${i}`] || '';
|
||||
this.data[`accTitle${i}`] = data[`accTitle${i}`] || '';
|
||||
this.data[`accContent${i}`] = data[`accContent${i}`] || '';
|
||||
this.data[`class${i}`] = data[`class${i}`] || '';
|
||||
this.data[`id${i}`] = data[`id${i}`] || '';
|
||||
this.data[`attr${i}`] = data[`attr${i}`] || data[`attributes${i}`] || '';
|
||||
this.data[`width${i}`] = parseInt(data[`width${i}`]) || 0; // 0 means auto
|
||||
this.data[`caption${i}`] = data[`caption${i}`] || '';
|
||||
}
|
||||
this.wrapper = undefined;
|
||||
}
|
||||
|
||||
render() {
|
||||
this.wrapper = document.createElement('div');
|
||||
this.wrapper.className = 'p-3 bg-light border rounded mb-3 ce-flex-tool-wrapper';
|
||||
this.wrapper.style.fontFamily = 'inherit';
|
||||
|
||||
const title = document.createElement('div');
|
||||
title.className = 'font-weight-bold text-primary small mb-3';
|
||||
title.innerHTML = '<i class="fas fa-boxes"></i> Custom Flexbox Layout Builder';
|
||||
this.wrapper.appendChild(title);
|
||||
|
||||
// Container Flex Settings Row
|
||||
const settingsRow = document.createElement('div');
|
||||
settingsRow.className = 'form-row mb-3 pb-3 border-bottom';
|
||||
|
||||
// 1. Direction
|
||||
const dirCol = document.createElement('div');
|
||||
dirCol.className = 'col-md-3 col-sm-6 mb-2';
|
||||
dirCol.innerHTML = '<label class="small font-weight-bold text-secondary mb-1">Direction</label>';
|
||||
const dirSelect = document.createElement('select');
|
||||
dirSelect.className = 'custom-select custom-select-sm';
|
||||
const dirOpts = [
|
||||
{ val: 'row', lbl: 'Row' },
|
||||
{ val: 'row-reverse', lbl: 'Row Reverse' },
|
||||
{ val: 'column', lbl: 'Column' },
|
||||
{ val: 'column-reverse', lbl: 'Column Reverse' }
|
||||
];
|
||||
dirOpts.forEach(o => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = o.val;
|
||||
opt.innerText = o.lbl;
|
||||
opt.selected = this.data.direction === o.val;
|
||||
dirSelect.appendChild(opt);
|
||||
});
|
||||
dirSelect.addEventListener('change', (e) => this.data.direction = e.target.value);
|
||||
dirCol.appendChild(dirSelect);
|
||||
settingsRow.appendChild(dirCol);
|
||||
|
||||
// 2. Justify Content
|
||||
const justifyCol = document.createElement('div');
|
||||
justifyCol.className = 'col-md-3 col-sm-6 mb-2';
|
||||
justifyCol.innerHTML = '<label class="small font-weight-bold text-secondary mb-1">Justify Content</label>';
|
||||
const justifySelect = document.createElement('select');
|
||||
justifySelect.className = 'custom-select custom-select-sm';
|
||||
const justifyOpts = [
|
||||
{ val: 'start', lbl: 'Start' },
|
||||
{ val: 'center', lbl: 'Center' },
|
||||
{ val: 'end', lbl: 'End' },
|
||||
{ val: 'between', lbl: 'Space Between' },
|
||||
{ val: 'around', lbl: 'Space Around' },
|
||||
{ val: 'evenly', lbl: 'Space Evenly' }
|
||||
];
|
||||
justifyOpts.forEach(o => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = o.val;
|
||||
opt.innerText = o.lbl;
|
||||
opt.selected = this.data.justify === o.val;
|
||||
justifySelect.appendChild(opt);
|
||||
});
|
||||
justifySelect.addEventListener('change', (e) => this.data.justify = e.target.value);
|
||||
justifyCol.appendChild(justifySelect);
|
||||
settingsRow.appendChild(justifyCol);
|
||||
|
||||
// 3. Align Items
|
||||
const alignCol = document.createElement('div');
|
||||
alignCol.className = 'col-md-3 col-sm-6 mb-2';
|
||||
alignCol.innerHTML = '<label class="small font-weight-bold text-secondary mb-1">Align Items</label>';
|
||||
const alignSelect = document.createElement('select');
|
||||
alignSelect.className = 'custom-select custom-select-sm';
|
||||
const alignOpts = [
|
||||
{ val: 'stretch', lbl: 'Stretch' },
|
||||
{ val: 'start', lbl: 'Start' },
|
||||
{ val: 'center', lbl: 'Center' },
|
||||
{ val: 'end', lbl: 'End' }
|
||||
];
|
||||
alignOpts.forEach(o => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = o.val;
|
||||
opt.innerText = o.lbl;
|
||||
opt.selected = this.data.align === o.val;
|
||||
alignSelect.appendChild(opt);
|
||||
});
|
||||
alignSelect.addEventListener('change', (e) => this.data.align = e.target.value);
|
||||
alignCol.appendChild(alignSelect);
|
||||
settingsRow.appendChild(alignCol);
|
||||
|
||||
// 4. Gap Level
|
||||
const gapCol = document.createElement('div');
|
||||
gapCol.className = 'col-md-3 col-sm-6 mb-2';
|
||||
gapCol.innerHTML = '<label class="small font-weight-bold text-secondary mb-1">Gap Level</label>';
|
||||
const gapSelect = document.createElement('select');
|
||||
gapSelect.className = 'custom-select custom-select-sm';
|
||||
const gapOpts = [
|
||||
{ val: '0', lbl: 'None' },
|
||||
{ val: '1', lbl: 'Extra Small' },
|
||||
{ val: '2', lbl: 'Small' },
|
||||
{ val: '3', lbl: 'Medium' },
|
||||
{ val: '4', lbl: 'Large' },
|
||||
{ val: '5', lbl: 'Extra Large' }
|
||||
];
|
||||
gapOpts.forEach(o => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = o.val;
|
||||
opt.innerText = o.lbl;
|
||||
opt.selected = this.data.gap === o.val;
|
||||
gapSelect.appendChild(opt);
|
||||
});
|
||||
gapSelect.addEventListener('change', (e) => this.data.gap = e.target.value);
|
||||
gapCol.appendChild(gapSelect);
|
||||
settingsRow.appendChild(gapCol);
|
||||
|
||||
this.wrapper.appendChild(settingsRow);
|
||||
|
||||
// Background Settings Row
|
||||
const bgSettingsRow = document.createElement('div');
|
||||
bgSettingsRow.className = 'form-row mb-3 pb-2 border-bottom';
|
||||
|
||||
// 1. Flex Items (Cols)
|
||||
const colDiv = document.createElement('div');
|
||||
colDiv.className = 'col-md-2 mb-2';
|
||||
colDiv.innerHTML = '<label class="small font-weight-bold text-secondary mb-1">Flex Items</label>';
|
||||
const input = document.createElement('input');
|
||||
input.type = 'number';
|
||||
input.className = 'form-control form-control-sm';
|
||||
input.min = '1';
|
||||
input.max = '12';
|
||||
input.value = this.data.cols;
|
||||
if (this.readOnly) input.disabled = true;
|
||||
input.addEventListener('input', (e) => {
|
||||
let val = parseInt(e.target.value) || 2;
|
||||
if (val < 1) val = 1;
|
||||
if (val > 12) val = 12;
|
||||
this.data.cols = val;
|
||||
this._renderColumnInputs();
|
||||
});
|
||||
colDiv.appendChild(input);
|
||||
bgSettingsRow.appendChild(colDiv);
|
||||
|
||||
// 2. Global CSS Class
|
||||
const classDiv = document.createElement('div');
|
||||
classDiv.className = 'col-md-3 mb-2';
|
||||
classDiv.innerHTML = '<label class="small font-weight-bold text-secondary mb-1">Flex CSS Class(es)</label>';
|
||||
const classInput = document.createElement('input');
|
||||
classInput.type = 'text';
|
||||
classInput.className = 'form-control form-control-sm';
|
||||
classInput.placeholder = 'e.g. my-flex-row';
|
||||
classInput.value = this.data.globalClass || '';
|
||||
if (this.readOnly) classInput.disabled = true;
|
||||
classInput.addEventListener('input', (e) => this.data.globalClass = e.target.value.trim());
|
||||
this.globalClassInput = classInput;
|
||||
classDiv.appendChild(classInput);
|
||||
bgSettingsRow.appendChild(classDiv);
|
||||
|
||||
// 3. Global HTML ID
|
||||
const idDiv = document.createElement('div');
|
||||
idDiv.className = 'col-md-3 mb-2';
|
||||
idDiv.innerHTML = '<label class="small font-weight-bold text-secondary mb-1">Flex HTML ID</label>';
|
||||
const idInput = document.createElement('input');
|
||||
idInput.type = 'text';
|
||||
idInput.className = 'form-control form-control-sm';
|
||||
idInput.placeholder = 'e.g. flex-section-1';
|
||||
idInput.value = this.data.globalId || '';
|
||||
if (this.readOnly) idInput.disabled = true;
|
||||
idInput.addEventListener('input', (e) => this.data.globalId = e.target.value.trim());
|
||||
this.globalIdInput = idInput;
|
||||
idDiv.appendChild(idInput);
|
||||
bgSettingsRow.appendChild(idDiv);
|
||||
|
||||
// 4. Global Custom Attributes
|
||||
const attrDiv = document.createElement('div');
|
||||
attrDiv.className = 'col-md-4 mb-2';
|
||||
attrDiv.innerHTML = '<label class="small font-weight-bold text-secondary mb-1"><i class="fas fa-sliders-h"></i> Custom Attributes</label>';
|
||||
const attrInput = document.createElement('input');
|
||||
attrInput.type = 'text';
|
||||
attrInput.className = 'form-control form-control-sm';
|
||||
attrInput.placeholder = 'e.g. data-aos="fade-up" style="background:#fff"';
|
||||
attrInput.value = this.data.globalAttributes || '';
|
||||
if (this.readOnly) attrInput.disabled = true;
|
||||
attrInput.addEventListener('input', (e) => this.data.globalAttributes = e.target.value.trim());
|
||||
this.globalAttrInput = attrInput;
|
||||
attrDiv.appendChild(attrInput);
|
||||
bgSettingsRow.appendChild(attrDiv);
|
||||
|
||||
this.wrapper.appendChild(bgSettingsRow);
|
||||
|
||||
// Columns inputs container
|
||||
this.inputsContainer = document.createElement('div');
|
||||
this.wrapper.appendChild(this.inputsContainer);
|
||||
|
||||
this._renderColumnInputs();
|
||||
|
||||
return this.wrapper;
|
||||
}
|
||||
|
||||
_renderColumnInputs() {
|
||||
this.inputsContainer.innerHTML = '';
|
||||
|
||||
const row = document.createElement('div');
|
||||
row.className = 'row';
|
||||
|
||||
const count = this.data.cols;
|
||||
const colWidthClass = count === 1 ? 'col-12' : (count === 2 ? 'col-md-6' : (count === 3 ? 'col-md-4' : (count === 4 ? 'col-md-3' : 'col-md')));
|
||||
|
||||
for (let i = 1; i <= count; i++) {
|
||||
const colDiv = document.createElement('div');
|
||||
colDiv.className = `${colWidthClass} mb-3 p-2 border rounded bg-white`;
|
||||
colDiv.style.boxShadow = '0 1px 3px rgba(0,0,0,0.05)';
|
||||
|
||||
const headerRow = document.createElement('div');
|
||||
headerRow.className = 'd-flex justify-content-between align-items-center mb-2 pb-1 border-bottom';
|
||||
|
||||
const label = document.createElement('label');
|
||||
label.className = 'small font-weight-bold text-dark mb-0';
|
||||
label.innerText = `Item ${i}`;
|
||||
headerRow.appendChild(label);
|
||||
|
||||
const controlsWrapper = document.createElement('div');
|
||||
controlsWrapper.className = 'd-flex align-items-center gap-1';
|
||||
|
||||
// Width Selector
|
||||
const widthSelect = document.createElement('select');
|
||||
widthSelect.className = 'custom-select custom-select-sm mr-1';
|
||||
widthSelect.style.width = '75px';
|
||||
if (this.readOnly) widthSelect.disabled = true;
|
||||
|
||||
const widthOpts = [
|
||||
{ val: 0, lbl: 'Auto' },
|
||||
{ val: 1, lbl: '1/12' },
|
||||
{ val: 2, lbl: '2/12' },
|
||||
{ val: 3, lbl: '3/12' },
|
||||
{ val: 4, lbl: '4/12' },
|
||||
{ val: 5, lbl: '5/12' },
|
||||
{ val: 6, lbl: '6/12' },
|
||||
{ val: 7, lbl: '7/12' },
|
||||
{ val: 8, lbl: '8/12' },
|
||||
{ val: 9, lbl: '9/12' },
|
||||
{ val: 10, lbl: '10/12' },
|
||||
{ val: 11, lbl: '11/12' },
|
||||
{ val: 12, lbl: '12/12' }
|
||||
];
|
||||
|
||||
widthOpts.forEach(o => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = o.val;
|
||||
opt.innerText = o.lbl;
|
||||
opt.selected = this.data[`width${i}`] === o.val;
|
||||
widthSelect.appendChild(opt);
|
||||
});
|
||||
|
||||
widthSelect.addEventListener('change', (e) => {
|
||||
this.data[`width${i}`] = parseInt(e.target.value) || 0;
|
||||
});
|
||||
|
||||
controlsWrapper.appendChild(widthSelect);
|
||||
|
||||
// Column Type Select (Built-ins + Registered plugins dynamically)
|
||||
const typeSelect = document.createElement('select');
|
||||
typeSelect.className = 'custom-select custom-select-sm';
|
||||
typeSelect.style.width = '100px';
|
||||
if (this.readOnly) typeSelect.disabled = true;
|
||||
|
||||
const types = [
|
||||
{ value: 'html', label: 'HTML/Text' },
|
||||
{ value: 'image', label: 'Image' },
|
||||
{ value: 'youtube', label: 'YouTube' },
|
||||
{ value: 'accordion', label: 'Accordion' }
|
||||
];
|
||||
|
||||
if (window.SISEditorPlugins) {
|
||||
Object.keys(window.SISEditorPlugins).forEach(key => {
|
||||
// Allow nesting any registered plugins (no exclusions)
|
||||
if (!types.some(t => t.value === key)) {
|
||||
types.push({ value: key, label: `[Plugin] ${key}` });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
types.forEach(t => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = t.value;
|
||||
opt.innerText = t.label;
|
||||
opt.selected = this.data[`type${i}`] === t.value;
|
||||
typeSelect.appendChild(opt);
|
||||
});
|
||||
|
||||
const contentDiv = document.createElement('div');
|
||||
|
||||
typeSelect.addEventListener('change', (e) => {
|
||||
this.data[`type${i}`] = e.target.value;
|
||||
this._renderTypeSpecificInput(i, contentDiv);
|
||||
});
|
||||
|
||||
controlsWrapper.appendChild(typeSelect);
|
||||
headerRow.appendChild(controlsWrapper);
|
||||
colDiv.appendChild(headerRow);
|
||||
colDiv.appendChild(contentDiv);
|
||||
|
||||
// --- Sub-Plugin (Sub-Content) Section ---
|
||||
const subWrapper = document.createElement('div');
|
||||
if (this.data[`subType${i}`] !== 'none') {
|
||||
subWrapper.className = 'mt-3 pt-2 border-top';
|
||||
}
|
||||
|
||||
const subHeader = document.createElement('div');
|
||||
subHeader.className = 'd-flex justify-content-between align-items-center mb-2';
|
||||
|
||||
const subLabel = document.createElement('label');
|
||||
subLabel.className = 'small font-weight-bold text-muted mb-0';
|
||||
subLabel.style.fontSize = '10px';
|
||||
subLabel.innerText = 'SUB-CONTENT / INJECT PLUGIN';
|
||||
subHeader.appendChild(subLabel);
|
||||
|
||||
const subTypeSelect = document.createElement('select');
|
||||
subTypeSelect.className = 'custom-select custom-select-sm';
|
||||
subTypeSelect.style.width = '130px';
|
||||
subTypeSelect.style.fontSize = '10px';
|
||||
if (this.readOnly) subTypeSelect.disabled = true;
|
||||
|
||||
const subTypes = [
|
||||
{ value: 'none', label: 'None' },
|
||||
{ value: 'caption', label: 'Text Caption' },
|
||||
{ value: 'html', label: 'HTML/Text' },
|
||||
{ value: 'image', label: 'Image' },
|
||||
{ value: 'youtube', label: 'YouTube' },
|
||||
{ value: 'accordion', label: 'Accordion' }
|
||||
];
|
||||
if (window.SISEditorPlugins) {
|
||||
Object.keys(window.SISEditorPlugins).forEach(key => {
|
||||
if (!subTypes.some(t => t.value === key)) {
|
||||
subTypes.push({ value: key, label: `[Plugin] ${key}` });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
subTypes.forEach(t => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = t.value;
|
||||
opt.innerText = t.label;
|
||||
opt.selected = this.data[`subType${i}`] === t.value;
|
||||
subTypeSelect.appendChild(opt);
|
||||
});
|
||||
subHeader.appendChild(subTypeSelect);
|
||||
subWrapper.appendChild(subHeader);
|
||||
|
||||
const subContentDiv = document.createElement('div');
|
||||
subWrapper.appendChild(subContentDiv);
|
||||
colDiv.appendChild(subWrapper);
|
||||
|
||||
const renderSubInputs = () => {
|
||||
subContentDiv.innerHTML = '';
|
||||
const subType = this.data[`subType${i}`];
|
||||
if (subType === 'none') {
|
||||
subWrapper.className = '';
|
||||
} else {
|
||||
subWrapper.className = 'mt-3 pt-2 border-top';
|
||||
}
|
||||
|
||||
if (subType === 'none') {
|
||||
// Empty
|
||||
} else if (subType === 'caption') {
|
||||
const captionGroup = document.createElement('div');
|
||||
captionGroup.className = 'form-group mb-2';
|
||||
const captionInput = document.createElement('input');
|
||||
captionInput.type = 'text';
|
||||
captionInput.className = 'form-control form-control-sm';
|
||||
captionInput.style.fontSize = '11px';
|
||||
captionInput.placeholder = 'Optional column caption / text...';
|
||||
captionInput.value = this.data[`caption${i}`] || '';
|
||||
if (this.readOnly) captionInput.disabled = true;
|
||||
captionInput.addEventListener('input', (e) => {
|
||||
this.data[`caption${i}`] = e.target.value;
|
||||
});
|
||||
captionGroup.appendChild(captionInput);
|
||||
subContentDiv.appendChild(captionGroup);
|
||||
} else {
|
||||
this._renderTypeSpecificInput(`sub_${i}`, subContentDiv, subType);
|
||||
}
|
||||
};
|
||||
|
||||
subTypeSelect.addEventListener('change', (e) => {
|
||||
this.data[`subType${i}`] = e.target.value;
|
||||
renderSubInputs();
|
||||
});
|
||||
|
||||
row.appendChild(colDiv);
|
||||
|
||||
this._renderTypeSpecificInput(i, contentDiv);
|
||||
renderSubInputs();
|
||||
}
|
||||
|
||||
this.inputsContainer.appendChild(row);
|
||||
}
|
||||
|
||||
_renderTypeSpecificInput(i, container, explicitType) {
|
||||
container.innerHTML = '';
|
||||
|
||||
// Remove tracking of previous instance
|
||||
if (this.activeInstances[i]) {
|
||||
delete this.activeInstances[i];
|
||||
}
|
||||
|
||||
const typeContainer = document.createElement('div');
|
||||
typeContainer.className = 'mb-3';
|
||||
container.appendChild(typeContainer);
|
||||
|
||||
const type = explicitType || this.data[`type${i}`] || 'html';
|
||||
|
||||
// Check if type is a dynamic editor plugin registered in window.SISEditorPlugins
|
||||
if (window.SISEditorPlugins && window.SISEditorPlugins[type]) {
|
||||
try {
|
||||
const pluginEntry = window.SISEditorPlugins[type];
|
||||
const pluginClass = pluginEntry.class;
|
||||
const pluginConfig = pluginEntry.config || {};
|
||||
let parsedData = {};
|
||||
if (this.data[`col${i}`]) {
|
||||
try {
|
||||
parsedData = typeof this.data[`col${i}`] === 'string' ? JSON.parse(this.data[`col${i}`]) : this.data[`col${i}`];
|
||||
} catch (e) {
|
||||
parsedData = { html: this.data[`col${i}`] }; // fallback
|
||||
}
|
||||
}
|
||||
const instance = new pluginClass({
|
||||
data: parsedData,
|
||||
api: this.api,
|
||||
readOnly: this.readOnly,
|
||||
config: pluginConfig,
|
||||
block: {
|
||||
id: 'nested-flex-' + i + '-' + Math.random().toString(36).substring(7),
|
||||
name: type,
|
||||
holder: typeContainer,
|
||||
isEmpty: false,
|
||||
selected: false,
|
||||
stretched: false,
|
||||
tunes: {}
|
||||
}
|
||||
});
|
||||
this.activeInstances[i] = instance;
|
||||
|
||||
const element = instance.render();
|
||||
typeContainer.appendChild(element);
|
||||
|
||||
// Fix for Editor.js Image plugin 404 errors (spinner hangs forever)
|
||||
if (type === 'image') {
|
||||
// Custom UI override for nested images
|
||||
const injectCustomButtons = () => {
|
||||
if (element.querySelector('.sis-custom-image-controls')) return;
|
||||
|
||||
const fileBtn = element.querySelector('.cdx-button');
|
||||
if (fileBtn) {
|
||||
fileBtn.style.display = 'none'; // Hide native button
|
||||
}
|
||||
|
||||
const controls = document.createElement('div');
|
||||
controls.className = 'sis-custom-image-controls mt-2 p-2 border rounded bg-light';
|
||||
controls.style.display = 'flex';
|
||||
controls.style.flexDirection = 'column';
|
||||
controls.style.gap = '8px';
|
||||
|
||||
controls.innerHTML = `
|
||||
<div class="text-muted small text-center mb-1"><i class="fas fa-cog"></i> Image Controls</div>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm w-100" id="btn-upload-img-${i}"><i class="fas fa-upload"></i> Upload Image</button>
|
||||
<button type="button" class="btn btn-outline-info btn-sm w-100" id="btn-fetch-url-${i}"><i class="fas fa-link"></i> Fetch URL</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm w-100" id="btn-media-lib-${i}"><i class="fas fa-images"></i> Media Library</button>
|
||||
<button type="button" class="btn btn-outline-danger btn-sm w-100 mt-2" id="btn-clear-img-${i}"><i class="fas fa-trash"></i> Remove Image</button>
|
||||
`;
|
||||
|
||||
const uploadBtn = controls.querySelector(`#btn-upload-img-${i}`);
|
||||
if (uploadBtn && fileBtn) {
|
||||
uploadBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
fileBtn.click();
|
||||
});
|
||||
}
|
||||
|
||||
const urlBtn = controls.querySelector(`#btn-fetch-url-${i}`);
|
||||
if (urlBtn) {
|
||||
urlBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const url = prompt('Enter Image URL:');
|
||||
if (url && url.trim() !== '') {
|
||||
this.data[`col${i}`] = {
|
||||
file: { url: url.trim() },
|
||||
caption: '',
|
||||
withBorder: false,
|
||||
withBackground: false,
|
||||
stretched: false
|
||||
};
|
||||
this._renderTypeSpecificInput(i, container);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const mediaBtn = controls.querySelector(`#btn-media-lib-${i}`);
|
||||
if (mediaBtn) {
|
||||
mediaBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (window.SISMediaPicker) {
|
||||
SISMediaPicker.open((url, mediaObj, config) => {
|
||||
if (url) {
|
||||
this.data[`col${i}`] = {
|
||||
file: { url: url },
|
||||
caption: (config && config.alt) || '',
|
||||
withBorder: false,
|
||||
withBackground: false,
|
||||
stretched: false
|
||||
};
|
||||
if (config) {
|
||||
this.data[`imgStyle${i}`] = config.style || '';
|
||||
this.data[`imgClass${i}`] = config.cssClass || '';
|
||||
this.data[`imgAlt${i}`] = config.alt || '';
|
||||
this.data[`imgAttrs${i}`] = config.customAttributes || '';
|
||||
this.data[`imgAspectRatio${i}`] = config.aspectRatio || '';
|
||||
}
|
||||
this._renderTypeSpecificInput(i, container);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
alert('Media Library is not yet configured on this system.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const clearBtn = controls.querySelector(`#btn-clear-img-${i}`);
|
||||
if (clearBtn) {
|
||||
clearBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (confirm('Are you sure you want to remove this image?')) {
|
||||
this.data[`col${i}`] = '';
|
||||
this._renderTypeSpecificInput(i, container);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
element.appendChild(controls);
|
||||
};
|
||||
injectCustomButtons();
|
||||
|
||||
const handleImageError = () => {
|
||||
element.classList.remove('image-tool--loading');
|
||||
element.classList.add('image-tool--empty');
|
||||
const preloader = element.querySelector('.image-tool__image-preloader');
|
||||
if (preloader) preloader.style.display = 'none';
|
||||
};
|
||||
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
mutations.forEach(mutation => {
|
||||
mutation.addedNodes.forEach(node => {
|
||||
if (node.tagName === 'IMG') {
|
||||
node.addEventListener('error', handleImageError);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
observer.observe(element, { childList: true, subtree: true });
|
||||
|
||||
const existingImg = element.querySelector('img');
|
||||
if (existingImg) existingImg.addEventListener('error', handleImageError);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Failed to render plugin ${type} inside FlexTool:`, err);
|
||||
typeContainer.innerHTML = `<div class="alert alert-danger small">Error loading plugin ${type}</div>`;
|
||||
}
|
||||
} else if (type === 'html') {
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.className = 'form-control form-control-sm';
|
||||
textarea.style.minHeight = '140px';
|
||||
textarea.style.fontSize = '12px';
|
||||
textarea.placeholder = `HTML / Text for Item ${i}...`;
|
||||
textarea.value = typeof this.data[`col${i}`] === 'object' ? JSON.stringify(this.data[`col${i}`]) : this.data[`col${i}`] || '';
|
||||
if (this.readOnly) textarea.disabled = true;
|
||||
|
||||
textarea.addEventListener('input', (e) => {
|
||||
this.data[`col${i}`] = e.target.value;
|
||||
});
|
||||
typeContainer.appendChild(textarea);
|
||||
|
||||
} else if (type === 'image') {
|
||||
const inputGroup = document.createElement('div');
|
||||
inputGroup.className = 'input-group input-group-sm mb-2';
|
||||
|
||||
const input = document.createElement('input');
|
||||
input.type = 'text';
|
||||
input.className = 'form-control form-control-sm';
|
||||
input.placeholder = 'Paste image URL here...';
|
||||
input.value = this.data[`imgUrl${i}`] || '';
|
||||
if (this.readOnly) input.disabled = true;
|
||||
|
||||
const appendDiv = document.createElement('div');
|
||||
appendDiv.className = 'input-group-append';
|
||||
|
||||
const chooseBtn = document.createElement('button');
|
||||
chooseBtn.type = 'button';
|
||||
chooseBtn.className = 'btn btn-outline-secondary';
|
||||
chooseBtn.innerText = 'Choose';
|
||||
if (this.readOnly) chooseBtn.disabled = true;
|
||||
chooseBtn.addEventListener('click', () => {
|
||||
if (window.SISMediaPicker) {
|
||||
SISMediaPicker.open((selectedUrl, mediaObj, config) => {
|
||||
input.value = selectedUrl;
|
||||
this.data[`imgUrl${i}`] = selectedUrl;
|
||||
if (config) {
|
||||
this.data[`imgStyle${i}`] = config.style || '';
|
||||
this.data[`imgClass${i}`] = config.cssClass || '';
|
||||
this.data[`imgAlt${i}`] = config.alt || '';
|
||||
this.data[`imgAttrs${i}`] = config.customAttributes || '';
|
||||
this.data[`imgAspectRatio${i}`] = config.aspectRatio || '';
|
||||
}
|
||||
previewImg.src = selectedUrl;
|
||||
previewBox.style.display = 'block';
|
||||
});
|
||||
} else {
|
||||
alert('Media Picker modal helper is not loaded.');
|
||||
}
|
||||
});
|
||||
|
||||
const uploadBtn = document.createElement('button');
|
||||
uploadBtn.type = 'button';
|
||||
uploadBtn.className = 'btn btn-outline-primary';
|
||||
uploadBtn.innerText = 'Upload';
|
||||
if (this.readOnly) uploadBtn.disabled = true;
|
||||
|
||||
const fileInput = document.createElement('input');
|
||||
fileInput.type = 'file';
|
||||
fileInput.accept = 'image/*';
|
||||
fileInput.style.display = 'none';
|
||||
|
||||
uploadBtn.addEventListener('click', () => fileInput.click());
|
||||
fileInput.addEventListener('change', async (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('image', file);
|
||||
|
||||
uploadBtn.disabled = true;
|
||||
uploadBtn.innerText = '...';
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/manage/media/upload', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
const result = await response.json();
|
||||
if (result.success && result.file && result.file.url) {
|
||||
const url = result.file.url;
|
||||
input.value = url;
|
||||
this.data[`imgUrl${i}`] = url;
|
||||
previewImg.src = url;
|
||||
previewBox.style.display = 'block';
|
||||
} else {
|
||||
alert('Upload failed: ' + (result.message || 'Unknown error'));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert('Upload error occurred.');
|
||||
} finally {
|
||||
uploadBtn.disabled = false;
|
||||
uploadBtn.innerText = 'Upload';
|
||||
}
|
||||
});
|
||||
|
||||
appendDiv.appendChild(chooseBtn);
|
||||
appendDiv.appendChild(uploadBtn);
|
||||
inputGroup.appendChild(input);
|
||||
inputGroup.appendChild(appendDiv);
|
||||
typeContainer.appendChild(inputGroup);
|
||||
typeContainer.appendChild(fileInput);
|
||||
|
||||
const previewBox = document.createElement('div');
|
||||
previewBox.className = 'border rounded p-1 text-center bg-light';
|
||||
previewBox.style.display = this.data[`imgUrl${i}`] ? 'block' : 'none';
|
||||
previewBox.style.maxHeight = '150px';
|
||||
previewBox.style.overflow = 'hidden';
|
||||
|
||||
const previewImg = document.createElement('img');
|
||||
previewImg.src = this.data[`imgUrl${i}`] || '';
|
||||
previewImg.style.maxWidth = '100%';
|
||||
previewImg.style.maxHeight = '140px';
|
||||
previewImg.style.objectFit = 'contain';
|
||||
|
||||
previewBox.appendChild(previewImg);
|
||||
typeContainer.appendChild(previewBox);
|
||||
|
||||
input.addEventListener('input', (e) => {
|
||||
const val = e.target.value.trim();
|
||||
this.data[`imgUrl${i}`] = val;
|
||||
if (val) {
|
||||
previewImg.src = val;
|
||||
previewBox.style.display = 'block';
|
||||
} else {
|
||||
previewBox.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
} else if (type === 'youtube') {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'text';
|
||||
input.className = 'form-control form-control-sm mb-2';
|
||||
input.placeholder = 'Paste YouTube video URL...';
|
||||
input.value = this.data[`ytUrl${i}`] || '';
|
||||
if (this.readOnly) input.disabled = true;
|
||||
|
||||
const preview = document.createElement('div');
|
||||
preview.className = 'small text-muted p-2 bg-light border rounded';
|
||||
preview.innerText = 'Format: https://www.youtube.com/watch?v=XXXXXX';
|
||||
|
||||
input.addEventListener('input', (e) => {
|
||||
this.data[`ytUrl${i}`] = e.target.value.trim();
|
||||
});
|
||||
|
||||
typeContainer.appendChild(input);
|
||||
typeContainer.appendChild(preview);
|
||||
|
||||
} else if (type === 'accordion') {
|
||||
const titleInput = document.createElement('input');
|
||||
titleInput.type = 'text';
|
||||
titleInput.className = 'form-control form-control-sm mb-2';
|
||||
titleInput.placeholder = 'Accordion Title...';
|
||||
titleInput.value = this.data[`accTitle${i}`] || '';
|
||||
if (this.readOnly) titleInput.disabled = true;
|
||||
|
||||
const contentTextarea = document.createElement('textarea');
|
||||
contentTextarea.className = 'form-control form-control-sm';
|
||||
contentTextarea.style.minHeight = '90px';
|
||||
contentTextarea.style.fontSize = '12px';
|
||||
contentTextarea.placeholder = 'Accordion Body HTML...';
|
||||
contentTextarea.value = this.data[`accContent${i}`] || '';
|
||||
if (this.readOnly) contentTextarea.disabled = true;
|
||||
|
||||
titleInput.addEventListener('input', (e) => {
|
||||
this.data[`accTitle${i}`] = e.target.value;
|
||||
});
|
||||
contentTextarea.addEventListener('input', (e) => {
|
||||
this.data[`accContent${i}`] = e.target.value;
|
||||
});
|
||||
|
||||
typeContainer.appendChild(titleInput);
|
||||
typeContainer.appendChild(contentTextarea);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Advanced Settings (ID and Class)
|
||||
const advancedHeader = document.createElement('div');
|
||||
advancedHeader.className = 'font-weight-bold text-muted small mb-2';
|
||||
advancedHeader.style.fontSize = '10px';
|
||||
advancedHeader.innerText = 'ADVANCED SETTINGS';
|
||||
container.appendChild(advancedHeader);
|
||||
|
||||
const advRow = document.createElement('div');
|
||||
advRow.className = 'form-row';
|
||||
|
||||
const classCol = document.createElement('div');
|
||||
classCol.className = 'col';
|
||||
const classInput = document.createElement('input');
|
||||
classInput.type = 'text';
|
||||
classInput.className = 'form-control form-control-sm';
|
||||
classInput.style.fontSize = '11px';
|
||||
classInput.placeholder = 'CSS Class(es)...';
|
||||
classInput.value = this.data[`class${i}`] || '';
|
||||
if (this.readOnly) classInput.disabled = true;
|
||||
classInput.addEventListener('input', (e) => {
|
||||
this.data[`class${i}`] = e.target.value.trim();
|
||||
});
|
||||
classCol.appendChild(classInput);
|
||||
|
||||
const idCol = document.createElement('div');
|
||||
idCol.className = 'col';
|
||||
const idInput = document.createElement('input');
|
||||
idInput.type = 'text';
|
||||
idInput.className = 'form-control form-control-sm';
|
||||
idInput.style.fontSize = '11px';
|
||||
idInput.placeholder = 'HTML ID...';
|
||||
idInput.value = this.data[`id${i}`] || '';
|
||||
if (this.readOnly) idInput.disabled = true;
|
||||
idInput.addEventListener('input', (e) => {
|
||||
this.data[`id${i}`] = e.target.value.trim();
|
||||
});
|
||||
idCol.appendChild(idInput);
|
||||
|
||||
advRow.appendChild(classCol);
|
||||
advRow.appendChild(idCol);
|
||||
container.appendChild(advRow);
|
||||
}
|
||||
|
||||
save(blockContent) {
|
||||
const savedData = {
|
||||
cols: this.data.cols,
|
||||
direction: this.data.direction,
|
||||
justify: this.data.justify,
|
||||
align: this.data.align,
|
||||
gap: this.data.gap,
|
||||
globalClass: this.globalClassInput ? this.globalClassInput.value.trim() : (this.data.globalClass || ''),
|
||||
globalId: this.globalIdInput ? this.globalIdInput.value.trim() : (this.data.globalId || ''),
|
||||
globalAttributes: this.globalAttrInput ? this.globalAttrInput.value.trim() : (this.data.globalAttributes || '')
|
||||
};
|
||||
for (let i = 1; i <= this.data.cols; i++) {
|
||||
const type = this.data[`type${i}`] || 'html';
|
||||
savedData[`type${i}`] = type;
|
||||
|
||||
// If it is a dynamically nested plugin, save its output data
|
||||
if (this.activeInstances[i]) {
|
||||
try {
|
||||
const pluginData = this.activeInstances[i].save();
|
||||
savedData[`col${i}`] = pluginData;
|
||||
} catch (e) {
|
||||
console.error(`Failed to save plugin ${type} inside FlexTool:`, e);
|
||||
savedData[`col${i}`] = this.data[`col${i}`];
|
||||
}
|
||||
} else {
|
||||
savedData[`col${i}`] = this.data[`col${i}`] || '';
|
||||
}
|
||||
|
||||
savedData[`imgUrl${i}`] = this.data[`imgUrl${i}`] || '';
|
||||
savedData[`ytUrl${i}`] = this.data[`ytUrl${i}`] || '';
|
||||
savedData[`accTitle${i}`] = this.data[`accTitle${i}`] || '';
|
||||
savedData[`accContent${i}`] = this.data[`accContent${i}`] || '';
|
||||
savedData[`class${i}`] = this.data[`class${i}`] || '';
|
||||
savedData[`id${i}`] = this.data[`id${i}`] || '';
|
||||
savedData[`attr${i}`] = this.data[`attr${i}`] || '';
|
||||
savedData[`width${i}`] = this.data[`width${i}`] || 0;
|
||||
savedData[`caption${i}`] = this.data[`caption${i}`] || '';
|
||||
|
||||
// Sub-plugin serialization
|
||||
const subType = this.data[`subType${i}`] || 'none';
|
||||
savedData[`subType${i}`] = subType;
|
||||
|
||||
if (this.activeInstances[`sub_${i}`]) {
|
||||
try {
|
||||
const subPluginData = this.activeInstances[`sub_${i}`].save();
|
||||
savedData[`colsub_${i}`] = subPluginData;
|
||||
} catch (e) {
|
||||
console.error(`Failed to save sub-plugin ${subType} inside FlexTool:`, e);
|
||||
savedData[`colsub_${i}`] = this.data[`colsub_${i}`];
|
||||
}
|
||||
} else {
|
||||
savedData[`colsub_${i}`] = this.data[`colsub_${i}`] || '';
|
||||
}
|
||||
|
||||
savedData[`imgUrlsub_${i}`] = this.data[`imgUrlsub_${i}`] || '';
|
||||
savedData[`ytUrlsub_${i}`] = this.data[`ytUrlsub_${i}`] || '';
|
||||
savedData[`accTitlesub_${i}`] = this.data[`accTitlesub_${i}`] || '';
|
||||
savedData[`accContentsub_${i}`] = this.data[`accContentsub_${i}`] || '';
|
||||
savedData[`classsub_${i}`] = this.data[`classsub_${i}`] || '';
|
||||
savedData[`idsub_${i}`] = this.data[`idsub_${i}`] || '';
|
||||
savedData[`attrsub_${i}`] = this.data[`attrsub_${i}`] || '';
|
||||
}
|
||||
return savedData;
|
||||
}
|
||||
}
|
||||
|
||||
// Register the plugin globally
|
||||
window.SISEditorPlugins = window.SISEditorPlugins || {};
|
||||
window.SISEditorPlugins['flex'] = {
|
||||
class: FlexTool
|
||||
};
|
||||
+806
@@ -0,0 +1,806 @@
|
||||
/**
|
||||
* Grid block tool for Editor.js.
|
||||
* Allows users to create responsive layouts with any number of columns and choose different content types, manual widths,
|
||||
* and dynamically instantiates other registered editor plugins inside columns.
|
||||
*/
|
||||
class GridTool {
|
||||
static get toolbox() {
|
||||
return {
|
||||
title: 'Grid / Columns',
|
||||
icon: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="9"></rect><rect x="14" y="3" width="7" height="9"></rect><rect x="14" y="15" width="7" height="6"></rect><rect x="3" y="15" width="7" height="6"></rect></svg>'
|
||||
};
|
||||
}
|
||||
|
||||
constructor({ data, api, readOnly }) {
|
||||
this.api = api;
|
||||
this.readOnly = readOnly;
|
||||
this.data = {
|
||||
cols: parseInt(data.cols) || 2,
|
||||
globalClass: data.globalClass || '',
|
||||
globalId: data.globalId || '',
|
||||
globalAttributes: data.globalAttributes || data.attributes || ''
|
||||
};
|
||||
this.activeInstances = {};
|
||||
|
||||
// Load column contents dynamically
|
||||
for (let i = 1; i <= 12; i++) {
|
||||
this.data[`type${i}`] = data[`type${i}`] || 'html';
|
||||
this.data[`col${i}`] = data[`col${i}`] || '';
|
||||
this.data[`imgUrl${i}`] = data[`imgUrl${i}`] || '';
|
||||
this.data[`ytUrl${i}`] = data[`ytUrl${i}`] || '';
|
||||
this.data[`accTitle${i}`] = data[`accTitle${i}`] || '';
|
||||
this.data[`accContent${i}`] = data[`accContent${i}`] || '';
|
||||
this.data[`class${i}`] = data[`class${i}`] || '';
|
||||
this.data[`id${i}`] = data[`id${i}`] || '';
|
||||
this.data[`attr${i}`] = data[`attr${i}`] || data[`attributes${i}`] || '';
|
||||
this.data[`width${i}`] = parseInt(data[`width${i}`]) || 0; // 0 means auto
|
||||
this.data[`caption${i}`] = data[`caption${i}`] || '';
|
||||
|
||||
// Sub-plugin properties
|
||||
this.data[`subType${i}`] = data[`subType${i}`] || 'none';
|
||||
this.data[`colsub_${i}`] = data[`colsub_${i}`] || '';
|
||||
this.data[`imgUrlsub_${i}`] = data[`imgUrlsub_${i}`] || '';
|
||||
this.data[`ytUrlsub_${i}`] = data[`ytUrlsub_${i}`] || '';
|
||||
this.data[`accTitlesub_${i}`] = data[`accTitlesub_${i}`] || '';
|
||||
this.data[`accContentsub_${i}`] = data[`accContentsub_${i}`] || '';
|
||||
this.data[`classsub_${i}`] = data[`classsub_${i}`] || '';
|
||||
this.data[`idsub_${i}`] = data[`idsub_${i}`] || '';
|
||||
this.data[`attrsub_${i}`] = data[`attrsub_${i}`] || '';
|
||||
}
|
||||
this.wrapper = undefined;
|
||||
}
|
||||
|
||||
render() {
|
||||
this.wrapper = document.createElement('div');
|
||||
this.wrapper.className = 'p-3 bg-light border rounded mb-3 ce-grid-tool-wrapper';
|
||||
this.wrapper.style.fontFamily = 'inherit';
|
||||
|
||||
const title = document.createElement('div');
|
||||
title.className = 'font-weight-bold text-primary small mb-3';
|
||||
title.innerHTML = '<i class="fas fa-columns"></i> Multi-Column Grid Layout';
|
||||
this.wrapper.appendChild(title);
|
||||
|
||||
// Layout Settings Row
|
||||
const settingsRow = document.createElement('div');
|
||||
settingsRow.className = 'form-row mb-3 pb-2 border-bottom';
|
||||
|
||||
// 1. Columns
|
||||
const colDiv = document.createElement('div');
|
||||
colDiv.className = 'col-md-2 mb-2';
|
||||
colDiv.innerHTML = '<label class="small font-weight-bold text-secondary mb-1">Columns</label>';
|
||||
const colInput = document.createElement('input');
|
||||
colInput.type = 'number';
|
||||
colInput.className = 'form-control form-control-sm';
|
||||
colInput.min = '1';
|
||||
colInput.max = '12';
|
||||
colInput.value = this.data.cols;
|
||||
if (this.readOnly) colInput.disabled = true;
|
||||
colInput.addEventListener('input', (e) => {
|
||||
let val = parseInt(e.target.value) || 2;
|
||||
if (val < 1) val = 1;
|
||||
if (val > 12) val = 12;
|
||||
this.data.cols = val;
|
||||
this._renderColumnInputs();
|
||||
});
|
||||
colDiv.appendChild(colInput);
|
||||
settingsRow.appendChild(colDiv);
|
||||
|
||||
// 2. Global CSS Class
|
||||
const classDiv = document.createElement('div');
|
||||
classDiv.className = 'col-md-3 mb-2';
|
||||
classDiv.innerHTML = '<label class="small font-weight-bold text-secondary mb-1">Grid CSS Class(es)</label>';
|
||||
const classInput = document.createElement('input');
|
||||
classInput.type = 'text';
|
||||
classInput.className = 'form-control form-control-sm';
|
||||
classInput.placeholder = 'e.g. my-custom-grid';
|
||||
classInput.value = this.data.globalClass || '';
|
||||
if (this.readOnly) classInput.disabled = true;
|
||||
classInput.addEventListener('input', (e) => this.data.globalClass = e.target.value.trim());
|
||||
this.globalClassInput = classInput;
|
||||
classDiv.appendChild(classInput);
|
||||
settingsRow.appendChild(classDiv);
|
||||
|
||||
// 3. Global HTML ID
|
||||
const idDiv = document.createElement('div');
|
||||
idDiv.className = 'col-md-3 mb-2';
|
||||
idDiv.innerHTML = '<label class="small font-weight-bold text-secondary mb-1">Grid HTML ID</label>';
|
||||
const idInput = document.createElement('input');
|
||||
idInput.type = 'text';
|
||||
idInput.className = 'form-control form-control-sm';
|
||||
idInput.placeholder = 'e.g. grid-section-1';
|
||||
idInput.value = this.data.globalId || '';
|
||||
if (this.readOnly) idInput.disabled = true;
|
||||
idInput.addEventListener('input', (e) => this.data.globalId = e.target.value.trim());
|
||||
this.globalIdInput = idInput;
|
||||
idDiv.appendChild(idInput);
|
||||
settingsRow.appendChild(idDiv);
|
||||
|
||||
// 4. Global Custom Attributes
|
||||
const attrDiv = document.createElement('div');
|
||||
attrDiv.className = 'col-md-4 mb-2';
|
||||
attrDiv.innerHTML = '<label class="small font-weight-bold text-secondary mb-1"><i class="fas fa-sliders-h"></i> Custom Attributes</label>';
|
||||
const attrInput = document.createElement('input');
|
||||
attrInput.type = 'text';
|
||||
attrInput.className = 'form-control form-control-sm';
|
||||
attrInput.placeholder = 'e.g. data-aos="fade-up" style="background:#fff"';
|
||||
attrInput.value = this.data.globalAttributes || '';
|
||||
if (this.readOnly) attrInput.disabled = true;
|
||||
attrInput.addEventListener('input', (e) => this.data.globalAttributes = e.target.value.trim());
|
||||
this.globalAttrInput = attrInput;
|
||||
attrDiv.appendChild(attrInput);
|
||||
settingsRow.appendChild(attrDiv);
|
||||
|
||||
this.wrapper.appendChild(settingsRow);
|
||||
|
||||
// Columns inputs container
|
||||
this.inputsContainer = document.createElement('div');
|
||||
this.wrapper.appendChild(this.inputsContainer);
|
||||
|
||||
this._renderColumnInputs();
|
||||
|
||||
return this.wrapper;
|
||||
}
|
||||
|
||||
_renderColumnInputs() {
|
||||
this.inputsContainer.innerHTML = '';
|
||||
|
||||
const row = document.createElement('div');
|
||||
row.className = 'row d-flex flex-wrap';
|
||||
|
||||
const count = this.data.cols;
|
||||
const colWidthClass = count === 1 ? 'col-12' : (count === 2 ? 'col-md-6' : (count === 3 ? 'col-md-4' : (count === 4 ? 'col-md-3' : 'col-md')));
|
||||
|
||||
for (let i = 1; i <= count; i++) {
|
||||
const colDiv = document.createElement('div');
|
||||
colDiv.className = `${colWidthClass} mb-3 p-2 border rounded bg-white`;
|
||||
colDiv.style.boxShadow = '0 1px 3px rgba(0,0,0,0.05)';
|
||||
|
||||
const headerRow = document.createElement('div');
|
||||
headerRow.className = 'd-flex justify-content-between align-items-center mb-2 pb-1 border-bottom';
|
||||
|
||||
const label = document.createElement('label');
|
||||
label.className = 'small font-weight-bold text-dark mb-0';
|
||||
label.innerText = `Col ${i}`;
|
||||
headerRow.appendChild(label);
|
||||
|
||||
const controlsWrapper = document.createElement('div');
|
||||
controlsWrapper.className = 'd-flex align-items-center gap-1';
|
||||
|
||||
// Width Selector
|
||||
const widthSelect = document.createElement('select');
|
||||
widthSelect.className = 'custom-select custom-select-sm mr-1';
|
||||
widthSelect.style.width = '75px';
|
||||
if (this.readOnly) widthSelect.disabled = true;
|
||||
|
||||
const widthOpts = [
|
||||
{ val: 0, lbl: 'Auto' },
|
||||
{ val: 1, lbl: '1/12' },
|
||||
{ val: 2, lbl: '2/12' },
|
||||
{ val: 3, lbl: '3/12' },
|
||||
{ val: 4, lbl: '4/12' },
|
||||
{ val: 5, lbl: '5/12' },
|
||||
{ val: 6, lbl: '6/12' },
|
||||
{ val: 7, lbl: '7/12' },
|
||||
{ val: 8, lbl: '8/12' },
|
||||
{ val: 9, lbl: '9/12' },
|
||||
{ val: 10, lbl: '10/12' },
|
||||
{ val: 11, lbl: '11/12' },
|
||||
{ val: 12, lbl: '12/12' }
|
||||
];
|
||||
|
||||
widthOpts.forEach(o => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = o.val;
|
||||
opt.innerText = o.lbl;
|
||||
opt.selected = this.data[`width${i}`] === o.val;
|
||||
widthSelect.appendChild(opt);
|
||||
});
|
||||
|
||||
widthSelect.addEventListener('change', (e) => {
|
||||
this.data[`width${i}`] = parseInt(e.target.value) || 0;
|
||||
});
|
||||
|
||||
controlsWrapper.appendChild(widthSelect);
|
||||
|
||||
// Column Type Select (Built-ins + Registered plugins dynamically)
|
||||
const typeSelect = document.createElement('select');
|
||||
typeSelect.className = 'custom-select custom-select-sm';
|
||||
typeSelect.style.width = '100px';
|
||||
if (this.readOnly) typeSelect.disabled = true;
|
||||
|
||||
// Base types
|
||||
const types = [
|
||||
{ value: 'html', label: 'HTML/Text' },
|
||||
{ value: 'image', label: 'Image' },
|
||||
{ value: 'youtube', label: 'YouTube' },
|
||||
{ value: 'accordion', label: 'Accordion' }
|
||||
];
|
||||
|
||||
// Dynamically add other registered plugins from window.SISEditorPlugins
|
||||
if (window.SISEditorPlugins) {
|
||||
Object.keys(window.SISEditorPlugins).forEach(key => {
|
||||
// Allow nesting any registered plugins (no exclusions)
|
||||
if (!types.some(t => t.value === key)) {
|
||||
types.push({ value: key, label: `[Plugin] ${key}` });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
types.forEach(t => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = t.value;
|
||||
opt.innerText = t.label;
|
||||
opt.selected = this.data[`type${i}`] === t.value;
|
||||
typeSelect.appendChild(opt);
|
||||
});
|
||||
|
||||
const contentDiv = document.createElement('div');
|
||||
|
||||
typeSelect.addEventListener('change', (e) => {
|
||||
this.data[`type${i}`] = e.target.value;
|
||||
this._renderTypeSpecificInput(i, contentDiv);
|
||||
});
|
||||
|
||||
controlsWrapper.appendChild(typeSelect);
|
||||
headerRow.appendChild(controlsWrapper);
|
||||
colDiv.appendChild(headerRow);
|
||||
colDiv.appendChild(contentDiv);
|
||||
|
||||
// --- Sub-Plugin (Sub-Content) Section ---
|
||||
const subWrapper = document.createElement('div');
|
||||
if (this.data[`subType${i}`] !== 'none') {
|
||||
subWrapper.className = 'mt-3 pt-2 border-top';
|
||||
}
|
||||
|
||||
const subHeader = document.createElement('div');
|
||||
subHeader.className = 'd-flex justify-content-between align-items-center mb-2';
|
||||
|
||||
const subLabel = document.createElement('label');
|
||||
subLabel.className = 'small font-weight-bold text-muted mb-0';
|
||||
subLabel.style.fontSize = '10px';
|
||||
subLabel.innerText = 'SUB-CONTENT / INJECT PLUGIN';
|
||||
subHeader.appendChild(subLabel);
|
||||
|
||||
const subTypeSelect = document.createElement('select');
|
||||
subTypeSelect.className = 'custom-select custom-select-sm';
|
||||
subTypeSelect.style.width = '130px';
|
||||
subTypeSelect.style.fontSize = '10px';
|
||||
if (this.readOnly) subTypeSelect.disabled = true;
|
||||
|
||||
const subTypes = [
|
||||
{ value: 'none', label: 'None' },
|
||||
{ value: 'caption', label: 'Text Caption' },
|
||||
{ value: 'html', label: 'HTML/Text' },
|
||||
{ value: 'image', label: 'Image' },
|
||||
{ value: 'youtube', label: 'YouTube' },
|
||||
{ value: 'accordion', label: 'Accordion' }
|
||||
];
|
||||
if (window.SISEditorPlugins) {
|
||||
Object.keys(window.SISEditorPlugins).forEach(key => {
|
||||
if (!subTypes.some(t => t.value === key)) {
|
||||
subTypes.push({ value: key, label: `[Plugin] ${key}` });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
subTypes.forEach(t => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = t.value;
|
||||
opt.innerText = t.label;
|
||||
opt.selected = this.data[`subType${i}`] === t.value;
|
||||
subTypeSelect.appendChild(opt);
|
||||
});
|
||||
subHeader.appendChild(subTypeSelect);
|
||||
subWrapper.appendChild(subHeader);
|
||||
|
||||
const subContentDiv = document.createElement('div');
|
||||
subWrapper.appendChild(subContentDiv);
|
||||
colDiv.appendChild(subWrapper);
|
||||
|
||||
const renderSubInputs = () => {
|
||||
subContentDiv.innerHTML = '';
|
||||
const subType = this.data[`subType${i}`];
|
||||
if (subType === 'none') {
|
||||
subWrapper.className = '';
|
||||
} else {
|
||||
subWrapper.className = 'mt-3 pt-2 border-top';
|
||||
}
|
||||
|
||||
if (subType === 'none') {
|
||||
// Empty
|
||||
} else if (subType === 'caption') {
|
||||
const captionGroup = document.createElement('div');
|
||||
captionGroup.className = 'form-group mb-2';
|
||||
const captionInput = document.createElement('input');
|
||||
captionInput.type = 'text';
|
||||
captionInput.className = 'form-control form-control-sm';
|
||||
captionInput.style.fontSize = '11px';
|
||||
captionInput.placeholder = 'Optional column caption / text...';
|
||||
captionInput.value = this.data[`caption${i}`] || '';
|
||||
if (this.readOnly) captionInput.disabled = true;
|
||||
captionInput.addEventListener('input', (e) => {
|
||||
this.data[`caption${i}`] = e.target.value;
|
||||
});
|
||||
captionGroup.appendChild(captionInput);
|
||||
subContentDiv.appendChild(captionGroup);
|
||||
} else {
|
||||
this._renderTypeSpecificInput(`sub_${i}`, subContentDiv, subType);
|
||||
}
|
||||
};
|
||||
|
||||
subTypeSelect.addEventListener('change', (e) => {
|
||||
this.data[`subType${i}`] = e.target.value;
|
||||
renderSubInputs();
|
||||
});
|
||||
|
||||
row.appendChild(colDiv);
|
||||
|
||||
this._renderTypeSpecificInput(i, contentDiv);
|
||||
renderSubInputs();
|
||||
}
|
||||
|
||||
this.inputsContainer.appendChild(row);
|
||||
}
|
||||
|
||||
_renderTypeSpecificInput(i, container, explicitType) {
|
||||
container.innerHTML = '';
|
||||
|
||||
// Remove tracking of previous instance
|
||||
if (this.activeInstances[i]) {
|
||||
delete this.activeInstances[i];
|
||||
}
|
||||
|
||||
const typeContainer = document.createElement('div');
|
||||
typeContainer.className = 'mb-3';
|
||||
container.appendChild(typeContainer);
|
||||
|
||||
const type = explicitType || this.data[`type${i}`] || 'html';
|
||||
|
||||
// Check if type is a dynamic editor plugin registered in window.SISEditorPlugins
|
||||
if (window.SISEditorPlugins && window.SISEditorPlugins[type]) {
|
||||
try {
|
||||
const pluginEntry = window.SISEditorPlugins[type];
|
||||
const pluginClass = pluginEntry.class;
|
||||
const pluginConfig = pluginEntry.config || {};
|
||||
let parsedData = {};
|
||||
if (this.data[`col${i}`]) {
|
||||
try {
|
||||
parsedData = typeof this.data[`col${i}`] === 'string' ? JSON.parse(this.data[`col${i}`]) : this.data[`col${i}`];
|
||||
} catch (e) {
|
||||
parsedData = { html: this.data[`col${i}`] }; // fallback
|
||||
}
|
||||
}
|
||||
const instance = new pluginClass({
|
||||
data: parsedData,
|
||||
api: this.api,
|
||||
readOnly: this.readOnly,
|
||||
config: pluginConfig,
|
||||
block: {
|
||||
id: 'nested-grid-' + i + '-' + Math.random().toString(36).substring(7),
|
||||
name: type,
|
||||
holder: typeContainer,
|
||||
isEmpty: false,
|
||||
selected: false,
|
||||
stretched: false,
|
||||
tunes: {}
|
||||
}
|
||||
});
|
||||
this.activeInstances[i] = instance;
|
||||
|
||||
const element = instance.render();
|
||||
typeContainer.appendChild(element);
|
||||
|
||||
// Fix for Editor.js Image plugin 404 errors (spinner hangs forever)
|
||||
if (type === 'image') {
|
||||
// Custom UI override for nested images
|
||||
const injectCustomButtons = () => {
|
||||
if (element.querySelector('.sis-custom-image-controls')) return;
|
||||
|
||||
const fileBtn = element.querySelector('.cdx-button');
|
||||
if (fileBtn) {
|
||||
fileBtn.style.display = 'none'; // Hide native button
|
||||
}
|
||||
|
||||
const controls = document.createElement('div');
|
||||
controls.className = 'sis-custom-image-controls mt-2 p-2 border rounded bg-light';
|
||||
controls.style.display = 'flex';
|
||||
controls.style.flexDirection = 'column';
|
||||
controls.style.gap = '8px';
|
||||
|
||||
controls.innerHTML = `
|
||||
<div class="text-muted small text-center mb-1"><i class="fas fa-cog"></i> Image Controls</div>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm w-100" id="btn-upload-img-${i}"><i class="fas fa-upload"></i> Upload Image</button>
|
||||
<button type="button" class="btn btn-outline-info btn-sm w-100" id="btn-fetch-url-${i}"><i class="fas fa-link"></i> Fetch URL</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm w-100" id="btn-media-lib-${i}"><i class="fas fa-images"></i> Media Library</button>
|
||||
<button type="button" class="btn btn-outline-danger btn-sm w-100 mt-2" id="btn-clear-img-${i}"><i class="fas fa-trash"></i> Remove Image</button>
|
||||
`;
|
||||
|
||||
const uploadBtn = controls.querySelector(`#btn-upload-img-${i}`);
|
||||
if (uploadBtn && fileBtn) {
|
||||
uploadBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
fileBtn.click();
|
||||
});
|
||||
}
|
||||
|
||||
const urlBtn = controls.querySelector(`#btn-fetch-url-${i}`);
|
||||
if (urlBtn) {
|
||||
urlBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const url = prompt('Enter Image URL:');
|
||||
if (url && url.trim() !== '') {
|
||||
this.data[`col${i}`] = {
|
||||
file: { url: url.trim() },
|
||||
caption: '',
|
||||
withBorder: false,
|
||||
withBackground: false,
|
||||
stretched: false
|
||||
};
|
||||
this._renderTypeSpecificInput(i, container);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const mediaBtn = controls.querySelector(`#btn-media-lib-${i}`);
|
||||
if (mediaBtn) {
|
||||
mediaBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (window.SISMediaPicker) {
|
||||
SISMediaPicker.open((url, mediaObj, config) => {
|
||||
if (url) {
|
||||
this.data[`col${i}`] = {
|
||||
file: { url: url },
|
||||
caption: (config && config.alt) || '',
|
||||
withBorder: false,
|
||||
withBackground: false,
|
||||
stretched: false
|
||||
};
|
||||
if (config) {
|
||||
this.data[`imgStyle${i}`] = config.style || '';
|
||||
this.data[`imgClass${i}`] = config.cssClass || '';
|
||||
this.data[`imgAlt${i}`] = config.alt || '';
|
||||
this.data[`imgAttrs${i}`] = config.customAttributes || '';
|
||||
this.data[`imgAspectRatio${i}`] = config.aspectRatio || '';
|
||||
}
|
||||
this._renderTypeSpecificInput(i, container);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
alert('Media Library is not yet configured on this system.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const clearBtn = controls.querySelector(`#btn-clear-img-${i}`);
|
||||
if (clearBtn) {
|
||||
clearBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (confirm('Are you sure you want to remove this image?')) {
|
||||
this.data[`col${i}`] = '';
|
||||
this._renderTypeSpecificInput(i, container);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
element.appendChild(controls);
|
||||
};
|
||||
injectCustomButtons();
|
||||
|
||||
const handleImageError = () => {
|
||||
element.classList.remove('image-tool--loading');
|
||||
element.classList.add('image-tool--empty');
|
||||
const preloader = element.querySelector('.image-tool__image-preloader');
|
||||
if (preloader) preloader.style.display = 'none';
|
||||
};
|
||||
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
mutations.forEach(mutation => {
|
||||
mutation.addedNodes.forEach(node => {
|
||||
if (node.tagName === 'IMG') {
|
||||
node.addEventListener('error', handleImageError);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
observer.observe(element, { childList: true, subtree: true });
|
||||
|
||||
const existingImg = element.querySelector('img');
|
||||
if (existingImg) existingImg.addEventListener('error', handleImageError);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Failed to render plugin ${type} inside GridTool:`, err);
|
||||
typeContainer.innerHTML = `<div class="alert alert-danger small">Error loading plugin ${type}</div>`;
|
||||
}
|
||||
} else if (type === 'html') {
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.className = 'form-control form-control-sm';
|
||||
textarea.style.minHeight = '140px';
|
||||
textarea.style.fontSize = '12px';
|
||||
textarea.placeholder = `HTML / Text for Column ${i}...`;
|
||||
textarea.value = typeof this.data[`col${i}`] === 'object' ? JSON.stringify(this.data[`col${i}`]) : this.data[`col${i}`] || '';
|
||||
if (this.readOnly) textarea.disabled = true;
|
||||
|
||||
textarea.addEventListener('input', (e) => {
|
||||
this.data[`col${i}`] = e.target.value;
|
||||
});
|
||||
typeContainer.appendChild(textarea);
|
||||
|
||||
} else if (type === 'image') {
|
||||
const inputGroup = document.createElement('div');
|
||||
inputGroup.className = 'input-group input-group-sm mb-2';
|
||||
|
||||
const input = document.createElement('input');
|
||||
input.type = 'text';
|
||||
input.className = 'form-control form-control-sm';
|
||||
input.placeholder = 'Paste image URL here...';
|
||||
input.value = this.data[`imgUrl${i}`] || '';
|
||||
if (this.readOnly) input.disabled = true;
|
||||
|
||||
const appendDiv = document.createElement('div');
|
||||
appendDiv.className = 'input-group-append';
|
||||
|
||||
const chooseBtn = document.createElement('button');
|
||||
chooseBtn.type = 'button';
|
||||
chooseBtn.className = 'btn btn-outline-secondary';
|
||||
chooseBtn.innerText = 'Choose';
|
||||
if (this.readOnly) chooseBtn.disabled = true;
|
||||
chooseBtn.addEventListener('click', () => {
|
||||
if (window.SISMediaPicker) {
|
||||
SISMediaPicker.open((selectedUrl, mediaObj, config) => {
|
||||
input.value = selectedUrl;
|
||||
this.data[`imgUrl${i}`] = selectedUrl;
|
||||
if (config) {
|
||||
this.data[`imgStyle${i}`] = config.style || '';
|
||||
this.data[`imgClass${i}`] = config.cssClass || '';
|
||||
this.data[`imgAlt${i}`] = config.alt || '';
|
||||
this.data[`imgAttrs${i}`] = config.customAttributes || '';
|
||||
this.data[`imgAspectRatio${i}`] = config.aspectRatio || '';
|
||||
}
|
||||
previewImg.src = selectedUrl;
|
||||
previewBox.style.display = 'block';
|
||||
});
|
||||
} else {
|
||||
alert('Media Picker modal helper is not loaded.');
|
||||
}
|
||||
});
|
||||
|
||||
const uploadBtn = document.createElement('button');
|
||||
uploadBtn.type = 'button';
|
||||
uploadBtn.className = 'btn btn-outline-primary';
|
||||
uploadBtn.innerText = 'Upload';
|
||||
if (this.readOnly) uploadBtn.disabled = true;
|
||||
|
||||
const fileInput = document.createElement('input');
|
||||
fileInput.type = 'file';
|
||||
fileInput.accept = 'image/*';
|
||||
fileInput.style.display = 'none';
|
||||
|
||||
uploadBtn.addEventListener('click', () => fileInput.click());
|
||||
fileInput.addEventListener('change', async (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('image', file);
|
||||
|
||||
uploadBtn.disabled = true;
|
||||
uploadBtn.innerText = '...';
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/manage/media/upload', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
const result = await response.json();
|
||||
if (result.success && result.file && result.file.url) {
|
||||
const url = result.file.url;
|
||||
input.value = url;
|
||||
this.data[`imgUrl${i}`] = url;
|
||||
previewImg.src = url;
|
||||
previewBox.style.display = 'block';
|
||||
} else {
|
||||
alert('Upload failed: ' + (result.message || 'Unknown error'));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert('Upload error occurred.');
|
||||
} finally {
|
||||
uploadBtn.disabled = false;
|
||||
uploadBtn.innerText = 'Upload';
|
||||
}
|
||||
});
|
||||
|
||||
appendDiv.appendChild(chooseBtn);
|
||||
appendDiv.appendChild(uploadBtn);
|
||||
inputGroup.appendChild(input);
|
||||
inputGroup.appendChild(appendDiv);
|
||||
typeContainer.appendChild(inputGroup);
|
||||
typeContainer.appendChild(fileInput);
|
||||
|
||||
const previewBox = document.createElement('div');
|
||||
previewBox.className = 'border rounded p-1 text-center bg-light';
|
||||
previewBox.style.display = this.data[`imgUrl${i}`] ? 'block' : 'none';
|
||||
previewBox.style.maxHeight = '150px';
|
||||
previewBox.style.overflow = 'hidden';
|
||||
|
||||
const previewImg = document.createElement('img');
|
||||
previewImg.src = this.data[`imgUrl${i}`] || '';
|
||||
previewImg.style.maxWidth = '100%';
|
||||
previewImg.style.maxHeight = '140px';
|
||||
previewImg.style.objectFit = 'contain';
|
||||
|
||||
previewBox.appendChild(previewImg);
|
||||
typeContainer.appendChild(previewBox);
|
||||
|
||||
input.addEventListener('input', (e) => {
|
||||
const val = e.target.value.trim();
|
||||
this.data[`imgUrl${i}`] = val;
|
||||
if (val) {
|
||||
previewImg.src = val;
|
||||
previewBox.style.display = 'block';
|
||||
} else {
|
||||
previewBox.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
} else if (type === 'youtube') {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'text';
|
||||
input.className = 'form-control form-control-sm mb-2';
|
||||
input.placeholder = 'Paste YouTube video URL...';
|
||||
input.value = this.data[`ytUrl${i}`] || '';
|
||||
if (this.readOnly) input.disabled = true;
|
||||
|
||||
const preview = document.createElement('div');
|
||||
preview.className = 'small text-muted p-2 bg-light border rounded';
|
||||
preview.innerText = 'Format: https://www.youtube.com/watch?v=XXXXXX';
|
||||
|
||||
input.addEventListener('input', (e) => {
|
||||
this.data[`ytUrl${i}`] = e.target.value.trim();
|
||||
});
|
||||
|
||||
typeContainer.appendChild(input);
|
||||
typeContainer.appendChild(preview);
|
||||
|
||||
} else if (type === 'accordion') {
|
||||
const titleInput = document.createElement('input');
|
||||
titleInput.type = 'text';
|
||||
titleInput.className = 'form-control form-control-sm mb-2';
|
||||
titleInput.placeholder = 'Accordion Title...';
|
||||
titleInput.value = this.data[`accTitle${i}`] || '';
|
||||
if (this.readOnly) titleInput.disabled = true;
|
||||
|
||||
const contentTextarea = document.createElement('textarea');
|
||||
contentTextarea.className = 'form-control form-control-sm';
|
||||
contentTextarea.style.minHeight = '90px';
|
||||
contentTextarea.style.fontSize = '12px';
|
||||
contentTextarea.placeholder = 'Accordion Body HTML...';
|
||||
contentTextarea.value = this.data[`accContent${i}`] || '';
|
||||
if (this.readOnly) contentTextarea.disabled = true;
|
||||
|
||||
titleInput.addEventListener('input', (e) => {
|
||||
this.data[`accTitle${i}`] = e.target.value;
|
||||
});
|
||||
contentTextarea.addEventListener('input', (e) => {
|
||||
this.data[`accContent${i}`] = e.target.value;
|
||||
});
|
||||
|
||||
typeContainer.appendChild(titleInput);
|
||||
typeContainer.appendChild(contentTextarea);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Advanced Settings (ID and Class)
|
||||
const advancedHeader = document.createElement('div');
|
||||
advancedHeader.className = 'font-weight-bold text-muted small mb-2';
|
||||
advancedHeader.style.fontSize = '10px';
|
||||
advancedHeader.innerText = 'ADVANCED SETTINGS';
|
||||
container.appendChild(advancedHeader);
|
||||
|
||||
const advRow = document.createElement('div');
|
||||
advRow.className = 'form-row';
|
||||
|
||||
const classCol = document.createElement('div');
|
||||
classCol.className = 'col';
|
||||
const classInput = document.createElement('input');
|
||||
classInput.type = 'text';
|
||||
classInput.className = 'form-control form-control-sm';
|
||||
classInput.style.fontSize = '11px';
|
||||
classInput.placeholder = 'CSS Class(es)...';
|
||||
classInput.value = this.data[`class${i}`] || '';
|
||||
if (this.readOnly) classInput.disabled = true;
|
||||
classInput.addEventListener('input', (e) => {
|
||||
this.data[`class${i}`] = e.target.value.trim();
|
||||
});
|
||||
classCol.appendChild(classInput);
|
||||
|
||||
const idCol = document.createElement('div');
|
||||
idCol.className = 'col';
|
||||
const idInput = document.createElement('input');
|
||||
idInput.type = 'text';
|
||||
idInput.className = 'form-control form-control-sm';
|
||||
idInput.style.fontSize = '11px';
|
||||
idInput.placeholder = 'HTML ID...';
|
||||
idInput.value = this.data[`id${i}`] || '';
|
||||
if (this.readOnly) idInput.disabled = true;
|
||||
idInput.addEventListener('input', (e) => {
|
||||
this.data[`id${i}`] = e.target.value.trim();
|
||||
});
|
||||
idCol.appendChild(idInput);
|
||||
|
||||
advRow.appendChild(classCol);
|
||||
advRow.appendChild(idCol);
|
||||
container.appendChild(advRow);
|
||||
}
|
||||
|
||||
save(blockContent) {
|
||||
const savedData = {
|
||||
cols: this.data.cols,
|
||||
globalClass: this.globalClassInput ? this.globalClassInput.value.trim() : (this.data.globalClass || ''),
|
||||
globalId: this.globalIdInput ? this.globalIdInput.value.trim() : (this.data.globalId || ''),
|
||||
globalAttributes: this.globalAttrInput ? this.globalAttrInput.value.trim() : (this.data.globalAttributes || '')
|
||||
};
|
||||
for (let i = 1; i <= this.data.cols; i++) {
|
||||
const type = this.data[`type${i}`] || 'html';
|
||||
savedData[`type${i}`] = type;
|
||||
|
||||
// If it is a dynamically nested plugin, save its output data
|
||||
if (this.activeInstances[i]) {
|
||||
try {
|
||||
const pluginData = this.activeInstances[i].save();
|
||||
savedData[`col${i}`] = pluginData;
|
||||
} catch (e) {
|
||||
console.error(`Failed to save plugin ${type} inside GridTool:`, e);
|
||||
savedData[`col${i}`] = this.data[`col${i}`];
|
||||
}
|
||||
} else {
|
||||
savedData[`col${i}`] = this.data[`col${i}`] || '';
|
||||
}
|
||||
|
||||
savedData[`imgUrl${i}`] = this.data[`imgUrl${i}`] || '';
|
||||
savedData[`ytUrl${i}`] = this.data[`ytUrl${i}`] || '';
|
||||
savedData[`accTitle${i}`] = this.data[`accTitle${i}`] || '';
|
||||
savedData[`accContent${i}`] = this.data[`accContent${i}`] || '';
|
||||
savedData[`class${i}`] = this.data[`class${i}`] || '';
|
||||
savedData[`id${i}`] = this.data[`id${i}`] || '';
|
||||
savedData[`attr${i}`] = this.data[`attr${i}`] || '';
|
||||
savedData[`width${i}`] = this.data[`width${i}`] || 0;
|
||||
savedData[`caption${i}`] = this.data[`caption${i}`] || '';
|
||||
|
||||
// Sub-plugin serialization
|
||||
const subType = this.data[`subType${i}`] || 'none';
|
||||
savedData[`subType${i}`] = subType;
|
||||
|
||||
if (this.activeInstances[`sub_${i}`]) {
|
||||
try {
|
||||
const subPluginData = this.activeInstances[`sub_${i}`].save();
|
||||
savedData[`colsub_${i}`] = subPluginData;
|
||||
} catch (e) {
|
||||
console.error(`Failed to save sub-plugin ${subType} inside GridTool:`, e);
|
||||
savedData[`colsub_${i}`] = this.data[`colsub_${i}`];
|
||||
}
|
||||
} else {
|
||||
savedData[`colsub_${i}`] = this.data[`colsub_${i}`] || '';
|
||||
}
|
||||
|
||||
savedData[`imgUrlsub_${i}`] = this.data[`imgUrlsub_${i}`] || '';
|
||||
savedData[`ytUrlsub_${i}`] = this.data[`ytUrlsub_${i}`] || '';
|
||||
savedData[`accTitlesub_${i}`] = this.data[`accTitlesub_${i}`] || '';
|
||||
savedData[`accContentsub_${i}`] = this.data[`accContentsub_${i}`] || '';
|
||||
savedData[`classsub_${i}`] = this.data[`classsub_${i}`] || '';
|
||||
savedData[`idsub_${i}`] = this.data[`idsub_${i}`] || '';
|
||||
savedData[`attrsub_${i}`] = this.data[`attrsub_${i}`] || '';
|
||||
}
|
||||
return savedData;
|
||||
}
|
||||
}
|
||||
|
||||
// Register the plugin globally
|
||||
window.SISEditorPlugins = window.SISEditorPlugins || {};
|
||||
window.SISEditorPlugins['grid'] = {
|
||||
class: GridTool
|
||||
};
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
(function(){"use strict";try{if(typeof document<"u"){var e=document.createElement("style");e.appendChild(document.createTextNode(".ce-header{padding:.6em 0 3px;margin:0;line-height:1.25em;outline:none}.ce-header p,.ce-header div{padding:0!important;margin:0!important}")),document.head.appendChild(e)}}catch(n){console.error("vite-plugin-css-injected-by-js",n)}})();
|
||||
(function(n,s){typeof exports=="object"&&typeof module<"u"?module.exports=s():typeof define=="function"&&define.amd?define(s):(n=typeof globalThis<"u"?globalThis:n||self,n.Header=s())})(this,function(){"use strict";const n="",s='<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M6 7L6 12M6 17L6 12M6 12L12 12M12 7V12M12 17L12 12"/><path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M19 17V10.2135C19 10.1287 18.9011 10.0824 18.836 10.1367L16 12.5"/></svg>',a='<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M6 7L6 12M6 17L6 12M6 12L12 12M12 7V12M12 17L12 12"/><path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M16 11C16 10 19 9.5 19 12C19 13.9771 16.0684 13.9997 16.0012 16.8981C15.9999 16.9533 16.0448 17 16.1 17L19.3 17"/></svg>',h='<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M6 7L6 12M6 17L6 12M6 12L12 12M12 7V12M12 17L12 12"/><path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M16 11C16 10.5 16.8323 10 17.6 10C18.3677 10 19.5 10.311 19.5 11.5C19.5 12.5315 18.7474 12.9022 18.548 12.9823C18.5378 12.9864 18.5395 13.0047 18.5503 13.0063C18.8115 13.0456 20 13.3065 20 14.8C20 16 19.5 17 17.8 17C17.8 17 16 17 16 16.3"/></svg>',d='<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M6 7L6 12M6 17L6 12M6 12L12 12M12 7V12M12 17L12 12"/><path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M18 10L15.2834 14.8511C15.246 14.9178 15.294 15 15.3704 15C16.8489 15 18.7561 15 20.2 15M19 17C19 15.7187 19 14.8813 19 13.6"/></svg>',u='<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M6 7L6 12M6 17L6 12M6 12L12 12M12 7V12M12 17L12 12"/><path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M16 15.9C16 15.9 16.3768 17 17.8 17C19.5 17 20 15.6199 20 14.7C20 12.7323 17.6745 12.0486 16.1635 12.9894C16.094 13.0327 16 12.9846 16 12.9027V10.1C16 10.0448 16.0448 10 16.1 10H19.8"/></svg>',g='<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M6 7L6 12M6 17L6 12M6 12L12 12M12 7V12M12 17L12 12"/><path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M19.5 10C16.5 10.5 16 13.3285 16 15M16 15V15C16 16.1046 16.8954 17 18 17H18.3246C19.3251 17 20.3191 16.3492 20.2522 15.3509C20.0612 12.4958 16 12.6611 16 15Z"/></svg>',c='<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M9 7L9 12M9 17V12M9 12L15 12M15 7V12M15 17L15 12"/></svg>';/**
|
||||
* Header block for the Editor.js.
|
||||
*
|
||||
* @author CodeX (team@ifmo.su)
|
||||
* @copyright CodeX 2018
|
||||
* @license MIT
|
||||
* @version 2.0.0
|
||||
*/class v{constructor({data:e,config:t,api:i,readOnly:r}){this.api=i,this.readOnly=r,this._settings=t,this._data=this.normalizeData(e),this._element=this.getTag()}get _CSS(){return{block:this.api.styles.block,wrapper:"ce-header"}}isHeaderData(e){return e.text!==void 0}normalizeData(e){const t={text:"",level:this.defaultLevel.number};return this.isHeaderData(e)&&(t.text=e.text||"",e.level!==void 0&&!isNaN(parseInt(e.level.toString()))&&(t.level=parseInt(e.level.toString()))),t}render(){return this._element}renderSettings(){return this.levels.map(e=>({icon:e.svg,label:this.api.i18n.t(`Heading ${e.number}`),onActivate:()=>this.setLevel(e.number),closeOnActivate:!0,isActive:this.currentLevel.number===e.number,render:()=>document.createElement("div")}))}setLevel(e){this.data={level:e,text:this.data.text}}merge(e){this._element.insertAdjacentHTML("beforeend",e.text)}validate(e){return e.text.trim()!==""}save(e){return{text:e.innerHTML,level:this.currentLevel.number}}static get conversionConfig(){return{export:"text",import:"text"}}static get sanitize(){return{level:!1,text:{}}}static get isReadOnlySupported(){return!0}get data(){return this._data.text=this._element.innerHTML,this._data.level=this.currentLevel.number,this._data}set data(e){if(this._data=this.normalizeData(e),e.level!==void 0&&this._element.parentNode){const t=this.getTag();t.innerHTML=this._element.innerHTML,this._element.parentNode.replaceChild(t,this._element),this._element=t}e.text!==void 0&&(this._element.innerHTML=this._data.text||"")}getTag(){const e=document.createElement(this.currentLevel.tag);return e.innerHTML=this._data.text||"",e.classList.add(this._CSS.wrapper),e.contentEditable=this.readOnly?"false":"true",e.dataset.placeholder=this.api.i18n.t(this._settings.placeholder||""),e}get currentLevel(){let e=this.levels.find(t=>t.number===this._data.level);return e||(e=this.defaultLevel),e}get defaultLevel(){if(this._settings.defaultLevel){const e=this.levels.find(t=>t.number===this._settings.defaultLevel);if(e)return e;console.warn("(ง'̀-'́)ง Heading Tool: the default level specified was not found in available levels")}return this.levels[1]}get levels(){const e=[{number:1,tag:"H1",svg:s},{number:2,tag:"H2",svg:a},{number:3,tag:"H3",svg:h},{number:4,tag:"H4",svg:d},{number:5,tag:"H5",svg:u},{number:6,tag:"H6",svg:g}];return this._settings.levels?e.filter(t=>this._settings.levels.includes(t.number)):e}onPaste(e){const t=e.detail;if("data"in t){const i=t.data;let r=this.defaultLevel.number;switch(i.tagName){case"H1":r=1;break;case"H2":r=2;break;case"H3":r=3;break;case"H4":r=4;break;case"H5":r=5;break;case"H6":r=6;break}this._settings.levels&&(r=this._settings.levels.reduce((o,l)=>Math.abs(l-r)<Math.abs(o-r)?l:o)),this.data={level:r,text:i.innerHTML}}}static get pasteConfig(){return{tags:["H1","H2","H3","H4","H5","H6"]}}static get toolbox(){return{icon:c,title:"Heading"}}}return v});
|
||||
BIN
Binary file not shown.
+246
@@ -0,0 +1,246 @@
|
||||
/**
|
||||
* SISHeroBannerTool — Hero Banner Plugin for Editor.js
|
||||
* Allows content managers to build dynamic hero banners with live preview.
|
||||
*/
|
||||
class SISHeroBannerTool {
|
||||
static get toolbox() {
|
||||
return {
|
||||
title: 'Hero Banner',
|
||||
icon: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="9" y1="21" x2="9" y2="9"/></svg>'
|
||||
};
|
||||
}
|
||||
|
||||
constructor({ data, api, readOnly }) {
|
||||
this.api = api;
|
||||
this.readOnly = readOnly;
|
||||
this.data = {
|
||||
title: (data && data.title) ? data.title : '',
|
||||
subtitle: (data && data.subtitle) ? data.subtitle : '',
|
||||
bgImage: (data && data.bgImage) ? data.bgImage : '',
|
||||
btnText: (data && data.btnText) ? data.btnText : '',
|
||||
btnLink: (data && data.btnLink) ? data.btnLink : '',
|
||||
height: (data && data.height) ? data.height : '350px',
|
||||
textAlign: (data && data.textAlign) ? data.textAlign : 'center',
|
||||
overlayOpacity: (data && data.overlayOpacity !== undefined) ? data.overlayOpacity : '0.4',
|
||||
stretched: (data && data.stretched !== undefined) ? !!data.stretched : true
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const container = document.createElement('div');
|
||||
container.style.border = '1px solid #e3e6f0';
|
||||
container.style.borderRadius = '8px';
|
||||
container.style.padding = '14px';
|
||||
container.style.background = '#fff';
|
||||
container.style.marginBottom = '12px';
|
||||
|
||||
const headerLabel = document.createElement('label');
|
||||
headerLabel.className = 'font-weight-bold text-primary small mb-2 d-block';
|
||||
headerLabel.innerHTML = '<i class="fas fa-image"></i> Hero Banner Block Settings';
|
||||
container.appendChild(headerLabel);
|
||||
|
||||
// Stretch toggle
|
||||
const stretchWrapper = document.createElement('div');
|
||||
stretchWrapper.className = 'custom-control custom-switch mb-3';
|
||||
const stretchCheck = document.createElement('input');
|
||||
stretchCheck.type = 'checkbox';
|
||||
stretchCheck.className = 'custom-control-input';
|
||||
stretchCheck.id = 'hero_stretch_' + Math.random().toString(36).substring(7);
|
||||
stretchCheck.checked = !!this.data.stretched;
|
||||
|
||||
const stretchLabel = document.createElement('label');
|
||||
stretchLabel.className = 'custom-control-label small font-weight-bold text-secondary';
|
||||
stretchLabel.htmlFor = stretchCheck.id;
|
||||
stretchLabel.innerHTML = '<i class="fas fa-arrows-alt-h"></i> Stretch Banner to Full Screen Width';
|
||||
|
||||
stretchCheck.addEventListener('change', () => {
|
||||
this.data.stretched = stretchCheck.checked;
|
||||
});
|
||||
stretchWrapper.appendChild(stretchCheck);
|
||||
stretchWrapper.appendChild(stretchLabel);
|
||||
container.appendChild(stretchWrapper);
|
||||
|
||||
// Inputs
|
||||
this.titleInput = this._createInput('Banner Title', 'e.g. Bệnh Viện Đa Khoa Quốc Tế S.I.S Cần Thơ', this.data.title);
|
||||
this.subtitleInput = this._createInput('Subtitle / Description', 'e.g. Trao niềm tin - Nhận sức khỏe...', this.data.subtitle);
|
||||
this.bgImageInput = this._createImageInput('Background Image URL', 'https://example.com/hero-banner.jpg', this.data.bgImage);
|
||||
|
||||
// Row for Height & Button Link
|
||||
const configRow = document.createElement('div');
|
||||
configRow.className = 'form-row';
|
||||
|
||||
const heightCol = document.createElement('div');
|
||||
heightCol.className = 'col-md-4 mb-2';
|
||||
this.heightInput = this._createInput('Banner Height (e.g. 350px, 500px, 60vh)', 'e.g. 350px', this.data.height);
|
||||
heightCol.appendChild(this.heightInput);
|
||||
|
||||
const btnCol1 = document.createElement('div');
|
||||
btnCol1.className = 'col-md-4 mb-2';
|
||||
this.btnTextInput = this._createInput('Button Text (Optional)', 'e.g. Đặt Lịch Khám', this.data.btnText);
|
||||
btnCol1.appendChild(this.btnTextInput);
|
||||
|
||||
const btnCol2 = document.createElement('div');
|
||||
btnCol2.className = 'col-md-4 mb-2';
|
||||
this.btnLinkInput = this._createInput('Button Link URL (Optional)', 'e.g. /dat-lich', this.data.btnLink);
|
||||
btnCol2.appendChild(this.btnLinkInput);
|
||||
|
||||
configRow.appendChild(heightCol);
|
||||
configRow.appendChild(btnCol1);
|
||||
configRow.appendChild(btnCol2);
|
||||
|
||||
// Live Preview Box
|
||||
const previewBox = document.createElement('div');
|
||||
previewBox.className = 'hero-banner-preview p-4 rounded text-white my-2';
|
||||
previewBox.style.position = 'relative';
|
||||
previewBox.style.minHeight = this.data.height || '350px';
|
||||
previewBox.style.display = 'flex';
|
||||
previewBox.style.flexDirection = 'column';
|
||||
previewBox.style.justifyContent = 'center';
|
||||
previewBox.style.alignItems = 'center';
|
||||
previewBox.style.textAlign = 'center';
|
||||
previewBox.style.backgroundSize = 'cover';
|
||||
previewBox.style.backgroundPosition = 'center';
|
||||
previewBox.style.overflow = 'hidden';
|
||||
|
||||
const overlay = document.createElement('div');
|
||||
overlay.style.position = 'absolute';
|
||||
overlay.style.top = '0';
|
||||
overlay.style.left = '0';
|
||||
overlay.style.right = '0';
|
||||
overlay.style.bottom = '0';
|
||||
overlay.style.background = '#000';
|
||||
overlay.style.zIndex = '1';
|
||||
previewBox.appendChild(overlay);
|
||||
|
||||
const contentBox = document.createElement('div');
|
||||
contentBox.style.position = 'relative';
|
||||
contentBox.style.zIndex = '2';
|
||||
previewBox.appendChild(contentBox);
|
||||
|
||||
const updatePreview = () => {
|
||||
const bg = this.bgImageInput.querySelector('input').value.trim();
|
||||
const title = this.titleInput.querySelector('input').value.trim() || 'Hero Banner Title';
|
||||
const sub = this.subtitleInput.querySelector('input').value.trim();
|
||||
const btnT = this.btnTextInput.querySelector('input').value.trim();
|
||||
const bannerH = this.heightInput.querySelector('input').value.trim() || '350px';
|
||||
|
||||
previewBox.style.minHeight = bannerH;
|
||||
previewBox.style.backgroundImage = bg ? 'url("' + bg + '")' : 'linear-gradient(135deg, #002554, #881C1C)';
|
||||
overlay.style.opacity = this.data.overlayOpacity || '0.4';
|
||||
|
||||
let html = '<h4 class="font-weight-bold mb-1 text-white">' + title + '</h4>';
|
||||
if (sub) html += '<p class="small mb-2 text-light">' + sub + '</p>';
|
||||
if (btnT) html += '<span class="btn btn-sm btn-danger font-weight-bold px-3">' + btnT + '</span>';
|
||||
contentBox.innerHTML = html;
|
||||
};
|
||||
|
||||
[this.titleInput, this.subtitleInput, this.bgImageInput].forEach(wrapper => {
|
||||
const input = wrapper.querySelector('input');
|
||||
input.addEventListener('input', updatePreview);
|
||||
if (this.readOnly) input.disabled = true;
|
||||
container.appendChild(wrapper);
|
||||
});
|
||||
|
||||
[this.heightInput, this.btnTextInput, this.btnLinkInput].forEach(wrapper => {
|
||||
const input = wrapper.querySelector('input');
|
||||
input.addEventListener('input', updatePreview);
|
||||
if (this.readOnly) input.disabled = true;
|
||||
});
|
||||
|
||||
container.appendChild(configRow);
|
||||
container.appendChild(previewBox);
|
||||
updatePreview();
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
_createInput(labelText, placeholder, value) {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'form-group mb-2';
|
||||
const lbl = document.createElement('label');
|
||||
lbl.className = 'small font-weight-bold text-secondary mb-1 d-block';
|
||||
lbl.innerText = labelText;
|
||||
const inp = document.createElement('input');
|
||||
inp.type = 'text';
|
||||
inp.className = 'form-control form-control-sm';
|
||||
inp.placeholder = placeholder;
|
||||
inp.value = value || '';
|
||||
wrapper.appendChild(lbl);
|
||||
wrapper.appendChild(inp);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
_createImageInput(labelText, placeholder, value) {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'form-group mb-2';
|
||||
|
||||
const lbl = document.createElement('label');
|
||||
lbl.className = 'small font-weight-bold text-secondary mb-1 d-block';
|
||||
lbl.innerText = labelText;
|
||||
wrapper.appendChild(lbl);
|
||||
|
||||
const inputGroup = document.createElement('div');
|
||||
inputGroup.className = 'input-group input-group-sm';
|
||||
|
||||
const inp = document.createElement('input');
|
||||
inp.type = 'text';
|
||||
inp.className = 'form-control';
|
||||
inp.placeholder = placeholder;
|
||||
inp.value = value || '';
|
||||
if (this.readOnly) inp.disabled = true;
|
||||
inputGroup.appendChild(inp);
|
||||
|
||||
if (!this.readOnly) {
|
||||
const appendDiv = document.createElement('div');
|
||||
appendDiv.className = 'input-group-append';
|
||||
|
||||
const mediaBtn = document.createElement('button');
|
||||
mediaBtn.type = 'button';
|
||||
mediaBtn.className = 'btn btn-outline-info';
|
||||
mediaBtn.innerHTML = '<i class="fas fa-images"></i> Media Library';
|
||||
mediaBtn.addEventListener('click', () => {
|
||||
if (window.SISMediaPicker) {
|
||||
SISMediaPicker.open((selectedUrl, mediaObj, config) => {
|
||||
inp.value = selectedUrl;
|
||||
if (config) {
|
||||
this._bgImageConfig = config;
|
||||
}
|
||||
// Dispatch input event to trigger preview update
|
||||
inp.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
});
|
||||
} else {
|
||||
alert('Media modal function openSISMediaModal is not available.');
|
||||
}
|
||||
});
|
||||
appendDiv.appendChild(mediaBtn);
|
||||
inputGroup.appendChild(appendDiv);
|
||||
}
|
||||
|
||||
wrapper.appendChild(inputGroup);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
save(blockContent) {
|
||||
const stretchCheck = blockContent.querySelector('.custom-control-input');
|
||||
return {
|
||||
title: this.titleInput ? this.titleInput.querySelector('input').value : this.data.title,
|
||||
subtitle: this.subtitleInput ? this.subtitleInput.querySelector('input').value : this.data.subtitle,
|
||||
bgImage: this.bgImageInput ? this.bgImageInput.querySelector('input').value : this.data.bgImage,
|
||||
bgImageStyle: (this._bgImageConfig && this._bgImageConfig.style) || this.data.bgImageStyle || '',
|
||||
bgImageClass: (this._bgImageConfig && this._bgImageConfig.cssClass) || this.data.bgImageClass || '',
|
||||
bgImageAlt: (this._bgImageConfig && this._bgImageConfig.alt) || this.data.bgImageAlt || '',
|
||||
bgImageAttrs: (this._bgImageConfig && this._bgImageConfig.customAttributes) || this.data.bgImageAttrs || '',
|
||||
bgImageAspectRatio: (this._bgImageConfig && this._bgImageConfig.aspectRatio) || this.data.bgImageAspectRatio || '',
|
||||
btnText: this.btnTextInput ? this.btnTextInput.querySelector('input').value : this.data.btnText,
|
||||
btnLink: this.btnLinkInput ? this.btnLinkInput.querySelector('input').value : this.data.btnLink,
|
||||
height: this.heightInput ? this.heightInput.querySelector('input').value : this.data.height,
|
||||
textAlign: this.data.textAlign || 'center',
|
||||
overlayOpacity: this.data.overlayOpacity || '0.4',
|
||||
stretched: stretchCheck ? stretchCheck.checked : !!this.data.stretched
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Register plugin globally
|
||||
window.SISEditorPlugins = window.SISEditorPlugins || {};
|
||||
window.SISEditorPlugins['hero'] = { class: SISHeroBannerTool };
|
||||
+473
@@ -0,0 +1,473 @@
|
||||
/**
|
||||
* SISHoverCardTool — Dedicated Hover Cards Block Tool for Editor.js
|
||||
* Allows content managers to freely add, remove, reorder, and configure interactive hover cards.
|
||||
*/
|
||||
class SISHoverCardTool {
|
||||
static get toolbox() {
|
||||
return {
|
||||
title: 'Hover Cards',
|
||||
icon: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7" rx="1"></rect><rect x="14" y="3" width="7" height="7" rx="1"></rect><rect x="14" y="14" width="7" height="7" rx="1"></rect><rect x="3" y="14" width="7" height="7" rx="1"></rect></svg>'
|
||||
};
|
||||
}
|
||||
|
||||
constructor({ data, api, readOnly }) {
|
||||
this.api = api;
|
||||
this.readOnly = readOnly;
|
||||
this.instanceId = 'hc_' + Math.random().toString(36).substring(7);
|
||||
this.data = {
|
||||
cols: parseInt(data && data.cols) || 4,
|
||||
styleVariant: (data && data.styleVariant) || 'v1',
|
||||
globalClass: (data && data.globalClass) || 'sis-hover-card-section',
|
||||
globalId: (data && data.globalId) || '',
|
||||
globalAttributes: (data && data.globalAttributes) || (data && data.attributes) || '',
|
||||
items: (data && data.items && Array.isArray(data.items)) ? data.items : []
|
||||
};
|
||||
|
||||
// Add initial default item if list is empty
|
||||
if (this.data.items.length === 0) {
|
||||
this.data.items.push({
|
||||
title: 'Card Title',
|
||||
description: 'Card description text...',
|
||||
imageUrl: '',
|
||||
active: true
|
||||
});
|
||||
}
|
||||
this.wrapper = undefined;
|
||||
}
|
||||
|
||||
render() {
|
||||
this.wrapper = document.createElement('div');
|
||||
this.wrapper.className = 'p-3 bg-light border rounded mb-3 ce-hovercard-tool-wrapper';
|
||||
this.wrapper.style.fontFamily = 'inherit';
|
||||
|
||||
const titleLabel = document.createElement('div');
|
||||
titleLabel.className = 'font-weight-bold text-primary small mb-3';
|
||||
titleLabel.innerHTML = '<i class="fas fa-th-large"></i> Dedicated Hover Cards Settings';
|
||||
this.wrapper.appendChild(titleLabel);
|
||||
|
||||
// Global Settings Row
|
||||
const settingsRow = document.createElement('div');
|
||||
settingsRow.className = 'form-row mb-3 pb-2 border-bottom';
|
||||
|
||||
// 1. Cols selector
|
||||
const colDiv = document.createElement('div');
|
||||
colDiv.className = 'col-md-3 mb-2';
|
||||
colDiv.innerHTML = '<label class="small font-weight-bold text-secondary mb-1">Columns per Row</label>';
|
||||
const colSelect = document.createElement('select');
|
||||
colSelect.className = 'form-control form-control-sm';
|
||||
[1, 2, 3, 4, 6].forEach(num => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = num;
|
||||
opt.textContent = `${num} Column${num > 1 ? 's' : ''}`;
|
||||
if (num === this.data.cols) opt.selected = true;
|
||||
colSelect.appendChild(opt);
|
||||
});
|
||||
if (this.readOnly) colSelect.disabled = true;
|
||||
colSelect.addEventListener('change', (e) => {
|
||||
this.data.cols = parseInt(e.target.value) || 4;
|
||||
});
|
||||
this.colsSelect = colSelect;
|
||||
colDiv.appendChild(colSelect);
|
||||
settingsRow.appendChild(colDiv);
|
||||
|
||||
// 2. Style Variant Selector (Hover 1 vs Hover 2)
|
||||
const variantDiv = document.createElement('div');
|
||||
variantDiv.className = 'col-md-3 mb-2';
|
||||
variantDiv.innerHTML = '<label class="small font-weight-bold text-primary mb-1"><i class="fas fa-palette"></i> Card Style Variant</label>';
|
||||
const variantSelect = document.createElement('select');
|
||||
variantSelect.className = 'form-control form-control-sm';
|
||||
|
||||
const optV1 = document.createElement('option');
|
||||
optV1.value = 'v1';
|
||||
optV1.textContent = 'Hover 1 (Standard Light)';
|
||||
if (this.data.styleVariant === 'v1') optV1.selected = true;
|
||||
variantSelect.appendChild(optV1);
|
||||
|
||||
const optV2 = document.createElement('option');
|
||||
optV2.value = 'v2';
|
||||
optV2.textContent = 'Hover 2 (Modern Blue & Glow)';
|
||||
if (this.data.styleVariant === 'v2') optV2.selected = true;
|
||||
variantSelect.appendChild(optV2);
|
||||
|
||||
if (this.readOnly) variantSelect.disabled = true;
|
||||
variantSelect.addEventListener('change', (e) => {
|
||||
this.data.styleVariant = e.target.value;
|
||||
});
|
||||
this.variantSelect = variantSelect;
|
||||
variantDiv.appendChild(variantSelect);
|
||||
settingsRow.appendChild(variantDiv);
|
||||
|
||||
// 2. Global CSS Class
|
||||
const classDiv = document.createElement('div');
|
||||
classDiv.className = 'col-md-3 mb-2';
|
||||
classDiv.innerHTML = '<label class="small font-weight-bold text-secondary mb-1">Global CSS Class(es)</label>';
|
||||
const classInput = document.createElement('input');
|
||||
classInput.type = 'text';
|
||||
classInput.className = 'form-control form-control-sm';
|
||||
classInput.placeholder = 'e.g. sis-hover-card-section';
|
||||
classInput.value = this.data.globalClass || '';
|
||||
if (this.readOnly) classInput.disabled = true;
|
||||
classInput.addEventListener('input', (e) => this.data.globalClass = e.target.value.trim());
|
||||
this.globalClassInput = classInput;
|
||||
classDiv.appendChild(classInput);
|
||||
settingsRow.appendChild(classDiv);
|
||||
|
||||
// 3. Global HTML ID
|
||||
const idDiv = document.createElement('div');
|
||||
idDiv.className = 'col-md-3 mb-2';
|
||||
idDiv.innerHTML = '<label class="small font-weight-bold text-secondary mb-1">Global HTML ID</label>';
|
||||
const idInput = document.createElement('input');
|
||||
idInput.type = 'text';
|
||||
idInput.className = 'form-control form-control-sm';
|
||||
idInput.placeholder = 'e.g. hover-cards-section';
|
||||
idInput.value = this.data.globalId || '';
|
||||
if (this.readOnly) idInput.disabled = true;
|
||||
idInput.addEventListener('input', (e) => this.data.globalId = e.target.value.trim());
|
||||
this.globalIdInput = idInput;
|
||||
idDiv.appendChild(idInput);
|
||||
settingsRow.appendChild(idDiv);
|
||||
|
||||
// 4. Global Custom Attributes
|
||||
const attrDiv = document.createElement('div');
|
||||
attrDiv.className = 'col-md-3 mb-2';
|
||||
attrDiv.innerHTML = '<label class="small font-weight-bold text-secondary mb-1"><i class="fas fa-sliders-h"></i> Custom Attributes</label>';
|
||||
const attrInput = document.createElement('input');
|
||||
attrInput.type = 'text';
|
||||
attrInput.className = 'form-control form-control-sm';
|
||||
attrInput.placeholder = 'e.g. data-aos="fade-up" style="..."';
|
||||
attrInput.value = this.data.globalAttributes || '';
|
||||
if (this.readOnly) attrInput.disabled = true;
|
||||
attrInput.addEventListener('input', (e) => this.data.globalAttributes = e.target.value.trim());
|
||||
this.globalAttrInput = attrInput;
|
||||
attrDiv.appendChild(attrInput);
|
||||
settingsRow.appendChild(attrDiv);
|
||||
|
||||
this.wrapper.appendChild(settingsRow);
|
||||
|
||||
// Cards items container
|
||||
this.itemsContainer = document.createElement('div');
|
||||
this.wrapper.appendChild(this.itemsContainer);
|
||||
|
||||
this._renderAllItems();
|
||||
|
||||
if (!this.readOnly) {
|
||||
const addButton = document.createElement('button');
|
||||
addButton.type = 'button';
|
||||
addButton.className = 'btn btn-sm btn-success mt-2';
|
||||
addButton.innerHTML = '<i class="fas fa-plus"></i> Add Hover Card';
|
||||
addButton.addEventListener('click', () => {
|
||||
this.data.items.push({
|
||||
title: 'New Card Title',
|
||||
description: 'New card description text...',
|
||||
imageUrl: '',
|
||||
active: false
|
||||
});
|
||||
this._renderAllItems();
|
||||
});
|
||||
this.wrapper.appendChild(addButton);
|
||||
}
|
||||
|
||||
return this.wrapper;
|
||||
}
|
||||
|
||||
_renderAllItems() {
|
||||
this.itemsContainer.innerHTML = '';
|
||||
this.data.items.forEach((item, index) => {
|
||||
this._renderItem(item, index);
|
||||
});
|
||||
}
|
||||
|
||||
_renderItem(item, index) {
|
||||
const itemBox = document.createElement('div');
|
||||
itemBox.className = 'mb-3 p-3 bg-white border rounded shadow-sm';
|
||||
itemBox.style.position = 'relative';
|
||||
|
||||
// Top Control Action buttons (Move Up, Move Down, Delete)
|
||||
if (!this.readOnly) {
|
||||
const actionGroup = document.createElement('div');
|
||||
actionGroup.style.position = 'absolute';
|
||||
actionGroup.style.top = '10px';
|
||||
actionGroup.style.right = '10px';
|
||||
actionGroup.className = 'btn-group btn-group-sm';
|
||||
|
||||
if (index > 0) {
|
||||
const upBtn = document.createElement('button');
|
||||
upBtn.type = 'button';
|
||||
upBtn.className = 'btn btn-outline-secondary';
|
||||
upBtn.innerHTML = '<i class="fas fa-arrow-up"></i>';
|
||||
upBtn.title = 'Move Up';
|
||||
upBtn.addEventListener('click', () => {
|
||||
const temp = this.data.items[index];
|
||||
this.data.items[index] = this.data.items[index - 1];
|
||||
this.data.items[index - 1] = temp;
|
||||
this._renderAllItems();
|
||||
});
|
||||
actionGroup.appendChild(upBtn);
|
||||
}
|
||||
|
||||
if (index < this.data.items.length - 1) {
|
||||
const downBtn = document.createElement('button');
|
||||
downBtn.type = 'button';
|
||||
downBtn.className = 'btn btn-outline-secondary';
|
||||
downBtn.innerHTML = '<i class="fas fa-arrow-down"></i>';
|
||||
downBtn.title = 'Move Down';
|
||||
downBtn.addEventListener('click', () => {
|
||||
const temp = this.data.items[index];
|
||||
this.data.items[index] = this.data.items[index + 1];
|
||||
this.data.items[index + 1] = temp;
|
||||
this._renderAllItems();
|
||||
});
|
||||
actionGroup.appendChild(downBtn);
|
||||
}
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.type = 'button';
|
||||
removeBtn.className = 'btn btn-danger';
|
||||
removeBtn.innerHTML = '<i class="fas fa-trash-alt"></i>';
|
||||
removeBtn.title = 'Remove Card';
|
||||
removeBtn.addEventListener('click', () => {
|
||||
this.data.items.splice(index, 1);
|
||||
this._renderAllItems();
|
||||
});
|
||||
actionGroup.appendChild(removeBtn);
|
||||
|
||||
itemBox.appendChild(actionGroup);
|
||||
}
|
||||
|
||||
// Header Label
|
||||
const itemHeader = document.createElement('h6');
|
||||
itemHeader.className = 'font-weight-bold text-dark mb-3';
|
||||
itemHeader.innerHTML = `<i class="fas fa-id-card text-info mr-1"></i> Card #${index + 1}`;
|
||||
itemBox.appendChild(itemHeader);
|
||||
|
||||
// 1. Card Title
|
||||
const titleGroup = document.createElement('div');
|
||||
titleGroup.className = 'form-group mb-2';
|
||||
titleGroup.innerHTML = '<label class="small font-weight-bold text-secondary mb-1">Card Title</label>';
|
||||
const titleInput = document.createElement('input');
|
||||
titleInput.type = 'text';
|
||||
titleInput.className = 'form-control form-control-sm';
|
||||
titleInput.placeholder = 'e.g. Tiên phong';
|
||||
titleInput.value = item.title || '';
|
||||
if (this.readOnly) titleInput.disabled = true;
|
||||
titleInput.addEventListener('input', (e) => { item.title = e.target.value; });
|
||||
titleGroup.appendChild(titleInput);
|
||||
itemBox.appendChild(titleGroup);
|
||||
|
||||
// 2. Card Description
|
||||
const descGroup = document.createElement('div');
|
||||
descGroup.className = 'form-group mb-2';
|
||||
descGroup.innerHTML = '<label class="small font-weight-bold text-secondary mb-1">Card Description</label>';
|
||||
const descInput = document.createElement('textarea');
|
||||
descInput.className = 'form-control form-control-sm';
|
||||
descInput.rows = 2;
|
||||
descInput.placeholder = 'e.g. Phát triển và ứng dụng kỹ thuật tiên tiến...';
|
||||
descInput.value = item.description || '';
|
||||
if (this.readOnly) descInput.disabled = true;
|
||||
descInput.addEventListener('input', (e) => { item.description = e.target.value; });
|
||||
descGroup.appendChild(descInput);
|
||||
itemBox.appendChild(descGroup);
|
||||
|
||||
// 3. Image URL + Media Library Selector
|
||||
const imgGroup = document.createElement('div');
|
||||
imgGroup.className = 'form-group mb-2';
|
||||
imgGroup.innerHTML = '<label class="small font-weight-bold text-secondary mb-1">Card Icon / Image URL</label>';
|
||||
|
||||
const inputGroup = document.createElement('div');
|
||||
inputGroup.className = 'input-group input-group-sm mb-2';
|
||||
|
||||
const imgInput = document.createElement('input');
|
||||
imgInput.type = 'text';
|
||||
imgInput.className = 'form-control';
|
||||
imgInput.placeholder = '/uploads/2026/07/icon.jpg';
|
||||
imgInput.value = item.imageUrl || '';
|
||||
if (this.readOnly) imgInput.disabled = true;
|
||||
|
||||
const previewContainer = document.createElement('div');
|
||||
previewContainer.className = 'mt-1';
|
||||
const updatePreview = (url) => {
|
||||
previewContainer.innerHTML = '';
|
||||
if (url) {
|
||||
const img = document.createElement('img');
|
||||
img.src = url;
|
||||
img.style.maxHeight = '60px';
|
||||
img.style.maxWidth = '100px';
|
||||
img.style.objectFit = 'contain';
|
||||
img.className = 'border rounded p-1 bg-light';
|
||||
previewContainer.appendChild(img);
|
||||
}
|
||||
};
|
||||
updatePreview(item.imageUrl);
|
||||
|
||||
imgInput.addEventListener('input', (e) => {
|
||||
item.imageUrl = e.target.value.trim();
|
||||
updatePreview(item.imageUrl);
|
||||
});
|
||||
|
||||
inputGroup.appendChild(imgInput);
|
||||
|
||||
if (!this.readOnly) {
|
||||
const appendDiv = document.createElement('div');
|
||||
appendDiv.className = 'input-group-append';
|
||||
|
||||
const mediaBtn = document.createElement('button');
|
||||
mediaBtn.type = 'button';
|
||||
mediaBtn.className = 'btn btn-outline-info';
|
||||
mediaBtn.innerHTML = '<i class="fas fa-images"></i> Media Library';
|
||||
mediaBtn.addEventListener('click', () => {
|
||||
if (window.SISMediaPicker) {
|
||||
SISMediaPicker.open((selectedUrl, mediaObj, config) => {
|
||||
imgInput.value = selectedUrl;
|
||||
item.imageUrl = selectedUrl;
|
||||
if (config) {
|
||||
item.imageStyle = config.style || '';
|
||||
item.imageClass = config.cssClass || '';
|
||||
item.imageAlt = config.alt || '';
|
||||
item.imageAttrs = config.customAttributes || '';
|
||||
item.imageAspectRatio = config.aspectRatio || '';
|
||||
}
|
||||
updatePreview(selectedUrl);
|
||||
});
|
||||
} else {
|
||||
alert('Media modal function openSISMediaModal is not available.');
|
||||
}
|
||||
});
|
||||
appendDiv.appendChild(mediaBtn);
|
||||
inputGroup.appendChild(appendDiv);
|
||||
}
|
||||
|
||||
imgGroup.appendChild(inputGroup);
|
||||
imgGroup.appendChild(previewContainer);
|
||||
itemBox.appendChild(imgGroup);
|
||||
|
||||
// 4. Hover Icon / Image URL (Swapped on hover)
|
||||
const hoverImgGroup = document.createElement('div');
|
||||
hoverImgGroup.className = 'form-group mb-2';
|
||||
hoverImgGroup.innerHTML = '<label class="small font-weight-bold text-info mb-1"><i class="fas fa-sync-alt"></i> Hover Icon / Image URL (Optional - Swaps on Hover)</label>';
|
||||
|
||||
const hoverInputGroup = document.createElement('div');
|
||||
hoverInputGroup.className = 'input-group input-group-sm mb-2';
|
||||
|
||||
const hoverImgInput = document.createElement('input');
|
||||
hoverImgInput.type = 'text';
|
||||
hoverImgInput.className = 'form-control';
|
||||
hoverImgInput.placeholder = '/uploads/2026/07/hover-icon.jpg (optional)';
|
||||
hoverImgInput.value = item.hoverImageUrl || '';
|
||||
if (this.readOnly) hoverImgInput.disabled = true;
|
||||
|
||||
const hoverPreviewContainer = document.createElement('div');
|
||||
hoverPreviewContainer.className = 'mt-1';
|
||||
const updateHoverPreview = (url) => {
|
||||
hoverPreviewContainer.innerHTML = '';
|
||||
if (url) {
|
||||
const img = document.createElement('img');
|
||||
img.src = url;
|
||||
img.style.maxHeight = '60px';
|
||||
img.style.maxWidth = '100px';
|
||||
img.style.objectFit = 'contain';
|
||||
img.className = 'border rounded p-1 bg-info text-white';
|
||||
hoverPreviewContainer.appendChild(img);
|
||||
}
|
||||
};
|
||||
updateHoverPreview(item.hoverImageUrl);
|
||||
|
||||
hoverImgInput.addEventListener('input', (e) => {
|
||||
item.hoverImageUrl = e.target.value.trim();
|
||||
updateHoverPreview(item.hoverImageUrl);
|
||||
});
|
||||
|
||||
hoverInputGroup.appendChild(hoverImgInput);
|
||||
|
||||
if (!this.readOnly) {
|
||||
const appendDiv = document.createElement('div');
|
||||
appendDiv.className = 'input-group-append';
|
||||
|
||||
const mediaBtn = document.createElement('button');
|
||||
mediaBtn.type = 'button';
|
||||
mediaBtn.className = 'btn btn-outline-info';
|
||||
mediaBtn.innerHTML = '<i class="fas fa-images"></i> Media Library';
|
||||
mediaBtn.addEventListener('click', () => {
|
||||
if (window.SISMediaPicker) {
|
||||
SISMediaPicker.open((selectedUrl, mediaObj, config) => {
|
||||
hoverImgInput.value = selectedUrl;
|
||||
item.hoverImageUrl = selectedUrl;
|
||||
if (config) {
|
||||
item.hoverImageStyle = config.style || '';
|
||||
item.hoverImageClass = config.cssClass || '';
|
||||
item.hoverImageAlt = config.alt || '';
|
||||
item.hoverImageAttrs = config.customAttributes || '';
|
||||
item.hoverImageAspectRatio = config.aspectRatio || '';
|
||||
}
|
||||
updateHoverPreview(selectedUrl);
|
||||
});
|
||||
} else {
|
||||
alert('Media modal function openSISMediaModal is not available.');
|
||||
}
|
||||
});
|
||||
appendDiv.appendChild(mediaBtn);
|
||||
hoverInputGroup.appendChild(appendDiv);
|
||||
}
|
||||
|
||||
hoverImgGroup.appendChild(hoverInputGroup);
|
||||
hoverImgGroup.appendChild(hoverPreviewContainer);
|
||||
// Custom Attributes field for individual card
|
||||
const cardAttrDiv = document.createElement('div');
|
||||
cardAttrDiv.className = 'form-group mb-2';
|
||||
cardAttrDiv.innerHTML = '<label class="small font-weight-bold text-secondary mb-1"><i class="fas fa-sliders-h"></i> Card Custom Attributes</label>';
|
||||
const cardAttrInput = document.createElement('input');
|
||||
cardAttrInput.type = 'text';
|
||||
cardAttrInput.className = 'form-control form-control-sm';
|
||||
cardAttrInput.placeholder = 'e.g. data-aos="fade-up" style="background:#fff"';
|
||||
cardAttrInput.value = item.attributes || '';
|
||||
if (this.readOnly) cardAttrInput.disabled = true;
|
||||
cardAttrInput.addEventListener('input', (e) => {
|
||||
item.attributes = e.target.value.trim();
|
||||
});
|
||||
cardAttrDiv.appendChild(cardAttrInput);
|
||||
itemBox.appendChild(cardAttrDiv);
|
||||
|
||||
// Default Active Radio (Only 1 option active at a time)
|
||||
const activeRadioDiv = document.createElement('div');
|
||||
activeRadioDiv.className = 'custom-control custom-radio mt-2';
|
||||
const activeRadio = document.createElement('input');
|
||||
activeRadio.type = 'radio';
|
||||
activeRadio.name = 'hovercard_active_' + this.instanceId;
|
||||
activeRadio.className = 'custom-control-input';
|
||||
const radioId = 'hovercard_active_' + index + '_' + Math.random().toString(36).substring(7);
|
||||
activeRadio.id = radioId;
|
||||
activeRadio.checked = !!item.active;
|
||||
if (this.readOnly) activeRadio.disabled = true;
|
||||
|
||||
activeRadio.addEventListener('change', () => {
|
||||
this.data.items.forEach((it, i) => {
|
||||
it.active = (i === index);
|
||||
});
|
||||
});
|
||||
|
||||
const activeLabel = document.createElement('label');
|
||||
activeLabel.className = 'custom-control-label small font-weight-bold text-secondary';
|
||||
activeLabel.htmlFor = radioId;
|
||||
activeLabel.innerHTML = 'Default Active Card (Only 1 active at a time)';
|
||||
|
||||
activeRadioDiv.appendChild(activeRadio);
|
||||
activeRadioDiv.appendChild(activeLabel);
|
||||
itemBox.appendChild(activeRadioDiv);
|
||||
|
||||
this.itemsContainer.appendChild(itemBox);
|
||||
}
|
||||
|
||||
save() {
|
||||
return {
|
||||
cols: this.colsSelect ? parseInt(this.colsSelect.value) : (this.data.cols || 4),
|
||||
styleVariant: this.variantSelect ? this.variantSelect.value : (this.data.styleVariant || 'v1'),
|
||||
globalClass: this.globalClassInput ? this.globalClassInput.value.trim() : (this.data.globalClass || 'sis-hover-card-section'),
|
||||
globalId: this.globalIdInput ? this.globalIdInput.value.trim() : (this.data.globalId || ''),
|
||||
globalAttributes: this.globalAttrInput ? this.globalAttrInput.value.trim() : (this.data.globalAttributes || ''),
|
||||
items: this.data.items || []
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Register the plugin globally
|
||||
window.SISEditorPlugins = window.SISEditorPlugins || {};
|
||||
window.SISEditorPlugins['hovercard'] = { class: SISHoverCardTool };
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* Custom Editor.js Block Tool for embedding predefined HTML snippets.
|
||||
* Uses iframe for WYSIWYG preview - snippets look exactly like on the live page.
|
||||
*/
|
||||
class HtmlSnippetTool {
|
||||
static get toolbox() {
|
||||
return {
|
||||
title: 'HTML Snippet',
|
||||
icon: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 18 22 12 16 6"></polyline><polyline points="8 6 2 12 8 18"></polyline></svg>'
|
||||
};
|
||||
}
|
||||
|
||||
constructor({ data, api }) {
|
||||
this.data = {
|
||||
id: data.id || '',
|
||||
globalAttributes: data.globalAttributes || data.attributes || ''
|
||||
};
|
||||
this.api = api;
|
||||
this.wrapper = undefined;
|
||||
}
|
||||
|
||||
render() {
|
||||
this.wrapper = document.createElement('div');
|
||||
this.wrapper.classList.add('ce-snippet-wrapper');
|
||||
this.wrapper.style.border = '1px solid #ddd';
|
||||
this.wrapper.style.borderRadius = '5px';
|
||||
this.wrapper.style.background = '#fafafa';
|
||||
this.wrapper.style.width = '100%';
|
||||
this.wrapper.style.boxSizing = 'border-box';
|
||||
|
||||
this._showInput();
|
||||
|
||||
return this.wrapper;
|
||||
}
|
||||
|
||||
_showInput() {
|
||||
this.wrapper.innerHTML = '';
|
||||
|
||||
const title = document.createElement('h4');
|
||||
title.style.marginTop = '0';
|
||||
title.style.marginBottom = '10px';
|
||||
title.style.padding = '15px 15px 0 15px';
|
||||
title.innerText = 'HTML Snippet ID & Custom Attributes:';
|
||||
title.style.fontSize = '14px';
|
||||
title.style.color = '#333';
|
||||
|
||||
const inputContainer = document.createElement('div');
|
||||
inputContainer.style.display = 'flex';
|
||||
inputContainer.style.gap = '10px';
|
||||
inputContainer.style.padding = '0 15px 15px 15px';
|
||||
|
||||
const input = document.createElement('input');
|
||||
input.classList.add('ce-input');
|
||||
input.placeholder = 'Enter Snippet ID (e.g., test_banner)';
|
||||
input.value = this.data.id;
|
||||
input.style.flex = '1';
|
||||
|
||||
input.addEventListener('input', (e) => {
|
||||
this.data.id = e.target.value.trim();
|
||||
});
|
||||
|
||||
const attrInput = document.createElement('input');
|
||||
attrInput.classList.add('ce-input');
|
||||
attrInput.placeholder = 'Custom Attributes (e.g. data-aos="fade-up" style="...")';
|
||||
attrInput.value = this.data.globalAttributes || '';
|
||||
attrInput.style.flex = '1';
|
||||
|
||||
attrInput.addEventListener('input', (e) => {
|
||||
this.data.globalAttributes = e.target.value.trim();
|
||||
});
|
||||
|
||||
inputContainer.appendChild(input);
|
||||
inputContainer.appendChild(attrInput);
|
||||
|
||||
this.wrapper.appendChild(title);
|
||||
this.wrapper.appendChild(inputContainer);
|
||||
}
|
||||
|
||||
save(blockContent) {
|
||||
return {
|
||||
id: this.data.id,
|
||||
globalAttributes: this.data.globalAttributes || ''
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Register the plugin globally
|
||||
window.SISEditorPlugins = window.SISEditorPlugins || {};
|
||||
window.SISEditorPlugins['snippet'] = {
|
||||
class: HtmlSnippetTool
|
||||
};
|
||||
+32
File diff suppressed because one or more lines are too long
BIN
Binary file not shown.
+2
@@ -0,0 +1,2 @@
|
||||
(function(){"use strict";try{if(typeof document<"u"){var e=document.createElement("style");e.appendChild(document.createTextNode(".inline-code{background:rgba(250,239,240,.78);color:#b44437;padding:3px 4px;border-radius:5px;margin:0 1px;font-family:inherit;font-size:.86em;font-weight:500;letter-spacing:.3px}")),document.head.appendChild(e)}}catch(n){console.error("vite-plugin-css-injected-by-js",n)}})();
|
||||
(function(i,s){typeof exports=="object"&&typeof module<"u"?module.exports=s():typeof define=="function"&&define.amd?define(s):(i=typeof globalThis<"u"?globalThis:i||self,i.InlineCode=s())})(this,function(){"use strict";const i="",s='<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 8L5 12L9 16"/><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 8L19 12L15 16"/></svg>';class n{constructor({api:t}){this.tag="CODE",this.api=t,this.button=null,this.iconClasses={base:this.api.styles.inlineToolButton,active:this.api.styles.inlineToolButtonActive}}static get CSS(){return"inline-code"}static get isInline(){return!0}render(){return this.button=document.createElement("button"),this.button.type="button",this.button.classList.add(this.iconClasses.base),this.button.innerHTML=this.toolboxIcon,this.button}surround(t){if(!t)return;let e=this.api.selection.findParentTag(this.tag,n.CSS);e?this.unwrap(e):this.wrap(t)}wrap(t){let e=document.createElement(this.tag);e.classList.add(n.CSS),e.appendChild(t.extractContents()),t.insertNode(e),this.api.selection.expandToTag(e)}unwrap(t){var r;this.api.selection.expandToTag(t);const e=window.getSelection();if(!e)return;const o=e.getRangeAt(0),a=o.extractContents();(r=t.parentNode)==null||r.removeChild(t),o.insertNode(a),e.removeAllRanges(),e.addRange(o)}checkState(){const t=this.api.selection.findParentTag(this.tag,n.CSS);return this.button&&this.button.classList.toggle(this.iconClasses.active,!!t),!!t}get toolboxIcon(){return s}static get sanitize(){return{code:{class:n.CSS}}}}return n});
|
||||
BIN
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
(function(factory){if(typeof define==="function"&&define.amd){define(["jquery"],function($){return factory($)})}else if(typeof module==="object"&&typeof module.exports==="object"){exports=factory(require("jquery"))}else{factory(jQuery)}})(function($){$.easing.jswing=$.easing.swing;var pow=Math.pow,sqrt=Math.sqrt,sin=Math.sin,cos=Math.cos,PI=Math.PI,c1=1.70158,c2=c1*1.525,c3=c1+1,c4=2*PI/3,c5=2*PI/4.5;function bounceOut(x){var n1=7.5625,d1=2.75;if(x<1/d1){return n1*x*x}else if(x<2/d1){return n1*(x-=1.5/d1)*x+.75}else if(x<2.5/d1){return n1*(x-=2.25/d1)*x+.9375}else{return n1*(x-=2.625/d1)*x+.984375}}$.extend($.easing,{def:"easeOutQuad",swing:function(x){return $.easing[$.easing.def](x)},easeInQuad:function(x){return x*x},easeOutQuad:function(x){return 1-(1-x)*(1-x)},easeInOutQuad:function(x){return x<.5?2*x*x:1-pow(-2*x+2,2)/2},easeInCubic:function(x){return x*x*x},easeOutCubic:function(x){return 1-pow(1-x,3)},easeInOutCubic:function(x){return x<.5?4*x*x*x:1-pow(-2*x+2,3)/2},easeInQuart:function(x){return x*x*x*x},easeOutQuart:function(x){return 1-pow(1-x,4)},easeInOutQuart:function(x){return x<.5?8*x*x*x*x:1-pow(-2*x+2,4)/2},easeInQuint:function(x){return x*x*x*x*x},easeOutQuint:function(x){return 1-pow(1-x,5)},easeInOutQuint:function(x){return x<.5?16*x*x*x*x*x:1-pow(-2*x+2,5)/2},easeInSine:function(x){return 1-cos(x*PI/2)},easeOutSine:function(x){return sin(x*PI/2)},easeInOutSine:function(x){return-(cos(PI*x)-1)/2},easeInExpo:function(x){return x===0?0:pow(2,10*x-10)},easeOutExpo:function(x){return x===1?1:1-pow(2,-10*x)},easeInOutExpo:function(x){return x===0?0:x===1?1:x<.5?pow(2,20*x-10)/2:(2-pow(2,-20*x+10))/2},easeInCirc:function(x){return 1-sqrt(1-pow(x,2))},easeOutCirc:function(x){return sqrt(1-pow(x-1,2))},easeInOutCirc:function(x){return x<.5?(1-sqrt(1-pow(2*x,2)))/2:(sqrt(1-pow(-2*x+2,2))+1)/2},easeInElastic:function(x){return x===0?0:x===1?1:-pow(2,10*x-10)*sin((x*10-10.75)*c4)},easeOutElastic:function(x){return x===0?0:x===1?1:pow(2,-10*x)*sin((x*10-.75)*c4)+1},easeInOutElastic:function(x){return x===0?0:x===1?1:x<.5?-(pow(2,20*x-10)*sin((20*x-11.125)*c5))/2:pow(2,-20*x+10)*sin((20*x-11.125)*c5)/2+1},easeInBack:function(x){return c3*x*x*x-c1*x*x},easeOutBack:function(x){return 1+c3*pow(x-1,3)+c1*pow(x-1,2)},easeInOutBack:function(x){return x<.5?pow(2*x,2)*((c2+1)*2*x-c2)/2:(pow(2*x-2,2)*((c2+1)*(x*2-2)+c2)+2)/2},easeInBounce:function(x){return 1-bounceOut(1-x)},easeOutBounce:bounceOut,easeInOutBounce:function(x){return x<.5?(1-bounceOut(1-2*x))/2:(1+bounceOut(2*x-1))/2}})});
|
||||
BIN
Binary file not shown.
+2
File diff suppressed because one or more lines are too long
BIN
Binary file not shown.
+2
@@ -0,0 +1,2 @@
|
||||
(function(){"use strict";try{if(typeof document<"u"){var e=document.createElement("style");e.appendChild(document.createTextNode(".cdx-marker{background:rgba(245,235,111,.29);padding:3px 0}")),document.head.appendChild(e)}}catch(d){console.error("vite-plugin-css-injected-by-js",d)}})();
|
||||
(function(i,s){typeof exports=="object"&&typeof module<"u"?module.exports=s():typeof define=="function"&&define.amd?define(s):(i=typeof globalThis<"u"?globalThis:i||self,i.Marker=s())})(this,function(){"use strict";const i="",s='<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-width="2" d="M11.3536 9.31802L12.7678 7.90381C13.5488 7.12276 14.8151 7.12276 15.5962 7.90381C16.3772 8.68486 16.3772 9.95119 15.5962 10.7322L14.182 12.1464M11.3536 9.31802L7.96729 12.7043C7.40889 13.2627 7.02827 13.9739 6.8734 14.7482L6.69798 15.6253C6.55804 16.325 7.17496 16.942 7.87468 16.802L8.75176 16.6266C9.52612 16.4717 10.2373 16.0911 10.7957 15.5327L14.182 12.1464M11.3536 9.31802L14.182 12.1464"/><line x1="15" x2="19" y1="17" y2="17" stroke="currentColor" stroke-linecap="round" stroke-width="2"/></svg>';class n{static get CSS(){return"cdx-marker"}constructor({api:t}){this.api=t,this.button=null,this.tag="MARK",this.iconClasses={base:this.api.styles.inlineToolButton,active:this.api.styles.inlineToolButtonActive}}static get isInline(){return!0}render(){return this.button=document.createElement("button"),this.button.type="button",this.button.classList.add(this.iconClasses.base),this.button.innerHTML=this.toolboxIcon,this.button}surround(t){if(!t)return;let e=this.api.selection.findParentTag(this.tag,n.CSS);e?this.unwrap(e):this.wrap(t)}wrap(t){let e=document.createElement(this.tag);e.classList.add(n.CSS),e.appendChild(t.extractContents()),t.insertNode(e),this.api.selection.expandToTag(e)}unwrap(t){this.api.selection.expandToTag(t);let e=window.getSelection(),o=e.getRangeAt(0),a=o.extractContents();t.parentNode.removeChild(t),o.insertNode(a),e.removeAllRanges(),e.addRange(o)}checkState(){const t=this.api.selection.findParentTag(this.tag,n.CSS);this.button.classList.toggle(this.iconClasses.active,!!t)}get toolboxIcon(){return s}static get sanitize(){return{mark:{class:n.CSS}}}}return n});
|
||||
BIN
Binary file not shown.
+314
@@ -0,0 +1,314 @@
|
||||
/**
|
||||
* SISStickyNavTool — Sticky Navigation & Sub-menu Plugin for Editor.js
|
||||
* Allows content managers to build dynamic page navigation sub-menus with smooth anchor links.
|
||||
*/
|
||||
class SISStickyNavTool {
|
||||
static get toolbox() {
|
||||
return {
|
||||
title: 'Sticky Nav / Sub-menu',
|
||||
icon: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>'
|
||||
};
|
||||
}
|
||||
|
||||
constructor({ data, api, readOnly }) {
|
||||
this.api = api;
|
||||
this.readOnly = readOnly;
|
||||
this.data = {
|
||||
title: (data && data.title) ? data.title : 'Quick Navigation',
|
||||
bgColor: (data && data.bgColor) ? data.bgColor : '#002554',
|
||||
globalClass: (data && data.globalClass) || '',
|
||||
globalId: (data && data.globalId) || '',
|
||||
globalAttributes: (data && data.globalAttributes) || (data && data.attributes) || '',
|
||||
items: (data && Array.isArray(data.items)) ? data.items : [
|
||||
{ label: 'Giới thiệu', link: '#sec-intro' },
|
||||
{ label: 'Chuyên khoa', link: '#sec-specialty' },
|
||||
{ label: 'Quy trình', link: '#sec-process' }
|
||||
],
|
||||
sticky: (data && data.sticky !== undefined) ? !!data.sticky : true,
|
||||
stretched: (data && data.stretched !== undefined) ? !!data.stretched : true
|
||||
};
|
||||
this.titleInput = null;
|
||||
this.itemsContainer = null;
|
||||
}
|
||||
|
||||
render() {
|
||||
const container = document.createElement('div');
|
||||
container.style.border = '1px solid #e3e6f0';
|
||||
container.style.borderRadius = '8px';
|
||||
container.style.padding = '14px';
|
||||
container.style.background = '#fff';
|
||||
container.style.marginBottom = '12px';
|
||||
|
||||
const headerLabel = document.createElement('label');
|
||||
headerLabel.className = 'font-weight-bold text-primary small mb-2 d-block';
|
||||
headerLabel.innerHTML = '<i class="fas fa-bars"></i> Sticky Page Navigation Bar Settings';
|
||||
container.appendChild(headerLabel);
|
||||
|
||||
// Settings Row for Title, Class, ID & Attributes
|
||||
const metaRow = document.createElement('div');
|
||||
metaRow.className = 'form-row mb-3';
|
||||
|
||||
// 1. Mobile Title
|
||||
const titleDiv = document.createElement('div');
|
||||
titleDiv.className = 'col-md-3 mb-2';
|
||||
titleDiv.innerHTML = '<label class="small font-weight-bold text-secondary mb-1 d-block">Dropdown Title</label>';
|
||||
this.titleInput = document.createElement('input');
|
||||
this.titleInput.type = 'text';
|
||||
this.titleInput.className = 'form-control form-control-sm';
|
||||
this.titleInput.placeholder = 'e.g. Quick Navigation';
|
||||
this.titleInput.value = this.data.title || 'Quick Navigation';
|
||||
if (this.readOnly) this.titleInput.disabled = true;
|
||||
titleDiv.appendChild(this.titleInput);
|
||||
metaRow.appendChild(titleDiv);
|
||||
|
||||
// 2. CSS Class
|
||||
const classDiv = document.createElement('div');
|
||||
classDiv.className = 'col-md-3 mb-2';
|
||||
classDiv.innerHTML = '<label class="small font-weight-bold text-secondary mb-1 d-block">CSS Class(es)</label>';
|
||||
this.classInput = document.createElement('input');
|
||||
this.classInput.type = 'text';
|
||||
this.classInput.className = 'form-control form-control-sm';
|
||||
this.classInput.placeholder = 'e.g. my-custom-nav';
|
||||
this.classInput.value = this.data.globalClass || '';
|
||||
if (this.readOnly) this.classInput.disabled = true;
|
||||
this.classInput.addEventListener('input', (e) => {
|
||||
this.data.globalClass = e.target.value.trim();
|
||||
});
|
||||
classDiv.appendChild(this.classInput);
|
||||
metaRow.appendChild(classDiv);
|
||||
|
||||
// 3. HTML ID
|
||||
const idDiv = document.createElement('div');
|
||||
idDiv.className = 'col-md-3 mb-2';
|
||||
idDiv.innerHTML = '<label class="small font-weight-bold text-secondary mb-1 d-block">HTML ID</label>';
|
||||
this.idInput = document.createElement('input');
|
||||
this.idInput.type = 'text';
|
||||
this.idInput.className = 'form-control form-control-sm';
|
||||
this.idInput.placeholder = 'e.g. main-sticky-nav';
|
||||
this.idInput.value = this.data.globalId || '';
|
||||
if (this.readOnly) this.idInput.disabled = true;
|
||||
this.idInput.addEventListener('input', (e) => {
|
||||
this.data.globalId = e.target.value.trim();
|
||||
});
|
||||
idDiv.appendChild(this.idInput);
|
||||
metaRow.appendChild(idDiv);
|
||||
|
||||
// 4. Custom Attributes
|
||||
const attrDiv = document.createElement('div');
|
||||
attrDiv.className = 'col-md-3 mb-2';
|
||||
attrDiv.innerHTML = '<label class="small font-weight-bold text-secondary mb-1 d-block"><i class="fas fa-sliders-h"></i> Custom Attributes</label>';
|
||||
this.attrInput = document.createElement('input');
|
||||
this.attrInput.type = 'text';
|
||||
this.attrInput.className = 'form-control form-control-sm';
|
||||
this.attrInput.placeholder = 'e.g. data-aos="fade-down" style="..."';
|
||||
this.attrInput.value = this.data.globalAttributes || '';
|
||||
if (this.readOnly) this.attrInput.disabled = true;
|
||||
this.attrInput.addEventListener('input', (e) => {
|
||||
this.data.globalAttributes = e.target.value.trim();
|
||||
});
|
||||
attrDiv.appendChild(this.attrInput);
|
||||
metaRow.appendChild(attrDiv);
|
||||
|
||||
container.appendChild(metaRow);
|
||||
|
||||
// Switches Row (Sticky + Stretched)
|
||||
const switchesRow = document.createElement('div');
|
||||
switchesRow.className = 'd-flex flex-wrap gap-3 mb-3';
|
||||
|
||||
// Sticky switch
|
||||
const stickyWrapper = document.createElement('div');
|
||||
stickyWrapper.className = 'custom-control custom-switch mr-3';
|
||||
const stickyCheck = document.createElement('input');
|
||||
stickyCheck.type = 'checkbox';
|
||||
stickyCheck.className = 'custom-control-input';
|
||||
stickyCheck.id = 'nav_sticky_' + Math.random().toString(36).substring(7);
|
||||
stickyCheck.checked = !!this.data.sticky;
|
||||
if (this.readOnly) stickyCheck.disabled = true;
|
||||
|
||||
const stickyLabel = document.createElement('label');
|
||||
stickyLabel.className = 'custom-control-label small font-weight-bold text-primary';
|
||||
stickyLabel.htmlFor = stickyCheck.id;
|
||||
stickyLabel.innerHTML = '<i class="fas fa-thumbtack"></i> Sticky to Top on Scroll';
|
||||
|
||||
stickyCheck.addEventListener('change', () => {
|
||||
this.data.sticky = stickyCheck.checked;
|
||||
});
|
||||
stickyWrapper.appendChild(stickyCheck);
|
||||
stickyWrapper.appendChild(stickyLabel);
|
||||
switchesRow.appendChild(stickyWrapper);
|
||||
|
||||
// Stretch switch
|
||||
const stretchWrapper = document.createElement('div');
|
||||
stretchWrapper.className = 'custom-control custom-switch';
|
||||
this.stretchCheck = document.createElement('input');
|
||||
this.stretchCheck.type = 'checkbox';
|
||||
this.stretchCheck.className = 'custom-control-input';
|
||||
this.stretchCheck.id = 'nav_stretch_' + Math.random().toString(36).substring(7);
|
||||
this.stretchCheck.checked = !!this.data.stretched;
|
||||
if (this.readOnly) this.stretchCheck.disabled = true;
|
||||
|
||||
const stretchLabel = document.createElement('label');
|
||||
stretchLabel.className = 'custom-control-label small font-weight-bold text-secondary';
|
||||
stretchLabel.htmlFor = this.stretchCheck.id;
|
||||
stretchLabel.innerHTML = '<i class="fas fa-arrows-alt-h"></i> Stretch to Full Screen Width';
|
||||
|
||||
this.stretchCheck.addEventListener('change', () => {
|
||||
this.data.stretched = this.stretchCheck.checked;
|
||||
});
|
||||
stretchWrapper.appendChild(this.stretchCheck);
|
||||
stretchWrapper.appendChild(stretchLabel);
|
||||
switchesRow.appendChild(stretchWrapper);
|
||||
|
||||
container.appendChild(switchesRow);
|
||||
|
||||
// Background Color Selection
|
||||
const bgGroup = document.createElement('div');
|
||||
bgGroup.className = 'form-group mb-3';
|
||||
const bgLabel = document.createElement('label');
|
||||
bgLabel.className = 'small font-weight-bold text-secondary mb-1 d-block';
|
||||
bgLabel.innerText = 'Background Style';
|
||||
|
||||
this.bgSelect = document.createElement('select');
|
||||
this.bgSelect.className = 'form-control form-control-sm';
|
||||
this.bgSelect.innerHTML = `
|
||||
<option value="#002554">Dark Blue (SIS Primary - #002554)</option>
|
||||
<option value="#881C1C">Brand Red (#881C1C)</option>
|
||||
<option value="#1a252f">Charcoal Dark (#1a252f)</option>
|
||||
<option value="#ffffff">White (#ffffff)</option>
|
||||
`;
|
||||
this.bgSelect.value = this.data.bgColor || '#002554';
|
||||
if (this.readOnly) this.bgSelect.disabled = true;
|
||||
bgGroup.appendChild(bgLabel);
|
||||
bgGroup.appendChild(this.bgSelect);
|
||||
container.appendChild(bgGroup);
|
||||
|
||||
// Navigation Items List
|
||||
const itemsLabel = document.createElement('label');
|
||||
itemsLabel.className = 'small font-weight-bold text-secondary mb-2 d-block';
|
||||
itemsLabel.innerText = 'Navigation Links / Anchors';
|
||||
container.appendChild(itemsLabel);
|
||||
|
||||
this.itemsContainer = document.createElement('div');
|
||||
this.itemsContainer.className = 'nav-items-list mb-2';
|
||||
container.appendChild(this.itemsContainer);
|
||||
|
||||
const renderItemsInputs = () => {
|
||||
this.itemsContainer.innerHTML = '';
|
||||
this.data.items.forEach((item, index) => {
|
||||
const itemRow = document.createElement('div');
|
||||
itemRow.className = 'form-row mb-2 align-items-center';
|
||||
|
||||
const colLabel = document.createElement('div');
|
||||
colLabel.className = 'col-md-5';
|
||||
const labelInp = document.createElement('input');
|
||||
labelInp.type = 'text';
|
||||
labelInp.className = 'form-control form-control-sm';
|
||||
labelInp.placeholder = 'Link Label (e.g. Giới thiệu)';
|
||||
labelInp.value = item.label || '';
|
||||
if (this.readOnly) labelInp.disabled = true;
|
||||
labelInp.addEventListener('input', () => {
|
||||
item.label = labelInp.value;
|
||||
updatePreview();
|
||||
});
|
||||
colLabel.appendChild(labelInp);
|
||||
|
||||
const colLink = document.createElement('div');
|
||||
colLink.className = 'col-md-5';
|
||||
const linkInp = document.createElement('input');
|
||||
linkInp.type = 'text';
|
||||
linkInp.className = 'form-control form-control-sm';
|
||||
linkInp.placeholder = 'Target ID or URL (e.g. #sec-intro)';
|
||||
linkInp.value = item.link || '';
|
||||
if (this.readOnly) linkInp.disabled = true;
|
||||
linkInp.addEventListener('input', () => {
|
||||
item.link = linkInp.value;
|
||||
updatePreview();
|
||||
});
|
||||
colLink.appendChild(linkInp);
|
||||
|
||||
const colDel = document.createElement('div');
|
||||
colDel.className = 'col-md-2';
|
||||
const delBtn = document.createElement('button');
|
||||
delBtn.type = 'button';
|
||||
delBtn.className = 'btn btn-sm btn-outline-danger btn-block';
|
||||
delBtn.innerHTML = '<i class="fas fa-trash"></i>';
|
||||
if (this.readOnly) delBtn.disabled = true;
|
||||
delBtn.addEventListener('click', () => {
|
||||
this.data.items.splice(index, 1);
|
||||
renderItemsInputs();
|
||||
updatePreview();
|
||||
});
|
||||
colDel.appendChild(delBtn);
|
||||
|
||||
itemRow.appendChild(colLabel);
|
||||
itemRow.appendChild(colLink);
|
||||
itemRow.appendChild(colDel);
|
||||
this.itemsContainer.appendChild(itemRow);
|
||||
});
|
||||
};
|
||||
|
||||
if (!this.readOnly) {
|
||||
const addBtn = document.createElement('button');
|
||||
addBtn.type = 'button';
|
||||
addBtn.className = 'btn btn-sm btn-outline-primary mb-3';
|
||||
addBtn.innerHTML = '<i class="fas fa-plus"></i> Add Nav Link';
|
||||
addBtn.addEventListener('click', () => {
|
||||
this.data.items.push({ label: 'New Link', link: '#section' });
|
||||
renderItemsInputs();
|
||||
updatePreview();
|
||||
});
|
||||
container.appendChild(addBtn);
|
||||
}
|
||||
|
||||
// Live Preview Box
|
||||
const previewBox = document.createElement('div');
|
||||
previewBox.className = 'nav-preview p-2 rounded my-2 d-flex flex-wrap justify-content-center align-items-center gap-2';
|
||||
previewBox.style.transition = 'background 0.3s ease';
|
||||
container.appendChild(previewBox);
|
||||
|
||||
const updatePreview = () => {
|
||||
const bg = this.bgSelect.value;
|
||||
const isLight = bg === '#ffffff';
|
||||
previewBox.style.background = bg;
|
||||
|
||||
let html = '';
|
||||
this.data.items.forEach(item => {
|
||||
const label = item.label || 'Link';
|
||||
const textColor = isLight ? '#002554' : '#ffffff';
|
||||
html += `<span class="badge px-3 py-2 mr-2 mb-1" style="background: rgba(255,255,255,0.15); color: ${textColor}; font-size: 13px; font-weight: 600;"><i class="fas fa-link mr-1"></i>${label}</span>`;
|
||||
});
|
||||
previewBox.innerHTML = html || '<span class="text-muted small">No nav links added</span>';
|
||||
};
|
||||
|
||||
this.bgSelect.addEventListener('change', () => {
|
||||
this.data.bgColor = this.bgSelect.value;
|
||||
updatePreview();
|
||||
});
|
||||
|
||||
renderItemsInputs();
|
||||
updatePreview();
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
save() {
|
||||
return {
|
||||
title: this.titleInput ? this.titleInput.value : (this.data.title || 'Quick Navigation'),
|
||||
bgColor: this.bgSelect ? this.bgSelect.value : this.data.bgColor,
|
||||
globalClass: this.classInput ? this.classInput.value.trim() : (this.data.globalClass || ''),
|
||||
globalId: this.idInput ? this.idInput.value.trim() : (this.data.globalId || ''),
|
||||
globalAttributes: this.attrInput ? this.attrInput.value.trim() : (this.data.globalAttributes || ''),
|
||||
items: this.data.items || [],
|
||||
sticky: this.data.sticky !== undefined ? !!this.data.sticky : true,
|
||||
stretched: this.stretchCheck ? this.stretchCheck.checked : !!this.data.stretched
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Alias class for nav
|
||||
class SISNavTool extends SISStickyNavTool {}
|
||||
|
||||
// Register plugin globally
|
||||
window.SISEditorPlugins = window.SISEditorPlugins || {};
|
||||
window.SISEditorPlugins['stickyNav'] = { class: SISStickyNavTool };
|
||||
window.SISEditorPlugins['nav'] = { class: SISNavTool };
|
||||
+2
File diff suppressed because one or more lines are too long
BIN
Binary file not shown.
+2
File diff suppressed because one or more lines are too long
BIN
Binary file not shown.
+3
@@ -0,0 +1,3 @@
|
||||
<!DOCTYPE html>
|
||||
<!-- saved from url=(0012)about:srcdoc -->
|
||||
<html><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><link rel="stylesheet" type="text/css" id="mce-u0" href="https://cdn.jsdelivr.net/npm/tinymce@6/skins/ui/oxide/content.min.css"><link rel="stylesheet" type="text/css" id="mce-u1" href="https://cdn.jsdelivr.net/npm/tinymce@6/skins/content/default/content.min.css"></head><body id="tinymce" class="mce-content-body " data-id="tinymce-editor-e06xjl" aria-label="Rich Text Area. Press ALT-0 for help." contenteditable="true" spellcheck="false"><h3 style="text-align: center;" data-mce-style="text-align: center;"><strong><span style="color: rgb(224, 62, 45);" data-mce-style="color: rgb(224, 62, 45);">Phó giám đốc điều hành</span></strong></h3><h3 style="text-align: center;" data-mce-style="text-align: center;"><strong><span style="color: rgb(224, 62, 45);" data-mce-style="color: rgb(224, 62, 45);">BS.CKII PHAN TRỊNH MINH HIẾU</span></strong></h3></body></html>
|
||||
BIN
Binary file not shown.
+3
@@ -0,0 +1,3 @@
|
||||
<!DOCTYPE html>
|
||||
<!-- saved from url=(0012)about:srcdoc -->
|
||||
<html><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><link rel="stylesheet" type="text/css" id="mce-u0" href="https://cdn.jsdelivr.net/npm/tinymce@6/skins/ui/oxide/content.min.css"><link rel="stylesheet" type="text/css" id="mce-u1" href="https://cdn.jsdelivr.net/npm/tinymce@6/skins/content/default/content.min.css"></head><body id="tinymce" class="mce-content-body " data-id="tinymce-editor-hkzhv" aria-label="Rich Text Area. Press ALT-0 for help." contenteditable="true" spellcheck="false"><h3 style="text-align: center;" data-mce-style="text-align: center;"><span style="color: rgb(224, 62, 45);" data-mce-style="color: rgb(224, 62, 45);">BS Thái</span></h3><h3 style="text-align: center;" data-mce-style="text-align: center;"><span style="color: rgb(224, 62, 45);" data-mce-style="color: rgb(224, 62, 45);">Trưởng Khoa</span></h3></body></html>
|
||||
BIN
Binary file not shown.
+3
@@ -0,0 +1,3 @@
|
||||
<!DOCTYPE html>
|
||||
<!-- saved from url=(0012)about:srcdoc -->
|
||||
<html><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><link rel="stylesheet" type="text/css" id="mce-u0" href="https://cdn.jsdelivr.net/npm/tinymce@6/skins/ui/oxide/content.min.css"><link rel="stylesheet" type="text/css" id="mce-u1" href="https://cdn.jsdelivr.net/npm/tinymce@6/skins/content/default/content.min.css"></head><body id="tinymce" class="mce-content-body " data-id="tinymce-editor-7uyna" aria-label="Rich Text Area. Press ALT-0 for help." contenteditable="true" spellcheck="false"><h3 style="text-align: center;" data-mce-style="text-align: center;"><span style="color: rgb(224, 62, 45);" data-mce-style="color: rgb(224, 62, 45);">BS Thái</span></h3><h3 style="text-align: center;" data-mce-style="text-align: center;"><span style="color: rgb(224, 62, 45);" data-mce-style="color: rgb(224, 62, 45);">Trưởng Khoa</span></h3></body></html>
|
||||
BIN
Binary file not shown.
+3
@@ -0,0 +1,3 @@
|
||||
<!DOCTYPE html>
|
||||
<!-- saved from url=(0012)about:srcdoc -->
|
||||
<html><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><link rel="stylesheet" type="text/css" id="mce-u0" href="https://cdn.jsdelivr.net/npm/tinymce@6/skins/ui/oxide/content.min.css"><link rel="stylesheet" type="text/css" id="mce-u1" href="https://cdn.jsdelivr.net/npm/tinymce@6/skins/content/default/content.min.css"></head><body id="tinymce" class="mce-content-body " data-id="tinymce-editor-h7dxj1d" aria-label="Rich Text Area. Press ALT-0 for help." contenteditable="true" spellcheck="false"><h3 style="text-align: center;" data-mce-style="text-align: center;"><span style="color: rgb(224, 62, 45);" data-mce-style="color: rgb(224, 62, 45);">BS Thái</span></h3><h3 style="text-align: center;" data-mce-style="text-align: center;"><span style="color: rgb(224, 62, 45);" data-mce-style="color: rgb(224, 62, 45);">Trưởng Khoa</span></h3></body></html>
|
||||
BIN
Binary file not shown.
+3
@@ -0,0 +1,3 @@
|
||||
<!DOCTYPE html>
|
||||
<!-- saved from url=(0012)about:srcdoc -->
|
||||
<html><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><link rel="stylesheet" type="text/css" id="mce-u0" href="https://cdn.jsdelivr.net/npm/tinymce@6/skins/ui/oxide/content.min.css"><link rel="stylesheet" type="text/css" id="mce-u1" href="https://cdn.jsdelivr.net/npm/tinymce@6/skins/content/default/content.min.css"></head><body id="tinymce" class="mce-content-body " data-id="tinymce-editor-yxw5l9" aria-label="Rich Text Area. Press ALT-0 for help." contenteditable="true" spellcheck="false"><h3 style="text-align: center;" data-mce-style="text-align: center;"><span style="color: rgb(224, 62, 45);" data-mce-style="color: rgb(224, 62, 45);">Giám đốc</span></h3><h3 style="text-align: center;" data-mce-style="text-align: center;"><span style="color: rgb(224, 62, 45);" data-mce-style="color: rgb(224, 62, 45);">TS.BS TRẦN CHÍ CƯỜNG</span></h3></body></html>
|
||||
BIN
Binary file not shown.
Vendored
+10
File diff suppressed because one or more lines are too long
BIN
Binary file not shown.
+7
@@ -0,0 +1,7 @@
|
||||
/*!
|
||||
* Start Bootstrap - SB Admin 2 v4.1.4 (https://startbootstrap.com/theme/sb-admin-2)
|
||||
* Copyright 2013-2021 Start Bootstrap
|
||||
* Licensed under MIT (https://github.com/StartBootstrap/startbootstrap-sb-admin-2/blob/master/LICENSE)
|
||||
*/
|
||||
|
||||
!function(l){"use strict";l("#sidebarToggle, #sidebarToggleTop").on("click",function(e){l("body").toggleClass("sidebar-toggled"),l(".sidebar").toggleClass("toggled"),l(".sidebar").hasClass("toggled")&&l(".sidebar .collapse").collapse("hide")}),l(window).resize(function(){l(window).width()<768&&l(".sidebar .collapse").collapse("hide"),l(window).width()<480&&!l(".sidebar").hasClass("toggled")&&(l("body").addClass("sidebar-toggled"),l(".sidebar").addClass("toggled"),l(".sidebar .collapse").collapse("hide"))}),l("body.fixed-nav .sidebar").on("mousewheel DOMMouseScroll wheel",function(e){var o;768<l(window).width()&&(o=(o=e.originalEvent).wheelDelta||-o.detail,this.scrollTop+=30*(o<0?1:-1),e.preventDefault())}),l(document).on("scroll",function(){100<l(this).scrollTop()?l(".scroll-to-top").fadeIn():l(".scroll-to-top").fadeOut()}),l(document).on("click","a.scroll-to-top",function(e){var o=l(this);l("html, body").stop().animate({scrollTop:l(o.attr("href")).offset().top},1e3,"easeInOutExpo"),e.preventDefault()})}(jQuery);
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
|
After Width: | Height: | Size: 286 KiB |
+461
@@ -0,0 +1,461 @@
|
||||
/**
|
||||
* SIS Media Picker & Library Modal Utility
|
||||
* Standalone reusable module to select and upload media files anywhere in the application.
|
||||
*
|
||||
* Usage (simple - just URL returned):
|
||||
* window.openSISMediaModal(function(selectedUrl, selectedMediaObj) {
|
||||
* console.log("Selected Image URL:", selectedUrl);
|
||||
* });
|
||||
*
|
||||
* Usage (extended - returns URL + config object with style, attributes, size):
|
||||
* SISMediaPicker.open(function(selectedUrl, selectedMediaObj, config) {
|
||||
* // config = { style, customAttributes, width, height }
|
||||
* });
|
||||
*
|
||||
* Or via object API:
|
||||
* SISMediaPicker.open(callback, options);
|
||||
* options = { showConfig: true } // default: true
|
||||
*/
|
||||
(function (window, document) {
|
||||
'use strict';
|
||||
|
||||
var PREDEFINED_SIZES = [
|
||||
{ label: 'Auto', width: '', height: '' },
|
||||
{ label: '25%', width: '25%', height: '' },
|
||||
{ label: '50%', width: '50%', height: '' },
|
||||
{ label: '75%', width: '75%', height: '' },
|
||||
{ label: '100%', width: '100%', height: '' },
|
||||
{ label: '100px', width: '100px', height: '100px' },
|
||||
{ label: '150px', width: '150px', height: '150px' },
|
||||
{ label: '200px', width: '200px', height: '200px' },
|
||||
{ label: '300px', width: '300px', height: 'auto' },
|
||||
{ label: '400px', width: '400px', height: 'auto' },
|
||||
{ label: '500px', width: '500px', height: 'auto' },
|
||||
{ label: 'Full Width', width: '100%', height: 'auto' },
|
||||
{ label: 'Thumbnail (80×80)', width: '80px', height: '80px' },
|
||||
{ label: 'Avatar (120×120)', width: '120px', height: '120px' },
|
||||
{ label: 'Banner (1200×400)', width: '1200px', height: '400px' },
|
||||
];
|
||||
|
||||
var SISMediaPicker = {
|
||||
callback: null,
|
||||
mediaData: [],
|
||||
modalId: 'sisMediaLibraryModal',
|
||||
configPanelId: 'sisMediaConfigPanel',
|
||||
selectedUrl: null,
|
||||
selectedMediaObj: null,
|
||||
|
||||
initModal: function () {
|
||||
var modalEl = document.getElementById(this.modalId);
|
||||
if (modalEl) return modalEl;
|
||||
|
||||
modalEl = document.createElement('div');
|
||||
modalEl.id = this.modalId;
|
||||
modalEl.className = 'modal fade';
|
||||
modalEl.setAttribute('tabindex', '-1');
|
||||
modalEl.setAttribute('role', 'dialog');
|
||||
modalEl.setAttribute('aria-hidden', 'true');
|
||||
modalEl.innerHTML =
|
||||
'<div class="modal-dialog modal-xl" role="document">' +
|
||||
'<div class="modal-content border-0 shadow-lg">' +
|
||||
'<div class="modal-header bg-primary text-white p-3">' +
|
||||
'<h5 class="modal-title font-weight-bold" id="' + this.modalId + 'Label">' +
|
||||
'<i class="fas fa-images mr-2"></i> Thư viện Media / Chọn Hình ảnh' +
|
||||
'</h5>' +
|
||||
'<button type="button" class="close text-white opacity-100" data-dismiss="modal" aria-label="Close">' +
|
||||
'<span aria-hidden="true">×</span>' +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
'<div class="modal-body bg-light p-3">' +
|
||||
/* Toolbar row */
|
||||
'<div class="d-flex flex-wrap justify-content-between align-items-center mb-3 bg-white p-2 border rounded shadow-sm">' +
|
||||
'<div class="input-group input-group-sm mb-2 mb-md-0" style="max-width: 380px;">' +
|
||||
'<div class="input-group-prepend"><span class="input-group-text bg-light"><i class="fas fa-search text-muted"></i></span></div>' +
|
||||
'<input type="text" class="form-control" id="sisMediaSearchInput" placeholder="Tìm kiếm theo tên file..." />' +
|
||||
'</div>' +
|
||||
'<div>' +
|
||||
'<label class="btn btn-sm btn-success mb-0 cursor-pointer shadow-sm" style="cursor: pointer;">' +
|
||||
'<i class="fas fa-cloud-upload-alt mr-1"></i> Tải ảnh mới lên' +
|
||||
'<input type="file" id="sisMediaUploadInput" accept="image/*" style="display: none;" />' +
|
||||
'</label>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
/* Media Grid */
|
||||
'<div id="sisMediaGrid" class="row" style="max-height: 380px; overflow-y: auto; align-content: start;">' +
|
||||
'<div class="col-12 text-center text-muted py-5"><i class="fas fa-spinner fa-spin fa-2x"></i><br/><span class="mt-2 d-block">Đang tải danh sách hình ảnh...</span></div>' +
|
||||
'</div>' +
|
||||
/* Config Panel - hidden until an image is selected */
|
||||
'<div id="' + this.configPanelId + '" class="mt-3 p-3 bg-white border rounded shadow-sm" style="display: none;">' +
|
||||
'<h6 class="font-weight-bold text-primary mb-3"><i class="fas fa-sliders-h mr-1"></i> Cấu hình hình ảnh đã chọn</h6>' +
|
||||
'<div class="row">' +
|
||||
/* Preview */
|
||||
'<div class="col-md-3 text-center mb-3">' +
|
||||
'<p class="small text-muted mb-1 font-weight-bold">Xem trước</p>' +
|
||||
'<div class="border rounded p-2 bg-light" style="min-height: 100px; display: flex; align-items: center; justify-content: center;">' +
|
||||
'<img id="sisMediaConfigPreview" src="" alt="Preview" style="max-width: 100%; max-height: 150px; object-fit: contain;" />' +
|
||||
'</div>' +
|
||||
'<p id="sisMediaConfigName" class="small text-truncate mt-1 text-muted mb-0"></p>' +
|
||||
'</div>' +
|
||||
/* Config Fields */
|
||||
'<div class="col-md-9">' +
|
||||
'<div class="row">' +
|
||||
/* Predefined Sizes */
|
||||
'<div class="col-12 mb-2">' +
|
||||
'<label class="small font-weight-bold text-muted d-block mb-1" style="font-size: 10px; text-transform: uppercase;">Kích thước nhanh</label>' +
|
||||
'<div id="sisMediaSizeButtons" class="d-flex flex-wrap gap-1" style="gap: 4px;">' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
/* Width */
|
||||
'<div class="col-md-4 mb-2">' +
|
||||
'<label class="small font-weight-bold text-muted mb-1" style="font-size: 10px;">Chiều rộng (width)</label>' +
|
||||
'<input type="text" id="sisMediaConfigWidth" class="form-control form-control-sm" placeholder="e.g. 100%, 300px, auto" />' +
|
||||
'</div>' +
|
||||
/* Height */
|
||||
'<div class="col-md-4 mb-2">' +
|
||||
'<label class="small font-weight-bold text-muted mb-1" style="font-size: 10px;">Chiều cao (height)</label>' +
|
||||
'<input type="text" id="sisMediaConfigHeight" class="form-control form-control-sm" placeholder="e.g. auto, 200px" />' +
|
||||
'</div>' +
|
||||
/* Object Fit */
|
||||
'<div class="col-md-4 mb-2">' +
|
||||
'<label class="small font-weight-bold text-muted mb-1" style="font-size: 10px;">Object Fit</label>' +
|
||||
'<select id="sisMediaConfigObjectFit" class="form-control form-control-sm">' +
|
||||
'<option value="">-- default --</option>' +
|
||||
'<option value="cover">cover</option>' +
|
||||
'<option value="contain">contain</option>' +
|
||||
'<option value="fill">fill</option>' +
|
||||
'<option value="none">none</option>' +
|
||||
'<option value="scale-down">scale-down</option>' +
|
||||
'</select>' +
|
||||
'</div>' +
|
||||
/* Aspect Ratio */
|
||||
'<div class="col-md-4 mb-2">' +
|
||||
'<label class="small font-weight-bold text-muted mb-1" style="font-size: 10px;">Aspect Ratio</label>' +
|
||||
'<select id="sisMediaConfigAspectRatio" class="form-control form-control-sm">' +
|
||||
'<option value="">-- none --</option>' +
|
||||
'<option value="1/1">1:1 (Square)</option>' +
|
||||
'<option value="4/3">4:3 (Standard)</option>' +
|
||||
'<option value="16/9">16:9 (Widescreen)</option>' +
|
||||
'<option value="21/9">21:9 (Ultrawide)</option>' +
|
||||
'<option value="3/4">3:4 (Portrait)</option>' +
|
||||
'<option value="9/16">9:16 (Vertical Video)</option>' +
|
||||
'<option value="2/1">2:1 (Panorama)</option>' +
|
||||
'<option value="3/2">3:2 (Photo)</option>' +
|
||||
'<option value="5/4">5:4</option>' +
|
||||
'</select>' +
|
||||
'</div>' +
|
||||
/* CSS Class */
|
||||
'<div class="col-md-4 mb-2">' +
|
||||
'<label class="small font-weight-bold text-muted mb-1" style="font-size: 10px;">CSS Class</label>' +
|
||||
'<input type="text" id="sisMediaConfigClass" class="form-control form-control-sm" placeholder="e.g. img-fluid rounded shadow" />' +
|
||||
'</div>' +
|
||||
/* Inline Style */
|
||||
'<div class="col-md-6 mb-2">' +
|
||||
'<label class="small font-weight-bold text-muted mb-1" style="font-size: 10px;">Inline Style (CSS)</label>' +
|
||||
'<input type="text" id="sisMediaConfigStyle" class="form-control form-control-sm" placeholder="e.g. border-radius: 8px; opacity: 0.9;" />' +
|
||||
'</div>' +
|
||||
/* Alt Text */
|
||||
'<div class="col-md-6 mb-2">' +
|
||||
'<label class="small font-weight-bold text-muted mb-1" style="font-size: 10px;">Alt Text</label>' +
|
||||
'<input type="text" id="sisMediaConfigAlt" class="form-control form-control-sm" placeholder="Image description..." />' +
|
||||
'</div>' +
|
||||
/* Custom Attributes */
|
||||
'<div class="col-md-6 mb-2">' +
|
||||
'<label class="small font-weight-bold text-muted mb-1" style="font-size: 10px;">Custom Attributes (HTML)</label>' +
|
||||
'<input type="text" id="sisMediaConfigAttrs" class="form-control form-control-sm" placeholder=\'e.g. data-id="5" loading="lazy"\' />' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="text-right mt-2">' +
|
||||
'<button type="button" class="btn btn-sm btn-secondary mr-2" id="sisMediaConfigCancel">Hủy</button>' +
|
||||
'<button type="button" class="btn btn-sm btn-primary" id="sisMediaConfigConfirm"><i class="fas fa-check mr-1"></i> Chọn ảnh này</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="modal-footer bg-white p-2">' +
|
||||
'<button type="button" class="btn btn-sm btn-secondary px-3" data-dismiss="modal">Đóng</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
|
||||
document.body.appendChild(modalEl);
|
||||
|
||||
// Build predefined size buttons
|
||||
var sizeContainer = document.getElementById('sisMediaSizeButtons');
|
||||
if (sizeContainer) {
|
||||
PREDEFINED_SIZES.forEach(function (size) {
|
||||
var btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = 'btn btn-xs btn-outline-secondary mb-1';
|
||||
btn.style.fontSize = '10px';
|
||||
btn.style.padding = '1px 6px';
|
||||
btn.style.marginRight = '3px';
|
||||
btn.innerText = size.label;
|
||||
btn.addEventListener('click', function () {
|
||||
document.getElementById('sisMediaConfigWidth').value = size.width;
|
||||
document.getElementById('sisMediaConfigHeight').value = size.height;
|
||||
sizeContainer.querySelectorAll('button').forEach(function (b) {
|
||||
b.classList.remove('btn-secondary');
|
||||
b.classList.add('btn-outline-secondary');
|
||||
});
|
||||
btn.classList.remove('btn-outline-secondary');
|
||||
btn.classList.add('btn-secondary');
|
||||
});
|
||||
sizeContainer.appendChild(btn);
|
||||
});
|
||||
}
|
||||
|
||||
// Bind Search Input Handler
|
||||
var self = this;
|
||||
document.addEventListener('input', function (e) {
|
||||
if (e.target && e.target.id === 'sisMediaSearchInput') {
|
||||
var term = e.target.value.toLowerCase().trim();
|
||||
var filtered = self.mediaData.filter(function (item) {
|
||||
var name = (item.originalFilename || item.name || '').toLowerCase();
|
||||
return name.includes(term);
|
||||
});
|
||||
self.renderGrid(filtered);
|
||||
}
|
||||
});
|
||||
|
||||
// Bind Upload Input Handler
|
||||
document.addEventListener('change', function (e) {
|
||||
if (e.target && e.target.id === 'sisMediaUploadInput') {
|
||||
var file = e.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
var formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
var grid = document.getElementById('sisMediaGrid');
|
||||
if (grid) {
|
||||
grid.innerHTML = '<div class="col-12 text-center text-info py-5"><i class="fas fa-spinner fa-spin fa-2x"></i><br/><span class="mt-2 d-block">Đang tải tệp lên server...</span></div>';
|
||||
}
|
||||
|
||||
fetch('/api/manage/media/upload', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(function (res) { return res.json(); })
|
||||
.then(function (data) {
|
||||
if (data && data.success === 1 && data.file && data.file.url) {
|
||||
self.showConfigPanel(data.file.url, data.file);
|
||||
} else {
|
||||
alert('Tải tệp lên thất bại. Vui lòng thử lại!');
|
||||
self.fetchMediaList();
|
||||
}
|
||||
})
|
||||
.catch(function (err) {
|
||||
alert('Lỗi tải tệp: ' + err.message);
|
||||
self.fetchMediaList();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Config panel Cancel button
|
||||
document.addEventListener('click', function (e) {
|
||||
if (e.target && e.target.id === 'sisMediaConfigCancel') {
|
||||
self.hideConfigPanel();
|
||||
}
|
||||
if (e.target && e.target.id === 'sisMediaConfigConfirm') {
|
||||
self.confirmWithConfig();
|
||||
}
|
||||
});
|
||||
|
||||
return modalEl;
|
||||
},
|
||||
|
||||
open: function (callback) {
|
||||
this.callback = callback;
|
||||
var modalEl = this.initModal();
|
||||
|
||||
if (typeof $ !== 'undefined' && $.fn && $.fn.modal) {
|
||||
$(modalEl).modal('show');
|
||||
} else {
|
||||
modalEl.style.display = 'block';
|
||||
modalEl.classList.add('show');
|
||||
}
|
||||
this.fetchMediaList();
|
||||
},
|
||||
|
||||
fetchMediaList: function () {
|
||||
var grid = document.getElementById('sisMediaGrid');
|
||||
if (grid) {
|
||||
grid.innerHTML = '<div class="col-12 text-center text-muted py-5"><i class="fas fa-spinner fa-spin fa-2x text-primary"></i><br/><span class="mt-2 d-block small">Đang nạp thư viện...</span></div>';
|
||||
}
|
||||
this.hideConfigPanel();
|
||||
|
||||
var self = this;
|
||||
fetch('/api/manage/media/list')
|
||||
.then(function (res) { return res.json(); })
|
||||
.then(function (data) {
|
||||
self.mediaData = data || [];
|
||||
self.renderGrid(self.mediaData);
|
||||
})
|
||||
.catch(function (err) {
|
||||
if (grid) {
|
||||
grid.innerHTML = '<div class="col-12 text-center text-danger py-4"><i class="fas fa-exclamation-triangle fa-2x mb-2"></i><br/>Không thể tải danh sách tệp. Bạn có thể dán đường dẫn URL trực tiếp.</div>';
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
renderGrid: function (list) {
|
||||
var grid = document.getElementById('sisMediaGrid');
|
||||
if (!grid) return;
|
||||
|
||||
if (!list || list.length === 0) {
|
||||
grid.innerHTML = '<div class="col-12 text-center text-muted py-5"><i class="far fa-folder-open fa-2x mb-2"></i><br/>Chưa có tệp hình ảnh nào. Bấm nút "Tải ảnh mới lên" để thêm.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
var self = this;
|
||||
grid.innerHTML = list.map(function (item) {
|
||||
var url = item.fileUrl || item.url || '';
|
||||
var name = item.originalFilename || item.name || 'Image';
|
||||
return '<div class="col-lg-2 col-md-3 col-sm-4 col-6 mb-3">' +
|
||||
'<div class="card h-100 border shadow-sm media-select-card" style="cursor: pointer; transition: all 0.2s;" data-url="' + url + '" data-name="' + name + '">' +
|
||||
'<div style="height: 110px; background: #f8f9fc; display: flex; align-items: center; justify-content: center; overflow: hidden;" class="p-1">' +
|
||||
'<img src="' + url + '" style="max-height: 100%; max-width: 100%; object-fit: contain;" alt="' + name + '" />' +
|
||||
'</div>' +
|
||||
'<div class="card-body p-2 text-center bg-white border-top">' +
|
||||
'<p class="card-text small text-truncate m-0 font-weight-bold text-dark" title="' + name + '">' + name + '</p>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}).join('');
|
||||
|
||||
// Add click listeners to items
|
||||
grid.querySelectorAll('.media-select-card').forEach(function (card) {
|
||||
card.addEventListener('click', function () {
|
||||
var selectedUrl = card.getAttribute('data-url');
|
||||
var selectedName = card.getAttribute('data-name');
|
||||
// Highlight selected
|
||||
grid.querySelectorAll('.media-select-card').forEach(function (c) {
|
||||
c.style.outline = '';
|
||||
});
|
||||
card.style.outline = '3px solid #007bff';
|
||||
self.showConfigPanel(selectedUrl, { name: selectedName, url: selectedUrl });
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
showConfigPanel: function (url, mediaObj) {
|
||||
this.selectedUrl = url;
|
||||
this.selectedMediaObj = mediaObj || {};
|
||||
|
||||
var panel = document.getElementById(this.configPanelId);
|
||||
if (!panel) return;
|
||||
|
||||
// Populate preview
|
||||
var previewImg = document.getElementById('sisMediaConfigPreview');
|
||||
if (previewImg) previewImg.src = url;
|
||||
|
||||
var nameEl = document.getElementById('sisMediaConfigName');
|
||||
if (nameEl) nameEl.textContent = mediaObj && (mediaObj.originalFilename || mediaObj.name) ? (mediaObj.originalFilename || mediaObj.name) : '';
|
||||
|
||||
// Reset all config fields
|
||||
var fields = ['sisMediaConfigWidth', 'sisMediaConfigHeight', 'sisMediaConfigStyle', 'sisMediaConfigClass', 'sisMediaConfigAlt', 'sisMediaConfigAttrs'];
|
||||
var selectFields = ['sisMediaConfigObjectFit', 'sisMediaConfigAspectRatio'];
|
||||
selectFields.forEach(function (id) { var el = document.getElementById(id); if (el) el.value = ''; });
|
||||
fields.forEach(function (id) {
|
||||
var el = document.getElementById(id);
|
||||
if (el) el.value = '';
|
||||
});
|
||||
var fitEl = document.getElementById('sisMediaConfigObjectFit');
|
||||
if (fitEl) fitEl.value = '';
|
||||
|
||||
// Reset size button highlights
|
||||
var sizeContainer = document.getElementById('sisMediaSizeButtons');
|
||||
if (sizeContainer) {
|
||||
sizeContainer.querySelectorAll('button').forEach(function (b) {
|
||||
b.classList.remove('btn-secondary');
|
||||
b.classList.add('btn-outline-secondary');
|
||||
});
|
||||
}
|
||||
|
||||
panel.style.display = 'block';
|
||||
panel.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
},
|
||||
|
||||
hideConfigPanel: function () {
|
||||
var panel = document.getElementById(this.configPanelId);
|
||||
if (panel) panel.style.display = 'none';
|
||||
this.selectedUrl = null;
|
||||
this.selectedMediaObj = null;
|
||||
},
|
||||
|
||||
confirmWithConfig: function () {
|
||||
if (!this.selectedUrl) return;
|
||||
|
||||
var width = (document.getElementById('sisMediaConfigWidth') || {}).value || '';
|
||||
var height = (document.getElementById('sisMediaConfigHeight') || {}).value || '';
|
||||
var objectFit = (document.getElementById('sisMediaConfigObjectFit') || {}).value || '';
|
||||
var aspectRatio = (document.getElementById('sisMediaConfigAspectRatio') || {}).value || '';
|
||||
var style = (document.getElementById('sisMediaConfigStyle') || {}).value || '';
|
||||
var cssClass = (document.getElementById('sisMediaConfigClass') || {}).value || '';
|
||||
var alt = (document.getElementById('sisMediaConfigAlt') || {}).value || '';
|
||||
var customAttrs = (document.getElementById('sisMediaConfigAttrs') || {}).value || '';
|
||||
|
||||
// Build composite style string from size + aspect-ratio + object-fit + inline style
|
||||
var computedStyle = '';
|
||||
if (width) computedStyle += 'width: ' + width + '; ';
|
||||
if (height) computedStyle += 'height: ' + height + '; ';
|
||||
if (aspectRatio) computedStyle += 'aspect-ratio: ' + aspectRatio + '; ';
|
||||
if (objectFit) computedStyle += 'object-fit: ' + objectFit + '; ';
|
||||
if (style) computedStyle += style;
|
||||
computedStyle = computedStyle.trim();
|
||||
|
||||
var config = {
|
||||
style: computedStyle,
|
||||
cssClass: cssClass,
|
||||
alt: alt,
|
||||
customAttributes: customAttrs,
|
||||
width: width,
|
||||
height: height,
|
||||
objectFit: objectFit,
|
||||
aspectRatio: aspectRatio
|
||||
};
|
||||
|
||||
if (typeof this.callback === 'function') {
|
||||
this.callback(this.selectedUrl, this.selectedMediaObj, config);
|
||||
}
|
||||
|
||||
// Close modal
|
||||
var modalEl = document.getElementById(this.modalId);
|
||||
if (modalEl) {
|
||||
if (typeof $ !== 'undefined' && $.fn && $.fn.modal) {
|
||||
$(modalEl).modal('hide');
|
||||
} else {
|
||||
modalEl.style.display = 'none';
|
||||
modalEl.classList.remove('show');
|
||||
}
|
||||
}
|
||||
this.hideConfigPanel();
|
||||
},
|
||||
|
||||
selectItem: function (url, mediaObj) {
|
||||
// Legacy direct select (no config panel) - used by upload flow or external callers
|
||||
if (typeof this.callback === 'function') {
|
||||
this.callback(url, mediaObj, {});
|
||||
}
|
||||
var modalEl = document.getElementById(this.modalId);
|
||||
if (modalEl) {
|
||||
if (typeof $ !== 'undefined' && $.fn && $.fn.modal) {
|
||||
$(modalEl).modal('hide');
|
||||
} else {
|
||||
modalEl.style.display = 'none';
|
||||
modalEl.classList.remove('show');
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Expose standalone module globally
|
||||
window.SISMediaPicker = SISMediaPicker;
|
||||
|
||||
// Backward-compatible global helper function
|
||||
window.openSISMediaModal = function (callback) {
|
||||
SISMediaPicker.open(callback);
|
||||
};
|
||||
|
||||
})(window, document);
|
||||
+1
File diff suppressed because one or more lines are too long
BIN
Binary file not shown.
+2
File diff suppressed because one or more lines are too long
BIN
Binary file not shown.
+201
@@ -0,0 +1,201 @@
|
||||
/**
|
||||
* SISTagTool — Custom HTML Tag Opening, Closing & Wrapper Block Tool for Editor.js
|
||||
* Allows content managers to define custom HTML tags (div, section, article, header, footer, etc.)
|
||||
* with custom Class(es), ID, and Attributes (data-*, style, aria-*, etc.).
|
||||
*/
|
||||
class SISTagTool {
|
||||
static get toolbox() {
|
||||
return {
|
||||
title: 'HTML Tag (Open/Close)',
|
||||
icon: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M16 18l6-6-6-6M8 6l-6 6 6 6"></path></svg>'
|
||||
};
|
||||
}
|
||||
|
||||
constructor({ data, api, readOnly }) {
|
||||
this.api = api;
|
||||
this.readOnly = readOnly;
|
||||
this.data = {
|
||||
mode: data && data.mode ? data.mode : 'open', // 'open' or 'close'
|
||||
tagName: data && data.tagName ? data.tagName : 'div',
|
||||
elementId: data && data.elementId ? data.elementId : '',
|
||||
elementClass: data && data.elementClass ? data.elementClass : '',
|
||||
attributes: data && data.attributes ? data.attributes : ''
|
||||
};
|
||||
this.wrapper = undefined;
|
||||
}
|
||||
|
||||
render() {
|
||||
this.wrapper = document.createElement('div');
|
||||
this.wrapper.className = 'p-3 bg-light border rounded mb-3 ce-tag-tool-wrapper';
|
||||
this.wrapper.style.fontFamily = 'inherit';
|
||||
this.wrapper.style.borderLeft = '4px solid #007bff';
|
||||
|
||||
const headerDiv = document.createElement('div');
|
||||
headerDiv.className = 'font-weight-bold text-primary small mb-3 d-flex justify-content-between align-items-center';
|
||||
headerDiv.innerHTML = '<span><i class="fas fa-code"></i> HTML Custom Tag Settings</span><span class="badge badge-primary tag-preview-badge" style="font-family: monospace;"><div></span>';
|
||||
this.wrapper.appendChild(headerDiv);
|
||||
|
||||
const formRow = document.createElement('div');
|
||||
formRow.className = 'form-row';
|
||||
|
||||
// 1. Tag Mode (Opening Tag / Closing Tag)
|
||||
const modeDiv = document.createElement('div');
|
||||
modeDiv.className = 'col-md-3 mb-2';
|
||||
modeDiv.innerHTML = '<label class="small font-weight-bold text-secondary mb-1">Tag Action / Mode</label>';
|
||||
const modeSelect = document.createElement('select');
|
||||
modeSelect.className = 'form-control form-control-sm';
|
||||
|
||||
const optOpen = document.createElement('option');
|
||||
optOpen.value = 'open';
|
||||
optOpen.textContent = 'Opening Tag (<tag>)';
|
||||
if (this.data.mode === 'open') optOpen.selected = true;
|
||||
modeSelect.appendChild(optOpen);
|
||||
|
||||
const optClose = document.createElement('option');
|
||||
optClose.value = 'close';
|
||||
optClose.textContent = 'Closing Tag (</tag>)';
|
||||
if (this.data.mode === 'close') optClose.selected = true;
|
||||
modeSelect.appendChild(optClose);
|
||||
|
||||
if (this.readOnly) modeSelect.disabled = true;
|
||||
this.modeSelect = modeSelect;
|
||||
modeDiv.appendChild(modeSelect);
|
||||
formRow.appendChild(modeDiv);
|
||||
|
||||
// 2. Tag Name (div, section, article, header, footer, etc.)
|
||||
const nameDiv = document.createElement('div');
|
||||
nameDiv.className = 'col-md-3 mb-2';
|
||||
nameDiv.innerHTML = '<label class="small font-weight-bold text-secondary mb-1">Tag Name</label>';
|
||||
const nameInput = document.createElement('input');
|
||||
nameInput.type = 'text';
|
||||
nameInput.className = 'form-control form-control-sm';
|
||||
nameInput.placeholder = 'e.g. div, section, article';
|
||||
nameInput.value = this.data.tagName || 'div';
|
||||
if (this.readOnly) nameInput.disabled = true;
|
||||
this.nameInput = nameInput;
|
||||
nameDiv.appendChild(nameInput);
|
||||
formRow.appendChild(nameDiv);
|
||||
|
||||
// 3. Element ID
|
||||
const idDiv = document.createElement('div');
|
||||
idDiv.className = 'col-md-3 mb-2';
|
||||
idDiv.innerHTML = '<label class="small font-weight-bold text-secondary mb-1">ID (id="..." )</label>';
|
||||
const idInput = document.createElement('input');
|
||||
idInput.type = 'text';
|
||||
idInput.className = 'form-control form-control-sm';
|
||||
idInput.placeholder = 'e.g. heroSection';
|
||||
idInput.value = this.data.elementId || '';
|
||||
if (this.readOnly) idInput.disabled = true;
|
||||
this.idInput = idInput;
|
||||
idDiv.appendChild(idInput);
|
||||
formRow.appendChild(idDiv);
|
||||
|
||||
// 4. Element CSS Class(es)
|
||||
const classDiv = document.createElement('div');
|
||||
classDiv.className = 'col-md-3 mb-2';
|
||||
classDiv.innerHTML = '<label class="small font-weight-bold text-secondary mb-1">Class(es) (class="..." )</label>';
|
||||
const classInput = document.createElement('input');
|
||||
classInput.type = 'text';
|
||||
classInput.className = 'form-control form-control-sm';
|
||||
classInput.placeholder = 'e.g. container py-5 my-section';
|
||||
classInput.value = this.data.elementClass || '';
|
||||
if (this.readOnly) classInput.disabled = true;
|
||||
this.classInput = classInput;
|
||||
classDiv.appendChild(classInput);
|
||||
formRow.appendChild(classDiv);
|
||||
|
||||
// 5. Custom Attributes Row
|
||||
const attrRow = document.createElement('div');
|
||||
attrRow.className = 'form-row mt-2 attr-row';
|
||||
const attrDiv = document.createElement('div');
|
||||
attrDiv.className = 'col-12';
|
||||
attrDiv.innerHTML = '<label class="small font-weight-bold text-secondary mb-1"><i class="fas fa-sliders-h"></i> Custom Attributes (e.g. data-aos="fade-up" style="background:#fff")</label>';
|
||||
const attrInput = document.createElement('input');
|
||||
attrInput.type = 'text';
|
||||
attrInput.className = 'form-control form-control-sm';
|
||||
attrInput.placeholder = 'e.g. data-aos="fade-up" style="background: #f8f9fa;" aria-label="Section"';
|
||||
attrInput.value = this.data.attributes || '';
|
||||
if (this.readOnly) attrInput.disabled = true;
|
||||
this.attrInput = attrInput;
|
||||
attrDiv.appendChild(attrInput);
|
||||
attrRow.appendChild(attrDiv);
|
||||
|
||||
// Live HTML Code Preview Box
|
||||
const previewBox = document.createElement('div');
|
||||
previewBox.className = 'mt-3 p-2 bg-dark text-success rounded small font-weight-bold text-monospace live-tag-preview';
|
||||
previewBox.style.fontSize = '12px';
|
||||
|
||||
const updatePreview = () => {
|
||||
const mode = this.modeSelect.value;
|
||||
const tag = (this.nameInput.value.trim() || 'div').toLowerCase();
|
||||
const idVal = this.idInput.value.trim();
|
||||
const classVal = this.classInput.value.trim();
|
||||
const attrVal = this.attrInput.value.trim();
|
||||
|
||||
if (mode === 'close') {
|
||||
previewBox.textContent = `</${tag}>`;
|
||||
attrRow.style.display = 'none';
|
||||
idDiv.style.display = 'none';
|
||||
classDiv.style.display = 'none';
|
||||
} else {
|
||||
attrRow.style.display = 'flex';
|
||||
idDiv.style.display = 'block';
|
||||
classDiv.style.display = 'block';
|
||||
let tagStr = `<${tag}`;
|
||||
if (idVal) tagStr += ` id="${idVal}"`;
|
||||
if (classVal) tagStr += ` class="${classVal}"`;
|
||||
if (attrVal) tagStr += ` ${attrVal}`;
|
||||
tagStr += `>`;
|
||||
previewBox.textContent = tagStr;
|
||||
}
|
||||
|
||||
const previewBadge = this.wrapper.querySelector('.tag-preview-badge');
|
||||
if (previewBadge) previewBadge.textContent = previewBox.textContent;
|
||||
};
|
||||
|
||||
this.modeSelect.addEventListener('change', updatePreview);
|
||||
this.nameInput.addEventListener('input', updatePreview);
|
||||
this.idInput.addEventListener('input', updatePreview);
|
||||
this.classInput.addEventListener('input', updatePreview);
|
||||
this.attrInput.addEventListener('input', updatePreview);
|
||||
|
||||
this.wrapper.appendChild(formRow);
|
||||
this.wrapper.appendChild(attrRow);
|
||||
this.wrapper.appendChild(previewBox);
|
||||
|
||||
updatePreview();
|
||||
return this.wrapper;
|
||||
}
|
||||
|
||||
save() {
|
||||
const mode = this.modeSelect ? this.modeSelect.value : (this.data.mode || 'open');
|
||||
const tagName = this.nameInput ? (this.nameInput.value.trim().toLowerCase() || 'div') : (this.data.tagName || 'div');
|
||||
const elementId = this.idInput ? this.idInput.value.trim() : (this.data.elementId || '');
|
||||
const elementClass = this.classInput ? this.classInput.value.trim() : (this.data.elementClass || '');
|
||||
const attributes = this.attrInput ? this.attrInput.value.trim() : (this.data.attributes || '');
|
||||
|
||||
let rawHtmlTag = '';
|
||||
if (mode === 'close') {
|
||||
rawHtmlTag = `</${tagName}>`;
|
||||
} else {
|
||||
rawHtmlTag = `<${tagName}`;
|
||||
if (elementId) rawHtmlTag += ` id="${elementId}"`;
|
||||
if (elementClass) rawHtmlTag += ` class="${elementClass}"`;
|
||||
if (attributes) rawHtmlTag += ` ${attributes}`;
|
||||
rawHtmlTag += `>`;
|
||||
}
|
||||
|
||||
return {
|
||||
mode: mode,
|
||||
tagName: tagName,
|
||||
elementId: elementId,
|
||||
elementClass: elementClass,
|
||||
attributes: attributes,
|
||||
rawHtmlTag: rawHtmlTag
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Register the plugin globally
|
||||
window.SISEditorPlugins = window.SISEditorPlugins || {};
|
||||
window.SISEditorPlugins['tag'] = { class: SISTagTool };
|
||||
+182
@@ -0,0 +1,182 @@
|
||||
/**
|
||||
* TextStylingTune — Block Tune for Editor.js
|
||||
* Adds alignment, custom CSS class, custom CSS style, and HTML ID settings to blocks.
|
||||
*/
|
||||
class TextStylingTune {
|
||||
constructor({ api, block, data }) {
|
||||
this.api = api;
|
||||
this.block = block;
|
||||
this.data = data || {
|
||||
alignment: '',
|
||||
cssClass: '',
|
||||
customStyle: '',
|
||||
elementId: '',
|
||||
customAttrs: ''
|
||||
};
|
||||
}
|
||||
|
||||
static get isTune() {
|
||||
return true;
|
||||
}
|
||||
|
||||
render() {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'cdx-settings-text-styling p-2 border-top';
|
||||
wrapper.style.fontFamily = 'inherit';
|
||||
|
||||
const title = document.createElement('div');
|
||||
title.className = 'small font-weight-bold text-primary mb-2';
|
||||
title.innerHTML = '<i class="fas fa-palette"></i> Style & Alignment';
|
||||
wrapper.appendChild(title);
|
||||
|
||||
// Alignment row
|
||||
const alignWrapper = document.createElement('div');
|
||||
alignWrapper.className = 'd-flex mb-2 align-items-center justify-content-start';
|
||||
|
||||
const alignments = [
|
||||
{ name: 'left', icon: '<i class="fas fa-align-left"></i>', title: 'Align Left' },
|
||||
{ name: 'center', icon: '<i class="fas fa-align-center"></i>', title: 'Align Center' },
|
||||
{ name: 'right', icon: '<i class="fas fa-align-right"></i>', title: 'Align Right' },
|
||||
{ name: 'justify', icon: '<i class="fas fa-align-justify"></i>', title: 'Align Justify' }
|
||||
];
|
||||
|
||||
alignments.forEach(align => {
|
||||
const btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = 'btn btn-xs btn-light mr-1 border';
|
||||
btn.style.width = '30px';
|
||||
btn.style.height = '30px';
|
||||
btn.style.padding = '0';
|
||||
btn.title = align.title;
|
||||
if (this.data.alignment === align.name) {
|
||||
btn.className = 'btn btn-xs btn-info mr-1 border';
|
||||
}
|
||||
btn.innerHTML = align.icon;
|
||||
btn.addEventListener('click', () => {
|
||||
this.data.alignment = this.data.alignment === align.name ? '' : align.name;
|
||||
alignWrapper.querySelectorAll('button').forEach((b, idx) => {
|
||||
b.className = (this.data.alignment === alignments[idx].name) ? 'btn btn-xs btn-info mr-1 border' : 'btn btn-xs btn-light mr-1 border';
|
||||
});
|
||||
this.applyTuneToBlock();
|
||||
});
|
||||
alignWrapper.appendChild(btn);
|
||||
});
|
||||
wrapper.appendChild(alignWrapper);
|
||||
|
||||
// CSS Class Group
|
||||
const classGroup = document.createElement('div');
|
||||
classGroup.className = 'form-group mb-1';
|
||||
const classLabel = document.createElement('label');
|
||||
classLabel.className = 'small font-weight-bold text-secondary mb-0 d-block';
|
||||
classLabel.innerText = 'CSS Class';
|
||||
const classInput = document.createElement('input');
|
||||
classInput.type = 'text';
|
||||
classInput.className = 'form-control form-control-sm py-0';
|
||||
classInput.style.height = '24px';
|
||||
classInput.placeholder = 'e.g. lead text-primary font-weight-bold';
|
||||
classInput.value = this.data.cssClass || '';
|
||||
classInput.addEventListener('input', (e) => {
|
||||
this.data.cssClass = e.target.value.trim();
|
||||
this.applyTuneToBlock();
|
||||
});
|
||||
classGroup.appendChild(classLabel);
|
||||
classGroup.appendChild(classInput);
|
||||
wrapper.appendChild(classGroup);
|
||||
|
||||
// Custom CSS Style Group
|
||||
const styleGroup = document.createElement('div');
|
||||
styleGroup.className = 'form-group mb-1';
|
||||
const styleLabel = document.createElement('label');
|
||||
styleLabel.className = 'small font-weight-bold text-secondary mb-0 d-block';
|
||||
styleLabel.innerText = 'CSS Style';
|
||||
const styleInput = document.createElement('input');
|
||||
styleInput.type = 'text';
|
||||
styleInput.className = 'form-control form-control-sm py-0';
|
||||
styleInput.style.height = '24px';
|
||||
styleInput.placeholder = 'e.g. color: #dc3545; font-size: 1.5rem;';
|
||||
styleInput.value = this.data.customStyle || '';
|
||||
styleInput.addEventListener('input', (e) => {
|
||||
this.data.customStyle = e.target.value.trim();
|
||||
this.applyTuneToBlock();
|
||||
});
|
||||
styleGroup.appendChild(styleLabel);
|
||||
styleGroup.appendChild(styleInput);
|
||||
wrapper.appendChild(styleGroup);
|
||||
|
||||
// HTML ID Group
|
||||
const idGroup = document.createElement('div');
|
||||
idGroup.className = 'form-group mb-1';
|
||||
const idLabel = document.createElement('label');
|
||||
idLabel.className = 'small font-weight-bold text-secondary mb-0 d-block';
|
||||
idLabel.innerText = 'Element ID';
|
||||
const idInput = document.createElement('input');
|
||||
idInput.type = 'text';
|
||||
idInput.className = 'form-control form-control-sm py-0';
|
||||
idInput.style.height = '24px';
|
||||
idInput.placeholder = 'e.g. section-main-title';
|
||||
idInput.value = this.data.elementId || '';
|
||||
idInput.addEventListener('input', (e) => {
|
||||
this.data.elementId = e.target.value.trim();
|
||||
this.applyTuneToBlock();
|
||||
});
|
||||
idGroup.appendChild(idLabel);
|
||||
idGroup.appendChild(idInput);
|
||||
wrapper.appendChild(idGroup);
|
||||
|
||||
// Custom HTML Attributes Group
|
||||
const attrsGroup = document.createElement('div');
|
||||
attrsGroup.className = 'form-group mb-0';
|
||||
const attrsLabel = document.createElement('label');
|
||||
attrsLabel.className = 'small font-weight-bold text-secondary mb-0 d-block';
|
||||
attrsLabel.innerText = 'Custom Attributes';
|
||||
const attrsInput = document.createElement('input');
|
||||
attrsInput.type = 'text';
|
||||
attrsInput.className = 'form-control form-control-sm py-0';
|
||||
attrsInput.style.height = '24px';
|
||||
attrsInput.placeholder = 'e.g. data-aos="fade-up" target="_blank"';
|
||||
attrsInput.value = this.data.customAttrs || '';
|
||||
attrsInput.addEventListener('input', (e) => {
|
||||
this.data.customAttrs = e.target.value.trim();
|
||||
});
|
||||
attrsGroup.appendChild(attrsLabel);
|
||||
attrsGroup.appendChild(attrsInput);
|
||||
wrapper.appendChild(attrsGroup);
|
||||
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
applyTuneToBlock() {
|
||||
if (!this.block || !this.block.holder) return;
|
||||
const blockContent = this.block.holder.querySelector('.ce-block__content');
|
||||
if (blockContent) {
|
||||
// Live alignment preview
|
||||
blockContent.style.textAlign = this.data.alignment || '';
|
||||
// Live styling preview
|
||||
const currentElement = blockContent.firstElementChild;
|
||||
if (currentElement) {
|
||||
// Apply custom inline styles on top of defaults
|
||||
currentElement.style.cssText = this.data.customStyle || '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
save() {
|
||||
return this.data;
|
||||
}
|
||||
|
||||
wrap(blockContent) {
|
||||
// Applies styling during initial render / loading
|
||||
if (this.data.alignment) {
|
||||
blockContent.style.textAlign = this.data.alignment;
|
||||
}
|
||||
const currentElement = blockContent.firstElementChild;
|
||||
if (currentElement && this.data.customStyle) {
|
||||
currentElement.style.cssText = this.data.customStyle;
|
||||
}
|
||||
return blockContent;
|
||||
}
|
||||
}
|
||||
|
||||
// Register globally
|
||||
window.SISEditorPlugins = window.SISEditorPlugins || {};
|
||||
window.SISEditorPlugins['textStyling'] = { class: TextStylingTune };
|
||||
+338
@@ -0,0 +1,338 @@
|
||||
class SISTimelineTool {
|
||||
static get toolbox() {
|
||||
return {
|
||||
title: 'Timeline Group',
|
||||
icon: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>'
|
||||
};
|
||||
}
|
||||
|
||||
constructor({ data, api, readOnly }) {
|
||||
this.api = api;
|
||||
this.readOnly = readOnly;
|
||||
this.data = {
|
||||
globalAttributes: (data && data.globalAttributes) || (data && data.attributes) || '',
|
||||
items: (data && data.items && Array.isArray(data.items)) ? data.items : []
|
||||
};
|
||||
// Add an initial empty item if list is empty
|
||||
if (this.data.items.length === 0) {
|
||||
this.data.items.push({ date: '', description: '', imageUrl: '' });
|
||||
}
|
||||
this.wrapper = undefined;
|
||||
}
|
||||
|
||||
render() {
|
||||
this.wrapper = document.createElement('div');
|
||||
this.wrapper.className = 'timeline-tool-wrapper';
|
||||
this.wrapper.style.border = '1px solid #e3e6f0';
|
||||
this.wrapper.style.borderRadius = '8px';
|
||||
this.wrapper.style.padding = '14px';
|
||||
this.wrapper.style.background = '#f8f9fc';
|
||||
this.wrapper.style.marginBottom = '12px';
|
||||
|
||||
const headerLabel = document.createElement('label');
|
||||
headerLabel.className = 'font-weight-bold text-primary small mb-2 d-block';
|
||||
headerLabel.innerHTML = '<i class="fas fa-stream"></i> Timeline Group Settings';
|
||||
this.wrapper.appendChild(headerLabel);
|
||||
|
||||
const globalAttrGroup = this._createInput('e.g. data-aos="fade-up" style="..."', this.data.globalAttributes, 'global-attr', '<i class="fas fa-sliders-h"></i> Global Custom Attributes');
|
||||
this.wrapper.appendChild(globalAttrGroup);
|
||||
|
||||
this.itemsContainer = document.createElement('div');
|
||||
this.wrapper.appendChild(this.itemsContainer);
|
||||
|
||||
this.data.items.forEach((item, index) => {
|
||||
this._renderItem(item, index);
|
||||
});
|
||||
|
||||
if (!this.readOnly) {
|
||||
const addButton = document.createElement('button');
|
||||
addButton.type = 'button';
|
||||
addButton.className = 'btn btn-sm btn-outline-primary mt-2';
|
||||
addButton.innerHTML = '<i class="fas fa-plus"></i> Add Event';
|
||||
addButton.addEventListener('click', () => {
|
||||
const newItem = { date: '', description: '', imageUrl: '' };
|
||||
this.data.items.push(newItem);
|
||||
this._renderItem(newItem, this.data.items.length - 1);
|
||||
});
|
||||
this.wrapper.appendChild(addButton);
|
||||
}
|
||||
|
||||
return this.wrapper;
|
||||
}
|
||||
|
||||
_renderItem(item, index) {
|
||||
const itemBox = document.createElement('div');
|
||||
itemBox.className = 'timeline-item-edit-box mb-3 p-3 bg-white border rounded';
|
||||
itemBox.style.position = 'relative';
|
||||
|
||||
if (!this.readOnly) {
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.type = 'button';
|
||||
removeBtn.className = 'btn btn-sm btn-danger';
|
||||
removeBtn.style.position = 'absolute';
|
||||
removeBtn.style.top = '10px';
|
||||
removeBtn.style.right = '10px';
|
||||
removeBtn.innerHTML = '<i class="fas fa-times"></i>';
|
||||
removeBtn.addEventListener('click', () => {
|
||||
itemBox.remove();
|
||||
});
|
||||
itemBox.appendChild(removeBtn);
|
||||
}
|
||||
|
||||
const dateGroup = this._createInput('Date (e.g. 10/04/1994)', item.date, 'date', '<i class="fas fa-calendar-alt"></i> Date / Milestone');
|
||||
const descGroup = this._createTextarea('Description (e.g. Bệnh viện khai trương)', item.description, 'desc', '<i class="fas fa-align-left"></i> Description');
|
||||
|
||||
// Image input with preview, upload, and media library buttons
|
||||
const imageGroup = document.createElement('div');
|
||||
imageGroup.className = 'form-group mb-2';
|
||||
|
||||
const imageLabel = document.createElement('label');
|
||||
imageLabel.className = 'small font-weight-bold text-secondary mb-1 d-block';
|
||||
imageLabel.innerHTML = '<i class="fas fa-image"></i> Image URL (Optional)';
|
||||
|
||||
const inputRow = document.createElement('div');
|
||||
inputRow.className = 'input-group input-group-sm mb-2';
|
||||
|
||||
const imageInput = document.createElement('input');
|
||||
imageInput.type = 'text';
|
||||
imageInput.className = 'form-control ce-timeline-input-image';
|
||||
imageInput.placeholder = 'Paste image URL here...';
|
||||
imageInput.value = item.imageUrl || '';
|
||||
if (this.readOnly) imageInput.disabled = true;
|
||||
|
||||
const appendDiv = document.createElement('div');
|
||||
appendDiv.className = 'input-group-append';
|
||||
|
||||
const chooseBtn = document.createElement('button');
|
||||
chooseBtn.type = 'button';
|
||||
chooseBtn.className = 'btn btn-info';
|
||||
chooseBtn.innerHTML = '<i class="fas fa-folder-open"></i> Choose';
|
||||
if (this.readOnly) chooseBtn.disabled = true;
|
||||
|
||||
const uploadBtn = document.createElement('button');
|
||||
uploadBtn.type = 'button';
|
||||
uploadBtn.className = 'btn btn-primary';
|
||||
uploadBtn.innerHTML = '<i class="fas fa-upload"></i> Upload';
|
||||
if (this.readOnly) uploadBtn.disabled = true;
|
||||
|
||||
// Hidden file input for uploading
|
||||
const fileInput = document.createElement('input');
|
||||
fileInput.type = 'file';
|
||||
fileInput.accept = 'image/*';
|
||||
fileInput.style.display = 'none';
|
||||
|
||||
chooseBtn.addEventListener('click', () => {
|
||||
if (window.SISMediaPicker) {
|
||||
SISMediaPicker.open((url, mediaObj, config) => {
|
||||
imageInput.value = url;
|
||||
// Store config in hidden inputs on the item box
|
||||
let styleInp = itemBox.querySelector('.ce-timeline-input-imgstyle');
|
||||
if (!styleInp) {
|
||||
styleInp = document.createElement('input'); styleInp.type = 'hidden';
|
||||
styleInp.className = 'ce-timeline-input-imgstyle'; itemBox.appendChild(styleInp);
|
||||
}
|
||||
let classInp = itemBox.querySelector('.ce-timeline-input-imgclass');
|
||||
if (!classInp) {
|
||||
classInp = document.createElement('input'); classInp.type = 'hidden';
|
||||
classInp.className = 'ce-timeline-input-imgclass'; itemBox.appendChild(classInp);
|
||||
}
|
||||
let altInp = itemBox.querySelector('.ce-timeline-input-imgalt');
|
||||
if (!altInp) {
|
||||
altInp = document.createElement('input'); altInp.type = 'hidden';
|
||||
altInp.className = 'ce-timeline-input-imgalt'; itemBox.appendChild(altInp);
|
||||
}
|
||||
let attrsInp = itemBox.querySelector('.ce-timeline-input-imgattrs');
|
||||
if (!attrsInp) {
|
||||
attrsInp = document.createElement('input'); attrsInp.type = 'hidden';
|
||||
attrsInp.className = 'ce-timeline-input-imgattrs'; itemBox.appendChild(attrsInp);
|
||||
}
|
||||
let arInp = itemBox.querySelector('.ce-timeline-input-imgar');
|
||||
if (!arInp) {
|
||||
arInp = document.createElement('input'); arInp.type = 'hidden';
|
||||
arInp.className = 'ce-timeline-input-imgar'; itemBox.appendChild(arInp);
|
||||
}
|
||||
if (config) {
|
||||
styleInp.value = config.style || '';
|
||||
classInp.value = config.cssClass || '';
|
||||
altInp.value = config.alt || '';
|
||||
attrsInp.value = config.customAttributes || '';
|
||||
arInp.value = config.aspectRatio || '';
|
||||
}
|
||||
updatePreview();
|
||||
});
|
||||
} else {
|
||||
alert('SISMediaPicker is not loaded.');
|
||||
}
|
||||
});
|
||||
|
||||
uploadBtn.addEventListener('click', () => {
|
||||
fileInput.click();
|
||||
});
|
||||
|
||||
const updatePreview = () => {
|
||||
const url = imageInput.value.trim();
|
||||
if (url) {
|
||||
imagePreview.style.backgroundImage = 'url("' + url + '")';
|
||||
imagePreview.style.display = 'block';
|
||||
} else {
|
||||
imagePreview.style.backgroundImage = 'none';
|
||||
imagePreview.style.display = 'none';
|
||||
}
|
||||
};
|
||||
|
||||
fileInput.addEventListener('change', (e) => {
|
||||
if (e.target.files && e.target.files[0]) {
|
||||
const formData = new FormData();
|
||||
formData.append('file', e.target.files[0]);
|
||||
|
||||
uploadBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
|
||||
uploadBtn.disabled = true;
|
||||
|
||||
fetch('/api/manage/media/upload', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success === 1 && data.file && data.file.url) {
|
||||
imageInput.value = data.file.url;
|
||||
updatePreview();
|
||||
} else {
|
||||
alert('Upload failed: ' + (data.message || 'Unknown error'));
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Error uploading image', err);
|
||||
alert('Upload failed: ' + err.message);
|
||||
})
|
||||
.finally(() => {
|
||||
uploadBtn.innerHTML = '<i class="fas fa-upload"></i> Upload';
|
||||
uploadBtn.disabled = false;
|
||||
fileInput.value = ''; // Reset
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
appendDiv.appendChild(chooseBtn);
|
||||
appendDiv.appendChild(uploadBtn);
|
||||
inputRow.appendChild(imageInput);
|
||||
inputRow.appendChild(appendDiv);
|
||||
inputRow.appendChild(fileInput);
|
||||
|
||||
// Image Preview Container
|
||||
const imagePreview = document.createElement('div');
|
||||
imagePreview.style.width = '100%';
|
||||
imagePreview.style.height = '180px';
|
||||
imagePreview.style.backgroundColor = '#f8f9fc';
|
||||
imagePreview.style.border = '1px dashed #d1d3e2';
|
||||
imagePreview.style.borderRadius = '6px';
|
||||
imagePreview.style.backgroundSize = 'cover';
|
||||
imagePreview.style.backgroundPosition = 'center';
|
||||
imagePreview.style.display = 'none';
|
||||
|
||||
imageInput.addEventListener('input', updatePreview);
|
||||
updatePreview();
|
||||
|
||||
imageGroup.appendChild(imageLabel);
|
||||
imageGroup.appendChild(inputRow);
|
||||
imageGroup.appendChild(imagePreview);
|
||||
|
||||
const attrGroup = this._createInput('e.g. data-aos="fade-right" style="..."', item.attributes, 'attr', '<i class="fas fa-sliders-h"></i> Step Custom Attributes');
|
||||
|
||||
itemBox.appendChild(dateGroup);
|
||||
itemBox.appendChild(descGroup);
|
||||
itemBox.appendChild(imageGroup);
|
||||
itemBox.appendChild(attrGroup);
|
||||
|
||||
this.itemsContainer.appendChild(itemBox);
|
||||
}
|
||||
|
||||
_createInput(placeholder, value, type, labelHtml) {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'form-group mb-2';
|
||||
|
||||
if (labelHtml) {
|
||||
const label = document.createElement('label');
|
||||
label.className = 'small font-weight-bold text-secondary mb-1 d-block';
|
||||
label.innerHTML = labelHtml;
|
||||
wrapper.appendChild(label);
|
||||
}
|
||||
|
||||
const inp = document.createElement('input');
|
||||
inp.type = 'text';
|
||||
inp.className = 'form-control form-control-sm ce-timeline-input-' + type;
|
||||
inp.placeholder = placeholder;
|
||||
inp.value = value || '';
|
||||
if (this.readOnly) inp.disabled = true;
|
||||
|
||||
wrapper.appendChild(inp);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
_createTextarea(placeholder, value, type, labelHtml) {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'form-group mb-2';
|
||||
|
||||
if (labelHtml) {
|
||||
const label = document.createElement('label');
|
||||
label.className = 'small font-weight-bold text-secondary mb-1 d-block';
|
||||
label.innerHTML = labelHtml;
|
||||
wrapper.appendChild(label);
|
||||
}
|
||||
|
||||
const inp = document.createElement('textarea');
|
||||
inp.className = 'form-control form-control-sm ce-timeline-input-' + type;
|
||||
inp.placeholder = placeholder;
|
||||
inp.value = value || '';
|
||||
inp.style.minHeight = '70px';
|
||||
if (this.readOnly) inp.disabled = true;
|
||||
|
||||
wrapper.appendChild(inp);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
save(blockContent) {
|
||||
const itemBoxes = this.itemsContainer.querySelectorAll('.timeline-item-edit-box');
|
||||
const items = [];
|
||||
itemBoxes.forEach(box => {
|
||||
const date = box.querySelector('.ce-timeline-input-date').value.trim();
|
||||
const desc = box.querySelector('.ce-timeline-input-desc').value.trim();
|
||||
const img = box.querySelector('.ce-timeline-input-image').value.trim();
|
||||
const attrEl = box.querySelector('.ce-timeline-input-attr');
|
||||
const attr = attrEl ? attrEl.value.trim() : '';
|
||||
const imgStyle = (box.querySelector('.ce-timeline-input-imgstyle') || {}).value || '';
|
||||
const imgClass = (box.querySelector('.ce-timeline-input-imgclass') || {}).value || '';
|
||||
const imgAlt = (box.querySelector('.ce-timeline-input-imgalt') || {}).value || '';
|
||||
const imgAttrs = (box.querySelector('.ce-timeline-input-imgattrs') || {}).value || '';
|
||||
const imgAspectRatio = (box.querySelector('.ce-timeline-input-imgar') || {}).value || '';
|
||||
if (date || desc || img || attr) {
|
||||
items.push({
|
||||
date: date, description: desc, imageUrl: img, attributes: attr,
|
||||
imageStyle: imgStyle, imageClass: imgClass, imageAlt: imgAlt,
|
||||
imageAttrs: imgAttrs, imageAspectRatio: imgAspectRatio
|
||||
});
|
||||
}
|
||||
});
|
||||
const globalAttrEl = this.wrapper.querySelector('.ce-timeline-input-global-attr');
|
||||
return {
|
||||
globalAttributes: globalAttrEl ? globalAttrEl.value.trim() : (this.data.globalAttributes || ''),
|
||||
items: items
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Register the plugin globally
|
||||
window.SISEditorPlugins = window.SISEditorPlugins || {};
|
||||
window.SISEditorPlugins['timeline'] = {
|
||||
class: SISTimelineTool
|
||||
};
|
||||
|
||||
// openMediaPickerModal — legacy stub, replaced by SISMediaPicker
|
||||
function openMediaPickerModal(onSelectCallback) {
|
||||
if (window.SISMediaPicker) {
|
||||
SISMediaPicker.open(function(url) { onSelectCallback(url); });
|
||||
} else {
|
||||
var url = prompt('Enter image URL:');
|
||||
if (url) onSelectCallback(url);
|
||||
}
|
||||
}
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
class SISTinyMceTool {
|
||||
static get toolbox() {
|
||||
return {
|
||||
title: 'TinyMCE Editor',
|
||||
icon: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 4h16v16H4z"></path><path d="M8 8h8"></path><path d="M12 8v8"></path><path d="M10 16h4"></path></svg>'
|
||||
};
|
||||
}
|
||||
|
||||
constructor({ data, api, readOnly }) {
|
||||
this.api = api;
|
||||
this.readOnly = readOnly;
|
||||
this.data = {
|
||||
html: data.html || ''
|
||||
};
|
||||
this.editorId = 'tinymce-editor-' + Math.random().toString(36).substring(7);
|
||||
this.editorInstance = null;
|
||||
}
|
||||
|
||||
render() {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'sis-tinymce-block-wrapper border rounded p-2 bg-white';
|
||||
wrapper.style.minHeight = '280px';
|
||||
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.id = this.editorId;
|
||||
textarea.value = this.data.html;
|
||||
textarea.style.width = '100%';
|
||||
textarea.style.minHeight = '250px';
|
||||
wrapper.appendChild(textarea);
|
||||
this.textareaElement = textarea;
|
||||
|
||||
this._initTinyMCE();
|
||||
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
_initTinyMCE() {
|
||||
const self = this;
|
||||
if (typeof tinymce !== 'undefined') {
|
||||
self._createEditor();
|
||||
return;
|
||||
}
|
||||
|
||||
// Global loading queue to prevent duplicate script tags
|
||||
window.SISTinyMceLoading = window.SISTinyMceLoading || {
|
||||
loaded: false,
|
||||
callbacks: []
|
||||
};
|
||||
|
||||
if (window.SISTinyMceLoading.loaded) {
|
||||
self._createEditor();
|
||||
return;
|
||||
}
|
||||
|
||||
window.SISTinyMceLoading.callbacks.push(() => {
|
||||
self._createEditor();
|
||||
});
|
||||
|
||||
if (window.SISTinyMceLoading.callbacks.length === 1) {
|
||||
const script = document.createElement('script');
|
||||
script.src = 'https://cdn.jsdelivr.net/npm/tinymce@6/tinymce.min.js';
|
||||
script.referrerPolicy = 'origin';
|
||||
script.onload = () => {
|
||||
window.SISTinyMceLoading.loaded = true;
|
||||
while (window.SISTinyMceLoading.callbacks.length > 0) {
|
||||
const cb = window.SISTinyMceLoading.callbacks.shift();
|
||||
try { cb(); } catch (e) { console.error(e); }
|
||||
}
|
||||
};
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
}
|
||||
|
||||
_createEditor() {
|
||||
const self = this;
|
||||
const textarea = this.textareaElement || document.getElementById(this.editorId);
|
||||
if (!textarea) return;
|
||||
|
||||
// Wait until the element is actually attached to the document DOM
|
||||
const checkAndInit = () => {
|
||||
if (!document.body.contains(textarea)) {
|
||||
requestAnimationFrame(checkAndInit);
|
||||
return;
|
||||
}
|
||||
|
||||
tinymce.init({
|
||||
target: textarea,
|
||||
height: 250,
|
||||
menubar: false,
|
||||
readonly: !!self.readOnly,
|
||||
plugins: 'advlist autolink lists link image charmap preview anchor searchreplace visualblocks code fullscreen insertdatetime media table code help wordcount',
|
||||
toolbar: 'undo redo | blocks | bold italic forecolor backcolor | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | table link image | code removeformat',
|
||||
setup: function(editor) {
|
||||
self.editorInstance = editor;
|
||||
editor.on('change keyup undo redo', function() {
|
||||
self.data.html = editor.getContent();
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
checkAndInit();
|
||||
}
|
||||
|
||||
save(blockContent) {
|
||||
if (this.editorInstance) {
|
||||
this.data.html = this.editorInstance.getContent();
|
||||
}
|
||||
return {
|
||||
html: this.data.html
|
||||
};
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this.editorInstance) {
|
||||
tinymce.remove(this.editorInstance);
|
||||
this.editorInstance = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Register globally
|
||||
window.SISEditorPlugins = window.SISEditorPlugins || {};
|
||||
window.SISEditorPlugins['tinymce'] = {
|
||||
class: SISTinyMceTool
|
||||
};
|
||||
})();
|
||||
+11
File diff suppressed because one or more lines are too long
BIN
Binary file not shown.
+38
@@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 25.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 108.3 108.3" style="enable-background:new 0 0 108.3 108.3;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#E6E6E6;}
|
||||
.st1{fill:#FFB8B8;}
|
||||
.st2{fill:#575A89;}
|
||||
.st3{fill:#2F2E41;}
|
||||
</style>
|
||||
<g id="Group_45" transform="translate(-191 -152.079)">
|
||||
<g id="Group_30" transform="translate(282.246 224.353)">
|
||||
<path id="Path_944" class="st0" d="M17.1-18.1c0,10.5-3,20.8-8.8,29.6c-1.2,1.9-2.5,3.6-4,5.3c-3.4,4-7.3,7.4-11.6,10.3
|
||||
c-1.2,0.8-2.4,1.5-3.6,2.2c-6.5,3.6-13.7,5.8-21,6.5c-1.7,0.2-3.4,0.2-5.1,0.2c-4.7,0-9.4-0.6-14-1.8c-2.6-0.7-5.1-1.6-7.6-2.6
|
||||
c-1.3-0.5-2.5-1.1-3.7-1.8c-2.9-1.5-5.6-3.3-8.2-5.3c-1.2-0.9-2.3-1.9-3.4-2.9C-95.8,1.3-97.1-33-76.8-54.9s54.6-23.3,76.5-2.9
|
||||
C10.8-47.6,17.1-33.2,17.1-18.1L17.1-18.1z"/>
|
||||
<path id="Path_945" class="st1" d="M-50.2-13.2c0,0,4.9,13.7,1.1,21.4s6,16.4,6,16.4s25.8-13.1,22.5-19.7s-8.8-15.3-7.7-20.8
|
||||
L-50.2-13.2z"/>
|
||||
<ellipse id="Ellipse_185" class="st1" cx="-40.6" cy="-25.5" rx="17.5" ry="17.5"/>
|
||||
<path id="Path_946" class="st2" d="M-51.1,34.2c-2.6-0.7-5.1-1.6-7.6-2.6l0.5-13.3l4.9-11c1.1,0.9,2.3,1.6,3.5,2.3
|
||||
c0.3,0.2,0.6,0.3,0.9,0.5c4.6,2.2,12.2,4.2,19.5-1.3c2.7-2.1,5-4.7,6.7-7.6L-8.8,9l0.7,8.4l0.8,9.8c-1.2,0.8-2.4,1.5-3.6,2.2
|
||||
c-6.5,3.6-13.7,5.8-21,6.5c-1.7,0.2-3.4,0.2-5.1,0.2C-41.8,36.1-46.5,35.4-51.1,34.2z"/>
|
||||
<path id="Path_947" class="st2" d="M-47.7-0.9L-47.7-0.9l-0.7,7.2l-0.4,3.8l-0.5,5.6l-1.8,18.5c-2.6-0.7-5.1-1.6-7.6-2.6
|
||||
c-1.3-0.5-2.5-1.1-3.7-1.8c-2.9-1.5-5.6-3.3-8.2-5.3l-1.9-9l0.1-0.1L-47.7-0.9z"/>
|
||||
<path id="Path_948" class="st2" d="M-10.9,29.3c-6.5,3.6-13.7,5.8-21,6.5c0.4-6.7,1-13.1,1.6-18.8c0.3-2.9,0.7-5.7,1.1-8.2
|
||||
c1.2-8,2.5-13.5,3.4-14.2l6.1,4L4.9,7.3l-0.5,9.5c-3.4,4-7.3,7.4-11.6,10.3C-8.5,27.9-9.7,28.7-10.9,29.3z"/>
|
||||
<path id="Path_949" class="st2" d="M-70.5,24.6c-1.2-0.9-2.3-1.9-3.4-2.9l0.9-6.1l0.7-0.1l3.1-0.4l6.8,14.8
|
||||
C-65.2,28.3-67.9,26.6-70.5,24.6L-70.5,24.6z"/>
|
||||
<path id="Path_950" class="st2" d="M8.3,11.5c-1.2,1.9-2.5,3.6-4,5.3c-3.4,4-7.3,7.4-11.6,10.3c-1.2,0.8-2.4,1.5-3.6,2.2l-0.6-2.8
|
||||
l3.5-9.1l4.2-11.1l8.8,1.1C6.1,8.7,7.2,10.1,8.3,11.5z"/>
|
||||
<path id="Path_951" class="st3" d="M-23.9-41.4c-2.7-4.3-6.8-7.5-11.6-8.9l-3.6,2.9l1.4-3.3c-1.2-0.2-2.3-0.2-3.5-0.2l-3.2,4.1
|
||||
l1.3-4c-5.6,0.7-10.7,3.7-14,8.3c-4.1,5.9-4.8,14.1-0.8,20c1.1-3.4,2.4-6.6,3.5-9.9c0.9,0.1,1.7,0.1,2.6,0l1.3-3.1l0.4,3
|
||||
c4.2-0.4,10.3-1.2,14.3-1.9l-0.4-2.3l2.3,1.9c1.2-0.3,1.9-0.5,1.9-0.7c2.9,4.7,5.8,7.7,8.8,12.5C-22.1-29.8-20.2-35.3-23.9-41.4z"
|
||||
/>
|
||||
<ellipse id="Ellipse_186" class="st1" cx="-24.9" cy="-26.1" rx="1.2" ry="2.4"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
BIN
Binary file not shown.
+2
@@ -0,0 +1,2 @@
|
||||
(function(){"use strict";try{if(typeof document<"u"){var e=document.createElement("style");e.appendChild(document.createTextNode(`.cdx-warning{position:relative}@media all and (min-width: 736px){.cdx-warning{padding-left:36px}}.cdx-warning [contentEditable=true][data-placeholder]:before{position:absolute;content:attr(data-placeholder);color:#707684;font-weight:400;opacity:0}.cdx-warning [contentEditable=true][data-placeholder]:empty:before{opacity:1}.cdx-warning [contentEditable=true][data-placeholder]:empty:focus:before{opacity:0}.cdx-warning:before{content:"";background-image:url("data:image/svg+xml,%3Csvg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Crect x='5' y='5' width='14' height='14' rx='4' stroke='black' stroke-width='2'/%3E%3Cline x1='12' y1='9' x2='12' y2='12' stroke='black' stroke-width='2' stroke-linecap='round'/%3E%3Cpath d='M12 15.02V15.01' stroke='black' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E");width:24px;height:24px;background-size:24px 24px;position:absolute;margin-top:8px;left:0}@media all and (max-width: 735px){.cdx-warning:before{display:none}}.cdx-warning__message{min-height:85px}.cdx-warning__title{margin-bottom:6px}`)),document.head.appendChild(e)}}catch(t){console.error("vite-plugin-css-injected-by-js",t)}})();
|
||||
(function(r,n){typeof exports=="object"&&typeof module<"u"?module.exports=n():typeof define=="function"&&define.amd?define(n):(r=typeof globalThis<"u"?globalThis:r||self,r.Warning=n())})(this,function(){"use strict";const r='<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><rect width="14" height="14" x="5" y="5" stroke="currentColor" stroke-width="2" rx="4"/><line x1="12" x2="12" y1="9" y2="12" stroke="currentColor" stroke-linecap="round" stroke-width="2"/><path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M12 15.02V15.01"/></svg>',n="";class a{static get isReadOnlySupported(){return!0}static get toolbox(){return{icon:r,title:"Warning"}}static get enableLineBreaks(){return!0}static get DEFAULT_TITLE_PLACEHOLDER(){return"Title"}static get DEFAULT_MESSAGE_PLACEHOLDER(){return"Message"}get CSS(){return{baseClass:this.api.styles.block,wrapper:"cdx-warning",title:"cdx-warning__title",input:this.api.styles.input,message:"cdx-warning__message"}}constructor({data:t,config:e,api:s,readOnly:i}){this.api=s,this.readOnly=i,this.titlePlaceholder=(e==null?void 0:e.titlePlaceholder)||a.DEFAULT_TITLE_PLACEHOLDER,this.messagePlaceholder=(e==null?void 0:e.messagePlaceholder)||a.DEFAULT_MESSAGE_PLACEHOLDER,this.data={title:t.title||"",message:t.message||""}}render(){const t=this._make("div",[this.CSS.baseClass,this.CSS.wrapper]),e=this._make("div",[this.CSS.input,this.CSS.title],{contentEditable:!this.readOnly,innerHTML:this.data.title}),s=this._make("div",[this.CSS.input,this.CSS.message],{contentEditable:!this.readOnly,innerHTML:this.data.message});return e.dataset.placeholder=this.titlePlaceholder,s.dataset.placeholder=this.messagePlaceholder,t.appendChild(e),t.appendChild(s),t}save(t){const e=t.querySelector(`.${this.CSS.title}`),s=t.querySelector(`.${this.CSS.message}`);return Object.assign(this.data,{title:(e==null?void 0:e.innerHTML)??"",message:(s==null?void 0:s.innerHTML)??""})}_make(t,e=null,s={}){const i=document.createElement(t);Array.isArray(e)?i.classList.add(...e):e&&i.classList.add(e);for(const l in s)i[l]=s[l];return i}static get sanitize(){return{title:{},message:{}}}}return a});
|
||||
Reference in New Issue
Block a user