mapshaper 0.7.64 → 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 +615 -161
- package/www/mapshaper.js +1 -1
- package/www/page.css +71 -4
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.
|
|
@@ -16663,10 +16706,15 @@
|
|
|
16663
16706
|
// aligned: whether it carries a label-align (see below)
|
|
16664
16707
|
// delta: {dx, dy} -- how far the pointer has moved, in the same space.
|
|
16665
16708
|
//
|
|
16666
|
-
// Returns
|
|
16667
|
-
//
|
|
16668
|
-
//
|
|
16669
|
-
//
|
|
16709
|
+
// Returns two descriptions of the same placement:
|
|
16710
|
+
//
|
|
16711
|
+
// dx, dy, text-anchor: what the drag writes to the record. The three have to
|
|
16712
|
+
// be written together: applying dx without its text-anchor moves the text
|
|
16713
|
+
// by half its own width or by all of it.
|
|
16714
|
+
// x, anchor: where the text sits and how it is justified while it is drawn,
|
|
16715
|
+
// which is what the preview writes onto the rendered label. The same as
|
|
16716
|
+
// dx and text-anchor except on an aligned label, whose stored offset is
|
|
16717
|
+
// measured against a justification it is not drawn with.
|
|
16670
16718
|
function getOffsetDragValues(start, delta) {
|
|
16671
16719
|
var width = start.width > 0 ? start.width : 0;
|
|
16672
16720
|
var drawnAnchor = normalizeAnchor(start.anchor);
|
|
@@ -16685,12 +16733,18 @@
|
|
|
16685
16733
|
// if the alignment were later removed.
|
|
16686
16734
|
var anchor = start.aligned ? 'start' :
|
|
16687
16735
|
width > 0 ? getAnchorForCentre(left + width / 2, width) : drawnAnchor;
|
|
16736
|
+
// An aligned label goes on being drawn with the anchor its alignment gives
|
|
16737
|
+
// it, whatever the drag writes: the alignment is unchanged, so the renderer
|
|
16738
|
+
// will justify it the same way afterwards. Drawing it as 'start' instead
|
|
16739
|
+
// would slide the text right by half its width or by all of it for the
|
|
16740
|
+
// length of the drag, and drop it back on release.
|
|
16741
|
+
var drawn = start.aligned ? drawnAnchor : anchor;
|
|
16688
16742
|
return {
|
|
16689
16743
|
dx: round$1(left + anchorOffsets[anchor] * width),
|
|
16690
16744
|
dy: round$1(start.dy + delta.dy),
|
|
16691
16745
|
'text-anchor': anchor,
|
|
16692
|
-
x: round$1(
|
|
16693
|
-
|
|
16746
|
+
x: round$1(left + anchorOffsets[drawn] * width),
|
|
16747
|
+
anchor: drawn
|
|
16694
16748
|
};
|
|
16695
16749
|
}
|
|
16696
16750
|
|
|
@@ -16771,6 +16825,11 @@
|
|
|
16771
16825
|
// no line-height until one is chosen. "auto" is the honest description of a
|
|
16772
16826
|
// blank field: something else decides.
|
|
16773
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';
|
|
16774
16833
|
var labelStyleMode = 'label_style';
|
|
16775
16834
|
var labelStylePanelMode = 'label_style_tool';
|
|
16776
16835
|
var labelMode = 'label';
|
|
@@ -17070,8 +17129,12 @@
|
|
|
17070
17129
|
applyInlineCss(cssInput.node().value.trim());
|
|
17071
17130
|
});
|
|
17072
17131
|
|
|
17132
|
+
// The caption for the grid and the drag-mode toggle below it, and what
|
|
17133
|
+
// gives Fixed|Draggable something to be an adjective of. "Position" read as
|
|
17134
|
+
// the label's own position -- which a drag moves in either mode -- and made
|
|
17135
|
+
// Fixed sound like a lock on a label that is still perfectly draggable.
|
|
17073
17136
|
var positionRow = El('label').addClass('label-style-row').appendTo(textSection);
|
|
17074
|
-
El('span').appendTo(positionRow).text('
|
|
17137
|
+
El('span').appendTo(positionRow).text('Offset from anchor');
|
|
17075
17138
|
|
|
17076
17139
|
// Whether the label has a symbol is one question and which symbol it has is
|
|
17077
17140
|
// another, so the first is a switch on the section's heading rather than a
|
|
@@ -17200,26 +17263,38 @@
|
|
|
17200
17263
|
return makePanelSection(panel, title, opts);
|
|
17201
17264
|
}
|
|
17202
17265
|
|
|
17203
|
-
// A
|
|
17204
|
-
//
|
|
17205
|
-
//
|
|
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.
|
|
17206
17277
|
function makeToggle(parent, opts) {
|
|
17207
|
-
var track = El('div').addClass('label-toggle').attr('role', '
|
|
17208
|
-
var
|
|
17278
|
+
var track = El('div').addClass('label-toggle').attr('role', 'checkbox').appendTo(parent);
|
|
17279
|
+
var state = 'off';
|
|
17209
17280
|
var disabled = false;
|
|
17210
17281
|
El('div').addClass('label-toggle-knob').appendTo(track);
|
|
17211
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.
|
|
17212
17286
|
track.on('click', function() {
|
|
17213
17287
|
if (disabled) return;
|
|
17214
|
-
opts.onChange(
|
|
17288
|
+
opts.onChange(state != 'on');
|
|
17215
17289
|
});
|
|
17216
17290
|
return {
|
|
17217
|
-
|
|
17218
|
-
|
|
17219
|
-
|
|
17220
|
-
|
|
17221
|
-
|
|
17222
|
-
|
|
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');
|
|
17223
17298
|
},
|
|
17224
17299
|
setDisabled: function(off) {
|
|
17225
17300
|
disabled = !!off;
|
|
@@ -17242,6 +17317,9 @@
|
|
|
17242
17317
|
function makeMeasureInput(parent, field, placeholder) {
|
|
17243
17318
|
var input = El('input').attr('type', 'text').addClass('label-measure-input')
|
|
17244
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)
|
|
17245
17323
|
.appendTo(parent)
|
|
17246
17324
|
.on('change', function() {
|
|
17247
17325
|
applyStyleValues([[field, input.node().value.trim()]]);
|
|
@@ -17413,51 +17491,72 @@
|
|
|
17413
17491
|
// With nothing selected the menu shows the font the next label will be
|
|
17414
17492
|
// made in; with labels selected it shows the font they are drawn in, which
|
|
17415
17493
|
// for a label carrying none is whatever sans-serif resolves to here.
|
|
17416
|
-
var
|
|
17494
|
+
var font = getShownValue(ids, fontField,
|
|
17417
17495
|
{useDefault: true, defaultValue: getFontNameForTarget(ids)});
|
|
17418
|
-
var
|
|
17496
|
+
var fontSize = getShownValue(ids, fontSizeField, {useDefault: true, defaultValue: defaultFontSize});
|
|
17419
17497
|
var fontStyleVal = getCommonValue(ids, fontStyleField, {useDefault: true, defaultValue: defaultFontStyle});
|
|
17420
17498
|
var fontWeightVal = getCommonValue(ids, fontWeightField, {useDefault: true, defaultValue: defaultFontWeight});
|
|
17421
|
-
var
|
|
17422
|
-
var
|
|
17423
|
-
var
|
|
17424
|
-
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);
|
|
17425
17503
|
var alignVal = getCommonAlignment(ids);
|
|
17426
|
-
var
|
|
17504
|
+
var css = getShownValue(ids, cssField);
|
|
17427
17505
|
var posVal = getCommonValue(ids, 'label-pos');
|
|
17428
|
-
|
|
17429
|
-
|
|
17430
|
-
|
|
17431
|
-
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});
|
|
17432
17514
|
updateEditingStatus(manualIds.length, !!getLabelTextSession(gui));
|
|
17433
17515
|
updateSavedStyleControls();
|
|
17434
17516
|
fontSelect.node().disabled = !showValues;
|
|
17435
|
-
updateFontControl(
|
|
17436
|
-
updateFontStyleControls(
|
|
17437
|
-
updateFontSizeControls(
|
|
17438
|
-
updateColorControls(
|
|
17439
|
-
updateOpacityControl(opacityInput,
|
|
17440
|
-
updateMeasureControl(letterSpacingInput,
|
|
17441
|
-
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);
|
|
17442
17524
|
updateAlignButtons(showValues ? alignVal : '');
|
|
17443
|
-
updateCssControl(
|
|
17525
|
+
updateCssControl(css);
|
|
17444
17526
|
updatePositionButtons(showValues ? posVal : '', ids);
|
|
17445
17527
|
// The symbol's controls keep showing their values while the switch is off,
|
|
17446
17528
|
// greyed: what they show is what the symbol comes back as.
|
|
17447
17529
|
var iconOff = updateIconControls(showValues ? iconVal : '');
|
|
17448
|
-
updateIconSizeControls(
|
|
17449
|
-
updateIconColorControls(
|
|
17450
|
-
updateOpacityControl(iconOpacityInput,
|
|
17530
|
+
updateIconSizeControls(iconSize, iconOff);
|
|
17531
|
+
updateIconColorControls(iconColor, iconOff);
|
|
17532
|
+
updateOpacityControl(iconOpacityInput, iconOpacity, iconOff);
|
|
17451
17533
|
}
|
|
17452
17534
|
|
|
17453
|
-
|
|
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) {
|
|
17454
17540
|
input.node().disabled = disabled || !controlsEnabled();
|
|
17455
|
-
input.node().value =
|
|
17541
|
+
input.node().value = shown.value === '' ? '' : formatOpacityPct(shown.value);
|
|
17542
|
+
setMixedPlaceholder(input, shown.mixed);
|
|
17456
17543
|
}
|
|
17457
17544
|
|
|
17458
|
-
function updateMeasureControl(input,
|
|
17545
|
+
function updateMeasureControl(input, shown) {
|
|
17546
|
+
var val = shown.value;
|
|
17459
17547
|
input.node().disabled = !controlsEnabled();
|
|
17460
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);
|
|
17461
17560
|
}
|
|
17462
17561
|
|
|
17463
17562
|
// Alignment stays live whatever is selected, including nothing. It was
|
|
@@ -17481,7 +17580,6 @@
|
|
|
17481
17580
|
// to take. The commands ignore a position given for one, and a disabled
|
|
17482
17581
|
// button says so where a console warning would not.
|
|
17483
17582
|
var disabled = !controlsEnabled() || everyLabelIsOnAPath(ids);
|
|
17484
|
-
var locked = !disabled && gridIsLockedToCentre(ids);
|
|
17485
17583
|
// Faintly, and only when no cell is lit: a label carrying offsets is not
|
|
17486
17584
|
// at any of the nine, but one of them is roughly where it is and clicking
|
|
17487
17585
|
// it is the way back.
|
|
@@ -17489,37 +17587,11 @@
|
|
|
17489
17587
|
labelPositions.forEach(function(name) {
|
|
17490
17588
|
posBtns[name].classed('selected', !disabled && name == pos);
|
|
17491
17589
|
posBtns[name].classed('nearest', name == nearest);
|
|
17492
|
-
setPanelButtonDisabled(posBtns[name], disabled
|
|
17590
|
+
setPanelButtonDisabled(posBtns[name], disabled);
|
|
17493
17591
|
});
|
|
17494
17592
|
updateDragModeButtons();
|
|
17495
17593
|
}
|
|
17496
17594
|
|
|
17497
|
-
// The nine positions place text around something, so with nothing drawn at
|
|
17498
|
-
// the anchor the grid is locked to the centre cell: there is no answer to
|
|
17499
|
-
// "north-east of what?", and the centre is what -add-label gives a new label
|
|
17500
|
-
// anyway. The cell stays clickable, because text over its own symbol is a
|
|
17501
|
-
// real thing to ask for.
|
|
17502
|
-
//
|
|
17503
|
-
// The test is whether the label draws a symbol, not whether it has an icon:
|
|
17504
|
-
// a styled dots layer given label text by the point panel's "Create labels"
|
|
17505
|
-
// keeps its r and fill, and keying this on icon= alone would lock the
|
|
17506
|
-
// commonest labels in the app to the middle of their own dots.
|
|
17507
|
-
//
|
|
17508
|
-
// A default and a guard rather than an invariant. A label that arrives
|
|
17509
|
-
// positioned with nothing at its anchor -- from the CLI, from an expression,
|
|
17510
|
-
// from the legacy positioning mode -- is shown as it is, with the grid live:
|
|
17511
|
-
// refusing to display what is in the record is worse than letting an odd
|
|
17512
|
-
// state be edited. Dragging is not gated on a symbol either, since a labels
|
|
17513
|
-
// layer whose dots live in a different layer still needs its text placed.
|
|
17514
|
-
function gridIsLockedToCentre(ids) {
|
|
17515
|
-
var table = getActiveTable();
|
|
17516
|
-
if (!everyTargetLacksASymbol(ids)) return false;
|
|
17517
|
-
if (ids.length === 0) return !labelIsPlaced(getNewLabelStyle(gui));
|
|
17518
|
-
return ids.every(function(id) {
|
|
17519
|
-
return !labelIsPlaced(table && table.getRecordAt(id));
|
|
17520
|
-
});
|
|
17521
|
-
}
|
|
17522
|
-
|
|
17523
17595
|
// Whether a label has been put somewhere other than on top of its anchor: a
|
|
17524
17596
|
// position of its own other than the centre one, or offsets from a drag.
|
|
17525
17597
|
function labelIsPlaced(rec) {
|
|
@@ -17529,16 +17601,6 @@
|
|
|
17529
17601
|
internal.hasStyleValue(rec, 'dy');
|
|
17530
17602
|
}
|
|
17531
17603
|
|
|
17532
|
-
function everyTargetLacksASymbol(ids) {
|
|
17533
|
-
var table = getActiveTable();
|
|
17534
|
-
if (ids.length === 0) {
|
|
17535
|
-
return !internal.featureHasSvgSymbol(getNewLabelStyle(gui));
|
|
17536
|
-
}
|
|
17537
|
-
return ids.every(function(id) {
|
|
17538
|
-
return !internal.featureHasSvgSymbol(table && table.getRecordAt(id));
|
|
17539
|
-
});
|
|
17540
|
-
}
|
|
17541
|
-
|
|
17542
17604
|
// Which of the nine positions a dragged label is nearest, or ''.
|
|
17543
17605
|
//
|
|
17544
17606
|
// Only for a single label: the cell is a hint about where one label sits,
|
|
@@ -17616,8 +17678,9 @@
|
|
|
17616
17678
|
return labelModeIsOn() ? 'new labels' : 'all';
|
|
17617
17679
|
}
|
|
17618
17680
|
|
|
17619
|
-
function updateFontSizeControls(
|
|
17620
|
-
fontSizeInput.setValue(
|
|
17681
|
+
function updateFontSizeControls(shown) {
|
|
17682
|
+
fontSizeInput.setValue(shown.value || '');
|
|
17683
|
+
fontSizeInput.setPlaceholder(shown.mixed ? MIXED_TEXT : '');
|
|
17621
17684
|
fontSizeInput.setDisabled(!controlsEnabled());
|
|
17622
17685
|
}
|
|
17623
17686
|
|
|
@@ -17630,15 +17693,43 @@
|
|
|
17630
17693
|
// here, and a project moves between machines. It is added to the menu as
|
|
17631
17694
|
// itself rather than replaced by an installed font, since the name is the
|
|
17632
17695
|
// user's data and choosing something else for them would restyle the label.
|
|
17633
|
-
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);
|
|
17634
17704
|
fontSelect.node().value = fontVal || '';
|
|
17635
17705
|
if (fontVal && fontSelect.node().selectedIndex < 0) {
|
|
17636
17706
|
El('option').attr('value', fontVal).text(fontVal).appendTo(fontSelect);
|
|
17637
17707
|
fontSelect.node().value = fontVal;
|
|
17638
17708
|
}
|
|
17639
|
-
// Nothing selected, rather than the first font in the list, for a
|
|
17640
|
-
//
|
|
17641
|
-
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
|
+
}
|
|
17642
17733
|
}
|
|
17643
17734
|
|
|
17644
17735
|
// The faces the chosen font is installed with, and no entry for "whatever
|
|
@@ -17658,12 +17749,13 @@
|
|
|
17658
17749
|
El('option').attr('value', variant.value).appendTo(fontStyleSelect).text(variant.label);
|
|
17659
17750
|
});
|
|
17660
17751
|
}
|
|
17752
|
+
// The faces are rebuilt above, so the "mixed" entry goes on afterwards.
|
|
17753
|
+
setMixedOption(fontStyleSelect, mixed);
|
|
17661
17754
|
fontStyleSelect.node().disabled = disabled;
|
|
17662
17755
|
fontStyleSelect.node().value = shown ? shown.value : '';
|
|
17663
|
-
// Nothing selected rather than the first face, for a
|
|
17664
|
-
//
|
|
17665
|
-
|
|
17666
|
-
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;
|
|
17667
17759
|
}
|
|
17668
17760
|
|
|
17669
17761
|
// The face the panel shows for a label in @fontName. A font-style and
|
|
@@ -17674,12 +17766,17 @@
|
|
|
17674
17766
|
return getNearestFontStyleVariant(fontName, fontStyleVal, fontWeightVal);
|
|
17675
17767
|
}
|
|
17676
17768
|
|
|
17677
|
-
function updateColorControls(
|
|
17769
|
+
function updateColorControls(shown) {
|
|
17770
|
+
var colorVal = shown.value;
|
|
17678
17771
|
var disabled = !controlsEnabled();
|
|
17679
17772
|
colorInput.node().disabled = disabled;
|
|
17680
17773
|
colorInput.node().value = colorVal || '';
|
|
17774
|
+
setMixedPlaceholder(colorInput, shown.mixed);
|
|
17681
17775
|
colorFieldBox.classed('disabled', disabled);
|
|
17682
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);
|
|
17683
17780
|
colorChit.css('background-color', isHexColor(colorVal) ? colorVal : 'transparent');
|
|
17684
17781
|
if (colorPicker.visible()) {
|
|
17685
17782
|
return; // avoid HSB -> RGB -> HSB rounding jumps after picker commits
|
|
@@ -17691,50 +17788,59 @@
|
|
|
17691
17788
|
}
|
|
17692
17789
|
}
|
|
17693
17790
|
|
|
17694
|
-
function updateCssControl(
|
|
17791
|
+
function updateCssControl(shown) {
|
|
17695
17792
|
cssInput.node().disabled = !controlsEnabled();
|
|
17696
|
-
cssInput.node().value =
|
|
17793
|
+
cssInput.node().value = shown.value || '';
|
|
17794
|
+
setMixedPlaceholder(cssInput, shown.mixed);
|
|
17697
17795
|
}
|
|
17698
17796
|
|
|
17699
17797
|
// The toggle reads the data rather than holding a state of its own: a symbol
|
|
17700
17798
|
// is on when the target has one, which is what makes it follow an undo.
|
|
17701
17799
|
//
|
|
17702
|
-
// A selection where only some labels have a symbol
|
|
17703
|
-
// section stays usable
|
|
17704
|
-
//
|
|
17705
|
-
//
|
|
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.
|
|
17706
17807
|
function updateIconControls(iconVal) {
|
|
17707
17808
|
var enabled = controlsEnabled();
|
|
17708
|
-
var
|
|
17809
|
+
var state = enabled ? getIconState() : 'off';
|
|
17810
|
+
var off = state == 'off';
|
|
17709
17811
|
if (iconVal) lastIconShape = iconVal;
|
|
17710
|
-
iconToggle.setState(
|
|
17812
|
+
iconToggle.setState(state);
|
|
17711
17813
|
iconToggle.setDisabled(!enabled);
|
|
17712
17814
|
// The group is faded as a whole rather than button by button, so that the
|
|
17713
17815
|
// border the buttons share fades with them -- a live border around dead
|
|
17714
17816
|
// buttons is the one part of a disabled control that still looks usable.
|
|
17715
|
-
iconGroupEl.classed('disabled',
|
|
17817
|
+
iconGroupEl.classed('disabled', off);
|
|
17716
17818
|
iconTypes.forEach(function(icon) {
|
|
17717
|
-
iconBtns[icon.name].classed('selected',
|
|
17718
|
-
setPanelButtonDisabled(iconBtns[icon.name],
|
|
17819
|
+
iconBtns[icon.name].classed('selected', !off && icon.name == iconVal);
|
|
17820
|
+
setPanelButtonDisabled(iconBtns[icon.name], off);
|
|
17719
17821
|
});
|
|
17720
|
-
return
|
|
17822
|
+
return off;
|
|
17721
17823
|
}
|
|
17722
17824
|
|
|
17723
17825
|
function setIconOn(on) {
|
|
17724
17826
|
applyIcon(on ? lastIconShape : '');
|
|
17725
17827
|
}
|
|
17726
17828
|
|
|
17727
|
-
function updateIconSizeControls(
|
|
17728
|
-
iconSizeInput.setValue(
|
|
17829
|
+
function updateIconSizeControls(shown, iconOff) {
|
|
17830
|
+
iconSizeInput.setValue(shown.value || '');
|
|
17831
|
+
iconSizeInput.setPlaceholder(shown.mixed ? MIXED_TEXT : '');
|
|
17729
17832
|
iconSizeInput.setDisabled(iconOff || !controlsEnabled());
|
|
17730
17833
|
}
|
|
17731
17834
|
|
|
17732
|
-
function updateIconColorControls(
|
|
17835
|
+
function updateIconColorControls(shown, iconOff) {
|
|
17836
|
+
var colorVal = shown.value;
|
|
17733
17837
|
var disabled = iconOff || !controlsEnabled();
|
|
17734
17838
|
iconColorInput.node().disabled = disabled;
|
|
17735
17839
|
iconColorInput.node().value = colorVal || '';
|
|
17840
|
+
setMixedPlaceholder(iconColorInput, shown.mixed);
|
|
17736
17841
|
iconColorFieldBox.classed('disabled', disabled);
|
|
17737
17842
|
setPanelButtonDisabled(iconColorChit, disabled);
|
|
17843
|
+
iconColorChit.classed('mixed', shown.mixed);
|
|
17738
17844
|
iconColorChit.css('background-color', isHexColor(colorVal) ? colorVal : 'transparent');
|
|
17739
17845
|
if (iconColorPicker.visible()) {
|
|
17740
17846
|
return; // avoid HSB -> RGB -> HSB rounding jumps after picker commits
|
|
@@ -17751,29 +17857,57 @@
|
|
|
17751
17857
|
}
|
|
17752
17858
|
|
|
17753
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) {
|
|
17754
17874
|
var table = getActiveTable();
|
|
17755
17875
|
var records = table && table.getRecords();
|
|
17756
17876
|
var value, val, hasValue;
|
|
17757
|
-
if (ids.length === 0)
|
|
17758
|
-
|
|
17877
|
+
if (ids.length === 0) {
|
|
17878
|
+
return {value: getNewLabelValue(field, opts), mixed: false};
|
|
17879
|
+
}
|
|
17880
|
+
if (!records) return {value: '', mixed: false};
|
|
17759
17881
|
for (var i=0; i<ids.length; i++) {
|
|
17760
17882
|
val = records[ids[i]] && records[ids[i]][field];
|
|
17761
17883
|
if (!val) {
|
|
17762
|
-
|
|
17763
|
-
|
|
17764
|
-
|
|
17765
|
-
|
|
17766
|
-
|
|
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 : '';
|
|
17767
17891
|
} else {
|
|
17768
17892
|
hasValue = true;
|
|
17769
17893
|
}
|
|
17770
17894
|
if (i === 0) {
|
|
17771
17895
|
value = val;
|
|
17772
17896
|
} else if (val != value) {
|
|
17773
|
-
return '';
|
|
17897
|
+
return {value: '', mixed: true};
|
|
17774
17898
|
}
|
|
17775
17899
|
}
|
|
17776
|
-
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);
|
|
17777
17911
|
}
|
|
17778
17912
|
|
|
17779
17913
|
// The alignment the labels are *drawn* with, rather than the one they carry.
|
|
@@ -18002,7 +18136,11 @@
|
|
|
18002
18136
|
var ids = getTargetIds();
|
|
18003
18137
|
var styles = [[iconField, iconName || '']];
|
|
18004
18138
|
if (iconName) {
|
|
18005
|
-
|
|
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)]);
|
|
18006
18144
|
// The symbol's own opacity goes on with it, because the label's opacity
|
|
18007
18145
|
// is applied to both elements: without this, text set to 50% would give
|
|
18008
18146
|
// a half-faded symbol while the Icon section showed it at 100%.
|
|
@@ -18014,25 +18152,21 @@
|
|
|
18014
18152
|
applyStyleValues(styles);
|
|
18015
18153
|
}
|
|
18016
18154
|
|
|
18017
|
-
// Switching a symbol on
|
|
18018
|
-
//
|
|
18019
|
-
//
|
|
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.
|
|
18020
18158
|
//
|
|
18021
|
-
//
|
|
18022
|
-
//
|
|
18023
|
-
//
|
|
18024
|
-
//
|
|
18025
|
-
// destructive, since a hand-placed label loses its placement; being one
|
|
18026
|
-
// 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.
|
|
18027
18163
|
//
|
|
18028
18164
|
// Not for path labels, whose text runs along a curve: the commands ignore a
|
|
18029
18165
|
// position given for one and warn about it, and a warning from switching a
|
|
18030
18166
|
// symbol on would be about something the user did not ask for.
|
|
18031
18167
|
function addIconPositionChange(styles, ids, iconOn) {
|
|
18032
|
-
if (anyLabelIsOnAPath(ids)) return;
|
|
18033
|
-
if (
|
|
18034
|
-
styles.push(['label-pos', 'c']);
|
|
18035
|
-
} else if (everyTargetIsCentred(ids)) {
|
|
18168
|
+
if (anyLabelIsOnAPath(ids) || !iconOn) return;
|
|
18169
|
+
if (everyTargetIsCentred(ids)) {
|
|
18036
18170
|
styles.push(['label-pos', labelPositionBesideIcon]);
|
|
18037
18171
|
}
|
|
18038
18172
|
}
|
|
@@ -18046,45 +18180,74 @@
|
|
|
18046
18180
|
}
|
|
18047
18181
|
|
|
18048
18182
|
function applyIconSize(value) {
|
|
18049
|
-
applyStyleValues([[iconSizeField, value]]);
|
|
18183
|
+
applyStyleValues([[iconSizeField, value]], getIconTargetIds());
|
|
18050
18184
|
}
|
|
18051
18185
|
|
|
18052
18186
|
function applyIconColor(color) {
|
|
18053
|
-
applyStyleValues([[iconColorField, color]]);
|
|
18187
|
+
applyStyleValues([[iconColorField, color]], getIconTargetIds());
|
|
18054
18188
|
}
|
|
18055
18189
|
|
|
18056
18190
|
function applyIconOpacity(value) {
|
|
18057
|
-
applyStyleValues([[iconOpacityField, value]]);
|
|
18191
|
+
applyStyleValues([[iconOpacityField, value]], getIconTargetIds());
|
|
18058
18192
|
}
|
|
18059
18193
|
|
|
18060
18194
|
function nudgeIconSize(delta) {
|
|
18061
|
-
var
|
|
18062
|
-
var size = getNumericSize(ids, iconSizeField, defaultIconSize);
|
|
18195
|
+
var size = getNumericSize(getIconTargetIds(), iconSizeField, defaultIconSize);
|
|
18063
18196
|
if (!controlsEnabled()) return;
|
|
18064
18197
|
size = Math.max(1, size + delta);
|
|
18065
18198
|
applyIconSize(size);
|
|
18066
18199
|
}
|
|
18067
18200
|
|
|
18201
|
+
// Whether the target labels have a symbol: 'on', 'off', or 'mixed' when only
|
|
18202
|
+
// some of them do.
|
|
18203
|
+
//
|
|
18068
18204
|
// An icon-size or icon-color on a label with no symbol draws nothing, which
|
|
18069
18205
|
// is why the shape, size and colour controls are inert until the toggle puts
|
|
18070
18206
|
// a symbol there to style.
|
|
18071
|
-
function
|
|
18207
|
+
function getIconState() {
|
|
18072
18208
|
var ids = getTargetIds();
|
|
18073
18209
|
var table = getActiveTable();
|
|
18074
|
-
if (ids.length === 0) return
|
|
18075
|
-
return ids.
|
|
18210
|
+
if (ids.length === 0) return getNewLabelValue(iconField) ? 'on' : 'off';
|
|
18211
|
+
return getToggleState(ids.map(function(id) {
|
|
18212
|
+
var rec = table && table.getRecordAt(id);
|
|
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) {
|
|
18076
18229
|
var rec = table && table.getRecordAt(id);
|
|
18077
|
-
return
|
|
18230
|
+
return !!(rec && rec[iconField]);
|
|
18078
18231
|
});
|
|
18079
18232
|
}
|
|
18080
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
|
+
|
|
18081
18244
|
function getIconOpacityToWrite() {
|
|
18082
18245
|
var val = parseOpacityValue(iconOpacityInput.node().value);
|
|
18083
18246
|
if (val !== null) return val;
|
|
18084
18247
|
// The field reads blank while the section is off, so a symbol switched off
|
|
18085
18248
|
// and on again takes back the fade still stored on the label rather than
|
|
18086
18249
|
// being reset to full.
|
|
18087
|
-
var stored = getCommonValue(
|
|
18250
|
+
var stored = getCommonValue(getIconValueIds(), iconOpacityField, {useDefault: true, defaultValue: 1});
|
|
18088
18251
|
val = stored === '' ? 1 : Number(stored);
|
|
18089
18252
|
return isFinite(val) ? val : 1;
|
|
18090
18253
|
}
|
|
@@ -18137,7 +18300,10 @@
|
|
|
18137
18300
|
}
|
|
18138
18301
|
}
|
|
18139
18302
|
|
|
18140
|
-
|
|
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();
|
|
18141
18307
|
if (styles.length === 0) return;
|
|
18142
18308
|
releaseFocus();
|
|
18143
18309
|
// What the panel is set to is always what the next label gets, whether or
|
|
@@ -18150,8 +18316,8 @@
|
|
|
18150
18316
|
// style, so it has to be redrawn to show the change -- it is the preview.
|
|
18151
18317
|
refreshPendingLabel();
|
|
18152
18318
|
}
|
|
18153
|
-
if (
|
|
18154
|
-
applyStyleCommand(styles);
|
|
18319
|
+
if (ids.length > 0) {
|
|
18320
|
+
applyStyleCommand(styles, ids);
|
|
18155
18321
|
return;
|
|
18156
18322
|
}
|
|
18157
18323
|
if (!labelModeIsOn()) return;
|
|
@@ -18163,9 +18329,8 @@
|
|
|
18163
18329
|
if (session && session.id == -1 && session.refresh) session.refresh();
|
|
18164
18330
|
}
|
|
18165
18331
|
|
|
18166
|
-
function applyStyleCommand(styles) {
|
|
18332
|
+
function applyStyleCommand(styles, ids) {
|
|
18167
18333
|
var lyr = getActiveLayer();
|
|
18168
|
-
var ids = getTargetIds();
|
|
18169
18334
|
var parts = ['-style'];
|
|
18170
18335
|
if (!gui.console || !lyr) return;
|
|
18171
18336
|
styles.forEach(function(style) {
|
|
@@ -25332,6 +25497,9 @@
|
|
|
25332
25497
|
// See docs/development/label-tool-design.md.
|
|
25333
25498
|
|
|
25334
25499
|
var SVG_NS$2 = 'http://www.w3.org/2000/svg';
|
|
25500
|
+
// How far outside its text a label's outline is drawn. Exported because the
|
|
25501
|
+
// tool grabs a label by the same box: what looks like the object is what takes
|
|
25502
|
+
// a drag on it.
|
|
25335
25503
|
var BOX_PADDING = 3;
|
|
25336
25504
|
var ANCHOR_RADIUS = 3.5;
|
|
25337
25505
|
var KNOT_RADIUS = 3;
|
|
@@ -25343,6 +25511,13 @@
|
|
|
25343
25511
|
// layer instead of rebuilding it, and the cue moves with the map by wearing the
|
|
25344
25512
|
// label's own transform -- but a select-all on a big layer would still pay it,
|
|
25345
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.
|
|
25346
25521
|
var MAX_OUTLINES = 200;
|
|
25347
25522
|
|
|
25348
25523
|
// getEditingId: returns the feature id of an open text editing session, or -1.
|
|
@@ -25350,6 +25525,7 @@
|
|
|
25350
25525
|
function LabelSelection(gui, ext, hit, getEditingId) {
|
|
25351
25526
|
var self = {};
|
|
25352
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
|
|
25353
25529
|
var drawn = null; // what those cues represent, so hover does not redraw them
|
|
25354
25530
|
var on = false;
|
|
25355
25531
|
var tetherId = -1; // the label whose text is being dragged off its anchor
|
|
@@ -25379,9 +25555,11 @@
|
|
|
25379
25555
|
// markup and takes the old cues with it.
|
|
25380
25556
|
self.refresh = function(force) {
|
|
25381
25557
|
var target = hit.getHitTarget();
|
|
25382
|
-
var
|
|
25383
|
-
var
|
|
25384
|
-
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;
|
|
25385
25563
|
// Hover fires on every pointer move, and most of them change nothing here.
|
|
25386
25564
|
if (!force && drawn === key) return;
|
|
25387
25565
|
clearAll();
|
|
@@ -25396,6 +25574,9 @@
|
|
|
25396
25574
|
// would offer handles that cannot be grabbed.
|
|
25397
25575
|
draw(target, id, 'label-cue-selected', true);
|
|
25398
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);
|
|
25399
25580
|
};
|
|
25400
25581
|
|
|
25401
25582
|
// The label under the pointer, when showing it would say something: not one
|
|
@@ -25407,10 +25588,23 @@
|
|
|
25407
25588
|
return id;
|
|
25408
25589
|
}
|
|
25409
25590
|
|
|
25410
|
-
//
|
|
25411
|
-
|
|
25412
|
-
|
|
25413
|
-
|
|
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 = [];
|
|
25414
25608
|
}
|
|
25415
25609
|
|
|
25416
25610
|
function draw(target, id, className, withHandles) {
|
|
@@ -25484,7 +25678,8 @@
|
|
|
25484
25678
|
var content;
|
|
25485
25679
|
if (!text) return null;
|
|
25486
25680
|
content = text.querySelector('textPath') || text;
|
|
25487
|
-
return {symbol: symbol,
|
|
25681
|
+
return {symbol: symbol, text: text, content: content,
|
|
25682
|
+
pathId: getPathId(content)};
|
|
25488
25683
|
}
|
|
25489
25684
|
|
|
25490
25685
|
// The id of the baseline a path label is laid along, or null for an anchored
|
|
@@ -25581,12 +25776,183 @@
|
|
|
25581
25776
|
if (g.parentNode) g.parentNode.removeChild(g);
|
|
25582
25777
|
});
|
|
25583
25778
|
groups = [];
|
|
25779
|
+
clearMarks();
|
|
25584
25780
|
drawn = null;
|
|
25585
25781
|
}
|
|
25586
25782
|
|
|
25587
25783
|
return self;
|
|
25588
25784
|
}
|
|
25589
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
|
+
|
|
25590
25956
|
// State of a label curve being drawn, and the operations the curvature tool
|
|
25591
25957
|
// performs on it.
|
|
25592
25958
|
//
|
|
@@ -25986,6 +26352,7 @@
|
|
|
25986
26352
|
if (!active()) return;
|
|
25987
26353
|
id = getRightClickedLabel(e);
|
|
25988
26354
|
if (target && id > -1) {
|
|
26355
|
+
e.selectActions = getSelectActions(target, id);
|
|
25989
26356
|
e.deleteFeature = getDeleteAction(target, id);
|
|
25990
26357
|
// Nobody guesses that text is dragged across its own curve, and there is
|
|
25991
26358
|
// no bracket drawn to suggest it, so the gesture has a second way in.
|
|
@@ -26014,6 +26381,43 @@
|
|
|
26014
26381
|
return -1;
|
|
26015
26382
|
}
|
|
26016
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
|
+
|
|
26017
26421
|
// Deleting a label ends whatever was being done to it first: its text
|
|
26018
26422
|
// session would otherwise write the text back to an id that now belongs to
|
|
26019
26423
|
// the label that moved up into the gap, and the same shift is why the
|
|
@@ -26524,7 +26928,10 @@
|
|
|
26524
26928
|
// ended.
|
|
26525
26929
|
setMultilineAttribute(nodes.text, 'x', o.values.x);
|
|
26526
26930
|
nodes.text.setAttribute('y', o.values.dy);
|
|
26527
|
-
|
|
26931
|
+
// The anchor the label is drawn with, not the one the drag writes: on an
|
|
26932
|
+
// aligned label the two differ, and the stored one belongs with the stored
|
|
26933
|
+
// dx rather than with the x being previewed here.
|
|
26934
|
+
nodes.text.setAttribute('text-anchor', o.values.anchor);
|
|
26528
26935
|
// The cue is drawn around the text rather than moved with it, so it has to
|
|
26529
26936
|
// be rebuilt to follow -- one getBBox on one label per mouse move.
|
|
26530
26937
|
selection.refresh(true);
|
|
@@ -26679,9 +27086,34 @@
|
|
|
26679
27086
|
hoverTextId = id > -1 && (!hoverHandle || glyphsOutrankHandle(id)) ? id : -1;
|
|
26680
27087
|
}
|
|
26681
27088
|
|
|
27089
|
+
// The exception is about the pointer being on the glyphs, not about the mode:
|
|
27090
|
+
// an offset label's anchor is out from under its text, and taking the handle
|
|
27091
|
+
// away there left it with no anchor handle at all -- dragging the symbol of a
|
|
27092
|
+
// label positioned ne slid its text instead of moving the label.
|
|
26682
27093
|
function glyphsOutrankHandle(id) {
|
|
26683
27094
|
return hoverHandle.id == id && labelTextIsDraggable(gui) &&
|
|
26684
|
-
!isPathLabel(hit.getHitTarget(), id);
|
|
27095
|
+
!isPathLabel(hit.getHitTarget(), id) && pointerIsOverText(id);
|
|
27096
|
+
}
|
|
27097
|
+
|
|
27098
|
+
// Whether the pointer is within the box drawn around a label's text -- the
|
|
27099
|
+
// outline the selection cue draws, padding and all, so that the thing on
|
|
27100
|
+
// screen that says "this is the object" is the thing that takes the drag.
|
|
27101
|
+
//
|
|
27102
|
+
// From the last hover rather than from an event, because this is asked from
|
|
27103
|
+
// the hit control's 'change' as well, which arrives without a position; the
|
|
27104
|
+
// pointer has not moved since the hover that preceded it.
|
|
27105
|
+
function pointerIsOverText(id) {
|
|
27106
|
+
var target = hit.getHitTarget();
|
|
27107
|
+
var shapes = target && getDisplayShapes(target);
|
|
27108
|
+
var shp = shapes && shapes[id];
|
|
27109
|
+
var nodes = findLabelNodes(target, id);
|
|
27110
|
+
var box = nodes && !nodes.textPath ? measureNode(nodes.text) : null;
|
|
27111
|
+
var pix = hoverPoint && ext.translateCoords(hoverPoint[0], hoverPoint[1]);
|
|
27112
|
+
var p;
|
|
27113
|
+
if (!box || !pix || !shp) return false;
|
|
27114
|
+
p = getLabelSpacePoint(shp[0], {x: pix[0], y: pix[1]});
|
|
27115
|
+
return p.x >= box.x - BOX_PADDING && p.x <= box.x + box.width + BOX_PADDING &&
|
|
27116
|
+
p.y >= box.y - BOX_PADDING && p.y <= box.y + box.height + BOX_PADDING;
|
|
26685
27117
|
}
|
|
26686
27118
|
|
|
26687
27119
|
// The label a drag on the glyphs would act on, or -1. Only a selected label
|
|
@@ -26761,8 +27193,17 @@
|
|
|
26761
27193
|
gui.keyboard.on('keydown', function(e) {
|
|
26762
27194
|
if (!active()) return;
|
|
26763
27195
|
// While a session is open the textarea has focus and handles its own keys;
|
|
26764
|
-
// 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.
|
|
26765
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
|
+
}
|
|
26766
27207
|
if (e.keyName == 'esc') {
|
|
26767
27208
|
// One rung per press, innermost first: the curve being drawn, then the
|
|
26768
27209
|
// selection, then the armed tool. The hit control's own escape handler
|
|
@@ -26793,6 +27234,16 @@
|
|
|
26793
27234
|
}
|
|
26794
27235
|
}, null, 10);
|
|
26795
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
|
+
|
|
26796
27247
|
// Suppresses the key's default action as well as the rest of the GUI's
|
|
26797
27248
|
// handlers. The default matters here: finishing a curve opens an editing
|
|
26798
27249
|
// session and focuses its textarea, and without this the same Enter goes on
|
|
@@ -30801,8 +31252,7 @@
|
|
|
30801
31252
|
var mapLayers = gui.container.findChild('.map-layers').node();
|
|
30802
31253
|
var map = gui.container.findChild('.mshp-main-map');
|
|
30803
31254
|
var toggle = gui.buttons.addButton('#frame-tool-icon')
|
|
30804
|
-
.addClass('menu-btn preview-toggle')
|
|
30805
|
-
.attr('title', 'Toggle map preview');
|
|
31255
|
+
.addClass('menu-btn preview-toggle');
|
|
30806
31256
|
var readout = El('div')
|
|
30807
31257
|
.addClass('preview-readout')
|
|
30808
31258
|
.appendTo(map)
|
|
@@ -30922,7 +31372,11 @@
|
|
|
30922
31372
|
var available = hasFrame();
|
|
30923
31373
|
toggle.removeClass('disabled');
|
|
30924
31374
|
toggle.classed('selected', available && self.isOn());
|
|
30925
|
-
|
|
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);
|
|
30926
31380
|
toggle.attr('aria-disabled', 'false');
|
|
30927
31381
|
toggle.attr('aria-pressed', available && self.isOn() ? 'true' : 'false');
|
|
30928
31382
|
if (gui.map.isPreviewView()) {
|