I'm trying to learn pure native JavaScript, so please, don't suggest using frameworks to solve the task. I know the're awesome and learning them will be the first thing I'm going to do when I'm done with pure JS :)
Now, the question. Fiddle
As you can see, I have two custom dropdowns there. Each of them can be opened and controlled both with mouse and keyboard. The last thing I need to do to solve this task is to implement the following functionality: opening one dropdown should close the other one, and clicking outside the dropdown should close any of the opened dropdowns (assuming only one can be opened at a time).
I've tried adding an onclick listener to the document, which would close the dropdowns if any of them were open but not used before, but after I've clicked the document once, the dropdowns are not showing any more. But that's not even a solution for half of the problem. since I allocate flags bIsOpen to the objects, I can't access them from another object to see if it's triggered.
Give me a hint, please :)
Move the opening and closing logic into their own functions:
DropDown.prototype.open = function (e) {
...
}
DropDown.prototype.close = function (e) {
...
}
And
this.htmlDropDown.onclick = function (e) {
if (this.bIsOpen) {
this.open(e);
} else {
this.close(e);
}
}
(Make sure that the open and close functions adjust bIsOpen, not the onclick handler.)
Then, add a list of all the current dropdowns that exist:
function DropDown(htmlObject) {
DropDown.dropdowns.push(this);
...
}
DropDown.dropdowns = []; // no 'prototype'
And finally, in the open-er, close all the other dropdowns:
DropDown.prototype.open = function (e) {
var dropdown;
for (var i = 0; i < DropDown.dropdowns.length; i++) {
dropdown = DropDown.dropdowns[i];
if (dropdown !== this) {
dropdown.close();
}
}
...
}
Related
TL;DR: Trying to fire a manual javascript click event on the chat button of twitch, won't send the message. Don't understand why the event doesn't do the same as a normal click and don't know how to make it work.
So I am trying to make a custom bot for twitch.tv, only reading his info from the HTML directly. I've got it perfectly working up to the point at where it can recognize commands and put text in the textbox. Now the problem I have is, as soon as I try to fire a manual click event on the "chat" button, it just doesn't seem to work. My guess is it has something to do with ember.js, and I frankly don't know anything about that. Anyway, here is the part of the code that doesn't work. EDIT: this works if I enter it as single in the console, doesn't work in context of the rest of my code though.
$('.send-chat-button').click();
What happens here is that I acquire a piece of html that contains the chat submit button, which is this:
<button class="button primary float-right send-chat-button" data-bindattr-3945="3945">
<span>
Chat
</span>
</button>
When I try to manually fire a click event on this, nothing happens. However, when I fire a manual click event on buttonContain.children[0] and buttonContain.children1 (which are, respectively, the settings and list of viewers buttons), it does work. They look like this:
<a data-ember-action="3943" class="button glyph-only float-left" title="Chat Settings"></a>
I'm guessing the difference is in the data-ember-action and the data-bindattr-*, but I don't know how to make it work. Anyone here knows why the click() event doesn't work and directly clicking does?
EDIT: If you have any questions about my question, feel free to ask.
EDIT2: I experimented a little more, and I can remove all HTML attributes from the button, and clicking on it will still work. I have no idea what is going on :(.
EDIT3: Okay, so it seems it only stops working when i remove the
Span within the button
Still no idea what is going on. (Yes, have also tried to fire the click event on the span)
EDIT4: As requested, here is all the code from my side. Note that I'm trying to click a button from twitch itself, of which ember side I do not own any code. This code is used by pasting it in the console on a twitch.tv stream and then starting it by calling initiateMessageProcessing. I'm sorry for the lot of hardcoded values, those are twitch' fields that I need. For now I'm just looking for a proof of concept.
var frequency = 5000;
var myInterval = 0;
var lastMessageId = 0;
function initiateMessageProcessing() {
if (myInterval > 0) {
clearInterval(myInterval);
}
myInterval = setInterval("checkMessages()", frequency);
}
function checkMessages() {
var chat = document.getElementsByClassName("chat-lines")[0];
processMessages(extractUnprocessedMessages(chat.children));
lastMessageId = parseInt(chat.lastElementChild.getAttribute("id").substring(5, 10));
}
function extractUnprocessedMessages(chat) {
var unprocessedMessages = [];
var chatId = 0;
for ( i = 0; i < chat.length; i++) {
chatId = parseInt(chat[i].getAttribute("id").substring(5, 10));
if (chatId > lastMessageId) {
unprocessedMessages.push(chat[i]);
}
}
return unprocessedMessages;
}
function processMessages(unprocessedMessages) {
var messageElement;
for ( i = 0; i < unprocessedMessages.length; i++) {
messageElement = unprocessedMessages[i].children[0].getElementsByClassName("message")[0];
if (messageElement != undefined && messageElement != null) {
if (messageElement.innerHTML.search("!test") !== -1) {
sendMessage('Hello world!');
}
}
}
}
function sendMessage(message) {
fillTextArea(message);
var button = $('.send-chat-button').get(0);
var event = new MouseEvent('click', {
bubbles : true
});
button.dispatchEvent(event);
}
function fillTextArea(message){
var textArea;
var chatInterface = document.getElementsByClassName("chat-interface")[0];
var textAreaContain = chatInterface.children[0];
textArea = textAreaContain.children[0].children[0];
textArea.value = message;
}
EDIT5: Eventlistener screenshot:
EDIT6: Edited source code to use $('.send-chat-button').click();
I have tried this, does not work in the current code, it does work if I manually fire this single command in the console when there is text in the chat. But sadly does not work in my code.
EDIT7: used Ember.run, still doesn't work.
EDIT8: used dispatchmouseevent, still doesn't work in context of code
It seems that the target site attaches event listeners without help of JQuery. If it is so, you cannot trigger it using jquery .click() method.
You can try directly mocking the browser event like this:
var button = $('.send-chat-button').get(0);
var event = new MouseEvent('click', {bubbles: true});
button.dispatchEvent(event);
This code will not work in IE8 and lower, but I guess it is not your case.
I know this post is quite old but I had been looking for an answer on this for a while and nothing really worked, after trying out A LOT of stuff I found it works when you focus the chatbox first then focus the button then triggering the click event!!! uuuhm yeah...
$('.chat_text_input').focus();
$('.send-chat-button').focus().trigger('click');
I have no idea why this works (and why it doesn't in any other way), but leaving any of the focusses out makes it fail or bug out.
Programmatically clicking a DOM element to make some action done is somewhat a wrong approach.
You should have define a method myAction() which will be called in two ways. First, from your ember action triggerMyAction() and second, after listening to a custom event, "myEvent".
Instead of $('.send-chat-button').click(); you will code $('something').trigger("myEvent") then.
Something like:
Em.Controller.extend({
myAction:function(){
//do your stuff
},
onMyEvent:function(){
$('something').on('myEvent',this.myAction);
}.on('didInsertElement'),
actions:{
triggerMyAction:function(){
this.myAction();
}
}
})
We are implementing a rather complex single page application and decided to use the "Jquery-context-menu" toolbox. However, we have a fundamental question, and after two days of searching, and reading stuff on the web we are somewhat out of ideas.
The basic question is: If and if yes, how, it will be possible to access the update function (i.e the disable function) from outsite the menu, while the menu is still upon?
The scenario:
We are implementing a game. We use the "jquery context menu" as a context menu for an icon that can be activated by the user (think of a rpg type of icon). After activating it via a click it will be deactvitated automatically (after a few seconds) and then be ready for reactivation again after a while. What we what is that the menu is capturing this state of the icon, by enabling or disabling the menu, while the menu is held open.
There is one example of how to change the visibility of a menu item, via a button press in the same menu, as can be found here:
http://medialize.github.io/jQuery-contextMenu/demo/disabled-changing.html
However, we aim to update the menu from outsite (via a knockout subscribe call). So we would like to do something like:
myknockoutobservable.subscribe(function(newValue){
correctRefernceToThis.data('disableItem1', newvalue)});
while the disabled function looks somehitng like:
disabled: function(key, opt) {
return !this.data('cutDisabled')};
OR, if this does not work we maybe could directly call the update fucntion
myKnockoutobservable.subscribe(function(newValue){
$.contextMenu.op.update(correctRefernceToOpt,correctRefernceToroot)});
and then querying the knockout observable in the callback
if (!myKnockoutobservable) {
return true;}
else {
return false;
The main problem seems to be that we don't reference the context correctly, so we don't have a handle on the correct this, opt, to root, variables, from outside of the Jquery context menu (At least that is our current opinion). We would be very happy if someone could help us finding a solution, or even some good ideas what to try (what we haven't yet).
Thanks you for your help! I understand your approach and it is indeed exactly what we have done:-) I might have not been so clear on this issue but this is the current code od the disabled callback.
disabled: function(key, opt) {
if (!self.item._blocks.Feature._processedStack().canBeActivated()) {
return true;
}
else {
return false;
}
}
This works such that when the menu in closed and reopned again the state is updated. However the update is NOT working while the menu is still open because nothing is triggering the menu to update to the new value.
At the moment we are trying to solve the issue with a different library, will keep you updated.
best, Caspar
You won't subscribe, you'll just have an observable in your viewmodel that you set to true or false, and the menu item will toggle in response. The disabled member of your menu item will look like this:
disabled: function() {
return myobservable();
}
As James Thorpe commented, you'll want to create a custom binding handler to set up your context menu.
It sounds like you're working with several unfamiliar tools. Here is a Fiddle with a minimal example: http://jsfiddle.net/sv3m7ok8/
Update
It occurred to me that since the context menu doesn't bind to a single element, but uses a selector, it makes more sense to do the binding on the body tag. So an updated example: http://jsfiddle.net/sv3m7ok8/1/
Updated again
I now understand that you are specifically trying to get the menu item to enable/disable while the menu is open (and that it doesn't do that normally). I had to go under the covers to get at the menu item node, and hook up a subscription to set the disabled class on it.
init: function (element, data, allbindings, data) {
var dynamicDisable = false;
$.contextMenu({
selector: '.context-menu-one',
callback: function (key, options) {
var m = "clicked: " + key;
window.console && console.log(m) || alert(m);
},
items: {
"edit": {
name: "Clickable",
icon: "edit",
disabled: function (opt, key) {
if (!dynamicDisable) {
var node = key.items.edit.$node[0];
data.disableMenu.subscribe(function (dis) {
$(node)[dis ? 'addClass' : 'removeClass']('disabled')
});
dynamicDisable = true;
}
return data.disableMenu();
}
}
}
});
}
My fiddle sets an interval to toggle disableMenu. You can watch the menu item become active and gray.
http://jsfiddle.net/sv3m7ok8/3/
I'm using the Win8 Grid View Template to display infos from a news site. In the lower menu bar i have implemented a function wich shuts off the titles, so that only the pictures are still visible.
This function is in a "global.js" file which is included in the "default.html" so it's available everywhere and it looks like this:
//function to turn titles off and on
function titleToggle() {
var titles = document.getElementsByClassName("item-overlay");
for (var i = 0; i < titles.length; i++) {
if (Global.titlesAreOn) {
titles[i].style.display = "none";
}
else {
titles[i].style.display = "";
}
}
Global.titlesAreOn = !Global.titlesAreOn;
};
So when i call this function from the menu bar it works for the first items, but when i scroll the end of the groupedItems view (hubview) the titles are still there. When i then scroll back to the beginning the titles are there again too.
I'm also calling the titleToggle function from the ready() function of the "groupedItems.js" to check whether or not to display the titles depending on a global variable. When i do that (whenever i come back to the hubpage) it works all the way, just as expected.
ui.Pages.define("/pages/groupedItems/groupedItems.html", {
navigateToGroup: function (key) {
nav.navigate("/pages/groupDetail/groupDetail.html", { groupKey: key });
},
ready: function (element, options) {
appbar.winControl.disabled = false;
appbar.winControl.hideCommands(["fontSizeBt"]);
appbar.winControl.showCommands(["titleToggle"]);
if (Global.titlesAreOn == false) {
Global.titlesAreOn = true;
Global.titleToggle();
}
I made a short video to show the problem, because its kinda hard to explain --> http://youtu.be/h4FpQf1fRBY I hope you get the idea?
Why does it work when i call it from the ready() function?
Does anyone have an idea? Is it some kind of automatic item caching in order to have better performance? And how could this be solved?
Greets and thanks!
First, here is why this might be happening - WinJS is using single page navigation for the app experience. This means that when you navigate to a new page, actually you don't. Instead the content is removed from the page and the new content is loaded in the same page. It is possible that at the moment you press the button not all elements have been loaded in the DOM and therefore they cannot be manipulated by your function. This is why when you call it from the ready() function it works - all contents are loaded in the DOM. It is generally better to do things in the ready() function.
About the behavior when you slide back left and the items are again reloaded with titles - for some reason the listView items are reloading. Maybe you are using live data from the news site and they are refreshing with the listView control's template again. I cannot know, but it doesn't matter. Hiding the elements is not the best approach I think. It is better to have two templates - one with a title element and one without. The button click handler should get the listView controls(they have to be loaded) and change their templates.
ready: function (element, options) {
var button = document.getElementById('btn');
button.addEventListener("click", btnClickHandler);
}
And the handler:
function btnClickHandler(e) {
var listView = document.getElementById("listView").winControl;
var template2 = document.getElementById("template2");
listView.itemTemplate = template2;
};
I'm using the YUI TabView widget for adding and removing tabs like described in yuilibrary-tabview-add-remove.
I've noticed a "bug" or maybe just a missing functionality: When you close all tabs and then add a new tab, the "add tab" button will get stuck on the left side of the tab bar, and all new tabs will be sorted on the right side. If you don't close all tabs, the button will always stay on the right side no matter what.
Now, I've added a workaround: When adding a new tab, the no-tabs state will be detected and the DOM li-item will be sorted with the jQuery after() method. Finally, the newly added tab will be selected:
onAddClick : function(e) {
e.stopPropagation();
var tabview = this.get('host'), input = this.getTabInput();
tabview.add(input, input.index);
// When previously no tabs present, move 'add button' to end after adding a new tab
if ( tabview.size() == 1) {
var addTabButton = $('#addTabButton');
addTabButton.next().after(addTabButton);
tabview.selectChild(0);
};
}
However, I'm not happy with this solution. Might there be a more elegant way to solve this issue?
Your solution is definitely valid. I'd just write it using YUI because loading YUI and jQuery is really expensive in kweight and maintenance cost (you and your coworkers need to master two libraries).
One clean option is to create a node in the initializer and keep a reference to it so that you can move it around later:
initializer: function (config) {
var tabview = this.get('host');
// create the node before rendering and keep a reference to it
this._addNode = Y.Node.create(this.ADD_TEMPLATE);
tabview.after('render', this.afterRender, this);
tabview.get('contentBox')
.delegate('click', this.onAddClick, '.yui3-tab-add', this);
},
_appendAddNode: function () {
var tabview = this.get('host');
tabview.get('contentBox').one('> ul').append(this._addNode);
},
afterRender: function (e) {
this._appendAddNode();
},
onAddClick: function (e) {
e.stopPropagation();
var tabview = this.get('host'), input = this.getTabInput();
tabview.add(input, input.index);
// When previously no tabs present, move 'add button' to end after adding a new tab
if ( tabview.size() == 1) {
// _addNode will already be present, but by using append() it'll be moved to the
// last place in the list
this._appendAddNode();
};
}
Here's a working version: http://jsbin.com/iLiM/2/
I have an accordion set up to handle registration. I am wanting to validate the data entered on each panel when the user clicks on a different panel tab. I have a continue button on each panel, and am able to validate to my heart's content when the user uses that to go to the next panel.
My problem is that they can also click independently on the accordion tabs (and I want them to be able to skip around for editing purposes), but I would like to validate on those events too.
I've done a bunch of searching, but have not found a satisfactory answer. I am fairly new to Javascript and super-brand-new to jQuery, so please, if you have code snippets for me, be thorough in explaining them.
This should be a straightforward problem (similar to on-click, etc.). I'm quite surprised and frustrated that I haven't found an answer yet.
Edit:
Eric, I couldn't get this to work. Here is my version. I put it in the head section. I have some test code in there that has worked reliably for me in the past (changing the label on one of the tabs). I'm assuming this code has worked for you? Anyway, thanks for your help and I hope we've understood each other sufficiently.
// add capability to detect when accordion tab has been clicked
RegFormAccordion.addEventListener('click', function(e){
var btnElement;
(function findAccordionButton(el){
//e.target is the original element actually clicked on
//the event bubbles up to ancestor/parent nodes which is why you can listen at
//the container
if(!btnElement){ btnElement = e.target; }
else { btnElement = el; }
if(e.target.className !== 'accordionBtn')
{
findAccordionButton(btnElement.parentNode);
}
else
{
var curr_panel_index = RegFormAccordion.getCurrentPanelIndex();
document.getElementById("verify-reg-panel-label").innerHTML = "Index = " + curr_panel_index; // test code to see if it's even getting here
if (curr_panel_index == 1) // contact section
{
ValidateContact();
}
else if (curr_panel_index == 2) // payment section
{
ValidatePayment();
}
UpdateVerifyPanel(); // update contents of verification panel
}
})()
} );
Event delegation.
someAccordianContainer.addEventListener('click', function(e){
var btnElement;
(function findAccordionButton(el){
//e.target is the original element actually clicked on
//the event bubbles up to ancestor/parent nodes which is why you can listen at
//the container
if(!btnElement){ btnElement = e.target; }
else { btnElement = el; }
if(e.target.className !== 'accordionBtn'){
findAccordionButton(btnElement.parentNode);
}
else { doSomething(btnElement); }
})()
} );
You will have to normalize for IE<=8 however if you're supporting older browsers, since it uses a proprietary attachEvent method. Hit quirksmode.org for the details or just use something like jQuery or MooTools.
OK. I found the function that SpryAccordion.js uses to open a new panel and added my own code. Simple and elegant. It's not what I would normally do (usually I leave "libraries" alone). But if you make it editable without giving me another way to take needed control, then the hack is gonna happen.
If I need to use another accordion somewhere else on my website, I will have to double check that I have the correct accordion before invoking the hack. A trade-off I'm willing to make. It works perfectly now. Here is the code:
Spry.Widget.Accordion.prototype.openPanel = function(elementOrIndex)
{
var panelA = this.currentPanel;
var panelB;
if (typeof elementOrIndex == "number")
panelB = this.getPanels()[elementOrIndex];
else
panelB = this.getElement(elementOrIndex);
if (!panelB || panelA == panelB)
return null;
// Start Becca's code
var panelIndex = this.getPanelIndex(panelA);
if (panelIndex == 1) // contact info panel
{
if (ValidateContact())
UpdateVerifyPanel();
else
return null;
}
else if (panelIndex == 2) // payment info panel
{
if (ValidatePayment())
UpdateVerifyPanel();
else
return null;
}
// End Becca's code
var contentA = panelA ? this.getPanelContent(panelA) : null;
var contentB = this.getPanelContent(panelB);
...
...
...
};
Yes, all I wanted was the same control over the panel tabs as I have over my own user-defined buttons, to make sure I could both validate before moving on, and to update my verification screen after any edit the user makes, not just the ones where they happen to hit my continue button. I'm a happy camper. So glad I took a couple of days off.
I hope this helps someone get more control over their own accordions. So glad I don't have to do a crash-course on jQuery when all I want right now is to get my blasted website up.