feat: 新增 WYSIWYG 编辑器功能和相关工具栏事件处理

This commit is contained in:
2026-04-10 16:18:01 +08:00
parent 40b77471b5
commit f20b7aa61c
128 changed files with 16048 additions and 84 deletions

View File

@@ -0,0 +1,279 @@
@keyframes slideInDown {
from {
transform: translate3d(0, -100%, 0);
visibility: visible;
}
to {
transform: translate3d(0, 0, 0);
}
}
.vditor {
display: flex;
flex-direction: column;
border: 1px solid var(--border-color);
border-radius: 3px;
box-sizing: border-box;
font-family: @font-family-base;
&--fullscreen {
position: fixed;
top: 0;
width: 100% !important;
left: 0;
height: 100vh !important;
z-index: 90;
border-radius: 0;
}
&-content {
display: flex;
min-height: 60px;
flex: 1;
min-width: 1px;
position: relative;
}
&-preview {
flex: 1;
min-width: 1px;
overflow: auto;
margin-left: -1px;
border-left: 1px solid var(--border-color);
box-sizing: border-box;
border-radius: 0 0 3px 0;
background-color: var(--textarea-background-color);
&::-webkit-scrollbar {
display: none;
}
&__action {
text-align: center;
padding: 10px;
background-color: var(--toolbar-background-color);
button {
background-color: var(--toolbar-background-color);
color: var(--toolbar-icon-color);
line-height: 20px;
border: 0;
margin: 0 10px;
cursor: pointer;
padding: 0 7px;
font-size: 12px;
&.vditor-preview__action--current,
&:hover {
color: var(--toolbar-icon-hover-color);
background-color: var(--toolbar-background-color);
}
&:focus {
outline: none;
}
svg {
fill: currentColor;
height: 15px;
width: 15px;
vertical-align: middle;
}
}
}
& > .vditor-reset {
padding: 10px;
margin: 0 auto;
}
img:not(.emoji) {
cursor: pointer;
}
}
&-devtools {
display: none;
background-color: var(--textarea-background-color);
overflow: auto;
flex: 1;
min-width: 1px;
box-shadow: inset 1px 0 var(--border-color);
box-sizing: border-box;
border-radius: 0 0 3px 0;
padding: 10px;
}
&-counter {
padding: 3px;
color: var(--toolbar-icon-color);
background-color: var(--count-background-color);
border-radius: 3px;
font-size: 12px;
user-select: none;
float: right;
margin: 8px 3px 0 0;
&--error {
color: @errorColor;
background-color: rgba(@errorColor, 0.1);
}
}
&-resize {
padding: 3px 0;
cursor: row-resize;
user-select: none;
position: absolute;
width: 100%;
&--top {
top: -3px;
}
&--bottom {
bottom: -3px;
}
& > div {
height: 3px;
background-color: var(--resize-background-color);
transition: @transition;
}
&:hover,
&--selected {
& > div {
background-color: var(--resize-hover-background-color);
}
svg {
color: var(--resize-hover-icon-color);
}
}
svg {
fill: currentColor;
stroke-width: 0;
stroke: currentColor;
width: 13px;
height: 3px;
display: block;
margin: 0 auto;
color: var(--resize-icon-color);
}
}
&-upload {
position: absolute;
height: 3px;
left: 0;
top: -2px;
transition: @transition;
background-color: @blurColor;
}
&-tip {
position: absolute;
font-size: 12px;
top: 10px;
animation-duration: 0.15s;
animation-fill-mode: both;
left: 50%;
z-index: 5;
&--show {
display: block;
animation-name: slideInDown;
}
&__content {
text-align: left;
display: inline-block;
line-height: 16px;
padding: 3px 10px;
border-radius: 3px;
background: var(--toolbar-background-color);
position: relative;
margin-left: -50%;
color: var(--toolbar-icon-color);
max-width: 100%;
box-shadow: var(--panel-shadow);
ul {
margin: 2px 0;
padding: 0 0 0 18px;
}
a {
color: @blurColor;
}
}
&__close {
position: absolute;
color: var(--toolbar-icon-color);
top: -7px;
right: -15px;
font-weight: bold;
cursor: pointer;
&:hover {
color: var(--toolbar-icon-hover-color);
}
}
}
&-img {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: column;
z-index: 100;
&__bar {
border-bottom: 1px solid var(--border-color);
background-color: var(--toolbar-background-color);
text-align: center;
height: 36px;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
}
&__btn {
display: flex;
align-items: center;
cursor: pointer;
margin-left: 24px;
user-select: none;
color: var(--toolbar-icon-color);
&:hover {
color: var(--toolbar-icon-hover-color);
}
svg {
height: 14px;
width: 14px;
margin-right: 8px;
fill: currentColor;
}
}
&__img {
flex: 1;
background-color: var(--textarea-background-color);
overflow: auto;
cursor: zoom-out;
img {
max-width: none;
}
}
}
}

View File

@@ -0,0 +1,67 @@
.vditor-hint {
background-color: var(--panel-background-color);
position: absolute;
box-shadow: var(--panel-shadow);
border-radius: 3px;
padding: 5px 0;
z-index: 4;
line-height: 20px;
list-style: none;
font-size: 12px;
margin: 0;
max-width: 250px;
min-width: 80px;
display: none;
.vditor-hint {
margin-top: -31px;
left: 100%;
right: auto;
&.vditor-panel--left {
right: 100%;
left: auto;
}
}
button {
color: var(--toolbar-icon-color);
display: block;
padding: 3px 10px;
border: 0;
border-radius: 0;
line-height: 20px;
width: 100%;
box-sizing: border-box;
text-align: left;
margin: 0;
background-color: transparent;
cursor: pointer;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
&:focus {
outline: none;
}
}
&--current,
button:not(.vditor-menu--disabled):hover {
background-color: var(--toolbar-background-color) !important;
color: var(--toolbar-icon-hover-color) !important;
}
&__emoji {
font-size: 16px;
float: left;
margin-right: 3px;
}
img {
height: 20px;
width: 20px;
float: left;
margin-right: 3px;
}
}

View File

@@ -0,0 +1,275 @@
.vditor-ir {
box-sizing: border-box;
flex: 1;
min-width: 1px;
position: relative;
width: 100%;
&__node {
&[data-type="code-block"]:before,
&[data-type="code-block"]:after,
&[data-type="yaml-front-matter"]:before,
&[data-type="yaml-front-matter"]:after,
&[data-type="math-block"]:before,
&[data-type="math-block"]:after {
content: ' ';
color: var(--second-color);
}
&:not(.vditor-ir__node--expand) .vditor-ir__marker {
padding: 0 !important;
}
&:not(.vditor-ir__node--expand)[data-type="a"] {
cursor: pointer;
}
&[data-type="link-ref"],
&[data-type="footnotes-ref"] {
color: @blurColor;
}
&[data-type="html-block"] {
margin-bottom: 1em;
}
.vditor-ir__marker {
width: 0;
overflow: hidden;
display: inline-block;
height: 0;
transition: @transition;
}
&--hidden {
.vditor-ir__marker {
visibility: hidden;
}
}
&--expand {
.vditor-ir__marker {
color: var(--second-color);
display: inline;
height: auto;
width: auto;
&--hide {
display: none;
}
&--heading {
color: var(--ir-heading-color);
}
&--bi {
color: var(--ir-bi-color);
}
&--link {
color: var(--ir-link-color);
}
&--title {
color: var(--ir-title-color);
}
&--bracket {
color: var(--ir-bracket-color);
text-decoration: underline;
}
&--paren {
color: var(--ir-paren-color);
}
&--info {
color: var(--ir-heading-color);
}
&--pre code {
color: var(--textarea-text-color);
height: auto;
text-align: left;
}
}
&[data-type="code-block"]:before,
&[data-type="code-block"]:after {
content: '```';
}
&[data-type="yaml-front-matter"]:before,
&[data-type="yaml-front-matter"]:after {
content: '---';
}
&[data-type="math-block"]:before,
&[data-type="math-block"]:after {
content: '$$';
}
}
span[data-type="code-block-open-marker"],
span[data-type="code-block-close-marker"],
span[data-type="yaml-front-matter-open-marker"],
span[data-type="yaml-front-matter-close-marker"],
span[data-type="math-block-open-marker"],
span[data-type="math-block-close-marker"] {
display: none;
}
}
&__preview {
cursor: pointer;
white-space: initial;
min-height: 27px;
}
&__link {
color: var(--ir-bracket-color);
text-decoration: underline;
}
pre.vditor-reset {
background-color: var(--panel-background-color);
margin: 0;
white-space: pre-wrap;
height: 100%;
box-sizing: border-box;
&[contenteditable="false"] {
opacity: 0.3;
cursor: not-allowed;
}
&:empty::before {
content: attr(placeholder);
color: var(--second-color);
}
&:focus {
outline: none;
background-color: var(--textarea-background-color);
}
&:after {
content: "";
height: var(--editor-bottom);
display: block;
}
pre {
margin: 0;
}
}
hr {
display: inline-block;
margin: 12px 0;
width: 100%;
}
blockquote:empty::before,
pre > code:empty::before,
p:empty::before,
h1:empty::after,
h2:empty::after,
h3:empty::after,
h4:empty::after,
h5:empty::after,
h6:empty::after {
content: ' ';
}
// block title
& .vditor-reset > h1:before,
& .vditor-reset > h2:before,
& .vditor-reset > h3:before,
& .vditor-reset > h4:before,
& .vditor-reset > h5:before,
& .vditor-reset > h6:before,
div[data-type="link-ref-defs-block"]:before,
div[data-type="footnotes-block"]:before,
.vditor-toc:before {
float: left;
padding-right: 4px;
margin-left: -29px;
content: 'H1';
font-size: 0.85rem;
font-weight: normal;
color: var(--second-color);
}
& .vditor-reset > h2:before {
content: 'H2';
}
& .vditor-reset > h3:before {
content: 'H3';
}
& .vditor-reset > h4:before {
content: 'H4';
}
& .vditor-reset > h5:before {
content: 'H5';
}
& .vditor-reset > h6:before {
content: 'H6';
}
div[data-type="link-ref-defs-block"] {
&:before {
content: '"A"';
}
color: var(--blockquote-color);
}
div[data-type="footnotes-block"] {
&:before {
content: '^F';
}
border-top: 2px solid var(--heading-border-color);
padding-top: 24px;
margin-top: 24px;
& > div[data-type="footnotes-def"] {
& > ul, & > ol, & > p, & > blockquote, & > pre, & > table, & > hr {
margin-left: 8px;
}
}
}
.vditor-toc {
&:before {
content: "ToC";
}
span[data-type="toc-h"] {
color: @blurColor;
text-decoration: underline;
}
}
}
@media screen and (max-width: @max-width) {
.vditor-ir {
h1:before,
h2:before,
h3:before,
h4:before,
h5:before,
h6:before,
div[data-type="link-ref-defs-block"]:before,
div[data-type="footnotes-block"]:before,
.vditor-toc:before {
content: none;
}
}
}

View File

@@ -0,0 +1,110 @@
/**
* panel.
*
* @author <a href="http://vanessa.b3log.org">Liyuan Li</a>
* @version 1.0.0.0, Jan 22, 2020
*/
@keyframes scale-in {
0% {
opacity: 0;
transform: scale(0.5);
}
100% {
opacity: 1;
transform: scale(1);
}
}
.vditor {
&-panel {
background-color: var(--panel-background-color);
position: absolute;
box-shadow: var(--panel-shadow);
border-radius: 3px;
padding: 5px;
z-index: 3;
font-size: 14px;
display: none;
user-select: none;
max-width: 320px;
min-width: 80px;
animation-duration: 0.15s;
animation-name: scale-in;
animation-timing-function: cubic-bezier(0.2, 0, 0.13, 1.5);
color: var(--toolbar-icon-color);
&--none {
padding: 0;
animation: none;
min-width: auto;
max-width: none;
white-space: nowrap;
opacity: 0.86;
}
&--arrow:before {
position: absolute;
width: 0;
height: 0;
pointer-events: none;
content: " ";
border: 7px solid transparent;
top: -14px;
left: 5px;
border-bottom-color: var(--panel-background-color);
}
&--left {
right: 0;
&.vditor-panel--arrow:before {
right: 5px;
left: auto;
}
}
}
&-input {
border: 0;
padding: 3px 5px;
background-color: var(--panel-background-color);
font-size: 12px;
color: var(--textarea-text-color);
&:focus {
background-color: var(--toolbar-background-color);
outline: none;
}
}
&-icon {
color: var(--toolbar-icon-color);
cursor: pointer;
float: left;
padding: 4px 5px;
height: 21px;
width: 23px;
background-color: transparent;
border: 0;
box-sizing: border-box;
&:hover,
&--current {
color: var(--toolbar-icon-hover-color);
background-color: transparent;
}
&:focus {
outline: none;
}
svg {
height: 13px !important;
width: 13px !important;
float: left;
fill: currentColor;
pointer-events: none;
}
}
}

View File

@@ -0,0 +1,610 @@
.vditor {
&-reset {
&--anchor {
padding-left: 20px;
}
&--error {
color: @errorColor;
font-size: 12px;
display: block;
line-height: 16px;
}
color: #24292e;
font-variant-ligatures: no-common-ligatures;
font-family: @font-family-base;
word-wrap: break-word;
overflow: auto;
line-height: 1.5;
font-size: 16px;
word-break: break-word;
ul ul ul {
list-style-type: square;
}
ul ul {
list-style-type: circle;
}
ul {
list-style-type: disc;
}
ul,
ol {
padding-left: 2em;
margin-top: 0;
margin-bottom: 16px;
}
li + li {
margin-top: 0.25em;
}
li p {
margin-top: 16px;
}
audio {
max-width: 100%;
&:focus {
outline: none;
}
}
video {
max-height: 90vh;
max-width: 100%;
}
img {
max-width: 100%;
}
img.emoji {
cursor: auto;
max-width: 20px;
vertical-align: sub;
}
h1,
h2,
h3,
h4,
h5,
h6 {
margin-top: 24px;
margin-bottom: 16px;
font-weight: 600;
line-height: 1.25;
&:hover .vditor-anchor svg {
visibility: visible;
}
}
h1 {
font-size: 1.75em;
}
h2 {
font-size: 1.55em;
}
h3 {
font-size: 1.38em;
}
h4 {
font-size: 1.25em;
}
h5 {
font-size: 1.13em;
}
h6 {
font-size: 1em;
}
hr {
height: 2px;
padding: 0;
margin: 24px 0;
background-color: #eaecef;
border: 0;
}
p {
margin-top: 0;
margin-bottom: 16px;
}
blockquote {
padding: 0 1em;
color: #6a737d;
border-left: 0.25em solid #eaecef;
margin: 0 0 16px 0;
& > :first-child {
margin-top: 0;
}
& > :last-child {
margin-bottom: 0;
}
}
ins > iframe {
border: 0;
}
iframe {
border: 1px solid #d1d5da;
max-width: 100%;
box-sizing: border-box;
&.iframe__video {
min-width: 80%;
min-height: 36vh;
}
}
table {
border-collapse: collapse;
empty-cells: show;
margin-bottom: 16px;
overflow: auto;
border-spacing: 0;
display: block;
word-break: keep-all;
width: 100%;
tr {
background-color: #fafbfc;
border-top: 1px solid #c6cbd1;
}
td,
th {
padding: 6px 13px;
border: 1px solid #dfe2e5;
word-break: normal;
white-space: nowrap;
&:first-child::after {
content: "";
display: inline-block;
vertical-align: top;
min-height: 24px;
}
}
th {
font-weight: 600;
}
tbody tr:nth-child(2n) {
background-color: #fff;
}
}
code:not(.hljs):not(.highlight-chroma) {
padding: 0.2em 0.4em;
margin: 0;
font-size: 85%;
border-radius: 3px;
font-family: @font-family-code;
word-break: break-word;
background-size: 20px 20px;
white-space: pre-wrap;
}
pre {
margin: 1em 0;
& > code {
margin: 0;
font-size: 85%;
padding: 0.5em;
border-radius: 5px;
display: block;
overflow: auto;
white-space: pre;
font-family: @font-family-code;
background-size: 20px 20px;
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA8AgMAAABHkjHhAAAACVBMVEWAgIBaWlo+Pj7rTFvWAAAAA3RSTlMHCAw+VhR4AAAA+klEQVQoz4WSMW7EQAhFPxKWNh2FCx+HkaZI6RRb5DYbyVfIJXLKDCFoMbaTKSw/8ZnPAPjaH2xgZcUNUDADD7D9LtDBCLZ45fbkvo/30K8yeI64pPwl6znd/3n/Oe93P3ho9qeh72btTFzqkz0rsJle8Zr81OLEwZ1dv/713uWqvu2pl+k0fy7MWtj9r/tN5q/02z89qa/L4Dc2LvM93kezPfXlME/O86EbY/V9GB9ePX8G1/6W+/9h1dq/HGfTfzT3j/xNo7522Bfnqe5jO/fvhVthlfk434v3iO9zG/UOphyPeinPl1J8Gtaa7xPTa/Dk+RIs4deMvwGvcGsmsCvJ0AAAAABJRU5ErkJggg==);
word-break: initial;
word-wrap: normal;
}
&:hover div.vditor-copy {
display: block;
}
}
.language-math,
.language-echarts,
.language-mindmap,
.language-plantuml,
.language-mermaid,
.language-markmap,
.language-abc,
.language-flowchart,
.language-graphviz {
margin-bottom: 16px;
}
.language-math mjx-container:focus {
outline: none;
cursor: context-menu;
}
.language-echarts,
.language-mindmap {
overflow: hidden;
height: 420px;
}
.language-mermaid,
.language-markmap,
.language-flowchart,
.language-graphviz {
text-align: center;
}
.language-graphviz parsererror {
overflow: auto;
}
kbd {
display: inline-block;
padding: 3px 5px;
font: 11px Consolas, "Liberation Mono", Menlo, Courier, monospace;
line-height: 10px;
color: #24292e;
vertical-align: middle;
background-color: #fafbfc;
border: solid 1px #d1d5da;
border-radius: 3px;
box-shadow: inset 0 -1px 0 #d1d5da;
}
summary {
cursor: pointer;
&:focus {
outline: none;
}
}
svg {
height: auto;
width: auto;
stroke-width: initial;
}
p:last-child,
blockquote:last-child,
pre:last-child,
ul:last-child,
ol:last-child,
hr:last-child {
margin-bottom: 0;
}
}
&-comment {
border-bottom: 2px solid rgb(248, 230, 171);
&--focus,
&--hover {
background-color: rgb(250, 241, 209);
border-bottom: 2px solid rgb(255, 198, 10);
.vditor-comment {
border-bottom: 2px solid rgb(255, 198, 10);
}
}
}
&-task {
list-style: none !important;
word-break: break-all;
input {
margin: 0 0.2em 0.25em -1.6em;
font-size: 12px;
vertical-align: middle;
}
}
&-copy {
position: relative;
display: none;
z-index: 1;
textarea {
position: absolute;
left: -100000px;
height: 10px;
}
span {
cursor: pointer;
position: absolute;
right: 15px;
top: 0.5em;
}
svg {
color: #586069;
height: 14px;
width: 14px !important;
display: block;
fill: currentColor;
}
}
&-linenumber {
padding-left: 4em !important;
position: relative;
&__rows {
position: absolute;
pointer-events: none;
top: 0.5em;
left: 0;
width: 3em; /* works for line-numbers below 1000 lines */
user-select: none;
counter-reset: linenumber;
& > span {
pointer-events: none;
display: block;
&::before {
counter-increment: linenumber;
content: counter(linenumber);
color: rgba(158, 150, 150, 0.38);
display: block;
padding-right: 1em;
text-align: right;
}
}
}
}
&-speech {
position: absolute;
display: none;
background-color: #f6f8fa;
border: 1px solid #d1d5da;
border-radius: 3px;
padding: 3px;
cursor: pointer;
color: #586069;
&:hover,
&--current {
color: #4285f4;
}
svg {
height: 14px;
width: 14px;
fill: currentColor;
display: block;
stroke-width: 0;
stroke: currentColor;
}
}
&-anchor {
margin-left: 5px;
&--left {
float: left;
padding-right: 4px;
margin-left: -20px;
}
svg {
visibility: hidden;
}
&:hover {
svg {
visibility: visible;
}
}
&:focus {
outline: none;
}
}
&-linkcard {
margin: 31px auto 16px;
transition: @transition;
cursor: pointer;
max-width: 768px;
padding: 0 10px;
a {
border-radius: 3px;
background-color: #f6f8fa;
overflow: hidden;
max-height: 250px;
display: flex;
text-decoration: none;
flex-wrap: wrap-reverse;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
&:hover {
box-shadow: 0 0 3px rgba(0, 0, 0, 0.13), 0 3px 6px rgba(0, 0, 0, 0.26);
text-decoration: none;
}
&:visited .vditor-linkcard__abstract {
color: rgba(88, 96, 105, 0.36);
}
}
&__info {
padding: 10px;
min-width: 200px;
box-sizing: border-box;
flex: 1;
}
&__title {
font-size: 14px;
font-weight: 400;
color: #24292e;
display: flex;
align-items: center;
img {
cursor: pointer;
height: 20px;
width: 20px;
border-radius: 3px;
flex-shrink: 0;
margin-right: 5px;
}
}
&__abstract {
word-wrap: break-word;
word-break: break-all;
-webkit-line-clamp: 2;
overflow: hidden;
text-overflow: ellipsis;
-webkit-box-orient: vertical;
display: -webkit-box;
font-size: 13px;
color: #586069;
margin: 5px 0;
}
&__site {
font-size: 12px;
color: #4285f4;
}
&__image {
background-size: cover;
background-repeat: no-repeat;
background-position: center center;
max-width: 250px;
min-width: 126px;
cursor: pointer;
background-color: rgba(88, 96, 105, 0.36);
}
}
&-footnotes__goto-ref {
text-decoration: none;
}
&-toc {
margin-bottom: 16px;
user-select: text;
color: @blurColor;
.vditor-outline__action {
display: none;
}
ul {
list-style: none !important;
padding-left: 1em;
}
& > ul {
padding-left: 0;
}
span {
cursor: pointer;
}
li > span > svg {
width: 0;
height: 0;
}
}
&-outline {
width: 250px;
border-right: 1px solid var(--border-color);
background-color: var(--panel-background-color);
display: none;
overflow: auto;
&--right {
border-right: 0;
border-left: 1px solid var(--border-color);
}
&::-webkit-scrollbar {
display: none;
}
ul {
list-style: none !important;
padding-left: 1em;
margin: 0;
}
&__content > ul {
padding-left: 0;
}
li > span {
display: flex;
align-items: center;
padding: 5px 10px;
cursor: pointer;
color: var(--textarea-text-color);
& > svg {
height: 10px;
width: 10px;
flex-shrink: 0;
}
&:hover {
color: var(--toolbar-icon-hover-color);
}
& > span {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
&__title {
border-bottom: 1px dashed var(--border-color);
padding: 5px 10px;
color: var(--toolbar-icon-color);
font-size: 12px;
}
&__action {
transition: @transition;
fill: currentColor;
margin-right: 5px;
flex-shrink: 0;
&--close {
transform: rotate(-90deg);
}
}
}
}

View File

@@ -0,0 +1,132 @@
.vditor-sv {
font-family: @font-family-base;
margin: 0 1px 0 0;
overflow: auto;
width: 100%;
flex: 1;
min-width: 1px;
border: 0;
resize: none;
padding: 10px 9px 10px 10px;
box-sizing: border-box;
background-color: var(--panel-background-color);
outline: 0 none;
font-size: 16px;
line-height: 22px;
color: var(--textarea-text-color);
border-radius: 0 0 3px 3px;
font-variant-ligatures: no-common-ligatures;
white-space: pre-wrap;
word-break: break-word;
word-wrap: break-word;
&[contenteditable="false"] {
opacity: 0.3;
cursor: not-allowed;
}
&:empty::before {
content: attr(placeholder);
color: var(--second-color);
}
&:focus {
background-color: var(--textarea-background-color);
}
&:after {
content: "";
height: var(--editor-bottom);
display: block;
}
span[data-type="newline"] + span[data-type="text"]:empty {
display: inherit;
}
.sup {
vertical-align: super;
font-size: smaller;
}
.strong {
font-weight: bold;
}
.em {
font-style: italic;
}
.s {
text-decoration: line-through;
}
.mark:not(.vditor-sv__marker) {
background-color: yellow;
color: black;
}
.h1 {
font-size: 1.75em;
line-height: 44px;
}
.h2 {
font-size: 1.55em;
line-height: 38px;
}
.h3 {
font-size: 1.38em;
line-height: 27px;
}
.h4 {
font-size: 1.25em;
line-height: 25px;
}
.h5 {
font-size: 1.13em;
}
.h6 {
font-size: 1em;
}
&__marker {
color: var(--second-color);
&--heading {
color: var(--ir-heading-color);
}
&--bi {
color: var(--ir-bi-color);
}
&--link {
color: var(--ir-link-color);
}
&--title {
color: var(--ir-title-color);
}
&--bracket {
color: var(--ir-bracket-color);
}
&--paren {
color: var(--ir-paren-color);
}
&--info {
color: var(--ir-heading-color);
}
&--strong {
font-weight: bold;
}
}
}

View File

@@ -0,0 +1,180 @@
.vditor {
&-toolbar {
background-color: var(--toolbar-background-color);
border-bottom: 1px solid var(--border-color);
padding: 0 5px;
line-height: 1;
&--pin {
position: sticky;
top: 0;
z-index: 1;
}
&--hide {
transition: @transition;
height: 5px;
overflow: hidden;
&:hover {
background-color: var(--toolbar-background-color);
height: auto;
overflow: visible;
}
}
&__item {
float: left;
position: relative;
.vditor-tooltipped {
color: var(--toolbar-icon-color);
border: 0;
padding: 10px 5px;
background-color: transparent;
height: var(--toolbar-height);
width: 25px;
box-sizing: border-box;
font-size: 0;
&:focus {
outline: none;
}
&:focus {
cursor: pointer;
color: var(--toolbar-icon-hover-color);
}
}
svg {
fill: currentColor;
display: inline-block;
stroke-width: 0;
stroke: currentColor;
width: 15px;
height: 15px;
}
input {
position: absolute;
width: 25px;
height: var(--toolbar-height);
top: 0;
left: 0;
cursor: pointer;
opacity: .001;
overflow: hidden;
}
}
&__divider {
float: left;
height: calc(var(--toolbar-height) - (var(--toolbar-divider-margin-top) * 2));
border-left: 1px solid var(--second-color);
margin: var(--toolbar-divider-margin-top) 8px;
}
&__br {
width: 100%;
padding: 0 !important;
height: 0 !important;
}
}
&-menu {
&--current {
color: var(--toolbar-icon-hover-color) !important;
}
&--disabled {
color: var(--second-color) !important;
cursor: not-allowed !important;
}
}
&-emojis {
display: inline-block;
overflow: auto;
&::-webkit-scrollbar {
display: none;
}
&__tip {
flex: 1;
min-width: 1px;
width: 200px;
margin-right: 10px;
color: var(--toolbar-icon-color);
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
&__tail {
margin-top: 5px;
font-size: 12px;
color: var(--toolbar-icon-color);
display: flex;
a {
text-decoration: none;
color: var(--toolbar-icon-color);
&:hover {
color: var(--toolbar-icon-hover-color);
}
}
}
button {
cursor: pointer;
border-radius: 3px;
float: left;
height: 30px;
width: 30px;
text-align: center;
line-height: 26px;
padding: 3px;
box-sizing: border-box;
font-size: 16px;
transition: @transition;
border: 0;
margin: 0;
background-color: transparent;
overflow: hidden;
&:focus {
outline: none;
}
&:hover .vditor-emojis__icon {
display: inline-block;
transform: scale(1.2);
}
}
img {
height: 20px;
width: 20px;
float: left;
margin: 3px 0 0 3px;
}
}
}
@media screen and (max-width: @max-width) {
.vditor-toolbar__item {
padding: 0 12px;
}
.vditor-panel--left.vditor-panel--arrow:before {
right: 17px;
}
}
@media(hover: hover) and (pointer: fine) {
.vditor-toolbar__item .vditor-tooltipped:hover {
color: var(--toolbar-icon-hover-color);
}
}

View File

@@ -0,0 +1,171 @@
/**
* tool tip.
*
* @author <a href="http://vanessa.b3log.org">Liyuan Li</a>
* @version 0.1.0.0, Jan 25, 2019
*/
@keyframes tooltip-appear {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.vditor-tooltipped {
position: relative;
cursor: pointer;
&::after {
position: absolute;
z-index: 1000000;
display: none;
padding: 5px 8px;
font-size: 11px;
font-weight: normal;
-webkit-font-smoothing: subpixel-antialiased;
color: #fff;
text-align: center;
text-decoration: none;
text-shadow: none;
text-transform: none;
letter-spacing: normal;
word-wrap: break-word;
white-space: pre;
pointer-events: none;
content: attr(aria-label);
background: #3b3e43;
border-radius: 3px;
line-height: 16px;
opacity: 0;
}
&::before {
position: absolute;
z-index: 1000001;
display: none;
width: 0;
height: 0;
color: #3b3e43;
pointer-events: none;
content: "";
border: 5px solid transparent;
opacity: 0;
}
&--hover::before,
&--hover::after,
&:hover::before,
&:hover::after,
&:active::before,
&:active::after,
&:focus::before,
&:focus::after {
display: inline-block;
text-decoration: none;
animation-name: tooltip-appear;
animation-duration: 0.15s;
animation-fill-mode: forwards;
animation-timing-function: ease-in;
}
&__s::after,
&__se::after,
&__sw::after {
top: 100%;
right: 50%;
margin-top: 5px;
}
&__s::before,
&__se::before,
&__sw::before {
top: auto;
right: 50%;
bottom: -5px;
margin-right: -5px;
border-bottom-color: #3b3e43;
}
&__se::after {
right: auto;
left: 50%;
margin-left: -15px;
}
&__sw::after {
margin-right: -15px;
}
&__n::after,
&__ne::after,
&__nw::after {
right: 50%;
bottom: 100%;
margin-bottom: 5px;
}
&__n::before,
&__ne::before,
&__nw::before {
top: -5px;
right: 50%;
bottom: auto;
margin-right: -5px;
border-top-color: #3b3e43;
}
&__ne::after {
right: auto;
left: 50%;
margin-left: -15px;
}
&__nw::after {
margin-right: -15px;
}
&__s::after,
&__n::after {
transform: translateX(50%);
}
&__w::after {
right: 100%;
bottom: 50%;
margin-right: 5px;
transform: translateY(50%);
}
&__w::before {
top: 50%;
bottom: 50%;
left: -5px;
margin-top: -5px;
border-left-color: #3b3e43;
}
&__e::after {
bottom: 50%;
left: 100%;
margin-left: 5px;
transform: translateY(50%);
}
&__e::before {
top: 50%;
right: -5px;
bottom: 50%;
margin-top: -5px;
border-right-color: #3b3e43;
}
}
@media screen and (max-width: @max-width) {
.vditor-tooltipped:before,
.vditor-tooltipped:after {
content: none;
}
}

View File

@@ -0,0 +1,195 @@
.vditor-wysiwyg {
box-sizing: border-box;
flex: 1;
position: relative;
width: 100%;
min-width: 1px;
pre.vditor-reset {
background-color: var(--panel-background-color);
margin: 0;
white-space: pre-wrap;
height: 100%;
box-sizing: border-box;
&[contenteditable="false"] {
opacity: 0.3;
cursor: not-allowed;
}
&:empty::before {
content: attr(placeholder);
color: var(--second-color);
}
&:focus {
outline: none;
background-color: var(--textarea-background-color);
}
&:after {
content: "";
height: var(--editor-bottom);
display: block;
}
}
blockquote:empty::before,
pre > code:empty::before,
p:empty::before,
h1:empty::after,
h2:empty::after,
h3:empty::after,
h4:empty::after,
h5:empty::after,
h6:empty::after {
content: ' ';
}
code[data-marker="`"] {
padding-left: 0 !important;
padding-right: 0 !important;
}
&__block {
pre:first-child {
margin-bottom: -1em;
code {
height: auto;
color: var(--textarea-text-color);
height: auto;
text-align: left;
}
}
pre:last-child {
margin-bottom: 1em;
}
}
&__preview {
cursor: pointer;
white-space: initial;
min-height: 27px;
}
& > .vditor-reset > h1:before,
& > .vditor-reset > h2:before,
& > .vditor-reset > h3:before,
& > .vditor-reset > h4:before,
& > .vditor-reset > h5:before,
& > .vditor-reset > h6:before,
div.vditor-wysiwyg__block:before,
div[data-type="link-ref-defs-block"]:before,
div[data-type="footnotes-block"]:before,
.vditor-toc:before {
float: left;
padding-right: 4px;
margin-left: -29px;
content: 'H1';
font-size: 0.85rem;
font-weight: normal;
color: var(--second-color);
}
& > .vditor-reset > h2:before {
content: 'H2';
}
& > .vditor-reset > h3:before {
content: 'H3';
}
& > .vditor-reset > h4:before {
content: 'H4';
}
& > .vditor-reset > h5:before {
content: 'H5';
}
& > .vditor-reset > h6:before {
content: 'H6';
}
div[data-type="link-ref-defs-block"]:before {
content: '"A"';
}
div[data-type="footnotes-block"]:before {
content: '^F';
}
div.vditor-wysiwyg__block:before {
content: "</>";
}
div.vditor-wysiwyg__block[data-type="yaml-front-matter"]:before {
content: "F";
}
div.vditor-wysiwyg__block[data-type="math-block"]:before {
content: "$$";
}
.vditor-toc:before {
content: "ToC";
}
hr {
display: inline-block;
margin: 12px 0;
width: 100%;
}
details {
white-space: initial;
}
a {
cursor: pointer;
}
span[data-type="backslash"] > span {
display: none;
color: var(--second-color);
}
span[data-type="link-ref"],
sup[data-type="footnotes-ref"] {
color: @blurColor;
}
span[data-type="toc-h"] {
color: @blurColor;
text-decoration: underline;
}
div[data-type="footnotes-block"] {
border-top: 2px solid var(--heading-border-color);
padding-top: 24px;
margin-top: 24px;
}
div[data-type="link-ref-defs-block"] {
color: var(--blockquote-color);
}
}
@media screen and (max-width: @max-width) {
.vditor-wysiwyg {
h1:before,
h2:before,
h3:before,
h4:before,
h5:before,
h6:before,
div.vditor-wysiwyg__block:before,
div[data-type="link-ref-defs-block"]:before,
div[data-type="footnotes-block"]:before,
.vditor-toc:before {
content: none;
}
}
}

View File

@@ -0,0 +1,88 @@
@errorColor: #d23f31;
@blurColor: #4285f4;
@font-family-base: "Helvetica Neue", "Luxi Sans", "DejaVu Sans",
"Hiragino Sans GB", "Microsoft Yahei", sans-serif, "Apple Color Emoji",
"Segoe UI Emoji", "Noto Color Emoji", "Segoe UI Symbol", "Android Emoji",
"EmojiSymbols";
@font-family-code: mononoki, Consolas, "Liberation Mono", Menlo, Courier,
monospace, "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji",
"Segoe UI Symbol", "Android Emoji", "EmojiSymbols";
@transition: all 0.15s ease-in-out;
@max-width: 520px;
.vditor {
--border-color: #d1d5da;
--second-color: rgba(88, 96, 105, 0.36);
--panel-background-color: #fff;
--panel-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
--toolbar-background-color: #f6f8fa;
--toolbar-icon-color: #586069;
--toolbar-icon-hover-color: @blurColor;
--toolbar-height: 35px;
--toolbar-divider-margin-top: 8px;
--textarea-background-color: #fafbfc;
--textarea-text-color: #24292e;
--resize-icon-color: var(--toolbar-icon-color);
--resize-background-color: var(--toolbar-background-color);
--resize-hover-icon-color: var(--panel-background-color);
--resize-hover-background-color: var(--toolbar-icon-hover-color);
--count-background-color: rgba(27, 31, 35, 0.05);
--heading-border-color: #eaecef;
--blockquote-color: #6a737d;
--ir-heading-color: #660e7a;
--ir-title-color: #808080;
--ir-bi-color: #0033b3;
--ir-link-color: #008000;
--ir-bracket-color: #0000ff;
--ir-paren-color: #008000;
&--dark {
--border-color: #141414;
--second-color: rgba(185, 185, 185, 0.36);
--panel-background-color: #24292e;
--panel-shadow: 0 1px 2px rgba(255, 255, 255, 0.2);
--toolbar-background-color: #1d2125;
--toolbar-icon-color: #b9b9b9;
--toolbar-icon-hover-color: #fff;
--textarea-background-color: #2f363d;
--textarea-text-color: #d1d5da;
--resize-icon-color: var(--border-color);
--resize-background-color: var(--second-color);
--resize-hover-icon-color: var(--toolbar-icon-hover-color);
--resize-hover-background-color: rgba(185, 185, 185, 0.86);
--count-background-color: rgba(@blurColor, 0.36);
--heading-border-color: var(--textarea-text-color);
--blockquote-color: var(--toolbar-icon-color);
--ir-heading-color: #9876aa;
--ir-title-color: #808080;
--ir-bi-color: #cc7832;
--ir-link-color: #ffc66d;
--ir-bracket-color: #287bde;
--ir-paren-color: #6a8759;
}
}
@import "_tooltipped";
@import "_panel";
@import "_toolbar";
@import "_content";
@import "_hint";
@import "_reset";
@import "_wysiwyg";
@import "_ir";
@import "_sv";

View File

@@ -0,0 +1,553 @@
import "./assets/less/index.less";
import VditorMethod from "./method";
import {Constants, VDITOR_VERSION} from "./ts/constants";
import {DevTools} from "./ts/devtools/index";
import {Hint} from "./ts/hint/index";
import {IR} from "./ts/ir/index";
import {input as irInput} from "./ts/ir/input";
import {processAfterRender} from "./ts/ir/process";
import {getHTML} from "./ts/markdown/getHTML";
import {getMarkdown} from "./ts/markdown/getMarkdown";
import {setLute} from "./ts/markdown/setLute";
import {Outline} from "./ts/outline/index";
import {Preview} from "./ts/preview/index";
import {Resize} from "./ts/resize/index";
import {Editor} from "./ts/sv/index";
import {inputEvent} from "./ts/sv/inputEvent";
import {processAfterRender as processSVAfterRender, processPaste} from "./ts/sv/process";
import {Tip} from "./ts/tip/index";
import {Toolbar} from "./ts/toolbar/index";
import {disableToolbar, hidePanel} from "./ts/toolbar/setToolbar";
import {enableToolbar} from "./ts/toolbar/setToolbar";
import {initUI, UIUnbindListener} from "./ts/ui/initUI";
import {setCodeTheme} from "./ts/ui/setCodeTheme";
import {setContentTheme} from "./ts/ui/setContentTheme";
import {setPreviewMode} from "./ts/ui/setPreviewMode";
import {setTheme} from "./ts/ui/setTheme";
import {Undo} from "./ts/undo/index";
import {Upload} from "./ts/upload/index";
import {addScript, addScriptSync} from "./ts/util/addScript";
import {getSelectText} from "./ts/util/getSelectText";
import {Options} from "./ts/util/Options";
import {processCodeRender} from "./ts/util/processCode";
import {getCursorPosition, getEditorRange, insertHTML} from "./ts/util/selection";
import {afterRenderEvent} from "./ts/wysiwyg/afterRenderEvent";
import {WYSIWYG} from "./ts/wysiwyg/index";
import {input} from "./ts/wysiwyg/input";
import {renderDomByMd} from "./ts/wysiwyg/renderDomByMd";
import {execAfterRender} from "./ts/util/fixBrowserBehavior";
import {accessLocalStorage} from "./ts/util/compatibility";
class Vditor extends VditorMethod {
public readonly version: string;
public vditor: IVditor;
/**
* @param id 要挂载 Vditor 的元素或者元素 ID。
* @param options Vditor 参数
*/
constructor(id: string | HTMLElement, options?: IOptions) {
super();
this.version = VDITOR_VERSION;
if (typeof id === "string") {
if (!options) {
options = {
cache: {
id: `vditor${id}`,
},
};
} else if (!options.cache) {
options.cache = {id: `vditor${id}`};
} else if (!options.cache.id) {
options.cache.id = `vditor${id}`;
}
if (!document.getElementById(id)) {
this.showErrorTip(`Failed to get element by id: ${id}`);
return;
}
id = document.getElementById(id);
}
const getOptions = new Options(options);
const mergedOptions = getOptions.merge();
// 支持自定义国际化
if (!mergedOptions.i18n) {
if (!["en_US", "fr_FR", "pt_BR", "ja_JP", "ko_KR", "ru_RU", "sv_SE", "zh_CN", "zh_TW"].includes(mergedOptions.lang)) {
throw new Error(
"options.lang error, see https://ld246.com/article/1549638745630#options",
);
} else {
const i18nScriptPrefix = "vditorI18nScript";
const i18nScriptID = i18nScriptPrefix + mergedOptions.lang;
document.querySelectorAll(`head script[id^="${i18nScriptPrefix}"]`).forEach((el) => {
if (el.id !== i18nScriptID) {
document.head.removeChild(el);
}
});
addScript(`${mergedOptions.cdn}/dist/js/i18n/${mergedOptions.lang}.js`, i18nScriptID).then(() => {
this.init(id as HTMLElement, mergedOptions);
}).catch(error => {
this.showErrorTip(`GET ${mergedOptions.cdn}/dist/js/i18n/${mergedOptions.lang}.js net::ERR_ABORTED 404 (Not Found)`);
});
}
} else {
window.VditorI18n = mergedOptions.i18n;
this.init(id, mergedOptions);
}
}
private showErrorTip(error: string) {
const tip = new Tip();
document.body.appendChild(tip.element);
tip.show(error, 0)
}
public updateToolbarConfig(options: IToolbarConfig) {
this.vditor.toolbar.updateConfig(this.vditor, options);
}
/** 设置主题 */
public setTheme(
theme: "dark" | "classic",
contentTheme?: string,
codeTheme?: string,
contentThemePath?: string,
) {
this.vditor.options.theme = theme;
setTheme(this.vditor);
if (contentTheme) {
this.vditor.options.preview.theme.current = contentTheme;
setContentTheme(contentTheme, contentThemePath || this.vditor.options.preview.theme.path);
}
if (codeTheme) {
this.vditor.options.preview.hljs.style = codeTheme;
setCodeTheme(codeTheme, this.vditor.options.cdn);
}
}
/** 获取 Markdown 内容 */
public getValue() {
return getMarkdown(this.vditor);
}
/** 获取编辑器当前编辑模式 */
public getCurrentMode() {
return this.vditor.currentMode;
}
/** 聚焦到编辑器 */
public focus() {
if (this.vditor.currentMode === "sv") {
this.vditor.sv.element.focus();
} else if (this.vditor.currentMode === "wysiwyg") {
this.vditor.wysiwyg.element.focus();
} else if (this.vditor.currentMode === "ir") {
this.vditor.ir.element.focus();
}
}
/** 让编辑器失焦 */
public blur() {
if (this.vditor.currentMode === "sv") {
this.vditor.sv.element.blur();
} else if (this.vditor.currentMode === "wysiwyg") {
this.vditor.wysiwyg.element.blur();
} else if (this.vditor.currentMode === "ir") {
this.vditor.ir.element.blur();
}
}
/** 禁用编辑器 */
public disabled() {
hidePanel(this.vditor, ["subToolbar", "hint", "popover"]);
disableToolbar(
this.vditor.toolbar.elements,
Constants.EDIT_TOOLBARS.concat(["undo", "redo", "fullscreen", "edit-mode"]),
);
this.vditor[this.vditor.currentMode].element.setAttribute(
"contenteditable",
"false",
);
}
/** 解除编辑器禁用 */
public enable() {
enableToolbar(
this.vditor.toolbar.elements,
Constants.EDIT_TOOLBARS.concat(["undo", "redo", "fullscreen", "edit-mode"]),
);
this.vditor.undo.resetIcon(this.vditor);
this.vditor[this.vditor.currentMode].element.setAttribute("contenteditable", "true");
}
/** 返回选中的字符串 */
public getSelection() {
if (this.vditor.currentMode === "wysiwyg") {
return getSelectText(this.vditor.wysiwyg.element);
} else if (this.vditor.currentMode === "sv") {
return getSelectText(this.vditor.sv.element);
} else if (this.vditor.currentMode === "ir") {
return getSelectText(this.vditor.ir.element);
}
}
/** 设置预览区域内容 */
public renderPreview(value?: string) {
this.vditor.preview.render(this.vditor, value);
}
/** 获取焦点位置 */
public getCursorPosition() {
return getCursorPosition(this.vditor[this.vditor.currentMode].element);
}
/** 上传是否还在进行中 */
public isUploading() {
return this.vditor.upload.isUploading;
}
/** 清除缓存 */
public clearCache() {
if (this.vditor.options.cache.enable && accessLocalStorage()) {
localStorage.removeItem(this.vditor.options.cache.id);
}
}
/** 禁用缓存 */
public disabledCache() {
this.vditor.options.cache.enable = false;
}
/** 启用缓存 */
public enableCache() {
if (!this.vditor.options.cache.id) {
throw new Error(
"need options.cache.id, see https://ld246.com/article/1549638745630#options",
);
}
this.vditor.options.cache.enable = true;
}
/** HTML 转 md */
public html2md(value: string) {
return this.vditor.lute.HTML2Md(value);
}
/** markdown 转 JSON 输出 */
public exportJSON(value: string) {
return this.vditor.lute.RenderJSON(value);
}
/** 获取 HTML */
public getHTML() {
return getHTML(this.vditor);
}
/** 消息提示。time 为 0 将一直显示 */
public tip(text: string, time?: number) {
this.vditor.tip.show(text, time);
}
/** 设置预览模式 */
public setPreviewMode(mode: "both" | "editor") {
setPreviewMode(mode, this.vditor);
}
/** 删除选中内容 */
public deleteValue() {
if (window.getSelection().isCollapsed) {
return;
}
document.execCommand("delete", false);
}
/** 更新选中内容 */
public updateValue(value: string) {
document.execCommand("insertHTML", false, value);
}
/** 在焦点处插入内容,并默认进行 Markdown 渲染 */
public insertValue(value: string, render = true) {
const range = getEditorRange(this.vditor);
range.collapse(true);
// https://github.com/Vanessa219/vditor/issues/716
// https://github.com/Vanessa219/vditor/issues/917
const tmpElement = document.createElement("template");
tmpElement.innerHTML = value;
range.insertNode(tmpElement.content.cloneNode(true));
range.collapse(false);
if (this.vditor.currentMode === "sv") {
this.vditor.sv.preventInput = true;
if (render) {
inputEvent(this.vditor);
}
} else if (this.vditor.currentMode === "wysiwyg") {
// 由于 https://github.com/Vanessa219/vditor/issues/1566 不能使用 this.vditor.wysiwyg.preventInput = true;
if (render) {
input(this.vditor, getSelection().getRangeAt(0));
}
} else if (this.vditor.currentMode === "ir") {
this.vditor.ir.preventInput = true;
if (render) {
irInput(this.vditor, getSelection().getRangeAt(0), true);
}
}
}
/** 在焦点处插入 Markdown */
public insertMD(md: string) {
// https://github.com/Vanessa219/vditor/issues/1640
if (this.vditor.currentMode === "ir") {
insertHTML(this.vditor.lute.Md2VditorIRDOM(md), this.vditor);
} else if (this.vditor.currentMode === "wysiwyg") {
insertHTML(this.vditor.lute.Md2VditorDOM(md), this.vditor);
} else {
processPaste(this.vditor, md);
}
this.vditor.outline.render(this.vditor);
execAfterRender(this.vditor);
}
/** 设置编辑器内容 */
public setValue(markdown: string, clearStack = false) {
if (this.vditor.currentMode === "sv") {
this.vditor.sv.element.innerHTML = `<div data-block='0'>${this.vditor.lute.SpinVditorSVDOM(markdown)}</div>`;
processSVAfterRender(this.vditor, {
enableAddUndoStack: true,
enableHint: false,
enableInput: false,
});
} else if (this.vditor.currentMode === "wysiwyg") {
renderDomByMd(this.vditor, markdown, {
enableAddUndoStack: true,
enableHint: false,
enableInput: false,
});
} else {
this.vditor.ir.element.innerHTML = this.vditor.lute.Md2VditorIRDOM(markdown);
this.vditor.ir.element
.querySelectorAll(".vditor-ir__preview[data-render='2']")
.forEach((item: HTMLElement) => {
processCodeRender(item, this.vditor);
});
processAfterRender(this.vditor, {
enableAddUndoStack: true,
enableHint: false,
enableInput: false,
});
}
this.vditor.outline.render(this.vditor);
if (!markdown) {
hidePanel(this.vditor, ["emoji", "headings", "submenu", "hint"]);
if (this.vditor.wysiwyg.popover) {
this.vditor.wysiwyg.popover.style.display = "none";
}
this.clearCache();
}
if (clearStack) {
this.clearStack();
}
}
/** 清空 undo & redo 栈 */
public clearStack() {
this.vditor.undo.clearStack(this.vditor);
this.vditor.undo.addToUndoStack(this.vditor);
}
/** 销毁编辑器 */
public destroy() {
this.vditor.element.innerHTML = this.vditor.originalInnerHTML;
this.vditor.element.classList.remove("vditor");
this.vditor.element.removeAttribute("style");
const iconScript = document.getElementById("vditorIconScript")
if (iconScript) {
iconScript.remove();
}
this.clearCache();
UIUnbindListener();
this.vditor.wysiwyg.unbindListener();
}
/** 获取评论 ID */
public getCommentIds() {
if (this.vditor.currentMode !== "wysiwyg") {
return [];
}
return this.vditor.wysiwyg.getComments(this.vditor, true);
}
/** 高亮评论 */
public hlCommentIds(ids: string[]) {
if (this.vditor.currentMode !== "wysiwyg") {
return;
}
const hlItem = (item: Element) => {
item.classList.remove("vditor-comment--hover");
ids.forEach((id) => {
if (item.getAttribute("data-cmtids").indexOf(id) > -1) {
item.classList.add("vditor-comment--hover");
}
});
};
this.vditor.wysiwyg.element
.querySelectorAll(".vditor-comment")
.forEach((item) => {
hlItem(item);
});
if (this.vditor.preview.element.style.display !== "none") {
this.vditor.preview.element
.querySelectorAll(".vditor-comment")
.forEach((item) => {
hlItem(item);
});
}
}
/** 取消评论高亮 */
public unHlCommentIds(ids: string[]) {
if (this.vditor.currentMode !== "wysiwyg") {
return;
}
const unHlItem = (item: Element) => {
ids.forEach((id) => {
if (item.getAttribute("data-cmtids").indexOf(id) > -1) {
item.classList.remove("vditor-comment--hover");
}
});
};
this.vditor.wysiwyg.element
.querySelectorAll(".vditor-comment")
.forEach((item) => {
unHlItem(item);
});
if (this.vditor.preview.element.style.display !== "none") {
this.vditor.preview.element
.querySelectorAll(".vditor-comment")
.forEach((item) => {
unHlItem(item);
});
}
}
/** 删除评论 */
public removeCommentIds(removeIds: string[]) {
if (this.vditor.currentMode !== "wysiwyg") {
return;
}
const removeItem = (item: Element, removeId: string) => {
const ids = item.getAttribute("data-cmtids").split(" ");
ids.find((id, index) => {
if (id === removeId) {
ids.splice(index, 1);
return true;
}
});
if (ids.length === 0) {
item.outerHTML = item.innerHTML;
getEditorRange(this.vditor).collapse(true);
} else {
item.setAttribute("data-cmtids", ids.join(" "));
}
};
removeIds.forEach((removeId) => {
this.vditor.wysiwyg.element
.querySelectorAll(".vditor-comment")
.forEach((item) => {
removeItem(item, removeId);
});
if (this.vditor.preview.element.style.display !== "none") {
this.vditor.preview.element
.querySelectorAll(".vditor-comment")
.forEach((item) => {
removeItem(item, removeId);
});
}
});
afterRenderEvent(this.vditor, {
enableAddUndoStack: true,
enableHint: false,
enableInput: false,
});
}
private init(id: HTMLElement, mergedOptions: IOptions) {
this.vditor = {
currentMode: mergedOptions.mode,
element: id,
hint: new Hint(mergedOptions.hint.extend),
lute: undefined,
options: mergedOptions,
originalInnerHTML: id.innerHTML,
outline: new Outline(window.VditorI18n.outline),
tip: new Tip(),
};
this.vditor.sv = new Editor(this.vditor);
this.vditor.undo = new Undo();
this.vditor.wysiwyg = new WYSIWYG(this.vditor);
this.vditor.ir = new IR(this.vditor);
this.vditor.toolbar = new Toolbar(this.vditor);
if (mergedOptions.resize.enable) {
this.vditor.resize = new Resize(this.vditor);
}
if (this.vditor.toolbar.elements.devtools) {
this.vditor.devtools = new DevTools();
}
if (mergedOptions.upload.url || mergedOptions.upload.handler) {
this.vditor.upload = new Upload();
}
addScript(
mergedOptions._lutePath ||
`${mergedOptions.cdn}/dist/js/lute/lute.min.js`,
"vditorLuteScript",
).then(() => {
this.vditor.lute = setLute({
autoSpace: this.vditor.options.preview.markdown.autoSpace,
gfmAutoLink: this.vditor.options.preview.markdown.gfmAutoLink,
codeBlockPreview: this.vditor.options.preview.markdown
.codeBlockPreview,
emojiSite: this.vditor.options.hint.emojiPath,
emojis: this.vditor.options.hint.emoji,
fixTermTypo: this.vditor.options.preview.markdown.fixTermTypo,
footnotes: this.vditor.options.preview.markdown.footnotes,
headingAnchor: false,
inlineMathDigit: this.vditor.options.preview.math.inlineDigit,
linkBase: this.vditor.options.preview.markdown.linkBase,
linkPrefix: this.vditor.options.preview.markdown.linkPrefix,
listStyle: this.vditor.options.preview.markdown.listStyle,
mark: this.vditor.options.preview.markdown.mark,
mathBlockPreview: this.vditor.options.preview.markdown
.mathBlockPreview,
paragraphBeginningSpace: this.vditor.options.preview.markdown
.paragraphBeginningSpace,
sanitize: this.vditor.options.preview.markdown.sanitize,
toc: this.vditor.options.preview.markdown.toc,
});
this.vditor.preview = new Preview(this.vditor);
initUI(this.vditor);
if (mergedOptions.after) {
mergedOptions.after();
}
if (mergedOptions.icon) {
// 防止初始化 2 个编辑器时加载 2 次
addScriptSync(`${mergedOptions.cdn}/dist/js/icons/${mergedOptions.icon}.js`, "vditorIconScript");
}
});
}
}
export default Vditor;

View File

@@ -0,0 +1,68 @@
import {abcRender} from "./ts/markdown/abcRender";
import * as adapterRender from "./ts/markdown/adapterRender";
import {chartRender} from "./ts/markdown/chartRender";
import {codeRender} from "./ts/markdown/codeRender";
import {flowchartRender} from "./ts/markdown/flowchartRender";
import {graphvizRender} from "./ts/markdown/graphvizRender";
import {highlightRender} from "./ts/markdown/highlightRender";
import {lazyLoadImageRender} from "./ts/markdown/lazyLoadImageRender";
import {mathRender} from "./ts/markdown/mathRender";
import {mediaRender} from "./ts/markdown/mediaRender";
import {mermaidRender} from "./ts/markdown/mermaidRender";
import {markmapRender} from "./ts/markdown/markmapRender";
import {mindmapRender} from "./ts/markdown/mindmapRender";
import {outlineRender} from "./ts/markdown/outlineRender";
import {plantumlRender} from "./ts/markdown/plantumlRender";
import {md2html, previewRender} from "./ts/markdown/previewRender";
import {speechRender} from "./ts/markdown/speechRender";
import {previewImage} from "./ts/preview/image";
import {setCodeTheme} from "./ts/ui/setCodeTheme";
import {setContentTheme} from "./ts/ui/setContentTheme";
class Vditor {
/** 点击图片放大 */
public static adapterRender = adapterRender;
/** 点击图片放大 */
public static previewImage = previewImage;
/** 为 element 中的代码块添加复制按钮 */
public static codeRender = codeRender;
/** 对 graphviz 进行渲染 */
public static graphvizRender = graphvizRender;
/** 为 element 中的代码块进行高亮渲染 */
public static highlightRender = highlightRender;
/** 对数学公式进行渲染 */
public static mathRender = mathRender;
/** 流程图/时序图/甘特图渲染 */
public static mermaidRender = mermaidRender;
/** 支持markdown的思维导图 */
public static markmapRender = markmapRender;
/** flowchart.js 渲染 */
public static flowchartRender = flowchartRender;
/** 图表渲染 */
public static chartRender = chartRender;
/** 五线谱渲染 */
public static abcRender = abcRender;
/** 脑图渲染 */
public static mindmapRender = mindmapRender;
/** plantuml渲染 */
public static plantumlRender = plantumlRender;
/** 大纲渲染 */
public static outlineRender = outlineRender;
/** 为[特定链接](https://github.com/Vanessa219/vditor/issues/7)分别渲染为视频、音频、嵌入的 iframe */
public static mediaRender = mediaRender;
/** 对选中的文字进行阅读 */
public static speechRender = speechRender;
/** 对图片进行懒加载 */
public static lazyLoadImageRender = lazyLoadImageRender;
/** Markdown 文本转换为 HTML该方法需使用[异步编程](https://ld246.com/article/1546828434083?r=Vaness) */
public static md2html = md2html;
/** 页面 Markdown 文章渲染 */
public static preview = previewRender;
/** 设置代码主题 */
public static setCodeTheme = setCodeTheme;
/** 设置内容主题 */
public static setContentTheme = setContentTheme;
}
export default Vditor;

View File

@@ -0,0 +1,69 @@
declare const VDITOR_VERSION: string;
const _VDITOR_VERSION = VDITOR_VERSION;
export {_VDITOR_VERSION as VDITOR_VERSION};
export abstract class Constants {
public static readonly ZWSP: string = "\u200b";
public static readonly DROP_EDITOR: string = "application/editor";
public static readonly MOBILE_WIDTH: number = 520;
public static readonly CLASS_MENU_DISABLED: string = "vditor-menu--disabled";
public static readonly EDIT_TOOLBARS: string[] = ["emoji", "headings", "bold", "italic", "strike", "link", "list",
"ordered-list", "outdent", "indent", "check", "line", "quote", "code", "inline-code", "insert-after",
"insert-before", "upload", "record", "table"];
public static readonly CODE_THEME: string[] = ["abap", "algol", "algol_nu", "arduino", "autumn", "borland", "bw",
"colorful", "dracula", "emacs", "friendly", "fruity", "github", "igor", "lovelace", "manni", "monokai",
"monokailight", "murphy", "native", "paraiso-dark", "paraiso-light", "pastie", "perldoc", "pygments",
"rainbow_dash", "rrt", "solarized-dark", "solarized-dark256", "solarized-light", "swapoff", "tango", "trac",
"vim", "vs", "xcode", "ant-design"];
public static readonly CODE_LANGUAGES: string[] = ["mermaid", "echarts", "mindmap", "plantuml", "abc", "graphviz", "flowchart", "apache",
"js", "ts", "html","markmap",
// common
"properties", "apache", "bash", "c", "csharp", "cpp", "css", "coffeescript", "diff", "go", "xml", "http",
"json", "java", "javascript", "kotlin", "less", "lua", "makefile", "markdown", "nginx", "objectivec", "php",
"php-template", "perl", "plaintext", "python", "python-repl", "r", "ruby", "rust", "scss", "sql", "shell",
"swift", "ini", "typescript", "vbnet", "yaml",
"ada", "clojure", "dart", "erb", "fortran", "gradle", "haskell", "julia", "julia-repl", "lisp", "matlab",
"pgsql", "powershell", "sql_more", "stata", "cmake", "mathematica",
// ext
"solidity", "yul"
];
public static readonly CDN = `https://unpkg.com/vditor@${VDITOR_VERSION}`;
public static readonly MARKDOWN_OPTIONS = {
autoSpace: false,
gfmAutoLink: true,
codeBlockPreview: true,
fixTermTypo: false,
footnotes: true,
linkBase: "",
linkPrefix: "",
listStyle: false,
mark: false,
mathBlockPreview: true,
paragraphBeginningSpace: false,
sanitize: true,
toc: false,
};
public static readonly HLJS_OPTIONS = {
enable: true,
lineNumber: false,
defaultLang: "",
style: "github",
};
public static readonly MATH_OPTIONS: IMath = {
engine: "KaTeX",
inlineDigit: false,
macros: {},
};
public static readonly THEME_OPTIONS = {
current: "light",
list: {
"ant-design": "Ant Design",
"dark": "Dark",
"light": "Light",
"wechat": "WeChat",
},
path: `${Constants.CDN}/dist/css/content-theme`,
};
}

View File

@@ -0,0 +1,83 @@
import {getMarkdown} from "../markdown/getMarkdown";
import {addScript} from "../util/addScript";
declare const echarts: {
init(element: HTMLElement): IEChart;
};
export class DevTools {
public element: HTMLDivElement;
private ASTChart: IEChart;
constructor() {
this.element = document.createElement("div");
this.element.className = "vditor-devtools";
this.element.innerHTML = '<div class="vditor-reset--error"></div><div style="height: 100%;"></div>';
}
public renderEchart(vditor: IVditor) {
if (vditor.devtools.element.style.display !== "block") {
return;
}
addScript(`${vditor.options.cdn}/dist/js/echarts/echarts.min.js?v=5.5.1`, "vditorEchartsScript").then(() => {
if (!this.ASTChart) {
this.ASTChart = echarts.init(vditor.devtools.element.lastElementChild as HTMLDivElement);
}
try {
(this.element.lastElementChild as HTMLElement).style.display = "block";
this.element.firstElementChild.innerHTML = "";
this.ASTChart.setOption({
series: [
{
data: JSON.parse(vditor.lute.RenderEChartsJSON(getMarkdown(vditor))),
initialTreeDepth: -1,
label: {
align: "left",
backgroundColor: "rgba(68, 77, 86, .68)",
borderRadius: 3,
color: "#d1d5da",
fontSize: 12,
lineHeight: 12,
offset: [9, 12],
padding: [2, 4, 2, 4],
position: "top",
verticalAlign: "middle",
},
lineStyle: {
color: "#4285f4",
type: "curve",
width: 1,
},
orient: "vertical",
roam: true,
type: "tree",
},
],
toolbox: {
bottom: 25,
emphasis: {
iconStyle: {
color: "#4285f4",
},
},
feature: {
restore: {
show: true,
},
saveAsImage: {
show: true,
},
},
right: 15,
show: true,
},
});
this.ASTChart.resize();
} catch (e) {
(this.element.lastElementChild as HTMLElement).style.display = "none";
this.element.firstElementChild.innerHTML = e;
}
});
}
}

View File

@@ -0,0 +1,83 @@
import {getHTML} from "../markdown/getHTML";
import {getMarkdown} from "../markdown/getMarkdown";
export const download = (vditor: IVditor, content: string, filename: string) => {
const aElement = document.createElement("a");
if ("download" in aElement) {
aElement.download = filename;
aElement.style.display = "none";
aElement.href = URL.createObjectURL(new Blob([content]));
document.body.appendChild(aElement);
aElement.click();
aElement.remove();
} else {
vditor.tip.show(window.VditorI18n.downloadTip, 0);
}
};
export const exportMarkdown = (vditor: IVditor) => {
const content = getMarkdown(vditor);
download(vditor, content, content.substr(0, 10) + ".md");
};
export const exportPDF = (vditor: IVditor) => {
vditor.tip.show(window.VditorI18n.generate, 3800);
const iframe = document.querySelector("#vditorExportIframe") as HTMLIFrameElement;
iframe.contentDocument.open();
iframe.contentDocument.write(`<link rel="stylesheet" href="${vditor.options.cdn}/dist/index.css"/>
<script src="${vditor.options.cdn}/dist/method.min.js"></script>
<div id="preview" style="width: 800px"></div>
<script>
window.addEventListener("message", (e) => {
if(!e.data) {
return;
}
Vditor.preview(document.getElementById('preview'), e.data, {
cdn: "${vditor.options.cdn}",
markdown: {
theme: ${JSON.stringify(vditor.options.preview.theme)}
},
hljs: {
style: "${vditor.options.preview.hljs.style}"
}
});
setTimeout(() => {
window.print();
}, 3600);
}, false);
</script>`);
iframe.contentDocument.close();
setTimeout(() => {
iframe.contentWindow.postMessage(getMarkdown(vditor), "*");
}, 200);
};
export const exportHTML = (vditor: IVditor) => {
const content = getHTML(vditor);
const html = `<html><head><link rel="stylesheet" type="text/css" href="${vditor.options.cdn}/dist/index.css"/>
<script src="${vditor.options.cdn}/dist/js/i18n/${vditor.options.lang}.js"></script>
<script src="${vditor.options.cdn}/dist/method.min.js"></script></head>
<body><div class="vditor-reset" id="preview">${content}</div>
<script>
const previewElement = document.getElementById('preview')
Vditor.setContentTheme('${vditor.options.preview.theme.current}', '${vditor.options.preview.theme.path}');
Vditor.codeRender(previewElement);
Vditor.highlightRender(${JSON.stringify(vditor.options.preview.hljs)}, previewElement, '${vditor.options.cdn}');
Vditor.mathRender(previewElement, {
cdn: '${vditor.options.cdn}',
math: ${JSON.stringify(vditor.options.preview.math)},
});
Vditor.mermaidRender(previewElement, '${vditor.options.cdn}', '${vditor.options.theme}');
Vditor.markmapRender(previewElement, '${vditor.options.cdn}', '${vditor.options.theme}');
Vditor.flowchartRender(previewElement, '${vditor.options.cdn}');
Vditor.graphvizRender(previewElement, '${vditor.options.cdn}');
Vditor.chartRender(previewElement, '${vditor.options.cdn}', '${vditor.options.theme}');
Vditor.mindmapRender(previewElement, '${vditor.options.cdn}', '${vditor.options.theme}');
Vditor.abcRender(previewElement, '${vditor.options.cdn}');
${vditor.options.preview.render.media.enable ? 'Vditor.mediaRender(previewElement);' : ""}
Vditor.speechRender(previewElement);
</script>
<script src="${vditor.options.cdn}/dist/js/icons/${vditor.options.icon}.js"></script></body></html>`;
download(vditor, html, content.substr(0, 10) + ".html");
};

View File

@@ -0,0 +1,276 @@
import {Constants} from "../constants";
import {processAfterRender} from "../ir/process";
import {code160to32} from "../util/code160to32";
import {isCtrl} from "../util/compatibility";
import {execAfterRender} from "../util/fixBrowserBehavior";
import {hasClosestByAttribute, hasClosestByClassName} from "../util/hasClosest";
import {processCodeRender} from "../util/processCode";
import {getCursorPosition, insertHTML, setSelectionFocus} from "../util/selection";
export class Hint {
public timeId: number;
public element: HTMLDivElement;
public recentLanguage: string;
private splitChar = "";
private lastIndex = -1;
constructor(hintExtends: IHintExtend[]) {
this.timeId = -1;
this.element = document.createElement("div");
this.element.className = "vditor-hint";
this.recentLanguage = "";
hintExtends.push({key: ":"});
}
public render(vditor: IVditor) {
if (!window.getSelection().focusNode) {
return;
}
let currentLineValue: string;
const range = getSelection().getRangeAt(0);
currentLineValue = range.startContainer.textContent.substring(0, range.startOffset) || "";
const key = this.getKey(currentLineValue, vditor.options.hint.extend);
if (typeof key === "undefined") {
this.element.style.display = "none";
clearTimeout(this.timeId);
} else {
if (this.splitChar === ":") {
const emojiHint = key === "" ? vditor.options.hint.emoji : vditor.lute.GetEmojis();
const matchEmojiData: IHintData[] = [];
Object.keys(emojiHint).forEach((keyName) => {
if (keyName.indexOf(key.toLowerCase()) === 0) {
if (emojiHint[keyName].indexOf(".") > -1) {
matchEmojiData.push({
html: `<img src="${emojiHint[keyName]}" title=":${keyName}:"/> :${keyName}:`,
value: `:${keyName}:`,
});
} else {
matchEmojiData.push({
html: `<span class="vditor-hint__emoji">${emojiHint[keyName]}</span>${keyName}`,
value: emojiHint[keyName],
});
}
}
});
this.genHTML(matchEmojiData, key, vditor);
} else {
vditor.options.hint.extend.forEach((item) => {
if (item.key === this.splitChar) {
clearTimeout(this.timeId);
this.timeId = window.setTimeout(async () => {
this.genHTML(await item.hint(key), key, vditor);
}, vditor.options.hint.delay);
}
});
}
}
}
public genHTML(data: IHintData[], key: string, vditor: IVditor) {
if (data.length === 0) {
this.element.style.display = "none";
return;
}
const editorElement = vditor[vditor.currentMode].element;
const textareaPosition = getCursorPosition(editorElement);
const x = textareaPosition.left +
(vditor.options.outline.position === "left" ? vditor.outline.element.offsetWidth : 0);
const y = textareaPosition.top;
let hintsHTML = "";
data.forEach((hintData, i) => {
if (i > 7) {
return;
}
// process high light
let html = hintData.html;
if (key !== "") {
const lastIndex = html.lastIndexOf(">") + 1;
let replaceHtml = html.substr(lastIndex);
const replaceIndex = replaceHtml.toLowerCase().indexOf(key.toLowerCase());
if (replaceIndex > -1) {
replaceHtml = replaceHtml.substring(0, replaceIndex) + "<b>" +
replaceHtml.substring(replaceIndex, replaceIndex + key.length) + "</b>" +
replaceHtml.substring(replaceIndex + key.length);
html = html.substr(0, lastIndex) + replaceHtml;
}
}
hintsHTML += `<button type="button" data-value="${encodeURIComponent(hintData.value)} "
${i === 0 ? "class='vditor-hint--current'" : ""}> ${html}</button>`;
});
this.element.innerHTML = hintsHTML;
const lineHeight = parseInt(document.defaultView.getComputedStyle(editorElement, null)
.getPropertyValue("line-height"), 10);
this.element.style.top = `${y + (lineHeight || 22)}px`;
this.element.style.left = `${x}px`;
this.element.style.display = "block";
this.element.style.right = "auto";
this.element.querySelectorAll("button").forEach((element) => {
element.addEventListener("click", (event) => {
this.fillEmoji(element, vditor);
event.preventDefault();
});
});
// hint 展现在上部
if (this.element.getBoundingClientRect().bottom > window.innerHeight) {
this.element.style.top = `${y - this.element.offsetHeight}px`;
}
if (this.element.getBoundingClientRect().right > window.innerWidth) {
this.element.style.left = "auto";
this.element.style.right = "0";
}
}
public fillEmoji = (element: HTMLElement, vditor: IVditor) => {
this.element.style.display = "none";
const value = decodeURIComponent(element.getAttribute("data-value"));
const range: Range = window.getSelection().getRangeAt(0);
// 代码提示
if (vditor.currentMode === "ir") {
const preBeforeElement = hasClosestByAttribute(range.startContainer, "data-type", "code-block-info");
if (preBeforeElement) {
preBeforeElement.textContent = Constants.ZWSP + value.trimRight();
range.selectNodeContents(preBeforeElement);
range.collapse(false);
processAfterRender(vditor);
preBeforeElement.parentElement.querySelectorAll("code").forEach((item) => {
item.className = "language-" + value.trimRight();
});
processCodeRender(preBeforeElement.parentElement.querySelector(".vditor-ir__preview"), vditor);
this.recentLanguage = value.trimRight();
return;
}
}
if (vditor.currentMode === "wysiwyg" && range.startContainer.nodeType !== 3 ) {
const startContainer = range.startContainer as HTMLElement;
let inputElement: HTMLInputElement;
if (startContainer.classList.contains("vditor-input")) {
inputElement = startContainer as HTMLInputElement;
} else {
inputElement = startContainer.firstElementChild as HTMLInputElement;
}
if (inputElement && inputElement.classList.contains("vditor-input")) {
inputElement.value = value.trimRight();
range.selectNodeContents(inputElement);
range.collapse(false);
// {detail: 1}用于标识这个自定义事件是在编程语言选择后触发的
// 用于在鼠标选择语言后,自动聚焦到代码输入框
inputElement.dispatchEvent(new CustomEvent("input", {detail: 1}));
this.recentLanguage = value.trimRight();
return;
}
}
range.setStart(range.startContainer, this.lastIndex);
range.deleteContents();
if (vditor.options.hint.parse) {
if (vditor.currentMode === "sv") {
insertHTML(vditor.lute.SpinVditorSVDOM(value), vditor);
} else if (vditor.currentMode === "wysiwyg") {
insertHTML(vditor.lute.SpinVditorDOM(value), vditor);
} else {
insertHTML(vditor.lute.SpinVditorIRDOM(value), vditor);
}
} else {
insertHTML(value, vditor);
}
if (this.splitChar === ":" && value.indexOf(":") > -1 && vditor.currentMode !== "sv") {
range.insertNode(document.createTextNode(" "));
}
range.collapse(false);
setSelectionFocus(range);
if (vditor.currentMode === "wysiwyg") {
const preElement = hasClosestByClassName(range.startContainer, "vditor-wysiwyg__block");
if (preElement && preElement.lastElementChild.classList.contains("vditor-wysiwyg__preview")) {
preElement.lastElementChild.innerHTML = preElement.firstElementChild.innerHTML;
processCodeRender(preElement.lastElementChild as HTMLElement, vditor);
}
} else if (vditor.currentMode === "ir") {
const preElement = hasClosestByClassName(range.startContainer, "vditor-ir__marker--pre");
if (preElement && preElement.nextElementSibling.classList.contains("vditor-ir__preview")) {
preElement.nextElementSibling.innerHTML = preElement.innerHTML;
processCodeRender(preElement.nextElementSibling as HTMLElement, vditor);
}
}
execAfterRender(vditor);
}
public select(event: KeyboardEvent, vditor: IVditor) {
if (this.element.querySelectorAll("button").length === 0 ||
this.element.style.display === "none") {
return false;
}
const currentHintElement: HTMLElement = this.element.querySelector(".vditor-hint--current");
if (event.key === "ArrowDown") {
event.preventDefault();
event.stopPropagation();
currentHintElement.removeAttribute("class");
if (!currentHintElement.nextElementSibling) {
this.element.children[0].className = "vditor-hint--current";
} else {
currentHintElement.nextElementSibling.className = "vditor-hint--current";
}
return true;
} else if (event.key === "ArrowUp") {
event.preventDefault();
event.stopPropagation();
currentHintElement.removeAttribute("class");
if (!currentHintElement.previousElementSibling) {
const length = this.element.children.length;
this.element.children[length - 1].className = "vditor-hint--current";
} else {
currentHintElement.previousElementSibling.className = "vditor-hint--current";
}
return true;
} else if (!isCtrl(event) && !event.shiftKey && !event.altKey && event.key === "Enter" && !event.isComposing) {
event.preventDefault();
event.stopPropagation();
this.fillEmoji(currentHintElement, vditor);
return true;
}
return false;
}
private getKey(currentLineValue: string, extend: IHintExtend[]) {
this.lastIndex = -1;
this.splitChar = "";
extend.forEach((item) => {
const currentLastIndex = currentLineValue.lastIndexOf(item.key);
if (this.lastIndex < currentLastIndex) {
this.splitChar = item.key;
this.lastIndex = currentLastIndex;
}
});
let key;
if (this.lastIndex === -1) {
return key;
}
const lineArray = currentLineValue.split(this.splitChar);
const lastItem = lineArray[lineArray.length - 1];
const maxLength = 32;
if (lineArray.length > 1 && lastItem.trim() === lastItem) {
if (lineArray.length === 2 && lineArray[0] === "" && lineArray[1].length < maxLength) {
key = lineArray[1];
} else {
const preChar = lineArray[lineArray.length - 2].slice(-1);
if (code160to32(preChar) === " " && lastItem.length < maxLength) {
key = lastItem;
}
}
}
return key;
}
}

View File

@@ -0,0 +1,79 @@
import {hasClosestByClassName, hasTopClosestByClassName} from "../util/hasClosest";
import {setSelectionFocus} from "../util/selection";
const nextIsNode = (range: Range) => {
const startContainer = range.startContainer;
if (startContainer.nodeType === 3 && startContainer.nodeValue.length !== range.startOffset) {
return false;
}
let nextNode: HTMLElement = startContainer.nextSibling as HTMLElement;
while (nextNode && nextNode.textContent === "") {
nextNode = nextNode.nextSibling as HTMLElement;
}
if (!nextNode) {
// *em*|**string**
const markerElement = hasClosestByClassName(startContainer, "vditor-ir__marker");
if (markerElement && !markerElement.nextSibling) {
const parentNextNode = startContainer.parentElement.parentElement.nextSibling as HTMLElement;
if (parentNextNode && parentNextNode.nodeType !== 3 &&
parentNextNode.classList.contains("vditor-ir__node")) {
return parentNextNode;
}
}
return false;
} else if (nextNode && nextNode.nodeType !== 3 && nextNode.classList.contains("vditor-ir__node") &&
!nextNode.getAttribute("data-block")) {
// test|*em*
return nextNode;
}
return false;
};
const previousIsNode = (range: Range) => {
const startContainer = range.startContainer;
const previousNode = startContainer.previousSibling as HTMLElement;
if (startContainer.nodeType === 3 && range.startOffset === 0 && previousNode && previousNode.nodeType !== 3 &&
// *em*|text
previousNode.classList.contains("vditor-ir__node") && !previousNode.getAttribute("data-block")) {
return previousNode;
}
return false;
};
export const expandMarker = (range: Range, vditor: IVditor) => {
vditor.ir.element.querySelectorAll(".vditor-ir__node--expand").forEach((item) => {
item.classList.remove("vditor-ir__node--expand");
});
const nodeElement = hasTopClosestByClassName(range.startContainer, "vditor-ir__node");
const nodeElementEnd = !range.collapsed && hasTopClosestByClassName(range.endContainer, "vditor-ir__node");
// 选中文本为同一个 nodeElement 内时,需要展开
if (!range.collapsed && (!nodeElement || nodeElement !== nodeElementEnd)) {
return;
}
if (nodeElement) {
nodeElement.classList.add("vditor-ir__node--expand");
nodeElement.classList.remove("vditor-ir__node--hidden");
// https://github.com/Vanessa219/vditor/issues/615 safari中光标位置跳动
setSelectionFocus(range);
}
const nextNode = nextIsNode(range);
if (nextNode) {
nextNode.classList.add("vditor-ir__node--expand");
nextNode.classList.remove("vditor-ir__node--hidden");
return;
}
const previousNode = previousIsNode(range);
if (previousNode) {
previousNode.classList.add("vditor-ir__node--expand");
previousNode.classList.remove("vditor-ir__node--hidden");
return;
}
};

View File

@@ -0,0 +1,97 @@
import {Constants} from "../constants";
import {disableToolbar, enableToolbar, removeCurrentToolbar, setCurrentToolbar} from "../toolbar/setToolbar";
import {hasClosestByAttribute, hasClosestByMatchTag} from "../util/hasClosest";
import {hasClosestByHeadings} from "../util/hasClosestByHeadings";
import {getEditorRange, selectIsEditor} from "../util/selection";
export const highlightToolbarIR = (vditor: IVditor) => {
clearTimeout(vditor[vditor.currentMode].hlToolbarTimeoutId);
vditor[vditor.currentMode].hlToolbarTimeoutId = window.setTimeout(() => {
if (vditor[vditor.currentMode].element.getAttribute("contenteditable") === "false") {
return;
}
if (!selectIsEditor(vditor[vditor.currentMode].element)) {
return;
}
removeCurrentToolbar(vditor.toolbar.elements, Constants.EDIT_TOOLBARS);
enableToolbar(vditor.toolbar.elements, Constants.EDIT_TOOLBARS);
const range = getEditorRange(vditor);
let typeElement = range.startContainer as HTMLElement;
if (range.startContainer.nodeType === 3) {
typeElement = range.startContainer.parentElement;
}
if (typeElement.classList.contains("vditor-reset")) {
typeElement = typeElement.childNodes[range.startOffset] as HTMLElement;
}
const headingElement = vditor.currentMode === "sv" ?
hasClosestByAttribute(typeElement, "data-type", "heading") : hasClosestByHeadings(typeElement);
if (headingElement) {
setCurrentToolbar(vditor.toolbar.elements, ["headings"]);
}
const quoteElement =
vditor.currentMode === "sv" ? hasClosestByAttribute(typeElement, "data-type", "blockquote") :
hasClosestByMatchTag(typeElement, "BLOCKQUOTE");
if (quoteElement) {
setCurrentToolbar(vditor.toolbar.elements, ["quote"]);
}
const strongElement = hasClosestByAttribute(typeElement, "data-type", "strong");
if (strongElement) {
setCurrentToolbar(vditor.toolbar.elements, ["bold"]);
}
const emElement = hasClosestByAttribute(typeElement, "data-type", "em");
if (emElement) {
setCurrentToolbar(vditor.toolbar.elements, ["italic"]);
}
const sElement = hasClosestByAttribute(typeElement, "data-type", "s");
if (sElement) {
setCurrentToolbar(vditor.toolbar.elements, ["strike"]);
}
const aElement = hasClosestByAttribute(typeElement, "data-type", "a");
if (aElement) {
setCurrentToolbar(vditor.toolbar.elements, ["link"]);
}
const liElement = hasClosestByMatchTag(typeElement, "LI");
if (liElement) {
if (liElement.classList.contains("vditor-task")) {
setCurrentToolbar(vditor.toolbar.elements, ["check"]);
} else if (liElement.parentElement.tagName === "OL") {
setCurrentToolbar(vditor.toolbar.elements, ["ordered-list"]);
} else if (liElement.parentElement.tagName === "UL") {
setCurrentToolbar(vditor.toolbar.elements, ["list"]);
}
enableToolbar(vditor.toolbar.elements, ["outdent", "indent"]);
} else {
disableToolbar(vditor.toolbar.elements, ["outdent", "indent"]);
}
const codeBlockElement = hasClosestByAttribute(typeElement, "data-type", "code-block");
if (codeBlockElement) {
disableToolbar(vditor.toolbar.elements, ["headings", "bold", "italic", "strike", "line", "quote",
"list", "ordered-list", "check", "code", "inline-code", "upload", "link", "table", "record"]);
setCurrentToolbar(vditor.toolbar.elements, ["code"]);
}
const codeElement = hasClosestByAttribute(typeElement, "data-type", "code");
if (codeElement) {
disableToolbar(vditor.toolbar.elements, ["headings", "bold", "italic", "strike", "line", "quote",
"list", "ordered-list", "check", "code", "upload", "link", "table", "record"]);
setCurrentToolbar(vditor.toolbar.elements, ["inline-code"]);
}
const tableElement = hasClosestByAttribute(typeElement, "data-type", "table");
if (tableElement) {
disableToolbar(vditor.toolbar.elements, ["headings", "list", "ordered-list", "check", "line",
"quote", "code", "table"]);
}
}, 200);
};

View File

@@ -0,0 +1,265 @@
import {Constants} from "../constants";
import {isCtrl, isFirefox} from "../util/compatibility";
import {
blurEvent,
copyEvent, cutEvent, dblclickEvent,
dropEvent,
focusEvent,
hotkeyEvent,
scrollCenter,
selectEvent,
} from "../util/editorCommonEvent";
import {paste} from "../util/fixBrowserBehavior";
import {hasClosestByAttribute, hasClosestByClassName} from "../util/hasClosest";
import {
getEditorRange, setRangeByWbr,
setSelectionFocus,
} from "../util/selection";
import {clickToc} from "../util/toc";
import {expandMarker} from "./expandMarker";
import {highlightToolbarIR} from "./highlightToolbarIR";
import {input} from "./input";
import {processAfterRender, processHint} from "./process";
import {hidePanel} from "../toolbar/setToolbar";
class IR {
public range: Range;
public element: HTMLPreElement;
public processTimeoutId: number;
public hlToolbarTimeoutId: number;
public composingLock: boolean = false;
public preventInput: boolean;
constructor(vditor: IVditor) {
const divElement = document.createElement("div");
divElement.className = "vditor-ir";
divElement.innerHTML = `<pre class="vditor-reset" placeholder="${vditor.options.placeholder}"
contenteditable="true" spellcheck="false"></pre>`;
this.element = divElement.firstElementChild as HTMLPreElement;
this.bindEvent(vditor);
focusEvent(vditor, this.element);
dblclickEvent(vditor, this.element);
blurEvent(vditor, this.element);
hotkeyEvent(vditor, this.element);
selectEvent(vditor, this.element);
dropEvent(vditor, this.element);
copyEvent(vditor, this.element, this.copy);
cutEvent(vditor, this.element, this.copy);
}
private copy(event: ClipboardEvent, vditor: IVditor) {
const range = getSelection().getRangeAt(0);
if (range.toString() === "") {
return;
}
event.stopPropagation();
event.preventDefault();
const tempElement = document.createElement("div");
tempElement.appendChild(range.cloneContents());
event.clipboardData.setData("text/plain", vditor.lute.VditorIRDOM2Md(tempElement.innerHTML).trim());
event.clipboardData.setData("text/html", "");
}
private bindEvent(vditor: IVditor) {
this.element.addEventListener("paste", (event: ClipboardEvent & { target: HTMLElement }) => {
paste(vditor, event, {
pasteCode: (code: string) => {
document.execCommand("insertHTML", false, code);
},
});
});
this.element.addEventListener("scroll", () => {
hidePanel(vditor, ["hint"]);
});
this.element.addEventListener("compositionstart", (event: InputEvent) => {
this.composingLock = true;
});
this.element.addEventListener("compositionend", (event: InputEvent) => {
if (!isFirefox()) {
input(vditor, getSelection().getRangeAt(0).cloneRange());
}
this.composingLock = false;
});
this.element.addEventListener("input", (event: InputEvent) => {
if (event.inputType === "deleteByDrag" || event.inputType === "insertFromDrop") {
// https://github.com/Vanessa219/vditor/issues/801 编辑器内容拖拽问题
return;
}
if (this.preventInput) {
this.preventInput = false;
processAfterRender(vditor, {
enableAddUndoStack: true,
enableHint: true,
enableInput: true,
});
return;
}
if (this.composingLock || event.data === "" || event.data === "“" || event.data === "《") {
return;
}
input(vditor, getSelection().getRangeAt(0).cloneRange(), false, event);
});
this.element.addEventListener("click", (event: MouseEvent & { target: HTMLInputElement }) => {
if (event.target.tagName === "INPUT") {
if (event.target.checked) {
event.target.setAttribute("checked", "checked");
} else {
event.target.removeAttribute("checked");
}
this.preventInput = true;
processAfterRender(vditor);
return;
}
const range = getEditorRange(vditor);
// 点击后光标落于预览区
let previewElement = hasClosestByClassName(event.target, "vditor-ir__preview");
if (!previewElement) {
previewElement = hasClosestByClassName(
range.startContainer, "vditor-ir__preview");
}
if (previewElement) {
if (previewElement.previousElementSibling.firstElementChild) {
range.selectNodeContents(previewElement.previousElementSibling.firstElementChild);
} else {
// 行内数学公式
range.selectNodeContents(previewElement.previousElementSibling);
}
range.collapse(true);
setSelectionFocus(range);
scrollCenter(vditor);
}
// 点击图片光标选中图片地址
if (event.target.tagName === "IMG") {
const linkElement =
event.target.parentElement.querySelector<HTMLSpanElement>(".vditor-ir__marker--link");
if (linkElement) {
range.selectNode(linkElement);
setSelectionFocus(range);
}
}
// 打开链接
const aElement = hasClosestByAttribute(event.target, "data-type", "a");
if (aElement && (!aElement.classList.contains("vditor-ir__node--expand"))) {
if (vditor.options.link.click) {
vditor.options.link.click(aElement.querySelector(":scope > .vditor-ir__marker--link"));
} else if (vditor.options.link.isOpen) {
window.open(aElement.querySelector(":scope > .vditor-ir__marker--link").textContent);
}
return;
}
if (event.target.isEqualNode(this.element) && this.element.lastElementChild && range.collapsed) {
const lastRect = this.element.lastElementChild.getBoundingClientRect();
if (event.y > lastRect.top + lastRect.height) {
if (this.element.lastElementChild.tagName === "P" &&
this.element.lastElementChild.textContent.trim().replace(Constants.ZWSP, "") === "") {
range.selectNodeContents(this.element.lastElementChild);
range.collapse(false);
} else {
this.element.insertAdjacentHTML("beforeend",
`<p data-block="0">${Constants.ZWSP}<wbr></p>`);
setRangeByWbr(this.element, range);
}
}
}
if (range.toString() === "") {
expandMarker(range, vditor);
} else {
// https://github.com/Vanessa219/vditor/pull/681 当点击选中区域时 eventTarget 与 range 不一致,需延迟等待 range 发生变化
setTimeout(() => {
expandMarker(getEditorRange(vditor), vditor);
});
}
clickToc(event, vditor);
highlightToolbarIR(vditor);
});
this.element.addEventListener("keyup", (event) => {
if (event.isComposing || isCtrl(event)) {
return;
}
if (event.key === "Enter") {
scrollCenter(vditor);
}
highlightToolbarIR(vditor);
if ((event.key === "Backspace" || event.key === "Delete") &&
vditor.ir.element.innerHTML !== "" && vditor.ir.element.childNodes.length === 1 &&
vditor.ir.element.firstElementChild && vditor.ir.element.firstElementChild.tagName === "P"
&& vditor.ir.element.firstElementChild.childElementCount === 0
&& (vditor.ir.element.textContent === "" || vditor.ir.element.textContent === "\n")) {
// 为空时显示 placeholder
vditor.ir.element.innerHTML = "";
return;
}
const range = getEditorRange(vditor);
if (event.key === "Backspace") {
// firefox headings https://github.com/Vanessa219/vditor/issues/211
if (isFirefox() && range.startContainer.textContent === "\n" && range.startOffset === 1) {
range.startContainer.textContent = "";
expandMarker(range, vditor);
}
// 数学公式前是空块,空块前是 table在空块前删除数学公式会多一个 br
this.element.querySelectorAll(".language-math").forEach((item) => {
const brElement = item.querySelector("br");
if (brElement) {
brElement.remove();
}
});
} else if (event.key.indexOf("Arrow") > -1) {
if (event.key === "ArrowLeft" || event.key === "ArrowRight") {
processHint(vditor);
}
expandMarker(range, vditor);
} else if (event.keyCode === 229 && event.code === "" && event.key === "Unidentified") {
// https://github.com/Vanessa219/vditor/issues/508 IR 删除到节点需展开
expandMarker(range, vditor);
}
const previewRenderElement = hasClosestByClassName(range.startContainer, "vditor-ir__preview");
if (previewRenderElement) {
if (event.key === "ArrowUp" || event.key === "ArrowLeft") {
if (previewRenderElement.previousElementSibling.firstElementChild) {
range.selectNodeContents(previewRenderElement.previousElementSibling.firstElementChild);
} else {
// 行内数学公式/html entity
range.selectNodeContents(previewRenderElement.previousElementSibling);
}
range.collapse(false);
event.preventDefault();
return true;
}
if (previewRenderElement.tagName === "SPAN" &&
(event.key === "ArrowDown" || event.key === "ArrowRight")) {
if (previewRenderElement.parentElement.getAttribute("data-type") === "html-entity") {
// html entity
previewRenderElement.parentElement.insertAdjacentText("afterend", Constants.ZWSP);
range.setStart(previewRenderElement.parentElement.nextSibling, 1);
} else {
range.selectNodeContents(previewRenderElement.parentElement.lastElementChild);
}
range.collapse(false);
event.preventDefault();
return true;
}
}
});
}
}
export {IR};

View File

@@ -0,0 +1,246 @@
import {Constants} from "../constants";
import {isHeadingMD, isHrMD} from "../util/fixBrowserBehavior";
import {
getTopList,
hasClosestBlock, hasClosestByAttribute,
hasClosestByClassName,
} from "../util/hasClosest";
import {hasClosestByTag} from "../util/hasClosestByHeadings";
import {log} from "../util/log";
import {processCodeRender} from "../util/processCode";
import {getSelectPosition, setRangeByWbr} from "../util/selection";
import {renderToc} from "../util/toc";
import {processAfterRender} from "./process";
import {getMarkdown} from "../markdown/getMarkdown";
export const input = (vditor: IVditor, range: Range, ignoreSpace = false, event?: InputEvent) => {
let blockElement = hasClosestBlock(range.startContainer);
// 前后可以输入空格
if (blockElement && !ignoreSpace && blockElement.getAttribute("data-type") !== "code-block") {
if ((isHrMD(blockElement.innerHTML) && blockElement.previousElementSibling) ||
isHeadingMD(blockElement.innerHTML)) {
return;
}
// 前后空格处理
const startOffset = getSelectPosition(blockElement, vditor.ir.element, range).start;
// 开始可以输入空格
let startSpace = true;
for (let i = startOffset - 1;
// 软换行后有空格
i > blockElement.textContent.substr(0, startOffset).lastIndexOf("\n");
i--) {
if (blockElement.textContent.charAt(i) !== " " &&
// 多个 tab 前删除不形成代码块 https://github.com/Vanessa219/vditor/issues/162 1
blockElement.textContent.charAt(i) !== "\t") {
startSpace = false;
break;
}
}
if (startOffset === 0) {
startSpace = false;
}
// 结尾可以输入空格
let endSpace = true;
for (let i = startOffset - 1; i < blockElement.textContent.length; i++) {
if (blockElement.textContent.charAt(i) !== " " && blockElement.textContent.charAt(i) !== "\n") {
endSpace = false;
break;
}
}
if (startSpace) {
if (typeof vditor.options.input === "function") {
vditor.options.input(getMarkdown(vditor));
}
return;
}
// https://github.com/Vanessa219/vditor/issues/729
if (endSpace && /^#{1,6} $/.test(blockElement.textContent)) {
endSpace = false;
}
if (endSpace) {
const markerElement = hasClosestByClassName(range.startContainer, "vditor-ir__marker");
if (markerElement) {
// inline marker space https://github.com/Vanessa219/vditor/issues/239
} else {
const previousNode = range.startContainer.previousSibling as HTMLElement;
if (previousNode && previousNode.nodeType !== 3 && previousNode.classList.contains("vditor-ir__node--expand")) {
// FireFox https://github.com/Vanessa219/vditor/issues/239
previousNode.classList.remove("vditor-ir__node--expand");
}
if (typeof vditor.options.input === "function") {
vditor.options.input(getMarkdown(vditor));
}
return;
}
}
}
vditor.ir.element.querySelectorAll(".vditor-ir__node--expand").forEach((item) => {
item.classList.remove("vditor-ir__node--expand");
});
if (!blockElement) {
// 使用顶级块元素,应使用 innerHTML
blockElement = vditor.ir.element;
}
// document.exeComment insertHTML 会插入 wbr
if (!blockElement.querySelector("wbr")) {
const previewRenderElement = hasClosestByClassName(range.startContainer, "vditor-ir__preview");
if (previewRenderElement) {
previewRenderElement.previousElementSibling.insertAdjacentHTML("beforeend", "<wbr>");
} else {
range.insertNode(document.createElement("wbr"));
}
}
// 清除浏览器自带的样式
blockElement.querySelectorAll("[style]").forEach((item) => {
item.removeAttribute("style");
});
if (blockElement.getAttribute("data-type") === "link-ref-defs-block") {
// 修改链接引用
blockElement = vditor.ir.element;
}
const isIRElement = blockElement.isEqualNode(vditor.ir.element);
const footnoteElement = hasClosestByAttribute(blockElement, "data-type", "footnotes-block");
let html = "";
if (!isIRElement) {
const blockquoteElement = hasClosestByTag(range.startContainer, "BLOCKQUOTE");
// 列表需要到最顶层
const topListElement = getTopList(range.startContainer);
if (topListElement) {
blockElement = topListElement;
}
// 应到引用层,否则 > --- 会解析为 front-matter列表中有 blockquote 则解析 blockquoteblockquote 中有列表则解析列表
if (blockquoteElement && (!topListElement || (topListElement && !blockquoteElement.contains(topListElement)))) {
blockElement = blockquoteElement;
}
// 修改脚注
if (footnoteElement) {
blockElement = footnoteElement;
}
html = blockElement.outerHTML;
if (blockElement.tagName === "UL" || blockElement.tagName === "OL") {
// 如果为列表的话,需要把上下的列表都重绘
const listPrevElement = blockElement.previousElementSibling;
const listNextElement = blockElement.nextElementSibling;
if (listPrevElement && (listPrevElement.tagName === "UL" || listPrevElement.tagName === "OL")) {
html = listPrevElement.outerHTML + html;
listPrevElement.remove();
}
if (listNextElement && (listNextElement.tagName === "UL" || listNextElement.tagName === "OL")) {
html = html + listNextElement.outerHTML;
listNextElement.remove();
}
// firefox 列表回车不会产生新的 list item https://github.com/Vanessa219/vditor/issues/194
html = html.replace("<div><wbr><br></div>", "<li><p><wbr><br></p></li>");
} else if (blockElement.previousElementSibling &&
blockElement.previousElementSibling.textContent.replace(Constants.ZWSP, "") !== "" &&
event && event.inputType === "insertParagraph") {
// 换行时需要处理上一段落
html = blockElement.previousElementSibling.outerHTML + html;
blockElement.previousElementSibling.remove();
}
if (!blockElement.innerText.startsWith("```")) {
// 添加链接引用
vditor.ir.element.querySelectorAll("[data-type='link-ref-defs-block']").forEach((item) => {
if (item && !(blockElement as HTMLElement).isEqualNode(item)) {
html += item.outerHTML;
item.remove();
}
});
// 添加脚注
vditor.ir.element.querySelectorAll("[data-type='footnotes-block']").forEach((item) => {
if (item && !(blockElement as HTMLElement).isEqualNode(item)) {
html += item.outerHTML;
item.remove();
}
});
}
} else {
html = blockElement.innerHTML;
}
log("SpinVditorIRDOM", html, "argument", vditor.options.debugger);
html = vditor.lute.SpinVditorIRDOM(html);
log("SpinVditorIRDOM", html, "result", vditor.options.debugger);
if (isIRElement) {
blockElement.innerHTML = html;
} else {
blockElement.outerHTML = html;
// 更新正文中的 tip
if (footnoteElement) {
const footnoteItemElement = hasClosestByAttribute(vditor.ir.element.querySelector("wbr"),
"data-type", "footnotes-def");
if (footnoteItemElement) {
const footnoteItemText = footnoteItemElement.textContent;
const marker = footnoteItemText.substring(1, footnoteItemText.indexOf("]:"));
const footnoteRefElement = vditor.ir.element.querySelector(`sup[data-type="footnotes-ref"][data-footnotes-label="${marker}"]`);
if (footnoteRefElement) {
footnoteRefElement.setAttribute("aria-label",
footnoteItemText.substr(marker.length + 3).trim().substr(0, 24));
}
}
}
}
// linkref 合并及添加
let firstLinkRefDefElement: HTMLElement;
const allLinkRefDefsElement = vditor.ir.element.querySelectorAll("[data-type='link-ref-defs-block']");
allLinkRefDefsElement.forEach((item: HTMLElement, index) => {
if (index === 0) {
firstLinkRefDefElement = item;
} else {
firstLinkRefDefElement.insertAdjacentHTML("beforeend", item.innerHTML);
item.remove();
}
});
if (allLinkRefDefsElement.length > 0) {
vditor.ir.element.insertAdjacentElement("beforeend", allLinkRefDefsElement[0]);
}
// 脚注合并后添加的末尾
let firstFootnoteElement: HTMLElement;
const allFootnoteElement = vditor.ir.element.querySelectorAll("[data-type='footnotes-block']");
allFootnoteElement.forEach((item: HTMLElement, index) => {
if (index === 0) {
firstFootnoteElement = item;
} else {
firstFootnoteElement.insertAdjacentHTML("beforeend", item.innerHTML);
item.remove();
}
});
if (allFootnoteElement.length > 0) {
vditor.ir.element.insertAdjacentElement("beforeend", allFootnoteElement[0]);
}
setRangeByWbr(vditor.ir.element, range);
vditor.ir.element.querySelectorAll(".vditor-ir__preview[data-render='2']").forEach((item: HTMLElement) => {
processCodeRender(item, vditor);
});
renderToc(vditor);
processAfterRender(vditor, {
enableAddUndoStack: true,
enableHint: true,
enableInput: true,
});
};

View File

@@ -0,0 +1,222 @@
import {Constants} from "../constants";
import {getMarkdown} from "../markdown/getMarkdown";
import {removeCurrentToolbar} from "../toolbar/setToolbar";
import {accessLocalStorage} from "../util/compatibility";
import {listToggle} from "../util/fixBrowserBehavior";
import {hasClosestBlock, hasClosestByAttribute, hasClosestByClassName, hasClosestByMatchTag} from "../util/hasClosest";
import {getEditorRange, getSelectPosition, setRangeByWbr, setSelectionFocus} from "../util/selection";
import {highlightToolbarIR} from "./highlightToolbarIR";
import {input} from "./input";
export const processHint = (vditor: IVditor) => {
vditor.hint.render(vditor);
const startContainer = getEditorRange(vditor).startContainer;
// 代码块语言提示
const preBeforeElement = hasClosestByAttribute(startContainer, "data-type", "code-block-info");
if (preBeforeElement) {
if (preBeforeElement.textContent.replace(Constants.ZWSP, "") === "" && vditor.hint.recentLanguage) {
preBeforeElement.textContent = Constants.ZWSP + vditor.hint.recentLanguage;
const range = getEditorRange(vditor);
range.selectNodeContents(preBeforeElement);
} else {
const matchLangData: IHintData[] = [];
const key =
preBeforeElement.textContent.substring(0, getSelectPosition(preBeforeElement, vditor.ir.element).start)
.replace(Constants.ZWSP, "");
(vditor.options.preview.hljs.langs || Constants.CODE_LANGUAGES).forEach((keyName) => {
if (keyName.indexOf(key.toLowerCase()) > -1) {
matchLangData.push({
html: keyName,
value: keyName,
});
}
});
vditor.hint.genHTML(matchLangData, key, vditor);
}
}
};
export const processAfterRender = (vditor: IVditor, options = {
enableAddUndoStack: true,
enableHint: false,
enableInput: true,
}) => {
if (options.enableHint) {
processHint(vditor);
}
clearTimeout(vditor.ir.processTimeoutId);
vditor.ir.processTimeoutId = window.setTimeout(() => {
if (vditor.ir.composingLock) {
return;
}
const text = getMarkdown(vditor);
if (typeof vditor.options.input === "function" && options.enableInput) {
vditor.options.input(text);
}
if (vditor.options.counter.enable) {
vditor.counter.render(vditor, text);
}
if (vditor.options.cache.enable && accessLocalStorage()) {
localStorage.setItem(vditor.options.cache.id, text);
if (vditor.options.cache.after) {
vditor.options.cache.after(text);
}
}
if (vditor.devtools) {
vditor.devtools.renderEchart(vditor);
}
if (options.enableAddUndoStack) {
vditor.undo.addToUndoStack(vditor);
}
}, vditor.options.undoDelay);
};
export const processHeading = (vditor: IVditor, value: string) => {
const range = getEditorRange(vditor);
const headingElement = hasClosestBlock(range.startContainer) || range.startContainer as HTMLElement;
if (headingElement) {
const headingMarkerElement = headingElement.querySelector(".vditor-ir__marker--heading");
if (headingMarkerElement) {
headingMarkerElement.innerHTML = value;
} else {
headingElement.insertAdjacentText("afterbegin", value);
range.selectNodeContents(headingElement);
range.collapse(false);
}
input(vditor, range.cloneRange());
highlightToolbarIR(vditor);
}
};
const removeInline = (range: Range, vditor: IVditor, type: string) => {
const inlineElement = hasClosestByAttribute(range.startContainer, "data-type", type) as HTMLElement;
if (inlineElement) {
inlineElement.firstElementChild.remove();
inlineElement.lastElementChild.remove();
range.insertNode(document.createElement("wbr"));
const tempElement = document.createElement("div");
tempElement.innerHTML = vditor.lute.SpinVditorIRDOM(inlineElement.outerHTML);
inlineElement.outerHTML = tempElement.firstElementChild.innerHTML.trim();
}
};
export const processToolbar = (vditor: IVditor, actionBtn: Element, prefix: string, suffix: string) => {
const range = getEditorRange(vditor);
const commandName = actionBtn.getAttribute("data-type");
let typeElement = range.startContainer as HTMLElement;
if (typeElement.nodeType === 3) {
typeElement = typeElement.parentElement;
}
let useHighlight = true;
// 移除
if (actionBtn.classList.contains("vditor-menu--current")) {
if (commandName === "quote") {
const quoteElement = hasClosestByMatchTag(typeElement, "BLOCKQUOTE");
if (quoteElement) {
range.insertNode(document.createElement("wbr"));
quoteElement.outerHTML = quoteElement.innerHTML.trim() === "" ?
`<p data-block="0">${quoteElement.innerHTML}</p>` : quoteElement.innerHTML;
}
} else if (commandName === "link") {
const aElement = hasClosestByAttribute(range.startContainer, "data-type", "a") as HTMLElement;
if (aElement) {
const aTextElement = hasClosestByClassName(range.startContainer, "vditor-ir__link");
if (aTextElement) {
range.insertNode(document.createElement("wbr"));
aElement.outerHTML = aTextElement.innerHTML;
} else {
aElement.outerHTML = aElement.querySelector(".vditor-ir__link").innerHTML + "<wbr>";
}
}
} else if (commandName === "italic") {
removeInline(range, vditor, "em");
} else if (commandName === "bold") {
removeInline(range, vditor, "strong");
} else if (commandName === "strike") {
removeInline(range, vditor, "s");
} else if (commandName === "inline-code") {
removeInline(range, vditor, "code");
} else if (commandName === "check" || commandName === "list" || commandName === "ordered-list") {
listToggle(vditor, range, commandName);
useHighlight = false;
actionBtn.classList.remove("vditor-menu--current");
}
} else {
// 添加
if (vditor.ir.element.childNodes.length === 0) {
vditor.ir.element.innerHTML = '<p data-block="0"><wbr></p>';
setRangeByWbr(vditor.ir.element, range);
}
const blockElement = hasClosestBlock(range.startContainer);
if (commandName === "line") {
if (blockElement) {
const hrHTML = '<hr data-block="0"><p data-block="0"><wbr>\n</p>';
if (blockElement.innerHTML.trim() === "") {
blockElement.outerHTML = hrHTML;
} else {
blockElement.insertAdjacentHTML("afterend", hrHTML);
}
}
} else if (commandName === "quote") {
if (blockElement) {
range.insertNode(document.createElement("wbr"));
blockElement.outerHTML = `<blockquote data-block="0">${blockElement.outerHTML}</blockquote>`;
useHighlight = false;
actionBtn.classList.add("vditor-menu--current");
}
} else if (commandName === "link") {
let html;
if (range.toString() === "") {
html = `${prefix}<wbr>${suffix}`;
} else {
html = `${prefix}${range.toString()}${suffix.replace(")", "<wbr>)")}`;
}
document.execCommand("insertHTML", false, html);
useHighlight = false;
actionBtn.classList.add("vditor-menu--current");
} else if (commandName === "italic" || commandName === "bold" || commandName === "strike"
|| commandName === "inline-code" || commandName === "code" || commandName === "table") {
let html;
if (range.toString() === "") {
html = `${prefix}<wbr>${suffix}`;
} else {
if (commandName === "code") {
html = `${prefix}\n${range.toString()}<wbr>${suffix}`;
} else if (commandName === "table") {
html = `${prefix}${range.toString()}<wbr>${suffix}`;
} else {
html = `${prefix}${range.toString()}${suffix}<wbr>`;
}
range.deleteContents();
}
if (commandName === "table" || commandName === "code") {
html = "\n" + html + "\n\n";
}
const spanElement = document.createElement("span");
spanElement.innerHTML = html;
range.insertNode(spanElement);
input(vditor, range);
if (commandName === "table") {
range.selectNodeContents(getSelection().getRangeAt(0).startContainer.parentElement);
setSelectionFocus(range);
}
} else if (commandName === "check" || commandName === "list" || commandName === "ordered-list") {
listToggle(vditor, range, commandName, false);
useHighlight = false;
removeCurrentToolbar(vditor.toolbar.elements, ["check", "list", "ordered-list"]);
actionBtn.classList.add("vditor-menu--current");
}
}
setRangeByWbr(vditor.ir.element, range);
processAfterRender(vditor);
if (useHighlight) {
highlightToolbarIR(vditor);
}
};

View File

@@ -0,0 +1,238 @@
import {Constants} from "../constants";
import {hidePanel} from "../toolbar/setToolbar";
import {isCtrl} from "../util/compatibility";
import {
fixBlockquote, fixCJKPosition,
fixCodeBlock, fixCursorDownInlineMath,
fixDelete, fixFirefoxArrowUpTable, fixGSKeyBackspace, fixHR,
fixList,
fixMarkdown,
fixTab,
fixTable,
fixTask,
insertAfterBlock, insertBeforeBlock, isFirstCell, isLastCell,
} from "../util/fixBrowserBehavior";
import {
hasClosestBlock,
hasClosestByAttribute,
hasClosestByClassName,
hasClosestByMatchTag,
} from "../util/hasClosest";
import {hasClosestByHeadings} from "../util/hasClosestByHeadings";
import {matchHotKey} from "../util/hotKey";
import {getEditorRange, getSelectPosition, setSelectionFocus} from "../util/selection";
import {keydownToc} from "../util/toc";
import {expandMarker} from "./expandMarker";
import {processAfterRender, processHeading} from "./process";
export const processKeydown = (vditor: IVditor, event: KeyboardEvent) => {
vditor.ir.composingLock = event.isComposing;
if (event.isComposing) {
return false;
}
// 添加第一次记录 undo 的光标
if (event.key.indexOf("Arrow") === -1 && event.key !== "Meta" && event.key !== "Control" && event.key !== "Alt" &&
event.key !== "Shift" && event.key !== "CapsLock" && event.key !== "Escape" && !/^F\d{1,2}$/.test(event.key)) {
vditor.undo.recordFirstPosition(vditor, event);
}
const range = getEditorRange(vditor);
const startContainer = range.startContainer;
if (!fixGSKeyBackspace(event, vditor, startContainer)) {
return false;
}
fixCJKPosition(range, vditor, event);
fixHR(range);
// 仅处理以下快捷键操作
if (event.key !== "Enter" && event.key !== "Tab" && event.key !== "Backspace" && event.key.indexOf("Arrow") === -1
&& !isCtrl(event) && event.key !== "Escape" && event.key !== "Delete") {
return false;
}
// 斜体、粗体、内联代码块中换行
const newlineElement = hasClosestByAttribute(startContainer, "data-newline", "1");
if (!isCtrl(event) && !event.altKey && !event.shiftKey && event.key === "Enter" && newlineElement
&& range.startOffset < newlineElement.textContent.length) {
const beforeMarkerElement = newlineElement.previousElementSibling;
if (beforeMarkerElement) {
range.insertNode(document.createTextNode(beforeMarkerElement.textContent));
range.collapse(false);
}
const afterMarkerElement = newlineElement.nextSibling;
if (afterMarkerElement) {
range.insertNode(document.createTextNode(afterMarkerElement.textContent));
range.collapse(true);
}
}
const pElement = hasClosestByMatchTag(startContainer, "P");
// md 处理
if (fixMarkdown(event, vditor, pElement, range)) {
return true;
}
// li
if (fixList(range, vditor, pElement, event)) {
return true;
}
// blockquote
if (fixBlockquote(vditor, range, event, pElement)) {
return true;
}
// 代码块
const preRenderElement = hasClosestByClassName(startContainer, "vditor-ir__marker--pre");
if (preRenderElement && preRenderElement.tagName === "PRE") {
const codeRenderElement = preRenderElement.firstChild as HTMLElement;
if (fixCodeBlock(vditor, event, preRenderElement, range)) {
return true;
}
// 数学公式上无元素,按上或左将添加新块
if ((codeRenderElement.getAttribute("data-type") === "math-block"
|| codeRenderElement.getAttribute("data-type") === "html-block") &&
insertBeforeBlock(vditor, event, range, codeRenderElement, preRenderElement.parentElement)) {
return true;
}
// 代码块下无元素或者为代码块/table 元素,添加空块
if (insertAfterBlock(vditor, event, range, codeRenderElement, preRenderElement.parentElement)) {
return true;
}
}
// 代码块语言
const preBeforeElement = hasClosestByAttribute(startContainer, "data-type", "code-block-info");
if (preBeforeElement) {
if (event.key === "Enter" || event.key === "Tab") {
range.selectNodeContents(preBeforeElement.nextElementSibling.firstChild);
range.collapse(true);
event.preventDefault();
hidePanel(vditor, ["hint"]);
return true;
}
if (event.key === "Backspace") {
const start = getSelectPosition(preBeforeElement, vditor.ir.element).start;
if (start === 1) { // 删除零宽空格
range.setStart(startContainer, 0);
}
if (start === 2) { // 删除时清空自动补全语言
vditor.hint.recentLanguage = "";
}
}
if (insertBeforeBlock(vditor, event, range, preBeforeElement, preBeforeElement.parentElement)) {
// 上无元素,按上或左将添加新块
hidePanel(vditor, ["hint"]);
return true;
}
}
// table
const cellElement = hasClosestByMatchTag(startContainer, "TD") ||
hasClosestByMatchTag(startContainer, "TH");
if (event.key.indexOf("Arrow") > -1 && cellElement) {
const tableElement = isFirstCell(cellElement);
if (tableElement && insertBeforeBlock(vditor, event, range, cellElement, tableElement)) {
return true;
}
const table2Element = isLastCell(cellElement);
if (table2Element && insertAfterBlock(vditor, event, range, cellElement, table2Element)) {
return true;
}
}
if (fixTable(vditor, event, range)) {
return true;
}
// task list
if (fixTask(vditor, range, event)) {
return true;
}
// tab
if (fixTab(vditor, range, event)) {
return true;
}
const headingElement = hasClosestByHeadings(startContainer);
if (headingElement) {
// enter++: 标题变大
if (matchHotKey("⌘=", event)) {
const headingMarkerElement = headingElement.querySelector(".vditor-ir__marker--heading");
if (headingMarkerElement && headingMarkerElement.textContent.trim().length > 1) {
processHeading(vditor, headingMarkerElement.textContent.substr(1));
}
event.preventDefault();
return true;
}
// enter++: 标题变小
if (matchHotKey("⌘-", event)) {
const headingMarkerElement = headingElement.querySelector(".vditor-ir__marker--heading");
if (headingMarkerElement && headingMarkerElement.textContent.trim().length < 6) {
processHeading(vditor, headingMarkerElement.textContent.trim() + "# ");
}
event.preventDefault();
return true;
}
}
const blockElement = hasClosestBlock(startContainer);
if (event.key === "Backspace" && !isCtrl(event) && !event.shiftKey && !event.altKey && range.toString() === "") {
if (fixDelete(vditor, range, event, pElement)) {
return true;
}
if (blockElement && blockElement.previousElementSibling
&& blockElement.tagName !== "UL" && blockElement.tagName !== "OL"
&& (blockElement.previousElementSibling.getAttribute("data-type") === "code-block" ||
blockElement.previousElementSibling.getAttribute("data-type") === "math-block")) {
const rangeStart = getSelectPosition(blockElement, vditor.ir.element, range).start;
if (rangeStart === 0 || (rangeStart === 1 && blockElement.innerText.startsWith(Constants.ZWSP))) {
// 当前块删除后光标落于代码渲染块上,当前块会被删除,因此需要阻止事件,不能和 keyup 中的代码块处理合并
range.selectNodeContents(blockElement.previousElementSibling.querySelector(".vditor-ir__marker--pre code"));
range.collapse(false);
expandMarker(range, vditor);
if (blockElement.textContent.trim().replace(Constants.ZWSP, "") === "") {
// 当前块为空且不是最后一个时,需要删除
blockElement.remove();
processAfterRender(vditor);
}
event.preventDefault();
return true;
}
}
// 光标位于标题前marker 后
if (headingElement) {
const headingLength = headingElement.firstElementChild.textContent.length;
if (getSelectPosition(headingElement, vditor.ir.element).start === headingLength && headingLength !== 0) {
range.setStart(headingElement.firstElementChild.firstChild, headingLength - 1);
range.collapse(true);
setSelectionFocus(range);
}
}
}
if ((event.key === "ArrowUp" || event.key === "ArrowDown") && blockElement) {
// https://github.com/Vanessa219/vditor/issues/358
blockElement.querySelectorAll(".vditor-ir__node").forEach((item: HTMLElement) => {
if (!item.contains(startContainer)) {
item.classList.add("vditor-ir__node--hidden");
}
});
if (fixFirefoxArrowUpTable(event, blockElement, range)) {
return true;
}
}
fixCursorDownInlineMath(range, event.key);
if (blockElement && keydownToc(blockElement, vditor, event, range)) {
event.preventDefault();
return true;
}
return false;
};

View File

@@ -0,0 +1,27 @@
import {Constants} from "../constants";
import {addScript} from "../util/addScript";
import {abcRenderAdapter} from "./adapterRender";
declare const ABCJS: {
renderAbc(element: HTMLElement, text: string): void;
};
export const abcRender = (element: (HTMLElement | Document) = document, cdn = Constants.CDN) => {
const abcElements = abcRenderAdapter.getElements(element);
if (abcElements.length > 0) {
addScript(`${cdn}/dist/js/abcjs/abcjs_basic.min.js`, "vditorAbcjsScript").then(() => {
abcElements.forEach((item: HTMLDivElement) => {
if (item.parentElement.classList.contains("vditor-wysiwyg__pre") ||
item.parentElement.classList.contains("vditor-ir__marker--pre")) {
return;
}
if (item.getAttribute("data-processed") === "true") {
return;
}
ABCJS.renderAbc(item, abcRenderAdapter.getCode(item).trim());
item.style.overflowX = "auto";
item.setAttribute("data-processed", "true");
});
});
}
};

View File

@@ -0,0 +1,37 @@
export const mathRenderAdapter = {
getCode: (mathElement: Element) => mathElement.textContent,
getElements: (element: HTMLElement) => element.querySelectorAll(".language-math"),
};
export const mermaidRenderAdapter = {
/** 不仅要返回code并且需要将 code 设置为 el 的 innerHTML */
getCode: (el: Element) => el.textContent,
getElements: (element: HTMLElement) => element.querySelectorAll(".language-mermaid"),
};
export const markmapRenderAdapter = {
getCode: (el: Element) => el.textContent,
getElements: (element: HTMLElement) => element.querySelectorAll(".language-markmap"),
};
export const mindmapRenderAdapter = {
getCode: (el: Element) => el.getAttribute("data-code"),
getElements: (el: HTMLElement | Document) => el.querySelectorAll(".language-mindmap"),
};
export const chartRenderAdapter = {
getCode: (el: HTMLElement) => el.innerText,
getElements: (el: HTMLElement | Document) => el.querySelectorAll(".language-echarts"),
};
export const abcRenderAdapter = {
getCode: (el: Element) => el.textContent,
getElements: (el: HTMLElement | Document) => el.querySelectorAll(".language-abc"),
};
export const graphvizRenderAdapter = {
getCode: (el: Element) => el.textContent,
getElements: (el: HTMLElement | Document) => el.querySelectorAll(".language-graphviz"),
};
export const flowchartRenderAdapter = {
getCode: (el: Element) => el.textContent,
getElements: (el: HTMLElement | Document) => el.querySelectorAll(".language-flowchart"),
};
export const plantumlRenderAdapter = {
getCode: (el: Element) => el.textContent,
getElements: (el: HTMLElement | Document) => el.querySelectorAll(".language-plantuml"),
};

View File

@@ -0,0 +1,19 @@
export const anchorRender = (type: number) => {
document.querySelectorAll(".vditor-anchor").forEach((anchor: HTMLLinkElement) => {
if (type === 1) {
anchor.classList.add("vditor-anchor--left");
}
anchor.onclick = () => {
const id = anchor.getAttribute("href").substr(1);
const top = document.getElementById("vditorAnchor-" + id).offsetTop;
document.querySelector("html").scrollTop = top;
};
});
window.onhashchange = () => {
const element = document.getElementById("vditorAnchor-" + decodeURIComponent(window.location.hash.substr(1)));
if (element) {
document.querySelector("html").scrollTop = element.offsetTop;
}
};
};

View File

@@ -0,0 +1,38 @@
import {Constants} from "../constants";
import {addScript} from "../util/addScript";
import {chartRenderAdapter} from "./adapterRender";
import {looseJsonParse} from "../util/function";
declare const echarts: {
init(element: HTMLElement, theme?: string): IEChart;
};
export const chartRender = (element: (HTMLElement | Document) = document, cdn = Constants.CDN, theme: string) => {
const echartsElements = chartRenderAdapter.getElements(element);
if (echartsElements.length > 0) {
addScript(`${cdn}/dist/js/echarts/echarts.min.js?v=5.5.1`, "vditorEchartsScript").then(() => {
echartsElements.forEach(async (e: HTMLDivElement) => {
if (e.parentElement.classList.contains("vditor-wysiwyg__pre") ||
e.parentElement.classList.contains("vditor-ir__marker--pre")) {
return;
}
const text = chartRenderAdapter.getCode(e).trim();
if (!text) {
return;
}
try {
if (e.getAttribute("data-processed") === "true") {
return;
}
const option = await looseJsonParse(text);
echarts.init(e, theme === "dark" ? "dark" : undefined).setOption(option);
e.setAttribute("data-processed", "true");
} catch (error) {
e.className = "vditor-reset--error";
e.innerHTML = `echarts render error: <br>${error}`;
}
});
});
}
};

View File

@@ -0,0 +1,60 @@
import {code160to32} from "../util/code160to32";
import {Constants} from "../constants";
export const codeRender = (element: HTMLElement, option?: IHljs) => {
Array.from<HTMLElement>(element.querySelectorAll("pre > code")).filter((e, index) => {
if (e.parentElement.classList.contains("vditor-wysiwyg__pre") ||
e.parentElement.classList.contains("vditor-ir__marker--pre")) {
return false;
}
if (e.classList.contains("language-mermaid") || e.classList.contains("language-flowchart") ||
e.classList.contains("language-echarts") || e.classList.contains("language-mindmap") ||
e.classList.contains("language-plantuml") || e.classList.contains("language-markmap") ||
e.classList.contains("language-abc") || e.classList.contains("language-graphviz") ||
e.classList.contains("language-math")) {
return false;
}
if (e.style.maxHeight.indexOf("px") > -1) {
return false;
}
// 避免预览区在渲染后由于代码块过多产生性能问题 https://github.com/b3log/vditor/issues/67
if (element.classList.contains("vditor-preview") && index > 5) {
return false;
}
return true;
}).forEach((e) => {
let codeText = e.innerText;
if (e.classList.contains("highlight-chroma")) {
const codeElement = e.cloneNode(true) as HTMLElement;
codeElement.querySelectorAll(".highlight-ln").forEach((item: HTMLElement) => {
item.remove();
});
codeText = codeElement.innerText;
} else if (codeText.endsWith("\n")) {
codeText = codeText.substr(0, codeText.length - 1)
}
let iconHTML = '<svg><use xlink:href="#vditor-icon-copy"></use></svg>';
if (!document.getElementById("vditorIconScript")) {
iconHTML = '<svg viewBox="0 0 32 32"><path d="M22.545-0h-17.455c-1.6 0-2.909 1.309-2.909 2.909v20.364h2.909v-20.364h17.455v-2.909zM26.909 5.818h-16c-1.6 0-2.909 1.309-2.909 2.909v20.364c0 1.6 1.309 2.909 2.909 2.909h16c1.6 0 2.909-1.309 2.909-2.909v-20.364c0-1.6-1.309-2.909-2.909-2.909zM26.909 29.091h-16v-20.364h16v20.364z"></path></svg>';
}
const divElement = document.createElement("div");
divElement.className = "vditor-copy";
divElement.innerHTML = `<span aria-label="${window.VditorI18n?.copy || "复制"}"
onmouseover="this.setAttribute('aria-label', '${window.VditorI18n?.copy || "复制"}')"
class="vditor-tooltipped vditor-tooltipped__w"
onclick="this.previousElementSibling.select();document.execCommand('copy');this.setAttribute('aria-label', '${window.VditorI18n?.copied || "已复制"}');this.previousElementSibling.blur()">${iconHTML}</span>`;
const textarea = document.createElement("textarea");
textarea.value = code160to32(codeText);
divElement.insertAdjacentElement("afterbegin", textarea);
if (option && option.renderMenu) {
option.renderMenu(e, divElement);
}
e.before(divElement);
e.style.maxHeight = (window.outerHeight - 40) + "px";
// https://github.com/Vanessa219/vditor/issues/1356
e.insertAdjacentHTML("afterend", `<span style="position: absolute">${Constants.ZWSP}</span>`)
});
};

View File

@@ -0,0 +1,25 @@
import {Constants} from "../constants";
import {addScript} from "../util/addScript";
import {flowchartRenderAdapter} from "./adapterRender";
declare const flowchart: {
parse(text: string): { drawSVG: (type: HTMLElement) => void };
};
export const flowchartRender = (element: HTMLElement, cdn = Constants.CDN) => {
const flowchartElements = flowchartRenderAdapter.getElements(element);
if (flowchartElements.length === 0) {
return;
}
addScript(`${cdn}/dist/js/flowchart.js/flowchart.min.js`, "vditorFlowchartScript").then(() => {
flowchartElements.forEach((item: HTMLElement) => {
if (item.getAttribute("data-processed") === "true") {
return;
}
const flowchartObj = flowchart.parse(flowchartRenderAdapter.getCode(item));
item.innerHTML = "";
flowchartObj.drawSVG(item);
item.setAttribute("data-processed", "true");
});
});
};

View File

@@ -0,0 +1,11 @@
import {getMarkdown} from "./getMarkdown";
export const getHTML = (vditor: IVditor) => {
if (vditor.currentMode === "sv") {
return vditor.lute.Md2HTML(getMarkdown(vditor));
} else if (vditor.currentMode === "wysiwyg") {
return vditor.lute.VditorDOM2HTML(vditor.wysiwyg.element.innerHTML);
} else if (vditor.currentMode === "ir") {
return vditor.lute.VditorIRDOM2HTML(vditor.ir.element.innerHTML);
}
};

View File

@@ -0,0 +1,12 @@
import {code160to32} from "../util/code160to32";
export const getMarkdown = (vditor: IVditor) => {
if (vditor.currentMode === "sv") {
return code160to32(`${vditor.sv.element.textContent}\n`.replace(/\n\n$/, "\n"));
} else if (vditor.currentMode === "wysiwyg") {
return vditor.lute.VditorDOM2Md(vditor.wysiwyg.element.innerHTML);
} else if (vditor.currentMode === "ir") {
return vditor.lute.VditorIRDOM2Md(vditor.ir.element.innerHTML);
}
return "";
};

View File

@@ -0,0 +1,49 @@
import {Constants} from "../constants";
import {addScript} from "../util/addScript";
import {graphvizRenderAdapter} from "./adapterRender";
declare class Viz {
public renderSVGElement: (code: string) => Promise<any>;
constructor({ }: { worker: Worker });
}
export const graphvizRender = (element: HTMLElement, cdn = Constants.CDN) => {
const graphvizElements = graphvizRenderAdapter.getElements(element);
if (graphvizElements.length === 0) {
return;
}
addScript(`${cdn}/dist/js/graphviz/viz.js`, "vditorGraphVizScript").then(() => {
graphvizElements.forEach((e: HTMLDivElement) => {
const code = graphvizRenderAdapter.getCode(e);
if (e.parentElement.classList.contains("vditor-wysiwyg__pre") ||
e.parentElement.classList.contains("vditor-ir__marker--pre")) {
return;
}
if (e.getAttribute("data-processed") === "true" || code.trim() === "") {
return;
}
try {
const blob = new Blob([`importScripts('${(document.getElementById("vditorGraphVizScript") as HTMLScriptElement).src.replace("viz.js", "full.render.js")}');`],
{ type: "application/javascript" });
const url = window.URL || window.webkitURL;
const blobUrl = url.createObjectURL(blob);
const worker = new Worker(blobUrl);
new Viz({ worker })
.renderSVGElement(code).then((result: HTMLElement) => {
e.innerHTML = result.outerHTML;
}).catch((error) => {
e.innerHTML = `graphviz render error: <br>${error}`;
e.className = "vditor-reset--error";
});
} catch (e) {
console.error("graphviz error", e);
}
e.setAttribute("data-processed", "true");
});
});
};

View File

@@ -0,0 +1,90 @@
import {Constants} from "../constants";
import {addScript} from "../util/addScript";
import {addStyle} from "../util/addStyle";
declare const hljs: {
highlightElement(element: Element): void;
};
export const highlightRender = (hljsOption?: IHljs, element: HTMLElement | Document = document,
cdn = Constants.CDN) => {
let style = hljsOption.style;
if (!Constants.CODE_THEME.includes(style)) {
style = "github";
}
const vditorHljsStyle = document.getElementById("vditorHljsStyle") as HTMLLinkElement;
const href = `${cdn}/dist/js/highlight.js/styles/${style}.css`;
if (vditorHljsStyle && vditorHljsStyle.getAttribute('href') !== href) {
vditorHljsStyle.remove();
}
addStyle(`${cdn}/dist/js/highlight.js/styles/${style}.css`, "vditorHljsStyle");
if (hljsOption.enable === false) {
return;
}
const codes = element.querySelectorAll("pre > code");
if (codes.length === 0) {
return;
}
addScript(`${cdn}/dist/js/highlight.js/highlight.pack.js`, "vditorHljsScript").then(() => {
addScript(`${cdn}/dist/js/highlight.js/solidity.min.js`, "vditorHljsSolidityScript").then(() => {
addScript(`${cdn}/dist/js/highlight.js/yul.min.js`, "vditorHljsYulScript").then(() => {
element.querySelectorAll("pre > code").forEach((block) => {
// ir & wysiwyg 区域不渲染
if (block.parentElement.classList.contains("vditor-ir__marker--pre") ||
block.parentElement.classList.contains("vditor-wysiwyg__pre")) {
return;
}
if (block.classList.contains("language-mermaid") || block.classList.contains("language-flowchart") ||
block.classList.contains("language-echarts") || block.classList.contains("language-mindmap") ||
block.classList.contains("language-plantuml") ||
block.classList.contains("language-abc") || block.classList.contains("language-graphviz") ||
block.classList.contains("language-math")) {
return;
}
if (hljsOption.defaultLang !== "" && block.className.indexOf("language-") === -1) {
block.classList.add("language-" + hljsOption.defaultLang)
}
hljs.highlightElement(block);
if (!hljsOption.lineNumber) {
return;
}
block.classList.add("vditor-linenumber");
let linenNumberTemp: HTMLDivElement = block.querySelector(".vditor-linenumber__temp");
if (!linenNumberTemp) {
linenNumberTemp = document.createElement("div");
linenNumberTemp.className = "vditor-linenumber__temp";
block.insertAdjacentElement("beforeend", linenNumberTemp);
}
const whiteSpace = getComputedStyle(block).whiteSpace;
let isSoftWrap = false;
if (whiteSpace === "pre-wrap" || whiteSpace === "pre-line") {
isSoftWrap = true;
}
let lineNumberHTML = "";
const lineList = block.textContent.split(/\r\n|\r|\n/g);
lineList.pop();
lineList.map((line) => {
let lineHeight = "";
if (isSoftWrap) {
linenNumberTemp.textContent = line || "\n";
lineHeight = ` style="height:${linenNumberTemp.getBoundingClientRect().height}px"`;
}
lineNumberHTML += `<span${lineHeight}></span>`;
});
linenNumberTemp.style.display = "none";
lineNumberHTML = `<span class="vditor-linenumber__rows">${lineNumberHTML}</span>`;
block.insertAdjacentHTML("beforeend", lineNumberHTML);
});
})
})
});
};

View File

@@ -0,0 +1,56 @@
declare global {
interface Window {
vditorImageIntersectionObserver: IntersectionObserver;
}
}
export const lazyLoadImageRender = (element: (HTMLElement | Document) = document) => {
const loadImg = (it: HTMLImageElement) => {
const testImage = document.createElement("img");
testImage.src = it.getAttribute("data-src");
testImage.addEventListener("load", () => {
if (!it.getAttribute("style") && !it.getAttribute("class") &&
!it.getAttribute("width") && !it.getAttribute("height")) {
if (testImage.naturalHeight > testImage.naturalWidth &&
testImage.naturalWidth / testImage.naturalHeight <
document.querySelector(".vditor-reset").clientWidth / (window.innerHeight - 40) &&
testImage.naturalHeight > (window.innerHeight - 40)) {
it.style.height = (window.innerHeight - 40) + "px";
}
}
it.src = testImage.src;
});
it.removeAttribute("data-src");
};
if (!("IntersectionObserver" in window)) {
element.querySelectorAll("img").forEach((imgElement: HTMLImageElement) => {
if (imgElement.getAttribute("data-src")) {
loadImg(imgElement);
}
});
return false;
}
if (window.vditorImageIntersectionObserver) {
window.vditorImageIntersectionObserver.disconnect();
element.querySelectorAll("img").forEach((imgElement) => {
window.vditorImageIntersectionObserver.observe(imgElement);
});
} else {
window.vditorImageIntersectionObserver = new IntersectionObserver((entries) => {
entries.forEach((entrie: IntersectionObserverEntry & { target: HTMLImageElement }) => {
if ((typeof entrie.isIntersecting === "undefined"
? entrie.intersectionRatio !== 0
: entrie.isIntersecting)
&& entrie.target.getAttribute("data-src")) {
loadImg(entrie.target);
}
});
});
element.querySelectorAll("img").forEach((imgElement) => {
window.vditorImageIntersectionObserver.observe(imgElement);
});
}
};

View File

@@ -0,0 +1,57 @@
import {Constants} from "../constants";
import {addScript} from "../util/addScript";
import {markmapRenderAdapter} from "./adapterRender";
declare const window: any;
const enabled: Record<string, boolean> = {};
const transform = (transformer: any,content: string)=>{
const result = transformer.transform(content);
const keys = Object.keys(result.features).filter((key) => !enabled[key]);
keys.forEach((key) => {
enabled[key] = true;
});
const { styles, scripts } = transformer.getAssets(keys);
const { markmap } = window;
if (styles) markmap.loadCSS(styles);
if (scripts) markmap.loadJS(scripts);
return result;
}
const init = (el: HTMLElement,code: string) => {
const { Transformer, Markmap, deriveOptions , globalCSS} = window.markmap;
const transformer = new Transformer();
el.innerHTML = '<svg style="width:100%"></svg>';
const svg = el.firstChild as SVGElement;
const mm = Markmap.create(svg, null);
const { root, frontmatter } = transform(transformer, code);
const markmapOptions = frontmatter?.markmap;
const frontmatterOptions = deriveOptions(markmapOptions);
mm.setData(root, frontmatterOptions);
mm.fit();
}
export const markmapRender = (element: HTMLElement, cdn = Constants.CDN, theme: string) => {
const markmapElements = markmapRenderAdapter.getElements(element);
if (markmapElements.length === 0) {
return;
}
addScript(`${cdn}/dist/js/markmap/markmap.min.js`, "vditorMermaidScript").then(() => {
markmapElements.forEach((item) => {
const code = markmapRenderAdapter.getCode(item);
if (item.getAttribute("data-processed") === "true" || code.trim() === "") {
return;
}
const render = document.createElement("div")
render.className = "language-markmap"
item.parentNode.appendChild(render)
init(render,code)
if(item.parentNode.childNodes[0].nodeName == "CODE"){
item.parentNode.removeChild(item.parentNode.childNodes[0])
}
});
});
};

View File

@@ -0,0 +1,148 @@
import {Constants} from "../constants";
import {addScript, addScriptSync} from "../util/addScript";
import {addStyle} from "../util/addStyle";
import {code160to32} from "../util/code160to32";
import {mathRenderAdapter} from "./adapterRender";
declare const katex: {
renderToString(math: string, option: {
displayMode: boolean;
output: string;
macros: object;
}): string;
};
declare global {
interface Window {
MathJax: any;
}
}
export const mathRender = (element: HTMLElement, options?: { cdn?: string, math?: IMath }) => {
const mathElements = mathRenderAdapter.getElements(element);
if (mathElements.length === 0) {
return;
}
const defaultOptions = {
cdn: Constants.CDN,
math: {
engine: "KaTeX",
inlineDigit: false,
macros: {},
},
};
if (options && options.math) {
options.math =
Object.assign({}, defaultOptions.math, options.math);
}
options = Object.assign({}, defaultOptions, options);
if (options.math.engine === "KaTeX") {
addStyle(`${options.cdn}/dist/js/katex/katex.min.css?v=0.16.9`, "vditorKatexStyle");
addScript(`${options.cdn}/dist/js/katex/katex.min.js?v=0.16.9`, "vditorKatexScript").then(() => {
addScript(`${options.cdn}/dist/js/katex/mhchem.min.js?v=0.16.9`, "vditorKatexChemScript").then(() => {
mathElements.forEach((mathElement) => {
if (mathElement.parentElement.classList.contains("vditor-wysiwyg__pre") ||
mathElement.parentElement.classList.contains("vditor-ir__marker--pre")) {
return;
}
if (mathElement.getAttribute("data-math")) {
return;
}
const math = code160to32(mathRenderAdapter.getCode(mathElement));
mathElement.setAttribute("data-math", math);
try {
mathElement.innerHTML = katex.renderToString(math, {
displayMode: mathElement.tagName === "DIV",
output: "html",
macros: options.math.macros,
});
} catch (e) {
mathElement.innerHTML = e.message;
mathElement.className = "language-math vditor-reset--error";
}
mathElement.addEventListener("copy", (event: ClipboardEvent) => {
event.stopPropagation();
event.preventDefault();
const vditorMathElement = (event.currentTarget as HTMLElement).closest(".language-math");
event.clipboardData.setData("text/html", vditorMathElement.innerHTML);
event.clipboardData.setData("text/plain",
vditorMathElement.getAttribute("data-math"));
});
});
});
});
} else if (options.math.engine === "MathJax") {
const chainAsync = (fns: any) => {
if (fns.length === 0) {
return;
}
let curr = 0;
const last = fns[fns.length - 1];
const next = () => {
const fn = fns[curr++];
fn === last ? fn() : fn(next);
};
next();
};
if (!window.MathJax) {
window.MathJax = {
loader: {
paths: {mathjax: `${options.cdn}/dist/js/mathjax`},
},
startup: {
typeset: false,
},
tex: {
macros: options.math.macros,
},
};
// https://github.com/Vanessa219/vditor/issues/1453
Object.assign(window.MathJax, options.math.mathJaxOptions);
}
// 循环加载会抛异常
addScriptSync(`${options.cdn}/dist/js/mathjax/tex-svg-full.js`, "protyleMathJaxScript");
const renderMath = (mathElement: Element, next?: () => void) => {
const math = code160to32(mathElement.textContent).trim();
const mathOptions = window.MathJax.getMetricsFor(mathElement);
mathOptions.display = mathElement.tagName === "DIV";
window.MathJax.tex2svgPromise(math, mathOptions).then((node: Element) => {
mathElement.innerHTML = "";
mathElement.setAttribute("data-math", math);
mathElement.append(node);
window.MathJax.startup.document.clear();
window.MathJax.startup.document.updateDocument();
const errorTextElement = node.querySelector('[data-mml-node="merror"]');
if (errorTextElement && errorTextElement.textContent.trim() !== "") {
mathElement.innerHTML = errorTextElement.textContent.trim();
mathElement.className = "vditor-reset--error";
}
if (next) {
next();
}
});
};
window.MathJax.startup.promise.then(() => {
const chains: any[] = [];
for (let i = 0; i < mathElements.length; i++) {
const mathElement = mathElements[i];
if (!mathElement.parentElement.classList.contains("vditor-wysiwyg__pre") &&
!mathElement.parentElement.classList.contains("vditor-ir__marker--pre") &&
!mathElement.getAttribute("data-math") && code160to32(mathElement.textContent).trim()) {
chains.push((next: () => void) => {
if (i === mathElements.length - 1) {
renderMath(mathElement);
} else {
renderMath(mathElement, next);
}
});
}
}
chainAsync(chains);
});
}
};

View File

@@ -0,0 +1,105 @@
import {getSearch} from "../util/function";
const videoRender = (element: HTMLElement, url: string) => {
element.insertAdjacentHTML("afterend", `<video controls="controls" src="${url}"></video>`);
element.remove();
};
const audioRender = (element: HTMLElement, url: string) => {
element.insertAdjacentHTML("afterend", `<audio controls="controls" src="${url}"></audio>`);
element.remove();
};
const iframeRender = (element: HTMLElement, url: string) => {
const youtubeMatch = url.match(/\/\/(?:www\.)?(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))([\w|-]{11})(?:(?:[\?&]t=)(\S+))?/);
const youkuMatch = url.match(/\/\/v\.youku\.com\/v_show\/id_(\w+)=*\.html/);
const qqMatch = url.match(/\/\/v\.qq\.com\/x\/cover\/.*\/([^\/]+)\.html\??.*/);
const coubMatch = url.match(/(?:www\.|\/\/)coub\.com\/view\/(\w+)/);
const facebookMatch = url.match(/(?:www\.|\/\/)facebook\.com\/([^\/]+)\/videos\/([0-9]+)/);
const dailymotionMatch = url.match(/.+dailymotion.com\/(video|hub)\/(\w+)\?/);
const bilibiliMatch = url.match(/(?:www\.|\/\/)bilibili\.com\/video\/(\w+)/);
const tedMatch = url.match(/(?:www\.|\/\/)ted\.com\/talks\/(\w+)/);
if (youtubeMatch && youtubeMatch[1].length === 11) {
element.insertAdjacentHTML("afterend",
`<iframe class="iframe__video" src="//www.youtube.com/embed/${youtubeMatch[1] +
(youtubeMatch[2] ? "?start=" + youtubeMatch[2] : "")}"></iframe>`);
element.remove();
} else if (youkuMatch && youkuMatch[1]) {
element.insertAdjacentHTML("afterend",
`<iframe class="iframe__video" src="//player.youku.com/embed/${youkuMatch[1]}"></iframe>`);
element.remove();
} else if (qqMatch && qqMatch[1]) {
element.insertAdjacentHTML("afterend",
`<iframe class="iframe__video" src="https://v.qq.com/txp/iframe/player.html?vid=${qqMatch[1]}"></iframe>`);
element.remove();
} else if (coubMatch && coubMatch[1]) {
element.insertAdjacentHTML("afterend",
`<iframe class="iframe__video"
src="//coub.com/embed/${coubMatch[1]}?muted=false&autostart=false&originalSize=true&startWithHD=true"></iframe>`);
element.remove();
} else if (facebookMatch && facebookMatch[0]) {
element.insertAdjacentHTML("afterend",
`<iframe class="iframe__video"
src="https://www.facebook.com/plugins/video.php?href=${encodeURIComponent(facebookMatch[0])}"></iframe>`);
element.remove();
} else if (dailymotionMatch && dailymotionMatch[2]) {
element.insertAdjacentHTML("afterend",
`<iframe class="iframe__video"
src="https://www.dailymotion.com/embed/video/${dailymotionMatch[2]}"></iframe>`);
element.remove();
} else if (url.indexOf("bilibili.com") > -1 && (url.indexOf("bvid=") > -1 || (bilibiliMatch && bilibiliMatch[1]))) {
const params: IObject = {
bvid: getSearch("bvid", url) || (bilibiliMatch && bilibiliMatch[1]),
page: "1",
high_quality: "1",
as_wide: "1",
allowfullscreen: "true",
autoplay: "0"
};
new URL(url.startsWith("http") ? url : "https:" + url).search.split("&").forEach((item, index) => {
if (!item) {
return;
}
if (index === 0) {
item = item.substr(1);
}
const keyValue = item.split("=");
params[keyValue[0]] = keyValue[1];
});
let src = "https://player.bilibili.com/player.html?";
const keys = Object.keys(params);
keys.forEach((key, index) => {
src += `${key}=${params[key]}`;
if (index < keys.length - 1) {
src += "&";
}
});
element.insertAdjacentHTML("afterend",
`<iframe class="iframe__video" src="${src}"></iframe>`);
element.remove();
} else if (tedMatch && tedMatch[1]) {
element.insertAdjacentHTML("afterend",
`<iframe class="iframe__video" src="//embed.ted.com/talks/${tedMatch[1]}"></iframe>`);
element.remove();
}
};
export const mediaRender = (element: HTMLElement) => {
if (!element) {
return;
}
element.querySelectorAll("a").forEach((aElement) => {
const url = aElement.getAttribute("href");
if (!url) {
return;
}
if (url.match(/^.+.(mp4|m4v|ogg|ogv|webm)$/)) {
videoRender(aElement, url);
} else if (url.match(/^.+.(mp3|wav|flac)$/)) {
audioRender(aElement, url);
} else {
iframeRender(aElement, url);
}
});
};

View File

@@ -0,0 +1,60 @@
import {Constants} from "../constants";
import {addScript} from "../util/addScript";
import {mermaidRenderAdapter} from "./adapterRender";
import {genUUID} from "../util/function";
declare const mermaid: {
initialize(options: any): void,
render(id: string, text: string): { svg: string }
};
export const mermaidRender = (element: HTMLElement, cdn = Constants.CDN, theme: string) => {
const mermaidElements = mermaidRenderAdapter.getElements(element);
if (mermaidElements.length === 0) {
return;
}
addScript(`${cdn}/dist/js/mermaid/mermaid.min.js`, "vditorMermaidScript").then(() => {
const config: any = {
securityLevel: "loose", // 升级后无 https://github.com/siyuan-note/siyuan/issues/3587可使用该选项
altFontFamily: "sans-serif",
fontFamily: "sans-serif",
startOnLoad: false,
flowchart: {
htmlLabels: true,
useMaxWidth: !0
},
sequence: {
useMaxWidth: true,
diagramMarginX: 8,
diagramMarginY: 8,
boxMargin: 8,
showSequenceNumbers: true // Mermaid 时序图增加序号 https://github.com/siyuan-note/siyuan/pull/6992 https://mermaid.js.org/syntax/sequenceDiagram.html#sequencenumbers
},
gantt: {
leftPadding: 75,
rightPadding: 20
}
};
if (theme === "dark") {
config.theme = "dark";
}
mermaid.initialize(config);
mermaidElements.forEach(async (item) => {
const code = mermaidRenderAdapter.getCode(item);
if (item.getAttribute("data-processed") === "true" || code.trim() === "") {
return;
}
const id = "mermaid"+genUUID()
try {
const mermaidData = await mermaid.render(id, item.textContent);
item.innerHTML = mermaidData.svg;
} catch (e) {
const errorElement = document.querySelector("#" + id);
item.innerHTML = `${errorElement.outerHTML}<br>
<div style="text-align: left"><small>${e.message.replace(/\n/, "<br>")}</small></div>`;
errorElement.parentElement.remove();
}
item.setAttribute("data-processed", "true");
});
});
};

View File

@@ -0,0 +1,74 @@
import {Constants} from "../constants";
import {addScript} from "../util/addScript";
import {mindmapRenderAdapter} from "./adapterRender";
declare const echarts: {
init(element: HTMLElement, theme?: string): IEChart;
};
export const mindmapRender = (element: (HTMLElement | Document) = document, cdn = Constants.CDN, theme: string) => {
const mindmapElements = mindmapRenderAdapter.getElements(element);
if (mindmapElements.length > 0) {
addScript(`${cdn}/dist/js/echarts/echarts.min.js?v=5.5.1`, "vditorEchartsScript").then(() => {
mindmapElements.forEach((e: HTMLDivElement) => {
if (e.parentElement.classList.contains("vditor-wysiwyg__pre") ||
e.parentElement.classList.contains("vditor-ir__marker--pre")) {
return;
}
const text = mindmapRenderAdapter.getCode(e);
if (!text) {
return;
}
try {
if (e.getAttribute("data-processed") === "true") {
return;
}
echarts.init(e, theme === "dark" ? "dark" : undefined).setOption({
series: [
{
data: [JSON.parse(decodeURIComponent(text))],
initialTreeDepth: -1,
itemStyle: {
borderWidth: 0,
color: "#4285f4",
},
label: {
backgroundColor: "#f6f8fa",
borderColor: "#d1d5da",
borderRadius: 5,
borderWidth: 0.5,
color: "#586069",
lineHeight: 20,
offset: [-5, 0],
padding: [0, 5],
position: "insideRight",
},
lineStyle: {
color: "#d1d5da",
width: 1,
},
roam: true,
symbol: (value: number, params: { data?: { children?: object } }) => {
if (params?.data?.children) {
return "circle";
} else {
return "path://";
}
},
type: "tree",
},
],
tooltip: {
trigger: "item",
triggerOn: "mousemove",
},
});
e.setAttribute("data-processed", "true");
} catch (error) {
e.className = "vditor-reset--error";
e.innerHTML = `mindmap render error: <br>${error}`;
}
});
});
}
};

View File

@@ -0,0 +1,110 @@
import {hasClosestByHeadings} from "../util/hasClosestByHeadings";
import {mathRender} from "./mathRender";
export const outlineRender = (contentElement: HTMLElement, targetElement: Element, vditor?: IVditor) => {
let tocHTML = "";
const ids: string[] = [];
Array.from(contentElement.children).forEach((item: HTMLElement, index: number) => {
if (hasClosestByHeadings(item)) {
if (vditor) {
const lastIndex = item.id.lastIndexOf("_");
item.id = item.id.substring(0, lastIndex === -1 ? undefined : lastIndex) + "_" + index;
}
ids.push(item.id);
tocHTML += item.outerHTML.replace("<wbr>", "");
}
});
if (tocHTML === "") {
targetElement.innerHTML = "";
return "";
}
const tempElement = document.createElement("div");
if (vditor) {
vditor.lute.SetToC(true);
if (vditor.currentMode === "wysiwyg" && !vditor.preview.element.contains(contentElement)) {
tempElement.innerHTML = vditor.lute.SpinVditorDOM("<p>[ToC]</p>" + tocHTML);
} else if (vditor.currentMode === "ir" && !vditor.preview.element.contains(contentElement)) {
tempElement.innerHTML = vditor.lute.SpinVditorIRDOM("<p>[ToC]</p>" + tocHTML);
} else {
tempElement.innerHTML = vditor.lute.HTML2VditorDOM("<p>[ToC]</p>" + tocHTML);
}
vditor.lute.SetToC(vditor.options.preview.markdown.toc);
} else {
targetElement.classList.add("vditor-outline");
const lute = Lute.New();
lute.SetToC(true);
tempElement.innerHTML = lute.HTML2VditorDOM("<p>[ToC]</p>" + tocHTML);
}
const headingsElement = tempElement.firstElementChild.querySelectorAll("li > span[data-target-id]");
headingsElement.forEach((item, index) => {
if (item.nextElementSibling && item.nextElementSibling.tagName === "UL") {
let iconHTML = "<svg class='vditor-outline__action'><use xlink:href='#vditor-icon-down'></use></svg>";
if (!document.getElementById("vditorIconScript")) {
iconHTML = '<svg class="vditor-outline__action" viewBox="0 0 32 32"><path d="M3.76 6.12l12.24 12.213 12.24-12.213 3.76 3.76-16 16-16-16 3.76-3.76z"></path></svg>';
}
item.innerHTML = `${iconHTML}<span>${item.innerHTML}</span>`;
} else {
item.innerHTML = `<svg></svg><span>${item.innerHTML}</span>`;
}
item.setAttribute("data-target-id", ids[index]);
});
tocHTML = tempElement.firstElementChild.innerHTML;
if (headingsElement.length === 0) {
targetElement.innerHTML = "";
return tocHTML;
}
targetElement.innerHTML = tocHTML;
if (vditor) {
mathRender(targetElement as HTMLElement, {
cdn: vditor.options.cdn,
math: vditor.options.preview.math,
});
}
targetElement.firstElementChild.addEventListener("click", (event: Event) => {
let target = event.target as HTMLElement;
while (target && !target.isEqualNode(targetElement)) {
if (target.classList.contains("vditor-outline__action")) {
if (target.classList.contains("vditor-outline__action--close")) {
target.classList.remove("vditor-outline__action--close");
target.parentElement.nextElementSibling.setAttribute("style", "display:block");
} else {
target.classList.add("vditor-outline__action--close");
target.parentElement.nextElementSibling.setAttribute("style", "display:none");
}
event.preventDefault();
event.stopPropagation();
break;
} else if (target.getAttribute("data-target-id")) {
event.preventDefault();
event.stopPropagation();
const idElement = document.getElementById(target.getAttribute("data-target-id"));
if (!idElement) {
return;
}
if (vditor) {
if (vditor.options.height === "auto") {
let windowScrollY = idElement.offsetTop + vditor.element.offsetTop;
if (!vditor.options.toolbarConfig.pin) {
windowScrollY += vditor.toolbar.element.offsetHeight;
}
window.scrollTo(window.scrollX, windowScrollY);
} else {
if (vditor.element.offsetTop < window.scrollY) {
window.scrollTo(window.scrollX, vditor.element.offsetTop);
}
if (vditor.preview.element.contains(contentElement)) {
contentElement.parentElement.scrollTop = idElement.offsetTop;
} else {
contentElement.scrollTop = idElement.offsetTop;
}
}
} else {
window.scrollTo(window.scrollX, idElement.offsetTop);
}
break;
}
target = target.parentElement;
}
});
return tocHTML;
};

View File

@@ -0,0 +1,32 @@
import {Constants} from "../constants";
import {addScript} from "../util/addScript";
import {plantumlRenderAdapter} from "./adapterRender";
declare const plantumlEncoder: {
encode(options: string): string,
};
export const plantumlRender = (element: (HTMLElement | Document) = document, cdn = Constants.CDN) => {
const plantumlElements = plantumlRenderAdapter.getElements(element);
if (plantumlElements.length === 0) {
return;
}
addScript(`${cdn}/dist/js/plantuml/plantuml-encoder.min.js`, "vditorPlantumlScript").then(() => {
plantumlElements.forEach((e: HTMLDivElement) => {
if (e.parentElement.classList.contains("vditor-wysiwyg__pre") ||
e.parentElement.classList.contains("vditor-ir__marker--pre")) {
return;
}
const text = plantumlRenderAdapter.getCode(e).trim();
if (!text) {
return;
}
try {
e.innerHTML = `<object type="image/svg+xml" data="https://www.plantuml.com/plantuml/svg/~1${plantumlEncoder.encode(text)}"/>`;
} catch (error) {
e.className = "vditor-reset--error";
e.innerHTML = `plantuml render error: <br>${error}`;
}
});
});
};

View File

@@ -0,0 +1,167 @@
import {Constants} from "../constants";
import {setContentTheme} from "../ui/setContentTheme";
import {addScript, addScriptSync} from "../util/addScript";
import {hasClosestByClassName, hasClosestByMatchTag} from "../util/hasClosest";
import {merge} from "../util/merge";
import {abcRender} from "./abcRender";
import {anchorRender} from "./anchorRender";
import {chartRender} from "./chartRender";
import {codeRender} from "./codeRender";
import {flowchartRender} from "./flowchartRender";
import {graphvizRender} from "./graphvizRender";
import {highlightRender} from "./highlightRender";
import {lazyLoadImageRender} from "./lazyLoadImageRender";
import {mathRender} from "./mathRender";
import {mediaRender} from "./mediaRender";
import {mermaidRender} from "./mermaidRender";
import {markmapRender} from "../markdown/markmapRender";
import {mindmapRender} from "./mindmapRender";
import {plantumlRender} from "./plantumlRender";
import {setLute} from "./setLute";
import {speechRender} from "./speechRender";
const mergeOptions = (options?: IPreviewOptions) => {
const defaultOption: IPreviewOptions = {
anchor: 0,
cdn: Constants.CDN,
customEmoji: {},
emojiPath: `${Constants.CDN}/dist/images/emoji`,
hljs: Constants.HLJS_OPTIONS,
icon: "ant",
lang: "zh_CN",
markdown: Constants.MARKDOWN_OPTIONS,
math: Constants.MATH_OPTIONS,
mode: "light",
speech: {
enable: false,
},
render: {
media: {
enable: true,
}
},
theme: Constants.THEME_OPTIONS,
};
if (options.cdn) {
if (!options.theme?.path) {
defaultOption.theme.path = `${options.cdn}/dist/css/content-theme`
}
if (!options.emojiPath) {
defaultOption.emojiPath = `${options.cdn}/dist/images/emoji`;
}
}
return merge(defaultOption, options);
};
export const md2html = (mdText: string, options?: IPreviewOptions) => {
const mergedOptions = mergeOptions(options);
return addScript(`${mergedOptions.cdn}/dist/js/lute/lute.min.js`, "vditorLuteScript").then(() => {
const lute = setLute({
autoSpace: mergedOptions.markdown.autoSpace,
gfmAutoLink: mergedOptions.markdown.gfmAutoLink,
codeBlockPreview: mergedOptions.markdown.codeBlockPreview,
emojiSite: mergedOptions.emojiPath,
emojis: mergedOptions.customEmoji,
fixTermTypo: mergedOptions.markdown.fixTermTypo,
footnotes: mergedOptions.markdown.footnotes,
headingAnchor: mergedOptions.anchor !== 0,
inlineMathDigit: mergedOptions.math.inlineDigit,
lazyLoadImage: mergedOptions.lazyLoadImage,
linkBase: mergedOptions.markdown.linkBase,
linkPrefix: mergedOptions.markdown.linkPrefix,
listStyle: mergedOptions.markdown.listStyle,
mark: mergedOptions.markdown.mark,
mathBlockPreview: mergedOptions.markdown.mathBlockPreview,
paragraphBeginningSpace: mergedOptions.markdown.paragraphBeginningSpace,
sanitize: mergedOptions.markdown.sanitize,
toc: mergedOptions.markdown.toc,
});
if (options?.renderers) {
lute.SetJSRenderers({
renderers: {
Md2HTML: options.renderers,
},
});
}
lute.SetHeadingID(true);
return lute.Md2HTML(mdText);
});
};
export const previewRender = async (previewElement: HTMLDivElement, markdown: string, options?: IPreviewOptions) => {
const mergedOptions: IPreviewOptions = mergeOptions(options);
let html = await md2html(markdown, mergedOptions);
if (mergedOptions.transform) {
html = mergedOptions.transform(html);
}
previewElement.innerHTML = html;
previewElement.classList.add("vditor-reset");
if (!mergedOptions.i18n) {
if (!["en_US", "fr_FR", "pt_BR", "ja_JP", "ko_KR", "ru_RU", "sv_SE", "zh_CN", "zh_TW"].includes(mergedOptions.lang)) {
throw new Error(
"options.lang error, see https://ld246.com/article/1549638745630#options",
);
} else {
const i18nScriptPrefix = "vditorI18nScript";
const i18nScriptID = i18nScriptPrefix + mergedOptions.lang;
document.querySelectorAll(`head script[id^="${i18nScriptPrefix}"]`).forEach((el) => {
if (el.id !== i18nScriptID) {
document.head.removeChild(el);
}
});
await addScript(`${mergedOptions.cdn}/dist/js/i18n/${mergedOptions.lang}.js`, i18nScriptID);
}
} else {
window.VditorI18n = mergedOptions.i18n;
}
if (mergedOptions.icon) {
await addScript(`${mergedOptions.cdn}/dist/js/icons/${mergedOptions.icon}.js`, "vditorIconScript");
}
setContentTheme(mergedOptions.theme.current, mergedOptions.theme.path);
if (mergedOptions.anchor === 1) {
previewElement.classList.add("vditor-reset--anchor");
}
codeRender(previewElement, mergedOptions.hljs);
highlightRender(mergedOptions.hljs, previewElement, mergedOptions.cdn);
mathRender(previewElement, {
cdn: mergedOptions.cdn,
math: mergedOptions.math,
});
mermaidRender(previewElement, mergedOptions.cdn, mergedOptions.mode);
markmapRender(previewElement, mergedOptions.cdn, mergedOptions.mode);
flowchartRender(previewElement, mergedOptions.cdn);
graphvizRender(previewElement, mergedOptions.cdn);
chartRender(previewElement, mergedOptions.cdn, mergedOptions.mode);
mindmapRender(previewElement, mergedOptions.cdn, mergedOptions.mode);
plantumlRender(previewElement, mergedOptions.cdn);
abcRender(previewElement, mergedOptions.cdn);
if (mergedOptions.render.media.enable) {
mediaRender(previewElement);
}
if (mergedOptions.speech.enable) {
speechRender(previewElement);
}
if (mergedOptions.anchor !== 0) {
anchorRender(mergedOptions.anchor);
}
if (mergedOptions.after) {
mergedOptions.after();
}
if (mergedOptions.lazyLoadImage) {
lazyLoadImageRender(previewElement);
}
previewElement.addEventListener("click", (event: MouseEvent & { target: HTMLElement }) => {
const spanElement = hasClosestByMatchTag(event.target, "SPAN");
if (spanElement && hasClosestByClassName(spanElement, "vditor-toc")) {
const headingElement =
previewElement.querySelector("#" + spanElement.getAttribute("data-target-id")) as HTMLElement;
if (headingElement) {
window.scrollTo(window.scrollX, headingElement.offsetTop);
}
return;
}
});
};

View File

@@ -0,0 +1,24 @@
export const setLute = (options: ILuteOptions) => {
const lute: Lute = Lute.New();
lute.PutEmojis(options.emojis);
lute.SetEmojiSite(options.emojiSite);
lute.SetHeadingAnchor(options.headingAnchor);
lute.SetInlineMathAllowDigitAfterOpenMarker(options.inlineMathDigit);
lute.SetAutoSpace(options.autoSpace);
lute.SetToC(options.toc);
lute.SetFootnotes(options.footnotes);
lute.SetFixTermTypo(options.fixTermTypo);
lute.SetVditorCodeBlockPreview(options.codeBlockPreview);
lute.SetVditorMathBlockPreview(options.mathBlockPreview);
lute.SetSanitize(options.sanitize);
lute.SetChineseParagraphBeginningSpace(options.paragraphBeginningSpace);
lute.SetRenderListStyle(options.listStyle);
lute.SetLinkBase(options.linkBase);
lute.SetLinkPrefix(options.linkPrefix);
lute.SetMark(options.mark);
lute.SetGFMAutoLink(options.gfmAutoLink);
if (options.lazyLoadImage) {
lute.SetImageLazyLoading(options.lazyLoadImage);
}
return lute;
};

View File

@@ -0,0 +1,107 @@
import {setSelectionFocus} from "../util/selection";
declare global {
interface Window {
vditorSpeechRange: Range;
}
}
export const speechRender = (element: HTMLElement, lang: keyof II18n = "zh_CN") => {
if (typeof speechSynthesis === "undefined" || typeof SpeechSynthesisUtterance === "undefined") {
return;
}
const getVoice = () => {
const voices = speechSynthesis.getVoices();
let currentVoice: SpeechSynthesisVoice;
let defaultVoice;
voices.forEach((item) => {
if (item.lang === lang.replace("_", "-")) {
currentVoice = item;
}
if (item.default) {
defaultVoice = item;
}
});
if (!currentVoice) {
currentVoice = defaultVoice;
}
return currentVoice;
};
let playSVG = '<svg><use xlink:href="#vditor-icon-play"></use></svg>';
let pauseSVG = '<svg><use xlink:href="#vditor-icon-pause"></use></svg>';
if (!document.getElementById("vditorIconScript")) {
playSVG = '<svg viewBox="0 0 32 32"><path d="M3.436 0l25.128 16-25.128 16v-32z"></path></svg>';
pauseSVG = '<svg viewBox="0 0 32 32"><path d="M20.617 0h9.128v32h-9.128v-32zM2.255 32v-32h9.128v32h-9.128z"></path></svg>';
}
let speechDom: HTMLButtonElement = document.querySelector(".vditor-speech");
if (!speechDom) {
speechDom = document.createElement("button");
speechDom.className = "vditor-speech";
element.insertAdjacentElement("beforeend", speechDom);
if (speechSynthesis.onvoiceschanged !== undefined) {
speechSynthesis.onvoiceschanged = getVoice;
}
}
const voice = getVoice();
const utterThis = new SpeechSynthesisUtterance();
utterThis.voice = voice;
utterThis.onend = utterThis.onerror = () => {
speechDom.style.display = "none";
speechSynthesis.cancel();
speechDom.classList.remove("vditor-speech--current");
speechDom.innerHTML = playSVG;
};
element.addEventListener(window.ontouchstart !== undefined ? "touchend" : "click", (event) => {
const target = event.target as HTMLElement
if (target.classList.contains("vditor-speech") || target.parentElement.classList.contains("vditor-speech")) {
if (!speechDom.classList.contains("vditor-speech--current")) {
utterThis.text = speechDom.getAttribute("data-text");
speechSynthesis.speak(utterThis);
speechDom.classList.add("vditor-speech--current");
speechDom.innerHTML = pauseSVG;
} else {
if (speechSynthesis.speaking) {
if (speechSynthesis.paused) {
speechSynthesis.resume();
speechDom.innerHTML = pauseSVG;
} else {
speechSynthesis.pause();
speechDom.innerHTML = playSVG;
}
}
}
setSelectionFocus(window.vditorSpeechRange);
element.focus();
return;
}
speechDom.style.display = "none";
speechSynthesis.cancel();
speechDom.classList.remove("vditor-speech--current");
speechDom.innerHTML = playSVG;
if (getSelection().rangeCount === 0) {
return;
}
const range = getSelection().getRangeAt(0)
const text = range.toString().trim();
if (!text) {
return;
}
window.vditorSpeechRange = range.cloneRange();
const rect = range.getBoundingClientRect();
speechDom.innerHTML = playSVG;
speechDom.style.display = "block";
speechDom.style.top = (rect.top + rect.height + document.querySelector("html").scrollTop - 20) + "px";
if (window.ontouchstart !== undefined) {
speechDom.style.left = ((event as TouchEvent).changedTouches[(event as TouchEvent).changedTouches.length - 1].pageX + 2) + "px";
} else {
speechDom.style.left = ((event as MouseEvent).clientX + 2) + "px";
}
speechDom.setAttribute("data-text", text);
});
};

View File

@@ -0,0 +1,45 @@
import {Constants} from "../constants";
import {outlineRender} from "../markdown/outlineRender";
import {setPadding} from "../ui/initUI";
import {setSelectionFocus} from "../util/selection";
export class Outline {
public element: HTMLElement;
constructor(outlineLabel: string) {
this.element = document.createElement("div");
this.element.className = "vditor-outline";
this.element.innerHTML = `<div class="vditor-outline__title">${outlineLabel}</div>
<div class="vditor-outline__content"></div>`;
}
public render(vditor: IVditor) {
let html = "";
if (vditor.preview.element.style.display === "block") {
html = outlineRender(vditor.preview.previewElement,
this.element.lastElementChild, vditor);
} else {
html = outlineRender(vditor[vditor.currentMode].element, this.element.lastElementChild, vditor);
}
return html;
}
public toggle(vditor: IVditor, show = true, focus = true) {
const btnElement = vditor.toolbar.elements.outline?.firstElementChild;
if (show && window.innerWidth >= Constants.MOBILE_WIDTH) {
this.element.style.display = "block";
this.render(vditor);
btnElement?.classList.add("vditor-menu--current");
} else {
this.element.style.display = "none";
btnElement?.classList.remove("vditor-menu--current");
}
if (focus && getSelection().rangeCount > 0) {
const range = getSelection().getRangeAt(0);
if (vditor[vditor.currentMode].element.contains(range.startContainer)) {
setSelectionFocus(range);
}
}
setPadding(vditor);
}
}

View File

@@ -0,0 +1,50 @@
export const previewImage = (oldImgElement: HTMLImageElement, lang: keyof II18n = "zh_CN", theme = "classic") => {
const oldImgRect = oldImgElement.getBoundingClientRect();
const height = 36;
document.body.insertAdjacentHTML("beforeend", `<div class="vditor vditor-img${theme === "dark" ? " vditor--dark" : ""}">
<div class="vditor-img__bar">
<span class="vditor-img__btn" data-deg="0">
<svg><use xlink:href="#vditor-icon-redo"></use></svg>
${window.VditorI18n.spin}
</span>
<span class="vditor-img__btn" onclick="this.parentElement.parentElement.outerHTML = '';document.body.style.overflow = ''">
X &nbsp;${window.VditorI18n.close}
</span>
</div>
<div class="vditor-img__img" onclick="this.parentElement.outerHTML = '';document.body.style.overflow = ''">
<img style="width: ${oldImgElement.width}px;height:${oldImgElement.height}px;transform: translate3d(${oldImgRect.left}px, ${oldImgRect.top - height}px, 0)" src="${oldImgElement.getAttribute("src")}">
</div>
</div>`);
document.body.style.overflow = "hidden";
// 图片从原始位置移动到预览正中间的动画效果
const imgElement = document.querySelector(".vditor-img img") as HTMLImageElement;
const translate3d = `translate3d(${Math.max(0, window.innerWidth - oldImgElement.naturalWidth) / 2}px, ${Math.max(0, window.innerHeight - height - oldImgElement.naturalHeight) / 2}px, 0)`;
setTimeout(() => {
imgElement.setAttribute("style", `transition: transform .3s ease-in-out;transform: ${translate3d}`);
setTimeout(() => {
imgElement.parentElement.scrollTo(
(imgElement.parentElement.scrollWidth - imgElement.parentElement.clientWidth) / 2,
(imgElement.parentElement.scrollHeight - imgElement.parentElement.clientHeight) / 2);
}, 400);
});
// 旋转
const btnElement = document.querySelector(".vditor-img__btn");
btnElement.addEventListener("click", () => {
const deg = parseInt(btnElement.getAttribute("data-deg"), 10) + 90;
if ((deg / 90) % 2 === 1 && oldImgElement.naturalWidth > imgElement.parentElement.clientHeight) {
imgElement.style.transform = `translate3d(${
Math.max(0, window.innerWidth - oldImgElement.naturalWidth) / 2}px, ${
oldImgElement.naturalWidth / 2 - oldImgElement.naturalHeight / 2}px, 0) rotateZ(${deg}deg)`;
} else {
imgElement.style.transform = `${translate3d} rotateZ(${deg}deg)`;
}
btnElement.setAttribute("data-deg", deg.toString());
setTimeout(() => {
imgElement.parentElement.scrollTo(
(imgElement.parentElement.scrollWidth - imgElement.parentElement.clientWidth) / 2,
(imgElement.parentElement.scrollHeight - imgElement.parentElement.clientHeight) / 2);
}, 400);
});
};

View File

@@ -0,0 +1,285 @@
import {abcRender} from "../markdown/abcRender";
import {chartRender} from "../markdown/chartRender";
import {codeRender} from "../markdown/codeRender";
import {flowchartRender} from "../markdown/flowchartRender";
import {getMarkdown} from "../markdown/getMarkdown";
import {graphvizRender} from "../markdown/graphvizRender";
import {highlightRender} from "../markdown/highlightRender";
import {mathRender} from "../markdown/mathRender";
import {mediaRender} from "../markdown/mediaRender";
import {mermaidRender} from "../markdown/mermaidRender";
import {markmapRender} from "../markdown/markmapRender";
import {mindmapRender} from "../markdown/mindmapRender";
import {plantumlRender} from "../markdown/plantumlRender";
import {getEventName} from "../util/compatibility";
import {hasClosestByClassName, hasClosestByMatchTag} from "../util/hasClosest";
import {hasClosestByTag} from "../util/hasClosestByHeadings";
import {setSelectionFocus} from "../util/selection";
import {previewImage} from "./image";
export class Preview {
public element: HTMLElement;
public previewElement: HTMLElement;
private mdTimeoutId: number;
constructor(vditor: IVditor) {
this.element = document.createElement("div");
this.element.className = `vditor-preview`;
this.previewElement = document.createElement("div");
this.previewElement.className = "vditor-reset";
if (vditor.options.classes.preview) {
this.previewElement.classList.add(vditor.options.classes.preview);
}
this.previewElement.style.maxWidth = vditor.options.preview.maxWidth + "px";
this.previewElement.addEventListener("copy", (event: ClipboardEvent & { target: HTMLElement }) => {
if (event.target.tagName === "TEXTAREA") {
// https://github.com/Vanessa219/vditor/issues/901
return;
}
const tempElement = document.createElement("div");
tempElement.className = "vditor-reset";
tempElement.appendChild(getSelection().getRangeAt(0).cloneContents());
this.copyToX(vditor, tempElement, "default");
event.preventDefault();
});
this.previewElement.addEventListener("click", (event: MouseEvent & { target: HTMLElement }) => {
const spanElement = hasClosestByMatchTag(event.target, "SPAN");
if (spanElement && hasClosestByClassName(spanElement, "vditor-toc")) {
const headingElement =
this.previewElement.querySelector("#" + spanElement.getAttribute("data-target-id")) as HTMLElement;
if (headingElement) {
this.element.scrollTop = headingElement.offsetTop;
}
return;
}
if (event.target.tagName === "A") {
if (vditor.options.link.click) {
vditor.options.link.click(event.target);
} else if (vditor.options.link.isOpen) {
window.open(event.target.getAttribute("href"));
}
event.preventDefault();
return;
}
if (event.target.tagName === "IMG") {
if (vditor.options.image.preview) {
vditor.options.image.preview(event.target)
} else if (vditor.options.image.isPreview) {
previewImage(event.target as HTMLImageElement, vditor.options.lang, vditor.options.theme);
}
}
});
this.element.appendChild(this.previewElement);
const actions = vditor.options.preview.actions;
if (actions.length === 0) {
return;
}
const actionElement = document.createElement("div");
actionElement.className = "vditor-preview__action";
const actionHtml: string[] = [];
for (let i = 0; i < actions.length; i++) {
const action = actions[i];
if (typeof action === "object") {
actionHtml.push(`<button type="button" data-type="${action.key}" class="${action.className}"${action.tooltip ? ` aria-label="${action.tooltip}"` : ""}">${action.text}</button>`);
continue;
}
switch (action) {
case "desktop":
actionHtml.push(`<button type="button" class="vditor-preview__action--current" data-type="desktop">Desktop</button>`);
break;
case "tablet":
actionHtml.push(`<button type="button" data-type="tablet">Tablet</button>`);
break;
case "mobile":
actionHtml.push(`<button type="button" data-type="mobile">Mobile/Wechat</button>`);
break;
case "mp-wechat":
actionHtml.push(`<button type="button" data-type="mp-wechat" class="vditor-tooltipped vditor-tooltipped__w" aria-label="复制到公众号"><svg><use xlink:href="#vditor-icon-mp-wechat"></use></svg></button>`);
break;
case "zhihu":
actionHtml.push(`<button type="button" data-type="zhihu" class="vditor-tooltipped vditor-tooltipped__w" aria-label="复制到知乎"><svg><use xlink:href="#vditor-icon-zhihu"></use></svg></button>`);
break;
}
}
actionElement.innerHTML = actionHtml.join("");
actionElement.addEventListener(getEventName(), (event) => {
const btn = hasClosestByTag(event.target as HTMLElement, "BUTTON");
if (!btn) {
return;
}
const type = btn.getAttribute("data-type");
const actionCustom = actions.find((w: IPreviewActionCustom) => w?.key === type) as IPreviewActionCustom;
if (actionCustom) {
actionCustom.click(type);
return;
}
if (type === "mp-wechat" || type === "zhihu") {
this.copyToX(vditor, this.previewElement.cloneNode(true) as HTMLElement, type);
return;
}
if (type === "desktop") {
this.previewElement.style.width = "auto";
} else if (type === "tablet") {
this.previewElement.style.width = "780px";
} else {
this.previewElement.style.width = "360px";
}
if (this.previewElement.scrollWidth > this.previewElement.parentElement.clientWidth) {
this.previewElement.style.width = "auto";
}
this.render(vditor);
actionElement.querySelectorAll("button").forEach((item) => {
item.classList.remove("vditor-preview__action--current");
});
btn.classList.add("vditor-preview__action--current");
});
this.element.insertBefore(actionElement, this.previewElement);
}
public render(vditor: IVditor, value?: string) {
clearTimeout(this.mdTimeoutId);
if (this.element.style.display === "none") {
if (this.element.getAttribute("data-type") === "renderPerformance") {
vditor.tip.hide();
}
return;
}
if (value) {
this.previewElement.innerHTML = value;
return;
}
if (getMarkdown(vditor)
.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, "") === "") {
this.previewElement.innerHTML = "";
return;
}
const renderStartTime = new Date().getTime();
const markdownText = getMarkdown(vditor);
this.mdTimeoutId = window.setTimeout(() => {
if (vditor.options.preview.url) {
const xhr = new XMLHttpRequest();
xhr.open("POST", vditor.options.preview.url);
xhr.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
xhr.onreadystatechange = () => {
if (xhr.readyState === XMLHttpRequest.DONE) {
if (xhr.status === 200) {
const responseJSON = JSON.parse(xhr.responseText);
if (responseJSON.code !== 0) {
vditor.tip.show(responseJSON.msg);
return;
}
if (vditor.options.preview.transform) {
responseJSON.data = vditor.options.preview.transform(responseJSON.data);
}
this.previewElement.innerHTML = responseJSON.data;
this.afterRender(vditor, renderStartTime);
} else {
let html = vditor.lute.Md2HTML(markdownText);
if (vditor.options.preview.transform) {
html = vditor.options.preview.transform(html);
}
this.previewElement.innerHTML = html;
this.afterRender(vditor, renderStartTime);
}
}
};
xhr.send(JSON.stringify({markdownText}));
} else {
let html = vditor.lute.Md2HTML(markdownText);
if (vditor.options.preview.transform) {
html = vditor.options.preview.transform(html);
}
this.previewElement.innerHTML = html;
this.afterRender(vditor, renderStartTime);
}
}, vditor.options.preview.delay);
}
private afterRender(vditor: IVditor, startTime: number) {
if (vditor.options.preview.parse) {
vditor.options.preview.parse(this.element);
}
const time = (new Date().getTime() - startTime);
if ((new Date().getTime() - startTime) > 2600) {
// https://github.com/b3log/vditor/issues/67
vditor.tip.show(window.VditorI18n.performanceTip.replace("${x}", time.toString()));
vditor.preview.element.setAttribute("data-type", "renderPerformance");
} else if (vditor.preview.element.getAttribute("data-type") === "renderPerformance") {
vditor.tip.hide();
vditor.preview.element.removeAttribute("data-type");
}
const cmtFocusElement = vditor.preview.element.querySelector(".vditor-comment--focus");
if (cmtFocusElement) {
cmtFocusElement.classList.remove("vditor-comment--focus");
}
codeRender(vditor.preview.previewElement, vditor.options.preview.hljs);
highlightRender(vditor.options.preview.hljs, vditor.preview.previewElement,
vditor.options.cdn);
mermaidRender(vditor.preview.previewElement, vditor.options.cdn, vditor.options.theme);
markmapRender(vditor.preview.previewElement, vditor.options.cdn, vditor.options.theme);
flowchartRender(vditor.preview.previewElement, vditor.options.cdn);
graphvizRender(vditor.preview.previewElement, vditor.options.cdn);
chartRender(vditor.preview.previewElement, vditor.options.cdn, vditor.options.theme);
mindmapRender(vditor.preview.previewElement, vditor.options.cdn, vditor.options.theme);
plantumlRender(vditor.preview.previewElement, vditor.options.cdn);
abcRender(vditor.preview.previewElement, vditor.options.cdn);
if (vditor.options.preview.render.media.enable) {
mediaRender(vditor.preview.previewElement);
}
vditor.options.customRenders.forEach((item) => {
item.render(vditor.preview.previewElement, vditor);
})
// toc render
const editorElement = vditor.preview.element;
let tocHTML = vditor.outline.render(vditor);
if (tocHTML === "") {
tocHTML = "[ToC]";
}
editorElement.querySelectorAll('[data-type="toc-block"]').forEach((item: HTMLElement) => {
item.innerHTML = tocHTML;
mathRender(item, {
cdn: vditor.options.cdn,
math: vditor.options.preview.math,
});
});
mathRender(vditor.preview.previewElement, {
cdn: vditor.options.cdn,
math: vditor.options.preview.math,
});
}
private copyToX(vditor: IVditor, copyElement: HTMLElement, type = "mp-wechat") {
// fix math render
if (type !== "zhihu") {
copyElement.querySelectorAll(".katex-html .base").forEach((item: HTMLElement) => {
item.style.display = "initial";
});
} else {
copyElement.querySelectorAll(".language-math").forEach((item: HTMLElement) => {
item.outerHTML = `<img class="Formula-image" data-eeimg="true" src="//www.zhihu.com/equation?tex=" alt="${item.getAttribute("data-math")}\\" style="display: block; margin: 0 auto; max-width: 100%;">`;
});
}
// 防止背景色被粘贴到公众号中
copyElement.style.backgroundColor = "#fff";
// 代码背景
copyElement.querySelectorAll("code").forEach((item) => {
item.style.backgroundImage = "none";
});
this.element.append(copyElement);
const range = copyElement.ownerDocument.createRange();
range.selectNode(copyElement);
setSelectionFocus(range);
document.execCommand("copy");
copyElement.remove();
vditor.tip.show(['zhihu', 'mp-wechat'].includes(type) ? `已复制,可到${type === "zhihu" ? "知乎" : "微信公众号平台"}进行粘贴` : `已复制到剪切板`);
}
}

View File

@@ -0,0 +1,57 @@
export class Resize {
public element: HTMLElement;
constructor(vditor: IVditor) {
this.element = document.createElement("div");
this.element.className = `vditor-resize vditor-resize--${vditor.options.resize.position}`;
this.element.innerHTML = `<div><svg><use xlink:href="#vditor-icon-resize"></use></svg></div>`;
this.bindEvent(vditor);
}
private bindEvent(vditor: IVditor) {
this.element.addEventListener("mousedown", (event: MouseEvent) => {
const documentSelf = document;
const y = event.clientY;
const height = vditor.element.offsetHeight;
const minHeight = 63 + vditor.element.querySelector(".vditor-toolbar").clientHeight;
documentSelf.ondragstart = () => false;
if (window.captureEvents) {
window.captureEvents();
}
this.element.classList.add("vditor-resize--selected");
documentSelf.onmousemove = (moveEvent: MouseEvent) => {
if (vditor.options.resize.position === "top") {
vditor.element.style.height = Math.max(minHeight, height + (y - moveEvent.clientY)) + "px";
} else {
vditor.element.style.height = Math.max(minHeight, height + (moveEvent.clientY - y)) + "px";
}
if (vditor.options.typewriterMode) {
vditor.sv.element.style.paddingBottom =
vditor.sv.element.parentElement.offsetHeight / 2 + "px";
}
};
documentSelf.onmouseup = () => {
if (vditor.options.resize.after) {
vditor.options.resize.after(vditor.element.offsetHeight - height);
}
if (window.captureEvents) {
window.captureEvents();
}
documentSelf.onmousemove = null;
documentSelf.onmouseup = null;
documentSelf.ondragstart = null;
documentSelf.onselectstart = null;
documentSelf.onselect = null;
this.element.classList.remove("vditor-resize--selected");
};
});
}
}

View File

@@ -0,0 +1,27 @@
/**
* 合并脚注
* @param elements vditor.sv.element
* @param afterCombine 每个脚注块合并完成后的回调, param: root为合并后的脚注块
*/
export const combineFootnote = (elements: HTMLElement, afterCombine?: (root: HTMLElement) => void ) => {
elements.querySelectorAll("[data-type=footnotes-link]").forEach((el: Element) => {
const root = el.parentElement
let footnote = root.nextSibling
// 寻找所有该脚注的块
while (footnote) {
if (footnote.textContent.startsWith(" ")) {
// 解析到四个空格加入到root并继续解析
const thisNode = footnote
thisNode.childNodes.forEach(node => {
root.append(node.cloneNode(true))
})
footnote = footnote.nextSibling
thisNode.remove()
} else {
// 非空格停止解析
break
}
}
afterCombine && afterCombine(root)
})
}

View File

@@ -0,0 +1,125 @@
import {isCtrl, isFirefox} from "../util/compatibility";
import {
blurEvent,
copyEvent, cutEvent,
dropEvent,
focusEvent,
hotkeyEvent,
scrollCenter,
selectEvent,
} from "../util/editorCommonEvent";
import {paste} from "../util/fixBrowserBehavior";
import {getSelectText} from "../util/getSelectText";
import {inputEvent} from "./inputEvent";
import {processAfterRender} from "./process";
class Editor {
public range: Range;
public element: HTMLPreElement;
public composingLock: boolean = false;
public processTimeoutId: number;
public hlToolbarTimeoutId: number;
public preventInput: boolean;
constructor(vditor: IVditor) {
this.element = document.createElement("pre");
this.element.className = "vditor-sv vditor-reset";
this.element.setAttribute("placeholder", vditor.options.placeholder);
this.element.setAttribute("contenteditable", "true");
this.element.setAttribute("spellcheck", "false");
this.bindEvent(vditor);
focusEvent(vditor, this.element);
blurEvent(vditor, this.element);
hotkeyEvent(vditor, this.element);
selectEvent(vditor, this.element);
dropEvent(vditor, this.element);
copyEvent(vditor, this.element, this.copy);
cutEvent(vditor, this.element, this.copy);
}
private copy(event: ClipboardEvent, vditor: IVditor) {
event.stopPropagation();
event.preventDefault();
event.clipboardData.setData("text/plain", getSelectText(vditor[vditor.currentMode].element));
}
private bindEvent(vditor: IVditor) {
this.element.addEventListener("paste", (event: ClipboardEvent & { target: HTMLElement }) => {
paste(vditor, event, {
pasteCode: (code: string) => {
document.execCommand("insertHTML", false, code);
},
});
});
this.element.addEventListener("scroll", () => {
if (vditor.preview.element.style.display !== "block") {
return;
}
const textScrollTop = this.element.scrollTop;
const textHeight = this.element.clientHeight;
const textScrollHeight = this.element.scrollHeight - parseFloat(this.element.style.paddingBottom || "0");
const preview = vditor.preview.element;
if ((textScrollTop / textHeight > 0.5)) {
preview.scrollTop = (textScrollTop + textHeight) *
preview.scrollHeight / textScrollHeight - textHeight;
} else {
preview.scrollTop = textScrollTop *
preview.scrollHeight / textScrollHeight;
}
});
this.element.addEventListener("compositionstart", (event: InputEvent) => {
this.composingLock = true;
});
this.element.addEventListener("compositionend", (event: InputEvent) => {
if (!isFirefox()) {
inputEvent(vditor, event);
}
this.composingLock = false;
});
this.element.addEventListener("input", (event: InputEvent) => {
if (event.inputType === "deleteByDrag" || event.inputType === "insertFromDrop") {
// https://github.com/Vanessa219/vditor/issues/801 编辑器内容拖拽问题
return;
}
if (this.composingLock || event.data === "" || event.data === "“" || event.data === "《") {
return;
}
if (this.preventInput) {
this.preventInput = false;
processAfterRender(vditor, {
enableAddUndoStack: true,
enableHint: true,
enableInput: true,
});
return;
}
inputEvent(vditor, event);
});
this.element.addEventListener("keyup", (event) => {
if (event.isComposing || isCtrl(event)) {
return;
}
if ((event.key === "Backspace" || event.key === "Delete") &&
vditor.sv.element.innerHTML !== "" && vditor.sv.element.childNodes.length === 1 &&
vditor.sv.element.firstElementChild && vditor.sv.element.firstElementChild.tagName === "DIV"
&& vditor.sv.element.firstElementChild.childElementCount === 2
&& (vditor.sv.element.firstElementChild.textContent === "" || vditor.sv.element.textContent === "\n")) {
// 为空时显示 placeholder
vditor.sv.element.innerHTML = "";
return;
}
if (event.key === "Enter") {
scrollCenter(vditor);
}
});
}
}
export {Editor};

View File

@@ -0,0 +1,188 @@
import {scrollCenter} from "../util/editorCommonEvent";
import {hasClosestByAttribute} from "../util/hasClosest";
import {getSelectPosition, setRangeByWbr} from "../util/selection";
import {getSideByType, processAfterRender, processSpinVditorSVDOM} from "./process";
import {combineFootnote} from "./combineFootnote";
export const inputEvent = (vditor: IVditor, event?: InputEvent) => {
const range = getSelection().getRangeAt(0).cloneRange();
let startContainer = range.startContainer;
if (range.startContainer.nodeType !== 3 && (range.startContainer as HTMLElement).tagName === "DIV") {
startContainer = range.startContainer.childNodes[range.startOffset - 1];
}
let blockElement = hasClosestByAttribute(startContainer, "data-block", "0");
// 不调用 lute 解析
if (blockElement && event && (event.inputType === "deleteContentBackward" || event.data === " ")) {
// 开始可以输入空格
const startOffset = getSelectPosition(blockElement, vditor.sv.element, range).start;
let startSpace = true;
for (let i = startOffset - 1;
// 软换行后有空格
i > blockElement.textContent.substr(0, startOffset).lastIndexOf("\n"); i--) {
if (blockElement.textContent.charAt(i) !== " " &&
// 多个 tab 前删除不形成代码块 https://github.com/Vanessa219/vditor/issues/162 1
blockElement.textContent.charAt(i) !== "\t") {
startSpace = false;
break;
}
}
if (startOffset === 0) {
startSpace = false;
}
if (startSpace) {
processAfterRender(vditor);
return;
}
if (event.inputType === "deleteContentBackward") {
// https://github.com/Vanessa219/vditor/issues/584 代码块 marker 删除
const codeBlockMarkerElement =
hasClosestByAttribute(startContainer, "data-type", "code-block-open-marker") ||
hasClosestByAttribute(startContainer, "data-type", "code-block-close-marker");
if (codeBlockMarkerElement) {
if (codeBlockMarkerElement.getAttribute("data-type") === "code-block-close-marker") {
const openMarkerElement = getSideByType(startContainer, "code-block-open-marker");
if (openMarkerElement) {
openMarkerElement.textContent = codeBlockMarkerElement.textContent;
processAfterRender(vditor);
return;
}
}
if (codeBlockMarkerElement.getAttribute("data-type") === "code-block-open-marker") {
const openMarkerElement = getSideByType(startContainer, "code-block-close-marker", false);
if (openMarkerElement) {
openMarkerElement.textContent = codeBlockMarkerElement.textContent;
processAfterRender(vditor);
return;
}
}
}
// https://github.com/Vanessa219/vditor/issues/877 数学公式输入删除生成节点
const mathBlockMarkerElement =
hasClosestByAttribute(startContainer, "data-type", "math-block-open-marker");
if (mathBlockMarkerElement) {
const mathBlockCloseElement = mathBlockMarkerElement.nextElementSibling.nextElementSibling;
if (mathBlockCloseElement && mathBlockCloseElement.getAttribute("data-type") === "math-block-close-marker") {
mathBlockCloseElement.remove();
processAfterRender(vditor);
}
return;
}
blockElement.querySelectorAll('[data-type="code-block-open-marker"]').forEach((item: HTMLElement) => {
if (item.textContent.length === 1) {
item.remove();
}
});
blockElement.querySelectorAll('[data-type="code-block-close-marker"]').forEach((item: HTMLElement) => {
if (item.textContent.length === 1) {
item.remove();
}
});
// 标题删除
const headingElement = hasClosestByAttribute(startContainer, "data-type", "heading-marker");
if (headingElement && headingElement.textContent.indexOf("#") === -1) {
processAfterRender(vditor);
return;
}
}
// 删除或空格不解析,否则会 format 回去
if ((event.data === " " || event.inputType === "deleteContentBackward") &&
(hasClosestByAttribute(startContainer, "data-type", "padding") // 场景b 前进行删除 [> 1. a\n> b]
|| hasClosestByAttribute(startContainer, "data-type", "li-marker") // 场景:删除最后一个字符 [* 1\n* ]
|| hasClosestByAttribute(startContainer, "data-type", "task-marker") // 场景:删除最后一个字符 [* [ ] ]
|| hasClosestByAttribute(startContainer, "data-type", "blockquote-marker") // 场景:删除最后一个字符 [> ]
)) {
processAfterRender(vditor);
return;
}
}
if (blockElement && blockElement.textContent.trimRight() === "$$") {
// 内联数学公式
processAfterRender(vditor);
return;
}
if (!blockElement) {
blockElement = vditor.sv.element;
}
if (blockElement.firstElementChild?.getAttribute("data-type") === "link-ref-defs-block") {
// 修改链接引用
blockElement = vditor.sv.element;
}
if (hasClosestByAttribute(startContainer, "data-type", "footnotes-link")) {
// 修改脚注角标
blockElement = vditor.sv.element;
}
// 添加光标位置
if (blockElement.textContent.indexOf(Lute.Caret) === -1) {
// 点击工具栏会插入 Caret
range.insertNode(document.createTextNode(Lute.Caret));
}
// 清除浏览器自带的样式
blockElement.querySelectorAll("[style]").forEach((item) => { // 不可前置,否则会影响 newline 的样式
item.removeAttribute("style");
});
blockElement.querySelectorAll("font").forEach((item) => { // 不可前置,否则会影响光标的位置
item.outerHTML = item.innerHTML;
});
let html = blockElement.textContent;
const isSVElement = blockElement.isEqualNode(vditor.sv.element);
if (isSVElement) {
html = blockElement.textContent;
} else {
// 添加前一个块元素
if (blockElement.previousElementSibling) {
html = blockElement.previousElementSibling.textContent + html;
blockElement.previousElementSibling.remove();
}
if (blockElement.previousElementSibling && html.indexOf("---\n") === 0) {
// 确认 yaml-front 是否为首行
html = blockElement.previousElementSibling.textContent + html;
blockElement.previousElementSibling.remove();
}
// 添加链接引用
let footnotes = ""
vditor.sv.element.querySelectorAll("[data-type='link-ref-defs-block']").forEach((item, index) => {
if (item && !(blockElement as HTMLElement).isEqualNode(item.parentElement)) {
footnotes += item.parentElement.textContent + "\n";
item.parentElement.remove();
}
});
// 添加脚注到文章头便于lute处理
vditor.sv.element.querySelectorAll("[data-type='footnotes-link']").forEach((item, index) => {
if (item && !(blockElement as HTMLElement).isEqualNode(item.parentElement)) {
footnotes += item.parentElement.textContent + "\n";
item.parentElement.remove();
}
});
html = footnotes + html;
}
html = processSpinVditorSVDOM(html, vditor);
if (isSVElement) {
blockElement.innerHTML = html;
} else {
blockElement.outerHTML = html;
}
vditor.sv.element.querySelectorAll("[data-type='link-ref-defs-block']").forEach((item) => {
vditor.sv.element.insertAdjacentElement("beforeend", item.parentElement)
})
// 合并脚注
combineFootnote(vditor.sv.element, (root: HTMLElement) => {
vditor.sv.element.insertAdjacentElement("beforeend", root)
})
setRangeByWbr(vditor.sv.element, range);
scrollCenter(vditor);
processAfterRender(vditor, {
enableAddUndoStack: true,
enableHint: true,
enableInput: true,
});
};

View File

@@ -0,0 +1,215 @@
import {getMarkdown} from "../markdown/getMarkdown";
import {accessLocalStorage} from "../util/compatibility";
import {scrollCenter} from "../util/editorCommonEvent";
import {hasClosestBlock, hasClosestByAttribute} from "../util/hasClosest";
import {hasClosestByTag} from "../util/hasClosestByHeadings";
import {log} from "../util/log";
import {getEditorRange, setRangeByWbr} from "../util/selection";
import {inputEvent} from "./inputEvent";
import {combineFootnote} from "./combineFootnote";
export const processPaste = (vditor: IVditor, text: string) => {
const range = getEditorRange(vditor);
range.extractContents();
range.insertNode(document.createTextNode(Lute.Caret));
range.insertNode(document.createTextNode(text));
let blockElement = hasClosestByAttribute(range.startContainer, "data-block", "0");
if (!blockElement) {
blockElement = vditor.sv.element;
}
let spinHTML = vditor.lute.SpinVditorSVDOM(blockElement.textContent)
spinHTML = "<div data-block='0'>" +
spinHTML.replace(/<span data-type="newline"><br \/><span style="display: none">\n<\/span><\/span><span data-type="newline"><br \/><span style="display: none">\n<\/span><\/span></g, '<span data-type="newline"><br /><span style="display: none">\n</span></span><span data-type="newline"><br /><span style="display: none">\n</span></span></div><div data-block="0"><') +
"</div>";
if (blockElement.isEqualNode(vditor.sv.element)) {
blockElement.innerHTML = spinHTML;
} else {
blockElement.outerHTML = spinHTML;
}
combineFootnote(vditor.sv.element)
setRangeByWbr(vditor.sv.element, range);
scrollCenter(vditor);
};
export const getSideByType = (spanNode: Node, type: string, isPrevious = true) => {
let sideElement = spanNode as Element;
if (sideElement.nodeType === 3) {
sideElement = sideElement.parentElement;
}
while (sideElement) {
if (sideElement.getAttribute("data-type") === type) {
return sideElement;
}
if (isPrevious) {
sideElement = sideElement.previousElementSibling;
} else {
sideElement = sideElement.nextElementSibling;
}
}
return false;
};
export const processSpinVditorSVDOM = (html: string, vditor: IVditor) => {
log("SpinVditorSVDOM", html, "argument", vditor.options.debugger);
const spinHTML = vditor.lute.SpinVditorSVDOM(html)
html = "<div data-block='0'>" +
spinHTML.replace(/<span data-type="newline"><br \/><span style="display: none">\n<\/span><\/span><span data-type="newline"><br \/><span style="display: none">\n<\/span><\/span></g, '<span data-type="newline"><br /><span style="display: none">\n</span></span><span data-type="newline"><br /><span style="display: none">\n</span></span></div><div data-block="0"><') +
"</div>";
log("SpinVditorSVDOM", html, "result", vditor.options.debugger);
return html;
};
export const processPreviousMarkers = (spanElement: HTMLElement) => {
const spanType = spanElement.getAttribute("data-type");
let previousElement = spanElement.previousElementSibling;
// 有内容的子列表/标题,在其 marker 后换行
let markerText = (spanType && spanType !== "text" && spanType !== "table" && spanType !== "heading-marker" &&
spanType !== "newline" && spanType !== "yaml-front-matter-open-marker" && spanType !== "yaml-front-matter-close-marker"
&& spanType !== "code-block-info" && spanType !== "code-block-close-marker" && spanType !== "code-block-open-marker") ?
spanElement.textContent : "";
let hasNL = false;
if (spanType === "newline") {
hasNL = true;
}
while (previousElement && !hasNL) {
const previousType = previousElement.getAttribute("data-type");
if (previousType === "li-marker" || previousType === "blockquote-marker" || previousType === "task-marker" ||
previousType === "padding") {
const previousText = previousElement.textContent;
if (previousType === "li-marker" &&
(spanType === "code-block-open-marker" || spanType === "code-block-info")) {
// https://github.com/Vanessa219/vditor/issues/586
markerText = previousText.replace(/\S/g, " ") + markerText;
} else if (spanType === "code-block-close-marker" &&
previousElement.nextElementSibling.isSameNode(spanElement)) {
// https://github.com/Vanessa219/vditor/issues/594
const openMarker = getSideByType(spanElement, "code-block-open-marker");
if (openMarker && openMarker.previousElementSibling) {
previousElement = openMarker.previousElementSibling;
markerText = previousText + markerText;
}
} else {
markerText = previousText + markerText;
}
} else if (previousType === "newline") {
hasNL = true;
}
previousElement = previousElement.previousElementSibling;
}
return markerText;
};
export const processAfterRender = (vditor: IVditor, options = {
enableAddUndoStack: true,
enableHint: false,
enableInput: true,
}) => {
if (options.enableHint) {
vditor.hint.render(vditor);
}
vditor.preview.render(vditor);
const text = getMarkdown(vditor);
if (typeof vditor.options.input === "function" && options.enableInput) {
vditor.options.input(text);
}
if (vditor.options.counter.enable) {
vditor.counter.render(vditor, text);
}
if (vditor.options.cache.enable && accessLocalStorage()) {
localStorage.setItem(vditor.options.cache.id, text);
if (vditor.options.cache.after) {
vditor.options.cache.after(text);
}
}
if (vditor.devtools) {
vditor.devtools.renderEchart(vditor);
}
clearTimeout(vditor.sv.processTimeoutId);
vditor.sv.processTimeoutId = window.setTimeout(() => {
if (options.enableAddUndoStack && !vditor.sv.composingLock) {
vditor.undo.addToUndoStack(vditor);
}
}, vditor.options.undoDelay);
};
export const processHeading = (vditor: IVditor, value: string) => {
const range = getEditorRange(vditor);
const headingElement = hasClosestByTag(range.startContainer, "SPAN");
if (headingElement && headingElement.textContent.trim() !== "") {
value = "\n" + value;
}
range.collapse(true);
document.execCommand("insertHTML", false, value);
};
export const processToolbar = (vditor: IVditor, actionBtn: Element, prefix: string, suffix: string) => {
const range = getEditorRange(vditor);
const commandName = actionBtn.getAttribute("data-type");
// 添加
if (vditor.sv.element.childNodes.length === 0) {
vditor.sv.element.innerHTML = `<span data-type="p" data-block="0"><span data-type="text"><wbr></span></span><span data-type="newline"><br><span style="display: none">
</span></span>`;
setRangeByWbr(vditor.sv.element, range);
}
const blockElement = hasClosestBlock(range.startContainer);
const spanElement = hasClosestByTag(range.startContainer, "SPAN");
if (!blockElement) {
return;
}
if (commandName === "link") {
let html;
if (range.toString() === "") {
html = `${prefix}${Lute.Caret}${suffix}`;
} else {
html = `${prefix}${range.toString()}${suffix.replace(")", Lute.Caret + ")")}`;
}
document.execCommand("insertHTML", false, html);
return;
} else if (commandName === "italic" || commandName === "bold" || commandName === "strike" ||
commandName === "inline-code" || commandName === "code" || commandName === "table" || commandName === "line") {
let html;
// https://github.com/Vanessa219/vditor/issues/563 代码块不需要后面的 ```
if (range.toString() === "") {
html = `${prefix}${Lute.Caret}${commandName === "code" ? "" : suffix}`;
} else {
html = `${prefix}${range.toString()}${Lute.Caret}${commandName === "code" ? "" : suffix}`;
}
if (commandName === "table" || (commandName === "code" && spanElement && spanElement.textContent !== "")) {
html = "\n\n" + html;
} else if (commandName === "line") {
html = `\n\n${prefix}\n${Lute.Caret}`;
}
document.execCommand("insertHTML", false, html);
return;
} else if (commandName === "check" || commandName === "list" || commandName === "ordered-list" ||
commandName === "quote") {
if (spanElement) {
let marker = "* ";
if (commandName === "check") {
marker = "* [ ] ";
} else if (commandName === "ordered-list") {
marker = "1. ";
} else if (commandName === "quote") {
marker = "> ";
}
const newLine = getSideByType(spanElement, "newline");
if (newLine) {
newLine.insertAdjacentText("afterend", marker);
} else {
blockElement.insertAdjacentText("afterbegin", marker);
}
inputEvent(vditor);
return;
}
}
setRangeByWbr(vditor.sv.element, range);
processAfterRender(vditor);
};

View File

@@ -0,0 +1,202 @@
import {isCtrl} from "../util/compatibility";
import {fixTab} from "../util/fixBrowserBehavior";
import {hasClosestByAttribute} from "../util/hasClosest";
import {hasClosestByTag} from "../util/hasClosestByHeadings";
import {getEditorRange, getSelectPosition} from "../util/selection";
import {inputEvent} from "./inputEvent";
import {processAfterRender, processPreviousMarkers} from "./process";
export const processKeydown = (vditor: IVditor, event: KeyboardEvent) => {
vditor.sv.composingLock = event.isComposing;
if (event.isComposing) {
return false;
}
if (event.key.indexOf("Arrow") === -1 && event.key !== "Meta" && event.key !== "Control" && event.key !== "Alt" &&
event.key !== "Shift" && event.key !== "CapsLock" && event.key !== "Escape" && !/^F\d{1,2}$/.test(event.key)) {
vditor.undo.recordFirstPosition(vditor, event);
}
// 仅处理以下快捷键操作
if (event.key !== "Enter" && event.key !== "Tab" && event.key !== "Backspace" && event.key.indexOf("Arrow") === -1
&& !isCtrl(event) && event.key !== "Escape") {
return false;
}
const range = getEditorRange(vditor);
let startContainer = range.startContainer;
if (range.startContainer.nodeType !== 3 && (range.startContainer as HTMLElement).tagName === "DIV") {
startContainer = range.startContainer.childNodes[range.startOffset - 1];
}
const textElement = hasClosestByAttribute(startContainer, "data-type", "text");
// blockquote
let blockquoteMarkerElement = hasClosestByAttribute(startContainer, "data-type", "blockquote-marker");
if (!blockquoteMarkerElement && range.startOffset === 0 && textElement && textElement.previousElementSibling &&
textElement.previousElementSibling.getAttribute("data-type") === "blockquote-marker") {
blockquoteMarkerElement = textElement.previousElementSibling as HTMLElement;
}
// 回车逐个删除 blockquote marker 标记
if (blockquoteMarkerElement) {
if (event.key === "Enter" && !isCtrl(event) && !event.altKey &&
blockquoteMarkerElement.nextElementSibling.textContent.trim() === "" &&
getSelectPosition(blockquoteMarkerElement, vditor.sv.element, range).start ===
blockquoteMarkerElement.textContent.length) {
if (blockquoteMarkerElement.previousElementSibling?.getAttribute("data-type") === "padding") {
// 列表中存在多行 BQ 时,标记回车需跳出列表
blockquoteMarkerElement.previousElementSibling.setAttribute("data-action", "enter-remove");
}
blockquoteMarkerElement.remove();
processAfterRender(vditor);
event.preventDefault();
return true;
}
}
// list item
const listMarkerElement = hasClosestByAttribute(startContainer, "data-type", "li-marker") as HTMLElement;
const taskMarkerElement = hasClosestByAttribute(startContainer, "data-type", "task-marker") as HTMLElement;
let listLastMarkerElement = listMarkerElement;
if (!listLastMarkerElement) {
if (taskMarkerElement && taskMarkerElement.nextElementSibling.getAttribute("data-type") !== "task-marker") {
listLastMarkerElement = taskMarkerElement;
}
}
if (!listLastMarkerElement && range.startOffset === 0 && textElement && textElement.previousElementSibling &&
(textElement.previousElementSibling.getAttribute("data-type") === "li-marker" ||
textElement.previousElementSibling.getAttribute("data-type") === "task-marker")) {
listLastMarkerElement = textElement.previousElementSibling as HTMLElement;
}
if (listLastMarkerElement) {
const startIndex = getSelectPosition(listLastMarkerElement, vditor.sv.element, range).start;
const isTask = listLastMarkerElement.getAttribute("data-type") === "task-marker";
let listFirstMarkerElement = listLastMarkerElement;
if (isTask) {
listFirstMarkerElement = listLastMarkerElement.previousElementSibling.previousElementSibling
.previousElementSibling as HTMLElement;
}
if (startIndex === listLastMarkerElement.textContent.length) {
// 回车清空列表标记符
if (event.key === "Enter" && !isCtrl(event) && !event.altKey && !event.shiftKey &&
listLastMarkerElement.nextElementSibling.textContent.trim() === "") {
if (listFirstMarkerElement.previousElementSibling?.getAttribute("data-type") === "padding") {
listFirstMarkerElement.previousElementSibling.remove();
inputEvent(vditor);
} else {
if (isTask) {
listFirstMarkerElement.remove();
listLastMarkerElement.previousElementSibling.previousElementSibling.remove();
listLastMarkerElement.previousElementSibling.remove();
}
listLastMarkerElement.nextElementSibling.remove();
listLastMarkerElement.remove();
processAfterRender(vditor);
}
event.preventDefault();
return true;
}
// 第一个 marker 后 tab 进行缩进
if (event.key === "Tab") {
if (event.shiftKey) {
if (listFirstMarkerElement.previousElementSibling.getAttribute("data-type") === "padding") {
listFirstMarkerElement.previousElementSibling.remove();
}
} else {
listFirstMarkerElement.insertAdjacentHTML("beforebegin",
`<span data-type="padding">${listFirstMarkerElement.textContent.replace(/\S/g, " ")}</span>`);
}
if (/^\d/.test(listFirstMarkerElement.textContent)) {
listFirstMarkerElement.textContent = listFirstMarkerElement.textContent.replace(/^\d{1,}/, "1");
range.selectNodeContents(listLastMarkerElement.firstChild);
range.collapse(false);
}
inputEvent(vditor);
event.preventDefault();
return true;
}
}
}
// tab
if (fixTab(vditor, range, event)) {
return true;
}
const blockElement = hasClosestByAttribute(startContainer, "data-block", "0");
const spanElement = hasClosestByTag(startContainer, "SPAN");
// 回车
if (event.key === "Enter" && !isCtrl(event) && !event.altKey && !event.shiftKey && blockElement) {
let isFirst = false;
const newLineMatch = blockElement.textContent.match(/^\n+/);
if (getSelectPosition(blockElement, vditor.sv.element).start <= (newLineMatch ? newLineMatch[0].length : 0)) {
// 允许段落开始换行
isFirst = true;
}
let newLineText = "\n";
if (spanElement) {
if (spanElement.previousElementSibling?.getAttribute("data-action") === "enter-remove") {
// https://github.com/Vanessa219/vditor/issues/596
spanElement.previousElementSibling.remove();
processAfterRender(vditor);
event.preventDefault();
return true;
} else {
newLineText += processPreviousMarkers(spanElement);
}
}
range.insertNode(document.createTextNode(newLineText));
range.collapse(false);
if (blockElement && blockElement.textContent.trim() !== "" && !isFirst) {
inputEvent(vditor);
} else {
processAfterRender(vditor);
}
event.preventDefault();
return true;
}
// 删除后光标前有 newline 的处理
if (event.key === "Backspace" && !isCtrl(event) && !event.altKey && !event.shiftKey) {
if (spanElement && spanElement.previousElementSibling?.getAttribute("data-type") === "newline" &&
getSelectPosition(spanElement, vditor.sv.element, range).start === 1 &&
// 飘号的处理需在 inputEvent 中,否则上下飘号对不齐
spanElement.getAttribute("data-type").indexOf("code-block-") === -1) {
// 光标在每一行的第一个字符后
range.setStart(spanElement, 0);
range.extractContents();
if (spanElement.textContent.trim() !== "") {
inputEvent(vditor);
} else {
processAfterRender(vditor);
}
event.preventDefault();
return true;
}
// 每一段第一个字符前
if (blockElement && getSelectPosition(blockElement, vditor.sv.element, range).start === 0 &&
blockElement.previousElementSibling) {
range.extractContents();
let previousLastElement = blockElement.previousElementSibling.lastElementChild;
if (previousLastElement.getAttribute("data-type") === "newline") {
previousLastElement.remove();
previousLastElement = blockElement.previousElementSibling.lastElementChild;
}
// 场景:末尾无法删除 [```\ntext\n```\n\n]
if (previousLastElement.getAttribute("data-type") !== "newline") {
previousLastElement.insertAdjacentHTML("afterend", blockElement.innerHTML);
blockElement.remove();
}
if (blockElement.textContent.trim() !== "" && !blockElement.previousElementSibling?.querySelector('[data-type="code-block-open-marker"]')) {
inputEvent(vditor);
} else {
if (previousLastElement.getAttribute("data-type") !== "newline") {
// https://github.com/Vanessa219/vditor/issues/597
range.selectNodeContents(previousLastElement.lastChild);
range.collapse(false);
}
processAfterRender(vditor);
}
event.preventDefault();
return true;
}
}
return false;
};

View File

@@ -0,0 +1,39 @@
export class Tip {
public element: HTMLElement;
constructor() {
this.element = document.createElement("div");
this.element.className = "vditor-tip";
}
public show(text: string, time: number = 6000) {
this.element.className = "vditor-tip vditor-tip--show";
if (time === 0) {
this.element.innerHTML = `<div class="vditor-tip__content">${text}
<div class="vditor-tip__close">X</div></div>`;
this.element.querySelector(".vditor-tip__close").addEventListener("click", () => {
this.hide();
});
} else {
this.element.innerHTML = `<div class="vditor-tip__content">${text}</div>`;
setTimeout(() => {
this.hide();
}, time);
}
// 需在动画结束后才能确定位置
this.element.removeAttribute("style")
setTimeout(() => {
const rect = this.element.getBoundingClientRect();
if (rect.top < 46) {
this.element.style.position = "fixed"
this.element.style.top = "46px"
}
}, 150);
}
public hide() {
this.element.className = "vditor-messageElementtip";
this.element.innerHTML = "";
}
}

View File

@@ -0,0 +1,28 @@
import {Constants} from "../constants";
import {setPreviewMode} from "../ui/setPreviewMode";
import {getEventName} from "../util/compatibility";
import {MenuItem} from "./MenuItem";
export class Both extends MenuItem {
constructor(vditor: IVditor, menuItem: IMenuItem) {
super(vditor, menuItem);
if (vditor.options.preview.mode === "both") {
this.element.children[0].classList.add("vditor-menu--current");
}
this.element.children[0].addEventListener(getEventName(), (event) => {
const btnElement = this.element.firstElementChild;
if (btnElement.classList.contains(Constants.CLASS_MENU_DISABLED)) {
return;
}
event.preventDefault();
if (vditor.currentMode !== "sv") {
return;
}
if (vditor.options.preview.mode === "both") {
setPreviewMode("editor", vditor);
} else {
setPreviewMode("both", vditor);
}
});
}
}

View File

@@ -0,0 +1,8 @@
export class Br {
public element: HTMLElement;
constructor() {
this.element = document.createElement("div");
this.element.className = "vditor-toolbar__br";
}
}

View File

@@ -0,0 +1,36 @@
import {Constants} from "../constants";
import {setCodeTheme} from "../ui/setCodeTheme";
import {getEventName} from "../util/compatibility";
import {MenuItem} from "./MenuItem";
import {hidePanel, toggleSubMenu} from "./setToolbar";
export class CodeTheme extends MenuItem {
public element: HTMLElement;
constructor(vditor: IVditor, menuItem: IMenuItem) {
super(vditor, menuItem);
const actionBtn = this.element.children[0] as HTMLElement;
const panelElement = document.createElement("div");
panelElement.className = `vditor-hint${menuItem.level === 2 ? "" : " vditor-panel--arrow"}`;
let innerHTML = "";
Constants.CODE_THEME.forEach((theme) => {
innerHTML += `<button>${theme}</button>`;
});
panelElement.innerHTML =
`<div style="overflow: auto;max-height:${window.innerHeight / 2}px">${innerHTML}</div>`;
panelElement.addEventListener(getEventName(), (event: MouseEvent & { target: HTMLElement }) => {
if (event.target.tagName === "BUTTON") {
hidePanel(vditor, ["subToolbar"]);
vditor.options.preview.hljs.style = event.target.textContent;
setCodeTheme(event.target.textContent, vditor.options.cdn);
event.preventDefault();
event.stopPropagation();
}
});
this.element.appendChild(panelElement);
toggleSubMenu(vditor, panelElement, actionBtn, menuItem.level);
}
}

View File

@@ -0,0 +1,35 @@
import {setContentTheme} from "../ui/setContentTheme";
import {getEventName} from "../util/compatibility";
import {MenuItem} from "./MenuItem";
import {hidePanel, toggleSubMenu} from "./setToolbar";
export class ContentTheme extends MenuItem {
public element: HTMLElement;
constructor(vditor: IVditor, menuItem: IMenuItem) {
super(vditor, menuItem);
const actionBtn = this.element.children[0] as HTMLElement;
const panelElement = document.createElement("div");
panelElement.className = `vditor-hint${menuItem.level === 2 ? "" : " vditor-panel--arrow"}`;
let innerHTML = "";
Object.keys(vditor.options.preview.theme.list).forEach((key) => {
innerHTML += `<button data-type="${key}">${vditor.options.preview.theme.list[key]}</button>`;
});
panelElement.innerHTML =
`<div style="overflow: auto;max-height:${window.innerHeight / 2}px">${innerHTML}</div>`;
panelElement.addEventListener(getEventName(), (event: MouseEvent & { target: HTMLElement }) => {
if (event.target.tagName === "BUTTON") {
hidePanel(vditor, ["subToolbar"]);
vditor.options.preview.theme.current = event.target.getAttribute("data-type");
setContentTheme(vditor.options.preview.theme.current, vditor.options.preview.theme.path);
event.preventDefault();
event.stopPropagation();
}
});
this.element.appendChild(panelElement);
toggleSubMenu(vditor, panelElement, actionBtn, menuItem.level);
}
}

View File

@@ -0,0 +1,40 @@
export class Counter {
public element: HTMLElement;
constructor(vditor: IVditor) {
this.element = document.createElement("span");
this.element.className = "vditor-counter vditor-tooltipped vditor-tooltipped__nw";
this.render(vditor, "");
}
public render(vditor: IVditor, mdText: string) {
let length = mdText.endsWith("\n") ? mdText.length - 1 : mdText.length;
if (vditor.options.counter.type === "text" && vditor[vditor.currentMode]) {
const tempElement = vditor[vditor.currentMode].element.cloneNode(true) as HTMLElement;
tempElement.querySelectorAll(".vditor-wysiwyg__preview").forEach((item) => {
item.remove();
});
length = tempElement.textContent.length;
}
if (typeof vditor.options.counter.max === "number") {
if (length > vditor.options.counter.max) {
this.element.className = "vditor-counter vditor-counter--error";
} else {
this.element.className = "vditor-counter";
}
this.element.innerHTML = `${length}/${vditor.options.counter.max}`;
} else {
this.element.innerHTML = `${length}`;
}
this.element.setAttribute("aria-label", vditor.options.counter.type);
if (vditor.options.counter.after) {
vditor.options.counter.after(length, {
enable: vditor.options.counter.enable,
max: vditor.options.counter.max,
type: vditor.options.counter.type,
});
}
}
}

View File

@@ -0,0 +1,17 @@
import {Constants} from "../constants";
import {getEventName} from "../util/compatibility";
import {MenuItem} from "./MenuItem";
export class Custom extends MenuItem {
constructor(vditor: IVditor, menuItem: IMenuItem) {
super(vditor, menuItem);
this.element.children[0].innerHTML = menuItem.icon;
this.element.children[0].addEventListener(getEventName(), (event: Event & { currentTarget: HTMLElement }) => {
event.preventDefault();
if (event.currentTarget.classList.contains(Constants.CLASS_MENU_DISABLED)) {
return;
}
menuItem.click(event, vditor);
});
}
}

View File

@@ -0,0 +1,28 @@
import {Constants} from "../constants";
import {setPadding} from "../ui/initUI";
import {getEventName} from "../util/compatibility";
import {MenuItem} from "./MenuItem";
export class Devtools extends MenuItem {
constructor(vditor: IVditor, menuItem: IMenuItem) {
super(vditor, menuItem);
this.element.firstElementChild.addEventListener(getEventName(), (event) => {
const btnElement = this.element.firstElementChild;
if (btnElement.classList.contains(Constants.CLASS_MENU_DISABLED)) {
return;
}
event.preventDefault();
if (btnElement.classList.contains("vditor-menu--current")) {
btnElement.classList.remove("vditor-menu--current");
vditor.devtools.element.style.display = "none";
setPadding(vditor);
} else {
btnElement.classList.add("vditor-menu--current");
vditor.devtools.element.style.display = "block";
setPadding(vditor);
vditor.devtools.renderEchart(vditor);
}
});
}
}

View File

@@ -0,0 +1,8 @@
export class Divider {
public element: HTMLElement;
constructor() {
this.element = document.createElement("div");
this.element.className = "vditor-toolbar__divider";
}
}

View File

@@ -0,0 +1,195 @@
import {Constants} from "../constants";
import {processAfterRender} from "../ir/process";
import {getMarkdown} from "../markdown/getMarkdown";
import {mathRender} from "../markdown/mathRender";
import {processAfterRender as processSVAfterRender, processSpinVditorSVDOM} from "../sv/process";
import {setPadding, setTypewriterPosition} from "../ui/initUI";
import {getEventName, updateHotkeyTip} from "../util/compatibility";
import {highlightToolbar} from "../util/highlightToolbar";
import {processCodeRender} from "../util/processCode";
import {renderToc} from "../util/toc";
import {renderDomByMd} from "../wysiwyg/renderDomByMd";
import {MenuItem} from "./MenuItem";
import {
disableToolbar,
enableToolbar,
hidePanel,
hideToolbar,
removeCurrentToolbar,
showToolbar, toggleSubMenu,
} from "./setToolbar";
import {combineFootnote} from "../sv/combineFootnote";
export const setEditMode = (vditor: IVditor, type: string, event: Event | string) => {
let markdownText;
if (typeof event !== "string") {
hidePanel(vditor, ["subToolbar", "hint"]);
event.preventDefault();
markdownText = getMarkdown(vditor);
} else {
markdownText = event;
}
if (vditor.currentMode === type && typeof event !== "string") {
return;
}
if (vditor.devtools) {
vditor.devtools.renderEchart(vditor);
}
if (vditor.options.preview.mode === "both" && type === "sv") {
vditor.preview.element.style.display = "block";
} else {
vditor.preview.element.style.display = "none";
}
enableToolbar(vditor.toolbar.elements, Constants.EDIT_TOOLBARS);
removeCurrentToolbar(vditor.toolbar.elements, Constants.EDIT_TOOLBARS);
disableToolbar(vditor.toolbar.elements, ["outdent", "indent"]);
if (type === "ir") {
hideToolbar(vditor.toolbar.elements, ["both"]);
showToolbar(vditor.toolbar.elements, ["outdent", "indent", "outline", "insert-before", "insert-after"]);
vditor.sv.element.style.display = "none";
vditor.wysiwyg.element.parentElement.style.display = "none";
vditor.ir.element.parentElement.style.display = "block";
vditor.lute.SetVditorIR(true);
vditor.lute.SetVditorWYSIWYG(false);
vditor.lute.SetVditorSV(false);
vditor.currentMode = "ir";
vditor.ir.element.innerHTML = vditor.lute.Md2VditorIRDOM(markdownText);
processAfterRender(vditor, {
enableAddUndoStack: true,
enableHint: false,
enableInput: false,
});
setPadding(vditor);
vditor.ir.element.querySelectorAll(".vditor-ir__preview[data-render='2']").forEach((item: HTMLElement) => {
processCodeRender(item, vditor);
});
vditor.ir.element.querySelectorAll(".vditor-toc").forEach((item: HTMLElement) => {
mathRender(item, {
cdn: vditor.options.cdn,
math: vditor.options.preview.math,
});
});
} else if (type === "wysiwyg") {
hideToolbar(vditor.toolbar.elements, ["both"]);
showToolbar(vditor.toolbar.elements, ["outdent", "indent", "outline", "insert-before", "insert-after"]);
vditor.sv.element.style.display = "none";
vditor.wysiwyg.element.parentElement.style.display = "block";
vditor.ir.element.parentElement.style.display = "none";
vditor.lute.SetVditorIR(false);
vditor.lute.SetVditorWYSIWYG(true);
vditor.lute.SetVditorSV(false);
vditor.currentMode = "wysiwyg";
setPadding(vditor);
renderDomByMd(vditor, markdownText, {
enableAddUndoStack: true,
enableHint: false,
enableInput: false,
});
vditor.wysiwyg.element.querySelectorAll(".vditor-toc").forEach((item: HTMLElement) => {
mathRender(item, {
cdn: vditor.options.cdn,
math: vditor.options.preview.math,
});
});
vditor.wysiwyg.popover.style.display = "none";
} else if (type === "sv") {
showToolbar(vditor.toolbar.elements, ["both"]);
hideToolbar(vditor.toolbar.elements, ["outdent", "indent", "outline", "insert-before", "insert-after"]);
vditor.wysiwyg.element.parentElement.style.display = "none";
vditor.ir.element.parentElement.style.display = "none";
if (vditor.options.preview.mode === "both") {
vditor.sv.element.style.display = "block";
} else if (vditor.options.preview.mode === "editor") {
vditor.sv.element.style.display = "block";
}
vditor.lute.SetVditorIR(false);
vditor.lute.SetVditorWYSIWYG(false);
vditor.lute.SetVditorSV(true);
vditor.currentMode = "sv";
let svHTML = processSpinVditorSVDOM(markdownText, vditor);
if (svHTML === "<div data-block='0'></div>") {
// https://github.com/Vanessa219/vditor/issues/654 SV 模式 Placeholder 显示问题
svHTML = "";
}
vditor.sv.element.innerHTML = svHTML;
combineFootnote(vditor.sv.element)
processSVAfterRender(vditor, {
enableAddUndoStack: true,
enableHint: false,
enableInput: false,
});
setPadding(vditor);
}
vditor.undo.resetIcon(vditor);
if (typeof event !== "string") {
// 初始化不 focus
vditor[vditor.currentMode].element.focus();
highlightToolbar(vditor);
}
renderToc(vditor);
setTypewriterPosition(vditor);
if (vditor.toolbar.elements["edit-mode"]) {
vditor.toolbar.elements["edit-mode"].querySelectorAll("button").forEach((item) => {
item.classList.remove("vditor-menu--current");
});
vditor.toolbar.elements["edit-mode"].querySelector(`button[data-mode="${vditor.currentMode}"]`).classList.add("vditor-menu--current");
}
vditor.outline.toggle(vditor, vditor.currentMode !== "sv" && vditor.options.outline.enable, typeof event !== "string");
};
export class EditMode extends MenuItem {
public element: HTMLElement;
constructor(vditor: IVditor, menuItem: IMenuItem) {
super(vditor, menuItem);
const panelElement = document.createElement("div");
panelElement.className = `vditor-hint${menuItem.level === 2 ? "" : " vditor-panel--arrow"}`;
panelElement.innerHTML = `<button data-mode="wysiwyg">${window.VditorI18n.wysiwyg} &lt;${updateHotkeyTip("⌥⌘7")}></button>
<button data-mode="ir">${window.VditorI18n.instantRendering} &lt;${updateHotkeyTip("⌥⌘8")}></button>
<button data-mode="sv">${window.VditorI18n.splitView} &lt;${updateHotkeyTip("⌥⌘9")}></button>`;
this.element.appendChild(panelElement);
this._bindEvent(vditor, panelElement, menuItem);
}
public _bindEvent(vditor: IVditor, panelElement: HTMLElement, menuItem: IMenuItem) {
const actionBtn = this.element.children[0] as HTMLElement;
toggleSubMenu(vditor, panelElement, actionBtn, menuItem.level);
panelElement.children.item(0).addEventListener(getEventName(), (event: Event) => {
// wysiwyg
setEditMode(vditor, "wysiwyg", event);
event.preventDefault();
event.stopPropagation();
});
panelElement.children.item(1).addEventListener(getEventName(), (event: Event) => {
// ir
setEditMode(vditor, "ir", event);
event.preventDefault();
event.stopPropagation();
});
panelElement.children.item(2).addEventListener(getEventName(), (event: Event) => {
// markdown
setEditMode(vditor, "sv", event);
event.preventDefault();
event.stopPropagation();
});
}
}

View File

@@ -0,0 +1,74 @@
import {getEventName} from "../util/compatibility";
import {execAfterRender} from "../util/fixBrowserBehavior";
import {hasClosestByTag} from "../util/hasClosestByHeadings";
import {getEditorRange, insertHTML, setSelectionFocus} from "../util/selection";
import {MenuItem} from "./MenuItem";
import {toggleSubMenu} from "./setToolbar";
export class Emoji extends MenuItem {
public element: HTMLElement;
constructor(vditor: IVditor, menuItem: IMenuItem) {
super(vditor, menuItem);
const panelElement = document.createElement("div");
panelElement.className = "vditor-panel vditor-panel--arrow";
let commonEmojiHTML = "";
Object.keys(vditor.options.hint.emoji).forEach((key) => {
const emojiValue = vditor.options.hint.emoji[key];
if (emojiValue.indexOf(".") > -1) {
commonEmojiHTML += `<button data-value=":${key}: " data-key=":${key}:"><img
data-value=":${key}: " data-key=":${key}:" class="vditor-emojis__icon" src="${emojiValue}"/></button>`;
} else {
commonEmojiHTML += `<button data-value="${emojiValue} "
data-key="${key}"><span class="vditor-emojis__icon">${emojiValue}</span></button>`;
}
});
panelElement.innerHTML = `<div class="vditor-emojis" style="max-height: ${
vditor.options.height === "auto" ? "auto" : vditor.options.height as number - 80
}px">${commonEmojiHTML}</div><div class="vditor-emojis__tail">
<span class="vditor-emojis__tip"></span><span>${vditor.options.hint.emojiTail || ""}</span>
</div>`;
this.element.appendChild(panelElement);
toggleSubMenu(vditor, panelElement, this.element.firstElementChild, menuItem.level);
this.bindEvent(vditor);
}
private bindEvent(vditor: IVditor) {
this.element.lastElementChild.addEventListener(getEventName(), (event: Event & { target: Element }) => {
const btnElement = hasClosestByTag(event.target, "BUTTON");
if (btnElement) {
event.preventDefault();
const value = btnElement.getAttribute("data-value");
const range = getEditorRange(vditor);
let html = value;
if (vditor.currentMode === "wysiwyg") {
html = vditor.lute.SpinVditorDOM(value);
} else if (vditor.currentMode === "ir") {
html = vditor.lute.SpinVditorIRDOM(value);
}
if (value.indexOf(":") > -1 && vditor.currentMode !== "sv") {
const tempElement = document.createElement("div");
tempElement.innerHTML = html;
html = tempElement.firstElementChild.firstElementChild.outerHTML + " ";
insertHTML(html, vditor);
} else {
range.extractContents();
range.insertNode(document.createTextNode(value));
}
range.collapse(false);
setSelectionFocus(range);
(this.element.lastElementChild as HTMLElement).style.display = "none";
execAfterRender(vditor);
}
});
this.element.lastElementChild.addEventListener("mouseover", (event: Event & { target: Element }) => {
const btnElement = hasClosestByTag(event.target, "BUTTON");
if (btnElement) {
this.element.querySelector(".vditor-emojis__tip").innerHTML = btnElement.getAttribute("data-key");
}
});
}
}

View File

@@ -0,0 +1,41 @@
import {exportHTML, exportMarkdown, exportPDF} from "../export";
import {getEventName} from "../util/compatibility";
import {MenuItem} from "./MenuItem";
import {hidePanel, toggleSubMenu} from "./setToolbar";
export class Export extends MenuItem {
public element: HTMLElement;
constructor(vditor: IVditor, menuItem: IMenuItem) {
super(vditor, menuItem);
const actionBtn = this.element.children[0] as HTMLElement;
const panelElement = document.createElement("div");
panelElement.className = `vditor-hint${menuItem.level === 2 ? "" : " vditor-panel--arrow"}`;
panelElement.innerHTML = `<button data-type="markdown">Markdown</button>
<button data-type="pdf">PDF</button>
<button data-type="html">HTML</button>`;
panelElement.addEventListener(getEventName(), (event: MouseEvent & { target: HTMLElement }) => {
const btnElement = event.target;
if (btnElement.tagName === "BUTTON") {
switch (btnElement.getAttribute("data-type")) {
case "markdown":
exportMarkdown(vditor);
break;
case "pdf":
exportPDF(vditor);
break;
case "html":
exportHTML(vditor);
break;
default:
break;
}
hidePanel(vditor, ["subToolbar"]);
event.preventDefault();
event.stopPropagation();
}
});
this.element.appendChild(panelElement);
toggleSubMenu(vditor, panelElement, actionBtn, menuItem.level);
}
}

View File

@@ -0,0 +1,66 @@
import {setPadding, setTypewriterPosition} from "../ui/initUI";
import {getEventName} from "../util/compatibility";
import {MenuItem} from "./MenuItem";
export class Fullscreen extends MenuItem {
constructor(vditor: IVditor, menuItem: IMenuItem) {
super(vditor, menuItem);
this._bindEvent(vditor, menuItem);
}
public _bindEvent(vditor: IVditor, menuItem: IMenuItem) {
this.element.children[0].addEventListener(getEventName(), function (event) {
event.preventDefault();
if (vditor.element.className.includes("vditor--fullscreen")) {
if (!menuItem.level) {
this.innerHTML = menuItem.icon;
}
vditor.element.style.zIndex = "";
document.body.style.overflow = "";
vditor.element.classList.remove("vditor--fullscreen");
Object.keys(vditor.toolbar.elements).forEach((key) => {
const svgElement = vditor.toolbar.elements[key].firstChild as HTMLElement;
if (svgElement) {
svgElement.className = svgElement.className.replace("__s", "__n");
vditor.options.toolbar.forEach(item => {
if (typeof item !== "string" && item.tipPosition && item.name === svgElement.dataset.type) {
svgElement.className = `vditor-tooltipped vditor-tooltipped__${item.tipPosition}`;
}
})
}
});
if (vditor.counter) {
vditor.counter.element.className = vditor.counter.element.className.replace("__s", "__n");
}
} else {
if (!menuItem.level) {
this.innerHTML = '<svg><use xlink:href="#vditor-icon-contract"></use></svg>';
}
vditor.element.style.zIndex = vditor.options.fullscreen.index.toString();
document.body.style.overflow = "hidden";
vditor.element.classList.add("vditor--fullscreen");
Object.keys(vditor.toolbar.elements).forEach((key) => {
const svgElement = vditor.toolbar.elements[key].firstChild as HTMLElement;
if (svgElement) {
svgElement.className = svgElement.className.replace("__n", "__s");
}
});
if (vditor.counter) {
vditor.counter.element.className = vditor.counter.element.className.replace("__n", "__s");
}
}
if (vditor.devtools) {
vditor.devtools.renderEchart(vditor);
}
if (menuItem.click) {
menuItem.click(event, vditor);
}
setPadding(vditor);
setTypewriterPosition(vditor);
});
}
}

View File

@@ -0,0 +1,73 @@
import {Constants} from "../constants";
import {processHeading} from "../ir/process";
import {processHeading as processHeadingSV} from "../sv/process";
import {getEventName, updateHotkeyTip} from "../util/compatibility";
import {afterRenderEvent} from "../wysiwyg/afterRenderEvent";
import {removeHeading, setHeading} from "../wysiwyg/setHeading";
import {MenuItem} from "./MenuItem";
import {hidePanel} from "./setToolbar";
export class Headings extends MenuItem {
public element: HTMLElement;
constructor(vditor: IVditor, menuItem: IMenuItem) {
super(vditor, menuItem);
const panelElement = document.createElement("div");
panelElement.className = "vditor-hint vditor-panel--arrow";
panelElement.innerHTML = `<button data-tag="h1" data-value="# ">${window.VditorI18n.heading1} ${updateHotkeyTip("&lt;⌥⌘1>")}</button>
<button data-tag="h2" data-value="## ">${window.VditorI18n.heading2} &lt;${updateHotkeyTip("⌥⌘2")}></button>
<button data-tag="h3" data-value="### ">${window.VditorI18n.heading3} &lt;${updateHotkeyTip("⌥⌘3")}></button>
<button data-tag="h4" data-value="#### ">${window.VditorI18n.heading4} &lt;${updateHotkeyTip("⌥⌘4")}></button>
<button data-tag="h5" data-value="##### ">${window.VditorI18n.heading5} &lt;${updateHotkeyTip("⌥⌘5")}></button>
<button data-tag="h6" data-value="###### ">${window.VditorI18n.heading6} &lt;${updateHotkeyTip("⌥⌘6")}></button>`;
this.element.appendChild(panelElement);
this._bindEvent(vditor, panelElement);
}
public _bindEvent(vditor: IVditor, panelElement: HTMLElement) {
const actionBtn = this.element.children[0] as HTMLElement;
actionBtn.addEventListener(getEventName(), (event) => {
event.preventDefault();
// https://github.com/Vanessa219/vditor/issues/1391
clearTimeout(vditor.wysiwyg.afterRenderTimeoutId);
clearTimeout(vditor.ir.processTimeoutId);
clearTimeout(vditor.sv.processTimeoutId);
if (actionBtn.classList.contains(Constants.CLASS_MENU_DISABLED)) {
return;
}
actionBtn.blur();
if (actionBtn.classList.contains("vditor-menu--current")) {
if (vditor.currentMode === "wysiwyg") {
removeHeading(vditor);
afterRenderEvent(vditor);
} else if (vditor.currentMode === "ir") {
processHeading(vditor, "");
}
actionBtn.classList.remove("vditor-menu--current");
} else {
hidePanel(vditor, ["subToolbar"]);
panelElement.style.display = "block";
}
});
for (let i = 0; i < 6; i++) {
panelElement.children.item(i).addEventListener(getEventName(), (event: Event) => {
event.preventDefault();
if (vditor.currentMode === "wysiwyg") {
setHeading(vditor, (event.target as HTMLElement).getAttribute("data-tag"));
afterRenderEvent(vditor);
actionBtn.classList.add("vditor-menu--current");
} else if (vditor.currentMode === "ir") {
processHeading(vditor, (event.target as HTMLElement).getAttribute("data-value"));
actionBtn.classList.add("vditor-menu--current");
} else {
processHeadingSV(vditor, (event.target as HTMLElement).getAttribute("data-value"));
}
panelElement.style.display = "none";
});
}
}
}

View File

@@ -0,0 +1,30 @@
import {getEventName} from "../util/compatibility";
import {MenuItem} from "./MenuItem";
export class Help extends MenuItem {
constructor(vditor: IVditor, menuItem: IMenuItem) {
super(vditor, menuItem);
this.element.children[0].addEventListener(getEventName(), (event) => {
event.preventDefault();
vditor.tip.show(`<div style="margin-bottom:14px;font-size: 14px;line-height: 22px;min-width:300px;max-width: 360px;display: flex;">
<div style="margin-top: 14px;flex: 1">
<div>Markdown 使用指南</div>
<ul style="list-style: none">
<li><a href="https://ld246.com/article/1583308420519" target="_blank">语法速查手册</a></li>
<li><a href="https://ld246.com/article/1583129520165" target="_blank">基础语法</a></li>
<li><a href="https://ld246.com/article/1583305480675" target="_blank">扩展语法</a></li>
<li><a href="https://ld246.com/article/1582778815353" target="_blank">键盘快捷键</a></li>
</ul>
</div>
<div style="margin-top: 14px;flex: 1">
<div>Vditor 支持</div>
<ul style="list-style: none">
<li><a href="https://github.com/Vanessa219/vditor/issues" target="_blank">Issues</a></li>
<li><a href="https://ld246.com/tag/vditor" target="_blank">官方讨论区</a></li>
<li><a href="https://ld246.com/article/1549638745630" target="_blank">开发手册</a></li>
<li><a href="https://ld246.com/guide/markdown" target="_blank">演示地址</a></li>
</ul>
</div></div>`, 0);
});
}
}

View File

@@ -0,0 +1,25 @@
import {Constants} from "../constants";
import {getEventName} from "../util/compatibility";
import {listIndent} from "../util/fixBrowserBehavior";
import {hasClosestByMatchTag} from "../util/hasClosest";
import {getEditorRange} from "../util/selection";
import {MenuItem} from "./MenuItem";
export class Indent extends MenuItem {
constructor(vditor: IVditor, menuItem: IMenuItem) {
super(vditor, menuItem);
this.element.children[0].addEventListener(getEventName(), (event) => {
event.preventDefault();
if (this.element.firstElementChild.classList.contains(Constants.CLASS_MENU_DISABLED) ||
vditor.currentMode === "sv") {
return;
}
const range = getEditorRange(vditor);
const liElement = hasClosestByMatchTag(range.startContainer, "LI");
if (liElement) {
listIndent(vditor, liElement, range);
}
});
}
}

View File

@@ -0,0 +1,43 @@
import {VDITOR_VERSION} from "../constants";
import {getEventName} from "../util/compatibility";
import {MenuItem} from "./MenuItem";
export class Info extends MenuItem {
constructor(vditor: IVditor, menuItem: IMenuItem) {
super(vditor, menuItem);
this.element.children[0].addEventListener(getEventName(), (event) => {
event.preventDefault();
vditor.tip.show(`<div style="max-width: 520px; font-size: 14px;line-height: 22px;margin-bottom: 14px;">
<p style="text-align: center;margin: 14px 0">
<em>下一代的 Markdown 编辑器,为未来而构建</em>
</p>
<div style="display: flex;margin-bottom: 14px;flex-wrap: wrap;align-items: center">
<img src="https://unpkg.com/vditor/dist/images/logo.png" style="margin: 0 auto;height: 68px"/>
<div>&nbsp;&nbsp;</div>
<div style="flex: 1;min-width: 250px">
Vditor 是一款浏览器端的 Markdown 编辑器,支持所见即所得、即时渲染(类似 Typora和分屏预览模式。
它使用 TypeScript 实现,支持原生 JavaScript 以及 Vue、React、Angular 和 Svelte 等框架。
</div>
</div>
<div style="display: flex;flex-wrap: wrap;">
<ul style="list-style: none;flex: 1;min-width:148px">
<li>
项目地址:<a href="https://b3log.org/vditor" target="_blank">b3log.org/vditor</a>
</li>
<li>
开源协议MIT
</li>
</ul>
<ul style="list-style: none;margin-right: 18px">
<li>
组件版本Vditor v${VDITOR_VERSION} / Lute v${Lute.Version}
</li>
<li>
赞助捐赠:<a href="https://ld246.com/sponsor" target="_blank">https://ld246.com/sponsor</a>
</li>
</ul>
</div>
</div>`, 0);
});
}
}

View File

@@ -0,0 +1,18 @@
import {Constants} from "../constants";
import {getEventName} from "../util/compatibility";
import {insertEmptyBlock} from "../util/fixBrowserBehavior";
import {MenuItem} from "./MenuItem";
export class InsertAfter extends MenuItem {
constructor(vditor: IVditor, menuItem: IMenuItem) {
super(vditor, menuItem);
this.element.children[0].addEventListener(getEventName(), (event) => {
event.preventDefault();
if (this.element.firstElementChild.classList.contains(Constants.CLASS_MENU_DISABLED) ||
vditor.currentMode === "sv") {
return;
}
insertEmptyBlock(vditor, "afterend");
});
}
}

View File

@@ -0,0 +1,18 @@
import {Constants} from "../constants";
import {getEventName} from "../util/compatibility";
import {insertEmptyBlock} from "../util/fixBrowserBehavior";
import {MenuItem} from "./MenuItem";
export class InsertBefore extends MenuItem {
constructor(vditor: IVditor, menuItem: IMenuItem) {
super(vditor, menuItem);
this.element.children[0].addEventListener(getEventName(), (event) => {
event.preventDefault();
if (this.element.firstElementChild.classList.contains(Constants.CLASS_MENU_DISABLED) ||
vditor.currentMode === "sv") {
return;
}
insertEmptyBlock(vditor, "beforebegin");
});
}
}

View File

@@ -0,0 +1,54 @@
import {Constants} from "../constants";
import {processToolbar} from "../ir/process";
import {processToolbar as processToolbarSV} from "../sv/process";
import {getEventName} from "../util/compatibility";
import {updateHotkeyTip} from "../util/compatibility";
import {toolbarEvent} from "../wysiwyg/toolbarEvent";
export class MenuItem {
public element: HTMLElement;
constructor(vditor: IVditor, menuItem: IMenuItem) {
this.element = document.createElement("div");
if (menuItem.className) {
this.element.classList.add(...menuItem.className.split(" "));
}
let hotkey = menuItem.hotkey ? ` <${updateHotkeyTip(menuItem.hotkey)}>` : "";
if (menuItem.level === 2) {
hotkey = menuItem.hotkey ? ` &lt;${updateHotkeyTip(menuItem.hotkey)}&gt;` : "";
}
const tip = menuItem.tip ? menuItem.tip + hotkey : `${window.VditorI18n[menuItem.name]}${hotkey}`;
const tagName = menuItem.name === "upload" ? "div" : "button";
if (menuItem.level === 2) {
this.element.innerHTML = `<${tagName} data-type="${menuItem.name}">${tip}</${tagName}>`;
} else {
this.element.classList.add("vditor-toolbar__item");
const iconElement = document.createElement(tagName);
iconElement.setAttribute("data-type", menuItem.name);
iconElement.className = `vditor-tooltipped vditor-tooltipped__${menuItem.tipPosition}`;
iconElement.setAttribute("aria-label", tip);
iconElement.innerHTML = menuItem.icon;
this.element.appendChild(iconElement);
}
if (!menuItem.prefix) {
return;
}
this.element.children[0].addEventListener(getEventName(), (event) => {
event.preventDefault();
if (this.element.firstElementChild.classList.contains(Constants.CLASS_MENU_DISABLED)) {
return;
}
if (vditor.currentMode === "wysiwyg") {
toolbarEvent(vditor, this.element.children[0], event);
} else if (vditor.currentMode === "ir") {
processToolbar(vditor, this.element.children[0],
menuItem.prefix || "", menuItem.suffix || "");
} else {
processToolbarSV(vditor, this.element.children[0],
menuItem.prefix || "", menuItem.suffix || "");
}
});
}
}

View File

@@ -0,0 +1,24 @@
import {Constants} from "../constants";
import {getEventName} from "../util/compatibility";
import { listOutdent} from "../util/fixBrowserBehavior";
import {hasClosestByMatchTag} from "../util/hasClosest";
import {getEditorRange} from "../util/selection";
import {MenuItem} from "./MenuItem";
export class Outdent extends MenuItem {
constructor(vditor: IVditor, menuItem: IMenuItem) {
super(vditor, menuItem);
this.element.children[0].addEventListener(getEventName(), (event) => {
event.preventDefault();
if (this.element.firstElementChild.classList.contains(Constants.CLASS_MENU_DISABLED) ||
vditor.currentMode === "sv") {
return;
}
const range = getEditorRange(vditor);
const liElement = hasClosestByMatchTag(range.startContainer, "LI");
if (liElement) {
listOutdent(vditor, liElement, range, liElement.parentElement);
}
});
}
}

View File

@@ -0,0 +1,21 @@
import {Constants} from "../constants";
import {getEventName} from "../util/compatibility";
import {MenuItem} from "./MenuItem";
export class Outline extends MenuItem {
constructor(vditor: IVditor, menuItem: IMenuItem) {
super(vditor, menuItem);
if (vditor.options.outline) {
this.element.firstElementChild.classList.add("vditor-menu--current");
}
this.element.children[0].addEventListener(getEventName(), (event) => {
event.preventDefault();
const btnElement = vditor.toolbar.elements.outline.firstElementChild;
if (btnElement.classList.contains(Constants.CLASS_MENU_DISABLED)) {
return;
}
vditor.options.outline.enable = !this.element.firstElementChild.classList.contains("vditor-menu--current");
vditor.outline.toggle(vditor, vditor.options.outline.enable);
});
}
}

View File

@@ -0,0 +1,55 @@
import {Constants} from "../constants";
import {setPadding} from "../ui/initUI";
import {getEventName} from "../util/compatibility";
import {MenuItem} from "./MenuItem";
import {disableToolbar, enableToolbar, hidePanel} from "./setToolbar";
export class Preview extends MenuItem {
constructor(vditor: IVditor, menuItem: IMenuItem) {
super(vditor, menuItem);
this._bindEvent(vditor);
}
public _bindEvent(vditor: IVditor) {
this.element.children[0].addEventListener(getEventName(), (event) => {
event.preventDefault();
const btnElement = this.element.firstElementChild;
if (btnElement.classList.contains(Constants.CLASS_MENU_DISABLED)) {
return;
}
const toolbars = Constants.EDIT_TOOLBARS.concat(["both", "edit-mode", "devtools"]);
if (btnElement.classList.contains("vditor-menu--current")) {
btnElement.classList.remove("vditor-menu--current");
if (vditor.currentMode === "sv") {
vditor.sv.element.style.display = "block";
if (vditor.options.preview.mode === "both") {
vditor.preview.element.style.display = "block";
} else {
vditor.preview.element.style.display = "none";
}
} else {
vditor[vditor.currentMode].element.parentElement.style.display = "block";
vditor.preview.element.style.display = "none";
}
enableToolbar(vditor.toolbar.elements, toolbars);
vditor.outline.render(vditor);
} else {
disableToolbar(vditor.toolbar.elements, toolbars);
vditor.preview.element.style.display = "block";
if (vditor.currentMode === "sv") {
vditor.sv.element.style.display = "none";
} else {
vditor[vditor.currentMode].element.parentElement.style.display = "none";
}
vditor.preview.render(vditor);
btnElement.classList.add("vditor-menu--current");
hidePanel(vditor, ["subToolbar", "hint", "popover"]);
setTimeout(() => {
vditor.outline.render(vditor);
}, vditor.options.preview.delay + 10);
}
setPadding(vditor);
});
}
}

View File

@@ -0,0 +1,60 @@
import {Constants} from "../constants";
import {uploadFiles} from "../upload/index";
import {getEventName} from "../util/compatibility";
import {RecordMedia} from "../util/RecordMedia";
import {MenuItem} from "./MenuItem";
export class Record extends MenuItem {
constructor(vditor: IVditor, menuItem: IMenuItem) {
super(vditor, menuItem);
this._bindEvent(vditor);
}
public _bindEvent(vditor: IVditor) {
let mediaRecorder: RecordMedia;
this.element.children[0].addEventListener(getEventName(), (event) => {
event.preventDefault();
if (this.element.firstElementChild.classList.contains(Constants.CLASS_MENU_DISABLED)) {
return;
}
const editorElement = vditor[vditor.currentMode].element;
if (!mediaRecorder) {
navigator.mediaDevices.getUserMedia({audio: true}).then((mediaStream: MediaStream) => {
mediaRecorder = new RecordMedia(mediaStream);
mediaRecorder.recorder.onaudioprocess = (e: AudioProcessingEvent) => {
// Do nothing if not recording:
if (!mediaRecorder.isRecording) {
return;
}
// Copy the data from the input buffers;
const left = e.inputBuffer.getChannelData(0);
const right = e.inputBuffer.getChannelData(1);
mediaRecorder.cloneChannelData(left, right);
};
mediaRecorder.startRecordingNewWavFile();
vditor.tip.show(window.VditorI18n.recording);
editorElement.setAttribute("contenteditable", "false");
this.element.children[0].classList.add("vditor-menu--current");
}).catch(() => {
vditor.tip.show(window.VditorI18n["record-tip"]);
});
return;
}
if (mediaRecorder.isRecording) {
mediaRecorder.stopRecording();
vditor.tip.hide();
const file: File = new File([mediaRecorder.buildWavFileBlob()],
`record${(new Date()).getTime()}.wav`, {type: "video/webm"});
uploadFiles(vditor, [file]);
this.element.children[0].classList.remove("vditor-menu--current");
} else {
vditor.tip.show(window.VditorI18n.recording);
editorElement.setAttribute("contenteditable", "false");
mediaRecorder.startRecordingNewWavFile();
this.element.children[0].classList.add("vditor-menu--current");
}
});
}
}

View File

@@ -0,0 +1,18 @@
import {Constants} from "../constants";
import {getEventName} from "../util/compatibility";
import {MenuItem} from "./MenuItem";
import {disableToolbar} from "./setToolbar";
export class Redo extends MenuItem {
constructor(vditor: IVditor, menuItem: IMenuItem) {
super(vditor, menuItem);
disableToolbar({redo: this.element}, ["redo"]);
this.element.children[0].addEventListener(getEventName(), (event) => {
event.preventDefault();
if (this.element.firstElementChild.classList.contains(Constants.CLASS_MENU_DISABLED)) {
return;
}
vditor.undo.redo(vditor);
});
}
}

View File

@@ -0,0 +1,18 @@
import {Constants} from "../constants";
import {getEventName} from "../util/compatibility";
import {MenuItem} from "./MenuItem";
import {disableToolbar} from "./setToolbar";
export class Undo extends MenuItem {
constructor(vditor: IVditor, menuItem: IMenuItem) {
super(vditor, menuItem);
disableToolbar({undo: this.element}, ["undo"]);
this.element.children[0].addEventListener(getEventName(), (event) => {
event.preventDefault();
if (this.element.firstElementChild.classList.contains(Constants.CLASS_MENU_DISABLED)) {
return;
}
vditor.undo.undo(vditor);
});
}
}

View File

@@ -0,0 +1,41 @@
import {Constants} from "../constants";
import {uploadFiles} from "../upload/index";
import {getEventName} from "../util/compatibility";
import {MenuItem} from "./MenuItem";
export class Upload extends MenuItem {
constructor(vditor: IVditor, menuItem: IMenuItem) {
super(vditor, menuItem);
let inputHTML = '<input type="file"';
if (vditor.options.upload.multiple) {
inputHTML += ' multiple="multiple"';
}
if (vditor.options.upload.accept) {
inputHTML += ` accept="${vditor.options.upload.accept}"`;
}
this.element.children[0].innerHTML = `${(menuItem.icon || '<svg><use xlink:href="#vditor-icon-upload"></use></svg>')}${inputHTML}>`;
this._bindEvent(vditor);
}
public _bindEvent(vditor: IVditor) {
this.element.children[0].addEventListener(getEventName(), (event) => {
if (this.element.firstElementChild.classList.contains(Constants.CLASS_MENU_DISABLED)) {
event.stopPropagation();
event.preventDefault();
return;
}
});
this.element.querySelector("input").addEventListener("change",
(event: InputEvent & { target: HTMLInputElement }) => {
if (this.element.firstElementChild.classList.contains(Constants.CLASS_MENU_DISABLED)) {
event.stopPropagation();
event.preventDefault();
return;
}
if (event.target.files.length === 0) {
return;
}
uploadFiles(vditor, event.target.files, event.target);
});
}
}

View File

@@ -0,0 +1,192 @@
import {getEventName} from "../util/compatibility";
import {Both} from "./Both";
import {Br} from "./Br";
import {CodeTheme} from "./CodeTheme";
import {ContentTheme} from "./ContentTheme";
import {Counter} from "./Counter";
import {Custom} from "./Custom";
import {Devtools} from "./Devtools";
import {Divider} from "./Divider";
import {EditMode} from "./EditMode";
import {Emoji} from "./Emoji";
import {Export} from "./Export";
import {Fullscreen} from "./Fullscreen";
import {Headings} from "./Headings";
import {Help} from "./Help";
import {Indent} from "./Indent";
import {Info} from "./Info";
import {InsertAfter} from "./InsertAfter";
import {InsertBefore} from "./InsertBefore";
import {MenuItem} from "./MenuItem";
import {Outdent} from "./Outdent";
import {Outline} from "./Outline";
import {Preview} from "./Preview";
import {Record} from "./Record";
import {Redo} from "./Redo";
import {toggleSubMenu} from "./setToolbar";
import {Undo} from "./Undo";
import {Upload} from "./Upload";
export class Toolbar {
public elements: { [key: string]: HTMLElement };
public element: HTMLElement;
constructor(vditor: IVditor) {
const options = vditor.options;
this.elements = {};
this.element = document.createElement("div");
this.element.className = "vditor-toolbar";
options.toolbar.forEach((menuItem: IMenuItem, i: number) => {
const itemElement = this.genItem(vditor, menuItem, i);
this.element.appendChild(itemElement);
if (menuItem.toolbar) {
const panelElement = document.createElement("div");
panelElement.className = "vditor-hint vditor-panel--arrow";
panelElement.addEventListener(getEventName(), (event) => {
panelElement.style.display = "none";
});
menuItem.toolbar.forEach((subMenuItem: IMenuItem, subI: number) => {
subMenuItem.level = 2;
panelElement.appendChild(this.genItem(vditor, subMenuItem, i + subI));
});
itemElement.appendChild(panelElement);
toggleSubMenu(vditor, panelElement, itemElement.children[0], 2);
}
});
if (vditor.options.toolbarConfig.hide) {
this.element.classList.add("vditor-toolbar--hide");
}
if (vditor.options.toolbarConfig.pin) {
this.element.classList.add("vditor-toolbar--pin");
}
if (vditor.options.counter.enable) {
vditor.counter = new Counter(vditor);
this.element.appendChild(vditor.counter.element);
}
}
public updateConfig(vditor: IVditor, options: IToolbarConfig) {
vditor.options.toolbarConfig = Object.assign({
hide: false,
pin: false,
}, options);
if (vditor.options.toolbarConfig.hide) {
this.element.classList.add("vditor-toolbar--hide");
} else {
this.element.classList.remove("vditor-toolbar--hide");
}
if (vditor.options.toolbarConfig.pin) {
this.element.classList.add("vditor-toolbar--pin");
} else {
this.element.classList.remove("vditor-toolbar--pin");
}
}
private genItem(vditor: IVditor, menuItem: IMenuItem, index: number) {
let menuItemObj;
switch (menuItem.name) {
case "bold":
case "italic":
case "more":
case "strike":
case "line":
case "quote":
case "list":
case "ordered-list":
case "check":
case "code":
case "inline-code":
case "link":
case "table":
menuItemObj = new MenuItem(vditor, menuItem);
break;
case "emoji":
menuItemObj = new Emoji(vditor, menuItem);
break;
case "headings":
menuItemObj = new Headings(vditor, menuItem);
break;
case "|":
menuItemObj = new Divider();
break;
case "br":
menuItemObj = new Br();
break;
case "undo":
menuItemObj = new Undo(vditor, menuItem);
break;
case "redo":
menuItemObj = new Redo(vditor, menuItem);
break;
case "help":
menuItemObj = new Help(vditor, menuItem);
break;
case "both":
menuItemObj = new Both(vditor, menuItem);
break;
case "preview":
menuItemObj = new Preview(vditor, menuItem);
break;
case "fullscreen":
menuItemObj = new Fullscreen(vditor, menuItem);
break;
case "upload":
menuItemObj = new Upload(vditor, menuItem);
break;
case "record":
menuItemObj = new Record(vditor, menuItem);
break;
case "info":
menuItemObj = new Info(vditor, menuItem);
break;
case "edit-mode":
menuItemObj = new EditMode(vditor, menuItem);
break;
case "devtools":
menuItemObj = new Devtools(vditor, menuItem);
break;
case "outdent":
menuItemObj = new Outdent(vditor, menuItem);
break;
case "indent":
menuItemObj = new Indent(vditor, menuItem);
break;
case "outline":
menuItemObj = new Outline(vditor, menuItem);
break;
case "insert-after":
menuItemObj = new InsertAfter(vditor, menuItem);
break;
case "insert-before":
menuItemObj = new InsertBefore(vditor, menuItem);
break;
case "code-theme":
menuItemObj = new CodeTheme(vditor, menuItem);
break;
case "content-theme":
menuItemObj = new ContentTheme(vditor, menuItem);
break;
case "export":
menuItemObj = new Export(vditor, menuItem);
break;
default:
menuItemObj = new Custom(vditor, menuItem);
break;
}
if (!menuItemObj) {
return;
}
let key = menuItem.name;
if (key === "br" || key === "|") {
key = key + index;
}
this.elements[key] = menuItemObj.element;
return menuItemObj.element;
}
}

View File

@@ -0,0 +1,120 @@
import {Constants} from "../constants";
import {getEventName} from "../util/compatibility";
export const removeCurrentToolbar = (toolbar: { [key: string]: HTMLElement }, names: string[]) => {
names.forEach((name) => {
if (!toolbar[name]) {
return;
}
const itemElement = toolbar[name].children[0];
if (itemElement && itemElement.classList.contains("vditor-menu--current")) {
itemElement.classList.remove("vditor-menu--current");
}
});
};
export const setCurrentToolbar = (toolbar: { [key: string]: HTMLElement }, names: string[]) => {
names.forEach((name) => {
if (!toolbar[name]) {
return;
}
const itemElement = toolbar[name].children[0];
if (itemElement && !itemElement.classList.contains("vditor-menu--current")) {
itemElement.classList.add("vditor-menu--current");
}
});
};
export const enableToolbar = (toolbar: { [key: string]: HTMLElement }, names: string[]) => {
names.forEach((name) => {
if (!toolbar[name]) {
return;
}
const itemElement = toolbar[name].children[0];
if (itemElement && itemElement.classList.contains(Constants.CLASS_MENU_DISABLED)) {
itemElement.classList.remove(Constants.CLASS_MENU_DISABLED);
}
});
};
export const disableToolbar = (toolbar: { [key: string]: HTMLElement }, names: string[]) => {
names.forEach((name) => {
if (!toolbar[name]) {
return;
}
const itemElement = toolbar[name].children[0];
if (itemElement && !itemElement.classList.contains(Constants.CLASS_MENU_DISABLED)) {
itemElement.classList.add(Constants.CLASS_MENU_DISABLED);
}
});
};
export const hideToolbar = (toolbar: { [key: string]: HTMLElement }, names: string[]) => {
names.forEach((name) => {
if (!toolbar[name]) {
return;
}
if (toolbar[name]) {
toolbar[name].style.display = "none";
}
});
};
export const showToolbar = (toolbar: { [key: string]: HTMLElement }, names: string[]) => {
names.forEach((name) => {
if (!toolbar[name]) {
return;
}
if (toolbar[name]) {
toolbar[name].style.display = "block";
}
});
};
// "subToolbar", "hint", "popover"
export const hidePanel = (vditor: IVditor, panels: string[], exceptElement?: HTMLElement) => {
if (panels.includes("subToolbar")) {
vditor.toolbar.element.querySelectorAll(".vditor-hint").forEach((item: HTMLElement) => {
if (exceptElement && item.isEqualNode(exceptElement)) {
return;
}
item.style.display = "none";
});
if (vditor.toolbar.elements.emoji) {
(vditor.toolbar.elements.emoji.lastElementChild as HTMLElement).style.display = "none";
}
}
if (panels.includes("hint")) {
vditor.hint.element.style.display = "none";
}
if (vditor.wysiwyg.popover && panels.includes("popover")) {
vditor.wysiwyg.popover.style.display = "none";
}
};
export const toggleSubMenu = (vditor: IVditor, panelElement: HTMLElement, actionBtn: Element, level: number) => {
actionBtn.addEventListener(getEventName(), (event) => {
event.preventDefault();
event.stopPropagation();
if (actionBtn.classList.contains(Constants.CLASS_MENU_DISABLED)) {
return;
}
vditor.toolbar.element.querySelectorAll(".vditor-hint--current").forEach((item) => {
item.classList.remove("vditor-hint--current");
});
if (panelElement.style.display === "block") {
panelElement.style.display = "none";
} else {
hidePanel(vditor, ["subToolbar", "hint", "popover"], actionBtn.parentElement.parentElement);
if (!actionBtn.classList.contains("vditor-tooltipped")) {
actionBtn.classList.add("vditor-hint--current");
}
panelElement.style.display = "block";
if (vditor.toolbar.element.getBoundingClientRect().right - actionBtn.getBoundingClientRect().right < 250) {
panelElement.classList.add("vditor-panel--left");
} else {
panelElement.classList.remove("vditor-panel--left");
}
}
});
};

View File

@@ -0,0 +1,180 @@
import {Constants} from "../constants";
import {setEditMode} from "../toolbar/EditMode";
import {hidePanel} from "../toolbar/setToolbar";
import {accessLocalStorage} from "../util/compatibility";
import {setContentTheme} from "./setContentTheme";
import {setTheme} from "./setTheme";
export const initUI = (vditor: IVditor) => {
vditor.element.innerHTML = "";
vditor.element.classList.add("vditor");
// 支持 RTL
if (vditor.options.rtl) {
vditor.element.setAttribute("dir", "rtl")
}
setTheme(vditor);
setContentTheme(vditor.options.preview.theme.current, vditor.options.preview.theme.path);
if (typeof vditor.options.height === "number") {
vditor.element.style.height = vditor.options.height + "px";
} else {
vditor.element.style.height = vditor.options.height;
}
if (typeof vditor.options.minHeight === "number") {
vditor.element.style.minHeight = vditor.options.minHeight + "px";
}
if (typeof vditor.options.width === "number") {
vditor.element.style.width = vditor.options.width + "px";
} else {
vditor.element.style.width = vditor.options.width;
}
vditor.element.appendChild(vditor.toolbar.element);
const contentElement = document.createElement("div");
contentElement.className = "vditor-content";
if (vditor.options.outline.position === "left") {
contentElement.appendChild(vditor.outline.element);
}
contentElement.appendChild(vditor.wysiwyg.element.parentElement);
contentElement.appendChild(vditor.sv.element);
contentElement.appendChild(vditor.ir.element.parentElement);
contentElement.appendChild(vditor.preview.element);
if (vditor.toolbar.elements.devtools) {
contentElement.appendChild(vditor.devtools.element);
}
if (vditor.options.outline.position === "right") {
vditor.outline.element.classList.add("vditor-outline--right");
contentElement.appendChild(vditor.outline.element);
}
if (vditor.upload) {
contentElement.appendChild(vditor.upload.element);
}
if (vditor.options.resize.enable) {
contentElement.appendChild(vditor.resize.element);
}
contentElement.appendChild(vditor.hint.element);
contentElement.appendChild(vditor.tip.element);
vditor.element.appendChild(contentElement);
contentElement.addEventListener("click", () => {
hidePanel(vditor, ["subToolbar"]);
});
if (vditor.toolbar.elements.export) {
// for export pdf
vditor.element.insertAdjacentHTML("beforeend",
'<iframe id="vditorExportIframe" style="width: 100%;height: 0;border: 0"></iframe>');
}
setEditMode(vditor, vditor.options.mode, afterRender(vditor));
document.execCommand("DefaultParagraphSeparator", false, "p");
if (navigator.userAgent.indexOf("iPhone") > -1 && typeof window.visualViewport !== "undefined") {
// https://github.com/Vanessa219/vditor/issues/379
let pendingUpdate = false;
const viewportHandler = (event: Event) => {
if (pendingUpdate) {
return;
}
pendingUpdate = true;
requestAnimationFrame(() => {
pendingUpdate = false;
const layoutViewport = vditor.toolbar.element;
layoutViewport.style.transform = "none";
if (layoutViewport.getBoundingClientRect().top < 0) {
layoutViewport.style.transform = `translate(0, ${-layoutViewport.getBoundingClientRect().top}px)`;
}
});
};
window.visualViewport.addEventListener("scroll", viewportHandler);
window.visualViewport.addEventListener("resize", viewportHandler);
}
};
export const setPadding = (vditor: IVditor) => {
const minPadding = window.innerWidth <= Constants.MOBILE_WIDTH ? 10 : 35;
if (vditor.wysiwyg.element.parentElement.style.display !== "none") {
const padding = (vditor.wysiwyg.element.parentElement.clientWidth
- vditor.options.preview.maxWidth) / 2;
vditor.wysiwyg.element.style.padding = `10px ${Math.max(minPadding, padding)}px`;
}
if (vditor.ir.element.parentElement.style.display !== "none") {
const padding = (vditor.ir.element.parentElement.clientWidth
- vditor.options.preview.maxWidth) / 2;
vditor.ir.element.style.padding = `10px ${Math.max(minPadding, padding)}px`;
}
if (vditor.preview.element.style.display !== "block") {
vditor.toolbar.element.style.paddingLeft = Math.max(5,
parseInt(vditor[vditor.currentMode].element.style.paddingLeft || "0", 10) +
(vditor.options.outline.position === "left" ? vditor.outline.element.offsetWidth : 0)) + "px";
} else {
vditor.toolbar.element.style.paddingLeft = (5 +
(vditor.options.outline.position === "left" ? vditor.outline.element.offsetWidth : 0)) + "px";
}
};
export const setTypewriterPosition = (vditor: IVditor) => {
if (!vditor.options.typewriterMode) {
return;
}
let height: number = window.innerHeight;
if (typeof vditor.options.height === "number") {
height = vditor.options.height;
if (typeof vditor.options.minHeight === "number") {
height = Math.max(height, vditor.options.minHeight);
}
height = Math.min(window.innerHeight, height);
} else {
height = vditor.element.clientHeight;
}
if (vditor.element.classList.contains("vditor--fullscreen")) {
height = window.innerHeight;
}
// 由于 Firefox padding-bottom bug只能使用 :after
vditor[vditor.currentMode].element.style.setProperty("--editor-bottom",
((height - vditor.toolbar.element.offsetHeight) / 2) + "px");
};
let resizeCb: () => void;
export function UIUnbindListener() {
window.removeEventListener("resize", resizeCb);
}
const afterRender = (vditor: IVditor) => {
setTypewriterPosition(vditor);
UIUnbindListener();
window.addEventListener("resize", resizeCb = () => {
setPadding(vditor);
setTypewriterPosition(vditor);
});
// set default value
let initValue = accessLocalStorage() && localStorage.getItem(vditor.options.cache.id);
if (!vditor.options.cache.enable || !initValue) {
if (vditor.options.value) {
initValue = vditor.options.value;
} else if (vditor.originalInnerHTML) {
initValue = vditor.lute.HTML2Md(vditor.originalInnerHTML);
} else if (!vditor.options.cache.enable) {
initValue = "";
}
}
return initValue || "";
};

View File

@@ -0,0 +1,16 @@
import {Constants} from "../constants";
import {addStyle} from "../util/addStyle";
export const setCodeTheme = (codeTheme: string, cdn = Constants.CDN) => {
if (!Constants.CODE_THEME.includes(codeTheme)) {
codeTheme = "github";
}
const vditorHljsStyle = document.getElementById("vditorHljsStyle") as HTMLLinkElement;
const href = `${cdn}/dist/js/highlight.js/styles/${codeTheme}.css`;
if (!vditorHljsStyle) {
addStyle(href, "vditorHljsStyle");
} else if (vditorHljsStyle.getAttribute('href') !== href) {
vditorHljsStyle.remove();
addStyle(href, "vditorHljsStyle");
}
};

View File

@@ -0,0 +1,15 @@
import {addStyle} from "../util/addStyle";
export const setContentTheme = (contentTheme: string, path: string) => {
if (!contentTheme || !path) {
return;
}
const vditorContentTheme = document.getElementById("vditorContentTheme") as HTMLLinkElement;
const cssPath = `${path}/${contentTheme}.css`;
if (!vditorContentTheme) {
addStyle(cssPath, "vditorContentTheme");
} else if (vditorContentTheme.getAttribute("href") !== cssPath) {
vditorContentTheme.remove();
addStyle(cssPath, "vditorContentTheme");
}
};

View File

@@ -0,0 +1,31 @@
import {removeCurrentToolbar} from "../toolbar/setToolbar";
import {setCurrentToolbar} from "../toolbar/setToolbar";
export const setPreviewMode = (mode: "both" | "editor", vditor: IVditor) => {
if (vditor.options.preview.mode === mode) {
return;
}
vditor.options.preview.mode = mode;
switch (mode) {
case "both":
vditor.sv.element.style.display = "block";
vditor.preview.element.style.display = "block";
vditor.preview.render(vditor);
setCurrentToolbar(vditor.toolbar.elements, ["both"]);
break;
case "editor":
vditor.sv.element.style.display = "block";
vditor.preview.element.style.display = "none";
removeCurrentToolbar(vditor.toolbar.elements, ["both"]);
break;
default:
break;
}
if (vditor.devtools) {
vditor.devtools.renderEchart(vditor);
}
};

View File

@@ -0,0 +1,7 @@
export const setTheme = (vditor: IVditor) => {
if (vditor.options.theme === "dark") {
vditor.element.classList.add("vditor--dark");
} else {
vditor.element.classList.remove("vditor--dark");
}
};

View File

@@ -0,0 +1,260 @@
import * as DiffMatchPatch from "diff-match-patch";
import {disableToolbar, enableToolbar, hidePanel} from "../toolbar/setToolbar";
import {isFirefox, isSafari} from "../util/compatibility";
import {scrollCenter} from "../util/editorCommonEvent";
import {execAfterRender} from "../util/fixBrowserBehavior";
import {highlightToolbar} from "../util/highlightToolbar";
import {processCodeRender} from "../util/processCode";
import {setRangeByWbr, setSelectionFocus} from "../util/selection";
import {renderToc} from "../util/toc";
interface IUndo {
hasUndo: boolean;
lastText: string;
redoStack: DiffMatchPatch.patch_obj[][];
undoStack: DiffMatchPatch.patch_obj[][];
}
class Undo {
private stackSize = 50;
private dmp: DiffMatchPatch.diff_match_patch;
private wysiwyg: IUndo;
private ir: IUndo;
private sv: IUndo;
constructor() {
this.resetStack();
// @ts-ignore
this.dmp = new DiffMatchPatch();
}
public clearStack(vditor: IVditor) {
this.resetStack();
this.resetIcon(vditor);
}
public resetIcon(vditor: IVditor) {
if (!vditor.toolbar) {
return;
}
if (this[vditor.currentMode].undoStack.length > 1) {
enableToolbar(vditor.toolbar.elements, ["undo"]);
} else {
disableToolbar(vditor.toolbar.elements, ["undo"]);
}
if (this[vditor.currentMode].redoStack.length !== 0) {
enableToolbar(vditor.toolbar.elements, ["redo"]);
} else {
disableToolbar(vditor.toolbar.elements, ["redo"]);
}
}
public undo(vditor: IVditor) {
if (vditor[vditor.currentMode].element.getAttribute("contenteditable") === "false") {
return;
}
if (this[vditor.currentMode].undoStack.length < 2) {
return;
}
const state = this[vditor.currentMode].undoStack.pop();
if (!state) {
return;
}
this[vditor.currentMode].redoStack.push(state);
this.renderDiff(state, vditor);
this[vditor.currentMode].hasUndo = true;
// undo 操作后,需要关闭 hint
hidePanel(vditor, ["hint"]);
}
public redo(vditor: IVditor) {
if (vditor[vditor.currentMode].element.getAttribute("contenteditable") === "false") {
return;
}
const state = this[vditor.currentMode].redoStack.pop();
if (!state) {
return;
}
this[vditor.currentMode].undoStack.push(state);
this.renderDiff(state, vditor, true);
}
public recordFirstPosition(vditor: IVditor, event: KeyboardEvent) {
if (getSelection().rangeCount === 0) {
return;
}
if (this[vditor.currentMode].undoStack.length !== 1 || this[vditor.currentMode].undoStack[0].length === 0 ||
this[vditor.currentMode].redoStack.length > 0) {
return;
}
if (isFirefox() && event.key === "Backspace") {
// Firefox 第一次删除无效
return;
}
if (isSafari()) {
// Safari keydown 在 input 之后,不需要重复记录历史
return;
}
const text = this.addCaret(vditor);
if (text.replace("<wbr>", "").replace(" vditor-ir__node--expand", "")
!== this[vditor.currentMode].undoStack[0][0].diffs[0][1].replace("<wbr>", "")) {
// 当还不没有存入 undo 栈时,按下 ctrl 后会覆盖 lastText
return;
}
this[vditor.currentMode].undoStack[0][0].diffs[0][1] = text;
this[vditor.currentMode].lastText = text;
// 不能添加 setSelectionFocus(cloneRange); 否则 windows chrome 首次输入会烂
}
public addToUndoStack(vditor: IVditor) {
// afterRenderEvent.ts 已经 debounce
const text = this.addCaret(vditor, true);
const diff = this.dmp.diff_main(text, this[vditor.currentMode].lastText, true);
const patchList = this.dmp.patch_make(text, this[vditor.currentMode].lastText, diff);
if (patchList.length === 0 && this[vditor.currentMode].undoStack.length > 0) {
return;
}
this[vditor.currentMode].lastText = text;
this[vditor.currentMode].undoStack.push(patchList);
if (this[vditor.currentMode].undoStack.length > this.stackSize) {
this[vditor.currentMode].undoStack.shift();
}
if (this[vditor.currentMode].hasUndo) {
this[vditor.currentMode].redoStack = [];
this[vditor.currentMode].hasUndo = false;
disableToolbar(vditor.toolbar.elements, ["redo"]);
}
if (this[vditor.currentMode].undoStack.length > 1) {
enableToolbar(vditor.toolbar.elements, ["undo"]);
}
}
private renderDiff(state: DiffMatchPatch.patch_obj[], vditor: IVditor, isRedo: boolean = false) {
let text;
if (isRedo) {
const redoPatchList = this.dmp.patch_deepCopy(state).reverse();
redoPatchList.forEach((patch) => {
patch.diffs.forEach((diff) => {
diff[0] = -diff[0];
});
});
text = this.dmp.patch_apply(redoPatchList, this[vditor.currentMode].lastText)[0];
} else {
text = this.dmp.patch_apply(state, this[vditor.currentMode].lastText)[0];
}
this[vditor.currentMode].lastText = text;
vditor[vditor.currentMode].element.innerHTML = text;
if (vditor.currentMode !== "sv") {
vditor[vditor.currentMode].element.querySelectorAll(`.vditor-${vditor.currentMode}__preview[data-render='2']`)
.forEach((blockElement: HTMLElement) => {
processCodeRender(blockElement, vditor);
});
}
if (!vditor[vditor.currentMode].element.querySelector("wbr")) {
// Safari 第一次输入没有光标,需手动定位到结尾
const range = getSelection().getRangeAt(0);
range.setEndBefore(vditor[vditor.currentMode].element);
range.collapse(false);
} else {
setRangeByWbr(
vditor[vditor.currentMode].element, vditor[vditor.currentMode].element.ownerDocument.createRange());
scrollCenter(vditor);
}
renderToc(vditor);
execAfterRender(vditor, {
enableAddUndoStack: false,
enableHint: false,
enableInput: true,
});
highlightToolbar(vditor);
vditor[vditor.currentMode].element.querySelectorAll(`.vditor-${vditor.currentMode}__preview[data-render='2']`)
.forEach((item: HTMLElement) => {
processCodeRender(item, vditor);
});
if (this[vditor.currentMode].undoStack.length > 1) {
enableToolbar(vditor.toolbar.elements, ["undo"]);
} else {
disableToolbar(vditor.toolbar.elements, ["undo"]);
}
if (this[vditor.currentMode].redoStack.length !== 0) {
enableToolbar(vditor.toolbar.elements, ["redo"]);
} else {
disableToolbar(vditor.toolbar.elements, ["redo"]);
}
}
private resetStack() {
this.ir = {
hasUndo: false,
lastText: "",
redoStack: [],
undoStack: [],
};
this.sv = {
hasUndo: false,
lastText: "",
redoStack: [],
undoStack: [],
};
this.wysiwyg = {
hasUndo: false,
lastText: "",
redoStack: [],
undoStack: [],
};
}
private addCaret(vditor: IVditor, setFocus = false) {
let cloneRange: Range;
if (getSelection().rangeCount !== 0 && !vditor[vditor.currentMode].element.querySelector("wbr")) {
const range = getSelection().getRangeAt(0);
if (vditor[vditor.currentMode].element.contains(range.startContainer)) {
cloneRange = range.cloneRange();
const wbrElement = document.createElement("span");
wbrElement.className = "vditor-wbr";
range.insertNode(wbrElement);
}
}
// 移除数学公式、echart 渲染 https://github.com/siyuan-note/siyuan/issues/537
const cloneElement = vditor.ir.element.cloneNode(true) as HTMLElement;
cloneElement.querySelectorAll(`.vditor-${vditor.currentMode}__preview[data-render='1']`)
.forEach((item: HTMLElement) => {
if (!item.firstElementChild) {
return
}
if (item.firstElementChild.classList.contains("language-echarts") ||
item.firstElementChild.classList.contains("language-plantuml") ||
item.firstElementChild.classList.contains("language-mindmap")) {
item.firstElementChild.removeAttribute("_echarts_instance_");
item.firstElementChild.removeAttribute("data-processed");
item.firstElementChild.innerHTML = item.previousElementSibling.firstElementChild.innerHTML;
item.setAttribute("data-render", "2");
} else if (item.firstElementChild.classList.contains("language-math")) {
item.setAttribute("data-render", "2");
item.firstElementChild.textContent = item.firstElementChild.getAttribute("data-math");
item.firstElementChild.removeAttribute("data-math");
}
});
const text = vditor[vditor.currentMode].element.innerHTML;
vditor[vditor.currentMode].element.querySelectorAll(".vditor-wbr").forEach((item) => {
item.remove();
// 使用 item.outerHTML = "" 会产生 https://github.com/Vanessa219/vditor/pull/686;
});
if (setFocus && cloneRange) {
setSelectionFocus(cloneRange);
}
return text.replace('<span class="vditor-wbr"></span>', "<wbr>");
}
}
export {Undo};

View File

@@ -0,0 +1,10 @@
export const getElement = (vditor: IVditor) => {
switch (vditor.currentMode) {
case "ir":
return vditor.ir.element;
case "wysiwyg":
return vditor.wysiwyg.element;
case "sv":
return vditor.sv.element;
}
};

View File

@@ -0,0 +1,260 @@
import {getEditorRange, setSelectionFocus} from "../util/selection";
import {getElement} from "./getElement";
import {setHeaders} from "./setHeaders";
class Upload {
public element: HTMLElement;
public isUploading: boolean;
public range: Range;
constructor() {
this.isUploading = false;
this.element = document.createElement("div");
this.element.className = "vditor-upload";
}
}
const validateFile = (vditor: IVditor, files: File[]) => {
vditor.tip.hide();
const uploadFileList = [];
let errorTip = "";
let uploadingStr = "";
const lang: keyof II18n | "" = vditor.options.lang;
const options: IOptions = vditor.options;
for (let iMax = files.length, i = 0; i < iMax; i++) {
const file = files[i];
let validate = true;
if (!file.name) {
errorTip += `<li>${window.VditorI18n.nameEmpty}</li>`;
validate = false;
}
if (file.size > vditor.options.upload.max) {
errorTip += `<li>${file.name} ${window.VditorI18n.over} ${vditor.options.upload.max / 1024 / 1024}M</li>`;
validate = false;
}
const lastIndex = file.name.lastIndexOf(".");
const fileExt = file.name.substr(lastIndex);
const filename = vditor.options.upload.filename(file.name.substr(0, lastIndex)) + fileExt;
if (vditor.options.upload.accept) {
const isAccept = vditor.options.upload.accept.split(",").some((item) => {
const type = item.trim();
if (type.indexOf(".") === 0) {
if (fileExt.toLowerCase() === type.toLowerCase()) {
return true;
}
} else {
if (file.type.split("/")[0] === type.split("/")[0]) {
return true;
}
}
return false;
});
if (!isAccept) {
errorTip += `<li>${file.name} ${window.VditorI18n.fileTypeError}</li>`;
validate = false;
}
}
if (validate) {
uploadFileList.push(file);
uploadingStr += `<li>${filename} ${window.VditorI18n.uploading}</li>`;
}
}
vditor.tip.show(`<ul>${errorTip}${uploadingStr}</ul>`);
return uploadFileList;
};
const genUploadedLabel = (responseText: string, vditor: IVditor) => {
const editorElement = getElement(vditor);
editorElement.focus();
const response = JSON.parse(responseText);
let errorTip = "";
if (response.code === 1) {
errorTip = `${response.msg}`;
}
if (response.data.errFiles && response.data.errFiles.length > 0) {
errorTip = `<ul><li>${errorTip}</li>`;
response.data.errFiles.forEach((data: string) => {
const lastIndex = data.lastIndexOf(".");
const filename = vditor.options.upload.filename(data.substr(0, lastIndex)) + data.substr(lastIndex);
errorTip += `<li>${filename} ${window.VditorI18n.uploadError}</li>`;
});
errorTip += "</ul>";
}
if (errorTip) {
vditor.tip.show(errorTip);
} else {
vditor.tip.hide();
}
let succFileText = "";
Object.keys(response.data.succMap).forEach((key) => {
const path = response.data.succMap[key];
const lastIndex = key.lastIndexOf(".");
let type = key.substr(lastIndex);
const filename = vditor.options.upload.filename(key.substr(0, lastIndex)) + type;
type = type.toLowerCase();
if (type.indexOf(".wav") === 0 || type.indexOf(".mp3") === 0 || type.indexOf(".ogg") === 0) {
if (vditor.currentMode === "wysiwyg") {
succFileText += `<div class="vditor-wysiwyg__block" data-type="html-block"
data-block="0"><pre><code>&lt;audio controls="controls" src="${path}"&gt;&lt;/audio&gt;</code></pre><pre class="vditor-wysiwyg__preview" data-render="1"><audio controls="controls" src="${path}"></audio></pre></div>\n`;
} else if (vditor.currentMode === "ir") {
succFileText += `<audio controls="controls" src="${path}"></audio>\n`;
} else {
succFileText += `[${filename}](${path})\n`;
}
} else if (type.indexOf(".apng") === 0
|| type.indexOf(".bmp") === 0
|| type.indexOf(".gif") === 0
|| type.indexOf(".ico") === 0 || type.indexOf(".cur") === 0
|| type.indexOf(".jpg") === 0 || type.indexOf(".jpeg") === 0 || type.indexOf(".jfif") === 0 || type.indexOf(".pjp") === 0 || type.indexOf(".pjpeg") === 0
|| type.indexOf(".png") === 0
|| type.indexOf(".svg") === 0
|| type.indexOf(".webp") === 0) {
if (vditor.currentMode === "wysiwyg") {
succFileText += `<img alt="${filename}" src="${path}">\n`;
} else {
succFileText += `![${filename}](${path})\n`;
}
} else {
if (vditor.currentMode === "wysiwyg") {
succFileText += `<a href="${path}">${filename}</a>\n`;
} else {
succFileText += `[${filename}](${path})\n`;
}
}
});
setSelectionFocus(vditor.upload.range);
document.execCommand("insertHTML", false, succFileText);
vditor.upload.range = getSelection().getRangeAt(0).cloneRange();
};
const uploadFiles =
async (vditor: IVditor, files: FileList | DataTransferItemList | File[], element?: HTMLInputElement) => {
// FileList | DataTransferItemList | File[] => File[]
let fileList = [];
const filesMax = vditor.options.upload.multiple === true ? files.length : 1;
for (let i = 0; i < filesMax; i++) {
let fileItem = files[i];
if (fileItem instanceof DataTransferItem) {
fileItem = fileItem.getAsFile();
}
fileList.push(fileItem);
}
if (vditor.options.upload.handler) {
const isValidate = await vditor.options.upload.handler(fileList);
if (element) {
element.value = "";
}
if (typeof isValidate === "string") {
vditor.tip.show(isValidate);
return;
}
return;
}
if (!vditor.options.upload.url || !vditor.upload) {
if (element) {
element.value = "";
}
vditor.tip.show("please config: options.upload.url");
return;
}
if (vditor.options.upload.file) {
fileList = await vditor.options.upload.file(fileList);
}
if (vditor.options.upload.validate) {
const isValidate = vditor.options.upload.validate(fileList);
if (typeof isValidate === "string") {
vditor.tip.show(isValidate);
return;
}
}
const editorElement = getElement(vditor);
vditor.upload.range = getEditorRange(vditor);
const validateResult = validateFile(vditor, fileList);
if (validateResult.length === 0) {
if (element) {
element.value = "";
}
return;
}
const formData = new FormData();
const extraData = vditor.options.upload.extraData;
for (const key of Object.keys(extraData)) {
formData.append(key, extraData[key]);
}
for (let i = 0, iMax = validateResult.length; i < iMax; i++) {
formData.append(vditor.options.upload.fieldName, validateResult[i]);
}
const xhr = new XMLHttpRequest();
xhr.open("POST", vditor.options.upload.url);
if (vditor.options.upload.token) {
xhr.setRequestHeader("X-Upload-Token", vditor.options.upload.token);
}
if (vditor.options.upload.withCredentials) {
xhr.withCredentials = true;
}
setHeaders(vditor, xhr);
vditor.upload.isUploading = true;
editorElement.setAttribute("contenteditable", "false");
xhr.onreadystatechange = () => {
if (xhr.readyState === XMLHttpRequest.DONE) {
vditor.upload.isUploading = false;
editorElement.setAttribute("contenteditable", "true");
if (xhr.status >= 200 && xhr.status < 300) {
if (vditor.options.upload.success) {
vditor.options.upload.success(editorElement, xhr.responseText);
} else {
let responseText = xhr.responseText;
if (vditor.options.upload.format) {
responseText = vditor.options.upload.format(files as File [], xhr.responseText);
}
genUploadedLabel(responseText, vditor);
}
} else {
if (vditor.options.upload.error) {
vditor.options.upload.error(xhr.responseText);
} else {
vditor.tip.show(xhr.responseText);
}
}
if (element) {
element.value = "";
}
vditor.upload.element.style.display = "none";
}
};
xhr.upload.onprogress = (event: ProgressEvent) => {
if (!event.lengthComputable) {
return;
}
const progress = event.loaded / event.total * 100;
vditor.upload.element.style.display = "block";
const progressBar = vditor.upload.element;
progressBar.style.width = progress + "%";
};
xhr.send(formData);
};
export {Upload, uploadFiles};

View File

@@ -0,0 +1,10 @@
export const setHeaders = (vditor: IVditor, xhr: XMLHttpRequest) => {
if (vditor.options.upload.setHeaders) {
vditor.options.upload.headers = vditor.options.upload.setHeaders();
}
if (vditor.options.upload.headers) {
Object.keys(vditor.options.upload.headers).forEach((key) => {
xhr.setRequestHeader(key, vditor.options.upload.headers[key]);
});
}
};

View File

@@ -0,0 +1,454 @@
import {Constants} from "../constants";
import {merge} from "./merge";
export class Options {
public options: IOptions;
private defaultOptions: IOptions = {
rtl: false,
after: undefined,
cache: {
enable: true,
},
cdn: Constants.CDN,
classes: {
preview: "",
},
comment: {
enable: false,
},
counter: {
enable: false,
type: "markdown",
},
customRenders: [],
debugger: false,
fullscreen: {
index: 90,
},
height: "auto",
hint: {
delay: 200,
emoji: {
"+1": "👍",
"-1": "👎",
"confused": "😕",
"eyes": "👀️",
"heart": "❤️",
"rocket": "🚀️",
"smile": "😄",
"tada": "🎉️",
},
emojiPath: `${Constants.CDN}/dist/images/emoji`,
extend: [],
parse: true,
},
icon: "ant",
lang: "zh_CN",
mode: "ir",
outline: {
enable: false,
position: "left",
},
placeholder: "",
preview: {
actions: ["desktop", "tablet", "mobile", "mp-wechat", "zhihu"],
delay: 1000,
hljs: Constants.HLJS_OPTIONS,
markdown: Constants.MARKDOWN_OPTIONS,
math: Constants.MATH_OPTIONS,
maxWidth: 800,
mode: "both",
theme: Constants.THEME_OPTIONS,
render: {
media: {
enable: true,
}
}
},
link: {
isOpen: true,
},
image: {
isPreview: true,
},
resize: {
enable: false,
position: "bottom",
},
theme: "classic",
toolbar: [
"emoji",
"headings",
"bold",
"italic",
"strike",
"link",
"|",
"list",
"ordered-list",
"check",
"outdent",
"indent",
"|",
"quote",
"line",
"code",
"inline-code",
"insert-before",
"insert-after",
"|",
"upload",
"record",
"table",
"|",
"undo",
"redo",
"|",
"fullscreen",
"edit-mode",
{
name: "more",
toolbar: [
"both",
"code-theme",
"content-theme",
"export",
"outline",
"preview",
"devtools",
"info",
"help",
],
},
],
toolbarConfig: {
hide: false,
pin: false,
},
typewriterMode: false,
undoDelay: 800,
upload: {
extraData: {},
fieldName: "file[]",
filename: (name: string) => name.replace(/\W/g, ""),
linkToImgUrl: "",
max: 10 * 1024 * 1024,
multiple: true,
url: "",
withCredentials: false,
},
value: "",
width: "auto",
};
constructor(options: IOptions) {
this.options = options;
}
public merge(): IOptions {
if (this.options) {
if (this.options.toolbar) {
this.options.toolbar = this.mergeToolbar(this.options.toolbar);
} else {
this.options.toolbar = this.mergeToolbar(this.defaultOptions.toolbar);
}
if (this.options.preview?.theme?.list) {
this.defaultOptions.preview.theme.list = this.options.preview.theme.list;
}
if (this.options.preview?.render?.media?.enable) {
this.defaultOptions.preview.render.media.enable = this.options.preview.render.media.enable;
}
if (this.options.hint?.emoji) {
this.defaultOptions.hint.emoji = this.options.hint.emoji;
}
if (this.options.comment) {
this.defaultOptions.comment = this.options.comment;
}
if (this.options.cdn) {
if (!this.options.preview?.theme?.path) {
this.defaultOptions.preview.theme.path = `${this.options.cdn}/dist/css/content-theme`
}
if (!this.options.hint?.emojiPath) {
this.defaultOptions.hint.emojiPath = `${this.options.cdn}/dist/images/emoji`;
}
}
}
const mergedOptions = merge(this.defaultOptions, this.options);
if (mergedOptions.cache.enable && !mergedOptions.cache.id) {
throw new Error(
"need options.cache.id, see https://ld246.com/article/1549638745630#options",
);
}
return mergedOptions;
}
private mergeToolbar(toolbar: Array<string | IMenuItem>) {
const toolbarItem = [
{
icon: '<svg><use xlink:href="#vditor-icon-export"></use></svg>',
name: "export",
tipPosition: "ne",
},
{
hotkey: "⌘E",
icon: '<svg><use xlink:href="#vditor-icon-emoji"></use></svg>',
name: "emoji",
tipPosition: "ne",
},
{
hotkey: "⌘H",
icon:
'<svg><use xlink:href="#vditor-icon-headings"></use></svg>',
name: "headings",
tipPosition: "ne",
},
{
hotkey: "⌘B",
icon: '<svg><use xlink:href="#vditor-icon-bold"></use></svg>',
name: "bold",
prefix: "**",
suffix: "**",
tipPosition: "ne",
},
{
hotkey: "⌘I",
icon: '<svg><use xlink:href="#vditor-icon-italic"></use></svg>',
name: "italic",
prefix: "*",
suffix: "*",
tipPosition: "ne",
},
{
hotkey: "⌘D",
icon: '<svg><use xlink:href="#vditor-icon-strike"></use></svg>',
name: "strike",
prefix: "~~",
suffix: "~~",
tipPosition: "ne",
},
{
hotkey: "⌘K",
icon: '<svg><use xlink:href="#vditor-icon-link"></use></svg>',
name: "link",
prefix: "[",
suffix: "](https://)",
tipPosition: "n",
},
{
name: "|",
},
{
hotkey: "⌘L",
icon: '<svg><use xlink:href="#vditor-icon-list"></use></svg>',
name: "list",
prefix: "* ",
tipPosition: "n",
},
{
hotkey: "⌘O",
icon:
'<svg><use xlink:href="#vditor-icon-ordered-list"></use></svg>',
name: "ordered-list",
prefix: "1. ",
tipPosition: "n",
},
{
hotkey: "⌘J",
icon: '<svg><use xlink:href="#vditor-icon-check"></use></svg>',
name: "check",
prefix: "* [ ] ",
tipPosition: "n",
},
{
hotkey: "⇧⌘I",
icon:
'<svg><use xlink:href="#vditor-icon-outdent"></use></svg>',
name: "outdent",
tipPosition: "n",
},
{
hotkey: "⇧⌘O",
icon: '<svg><use xlink:href="#vditor-icon-indent"></use></svg>',
name: "indent",
tipPosition: "n",
},
{
name: "|",
},
{
hotkey: "⌘;",
icon: '<svg><use xlink:href="#vditor-icon-quote"></use></svg>',
name: "quote",
prefix: "> ",
tipPosition: "n",
},
{
hotkey: "⇧⌘H",
icon: '<svg><use xlink:href="#vditor-icon-line"></use></svg>',
name: "line",
prefix: "---",
tipPosition: "n",
},
{
hotkey: "⌘U",
icon: '<svg><use xlink:href="#vditor-icon-code"></use></svg>',
name: "code",
prefix: "```",
suffix: "\n```",
tipPosition: "n",
},
{
hotkey: "⌘G",
icon:
'<svg><use xlink:href="#vditor-icon-inline-code"></use></svg>',
name: "inline-code",
prefix: "`",
suffix: "`",
tipPosition: "n",
},
{
hotkey: "⇧⌘B",
icon: '<svg><use xlink:href="#vditor-icon-before"></use></svg>',
name: "insert-before",
tipPosition: "n",
},
{
hotkey: "⇧⌘E",
icon: '<svg><use xlink:href="#vditor-icon-after"></use></svg>',
name: "insert-after",
tipPosition: "n",
},
{
name: "|",
},
{
icon: '<svg><use xlink:href="#vditor-icon-upload"></use></svg>',
name: "upload",
tipPosition: "n",
},
{
icon: '<svg><use xlink:href="#vditor-icon-record"></use></svg>',
name: "record",
tipPosition: "n",
},
{
hotkey: "⌘M",
icon: '<svg><use xlink:href="#vditor-icon-table"></use></svg>',
name: "table",
prefix: "| col1",
suffix:
" | col2 | col3 |\n| --- | --- | --- |\n| | | |\n| | | |",
tipPosition: "n",
},
{
name: "|",
},
{
hotkey: "⌘Z",
icon: '<svg><use xlink:href="#vditor-icon-undo"></use></svg>',
name: "undo",
tipPosition: "nw",
},
{
hotkey: "⌘Y",
icon: '<svg><use xlink:href="#vditor-icon-redo"></use></svg>',
name: "redo",
tipPosition: "nw",
},
{
name: "|",
},
{
icon: '<svg><use xlink:href="#vditor-icon-more"></use></svg>',
name: "more",
tipPosition: "e",
},
{
hotkey: "⌘'",
icon:
'<svg><use xlink:href="#vditor-icon-fullscreen"></use></svg>',
name: "fullscreen",
tipPosition: "nw",
},
{
icon: '<svg><use xlink:href="#vditor-icon-edit"></use></svg>',
name: "edit-mode",
tipPosition: "nw",
},
{
hotkey: "⌘P",
icon: '<svg><use xlink:href="#vditor-icon-both"></use></svg>',
name: "both",
tipPosition: "nw",
},
{
icon:
'<svg><use xlink:href="#vditor-icon-preview"></use></svg>',
name: "preview",
tipPosition: "nw",
},
{
icon:
'<svg><use xlink:href="#vditor-icon-align-center"></use></svg>',
name: "outline",
tipPosition: "nw",
},
{
icon: '<svg><use xlink:href="#vditor-icon-theme"></use></svg>',
name: "content-theme",
tipPosition: "nw",
},
{
icon:
'<svg><use xlink:href="#vditor-icon-code-theme"></use></svg>',
name: "code-theme",
tipPosition: "nw",
},
{
icon: '<svg><use xlink:href="#vditor-icon-bug"></use></svg>',
name: "devtools",
tipPosition: "nw",
},
{
icon: '<svg><use xlink:href="#vditor-icon-info"></use></svg>',
name: "info",
tipPosition: "nw",
},
{
icon: '<svg><use xlink:href="#vditor-icon-help"></use></svg>',
name: "help",
tipPosition: "nw",
},
{
name: "br",
},
];
const toolbarResult: IMenuItem[] = [];
toolbar.forEach((menuItem: IMenuItem) => {
let currentMenuItem = menuItem;
toolbarItem.forEach((defaultMenuItem: IMenuItem) => {
if (
typeof menuItem === "string" &&
defaultMenuItem.name === menuItem
) {
currentMenuItem = defaultMenuItem;
}
if (
typeof menuItem === "object" &&
defaultMenuItem.name === menuItem.name
) {
currentMenuItem = Object.assign({}, defaultMenuItem, menuItem);
}
});
if (menuItem.toolbar) {
currentMenuItem.toolbar = this.mergeToolbar(menuItem.toolbar);
}
toolbarResult.push(currentMenuItem);
});
return toolbarResult;
}
}

View File

@@ -0,0 +1,170 @@
export class RecordMedia {
public SAMPLE_RATE = 5000; // 44100 suggested by demos;
public DEFAULT_SAMPLE_RATE: number;
public isRecording = false;
public readyFlag = false;
public leftChannel: Float32List[] = [];
public rightChannel: Float32List[] = [];
public recordingLength = 0;
// This needs to be public so the 'onaudioprocess' event handler can be defined externally.
public recorder: ScriptProcessorNode;
constructor(e: MediaStream) {
let context;
// creates the audio context
if (typeof AudioContext !== "undefined") {
context = new AudioContext();
} else if (webkitAudioContext) {
context = new webkitAudioContext();
} else {
return;
}
this.DEFAULT_SAMPLE_RATE = context.sampleRate;
// creates a gain node
const volume = context.createGain();
// creates an audio node from the microphone incoming stream
const audioInput = context.createMediaStreamSource(e);
// connect the stream to the gain node
audioInput.connect(volume);
/* From the spec: The size of the buffer controls how frequently the audioprocess event is
dispatched and how many sample-frames need to be processed each call.
Lower values for buffer size will result in a lower (better) latency.
Higher values will be necessary to avoid audio breakup and glitches */
this.recorder = context.createScriptProcessor(2048, 2, 1);
// The onaudioprocess event needs to be defined externally, so make sure it is not set:
this.recorder.onaudioprocess = null;
// we connect the recorder
volume.connect(this.recorder);
this.recorder.connect(context.destination);
this.readyFlag = true;
}
// Publicly accessible methods:
public cloneChannelData(leftChannelData: Float32List, rightChannelData: Float32List) {
this.leftChannel.push(new Float32Array(leftChannelData));
this.rightChannel.push(new Float32Array(rightChannelData));
this.recordingLength += 2048;
}
public startRecordingNewWavFile() {
if (this.readyFlag) {
this.isRecording = true;
this.leftChannel.length = this.rightChannel.length = 0;
this.recordingLength = 0;
}
}
public stopRecording() {
this.isRecording = false;
}
public buildWavFileBlob() {
// we flat the left and right channels down
const leftBuffer = this.mergeBuffers(this.leftChannel);
const rightBuffer = this.mergeBuffers(this.rightChannel);
// Interleave the left and right channels together:
let interleaved: Float32Array = new Float32Array(leftBuffer.length);
for (let i = 0; i < leftBuffer.length; ++i) {
interleaved[i] = 0.5 * (leftBuffer[i] + rightBuffer[i]);
}
// Downsample the audio data if necessary:
if (this.DEFAULT_SAMPLE_RATE > this.SAMPLE_RATE) {
interleaved = this.downSampleBuffer(interleaved, this.SAMPLE_RATE);
}
const totalByteCount = (44 + interleaved.length * 2);
const buffer = new ArrayBuffer(totalByteCount);
const view = new DataView(buffer);
// Build the RIFF chunk descriptor:
this.writeUTFBytes(view, 0, "RIFF");
view.setUint32(4, totalByteCount, true);
this.writeUTFBytes(view, 8, "WAVE");
// Build the FMT sub-chunk:
this.writeUTFBytes(view, 12, "fmt "); // subchunk1 ID is format
view.setUint32(16, 16, true); // The sub-chunk size is 16.
view.setUint16(20, 1, true); // The audio format is 1.
view.setUint16(22, 1, true); // Number of interleaved channels.
view.setUint32(24, this.SAMPLE_RATE, true); // Sample rate.
view.setUint32(28, this.SAMPLE_RATE * 2, true); // Byte rate.
view.setUint16(32, 2, true); // Block align
view.setUint16(34, 16, true); // Bits per sample.
// Build the data sub-chunk:
const subChunk2ByteCount = interleaved.length * 2;
this.writeUTFBytes(view, 36, "data");
view.setUint32(40, subChunk2ByteCount, true);
// Write the PCM samples to the view:
const lng = interleaved.length;
let index = 44;
const volume = 1;
for (let j = 0; j < lng; j++) {
view.setInt16(index, interleaved[j] * (0x7FFF * volume), true);
index += 2;
}
return new Blob([view], {type: "audio/wav"});
}
private downSampleBuffer(buffer: Float32Array, rate: number) {
if (rate === this.DEFAULT_SAMPLE_RATE) {
return buffer;
}
if (rate > this.DEFAULT_SAMPLE_RATE) {
// throw "downsampling rate show be smaller than original sample rate";
return buffer;
}
const sampleRateRatio = this.DEFAULT_SAMPLE_RATE / rate;
const newLength = Math.round(buffer.length / sampleRateRatio);
const result = new Float32Array(newLength);
let offsetResult = 0;
let offsetBuffer = 0;
while (offsetResult < result.length) {
const nextOffsetBuffer = Math.round((offsetResult + 1) * sampleRateRatio);
let accum = 0;
let count = 0;
for (let i = offsetBuffer; i < nextOffsetBuffer && i < buffer.length; i++) {
accum += buffer[i];
count++;
}
result[offsetResult] = accum / count;
offsetResult++;
offsetBuffer = nextOffsetBuffer;
}
return result;
}
private mergeBuffers(desiredChannelBuffer: Float32List[]) {
const result = new Float32Array(this.recordingLength);
let offset = 0;
const lng = desiredChannelBuffer.length;
for (let i = 0; i < lng; ++i) {
const buffer = desiredChannelBuffer[i];
result.set(buffer, offset);
offset += buffer.length;
}
return result;
}
private writeUTFBytes(view: DataView, offset: number, value: string) {
const lng = value.length;
for (let i = 0; i < lng; i++) {
view.setUint8(offset + i, value.charCodeAt(i));
}
}
}

View File

@@ -0,0 +1,43 @@
export const addScriptSync = (path: string, id: string) => {
if (document.getElementById(id)) {
return false;
}
const xhrObj = new XMLHttpRequest();
xhrObj.open("GET", path, false);
xhrObj.setRequestHeader("Accept",
"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript, */*; q=0.01");
xhrObj.send("");
const scriptElement = document.createElement("script");
scriptElement.type = "text/javascript";
scriptElement.text = xhrObj.responseText;
scriptElement.id = id;
document.head.appendChild(scriptElement);
};
export const addScript = (path: string, id: string) => {
return new Promise((resolve, reject) => {
if (document.getElementById(id)) {
// 脚本加载后再次调用直接返回
resolve(true);
return false;
}
const scriptElement = document.createElement("script");
scriptElement.src = path;
scriptElement.async = true;
// 循环调用时 Chrome 不会重复请求 js
document.head.appendChild(scriptElement);
scriptElement.onerror = (event) => {
reject(event);
}
scriptElement.onload = () => {
if (document.getElementById(id)) {
// 循环调用需清除 DOM 中的 script 标签
scriptElement.remove();
resolve(true);
return false;
}
scriptElement.id = id;
resolve(true);
};
});
};

View File

@@ -0,0 +1,10 @@
export const addStyle = (url: string, id: string) => {
if (!document.getElementById(id)) {
const styleElement = document.createElement("link");
styleElement.id = id;
styleElement.rel = "stylesheet";
styleElement.type = "text/css";
styleElement.href = url;
document.getElementsByTagName("head")[0].appendChild(styleElement);
}
};

View File

@@ -0,0 +1,4 @@
export const code160to32 = (text: string) => {
// 非打断空格转换为空格
return text.replace(/\u00a0/g, " ");
};

View File

@@ -0,0 +1,67 @@
export const isSafari = () => {
return navigator.userAgent.indexOf("Safari") > -1 && navigator.userAgent.indexOf("Chrome") === -1;
};
export const isFirefox = () => {
return navigator.userAgent.toLowerCase().indexOf("firefox") > -1;
};
export const accessLocalStorage = () => {
try {
return typeof localStorage !== "undefined";
} catch (e) {
return false;
}
};
// 用户 iPhone 点击延迟/需要双击的处理
export const getEventName = () => {
if (navigator.userAgent.indexOf("iPhone") > -1) {
return "touchstart";
} else {
return "click";
}
};
// 区别 mac 上的 ctrl 和 meta
export const isCtrl = (event: KeyboardEvent) => {
if (navigator.platform.toUpperCase().indexOf("MAC") >= 0) {
// mac
if (event.metaKey && !event.ctrlKey) {
return true;
}
return false;
} else {
if (!event.metaKey && event.ctrlKey) {
return true;
}
return false;
}
};
// MacWindows 快捷键展示
export const updateHotkeyTip = (hotkey: string) => {
if (/Mac/.test(navigator.platform) || navigator.platform === "iPhone") {
if (hotkey.indexOf("⇧") > -1 && isFirefox()) {
// Mac Firefox 按下 shift 后key 同 windows 系统
hotkey = hotkey.replace(";", ":").replace("=", "+").replace("-", "_");
}
} else {
if (hotkey.startsWith("⌘")) {
hotkey = hotkey.replace("⌘", "⌘+");
} else if (hotkey.startsWith("⌥") && hotkey.substr(1, 1) !== "⌘") {
hotkey = hotkey.replace("⌥", "⌥+");
} else {
hotkey = hotkey.replace("⇧⌘", "⌘+⇧+").replace("⌥⌘", "⌥+⌘+");
}
hotkey = hotkey.replace("⌘", "Ctrl").replace("⇧", "Shift")
.replace("⌥", "Alt");
if (hotkey.indexOf("Shift") > -1) {
hotkey = hotkey.replace(";", ":").replace("=", "+").replace("-", "_");
}
}
return hotkey;
};
export const isChrome = () => {
return /Chrome/.test(navigator.userAgent) && /Google Inc/.test(navigator.vendor);
};

View File

@@ -0,0 +1,261 @@
import {Constants} from "../constants";
import {processHeading} from "../ir/process";
import {processKeydown as irProcessKeydown} from "../ir/processKeydown";
import {getMarkdown} from "../markdown/getMarkdown";
import {previewImage} from "../preview/image";
import {processHeading as processHeadingSV} from "../sv/process";
import {processKeydown as mdProcessKeydown} from "../sv/processKeydown";
import {setEditMode} from "../toolbar/EditMode";
import {hidePanel} from "../toolbar/setToolbar";
import {afterRenderEvent} from "../wysiwyg/afterRenderEvent";
import {processKeydown} from "../wysiwyg/processKeydown";
import {removeHeading, setHeading} from "../wysiwyg/setHeading";
import {getEventName, isCtrl} from "./compatibility";
import {execAfterRender, paste} from "./fixBrowserBehavior";
import {getSelectText} from "./getSelectText";
import {hasClosestByAttribute, hasClosestByMatchTag} from "./hasClosest";
import {matchHotKey} from "./hotKey";
import {getCursorPosition, getEditorRange} from "./selection";
export const focusEvent = (vditor: IVditor, editorElement: HTMLElement) => {
editorElement.addEventListener("focus", () => {
if (vditor.options.focus) {
vditor.options.focus(getMarkdown(vditor));
}
hidePanel(vditor, ["subToolbar", "hint"]);
});
};
export const dblclickEvent = (vditor: IVditor, editorElement: HTMLElement) => {
editorElement.addEventListener("dblclick", (event: MouseEvent & { target: HTMLElement }) => {
if (event.target.tagName === "IMG") {
if (vditor.options.image.preview) {
vditor.options.image.preview(event.target);
} else if (vditor.options.image.isPreview) {
previewImage(event.target as HTMLImageElement, vditor.options.lang, vditor.options.theme);
}
}
});
};
export const blurEvent = (vditor: IVditor, editorElement: HTMLElement) => {
editorElement.addEventListener("blur", (event) => {
if (vditor.currentMode === "ir") {
const expandElement = vditor.ir.element.querySelector(".vditor-ir__node--expand");
if (expandElement) {
expandElement.classList.remove("vditor-ir__node--expand");
}
} else if (vditor.currentMode === "wysiwyg" &&
!vditor.wysiwyg.selectPopover.contains(event.relatedTarget as HTMLElement)) {
vditor.wysiwyg.hideComment();
}
vditor[vditor.currentMode].range = getEditorRange(vditor);
if (vditor.options.blur) {
vditor.options.blur(getMarkdown(vditor));
}
});
};
export const dropEvent = (vditor: IVditor, editorElement: HTMLElement) => {
editorElement.addEventListener("dragstart", (event) => {
// 选中编辑器中的文字进行拖拽
event.dataTransfer.setData(Constants.DROP_EDITOR, Constants.DROP_EDITOR);
});
editorElement.addEventListener("drop",
(event: ClipboardEvent & { dataTransfer?: DataTransfer, target: HTMLElement }) => {
if (event.dataTransfer.getData(Constants.DROP_EDITOR)) {
// 编辑器内选中文字拖拽
execAfterRender(vditor);
} else if (event.dataTransfer.types.includes("Files") || event.dataTransfer.types.includes("text/html")) {
// 外部文件拖入编辑器中或者编辑器内选中文字拖拽
paste(vditor, event, {
pasteCode: (code: string) => {
document.execCommand("insertHTML", false, code);
},
});
}
});
};
export const copyEvent =
(vditor: IVditor, editorElement: HTMLElement, copy: (event: ClipboardEvent, vditor: IVditor) => void) => {
editorElement.addEventListener("copy", (event: ClipboardEvent) => copy(event, vditor));
};
export const cutEvent =
(vditor: IVditor, editorElement: HTMLElement, copy: (event: ClipboardEvent, vditor: IVditor) => void) => {
editorElement.addEventListener("cut", (event: ClipboardEvent) => {
copy(event, vditor);
// 获取 comment
if (vditor.options.comment.enable && vditor.currentMode === "wysiwyg") {
vditor.wysiwyg.getComments(vditor);
}
document.execCommand("delete");
});
};
export const scrollCenter = (vditor: IVditor) => {
if (vditor.currentMode === "wysiwyg" && vditor.options.comment.enable) {
vditor.options.comment.adjustTop(vditor.wysiwyg.getComments(vditor, true));
}
if (!vditor.options.typewriterMode) {
return;
}
const editorElement = vditor[vditor.currentMode].element;
const cursorTop = getCursorPosition(editorElement).top;
if (vditor.options.height === "auto" && !vditor.element.classList.contains("vditor--fullscreen")) {
window.scrollTo(window.scrollX,
cursorTop + vditor.element.offsetTop + vditor.toolbar.element.offsetHeight - window.innerHeight / 2 + 10);
}
if (vditor.options.height !== "auto" || vditor.element.classList.contains("vditor--fullscreen")) {
editorElement.scrollTop = cursorTop + editorElement.scrollTop - editorElement.clientHeight / 2 + 10;
}
};
export const hotkeyEvent = (vditor: IVditor, editorElement: HTMLElement) => {
editorElement.addEventListener("keydown", (event: KeyboardEvent & { target: HTMLElement }) => {
if (!event.isComposing && vditor.options.keydown) {
vditor.options.keydown(event);
}
// hint: 上下选择
if ((vditor.options.hint.extend.length > 1 || vditor.toolbar.elements.emoji) &&
vditor.hint.select(event, vditor)) {
return;
}
// 重置 comment
if (vditor.options.comment.enable && vditor.currentMode === "wysiwyg" &&
(event.key === "Backspace" || matchHotKey("⌘X", event))) {
vditor.wysiwyg.getComments(vditor);
}
if (vditor.currentMode === "sv") {
if (mdProcessKeydown(vditor, event)) {
return;
}
} else if (vditor.currentMode === "wysiwyg") {
if (processKeydown(vditor, event)) {
return;
}
} else if (vditor.currentMode === "ir") {
if (irProcessKeydown(vditor, event)) {
return;
}
}
if (vditor.options.ctrlEnter && matchHotKey("⌘Enter", event)) {
vditor.options.ctrlEnter(getMarkdown(vditor));
event.preventDefault();
return;
}
// undo
if (matchHotKey("⌘Z", event) && !vditor.toolbar.elements.undo) {
vditor.undo.undo(vditor);
event.preventDefault();
return;
}
// redo
if (matchHotKey("⌘Y", event) && !vditor.toolbar.elements.redo) {
vditor.undo.redo(vditor);
event.preventDefault();
return;
}
// esc
if (event.key === "Escape") {
if (vditor.hint.element.style.display === "block") {
vditor.hint.element.style.display = "none";
} else if (vditor.options.esc && !event.isComposing) {
vditor.options.esc(getMarkdown(vditor));
}
event.preventDefault();
return;
}
// h1 - h6 hotkey
if (isCtrl(event) && event.altKey && !event.shiftKey && /^Digit[1-6]$/.test(event.code)) {
if (vditor.currentMode === "wysiwyg") {
const tagName = event.code.replace("Digit", "H");
if (hasClosestByMatchTag(getSelection().getRangeAt(0).startContainer, tagName)) {
removeHeading(vditor);
} else {
setHeading(vditor, tagName);
}
afterRenderEvent(vditor);
} else if (vditor.currentMode === "sv") {
processHeadingSV(vditor, "#".repeat(parseInt(event.code.replace("Digit", ""), 10)) + " ");
} else if (vditor.currentMode === "ir") {
processHeading(vditor, "#".repeat(parseInt(event.code.replace("Digit", ""), 10)) + " ");
}
event.preventDefault();
return true;
}
// toggle edit mode
if (isCtrl(event) && event.altKey && !event.shiftKey && /^Digit[7-9]$/.test(event.code)) {
if (event.code === "Digit7") {
setEditMode(vditor, "wysiwyg", event);
} else if (event.code === "Digit8") {
setEditMode(vditor, "ir", event);
} else if (event.code === "Digit9") {
setEditMode(vditor, "sv", event);
}
return true;
}
// toolbar action
vditor.options.toolbar.find((menuItem: IMenuItem) => {
if (!menuItem.hotkey || menuItem.toolbar) {
if (menuItem.toolbar) {
const sub = menuItem.toolbar.find((subMenuItem: IMenuItem) => {
if (!subMenuItem.hotkey) {
return false;
}
if (matchHotKey(subMenuItem.hotkey, event)) {
vditor.toolbar.elements[subMenuItem.name].children[0]
.dispatchEvent(new CustomEvent(getEventName()));
event.preventDefault();
return true;
}
});
return sub ? true : false;
}
return false;
}
if (matchHotKey(menuItem.hotkey, event)) {
vditor.toolbar.elements[menuItem.name].children[0].dispatchEvent(new CustomEvent(getEventName()));
event.preventDefault();
return true;
}
});
});
};
export const selectEvent = (vditor: IVditor, editorElement: HTMLElement) => {
editorElement.addEventListener("selectstart", (event: Event & { target: HTMLElement }) => {
editorElement.onmouseup = () => {
setTimeout(() => { // 鼠标放开后 range 没有即时更新
const selectText = getSelectText(vditor[vditor.currentMode].element);
if (selectText.trim()) {
if (vditor.currentMode === "wysiwyg" && vditor.options.comment.enable) {
if (!hasClosestByAttribute(event.target, "data-type", "footnotes-block") &&
!hasClosestByAttribute(event.target, "data-type", "link-ref-defs-block")) {
vditor.wysiwyg.showComment();
} else {
vditor.wysiwyg.hideComment();
}
}
if (vditor.options.select) {
vditor.options.select(selectText);
}
} else {
if (vditor.currentMode === "wysiwyg" && vditor.options.comment.enable) {
vditor.wysiwyg.hideComment();
}
}
});
};
});
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,15 @@
export const genUUID = () => ([1e7].toString() + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (c) =>
(parseInt(c, 10) ^ (window.crypto.getRandomValues(new Uint32Array(1))[0] & (15 >> (parseInt(c, 10) / 4)))).toString(16)
);
export const getSearch = (key: string, link = window.location.search) => {
const params = link.substring(link.indexOf("?"));
const hashIndex = params.indexOf("#");
// REF https://developer.mozilla.org/zh-CN/docs/Web/API/URLSearchParams
const urlSearchParams = new URLSearchParams(params.substring(0, hashIndex >= 0 ? hashIndex : undefined));
return urlSearchParams.get(key);
};
export const looseJsonParse = (text: string) => {
return Function(`"use strict";return (${text})`)();
};

View File

@@ -0,0 +1,8 @@
import {selectIsEditor} from "./selection";
export const getSelectText = (editor: HTMLElement, range?: Range) => {
if (selectIsEditor(editor, range)) {
return getSelection().toString();
}
return "";
};

Some files were not shown because too many files have changed in this diff Show More