feat: improve editor

This commit is contained in:
fantasticit 2022-03-25 17:17:10 +08:00
parent 503e4c5b57
commit edd964ab5f
26 changed files with 204 additions and 102 deletions

View File

@ -5,12 +5,11 @@ const config = getConfig().client;
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const nextConfig = semi({ const nextConfig = semi({
reactStrictMode: true,
assetPrefix: config.assetPrefix, assetPrefix: config.assetPrefix,
env: { env: {
SERVER_API_URL: config.apiUrl, SERVER_API_URL: config.apiUrl,
COLLABORATION_API_URL: config.collaborationUrl, COLLABORATION_API_URL: config.collaborationUrl,
ENABLE_ALIYUN_OSS: config?.oss?.aliyun?.accessKeyId, ENABLE_ALIYUN_OSS: !!config?.oss?.aliyun?.accessKeyId,
}, },
webpack: (config, { dev, isServer }) => { webpack: (config, { dev, isServer }) => {
config.resolve.plugins.push(new TsconfigPathsPlugin()); config.resolve.plugins.push(new TsconfigPathsPlugin());

View File

@ -22,15 +22,15 @@ export const CreateUser: React.FC<{ document: IDocument; container: () => HTMLEl
}} }}
> >
<Space> <Space>
<Avatar size="extra-extra-small" src={document.createUser && document.createUser.avatar}> <Avatar size="small" src={document.createUser && document.createUser.avatar}>
<IconUser /> <IconUser />
</Avatar> </Avatar>
<div> <div>
<p> <p style={{ margin: 0 }}>
{document.createUser && document.createUser.name} {document.createUser && document.createUser.name}
</p> </p>
<p> <p style={{ margin: '8px 0 0' }}>
<LocaleTime date={document.updatedAt} timeago /> <LocaleTime date={document.updatedAt} timeago />
{' ⦁ '} {' ⦁ '}

View File

@ -0,0 +1,17 @@
import { Icon } from '@douyinfe/semi-ui';
export const IconTableHeaderCell: React.FC<{ style?: React.CSSProperties }> = ({ style = {} }) => {
return (
<Icon
style={style}
svg={
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="20" height="20">
<path
d="M128 266.666667A138.666667 138.666667 0 0 1 266.666667 128h490.666666A138.666667 138.666667 0 0 1 896 266.666667v490.666666A138.666667 138.666667 0 0 1 757.333333 896H266.666667A138.666667 138.666667 0 0 1 128 757.333333V266.666667zM266.666667 192A74.666667 74.666667 0 0 0 192 266.666667V362.666667h170.666667v-170.666667H266.666667zM192 426.666667h170.666667v170.666666h-170.666667v-170.666666z m234.666667 0v170.666666h170.666666v-170.666666h-170.666666z m234.666666 0v170.666666h170.666667v-170.666666h-170.666667zM597.333333 661.333333h-170.666666v170.666667h170.666666v-170.666667z m64 170.666667v-170.666667h170.666667v96a74.666667 74.666667 0 0 1-74.666667 74.666667H661.333333z m0-469.333333v-170.666667h96c41.216 0 74.666667 33.450667 74.666667 74.666667V362.666667h-170.666667z m-64-170.666667v170.666667h-170.666666v-170.666667h170.666666z m-405.333333 469.333333h170.666667v170.666667H266.666667a74.666667 74.666667 0 0 1-74.666667-74.666667V661.333333z"
p-id="15067"
></path>
</svg>
}
/>
);
};

View File

@ -0,0 +1,14 @@
import { Icon } from '@douyinfe/semi-ui';
export const IconTableHeaderColumn: React.FC<{ style?: React.CSSProperties }> = ({ style = {} }) => {
return (
<Icon
style={style}
svg={
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="18" height="18">
<path d="M64 960l896 0L960 64 64 64 64 960zM640 384l0 256L384 640 384 384 640 384zM384 896l0-192 256 0 0 192L384 896zM320 896 258.88 896 320 834.88 320 896zM320 744.384 168.384 896 128 896l0-76.096 192-192L320 744.384zM128 729.344 128 611.904l192-192 0 117.504L128 729.344zM128 521.344 128 403.904l192-192 0 117.504L128 521.344zM128 313.344 128 227.904 227.904 128l85.504 0L128 313.344zM896 896l-192 0 0-192 192 0L896 896zM896 640l-192 0L704 384l192 0L896 640zM896 128l0 192-192 0L704 128 896 128zM640 320 384 320 384 128l256 0L640 320z"></path>
</svg>
}
/>
);
};

View File

@ -0,0 +1,15 @@
import { Icon } from '@douyinfe/semi-ui';
export const IconTableHeaderRow: React.FC<{ style?: React.CSSProperties }> = ({ style = {} }) => {
return (
<Icon
style={style}
svg={
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="20" height="20">
<path d="M128.1024 371.5072V216.9856a28.5696 28.5696 0 0 1 28.8768-28.2624h711.8848a28.5696 28.5696 0 0 1 28.8768 28.2624v154.5216z m769.6384 231.8336H675.0208v-182.272h222.8224v182.272z m0 204.0832a28.5696 28.5696 0 0 1-28.8768 28.2624H675.0208v-182.272h222.8224v153.6z m-496.128 27.7504v-182.272H624.64v182.272z m-244.6336 0a28.5696 28.5696 0 0 1-28.8768-28.2624V652.3904h222.8224v182.3744H156.9792z m193.9456-231.8336H128.1024v-182.272h222.8224zM624.64 421.0688v182.3744H401.6128V421.0688z m251.392-281.9072h-727.04a71.0656 71.0656 0 0 0-71.68 70.4512v605.3888a71.0656 71.0656 0 0 0 71.68 70.3488h727.04a71.68 71.68 0 0 0 71.68-70.3488V209.5104a71.0656 71.0656 0 0 0-71.68-70.3488z m0 0"></path>
<path d="M169.984 211.2512h685.568a20.48 20.48 0 0 1 20.48 20.48v120.4224H149.504V231.7312a20.48 20.48 0 0 1 20.48-20.48z"></path>
</svg>
}
/>
);
};

View File

@ -42,3 +42,6 @@ export * from './IconList';
export * from './IconHeading1'; export * from './IconHeading1';
export * from './IconHeading2'; export * from './IconHeading2';
export * from './IconHeading3'; export * from './IconHeading3';
export * from './IconTableHeaderRow';
export * from './IconTableHeaderColumn';
export * from './IconTableHeaderCell';

View File

@ -16,7 +16,7 @@ export const Attachment = Node.create({
content: '', content: '',
marks: '', marks: '',
group: 'block', group: 'block',
draggable: true, selectable: true,
atom: true, atom: true,
addOptions() { addOptions() {

View File

@ -17,6 +17,7 @@ export const Banner = Node.create({
content: 'paragraph+', content: 'paragraph+',
group: 'block', group: 'block',
defining: true, defining: true,
selectable: true,
addAttributes() { addAttributes() {
return { return {

View File

@ -16,6 +16,7 @@ export const Status = Node.create({
group: 'inline', group: 'inline',
inline: true, inline: true,
atom: true, atom: true,
selectable: true,
addAttributes() { addAttributes() {
return { return {

View File

@ -47,7 +47,7 @@ export const Table = BuiltInTable.extend({
return [ return [
'div', 'div',
{ class: 'tableWrapper adas' }, { class: 'tableWrapper' },
['table', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), ['tbody', 0]], ['table', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), ['tbody', 0]],
]; ];
}, },

View File

@ -9,6 +9,9 @@ import {
IconMergeCell, IconMergeCell,
IconSplitCell, IconSplitCell,
IconDeleteTable, IconDeleteTable,
IconTableHeaderRow,
IconTableHeaderColumn,
IconTableHeaderCell,
} from 'components/icons'; } from 'components/icons';
import { Tooltip } from 'components/tooltip'; import { Tooltip } from 'components/tooltip';
import { Divider } from '../wrappers/divider'; import { Divider } from '../wrappers/divider';
@ -24,6 +27,7 @@ export const TableBubbleMenu = ({ editor }) => {
shouldShow={() => editor.isActive(Table.name)} shouldShow={() => editor.isActive(Table.name)}
tippyOptions={{ tippyOptions={{
maxWidth: 456, maxWidth: 456,
placement: 'bottom',
}} }}
matchRenderContainer={(node: HTMLElement) => matchRenderContainer={(node: HTMLElement) =>
node && node.classList && node.classList.contains('tableWrapper') && node.tagName === 'DIV' node && node.classList && node.classList.contains('tableWrapper') && node.tagName === 'DIV'
@ -93,6 +97,38 @@ export const TableBubbleMenu = ({ editor }) => {
<Divider /> <Divider />
<Tooltip content="设置(或取消)当前列为表头">
<Button
size="small"
type="tertiary"
theme="borderless"
icon={<IconTableHeaderColumn />}
onClick={() => editor.chain().focus().toggleHeaderColumn().run()}
/>
</Tooltip>
<Tooltip content="设置(或取消)当前行为表头">
<Button
size="small"
type="tertiary"
theme="borderless"
icon={<IconTableHeaderRow />}
onClick={() => editor.chain().focus().toggleHeaderRow().run()}
/>
</Tooltip>
<Tooltip content="设置(或取消)当前单元格为表头">
<Button
size="small"
type="tertiary"
theme="borderless"
icon={<IconTableHeaderCell />}
onClick={() => editor.chain().focus().toggleHeaderCell().run()}
/>
</Tooltip>
<Divider />
<Tooltip content="合并单元格"> <Tooltip content="合并单元格">
<Button <Button
size="small" size="small"

View File

@ -21,14 +21,6 @@ export const extractFileExtension = (fileName) => {
return fileName.split('.').pop(); return fileName.split('.').pop();
}; };
export const readFileAsDataURL = (file) => {
return new Promise((resolve) => {
const reader = new FileReader();
reader.addEventListener('load', (e) => resolve(e.target.result), { once: true });
reader.readAsDataURL(file);
});
};
export const normalizeFileSize = (size) => { export const normalizeFileSize = (size) => {
if (size < 1024) { if (size < 1024) {
return size + ' Byte'; return size + ' Byte';

View File

@ -13,5 +13,7 @@ export const markdownToProsemirror = ({ schema, content, hasTitle }) => {
const parser = new DOMParser(); const parser = new DOMParser();
const { body } = parser.parseFromString(html, 'text/html'); const { body } = parser.parseFromString(html, 'text/html');
body.append(document.createComment(content)); body.append(document.createComment(content));
return htmlToPromsemirror(body, !hasTitle); const node = htmlToPromsemirror(body, !hasTitle);
return node;
}; };

View File

@ -3,7 +3,6 @@
line-height: 0; line-height: 0;
border-radius: var(--border-radius); border-radius: var(--border-radius);
border: 1px solid var(--semi-color-border); border: 1px solid var(--semi-color-border);
margin: 16px 0;
p { p {
margin-top: 0.25em; margin-top: 0.25em;

View File

@ -2,11 +2,8 @@
margin-top: 12px; margin-top: 12px;
padding: 12px; padding: 12px;
border: 1px solid var(--semi-color-border); border: 1px solid var(--semi-color-border);
border-left: 0;
border-right: 0;
&.isEditable { &.isEditable {
border: 0;
background-color: var(--semi-color-fill-0); background-color: var(--semi-color-fill-0);
&:hover { &:hover {

View File

@ -2,6 +2,7 @@
margin-top: 12px; margin-top: 12px;
&.isEditable { &.isEditable {
border-color: transparent !important;
padding: 12px; padding: 12px;
background-color: var(--semi-color-fill-0); background-color: var(--semi-color-fill-0);
@ -11,12 +12,17 @@
.itemWrap { .itemWrap {
pointer-events: none; pointer-events: none;
margin-top: 12px;
&:hover { &:hover {
color: var(--semi-color-text-1); color: var(--semi-color-text-1);
border-color: var(--semi-color-border); border-color: var(--semi-color-border);
} }
} }
.empty {
margin-top: 12px;
}
} }
.itemWrap { .itemWrap {
@ -24,7 +30,6 @@
align-items: center; align-items: center;
padding: 8px; padding: 8px;
border: 1px solid var(--semi-color-border); border: 1px solid var(--semi-color-border);
margin-top: 12px;
border-radius: var(--border-radius); border-radius: var(--border-radius);
text-decoration: none; text-decoration: none;
color: var(--semi-color-text-1); color: var(--semi-color-text-1);
@ -44,7 +49,6 @@
align-items: center; align-items: center;
padding: 8px; padding: 8px;
border: 1px solid var(--semi-color-border); border: 1px solid var(--semi-color-border);
margin-top: 12px;
border-radius: var(--border-radius); border-radius: var(--border-radius);
text-decoration: none; text-decoration: none;
color: var(--semi-color-text-1); color: var(--semi-color-text-1);

View File

@ -22,7 +22,10 @@ export const DocumentReferenceWrapper = ({ editor, node, updateAttributes }) =>
}; };
return ( return (
<NodeViewWrapper as="div" className={cls('render-wrapper', styles.wrap, isEditable && styles.isEditable)}> <NodeViewWrapper
as="div"
className={cls(styles.wrap, isEditable && styles.isEditable, isEditable && 'render-wrapper')}
>
<div> <div>
{isEditable && ( {isEditable && (
<DataRender <DataRender
@ -55,13 +58,13 @@ export const DocumentReferenceWrapper = ({ editor, node, updateAttributes }) =>
query: { wikiId, documentId }, query: { wikiId, documentId },
}} }}
> >
<a className={styles.itemWrap} target="_blank"> <a className={cls(styles.itemWrap, !isEditable && 'render-wrapper')} target="_blank">
<IconDocument /> <IconDocument />
<span>{title || '请选择文档'}</span> <span>{title || '请选择文档'}</span>
</a> </a>
</Link> </Link>
) : ( ) : (
<div className={styles.empty}> <div className={cls(styles.empty, !isEditable && 'render-wrapper')}>
<span>{'用户未选择文档'}</span> <span>{'用户未选择文档'}</span>
</div> </div>
)} )}

View File

@ -1,34 +1,38 @@
.items { .items {
max-height: 50vh; border-radius: var(--border-radius);
overflow: auto;
padding: 0.2rem;
position: relative;
border-radius: 0.5rem;
font-size: 0.9rem;
color: var(--semi-color-text-0);
border-radius: var(--semi-border-radius-medium);
background-color: var(--semi-color-bg-0); background-color: var(--semi-color-bg-0);
border: 1px solid var(--semi-color-border); box-shadow: rgb(9 30 66 / 31%) 0px 0px 1px, rgb(9 30 66 / 25%) 0px 4px 8px -2px;
width: 200px;
max-height: 380px;
overflow-x: hidden;
overflow-y: auto;
} }
.item { .item {
display: block; align-items: center;
margin: 0; border-radius: 0px;
width: 100%; box-sizing: border-box;
text-align: left;
background: transparent;
border-radius: 0.4rem;
border: 1px solid transparent;
padding: 0.2rem 0.4rem;
color: inherit;
cursor: pointer; cursor: pointer;
display: flex;
flex: 0 0 auto;
background-color: rgb(255, 255, 255);
color: rgb(9, 30, 66);
fill: rgb(255, 255, 255);
text-decoration: none;
padding: 12px 12px 11px;
width: 100%;
border: 0;
outline: 0;
&:hover { &:hover {
border-color: var(--semi-color-info); background-color: #f4f5f7;
} }
&.is-selected { &.is-selected {
border-color: var(--semi-color-info); background-color: rgb(222, 235, 255);
color: rgb(0, 82, 204);
fill: rgb(222, 235, 255);
text-decoration: none;
} }
img { img {

View File

@ -71,7 +71,7 @@ export const ImageWrapper = ({ editor, node, updateAttributes }) => {
); );
} }
const img = <img src={src} alt={alt} width={width} height={height} />; const img = <img className="render-wrapper" src={src} alt={alt} width={width} height={height} />;
if (isEditable) { if (isEditable) {
return ( return (

View File

@ -1,35 +1,38 @@
.items { .items {
width: 160px; border-radius: var(--border-radius);
max-height: 40vh;
overflow: auto;
padding: 0.2rem;
position: relative;
border-radius: 0.5rem;
font-size: 0.9rem;
color: var(--semi-color-text-0);
border-radius: var(--semi-border-radius-medium);
background-color: var(--semi-color-bg-0); background-color: var(--semi-color-bg-0);
border: 1px solid var(--semi-color-border); box-shadow: rgb(9 30 66 / 31%) 0px 0px 1px, rgb(9 30 66 / 25%) 0px 4px 8px -2px;
width: 200px;
max-height: 380px;
overflow-x: hidden;
overflow-y: auto;
} }
.item { .item {
display: block; align-items: center;
margin: 0; border-radius: 0px;
width: 100%; box-sizing: border-box;
text-align: left;
background: transparent;
border-radius: 0.4rem;
border: 1px solid transparent;
padding: 0.2rem 0.4rem;
color: inherit;
cursor: pointer; cursor: pointer;
display: flex;
flex: 0 0 auto;
background-color: rgb(255, 255, 255);
color: rgb(9, 30, 66);
fill: rgb(255, 255, 255);
text-decoration: none;
padding: 12px 12px 11px;
width: 100%;
border: 0;
outline: 0;
&:hover { &:hover {
border-color: var(--semi-color-info); background-color: #f4f5f7;
} }
&.is-selected { &.is-selected {
border-color: var(--semi-color-info); background-color: rgb(222, 235, 255);
color: rgb(0, 82, 204);
fill: rgb(222, 235, 255);
text-decoration: none;
} }
img { img {

View File

@ -5,8 +5,6 @@
line-height: 0; line-height: 0;
overflow: visible; overflow: visible;
outline: none; outline: none;
border-radius: var(--border-radius);
border: 1px solid var(--semi-color-border);
.jsmindWrap { .jsmindWrap {
position: absolute; position: absolute;
@ -30,8 +28,9 @@
.renderWrap { .renderWrap {
position: relative; position: relative;
min-height: 50px; min-height: 50px;
border-radius: var(--border-radius);
border: 1px solid var(--semi-color-border);
overflow: hidden; overflow: hidden;
outline: none; outline: none;
> input { > input {

View File

@ -98,7 +98,12 @@ export const MindWrapper = ({ editor, node, updateAttributes }) => {
}, [isEditable]); }, [isEditable]);
const content = ( const content = (
<div ref={$container} className={styles.renderWrap} tabIndex={0} style={{ width: '100%', height: '100%' }}> <div
ref={$container}
className={cls(styles.renderWrap, 'render-wrapper')}
tabIndex={0}
style={{ width: '100%', height: '100%' }}
>
{!isEditable && ( {!isEditable && (
<div className={styles.mindHandlerWrap}> <div className={styles.mindHandlerWrap}>
<Button <Button
@ -121,16 +126,15 @@ export const MindWrapper = ({ editor, node, updateAttributes }) => {
); );
return ( return (
<NodeViewWrapper className={cls(styles.wrap, 'render-wrapper')}> <NodeViewWrapper className={cls(styles.wrap)}>
<NodeViewContent as="div"> {isEditable ? (
{isEditable ? ( <Resizeable width={width} height={height} onChange={onResize}>
<Resizeable width={width} height={height} onChange={onResize}> {content}
{content} </Resizeable>
</Resizeable> ) : (
) : ( <div style={{ display: 'inline-block', width, height }}>{content}</div>
<div style={{ display: 'inline-block', width, height }}>{content}</div> )}
)} {/* <NodeViewContent as="div"></NodeViewContent> */}
</NodeViewContent>
</NodeViewWrapper> </NodeViewWrapper>
); );
}; };

View File

@ -1,8 +1,17 @@
import { string } from 'lib0';
import { HttpClient } from './HttpClient'; import { HttpClient } from './HttpClient';
export const uploadFile = async (file: Blob): Promise<string> => { export const readFileAsDataURL = (file): Promise<string | ArrayBuffer> => {
if (process.env.ENABLE_ALIYUN_OSS) { return new Promise((resolve) => {
return Promise.reject(new Error('阿里云OSS配置不完善请自行实现上传文件')); const reader = new FileReader();
reader.addEventListener('load', (e) => resolve(e.target.result), { once: true });
reader.readAsDataURL(file);
});
};
export const uploadFile = async (file: Blob): Promise<string | ArrayBuffer> => {
if (!process.env.ENABLE_ALIYUN_OSS) {
return readFileAsDataURL(file);
} }
const formData = new FormData(); const formData = new FormData();

View File

@ -6,10 +6,6 @@
box-shadow: var(--box-shadow); box-shadow: var(--box-shadow);
background-color: var(--semi-color-nav-bg); background-color: var(--semi-color-nav-bg);
overflow-x: auto; overflow-x: auto;
&.table-bubble-menu {
transform: translateY(-2em);
}
} }
.table-controller-wrapper { .table-controller-wrapper {

View File

@ -165,6 +165,11 @@
p { p {
font-size: 1em; font-size: 1em;
line-height: 1.714;
font-weight: normal;
margin-top: 0.75rem;
margin-bottom: 0px;
letter-spacing: -0.005em;
} }
ul[data-type='taskList'] { ul[data-type='taskList'] {
@ -261,14 +266,12 @@
min-width: 48px; min-width: 48px;
overflow-x: auto; overflow-x: auto;
line-height: 1.3; line-height: 1.3;
background-color: #0d0d0d;
background-color: var(--semi-color-fill-0); background-color: var(--semi-color-fill-0);
code { code {
color: inherit; color: inherit;
font-size: 0.875rem; font-size: 0.875rem;
line-height: 1.5rem; line-height: 1.5rem;
border-radius: var(--border-radius);
margin: 8px; margin: 8px;
padding: 0; padding: 0;
white-space: pre; white-space: pre;
@ -315,11 +318,12 @@
th { th {
font-weight: bold; font-weight: bold;
background-color: rgb(244, 245, 247);
} }
.selectedCell { .selectedCell {
border-style: double; border-style: double;
border-color: var(--semi-color-info); border-color: rgb(0 101 255);
background: var(--semi-color-info-light-hover); background: var(--semi-color-info-light-hover);
} }
@ -414,14 +418,15 @@
} }
.node-codeBlock, .node-codeBlock,
.node-katex,
.node-documentChildren, .node-documentChildren,
.node-documentReference, .node-documentReference {
.node-katex {
.render-wrapper { .render-wrapper {
border: 1px solid transparent; border: 1px solid var(--semi-color-border);
} }
} }
.node-codeBlock,
.node-katex { .node-katex {
.render-wrapper { .render-wrapper {
border-radius: var(--border-radius); border-radius: var(--border-radius);
@ -442,7 +447,6 @@
background-color: transparent; background-color: transparent;
} }
} }
// #e0ebfa
.render-wrapper { .render-wrapper {
position: relative; position: relative;
@ -451,8 +455,8 @@
&.selected-node { &.selected-node {
.render-wrapper { .render-wrapper {
border: 1px solid rgb(0 101 255); border: 1px solid rgb(0 101 255) !important;
background-color: var(--semi-color-info-light-hover); background-color: #e0ebfa;
} }
} }
} }
@ -466,7 +470,7 @@
td, td,
th { th {
border-color: rgb(0 101 255); border-color: rgb(0 101 255);
background-color: var(--semi-color-info-light-hover); background-color: #e0ebfa;
} }
} }
} }

View File

@ -24,11 +24,11 @@ db:
# oss 文件存储服务 # oss 文件存储服务
oss: oss:
aliyun: aliyun:
accessKeyId: 'LTAI5tNd19aX1TZJwGjKJWVE' accessKeyId: ''
accessKeySecret: 'nlZJQprIO45QDeeANVlBZ6F7SIjBZs' accessKeySecret: ''
bucket: 'wipi' bucket: ''
https: true https: true
region: 'oss-cn-shanghai' region: ''
# jwt 配置 # jwt 配置
jwt: jwt: