layer.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515
  1. (function (global, factory) {
  2. const LayerClass = factory();
  3. // Allow usage as `Layer({...})` or `new Layer()`
  4. // But Layer is a class. We can wrap it in a proxy or factory function.
  5. function LayerFactory(options) {
  6. if (options && typeof options === 'object') {
  7. return LayerClass.$(options);
  8. }
  9. return new LayerClass();
  10. }
  11. // Copy static methods
  12. Object.assign(LayerFactory, LayerClass);
  13. // Also copy prototype for instanceof checks if needed (though tricky with factory)
  14. LayerFactory.prototype = LayerClass.prototype;
  15. typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = LayerFactory :
  16. typeof define === 'function' && define.amd ? define(() => LayerFactory) :
  17. (global.Layer = LayerFactory);
  18. }(this, (function () {
  19. 'use strict';
  20. const PREFIX = 'layer-';
  21. // Theme & Styles
  22. const css = `
  23. .${PREFIX}overlay {
  24. position: fixed;
  25. top: 0;
  26. left: 0;
  27. width: 100%;
  28. height: 100%;
  29. background: rgba(0, 0, 0, 0.4);
  30. display: flex;
  31. justify-content: center;
  32. align-items: center;
  33. z-index: 1000;
  34. opacity: 0;
  35. transition: opacity 0.3s;
  36. }
  37. .${PREFIX}overlay.show {
  38. opacity: 1;
  39. }
  40. .${PREFIX}popup {
  41. background: #fff;
  42. border-radius: 8px;
  43. box-shadow: 0 4px 12px rgba(0,0,0,0.15);
  44. width: 32em;
  45. max-width: 90%;
  46. padding: 1.5em;
  47. display: flex;
  48. flex-direction: column;
  49. align-items: center;
  50. transform: scale(0.92);
  51. transition: transform 0.26s ease;
  52. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  53. }
  54. .${PREFIX}overlay.show .${PREFIX}popup {
  55. transform: scale(1);
  56. }
  57. .${PREFIX}title {
  58. font-size: 1.8em;
  59. font-weight: 600;
  60. color: #333;
  61. margin: 0 0 0.5em;
  62. text-align: center;
  63. }
  64. .${PREFIX}content {
  65. font-size: 1.125em;
  66. color: #545454;
  67. margin-bottom: 1.5em;
  68. text-align: center;
  69. line-height: 1.5;
  70. }
  71. .${PREFIX}actions {
  72. display: flex;
  73. justify-content: center;
  74. gap: 1em;
  75. width: 100%;
  76. }
  77. .${PREFIX}button {
  78. border: none;
  79. border-radius: 4px;
  80. padding: 0.6em 1.2em;
  81. font-size: 1em;
  82. cursor: pointer;
  83. transition: background-color 0.2s, box-shadow 0.2s;
  84. color: #fff;
  85. }
  86. .${PREFIX}confirm {
  87. background-color: #3085d6;
  88. }
  89. .${PREFIX}confirm:hover {
  90. background-color: #2b77c0;
  91. }
  92. .${PREFIX}cancel {
  93. background-color: #aaa;
  94. }
  95. .${PREFIX}cancel:hover {
  96. background-color: #999;
  97. }
  98. .${PREFIX}icon {
  99. width: 5em;
  100. height: 5em;
  101. margin: 1.5em auto 1.2em;
  102. position: relative;
  103. display: grid;
  104. place-items: center;
  105. }
  106. /* SVG Icon (SweetAlert-like, animated via xjs.draw + spring) */
  107. .${PREFIX}icon svg {
  108. width: 100%;
  109. height: 100%;
  110. display: block;
  111. overflow: visible;
  112. }
  113. .${PREFIX}svg-ring,
  114. .${PREFIX}svg-mark {
  115. fill: none;
  116. stroke-linecap: round;
  117. stroke-linejoin: round;
  118. }
  119. .${PREFIX}svg-ring {
  120. stroke-width: 4.5;
  121. opacity: 0.95;
  122. }
  123. .${PREFIX}svg-mark {
  124. stroke-width: 6;
  125. }
  126. .${PREFIX}svg-dot {
  127. transform-box: fill-box;
  128. transform-origin: center;
  129. }
  130. .${PREFIX}icon.success { color: #a5dc86; }
  131. .${PREFIX}icon.error { color: #f27474; }
  132. .${PREFIX}icon.warning { color: #f8bb86; }
  133. .${PREFIX}icon.info { color: #3fc3ee; }
  134. `;
  135. // Inject Styles
  136. const injectStyles = () => {
  137. if (document.getElementById(`${PREFIX}styles`)) return;
  138. const styleSheet = document.createElement('style');
  139. styleSheet.id = `${PREFIX}styles`;
  140. styleSheet.textContent = css;
  141. document.head.appendChild(styleSheet);
  142. };
  143. class Layer {
  144. constructor() {
  145. injectStyles();
  146. this.params = {};
  147. this.dom = {};
  148. this.promise = null;
  149. this.resolve = null;
  150. this.reject = null;
  151. this._onKeydown = null;
  152. }
  153. // Constructor helper when called as function: const popup = Layer({...})
  154. static get isProxy() { return true; }
  155. // Static entry point
  156. static fire(options) {
  157. const instance = new Layer();
  158. return instance._fire(options);
  159. }
  160. // Chainable entry point (builder-style)
  161. // Example:
  162. // Layer.$({ title: 'Hi' }).fire().then(...)
  163. // Layer.$().config({ title: 'Hi' }).fire()
  164. static $(options) {
  165. const instance = new Layer();
  166. if (options !== undefined) instance.config(options);
  167. return instance;
  168. }
  169. // Chainable config helper (does not render until `.fire()` is called)
  170. config(options = {}) {
  171. // Support the same shorthand as Layer.fire(title, text, icon)
  172. if (typeof options === 'string') {
  173. options = { title: options };
  174. if (arguments[1]) options.text = arguments[1];
  175. if (arguments[2]) options.icon = arguments[2];
  176. }
  177. this.params = { ...(this.params || {}), ...options };
  178. return this;
  179. }
  180. // Instance entry point (chainable)
  181. fire(options) {
  182. const merged = (options === undefined) ? (this.params || {}) : options;
  183. return this._fire(merged);
  184. }
  185. _fire(options = {}) {
  186. if (typeof options === 'string') {
  187. options = { title: options };
  188. if (arguments[1]) options.text = arguments[1];
  189. if (arguments[2]) options.icon = arguments[2];
  190. }
  191. this.params = {
  192. title: '',
  193. text: '',
  194. icon: null,
  195. confirmButtonText: 'OK',
  196. cancelButtonText: 'Cancel',
  197. showCancelButton: false,
  198. confirmButtonColor: '#3085d6',
  199. cancelButtonColor: '#aaa',
  200. closeOnClickOutside: true,
  201. closeOnEsc: true,
  202. iconAnimation: true,
  203. popupAnimation: true,
  204. ...options
  205. };
  206. this.promise = new Promise((resolve, reject) => {
  207. this.resolve = resolve;
  208. this.reject = reject;
  209. });
  210. this._render();
  211. return this.promise;
  212. }
  213. static _getXjs() {
  214. // Prefer xjs, fallback to animal (compat)
  215. const g = (typeof window !== 'undefined') ? window : (typeof globalThis !== 'undefined' ? globalThis : null);
  216. if (!g) return null;
  217. const x = g.xjs || g.animal;
  218. return (typeof x === 'function') ? x : null;
  219. }
  220. _render() {
  221. // Remove existing if any
  222. const existing = document.querySelector(`.${PREFIX}overlay`);
  223. if (existing) existing.remove();
  224. // Create Overlay
  225. this.dom.overlay = document.createElement('div');
  226. this.dom.overlay.className = `${PREFIX}overlay`;
  227. // Create Popup
  228. this.dom.popup = document.createElement('div');
  229. this.dom.popup.className = `${PREFIX}popup`;
  230. this.dom.overlay.appendChild(this.dom.popup);
  231. // Icon
  232. if (this.params.icon) {
  233. this.dom.icon = this._createIcon(this.params.icon);
  234. this.dom.popup.appendChild(this.dom.icon);
  235. }
  236. // Title
  237. if (this.params.title) {
  238. this.dom.title = document.createElement('h2');
  239. this.dom.title.className = `${PREFIX}title`;
  240. this.dom.title.textContent = this.params.title;
  241. this.dom.popup.appendChild(this.dom.title);
  242. }
  243. // Content (Text / HTML / Element)
  244. if (this.params.text || this.params.html || this.params.content) {
  245. this.dom.content = document.createElement('div');
  246. this.dom.content.className = `${PREFIX}content`;
  247. if (this.params.content) {
  248. // DOM Element or Selector
  249. let el = this.params.content;
  250. if (typeof el === 'string') el = document.querySelector(el);
  251. if (el instanceof Element) this.dom.content.appendChild(el);
  252. } else if (this.params.html) {
  253. this.dom.content.innerHTML = this.params.html;
  254. } else {
  255. this.dom.content.textContent = this.params.text;
  256. }
  257. this.dom.popup.appendChild(this.dom.content);
  258. }
  259. // Actions
  260. this.dom.actions = document.createElement('div');
  261. this.dom.actions.className = `${PREFIX}actions`;
  262. // Cancel Button
  263. if (this.params.showCancelButton) {
  264. this.dom.cancelBtn = document.createElement('button');
  265. this.dom.cancelBtn.className = `${PREFIX}button ${PREFIX}cancel`;
  266. this.dom.cancelBtn.textContent = this.params.cancelButtonText;
  267. this.dom.cancelBtn.style.backgroundColor = this.params.cancelButtonColor;
  268. this.dom.cancelBtn.onclick = () => this._close(false);
  269. this.dom.actions.appendChild(this.dom.cancelBtn);
  270. }
  271. // Confirm Button
  272. this.dom.confirmBtn = document.createElement('button');
  273. this.dom.confirmBtn.className = `${PREFIX}button ${PREFIX}confirm`;
  274. this.dom.confirmBtn.textContent = this.params.confirmButtonText;
  275. this.dom.confirmBtn.style.backgroundColor = this.params.confirmButtonColor;
  276. this.dom.confirmBtn.onclick = () => this._close(true);
  277. this.dom.actions.appendChild(this.dom.confirmBtn);
  278. this.dom.popup.appendChild(this.dom.actions);
  279. // Event Listeners
  280. if (this.params.closeOnClickOutside) {
  281. this.dom.overlay.addEventListener('click', (e) => {
  282. if (e.target === this.dom.overlay) {
  283. this._close(null); // Dismiss
  284. }
  285. });
  286. }
  287. document.body.appendChild(this.dom.overlay);
  288. // Animation
  289. requestAnimationFrame(() => {
  290. this.dom.overlay.classList.add('show');
  291. this._didOpen();
  292. });
  293. }
  294. _createIcon(type) {
  295. const icon = document.createElement('div');
  296. icon.className = `${PREFIX}icon ${type}`;
  297. const svgNs = 'http://www.w3.org/2000/svg';
  298. const svg = document.createElementNS(svgNs, 'svg');
  299. svg.setAttribute('viewBox', '0 0 80 80');
  300. svg.setAttribute('aria-hidden', 'true');
  301. svg.setAttribute('focusable', 'false');
  302. const ring = document.createElementNS(svgNs, 'circle');
  303. ring.setAttribute('class', `${PREFIX}svg-ring ${PREFIX}svg-draw`);
  304. ring.setAttribute('cx', '40');
  305. ring.setAttribute('cy', '40');
  306. ring.setAttribute('r', '34');
  307. ring.setAttribute('stroke', 'currentColor');
  308. svg.appendChild(ring);
  309. const addPath = (d, extraClass) => {
  310. const p = document.createElementNS(svgNs, 'path');
  311. p.setAttribute('d', d);
  312. p.setAttribute('class', `${PREFIX}svg-mark ${PREFIX}svg-draw${extraClass ? ' ' + extraClass : ''}`);
  313. p.setAttribute('stroke', 'currentColor');
  314. svg.appendChild(p);
  315. return p;
  316. };
  317. if (type === 'success') {
  318. addPath('M23 41 L34.5 53 L58 29', `${PREFIX}svg-success`);
  319. } else if (type === 'error') {
  320. addPath('M28 28 L52 52', `${PREFIX}svg-error-left`);
  321. addPath('M52 28 L28 52', `${PREFIX}svg-error-right`);
  322. } else if (type === 'warning') {
  323. addPath('M40 20 L40 46', `${PREFIX}svg-warning-line`);
  324. const dot = document.createElementNS(svgNs, 'circle');
  325. dot.setAttribute('class', `${PREFIX}svg-dot`);
  326. dot.setAttribute('cx', '40');
  327. dot.setAttribute('cy', '58');
  328. dot.setAttribute('r', '3.2');
  329. dot.setAttribute('fill', 'currentColor');
  330. svg.appendChild(dot);
  331. } else if (type === 'info') {
  332. addPath('M40 34 L40 56', `${PREFIX}svg-info-line`);
  333. const dot = document.createElementNS(svgNs, 'circle');
  334. dot.setAttribute('class', `${PREFIX}svg-dot`);
  335. dot.setAttribute('cx', '40');
  336. dot.setAttribute('cy', '25');
  337. dot.setAttribute('r', '3.2');
  338. dot.setAttribute('fill', 'currentColor');
  339. svg.appendChild(dot);
  340. } else {
  341. // Fallback to info-like ring only
  342. }
  343. icon.appendChild(svg);
  344. return icon;
  345. }
  346. _didOpen() {
  347. // Keyboard close (ESC)
  348. if (this.params.closeOnEsc) {
  349. this._onKeydown = (e) => {
  350. if (!e) return;
  351. if (e.key === 'Escape') this._close(null);
  352. };
  353. document.addEventListener('keydown', this._onKeydown);
  354. }
  355. // Popup animation (optional)
  356. if (this.params.popupAnimation) {
  357. const X = Layer._getXjs();
  358. if (X && this.dom.popup) {
  359. // Override the CSS scale transition with a spring-ish entrance.
  360. try {
  361. this.dom.popup.style.transition = 'none';
  362. this.dom.popup.style.transform = 'scale(0.92)';
  363. X(this.dom.popup).animate({
  364. scale: [0.92, 1],
  365. y: [-6, 0],
  366. duration: 520,
  367. easing: { stiffness: 260, damping: 16 }
  368. });
  369. } catch {}
  370. }
  371. }
  372. // Icon SVG draw animation
  373. if (this.params.iconAnimation) {
  374. this._animateIcon();
  375. }
  376. // User hook (SweetAlert-ish naming)
  377. try {
  378. if (typeof this.params.didOpen === 'function') this.params.didOpen(this.dom.popup);
  379. } catch {}
  380. }
  381. _animateIcon() {
  382. const icon = this.dom.icon;
  383. if (!icon) return;
  384. const X = Layer._getXjs();
  385. if (!X) return;
  386. try {
  387. X(icon).animate({
  388. opacity: [0, 1],
  389. scale: [0.68, 1],
  390. duration: 520,
  391. easing: { stiffness: 240, damping: 14 }
  392. });
  393. } catch {}
  394. const svg = icon.querySelector('svg');
  395. if (!svg) return;
  396. const type = (this.params && this.params.icon) || '';
  397. const ring = svg.querySelector(`.${PREFIX}svg-ring`);
  398. const marks = Array.from(svg.querySelectorAll(`.${PREFIX}svg-mark`));
  399. const dot = svg.querySelector(`.${PREFIX}svg-dot`);
  400. // draw ring first
  401. try { if (ring) X(ring).draw({ duration: 520, easing: 'ease-in-out', delay: 60 }); } catch {}
  402. if (type === 'error') {
  403. // two lines staggered
  404. const left = marks[0];
  405. const right = marks[1];
  406. try { if (left) X(left).draw({ duration: 360, easing: 'ease-out', delay: 180 }); } catch {}
  407. try { if (right) X(right).draw({ duration: 360, easing: 'ease-out', delay: 250 }); } catch {}
  408. return;
  409. }
  410. // single mark (success/warning/info)
  411. marks.forEach((m, i) => {
  412. try { X(m).draw({ duration: 420, easing: 'ease-out', delay: 180 + i * 60 }); } catch {}
  413. });
  414. if (dot) {
  415. try {
  416. dot.style.opacity = '0';
  417. X(dot).animate({
  418. opacity: [0, 1],
  419. scale: [0.2, 1],
  420. duration: 320,
  421. delay: 280,
  422. easing: { stiffness: 320, damping: 18 }
  423. });
  424. } catch {}
  425. }
  426. }
  427. _close(isConfirmed) {
  428. this.dom.overlay.classList.remove('show');
  429. setTimeout(() => {
  430. if (this.dom.overlay && this.dom.overlay.parentNode) {
  431. this.dom.overlay.parentNode.removeChild(this.dom.overlay);
  432. }
  433. }, 300);
  434. if (this._onKeydown) {
  435. try { document.removeEventListener('keydown', this._onKeydown); } catch {}
  436. this._onKeydown = null;
  437. }
  438. try {
  439. if (typeof this.params.willClose === 'function') this.params.willClose(this.dom.popup);
  440. } catch {}
  441. try {
  442. if (typeof this.params.didClose === 'function') this.params.didClose();
  443. } catch {}
  444. if (isConfirmed === true) {
  445. this.resolve({ isConfirmed: true, isDenied: false, isDismissed: false });
  446. } else if (isConfirmed === false) {
  447. this.resolve({ isConfirmed: false, isDenied: false, isDismissed: true, dismiss: 'cancel' });
  448. } else {
  449. this.resolve({ isConfirmed: false, isDenied: false, isDismissed: true, dismiss: 'backdrop' });
  450. }
  451. }
  452. }
  453. return Layer;
  454. })));