Fix history scroll behavior and text selection

Related to #92
This commit is contained in:
Thomas Nordquist
2019-04-14 21:35:02 +02:00
parent 425bbb36e3
commit 499dfd1b68
7 changed files with 70 additions and 50 deletions

View File

@@ -1,10 +1,11 @@
import * as diff from 'diff' import * as diff from 'diff'
import * as Prism from 'prismjs' import * as Prism from 'prismjs'
import * as React from 'react' import * as React from 'react'
import { CodeBlockColors, CodeBlockColorsBraceMonokai } from './CodeBlockColors'
import { selectTextWithCtrlA } from '../../utils/handleTextSelectWithCtrlA'
import { Theme, withStyles } from '@material-ui/core' import { Theme, withStyles } from '@material-ui/core'
import 'prismjs/components/prism-json' import 'prismjs/components/prism-json'
import 'prismjs/themes/prism-tomorrow.css' import 'prismjs/themes/prism-tomorrow.css'
import { CodeBlockColors, CodeBlockColorsBraceMonokai } from './CodeBlockColors'
interface Props { interface Props {
previous: string previous: string
@@ -15,6 +16,8 @@ interface Props {
} }
class CodeDiff extends React.Component<Props, {}> { class CodeDiff extends React.Component<Props, {}> {
private handleCtrlA = selectTextWithCtrlA({ targetSelector: 'pre' })
constructor(props: Props) { constructor(props: Props) {
super(props) super(props)
} }
@@ -58,23 +61,6 @@ class CodeDiff extends React.Component<Props, {}> {
return this.props.classes.noChange return this.props.classes.noChange
} }
private selectText = (e: React.KeyboardEvent<HTMLDivElement>) => {
const isCtrlA = (e.metaKey || e.ctrlKey) && e.key === 'a'
if (isCtrlA && window.getSelection) {
e.persist()
e.preventDefault()
e.stopPropagation()
const selection = window.getSelection()
const range = document.createRange()
range.selectNodeContents((e.target as HTMLElement).getElementsByTagName('pre')[0])
if (selection) {
selection.removeAllRanges()
selection.addRange(range)
}
}
}
public render() { public render() {
const changes = diff.diffLines(this.props.previous, this.props.current) const changes = diff.diffLines(this.props.previous, this.props.current)
const styledLines = Prism.highlight(this.props.current, Prism.languages.json, 'json').split('\n') const styledLines = Prism.highlight(this.props.current, Prism.languages.json, 'json').split('\n')
@@ -102,7 +88,7 @@ class CodeDiff extends React.Component<Props, {}> {
return ( return (
<div> <div>
<div className={this.props.classes.gutters} tabIndex={0} onKeyDown={this.selectText}> <div className={this.props.classes.gutters} tabIndex={0} onKeyDown={this.handleCtrlA}>
<pre className={`language-json ${this.props.classes.codeBlock}`}> <pre className={`language-json ${this.props.classes.codeBlock}`}>
{code} {code}
</pre> </pre>

View File

@@ -1,9 +1,10 @@
import * as React from 'react' import * as React from 'react'
import { Badge, Typography } from '@material-ui/core' import { Badge, Typography } from '@material-ui/core'
import { selectTextWithCtrlA } from '../../utils/handleTextSelectWithCtrlA'
import { Theme, withStyles } from '@material-ui/core/styles' import { Theme, withStyles } from '@material-ui/core/styles'
interface HistoryItem { interface HistoryItem {
key: string
title: JSX.Element | string title: JSX.Element | string
value: string value: string
selected?: boolean selected?: boolean
@@ -33,6 +34,8 @@ class HistoryDrawer extends React.Component<Props, State> {
this.setState({ collapsed: !this.state.collapsed }) this.setState({ collapsed: !this.state.collapsed })
} }
private handleCtrlA = selectTextWithCtrlA({targetSelector: 'pre'})
public renderHistory() { public renderHistory() {
const style = (element: HistoryItem) => ({ const style = (element: HistoryItem) => ({
backgroundColor: element.selected ? this.props.theme.palette.action.selected : this.props.theme.palette.action.hover, backgroundColor: element.selected ? this.props.theme.palette.action.selected : this.props.theme.palette.action.hover,
@@ -44,9 +47,10 @@ class HistoryDrawer extends React.Component<Props, State> {
const messageStyle: React.CSSProperties = { textOverflow: 'ellipsis', whiteSpace: 'nowrap', overflow: 'hidden' } const messageStyle: React.CSSProperties = { textOverflow: 'ellipsis', whiteSpace: 'nowrap', overflow: 'hidden' }
const elements = this.props.items.map((element, index) => ( const elements = this.props.items.map((element, index) => (
<div <div
key={index} key={element.key}
style={style(element)} style={style(element)}
onClick={(event: React.MouseEvent) => this.props.onClick && this.props.onClick(index, event.target)} onClick={(event: React.MouseEvent) => this.props.onClick && this.props.onClick(index, event.target)}
tabIndex={0} onKeyDown={this.handleCtrlA}
> >
<div><i>{element.title}</i></div> <div><i>{element.title}</i></div>
<div style={messageStyle}> <div style={messageStyle}>

View File

@@ -1,12 +1,22 @@
import 'react-ace'
import 'brace/mode/json'
import 'brace/mode/text'
import 'brace/mode/xml'
import 'brace/theme/monokai'
import 'brace/theme/dawn'
import * as React from 'react'
import * as q from '../../../../../backend/src/Model' import * as q from '../../../../../backend/src/Model'
import * as React from 'react'
import ClearAdornment from '../../helper/ClearAdornment'
import FormatAlignLeft from '@material-ui/icons/FormatAlignLeft'
import History from '../HistoryDrawer'
import Message from './Model/Message'
import Navigation from '@material-ui/icons/Navigation'
import { AppState } from '../../../reducers'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { default as AceEditor } from 'react-ace'
import { globalActions, publishActions } from '../../../actions'
import { TopicViewModel } from '../../../model/TopicViewModel'
import 'brace/mode/json'
import 'brace/theme/dawn'
import 'brace/theme/monokai'
import 'brace/mode/xml'
import 'brace/mode/text'
import 'react-ace'
import { import {
Button, Button,
@@ -25,17 +35,7 @@ import {
withTheme, withTheme,
} from '@material-ui/core' } from '@material-ui/core'
import { default as AceEditor } from 'react-ace' const sha1 = require('sha1')
import { AppState } from '../../../reducers'
import History from '../History'
import Message from './Model/Message'
import Navigation from '@material-ui/icons/Navigation'
import FormatAlignLeft from '@material-ui/icons/FormatAlignLeft'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { publishActions, globalActions } from '../../../actions'
import ClearAdornment from '../../helper/ClearAdornment'
import { TopicViewModel } from '../../../model/TopicViewModel'
interface Props { interface Props {
node?: q.TreeNode<TopicViewModel> node?: q.TreeNode<TopicViewModel>
@@ -276,10 +276,10 @@ class Publish extends React.Component<Props, State> {
private history() { private history() {
const items = this.state.history.reverse().map(message => ({ const items = this.state.history.reverse().map(message => ({
key: sha1(message.topic + message.payload),
title: message.topic, title: message.topic,
value: message.payload || '', value: message.payload || '',
})) }))
return <History items={items} onClick={this.didSelectHistoryEntry} /> return <History items={items} onClick={this.didSelectHistoryEntry} />
} }

View File

@@ -2,9 +2,11 @@ import * as q from '../../../../../backend/src/Model'
import * as React from 'react' import * as React from 'react'
import BarChart from '@material-ui/icons/BarChart' import BarChart from '@material-ui/icons/BarChart'
import DateFormatter from '../../helper/DateFormatter' import DateFormatter from '../../helper/DateFormatter'
import History from '../History' import History from '../HistoryDrawer'
import { TopicViewModel } from '../../../model/TopicViewModel' import { TopicViewModel } from '../../../model/TopicViewModel'
import { Base64Message } from '../../../../../backend/src/Model/Base64Message' import { Base64Message } from '../../../../../backend/src/Model/Base64Message'
import Copy from '../../helper/Copy';
import { selectTextWithCtrlA } from '../../../utils/handleTextSelectWithCtrlA';
const PlotHistory = React.lazy(() => import('./PlotHistory')) const PlotHistory = React.lazy(() => import('./PlotHistory'))
@@ -59,10 +61,16 @@ class MessageHistory extends React.Component<Props, State> {
const history = node.messageHistory.toArray() const history = node.messageHistory.toArray()
let previousMessage: q.Message | undefined = node.message let previousMessage: q.Message | undefined = node.message
const historyElements = history.reverse().map((message) => { const historyElements = history.reverse().map((message, idx) => {
const value = message.value ? Base64Message.toUnicodeString(message.value) : ''
const element = { const element = {
title: <span><DateFormatter date={message.received} /> {previousMessage ? <i>(-<DateFormatter date={message.received} intervalSince={previousMessage.received} />)</i> : null}</span>, value,
value: message.value ? Base64Message.toUnicodeString(message.value) : '', key: `${message.messageNumber}-${message.received}`,
title: (<span>
<DateFormatter date={message.received} />
{previousMessage ? <i>(-<DateFormatter date={message.received} intervalSince={previousMessage.received} />)</i> : null}
<div style={{ float: 'right' }}><Copy value={value} /></div>
</span>),
selected: message && message === this.props.selected, selected: message && message === this.props.selected,
} }
previousMessage = message previousMessage = message

View File

@@ -0,0 +1,22 @@
export const selectTextWithCtrlA = (options?: {targetSelector: string}) => (e: React.KeyboardEvent<HTMLDivElement>) => {
const isCtrlA = (e.metaKey || e.ctrlKey) && e.key === 'a'
if (isCtrlA && window.getSelection) {
e.persist()
e.preventDefault()
e.stopPropagation()
const selection = window.getSelection()
const range = document.createRange()
const eventTarget = (e.target as HTMLElement)
const target: HTMLElement | null = (options) ? eventTarget.querySelector(options.targetSelector) : eventTarget
if (!target) {
console.error('Could not find matching target for Ctrl+A Event')
}
if (selection && target) {
range.selectNodeContents(target)
selection.removeAllRanges()
selection.addRange(range)
}
}
}

View File

@@ -4,4 +4,5 @@ export interface Message {
value?: Base64Message value?: Base64Message
length: number length: number
received: Date received: Date
messageNumber: number
} }

View File

@@ -1,11 +1,8 @@
import { Base64Message } from './Base64Message' import { Base64Message } from './Base64Message'
import { Edge, Tree, TreeNode } from './' import { Edge, Tree, TreeNode } from './'
interface HasLength {
length: number
}
export abstract class TreeNodeFactory { export abstract class TreeNodeFactory {
private static messageCounter = 0
public static insertNodeAtPosition<ViewModel>(edgeNames: Array<string>, node: TreeNode<ViewModel>) { public static insertNodeAtPosition<ViewModel>(edgeNames: Array<string>, node: TreeNode<ViewModel>) {
let currentNode: TreeNode<ViewModel> = new Tree() let currentNode: TreeNode<ViewModel> = new Tree()
let edge let edge
@@ -25,7 +22,9 @@ export abstract class TreeNodeFactory {
value: value || undefined, value: value || undefined,
length: value ? value.length : 0, length: value ? value.length : 0,
received: new Date(), received: new Date(),
messageNumber: this.messageCounter,
}) })
this.messageCounter += 1
this.insertNodeAtPosition<ViewModel>(edgeNames, node) this.insertNodeAtPosition<ViewModel>(edgeNames, node)