From 7de84530b87c7b884bfc910767a1fdd4a39a2402 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=B5=D0=BD=D0=B8=D1=81=20=D0=A1=D1=82=D1=80=D0=BE?= =?UTF-8?q?=D0=BA=D0=BE=D0=B2?= Date: Sun, 12 Feb 2023 23:28:55 +0300 Subject: [PATCH] initial commut --- .github/workflows/release-package.yml | 34 ++ .gitignore | 2 + .npmignore | 1 + .npmrc | 1 + README.md | 51 ++ dist/core/el.js | 1 + dist/core/translates.js | 1 + dist/sass/content.css | 462 ++++++++++++++++ dist/sass/wc-wysiwyg.css | 203 +++++++ dist/wc-wysiwyg.js | 1 + package.json | 39 ++ src/core/el.ts | 61 +++ src/core/translates.ts | 91 ++++ src/sass/content.scss | 490 +++++++++++++++++ src/sass/wc-wysiwyg.scss | 242 +++++++++ src/wc-wysiwyg.ts | 731 ++++++++++++++++++++++++++ tsconfig.json | 13 + 17 files changed, 2424 insertions(+) create mode 100644 .github/workflows/release-package.yml create mode 100644 .gitignore create mode 100644 .npmignore create mode 100644 .npmrc create mode 100644 README.md create mode 100644 dist/core/el.js create mode 100644 dist/core/translates.js create mode 100644 dist/sass/content.css create mode 100644 dist/sass/wc-wysiwyg.css create mode 100644 dist/wc-wysiwyg.js create mode 100644 package.json create mode 100644 src/core/el.ts create mode 100644 src/core/translates.ts create mode 100644 src/sass/content.scss create mode 100644 src/sass/wc-wysiwyg.scss create mode 100644 src/wc-wysiwyg.ts create mode 100644 tsconfig.json diff --git a/.github/workflows/release-package.yml b/.github/workflows/release-package.yml new file mode 100644 index 0000000..df59c05 --- /dev/null +++ b/.github/workflows/release-package.yml @@ -0,0 +1,34 @@ +name: wc-wysiwyg package + +on: + release: + types: [created] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 16 + - run: npm install + - run: npm run tsc + - run: npm run babel-minify + - run: npm run sass + + publish-gpr: + needs: build + runs-on: ubuntu-latest + permissions: + packages: write + contents: read + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 16 + registry-url: https://npm.pkg.github.com/ + - run: npm publish --public + env: + NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..572406b --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/node_modules +package-lock.json \ No newline at end of file diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..c7db386 --- /dev/null +++ b/.npmignore @@ -0,0 +1 @@ +/.github \ No newline at end of file diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..1120c80 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +@OWNER:registry=https://npm.pkg.github.com \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..307591d --- /dev/null +++ b/README.md @@ -0,0 +1,51 @@ +# wc-time +WYWSIWYG HTML5 Editor written in TypeScript and designed by web-componennt, support all JS frameworks and browsers. +See full demo - [wc-wysiwyg demo](https://webislife.ru/demo/wc-wysiwyg/) list and demo of all editor features + + +## Install + +``` +npm i wc-wysiwyg +``` + +## Commands + +Available package commands + +``` +`npm run sass' - build scss styles +`npm run tsc' - run typescript +`npm run babel-minify' - minify code after typescript +`npm run build' - build all stpes 1.sass 2.tsc 3.babel-minify +``` + +## Custom element demo + +```html + + + +``` + +See full demo - [wc-wysiwyg demo](https://webislife.ru/demo/wc-wysiwyg/) list and demo of all editor features + +Dont forgot star on git! Thank you! Enojoy! + +Dev by strokoff.ru - make web, not war) \ No newline at end of file diff --git a/dist/core/el.js b/dist/core/el.js new file mode 100644 index 0000000..6b6e9f4 --- /dev/null +++ b/dist/core/el.js @@ -0,0 +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 caption { + background-color: var(--color-blue-gray-200); } + +th { + background-color: var(--color-blue-gray-100); } + +td, th { + padding: 3px; + border: 1px solid var(--color-blue-gray-400); } + +/* definiton */ +dfn { + color: var(--color-blue-900); + position: relative; } + +dfn:after { + content: "*"; + color: var(--color-blue-900); } + +dfn:after { + content: "*"; + color: var(--color-blue-900); } + +dfn::before { + transition: all 0.2s ease; + display: inline-block; + position: absolute; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 90vw; + min-width: 50px; + padding: 3px 6px; + background-color: var(--color-blue-50); + top: 0; + font-size: 0.9em; + line-height: 0.9em; + left: 50%; + transform: translate(0, 0); } + +dfn:hover::before { + content: attr(title); + transform: translate(-50%, -110%); } + +small { + font-size: 0.8em; } + +samp { + padding: 0 3px; + background-color: var(--color-blue-gray-50); + border-radius: 3px; + font-family: inherit; + border-bottom: 1px solid var(--color-blue-gray-300); } + +samp::before { + content: '> '; + color: var(--color-blue-gray-300); + font-family: sans-serif; } + +a, +a.wc-a { + color: var(--color-blue-800); } + +a:hover, +a.wc-a:hover { + color: var(--color-blue-900); + border-color: var(--color-blue-900); } + +a.wc-a[target=_blank]::after { + user-select: none; + content: '↗'; + display: inline-flex; + margin-left: 5px; + border-radius: 3px; + align-items: center; + justify-content: center; + width: 1em; + line-height: 16px; + font-size: 0.7em; + height: 1em; + border: 1px solid var(--color-blue-gray-200); + transition: all 0.2s ease; } + +a.wc-a[target=_blank]:hover::after { + border: 1px solid var(--color-blue-500); + background-color: var(--color-blue-500); + color: #fff; } + +blockquote { + margin: 10px 0 10px 0; + background-color: var(--color-amber-50); + color: #412207; + padding: 10px 10px 10px 20px; + border-left: 2px solid #F57F17; + border-radius: 3px; + position: relative; } + +blockquote::before { + content: '❝'; + font-size: 1em; + color: #F57F17; + display: block; + position: absolute; + top: 0px; + left: 10px; + user-select: none; } + +p { + margin-bottom: 1em; } + +/** +* +*/ +q { + background-color: #FFF8E1; + display: inline; + font-style: italic; + padding: 0 5px; + border-radius: 3px; } + +q:before { + content: open-quote; } + +q:after { + content: close-quote; } + +mark { + background-color: var(--color-lime-50); + padding: 0 5px; + border-radius: 3px; + line-height: 1.2em; + display: inline-block; } + +strong { + background-color: var(--color-deep-orange-50); + color: var(--color-deep-orange-900); + font-weight: 400; + padding: 0 5px; + border-radius: 3px; + line-height: 1.2em; + display: inline-block; } + +span.inline-code { + background-color: var(--color-blue-gray-50); + color: var(--color-blue-grey-800); + padding: 15px 10px 10px 10px; + border-radius: 6px; } + +kbd { + background-color: #eee; + border-radius: 3px; + border: 1px solid #b4b4b4; + border-bottom: 3px solid #b4b4b4; + color: #333; + display: inline-block; + text-transform: uppercase; + font-family: inherit; + font-size: 0.85em; + font-weight: 700; + line-height: 1; + padding: 2px 4px; + white-space: nowrap; } + +/* spoiler section */ +details { + border: 1px dashed var(--color-blue-gray-200); + border-radius: 5px; + margin-bottom: 1em; } + +details > summary { + text-decoration: dotted; + color: var(--color-blue-grey-900); + padding: 5px; + border-radius: 3px; + cursor: pointer; } + +details > p { + padding: 0 10px 10px 10px; } + +/* abbr section */ +abbr { + color: #1A237E; + position: relative; + cursor: help; + transition: all 0.2s ease; } + +abbr:hover { + background-color: #E3F2FD; } + +abbr::after { + transition: all 0.2s ease; + display: inline-block; + position: absolute; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 33vw; + min-width: 50px; + padding: 3px 6px; + background-color: #BBDEFB; + top: 0; + font-size: 0.9em; + line-height: 0.9em; + left: 50%; + transform: translate(0, 0); } + +abbr:hover::after { + content: attr(title); + transform: translate(-50%, -110%); } + +/* code block style */ +pre, .hljs { + background-color: var(--color-blue-gray-50); + color: var(--color-blue-grey-900); + padding: 15px 10px 10px 10px; + border-radius: 6px; + overflow: hidden; + overflow-x: auto; + margin-bottom: 5px; + position: relative; } + +pre[data-lang]:before { + content: attr(data-lang); + display: inline-block; + background-color: var(--color-blue-gray-100); + color: var(--color-blue-grey-900); + border-radius: 6px; + position: absolute; + left: 0; + top: 0; + padding: 0 5px; + text-transform: uppercase; + font-size: 0.7em; + line-height: 16px; } + +/* inlinde-code */ +code { + display: inline-block; + padding: 0 4px; + line-height: 1.2em; + box-sizing: border-box; + background-color: var(--color-blue-gray-50); + color: var(--color-blue-grey-900); + border-radius: 3px; } + +/* ul\li section */ +ul, ol { + margin-left: 0.5em; + margin-bottom: 0.5em; + padding-inline-start: 0; } + +ol li { + list-style: decimal; + margin-left: 1em; } + +ul li { + list-style-type: square; + margin-left: 1em; } + +li[data-listStyle] { + list-style: none; + list-style-type: none; + position: relative; } + +li[data-listStyle]:before { + content: attr(data-liststyle); + position: absolute; + margin-left: -20px; } + +/* Images */ +img { + box-sizing: border-box; + height: auto; + max-width: 100%; + max-height: 100%; + display: block; + border-radius: 3px; } + +figure { + border-radius: 4px; + margin: 5px; + padding: 5px; + border: 1px solid var(--color-blue-gray-200); } + +figure > figcaption { + font-weight: normal; + font-size: 0.8em; } + +hr { + display: block; + outline: none; + border: 1px dashed var(--color-blue-gray-200); + margin: 1em 0; } + +button, .btn { + background: var(--color-blue-gray-50); + outline: none; + padding: 3px 6px; + border-radius: 3px; + border: 1px solid var(--color-blue-grey-200); + border-bottom: 3px solid var(--color-blue-grey-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; + user-select: none; + cursor: pointer; } + button:hover, .btn:hover { + background: var(--color-blue-gray-100); + border-color: var(--color-blue-gray-300); } + button:focus, .btn:focus { + border-color: var(--color-blue-500); } + button:active, .btn:active { + padding-top: 2px; + background: var(--color-blue-gray-100); + border-color: var(--color-blue-gray-700); + border-bottom: 1px solid; } + button:disabled, .btn:disabled { + pointer-events: none; + color: var(--color-blue-gray-400); + border: 1px solid var(--color-blue-gray-300); + background-color: var(--color-blue-gray-200); } + button > a, .btn > a { + text-decoration: none; } + button.-green, .btn.-green { + border-color: var(--color-green-500); } + button.-blue, .btn.-blue { + background-color: var(--color-blue-500); + color: #fff; + border-color: var(--color-blue-800); } + button.-blue:active, .btn.-blue:active { + background-color: var(--color-blue-600); + border-color: var(--color-blue-900); } + +.-text-center { + text-align: center; } + +.-text-right { + text-align: right; } + +.-text-left { + text-align: left; } diff --git a/dist/sass/wc-wysiwyg.css b/dist/sass/wc-wysiwyg.css new file mode 100644 index 0000000..bc502ed --- /dev/null +++ b/dist/sass/wc-wysiwyg.css @@ -0,0 +1,203 @@ +.wc-wysiwyg { + background-color: #eee; + font-family: Arial, Helvetica, sans-serif; + position: relative; + border: 1px solid var(--color-blue-gray-400); + border-radius: 3px; + display: block; + /* custom elements */ + /* preview */ + /* inline dialog */ + /* props form */ + /* hints */ } + .wc-wysiwyg_bt { + display: block; + padding: 5px; + margin: 0; + border: none; + outline: none; + border-radius: 3px; + background-color: var(--color-blue-gray-100); } + .wc-wysiwyg_bt .-errors { + background-color: var(--color-red-500); + color: var(--color-red-50); + padding: 3px; + font-size: 10px; + line-height: 15px; + 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); } + .wc-wysiwyg_pr { + width: 100%; + box-sizing: border-box; + max-width: 100%; + min-height: 200px; } + .wc-wysiwyg_content { + padding: 5px 5px 2em 5px; + border: 1px solid #ccc; + background: #fff; + overflow-x: hidden; + overflow-y: scroll; + max-width: 960px; + width: 100%; + box-sizing: border-box; + margin: 0 auto; + box-sizing: border-box; + display: inline-block; + resize: vertical; } + .wc-wysiwyg_content .-selected { + background-color: var(--color-blue-100); } + .wc-wysiwyg_content:focus, .wc-wysiwyg_content:active { + outline: 5px solid var(--color-green-300); + border: none; } + .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); } + .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; + 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); } + .wc-wysiwyg_btn:focus { + border-color: var(--color-blue-500); } + .wc-wysiwyg_btn:active { + padding-top: 2px; + background: var(--color-blue-gray-100); + border-color: var(--color-blue-gray-700); + border-bottom: 1px solid; } + .wc-wysiwyg_btn.-clear { + text-decoration: line-through; + font-weight: bold; } + .wc-wysiwyg_ia { + display: flex; + flex-wrap: wrap; } + .wc-wysiwyg_ia > input { + box-sizing: border-box; + min-width: 100%; + position: relative; } + .wc-wysiwyg_di { + z-index: 99901; + 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; } + .wc-wysiwyg_di > form:nth-child(1n+2) { + margin-top: 10px; } + .wc-wysiwyg_pf { + display: flex; + border-radius: 3px; + padding: 3px; + background-color: var(--color-blue-100); + flex-wrap: nowrap; + font-size: 0.9em; + align-items: center; } + .wc-wysiwyg_pf:before { + padding: 0 0 0 5px; + height: 25px; + display: inline-block; + content: "<" attr(data-tag) " "; + text-transform: lowercase; + border-top-left-radius: 3px; + border-bottom-left-radius: 3px; + font-weight: bold; + margin-right: 5px; } + .wc-wysiwyg_pf > label { + background-color: var(--color-blue-200); + color: var(--color-blue-gray-800); + padding: 3px 3px 3px 5px; + display: flex; + align-items: center; + border-radius: 6px; + margin-right: 5px; + height: 25px; + line-height: 25px; } + .wc-wysiwyg_pf input.wc_ed_inp { + background: #fff; + padding: 0 5px; + height: 25px; + display: inline-block; + border-radius: 6px; + box-sizing: border-box; + border: none; } + .wc-wysiwyg_pf input.wc_ed_inp:focus { + border: none; + outline: 2px solid var(--color-green-500); } + .wc-wysiwyg_pf > button { + height: 25px; + width: 25px; + border-radius: 6px; } + .wc-wysiwyg *[data-hint] { + position: relative; } + .wc-wysiwyg *[data-hint]:hover:after { + visibility: visible; + z-index: 9900; + transform: translate(-50%, -100%); } + .wc-wysiwyg *[data-hint]:after { + visibility: hidden; + position: absolute; + transition: transform .2s ease; + left: 50%; + top: -3px; + transform: translate(-50%, 0%); + background-color: var(--color-blue-gray-700); + color: #fff; + content: attr(data-hint); + display: inline-block; + padding: 3px; + border-radius: 3px; + font-size: 10px; + line-height: 10px; } + .wc-wysiwyg .-display-none { + display: none; } diff --git a/dist/wc-wysiwyg.js b/dist/wc-wysiwyg.js new file mode 100644 index 0000000..b88443c --- /dev/null +++ b/dist/wc-wysiwyg.js @@ -0,0 +1 @@ +import{t}from"./core/translates.js";import{el}from"./core/el.js";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"}];class WCWYSIWYG extends HTMLElement{EditorTags;EditorCustomTags;EditorNode;EditorActionsSection;EditorInlineActions;EditorInlineDialog;EditorInlineActionsForm;EditorPropertyForm;EditorClearFormatBtn;EditorAutoCompleteForm;EditorBottomForm;EditorBottomFormNewP;EditorBottomFormViewToggle;EditorPreviewText;EditorCustomTagsForm;EditorTagsMethods;EditorAllowTags;EditorFullScreenButton;lang="ru";value="";static observedAttributes=["value"];#EditProps;#Autocomplete;#SotrageKey;#HideBottomActions;#Connected=!1;constructor(){super(),this.classList.add("wc-wysiwyg"),this.onpointerup=()=>{const selection=window.getSelection();null!==selection&&0{const isFullScreen=document.fullscreenElement;this.classList.toggle("-fullscreen",null!==isFullScreen)}}connectedCallback(){if(!1===this.#Connected){const allowTags=this.getAttribute("data-allow-tags")||allTags.map(t=>t.tag).join(",");if(this.EditorPreviewText=this.querySelector("textarea"),this.EditorPreviewText.className="wc-wysiwyg_pr -display-none",this.EditorPreviewText.oninput=event=>{const target=event.target;this.EditorNode.innerHTML=target.value,this.value=target.value},this.EditorAllowTags=allowTags.split(","),this.EditorTags=allTags.filter(tag=>allowTags.includes(tag.tag)),this.#EditProps=null!==this.getAttribute("data-edit-props")&&JSON.parse(this.getAttribute("data-edit-props")),this.#Autocomplete="1"===this.getAttribute("data-autocomplete"),this.#HideBottomActions="1"===this.getAttribute("data-hide-bottom-actions"),this.EditorInlineActions=this.EditorTags.filter(action=>!1===["video","audio","img"].includes(action.tag)),this.#SotrageKey=this.getAttribute("data-storage"),this.#SotrageKey){let storeValue=window.localStorage.getItem(this.#SotrageKey);storeValue&&(this.value=storeValue)}this.EditorActionsSection=el("section",{classList:["wc-wysiwyg_ec"]}),this.EditorClearFormatBtn=el("button",{classList:["wc-wysiwyg_btn","-clear"],attrs:{"data-hint":this.#t("clearFormat")},props:{innerHTML:"\u023E"}}),this.EditorInlineActionsForm=el("form"),this.EditorInlineDialog=el("dialog",{classList:["wc-wysiwyg_di"],append:[this.EditorInlineActionsForm,this.EditorClearFormatBtn],props:{onsubmit:event=>{event.preventDefault(),event.stopPropagation()}}}),this.#EditProps&&(this.EditorPropertyForm=el("form",{styles:{display:"none"},classList:["wc-wysiwyg_pf"],props:{onsubmit:event=>{event.preventDefault(),this.hideEditorInlineDialog()},onpointerup:event=>event.stopPropagation()}}),this.EditorInlineDialog.append(this.EditorPropertyForm)),this.#Autocomplete&&(this.EditorAutoCompleteForm=el("form",{classList:["wc-wysiwyg_au"],props:{onsubmit:submitEvent=>{submitEvent.preventDefault(),submitEvent.stopPropagation();const tagName=submitEvent.submitter.value,newEl=el(tagName,{props:{innerHTML:tagName}});submitEvent.target.parentElement.replaceWith(newEl),newEl.focus()}}})),this.EditorBottomForm=el("fieldset",{classList:["wc-wysiwyg_bt"]}),!1===this.#HideBottomActions&&(this.EditorBottomFormViewToggle=el("button",{classList:["wc-wysiwyg_btn"],attrs:{"data-hint":this.#t("toggleViewMode"),"data-mode":"html5"},props:{type:"button",innerText:"\u0442\u0435\u043A\u0441\u0442/html5",onpointerup:()=>{let mode=this.EditorBottomFormViewToggle.getAttribute("data-mode"),newMode="html5"===mode?"text":"html5";this.EditorBottomFormViewToggle.setAttribute("data-mode",newMode),this.EditorNode.style.display="html5"===newMode?"":"none",this.EditorPreviewText.classList.toggle("-display-none","html5"===newMode),"text"===newMode&&(this.EditorPreviewText.value=this.EditorNode.innerHTML)}}}),this.EditorBottomFormNewP=el("button",{classList:["wc-wysiwyg_btn"],attrs:{"data-hint":this.#t("addNewParahraph")},props:{type:"button",innerText:"+ P",onpointerup:()=>{const P=el("p",{props:{innerText:"/"}});this.EditorNode.appendChild(P),P.focus()}}}),this.EditorFullScreenButton=el("button",{classList:["wc-wysiwyg_btn"],attrs:{"data-hint":this.#t("fullScreen")},props:{type:"button",ariaRoleDescription:"button",innerText:"\uD83D\uDDA5\uFE0F",onpointerup:()=>{this.requestFullscreen()}}}),this.EditorBottomForm.append(this.EditorBottomFormNewP,this.EditorBottomFormViewToggle,this.EditorFullScreenButton)),this.EditorCustomTags=JSON.parse(this.getAttribute("data-custom-tags")+""),null!==this.EditorCustomTags&&(this.EditorCustomTagsForm=el("fieldset",{classList:["wc-wysiwyg_ce"]}),this.#makeActionButtons(this.EditorCustomTagsForm,this.EditorCustomTags),this.appendChild(this.EditorCustomTagsForm)),this.EditorNode=el("article",{classList:["wc-wysiwyg_content",this.getAttribute("data-content-class")],props:{contentEditable:!0,onpointerup:event=>{this.checkCanClearElement(event),this.#EditProps&&this.checkEditProps(event)},oninput:()=>{this.updateContent(),this.#Autocomplete&&this.checkAutoComplete()},onkeydown:event=>{if(event.altKey&&"Space"===event.code){const Selection=window.getSelection();if("Caret"===Selection.type){const span=el("span");Selection.anchorNode.parentElement.insertAdjacentElement("afterend",span);const textN=document.createTextNode(" ");span.replaceWith(textN);const range=document.createRange();range.selectNodeContents(textN),Selection.removeAllRanges(),Selection.addRange(range)}}if("Escape"===event.code&&this.hideEditorInlineDialog(),"Enter"===event.code&&!1===event.shiftKey){const Selection=window.getSelection();if(["LI","ARTICLE","P"].includes(Selection.anchorNode.parentElement.tagName))return!0;const p=el("p",{props:{innerHTML:` `}});Selection.anchorNode.parentElement.insertAdjacentElement("afterend",p);const range=document.createRange();range.selectNodeContents(p),Selection.removeAllRanges(),Selection.addRange(range),event.stopPropagation(),event.preventDefault()}}}}),this.#makeActionButtons(this.EditorActionsSection,this.EditorTags),this.#makeActionButtons(this.EditorInlineActionsForm,this.EditorInlineActions),this.append(this.EditorActionsSection,this.EditorInlineDialog,this.EditorNode,this.EditorPreviewText,this.EditorBottomForm),this.EditorNode.innerHTML=this.EditorPreviewText.value,this.updateContent(),this.#Connected=!0}}updateContent(){this.value=this.EditorNode.innerHTML,this.checkValidity(),this.#SotrageKey&&window.localStorage.setItem(this.#SotrageKey,this.value),this.dispatchEvent(new Event("oninput",{bubbles:!0,cancelable:!1})),this.updatePreviewEl(this.getAttribute("data-preview-el"))}updatePreviewEl(selector){if(selector){const previewEl=window.document.body.querySelector(selector);previewEl&&(previewEl.innerHTML=this.value)}}checkValidity(){let hasErros=!1,errors=[];if(null!==this.getAttribute("required")&&0===(this.EditorNode.textContent+"").length&&(hasErros=!0,errors.push(this.#t("required"))),+this.getAttribute("minlength")&&(this.EditorNode.textContent+"").length<+this.getAttribute("minlength")&&(hasErros=!0,errors.push(`${this.#t("minlength")} ${this.getAttribute("minlength")}`)),+this.getAttribute("maxlength")&&(this.EditorNode.textContent+"").length>+this.getAttribute("maxlength")&&(hasErros=!0,errors.push(`${this.#t("maxlength")} ${this.getAttribute("maxlength")}`)),this.getAttribute("filtertags")){const disallowTags=this.getAttribute("filtertags").split(",");for(let i=0;i")},classList:["-errors"]});this.append(errosEl)}return!1==hasErros}checkAutoComplete(){const Selecton=window.getSelection();if(null!==Selecton&&null!==Selecton.anchorNode){const SelectionParentEl=Selecton.anchorNode.parentElement;if(null!==SelectionParentEl&&""===Selecton.toString()&&"P"===SelectionParentEl.nodeName&&SelectionParentEl.parentElement===this.EditorNode&&SelectionParentEl.innerText.startsWith("/")){const parsedTagName=SelectionParentEl.innerText.replace("/",""),filteredActions=this.EditorTags.filter(action=>action.tag.toLocaleLowerCase().startsWith(parsedTagName.toLocaleLowerCase()));0{this.EditorAutoCompleteForm.appendChild(el("button",{classList:["wc-wysiwyg_btn",`-${action.tag}`],attrs:{"data-hint":this.#t(action.tag)||null},props:{type:"submit",innerText:action.tag,value:action.tag}}))}),SelectionParentEl.appendChild(this.EditorAutoCompleteForm)):(this.EditorAutoCompleteForm.innerHTML="",this.EditorAutoCompleteForm.parentElement&&this.EditorAutoCompleteForm.parentElement.removeChild(this.EditorAutoCompleteForm))}}}showEditorInlineDialog(){this.EditorInlineDialog.show()}hideEditorInlineDialog(){this.#EditProps&&(this.EditorPropertyForm.style.display="none"),this.EditorInlineDialog.close()}checkCanClearElement(event){const eventTarget=event.target;eventTarget!==this.EditorNode&&("P"!==eventTarget.nodeName&&"SPAN"!==eventTarget.nodeName?(this.EditorClearFormatBtn.style.display="inline-block",this.EditorClearFormatBtn.innerHTML=`Ⱦ ${eventTarget.nodeName}`,this.EditorClearFormatBtn.onpointerup=()=>{eventTarget.replaceWith(document.createTextNode(eventTarget.textContent))},this.showEditorInlineDialog()):(this.EditorClearFormatBtn.style.display="none",this.EditorClearFormatBtn.onpointerup=void 0))}checkEditProps(event){const eventTarget=event.target;if(this.#EditProps[eventTarget.nodeName]){const props=this.#EditProps[eventTarget.nodeName];event.stopPropagation(),this.EditorPropertyForm.style.display="",this.showEditorInlineDialog(),this.EditorPropertyForm.setAttribute("data-tag",eventTarget.nodeName),this.EditorPropertyForm.innerHTML="";for(let i=0;i{const eventInputTarget=eventInput.target;"class"===tagProp&&(eventTarget.className=eventInputTarget.value),(isAttr||"datetime"===tagProp)&&null!==eventInputTarget?eventTarget.setAttribute(tagProp,eventInputTarget.value):eventTarget[tagProp]=eventInputTarget.value,this.updateContent()}}})]}))}this.EditorPropertyForm.append(el("button",{classList:["wc-wysiwyg_btn"],props:{type:"submit",innerHTML:"↳"}}))}}#makeActionButtons(toEl,actions){for(let i=0;ithis.#tag(action.tag,event,action.is)},attrs:{"data-hint":action.hint?action.hint:this.#t(action.tag)||"-"}});toEl.appendChild(button)}}#tag=(tag,event=!1,is=!1)=>{switch(tag){case"audio":this.#Media("audio");break;case"video":this.#Media("video");break;case"img":this.#Image();break;default:this.#wrapTag(tag,is);}};#wrapTag=(tag,is=!1)=>{const isList=["ul","ol"].includes(tag),Selection=window.getSelection();let className=null,defaultOptions={classList:void 0};isList&&(tag="li"),is&&(defaultOptions.options={is});let tagNode=el(tag,defaultOptions);if(null!==Selection&&Selection.rangeCount){if(["ul","ol"].includes(tag)){const list=el(tag);tagNode.replaceWith(list),list.append(tagNode)}const range=Selection.getRangeAt(0).cloneRange();range.surroundContents(tagNode),Selection.removeAllRanges(),Selection.addRange(range),0===Selection.toString().length&&(tagNode.innerText=tag),this.updateContent()}};#Media=tagName=>{const mediaSrc=prompt("src","");if(""===mediaSrc)return!1;const mediaEl=el(tagName,{attrs:{controls:!0},props:{src:mediaSrc}});this.EditorNode.append(mediaEl),this.updateContent()};#Image=()=>{const src=prompt("IMG URL"),caption=prompt("IMG caption"),img=new Image;if(src)img.src=src;else return alert("Invalid src");if(caption){const figure=el("figure"),figcaption=el("figcaption",{props:{textContent:caption}});figure.appendChild(img),figure.appendChild(figcaption),img.setAttribute("alt",caption),this.EditorNode.appendChild(figure)}else this.EditorNode.appendChild(img)};#t(key){let lang=this.lang;return t[lang]?t[lang][key]||"-":t.en[key]}static define(name="wc-wysiwyg"){window.customElements.define(name,WCWYSIWYG)}}export default WCWYSIWYG;export const define=WCWYSIWYG.define; \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..37fc0c1 --- /dev/null +++ b/package.json @@ -0,0 +1,39 @@ +{ + "name": "wc-wysiwyg", + "version": "0.9.0", + "description": "WYWSIWYG HTML5 Editor written in ts and designed by web-componennt, support all JS frameworks and browsers", + "main": "src/wc-wysiwyg.ts", + "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" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/webislife/wc-wysiwyg.git" + }, + "keywords": [ + "web-component", + "custom-element", + "time", + "html5", + "esnext", + "typescript" + ], + "author": "srokoff", + "license": "MIT", + "bugs": { + "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" + }, + "publishConfig": { + "@webislife:registry": "https://npm.pkg.github.com" + } + } + \ No newline at end of file diff --git a/src/core/el.ts b/src/core/el.ts new file mode 100644 index 0000000..7dab354 --- /dev/null +++ b/src/core/el.ts @@ -0,0 +1,61 @@ +/** + * Short + * @param tagName element tag name + * @param params list of object params for document.createElements + * @returns + */ + export const el = (tagName:keyof HTMLElementTagNameMap|string, {classList, styles, props, attrs, options, append}:{ + classList?: string[], + styles?: object, + props?: object, + attrs?: object, + options?: { + is?:string + }, + append?: Element[] +} = {}):any => { + if(!tagName) { + throw new Error(`Undefined tag ${tagName}`); + } + const element = document.createElement(tagName, options); + // element.classList + if(classList) { + for (let i = 0; i < classList.length; i++) { + const styleClass = classList[i]; + element.classList.add(styleClass) + } + } + // 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]; + } + } + // element[prop] + if(props) { + const propKeys = Object.keys(props); + for (let i = 0; i < propKeys.length; i++) { + const key = propKeys[i]; + element[key] = props[key]; + } + } + // element.setAttribute(key,val) + if(attrs) { + const attrsKeys = Object.keys(attrs); + for (let i = 0; i < attrsKeys.length; i++) { + const key = attrsKeys[i]; + if(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; +}; \ No newline at end of file diff --git a/src/core/translates.ts b/src/core/translates.ts new file mode 100644 index 0000000..aba4252 --- /dev/null +++ b/src/core/translates.ts @@ -0,0 +1,91 @@ +//Translates +export const t = { + en: { + h1:'Header 1 level', + h2:'Header 2 level', + h3:'Header 3 level', + h4:'Header 4 level', + h5:'Header 5 level', + h6:'Header 6 level', + span:'String', + mark:'Marked text', + small:'Small text', + dfn:'Definition', + a:'Link', + q:'Inline quote', + b:'Bold', + i:'Italic', + u:'Underlined', + s:'Small', + sup:'Superscript', + sub:'Subscript', + kbd:'Button', + abbr:'Abbrevature', + strong:'Important text', + code:'Inline code', + samp:'PC program output', + del:'Deleted text from document', + ins:'Insert text into document', + var:'Variable designation', + ul:'The bulleted list', + ol:'Numbered list', + pre:'Formatted text', + time:'Date and time', + img:'Image', + audio:'Audio element', + video:'Video element', + blockquote:'Blockquote', + clearFormat: 'Clear text format', + toggleViewMode: 'Toggle view Text/HTML5', + addNewParahraph: 'Add new parahraph', + fullScreen: 'fullScreen', + required: 'Field required', + minlength: 'Min length is:', + maxlength: 'Max length is:', + filtertags: 'Found filter tag:', + }, + ru: { + h1:'Заголовок 1 уровня', + h2:'Заголовок 2 уровня', + h3:'Заголовок 3 уровня', + h4:'Заголовок 4 уровня', + h5:'Заголовок 5 уровня', + h6:'Заголовок 6 уровня', + span:'Строка', + mark:'Помеченный текст', + small:'Маленький текст', + dfn:'Определение', + a:'Ссылка', + q:'Внутритекстовая цитата', + b:'Жирный', + i:'Курсив', + u:'Подчеркнутый', + s:'Маленький', + sup:'Надстрочный', + sub:'Подстрочный', + kbd:'Кнопка', + abbr:'Аббривеатура', + strong:'Важный текст', + code:'Внутритекстовый код', + samp:'Вывод ПК программы', + del:'Удаленный текст из документа', + ins:'Вставленный текст в документ', + var:'Обозначение переменной', + ul:'Маркированный список', + ol:'Нумерованный список', + pre:'Форматированный текст', + time:'Дата и время', + img:'Изображение', + audio:'Аудио элемент', + video:'Видео элемент', + blockquote:'Блок цитаты', + clearFormat: 'Очистить формат', + toggleViewMode: 'Переключить вид текст/html5', + addNewParahraph: 'Добавить параграф', + fullScreen: 'Полноэкранный режим', + required: 'Обязательно для заполнения', + minlength: 'Минимальная длинна:', + maxlength: 'Максимальная длинна:', + filtertags: 'Найден запрещенный тег:', + } +}; \ No newline at end of file diff --git a/src/sass/content.scss b/src/sass/content.scss new file mode 100644 index 0000000..99f725d --- /dev/null +++ b/src/sass/content.scss @@ -0,0 +1,490 @@ +: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-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; + } +body { + font-family: Arial, Helvetica, sans-serif +} +h1,h2,h3,h4,h5,h6 { + font-family: Georgia, 'Times New Roman', Times, serif; + font-weight: normal; + margin-bottom: 1em; + margin-top: 1.5em; +} +h1 { + font-size: 2em; +} +h2 { + font-size: 1.8em; +} +h3 { + font-size: 1.6em; +} +h4 { + font-size: 1.4em; +} +h5 { + font-size: 1.2em; +} +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); + background-color: var(--color-red-50); + text-decoration: none; +} +video { + max-width: 100%; +} +var { + font-weight: bold; + font-style: italic; +} +del:before { + content: '- '; + font-weight: 400; +} +ins { + color: var(--color-green-900); + background-color: var(--color-green-50); + text-decoration: none; +} +ins:before { + content: '+ '; + font-weight: 400; +} +table > caption { + background-color: var(--color-blue-gray-200); +} +th { + background-color: var(--color-blue-gray-100); +} +td,th { + padding: 3px; + border: 1px solid var(--color-blue-gray-400); +} +/* definiton */ +dfn { + color: var(--color-blue-900); + position: relative; +} +dfn:after { + content: "*"; + color: var(--color-blue-900); +} + +dfn:after { + content: "*"; + color: var(--color-blue-900); +} +dfn::before { + transition: all 0.2s ease; + display: inline-block; + position: absolute; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 90vw; + min-width: 50px; + padding: 3px 6px; + background-color: var(--color-blue-50); + top:0; + font-size: 0.9em; + line-height: 0.9em; + left: 50%; + transform: translate(0, 0); +} +dfn:hover::before { + content: attr(title); + transform: translate(-50%, -110%); +} + +small { + font-size: 0.8em; +} +samp { + padding: 0 3px; + background-color: var(--color-blue-gray-50); + border-radius: 3px; + font-family: inherit; + border-bottom: 1px solid var(--color-blue-gray-300); +} +samp::before { + content: '> '; + color: var(--color-blue-gray-300); + font-family: sans-serif; +} +a, +a.wc-a { + color: var(--color-blue-800); +} + +a:hover, +a.wc-a:hover { + color: var(--color-blue-900); + border-color: var(--color-blue-900); +} +a.wc-a[target=_blank]::after { + user-select: none; + content: '↗'; + display: inline-flex; + margin-left:5px; + border-radius: 3px; + align-items: center; + justify-content: center; + width: 1em; + line-height: 16px; + font-size: 0.7em; + height: 1em; + border: 1px solid var(--color-blue-gray-200); + transition: all 0.2s ease; +} +a.wc-a[target=_blank]:hover::after { + border: 1px solid var(--color-blue-500); + background-color: var(--color-blue-500); + color: #fff; +} +blockquote { + margin: 10px 0 10px 0; + background-color: var(--color-amber-50); + color: #412207; + padding: 10px 10px 10px 20px; + border-left: 2px solid #F57F17; + border-radius: 3px; + position: relative; +} +blockquote::before { + content: '❝'; + font-size: 1em; + color:#F57F17; + display: block; + position: absolute; + top: 0px; + left: 10px; + user-select: none; +} +p { + margin-bottom: 1em; +} +/** +* +*/ +q { + background-color: #FFF8E1; + display: inline; + font-style: italic; + padding: 0 5px; + border-radius: 3px; +} +q:before { + content: open-quote; +} +q:after { + content: close-quote; +} +mark { + background-color: var(--color-lime-50); + padding: 0 5px; + border-radius: 3px; + line-height: 1.2em; + display: inline-block; +} +strong { + background-color: var(--color-deep-orange-50); + color: var(--color-deep-orange-900); + font-weight: 400; + padding: 0 5px; + border-radius: 3px; + line-height: 1.2em; + display: inline-block; +} +span.inline-code { + background-color: var(--color-blue-gray-50); + color: var(--color-blue-grey-800); + padding: 15px 10px 10px 10px; + border-radius: 6px; +} +kbd { + background-color: #eee; + border-radius: 3px; + border: 1px solid #b4b4b4; + border-bottom: 3px solid #b4b4b4; + color: #333; + display: inline-block; + text-transform: uppercase; + font-family: inherit; + font-size: 0.85em; + font-weight: 700; + line-height: 1; + padding: 2px 4px; + white-space: nowrap; +} +/* spoiler section */ +details { + border:1px dashed var(--color-blue-gray-200); + border-radius: 5px; + margin-bottom: 1em; +} +details > summary { + text-decoration: dotted; + color: var(--color-blue-grey-900); + padding: 5px; + border-radius: 3px; + cursor: pointer; +} +details > p { + padding: 0 10px 10px 10px; +} +/* abbr section */ +abbr { + color: #1A237E; + position: relative; + cursor: help; + transition: all 0.2s ease; + +} +abbr:hover { + background-color: #E3F2FD; +} +abbr::after { + transition: all 0.2s ease; + display: inline-block; + position: absolute; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 33vw; + min-width: 50px; + padding: 3px 6px; + background-color: #BBDEFB; + top:0; + font-size: 0.9em; + line-height: 0.9em; + left: 50%; + transform: translate(0, 0); +} +abbr:hover::after { + content: attr(title); + transform: translate(-50%, -110%); +} +/* code block style */ +pre, .hljs { + background-color: var(--color-blue-gray-50); + color: var(--color-blue-grey-900); + padding: 15px 10px 10px 10px; + border-radius: 6px; + overflow: hidden; + overflow-x: auto; + margin-bottom: 5px; + position: relative; +} +pre[data-lang]:before { + content: attr(data-lang); + display: inline-block; + background-color: var(--color-blue-gray-100); + color: var(--color-blue-grey-900); + border-radius: 6px; + position: absolute; + left: 0; + top: 0; + padding: 0 5px; + text-transform: uppercase; + font-size: 0.7em; + line-height: 16px; +} +/* inlinde-code */ +code { + display: inline-block; + padding: 0 4px; + line-height: 1.2em; + box-sizing: border-box; + background-color: var(--color-blue-gray-50); + color: var(--color-blue-grey-900); + border-radius: 3px; +} +/* ul\li section */ + ul,ol { + margin-left: 0.5em; + margin-bottom: 0.5em; + padding-inline-start: 0; +} +ol { + li { list-style: decimal; margin-left: 1em;} +} +ul { + li { list-style-type: square;margin-left: 1em;} +} +li[data-listStyle] { + list-style: none; + list-style-type: none; + position: relative; + +} +li[data-listStyle]:before { + content: attr(data-liststyle); + position: absolute; + margin-left: -20px; +} + +/* Images */ +img { + box-sizing: border-box; + height: auto; + max-width: 100%; + max-height: 100%; + display: block; + border-radius: 3px; +} +figure { + border-radius: 4px; + margin: 5px; + padding: 5px; + border:1px solid var(--color-blue-gray-200); +} +figure > figcaption { + font-weight: normal; + font-size: 0.8em; +} +hr { + display: block; + outline: none; + border: 1px dashed var(--color-blue-gray-200); + margin: 1em 0; +} +button, .btn { + background: var(--color-blue-gray-50); + outline: none; + padding: 3px 6px; + border-radius: 3px; + border: 1px solid var(--color-blue-grey-200); + border-bottom: 3px solid var(--color-blue-grey-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; + user-select: none; + cursor: pointer; + &:hover { + background: var(--color-blue-gray-100); + border-color: var(--color-blue-gray-300); + } + &:focus { + border-color: var(--color-blue-500); + } + &:active { + padding-top:2px; + background: var(--color-blue-gray-100); + border-color: var(--color-blue-gray-700); + border-bottom: 1px solid; + } + &:disabled { + pointer-events:none; + color: var(--color-blue-gray-400); + border: 1px solid var(--color-blue-gray-300); + background-color: var(--color-blue-gray-200); + } + & > a { + text-decoration: none; + } + &.-green { + border-color: var(--color-green-500) + } + &.-blue { + background-color: var(--color-blue-500); + color:#fff; + border-color: var(--color-blue-800); + &:active { + background-color: var(--color-blue-600); + border-color: var(--color-blue-900); + } + } +} +.-text-center { + text-align: center; +} +.-text-right { + text-align: right; +} +.-text-left { + text-align: left; +} \ No newline at end of file diff --git a/src/sass/wc-wysiwyg.scss b/src/sass/wc-wysiwyg.scss new file mode 100644 index 0000000..dedbf5e --- /dev/null +++ b/src/sass/wc-wysiwyg.scss @@ -0,0 +1,242 @@ + +.wc-wysiwyg { + background-color: #eee; + font-family: Arial, Helvetica, sans-serif; + position: relative; + border: 1px solid var(--color-blue-gray-400); + border-radius: 3px; + display: block; + &_bt { + display: block; + padding:5px; + margin: 0; + border: none; + outline: none; + border-radius: 3px; + background-color: var(--color-blue-gray-100); + & .-errors { + background-color: var(--color-red-500); + color: var(--color-red-50); + padding: 3px; + font-size: 10px; + line-height: 15px; + border-radius: 3px; + } + } + /* custom elements */ + + &_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); + } + } + /* preview */ + &_pr { + width: 100%; + box-sizing: border-box; + max-width: 100%; + min-height: 200px; + } + &_content { + padding:5px 5px 2em 5px; + border:1px solid #ccc; + background: #fff; + overflow-x: hidden; + overflow-y: scroll; + max-width: 960px; + width: 100%; + box-sizing: border-box; + margin: 0 auto; + box-sizing: border-box; + display: inline-block; + resize: vertical; + & .-selected { + background-color: var(--color-blue-100); + } + &:focus, &:active { + outline: 5px solid var(--color-green-300); + border: none; + } + &.-invalid:focus { + outline: 5px solid var(--color-red-500); + } + } + &_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); + } + &_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; + user-select: none; + cursor: pointer; + &:hover { + background: var(--color-blue-gray-100); + border-color: var(--color-blue-gray-300); + } + &:focus { + border-color: var(--color-blue-500); + } + &:active { + padding-top:2px; + background: var(--color-blue-gray-100); + border-color: var(--color-blue-gray-700); + border-bottom: 1px solid; + } + &.-clear { + text-decoration: line-through; + font-weight: bold; + } + } + &_ia { + display: flex; + flex-wrap: wrap; + } + &_ia > input { + box-sizing: border-box; + min-width: 100%; + position: relative; + } + /* inline dialog */ + &_di { + z-index: 99901; + 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; + & > form { + &:nth-child(1n+2) { + margin-top: 10px; + } + } + } + /* props form */ + &_pf { + display: flex; + border-radius: 3px; + padding: 3px; + background-color: var(--color-blue-100); + flex-wrap: nowrap; + font-size: 0.9em; + align-items: center; + &:before { + padding: 0 0 0 5px; + height: 25px; + display: inline-block; + content: '<' attr(data-tag) ' '; + text-transform: lowercase; + border-top-left-radius: 3px; + border-bottom-left-radius: 3px; + font-weight: bold; + margin-right: 5px; + + } + & > label { + background-color: var(--color-blue-200); + color: var(--color-blue-gray-800); + padding: 3px 3px 3px 5px; + display: flex; + align-items: center; + border-radius: 6px; + margin-right: 5px; + height: 25px; + line-height: 25px; + } + & input.wc_ed_inp { + background: #fff; + padding: 0 5px; + height: 25px; + display: inline-block; + border-radius: 6px; + box-sizing: border-box; + border: none; + &:focus { + border: none; + outline: 2px solid var(--color-green-500); + } + } + & > button { + height: 25px; + width: 25px; + border-radius: 6px; + } + } + + /* hints */ + & *[data-hint] { + position: relative; + } + & *[data-hint]:hover:after { + visibility: visible; + z-index: 9900; + transform: translate(-50%, -100%); + } + & *[data-hint]:after { + visibility: hidden; + position: absolute; + transition: transform .2s ease; + left: 50%; + top: -3px; + transform: translate(-50%, 0%); + background-color: var(--color-blue-gray-700); + color: #fff; + content: attr(data-hint); + display: inline-block; + padding: 3px; + border-radius: 3px; + font-size: 10px; + line-height: 10px; + } + & .-display-none { + display: none; + } +} \ No newline at end of file diff --git a/src/wc-wysiwyg.ts b/src/wc-wysiwyg.ts new file mode 100644 index 0000000..39c56c4 --- /dev/null +++ b/src/wc-wysiwyg.ts @@ -0,0 +1,731 @@ +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, +} + +//All semantic html5 known editor tags +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[]; + +class WCWYSIWYG extends HTMLElement { + public EditorTags:WCWYSIWYGTag[] + public EditorCustomTags:WCWYSIWYGTag[] + //Content editable wc-editor element + public EditorNode:HTMLElement + public EditorActionsSection:HTMLElement + //Inline edites + public EditorInlineActions:any[] + public EditorInlineDialog:HTMLDialogElement + public EditorInlineActionsForm:HTMLElement + //Editor props + public EditorPropertyForm?:HTMLElement + //Clear btn + public EditorClearFormatBtn:HTMLElement + //Autocomplete area + public EditorAutoCompleteForm?:HTMLElement + //Bottom actions + public EditorBottomForm?:HTMLElement + public EditorBottomFormNewP?:HTMLElement + public EditorBottomFormViewToggle?:HTMLElement + + public EditorPreviewText:HTMLTextAreaElement + public EditorCustomTagsForm?:HTMLElement + public EditorTagsMethods:WCWYSIWYGActions + public EditorAllowTags:string[] + public EditorFullScreenButton?:HTMLElement + public lang:string = 'ru' + public value:string = '' + + static observedAttributes = ['value']; + + #EditProps:boolean|object + #Autocomplete:boolean + #SotrageKey:string|null + #HideBottomActions:boolean + #Connected:boolean = false; + + constructor() { + super(); + this.classList.add('wc-wysiwyg'); + + //Listen root element events + this.onpointerup = (event) => { + const selection = window.getSelection(); + //if check exist selection string + if(selection !== null && selection.toString().length > 0) { + this.EditorInlineActionsForm.style.display = ''; + this.EditorPropertyForm.style.display = 'none'; + this.showEditorInlineDialog(); + } else { + this.hideEditorInlineDialog(); + } + }; + this.onfullscreenchange = (event) => { + const isFullScreen = document.fullscreenElement; + this.classList.toggle('-fullscreen', isFullScreen !== null); + }; + } + + connectedCallback() { + if(this.#Connected === false) { + //Check Tags + const allowTags = this.getAttribute('data-allow-tags') || allTags.map(t => t.tag).join(','); + + //Bind inner textarea to wc-wysiwyg + this.EditorPreviewText = this.querySelector('textarea') as HTMLTextAreaElement; + this.EditorPreviewText.className = 'wc-wysiwyg_pr -display-none'; + this.EditorPreviewText.oninput = event => { + const target = event.target as HTMLTextAreaElement; + this.EditorNode.innerHTML = target.value; + this.value = target.value; + }; + + this.EditorAllowTags = allowTags.split(','); + this.EditorTags = allTags.filter(tag => allowTags.includes(tag.tag)); + + this.#EditProps = this.getAttribute('data-edit-props') !== null ? JSON.parse(this.getAttribute('data-edit-props')) : false; + this.#Autocomplete = this.getAttribute('data-autocomplete') === '1'; + this.#HideBottomActions = this.getAttribute('data-hide-bottom-actions') === '1'; + //allow inline without ['video','audio','img'] + 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 + this.EditorClearFormatBtn = el('button', { + classList: ['wc-wysiwyg_btn', '-clear'], + attrs: { + 'data-hint': this.#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(); + }, + } + }); + + //Edit props + if(this.#EditProps) { + //Inline property editor + this.EditorPropertyForm = el('form',{ + styles: { display: 'none' }, + classList: ['wc-wysiwyg_pf'], + props: { + onsubmit: event => { + event.preventDefault(); + this.hideEditorInlineDialog(); + }, + onpointerup: event => event.stopPropagation(), + } + }); + this.EditorInlineDialog.append(this.EditorPropertyForm); + } + + //Autocomplete form + if(this.#Autocomplete) { + this.EditorAutoCompleteForm = el('form', { + classList: ['wc-wysiwyg_au'], + props: { + onsubmit: submitEvent => { + submitEvent.preventDefault(); + submitEvent.stopPropagation(); + const tagName = submitEvent.submitter.value; + const newEl = el(tagName, { + props: { + innerHTML: tagName + } + }); + submitEvent.target.parentElement.replaceWith(newEl); + newEl.focus(); + } + } + }); + } + + //Actions in footer + this.EditorBottomForm = el('fieldset', { + classList: ['wc-wysiwyg_bt'], + }); + + if(this.#HideBottomActions === false) { + //Toggler btn text/html + this.EditorBottomFormViewToggle = el('button', { + classList: ['wc-wysiwyg_btn'], + attrs: { + 'data-hint': this.#t('toggleViewMode'), + 'data-mode': 'html5', + }, + props: { + type:'button', + innerText: 'текст/html5', + onpointerup: event => { + let mode = this.EditorBottomFormViewToggle.getAttribute('data-mode'); + let newMode = mode === 'html5' ? 'text' : 'html5'; + this.EditorBottomFormViewToggle.setAttribute('data-mode', newMode); + this.EditorNode.style.display = newMode === 'html5' ? '' : 'none'; + this.EditorPreviewText.classList.toggle('-display-none', newMode === 'html5' ? true : false) + if(newMode === 'text') { + this.EditorPreviewText.value = this.EditorNode.innerHTML; + } + } + } + }); + ///New

append btn + this.EditorBottomFormNewP = el('button', { + classList: ['wc-wysiwyg_btn'], + attrs: { + 'data-hint': this.#t('addNewParahraph'), + }, + props: { + type:'button', + innerText: '+ P', + onpointerup: event => { + const P = el('p', {props: {innerText: '/'}}); + this.EditorNode.appendChild(P); + P.focus(); + } + } + }); + //Fullscreen button + this.EditorFullScreenButton = el('button', { + classList: ['wc-wysiwyg_btn'], + attrs: { + 'data-hint': this.#t('fullScreen'), + }, + props: { + type: "button", + ariaRoleDescription: "button", + innerText: '🖥️', + onpointerup: event => { + this.requestFullscreen(); + } + } + }); + + this.EditorBottomForm.append( + this.EditorBottomFormNewP, + this.EditorBottomFormViewToggle, + this.EditorFullScreenButton, + ); + } + + //Check custom tags + this.EditorCustomTags = JSON.parse( String(this.getAttribute('data-custom-tags')) ); + if(this.EditorCustomTags !== null) { + //Custom panel tags + this.EditorCustomTagsForm = el('fieldset', { + classList: ['wc-wysiwyg_ce'], + }); + //Make custom actions buttons panel + this.#makeActionButtons(this.EditorCustomTagsForm, this.EditorCustomTags); + + this.appendChild(this.EditorCustomTagsForm); + } + + //Node editable + this.EditorNode = el('article', { + classList: ['wc-wysiwyg_content', this.getAttribute('data-content-class')], + props: { + contentEditable: true, + onpointerup: event => { + this.checkCanClearElement(event); + if(this.#EditProps) { + this.checkEditProps(event); + } + }, + oninput: event => { + this.updateContent(); + if(this.#Autocomplete) { + this.checkAutoComplete(); + } + }, + //Handle key bindings + 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(' '); + 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: ` ` } }); + 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 + this.append( + this.EditorActionsSection, + this.EditorInlineDialog, + this.EditorNode, + this.EditorPreviewText, + this.EditorBottomForm, + ); + this.EditorNode.innerHTML = this.EditorPreviewText.value; + this.updateContent(); + + this.#Connected = true; + } + } + + /** + * Update content value and update behaviors + */ + updateContent() { + this.value = this.EditorNode.innerHTML; + this.checkValidity(); + 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')); + } + + /** + * Update content at preview element if exists + * @param selector css + */ + updatePreviewEl(selector) { + if(selector) { + const previewEl = window.document.body.querySelector(selector); + if(previewEl) { + previewEl.innerHTML = this.value; + } + } + } + + /** + * Validate content + * @returns boolean hasErrors + */ + checkValidity() { + let hasErros = false, + errors= []; + + //Check attrs + if(this.getAttribute('required') !== null) { + if(String(this.EditorNode.textContent).length === 0) { + hasErros = true; + errors.push(this.#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')}`); + } + } + 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')}`); + } + } + if(this.getAttribute('filtertags')) { + const disallowTags = this.getAttribute('filtertags').split(','); + for (let i = 0; i < disallowTags.length; i++) { + const checkTag = disallowTags[i]; + if(this.EditorNode.querySelector(checkTag)) { + hasErros = true; + errors.push(`${this.#t('filtertags')} ${checkTag}`); + break; + } + } + } + this.EditorNode.classList.toggle('-invalid', hasErros); + let oldErrors = this.querySelector('.-errors'); + if(oldErrors) { + oldErrors.parentElement.removeChild(oldErrors); + } + if(hasErros) { + const errosEl = el('p', { + props: { + innerHTML: errors.join('
') + }, + classList: ['-errors'], + }) + + this.append(errosEl); + } + return hasErros === false; + } + + /** + * Check if need append autocompleted tags variants + */ + checkAutoComplete() { + //CHeck autococmplete + const Selecton = window.getSelection(); + if(Selecton !== null && Selecton.anchorNode !== null) { + const SelectionParentEl = Selecton.anchorNode.parentElement as HTMLParagraphElement; + + if(SelectionParentEl !== null && + //if empty selection + Selecton.toString() === '' && + //and parent node is

+ SelectionParentEl.nodeName === 'P' && + //and parent

is parentElement in EditorNode + SelectionParentEl.parentElement === this.EditorNode) { + //and parent

inner text starts with `/` + if(SelectionParentEl.innerText.startsWith('/')) { + const parsedTagName = SelectionParentEl.innerText.replace('/', ''); + const filteredActions = this.EditorTags.filter(action => action.tag.toLocaleLowerCase().startsWith(parsedTagName.toLocaleLowerCase())); + if(filteredActions.length > 0) { + this.EditorAutoCompleteForm.innerHTML = ''; + filteredActions.forEach(action => { + this.EditorAutoCompleteForm.appendChild(el('button', { + classList: ['wc-wysiwyg_btn', `-${action.tag}`], + attrs: { + 'data-hint': this.#t(action.tag) || null, + }, + props: { + type: 'submit', + innerText: action.tag, + value: action.tag, + } + })) + }); + SelectionParentEl.appendChild(this.EditorAutoCompleteForm); + } else { + //clear form + this.EditorAutoCompleteForm.innerHTML = ''; + //if exist in DOM detach + if(this.EditorAutoCompleteForm.parentElement) { + this.EditorAutoCompleteForm.parentElement.removeChild(this.EditorAutoCompleteForm); + } + } + } + } + } + } + + /** + * 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 + * @param event + */ + checkCanClearElement(event:Event) { + const eventTarget = event.target as HTMLElement; + 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) => { + eventTarget.replaceWith(document.createTextNode(eventTarget.textContent)); + } + this.showEditorInlineDialog(); + } else { + this.EditorClearFormatBtn.style.display = 'none'; + this.EditorClearFormatBtn.onpointerup = undefined; + } + } + } + + /** + * Checking click tag for editable props + **/ + checkEditProps(event) { + //Check need edit props + const eventTarget = event.target as HTMLElement; + + //Check exist prop\attr + if(this.#EditProps[eventTarget.nodeName]) { + const props = this.#EditProps[eventTarget.nodeName]; + event.stopPropagation(); + this.EditorPropertyForm.style.display = ''; + this.showEditorInlineDialog(); + this.EditorPropertyForm.setAttribute('data-tag', eventTarget.nodeName); + this.EditorPropertyForm.innerHTML = ''; + for (let i = 0; i < props.length; i++) { + const tagProp = props[i]; + const isAttr = tagProp.indexOf('data-') > -1 || tagProp === 'class'; + this.EditorPropertyForm.append(el('label', { + props: { innerText: `${tagProp}=` }, + append: [ + el('input', { + attrs: { placeholder: tagProp }, + classList: ['wc-wysiwyg_inp'], + props: { + value: isAttr ? eventTarget.getAttribute(tagProp) : eventTarget[tagProp] || '', + oninput: (eventInput) => { + const eventInputTarget = eventInput.target as HTMLInputElement; + if(tagProp === 'class') { + eventTarget.className = eventInputTarget.value; + } + if((isAttr || tagProp === 'datetime') && eventInputTarget !== null) { + eventTarget.setAttribute(tagProp, eventInputTarget.value) + } else { + eventTarget[tagProp] = eventInputTarget.value; + } + this.updateContent(); + } + } + }) + ] + })); + } + //add submit button for better UX + this.EditorPropertyForm.append(el('button', { + classList: ['wc-wysiwyg_btn'], + props: { + type: 'submit', + innerHTML: '↳', + }, + })); + } + } + + #makeActionButtons(toEl:HTMLElement, actions) { + 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, + type:'button', + textContent: action.is ? `${action.tag} is=${action.is}` : action.tag, + onpointerup: (event) => this.#tag(action.tag, event, action.is), + }, + attrs: { + 'data-hint': action.hint ? action.hint : this.#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) { + case 'audio': + this.#Media('audio'); + break; + case 'video': + this.#Media('video'); + break; + case 'img': + this.#Image(); + break; + default: + this.#wrapTag(tag, is); + break; + } + } + + /** + * Wrap content in + **/ + #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'; + } + if(is) { + defaultOptions.options = {is}; + } + let tagNode = el(tag, defaultOptions); + + if (Selection !== null && Selection.rangeCount) { + if(['ul', 'ol'].includes(tag)) { + const list = el(tag); + tagNode.replaceWith(list); + list.append(tagNode) + } + const range = Selection.getRangeAt(0).cloneRange(); + range.surroundContents(tagNode); + Selection.removeAllRanges(); + Selection.addRange(range); + if(Selection.toString().length === 0) { + tagNode.innerText = tag; + } + this.updateContent(); + } + } + + /** + * Insert