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/
Related
I'm working with a provider and factory set up in angular, and in the factory is where I do all the heavy lifting of generating the templates, creating the instances, and doing all of the animations. The provider creates a very nifty slider menu from the left.
Problem
What's happening though, is that after the first instance of the slider menu, menu options start to double themselves. So I'll have the original 5, then 10, then 20, then 40... I have found a solution where we start with a null instance, and check if that instance is null, if it is null render the menu. So that forces it to only continuously render the initial 5, but then if we dynamically change the menu we won't ever see those changes and that is not what we want.
Fiddle
https://jsfiddle.net/Mr_Pikachu/chdbxt1h/351/
Broken Code
This is the chunk of code that I am most focused on, as it is the bit that is causing us the issue.
backdropScope.close = function(){
$animate.leave(menu).then(function(){
backdrop.remove();
//menuOpts.scope.$destroy();
// menu_rendered = null;
menu.remove();
});
}
// menustack object
$menuStack = {
'open': function(menuOpts){
menuOpts.scope.main = menuOpts.menu.main;
if(!menu_rendered) {
menu_rendered = menu_template(menuOpts.scope);
}
if(!backdropRendered) {
backdropRendered = backdropTemplate(backdropScope);
}
menuOpts.scope.$apply(function(){
$animate.enter(backdropRendered, body).then(function(){
$animate.enter(menu_rendered, body);
});
});
}
};
List of Attempted Fixes
setting menu_rendered = null in the $animate.leave() will work on the first instance, and re-render the menu properly, but then the backdrop instance won't recognize a click event
Using menuOpts.scope.$destory(), but it did absolutely nothing
Using the current solution of menu_rendered check. It is not optimal and looking for a solution that allows the use of dynamic content.
Updated Fiddle: https://jsfiddle.net/chdbxt1h/355/
I moved the angular.element calls into the body of the $menuStack.open method. The menu content does not get duplicated in repeated exposures. Presumably, this is because the DOM Node is created anew on each open, and garbage collected cleanly on leave and/or remove.
Both the background (menu-overlay) and menu are re-created on each open, so this should honor changes in the source menu data, though possibly not while the menu is open.
I build a small color-picker module. But it only opens up (and then works) when pickColor is called a second time. I also tried to wrap the _openColorPicker into a setTimeout but that didn't work either. In fact, the color-picker didn't show up at all when I did that.
What I found interesting is that the binding to the change event works, so the $ selector must have found the element already.
So I have two questions:
1) why is the picker only showing after the second call to _openColorPicker?
2) why didn't the picker open at all when I wrapper the _openColorPicker call in a setTimeout?
Edit: The _openColorPicker functions gets executed after the user has right-clicked into the document and then clicked on context-menu which is now showing.
Complete Code:
const ColorUtils = {
_initialized: false,
_openColorPicker: function () {
$('#color-picker').click();
},
pickColor: function (onChangeCallback, context) {
if (!this._initialized) {
$('<input/>').attr({
type: 'color',
id: 'color-picker',
display: 'hidden',
value: '#ffffff'
}).appendTo('#centralRow');
this._initialized = true;
$('#color-picker').on('change', onChangeCallback.bind(context));
}
this._openColorPicker();
// version with timeOut
const scope = this;
setTimeout(function () {
scope._openColorPicker();
}, 1000);
}
};
export default ColorUtils;
Above code is used like ColorUtils.pickColor(onColorPicked, this);
Check out this post. Looks like you can't trigger a click on an invisible color picker. That answer suggests giving the element an absolute position and placing it off screen, like so:
position:absolute;
left:-9999px;
top:-9999px;
I tried to replicate your case (for what I understood) : JSFIddle
I made some changes.
I moved the $('<input/>') in a property of the object ColorUtils and appended it to the DOM with absolute position and outside the screen.
(And also commented display:'hidden' because it's either display:none or visibility:hidden and as a CSS property, not Html attribute)
On right clic on the document I instantiate the picker (and register the callback + context) then add a button to the DOM to trigger the picker again.
Does it fulfill your requirements ?
I'm using Polymer for my web-app and I'm currently having some issues with page linking. It's the first site I'm making using all ajax/javascript, thus I haven't used the history functions of javascript a lot yet.
Anyhow, I have a main menu in the left sidebar. When one of those is pressed, it should change the url of the browser and also put it in the history of the browser. To do so I have the following code:
Polymer('my-app', {
mainMenu: function(){
this.$.mainPages.selected = this.$.mainMenu.selected;
console.log("Pusing state " + this.$.mainMenu.selected);
history.pushState(null , "title", this.$.mainMenu.selected);
}
});
Now the problem is that it gets called twice, so if you click once the pushState is called twice. Needless to say this is not good.
I have made a sample code here: click me.
In this sample code you can see after pressing a couple of menu-items, first of all the event gets fired two times. I've also noticed when pressing the back-button, updating the page seems to also refire the pushState.
So in short, my first concern is that the event of pressing an element from the core-menu, the mainMenu function is called twice. Second concern is that I am repushing states when going back, which I presume should be prevented as well.
Updated code:
Polymer('my-app', {
ready: function(){
var link = document.URL.split("/");
this.$.mainMenu.selected = link[link.length-1];
console.log("ready with link: "+link[link.length-1]);
window.onpopstate = function(){
var link = document.URL.split("/");
console.log("Calling onpopstate. New link is: "+link[link.length-1]);
this.$.mainMenu.selected = link[link.length-1];
this.$.mainPages.selected = this.$.mainMenu.selected;
}
},
nav: function(){
this.$.drawerPanel.togglePanel();
},
back: function(){
window.history.back();
},
mainMenu: function(){
console.log("Pusing state " + this.$.mainMenu.selected);
this.$.mainPages.selected = this.$.mainMenu.selected;
history.pushState(null , "title", this.$.mainMenu.selected);
}
}
You are much better off using data-binding. The idea here is to make your element model-driven, an model-view-presenter (MVP) pattern. The model is made up of properties in your element, the view is described by the template, and the presenter is in your script. Data-binding allows us to loosely couple the view from the logic.
Let's decide the current page will be controlled by a property called page.
Then we can set up our UI to be driven by the page property:
<core-menu selected="{{page}}" valueattr="id">
...
</core-menu>
<core-animated-pages class="main" selected="{{page}}" valueattr="id" transitions="slide-from-right">
This is good because we have decoupled the UI from the code. The code never talks directly to the core-animated-pages or the core-menu (notice I removed the ids). We don't listen to any events. This way we can change the UI at will without having to modify the script.
Now, we want the page to be initially selected by the URL, so we'll initialize it that way, or default to 'home'. We also want to synchronize page to back events, so we'll listen to `onpopstate'.
ready: function() {
// scrape the initial page off the window location
this.page = location.hash.slice(1) || 'home';
// listen for 'back' events
addEventListener('popstate', this.popstate.bind(this));
},
popstate: function(event) {
// comes here whether we went 'back' from code or UI
this.poppedPage = this.page = event.state;
}
We keep track of poppedPage so we can differentiate back and forward. We only want to push a new state when we go forward.
Now we need the page to be reflected in the history, so we need to push state when the page changes. As above, the one caveat is that we need to only push the state if we are going forward.
pageChanged: function() {
// if the selected page changes, push a state (unless we are going backward)
if (this.poppedPage !== this.page) {
history.pushState(this.page, "Title", '#' + this.page);
}
}
Here it is all put together:
http://jsbin.com/luwitudu/9/edit
You get two events, one for the deselection of the currently selected item and one for the new selected item. Check the event object that is passed to your callback function:
mainMenu: function(e) {
console.log(e.detail.isSelected);
console.log(e.detail.item);
}
But if you use on-core-activate your callback is only called once when a menu item is tapped by the user (it is not called when you change the menu selection programmatically.) This prevents the unwanted window history push when you change the menu selection in your program.
Then you should setup a window.onpopstate callback in which you select the (previous) menu item. If you call document.URL in you back() function, it will return the current URL, not the new one set by window.history.back(). Instead only pop from the history in this function.
This should work:
<core-menu ... on-core-activate="{{mainMenu}}">
Polymer("my-app", {
ready: function() {
var self = this;
this.$.mainMenu.selected = "home";
history.pushState(null, "Title", this.$.mainMenu.selected);
window.onpopstate = function() {
var link = document.URL.split("/");
self.$.mainMenu.selected = link[link.length-1];
self.$.mainPages.selected = self.$.mainMenu.selected;
};
},
mainMenu: function() {
this.$.mainPages.selected = this.$.mainMenu.selected;
history.pushState(null, "Title", this.$.mainMenu.selected);
},
back: function() {
window.history.back();
}
});
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'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();
}
}
...
}