feat: improve editor

pull/15/head
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} */
const nextConfig = semi({
reactStrictMode: true,
assetPrefix: config.assetPrefix,
env: {
SERVER_API_URL: config.apiUrl,
COLLABORATION_API_URL: config.collaborationUrl,
ENABLE_ALIYUN_OSS: config?.oss?.aliyun?.accessKeyId,
ENABLE_ALIYUN_OSS: !!config?.oss?.aliyun?.accessKeyId,
},
webpack: (config, { dev, isServer }) => {
config.resolve.plugins.push(new TsconfigPathsPlugin());

View File

@ -22,15 +22,15 @@ export const CreateUser: React.FC<{ document: IDocument; container: () => HTMLEl
}}
>
<Space>
<Avatar size="extra-extra-small" src={document.createUser && document.createUser.avatar}>
<Avatar size="small" src={document.createUser && document.createUser.avatar}>
<IconUser />
</Avatar>
<div>
<p>
<p style={{ margin: 0 }}>
{document.createUser && document.createUser.name}
</p>
<p>
<p style={{ margin: '8px 0 0' }}>
<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 './IconHeading2';
export * from './IconHeading3';
export * from './IconTableHeaderRow';
export * from './IconTableHeaderColumn';
export * from './IconTableHeaderCell';

View File

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

View File

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

View File

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

View File

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

View File

@ -9,6 +9,9 @@ import {
IconMergeCell,
IconSplitCell,
IconDeleteTable,
IconTableHeaderRow,
IconTableHeaderColumn,
IconTableHeaderCell,
} from 'components/icons';
import { Tooltip } from 'components/tooltip';
import { Divider } from '../wrappers/divider';
@ -24,6 +27,7 @@ export const TableBubbleMenu = ({ editor }) => {
shouldShow={() => editor.isActive(Table.name)}
tippyOptions={{
maxWidth: 456,
placement: 'bottom',
}}
matchRenderContainer={(node: HTMLElement) =>
node && node.classList && node.classList.contains('tableWrapper') && node.tagName === 'DIV'
@ -93,6 +97,38 @@ export const TableBubbleMenu = ({ editor }) => {
<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="合并单元格">
<Button
size="small"

View File

@ -21,14 +21,6 @@ export const extractFileExtension = (fileName) => {
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) => {
if (size < 1024) {
return size + ' Byte';

View File

@ -13,5 +13,7 @@ export const markdownToProsemirror = ({ schema, content, hasTitle }) => {
const parser = new DOMParser();
const { body } = parser.parseFromString(html, 'text/html');
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;
border-radius: var(--border-radius);
border: 1px solid var(--semi-color-border);
margin: 16px 0;
p {
margin-top: 0.25em;

View File

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

View File

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

View File

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

View File

@ -1,34 +1,38 @@
.items {
max-height: 50vh;
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);
border-radius: var(--border-radius);
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 {
display: block;
margin: 0;
width: 100%;
text-align: left;
background: transparent;
border-radius: 0.4rem;
border: 1px solid transparent;
padding: 0.2rem 0.4rem;
color: inherit;
align-items: center;
border-radius: 0px;
box-sizing: border-box;
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 {
border-color: var(--semi-color-info);
background-color: #f4f5f7;
}
&.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 {

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) {
return (

View File

@ -1,35 +1,38 @@
.items {
width: 160px;
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);
border-radius: var(--border-radius);
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 {
display: block;
margin: 0;
width: 100%;
text-align: left;
background: transparent;
border-radius: 0.4rem;
border: 1px solid transparent;
padding: 0.2rem 0.4rem;
color: inherit;
align-items: center;
border-radius: 0px;
box-sizing: border-box;
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 {
border-color: var(--semi-color-info);
background-color: #f4f5f7;
}
&.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 {

View File

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

View File

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

View File

@ -1,8 +1,17 @@
import { string } from 'lib0';
import { HttpClient } from './HttpClient';
export const uploadFile = async (file: Blob): Promise<string> => {
if (process.env.ENABLE_ALIYUN_OSS) {
return Promise.reject(new Error('阿里云OSS配置不完善请自行实现上传文件'));
export const readFileAsDataURL = (file): Promise<string | ArrayBuffer> => {
return new Promise((resolve) => {
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();

View File

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

View File

@ -165,6 +165,11 @@
p {
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'] {
@ -261,14 +266,12 @@
min-width: 48px;
overflow-x: auto;
line-height: 1.3;
background-color: #0d0d0d;
background-color: var(--semi-color-fill-0);
code {
color: inherit;
font-size: 0.875rem;
line-height: 1.5rem;
border-radius: var(--border-radius);
margin: 8px;
padding: 0;
white-space: pre;
@ -315,11 +318,12 @@
th {
font-weight: bold;
background-color: rgb(244, 245, 247);
}
.selectedCell {
border-style: double;
border-color: var(--semi-color-info);
border-color: rgb(0 101 255);
background: var(--semi-color-info-light-hover);
}
@ -414,14 +418,15 @@
}
.node-codeBlock,
.node-katex,
.node-documentChildren,
.node-documentReference,
.node-katex {
.node-documentReference {
.render-wrapper {
border: 1px solid transparent;
border: 1px solid var(--semi-color-border);
}
}
.node-codeBlock,
.node-katex {
.render-wrapper {
border-radius: var(--border-radius);
@ -442,7 +447,6 @@
background-color: transparent;
}
}
// #e0ebfa
.render-wrapper {
position: relative;
@ -451,8 +455,8 @@
&.selected-node {
.render-wrapper {
border: 1px solid rgb(0 101 255);
background-color: var(--semi-color-info-light-hover);
border: 1px solid rgb(0 101 255) !important;
background-color: #e0ebfa;
}
}
}
@ -466,7 +470,7 @@
td,
th {
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:
aliyun:
accessKeyId: 'LTAI5tNd19aX1TZJwGjKJWVE'
accessKeySecret: 'nlZJQprIO45QDeeANVlBZ6F7SIjBZs'
bucket: 'wipi'
accessKeyId: ''
accessKeySecret: ''
bucket: ''
https: true
region: 'oss-cn-shanghai'
region: ''
# jwt 配置
jwt: