Merge pull request #33 from fantasticit/feat/mobile

This commit is contained in:
fantasticit 2022-05-04 17:21:37 +08:00 committed by GitHub
commit dab13b20f8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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; let timer;
return (...args) => { return (...args) => {
if (!timer) {
func.apply(this, args);
}
clearTimeout(timer); clearTimeout(timer);
timer = setTimeout(() => { timer = setTimeout(() => {
func.apply(this, args); timer = undefined;
}, timeout); }, 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) => { export const createKeysLocalStorageLRUCache = (storageKey, capacity) => {
const lruCache = new LRUCache(capacity); const lruCache = new LRUCache(capacity);
if (USED_STORAGE_KEYS.includes(storageKey)) { if (CacheMap.has(storageKey)) {
// @ts-ignore return CacheMap.get(setStorage);
if (module.hot) {
console.error(`Storage Key ${storageKey} has been used!`);
return;
} else {
throw new Error(`Storage Key ${storageKey} has been used!`);
}
} }
USED_STORAGE_KEYS.push(storageKey); const manager = {
return {
syncFromStorage() { syncFromStorage() {
const data = getStorage(storageKey) || []; const data = getStorage(storageKey) || [];
data data
@ -152,4 +144,8 @@ export const createKeysLocalStorageLRUCache = (storageKey, capacity) => {
return key ? lruCache.get(key) : lruCache.keys(); 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'; import Router from 'next/router';
export const HttpClient = axios.create({ 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, 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}> <div ref={$container}>
{props.items.length ? ( {props.items.length ? (
props.items.map((item, index) => ( props.items.map((item, index) => (
<button <span
className={cls(styles.item, index === selectedIndex ? styles['is-selected'] : '')} className={cls(styles.item, index === selectedIndex ? styles['is-selected'] : '')}
key={index} key={index}
onClick={() => selectItem(index)} onClick={() => selectItem(index)}
> >
{item.fallbackImage ? <img src={item.fallbackImage} /> : item.emoji}:{item.name}: {item.fallbackImage ? <img src={item.fallbackImage} /> : item.emoji}:{item.name}:
</button> </span>
)) ))
) : ( ) : (
<div className={styles.item}></div> <div className={styles.item}></div>

View File

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

View File

@ -5,10 +5,6 @@
overflow-y: auto; overflow-y: auto;
border-radius: var(--border-radius); border-radius: var(--border-radius);
box-shadow: rgb(9 30 66 / 31%) 0 0 1px, rgb(9 30 66 / 25%) 0 4px 8px -2px; 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 { .item {
@ -21,7 +17,6 @@
background-color: rgb(255 255 255); background-color: rgb(255 255 255);
border: 0; border: 0;
border-radius: 0; border-radius: 0;
outline: none;
flex: 0 0 auto; flex: 0 0 auto;
align-items: center; align-items: center;

View File

@ -72,13 +72,13 @@ export const MenuList: React.FC<IProps> = forwardRef((props, ref) => {
<div ref={$container}> <div ref={$container}>
{props.items.length ? ( {props.items.length ? (
props.items.map((item, index) => ( props.items.map((item, index) => (
<button <span
className={cls(styles.item, index === selectedIndex ? styles['is-selected'] : '')} className={cls(styles.item, index === selectedIndex ? styles['is-selected'] : '')}
key={index} key={index}
onClick={() => selectItem(index)} onClick={() => selectItem(index)}
> >
{typeof item.label === 'function' ? item.label(props.editor) : item.label} {typeof item.label === 'function' ? item.label(props.editor) : item.label}
</button> </span>
)) ))
) : ( ) : (
<div className={styles.item}></div> <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 { Toast, BackTop } from '@douyinfe/semi-ui';
import { HocuspocusProvider } from '@hocuspocus/provider'; import { HocuspocusProvider } from '@hocuspocus/provider';
import cls from 'classnames'; import cls from 'classnames';
import { debounce } from 'helpers/debounce';
import { useNetwork } from 'hooks/use-network'; import { useNetwork } from 'hooks/use-network';
import { useToggle } from 'hooks/use-toggle'; import { useToggle } from 'hooks/use-toggle';
import { useWindowSize } from 'hooks/use-window-size'; import { useWindowSize } from 'hooks/use-window-size';
@ -11,6 +10,7 @@ import { Banner } from 'components/banner';
import { Collaboration } from 'tiptap/core/extensions/collaboration'; import { Collaboration } from 'tiptap/core/extensions/collaboration';
import { CollaborationCursor } from 'tiptap/core/extensions/collaboration-cursor'; import { CollaborationCursor } from 'tiptap/core/extensions/collaboration-cursor';
import { getRandomColor } from 'helpers/color'; import { getRandomColor } from 'helpers/color';
import { isAndroid, isIOS } from 'helpers/env';
import { useEditor, EditorContent } from '../../react'; import { useEditor, EditorContent } from '../../react';
import { CollaborationKit } from '../kit'; import { CollaborationKit } from '../kit';
import { MenuBar } from './menubar'; import { MenuBar } from './menubar';
@ -25,8 +25,30 @@ type IProps = Pick<
status: ProviderStatus; 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) => { export const EditorInstance = forwardRef((props: IProps, ref) => {
const { hocuspocusProvider, editable, user, onTitleUpdate, status, menubar, renderInEditorPortal } = props; const { hocuspocusProvider, editable, user, onTitleUpdate, status, menubar, renderInEditorPortal } = props;
const $headerContainer = useRef<HTMLDivElement>();
const $mainContainer = useRef<HTMLDivElement>(); const $mainContainer = useRef<HTMLDivElement>();
const { isMobile } = useWindowSize(); const { isMobile } = useWindowSize();
const { online } = useNetwork(); const { online } = useNetwork();
@ -49,16 +71,24 @@ export const EditorInstance = forwardRef((props: IProps, ref) => {
}, },
}), }),
].filter(Boolean), ].filter(Boolean),
onTransaction: debounce(({ transaction }) => { onTransaction({ transaction }) {
try { try {
const title = transaction.doc.content.firstChild.content.firstChild.textContent; const title = transaction.doc.content.firstChild.content.firstChild.textContent;
onTitleUpdate(title); onTitleUpdate(title);
} catch (e) { } catch (e) {
// //
} }
}, 50),
if (editable) {
scrollEditor(this);
}
},
onCreate() { onCreate() {
toggleCreated(true); toggleCreated(true);
if (editable) {
scrollEditor(this);
}
}, },
onDestroy() {}, 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 ( return (
<> <>
{(!online || status === 'disconnected') && ( {(!online || status === 'disconnected') && (
@ -102,7 +172,7 @@ export const EditorInstance = forwardRef((props: IProps, ref) => {
<Banner type="warning" description="您没有编辑权限,暂不能编辑该文档。" closeable={false} /> <Banner type="warning" description="您没有编辑权限,暂不能编辑该文档。" closeable={false} />
)} )}
{menubar && ( {menubar && (
<header className={cls(isMobile && styles.mobileToolbar)}> <header className={cls(isMobile && styles.mobileToolbar)} ref={$headerContainer}>
<MenuBar editor={editor} /> <MenuBar editor={editor} />
</header> </header>
)} )}

View File

@ -34,6 +34,10 @@
justify-content: space-around; justify-content: space-around;
align-items: center; align-items: center;
border-top: 1px solid var(--semi-color-border); 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(() => { const hocuspocusProvider = useMemo(() => {
return new HocuspocusProvider({ return new HocuspocusProvider({
url: process.env.COLLABORATION_API_URL, url: 'ws://192.168.31.124:5003' || process.env.COLLABORATION_API_URL,
name: documentId, name: documentId,
token: (user && user.token) || 'read-public', token: (user && user.token) || 'read-public',
parameters: { parameters: {

View File

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

View File

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

View File

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