14 Commits
0.9.31 ... 1.04

Author SHA1 Message Date
83613ca5cc Merge pull request #14 from webislife/v1
add vite.config.js, remove legacy code, improve readme.md
2023-05-31 21:18:38 +03:00
fabec69fa9 add vite.config.js, remove legacy code, improve readme.md 2023-05-31 21:18:09 +03:00
ef66238fec Merge pull request #13 from webislife/v1
push demo extensions
2023-05-19 02:51:33 +03:00
071e8f8eee push demo extensions 2023-05-19 02:50:51 +03:00
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
17 changed files with 1848 additions and 381 deletions

View File

@ -40,6 +40,11 @@ See full demo - [wc-wysiwyg demo](https://webislife.ru/demo/wc-wysiwyg/) list an
✅ Inserting `<video>` element
- ✅ Suppoer extensions
- Color text and background editor
- Emoji table
🚀 Vite support for wc-wysiwyg develop
## Install
@ -51,7 +56,7 @@ npm i wc-wysiwyg-editor --save
- Available package commands
Build scss styles
- Build scss styles
```
npm run sass
```
@ -63,11 +68,12 @@ npm run tsc
```
npm run babel-minify
```
build all stpes 1.sass 2.tsc 3.babel-minif
- build all stpes 1.sass 2.tsc 3.babel-minif
```
npm run build
```
- start vite serve mode for wc-wysiwyg development
## Integration WC-WYSIWYG element demo
<!--
@ -77,11 +83,26 @@ npm run build
</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
import('/src/components/wc-wysiwyg.js').then(esm => {
//you can pass any name into define fn
esm.define();
});
```
For use extensions, load before wc-wysiwig
```javascript
Promise.all([
import('./src/extensions/colorerDialog.ts'),
import('./src/extensions/emojiDialog.ts'),
import('./src/extensions/presetList.ts'),
]).then(modules => {
import('./src/wc-wysiwyg.ts').then(esm => esm.define());
});
```
And use in HTML

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 {
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 {
color: var(--color-red-900);
border-bottom: 1px solid var(--color-red-900);
background-color: var(--color-red-50);
text-decoration: none; }
@ -104,6 +95,7 @@ del:before {
ins {
color: var(--color-green-900);
border-bottom: 1px solid var(--color-green-900);
background-color: var(--color-green-50);
text-decoration: none; }

View File

@ -1,8 +1,85 @@
@charset "UTF-8";
:root {
--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;
--color-deep-orange-50: #FBE9E7;
--color-deep-orange-900: #BF360C;
--color-lime-50: #F9FBE7;
--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;
--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;
--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;
--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 {
background-color: #eee;
background-color: var(--wc-wysiwyg-light);
font-family: Arial, Helvetica, sans-serif;
position: relative;
border: 1px solid var(--color-blue-gray-400);
padding: 5px;
border-radius: 3px;
display: block;
/* custom elements */
@ -10,6 +87,37 @@
/* inline dialog */
/* props form */
/* hints */ }
.wc-wysiwyg_dialog {
border-radius: 10px;
border: none;
outline: none; }
.wc-wysiwyg_dialog::backdrop {
background: rgba(0, 0, 0, 0.8); }
.wc-wysiwyg_dialog.-modal {
min-width: 90vw;
min-height: 90vh;
max-height: 90vh;
max-height: 90vh; }
.wc-wysiwyg_dialog.-colors {
display: flex;
flex-wrap: wrap;
justify-content: center;
min-width: 0;
min-height: 0;
max-width: 300px; }
.wc-wysiwyg_dialog.-colors:not([open]) {
display: none; }
.wc-wysiwyg_dialog.-colors fieldset.-palette {
padding: 0;
display: flex;
flex: 1;
margin: 0;
border: 0;
outline: 0;
max-width: 20px; }
.wc-wysiwyg_dialog.-colors fieldset.-palette > button {
flex: 1;
margin: 2px; }
.wc-wysiwyg_bt {
display: block;
padding: 5px;
@ -27,36 +135,30 @@
border-radius: 3px; }
.wc-wysiwyg_ce {
position: relative;
margin: 0;
padding: 7px;
border: 1px solid var(--color-blue-500);
border-radius: 3px;
background-color: var(--color-blue-50); }
.wc-wysiwyg_ce:before {
content: 'HTML5 custom-elements';
color: #fff;
background-color: var(--color-blue-500);
position: absolute;
font-size: 10px;
line-height: 0.6em;
padding: 3px;
border-radius: 3px;
transform: translate(0, -50%);
top: 0;
left: 0; }
.wc-wysiwyg_ce > button {
border-color: var(--color-blue-500);
background-color: var(--color-blue-100); }
.wc-wysiwyg_ce > button:hover {
border-color: var(--color-blue-900);
background-color: var(--color-blue-200); }
margin-bottom: 5px;
padding: 5px;
border-radius: 5px;
border: none;
background: rgba(0, 0, 0, 0.2); }
.wc-wysiwyg_ce:before {
content: 'HTML5 custom-elements';
color: #fff;
background-color: var(--color-blue-500);
position: absolute;
font-size: 10px;
line-height: 0.6em;
padding: 3px;
border-radius: 3px;
transform: translate(0, -50%);
top: 0;
left: 0; }
.wc-wysiwyg_pr {
width: 100%;
box-sizing: border-box;
max-width: 100%;
min-height: 200px; }
.wc-wysiwyg_content {
padding: 5px 5px 2em 5px;
padding: 0.5em;
border: 1px solid #ccc;
background: #fff;
overflow-x: hidden;
@ -66,7 +168,7 @@
box-sizing: border-box;
margin: 0 auto;
box-sizing: border-box;
display: inline-block;
display: block;
resize: vertical; }
.wc-wysiwyg_content .-selected {
background-color: var(--color-blue-100); }
@ -76,46 +178,186 @@
.wc-wysiwyg_content.-invalid:focus {
outline: 5px solid var(--color-red-500); }
.wc-wysiwyg_ec {
background: var(--color-blue-gray-100);
padding: 0.5em 0.25em 0.25em 0.25em;
border-radius: 3px;
border: 1px solid;
border-color: var(--color-blue-gray-100); }
.wc-wysiwyg_ec:focus-within {
border-color: var(--color-blue-500); }
background: #2b393f;
padding: 5px;
border-radius: 5px;
margin-bottom: 10px;
top: 0;
position: sticky;
z-index: 2; }
.wc-wysiwyg_btn {
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;
min-width: 0px;
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;
min-width: 30px;
line-height: 20px;
background-color: var(--wc-wysiwyg-light);
box-shadow: 1px 2px 5px rgba(0, 0, 0, 0.3);
border: 0;
border-radius: 5px;
padding: 2px 5px;
margin-right: 5px;
user-select: none;
cursor: pointer; }
.wc-wysiwyg_btn:hover {
background: var(--color-blue-gray-100);
border-color: var(--color-blue-gray-300); }
box-shadow: 0 2px 5px rgba(0, 110, 253, 0.9); }
.wc-wysiwyg_btn:focus {
border-color: var(--color-blue-500); }
box-shadow: 0 2px 5px rgba(0, 110, 253, 0.9); }
.wc-wysiwyg_btn:active {
padding-top: 2px;
background: var(--color-blue-gray-100);
border-color: var(--color-blue-gray-700);
border-bottom: 1px solid; }
box-shadow: 0 2px 5px rgba(1, 181, 52, 0.9); }
.wc-wysiwyg_btn.-clear {
text-decoration: line-through;
font-weight: bold; }
.wc-wysiwyg_btn.-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; }
.wc-wysiwyg_btn.-color {
min-width: 20px;
min-height: 20px;
border-color: rgba(0, 0, 0, 0.2); }
.wc-wysiwyg_btn.-prevcolor::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); }
.wc-wysiwyg_btn.-b {
font-weight: bold; }
.wc-wysiwyg_btn.-i {
font-style: italic; }
.wc-wysiwyg_btn.-u {
text-decoration: underline; }
.wc-wysiwyg_btn.-s {
text-decoration: line-through; }
.wc-wysiwyg_btn.-sub {
vertical-align: sub;
font-size: 0.5em; }
.wc-wysiwyg_btn.-del::before {
content: '- ';
font-weight: 400; }
.wc-wysiwyg_btn.-h1::before {
content: '§ ';
font-weight: 400; }
.wc-wysiwyg_btn.-del {
color: var(--color-red-900);
border-bottom: 1px solid var(--color-red-900);
background-color: var(--color-red-50); }
.wc-wysiwyg_btn.-a::before {
content: "🔗 "; }
.wc-wysiwyg_btn.-ul::before {
content: "● "; }
.wc-wysiwyg_btn.-ol::before {
content: "1. "; }
.wc-wysiwyg_btn.-var::before {
content: "∫ "; }
.wc-wysiwyg_btn.-var {
font-weight: bold;
font-style: italic; }
.wc-wysiwyg_btn.-details:before {
content: "&rarr;"; }
.wc-wysiwyg_btn.-details {
text-decoration: dotted;
border: 1px dashed var(--color-blue-gray-200); }
.wc-wysiwyg_btn.-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; }
.wc-wysiwyg_btn.-pre {
background-color: var(--color-blue-gray-50);
color: var(--color-blue-grey-900);
padding-left: 15px; }
.wc-wysiwyg_btn.-ins::before {
content: '+ ';
font-weight: 400; }
.wc-wysiwyg_btn.-ins {
color: var(--color-green-900);
border-bottom: 1px solid var(--color-green-900);
background-color: var(--color-green-50); }
.wc-wysiwyg_btn.-sup {
vertical-align: super;
font-size: 0.5em; }
.wc-wysiwyg_btn.-q:before {
content: open-quote; }
.wc-wysiwyg_btn.-samp:before {
content: '> ';
color: var(--color-blue-gray-300);
font-family: sans-serif; }
.wc-wysiwyg_btn.-samp {
background-color: var(--color-blue-gray-50);
border-bottom: 1px solid var(--color-blue-gray-300); }
.wc-wysiwyg_btn.-blockquote::before {
content: '❝';
font-size: 1em;
color: #F57F17;
display: block;
position: absolute;
top: 0px;
left: 4px;
user-select: none; }
.wc-wysiwyg_btn.-blockquote {
background-color: var(--color-amber-50);
color: #412207;
padding-left: 15px;
border-left: 2px solid #F57F17; }
.wc-wysiwyg_btn.-time:before {
content: "📅 "; }
.wc-wysiwyg_btn.-img:before {
content: "🌅 "; }
.wc-wysiwyg_btn.-video:before {
content: "🎦 "; }
.wc-wysiwyg_btn.-audio:before {
content: "🎵 "; }
.wc-wysiwyg_btn.-details:before {
content: "▸ "; }
.wc-wysiwyg_btn.-code {
content: "<code>";
background-color: var(--color-blue-gray-50);
color: var(--color-blue-grey-900); }
.wc-wysiwyg_btn.-strong {
background-color: var(--color-deep-orange-50);
color: var(--color-deep-orange-900);
font-weight: 400; }
.wc-wysiwyg_btn.-abbr {
color: #1A237E; }
.wc-wysiwyg_btn.-q {
color: #000;
background-color: #FFF8E1; }
.wc-wysiwyg_btn.-small {
font-size: 0.5em; }
.wc-wysiwyg_btn.-dfn {
color: var(--color-blue-900);
font-style: italic; }
.wc-wysiwyg_btn.-mark {
background-color: var(--color-lime-100);
color: var(--color-lime-900); }
.wc-wysiwyg_btn.-mark:hover {
background-color: var(--color-lime-200);
color: var(--color-lime-900); }
.wc-wysiwyg_btn.-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; }
.wc-wysiwyg_ia {
display: flex;
flex-wrap: wrap; }
@ -128,17 +370,17 @@
position: fixed;
bottom: 0;
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; }
.wc-wysiwyg_di > form:nth-child(1n+2) {
margin-top: 10px; }
.wc-wysiwyg_pf {
display: flex;
border-radius: 3px;
padding: 3px;
margin: 10px 0;
background-color: var(--color-blue-100);
flex-wrap: nowrap;
font-size: 0.9em;
@ -157,7 +399,8 @@
background-color: var(--color-blue-200);
color: var(--color-blue-gray-800);
padding: 3px 3px 3px 5px;
display: flex;
margin: 5px 0;
display: inline-flex;
align-items: center;
border-radius: 6px;
margin-right: 5px;
@ -201,3 +444,17 @@
line-height: 10px; }
.wc-wysiwyg .-display-none {
display: none; }
@media (prefers-color-scheme: dark) {
.wc-wysiwyg {
background-color: var(--wc-wysiwyg-dark); }
.wc-wysiwyg_di {
background-color: var(--wc-wysiwyg-dark); }
.wc-wysiwyg_bt {
background-color: var(--wc-wysiwyg-dark); }
.wc-wysiwyg_btn {
background-color: rgba(0, 0, 0, 0.5);
color: #ccc; }
.wc-wysiwyg_btn:hover {
background-color: rgba(0, 0, 0, 0.5);
color: #ccc; } }

2
dist/wc-wysiwyg.js vendored

File diff suppressed because one or more lines are too long

320
index.html Normal file
View File

@ -0,0 +1,320 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>wc-wysiwyg</title>
</head>
<body>
<wc-wysiwyg
data-custom-tags='[{"method":"wrapTag","tag":"a","displayType":"inline","hint":"Продвинутая ссылка a is=\"wc-a\"","is":"wc-a"},{"method":"wrapTag","tag":"pre","displayType":"block","hint":"Вставка кода pre is=\"wc-pre\"","is":"wc-pre"},{"method":"wrapTag","tag":"time","displayType":"inline","hint":"Вставка кода pre is=\"wc-pre\"","is":"wc-time"}]'
data-edit-props='{"A":["href"],"ABBR":["title"],"PRE":["data-lang"],"LI":["data-liststyle"],"DFN":["title"],"H1":["id"],"H2":["id"],"H3":["id"],"H4":["id"],"H5":["id"],"H6":["id"],"SPAN":["class"],"TIME":["datetime"],"IMG":["alt","src","class"],"P":["class"],"AUDIO":["src"]}'
data-autocomplete="1"
data-content-class="a__content"
data-preset-list='[{"tag":"mark", "class": "-custom-mark", "name": "Выделенный синий"}, {"tag":"h1", "style":"color: red;", "name": "Красный заголовок H1"}, {"tag":"h2", "style":"color: green;", "name": "Зеленый заголовок H2"}]'
id="wceditor"
filtertags="script,iframe,object"
required
minlength="20"
maxlength="20000000"
data-preview-el="#preview">
<textarea><h1>wc-wysiwyg</h1>
<p><mark>Д</mark><small>е</small><sup>м</sup><sub>о</sub><dfn title="definiton title">н</dfn><kbd>с</kbd><strong>т</strong><abbr title="ABBR">р</abbr><code>а</code><q>ц</q><del>и</del><ins>я</ins> <samp>и</samp> обзор возможностей веб-компонента wc-wysiwyg</p>
<blockquote><b>WYSIWYG</b> (произносится [ˈwɪziwɪɡ], является аббревиатурой от англ. What You See Is What You Get, «что видишь, то и получишь») — свойство прикладных программ или веб-интерфейсов, в которых содержание отображается в процессе редактирования и выглядит максимально близко похожим на конечную продукцию, которая может быть печатным документом, веб-страницей или презентацией. В настоящее время для подобных программ также широко используется понятие «визуальный редактор».</blockquote>
<ul>
<li><a is="wc-a" href="https://github.com/webislife/wc-wysiwyg">github репозиторий</a></li>
<li><a class="wc-a" href="https://www.npmjs.com/package/wc-wysiwyg-editor">npm package</a></li>
</ul>
<h2>Базовые возможности редактора</h2>
<p>
Редактор вдохновлен <a is="wc-a" href="https://html.spec.whatwg.org/multipage/" v-is="HTML5" target="_blank">HTML5</a> идеями семантики веба и старается максимально их поддерживать. В основе интерактивности редактора лежит нативная javascript реализация с применением веб-компонентов и браузерных API. Данный веб-компонент не имеет зависимостей в <code>package.json</code> и не требует дополнительной установки каких либо библиотек и javascript фреймворков.
</p>
<h3>Текстовое форматирование</h3>
<p>
Текстовое базовое форматирование основано на HTML5 тегах, <abbr title="но никто не мешает их добавить"> вместо CSS стилей</abbr>. Базовая палитра цветов взята из <a is="wc-a" href="https://materialui.co/colors/">materialui.co</a>
</p>
<h4>Строчные элементы</h4>
<p>
<ol>
<li><code>b</code> - <b>жирный</b> </li>
<li><code>u</code> - <u>подчеркнутый</u></li>
<li><code>s</code> - <s>зачерекнутый</s></li>
<li><code>i</code> - <i>курсивный</i></li>
<li><code>q</code> - <q>цитата</q></li>
<li><code>small</code> - <small>маленький текст</small></li>
<li><code>sup</code> - <sup>надстрочный</sup></li>
<li><code>sub</code> - <sub>подстрочный</sub></li>
<li><code>mark</code> - <mark>выделенный текст</mark></li>
<li><code>strong</code> - <strong>важный текст</strong></li>
<li><code>samp</code> - <samp>вывод компьютерной программы</samp></li>
<li><code>kbd</code> - <kbd>ПРОБЕЛ</kbd> обозначение кнопки </li>
<li><code>del</code> - <del>удаленный текст</del> из документа </li>
<li><code>ins</code> - <ins>добавленный текст</ins> в документ </li>
<li><code>code</code> - Внутритекстовый <code>код</code> </li>
<li><code>var</code> - обозначение <var>X</var> переменных </li>
<li><code>dfn</code> - <dfn title="Тег dfn (сокращенно от definition)">определение</dfn></li>
<li><code>abbr</code> - <abbr title="Тег abbr (сокращенно от abbreviation)">аббрeвиатура</abbr></li>
</ol>
</p>
<h4>HTML5 строчные элементы</h4>
<p>
<ol>
<li>
<code>wc-time</code> - веб-компонент расширяющий возможности показа даты и времени, совместим с этим редактором, вы открыли эту страницу - <time is="wc-time" data-format-time="minute,hour,second"></time> больше возможностей и реализацию, <a is="wc-a" href="https://webislife.ru/demo/wc-time/">читайте здесь</a> или <a is="wc-a" href="https://github.com/webislife/wc-time">смотрите на github</a>.
<br> Сравните две даты <time datetime="12.12.2023 01:23">12.12.2023 01:23</time> и версию <code>is="wc-time"</code> <time is="wc-time" datetime="12.12.2023 01:23">12.12.2023 01:23</time>
</li>
<li><code>a is="wc-a"</code> - пример интеграции анонимного веб-компонента <a href="https://webislife.ru">просто ссылка</a> и <a https:="https://webislife.ru/demo/wc-editor" is="wc-a">wc-a ссылка</a></a></li>
</ol>
</p>
<h4>Аббревиатуры <code>abbr</code></h4>
<p>
Использование аббревиатур в статьях и комментариях бывает уместно и позволяет подсказать пользователям, что такое
<abbr title="Hyper Text Markup Language">HTML</abbr> или как расшифровать <abbr title="Имею Мнение Хрен Оспоришь">ИМХО</abbr> или
<abbr title="Синхрофазотро́н (от синхронизация + фаза + электрон) — резонансный циклический ускоритель с неизменной в процессе ускорения длиной равновесной орбиты.">Синхрофазотрон</abbr>
</p>
<h3>Блочные элементы</h3>
<p>
В блочные элементы входят все заголовки, а также:
</p>
<p>
<ul>
<li><code>details</code> - спойлер
<details>
<summary>Заголовок спойлера</summary>
текст в спойлере
</details>
</li>
<li><code>pre</code> - блок форматированного текста, примеры применения:
<pre data-lang="English proverb">Strike while the iron is hot</pre>
<pre data-lang="file.txt">содержимое file.txt</pre>
<pre data-lang="javascript">window.customElements.define('wc-wysiwyg', WCWYSIWYG);</pre>
А вот пример применения <code>is="wc-pre"</code>
<pre is="wc-pre" data-lang="javascript">window.customElements.define('wc-wysiwyg', WCWYSIWYG);</pre>
</li>
<li>
<code>audio</code> - а что если дать возможность читателю запустить трек перед длительным чтением статьи? Или автору вставлять аудиофрагменты
<audio controls src="https://www.chosic.com/wp-content/uploads/2021/04/Luke-Bergs-Bliss.mp3"></audio>
</li>
<li>
<code>video</code> - простая интеграция video не сложнее чем audio
<video controls width="300" src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4">
</video>
</li>
<li>
<code>figure</code> - тоже является блочным элементом, может быть использован для оборачивания изображений и audio элементов
<figure>
<audio controls src="https://www.chosic.com/wp-content/uploads/2021/04/Luke-Bergs-Bliss.mp3"></audio>
<figcaption>🎵 Luke-Bergs-Bliss.mp3</figcaption>
</figure>
</li>
<li>
<code>blockquote</code> - блок цитаты
<blockquote><h5>для больших</h5><br><h5>цитат</h5></blockquote>
</li>
</ul>
</p>
<h4>
Спойлер
</h4>
<p>
Для спойлеров используется тег <code>details</code>
</p>
<details>
<summary>Нажми, чтобы увидеть спойлер</summary>
<p>
Ура, спойлерить в комментариях можно! Но скрыть <abbr title="когда хочешь немного больше">одно или пару</abbr> можно и аббривеатурой.
</p>
</details>
<p>
Можно спрятать код в спойлер, если его чтение опционально или затрудняет статью
</p>
<details>
<summary>Код под спойлером</summary>
<p>
<pre is="wc-pre" data-lang="ts">//Все знакомые редатору теги
const allTags = [
{ tag: 'h1' },
{ tag: 'h2' },
{ tag: 'h3' },
{ tag: 'h4' },
{ tag: 'h5' },
{ tag: 'h6' },
{ tag: 'span' },
{ tag: 'mark' },
{ tag: 'small' },
{ tag: 'dfn' },
{ tag: 'a'},
{ tag: 'q'},
{ tag: 'b'},
{ tag: 'i'},
{ tag: 'u'},
{ tag: 's'},
{ tag: 'sup'},
{ tag: 'sub'},
{ tag: 'kbd'},
{ tag: 'abbr'},
{ tag: 'strong'},
{ tag: 'code'},
{ tag: 'samp'},
{ tag: 'del'},
{ tag: 'ins'},
{ tag: 'var'},
{ tag: 'ul'},
{ tag: 'ol'},
{ tag: 'pre'},
{ tag: 'time'},
{ tag: 'img'},
{ tag: 'audio'},
{ tag: 'video'},
{ tag: 'blockquote'},
] as WCWYSIWYGTag[];</pre>
</p>
</details>
<h4>
Цитаты
</h4>
<p>
Используем тег <code>q</code> для коротких <abbr title="внутритекстовых">inline</abbr> цитат
</p>
<p>
Есть такая фраза <q>Я не слишком богат, чтобы покупать вещи дважды</q> - вы слышали?
</p>
<p>
Используем тег <code>blockquote</code> для блока цитаты
</p>
<blockquote>
<b>БлокЦитаты</b> - лучше использовать отдельно<br>
Он лучше подходит для цитат и выдержек на несколько строк
</blockquote>
<p>
Для интеграции редактора на страницу достаточно в HTML разместить разметку
</p>
<pre data-lang="HTML">&lt; wc-wysiwyg id="wc-demo-comment"
data-allow-tags="strong,u,i,b,q,blockquote,a,img,pre"
data-storage="demo-comment"
data-hide-bottom-actions="1"
is="wc-wysiwyg"
required
minlength="5"
maxlength="500">
&lt; textarea>&lt; /textarea>&lt; wc-wysiwyg></pre>
<p>
Подключить модуль компонента на странице или импортировать его из <code>javaScript</code> и вызвать метод <code>define</code>. Пример подключения на этой странице:
</p>
<pre is="wc-pre" data-lang="javascript">import('/wp-content/themes/wl/ts/components/web/wc-editor.js').then(esm => {
esm.define();
});</pre>
<p>
Попытки вставить javaScript и прочие пакости как value для этого веб-компонента должны обрезаться и пресекаться на стороне сервера,
список доступных тегов для каждого ресурса может быть индивидуален.
</p>
<h4>Выравнивание текста</h4>
<p>В будущем автор возможно добавит выравнивание элементов кнопками, но на уровне CSS классов это доступно уже сейчас</p>
<p class="-text-center">Текст по центру с применением CSS класса <code>-text-center</code></p>
<p class="-text-right">Текст по правому краю с применением CSS класса <code>-text-right</code></p>
<p class="-text-left">Текст по левому краю с применением CSS класса <code>-text-left</code></p>
<h4>Копирование в буфер</h4>
<p>
А что если прямо в редакторе:
<ol>
<li>
нажать <kbd>ctrl</kbd>+<kbd>a</kbd> и просто все выделить
</li>
<li>
потом все вырезать <kbd>ctrl</kbd>+<kbd>X</kbd>
</li>
<li>
а потом вставить обратно <kbd>ctrl</kbd>+<kbd>V</kbd>
</li>
</ol>
Останется ли на месте все созданное? - <q>все кроме скрытого содержания спойлеров вернется на место, но будет вставлено в виде форматированного инлайн стилями html</q>
</p>
<h4>Списки</h4>
<ol>
<li> Нумерованный </li>
<li> Список </li>
</ol>
<ul>
<li> Маркированный </li>
<li> Список </li>
</ul>
<p>
🔥 Cписок возможностей <code>textarea is="wc-wysiwyg"</code> или
</p>
<ul>
<li data-listStyle="✅">Поддержка мультиязычности через аттрибут <a is="wc-a" href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/lang">HTMLElement.lang</a> <br>
🇷🇺/🇺🇸 поддерживаются по умолчанию</li>
<li data-listStyle="✅">CSS стили для всех популярных HTML5 тегов </li>
<li data-listStyle="✅">CSS поддержка emoji в маркированных списках </li>
<li data-listStyle="✅">Поддержка наследования стилей через CSS class в аттрибуте <code>data-content-class</code> emoji в маркированных списках </li>
<li data-listStyle="✅">Внутритекстовое действия над выделенным текстом </li>
<li data-listStyle="✅">Хранение значения в <code>window.localStorage</code> и восстановление после перезагрузки, проверьте в форме комментариев ниже</li>
<li data-listStyle="✅">Редактирование сразу нескольких атрибутов и свойств для тегов <code>h1-6</code> / <code>abbr</code> / <code>dfn</code> / <code>a</code> / <code>pre</code> / <code>ul > li</code> и любых других тегов, количество тегов и атрибуты настраиваются</li>
<li data-listStyle="✅">Autocomplete при вводе <code>/</code> для поддерживаемых тегов в новом параграфе</li>
<li data-listStyle="✅">Переключатель вида текст\HTML5</li>
<li data-listStyle="✅">Кнопка очистки формата <kbd>Ⱦ</kbd></li>
<li data-listStyle="✅">Предпросмотр в режиме реального времени</li>
<li data-listStyle="✅">Сочетания клавиш
<ol>
<li><kbd>alt</kbd>+<kbd>space</kbd> переключить текущий указатель каретки вне тега</li>
<li><kbd>Escape</kbd> закрыть нижнюю диалоговую панель редактора</li>
</ol>
</li>
<li data-listStyle="✅">Валидация <code>required</code>, <code>minlength</code>, <code>maxlength</code>, <code>filtertags</code></li>
<li data-listStyle="✅">Вставка <code>audio</code> элемента</li>
<li data-listStyle="✅">Вставка <code>video</code> элемента</li>
<li data-listStyle="⌛">
В разработке
<ul>
<li data-listStyle="🖊">Работа с таблицами</li>
<li data-listStyle="🖊">Изменение размеров изображения прямо в редакторе</li>
<li data-listStyle="🖊">Поддержка placeholder</li>
<li data-listStyle="🖊">Выравнивание текста по краям в параграфе с помощью кнопок</li>
<li data-listStyle="🖊">Цветовое оформление цвета и фона у текста с <code>colorPicker</code></li>
<li data-listStyle="🖊">Поддержка <dfn title="горячих клавиш">hotkeys</dfn></li>
<li data-listStyle="🖊">Разметка <dfn title="markdown разметки">.md</dfn> </li>
<li data-listStyle="🖊">Поддержка <dfn title="еще один популярный язык разметки">bbcode</dfn></li>
</ul>
</li>
</ul>
<h3>
Изображения
</h3>
<p>Изображение c описанием будет вставлено как <code>figure->img+figcaption</code> также описание картинки будет продублировано в <code>alt=""</code> аттрибут <code>img</code></p>
<figure>
<img src="https://webislife.ru/wp-content/uploads/2023/01/snimok-ekrana-2023-01-24-v-20.24.51.png">
<figcaption>
Поддержка браузерами <code>custom-elements</code> в 2023 году <code>figcaption</code>
</figcaption>
</figure>
<p>Изображение без описания будет вставлено как <code>img</code></p>
<img src="https://webislife.ru/wp-content/uploads/2023/01/snimok-ekrana-2023-01-20-v-21.22.19-e1674239288838.png" alt="https://webislife.ru/wp-content/uploads/2023/01/snimok-ekrana-2023-01-20-v-21.22.19-e1674239288838.png">
</textarea>
</wc-wysiwyg>
<style>
*, html, body {
margin:0;
padding:0;
}
</style>
<script>
import('./src/sass/content.scss');
import('./src/sass/wc-wysiwyg.scss');
Promise.all([
import('./src/extensions/colorerDialog.ts'),
import('./src/extensions/emojiDialog.ts'),
import('./src/extensions/presetList.ts'),
]).then(modules => {
import('./src/wc-wysiwyg.ts').then(esm => esm.define());
});
</script>
</body>
</html>

View File

@ -1,41 +1,43 @@
{
"name": "@webislife/wc-wysiwyg",
"version": "0.9.31",
"version": "1.0.4",
"description": "WYWSIWYG HTML5 Editor written in ts and designed by web-componennt, support all JS frameworks and browsers",
"main": "dist/wc-wysiwyg.js",
"type": "module",
"scripts": {
"sass": "./node_modules/node-sass/bin/node-sass ./src -o ./dist",
"tsc": "./node_modules/.bin/tsc",
"babel-minify": " ./node_modules/.bin/minify ./dist --mangle=false --out-dir ./dist --sourceType=module",
"build": "npm run sass && npm run tsc && npm run babel-minify"
"vite": "./node_modules/vite/bin/vite.js",
"sass": "./node_modules/node-sass/bin/node-sass ./src -o ./dist",
"tsc": "./node_modules/.bin/tsc",
"babel-minify": " ./node_modules/.bin/minify ./dist --mangle=false --out-dir ./dist --sourceType=module",
"build": "npm run sass && npm run tsc && npm run babel-minify"
},
"repository": {
"type": "git",
"url": "git+https://github.com/webislife/wc-wysiwyg.git"
"type": "git",
"url": "git+https://github.com/webislife/wc-wysiwyg.git"
},
"keywords": [
"web-component",
"custom-element",
"html5 editor",
"wysiwyg",
"html5",
"esnext",
"typescript"
"web-component",
"custom-element",
"html5 editor",
"wysiwyg",
"html5",
"esnext",
"typescript"
],
"author": "srokoff",
"license": "MIT",
"bugs": {
"url": "https://github.com/webislife/wc-wysiwyg/issues"
"url": "https://github.com/webislife/wc-wysiwyg/issues"
},
"homepage": "https://github.com/webislife/wc-wysiwyg#readme",
"devDependencies": {
"babel-minify": "^0.5.2",
"node-sass": "^8.0.0",
"typescript": "^4.9.4"
"babel-minify": "^0.5.2",
"node-sass": "^8.0.0",
"sass": "^1.62.1",
"typescript": "^4.9.4",
"vite": "^4.3.9"
},
"publishConfig": {
"@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 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}:{
classList?: string[],
@ -21,17 +21,14 @@
// element.classList
if(classList) {
for (let i = 0; i < classList.length; i++) {
const styleClass = classList[i];
element.classList.add(styleClass)
if(classList[i]){
element.classList.add(classList[i]);
}
}
}
// element.style[prop]
if(styles) {
const stylesKeys = Object.keys(styles);
for (let i = 0; i < stylesKeys.length; i++) {
const key = stylesKeys[i];
element.style[key] = styles[key];
}
Object.assign(element.style, styles);
}
// element[prop]
if(props) {
@ -51,11 +48,10 @@
}
}
}
//append child elements
if(append) {
for (let i = 0; i < append.length; i++) {
const appendEl = append[i];
element.append(appendEl);
}
element.append(...append);
}
return element;
};

View File

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

View File

@ -0,0 +1,220 @@
import { el } from '../core/el.js';
import WCWYSIWYG from '../wc-wysiwyg.js';
//Extnesion translates
const t = {
ru: {
bg: 'Фон',
text: 'Текст',
bgColor: 'Цвет фона',
textColor: 'Цвет текста',
},
en: {
bg: 'Background',
text: 'Text',
bgColor: 'Background color',
textColor: 'Text color',
}
};
const _t = (key:string, lang = navigator.language):string => t[lang] ? t[lang][key] || "-" : t["en"][key];
const Colors = {
red: ['FFEBEE', 'FFCDD2', 'EF9A9A', 'E57373', 'EF5350', 'F44336', 'E53935', 'D32F2F', 'C62828', 'B71C1C'],
pink: ['FCE4EC', 'F8BBD0', 'F48FB1', 'F06292', 'EC407A', 'E91E63', 'D81B60', 'C2185B', 'AD1457', '880E4F'],
purple: ['F3E5F5', 'E1BEE7', 'CE93D8', 'BA68C8', 'AB47BC', '9C27B0', '8E24AA', '7B1FA2', '6A1B9A', '4A148C'],
// deepPurple: ['EDE7F6', 'D1C4E9', 'B39DDB', '9575CD', '7E57C2', '673AB7', '5E35B1', '512DA8', '4527A0', '311B92'],
indigo: ['E8EAF6', 'C5CAE9', '9FA8DA', '7986CB', '5C6BC0', '3F51B5', '3949AB', '303F9F', '283593', '1A237E'],
blue: ['E3F2FD', 'BBDEFB', '90CAF9', '64B5F6', '42A5F5', '2196F3', '1E88E5', '1976D2', '1565C0', '0D47A1'],
// lightBlue: ['E1F5FE', 'B3E5FC', '81D4FA', '4FC3F7', '29B6F6', '03A9F4', '039BE5', '0288D1', '0277BD', '01579B'],
cyan: ['E0F7FA', 'B2EBF2', '80DEEA', '4DD0E1', '26C6DA', '00BCD4', '00ACC1', '0097A7', '00838F', '006064'],
teal: ['E0F2F1', 'B2DFDB', '80CBC4', '4DB6AC', '26A69A', '009688', '00897B', '00796B', '00695C', '004D40'],
green: ['E8F5E9', 'C8E6C9', 'A5D6A7', '81C784', '66BB6A', '4CAF50', '43A047', '388E3C', '2E7D32', '1B5E20'],
// lightGreen: ['F1F8E9', 'DCEDC8', 'C5E1A5', 'AED581', '9CCC65', '8BC34A', '7CB342', '689F38', '558B2F', '33691E'],
lime: ['FFFDE7', 'FFF9C4', 'FFF59D', 'FFF176', 'FFEE58', 'FFEB3B', 'FDD835', 'FBC02D', 'F9A825', 'F57F17'],
yellow: ['FFF8E1', 'FFECB3', 'FFE082', 'FFD54F', 'FFCA28', 'FFC107', 'FFB300', 'FFA000', 'FF8F00', 'FF6F00'],
// amber: ['FFF3E0', 'FFE0B2', 'FFCC80', 'FFB74D', 'FFA726', 'FF9800', 'FB8C00', 'F57C00', 'EF6C00', 'E65100'],
orange: ['FBE9E7', 'FFCCBC', 'FFAB91', 'FF8A65', 'FF7043', 'FF5722', 'F4511E', 'E64A19', 'D84315', 'BF360C'],
// deepOrange: ['EFEBE9', 'D7CCC8', 'BCAAA4', 'A1887F', '8D6E63', '795548', '6D4C41', '5D4037', '4E342E', '3E2723'],
brown: ['EFEBE9', 'D7CCC8', 'BCAAA4', 'A1887F', '8D6E63', '795548', '6D4C41', '5D4037', '4E342E', '3E2723'],
grey:['FAFAFA', 'F5F5F5', 'EEEEEE', 'E0E0E0', 'BDBDBD', '9E9E9E', '757575', '616161', '424242', '212121'],
blueGrey: ['ECEFF1', 'CFD8DC', 'B0BEC5', '90A4AE', '78909C', '607D8B', '546E7A', '455A64', '37474F', '263238'],
};
class WCWYSIWYGExtensionColorerDialog {
WCWYSIWYG:WCWYSIWYG
ColorerText:HTMLButtonElement
ColorerBackground:HTMLButtonElement
ColorerDialogLabel:HTMLLabelElement
ColorerDialogColorInput:HTMLInputElement
Dialog:HTMLDialogElement|null = null
ActiveColors:{
text:string|null,
bg:string|null,
}
ColorerTarget: HTMLElement
constructor(WYSIWYG) {
this.WCWYSIWYG = WYSIWYG;
this.ActiveColors = new Proxy({
text: null,
bg: null,
}, {
get(target,prop) {
return target[prop]
},
set: (target, prop, value) => {
console.log('set color', value, prop);
this.ColorerDialogColorInput.value = value;
target[prop] = value;
if(prop == 'text') {
this.ColorerTarget.style.color = value;
this.ColorerText.setAttribute('style', `--colorer:${value}`);
} else {
this.ColorerTarget.style.backgroundColor = value;
this.ColorerBackground.setAttribute('style', `--colorer:${value}`);
}
return true
}
});
console.log('colorer constructor', this);
}
connectedCallback() {
console.log('colorer connectedCallback');
this.ColorerText = el('button', {
classList: ['wc-wysiwyg_btn', '-prevcolor'],
props: {
innerHTML: _t('text'),
onpointerup: () => this.#showDialog('text'),
},
attrs: {
"data-hint": _t('textColor')
},
});
this.ColorerBackground = el('button', {
classList: ['wc-wysiwyg_btn', '-prevcolor'],
props: {
innerHTML: _t('bg'),
onpointerup: () => this.#showDialog('bg'),
},
attrs: {
"data-hint": _t('bgColor')
},
});
this.ColorerDialogLabel = el('label', {
props: {
innerText: ''
}
});
this.ColorerDialogColorInput = el('input', {
props: {
type: 'color',
value: '#fffccc'
}
});
this.WCWYSIWYG.addEventListener('editprops', (event:CustomEvent) => {
if(event.detail.eventTarget) {
const target = event.detail.eventTarget;
console.log('check target', target);
this.ColorerTarget = target;
this.ActiveColors.text = this.#rgbToHex(target.style.color);
this.ActiveColors.bg = this.#rgbToHex(target.style.backgroundColor);
// this.ColorerText.setAttribute('data-color', value);
// this.ColorerBackground.setAttribute('data-color', value);
}
});
this.WCWYSIWYG.EditorActionsSection.append(
this.ColorerText,
this.ColorerBackground
);
}
/**
* Converts a rgb string to hex string
* @param {String} rgbString - A string of rgb values (e.g. "255, 0, 128")
* @return {String} A hex string in the format of #RRGGBB (e.g. "#FF0080")
*/
#rgbToHex(rgbString) {
const arrRgb = rgbString.match(/([0-9]{1,3})/gm);
if(arrRgb === null || arrRgb.length !== 3) {
return null;
}
const r = +arrRgb[0];
const g = +arrRgb[1];
const b = +arrRgb[2];
return "#" + (1 << 24 | r << 16 | g << 8 | b).toString(16).slice(1);
}
#showDialog(colorType:string) {
console.log('show dialog');
if(this.Dialog === null) {
this.Dialog = el('dialog', {
classList: ['wc-wysiwyg_dialog', '-modal', '-colors'],
props: {
innerHTML: (() => {
let html = '';
for(let colorName in Colors) {
html += `<fieldset class="-palette">`;
for (let i = 0; i < Colors[colorName].length; i++) {
const hexColor = Colors[colorName][i];
html += `<button class="wc-wysiwyg_btn -color" data-color="#${hexColor}" style="background-color: #${hexColor};"></button>`;
}
html += `</fieldset>`;
}
return html
})(),
onpointerup: event => {
if(event.target.classList.contains('-color')) {
this.ActiveColors[colorType] = event.target.getAttribute('data-color');
}
}
},
append: [
el('fieldset', {
styles: {
background: 'var(--color-blue-gray-50)',
outline: '0',
display: 'flex',
border: '0',
marginTop: '10px',
borderRadius: '0.5em',
width: '100%',
},
append: [
this.ColorerDialogLabel,
this.ColorerDialogColorInput,
el('button', {
classList: ['wc-wysiwyg_btn'],
props: {
type: 'button',
innerText: 'OK',
onpointerup: event => event.target.closest('dialog').close(),
}
}),
el('button', {
classList: ['wc-wysiwyg_btn'],
props: {
type: 'button',
innerText: 'Clear',
onpointerup: event => this.ColorerDialogColorInput.value = null
}
}),
]
})
]
});
this.WCWYSIWYG.EditorActionsSection.append(this.Dialog);
// this.WCWYSIWYG.EditorInlineDialog.append(this.Dialog);
}
this.Dialog.onpointerup = event => {
const target = event.target as HTMLElement;
if(target.classList.contains('-color')) {
this.ActiveColors[colorType] = target.getAttribute('data-color');
}
};
this.ColorerDialogLabel.innerText = _t(colorType);
this.Dialog.showModal();
}
}
export default WCWYSIWYGExtensionColorerDialog;

View File

@ -0,0 +1,100 @@
import { el } from '../core/el.js';
//Extnesion translates
const t = {
ru: {
emoji: 'Смайлики',
action: 'Нажмите чтобы скопировать',
emoticons: 'Эмоции',
dingbats: 'Значки',
map: 'Транспорт и карты',
additional: 'Дополнительные',
},
en: {
emoji: 'Emoji',
action: 'Click to copy',
emoticons: 'Emoticons',
dingbats: 'Dsingbats',
map: 'Transport and map',
additional: 'Other additional',
}
};
const _t = (key:string, lang = navigator.language):string => t[lang] ? t[lang][key] || "-" : t["en"][key];
/**
* Emoji dialog with list of emoji buttons
*/
class WCWYSIWYGExtensionEmojiDialog {
Dialog: HTMLDialogElement|null = null
WCWYSIWYG:any
constructor(WCWYSIWYG:any) {
this.WCWYSIWYG = WCWYSIWYG;
}
connectedCallback() {
this.WCWYSIWYG.EditorActionsSection.append(el('button', {
classList: ['wc-wysiwyg_btn'],
props: {
innerHTML: '😃',
onpointerup: () => this.#showDialog(),
},
attrs: {
"data-hint": _t('emoji')
}
}));
}
#showDialog() {
//Check if first show dialog
if(this.Dialog === null) {
const emojiRanges = {
emoticons: [128513, 128591],
dingbats: [9986,10160],
map: [128640,128704],
additional: [127757,128359],
};
this.Dialog = el('dialog', {
classList: ['wc-wysiwyg_dialog', '-modal'],
styles: {
maxWidth: '90vw',
},
props: {
onpointerup: event => {
const target = event.target;
if(target.tagName === 'BUTTON') {
const emojiCode = target.innerHTML;
const data = [
new ClipboardItem({ "text/html": new Blob([emojiCode], { type:"text/html" }) }),
];
navigator.clipboard.write(data).then(
() => {
console.log('writed');
this.Dialog.close();
},
(err) => { throw new Error("WC-WYSIWYG: Copy emoji to clipboard failed", err); }
);
}
},
innerHTML: (() => {
let html = '';
let showFirst = false;
for (let range in emojiRanges) {
html += `<details class="wc-wysiwyg_ed" ${showFirst === false ? 'open': ''}><summary>${_t(range)}</summary>`;
for (let emojiCode = emojiRanges[range][0]; emojiCode < emojiRanges[range][1]; emojiCode++) {
html += '<button class="wc-wysiwyg_btn -emoji" data-hint="' + _t('action') + '">&#'+emojiCode+';</button>';
}
html += '</details>';
showFirst = true;
}
return html;
})()
}
});
this.WCWYSIWYG.append(this.Dialog);
}
this.Dialog.showModal();
}
}
//Put extension in global view
(window._WCWYSIWYG !== undefined) ? window._WCWYSIWYG.extensions.push(WCWYSIWYGExtensionEmojiDialog) : window._WCWYSIWYG = {extensions:[WCWYSIWYGExtensionEmojiDialog]};
export default WCWYSIWYGExtensionEmojiDialog;

View File

@ -0,0 +1,114 @@
import { el } from '../core/el.js';
/**
* Presets format
*/
interface WCWYSIWYGPreset {
/**
* Preset name on drop down
*/
name:string,
/**
* tag name
*/
tag:string,
/**
* will set to HTMLElement.className property
*/
class?:string,
/**
* will set to HTMLElement.style property
*/
style?:string
}
class WCWYSIWYGExtensionPresetList {
PresetList: HTMLElement|null = null
PresetBtn:HTMLElement
Presets: Array<WCWYSIWYGPreset> = []
WCWYSIWYG:any
constructor(WCWYSIWYG:any) {
this.WCWYSIWYG = WCWYSIWYG;
this.Presets = JSON.parse(WCWYSIWYG.getAttribute('data-preset-list'));
this.PresetBtn = el('button', {
classList: ['wc-wysiwyg_btn'],
props: {
innerHTML: 'Оформление',
onpointerup: () => this.#showPresetList(),
},
});
}
connectedCallback() {
if(this.Presets !== null) {
this.WCWYSIWYG.EditorActionsSection.append(this.PresetBtn);
}
}
#showPresetList() {
console.log('show dialog');
if(this.PresetList === null) {
const section = el("section", { });
//Make presets blocks
this.Presets.forEach(preset => {
const presetEl = el(preset.tag, {
props: {
innerHTML: `${preset.name}`,
className: preset.class || null,
style: preset.style || null,
}
});
section.append(el('button', {
props: {
type: 'button',
style: "cursor:pointer;border: 1px solid rgba(0,0,0,0.5); margin-bottom:5px; border-radius:5px; background: transparent; display:block;",
onclick: e => this.#makePreset(preset),
},
append: [presetEl],
}));
});
//Close dialog button
this.PresetList = el('div', {
// classList: ['wc-wysiwyg_ec'],
append: [
section,
el('button', {
classList: ['wc-wysiwyg_btn'],
props: {
type: 'button',
innerText: 'Закрыть',
onclick: event => this.PresetList.style.display = 'none'
}
})
]
});
// this.WCWYSIWYG.EditorActionsSection.append(this.PresetList);
this.WCWYSIWYG.EditorActionsSection.insertAdjacentElement('beforeend', this.PresetList);
// this.PresetBtn.append(this.PresetList);
}
// this.PresetList.show();
this.PresetList.style.display = 'block';
}
#makePreset(preset:WCWYSIWYGPreset) {
const editorSelection = this.WCWYSIWYG.getSelection();
console.log('make preset', preset, editorSelection);
let tagNode = el(preset.tag, {});
if(preset.style) {
tagNode.setAttribute('style', preset.style);
}
if(preset.class) {
tagNode.className = preset.class;
}
if (editorSelection.selection !== null && editorSelection.selection.rangeCount && editorSelection.text !== null) {
const range = editorSelection.selection.getRangeAt(0).cloneRange();
range.surroundContents(tagNode);
editorSelection.selection.removeAllRanges();
editorSelection.selection.addRange(range);
tagNode.innerText = editorSelection.text;
this.WCWYSIWYG.updateContent();
// this.WCWYSIWY.#checkEditProps({target:tagNode, stopPropagation: () => false});
}
}
}
//Put extension in global view
(window._WCWYSIWYG !== undefined) ? window._WCWYSIWYG.extensions.push(WCWYSIWYGExtensionPresetList) : window._WCWYSIWYG = {extensions:[WCWYSIWYGExtensionPresetList]};
export default WCWYSIWYGExtensionPresetList;

View File

@ -90,19 +90,10 @@ h5 {
h6 {
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 {
color: var(--color-red-900);
border-bottom: 1px solid var(--color-red-900);
background-color: var(--color-red-50);
text-decoration: none;
}
@ -119,6 +110,7 @@ del:before {
}
ins {
color: var(--color-green-900);
border-bottom: 1px solid var(--color-green-900);
background-color: var(--color-green-50);
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 {
background-color: #eee;
background-color: var(--wc-wysiwyg-light);
font-family: Arial, Helvetica, sans-serif;
position: relative;
border: 1px solid var(--color-blue-gray-400);
padding:5px;
border-radius: 3px;
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 {
display: block;
padding:5px;
@ -27,31 +158,23 @@
&_ce {
position: relative;
margin: 0;
padding: 7px;
border: 1px solid var(--color-blue-500);
border-radius: 3px;
background-color: var(--color-blue-50);
}
&_ce:before {
content: 'HTML5 custom-elements';
color: #fff;
background-color: var(--color-blue-500);
position: absolute;
font-size: 10px;
line-height: 0.6em;
padding: 3px;
border-radius: 3px;
transform: translate(0, -50%);
top: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);
margin-bottom: 5px;
padding: 5px;
border-radius: 5px;
border: none;
background: rgba(0,0,0,0.2);
&:before {
content: 'HTML5 custom-elements';
color: #fff;
background-color: var(--color-blue-500);
position: absolute;
font-size: 10px;
line-height: 0.6em;
padding: 3px;
border-radius: 3px;
transform: translate(0, -50%);
top:0;
left:0;
}
}
/* preview */
@ -62,7 +185,7 @@
min-height: 200px;
}
&_content {
padding:5px 5px 2em 5px;
padding:0.5em;
border:1px solid #ccc;
background: #fff;
overflow-x: hidden;
@ -72,7 +195,7 @@
box-sizing: border-box;
margin: 0 auto;
box-sizing: border-box;
display: inline-block;
display: block;
resize: vertical;
& .-selected {
background-color: var(--color-blue-100);
@ -86,52 +209,237 @@
}
}
&_ec {
background: var(--color-blue-gray-100);
padding: 0.5em 0.25em 0.25em 0.25em;
border-radius: 3px;
border: 1px solid;
border-color: var(--color-blue-gray-100);
}
&_ec:focus-within {
border-color: var(--color-blue-500);
background: #2b393f;
padding: 5px;
border-radius: 5px;
margin-bottom: 10px;
top:0;
position: sticky;
z-index: 2;
}
&_btn {
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;
min-width: 0px;
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;
min-width: 30px;
line-height: 20px;
background-color: var(--wc-wysiwyg-light);
box-shadow: 1px 2px 5px rgba(0,0,0,0.3);
border:0;
border-radius: 5px;
padding:2px 5px;
margin-right:5px;
user-select: none;
cursor: pointer;
&:hover {
background: var(--color-blue-gray-100);
border-color: var(--color-blue-gray-300);
box-shadow: 0 2px 5px rgba(0, 110, 253, 0.9);
}
&:focus {
border-color: var(--color-blue-500);
box-shadow: 0 2px 5px rgba(0, 110, 253, 0.9);
}
&:active {
padding-top:2px;
background: var(--color-blue-gray-100);
border-color: var(--color-blue-gray-700);
border-bottom: 1px solid;
box-shadow: 0 2px 5px rgba(1, 181, 52, 0.9);
}
&.-clear {
text-decoration: line-through;
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 {
display: flex;
@ -148,11 +456,10 @@
position: fixed;
bottom:0;
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;
background-color: var(--wc-wysiwyg-light);
border-radius: 5px;
padding: 5px;
& > form {
&:nth-child(1n+2) {
margin-top: 10px;
@ -164,6 +471,7 @@
display: flex;
border-radius: 3px;
padding: 3px;
margin: 10px 0;
background-color: var(--color-blue-100);
flex-wrap: nowrap;
font-size: 0.9em;
@ -184,7 +492,8 @@
background-color: var(--color-blue-200);
color: var(--color-blue-gray-800);
padding: 3px 3px 3px 5px;
display: flex;
margin: 5px 0;
display: inline-flex;
align-items: center;
border-radius: 6px;
margin-right: 5px;
@ -239,4 +548,23 @@
& .-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,12 @@
import {t} from './core/translates.js';
import { t } from './core/translates.js';
import { el } from "./core/el.js";
interface WCWYSIWYGTag {
tag:string
method?:Function,
hint?:string,
is?: string,
}
interface WCWYSIWYGActions {
wrapTag: Function,
insertImageBlock: Function,
insertAudio: Function,
insertVideo: Function,
}
/**
* Translate function
* @param key:string phrase key
* @param lang:string language key defaul navigatol.language
* @returns
*/
const _t = (key:string, lang = navigator.language):string => t[lang] ? t[lang][key] || "-" : t["en"][key];
//All semantic html5 known editor tags
const allTags = [
@ -50,22 +44,38 @@ const allTags = [
{ tag: 'audio'},
{ tag: 'video'},
{ tag: 'blockquote'},
{ tag: 'details'},
] as WCWYSIWYGTag[];
/**
* Base class for WCWYSIWYG web component
*/
class WCWYSIWYG extends HTMLElement {
/**
* Tags included to action bar in editor
*/
public EditorTags:WCWYSIWYGTag[]
/**
* Custom tags included to action bar in editor
*/
public EditorCustomTags:WCWYSIWYGTag[]
//Content editable wc-editor element
/**
* Main ContentEditable element
*/
public EditorNode:HTMLElement
/**
* Editor actions block elements with EditorTags buttons
*/
public EditorActionsSection:HTMLElement
/**
* Custom web-components buttons <fieldset>
*/
public EditorCustomTagsForm?:HTMLElement
//Inline edites
public EditorInlineActions:any[]
public EditorInlineDialog:HTMLDialogElement
public EditorInlineActionsForm:HTMLElement
//Editor props
public EditorPropertyForm?:HTMLElement
//Clear btn
public EditorClearFormatBtn:HTMLElement
public EditorClearFormatBtn:HTMLButtonElement
//Autocomplete area
public EditorAutoCompleteForm?:HTMLElement
//Bottom actions
@ -74,7 +84,6 @@ class WCWYSIWYG extends HTMLElement {
public EditorBottomFormViewToggle?:HTMLElement
public EditorPreviewText:HTMLTextAreaElement
public EditorCustomTagsForm?:HTMLElement
public EditorTagsMethods:WCWYSIWYGActions
public EditorAllowTags:string[]
public EditorFullScreenButton?:HTMLElement
@ -85,26 +94,24 @@ class WCWYSIWYG extends HTMLElement {
#EditProps:boolean|object
#Autocomplete:boolean
#Extensions?: any[]
#SotrageKey:string|null
#HideBottomActions:boolean
#Connected:boolean = false;
constructor() {
super();
this.#checkExtensions();
this.classList.add('wc-wysiwyg');
//Listen root element events
this.onpointerup = (event) => {
const selection = window.getSelection();
const editorSelection = this.getSelection();
//if check exist selection string
if(selection !== null && selection.toString().length > 0) {
this.EditorInlineActionsForm.style.display = '';
if(editorSelection.selection !== null && editorSelection.selection.text !== null) {
if(this.EditorPropertyForm){
this.EditorPropertyForm.style.display = 'none';
}
this.showEditorInlineDialog();
} else {
this.hideEditorInlineDialog();
}
};
this.onfullscreenchange = (event) => {
@ -115,6 +122,23 @@ class WCWYSIWYG extends HTMLElement {
connectedCallback() {
if(this.#Connected === false) {
const asyncExtensions = this.getAttribute('data-async-extensions') || false;
if(asyncExtensions !== false) {
const asyncExtensionsPaths = asyncExtensions.split(',');
asyncExtensionsPaths.forEach((extensionPath:string) => {
import(extensionPath).then(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
const allowTags = this.getAttribute('data-allow-tags') || allTags.map(t => t.tag).join(',');
@ -136,32 +160,23 @@ class WCWYSIWYG extends HTMLElement {
//allow inline without ['video','audio','img']
this.EditorInlineActions = this.EditorTags.filter(action => ['video','audio','img'].includes(action.tag) === false);
this.EditorActionsSection = el('section', { classList: ['wc-wysiwyg_ec'] });
//Clear format button
this.EditorClearFormatBtn = el('button', {
classList: ['wc-wysiwyg_btn', '-clear'],
attrs: {
'data-hint': this.#t('clearFormat'),
'data-hint': _t('clearFormat'),
},
props: {
innerHTML:'Ⱦ',
},
});
//Inline selection actions panel
this.EditorInlineActionsForm = el('form');
this.EditorInlineDialog = el('dialog', {
classList: ['wc-wysiwyg_di'],
append: [this.EditorInlineActionsForm, this.EditorClearFormatBtn],
props: {
//prevent submit
onsubmit: event => {
event.preventDefault();
event.stopPropagation();
},
}
//Top editor actions sections
this.EditorActionsSection = el('section', {
classList: ['wc-wysiwyg_ec'],
});
this.#makeActionButtons(this.EditorActionsSection, this.EditorTags);
this.EditorActionsSection.append(this.EditorClearFormatBtn);
//Edit props
if(this.#EditProps) {
@ -172,12 +187,12 @@ class WCWYSIWYG extends HTMLElement {
props: {
onsubmit: event => {
event.preventDefault();
this.hideEditorInlineDialog();
this.EditorPropertyForm.style.display = 'none';
},
onpointerup: event => event.stopPropagation(),
}
});
this.EditorInlineDialog.append(this.EditorPropertyForm);
this.EditorActionsSection.append(this.EditorPropertyForm);
}
//Autocomplete form
@ -202,9 +217,7 @@ class WCWYSIWYG extends HTMLElement {
}
//Actions in footer
this.EditorBottomForm = el('fieldset', {
classList: ['wc-wysiwyg_bt'],
});
this.EditorBottomForm = el('fieldset', { classList: ['wc-wysiwyg_bt'] });
//Check custom tags
this.EditorCustomTags = JSON.parse( String(this.getAttribute('data-custom-tags')) );
@ -212,11 +225,11 @@ class WCWYSIWYG extends HTMLElement {
//Custom panel tags
this.EditorCustomTagsForm = el('fieldset', {
classList: ['wc-wysiwyg_ce'],
});
}) as HTMLElement;
//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
@ -224,72 +237,48 @@ class WCWYSIWYG extends HTMLElement {
classList: ['wc-wysiwyg_content', this.getAttribute('data-content-class') || ''],
props: {
contentEditable: true,
//Pointer event behaviors
onpointerup: event => {
this.checkCanClearElement(event);
this.#checkCanClearElement(event);
if(this.#EditProps) {
this.checkEditProps(event);
this.#checkEditProps(event);
}
},
//Update content on input event
oninput: event => {
this.updateContent();
if(this.#Autocomplete) {
this.checkAutoComplete();
this.#checkAutoComplete();
}
},
//Handle key bindings
//Check hot keys is pressed
onkeydown: event => {
//check hold alt
if(event.altKey) {
//alt+space - move caret to parent node next sibling
if(event.code === 'Space') {
const Selection = window.getSelection();
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 = 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();
}
this.#checkKeyBindings(event);
},
onpaste: (event:ClipboardEvent) => {
// if(event.type === 'paste') {
// window.navigator.clipboard.readText().then(text => {
// const target = event.target as HTMLElement;
// //Check isnert html symbol
// if(text.startsWith('&#') === false) {
// if(target.tagName === 'BR') {
// target.insertAdjacentText('afterend',text);
// }
// this.updateContent();
// event.preventDefault();
// event.stopPropagation();
// return false;
// }
// });
// }
}
},
});
//Make action buttons
this.#makeActionButtons(this.EditorActionsSection, this.EditorTags);
this.#makeActionButtons(this.EditorInlineActionsForm, this.EditorInlineActions);
//Inser wc-editor after textarea node
this.append(
this.EditorActionsSection,
this.EditorInlineDialog,
this.EditorNode,
);
@ -298,7 +287,7 @@ class WCWYSIWYG extends HTMLElement {
this.EditorBottomFormViewToggle = el('button', {
classList: ['wc-wysiwyg_btn'],
attrs: {
'data-hint': this.#t('toggleViewMode'),
'data-hint': _t('toggleViewMode'),
'data-mode': 'html5',
},
props: {
@ -320,11 +309,11 @@ class WCWYSIWYG extends HTMLElement {
this.EditorBottomFormNewP = el('button', {
classList: ['wc-wysiwyg_btn'],
attrs: {
'data-hint': this.#t('addNewParahraph'),
'data-hint': _t('addNewParahraph'),
},
props: {
type:'button',
innerText: '+ P',
innerText: '+ ',
onpointerup: event => {
const P = el('p', {props: {innerText: '/'}});
this.EditorNode.appendChild(P);
@ -336,7 +325,7 @@ class WCWYSIWYG extends HTMLElement {
this.EditorFullScreenButton = el('button', {
classList: ['wc-wysiwyg_btn'],
attrs: {
'data-hint': this.#t('fullScreen'),
'data-hint': _t('fullScreen'),
},
props: {
type: "button",
@ -358,14 +347,16 @@ class WCWYSIWYG extends HTMLElement {
this.EditorNode.innerHTML = this.EditorPreviewText.value;
//Check local storage key
this.#SotrageKey = this.getAttribute('data-storage');
console.log('storage key is ', this.#SotrageKey);
if(this.#SotrageKey) {
let storeValue = window.localStorage.getItem(this.#SotrageKey);
console.log('restore from storage', storeValue);
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.#Connected = true;
@ -376,14 +367,20 @@ class WCWYSIWYG extends HTMLElement {
* Update content value and update behaviors
*/
updateContent() {
// Update the value of the editor node to the current innerHTML
this.value = this.EditorNode.innerHTML;
// Update the value of the EditorPreviewText to the current value
this.EditorPreviewText.value = this.value;
// Check the validity of the current value
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) {
window.localStorage.setItem(this.#SotrageKey, this.value);
}
this.dispatchEvent(new Event('oninput', { bubbles: true, cancelable: false }));
this.updatePreviewEl(this.getAttribute('data-preview-el'));
}
/**
@ -411,19 +408,19 @@ class WCWYSIWYG extends HTMLElement {
if(this.getAttribute('required') !== null) {
if(String(this.EditorNode.textContent).length === 0) {
hasErros = true;
errors.push(this.#t('required'));
errors.push(_t('required'));
}
}
if(Number(this.getAttribute('minlength'))) {
if(String(this.EditorNode.textContent).length < Number(this.getAttribute('minlength'))) {
hasErros = true;
errors.push(`${this.#t('minlength')} ${this.getAttribute('minlength')}`);
errors.push(`${_t('minlength')} ${this.getAttribute('minlength')}`);
}
}
if(Number(this.getAttribute('maxlength'))) {
if(String(this.EditorNode.textContent).length > Number(this.getAttribute('maxlength'))) {
hasErros = true;
errors.push(`${this.#t('maxlength')} ${this.getAttribute('maxlength')}`);
errors.push(`${_t('maxlength')} ${this.getAttribute('maxlength')}`);
}
}
const filterTags = this.getAttribute('filtertags');
@ -433,7 +430,7 @@ class WCWYSIWYG extends HTMLElement {
const checkTag = disallowTags[i];
if(this.EditorNode.querySelector(checkTag)) {
hasErros = true;
errors.push(`${this.#t('filtertags')} ${checkTag}`);
errors.push(`${_t('filtertags')} ${checkTag}`);
break;
}
}
@ -459,19 +456,19 @@ class WCWYSIWYG extends HTMLElement {
/**
* Check if need append autocompleted tags variants
*/
checkAutoComplete() {
#checkAutoComplete() {
//CHeck autococmplete
const Selecton = window.getSelection();
if(Selecton !== null && Selecton.anchorNode !== null) {
const SelectionParentEl = Selecton.anchorNode.parentElement as HTMLParagraphElement;
const editorSelection = this.getSelection();
if(editorSelection.selection !== null && editorSelection.selection.anchorNode !== null) {
const SelectionParentEl = editorSelection.selection.anchorNode.parentElement as HTMLParagraphElement;
const AutoCompleteForm = this.EditorAutoCompleteForm as HTMLElement;
if(SelectionParentEl !== null &&
//if empty selection
Selecton.toString() === '' &&
//and parent node is <p>
SelectionParentEl.nodeName === 'P' &&
//and parent <p> is parentElement in EditorNode
SelectionParentEl.parentElement === this.EditorNode) {
//if empty selection
editorSelection.text === null &&
//and parent node is <p>
SelectionParentEl.nodeName === 'P' &&
//and parent <p> is parentElement in EditorNode
SelectionParentEl.parentElement === this.EditorNode) {
//and parent <p> inner text starts with `/`
if(SelectionParentEl.innerText.startsWith('/')) {
const parsedTagName = SelectionParentEl.innerText.replace('/', '');
@ -482,7 +479,7 @@ class WCWYSIWYG extends HTMLElement {
AutoCompleteForm?.appendChild(el('button', {
classList: ['wc-wysiwyg_btn', `-${action.tag}`],
attrs: {
'data-hint': this.#t(action.tag) || null,
'data-hint': _t(action.tag) || null,
},
props: {
type: 'submit',
@ -501,45 +498,28 @@ class WCWYSIWYG extends HTMLElement {
}
}
}
}
}
}
}
/**
* Show and position inline actions dialog at targetNode
**/
showEditorInlineDialog() {
this.EditorInlineDialog.show();
}
/**
* Hide inline dialog
**/
hideEditorInlineDialog() {
if(this.#EditProps){
this.EditorPropertyForm.style.display = 'none';
}
this.EditorInlineDialog.close();
}
/**
* Checking and clear tag, if can do it
* Checking clear form and clear, if can do it
* @param event
*/
checkCanClearElement(event:Event) {
const eventTarget = event.target as HTMLElement;
#checkCanClearElement(event:Event) {
const eventTarget = event.target as HTMLElement,
clearBtn = this.EditorClearFormatBtn;
if(eventTarget !== this.EditorNode) {
if(eventTarget.nodeName !== 'P'
&& eventTarget.nodeName !== 'SPAN') {
this.EditorClearFormatBtn.style.display = 'inline-block';
this.EditorClearFormatBtn.innerHTML = `Ⱦ ${eventTarget.nodeName}`,
this.EditorClearFormatBtn.onpointerup = (event) => {
clearBtn.style.display = 'inline-block';
clearBtn.innerHTML = `Ⱦ ${eventTarget.nodeName}`,
clearBtn.onpointerup = (event) => {
eventTarget.replaceWith(document.createTextNode(eventTarget.textContent));
}
this.showEditorInlineDialog();
};
} else {
this.EditorClearFormatBtn.style.display = 'none';
this.EditorClearFormatBtn.onpointerup = null;
clearBtn.style.display = 'none';
clearBtn.onpointerup = null;
}
}
}
@ -547,16 +527,18 @@ class WCWYSIWYG extends HTMLElement {
/**
* Checking click tag for editable props
**/
checkEditProps(event) {
//Check need edit props
#checkEditProps(event) {
const eventTarget = event.target as HTMLElement;
//Check exist prop\attr
if(this.#EditProps[eventTarget.nodeName]) {
const props = this.#EditProps[eventTarget.nodeName];
event.stopPropagation();
this.dispatchEvent(new CustomEvent('editprops', {
detail: { eventTarget }
}));
this.EditorPropertyForm.style.display = '';
this.showEditorInlineDialog();
this.EditorPropertyForm.style.display = 'block';
this.EditorPropertyForm.setAttribute('data-tag', eventTarget.nodeName);
this.EditorPropertyForm.innerHTML = '';
for (let i = 0; i < props.length; i++) {
@ -598,69 +580,142 @@ 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);
}
}
}
}
//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++) {
const action = actions[i];
const button = el('button', {
classList: ['wc-wysiwyg_btn', `-${action.tag}`],
props: {
tabIndex: -1,
tabIndex: 0,
type:'button',
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: {
'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);
}
}
/**
* Default behaviors fot tag actions
*/
#tag = (tag:string, event:Event|false = false, is:boolean|string = false) => {
switch (tag) {
#tag = (tag:WCWYSIWYGTag) => {
switch (tag.tag) {
case 'audio':
this.#Media('audio');
break;
case 'video':
this.#Media('video');
break;
case 'details':
this.#Details();
break;
case 'img':
this.#Image();
break;
default:
this.#wrapTag(tag, is);
if(typeof tag.method === 'function') {
tag.method.apply(this, tag);
} else {
this.#wrapTag(tag, tag.is);
}
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>
* @param tag:WCWYSIWYGTag
**/
#wrapTag = (tag, is:boolean|string = false) => {
const isList = ['ul', 'ol'].includes(tag);
const Selection = window.getSelection();
let className = null;
let defaultOptions = {
classList: className ? className : undefined,
} as any;
if(isList) {
tag = 'li';
}
#wrapTag = (tag:WCWYSIWYGTag, is:boolean|string = false) => {
//First check if this tag is list item
const listTag = ['ul', 'ol'].includes(tag.tag) ? tag.tag : false;
//If list tag is true - set newTag is list item as <li>
const newtag = listTag !== false ? 'li' : tag.tag;
const Selection = this.getSelection().selection;
let defaultOptions = {} as any;
if(is) {
defaultOptions.options = {is};
}
let tagNode = el(tag, defaultOptions);
let tagNode = el(newtag, defaultOptions);
if (Selection !== null && Selection.rangeCount) {
if(['ul', 'ol'].includes(tag)) {
const list = el(tag);
if(listTag !== false) {
const list = el(listTag);
tagNode.replaceWith(list);
list.append(tagNode)
}
@ -668,10 +723,13 @@ class WCWYSIWYG extends HTMLElement {
range.surroundContents(tagNode);
Selection.removeAllRanges();
Selection.addRange(range);
//If selection has text, insert it
if(Selection.toString().length === 0) {
tagNode.innerText = tag;
}
this.updateContent();
//Check if new tag has editable props
this.#checkEditProps({target:tagNode, stopPropagation: () => false});
}
}
@ -703,15 +761,16 @@ class WCWYSIWYG extends HTMLElement {
}
if(caption) {
const figure = el('figure');
const figcaption = el('figcaption', {
props: {
textContent: caption
}
const figure = el('figure', {
append: [
img,
el('figcaption', {
props: {
textContent: caption
}
})
]
});
figure.appendChild(img);
figure.appendChild(figcaption);
img.setAttribute('alt', caption);
this.EditorNode.appendChild(figure);
@ -721,13 +780,42 @@ class WCWYSIWYG extends HTMLElement {
}
/**
* Translate function
* @param key:string phrase key
* @returns
* Check available extensions in global scope
*/
#t(key:string):string {
let lang = this.lang;
return t[lang] ? t[lang][key] || "-" : t["en"][key];
#checkExtensions() {
// Check extensions in global
const _WCWYSIWYG = window._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;
}
}
return editorSelection;
}
//define WCWYSIWYG as custom element
static define(name = 'wc-wysiwyg') {
@ -735,4 +823,28 @@ class WCWYSIWYG extends HTMLElement {
}
}
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[],
}
}
}

11
vite.config.js Normal file
View File

@ -0,0 +1,11 @@
import { defineConfig } from 'vite';
import { resolve } from 'path';
export default defineConfig({
build: {
lib: {
entry: resolve(__dirname, './src/wc-wysiwyg.ts'),
name: 'WcWysiwyg',
fileName: (format) => `wc-wysiwyg.${format}.js`,
},
},
});