diff --git a/css/65_data.css b/css/65_data.css index d672fccff..fbad3ba9e 100644 --- a/css/65_data.css +++ b/css/65_data.css @@ -21,6 +21,11 @@ color: #ff3300; stroke: #333; } +.note-header-icon.new .note-fill, +.layer-notes .note.new .note-fill { + color: #00bcdd; + stroke: #333; +} .note-header-icon.closed .note-fill, .layer-notes .note.closed .note-fill { color: #55dd00; @@ -99,7 +104,6 @@ .comments-container { background: #ececec; padding: 1px 10px; - margin: 10px 0; border-radius: 8px; } @@ -157,6 +161,10 @@ min-height: 100px; } +.note-save-section { + margin: 10px 0; +} + .note-report { float: right; } diff --git a/data/core.yaml b/data/core.yaml index 308ffdc9c..9827276fd 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -635,6 +635,9 @@ en: close_comment: Close and Comment open_comment: Reopen and Comment report: Report + new: New Note + newDescription: "Describe the issue." + newNote: Add Note help: title: Help key: H diff --git a/dist/locales/en.json b/dist/locales/en.json index 0df3c4de9..0ecfd4606 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -769,7 +769,10 @@ "comment": "Comment", "close_comment": "Close and Comment", "open_comment": "Reopen and Comment", - "report": "Report" + "report": "Report", + "new": "New Note", + "newDescription": "Describe the issue.", + "newNote": "Add Note" }, "help": { "title": "Help", diff --git a/modules/actions/add_note.js b/modules/actions/add_note.js deleted file mode 100644 index d36963a61..000000000 --- a/modules/actions/add_note.js +++ /dev/null @@ -1,6 +0,0 @@ -import osm from '../services/osm'; - -export function actionAddNote(note) { - osm.replaceNote(note); - console.log('actionAddNote: ', note); -} \ No newline at end of file diff --git a/modules/actions/index.js b/modules/actions/index.js index 1e2339644..330690db2 100644 --- a/modules/actions/index.js +++ b/modules/actions/index.js @@ -1,7 +1,6 @@ export { actionAddEntity } from './add_entity'; export { actionAddMember } from './add_member'; export { actionAddMidpoint } from './add_midpoint'; -export { actionAddNote } from './add_note'; export { actionAddVertex } from './add_vertex'; export { actionChangeMember } from './change_member'; export { actionChangePreset } from './change_preset'; diff --git a/modules/modes/add_note.js b/modules/modes/add_note.js index 57f133c9b..e44a2f763 100644 --- a/modules/modes/add_note.js +++ b/modules/modes/add_note.js @@ -1,13 +1,16 @@ import { t } from '../util/locale'; -import { actionAddNote } from '../actions'; import { behaviorDraw } from '../behavior'; import { modeBrowse, modeSelectNote } from './index'; import { osmNote } from '../osm'; +import { services } from '../services'; import { dispatch as d3_dispatch } from 'd3-dispatch'; export function modeAddNote(context) { + + var dispatch = d3_dispatch('change'); + var mode = { id: 'add-note', button: 'note', @@ -24,13 +27,20 @@ export function modeAddNote(context) { function add(loc) { - var note = osmNote({ loc: loc }); + var note = osmNote({ + loc: loc, + status: 'open', + comments: {}, + newFeature: true + }); - // actionAddNote(note); + services.osm.replaceNote(note); + dispatch.call('change'); - context.enter( - modeSelectNote(context, [note.id]).newFeature(true) - ); + + context + .selectedNoteID(note.id) + .enter(modeSelectNote(context, [note.id]).newFeature(true)); } diff --git a/modules/modes/select_note.js b/modules/modes/select_note.js index 9433152d4..b5294ab75 100644 --- a/modules/modes/select_note.js +++ b/modules/modes/select_note.js @@ -98,7 +98,7 @@ export function modeSelectNote(context, selectedNoteID) { .call(keybinding); context.ui().sidebar - .show(noteEditor.note(note)); + .show(noteEditor.note(note, newFeature)); context.map() .on('drawn.select', selectNote); diff --git a/modules/services/osm.js b/modules/services/osm.js index 7e9faf49d..7b641715f 100644 --- a/modules/services/osm.js +++ b/modules/services/osm.js @@ -159,6 +159,17 @@ function parseComments(comments) { } +function encodeNoteRtree(note) { + return { + minX: note.loc[0], + minY: note.loc[1], + maxX: note.loc[0], + maxY: note.loc[1], + data: note + }; +} + + var parsers = { node: function nodeData(obj, uid) { var attrs = obj.attributes; @@ -239,9 +250,10 @@ var parsers = { } var note = new osmNote(props); - var item = { minX: note.loc[0], minY: note.loc[1], maxX: note.loc[0], maxY: note.loc[1], data: note }; - _noteCache.rtree.insert(item); + var item = encodeNoteRtree(note); _noteCache.note[note.id] = note; + _noteCache.rtree.insert(item); + return note; }, @@ -906,7 +918,7 @@ export default { if (err) { return callback(err); } // we get the updated note back, remove from caches and reparse.. - var item = { minX: note.loc[0], minY: note.loc[1], maxX: note.loc[0], maxY: note.loc[1], data: note }; + var item = encodeNoteRtree(note); _noteCache.rtree.remove(item, function isEql(a, b) { return a.data.id === b.data.id; }); delete _noteCache.note[note.id]; @@ -1052,11 +1064,24 @@ export default { // replace a single note in the cache - replaceNote: function(n) { - if (n instanceof osmNote) { - _noteCache.note[n.id] = n; + replaceNote: function(note) { + if (!(note instanceof osmNote) || !note.id) return; + + _noteCache.note[note.id] = note; // update (or insert) in _noteCache.note + + function updateRtree(item) { // update (or insert) in _noteCache.rtree + + // TODO: other checks needed? (e.g., if cache.data.children.length decrements ...) + + // remove note + _noteCache.rtree.remove(item, function isEql(a, b) { return a.data.id === b.data.id; }); + _noteCache.rtree.insert(item); // add note (updated) + } - return n; + + updateRtree(encodeNoteRtree(note)); + + return note; } }; diff --git a/modules/svg/notes.js b/modules/svg/notes.js index 43e23f080..511f10c00 100644 --- a/modules/svg/notes.js +++ b/modules/svg/notes.js @@ -43,25 +43,35 @@ export function svgNotes(projection, context, dispatch) { function showLayer() { - editOn(); + // editOn(); layer + .classed('disabled', false) .style('opacity', 0) .transition() .duration(250) .style('opacity', 1) - .on('end', function () { dispatch.call('change'); }); + .on('end interrupt', function () { + dispatch.call('change'); + }); } function hideLayer() { + // editOff(); + throttledRedraw.cancel(); + layer.interrupt(); layer .transition() .duration(250) .style('opacity', 0) - .on('end', editOff); + .on('end interrupt', function () { + layer.classed('disabled', true); + dispatch.call('change'); + }); + } @@ -80,7 +90,8 @@ export function svgNotes(projection, context, dispatch) { // enter var notesEnter = notes.enter() .append('g') - .attr('class', function(d) { return 'note note-' + d.id + ' ' + d.status; }); + .attr('class', function(d) { return 'note note-' + d.id + ' ' + d.status; }) + .classed('new', function(d){ return d.id < 0; }); notesEnter .append('use') @@ -103,6 +114,18 @@ export function svgNotes(projection, context, dispatch) { .attr('y', '-20px') .attr('xlink:href', '#iD-icon-more'); + // add plus if this is a new note + notesEnter.selectAll('.note-annotation') + .data(function(d) { return d.id < 0 ? [0] : []; }) + .enter() + .append('use') + .attr('class', 'note-annotation thread') + .attr('width', '14px') + .attr('height', '14px') + .attr('x', '-7px') + .attr('y', '-20px') + .attr('xlink:href', '#iD-icon-plus'); + // update notes .merge(notesEnter) diff --git a/modules/ui/init.js b/modules/ui/init.js index cbdc7bd4b..7e1d134fc 100644 --- a/modules/ui/init.js +++ b/modules/ui/init.js @@ -101,7 +101,7 @@ export function uiInit(context) { limiter .append('div') - .attr('class', 'button-wrap joined col5') + .attr('class', 'button-wrap joined col3') .call(uiModes(context), limiter); limiter diff --git a/modules/ui/modes.js b/modules/ui/modes.js index f024dfa96..4d01b6820 100644 --- a/modules/ui/modes.js +++ b/modules/ui/modes.js @@ -48,10 +48,8 @@ export function uiModes(context) { .append('button') .attr('tabindex', -1) .attr('class', function(mode) { return mode.id + ' add-button col3'; }) - // .classed('disabled', function(mode) { - // return mode.id === 'add-note' && !svgNotes.enabled; // disable notes button - // }) .on('click.mode-buttons', function(mode) { + //TODO: prevent modeBrowse when in modeAddNote & osm layer is turned off // When drawing, ignore accidental clicks on mode buttons - #4042 var currMode = context.mode().id; if (currMode.match(/^draw/) !== null) return; @@ -100,10 +98,7 @@ export function uiModes(context) { modes.forEach(function(mode) { keybinding.on(mode.key, function() { // TODO: allow zooming out beyond minZoom when adding new note. Currently prevented - if ( - (editable() && mode.id !== 'add-note') - || (toggleNewNote() && mode.id === 'add-note') - ) { + if ((editable() && mode.id !== 'add-note') || (toggleNewNote() && mode.id === 'add-note')) { if (mode.id === context.mode().id) { context.enter(modeBrowse(context)); } else { diff --git a/modules/ui/note_comments.js b/modules/ui/note_comments.js index a9890779c..325213cb3 100644 --- a/modules/ui/note_comments.js +++ b/modules/ui/note_comments.js @@ -11,6 +11,8 @@ export function uiNoteComments() { function noteComments(selection) { + if (_note.newFeature) { return; } + var comments = selection.selectAll('.comments-container') .data([0]); diff --git a/modules/ui/note_editor.js b/modules/ui/note_editor.js index 4a15e3adc..e70df71be 100644 --- a/modules/ui/note_editor.js +++ b/modules/ui/note_editor.js @@ -90,7 +90,9 @@ export function uiNoteEditor(context) { noteSaveEnter .append('h4') .attr('class', '.note-save-header') - .text(t('note.newComment')); + .text(function() { + return _note.newFeature ? t('note.newDescription') : t('note.newComment'); + }); noteSaveEnter .append('textarea') @@ -140,18 +142,26 @@ export function uiNoteEditor(context) { .append('div') .attr('class', 'buttons'); - buttonEnter - .append('button') - .attr('class', 'button status-button action') - .append('span') - .attr('class', 'label'); + if (_note.newFeature) { + buttonEnter + .append('button') + .attr('class', 'button add-note-button action') + .append('span') + .attr('class', 'label'); + } else { + buttonEnter + .append('button') + .attr('class', 'button status-button action') + .append('span') + .attr('class', 'label'); - buttonEnter - .append('button') - .attr('class', 'button comment-button action') - .append('span') - .attr('class', 'label') - .text(t('note.comment')); + buttonEnter + .append('button') + .attr('class', 'button comment-button action') + .append('span') + .attr('class', 'label') + .text(t('note.comment')); + } // update buttonSection = buttonSection @@ -187,12 +197,28 @@ export function uiNoteEditor(context) { }); } }); + + buttonSection.select('.add-note-button') // select and propagate data + .text(t('note.newNote')) + .attr('disabled', function(d) { + return (d.status === 'open' && d.newComment) ? null : true; + }) + .on('click.save', function(d) { + this.blur(); // avoid keeping focus on the button - #4641 + var osm = services.osm; + if (osm) { + osm.postNoteAdd(d, d.status, function(err, note) { + dispatch.call('change', note); + }); + } + }); } - noteEditor.note = function(_) { + noteEditor.note = function(_, __) { if (!arguments.length) return _note; _note = _; + _note.update({ newFeature: __ }); return noteEditor; }; diff --git a/modules/ui/note_header.js b/modules/ui/note_header.js index dcaf82d46..63a6a143e 100644 --- a/modules/ui/note_header.js +++ b/modules/ui/note_header.js @@ -22,7 +22,8 @@ export function uiNoteHeader() { var iconEnter = headerEnter .append('div') - .attr('class', function(d) { return 'note-header-icon ' + d.status; }); + .attr('class', function(d) { return 'note-header-icon ' + d.status; }) + .classed('new', function(d) { return d.id < 0; }); iconEnter .append('div') @@ -35,6 +36,11 @@ export function uiNoteHeader() { .append('div') .attr('class', 'note-icon-annotation') .call(svgIcon('#iD-icon-more', 'note-annotation')); + } else if (_note.newFeature) { + iconEnter + .append('div') + .attr('class', 'note-icon-annotation') + .call(svgIcon('#iD-icon-plus', 'note-annotation')); } }); @@ -42,13 +48,14 @@ export function uiNoteHeader() { .append('div') .attr('class', 'note-header-label') .text(function(d) { + if (_note.newFeature) { return t('note.new'); } return t('note.note') + ' ' + d.id + ' ' + (d.status === 'closed' ? t('note.closed') : ''); }); } - noteHeader.note = function(_) { + noteHeader.note = function(_, __) { if (!arguments.length) return _note; _note = _; return noteHeader;