From 499dfd1b683cedec31177b80d457458d8b6f5965 Mon Sep 17 00:00:00 2001 From: Thomas Nordquist Date: Sun, 14 Apr 2019 21:35:02 +0200 Subject: [PATCH] Fix history scroll behavior and text selection Related to #92 --- app/src/components/Sidebar/CodeDiff.tsx | 26 +++--------- app/src/components/Sidebar/HistoryDrawer.tsx | 8 +++- .../components/Sidebar/Publish/Publish.tsx | 40 +++++++++---------- .../Sidebar/ValueRenderer/MessageHistory.tsx | 16 ++++++-- app/src/utils/handleTextSelectWithCtrlA.ts | 22 ++++++++++ backend/src/Model/Message.ts | 1 + backend/src/Model/TreeNodeFactory.ts | 7 ++-- 7 files changed, 70 insertions(+), 50 deletions(-) create mode 100644 app/src/utils/handleTextSelectWithCtrlA.ts diff --git a/app/src/components/Sidebar/CodeDiff.tsx b/app/src/components/Sidebar/CodeDiff.tsx index bacbafe..cf39dc5 100644 --- a/app/src/components/Sidebar/CodeDiff.tsx +++ b/app/src/components/Sidebar/CodeDiff.tsx @@ -1,10 +1,11 @@ import * as diff from 'diff' import * as Prism from 'prismjs' import * as React from 'react' +import { CodeBlockColors, CodeBlockColorsBraceMonokai } from './CodeBlockColors' +import { selectTextWithCtrlA } from '../../utils/handleTextSelectWithCtrlA' import { Theme, withStyles } from '@material-ui/core' import 'prismjs/components/prism-json' import 'prismjs/themes/prism-tomorrow.css' -import { CodeBlockColors, CodeBlockColorsBraceMonokai } from './CodeBlockColors' interface Props { previous: string @@ -15,6 +16,8 @@ interface Props { } class CodeDiff extends React.Component { + private handleCtrlA = selectTextWithCtrlA({ targetSelector: 'pre' }) + constructor(props: Props) { super(props) } @@ -58,23 +61,6 @@ class CodeDiff extends React.Component { return this.props.classes.noChange } - private selectText = (e: React.KeyboardEvent) => { - 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() { const changes = diff.diffLines(this.props.previous, this.props.current) const styledLines = Prism.highlight(this.props.current, Prism.languages.json, 'json').split('\n') @@ -102,8 +88,8 @@ class CodeDiff extends React.Component { return (
-
-
+        
+
             {code}
           
diff --git a/app/src/components/Sidebar/HistoryDrawer.tsx b/app/src/components/Sidebar/HistoryDrawer.tsx index 33778eb..e59cc0c 100644 --- a/app/src/components/Sidebar/HistoryDrawer.tsx +++ b/app/src/components/Sidebar/HistoryDrawer.tsx @@ -1,9 +1,10 @@ import * as React from 'react' - import { Badge, Typography } from '@material-ui/core' +import { selectTextWithCtrlA } from '../../utils/handleTextSelectWithCtrlA' import { Theme, withStyles } from '@material-ui/core/styles' interface HistoryItem { + key: string title: JSX.Element | string value: string selected?: boolean @@ -33,6 +34,8 @@ class HistoryDrawer extends React.Component { this.setState({ collapsed: !this.state.collapsed }) } + private handleCtrlA = selectTextWithCtrlA({targetSelector: 'pre'}) + public renderHistory() { const style = (element: HistoryItem) => ({ backgroundColor: element.selected ? this.props.theme.palette.action.selected : this.props.theme.palette.action.hover, @@ -44,9 +47,10 @@ class HistoryDrawer extends React.Component { const messageStyle: React.CSSProperties = { textOverflow: 'ellipsis', whiteSpace: 'nowrap', overflow: 'hidden' } const elements = this.props.items.map((element, index) => (
this.props.onClick && this.props.onClick(index, event.target)} + tabIndex={0} onKeyDown={this.handleCtrlA} >
{element.title}
diff --git a/app/src/components/Sidebar/Publish/Publish.tsx b/app/src/components/Sidebar/Publish/Publish.tsx index c6fc64c..4456a32 100644 --- a/app/src/components/Sidebar/Publish/Publish.tsx +++ b/app/src/components/Sidebar/Publish/Publish.tsx @@ -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 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 { Button, @@ -25,17 +35,7 @@ import { withTheme, } from '@material-ui/core' -import { default as AceEditor } from 'react-ace' -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' +const sha1 = require('sha1') interface Props { node?: q.TreeNode @@ -276,10 +276,10 @@ class Publish extends React.Component { private history() { const items = this.state.history.reverse().map(message => ({ + key: sha1(message.topic + message.payload), title: message.topic, value: message.payload || '', })) - return } diff --git a/app/src/components/Sidebar/ValueRenderer/MessageHistory.tsx b/app/src/components/Sidebar/ValueRenderer/MessageHistory.tsx index 9a2fc32..c17c810 100644 --- a/app/src/components/Sidebar/ValueRenderer/MessageHistory.tsx +++ b/app/src/components/Sidebar/ValueRenderer/MessageHistory.tsx @@ -2,9 +2,11 @@ import * as q from '../../../../../backend/src/Model' import * as React from 'react' import BarChart from '@material-ui/icons/BarChart' import DateFormatter from '../../helper/DateFormatter' -import History from '../History' +import History from '../HistoryDrawer' import { TopicViewModel } from '../../../model/TopicViewModel' import { Base64Message } from '../../../../../backend/src/Model/Base64Message' +import Copy from '../../helper/Copy'; +import { selectTextWithCtrlA } from '../../../utils/handleTextSelectWithCtrlA'; const PlotHistory = React.lazy(() => import('./PlotHistory')) @@ -59,10 +61,16 @@ class MessageHistory extends React.Component { const history = node.messageHistory.toArray() 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 = { - title: {previousMessage ? (-) : null}, - value: message.value ? Base64Message.toUnicodeString(message.value) : '', + value, + key: `${message.messageNumber}-${message.received}`, + title: ( + + {previousMessage ? (-) : null} +
+
), selected: message && message === this.props.selected, } previousMessage = message diff --git a/app/src/utils/handleTextSelectWithCtrlA.ts b/app/src/utils/handleTextSelectWithCtrlA.ts new file mode 100644 index 0000000..8e29cd8 --- /dev/null +++ b/app/src/utils/handleTextSelectWithCtrlA.ts @@ -0,0 +1,22 @@ +export const selectTextWithCtrlA = (options?: {targetSelector: string}) => (e: React.KeyboardEvent) => { + 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) + } + } +} diff --git a/backend/src/Model/Message.ts b/backend/src/Model/Message.ts index 85bc5a8..1be9f2b 100644 --- a/backend/src/Model/Message.ts +++ b/backend/src/Model/Message.ts @@ -4,4 +4,5 @@ export interface Message { value?: Base64Message length: number received: Date + messageNumber: number } diff --git a/backend/src/Model/TreeNodeFactory.ts b/backend/src/Model/TreeNodeFactory.ts index 9948555..176aaa1 100644 --- a/backend/src/Model/TreeNodeFactory.ts +++ b/backend/src/Model/TreeNodeFactory.ts @@ -1,11 +1,8 @@ import { Base64Message } from './Base64Message' import { Edge, Tree, TreeNode } from './' -interface HasLength { - length: number -} - export abstract class TreeNodeFactory { + private static messageCounter = 0 public static insertNodeAtPosition(edgeNames: Array, node: TreeNode) { let currentNode: TreeNode = new Tree() let edge @@ -25,7 +22,9 @@ export abstract class TreeNodeFactory { value: value || undefined, length: value ? value.length : 0, received: new Date(), + messageNumber: this.messageCounter, }) + this.messageCounter += 1 this.insertNodeAtPosition(edgeNames, node)