mirror of
https://github.com/FoggedLens/iD.git
synced 2026-03-31 09:19:25 +02:00
Merge branch 'notes'
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -19,3 +19,5 @@ land.html
|
||||
/css/img
|
||||
/test/css
|
||||
/test/img
|
||||
|
||||
\.vscode/
|
||||
|
||||
@@ -352,7 +352,7 @@ Drawing is then accomplished with
|
||||
.merge(footer);
|
||||
|
||||
footer
|
||||
.call(uiViewOnOSM(context).entityID(entityID));
|
||||
.call(uiViewOnOSM(context).what(entity));
|
||||
```
|
||||
|
||||
Some components are reconfigurable, and some provide functionality beyond
|
||||
|
||||
162
css/65_data.css
Normal file
162
css/65_data.css
Normal file
@@ -0,0 +1,162 @@
|
||||
|
||||
/* OSM Notes Layer */
|
||||
.layer-notes {
|
||||
pointer-events: none;
|
||||
}
|
||||
.layer-notes .note * {
|
||||
pointer-events: none;
|
||||
}
|
||||
.layer-notes .note .note-fill {
|
||||
pointer-events: visible;
|
||||
cursor: pointer; /* Opera */
|
||||
cursor: url(img/cursor-select-point.png), pointer; /* FF */
|
||||
}
|
||||
|
||||
.note-header-icon .note-shadow,
|
||||
.layer-notes .note .note-shadow {
|
||||
color: #000;
|
||||
}
|
||||
.note-header-icon .note-fill,
|
||||
.layer-notes .note .note-fill {
|
||||
color: #ff3300;
|
||||
stroke: #333;
|
||||
}
|
||||
.note-header-icon.closed .note-fill,
|
||||
.layer-notes .note.closed .note-fill {
|
||||
color: #55dd00;
|
||||
stroke: #333;
|
||||
}
|
||||
.layer-notes .note.hovered .note-fill {
|
||||
color: #eebb00;
|
||||
stroke: #333;
|
||||
}
|
||||
.layer-notes .note.selected .note-fill {
|
||||
color: #ffee00;
|
||||
stroke: #333;
|
||||
}
|
||||
|
||||
/* slight adjustments to preset icon for note icons */
|
||||
.note-header-icon .preset-icon-28 {
|
||||
top: 18px;
|
||||
}
|
||||
|
||||
.note-header-icon .note-icon-annotation {
|
||||
position: absolute;
|
||||
top: 21px;
|
||||
left: 21px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.note-header-icon .note-icon-annotation .icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
|
||||
/* OSM Note UI */
|
||||
.note-header {
|
||||
background-color: #f6f6f6;
|
||||
border-radius: 5px;
|
||||
border: 1px solid #ccc;
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.note-header-icon {
|
||||
background-color: #fff;
|
||||
padding: 10px;
|
||||
flex: 0 0 62px;
|
||||
position: relative;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-right: 1px solid #ccc;
|
||||
border-radius: 5px 0 0 5px;
|
||||
}
|
||||
[dir='rtl'] .note-header-icon {
|
||||
border-right: unset;
|
||||
border-left: 1px solid #ccc;
|
||||
border-radius: 0 5px 5px 0;
|
||||
}
|
||||
|
||||
.note-header-icon .icon-wrap {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
}
|
||||
|
||||
.note-header-label {
|
||||
background-color: #f6f6f6;
|
||||
padding: 0 15px;
|
||||
flex: 1 1 100%;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
border-radius: 0 5px 5px 0;
|
||||
}
|
||||
[dir='rtl'] .note-header-label {
|
||||
border-radius: 5px 0 0 5px;
|
||||
}
|
||||
|
||||
.comments-container {
|
||||
background: #ececec;
|
||||
padding: 1px 10px;
|
||||
margin: 10px 0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.comment {
|
||||
background-color: #fff;
|
||||
border-radius: 5px;
|
||||
border: 1px solid #ccc;
|
||||
margin: 10px auto;
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
}
|
||||
.comment-avatar {
|
||||
padding: 10px;
|
||||
flex: 0 0 62px;
|
||||
}
|
||||
.comment-avatar .icon.comment-avatar-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
object-fit: cover;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 20px;
|
||||
}
|
||||
.comment-main {
|
||||
padding: 10px;
|
||||
flex: 1 1 100%;
|
||||
flex-flow: column nowrap;
|
||||
overflow: hidden;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
.comment-metadata {
|
||||
flex-flow: row nowrap;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.comment-author {
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
.comment-date {
|
||||
color: #aaa;
|
||||
}
|
||||
.comment-text {
|
||||
color: #333;
|
||||
margin-top: 10px;
|
||||
overflow-y: auto;
|
||||
max-height: 250px;
|
||||
}
|
||||
.comment-text::-webkit-scrollbar {
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
#new-comment-input {
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
max-height: 300px;
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
.note-report {
|
||||
float: right;
|
||||
}
|
||||
@@ -689,6 +689,7 @@ button.save.has-count .count::before {
|
||||
}
|
||||
|
||||
.field-help-title button.close,
|
||||
.sidebar-component .header button.note-editor-close,
|
||||
.entity-editor-pane .header button.preset-close,
|
||||
.preset-list-pane .header button.preset-choose {
|
||||
position: absolute;
|
||||
@@ -696,6 +697,7 @@ button.save.has-count .count::before {
|
||||
top: 0;
|
||||
}
|
||||
[dir='rtl'] .field-help-title button.close,
|
||||
[dir='rtl'] .sidebar-component .header button.note-editor-close,
|
||||
[dir='rtl'] .entity-editor-pane .header button.preset-close,
|
||||
[dir='rtl'] .preset-list-pane .header button.preset-choose {
|
||||
left: 0;
|
||||
@@ -733,11 +735,21 @@ button.save.has-count .count::before {
|
||||
.footer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
margin: 0;
|
||||
padding: 5px 20px 5px 20px;
|
||||
border-top: 1px solid #ccc;
|
||||
background-color: #fafafa;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
list-style: none;
|
||||
display: flex;
|
||||
|
||||
}
|
||||
|
||||
.footer > a {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.sidebar-component .body {
|
||||
@@ -1290,6 +1302,7 @@ a.hide-toggle {
|
||||
.inspector-hover .preset-input-wrap .label,
|
||||
.inspector-hover .form-field-multicombo,
|
||||
.inspector-hover .structure-extras-wrap,
|
||||
.inspector-hover .comments-container .comment,
|
||||
.inspector-hover input,
|
||||
.inspector-hover textarea,
|
||||
.inspector-hover label {
|
||||
@@ -1329,14 +1342,14 @@ a.hide-toggle {
|
||||
/* hide but preserve in layout */
|
||||
.inspector-hover .entity-editor-pane button.minor,
|
||||
.inspector-hover .combobox-caret,
|
||||
.inspector-hover .entity-editor-pane .header button,
|
||||
.inspector-hover .header button,
|
||||
.inspector-hover .spin-control,
|
||||
.inspector-hover .form-field-multicombo .chips .remove,
|
||||
.inspector-hover .hide-toggle:before,
|
||||
.inspector-hover .more-fields,
|
||||
.inspector-hover .form-label-button-wrap,
|
||||
.inspector-hover .tag-reference-button,
|
||||
.inspector-hover .view-on-osm {
|
||||
.inspector-hover .footer * {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@@ -3564,10 +3577,17 @@ img.tile-debug {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.save-section .buttons {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-around;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.save-section .buttons .action,
|
||||
.save-section .buttons .secondary-action {
|
||||
display: inline-block;
|
||||
margin: 0 20px 0 0;
|
||||
width: 45%;
|
||||
margin: 10px auto;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
@@ -441,6 +441,9 @@ en:
|
||||
osm:
|
||||
tooltip: Map data from OpenStreetMap
|
||||
title: OpenStreetMap data
|
||||
notes:
|
||||
tooltip: Note data from OpenStreetMap
|
||||
title: OpenStreetMap notes
|
||||
fill_area: Fill Areas
|
||||
map_features: Map Features
|
||||
autohidden: "These features have been automatically hidden because too many would be shown on the screen. You can zoom in to edit them."
|
||||
@@ -613,6 +616,20 @@ en:
|
||||
title: "Photo Overlay (OpenStreetCam)"
|
||||
openstreetcam:
|
||||
view_on_openstreetcam: "View this image on OpenStreetCam"
|
||||
note:
|
||||
note: Note
|
||||
title: Edit note
|
||||
anonymous: anonymous
|
||||
closed: "(Closed)"
|
||||
commentTitle: Comments
|
||||
newComment: New Comment
|
||||
inputPlaceholder: Enter a comment to share with other users.
|
||||
close: Close Note
|
||||
open: Reopen Note
|
||||
comment: Comment
|
||||
close_comment: Close and Comment
|
||||
open_comment: Reopen and Comment
|
||||
report: Report
|
||||
help:
|
||||
title: Help
|
||||
key: H
|
||||
|
||||
19
dist/locales/en.json
vendored
19
dist/locales/en.json
vendored
@@ -536,6 +536,10 @@
|
||||
"osm": {
|
||||
"tooltip": "Map data from OpenStreetMap",
|
||||
"title": "OpenStreetMap data"
|
||||
},
|
||||
"notes": {
|
||||
"tooltip": "Note data from OpenStreetMap",
|
||||
"title": "OpenStreetMap notes"
|
||||
}
|
||||
},
|
||||
"fill_area": "Fill Areas",
|
||||
@@ -746,6 +750,21 @@
|
||||
"openstreetcam": {
|
||||
"view_on_openstreetcam": "View this image on OpenStreetCam"
|
||||
},
|
||||
"note": {
|
||||
"note": "Note",
|
||||
"title": "Edit note",
|
||||
"anonymous": "anonymous",
|
||||
"closed": "(Closed)",
|
||||
"commentTitle": "Comments",
|
||||
"newComment": "New Comment",
|
||||
"inputPlaceholder": "Enter a comment to share with other users.",
|
||||
"close": "Close Note",
|
||||
"open": "Reopen Note",
|
||||
"comment": "Comment",
|
||||
"close_comment": "Close and Comment",
|
||||
"open_comment": "Reopen and Comment",
|
||||
"report": "Report"
|
||||
},
|
||||
"help": {
|
||||
"title": "Help",
|
||||
"key": "H",
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -108,7 +111,7 @@ export function behaviorHover(context) {
|
||||
.classed('hover-suppressed', false);
|
||||
|
||||
var entity;
|
||||
if (datum instanceof osmEntity) {
|
||||
if (datum instanceof osmNote || datum instanceof osmEntity) {
|
||||
entity = datum;
|
||||
} else {
|
||||
entity = datum && datum.properties && datum.properties.entity;
|
||||
@@ -122,7 +125,7 @@ export function behaviorHover(context) {
|
||||
return;
|
||||
}
|
||||
|
||||
var selector = '.' + entity.id;
|
||||
var selector = (datum instanceof osmNote) ? 'note-' + entity.id : '.' + entity.id;
|
||||
|
||||
if (entity.type === 'relation') {
|
||||
entity.members.forEach(function(member) {
|
||||
@@ -135,7 +138,11 @@ export function behaviorHover(context) {
|
||||
_selection.selectAll(selector)
|
||||
.classed(suppressed ? 'hover-suppressed' : 'hover', true);
|
||||
|
||||
dispatch.call('hover', this, !suppressed && entity.id);
|
||||
if (datum instanceof osmNote) {
|
||||
dispatch.call('hover', this, !suppressed && entity);
|
||||
} else {
|
||||
dispatch.call('hover', this, !suppressed && entity.id);
|
||||
}
|
||||
|
||||
} else {
|
||||
dispatch.call('hover', this, null);
|
||||
|
||||
@@ -10,10 +10,14 @@ import { geoVecLength } from '../geo';
|
||||
|
||||
import {
|
||||
modeBrowse,
|
||||
modeSelect
|
||||
modeSelect,
|
||||
modeSelectNote
|
||||
} from '../modes';
|
||||
|
||||
import { osmEntity } from '../osm';
|
||||
import {
|
||||
osmEntity,
|
||||
osmNote
|
||||
} from '../osm';
|
||||
|
||||
|
||||
export function behaviorSelect(context) {
|
||||
@@ -122,15 +126,9 @@ export function behaviorSelect(context) {
|
||||
datum = datum.parents[0];
|
||||
}
|
||||
|
||||
if (!(datum instanceof osmEntity)) {
|
||||
// clicked nothing..
|
||||
if (!isMultiselect && mode.id !== 'browse') {
|
||||
context.enter(modeBrowse(context));
|
||||
}
|
||||
|
||||
} else {
|
||||
// clicked an entity..
|
||||
if (datum instanceof osmEntity) { // clicked an entity..
|
||||
var selectedIDs = context.selectedIDs();
|
||||
context.selectedNoteID(null);
|
||||
|
||||
if (!isMultiselect) {
|
||||
if (selectedIDs.length > 1 && (!suppressMenu && !isShowAlways)) {
|
||||
@@ -158,6 +156,18 @@ export function behaviorSelect(context) {
|
||||
context.enter(modeSelect(context, selectedIDs).suppressMenu(suppressMenu));
|
||||
}
|
||||
}
|
||||
|
||||
} else if (datum instanceof osmNote && !isMultiselect) { // clicked a Note..
|
||||
context
|
||||
.selectedNoteID(datum.id)
|
||||
.enter(modeSelectNote(context, datum.id));
|
||||
|
||||
} else { // clicked nothing..
|
||||
|
||||
context.selectedNoteID(null);
|
||||
if (!isMultiselect && mode.id !== 'browse') {
|
||||
context.enter(modeBrowse(context));
|
||||
}
|
||||
}
|
||||
|
||||
// reset for next time..
|
||||
|
||||
@@ -255,10 +255,18 @@ export function coreContext() {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
context.activeID = function() {
|
||||
return mode && mode.activeID && mode.activeID();
|
||||
};
|
||||
|
||||
var _selectedNoteID;
|
||||
context.selectedNoteID = function(noteID) {
|
||||
if (!arguments.length) return _selectedNoteID;
|
||||
_selectedNoteID = noteID;
|
||||
return context;
|
||||
};
|
||||
|
||||
|
||||
/* Behaviors */
|
||||
context.install = function(behavior) {
|
||||
|
||||
@@ -9,3 +9,4 @@ export { modeMove } from './move';
|
||||
export { modeRotate } from './rotate';
|
||||
export { modeSave } from './save';
|
||||
export { modeSelect } from './select';
|
||||
export { modeSelectNote } from './select_note';
|
||||
|
||||
@@ -513,7 +513,6 @@ export function modeSelect(context, selectedIDs) {
|
||||
showMenu();
|
||||
}
|
||||
}, 270); /* after any centerEase completes */
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
123
modules/modes/select_note.js
Normal file
123
modules/modes/select_note.js
Normal file
@@ -0,0 +1,123 @@
|
||||
import {
|
||||
event as d3_event,
|
||||
select as d3_select
|
||||
} from 'd3-selection';
|
||||
|
||||
import { d3keybinding as d3_keybinding } from '../lib/d3.keybinding.js';
|
||||
|
||||
import {
|
||||
behaviorHover,
|
||||
behaviorLasso,
|
||||
behaviorSelect
|
||||
} from '../behavior';
|
||||
|
||||
import { services } from '../services';
|
||||
import { modeBrowse } from './browse';
|
||||
import { uiNoteEditor } from '../ui';
|
||||
|
||||
|
||||
export function modeSelectNote(context, selectedNoteID) {
|
||||
var mode = {
|
||||
id: 'select_note',
|
||||
button: 'browse'
|
||||
};
|
||||
|
||||
var osm = services.osm;
|
||||
var keybinding = d3_keybinding('select-note');
|
||||
var noteEditor = uiNoteEditor(context)
|
||||
.on('change', function() {
|
||||
context.map().pan([0,0]); // trigger a redraw
|
||||
var note = checkSelectedID();
|
||||
if (!note) return;
|
||||
context.ui().sidebar
|
||||
.show(noteEditor.note(note));
|
||||
});
|
||||
|
||||
var behaviors = [
|
||||
behaviorHover(context),
|
||||
behaviorSelect(context),
|
||||
behaviorLasso(context),
|
||||
];
|
||||
|
||||
|
||||
function checkSelectedID() {
|
||||
if (!osm) return;
|
||||
var note = osm.getNote(selectedNoteID);
|
||||
if (!note) {
|
||||
context.enter(modeBrowse(context));
|
||||
}
|
||||
return note;
|
||||
}
|
||||
|
||||
|
||||
mode.enter = function() {
|
||||
|
||||
// class the note as selected, or return to browse mode if the note is gone
|
||||
function selectNote(drawn) {
|
||||
if (!checkSelectedID()) return;
|
||||
|
||||
var selection = context.surface()
|
||||
.selectAll('.note-' + selectedNoteID);
|
||||
|
||||
if (selection.empty()) {
|
||||
// Return to browse mode if selected DOM elements have
|
||||
// disappeared because the user moved them out of view..
|
||||
var source = d3_event && d3_event.type === 'zoom' && d3_event.sourceEvent;
|
||||
if (drawn && source && (source.type === 'mousemove' || source.type === 'touchmove')) {
|
||||
context.enter(modeBrowse(context));
|
||||
}
|
||||
|
||||
} else {
|
||||
selection
|
||||
.classed('selected', true);
|
||||
}
|
||||
}
|
||||
|
||||
function esc() {
|
||||
context.enter(modeBrowse(context));
|
||||
}
|
||||
|
||||
var note = checkSelectedID();
|
||||
if (!note) return;
|
||||
|
||||
behaviors.forEach(function(behavior) {
|
||||
context.install(behavior);
|
||||
});
|
||||
|
||||
keybinding
|
||||
.on('⎋', esc, true);
|
||||
|
||||
d3_select(document)
|
||||
.call(keybinding);
|
||||
|
||||
context.ui().sidebar
|
||||
.show(noteEditor.note(note));
|
||||
|
||||
context.map()
|
||||
.on('drawn.select', selectNote);
|
||||
|
||||
selectNote();
|
||||
};
|
||||
|
||||
|
||||
mode.exit = function() {
|
||||
behaviors.forEach(function(behavior) {
|
||||
context.uninstall(behavior);
|
||||
});
|
||||
|
||||
keybinding.off();
|
||||
|
||||
context.surface()
|
||||
.selectAll('.note.selected')
|
||||
.classed('selected hovered', false);
|
||||
|
||||
context.map()
|
||||
.on('drawn.select', null);
|
||||
|
||||
context.ui().sidebar
|
||||
.hide();
|
||||
};
|
||||
|
||||
|
||||
return mode;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
export { osmChangeset } from './changeset';
|
||||
export { osmEntity } from './entity';
|
||||
export { osmNode } from './node';
|
||||
export { osmNote } from './note';
|
||||
export { osmRelation } from './relation';
|
||||
export { osmWay } from './way';
|
||||
|
||||
|
||||
60
modules/osm/note.js
Normal file
60
modules/osm/note.js
Normal file
@@ -0,0 +1,60 @@
|
||||
import _extend from 'lodash-es/extend';
|
||||
|
||||
import { geoExtent } from '../geo';
|
||||
|
||||
|
||||
export function osmNote() {
|
||||
if (!(this instanceof osmNote)) {
|
||||
return (new osmNote()).initialize(arguments);
|
||||
} else if (arguments.length) {
|
||||
this.initialize(arguments);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
osmNote.id = function() {
|
||||
return osmNote.id.next--;
|
||||
};
|
||||
|
||||
|
||||
osmNote.id.next = -1;
|
||||
|
||||
|
||||
_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.id = osmNote.id();
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
extent: function() {
|
||||
return new geoExtent(this.loc);
|
||||
},
|
||||
|
||||
update: function(attrs) {
|
||||
return osmNote(this, attrs, {v: 1 + (this.v || 0)});
|
||||
},
|
||||
|
||||
isNew: function() {
|
||||
return this.id < 0;
|
||||
}
|
||||
|
||||
});
|
||||
@@ -350,7 +350,7 @@ export function rendererMap(context) {
|
||||
surface.selectAll('.layer-osm *').remove();
|
||||
|
||||
var mode = context.mode();
|
||||
if (mode && mode.id !== 'save') {
|
||||
if (mode && mode.id !== 'save' && mode.id !== 'select_note') {
|
||||
context.enter(modeBrowse(context));
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -10,6 +10,7 @@ export { svgLines } from './lines.js';
|
||||
export { svgMapillaryImages } from './mapillary_images.js';
|
||||
export { svgMapillarySigns } from './mapillary_signs.js';
|
||||
export { svgMidpoints } from './midpoints.js';
|
||||
export { svgNotes } from './notes.js';
|
||||
export { svgOneWaySegments } from './helpers.js';
|
||||
export { svgOpenstreetcamImages } from './openstreetcam_images.js';
|
||||
export { svgOsm } from './osm.js';
|
||||
|
||||
@@ -15,6 +15,7 @@ import { svgMapillaryImages } from './mapillary_images';
|
||||
import { svgMapillarySigns } from './mapillary_signs';
|
||||
import { svgOpenstreetcamImages } from './openstreetcam_images';
|
||||
import { svgOsm } from './osm';
|
||||
import { svgNotes } from './notes';
|
||||
import { utilRebind } from '../util/rebind';
|
||||
import { utilGetDimensions, utilSetDimensions } from '../util/dimensions';
|
||||
|
||||
@@ -24,6 +25,7 @@ export function svgLayers(projection, context) {
|
||||
var svg = d3_select(null);
|
||||
var layers = [
|
||||
{ id: 'osm', layer: svgOsm(projection, context, dispatch) },
|
||||
{ id: 'notes', layer: svgNotes(projection, context, dispatch) },
|
||||
{ id: 'gpx', layer: svgGpx(projection, context, dispatch) },
|
||||
{ id: 'mvt', layer: svgMvt(projection, context, dispatch) },
|
||||
{ id: 'streetside', layer: svgStreetside(projection, context, dispatch)},
|
||||
|
||||
173
modules/svg/notes.js
Normal file
173
modules/svg/notes.js
Normal file
@@ -0,0 +1,173 @@
|
||||
import _throttle from 'lodash-es/throttle';
|
||||
|
||||
import { select as d3_select } from 'd3-selection';
|
||||
|
||||
import { svgPointTransform } from './index';
|
||||
import { services } from '../services';
|
||||
|
||||
|
||||
export function svgNotes(projection, context, dispatch) {
|
||||
var throttledRedraw = _throttle(function () { dispatch.call('change'); }, 1000);
|
||||
var minZoom = 12;
|
||||
var layer = d3_select(null);
|
||||
var _notes;
|
||||
|
||||
|
||||
function init() {
|
||||
if (svgNotes.initialized) return; // run once
|
||||
svgNotes.enabled = false;
|
||||
svgNotes.initialized = true;
|
||||
}
|
||||
|
||||
function editOn() {
|
||||
layer.style('display', 'block');
|
||||
}
|
||||
|
||||
|
||||
function editOff() {
|
||||
layer.selectAll('.note').remove();
|
||||
layer.style('display', 'none');
|
||||
}
|
||||
|
||||
|
||||
function getService() {
|
||||
if (services.osm && !_notes) {
|
||||
_notes = services.osm;
|
||||
_notes.on('loadedNotes', throttledRedraw);
|
||||
} else if (!services.osm && _notes) {
|
||||
_notes = null;
|
||||
}
|
||||
|
||||
return _notes;
|
||||
}
|
||||
|
||||
|
||||
function showLayer() {
|
||||
editOn();
|
||||
|
||||
layer
|
||||
.style('opacity', 0)
|
||||
.transition()
|
||||
.duration(250)
|
||||
.style('opacity', 1)
|
||||
.on('end', function () { dispatch.call('change'); });
|
||||
}
|
||||
|
||||
|
||||
function hideLayer() {
|
||||
throttledRedraw.cancel();
|
||||
|
||||
layer
|
||||
.transition()
|
||||
.duration(250)
|
||||
.style('opacity', 0)
|
||||
.on('end', editOff);
|
||||
}
|
||||
|
||||
|
||||
function update() {
|
||||
var service = getService();
|
||||
var selectedID = context.selectedNoteID();
|
||||
var data = (service ? service.notes(projection) : []);
|
||||
var transform = svgPointTransform(projection);
|
||||
var notes = layer.selectAll('.note')
|
||||
.data(data, function(d) { return d.status + d.id; });
|
||||
|
||||
// exit
|
||||
notes.exit()
|
||||
.remove();
|
||||
|
||||
// enter
|
||||
var notesEnter = notes.enter()
|
||||
.append('g')
|
||||
.attr('class', function(d) { return 'note note-' + d.id + ' ' + d.status; });
|
||||
|
||||
// notesEnter
|
||||
// .append('use')
|
||||
// .attr('class', 'note-shadow')
|
||||
// .attr('width', '24px')
|
||||
// .attr('height', '24px')
|
||||
// .attr('x', '-12px')
|
||||
// .attr('y', '-24px')
|
||||
// .attr('xlink:href', '#iD-icon-note');
|
||||
|
||||
notesEnter
|
||||
.append('use')
|
||||
.attr('class', 'note-fill')
|
||||
.attr('width', '20px')
|
||||
.attr('height', '20px')
|
||||
.attr('x', '-10px')
|
||||
.attr('y', '-22px')
|
||||
.attr('xlink:href', '#iD-icon-note');
|
||||
|
||||
// add dots if there's a comment thread
|
||||
notesEnter.selectAll('.note-annotation')
|
||||
.data(function(d) { return d.comments.length > 1 ? [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-more');
|
||||
|
||||
// update
|
||||
notes
|
||||
.merge(notesEnter)
|
||||
.sort(function(a, b) {
|
||||
return (a.id === selectedID) ? 1
|
||||
: (b.id === selectedID) ? -1
|
||||
: b.loc[1] - a.loc[1]; // sort Y
|
||||
})
|
||||
.classed('selected', function(d) { return d.id === selectedID; })
|
||||
.attr('transform', transform);
|
||||
}
|
||||
|
||||
|
||||
function drawNotes(selection) {
|
||||
var enabled = svgNotes.enabled;
|
||||
var service = getService();
|
||||
|
||||
layer = selection.selectAll('.layer-notes')
|
||||
.data(service ? [0] : []);
|
||||
|
||||
layer.exit()
|
||||
.remove();
|
||||
|
||||
layer.enter()
|
||||
.append('g')
|
||||
.attr('class', 'layer-notes')
|
||||
.style('display', enabled ? 'block' : 'none')
|
||||
.merge(layer);
|
||||
|
||||
function dimensions() {
|
||||
return [window.innerWidth, window.innerHeight];
|
||||
}
|
||||
|
||||
if (enabled) {
|
||||
if (service && ~~context.map().zoom() >= minZoom) {
|
||||
editOn();
|
||||
service.loadNotes(projection, dimensions());
|
||||
update();
|
||||
} else {
|
||||
editOff();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
drawNotes.enabled = function(_) {
|
||||
if (!arguments.length) return svgNotes.enabled;
|
||||
svgNotes.enabled = _;
|
||||
if (svgNotes.enabled) {
|
||||
showLayer();
|
||||
} else {
|
||||
hideLayer();
|
||||
}
|
||||
dispatch.call('change');
|
||||
return this;
|
||||
};
|
||||
|
||||
init();
|
||||
return drawNotes;
|
||||
}
|
||||
@@ -220,14 +220,14 @@ export function uiCommit(context) {
|
||||
|
||||
buttonEnter
|
||||
.append('button')
|
||||
.attr('class', 'secondary-action col5 button cancel-button')
|
||||
.attr('class', 'secondary-action button cancel-button')
|
||||
.append('span')
|
||||
.attr('class', 'label')
|
||||
.text(t('commit.cancel'));
|
||||
|
||||
buttonEnter
|
||||
.append('button')
|
||||
.attr('class', 'action col5 button save-button')
|
||||
.attr('class', 'action button save-button')
|
||||
.append('span')
|
||||
.attr('class', 'label')
|
||||
.text(t('commit.save'));
|
||||
|
||||
@@ -34,6 +34,10 @@ export { uiMapInMap } from './map_in_map';
|
||||
export { uiModal } from './modal';
|
||||
export { uiModes } from './modes';
|
||||
export { uiNotice } from './notice';
|
||||
export { uiNoteComments } from './note_comments';
|
||||
export { uiNoteEditor } from './note_editor';
|
||||
export { uiNoteHeader } from './note_header';
|
||||
export { uiNoteReport } from './note_report';
|
||||
export { uiPresetEditor } from './preset_editor';
|
||||
export { uiPresetIcon } from './preset_icon';
|
||||
export { uiPresetList } from './preset_list';
|
||||
|
||||
@@ -44,11 +44,12 @@ export function uiInspector(context) {
|
||||
var presetPane = wrap.selectAll('.preset-list-pane');
|
||||
var editorPane = wrap.selectAll('.entity-editor-pane');
|
||||
|
||||
var graph = context.graph(),
|
||||
entity = context.entity(_entityID),
|
||||
showEditor = _state === 'hover' ||
|
||||
entity.isUsed(graph) ||
|
||||
entity.isHighwayIntersection(graph);
|
||||
var graph = context.graph();
|
||||
var entity = context.entity(_entityID);
|
||||
|
||||
var showEditor = _state === 'hover' ||
|
||||
entity.isUsed(graph) ||
|
||||
entity.isHighwayIntersection(graph);
|
||||
|
||||
if (showEditor) {
|
||||
wrap.style('right', '0%');
|
||||
@@ -67,7 +68,9 @@ export function uiInspector(context) {
|
||||
.merge(footer);
|
||||
|
||||
footer
|
||||
.call(uiViewOnOSM(context).entityID(_entityID));
|
||||
.call(uiViewOnOSM(context)
|
||||
.what(context.hasEntity(_entityID))
|
||||
);
|
||||
|
||||
|
||||
function showList(preset) {
|
||||
|
||||
@@ -71,7 +71,7 @@ export function uiIntro(context) {
|
||||
var background = context.background().baseLayerSource();
|
||||
var overlays = context.background().overlayLayerSources();
|
||||
var opacity = d3_selectAll('#map .layer-background').style('opacity');
|
||||
var loadedTiles = osm && osm.loadedTiles();
|
||||
var caches = osm && osm.caches();
|
||||
var baseEntities = context.history().graph().base().entities;
|
||||
var countryCode = services.geocoder.countryCode;
|
||||
|
||||
@@ -147,7 +147,7 @@ export function uiIntro(context) {
|
||||
curtain.remove();
|
||||
navwrap.remove();
|
||||
d3_selectAll('#map .layer-background').style('opacity', opacity);
|
||||
if (osm) { osm.toggle(true).reset().loadedTiles(loadedTiles); }
|
||||
if (osm) { osm.toggle(true).reset().caches(caches); }
|
||||
context.history().reset().merge(_values(baseEntities));
|
||||
context.background().baseLayerSource(background);
|
||||
overlays.forEach(function (d) { context.background().toggleOverlayLayer(d); });
|
||||
|
||||
@@ -86,7 +86,7 @@ export function uiMapData(context) {
|
||||
|
||||
|
||||
function drawPhotoItems(selection) {
|
||||
var photoKeys = ['streetside','mapillary-images', 'mapillary-signs', 'openstreetcam-images'];
|
||||
var photoKeys = ['streetside', 'mapillary-images', 'mapillary-signs', 'openstreetcam-images'];
|
||||
var photoLayers = layers.all().filter(function(obj) { return photoKeys.indexOf(obj.id) !== -1; });
|
||||
var data = photoLayers.filter(function(obj) { return obj.layer.supported(); });
|
||||
|
||||
@@ -147,58 +147,64 @@ export function uiMapData(context) {
|
||||
}
|
||||
|
||||
|
||||
function drawOsmItem(selection) {
|
||||
var osm = layers.layer('osm'),
|
||||
showsOsm = osm.enabled();
|
||||
function drawOsmItems(selection) {
|
||||
var osmKeys = ['osm', 'notes'];
|
||||
var osmLayers = layers.all().filter(function(obj) { return osmKeys.indexOf(obj.id) !== -1; });
|
||||
|
||||
var ul = selection
|
||||
.selectAll('.layer-list-osm')
|
||||
.data(osm ? [0] : []);
|
||||
.data([0]);
|
||||
|
||||
// Exit
|
||||
ul.exit()
|
||||
ul = ul.enter()
|
||||
.append('ul')
|
||||
.attr('class', 'layer-list layer-list-osm')
|
||||
.merge(ul);
|
||||
|
||||
var li = ul.selectAll('.list-item')
|
||||
.data(osmLayers);
|
||||
|
||||
li.exit()
|
||||
.remove();
|
||||
|
||||
// Enter
|
||||
var ulEnter = ul.enter()
|
||||
.append('ul')
|
||||
.attr('class', 'layer-list layer-list-osm');
|
||||
|
||||
var liEnter = ulEnter
|
||||
var liEnter = li.enter()
|
||||
.append('li')
|
||||
.attr('class', 'list-item-osm');
|
||||
.attr('class', function(d) { return 'list-item list-item-' + d.id; });
|
||||
|
||||
var labelEnter = liEnter
|
||||
.append('label')
|
||||
.call(tooltip()
|
||||
.title(t('map_data.layers.osm.tooltip'))
|
||||
.placement('bottom')
|
||||
);
|
||||
.each(function(d) {
|
||||
d3_select(this)
|
||||
.call(tooltip()
|
||||
.title(t('map_data.layers.' + d.id + '.tooltip'))
|
||||
.placement('bottom')
|
||||
);
|
||||
});
|
||||
|
||||
labelEnter
|
||||
.append('input')
|
||||
.attr('type', 'checkbox')
|
||||
.on('change', function() { toggleLayer('osm'); });
|
||||
.on('change', function(d) { toggleLayer(d.id); });
|
||||
|
||||
labelEnter
|
||||
.append('span')
|
||||
.text(t('map_data.layers.osm.title'));
|
||||
.text(function(d) { return t('map_data.layers.' + d.id + '.title'); });
|
||||
|
||||
|
||||
// Update
|
||||
ul = ul
|
||||
.merge(ulEnter);
|
||||
li = li
|
||||
.merge(liEnter);
|
||||
|
||||
ul.selectAll('.list-item-osm')
|
||||
.classed('active', showsOsm)
|
||||
li
|
||||
.classed('active', function (d) { return d.layer.enabled(); })
|
||||
.selectAll('input')
|
||||
.property('checked', showsOsm);
|
||||
.property('checked', function (d) { return d.layer.enabled(); });
|
||||
}
|
||||
|
||||
|
||||
function drawGpxItem(selection) {
|
||||
var gpx = layers.layer('gpx'),
|
||||
hasGpx = gpx && gpx.hasGpx(),
|
||||
showsGpx = hasGpx && gpx.enabled();
|
||||
var gpx = layers.layer('gpx');
|
||||
var hasGpx = gpx && gpx.hasGpx();
|
||||
var showsGpx = hasGpx && gpx.enabled();
|
||||
|
||||
var ul = selection
|
||||
.selectAll('.layer-list-gpx')
|
||||
@@ -448,7 +454,7 @@ export function uiMapData(context) {
|
||||
|
||||
function update() {
|
||||
_dataLayerContainer
|
||||
.call(drawOsmItem)
|
||||
.call(drawOsmItems)
|
||||
.call(drawPhotoItems)
|
||||
.call(drawGpxItem);
|
||||
// .call(drawMvtItem);
|
||||
|
||||
116
modules/ui/note_comments.js
Normal file
116
modules/ui/note_comments.js
Normal file
@@ -0,0 +1,116 @@
|
||||
import { select as d3_select } from 'd3-selection';
|
||||
|
||||
import { t } from '../util/locale';
|
||||
import { svgIcon } from '../svg';
|
||||
import { services } from '../services';
|
||||
import { utilDetect } from '../util/detect';
|
||||
|
||||
|
||||
export function uiNoteComments() {
|
||||
var _note;
|
||||
|
||||
|
||||
function noteComments(selection) {
|
||||
var comments = selection.selectAll('.comments-container')
|
||||
.data([0]);
|
||||
|
||||
comments = comments.enter()
|
||||
.append('div')
|
||||
.attr('class', 'comments-container')
|
||||
.merge(comments);
|
||||
|
||||
var commentEnter = comments.selectAll('.comment')
|
||||
.data(_note.comments)
|
||||
.enter()
|
||||
.append('div')
|
||||
.attr('class', 'comment');
|
||||
|
||||
commentEnter
|
||||
.append('div')
|
||||
.attr('class', function(d) { return 'comment-avatar user-' + d.uid; })
|
||||
.call(svgIcon('#iD-icon-avatar', 'comment-avatar-icon'));
|
||||
|
||||
var mainEnter = commentEnter
|
||||
.append('div')
|
||||
.attr('class', 'comment-main');
|
||||
|
||||
var metadataEnter = mainEnter
|
||||
.append('div')
|
||||
.attr('class', 'comment-metadata');
|
||||
|
||||
metadataEnter
|
||||
.append('div')
|
||||
.attr('class', 'comment-author')
|
||||
.each(function(d) {
|
||||
var selection = d3_select(this);
|
||||
var osm = services.osm;
|
||||
if (osm && d.user) {
|
||||
selection = selection
|
||||
.append('a')
|
||||
.attr('class', 'comment-author-link')
|
||||
.attr('href', osm.userURL(d.user))
|
||||
.attr('tabindex', -1)
|
||||
.attr('target', '_blank');
|
||||
}
|
||||
selection
|
||||
.text(function(d) { return d.user || t('note.anonymous'); });
|
||||
});
|
||||
|
||||
metadataEnter
|
||||
.append('div')
|
||||
.attr('class', 'comment-date')
|
||||
.text(function(d) { return d.action + ' ' + localeDateString(d.date); });
|
||||
|
||||
mainEnter
|
||||
.append('div')
|
||||
.attr('class', 'comment-text')
|
||||
.text(function(d) { return d.text; });
|
||||
|
||||
comments
|
||||
.call(replaceAvatars);
|
||||
}
|
||||
|
||||
|
||||
function replaceAvatars(selection) {
|
||||
var osm = services.osm;
|
||||
if (!osm) return;
|
||||
|
||||
var uids = {}; // gather uids in the comment thread
|
||||
_note.comments.forEach(function(d) {
|
||||
if (d.uid) uids[d.uid] = true;
|
||||
});
|
||||
|
||||
Object.keys(uids).forEach(function(uid) {
|
||||
osm.loadUser(uid, function(err, user) {
|
||||
if (!user || !user.image_url) return;
|
||||
|
||||
selection.selectAll('.comment-avatar.user-' + uid)
|
||||
.html('')
|
||||
.append('img')
|
||||
.attr('class', 'icon comment-avatar-icon')
|
||||
.attr('src', user.image_url)
|
||||
.attr('alt', user.display_name);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function localeDateString(s) {
|
||||
if (!s) return null;
|
||||
var detected = utilDetect();
|
||||
var options = { day: 'numeric', month: 'short', year: 'numeric' };
|
||||
var d = new Date(s);
|
||||
if (isNaN(d.getTime())) return null;
|
||||
return d.toLocaleDateString(detected.locale, options);
|
||||
}
|
||||
|
||||
|
||||
noteComments.note = function(_) {
|
||||
if (!arguments.length) return _note;
|
||||
_note = _;
|
||||
return noteComments;
|
||||
};
|
||||
|
||||
|
||||
return noteComments;
|
||||
}
|
||||
201
modules/ui/note_editor.js
Normal file
201
modules/ui/note_editor.js
Normal file
@@ -0,0 +1,201 @@
|
||||
import { dispatch as d3_dispatch } from 'd3-dispatch';
|
||||
import { select as d3_select } from 'd3-selection';
|
||||
|
||||
import { t } from '../util/locale';
|
||||
import { services } from '../services';
|
||||
import { modeBrowse } from '../modes';
|
||||
import { svgIcon } from '../svg';
|
||||
|
||||
import {
|
||||
uiNoteComments,
|
||||
uiNoteHeader,
|
||||
uiNoteReport,
|
||||
uiViewOnOSM,
|
||||
} from './index';
|
||||
|
||||
import {
|
||||
utilNoAuto,
|
||||
utilRebind
|
||||
} from '../util';
|
||||
|
||||
|
||||
export function uiNoteEditor(context) {
|
||||
var dispatch = d3_dispatch('change');
|
||||
var noteComments = uiNoteComments();
|
||||
var noteHeader = uiNoteHeader();
|
||||
var _note;
|
||||
|
||||
|
||||
function noteEditor(selection) {
|
||||
var header = selection.selectAll('.header')
|
||||
.data([0]);
|
||||
|
||||
var headerEnter = header.enter()
|
||||
.append('div')
|
||||
.attr('class', 'header fillL');
|
||||
|
||||
headerEnter
|
||||
.append('button')
|
||||
.attr('class', 'fr note-editor-close')
|
||||
.on('click', function() { context.enter(modeBrowse(context)); })
|
||||
.call(svgIcon('#iD-icon-close'));
|
||||
|
||||
headerEnter
|
||||
.append('h3')
|
||||
.text(t('note.title'));
|
||||
|
||||
|
||||
var body = selection.selectAll('.body')
|
||||
.data([0]);
|
||||
|
||||
body = body.enter()
|
||||
.append('div')
|
||||
.attr('class', 'body')
|
||||
.merge(body);
|
||||
|
||||
body.selectAll('.note-editor')
|
||||
.data([0])
|
||||
.enter()
|
||||
.append('div')
|
||||
.attr('class', 'modal-section note-editor')
|
||||
.call(noteHeader.note(_note))
|
||||
.call(noteComments.note(_note))
|
||||
.call(noteSave);
|
||||
|
||||
|
||||
selection.selectAll('.footer')
|
||||
.data([0])
|
||||
.enter()
|
||||
.append('div')
|
||||
.attr('class', 'footer')
|
||||
.call(uiViewOnOSM(context).what(_note))
|
||||
.call(uiNoteReport(context).note(_note));
|
||||
}
|
||||
|
||||
|
||||
function noteSave(selection) {
|
||||
var isSelected = (_note && _note.id === context.selectedNoteID());
|
||||
var noteSave = selection.selectAll('.note-save-section')
|
||||
.data((isSelected ? [_note] : []), function(d) { return d.status + d.id; });
|
||||
|
||||
// exit
|
||||
noteSave.exit()
|
||||
.remove();
|
||||
|
||||
// enter
|
||||
var noteSaveEnter = noteSave.enter()
|
||||
.append('div')
|
||||
.attr('class', 'note-save-section save-section cf');
|
||||
|
||||
noteSaveEnter
|
||||
.append('h4')
|
||||
.attr('class', '.note-save-header')
|
||||
.text(t('note.newComment'));
|
||||
|
||||
noteSaveEnter
|
||||
.append('textarea')
|
||||
.attr('id', 'new-comment-input')
|
||||
.attr('placeholder', t('note.inputPlaceholder'))
|
||||
.attr('maxlength', 1000)
|
||||
.property('value', function(d) { return d.newComment; })
|
||||
.call(utilNoAuto)
|
||||
.on('input', change)
|
||||
.on('blur', change);
|
||||
|
||||
// update
|
||||
noteSave = noteSaveEnter
|
||||
.merge(noteSave)
|
||||
.call(noteSaveButtons);
|
||||
|
||||
|
||||
function change() {
|
||||
var input = d3_select(this);
|
||||
var val = input.property('value').trim() || undefined;
|
||||
|
||||
// store the unsaved comment with the note itself
|
||||
_note = _note.update({ newComment: val });
|
||||
|
||||
var osm = services.osm;
|
||||
if (osm) {
|
||||
osm.replaceNote(_note); // update note cache
|
||||
}
|
||||
|
||||
noteSave
|
||||
.call(noteSaveButtons);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function noteSaveButtons(selection) {
|
||||
var isSelected = (_note && _note.id === context.selectedNoteID());
|
||||
var buttonSection = selection.selectAll('.buttons')
|
||||
.data((isSelected ? [_note] : []), function(d) { return d.status + d.id; });
|
||||
|
||||
// exit
|
||||
buttonSection.exit()
|
||||
.remove();
|
||||
|
||||
// enter
|
||||
var buttonEnter = buttonSection.enter()
|
||||
.append('div')
|
||||
.attr('class', 'buttons');
|
||||
|
||||
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'));
|
||||
|
||||
// update
|
||||
buttonSection = buttonSection
|
||||
.merge(buttonEnter);
|
||||
|
||||
buttonSection.select('.status-button') // select and propagate data
|
||||
.text(function(d) {
|
||||
var action = (d.status === 'open' ? 'close' : 'open');
|
||||
var andComment = (d.newComment ? '_comment' : '');
|
||||
return t('note.' + action + andComment);
|
||||
})
|
||||
.on('click.status', function(d) {
|
||||
this.blur(); // avoid keeping focus on the button - #4641
|
||||
var osm = services.osm;
|
||||
if (osm) {
|
||||
var setStatus = (d.status === 'open' ? 'closed' : 'open');
|
||||
osm.postNoteUpdate(d, setStatus, function(err, note) {
|
||||
dispatch.call('change', note);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
buttonSection.select('.comment-button') // select and propagate data
|
||||
.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.postNoteUpdate(d, d.status, function(err, note) {
|
||||
dispatch.call('change', note);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
noteEditor.note = function(_) {
|
||||
if (!arguments.length) return _note;
|
||||
_note = _;
|
||||
return noteEditor;
|
||||
};
|
||||
|
||||
|
||||
return utilRebind(noteEditor, dispatch, 'on');
|
||||
}
|
||||
59
modules/ui/note_header.js
Normal file
59
modules/ui/note_header.js
Normal file
@@ -0,0 +1,59 @@
|
||||
import { t } from '../util/locale';
|
||||
import { svgIcon } from '../svg';
|
||||
|
||||
|
||||
export function uiNoteHeader() {
|
||||
var _note;
|
||||
|
||||
|
||||
function noteHeader(selection) {
|
||||
var header = selection.selectAll('.note-header')
|
||||
.data(
|
||||
(_note ? [_note] : []),
|
||||
function(d) { return d.status + d.id; }
|
||||
);
|
||||
|
||||
header.exit()
|
||||
.remove();
|
||||
|
||||
var headerEnter = header.enter()
|
||||
.append('div')
|
||||
.attr('class', 'note-header');
|
||||
|
||||
var iconEnter = headerEnter
|
||||
.append('div')
|
||||
.attr('class', function(d) { return 'note-header-icon ' + d.status; });
|
||||
|
||||
iconEnter
|
||||
.append('div')
|
||||
.attr('class', 'preset-icon-28')
|
||||
.call(svgIcon('#iD-icon-note', 'note-fill'));
|
||||
|
||||
iconEnter.each(function(d) {
|
||||
if (d.comments.length > 1) {
|
||||
iconEnter
|
||||
.append('div')
|
||||
.attr('class', 'note-icon-annotation')
|
||||
.call(svgIcon('#iD-icon-more', 'note-annotation'));
|
||||
}
|
||||
});
|
||||
|
||||
headerEnter
|
||||
.append('div')
|
||||
.attr('class', 'note-header-label')
|
||||
.text(function(d) {
|
||||
return t('note.note') + ' ' + d.id + ' ' +
|
||||
(d.status === 'closed' ? t('note.closed') : '');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
noteHeader.note = function(_) {
|
||||
if (!arguments.length) return _note;
|
||||
_note = _;
|
||||
return noteHeader;
|
||||
};
|
||||
|
||||
|
||||
return noteHeader;
|
||||
}
|
||||
47
modules/ui/note_report.js
Normal file
47
modules/ui/note_report.js
Normal file
@@ -0,0 +1,47 @@
|
||||
import { t } from '../util/locale';
|
||||
import { svgIcon } from '../svg';
|
||||
import {
|
||||
osmNote
|
||||
} from '../osm';
|
||||
|
||||
|
||||
export function uiNoteReport() {
|
||||
var _note;
|
||||
var url = 'https://www.openstreetmap.org/reports/new?reportable_id=';
|
||||
|
||||
function noteReport(selection) {
|
||||
|
||||
if (!(_note instanceof osmNote)) return;
|
||||
|
||||
url += _note.id + '&reportable_type=Note';
|
||||
|
||||
var data = ((!_note || _note.isNew()) ? [] : [_note]);
|
||||
var link = selection.selectAll('.note-report')
|
||||
.data(data, function(d) { return d.id; });
|
||||
|
||||
// exit
|
||||
link.exit()
|
||||
.remove();
|
||||
|
||||
// enter
|
||||
var linkEnter = link.enter()
|
||||
.append('a')
|
||||
.attr('class', 'note-report')
|
||||
.attr('target', '_blank')
|
||||
.attr('href', url)
|
||||
.call(svgIcon('#iD-icon-out-link', 'inline'));
|
||||
|
||||
linkEnter
|
||||
.append('span')
|
||||
.text(t('note.report'));
|
||||
}
|
||||
|
||||
|
||||
noteReport.note = function(_) {
|
||||
if (!arguments.length) return _note;
|
||||
_note = _;
|
||||
return noteReport;
|
||||
};
|
||||
|
||||
return noteReport;
|
||||
}
|
||||
@@ -1,11 +1,19 @@
|
||||
import _throttle from 'lodash-es/throttle';
|
||||
|
||||
import { selectAll as d3_selectAll } from 'd3-selection';
|
||||
|
||||
import { osmNote } from '../osm';
|
||||
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);
|
||||
var noteEditor = uiNoteEditor(context);
|
||||
var _current;
|
||||
var _wasNote = false;
|
||||
// var layer = d3_select(null);
|
||||
|
||||
|
||||
function sidebar(selection) {
|
||||
@@ -20,8 +28,19 @@ export function uiSidebar(context) {
|
||||
.attr('class', 'inspector-hidden inspector-wrap fr');
|
||||
|
||||
|
||||
function hover(id) {
|
||||
if (!current && context.hasEntity(id)) {
|
||||
function hover(what) {
|
||||
if ((what instanceof osmNote)) {
|
||||
_wasNote = true;
|
||||
var notes = d3_selectAll('.note');
|
||||
notes
|
||||
.classed('hovered', function(d) { return d === what; });
|
||||
|
||||
sidebar.show(noteEditor.note(what));
|
||||
|
||||
selection.selectAll('.sidebar-component')
|
||||
.classed('inspector-hover', true);
|
||||
|
||||
} else if (!_current && context.hasEntity(what)) {
|
||||
featureListWrap
|
||||
.classed('inspector-hidden', true);
|
||||
|
||||
@@ -29,22 +48,28 @@ export function uiSidebar(context) {
|
||||
.classed('inspector-hidden', false)
|
||||
.classed('inspector-hover', true);
|
||||
|
||||
if (inspector.entityID() !== id || inspector.state() !== 'hover') {
|
||||
if (inspector.entityID() !== what || inspector.state() !== 'hover') {
|
||||
inspector
|
||||
.state('hover')
|
||||
.entityID(id);
|
||||
.entityID(what);
|
||||
|
||||
inspectorWrap
|
||||
.call(inspector);
|
||||
}
|
||||
|
||||
} else if (!current) {
|
||||
} else if (!_current) {
|
||||
featureListWrap
|
||||
.classed('inspector-hidden', false);
|
||||
inspectorWrap
|
||||
.classed('inspector-hidden', true);
|
||||
inspector
|
||||
.state('hide');
|
||||
|
||||
} else if (_wasNote) {
|
||||
_wasNote = false;
|
||||
d3_selectAll('.note')
|
||||
.classed('hovered', false);
|
||||
sidebar.hide();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,7 +78,7 @@ export function uiSidebar(context) {
|
||||
|
||||
|
||||
sidebar.select = function(id, newFeature) {
|
||||
if (!current && id) {
|
||||
if (!_current && id) {
|
||||
featureListWrap
|
||||
.classed('inspector-hidden', true);
|
||||
|
||||
@@ -71,7 +96,7 @@ export function uiSidebar(context) {
|
||||
.call(inspector);
|
||||
}
|
||||
|
||||
} else if (!current) {
|
||||
} else if (!_current) {
|
||||
featureListWrap
|
||||
.classed('inspector-hidden', false);
|
||||
inspectorWrap
|
||||
@@ -82,17 +107,17 @@ export function uiSidebar(context) {
|
||||
};
|
||||
|
||||
|
||||
sidebar.show = function(component) {
|
||||
sidebar.show = function(component, element) {
|
||||
featureListWrap
|
||||
.classed('inspector-hidden', true);
|
||||
inspectorWrap
|
||||
.classed('inspector-hidden', true);
|
||||
|
||||
if (current) current.remove();
|
||||
current = selection
|
||||
if (_current) _current.remove();
|
||||
_current = selection
|
||||
.append('div')
|
||||
.attr('class', 'sidebar-component')
|
||||
.call(component);
|
||||
.call(component, element);
|
||||
};
|
||||
|
||||
|
||||
@@ -102,8 +127,8 @@ export function uiSidebar(context) {
|
||||
inspectorWrap
|
||||
.classed('inspector-hidden', true);
|
||||
|
||||
if (current) current.remove();
|
||||
current = null;
|
||||
if (_current) _current.remove();
|
||||
_current = null;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,37 +1,48 @@
|
||||
import { t } from '../util/locale';
|
||||
import { svgIcon } from '../svg';
|
||||
import {
|
||||
osmEntity,
|
||||
osmNote
|
||||
} from '../osm';
|
||||
|
||||
|
||||
export function uiViewOnOSM(context) {
|
||||
var id;
|
||||
var _what; // an osmEntity or osmNote
|
||||
|
||||
|
||||
function viewOnOSM(selection) {
|
||||
var entity = context.entity(id);
|
||||
|
||||
selection.style('display', entity.isNew() ? 'none' : null);
|
||||
var url;
|
||||
if (_what instanceof osmEntity) {
|
||||
url = context.connection().entityURL(_what);
|
||||
} else if (_what instanceof osmNote) {
|
||||
url = context.connection().noteURL(_what);
|
||||
}
|
||||
|
||||
var data = ((!_what || _what.isNew()) ? [] : [_what]);
|
||||
var link = selection.selectAll('.view-on-osm')
|
||||
.data([0]);
|
||||
.data(data, function(d) { return d.id; });
|
||||
|
||||
var enter = link.enter()
|
||||
// exit
|
||||
link.exit()
|
||||
.remove();
|
||||
|
||||
// enter
|
||||
var linkEnter = link.enter()
|
||||
.append('a')
|
||||
.attr('class', 'view-on-osm')
|
||||
.attr('target', '_blank')
|
||||
.attr('href', url)
|
||||
.call(svgIcon('#iD-icon-out-link', 'inline'));
|
||||
|
||||
enter
|
||||
linkEnter
|
||||
.append('span')
|
||||
.text(t('inspector.view_on_osm'));
|
||||
|
||||
link
|
||||
.merge(enter)
|
||||
.attr('href', context.connection().entityURL(entity));
|
||||
}
|
||||
|
||||
|
||||
viewOnOSM.entityID = function(_) {
|
||||
if (!arguments.length) return id;
|
||||
id = _;
|
||||
viewOnOSM.what = function(_) {
|
||||
if (!arguments.length) return _what;
|
||||
_what = _;
|
||||
return viewOnOSM;
|
||||
};
|
||||
|
||||
|
||||
5
svg/iD-sprite/icons/icon-note.svg
Normal file
5
svg/iD-sprite/icons/icon-note.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-20 -20 552 552">
|
||||
<path stroke="inherit" stroke-width="40px" fill="currentColor" d="M448 0H64C28.7 0 0 28.7 0 64v288c0 35.3 28.7 64 64 64h96v84c0 9.8 11.2 15.5 19.1 9.7L304 416h144c35.3 0 64-28.7 64-64V64c0-35.3-28.7-64-64-64z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 428 B |
@@ -89,6 +89,7 @@
|
||||
<script src='spec/osm/multipolygon.js'></script>
|
||||
<script src='spec/osm/lanes.js'></script>
|
||||
<script src='spec/osm/node.js'></script>
|
||||
<script src='spec/osm/note.js'></script>
|
||||
<script src='spec/osm/relation.js'></script>
|
||||
<script src='spec/osm/way.js'></script>
|
||||
|
||||
|
||||
15
test/spec/osm/note.js
Normal file
15
test/spec/osm/note.js
Normal file
@@ -0,0 +1,15 @@
|
||||
describe('iD.osmNote', function () {
|
||||
it('returns a note', function () {
|
||||
expect(iD.osmNote()).to.be.an.instanceOf(iD.osmNote);
|
||||
expect(iD.osmNote().type).to.equal('note');
|
||||
});
|
||||
|
||||
describe('#extent', function() {
|
||||
it('returns a note extent', function() {
|
||||
expect(iD.osmNote({loc: [5, 10]}).extent().equals([[5, 10], [5, 10]])).to.be.ok;
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: add tests for #update, or remove function
|
||||
|
||||
});
|
||||
@@ -137,8 +137,8 @@ describe('iD.serviceOsm', function () {
|
||||
});
|
||||
|
||||
describe('#loadFromAPI', function () {
|
||||
var path = '/api/0.6/map?bbox=-74.542,40.655,-74.541,40.656',
|
||||
response = '<?xml version="1.0" encoding="UTF-8"?>' +
|
||||
var path = '/api/0.6/map?bbox=-74.542,40.655,-74.541,40.656';
|
||||
var response = '<?xml version="1.0" encoding="UTF-8"?>' +
|
||||
'<osm version="0.6">' +
|
||||
' <bounds minlat="40.655" minlon="-74.542" maxlat="40.656" maxlon="-74.541' +
|
||||
' <node id="105340439" visible="true" version="2" changeset="2880013" timestamp="2009-10-18T07:47:39Z" user="woodpeck_fixbot" uid="147510" lat="40.6555" lon="-74.5415"/>' +
|
||||
@@ -290,14 +290,14 @@ describe('iD.serviceOsm', function () {
|
||||
);
|
||||
server.respond();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('#loadEntity', function () {
|
||||
var nodeXML = '<?xml version="1.0" encoding="UTF-8"?><osm>' +
|
||||
'<node id="1" version="1" changeset="1" lat="0" lon="0" visible="true" timestamp="2009-03-07T03:26:33Z"></node>' +
|
||||
'</osm>',
|
||||
wayXML = '<?xml version="1.0" encoding="UTF-8"?><osm>' +
|
||||
'</osm>';
|
||||
var wayXML = '<?xml version="1.0" encoding="UTF-8"?><osm>' +
|
||||
'<node id="1" version="1" changeset="2817006" lat="0" lon="0" visible="true" timestamp="2009-10-11T18:03:23Z"/>' +
|
||||
'<way id="1" visible="true" timestamp="2008-01-03T05:24:43Z" version="1" changeset="522559"><nd ref="1"/></way>' +
|
||||
'</osm>';
|
||||
@@ -355,11 +355,12 @@ describe('iD.serviceOsm', function () {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('#loadEntityVersion', function () {
|
||||
var nodeXML = '<?xml version="1.0" encoding="UTF-8"?><osm>' +
|
||||
'<node id="1" version="1" changeset="1" lat="0" lon="0" visible="true" timestamp="2009-03-07T03:26:33Z"></node>' +
|
||||
'</osm>',
|
||||
wayXML = '<?xml version="1.0" encoding="UTF-8"?><osm>' +
|
||||
'</osm>';
|
||||
var wayXML = '<?xml version="1.0" encoding="UTF-8"?><osm>' +
|
||||
'<way id="1" visible="true" timestamp="2008-01-03T05:24:43Z" version="1" changeset="522559"><nd ref="1"/></way>' +
|
||||
'</osm>';
|
||||
|
||||
@@ -416,6 +417,7 @@ describe('iD.serviceOsm', function () {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('#loadMultiple', function () {
|
||||
beforeEach(function() {
|
||||
server = sinon.fakeServer.create();
|
||||
@@ -428,7 +430,6 @@ describe('iD.serviceOsm', function () {
|
||||
it('loads nodes');
|
||||
it('loads ways');
|
||||
it('does not ignore repeat requests');
|
||||
|
||||
});
|
||||
|
||||
|
||||
@@ -536,6 +537,146 @@ describe('iD.serviceOsm', function () {
|
||||
|
||||
});
|
||||
|
||||
describe('#caches', function() {
|
||||
it('loads reset caches', function (done) {
|
||||
var resetCaches = {
|
||||
tile: {
|
||||
inflight: {}, loaded: {}, seen: {}
|
||||
},
|
||||
note: {
|
||||
loaded: {}, inflight: {}, inflightPost: {}, note: {} // not including rtree
|
||||
},
|
||||
user: {
|
||||
toLoad: {}, user: {}
|
||||
}
|
||||
};
|
||||
var caches = connection.caches();
|
||||
expect(caches.tile).to.eql(resetCaches.tile);
|
||||
expect(caches.note.loaded).to.eql(resetCaches.note.loaded);
|
||||
expect(caches.user).to.eql(resetCaches.user);
|
||||
done();
|
||||
});
|
||||
|
||||
describe('sets/gets caches', function() {
|
||||
it('sets/gets a tile', function (done) {
|
||||
var obj = {
|
||||
tile: { loaded: { '1,2,16': true, '3,4,16': true } }
|
||||
};
|
||||
connection.caches(obj);
|
||||
expect(connection.caches().tile.loaded['1,2,16']).to.eql(true);
|
||||
expect(Object.keys(connection.caches().tile.loaded).length).to.eql(2);
|
||||
done();
|
||||
});
|
||||
|
||||
it('sets/gets a note', function (done) {
|
||||
var note = iD.osmNote({ id: 1, loc: [0, 0] });
|
||||
var note2 = iD.osmNote({ id: 2, loc: [0, 0] });
|
||||
var obj = {
|
||||
note: { note: { 1: note, 2: note2 } }
|
||||
};
|
||||
connection.caches(obj);
|
||||
expect(connection.caches().note.note[note.id]).to.eql(note);
|
||||
expect(Object.keys(connection.caches().note.note).length).to.eql(2);
|
||||
done();
|
||||
});
|
||||
|
||||
it('sets/gets a user', function (done) {
|
||||
var user = { id: 1, display_name: 'Name' };
|
||||
var user2 = { id: 2, display_name: 'Name' };
|
||||
var obj = {
|
||||
user: { user: { 1: user, 2: user2 } }
|
||||
};
|
||||
connection.caches(obj);
|
||||
expect(connection.caches().user.user[user.id]).to.eql(user);
|
||||
expect(Object.keys(connection.caches().user.user).length).to.eql(2);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#loadNotes', function() {
|
||||
beforeEach(function() {
|
||||
context.projection
|
||||
.scale(116722210.56960216)
|
||||
.translate([244505613.61327893, 74865520.92230521])
|
||||
.clipExtent([[0,0], [609.34375, 826]]);
|
||||
});
|
||||
|
||||
it('fires loadedNotes when notes are loaded', function() {
|
||||
connection.on('loadedNotes', spy);
|
||||
connection.loadNotes(context.projection, [64, 64], {});
|
||||
|
||||
var url = 'http://www.openstreetmap.org/api/0.6/notes?limit=10000&closed=7&bbox=-120.05859375,34.45221847282654,-119.970703125,34.52466147177173';
|
||||
var notesXML = ''; // TODO: determine output even though this test note is closed and will be gone soon
|
||||
|
||||
server.respondWith('GET', url,
|
||||
[200, { 'Content-Type': 'text/xml' }, notesXML ]);
|
||||
server.respond();
|
||||
|
||||
expect(spy).to.have.been.calledOnce;
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('#notes', function() {
|
||||
beforeEach(function() {
|
||||
var dimensions = [64, 64];
|
||||
context.projection
|
||||
.scale(667544.214430109) // z14
|
||||
.translate([-116508, 0]) // 10,0
|
||||
.clipExtent([[0,0], dimensions]);
|
||||
});
|
||||
it('returns notes in the visible map area', function() {
|
||||
var notes = [
|
||||
{ minX: 10, minY: 0, maxX: 10, maxY: 0, data: { key: '0', loc: [10,0] } },
|
||||
{ minX: 10, minY: 0, maxX: 10, maxY: 0, data: { key: '1', loc: [10,0] } },
|
||||
{ minX: 10, minY: 1, maxX: 10, maxY: 1, data: { key: '2', loc: [10,1] } }
|
||||
];
|
||||
|
||||
connection.caches('get').note.rtree.load(notes);
|
||||
var res = connection.notes(context.projection);
|
||||
|
||||
expect(res).to.deep.eql([
|
||||
{ key: '0', loc: [10,0] },
|
||||
{ key: '1', loc: [10,0] }
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('#getNote', function() {
|
||||
it('returns a note', function (done) {
|
||||
var note = iD.osmNote({ id: 1, loc: [0, 0], });
|
||||
var obj = {
|
||||
note: { note: { 1: note } }
|
||||
};
|
||||
connection.caches(obj);
|
||||
var result = connection.getNote(1);
|
||||
expect(result).to.deep.equal(note);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('#replaceNote', function() {
|
||||
it('returns a new note', function (done) {
|
||||
var note = iD.osmNote({ id: 2, loc: [0, 0], });
|
||||
var result = connection.replaceNote(note);
|
||||
expect(result.id).to.eql(2);
|
||||
done();
|
||||
});
|
||||
|
||||
it('replaces a note', function (done) {
|
||||
var note = iD.osmNote({ id: 2, loc: [0, 0], });
|
||||
connection.replaceNote(note);
|
||||
note.status = 'closed';
|
||||
var result = connection.replaceNote(note);
|
||||
expect(result.status).to.eql('closed');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('API capabilities', function() {
|
||||
var capabilitiesXML = '<?xml version="1.0" encoding="UTF-8"?><osm>' +
|
||||
|
||||
@@ -26,15 +26,16 @@ describe('iD.svgLayers', function () {
|
||||
it('creates default data layers', function () {
|
||||
container.call(iD.svgLayers(projection, context));
|
||||
var nodes = container.selectAll('svg .data-layer').nodes();
|
||||
expect(nodes.length).to.eql(8);
|
||||
expect(nodes.length).to.eql(9);
|
||||
expect(d3.select(nodes[0]).classed('data-layer-osm')).to.be.true;
|
||||
expect(d3.select(nodes[1]).classed('data-layer-gpx')).to.be.true;
|
||||
expect(d3.select(nodes[2]).classed('data-layer-mvt')).to.be.true;
|
||||
expect(d3.select(nodes[3]).classed('data-layer-streetside')).to.be.true;
|
||||
expect(d3.select(nodes[4]).classed('data-layer-mapillary-images')).to.be.true;
|
||||
expect(d3.select(nodes[5]).classed('data-layer-mapillary-signs')).to.be.true;
|
||||
expect(d3.select(nodes[6]).classed('data-layer-openstreetcam-images')).to.be.true;
|
||||
expect(d3.select(nodes[7]).classed('data-layer-debug')).to.be.true;
|
||||
expect(d3.select(nodes[1]).classed('data-layer-notes')).to.be.true;
|
||||
expect(d3.select(nodes[2]).classed('data-layer-gpx')).to.be.true;
|
||||
expect(d3.select(nodes[3]).classed('data-layer-mvt')).to.be.true;
|
||||
expect(d3.select(nodes[4]).classed('data-layer-streetside')).to.be.true;
|
||||
expect(d3.select(nodes[5]).classed('data-layer-mapillary-images')).to.be.true;
|
||||
expect(d3.select(nodes[6]).classed('data-layer-mapillary-signs')).to.be.true;
|
||||
expect(d3.select(nodes[7]).classed('data-layer-openstreetcam-images')).to.be.true;
|
||||
expect(d3.select(nodes[8]).classed('data-layer-debug')).to.be.true;
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user