1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-07-08 20:54:47 +00:00

change viewer (#3115)

This commit is contained in:
Michael An 2019-03-15 12:11:32 +08:00 committed by Daniel Pan
parent 3e860e882a
commit db64e5a18e
9 changed files with 208 additions and 720 deletions

View File

@ -1,305 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { processor } from '@seafile/seafile-editor/dist/utils/seafile-markdown2html';
import { Button, Dropdown, DropdownToggle, DropdownMenu, DropdownItem } from 'reactstrap';
import Loading from '../loading';
import { gettext } from '../../utils/constants';
import moment from 'moment';
import '../../css/markdown-viewer/comments-list.css';
const propTypes = {
editorUtilities: PropTypes.object.isRequired,
scrollToQuote: PropTypes.func.isRequired,
getCommentsNumber: PropTypes.func.isRequired,
commentsNumber: PropTypes.number.isRequired,
};
class CommentsList extends React.Component {
constructor(props) {
super(props);
this.state = {
commentsList: [],
showResolvedComment: true,
};
}
listComments = () => {
this.props.editorUtilities.listComments().then((response) => {
this.setState({
commentsList: response.data.comments
});
});
}
handleCommentChange = (event) => {
this.setState({
comment: event.target.value,
});
}
submitComment = () => {
let comment = this.refs.commentTextarea.value;
if (comment.trim().length > 0) {
this.props.editorUtilities.postComment(comment.trim()).then((response) => {
this.listComments();
this.props.getCommentsNumber();
});
}
this.refs.commentTextarea.value = '';
}
resolveComment = (event) => {
this.props.editorUtilities.updateComment(event.target.id, 'true').then((response) => {
this.listComments();
});
}
deleteComment = (event) => {
this.props.editorUtilities.deleteComment(event.target.id).then((response) => {
this.props.getCommentsNumber();
this.listComments();
});
}
editComment = (commentID, newComment) => {
this.props.editorUtilities.updateComment(commentID, null, null, newComment).then((res) => {
this.props.getCommentsNumber();
this.listComments();
});
}
setQuoteText = (text) => {
if (text.length > 0) {
this.refs.commentTextarea.value = '> ' + text;
}
}
scrollToQuote = (detail) => {
this.props.scrollToQuote(detail);
this.refs.commentTextarea.value = '';
}
toggleResolvedComment = () => {
this.setState({
showResolvedComment: !this.state.showResolvedComment
});
}
componentWillMount() {
this.listComments();
}
componentWillReceiveProps(nextProps) {
if (this.props.commentsNumber !== nextProps.commentsNumber) {
this.listComments();
}
}
render() {
return (
<div className="seafile-comment">
<div className="seafile-comment-title">
<div className="seafile-comment-text">{gettext('Show resolved comments')}</div>
<div className="d-flex">
<label className="custom-switch" id="toggle-resolved-comments">
<input type="checkbox" name="option" className="custom-switch-input"
onChange={this.toggleResolvedComment}
checked={this.state.showResolvedComment && 'checked'}
/>
<span className="custom-switch-indicator"></span>
</label>
</div>
</div>
<ul className={'seafile-comment-list'}>
{ (this.state.commentsList.length > 0 && this.props.commentsNumber > 0) &&
this.state.commentsList.map((item, index = 0, arr) => {
let oldTime = (new Date(item.created_at)).getTime();
let time = moment(oldTime).format('YYYY-MM-DD HH:mm');
return (
<CommentItem
item={item} time={time} key={index}
editorUtilities={this.props.editorUtilities}
editComment={this.editComment}
showResolvedComment={this.state.showResolvedComment}
deleteComment={this.deleteComment} resolveComment={this.resolveComment}
commentsList={this.state.commentsList} scrollToQuote={this.scrollToQuote}
/>
);
})
}
{(this.state.commentsList.length == 0 && this.props.commentsNumber > 0) && <Loading/>}
{ this.props.commentsNumber == 0 &&
<li className="comment-vacant">{gettext('No comment yet.')}</li>
}
</ul>
<div className="seafile-comment-footer">
<div className="seafile-add-comment">
<textarea className="add-comment-input" ref="commentTextarea"
placeholder={gettext('Add a comment')}
clos="100" rows="3" warp="virtual"></textarea>
<Button className="submit-comment" color="success"
size="sm" onClick={this.submitComment} >
{gettext('Submit')}</Button>
</div>
</div>
</div>
);
}
}
CommentsList.propTypes = propTypes;
const CommentItempropTypes = {
editorUtilities: PropTypes.object.isRequired,
item: PropTypes.object,
time: PropTypes.string,
editComment: PropTypes.func,
showResolvedComment: PropTypes.bool,
deleteComment: PropTypes.func,
resolveComment: PropTypes.func,
commentsList: PropTypes.array,
scrollToQuote: PropTypes.func.isRequired,
};
class CommentItem extends React.Component {
constructor(props) {
super(props);
this.state = {
dropdownOpen: false,
html: '',
quote: '',
newComment: this.props.item.comment,
editable: false,
};
}
toggleDropDownMenu = () => {
this.setState({
dropdownOpen: !this.state.dropdownOpen,
});
}
convertComment = (item) => {
processor.process(item.comment).then(
(result) => {
let comment = String(result);
this.setState({
comment: comment
});
}
);
if (item.detail) {
const quote = JSON.parse(item.detail).quote;
processor.process(quote).then(
(result) => {
let quote = String(result);
this.setState({
quote: quote
});
}
);
}
}
toggleEditComment = () => {
this.setState({
editable: !this.state.editable
});
}
updateComment = (event) => {
const newComment = this.state.newComment;
if (this.props.item.comment !== newComment) {
this.props.editComment(event.target.id, newComment);
}
this.toggleEditComment();
}
handleCommentChange = (event) => {
this.setState({
newComment: event.target.value,
});
}
scrollToQuote = () => {
const position = JSON.parse(this.props.item.detail).position;
this.props.scrollToQuote(position);
}
componentWillMount() {
this.convertComment(this.props.item);
}
componentWillReceiveProps(nextProps) {
this.convertComment(nextProps.item);
}
render() {
const item = this.props.item;
const { id, user_email, avatar_url, user_name, resolved } = item;
if (item.resolved && !this.props.showResolvedComment) {
return null;
}
if (this.state.editable) {
return(
<li className="seafile-comment-item" id={item.id}>
<div className="seafile-comment-info">
<img className="avatar" src={avatar_url} alt=""/>
<div className="reviewer-info">
<div className="reviewer-name">{user_name}</div>
<div className="review-time">{this.props.time}</div>
</div>
</div>
<div className="seafile-edit-comment">
<textarea className="edit-comment-input" value={this.state.newComment} onChange={this.handleCommentChange} clos="100" rows="3" warp="virtual"></textarea>
<Button className="comment-btn" color="success" size="sm" onClick={this.updateComment} id={item.id}>{gettext('Update')}</Button>{' '}
<Button className="comment-btn" color="secondary" size="sm" onClick={this.toggleEditComment}>{gettext('Cancle')}</Button>
</div>
</li>
);
}
return (
<li className={resolved ? 'seafile-comment-item seafile-comment-item-resolved' : 'seafile-comment-item'} id={id}>
<div className="seafile-comment-info">
<img className="avatar" src={avatar_url} alt=""/>
<div className="reviewer-info">
<div className="reviewer-name">{user_name}</div>
<div className="review-time">{this.props.time}</div>
</div>
<Dropdown isOpen={this.state.dropdownOpen} size="sm"
className="seafile-comment-dropdown" toggle={this.toggleDropDownMenu}>
<DropdownToggle className="seafile-comment-dropdown-btn">
<i className="fas fa-ellipsis-v"></i>
</DropdownToggle>
<DropdownMenu>
{(user_email === this.props.editorUtilities.userName) &&
<DropdownItem onClick={this.props.deleteComment}
className="delete-comment" id={item.id}>{gettext('Delete')}</DropdownItem>
}
{(user_email === this.props.editorUtilities.userName) &&
<DropdownItem onClick={this.toggleEditComment}
className="edit-comment" id={item.id}>{gettext('Edit')}</DropdownItem>
}
{!resolved &&
<DropdownItem onClick={this.props.resolveComment} className="seafile-comment-resolved"
id={item.id}>{gettext('Mark as resolved')}</DropdownItem>
}
</DropdownMenu>
</Dropdown>
</div>
{item.detail &&
<blockquote className="seafile-comment-content">
<div onClick={this.scrollToQuote} dangerouslySetInnerHTML={{ __html: this.state.quote }}></div>
</blockquote>
}
<div className="seafile-comment-content" dangerouslySetInnerHTML={{ __html: this.state.comment }}></div>
</li>
);
}
}
CommentItem.propTypes = CommentItempropTypes;
export default CommentsList;

View File

@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
import axios from 'axios';
import Loading from '../loading';
import moment from 'moment';
import { gettext } from '../../utils/constants';
import '../../css/markdown-viewer/history-viewer.css';
const propTypes = {
@ -97,6 +98,12 @@ class HistoryList extends React.Component {
render() {
return (
<div className="seafile-history-side-panel">
<div className="seafile-history-title">
<div onClick={this.props.toggleHistoryPanel} className={'seafile-history-title-close'}>
<i className={'fa fa-times-circle'}/>
</div>
<div className={'seafile-history-title-text'}>{gettext('History Versions')}</div>
</div>
<ul onScroll={this.onScroll} className={'history-list-container'}>
{this.state.historyList ?
this.state.historyList.map((item, index = 0, arr) => {

View File

@ -1,164 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { Nav, NavItem, NavLink, TabContent, TabPane } from 'reactstrap';
import HistoryList from './history-list';
import CommentsList from './comments-list';
import OutlineView from './outline';
const URL = require('url-parse');
const propTypes = {
editorUtilities: PropTypes.object.isRequired,
markdownContent: PropTypes.string.isRequired,
commentsNumber: PropTypes.number.isRequired,
viewer: PropTypes.object.isRequired,
value: PropTypes.object.isRequired,
activeTab: PropTypes.string.isRequired,
showDiffViewer: PropTypes.func.isRequired,
setDiffViewerContent: PropTypes.func.isRequired,
reloadDiffContent: PropTypes.func.isRequired,
tabItemClick: PropTypes.func.isRequired,
getCommentsNumber: PropTypes.func.isRequired,
};
class MarkdownViewerSidePanel extends React.Component {
constructor(props) {
super(props);
}
tabItemClick = (tab) => {
this.props.tabItemClick(tab);
}
showNavItem = (showTab) => {
switch(showTab) {
case 'outline':
return (
<NavLink className={classnames({ active: this.props.activeTab === 'outline' })}
onClick={() => { this.tabItemClick('outline');}} ><i className="fa fa-list"></i>
</NavLink>
);
case 'comments':
return (
<NavLink className={classnames({ active: this.props.activeTab === 'comments' })}
onClick={() => {this.tabItemClick('comments');}}><i className="fa fa-comments"></i>
{this.props.commentsNumber > 0 && <div className='comments-number'>{this.props.commentsNumber}</div>}
</NavLink>
);
case 'history':
return (
<NavLink className={classnames({ active: this.props.activeTab === 'history' })}
onClick={() => { this.tabItemClick('history');}}><i className="fas fa-history"></i>
</NavLink>
);
}
}
renderNavItems = () => {
return (
<Nav tabs className="md-side-panel-nav">
<NavItem className="nav-item">{this.showNavItem('outline')}</NavItem>
<NavItem className="nav-item">{this.showNavItem('comments')}</NavItem>
<NavItem className="nav-item">{this.showNavItem('history')}</NavItem>
</Nav>
);
}
scrollToNode = (node) => {
let url = new URL(window.location.href);
url.set('hash', 'user-content-' + node.text);
window.location.href = url.toString();
}
findScrollContainer = (el, window) => {
let parent = el.parentNode;
const OVERFLOWS = ['auto', 'overlay', 'scroll'];
let scroller;
while (!scroller) {
if (!parent.parentNode) break;
const style = window.getComputedStyle(parent);
const { overflowY } = style;
if (OVERFLOWS.includes(overflowY)) {
scroller = parent;
break;
}
parent = parent.parentNode;
}
if (!scroller) {
return window.document.body;
}
return scroller;
}
scrollToQuote = (path) => {
if (!path) return;
const win = window;
if (path.length > 2) {
// deal with code block or chart
path[0] = path[0] > 1 ? path[0] - 1 : path[0] + 1;
path = path.slice(0, 1);
}
let node = this.props.value.document.getNode(path);
if (!node) {
path = path.slice(0, 1);
node = this.props.value.document.getNode(path);
}
if (node) {
let element = win.document.querySelector(`[data-key="${node.key}"]`);
while (element.tagName === 'CODE') {
element = element.parentNode;
}
const scroller = this.findScrollContainer(element, win);
const isWindow = scroller == win.document.body || scroller == win.document.documentElement;
if (isWindow) {
win.scrollTo(0, element.offsetTop);
} else {
scroller.scrollTop = element.offsetTop;
}
}
}
componentDidMount() {
this.tabItemClick('outline');
}
render() {
return (
<div className="seafile-md-viewer-side-panel">
{this.renderNavItems()}
<TabContent activeTab={this.props.activeTab}>
<TabPane tabId="outline" className="outline">
<OutlineView
isViewer={true}
document={this.props.value.document}
editor={this.props.viewer}
scrollToNode={this.scrollToNode}
/>
</TabPane>
<TabPane tabId="comments" className="comments">
<CommentsList
editorUtilities={this.props.editorUtilities}
scrollToQuote={this.scrollToQuote}
getCommentsNumber={this.props.getCommentsNumber}
commentsNumber={this.props.commentsNumber}
/>
</TabPane>
<TabPane tabId="history" className="history">
<HistoryList
editorUtilities={this.props.editorUtilities}
showDiffViewer={this.props.showDiffViewer}
setDiffViewerContent={this.props.setDiffViewerContent}
reloadDiffContent={this.props.reloadDiffContent}
/>
</TabPane>
</TabContent>
</div>
);
}
}
MarkdownViewerSidePanel.propTypes = propTypes;
export default MarkdownViewerSidePanel;

View File

@ -2,7 +2,6 @@ import React from 'react';
import PropTypes from 'prop-types';
import { gettext } from '../../utils/constants';
const OutlineItempropTypes = {
scrollToNode: PropTypes.func.isRequired,
node: PropTypes.object.isRequired,
@ -41,7 +40,6 @@ const propTypes = {
scrollToNode: PropTypes.func.isRequired,
isViewer: PropTypes.bool.isRequired,
document: PropTypes.object.isRequired,
editor: PropTypes.object.isRequired,
activeTitleIndex: PropTypes.number,
value: PropTypes.object,
};
@ -56,13 +54,13 @@ class OutlineView extends React.PureComponent {
return (
<div className="seafile-editor-outline">
<div className="seafile-editor-outline-heading">{gettext('Contents')}</div>
{headerList.size > 0 ?
headerList.map((node, index) => {
let active = (index === this.props.activeTitleIndex) ? ' active' : '';
return (
<OutlineItem
key={node.key}
editor={this.props.editor}
value={this.props.value}
node={node}
active={active}

View File

@ -18,6 +18,10 @@ const propTypes = {
toggleStar: PropTypes.func.isRequired,
backToParentDirectory: PropTypes.func.isRequired,
openDialogs: PropTypes.func.isRequired,
showFileHistory: PropTypes.bool.isRequired,
toggleHistory: PropTypes.func.isRequired,
commentsNumber: PropTypes.number.isRequired,
toggleCommentList: PropTypes.func.isRequired,
};
class MarkdownViewerToolbar extends React.Component {
@ -47,12 +51,25 @@ class MarkdownViewerToolbar extends React.Component {
<ButtonGroup>
<IconButton id={'shareBtn'} text={gettext('Share')} icon={'fa fa-share-alt'}
onMouseDown={this.props.toggleShareLinkDialog}/>
{
this.props.commentsNumber > 0 ?
<button className="btn btn-icon btn-secondary btn-active" id="commentsNumber" type="button"
data-active="false" onMouseDown={this.props.toggleCommentList}>
<i className="fa fa-comments"></i>{' '}<span>{this.props.commentsNumber}</span>
</button>
:
<IconButton id={'commentsNumber'} text={gettext('Comments')} icon={'fa fa-comments'} onMouseDown={this.props.toggleCommentList}/>
}
<IconButton text={gettext('Back to parent directory')} id={'parentDirectory'}
icon={'fa fa-folder-open'} onMouseDown={this.props.backToParentDirectory}/>
{
(!this.props.hasDraft && this.props.fileInfo.permission === 'rw')? <IconButton text={gettext('Edit')}
id={'editButton'} icon={'fa fa-edit'} onMouseDown={this.props.onEdit}/>: null
}
{
this.props.showFileHistory && <IconButton id={'historyButton'}
text={gettext('File history')} onMouseDown={this.props.toggleHistory} icon={'fa fa-history'}/>
}
</ButtonGroup>
</div>
</div>

View File

@ -4,7 +4,8 @@
flex-direction: column;
width: 29%;
}
.seafile-comment-title {
.seafile-comment-title,
.seafile-history-title {
border-bottom: 1px solid #e5e5e5;
min-height: 3em;
line-height: 3em;
@ -12,15 +13,18 @@
display: flex;
background-color: #fafaf9;
}
.seafile-comment-title .seafile-comment-title-text {
.seafile-comment-title .seafile-comment-title-text,
.seafile-history-title .seafile-history-title-text {
width: 100%;
text-align: center;
font-weight: 700;
}
.seafile-comment-title .seafile-comment-title-close {
.seafile-comment-title .seafile-comment-title-close,
.seafile-history-title .seafile-history-title-close {
color: #b9b9b9;
}
.seafile-comment-title .seafile-comment-title-close:hover {
.seafile-comment-title .seafile-comment-title-close:hover,
.seafile-history-title .seafile-history-title-close:hover {
color: #888;
}
.seafile-comment-toggle-resolved {

View File

@ -1,138 +0,0 @@
.seafile-comment {
background-color: #fff;
display: flex;
flex-direction: column;
flex: 0 0 auto;
min-height: 18.5em;
z-index: 3;
width: 380px;
}
.seafile-comment-title {
border-bottom: 1px solid #e5e5e5;
min-height: 2em;
line-height: 2em;
padding: 2px 1em;
display: flex;
flex-direction: row;
justify-content: space-between;
background-color: #fafaf9;
position: absolute;
z-index: 1;
width: 30%;
}
.seafile-comment-title .seafile-comment-title-close {
color: #b9b9b9;
}
.seafile-comment-title .seafile-comment-title-close:hover {
color: #888;
}
.seafile-comment-list {
height: calc(100% - 40px);
overflow-y: auto;
margin: 30px 0 120px;
}
.seafile-comment::-webkit-scrollbar {
display: none;
}
.seafile-comment-list .comment-vacant {
padding: 1em;
text-align: center;
}
.seafile-comment-item {
padding: 15px 10px;
margin-bottom: 0;
}
.seafile-comment-item .seafile-comment-info {
padding-bottom: 0.5em;
height: 3em;
display: flex;
justify-content: flex-start;
}
.seafile-comment-item .seafile-comment-info .reviewer-info {
padding-left: 10px;
}
.seafile-comment-item .seafile-comment-info .review-time {
font-size: 10px;
color: #777;
}
.seafile-comment-item .seafile-comment-info .seafile-comment-dropdown {
margin-left: auto;
}
.seafile-comment-item .seafile-comment-info .seafile-comment-dropdown button {
border: none;
box-shadow: none;
background-color: #fff;
}
.seafile-comment-item .seafile-comment-info .seafile-comment-dropdown .seafile-comment-dropdown-btn {
color: #999;
background-color: transparent;
}
.seafile-comment-item .seafile-comment-info .seafile-comment-dropdown:hover .seafile-comment-dropdown-btn {
color: #555;
background-color: transparent;
}
.seafile-comment-item .seafile-comment-info .seafile-comment-dropdown button:hover,
.seafile-comment-item .seafile-comment-info .seafile-comment-dropdown button:focus {
border: none;
box-shadow: none;
background-color: #eee;
}
.seafile-comment-item .seafile-comment-content {
margin-left: 42px;
}
.seafile-comment-item .seafile-comment-content ol,
.seafile-comment-item .seafile-comment-content ul,
.seafile-comment-item .seafile-comment-content li {
margin-left: 10px;
}
.seafile-comment-item .seafile-comment-content table,
.seafile-comment-item .seafile-comment-content th,
.seafile-comment-item .seafile-comment-content td {
border: 1px solid #333;
}
.seafile-comment-item blockquote {
cursor: pointer;
}
.seafile-comment-footer {
background-color: #fafaf9;
border-top: 1px solid #e5e5e5;
min-height: 120px;
position: absolute;
bottom: 0;
padding: 0;
width: 30%;
}
.seafile-comment-footer .seafile-add-comment {
margin: 10px 20px 5px 15px;
height: 100%;
width: 100%;
}
.seafile-add-comment .add-comment-input,
.seafile-edit-comment .edit-comment-input {
background-color: #fff;
border: 1px solid #e6e6dd;
padding: 5px;
min-height: 70px;
border-radius: 5px;
width: 100%;
}
.seafile-add-comment .add-comment-input {
height: calc(100% - 50px);
width: 92%;
max-height: 300px;
}
.seafile-comment-footer .seafile-add-comment .submit-comment {
margin-top: 5px;
width: 60px;
height: 28px;
}
.seafile-comment-item-resolved {
background-color: #e6ffed;
}
.seafile-comment-footer .seafile-add-comment .comment-btn,
.seafile-edit-comment .comment-btn {
height: 28px;
}
.seafile-edit-comment {
margin-top: 10px;
}

View File

@ -13,29 +13,52 @@
align-items: center;
}
.seafile-md-viewer-container {
width: 70%;
width: 100%;
background-color: #fafaf9;
overflow: hidden;
height: 100%;
}
.seafile-md-viewer-container:hover {
position: relative;
overflow: auto;
}
.seafile-md-viewer-container.side-panel-on {
width: 70%;
}
.seafile-md-viewer-slate {
flex: auto;
position: relative;
margin: 20px 40px;
margin-right: 30%;
}
.seafile-md-viewer-main {
flex:auto;
overflow:auto;
flex: auto;
overflow: auto;
background:#fafaf9;
width: 70%;
}
.seafile-editor-outline .active {
.seafile-md-viewer-slate.side-panel-on {
margin-right: 40px;
}
/* outline */
.seafile-md-viewer .seafile-editor-outline {
background-color: #fafaf9;
margin: 40px;
border-left: 0;
width: 20%;
position: fixed;
top: 68px;
overflow-y: auto;
right: 5%;
z-index: 1;
height: 80%;
}
.seafile-md-viewer .seafile-editor-outline .active {
color: #eb8205;
border-left: 1px solid #eb8205;
}
.seafile-md-viewer .seafile-editor-outline-heading {
padding: 7px 0;
border-bottom: 1px solid #eee;
color: #a0a0a0;
}
.seafile-editor-outline .outline-h2, .seafile-editor-outline .outline-h3 {
height: 30px;
margin-left: 0;
@ -52,71 +75,26 @@
}
/* side-panel */
.seafile-md-viewer-side-panel {
height: 100%;
overflow:hidden;
user-select: none;
width: 30%;
}
.seafile-md-viewer-side-panel .seafile-editor-outline {
border-left: 0;
}
.seafile-md-viewer-side-panel:hover {
overflow: auto;
}
.seafile-md-viewer-side-panel-heading {
padding:7px 0;
border-bottom: 1px solid #eee;
color: #a0a0a0;
}
.seafile-md-viewer-side-panel-content {
padding:8px 0;
font-size: 0.875rem;
}
.seafile-md-viewer-side-panel {
border-left: 1px solid #e6e6dd;
background-color: #fff;
height: 100%;
overflow: hidden;
width: 30%;
position: fixed;
right: 0;
top: 87px;
}
.seafile-md-viewer-side-panel .tab-content {
height: calc(100% - 39px);
overflow-y: auto;
overflow-x: hidden;
.seafile-md-viewer-side-panel .seafile-comment,
.seafile-md-viewer-side-panel .seafile-history-side-panel {
width: 100%;
height: 100%;
}
.seafile-md-viewer-side-panel .tab-content .outline {
padding: 20px;
.seafile-md-viewer-side-panel .seafile-comment .add-comment-input,
.seafile-md-viewer-side-panel .seafile-comment .edit-comment-input {
background-color: #fff;
width: 100%;
}
.seafile-md-viewer-side-panel .md-side-panel-nav {
margin: 0;
.seafile-md-viewer-side-panel .seafile-history-side-panel {
border-left: 1px solid #e6e6dd;
}
.md-side-panel-nav .nav-item {
width: 33.3%;
padding-top: 4px;
}
.md-side-panel-nav .nav-item .nav-link {
margin: 0 auto;
}
.md-side-panel-nav .nav-item i {
padding: 0 8px;
font-size: 1rem;
width: 1rem;
}
.comments-number {
font-size: 12px;
width: 16px;
height: 16px;
border-radius: 8px;
text-align: center;
line-height: 16px;
font-weight: 600;
background-color: #fd9644;
position: absolute;
top: 10%;
right: 30%;
color: #fff;
}
.seafile-viewer-comment-btn {
position: absolute;
top: 0;
@ -131,11 +109,13 @@
cursor: pointer;
background-color: #eee;
}
.seafile-md-viewer .seafile-comment .seafile-comment-footer {
min-height: 230px;
}
.seafile-md-viewer .seafile-comment-toggle-resolved {
width: 100%;
}
@media (max-width:991.8px) {
.seafile-md-viewer-side-panel {
display:none;
}
.seafile-editor-outline {
display: none;
}
@ -143,9 +123,4 @@
width: calc(100% - 80px);
margin: 20px 40px;
}
}
@media (min-width:992px) {
.seafile-md-viewer-side-panel {
width: 30%;
}
}

View File

@ -18,13 +18,16 @@ import { serialize, deserialize } from '@seafile/seafile-editor/dist/utils/slate
import LocalDraftDialog from '@seafile/seafile-editor/dist/components/local-draft-dialog';
import DiffViewer from '@seafile/seafile-editor/dist/viewer/diff-viewer';
import MarkdownViewerToolbar from './components/toolbar/markdown-viewer-toolbar';
import MarkdownViewerSidePanel from './components/markdown-view/markdown-viewer-side-panel';
import HistoryList from './components/markdown-view/history-list';
import CommentPanel from './components/file-view/comment-panel';
import OutlineView from './components/markdown-view/outline';
import Loading from './components/loading';
import { findRange } from '@seafile/slate-react';
import './css/markdown-viewer/markdown-editor.css';
const CryptoJS = require('crypto-js');
const URL = require('url-parse');
const { repoID, repoName, filePath, fileName, mode, draftID, isDraft, hasDraft } = window.app.pageOptions;
const { siteRoot, serviceUrl, seafileCollabServer } = window.app.config;
const userInfo = window.app.userInfo;
@ -300,9 +303,11 @@ class MarkdownEditor extends React.Component {
collabUsers: userInfo ?
[{user: userInfo, is_editing: false}] : [],
commentsNumber: null,
activeTab: 'outline',
loadingDiff: false,
value: null,
isShowComments: false,
isShowHistory: false,
isShowOutline: true,
};
if (this.state.collabServer) {
@ -721,17 +726,8 @@ class MarkdownEditor extends React.Component {
});
}
tabItemClick = (tab) => {
if (this.state.activeTab !== tab) {
this.setState({
activeTab: tab
});
}
}
setBtnPosition = (e) => {
let isShowComments = this.state.activeTab === 'comments' ? true : false;
if (!isShowComments) return;
if (!this.state.isShowComments) return;
const nativeSelection = window.getSelection();
if (!nativeSelection.rangeCount) {
this.range = null;
@ -855,9 +851,95 @@ class MarkdownEditor extends React.Component {
return newNodes;
}
scrollToNode = (node) => {
let url = new URL(window.location.href);
url.set('hash', 'user-content-' + node.text);
window.location.href = url.toString();
}
findScrollContainer = (el, window) => {
let parent = el.parentNode;
const OVERFLOWS = ['auto', 'overlay', 'scroll'];
let scroller;
while (!scroller) {
if (!parent.parentNode) break;
const style = window.getComputedStyle(parent);
const { overflowY } = style;
if (OVERFLOWS.includes(overflowY)) {
scroller = parent;
break;
}
parent = parent.parentNode;
}
if (!scroller) {
return window.document.body;
}
return scroller;
}
scrollToQuote = (path) => {
if (!path) return;
const win = window;
if (path.length > 2) {
// deal with code block or chart
path[0] = path[0] > 1 ? path[0] - 1 : path[0] + 1;
path = path.slice(0, 1);
}
let node = this.state.value.document.getNode(path);
if (!node) {
path = path.slice(0, 1);
node = this.state.value.document.getNode(path);
}
if (node) {
let element = win.document.querySelector(`[data-key="${node.key}"]`);
while (element.tagName === 'CODE') {
element = element.parentNode;
}
const scroller = this.findScrollContainer(element, win);
const isWindow = scroller == win.document.body || scroller == win.document.documentElement;
if (isWindow) {
win.scrollTo(0, element.offsetTop);
} else {
scroller.scrollTop = element.offsetTop;
}
}
}
toggleHistory = () => {
if (this.state.isShowHistory) {
this.setState({
isShowHistory: false,
isShowOutline: true,
isShowComments: false,
});
} else {
this.setState({
isShowHistory: true,
isShowOutline: false,
isShowComments: false,
});
}
}
toggleCommentList = () => {
if (this.state.isShowComments) {
this.setState({
isShowHistory: false,
isShowOutline: true,
isShowComments: false,
});
} else {
this.setState({
isShowHistory: false,
isShowOutline: false,
isShowComments: true,
});
}
}
render() {
let component;
let isShowComments = this.state.activeTab === 'comments' ? true : false;
let sidePanel = (this.state.isShowHistory || this.state.isShowComments) ? true : false;
if (this.state.loading) {
return (
<div className="empty-loading-page">
@ -882,11 +964,15 @@ class MarkdownEditor extends React.Component {
toggleShareLinkDialog={this.toggleShareLinkDialog}
onEdit={this.onEdit}
toggleNewDraft={editorUtilities.createDraftFile}
commentsNumber={this.state.commentsNumber}
toggleCommentList={this.toggleCommentList}
showFileHistory={true}
toggleHistory={this.toggleHistory}
/>
<div className="seafile-md-viewer d-flex">
<div className="seafile-md-viewer-container" ref="markdownContainer">
<div className={sidePanel ? "seafile-md-viewer-container side-panel-on":"seafile-md-viewer-container"} ref="markdownContainer">
{
this.state.activeTab === 'history' ?
this.state.isShowHistory ?
<div className="diff-container">
<div className="diff-wrapper article">
{ this.state.loadingDiff ?
@ -899,30 +985,38 @@ class MarkdownEditor extends React.Component {
</div>
</div>
:
<div className='seafile-md-viewer-slate'>
<div className={sidePanel ? "seafile-md-viewer-slate side-panel-on" : "seafile-md-viewer-slate"}>
<MarkdownViewerSlate
relatedFiles={this.state.relatedFiles}
siteRoot={siteRoot}
value={this.state.value}
/>
{isShowComments &&
{this.state.isShowComments &&
<i className="fa fa-plus-square seafile-viewer-comment-btn" ref="commentbtn" onMouseDown={this.addComment}></i>}
</div>
}
{
this.state.isShowOutline &&
<OutlineView
isViewer={true}
document={this.state.value.document}
scrollToNode={this.scrollToNode}
/>
}
</div>
<div className="seafile-md-viewer-side-panel">
{this.state.isShowComments && <CommentPanel toggleCommentPanel={this.toggleCommentList}/>}
{
this.state.isShowHistory &&
<HistoryList
editorUtilities={editorUtilities}
showDiffViewer={this.showDiffViewer}
setDiffViewerContent={this.setDiffViewerContent}
reloadDiffContent={this.reloadDiffContent}
toggleHistoryPanel={this.toggleHistory}
/>
}
</div>
<MarkdownViewerSidePanel
viewer={this}
value={this.state.value}
markdownContent={this.state.markdownContent}
editorUtilities={editorUtilities}
commentsNumber={this.state.commentsNumber}
getCommentsNumber={this.getCommentsNumber}
showDiffViewer={this.showDiffViewer}
setDiffViewerContent={this.setDiffViewerContent}
reloadDiffContent={this.reloadDiffContent}
activeTab={this.state.activeTab}
tabItemClick={this.tabItemClick}
/>
</div>
</div>
);
@ -952,7 +1046,7 @@ class MarkdownEditor extends React.Component {
fileTagList={this.state.fileTagList}
deleteDraft={this.deleteDraft}
showDraftSaved={this.state.showDraftSaved}
/>;
/>
}
return (