From 2895ff9405b228be6a3b0ca870dda4cb55f7cbc5 Mon Sep 17 00:00:00 2001 From: Quincy Morgan Date: Mon, 25 Mar 2019 17:00:33 -0400 Subject: [PATCH] Break modes toolbar item into add favorites and add recent items --- data/core.yaml | 1 + dist/locales/en.json | 3 +- modules/ui/tools/add_favorite.js | 303 +++++++++++++++++++++++++++++++ modules/ui/tools/add_recent.js | 135 ++++++++------ modules/ui/tools/index.js | 1 + modules/ui/top_toolbar.js | 25 ++- 6 files changed, 405 insertions(+), 63 deletions(-) create mode 100644 modules/ui/tools/add_favorite.js diff --git a/data/core.yaml b/data/core.yaml index 9fdd6ac04..a7597fffb 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -12,6 +12,7 @@ en: inspect: Inspect undo_redo: Undo / Redo recent: Recent + favorites: Favorites modes: add_feature: title: Add a feature diff --git a/dist/locales/en.json b/dist/locales/en.json index bc36a9b1e..6965e3523 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -13,7 +13,8 @@ "toolbar": { "inspect": "Inspect", "undo_redo": "Undo / Redo", - "recent": "Recent" + "recent": "Recent", + "favorites": "Favorites" }, "modes": { "add_feature": { diff --git a/modules/ui/tools/add_favorite.js b/modules/ui/tools/add_favorite.js new file mode 100644 index 000000000..6f4270d93 --- /dev/null +++ b/modules/ui/tools/add_favorite.js @@ -0,0 +1,303 @@ +import _debounce from 'lodash-es/debounce'; + +import { drag as d3_drag } from 'd3-drag'; +import { event as d3_event, select as d3_select } from 'd3-selection'; + +import { modeAddArea, modeAddLine, modeAddPoint, modeBrowse } from '../../modes'; +import { t, textDirection } from '../../util/locale'; +import { tooltip } from '../../util/tooltip'; +import { uiPresetIcon } from '../preset_icon'; +import { uiTooltipHtml } from '../tooltipHtml'; + + +export function uiToolAddFavorite(context) { + + var tool = { + id: 'add_favorite', + klass: 'modes', + label: t('toolbar.favorites') + }; + + function enabled() { + return osmEditable(); + } + + function osmEditable() { + var mode = context.mode(); + return context.editable() && mode && mode.id !== 'save'; + } + + function toggleMode(d) { + if (!enabled(d)) return; + + if (d.button === context.mode().button) { + context.enter(modeBrowse(context)); + } else { + if (d.preset) { + context.presets().setMostRecent(d.preset, d.geometry); + } + context.enter(d); + } + } + + + tool.render = function(selection) { + context + .on('enter.editor.favorite', function(entered) { + selection.selectAll('button.add-button') + .classed('active', function(mode) { return entered.button === mode.button; }); + context.container() + .classed('mode-' + entered.id, true); + }); + + context + .on('exit.editor.favorite', function(exited) { + context.container() + .classed('mode-' + exited.id, false); + }); + + var debouncedUpdate = _debounce(update, 500, { leading: true, trailing: true }); + + context.map() + .on('move.favorite', debouncedUpdate) + .on('drawn.favorite', debouncedUpdate); + + context + .on('enter.favorite', update) + .presets() + .on('favoritePreset.favorite', update) + .on('recentsChange.favorite', update); + + update(); + + + function update() { + + for (var i = 0; i <= 9; i++) { + context.keybinding().off(i.toString()); + } + + var items = context.presets().getFavorites(); + + var modes = items.map(function(d, index) { + var presetName = d.preset.name().split(' – ')[0]; + var markerClass = 'add-preset add-' + d.geometry + ' add-preset-' + presetName.replace(/\s+/g, '_') + + '-' + d.geometry + ' add-' + d.source; // replace spaces with underscores to avoid css interpretation + if (d.preset.isFallback()) { + markerClass += ' add-generic-preset'; + } + + var supportedGeometry = d.preset.geometry.filter(function(geometry) { + return ['vertex', 'point', 'line', 'area'].indexOf(geometry) !== -1; + }); + var vertexIndex = supportedGeometry.indexOf('vertex'); + if (vertexIndex !== -1 && supportedGeometry.indexOf('point') !== -1) { + // both point and vertex allowed, just combine them + supportedGeometry.splice(vertexIndex, 1); + } + var tooltipTitleID = 'modes.add_preset.title'; + if (supportedGeometry.length !== 1) { + if (d.preset.setTags({}, d.geometry).building) { + tooltipTitleID = 'modes.add_preset.building.title'; + } else { + tooltipTitleID = 'modes.add_preset.' + context.presets().fallback(d.geometry).id + '.title'; + } + } + var protoMode = Object.assign({}, d); // shallow copy + protoMode.button = markerClass; + protoMode.title = presetName; + protoMode.description = t(tooltipTitleID, { feature: '' + presetName + '' }); + + var keyCode; + if (textDirection === 'ltr') { + // use number row order: 1 2 3 4 5 6 7 8 9 0 + if (index === 9) { + keyCode = 0; + } else if (index < 10) { + keyCode = index + 1; + } + } else { + // use number row order from right to left + if (index === 0) { + keyCode = 0; + } else if (index < 10) { + keyCode = 10 - index; + } + } + if (keyCode !== undefined) { + protoMode.key = keyCode.toString(); + } + + var mode; + switch (d.geometry) { + case 'point': + case 'vertex': + mode = modeAddPoint(context, protoMode); + break; + case 'line': + mode = modeAddLine(context, protoMode); + break; + case 'area': + mode = modeAddArea(context, protoMode); + } + + if (mode.key) { + context.keybinding().off(mode.key); + context.keybinding().on(mode.key, function() { + toggleMode(mode); + }); + } + + return mode; + }); + + var buttons = selection.selectAll('button.add-button') + .data(modes, function(d, index) { return d.button + index; }); + + // exit + buttons.exit() + .remove(); + + // enter + var buttonsEnter = buttons.enter() + .append('button') + .attr('tabindex', -1) + .attr('class', function(d) { + return d.button + ' add-button bar-button'; + }) + .on('click.mode-buttons', function(d) { + + // When drawing, ignore accidental clicks on mode buttons - #4042 + if (/^draw/.test(context.mode().id)) return; + + toggleMode(d); + }) + .call(tooltip() + .placement('bottom') + .html(true) + .title(function(d) { return uiTooltipHtml(d.description, d.key); }) + ); + + buttonsEnter + .each(function(d) { + d3_select(this) + .call(uiPresetIcon() + .geometry((d.geometry === 'point' && !d.preset.matchGeometry(d.geometry)) ? 'vertex' : d.geometry) + .preset(d.preset) + .sizeClass('small') + ); + }); + + var dragOrigin, dragMoved, targetIndex; + + buttonsEnter.call(d3_drag() + .on('start', function() { + dragOrigin = { + x: d3_event.x, + y: d3_event.y + }; + targetIndex = null; + dragMoved = false; + }) + .on('drag', function(d, index) { + dragMoved = true; + var x = d3_event.x - dragOrigin.x, + y = d3_event.y - dragOrigin.y; + + if (!d3_select(this).classed('dragging') && + // don't display drag until dragging beyond a distance threshold + Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2)) <= 5) return; + + d3_select(this) + .classed('dragging', true) + .classed('removing', y > 50); + + targetIndex = null; + + selection.selectAll('button.add-preset') + .style('transform', function(d2, index2) { + var node = d3_select(this).node(); + if (index === index2) { + return 'translate(' + x + 'px, ' + y + 'px)'; + } else if (y > 50) { + if (index2 > index) { + return 'translateX(' + (textDirection === 'rtl' ? '' : '-') + '100%)'; + } + } else if (d.source === d2.source) { + if (index2 > index && ( + (d3_event.x > node.offsetLeft && textDirection === 'ltr') || + (d3_event.x < node.offsetLeft + node.offsetWidth && textDirection === 'rtl') + )) { + if (targetIndex === null || index2 > targetIndex) { + targetIndex = index2; + } + return 'translateX(' + (textDirection === 'rtl' ? '' : '-') + '100%)'; + } else if (index2 < index && ( + (d3_event.x < node.offsetLeft + node.offsetWidth && textDirection === 'ltr') || + (d3_event.x > node.offsetLeft && textDirection === 'rtl') + )) { + if (targetIndex === null || index2 < targetIndex) { + targetIndex = index2; + } + return 'translateX(' + (textDirection === 'rtl' ? '-' : '') + '100%)'; + } + } + return null; + }); + }) + .on('end', function(d, index) { + + if (dragMoved && !d3_select(this).classed('dragging')) { + toggleMode(d); + return; + } + + d3_select(this) + .classed('dragging', false) + .classed('removing', false); + + selection.selectAll('button.add-preset') + .style('transform', null); + + var y = d3_event.y - dragOrigin.y; + if (y > 50) { + // dragged out of the top bar, remove + if (d.isFavorite()) { + context.presets().removeFavorite(d.preset, d.geometry); + // also remove this as a recent so it doesn't still appear + context.presets().removeRecent(d.preset, d.geometry); + } + } else if (targetIndex !== null) { + // dragged to a new position, reorder + if (d.isFavorite()) { + context.presets().moveFavorite(index, targetIndex); + } + } + }) + ); + + // update + buttons = buttons + .merge(buttonsEnter) + .classed('disabled', function(d) { return !enabled(d); }); + } + }; + + tool.uninstall = function() { + + context + .on('enter.editor.favorite', null) + .on('exit.editor.favorite', null) + .on('enter.favorite', null); + + context.presets() + .on('favoritePreset.favorite', null) + .on('recentsChange.favorite', null); + + context.map() + .on('move.favorite', null) + .on('drawn.favorite', null); + }; + + return tool; +} diff --git a/modules/ui/tools/add_recent.js b/modules/ui/tools/add_recent.js index b0a4c0358..f07ba6c41 100644 --- a/modules/ui/tools/add_recent.js +++ b/modules/ui/tools/add_recent.js @@ -1,5 +1,4 @@ import _debounce from 'lodash-es/debounce'; -import _uniqWith from 'lodash-es/uniqWith'; import { drag as d3_drag } from 'd3-drag'; import { event as d3_event, select as d3_select } from 'd3-selection'; @@ -14,7 +13,8 @@ import { uiTooltipHtml } from '../tooltipHtml'; export function uiToolAddRecent(context) { var tool = { - id: 'modes', + id: 'add_recent', + klass: 'modes', label: t('toolbar.recent') }; @@ -42,10 +42,47 @@ export function uiToolAddRecent(context) { } } + function recentsToDraw() { + var maxShown = 10; + var maxRecents = 5; + + var favorites = context.presets().getFavorites().slice(0, maxShown); + + function isAFavorite(recent) { + return favorites.some(function(favorite) { + return favorite.matches(recent.preset, recent.geometry); + }); + } + + var favoritesCount = favorites.length; + maxRecents = Math.min(maxRecents, maxShown - favoritesCount); + var items = []; + if (maxRecents > 0) { + var recents = context.presets().getRecents().filter(function(recent) { + return recent.geometry !== 'relation'; + }); + for (var i in recents) { + var recent = recents[i]; + if (!isAFavorite(recent)) { + items.push(recent); + if (items.length === maxRecents) { + break; + } + } + } + } + + return items; + } + + tool.shouldShow = function() { + return recentsToDraw().length > 0; + }; + tool.render = function(selection) { context - .on('enter.editor', function(entered) { + .on('enter.editor.recent', function(entered) { selection.selectAll('button.add-button') .classed('active', function(mode) { return entered.button === mode.button; }); context.container() @@ -53,7 +90,7 @@ export function uiToolAddRecent(context) { }); context - .on('exit.editor', function(exited) { + .on('exit.editor.recent', function(exited) { context.container() .classed('mode-' + exited.id, false); }); @@ -61,43 +98,22 @@ export function uiToolAddRecent(context) { var debouncedUpdate = _debounce(update, 500, { leading: true, trailing: true }); context.map() - .on('move.modes', debouncedUpdate) - .on('drawn.modes', debouncedUpdate); + .on('move.recent', debouncedUpdate) + .on('drawn.recent', debouncedUpdate); context - .on('enter.modes', update) + .on('enter.recent', update) .presets() - .on('favoritePreset.modes', update) - .on('recentsChange.modes', update); + .on('favoritePreset.recent', update) + .on('recentsChange.recent', update); update(); function update() { - for (var i = 0; i <= 9; i++) { - context.keybinding().off(i.toString()); - } - - var items = context.presets().getFavorites(); - - var favoritesCount = items.length; - - if (favoritesCount < 10) { - var recents = context.presets().getRecents().filter(function(recent) { - return recent.geometry !== 'relation'; - }); - items = _uniqWith(items.concat(recents), function(item1, item2) { - return item1.matches(item2.preset, item2.geometry); - }); - var maxShown = 10; - var maxRecents = 5; - if (favoritesCount < maxRecents) { - // only show five most recent at most - maxShown = maxRecents + favoritesCount; - } - items = items.slice(0, maxShown); - } + var items = recentsToDraw(); + var favoritesCount = context.presets().getFavorites().length; var modes = items.map(function(d, index) { var presetName = d.preset.name().split(' – ')[0]; @@ -128,23 +144,24 @@ export function uiToolAddRecent(context) { protoMode.title = presetName; protoMode.description = t(tooltipTitleID, { feature: '' + presetName + '' }); + var totalIndex = favoritesCount + index; var keyCode; if (textDirection === 'ltr') { // use number row order: 1 2 3 4 5 6 7 8 9 0 - if (index === 9) { + if (totalIndex === 9) { keyCode = 0; - } else if (index < 10) { - keyCode = index + 1; + } else if (totalIndex < 10) { + keyCode = totalIndex + 1; } } else { // use number row order from right to left - if (index === 0) { + if (totalIndex === 0) { keyCode = 0; - } else if (index < 10) { - keyCode = 10 - index; + } else if (totalIndex < 10) { + keyCode = 10 - totalIndex; } } - if (keyCode !== null) { + if (keyCode !== undefined) { protoMode.key = keyCode.toString(); } @@ -162,6 +179,7 @@ export function uiToolAddRecent(context) { } if (mode.key) { + context.keybinding().off(mode.key); context.keybinding().on(mode.key, function() { toggleMode(mode); }); @@ -181,12 +199,8 @@ export function uiToolAddRecent(context) { var buttonsEnter = buttons.enter() .append('button') .attr('tabindex', -1) - .attr('class', function(d, index) { - var classes = d.button + ' add-button bar-button'; - if (index === favoritesCount && index !== 0) { - classes += ' first-recent'; - } - return classes; + .attr('class', function(d) { + return d.button + ' add-button bar-button'; }) .on('click.mode-buttons', function(d) { @@ -272,7 +286,7 @@ export function uiToolAddRecent(context) { return null; }); }) - .on('end', function(d, index) { + .on('end', function(d) { if (dragMoved && !d3_select(this).classed('dragging')) { toggleMode(d); @@ -289,18 +303,12 @@ export function uiToolAddRecent(context) { var y = d3_event.y - dragOrigin.y; if (y > 50) { // dragged out of the top bar, remove - if (d.isFavorite()) { - context.presets().removeFavorite(d.preset, d.geometry); - // also remove this as a recent so it doesn't still appear - context.presets().removeRecent(d.preset, d.geometry); - } else if (d.isRecent()) { + if (d.isRecent()) { context.presets().removeRecent(d.preset, d.geometry); } } else if (targetIndex !== null) { // dragged to a new position, reorder - if (d.isFavorite()) { - context.presets().moveFavorite(index, targetIndex); - } else if (d.isRecent()) { + if (d.isRecent()) { var item = context.presets().recentMatching(d.preset, d.geometry); var beforeItem = context.presets().recentMatching(targetData.preset, targetData.geometry); context.presets().moveRecent(item, beforeItem); @@ -313,11 +321,24 @@ export function uiToolAddRecent(context) { buttons = buttons .merge(buttonsEnter) .classed('disabled', function(d) { return !enabled(d); }); - - // check if toolbar has overflowed - context.ui().checkOverflow('#bar', true); } }; + tool.uninstall = function() { + + context + .on('enter.editor.recent', null) + .on('exit.editor.recent', null) + .on('enter.recent', null); + + context.presets() + .on('favoritePreset.recent', null) + .on('recentsChange.recent', null); + + context.map() + .on('move.recent', null) + .on('drawn.recent', null); + }; + return tool; } diff --git a/modules/ui/tools/index.js b/modules/ui/tools/index.js index 65d9e7de3..28391185d 100644 --- a/modules/ui/tools/index.js +++ b/modules/ui/tools/index.js @@ -1,3 +1,4 @@ +export * from './add_favorite'; export * from './add_recent'; export * from './notes'; export * from './save'; diff --git a/modules/ui/top_toolbar.js b/modules/ui/top_toolbar.js index 0595724d1..85f0bcf23 100644 --- a/modules/ui/top_toolbar.js +++ b/modules/ui/top_toolbar.js @@ -4,13 +4,14 @@ import { } from 'd3-selection'; import _debounce from 'lodash-es/debounce'; -import { uiToolAddRecent, uiToolNotes, uiToolSave, uiToolSearchAdd, uiToolSidebarToggle, uiToolUndoRedo } from './tools'; +import { uiToolAddFavorite, uiToolAddRecent, uiToolNotes, uiToolSave, uiToolSearchAdd, uiToolSidebarToggle, uiToolUndoRedo } from './tools'; export function uiTopToolbar(context) { var sidebarToggle = uiToolSidebarToggle(context), searchAdd = uiToolSearchAdd(context), + addFavorite = uiToolAddFavorite(context), addRecent = uiToolAddRecent(context), notes = uiToolNotes(context), undoRedo = uiToolUndoRedo(context), @@ -27,6 +28,10 @@ export function uiTopToolbar(context) { context.layers() .on('change.topToolbar', debouncedUpdate); + context.presets() + .on('favoritePreset.topToolbar', update) + .on('recentsChange.topToolbar', update); + update(); function update() { @@ -34,11 +39,19 @@ export function uiTopToolbar(context) { var tools = [ sidebarToggle, 'spacer', - searchAdd, - addRecent, - 'spacer' + searchAdd ]; + if (context.presets().getFavorites().length > 0) { + tools.push(addFavorite); + } + + if (addRecent.shouldShow()) { + tools.push(addRecent); + } + + tools.push('spacer'); + if (notesEnabled()) { tools = tools.concat([notes, 'spacer']); } @@ -62,7 +75,9 @@ export function uiTopToolbar(context) { .enter() .append('div') .attr('class', function(d) { - return 'toolbar-item ' + (d.id || d).replace('_', '-'); + var classes = 'toolbar-item ' + (d.id || d).replace('_', '-'); + if (d.klass) classes += ' ' + d.klass; + return classes; }); var actionableItems = itemsEnter.filter(function(d) { return d !== 'spacer'; });