This commit is contained in:
yanmao 2021-11-03 19:58:08 +08:00
commit d1c3366ac5
826 changed files with 123028 additions and 0 deletions

14
.circleci/config.yml Normal file
View File

@ -0,0 +1,14 @@
# This config is equivalent to both the '.circleci/extended/orb-free.yml' and the base '.circleci/config.yml'
version: 2.1
orbs:
node: circleci/node@4.1
workflows:
sample:
jobs:
- node/test:
version: '15.1'
# This is the node version to use for the `cimg/node` tag
# Relevant tags can be found on the CircleCI Developer Hub
# https://circleci.com/developer/images/image/cimg/node

View File

@ -0,0 +1,48 @@
@import (reference) '../style/variables.less';
.@{prefix}-locale-select {
position: relative;
display: inline-block;
border: 1px solid #dadadf;
border-radius: 14px;
transition: background 0.2s;
&:hover {
background-color: #fafafa;
}
&:not([data-locale-count='1']):not([data-locale-count='2'])::after {
content: '';
position: absolute;
top: 50%;
right: 10px;
margin-top: -3px;
width: 0;
height: 0;
border: 4px solid transparent;
border-top: 6px solid #7b7f8d;
pointer-events: none;
}
a,
span,
select {
padding: 0 24px 0 16px;
height: 28px;
text-align: center;
text-decoration: none;
line-height: 28px;
appearance: none;
border: 0;
font-size: 16px;
color: #7b7f8d;
background: transparent;
outline: none;
cursor: pointer;
}
a,
span {
padding-right: 16px;
}
}

View File

@ -0,0 +1,67 @@
import { FC } from 'react';
import React, { useContext } from 'react';
// @ts-ignore
import { history } from 'dumi';
import { context, Link } from 'dumi/theme';
import './LocaleSelect.less';
const LocaleSelect: FC<{ location: any }> = ({ location }) => {
const {
base,
locale,
config: { locales },
} = useContext(context);
const firstDiffLocale = locales.find(({ name }) => name !== locale);
function getLocaleTogglePath(target: string) {
const baseWithoutLocale = base.replace(`/${locale}`, '');
const pathnameWithoutLocale =
location.pathname.replace(base, baseWithoutLocale) || '/';
// append locale prefix to path if it is not the default locale
if (target !== locales[0].name) {
// compatiable with integrate route prefix /~docs
const routePrefix = `${baseWithoutLocale}/${target}`.replace(
/\/\//,
'/',
);
const pathnameWithoutBase = location.pathname.replace(
// to avoid stripped the first /
base.replace(/^\/$/, '//'),
'',
);
return `${routePrefix}${pathnameWithoutBase}`.replace(/\/$/, '');
}
return pathnameWithoutLocale;
}
return firstDiffLocale ? (
<div
className="__dumi-default-locale-select"
data-locale-count={locales.length}
>
{locales.length > 2 ? (
<select
value={locale}
onChange={(ev) =>
history.push(getLocaleTogglePath(ev.target.value))
}
>
{locales.map((localeItem) => (
<option value={localeItem.name} key={localeItem.name}>
{localeItem.label}
</option>
))}
</select>
) : (
<Link to={getLocaleTogglePath(firstDiffLocale.name)}>
{firstDiffLocale.label}
</Link>
)}
</div>
) : null;
};
export default LocaleSelect;

View File

@ -0,0 +1,17 @@
@import (reference) '../style/variables.less';
.@{prefix}-nav-right {
position: relative;
flex: 1 1;
justify-content: flex-end;
display: flex;
align-items: center;
@media @mobile {
position: absolute;
display: block;
align-items: center;
top: 0;
right: 0;
}
}

View File

@ -0,0 +1,11 @@
import React from 'react';
import SearchBar from './SearchBar';
import './NavRight.less';
export default () => {
return (
<div className="__dumi-default-nav-right">
<SearchBar />
</div>
);
};

View File

@ -0,0 +1,173 @@
@import (reference) '../style/variables.less';
.@{prefix}-navbar {
position: fixed;
z-index: 101;
top: 0;
left: 0;
right: 0;
display: none;
align-items: center;
padding: 0 14px;
height: @s-nav-height;
white-space: nowrap;
background: #fff;
box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.08);
z-index: 9999;
@media @mobile {
display: flex;
justify-content: center;
height: @s-mobile-nav-height;
}
&-toggle {
position: absolute;
top: 14px;
left: 16px;
display: none;
width: 22px;
height: 22px;
border: 0;
outline: none;
background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFgAAAA4CAYAAAB5YT9uAAAAAXNSR0IArs4c6QAAASVJREFUeAHt3DuOwjAUBVAHscWZjaSkAIqU2QjskV80QwpsF3ArdGiw3hW3OGnQM2Iof6/dfr7+n71/LjAdx+HRsvm8SkNPAHBPJ5ABDiD2KgD3dAIZ4ABirwJwTyeQAQ4g9ioA93QC2fbZsSm/z7MDAQIECBAgQIAAAQIECBAgQIAAAQKvAsvV8mO8O8yn19jkXYHpMC7byXVdeS0/75b5XFvAwr1tE0kARxjbJYDbNpEEcISxXQK4bRNJAEcY2yWA2zaRZP0ePJRzpFEJAQIECBAgQIAAAQIECBAgQIAAgW8VWK/tj7Nb5eBTnvbjsp1c15WX4ncRQeB7lb8zyHrW29xo1F1iU8AxynoR4LpLbAo4RlkvAlx3iU0BxyjrRYDrLrHpDVSAEEPXScHTAAAAAElFTkSuQmCC')
no-repeat center / contain;
@media @mobile {
display: block;
}
}
&-logo {
display: inline-block;
height: 40px;
color: #080e29;
font-weight: 500;
text-decoration: none;
font-size: 18px;
line-height: 40px;
&:not([data-plaintext]) {
padding-left: 56px;
background: url(@img-logo) no-repeat 0 / contain;
}
&:active,
&:hover {
color: #080e29;
}
@media @mobile {
height: 28px;
line-height: 28px;
&:not([data-plaintext]) {
padding-left: 36px;
}
}
}
nav {
margin-left: 20px;
display: flex;
> span {
position: relative;
margin-left: 40px;
display: inline-block;
color: @c-text;
height: @s-nav-height;
cursor: pointer;
font-size: 14px;
line-height: @s-nav-height;
text-decoration: none;
letter-spacing: 0;
> a {
color: #4d5164;
text-decoration: none;
&:hover,
&.active {
color: @c-primary;
}
&::before {
content: '';
position: absolute;
top: 0;
bottom: 0;
right: -18px;
left: -18px;
}
&.active::after {
content: '';
position: absolute;
bottom: 0;
left: -2px;
right: -2px;
height: 2px;
background-color: @c-primary;
border-radius: 1px;
}
}
+ *:not(a) {
margin-left: 40px;
}
// second nav
> ul {
list-style: none;
position: absolute;
top: 100%;
left: 50%;
margin: 0;
min-width: 100px;
padding: 8px 18px;
line-height: 2;
background-color: #fff;
box-shadow: 0 8px 24px -2px rgba(0, 0, 0, 0.08);
transform: translate(-50%);
transform-origin: top;
border-radius: 1px;
transition: all 0.2s;
a {
position: relative;
display: block;
color: @c-text;
text-decoration: none;
&:hover,
&.active {
color: @c-primary;
}
}
}
&:not(:hover) > ul {
visibility: hidden;
pointer-events: none;
transform: translate(-50%) scaleY(0.9);
opacity: 0;
}
}
.@{prefix}-search + .@{prefix}-locale-select {
margin-left: 40px;
}
@media @mobile {
> a,
> span,
> div {
display: none;
}
}
}
&[data-mode='site'] {
display: flex;
}
}

View File

@ -0,0 +1,104 @@
import { FC, MouseEvent } from 'react';
import React, { useContext } from 'react';
import { context, Link, NavLink } from 'dumi/theme';
import LocaleSelect from './LocaleSelect';
import './Navbar.less';
interface INavbarProps {
location: any;
navPrefix?: React.ReactNode;
navSuffix?: React.ReactNode;
navLast?: React.ReactNode;
onMobileMenuClick: (ev: MouseEvent<HTMLButtonElement>) => void;
}
const Navbar: FC<INavbarProps> = ({
onMobileMenuClick,
navPrefix,
navSuffix,
navLast,
location,
}) => {
const {
base,
config: { mode, title, logo },
nav: navItems,
} = useContext(context);
return (
<div className="__dumi-default-navbar" data-mode={mode}>
{/* menu toogle button (only for mobile) */}
<button
className="__dumi-default-navbar-toggle"
onClick={onMobileMenuClick}
/>
{/* logo & title */}
<Link
className="__dumi-default-navbar-logo"
style={{
backgroundImage: logo && `url('${logo}')`,
}}
to={base}
data-plaintext={logo === false || undefined}
>
{title}
</Link>
<nav>
{navPrefix}
{/* nav */}
{navItems.map((nav) => {
const child = Boolean(nav.children?.length) && (
<ul>
{nav.children.map((item) => (
<li key={item.path}>
<NavLink to={item.path}>
{item.title}
</NavLink>
</li>
))}
</ul>
);
return (
<span key={nav.title || nav.path}>
{nav.path ? (
<NavLink
isActive={(match, location) => {
if (
match &&
(match.url === '' ||
match.url === '/')
) {
return (
'' === location.pathname ||
'/' === location.pathname
);
}
return (
match &&
location.pathname.indexOf(
match.url,
) === 0
);
}}
to={nav.path}
key={nav.path}
>
{nav.title}
</NavLink>
) : (
nav.title
)}
{child}
</span>
);
})}
{navSuffix}
<LocaleSelect location={location} />
</nav>
{navLast}
</div>
);
};
export default Navbar;

View File

@ -0,0 +1,107 @@
@import (reference) '../style/variables.less';
.@{prefix}-search {
margin-left: 20px;
position: relative;
display: inline-block;
&-input {
width: 200px;
height: 32px;
padding: 0 38px 0 14px;
color: @c-heading;
font-size: 14px;
border: 0;
outline: none;
transition: all 0.2s;
border-radius: 16px;
background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAAAXNSR0IArs4c6QAABpRJREFUaAXNWWts01UUv+f+264DVEABqY8IjhhCCCGKRoMaE0HACKJxsI3WDwqd67qHEoiPT2qCEpJ1bB1vElpJZYnyMMEgfCCQEUFIDOGDihLiY7yUGAHb9XGP59+t3b3/de2/XYvtl/855557zvndc+6zwAr4tW0KPQQithQRZjEUVcCgChkbw4CFGcMwIPxJZk8zzk5qFkuP1137QwFuTHUBU1qk1N3dXdl7LVKPyGoZ4mNm+w3oneKMb7bbxu5xu1/6N8++WdVzAkBE7usKukCwj5Dh/Vmt5WgEgL8oS2ubG5w7iaakjfyXFUD7lsCDGMMvadQfHbmrQQsA7BjjfFVLg/PHQWlh1LAA2v2BuQLZF1QuE42mB0bvNGOwj3NxinGtN1Exulez3hJwizuYiDsSgs1myF6mDD5F/bnRBmXiBkO+uNXrPDqkLQ9BRgDtnYHlVC4BGnmrbItGLoYMtmoV/JOmlSt+l9uGozdtCkyMxNk7VDFNZM8u65G9CAeobvK4vpLl+dBDAGzsCj6dSOARWk1ssiEa9UNWgEaPx/mzLDdL+/2hB2IYa6OMvKr2gbjG+YtNnhXfqHJznAKgq2v31D6ROEllc4/aHda3NDrfJRBClefP+ToDH1CvDwnIoG+Aqzawz/J4qi/nazFdm7rBvkQ8ZAweOK9v9brWFiN4PbiWRtfHCFA3MI/646V5FsPwZxRDOh6zQLSU4riJVS4KvjHFJ7+cbWj1uNYpsiIwhw7uPbdg0VJ9GX1OMjf129NnL1Lb95IsJ5lMo9/fPSYqIj9R3U9O9aAROtzscS4o1sin7Ka+esZ9/uBeGrQlKRkFc94xqXJ6dXV1IiXL9U2mLMbCLjl4Wh7jjIO3VMHrQSVLSLO2UNB9qSApJdP+uNZXneLNfPtrDmGFrExL3o5ibDKyzUx061s1FwlKl9wGQjTJfC6a+/3BKkrnk7KijVvWy3wpaY1ZNySzMeCEsvD49u3d48365HEhFsrKtLmcbWiouyDLSkl7vTW9NIDfST74zb7wPInPSnLaWWeqGrBP5UvPAfD9shfasZ+X+Ww0zQFUASCcydahJG2Iik+a2FPM+tEn8cOyssawV+ZvB60xbvQ5waxfKiGW3sz6O1mNxszaKljPZuOXDJ2HnIAN7Wm2fxlNs4xZLBpl8H/+GU6t2aLhFK2y6/WJqCNbh1K0RaMifQLotw+mq0DPwC9KUGLwOKHIS8jQEU4BQHcRU3cNPSROJ8OzSmyAcxT+NjAJFMqVlV45fjPrllMKFAA0qdOHK7NGRqoHiItlGwh4Quaz0dxCNy1FAXGmfrFRZCVk6CR8L53snki5oGNFYpTVeiDF5/rygStij6wYFfE1Ml9KOoaR1frRWvJx3O2u1R/GTP2Syyhw2CVrk7036C3oEVlWClq/JxsvUfRatCcfX0kAVmYP0bFW2kzQggmxkUYm2Z6PwXx0YyLmozlXke4DcGHyJPvONG+CSAZIl+mbRKw26M/3dQbXGWRFY8n2e7RcviIb1ICvpdtYVJblouXaY20dgaN0uHtW7kTltarF49omy0ZK+/y7ljGEkKH2e1q9r8/N17ZSIjYOb5KB67IRFLjV1xFYV6xyokF6P0PwSZeBQGC07NsMrWRA76A/bImEOKzUJslJ8Wtuq/A2uZerO7cZL6STfGeNsjZj2cjd6TJ1bPydsMjlct2S5dnoIQB05TZ/sIY2lyCNunJSJQcxeu/cwu3ap2afFjs6Qo4ExN7WVxvjoGQKLF8QGQHohqlsXqD58Dk5HWt0pN9hSX6GAx5AAae4FS5bES9xbhHhBHMgE/eBwNmIYgnpzjHUutHcED4fEMMC0K22b949TcTj9HbDZgzxMjJBDzn+lQahZjgzZkEok9horLm+7vwo64zZ9ILcSNv9VWN73jyt8xrXXtNXm+ZGVx0d2oZd3SjFz1z/Bw/mmthZMyAHuGPH/jtuhP+uJ9kyMq6cHmW9DLSgMjrBAEOOCZXb5XVeL632zuAWmtgrM/RLinJlwjQA2UHHtsAUEYHFCGI6gZlC2ZlKZfYgGYvQvLlCpXGVRreXAj+CFdr+lpW1V+T+Mm0GBK2Bx+++iy3MtDoVBEAOoBj0SECUBQB9EMyCsHH7Iv3okxq4sgFQKIiyAlAIiLIDkC+IsgRgFgQtsQfKFkAuEBT8ZQ21+WUNYFgQtKNXcG2e/jdA2QMwgqAN8lylDea73U7pCqxrlfkvuU/4A2voyWecHOp/C76d7/ws9hcAAAAASUVORK5CYII=')
#f5f6f7 no-repeat right 14px center / 16px;
appearance: none;
}
> ul {
list-style: none;
position: absolute;
top: 100%;
right: 0;
z-index: 10;
margin: 8px 0 0;
min-width: 280px;
max-width: 400px;
padding: 6px 0;
background-color: #fff;
border: 1px solid @c-border;
border-radius: 1px;
box-shadow: 0 2px 20px 0 rgba(0, 0, 0, 0.05);
box-sizing: border-box;
&:empty {
display: none;
}
li {
font-size: 15px;
a {
display: block;
padding: 6px 20px;
color: @c-secondary;
text-decoration: none;
transition: background-color 0.3s;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
&:hover {
color: @c-primary;
background-color: @c-light-bg;
}
}
span:first-child {
position: relative;
display: inline-block;
max-width: 50%;
padding-right: 26px;
vertical-align: -0.37em;
overflow: hidden;
text-overflow: ellipsis;
opacity: 0.8;
&::after {
content: '>';
position: absolute;
top: 50%;
right: 6px;
opacity: 0.6;
transform: translateY(-54%);
}
}
}
}
@media only screen and (max-width: 1024px) {
margin-right: -14px;
> input:not(:focus) {
width: 32px;
padding-right: 0;
box-shadow: none;
cursor: pointer;
background-position: right 8px center;
+ ul {
transition: 0.1s visibility;
visibility: hidden;
}
}
}
@media @mobile {
position: absolute;
top: 9px;
right: 24px;
display: block !important;
}
}

View File

@ -0,0 +1,46 @@
import React, { useState, useEffect, useRef } from 'react';
import { useSearch, AnchorLink } from 'dumi/theme';
import './SearchBar.less';
export default () => {
const [keywords, setKeywords] = useState<string>('');
const [items, setItems] = useState([]);
const input = useRef<HTMLInputElement>();
const result = useSearch(keywords);
useEffect(() => {
if (Array.isArray(result)) {
setItems(result);
} else if (typeof result === 'function') {
result(`.${input.current.className}`);
}
}, [result]);
return (
<div className="__dumi-default-search">
<input
className="__dumi-default-search-input"
type="search"
ref={input}
{...(Array.isArray(result)
? {
value: keywords,
onChange: (ev) => setKeywords(ev.target.value),
}
: {})}
/>
<ul>
{items.map((meta) => (
<li key={meta.path} onClick={() => setKeywords('')}>
<AnchorLink to={meta.path}>
{meta.parent?.title && (
<span>{meta.parent.title}</span>
)}
{meta.title}
</AnchorLink>
</li>
))}
</ul>
</div>
);
};

View File

@ -0,0 +1,334 @@
@import (reference) '../style/variables.less';
.@{prefix}-menu {
position: fixed;
z-index: 100;
top: 0;
left: 0;
bottom: 0;
width: @s-menu-width;
background-color: #f2f5fa;
box-sizing: border-box;
transition: left 0.3s;
&[data-hidden] {
display: none;
}
@media @mobile {
left: -@s-menu-mobile-width;
top: @s-mobile-nav-height;
display: block !important;
width: @s-menu-mobile-width;
background-color: #fff;
&[data-mobile-show] {
left: 0;
}
}
// shadow
&::after {
content: '';
position: absolute;
top: 0;
right: 0;
bottom: 0;
display: block;
width: 20px;
background: linear-gradient(
to right,
rgba(0, 0, 0, 0),
rgba(0, 0, 0, 0.03)
);
pointer-events: none;
// use border on mobile devices
@media @mobile {
width: 1px;
background: @c-border;
}
}
&-header {
position: relative;
padding-top: 40px;
text-align: center;
border-bottom: 1px solid @c-border;
@media @mobile {
display: none;
}
.@{prefix}-menu-logo {
display: inline-block;
width: 66px;
height: 65px;
background: url(@img-logo) no-repeat 0 / contain;
}
h1 {
margin: 10px 0 0;
color: @c-heading;
font-weight: 500;
line-height: 1.40625;
}
p {
margin: 0 0 5px;
color: lighten(@c-secondary, 10%);
// badges
> object[data^='https://img.shields.io']
{
max-height: 20px;
}
+ p {
margin-bottom: 10px;
}
}
}
&-doc-locale {
padding: 16px 0;
text-align: center;
border-bottom: 1px solid @c-border;
&:empty {
display: none;
}
}
&-inner {
width: 100%;
height: 100%;
overflow: auto;
overscroll-behavior: contain;
// common list styles
ul {
list-style: none;
margin: 0;
padding: 0;
font-size: 16px;
li {
color: @c-text;
a,
> span {
position: relative;
display: block;
padding-right: 24px;
color: @c-heading;
line-height: 2.4;
text-decoration: none;
outline: none;
transition: color 0.3s, background 0.3s;
span {
display: block;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
&:hover,
&.active {
color: @c-primary;
}
&::before {
content: '';
position: absolute;
top: 50%;
left: -10px;
margin-top: -2.5px;
display: inline-block;
width: 5px;
height: 5px;
background-color: @c-primary;
border-radius: 50%;
opacity: 0;
transition: transform 0.2s, opacity 0.2s;
transform: scale(0) translateX(-10px);
}
}
&.active a,
a.active {
&::before {
opacity: 1;
transform: scale(1) translateX(0);
}
}
// level larger, offset larger, font size smaller
ul {
font-size: 0.9em;
padding-left: 1em;
}
}
}
// 1-level list styles
> ul {
> li > a {
line-height: 2.875;
&:not([href]) {
padding-top: 24px;
line-height: 1;
font-weight: 500;
color: @c-heading !important;
background: transparent !important;
cursor: default;
}
}
> li:first-child > a:not([href]) {
padding-top: 0;
}
}
// n-level list styles
> ul ul {
a {
color: @c-secondary;
&.active {
color: @c-primary;
}
}
}
.@{prefix}-menu-mobile-area {
display: none;
padding-bottom: 16px;
margin-bottom: 16px;
text-align: center;
border-bottom: 1px solid @c-border;
@media @mobile {
display: block;
}
}
// mobile nav list
.@{prefix}-menu-nav-list {
padding: 16px 0;
> li,
> li > a {
padding-right: 0;
line-height: 2.4;
ul {
padding-left: 0;
a {
padding-right: 0;
font-size: 90%;
}
}
}
}
// menu list
.@{prefix}-menu-list {
padding: 8px 0;
margin-bottom: 40px;
> li > a {
@c-active-bg: #e8ecf4;
padding-left: 28px;
&.active {
background: linear-gradient(
to left,
#e8ecf4,
rgba(232, 236, 244, 0)
);
}
~ ul {
margin-top: 8px;
margin-left: 28px;
}
@media @mobile {
padding-left: 16px;
~ ul {
margin-left: 16px;
}
}
}
}
}
&[data-mode='site'] {
&::after {
width: 1px;
background: @c-border;
}
.@{prefix}-menu-list {
padding: 0;
> li > a {
position: relative;
&::after {
content: '';
position: absolute;
top: 0;
bottom: 0;
right: 0;
display: block;
width: 3px;
background-color: @c-primary;
visibility: hidden;
opacity: 0;
transition: all 0.3s;
border-radius: 1px;
}
&.active {
z-index: 1;
background: linear-gradient(
to left,
#f8faff,
rgba(248, 250, 255, 0)
);
&::after {
opacity: 1;
visibility: visible;
}
}
}
}
@media @desktop {
top: @s-nav-height;
width: @s-site-menu-width;
padding-top: 50px;
background: transparent;
.@{prefix}-menu-nav,
.@{prefix}-menu-header {
display: none;
}
.@{prefix}-menu-list > li > a {
padding-left: 58px;
~ ul {
margin-left: 58px;
}
}
}
}
}

View File

@ -0,0 +1,180 @@
import { FC } from 'react';
import React, { useContext } from 'react';
import { context, Link, NavLink } from 'dumi/theme';
import LocaleSelect from './LocaleSelect';
import SlugList from './SlugList';
import './SideMenu.less';
interface INavbarProps {
mobileMenuCollapsed: boolean;
location: any;
}
const SideMenu: FC<INavbarProps> = ({ mobileMenuCollapsed, location }) => {
const {
config: {
logo,
title,
description,
mode,
repository: { url: repoUrl },
},
menu,
nav: navItems,
base,
meta,
} = useContext(context);
const isHiddenMenus =
Boolean(
(meta.hero || meta.features || meta.gapless) && mode === 'site',
) ||
meta.sidemenu === false ||
undefined;
return (
<div
className="__dumi-default-menu"
data-mode={mode}
data-hidden={isHiddenMenus}
data-mobile-show={!mobileMenuCollapsed || undefined}
>
<div className="__dumi-default-menu-inner">
<div className="__dumi-default-menu-header">
<Link
to={base}
className="__dumi-default-menu-logo"
style={{
backgroundImage: logo && `url('${logo}')`,
}}
/>
<h1>{title}</h1>
<p>{description}</p>
{/* github star badge */}
{/github\.com/.test(repoUrl) && mode === 'doc' && (
<p>
<object
type="image/svg+xml"
data={`https://img.shields.io/github/stars${
repoUrl.match(/((\/[^\/]+){2})$/)[1]
}?style=social`}
/>
</p>
)}
</div>
{/* mobile nav list */}
{navItems.length ? (
<div className="__dumi-default-menu-mobile-area">
<ul className="__dumi-default-menu-nav-list">
{navItems.map((nav) => {
const child = Boolean(nav.children?.length) && (
<ul>
{nav.children.map((item) => (
<li key={item.path || item.title}>
<NavLink to={item.path}>
{item.title}
</NavLink>
</li>
))}
</ul>
);
return (
<li key={nav.path || nav.title}>
{nav.path ? (
<NavLink to={nav.path}>
{nav.title}
</NavLink>
) : (
nav.title
)}
{child}
</li>
);
})}
</ul>
{/* site mode locale select */}
<LocaleSelect location={location} />
</div>
) : (
<div className="__dumi-default-menu-doc-locale">
{/* doc mode locale select */}
<LocaleSelect location={location} />
</div>
)}
{/* menu list */}
<ul className="__dumi-default-menu-list">
{!isHiddenMenus &&
menu.map((item) => {
// always use meta from routes to reduce menu data size
const hasSlugs = Boolean(meta.slugs?.length);
const hasChildren =
item.children && Boolean(item.children.length);
const show1LevelSlugs =
meta.toc === 'menu' &&
!hasChildren &&
hasSlugs &&
item.path ===
location.pathname.replace(
/([^^])\/$/,
'$1',
);
return (
<li key={item.path || item.title}>
<NavLink
to={item.path}
exact={
!(
item.children &&
item.children.length
)
}
>
{item.title}
</NavLink>
{/* group children */}
{Boolean(
item.children && item.children.length,
) && (
<ul>
{item.children.map((child) => (
<li key={child.path}>
<NavLink
to={child.path}
exact
>
<span>
{child.title}
</span>
</NavLink>
{/* group children slugs */}
{Boolean(
meta.toc === 'menu' &&
typeof window !==
'undefined' &&
child.path ===
location.pathname &&
hasSlugs,
) && (
<SlugList
slugs={meta.slugs}
/>
)}
</li>
))}
</ul>
)}
{/* group slugs */}
{show1LevelSlugs && (
<SlugList slugs={meta.slugs} />
)}
</li>
);
})}
</ul>
</div>
</div>
);
};
export default SideMenu;

View File

@ -0,0 +1,18 @@
@import (reference) '../style/variables.less';
ul[role='slug-list'] {
&:empty {
margin: 0 !important;
padding: 0 !important;
}
li {
> a.active {
color: darken(@c-primary, 2%);
}
&[data-depth='3'] {
padding-left: 12px;
}
}
}

View File

@ -0,0 +1,27 @@
import { FC } from 'react';
import React from 'react';
import { AnchorLink } from 'dumi/theme';
import './SlugList.less';
const SlugsList: FC<{ slugs: any; className?: string }> = ({
slugs,
...props
}) => (
<ul role="slug-list" {...props}>
{slugs
.filter(({ depth }) => depth > 1 && depth < 4)
.map((slug) => (
<li
key={slug.heading}
title={slug.value}
data-depth={slug.depth}
>
<AnchorLink to={`#${slug.heading}`}>
<span>{slug.value}</span>
</AnchorLink>
</li>
))}
</ul>
);
export default SlugsList;

View File

@ -0,0 +1,145 @@
import React, { useContext, useState } from 'react';
import { IRouteComponentProps } from '@umijs/types';
import { context, Link } from 'dumi/theme';
import Navbar from '../components/Navbar';
import SideMenu from '../components/SideMenu';
import SlugList from '../components/SlugList';
import NavRight from '../components/NavRight';
import './layout.less';
const Hero = (hero) => (
<>
<div className="__dumi-default-layout-hero">
{hero.image && <img src={hero.image} />}
<h1>{hero.title}</h1>
<div dangerouslySetInnerHTML={{ __html: hero.desc }} />
{hero.actions &&
hero.actions.map((action) => (
<Link to={action.link} key={action.text}>
<button type="button">{action.text}</button>
</Link>
))}
</div>
</>
);
const Features = (features) => (
<div className="__dumi-default-layout-features">
{features.map((feat) => (
<dl
key={feat.title}
style={{
backgroundImage: feat.icon
? `url(${feat.icon})`
: undefined,
}}
>
{feat.link ? (
<Link to={feat.link}>
<dt>{feat.title}</dt>
</Link>
) : (
<dt>{feat.title}</dt>
)}
<dd dangerouslySetInnerHTML={{ __html: feat.desc }} />
</dl>
))}
</div>
);
const Layout: React.FC<IRouteComponentProps> = ({ children, location }) => {
const {
config: { mode, repository },
meta,
locale,
} = useContext(context);
const { url: repoUrl, branch, platform } = repository;
const [menuCollapsed, setMenuCollapsed] = useState<boolean>(true);
const isSiteMode = mode === 'site';
const showHero = isSiteMode && meta.hero;
const showFeatures = isSiteMode && meta.features;
const showSideMenu =
meta.sidemenu !== false && !showHero && !showFeatures && !meta.gapless;
const showSlugs =
!showHero &&
!showFeatures &&
Boolean(meta.slugs?.length) &&
(meta.toc === 'content' || meta.toc === undefined) &&
!meta.gapless;
const isCN = /^zh|cn$/i.test(locale);
const updatedTimeIns = new Date(meta.updatedTime);
const updatedTime: any = `${updatedTimeIns.toLocaleDateString([], {
hour12: false,
})} ${updatedTimeIns.toLocaleTimeString([], { hour12: false })}`;
const repoPlatform =
{ github: 'GitHub', gitlab: 'GitLab' }[
(repoUrl || '').match(/(github|gitlab)/)?.[1] || 'nothing'
] || platform;
return (
<div
className="__dumi-default-layout"
data-route={location.pathname}
data-show-sidemenu={String(showSideMenu)}
data-show-slugs={String(showSlugs)}
data-site-mode={isSiteMode}
data-gapless={String(!!meta.gapless)}
onClick={() => {
if (menuCollapsed) return;
setMenuCollapsed(true);
}}
>
<Navbar
location={location}
navLast={<NavRight />}
onMobileMenuClick={(ev) => {
setMenuCollapsed((val) => !val);
ev.stopPropagation();
}}
/>
<SideMenu mobileMenuCollapsed={menuCollapsed} location={location} />
{showSlugs && (
<SlugList
slugs={meta.slugs}
className="__dumi-default-layout-toc"
/>
)}
{showHero && Hero(meta.hero)}
{showFeatures && Features(meta.features)}
<div className="__dumi-default-layout-content">
{children}
{!showHero &&
!showFeatures &&
meta.filePath &&
meta.showFooter !== false &&
!meta.gapless && (
<div className="__dumi-default-layout-footer-meta">
{repoPlatform && (
<Link
to={`${repoUrl}/edit/${branch}/${meta.filePath}`}
>
{isCN
? `${repoPlatform} 上编辑此页`
: `Edit this doc on ${repoPlatform}`}
</Link>
)}
<span
data-updated-text={
isCN ? '最后更新时间:' : 'Last update: '
}
>
{updatedTime}
</span>
</div>
)}
{(showHero || showFeatures) && meta.footer && (
<div
className="__dumi-default-layout-footer"
dangerouslySetInnerHTML={{ __html: meta.footer }}
/>
)}
</div>
</div>
);
};
export default Layout;

View File

@ -0,0 +1,327 @@
@import '../style/markdown.less';
@import '../style/variables.less';
@s-toc-width: 136px;
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, PingFang SC,
Hiragino Sans GB, Microsoft YaHei, Helvetica Neue, Helvetica, Arial,
sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol;
font-variant: tabular-nums;
font-feature-settings: 'tnum';
}
.@{prefix}-layout {
box-sizing: border-box;
min-height: 100vh;
//padding: 16px (@s-content-margin + @s-toc-width) 50px @s-menu-width +
//@s-content-margin;
@media @mobile {
padding-top: 66px !important;
padding-left: 16px !important;
padding-right: 16px !important;
}
&[data-gapless='true'] {
padding-top: @s-nav-height !important;
padding-right: 0 !important;
padding-left: 0 !important;
padding-bottom: 0;
@media @mobile {
padding-top: @s-mobile-nav-height !important;
}
}
&[data-show-sidemenu='false'] {
padding-left: 0;
}
&[data-show-slugs='false'] {
padding-right: 0;
}
&[data-site-mode='true'] {
padding-top: 64px;
&[data-show-sidemenu='true'] {
padding-top: @s-nav-height + 50px;
padding-left: @s-site-menu-width + 50px;
padding-right: 50px;
padding-bottom: 50px;
}
&[data-show-slugs='true'] {
padding-right: @s-content-margin + @s-toc-width + 14;
}
.@{prefix}-layout-content > .markdown:first-child > *:first-child {
margin-top: 0;
}
.@{prefix}-layout-toc {
top: 114px;
max-height: calc(90vh - 144px);
}
}
&-hero {
margin: -50px -58px 0;
padding: 100px 0;
text-align: center;
background-color: #f5f6f8;
@media @mobile {
margin: -16px -16px 0;
padding: 48px 0;
}
img {
max-width: 100%;
max-height: 200px;
margin-bottom: 1rem;
}
h1 {
margin: 0 0 16px;
font-size: 48px;
font-weight: 600;
line-height: 56px;
color: #080e29;
+ div {
margin: 16px 0 32px;
opacity: 0.78;
.markdown {
font-size: 16px;
}
}
}
button {
margin-right: 16px;
padding: 0 32px;
height: 44px;
color: @c-primary;
font-size: 16px;
background: transparent;
border: 1px solid @c-primary;
border-radius: 22px;
box-sizing: border-box;
cursor: pointer;
outline: none;
transition: all 0.3s;
&:hover {
opacity: 0.8;
}
&:active {
opacity: 0.9;
}
}
a:last-child button {
margin-right: 0;
color: #fff;
background: @c-primary;
}
}
&-features {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-column-gap: 96px;
grid-row-gap: 56px;
padding: 72px 0;
> dl {
flex: 1;
margin: 0;
text-align: center;
background: no-repeat center top / auto 48px;
&[style*='background-image'] {
padding-top: 64px;
}
dt {
margin-bottom: 12px;
font-size: 20px;
line-height: 1;
color: @c-heading;
}
a {
transition-duration: none;
}
a dt {
color: @c-link;
transition: opacity 0.2s;
&:hover {
opacity: 0.7;
text-decoration: underline;
}
&:active {
opacity: 0.9;
}
}
dd {
margin: 0;
.markdown {
color: @c-secondary;
font-size: 14px;
line-height: 22px;
> p:first-child {
margin-top: 0;
}
> p:last-child {
margin-bottom: 0;
}
}
}
}
@media @mobile {
display: block;
padding: 40px 0;
> dl {
text-align: left;
background-position: left top;
&[style*='background-image'] {
padding: 0 0 0 60px;
}
+ dl {
margin-top: 32px;
}
}
}
}
&-features,
&-features + &-content,
&-hero + &-content {
margin-left: auto;
margin-right: auto;
max-width: 960px;
}
&-hero + &-content {
margin-top: 60px;
}
&-toc {
list-style: none;
position: fixed;
z-index: 10;
top: 50px;
right: 0;
width: @s-toc-width;
max-height: calc(90vh - 80px);
margin: 0;
padding: 0 24px 0 0;
background-color: #fff;
box-shadow: 0 0 16px 16px #fff;
box-sizing: content-box;
overflow: auto;
@media @mobile {
display: none;
}
li {
position: relative;
margin: 0;
padding: 4px 0 4px 6px;
text-indent: 12px;
font-size: 13px;
line-height: 1.40625;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
a {
color: @c-text;
text-decoration: none;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
bottom: 0;
display: inline-block;
width: 2px;
background: @c-border;
}
&:hover {
color: lighten(@c-primary, 5%);
}
&:active {
color: lighten(@c-primary, 3%);
}
&.active {
color: @c-primary;
&::before {
background: @c-primary;
}
}
}
}
}
&-footer-meta {
margin-top: 40px;
padding-top: 24px;
display: flex;
color: @c-secondary;
font-size: 14px;
justify-content: space-between;
border-top: 1px solid @c-border;
@media only screen and (max-width: 960px) {
display: block;
}
> a {
margin-bottom: 4px;
display: block;
color: @c-primary;
text-decoration: none;
}
> span:last-child {
&::before {
content: attr(data-updated-text);
color: @c-primary;
}
}
}
}
.__dumi-default-layout-footer {
margin: 72px 0 -32px;
padding-top: 24px;
border-top: 1px solid @c-border;
text-align: center;
.markdown {
color: #b0b1ba;
}
}

View File

@ -0,0 +1,196 @@
@import (reference) './variables.less';
.markdown {
color: @c-text;
font-size: 15px;
line-height: 1.60625;
&:not(:first-child):empty {
min-height: 32px;
}
// titles
h1,
h2,
h3,
h4,
h5,
h6 {
margin: 42px 0 18px;
color: @c-heading;
font-weight: 500;
line-height: 1.40625;
// anchor link
&:hover > a[aria-hidden] {
float: left;
margin-top: 0.06em;
margin-left: -20px;
width: 20px;
padding-right: 4px;
line-height: 1;
box-sizing: border-box;
@media @mobile {
width: 14px;
margin-left: -14px;
}
&::after {
content: '#';
display: inline-block;
vertical-align: middle;
font-size: 20px;
}
span {
display: none;
}
}
+ h1,
+ h2,
+ h3,
+ h4,
+ h5,
+ h6 {
margin-top: 16px;
}
}
h1 {
margin-top: 48px;
margin-bottom: 32px;
font-size: 32px;
}
h2 {
font-size: 24px;
}
h3 {
font-size: 20px;
}
h4 {
font-size: 18px;
}
h5 {
font-size: 16px;
}
h6 {
font-size: 14px;
}
// paragraph
p {
margin: 16px 0;
}
// inline code
*:not(pre) code {
padding: 2px 5px;
color: #d56161;
background: darken(@c-light-bg, 1%);
}
code {
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
}
// code block
pre {
font-size: 14px;
background: darken(@c-light-bg, 1%);
&:not([class^='language-']) {
padding: 1em;
}
}
// horizontal line
hr {
margin: 16px 0;
border: 0;
border-top: 1px solid @c-border;
}
// blockquote
blockquote {
margin: 16px 0;
padding: 0 24px;
color: fadeout(@c-text, 30%);
border-left: 4px solid @c-border;
overflow: hidden;
}
// list
ul,
ol {
margin: 8px 0 8px 32px;
padding: 0;
li {
margin-bottom: 4px;
}
}
// table
table {
width: 100%;
border-collapse: collapse;
border: 1px solid @c-border;
th,
td {
padding: 10px 24px;
border: 1px solid @c-border;
}
th {
font-weight: 600;
background: @c-light-bg;
}
td:first-child {
font-weight: 500;
}
a {
svg {
display: none;
}
}
}
// links
a {
color: @c-link;
text-decoration: none;
transition: opacity 0.2s;
outline: none;
&:hover {
opacity: 0.7;
text-decoration: underline;
}
&:active {
opacity: 0.9;
}
}
// images
img {
max-width: 100%;
}
}
.@{prefix} {
&-external-link-icon {
vertical-align: -0.155em;
margin-left: 2px;
}
}

View File

@ -0,0 +1,26 @@
/* 颜色表 */
@c-primary: #2f54eb;
@c-heading: #454d64;
@c-text: #454d64;
@c-secondary: #717484;
@c-link: @c-primary;
@c-border: #ebedf1;
@c-light-bg: #f9fafb;
/* 尺寸表 */
@s-nav-height: 64px;
@s-mobile-nav-height: 50px;
@s-menu-width: 260px;
@s-site-menu-width: 300px;
@s-menu-mobile-width: 240px;
@s-content-margin: 58px;
@img-logo: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIQAAACCCAMAAACww5CIAAACf1BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///8YkP8AAAACCxMamv/6+voaGhoXi/YYjv8aoP8cq/8dr/8bo/8cqP8bpv8Ykv8drv8BAwUcrP8Zlf8Xjf/s7OzLy8scp/8anP8ZmP/d3d0BBArg4ODT09O7u7sEGCsKCgoanf8YlP/8/Pz09PTIyMgMTIV1dXUGKEVEREQ0NDQODg4GBgYdsv8dsf8Zl//m5uYVgOXj4+MWgtfW1tYTc87BwcERbLWzs7Ovr6+np6cQX6OgoKCTk5MMSXlwcHBra2tiYmIVFRUetf/39/fp6ekWhOkXi+QVfNvY2NjPz88TdcUSb7u6urq3t7cPYK0NUJGQkJCLi4ttbW0JO2cINFtVVVVRUVEHMFEHLEs6OjoEHDEiIiIcHBwXj/vx8fEWh+4Sb8gRbL+rq6upqakOVZiWlpaJiYmGhoYMSIF9fX15eXkKPnQLRHJMTExHR0c9PT0FHzkqKiomJiYEFyUBBw8bovfu7u4Wht4UedsUeMrFxcW9vb0RZrOkpKSampoPXZqAgIALQmtlZWUJOGJZWVkIMFcFIUExMTEwMDAtLS0DEh8Zl/v4+PgXj/QWhvEWhvAYku8YjuwUfNcUfNAVfc0RaLkSaKsRZ6kPWqENUYlbW1sCEBhkSPCkAAAAOHRSTlMA87y4BeKrltbFnUDo0MCup6D67t7ayZKGemtmWS8rEwLNso1wVEpFGaR+UDUlHwmBYls5i1oN/DMym4YAAAfTSURBVHjaxNndS1NxHMfxX5s6t1Kz1KzsuazMnqjgyxv03ovtQrYxUBEfLkREVBQf0AsFBRUUQvEiSVFQ0YsuiiIiqKC/oH+o31lzjtPZg55zttfVNnbx5ffw+X53pmx5UFl2+XLZ4zpVOPWlJFTntYyiBwF/VbX39Sv9upYU9/QHjbXe6qqayrrnylXXi0kov3GVuFiMuNqbHhIu3FcuuohZZ+jDh7mdXkwqlGtKMGmOSFzrGiYe5ZL4+vdsd/SHFyYxtIQlIdiD4ftCa39osTlxRtzwHO1tUOLm0XYk6T3asMRtdKHdUs6qv+L1l/vKgak2SYjqN+1yYg2G5NgR4Pd5/F7fk9sO3YhSkoYkaW40KCk2Rj9KUoikqmtOn8YpydE6J7xFyq5yUhxIjvZJcUfZ5EOb6oxGQmPdtEQlR4Mxupc6IoOdzWiVypabaF1BiesIS876OiSufRXtvO0DcSi2dAN+ZcclYFZsCaOps3nYUOKprDTiSWzqAioCnpIX9ep03pxkw7jYtMWx0pdn7Jb2i1jixN3cM6OGFCti0zgpyopOsw6xiZHoyHIPLIhNHdD7bWR+c7znFD3+PNp+vxhmRkNi28BoWAzBPbQHKhdlQLe4ogsoVTl4ijYjrmiKATdUdvfjh9Ely8DVHFvWe3HJMBBQ2QWAd+KSeeBxjtuxKC7ZzG07Ht0DusQlfwDfs2wZ4b2EYVBcESHO81BlcIWESXHFV7Qss5aXY1FxRSj7L7QAhv3tsaVBMVn8Ou1MFUtjW3sYKjL0jO6QWJiA7iZxysBbtDplpRT4KZbQWkUbHRMnGFUUKwuNaH1iaRJ+Tf8bDbqcWJH2HuCV+l9DpkuxtdsuGlpYHNAJ1FqNMjnE9QocOXJCPwJ309zPT9la8e5yUJwwC/jTBNWQ5EkIqEyzHROSJzvWSeFDW5M8OUArsdgMq2EmanOyGB4WSyMYAhZp2TwkJouw2mZvmusUSwtraA//m7DXZ8SsBxiQM5tGSxNuv3+ZU/NmIpfN9qDXxp1sO4LDNrE202J6cHE1TVq2f1uNiA39K9/7JJ0JwGe6nvOSZ4OA1/R0bFbyrBWoMUX2nOTZAOA3pcSXjFW7UOJnU17VAYeZv98pTvsB1KsTRVXAtqQVA/rFWSNo11SKiuRYZeknEBRn7WJ4rZKuX8pcROvBj6g4rLUZQ8NJYBo2Jb/ax2KkhKYf6I1I3oWngKqUhfgkBTCL1pics1elICaS/5Y9jk+XBdEBeJKhHZGCCLZAWTIkBqQgNlr+NbGi2wHgS1tTAbQNAxW3i1R58WWgd725ANZ7gXPFNaqagrvwt1t7aW0qiOIAPlErPqJCq6JWrW8r1ar1xf0n4NxnnpCELEKyCNmkJZSQRSCbQltooS4sVApiC10U2kWhFRUEEdGF4vuNH8g7c9NQ2pjepPcB/r5ADjlnzp2ZM+QMXHeYb+1WfO5hi5QfveYe33XJ4+d8a3MNQHbI75KhMt9z9wF4FRNcIi3wO94bAHJiQHCHNgmgh3QD8D1MCK6I+KeNCUgbgFFRcEX8Qwhov014o/juUlEoxeqrgpsA7oWp4AZprnpv1ANgShFcoU4a+36jMgOuVGYmnuJ1Wb0hKWqCC8QCgI4dqyfRbNCFoqDBX7Xz6C0AS660K3UKQCdhuqAbdqFT+B8mAXQTbhtbpM7ng4Yn1oytOwFMu5AP9QGAa4Qz8lFwvFWIH6G7Qjijc8/LDueDyvd4z151EYBvwOF+lRFTAK6TGi+ACWdLk0ozANqvkpojAFJKRnCSlFt3m8pLc9bJTylVn64ty9rJfEl1cpVKbH3uJ2v1QleUqOCI2h9xeeP0aVqLCA4JSLk6s7hu6CbkqOAIGpyB7iRZ5xLvFWlHEkITyjK/41/v9h0AC3lngpCz0PXWf0yDUcmBhFDt0T/flx8CkNL8VLAZjUhvAHSQek5AtyALdqP5e9BdbPCkZsbuFRKVvlRHs/W1AfC902yNgoriWwCeqw1fSL+J2VkWNBF8vckr6mPQ3ZcjtkVBA/3z4Ju6Bs5ANzck2BQFpUMTxlVZQ4ege95vUxRUHoPOe5s01OWBbryf2hEFDX4Fc4Vs4gaYZ3ZEQeXBJPgMcFPnwYzJVmeE6jGsGCNAE/rAlPIBamkMQv9YCLpzxJRjYMr5BLXyg5EvgTlKTOoEkw2LUct6dTz4ojqCNO04mMm4ZE150mhMuQ+jHppwAUxqUM5QK9qkPLIE5jhpygkvmHJYiW45FaL8IwmdZy9pUtc2MK9HtvgloZngJyMVp3tJ846ASb7Q1NYrg1JN+ukDs4e05LwHTO5bUKG0tRBEeXAKzJ3rpEXdB8C9fBIWKW0hhOBIBdy2K6R11zvALY6EFYE21yHF4OdKEkz7ObIlXXvAhV4OquoApaYbpCo9qayA29lLturibhimSgOSFjG1ILRwYnwShn09xArnT8PwdnHML6n+hl+2gD8Wjj+rLMOwq49Y5dZpVKUWS++VcCwdCdT5/Uhck5SH45VpVO3qJFbq2Y5Vvly2VBgQY5KqKWI6HY+n06KiqVJMSQyP/37wB6v29xGrnThyEDWh5dyr+fJscbQw/OjRcGG0OFvO3n+QSqKm7exlYgsvNgolkyFs1HGV2OQgTGsjNjnVBtO8Owj3nwbhgWnttgWxy2PaoWaC+AuAXqWYKHupMgAAAABJRU5ErkJggg==';
@prefix: __dumi-default;
@mobile: ~'only screen and (max-width: 767px)';
@desktop: ~'only screen and (min-width: 768px)';
@icons: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAcwAAAA8CAMAAADc4HoZAAABFFBMVEUAAABGT2VGTmZaYXpCvGtIUGg3tGBGTmU3s2A/tmFKUGxFTmRFTWVQWmxFTWRJUGZFTWRGTmRLWWpFTWRGTmVGTmVLVG1FTmRGTWVHTmVFTWRHUGdFTWRGT2ZFTWVGTmU6t2I7xHA3tF9GTWRIT2dFTmRGTmVFTWQ3s2BFTWRGTmVGTmZKUmVFTWRFTWRGTWRGTmVcXHxFTmVFTmVGTmVFTWRIUGdGTWVNU3FGT2ZGTmVHTmVFTWRFTWVFTmVITWRHUGZFTWVFTmRGTmZGTmVFTWVLU2g4tF83tGBFTWQ3s1/LzdT09faoq7Zwdoieoq58gpLj5OeCh5fq6+2/wsmTmKWQlKJfZnpIUGfU1tu0t8BOVWynlyFSAAAASXRSTlMAkoAHEkDQ/dgYFuf0C8gj+KQQmm1oEuyNWNg53kSXfCYI5tEtzq7ivbOgTBy924R1BfHUibcpYw1JcU7v+7E3Nav6XVPDGraPqQuKawAACh1JREFUeNrsm2lT6kgUhg9QFFUkgWDYZfnAVsi+KQJeQdGqt1xm7jZ3lv//PyYdhe7QWQw1zP0w83xQsY9Um4fTp7vToeBczmaX5MN5rXZO/+NGJzGuLejnkw3dADehLHkQyceAWD5C/0my9XqWPLlCK9WHQScirUMk18g7J9ZosYLFajFyT8siLIpuyQkHKBDw4NgYsnDr0Xybaii60rjYzsmdbrqnw0TvpbvkhjYuzinygDXJXLewR2/O/f73w1cWCUj0LkmiU8SeYsc9LXMZIJNjyXkqmbWQCzV8ICawzLO8jh3q4IyciYfugMnMMGYT4C4UJ2fOEbbSc0EyrVp4T/7u4kiZs6jANjwBxkupWMLG7NIlLZvxM+As3nRLTsD/N5xtekmHIEQuhBAoBuREtmaXWVgB41Smc97JbMZA7pqcKKgopbu7FC1BLUgD22MyeVnPWD0bonLLeCQRhIkzQNnz6gHiK0HmxeF4qkKPSsVygh2x2q50SmlZIGIyiQo8OY+XGVExOLVM2WVRbAkDSma0609aQaxKMgOo6YjQ77Tc8d3laxPRxS7R564yI8WSFkymgUNuJqlbomQLisblpnNAf0nrB1j06rTsA7n0SE5L2skkh+Qcm2CP3vGV2QHWp5Ypu4wDosumRpyzNrBwcFmqk4166dBmrFgJ5aeDKhvSklWLBLokgBhcaF3bFL59lV45EQsR3QLVfV0uAuNFhEy2JaC/fcveMVC8ltKSy3RITtjRl34yDSj0r8rMNkyXQksByJOdCmIdslNAKS7V0BIKdpmGQ1+S9slA2IVa60My89HoRKyZ5XTD8rhBX1DwEN85Gw53drIsT6W0FGTKyYmYtgcI427rI1NB5bQyZZeTuNCSXaEpBX2Cotm9qWqdJOqqajN85y8zTC6E8SGZGalmjja4uaQC0OUy0UzSAckNTKS0FGTKyYmYbfQP42brcFGr/X5+N/XDNVG+36+eXCZ3Kbbkbd644cHBW6bpnTlx0vZO6PL0NI/LE8uksxtUqQ7sUgpoAfp0TgLzqQ4oAFkkeFqadCwFxJMz4SKTwogVpIsaBtrv+qdQzZ8ibSB8cpncJW+Z68iQTBq5EXG6N6UIvTHVr2hPpHTX9ZY5Rf0ImfIEyEMmFWHQmk89gHKhBShCP68UoHVfFtZnqV0yahWYVLTdJyMFwE0ms8l+cnFJfWyIuM2TyuQuecsW4xFJMMcd0S1PzBRQGdkaOKosc4DKYn1amSM2rb4H5lwmaVUVqEXJItoA1LBGokwoHWKUS0AqBZTKxOgocJXJl74uLi+Be+I2TyuTu+SkkCInmrZS3kNXkMnnF9RFT5Qpv1cVJkYwmRzxlavMIRClmTgBYmIeU1bpfC+WqS6RKPOKOTxjaWlZXSpWcp4xq1dBZIaBTxH+v95kySLyCQifSCZ3WYuTnYbDKNvpnVMVPUpulvSGPiFRJlq39M5E95bZNYZXD1icTOaoHophQ1EgLcpkrBOsdLJimbglsstMzpnGxZtSE0vjwlKalGVyuEzZJSXQIxJs2kVVDJOLC6NKVK/0jLWrzEzPYB/G6SxV9pJZq2XlyXSHDqlAjW5XjaSCzfsfom2XiX3hbEN4y3G/r64agy7ZifRrXOa6wmMkmT7YZfbwTuPsUoGi2WUyWOlkxZJIkskGWD7YkpWcb4NtAJlVm8tHYEF2m6KofW/pXLe2INxkTs0QeszB5N5rmJVckg55RzI+gTpEToFySRZ1GAcy94lg8AmOtmtSh2QnNebrTCnmWJlzHRatYeRegbomWSZpU2Cq0UdkdgLKlBMzA2EZNpJkmnmZQ9EwqtSDMijqGU+ZeeSqD/pCkikhZ6ZsU8cNc+kuc3EoU0tgT4hE5q3ELgZCTIBh1nECVAWm0fMs3daA8bV4wUN7f0nhAkdCgkztnx9mZ5iQ+zDLSLxdx5bZFK+Tp8wZDNLqFEAmr5myzRh36TfM8obXX01eAeyaqT4LhYvouMccLzNSRIlZmwGzLnGskVWWWWhBmgBZlXPpOwHieEyA5joGsktZJvumXBN5yzSQW/puGhy2XGBDTjZbWDGXLhMgRZ4ArQF8f375+vnP5y/gFawKYHzlEuOzNPGRSVFgSkT37LcCYDSidpnnCUCQaTmUlyaW1QAyxaVJAVjLLmWZViQSUW+Z9RsWE1DmFuMIOZAddIMtTSrA69PTy/dfXr798QMo7GVmzjXyijleJqVwV7d6t4rL2+NlUeY5GE6bBnNp0yCQTG4zBYVIWGa6y6SMCmDoKZOuFQDVYDI1FWlyJtimQR8/vv76/O319enrl89/wdjLZEnsFeO/nee6NImv8MAW6zpSssylKLMMxrHbebJM2eZohYrkUpL5HhKfqohdesokbZED1oFk0gC5M/Kje+e7nafi9fnl8y8mn1+ef6AtyXSNOV4mZd4q7wAo+8s8fqOdA7httJd3Hwlpo12WeUZUv0PaVWaCuTSVqxgGkznPYTYiP/w32lfAr0+/fAF+++2PV6ApyvSK8ZcpL034LbAWclm2kEU/4i8z8C0wf5mcENQIcTxkJnuTOMV1ZBxkniceqYkmnRmtR4ooQWVSJwbD16b/LbAGTEffnvD705NpC3lZphxzrEwbYVZg2Dd+c9pZZpCb08FltrChj8nsAGpiDD0py9RWUIvAkFWOuwcFuA0ok4bALCuKswQFvTk9gMnL85fvz99h0ttsmp8+tdt9LlOKuXC5OS1fOa42c3jUUrW6sIGetB8bwVCUuUCgYyPBZa6B+w/KpHsVgOq4adBhTQ8RonIOwE3ACRBjGMNquJ/ODcc9YgQ8NtJVYfLn568vMImtVrmcoiitVmLuFON6bMRfpiOPY/QOD3T16juZ9V6AA10+MhkkE0Ys6yuzXFgTY35fzTw6L03iV8MOMbTt8CpJwWVa02C9PSyUt8NPKtBK0hEHuoYAzAH0G0z0c+IEjIGALDMfdeYCuD88ahmrxJnMuBE77qilLHPkKnOZlhLz9CcNnFu06hg7lLBGRx21DMHkr9+ZJ6HFKya4TC9atIOf6woBIX6SK8AhaM8D0D//ELR3ryLXlV4xV0qElhEiz0PQbcNoOx+CvlJgIT6H4xUTHCMGd1LE4aVTKpa+jyf4y/z5jycE7lXwxxO0gtFu5svECRrz/4NDf7dvxjYQwzAMdGEE8RaWq2ySh/cf6OGoyQCRANLkBHenWqnzxyGU6aVP0zRN0zTtmzUru64ZWZ923kC0n6tT9WnnnL+y5R51pj6L9ahlx7k6UR8kVt2Sh1W35GHVLXlYdUseVt2Sh1W3fK8aDmuSOmyfelyGwpqkjtvnnvMyENYcdeA+fSxaDNYUdeg+TovBmqAO3sdpMVjD1eH7OC0Ga7A6QR+nxWANVafo47QYrIHqJH0eWhDWMHWaPosWhTVInahPHzisIepUffrAYQ1QJ+vTgVgD1IP6/AHM0QJdY511NAAAAABJRU5ErkJggg==';
.@{prefix}-icon {
background: url(@icons) no-repeat ~'0 0/230px auto';
}

16
.editorconfig Executable file
View File

@ -0,0 +1,16 @@
# http://editorconfig.org
root = true
[*]
indent_style = tab
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
[Makefile]
indent_style = tab

5
.fatherrc.ts Normal file
View File

@ -0,0 +1,5 @@
export default {
esm: 'rollup',
cjs: 'rollup',
runtimeHelpers: true,
};

40
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -0,0 +1,40 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.

View File

@ -0,0 +1,19 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

25
.gitignore vendored Normal file
View File

@ -0,0 +1,25 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
**/node_modules
/npm-debug.log*
/yarn-error.log
/yarn.lock
/package-lock.json
# production
**/dist
/docs-dist
# misc
.DS_Store
# umi
.umi
.umi-production
.umi-test
.env.local
# log
*.log
.vscode

7
.prettierignore Normal file
View File

@ -0,0 +1,7 @@
**/*.svg
**/*.ejs
**/*.html
package.json
.umi
.umi-production
.umi-test

11
.prettierrc Normal file
View File

@ -0,0 +1,11 @@
{
"singleQuote": true,
"trailingComma": "all",
"printWidth": 80,
"overrides": [
{
"files": ".prettierrc",
"options": { "parser": "json" }
}
]
}

360
.umirc.ts Normal file
View File

@ -0,0 +1,360 @@
import { defineConfig } from 'dumi';
function getMenus(opts: { lang?: string; base: '/docs' | '/plugin' | '/api' }) {
const menus = {
'/docs': [
{
title: 'Introduction',
'title_zh-CN': '介绍',
children: ['/docs/README', '/docs/getting-started'],
},
{
title: 'Basis',
'title_zh-CN': '基础',
children: [
'/docs/concepts-node',
'/docs/concepts-schema',
'/docs/concepts-range',
'/docs/concepts-editor',
'/docs/concepts-event',
'/docs/concepts-plugin',
'/docs/concepts-history',
],
},
{
title: 'Resource',
'title_zh-CN': '资源文件',
children: ['/docs/resources-icon'],
},
{
title: 'Contribution',
'title_zh-CN': '贡献',
path: '/docs/contributing',
},
{
title: 'FAQ',
path: '/docs/faq',
},
],
'/plugin': [
{
title: 'Plug-in development',
'title_zh-CN': '插件开发',
children: [
{
title: opts.lang === 'zh-CN' ? '基础' : 'Basis',
path: `${
opts.lang === 'zh-CN' ? '/zh-CN' : ''
}/plugin/tutorials`,
exact: true,
},
'plugin/tutorials-element',
'plugin/tutorials-mark',
'plugin/tutorials-inline',
'plugin/tutorials-block',
'plugin/tutorials-list',
'plugin/tutorials-card',
],
},
{
title: 'List of plugins',
'title_zh-CN': '插件列表',
children: [
'/plugin/plugin-alignment',
'/plugin/plugin-backcolor',
'/plugin/plugin-bold',
'/plugin/plugin-code',
'/plugin/plugin-codelock',
'/plugin/plugin-file',
'/plugin/plugin-fontcolor',
'/plugin/plugin-fontsize',
'/plugin/plugin-fontfamily',
'/plugin/plugin-heading',
'/plugin/plugin-hr',
'/plugin/plugin-indent',
'/plugin/plugin-italic',
'/plugin/plugin-image',
'/plugin/plugin-link',
'/plugin/plugin-line-height',
{
title: '@aomao/plugin-mark',
path: '/plugin/plugin-mark',
exact: true,
},
'/plugin/plugin-mark-range',
'/plugin/plugin-math',
'/plugin/plugin-mention',
'/plugin/plugin-orderedlist',
'/plugin/plugin-paintformat',
'/plugin/plugin-quote',
'/plugin/plugin-redo',
'/plugin/plugin-removeformat',
'/plugin/plugin-selectall',
'/plugin/plugin-strikethrough',
'/plugin/plugin-status',
'/plugin/plugin-sub',
'/plugin/plugin-sup',
'/plugin/plugin-table',
'/plugin/plugin-tasklist',
'/plugin/plugin-underline',
'/plugin/plugin-undo',
'/plugin/plugin-unorderedlist',
'/plugin/plugin-video',
],
},
],
'/api': [
{
title: 'Node',
'title_zh-CN': 'DOM节点',
children: [
'/api/node',
'/api/editor-node',
'/api/editor-mark',
'/api/editor-inline',
'/api/editor-block',
'/api/editor-list',
],
},
{
title: 'Card',
'title_zh-CN': '卡片',
children: [
{
title: 'Card',
path: `${
opts.lang === 'zh-CN' ? '/zh-CN' : ''
}/api/editor-card`,
exact: true,
},
'/api/editor-card-toolbar',
'/api/editor-card-resize',
'/api/editor-card-maximize',
],
},
{
title: 'Schema',
'title_zh-CN': '架构',
path: '/api/schema',
},
{
title: 'Range',
'title_zh-CN': '光标范围',
children: ['/api/range', '/api/selection'],
},
{
title: 'History',
'title_zh-CN': '历史记录',
path: '/api/history',
},
{
title: 'Editor',
'title_zh-CN': '编辑器',
children: [
{
title: 'Change',
path: `${
opts.lang === 'zh-CN' ? '/zh-CN' : ''
}/api/editor-change`,
children: [
`${
opts.lang === 'zh-CN' ? '/zh-CN' : ''
}/api/editor-change-event`,
],
},
{
title:
opts.lang === 'zh-CN'
? '共有属性和方法'
: 'Common attributes and methods',
path: `${
opts.lang === 'zh-CN' ? '/zh-CN' : ''
}/api/editor`,
exact: true,
},
{
title: opts.lang === 'zh-CN' ? '引擎' : 'Engine',
path: `${
opts.lang === 'zh-CN' ? '/zh-CN' : ''
}/api/engine`,
},
{
title: opts.lang === 'zh-CN' ? '阅读器' : 'View',
path: `${
opts.lang === 'zh-CN' ? '/zh-CN' : ''
}/api/view`,
},
],
},
{
title: 'Language',
'title_zh-CN': '语言',
path: '/api/language',
},
{
title: 'Command',
'title_zh-CN': '命令',
path: '/api/command',
},
{
title: 'Constants',
'title_zh-CN': '常量',
path: '/api/constants',
},
{
title: 'Hotkey',
'title_zh-CN': '热键',
path: '/api/hotkey',
},
{
title: 'Clipboard',
'title_zh-CN': '剪贴板',
path: '/api/clipboard',
},
{
title: 'Parser',
'title_zh-CN': '解析器',
path: '/api/parser',
},
{
title: 'Utility method/constant',
'title_zh-CN': '实用方法/常量',
path: '/api/utils',
},
],
};
return (menus[opts.base] as []).map((menu: any) => {
if (!opts.lang) return menu;
return {
...menu,
title: menu[`title_${opts.lang}`] || menu.title,
};
});
}
export default defineConfig({
title: 'AoMao Editor',
favicon: 'https://cdn-object.yanmao.cc/icon/shortcut.png',
logo: 'https://cdn-object.yanmao.cc/icon/icon.svg',
outputPath: 'docs-dist',
hash: true,
mode: 'site',
locales: [
['en-US', 'English'],
['zh-CN', '中文'],
],
ssr: {
devServerRender: false,
removeWindowInitialProps: true,
},
navs: {
'en-US': [
{
title: 'Edit',
path: '/',
},
{
title: 'View',
path: '/view',
},
{
title: 'Docs',
path: '/docs',
},
{
title: 'Config',
path: '/config',
},
{
title: 'Plug-in',
path: '/plugin',
},
{
title: 'API',
path: '/api',
},
{
title: 'AoMao',
path: 'https://www.yanmao.cc',
},
{
title: 'Github',
path: 'https://github.com/yanmao-cc/am-editor',
},
],
'zh-CN': [
{
title: '编辑',
path: '/zh-CN',
},
{
title: '阅读',
path: '/zh-CN/view',
},
{
title: '文档',
path: '/zh-CN/docs',
},
{
title: '配置',
path: '/zh-CN/config',
},
{
title: '插件',
path: '/zh-CN/plugin',
},
{
title: 'API',
path: '/zh-CN/api',
},
{
title: 'AoMao',
path: 'https://www.yanmao.cc',
},
{
title: 'Github',
path: 'https://github.com/yanmao-cc/am-editor',
},
],
},
menus: {
'/zh-CN/docs': getMenus({ lang: 'zh-CN', base: '/docs' }),
'/docs': getMenus({ base: '/docs' }),
'/zh-CN/plugin': getMenus({ lang: 'zh-CN', base: '/plugin' }),
'/plugin': getMenus({ base: '/plugin' }),
'/zh-CN/api': getMenus({ lang: 'zh-CN', base: '/api' }),
'/api': getMenus({ base: '/api' }),
},
analytics: {
baidu: 'c2e2e4254b6e4388806848d06be68a69',
},
manifest: {
fileName: 'manifest.json',
},
metas: [
{
name: 'viewport',
content:
'viewport-fit=cover,width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no',
},
{
name: 'apple-mobile-web-app-capable',
content: 'yes',
},
{
name: 'apple-mobile-web-app-status-bar-style',
content: 'black',
},
{
name: 'renderer',
content: 'webkit',
},
],
headScripts: [
{
src: 'https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js',
'data-ad-client': 'ca-pub-3706417744839656',
},
],
// more config: https://d.umijs.org/config
});

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2021-present AoMao (me@yanmao.cc)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

331
README.md Normal file
View File

@ -0,0 +1,331 @@
# am-editor
<p align="center">
A rich text <em>collaborative</em> editor framework that can use <em>React</em> and <em>Vue</em> custom plug-ins
</p>
<p align="center">
<a href="https://github.com/yanmao-cc/am-editor/blob/master/README.zh-CN.md"><strong>中文</strong></a> ·
<a href="https://editor.yanmao.cc"><strong>Demo</strong></a> ·
<a href="https://editor.yanmao.cc/docs"><strong>Documentation</strong></a> ·
<a href="#plugins"><strong>Plugins</strong></a> ·
<a href="https://qm.qq.com/cgi-bin/qm/qr?k=Gva5NtZ2USlHSLbFOeMroysk8Uwo7fCS&jump_from=webapi"><strong>QQ-Group 907664876</strong></a> ·
</p>
![aomao-preview](https://user-images.githubusercontent.com/55792257/125074830-62d79300-e0f0-11eb-8d0f-bb96a7775568.png)
<p align="center">
<a href="./packages/engine/package.json">
<img src="https://img.shields.io/npm/l/@aomao/engine">
</a>
<a href="https://unpkg.com/@aomao/engine/dist/index.js">
<img src="http://img.badgesize.io/https://unpkg.com/@aomao/engine/dist/index.js?compression=gzip&amp;label=size">
</a>
<a href="./packages/engine/package.json">
<img src="https://img.shields.io/npm/v/@aomao/engine.svg?maxAge=3600&label=version&colorB=007ec6">
</a>
<a href="https://www.npmjs.com/package/@aomao/engine">
<img src="https://img.shields.io/npm/dw/@aomao/engine">
</a>
<a href="https://github.com/umijs/dumi">
<img src="https://img.shields.io/badge/docs%20by-dumi-blue">
</a>
</p>
> Thanks to Google Translate
Use the `contenteditable` attribute provided by the browser to make a DOM node editable.
The engine takes over most of the browser's default behaviors such as cursors and events.
Monitor the changes of the `DOM` tree in the editing area through `MutationObserver`, and generate a data format of `json0` type to interact with the [ShareDB](https://github.com/share/sharedb) library to achieve collaborative editing Needs.
**`Vue2`** example [https://github.com/zb201307/am-editor-vue2](https://github.com/zb201307/am-editor-vue2)
**`Vue3`** example [https://github.com/yanmao-cc/am-editor/tree/master/examples/vue](https://github.com/yanmao-cc/am-editor/tree/master/examples/vue)
**`React`** example [https://github.com/yanmao-cc/am-editor/tree/master/examples/react](https://github.com/yanmao-cc/am-editor/tree/master/examples/react)
## Features
- Out of the box, it provides dozens of rich plug-ins to meet most needs
- High extensibility, in addition to the basic plug-in of `mark`, inline`and`block`type`, we also provide`card`component combined with`React`, `Vue` and other front-end libraries to render the plug-in UI
- Rich multimedia support, not only supports pictures, audio and video, but also supports insertion of embedded multimedia content
- Support Markdown syntax
- Support internationalization
- The engine is written in pure JavaScript and does not rely on any front-end libraries. Plug-ins can be rendered using front-end libraries such as `React` and `Vue`. Easily cope with complex architecture
- Built-in collaborative editing program, ready to use with lightweight configuration
- Compatible with most of the latest mobile browsers
## Plugins
| **Package** | **Version** | **Size** | **Description** |
| :---------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------- |
| [`@aomao/toolbar`](./packages/toolbar) | [![](https://img.shields.io/npm/v/@aomao/toolbar.svg?maxAge=3600&label=&colorB=007ec6)](./packages/toolbar/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/toolbar/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/toolbar/dist/index.js) | Toolbar, for React. |
| [`@aomao/toolbar-vue`](./packages/toolbar-vue) | [![](https://img.shields.io/npm/v/@aomao/toolbar-vue.svg?maxAge=3600&label=&colorB=007ec6)](./packages/toolbar-vue/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/toolbar-vue/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/toolbar-vue/dist/index.js) | Toolbar, for `Vue3`. |
| [`am-editor-toolbar-vue2`](https://github.com/zb201307/am-editor-vue2/tree/main/packages/toolbar) | [![](https://img.shields.io/npm/v/am-editor-toolbar-vue2.svg?maxAge=3600&label=&colorB=007ec6)](https://github.com/zb201307/am-editor-vue2/blob/main/packages/toolbar/package.json) | [![](http://img.badgesize.io/https://unpkg.com/am-editor-toolbar-vue2/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/am-editor-toolbar-vue2/dist/index.js) | Toolbar, for `Vue2` |
| [`@aomao/plugin-alignment`](./plugins/alignment) | [![](https://img.shields.io/npm/v/@aomao/plugin-alignment.svg?maxAge=3600&label=&colorB=007ec6)](./plugins/alignment/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/plugin-alignment/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/plugin-alignment/dist/index.js) | Alignment. |
| [`@aomao/plugin-backcolor`](./plugins/backcolor) | [![](https://img.shields.io/npm/v/@aomao/plugin-backcolor.svg?maxAge=3600&label=&colorB=007ec6)](./plugins/backcolor/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/plugin-backcolor/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/plugin-backcolor/dist/index.js) | Background color. |
| [`@aomao/plugin-bold`](./plugins/bold) | [![](https://img.shields.io/npm/v/@aomao/plugin-bold.svg?maxAge=3600&label=&colorB=007ec6)](./plugins/bold/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/plugin-bold/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/plugin-bold/dist/index.js) | Bold. |
| [`@aomao/plugin-code`](./plugins/code) | [![](https://img.shields.io/npm/v/@aomao/plugin-code.svg?maxAge=3600&label=&colorB=007ec6)](./plugins/code/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/plugin-code/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/plugin-code/dist/index.js) | Inline code. |
| [`@aomao/plugin-codeblock`](./plugins/codeblock) | [![](https://img.shields.io/npm/v/@aomao/plugin-codeblock.svg?maxAge=3600&label=&colorB=007ec6)](./plugins/codeblock/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/plugin-codeblock/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/plugin-codeblock/dist/index.js) | Code block, for React. |
| [`@aomao/plugin-codeblock-vue`](./plugins/codeblock-vue) | [![](https://img.shields.io/npm/v/@aomao/plugin-codeblock-vue.svg?maxAge=3600&label=&colorB=007ec6)](./plugins/codeblock-vue/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/plugin-codeblock-vue/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/plugin-codeblock-vue/dist/index.js) | Code block, for `Vue3`. |
| [`am-editor-codeblock-vue2`](https://github.com/zb201307/am-editor-vue2/tree/main/packages/codeblock) | [![](https://img.shields.io/npm/v/am-editor-codeblock-vue2.svg?maxAge=3600&label=&colorB=007ec6)](https://github.com/zb201307/am-editor-vue2/tree/main/packages/codeblock/package.json) | [![](http://img.badgesize.io/https://unpkg.com/am-editor-codeblock-vue2/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/am-editor-codeblock-vue2/dist/index.js) | Code Block, for `Vue2` |
| [`@aomao/plugin-fontcolor`](./plugins/fontcolor) | [![](https://img.shields.io/npm/v/@aomao/plugin-fontcolor.svg?maxAge=3600&label=&colorB=007ec6)](./plugins/fontcolor/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/plugin-fontcolor/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/plugin-fontcolor/dist/index.js) | Font color. |
| [`@aomao/plugin-fontfamily`](./plugins/fontfamily) | [![](https://img.shields.io/npm/v/@aomao/plugin-fontfamily.svg?maxAge=3600&label=&colorB=007ec6)](./plugins/fontfamily/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/plugin-fontfamily/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/plugin-fontfamily/dist/index.js) | Font. |
| [`@aomao/plugin-fontsize`](./plugins/fontsize) | [![](https://img.shields.io/npm/v/@aomao/plugin-fontsize.svg?maxAge=3600&label=&colorB=007ec6)](./plugins/fontsize/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/plugin-fontsize/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/plugin-fontsize/dist/index.js) | Font size. |
| [`@aomao/plugin-heading`](./plugins/heading) | [![](https://img.shields.io/npm/v/@aomao/plugin-heading.svg?maxAge=3600&label=&colorB=007ec6)](./plugins/heading/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/plugin-heading/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/plugin-heading/dist/index.js) | Heading. |
| [`@aomao/plugin-hr`](./plugins/hr) | [![](https://img.shields.io/npm/v/@aomao/plugin-hr.svg?maxAge=3600&label=&colorB=007ec6)](./plugins/hr/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/plugin-hr/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/plugin-hr/dist/index.js) | Dividing line. |
| [`@aomao/plugin-indent`](./plugins/indent) | [![](https://img.shields.io/npm/v/@aomao/plugin-indent.svg?maxAge=3600&label=&colorB=007ec6)](./plugins/indent/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/plugin-indent/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/plugin-indent/dist/index.js) | Indent. |
| [`@aomao/plugin-italic`](./plugins/italic) | [![](https://img.shields.io/npm/v/@aomao/plugin-italic.svg?maxAge=3600&label=&colorB=007ec6)](./plugins/italic/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/plugin-italic/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/plugin-italic/dist/index.js) | Italic. |
| [`@aomao/plugin-link`](./plugins/link) | [![](https://img.shields.io/npm/v/@aomao/plugin-link.svg?maxAge=3600&label=&colorB=007ec6)](./plugins/link/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/plugin-link/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/plugin-link/dist/index.js) | Link, for React. |
| [`@aomao/plugin-link-vue`](./plugins/link-vue) | [![](https://img.shields.io/npm/v/@aomao/plugin-link-vue.svg?maxAge=3600&label=&colorB=007ec6)](./plugins/link-vue/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/plugin-link-vue/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/plugin-link-vue/dist/index.js) | Link, for `Vue3`. |
| [`am-editor-link-vue2`](https://github.com/zb201307/am-editor-vue2/tree/main/packages/link) | [![](https://img.shields.io/npm/v/am-editor-link-vue2.svg?maxAge=3600&label=&colorB=007ec6)](https://github.com/zb201307/am-editor-vue2/tree/main/packages/link/package.json) | [![](http://img.badgesize.io/https://unpkg.com/am-editor-link-vue2/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/am-editor-link-vue2/dist/index.js) | Link, for `Vue2` |
| [`@aomao/plugin-line-height`](./plugins/line-height) | [![](https://img.shields.io/npm/v/@aomao/plugin-line-height.svg?maxAge=3600&label=&colorB=007ec6)](./plugins/line-height/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/plugin-line-height/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/plugin-line-height/dist/index.js) | Line height. |
| [`@aomao/plugin-mark`](./plugins/mark) | [![](https://img.shields.io/npm/v/@aomao/plugin-mark.svg?maxAge=3600&label=&colorB=007ec6)](./plugins/mark/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/plugin-mark/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/plugin-mark/dist/index.js) | Mark. |
| [`@aomao/plugin-mention`](./plugins/mention) | [![](https://img.shields.io/npm/v/@aomao/plugin-mention.svg?maxAge=3600&label=&colorB=007ec6)](./plugins/mention/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/plugin-mention/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/plugin-mention/dist/index.js) | Mention |
| [`@aomao/plugin-orderedlist`](./plugins/orderedlist) | [![](https://img.shields.io/npm/v/@aomao/plugin-orderedlist.svg?maxAge=3600&label=&colorB=007ec6)](./plugins/orderedlist/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/plugin-orderedlist/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/plugin-orderedlist/dist/index.js) | Ordered list. |
| [`@aomao/plugin-paintformat`](./plugins/paintformat) | [![](https://img.shields.io/npm/v/@aomao/plugin-paintformat.svg?maxAge=3600&label=&colorB=007ec6)](./plugins/paintformat/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/plugin-paintformat/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/plugin-paintformat/dist/index.js) | Format Painter. |
| [`@aomao/plugin-quote`](./plugins/quote) | [![](https://img.shields.io/npm/v/@aomao/plugin-quote.svg?maxAge=3600&label=&colorB=007ec6)](./plugins/quote/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/plugin-quote/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/plugin-quote/dist/index.js) | Quote block. |
| [`@aomao/plugin-redo`](./plugins/redo) | [![](https://img.shields.io/npm/v/@aomao/plugin-redo.svg?maxAge=3600&label=&colorB=007ec6)](./plugins/redo/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/plugin-redo/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/plugin-redo/dist/index.js) | Redo history. |
| [`@aomao/plugin-removeformat`](./plugins/removeformat) | [![](https://img.shields.io/npm/v/@aomao/plugin-removeformat.svg?maxAge=3600&label=&colorB=007ec6)](./plugins/removeformat/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/plugin-removeformat/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/plugin-removeformat/dist/index.js) | Remove style. |
| [`@aomao/plugin-selectall`](./plugins/selectall) | [![](https://img.shields.io/npm/v/@aomao/plugin-selectall.svg?maxAge=3600&label=&colorB=007ec6)](./plugins/selectall/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/plugin-selectall/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/plugin-selectall/dist/index.js) | Select all. |
| [`@aomao/plugin-status`](./plugins/status) | [![](https://img.shields.io/npm/v/@aomao/plugin-status.svg?maxAge=3600&label=&colorB=007ec6)](./plugins/status/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/plugin-status/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/plugin-status/dist/index.js) | Status. |
| [`@aomao/plugin-strikethrough`](./plugins/strikethrough) | [![](https://img.shields.io/npm/v/@aomao/plugin-strikethrough.svg?maxAge=3600&label=&colorB=007ec6)](./plugins/strikethrough/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/plugin-strikethrough/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/plugin-strikethrough/dist/index.js) | Strikethrough. |
| [`@aomao/plugin-sub`](./plugins/sub) | [![](https://img.shields.io/npm/v/@aomao/plugin-sub.svg?maxAge=3600&label=&colorB=007ec6)](./plugins/sub/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/plugin-sub/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/plugin-sub/dist/index.js) | Sub. |
| [`@aomao/plugin-sup`](./plugins/sup) | [![](https://img.shields.io/npm/v/@aomao/plugin-sup.svg?maxAge=3600&label=&colorB=007ec6)](./plugins/sup/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/plugin-sup/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/plugin-sup/dist/index.js) | Sup. |
| [`@aomao/plugin-tasklist`](./plugins/tasklist) | [![](https://img.shields.io/npm/v/@aomao/plugin-tasklist.svg?maxAge=3600&label=&colorB=007ec6)](./plugins/tasklist/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/plugin-tasklist/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/plugin-tasklist/dist/index.js) | task list. |
| [`@aomao/plugin-underline`](./plugins/underline) | [![](https://img.shields.io/npm/v/@aomao/plugin-underline.svg?maxAge=3600&label=&colorB=007ec6)](./plugins/underline/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/plugin-underline/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/plugin-underline/dist/index.js) | Underline. |
| [`@aomao/plugin-undo`](./plugins/undo) | [![](https://img.shields.io/npm/v/@aomao/plugin-undo.svg?maxAge=3600&label=&colorB=007ec6)](./plugins/undo/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/plugin-undo/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/plugin-undo/dist/index.js) | Undo history. |
| [`@aomao/plugin-unorderedlist`](./plugins/unorderedlist) | [![](https://img.shields.io/npm/v/@aomao/plugin-unorderedlist.svg?maxAge=3600&label=&colorB=007ec6)](./plugins/unorderedlist/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/plugin-unorderedlist/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/plugin-unorderedlist/dist/index.js) | Unordered list. |
| [`@aomao/plugin-image`](./plugins/image) | [![](https://img.shields.io/npm/v/@aomao/plugin-image.svg?maxAge=3600&label=&colorB=007ec6)](./plugins/image/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/plugin-image/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/plugin-image/dist/index.js) | Image. |
| [`@aomao/plugin-table`](./plugins/table) | [![](https://img.shields.io/npm/v/@aomao/plugin-table.svg?maxAge=3600&label=&colorB=007ec6)](./plugins/table/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/plugin-table/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/plugin-table/dist/index.js) | Table. |
| [`@aomao/plugin-file`](./plugins/file) | [![](https://img.shields.io/npm/v/@aomao/plugin-file.svg?maxAge=3600&label=&colorB=007ec6)](./plugins/file/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/plugin-file/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/plugin-file/dist/index.js) | File. |
| [`@aomao/plugin-mark-range`](./plugins/mark-range) | [![](https://img.shields.io/npm/v/@aomao/plugin-mark-range.svg?maxAge=3600&label=&colorB=007ec6)](./plugins/mark-range/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/plugin-mark-range/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/plugin-mark-range/dist/index.js) | Mark the cursor, for example: comment. |
| [`@aomao/plugin-math`](./plugins/math) | [![](https://img.shields.io/npm/v/@aomao/plugin-math.svg?maxAge=3600&label=&colorB=007ec6)](./plugins/math/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/plugin-math/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/plugin-math/dist/index.js) | Mathematical formula. |
| [`@aomao/plugin-video`](./plugins/video) | [![](https://img.shields.io/npm/v/@aomao/plugin-video.svg?maxAge=3600&label=&colorB=007ec6)](./plugins/video/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/plugin-video/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/plugin-video/dist/index.js) | Video. |
## Getting Started
### Installation
The editor consists of `engine`, `toolbar`, and `plugin`. `Engine` provides us with core editing capabilities.
Install engine package using npm or yarn
```bash
$ npm install @aomao/engine
# or
$ yarn add @aomao/engine
```
### Usage
We follow the convention to output a `Hello word!`
```tsx
import React, { useEffect, useRef, useState } from 'react';
import Engine, { EngineInterface } from '@aomao/engine';
const EngineDemo = () => {
//Editor container
const ref = useRef<HTMLDivElement | null>(null);
//Engine instance
const [engine, setEngine] = useState<EngineInterface>();
//Editor content
const [content, setContent] = useState<string>('<p>Hello word!</p>');
useEffect(() => {
if (!ref.current) return;
//Instantiate the engine
const engine = new Engine(ref.current);
//Set the editor value
engine.setValue(content);
//Listen to the editor value change event
engine.on('change', (value) => {
setContent(value);
console.log(`value:${value}`);
});
//Set the engine instance
setEngine(engine);
}, []);
return <div ref={ref} />;
};
export default EngineDemo;
```
### Plugins
Import `@aomao/plugin-bold` bold plug-in
```tsx
import Bold from '@aomao/plugin-bold';
```
Add the `Bold` plugin to the engine
```tsx
//Instantiate the engine
const engine = new Engine(ref.current, {
plugins: [Bold],
});
```
### Card
A card is a separate area in the editor. The UI and logic inside the card can be customized using React, Vue or other front-end libraries to customize the rendering content, and finally mount it to the editor.
Import the `@aomao/plugin-codeblock` code block plugin. The `Language drop-down box` of this plugin is rendered using `React`, so there is a distinction. `Vue3` uses `@aomao/plugin-codeblock-vue`
```tsx
import CodeBlock, { CodeBlockComponent } from '@aomao/plugin-codeblock';
```
Add the `CodeBlock` plugin and `CodeBlockComponent` card component to the engine
```tsx
//Instantiate the engine
const engine = new Engine(ref.current, {
plugins: [CodeBlock],
cards: [CodeBlockComponent],
});
```
The `CodeBlock` plugin supports `markdown` by default. Enter the code block syntax ````javascript` at the beginning of a line in the editor to trigger it after pressing Enter.
### Toolbar
Import the `@aomao/toolbar` toolbar. Due to the complex interaction, the toolbar is basically rendered using `React` + `Antd` UI components, while `Vue3` uses `@aomao/toolbar-vue`
Except for UI interaction, most of the work of the toolbar is just to call the engine to execute the corresponding plug-in commands after different button events are triggered. In the case of complicated requirements or the need to re-customize the UI, it is easier to modify after the fork.
```tsx
import Toolbar, { ToolbarPlugin, ToolbarComponent } from '@aomao/toolbar';
```
Add the `ToolbarPlugin` plugin and `ToolbarComponent` card component to the engine, which allows us to use the shortcut key `/` in the editor to wake up the card toolbar
```tsx
//Instantiate the engine
const engine = new Engine(ref.current, {
plugins: [ToolbarPlugin],
cards: [ToolbarComponent],
});
```
Rendering toolbar, the toolbar has been configured with all plug-ins, here we only need to pass in the plug-in name
```tsx
return (
...
{
engine && (
<Toolbar
engine={engine}
items={[
['collapse'],
[
'bold',
],
]}
/>
)
}
...
)
```
For more complex toolbar configuration, please check the document [https://editor.yanmao.cc/config/toolbar](https://editor.yanmao.cc/config/toolbar)
### Collaborative editing
Collaborative editing is implemented based on the [ShareDB](https://github.com/share/sharedb) open source library. Those who are unfamiliar can learn about it first.
#### Interactive mode
Each editor acts as a [Client](https://github.com/yanmao-cc/am-editor/tree/master/examples/react/components/editor/ot/client.ts) through `WebSocket` and [ Server](https://github.com/yanmao-cc/am-editor/tree/master/ot-server) Communication and exchange of data in `json0` format generated by the editor.
The server will keep a copy of the `html` structure data in the `json` format. After receiving the instructions from the client, it will modify the data, and finally forward it to each client.
Before enabling collaborative editing, we need to configure [Client](https://github.com/yanmao-cc/am-editor/tree/master/examples/react/components/editor/ot/client.ts) and [Server](https://github.com/yanmao-cc/am-editor/tree/master/ot-server)
The server is a `NodeJs` environment, and a network service built using `express` + `WebSocket`.
#### Example
In the example, we have a relatively basic client code
[View the complete React example](https://github.com/yanmao-cc/am-editor/tree/master/examples/react)
[View the complete example of Vue3](https://github.com/yanmao-cc/am-editor/tree/master/examples/vue)
[View the complete example of Vue2](https://github.com/zb201307/am-editor-vue2)
```tsx
//Instantiate the collaborative editing client and pass in the current editor engine instance
const otClient = new OTClient(engine);
//Connect to the collaboration server, `demo` is the same as the server document ID
otClient.connect(
`ws://127.0.0.1:8080${currentMember ? '?uid=' + currentMember.id : ''}`,
'demo',
);
```
### Project icon
[Iconfont](https://at.alicdn.com/t/project/1456030/0cbd04d3-3ca1-4898-b345-e0a9150fcc80.html?spm=a313x.7781069.1998910419.35)
## Development
### React
Need to install dependencies separately in `am-editor root directory` `site-ssr` `ot-server`
```base
//After the dependencies are installed, you only need to execute the following commands in the root directory
yarn ssr
```
- `packages` engine and toolbar
- `plugins` all plugins
- `site-ssr` All backend API and SSR configuration. The egg used. Use yarn ssr in the am-editor root directory to automatically start `site-ssr`
- `ot-server` collaborative server. Start: yarn start
Visit localhost:7001 after startup
### Vue
Just enter the examples/vue directory to install the dependencies
```base
//After the dependencies are installed, execute the following commands in the examples/vue directory
yarn serve
```
In the Vue runtime environment, the default is the installed code that has been published to npm. If you need to modify the code of the engine or plug-in and see the effect immediately, we need to do the following steps:
- Delete the examples/vue/node_modules/@aomao folder
- Delete the examples/vue/node_modules/vue folder. Because there are plugins that depend on Vue, the Vue package will be installed in the project root directory. If you do not delete the Vue package in examples/vue, and the Vue package of the plugin is not in the same environment, the plugin cannot be loaded
- Execute and install all dependent commands in the root directory of am-editor, for example: `yarn`
- Finally restart in examples/vue
There is no backend API configured in the `Vue` case. For details, please refer to `React` and `site-ssr`
## Contribution
Thanks [pleasedmi](https://github.com/pleasedmi)、[Elena211314](https://github.com/Elena211314)、[zb201307](https://github.com/zb201307) for donation
### Alipay
![alipay](https://cdn-object.yanmao.cc/contribution/alipay.png?x-oss-process=image/resize,w_200)
### WeChat Pay
![wechat](https://cdn-object.yanmao.cc/contribution/weichat.png?x-oss-process=image/resize,w_200)
### PayPal
[https://paypal.me/aomaocom](https://paypal.me/aomaocom)

335
README.zh-CN.md Normal file
View File

@ -0,0 +1,335 @@
# am-editor
<p align="center">
一个富文本<em>协同</em>编辑器框架,可以使用<em>React</em><em>Vue</em>自定义插件
</p>
<p align="center">
<a href="https://github.com/yanmao-cc/am-editor/blob/master/README.md"><strong>English</strong></a> ·
<a href="https://editor.yanmao.cc"><strong>Demo</strong></a> ·
<a href="https://editor.yanmao.cc/docs"><strong>文档</strong></a> ·
<a href="#plugins"><strong>插件</strong></a> ·
<a href="https://qm.qq.com/cgi-bin/qm/qr?k=Gva5NtZ2USlHSLbFOeMroysk8Uwo7fCS&jump_from=webapi"><strong>QQ群 907664876</strong></a> ·
</p>
![aomao-preview](https://user-images.githubusercontent.com/55792257/125074830-62d79300-e0f0-11eb-8d0f-bb96a7775568.png)
<p align="center">
<a href="./packages/engine/package.json">
<img src="https://img.shields.io/npm/l/@aomao/engine">
</a>
<a href="https://unpkg.com/@aomao/engine/dist/index.js">
<img src="http://img.badgesize.io/https://unpkg.com/@aomao/engine/dist/index.js?compression=gzip&amp;label=size">
</a>
<a href="./packages/engine/package.json">
<img src="https://img.shields.io/npm/v/@aomao/engine.svg?maxAge=3600&label=version&colorB=007ec6">
</a>
<a href="https://www.npmjs.com/package/@aomao/engine">
<img src="https://img.shields.io/npm/dw/@aomao/engine">
</a>
<a href="https://github.com/umijs/dumi">
<img src="https://img.shields.io/badge/docs%20by-dumi-blue">
</a>
</p>
`广告`[科学上网,方便、快捷的上网冲浪](https://xiyou4you.us/r/?s=18517120) 稳定、可靠,访问 Github 或者其它外网资源很方便。
使用浏览器提供的 `contenteditable` 属性让一个 DOM 节点具有可编辑能力。
引擎接管了浏览器大部分光标、事件等默认行为。
可编辑器区域内的节点通过 `schema` 规则,制定了 `mark` `inline` `block` `card` 4 种组合节点,他们由不同的属性、样式或 `html` 结构组成,并对它们的嵌套进行了一定的约束。
通过 `MutationObserver` 监听编辑区域内的 `DOM` 树的改变,并生成 `json0` 类型的数据格式与 [ShareDB](https://github.com/share/sharedb) 库进行交互,从而达到协同编辑的需要。
**`Vue2`** 案例 [https://github.com/zb201307/am-editor-vue2](https://github.com/zb201307/am-editor-vue2)
**`Vue3`** 案例 [https://github.com/yanmao-cc/am-editor/tree/master/examples/vue](https://github.com/yanmao-cc/am-editor/tree/master/examples/vue)
**`React`** 案例 [https://github.com/yanmao-cc/am-editor/tree/master/examples/react](https://github.com/yanmao-cc/am-editor/tree/master/examples/react)
## 特性
- 开箱即用,提供几十种丰富的插件来满足大部分需求
- 高扩展性,除了 `mark` `inline` `block` 类型基础插件外,我们还提供 `card` 组件结合`React` `Vue`等前端库渲染插件 UI
- 丰富的多媒体支持,不仅支持图片和音视频,更支持插入嵌入式多媒体内容
- 支持 Markdown 语法
- 支持国际化
- 引擎纯 JavaScript 编写,不依赖任何前端库,插件可以使用 `React` `Vue` 等前端库渲染。复杂架构轻松应对
- 内置协同编辑方案,轻量配置即可使用
- 兼容大部分最新移动端浏览器
## 插件
| **包** | **版本** | **大小** | **描述** |
| :---------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :--------------------- |
| [`@aomao/toolbar`](./packages/toolbar) | [![](https://img.shields.io/npm/v/@aomao/toolbar.svg?maxAge=3600&label=&colorB=007ec6)](./packages/toolbar/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/toolbar/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/toolbar/dist/index.js) | 工具栏, 适用于 `React` |
| [`@aomao/toolbar-vue`](./packages/toolbar-vue) | [![](https://img.shields.io/npm/v/@aomao/toolbar-vue.svg?maxAge=3600&label=&colorB=007ec6)](./packages/toolbar-vue/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/toolbar-vue/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/toolbar-vue/dist/index.js) | 工具栏, 适用于 `Vue3` |
| [`am-editor-toolbar-vue2`](https://github.com/zb201307/am-editor-vue2/tree/main/packages/toolbar) | [![](https://img.shields.io/npm/v/am-editor-toolbar-vue2.svg?maxAge=3600&label=&colorB=007ec6)](https://github.com/zb201307/am-editor-vue2/blob/main/packages/toolbar/package.json) | [![](http://img.badgesize.io/https://unpkg.com/am-editor-toolbar-vue2/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/am-editor-toolbar-vue2/dist/index.js) | 工具栏, 适用于 `Vue2` |
| [`@aomao/plugin-alignment`](./plugins/alignment) | [![](https://img.shields.io/npm/v/@aomao/plugin-alignment.svg?maxAge=3600&label=&colorB=007ec6)](./plugins/alignment/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/plugin-alignment/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/plugin-alignment/dist/index.js) | 对齐方式 |
| [`@aomao/plugin-backcolor`](./plugins/backcolor) | [![](https://img.shields.io/npm/v/@aomao/plugin-backcolor.svg?maxAge=3600&label=&colorB=007ec6)](./plugins/backcolor/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/plugin-backcolor/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/plugin-backcolor/dist/index.js) | 背景色 |
| [`@aomao/plugin-bold`](./plugins/bold) | [![](https://img.shields.io/npm/v/@aomao/plugin-bold.svg?maxAge=3600&label=&colorB=007ec6)](./plugins/bold/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/plugin-bold/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/plugin-bold/dist/index.js) | 加粗 |
| [`@aomao/plugin-code`](./plugins/code) | [![](https://img.shields.io/npm/v/@aomao/plugin-code.svg?maxAge=3600&label=&colorB=007ec6)](./plugins/code/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/plugin-code/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/plugin-code/dist/index.js) | 行内代码 |
| [`@aomao/plugin-codeblock`](./plugins/codeblock) | [![](https://img.shields.io/npm/v/@aomao/plugin-codeblock.svg?maxAge=3600&label=&colorB=007ec6)](./plugins/codeblock/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/plugin-codeblock/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/plugin-codeblock/dist/index.js) | 代码块, 适用于 `React` |
| [`@aomao/plugin-codeblock-vue`](./plugins/codeblock-vue) | [![](https://img.shields.io/npm/v/@aomao/plugin-codeblock-vue.svg?maxAge=3600&label=&colorB=007ec6)](./plugins/codeblock-vue/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/plugin-codeblock-vue/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/plugin-codeblock-vue/dist/index.js) | 代码块, 适用于 `Vue3` |
| [`am-editor-codeblock-vue2`](https://github.com/zb201307/am-editor-vue2/tree/main/packages/codeblock) | [![](https://img.shields.io/npm/v/am-editor-codeblock-vue2.svg?maxAge=3600&label=&colorB=007ec6)](https://github.com/zb201307/am-editor-vue2/tree/main/packages/codeblock/package.json) | [![](http://img.badgesize.io/https://unpkg.com/am-editor-codeblock-vue2/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/am-editor-codeblock-vue2/dist/index.js) | 代码块, 适用于 `Vue2` |
| [`@aomao/plugin-fontcolor`](./plugins/fontcolor) | [![](https://img.shields.io/npm/v/@aomao/plugin-fontcolor.svg?maxAge=3600&label=&colorB=007ec6)](./plugins/fontcolor/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/plugin-fontcolor/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/plugin-fontcolor/dist/index.js) | 前景色 |
| [`@aomao/plugin-fontfamily`](./plugins/fontfamily) | [![](https://img.shields.io/npm/v/@aomao/plugin-fontfamily.svg?maxAge=3600&label=&colorB=007ec6)](./plugins/fontfamily/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/plugin-fontfamily/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/plugin-fontfamily/dist/index.js) | 字体 |
| [`@aomao/plugin-fontsize`](./plugins/fontsize) | [![](https://img.shields.io/npm/v/@aomao/plugin-fontsize.svg?maxAge=3600&label=&colorB=007ec6)](./plugins/fontsize/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/plugin-fontsize/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/plugin-fontsize/dist/index.js) | 字体大小 |
| [`@aomao/plugin-heading`](./plugins/heading) | [![](https://img.shields.io/npm/v/@aomao/plugin-heading.svg?maxAge=3600&label=&colorB=007ec6)](./plugins/heading/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/plugin-heading/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/plugin-heading/dist/index.js) | 标题 |
| [`@aomao/plugin-hr`](./plugins/hr) | [![](https://img.shields.io/npm/v/@aomao/plugin-hr.svg?maxAge=3600&label=&colorB=007ec6)](./plugins/hr/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/plugin-hr/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/plugin-hr/dist/index.js) | 分割线 |
| [`@aomao/plugin-indent`](./plugins/indent) | [![](https://img.shields.io/npm/v/@aomao/plugin-indent.svg?maxAge=3600&label=&colorB=007ec6)](./plugins/indent/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/plugin-indent/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/plugin-indent/dist/index.js) | 缩进 |
| [`@aomao/plugin-italic`](./plugins/italic) | [![](https://img.shields.io/npm/v/@aomao/plugin-italic.svg?maxAge=3600&label=&colorB=007ec6)](./plugins/italic/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/plugin-italic/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/plugin-italic/dist/index.js) | 斜体 |
| [`@aomao/plugin-link`](./plugins/link) | [![](https://img.shields.io/npm/v/@aomao/plugin-link.svg?maxAge=3600&label=&colorB=007ec6)](./plugins/link/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/plugin-link/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/plugin-link/dist/index.js) | 链接, 适用于 `React` |
| [`@aomao/plugin-link-vue`](./plugins/link-vue) | [![](https://img.shields.io/npm/v/@aomao/plugin-link-vue.svg?maxAge=3600&label=&colorB=007ec6)](./plugins/link-vue/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/plugin-link-vue/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/plugin-link-vue/dist/index.js) | 链接, 适用于 `Vue3` |
| [`am-editor-link-vue2`](https://github.com/zb201307/am-editor-vue2/tree/main/packages/link) | [![](https://img.shields.io/npm/v/am-editor-link-vue2.svg?maxAge=3600&label=&colorB=007ec6)](https://github.com/zb201307/am-editor-vue2/tree/main/packages/link/package.json) | [![](http://img.badgesize.io/https://unpkg.com/am-editor-link-vue2/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/am-editor-link-vue2/dist/index.js) | 链接, 适用于 `Vue2` |
| [`@aomao/plugin-line-height`](./plugins/line-height) | [![](https://img.shields.io/npm/v/@aomao/plugin-line-height.svg?maxAge=3600&label=&colorB=007ec6)](./plugins/line-height/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/plugin-line-height/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/plugin-line-height/dist/index.js) | 行高 |
| [`@aomao/plugin-mark`](./plugins/mark) | [![](https://img.shields.io/npm/v/@aomao/plugin-mark.svg?maxAge=3600&label=&colorB=007ec6)](./plugins/mark/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/plugin-mark/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/plugin-mark/dist/index.js) | 标记 |
| [`@aomao/plugin-mention`](./plugins/mention) | [![](https://img.shields.io/npm/v/@aomao/plugin-mention.svg?maxAge=3600&label=&colorB=007ec6)](./plugins/mention/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/plugin-mention/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/plugin-mention/dist/index.js) | 提及 |
| [`@aomao/plugin-orderedlist`](./plugins/orderedlist) | [![](https://img.shields.io/npm/v/@aomao/plugin-orderedlist.svg?maxAge=3600&label=&colorB=007ec6)](./plugins/orderedlist/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/plugin-orderedlist/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/plugin-orderedlist/dist/index.js) | 有序列表 |
| [`@aomao/plugin-paintformat`](./plugins/paintformat) | [![](https://img.shields.io/npm/v/@aomao/plugin-paintformat.svg?maxAge=3600&label=&colorB=007ec6)](./plugins/paintformat/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/plugin-paintformat/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/plugin-paintformat/dist/index.js) | 格式刷 |
| [`@aomao/plugin-quote`](./plugins/quote) | [![](https://img.shields.io/npm/v/@aomao/plugin-quote.svg?maxAge=3600&label=&colorB=007ec6)](./plugins/quote/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/plugin-quote/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/plugin-quote/dist/index.js) | 引用块 |
| [`@aomao/plugin-redo`](./plugins/redo) | [![](https://img.shields.io/npm/v/@aomao/plugin-redo.svg?maxAge=3600&label=&colorB=007ec6)](./plugins/redo/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/plugin-redo/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/plugin-redo/dist/index.js) | 重做 |
| [`@aomao/plugin-removeformat`](./plugins/removeformat) | [![](https://img.shields.io/npm/v/@aomao/plugin-removeformat.svg?maxAge=3600&label=&colorB=007ec6)](./plugins/removeformat/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/plugin-removeformat/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/plugin-removeformat/dist/index.js) | 移除样式 |
| [`@aomao/plugin-selectall`](./plugins/selectall) | [![](https://img.shields.io/npm/v/@aomao/plugin-selectall.svg?maxAge=3600&label=&colorB=007ec6)](./plugins/selectall/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/plugin-selectall/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/plugin-selectall/dist/index.js) | 全选 |
| [`@aomao/plugin-status`](./plugins/status) | [![](https://img.shields.io/npm/v/@aomao/plugin-status.svg?maxAge=3600&label=&colorB=007ec6)](./plugins/status/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/plugin-status/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/plugin-status/dist/index.js) | 状态 |
| [`@aomao/plugin-strikethrough`](./plugins/strikethrough) | [![](https://img.shields.io/npm/v/@aomao/plugin-strikethrough.svg?maxAge=3600&label=&colorB=007ec6)](./plugins/strikethrough/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/plugin-strikethrough/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/plugin-strikethrough/dist/index.js) | 删除线 |
| [`@aomao/plugin-sub`](./plugins/sub) | [![](https://img.shields.io/npm/v/@aomao/plugin-sub.svg?maxAge=3600&label=&colorB=007ec6)](./plugins/sub/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/plugin-sub/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/plugin-sub/dist/index.js) | 下标 |
| [`@aomao/plugin-sup`](./plugins/sup) | [![](https://img.shields.io/npm/v/@aomao/plugin-sup.svg?maxAge=3600&label=&colorB=007ec6)](./plugins/sup/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/plugin-sup/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/plugin-sup/dist/index.js) | 上标 |
| [`@aomao/plugin-tasklist`](./plugins/tasklist) | [![](https://img.shields.io/npm/v/@aomao/plugin-tasklist.svg?maxAge=3600&label=&colorB=007ec6)](./plugins/tasklist/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/plugin-tasklist/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/plugin-tasklist/dist/index.js) | 任务列表 |
| [`@aomao/plugin-underline`](./plugins/underline) | [![](https://img.shields.io/npm/v/@aomao/plugin-underline.svg?maxAge=3600&label=&colorB=007ec6)](./plugins/underline/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/plugin-underline/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/plugin-underline/dist/index.js) | 下划线 |
| [`@aomao/plugin-undo`](./plugins/undo) | [![](https://img.shields.io/npm/v/@aomao/plugin-undo.svg?maxAge=3600&label=&colorB=007ec6)](./plugins/undo/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/plugin-undo/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/plugin-undo/dist/index.js) | 撤销 |
| [`@aomao/plugin-unorderedlist`](./plugins/unorderedlist) | [![](https://img.shields.io/npm/v/@aomao/plugin-unorderedlist.svg?maxAge=3600&label=&colorB=007ec6)](./plugins/unorderedlist/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/plugin-unorderedlist/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/plugin-unorderedlist/dist/index.js) | 无序列表 |
| [`@aomao/plugin-image`](./plugins/image) | [![](https://img.shields.io/npm/v/@aomao/plugin-image.svg?maxAge=3600&label=&colorB=007ec6)](./plugins/image/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/plugin-image/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/plugin-image/dist/index.js) | 图片 |
| [`@aomao/plugin-table`](./plugins/table) | [![](https://img.shields.io/npm/v/@aomao/plugin-table.svg?maxAge=3600&label=&colorB=007ec6)](./plugins/table/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/plugin-table/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/plugin-table/dist/index.js) | 表格 |
| [`@aomao/plugin-file`](./plugins/file) | [![](https://img.shields.io/npm/v/@aomao/plugin-file.svg?maxAge=3600&label=&colorB=007ec6)](./plugins/file/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/plugin-file/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/plugin-file/dist/index.js) | 文件 |
| [`@aomao/plugin-mark-range`](./plugins/mark-range) | [![](https://img.shields.io/npm/v/@aomao/plugin-mark-range.svg?maxAge=3600&label=&colorB=007ec6)](./plugins/mark-range/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/plugin-mark-range/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/plugin-mark-range/dist/index.js) | 标记光标, 例如: 批注. |
| [`@aomao/plugin-math`](./plugins/math) | [![](https://img.shields.io/npm/v/@aomao/plugin-math.svg?maxAge=3600&label=&colorB=007ec6)](./plugins/math/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/plugin-math/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/plugin-math/dist/index.js) | 数学公式 |
| [`@aomao/plugin-video`](./plugins/video) | [![](https://img.shields.io/npm/v/@aomao/plugin-video.svg?maxAge=3600&label=&colorB=007ec6)](./plugins/video/package.json) | [![](http://img.badgesize.io/https://unpkg.com/@aomao/plugin-video/dist/index.js?compression=gzip&label=%20)](https://unpkg.com/@aomao/plugin-video/dist/index.js) | 视频 |
## 快速上手
### 安装
编辑器由 `引擎`、`工具栏`、`插件` 组成。`引擎` 为我们提供了核心的编辑能力。
使用 npm 或者 yarn 安装引擎包
```bash
$ npm install @aomao/engine
# or
$ yarn add @aomao/engine
```
### 使用
我们按照惯例先输出一个`Hello word!`
```tsx
import React, { useEffect, useRef, useState } from 'react';
import Engine, { EngineInterface } from '@aomao/engine';
const EngineDemo = () => {
//编辑器容器
const ref = useRef<HTMLDivElement | null>(null);
//引擎实例
const [engine, setEngine] = useState<EngineInterface>();
//编辑器内容
const [content, setContent] = useState<string>('<p>Hello word!</p>');
useEffect(() => {
if (!ref.current) return;
//实例化引擎
const engine = new Engine(ref.current);
//设置编辑器值
engine.setValue(content);
//监听编辑器值改变事件
engine.on('change', (value) => {
setContent(value);
console.log(`value:${value}`);
});
//设置引擎实例
setEngine(engine);
}, []);
return <div ref={ref} />;
};
export default EngineDemo;
```
### 插件
引入 `@aomao/plugin-bold` 加粗插件
```tsx
import Bold from '@aomao/plugin-bold';
```
`Bold` 插件加入引擎
```tsx
//实例化引擎
const engine = new Engine(ref.current, {
plugins: [Bold],
});
```
### 卡片
卡片是编辑器中单独划分的一个区域,其 UI 以及逻辑在卡片内部可以使用 React、Vue 或其它前端库自定义渲染内容,最后再挂载到编辑器上。
引入 `@aomao/plugin-codeblock` 代码块插件,这个插件的 `语言下拉框` 使用 `React` 渲染,所以有区分。 `Vue3` 使用 `@aomao/plugin-codeblock-vue`
```tsx
import CodeBlock, { CodeBlockComponent } from '@aomao/plugin-codeblock';
```
`CodeBlock` 插件和 `CodeBlockComponent` 卡片组件加入引擎
```tsx
//实例化引擎
const engine = new Engine(ref.current, {
plugins: [CodeBlock],
cards: [CodeBlockComponent],
});
```
`CodeBlock` 插件默认支持 `markdown`,在编辑器一行开头位置输入代码块语法` ```javascript ` 回车后即可触发。
### 工具栏
引入 `@aomao/toolbar` 工具栏,工具栏由于交互复杂,基本上都是使用 `React` + `Antd` UI 组件渲染,`Vue3` 使用 `@aomao/toolbar-vue`
工具栏除了 UI 交互外,大部分工作只是对不同的按钮事件触发后调用了引擎执行对应的插件命令,在需求比较复杂或需要重新定制 UI 的情况下Fork 后修改起来也比较容易。
```tsx
import Toolbar, { ToolbarPlugin, ToolbarComponent } from '@aomao/toolbar';
```
`ToolbarPlugin` 插件和 `ToolbarComponent` 卡片组件加入引擎,它可以让我们在编辑器中可以使用快捷键 `/` 唤醒出卡片工具栏
```tsx
//实例化引擎
const engine = new Engine(ref.current, {
plugins: [ToolbarPlugin],
cards: [ToolbarComponent],
});
```
渲染工具栏,工具栏已配置好所有插件,这里我们只需要传入插件名称即可
```tsx
return (
...
{
engine && (
<Toolbar
engine={engine}
items={[
['collapse'],
[
'bold',
],
]}
/>
)
}
...
)
```
更复杂的工具栏配置请查看文档 [https://editor.yanmao.cc/zh-CN/config/toolbar](https://editor.yanmao.cc/zh-CN/config/toolbar)
### 协同编辑
协同编辑基于 [ShareDB](https://github.com/share/sharedb) 开源库实现,比较陌生的朋友可以先了解它。
#### 交互模式
每位编辑者作为 [客户端](https://github.com/yanmao-cc/am-editor/tree/master/examples/react/components/editor/ot/client.ts) 通过 `WebSocket` 与 [服务端](https://github.com/yanmao-cc/am-editor/tree/master/ot-server) 通信交换由编辑器生成的 `json0` 格式的数据。
服务端会保留一份 `json` 格式的 `html` 结构数据,接收到来自客户端的指令后,再去修改这份数据,最后再转发到每个客户端。
在启用协同编辑前,我们需要配置好 [客户端](https://github.com/yanmao-cc/am-editor/tree/master/examples/react/components/editor/ot/client.ts) 和 [服务端](https://github.com/yanmao-cc/am-editor/tree/master/ot-server)
服务端是 `NodeJs` 环境,使用 `express` + `WebSocket` 搭建的网络服务。
#### 案例
案例中我们已经一份比较基础的客户端代码
[查看 React 完整案例](https://github.com/yanmao-cc/am-editor/tree/master/examples/react)
[查看 Vue3 完整案例](https://github.com/yanmao-cc/am-editor/tree/master/examples/vue)
[查看 Vue2 完整案例](https://github.com/zb201307/am-editor-vue2)
```tsx
//实例化协作编辑客户端,传入当前编辑器引擎实例
const otClient = new OTClient(engine);
//连接到协作服务端,`demo` 与服务端文档ID相同
otClient.connect(
`ws://127.0.0.1:8080${currentMember ? '?uid=' + currentMember.id : ''}`,
'demo',
);
```
### 项目图标
[Iconfont](https://at.alicdn.com/t/project/1456030/0cbd04d3-3ca1-4898-b345-e0a9150fcc80.html?spm=a313x.7781069.1998910419.35)
## 开发
### React
需要在 `am-editor 根目录` `site-ssr` `ot-server` 中分别安装依赖
```base
//依赖安装好后,只需要在根目录执行以下命令
yarn ssr
```
- `packages` 引擎和工具栏
- `plugins` 所有的插件
- `site-ssr` 所有的后端 API 和 SSR 配置。使用的 egg 。在 am-editor 根目录下使用 yarn ssr 自动启动 `site-ssr`
- `ot-server` 协同服务端。启动yarn start
启动后访问 localhost:7001
### Vue
只需要进入 examples/vue 目录安装依赖
```base
//依赖安装好后,在 examples/vue 目录执行以下命令
yarn serve
```
在 Vue 运行环境中,默认是安装的已发布到 npm 上的代码。如果需要修改引擎或者插件的代码后立即看到效果,我们需要做以下步骤:
- 删除 examples/vue/node_modules/@aomao 文件夹
- 删除 examples/vue/node_modules/vue 文件夹。因为有插件依赖了 Vue所以 Vue 的包会在项目根目录中安装。如果不删除 examples/vue 中的 Vue 包,和插件的 Vue 包不在一个环境中,就无法加载插件
- 在 am-editor 根目录下执行安装所有依赖命令,例如:`yarn`
- 最后在 examples/vue 中重新启动
`Vue` 案例中没有配置任何后端 API具体可以参考 `React``site-ssr`
## 贡献
感谢 [pleasedmi](https://github.com/pleasedmi)、[Elena211314](https://github.com/Elena211314)、[zb201307](https://github.com/zb201307) 的捐赠
如果您愿意,可以在这里留下你的名字。
### 支付宝
![alipay](https://cdn-object.yanmao.cc/contribution/alipay.png?x-oss-process=image/resize,w_200)
### 微信支付
![wechat](https://cdn-object.yanmao.cc/contribution/weichat.png?x-oss-process=image/resize,w_200)
### PayPal
[https://paypal.me/aomaocom](https://paypal.me/aomaocom)

68
docs/api/clipboard.md Normal file
View File

@ -0,0 +1,68 @@
# Clipboard
Clipboard related operations
Type: `ClipboardInterface`
## Constructor
```ts
new (editor: EditorInterface): CommandInterface
```
## Method
### `getData`
Get clipboard data
```ts
/**
* Get clipboard data
* @param event event
*/
getData(event: DragEvent | ClipboardEvent): ClipboardData;
```
### `write`
Write to clipboard
```ts
/**
* Write to clipboard
* @param event event
* @param range cursor, get the current cursor position by default
* @param callback callback
*/
write(
event: ClipboardEvent,
range?: RangeInterface | null,
callback?: (data: {html: string; text: string }) => void,
): void;
```
### `cut`
Perform cut and paste operations at the current cursor position
```ts
/**
* Perform cut and paste operations at the current cursor position
*/
cut(): void;
```
### `copy`
copy
```ts
/**
* Copy
* @param data The data to be copied, which can be a node or a string
* @param trigger Whether to trigger the clipping event and notify the plug-in to process the conversion
* @returns returns whether the copy is successful
*/
copy(data: Node | string, trigger?: boolean): boolean;
```

View File

@ -0,0 +1,68 @@
# 剪贴板
剪贴板相关操作
类型:`ClipboardInterface`
## 构造函数
```ts
new (editor: EditorInterface): CommandInterface
```
## 方法
### `getData`
获取剪贴板数据
```ts
/**
* 获取剪贴板数据
* @param event 事件
*/
getData(event: DragEvent | ClipboardEvent): ClipboardData;
```
### `write`
写入剪贴板
```ts
/**
* 写入剪贴板
* @param event 事件
* @param range 光标,默认获取当前光标位置
* @param callback 回调
*/
write(
event: ClipboardEvent,
range?: RangeInterface | null,
callback?: (data: { html: string; text: string }) => void,
): void;
```
### `cut`
在当前光标位置执行剪贴操作
```ts
/**
* 在当前光标位置执行剪贴操作
*/
cut(): void;
```
### `copy`
复制
```ts
/**
* 复制
* @param data 要复制的数据,可以是节点或者字符串
* @param trigger 是否触发剪贴事件,通知插件处理转换
* @returns 返回是否复制成功
*/
copy(data: Node | string, trigger?: boolean): boolean;
```

45
docs/api/command.md Normal file
View File

@ -0,0 +1,45 @@
# Command
Execute plugin commands
Type: `CommandInterface`
## Constructor
```ts
new (editor: EditorInterface): CommandInterface
```
## Method
### `queryEnabled`
Query whether there is a command to enable the specified plug-in
```ts
queryEnabled(name: string): boolean;
```
### `queryState`
Check plug-in status
```ts
queryState(name: string, ...args: any): any;
```
### `execute`
Execute plugin commands
```ts
execute(name: string, ...args: any): any;
```
### `executeMethod`
To simply execute the plug-in method, you need to ensure that there are methods defined in the plug-in that need to be called. The difference with `execute`: the `execute` method mainly changes the editor
```ts
executeMethod(name: string, method: string, ...args: any): any;
```

45
docs/api/command.zh-CN.md Normal file
View File

@ -0,0 +1,45 @@
# 命令
执行插件命令
类型:`CommandInterface`
## 构造函数
```ts
new (editor: EditorInterface): CommandInterface
```
## 方法
### `queryEnabled`
查询是否有启用指定插件命令
```ts
queryEnabled(name: string): boolean;
```
### `queryState`
查询插件状态
```ts
queryState(name: string, ...args: any): any;
```
### `execute`
执行插件命令
```ts
execute(name: string, ...args: any): any;
```
### `executeMethod`
单纯的执行插件方法,需要保证插件中有定义需要调用的方法。与 `execute` 的区别:`execute` 方法主要对编辑器有所更改
```ts
executeMethod(name: string, method: string, ...args: any): any;
```

111
docs/api/constants.md Normal file
View File

@ -0,0 +1,111 @@
# Constant
## Node
### `DATA_ELEMENT`
Mark node type
### `ROOT`
Mark as root node
### `ROOT_SELECTOR`
Root node selector
### `UI`
Mark as UI node
### `UI_SELECTOR`
UI node CSS selector
### `EDITABLE`
Mark as editable node
### `EDITABLE_SELECTOR`
Editable node CSS selector
### `DATA_TRANSIENT_ATTRIBUTES`
Mark node attributes that do not participate in collaboration
### `DATA_TRANSIENT_ELEMENT`
Mark nodes that do not participate in collaboration
## Selection area
### `ANCHOR`
Start node marker
### `FOCUS`
End node marker
### `CURSOR`
Mark where the start position and end position coincide
### `ANCHOR_SELECTOR`
Start Node Marker CSS Queryer
### `FOCUS_SELECTOR`
End node marker CSS finder
### `CURSOR_SELECTOR`
Mark the CSS finder where the start position and end position coincide
## Card
### `CARD_TAG`
Card node label name
### `CARD_KEY`
Card name
### `READY_CARD_KEY`
Name of the card to be rendered
### `CARD_TYPE_KEY`
Card type
### `CARD_VALUE_KEY`
Card value
### `CARD_ELEMENT_KEY`
Card node
### `CARD_SELECTOR`
Card CSS selector
### `READY_CARD_SELECTOR`
CSS selector for the card to be rendered
### `CARD_LEFT_SELECTOR`
CSS selector on the left side of the card
### `CARD_CENTER_SELECTOR`
CSS selector for card center node
### `CARD_RIGHT_SELECTOR`
CSS selector on the right side of the card

111
docs/api/constants.zh-CN.md Normal file
View File

@ -0,0 +1,111 @@
# 常量
## 节点
### `DATA_ELEMENT`
标记节点类型
### `ROOT`
标记为根节点
### `ROOT_SELECTOR`
根节点选择器
### `UI`
标记为 UI 节点
### `UI_SELECTOR`
UI 节点 CSS 选择器
### `EDITABLE`
标记为可编辑器节点
### `EDITABLE_SELECTOR`
可编辑节点 CSS 选择器
### `DATA_TRANSIENT_ATTRIBUTES`
标记不参与协同的节点属性
### `DATA_TRANSIENT_ELEMENT`
标记不参与协同的节点
## 选区范围
### `ANCHOR`
开始节点标记
### `FOCUS`
结束节点标记
### `CURSOR`
开始位置和结束位置重合处标记
### `ANCHOR_SELECTOR`
开始节点标记 CSS 查询器
### `FOCUS_SELECTOR`
结束节点标记 CSS 查询器
### `CURSOR_SELECTOR`
开始位置和结束位置重合处标记 CSS 查询器
## 卡片
### `CARD_TAG`
卡片节点标签名称
### `CARD_KEY`
卡片名称
### `READY_CARD_KEY`
待渲染卡片名称
### `CARD_TYPE_KEY`
卡片类型
### `CARD_VALUE_KEY`
卡片值
### `CARD_ELEMENT_KEY`
卡片节点
### `CARD_SELECTOR`
卡片 CSS 选择器
### `READY_CARD_SELECTOR`
待渲染卡片 CSS 选择器
### `CARD_LEFT_SELECTOR`
卡片左侧 CSS 选择器
### `CARD_CENTER_SELECTOR`
卡片中心节点 CSS 选择器
### `CARD_RIGHT_SELECTOR`
卡片右侧 CSS 选择器

320
docs/api/editor-block.md Normal file
View File

@ -0,0 +1,320 @@
# BlockModel
Edit related operations of block-level nodes
Type: `BlockModelInterface`
## Use
```ts
new Engine(...).block
```
## Constructor
```ts
new (editor: EditorInterface): BlockModelInterface
```
## Method
### `init`
initialization
```ts
/**
* Initialization
*/
init(): void;
```
### `findPlugin`
Find the block plugin instance according to the node
```ts
/**
* Find the block plugin instance according to the node
* @param node node
*/
findPlugin(node: NodeInterface): BlockInterface | undefined;
```
### `findTop`
Find the first-level node of the Block node. For example, div -> H2 returns H2 node
```ts
/**
* Find the first level node of the Block node. For example, div -> H2 returns H2 node
* @param parentNode parent node
* @param childNode child node
*/
findTop(parentNode: NodeInterface, childNode: NodeInterface): NodeInterface;
```
### `closest`
Get the nearest block node, can not find the return node
```ts
/**
* Get the nearest block node, the return node cannot be found
* @param node node
*/
closest(node: NodeInterface): NodeInterface;
```
### `wrap`
Wrap a block node at the cursor position
```ts
/**
* Wrap a block node at the cursor position
* @param block node
* @param range cursor
*/
wrap(block: NodeInterface | Node | string, range?: RangeInterface): void;
```
### `unwrap`
Remove the package of the block node where the cursor is located
```ts
/**
* Remove the package of the block node where the cursor is located
* @param block node
* @param range cursor
*/
unwrap(block: NodeInterface | Node | string, range?: RangeInterface): void;
```
### `getSiblings`
Get the node's sibling node set relative to the cursor start position and end position
```ts
/**
* Get the node's sibling node set relative to the cursor start position and end position
* @param range cursor
* @param block node
*/
getSiblings(
range: RangeInterface,
block: NodeInterface,
): Array<{ node: NodeInterface; position:'left' |'center' |'right' }>;
```
### `split`
Split the block node selected by the current cursor
```ts
/**
* Split the block node selected by the current cursor
* @param range cursor
*/
split(range?: RangeInterface): void;
```
### `insert`
Insert a block node at the current cursor position
```ts
/**
* Insert a block node at the current cursor position
* @param block node
* @param range cursor
* @param splitNode split node, the default is the block node at the beginning of the cursor
*/
insert(
block: NodeInterface | Node | string,
range?: RangeInterface,
splitNode?: (node: NodeInterface) => NodeInterface,
): void;
```
### `setBlocks`
Set all block nodes where the current cursor is located as new nodes or set new attributes
```ts
/**
* Set all block nodes where the current cursor is located as new nodes or set new attributes
* @param block The node or node attribute that needs to be set
* @param range cursor
*/
setBlocks(
block: string | {[k: string]: any },
range?: RangeInterface,
): void;
```
### `merge`
Merge blocks adjacent to the current cursor position
```ts
/**
* Combine blocks adjacent to the current cursor position
* @param range cursor
*/
merge(range?: RangeInterface): void;
```
### `findBlocks`
Find all blocks that have an effect on the range
```ts
/**
* Find all blocks that have an effect on the range
* @param range
*/
findBlocks(range: RangeInterface): Array<NodeInterface>;
```
### `isFirstOffset`
Determine whether the {Edge}Offset of the range is at the beginning of the Block
```ts
/**
* Determine whether the {Edge}Offset of the range is at the beginning of the Block
* @param range cursor
* @param edge start end
*/
isFirstOffset(range: RangeInterface, edge:'start' |'end'): boolean;
```
### `isLastOffset`
Determine whether the {Edge}Offset of the range is at the last position of the Block
```ts
/**
* Determine whether the {Edge}Offset of the range is at the last position of the Block
* @param range cursor
* @param edge start end
*/
isLastOffset(range: RangeInterface, edge:'start' |'end'): boolean;
```
### `getBlocks`
Get all blocks in the range
```ts
/**
* Get all blocks in the range
* @param range cursors
*/
getBlocks(range: RangeInterface): Array<NodeInterface>;
```
### `getLeftText`
Get the left text of Block
```ts
/**
* Get the left text of Block
* @param block node
*/
getLeftText(block: NodeInterface | Node): string;
```
### `removeLeftText`
Delete the left text of Block
```ts
/**
* Delete the text on the left side of Block
* @param block node
*/
removeLeftText(block: NodeInterface | Node): void;
```
### `getBlockByRange`
Generate the node on the left or right side of the cursor and place it in the same container as the parent node
```ts
/**
* Generate the node on the left or right side of the cursor and place it in the same container as the parent node
* isLeft = true: left
* isLeft = false: the right side
* @param {block,range,isLeft,clone,keepID} node, cursor, left or right, whether to copy, whether to keep id
*
*/
getBlockByRange({
block,
range,
isLeft,
clone,
keepID,
}: {
block: NodeInterface | Node;
range: RangeInterface;
isLeft: boolean;
clone?: boolean;
keepID?: boolean;
}): NodeInterface;
```
### `normal`
Sort block-level nodes into standard editor values
```ts
/**
* Sorting block-level nodes
* @param node node
* @param root root node
*/
normal(node: NodeInterface, root: NodeInterface): void;
```
### `brToBlock`
br change line to paragraph
```ts
/**
* br change lines to paragraphs
* @param block node
*/
brToBlock(block: NodeInterface): void;
```
### `insertEmptyBlock`
Insert an empty block node
```ts
/**
* Insert an empty block node
* @param range cursor position
* @param block node
* @returns
*/
insertEmptyBlock(range: RangeInterface, block: NodeInterface): void;
```
### `insertOrSplit`
Insert or split node at cursor position
```ts
/**
* Insert or split a node at the cursor position
* @param range cursor position
* @param block node
*/
insertOrSplit(range: RangeInterface, block: NodeInterface): void;
```

View File

@ -0,0 +1,320 @@
# BlockModel
编辑块级节点的相关操作
类型:`BlockModelInterface`
## 使用
```ts
new Engine(...).block
```
## 构造函数
```ts
new (editor: EditorInterface): BlockModelInterface
```
## 方法
### `init`
初始化
```ts
/**
* 初始化
*/
init(): void;
```
### `findPlugin`
根据节点查找 block 插件实例
```ts
/**
* 根据节点查找block插件实例
* @param node 节点
*/
findPlugin(node: NodeInterface): BlockInterface | undefined;
```
### `findTop`
查找 Block 节点的一级节点。如 div -> H2 返回 H2 节点
```ts
/**
* 查找Block节点的一级节点。如 div -> H2 返回 H2节点
* @param parentNode 父节点
* @param childNode 子节点
*/
findTop(parentNode: NodeInterface, childNode: NodeInterface): NodeInterface;
```
### `closest`
获取最近的 block 节点,找不到返回 node
```ts
/**
* 获取最近的block节点找不到返回 node
* @param node 节点
*/
closest(node: NodeInterface): NodeInterface;
```
### `wrap`
在光标位置包裹一个 block 节点
```ts
/**
* 在光标位置包裹一个block节点
* @param block 节点
* @param range 光标
*/
wrap(block: NodeInterface | Node | string, range?: RangeInterface): void;
```
### `unwrap`
移除光标所在 block 节点包裹
```ts
/**
* 移除光标所在block节点包裹
* @param block 节点
* @param range 光标
*/
unwrap(block: NodeInterface | Node | string, range?: RangeInterface): void;
```
### `getSiblings`
获取节点相对于光标开始位置、结束位置下的兄弟节点集合
```ts
/**
* 获取节点相对于光标开始位置、结束位置下的兄弟节点集合
* @param range 光标
* @param block 节点
*/
getSiblings(
range: RangeInterface,
block: NodeInterface,
): Array<{ node: NodeInterface; position: 'left' | 'center' | 'right' }>;
```
### `split`
分割当前光标选中的 block 节点
```ts
/**
* 分割当前光标选中的block节点
* @param range 光标
*/
split(range?: RangeInterface): void;
```
### `insert`
在当前光标位置插入 block 节点
```ts
/**
* 在当前光标位置插入block节点
* @param block 节点
* @param range 光标
* @param splitNode 分割节点默认为光标开始位置的block节点
*/
insert(
block: NodeInterface | Node | string,
range?: RangeInterface,
splitNode?: (node: NodeInterface) => NodeInterface,
): void;
```
### `setBlocks`
设置当前光标所在的所有 block 节点为新的节点或设置新属性
```ts
/**
* 设置当前光标所在的所有block节点为新的节点或设置新属性
* @param block 需要设置的节点或者节点属性
* @param range 光标
*/
setBlocks(
block: string | { [k: string]: any },
range?: RangeInterface,
): void;
```
### `merge`
合并当前光标位置相邻的 block
```ts
/**
* 合并当前光标位置相邻的block
* @param range 光标
*/
merge(range?: RangeInterface): void;
```
### `findBlocks`
查找对范围有效果的所有 Block
```ts
/**
* 查找对范围有效果的所有 Block
* @param range 范围
*/
findBlocks(range: RangeInterface): Array<NodeInterface>;
```
### `isFirstOffset`
判断范围的 {Edge}Offset 是否在 Block 的开始位置
```ts
/**
* 判断范围的 {Edge}Offset 是否在 Block 的开始位置
* @param range 光标
* @param edge start end
*/
isFirstOffset(range: RangeInterface, edge: 'start' | 'end'): boolean;
```
### `isLastOffset`
判断范围的 {Edge}Offset 是否在 Block 的最后位置
```ts
/**
* 判断范围的 {Edge}Offset 是否在 Block 的最后位置
* @param range 光标
* @param edge start end
*/
isLastOffset(range: RangeInterface, edge: 'start' | 'end'): boolean;
```
### `getBlocks`
获取范围内的所有 Block
```ts
/**
* 获取范围内的所有 Block
* @param range 光标s
*/
getBlocks(range: RangeInterface): Array<NodeInterface>;
```
### `getLeftText`
获取 Block 左侧文本
```ts
/**
* 获取 Block 左侧文本
* @param block 节点
*/
getLeftText(block: NodeInterface | Node): string;
```
### `removeLeftText`
删除 Block 左侧文本
```ts
/**
* 删除 Block 左侧文本
* @param block 节点
*/
removeLeftText(block: NodeInterface | Node): void;
```
### `getBlockByRange`
生成 cursor 左侧或右侧的节点,放在一个和父节点一样的容器里
```ts
/**
* 生成 cursor 左侧或右侧的节点,放在一个和父节点一样的容器里
* isLeft = true左侧
* isLeft = false右侧
* @param {block,range,isLeft,clone,keepID} 节点,光标,左侧或右侧, 是否复制是否保持id
*
*/
getBlockByRange({
block,
range,
isLeft,
clone,
keepID,
}: {
block: NodeInterface | Node;
range: RangeInterface;
isLeft: boolean;
clone?: boolean;
keepID?: boolean;
}): NodeInterface;
```
### `normal`
整理块级节点为符合标准的编辑器值
```ts
/**
* 整理块级节点
* @param node 节点
* @param root 根节点
*/
normal(node: NodeInterface, root: NodeInterface): void;
```
### `brToBlock`
br 换行改成段落
```ts
/**
* br 换行改成段落
* @param block 节点
*/
brToBlock(block: NodeInterface): void;
```
### `insertEmptyBlock`
插入一个空的 block 节点
```ts
/**
* 插入一个空的block节点
* @param range 光标所在位置
* @param block 节点
* @returns
*/
insertEmptyBlock(range: RangeInterface, block: NodeInterface): void;
```
### `insertOrSplit`
在光标位置插入或分割节点
```ts
/**
* 在光标位置插入或分割节点
* @param range 光标所在位置
* @param block 节点
*/
insertOrSplit(range: RangeInterface, block: NodeInterface): void;
```

View File

@ -0,0 +1,35 @@
# Maximize
Adjust the card to maximize/minimize
Type: `MaximizeInterface`
## Constructor
```ts
new (editor: EditorInterface, card: CardInterface): MaximizeInterface
```
## Method
### `restore`
restore
```ts
/**
* Restore
*/
restore(): void;
```
### `maximize`
maximize
```ts
/**
* Maximize
*/
maximize(): void;
```

View File

@ -0,0 +1,35 @@
# 最大化
调整卡片最大化/最小化
类型:`MaximizeInterface`
## 构造函数
```ts
new (editor: EditorInterface, card: CardInterface): MaximizeInterface
```
## 方法
### `restore`
恢复
```ts
/**
* 恢复
*/
restore(): void;
```
### `maximize`
最大化
```ts
/**
* 最大化
*/
maximize(): void;
```

View File

@ -0,0 +1,106 @@
# Resize
A tool that can adjust the size of the card content area
Type: `ResizeInterface`
## Constructor
```ts
new (editor: EditorInterface, card: CardInterface): ResizeInterface
```
## Method
### `create`
Create and bind events
```ts
/**
* Create and bind events
* @param options optional
*/
create(options: ResizeCreateOptions): void;
```
### `render`
Rendering tools
```ts
/**
* Render
* The target node rendered by @param container, the default is the root node of the current card
* @param minHeight minimum height, default 80px
*/
render(container?: NodeInterface, minHeight?: number): void;
```
### `dragStart`
Pull start
```ts
/**
* Pull to start
* @param event event
*/
dragStart(event: MouseEvent): void;
```
### `dragMove`
Pulling moving
```ts
/**
* Pulling and moving
* @param event event
*/
dragMove(event: MouseEvent): void;
```
### `dragEnd`
Pull over
```ts
/**
* Pull end
*/
dragEnd(event: MouseEvent): void;
```
### `show`
Show off
```ts
/**
* Show
*/
show(): void;
```
### `hide`
hide
```ts
/**
* Hide
*/
hide(): void;
```
### `destroy`
Logout
```ts
/**
* Logout
*/
destroy(): void;
```

View File

@ -0,0 +1,106 @@
# 调整大小
可以调整卡片内容区域大小的工具
类型:`ResizeInterface`
## 构造函数
```ts
new (editor: EditorInterface, card: CardInterface): ResizeInterface
```
## 方法
### `create`
创建并绑定事件
```ts
/**
* 创建并绑定事件
* @param options 可选项
*/
create(options: ResizeCreateOptions): void;
```
### `render`
渲染工具
```ts
/**
* 渲染
* @param container 渲染到的目标节点,默认为当前卡片根节点
* @param minHeight 最小高度默认80px
*/
render(container?: NodeInterface, minHeight?: number): void;
```
### `dragStart`
拉动开始
```ts
/**
* 拉动开始
* @param event 事件
*/
dragStart(event: MouseEvent): void;
```
### `dragMove`
拉动移动中
```ts
/**
* 拉动移动中
* @param event 事件
*/
dragMove(event: MouseEvent): void;
```
### `dragEnd`
拉动结束
```ts
/**
* 拉动结束
*/
dragEnd(event: MouseEvent): void;
```
### `show`
展示
```ts
/**
* 展示
*/
show(): void;
```
### `hide`
隐藏
```ts
/**
* 隐藏
*/
hide(): void;
```
### `destroy`
注销
```ts
/**
* 注销
*/
destroy(): void;
```

View File

@ -0,0 +1,81 @@
# Toolbar
Card toolbar
Type: `CardToolbarInterface`
## Constructor
```ts
new (editor: EditorInterface, card: CardInterface): CardToolbarInterface
```
## Method
### `create`
Create card toolbar
```ts
/**
* Toolbar for creating cards
*/
create(): void;
```
### `hide`
Hide toolbar, including dnd
```ts
/**
* Hide toolbar, including dnd
*/
hide(): void;
```
### `show`
Show toolbar, including dnd
```ts
/**
* Display toolbar, including dnd
* @param event mouse event, used for positioning
*/
show(event?: MouseEvent): void;
```
### `hideCardToolbar`
Only hide the toolbar of the card, not including dnd
```ts
/**
* Only hide the toolbar of the card, not including dnd
*/
hideCardToolbar(): void;
```
### `showCardToolbar`
Only show the toolbar of the card, not including dnd
```ts
/**
* Only display the toolbar of the card, not including dnd
* @param event mouse event, used for positioning
*/
showCardToolbar(event?: MouseEvent): void;
```
### `getContainer`
Get toolbar container
```ts
/**
* Get the toolbar container
*/
getContainer(): NodeInterface | undefined;
```

View File

@ -0,0 +1,81 @@
# 工具栏
卡片工具栏
类型:`CardToolbarInterface`
## 构造函数
```ts
new (editor: EditorInterface, card: CardInterface): CardToolbarInterface
```
## 方法
### `create`
创建卡片的 toolbar
```ts
/**
* 创建卡片的toolbar
*/
create(): void;
```
### `hide`
隐藏 toolbar包含 dnd
```ts
/**
* 隐藏toolbar包含dnd
*/
hide(): void;
```
### `show`
展示 toolbar包含 dnd
```ts
/**
* 展示toolbar包含dnd
* @param event 鼠标事件,用于定位
*/
show(event?: MouseEvent): void;
```
### `hideCardToolbar`
只隐藏卡片的 toolbar不包含 dnd
```ts
/**
* 只隐藏卡片的toolbar不包含dnd
*/
hideCardToolbar(): void;
```
### `showCardToolbar`
只显示卡片的 toolbar不包含 dnd
```ts
/**
* 只显示卡片的toolbar不包含dnd
* @param event 鼠标事件,用于定位
*/
showCardToolbar(event?: MouseEvent): void;
```
### `getContainer`
获取工具栏容器
```ts
/**
* 获取工具栏容器
*/
getContainer(): NodeInterface | undefined;
```

335
docs/api/editor-card.md Normal file
View File

@ -0,0 +1,335 @@
# Card
Edit card related operations
Type: `CardModelInterface`
## Use
```ts
new Engine(...).card
```
## Constructor
```ts
new (editor: EditorInterface): CardModelInterface
```
## Attributes
### `classes`
Instantiated card collection object
### `active`
Currently activated card
### `length`
The length of the instantiated card collection object
## Method
### `init`
Instantiate
```ts
/**
* Instantiate cards
* @param cards card collection
*/
init(cards: Array<CardEntry>): void;
```
### `add`
Add card
```ts
/**
* Add cards
* @param name name
* @param clazz class
*/
add(clazz: CardEntry): void;
```
### `each`
Traverse all created cards
```ts
/**
* Traverse all created cards
* @param callback callback function
*/
each(callback: (card: CardInterface) => boolean | void): void;
```
### `closest`
Query the card node closest to the parent node
```ts
/**
* Query the card node closest to the parent node
* @param selector querier
* @param ignoreEditable Whether to ignore editable nodes
*/
closest(
selector: Node | NodeInterface,
ignoreEditable?: boolean,
): NodeInterface | undefined;
```
### `find`
Find Card according to the selector
```ts
/**
* Find Card according to the selector
* @param selector card ID, or child node
* @param ignoreEditable Whether to ignore editable nodes
*/
find(
selector: NodeInterface | Node | string,
ignoreEditable?: boolean,
): CardInterface | undefined;
```
### `findBlock`
Find Block Type Card according to the selector
```ts
/**
* Find the Block type Card according to the selector
* @param selector card ID, or child node
*/
findBlock(selector: Node | NodeInterface): CardInterface | undefined;
```
### `getSingleCard`
Get a single card in the cursor selection
```ts
/**
* Get a single card
* @param range cursor range
*/
getSingleCard(range: RangeInterface): CardInterface | undefined;
```
### `getSingleSelectedCard`
Get the card when a node is selected in the selection
```ts
/**
* Get the card when a node is selected in the selection
* @param rang selection
*/
getSingleSelectedCard(rang: RangeInterface): CardInterface | undefined;
```
### `insertNode`
Insert card
```ts
/**
* Insert card
* @param range selection
* @param card card
*/
insertNode(range: RangeInterface, card: CardInterface): CardInterface;
```
### `removeNode`
Remove card node
```ts
/**
* Remove card node
* @param card card
*/
removeNode(card: CardInterface): void;
```
### `replaceNode`
Replace the specified node with the Card DOM node waiting to be created
```ts
/**
* Replace the specified node with the Card DOM node waiting to be created
* @param node node
* @param name card name
* @param value card value
*/
replaceNode(
node: NodeInterface,
name: string,
value?: CardValue,
): NodeInterface;
```
### `updateNode`
Update the card to re-render
```ts
/**
* Update the card to re-render
* @param card card
* @param value
*/
updateNode(card: CardInterface, value: CardValue): void;
```
### `activate`
Activate the card where the card node is located
```ts
/**
* Activate the card where the card node is located
* @param node node
* @param trigger activation method
* @param event event
*/
activate(
node: NodeInterface,
trigger?: CardActiveTrigger,
event?: MouseEvent,
): void;
```
### `select`
Selected card
```ts
/**
* Select the card
* @param card card
*/
select(card: CardInterface): void;
```
### `focus`
Focus card
```ts
/**
* Focus card
* @param card card
* @param toStart Whether to focus to the start position
*/
focus(card: CardInterface, toStart?: boolean): void;
```
### `insert`
Insert card
```ts
/**
* Insert card
* @param name card name
* @param value card value
*/
insert(name: string, value?: CardValue): CardInterface;
```
### `update`
Update card
```ts
/**
* Update card
* @param selector card selector
* @param value The card value to be updated
*/
update(selector: NodeInterface | Node | string, value: CardValue): void;
```
### `replace`
Replace the location of a card with another specified card to be rendered
### `replace`
Replace the location of a card with another specified card to be rendered
```ts
/**
* Replace card
* @param source The card to be replaced
* @param name new card name
* @param value New card value
*/
replace(source: CardInterface, name: string, value?: CardValue)
```
### `remove`
Remove card
```ts
/**
* Remove card
* @param selector card selector
*/
remove(selector: NodeInterface | Node | string): void;
```
### `create`
Create a card
```ts
/**
* Create a card
* @param name plugin name
* @param options option
*/
create(
name: string,
options?: {
value?: CardValue;
root?: NodeInterface;
},
): CardInterface;
```
### `render`
Render the card
```ts
/**
* Render the card
* @param container needs to re-render the node containing the card, if not passed, then render all the card nodes to be created
*/
render(container?: NodeInterface): void;
```
### `gc`
Release card
```ts
/**
* Release card
*/
gc(): void;
```

View File

@ -0,0 +1,331 @@
# Card
编辑卡片的相关操作
类型:`CardModelInterface`
## 使用
```ts
new Engine(...).card
```
## 构造函数
```ts
new (editor: EditorInterface): CardModelInterface
```
## 属性
### `classes`
已实例化的卡片集合对象
### `active`
当前已激活的卡片
### `length`
已实例化的卡片集合对象长度
## 方法
### `init`
实例化
```ts
/**
* 实例化卡片
* @param cards 卡片集合
*/
init(cards: Array<CardEntry>): void;
```
### `add`
增加卡片
```ts
/**
* 增加卡片
* @param name 名称
* @param clazz 类
*/
add(clazz: CardEntry): void;
```
### `each`
遍历所有已创建的卡片
```ts
/**
* 遍历所有已创建的卡片
* @param callback 回调函数
*/
each(callback: (card: CardInterface) => boolean | void): void;
```
### `closest`
查询父节点距离最近的卡片节点
```ts
/**
* 查询父节点距离最近的卡片节点
* @param selector 查询器
* @param ignoreEditable 是否忽略可编辑节点
*/
closest(
selector: Node | NodeInterface,
ignoreEditable?: boolean,
): NodeInterface | undefined;
```
### `find`
根据选择器查找 Card
```ts
/**
* 根据选择器查找Card
* @param selector 卡片ID或者子节点
* @param ignoreEditable 是否忽略可编辑节点
*/
find(
selector: NodeInterface | Node | string,
ignoreEditable?: boolean,
): CardInterface | undefined;
```
### `findBlock`
根据选择器查找 Block 类型 Card
```ts
/**
* 根据选择器查找Block 类型 Card
* @param selector 卡片ID或者子节点
*/
findBlock(selector: Node | NodeInterface): CardInterface | undefined;
```
### `getSingleCard`
获取光标选区中的单个卡片
```ts
/**
* 获取单个卡片
* @param range 光标范围
*/
getSingleCard(range: RangeInterface): CardInterface | undefined;
```
### `getSingleSelectedCard`
获取选区选中一个节点时候的卡片
```ts
/**
* 获取选区选中一个节点时候的卡片
* @param rang 选区
*/
getSingleSelectedCard(rang: RangeInterface): CardInterface | undefined;
```
### `insertNode`
插入卡片
```ts
/**
* 插入卡片
* @param range 选区
* @param card 卡片
*/
insertNode(range: RangeInterface, card: CardInterface): CardInterface;
```
### `removeNode`
移除卡片节点
```ts
/**
* 移除卡片节点
* @param card 卡片
*/
removeNode(card: CardInterface): void;
```
### `replaceNode`
将指定节点替换成等待创建的 Card DOM 节点
```ts
/**
* 将指定节点替换成等待创建的Card DOM 节点
* @param node 节点
* @param name 卡片名称
* @param value 卡片值
*/
replaceNode(
node: NodeInterface,
name: string,
value?: CardValue,
): NodeInterface;
```
### `updateNode`
更新卡片重新渲染
```ts
/**
* 更新卡片重新渲染
* @param card 卡片
* @param value 值
*/
updateNode(card: CardInterface, value: CardValue): void;
```
### `activate`
激活卡片节点所在的卡片
```ts
/**
* 激活卡片节点所在的卡片
* @param node 节点
* @param trigger 激活方式
* @param event 事件
*/
activate(
node: NodeInterface,
trigger?: CardActiveTrigger,
event?: MouseEvent,
): void;
```
### `select`
选中卡片
```ts
/**
* 选中卡片
* @param card 卡片
*/
select(card: CardInterface): void;
```
### `focus`
聚焦卡片
```ts
/**
* 聚焦卡片
* @param card 卡片
* @param toStart 是否聚焦到开始位置
*/
focus(card: CardInterface, toStart?: boolean): void;
```
### `insert`
插入卡片
```ts
/**
* 插入卡片
* @param name 卡片名称
* @param value 卡片值
*/
insert(name: string, value?: CardValue): CardInterface;
```
### `update`
更新卡片
```ts
/**
* 更新卡片
* @param selector 卡片选择器
* @param value 要更新的卡片值
*/
update(selector: NodeInterface | Node | string, value: CardValue): void;
```
### `replace`
把一个卡片所在位置替换成另一个指定的待渲染卡片
```ts
/**
* 替换卡片
* @param source 需要替换的卡片
* @param name 新的卡片名称
* @param value 新的卡片值
*/
replace(source: CardInterface, name: string, value?: CardValue)
```
### `remove`
移除卡片
```ts
/**
* 移除卡片
* @param selector 卡片选择器
*/
remove(selector: NodeInterface | Node | string): void;
```
### `create`
创建卡片
```ts
/**
* 创建卡片
* @param name 插件名称
* @param options 选项
*/
create(
name: string,
options?: {
value?: CardValue;
root?: NodeInterface;
},
): CardInterface;
```
### `render`
渲染卡片
```ts
/**
* 渲染卡片
* @param container 需要重新渲染包含卡片的节点,如果不传,则渲染全部待创建的卡片节点
*/
render(container?: NodeInterface): void;
```
### `gc`
释放卡片
```ts
/**
* 释放卡片
*/
gc(): void;
```

View File

@ -0,0 +1,112 @@
# Change events
Related events in editor changes
Type: `ChangeEventInterface`
## Constructor
```ts
new (engine: EngineInterface, options: ChangeEventOptions = {}): ChangeEventInterface;
```
## Attributes
### `isComposing`
Whether to combine input
### `isSelecting`
Is it being selected
## Method
### `isCardInput`
Is it entered in the card
```ts
isCardInput(e: Event): boolean;
```
### `onInput`
Input event
```ts
onInput(callback: (event?: Event) => void): void;
```
### `onSelect`
Cursor selection event
```ts
onSelect(callback: (event?: Event) => void): void;
```
### `onPaste`
Paste event
```ts
onPaste(
callback: (data: ClipboardData & {isPasteText: boolean }) => void,
): void;
```
### `onDrop`
Drag event
```ts
onDrop(
callback: (params: {
event: DragEvent;
range?: RangeInterface;
card?: CardInterface;
files: Array<File | null>;
}) => void,
): void;
```
### `onDocument`
Bind the document event
```ts
onDocument(
eventType: string,
listener: EventListener,
rewrite?: boolean,
): void;
```
### `onWindow`
Bind window events
```ts
onWindow(
eventType: string,
listener: EventListener,
rewrite?: boolean,
): void;
```
### `onContainer`
Binding editor root node event
```ts
onContainer(eventType: string, listener: EventListener): void;
```
### `destroy`
destroy
```ts
destroy(): void;
```

View File

@ -0,0 +1,112 @@
# 变更中的事件
编辑器变更中的相关事件
类型:`ChangeEventInterface`
## 构造函数
```ts
new (engine: EngineInterface, options: ChangeEventOptions = {}): ChangeEventInterface;
```
## 属性
### `isComposing`
是否组合输入中
### `isSelecting`
是否正在选择中
## 方法
### `isCardInput`
是否是在卡片输入
```ts
isCardInput(e: Event): boolean;
```
### `onInput`
输入事件
```ts
onInput(callback: (event?: Event) => void): void;
```
### `onSelect`
光标选择事件
```ts
onSelect(callback: (event?: Event) => void): void;
```
### `onPaste`
粘贴事件
```ts
onPaste(
callback: (data: ClipboardData & { isPasteText: boolean }) => void,
): void;
```
### `onDrop`
拖动事件
```ts
onDrop(
callback: (params: {
event: DragEvent;
range?: RangeInterface;
card?: CardInterface;
files: Array<File | null>;
}) => void,
): void;
```
### `onDocument`
绑定 document 事件
```ts
onDocument(
eventType: string,
listener: EventListener,
rewrite?: boolean,
): void;
```
### `onWindow`
绑定 window 事件
```ts
onWindow(
eventType: string,
listener: EventListener,
rewrite?: boolean,
): void;
```
### `onContainer`
绑定编辑器根节点事件
```ts
onContainer(eventType: string, listener: EventListener): void;
```
### `destroy`
销毁
```ts
destroy(): void;
```

295
docs/api/editor-change.md Normal file
View File

@ -0,0 +1,295 @@
# Change
Operations related to editor changes
Type: `ChangeInterface`
## Use
```ts
new Engine(...).change
```
## Constructor
```ts
new (container: NodeInterface, options: ChangeOptions): ChangeInterface;
```
## Attributes
### `rangePathBeforeCommand`
Path after cursor conversion before command execution
```ts
rangePathBeforeCommand: Path[] | null;
```
### `event`
event
```ts
event: ChangeEventInterface;
```
### `marks`
All style nodes in the current cursor selection
```ts
marks: Array<NodeInterface>;
```
### `blocks`
All block-level nodes in the current cursor selection
```ts
blocks: Array<NodeInterface>;
```
### `inlines`
All inline nodes in the current cursor selection
```ts
inlines: Array<NodeInterface>;
```
## Method
### `getRange`
Get the range of the current selection
```ts
/**
* Get the range of the current selection
*/
getRange(): RangeInterface;
```
### `getSafeRange`
Obtain a safe and controllable cursor object
```ts
/**
* Obtain a safe and controllable cursor object
* @param range default current cursor
*/
getSafeRange(range?: RangeInterface): RangeInterface;
```
### `select`
Select the specified range
```ts
/**
* Select the specified range
* @param range cursor
*/
select(range: RangeInterface): ChangeInterface;
```
### `focus`
Focus editor
```ts
/**
* Focus editor
* @param toStart true: start position, false: end position, the default is the previous operation position
*/
focus(toStart?: boolean): ChangeInterface;
```
### `blur`
Cancel focus
```ts
/**
* Cancel focus
*/
blur(): ChangeInterface;
```
### `apply`
Apply an operation that changes the dom structure
```ts
/**
* Apply an operation that changes the dom structure
* @param range cursor
*/
apply(range?: RangeInterface): void;
```
### `combinText`
Combine the interrupted characters in the current editing into an uninterrupted character
```ts
combinText(): void;
```
### `isComposing`
Is it in the combined input
```ts
isComposing(): boolean;
```
### `isSelecting`
Is it being selected
```ts
isSelecting(): boolean;
```
### `setValue`
Set editor value
```ts
/**
* @param value
* @param onParse uses root node parsing and filtering before converting to standard editor values
* @param options Card asynchronous rendering callback
* */
setValue(value: string, onParse?: (node: Node) => void, callback?: (count: number) => void): void;
```
### `setHtml`
Set html as editor value
```ts
/**
* Set html, it will be formatted as a legal editor value
* @param html html
* @param options Card asynchronous rendering callback
*/
setHtml(html: string, , callback?: (count: number) => void): void
```
### `getOriginValue`
Get the original value of the editor
```ts
getOriginValue(): string;
```
### `getValue`
Get editor value
```ts
/**
* @param ignoreCursor Whether to fool the record node where the cursor is located
* */
getValue(options: {ignoreCursor?: boolean }): string;
```
### `cacheRangeBeforeCommand`
Cache the cursor object before executing the command
```ts
cacheRangeBeforeCommand(): void;
```
### `getRangePathBeforeCommand`
Get the path after the cursor conversion before the command is executed
```ts
getRangePathBeforeCommand(): Path[] | null;
```
### `isEmpty`
Whether the current editor is empty
```ts
isEmpty(): boolean;
```
### `destroy`
destroy
```ts
destroy(): void;
```
### `insert`
Insert
```ts
/**
* Insert fragment
* @param fragment fragment
* @param callback callback function after insertion
*/
insert(fragment: DocumentFragment, callback?: () => void): void;
```
### `delete`
Delete content
```ts
/**
* Delete content
* @param range cursor, get the current cursor by default
* @param isDeepMerge Perform merge operation after deletion
*/
delete(range?: RangeInterface, isDeepMerge?: boolean): void;
```
### `unwrap`
Remove the block node closest to the current cursor or the outer package of the incoming node
```ts
/**
* Remove the block node closest to the current cursor or the outer package of the incoming node
* @param node node
*/
unwrap(node?: NodeInterface): void;
```
### `mergeAfterDelete`
Delete the block node closest to the current cursor or the previous node of the incoming node and merge it
```ts
/**
* Delete the block node closest to the current cursor or the previous node of the incoming node and merge it
* @param node node
*/
mergeAfterDelete(node?: NodeInterface): void;
```
### `focusPrevBlock`
The focus moves to the block node closest to the current cursor or the block before the incoming node
```ts
/**
* The focus moves to the block node closest to the current cursor or the block before the incoming node
* @param block node
* @param isRemoveEmptyBlock If the previous block is empty, whether to delete, the default is no
*/
focusPrevBlock(block?: NodeInterface, isRemoveEmptyBlock?: boolean): void;
```

View File

@ -0,0 +1,295 @@
# Change
编辑器变更的相关操作
类型:`ChangeInterface`
## 使用
```ts
new Engine(...).change
```
## 构造函数
```ts
new (container: NodeInterface, options: ChangeOptions): ChangeInterface;
```
## 属性
### `rangePathBeforeCommand`
命令执行前的光标转换后的路径
```ts
rangePathBeforeCommand: Path[] | null;
```
### `event`
事件
```ts
event: ChangeEventInterface;
```
### `marks`
当前光标选区中的所有样式节点
```ts
marks: Array<NodeInterface>;
```
### `blocks`
当前光标选区中的所有块级节点
```ts
blocks: Array<NodeInterface>;
```
### `inlines`
当前光标选区中的所有行内节点
```ts
inlines: Array<NodeInterface>;
```
## 方法
### `getRange`
获取当前选区的范围
```ts
/**
* 获取当前选区的范围
*/
getRange(): RangeInterface;
```
### `getSafeRange`
获取安全可控的光标对象
```ts
/**
* 获取安全可控的光标对象
* @param range 默认当前光标
*/
getSafeRange(range?: RangeInterface): RangeInterface;
```
### `select`
选中指定的范围
```ts
/**
* 选中指定的范围
* @param range 光标
*/
select(range: RangeInterface): ChangeInterface;
```
### `focus`
聚焦编辑器
```ts
/**
* 聚焦编辑器
* @param toStart true:开始位置,false:结束位置,默认为之前操作位置
*/
focus(toStart?: boolean): ChangeInterface;
```
### `blur`
取消焦点
```ts
/**
* 取消焦点
*/
blur(): ChangeInterface;
```
### `apply`
应用一个具有改变 dom 结构的操作
```ts
/**
* 应用一个具有改变dom结构的操作
* @param range 光标
*/
apply(range?: RangeInterface): void;
```
### `combinText`
把当前编辑中间断的字符组合成一段不间断的字符
```ts
combinText(): void;
```
### `isComposing`
是否在组合输入中
```ts
isComposing(): boolean;
```
### `isSelecting`
是否正在选择中
```ts
isSelecting(): boolean;
```
### `setValue`
设置编辑器值
```ts
/**
* @param value 值
* @param onParse 在转换为符合标准的编辑器值前使用根节点解析过滤
* @param options 异步渲染卡片回调
* */
setValue(value: string, onParse?: (node: Node) => void, callback?: (count: number) => void): void;
```
### `setHtml`
设置 html 作为编辑器值
```ts
/**
* 设置html会格式化为合法的编辑器值
* @param html html
* @param options 异步渲染卡片回调
*/
setHtml(html: string, callback?: (count: number) => void): void
```
### `getOriginValue`
获取编辑器原始值
```ts
getOriginValue(): string;
```
### `getValue`
获取编辑器值
```ts
/**
* @param ignoreCursor 是否忽悠光标所在的记录节点
* */
getValue(options: { ignoreCursor?: boolean }): string;
```
### `cacheRangeBeforeCommand`
在执行命令前缓存光标对象
```ts
cacheRangeBeforeCommand(): void;
```
### `getRangePathBeforeCommand`
获取命令执行前的光标转换后的路径
```ts
getRangePathBeforeCommand(): Path[] | null;
```
### `isEmpty`
当前编辑器是否是空值
```ts
isEmpty(): boolean;
```
### `destroy`
销毁
```ts
destroy(): void;
```
### `insert`
插入片段
```ts
/**
* 插入片段
* @param fragment 片段
* @param callback 插入后的回调函数
*/
insert(fragment: DocumentFragment, callback?: () => void): void;
```
### `delete`
删除内容
```ts
/**
* 删除内容
* @param range 光标,默认获取当前光标
* @param isDeepMerge 删除后执行合并操作
*/
delete(range?: RangeInterface, isDeepMerge?: boolean): void;
```
### `unwrap`
去除当前光标最接近的 block 节点或传入的节点外层包裹
```ts
/**
* 去除当前光标最接近的block节点或传入的节点外层包裹
* @param node 节点
*/
unwrap(node?: NodeInterface): void;
```
### `mergeAfterDelete`
删除当前光标最接近的 block 节点或传入的节点的前面一个节点后合并
```ts
/**
* 删除当前光标最接近的block节点或传入的节点的前面一个节点后合并
* @param node 节点
*/
mergeAfterDelete(node?: NodeInterface): void;
```
### `focusPrevBlock`
焦点移动到当前光标最接近的 block 节点或传入的节点前一个 Block
```ts
/**
* 焦点移动到当前光标最接近的block节点或传入的节点前一个 Block
* @param block 节点
* @param isRemoveEmptyBlock 如果前一个block为空是否删除默认为否
*/
focusPrevBlock(block?: NodeInterface, isRemoveEmptyBlock?: boolean): void;
```

138
docs/api/editor-inline.md Normal file
View File

@ -0,0 +1,138 @@
# InlineModel
Edit related operations of in-line nodes
Type: `InlineModelInterface`
## Use
```ts
new Engine(...).inline
```
## Constructor
```ts
new (editor: EditorInterface): InlineModelInterface
```
## Method
### `init`
initialization
```ts
/**
* Initialization
*/
init(): void;
```
### `closest`
Get the nearest Inline node, the return node cannot be found
```ts
/**
* Get the nearest Inline node, the return node cannot be found
*/
closest(node: NodeInterface): NodeInterface;
```
### `closestNotInline`
Get the first non-inline node up
```ts
/**
* Get the first non-inline node up
*/
closestNotInline(node: NodeInterface): NodeInterface;
```
### `wrap`
Add an inline package to the current cursor node
```ts
/**
* Add an inline package to the current cursor node
* @param inline inline tag
* @param range cursor, get the current cursor by default
*/
wrap(inline: NodeInterface | Node | string, range?: RangeInterface): void;
```
### `unwrap`
Remove inline package
```ts
/**
* Remove inline package
* @param range cursor, the current editor cursor is the default, or the inline node that needs to be removed
*/
unwrap(range?: RangeInterface | NodeInterface): void;
```
### `insert`
Insert inline tag
```ts
/**
* Insert inline tag
* @param inline inline tag
* @param range cursor
*/
insert(inline: NodeInterface | Node | string, range?: RangeInterface): void;
```
### `split`
Split inline tags
```ts
/**
* Split inline tags
* @param range cursor, get the current cursor by default
*/
split(range?: RangeInterface): void;
```
### `findInlines`
Get all inline tags within the cursor
```ts
/**
* Get all inline tags within the cursor
* @param range cursor
*/
findInlines(range: RangeInterface): Array<NodeInterface>;
```
### `repairCursor`
Fix inline node cursor placeholder
```ts
/**
* Fix cursor placeholder for inline node
* @param node inlne node
*/
repairCursor(node: NodeInterface | Node): void;
```
### `repairRange`
Fix the cursor selection position, &#8203;<a>&#8203;<anchor />acde<focus />&#8203;</a>&#8203; -><anchor />&#8203;<a> &#8203;acde&#8203;</a>&#8203;<focus />
```ts
/**
* Fix the cursor selection position, &#8203;<a>&#8203;<anchor />acde<focus />&#8203;</a>&#8203; -><anchor />&#8203;<a >&#8203;acde&#8203;</a>&#8203;<focus />
* Otherwise, in ot, the &#8203; changes on both sides of the inline node may not be applied correctly
*/
repairRange(range?: RangeInterface): RangeInterface;
```

View File

@ -0,0 +1,138 @@
# InlineModel
编辑行内节点的相关操作
类型:`InlineModelInterface`
## 使用
```ts
new Engine(...).inline
```
## 构造函数
```ts
new (editor: EditorInterface): InlineModelInterface
```
## 方法
### `init`
初始化
```ts
/**
* 初始化
*/
init(): void;
```
### `closest`
获取最近的 Inline 节点,找不到返回 node
```ts
/**
* 获取最近的 Inline 节点,找不到返回 node
*/
closest(node: NodeInterface): NodeInterface;
```
### `closestNotInline`
获取向上第一个非 Inline 节点
```ts
/**
* 获取向上第一个非 Inline 节点
*/
closestNotInline(node: NodeInterface): NodeInterface;
```
### `wrap`
给当前光标节点添加 inline 包裹
```ts
/**
* 给当前光标节点添加inline包裹
* @param inline inline标签
* @param range 光标,默认获取当前光标
*/
wrap(inline: NodeInterface | Node | string, range?: RangeInterface): void;
```
### `unwrap`
移除 inline 包裹
```ts
/**
* 移除inline包裹
* @param range 光标,默认当前编辑器光标,或者需要移除的inline节点
*/
unwrap(range?: RangeInterface | NodeInterface): void;
```
### `insert`
插入 inline 标签
```ts
/**
* 插入inline标签
* @param inline inline标签
* @param range 光标
*/
insert(inline: NodeInterface | Node | string, range?: RangeInterface): void;
```
### `split`
分割 inline 标签
```ts
/**
* 分割inline标签
* @param range 光标,默认获取当前光标
*/
split(range?: RangeInterface): void;
```
### `findInlines`
获取光标范围内的所有 inline 标签
```ts
/**
* 获取光标范围内的所有 inline 标签
* @param range 光标
*/
findInlines(range: RangeInterface): Array<NodeInterface>;
```
### `repairCursor`
修复 inline 节点光标占位符
```ts
/**
* 修复inline节点光标占位符
* @param node inlne 节点
*/
repairCursor(node: NodeInterface | Node): void;
```
### `repairRange`
修复光标选区位置,&#8203;<a>&#8203;<anchor />acde<focus />&#8203;</a>&#8203; -><anchor />&#8203;<a>&#8203;acde&#8203;</a>&#8203;<focus />
```ts
/**
* 修复光标选区位置,&#8203;<a>&#8203;<anchor />acde<focus />&#8203;</a>&#8203; -><anchor />&#8203;<a>&#8203;acde&#8203;</a>&#8203;<focus />
* 否则在ot中可能无法正确的应用inline节点两边&#8203;的更改
*/
repairRange(range?: RangeInterface): RangeInterface;
```

331
docs/api/editor-list.md Normal file
View File

@ -0,0 +1,331 @@
# ListModel
Related operations for editing list nodes
Type: `ListModelInterface`
## Use
```ts
new Engine(...).list
```
## Constructor
```ts
new (editor: EditorInterface): ListModelInterface
```
## Attributes
### `CUSTOMZIE_UL_CLASS`
Read only
Custom list style markup
### `CUSTOMZIE_LI_CLASS`
Read only
Custom list item style mark
### `INDENT_KEY`
Read only
List indentation key tag, used to get the list indentation value
## Method
### `init`
initialization
```ts
/**
* Initialization
*/
init(): void;
```
### `isEmptyItem`
Determine whether the list item node is empty
```ts
/**
* Determine whether the list item node is empty
* @param node node
*/
isEmptyItem(node: NodeInterface): boolean;
```
### `isSame`
Determine whether two nodes are the same List node
```ts
/**
* Determine whether two nodes are the same List node
* @param sourceNode source node
* @param targetNode target node
*/
isSame(sourceNode: NodeInterface, targetNode: NodeInterface): boolean;
```
### `isSpecifiedType`
Determine whether the node set is a List list of the specified type
```ts
/**
* Determine whether the node collection is a List list of the specified type
* @param blocks node collection
* @param name node label type
* @param card is the card name of the specified custom list item
*/
isSpecifiedType(
blocks: Array<NodeInterface>,
name?:'ul' |'ol',
card?: string,
): boolean;
```
### `getPlugins`
Get all List plugins
```ts
/**
* Get all List plugins
*/
getPlugins(): Array<ListInterface>;
```
### `getPluginNameByNode`
Get the name of the list plugin according to the list node
```ts
/**
* Get the name of the list plug-in according to the list node
* @param block node
*/
getPluginNameByNode(block: NodeInterface): string;
```
### `getPluginNameByNodes`
Get the name of the list plugin that a list node collection belongs
```ts
/**
* Get the name of the list plugin to which a list node collection belongs
* @param blocks node collection
*/
getPluginNameByNodes(blocks: Array<NodeInterface>): string;
```
### `unwrapCustomize`
Clear the related attributes of the custom list node
```ts
/**
* Clear the related attributes of the custom list node
* @param node node
*/
unwrapCustomize(node: NodeInterface): NodeInterface;
```
### `unwrap`
Cancel the list of nodes
```ts
/**
* Cancel the list of nodes
* @param blocks node collection
*/
unwrap(blocks: Array<NodeInterface>): void;
```
### `normalize`
Get the node collection after the repair list of the current selection
```ts
/**
* Get the node collection after the repair list of the current selection
*/
normalize(): Array<NodeInterface>;
```
### `split`
Split the list of selected items into a single list
```ts
/**
* Split the list of selected items into a single list
*/
split(): void;
```
### `merge`
Merge list
```ts
/**
* Consolidated list
* @param blocks node collection, the default is the blocks of the current selection
*/
merge(blocks?: Array<NodeInterface>, range?: RangeInterface): void;
```
### `addStart`
Add the start number to the list
```ts
/**
* Add the start number to the list
* @param block list node
*/
addStart(block?: NodeInterface): void;
```
### `addIndent`
Add indentation to list nodes
```ts
/**
* Add indentation to list nodes
* @param block list node
* @param value indentation value
*/
addIndent(block: NodeInterface, value: number, maxValue?: number): void;
```
### `getIndent`
Get the indent value of the list node
```ts
/**
* Get the indent value of the list node
* @param block list node
* @returns
*/
getIndent(block: NodeInterface): number;
```
### `addCardToCustomize`
Add card nodes to custom list items
```ts
/**
* Add card nodes for custom list items
* @param node list node item
* @param cardName card name, must support inline card type
* @param value card value
*/
addCardToCustomize(
node: NodeInterface | Node,
cardName: string,
value?: any,
): CardInterface | undefined;
```
### `addReadyCardToCustomize`
Add a card node to be rendered for the custom list item
```ts
/**
* Add a card node to be rendered for the custom list item
* @param node list node item
* @param cardName card name, must support inline card type
* @param value card value
*/
addReadyCardToCustomize(
node: NodeInterface | Node,
cardName: string,
value?: any,
): NodeInterface | undefined;
```
### `addBr`
Add the BR tag to the list
```ts
/**
* Add the BR tag to the list
* @param node list node item
*/
addBr(node: NodeInterface): void;
```
### `toCustomize`
Convert node to custom node
```ts
/**
* Convert nodes to custom nodes
* @param blocks node
* @param cardName card name
* @param value card value
*/
toCustomize(
blocks: Array<NodeInterface> | NodeInterface,
cardName: string,
value?: any,
): Array<NodeInterface> | NodeInterface;
```
### `toNormal`
Convert node to list node
```ts
/**
* Convert a node to a list node
* @param blocks node
* @param tagName list node name, ul or ol, the default is ul
* @param start the start number of the ordered list
*/
toNormal(
blocks: Array<NodeInterface> | NodeInterface,
tagName?:'ul' |'ol',
start?: number,
): Array<NodeInterface> | NodeInterface;
```
### `isFirst`
Determine whether the selected area is at the beginning of the list
```ts
/**
* Determine whether the selected area is at the beginning of the list
* Selected area
*/
isFirst(range: RangeInterface): boolean;
```
### `isLast`
Determine whether the selected area is at the end of the list
```ts
/**
* Determine whether the selected area is at the end of the list
*/
isLast(range: RangeInterface): boolean;
```

View File

@ -0,0 +1,331 @@
# ListModel
编辑列表节点的相关操作
类型:`ListModelInterface`
## 使用
```ts
new Engine(...).list
```
## 构造函数
```ts
new (editor: EditorInterface): ListModelInterface
```
## 属性
### `CUSTOMZIE_UL_CLASS`
只读
自定义列表样式标记
### `CUSTOMZIE_LI_CLASS`
只读
自定义列表项样式标记
### `INDENT_KEY`
只读
列表缩进 key 标记,用于获取列表缩进值
## 方法
### `init`
初始化
```ts
/**
* 初始化
*/
init(): void;
```
### `isEmptyItem`
判断列表项节点是否为空
```ts
/**
* 判断列表项节点是否为空
* @param node 节点
*/
isEmptyItem(node: NodeInterface): boolean;
```
### `isSame`
判断两个节点是否是一样的 List 节点
```ts
/**
* 判断两个节点是否是一样的List节点
* @param sourceNode 源节点
* @param targetNode 目标节点
*/
isSame(sourceNode: NodeInterface, targetNode: NodeInterface): boolean;
```
### `isSpecifiedType`
判断节点集合是否是指定类型的 List 列表
```ts
/**
* 判断节点集合是否是指定类型的List列表
* @param blocks 节点集合
* @param name 节点标签类型
* @param card 是否是指定的自定义列表项的卡片名称
*/
isSpecifiedType(
blocks: Array<NodeInterface>,
name?: 'ul' | 'ol',
card?: string,
): boolean;
```
### `getPlugins`
获取所有 List 插件
```ts
/**
* 获取所有List插件
*/
getPlugins(): Array<ListInterface>;
```
### `getPluginNameByNode`
根据列表节点获取列表插件名称
```ts
/**
* 根据列表节点获取列表插件名称
* @param block 节点
*/
getPluginNameByNode(block: NodeInterface): string;
```
### `getPluginNameByNodes`
获取一个列表节点集合所属列表插件名称
```ts
/**
* 获取一个列表节点集合所属列表插件名称
* @param blocks 节点集合
*/
getPluginNameByNodes(blocks: Array<NodeInterface>): string;
```
### `unwrapCustomize`
清除自定义列表节点相关属性
```ts
/**
* 清除自定义列表节点相关属性
* @param node 节点
*/
unwrapCustomize(node: NodeInterface): NodeInterface;
```
### `unwrap`
取消节点的列表
```ts
/**
* 取消节点的列表
* @param blocks 节点集合
*/
unwrap(blocks: Array<NodeInterface>): void;
```
### `normalize`
获取当前选区的修复列表后的节点集合
```ts
/**
* 获取当前选区的修复列表后的节点集合
*/
normalize(): Array<NodeInterface>;
```
### `split`
将选中列表项列表分割出来单独作为一个列表
```ts
/**
* 将选中列表项列表分割出来单独作为一个列表
*/
split(): void;
```
### `merge`
合并列表
```ts
/**
* 合并列表
* @param blocks 节点集合默认为当前选区的blocks
*/
merge(blocks?: Array<NodeInterface>, range?: RangeInterface): void;
```
### `addStart`
给列表添加 start 序号
```ts
/**
* 给列表添加start序号
* @param block 列表节点
*/
addStart(block?: NodeInterface): void;
```
### `addIndent`
给列表节点增加缩进
```ts
/**
* 给列表节点增加缩进
* @param block 列表节点
* @param value 缩进值
*/
addIndent(block: NodeInterface, value: number, maxValue?: number): void;
```
### `getIndent`
获取列表节点 indent 值
```ts
/**
* 获取列表节点 indent 值
* @param block 列表节点
* @returns
*/
getIndent(block: NodeInterface): number;
```
### `addCardToCustomize`
为自定义列表项添加卡片节点
```ts
/**
* 为自定义列表项添加卡片节点
* @param node 列表节点项
* @param cardName 卡片名称必须是支持inline卡片类型
* @param value 卡片值
*/
addCardToCustomize(
node: NodeInterface | Node,
cardName: string,
value?: any,
): CardInterface | undefined;
```
### `addReadyCardToCustomize`
为自定义列表项添加待渲染卡片节点
```ts
/**
* 为自定义列表项添加待渲染卡片节点
* @param node 列表节点项
* @param cardName 卡片名称必须是支持inline卡片类型
* @param value 卡片值
*/
addReadyCardToCustomize(
node: NodeInterface | Node,
cardName: string,
value?: any,
): NodeInterface | undefined;
```
### `addBr`
给列表添加 BR 标签
```ts
/**
* 给列表添加BR标签
* @param node 列表节点项
*/
addBr(node: NodeInterface): void;
```
### `toCustomize`
将节点转换为自定义节点
```ts
/**
* 将节点转换为自定义节点
* @param blocks 节点
* @param cardName 卡片名称
* @param value 卡片值
*/
toCustomize(
blocks: Array<NodeInterface> | NodeInterface,
cardName: string,
value?: any,
): Array<NodeInterface> | NodeInterface;
```
### `toNormal`
将节点转换为列表节点
```ts
/**
* 将节点转换为列表节点
* @param blocks 节点
* @param tagName 列表节点名称ul 或者 ol默认为ul
* @param start 有序列表开始序号
*/
toNormal(
blocks: Array<NodeInterface> | NodeInterface,
tagName?: 'ul' | 'ol',
start?: number,
): Array<NodeInterface> | NodeInterface;
```
### `isFirst`
判断选中的区域是否在列表的开始
```ts
/**
* 判断选中的区域是否在列表的开始
* 选中的区域
*/
isFirst(range: RangeInterface): boolean;
```
### `isLast`
判断选中的区域是否在列表的末尾
```ts
/**
* 判断选中的区域是否在列表的末尾
*/
isLast(range: RangeInterface): boolean;
```

179
docs/api/editor-mark.md Normal file
View File

@ -0,0 +1,179 @@
# MarkModel
Related operations for editing style nodes
Type: `MarkModelInterface`
## Use
```ts
new Engine(...).mark
```
## Constructor
```ts
new (editor: EditorInterface): MarkModelInterface
```
## Method
### `init`
initialization
```ts
/**
* Initialization
*/
init(): void;
```
### `findPlugin`
Find the mark plugin instance according to the node
```ts
/**
* Find the mark plug-in instance according to the node
* @param node node
*/
findPlugin(node: NodeInterface): MarkInterface | undefined;
```
### `closestNotMark`
Get the first non-mark node up
```ts
/**
* Get the first non-Mark node up
*/
closestNotMark(node: NodeInterface): NodeInterface;
```
### `compare`
Compare whether two nodes are the same, including attributes, style, class
```ts
/**
* Compare whether two nodes are the same, including attributes, style, and class
* @param source source node
* @param target target node
* @param isCompareValue Whether to compare the value of each attribute
*/
compare(
source: NodeInterface,
target: NodeInterface,
isCompareValue?: boolean,
): boolean;
```
### `contain`
Determine whether the source node contains all the attributes and styles of the target node
```ts
/**
* Determine whether the source node contains all the attributes and styles of the target node
* @param source source node
* @param target target node
*/
contain(source: NodeInterface, target: NodeInterface): boolean;
```
### `split`
Split mark tags
```ts
/**
* Split mark tags
* @param range cursor, get the current cursor by default
* @param removeMark The empty mark tag that needs to be removed
*/
split(
range?: RangeInterface,
removeMark?: NodeInterface | Node | string | Array<NodeInterface>,
): void;
```
### `wrap`
Wrap the mark label in the current cursor selection
```ts
/**
* Wrap the mark label in the current cursor selection area
* @param mark mark tag
* @param both mark nodes on both sides of the label
*/
wrap(mark: NodeInterface | Node | string, range?: RangeInterface): void;
```
### `unwrap`
Remove the mark package
```ts
/**
* Remove the mark package
* @param range cursor
* @param removeMark the mark tag to be removed
*/
unwrap(
removeMark?: NodeInterface | Node | string | Array<NodeInterface>,
range?: RangeInterface,
): void;
```
### `merge`
Merge the mark node of the selection
```ts
/**
* Merge the mark node of the selection
* @param range cursor, the current selection cursor by default
*/
merge(range?: RangeInterface): void;
```
### `insert`
Insert the mark tag at the cursor
```ts
/**
* Insert a mark tag at the cursor
* @param mark mark tag
* @param range specifies the cursor, the default is the cursor selected by the editor
*/
insert(mark: NodeInterface | Node | string, range?: RangeInterface): void;
```
### `findMarks`
Find all Marks that have an effect on the range
```ts
/**
* Find all Marks that have an effect on the range
* @param range
*/
findMarks(range: RangeInterface): Array<NodeInterface>;
```
### `removeEmptyMarks`
Traverse from the bottom up to delete empty Marks, when encountering empty Blocks, add BR tags
```ts
/**
* Traverse from bottom to top to delete empty Marks, when encountering empty Blocks, add BR tags
* @param node node
* @param addBr whether to add br
*/
removeEmptyMarks(node: NodeInterface, addBr?: boolean): void;
```

View File

@ -0,0 +1,179 @@
# MarkModel
编辑样式节点的相关操作
类型:`MarkModelInterface`
## 使用
```ts
new Engine(...).mark
```
## 构造函数
```ts
new (editor: EditorInterface): MarkModelInterface
```
## 方法
### `init`
初始化
```ts
/**
* 初始化
*/
init(): void;
```
### `findPlugin`
根据节点查找 mark 插件实例
```ts
/**
* 根据节点查找mark插件实例
* @param node 节点
*/
findPlugin(node: NodeInterface): MarkInterface | undefined;
```
### `closestNotMark`
获取向上第一个非 Mark 节点
```ts
/**
* 获取向上第一个非 Mark 节点
*/
closestNotMark(node: NodeInterface): NodeInterface;
```
### `compare`
比较两个节点是否相同,包括 attributes、style、class
```ts
/**
* 比较两个节点是否相同包括attributes、style、class
* @param source 源节点
* @param target 目标节点
* @param isCompareValue 是否比较每项属性的值
*/
compare(
source: NodeInterface,
target: NodeInterface,
isCompareValue?: boolean,
): boolean;
```
### `contain`
判断源节点是否包含目标节点的所有属性和样式
```ts
/**
* 判断源节点是否包含目标节点的所有属性和样式
* @param source 源节点
* @param target 目标节点
*/
contain(source: NodeInterface, target: NodeInterface): boolean;
```
### `split`
分割 mark 标签
```ts
/**
* 分割mark标签
* @param range 光标,默认获取当前光标
* @param removeMark 需要移除的空mark标签
*/
split(
range?: RangeInterface,
removeMark?: NodeInterface | Node | string | Array<NodeInterface>,
): void;
```
### `wrap`
在当前光标选区包裹 mark 标签
```ts
/**
* 在当前光标选区包裹mark标签
* @param mark mark标签
* @param both mark标签两侧节点
*/
wrap(mark: NodeInterface | Node | string, range?: RangeInterface): void;
```
### `unwrap`
去掉 mark 包裹
```ts
/**
* 去掉mark包裹
* @param range 光标
* @param removeMark 要移除的mark标签
*/
unwrap(
removeMark?: NodeInterface | Node | string | Array<NodeInterface>,
range?: RangeInterface,
): void;
```
### `merge`
合并选区的 mark 节点
```ts
/**
* 合并选区的mark节点
* @param range 光标,默认当前选区光标
*/
merge(range?: RangeInterface): void;
```
### `insert`
光标处插入 mark 标签
```ts
/**
* 光标处插入mark标签
* @param mark mark标签
* @param range 指定光标,默认为编辑器选中的光标
*/
insert(mark: NodeInterface | Node | string, range?: RangeInterface): void;
```
### `findMarks`
查找对范围有效果的所有 Mark
```ts
/**
* 查找对范围有效果的所有 Mark
* @param range 范围
*/
findMarks(range: RangeInterface): Array<NodeInterface>;
```
### `removeEmptyMarks`
从下开始往上遍历删除空 Mark当遇到空 Block添加 BR 标签
```ts
/**
* 从下开始往上遍历删除空 Mark当遇到空 Block添加 BR 标签
* @param node 节点
* @param addBr 是否添加br
*/
removeEmptyMarks(node: NodeInterface, addBr?: boolean): void;
```

362
docs/api/editor-node.md Normal file
View File

@ -0,0 +1,362 @@
# NodeModel
Edit node related operations
Type: `NodeModelInterface`
## Use
```ts
new Engine(...).node
```
## Constructor
```ts
new (editor: EditorInterface)
```
## Method
### `isVoid`
Whether it is an empty node
```ts
/**
* Is it an empty node
* @param node node or node name
* @param schema takes the schema from this.editor by default
*/
isVoid(
node: NodeInterface | Node | string,
schema?: SchemaInterface,
): boolean;
```
### `isMark`
Whether it is a mark style label
```ts
/**
* Is it a mark tag
* @param node node
*/
isMark(node: NodeInterface | Node, schema?: SchemaInterface): boolean;
```
### `isInline`
Is it an inline tag
```ts
/**
* Is it an inline tag
* @param node node
*/
isInline(node: NodeInterface | Node, schema?: SchemaInterface): boolean;
```
### `isBlock`
Is it a block node
```ts
/**
* Is it a block node
* @param node node
*/
isBlock(node: NodeInterface | Node, schema?: SchemaInterface): boolean;
```
### `isSimpleBlock`
Determine whether the node is a simple node of block type (child nodes do not contain blcok tags)
```ts
/**
* Determine whether the node is a simple node of block type (child nodes do not contain blcok tags)
*/
isSimpleBlock(node: NodeInterface): boolean;
```
### `isRootBlock`
Determine whether the node is the top-level root node, the parent is the editor root node, and the child node does not have a block node
```ts
/**
* Determine whether the node is the top-level root node, the parent is the editor root node, and the child node has no block node
* @param node node
* @returns
*/
isRootBlock(node: NodeInterface, schema?: SchemaInterface): boolean;
```
### `isEmpty`
Determine whether the text under the node is empty
```ts
/**
* Determine whether the text under the node is empty
* @param node node
* @param withTrim is trim
*/
isEmpty(node: NodeInterface, withTrim?: boolean): boolean;
```
### `isEmptyWithTrim`
Determine whether the text under a node is empty or only white space characters
```ts
/**
* Determine whether the text under a node is empty, or there are only blank characters
* @param node node
*/
isEmptyWithTrim(node: NodeInterface): boolean;
```
### `isLikeEmpty`
Judge whether a node is empty, a card is not counted as an empty node
```ts
/**
* Determine whether a node is empty
* @param node node
*/
isLikeEmpty(node: NodeInterface): boolean;
```
### `isList`
Determine whether the node is a list node
```ts
/**
* Determine whether the node is a list node
* @param node node or node name
*/
isList(node: NodeInterface | string | Node): boolean;
```
### `isCustomize`
Determine whether the node is a custom list
```ts
/**
* Determine whether the node is a custom list
* @param node node
*/
isCustomize(node: NodeInterface): boolean;
```
### `unwrap`
Remove the outer wrapper of the node
```ts
/**
* Remove package
* @param node The node that needs to remove the package
*/
unwrap(node: NodeInterface): void;
```
### `wrap`
Wrap a layer of nodes outside the node
```ts
/**
* Package node
* @param source The node that needs to be wrapped
* @param outer packaged external node
* @param mergeSame merges the node styles and attributes of the same name on the same node
*/
wrap(
source: NodeInterface | Node,
outer: NodeInterface,
mergeSame?: boolean,
): NodeInterface;
```
### `merge`
Merge node
```ts
/**
* Merge nodes
* @param source merged node
* @param target The node that needs to be merged
* @param remove Whether to remove after merging
*/
merge(source: NodeInterface, target: NodeInterface, remove?: boolean): void;
```
### `replace`
Append the child nodes of the source node to the target node and replace the source node
```ts
/**
* Append the child nodes of the source node to the target node and replace the source node
* @param source old node
* @param target new node
*/
replace(source: NodeInterface, target: NodeInterface): NodeInterface;
```
### `insert`
Insert a node at the cursor position
```ts
/**
* Insert a node at the cursor position
* @param node node
* @param range cursor
*/
insert(
node: Node | NodeInterface,
range?: RangeInterface,
): RangeInterface | undefined;
```
### `insertText`
Insert text at cursor position
```ts
/**
* Insert text at the cursor position
* @param text text
* @param range cursor
*/
insertText(
text: string,
range?: RangeInterface,
): RangeInterface | undefined;
```
### `setAttributes`
Set node properties
```ts
/**
* Set node attributes
* @param node node
* @param props property
*/
setAttributes(node: NodeInterface, attributes: any): NodeInterface;
```
### `removeMinusStyle`
Remove styles with negative values
```ts
/**
* Remove styles with negative values
* @param node node
* @param style style name
*/
removeMinusStyle(node: NodeInterface, style: string): void;
```
### `mergeAdjacent`
The child nodes under the merged node, the child nodes of two identical adjacent nodes, usually blockquote, ul, ol tags
```ts
/**
* The child nodes under the merged node, the child nodes of two identical adjacent nodes, usually blockquote, ul, ol tags
* @param node current node
*/
mergeAdjacent(node: NodeInterface): void;
```
### `removeSide`
Remove the labels on both sides of the node
```ts
/**
* Delete the labels on both sides of the node
* @param node node
* @param tagName tag name, the default is br tag
*/
removeSide(node: NodeInterface, tagName?: string): void;
```
### `flatten`
Organize the nodes and restore the nodes to the state that meets the editor value
```ts
/**
* Organize nodes
* @param node node
* @param root root node, the default is node node
*/
flatten(node: NodeInterface, root?: NodeInterface): void;
```
### `normalize`
Standardized node
```ts
/**
* Standardized node
* @param node node
*/
normalize(node: NodeInterface): void;
```
### `html`
Get or set the html text of the element node
```ts
/**
* Get or set the html text of the element node
* @param {string|undefined} val html text
* @return {NodeEntry|string} current instance or html text
*/
html(node: NodeInterface): string;
html(node: NodeInterface, val: string): NodeInterface;
html(node: NodeInterface, val?: string): NodeInterface | string;
```
### `clone`
Copy element node
```ts
/**
* Copy element node
* @param {boolean} deep whether deep copy
* @return copied element node
*/
clone(node: NodeInterface, deep?: boolean): NodeInterface;
```
### `getBatchAppendHTML`
Get outerHTML after batch appending child nodes
```ts
/**
* Get outerHTML after batch appending child nodes
* @param nodes node collection
* @param appendExp appended node
*/
getBatchAppendHTML(nodes: Array<NodeInterface>, appendExp: string): string;
```

View File

@ -0,0 +1,362 @@
# NodeModel
编辑节点的相关操作
类型:`NodeModelInterface`
## 使用
```ts
new Engine(...).node
```
## 构造函数
```ts
new (editor: EditorInterface)
```
## 方法
### `isVoid`
是否是空节点
```ts
/**
* 是否是空节点
* @param node 节点或节点名称
* @param schema 默认从 this.editor 中取 schema
*/
isVoid(
node: NodeInterface | Node | string,
schema?: SchemaInterface,
): boolean;
```
### `isMark`
是否是 mark 样式标签
```ts
/**
* 是否是mark标签
* @param node 节点
*/
isMark(node: NodeInterface | Node, schema?: SchemaInterface): boolean;
```
### `isInline`
是否是 inline 标签
```ts
/**
* 是否是inline标签
* @param node 节点
*/
isInline(node: NodeInterface | Node, schema?: SchemaInterface): boolean;
```
### `isBlock`
是否是 block 节点
```ts
/**
* 是否是block节点
* @param node 节点
*/
isBlock(node: NodeInterface | Node, schema?: SchemaInterface): boolean;
```
### `isSimpleBlock`
判断节点是否为 block 类型的简单节点(子节点不包含 blcok 标签)
```ts
/**
* 判断节点是否为block类型的简单节点子节点不包含blcok标签
*/
isSimpleBlock(node: NodeInterface): boolean;
```
### `isRootBlock`
判断节点是否是顶级根节点,父级为编辑器根节点,且,子级节点没有 block 节点
```ts
/**
* 判断节点是否是顶级根节点父级为编辑器根节点子级节点没有block节点
* @param node 节点
* @returns
*/
isRootBlock(node: NodeInterface, schema?: SchemaInterface): boolean;
```
### `isEmpty`
判断节点下的文本是否为空
```ts
/**
* 判断节点下的文本是否为空
* @param node 节点
* @param withTrim 是否 trim
*/
isEmpty(node: NodeInterface, withTrim?: boolean): boolean;
```
### `isEmptyWithTrim`
判断一个节点下的文本是否为空,或者只有空白字符
```ts
/**
* 判断一个节点下的文本是否为空,或者只有空白字符
* @param node 节点
*/
isEmptyWithTrim(node: NodeInterface): boolean;
```
### `isLikeEmpty`
判断一个节点是否为空,有卡片不算作空节点
```ts
/**
* 判断一个节点是否为空
* @param node 节点
*/
isLikeEmpty(node: NodeInterface): boolean;
```
### `isList`
判断节点是否为列表节点
```ts
/**
* 判断节点是否为列表节点
* @param node 节点或者节点名称
*/
isList(node: NodeInterface | string | Node): boolean;
```
### `isCustomize`
判断节点是否是自定义列表
```ts
/**
* 判断节点是否是自定义列表
* @param node 节点
*/
isCustomize(node: NodeInterface): boolean;
```
### `unwrap`
去除节点的外层包裹
```ts
/**
* 去除包裹
* @param node 需要去除包裹的节点
*/
unwrap(node: NodeInterface): void;
```
### `wrap`
给节点外面包裹一层节点
```ts
/**
* 包裹节点
* @param source 需要包裹的节点
* @param outer 包裹的外部节点
* @param mergeSame 合并相同名称的节点样式和属性在同一个节点上
*/
wrap(
source: NodeInterface | Node,
outer: NodeInterface,
mergeSame?: boolean,
): NodeInterface;
```
### `merge`
合并节点
```ts
/**
* 合并节点
* @param source 合并的节点
* @param target 需要合并的节点
* @param remove 合并后是否移除
*/
merge(source: NodeInterface, target: NodeInterface, remove?: boolean): void;
```
### `replace`
将源节点的子节点追加到目标节点,并替换源节点
```ts
/**
* 将源节点的子节点追加到目标节点,并替换源节点
* @param source 旧节点
* @param target 新节点
*/
replace(source: NodeInterface, target: NodeInterface): NodeInterface;
```
### `insert`
在光标位置插入一个节点
```ts
/**
* 在光标位置插入一个节点
* @param node 节点
* @param range 光标
*/
insert(
node: Node | NodeInterface,
range?: RangeInterface,
): RangeInterface | undefined;
```
### `insertText`
光标位置插入文本
```ts
/**
* 光标位置插入文本
* @param text 文本
* @param range 光标
*/
insertText(
text: string,
range?: RangeInterface,
): RangeInterface | undefined;
```
### `setAttributes`
设置节点属性
```ts
/**
* 设置节点属性
* @param node 节点
* @param props 属性
*/
setAttributes(node: NodeInterface, attributes: any): NodeInterface;
```
### `removeMinusStyle`
移除值为负的样式
```ts
/**
* 移除值为负的样式
* @param node 节点
* @param style 样式名称
*/
removeMinusStyle(node: NodeInterface, style: string): void;
```
### `mergeAdjacent`
合并节点下的子节点,两个相同的相邻节点的子节点,通常是 blockquote、ul、ol 标签
```ts
/**
* 合并节点下的子节点,两个相同的相邻节点的子节点,通常是 blockquote、ul、ol 标签
* @param node 当前节点
*/
mergeAdjacent(node: NodeInterface): void;
```
### `removeSide`
删除节点两边标签
```ts
/**
* 删除节点两边标签
* @param node 节点
* @param tagName 标签名称默认为br标签
*/
removeSide(node: NodeInterface, tagName?: string): void;
```
### `flatten`
整理节点,把节点修复到符合编辑器值的状态
```ts
/**
* 整理节点
* @param node 节点
* @param root 根节点默认为node节点
*/
flatten(node: NodeInterface, root?: NodeInterface): void;
```
### `normalize`
标准化节点
```ts
/**
* 标准化节点
* @param node 节点
*/
normalize(node: NodeInterface): void;
```
### `html`
获取或设置元素节点 html 文本
```ts
/**
* 获取或设置元素节点html文本
* @param {string|undefined} val html文本
* @return {NodeEntry|string} 当前实例或html文本
*/
html(node: NodeInterface): string;
html(node: NodeInterface, val: string): NodeInterface;
html(node: NodeInterface, val?: string): NodeInterface | string;
```
### `clone`
复制元素节点
```ts
/**
* 复制元素节点
* @param {boolean} deep 是否深度复制
* @return 复制后的元素节点
*/
clone(node: NodeInterface, deep?: boolean): NodeInterface;
```
### `getBatchAppendHTML`
获取批量追加子节点后的 outerHTML
```ts
/**
* 获取批量追加子节点后的outerHTML
* @param nodes 节点集合
* @param appendExp 追加的节点
*/
getBatchAppendHTML(nodes: Array<NodeInterface>, appendExp: string): string;
```

201
docs/api/editor.md Normal file
View File

@ -0,0 +1,201 @@
# Engine and reader share attributes and methods
Type: `EditorInterface`
Editing engine and reader share attributes and methods
## Attributes
### `kind`
Editor type, editing engine or reader
```ts
readonly kind:'engine' |'view';
```
### `language`
Language
Type: `LanguageInterface`
### `container`
Editor node
Type: `NodeInterface`
### `root`
Editor root node, the default is the parent node of the editor node
Type: `NodeInterface`
### `command`
Editor commands
Type: `CommandInterface`
### `card`
Card management, you can create cards, delete, modify, update and other related operations
Type: `CardModelInterface`
### `plugin`
Can manage all instantiated plugin instances
Type: `PluginModelInterface`
### `node`
Node management, including node type judgment, inserting nodes in the DOM tree
Type: `NodeModelInterface`
### `list`
List node management
Type: `ListModelInterface`
### `mark`
Style node management
Type: `MarkModelInterface`
### `inline`
In-line node management
Type: `InlineModelInterface`
### `block`
Block-level node management
Type: `BlockModelInterface`
### `event`
Incident management
Type: `EventInterface`
### `schema`
Element structure management
Type: `SchemaInterface`
### `conversion`
Element name conversion rules
Type: `ConversionInterface`
### `clipboard`
Clipboard management
Type: `ClipboardInterface`
## Method
### `on`
Event binding
```ts
/**
* Bind event
* @param eventType event type
* @param listener event callback
* @param rewrite whether to rewrite
*/
on(eventType: string, listener: EventListener, rewrite?: boolean): void;
```
### `off`
Remove event binding
```ts
/**
* Remove bound event
* @param eventType event type
* @param listener event callback
*/
off(eventType: string, listener: EventListener): void;
```
### `trigger`
trigger event
```ts
/**
* trigger event
* @param eventType event name
* @param args trigger parameters
*/
trigger(eventType: string, ...args: any): any;
```
### `messageSuccess`
Show success messages, and print messages on the console by default. You can modify the `messageSuccess` method and use the UI to display `engine.messageSuccess = text => Message.show(text)`
This method may be called in the plug-in or the engine to pop up a message
```ts
/**
* Show success information
* @param message
*/
messageSuccess(message: string): void;
```
### `messageError`
Show error message
```ts
/**
* Display error message
* @param error error message
*/
messageError(error: string): void;
```
### `messageConfirm`
A confirmation prompt box pops up, no UI is displayed in the engine by default, and false is always returned. So you need to re-assign a meaningful confirmation prompt box function
For example, using the Modal.confirm component of antd
```ts
engine.messageConfirm = (msg: string) => {
return new Promise<boolean>((resolve, reject) => {
Modal.confirm({
content: msg,
onOk: () => resolve(true),
onCancel: () => reject(),
});
});
};
```
Method signature
```ts
/**
* Message confirmation
* @param message
*/
messageConfirm(message: string): Promise<boolean>;
```

201
docs/api/editor.zh-CN.md Normal file
View File

@ -0,0 +1,201 @@
# 引擎和阅读器共有属性和方法
类型:`EditorInterface`
编辑引擎和阅读器共有属性和方法
## 属性
### `kind`
编辑器类型,编辑引擎或者阅读器
```ts
readonly kind: 'engine' | 'view';
```
### `language`
语言
类型:`LanguageInterface`
### `container`
编辑器节点
类型:`NodeInterface`
### `root`
编辑器根节点,默认为编辑器节点的父节点
类型:`NodeInterface`
### `command`
编辑器命令
类型:`CommandInterface`
### `card`
卡片管理,可以创建卡片、删除、修改、更新等相关操作
类型:`CardModelInterface`
### `plugin`
可以管理所有已实例化的插件实例
类型:`PluginModelInterface`
### `node`
节点管理,包括节点类型判断,在 DOM 树中插入节点
类型:`NodeModelInterface`
### `list`
列表节点管理
类型:`ListModelInterface`
### `mark`
样式节点管理
类型:`MarkModelInterface`
### `inline`
行内节点管理
类型:`InlineModelInterface`
### `block`
块级节点管理
类型:`BlockModelInterface`
### `event`
事件管理
类型:`EventInterface`
### `schema`
元素结构管理
类型:`SchemaInterface`
### `conversion`
元素名称转换规则
类型:`ConversionInterface`
### `clipboard`
剪贴板管理
类型:`ClipboardInterface`
## 方法
### `on`
事件绑定
```ts
/**
* 绑定事件
* @param eventType 事件类型
* @param listener 事件回调
* @param rewrite 是否重写
*/
on(eventType: string, listener: EventListener, rewrite?: boolean): void;
```
### `off`
移除事件绑定
```ts
/**
* 移除绑定事件
* @param eventType 事件类型
* @param listener 事件回调
*/
off(eventType: string, listener: EventListener): void;
```
### `trigger`
触发事件
```ts
/**
* 触发事件
* @param eventType 事件名称
* @param args 触发参数
*/
trigger(eventType: string, ...args: any): any;
```
### `messageSuccess`
显示成功类的消息,默认在控制台打印消息。可以修改`messageSuccess`方法,使用 UI 显示 `engine.messageSuccess = text => Message.show(text)`
在插件内部或引擎内部都可能会调用此方法弹出讯息
```ts
/**
* 显示成功的信息
* @param message 信息
*/
messageSuccess(message: string): void;
```
### `messageError`
显示错误消息
```ts
/**
* 显示错误信息
* @param error 错误信息
*/
messageError(error: string): void;
```
### `messageConfirm`
弹出一个确认提示框,引擎内默认没有 UI 显示,并且始终返回 false。所以需要重新赋值一个有意义的确认提示框功能
例如,使用 antd 的 Modal.confirm 组件
```ts
engine.messageConfirm = (msg: string) => {
return new Promise<boolean>((resolve, reject) => {
Modal.confirm({
content: msg,
onOk: () => resolve(true),
onCancel: () => reject(),
});
});
};
```
方法签名
```ts
/**
* 消息确认
* @param message 消息
*/
messageConfirm(message: string): Promise<boolean>;
```

184
docs/api/engine.md Normal file
View File

@ -0,0 +1,184 @@
# Engine
Type: `EngineInterface`
## Attributes
### `options`
Options
Type: `EngineOptions`
### `readonly`
Read-only
Type: `boolean`
### `change`
Edit state
Type: `ChangeInterface`
### `typing`
Key processing
Type: `TypingInterface`
### `ot`
Co-editing related
Type: `OTInterface`
### `history`
history record
Type: `HistoryInterface`
### `request`
Network request
Type: `RequestInterface`
## Method
### `focus`
Focus on the editor
```ts
/**
* Focus on the editor
* @param start is the start position of the focus, the default is true, false is the focus to the end position
*/
focus(start?: boolean): void;
```
### `isFocus`
Whether the current cursor is focused on the editor
```ts
/**
* Whether the current cursor has been focused on the editor
*/
isFocus(): boolean;
```
### `isEmpty`
Whether the current editor is empty
```ts
/**
* Whether the current editor is empty
*/
isEmpty(): boolean;
```
### `getValue`
Get editor value
```ts
/**
* Get editor value
* @param ignoreCursor whether to include cursor position information
*/
getValue(ignoreCursor?: boolean): string;
```
### `getValueAsync`
Get the editor value asynchronously, and will wait for the plug-in processing to complete before getting the value
```ts
/**
* Obtain the editor value asynchronously, and wait for the plug-in processing to complete before obtaining the value
* For example, plug-in upload is waiting, and the value will be obtained after the upload is completed.
* @param ignoreCursor whether to include cursor position information
*/
getValueAsync(ignoreCursor?: boolean): Promise<string>;
```
### `getHtml`
Get the html of the editor
```ts
/**
* Get the html of the editor
*/
getHtml(): string;
```
### `getJsonValue`
Get the value in JSON format
```ts
/**
* Get the value in JSON format
*/
getJsonValue(): string | undefined | (string | {})[];
```
### `setValue`
Set editor value
```ts
/**
* Set editor value
* @param value
* @param options Card asynchronous rendering callback
*/
setValue(value: string, callback?: (count: number) => void): EngineInterface;
```
### `setHtml`
Set html as editor value
```ts
/**
* Set html, it will be formatted as a legal editor value
* @param html html
* @param options Card asynchronous rendering callback
*/
setHtml(html: string, callback?: (count: number) => void): EngineInterface
```
### `setJsonValue`
Set the json format value, which is mainly used to synchronize with the value of the collaborative server
```ts
/**
* Set the json format value, mainly used for collaboration
* @param value
*/
setJsonValue(value: Array<any>): EngineInterface;
```
### `setScrollNode`
Set editor scroll bar node
```ts
setScrollNode(node?: HTMLElement)
```
### `destroy`
Destroy the editor
```ts
destroy():void
```

184
docs/api/engine.zh-CN.md Normal file
View File

@ -0,0 +1,184 @@
# 引擎
类型:`EngineInterface`
## 属性
### `options`
选项
类型:`EngineOptions`
### `readonly`
是否只读
类型:`boolean`
### `change`
编辑时状态
类型:`ChangeInterface`
### `typing`
按键处理
类型:`TypingInterface`
### `ot`
协同编辑相关
类型:`OTInterface`
### `history`
历史记录
类型:`HistoryInterface`
### `request`
网络请求
类型:`RequestInterface`
## 方法
### `focus`
聚焦到编辑器
```ts
/**
* 聚焦到编辑器
* @param start 是否聚焦的开始位置,默认为 truefalse 为聚焦到结束位置
*/
focus(start?: boolean): void;
```
### `isFocus`
当前光标是否已聚焦到编辑器
```ts
/**
* 当前光标是否已聚焦到编辑器
*/
isFocus(): boolean;
```
### `isEmpty`
当前编辑器是否为空值
```ts
/**
* 当前编辑器是否为空值
*/
isEmpty(): boolean;
```
### `getValue`
获取编辑器值
```ts
/**
* 获取编辑器值
* @param ignoreCursor 是否包含光标位置信息
*/
getValue(ignoreCursor?: boolean): string;
```
### `getValueAsync`
异步获取编辑器值,将等候插件处理完成后再获取值
```ts
/**
* 异步获取编辑器值,将等候插件处理完成后再获取值
* 比如插件上传等待中,将等待上传完成后再获取值
* @param ignoreCursor 是否包含光标位置信息
*/
getValueAsync(ignoreCursor?: boolean): Promise<string>;
```
### `getHtml`
获取编辑器的 html
```ts
/**
* 获取编辑器的html
*/
getHtml(): string;
```
### `getJsonValue`
获取 JSON 格式的值
```ts
/**
* 获取JSON格式的值
*/
getJsonValue(): string | undefined | (string | {})[];
```
### `setValue`
设置编辑器值
```ts
/**
* 设置编辑器值
* @param value 值
* @param options 异步渲染卡片回调
*/
setValue(value: string, callback?: (count: number) => void): EngineInterface;
```
### `setHtml`
设置 html 作为编辑器值
```ts
/**
* 设置html会格式化为合法的编辑器值
* @param html html
* @param options 异步渲染卡片回调
*/
setHtml(html: string, callback?: (count: number) => void): EngineInterface
```
### `setJsonValue`
设置 json 格式值,主要用于与协同服务端的值同步
```ts
/**
* 设置json格式值主要用于协同
* @param value 值
*/
setJsonValue(value: Array<any>): EngineInterface;
```
### `setScrollNode`
设置编辑器滚动条节点
```ts
setScrollNode(node?: HTMLElement)
```
### `destroy`
销毁编辑器
```ts
destroy():void
```

170
docs/api/history.md Normal file
View File

@ -0,0 +1,170 @@
# History
Editor's edit history
Type: `HistoryInterface`
## Constructor
```ts
new (engine: EngineInterface): HistoryInterface
```
## Method
### `reset`
Reset history, it will clear all history
```ts
reset(): void;
```
### `hasUndo`
Is there an undo operation
```ts
hasUndo(): boolean;
```
### `hasRedo`
Is there a redo operation
```ts
hasRedo(): boolean;
```
### `undo`
Perform undo operation
```ts
undo(): void;
```
### `redo`
Perform redo operation
```ts
redo(): void;
```
### `hold`
The action in the next milliseconds remains as a historical segment
```ts
/**
* How many milliseconds the action remains as a historical segment
* @param time milliseconds
*/
hold(time?: number): void;
```
### `releaseHold`
Reset hold
```ts
/**
* Reset hold
*/
releaseHold(): void;
```
### `onFilter`
Monitor and filter ops stored in history
```ts
/**
* Monitoring and filtering are stored in the history stack
* @param filter true to filter and exclude, false to record in the history stack
*/
onFilter(filter: (op: Op) => boolean): void
```
### `onSelf`
Monitor the current change ops and decide whether to write to the history record
```ts
/**
*
* @param collect method undefined The default delay save, true save immediately, false immediately discard. Promise<boolean> blocks all subsequent ops until it returns false or true
*/
onSelf(collect: (ops: Op[]) => Promise<boolean> | boolean | undefined): void
```
### `clear`
Delay to clear all history records
```ts
clear(): void;
```
### `saveOp`
Save the currently unmaintained operations to the stack
```ts
saveOp(): void;
```
### `handleSelfOps`
Collect local editing operations
```ts
/**
* @param ops operation set
* */
handleSelfOps(ops: Op[]): void;
```
### `collectRemoteOps`
Collect remote operations (operations from other coordinators)
```ts
/**
* @param ops operation set
* */
collectRemoteOps(ops: Op[]): void;
```
### `getUndoOp`
Get the undo operation of the current top position
```ts
getUndoOp(): Operation | undefined;
```
### `getRedoOp`
Get the redo operation of the current top position
```ts
getRedoOp(): Operation | undefined;
```
### `getCurrentRangePath`
Get the converted path of the current cursor
```ts
getCurrentRangePath(): Path[];
```
### `getRangePathBeforeCommand`
Get the converted path of the cursor recorded before executing the command
```ts
getRangePathBeforeCommand(): Path[];
```

174
docs/api/history.zh-CN.md Normal file
View File

@ -0,0 +1,174 @@
# 历史
编辑器的编辑历史记录
类型:`HistoryInterface`
## 构造函数
```ts
new (engine: EngineInterface): HistoryInterface
```
## 方法
### `reset`
重置历史记录,会清空所有的历史记录
```ts
reset(): void;
```
### `hasUndo`
是否有撤销操作
```ts
hasUndo(): boolean;
```
### `hasRedo`
是否有重做操作
```ts
hasRedo(): boolean;
```
### `undo`
执行撤销操作
```ts
undo(): void;
```
### `redo`
执行重做操作
```ts
redo(): void;
```
### `hold`
在接下来的多少毫秒内的动作保持为一个历史片段
```ts
/**
* 多少毫秒内的动作保持为一个历史片段
* @param time 毫秒
*/
hold(time?: number): void;
```
### `releaseHold`
重置 hold
```ts
/**
* 重置 hold
*/
releaseHold(): void;
```
### `lock`
在接下来的多少毫秒内的动作将不作为历史记录
### `onFilter`
监听过滤存入历史记录的 ops
```ts
/**
* 监听过滤存入历史记录堆栈中
* @param filter true 过滤排除false 记录到历史堆栈中
*/
onFilter(filter: (op: Op) => boolean): void
```
### `onSelf`
监听当前变更 ops并决定是否写入到历史记录
```ts
/**
*
* @param collect 方法 undefined 默认延时保存true 立即保存false 立即丢弃。Promise<boolean> 阻拦接下来的所有ops直到返回false或者true
*/
onSelf(collect: (ops: Op[]) => Promise<boolean> | boolean | undefined): void
```
### `clear`
延时清除全部的历史记录
```ts
clear(): void;
```
### `saveOp`
把当前还未保持的操作保存到堆栈里
```ts
saveOp(): void;
```
### `handleSelfOps`
收集本地编辑的操作
```ts
/**
* @param ops 操作集合
* */
handleSelfOps(ops: Op[]): void;
```
### `collectRemoteOps`
收集远程的操作(来自其它协同者的操作)
```ts
/**
* @param ops 操作集合
* */
collectRemoteOps(ops: Op[]): void;
```
### `getUndoOp`
获取当前最前位置的撤销操作
```ts
getUndoOp(): Operation | undefined;
```
### `getRedoOp`
获取当前最前位置的重做操作
```ts
getRedoOp(): Operation | undefined;
```
### `getCurrentRangePath`
获取当前光标转换后的路径
```ts
getCurrentRangePath(): Path[];
```
### `getRangePathBeforeCommand`
获取执行命令前记录的光标转换后的路径
```ts
getRangePathBeforeCommand(): Path[];
```

45
docs/api/hotkey.md Normal file
View File

@ -0,0 +1,45 @@
# Hotkey
Editor hotkeys/shortcut keys
Type: `HotkeyInterface`
## Constructor
```ts
new (engine: EngineInterface): HotkeyInterface
```
## Method
### `trigger`
Trigger a keyboard event
```ts
trigger(e: KeyboardEvent): void;
```
### `enable`
Enable shortcut keys
```ts
enable(): void;
```
### `disable`
Disable shortcut keys
```ts
disable(): void;
```
### `destroy`
destroy
```ts
destroy(): void;
```

45
docs/api/hotkey.zh-CN.md Normal file
View File

@ -0,0 +1,45 @@
# 热键
编辑器热键/快捷键
类型:`HotkeyInterface`
## 构造函数
```ts
new (engine: EngineInterface): HotkeyInterface
```
## 方法
### `trigger`
触发一个键盘事件
```ts
trigger(e: KeyboardEvent): void;
```
### `enable`
启用快捷键
```ts
enable(): void;
```
### `disable`
禁用快捷键
```ts
disable(): void;
```
### `destroy`
销毁
```ts
destroy(): void;
```

34
docs/api/language.md Normal file
View File

@ -0,0 +1,34 @@
# Language
Add multi-language configuration to the editor
Type: `LanguageInterface`
## Constructor
```ts
new (lange: string, data: {} = {}): LanguageInterface
```
## Method
### `add`
Increase language configuration
Method signature
```ts
add(data: {}): void;
```
### `get`
Get the value of the language configuration item
```ts
/**
* @param keys The keys of multiple configuration items
* */
get<T extends string | {}>(...keys: Array<string>): T;
```

View File

@ -0,0 +1,34 @@
# 语言
给编辑器增加多语言配置
类型:`LanguageInterface`
## 构造函数
```ts
new (lange: string, data: {} = {}): LanguageInterface
```
## 方法
### `add`
增加语言配置
方法签名
```ts
add(data: {}): void;
```
### `get`
获取语言配置项的值
```ts
/**
* @param keys 多个配置项的key
* */
get<T extends string | {}>(...keys: Array<string>): T;
```

767
docs/api/node.md Normal file
View File

@ -0,0 +1,767 @@
# NodeInterface
Expand on the `Node` node of the DOM
Type: `NodeInterface`
## Create `NodeInterface` object
Use the `$` node selector provided in the engine to instantiate the `NodeInterface` object
```ts
import { $ } from '@aomao/engine';
//Use CSS selector to find nodes
const content = $('.content');
//Create node
const div = $('<div></div>');
document.body.append(div[0]);
//Conversion
const p = $(document.querySelector('p'));
const target = $(event.target);
```
## Attributes
### `length`
Node node collection length
Type: `number`
### `events`
The collection of event objects of all Node nodes in the current object
Type: `EventInterface[]`
### `document`
The Document object where the current Node node is located. In the use of iframe, the document in different frames is not consistent, and there are other environments as well, so we need to follow this object.
Type: `Document | null`
### `window`
The Window object where the current Node node is located. In the use of iframes, the windows in different frames are not consistent, and the same is true in some other environments, so we need to follow this object.
Type: `Window | null`
### `context`
Context node
Type: `Context | undefined`
### `name`
Node name
Type: `string`
### `type`
Node type, consistent with `Node.nodeType` [API](https://developer.mozilla.org/zh-CN/docs/Web/API/Node/nodeType)
Type: `number | undefined`
### `display`
Node display status
Type: `string | undefined`
### `isFragment`
Whether the Node node collection in the current object is a frame fragment
Type: `boolean`
### `[n: number]`
Node node collection, which can be accessed by subscript index
Return type: Node
## Method
### `each`
Traverse all Node nodes in the current object
```ts
/**
* Traverse
* @param {Function} callback callback function
* @return {NodeInterface} returns the current instance
*/
each(
callback: (node: Node, index: number) => boolean | void,
): NodeInterface;
```
### `toArray`
Convert all Node nodes in the current object to an array
```ts
toArray(): Array<NodeInterface>;
```
### `isElement`
Whether the current node is Node.ELEMENT_NODE node type
```ts
isElement(): boolean;
```
### `isText`
Whether the current node is Node.TEXT_NODE node type
```ts
isText(): boolean;
```
### `isCard`
Whether the current node is a Card component
```ts
isCard(): boolean;
```
### `isBlockCard`
Whether the current node is a Card component of block type
```ts
isBlockCard(): boolean;
```
### `isInlineCard`
Whether the current node is a Card component of inline type
```ts
isInlineCard(): boolean;
```
### `isEditableCard`
Is it an editable card
```ts
isEditableCard(): boolean;
```
### `isRoot`
Whether it is the root node
```ts
isRoot(): boolean;
```
### `isEditable`
Whether it is an editable node
```ts
isEditable(): boolean;
```
### `inEditor`
Is it in the root node
```ts
inEditor(): boolean;
```
### `isCursor`
Whether it is a cursor marked node
```ts
isCursor(): boolean
```
### `get`
Get the current Node node
```ts
get<E extends Node>(): E | null;
```
### `eq`
Get the current index node
```ts
/**
* Get the current index node
* @param {number} index
* @return {NodeInterface|undefined} NodeInterface class, or undefined
*/
eq(index: number): NodeInterface | undefined;
```
### `index`
Get the index of the parent node where the current node is located, and only count the nodes whose node type is ELEMENT_NODE
```ts
/**
* Get the index of the parent node where the current node is located, and only calculate the node whose node type is ELEMENT_NODE
* @return {number} return index
*/
index(): number;
```
### `parent`
Get the parent node of the current node
```ts
/**
* Get the parent node of the current node
* @return {NodeInterface} parent node
*/
parent(): NodeInterface | undefined;
```
### `children`
Query all child nodes of the current node
```ts
/**
*
* @param {Node | string} selector finder
* @return {NodeInterface} Eligible child nodes
*/
children(selector?: string): NodeInterface;
```
### `first`
Get the first child node of the current node
```ts
/**
* Get the first child node of the current node
* @return {NodeInterface} NodeInterface child node
*/
first(): NodeInterface | null;
```
### `last`
Get the last child node of the current node
```ts
/**
* Get the last child node of the current node
* @return {NodeInterface} NodeInterface child node
*/
last(): NodeInterface | null;
```
### `prev`
Return the sibling nodes before the node (including text nodes and comment nodes)
```ts
/**
* Return the sibling nodes before the node (including text nodes and comment nodes)
* @return {NodeInterface} NodeInterface node
*/
prev(): NodeInterface | null;
```
### `next`
Return the sibling nodes after the node (including text nodes and comment nodes)
```ts
/**
* Return the sibling nodes after the node (including text nodes and comment nodes)
* @return {NodeInterface} NodeInterface node
*/
next(): NodeInterface | null;
```
### `prevElement`
Return the sibling nodes before the node (not including text nodes and comment nodes)
```ts
/**
* Return the sibling nodes before the node (not including text nodes and comment nodes)
* @return {NodeInterface} NodeInterface node
*/
prevElement(): NodeInterface | null;
```
### `nextElement`
Return the sibling nodes after the node (not including text nodes and comment nodes)
```ts
/**
* Return the sibling nodes after the node (not including text nodes and comment nodes)
* @return {NodeInterface} NodeInterface node
*/
nextElement(): NodeInterface | null;
```
### `getPath`
Returns the path of the root node where the node is located, the default root node is document.body
```ts
/**
* Return the path of the root node where the node is located, the default root node is document.body
* @param {Node} context root node, the default is document.body
* @return {number} path
*/
getPath(context?: Node | NodeInterface): Array<number>;
```
### `contains`
Determine whether the node contains the node to be queried
```ts
/**
* Determine whether the node contains the node to be queried
* @param {NodeInterface | Node} node The node to be queried
* @return {Boolean} Does it contain
*/
contains(node: NodeInterface | Node): boolean;
```
### `find`
Query the current node according to the querier
```ts
/**
* Query the current node according to the querier
* @param {String} selector finder
* @return {NodeInterface} returns a NodeInterface instance
*/
find(selector: string): NodeInterface;
```
### closest
Query the parent node closest to the current node that meets the criteria according to the querier
```ts
/**
* Query the parent node closest to the current node that meets the criteria according to the querier
* @param {string} selector querier
* @return {NodeInterface} returns a NodeInterface instance
*/
closest(
selector: string,
callback?: (node: Node) => Node | undefined,
): NodeInterface;
```
### `on`
Bind events to the current node
```ts
/**
* Bind events to the current node
* @param {String} eventType event type
* @param {Function} listener event function
* @return {NodeInterface} returns the current instance
*/
on(eventType: string, listener: EventListener): NodeInterface;
```
### `off`
Remove current node event
```ts
/**
* Remove the current node event
* @param {String} eventType event type
* @param {Function} listener event function
* @return {NodeInterface} returns the current instance
*/
off(eventType: string, listener: EventListener): NodeInterface;
```
### `getBoundingClientRect`
Get the position of the current node relative to the viewport
```ts
/**
* Get the position of the current node relative to the viewport
* @param {Object} defaultValue default value
* @return {Object}
* {
* top,
* bottom,
* left,
* right
*}
*/
getBoundingClientRect(defaultValue?: {
top: number;
bottom: number;
left: number;
right: number;
}):
| {top: number; bottom: number; left: number; right: number}
| undefined;
```
### `removeAllEvents`
Remove all bound events of the current node
```ts
/**
* Remove all bound events of the current node
* @return {NodeInterface} current NodeInterface instance
*/
removeAllEvents(): NodeInterface;
```
### `offset`
Get the offset of the current node relative to the parent node
```ts
/**
* Get the offset of the current node relative to the parent node
*/
offset(): number;
```
### `attributes`
Get or set node attributes
```ts
/**
* Get or set node attributes
* @param {string|undefined} key attribute name, key is empty to get all attributes, return Map
* @param {string|undefined} val attribute value, val is empty to get the attribute of the current key, return string|null
* @return {NodeInterface|{[k:string]:string}} return value or current instance
*/
attributes(): {[k: string]: string };
attributes(key: {[k: string]: string }): string;
attributes(key: string, val: string | number): NodeInterface;
attributes(key: string): string;
attributes(
key?: string | {[k: string]: string },
val?: string | number,
): NodeInterface | {[k: string]: string} | string;
```
### `removeAttributes`
Remove node attributes
```ts
/**
* Remove node attributes
* @param {String} key attribute name
* @return {NodeInterface} returns the current instance
*/
removeAttributes(key: string): NodeInterface;
```
### `hasClass`
Determine whether the node contains a certain class
```ts
/**
* Determine whether the node contains a certain class
* @param {String} className style name
* @return {Boolean} Does it contain
*/
hasClass(className: string): boolean;
```
### `addClass`
Add a class to the node
```ts
/**
*
* @param {string} className
* @return {NodeInterface} returns the current instance
*/
addClass(className: string): NodeInterface;
```
### `removeClass`
Remove node class
```ts
/**
* Remove node class
* @param {String} className
* @return {NodeInterface} returns the current instance
*/
removeClass(className: string): NodeInterface;
```
### `css`
Get or set the node style
```ts
/**
* Get or set the node style
* @param {String|undefined} key style name
* @param {String|undefined} val style value
* @return {NodeInterface|{[k:string]:string}} return value or current instance
*/
css(): {[k: string]: string };
css(key: {[k: string]: string | number }): NodeInterface;
css(key: string): string;
css(key: string, val: string | number): NodeInterface;
css(
key?: string | {[k: string]: string | number },
val?: string | number,
): NodeInterface | {[k: string]: string} | string;
```
### `width`
Get node width
```ts
/**
* Get node width
* @return {number} width
*/
width(): number;
```
### `height`
Get node height
```ts
/**
* Get node height
* @return {Number} height
*/
height(): number;
```
### `html`
Get or set node html text
```ts
/**
* Get or set node html text
*/
html(): string;
html(html: string): NodeInterface;
html(html?: string): NodeInterface | string;
```
### `text`
```ts
/**
* Get or set the node text
*/
text(): string;
text(text: string): NodeInterface;
text(text?: string): string | NodeInterface;
```
### `show`
Set the node to display state
```ts
/**
* Set the node to display state
* @param {String} display display value
* @return {NodeInterface} current instance
*/
show(display?: string): NodeInterface;
```
### `hide`
Set node to hidden state
```ts
/**
* Set the node to hidden
* @return {NodeInterface} current instance
*/
hide(): NodeInterface;
```
### `remove`
Remove all nodes of the current instance
```ts
/**
* Remove all nodes of the current instance
* @return {NodeInterface} current instance
*/
remove(): NodeInterface;
```
### `empty`
Clear all child nodes under the node, including text
```ts
/**
* Clear all child nodes under the node
* @return {NodeInterface} current instance
*/
empty(): NodeInterface;
```
### `equal`
Compare whether two nodes are the same, including the reference address
```ts
/**
* Compare whether two nodes are the same
* @param {NodeInterface|Node} node The node to compare
* @return {Boolean} are they the same
*/
equal(node: NodeInterface | Node): boolean;
```
### `clone`
Copy node
```ts
/**
* Copy node
* @param deep Whether to deep copy
*/
clone(deep?: boolean): NodeInterface;
```
### `prepend`
Insert the specified content at the beginning of the node
```ts
/**
* Insert the specified content at the beginning of the node
* @param {Selector} selector selector or node
* @return {NodeInterface} current instance
*/
prepend(selector: Selector): NodeInterface;
```
### `append`
Insert the specified content at the end of the node
```ts
/**
* Insert the specified content at the end of the node
* @param {Selector} selector selector or node
* @return {NodeInterface} current instance
*/
append(selector: Selector): NodeInterface;
```
### `before`
Insert a new node before the node
```ts
/**
* Insert a new node before the node
* @param {Selector} selector selector or node
* @return {NodeInterface} current instance
*/
before(selector: Selector): NodeInterface;
```
### `after`
Insert content after the node
```ts
/**
* Insert content after the node
* @param {Selector} selector selector or node
* @return {NodeInterface} current instance
*/
after(selector: Selector): NodeInterface;
```
### `replaceWith`
Replace node with new content
```ts
/**
* Replace the node with new content
* @param {Selector} selector selector or node
* @return {NodeInterface} current instance
*/
replaceWith(selector: Selector): NodeInterface;
```
### `getRoot`
Get the root node of the editing area where the node is located
```ts
/**
* Get the root node of the editing area where the node is located
*/
getRoot(): NodeInterface;
```
### `traverse`
Traverse all child nodes
```ts
/**
* Traverse all child nodes
* @param callback callback function, false: stop traversal, true: stop traversing the current node and child nodes, and continue to traverse the next sibling node
* @param order true: order, false: reverse order, default true
*/
traverse(
callback: (node: NodeInterface) => boolean | void,
order?: boolean,
): void;
```
### `getChildByPath`
Get child nodes according to path
```ts
/**
* According to the path to obtain
```

861
docs/api/node.zh-CN.md Normal file
View File

@ -0,0 +1,861 @@
# NodeInterface
在 DOM 的 `Node` 节点上进行扩展
类型:`NodeInterface`
## 创建 `NodeInterface` 对象
使用引擎内提供的 `$` 节点选择器来实例化 `NodeInterface` 对象
```ts
import { $ } from '@aomao/engine';
//使用CSS选择器查找节点
const content = $('.content');
//创建节点
const div = $('<div></div>');
document.body.append(div[0]);
//转换
const p = $(document.querySelector('p'));
const target = $(event.target);
```
## 属性
### `length`
Node 节点集合长度
类型:`number`
### `events`
当前对象中所有 Node 节点的事件对象集合
类型:`EventInterface[]`
### `document`
当前 Node 节点所在的 Document 对象。在使用 iframe 中,不同框架中的 document 并是不一致的,还有一些其它环境中也是如此,所以我们需要跟随这个对象。
类型:`Document | null`
### `window`
当前 Node 节点所在的 Window 对象。在使用 iframe 中,不同框架中的 window 并是不一致的,还有一些其它环境中也是如此,所以我们需要跟随这个对象。
类型:`Window | null`
### `context`
上下文节点
类型:`Context | undefined`
### `name`
节点名称
类型:`string`
### `type`
节点类型,与 `Node.nodeType` 一致 [API](https://developer.mozilla.org/zh-CN/docs/Web/API/Node/nodeType)
类型:`number | undefined`
### `display`
节点显示状态
类型:`string | undefined`
### `isFragment`
当前对象中的 Node 节点集合是否是框架片段
类型:`boolean`
### `[n: number]`
Node 节点集合,可以通过下标索引访问
返回类型Node
## 方法
### `each`
遍历当前对象内的所有 Node 节点
```ts
/**
* 遍历
* @param {Function} callback 回调函数
* @return {NodeInterface} 返回当前实例
*/
each(
callback: (node: Node, index: number) => boolean | void,
): NodeInterface;
```
### `toArray`
把当前对象内的所有 Node 节点转换为数组
```ts
toArray(): Array<NodeInterface>;
```
### `isElement`
当前节点是否为 Node.ELEMENT_NODE 节点类型
```ts
isElement(): boolean;
```
### `isText`
当前节点是否为 Node.TEXT_NODE 节点类型
```ts
isText(): boolean;
```
### `isCard`
当前节点是否为 Card 组件
```ts
isCard(): boolean;
```
### `isBlockCard`
当前节点是否为 block 类型的 Card 组件
```ts
isBlockCard(): boolean;
```
### `isInlineCard`
当前节点是否为 inline 类型的 Card 组件
```ts
isInlineCard(): boolean;
```
### `isEditableCard`
是否是可编辑的卡片
```ts
isEditableCard(): boolean;
```
### `isRoot`
是否为根节点
```ts
isRoot(): boolean;
```
### `isEditable`
是否为可编辑节点
```ts
isEditable(): boolean;
```
### `inEditor`
是否在根节点内
```ts
inEditor(): boolean;
```
### `isCursor`
是否是光标标记节点
```ts
isCursor(): boolean
```
### `get`
获取当前 Node 节点
```ts
get<E extends Node>(): E | null;
```
### `eq`
获取当前第 index 个节点
```ts
/**
* 获取当前第 index 节点
* @param {number} index
* @return {NodeInterface|undefined} NodeInterface 类,或 undefined
*/
eq(index: number): NodeInterface | undefined;
```
### `index`
获取当前节点所在父节点中的索引,仅计算节点类型为 ELEMENT_NODE 的节点
```ts
/**
* 获取当前节点所在父节点中的索引仅计算节点类型为ELEMENT_NODE的节点
* @return {number} 返回索引
*/
index(): number;
```
### `parent`
获取当前节点父节点
```ts
/**
* 获取当前节点父节点
* @return {NodeInterface} 父节点
*/
parent(): NodeInterface | undefined;
```
### `children`
查询当前节点的所有子节点
```ts
/**
*
* @param {Node | string} selector 查询器
* @return {NodeInterface} 符合条件的子节点
*/
children(selector?: string): NodeInterface;
```
### `first`
获取当前节点第一个子节点
```ts
/**
* 获取当前节点第一个子节点
* @return {NodeInterface} NodeInterface 子节点
*/
first(): NodeInterface | null;
```
### `last`
获取当前节点最后一个子节点
```ts
/**
* 获取当前节点最后一个子节点
* @return {NodeInterface} NodeInterface 子节点
*/
last(): NodeInterface | null;
```
### `prev`
返回节点之前的兄弟节点(包括文本节点、注释节点)
```ts
/**
* 返回节点之前的兄弟节点(包括文本节点、注释节点)
* @return {NodeInterface} NodeInterface 节点
*/
prev(): NodeInterface | null;
```
### `next`
返回节点之后的兄弟节点(包括文本节点、注释节点)
```ts
/**
* 返回节点之后的兄弟节点(包括文本节点、注释节点)
* @return {NodeInterface} NodeInterface 节点
*/
next(): NodeInterface | null;
```
### `prevElement`
返回节点之前的兄弟节点(不包括文本节点、注释节点)
```ts
/**
* 返回节点之前的兄弟节点(不包括文本节点、注释节点)
* @return {NodeInterface} NodeInterface 节点
*/
prevElement(): NodeInterface | null;
```
### `nextElement`
返回节点之后的兄弟节点(不包括文本节点、注释节点)
```ts
/**
* 返回节点之后的兄弟节点(不包括文本节点、注释节点)
* @return {NodeInterface} NodeInterface 节点
*/
nextElement(): NodeInterface | null;
```
### `getPath`
返回节点所在根节点路径,默认根节点为 document.body
```ts
/**
* 返回节点所在根节点路径,默认根节点为 document.body
* @param {Node} context 根节点,默认为 document.body
* @return {number} 路径
*/
getPath(context?: Node | NodeInterface): Array<number>;
```
### `contains`
判断节点是否包含要查询的节点
```ts
/**
* 判断节点是否包含要查询的节点
* @param {NodeInterface | Node} node 要查询的节点
* @return {Boolean} 是否包含
*/
contains(node: NodeInterface | Node): boolean;
```
### `find`
根据查询器查询当前节点
```ts
/**
* 根据查询器查询当前节点
* @param {String} selector 查询器
* @return {NodeInterface} 返回一个 NodeInterface 实例
*/
find(selector: string): NodeInterface;
```
### closest
根据查询器查询符合条件的离当前节点最近的父节点
```ts
/**
* 根据查询器查询符合条件的离当前节点最近的父节点
* @param {string} selector 查询器
* @return {NodeInterface} 返回一个 NodeInterface 实例
*/
closest(
selector: string,
callback?: (node: Node) => Node | undefined,
): NodeInterface;
```
### `on`
为当前节点绑定事件
```ts
/**
* 为当前节点绑定事件
* @param {String} eventType 事件类型
* @param {Function} listener 事件函数
* @return {NodeInterface} 返回当前实例
*/
on(eventType: string, listener: EventListener): NodeInterface;
```
### `off`
移除当前节点事件
```ts
/**
* 移除当前节点事件
* @param {String} eventType 事件类型
* @param {Function} listener 事件函数
* @return {NodeInterface} 返回当前实例
*/
off(eventType: string, listener: EventListener): NodeInterface;
```
### `getBoundingClientRect`
获取当前节点相对于视口的位置
```ts
/**
* 获取当前节点相对于视口的位置
* @param {Object} defaultValue 默认值
* @return {Object}
* {
* top,
* bottom,
* left,
* right
* }
*/
getBoundingClientRect(defaultValue?: {
top: number;
bottom: number;
left: number;
right: number;
}):
| { top: number; bottom: number; left: number; right: number }
| undefined;
```
### `removeAllEvents`
移除当前节点所有已绑定的事件
```ts
/**
* 移除当前节点所有已绑定的事件
* @return {NodeInterface} 当前 NodeInterface 实例
*/
removeAllEvents(): NodeInterface;
```
### `offset`
获取当前节点相对父节点的偏移量
```ts
/**
* 获取当前节点相对父节点的偏移量
*/
offset(): number;
```
### `attributes`
获取或设置节点属性
```ts
/**
* 获取或设置节点属性
* @param {string|undefined} key 属性名称key为空获取所有属性返回Map
* @param {string|undefined} val 属性值val为空获取当前key的属性返回string|null
* @return {NodeInterface|{[k:string]:string}} 返回值或当前实例
*/
attributes(): { [k: string]: string };
attributes(key: { [k: string]: string }): string;
attributes(key: string, val: string | number): NodeInterface;
attributes(key: string): string;
attributes(
key?: string | { [k: string]: string },
val?: string | number,
): NodeInterface | { [k: string]: string } | string;
```
### `removeAttributes`
移除节点属性
```ts
/**
* 移除节点属性
* @param {String} key 属性名称
* @return {NodeInterface} 返当前实例
*/
removeAttributes(key: string): NodeInterface;
```
### `hasClass`
判断节点是否包含某个 class
```ts
/**
* 判断节点是否包含某个 class
* @param {String} className 样式名称
* @return {Boolean} 是否包含
*/
hasClass(className: string): boolean;
```
### `addClass`
为节点增加一个 class
```ts
/**
*
* @param {string} className
* @return {NodeInterface} 返当前实例
*/
addClass(className: string): NodeInterface;
```
### `removeClass`
移除节点 class
```ts
/**
* 移除节点 class
* @param {String} className
* @return {NodeInterface} 返当前实例
*/
removeClass(className: string): NodeInterface;
```
### `css`
获取或设置节点样式
```ts
/**
* 获取或设置节点样式
* @param {String|undefined} key 样式名称
* @param {String|undefined} val 样式值
* @return {NodeInterface|{[k:string]:string}} 返回值或当前实例
*/
css(): { [k: string]: string };
css(key: { [k: string]: string | number }): NodeInterface;
css(key: string): string;
css(key: string, val: string | number): NodeInterface;
css(
key?: string | { [k: string]: string | number },
val?: string | number,
): NodeInterface | { [k: string]: string } | string;
```
### `width`
获取节点宽度
```ts
/**
* 获取节点宽度
* @return {number} 宽度
*/
width(): number;
```
### `height`
获取节点高度
```ts
/**
* 获取节点高度
* @return {Number} 高度
*/
height(): number;
```
### `html`
获取或设置节点 html 文本
```ts
/**
* 获取或设置节点html文本
*/
html(): string;
html(html: string): NodeInterface;
html(html?: string): NodeInterface | string;
```
### `text`
```ts
/**
* 获取或设置节点文本
*/
text(): string;
text(text: string): NodeInterface;
text(text?: string): string | NodeInterface;
```
### `show`
设置节点为显示状态
```ts
/**
* 设置节点为显示状态
* @param {String} display display值
* @return {NodeInterface} 当前实例
*/
show(display?: string): NodeInterface;
```
### `hide`
设置节点为隐藏状态
```ts
/**
* 设置节点为隐藏状态
* @return {NodeInterface} 当前实例
*/
hide(): NodeInterface;
```
### `remove`
移除当前实例所有节点
```ts
/**
* 移除当前实例所有节点
* @return {NodeInterface} 当前实例
*/
remove(): NodeInterface;
```
### `empty`
清空节点下的所有子节点,包括文本
```ts
/**
* 清空节点下的所有子节点
* @return {NodeInterface} 当前实例
*/
empty(): NodeInterface;
```
### `equal`
比较两个节点是否相同,包括引用地址
```ts
/**
* 比较两个节点是否相同
* @param {NodeInterface|Node} node 比较的节点
* @return {Boolean} 是否相同
*/
equal(node: NodeInterface | Node): boolean;
```
### `clone`
复制节点
```ts
/**
* 复制节点
* @param deep 是否深度复制
*/
clone(deep?: boolean): NodeInterface;
```
### `prepend`
在节点的开头插入指定内容
```ts
/**
* 在节点的开头插入指定内容
* @param {Selector} selector 选择器或节点
* @return {NodeInterface} 当前实例
*/
prepend(selector: Selector): NodeInterface;
```
### `append`
在节点的结尾插入指定内容
```ts
/**
* 在节点的结尾插入指定内容
* @param {Selector} selector 选择器或节点
* @return {NodeInterface} 当前实例
*/
append(selector: Selector): NodeInterface;
```
### `before`
在节点前插入新的节点
```ts
/**
* 在节点前插入新的节点
* @param {Selector} selector 选择器或节点
* @return {NodeInterface} 当前实例
*/
before(selector: Selector): NodeInterface;
```
### `after`
在节点后插入内容
```ts
/**
* 在节点后插入内容
* @param {Selector} selector 选择器或节点
* @return {NodeInterface} 当前实例
*/
after(selector: Selector): NodeInterface;
```
### `replaceWith`
将节点替换为新的内容
```ts
/**
* 将节点替换为新的内容
* @param {Selector} selector 选择器或节点
* @return {NodeInterface} 当前实例
*/
replaceWith(selector: Selector): NodeInterface;
```
### `getRoot`
获取节点所在编辑区域的根节点
```ts
/**
* 获取节点所在编辑区域的根节点
*/
getRoot(): NodeInterface;
```
### `traverse`
遍历所有子节点
```ts
/**
* 遍历所有子节点
* @param callback 回调函数false停止遍历 true停止遍历当前节点及子节点继续遍历下一个兄弟节点
* @param order true:顺序 false:倒序,默认 true
*/
traverse(
callback: (node: NodeInterface) => boolean | void,
order?: boolean,
): void;
```
### `getChildByPath`
根据路径获取子节点
```ts
/**
* 根据路径获取子节点
* @param path 路径
*/
getChildByPath(path: Path, filter?: (node: Node) => boolean): Node;
```
### `getIndex`
获取当前节点所在父节点中的索引
```ts
/**
* 获取当前节点所在父节点中的索引
*/
getIndex(filter?: (node: Node) => boolean): number;
```
### `findParent`
在指定容器里获取父节点
```ts
/**
* 在指定容器里获取父节点
* @param container 容器节点,默认为编辑器根节点
*/
findParent(container?: Node | NodeInterface): NodeInterface | null;
```
### `allChildren`
获取节点下的所有子节点
```ts
/**
* 获取节点下的所有子节点
*/
allChildren(): Array<Node>;
```
### `getViewport`
返回当前节点或者传入的节点所在当前节点的顶级 window 对象的视图边界
```ts
/**
* 返回当前节点或者传入的节点所在当前节点的顶级window对象的视图边界
* @param node 节点
*/
getViewport(
node?: NodeInterface,
): { top: number; left: number; bottom: number; right: number };
```
### `inViewport`
判断 view 是否在 node 节点根据当前节点的顶级 window 对象计算的视图边界内
```ts
/**
* 判断view是否在node节点根据当前节点的顶级window对象计算的视图边界内
* @param node 节点
* @param view 是否在视图的节点
*/
inViewport(node: NodeInterface, view: NodeInterface): boolean;
```
### `scrollIntoView`
如果 view 节点不可见,将滚动到 align 位置,默认为 nearest
```ts
/**
* 如果view节点不可见将滚动到align位置默认为nearest
* @param node 节点
* @param view 视图节点
* @param align 位置
*/
scrollIntoView(
node: NodeInterface,
view: NodeInterface,
align?: 'start' | 'center' | 'end' | 'nearest',
): void;
```
### `removeZeroWidthSpace`
移除节点内的所有零宽字符占位符 \u200B
```ts
/**
* 移除占位符 \u200B
* @param root 节点
*/
removeZeroWidthSpace(): void;
```

99
docs/api/parser.md Normal file
View File

@ -0,0 +1,99 @@
# Parser
Type: `ParserInterface`
## Constructor
```ts
/**
* @param source value or node is finally parsed as a DOM tree
* @param editor editor example
* @param paserBefore callback before parsing
* */
new (source: string | Node | NodeInterface, editor: EditorInterface, paserBefore?: (node: NodeInterface) => void): ParserInterface
```
## Method
### `traverse`
Traverse nodes
```ts
/**
* Traverse nodes
* @param node root node
* @param conversionRules tag name converter
* @param callbacks callbacks
* @param isCardNode is it a card
* @param includeCard whether to include the card
*/
traverse(
node: NodeInterface,
conversionRules: any,
callbacks: Callbacks,
isCardNode?: boolean,
includeCard?: boolean,
): void
```
### `toValue`
Traverse the DOM tree to generate standard editor values
```ts
/**
* Traverse the DOM tree to generate standard editor values
* @param schemaRules tag retention rules
* @param conversionRules tag conversion rules
* @param replaceSpaces whether to replace spaces
* @param customTags Whether to convert the cursor and card nodes into standard codes
*/
toValue(
schema?: SchemaInterface | null,
conversionRules?: any,
replaceSpaces?: boolean,
customTags?: boolean,
): string
```
### `toHTML`
Convert to HTML code
```ts
/**
* Convert to HTML code
* @param inner inner package node
* @param outter outer package node
*/
toHTML(inner?: Node, outter?: Node): {html: string, text: string}
```
### `toDOM`
Return to the DOM tree
```ts
/**
* Return to the DOM tree
*/
toDOM(schema?: SchemaInterface | null, conversionRules?: any): DocumentFragment
```
### `toText`
Convert to text
```ts
/**
* Convert to text
* @param conversionRules tag conversion rules
* @param includeCard whether to include the card
*/
toText(
schema?: SchemaInterface | null,
conversionRules?: any,
includeCard?: boolean,
): string
```

99
docs/api/parser.zh-CN.md Normal file
View File

@ -0,0 +1,99 @@
# 解析器
类型:`ParserInterface`
## 构造函数
```ts
/**
* @param source 值或者节点最终为解析为DOM树
* @param editor 编辑器实例
* @param paserBefore 解析前回调
* */
new (source: string | Node | NodeInterface, editor: EditorInterface, paserBefore?: (node: NodeInterface) => void): ParserInterface
```
## 方法
### `traverse`
遍历节点
```ts
/**
* 遍历节点
* @param node 根节点
* @param conversionRules 标签名称转换器
* @param callbacks 回调
* @param isCardNode 是否是卡片
* @param includeCard 是否包含卡片
*/
traverse(
node: NodeInterface,
conversionRules: any,
callbacks: Callbacks,
isCardNode?: boolean,
includeCard?: boolean,
): void
```
### `toValue`
遍历 DOM 树,生成符合标准的编辑器值
```ts
/**
* 遍历 DOM 树,生成符合标准的编辑器值
* @param schemaRules 标签保留规则
* @param conversionRules 标签转换规则
* @param replaceSpaces 是否替换空格
* @param customTags 是否将光标、卡片节点转换为标准代码
*/
toValue(
schema?: SchemaInterface | null,
conversionRules?: any,
replaceSpaces?: boolean,
customTags?: boolean,
): string
```
### `toHTML`
转换为 HTML 代码
```ts
/**
* 转换为HTML代码
* @param inner 内包裹节点
* @param outter 外包裹节点
*/
toHTML(inner?: Node, outter?: Node): { html: string, text: string}
```
### `toDOM`
返回 DOM 树
```ts
/**
* 返回DOM树
*/
toDOM(schema?: SchemaInterface | null, conversionRules?: any): DocumentFragment
```
### `toText`
转换为文本
```ts
/**
* 转换为文本
* @param conversionRules 标签转换规则
* @param includeCard 是否包含卡片
*/
toText(
schema?: SchemaInterface | null,
conversionRules?: any,
includeCard?: boolean,
): string
```

345
docs/api/range.md Normal file
View File

@ -0,0 +1,345 @@
# Range
Inherited from `Range`, has all the methods and attributes of `Range`, if you need to know the detailed attributes and methods, please visit the browser API [Range](https://developer.mozilla.org/zh-CN/docs/Web/ API/Range/Range)
Type: `RangeInterface`
## Attributes
The following only lists the properties and methods extended from the `Range` object
### `base`
`Range` object
Read only
### `startNode`
The node where the range starts, read-only
Type: `NodeInterface`
### `endNode`
Node at the end of the range, read-only
Type: `NodeInterface`
### `commonAncestorNode`
The nearest parent node shared by the start node and the end node
Type: `NodeInterface`
## Static method
### `create`
Create a RangeInterface object from a Point position
Point can be understood as the x,y coordinate point of the mouse pointer position
```ts
/**
* Create a RangeInterface object from a Point position
*/
create: (
editor: EditorInterface,
doc?: Document,
point?: { x: number; y: number },
) => RangeInterface;
```
### `from`
Create RangeInterface objects from Window, Selection, Range
```ts
/**
* Create RangeInterface objects from Window, Selection, Range
*/
from: (
editor: EditorInterface,
win?: Window | globalThis.Selection | globalThis.Range,
) => RangeInterface | null;
```
### `fromPath`
Restore the path to a RangeInterface object
```ts
/**
* Convert from path to range
* @param path
* @param context, the default editor node
*/
fromPath(path: Path[], context?: NodeInterface): RangeInterface;
```
## Method
### `select`
Let the range select a node
```ts
/**
* Select a node
* @param node node
* @param contents whether only selected contents
*/
select(node: NodeInterface | Node, contents?: boolean): RangeInterface;
```
### `getText`
Get the text of all nodes selected by the range
```ts
/**
* Get the text selected by the range
*/
getText(): string | null;
```
### `getClientRect`
Get the area occupied by the range
```ts
/**
* Get the area occupied by the range
*/
getClientRect(): DOMRect;
```
### `enlargeFromTextNode`
Extend the selection marker from the TextNode to the nearest non-TextNode node
```ts
/**
* Expand the selection mark from TextNode to the nearest non-TextNode node
* The selected content of the range remains unchanged
*/
enlargeFromTextNode(): RangeInterface;
```
### `shrinkToTextNode`
Reduce the selection marker from a non-TextNode to a TextNode node, as opposed to enlargeFromTextNode
```ts
/**
* Reduce the selection marker from a non-TextNode to a TextNode node, as opposed to enlargeFromTextNode
* The selected content of the range remains unchanged
*/
shrinkToTextNode(): RangeInterface;
```
### `enlargeToElementNode`
Extend the range selection boundary
```ts
/**
* Expand the border
* <p><strong><span>[123</span>abc]</strong>def</p>
* to
* <p>[<strong><span>123</span>abc</strong>]def</p>
* @param range selection
* @param toBlock whether to expand to block-level nodes
*/
enlargeToElementNode(toBlock?: boolean): RangeInterface;
```
### `shrinkToElementNode`
Shrink the range selection boundary
```ts
/**
* Reduce the border
* <body>[<p><strong>123</strong></p>]</body>
* to
* <body><p><strong>[123]</strong></p></body>
*/
shrinkToElementNode(): RangeInterface;
```
### `createSelection`
Create selectionElement and mark the position of the range, focus or range by inserting a custom span node. Through these marks, we can easily get the nodes in the selection area
For more properties and methods, please see the `SelectionInterface` API
```ts
/**
* Create selectionElement, mark the position by inserting a span node
*/
createSelection(): SelectionInterface;
```
### `getSubRanges`
Split the range selection into multiple sub-selections according to text nodes and card nodes
```ts
/**
* Get a collection of sub-selections
* @param includeCard whether to include the card
*/
getSubRanges(includeCard?: boolean): Array<RangeInterface>;
```
### `setOffset`
Let the range select a node and set its start position offset and end position offset
```ts
/**
* @param node The node to be set
* @param start the offset of the starting position
* @param end The offset of the end position
* */
setOffset(
node: Node | NodeInterface,
start: number,
end: number,
): RangeInterface;
```
### `findElements`
Find a collection of element nodes in the range area, excluding Text text nodes
```ts
findElements(): Array<Node>;
```
### `inCard`
Query whether the range is in the card
```ts
inCard(): boolean;
```
### `getStartOffsetNode`
Get the node at the offset relative to the node at the beginning of the range
```ts
getStartOffsetNode(): Node;
```
### `getEndOffsetNode`
Get the node at the offset relative to the node at the end of the range
```ts
getEndOffsetNode(): Node;
```
### `containsCard`
Whether the range area contains a card
```ts
/**
* Whether to include a card
*/
containsCard(): boolean;
```
### `handleBr`
Repair the Br node at the range position
```ts
/**
* When entering content, delete the BR tag generated by the browser, and add BR to the empty block
* Delete scene
* <p><br />foo</p>
* <p>foo<br /></p>
* Keep the scene
* <p><br /><br />foo</p>
* <p>foo<br /><br /></p>
* <p>foo<br />bar</p>
* Add scene
* <p></p>
* @param isLeft
*/
handleBr(isLeft?: boolean): RangeInterface;
```
### `getPrevNode`
Get the node before the range start position
```ts
/**
* Get the node before the start position
* <strong>foo</strong>|bar
*/
getPrevNode(): NodeInterface | undefined;
```
### `getNextNode`
Get the node after the end position
```ts
/**
* Get the node after the end position
* foo|<strong>bar</strong>
*/
getNextNode(): NodeInterface | undefined;
```
### `deepCut`
Cut the contents of the area selected by the range. Data will be on the clipboard
```ts
/**
* Deep cut
*/
deepCut(): void;
```
### `equal`
Compare whether the range of two range objects are equal
```ts
/**
* Compare whether the two ranges are equal
*range
*/
equal(range: RangeInterface | globalThis.Range): boolean;
```
### `getRootBlock`
Get the nearest root node of the current selection
```ts
/**
* Get the nearest root node of the current selection
*/
getRootBlock(): NodeInterface | undefined;
```
### `toPath`
Convert range selection to path
```ts
/**
* Get the range path
*/
toPath(): Path[];
```

345
docs/api/range.zh-CN.md Normal file
View File

@ -0,0 +1,345 @@
# 光标
继承自 `Range`,拥有`Range`所有的方法和属性,需要了解详细属性和方法,请访问浏览器 API[Range](https://developer.mozilla.org/zh-CN/docs/Web/API/Range/Range)
类型:`RangeInterface`
## 属性
以下只列出从`Range`对象扩展出来的属性和方法
### `base`
`Range` 对象
只读
### `startNode`
光标开始位置节点,只读
类型:`NodeInterface`
### `endNode`
光标结束位置节点,只读
类型:`NodeInterface`
### `commonAncestorNode`
开始节点和结束节点所共有最近的父节点
类型:`NodeInterface`
## 静态方法
### `create`
从一个 Point 位置创建 RangeInterface 对象
Point 可以理解为鼠标指针位置的 x,y 坐标点
```ts
/**
* 从一个 Point 位置创建 RangeInterface 对象
*/
create: (
editor: EditorInterface,
doc?: Document,
point?: { x: number; y: number },
) => RangeInterface;
```
### `from`
从 Window 、Selection、Range 中创建 RangeInterface 对象
```ts
/**
* 从 Window 、Selection、Range 中创建 RangeInterface 对象
*/
from: (
editor: EditorInterface,
win?: Window | globalThis.Selection | globalThis.Range,
) => RangeInterface | null;
```
### `fromPath`
把路径还原为 RangeInterface 对象
```ts
/**
* 从路径转换为光标
* @param path
* @param 上下文,默认编辑器节点
*/
fromPath(path: Path[], context?: NodeInterface): RangeInterface;
```
## 方法
### `select`
让光标选中一个节点
```ts
/**
* 选中一个节点
* @param node 节点
* @param contents 是否只选中内容
*/
select(node: NodeInterface | Node, contents?: boolean): RangeInterface;
```
### `getText`
获取光标选中的所有节点的文本
```ts
/**
* 获取光标选中的文本
*/
getText(): string | null;
```
### `getClientRect`
获取光标所占的区域
```ts
/**
* 获取光标所占的区域
*/
getClientRect(): DOMRect;
```
### `enlargeFromTextNode`
将选择标记从 TextNode 扩大到最近非 TextNode 节点
```ts
/**
* 将选择标记从 TextNode 扩大到最近非TextNode节点
* range 实质所选择的内容不变
*/
enlargeFromTextNode(): RangeInterface;
```
### `shrinkToTextNode`
将选择标记从非 TextNode 缩小到 TextNode 节点上,与 enlargeFromTextNode 相反
```ts
/**
* 将选择标记从非 TextNode 缩小到TextNode节点上与 enlargeFromTextNode 相反
* range 实质所选择的内容不变
*/
shrinkToTextNode(): RangeInterface;
```
### `enlargeToElementNode`
扩大光标选区边界
```ts
/**
* 扩大边界
* <p><strong><span>[123</span>abc]</strong>def</p>
* to
* <p>[<strong><span>123</span>abc</strong>]def</p>
* @param range 选区
* @param toBlock 是否扩大到块级节点
*/
enlargeToElementNode(toBlock?: boolean): RangeInterface;
```
### `shrinkToElementNode`
缩小光标选区边界
```ts
/**
* 缩小边界
* <body>[<p><strong>123</strong></p>]</body>
* to
* <body><p><strong>[123]</strong></p></body>
*/
shrinkToElementNode(): RangeInterface;
```
### `createSelection`
创建 selectionElement通过插入自定义 span 节点标记光标 anchor、focus 或 cursor 的位置。通过这些标记我们可以很轻松的获取到选区内的节点
更多属性和方法请查看 `SelectionInterface` API
```ts
/**
* 创建 selectionElement通过插入 span 节点标记位置
*/
createSelection(): SelectionInterface;
```
### `getSubRanges`
将光标选区按照文本节点和卡片节点分割为多个子选区
```ts
/**
* 获取子选区集合
* @param includeCard 是否包含卡片
*/
getSubRanges(includeCard?: boolean): Array<RangeInterface>;
```
### `setOffset`
让光标选择一个节点,并设置它的开始位置偏移量和结束位置偏移量
```ts
/**
* @param node 要设置的节点
* @param start 开始位置的偏移量
* @param end 结束位置的偏移量
* */
setOffset(
node: Node | NodeInterface,
start: number,
end: number,
): RangeInterface;
```
### `findElements`
在光标区域中查找元素节点集合,不包括 Text 文本节点
```ts
findElements(): Array<Node>;
```
### `inCard`
查询光标是否在卡片内
```ts
inCard(): boolean;
```
### `getStartOffsetNode`
获取相对于光标开始位置节点的偏移量处的节点
```ts
getStartOffsetNode(): Node;
```
### `getEndOffsetNode`
获取相对于光标结束位置节点的偏移量处的节点
```ts
getEndOffsetNode(): Node;
```
### `containsCard`
光标区域是否包含卡片
```ts
/**
* 是否包含卡片
*/
containsCard(): boolean;
```
### `handleBr`
在光标位置修复 Br 节点
```ts
/**
* 输入内容时,删除浏览器生成的 BR 标签,对空 block 添加 BR
* 删除场景
* <p><br />foo</p>
* <p>foo<br /></p>
* 保留场景
* <p><br /><br />foo</p>
* <p>foo<br /><br /></p>
* <p>foo<br />bar</p>
* 添加场景
* <p></p>
* @param isLeft
*/
handleBr(isLeft?: boolean): RangeInterface;
```
### `getPrevNode`
获取光标开始位置前的节点
```ts
/**
* 获取开始位置前的节点
* <strong>foo</strong>|bar
*/
getPrevNode(): NodeInterface | undefined;
```
### `getNextNode`
获取结束位置后的节点
```ts
/**
* 获取结束位置后的节点
* foo|<strong>bar</strong>
*/
getNextNode(): NodeInterface | undefined;
```
### `deepCut`
剪切光标选择区域的内容。数据会在剪贴板上
```ts
/**
* 深度剪切
*/
deepCut(): void;
```
### `equal`
对比两个光标对象范围是否相等
```ts
/**
* 对比两个范围是否相等
*范围
*/
equal(range: RangeInterface | globalThis.Range): boolean;
```
### `getRootBlock`
获取当前选区最近的根节点
```ts
/**
* 获取当前选区最近的根节点
*/
getRootBlock(): NodeInterface | undefined;
```
### `toPath`
将光标选区转换为路径
```ts
/**
* 获取光标路径
*/
toPath(): Path[];
```

287
docs/api/schema.md Normal file
View File

@ -0,0 +1,287 @@
# Schema
Type: `SchemaInterface`
## Attributes
### `data`
Set of all rule constraints
```ts
data: {
blocks: Array<SchemaRule>;//Block-level nodes
inlines: Array<SchemaRule>;//Inline node
marks: Array<SchemaRule>;//Style node
globals: {[key: string]: SchemaAttributes | SchemaStyle };//Global rules
};
```
## Method
### `add`
Increase rule constraints
```ts
/**
* Added rules, div tags are not allowed, div will be used as card
* When only type and attributes are used, they will be regarded as global attributes of this type, and will be merged with all other label attributes of the same type
* @param rules
*/
add(
rules: SchemaRule | SchemaGlobal | Array<SchemaRule | SchemaGlobal>,
): void;
```
### `find`
Find rules
```ts
/**
* Find rules
* @param callback search condition
*/
find(callback: (rule: SchemaRule) => boolean): Array<SchemaRule>;
```
### `getType`
Get node type
```ts
/**
* Get the node type
* @param node node
*/
getType(node: NodeInterface):'block' |'mark' |'inline' | undefined;
```
### `checkNode`
Check whether the node conforms to a certain attribute rule
```ts
/**
* Check whether the node conforms to a certain attribute rule
* @param node node
* @param attributes attribute rules
*/
checkNode(
node: NodeInterface,
attributes?: SchemaAttributes | SchemaStyle,
): boolean;
```
### `checkStyle`
Check whether the style value meets the node style rules
```ts
/**
* Check whether the style value meets the node style rules
* @param name node name
* @param styleName style name
* @param styleValue style value
*/
checkStyle(name: string, styleName: string, styleValue: string): boolean;
```
### `checkAttributes`
Check whether the value meets the rules of node attributes
```ts
/**
* Check whether the value meets the rules of node attributes
* @param name node name
* @param attributesName attribute name
* @param attributesValue attribute value
*/
checkAttributes(
name: string,
attributesName: string,
attributesValue: string,
): boolean;
```
### `checkValue`
Check whether the value meets the rules
```ts
/**
* Whether the detection value meets the rules
* @param rule
* @param attributesName attribute name
* @param attributesValue attribute value
*/
checkValue(
rule: SchemaAttributes | SchemaStyle,
attributesName: string,
attributesValue: string,
): boolean;
```
### `checkStyle`
Check whether the style value meets the node style rules
```ts
/**
* Check whether the style value meets the node style rules
* @param name node name
* @param styleName style name
* @param styleValue style value
* @param type specifies the type
*/
checkStyle(
name: string,
styleName: string,
styleValue: string,
type?:'block' |'mark' |'inline',
): void;
```
### `checkAttributes`
Check whether the value meets the rules of node attributes
```ts
/**
* Check whether the value meets the rules of node attributes
* @param name node name
* @param attributesName attribute name
* @param attributesValue attribute value
* @param type specifies the type
*/
checkAttributes(
name: string,
attributesName: string,
attributesValue: string,
type?:'block' |'mark' |'inline',
): void;
```
### `filterStyles`
Filter node style
```ts
/**
* Filter node style
* @param name node name
* @param styles style
* @param type specifies the type
*/
filterStyles(
name: string,
styles: {[k: string]: string },
type?:'block' |'mark' |'inline',
): void;
```
### `filterAttributes`
Filter node attributes
```ts
/**
* Filter node attributes
* @param name node name
* @param attributes
* @param type specifies the type
*/
filterAttributes(
name: string,
attributes: {[k: string]: string },
type?:'block' |'mark' |'inline',
): void;
```
### `clone`
Clone the current schema object
```ts
/**
* Clone the current schema object
*/
clone(): SchemaInterface;
```
### `toAttributesMap`
Combine attributes of the same label and gloals attributes into map format
```ts
/**
* Combine and convert the attributes of the same tag and the attributes of gloals into map format
* @param type specifies the type of conversion "block" | "mark" | "inline"
*/
toAttributesMap(type?:'block' |'mark' |'inline'): SchemaMap;
```
### `getMapCache`
Get the merged Map format
```ts
/**
* Get the merged Map format
* @param type, default is all
*/
getMapCache(type?:'block' |'mark' |'inline'): SchemaMap;
```
### `closest`
Find the name of the topmost node where the node matches the rule
```ts
/**
* Find the name of the top-level node where the node meets the rule
* @param name node name
* @returns The name of the top block node
*/
closest(name: string): string;
```
### `isAllowIn`
Determine whether the child node name is allowed to be placed in the specified parent node
```ts
/**
* Determine whether the child node name is allowed to be placed in the specified parent node
* @param source parent node name
* @param target child node name
* @returns true | false
*/
isAllowIn(source: string, target: string): boolean;
```
### `getAllowInTags`
Get the label collection that allows sub-block nodes
```ts
/**
* Get the label collection that allows child block nodes
* @returns
*/
getAllowInTags(): Array<string>;
```
### `getCanMergeTags`
Get the label collection of block nodes that can be merged
```ts
/**
* Get the label collection of block nodes that can be merged
* @returns
*/
getCanMergeTags(): Array<string>;
```

287
docs/api/schema.zh-CN.md Normal file
View File

@ -0,0 +1,287 @@
# Schema
类型:`SchemaInterface`
## 属性
### `data`
所有规则约束集合
```ts
data: {
blocks: Array<SchemaRule>;//块级节点
inlines: Array<SchemaRule>;//行内节点
marks: Array<SchemaRule>;//样式节点
globals: { [key: string]: SchemaAttributes | SchemaStyle };//全局规则
};
```
## 方法
### `add`
增加规则约束
```ts
/**
* 增加规则不允许设置div标签div将用作card使用
* 只有 type 和 attributes 时,将作为此类型全局属性,与其它所有同类型标签属性将合并
* @param rules 规则
*/
add(
rules: SchemaRule | SchemaGlobal | Array<SchemaRule | SchemaGlobal>,
): void;
```
### `find`
查找规则
```ts
/**
* 查找规则
* @param callback 查找条件
*/
find(callback: (rule: SchemaRule) => boolean): Array<SchemaRule>;
```
### `getType`
获取节点类型
```ts
/**
* 获取节点类型
* @param node 节点
*/
getType(node: NodeInterface): 'block' | 'mark' | 'inline' | undefined;
```
### `checkNode`
检测节点是否符合某一属性规则
```ts
/**
* 检测节点是否符合某一属性规则
* @param node 节点
* @param attributes 属性规则
*/
checkNode(
node: NodeInterface,
attributes?: SchemaAttributes | SchemaStyle,
): boolean;
```
### `checkStyle`
检测样式值是否符合节点样式规则
```ts
/**
* 检测样式值是否符合节点样式规则
* @param name 节点名称
* @param styleName 样式名称
* @param styleValue 样式值
*/
checkStyle(name: string, styleName: string, styleValue: string): boolean;
```
### `checkAttributes`
检测值是否符合节点属性的规则
```ts
/**
* 检测值是否符合节点属性的规则
* @param name 节点名称
* @param attributesName 属性名称
* @param attributesValue 属性值
*/
checkAttributes(
name: string,
attributesName: string,
attributesValue: string,
): boolean;
```
### `checkValue`
检测值是否符合规则
```ts
/**
* 检测值是否符合规则
* @param rule 规则
* @param attributesName 属性名称
* @param attributesValue 属性值
*/
checkValue(
rule: SchemaAttributes | SchemaStyle,
attributesName: string,
attributesValue: string,
): boolean;
```
### `checkStyle`
检测样式值是否符合节点样式规则
```ts
/**
* 检测样式值是否符合节点样式规则
* @param name 节点名称
* @param styleName 样式名称
* @param styleValue 样式值
* @param type 指定类型
*/
checkStyle(
name: string,
styleName: string,
styleValue: string,
type?: 'block' | 'mark' | 'inline',
): void;
```
### `checkAttributes`
检测值是否符合节点属性的规则
```ts
/**
* 检测值是否符合节点属性的规则
* @param name 节点名称
* @param attributesName 属性名称
* @param attributesValue 属性值
* @param type 指定类型
*/
checkAttributes(
name: string,
attributesName: string,
attributesValue: string,
type?: 'block' | 'mark' | 'inline',
): void;
```
### `filterStyles`
过滤节点样式
```ts
/**
* 过滤节点样式
* @param name 节点名称
* @param styles 样式
* @param type 指定类型
*/
filterStyles(
name: string,
styles: { [k: string]: string },
type?: 'block' | 'mark' | 'inline',
): void;
```
### `filterAttributes`
过滤节点属性
```ts
/**
* 过滤节点属性
* @param name 节点名称
* @param attributes 属性
* @param type 指定类型
*/
filterAttributes(
name: string,
attributes: { [k: string]: string },
type?: 'block' | 'mark' | 'inline',
): void;
```
### `clone`
克隆当前 schema 对象
```ts
/**
* 克隆当前schema对象
*/
clone(): SchemaInterface;
```
### `toAttributesMap`
将相同标签的属性和 gloals 属性合并转换为 map 格式
```ts
/**
* 将相同标签的属性和gloals属性合并转换为map格式
* @param type 指定转换的类别 "block" | "mark" | "inline"
*/
toAttributesMap(type?: 'block' | 'mark' | 'inline'): SchemaMap;
```
### `getMapCache`
获取合并后的 Map 格式
```ts
/**
* 获取合并后的Map格式
* @param 类型,默认为所有
*/
getMapCache(type?: 'block' | 'mark' | 'inline'): SchemaMap;
```
### `closest`
查找节点符合规则的最顶层的节点名称
```ts
/**
* 查找节点符合规则的最顶层的节点名称
* @param name 节点名称
* @returns 最顶级的block节点名称
*/
closest(name: string): string;
```
### `isAllowIn`
判断子节点名称是否允许放入指定的父节点中
```ts
/**
* 判断子节点名称是否允许放入指定的父节点中
* @param source 父节点名称
* @param target 子节点名称
* @returns true | false
*/
isAllowIn(source: string, target: string): boolean;
```
### `getAllowInTags`
获取允许有子 block 节点的标签集合
```ts
/**
* 获取允许有子block节点的标签集合
* @returns
*/
getAllowInTags(): Array<string>;
```
### `getCanMergeTags`
获取能够合并的 block 节点的标签集合
```ts
/**
* 获取能够合并的block节点的标签集合
* @returns
*/
getCanMergeTags(): Array<string>;
```

89
docs/api/selection.md Normal file
View File

@ -0,0 +1,89 @@
# Selection
With `Selection`, you can easily create a mark in the DOM tree based on the selection of `RangeInterface`, and then get the nodes in the middle or on both sides of the mark
## Constructor
```ts
new (editor: EditorInterface, range: RangeInterface): SelectionInterface
```
## Attributes
### `anchor`
Mark the node at the beginning of the selection
Type: `NodeInterface | null`
### `focus`
Mark the node at the end of the selection. If the collapsed of `Range` is true, then the focus node and the anchor node are consistent
Type: `NodeInterface | null`
## Static method
### `removeTags`
Remove cursor position placeholder label
```ts
/**
* Remove the placeholder label at the cursor position
* @param value The string to be removed
*/
static removeTags = (value: string) => void
```
## Method
### `has`
Is there a created mark?
```ts
has(): boolean;
```
### `create`
Create a mark
```ts
/**
* Create mark
*/
create(): void;
```
### `move`
Set Range to return to the marked position and delete the mark
```ts
/**
* Let Range select the mark position and delete the mark
*/
move(): void;
```
### `getNode`
Get the node relative to the marked position of the node, and the mark will be removed after acquisition
```ts
/**
* Get the node of the node relative to the marked position, and the mark will be removed after acquisition
* @param node node
* @param position
* @param isClone whether to make a copy
* @param callback Call back when deleting a node, return a boolean to indicate whether the current node is deleted
*/
getNode(
node: NodeInterface,
position?:'left' |'center' |'right',
isClone?: boolean,
callback?: (node: NodeInterface) => boolean
): NodeInterface;
```

View File

@ -0,0 +1,89 @@
# 范围标记
通过 `Selection` 可以很轻松的根据`RangeInterface`的选区在 DOM 树中创建标记,然后获取标记中间或者两侧的节点
## 构造函数
```ts
new (editor: EditorInterface, range: RangeInterface): SelectionInterface
```
## 属性
### `anchor`
选区开始位置标记节点
类型:`NodeInterface | null`
### `focus`
选区结束位置标记节点。如果 `Range` 的 collapsed 为 true那么 focus 节点与 anchor 节点是一致的
类型:`NodeInterface | null`
## 静态方法
### `removeTags`
移除光标位置占位标签
```ts
/**
* 移除光标位置占位标签
* @param value 需要移除的字符串
*/
static removeTags = (value: string) => void
```
## 方法
### `has`
是否有创建好的标记
```ts
has(): boolean;
```
### `create`
创建标记
```ts
/**
* 创建标记
*/
create(): void;
```
### `move`
设置 Range 恢复到标记位置,并删除标记
```ts
/**
* 让Range选择标记位置并删除标记
*/
move(): void;
```
### `getNode`
获取节点相对于标记位置的节点,获取后会移除标记
```ts
/**
* 获取节点相对于标记位置的节点,获取后会移除标记
* @param node 节点
* @param position 位置
* @param isClone 是否复制一个副本
* @param callback 删除节点时回调,返回一个 boolean 来表示当前节点是否删除
*/
getNode(
node: NodeInterface,
position?: 'left' | 'center' | 'right',
isClone?: boolean,
callback?: (node: NodeInterface) => boolean
): NodeInterface;
```

185
docs/api/utils.md Normal file
View File

@ -0,0 +1,185 @@
# Useful methods and constants
## Constant
### `isEdge`
Edge browser
### `isChrome`
Is it a Chrome browser
### `isFirefox`
Is it a Firefox browser
### `isSafari`
Is it a Safari browser
### `isMobile`
Is it a mobile browser
### `isIos`
Is it an iOS system
### `isAndroid`
Whether it is Android
### `isMacos`
Is it a Mac OS X system
### `isWindows`
Is it a Windows system
## Method
### `isNodeEntry`
Whether it is a NodeInterface object
Accept the following types of objects
- `string`
- `HTMLElement`
- `Node`
- `Array<Node>`
- `NodeList`
- `NodeInterface`
- `EventTarget`
### `isNodeList`
Is it a NodeList object
Accept the following types of objects
- `string`
- `HTMLElement`
- `Node`
- `Array<Node>`
- `NodeList`
- `NodeInterface`
- `EventTarget`
### `isNode`
Is it a Node object
Accept the following types of objects
- `string`
- `HTMLElement`
- `Node`
- `Array<Node>`
- `NodeList`
- `NodeInterface`
- `EventTarget`
### `isSelection`
Is it a window.Selection object
Accept the following types of objects
- Window
- Selection
- Range
### `isRange`
Is it window.Range
Accept the following types of objects
- Window
- Selection
- Range
### `isRangeInterface`
Whether it is a RangeInterface object extended from Range
Accept the following types of objects
- NodeInterface
- RangeInterface
### `isSchemaRule`
Is it an object of type `SchemaRule`
Accept the following types of objects
- SchemaRule
- SchemaGlobal
### `isMarkPlugin`
Is it a Mark type plug-in
Accepted object: `PluginInterface`
### `isInlinePlugin`
Is it an Inline type plug-in
Accepted object: `PluginInterface`
### `isBlockPlugin`
Is it a Block type plug-in
Accepted object: `PluginInterface`
### `isEngine`
Is it an engine
Accepted object: `EditorInterface`
### `getWindow`
Get the window object from the node
If window is undefined, it will try to get the window object from global['__amWindow']
```ts
(node?: Node): Window & typeof globalThis
```
### `getDocument`
Get the document object from the node
```ts
getDocument(node?: Node): Document
```
### `combinText`
Remove empty text nodes and connect adjacent text nodes
```ts
combinText(node: NodeInterface | Node): void
```
### `getTextNodes`
Get all textnode type elements in a dom element
```ts
/**
* Get all textnode type elements in a dom element
* @param {Node} node-dom node
* @param {Function} filter-filter
* @return {Array} the obtained text node
*/
getTextNodes(node: Node, filter?:(node: Node) => boolean): Array<Node>
```

185
docs/api/utils.zh-CN.md Normal file
View File

@ -0,0 +1,185 @@
# 实用方法和常量
## 常量
### `isEdge`
否是 Edge 浏览器
### `isChrome`
是否是 Chrome 浏览器
### `isFirefox`
是否是 Firefox 浏览器
### `isSafari`
是否是 Safari 浏览器
### `isMobile`
是否是 手机浏览器
### `isIos`
是否是 iOS 系统
### `isAndroid`
是否是 安卓系统
### `isMacos`
是否是 Mac OS X 系统
### `isWindows`
是否是 Windows 系统
## 方法
### `isNodeEntry`
是否是 NodeInterface 对象
接受以下类型对象
- `string`
- `HTMLElement`
- `Node`
- `Array<Node>`
- `NodeList`
- `NodeInterface`
- `EventTarget`
### `isNodeList`
是否是 NodeList 对象
接受以下类型对象
- `string`
- `HTMLElement`
- `Node`
- `Array<Node>`
- `NodeList`
- `NodeInterface`
- `EventTarget`
### `isNode`
是否是 Node 对象
接受以下类型对象
- `string`
- `HTMLElement`
- `Node`
- `Array<Node>`
- `NodeList`
- `NodeInterface`
- `EventTarget`
### `isSelection`
是否是 window.Selection 对象
接受以下类型对象
- Window
- Selection
- Range
### `isRange`
是否是 window.Range
接受以下类型对象
- Window
- Selection
- Range
### `isRangeInterface`
是否是从 Range 扩展的 RangeInterface 对象
接受以下类型对象
- NodeInterface
- RangeInterface
### `isSchemaRule`
是否是 `SchemaRule` 类型对象
接受以下类型对象
- SchemaRule
- SchemaGlobal
### `isMarkPlugin`
是否是 Mark 类型插件
接受对象:`PluginInterface`
### `isInlinePlugin`
是否是 Inline 类型插件
接受对象:`PluginInterface`
### `isBlockPlugin`
是否是 Block 类型插件
接受对象:`PluginInterface`
### `isEngine`
是否是引擎
接受对象:`EditorInterface`
### `getWindow`
从节点中获取 window 对象
如果 window 是 undefined 会尝试从 global['__amWindow'] 中获取 window 对象
```ts
(node?: Node): Window & typeof globalThis
```
### `getDocument`
从节点中获取 document 对象
```ts
getDocument(node?: Node): Document
```
### `combinText`
移除空的文本节点,并连接相邻的文本节点
```ts
combinText(node: NodeInterface | Node): void
```
### `getTextNodes`
获取一个 dom 元素内所有的 textnode 类型的元素
```ts
/**
* 获取一个 dom 元素内所有的 textnode 类型的元素
* @param {Node} node - dom节点
* @param {Function} filter - 过滤器
* @return {Array} 获取的文本节点
*/
getTextNodes(node: Node, filter?:(node: Node) => boolean): Array<Node>
```

31
docs/api/view.md Normal file
View File

@ -0,0 +1,31 @@
# View
Type: `ViewInterface`
## Method
### `render`
Render content
```ts
/**
* Render content
* @param content rendered content
* @param trigger Whether to trigger the rendering completion event, used to show the special effects of the plug-in. For example, in the heading plug-in, the anchor point display function is displayed. The default is true
*/
render(content: string, trigger?: boolean): void;
```
### `trigger`
Trigger events, you can actively trigger `render` events `trigger("render","nodes to be rendered")`
```ts
/**
* trigger event
* @param eventType event name
* @param args parameters
*/
trigger(eventType: string, ...args: any): any;
```

31
docs/api/view.zh-CN.md Normal file
View File

@ -0,0 +1,31 @@
# 阅读器
类型:`ViewInterface`
## 方法
### `render`
渲染内容
```ts
/**
* 渲染内容
* @param content 渲染的内容
* @param trigger 是否触发渲染完成事件用来展示插件的特俗效果。例如在heading插件中展示锚点显示功能。默认为 true
*/
render(content: string, trigger?: boolean): void;
```
### `trigger`
触发事件,可以主动触发 `render` 事件 `trigger("render","需要渲染的节点")`
```ts
/**
* 触发事件
* @param eventType 事件名称
* @param args 参数
*/
trigger(eventType: string, ...args: any): any;
```

234
docs/config/index.md Normal file
View File

@ -0,0 +1,234 @@
---
toc: menu
---
# Engine configuration
Passed in when instantiating the engine
```ts
//Instantiate the engine
const engine = new Engine(render node, {
... configuration items,
});
```
### lang
- Type: `string`
- Default value: `zh-CN`
- Detailed: Language configuration, temporarily supports `zh-CN`, `en-US`. Can use `locale` configuration
```ts
const view = new View(render node, {
lang:'zh-CN',
});
```
### locale
- Type: `object`
- Default value: `zh-CN`
- Detailed: Configure additional language packs
Language pack, default language pack [https://github.com/yanmao-cc/am-editor/blob/master/locale](https://github.com/yanmao-cc/am-editor/blob/master/locale)
```ts
const view = new View(render node, {
locale: {
'zh-CN': {
test:'Test',
a: {
b: "B"
}
},
}
});
console.log(view.language.get<string>('test'));
```
### className
- Type: `string`
- Default value: `null`
- Detailed: Add additional styles of editor render nodes
### tabIndex
- Type: `number`
- Default value: `null`
- Detailed: Which tab item is the current editor located in
### root
- Type: `Node`
- Default value: the parent node of the render node of the current editor
- Detailed: Editor root node
### plugins
- Type: `Array<Plugin>`
- Default value: `[]`
- Detailed: A collection of plugins that implement the abstract class of `Plugin`
### cards
- Type: `Array<Card>`
- Default value: `[]`
- Detailed: Implement the card collection of the `Card` abstract class
### config
- Type: `{ [key: string]: PluginOptions }`
- Default value: `{}`
- Detailed: the configuration item of each plug-in, the key is the name of the plug-in, please refer to the description of each plug-in for detailed configuration. [Configuration example](https://github.com/yanmao-cc/am-editor/blob/master/examples/react/components/editor/config.tsx)
Some plugins require the configuration of additional properties:
```ts
// Configure italic markdown syntax
[Italic.pluginName]: {
// The default is _ underscore, here is modified to a single * sign
markdown:'*',
},
// upload picture
[ImageUploader.pluginName]: {
file: {
action: `${DOMAIN}/upload/image`,
headers: {Authorization: 213434 },
},
remote: {
action: `${DOMAIN}/upload/image`,
},
isRemote: (src: string) => src.indexOf(DOMAIN) <0,
},
// File Upload
[FileUploader.pluginName]: {
action: `${DOMAIN}/upload/file`,
},
// video upload
[VideoUploader.pluginName]: {
action: `${DOMAIN}/upload/video`,
},
// Mathematical formula generation address, the project is at: https://drawing.yanmao.cc
[Math.pluginName]: {
action: `https://g.yanmao.cc/latex`,
parse: (res: any) => {
if (res.success) return {result: true, data: res.svg };
return {result: false };
},
},
// Submit plugin configuration
[Mention.pluginName]: {
action: `${DOMAIN}/user/search`,
onLoading: (root: NodeInterface) => {
// Vue can be rendered using createApp
return ReactDOM.render(<Loading />, root.get<HTMLElement>()!);
},
onEmpty: (root: NodeInterface) => {
// Vue can be rendered using createApp
return ReactDOM.render(<Empty />, root.get<HTMLElement>()!);
},
onClick: (
root: NodeInterface,
{key, name }: {key: string; name: string },
) => {
console.log('mention click:', key,'-', name);
},
onMouseEnter: (
layout: NodeInterface,
{name }: {key: string; name: string },
) => {
// Vue can be rendered using createApp
ReactDOM.render(
<div style={{ padding: 5 }}>
<p>This is name: {name}</p>
<p>Configure the onMouseEnter method of the mention plugin</p>
<p>Use ReactDOM.render to customize rendering here</p>
<p>Use ReactDOM.render to customize rendering here</p>
</div>,
layout.get<HTMLElement>()!,
);
},
},
// Font size configuration
[Fontsize.pluginName]: {
//Configure the font size to be filtered after pasting
filter: (fontSize: string) => {
return (
[
'12px',
'13px',
'14px',
'15px',
'16px',
'19px',
'22px',
'24px',
'29px',
'32px',
'40px',
'48px',
].indexOf(fontSize)> -1
);
},
},
// Font configuration
[Fontfamily.pluginName]: {
//Configure the font to be filtered after pasting
filter: (fontfamily: string) => {
const item = fontFamilyDefaultData.find((item) =>
fontfamily
.split(',')
.some(
(name) =>
item.value
.toLowerCase()
.indexOf(name.replace(/"/,'').toLowerCase())>
-1,
),
);
return item? item.value: false;
},
},
// Row height configuration
[LineHeight.pluginName]: {
//Configure the row height to be filtered after pasting
filter: (lineHeight: string) => {
if (lineHeight === '14px') return '1';
if (lineHeight === '16px') return '1.15';
if (lineHeight === '21px') return '1.5';
if (lineHeight === '28px') return '2';
if (lineHeight === '35px') return '2.5';
if (lineHeight === '42px') return '3';
// Remove if the conditions are not met
return (
['1', '1.15', '1.5', '2', '2.5', '3'].indexOf(lineHeight)> -1
);
},
},
```
### placeholder
- Type: `string`
- Default value: `None`
- Detailed: placeholder
### readonly
- Type: `boolean`
- Default value: `false`
- Detailed: Whether it is read-only or not, and cannot be edited after setting to read-only
The difference with `View` rendering is that you can still see the editor's edits after `readonly` is set to read-only status.
After rendering, `View` loses all editing capabilities and collaboration capabilities, `View` can render a `card` plug-in with interactive effects
`engine.getHtml()` can only get static `html` and cannot restore the interaction effect of `card` component, but it is very friendly to search engines
### scrollNode
- Type: `Node | (() => Node | null)`
- Default value: Find the node whose parent style `overflow` or `overflow-y` is `auto` or `scroll`, if not, take `document.body`
- Detailed: The editor scroll bar node is mainly used to monitor the `scroll` event to set the floating position of the bomb layer and actively set the scroll to the editor target position

234
docs/config/index.zh-CN.md Normal file
View File

@ -0,0 +1,234 @@
---
toc: menu
---
# 引擎配置
在实例化引擎时传入
```ts
//实例化引擎
const engine = new Engine(渲染节点, {
...配置项,
});
```
### lang
- 类型: `string`
- 默认值:`zh-CN`
- 详细:语言配置,暂时支持 `zh-CN`、`en-US`。可使用 `locale` 配置
```ts
const view = new View(渲染节点, {
lang: 'zh-CN',
});
```
### locale
- 类型: `object`
- 默认值:`zh-CN`
- 详细:配置额外语言包
语言包,默认语言包 [https://github.com/yanmao-cc/am-editor/blob/master/locale](https://github.com/yanmao-cc/am-editor/blob/master/locale)
```ts
const view = new View(渲染节点, {
locale: {
'zh-CN': {
test: '测试',
a: {
b: 'B',
},
},
},
});
console.log(view.language.get<string>('test'));
```
### className
- 类型: `string`
- 默认值:`null`
- 详细:添加编辑器渲染节点额外样式
### tabIndex
- 类型: `number`
- 默认值:`null`
- 详细:当前编辑器位于第几个 tab 项
### root
- 类型: `Node`
- 默认值:当前编辑器渲染节点父节点
- 详细:编辑器根节点
### plugins
- 类型: `Array<Plugin>`
- 默认值:`[]`
- 详细:实现 `Plugin` 抽象类的插件集合
### cards
- 类型: `Array<Card>`
- 默认值:`[]`
- 详细:实现 `Card` 抽象类的卡片集合
### config
- 类型: `{ [key: string]: PluginOptions }`
- 默认值:`{}`
- 详细每个插件的配置项key 为插件名称,详细配置请参考每个插件的说明。 [配置案例](https://github.com/yanmao-cc/am-editor/blob/master/examples/react/components/editor/config.tsx)
一些插件需要额外属性的配置:
```ts
// 配置斜体 markdown 语法
[Italic.pluginName]: {
// 默认为 _ 下划线,这里修改为单个 * 号
markdown: '*',
},
// 图片上传
[ImageUploader.pluginName]: {
file: {
action: `${DOMAIN}/upload/image`,
headers: { Authorization: 213434 },
},
remote: {
action: `${DOMAIN}/upload/image`,
},
isRemote: (src: string) => src.indexOf(DOMAIN) < 0,
},
// 文件上传
[FileUploader.pluginName]: {
action: `${DOMAIN}/upload/file`,
},
// 视频上传
[VideoUploader.pluginName]: {
action: `${DOMAIN}/upload/video`,
},
// 数学公式生成地址项目在https://drawing.yanmao.cc
[Math.pluginName]: {
action: `https://g.yanmao.cc/latex`,
parse: (res: any) => {
if (res.success) return { result: true, data: res.svg };
return { result: false };
},
},
// 提交插件配置
[Mention.pluginName]: {
action: `${DOMAIN}/user/search`,
onLoading: (root: NodeInterface) => {
// Vue 可以使用 createApp 渲染
return ReactDOM.render(<Loading />, root.get<HTMLElement>()!);
},
onEmpty: (root: NodeInterface) => {
// Vue 可以使用 createApp 渲染
return ReactDOM.render(<Empty />, root.get<HTMLElement>()!);
},
onClick: (
root: NodeInterface,
{ key, name }: { key: string; name: string },
) => {
console.log('mention click:', key, '-', name);
},
onMouseEnter: (
layout: NodeInterface,
{ name }: { key: string; name: string },
) => {
// Vue 可以使用 createApp 渲染
ReactDOM.render(
<div style={{ padding: 5 }}>
<p>This is name: {name}</p>
<p>配置 mention 插件的 onMouseEnter 方法</p>
<p>此处使用 ReactDOM.render 自定义渲染</p>
<p>Use ReactDOM.render to customize rendering here</p>
</div>,
layout.get<HTMLElement>()!,
);
},
},
// 字体大小配置
[Fontsize.pluginName]: {
//配置粘贴后需要过滤的字体大小
filter: (fontSize: string) => {
return (
[
'12px',
'13px',
'14px',
'15px',
'16px',
'19px',
'22px',
'24px',
'29px',
'32px',
'40px',
'48px',
].indexOf(fontSize) > -1
);
},
},
// 字体配置
[Fontfamily.pluginName]: {
//配置粘贴后需要过滤的字体
filter: (fontfamily: string) => {
const item = fontFamilyDefaultData.find((item) =>
fontfamily
.split(',')
.some(
(name) =>
item.value
.toLowerCase()
.indexOf(name.replace(/"/, '').toLowerCase()) >
-1,
),
);
return item ? item.value : false;
},
},
// 行高配置
[LineHeight.pluginName]: {
//配置粘贴后需要过滤的行高
filter: (lineHeight: string) => {
if (lineHeight === '14px') return '1';
if (lineHeight === '16px') return '1.15';
if (lineHeight === '21px') return '1.5';
if (lineHeight === '28px') return '2';
if (lineHeight === '35px') return '2.5';
if (lineHeight === '42px') return '3';
// 不满足条件就移除掉
return (
['1', '1.15', '1.5', '2', '2.5', '3'].indexOf(lineHeight) > -1
);
},
},
```
### placeholder
- 类型: `string`
- 默认值:`无`
- 详细:占位符
### readonly
- 类型: `boolean`
- 默认值:`false`
- 详细:是否只读,设置为只读后不可编辑
`View` 渲染不同的是,`readonly` 设置只读状态后依然可以看到协同者的编辑。
`View` 渲染后失去一切编辑能力和协同能力,`View` 能够渲染出具有交互效果的 `card` 插件
`engine.getHtml()` 只能获取到静态的 `html`,无法还原 `card` 组件的交互效果,但是它对搜索引擎很友好
### scrollNode
- 类型: `Node | (() => Node | null)`
- 默认值:查找父级样式 `overflow` 或者 `overflow-y``auto` 或者 `scroll` 的节点,如果没有就取 `document.body`
- 详细:编辑器滚动条节点,主要用于监听 `scroll` 事件设置弹层浮动位置和主动设置滚动到编辑器目标位置

74
docs/config/ot.md Normal file
View File

@ -0,0 +1,74 @@
---
toc: menu
---
# Collaborative editing configuration
The editor is based on the [sharedb](https://github.com/share/sharedb) and [json0](https://github.com/ottypes/json0) protocols to interactively manipulate data
The client (editor) establishes a long connection communication with the server through `WebSocket`, and every time the editor changes the dom structure, it will be converted to `json0` format operation command (ops) and sent to the server and modify the server data. Distribute to each client
## Client
In the demo case, an editor has provided a client code that uses the `json0` protocol to interact with `sharedb` through `WebSocket` and `sharedb` according to common needs.
[React](https://github.com/yanmao-cc/am-editor/blob/master/examples/react/components/editor/ot/client.ts)
[Vue](https://github.com/yanmao-cc/am-editor/blob/master/examples/vue/src/components/ot-client.ts)
```ts
//Instantiate the collaborative editing client, you need to pass in the current editor instance
const ot = new OTClient(engine);
// Engine.setValue is no longer needed here. Only need to pass the value to OTClient when connecting. After connecting to the server, if the server does not have the document, it will be created with the default value, otherwise the latest document data of the server will be returned
// Connect to the collaborative server, if the server does not have a document corresponding to the docId, it will be initialized with defaultValue
// url server ws link
// docId The unique identification id of the document
// defaultValue on the server side, if the document corresponding to docId does not exist, a new document will be created with this value
ot.connect(url, docId, defaultValue);
ot.on('ready', (member) => {
console.log('OT Ready');
});
```
This code has been able to meet the basic editing needs, if you need more functions, you can expand by yourself
## Server
`ot-server` is a network service created with `nodejs`, and `WebSocket.Server` is used to handle the client's `WebSocket` connection
[ot-server](https://github.com/yanmao-cc/am-editor/tree/master/ot-server)
In the demonstration case, only simulated user data is provided. In the production environment, we need the client to transmit the `token` parameter for identity verification
Use the command
```bash
# Development environment
yarn dev
# or
# Formal environment
yarn start
```
## Collaborative data
`sharedb` will save the operation data of each client and server as a log, and will keep the newly generated document data after each operation. These operations are performed in `ot-server`
`sharedb` provides two ways to save these data
- RAM
- Database
In the case that no database is provided by default, `sharedb` saves these data in memory by default, but these data cannot be persisted and will disappear after restart. It is not recommended to use in a production environment.
If you need to use the memory storage test, [ot-server -> client](https://github.com/yanmao-cc/am-editor/blob/master/ot-server/src/client.js) this in the file Delete the `{db: mongodb}` of the code `constructor(backend = new ShareDB({ db: mongodb }))`, and the corresponding reference and instantiation of `mongodb` need to be deleted
In the demonstration case, the `mongodb` database is used to save all data persistence, so we need to install the `mongodb` database
- You can install the database version that suits your environment from [MongoDB official website download](https://www.mongodb.com/try/download/community)
- After the installation is complete, create a database and set the user name, password and other permissions
- Finally, configure the database name, user name, password and other information to [config](https://github.com/yanmao-cc/am-editor/tree/master/ot-server/config) to start normally `ot -server`
[Linux Installation Tutorial](https://www.jianshu.com/p/62455ccaeefe)
[Windows installation tutorial](https://segmentfault.com/a/1190000039742854) Windows can download the msi version directly, the installation is relatively easy, the next step is the next step. . .

74
docs/config/ot.zh-CN.md Normal file
View File

@ -0,0 +1,74 @@
---
toc: menu
---
# 协同编辑配置
编辑器基于 [sharedb](https://github.com/share/sharedb) 与 [json0](https://github.com/ottypes/json0) 协议交互协作操作数据
客户端(编辑器)通过 `WebSocket` 与服务端建立长连接通信,编辑器每次的 dom 结构变更都将转换为`json0`格式操作命令ops发送到服务端并修改服务端数据后再分发给各个客户端
## 客户端
在演示案例中已经有根据常用需求提供了一份编辑器通过 `WebSocket``sharedb` 使用 `json0` 协议交互的客户端代码
[React](https://github.com/yanmao-cc/am-editor/blob/master/examples/react/components/editor/ot/client.ts)
[Vue](https://github.com/yanmao-cc/am-editor/blob/master/examples/vue/src/components/ot-client.ts)
```ts
//实例化协作编辑客户端,需要传入当前编辑器实例
const ot = new OTClient(engine);
// 这里不再需要使用 engine.setValue。只需要在连接的时候把 value 传给 OTClient。在连接到服务端后如果服务端没有该文档将以默认值创建否则就返回服务端的最新文档数据
// 连接协同服务端如果服务端没有对应docId的文档将使用 defaultValue 初始化
// url 服务端ws链接
// docId 文档的唯一识别id
// defaultValue 在服务端如果docId对应的文档不存在将会以这个值创建一个新文档
ot.connect(url, docId, defaultValue);
ot.on('ready', (member) => {
console.log('OT Ready');
});
```
这份代码已经能满足基本的编辑需求,如果需要更多功能可自行扩展
## 服务端
`ot-server` 是使用 `nodejs` 创建的一个网络服务,使用 `WebSocket.Server` 处理客户端的 `WebSocket` 连接
[ot-server](https://github.com/yanmao-cc/am-editor/tree/master/ot-server)
演示案例中仅提供了模拟的用户数据,在生产环境中我们需要客户端传输`token`参数进行身份效验
使用命令
```bash
# 开发环境
yarn dev
# or
# 正式环境
yarn start
```
## 协同数据
`sharedb` 会把每次客户端与服务端的操作数据保存为日志,并且在每次操作后都会把最新生成的文档数据保留下来。这些操作都在 `ot-server` 中进行
`sharedb` 提供了两种方式保存这些数据
- 内存
- 数据库
在默认不提供数据库的情况下,`sharedb` 默认把这些数据都保存在内存中,但是这些数据并不能持久化,重启后将会消失,在生产环境中并不建议使用。
如果需要使用内存存储测试,[ot-server -> client](https://github.com/yanmao-cc/am-editor/blob/master/ot-server/src/client.js) 文件中的这段代码 `constructor(backend = new ShareDB({ db: mongodb }))``{db: mongodb}` 删除即可,相应的也需要把 `mongodb` 上面的引用及实例化删除
演示案例中使用了 `mongodb` 数据库保存了所有的数据持久化,所以我们需要安装 `mongodb` 数据库
- 可以在 [MongoDB 官网下载](https://www.mongodb.com/try/download/community) 安装符合自己环境的数据库版本
- 安装完成后,创建一个数据库,并设置用户名、密码等权限
- 最后把数据库名称、用户名、密码等信息配置到[config](https://github.com/yanmao-cc/am-editor/tree/master/ot-server/config) 就可以正常启动 `ot-server`
[Linux 安装教程](https://www.jianshu.com/p/62455ccaeefe)
[Windows 安装教程](https://segmentfault.com/a/1190000039742854) Windows 可以直接下载 msi 版本的,安装比较容易,下一步下一步。。。

509
docs/config/toolbar.md Normal file
View File

@ -0,0 +1,509 @@
# Toolbar configuration
Introduce the toolbar
```ts
//vue3 please use @aomao/toolbar-vue
//vue2 please use am-editor-toolbar-vue2
import Toolbar, { ToolbarPlugin, ToolbarComponent } from '@aomao/toolbar';
```
-Toolbar Toolbar component
-ToolbarPlugin provides plugins to the engine
-ToolbarComponent provides the card component to the engine
Except for the `Toolbar` component, the latter two are shortcuts to realize the toolbar card plug-in option when you press `/` in the editor
## Types of
There are now four ways to display the toolbar
-`button` button -`downdrop` drop-down box -`color` color palette -`collapse` drop-down panel, the drop-down box that appears on the first button of the toolbar, and card-form components are basically placed here
## Attributes
The attributes that the Toolbar component needs to pass in:
-An instance of the `editor` editor, which can be used to automatically invoke the plug-in execution -`items` plugin display configuration list
## Configuration item
items is a two-dimensional array. We can put plugins of the same concept in a group for easy searching. After rendering, each group will be separated by a dividing line
```ts
items: [['collapse'], ['bold', 'italic']];
```
All the display forms of the existing plug-ins have been configured in the Toolbar component, and we can directly pass in the plug-in name to use these configurations. Of course, we can also pass in an object to cover part of the configuration
```ts
items: [
['collapse'],
[
{
name: 'bold',
icon: 'icon',
title: 'Prompt text',
},
'italic',
],
];
```
If the default configuration is found through the `name` attribute, the `type` attribute will not be overwritten. If the configured `name` is not part of the default configuration, it will be processed according to the custom button
## Collapse
Usually used to configure the card drop-down box
Need to specify `type` as `collapse`
### className
Custom style name
### icon
Optional
The button icon, which can be a React component, or a string of html in Vue
### content
Optional
Button display content, will be displayed together with icon
It can be a React component, or it can be a string of html in Vue. Or a method, and return React component or html string
### onSelect
List item selected event, return `false`, the default command of list item configuration will not be executed
```ts
onSelect?: (event: React.MouseEvent, name: string) => void | boolean;
```
### groups
Group display
The `groups` property can be set to classify cards for different purposes as needed
If `title` is not filled in, the grouping style will not appear
```ts
// Display group information
items: [
[
{
type: 'collapse',
groups: [
{
title: 'File',
items: ['image-uploader', 'file-uploader'],
},
],
},
],
];
// or do not display group information
items: [
[
{
type: 'collapse',
groups: [
{
items: ['bold', 'underline'],
},
],
},
],
];
```
### items
Configure `items` of `collapse`
The following cards have been configured by default
```ts
'image-uploader',
'codeblock',
'table',
'file-uploader',
'video-uploader',
'math',
'status',
```
We can specify `name` as the name of an existing card, and configure other options to override the default configuration.
Of course, we can also specify other names to complete custom `item`
```ts
items: [
[
{
type: 'collapse',
groups: [
{
items: [{ name: 'codeblock', content: 'I am CodeBlock' }],
},
],
},
],
];
```
The basic properties are the same as the `button` properties, which can be viewed in the following part of the article, here are the special properties relative to the `button`
#### search
To query characters, in the toolbar plug-in, we can use `/` to call up shortcut options in the editor, and search for related cards, so you can specify a combination of related keywords and characters here
#### description
List item description, can return a `React` component, or `Vue` can return `html` string
#### prompt
The content that needs to be rendered when the mouse is moved into the list item can return a `React` component, or `Vue` can return a `html` string
The effect is similar to the `table` card item. After the input is moved in, a table with selected columns and rows will be displayed
#### onClick
List item click event, return `false` will not execute the configured default command
```ts
onClick?: (event: React.MouseEvent, name: string) => void | boolean;
```
## Button
button configuration properties
Configure in the toolbar items, you need to specify the `type` as `button`
```ts
items:[
[
{
type:'button',
name:'test',
...
}
]
]
```
### name
Button name
If the button name is the same as the toolbar default configuration item name, then the default configuration will be overwritten, otherwise it will be used as a custom button
### icon
Optional
The button icon, which can be a React component, or a string of html in Vue
### content
Optional
Button display content, will be displayed together with icon
It can be a React component, or it can be a string of html in Vue. Or a method, and return React component or html string
### title
The prompt message displayed when the mouse moves into the button
### placement
Set the location of the prompt message
```ts
placement?:
|'right'
|'top'
|'left'
|'bottom'
|'topLeft'
|'topRight'
|'bottomLeft'
|'bottomRight'
|'leftTop'
|'leftBottom'
|'rightTop'
|'rightBottom';
```
### hotkey
Whether to display the hot key, or set the information of the hot key
The default is to display the hotkey to the prompt message (`title`), and use the `name` information to find the hotkey set by the plug-in
```ts
hotkey?: boolean | string;
```
### autoExecute
When the button is clicked, whether to automatically execute the plug-in command, it is enabled by default
### command
Plug-in command or parameter
If this parameter is configured and the `autoExecute` property is enabled, when the button is clicked, this configuration is called to execute the plug-in command
If `name` is configured, execute the plugin corresponding to `name`, otherwise execute the plugin corresponding to `name` specified by `button`
If there is a configuration of `args` or `command` as a pure array, it will be passed as a parameter to the command to execute the plugin
```ts
command?: {name: string; args: Array<any>} | Array<any>;
```
### className
Configure the style name for the button
### onClick
Mouse click event
If it returns false, the plugin command will not be executed automatically
```ts
onClick?: (event: React.MouseEvent) => void | boolean;
```
### onMouseDown
Mouse button press event
```ts
onMouseDown?: (event: React.MouseEvent) => void;
```
### onMouseEnter
Mouse in button event
```ts
onMouseEnter?: (event: React.MouseEvent) => void;
```
### onMouseLeave
Mouse off button event
```ts
onMouseLeave?: (event: React.MouseEvent) => void;
```
### onActive
The custom button is activated and selected, and the plug-in `engine.command.queryState` method is called by default
```ts
onActive?: () => boolean;
```
### onDisabled
The custom button is disabled, and the plugin `engine.command.queryEnabled` is called by default
```ts
onDisabled?: () => boolean;
```
## Dropdown
dropdown configuration properties
Configure in the toolbar items, you need to specify `type` as `dropdown`
```ts
items:[
[
{
type:'dropdown',
name:'test',
items: [
{
key:'item1',
content:'item1'
}
]
...
}
]
]
```
### items
Drop-down list items, similar to buttons
```ts
items:[{
key: string;
icon?: React.ReactNode;
content?: React.ReactNode | (() => React.ReactNode);
hotkey?: boolean | string;
isDefault?: boolean;
title?: string;
placement?:
|'right'
|'top'
|'left'
|'bottom'
|'topLeft'
|'topRight'
|'bottomLeft'
|'bottomRight'
|'leftTop'
|'leftBottom'
|'rightTop'
|'rightBottom';
className?: string;
disabled?: boolean;
command?: {name: string; args: Array<any>} | Array<any>;
autoExecute?: boolean;
}]
```
### name
Drop-down list name
If the name is the same as the toolbar default configuration item name, then the default existing configuration will be overwritten, otherwise it will be used as a custom drop-down list
### icon
Optional
The button icon, which can be a React component, or a string of html in Vue
### content
Optional
Button display content, will be displayed together with icon
It can be a React component, or it can be a string of html in Vue. Or a method, and return React component or html string
### title
The prompt message displayed when the mouse moves into the button
### values
The selected value in the drop-down list is obtained by `engine.command.queryState` by default. If there is a configuration of `onActive`, the value will be obtained from the custom `onActive`
```ts
values?: string | Array<string>;
```
### single
Single selection or multiple selection
```ts
single?: boolean;
```
### className
Drop-down list style
### direction
Arrangement direction `vertical` | `horizontal`
```ts
direction?:'vertical' |'horizontal';
```
### onSelect
List item selection event, return `false` will not automatically execute the command configured for the selected item
```ts
onSelect?: (event: React.MouseEvent, key: string) => void | boolean;
```
### hasArrow
Whether to show the drop-down arrow
```ts
hasArrow?: boolean;
```
### hasDot
Whether to display the check effect after the selected value
```ts
hasDot?: boolean;
```
### renderContent
Custom render the content displayed after the drop-down list is selected, the default is the `icon` or `content` configured by the drop-down list
Can return React components or Vue can return html strings
```ts
renderContent?: (item: DropdownListItem) => React.ReactNode;
```
### onActive
The custom button is activated and selected, and the plug-in `engine.command.queryState` method is called by default
```ts
onActive?: () => boolean;
```
### onDisabled
The custom button is disabled, and the plugin `engine.command.queryEnabled` is called by default
```ts
onDisabled?: () => boolean;
```
## Default configuration of all plugins
```ts
[
['collapse'],
['undo', 'redo', 'paintformat', 'removeformat'],
['heading', 'fontfamily', 'fontsize'],
['bold', 'italic', 'strikethrough', 'underline', 'moremark'],
['fontcolor', 'backcolor'],
['alignment'],
['unorderedlist', 'orderedlist', 'tasklist', 'indent', 'line-height'],
['link', 'quote', 'hr'],
];
```
These default configuration details can be found here:
React: [https://github.com/yanmao-cc/am-editor/blob/master/packages/toolbar/src/config/toolbar/index.tsx](https://github.com/yanmao-cc/am-editor/blob/master/packages/toolbar/src/config/toolbar/index.tsx)
Vue3: [https://github.com/yanmao-cc/am-editor/blob/master/packages/toolbar-vue/src/config/index.ts](https://github.com/yanmao-cc/am-editor/blob/master/packages/toolbar-vue/src/config/index.ts)
Vue2: [https://github.com/zb201307/am-editor-vue2/blob/main/packages/toolbar/src/config/index.ts](https://github.com/zb201307/am-editor-vue2/blob/main/packages/toolbar/src/config/index.ts)

View File

@ -0,0 +1,513 @@
# 工具栏配置
引入工具栏
```ts
//vue3 请使用 @aomao/toolbar-vue
//vue2 请使用 am-editor-toolbar-vue2
import Toolbar, { ToolbarPlugin, ToolbarComponent } from '@aomao/toolbar';
```
- Toolbar 工具栏组件
- ToolbarPlugin 提供给引擎的插件
- ToolbarComponent 提供给引擎的卡片组件
除了 `Toolbar` 组件,后两者都是实现在编辑器按下 `/` 出现工具栏卡片插件选项的快捷方式
## 类型
工具栏现在有四种展现方式
- `button` 按钮
- `downdrop` 下拉框
- `color` 颜色板
- `collapse` 下拉面板,工具栏的第一个按钮出现的下拉框,卡片形式的组件基本上都放在这里
## 属性
Toolbar 组件需要传入的属性:
- `editor` 编辑器实例,可以用于自动调用插件执行
- `items` 插件展示配置列表
## 配置项
items 是一个二维数组,我们可以把相同概念的插件放在一个组里面,便于寻找。渲染出来后,每个组都会有分割线分开
```ts
items: [['collapse'], ['bold', 'italic']];
```
在 Toolbar 组件里面已经配置好了现有插件的所有展现形式,我们可以直接传入插件名称使用这些配置。当然,我们也可以传入一个对象覆盖部分配置
```ts
items: [
['collapse'],
[
{
name: 'bold',
icon: '图标',
title: '提示文字',
},
'italic',
],
];
```
如果通过 `name` 属性找到了默认配置,那么 `type` 属性是不会被覆盖的。如果配置的`name`不属于默认配置的一部分,就按照自定义按钮处理
## Collapse
通常用于配置卡片下拉框
需要指定 `type``collapse`
### className
自定义样式名称
### icon
可选
按钮图标,可以是 React 组件,在 Vue 中也可以是一段字符串的 html
### content
可选
按钮显示内容,会与 icon 一起显示
可以是 React 组件,在 Vue 中也可以是一段字符串的 html。或者是一个方法并且返回 React 组件或者 html 字符串
### onSelect
列表项选中事件,返回 `false` 不会执行列表项配置的默认命令
```ts
onSelect?: (event: React.MouseEvent, name: string) => void | boolean;
```
### groups
分组显示
通过 `groups` 属性可以设置按需要把不同用途的卡片分类
不填写 `title` 将不会出现分组样式
```ts
// 显示分组信息
items: [
[
{
type: 'collapse',
groups: [
{
title: '文件',
items: ['image-uploader', 'file-uploader'],
},
],
},
],
];
// or 不显示分组信息
items: [
[
{
type: 'collapse',
groups: [
{
items: ['bold', 'underline'],
},
],
},
],
];
```
### items
配置 `collapse``items`
默认情况下已经配置了以下卡片
```ts
'image-uploader',
'codeblock',
'table',
'file-uploader',
'video-uploader',
'math',
'status',
```
我们可以指定 `name` 为已存在的卡片名称,并且配置其它选项覆盖默认配置。
当然我们也可以指定其它名称,完成自定义`item`
```ts
items: [
[
{
type: 'collapse',
groups: [
{
items: [{ name: 'codeblock', content: '我是CodeBlock' }],
},
],
},
],
];
```
基本属性与 `button` 属性一样,可以在文章以下部分查看,这里列出了相对于 `button` 外的特俗属性
#### search
查询字符,在工具栏插件中我们可以使用 `/` 在编辑器唤出快捷选项,并且可以搜索相关卡片,所以这里可以指定相关关键字字符组合
#### description
列表项描述,可以返回一个 `React` 组件,或者 `Vue` 可以返回 `html` 字符串
#### prompt
鼠标移入到列表项时需要渲染的内容,可以返回一个 `React` 组件,或者 `Vue` 可以返回 `html` 字符串
效果类似于 `table` 卡片项,输入移入后展示一个选择列和行数的表格
#### onClick
列表项单击事件,返回 `false` 将不会执行配置的默认命令
```ts
onClick?: (event: React.MouseEvent, name: string) => void | boolean;
```
## Button
button 配置属性
在工具栏 items 里面配置,需要指定 `type``button`
```ts
items:[
[
{
type: 'button',
name: 'test',
...
}
]
]
```
### name
按钮名称
如果按钮名称与工具栏默认配置项名称相同,那么会覆盖默认已有配置,否则将作为自定义按钮
### icon
可选
按钮图标,可以是 React 组件,在 Vue 中也可以是一段字符串的 html
### content
可选
按钮显示内容,会与 icon 一起显示
可以是 React 组件,在 Vue 中也可以是一段字符串的 html。或者是一个方法并且返回 React 组件或者 html 字符串
### title
鼠标移入按钮时显示的提示信息
### placement
设置提示信息的位置
```ts
placement?:
| 'right'
| 'top'
| 'left'
| 'bottom'
| 'topLeft'
| 'topRight'
| 'bottomLeft'
| 'bottomRight'
| 'leftTop'
| 'leftBottom'
| 'rightTop'
| 'rightBottom';
```
### hotkey
是否显示热键,或者设置热键的信息
默认为显示热键到提示信息(`title`),并且通过 `name` 信息找到插件设置的热键
```ts
hotkey?: boolean | string;
```
### autoExecute
按钮单击时,是否自动执行插件命令,默认启用
### command
插件命令或参数
如果有配置此参数,并且 `autoExecute` 属性为启用状态,在按钮单击时,调用此配置执行插件命令
如果有配置 `name` 就执行`name` 对应的插件,否则就执行 `button` 指定的 `name` 对应的插件
如果有配置 `args` 或者 `command` 为纯数组,会作为参数传入执行插件的命令
```ts
command?: { name: string; args: Array<any> } | Array<any>;
```
### className
为按钮配置样式名称
### onClick
鼠标单击事件
如果返回 `false` 将不会自动执行插件命令
```ts
onClick?: (event: React.MouseEvent) => void | boolean;
```
### onMouseDown
鼠标按下按钮事件
```ts
onMouseDown?: (event: React.MouseEvent) => void;
```
### onMouseEnter
鼠标移入按钮事件
```ts
onMouseEnter?: (event: React.MouseEvent) => void;
```
### onMouseLeave
鼠标移开按钮事件
```ts
onMouseLeave?: (event: React.MouseEvent) => void;
```
### onActive
自定义按钮激活选中,默认调用插件 `engine.command.queryState` 方法
```ts
onActive?: () => boolean;
```
### onDisabled
自定义按钮禁用,默认调用插件 `engine.command.queryEnabled`
```ts
onDisabled?: () => boolean;
```
## Dropdown
dropdown 配置属性
在工具栏 items 里面配置,需要指定 `type``dropdown`
```ts
items:[
[
{
type: 'dropdown',
name: 'test',
items: [
{
key: 'item1',
content: 'item1'
}
]
...
}
]
]
```
### items
下拉列表项,与按钮类似
```ts
items:[{
key: string;
icon?: React.ReactNode;
content?: React.ReactNode | (() => React.ReactNode);
hotkey?: boolean | string;
isDefault?: boolean;
title?: string;
placement?:
| 'right'
| 'top'
| 'left'
| 'bottom'
| 'topLeft'
| 'topRight'
| 'bottomLeft'
| 'bottomRight'
| 'leftTop'
| 'leftBottom'
| 'rightTop'
| 'rightBottom';
className?: string;
disabled?: boolean;
command?: { name: string; args: Array<any> } | Array<any>;
autoExecute?: boolean;
}]
```
### name
下拉列表名称
如果名称与工具栏默认配置项名称相同,那么会覆盖默认已有配置,否则将作为自定义下拉列表
### icon
可选
按钮图标,可以是 React 组件,在 Vue 中也可以是一段字符串的 html
### content
可选
按钮显示内容,会与 icon 一起显示
可以是 React 组件,在 Vue 中也可以是一段字符串的 html。或者是一个方法并且返回 React 组件或者 html 字符串
### title
鼠标移入按钮时显示的提示信息
### values
下拉列表选中值,默认通过 `engine.command.queryState` 获取,如果有配置 `onActive` 将会从自定义 `onActive` 中获取值
```ts
values?: string | Array<string>;
```
### single
单选还是可以多选
```ts
single?: boolean;
```
### className
下拉列表样式
### direction
排列方向 `vertical` | `horizontal`
```ts
direction?: 'vertical' | 'horizontal';
```
### onSelect
列表项选中事件,返回 `false` 将不自动执行选中项配置的命令
```ts
onSelect?: (event: React.MouseEvent, key: string) => void | boolean;
```
### hasArrow
是否显示下拉箭头
```ts
hasArrow?: boolean;
```
### hasDot
是否显示选中值后的勾选效果
```ts
hasDot?: boolean;
```
### renderContent
自定义渲染下拉列表选中后显示的内容,默认为下拉列表配置的 `icon` 或者 `content`
可以返回 React 组件或者 Vue 可以返回 html 字符串
```ts
renderContent?: (item: DropdownListItem) => React.ReactNode;
```
### onActive
自定义按钮激活选中,默认调用插件 `engine.command.queryState` 方法
```ts
onActive?: () => boolean;
```
### onDisabled
自定义按钮禁用,默认调用插件 `engine.command.queryEnabled`
```ts
onDisabled?: () => boolean;
```
## 所有插件的默认配置
```ts
[
['collapse'],
['undo', 'redo', 'paintformat', 'removeformat'],
['heading', 'fontfamily', 'fontsize'],
['bold', 'italic', 'strikethrough', 'underline', 'moremark'],
['fontcolor', 'backcolor'],
['alignment'],
['unorderedlist', 'orderedlist', 'tasklist', 'indent', 'line-height'],
['link', 'quote', 'hr'],
];
```
这些默认配置详细信息可以在这里找到定义:
React: [https://github.com/yanmao-cc/am-editor/blob/master/packages/toolbar/src/config/toolbar/index.tsx](https://github.com/yanmao-cc/am-editor/blob/master/packages/toolbar/src/config/toolbar/index.tsx)
Vue3: [https://github.com/yanmao-cc/am-editor/blob/master/packages/toolbar-vue/src/config/index.ts](https://github.com/yanmao-cc/am-editor/blob/master/packages/toolbar-vue/src/config/index.ts)
Vue2: [https://github.com/zb201307/am-editor-vue2/blob/main/packages/toolbar/src/config/index.ts](https://github.com/zb201307/am-editor-vue2/blob/main/packages/toolbar/src/config/index.ts)

428
docs/config/upload.md Normal file
View File

@ -0,0 +1,428 @@
---
toc: menu
---
# Upload configuration
The editor implements upload logic by default
We can access it in `request.upload` in the engine instance
`request.upload` internally uses `XMLHttpRequest` to upload files, the advantage is that you can get the upload progress
```ts
engine.request.upload(options: UploaderOptions, files: Array<File>, name?: string)
// Upload optional type
export type UploaderOptions = {
// Upload address
url: string;
// Request type, default json
type?: string;
// content type
contentType?: string;
// additional data
data?: {};
// cross domain
crossOrigin?: boolean;
// request header
headers?: {[key: string]: string };
// Before uploading, you can judge the file size limit
onBefore?: (file: File) => Promise<boolean | void>;
// Start upload
onReady?: (fileInfo: FileInfo, file: File) => Promise<void>;
// uploading
onUploading?: (file: File, progress: {percent: number }) => void;
// upload error
onError?: (error: Error, file: File) => void;
// Upload successfully
onSuccess?: (response: any, file: File) => void;
};
// FileInfo type
export type FileInfo = {
uid: string;
src: string | ArrayBuffer | null;
name: string;
size: number;
type: string;
ext: string;
};
```
In addition to upload, there is a utility method called `getFiles(options?: OpenDialogOptions)` that can pop up a local file selector
```ts
export type OpenDialogOptions = {
event?: MouseEvent;
accept?: string;
multiple?: boolean | number;
};
```
The following plugins all rely on `engine.request.upload` to achieve upload
We only need to follow the instructions of the corresponding plug-in and simply configure it to upload.
- ImageUploader
- FileUploader
- VideoUploader
## Custom upload
### Single plugin upload
Take ImageUploader as an example
```ts
import {
getExtensionName,
FileInfo,
File,
isAndroid,
isEngine,
} from '@aomao/engine';
import { ImageComponent, ImageUploader } from '@aomao/plugin-image';
import { ImageValue } from 'plugins/image/dist/component';
// Inherit the original ImageUploader class and override the execute method
class CustomizeImageUploader extends ImageUploader {
// The card instance currently being uploaded
private imageComponents: Record<string, ImageComponent> = {};
// Process the picture before uploading, and the base64 of the obtained picture will be displayed in the editor while the upload is waiting
handleBefore(uid: string, file: File) {
const { type, name, size } = file;
// Get the file extension
const ext = getExtensionName(file);
// read files asynchronously
return new Promise<false | { file: File; info: FileInfo }>(
(resolve, reject) => {
const fileReader = new FileReader();
fileReader.addEventListener(
'load',
() => {
resolve({
file,
info: {
// unique number
uid,
// Blob
src: fileReader.result,
// file name
name,
// File size
size,
// file type
type,
// File suffix
ext,
},
});
},
false,
);
fileReader.addEventListener('error', () => {
reject(false);
});
fileReader.readAsDataURL(file);
},
);
}
// Insert the editor before uploading
onReady(fileInfo: FileInfo) {
// If the ImageComponent instance of the current picture exists, it will not be processed
if (!isEngine(this.editor) || !!this.imageComponents[fileInfo.uid])
return;
// Insert ImageComponent card
const component = this.editor.card.insert(ImageComponent.cardName, {
// Set the status to uploading
status: 'uploading',
// Display the base64 image obtained in handleBefore, so as not to cause the editor area to be blank
src: fileInfo.src,
}) as ImageComponent;
// Record the card instance of the currently uploaded file
this.imageComponents[fileInfo.uid] = component;
}
// uploading
onUploading(uid: string, { percent }: { percent: number }) {
// Get the ImageComponent instance corresponding to file
const component = this.imageComponents[uid];
if (!component) return;
// Set the current upload progress percentage
component.setProgressPercent(percent);
}
// Upload successfully
onSuccess(response: any, uid: string) {
// Get the ImageComponent instance corresponding to file
const component = this.imageComponents[uid];
if (!component) return;
// Get the image address after the upload is successful
let src = '';
// Process the response returned by the server, and update the status value of the ImageComponent instance corresponding to the file if there is an error in the upload
if (!response.result) {
// Update the value of the card
this.editor.card.update(component.id, {
status: 'error',
message:
response.message ||
this.editor.language.get('image', 'uploadError'),
});
} else {
// Upload successfully
src = response.data;
}
// Set the status value of the ImageComponent instance corresponding to file to done
const value: ImageValue = {
status: 'done',
src,
};
// There is a url after the uploaded image is obtained
if (src) {
// Call the method of the current instance of ImageUploader to load the url image. If the loading fails, set the status to error and display that it cannot be loaded, otherwise the image will be loaded normally
this.loadImage(component.id, value);
}
// Delete the current temporary record
delete this.imageComponents[uid];
}
// upload error
onError(error: Error, uid: string) {
const component = this.imageComponents[uid];
if (!component) return;
// Update the card status to error and display the error message
this.editor.card.update(component.id, {
status: 'error',
message:
error.message ||
this.editor.language.get('image', 'uploadError'),
});
// Delete the current temporary record
delete this.imageComponents[uid];
}
async execute(files?: Array<File> | string | MouseEvent) {
// It is the reader View that will not handle it
if (!isEngine(this.editor)) return;
// Get the currently passed in optional value
const { request, language } = this.editor;
const { multiple } = this.options.file;
// Upload size limit
const limitSize = this.options.file.limitSize || 5 * 1024 * 1024;
// The incoming files is not an array to get a picture address, that is, MouseEvent pops up the file selector
if (!Array.isArray(files) && typeof files !== 'string') {
// A file selector pops up, allowing the user to select a file
files = await request.getFiles({
// Click event of user target
event: files,
// Selectable file suffix name. this.extensionNames is the combined value of the suffixes supported by default in the ImageUploader plugin and the suffixes passed in by the options
accept: isAndroid
? 'image/*'
: this.extensionNames.length > 0
? '.' + this.extensionNames.join(',.')
: '',
// The maximum number can be selected
multiple,
});
}
// If the file address is passed in, then upload the image address. If insertRemote judges that it is a third-party website image address, it will request the api to download from the server, and then the server will store it before returning the new image address.
// Because the pictures of third-party websites that are not on this site may be cross-domain or inaccessible, it is recommended to perform back-end download processing
else if (typeof files === 'string') {
this.insertRemote(files);
return;
}
// don't process if there is no file
if (files.length === 0) return;
const promiseList = [];
for (let f = 0; f < files.length; f++) {
const file = files[f];
// The unique identifier of the currently uploaded file
const uid = Date.now() + '-' + f;
// Determine the file size
if (file.size > limitSize) {
// Display error
this.editor.messageError(
language
.get<string>('image', 'uploadLimitError')
.replace(
'$size',
(limitSize / 1024 / 1024).toFixed(0) + 'M',
),
);
return;
}
promiseList.push(this.handleBefore(uid, file));
}
//After all the pictures are read, insert the editor
Promise.all(promiseList).then((values) => {
if (values.some((value) => value === false)) {
this.editor.messageError('read image failed');
return;
}
const files = values as { file: File; info: FileInfo }[];
files.forEach((v) => {
// insert editor
this.onReady(v.info);
});
// Process upload
this.handleUpload(files);
});
}
/**
* Process file upload
* @param values
*/
handleUpload(values: { file: File; info: FileInfo }[]) {
const files = values.map((v) => {
v.file.uid = v.info.uid;
return v.file;
});
// Custom upload method
this.editor.request.upload(
{
url: this.options.file.action,
onUploading: (file, percent) => {
this.onUploading(file.uid || '', percent);
},
onSuccess: (response, file) => {
this.onSuccess(response, file.uid || '');
},
onError: (error, file) => {
this.onError(error, file.uid || '');
},
},
files,
);
}
}
export default CustomizeImageUploader;
```
### Global upload
Override the editor `engine.request.upload` method
```ts
import Engine, {
EngineInterface,
FileInfo,
File,
getExtensionName,
UploaderOptions,
} from '@aomao/engine';
export default class {
// Process the picture before uploading, and the blob of the file will be displayed in the editor while waiting for upload
handleBefore(uid: string, file: File) {
const { type, name, size } = file;
// Get the file extension
const ext = getExtensionName(file);
// read files asynchronously
return new Promise<false | { file: File; info: FileInfo }>(
(resolve, reject) => {
const fileReader = new FileReader();
fileReader.addEventListener(
'load',
() => {
resolve({
file,
info: {
// unique number
uid,
// Blob format
src: fileReader.result,
// file name
name,
// File size
size,
// file type
type,
// File suffix
ext,
},
});
},
false,
);
fileReader.addEventListener('error', () => {
reject(false);
});
fileReader.readAsDataURL(file);
},
);
}
setGlobalUpload(engine: EngineInterface = new Engine('.container')) {
// Override the upload method in the editor
engine.request.upload = async (options, files, name) => {
const { onBefore, onReady } = options;
// do not process if there is no file
if (files.length === 0) return;
const promiseList = [];
for (let f = 0; f < files.length; f++) {
const file = files[f];
// The unique identifier of the currently uploaded file
const uid = Date.now() + '-' + f;
file.uid = uid;
if (onBefore && (await onBefore(file)) === false) return;
promiseList.push(this.handleBefore(uid, file));
}
//Insert the editor after reading all files
Promise.all(promiseList).then(async (values) => {
if (values.some((value) => value === false)) {
engine.messageError('read image failed');
return;
}
const files = values as { file: File; info: FileInfo }[];
Promise.all([
...files.map(async (v) => {
return new Promise(async (resolve) => {
if (onReady) {
await onReady(v.info, v.file);
}
resolve(true);
});
}),
]).then(() => {
files.forEach(async (file) => {
// Process upload
this.handleUpload(file.file, options, name);
});
});
});
};
}
/**
* Process upload
* @param url upload address
* @param name formData parameter name
* @param file file
*/
handleUpload(file: File, options: UploaderOptions, name: string = 'file') {
// form data
const formData = new FormData();
formData.append(name, file, file.name);
if (file.data) {
Object.keys(file.data).forEach((key) => {
formData.append(key, file.data![key]);
});
}
const {
// Upload address
url,
// additional data
data,
// Progress callback during upload
onUploading,
// Upload successful callback
onSuccess,
// Upload error callback
onError,
} = options;
if (data) {
Object.keys(data).forEach((key) => {
formData.append(key, data![key]);
});
}
// Custom upload and call onUploading onSuccess onError callback method
}
}
```

429
docs/config/upload.zh-CN.md Normal file
View File

@ -0,0 +1,429 @@
---
toc: menu
---
# 上传配置
编辑器默认实现了上传逻辑
我们可以在引擎实例中的 `request.upload` 访问到
`request.upload` 内部实用 `XMLHttpRequest` 上传文件,好处是可以获取到上传进度
```ts
engine.request.upload(options: UploaderOptions, files: Array<File>, name?: string)
// 上传可选项类型
export type UploaderOptions = {
// 上传地址
url: string;
// 请求类型,默认 json
type?: string;
// 内容类型
contentType?: string;
// 额外数据
data?: {};
// 跨域
crossOrigin?: boolean;
// 请求头
headers?: { [key: string]: string };
// 上传前,可以做文件大小限制判断
onBefore?: (file: File) => Promise<boolean | void>;
// 开始上传
onReady?: (fileInfo: FileInfo, file: File) => Promise<void>;
// 上传中
onUploading?: (file: File, progress: { percent: number }) => void;
// 上传错误
onError?: (error: Error, file: File) => void;
// 上传成功
onSuccess?: (response: any, file: File) => void;
};
// FileInfo 类型
export type FileInfo = {
uid: string;
src: string | ArrayBuffer | null;
name: string;
size: number;
type: string;
ext: string;
};
```
除了 upload 外,还有 `getFiles(options?: OpenDialogOptions)` 实用方法,可以弹出本地文件选择器
```ts
export type OpenDialogOptions = {
event?: MouseEvent;
accept?: string;
multiple?: boolean | number;
};
```
下面的插件都是依赖 `engine.request.upload` 实现上传的
我们只需要按照对应插件的说明简单配置后就可以实现上传
- ImageUploader
- FileUploader
- VideoUploader
## 自定义上传
### 单个插件上传
以 ImageUploader 为例
```ts
import {
getExtensionName,
FileInfo,
File,
isAndroid,
isEngine,
} from '@aomao/engine';
import { ImageComponent, ImageUploader } from '@aomao/plugin-image';
import { ImageValue } from 'plugins/image/dist/component';
// 继承原 ImageUploader 类,重写 execute 方法
class CustomizeImageUploader extends ImageUploader {
// 当前上传中的卡片实例
private imageComponents: Record<string, ImageComponent> = {};
// 上传前处理图片获取图片的base64在上传等待中显示在编辑器中
handleBefore(uid: string, file: File) {
const { type, name, size } = file;
// 获取文件后缀名
const ext = getExtensionName(file);
// 异步读取文件
return new Promise<false | { file: File; info: FileInfo }>(
(resolve, reject) => {
const fileReader = new FileReader();
fileReader.addEventListener(
'load',
() => {
resolve({
file,
info: {
// 唯一编号
uid,
// Blob
src: fileReader.result,
// 文件名称
name,
// 文件大小
size,
// 文件类型
type,
// 文件后缀名
ext,
},
});
},
false,
);
fileReader.addEventListener('error', () => {
reject(false);
});
fileReader.readAsDataURL(file);
},
);
}
// 上传前插入编辑器
onReady(fileInfo: FileInfo) {
// 如果当前图片的 ImageComponent 实例存在就不处理
if (!isEngine(this.editor) || !!this.imageComponents[fileInfo.uid])
return;
// 插入ImageComponent 卡片
const component = this.editor.card.insert(ImageComponent.cardName, {
// 设置状态为上传中
status: 'uploading',
// 显示在 handleBefore 中获取的 base64 图片,这样不会导致编辑器区域空白
src: fileInfo.src,
}) as ImageComponent;
// 记录当前上传文件的 卡片实例
this.imageComponents[fileInfo.uid] = component;
}
// 上传中
onUploading(uid: string, { percent }: { percent: number }) {
// 获取file 对应的 ImageComponent 实例
const component = this.imageComponents[uid];
if (!component) return;
// 设置当前上传进度百分比
component.setProgressPercent(percent);
}
// 上传成功
onSuccess(response: any, uid: string) {
// 获取file 对应的 ImageComponent 实例
const component = this.imageComponents[uid];
if (!component) return;
// 获取上传成功后的图片地址
let src = '';
// 处理服务端返回的 response如果上传出错就更新对应 file 对应的 ImageComponent 实例的状态值
if (!response.result) {
// 更新卡片的值
this.editor.card.update(component.id, {
status: 'error',
message:
response.message ||
this.editor.language.get('image', 'uploadError'),
});
} else {
// 上传成功
src = response.data;
}
// 设置为file 对应的 ImageComponent 实例的状态值为 done
const value: ImageValue = {
status: 'done',
src,
};
// 有获取的上传图片后的url
if (src) {
// 调用 ImageUploader 当前实例的方法去加载这个 url 图片如果加载失败就设置状态为error并显示无法加载否则就正常加载图片
this.loadImage(component.id, value);
}
// 删除当前的临时记录
delete this.imageComponents[uid];
}
// 上传出错
onError(error: Error, uid: string) {
const component = this.imageComponents[uid];
if (!component) return;
// 更新卡片状态为 error并显示错误信息
this.editor.card.update(component.id, {
status: 'error',
message:
error.message ||
this.editor.language.get('image', 'uploadError'),
});
// 删除当前的临时记录
delete this.imageComponents[uid];
}
async execute(files?: Array<File> | string | MouseEvent) {
// 是阅读器View就不处理
if (!isEngine(this.editor)) return;
// 获取当前传入的可选项值
const { request, language } = this.editor;
const { multiple } = this.options.file;
// 上传大小限制
const limitSize = this.options.file.limitSize || 5 * 1024 * 1024;
// 传入的files不是数组获取不是图片地址那就是 MouseEvent 弹出文件选择器
if (!Array.isArray(files) && typeof files !== 'string') {
// 弹出文件选择器,让用户选择文件
files = await request.getFiles({
// 用户目标的单击事件
event: files,
// 可选取的文件后缀名称。this.extensionNames 是 ImageUploader 插件内默认支持的后缀和可选项传进来的后缀合并后的值
accept: isAndroid
? 'image/*'
: this.extensionNames.length > 0
? '.' + this.extensionNames.join(',.')
: '',
// 最多可选取数量
multiple,
});
}
// 如果传入的文件地址那就执行图片地址的上传insertRemote 如果判断是非本站第三方网站图片地址就会请求api到服务端下载然后服务端存储后再返回新的图片地址
// 因为非本站第三方网站的图片可能存在跨域或者无法访问的情况,建议进行后端下载处理
else if (typeof files === 'string') {
this.insertRemote(files);
return;
}
// 如果没有任何文件就不处理
if (files.length === 0) return;
const promiseList = [];
for (let f = 0; f < files.length; f++) {
const file = files[f];
// 当前上传文件唯一标识
const uid = Date.now() + '-' + f;
// 判断文件大小
if (file.size > limitSize) {
// 显示错误
this.editor.messageError(
language
.get<string>('image', 'uploadLimitError')
.replace(
'$size',
(limitSize / 1024 / 1024).toFixed(0) + 'M',
),
);
return;
}
promiseList.push(this.handleBefore(uid, file));
}
//全部图片读取完成后再插入编辑器
Promise.all(promiseList).then((values) => {
if (values.some((value) => value === false)) {
this.editor.messageError('read image failed');
return;
}
const files = values as { file: File; info: FileInfo }[];
files.forEach((v) => {
// 插入编辑器
this.onReady(v.info);
});
// 处理上传
this.handleUpload(files);
});
}
/**
* 处理文件上传
* @param values
*/
handleUpload(values: { file: File; info: FileInfo }[]) {
const files = values.map((v) => {
v.file.uid = v.info.uid;
return v.file;
});
// 自定义上传方法
this.editor.request.upload(
{
url: this.options.file.action,
onUploading: (file, percent) => {
this.onUploading(file.uid || '', percent);
},
onSuccess: (response, file) => {
this.onSuccess(response, file.uid || '');
},
onError: (error, file) => {
this.onError(error, file.uid || '');
},
},
files,
);
}
}
export default CustomizeImageUploader;
```
### 全局上传
重写编辑器 `engine.request.upload` 方法
```ts
import Engine, {
EngineInterface,
FileInfo,
File,
getExtensionName,
UploaderOptions,
} from '@aomao/engine';
export default class {
// 上传前处理图片获取文件的Blob在上传等待中显示在编辑器中
handleBefore(uid: string, file: File) {
const { type, name, size } = file;
// 获取文件后缀名
const ext = getExtensionName(file);
// 异步读取文件
return new Promise<false | { file: File; info: FileInfo }>(
(resolve, reject) => {
const fileReader = new FileReader();
fileReader.addEventListener(
'load',
() => {
resolve({
file,
info: {
// 唯一编号
uid,
// Blob格式
src: fileReader.result,
// 文件名称
name,
// 文件大小
size,
// 文件类型
type,
// 文件后缀名
ext,
},
});
},
false,
);
fileReader.addEventListener('error', () => {
reject(false);
});
fileReader.readAsDataURL(file);
},
);
}
setGlobalUpload(engine: EngineInterface = new Engine('.container')) {
// 重写编辑器中 upload 方法
engine.request.upload = async (options, files, name) => {
const { onBefore, onReady } = options;
// 如果没有任何文件就不处理
if (files.length === 0) return;
const promiseList = [];
for (let f = 0; f < files.length; f++) {
const file = files[f];
// 当前上传文件唯一标识
const uid = Date.now() + '-' + f;
file.uid = uid;
if (onBefore && (await onBefore(file)) === false) return;
promiseList.push(this.handleBefore(uid, file));
}
//全部文件读取完成后再插入编辑器
Promise.all(promiseList).then(async (values) => {
if (values.some((value) => value === false)) {
engine.messageError('read image failed');
return;
}
const files = values as { file: File; info: FileInfo }[];
Promise.all([
...files.map(async (v) => {
return new Promise(async (resolve) => {
if (onReady) {
await onReady(v.info, v.file);
}
resolve(true);
});
}),
]).then(() => {
files.forEach(async (file) => {
// 处理上传
this.handleUpload(file.file, options, name);
});
});
});
};
}
/**
* 处理上传
* @param url 上传地址
* @param name formData 参数名称
* @param file 文件
*/
handleUpload(file: File, options: UploaderOptions, name: string = 'file') {
// 表单数据
const formData = new FormData();
formData.append(name, file, file.name);
if (file.data) {
Object.keys(file.data).forEach((key) => {
formData.append(key, file.data![key]);
});
}
const {
// 上传地址
url,
// 额外数据
data,
// 上传中的进度回调
onUploading,
// 上传成功回调
onSuccess,
// 上传错误回调
onError,
} = options;
if (data) {
Object.keys(data).forEach((key) => {
formData.append(key, data![key]);
});
}
// 自定义上传,并调用 onUploading onSuccess onError 回调方法
}
}
```

75
docs/config/view.md Normal file
View File

@ -0,0 +1,75 @@
---
toc: menu
---
# View configuration
The reader is mainly used for draft mode editing or simple content display. It needs real-time collaborative display and is set to be non-editable. You can use the engine's readonly attribute
Passed in when instantiating the reader
```ts
import {View} from'@aomao/engine';
//Instantiate the view
const view = new View(render node, {
... configuration items,
});
```
### lang
- Type: `string`
- Default value: `zh-CN`
- Detailed: Language configuration, temporarily supports `zh-CN`, `en-US`. Can use `locale` configuration
```ts
const view = new View(render node, {
lang:'zh-CN',
});
```
### locale
- Type: `object`
- Default value: `zh-CN`
- Detailed: Configure additional language packs
Language pack, default language pack [https://github.com/yanmao-cc/am-editor/blob/master/locale](https://github.com/yanmao-cc/am-editor/tree/master/ locale)
```ts
const view = new View(render node, {
locale: {
'zh-CN': {
test:'Test',
a: {
b: "B"
}
},
}
});
console.log(view.language.get<string>('test'));
```
### root
- Type: `Node`
- Default value: the parent node of the current reader render node
- Detailed: Reader root node
### plugins
- Type: `Array<Plugin>`
- Default value: `[]`
- Detailed: A collection of plugins that implement the abstract class of `Plugin`
### cards
- Type: `Array<Card>`
- Default value: `[]`
- Detailed: Implement the card collection of the `Card` abstract class
### config
- Type: `{ [key: string]: PluginOptions }`
- Default value: `{}`
- Detailed: the configuration item of each plug-in, the key is the name of the plug-in, please refer to the description of each plug-in for detailed configuration

75
docs/config/view.zh-CN.md Normal file
View File

@ -0,0 +1,75 @@
---
toc: menu
---
# 阅读器配置
阅读器主要用于草稿模式编辑或单纯的内容显示,需要实时协同显示并且设置为不可编辑,可以使用引擎的 `readonly` 属性
在实例化阅读器时传入
```ts
import { View } from '@aomao/engine';
//实例化引擎
const view = new View(渲染节点, {
...配置项,
});
```
### lang
- 类型: `string`
- 默认值:`zh-CN`
- 详细:语言配置,暂时支持 `zh-CN`、`en-US`。可使用 `locale` 配置
```ts
const view = new View(渲染节点, {
lang: 'zh-CN',
});
```
### locale
- 类型: `object`
- 默认值:`zh-CN`
- 详细:配置额外语言包
语言包,默认语言包 [https://github.com/yanmao-cc/am-editor/blob/master/locale](https://github.com/yanmao-cc/am-editor/blob/master/locale)
```ts
const view = new View(渲染节点, {
locale: {
'zh-CN': {
test: '测试',
a: {
b: 'B',
},
},
},
});
console.log(view.language.get<string>('test'));
```
### root
- 类型: `Node`
- 默认值:当前阅读器渲染节点父节点
- 详细:阅读器根节点
### plugins
- 类型: `Array<Plugin>`
- 默认值:`[]`
- 详细:实现 `Plugin` 抽象类的插件集合
### cards
- 类型: `Array<Card>`
- 默认值:`[]`
- 详细:实现 `Card` 抽象类的卡片集合
### config
- 类型: `{ [key: string]: PluginOptions }`
- 默认值:`{}`
- 详细每个插件的配置项key 为插件名称,详细配置请参考每个插件的说明

31
docs/docs/README.md Normal file
View File

@ -0,0 +1,31 @@
---
title: Introduction
---
## What is it?
> Thanks to Google Translate
Use the `contenteditable` attribute provided by the browser to make a DOM node editable.
The engine takes over most of the browser's default behaviors such as cursors and events.
The nodes in the editor area have four types of combined nodes of `mark`, `inline`, `block` and `card` through the `schema` rule. They are composed of different attributes, styles or `html` structures. Certain constraints are imposed on nesting.
Use the `MutationObserver` to monitor the changes of the `html` structure in the editing area, and generate a `json0` type data format to interact with the [ShareDB](https://github.com/share/sharedb) library to meet the needs of collaborative editing .
**`Vue2`** example [https://github.com/zb201307/am-editor-vue2](https://github.com/zb201307/am-editor-vue2)
**`Vue3`** example [https://github.com/yanmao-cc/am-editor/tree/master/examples/vue](https://github.com/yanmao-cc/am-editor/tree/master/examples/vue)
**`React`** example [https://github.com/yanmao-cc/am-editor/tree/master/examples/react](https://github.com/yanmao-cc/am-editor/tree/master/examples/react)
## Features
- Out of the box, it provides dozens of rich plug-ins to meet most needs
- High extensibility, in addition to the basic plug-in of `mark`, inline`and`block`type, we also provide`card`component combined with`React`, `Vue` and other front-end libraries to render the plug-in UI
- Rich multimedia support, not only supports pictures, audio and video, but also supports insertion of embedded multimedia content
- Support Markdown syntax
- The engine is written in pure JavaScript and does not rely on any front-end libraries. Plug-ins can be rendered using front-end libraries such as `React` and `Vue`. Easily cope with complex architecture
- Built-in collaborative editing program, ready to use with lightweight configuration
- Compatible with most of the latest mobile browsers

33
docs/docs/README.zh-CN.md Normal file
View File

@ -0,0 +1,33 @@
---
title: 介绍
---
## 是什么?
一个富文本<em>协同</em>编辑器框架,可以使用<em>React</em><em>Vue</em>自定义插件
`广告`[科学上网,方便、快捷的上网冲浪](https://xiyou4you.us/r/?s=18517120) 稳定、可靠,访问 Github 或者其它外网资源很方便。
使用浏览器提供的 `contenteditable` 属性让一个 DOM 节点具有可编辑能力。
引擎接管了浏览器大部分光标、事件等默认行为。
可编辑器区域内的节点通过 `schema` 规则,制定了 `mark` `inline` `block` `card` 4 种组合节点,他们由不同的属性、样式或 `html` 结构组成,并对它们的嵌套进行了一定的约束。
通过 `MutationObserver` 监听编辑区域内的 `html` 结构的改变,并生成 `json0` 类型的数据格式与 [ShareDB](https://github.com/share/sharedb) 库进行交互达到协同编辑的需要。
**`Vue2`** 案例 [https://github.com/zb201307/am-editor-vue2](https://github.com/zb201307/am-editor-vue2)
**`Vue3`** 案例 [https://github.com/yanmao-cc/am-editor/tree/master/examples/vue](https://github.com/yanmao-cc/am-editor/tree/master/examples/vue)
**`React`** 案例 [https://github.com/yanmao-cc/am-editor/tree/master/examples/react](https://github.com/yanmao-cc/am-editor/tree/master/examples/react)
## 特性
- 开箱即用,提供几十种丰富的插件来满足大部分需求
- 高扩展性,除了 `mark` `inline` `block` 类型基础插件外,我们还提供 `card` 组件结合`React` `Vue`等前端库渲染插件 UI
- 丰富的多媒体支持,不仅支持图片和音视频,更支持插入嵌入式多媒体内容
- 支持 Markdown 语法
- 引擎纯 JavaScript 编写,不依赖任何前端库,插件可以使用 `React` `Vue` 等前端库渲染。复杂架构轻松应对
- 内置协同编辑方案,轻量配置即可使用
- 兼容大部分最新移动端浏览器

View File

@ -0,0 +1,53 @@
# editor
In am-editor, the editor is a separate mode for reading and writing. Editing mode and reading mode need to be rendered through different modules. Because of the existence of `card` mode, the content output by the editor may also be interactive. In reading mode, we can also let the plug-in use `React` `Vue`Wait for the front-end framework to render some interactive components. Simply put, all the effects that can be achieved under the front-end framework can be placed in the plug-in. For example: you can make a voting plug-in, you can set voting options in edit mode, you can choose to vote in reading mode, show the number of votes after voting, and so on. These functions may be very interesting. Unlike traditional editors that output fixed html or json data, of course we also support the output of pure html and present non-interactive content.
Instantiate engine
```ts
import Engine from'@aomao/engine';
...
//initialization
const engine = new Engine("Editor root node", {
plugins: [],
cards: [],
});
```
Although it is a read-write separation mode, most of the logic for rendering content in reading mode is the same as in editing mode, so the engine provides the View module to render the reading mode
The instantiation method is roughly the same as the engine
```ts
import {View} from'@aomao/engine';
...
//initialization
const view = new View("Renderer root node", {
plugins: [],
cards: [],
});
```
Inside the plug-in, we may need to control the reading mode, we can use `isEngine` to determine
```ts
import {isEngine} from'@aomao/engine';
...
if(isEngine(this.editor)) {
//Edit mode
} else {
//Reading mode
}
...
```
## Edit mode
In the editing mode, we need to control the DOM tree, cursor, events, etc., so that the user's input can achieve the best expected value and experience, all of which will be done by the engine `@aomao/engine`
## Reading Mode
The reading mode is much simpler than the editing mode. There is no need to change the DOM tree, and the cursor can hardly be controlled. The interaction of the `card` plugin is exactly the same as writing the components of the front-end framework such as `React` and `Vue` under normal circumstances.

View File

@ -0,0 +1,53 @@
# 编辑器
在 am-editor 中编辑器是读写分离的模式。编辑模式和阅读模式需要通过不同的模块呈现渲染,因为有`card`模式的存在,编辑器输出的内容也可能是存在交互的,在阅读模式下,我们也可以让插件借助`React` `Vue`等前端框架渲染一些交互组件,简单的说在前端框架下可以实现的效果都可以放在插件里面。例如:可以做一个投票插件,编辑模式下我们可以设置投票选项,阅读模式下可以选择投票,投票后展现投票数量等等。这些功能可能会非常有意思,不像传统编辑器那样输出固定的 html 或者 json 数据,当然我们也支持输出纯 html呈现无交互内容。
实例化引擎
```ts
import Engine from '@aomao/engine';
...
//初始化
const engine = new Engine("编辑器根节点", {
plugins: [],
cards: [],
});
```
虽然是读写分离的模式,但是阅读模式渲染内容的大部分逻辑与编辑模式下相同,所以由引擎提供 View 模块来渲染阅读模式
实例化方式与引擎大致相同
```ts
import { View } from '@aomao/engine';
...
//初始化
const view = new View("渲染器根节点", {
plugins: [],
cards: [],
});
```
在插件内部,我们可能需要对阅读模式做一些控制,我们可以通过 `isEngine` 来判定
```ts
import { isEngine } from '@aomao/engine';
...
if(isEngine(this.editor)) {
//编辑模式
} else {
//阅读模式
}
...
```
## 编辑模式
编辑模式我们需要控制 DOM 树、光标、事件等等让用户的输入达到最好的预期值与体验,这些都将由引擎`@aomao/engine`来完成
## 阅读模式
阅读模式相对于编辑模式简单得多,不需要改变 DOM 树,光标几乎可以不用控制。`card`插件的交互完全和正常情况下写`React` `Vue`等前端框架的组件一样。

504
docs/docs/concepts-event.md Normal file
View File

@ -0,0 +1,504 @@
# Incident
In the engine, we handle many events by default, such as: text input, delete, copy, paste, left and right arrow keys, markdown syntax input monitoring, plug-in shortcut keys, and so on. These events may have different processing logic at different cursor positions. Most operations are to modify the DOM tree structure and repair the cursor position. In addition, we also expose these events to the plug-in to handle by itself.
Method signature
```ts
/**
* Bind event
* @param eventType event type
* @param listener event callback
* @param rewrite whether to rewrite
*/
on(eventType: string, listener: EventListener, rewrite?: boolean): void;
/**
* Remove bound event
* @param eventType event type
* @param listener event callback
*/
off(eventType: string, listener: EventListener): void;
/**
* trigger event
* @param eventType event name
* @param args trigger parameters
*/
trigger(eventType: string, ...args: any): any;
```
### Element events
In javascript, we usually use document.addEventListener document.removeEventListener to bind DOM element events. In the engine, we abstract an `EventInterface` type interface, and elements of the `NodeInterface` type are bound to an attribute event of the `EventInterface` type. So as long as the element of type `NodeInterface` can be bound, removed, and triggered by on off trigger. Not only can bind DOM native events, but also custom events
```ts
const node = $('<div></div>');
//Native event
node.on('click', () => alert('click'));
//Custom event
node.on('customer', () => alert('customer'));
node.trigger('customer');
```
### Editor events
We have processed the specific combination of keys. The following are some of the events we exposed, which are effective in both editing mode and reading mode.
```ts
//engine
engine.on('event name', 'processing method');
//read
view.on('event name', 'processing method');
```
### `keydown:all`
Select all ctrl+a key press, if it returns false, stop processing other monitors
```ts
/**
* @param event key event
* */
(event: KeyboardEvent) => boolean | void
```
### `card:minimiz`
Triggered when the card is minimized
```ts
/**
* @param card card instance
* */
(card: CardInterface) => void
```
### `card:maximize`
Triggered when the card is maximized
```ts
/**
* @param card card instance
* */
(card: CardInterface) => void
```
### `parse:value-before`
Triggered before parsing DOM nodes and generating standard editor values
```ts
/**
* @param root DOM root node
*/
(root: NodeInterface) => void
```
### `parse:value`
Parse the DOM node, generate the editor value that meets the standard, and trigger when it traverses the child nodes. Return false to skip the current node
```ts
/**
* @param node The node currently traversed
* @param attributes The filtered attributes of the current node
* @param styles The filtered style of the current node
* @param value The currently generated editor value collection
*/
(
node: NodeInterface,
attributes: {[key: string]: string },
styles: {[key: string]: string },
value: Array<string>,
) => boolean | void
```
### `parse:value-after`
Analyze DOM nodes and generate editor values that conform to the standard. Triggered after generating xml code
```ts
/**
* @param value xml code
*/
(value: Array<string>) => void
```
### `parse:html-before`
Triggered before conversion to HTML code
```ts
/**
* @param root The root node to be converted
*/
(root: NodeInterface) => void
```
### `parse:html`
Convert to HTML code
```ts
/**
* @param root The root node to be converted
*/
(root: NodeInterface) => void
```
### `parse:html-after`
Triggered after conversion to HTML code
```ts
/**
* @param root The root node to be converted
*/
(root: NodeInterface) => void
```
### `copy`
Triggered when DOM node is copied
```ts
/**
* @param node The child node currently traversed
*/
(root: NodeInterface) => void
```
## Engine events
### `change`
Editor value change event
```ts
/**
* @param value Editor value
* */
(value: string) => void
```
### `select`
Editor cursor selection trigger
```ts
() => void
```
### `focus`
Triggered when the editor is focused
```ts
() => void
```
### `blur`
Triggered when the editor loses focus
```ts
() => void
```
### `beforeCommandExecute`
Triggered before the editor executes the command
```ts
/**
* @param name Execute plug-in command name
* @param args command execution parameters
* */
(name: string, ...args: any) => void
```
### `afterCommandExecute`
Triggered after the editor executes a command
```ts
/**
* @param name Execute plug-in command name
* @param args command execution parameters
* */
(name: string, ...args: any) => void
```
### `drop:files`
Triggered when a file is dragged to the editor
```ts
/**
* @param files file collection
* */
(files: Array<File>) => void
```
### `beforeSetValue`
Triggered before assigning a value to the editor
```ts
/**
* @param value Editor value
* */
(value: string) => void
```
### `afterSetValue`
Triggered after assigning a value to the editor
```ts
/**
* @param value Editor value
* */
(value: string) => void
```
### `readonly`
Triggered when the editor's read-only attribute is changed
```ts
/**
* @param readonly is read-only
* */
(readonly: boolean) => void
```
### `paste:event`
Triggered when the paste to editor event occurs, if it returns false, the paste will not be processed
```ts
/**
* @param data Pasteboard related data
* @param source pasted rich text
* */
(data: ClipboardData & {isPasteText: boolean }, source: string) => boolean | void
```
### `paste:schema`
Set the structural rules of the DOM elements that need to be retained for this pasting, and the structural rules that the attributes need to retain
```ts
/**
* @param schema Schema object, you can add, modify, delete and other operations to the structure rules
* */
(schema: SchemaInterface) => void
```
### `paste:origin`
Parse the pasted data, and trigger before generating a fragment that matches the editor data
```ts
/**
* @param root pasted DOM node
* */
(root: NodeInterface) => void
```
### `paste:each`
Analyze the pasted data, generate a fragment that matches the editor data, and then cyclically organize the sub-elements to trigger
```ts
/**
* @param node Paste the element child nodes traversed by the fragment
* */
(root: NodeInterface) => void,
```
### `paste:each-after`
Analyze the pasted data, generate a fragment that matches the editor data, and then cycle through the sub-element stage to trigger
```ts
/**
* @param node Paste the element child nodes traversed by the fragment
* */
(root: NodeInterface) => void
```
### `paste:before`
After the DOM fragment is generated from the pasted data, it is triggered before it is written to the editor
```ts
/**
* @param fragment pasted fragment
* */
(fragment: DocumentFragment) => void
```
### `paste:insert`
Triggered after inserting the currently pasted fragment, the card has not been rendered yet
```ts
/**
* @param range cursor instance after current insertion
* */
(range: RangeInterface) => void
```
### `paste:after`
Triggered after the paste action is completed
```ts
() => void
```
### `ops`
Triggered by DOM changes, these operational changes are usually sent to the collaborative server for interaction
```ts
/**
* @param ops operation item
* */
(ops: Op[]) => void
```
### `keydown:enter`
Press the enter key, if it returns false, stop processing other monitors
```ts
(event: KeyboardEvent) => boolean | void
```
### `keydown:backspace`
The delete key is pressed, if it returns false, the processing of other monitors is terminated
```ts
(event: KeyboardEvent) => boolean | void
```
### `keydown:tab`
Tab key is pressed, if it returns false, stop processing other monitors
```ts
(event: KeyboardEvent) => boolean | void
```
### `keydown:shift-tab`
Press the Shift-Tab key, if it returns false, stop processing other monitors
```ts
(event: KeyboardEvent) => boolean |void
```
### `keydown:at`
@ The corresponding key is pressed, if it returns false, the processing of other monitors will be terminated
```ts
(event: KeyboardEvent) => boolean | void
```
### `keydown:space`
Press the space bar, if it returns false, stop processing other monitors
```ts
(event: KeyboardEvent) => boolean | void
```
### `keydown:slash`
Press the backslash key to call out the Toolbar, if it returns false, stop processing other monitors
```ts
(event: KeyboardEvent) => boolean | void
```
### `keydown:left`
Press the left arrow key, if it returns false, stop processing other monitors
```ts
(event: KeyboardEvent) => boolean | void
```
### `keydown:right`
Press the right arrow key, if it returns false, stop processing other monitors
```ts
(event: KeyboardEvent) => boolean | void
```
### `keydown:up`
Press the up arrow key, if it returns false, stop processing other monitors
```ts
(event: KeyboardEvent) => boolean | void
```
### `keydown:down`
Press the down arrow key, if it returns false, stop processing other monitors
```ts
(event: KeyboardEvent) => boolean | void
```
### `keyup:enter`
Press the enter key to bounce up, if it returns false, stop processing other monitors
```ts
(event: KeyboardEvent) => boolean | void
```
### `keyup:backspace`
Press the delete button to pop up, if it returns false, terminate the processing of other monitors
```ts
(event: KeyboardEvent) => boolean | void
```
### `keyup:tab`
Tab key presses and pops up, if it returns false, terminate the processing of other monitors
```ts
(event: KeyboardEvent) => boolean | void
```
### `keyup:space`
Press the space bar to pop up, if it returns false, terminate the processing of other monitors
```ts
(event: KeyboardEvent) => boolean | void
```
## Reader events
### `render`
Triggered after the reader has finished rendering
```ts
/**
* @param node render root node
* */
(node: NodeInterface) => void
```

View File

@ -0,0 +1,504 @@
# 事件
在引擎中我们默认处理了很多事件例如文字输入、删除、复制、粘贴、左右方向键、markdown 语法输入监听、插件快捷键等等。这些事件在不同光标位置可能会有不同的处理逻辑,大多数操作都是修改 DOM 树结构、修复光标位置。另外,我们还把这些事件暴露给插件自行处理。
方法签名
```ts
/**
* 绑定事件
* @param eventType 事件类型
* @param listener 事件回调
* @param rewrite 是否重写
*/
on(eventType: string, listener: EventListener, rewrite?: boolean): void;
/**
* 移除绑定事件
* @param eventType 事件类型
* @param listener 事件回调
*/
off(eventType: string, listener: EventListener): void;
/**
* 触发事件
* @param eventType 事件名称
* @param args 触发参数
*/
trigger(eventType: string, ...args: any): any;
```
### 元素事件
在 javascript 中我们通常使用 document.addEventListener document.removeEventListener 绑定 DOM 元素事件。在引擎中,我们抽象了一个 `EventInterface` 类型接口,并且 `NodeInterface` 类型的元素绑定了`EventInterface`类型的属性 event。所以只要是 `NodeInterface` 类型的元素都可以通过 on off trigger绑定、移除、触发事件。不仅可以绑定 DOM 原生事件,还可以绑定自定义事件
```ts
const node = $('<div></div>');
//原生事件
node.on('click', () => alert('click'));
//自定义事件
node.on('customer', () => alert('customer'));
node.trigger('customer');
```
### 编辑器事件
我们对特定的组合按键进行了处理,以下是我们暴露出来的一些事件,在编辑模式和阅读模式都有效
```ts
//引擎
engine.on('事件名称', '处理方法');
//阅读
view.on('事件名称', '处理方法');
```
### `keydown:all`
全选 ctrl+a 键按下,如果返回 false终止处理其它监听
```ts
/**
* @param event 按键事件
* */
(event: KeyboardEvent) => boolean | void
```
### `card:minimiz`
卡片最小化时触发
```ts
/**
* @param card 卡片实例
* */
(card: CardInterface) => void
```
### `card:maximize`
卡片最大化时触发
```ts
/**
* @param card 卡片实例
* */
(card: CardInterface) => void
```
### `parse:value-before`
解析 DOM 节点,生成符合标准的编辑器值之前触发
```ts
/**
* @param root DOM根节点
*/
(root: NodeInterface) => void
```
### `parse:value`
解析 DOM 节点,生成符合标准的编辑器值,遍历子节点时触发。返回 false 跳过当前节点
```ts
/**
* @param node 当前遍历的节点
* @param attributes 当前节点已过滤后的属性
* @param styles 当前节点已过滤后的样式
* @param value 当前已经生成的编辑器值集合
*/
(
node: NodeInterface,
attributes: { [key: string]: string },
styles: { [key: string]: string },
value: Array<string>,
) => boolean | void
```
### `parse:value-after`
解析 DOM 节点,生成符合标准的编辑器值。生成 xml 代码结束后触发
```ts
/**
* @param value xml代码
*/
(value: Array<string>) => void
```
### `parse:html-before`
转换为 HTML 代码之前触发
```ts
/**
* @param root 需要转换的根节点
*/
(root: NodeInterface) => void
```
### `parse:html`
转换为 HTML 代码
```ts
/**
* @param root 需要转换的根节点
*/
(root: NodeInterface) => void
```
### `parse:html-after`
转换为 HTML 代码之后触发
```ts
/**
* @param root 需要转换的根节点
*/
(root: NodeInterface) => void
```
### `copy`
复制 DOM 节点时触发
```ts
/**
* @param node 当前遍历的子节点
*/
(root: NodeInterface) => void
```
## 引擎事件
### `change`
编辑器值改变事件
```ts
/**
* @param value 编辑器值
* */
(value: string) => void
```
### `select`
编辑器光标选中触发
```ts
() => void
```
### `focus`
编辑器聚焦点时触发
```ts
() => void
```
### `blur`
编辑器失去焦点时触发
```ts
() => void
```
### `beforeCommandExecute`
在编辑器执行命令之前触发
```ts
/**
* @param name 执行插件命令名称
* @param args 命令执行参数
* */
(name: string, ...args: any) => void
```
### `afterCommandExecute`
在编辑器执行命令之后触发
```ts
/**
* @param name 执行插件命令名称
* @param args 命令执行参数
* */
(name: string, ...args: any) => void
```
### `drop:files`
拖动文件到编辑器时触发
```ts
/**
* @param files 文件集合
* */
(files: Array<File>) => void
```
### `beforeSetValue`
在给编辑器赋值前触发
```ts
/**
* @param value 编辑器值
* */
(value: string) => void
```
### `afterSetValue`
在给编辑器赋值后触发
```ts
/**
* @param value 编辑器值
* */
(value: string) => void
```
### `readonly`
编辑器只读属性变更后触发
```ts
/**
* @param readonly 是否只读
* */
(readonly: boolean) => void
```
### `paste:event`
当粘贴到编辑器事件发生时触发,如果返回 false将不在处理粘贴
```ts
/**
* @param data 粘贴板相关数据
* @param source 粘贴的富文本
* */
(data: ClipboardData & { isPasteText: boolean }, source: string) => boolean | void
```
### `paste:schema`
设置本次粘贴所需保留 DOM 元素的结构规则,以及属性所需保留的结构规则
```ts
/**
* @param schema Schema对象可以对结构规则增加修改删除等操作
* */
(schema: SchemaInterface) => void
```
### `paste:origin`
解析粘贴数据,还未生成符合编辑器数据的片段之前触发
```ts
/**
* @param root 粘贴的DOM节点
* */
(root: NodeInterface) => void
```
### `paste:each`
解析粘贴数据,生成符合编辑器数据的片段之后循环整理子元素阶段触发
```ts
/**
* @param node 粘贴片段遍历的元素子节点
* */
(root: NodeInterface) => void,
```
### `paste:each-after`
解析粘贴数据,生成符合编辑器数据的片段之后循环整理子元素阶段后触发
```ts
/**
* @param node 粘贴片段遍历的元素子节点
* */
(root: NodeInterface) => void
```
### `paste:before`
由粘贴数据生成 DOM 片段后,还未写入到编辑器之前触发
```ts
/**
* @param fragment 粘贴的片段
* */
(fragment: DocumentFragment) => void
```
### `paste:insert`
插入当前粘贴的片段后触发,此时还未渲染卡片
```ts
/**
* @param range 当前插入后的光标实例
* */
(range: RangeInterface) => void
```
### `paste:after`
粘贴动作完成后触发
```ts
() => void
```
### `ops`
DOM 改变触发,这些操作改变通常用于发送到协同服务端交互
```ts
/**
* @param ops 操作项
* */
(ops: Op[]) => void
```
### `keydown:enter`
回车键按下,如果返回 false终止处理其它监听
```ts
(event: KeyboardEvent) => boolean | void
```
### `keydown:backspace`
删除键按下,如果返回 false终止处理其它监听
```ts
(event: KeyboardEvent) => boolean | void
```
### `keydown:tab`
Tab 键按下,如果返回 false终止处理其它监听
```ts
(event: KeyboardEvent) => boolean | void
```
### `keydown:shift-tab`
Shift-Tab 键按下,如果返回 false终止处理其它监听
```ts
(event: KeyboardEvent) => boolean | void
```
### `keydown:at`
@ 符合键按下,如果返回 false终止处理其它监听
```ts
(event: KeyboardEvent) => boolean | void
```
### `keydown:space`
空格键按下,如果返回 false终止处理其它监听
```ts
(event: KeyboardEvent) => boolean | void
```
### `keydown:slash`
反斜杠键按下,唤出 Toolbar如果返回 false终止处理其它监听
```ts
(event: KeyboardEvent) => boolean | void
```
### `keydown:left`
左方向键按下,如果返回 false终止处理其它监听
```ts
(event: KeyboardEvent) => boolean | void
```
### `keydown:right`
右方向键按下,如果返回 false终止处理其它监听
```ts
(event: KeyboardEvent) => boolean | void
```
### `keydown:up`
上方向键按下,如果返回 false终止处理其它监听
```ts
(event: KeyboardEvent) => boolean | void
```
### `keydown:down`
下方向键按下,如果返回 false终止处理其它监听
```ts
(event: KeyboardEvent) => boolean | void
```
### `keyup:enter`
回车键按下弹起,如果返回 false终止处理其它监听
```ts
(event: KeyboardEvent) => boolean | void
```
### `keyup:backspace`
删除键按下弹起,如果返回 false终止处理其它监听
```ts
(event: KeyboardEvent) => boolean | void
```
### `keyup:tab`
Tab 键按下弹起,如果返回 false终止处理其它监听
```ts
(event: KeyboardEvent) => boolean | void
```
### `keyup:space`
空格键按下弹起,如果返回 false终止处理其它监听
```ts
(event: KeyboardEvent) => boolean | void
```
## 阅读器事件
### `render`
在阅读器渲染完成后触发
```ts
/**
* @param node 渲染根节点
* */
(node: NodeInterface) => void
```

View File

@ -0,0 +1,5 @@
# History
In the engine, the observe method of [`MutationObserver`](https://dom.spec.whatwg.org/#mutationobserver) is used to monitor the changes of the DOM tree. The observe method is not triggered immediately after every change, but in Triggered when there is no subsequent editing behavior. This interval is very short, so short that we can think of it as changing from time to time. This interval depends on the implementation of `MutationObserver` and we cannot control it.
For each change, we will provide the DOM node data (similar to addition, deletion and modification operations) provided to us by `MutationObserver`, including attribute changes. Converting to `Ops` is equivalent to a set of description operations, and secondly, we also record before the change The cursor position. We treat these data as a kind of snapshot in the memory in the form of a stack. When executing undo and redo commands, we will search and restore by index from this stack. At the same time, these `ops` will also be transmitted to our collaborative server to notify each client of the changes in the DOM tree

View File

@ -0,0 +1,5 @@
# 历史
在引擎中会使用[`MutationObserver`](https://dom.spec.whatwg.org/#mutationobserver)的 observe 方法监听 DOM 树的变更,并不是每次变更后都会立马触发 observe 方法,而是在没有后续编辑行为时触发,这个间隔非常短,短到能让我们认为是时时变更的。这个间隔时间取决于`MutationObserver`的实现,我们无法控制。
每次的变更,我们会将`MutationObserver`提供给我们的 DOM 节点数据(类似增删改的操作)包括属性的变更,转换为`Ops`相当于是一个描述操作的集合,其次我们还有记录变更前的光标位置。这些数据我们都把它当作一种快照按堆栈形式存在内存中。在执行撤销、重做命令时,我们会从这个堆栈中按索引查找并还原。同时这些`Ops`还会传输到我们的协同服务端上,以此来通知各个客户端 DOM 树的变更

118
docs/docs/concepts-node.md Normal file
View File

@ -0,0 +1,118 @@
# Node
The DOM node is the most important object in the editor, and the editor data structure is a DOM tree. According to functions and characteristics, we can be divided into
- `mark` style node, we can add color, bold, font size and other effects to the text, and can nest effects in each other
- `inline` Inline nodes, for example, links. Add special attributes or style effects to a paragraph of text, not nested.
- `block` block-level node, can occupy a line alone, and can have multiple `mark` `inline` style nodes as child nodes
- `card` is a single area, which can be in-line nodes or block-level nodes. In this area, unless there is a specific area that can be edited, it will be handed over to the developer to customize
This is a simple plain text value:
```html
<p>This is a <strong>paragraph</strong></p>
```
Nodes usually consist of html tags and some style attributes. In order to facilitate the distinction, the composition of each style node should be unique.
For example, to have a unique label name:
```html
<strong>Bold</strong> <em>Italic</em>
```
Or modified by attributes and styles:
```html
<span style="font-weight:bold">Bold</span>
<span style="font-style:italic">Italic</span>
```
They all have the same effect, but the engine judges that they all belong to different plug-ins.
## Style node
The style node is usually used to describe the text size, bold, italic, color and other styles of the text.
The child nodes of a style node can only be a text node or a style node. The style node must have a parent node (inline node or block-level node) and cannot exist in the editor alone.
```html
<p>
This is a <span style="color:red"><em>red</em> text</span>
</p>
```
## In-line node
Inline nodes have all the characteristics of style nodes, but inline nodes cannot be nested, and the child nodes of inline nodes can only be style nodes or text nodes. Similarly, inline nodes must have a parent node (only block-level nodes), and cannot exist alone in the editor.
```html
<p>
This is <a href="https://www.yanmao.cc">a <strong>link</strong></a>
</p>
```
## Block node
The block-level node occupies a line in the editor. Except for explicitly specifying the nesting relationship with `schema`, it can only be under `$root` (editor root node) by default. The child node can be any other node, unless it has been specified that the plug-in cannot contain certain style node classes. For example, bolding and adjusting the font size cannot be used in the title.
```html
<!-- strong tags will be filtered out -->
<h2>This is a <strong>title</strong></h2>
```
The p tag belongs to the block-level node required by default in the engine and is used to indicate a paragraph. In custom nodes, it is not recommended to use the p tag.
## Card
We can divide a separate area in the editor to display a complex editing module. This area is like a piece of white paper, you can sway freely on it. His structure looks like this:
```html
<div
data-card-value="data:%7B%22id%22%3A%22eIxTM%22%7D"
data-card-type="block"
data-card-key="hr"
>
<div data-card-element="body">
<span data-card-element="left" data-transient-element="true"></span>
<div data-card-element="center" contenteditable="false" class="card-hr">
<!-- Card content node -->
</div>
<span data-card-element="right" data-transient-element="true"></span>
</div>
</div>
```
### Attributes
`data-card-type` indicates the card type, there are two types of cards:
- Inline `inline` can be embedded in a block-level label as a child node, which can be displayed at the same level as text, style nodes, and other inline nodes
- `block` as a block-level node on its own line
`data-card-value` card custom value, which can be dynamically rendered with the help of the value during rendering
`data-card-key` card name identification
### Child node
`data-card-element` card sub-fixed node identification attribute
- `body` The main node of the card, which contains all the content of the card
- `left` `right` The user controls the cursor on both sides of the card, which is also a fixed node and cannot store any content
- The `center` card content node is also a custom rendering node. All your nodes should be placed here.
## Node selector
To manipulate the complex DOM tree, it seems more troublesome to use the document.createElement related function that comes with the browser. It would be very convenient if there is a javascript library like `JQuery`, so we encapsulated a "simple version of the jquery library".
```ts
import { $ } from '@aomao/engine';
//Select node
const node = $('CSS selector');
//Create node
const divNode = $('<div></div>');
```
Using \$ to create or select a node will return a `NodeInterface` type object, which can better help you manage DOM `Node` nodes. Please check the API for specific properties and methods

View File

@ -0,0 +1,118 @@
# 节点
DOM 节点在编辑器中是最重要的对象,编辑器数据结构就是一个 DOM 树。按照功能和特性我们可以划分为
- `mark` 样式节点,我们可以给文本加上颜色、加粗、字体大小等效果,并且可以互相嵌套效果
- `inline` 行内节点,例如,链接。给一段文字添加特殊属性或者样式效果,不可嵌套。
- `block` 块级节点,可以独占一行,并且可以有多个 `mark` `inline` 样式节点作为子节点
- `card` 一个单独区域,可以是行内节点也可以是块级节点。在这个区域内,除非有指定特定区域可编辑,否则都将交由开发者自定义
这是一个简单的纯文本值:
```html
<p>这是一个<strong>段落</strong></p>
```
节点通常由 html 标签和一些样式属性组成。为了有利于区分,每个样式节点的组成都应唯一。
例如,拥有一个独特的标签名称:
```html
<strong>加粗</strong> <em>斜体</em>
```
或者通过属性以及样式来修饰:
```html
<span style="font-weight:bold">加粗</span>
<span style="font-style:italic">斜体</span>
```
他们都有一样的效果,但引擎在判定上,他们都属于不同插件。
## 样式节点
样式节点通常用来描述文本的文字大小、粗体、斜体、颜色等样式。
样式节点的子节点只能是文本节点或者样式节点,样式节点必须有父节点(行内节点或块级节点),不能单独存在于编辑器中。
```html
<p>
This is a <span style="color:red"><em>red</em> text</span>
</p>
```
## 行内节点
行内节点拥有样式节点的所有的特质,但是行内节点不可以嵌套,行内节点的子节点只能是样式节点或者文本节点。同样,行内节点必须有父节点(只能是块级节点),不能单独存在于编辑器中。
```html
<p>
This is <a href="https://www.yanmao.cc">a <strong>link</strong></a>
</p>
```
## 块级节点
块级节点在编辑器中独占一行,除了使用 `schema` 明确指定嵌套关系外,默认只能在 `$root` (编辑器根节点)下。子节点可以是其它任意节点,除非已指定不能包含某些样式节点类的插件。例如,标题中不能使用加粗、调整字体大小。
```html
<!-- strong 标签将会被过滤掉 -->
<h2>This is a <strong>title</strong></h2>
```
p 标签在引擎中属于默认所需的块级节点,用于表明一个段落。在自定义节点中,不建议再使用 p 标签。
## 卡片
我们可以在编辑器中划分一个单独区域,用于展示一个复杂的编辑模块。该区域就像一张白纸,你可以在上面挥洒自如。他的结构看起来像这样:
```html
<div
data-card-value="data:%7B%22id%22%3A%22eIxTM%22%7D"
data-card-type="block"
data-card-key="hr"
>
<div data-card-element="body">
<span data-card-element="left" data-transient-element="true"></span>
<div data-card-element="center" contenteditable="false" class="card-hr">
<!-- 卡片内容节点 -->
</div>
<span data-card-element="right" data-transient-element="true"></span>
</div>
</div>
```
### 属性
`data-card-type` 表示卡片类型,卡片有两种类型:
- `inline` 行内,可以嵌入一个块级标签中作为子节点,可以和文本、样式节点、其它行内节点同级别展示
- `block` 作为一个块级节点独占一行
`data-card-value` 卡片自定义值,在渲染时可以借助值动态渲染
`data-card-key` 卡片名称标识
### 子节点
`data-card-element` 卡片子固定节点标识属性
- `body` 卡片主体节点,包含卡片所有的内容
- `left` `right` 用户控制卡片两侧光标,也是固定的节点,不能存任何内容
- `center` 卡片内容节点,也是自定义渲染节点。你的所有节点要放在这里。
## 节点选择器
要操作复杂的 DOM 树,使用浏览器自带的 document.createElement 相关函数看起来比较麻烦。如果有像`JQuery`的 javascript 库则会很方便,因此我们封装了一个"简易版的 jquery 库"。
```ts
import { $ } from '@aomao/engine';
//选择节点
const node = $('CSS选择器');
//创建节点
const divNode = $('<div></div>');
```
使用 \$ 创建或选择节点后会返回一个 `NodeInterface` 类型对象,能更好的帮助你管理 DOM `Node` 节点。具体属性和方法请查看 API

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