feat: imporve editor mobile ux

This commit is contained in:
fantasticit 2022-05-04 17:20:06 +08:00
parent ef61f1bdf3
commit 12379571cb
15 changed files with 146 additions and 33 deletions

View File

@ -1,9 +1,12 @@
export function debounce(func, timeout = 300) {
export function debounce(func, timeout = 200) {
let timer;
return (...args) => {
if (!timer) {
func.apply(this, args);
}
clearTimeout(timer);
timer = setTimeout(() => {
func.apply(this, args);
timer = undefined;
}, timeout);
};
}

View File

@ -0,0 +1,9 @@
export function isIOS() {
const userAgent = navigator.userAgent || navigator.vendor || (window as any).opera;
return /iPad|iPhone|iPod/.test(userAgent) && !(window as any).MSStream;
}
export function isAndroid() {
const userAgent = navigator.userAgent || navigator.vendor || (window as any).opera;
return /android/i.test(userAgent);
}

View File

@ -114,24 +114,16 @@ export class LRUCache {
}
}
const USED_STORAGE_KEYS = [];
const CacheMap = new Map();
export const createKeysLocalStorageLRUCache = (storageKey, capacity) => {
const lruCache = new LRUCache(capacity);
if (USED_STORAGE_KEYS.includes(storageKey)) {
// @ts-ignore
if (module.hot) {
console.error(`Storage Key ${storageKey} has been used!`);
return;
} else {
throw new Error(`Storage Key ${storageKey} has been used!`);
}
if (CacheMap.has(storageKey)) {
return CacheMap.get(setStorage);
}
USED_STORAGE_KEYS.push(storageKey);
return {
const manager = {
syncFromStorage() {
const data = getStorage(storageKey) || [];
data
@ -152,4 +144,8 @@ export const createKeysLocalStorageLRUCache = (storageKey, capacity) => {
return key ? lruCache.get(key) : lruCache.keys();
},
};
CacheMap.set(storageKey, manager);
return manager;
};

View File

@ -3,7 +3,7 @@ import { Toast } from '@douyinfe/semi-ui';
import Router from 'next/router';
export const HttpClient = axios.create({
baseURL: process.env.SERVER_API_URL,
baseURL: 'http://192.168.31.124:5001/api' || process.env.SERVER_API_URL,
timeout: 60000,
});

View File

@ -0,0 +1,33 @@
import { Extension } from '@tiptap/core';
import { Plugin, PluginKey, Transaction } from 'prosemirror-state';
import { debounce } from 'helpers/debounce';
export const scrollIntoViewPluginKey = new PluginKey('scrollIntoViewPlugin');
type TransactionWithScroll = Transaction & { scrolledIntoView: boolean };
export const ScrollIntoView = Extension.create({
name: 'scrollIntoView',
addProseMirrorPlugins() {
const { editor } = this;
return [
new Plugin({
key: scrollIntoViewPluginKey,
appendTransaction: debounce((transactions, oldState, newState) => {
if (!transactions.length || !editor.isEditable) {
return;
}
const tr = transactions[0] as TransactionWithScroll;
if (
(tr.docChanged || tr.storedMarksSet) &&
!tr.scrolledIntoView &&
tr.getMeta('scrollIntoView') !== false &&
tr.getMeta('addToHistory') !== false
) {
return newState.tr.scrollIntoView();
}
}, 100),
}),
];
},
});

View File

@ -66,13 +66,13 @@ export const EmojiList: React.FC<IProps> = forwardRef((props, ref) => {
<div ref={$container}>
{props.items.length ? (
props.items.map((item, index) => (
<button
<span
className={cls(styles.item, index === selectedIndex ? styles['is-selected'] : '')}
key={index}
onClick={() => selectItem(index)}
>
{item.fallbackImage ? <img src={item.fallbackImage} /> : item.emoji}:{item.name}:
</button>
</span>
))
) : (
<div className={styles.item}></div>

View File

@ -66,13 +66,13 @@ export const MentionList: React.FC<IProps> = forwardRef((props, ref) => {
<div ref={$container}>
{props.items.length ? (
props.items.map((item, index) => (
<button
<span
className={cls(styles.item, index === selectedIndex ? styles['is-selected'] : '')}
key={index}
onClick={() => selectItem(index)}
>
{item}
</button>
</span>
))
) : (
<div className={styles.item}></div>

View File

@ -5,10 +5,6 @@
overflow-y: auto;
border-radius: var(--border-radius);
box-shadow: rgb(9 30 66 / 31%) 0 0 1px, rgb(9 30 66 / 25%) 0 4px 8px -2px;
&::-webkit-scrollbar {
background-color: #fff !important;
}
}
.item {
@ -21,7 +17,6 @@
background-color: rgb(255 255 255);
border: 0;
border-radius: 0;
outline: none;
flex: 0 0 auto;
align-items: center;

View File

@ -72,13 +72,13 @@ export const MenuList: React.FC<IProps> = forwardRef((props, ref) => {
<div ref={$container}>
{props.items.length ? (
props.items.map((item, index) => (
<button
<span
className={cls(styles.item, index === selectedIndex ? styles['is-selected'] : '')}
key={index}
onClick={() => selectItem(index)}
>
{typeof item.label === 'function' ? item.label(props.editor) : item.label}
</button>
</span>
))
) : (
<div className={styles.item}></div>

View File

@ -2,7 +2,6 @@ import React, { useEffect, forwardRef, useImperativeHandle, useRef, useMemo } fr
import { Toast, BackTop } from '@douyinfe/semi-ui';
import { HocuspocusProvider } from '@hocuspocus/provider';
import cls from 'classnames';
import { debounce } from 'helpers/debounce';
import { useNetwork } from 'hooks/use-network';
import { useToggle } from 'hooks/use-toggle';
import { useWindowSize } from 'hooks/use-window-size';
@ -11,6 +10,7 @@ import { Banner } from 'components/banner';
import { Collaboration } from 'tiptap/core/extensions/collaboration';
import { CollaborationCursor } from 'tiptap/core/extensions/collaboration-cursor';
import { getRandomColor } from 'helpers/color';
import { isAndroid, isIOS } from 'helpers/env';
import { useEditor, EditorContent } from '../../react';
import { CollaborationKit } from '../kit';
import { MenuBar } from './menubar';
@ -25,8 +25,30 @@ type IProps = Pick<
status: ProviderStatus;
};
function scrollEditor(editor) {
try {
/**
*
*/
setTimeout(() => {
try {
const element = editor.options.element;
// 脏代码:这里使用 parentElement 是和布局有关的,需要根据实际情况修改
const parentElement = element.parentNode as HTMLElement;
const nextScrollTop = element.scrollHeight;
parentElement.scrollTop = nextScrollTop;
} catch (e) {
//
}
}, 0);
} catch (e) {
//
}
}
export const EditorInstance = forwardRef((props: IProps, ref) => {
const { hocuspocusProvider, editable, user, onTitleUpdate, status, menubar, renderInEditorPortal } = props;
const $headerContainer = useRef<HTMLDivElement>();
const $mainContainer = useRef<HTMLDivElement>();
const { isMobile } = useWindowSize();
const { online } = useNetwork();
@ -49,16 +71,24 @@ export const EditorInstance = forwardRef((props: IProps, ref) => {
},
}),
].filter(Boolean),
onTransaction: debounce(({ transaction }) => {
onTransaction({ transaction }) {
try {
const title = transaction.doc.content.firstChild.content.firstChild.textContent;
onTitleUpdate(title);
} catch (e) {
//
}
}, 50),
if (editable) {
scrollEditor(this);
}
},
onCreate() {
toggleCreated(true);
if (editable) {
scrollEditor(this);
}
},
onDestroy() {},
},
@ -89,6 +119,46 @@ export const EditorInstance = forwardRef((props: IProps, ref) => {
};
}, []);
// 监听键盘收起、打开
useEffect(() => {
let cleanUp = () => {};
const focusIn = () => {
setTimeout(() => {
if (!$headerContainer.current) return;
$headerContainer.current.classList.add(styles.keyUp);
$headerContainer.current.scrollIntoView();
}, 200);
};
const focusOut = () => {
if (!$headerContainer.current) return;
$headerContainer.current.classList.remove(styles.iOSKeyUp);
};
if (isIOS()) {
document.body.addEventListener('focusin', focusIn);
document.body.addEventListener('focusout', focusOut);
cleanUp = () => {
document.body.removeEventListener('focusin', focusIn);
document.body.removeEventListener('focusout', focusOut);
};
} else if (isAndroid) {
const originalHeight = document.documentElement.clientHeight || document.body.clientHeight;
window.onresize = function () {
//键盘弹起与隐藏都会引起窗口的高度发生变化
const resizeHeight = document.documentElement.clientHeight || document.body.clientHeight;
if (resizeHeight < originalHeight) {
focusIn();
} else {
focusOut();
}
};
}
return () => {
cleanUp();
};
}, []);
return (
<>
{(!online || status === 'disconnected') && (
@ -102,7 +172,7 @@ export const EditorInstance = forwardRef((props: IProps, ref) => {
<Banner type="warning" description="您没有编辑权限,暂不能编辑该文档。" closeable={false} />
)}
{menubar && (
<header className={cls(isMobile && styles.mobileToolbar)}>
<header className={cls(isMobile && styles.mobileToolbar)} ref={$headerContainer}>
<MenuBar editor={editor} />
</header>
)}

View File

@ -34,6 +34,10 @@
justify-content: space-around;
align-items: center;
border-top: 1px solid var(--semi-color-border);
&.keyUp {
position: absolute;
}
}
}

View File

@ -34,7 +34,7 @@ export const CollaborationEditor = forwardRef((props: ICollaborationEditorProps,
const hocuspocusProvider = useMemo(() => {
return new HocuspocusProvider({
url: process.env.COLLABORATION_API_URL,
url: 'ws://192.168.31.124:5003' || process.env.COLLABORATION_API_URL,
name: documentId,
token: (user && user.token) || 'read-public',
parameters: {

View File

@ -27,6 +27,7 @@ import { OrderedList } from 'tiptap/core/extensions/ordered-list';
import { Paragraph } from 'tiptap/core/extensions/paragraph';
import { Placeholder } from 'tiptap/core/extensions/placeholder';
import { SelectionExtension } from 'tiptap/core/extensions/selection';
import { ScrollIntoView } from 'tiptap/core/extensions/scroll-into-view';
import { Strike } from 'tiptap/core/extensions/strike';
import { Subscript } from 'tiptap/core/extensions/subscript';
import { Superscript } from 'tiptap/core/extensions/superscript';
@ -106,6 +107,7 @@ export const CollaborationKit = [
Loading,
OrderedList,
SelectionExtension,
ScrollIntoView,
Strike,
Subscript,
Superscript,
@ -139,6 +141,5 @@ export const CollaborationKit = [
SearchNReplace,
Status,
Title,
// Document,
DocumentWithTitle,
];

View File

@ -28,6 +28,7 @@ import { OrderedList } from 'tiptap/core/extensions/ordered-list';
import { Paragraph } from 'tiptap/core/extensions/paragraph';
import { Placeholder } from 'tiptap/core/extensions/placeholder';
import { Strike } from 'tiptap/core/extensions/strike';
import { ScrollIntoView } from 'tiptap/core/extensions/scroll-into-view';
import { Subscript } from 'tiptap/core/extensions/subscript';
import { Superscript } from 'tiptap/core/extensions/superscript';
import { Table } from 'tiptap/core/extensions/table';
@ -87,6 +88,7 @@ export const CommentKit = [
showOnlyWhenEditable: true,
}),
Strike,
ScrollIntoView,
Subscript,
Superscript,
Table,

View File

@ -6,5 +6,5 @@ import { triggerOpenCountSettingModal } from '../_event';
* @param editor
*/
export const createCountdown = (editor: Editor) => {
triggerOpenCountSettingModal(null);
triggerOpenCountSettingModal(editor, null);
};