mapshaper 0.7.65 → 0.7.66
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/mapshaper.js +1 -1
- package/package.json +1 -1
- package/www/mapshaper-gui.js +560 -152
- package/www/mapshaper.js +1 -1
- package/www/page.css +71 -4
package/mapshaper.js
CHANGED
|
@@ -82789,7 +82789,7 @@ ${svg}
|
|
|
82789
82789
|
return name == 'rectangle' || name == 'rectangles' || name == 'filter' && opts.cleanup;
|
|
82790
82790
|
}
|
|
82791
82791
|
|
|
82792
|
-
var version = "0.7.
|
|
82792
|
+
var version = "0.7.66";
|
|
82793
82793
|
|
|
82794
82794
|
// Parse command line args into commands and run them
|
|
82795
82795
|
// Function takes an optional Node-style callback. A Promise is returned if no callback is given.
|
package/package.json
CHANGED
package/www/mapshaper-gui.js
CHANGED
|
@@ -11373,10 +11373,22 @@
|
|
|
11373
11373
|
}
|
|
11374
11374
|
|
|
11375
11375
|
if (lyr && lyr.gui.geographic) {
|
|
11376
|
+
// Above the actions, because it is what they would be applied to: the
|
|
11377
|
+
// labels that share something with the one pointed at, read and chosen
|
|
11378
|
+
// before "copy as GeoJSON" or "delete label" means anything definite.
|
|
11379
|
+
// Each item carries the number it would select, so that what the click
|
|
11380
|
+
// is about to do is visible before it happens.
|
|
11381
|
+
if (e.selectActions?.length) {
|
|
11382
|
+
addMenuLabel('select');
|
|
11383
|
+
e.selectActions.forEach(function(action) {
|
|
11384
|
+
addMenuItem(action.label + getCountHtml(action.count), action.run);
|
|
11385
|
+
});
|
|
11386
|
+
}
|
|
11387
|
+
|
|
11376
11388
|
if (e.deleteVertex || e.deletePoint || copyable || e.deleteFeature ||
|
|
11377
11389
|
e.flipLabel) {
|
|
11378
11390
|
|
|
11379
|
-
addMenuLabel('
|
|
11391
|
+
addMenuLabel('actions');
|
|
11380
11392
|
if (e.deleteVertex) {
|
|
11381
11393
|
addMenuItem('delete vertex', e.deleteVertex);
|
|
11382
11394
|
}
|
|
@@ -11524,6 +11536,12 @@
|
|
|
11524
11536
|
return 'band values';
|
|
11525
11537
|
}
|
|
11526
11538
|
|
|
11539
|
+
// How many features a "select" item would select, in the grey of a menu
|
|
11540
|
+
// heading: it qualifies the item rather than being part of what it says.
|
|
11541
|
+
function getCountHtml(count) {
|
|
11542
|
+
return ' <span class="contextmenu-count">' + count + '</span>';
|
|
11543
|
+
}
|
|
11544
|
+
|
|
11527
11545
|
// A color tile, used in place of the bullet that other menu items get.
|
|
11528
11546
|
function getSwatchHtml(color) {
|
|
11529
11547
|
return '<span class="contextmenu-swatch" style="background-color: ' +
|
|
@@ -16193,6 +16211,13 @@
|
|
|
16193
16211
|
setDisplay(shown);
|
|
16194
16212
|
};
|
|
16195
16213
|
|
|
16214
|
+
// What the field shows while it is blank, or '' for nothing. The panel uses
|
|
16215
|
+
// it to say that the selected features disagree about the size, which is one
|
|
16216
|
+
// of the two reasons the field can be empty.
|
|
16217
|
+
this.setPlaceholder = function(str) {
|
|
16218
|
+
input.attr('placeholder', str || '');
|
|
16219
|
+
};
|
|
16220
|
+
|
|
16196
16221
|
this.setDisabled = function(off) {
|
|
16197
16222
|
disabled = !!off;
|
|
16198
16223
|
input.node().disabled = disabled;
|
|
@@ -16551,6 +16576,24 @@
|
|
|
16551
16576
|
gui.state.new_label_style = null;
|
|
16552
16577
|
}
|
|
16553
16578
|
|
|
16579
|
+
// What a two-state control shows for a group of labels: 'on', 'off', or
|
|
16580
|
+
// 'mixed' when they disagree.
|
|
16581
|
+
//
|
|
16582
|
+
// The panel's other controls say "they disagree" by showing nothing -- an empty
|
|
16583
|
+
// select, an unlit button, a blank field. A switch has no empty state to fall
|
|
16584
|
+
// back on, so it needs a third one named here.
|
|
16585
|
+
//
|
|
16586
|
+
// flags: one truthy/falsy value per label, in any order
|
|
16587
|
+
function getToggleState(flags) {
|
|
16588
|
+
var on = 0;
|
|
16589
|
+
var i;
|
|
16590
|
+
for (i = 0; i < flags.length; i++) {
|
|
16591
|
+
if (flags[i]) on++;
|
|
16592
|
+
}
|
|
16593
|
+
if (on === 0) return 'off';
|
|
16594
|
+
return on == flags.length ? 'on' : 'mixed';
|
|
16595
|
+
}
|
|
16596
|
+
|
|
16554
16597
|
// What a drag on a label's glyphs means: 'fixed' moves the label, anchor and
|
|
16555
16598
|
// text together, and 'draggable' leaves the anchor where it is and offsets the
|
|
16556
16599
|
// text from it.
|
|
@@ -16782,6 +16825,11 @@
|
|
|
16782
16825
|
// no line-height until one is chosen. "auto" is the honest description of a
|
|
16783
16826
|
// blank field: something else decides.
|
|
16784
16827
|
var lineHeightPlaceholder = 'auto';
|
|
16828
|
+
// What a control shows for a selection whose labels disagree about it. One
|
|
16829
|
+
// word, used in every field and menu that can be in that state: the panel
|
|
16830
|
+
// otherwise says it by showing nothing, which is also what an unset property
|
|
16831
|
+
// looks like.
|
|
16832
|
+
var MIXED_TEXT = 'mixed';
|
|
16785
16833
|
var labelStyleMode = 'label_style';
|
|
16786
16834
|
var labelStylePanelMode = 'label_style_tool';
|
|
16787
16835
|
var labelMode = 'label';
|
|
@@ -17215,26 +17263,38 @@
|
|
|
17215
17263
|
return makePanelSection(panel, title, opts);
|
|
17216
17264
|
}
|
|
17217
17265
|
|
|
17218
|
-
// A
|
|
17219
|
-
//
|
|
17220
|
-
//
|
|
17266
|
+
// A switch: a track with a knob that sits left when off and right when on,
|
|
17267
|
+
// which is the direction users expect and the only thing that says which
|
|
17268
|
+
// state is which without a label for each. A third state says that the
|
|
17269
|
+
// labels it is asking about disagree -- the knob sits over the join of a
|
|
17270
|
+
// half-and-half track, which is the panel's only control that has to show
|
|
17271
|
+
// "some of them" rather than a value.
|
|
17272
|
+
//
|
|
17273
|
+
// role=checkbox rather than switch, which is what it looks like: 'mixed' is a
|
|
17274
|
+
// legal aria-checked value for a checkbox and not for a switch, and a switch
|
|
17275
|
+
// reporting a mixed selection as unchecked would be telling a screen reader
|
|
17276
|
+
// the one thing the third state exists to avoid saying.
|
|
17221
17277
|
function makeToggle(parent, opts) {
|
|
17222
|
-
var track = El('div').addClass('label-toggle').attr('role', '
|
|
17223
|
-
var
|
|
17278
|
+
var track = El('div').addClass('label-toggle').attr('role', 'checkbox').appendTo(parent);
|
|
17279
|
+
var state = 'off';
|
|
17224
17280
|
var disabled = false;
|
|
17225
17281
|
El('div').addClass('label-toggle-knob').appendTo(track);
|
|
17226
17282
|
if (opts.title) track.attr('title', opts.title);
|
|
17283
|
+
// A click on a mixed switch turns everything on. It is the convention, and
|
|
17284
|
+
// it is the reading that reaches a state the switch can describe: the next
|
|
17285
|
+
// click then turns everything off, so both are one click away.
|
|
17227
17286
|
track.on('click', function() {
|
|
17228
17287
|
if (disabled) return;
|
|
17229
|
-
opts.onChange(
|
|
17288
|
+
opts.onChange(state != 'on');
|
|
17230
17289
|
});
|
|
17231
17290
|
return {
|
|
17232
|
-
|
|
17233
|
-
|
|
17234
|
-
|
|
17235
|
-
|
|
17236
|
-
|
|
17237
|
-
|
|
17291
|
+
// val: 'on', 'off' or 'mixed'; anything else reads as off
|
|
17292
|
+
setState: function(val) {
|
|
17293
|
+
state = val == 'on' || val == 'mixed' ? val : 'off';
|
|
17294
|
+
track.classed('on', state == 'on')
|
|
17295
|
+
.classed('mixed', state == 'mixed')
|
|
17296
|
+
.attr('aria-checked', state == 'mixed' ? 'mixed' :
|
|
17297
|
+
state == 'on' ? 'true' : 'false');
|
|
17238
17298
|
},
|
|
17239
17299
|
setDisabled: function(off) {
|
|
17240
17300
|
disabled = !!off;
|
|
@@ -17257,6 +17317,9 @@
|
|
|
17257
17317
|
function makeMeasureInput(parent, field, placeholder) {
|
|
17258
17318
|
var input = El('input').attr('type', 'text').addClass('label-measure-input')
|
|
17259
17319
|
.attr('placeholder', placeholder)
|
|
17320
|
+
// The field's own placeholder, kept because the shown one is replaced
|
|
17321
|
+
// while the selection disagrees -- see setMixedPlaceholder().
|
|
17322
|
+
.attr('data-placeholder', placeholder)
|
|
17260
17323
|
.appendTo(parent)
|
|
17261
17324
|
.on('change', function() {
|
|
17262
17325
|
applyStyleValues([[field, input.node().value.trim()]]);
|
|
@@ -17428,51 +17491,72 @@
|
|
|
17428
17491
|
// With nothing selected the menu shows the font the next label will be
|
|
17429
17492
|
// made in; with labels selected it shows the font they are drawn in, which
|
|
17430
17493
|
// for a label carrying none is whatever sans-serif resolves to here.
|
|
17431
|
-
var
|
|
17494
|
+
var font = getShownValue(ids, fontField,
|
|
17432
17495
|
{useDefault: true, defaultValue: getFontNameForTarget(ids)});
|
|
17433
|
-
var
|
|
17496
|
+
var fontSize = getShownValue(ids, fontSizeField, {useDefault: true, defaultValue: defaultFontSize});
|
|
17434
17497
|
var fontStyleVal = getCommonValue(ids, fontStyleField, {useDefault: true, defaultValue: defaultFontStyle});
|
|
17435
17498
|
var fontWeightVal = getCommonValue(ids, fontWeightField, {useDefault: true, defaultValue: defaultFontWeight});
|
|
17436
|
-
var
|
|
17437
|
-
var
|
|
17438
|
-
var
|
|
17439
|
-
var
|
|
17499
|
+
var fill = getShownValue(ids, fillField, {useDefault: true, defaultValue: defaultLabelColor});
|
|
17500
|
+
var opacity = getShownValue(ids, opacityField, {useDefault: true, defaultValue: 1});
|
|
17501
|
+
var letterSpacing = getShownValue(ids, letterSpacingField);
|
|
17502
|
+
var lineHeight = getShownValue(ids, lineHeightField);
|
|
17440
17503
|
var alignVal = getCommonAlignment(ids);
|
|
17441
|
-
var
|
|
17504
|
+
var css = getShownValue(ids, cssField);
|
|
17442
17505
|
var posVal = getCommonValue(ids, 'label-pos');
|
|
17443
|
-
|
|
17444
|
-
|
|
17445
|
-
|
|
17446
|
-
var
|
|
17506
|
+
// From the labels that have a symbol, so that a selection where only some
|
|
17507
|
+
// do shows the symbols it has rather than blanking every field because the
|
|
17508
|
+
// rest of the selection has nothing to compare.
|
|
17509
|
+
var iconIds = getIconValueIds();
|
|
17510
|
+
var iconVal = getCommonValue(iconIds, iconField);
|
|
17511
|
+
var iconSize = getShownValue(iconIds, iconSizeField, {useDefault: true, defaultValue: defaultIconSize});
|
|
17512
|
+
var iconColor = getShownValue(iconIds, iconColorField, {useDefault: true, defaultValue: defaultIconColor});
|
|
17513
|
+
var iconOpacity = getShownValue(iconIds, iconOpacityField, {useDefault: true, defaultValue: 1});
|
|
17447
17514
|
updateEditingStatus(manualIds.length, !!getLabelTextSession(gui));
|
|
17448
17515
|
updateSavedStyleControls();
|
|
17449
17516
|
fontSelect.node().disabled = !showValues;
|
|
17450
|
-
updateFontControl(
|
|
17451
|
-
updateFontStyleControls(
|
|
17452
|
-
updateFontSizeControls(
|
|
17453
|
-
updateColorControls(
|
|
17454
|
-
updateOpacityControl(opacityInput,
|
|
17455
|
-
updateMeasureControl(letterSpacingInput,
|
|
17456
|
-
updateMeasureControl(lineHeightInput,
|
|
17517
|
+
updateFontControl(font);
|
|
17518
|
+
updateFontStyleControls(font.value, fontStyleVal, fontWeightVal);
|
|
17519
|
+
updateFontSizeControls(fontSize);
|
|
17520
|
+
updateColorControls(fill);
|
|
17521
|
+
updateOpacityControl(opacityInput, opacity);
|
|
17522
|
+
updateMeasureControl(letterSpacingInput, letterSpacing);
|
|
17523
|
+
updateMeasureControl(lineHeightInput, lineHeight);
|
|
17457
17524
|
updateAlignButtons(showValues ? alignVal : '');
|
|
17458
|
-
updateCssControl(
|
|
17525
|
+
updateCssControl(css);
|
|
17459
17526
|
updatePositionButtons(showValues ? posVal : '', ids);
|
|
17460
17527
|
// The symbol's controls keep showing their values while the switch is off,
|
|
17461
17528
|
// greyed: what they show is what the symbol comes back as.
|
|
17462
17529
|
var iconOff = updateIconControls(showValues ? iconVal : '');
|
|
17463
|
-
updateIconSizeControls(
|
|
17464
|
-
updateIconColorControls(
|
|
17465
|
-
updateOpacityControl(iconOpacityInput,
|
|
17530
|
+
updateIconSizeControls(iconSize, iconOff);
|
|
17531
|
+
updateIconColorControls(iconColor, iconOff);
|
|
17532
|
+
updateOpacityControl(iconOpacityInput, iconOpacity, iconOff);
|
|
17466
17533
|
}
|
|
17467
17534
|
|
|
17468
|
-
|
|
17535
|
+
// Every field that can show a value can also show nothing, which is why each
|
|
17536
|
+
// of these takes a {value, mixed} rather than a value: a field blank because
|
|
17537
|
+
// the selected labels disagree says so, in the placeholder, where one blank
|
|
17538
|
+
// because the property is unset shows what the renderer will do instead.
|
|
17539
|
+
function updateOpacityControl(input, shown, disabled) {
|
|
17469
17540
|
input.node().disabled = disabled || !controlsEnabled();
|
|
17470
|
-
input.node().value =
|
|
17541
|
+
input.node().value = shown.value === '' ? '' : formatOpacityPct(shown.value);
|
|
17542
|
+
setMixedPlaceholder(input, shown.mixed);
|
|
17471
17543
|
}
|
|
17472
17544
|
|
|
17473
|
-
function updateMeasureControl(input,
|
|
17545
|
+
function updateMeasureControl(input, shown) {
|
|
17546
|
+
var val = shown.value;
|
|
17474
17547
|
input.node().disabled = !controlsEnabled();
|
|
17475
17548
|
input.node().value = val || val === 0 ? String(val) : '';
|
|
17549
|
+
setMixedPlaceholder(input, shown.mixed);
|
|
17550
|
+
}
|
|
17551
|
+
|
|
17552
|
+
// A field showing nothing because the labels disagree says "mixed" where it
|
|
17553
|
+
// would otherwise show its own placeholder -- the spacing fields show the
|
|
17554
|
+
// value the renderer uses when the property is absent, which is not what is
|
|
17555
|
+
// true of a mixed selection.
|
|
17556
|
+
function setMixedPlaceholder(input, mixed) {
|
|
17557
|
+
var el = input.node();
|
|
17558
|
+
var own = el.getAttribute('data-placeholder') || '';
|
|
17559
|
+
el.setAttribute('placeholder', mixed ? MIXED_TEXT : own);
|
|
17476
17560
|
}
|
|
17477
17561
|
|
|
17478
17562
|
// Alignment stays live whatever is selected, including nothing. It was
|
|
@@ -17496,7 +17580,6 @@
|
|
|
17496
17580
|
// to take. The commands ignore a position given for one, and a disabled
|
|
17497
17581
|
// button says so where a console warning would not.
|
|
17498
17582
|
var disabled = !controlsEnabled() || everyLabelIsOnAPath(ids);
|
|
17499
|
-
var locked = !disabled && gridIsLockedToCentre(ids);
|
|
17500
17583
|
// Faintly, and only when no cell is lit: a label carrying offsets is not
|
|
17501
17584
|
// at any of the nine, but one of them is roughly where it is and clicking
|
|
17502
17585
|
// it is the way back.
|
|
@@ -17504,37 +17587,11 @@
|
|
|
17504
17587
|
labelPositions.forEach(function(name) {
|
|
17505
17588
|
posBtns[name].classed('selected', !disabled && name == pos);
|
|
17506
17589
|
posBtns[name].classed('nearest', name == nearest);
|
|
17507
|
-
setPanelButtonDisabled(posBtns[name], disabled
|
|
17590
|
+
setPanelButtonDisabled(posBtns[name], disabled);
|
|
17508
17591
|
});
|
|
17509
17592
|
updateDragModeButtons();
|
|
17510
17593
|
}
|
|
17511
17594
|
|
|
17512
|
-
// The nine positions place text around something, so with nothing drawn at
|
|
17513
|
-
// the anchor the grid is locked to the centre cell: there is no answer to
|
|
17514
|
-
// "north-east of what?", and the centre is what -add-label gives a new label
|
|
17515
|
-
// anyway. The cell stays clickable, because text over its own symbol is a
|
|
17516
|
-
// real thing to ask for.
|
|
17517
|
-
//
|
|
17518
|
-
// The test is whether the label draws a symbol, not whether it has an icon:
|
|
17519
|
-
// a styled dots layer given label text by the point panel's "Create labels"
|
|
17520
|
-
// keeps its r and fill, and keying this on icon= alone would lock the
|
|
17521
|
-
// commonest labels in the app to the middle of their own dots.
|
|
17522
|
-
//
|
|
17523
|
-
// A default and a guard rather than an invariant. A label that arrives
|
|
17524
|
-
// positioned with nothing at its anchor -- from the CLI, from an expression,
|
|
17525
|
-
// from the legacy positioning mode -- is shown as it is, with the grid live:
|
|
17526
|
-
// refusing to display what is in the record is worse than letting an odd
|
|
17527
|
-
// state be edited. Dragging is not gated on a symbol either, since a labels
|
|
17528
|
-
// layer whose dots live in a different layer still needs its text placed.
|
|
17529
|
-
function gridIsLockedToCentre(ids) {
|
|
17530
|
-
var table = getActiveTable();
|
|
17531
|
-
if (!everyTargetLacksASymbol(ids)) return false;
|
|
17532
|
-
if (ids.length === 0) return !labelIsPlaced(getNewLabelStyle(gui));
|
|
17533
|
-
return ids.every(function(id) {
|
|
17534
|
-
return !labelIsPlaced(table && table.getRecordAt(id));
|
|
17535
|
-
});
|
|
17536
|
-
}
|
|
17537
|
-
|
|
17538
17595
|
// Whether a label has been put somewhere other than on top of its anchor: a
|
|
17539
17596
|
// position of its own other than the centre one, or offsets from a drag.
|
|
17540
17597
|
function labelIsPlaced(rec) {
|
|
@@ -17544,16 +17601,6 @@
|
|
|
17544
17601
|
internal.hasStyleValue(rec, 'dy');
|
|
17545
17602
|
}
|
|
17546
17603
|
|
|
17547
|
-
function everyTargetLacksASymbol(ids) {
|
|
17548
|
-
var table = getActiveTable();
|
|
17549
|
-
if (ids.length === 0) {
|
|
17550
|
-
return !internal.featureHasSvgSymbol(getNewLabelStyle(gui));
|
|
17551
|
-
}
|
|
17552
|
-
return ids.every(function(id) {
|
|
17553
|
-
return !internal.featureHasSvgSymbol(table && table.getRecordAt(id));
|
|
17554
|
-
});
|
|
17555
|
-
}
|
|
17556
|
-
|
|
17557
17604
|
// Which of the nine positions a dragged label is nearest, or ''.
|
|
17558
17605
|
//
|
|
17559
17606
|
// Only for a single label: the cell is a hint about where one label sits,
|
|
@@ -17631,8 +17678,9 @@
|
|
|
17631
17678
|
return labelModeIsOn() ? 'new labels' : 'all';
|
|
17632
17679
|
}
|
|
17633
17680
|
|
|
17634
|
-
function updateFontSizeControls(
|
|
17635
|
-
fontSizeInput.setValue(
|
|
17681
|
+
function updateFontSizeControls(shown) {
|
|
17682
|
+
fontSizeInput.setValue(shown.value || '');
|
|
17683
|
+
fontSizeInput.setPlaceholder(shown.mixed ? MIXED_TEXT : '');
|
|
17636
17684
|
fontSizeInput.setDisabled(!controlsEnabled());
|
|
17637
17685
|
}
|
|
17638
17686
|
|
|
@@ -17645,15 +17693,43 @@
|
|
|
17645
17693
|
// here, and a project moves between machines. It is added to the menu as
|
|
17646
17694
|
// itself rather than replaced by an installed font, since the name is the
|
|
17647
17695
|
// user's data and choosing something else for them would restyle the label.
|
|
17648
|
-
function updateFontControl(
|
|
17696
|
+
function updateFontControl(shown) {
|
|
17697
|
+
var fontVal = shown.value;
|
|
17698
|
+
// A font menu is a list of fonts, so "they are in different fonts" cannot
|
|
17699
|
+
// be one of the fonts: it is an entry that appears only while it is true,
|
|
17700
|
+
// and picking it does nothing (applyFont() ignores a blank value). The
|
|
17701
|
+
// list stays live, so choosing a font is how the selection is brought into
|
|
17702
|
+
// line -- as it is for the face menu below.
|
|
17703
|
+
setMixedOption(fontSelect, shown.mixed);
|
|
17649
17704
|
fontSelect.node().value = fontVal || '';
|
|
17650
17705
|
if (fontVal && fontSelect.node().selectedIndex < 0) {
|
|
17651
17706
|
El('option').attr('value', fontVal).text(fontVal).appendTo(fontSelect);
|
|
17652
17707
|
fontSelect.node().value = fontVal;
|
|
17653
17708
|
}
|
|
17654
|
-
// Nothing selected, rather than the first font in the list, for a
|
|
17655
|
-
//
|
|
17656
|
-
if (!fontVal) fontSelect.node().selectedIndex = -1;
|
|
17709
|
+
// Nothing selected, rather than the first font in the list, for a target
|
|
17710
|
+
// there is nothing to say about -- a disabled menu with no labels behind it.
|
|
17711
|
+
if (!fontVal && !shown.mixed) fontSelect.node().selectedIndex = -1;
|
|
17712
|
+
}
|
|
17713
|
+
|
|
17714
|
+
// Adds or removes a menu's "mixed" entry, which carries no value: a select
|
|
17715
|
+
// showing nothing looks like a control that failed to load, and this is the
|
|
17716
|
+
// one state the panel cannot express by lighting no button.
|
|
17717
|
+
//
|
|
17718
|
+
// First in the list, and removed as soon as the selection agrees, so that it
|
|
17719
|
+
// is never an option among the real ones.
|
|
17720
|
+
function setMixedOption(select, mixed) {
|
|
17721
|
+
var el = select.node();
|
|
17722
|
+
var first = el.options[0];
|
|
17723
|
+
var has = !!first && first.value === '' && first.dataset.mixed == 'true';
|
|
17724
|
+
if (mixed && !has) {
|
|
17725
|
+
first = document.createElement('option');
|
|
17726
|
+
first.value = '';
|
|
17727
|
+
first.textContent = MIXED_TEXT;
|
|
17728
|
+
first.dataset.mixed = 'true';
|
|
17729
|
+
el.insertBefore(first, el.firstChild);
|
|
17730
|
+
} else if (!mixed && has) {
|
|
17731
|
+
el.removeChild(first);
|
|
17732
|
+
}
|
|
17657
17733
|
}
|
|
17658
17734
|
|
|
17659
17735
|
// The faces the chosen font is installed with, and no entry for "whatever
|
|
@@ -17673,12 +17749,13 @@
|
|
|
17673
17749
|
El('option').attr('value', variant.value).appendTo(fontStyleSelect).text(variant.label);
|
|
17674
17750
|
});
|
|
17675
17751
|
}
|
|
17752
|
+
// The faces are rebuilt above, so the "mixed" entry goes on afterwards.
|
|
17753
|
+
setMixedOption(fontStyleSelect, mixed);
|
|
17676
17754
|
fontStyleSelect.node().disabled = disabled;
|
|
17677
17755
|
fontStyleSelect.node().value = shown ? shown.value : '';
|
|
17678
|
-
// Nothing selected rather than the first face, for a
|
|
17679
|
-
//
|
|
17680
|
-
|
|
17681
|
-
if (!shown) fontStyleSelect.node().selectedIndex = -1;
|
|
17756
|
+
// Nothing selected rather than the first face, for a menu with no font to
|
|
17757
|
+
// list the faces of.
|
|
17758
|
+
if (!shown && !mixed) fontStyleSelect.node().selectedIndex = -1;
|
|
17682
17759
|
}
|
|
17683
17760
|
|
|
17684
17761
|
// The face the panel shows for a label in @fontName. A font-style and
|
|
@@ -17689,12 +17766,17 @@
|
|
|
17689
17766
|
return getNearestFontStyleVariant(fontName, fontStyleVal, fontWeightVal);
|
|
17690
17767
|
}
|
|
17691
17768
|
|
|
17692
|
-
function updateColorControls(
|
|
17769
|
+
function updateColorControls(shown) {
|
|
17770
|
+
var colorVal = shown.value;
|
|
17693
17771
|
var disabled = !controlsEnabled();
|
|
17694
17772
|
colorInput.node().disabled = disabled;
|
|
17695
17773
|
colorInput.node().value = colorVal || '';
|
|
17774
|
+
setMixedPlaceholder(colorInput, shown.mixed);
|
|
17696
17775
|
colorFieldBox.classed('disabled', disabled);
|
|
17697
17776
|
setPanelButtonDisabled(colorChit, disabled);
|
|
17777
|
+
// An empty swatch is how the field says "no colour", so a mixed one is
|
|
17778
|
+
// marked instead of being left blank to mean two things.
|
|
17779
|
+
colorChit.classed('mixed', shown.mixed);
|
|
17698
17780
|
colorChit.css('background-color', isHexColor(colorVal) ? colorVal : 'transparent');
|
|
17699
17781
|
if (colorPicker.visible()) {
|
|
17700
17782
|
return; // avoid HSB -> RGB -> HSB rounding jumps after picker commits
|
|
@@ -17706,50 +17788,59 @@
|
|
|
17706
17788
|
}
|
|
17707
17789
|
}
|
|
17708
17790
|
|
|
17709
|
-
function updateCssControl(
|
|
17791
|
+
function updateCssControl(shown) {
|
|
17710
17792
|
cssInput.node().disabled = !controlsEnabled();
|
|
17711
|
-
cssInput.node().value =
|
|
17793
|
+
cssInput.node().value = shown.value || '';
|
|
17794
|
+
setMixedPlaceholder(cssInput, shown.mixed);
|
|
17712
17795
|
}
|
|
17713
17796
|
|
|
17714
17797
|
// The toggle reads the data rather than holding a state of its own: a symbol
|
|
17715
17798
|
// is on when the target has one, which is what makes it follow an undo.
|
|
17716
17799
|
//
|
|
17717
|
-
// A selection where only some labels have a symbol
|
|
17718
|
-
// section stays usable
|
|
17719
|
-
//
|
|
17720
|
-
//
|
|
17800
|
+
// A selection where only some labels have a symbol shows the switch mixed,
|
|
17801
|
+
// and the section stays usable: there are symbols in the selection to style.
|
|
17802
|
+
// What the controls below it then act on narrows to the labels that have one
|
|
17803
|
+
// -- see getIconTargetIds() -- so styling a symbol never creates one. The
|
|
17804
|
+
// switch is still the only way to ask for that, and clicking a shape is the
|
|
17805
|
+
// one thing in the section that applies to every selected label, because
|
|
17806
|
+
// choosing a shape for a group is a plain statement about all of it.
|
|
17721
17807
|
function updateIconControls(iconVal) {
|
|
17722
17808
|
var enabled = controlsEnabled();
|
|
17723
|
-
var
|
|
17809
|
+
var state = enabled ? getIconState() : 'off';
|
|
17810
|
+
var off = state == 'off';
|
|
17724
17811
|
if (iconVal) lastIconShape = iconVal;
|
|
17725
|
-
iconToggle.setState(
|
|
17812
|
+
iconToggle.setState(state);
|
|
17726
17813
|
iconToggle.setDisabled(!enabled);
|
|
17727
17814
|
// The group is faded as a whole rather than button by button, so that the
|
|
17728
17815
|
// border the buttons share fades with them -- a live border around dead
|
|
17729
17816
|
// buttons is the one part of a disabled control that still looks usable.
|
|
17730
|
-
iconGroupEl.classed('disabled',
|
|
17817
|
+
iconGroupEl.classed('disabled', off);
|
|
17731
17818
|
iconTypes.forEach(function(icon) {
|
|
17732
|
-
iconBtns[icon.name].classed('selected',
|
|
17733
|
-
setPanelButtonDisabled(iconBtns[icon.name],
|
|
17819
|
+
iconBtns[icon.name].classed('selected', !off && icon.name == iconVal);
|
|
17820
|
+
setPanelButtonDisabled(iconBtns[icon.name], off);
|
|
17734
17821
|
});
|
|
17735
|
-
return
|
|
17822
|
+
return off;
|
|
17736
17823
|
}
|
|
17737
17824
|
|
|
17738
17825
|
function setIconOn(on) {
|
|
17739
17826
|
applyIcon(on ? lastIconShape : '');
|
|
17740
17827
|
}
|
|
17741
17828
|
|
|
17742
|
-
function updateIconSizeControls(
|
|
17743
|
-
iconSizeInput.setValue(
|
|
17829
|
+
function updateIconSizeControls(shown, iconOff) {
|
|
17830
|
+
iconSizeInput.setValue(shown.value || '');
|
|
17831
|
+
iconSizeInput.setPlaceholder(shown.mixed ? MIXED_TEXT : '');
|
|
17744
17832
|
iconSizeInput.setDisabled(iconOff || !controlsEnabled());
|
|
17745
17833
|
}
|
|
17746
17834
|
|
|
17747
|
-
function updateIconColorControls(
|
|
17835
|
+
function updateIconColorControls(shown, iconOff) {
|
|
17836
|
+
var colorVal = shown.value;
|
|
17748
17837
|
var disabled = iconOff || !controlsEnabled();
|
|
17749
17838
|
iconColorInput.node().disabled = disabled;
|
|
17750
17839
|
iconColorInput.node().value = colorVal || '';
|
|
17840
|
+
setMixedPlaceholder(iconColorInput, shown.mixed);
|
|
17751
17841
|
iconColorFieldBox.classed('disabled', disabled);
|
|
17752
17842
|
setPanelButtonDisabled(iconColorChit, disabled);
|
|
17843
|
+
iconColorChit.classed('mixed', shown.mixed);
|
|
17753
17844
|
iconColorChit.css('background-color', isHexColor(colorVal) ? colorVal : 'transparent');
|
|
17754
17845
|
if (iconColorPicker.visible()) {
|
|
17755
17846
|
return; // avoid HSB -> RGB -> HSB rounding jumps after picker commits
|
|
@@ -17766,29 +17857,57 @@
|
|
|
17766
17857
|
}
|
|
17767
17858
|
|
|
17768
17859
|
function getCommonValue(ids, field, opts) {
|
|
17860
|
+
return getCommonValueInfo(ids, field, opts).value;
|
|
17861
|
+
}
|
|
17862
|
+
|
|
17863
|
+
// What the target labels say about one property:
|
|
17864
|
+
//
|
|
17865
|
+
// value: the value they agree on, or '' if they do not
|
|
17866
|
+
// mixed: whether they disagree
|
|
17867
|
+
//
|
|
17868
|
+
// Both, because a control showing nothing has to be able to say which of the
|
|
17869
|
+
// two kinds of nothing it means. Blank is also what an unset property looks
|
|
17870
|
+
// like -- a label carries no letter-spacing until one is chosen -- and a
|
|
17871
|
+
// field that reads the same either way leaves the user to guess whether the
|
|
17872
|
+
// selection is uniform.
|
|
17873
|
+
function getCommonValueInfo(ids, field, opts) {
|
|
17769
17874
|
var table = getActiveTable();
|
|
17770
17875
|
var records = table && table.getRecords();
|
|
17771
17876
|
var value, val, hasValue;
|
|
17772
|
-
if (ids.length === 0)
|
|
17773
|
-
|
|
17877
|
+
if (ids.length === 0) {
|
|
17878
|
+
return {value: getNewLabelValue(field, opts), mixed: false};
|
|
17879
|
+
}
|
|
17880
|
+
if (!records) return {value: '', mixed: false};
|
|
17774
17881
|
for (var i=0; i<ids.length; i++) {
|
|
17775
17882
|
val = records[ids[i]] && records[ids[i]][field];
|
|
17776
17883
|
if (!val) {
|
|
17777
|
-
|
|
17778
|
-
|
|
17779
|
-
|
|
17780
|
-
|
|
17781
|
-
|
|
17884
|
+
// A property nobody set reads as what it renders as, where the caller
|
|
17885
|
+
// says what that is: a label with font-size=12 and one with no
|
|
17886
|
+
// font-size are drawn the same and agree. Without a default the two
|
|
17887
|
+
// disagree, which is right for a property that means nothing when it
|
|
17888
|
+
// is absent -- one label carrying inline css and one not are not in
|
|
17889
|
+
// the same state.
|
|
17890
|
+
val = opts && opts.useDefault ? opts.defaultValue : '';
|
|
17782
17891
|
} else {
|
|
17783
17892
|
hasValue = true;
|
|
17784
17893
|
}
|
|
17785
17894
|
if (i === 0) {
|
|
17786
17895
|
value = val;
|
|
17787
17896
|
} else if (val != value) {
|
|
17788
|
-
return '';
|
|
17897
|
+
return {value: '', mixed: true};
|
|
17789
17898
|
}
|
|
17790
17899
|
}
|
|
17791
|
-
return
|
|
17900
|
+
return {
|
|
17901
|
+
value: hasValue || opts && opts.useDefault ? value : '',
|
|
17902
|
+
mixed: false
|
|
17903
|
+
};
|
|
17904
|
+
}
|
|
17905
|
+
|
|
17906
|
+
// The same, as a control shows it: nothing at all, and nothing to say about
|
|
17907
|
+
// it, while there is no target to read.
|
|
17908
|
+
function getShownValue(ids, field, opts) {
|
|
17909
|
+
if (!controlsEnabled()) return {value: '', mixed: false};
|
|
17910
|
+
return getCommonValueInfo(ids, field, opts);
|
|
17792
17911
|
}
|
|
17793
17912
|
|
|
17794
17913
|
// The alignment the labels are *drawn* with, rather than the one they carry.
|
|
@@ -18017,7 +18136,11 @@
|
|
|
18017
18136
|
var ids = getTargetIds();
|
|
18018
18137
|
var styles = [[iconField, iconName || '']];
|
|
18019
18138
|
if (iconName) {
|
|
18020
|
-
|
|
18139
|
+
// The size the symbols in the selection already share, where they share
|
|
18140
|
+
// one: a shape applied to a mixed selection gives the labels that had no
|
|
18141
|
+
// symbol the size of the ones that did, rather than resetting them all to
|
|
18142
|
+
// the default.
|
|
18143
|
+
styles.push([iconSizeField, getNumericSize(getIconValueIds(), iconSizeField, defaultIconSize)]);
|
|
18021
18144
|
// The symbol's own opacity goes on with it, because the label's opacity
|
|
18022
18145
|
// is applied to both elements: without this, text set to 50% would give
|
|
18023
18146
|
// a half-faded symbol while the Icon section showed it at 100%.
|
|
@@ -18029,25 +18152,21 @@
|
|
|
18029
18152
|
applyStyleValues(styles);
|
|
18030
18153
|
}
|
|
18031
18154
|
|
|
18032
|
-
// Switching a symbol on
|
|
18033
|
-
//
|
|
18034
|
-
//
|
|
18155
|
+
// Switching a symbol on moves a label sitting at the centre out from under
|
|
18156
|
+
// it, in the same command, so that the pair is one undo step and the text
|
|
18157
|
+
// is never briefly drawn over the symbol it just asked for.
|
|
18035
18158
|
//
|
|
18036
|
-
//
|
|
18037
|
-
//
|
|
18038
|
-
//
|
|
18039
|
-
//
|
|
18040
|
-
// destructive, since a hand-placed label loses its placement; being one
|
|
18041
|
-
// undoable command is what makes that acceptable.
|
|
18159
|
+
// Switching one off moves nothing. It used to put the label back to the
|
|
18160
|
+
// centre, which threw away a placement the user had made by hand for the
|
|
18161
|
+
// sake of an invariant -- that text with nothing at its anchor sits on the
|
|
18162
|
+
// anchor -- that was never worth what it cost.
|
|
18042
18163
|
//
|
|
18043
18164
|
// Not for path labels, whose text runs along a curve: the commands ignore a
|
|
18044
18165
|
// position given for one and warn about it, and a warning from switching a
|
|
18045
18166
|
// symbol on would be about something the user did not ask for.
|
|
18046
18167
|
function addIconPositionChange(styles, ids, iconOn) {
|
|
18047
|
-
if (anyLabelIsOnAPath(ids)) return;
|
|
18048
|
-
if (
|
|
18049
|
-
styles.push(['label-pos', 'c']);
|
|
18050
|
-
} else if (everyTargetIsCentred(ids)) {
|
|
18168
|
+
if (anyLabelIsOnAPath(ids) || !iconOn) return;
|
|
18169
|
+
if (everyTargetIsCentred(ids)) {
|
|
18051
18170
|
styles.push(['label-pos', labelPositionBesideIcon]);
|
|
18052
18171
|
}
|
|
18053
18172
|
}
|
|
@@ -18061,45 +18180,74 @@
|
|
|
18061
18180
|
}
|
|
18062
18181
|
|
|
18063
18182
|
function applyIconSize(value) {
|
|
18064
|
-
applyStyleValues([[iconSizeField, value]]);
|
|
18183
|
+
applyStyleValues([[iconSizeField, value]], getIconTargetIds());
|
|
18065
18184
|
}
|
|
18066
18185
|
|
|
18067
18186
|
function applyIconColor(color) {
|
|
18068
|
-
applyStyleValues([[iconColorField, color]]);
|
|
18187
|
+
applyStyleValues([[iconColorField, color]], getIconTargetIds());
|
|
18069
18188
|
}
|
|
18070
18189
|
|
|
18071
18190
|
function applyIconOpacity(value) {
|
|
18072
|
-
applyStyleValues([[iconOpacityField, value]]);
|
|
18191
|
+
applyStyleValues([[iconOpacityField, value]], getIconTargetIds());
|
|
18073
18192
|
}
|
|
18074
18193
|
|
|
18075
18194
|
function nudgeIconSize(delta) {
|
|
18076
|
-
var
|
|
18077
|
-
var size = getNumericSize(ids, iconSizeField, defaultIconSize);
|
|
18195
|
+
var size = getNumericSize(getIconTargetIds(), iconSizeField, defaultIconSize);
|
|
18078
18196
|
if (!controlsEnabled()) return;
|
|
18079
18197
|
size = Math.max(1, size + delta);
|
|
18080
18198
|
applyIconSize(size);
|
|
18081
18199
|
}
|
|
18082
18200
|
|
|
18201
|
+
// Whether the target labels have a symbol: 'on', 'off', or 'mixed' when only
|
|
18202
|
+
// some of them do.
|
|
18203
|
+
//
|
|
18083
18204
|
// An icon-size or icon-color on a label with no symbol draws nothing, which
|
|
18084
18205
|
// is why the shape, size and colour controls are inert until the toggle puts
|
|
18085
18206
|
// a symbol there to style.
|
|
18086
|
-
function
|
|
18207
|
+
function getIconState() {
|
|
18087
18208
|
var ids = getTargetIds();
|
|
18088
18209
|
var table = getActiveTable();
|
|
18089
|
-
if (ids.length === 0) return
|
|
18090
|
-
return ids.
|
|
18210
|
+
if (ids.length === 0) return getNewLabelValue(iconField) ? 'on' : 'off';
|
|
18211
|
+
return getToggleState(ids.map(function(id) {
|
|
18091
18212
|
var rec = table && table.getRecordAt(id);
|
|
18092
|
-
return
|
|
18213
|
+
return !!(rec && rec[iconField]);
|
|
18214
|
+
}));
|
|
18215
|
+
}
|
|
18216
|
+
|
|
18217
|
+
// The labels an icon-size, icon-color or icon-opacity goes to: those in the
|
|
18218
|
+
// target that have a symbol, which is all of them unless the switch is mixed.
|
|
18219
|
+
// Writing one of these to a label with no icon would add a property that
|
|
18220
|
+
// draws nothing and a column to the user's table.
|
|
18221
|
+
//
|
|
18222
|
+
// Empty for a target with no symbol anywhere, which is the state those
|
|
18223
|
+
// controls are disabled in, and empty for "new labels", where the style is
|
|
18224
|
+
// held by the tool rather than by any feature.
|
|
18225
|
+
function getIconTargetIds() {
|
|
18226
|
+
var ids = getTargetIds();
|
|
18227
|
+
var table = getActiveTable();
|
|
18228
|
+
return ids.filter(function(id) {
|
|
18229
|
+
var rec = table && table.getRecordAt(id);
|
|
18230
|
+
return !!(rec && rec[iconField]);
|
|
18093
18231
|
});
|
|
18094
18232
|
}
|
|
18095
18233
|
|
|
18234
|
+
// The labels the section's controls *show* the values of, which is not quite
|
|
18235
|
+
// the set they write to: with no symbol anywhere the fields go on showing
|
|
18236
|
+
// what the target carries, greyed, because that is what the symbol comes back
|
|
18237
|
+
// as when the switch is turned on. Narrowing there would show the values held
|
|
18238
|
+
// for the next label instead of the ones stored on the selection.
|
|
18239
|
+
function getIconValueIds() {
|
|
18240
|
+
var ids = getIconTargetIds();
|
|
18241
|
+
return ids.length > 0 ? ids : getTargetIds();
|
|
18242
|
+
}
|
|
18243
|
+
|
|
18096
18244
|
function getIconOpacityToWrite() {
|
|
18097
18245
|
var val = parseOpacityValue(iconOpacityInput.node().value);
|
|
18098
18246
|
if (val !== null) return val;
|
|
18099
18247
|
// The field reads blank while the section is off, so a symbol switched off
|
|
18100
18248
|
// and on again takes back the fade still stored on the label rather than
|
|
18101
18249
|
// being reset to full.
|
|
18102
|
-
var stored = getCommonValue(
|
|
18250
|
+
var stored = getCommonValue(getIconValueIds(), iconOpacityField, {useDefault: true, defaultValue: 1});
|
|
18103
18251
|
val = stored === '' ? 1 : Number(stored);
|
|
18104
18252
|
return isFinite(val) ? val : 1;
|
|
18105
18253
|
}
|
|
@@ -18152,7 +18300,10 @@
|
|
|
18152
18300
|
}
|
|
18153
18301
|
}
|
|
18154
18302
|
|
|
18155
|
-
|
|
18303
|
+
// idsArg: (optional) the labels to write to, when they are not the whole
|
|
18304
|
+
// target -- an icon property goes only to the labels that have a symbol.
|
|
18305
|
+
function applyStyleValues(styles, idsArg) {
|
|
18306
|
+
var ids = idsArg || getTargetIds();
|
|
18156
18307
|
if (styles.length === 0) return;
|
|
18157
18308
|
releaseFocus();
|
|
18158
18309
|
// What the panel is set to is always what the next label gets, whether or
|
|
@@ -18165,8 +18316,8 @@
|
|
|
18165
18316
|
// style, so it has to be redrawn to show the change -- it is the preview.
|
|
18166
18317
|
refreshPendingLabel();
|
|
18167
18318
|
}
|
|
18168
|
-
if (
|
|
18169
|
-
applyStyleCommand(styles);
|
|
18319
|
+
if (ids.length > 0) {
|
|
18320
|
+
applyStyleCommand(styles, ids);
|
|
18170
18321
|
return;
|
|
18171
18322
|
}
|
|
18172
18323
|
if (!labelModeIsOn()) return;
|
|
@@ -18178,9 +18329,8 @@
|
|
|
18178
18329
|
if (session && session.id == -1 && session.refresh) session.refresh();
|
|
18179
18330
|
}
|
|
18180
18331
|
|
|
18181
|
-
function applyStyleCommand(styles) {
|
|
18332
|
+
function applyStyleCommand(styles, ids) {
|
|
18182
18333
|
var lyr = getActiveLayer();
|
|
18183
|
-
var ids = getTargetIds();
|
|
18184
18334
|
var parts = ['-style'];
|
|
18185
18335
|
if (!gui.console || !lyr) return;
|
|
18186
18336
|
styles.forEach(function(style) {
|
|
@@ -25361,6 +25511,13 @@
|
|
|
25361
25511
|
// layer instead of rebuilding it, and the cue moves with the map by wearing the
|
|
25362
25512
|
// label's own transform -- but a select-all on a big layer would still pay it,
|
|
25363
25513
|
// for an outline per label too small to tell apart anyway.
|
|
25514
|
+
//
|
|
25515
|
+
// Over the cap the selected labels wear a halo instead (.label-cue-marked),
|
|
25516
|
+
// which is a class on the text node and costs no measurement. It says less
|
|
25517
|
+
// than an outline -- no anchors, no knots, no box -- but a selection of
|
|
25518
|
+
// hundreds is a group being restyled rather than objects being handled one by
|
|
25519
|
+
// one, and the one thing it has to say is which labels are in it. This used to
|
|
25520
|
+
// draw nothing at all, so selecting a whole layer left the map unchanged.
|
|
25364
25521
|
var MAX_OUTLINES = 200;
|
|
25365
25522
|
|
|
25366
25523
|
// getEditingId: returns the feature id of an open text editing session, or -1.
|
|
@@ -25368,6 +25525,7 @@
|
|
|
25368
25525
|
function LabelSelection(gui, ext, hit, getEditingId) {
|
|
25369
25526
|
var self = {};
|
|
25370
25527
|
var groups = []; // one <g> per drawn cue, in the layer's markup
|
|
25528
|
+
var marked = []; // text nodes wearing the halo, when there are too many to outline
|
|
25371
25529
|
var drawn = null; // what those cues represent, so hover does not redraw them
|
|
25372
25530
|
var on = false;
|
|
25373
25531
|
var tetherId = -1; // the label whose text is being dragged off its anchor
|
|
@@ -25397,9 +25555,11 @@
|
|
|
25397
25555
|
// markup and takes the old cues with it.
|
|
25398
25556
|
self.refresh = function(force) {
|
|
25399
25557
|
var target = hit.getHitTarget();
|
|
25400
|
-
var
|
|
25401
|
-
var
|
|
25402
|
-
var
|
|
25558
|
+
var selected = on ? hit.getSelectionIds() : [];
|
|
25559
|
+
var tooMany = selected.length > MAX_OUTLINES;
|
|
25560
|
+
var ids = tooMany ? [] : selected;
|
|
25561
|
+
var hoverId = on ? getHoverId(selected) : -1;
|
|
25562
|
+
var key = selected.join(',') + '/' + hoverId + '/' + tetherId;
|
|
25403
25563
|
// Hover fires on every pointer move, and most of them change nothing here.
|
|
25404
25564
|
if (!force && drawn === key) return;
|
|
25405
25565
|
clearAll();
|
|
@@ -25414,6 +25574,9 @@
|
|
|
25414
25574
|
// would offer handles that cannot be grabbed.
|
|
25415
25575
|
draw(target, id, 'label-cue-selected', true);
|
|
25416
25576
|
});
|
|
25577
|
+
// Too many to outline: a halo on the glyphs instead, which is the whole cue
|
|
25578
|
+
// for those labels.
|
|
25579
|
+
if (tooMany) markAll(target, selected);
|
|
25417
25580
|
};
|
|
25418
25581
|
|
|
25419
25582
|
// The label under the pointer, when showing it would say something: not one
|
|
@@ -25425,10 +25588,23 @@
|
|
|
25425
25588
|
return id;
|
|
25426
25589
|
}
|
|
25427
25590
|
|
|
25428
|
-
//
|
|
25429
|
-
|
|
25430
|
-
|
|
25431
|
-
|
|
25591
|
+
// Puts the halo on each selected label's glyphs. A class of its own rather
|
|
25592
|
+
// than the label_style mode's yellow one: that class is cleared on every
|
|
25593
|
+
// model update while the label tool is on, so the two would fight.
|
|
25594
|
+
function markAll(target, ids) {
|
|
25595
|
+
ids.forEach(function(id) {
|
|
25596
|
+
var nodes = findNodes(target, id);
|
|
25597
|
+
if (!nodes) return;
|
|
25598
|
+
nodes.text.classList.add('label-cue-marked');
|
|
25599
|
+
marked.push(nodes.text);
|
|
25600
|
+
});
|
|
25601
|
+
}
|
|
25602
|
+
|
|
25603
|
+
function clearMarks() {
|
|
25604
|
+
marked.forEach(function(node) {
|
|
25605
|
+
node.classList.remove('label-cue-marked');
|
|
25606
|
+
});
|
|
25607
|
+
marked = [];
|
|
25432
25608
|
}
|
|
25433
25609
|
|
|
25434
25610
|
function draw(target, id, className, withHandles) {
|
|
@@ -25502,7 +25678,8 @@
|
|
|
25502
25678
|
var content;
|
|
25503
25679
|
if (!text) return null;
|
|
25504
25680
|
content = text.querySelector('textPath') || text;
|
|
25505
|
-
return {symbol: symbol,
|
|
25681
|
+
return {symbol: symbol, text: text, content: content,
|
|
25682
|
+
pathId: getPathId(content)};
|
|
25506
25683
|
}
|
|
25507
25684
|
|
|
25508
25685
|
// The id of the baseline a path label is laid along, or null for an anchored
|
|
@@ -25599,12 +25776,183 @@
|
|
|
25599
25776
|
if (g.parentNode) g.parentNode.removeChild(g);
|
|
25600
25777
|
});
|
|
25601
25778
|
groups = [];
|
|
25779
|
+
clearMarks();
|
|
25602
25780
|
drawn = null;
|
|
25603
25781
|
}
|
|
25604
25782
|
|
|
25605
25783
|
return self;
|
|
25606
25784
|
}
|
|
25607
25785
|
|
|
25786
|
+
// Selecting a group of labels by pointing at one of them: every label on the
|
|
25787
|
+
// layer, or the ones that share its text style, its colour or its symbol.
|
|
25788
|
+
//
|
|
25789
|
+
// Styling a group is something the panel could already do -- a control shows
|
|
25790
|
+
// nothing when the selected labels disagree, and writes to all of them -- but
|
|
25791
|
+
// there was no way to select more than a shift-click's worth. These are the
|
|
25792
|
+
// predicates the context menu offers, as pure functions so that they can be
|
|
25793
|
+
// tested without a map.
|
|
25794
|
+
//
|
|
25795
|
+
// See docs/development/label-tool-design.md.
|
|
25796
|
+
|
|
25797
|
+
// The properties that make up "the same text style": how the glyphs are drawn,
|
|
25798
|
+
// and nothing about where the text sits.
|
|
25799
|
+
//
|
|
25800
|
+
// Alignment, position, offsets and a path label's start offset are left out
|
|
25801
|
+
// deliberately. They are what the panel's other sections edit, and two labels
|
|
25802
|
+
// in the same font at the same size are in the same text style whether one of
|
|
25803
|
+
// them is centred and the other hangs north-east of its anchor -- which is
|
|
25804
|
+
// usually the difference between a label placed by hand and one that was not.
|
|
25805
|
+
//
|
|
25806
|
+
// css and class are in, because either can carry anything the properties above
|
|
25807
|
+
// carry and more: two labels are not in the same text style if one of them is
|
|
25808
|
+
// wearing a stylesheet the other is not.
|
|
25809
|
+
var TEXT_STYLE_FIELDS = [
|
|
25810
|
+
'font-family', 'font-size', 'font-style', 'font-weight', 'font-stretch',
|
|
25811
|
+
'letter-spacing', 'line-height', 'css', 'class'
|
|
25812
|
+
];
|
|
25813
|
+
|
|
25814
|
+
// The colour of the text, and not its opacity: the item says colour, and a
|
|
25815
|
+
// label faded to 50% is the same colour as one that is not.
|
|
25816
|
+
var FILL_FIELDS = ['fill'];
|
|
25817
|
+
|
|
25818
|
+
// The shape of the symbol, and not its size or colour, for the same reason.
|
|
25819
|
+
var ICON_FIELDS = ['icon'];
|
|
25820
|
+
|
|
25821
|
+
// What a property means when a label does not carry it.
|
|
25822
|
+
//
|
|
25823
|
+
// Compared after this substitution, so that a label with font-size=12 and one
|
|
25824
|
+
// with no font-size match: they render identically, and the panel already
|
|
25825
|
+
// treats them as agreeing. Without it the commonest pair of labels on a map --
|
|
25826
|
+
// one the panel has touched and one it has not -- would count as different
|
|
25827
|
+
// text styles while looking the same.
|
|
25828
|
+
//
|
|
25829
|
+
// font-family is deliberately absent. An unset font is whatever the browser
|
|
25830
|
+
// resolves sans-serif to, which differs by machine and is a real difference
|
|
25831
|
+
// from a font the user named, so an unset font is its own value and matches
|
|
25832
|
+
// only another unset one. css, class and icon are absent because they have no
|
|
25833
|
+
// default: not having one is not the same as having a particular one.
|
|
25834
|
+
var STYLE_DEFAULTS = {
|
|
25835
|
+
'font-size': 12,
|
|
25836
|
+
'font-style': 'normal',
|
|
25837
|
+
'font-weight': '400',
|
|
25838
|
+
'letter-spacing': 0,
|
|
25839
|
+
fill: '#000000'
|
|
25840
|
+
};
|
|
25841
|
+
|
|
25842
|
+
// The items the "select" section offers, in menu order.
|
|
25843
|
+
//
|
|
25844
|
+
// needs: a property the pointed-at label must carry for the item to be worth
|
|
25845
|
+
// offering. "Same icon" on a label with no symbol would select every label
|
|
25846
|
+
// that has none, which is a question nobody asked and which "all labels"
|
|
25847
|
+
// nearly answers anyway.
|
|
25848
|
+
// always: offered whatever it matches, rather than only when it says something
|
|
25849
|
+
// the other items do not. See getLabelSelectActions().
|
|
25850
|
+
var SELECT_KINDS = [
|
|
25851
|
+
{name: 'all', label: 'all labels'},
|
|
25852
|
+
{name: 'text-style', label: 'same text style', fields: TEXT_STYLE_FIELDS,
|
|
25853
|
+
always: true},
|
|
25854
|
+
{name: 'fill', label: 'same fill color', fields: FILL_FIELDS},
|
|
25855
|
+
{name: 'icon', label: 'same icon', fields: ICON_FIELDS, needs: 'icon'}
|
|
25856
|
+
];
|
|
25857
|
+
|
|
25858
|
+
// Every label on the layer, in ascending id order.
|
|
25859
|
+
//
|
|
25860
|
+
// Not every feature: a labels layer can hold plain points as well -- a dots
|
|
25861
|
+
// layer given label text by the point panel is one -- and a point is not
|
|
25862
|
+
// something the label panel can style.
|
|
25863
|
+
//
|
|
25864
|
+
// records: the layer's data records, or null
|
|
25865
|
+
// isLabel: (rec) -> whether a record is a label at all. Injected rather than
|
|
25866
|
+
// imported so that this module stays testable without the GUI's internals;
|
|
25867
|
+
// the caller passes internal.svg.featureIsLabel.
|
|
25868
|
+
function getAllLabelIds(records, isLabel) {
|
|
25869
|
+
var ids = [];
|
|
25870
|
+
var i;
|
|
25871
|
+
for (i = 0; records && i < records.length; i++) {
|
|
25872
|
+
if (isLabel(records[i])) ids.push(i);
|
|
25873
|
+
}
|
|
25874
|
+
return ids;
|
|
25875
|
+
}
|
|
25876
|
+
|
|
25877
|
+
// The labels that match label @id on @kind, in ascending id order.
|
|
25878
|
+
//
|
|
25879
|
+
// records: the layer's data records, or null
|
|
25880
|
+
// id: the label the predicate is taken from, which is always in the result
|
|
25881
|
+
// kind: 'all', 'text-style', 'fill' or 'icon'
|
|
25882
|
+
// isLabel: as getAllLabelIds()
|
|
25883
|
+
function getMatchingLabelIds(records, id, kind, isLabel) {
|
|
25884
|
+
var defn = findKind(kind);
|
|
25885
|
+
var rec = records && records[id];
|
|
25886
|
+
if (!defn || !rec || !isLabel(rec)) return [];
|
|
25887
|
+
return getAllLabelIds(records, isLabel).filter(function(i) {
|
|
25888
|
+
return !defn.fields || fieldsMatch(records[i], rec, defn.fields);
|
|
25889
|
+
});
|
|
25890
|
+
}
|
|
25891
|
+
|
|
25892
|
+
// The menu items worth offering for a right-click on label @id, as
|
|
25893
|
+
// [{name, label, ids}, ...].
|
|
25894
|
+
//
|
|
25895
|
+
// "All labels" and "same text style" are always offered, the first because it
|
|
25896
|
+
// asks nothing of the layer and the second because it is the item this section
|
|
25897
|
+
// is mostly for: styling the type of a map as a group. Offering it only when it
|
|
25898
|
+
// differs from "all labels" would make it come and go between one right-click
|
|
25899
|
+
// and the next, and its count is worth reading either way -- "same text style
|
|
25900
|
+
// 40" over a layer of 40 says the labels are uniform, which is a fact about the
|
|
25901
|
+
// layer and not a reason to hide the item.
|
|
25902
|
+
//
|
|
25903
|
+
// The rest are dropped rather than shown and disabled when:
|
|
25904
|
+
//
|
|
25905
|
+
// - They would select the label already pointed at and nothing else, which
|
|
25906
|
+
// would look like a way of narrowing the selection to one label -- a plain
|
|
25907
|
+
// click on that label already is one.
|
|
25908
|
+
// - They match every label on the layer, which "all labels" says more plainly.
|
|
25909
|
+
// A layer with one label on it gets no section at all: every item there would
|
|
25910
|
+
// select the label that was right-clicked, which clicking it already does.
|
|
25911
|
+
function getLabelSelectActions(records, id, isLabel) {
|
|
25912
|
+
var rec = records && records[id];
|
|
25913
|
+
var all = getMatchingLabelIds(records, id, 'all', isLabel);
|
|
25914
|
+
var out = [];
|
|
25915
|
+
if (!rec || all.length < 2) return out;
|
|
25916
|
+
SELECT_KINDS.forEach(function(defn) {
|
|
25917
|
+
var always = defn.name == 'all' || defn.always;
|
|
25918
|
+
var ids;
|
|
25919
|
+
if (defn.needs && !hasValue(rec[defn.needs])) return;
|
|
25920
|
+
ids = defn.name == 'all' ? all :
|
|
25921
|
+
getMatchingLabelIds(records, id, defn.name, isLabel);
|
|
25922
|
+
if (!always && (ids.length < 2 || ids.length === all.length)) return;
|
|
25923
|
+
out.push({name: defn.name, label: defn.label, ids: ids});
|
|
25924
|
+
});
|
|
25925
|
+
return out;
|
|
25926
|
+
}
|
|
25927
|
+
|
|
25928
|
+
function findKind(name) {
|
|
25929
|
+
var found = null;
|
|
25930
|
+
SELECT_KINDS.forEach(function(defn) {
|
|
25931
|
+
if (defn.name == name) found = defn;
|
|
25932
|
+
});
|
|
25933
|
+
return found;
|
|
25934
|
+
}
|
|
25935
|
+
|
|
25936
|
+
function fieldsMatch(a, b, fields) {
|
|
25937
|
+
return fields.every(function(field) {
|
|
25938
|
+
return styleValue(a, field) === styleValue(b, field);
|
|
25939
|
+
});
|
|
25940
|
+
}
|
|
25941
|
+
|
|
25942
|
+
// A property as it is compared: what the label carries, or what it means to
|
|
25943
|
+
// carry nothing, as a string so that 12 and '12' are one value.
|
|
25944
|
+
function styleValue(rec, field) {
|
|
25945
|
+
var val = rec ? rec[field] : null;
|
|
25946
|
+
if (!hasValue(val)) {
|
|
25947
|
+
val = field in STYLE_DEFAULTS ? STYLE_DEFAULTS[field] : '';
|
|
25948
|
+
}
|
|
25949
|
+
return String(val);
|
|
25950
|
+
}
|
|
25951
|
+
|
|
25952
|
+
function hasValue(val) {
|
|
25953
|
+
return !(val === undefined || val === null || val === '');
|
|
25954
|
+
}
|
|
25955
|
+
|
|
25608
25956
|
// State of a label curve being drawn, and the operations the curvature tool
|
|
25609
25957
|
// performs on it.
|
|
25610
25958
|
//
|
|
@@ -26004,6 +26352,7 @@
|
|
|
26004
26352
|
if (!active()) return;
|
|
26005
26353
|
id = getRightClickedLabel(e);
|
|
26006
26354
|
if (target && id > -1) {
|
|
26355
|
+
e.selectActions = getSelectActions(target, id);
|
|
26007
26356
|
e.deleteFeature = getDeleteAction(target, id);
|
|
26008
26357
|
// Nobody guesses that text is dragged across its own curve, and there is
|
|
26009
26358
|
// no bracket drawn to suggest it, so the gesture has a second way in.
|
|
@@ -26032,6 +26381,43 @@
|
|
|
26032
26381
|
return -1;
|
|
26033
26382
|
}
|
|
26034
26383
|
|
|
26384
|
+
// The "select" items for a right-click on label @id: the labels that share
|
|
26385
|
+
// something with it, so that a style can be changed across a group without
|
|
26386
|
+
// shift-clicking every one of it.
|
|
26387
|
+
//
|
|
26388
|
+
// The predicate comes from the label pointed at rather than from whatever
|
|
26389
|
+
// happens to be selected -- a right-click deliberately leaves the selection
|
|
26390
|
+
// alone -- and running an item replaces the selection with what it matched.
|
|
26391
|
+
function getSelectActions(target, id) {
|
|
26392
|
+
var records = target.data ? target.data.getRecords() : null;
|
|
26393
|
+
return getLabelSelectActions(records, id, internal.svg.featureIsLabel)
|
|
26394
|
+
.map(function(action) {
|
|
26395
|
+
return {
|
|
26396
|
+
label: action.label,
|
|
26397
|
+
count: action.ids.length,
|
|
26398
|
+
run: function() { selectLabels(action.ids); }
|
|
26399
|
+
};
|
|
26400
|
+
});
|
|
26401
|
+
}
|
|
26402
|
+
|
|
26403
|
+
// A label being typed into is not a label being styled, so a group selection
|
|
26404
|
+
// ends the session -- which saves its text, as clicking away would. Closing
|
|
26405
|
+
// first and selecting second, because closing puts the label it was editing
|
|
26406
|
+
// back in the selection when the selection is empty.
|
|
26407
|
+
function selectLabels(ids) {
|
|
26408
|
+
if (editor.isOpen()) editor.close();
|
|
26409
|
+
hit.setSelectionIds(ids);
|
|
26410
|
+
}
|
|
26411
|
+
|
|
26412
|
+
// Cmd/Ctrl-A: the keyboard way to the menu's "all labels", for the commonest
|
|
26413
|
+
// of the group selections -- restyling a layer's labels as one.
|
|
26414
|
+
function selectAllLabels() {
|
|
26415
|
+
var target = hit.getHitTarget();
|
|
26416
|
+
var records = target && target.data ? target.data.getRecords() : null;
|
|
26417
|
+
var ids = getAllLabelIds(records, internal.svg.featureIsLabel);
|
|
26418
|
+
if (ids.length > 0) selectLabels(ids);
|
|
26419
|
+
}
|
|
26420
|
+
|
|
26035
26421
|
// Deleting a label ends whatever was being done to it first: its text
|
|
26036
26422
|
// session would otherwise write the text back to an id that now belongs to
|
|
26037
26423
|
// the label that moved up into the gap, and the same shift is why the
|
|
@@ -26807,8 +27193,17 @@
|
|
|
26807
27193
|
gui.keyboard.on('keydown', function(e) {
|
|
26808
27194
|
if (!active()) return;
|
|
26809
27195
|
// While a session is open the textarea has focus and handles its own keys;
|
|
26810
|
-
// it stops propagation for the ones it acts on.
|
|
27196
|
+
// it stops propagation for the ones it acts on. Cmd-A there means select
|
|
27197
|
+
// the text being typed, which is the browser's to handle.
|
|
26811
27198
|
if (editor.isOpen()) return;
|
|
27199
|
+
// Not while a curve is being drawn: that gesture owns the keyboard, and
|
|
27200
|
+
// selecting the layer in the middle of placing a label is not what the
|
|
27201
|
+
// keystroke can have meant.
|
|
27202
|
+
if (isSelectAll(e) && !drawingCurve()) {
|
|
27203
|
+
selectAllLabels();
|
|
27204
|
+
consume(e); // the default is to select the whole page
|
|
27205
|
+
return;
|
|
27206
|
+
}
|
|
26812
27207
|
if (e.keyName == 'esc') {
|
|
26813
27208
|
// One rung per press, innermost first: the curve being drawn, then the
|
|
26814
27209
|
// selection, then the armed tool. The hit control's own escape handler
|
|
@@ -26839,6 +27234,16 @@
|
|
|
26839
27234
|
}
|
|
26840
27235
|
}, null, 10);
|
|
26841
27236
|
|
|
27237
|
+
// Cmd-A on a Mac, Ctrl-A elsewhere. Read from the key rather than the
|
|
27238
|
+
// keyCode, as undo and redo are.
|
|
27239
|
+
function isSelectAll(e) {
|
|
27240
|
+
var evt = e.originalEvent;
|
|
27241
|
+
if (!evt || !(evt.metaKey || evt.ctrlKey) || evt.shiftKey || evt.altKey) {
|
|
27242
|
+
return false;
|
|
27243
|
+
}
|
|
27244
|
+
return (evt.key || '').toLowerCase() == 'a';
|
|
27245
|
+
}
|
|
27246
|
+
|
|
26842
27247
|
// Suppresses the key's default action as well as the rest of the GUI's
|
|
26843
27248
|
// handlers. The default matters here: finishing a curve opens an editing
|
|
26844
27249
|
// session and focuses its textarea, and without this the same Enter goes on
|
|
@@ -30847,8 +31252,7 @@
|
|
|
30847
31252
|
var mapLayers = gui.container.findChild('.map-layers').node();
|
|
30848
31253
|
var map = gui.container.findChild('.mshp-main-map');
|
|
30849
31254
|
var toggle = gui.buttons.addButton('#frame-tool-icon')
|
|
30850
|
-
.addClass('menu-btn preview-toggle')
|
|
30851
|
-
.attr('title', 'Toggle map preview');
|
|
31255
|
+
.addClass('menu-btn preview-toggle');
|
|
30852
31256
|
var readout = El('div')
|
|
30853
31257
|
.addClass('preview-readout')
|
|
30854
31258
|
.appendTo(map)
|
|
@@ -30968,7 +31372,11 @@
|
|
|
30968
31372
|
var available = hasFrame();
|
|
30969
31373
|
toggle.removeClass('disabled');
|
|
30970
31374
|
toggle.classed('selected', available && self.isOn());
|
|
30971
|
-
|
|
31375
|
+
// data-tooltip, not title: the CSS label in .nav-btn[data-tooltip] appears
|
|
31376
|
+
// without the browser's delay and matches the arrow menu's type.
|
|
31377
|
+
var label = available ? 'Toggle map preview' : 'Add map frame';
|
|
31378
|
+
toggle.attr('data-tooltip', label);
|
|
31379
|
+
toggle.attr('aria-label', label);
|
|
30972
31380
|
toggle.attr('aria-disabled', 'false');
|
|
30973
31381
|
toggle.attr('aria-pressed', available && self.isOn() ? 'true' : 'false');
|
|
30974
31382
|
if (gui.map.isPreviewView()) {
|
package/www/mapshaper.js
CHANGED
|
@@ -82789,7 +82789,7 @@ ${svg}
|
|
|
82789
82789
|
return name == 'rectangle' || name == 'rectangles' || name == 'filter' && opts.cleanup;
|
|
82790
82790
|
}
|
|
82791
82791
|
|
|
82792
|
-
var version = "0.7.
|
|
82792
|
+
var version = "0.7.66";
|
|
82793
82793
|
|
|
82794
82794
|
// Parse command line args into commands and run them
|
|
82795
82795
|
// Function takes an optional Node-style callback. A Promise is returned if no callback is given.
|
package/www/page.css
CHANGED
|
@@ -2259,6 +2259,19 @@ body.pan.panning .map-layers:not(.drawing) {
|
|
|
2259
2259
|
vector-effect: non-scaling-stroke;
|
|
2260
2260
|
}
|
|
2261
2261
|
|
|
2262
|
+
/* The cue for a selection too large to outline label by label -- selecting a
|
|
2263
|
+
whole layer to restyle it. A halo in the selection's own blue, the same
|
|
2264
|
+
treatment the older styling mode uses in yellow: at that size the question
|
|
2265
|
+
is which labels are in the selection, not where each one's anchor is. */
|
|
2266
|
+
.map-layers > svg text.label-cue-marked {
|
|
2267
|
+
stroke: #3a7ddc;
|
|
2268
|
+
stroke-opacity: 0.3;
|
|
2269
|
+
paint-order: stroke fill;
|
|
2270
|
+
stroke-width: 4px;
|
|
2271
|
+
stroke-linecap: round;
|
|
2272
|
+
stroke-linejoin: round;
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2262
2275
|
/* Transparent rather than fill:none / stroke:none, because an unpainted shape
|
|
2263
2276
|
is not hit-testable. */
|
|
2264
2277
|
.label-edit-hit-area {
|
|
@@ -2412,6 +2425,28 @@ body.pan.panning .map-layers:not(.drawing) {
|
|
|
2412
2425
|
fill: white;
|
|
2413
2426
|
}
|
|
2414
2427
|
|
|
2428
|
+
/* Tooltip shown to the left of a nav button on hover. It borrows the type of
|
|
2429
|
+
.nav-menu-item so the strip reads as one family, but stays a flat label --
|
|
2430
|
+
no border, radius or shadow -- so it is not mistaken for the arrow menu.
|
|
2431
|
+
The translucent white wash keeps it legible over dark map content. */
|
|
2432
|
+
.nav-btn[data-tooltip]:hover:not(.disabled)::after {
|
|
2433
|
+
content: attr(data-tooltip);
|
|
2434
|
+
position: absolute;
|
|
2435
|
+
/* Below .nav-sub-menu, which is at -1 and lingers for a moment after the
|
|
2436
|
+
pointer leaves the arrow button: the tooltip must pass behind it. */
|
|
2437
|
+
z-index: -2;
|
|
2438
|
+
top: 50%;
|
|
2439
|
+
right: calc(100% + 2px);
|
|
2440
|
+
transform: translateY(-50%);
|
|
2441
|
+
pointer-events: none;
|
|
2442
|
+
background-color: rgba(255, 255, 255, 0.85);
|
|
2443
|
+
color: var(--colored-text);
|
|
2444
|
+
font-size: 13px;
|
|
2445
|
+
line-height: 13px;
|
|
2446
|
+
padding: 3px 6px 4px 6px;
|
|
2447
|
+
white-space: nowrap;
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2415
2450
|
.nav-btn.preview-toggle {
|
|
2416
2451
|
align-items: center;
|
|
2417
2452
|
box-sizing: border-box;
|
|
@@ -3215,7 +3250,8 @@ body.pan.panning .map-layers:not(.drawing) {
|
|
|
3215
3250
|
|
|
3216
3251
|
/* A switch, because whether the label has a symbol at all is a yes/no that the
|
|
3217
3252
|
four shapes below it then qualify. Off is a pale track with the knob left,
|
|
3218
|
-
on a dark one with the knob right
|
|
3253
|
+
on a dark one with the knob right, and mixed -- some of the selected labels
|
|
3254
|
+
on, some off -- is both halves at once with the knob over the join. */
|
|
3219
3255
|
.label-toggle {
|
|
3220
3256
|
position: relative;
|
|
3221
3257
|
flex: none;
|
|
@@ -3248,6 +3284,16 @@ body.pan.panning .map-layers:not(.drawing) {
|
|
|
3248
3284
|
left: 13px;
|
|
3249
3285
|
}
|
|
3250
3286
|
|
|
3287
|
+
/* The hard edge is the point: a gradient stop rather than a blend, so that the
|
|
3288
|
+
track reads as one half on and one half off rather than as a third colour. */
|
|
3289
|
+
.label-toggle.mixed {
|
|
3290
|
+
background: linear-gradient(to right, #333 50%, #ccc 50%);
|
|
3291
|
+
}
|
|
3292
|
+
|
|
3293
|
+
.label-toggle.mixed .label-toggle-knob {
|
|
3294
|
+
left: 7.5px;
|
|
3295
|
+
}
|
|
3296
|
+
|
|
3251
3297
|
.label-toggle.disabled {
|
|
3252
3298
|
opacity: 0.45;
|
|
3253
3299
|
cursor: default;
|
|
@@ -3349,6 +3395,13 @@ body.pan.panning .map-layers:not(.drawing) {
|
|
|
3349
3395
|
border-radius: 2px;
|
|
3350
3396
|
}
|
|
3351
3397
|
|
|
3398
|
+
/* A swatch for a selection whose colours disagree: split rather than empty,
|
|
3399
|
+
because empty is how the swatch says the colour is unset. Two greys, so that
|
|
3400
|
+
it cannot be read as one of the colours in the selection. */
|
|
3401
|
+
.label-style-panel .label-color-field .label-color-chit.mixed {
|
|
3402
|
+
background-image: linear-gradient(135deg, #d9d9d9 0 50%, #8c8c8c 50% 100%);
|
|
3403
|
+
}
|
|
3404
|
+
|
|
3352
3405
|
.label-style-panel .label-color-field input[type="text"] {
|
|
3353
3406
|
flex: 1;
|
|
3354
3407
|
width: auto;
|
|
@@ -3761,21 +3814,35 @@ body.dragging-color-picker * {
|
|
|
3761
3814
|
.contextmenu-item {
|
|
3762
3815
|
background-color: #fff;
|
|
3763
3816
|
white-space: nowrap;
|
|
3764
|
-
padding: 3px 10px 4px 10px;
|
|
3817
|
+
/* padding: 3px 10px 4px 10px; */
|
|
3818
|
+
margin: 1px 10px;
|
|
3819
|
+
padding: 2px 0;
|
|
3765
3820
|
cursor: pointer;
|
|
3766
|
-
font-size:
|
|
3821
|
+
font-size: 12px;
|
|
3767
3822
|
line-height: 13px;
|
|
3768
3823
|
}
|
|
3769
3824
|
|
|
3770
3825
|
.contextmenu-label {
|
|
3771
3826
|
color: #999;
|
|
3772
3827
|
white-space: nowrap;
|
|
3773
|
-
padding:
|
|
3828
|
+
padding: 5px 10px 1px 10px;
|
|
3774
3829
|
font-size: 12px;
|
|
3775
3830
|
line-height: 12px;
|
|
3776
3831
|
margin-top: -2px;
|
|
3777
3832
|
}
|
|
3778
3833
|
|
|
3834
|
+
/* How many labels a "select" item would select. In the grey of a menu heading
|
|
3835
|
+
rather than the item's own colour: it qualifies what the item says rather
|
|
3836
|
+
than being part of it. */
|
|
3837
|
+
.contextmenu-count {
|
|
3838
|
+
font-size: 10px;
|
|
3839
|
+
color: #999;
|
|
3840
|
+
}
|
|
3841
|
+
|
|
3842
|
+
.contextmenu-item:hover .contextmenu-count {
|
|
3843
|
+
color: #777;
|
|
3844
|
+
}
|
|
3845
|
+
|
|
3779
3846
|
/* color tile shown instead of a bullet in the raster color readout */
|
|
3780
3847
|
.contextmenu-swatch {
|
|
3781
3848
|
display: inline-block;
|