From a1d7d0515567d7a9c8d4e5246dbcd668b4f4a1c9 Mon Sep 17 00:00:00 2001 From: John Firebaugh Date: Mon, 3 Jun 2013 17:47:09 -0700 Subject: [PATCH] Feature List (#1545) --- css/app.css | 101 +++++++++++++++++++++-------- data/core.yaml | 1 + dist/locales/en.json | 3 +- index.html | 1 + js/id/ui/feature_list.js | 135 +++++++++++++++++++++++++++++++++++++++ js/id/ui/preset_icon.js | 26 +++++--- js/id/ui/preset_list.js | 5 +- js/id/ui/sidebar.js | 26 +++++--- 8 files changed, 251 insertions(+), 47 deletions(-) create mode 100644 js/id/ui/feature_list.js diff --git a/css/app.css b/css/app.css index bf826ef27..756036de5 100644 --- a/css/app.css +++ b/css/app.css @@ -182,14 +182,6 @@ input:focus { background-color: #F1F1F1; } -input.major { - width: 100%; - padding:5px 10px; - font-size: 18px; - font-weight: bold; - height:60px; -} - /* remove bottom border radius when combobox is open */ .combobox + * textarea:focus, .combobox + * input:focus { @@ -698,6 +690,10 @@ a:hover .icon.out-link { background-position: -500px -14px;} bottom: 0; } +.feature-list-pane .inspector-body { + top: 120px; +} + .preset-list-pane .inspector-body { top: 120px; } @@ -711,6 +707,78 @@ a:hover .icon.out-link { background-position: -500px -14px;} position: relative; } +#sidebar .search-header .icon { + display: block; + position: absolute; + left: 10px; + top: 80px; + pointer-events: none; +} + +#sidebar .search-header input { + position: absolute; + top: 60px; + height: 60px; + width: 100%; + padding: 5px 10px; + border-radius: 0; + border-width: 0; + border-bottom-width: 1px; + text-indent: 30px; + font-size: 18px; + font-weight: bold; +} + +/* Feature list */ + +.feature-list { + width:100%; + padding: 20px 20px 10px 20px; + border-bottom: 1px solid #ccc; +} + +.feature-list-button-wrap { + position: relative; + margin-bottom: 10px; + height: 60px; +} + +.feature-list-button { + width: 100%; + height: 100%; + position: relative; + border: 1px solid #ccc; +} + +.feature-list-button .label { + background-color: #f6f6f6; + text-align: left; + position: absolute; + top: 0; + bottom: 0; + right: 0; + padding: 5px 10px; + left: 60px; + line-height: 50px; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + border-left: 1px solid rgba(0, 0, 0, .1); + -moz-transition: all 100ms; + -o-transition: all 100ms; + transition: all 100ms; + border-radius: 0 3px 3px 0; +} + +.feature-list-button:hover .label { + background-color: #ececec; +} + +.feature-list-button .entity-name { + font-weight: normal; + padding-left: 10px; +} + /* Presets ------------------------------------------------------- */ @@ -803,23 +871,6 @@ a:hover .icon.out-link { background-position: -500px -14px;} top: -3px; } -.preset-list-pane .preset-search-icon { - display: block; - position: absolute; - left: 10px; - top: 80px; - pointer-events: none; -} - -.preset-list-pane .preset-search-input { - position: absolute; - top: 60px; - border-radius: 0; - border-width: 0; - border-bottom-width: 1px; - text-indent: 30px; -} - .subgrid .preset-list { padding: 10px 10px 0 10px; border: 1px solid #CCC; diff --git a/data/core.yaml b/data/core.yaml index 0a065c096..c50fd9d9c 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -182,6 +182,7 @@ en: search: Search unknown: Unknown incomplete: + feature_list: Feature List background: title: Background description: Background settings diff --git a/dist/locales/en.json b/dist/locales/en.json index 3255bf010..99f216c42 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -223,7 +223,8 @@ "remove": "Remove", "search": "Search", "unknown": "Unknown", - "incomplete": "" + "incomplete": "", + "feature_list": "Feature List" }, "background": { "title": "Background", diff --git a/index.html b/index.html index 11c0f8941..d83fde6f3 100644 --- a/index.html +++ b/index.html @@ -95,6 +95,7 @@ + diff --git a/js/id/ui/feature_list.js b/js/id/ui/feature_list.js new file mode 100644 index 000000000..1b0210f02 --- /dev/null +++ b/js/id/ui/feature_list.js @@ -0,0 +1,135 @@ +iD.ui.FeatureList = function(context) { + function featureList(selection) { + var header = selection.append('div') + .attr('class', 'header fillL cf'); + + header.append('h3') + .text(t('inspector.feature_list')); + + function keyup() { + var q = search.property('value'); + if (d3.event.keyCode === 13 && q.length) { + click(list.selectAll('.feature-list-item:first-child').datum()); + } else { + drawList(); + } + } + + var searchWrap = selection.append('div') + .attr('class', 'search-header'); + + var search = searchWrap.append('input') + .attr('placeholder', t('inspector.search')) + .attr('type', 'search') + .on('keyup', keyup); + + searchWrap.append('span') + .attr('class', 'icon search'); + + var listWrap = selection.append('div') + .attr('class', 'inspector-body'); + + var list = listWrap.append('div') + .attr('class', 'feature-list fillL cf'); + + drawList(); + + context.history() + .on('change.feature-list', drawList); + + context.map() + .on('move', drawList); + + function features() { + var result = [], + graph = context.graph(), + q = search.property('value').toLowerCase(); + + context.intersects(context.extent()).forEach(function(entity) { + var preset = context.presets().match(entity, context.graph()), + name = iD.util.displayName(entity) || ''; + + if (entity.geometry(graph) === 'vertex') + return; + + if (q && name.toLowerCase().indexOf(q) === -1 && + preset.name().toLowerCase().indexOf(q) === -1) + return; + + result.push({ + entity: entity, + geometry: context.geometry(entity.id), + preset: preset, + name: name + }); + }); + + return result; + } + + function drawList() { + list.classed('filtered', search.property('value').length); + + var items = list.selectAll('.feature-list-item') + .data(features(), function(d) { return d.entity.id; }); + + var enter = items.enter().append('div') + .attr('class', 'feature-list-item'); + + var wrap = enter.append('div') + .attr('class', 'feature-list-button-wrap col12'); + + var label = wrap.append('button') + .attr('class', 'feature-list-button') + .call(iD.ui.PresetIcon() + .geometry(function(d) { return d.geometry }) + .preset(function(d) { return d.preset; })) + .on('mouseover', function(d) { mouseover(d.entity); }) + .on('mouseout', function(d) { mouseout(); }) + .on('click', function(d) { click(d.entity); }) + .append('div') + .attr('class', 'label'); + + label.append('span') + .attr('class', 'entity-type') + .text(function(d) { return d.preset.name(); }); + + label.append('span') + .attr('class', 'entity-name') + .text(function(d) { return d.name; }); + + enter.style('opacity', 0) + .transition() + .style('opacity', 1); + + items.order(); + + items.exit() + .remove(); + } + + function mouseover(entity) { + var selector = '.' + entity.id; + + if (entity.type === 'relation') { + entity.members.forEach(function(member) { + selector += ', .' + member.id; + }); + } + + context.surface().selectAll(selector) + .classed('hover', true); + } + + function mouseout() { + context.surface().selectAll('.hover') + .classed('hover', false); + } + + function click(entity) { + context.enter(iD.modes.Select(context, [entity.id])); + } + } + + return featureList; +}; diff --git a/js/id/ui/preset_icon.js b/js/id/ui/preset_icon.js index 60eb7f7ea..72c75fd45 100644 --- a/js/id/ui/preset_icon.js +++ b/js/id/ui/preset_icon.js @@ -2,33 +2,39 @@ iD.ui.PresetIcon = function() { var preset, geometry; function presetIcon(selection) { + selection.each(setup); + } + + function setup() { + var selection = d3.select(this), + p = preset.apply(this, arguments), + geom = geometry.apply(this, arguments); + var $fill = selection.selectAll('.preset-icon-fill') .data([0]); $fill.enter().append('div'); $fill.attr('class', function() { - var s = 'preset-icon-fill icon-' + geometry; - for (var i in preset.tags) { - s += ' tag-' + i + ' tag-' + i + '-' + preset.tags[i]; + var s = 'preset-icon-fill icon-' + geom; + for (var i in p.tags) { + s += ' tag-' + i + ' tag-' + i + '-' + p.tags[i]; } return s; }); - var fallbackIcon = geometry === 'line' ? 'other-line' : 'marker-stroked'; - var $icon = selection.selectAll('.preset-icon') .data([0]); $icon.enter().append('div'); $icon.attr('class', function() { - var icon = preset.icon || fallbackIcon, + var icon = p.icon || (geom === 'line' ? 'other-line' : 'marker-stroked'), klass = 'feature-' + icon + ' preset-icon'; var featureicon = iD.data.featureIcons[icon]; - if (featureicon && featureicon[geometry]) { - klass += ' preset-icon-' + geometry; + if (featureicon && featureicon[geom]) { + klass += ' preset-icon-' + geom; } else if (icon === 'multipolygon') { // Special case (geometry === 'area') klass += ' preset-icon-relation'; @@ -40,13 +46,13 @@ iD.ui.PresetIcon = function() { presetIcon.preset = function(_) { if (!arguments.length) return preset; - preset = _; + preset = d3.functor(_); return presetIcon; }; presetIcon.geometry = function(_) { if (!arguments.length) return geometry; - geometry = _; + geometry = d3.functor(_); return presetIcon; }; diff --git a/js/id/ui/preset_list.js b/js/id/ui/preset_list.js index fc8466e30..279a0ee6f 100644 --- a/js/id/ui/preset_list.js +++ b/js/id/ui/preset_list.js @@ -72,17 +72,16 @@ iD.ui.PresetList = function(context) { } var searchWrap = selection.append('div') - .attr('class', 'preset-search'); + .attr('class', 'search-header'); var search = searchWrap.append('input') - .attr('class', 'preset-search-input major') .attr('placeholder', t('inspector.search')) .attr('type', 'search') .on('keydown', keydown) .on('keyup', keyup); searchWrap.append('span') - .attr('class', 'preset-search-icon icon search'); + .attr('class', 'icon search'); if (autofocus) { search.node().focus(); diff --git a/js/id/ui/sidebar.js b/js/id/ui/sidebar.js index 3f2eaeba0..ebb292462 100644 --- a/js/id/ui/sidebar.js +++ b/js/id/ui/sidebar.js @@ -3,12 +3,17 @@ iD.ui.Sidebar = function(context) { current; function sidebar(selection) { - var wrap = selection.append('div') + var featureListWrap = selection.append('div') + .attr('class', 'feature-list-pane') + .call(iD.ui.FeatureList(context)); + + var inspectorWrap = selection.append('div') .attr('class', 'inspector-hidden inspector-wrap fr'); sidebar.hover = function(id) { if (!current && id) { - wrap.classed('inspector-hidden', false) + featureListWrap.classed('inspector-hidden', true); + inspectorWrap.classed('inspector-hidden', false) .classed('inspector-hover', true); if (inspector.entityID() !== id || inspector.state() !== 'hover') { @@ -16,16 +21,18 @@ iD.ui.Sidebar = function(context) { .state('hover') .entityID(id); - wrap.call(inspector); + inspectorWrap.call(inspector); } } else { - wrap.classed('inspector-hidden', true); + featureListWrap.classed('inspector-hidden', false); + inspectorWrap.classed('inspector-hidden', true); } }; sidebar.select = function(id, newFeature) { if (!current && id) { - wrap.classed('inspector-hidden', false) + featureListWrap.classed('inspector-hidden', true); + inspectorWrap.classed('inspector-hidden', false) .classed('inspector-hover', false); if (inspector.entityID() !== id || inspector.state() !== 'select') { @@ -34,21 +41,24 @@ iD.ui.Sidebar = function(context) { .entityID(id) .newFeature(newFeature); - wrap.call(inspector); + inspectorWrap.call(inspector); } } else { - wrap.classed('inspector-hidden', true); + featureListWrap.classed('inspector-hidden', false); + inspectorWrap.classed('inspector-hidden', true); } }; sidebar.show = function(component) { - wrap.classed('inspector-hidden', true); + featureListWrap.classed('inspector-hidden', true); + inspectorWrap.classed('inspector-hidden', true); current = selection.append('div') .attr('class', 'sidebar-component') .call(component); }; sidebar.hide = function() { + featureListWrap.classed('inspector-hidden', false); current.remove(); current = null; };