Leaflet and Mapbox: Buttons not working when added via Javascript - javascript

Anyone know why that, when clicked, the buttons do not add or remove overlays from the map? Full PLNKR here
The HTML
<div id="toggleButtons" style="display: none">
<button id="add">Add Overlays</button>
<button id="remove">Remove Overlays</button>
</div>
The Javascript
L.Control.GroupedLayers.include({
addOverlays: function () {
for (var i in this._layers) {
if (this._layers[i].overlay) {
if (!this._map.hasLayer(this._layers[i].layer)) {
this._map.addLayer(this._layers[i].layer);
}
}
}
},
removeOverlays: function () {
for (var i in this._layers) {
if (this._layers[i].overlay) {
if (this._map.hasLayer(this._layers[i].layer)) {
this._map.removeLayer(this._layers[i].layer);
}
}
}
}
});
var control = new L.Control.GroupedLayers(ExampleData.Basemaps, {
'Landmarks': {
'Cities': ExampleData.LayerGroups.cities,
'Restaurants': ExampleData.LayerGroups.restaurants
},
'Random': {
'Dogs': ExampleData.LayerGroups.dogs,
'Cats': ExampleData.LayerGroups.cats
}
}).addTo(map);
L.DomEvent.addListener(L.DomUtil.get('add'), 'click', function () {
control.addOverlays();
});
L.DomEvent.addListener(L.DomUtil.get('remove'), 'click', function () {
control.removeOverlays();
});
And then I added the mapbox legendControl.addLegend method (from the mapbox API documentation)
map.legendControl.addLegend(document.getElementById('toggleButtons').innerHTML);
Although the buttons are shown in the map, their click properties are not working. Any clues? Thanks!

You're not 'adding' the buttons with javascript, you're making a copy of them and placing the copy into the legendControl. The actual buttons with the eventhandlers are still present in the DOM but hidden because you've added display: none as inline style. What you want to do is select the buttons and remove them from the body:
var buttons = document.getElementById('toggleButtons');
document.body.removeChild(buttons);
Then you can add them to the legend and attach the eventhandlers:
var legendControl = L.mapbox.legendControl().addTo(map);
legendControl.addLegend(buttons.innerHTML);
L.DomEvent.addListener(L.DomUtil.get('add'), 'click', function () {
control.addOverlays();
});
L.DomEvent.addListener(L.DomUtil.get('remove'), 'click', function () {
control.removeOverlays();
});
Working example on Plunker: http://plnkr.co/edit/7pDkrZbS7Re1YshKZSLs?p=preview
PS. I'm quite baffled as to why you would abuse mapbox's legend control class to add two buttons. If you need a custom control you can just create one using leaflet's L.Control class. It spares you from loading the legend control class which you're not using, thus bloat.
EDIT: As promised in the comments below an example of rolling this solution into your own custom control. I'll explain to more throughout the comments in the code but the general idea is take the basic L.Control interface and adding the functionality and DOM generation to it:
// Create a new custom control class extended from L.Control
L.Control.Toggle = L.Control.extend({
// Have some default options, you can also change/set
// these when intializing the control
options: {
position: 'topright',
addText: 'Add',
removeText: 'Remove'
},
initialize: function (control, options) {
// Add the options to the instance
L.setOptions(this, options);
// Add a reference to the layers in the layer control
// which is added to the constructor upon intialization
this._layers = control._layers;
},
onAdd: function (map) {
// Create the container
var container = L.DomUtil.create('div', 'control-overlaystoggle'),
// Create add button with classname, append to container
addButton = L.DomUtil.create('button', 'control-overlaystoggle-add', container),
// Create remove button with classname, append to container
removeButton = L.DomUtil.create('button', 'control-overlays-toggleremove', container);
// Add texts from options to the buttons
addButton.textContent = this.options.addText;
removeButton.textContent = this.options.removeText;
// Listen for click events on button, delegate to methods below
L.DomEvent.addListener(addButton, 'click', this.addOverlays, this);
L.DomEvent.addListener(removeButton, 'click', this.removeOverlays, this);
// Make sure clicks don't bubble up to the map
L.DomEvent.disableClickPropagation(container);
// Return the container
return container;
},
// Methods to add/remove extracted from the groupedLayerControl
addOverlays: function () {
for (var i in this._layers) {
if (this._layers[i].overlay) {
if (!this._map.hasLayer(this._layers[i].layer)) {
this._map.addLayer(this._layers[i].layer);
}
}
}
},
removeOverlays: function () {
for (var i in this._layers) {
if (this._layers[i].overlay) {
if (this._map.hasLayer(this._layers[i].layer)) {
this._map.removeLayer(this._layers[i].layer);
}
}
}
}
});
Now you can use your new control as follows:
// Create a new instance of your layer control and add it to the map
var layerControl = new L.Control.GroupedLayers(baselayers, overlays).addTo(map);
// Create a new instance of your toggle control
// set the layercontrol and options as parameters
// and add it to the map
var toggleControl = new L.Control.Toggle(layerControl, {
position: 'bottomleft',
addText: 'Add overlays',
removeText: 'Remove overlays'
}).addTo(map);
I know, this is quick and dirty but it should give you a decent idea of what you can do with the L.Control class in general.
Here's a working example on Plunker: http://plnkr.co/edit/7pDkrZbS7Re1YshKZSLs?p=preview
And here's the reference for L.Control: http://leafletjs.com/reference.html#control

You need to follow delegation strategy here..
document.querySelector('body').addEventListener('click', function(event) {
if (event.target.id.toLowerCase() === 'add') {
control.addOverlays();
}
if (event.target.id.toLowerCase() === 'remove') {
control.removeOverlays();
}
});

Related

Affecting other views in Marionette

I am working in Marionette and have an accordion which we have set up so that the individual panels are templates that are called in and created by
var AccorionView = require(“../folder/AccordionView”);
var expandButtons = require(“../folder/expandButtons”);
var MainPage = Marionette.View.extend({
regions: {
region: “.region”,
button: “.buttons”
},
this.newAccordion = new AccordionView({
header: “header goes here”,
childView: new panelView(),
});
this.showChildView(‘region’, this.newAccordion);”
I am going to pull in another view with the actual Expand/Collapse All button in it, which will expand and collapse all of the accordion panels on this page. The JavaScript that would be used on this page would be
expandAll: function() {
this.newAccordion.expand();
},
However, this function will be put into the new JavaScript view of the buttons. I am going to send the names of the accordion panels to the button view when calling it into this page, but how do I get the function on that view to influence the accordion panels on this main page?
I would use Backbone.Radio in this case:
const Radio = require('backbone.radio');
const accorionChannel = Radio.channel('accorion');
const MainPage = Marionette.View.extend({
// ...
initialize() {
accorionChannel.on('expand', function() {
this.newAccordion.expand();
});
accorionChannel.on('unexpand', function() {
this.newAccordion.unexpand();
});
}
// ...
});
const WhateverView = Marionette.View.extend({
someEventHandler() {
accorionChannel.trigger('expand');
// OR
accorionChannel.trigger('unexpand');
}
});
Radio channel is singleton, you can create a new one every time but it will refer to the same channel. This saves you from passing the channel variable around or having a global variable.
You can do this one of two ways
1) With triggers/childViewEvents
// in expandButtons
expandButtons = Marionette.View.extend(
triggers: {
'click #ui.expandAll': 'expandAll'
}
);
// in MainPage
MainPage = Marionette.View.extend({
childViewEvents: {
'expandAll': 'expandAll'
},
expandAll: function(child) {
this.newAccordion.expand();
// OR
this.getChildView('region').expand();
}
})
OR
2) With Backbone.Radio
// in expandButtons
var Radio = require('Backbone.Radio');
var expandChannel = Radio.channel('expand');
var expandButtons = Marionette.View.extend({
events: {
'click #ui.expandAll': 'expandAll'
},
expandAll: function(e) {
expandChannel.trigger('expand:all');
}
});
// in AccordionView
var AccordionView = Marionette.View.extend({
channelName: 'expand',
radioEvents: {
'expand:all': 'expand' // triggers this.expand();
}
});
In this case, it might be even easier to do #2 but instead of adding the radio listener to the AccordionView, attach the listeners to the PanelView (AccordionView's childView). This is because AccordionView's expand function will likely have to iterate each of its children like:
this.children.each(function(childView) {
childView.expand();
});

Dynamically instantiate QuillJS editor

(I'm going to preface this with the fact that I'm a new javascript developer, and I'm sure I have gaps in my knowledge about how javascript/angular/quill all work together on the page.)
I'm wanting to know if this is possible. Instead of instantiating the editor in the script tag on the page, I want to instantiate the editor for the div when it gets clicked. I'm using an Angular controller for my page, and inside the on click event I set up for the div, I tried a few things:
editor = new Quill(myDiv, {
modules: { toolbar: '#toolbar' },
theme: 'snow'
});
But that didn't work, so I thought maybe I had to explicitly pass the id of the div:
editor = new Quill('#editor', {
modules: { toolbar: '#toolbar' },
theme: 'snow'
});
This didn't work, and didn't focus inside the div and allow me to edit. So I thought maybe the problem was that I was hijacking the click event with angular, and maybe I need to switch the focus to the div after instantiating the editor. So I created a focus directive (just copy/pasted from another SO article) which worked fine when I tested on an input.
app.directive('focusOn', function () {
return function (scope, elem, attr) {
scope.$on(attr.focusOn, function (e) {
elem[0].focus();
});
};
then in the on click function in the angular controller:
$scope.$broadcast('focussec123');
if (editor == null) {
editor = new Quill('#editor', {
modules: { toolbar: '#toolbar' },
theme: 'snow'
});
}
That worked to select the text inside the div, but it didn't show the toolbar and so I suspected it didn't really work. I'm sure I'm misunderstanding some interactions and I'm fully aware I lack a lot of necessary knowledge about JS. My bottom line is I want to know:
Is it possible to dynamically instantiate the editor only for the current section, and to instantiate the editor again for another section when it gets clicked, etc.
If so, how?
Thanks in advance.
yes you can create Quill instances dynamically by clicking on a <div>.
It's exactly what we do.
That's how (roughly):
export class TextbocComponent ... {
private quill: Quill;
private target: HTMLElement;
private Quill = require("quill/dist/quill");
private onParagraphClicked(event: MouseEvent): void {
const options = {
theme: "bubble"
};
if (!this.quill) {
this.target = <HTMLElement>event.currentTarget;
this.quill = new this.Quill($(target).get(0), options);
$(target).children(".ql-editor").on("click", function(e) {
e.preventDefault();
});
}
this.quill.focus();
event.stopPropagation();
event.preventDefault();
}
}
For those who aren't using Angular:
$(document).on('click', '#editor', function() {
if (!$(this).hasClass('ql-container')) {
var quill = new Quill($('#editor').get(0), {
theme: 'snow'
});
quill.focus()
}
});
Its much easier:
var quills = [];
counter = 0;
$( ".init_quill_class" ).each(function() { // add this class to desired div
quills[counter] = new Quill($(".init_quill_class")[counter], {});
//quills[counter].enable(false); // if u only want to show elems
counter++;
});

Attach component to dynamically created elements with Twitter Flight

Been looking to figure out how with Twitter Flight can attach to dynamic created elements.
Having the following HTML
<article>Add element</article>
And the following component definition
var Article = flight.component(function () {
this.addElement = function () {
this.$node.parent().append('<article>Add element</article>');
};
this.after('initialize', function () {
this.on('click', this.addElement);
});
});
Article.attachTo('article');
Once a new element is created, the click event doesn't fire. Here's the fiddle: http://jsfiddle.net/smxx5/
That's not how you should be using Flight imho.
Each component should be isolated from the rest of the application, therefore you should avoid this.$node.parent()
On the other hand you can interact with descendants.
My suggestion is to create an "Articles manager" component that uses event delegation.
eg.
http://jsfiddle.net/kd75v/
<div class="js-articles">
<article class="js-article-add">Add element</article>
<div/>
and
var ArticlesManager = flight.component(function () {
this.defaultAttrs({
addSelector: '.js-article-add',
articleTpl: '<article class="js-article-add">Add element</article>'
});
this.addArticle = function () {
this.$node.append(this.attr.articleTpl);
};
this.after('initialize', function () {
this.on('click', {
addSelector: this.addArticle
});
});
});
ArticlesManager.attachTo('.js-articles');
Try attaching Article to each new article added:
JSFiddle: http://jsfiddle.net/TrueBlueAussie/smxx5/2/
var Article = flight.component(function () {
this.addElement = function () {
var newArticle = $('<article>Add element</article>');
this.$node.parent().append(newArticle);
Article.attachTo(newArticle);
};
this.after('initialize', function () {
this.on('click', this.addElement);
});
});
Article.attachTo('article');
The Article.attachTo('article'); at the end, that runs once on load, will only attach to existing article elements.
I hit this problem, and worked around is as follows...
Javascript: All thrown together for brevity, but could easily be separated.
(function(){
var TestComponent, LoaderComponent;
TestComponent = flight.component(function() {
this.doSomething = function()
{
console.log('hi there...');
};
this.after('initialize', function() {
this.on('mouseover', this.doSomething);
});
});
LoaderComponent = flight.component(function() {
this.attachComponents = function()
{
TestComponent.attachTo('.test');
};
this.after('initialize', function() {
// Initalise existing components
this.attachComponents();
// New item created, so re-attach components
this.on('newItem:testComponent', this.attachComponents);
});
});
LoaderComponent.attachTo('body');
}());
HTML: Note that one .test node exists. This will be picked up by Flight on initialization (i.e. not dynamic). We then add a second .test node using jQuery and fire off the event that the LoaderComponent is listening on.
<div class="test">
<p>Some sample text.</p>
</div>
<script>
$('body').append('<div class="test"><p>Some other text</p></div>').trigger('newItem:testComponent');
</script>
This is obviously a very contrived example, but should show that it's possible to use Flight with dynamically created elements.
Hope that helped :)

Implementation knockout custom binding with afterRender functionality

I'm working with a list of images. The images are loaded dynamically; the list of references is stored in observableArray.
After a full load of the image list I want to connect handlers of DOM-elements. My implementation at the moment:
in View:
<div class="carousel_container" data-bind="template: { 'name': 'photoTemplate', 'foreach': ImageInfos, 'afterRender': renderCarousel }">
<script type="text/html" id="photoTemplate">
//...content of template
</script>
in ViewModel:
self.counterCarousel = 0;
self.renderCarousel = function (elements) {
var allImagesCount = self.ImageInfos().length;
self.counterCarousel++;
if (self.counterCarousel >= allImagesCount) {
self.counterCarousel = 0;
// ... add handlers here
}
}
This is a very ugly approach. In addition, user can add / delete images, so after each addition or removal is required remove all handlers and connect it again. How can I organize a custom binding to handle this scenario?
I don't see why this approach would not work -
ko.utils.arrayForEach(ImageInfos(), function (image) {
// ... add handlers here
});
Or better yet, bind an event to each item with a class of 'image-info' so that you don't have to redo the bindings when items are added or changed -
var afterRender = function (view) {
bindEventToImages(view, '.image-info', doSomething);
};
var bindEventToImages= function (rootSelector, selector, callback, eventName) {
var eName = eventName || 'click';
$(rootSelector).on(eName, selector, function () {
var selectedImage = ko.dataFor(this);
callback(selectedImage);
return false;
});
};
function doSomething(sender) {
alert(sender);
// handlers go here
}
This binds an event to every class 'image-info' and on-click handles the calling element, executing doSomething.

Make an OverlayView clickable in Google Maps V3

I'm trying to add a mouseover event listener to a Google Maps overlay view. I've been following this question, but can't get the listener to work. This is my code:
InfoWindow.prototype = new google.maps.OverlayView;
InfoWindow.prototype.onAdd = function() {
this.getPanes().overlayMouseTarget.appendChild(this.$content.get(0));
this.getPanes().overlayMouseTarget.parentNode.style.zIndex = 100000;
google.maps.event.addListener(this, 'mouseover', function() {
console.log('MOUSEOVER');
});
};
This doesn't give any errors, but also doesn't do anything on mouseover. I've also tried using this.listeners:
this.listeners = [
google.maps.event.addDomListener(this, "mouseover", function (e) {
console.log('MOUSEOVER');
})
];
but it doesn't help either. What am I doing wrong?
For the domListener to work, you need to attach it to a dom node:
google.maps.event.addDomListener(this.$content.get(0), "mouseover", function (e) {
console.log('MOUSEOVER');
});

Categories

Resources