1
\$\begingroup\$

I started working on a notification system that is pluggable into any project. I have not made any "plugin" type javascript libraries, usually just functions very specific to the project at hand. That said I wanted to reach out for pointers to avoid conflicts in both javascript (and css). I'm aware specificity is key in most places, common namespaces likely should be avoided (I have yet to apply more specific classing), but any insight beyond that is greatly appreciated.

I only just started, so in the early stages at this point, figured I'd get insight early before going down the rabbit hole too far. Lots of "To Do's" remain.

Early mock/demo of project in snippet:

/** TODO:
  * add close on backdrop click
  * add automatic close function to hide after a designated timeframe; if specified.
  * add controller/id to pair toggles with dialog (data-controls/id pair)
  * add localStorage for repeat visitors.
  * improve animations
  * Dock Only: add minify option, minify into small icon 
  * Dock & popup Only: stack notifiers if more than 1 dock/popup
*/


// convert string into fragment
String.prototype.toFragment = function() {
  return document.createRange().createContextualFragment(this);
};

// convert html collection into an array
HTMLCollection.prototype.toArray = function() {
  return toArray(this);
};

// convert html collection into an array
NodeList.prototype.toArray = function() {
  return toArray(this);
};

// matches polyfill
if (!Element.prototype.matches) {
  Element.prototype.matches = Element.prototype.msMatchesSelector || Element.prototype.webkitMatchesSelector;
}

// closest polyfill
if (!Element.prototype.closest) {
  Element.prototype.closest = function(s) {
    var el = this;
    do {
      if (el.matches(s)) return el;
      el = el.parentElement || el.parentNode;
    } while (el !== null && el.nodeType === 1);
    return null;
  };
}

// convert array-like objects into a true array
function toArray(arrayLikeObject) {
  return !Array.from ? [].slice.call(arrayLikeObject) : Array.from(arrayLikeObject);
}

// remove focus from dialogs
function deselectAllDialogs() {
  return document.querySelectorAll('.dialog').toArray().forEach(removeSelected);
}

// set aria-selected to false
function removeSelected(node) {
  return node.setAttribute("aria-selected", false);
}

// set aria-selected to true on target
function selectTarget(event) {
  return event.target.setAttribute("aria-selected", true);
}

// set aria-selected to false on parent element of target
function removeDialogSelection(event) {
  return removeSelected(event.target.closest('.dialog'));
}

// add boolean hidden attribute to parent element node
function hideTargetParent(event) {
	var overlay = document.querySelector('.dialog-backdrop');
  if (overlay !== null) overlay.setAttribute("hidden", "");
  return event.target.closest('.dialog').setAttribute("hidden", "");
}

// dialog constructor
function Dialog(params) {
  var empty = _dialog.fallbacks.blank;

  // setup params/attributes & fallbacks
  this.title = params.title;
  this.action = params.action;
  this.message = params.message || _dialog.fallbacks.warning;
  this.html = params.html || empty;
  this.type = params.type || empty;
  this.method = params.method || empty;
  this.state = params.state ? " hidden" : empty;
  
  var overlay = "";
  if (this.method.trim() === "popup" && document.querySelector('.dialog-backdrop') === null) {
  	overlay = '<div class="dialog-backdrop" />';
  }

  // create html strings of dialog using only items available
  var nodes = {};
  nodes.start = '<div class="dialog' + this.type + this.method + '" tabindex="0"' + this.state + '><div class="inner">';
  nodes.title = this.title !== void 0 ? '<strong class="title">' + this.title + '</strong>' : empty;
  nodes.message = "<p>" + this.message + "</p>";
  nodes.html = this.html;
  nodes.action = this.action !== void 0 ? '<div class="ui">' + this.action + '</div>' : empty;
  nodes.end = "</div></div>" + overlay;

  // add to callable property
  this.toString = Object.keys(nodes).map(function(node) {
    return nodes[node]
  }).join("");

  // create to appendable fragment
  this.toHTML = this.toString.toFragment();
}

Dialog.prototype.setup = function() {

  // get fragment contents
  var dialog = this.toHTML.firstChild;

  // create button string
  var closeString = '<button class="close" title="Close' + this.type + ' dialog.">&times;</button>';

  // make focusable
  dialog.setAttribute("tabindex", 0);

  // insert close button
  dialog.insertBefore(closeString.toFragment(), dialog.firstChild);

  // add events
  dialog.addEventListener('click', deselectAllDialogs, false);
  dialog.addEventListener("focusin", selectTarget, false);

  // add defocus/hidden on tab out/button close
  var focusable = dialog.querySelectorAll('button, input, a, [tabindex="0"], textarea');
  var lastFocusableItem = focusable.toArray().slice(-1)[0];
  closeDialog(dialog.querySelector(".close"), lastFocusableItem);
  
}

function closeDialog(closeButton, lastFocusableItem) {
  closeButton.addEventListener("click", hideTargetParent, false);
  lastFocusableItem.addEventListener("focusout", removeDialogSelection, false);
}

console.clear();

var _dialog = {
  type: {
    inform: " information",
    warning: " warning",
    error: " error",
    success: " success"
  },
  method: {
    inline: " inline",
    popup: " popup",
    dock: " dock"
  },
  state: {
  	hidden: true
  },
  fallbacks: {
    warning: "Intended message could not found; Contact an administrator.",
    blank: ""
  }
}

var InfoExample = new Dialog({
  title: "Information Example",
  message: "An “Information Dialog” message displays information to your user for a variety of reasons.",
  action: "<button>More Details</button>",
  type: _dialog.type.inform,
  method: _dialog.method.inline,
  state: !_dialog.state.hidden
});

InfoExample.setup();
document.body.appendChild(InfoExample.toHTML);


var WarningExample = new Dialog({
  title: "Warning Example",
  message: "A “Warning Dialog” message displays concerns to your users who are in need of such a notice.",
  type: _dialog.type.warning,
  method: _dialog.method.dock,
  state: !_dialog.state.hidden
});

WarningExample.setup();
setTimeout(function() {
	return document.body.appendChild(WarningExample.toHTML);
  }, 2000);


var ErrorExample = new Dialog({
  title: "Error Example",
  message: "An “Error Dialog” message displays issues either unfixable or in need of user interaction in order to correct the concern.",
  html: "<ul><li>additional html mark-up</li><li>is appendable</li><li>via the html property</li></ul>",
  action: "<button>Reload?</button>",
  type: _dialog.type.error,
  method: _dialog.method.inline,
  state: !_dialog.state.hidden
});

ErrorExample.setup();
document.body.appendChild(ErrorExample.toHTML);


var SuccessExample = new Dialog({
  title: "Success Example",
  message: "A “Success Dialog” message displays some success to the users so they are aware their interaction succeeded.",
  type: _dialog.type.success,
  method: _dialog.method.popup,
  state: !_dialog.state.hidden
});

SuccessExample.setup();
document.getElementById('toggle').addEventListener('click', function() {
	return document.body.appendChild(SuccessExample.toHTML);
  }, false);
html {
  font: normal 100%/1.75 sans-serif;
}

body {
  font-size: 90%;
}

.dialog {
  padding: 1rem 2rem;
  border: 1px solid transparent;
  width: 100%;
  border-radius: 2px;
  box-sizing: border-box;
  overflow: hidden;
  text-shadow: 0 0 1rem #fff;
  min-height: 125px;
  background-repeat: no-repeat;
  color: hsla(0, 0%, 0%, 0.75);
}

.dialog-backdrop {
  position: fixed;
  background-color: hsla(0, 100%, 100%, .9);
  z-index:98;
  width: 100%;
  height: 100%;
  left:0;
  top:0;
}

.dialog.inline {
  display: table;
  position: relative;
  margin-bottom: 1rem;
  box-shadow: inset 0 0 0 100vh hsla(0, 0%, 100%, .75);
}

.dialog.dock,
.dialog.popup {
  display: table;
  position: fixed;
  box-shadow:
    inset 0 0 0 100vh hsla(0, 0%, 100%, .75),
    0 15px 15px -15px hsla(0, 0%, 0%, .5);
}

.dialog.dock {
  bottom:1rem;
  right: 1rem;
  max-width: 320px;
  width: 90%;
  z-index:999;
}

.dialog.popup {
  left: 50%;
  top:50%;
  transform: translate3d(-50%, -50%, 0);
  max-width: 600px;
  width: 90%;
  z-index: 99;
}

.dialog .inner {
  display: table-cell;
  vertical-align: middle;
  height: 100%;
}

@media (min-width:0) and (max-width:600px) {
  .dialog .ui {
    text-align: right;
  }
}

.dialog button:not(.close) {
  border-radius: 0.125rem;
  padding: 0.5rem 0.666rem;
  font-weight: 500;
  opacity: 0.85;
  background-color: transparent;
  color: #fff;
  transition: all 0.1s;
}

.dialog button:not(.close):focus,
.dialog button:not(.close):hover {
  transform: scale(1.025);
  outline: 0;
  opacity: 1;
  cursor: pointer;
  box-shadow: 0 6px 6px -6px hsla(0, 0%, 0%, 0.5);
}

.dialog button:not(.close):active {
  box-shadow: none;
  border-style: solid;
  transform: scale(1);
}

.dialog ul,
.dialog ol,
.dialog dl,
.dialog p {
  margin-top: 0;
}

.dialog .title {
  display: block;
  opacity: 0.666;
}

.dialog *:last-child {
  margin-bottom: 0;
}

.dialog .close {
  opacity: 0;
  border: 1px solid transparent;
  background-color: transparent;
  position: absolute;
  right: 0.25rem;
  top: 0.333rem;
  border-radius: 2px;
  font-size: 125%;
  line-height: 1;
  transform: scale(0.001);
  transition: all 0.2s;
}

.dialog:hover .close,
.dialog[aria-selected="true"] .close {
  opacity: 1;
  transform: scale(1);
  transition: all 0.2s;
}

.dialog .close:hover {
  cursor: pointer;
  transition: all 0.075s;
  background-color: hsla(0, 0%, 0%, 0.05);
}

.dialog:focus,
.dialog .close:focus,
.dialog .close:active {
  outline: none;
}

.dialog.information .title,
.dialog.information .close {
  color: #3498db;
}

.dialog.error .title,
.dialog.error .close {
  color: #e74c3c;
}

.dialog.success .title,
.dialog.success .close {
  color: #27ae60;
}

.dialog.warning .title,
.dialog.warning .close {
  color: #f39c12;
}

.dialog.information button:not(.close) {
  background-color: #3498db;
}

.dialog.error button:not(.close) {
  background-color: #e74c3c;
}

.dialog.success button:not(.close) {
  background-color: #27ae60;
}

.dialog.warning button:not(.close) {
  background-color: #f39c12;
}

.dialog.information,
.dialog.information button:not(.close),
.dialog.information .close:focus {
  border-color: #3498db;
}

.dialog.error,
.dialog.error button:not(.close),
.dialog.error .close:focus {
  border-color: #e74c3c;
}

.dialog.success,
.dialog.success button:not(.close),
.dialog.success .close:focus {
  border-color: #27ae60;
}

.dialog.warning,
.dialog.warning button:not(.close),
.dialog.warning .close:focus {
  border-color: #f39c12;
}

.dialog[aria-selected="true"] {
  outline-offset: 2px;
}

.dialog.information[aria-selected="true"] {
  outline: 1px dotted #3498db;
}

.dialog.error[aria-selected="true"] {
  outline: 1px dotted #e74c3c;
}

.dialog.success[aria-selected="true"] {
  outline: 1px dotted #27ae60;
}

.dialog.warning[aria-selected="true"] {
  outline: 1px dotted #f39c12;
}

.dialog.warning {
  background-image: /*url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512"><path fill="hsl(37, 90%, 51%)" transform="rotate(-24 189.3957061767577,212.6444854736327)" opacity="0.25" d="m157.686503,13.297016c17.52217,-28.348917 45.973192,-28.281451 63.453674,0l217.201324,351.405852c23.392095,37.845479 6.352658,68.525489 -38.036599,68.525489l-421.783126,0c-44.399125,0 -61.504216,-30.557766 -38.036599,-68.525489l217.201324,-351.405852l0,0zm31.734289,117.845789c-11.122387,0 -20.139037,9.123588 -20.139037,20.079224l0,120.953847c0,11.089359 8.939114,20.079224 20.139037,20.079224c11.122589,0 20.139037,-9.123789 20.139037,-20.079224l0,-120.953847c0,-11.089561 -8.938913,-20.079224 -20.139037,-20.079224l0,0zm0,241.668442c11.122589,0 20.139037,-9.01665 20.139037,-20.139037c0,-11.122589 -9.016448,-20.139037 -20.139037,-20.139037c-11.122387,0 -20.139037,9.016448 -20.139037,20.139037c0,11.122387 9.01665,20.139037 20.139037,20.139037l0,0z"/></svg>'),*/
  linear-gradient(45deg, #fff 0%, hsla(37, 90%, 51%, .25) 100%);
}

.dialog.information {
  background-image: /*url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512"><path fill="hsl(204, 70%, 53%)" transform="rotate(-45 256.01535034179676,255.98666381835938)" opacity="0.25" d="m480.014141,252.986668c-1.7,-123.7 -103.3,-222.6 -227,-221s-222.6,103.3 -221,227c1.7,123.7 103.3,222.6 227,221c123.7,-1.7 222.7,-103.3 221,-227zm-224,-141.1c17.7,0 32,14.3 32,32s-14.3,32 -32,32c-17.7,0 -32,-14.3 -32,-32s14.3,-32 32,-32zm44,283.1l-88,0l0,-11l22,0l0,-160l-22,0l0,-12l66,0l0,172l22,0l0,11z" /></svg>'),*/
  linear-gradient(45deg, #fff 0%, hsla(204, 70%, 53%, .25) 100%);
}

.dialog.error {
  background-image: /* url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512"><circle fill="hsl(6, 78%, 57%)" opacity="0.25" r="253.44" cy="256" cx="256"/><path fill="hsl(255, 0%, 100%)" d="m350.019,144.066l17.521,17.522c6.047,6.047 6.047,15.852 0,21.9l-183.933,183.931c-6.047,6.048 -15.852,6.047 -21.9,0l-17.521,-17.522c-6.047,-6.047 -6.047,-15.852 0,-21.9l183.932,-183.933c6.048,-6.046 15.853,-6.046 21.901,0.002z"/><path fill="hsl(255, 100%, 100%)" d="m367.54,349.899l-17.522,17.522c-6.047,6.047 -15.852,6.047 -21.9,0l-183.932,-183.933c-6.047,-6.047 -6.047,-15.852 0,-21.9l17.522,-17.522c6.047,-6.047 15.852,-6.047 21.9,0l183.932,183.933c6.048,6.048 6.048,15.853 0,21.9z"/></svg>'),*/
  linear-gradient(45deg, #fff 0%, hsla(6, 78%, 57%, .25) 100%);
}

.dialog.success {
  background-image: /* url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512"><path opacity="0.25" fill="hsl(145, 63%, 42%)" id="svg_2" d="m255.999998,7.084824c-137.5017,0 -248.915175,111.413475 -248.915175,248.915175s111.413475,248.915175 248.915175,248.915175c137.5017,0 248.915175,-111.413475 248.915175,-248.915175s-111.413475,-248.915175 -248.915175,-248.915175zm-38.414313,337.112522c-2.872098,2.872098 -6.940904,5.265513 -10.531027,5.265513s-7.658928,-2.513086 -10.650697,-5.385184l- 67.015624,-67.015624l21.301395,-21.301395l56.484597,56.484597l149.349105,-150.426142l20.942382,21.660407l-159.880131,160.717827z"/></svg>'),*/
  linear-gradient(45deg, #fff 0%, hsla(145, 63%, 42%, .25) 100%);
}

[hidden] {
  display: none !important;
}
<button id="toggle" style="margin-bottom:1rem;">
  Toggle Success Modal
</button>

Sidenote
Would a custom event method be a source of concern for matching random selectors? e.g. the following code block:

function emitEvent(args, fn) {
  var parent = args.parent === void 0 ? window : args.parent;
  parent.addEventListener(args.type, function (event) {
      if (!event.target.matches(args.selector)) return;
      return fn(event);
    }, false );
}

emitEvent({type: "click", selector: ".someClass"}, callback);
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.