I am using the jQuery U.S. map plugin.
Found here. https://github.com/NewSignature/us-map/
I am trying to add 10 additional buttons, that will ALSO correlate with the state data. For instance by default you can HOVER over a state and there is a HOVER COLOR. I'm trying to maintain this effect over the new states in which I'm creating ADDITIONAL buttons for.
For instance, new button below.
$("#star_btn1").click(function() {
$('#ca').toggle();
The plugin has hover styles defined and customizeable.
'stateHoverStyles': {
fill: '#ffc600',
},
and:
_defaultMouseOverAction: function(stateData) {
// hover effect
this.bringShapeToFront(stateData.shape);
this.paper.safari();
// ... for the state
var attrs = {};
if(this.options.stateSpecificHoverStyles[stateData.name]) {
$.extend(attrs, this.options.stateHoverStyles, this.options.stateSpecificHoverStyles[stateData.name]);
} else {
attrs = this.options.stateHoverStyles;
}
stateData.shape.animate(attrs, this.options.stateHoverAnimation);
Any ideas how I can incorporate this with my new buttons as well?
You can have the event handlers for the buttons trigger the events for the map plugin.
HTML:
<button type="button" class="stateBtn" data-name="CA">California</button>
<button type="button" class="stateBtn" data-name="MN">Minnesota</button>
<br />
<br />
<div id="map" style="width: 550px; height: 350px;"></div>
<div id="clicked-state"></div>
Original JavaScript for the map:
$('#map').usmap({
click: function (event, data) {
$('#clicked-state')
.text('You clicked: ' + data.name)
.parent().effect('highlight', {
color: '#C7F464'
}, 2000);
}
});
Additional JavaScript for the buttons:
$('.stateBtn').on('click mouseout mouseover', function(e) {
var name = $(this).attr('data-name');
$('#map').usmap('trigger', name, e.type, e);
});
Notice how additional state buttons can be added without needing to add any additional JavaScript code.
Demo on jsfiddle
US Map Plugin Documentation
Related
I'm using clipboard js to copy-paste content and there is a tooltip attached to the button by Tippy js.
But how do I change the tooltip content and make it appear for two seconds as long as the clipboard success event is fired? I cannot think of a way to change the content because I'm not using the prop but the attribute method.
Possible related doc:
https://atomiks.github.io/tippyjs/v6/constructor/
https://atomiks.github.io/tippyjs/v6/tippy-instance/
https://atomiks.github.io/tippyjs/v6/methods/
var clipboard = new ClipboardJS('.btn');
tippy('.btn', {
placement: 'bottom',
theme: 'clean'
});
clipboard.on('success', function(e) {
//Change the tooltip content and show for two seconds
});
<button class="btn" data-clipboard-text="Testing...123" data-tippy-content="Tooltip">
Copy to clipboard
</button>
<script src="https://unpkg.com/#popperjs/core#2"></script>
<script src="https://unpkg.com/tippy.js#6"></script>
<script src="https://unpkg.com/clipboard#2/dist/clipboard.min.js"></script>
You just need to get instance of your tippy object and call some methods on it in your event listener.
var clipboard = new ClipboardJS('.btn');
const btnTippy = tippy('.btn', {
placement: 'bottom',
theme: 'clean',
})[0]; // 0-index used because tippy will return array of buttons here. You can use `querySelector` that will return only one instance if don't need array.
const copiedTippy = tippy('.btn', {
placement: 'bottom',
theme: 'clean',
trigger: '',
})[0];
copiedTippy.setContent('Just copied')
clipboard.on('success', function (e) {
copiedTippy.show()
setTimeout(copiedTippy.hide, 2000) // to hide tip after 2 seconds
});
I'm trying to create a Web mapping application with some layers, the idea it's that I have some checkboxes and when I click them the layer is added or removed (I have already done that) but there are a lot of layers and of course a lot of code, I realize that with a function I can do the work, but after hours and hours of work it still doesn't work and I'm ready to give up:
function turnon(idlayer, layer) {
var local = document.getElementById(layer);//verify whick checkbox is the one for the layer
if (local.checked == true) {
map.addOverlay(layer);//an OpenLayers method to add the layer to the map
} else {
map.removeOverlay(layer);
}
}
var wmsSector=document.getElementById('sector')//This is the checkbox
wmsSector.addEventListener("click", turnon);
The thing its that I don't know how to add the parameter on the addEventListener Handler I have tried this:
wmsSector.addEventListener("click",turnon('wmsSector',sector))
I appreciate any help you can give me, because right now the application works but I believe it can be more elegant.
I would be more generic in attaching the listeners so that the checkbox provides the id or name of the overlay to attach, e.g.
// For testing
var map = {
addOverlay: function(layer) {
console.log(layer + ' added');
},
removeOverlay: function(layer) {
console.log(layer + ' removed');
}
};
// Toggle layer on or off
function toggleLayer(evt) {
this.checked? map.addOverlay(this.value) : map.removeOverlay(this.value);
}
// Add listener to each layer checkbox
window.onload = function() {
[].forEach.call(document.querySelectorAll('.layerToggle'), function(node) {
node.addEventListener('click', toggleLayer, false);
});
}
<label for="wmsSector"><input type="checkbox" id="wmsSector" value="wmsSector" class="layerToggle"> WMS Sector</label><br>
<label for="vegetation"><input type="checkbox" id="vegetation" value="vegetation" class="layerToggle"> Vegetation</label><br>
<label for="adminBoundaries"><input type="checkbox" id="adminBoundaries" value="adminBoundaries" class="layerToggle"> Admin Boundaries</label>
I've used the value attribute, but you could also use a data-
* attribute, say data-mapLayer="wmsSector" or similar.
Use an anonymous function,
wmsSector.addEventListener("click",function(){
turnon('wmsSector',sector);
})
Context -
I have a chat component and each individual chat message has a dropdown.
And the dropdown menu is opened by clicking the "More Options icon"(3 dots).
Each individual chat message is a "backbone item view"
One solution is to listen to click on "body", loop through all the menus and then close the dropdown by removing a class on it.
$("body").on("click", function() {
$(".drop-down-menu").each(function(idx, item) {
$(item).removeClass("open"); // open class indicated it is open via CSS
});
});
The CSS -
.drop-down-menu {
visibility: hidden;
opacity: 0;
&.open {
opacity: 1;
visibility: visible;
}
}
Will there be any performance impact if there are 10,000 messages or more?
Hence, I am looking for the best solution to hide the drop down if user clicks anywhere on the screen.
Thanks.
You can make some trivial changes that should improve the performance of your code. The first thing is that there's no reason to loop like you are doing. jQuery objects are collections and jQuery operations usually loop over the elements of a jQuery object. So:
$("body").on("click", function() {
$(".drop-down-menu").removeClass("open");
});
This will automatically remove the class open from all elements matched by the selector ".drop-down-menu". jQuery will still go over a loop internally, but it is faster to let jQuery iterate by itself than to have .each call your own callback and then inside the callback create a new jQuery object on which to call .removeClass.
Furthermore, you logically know that removing the open class from elements that do not have this class is pointless. So you can narrow the operation to only those elements where removing open makes sense:
$("body").on("click", function() {
$(".drop-down-menu.open").removeClass("open");
});
These are principles that are widely applicable and that have trivial cost to implement. Anything more than this runs into the realm of optimizations that may have downsides, and should be supported by actually profiling your code. You could replace the jQuery code with code that only uses stock DOM calls but then if you need support for old browsers the cost of dealing with this and that quirk may not be worth it. And if you are using stock DOM methods, there are different approaches that may yield different performance increases, at the cost of code complexity.
Louis is offering a quick fix with efficient jQuery selectors.
For the long run, I would suggest making each message a MessageView component which has a ContextMenuView component. That way, each view only has one menu to take care of.
Catching clicks outside of an element
Then, use the following ClickOutside view as the context menu base view. It looks complicated, but it only wraps the blur and focus DOM events to know if you clicked outside the view.
It offers a simple onClickOutside callback for the view itself and a click:outside event which is triggered on the element.
The menu view now only has to implement the following:
var ContextMenuView = ClickOutside.extend({
toggle: function(val) {
this.$el.toggleClass("open", val);
this.focus(); // little requirement
},
// here's where the magic happens!
onClickOutside: function() {
this.$el.removeClass("open");
}
});
See the demo
var app = {};
(function() {
var $body = Backbone.$(document.body);
/**
* Backbone view mixin that enables the view to catch simulated
* "click:outside" events (or simple callback) by tracking the
* mouse and focusing the element.
*
* Additional information: Since the blur event is triggered on a mouse
* button pressed and the click is triggered on mouse button released, the
* blur callback gets called first which then listen for click event on the
* body to trigger the simulated outside click.
*/
var ClickOutside = app.ClickOutside = Backbone.View.extend({
events: {
"mouseleave": "_onMouseLeave",
"mouseenter": "_onMouseEnter",
"blur": "_onBlur",
},
/**
* Overwrite the default constructor to extends events.
*/
constructor: function() {
this.mouseInside = false;
var proto = ClickOutside.prototype;
this.events = _.extend({}, proto.events, this.events);
ClickOutside.__super__.constructor.apply(this, arguments);
this.clickOnceEventName = 'click.once' + this.cid;
},
/**
* Hijack this private method to ensure the element has
* the tabindex attribute and is ready to be used.
*/
_setElement: function(el) {
ClickOutside.__super__._setElement.apply(this, arguments);
var focusEl = this.focusEl;
if (focusEl && !this.$focusElem) {
this.$focusElem = focusEl;
if (!(focusEl instanceof Backbone.$)) {
this.$focusElem = Backbone.$(focusEl);
}
} else {
this.$focusElem = this.$el;
}
this.$focusElem.attr('tabindex', -1);
},
focus: function() {
this.$focusElem.focus();
},
unfocus: function() {
this.$focusElem.blur();
$body.off(this.clickOnceEventName);
},
isMouseInside: function() {
return this.mouseInside;
},
////////////////////////////
// private Event handlers //
////////////////////////////
onClickOutside: _.noop,
_onClickOutside: function(e) {
this.onClickOutside(e);
this.$focusElem.trigger("click:outside", e);
},
_onBlur: function(e) {
var $focusElem = this.$focusElem;
if (!this.isMouseInside() && $focusElem.is(':visible')) {
$body.one(this.clickOnceEventName, this._onClickOutside.bind(this));
} else {
$focusElem.focus(); // refocus on inside click
}
},
_onMouseEnter: function(e) {
this.mouseInside = true;
},
_onMouseLeave: function(e) {
this.mouseInside = false;
},
});
var DropdownView = app.Dropdown = ClickOutside.extend({
toggle: function(val) {
this.$el.toggle(val);
this.focus();
},
onClickOutside: function() {
this.$el.hide();
}
});
})();
var DemoView = Backbone.View.extend({
className: "demo-view",
template: $("#demo-template").html(),
events: {
"click .toggle": "onToggleClick",
},
initialize: function() {
this.dropdown = new app.Dropdown();
},
render: function() {
this.$el.html(this.template);
this.dropdown.setElement(this.$(".dropdown"));
return this;
},
onToggleClick: function() {
this.dropdown.toggle(true);
},
});
$("#app")
.append(new DemoView().render().el)
.append(new DemoView().render().el);
html,
body {
height: 100%;
width: 100%;
}
.demo-view {
position: relative;
margin-bottom: 10px;
}
.dropdown {
z-index: 2;
position: absolute;
top: 100%;
background-color: gray;
padding: 10px;
outline: none;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/backbone.js/1.3.3/backbone-min.js"></script>
<div id="app"></div>
<script type="text/template" id="demo-template">
<button type="button" class="toggle">Toggle</button>
<div class="dropdown" style="display:none;">
This is a drop down menu.
</div>
</script>
Alternatives to detect a click outside an element
If you don't want, or can't use blur and focus events, take a look at How do I detect a click outside an element? for alternative techniques.
Lazy initialization of views
Another way to make an SPA more efficient is to delay the creation of new view to the very moment you need it. Instead a creating 10k context menu views, wait for the first time the user clicks on the toggle button and create a new view if it doesn't exist yet.
toggleMenu: function(){
var menuView = this.menuView;
if (!menuView) {
menuView = this.menuView = new ContextMenuView();
this.$('.dropdown').html(menuView.render().el);
}
menuView.toggle();
}
Pagination
Passed a certain threshold of HTML inside a webpage, the browser starts to lag and it impedes the user experience. Instead of dumping 10k views into a div, only show like a 100, or the minimum to cover the visible space.
Then, when scrolling to an edge (top or bottom), append or prepend new views on demand. Like the message list in any web-based chat app, like messenger.com.
Since you will only have one drop down menu open at a time, maybe you can keep a pointer to the element or index of the element it is attached to, instead of looping through all the menus.
I am very new to meteor and it is possible that I am going about this entirely incorrectly.
I have a simple template that represents a menu bar. When the user clicks an Icon, the menu is supposed to appear. When they click it again, it is supposed to disappear.
Here is the markup:
<template name="menu">
<div class="menu">
<div class="toggler">
<i class="fa fa-bars fa-3x"></i>
</div>
<div class="menu-body">
<!-- ... -->
</div>
</div>
</template>
Here is the JS that I have:
Template.menu.helpers({
self: Template.instance(),
menu_body: self.find('.menu-body'),
toggler: self.find('.toggler'),
currently_open: false,
open: function() {
menu_body.style.display = 'flex';
},
close: function() {
menu_body.style.display = 'none';
},
toggle: function() {
if(currently_open) close();
else open();
}
});
Template.menu.events({
'click .toggler': function(event, template) {
console.log(template);
template.toggle();
}
});
I thought the template instance would have access to the helper functions, but according to the log statement, this is what the template instance consists of:
B…e.TemplateInstance {view: B…e.View, data: null, firstNode: div.menu, lastNode: div.menu, _allSubsReadyDep: T…r.Dependency…}
_allSubsReady: false
_allSubsReadyDep: Tracker.Dependency
_subscriptionHandles: Object
data: null
firstNode: div.menu
lastNode: div.menu
view: Blaze.View
__proto__: Blaze.TemplateInstance
Can someone point me in the right direction here. Please feel free to be scrutinous if I am going about it wrong.
Helpers are for functional calls - not event driven works.
Meteor has an events handle that you can use to track events like clicks. Also you can use your css classes to define the styles nicely without programatically overwriting them.
Template.name.events({
'click .menuToggler': function(event, template) {
event.preventDefault();
var menu = template.find('.menu-body'); //(make this in ID!)
if($(menu).hasClass('menuOpen')) {
$(menu).removeClass('menuOpen');
//menu.style.display = 'none';
} else {
$(menu).addClass('menuOpen');
//menu.style.display = 'flex'; Use css to define these on the menuOpen class
}
});
Some things to note: This event handle assumes that your menu-body class is available under the template called "name" in my example. So you will want this event handler at the most top level template you have. It also assumes.
If you want to share state between the various components of your template (helpers, event callbacks etc) it should be done by setting properties directly on the template instances.
This can be done through the onCreated() callback
As per the documentation:
Callbacks added with this method [are] called before your template's logic
is evaluated for the first time. Inside a callback, this is the new
template instance object. Properties you set on this object will be
visible from the callbacks added with onRendered and onDestroyed
methods and from event handlers.
These callbacks fire once and are the first group of callbacks to
fire. Handling the created event is a useful way to set up values on
template instance that are read from template helpers using
Template.instance().
So, to provide a more relevant and concise example than the one in my original question.
Template.menu.onCreated(function() {
this.items = [
{title: 'Home', icon: 'home'},
{title: 'Profile', icon: 'user'},
{title: 'Work', icon: 'line-chart'},
{title: 'Contact', icon: 'phone'}
];
});
Template.menu.helpers({
items: function() {
var self = Template.instance();
return self.items;
}
});
Template.menu.events({
'click .toggler': function(event, template) {
console.log(template.items); //[Object,Object,Object,Object]
//do something with items
}
});
That's actually funny but I created a mini package that helps you do just that: https://atmospherejs.com/voidale/helpers-everywhere
But in your case it's not the right way of doing it. I can you see you want to add an display either flex or none it's better add CSS element like active that hold display: flex and add active or remove it on click like this: $('').addClass('active') or $().removeClass('active')
one liner can also work here: $('.menu-body').toggleClass('active')
I am using ko.js to display a table of venues.
Each venue has an edit button that brings up a dialog showing the editable data.
When the edit button is pressed I bind the venue to the dialog and I store a copy of the data in an undo object.
When I edit fields on the dialog, both the dialog and table are updated.
When I cancel the edit, I bind the venue to the undo objects state. This updates the dialog but it does not update on the table.
Any idea what I am doing wrong here?
Here is my view model.
VenueViewModel = function(venues) {
var self = this;
var venueModal = $("#venueModal");
this.venues = ko.mapping.fromJS(venues);
this.venue = ko.observable();
this.venueUndo = null;
//Cancel an edit
this.cancel = function() {
self.venue(ko.mapping.fromJS(self.venueUndo));
venueModal.modal("hide");
}
//Edit an existing venue
this.edit = function(venue) {
self.venue(venue);
self.venueUndo = ko.mapping.toJS(venue);
venueModal.modal("show");
};
//Create a new venue
this.create = function() {
self.venue(new Venue());
venueModal.modal("show");
};
};
ko.applyBindings(new VenueViewModel(venues));
The article nemesv linked in his comment was the answer.
http://www.knockmeout.net/2013/01/simple-editor-pattern-knockout-js.html
You might consider using KO-UndoManager for this. Here's a sample code to register your viewmodel:
VenueViewModel.undoMgr = ko.undoManager(VenueViewModel, {
levels: 12,
undoLabel: "Undo (#COUNT#)",
redoLabel: "Redo"
});
You can then add undo/redo buttons in your html as follows:
<div class="row center-block">
<button class="btn btn-primary" data-bind="
click: undoMgr.undoCommand.execute,
text: undoMgr.undoCommand.name,
css: { disabled: !undoMgr.undoCommand.enabled() }">UNDO</button>
<button class="btn btn-primary" data-bind="
click: undoMgr.redoCommand.execute,
text: undoMgr.redoCommand.name,
css: { disabled: !undoMgr.redoCommand.enabled() }">REDO</button>
</div>
And here's a Plunkr showing it in action.
You set Knockout observables like this:
self.venue(ko.mapping.fromJS(self.venueUndo));
We Have a small extension to Knockout.js as part out project that extends observables so they can be registered to different stacks of there history.
Maybe it can help you.
Knockout-Memento