mirror of https://github.com/fantasticit/think.git
Merge pull request #159 from lixiaoming-bit/feature/doc-fullscreen
This commit is contained in:
commit
20b6e537a8
|
@ -89,6 +89,7 @@
|
|||
"react": "17.0.2",
|
||||
"react-countdown": "^2.3.2",
|
||||
"react-dom": "17.0.2",
|
||||
"react-full-screen": "^1.1.1",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-lazy-load-image-component": "^1.5.4",
|
||||
"react-pdf": "^5.7.2",
|
||||
|
|
|
@ -0,0 +1,123 @@
|
|||
.wrap {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: auto;
|
||||
background-color: var(--semi-color-bg-0);
|
||||
}
|
||||
|
||||
.fullscreenContainer {
|
||||
position: fixed;
|
||||
background: var(--semi-color-bg-0);
|
||||
|
||||
.show {
|
||||
z-index: 2;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
z-index: -1;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.fullscreenContent {
|
||||
position: relative;
|
||||
min-width: 100%;
|
||||
min-height: 100%;
|
||||
|
||||
.header {
|
||||
position: fixed;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 60px;
|
||||
padding: 0 24px;
|
||||
background-color: var(--semi-color-bg-0);
|
||||
opacity: 0.9;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: auto;
|
||||
height: 100vh;
|
||||
min-height: 680px;
|
||||
padding: 0 120px;
|
||||
margin: 0 auto;
|
||||
overflow: auto;
|
||||
letter-spacing: 0.05em;
|
||||
background: var(--semi-color-bg-1);
|
||||
transition: width 0.3s linear, padding 0.3s linear;
|
||||
|
||||
.title {
|
||||
display: table-cell;
|
||||
height: 100vh;
|
||||
font-size: 4rem;
|
||||
line-height: 4.6rem;
|
||||
color: var(--semi-color-text-0);
|
||||
text-align: left;
|
||||
word-wrap: break-word;
|
||||
vertical-align: middle;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.node-title.is-empty {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fullScreenToolbar {
|
||||
position: fixed;
|
||||
bottom: 40px;
|
||||
left: 50%;
|
||||
z-index: 2;
|
||||
padding: 12px;
|
||||
background-color: rgb(var(--semi-grey-9));
|
||||
border-radius: var(--semi-border-radius-medium);
|
||||
opacity: 0;
|
||||
transform: translateX(-50%);
|
||||
transition: opacity 0.3s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
transition: opacity 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.selected {
|
||||
background-color: rgb(var(--semi-grey-8)) !important;
|
||||
}
|
||||
|
||||
.customButton {
|
||||
display: flex;
|
||||
height: 32px;
|
||||
min-width: 32px;
|
||||
padding: 0 8px;
|
||||
font-size: 14px;
|
||||
color: rgb(var(--semi-grey-0));
|
||||
cursor: pointer;
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: 2px;
|
||||
outline: none;
|
||||
align-items: center;
|
||||
|
||||
&:hover {
|
||||
background-color: rgb(var(--semi-grey-8));
|
||||
}
|
||||
}
|
||||
|
||||
.divider {
|
||||
width: 1px;
|
||||
height: 1em;
|
||||
background: rgb(var(--semi-grey-0));
|
||||
}
|
||||
}
|
||||
|
||||
.drawingCursor {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 999999;
|
||||
}
|
|
@ -0,0 +1,122 @@
|
|||
import { IconShrinkScreenStroked } from '@douyinfe/semi-icons';
|
||||
import { Button, Space, Tooltip, Typography } from '@douyinfe/semi-ui';
|
||||
import { EditorContent, useEditor } from '@tiptap/react';
|
||||
import cls from 'classnames';
|
||||
import { IconFullscreen } from 'components/icons/IconFullscreen';
|
||||
import { IconPencil } from 'components/icons/IconPencil';
|
||||
import { safeJSONParse } from 'helpers/json';
|
||||
import { useDrawingCursor } from 'hooks/use-cursor';
|
||||
import { useToggle } from 'hooks/use-toggle';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { FullScreen, useFullScreenHandle } from 'react-full-screen';
|
||||
import { CollaborationKit } from 'tiptap/editor';
|
||||
|
||||
import styles from './index.module.scss';
|
||||
|
||||
// 控制器
|
||||
const FullscreenController = ({ handle: fullscreenHandler, isDrawing, toggleDrawing }) => {
|
||||
const startDrawing = useCallback(() => {
|
||||
toggleDrawing(!isDrawing);
|
||||
}, [isDrawing, toggleDrawing]);
|
||||
|
||||
const close = useCallback(() => {
|
||||
fullscreenHandler.exit();
|
||||
toggleDrawing(false);
|
||||
}, [fullscreenHandler, toggleDrawing]);
|
||||
return (
|
||||
<div className={styles.fullScreenToolbar}>
|
||||
<Space>
|
||||
<button type="button" className={cls(styles.customButton, isDrawing && styles.selected)} onClick={startDrawing}>
|
||||
<IconPencil />
|
||||
</button>
|
||||
<div className={styles.divider}></div>
|
||||
<button type="button" className={styles.customButton} onClick={close}>
|
||||
<IconShrinkScreenStroked style={{ rotate: '90deg' }} />
|
||||
</button>
|
||||
</Space>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 画笔
|
||||
const DrawingCursor = ({ isDrawing }) => {
|
||||
useDrawingCursor(isDrawing);
|
||||
return isDrawing && <div className={cls(styles.drawingCursor, 'drawing-cursor')}></div>;
|
||||
};
|
||||
|
||||
interface IProps {
|
||||
data?: any;
|
||||
}
|
||||
|
||||
// 全屏按钮
|
||||
export const DocumentFullscreen: React.FC<IProps> = ({ data }) => {
|
||||
const fullscreenHandler = useFullScreenHandle();
|
||||
const [visible, toggleVisible] = useToggle(false);
|
||||
const [isDrawing, toggleDrawing] = useToggle(false);
|
||||
|
||||
const editor = useEditor({
|
||||
editable: false,
|
||||
extensions: CollaborationKit,
|
||||
content: { type: 'doc', content: [] },
|
||||
});
|
||||
|
||||
const startPowerpoint = () => {
|
||||
toggleVisible(true);
|
||||
fullscreenHandler.enter();
|
||||
};
|
||||
|
||||
const fullscreenChange = useCallback(
|
||||
(state) => {
|
||||
if (!state) {
|
||||
toggleVisible(false);
|
||||
toggleDrawing(false);
|
||||
}
|
||||
},
|
||||
[toggleVisible, toggleDrawing]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor) return;
|
||||
const docJSON = safeJSONParse(data.content, { default: {} }).default;
|
||||
docJSON.content = docJSON.content.filter((item) => item.type !== 'title');
|
||||
editor.commands.setContent(docJSON);
|
||||
}, [editor, data]);
|
||||
|
||||
const { Title } = Typography;
|
||||
return (
|
||||
<Tooltip content="演示文档" position="bottom">
|
||||
<Button
|
||||
type="tertiary"
|
||||
icon={<IconFullscreen />}
|
||||
theme="borderless"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
startPowerpoint();
|
||||
}}
|
||||
/>
|
||||
|
||||
<FullScreen
|
||||
handle={fullscreenHandler}
|
||||
onChange={fullscreenChange}
|
||||
className={cls(styles.fullscreenContainer, visible ? styles.show : styles.hidden)}
|
||||
>
|
||||
{visible && (
|
||||
<div className={styles.fullscreenContent}>
|
||||
<div className={styles.header}>
|
||||
<Title heading={4} type="tertiary">
|
||||
{data.title}
|
||||
</Title>
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.title}>{data.title || '未命名文档'}</div>
|
||||
<EditorContent editor={editor} />
|
||||
</div>
|
||||
<DrawingCursor isDrawing={isDrawing} />
|
||||
<FullscreenController handle={fullscreenHandler} isDrawing={isDrawing} toggleDrawing={toggleDrawing} />
|
||||
</div>
|
||||
)}
|
||||
</FullScreen>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
|
@ -19,6 +19,7 @@ import { createPortal } from 'react-dom';
|
|||
import { CollaborationEditor } from 'tiptap/editor';
|
||||
|
||||
import { DocumentActions } from '../actions';
|
||||
import { DocumentFullscreen } from '../fullscreen';
|
||||
import { Author } from './author';
|
||||
import styles from './index.module.scss';
|
||||
|
||||
|
@ -73,6 +74,7 @@ export const DocumentReader: React.FC<IProps> = ({ documentId }) => {
|
|||
documentId={documentId}
|
||||
/>
|
||||
)}
|
||||
{document && !isMobile && <DocumentFullscreen data={document} />}
|
||||
{document && (
|
||||
<DocumentStar
|
||||
disabled={!readable}
|
||||
|
@ -96,7 +98,7 @@ export const DocumentReader: React.FC<IProps> = ({ documentId }) => {
|
|||
<DocumentVersion documentId={documentId} />
|
||||
</Space>
|
||||
);
|
||||
}, [document, documentId, readable, editable, gotoEdit]);
|
||||
}, [document, documentId, readable, editable, gotoEdit, isMobile]);
|
||||
|
||||
return (
|
||||
<div className={styles.wrap}>
|
||||
|
|
|
@ -23,6 +23,7 @@ import styles from './index.module.scss';
|
|||
import Router from 'next/router';
|
||||
import { useRouterQuery } from 'hooks/use-router-query';
|
||||
import { IDocument, IWiki } from '@think/domains';
|
||||
import { DocumentFullscreen } from 'components/document/fullscreen';
|
||||
|
||||
const { Header } = Layout;
|
||||
const { Text } = Typography;
|
||||
|
@ -148,6 +149,7 @@ export const DocumentPublicReader: React.FC<IProps> = ({ documentId, hideLogo =
|
|||
}
|
||||
footer={
|
||||
<Space>
|
||||
{!isMobile && <DocumentFullscreen data={data}/>}
|
||||
<Tooltip content={currentWikiId ? '独立模式' : '嵌入模式'}>
|
||||
<Button theme="borderless" type="tertiary" icon={<IconRoute />} onClick={toPublicWikiOrDocumentURL} />
|
||||
</Tooltip>
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
import { Icon } from '@douyinfe/semi-ui';
|
||||
|
||||
export const IconFullscreen: React.FC<{ style?: React.CSSProperties }> = ({ style = {} }) => {
|
||||
return (
|
||||
<Icon
|
||||
style={style}
|
||||
svg={
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1.2em" height="1.2em" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M3 3a1 1 0 000 2v8a2 2 0 002 2h2.586l-1.293 1.293a1 1 0 101.414 1.414L10 15.414l2.293 2.293a1 1 0 001.414-1.414L12.414 15H15a2 2 0 002-2V5a1 1 0 100-2H3zm11 4a1 1 0 10-2 0v4a1 1 0 102 0V7zm-3 1a1 1 0 10-2 0v3a1 1 0 102 0V8zM8 9a1 1 0 00-2 0v2a1 1 0 102 0V9z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,26 @@
|
|||
import { Icon } from '@douyinfe/semi-ui';
|
||||
|
||||
export const IconPencil: React.FC<{ style?: React.CSSProperties }> = ({ style = {} }) => {
|
||||
return (
|
||||
<Icon
|
||||
style={style}
|
||||
svg={
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em"
|
||||
height="1em"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"
|
||||
/>
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,107 @@
|
|||
import { useEffect } from 'react';
|
||||
|
||||
let timer;
|
||||
let cursor = { x: 0, y: 0 };
|
||||
let particles = [];
|
||||
|
||||
const applyProperties = (target, properties) => {
|
||||
for (const key in properties) {
|
||||
target.style[key] = properties[key];
|
||||
}
|
||||
};
|
||||
|
||||
// Particles
|
||||
class Particle {
|
||||
lifeSpan: number;
|
||||
initialStyles: any;
|
||||
velocity: { x: number; y: number };
|
||||
position: { x: number; y: number };
|
||||
element: HTMLElement;
|
||||
constructor() {
|
||||
this.lifeSpan = 20; //ms
|
||||
this.initialStyles = {
|
||||
'position': 'fixed',
|
||||
'display': 'block',
|
||||
'width': '12px',
|
||||
'height': '12px',
|
||||
'border-radius': '50%',
|
||||
'pointer-events': 'none',
|
||||
'backgroundColor': '#D61C11',
|
||||
'will-change': 'transform',
|
||||
'z-index': '9999999',
|
||||
};
|
||||
}
|
||||
init(x, y) {
|
||||
this.position = { x: x - 6, y: y - 6 };
|
||||
|
||||
this.element = document.createElement('span');
|
||||
applyProperties(this.element, this.initialStyles);
|
||||
this.update();
|
||||
|
||||
document.querySelector('.drawing-cursor').appendChild(this.element);
|
||||
}
|
||||
update() {
|
||||
this.lifeSpan--;
|
||||
|
||||
this.element.style.transform =
|
||||
'translate3d(' + this.position.x + 'px,' + this.position.y + 'px, 0) scale(' + this.lifeSpan / 20 + ')';
|
||||
this.element.style.opacity = '0.5';
|
||||
}
|
||||
destroy() {
|
||||
this.element.parentNode.removeChild(this.element);
|
||||
}
|
||||
}
|
||||
|
||||
const addParticle = (x, y) => {
|
||||
const particle = new Particle();
|
||||
particle.init(x, y);
|
||||
particles.push(particle);
|
||||
};
|
||||
|
||||
const onMousemove = (e) => {
|
||||
cursor.x = e.clientX;
|
||||
cursor.y = e.clientY;
|
||||
|
||||
addParticle(cursor.x, cursor.y);
|
||||
};
|
||||
|
||||
const updateParticles = () => {
|
||||
// Updated particles
|
||||
for (let i = 0; i < particles.length; i++) {
|
||||
particles[i].update();
|
||||
}
|
||||
// Remove dead particles
|
||||
for (let i = particles.length - 1; i >= 0; i--) {
|
||||
if (particles[i].lifeSpan < 0) {
|
||||
particles[i].destroy();
|
||||
particles.splice(i, 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
const loop = () => {
|
||||
timer = requestAnimationFrame(loop);
|
||||
updateParticles();
|
||||
};
|
||||
|
||||
const destroyDrawingCursor = () => {
|
||||
document.removeEventListener('mousemove', onMousemove);
|
||||
cancelAnimationFrame(timer);
|
||||
cursor = { x: 0, y: 0 };
|
||||
particles = [];
|
||||
};
|
||||
const mountDrawingCursor = () => {
|
||||
document.querySelector('.drawing-cursor').innerHTML = '';
|
||||
document.addEventListener('mousemove', onMousemove);
|
||||
loop();
|
||||
};
|
||||
|
||||
export const useDrawingCursor = (isDrawing) => {
|
||||
useEffect(() => {
|
||||
if (isDrawing) {
|
||||
mountDrawingCursor();
|
||||
}
|
||||
return () => {
|
||||
destroyDrawingCursor();
|
||||
};
|
||||
}, [isDrawing]);
|
||||
};
|
|
@ -9,18 +9,16 @@
|
|||
|
||||
&::before {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
top: 0;
|
||||
height: 0;
|
||||
color: var(--semi-color-text-0);
|
||||
pointer-events: none;
|
||||
content: attr(data-placeholder);
|
||||
transform: translateY(-4.2em);
|
||||
}
|
||||
|
||||
&.is-editable {
|
||||
&::before {
|
||||
color: #aaa;
|
||||
transform: translateY(-1.7em);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -135,6 +135,7 @@ importers:
|
|||
react: 17.0.2
|
||||
react-countdown: ^2.3.2
|
||||
react-dom: 17.0.2
|
||||
react-full-screen: ^1.1.1
|
||||
react-helmet: ^6.1.0
|
||||
react-lazy-load-image-component: ^1.5.4
|
||||
react-pdf: ^5.7.2
|
||||
|
@ -234,6 +235,7 @@ importers:
|
|||
react: 17.0.2
|
||||
react-countdown: 2.3.2_sfoxds7t5ydpegc3knd667wn6m
|
||||
react-dom: 17.0.2_react@17.0.2
|
||||
react-full-screen: 1.1.1_react@17.0.2
|
||||
react-helmet: 6.1.0_react@17.0.2
|
||||
react-lazy-load-image-component: 1.5.4_sfoxds7t5ydpegc3knd667wn6m
|
||||
react-pdf: 5.7.2_sfoxds7t5ydpegc3knd667wn6m
|
||||
|
@ -6392,6 +6394,10 @@ packages:
|
|||
/fs.realpath/1.0.0:
|
||||
resolution: {integrity: sha1-FQStJSMVjKpA20onh8sBQRmU6k8=}
|
||||
|
||||
/fscreen/1.2.0:
|
||||
resolution: {integrity: sha512-hlq4+BU0hlPmwsFjwGGzZ+OZ9N/wq9Ljg/sq3pX+2CD7hrJsX9tJgWWK/wiNTFM212CLHWhicOoqwXyZGGetJg==}
|
||||
dev: false
|
||||
|
||||
/fsevents/2.3.2:
|
||||
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
|
@ -9740,6 +9746,16 @@ packages:
|
|||
resolution: {integrity: sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==}
|
||||
dev: false
|
||||
|
||||
/react-full-screen/1.1.1_react@17.0.2:
|
||||
resolution: {integrity: sha512-xoEgkoTiN0dw9cjYYGViiMCBYbkS97BYb4bHPhQVWXj1UnOs8PZ1rPzpX+2HMhuvQV1jA5AF9GaRbO3fA5aZtg==}
|
||||
engines: {node: '>=10'}
|
||||
peerDependencies:
|
||||
react: '>= 16.8.0'
|
||||
dependencies:
|
||||
fscreen: 1.2.0
|
||||
react: 17.0.2
|
||||
dev: false
|
||||
|
||||
/react-helmet/6.1.0_react@17.0.2:
|
||||
resolution: {integrity: sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw==}
|
||||
peerDependencies:
|
||||
|
|
Loading…
Reference in New Issue