I'm trying to create a vanilla JavaScript Modal that has the capability of being customized by the User, when instantiating it from the HTML file (or JS file). However, when it comes to dealing with the close() function to close the modal, instead of closing ONE modal at a time, using its close button, the close button of the FIRST modal closes ALL modals of the page. I'm not sure what I'm doing wrong...
I've researched other similar vanilla JavaScript, customizable modal libraries, but most of them use either jQuery, some framework, or include a lot of complications that I am not familiar with (I'm still a beginner). I've researched on GitHub, CodePen, Google, and on here; but I have yet to find a solution that satisfies what I need.
Since the code is quite long, I'd suggest you go directly to my CodePen account, where you can have the full code.
https://codepen.io/jdriviere/pen/zYOyJvv?editors=0011
But here is my close() function:
Modal.prototype.close = function() {
let modal = document.getElementById(this.options.id);
let modalBody = modal.children[0];
// Delete elements from Modal Body
for (let i = 0; i < modalBody.children.length; i++) {
modalBody.removeChild(modalBody.children[i]);
} // End of LOOP
// Delete Modal Body from Modal
modal.removeChild(modalBody);
// Delete Modal from DOM
modal.style.display = 'none';
document.body.removeChild(modal);
return this;
};
I would expect the code to close ONE modal at a time, and preferably the modal that has the proper ID (which should be either assigned by the User or by default have a "NoID" ID). Instead, if I close subsequent modals, it closes them; but if I close the FIRST one, it closes ALL of them. Also, is there a way to init() the Modal functionalities as soon as you create the modal instance (I hate manually initiating them)? If so, please include your solution here too, if not much asked.
Been at it for quite some time now. Your help would be greatly appreciated.
Thank you. :)
You have couple of mistakes in your code:
Always use a proper id pattern for the HTML element. You have used n/a for the modal that does not have id property in their options object. Using such id will break the query selector when you use jQuery.
Since, you are calling the init() function twice and in each call for init() the closeBtn is selecting both the close buttons of two modals and assigning the click event handler to each of them twice. That was the reason when you clicked on one button the click event for another button was executing itself. So, what you can do is, only associate a click function once only to that close button of the modal for which the init() function was called. I used let closeBtn = document.querySelector('#'+this.options.id + ' .modal-close'); to select that particular close button inside that init() function.
Overall your JS code will look like:
/**
* Blueprint function (class) that describes a Modal object.
* #param {Object} options Object parameter containing elements that describe the Modal.
* #returns {Object} options Returns options from current modal object.
*/
function Modal(options) {
// If constructor params is available
if (options) {
this.options = options;
} else {
this.options = {};
} // End of IF-ELSE
// Add to options object
if (options.id) {
// Check type of ID entry
if (typeof options.id === 'number') {
this.options.id = options.id.toString();
} else {
this.options.id = options.id;
} // End of IF-ELSE
} else if (options.id === undefined) {
this.options.id = 'NA';
} // End of IF-ELSE
if (options.name) {
this.options.name = options.name;
} // End of IF
if (options.closable) {
this.options.closable = options.closable;
} // End of IF
return this;
};
// Prototypes
/**
* Displays some information concerning the current Modal object.
* #returns {Object} this Returns current modal object.
*/
Modal.prototype.open = function() {
let demo = document.getElementById('demo');
return this;
};
/**
* Creates an instance of a Modal object with the specified object elements.
* #returns {Object} this Returns current Modal object.
*/
Modal.prototype.create = function() {
// Create Modal Element
let modal = document.createElement('div');
let modalBody = document.createElement('div');
// Create Modal
!modal.classList.contains('modal') ?
modal.classList.add('modal') :
modal.classList.add('');
modal.id = this.options.id || 'noID';
// Create modal body element
!modalBody.classList.contains('modal-body') ?
modalBody.classList.add('modal-body') :
modalBody.classList.add('');document.querySelector('#' + this.options.id + ' .modal-close');
modal.appendChild(modalBody);
// Adding modal sub-elements
if (this.options.title) {
let modalTitle = document.createElement('h2');
!modalTitle.classList.contains('modal-title') ?
modalTitle.classList.add('modal-title') :
modalTitle.classList.add('');
modalTitle.textContent = this.options.title;
modalBody.appendChild(modalTitle);
console.log('Added title!');
} // End of IF
if (this.options.subtitle) {
let modalSubtitle = document.createElement('h4');
!modalSubtitle.classList.contains('modal-subtitle') ?
modalSubtitle.classList.add('modal-subtitle') :
modalSubtitle.classList.add('');
modalSubtitle.textContent = this.options.subtitle;
modalBody.appendChild(modalSubtitle);
console.log('Added subtitle!');
} // End of IF
if (this.options.content) {
let modalContent = document.createElement('p');
!modalContent.classList.contains('modal-content') ?
modalContent.classList.add('modal-content') :
modalContent.classList.add('');
modalContent.textContent = this.options.content;
modalBody.appendChild(modalContent);
console.log('Added contents!');
} // End of IF
if (this.options.closable) {
let modalClose = document.createElement('span');
!modalClose.classList.contains('modal-close') ?
modalClose.classList.add('modal-close') :
modalClose.classList.add('');
modalClose.innerHTML = '×';
modalBody.appendChild(modalClose);
console.log('Close button added!');
} // End of IF
document.body.appendChild(modal);
console.log('Modal created with ID', modal.id);
return this;
};
/**
* Closes the current Modal object.
* #returns {Object} this Returns current Modal object.
*/
Modal.prototype.close = function() {
let modal = document.getElementById(this.options.id);
let modalBody = modal.children[0];
// Delete elements from Modal Body
for (let i = 0; i < modalBody.children.length; i++) {
modalBody.removeChild(modalBody.children[i]);
} // End of LOOP
// Delete Modal Body from Modal
modal.removeChild(modalBody);
// Delete Modal from DOM
modal.style.display = 'none';
document.body.removeChild(modal);
return this;
};
/**
* Initializes the inner functions of the modal, such as the closing capacity.
* #returns {Object} this Returns current Modal object.
*/
Modal.prototype.init = function(e) {
// let closeBtnAll = document.querySelectorAll('.modal-close');
let closeBtn = document.querySelector('#'+this.options.id + ' .modal-close');
// Assign close() function to all close buttons
closeBtn.addEventListener('click', () => {
if (this.options.closable) {
this.close();
}
})
// Press ESC to close ALL modals
return this;
};
// Create a Modal object
let modal1 = new Modal({
id: 'post1',
name: 'modal',
title: 'First Post',
subtitle: 'I contain all the elements',
content: 'This is awesome!',
closable: true
});
let modal2 = new Modal({
title: 'Second Post',
subtitle: 'Trying new things',
content: 'Hehehehehe',
closable: true
});
modal1.open();
modal1.create();
modal1.init();
modal2.open();
modal2.create();
modal2.init();
Just replace the above JS code in your codepen and try. It will work.
The problem is the init-function:
/**
* Initializes the inner functions of the modal, such as the closing capacity.
* #returns {Object} this Returns current Modal object.
*/
Modal.prototype.init = function() {
// let closeBtnAll = document.querySelectorAll('.modal-close');
let modal = document.getElementById(this.options.id);
let closeBtn = modal.querySelector('.modal-close');
// Assign close() function to all close buttons
closeBtn.addEventListener('click', () => {
console.log(this.options);
if (this.options.closable) {
this.close();
}
})
// Press ESC to close ALL modals
return this;
};
If you dont specify that you want to use the eventlistener on the current modal then it will be set to both modals.
Related
I am currently working on a dialog in order to replace the browser‘s original alert and confirm dialogs since I do not like them. My dialog is very flexible in it‘s use.
So my function is called msgBox and it has got the following syntax:
msgBox (prompt, title, buttons, modal, gradient)
All params are optional. In that case the function uses default values.
So far it works excellent already. Please see the code below.
I want to achieve now, getting as result the button that is clicked by the user to assign it to a variable or use it in an If-statement.
For example:
let response = msgBox("Load Settings?", "Confirm", ["Yes","No", "Restore defaults"]);
or
if (msgBox("Load file?", "Confirm", ["Yes","No", "Search"]) == 'Yes') {
// do some stuff
}
I want the dialog work this way, that it returns'false' in case it is non-modal and the user clicked on an area outside, otherwise it should return the clicked button as string in order to evaluate it.
Here is the code so far:
let isModal = true;
let msgBoxAnswer = null;
// #####################################################################################
// PURPOSE: Displays a modal | modeless dialog with dynamical buttons
//
// PARAMETER: - prompt = prompt text
// - titel = Titelzeie des Dialogs
// - buttons = Button-Array, sepatated by komma - Default = 'Ok'
// - modal = dialog modal | modeless - default = modal
// - gradient = optional: gradient of the titlebar
// RETURNS: : the value of the clicked button as string
// #####################################################################################
async function asyncMsgBox(prompt, title, buttons = ['Ok'], modal = true, gradient) {
// do some init stuff...
msgBoxAnswer = null;
if (prompt == null) {prompt = ''}
if (title == null || title.trim() == '') {
title = document.getElementsByTagName("title")[0].innerHTML;
}
isModal = modal;
let classnames = 'overlay';
if (modal) { classnames += ' dark'}
// search for the parent container (usually it's the <body>)
let parent = document.getElementsByTagName('body')[0];
parent.innerHTML += `<div id="msgBoxBG" class="${classnames}" onclick="closeDialog('cancel')"></div>`;
// catching an empty button-array
if (buttons[0] == '') {buttons[0] = 'Ok'}
// first create the buttons!
// otherwise the <div> is going to be closed automatically
let btnCode ='';
for (let i = 0; i < buttons.length; i++) {
const btn = buttons[i];
btnCode += `<button class="btnMsgbox" onclick="closeDialog('${btn}')">${btn}</button>`;
}
classnames = 'titlebar';
if (gradient) { classnames += ' gradient'}
let container = $('msgBoxBG');
container.innerHTML = `
<div class="dialog">
<div class ="${classnames}">
<h2 id="msgCaption">${title}</h2>
</div>
<p id="msgPrompt">${prompt}</p>
${btnCode}
</div>`;
try {
// HERE START THE PROBLEMS...
let answer = await closeDialog();
return answer; // resolve()
} catch(err){
return false; // reject()
}
}
function closeDialog(response){
return new Promise(function(resolve, reject) {
if (response) {
if (isModal && response == 'cancel') {return};
if (!isModal && response == 'cancel') {
$('msgBoxBG').remove();
reject('false');
} else if (response !='cancel') {
$('msgBoxBG').remove();
resolve(response);
} else {
return;
}
}
});
}
The problem is, that await closeDialog() finds the response-parameter 'undefined' at the first call and returns before the user is able to click on any button at all, which should call the same function.
I must add that I am not very experienced in Javascript yet. So if anyone has another suggestion or solution how to solve the problem, I am open to learn.
Thanks in advance for your help!
I'm struggling to come up with a clean solution for a design that enables a class to do something when something has happened in another class. I've been looking at Promises, callbacks and events but the application of Promises and events hasnt stuck yet.
My design issue is that I want a user to click on a button, which opens a filemanager (basically a popup with lots if images to select) and then update a thumbnail on the page when the user has selected an image from the filemanager.
I have a class that handles the controls on the main page which creates the filemanager class and opens it.
I want the main class to respond to the selected image and update the widget - I want the filemanager to just be responsible for selections not updating the main page.
So, how do I communicate between the classes so that the main class gets the selected image and does the update?
I've looked at loads of tutorials but confused on the implementation in this case.
Also - do events only relate to DOM elements. For example could I create a custom event to fire once a selection is made? confused.com
here#s the bones of my classes (removed some elements to keep brief):
export let fileImageWidgetControls = class {
/**
* sets up buttons
* sets up control actions
* opens filemanager
* handles update image
*
* */
options = {
scope: ['file', 'image'],
addBtns: {
image: '#add-image',
file: '#add-file'
},
editBtns: {
image: [
'.preview-image',
'#replace-image',
'#remove-image'
],
file: [
'.preview-file',
'#replace-file',
'#remove-file'
]
}
};
constructor() {
this.imageWidget = new updateWidget;
this.imageControls = new ImageControls(this.options);
this.initialiseControls();
this.observer();
}
openFileManager = () => {
const filemanager = new filemanagerHandler({
images:true
});
filemanager.open();
/**HOW WILL THIS CLASS KNOW THAT AN IMAGE HAS BEEN SELECTED?**/
let selectedItem = filemanager.getSelectedAsset();
}
/**
* select image via filemanager
*/
select = () => {
this.openFileManager();
};
initialiseControls = () => {
const module = this;
this.options.scope.forEach((scope) => {
//add
$(this.options.addBtns[scope]).on('click', (e, scope)=> {
e.preventDefault();
module.select(scope);
});
});
}
and here's the filemanager class (again trimmed down to relevant parts):
export let filemanagerHandler = class {
constructor({
images = false, //show image panel
files = false, //hide files panel
upload = true, //show upload panel
serverURL = '/filemanager',
baseUrl = 'http://site.s3.amazonaws.com'
} = {}) {
this.options = {
activeTabs: {
images: images,
files: files,
upload: upload
},
serverURL: serverURL,
baseURL: baseUrl
}
}
/**
* set up filemanager panels and controls
*/
init = () => {
this.imagePreviewModal = $('#imagePreview3');
this.modalSelectBtn = $('#modalSelectBtn');
this.tabClick();
this.setUpPanels(this.options.activeTabs);
this.setUpControls();
this.uploadHander = new uploadHandler;
this.observer();
this.setUpEvents();
}
open = ()=> {
colBox.init({
href: this.options.serverURL
});
colBox.colorbox();
}
/**
* set up controls within filemanager form (once loaded)
*/
setUpControls = () => {
let module = this;
$('.select-image').on('click', function (e) {
e.preventDefault();
module.handleSelection($(this), 'img');
})
}
/**
* close colorbox
* selection then handled from within bind colbox closed
* in calling class
*/
closeColorbox() {
colBox.closeColorbox();
}
setUrl = (url) => {
this.options.serverURL = url;
}
/**
* get the properties of the selected file
* from the element and updates selectedAsset attribute
* #param element (selected element)
* #param type (image or file
* #param callback
*/
handleSelection = (element, type, callback) => {
//set up selected element to be used by calling method in calling class
this.selectedAsset = {
filename: element.attr('href'),
id: element.data('id'),
caption: element.data('caption'),
type: type
}
/** HOW CAN I GET THE CALLING CLASS TO RESPOND TO THIS EVENT/SELECTION?**/
/** IVE THOUGHT ABOUT A CALLBACK HERE BUT FEELS CLUMSY ?**/
callback();
}
getSelectedAsset = () => {
return (this.selectedAsset === undefined ? false : this.selectedAsset);
}
setUpEvents = () =>{
//watch for colorbox to complete and initiate controls in the open document
$(document).bind('cbox_complete', () => {
this.init();
});
}
};
I'm trying to find a way to assign event handlers to each box that I create dynamically. At the moment the user can click "Add above" or "Add below" and 2 rows of boxes will appear wherever they clicked.
I'm trying to make it so that when the user clicks on a specific square, a colorPicker will pop up and that specific square's color can be changed.
However, my program is a bit buggy, it only works for the first square that the user clicks on and the colorPicker never pops up again.
Does anyone know how I can go about fixing this or if there is a better alternative?
My code:
http://codepen.io/anon/pen/bwBRmw
var theParent = document.querySelector(".container");
theParent.addEventListener("click", doSomething, false)
//var picker = new Picker()
function doSomething(e) {
console.log("gets inside doSomething")
console.log(e.target)
console.log(e.currentTarget)
if (e.target !== e.currentTarget) {
var clickedItem = e.target.id;
console.log("Hello " + clickedItem);
var k = document.getElementById(clickedItem)
var picker = new Picker(k)
picker.show();
}
picker.on_done = function(colour) {
$(k).css('background-color',colour.rgba().toString());
picker.hide()
}
//e.stopPropagation();
}
I noticed in your CodePen that you didn't post the full code for doSomething. You have a problem because the picker variable is local to the function. If the code execution doesn't land inside the IF-statement, the picker variable is never created. Simply uncomment the code declaring the picker variable outside the function, and remove the var directive from in front of the line of code where you instantiate a new picker. Furthermore, you need to reset the "parent" element of the picker, since there is only one picker on the page: picker.settings.parent = k;
var picker = null; // Initialize global variable
function doSomething(e) {
console.log("gets inside doSomething")
console.log(e.target)
console.log(e.currentTarget)
if (e.target !== e.currentTarget) {
var clickedItem = e.target.id;
console.log("Hello " + clickedItem);
var k = document.getElementById(clickedItem)
// Reference the global "picker" variable
if (!picker) {
picker = new Picker(k)
} else {
// Set the "parent" element of the picker to the newly clicked element
picker.settings.parent = k;
}
picker.show();
}
picker.on_done = function(colour) {
$(k).css('background-color',colour.rgba().toString());
picker.hide()
}
//e.stopPropagation();
}
I created an object with map Areas by using DOM-Elements. Now I want to execute a function when clicked on an area(coordinates). I think I also Need to check the area if it is clicked or not ? Also I think I'm missing something on the DOM-Element.
var _images = { //Object-MapArea
start: {
path: 'start.jpg',
areas: [{
coords: '18,131,113,140',
text: 'Homepage',
onclick: doSomething, // ???
}]
},
}
// ..... there is more code not shown here
//Creating DOM-Elements
var areaElem = document.createElement('AREA');
// Set attributes
areaElem.coords = area.coords;
areaElem.setAttribute('link', area.goto);
areaElem.setAttribute("title", area.text);
areaElem.setAttribute("shape", "rect");
areaElem.onclick = doSomething; ? ? ?
if (element.captureEvents) element.captureEvents(Event.CLICK); ? ? ?
areaElem.addEventListener('click', onArea_click);
_map.appendChild(areaElem);
function doSomething(e) {
if (!e) var e = window.event
e.target = ... ? ?
var r = confirm("Data will get lost!");
if (r == true) {
window.open("homepage.html", "_parent");
} else {
window.open(" ", "_parent"); // here I want to stay in the current pictures. I mean the current object map area. But how can I refer to it so it stays there ?
}
}
See the comments I added to your code ;)
var _images = { //Object-MapArea
start: {
path: 'start.jpg',
areas: [{
coords: '18,131,113,140',
text: 'Homepage',
clickHandler: doSomething // I changed the name to avoid confusion but you can keep onclick
}]
},
}
// ..... there is more code not shown here
//Creating DOM-Elements
var areaElem = document.createElement('AREA');
// Set attributes
areaElem.coords = area.coords;
areaElem.setAttribute('link', area.goto);
areaElem.setAttribute("title", area.text);
areaElem.setAttribute("shape", "rect");
// Add a listener on click event (using the function you defined in the area object above)
areaElem.addEventListener('click', area.clickHandler);
// Add your other click handler
areaElem.addEventListener('click', onArea_click);
_map.appendChild(areaElem);
function doSomething(e) {
// Get the area clicked
var areaClicked = e.target;
// I dont understand the following code...
var r = confirm("Data will get lost!");
if (r == true) {
window.open("homepage.html", "_parent");
} else {
window.open(" ", "_parent"); // here I want to stay in the current pictures. I mean the current object map area. But how can I refer to it so it stays there ?
}
}
Hope it helps ;)
I think I also Need to check the area if it is clicked or not
You are creating an element areaElem and attaching event to the same dom.
So there may not be need of any more check
You may not need to add both onclick & addEventListner on same element and for same event. Either onclick function or addEventListener will be sufficient to handle the event.
areaElem.onclick = function(event){
// Rest of the code
// put an alert to validate if this function is responding onclick
}
areaElem.addEventListener('click', onArea_click);
function onArea_click(){
// Rest of the code
}
I am using a simple jQuery plugin to style my javascript alerts. The problem being the usual methods for adding new lines does not appear to work. Here is the javascript for the plugin, any ideas?
(function($) {
$.fn.customAlert = function(options) {
var settings = {
'alertTitle' : 'Notice!',
'alertOk' : 'OK',
'alertClose' : 'x',
'draggable' : false
};
if (options) $.extend(settings, options);
if(document.getElementById) {
window.defaultAlert = window.alert;
window.alert = function(msgTxt) {
if ($('#modalDiv').length > 0) return; // Only ever show one alert
// The modal div to block out the rest of the document whilst the alert is shown
var modalDiv = $('<div></div>');
modalDiv.attr('id', 'modalDiv');
modalDiv.height($(document).height()); // Make overlay cover the whole window
// The alert container
var alertDiv = $('<div></div>');
alertDiv.attr('id', 'alertDiv');
// The alert title
var titleH1 = $('<h1></h1>');
titleH1.addClass('titleH1');
titleH1.text(settings.alertTitle);
// The alert text to display
var msgP = $('<p></p>');
msgP.text(msgTxt);
// OK button - will remove/close the alert on click
var okBtn = $('<a></a>');
okBtn.addClass('okBtn');
okBtn.text(settings.alertOk);
okBtn.attr('href', '#');
// X button - will remove/close the alert on click
var closeBtn = $('<span></span>');
closeBtn.addClass('alert-close');
closeBtn.text(settings.alertClose);
// Append elements to document body
alertDiv.append(titleH1);
alertDiv.append(msgP);
alertDiv.append(okBtn);
alertDiv.append(closeBtn);
$('body').append(modalDiv);
$('body').append(alertDiv);
// Center alert on page
$('#alertDiv').css('top', ($(window).height()/2) - ($('#alertDiv').height()/2)+'px');
$('#alertDiv').css('left', ($(window).width()/2) - ($('#alertDiv').width()/2)+'px');
// Make draggable
if (settings.draggable && $('#alertDiv').draggable) {
$('#alertDiv').draggable({
handle: 'h1',
opacity: 0.4
});
$('#alertDiv h1').css('cursor', 'move');
}
// Bind OK button to remove/close alert
$('#alertDiv .okBtn, #alertDiv .alert-close').bind('click', function(e) {
$('#alertDiv').remove();
$('#modalDiv').remove();
e.preventDefault();
});
};
}
};
})(jQuery);
I am guessing its a case of in this above finding /n and adding a <br> or </p><p>. I am unsure how to do that if even possible though.
Change this line:
msgP.text(msgTxt);
to
msgP.html(msgTxt);
and I think then you can use <br /> and other html tags.