├── .eslintignore
├── .eslintrc
├── LICENSE
├── README.md
├── dist
├── mapbox-gl-layers.css
└── mapbox-gl-layers.js
├── example.html
├── index.js
└── package.json
/.eslintignore:
--------------------------------------------------------------------------------
1 | dist
2 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["standard"],
3 | "env": {
4 | "node": true,
5 | "es6": true,
6 | "browser": true
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Development Seed
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://circleci.com/gh/developmentseed/mapbox-gl-layers)
2 |
3 | # mapbox-gl-layers
4 |
5 | Layer toggle for [Mapbox GL JS](https://www.mapbox.com/mapbox-gl-js/)
6 |
7 | ## Install
8 |
9 | `npm install mapbox-gl-layers`
10 |
11 | (Note the peer dependency on `mapbox-gl`!)
12 |
13 | ## Use
14 |
15 | ### CommonJS
16 |
17 | ```js
18 | var Layers = require('mapbox-gl-layers')
19 |
20 | new Layers({
21 | layers: {
22 | 'ALL PARKS': ['national_park', 'parks'],
23 | 'National Parks': 'national_park',
24 | 'Other Parks': 'parks'
25 | }
26 | }).addTo(map) // map is the mapbox gl map instance
27 | ```
28 |
29 | ### Standalone script
30 |
31 | Add to `
`:
32 |
33 | ```html
34 |
35 |
36 | ```
37 |
38 | And then:
39 |
40 | ```html
41 |
52 | ```
53 |
54 | ## API
55 |
56 | ### Layers
57 |
58 | Creates a layer toggle control
59 |
60 | **Parameters**
61 |
62 | - `options` **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)=**
63 | - `options.type` **[string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)=** Selection type: `multiple` to allow independently toggling each layer/group, `single` to only choose one at a time. (optional, default `'multiple'`)
64 | - `options.layers` **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)=** An object determining which layers to include. Each key is a display name (what's shown in the UI), and each value is the corresponding layer id in the map style (or an array of layer ids).
65 | - `options.position` **[string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)=** A string indicating position on the map. Options are `top-right`, `top-left`, `bottom-right`, `bottom-left`. (optional, default `'top-right'`)
66 | - `options.onChange` **[function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function)=** Optional callback called with `{name: dispayName, layerIds: [...], active: true|false }` for the clicked layer
67 |
68 | **Examples**
69 |
70 | ```javascript
71 | (new Layers({ 'National Parks': 'national_park', 'Other Parks': 'parks' }))
72 | .addTo(map)
73 | ```
74 |
75 | ## Contributing
76 |
77 | This is an [OPEN open source](http://openopensource.org/) project.
78 | Contributions are welcome!
79 |
80 | Steps:
81 |
82 | 1. Clone the repo and run `npm install`.
83 | 2. Start test server with `npm start`, open ,
84 | and start make changes to `index.js` and friends.
85 |
--------------------------------------------------------------------------------
/dist/mapbox-gl-layers.css:
--------------------------------------------------------------------------------
1 | .mapboxgl-layers {
2 | max-height: 100vh;
3 | overflow: scroll;
4 | }
5 | .mapboxgl-layers ul {
6 | background: #fff;
7 | margin: 0;
8 | padding: 10px;
9 | border-radius: 4px;
10 | list-style-type: none;
11 | }
12 |
13 | .mapboxgl-layers li {
14 | cursor: pointer;
15 | position: relative;
16 | margin: 0 5px;
17 | }
18 | .mapboxgl-layers li.active:before {
19 | position: absolute;
20 | right: 100%;
21 | content: '\2713 ';
22 | }
23 | .mapboxgl-layers li.partially-active:before {
24 | color: #ccc;
25 | }
26 |
27 |
--------------------------------------------------------------------------------
/dist/mapbox-gl-layers.js:
--------------------------------------------------------------------------------
1 | (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.MapboxGLLayers = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o layer.id)
43 | if (!this.options.layers) {
44 | this.options.layers = {}
45 |
46 | // if there's Mapbox Studio metadata available, use any groups we can find
47 | var groups = {}
48 | if (style.metadata && style.metadata['mapbox:groups']) {
49 | groups = style.metadata['mapbox:groups']
50 | Object.keys(groups).forEach((g) => { this.options.layers[groups[g].name] = [] })
51 | }
52 |
53 | style.layers.forEach((layer) => {
54 | var group = layer.metadata ? groups[layer.metadata['mapbox:group']] : null
55 | if (layer.metadata && group) {
56 | this.options.layers[group.name].push(layer.id)
57 | } else {
58 | this.options.layers[layer.id] = [layer.id]
59 | }
60 | })
61 | }
62 | this._map.on('style.change', this._update)
63 | this._map.style.on('layer.remove', this._update)
64 | this._map.style.on('layer.add', this._update)
65 | return this._render()
66 | }
67 |
68 | Layers.prototype.onRemove = function onRemove () {
69 | this._map.off('style.change', this._update)
70 | this._map.style.off('layer.remove', this._update)
71 | this._map.style.off('layer.add', this._update)
72 | }
73 |
74 | Layers.prototype._update = function _update () {
75 | this._allLayers = this._map.getStyle().layers.map((layer) => layer.id)
76 | yo.update(this._container, this._render())
77 | }
78 | Layers.prototype._render = function _render () {
79 | var layers = this.options.layers
80 | var className = 'mapboxgl-ctrl mapboxgl-layers'
81 | return (function () {
82 | function appendChild (el, childs) {
83 | for (var i = 0; i < childs.length; i++) {
84 | var node = childs[i];
85 | if (Array.isArray(node)) {
86 | appendChild(el, node)
87 | continue
88 | }
89 | if (typeof node === "number" ||
90 | typeof node === "boolean" ||
91 | node instanceof Date ||
92 | node instanceof RegExp) {
93 | node = node.toString()
94 | }
95 |
96 | if (typeof node === "string") {
97 | if (el.lastChild && el.lastChild.nodeName === "#text") {
98 | el.lastChild.nodeValue += node
99 | continue
100 | }
101 | node = document.createTextNode(node)
102 | }
103 |
104 | if (node && node.nodeType) {
105 | el.appendChild(node)
106 | }
107 | }
108 | }
109 | var bel1 = document.createElement("div")
110 | bel1.setAttribute("class", arguments[1])
111 | var bel0 = document.createElement("ul")
112 | appendChild(bel0, ["\n ",arguments[0],"\n "])
113 | appendChild(bel1, ["\n ",bel0,"\n "])
114 | return bel1
115 | }(Object.keys(layers)
116 | .filter((name) => layers[name].some(this._layerExists))
117 | .map((name) => {
118 | var ids = layers[name].filter(this._layerExists)
119 | var className = ids.every(this._isActive) ? 'active'
120 | : ids.some(this._isActive) ? 'active partially-active'
121 | : ''
122 | return (function () {
123 | function appendChild (el, childs) {
124 | for (var i = 0; i < childs.length; i++) {
125 | var node = childs[i];
126 | if (Array.isArray(node)) {
127 | appendChild(el, node)
128 | continue
129 | }
130 | if (typeof node === "number" ||
131 | typeof node === "boolean" ||
132 | node instanceof Date ||
133 | node instanceof RegExp) {
134 | node = node.toString()
135 | }
136 |
137 | if (typeof node === "string") {
138 | if (el.lastChild && el.lastChild.nodeName === "#text") {
139 | el.lastChild.nodeValue += node
140 | continue
141 | }
142 | node = document.createTextNode(node)
143 | }
144 |
145 | if (node && node.nodeType) {
146 | el.appendChild(node)
147 | }
148 | }
149 | }
150 | var bel0 = document.createElement("li")
151 | bel0.setAttribute("data-layer-name", arguments[0])
152 | bel0.setAttribute("data-layer-id", arguments[1])
153 | bel0["onclick"] = arguments[2]
154 | bel0.setAttribute("class", arguments[3])
155 | appendChild(bel0, ["\n ",arguments[4],"\n "])
156 | return bel0
157 | }(name,ids.join(','),this._onClick,className,name))
158 | }),className))
159 | }
160 |
161 | Layers.prototype._onClick = function _onClick (e) {
162 | var ids = e.currentTarget.getAttribute('data-layer-id').split(',')
163 | .filter(this._layerExists)
164 |
165 | var activated = false
166 | if (this.options.type === 'single') {
167 | // single selection mode
168 | if (this._currentSelection) {
169 | this._currentSelection.forEach((id) => {
170 | this._map.setLayoutProperty(id, 'visibility', 'none')
171 | })
172 | }
173 | // turn on any layer that IS in the selected group
174 | ids.forEach((id) => {
175 | this._map.setLayoutProperty(id, 'visibility', 'visible')
176 | })
177 | this._currentSelection = ids
178 | activated = true
179 | } else {
180 | // 'toggle' mode
181 | var visibility = ids.some(this._isActive) ? 'none' : 'visible'
182 | ids.forEach((id) => {
183 | this._map.setLayoutProperty(id, 'visibility', visibility)
184 | })
185 | activated = visibility === 'visible'
186 | }
187 |
188 | if (this.options.onChange) {
189 | this.options.onChange({
190 | name: e.currentTarget.getAttribute('data-layer-name'),
191 | layerIds: ids,
192 | active: activated
193 | })
194 | }
195 | }
196 |
197 | Layers.prototype._isActive = function isActive (id) {
198 | return this._map.getLayoutProperty(id, 'visibility') === 'visible'
199 | }
200 |
201 | Layers.prototype._layerExists = function (id) {
202 | return this._allLayers.indexOf(id) >= 0
203 | }
204 |
205 |
206 | },{"mapbox-gl/js/ui/control/control":2,"yo-yo":4}],2:[function(require,module,exports){
207 | 'use strict';
208 |
209 | module.exports = Control;
210 |
211 | /**
212 | * A base class for map-related interface elements.
213 | *
214 | * @class Control
215 | */
216 | function Control() {}
217 |
218 | Control.prototype = {
219 | /**
220 | * Add this control to the map, returning the control itself
221 | * for chaining. This will insert the control's DOM element into
222 | * the map's DOM element if the control has a `position` specified.
223 | *
224 | * @param {Map} map
225 | * @returns {Control} `this`
226 | */
227 | addTo: function(map) {
228 | this._map = map;
229 | var container = this._container = this.onAdd(map);
230 | if (this.options && this.options.position) {
231 | var pos = this.options.position;
232 | var corner = map._controlCorners[pos];
233 | container.className += ' mapboxgl-ctrl';
234 | if (pos.indexOf('bottom') !== -1) {
235 | corner.insertBefore(container, corner.firstChild);
236 | } else {
237 | corner.appendChild(container);
238 | }
239 | }
240 |
241 | return this;
242 | },
243 |
244 | /**
245 | * Remove this control from the map it has been added to.
246 | *
247 | * @returns {Control} `this`
248 | */
249 | remove: function() {
250 | this._container.parentNode.removeChild(this._container);
251 | if (this.onRemove) this.onRemove(this._map);
252 | this._map = null;
253 | return this;
254 | }
255 | };
256 |
257 | },{}],3:[function(require,module,exports){
258 | // Create a range object for efficently rendering strings to elements.
259 | var range;
260 |
261 | var testEl = typeof document !== 'undefined' ? document.body || document.createElement('div') : {};
262 |
263 | // Fixes https://github.com/patrick-steele-idem/morphdom/issues/32 (IE7+ support)
264 | // <=IE7 does not support el.hasAttribute(name)
265 | var hasAttribute;
266 | if (testEl.hasAttribute) {
267 | hasAttribute = function hasAttribute(el, name) {
268 | return el.hasAttribute(name);
269 | };
270 | } else {
271 | hasAttribute = function hasAttribute(el, name) {
272 | return el.getAttributeNode(name);
273 | };
274 | }
275 |
276 | function empty(o) {
277 | for (var k in o) {
278 | if (o.hasOwnProperty(k)) {
279 | return false;
280 | }
281 | }
282 |
283 | return true;
284 | }
285 | function toElement(str) {
286 | if (!range && document.createRange) {
287 | range = document.createRange();
288 | range.selectNode(document.body);
289 | }
290 |
291 | var fragment;
292 | if (range && range.createContextualFragment) {
293 | fragment = range.createContextualFragment(str);
294 | } else {
295 | fragment = document.createElement('body');
296 | fragment.innerHTML = str;
297 | }
298 | return fragment.childNodes[0];
299 | }
300 |
301 | var specialElHandlers = {
302 | /**
303 | * Needed for IE. Apparently IE doesn't think
304 | * that "selected" is an attribute when reading
305 | * over the attributes using selectEl.attributes
306 | */
307 | OPTION: function(fromEl, toEl) {
308 | if ((fromEl.selected = toEl.selected)) {
309 | fromEl.setAttribute('selected', '');
310 | } else {
311 | fromEl.removeAttribute('selected', '');
312 | }
313 | },
314 | /**
315 | * The "value" attribute is special for the element
316 | * since it sets the initial value. Changing the "value"
317 | * attribute without changing the "value" property will have
318 | * no effect since it is only used to the set the initial value.
319 | * Similar for the "checked" attribute.
320 | */
321 | INPUT: function(fromEl, toEl) {
322 | fromEl.checked = toEl.checked;
323 |
324 | if (fromEl.value != toEl.value) {
325 | fromEl.value = toEl.value;
326 | }
327 |
328 | if (!hasAttribute(toEl, 'checked')) {
329 | fromEl.removeAttribute('checked');
330 | }
331 |
332 | if (!hasAttribute(toEl, 'value')) {
333 | fromEl.removeAttribute('value');
334 | }
335 | },
336 |
337 | TEXTAREA: function(fromEl, toEl) {
338 | var newValue = toEl.value;
339 | if (fromEl.value != newValue) {
340 | fromEl.value = newValue;
341 | }
342 |
343 | if (fromEl.firstChild) {
344 | fromEl.firstChild.nodeValue = newValue;
345 | }
346 | }
347 | };
348 |
349 | function noop() {}
350 |
351 | /**
352 | * Loop over all of the attributes on the target node and make sure the
353 | * original DOM node has the same attributes. If an attribute
354 | * found on the original node is not on the new node then remove it from
355 | * the original node
356 | * @param {HTMLElement} fromNode
357 | * @param {HTMLElement} toNode
358 | */
359 | function morphAttrs(fromNode, toNode) {
360 | var attrs = toNode.attributes;
361 | var i;
362 | var attr;
363 | var attrName;
364 | var attrValue;
365 | var foundAttrs = {};
366 |
367 | for (i=attrs.length-1; i>=0; i--) {
368 | attr = attrs[i];
369 | if (attr.specified !== false) {
370 | attrName = attr.name;
371 | attrValue = attr.value;
372 | foundAttrs[attrName] = true;
373 |
374 | if (fromNode.getAttribute(attrName) !== attrValue) {
375 | fromNode.setAttribute(attrName, attrValue);
376 | }
377 | }
378 | }
379 |
380 | // Delete any extra attributes found on the original DOM element that weren't
381 | // found on the target element.
382 | attrs = fromNode.attributes;
383 |
384 | for (i=attrs.length-1; i>=0; i--) {
385 | attr = attrs[i];
386 | if (attr.specified !== false) {
387 | attrName = attr.name;
388 | if (!foundAttrs.hasOwnProperty(attrName)) {
389 | fromNode.removeAttribute(attrName);
390 | }
391 | }
392 | }
393 | }
394 |
395 | /**
396 | * Copies the children of one DOM element to another DOM element
397 | */
398 | function moveChildren(fromEl, toEl) {
399 | var curChild = fromEl.firstChild;
400 | while(curChild) {
401 | var nextChild = curChild.nextSibling;
402 | toEl.appendChild(curChild);
403 | curChild = nextChild;
404 | }
405 | return toEl;
406 | }
407 |
408 | function defaultGetNodeKey(node) {
409 | return node.id;
410 | }
411 |
412 | function morphdom(fromNode, toNode, options) {
413 | if (!options) {
414 | options = {};
415 | }
416 |
417 | if (typeof toNode === 'string') {
418 | if (fromNode.nodeName === '#document' || fromNode.nodeName === 'HTML') {
419 | var toNodeHtml = toNode;
420 | toNode = document.createElement('html');
421 | toNode.innerHTML = toNodeHtml;
422 | } else {
423 | toNode = toElement(toNode);
424 | }
425 | }
426 |
427 | var savedEls = {}; // Used to save off DOM elements with IDs
428 | var unmatchedEls = {};
429 | var getNodeKey = options.getNodeKey || defaultGetNodeKey;
430 | var onBeforeNodeAdded = options.onBeforeNodeAdded || noop;
431 | var onNodeAdded = options.onNodeAdded || noop;
432 | var onBeforeElUpdated = options.onBeforeElUpdated || options.onBeforeMorphEl || noop;
433 | var onElUpdated = options.onElUpdated || noop;
434 | var onBeforeNodeDiscarded = options.onBeforeNodeDiscarded || noop;
435 | var onNodeDiscarded = options.onNodeDiscarded || noop;
436 | var onBeforeElChildrenUpdated = options.onBeforeElChildrenUpdated || options.onBeforeMorphElChildren || noop;
437 | var childrenOnly = options.childrenOnly === true;
438 | var movedEls = [];
439 |
440 | function removeNodeHelper(node, nestedInSavedEl) {
441 | var id = getNodeKey(node);
442 | // If the node has an ID then save it off since we will want
443 | // to reuse it in case the target DOM tree has a DOM element
444 | // with the same ID
445 | if (id) {
446 | savedEls[id] = node;
447 | } else if (!nestedInSavedEl) {
448 | // If we are not nested in a saved element then we know that this node has been
449 | // completely discarded and will not exist in the final DOM.
450 | onNodeDiscarded(node);
451 | }
452 |
453 | if (node.nodeType === 1) {
454 | var curChild = node.firstChild;
455 | while(curChild) {
456 | removeNodeHelper(curChild, nestedInSavedEl || id);
457 | curChild = curChild.nextSibling;
458 | }
459 | }
460 | }
461 |
462 | function walkDiscardedChildNodes(node) {
463 | if (node.nodeType === 1) {
464 | var curChild = node.firstChild;
465 | while(curChild) {
466 |
467 |
468 | if (!getNodeKey(curChild)) {
469 | // We only want to handle nodes that don't have an ID to avoid double
470 | // walking the same saved element.
471 |
472 | onNodeDiscarded(curChild);
473 |
474 | // Walk recursively
475 | walkDiscardedChildNodes(curChild);
476 | }
477 |
478 | curChild = curChild.nextSibling;
479 | }
480 | }
481 | }
482 |
483 | function removeNode(node, parentNode, alreadyVisited) {
484 | if (onBeforeNodeDiscarded(node) === false) {
485 | return;
486 | }
487 |
488 | parentNode.removeChild(node);
489 | if (alreadyVisited) {
490 | if (!getNodeKey(node)) {
491 | onNodeDiscarded(node);
492 | walkDiscardedChildNodes(node);
493 | }
494 | } else {
495 | removeNodeHelper(node);
496 | }
497 | }
498 |
499 | function morphEl(fromEl, toEl, alreadyVisited, childrenOnly) {
500 | var toElKey = getNodeKey(toEl);
501 | if (toElKey) {
502 | // If an element with an ID is being morphed then it is will be in the final
503 | // DOM so clear it out of the saved elements collection
504 | delete savedEls[toElKey];
505 | }
506 |
507 | if (!childrenOnly) {
508 | if (onBeforeElUpdated(fromEl, toEl) === false) {
509 | return;
510 | }
511 |
512 | morphAttrs(fromEl, toEl);
513 | onElUpdated(fromEl);
514 |
515 | if (onBeforeElChildrenUpdated(fromEl, toEl) === false) {
516 | return;
517 | }
518 | }
519 |
520 | if (fromEl.tagName != 'TEXTAREA') {
521 | var curToNodeChild = toEl.firstChild;
522 | var curFromNodeChild = fromEl.firstChild;
523 | var curToNodeId;
524 |
525 | var fromNextSibling;
526 | var toNextSibling;
527 | var savedEl;
528 | var unmatchedEl;
529 |
530 | outer: while(curToNodeChild) {
531 | toNextSibling = curToNodeChild.nextSibling;
532 | curToNodeId = getNodeKey(curToNodeChild);
533 |
534 | while(curFromNodeChild) {
535 | var curFromNodeId = getNodeKey(curFromNodeChild);
536 | fromNextSibling = curFromNodeChild.nextSibling;
537 |
538 | if (!alreadyVisited) {
539 | if (curFromNodeId && (unmatchedEl = unmatchedEls[curFromNodeId])) {
540 | unmatchedEl.parentNode.replaceChild(curFromNodeChild, unmatchedEl);
541 | morphEl(curFromNodeChild, unmatchedEl, alreadyVisited);
542 | curFromNodeChild = fromNextSibling;
543 | continue;
544 | }
545 | }
546 |
547 | var curFromNodeType = curFromNodeChild.nodeType;
548 |
549 | if (curFromNodeType === curToNodeChild.nodeType) {
550 | var isCompatible = false;
551 |
552 | if (curFromNodeType === 1) { // Both nodes being compared are Element nodes
553 | if (curFromNodeChild.tagName === curToNodeChild.tagName) {
554 | // We have compatible DOM elements
555 | if (curFromNodeId || curToNodeId) {
556 | // If either DOM element has an ID then we handle
557 | // those differently since we want to match up
558 | // by ID
559 | if (curToNodeId === curFromNodeId) {
560 | isCompatible = true;
561 | }
562 | } else {
563 | isCompatible = true;
564 | }
565 | }
566 |
567 | if (isCompatible) {
568 | // We found compatible DOM elements so transform the current "from" node
569 | // to match the current target DOM node.
570 | morphEl(curFromNodeChild, curToNodeChild, alreadyVisited);
571 | }
572 | } else if (curFromNodeType === 3) { // Both nodes being compared are Text nodes
573 | isCompatible = true;
574 | // Simply update nodeValue on the original node to change the text value
575 | curFromNodeChild.nodeValue = curToNodeChild.nodeValue;
576 | }
577 |
578 | if (isCompatible) {
579 | curToNodeChild = toNextSibling;
580 | curFromNodeChild = fromNextSibling;
581 | continue outer;
582 | }
583 | }
584 |
585 | // No compatible match so remove the old node from the DOM and continue trying
586 | // to find a match in the original DOM
587 | removeNode(curFromNodeChild, fromEl, alreadyVisited);
588 | curFromNodeChild = fromNextSibling;
589 | }
590 |
591 | if (curToNodeId) {
592 | if ((savedEl = savedEls[curToNodeId])) {
593 | morphEl(savedEl, curToNodeChild, true);
594 | curToNodeChild = savedEl; // We want to append the saved element instead
595 | } else {
596 | // The current DOM element in the target tree has an ID
597 | // but we did not find a match in any of the corresponding
598 | // siblings. We just put the target element in the old DOM tree
599 | // but if we later find an element in the old DOM tree that has
600 | // a matching ID then we will replace the target element
601 | // with the corresponding old element and morph the old element
602 | unmatchedEls[curToNodeId] = curToNodeChild;
603 | }
604 | }
605 |
606 | // If we got this far then we did not find a candidate match for our "to node"
607 | // and we exhausted all of the children "from" nodes. Therefore, we will just
608 | // append the current "to node" to the end
609 | if (onBeforeNodeAdded(curToNodeChild) !== false) {
610 | fromEl.appendChild(curToNodeChild);
611 | onNodeAdded(curToNodeChild);
612 | }
613 |
614 | if (curToNodeChild.nodeType === 1 && (curToNodeId || curToNodeChild.firstChild)) {
615 | // The element that was just added to the original DOM may have
616 | // some nested elements with a key/ID that needs to be matched up
617 | // with other elements. We'll add the element to a list so that we
618 | // can later process the nested elements if there are any unmatched
619 | // keyed elements that were discarded
620 | movedEls.push(curToNodeChild);
621 | }
622 |
623 | curToNodeChild = toNextSibling;
624 | curFromNodeChild = fromNextSibling;
625 | }
626 |
627 | // We have processed all of the "to nodes". If curFromNodeChild is non-null then
628 | // we still have some from nodes left over that need to be removed
629 | while(curFromNodeChild) {
630 | fromNextSibling = curFromNodeChild.nextSibling;
631 | removeNode(curFromNodeChild, fromEl, alreadyVisited);
632 | curFromNodeChild = fromNextSibling;
633 | }
634 | }
635 |
636 | var specialElHandler = specialElHandlers[fromEl.tagName];
637 | if (specialElHandler) {
638 | specialElHandler(fromEl, toEl);
639 | }
640 | } // END: morphEl(...)
641 |
642 | var morphedNode = fromNode;
643 | var morphedNodeType = morphedNode.nodeType;
644 | var toNodeType = toNode.nodeType;
645 |
646 | if (!childrenOnly) {
647 | // Handle the case where we are given two DOM nodes that are not
648 | // compatible (e.g.
--> or
--> TEXT)
649 | if (morphedNodeType === 1) {
650 | if (toNodeType === 1) {
651 | if (fromNode.tagName !== toNode.tagName) {
652 | onNodeDiscarded(fromNode);
653 | morphedNode = moveChildren(fromNode, document.createElement(toNode.tagName));
654 | }
655 | } else {
656 | // Going from an element node to a text node
657 | morphedNode = toNode;
658 | }
659 | } else if (morphedNodeType === 3) { // Text node
660 | if (toNodeType === 3) {
661 | morphedNode.nodeValue = toNode.nodeValue;
662 | return morphedNode;
663 | } else {
664 | // Text node to something else
665 | morphedNode = toNode;
666 | }
667 | }
668 | }
669 |
670 | if (morphedNode === toNode) {
671 | // The "to node" was not compatible with the "from node"
672 | // so we had to toss out the "from node" and use the "to node"
673 | onNodeDiscarded(fromNode);
674 | } else {
675 | morphEl(morphedNode, toNode, false, childrenOnly);
676 |
677 | /**
678 | * What we will do here is walk the tree for the DOM element
679 | * that was moved from the target DOM tree to the original
680 | * DOM tree and we will look for keyed elements that could
681 | * be matched to keyed elements that were earlier discarded.
682 | * If we find a match then we will move the saved element
683 | * into the final DOM tree
684 | */
685 | var handleMovedEl = function(el) {
686 | var curChild = el.firstChild;
687 | while(curChild) {
688 | var nextSibling = curChild.nextSibling;
689 |
690 | var key = getNodeKey(curChild);
691 | if (key) {
692 | var savedEl = savedEls[key];
693 | if (savedEl && (curChild.tagName === savedEl.tagName)) {
694 | curChild.parentNode.replaceChild(savedEl, curChild);
695 | morphEl(savedEl, curChild, true /* already visited the saved el tree */);
696 | curChild = nextSibling;
697 | if (empty(savedEls)) {
698 | return false;
699 | }
700 | continue;
701 | }
702 | }
703 |
704 | if (curChild.nodeType === 1) {
705 | handleMovedEl(curChild);
706 | }
707 |
708 | curChild = nextSibling;
709 | }
710 | };
711 |
712 | // The loop below is used to possibly match up any discarded
713 | // elements in the original DOM tree with elemenets from the
714 | // target tree that were moved over without visiting their
715 | // children
716 | if (!empty(savedEls)) {
717 | handleMovedElsLoop:
718 | while (movedEls.length) {
719 | var movedElsTemp = movedEls;
720 | movedEls = [];
721 | for (var i=0; i
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
15 |
16 |
17 |
18 |
19 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | var yo = require('yo-yo')
2 | var Control = require('mapbox-gl/js/ui/control/control')
3 |
4 | module.exports = Layers
5 |
6 | /**
7 | * Creates a layer toggle control
8 | * @param {Object} [options]
9 | * @param {string} [options.type='multiple'] Selection type: `multiple` to allow independently toggling each layer/group, `single` to only choose one at a time.
10 | * @param {Object} [options.layers] An object determining which layers to include. Each key is a display name (what's shown in the UI), and each value is the corresponding layer id in the map style (or an array of layer ids).
11 | * @param {string} [options.position='top-right'] A string indicating position on the map. Options are `top-right`, `top-left`, `bottom-right`, `bottom-left`.
12 | * @param {function} [options.onChange] Optional callback called with `{name: dispayName, layerIds: [...], active: true|false }` for the clicked layer
13 | * @example
14 | * (new Layers({ 'National Parks': 'national_park', 'Other Parks': 'parks' }))
15 | * .addTo(map)
16 | */
17 | function Layers (options) {
18 | this.options = Object.assign({}, this.options, options)
19 | if (options.layers) {
20 | // normalize layers to arrays
21 | var layers = {}
22 | for (var k in this.options.layers) {
23 | layers[k] = Array.isArray(this.options.layers[k])
24 | ? this.options.layers[k] : [this.options.layers[k]]
25 | }
26 | this.options.layers = layers
27 | }
28 |
29 | this._onClick = this._onClick.bind(this)
30 | this._isActive = this._isActive.bind(this)
31 | this._layerExists = this._layerExists.bind(this)
32 | this._update = this._update.bind(this)
33 | }
34 |
35 | Layers.prototype = Object.create(Control.prototype)
36 | Layers.prototype.constructor = Layers
37 | Layers.prototype.options = { position: 'top-right', type: 'multiple' }
38 | Layers.prototype.onAdd = function onAdd (map) {
39 | this._map = map
40 | var style = map.getStyle()
41 | this._allLayers = style.layers.map((layer) => layer.id)
42 | if (!this.options.layers) {
43 | this.options.layers = {}
44 |
45 | // if there's Mapbox Studio metadata available, use any groups we can find
46 | var groups = {}
47 | if (style.metadata && style.metadata['mapbox:groups']) {
48 | groups = style.metadata['mapbox:groups']
49 | Object.keys(groups).forEach((g) => { this.options.layers[groups[g].name] = [] })
50 | }
51 |
52 | style.layers.forEach((layer) => {
53 | var group = layer.metadata ? groups[layer.metadata['mapbox:group']] : null
54 | if (layer.metadata && group) {
55 | this.options.layers[group.name].push(layer.id)
56 | } else {
57 | this.options.layers[layer.id] = [layer.id]
58 | }
59 | })
60 | }
61 | this._map.on('style.change', this._update)
62 | this._map.style.on('layer.remove', this._update)
63 | this._map.style.on('layer.add', this._update)
64 | return this._render()
65 | }
66 |
67 | Layers.prototype.onRemove = function onRemove () {
68 | this._map.off('style.change', this._update)
69 | this._map.style.off('layer.remove', this._update)
70 | this._map.style.off('layer.add', this._update)
71 | }
72 |
73 | Layers.prototype._update = function _update () {
74 | this._allLayers = this._map.getStyle().layers.map((layer) => layer.id)
75 | yo.update(this._container, this._render())
76 | }
77 | Layers.prototype._render = function _render () {
78 | var layers = this.options.layers
79 | var className = 'mapboxgl-ctrl mapboxgl-layers'
80 | return yo`
81 |