I'm trying to create a $ionicPopup where one of the buttons is disabled under certain conditions (being the return value of a function, let's call it MyFunction()). I want to use ng-disabled for this purpose.
The problem is, I don't know how to programmatically add the attribute "ng-disabled".
What I tried so far:
Adding the attribute when creating the popup, like attr:"ng-disabled='myFunction()'"
Adding the attribute after the popup was created, using JavaScript => The problem is that the setAttribute() method is executed before the popup is actually shown, so I would need a way to detect when the popup is open, and execute the method only then.
Creating the button as html elements inside the popup template, and not setting any button with the $ionicPopup.show() method. This works, but I'm not satisfied with it because I don't want to "reinvent the wheel" and redefine CSS styles for buttons that are already covered by Ionic framework.
My JS function:
$scope.displayPopUp=function(){
var alertPopup = $ionicPopup.show({
templateUrl: 'sharePopUp.html',
title: 'Invite a friend',
cssClass: 'popupShare',
buttons:[
{
text:'Close',
type: 'button-round button-no',
onTap: function(){
/* Some instructions here */
}
},
{ /* v THIS IS THE BUTTON I WANT TO DISABLE UNDER CERTAIN CONDITIONS v */
text:'Share',
type: 'button-round button-yes',
onTap: function(){
/* Some instructions here */
}
}
]
});
$(".button-yes")[0].setAttribute("ng-disabled", "MyFunction()"); /* NOT WORKING BECAUSE button-yes IS NOT EXISTING YET */
}
TL;DR
$timeout(function () { // wait 'till the button exists
const elem = $('.button-yes')[0];
elem.setAttribute('ng-disabled', 'MyFunction()'); // set the attribute
$compile(elem)(angular.element(elem).scope()); // Angular-ify the new attribute
});
Live demo: working plunk
Introduction
That problem you're encountering, it's a real one, and it has apparently been for years.
Here's the latest version of the code used by $ionicPopup (last updated in December 2015)
This template is the one used by your Ionic-1 popups (from the first lines of the code linked above):
var POPUP_TPL =
'<div class="popup-container" ng-class="cssClass">' +
'<div class="popup">' +
'<div class="popup-head">' +
'<h3 class="popup-title" ng-bind-html="title"></h3>' +
'<h5 class="popup-sub-title" ng-bind-html="subTitle" ng-if="subTitle"></h5>' +
'</div>' +
'<div class="popup-body">' +
'</div>' +
'<div class="popup-buttons" ng-show="buttons.length">' +
'<button ng-repeat="button in buttons" ng-click="$buttonTapped(button, $event)" class="button" ng-class="button.type || \'button-default\'" ng-bind-html="button.text"></button>' +
'</div>' +
'</div>' +
'</div>';
There's one line in particular that's interesting to us: the button template:
<button ng-repeat="button in buttons" ng-click="$buttonTapped(button, $event)" class="button" ng-class="button.type || \'button-default\'" ng-bind-html="button.text"></button>
As you can see, there's just no built-in way to alter its button's attributes.
Two approaches
From here, you've got two fixes:
We can contribute to their project on GitHub, implement the missing functionality, write the tests for it, document it, submit an issue, a Pull Request, ask for a newer version to be released and use the newer version.
This is the ideal solution, 'cause it fixes everyone's problems forever. Although, it does take some time. Maybe I'll do it. Feel free to do it yourself though, and tag me, I'll +1 your PR 👍
Write a dirty piece of code that monkey-patches your specific problem in your specific case
This isn't ideal, but it can be working right now.
I will explore and expand on the (quick 'n dirty) option #2 below.
The fix
Of the 3 things you've tried so far:
the first one is simply not a thing (although it could be if we implement it, test it, document it and release it)
the third one is rather unmaintainable (as you know)
That leaves us with the second thing you mentioned:
Adding the attribute after the popup was created, using JavaScript
The problem is that the setAttribute() method is executed before the popup is actually shown, so I would need a way to detect when the popup is open, and execute the method only then.
You're right, but that's only part one of a two-fold problem.
Part 1: The button isn't created yet
Actually, you can delay that call to setAttribute to later, when the popup is shown. You wouldn't wanna delay it by any longer than would be noticeable by a human, so you can't reasonably go for anything longer than 20ms.
Would there be some callback when the popup is ready, we could use that, but there isn't.
Anyways, I'm just teasing you: JavaScript's "multi-tasking" comes into play here and you can delay it by 0 millisecond! 😎
In essence, it has to do with the way JS queues what it has to do. Delaying the execution of a piece of code by 0ms puts it at the end of the queue of things to be done "right away".
Just use:
setTimeout(function () {
$(".button-yes")[0].setAttribute("ng-disabled", "MyFunction()");
}, 0); // <-- 0, that's right
And you're all set!
Well, you do have a button whose ng-disabled attribute indeed is "MyFunction()". But it's not doing anything...
So far, you simply have an HTML element with an attribute that doesn't do anything for a simple HTML button: Angular hasn't sunk its teeth into your new DOM and hooked itself in there.
Part 2: Angular isn't aware of the new attribute
There's a lot to read here about this, but it boils down to the following: Angular needs to compile your DOM elements so that it sets things in motion according to your Angular-specific attributes.
Angular simply hasn't been made aware that there's a new attribute to your button, or that it should even concern itself with it.
To tell Angular to re-compile your component, you use the (conveniently named) $compile service.
It will need the element to compile, as well as an Angular $scope to compile it against (for instance, MyFunction probably doesn't exist in your $rootScope).
Use it once, like so:
$compile(/* the button */ elem)(/* the scope */ scope);
Assuming the following element is your button:
const elem = $(".button-yes")[0];
... you get its actual scope through its corresponding Angular-decorated element thingy:
const scope = angular.element(elem).scope();
So, basically:
const elem = $('.button-yes')[0];
elem.setAttribute('ng-disabled', 'MyFunction()');
$compile(elem)(angular.element(elem).scope());
Tadaaa! That's it! 🎉
... sort of. Until there's some user interaction that would alter the corresponding $scope, the button is actually not even displayed.
Bonus Part: Avoid $scope.$apply() or $scope.$digest()
Angular isn't actually magically picking up things changing and bubbling it all to the right places. Sometimes, it needs to explicitly be told to have a look around and see if the elements are in sync with their $scope.
Well, more specifically, any change that happens asynchronously won't be picked up by itself: typically, I'm talking about AJAX calls and setTimeout-delayed functions. The methods that are used to tell Angular to synchronise scopes and elements are $scope.$apply and $scope.$digest... and we should thrive on avoiding them :)
Again, there's lots of reading out there about that. In the meantime, there's an Angular service (again), that can (conceptually, it's not the literal implementation) wrap all your asynchronous code into a $scope.$apply() -- I'm talking about $timeout.
Use $timeout instead of setTimeout when you will change things that should alter your DOM!
Summing it all up:
$timeout(function () { // wait 'till the button exists
const elem = $('.button-yes')[0];
elem.setAttribute('ng-disabled', 'MyFunction()'); // set the attribute
$compile(elem)(angular.element(elem).scope()); // Angular-ify the new attribute
});
Live demo: working plunk
I think in ionic v1 Ionic Framework team have not implemented this yet as per (Oct 6, '14 10:49 PM). I think still situation is same. But there is a work around for that.
Option 1:
What I understand from your question, your main purpose is to prevent user to click on buttonDelete ionicPopup buttons and perform some instructions until MyFunction() returns truecreate your own template with buttons which you can fully control them. Below is code:
You can achieve this inside onTap :. Here you can add condition of your MyFunction() like below:
JavaScript:
// Triggered on a button click, or some other target
$scope.showPopup = function() {
// Enable/disable text"Share" button based on the condition
$scope.MyFunction = function() {
return true;
};
//custom popup
var myPopup = $ionicPopup.show({
templateUrl: 'Share'"popup-template.html",
typetitle: 'button-round"Invite button-yes'a friend",
onTapscope: function(e)$scope
{ });
// close popup on Cancel ifbutton (MyFunctionclick
$scope.closePopup = function()) {
myPopup.close();
};
};
HTML:
/*<button Someclass="button instructionsbutton-dark" hereng-click="showPopup()">
*/ show
</button>
}<script elseid="popup-template.html" {type="text/ng-template">
<p>Share button is disabled if condition not /satisfied</don'tp>
allow the user to<button performclass="button unlessbutton-dark" MyFunctionng-click="closePopup()"> returns
true Cancel
</button>
e.preventDefault<button class="button button-dark" ng-disabled="MyFunction(); == true">
}Share
}</button>
}</script>
Working example here Here is working codepen snippet:
https://codepen.io/anon/pen/bvXXKG?editors=1011
Option 2:
Delete ionicPopup buttons and create your own template with buttons which you can fully control them. Below is code:
JavaScript:
// Triggered on a button click, or some other target
$scope.showPopup = function() {
// Enable/disable "Share" button based on the condition
$scope.MyFunction = function() {
return true;
};
//custom popup
var myPopup = $ionicPopup.show({
templateUrl: "popup-template.html",
title: "Invite a friend",
scope: $scope
});
// close popup on Cancel button click
$scope.closePopup = function() {
myPopup.close();
};
};
HTML:
<button class="button button-dark" ng-click="showPopup()">
show
</button>
<script id="popup-template.html" type="text/ng-template">
<p>Share button is disabled if condition not satisfied</p>
<button class="button button-dark" ng-click="closePopup()">
Close
</button>
<button class="button button-dark" ng-disabled="MyFunction() == true">
Share
</button>
</script>
Here is working codepen snippet:
https://codepen.io/anon/pen/qYEWmY?editors=1010
Note: Apply your own styles/button's alignment etc
I hope it will help you.
Related
I have the following button positioned absolutely over a canvas element:
<button class="left">Left</button>
This is the JavaScript that is supposed to respond to button click:
$(".left").mousedown(function() {
leftTimer = setTimeout(moveLeft, 10);
}).mouseup(function() {
clearTimeout(leftTimer);
});
var leftTimer,
moveLeft = function() {
if (circleX > 0) {
circleX -= 4;
}
$("h1.title").text('Clicked Left');
leftTimer = setTimeout(moveLeft, 10);
};
I added the title line to check if there was a delay. When I click the button normally, nothing happens the title does not change. I have to keep the button pressed for some time to change the text to "Clicked Left" or to move my target element. Even then there is a considerable delay between the time I first touch the button and the time the text changes. Why is that ? Let me know if I need to provide more information.
You should actually have Angular with your Ionic app, and use the ng-click element on the div you want to handle.
According to the documentation, AngularJS is required :
Ionic currently requires AngularJS in order to work at its full potential. While you can still use the CSS portion of the framework, you'll miss out on powerful UI interactions, gestures, animations, and other things.
Here is a JSBin, to make you a basic binding between your HTML and your AngularJS : https://jsbin.com/rifigakube/edit?html,js,output
If you are new to this technology here is a list to get started with AngularJS (it's pretty easy to learn if you know Javascript) :
https://angularjs.org/
https://docs.angularjs.org/tutorial/step_00
http://www.w3schools.com/angular/
https://thinkster.io/a-better-way-to-learn-angularjs
https://egghead.io/technologies/angularjs
And finally, when you think you're confident enough with AngularJS : https://scotch.io/tutorials/create-your-first-mobile-app-with-angularjs-and-ionic
(I really suggest you learn AngularJS before doing the last one, or you will get stuck after)
Hope this helps.
I am extending a cloud-hosted LMS with javascript. Therefore, we can add javascript to the page, but cannot modify the vendor javascript for different components.
The LMS uses tinyMCE frequently. The goal is to add a new button on to the toolbar of each tinyMCE editor.
The problem is that since the tinyMCE modules are initialized in the vendor's untouchable code, we cannot modify the init() call. Therefore, we cannot add any text on to the "toolbar" property of the init() object.
So I accomplished this in a moderately hacky way:
tinyMCE.on('AddEditor', function(e){
e.editor.on('init', function(){
tinyMCE.ui.Factory.create({
type: 'button',
icon: 'icon'
}).on('click', function(){
// button pressing logic
})
.renderTo($(e.editor.editorContainer).find('.mce-container-body .mce-toolbar:last .mce-btn-group > div')[0])
});
});
So this works, but needless to say I am not totally comfortable having to look for such a specific location in the DOM like that to insert the button. Although this works, I do not believe it was the creator's intention for it to be used like this.
Is there a proper way to add the button to a toolbar, after initialization, if we cannot modify the initialization code?
I found a more elegant solution, but it still feels a bit like a hack. Here is what I got:
// get an instance of the editor
var editor=tinymce.activeEditor; //or tinymce.editors[0], or loop, whatever
//add a button to the editor buttons
editor.addButton('mysecondbutton', {
text: 'My second button',
icon: false,
onclick: function () {
editor.insertContent(' <b>It\'s my second button!</b> ');
}
});
//the button now becomes
var button=editor.buttons['mysecondbutton'];
//find the buttongroup in the toolbar found in the panel of the theme
var bg=editor.theme.panel.find('toolbar buttongroup')[0];
//without this, the buttons look weird after that
bg._lastRepaintRect=bg._layoutRect;
//append the button to the group
bg.append(button);
I feel like there should be something better than this, but I didn't find it.
Other notes:
the ugly _lastRepaintRect is needed because of the repaint
method, which makes the buttons look ugly regardless if you add new
controls or not
looked in the code, there is no way of adding new controls to the
toolbar without repainting and there is no way to get around it
without the ugly hack
append(b) is equivalent to add(b).renderNew()
you can use the following code to add the button without the hack, but you are shortcircuiting a lot of other stuff:
Code:
bg.add(button);
var buttonElement=bg.items().filter(function(i) { return i.settings.text==button.text; })[0];
var bgElement=bg.getEl('body');
buttonElement.renderTo(bgElement);
I've been trying out the excellent Medium Editor. The problem that I've been having is that I can't seem to get links to "work".
At the simplest, here's some HTML/JS to use to demonstrate the problem:
HTML:
<html>
<head>
<script src="//cdn.jsdelivr.net/medium-editor/latest/js/medium-editor.min.js"></script>
<link rel="stylesheet" href="//cdn.jsdelivr.net/medium-editor/latest/css/medium-editor.min.css" type="text/css" media="screen" charset="utf-8">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/medium-editor/latest/css/themes/beagle.min.css" type="text/css">
</head>
<body>
<div class='editable'>
Hello world. link
</div>
</body>
</html>
Javascript:
var editor = new MediumEditor('.editable');
This fiddle demonstrates the problem (using the code above).
If you hover on the link, a popup appears.
If you click the link, nothing happens.
If you click the popup, a form appears where you can edit the link.
It seems to me that clicking the link should take me wherever the link's href is targeting. The only way to use the link is to right click and either open in a new tab or new window -- which I don't want to ask my users to do.
I feel like I must be missing something simple in the configuration (either the Anchor Preview Options or the Anchor Form Options). Unfortunately, I'm not seeing it.
In my actual application, I'm not using jQuery, but I am using angularjs. If a strictly Medium Editor answer doesn't exist, I can fall back to using basic JS or anything that angularjs provides.
I've found how to bind event.
Here is full event list https://github.com/yabwe/medium-editor/blob/master/CUSTOM-EVENTS.md
Try to change your code to
var editor = new MediumEditor('.editable')
.subscribe("editableClick", function(e){if (e.target.href) {window.open(e.target.href)}})
https://jsfiddle.net/fhr18gm1/
So medium-editor is built on top of the built-in browser support for contenteditable elements. When you instantiate medium-editor, it will add the contenteditable=true attribute to whatever element(s) you provided it.
By default, since the text is now editable (the contenteditable attribute makes the browser treat it as WYSIWYG text) the browser no longer supports clicking on the links to navigate. So, medium-editor is not blocking these link clicks from happening, the browsers do it inherently as part of making the text editable.
medium-editor has built in extensions for interacting with links:
anchor extension
allows for adding/removing links
anchor-preview extension
shows a tooltip when hovering a link
when the tooltip is clicked, allows for editing the href of the link via the anchor extension
I think the underlying goal of the editor is the misunderstanding here. The editor allows for editing text, and in order to add/remove/update links, you need to be able to click into it without automatically navigating away. This is what I think of as 'edit' mode.
However, the html produced as a result of editing is valid html, and if you take that html and put it inside an element that does NOT have the contenteditable=true attribute, everything will work as expected. I think of this as 'publish mode'
I look at editors like word or google docs, and you see a similar kind of behavior where when you edit the document, the links don't just navigate away when you click on them, you have to actually choose to navigate them through a separate action after you click the link. However, on a 'published' version of the document, clicking the link will actually open a browser window and navigate there.
I think this does make for a good suggestion as an enhancement to the existing anchor-preview extension. Perhaps the tooltip that appears when hovering a link could have multiple options in it (ie Edit Link | Remove Link | Navigate to URL).
tldr;
Links are not navigable on click when 'editing' text in a browser via the built-in WYSIWYG support (contenteditable). When not in 'edit' mode, the links will work as expected.
This could make for a nice enhancement to the medium-editor anchor-preview extension.
Working off some ideas from #Valijon in the comments, I was able to get it to work using the following code:
var iElement = angular.element(mediumEditorElement);
iElement.on('click', function(event) {
if (
event.target && event.target.tagName == 'A' &&
event.target.href && !event.defaultPrevented) {
$window.open(event.target.href, '_blank');
}
});
I think the key is that apparently the editor lets the event propogate to the ancestor elements, so I was able to just listen for the click on the top level editor element.
Here, $window is angular's $window service -- If you're not using angularjs, window would do the trick and I used angular.element to ease the event listener registry, but you could do it the old-fashioned way (or using the JS framework of your choice).
What I really wanted when I asked the question was behavior similar to Google Docs when in "edit" mode (as described by Nate Mielnik). I opened an issue on the Medium Editor tracker and they decided not to implement it as part of the core medium editor, but they noted that they would be happy to have someone add that functionality as an extension.
So, I decided to implement that functionality as an extension as suggested. It can be found as part of MediumTools1. The project is still in very early stages (e.g. I haven't done anything to make the styling look better, or to use better minifying practices, etc. but we'll happily accept Pull Requests for that).
The guts of the code look like this:
var ClassName = {
INNER: 'medium-editor-toolbar-anchor-preview-inner',
INNER_CHANGE: 'medium-editor-toolbar-anchor-preview-inner-change',
INNER_REMOVE: 'medium-editor-toolbar-anchor-preview-inner-remove'
}
var AnchorPreview = MediumEditor.extensions.anchorPreview;
GdocMediumAnchorPreview = MediumEditor.Extension.extend.call(
AnchorPreview, {
/** #override */
getTemplate: function () {
return '<div class="medium-editor-toolbar-anchor-preview">' +
' <a class="' + ClassName.INNER + '"></a>' +
' -' +
' <a class="' + ClassName.INNER_CHANGE + '">Change</a>' +
' |' +
' <a class="' + ClassName.INNER_REMOVE + '">Remove</a>' +
'</div>';
},
/** #override */
createPreview: function () {
var el = this.document.createElement('div');
el.id = 'medium-editor-anchor-preview-' + this.getEditorId();
el.className = 'medium-editor-anchor-preview';
el.innerHTML = this.getTemplate();
var targetBlank =
this.getEditorOption('targetBlank') ||
this.getEditorOption('gdocAnchorTargetBlank');
if (targetBlank) {
el.querySelector('.' + ClassName.INNER).target = '_blank';
}
var changeEl = el.querySelector('.' + ClassName.INNER_CHANGE);
this.on(changeEl, 'click', this.handleClick.bind(this));
var unlinkEl = el.querySelector('.' + ClassName.INNER_REMOVE);
this.on(unlinkEl, 'click', this.handleUnlink.bind(this));
return el;
},
/** Unlink the currently active anchor. */
handleUnlink: function() {
var activeAnchor = this.activeAnchor;
if (activeAnchor) {
this.activeAnchor.outerHTML = this.activeAnchor.innerHTML;
this.hidePreview();
}
}
});
As an explanation, I just use medium's flavor of prototypical inheritance to "subclass" the original/builtin AnchorPreview extension. I override the getTemplate method to add the additional links into the markup. Then I borrowed a lot from the base implementation of getPreview, but I bound new actions to each of the links as appropriate. Finally, I needed to have an action for "unlinking" the link when "Remove" is clicked, so I added a method for that. The unlink method could probably be done a little better using contenteditable magic (to make sure that it is part of the browser's undo stack), but I didn't spend the time to figure that out (though it would make a good Pull Request for anyone interested :-).
1Currently, it's the only part, but I hope that'll change at some point. . .
Here is my HTML entry that fires the GenerateBill() Javascript at the moment :
<a class="btn btn-primary" id="loading-example-btn" data-loading-text="Loading..." onclick="GenerateBill()">Generate Bill</a>
Here is the GenerateBill() method, this all works fine, all I want to do is add the button state feedback
function GenerateBill() {
var url = '/PremiseProvider/GenerateBill';
var data = {
StartDate: $('#from').val(),
EndDate: $('#to').val(),
premiseProviderId: $('#PremiseProviderId').val()
};
$("body").load(url, data);
};
Here is a code snippet from the Bootstrap 3 official Site on how to implement the button state feedback:
<script>
$('#loading-example-btn').click(function () {
var btn = $(this)
btn.button('loading')
$.ajax(...).always(function () {
btn.button('reset')
});
});
</script>
My Question is how can I implement in my GenerateBill script, the bootstrap example uses an Ajax call, can I make it work without making too many changes to what I have?
If I might make a few suggestions that will both fix your issue and improve your code.
Instead of using an onclick event, add an event listener in your javascript, and call the function from there.
Add the .button('loading') call to that same event listener.
Don't leave off the href for an <a> tag. It will cause some browsers to not show the pointer correctly on hover.
Your link will look as follows:
Generate Bill
Leaving your GenerateBill() logic alone, the listener you need to add to your javascript:
$('#loading-example-btn').click(function () {
$(this).button('loading');
GenerateBill();
});
A working example of this code (with GenerateBill() simplified) is available here: http://www.bootply.com/VTSNA1XMcm
When using tinyMCE in a jqueryUI modal dialog, I can't use the hyperlink or 'insert image' features.
Basically, after lots of searching, I've found this:
http://www.tinymce.com/develop/bugtracker_view.php?id=5917
The weird thing is that to me it seams less of a tinyMCE issue and more of a jqueryUI issue since the problem is not present when jqueryUI's modal property is set to false.
With a richer form I saw that what happens is that whenever the tinyMCE loses focus, the first element in the form gets focus even if it's not the one focused / clicked.
Does some JavaScript guru have any idea how I might be able to keep the dialog modal and make tinyMCE work?
This fixed it for me when overriding _allowInteraction would not:
$(document).on('focusin', function(e) {
if ($(event.target).closest(".mce-window").length) {
e.stopImmediatePropagation();
}
});
I can't really take credit for it. I got it from this thread on the TinyMCE forums.
(They have moved their bugtracker to github. tinymce/issues/703 is the corresponding github issue.)
It seems there are no propper solution for this issue yet. This is kind of a hack but it really worked for me.
Every time you open the Dialog remove the text area and re add it like following,
var myDialog = $('#myDialog');
var myTextarea = myDialog.find('textarea');
var clonedTextArea = myTextarea.clone(); // create a copy before deleting from the DOM
var myTextAreaParent = myTextarea.parent(); // get the parent to add the created copy later
myTextarea.remove(); // remove the textarea
myDialog.find('.mce-container').remove(); // remove existing mce control if exists
myTextAreaParent.append(clonedTextArea); // re-add the copy
myDialog.dialog({
open: function(e1,e2){
setTimeout(function () {
// Add your tinymce creation code here
},50);
}
});
myDialog.dialog('open');
This seems to fix it for me, or at least work around it (put it somewhere in your $(document).ready()):
$.widget('ui.dialog', $.ui.dialog, {
_allowInteraction: function(event) {
return ($('.mce-panel:visible').length > 0);
}
});