mirror of
https://github.com/haiwen/seahub.git
synced 2025-07-09 05:03:47 +00:00
change viewer (#3115)
This commit is contained in:
parent
3e860e882a
commit
db64e5a18e
@ -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;
|
|
@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import Loading from '../loading';
|
import Loading from '../loading';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
import { gettext } from '../../utils/constants';
|
||||||
import '../../css/markdown-viewer/history-viewer.css';
|
import '../../css/markdown-viewer/history-viewer.css';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
@ -97,6 +98,12 @@ class HistoryList extends React.Component {
|
|||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div className="seafile-history-side-panel">
|
<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'}>
|
<ul onScroll={this.onScroll} className={'history-list-container'}>
|
||||||
{this.state.historyList ?
|
{this.state.historyList ?
|
||||||
this.state.historyList.map((item, index = 0, arr) => {
|
this.state.historyList.map((item, index = 0, arr) => {
|
||||||
|
@ -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;
|
|
@ -2,7 +2,6 @@ import React from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { gettext } from '../../utils/constants';
|
import { gettext } from '../../utils/constants';
|
||||||
|
|
||||||
|
|
||||||
const OutlineItempropTypes = {
|
const OutlineItempropTypes = {
|
||||||
scrollToNode: PropTypes.func.isRequired,
|
scrollToNode: PropTypes.func.isRequired,
|
||||||
node: PropTypes.object.isRequired,
|
node: PropTypes.object.isRequired,
|
||||||
@ -41,7 +40,6 @@ const propTypes = {
|
|||||||
scrollToNode: PropTypes.func.isRequired,
|
scrollToNode: PropTypes.func.isRequired,
|
||||||
isViewer: PropTypes.bool.isRequired,
|
isViewer: PropTypes.bool.isRequired,
|
||||||
document: PropTypes.object.isRequired,
|
document: PropTypes.object.isRequired,
|
||||||
editor: PropTypes.object.isRequired,
|
|
||||||
activeTitleIndex: PropTypes.number,
|
activeTitleIndex: PropTypes.number,
|
||||||
value: PropTypes.object,
|
value: PropTypes.object,
|
||||||
};
|
};
|
||||||
@ -56,13 +54,13 @@ class OutlineView extends React.PureComponent {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="seafile-editor-outline">
|
<div className="seafile-editor-outline">
|
||||||
|
<div className="seafile-editor-outline-heading">{gettext('Contents')}</div>
|
||||||
{headerList.size > 0 ?
|
{headerList.size > 0 ?
|
||||||
headerList.map((node, index) => {
|
headerList.map((node, index) => {
|
||||||
let active = (index === this.props.activeTitleIndex) ? ' active' : '';
|
let active = (index === this.props.activeTitleIndex) ? ' active' : '';
|
||||||
return (
|
return (
|
||||||
<OutlineItem
|
<OutlineItem
|
||||||
key={node.key}
|
key={node.key}
|
||||||
editor={this.props.editor}
|
|
||||||
value={this.props.value}
|
value={this.props.value}
|
||||||
node={node}
|
node={node}
|
||||||
active={active}
|
active={active}
|
||||||
|
@ -18,6 +18,10 @@ const propTypes = {
|
|||||||
toggleStar: PropTypes.func.isRequired,
|
toggleStar: PropTypes.func.isRequired,
|
||||||
backToParentDirectory: PropTypes.func.isRequired,
|
backToParentDirectory: PropTypes.func.isRequired,
|
||||||
openDialogs: 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 {
|
class MarkdownViewerToolbar extends React.Component {
|
||||||
@ -47,12 +51,25 @@ class MarkdownViewerToolbar extends React.Component {
|
|||||||
<ButtonGroup>
|
<ButtonGroup>
|
||||||
<IconButton id={'shareBtn'} text={gettext('Share')} icon={'fa fa-share-alt'}
|
<IconButton id={'shareBtn'} text={gettext('Share')} icon={'fa fa-share-alt'}
|
||||||
onMouseDown={this.props.toggleShareLinkDialog}/>
|
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'}
|
<IconButton text={gettext('Back to parent directory')} id={'parentDirectory'}
|
||||||
icon={'fa fa-folder-open'} onMouseDown={this.props.backToParentDirectory}/>
|
icon={'fa fa-folder-open'} onMouseDown={this.props.backToParentDirectory}/>
|
||||||
{
|
{
|
||||||
(!this.props.hasDraft && this.props.fileInfo.permission === 'rw')? <IconButton text={gettext('Edit')}
|
(!this.props.hasDraft && this.props.fileInfo.permission === 'rw')? <IconButton text={gettext('Edit')}
|
||||||
id={'editButton'} icon={'fa fa-edit'} onMouseDown={this.props.onEdit}/>: null
|
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>
|
</ButtonGroup>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -4,7 +4,8 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 29%;
|
width: 29%;
|
||||||
}
|
}
|
||||||
.seafile-comment-title {
|
.seafile-comment-title,
|
||||||
|
.seafile-history-title {
|
||||||
border-bottom: 1px solid #e5e5e5;
|
border-bottom: 1px solid #e5e5e5;
|
||||||
min-height: 3em;
|
min-height: 3em;
|
||||||
line-height: 3em;
|
line-height: 3em;
|
||||||
@ -12,15 +13,18 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
background-color: #fafaf9;
|
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%;
|
width: 100%;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-weight: 700;
|
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;
|
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;
|
color: #888;
|
||||||
}
|
}
|
||||||
.seafile-comment-toggle-resolved {
|
.seafile-comment-toggle-resolved {
|
||||||
|
@ -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;
|
|
||||||
}
|
|
@ -13,29 +13,52 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
.seafile-md-viewer-container {
|
.seafile-md-viewer-container {
|
||||||
width: 70%;
|
width: 100%;
|
||||||
background-color: #fafaf9;
|
background-color: #fafaf9;
|
||||||
overflow: hidden;
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
position: relative;
|
||||||
.seafile-md-viewer-container:hover {
|
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
.seafile-md-viewer-container.side-panel-on {
|
||||||
|
width: 70%;
|
||||||
|
}
|
||||||
.seafile-md-viewer-slate {
|
.seafile-md-viewer-slate {
|
||||||
flex: auto;
|
flex: auto;
|
||||||
position: relative;
|
position: relative;
|
||||||
margin: 20px 40px;
|
margin: 20px 40px;
|
||||||
|
margin-right: 30%;
|
||||||
}
|
}
|
||||||
.seafile-md-viewer-main {
|
.seafile-md-viewer-main {
|
||||||
flex:auto;
|
flex: auto;
|
||||||
overflow:auto;
|
overflow: auto;
|
||||||
background:#fafaf9;
|
background:#fafaf9;
|
||||||
width: 70%;
|
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;
|
color: #eb8205;
|
||||||
border-left: 1px solid #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 {
|
.seafile-editor-outline .outline-h2, .seafile-editor-outline .outline-h3 {
|
||||||
height: 30px;
|
height: 30px;
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
@ -52,71 +75,26 @@
|
|||||||
}
|
}
|
||||||
/* side-panel */
|
/* side-panel */
|
||||||
.seafile-md-viewer-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%;
|
height: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
width: 30%;
|
||||||
|
position: fixed;
|
||||||
|
right: 0;
|
||||||
|
top: 87px;
|
||||||
}
|
}
|
||||||
.seafile-md-viewer-side-panel .tab-content {
|
.seafile-md-viewer-side-panel .seafile-comment,
|
||||||
height: calc(100% - 39px);
|
.seafile-md-viewer-side-panel .seafile-history-side-panel {
|
||||||
overflow-y: auto;
|
width: 100%;
|
||||||
overflow-x: hidden;
|
height: 100%;
|
||||||
}
|
}
|
||||||
.seafile-md-viewer-side-panel .tab-content .outline {
|
.seafile-md-viewer-side-panel .seafile-comment .add-comment-input,
|
||||||
padding: 20px;
|
.seafile-md-viewer-side-panel .seafile-comment .edit-comment-input {
|
||||||
|
background-color: #fff;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
.seafile-md-viewer-side-panel .md-side-panel-nav {
|
.seafile-md-viewer-side-panel .seafile-history-side-panel {
|
||||||
margin: 0;
|
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 {
|
.seafile-viewer-comment-btn {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
@ -131,11 +109,13 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background-color: #eee;
|
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) {
|
@media (max-width:991.8px) {
|
||||||
.seafile-md-viewer-side-panel {
|
|
||||||
display:none;
|
|
||||||
}
|
|
||||||
.seafile-editor-outline {
|
.seafile-editor-outline {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@ -144,8 +124,3 @@
|
|||||||
margin: 20px 40px;
|
margin: 20px 40px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@media (min-width:992px) {
|
|
||||||
.seafile-md-viewer-side-panel {
|
|
||||||
width: 30%;
|
|
||||||
}
|
|
||||||
}
|
|
@ -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 LocalDraftDialog from '@seafile/seafile-editor/dist/components/local-draft-dialog';
|
||||||
import DiffViewer from '@seafile/seafile-editor/dist/viewer/diff-viewer';
|
import DiffViewer from '@seafile/seafile-editor/dist/viewer/diff-viewer';
|
||||||
import MarkdownViewerToolbar from './components/toolbar/markdown-viewer-toolbar';
|
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 Loading from './components/loading';
|
||||||
import { findRange } from '@seafile/slate-react';
|
import { findRange } from '@seafile/slate-react';
|
||||||
|
|
||||||
import './css/markdown-viewer/markdown-editor.css';
|
import './css/markdown-viewer/markdown-editor.css';
|
||||||
|
|
||||||
const CryptoJS = require('crypto-js');
|
const CryptoJS = require('crypto-js');
|
||||||
|
const URL = require('url-parse');
|
||||||
const { repoID, repoName, filePath, fileName, mode, draftID, isDraft, hasDraft } = window.app.pageOptions;
|
const { repoID, repoName, filePath, fileName, mode, draftID, isDraft, hasDraft } = window.app.pageOptions;
|
||||||
const { siteRoot, serviceUrl, seafileCollabServer } = window.app.config;
|
const { siteRoot, serviceUrl, seafileCollabServer } = window.app.config;
|
||||||
const userInfo = window.app.userInfo;
|
const userInfo = window.app.userInfo;
|
||||||
@ -300,9 +303,11 @@ class MarkdownEditor extends React.Component {
|
|||||||
collabUsers: userInfo ?
|
collabUsers: userInfo ?
|
||||||
[{user: userInfo, is_editing: false}] : [],
|
[{user: userInfo, is_editing: false}] : [],
|
||||||
commentsNumber: null,
|
commentsNumber: null,
|
||||||
activeTab: 'outline',
|
|
||||||
loadingDiff: false,
|
loadingDiff: false,
|
||||||
value: null,
|
value: null,
|
||||||
|
isShowComments: false,
|
||||||
|
isShowHistory: false,
|
||||||
|
isShowOutline: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.state.collabServer) {
|
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) => {
|
setBtnPosition = (e) => {
|
||||||
let isShowComments = this.state.activeTab === 'comments' ? true : false;
|
if (!this.state.isShowComments) return;
|
||||||
if (!isShowComments) return;
|
|
||||||
const nativeSelection = window.getSelection();
|
const nativeSelection = window.getSelection();
|
||||||
if (!nativeSelection.rangeCount) {
|
if (!nativeSelection.rangeCount) {
|
||||||
this.range = null;
|
this.range = null;
|
||||||
@ -855,9 +851,95 @@ class MarkdownEditor extends React.Component {
|
|||||||
return newNodes;
|
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() {
|
render() {
|
||||||
let component;
|
let component;
|
||||||
let isShowComments = this.state.activeTab === 'comments' ? true : false;
|
let sidePanel = (this.state.isShowHistory || this.state.isShowComments) ? true : false;
|
||||||
if (this.state.loading) {
|
if (this.state.loading) {
|
||||||
return (
|
return (
|
||||||
<div className="empty-loading-page">
|
<div className="empty-loading-page">
|
||||||
@ -882,11 +964,15 @@ class MarkdownEditor extends React.Component {
|
|||||||
toggleShareLinkDialog={this.toggleShareLinkDialog}
|
toggleShareLinkDialog={this.toggleShareLinkDialog}
|
||||||
onEdit={this.onEdit}
|
onEdit={this.onEdit}
|
||||||
toggleNewDraft={editorUtilities.createDraftFile}
|
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 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-container">
|
||||||
<div className="diff-wrapper article">
|
<div className="diff-wrapper article">
|
||||||
{ this.state.loadingDiff ?
|
{ this.state.loadingDiff ?
|
||||||
@ -899,30 +985,38 @@ class MarkdownEditor extends React.Component {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
:
|
:
|
||||||
<div className='seafile-md-viewer-slate'>
|
<div className={sidePanel ? "seafile-md-viewer-slate side-panel-on" : "seafile-md-viewer-slate"}>
|
||||||
<MarkdownViewerSlate
|
<MarkdownViewerSlate
|
||||||
relatedFiles={this.state.relatedFiles}
|
relatedFiles={this.state.relatedFiles}
|
||||||
siteRoot={siteRoot}
|
siteRoot={siteRoot}
|
||||||
value={this.state.value}
|
value={this.state.value}
|
||||||
/>
|
/>
|
||||||
{isShowComments &&
|
{this.state.isShowComments &&
|
||||||
<i className="fa fa-plus-square seafile-viewer-comment-btn" ref="commentbtn" onMouseDown={this.addComment}></i>}
|
<i className="fa fa-plus-square seafile-viewer-comment-btn" ref="commentbtn" onMouseDown={this.addComment}></i>}
|
||||||
</div>
|
</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>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -952,7 +1046,7 @@ class MarkdownEditor extends React.Component {
|
|||||||
fileTagList={this.state.fileTagList}
|
fileTagList={this.state.fileTagList}
|
||||||
deleteDraft={this.deleteDraft}
|
deleteDraft={this.deleteDraft}
|
||||||
showDraftSaved={this.state.showDraftSaved}
|
showDraftSaved={this.state.showDraftSaved}
|
||||||
/>;
|
/>
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
Loading…
Reference in New Issue
Block a user