mirror of https://github.com/fantasticit/think.git
feat: imporve editor mobile ux
This commit is contained in:
parent
ef61f1bdf3
commit
12379571cb
|
@ -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);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
||||
|
|
|
@ -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),
|
||||
}),
|
||||
];
|
||||
},
|
||||
});
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
@ -34,6 +34,10 @@
|
|||
justify-content: space-around;
|
||||
align-items: center;
|
||||
border-top: 1px solid var(--semi-color-border);
|
||||
|
||||
&.keyUp {
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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,
|
||||
];
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -6,5 +6,5 @@ import { triggerOpenCountSettingModal } from '../_event';
|
|||
* @param editor
|
||||
*/
|
||||
export const createCountdown = (editor: Editor) => {
|
||||
triggerOpenCountSettingModal(null);
|
||||
triggerOpenCountSettingModal(editor, null);
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue