diff --git a/src/components/annotations-view.js b/src/components/annotations-view.js index ed99f56b..6a9130b6 100644 --- a/src/components/annotations-view.js +++ b/src/components/annotations-view.js @@ -6,6 +6,8 @@ import { FormattedMessage, useIntl } from 'react-intl'; import cx from 'classnames'; import { SidebarPreview } from './preview'; import { searchAnnotations } from '../lib/search'; +import { IconColor } from "./icons"; +import { annotationColors } from "../lib/colors"; function AnnotationsViewSearch({ query, onInput, onClear }) { const intl = useIntl(); @@ -42,6 +44,34 @@ function AnnotationsViewSearch({ query, onInput, onClear }) { ); } +function Selector({ tags, colors, onClickTag, onClickColor }) { + const intl = useIntl(); + return ( +
+ {colors.length > 1 &&
+ {colors.map((color, index) => ( +
onClickColor(color.color)} + >
+ ))} +
} + {!!tags.length &&
+ {tags.map((tag, index) => ( + onClickTag(tag.name)} + >{tag.name} + ))} +
} +
+ ); +} + // We get significant performance boost here because `props.annotation` // reference is updated only when annotation data is updated const Annotation = React.memo((props) => { @@ -68,8 +98,10 @@ const Annotation = React.memo((props) => { }); const AnnotationsView = React.memo(function (props) { - const [filteredAnnotations, setFilteredAnnotations] = useState(null); + const [searchedAnnotations, setSearchedAnnotations] = useState(null); const [query, setQuery] = useState(''); + const [selectedTags, setSelectedTags] = useState([]); + const [selectedColors, setSelectedColors] = useState([]); function getContainerNode() { return document.getElementById('annotationsView'); @@ -78,10 +110,10 @@ const AnnotationsView = React.memo(function (props) { function search(query) { let { annotations } = props; if (query) { - setFilteredAnnotations(searchAnnotations(annotations, query)); + setSearchedAnnotations(searchAnnotations(annotations, query)); } else { - setFilteredAnnotations(null); + setSearchedAnnotations(null); } } @@ -95,50 +127,141 @@ const AnnotationsView = React.memo(function (props) { search(); } + function handleTagClick(name) { + console.log('tag click', name) + if (selectedTags.includes(name)) { + setSelectedTags(selectedTags.filter(x => x !== name)); + } + else { + setSelectedTags([...selectedTags, name]); + } + } + + function handleColorClick(color) { + if (selectedColors.includes(color)) { + setSelectedColors(selectedColors.filter(x => x !== color)); + } + else { + setSelectedColors([...selectedColors, color]); + } + } let containerNode = getContainerNode(); - if (!containerNode) return null; + if (!containerNode) { + return null; + } let { annotations } = props; - if (filteredAnnotations) { - let newFilteredAnnotations = []; - for (let filteredAnnotation of filteredAnnotations) { + if (searchedAnnotations) { + let newSearchedAnnotations = []; + for (let filteredAnnotation of searchedAnnotations) { let annotation = annotations.find(x => x.id === filteredAnnotation.id); if (annotation) { - newFilteredAnnotations.push(annotation); + newSearchedAnnotations.push(annotation); } } - annotations = newFilteredAnnotations; + annotations = newSearchedAnnotations; + } + + if (selectedTags.length || selectedColors.length) { + annotations = annotations.filter(x => x.tags.some(t => selectedTags.includes(t.name)) || selectedColors.includes(x.color)); } + let tags = {}; + let colors = {}; + for (let annotation of props.annotations) { + for (let tag of annotation.tags) { + if (!tags[tag.name]) { + tags[tag.name] = { ...tag }; + tags[tag.name].selected = selectedTags.includes(tag.name); + tags[tag.name].inactive = true; + } + } + let color = annotation.color; + if (!colors[color]) { + let predefinedColor = annotationColors.find(x => x[1] === color); + colors[color] = { + color, + selected: selectedColors.includes(color), + inactive: true, + name: predefinedColor ? predefinedColor[0] : null + }; + } + } + + for (let annotation of annotations) { + for (let tag of annotation.tags) { + tags[tag.name].inactive = false; + } + colors[annotation.color].inactive = false; + } + + let coloredTags = []; + for (let key in tags) { + let tag = tags[key]; + if (tag.color) { + coloredTags.push(tag); + delete tags[key]; + } + } + + // Sort colored tags and place at beginning + coloredTags.sort((a, b) => { + return a.position - b.position; + }); + + let primaryColors = []; + for (let annotationColor of annotationColors) { + if (colors[annotationColor[1]]) { + primaryColors.push(colors[annotationColor[1]]); + delete colors[annotationColor[1]]; + } + } + + tags = Object.values(tags); + let collator = new Intl.Collator(); + tags.sort(function (a, b) { + return collator.compare(a.tag, b.tag); + }); + tags = [...coloredTags, ...tags]; + + colors = Object.values(colors); + colors = [...primaryColors, ...colors]; + return ReactDOM.createPortal( - - {annotations.length - ? annotations.map(annotation => ( - - )) - : !query.length &&
} -
, - containerNode - ); +
+ + {annotations.length + ? annotations.map(annotation => ( + + )) + : !query.length &&
} +
+ {(!!tags.length || colors.length > 1) && } + , containerNode); }); export default AnnotationsView; diff --git a/src/components/icons.js b/src/components/icons.js index 301372b4..3214085b 100644 --- a/src/components/icons.js +++ b/src/components/icons.js @@ -51,3 +51,21 @@ export function IconNoteLarge() { ); } + +export function IconColor({ color }) { + return ( + + + + ); +} diff --git a/src/demo-annotations.js b/src/demo-annotations.js index 47c78223..2be83a5a 100644 --- a/src/demo-annotations.js +++ b/src/demo-annotations.js @@ -111,10 +111,9 @@ let annotations = [ tags: [{ name: 'programming languages', color: '#ff0000' - }, - { - name: 'compiler' - }], + }, { + name: 'compiler' + }], id: 690898672271502, dateModified: '2020-02-07T07:24:37.870Z', authorName: '', @@ -162,7 +161,13 @@ let annotations = [ }, text: 'Nested loops can be difficult to optimize for tracing VMs. In a na ̈ıve implementation, inner loops would become hot first, and the VM would start tracing there. ', comment: 'A problem of nested loops', - tags: [], + tags: [{ + name: 'This is another tag' + }, { + name: 'Astrophysics' + }, { + name: 'Sleep patters and habits testing options' + }], id: 674358779481824, dateModified: '2020-02-07T07:23:50.968Z', authorName: '', diff --git a/src/index.web.js b/src/index.web.js index ded1284e..a2103d6c 100644 --- a/src/index.web.js +++ b/src/index.web.js @@ -122,7 +122,7 @@ class ViewerInstance { state: options.state, location: options.location, sidebarWidth: 240, - sidebarOpen: false, + sidebarOpen: true, bottomPlaceholderHeight: 0, localizedStrings: {}, readOnly: options.readOnly diff --git a/src/stylesheets/components/annotator/_annotations-view.scss b/src/stylesheets/components/annotator/_annotations-view.scss index 03c808f9..d4b04f5d 100644 --- a/src/stylesheets/components/annotator/_annotations-view.scss +++ b/src/stylesheets/components/annotator/_annotations-view.scss @@ -3,7 +3,12 @@ // #annotationsView { - @extend %sidebar-view; + width: 100%; + height: 100%; + user-select: none; + + display: flex; + flex-direction: column; &:focus { outline: none; @@ -65,6 +70,92 @@ border: none; } } + + .annotations { + overflow: auto; + padding: 4px 4px 0; // Firefox does not render padding-bottom + flex: 1; + //-webkit-overflow-scrolling: touch; + + > *:last-child { + margin-bottom: 8px; + } + } + + .selector { + padding: 8px 8px; + border-top: 1px solid #a9a9a9; + box-shadow: 0 0 0 1px rgba(0,0,0,0.05); + z-index: 2; + max-height: 120px; + overflow: auto; + + .colors { + display: flex; + + .color { + padding: 2px; + display: flex; + border-radius: 3px; + margin-left: 1px; + + &:hover, &.selected { + background-color: #bcc4d2; + } + + &.inactive { + svg { + opacity: 0.4; + } + } + } + } + + .tags { + display: flex; + flex-wrap: wrap; + + &:nth-child(2) { + margin-top: 4px; + } + + .tag { + cursor: default; + overflow: hidden; + text-overflow: ellipsis; + white-space: pre; + padding: 2px 4px; + margin-left: 1px; + margin-bottom: 1px; + + border-radius: 6px; + border: 1px solid transparent; + + &.color { + font-weight: bold; + } + + &.selected { + color: white; + background: rgb(89, 139, 236); + } + + &:hover { + background: rgb(187, 206, 241); + border: 1px solid rgb(109, 149, 224); + } + + &:active { + color: white; + background: rgb(89, 139, 236); + } + + &.inactive { + opacity: 0.4; + } + } + } + } } #measure { diff --git a/src/stylesheets/themes/_light-darwin.scss b/src/stylesheets/themes/_light-darwin.scss index 72028987..56515ecd 100644 --- a/src/stylesheets/themes/_light-darwin.scss +++ b/src/stylesheets/themes/_light-darwin.scss @@ -331,7 +331,7 @@ $area-tool-icon: "darwin/area"; $area-tool-icon-active: "darwin/area-white"; // Search -$search-margin: 1px 9px 9px 9px; +$search-margin: 1px 13px 9px 13px; $search-height: 20px; $search-bg: #bcc4d2; $search-blurred-bg: #dbdbdb;