14 Commits
0.9.2 ... 1.0.0

Author SHA1 Message Date
bff5b6befa Merge pull request #12 from webislife/v1
Up to v1
2023-05-14 01:04:13 +03:00
8ae8da7c4a update to v1 styles 2023-05-14 01:03:08 +03:00
d12b5a55c8 Merge pull request #11 from webislife/v1
0.9.34 fix for bug with el.fn, update dist folder
2023-02-17 21:19:46 +03:00
9e9b0d75fd 0.9.34 fix for bug with el.fn, update dist folder 2023-02-17 21:19:20 +03:00
fa51c1ba5f Merge pull request #10 from webislife/v1
minor updates for el function after habr.com feedback
2023-02-16 00:06:36 +03:00
bc2926ce61 minor updates for el function after habr.com feedback 2023-02-16 00:06:12 +03:00
9f597ce45a Merge pull request #9 from webislife/v1
fixes for #wrapTag
2023-02-15 02:45:47 +03:00
26f9927437 fixes for #wrapTag 2023-02-15 02:45:24 +03:00
3a9424118c Merge pull request #8 from webislife/v1
0.9.32
2023-02-15 02:36:12 +03:00
338e91724e updates for details block, minor styles fixes,update readme.md 2023-02-15 02:35:45 +03:00
5c29bb3bb9 Merge pull request #7 from webislife/v1
0.9.31 fix bug with localstorage restore state
2023-02-13 19:09:19 +03:00
e095392d54 Merge branch 'main' into v1 2023-02-13 19:09:06 +03:00
dc0482166a 0.9.31 fix bug with localstorage restore state 2023-02-13 19:07:19 +03:00
92d68cefc8 Update package.json 2023-02-13 02:09:08 +03:00
11 changed files with 775 additions and 279 deletions

View File

@ -40,6 +40,10 @@ See full demo - [wc-wysiwyg demo](https://webislife.ru/demo/wc-wysiwyg/) list an
✅ Inserting `<video>` element ✅ Inserting `<video>` element
- ✅ Suppoer extensions
- Color text and background editor
- Emoji table
## Install ## Install
@ -77,9 +81,12 @@ npm run build
</wc-wysiwyg> </wc-wysiwyg>
``` ```
--> -->
First, include JS and define custom element First need integrate wc-wysiwyg styles, you have 2 way, vanila css in `dist/sass` or scss in `src/sass` just include in your web project
Second, include JS and define custom element
```javascript ```javascript
import('/src/components/wc-wysiwyg.js').then(esm => { import('/src/components/wc-wysiwyg.js').then(esm => {
//you can pass any name into define fn
esm.define(); esm.define();
}); });
``` ```

2
dist/core/el.js vendored
View File

@ -1 +1 @@
export const el=(tagName,{classList,styles,props,attrs,options,append}={})=>{if(!tagName)throw new Error(`Undefined tag ${tagName}`);const element=document.createElement(tagName,options);if(classList)for(let i=0;i<classList.length;i++){const styleClass=classList[i];element.classList.add(styleClass)}if(styles){const stylesKeys=Object.keys(styles);for(let i=0;i<stylesKeys.length;i++){const key=stylesKeys[i];element.style[key]=styles[key]}}if(props){const propKeys=Object.keys(props);for(let i=0;i<propKeys.length;i++){const key=propKeys[i];element[key]=props[key]}}if(attrs){const attrsKeys=Object.keys(attrs);for(let i=0;i<attrsKeys.length;i++){const key=attrsKeys[i];attrs[key]&&element.setAttribute(key,attrs[key])}}if(append)for(let i=0;i<append.length;i++){const appendEl=append[i];element.append(appendEl)}return element}; export const el=(tagName,{classList,styles,props,attrs,options,append}={})=>{if(!tagName)throw new Error(`Undefined tag ${tagName}`);const element=document.createElement(tagName,options);if(classList)for(let i=0;i<classList.length;i++)classList[i]&&element.classList.add(classList[i]);if(styles&&Object.assign(element.style,styles),props){const propKeys=Object.keys(props);for(let i=0;i<propKeys.length;i++){const key=propKeys[i];element[key]=props[key]}}if(attrs){const attrsKeys=Object.keys(attrs);for(let i=0;i<attrsKeys.length;i++){const key=attrsKeys[i];attrs[key]&&element.setAttribute(key,attrs[key])}}return append&&element.append(...append),element};

File diff suppressed because one or more lines are too long

12
dist/sass/content.css vendored
View File

@ -75,19 +75,10 @@ h5 {
h6 { h6 {
font-size: 1em; } font-size: 1em; }
/* h1[id]::before,
h2[id]::before,
h3[id]::before,
h4[id]::before,
h5[id]::before,
h6[id]::before {
content: '§';
color: var(--color-blue-gray-300);
margin-right: 0.5em;
} */
/* del\ins */ /* del\ins */
del { del {
color: var(--color-red-900); color: var(--color-red-900);
border-bottom: 1px solid var(--color-red-900);
background-color: var(--color-red-50); background-color: var(--color-red-50);
text-decoration: none; } text-decoration: none; }
@ -104,6 +95,7 @@ del:before {
ins { ins {
color: var(--color-green-900); color: var(--color-green-900);
border-bottom: 1px solid var(--color-green-900);
background-color: var(--color-green-50); background-color: var(--color-green-50);
text-decoration: none; } text-decoration: none; }

2
dist/wc-wysiwyg.js vendored

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
{ {
"name": "@webislife/wc-wysiwyg", "name": "@webislife/wc-wysiwyg",
"version": "0.9.2", "version": "1.0.1",
"description": "WYWSIWYG HTML5 Editor written in ts and designed by web-componennt, support all JS frameworks and browsers", "description": "WYWSIWYG HTML5 Editor written in ts and designed by web-componennt, support all JS frameworks and browsers",
"main": "dist/wc-wysiwyg.js", "main": "dist/wc-wysiwyg.js",
"type": "module", "type": "module",
@ -38,4 +38,4 @@
"@webislife:registry": "https://npm.pkg.github.com" "@webislife:registry": "https://npm.pkg.github.com"
} }
} }

View File

@ -1,8 +1,8 @@
/** /**
* Short * Short for document.createElement
* @param tagName element tag name * @param tagName element tag name
* @param params list of object params for document.createElements * @param params list of object params for document.createElements
* @returns * @returns HTML\CUSTOMElement
*/ */
export const el = (tagName:keyof HTMLElementTagNameMap|string, {classList, styles, props, attrs, options, append}:{ export const el = (tagName:keyof HTMLElementTagNameMap|string, {classList, styles, props, attrs, options, append}:{
classList?: string[], classList?: string[],
@ -21,17 +21,14 @@
// element.classList // element.classList
if(classList) { if(classList) {
for (let i = 0; i < classList.length; i++) { for (let i = 0; i < classList.length; i++) {
const styleClass = classList[i]; if(classList[i]){
element.classList.add(styleClass) element.classList.add(classList[i]);
}
} }
} }
// element.style[prop] // element.style[prop]
if(styles) { if(styles) {
const stylesKeys = Object.keys(styles); Object.assign(element.style, styles);
for (let i = 0; i < stylesKeys.length; i++) {
const key = stylesKeys[i];
element.style[key] = styles[key];
}
} }
// element[prop] // element[prop]
if(props) { if(props) {
@ -51,11 +48,10 @@
} }
} }
} }
//append child elements
if(append) { if(append) {
for (let i = 0; i < append.length; i++) { element.append(...append);
const appendEl = append[i];
element.append(appendEl);
}
} }
return element; return element;
}; };

View File

@ -43,6 +43,7 @@ export const t = {
minlength: 'Min length is:', minlength: 'Min length is:',
maxlength: 'Max length is:', maxlength: 'Max length is:',
filtertags: 'Found filter tag:', filtertags: 'Found filter tag:',
details: 'Spoiler block',
}, },
ru: { ru: {
h1:'Заголовок 1 уровня', h1:'Заголовок 1 уровня',
@ -60,7 +61,7 @@ export const t = {
b:'Жирный', b:'Жирный',
i:'Курсив', i:'Курсив',
u:'Подчеркнутый', u:'Подчеркнутый',
s:'Маленький', s:'Перечеркнутый',
sup:'Надстрочный', sup:'Надстрочный',
sub:'Подстрочный', sub:'Подстрочный',
kbd:'Кнопка', kbd:'Кнопка',
@ -87,5 +88,6 @@ export const t = {
minlength: 'Минимальная длинна:', minlength: 'Минимальная длинна:',
maxlength: 'Максимальная длинна:', maxlength: 'Максимальная длинна:',
filtertags: 'Найден запрещенный тег:', filtertags: 'Найден запрещенный тег:',
details: 'Блок спойлера',
} }
}; };

View File

@ -90,19 +90,10 @@ h5 {
h6 { h6 {
font-size: 1em; font-size: 1em;
} }
/* h1[id]::before,
h2[id]::before,
h3[id]::before,
h4[id]::before,
h5[id]::before,
h6[id]::before {
content: '§';
color: var(--color-blue-gray-300);
margin-right: 0.5em;
} */
/* del\ins */ /* del\ins */
del { del {
color: var(--color-red-900); color: var(--color-red-900);
border-bottom: 1px solid var(--color-red-900);
background-color: var(--color-red-50); background-color: var(--color-red-50);
text-decoration: none; text-decoration: none;
} }
@ -119,6 +110,7 @@ del:before {
} }
ins { ins {
color: var(--color-green-900); color: var(--color-green-900);
border-bottom: 1px solid var(--color-green-900);
background-color: var(--color-green-50); background-color: var(--color-green-50);
text-decoration: none; text-decoration: none;
} }

View File

@ -1,11 +1,142 @@
:root {
// orange
--color-orange-50: #FFF8E1;
--color-orange-100: #FFF8E1;
--color-orange-500: #FF9800;
--color-orange-700: #F57C00;
--color-green-50: #E8F5E9;
--color-green-100: #C8E6C9;
--color-green-300: #AED581;
--color-green-500: #4CAF50;
--color-green-900: #1B5E20;
--color-red-50: #FFEBEE;
--color-red-100: #FFCDD2;
--color-red-200: #FF8A65;
--color-red-500: #F44336;
--color-red-900: #B71C1C;
--color-amber-50: #FFF8E1;
--color-amber-100: #FFE0B2;
--color-amber-900: #FF6F00;
--color-indigo-100: #C5CAE9;
// orange
--color-deep-orange-50: #FBE9E7;
--color-deep-orange-900: #BF360C;
// lime
--color-lime-50: #F9FBE7;
// grey
--color-grey-50: #FAFAFA;
--color-grey-100: #F5F5F5;
--color-grey-200: #EEEEEE;
--color-grey-300: #E0E0E0;
--color-grey-400: #BDBDBD;
--color-grey-500: #9E9E9E;
--color-grey-600: #757575;
--color-grey-700: #757575;
--color-grey-800: #424242;
--color-grey-900: #212121;
// blue
--color-blue-50: #E3F2FD;
--color-blue-100: #BBDEFB;
--color-blue-200: #90CAF9;
--color-blue-300: #64B5F6;
--color-blue-400: #42A5F5;
--color-blue-500: #2196F3;
--color-blue-800: #1565C0;
--color-blue-900: #0D47A1;
// blue-gray
--color-blue-gray-50: #ECEFF1;
--color-blue-gray-100: #CFD8DC;
--color-blue-gray-200: #B0BEC5;
--color-blue-gray-300: #90A4AE;
--color-blue-gray-400: #78909C;
--color-blue-gray-700: #455A64;
--color-blue-gray-800: #37474F;
--color-blue-gray-900: #263238;
// blue-light
--color-blue-light-50: #E1F5FE;
--color-blue-light-100: #B3E5FC;
--wc-wysiwyg-light: #fff;
--wc-wysiwyg-dark: #37474F;
}
wc-wysiwyg.-word .wc-wysiwyg_pf > label {
background-color: var(--color-blue-500);
}
wc-wysiwyg.-word .wc-wysiwyg_pf {
background-color: var(--color-blue-400);
border-radius: 5px;
padding: 5px;
}
wc-wysiwyg.-word .wc-wysiwyg_content {
border-radius: 5px;
}
wc-wysiwyg.-word .wc-wysiwyg_bt {
background-color: var(--color-blue-300);
padding:5px;
border-radius: 3px;
}
wc-wysiwyg.-word .wc-wysiwyg_ec {
background-color: var(--wc-wysiwyg-light);
padding:5px;
border-radius: 3px;
margin: 5px 0;
border: 0;
}
.wc-wysiwyg { .wc-wysiwyg {
background-color: #eee; background-color: var(--wc-wysiwyg-light);
font-family: Arial, Helvetica, sans-serif; font-family: Arial, Helvetica, sans-serif;
position: relative; position: relative;
border: 1px solid var(--color-blue-gray-400); padding:5px;
border-radius: 3px; border-radius: 3px;
display: block; display: block;
&_dialog {
border-radius: 10px;
border:none;
outline: none;
&::backdrop {
background: rgba(0, 0, 0, 0.8);
}
&.-modal {
min-width: 90vw;
min-height: 90vh;
max-height: 90vh;
max-height: 90vh;
}
&.-colors {
display: flex;
flex-wrap: wrap;
justify-content: center;
min-width: 0;
min-height: 0;
max-width: 300px;
&:not([open]) {
display: none;
}
& fieldset.-palette {
padding: 0;
display: flex;
flex: 1;
margin: 0;
border: 0;
outline: 0;
max-width: 20px;
& > button {
flex: 1;
margin: 2px;
}
}
}
}
&_bt { &_bt {
display: block; display: block;
padding:5px; padding:5px;
@ -27,31 +158,23 @@
&_ce { &_ce {
position: relative; position: relative;
margin: 0; margin-bottom: 5px;
padding: 7px; padding: 5px;
border: 1px solid var(--color-blue-500); border-radius: 5px;
border-radius: 3px; border: none;
background-color: var(--color-blue-50); background: rgba(0,0,0,0.2);
} &:before {
&_ce:before { content: 'HTML5 custom-elements';
content: 'HTML5 custom-elements'; color: #fff;
color: #fff; background-color: var(--color-blue-500);
background-color: var(--color-blue-500); position: absolute;
position: absolute; font-size: 10px;
font-size: 10px; line-height: 0.6em;
line-height: 0.6em; padding: 3px;
padding: 3px; border-radius: 3px;
border-radius: 3px; transform: translate(0, -50%);
transform: translate(0, -50%); top:0;
top:0; left:0;
left:0;
}
&_ce > button {
border-color: var(--color-blue-500);
background-color: var(--color-blue-100);
&:hover {
border-color: var(--color-blue-900);
background-color: var(--color-blue-200);
} }
} }
/* preview */ /* preview */
@ -62,7 +185,7 @@
min-height: 200px; min-height: 200px;
} }
&_content { &_content {
padding:5px 5px 2em 5px; padding:0.5em;
border:1px solid #ccc; border:1px solid #ccc;
background: #fff; background: #fff;
overflow-x: hidden; overflow-x: hidden;
@ -72,7 +195,7 @@
box-sizing: border-box; box-sizing: border-box;
margin: 0 auto; margin: 0 auto;
box-sizing: border-box; box-sizing: border-box;
display: inline-block; display: block;
resize: vertical; resize: vertical;
& .-selected { & .-selected {
background-color: var(--color-blue-100); background-color: var(--color-blue-100);
@ -86,52 +209,237 @@
} }
} }
&_ec { &_ec {
background: var(--color-blue-gray-100); background: #2b393f;
padding: 0.5em 0.25em 0.25em 0.25em; padding: 5px;
border-radius: 3px; border-radius: 5px;
border: 1px solid; margin-bottom: 10px;
border-color: var(--color-blue-gray-100); top:0;
} position: sticky;
&_ec:focus-within { z-index: 2;
border-color: var(--color-blue-500);
} }
&_btn { &_btn {
background: var(--color-blue-gray-50); min-width: 30px;
outline: none; line-height: 20px;
padding: 3px 6px; background-color: var(--wc-wysiwyg-light);
border-radius: 3px; box-shadow: 1px 2px 5px rgba(0,0,0,0.3);
border: 1px solid var(--color-blue-gray-200); border:0;
border-bottom: 3px solid var(--color-blue-gray-200); border-radius: 5px;
color: #333; padding:2px 5px;
min-width: 0px; margin-right:5px;
text-align: center;
box-sizing: border-box;
display: inline-block;
text-transform: uppercase;
font-size: 0.85em;
font-weight: 400;
line-height: 1;
white-space: nowrap;
margin-right: 5px;
user-select: none; user-select: none;
cursor: pointer; cursor: pointer;
&:hover { &:hover {
background: var(--color-blue-gray-100); box-shadow: 0 2px 5px rgba(0, 110, 253, 0.9);
border-color: var(--color-blue-gray-300);
} }
&:focus { &:focus {
border-color: var(--color-blue-500); box-shadow: 0 2px 5px rgba(0, 110, 253, 0.9);
} }
&:active { &:active {
padding-top:2px; box-shadow: 0 2px 5px rgba(1, 181, 52, 0.9);
background: var(--color-blue-gray-100);
border-color: var(--color-blue-gray-700);
border-bottom: 1px solid;
} }
&.-clear { &.-clear {
text-decoration: line-through; text-decoration: line-through;
font-weight: bold; font-weight: bold;
} }
&.-emoji {
border:none;
font-size: 20px;
min-width: 32px;
min-height: 32px;
line-height: 32px;
box-sizing: border-box;
padding:0;
border-radius: 1em;
margin: 2px;
}
&.-color {
min-width: 20px;
min-height: 20px;
border-color: rgba(0,0,0,0.2);
}
&.-prevcolor {
// background-color: transparent;
// border: 1px solid #ccc;
&::before {
display: inline-block;
background-color: var(--colorer);
width: 12px;
content: '';
height: 12px;
margin-right: 5px;
border-radius: 2px;
box-sizing: border-box;
border: 1px solid rgba(0,0,0,0.5);
}
}
&.-b {
font-weight: bold;
}
&.-i {
font-style: italic;
}
&.-u {
text-decoration: underline;
}
&.-s {
text-decoration: line-through;
}
&.-sub {
vertical-align: sub;
font-size: 0.5em;
}
&.-del::before {
content: '- ';
font-weight: 400;
}
&.-h1::before {
content: '§ ';
font-weight: 400;
}
&.-del {
color: var(--color-red-900);
border-bottom: 1px solid var(--color-red-900);
background-color: var(--color-red-50);
}
&.-a::before {
content: "🔗 ";
}
&.-ul::before {
content: "";
}
&.-ol::before {
content: "1. ";
}
&.-var::before {
content: "";
}
&.-var {
font-weight: bold;
font-style: italic;
}
&.-details:before {
content: "&rarr;";
}
&.-details {
text-decoration: dotted;
border: 1px dashed var(--color-blue-gray-200);
}
&.-pre::before {
content: "...";
display: inline-block;
background-color: var(--color-blue-gray-100);
color: var(--color-blue-grey-900);
border-radius: 2px;
position: absolute;
left: 0;
top: 0;
padding: 0 3px;
text-transform: uppercase;
font-size: 0.8em;
line-height: 10px;
}
&.-pre {
background-color: var(--color-blue-gray-50);
color: var(--color-blue-grey-900);
padding-left: 15px;
}
&.-ins::before {
content: '+ ';
font-weight: 400;
}
&.-ins {
color: var(--color-green-900);
border-bottom: 1px solid var(--color-green-900);
background-color: var(--color-green-50);
}
&.-sup {
vertical-align: super;
font-size: 0.5em;
}
&.-q:before {
content: open-quote;
}
&.-samp:before {
content: '> ';
color: var(--color-blue-gray-300);
font-family: sans-serif;
}
&.-samp {
background-color: var(--color-blue-gray-50);
border-bottom: 1px solid var(--color-blue-gray-300);
}
&.-blockquote::before {
content: '';
font-size: 1em;
color: #F57F17;
display: block;
position: absolute;
top: 0px;
left: 4px;
user-select: none;
}
&.-blockquote {
background-color: var(--color-amber-50);
color: #412207;
padding-left: 15px;
border-left: 2px solid #F57F17;
}
&.-time:before {
content: "📅 ";
}
&.-img:before {
content: "🌅 ";
}
&.-video:before {
content: "🎦 ";
}
&.-audio:before {
content: "🎵 ";
}
&.-details:before {
content: "";
}
&.-code {
content: "<code>";
background-color: var(--color-blue-gray-50);
color: var(--color-blue-grey-900);
}
&.-strong {
background-color: var(--color-deep-orange-50);
color: var(--color-deep-orange-900);
font-weight: 400;
}
&.-abbr {
color: #1A237E;
}
&.-q {
color:#000;
background-color: #FFF8E1;
}
&.-small {
font-size: 0.5em;
}
&.-dfn {
color: var(--color-blue-900);
font-style: italic;
}
&.-mark {
background-color: var(--color-lime-100);
color: var(--color-lime-900);
&:hover {
background-color: var(--color-lime-200);
color: var(--color-lime-900);
}
}
&.-kbd {
background: var(--color-blue-gray-50);
outline: none;
padding: 3px 6px;
border-radius: 3px;
border: 1px solid var(--color-blue-gray-200);
border-bottom: 3px solid var(--color-blue-gray-200);
color: #333;
}
} }
&_ia { &_ia {
display: flex; display: flex;
@ -148,11 +456,10 @@
position: fixed; position: fixed;
bottom:0; bottom:0;
width: 100%; width: 100%;
background: var(--color-blue-gray-50);
padding:3px;
border-radius: 3px;
border: 1px solid var(--color-blue-gray-300);
box-sizing: border-box; box-sizing: border-box;
background-color: var(--wc-wysiwyg-light);
border-radius: 5px;
padding: 5px;
& > form { & > form {
&:nth-child(1n+2) { &:nth-child(1n+2) {
margin-top: 10px; margin-top: 10px;
@ -184,7 +491,8 @@
background-color: var(--color-blue-200); background-color: var(--color-blue-200);
color: var(--color-blue-gray-800); color: var(--color-blue-gray-800);
padding: 3px 3px 3px 5px; padding: 3px 3px 3px 5px;
display: flex; margin: 5px 0;
display: inline-flex;
align-items: center; align-items: center;
border-radius: 6px; border-radius: 6px;
margin-right: 5px; margin-right: 5px;
@ -239,4 +547,23 @@
& .-display-none { & .-display-none {
display: none; display: none;
} }
}
@media (prefers-color-scheme: dark) {
.wc-wysiwyg {
background-color: var(--wc-wysiwyg-dark);
&_di {
background-color: var(--wc-wysiwyg-dark);
}
&_bt {
background-color: var(--wc-wysiwyg-dark);
}
&_btn {
background-color: rgba(0,0,0,0.5);
color: #ccc;
&:hover {
background-color: rgba(0,0,0,0.5);
color: #ccc;
}
}
}
} }

View File

@ -1,18 +1,14 @@
import {t} from './core/translates.js'; import { t } from './core/translates.js';
import { el } from "./core/el.js"; import { el } from "./core/el.js";
import test from './extensions/colorerDialog.js';
interface WCWYSIWYGTag { import test2 from './extensions/presetList.js';
tag:string /**
method?:Function, * Translate function
hint?:string, * @param key:string phrase key
is?: string, * @param lang:string language key defaul navigatol.language
} * @returns
interface WCWYSIWYGActions { */
wrapTag: Function, const _t = (key:string, lang = navigator.language):string => t[lang] ? t[lang][key] || "-" : t["en"][key];
insertImageBlock: Function,
insertAudio: Function,
insertVideo: Function,
}
//All semantic html5 known editor tags //All semantic html5 known editor tags
const allTags = [ const allTags = [
@ -50,14 +46,32 @@ const allTags = [
{ tag: 'audio'}, { tag: 'audio'},
{ tag: 'video'}, { tag: 'video'},
{ tag: 'blockquote'}, { tag: 'blockquote'},
{ tag: 'details'},
] as WCWYSIWYGTag[]; ] as WCWYSIWYGTag[];
/**
* Base class for WCWYSIWYG web component
*/
class WCWYSIWYG extends HTMLElement { class WCWYSIWYG extends HTMLElement {
/**
* Tags included to action bar in editor
*/
public EditorTags:WCWYSIWYGTag[] public EditorTags:WCWYSIWYGTag[]
/**
* Custom tags included to action bar in editor
*/
public EditorCustomTags:WCWYSIWYGTag[] public EditorCustomTags:WCWYSIWYGTag[]
//Content editable wc-editor element /**
* Main ContentEditable element
*/
public EditorNode:HTMLElement public EditorNode:HTMLElement
/**
* Editor actions block elements with EditorTags buttons
*/
public EditorActionsSection:HTMLElement public EditorActionsSection:HTMLElement
/**
* Custom web-components buttons <fieldset>
*/
public EditorCustomTagsForm?:HTMLElement
//Inline edites //Inline edites
public EditorInlineActions:any[] public EditorInlineActions:any[]
public EditorInlineDialog:HTMLDialogElement public EditorInlineDialog:HTMLDialogElement
@ -65,7 +79,7 @@ class WCWYSIWYG extends HTMLElement {
//Editor props //Editor props
public EditorPropertyForm?:HTMLElement public EditorPropertyForm?:HTMLElement
//Clear btn //Clear btn
public EditorClearFormatBtn:HTMLElement public EditorClearFormatBtn:HTMLButtonElement
//Autocomplete area //Autocomplete area
public EditorAutoCompleteForm?:HTMLElement public EditorAutoCompleteForm?:HTMLElement
//Bottom actions //Bottom actions
@ -74,7 +88,6 @@ class WCWYSIWYG extends HTMLElement {
public EditorBottomFormViewToggle?:HTMLElement public EditorBottomFormViewToggle?:HTMLElement
public EditorPreviewText:HTMLTextAreaElement public EditorPreviewText:HTMLTextAreaElement
public EditorCustomTagsForm?:HTMLElement
public EditorTagsMethods:WCWYSIWYGActions public EditorTagsMethods:WCWYSIWYGActions
public EditorAllowTags:string[] public EditorAllowTags:string[]
public EditorFullScreenButton?:HTMLElement public EditorFullScreenButton?:HTMLElement
@ -85,19 +98,21 @@ class WCWYSIWYG extends HTMLElement {
#EditProps:boolean|object #EditProps:boolean|object
#Autocomplete:boolean #Autocomplete:boolean
#Extensions?: any[]
#SotrageKey:string|null #SotrageKey:string|null
#HideBottomActions:boolean #HideBottomActions:boolean
#Connected:boolean = false; #Connected:boolean = false;
constructor() { constructor() {
super(); super();
this.#checkExtensions();
this.classList.add('wc-wysiwyg'); this.classList.add('wc-wysiwyg');
//Listen root element events //Listen root element events
this.onpointerup = (event) => { this.onpointerup = (event) => {
const selection = window.getSelection(); const editorSelection = this.getSelection();
//if check exist selection string //if check exist selection string
if(selection !== null && selection.toString().length > 0) { if(editorSelection.selection !== null && editorSelection.selection.text !== null) {
this.EditorInlineActionsForm.style.display = ''; this.EditorInlineActionsForm.style.display = '';
if(this.EditorPropertyForm){ if(this.EditorPropertyForm){
this.EditorPropertyForm.style.display = 'none'; this.EditorPropertyForm.style.display = 'none';
@ -115,6 +130,25 @@ class WCWYSIWYG extends HTMLElement {
connectedCallback() { connectedCallback() {
if(this.#Connected === false) { if(this.#Connected === false) {
const asyncExtensions = this.getAttribute('data-async-extensions') || false;
if(asyncExtensions !== false) {
const asyncExtensionsPaths = asyncExtensions.split(',');
console.log('need load', asyncExtensionsPaths);
asyncExtensionsPaths.forEach((extensionPath:string) => {
import(extensionPath).then(esm => {
console.log('asyncExtensionsPaths loaded', esm);
const Extension = new esm.default(this);
if(typeof Extension.connectedCallback === 'function') {
Extension.connectedCallback();
}
if(this.#Extensions instanceof Array) {
this.#Extensions.push(Extension);
} else {
this.#Extensions = [Extension];
}
});
})
}
//Check Tags //Check Tags
const allowTags = this.getAttribute('data-allow-tags') || allTags.map(t => t.tag).join(','); const allowTags = this.getAttribute('data-allow-tags') || allTags.map(t => t.tag).join(',');
@ -133,41 +167,43 @@ class WCWYSIWYG extends HTMLElement {
this.#EditProps = this.getAttribute('data-edit-props') !== null ? JSON.parse(this.getAttribute('data-edit-props') || '') : false; this.#EditProps = this.getAttribute('data-edit-props') !== null ? JSON.parse(this.getAttribute('data-edit-props') || '') : false;
this.#Autocomplete = this.getAttribute('data-autocomplete') === '1'; this.#Autocomplete = this.getAttribute('data-autocomplete') === '1';
this.#HideBottomActions = this.getAttribute('data-hide-bottom-actions') === '1'; this.#HideBottomActions = this.getAttribute('data-hide-bottom-actions') === '1';
//allow inline without ['video','audio','img'] //allow inline without ['video','audio','img']
this.EditorInlineActions = this.EditorTags.filter(action => ['video','audio','img'].includes(action.tag) === false); this.EditorInlineActions = this.EditorTags.filter(action => ['video','audio','img'].includes(action.tag) === false);
//Check local storage key
this.#SotrageKey = this.getAttribute('data-storage');
if(this.#SotrageKey) {
let storeValue = window.localStorage.getItem(this.#SotrageKey);
if(storeValue) {
this.value = storeValue;
}
}
this.EditorActionsSection = el('section', { classList: ['wc-wysiwyg_ec'] });
//Clear format button //Clear format button
this.EditorClearFormatBtn = el('button', { this.EditorClearFormatBtn = el('button', {
classList: ['wc-wysiwyg_btn', '-clear'], classList: ['wc-wysiwyg_btn', '-clear'],
attrs: { attrs: {
'data-hint': this.#t('clearFormat'), 'data-hint': _t('clearFormat'),
}, },
props: { props: {
innerHTML:'Ⱦ', innerHTML:'Ⱦ',
}, },
}); });
//Inline selection actions panel //Top editor actions sections
this.EditorInlineActionsForm = el('form'); this.EditorActionsSection = el('section', {
this.EditorInlineDialog = el('dialog', { classList: ['wc-wysiwyg_ec'],
classList: ['wc-wysiwyg_di'],
append: [this.EditorInlineActionsForm, this.EditorClearFormatBtn],
props: {
//prevent submit
onsubmit: event => {
event.preventDefault();
event.stopPropagation();
},
}
}); });
this.#makeActionButtons(this.EditorActionsSection, this.EditorTags);
this.EditorActionsSection.append(this.EditorClearFormatBtn);
//Inline selection actions panel
// this.EditorInlineActionsForm = el('form');
// this.EditorInlineDialog = el('dialog', {
// classList: ['wc-wysiwyg_di'],
// append: [this.EditorInlineActionsForm],
// props: {
// //prevent submit
// onsubmit: event => {
// event.preventDefault();
// event.stopPropagation();
// },
// },
// attrs: {
// tabIndex: -1,
// }
// });
//Edit props //Edit props
if(this.#EditProps) { if(this.#EditProps) {
@ -178,12 +214,12 @@ class WCWYSIWYG extends HTMLElement {
props: { props: {
onsubmit: event => { onsubmit: event => {
event.preventDefault(); event.preventDefault();
this.hideEditorInlineDialog(); this.EditorPropertyForm.style.display = 'none';
}, },
onpointerup: event => event.stopPropagation(), onpointerup: event => event.stopPropagation(),
} }
}); });
this.EditorInlineDialog.append(this.EditorPropertyForm); this.EditorActionsSection.append(this.EditorPropertyForm);
} }
//Autocomplete form //Autocomplete form
@ -208,9 +244,7 @@ class WCWYSIWYG extends HTMLElement {
} }
//Actions in footer //Actions in footer
this.EditorBottomForm = el('fieldset', { this.EditorBottomForm = el('fieldset', { classList: ['wc-wysiwyg_bt'] });
classList: ['wc-wysiwyg_bt'],
});
//Check custom tags //Check custom tags
this.EditorCustomTags = JSON.parse( String(this.getAttribute('data-custom-tags')) ); this.EditorCustomTags = JSON.parse( String(this.getAttribute('data-custom-tags')) );
@ -218,11 +252,11 @@ class WCWYSIWYG extends HTMLElement {
//Custom panel tags //Custom panel tags
this.EditorCustomTagsForm = el('fieldset', { this.EditorCustomTagsForm = el('fieldset', {
classList: ['wc-wysiwyg_ce'], classList: ['wc-wysiwyg_ce'],
}); }) as HTMLElement;
//Make custom actions buttons panel //Make custom actions buttons panel
this.#makeActionButtons(this.EditorCustomTagsForm as HTMLElement, this.EditorCustomTags); this.#makeActionButtons(this.EditorCustomTagsForm, this.EditorCustomTags);
this.appendChild(this.EditorCustomTagsForm as HTMLElement); this.EditorActionsSection.prepend(this.EditorCustomTagsForm);
} }
//Node editable //Node editable
@ -230,72 +264,48 @@ class WCWYSIWYG extends HTMLElement {
classList: ['wc-wysiwyg_content', this.getAttribute('data-content-class') || ''], classList: ['wc-wysiwyg_content', this.getAttribute('data-content-class') || ''],
props: { props: {
contentEditable: true, contentEditable: true,
//Pointer event behaviors
onpointerup: event => { onpointerup: event => {
this.checkCanClearElement(event); this.#checkCanClearElement(event);
if(this.#EditProps) { if(this.#EditProps) {
this.checkEditProps(event); this.#checkEditProps(event);
} }
}, },
//Update content on input event
oninput: event => { oninput: event => {
this.updateContent(); this.updateContent();
if(this.#Autocomplete) { if(this.#Autocomplete) {
this.checkAutoComplete(); this.#checkAutoComplete();
} }
}, },
//Handle key bindings //Check hot keys is pressed
onkeydown: event => { onkeydown: event => {
//check hold alt this.#checkKeyBindings(event);
if(event.altKey) { },
//alt+space - move caret to parent node next sibling onpaste: (event:ClipboardEvent) => {
if(event.code === 'Space') { // if(event.type === 'paste') {
const Selection = window.getSelection(); // window.navigator.clipboard.readText().then(text => {
if(Selection?.type === 'Caret') { // const target = event.target as HTMLElement;
//insertAdjacentElement dont support textNodes, first insert span // //Check isnert html symbol
const span = el('span'); // if(text.startsWith('&#') === false) {
Selection?.anchorNode?.parentElement?.insertAdjacentElement('afterend', span) // if(target.tagName === 'BR') {
//after replace span with textnode and select it // target.insertAdjacentText('afterend',text);
const textN = document.createTextNode('&nbsp'); // }
span.replaceWith(textN); // this.updateContent();
const range = document.createRange(); // event.preventDefault();
range.selectNodeContents(textN); // event.stopPropagation();
Selection.removeAllRanges();
Selection.addRange(range); // return false;
} // }
} // });
} // }
//tag - hide editor dialog
if(event.code === 'Escape') {
this.hideEditorInlineDialog();
}
//enter - set p as default tag in newline
if(event.code === 'Enter' && event.shiftKey === false) {
const Selection = window.getSelection();
let tagName = 'p';
//tags with return default browser behavior
if(['LI', 'ARTICLE', 'P'].includes(Selection.anchorNode.parentElement.tagName)) {
return true;
}
const p = el(tagName, { props: { innerHTML: `&nbsp;` } });
Selection?.anchorNode?.parentElement?.insertAdjacentElement('afterend', p);
const range = document.createRange();
range.selectNodeContents(p);
Selection?.removeAllRanges();
Selection?.addRange(range);
event.stopPropagation();
event.preventDefault();
}
} }
}, },
}); });
//Make action buttons
this.#makeActionButtons(this.EditorActionsSection, this.EditorTags);
this.#makeActionButtons(this.EditorInlineActionsForm, this.EditorInlineActions);
//Inser wc-editor after textarea node //Inser wc-editor after textarea node
this.append( this.append(
this.EditorActionsSection, this.EditorActionsSection,
this.EditorInlineDialog,
this.EditorNode, this.EditorNode,
); );
@ -304,7 +314,7 @@ class WCWYSIWYG extends HTMLElement {
this.EditorBottomFormViewToggle = el('button', { this.EditorBottomFormViewToggle = el('button', {
classList: ['wc-wysiwyg_btn'], classList: ['wc-wysiwyg_btn'],
attrs: { attrs: {
'data-hint': this.#t('toggleViewMode'), 'data-hint': _t('toggleViewMode'),
'data-mode': 'html5', 'data-mode': 'html5',
}, },
props: { props: {
@ -326,11 +336,11 @@ class WCWYSIWYG extends HTMLElement {
this.EditorBottomFormNewP = el('button', { this.EditorBottomFormNewP = el('button', {
classList: ['wc-wysiwyg_btn'], classList: ['wc-wysiwyg_btn'],
attrs: { attrs: {
'data-hint': this.#t('addNewParahraph'), 'data-hint': _t('addNewParahraph'),
}, },
props: { props: {
type:'button', type:'button',
innerText: '+ P', innerText: '+ ',
onpointerup: event => { onpointerup: event => {
const P = el('p', {props: {innerText: '/'}}); const P = el('p', {props: {innerText: '/'}});
this.EditorNode.appendChild(P); this.EditorNode.appendChild(P);
@ -342,7 +352,7 @@ class WCWYSIWYG extends HTMLElement {
this.EditorFullScreenButton = el('button', { this.EditorFullScreenButton = el('button', {
classList: ['wc-wysiwyg_btn'], classList: ['wc-wysiwyg_btn'],
attrs: { attrs: {
'data-hint': this.#t('fullScreen'), 'data-hint': _t('fullScreen'),
}, },
props: { props: {
type: "button", type: "button",
@ -362,6 +372,18 @@ class WCWYSIWYG extends HTMLElement {
this.append(this.EditorBottomForm); this.append(this.EditorBottomForm);
} }
this.EditorNode.innerHTML = this.EditorPreviewText.value; this.EditorNode.innerHTML = this.EditorPreviewText.value;
//Check local storage key
this.#SotrageKey = this.getAttribute('data-storage');
if(this.#SotrageKey) {
let storeValue = window.localStorage.getItem(this.#SotrageKey);
if(storeValue) {
this.EditorNode.innerHTML = storeValue;
}
}
//Check and call connectedCallback in extensions
if(this.#Extensions instanceof Array) {
this.#Extensions.forEach(extension => typeof extension.connectedCallback === 'function' ? extension.connectedCallback(this) : false);
}
this.updateContent(); this.updateContent();
this.#Connected = true; this.#Connected = true;
@ -372,14 +394,20 @@ class WCWYSIWYG extends HTMLElement {
* Update content value and update behaviors * Update content value and update behaviors
*/ */
updateContent() { updateContent() {
// Update the value of the editor node to the current innerHTML
this.value = this.EditorNode.innerHTML; this.value = this.EditorNode.innerHTML;
// Update the value of the EditorPreviewText to the current value
this.EditorPreviewText.value = this.value; this.EditorPreviewText.value = this.value;
// Check the validity of the current value
this.checkValidity(); this.checkValidity();
// Dispatch an event to indicate that the input has been changed
this.dispatchEvent(new Event('oninput', { bubbles: true, cancelable: false }));
// Update the preview element with the data attribute 'data-preview-el'
this.updatePreviewEl(this.getAttribute('data-preview-el'));
//Update storage value if StorageKey exits
if(this.#SotrageKey) { if(this.#SotrageKey) {
window.localStorage.setItem(this.#SotrageKey, this.value); window.localStorage.setItem(this.#SotrageKey, this.value);
} }
this.dispatchEvent(new Event('oninput', { bubbles: true, cancelable: false }));
this.updatePreviewEl(this.getAttribute('data-preview-el'));
} }
/** /**
@ -407,19 +435,19 @@ class WCWYSIWYG extends HTMLElement {
if(this.getAttribute('required') !== null) { if(this.getAttribute('required') !== null) {
if(String(this.EditorNode.textContent).length === 0) { if(String(this.EditorNode.textContent).length === 0) {
hasErros = true; hasErros = true;
errors.push(this.#t('required')); errors.push(_t('required'));
} }
} }
if(Number(this.getAttribute('minlength'))) { if(Number(this.getAttribute('minlength'))) {
if(String(this.EditorNode.textContent).length < Number(this.getAttribute('minlength'))) { if(String(this.EditorNode.textContent).length < Number(this.getAttribute('minlength'))) {
hasErros = true; hasErros = true;
errors.push(`${this.#t('minlength')} ${this.getAttribute('minlength')}`); errors.push(`${_t('minlength')} ${this.getAttribute('minlength')}`);
} }
} }
if(Number(this.getAttribute('maxlength'))) { if(Number(this.getAttribute('maxlength'))) {
if(String(this.EditorNode.textContent).length > Number(this.getAttribute('maxlength'))) { if(String(this.EditorNode.textContent).length > Number(this.getAttribute('maxlength'))) {
hasErros = true; hasErros = true;
errors.push(`${this.#t('maxlength')} ${this.getAttribute('maxlength')}`); errors.push(`${_t('maxlength')} ${this.getAttribute('maxlength')}`);
} }
} }
const filterTags = this.getAttribute('filtertags'); const filterTags = this.getAttribute('filtertags');
@ -429,7 +457,7 @@ class WCWYSIWYG extends HTMLElement {
const checkTag = disallowTags[i]; const checkTag = disallowTags[i];
if(this.EditorNode.querySelector(checkTag)) { if(this.EditorNode.querySelector(checkTag)) {
hasErros = true; hasErros = true;
errors.push(`${this.#t('filtertags')} ${checkTag}`); errors.push(`${_t('filtertags')} ${checkTag}`);
break; break;
} }
} }
@ -455,19 +483,19 @@ class WCWYSIWYG extends HTMLElement {
/** /**
* Check if need append autocompleted tags variants * Check if need append autocompleted tags variants
*/ */
checkAutoComplete() { #checkAutoComplete() {
//CHeck autococmplete //CHeck autococmplete
const Selecton = window.getSelection(); const editorSelection = this.getSelection();
if(Selecton !== null && Selecton.anchorNode !== null) { if(editorSelection.selection !== null && editorSelection.selection.anchorNode !== null) {
const SelectionParentEl = Selecton.anchorNode.parentElement as HTMLParagraphElement; const SelectionParentEl = editorSelection.selection.anchorNode.parentElement as HTMLParagraphElement;
const AutoCompleteForm = this.EditorAutoCompleteForm as HTMLElement; const AutoCompleteForm = this.EditorAutoCompleteForm as HTMLElement;
if(SelectionParentEl !== null && if(SelectionParentEl !== null &&
//if empty selection //if empty selection
Selecton.toString() === '' && editorSelection.text === null &&
//and parent node is <p> //and parent node is <p>
SelectionParentEl.nodeName === 'P' && SelectionParentEl.nodeName === 'P' &&
//and parent <p> is parentElement in EditorNode //and parent <p> is parentElement in EditorNode
SelectionParentEl.parentElement === this.EditorNode) { SelectionParentEl.parentElement === this.EditorNode) {
//and parent <p> inner text starts with `/` //and parent <p> inner text starts with `/`
if(SelectionParentEl.innerText.startsWith('/')) { if(SelectionParentEl.innerText.startsWith('/')) {
const parsedTagName = SelectionParentEl.innerText.replace('/', ''); const parsedTagName = SelectionParentEl.innerText.replace('/', '');
@ -478,7 +506,7 @@ class WCWYSIWYG extends HTMLElement {
AutoCompleteForm?.appendChild(el('button', { AutoCompleteForm?.appendChild(el('button', {
classList: ['wc-wysiwyg_btn', `-${action.tag}`], classList: ['wc-wysiwyg_btn', `-${action.tag}`],
attrs: { attrs: {
'data-hint': this.#t(action.tag) || null, 'data-hint': _t(action.tag) || null,
}, },
props: { props: {
type: 'submit', type: 'submit',
@ -497,7 +525,7 @@ class WCWYSIWYG extends HTMLElement {
} }
} }
} }
} }
} }
} }
@ -505,7 +533,17 @@ class WCWYSIWYG extends HTMLElement {
* Show and position inline actions dialog at targetNode * Show and position inline actions dialog at targetNode
**/ **/
showEditorInlineDialog() { showEditorInlineDialog() {
this.EditorInlineDialog.show(); //Save selection range
const editorSelection = this.getSelection();
if(editorSelection.selection !== null) {
const range = editorSelection.selection.getRangeAt(0).cloneRange();
this.EditorInlineDialog.show();
//Restore selection range after dialog show
editorSelection.selection.removeAllRanges();
editorSelection.selection.addRange(range);
} else {
this.EditorInlineDialog.show();
}
} }
/** /**
@ -519,23 +557,24 @@ class WCWYSIWYG extends HTMLElement {
} }
/** /**
* Checking and clear tag, if can do it * Checking clear form and clear, if can do it
* @param event * @param event
*/ */
checkCanClearElement(event:Event) { #checkCanClearElement(event:Event) {
const eventTarget = event.target as HTMLElement; const eventTarget = event.target as HTMLElement,
clearBtn = this.EditorClearFormatBtn;
if(eventTarget !== this.EditorNode) { if(eventTarget !== this.EditorNode) {
if(eventTarget.nodeName !== 'P' if(eventTarget.nodeName !== 'P'
&& eventTarget.nodeName !== 'SPAN') { && eventTarget.nodeName !== 'SPAN') {
this.EditorClearFormatBtn.style.display = 'inline-block'; clearBtn.style.display = 'inline-block';
this.EditorClearFormatBtn.innerHTML = `Ⱦ ${eventTarget.nodeName}`, clearBtn.innerHTML = `Ⱦ ${eventTarget.nodeName}`,
this.EditorClearFormatBtn.onpointerup = (event) => { clearBtn.onpointerup = (event) => {
eventTarget.replaceWith(document.createTextNode(eventTarget.textContent)); eventTarget.replaceWith(document.createTextNode(eventTarget.textContent));
} };
this.showEditorInlineDialog(); this.showEditorInlineDialog();
} else { } else {
this.EditorClearFormatBtn.style.display = 'none'; clearBtn.style.display = 'none';
this.EditorClearFormatBtn.onpointerup = null; clearBtn.onpointerup = null;
} }
} }
} }
@ -543,16 +582,18 @@ class WCWYSIWYG extends HTMLElement {
/** /**
* Checking click tag for editable props * Checking click tag for editable props
**/ **/
checkEditProps(event) { #checkEditProps(event) {
//Check need edit props
const eventTarget = event.target as HTMLElement; const eventTarget = event.target as HTMLElement;
//Check exist prop\attr //Check exist prop\attr
if(this.#EditProps[eventTarget.nodeName]) { if(this.#EditProps[eventTarget.nodeName]) {
const props = this.#EditProps[eventTarget.nodeName]; const props = this.#EditProps[eventTarget.nodeName];
event.stopPropagation(); event.stopPropagation();
this.dispatchEvent(new CustomEvent('editprops', {
detail: { eventTarget }
}));
this.EditorPropertyForm.style.display = ''; this.EditorPropertyForm.style.display = '';
this.showEditorInlineDialog(); this.EditorPropertyForm.style.display = 'block';
this.EditorPropertyForm.setAttribute('data-tag', eventTarget.nodeName); this.EditorPropertyForm.setAttribute('data-tag', eventTarget.nodeName);
this.EditorPropertyForm.innerHTML = ''; this.EditorPropertyForm.innerHTML = '';
for (let i = 0; i < props.length; i++) { for (let i = 0; i < props.length; i++) {
@ -594,23 +635,79 @@ class WCWYSIWYG extends HTMLElement {
} }
} }
#makeActionButtons(toEl:HTMLElement, actions) { /**
* Cheking hot keys when keydown pressed
* @param event Keyboard event
*/
#checkKeyBindings(event:KeyboardEvent) {
//check hold alt
if(event.altKey) {
//alt+space - move caret to parent node next sibling
if(event.code === 'Space') {
const editorSelection = this.getSelection();
const Selection = editorSelection.selection;
if(Selection !== null) {
if(Selection.type === 'Caret') {
//insertAdjacentElement dont support textNodes, first insert span
const span = el('span');
Selection.anchorNode?.parentElement?.insertAdjacentElement('afterend', span)
//after replace span with textnode and select it
const textN = document.createTextNode('&nbsp');
span.replaceWith(textN);
const range = document.createRange();
range.selectNodeContents(textN);
Selection.removeAllRanges();
Selection.addRange(range);
}
}
}
}
//tag - hide editor dialog
if(event.code === 'Escape') {
this.hideEditorInlineDialog();
}
//enter - set p as default tag in newline
if(event.code === 'Enter' && event.shiftKey === false) {
const Selection = this.getSelection().selection;
let tagName = 'p';
//tags with return default browser behavior
if(['LI', 'ARTICLE', 'P'].includes(Selection.anchorNode.parentElement.tagName)) {
return false;
}
const p = el(tagName, { props: { innerHTML: `&nbsp;` } });
Selection?.anchorNode?.parentElement?.insertAdjacentElement('afterend', p);
const range = document.createRange();
range.selectNodeContents(p);
Selection?.removeAllRanges();
Selection?.addRange(range);
event.stopPropagation();
event.preventDefault();
}
}
/**
* Make buttons and bind actions
* @param toEl htmlelement where append el
* @param actions
*/
#makeActionButtons(toEl:HTMLElement, actions:WCWYSIWYGTag[]) {
for (let i = 0; i < actions.length; i++) { for (let i = 0; i < actions.length; i++) {
const action = actions[i]; const action = actions[i];
const button = el('button', { const button = el('button', {
classList: ['wc-wysiwyg_btn', `-${action.tag}`], classList: ['wc-wysiwyg_btn', `-${action.tag}`],
props: { props: {
tabIndex: -1, tabIndex: 0,
type:'button', type:'button',
textContent: action.is ? `${action.tag} is=${action.is}` : action.tag, textContent: action.is ? `${action.tag} is=${action.is}` : action.tag,
onpointerup: (event) => this.#tag(action.tag, event, action.is), onpointerup: (event) => {
event.stopPropagation();
this.#tag(action)
},
}, },
attrs: { attrs: {
'data-hint': action.hint ? action.hint : this.#t(action.tag) || '-', 'data-hint': action.hint ? action.hint : _t(action.tag) || '-',
} },
}); });
//-wc
//Make button with
toEl.appendChild(button); toEl.appendChild(button);
} }
} }
@ -619,44 +716,66 @@ class WCWYSIWYG extends HTMLElement {
/** /**
* Default behaviors fot tag actions * Default behaviors fot tag actions
*/ */
#tag = (tag:string, event:Event|false = false, is:boolean|string = false) => { #tag = (tag:WCWYSIWYGTag) => {
switch (tag) { switch (tag.tag) {
case 'audio': case 'audio':
this.#Media('audio'); this.#Media('audio');
break; break;
case 'video': case 'video':
this.#Media('video'); this.#Media('video');
break; break;
case 'details':
this.#Details();
break;
case 'img': case 'img':
this.#Image(); this.#Image();
break; break;
default: default:
this.#wrapTag(tag, is); if(typeof tag.method === 'function') {
tag.method.apply(this, tag);
} else {
this.#wrapTag(tag, tag.is);
}
break; break;
} }
} }
/**
* Insert spoiler
**/
#Details() {
const summaryTitle = prompt('Title', '');
if(summaryTitle === '') {
return false;
}
const mediaEl = el('details', {
append: [
el('summary', { props: {innerText: summaryTitle} }),
el('p', { props: {innerText: '...'} })] }
);
this.EditorNode.append(mediaEl);
this.updateContent();
}
/** /**
* Wrap content in <tag> * Wrap content in <tag>
* @param tag:WCWYSIWYGTag
**/ **/
#wrapTag = (tag, is:boolean|string = false) => { #wrapTag = (tag:WCWYSIWYGTag, is:boolean|string = false) => {
const isList = ['ul', 'ol'].includes(tag); //First check if this tag is list item
const Selection = window.getSelection(); const listTag = ['ul', 'ol'].includes(tag.tag) ? tag.tag : false;
let className = null; //If list tag is true - set newTag is list item as <li>
let defaultOptions = { const newtag = listTag !== false ? 'li' : tag.tag;
classList: className ? className : undefined, const Selection = this.getSelection().selection;
} as any; let defaultOptions = {} as any;
if(isList) {
tag = 'li';
}
if(is) { if(is) {
defaultOptions.options = {is}; defaultOptions.options = {is};
} }
let tagNode = el(tag, defaultOptions); let tagNode = el(newtag, defaultOptions);
if (Selection !== null && Selection.rangeCount) { if (Selection !== null && Selection.rangeCount) {
if(['ul', 'ol'].includes(tag)) { if(listTag !== false) {
const list = el(tag); const list = el(listTag);
tagNode.replaceWith(list); tagNode.replaceWith(list);
list.append(tagNode) list.append(tagNode)
} }
@ -664,10 +783,13 @@ class WCWYSIWYG extends HTMLElement {
range.surroundContents(tagNode); range.surroundContents(tagNode);
Selection.removeAllRanges(); Selection.removeAllRanges();
Selection.addRange(range); Selection.addRange(range);
//If selection has text, insert it
if(Selection.toString().length === 0) { if(Selection.toString().length === 0) {
tagNode.innerText = tag; tagNode.innerText = tag;
} }
this.updateContent(); this.updateContent();
//Check if new tag has editable props
this.#checkEditProps({target:tagNode, stopPropagation: () => false});
} }
} }
@ -699,15 +821,16 @@ class WCWYSIWYG extends HTMLElement {
} }
if(caption) { if(caption) {
const figure = el('figure'); const figure = el('figure', {
append: [
const figcaption = el('figcaption', { img,
props: { el('figcaption', {
textContent: caption props: {
} textContent: caption
}
})
]
}); });
figure.appendChild(img);
figure.appendChild(figcaption);
img.setAttribute('alt', caption); img.setAttribute('alt', caption);
this.EditorNode.appendChild(figure); this.EditorNode.appendChild(figure);
@ -717,13 +840,46 @@ class WCWYSIWYG extends HTMLElement {
} }
/** /**
* Translate function * Check available extensions in global scope
* @param key:string phrase key
* @returns
*/ */
#t(key:string):string { #checkExtensions() {
let lang = this.lang; // Check extensions in global
return t[lang] ? t[lang][key] || "-" : t["en"][key]; const _WCWYSIWYG = window._WCWYSIWYG;
console.log('#checkExtensions',_WCWYSIWYG);
if(_WCWYSIWYG !== undefined && _WCWYSIWYG.extensions.length > 0) {
this.#Extensions = [];
for (let i = 0; i < _WCWYSIWYG.extensions.length; i++) {
const Extension = new _WCWYSIWYG.extensions[i](this);
this.#Extensions.push(Extension);
}
}
}
/**
* Get selection info from editor
* @returns editorSelection
*/
getSelection() {
const windowSelection = window.getSelection();
const editorSelection = {
selection: null,
element: null,
text: null,
node: null,
};
if(windowSelection !== null) {
editorSelection.selection = windowSelection;
editorSelection.node = windowSelection.anchorNode;
editorSelection.element = windowSelection.anchorNode.parentElement;
let selectionText = windowSelection.toString();
if(selectionText.length > 0) {
editorSelection.text = selectionText;
}
}
console.log('wc selection', editorSelection);
return editorSelection;
} }
//define WCWYSIWYG as custom element //define WCWYSIWYG as custom element
static define(name = 'wc-wysiwyg') { static define(name = 'wc-wysiwyg') {
@ -731,4 +887,28 @@ class WCWYSIWYG extends HTMLElement {
} }
} }
export default WCWYSIWYG; export default WCWYSIWYG;
export const define = WCWYSIWYG.define; export const define = WCWYSIWYG.define;
export interface WCWYSIWYGTag {
// The HTML tag name
tag: string,
// Optional method to be called when the tag is used
method?: Function,
// Optional hint to be displayed when the tag button is pressed\hovered
hint?: string,
// Optional string to specify the custom tag element
is?: string,
}
export interface WCWYSIWYGActions {
wrapTag: Function,
insertImageBlock: Function,
insertAudio: Function,
insertVideo: Function,
}
declare global {
interface Window {
_WCWYSIWYG: {
extensions?:any[],
asyncExtensions?:string[],
}
}
}