UNPKG

13.1 kBJavaScriptView Raw
1/**
2 * Permission is hereby granted, free of charge, to any person obtaining a copy
3 * of this software and associated documentation files (the "Software"), to deal
4 * in the Software without restriction, including without limitation the rights
5 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
6 * copies of the Software, and to permit persons to whom the Software is
7 * furnished to do so, subject to the following conditions:
8 *
9 * The above copyright notice and this permission notice shall be included in
10 * all copies or substantial portions of the Software.
11 *
12 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
13 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
14 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
15 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
16 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
17 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
18 * SOFTWARE.
19 */
20
21(function (root, factory) {
22 "use strict";
23
24 if (typeof define === 'function' && define.amd) {
25 define([], factory);
26 }
27 else if (typeof module === 'object' && module.exports) {
28 module.exports = factory();
29 }
30 else {
31 root.picoModal = factory();
32 }
33}(this, function () {
34
35 /**
36 * A self-contained modal library
37 */
38 "use strict";
39
40 /** Returns whether a value is a dom node */
41 function isNode(value) {
42 if ( typeof Node === "object" ) {
43 return value instanceof Node;
44 }
45 else {
46 return value &&
47 typeof value === "object" &&
48 typeof value.nodeType === "number";
49 }
50 }
51
52 /** Returns whether a value is a string */
53 function isString(value) {
54 return typeof value === "string";
55 }
56
57 /**
58 * Generates observable objects that can be watched and triggered
59 */
60 function observable() {
61 var callbacks = [];
62 return {
63 watch: callbacks.push.bind(callbacks),
64 trigger: function( modal ) {
65
66 var unprevented = true;
67 var event = {
68 preventDefault: function preventDefault () {
69 unprevented = false;
70 }
71 };
72
73 for (var i = 0; i < callbacks.length; i++) {
74 callbacks[i](modal, event);
75 }
76
77 return unprevented;
78 }
79 };
80 }
81
82
83 /**
84 * A small interface for creating and managing a dom element
85 */
86 function Elem( elem ) {
87 this.elem = elem;
88 }
89
90 /**
91 * Creates a new div
92 */
93 Elem.div = function ( parent ) {
94 if ( typeof parent === "string" ) {
95 parent = document.querySelector(parent);
96 }
97 var elem = document.createElement('div');
98 (parent || document.body).appendChild(elem);
99 return new Elem(elem);
100 };
101
102 Elem.prototype = {
103
104 /** Creates a child of this node */
105 child: function () {
106 return Elem.div(this.elem);
107 },
108
109 /** Applies a set of styles to an element */
110 stylize: function(styles) {
111 styles = styles || {};
112
113 if ( typeof styles.opacity !== "undefined" ) {
114 styles.filter =
115 "alpha(opacity=" + (styles.opacity * 100) + ")";
116 }
117
118 for (var prop in styles) {
119 if (styles.hasOwnProperty(prop)) {
120 this.elem.style[prop] = styles[prop];
121 }
122 }
123
124 return this;
125 },
126
127 /** Adds a class name */
128 clazz: function (clazz) {
129 this.elem.className += " " + clazz;
130 return this;
131 },
132
133 /** Sets the HTML */
134 html: function (content) {
135 if ( isNode(content) ) {
136 this.elem.appendChild( content );
137 }
138 else {
139 this.elem.innerHTML = content;
140 }
141 return this;
142 },
143
144 /** Adds a click handler to this element */
145 onClick: function(callback) {
146 this.elem.addEventListener('click', callback);
147 return this;
148 },
149
150 /** Removes this element from the DOM */
151 destroy: function() {
152 this.elem.parentNode.removeChild(this.elem);
153 },
154
155 /** Hides this element */
156 hide: function() {
157 this.elem.style.display = "none";
158 },
159
160 /** Shows this element */
161 show: function() {
162 this.elem.style.display = "block";
163 },
164
165 /** Sets an attribute on this element */
166 attr: function ( name, value ) {
167 this.elem.setAttribute(name, value);
168 return this;
169 },
170
171 /** Executes a callback on all the ancestors of an element */
172 anyAncestor: function ( predicate ) {
173 var elem = this.elem;
174 while ( elem ) {
175 if ( predicate( new Elem(elem) ) ) {
176 return true;
177 }
178 else {
179 elem = elem.parentNode;
180 }
181 }
182 return false;
183 }
184 };
185
186
187 /** Generates the grey-out effect */
188 function buildOverlay( getOption, close ) {
189 return Elem.div( getOption("parent") )
190 .clazz("pico-overlay")
191 .clazz( getOption("overlayClass", "") )
192 .stylize({
193 display: "none",
194 position: "fixed",
195 top: "0px",
196 left: "0px",
197 height: "100%",
198 width: "100%",
199 zIndex: 10000
200 })
201 .stylize(getOption('overlayStyles', {
202 opacity: 0.5,
203 background: "#000"
204 }))
205 .onClick(function () {
206 if ( getOption('overlayClose', true) ) {
207 close();
208 }
209 });
210 }
211
212 /** Builds the content of a modal */
213 function buildModal( getOption, close ) {
214 var width = getOption('width', 'auto');
215 if ( typeof width === "number" ) {
216 width = "" + width + "px";
217 }
218
219 var elem = Elem.div( getOption("parent") )
220 .clazz("pico-content")
221 .clazz( getOption("modalClass", "") )
222 .stylize({
223 display: 'none',
224 position: 'fixed',
225 zIndex: 10001,
226 left: "50%",
227 top: "50px",
228 width: width,
229 '-ms-transform': 'translateX(-50%)',
230 '-moz-transform': 'translateX(-50%)',
231 '-webkit-transform': 'translateX(-50%)',
232 '-o-transform': 'translateX(-50%)',
233 'transform': 'translateX(-50%)'
234 })
235 .stylize(getOption('modalStyles', {
236 backgroundColor: "white",
237 padding: "20px",
238 borderRadius: "5px"
239 }))
240 .html( getOption('content') )
241 .attr("role", "dialog")
242 .onClick(function (event) {
243 var isCloseClick = new Elem(event.target)
244 .anyAncestor(function (elem) {
245 return /\bpico-close\b/.test(elem.elem.className);
246 });
247 if ( isCloseClick ) {
248 close();
249 }
250 });
251
252 return elem;
253 }
254
255 /** Builds the close button */
256 function buildClose ( elem, getOption ) {
257 if ( getOption('closeButton', true) ) {
258 return elem.child()
259 .html( getOption('closeHtml', "&#xD7;") )
260 .clazz("pico-close")
261 .clazz( getOption("closeClass", "") )
262 .stylize( getOption('closeStyles', {
263 borderRadius: "2px",
264 cursor: "pointer",
265 height: "15px",
266 width: "15px",
267 position: "absolute",
268 top: "5px",
269 right: "5px",
270 fontSize: "16px",
271 textAlign: "center",
272 lineHeight: "15px",
273 background: "#CCC"
274 }) );
275 }
276 }
277
278 /** Builds a method that calls a method and returns an element */
279 function buildElemAccessor( builder ) {
280 return function () {
281 return builder().elem;
282 };
283 }
284
285
286 /**
287 * Displays a modal
288 */
289 return function picoModal(options) {
290
291 if ( isString(options) || isNode(options) ) {
292 options = { content: options };
293 }
294
295 var afterCreateEvent = observable();
296 var beforeShowEvent = observable();
297 var afterShowEvent = observable();
298 var beforeCloseEvent = observable();
299 var afterCloseEvent = observable();
300
301 /**
302 * Returns a named option if it has been explicitly defined. Otherwise,
303 * it returns the given default value
304 */
305 function getOption ( opt, defaultValue ) {
306 var value = options[opt];
307 if ( typeof value === "function" ) {
308 value = value( defaultValue );
309 }
310 return value === undefined ? defaultValue : value;
311 }
312
313 /** Hides this modal */
314 function forceClose () {
315 shadowElem().hide();
316 modalElem().hide();
317 afterCloseEvent.trigger(iface);
318 }
319
320 /** Gracefully hides this modal */
321 function close () {
322 if ( beforeCloseEvent.trigger(iface) ) {
323 forceClose();
324 }
325 }
326
327 /** Wraps a method so it returns the modal interface */
328 function returnIface ( callback ) {
329 return function () {
330 callback.apply(this, arguments);
331 return iface;
332 };
333 }
334
335
336 // The constructed dom nodes
337 var built;
338
339 /** Builds a method that calls a method and returns an element */
340 function build ( name ) {
341 if ( !built ) {
342 var modal = buildModal(getOption, close);
343 built = {
344 modal: modal,
345 overlay: buildOverlay(getOption, close),
346 close: buildClose(modal, getOption)
347 };
348 afterCreateEvent.trigger(iface);
349 }
350 return built[name];
351 }
352
353 var modalElem = build.bind(window, 'modal');
354 var shadowElem = build.bind(window, 'overlay');
355 var closeElem = build.bind(window, 'close');
356
357
358 var iface = {
359
360 /** Returns the wrapping modal element */
361 modalElem: buildElemAccessor(modalElem),
362
363 /** Returns the close button element */
364 closeElem: buildElemAccessor(closeElem),
365
366 /** Returns the overlay element */
367 overlayElem: buildElemAccessor(shadowElem),
368
369 /** Builds the dom without showing the modal */
370 buildDom: returnIface(build),
371
372 /** Shows this modal */
373 show: function () {
374 if ( beforeShowEvent.trigger(iface) ) {
375 shadowElem().show();
376 closeElem();
377 modalElem().show();
378 afterShowEvent.trigger(iface);
379 }
380 return this;
381 },
382
383 /** Hides this modal */
384 close: returnIface(close),
385
386 /**
387 * Force closes this modal. This will not call beforeClose
388 * events and will just immediately hide the modal
389 */
390 forceClose: returnIface(forceClose),
391
392 /** Destroys this modal */
393 destroy: function () {
394 modalElem = modalElem().destroy();
395 shadowElem = shadowElem().destroy();
396 closeElem = undefined;
397 },
398
399 /**
400 * Updates the options for this modal. This will only let you
401 * change options that are re-evaluted regularly, such as
402 * `overlayClose`.
403 */
404 options: function ( opts ) {
405 options = opts;
406 },
407
408 /** Executes after the DOM nodes are created */
409 afterCreate: returnIface(afterCreateEvent.watch),
410
411 /** Executes a callback before this modal is closed */
412 beforeShow: returnIface(beforeShowEvent.watch),
413
414 /** Executes a callback after this modal is shown */
415 afterShow: returnIface(afterShowEvent.watch),
416
417 /** Executes a callback before this modal is closed */
418 beforeClose: returnIface(beforeCloseEvent.watch),
419
420 /** Executes a callback after this modal is closed */
421 afterClose: returnIface(afterCloseEvent.watch)
422 };
423
424 return iface;
425 };
426
427}));