Skip to content

Instantly share code, notes, and snippets.

@Romiko
Last active August 29, 2015 14:20
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Romiko/84bfd02cf4a087738d54 to your computer and use it in GitHub Desktop.
Save Romiko/84bfd02cf4a087738d54 to your computer and use it in GitHub Desktop.
Modal Dialog Box
/*global Modernizr, console */
if (!window.CS) {
window.CS = {};
}
if (!window.CS.Components) {
window.CS.Components = {};
}
(function ($) {
var name = 'modalDialog',
hook = '.js-modal-dialog',
nextId = 0;
/**
* Modal dialog
* http://campmon.com/wiki/display/FG/Modal+dialog+component
*
* @param {string=|object=} component - Component selector, element or JQuery object. Defaults to '.js-modal-dialog'.
* @param {object=|boolean=} options - Options for the component, or true to obtain options from the 'data-cs-<option>' attributes.
* @param {boolean=} options.allowclose - If true, 'Esc' key can be used to close the dialog.
* @param {string=|object=} options.callback - Callback function on dialog hide, executed in global scope. Will be passed the component object.
* @param {string=|object=} options.siblingWrapper - HTML structure to wrap the content adjacent to the dialog. If undefined, wrapping will not be performed.
*/
$[name] = function (component, options) {
this.$component = $(component || hook);
this.id = this.$component.attr('id') || nextId++;
this.debug('Binding', this.$component);
this.$prevTrigger = $('.js-modal-dialog-pagination-prev', this.$component);
this.$nextTrigger = $('.js-modal-dialog-pagination-next', this.$component);
this.$closePageTrigger = $('.js-modal-dialog-pagination-close', this.$component);
this.$closeTrigger = $('.js-modal-dialog-close', this.$component);
this.$featureTarget = $('.js-modal-dialog-feature-target', this.$component);
this.$featureItems = $('.js-modal-dialog-feature-item', this.$component);
this.$callToAction = $('.js-modal-dialog-call-to-action', this.$component);
this.$overlay = this.$component.siblings('.js-modal-dialog-overlay');
this.$siblingContent = this.$component.nextAll().not(this.$overlay);
this.existingFocus = undefined;
this.currentFeature = undefined;
this.init(options);
};
$[name].defaults = {
allowclose: false,
callback: undefined,
siblingWrapper: '<div aria-hidden="false" />'
};
$[name].prototype = {
init: function (options) {
var that = this,
isWebkit = 'WebkitAppearance' in document.documentElement.style;
if (options === true) {
options = this.getOptions(this.$component);
}
this.setOptions(options);
// Cross-browser/markup tweaks...
if (isWebkit) {
document.documentElement.className += " vendor-webkit";
}
if (!Modernizr.csstransforms || isWebkit) {
this.$component[0].style.marginTop = '-' + (this.$component[0].offsetHeight / 2) + 'px';
this.$component[0].style.marginLeft = '-' + (this.$component[0].offsetWidth / 2) + 'px';
}
if (this.options.siblingWrapper) {
this.$siblingContent.wrapAll(this.options.siblingWrapper);
}
// Event handlers...
if (this.$prevTrigger != undefined)
this.$prevTrigger.click(function (e) {
that.setFeature(that.currentFeature - 1);
return false;
});
if (this.$nextTrigger != undefined)
this.$nextTrigger.click(function (e) {
that.setFeature(that.currentFeature + 1);
return false;
});
if (this.$closeTrigger != undefined)
this.$closeTrigger.click(function (e) {
that.hide();
return false;
});
if (this.$closePageTrigger != undefined)
this.$closePageTrigger.click(function (e) {
that.hide();
return false;
});
if (this.$closeNoCallback != undefined)
this.$closeNoCallback.click(function (e) {
that.hide(true);
return false;
});
if (this.$callToAction != undefined)
this.$callToAction.click(function (e) {
var noCallback = false;
var element = $(e.currentTarget);
if (element.hasClass('js-no-callback')) {
noCallback = true;
}
that.hide(noCallback);
if (element.attr('href') != undefined && element.attr('href').length > 0) {
window.location = e.currentTarget.href;
}
return false;
});
this.$component.keydown(function (e) {
if (e.which === 9) {
// Tab - trapped to prevent navigation outside the modal.
var $tabbable = that.$component.find(':tabbable');
var focused = $tabbable.index(document.activeElement);
focused = (focused + (e.shiftKey ? -1 : 1)) % $tabbable.length;
if (focused < 0) {
focused += $tabbable.length;
}
$tabbable[focused].focus();
return false;
} else if (e.which === 37) {
// Left arrow - previous feature.
if (that.$prevTrigger.hasClass('is-visible')) {
that.$prevTrigger.click();
return false;
}
} else if (e.which === 39) {
// Right arrow - next feature.
if (that.$nextTrigger.hasClass('is-visible')) {
that.$nextTrigger.click();
return false;
}
} else if (e.which === 27) {
// Esc - close dialog.
if (that.options.allowclose) {
that.hide();
}
return false;
}
return true;
});
this.$component.click(function (e) {
// Keep focus in dialog.
that.update();
return false;
});
this.$overlay.click(function (e) {
// Keep focus in dialog.
var element = $(e.currentTarget);
if (element.hasClass('js-no-modal-focus')) {
that.hide();
} else {
that.update();
}
return false;
});
// Start with the first feature.
this.setFeature(0);
},
// Get options from the 'data-cs-<option>' attributes.
getOptions: function ($component) {
var options = {};
$.each($component.get(0).attributes, function (index, attr) {
if (attr.name.match(/^data-cs-/)) {
options[attr.name.replace(/^data-cs-/, '')] = attr.value;
}
});
return options;
},
setOptions: function (options) {
this.options = $.extend(true, $[name].defaults, options);
this.debug('Options', this.options);
},
setFeature: function (i) {
this.currentFeature = i;
this.update();
},
// Update dialog presentation.
update: function () {
this.debug('Current feature', this.currentFeature);
if (this.$featureTarget[0]) {
this.$featureTarget[0].setAttribute('data-visible-feature-item', this.currentFeature + 1);
}
if (this.currentFeature < this.$featureItems.length - 1) {
// More features to get to...
this.$closePageTrigger.removeClass('is-visible');
this.$nextTrigger.addClass('is-visible').focus();
} else {
// At the last feature.
this.$nextTrigger.removeClass('is-visible');
if (this.$closePageTrigger.length > 0) {
this.$closePageTrigger.addClass('is-visible').focus();
} else if (this.$closeTrigger.length > 0) {
this.$closeTrigger.addClass('is-visible').focus();
}
}
if (this.currentFeature > 0 && this.$featureItems.length > 1) {
this.$prevTrigger.addClass('is-visible');
} else {
this.$prevTrigger.removeClass('is-visible');
}
},
show: function () {
var that = this;
this.debug('Show dialog');
this.existingFocus = document.activeElement;
if (this.options.siblingWrapper) {
this.$siblingContent.parent().attr('aria-hidden', 'true');
}
if (Modernizr.csstransitions) {
// Set focus after animation finishes.
this.$component.one("transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd", function () {
that.update();
});
}
this.$component.addClass('is-visible');
this.$component.attr('aria-hidden', 'false');
if (!Modernizr.csstransitions) {
// No animation, set the focus right away.
this.update();
}
},
hide: function (nocallback) {
this.debug('Hide dialog');
this.$component.removeClass('is-visible');
this.$component.attr('aria-hidden', 'true');
if (this.options.siblingWrapper) {
this.$siblingContent.parent().attr('aria-hidden', 'false');
}
this.existingFocus.focus();
if (nocallback === true)
return;
switch (typeof this.options.callback) {
case 'function':
this.debug('Callback', this.options.callback, this);
this.options.callback.call(window, this);
break;
case 'string':
this.debug('Callback', this.options.callback, this);
CS.executeFunctionByName(this.options.callback, window, this);
break;
}
},
debug: function (/* arguments */) {
if (CS.Components.debug) {
console.log(name, this.id, arguments);
}
}
};
/**
* Install JQuery plugin.
*/
$.fn[name] = function (options /*, args */) {
var args = Array.prototype.slice.call(arguments, 1);
return this.each(function () {
var instance = $(this).data(name);
if (instance) {
if (typeof options === 'string') {
// Method call.
instance[options].apply(instance, args);
} else {
instance.debug('Already bound');
}
} else {
instance = new $[name](this, options || true);
$(this).data(name, instance);
}
});
};
/**
* Auto-bind components with attribute 'data-cs-bind'.
*/
$[name].autoBind = function () {
$(hook).each(function () {
if ($(this).attr('data-cs-bind') !== undefined) {
$(this)[name](true);
}
});
};
$($[name].autoBind);
})(jQuery);
<div class="modal-dialog modal-dialog--transition-zoom-fade js-modal-dialog s-templatestabmodal is-visible" aria-hidden="true" aria-labelledby="modal-dialog-title" aria-describedby="modal-dialog-description" role="dialog" data-cs-callback="finishOnboarding" data-cs-allowclose="true">
<div class="modal-dialog__anim-target">
<div class="modal-dialog__content">
<h1 id="modal-dialog-title" class="modal-dialog__heading">Want to create a campaign?</h1>
<p id="modal-dialog-description" class="modal-dialog__text">This is where you manage templates. If you want to create and send a campaign, the easiest way is to go to the campaigns tab.</p>
<div class="modal-dialog__tour">
<ul class="modal-dialog__tour__features js-modal-dialog-feature-target">
<li class="modal-dialog__tour__feature-item js-modal-dialog-feature-item">
<div class="demo-header">
<div class="demo-header__main">
<div class="demo-header__logo"></div>
<div class="demo-header__links">
<div class="demo-header__links__blank"></div>
<div class="demo-header__links__dummy">Campaigns</div>
<div class="demo-header__links__blank"></div>
<div class="demo-header__links__blank"></div>
</div>
</div>
</div>
</li>
</ul>
</div>
<div class="modal-dialog__pagination">
<a href="/campaigns/create/new/" class="button primary huge js-modal-dialog-call-to-action js-no-callback">Create a new campaign</a>
<span class="cancel">or, <a href="/templates/" class="js-modal-dialog-close">Manage templates</a></span>
</div>
</div>
<button class="modal-dialog_closeicon js-modal-dialog-close"><em class="u-hide-visually">Close</em></button>
</div>
</div>
<div class="modal-dialog-overlay js-modal-dialog-overlay js-no-modal-focus"></div>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment