updated: siebar displays note details on hover (via svg)

This commit is contained in:
Thomas Hervey
2018-06-29 14:43:01 -04:00
parent 0859d00195
commit 737ccfcfba
11 changed files with 571 additions and 33 deletions

View File

@@ -118,10 +118,37 @@
pointer-events: none;
}
.layer-notes * {
pointer-events: visible;
cursor: pointer;
color: #eebb00;
}
/* TODO: possibly move this note detail .css to another file */
.comment-first {
background-color:#ddd;
border-radius: 5px;
padding: 5px;
margin: 5px auto;
}
.comment {
background-color:#fff;
border-radius: 5px;
padding: 5px;
margin: 5px auto;
}
.commentCreator {
color: #666;
}
.commentText {
margin: 20px auto;
}
/* Streetside Image Layer */
.layer-streetside-images {
pointer-events: none;

View File

@@ -609,6 +609,19 @@ en:
title: "Photo Overlay (OpenStreetCam)"
openstreetcam:
view_on_openstreetcam: "View this image on OpenStreetCam"
note:
title: "Edit note"
unresolved: "Unresolved note #"
description: "Description"
creator: "Comment from"
anonymous: 'anonymous'
creatorOn: 'on'
commentTitle: 'Comments'
resolve: "Resolve"
comment: "Comment"
commentResolve: "Comment & Resolve"
save: "Save new note"
cancel: "Cancel"
help:
title: Help
key: H

14
dist/locales/en.json vendored
View File

@@ -742,6 +742,20 @@
"openstreetcam": {
"view_on_openstreetcam": "View this image on OpenStreetCam"
},
"note": {
"title": "Edit note",
"unresolved": "Unresolved note #",
"description": "Description",
"creator": "Comment from",
"anonymous": "anonymous",
"creatorOn": "on",
"commentTitle": "Comments",
"resolve": "Resolve",
"comment": "Comment",
"commentResolve": "Comment & Resolve",
"save": "Save new note",
"cancel": "Cancel"
},
"help": {
"title": "Help",
"key": "H",

View File

@@ -0,0 +1,225 @@
import _without from 'lodash-es/without';
import {
event as d3_event,
mouse as d3_mouse,
select as d3_select
} from 'd3-selection';
import { geoVecLength } from '../geo';
import {
modeBrowse,
modeSelect
} from '../modes';
import {
osmEntity,
osmNote
} from '../osm';
export function behaviorSelect(context) {
var lastMouse = null;
var suppressMenu = true;
var tolerance = 4;
var p1 = null;
function point() {
return d3_mouse(context.container().node());
}
function keydown() {
var e = d3_event;
if (e && e.shiftKey) {
context.surface()
.classed('behavior-multiselect', true);
}
if (e && e.keyCode === 93) { // context menu
e.preventDefault();
e.stopPropagation();
}
}
function keyup() {
var e = d3_event;
if (!e || !e.shiftKey) {
context.surface()
.classed('behavior-multiselect', false);
}
if (e && e.keyCode === 93) { // context menu
e.preventDefault();
e.stopPropagation();
contextmenu();
}
}
function mousedown() {
if (!p1) p1 = point();
d3_select(window)
.on('mouseup.select', mouseup, true);
var isShowAlways = +context.storage('edit-menu-show-always') === 1;
suppressMenu = !isShowAlways;
}
function mousemove() {
if (d3_event) lastMouse = d3_event;
}
function mouseup() {
click();
}
function contextmenu() {
var e = d3_event;
e.preventDefault();
e.stopPropagation();
if (!+e.clientX && !+e.clientY) {
if (lastMouse) {
e.sourceEvent = lastMouse;
} else {
return;
}
}
if (!p1) p1 = point();
suppressMenu = false;
click();
}
function click() {
d3_select(window)
.on('mouseup.select', null, true);
if (!p1) return;
var p2 = point();
var dist = geoVecLength(p1, p2);
p1 = null;
if (dist > tolerance) {
return;
}
var isMultiselect = d3_event.shiftKey || d3_select('#surface .lasso').node();
var isShowAlways = +context.storage('edit-menu-show-always') === 1;
var datum = d3_event.target.__data__ || (lastMouse && lastMouse.target.__data__);
var mode = context.mode();
var entity;
if (datum instanceof osmNote) {
entity = datum;
} else {
entity = datum && datum.properties && datum.properties.entity;
}
if (entity) datum = entity;
if (datum && datum.type === 'midpoint') {
datum = datum.parents[0];
}
if (!(datum instanceof osmEntity) && !(datum instanceof osmNote)) {
// clicked nothing..
if (!isMultiselect && mode.id !== 'browse') {
context.enter(modeBrowse(context));
}
} else {
// clicked an entity.. (or a notes)
var selectedIDs = context.selectedIDs();
if (!isMultiselect) {
if (selectedIDs.length > 1 && (!suppressMenu && !isShowAlways)) {
// multiple things already selected, just show the menu...
mode.suppressMenu(false).reselect();
} else {
// select a single thing..
context.enter(modeSelect(context, [datum.id]).suppressMenu(suppressMenu));
}
} else {
if (selectedIDs.indexOf(datum.id) !== -1) {
// clicked entity is already in the selectedIDs list..
if (!suppressMenu && !isShowAlways) {
// don't deselect clicked entity, just show the menu.
mode.suppressMenu(false).reselect();
} else {
// deselect clicked entity, then reenter select mode or return to browse mode..
selectedIDs = _without(selectedIDs, datum.id);
context.enter(selectedIDs.length ? modeSelect(context, selectedIDs) : modeBrowse(context));
}
} else {
// clicked entity is not in the selected list, add it..
selectedIDs = selectedIDs.concat([datum.id]);
context.enter(modeSelect(context, selectedIDs).suppressMenu(suppressMenu));
}
}
}
// reset for next time..
suppressMenu = true;
}
var behavior = function(selection) {
lastMouse = null;
suppressMenu = true;
p1 = null;
d3_select(window)
.on('keydown.select', keydown)
.on('keyup.select', keyup)
.on('contextmenu.select-window', function() {
// Edge and IE really like to show the contextmenu on the
// menubar when user presses a keyboard menu button
// even after we've already preventdefaulted the key event.
var e = d3_event;
if (+e.clientX === 0 && +e.clientY === 0) {
d3_event.preventDefault();
d3_event.stopPropagation();
}
});
selection
.on('mousedown.select', mousedown)
.on('mousemove.select', mousemove)
.on('contextmenu.select', contextmenu);
if (d3_event && d3_event.shiftKey) {
context.surface()
.classed('behavior-multiselect', true);
}
};
behavior.off = function(selection) {
d3_select(window)
.on('keydown.select', null)
.on('keyup.select', null)
.on('contextmenu.select-window', null)
.on('mouseup.select', null, true);
selection
.on('mousedown.select', null)
.on('mousemove.select', null)
.on('contextmenu.select', null);
context.surface()
.classed('behavior-multiselect', false);
};
return behavior;
}

View File

@@ -6,7 +6,10 @@ import {
} from 'd3-selection';
import { d3keybinding as d3_keybinding } from '../lib/d3.keybinding.js';
import { osmEntity } from '../osm';
import {
osmEntity,
osmNote
} from '../osm';
import { utilRebind } from '../util/rebind';
@@ -110,7 +113,12 @@ export function behaviorHover(context) {
var entity;
if (datum instanceof osmEntity) {
entity = datum;
} else {
}
// TODO: TAH - reintroduce if we need a check for osmNote here
// else if (datum instanceof osmNote) {
// entity = datum;
// }
else {
entity = datum && datum.properties && datum.properties.entity;
}

View File

@@ -3,32 +3,72 @@ import _extend from 'lodash-es/extend';
import { osmEntity } from './entity';
import { geoExtent } from '../geo';
import { debug } from '../index';
export function osmNote() {
if (!(this instanceof osmNote)) {
return (new osmNote()).initialize(arguments);
} else if (arguments.length) {
this.initialize(arguments);
}
if (!(this instanceof osmNote)) return;
this.initialize(arguments);
return this;
}
osmEntity.note = osmNote;
osmNote.prototype = Object.create(osmEntity.prototype);
_extend(osmNote.prototype, {
type: 'note',
initialize: function(sources) {
for (var i = 0; i < sources.length; ++i) {
var source = sources[i];
for (var prop in source) {
if (Object.prototype.hasOwnProperty.call(source, prop)) {
if (source[prop] === undefined) {
delete this[prop];
} else {
this[prop] = source[prop];
}
}
}
}
if (!this.id && this.type) {
this.id = osmEntity.id(this.type);
}
if (!this.hasOwnProperty('visible')) {
this.visible = true;
}
if (debug) {
Object.freeze(this);
Object.freeze(this.tags);
if (this.loc) Object.freeze(this.loc);
if (this.nodes) Object.freeze(this.nodes);
if (this.members) Object.freeze(this.members);
}
return this;
},
extent: function() {
return new geoExtent(this.loc);
},
geometry: function(graph) {
return graph.transient(this, 'geometry', function() {
return graph.isPoi(this) ? 'point' : 'vertex';
});
},
getID: function() {
return this.id;
},
getType: function() {
return this.type;
},
getComments: function() {
return this.comments;
}
});
});

View File

@@ -16,15 +16,16 @@ import { xml as d3_xml } from 'd3-request';
import { d3geoTile as d3_geoTile } from '../lib/d3.geo.tile';
import { geoExtent } from '../geo';
import {
osmNote,
} from '../osm';
import {
utilRebind,
utilIdleWorker
} from '../util';
import {
osmNote
} from '../osm';
import { actionRestrictTurn } from '../actions';
var urlroot = 'https://api.openstreetmap.org',
_notesCache,
dispatch = d3_dispatch('loadedNotes', 'loading'),
@@ -111,7 +112,7 @@ function parseComments(comments) {
}
var parsers = {
note: function parseNote(obj) {
note: function parseNote(obj, uid) {
var attrs = obj.attributes;
var childNodes = obj.childNodes;
var parsedNote = {};
@@ -130,6 +131,9 @@ var parsers = {
}
});
parsedNote.id = uid;
parsedNote.type = 'note';
return {
minX: parsedNote.loc[0],
minY: parsedNote.loc[1],
@@ -151,17 +155,19 @@ function parse(xml, callback, options) {
var parser = parsers[child.nodeName];
if (parser) {
// TODO: change how a note uid is parsed. Nodes & notes share 'n' + id combination
var childNodes = child.childNodes;
var id;
var i;
for (i = 0; i < childNodes.length; i++) {
if (childNodes[i].nodeName === 'id') { id = childNodes[i].nodeName; }
}
if (options.cache && _entityCache[id]) {
var uid;
_forEach(childNodes, function(node) {
if (node.nodeName === 'id') {
uid = child.nodeName + node.innerHTML;
}
});
if (options.cache && _entityCache[uid]) {
return null;
}
return parser(child);
return parser(child, uid);
}
}
utilIdleWorker(children, parseChild, callback);

View File

@@ -3,12 +3,16 @@ import { select as d3_select } from 'd3-selection';
import { svgPointTransform } from './index';
import { services } from '../services';
import { uiNoteEditor } from '../ui';
export function svgNotes(projection, context, dispatch) {
var throttledRedraw = _throttle(function () { dispatch.call('change'); }, 1000);
var minZoom = 12;
var layer = d3_select(null);
var _notes;
var noteEditor = uiNoteEditor(context);
function init() {
if (svgNotes.initialized) return; // run once
svgNotes.enabled = false;
@@ -57,6 +61,20 @@ export function svgNotes(projection, context, dispatch) {
.on('end', editOff);
}
function click(d) {
context.ui().sidebar.show(noteEditor, d);
}
function mouseover(d) {
context.ui().sidebar.show(noteEditor, d);
}
function mouseout(d) {
// TODO: check if the item was clicked. If so, it should remain on the sidebar.
// TODO: handle multi-clicks. Otherwise, utilize behavior/select.js
context.ui().sidebar.hide();
}
function update() {
var service = getService();
var data = (service ? service.notes(projection) : []);
@@ -70,12 +88,15 @@ export function svgNotes(projection, context, dispatch) {
var notesEnter = notes.enter()
.append('use')
.attr('class', 'note')
.attr('class', function(d) { return 'note ' + d.id; })
.attr('width', '24px')
.attr('height', '24px')
.attr('x', '-12px')
.attr('y', '-12px')
.attr('xlink:href', '#fas-comment-alt');
.attr('xlink:href', '#fas-comment-alt')
.on('click', click)
.on('mouseover', mouseover)
.on('mouseout', mouseout);
notes
.merge(notesEnter)

View File

@@ -34,6 +34,7 @@ export { uiMapInMap } from './map_in_map';
export { uiModal } from './modal';
export { uiModes } from './modes';
export { uiNotice } from './notice';
export { uiNoteEditor } from './note_editor';
export { uiPresetEditor } from './preset_editor';
export { uiPresetIcon } from './preset_icon';
export { uiPresetList } from './preset_list';

166
modules/ui/note_editor.js Normal file
View File

@@ -0,0 +1,166 @@
import { dispatch as d3_dispatch } from 'd3-dispatch';
import { uiFormFields } from './form_fields';
import { uiField } from './field';
import { utilRebind } from '../util';
import { t } from '../util/locale';
export function uiNoteEditor(context) {
var dispatch = d3_dispatch('change');
var formFields = uiFormFields(context);
var _fieldsArr;
var _noteID;
function noteEditor(selection, note) {
render(selection, note);
}
function parseNoteUnresolved(selection, note) {
var unresolved = selection.selectAll('.noteUnresolved')
.data(note, function(d) { return d.id; })
.enter()
.append('h3')
.attr('class', 'noteUnresolved')
.text(function(d) { return String(t('note.unresolved') + ' ' + d.id); });
selection.merge(unresolved);
return selection;
}
function parseNoteComments(selection, note) {
function noteCreator(d) {
var userName = d.user ? d.user : t('note.anonymous');
return String(t('note.creator') + ' ' + userName + ' ' + t('note.creatorOn') + ' ' + d.date);
}
var comments = selection
.append('div')
.attr('class', 'comments');
var comment = comments.selectAll('.comment')
.data(note.comments, function(d) { return d.uid; })
.enter()
.append('div')
.attr('class', 'comment');
// append the creator
comment
.append('p')
.attr('class', 'commentCreator')
.text(function(d) { return noteCreator(d); });
// append the comment
comment
.append('p')
.attr('class', 'commentText')
.text(function(d) { return d.text; });
comments.insert('h4', ':first-child')
.text(t('note.description'));
// TODO: have a better check to highlight the first/author comment (e.g., check if `author: true`)
comments.select('div')
.attr('class', 'comment-first');
selection.merge(comments);
return selection;
}
function render(selection, note) {
var exampleNote = {
close_url: 'example_close_url',
comment_url: 'example_comment_url',
comments: [
{
action: 'opened',
date: '2016-11-20 00:50:20 UTC',
html: '&lt;p&gt;Test comment1.&lt;/p&gt;',
text: 'Test comment1',
uid: '111111',
user: 'User1',
user_url: 'example_user_url1'
},
{
action: 'opened',
date: '2016-11-20 00:50:20 UTC',
html: '&lt;p&gt;Test comment2.&lt;/p&gt;',
text: 'Test comment2',
uid: '222222',
user: 'User2',
user_url: 'example_user_url2'
},
{
action: 'opened',
date: '2016-11-20 00:50:20 UTC',
html: '&lt;p&gt;Test comment3.&lt;/p&gt;',
text: 'Test comment3',
uid: '333333',
user: 'User3',
user_url: 'example_user_url3'
}
],
date_created: '2016-11-20 00:50:20 UTC',
id: 'note789148',
loc: [
-120.0219036,
34.4611879
],
status: 'open',
type: 'note',
url: 'https://api.openstreetmap.org/api/0.6/notes/789148',
visible: true
};
var currentNote = note ? [note] : [exampleNote];
var author = currentNote[0].comments[0];
author.author = true;
var header = selection.selectAll('.header')
.data([0]);
header.enter()
.append('div')
.attr('class', 'header fillL')
.append('h3')
.text(t('note.title'));
var body = selection.selectAll('.body')
.data([0]);
body = body.enter()
.append('div')
.attr('class', 'body')
.merge(body);
// Note Section
var noteSection = body.selectAll('.changeset-editor')
.data([0]);
noteSection = noteSection.enter()
.append('div')
.attr('class', 'modal-section changeset-editor')
.merge(noteSection);
noteSection = noteSection.call(parseNoteUnresolved, currentNote);
noteSection = noteSection.call(parseNoteComments, currentNote[0]);
// TODO: revisit commit.js, changeset_editor.js to get warnings, fields array, button toggles, etc.
}
noteEditor.noteID = function(_) {
if (!arguments.length) return _noteID;
if (_noteID === _) return noteEditor;
_noteID = _;
_fieldsArr = null;
return noteEditor;
};
return utilRebind(noteEditor, dispatch, 'on');
}

View File

@@ -1,12 +1,26 @@
import _throttle from 'lodash-es/throttle';
import { uiFeatureList } from './feature_list';
import { uiInspector } from './inspector';
import { uiNoteEditor } from './note_editor';
export function uiSidebar(context) {
var inspector = uiInspector(context),
current;
var inspector = uiInspector(context),
noteEditor = uiNoteEditor(context),
current,
wasNote;
function isNote(id) {
var isNote = (id && id.slice(0,4) === 'note') ? id.slice(0,4) : null;
// TODO: have a better check, perhaps see if the hover class is activated on a note
if (!isNote && wasNote) {
wasNote = false;
sidebar.hide();
} else if (isNote) {
wasNote = true;
sidebar.show(noteEditor);
}
}
function sidebar(selection) {
var featureListWrap = selection
@@ -21,6 +35,8 @@ export function uiSidebar(context) {
function hover(id) {
// isNote(id); TODO: instantiate check if needed
if (!current && context.hasEntity(id)) {
featureListWrap
.classed('inspector-hidden', true);
@@ -46,6 +62,7 @@ export function uiSidebar(context) {
inspector
.state('hide');
}
// } // TODO: - remove if note check logic is moved
}
@@ -82,7 +99,7 @@ export function uiSidebar(context) {
};
sidebar.show = function(component) {
sidebar.show = function(component, element) {
featureListWrap
.classed('inspector-hidden', true);
inspectorWrap
@@ -92,7 +109,7 @@ export function uiSidebar(context) {
current = selection
.append('div')
.attr('class', 'sidebar-component')
.call(component);
.call(component, element);
};