layer.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427
  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.9);
  51. transition: transform 0.3s;
  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. border: 0.25em solid transparent;
  102. border-radius: 50%;
  103. margin: 1.5em auto 1.2em;
  104. box-sizing: content-box;
  105. position: relative;
  106. }
  107. /* Success Icon */
  108. .${PREFIX}icon.success {
  109. border-color: #a5dc86;
  110. color: #a5dc86;
  111. }
  112. .${PREFIX}icon.success::before, .${PREFIX}icon.success::after {
  113. content: '';
  114. position: absolute;
  115. background: #fff;
  116. border-radius: 50%;
  117. }
  118. .${PREFIX}success-line-tip {
  119. width: 1.5em;
  120. height: 0.3em;
  121. background-color: #a5dc86;
  122. display: block;
  123. position: absolute;
  124. top: 2.85em;
  125. left: 0.85em;
  126. transform: rotate(45deg);
  127. border-radius: 0.15em;
  128. }
  129. .${PREFIX}success-line-long {
  130. width: 2.9em;
  131. height: 0.3em;
  132. background-color: #a5dc86;
  133. display: block;
  134. position: absolute;
  135. top: 2.4em;
  136. right: 0.5em;
  137. transform: rotate(-45deg);
  138. border-radius: 0.15em;
  139. }
  140. /* Error Icon */
  141. .${PREFIX}icon.error {
  142. border-color: #f27474;
  143. color: #f27474;
  144. }
  145. .${PREFIX}error-x-mark {
  146. position: relative;
  147. display: block;
  148. }
  149. .${PREFIX}error-line {
  150. position: absolute;
  151. height: 0.3em;
  152. width: 3em;
  153. background-color: #f27474;
  154. display: block;
  155. top: 2.3em;
  156. left: 1em;
  157. border-radius: 0.15em;
  158. }
  159. .${PREFIX}error-line.left {
  160. transform: rotate(45deg);
  161. }
  162. .${PREFIX}error-line.right {
  163. transform: rotate(-45deg);
  164. }
  165. /* Warning Icon */
  166. .${PREFIX}icon.warning {
  167. border-color: #facea8;
  168. color: #f8bb86;
  169. }
  170. .${PREFIX}warning-body {
  171. position: absolute;
  172. width: 0.3em;
  173. height: 2.9em;
  174. background-color: #f8bb86;
  175. left: 50%;
  176. top: 0.6em;
  177. margin-left: -0.15em;
  178. border-radius: 0.15em;
  179. }
  180. .${PREFIX}warning-dot {
  181. position: absolute;
  182. width: 0.3em;
  183. height: 0.3em;
  184. background-color: #f8bb86;
  185. left: 50%;
  186. bottom: 0.6em;
  187. margin-left: -0.15em;
  188. border-radius: 50%;
  189. }
  190. `;
  191. // Inject Styles
  192. const injectStyles = () => {
  193. if (document.getElementById(`${PREFIX}styles`)) return;
  194. const styleSheet = document.createElement('style');
  195. styleSheet.id = `${PREFIX}styles`;
  196. styleSheet.textContent = css;
  197. document.head.appendChild(styleSheet);
  198. };
  199. class Layer {
  200. constructor() {
  201. injectStyles();
  202. this.params = {};
  203. this.dom = {};
  204. this.promise = null;
  205. this.resolve = null;
  206. this.reject = null;
  207. }
  208. // Constructor helper when called as function: const popup = Layer({...})
  209. static get isProxy() { return true; }
  210. // Static entry point
  211. static fire(options) {
  212. const instance = new Layer();
  213. return instance._fire(options);
  214. }
  215. // Chainable entry point (builder-style)
  216. // Example:
  217. // Layer.$({ title: 'Hi' }).fire().then(...)
  218. // Layer.$().config({ title: 'Hi' }).fire()
  219. static $(options) {
  220. const instance = new Layer();
  221. if (options !== undefined) instance.config(options);
  222. return instance;
  223. }
  224. // Chainable config helper (does not render until `.fire()` is called)
  225. config(options = {}) {
  226. // Support the same shorthand as Layer.fire(title, text, icon)
  227. if (typeof options === 'string') {
  228. options = { title: options };
  229. if (arguments[1]) options.text = arguments[1];
  230. if (arguments[2]) options.icon = arguments[2];
  231. }
  232. this.params = { ...(this.params || {}), ...options };
  233. return this;
  234. }
  235. // Instance entry point (chainable)
  236. fire(options) {
  237. const merged = (options === undefined) ? (this.params || {}) : options;
  238. return this._fire(merged);
  239. }
  240. _fire(options = {}) {
  241. if (typeof options === 'string') {
  242. options = { title: options };
  243. if (arguments[1]) options.text = arguments[1];
  244. if (arguments[2]) options.icon = arguments[2];
  245. }
  246. this.params = {
  247. title: '',
  248. text: '',
  249. icon: null,
  250. confirmButtonText: 'OK',
  251. cancelButtonText: 'Cancel',
  252. showCancelButton: false,
  253. confirmButtonColor: '#3085d6',
  254. cancelButtonColor: '#aaa',
  255. closeOnClickOutside: true,
  256. ...options
  257. };
  258. this.promise = new Promise((resolve, reject) => {
  259. this.resolve = resolve;
  260. this.reject = reject;
  261. });
  262. this._render();
  263. return this.promise;
  264. }
  265. _render() {
  266. // Remove existing if any
  267. const existing = document.querySelector(`.${PREFIX}overlay`);
  268. if (existing) existing.remove();
  269. // Create Overlay
  270. this.dom.overlay = document.createElement('div');
  271. this.dom.overlay.className = `${PREFIX}overlay`;
  272. // Create Popup
  273. this.dom.popup = document.createElement('div');
  274. this.dom.popup.className = `${PREFIX}popup`;
  275. this.dom.overlay.appendChild(this.dom.popup);
  276. // Icon
  277. if (this.params.icon) {
  278. this.dom.icon = this._createIcon(this.params.icon);
  279. this.dom.popup.appendChild(this.dom.icon);
  280. }
  281. // Title
  282. if (this.params.title) {
  283. this.dom.title = document.createElement('h2');
  284. this.dom.title.className = `${PREFIX}title`;
  285. this.dom.title.textContent = this.params.title;
  286. this.dom.popup.appendChild(this.dom.title);
  287. }
  288. // Content (Text / HTML / Element)
  289. if (this.params.text || this.params.html || this.params.content) {
  290. this.dom.content = document.createElement('div');
  291. this.dom.content.className = `${PREFIX}content`;
  292. if (this.params.content) {
  293. // DOM Element or Selector
  294. let el = this.params.content;
  295. if (typeof el === 'string') el = document.querySelector(el);
  296. if (el instanceof Element) this.dom.content.appendChild(el);
  297. } else if (this.params.html) {
  298. this.dom.content.innerHTML = this.params.html;
  299. } else {
  300. this.dom.content.textContent = this.params.text;
  301. }
  302. this.dom.popup.appendChild(this.dom.content);
  303. }
  304. // Actions
  305. this.dom.actions = document.createElement('div');
  306. this.dom.actions.className = `${PREFIX}actions`;
  307. // Cancel Button
  308. if (this.params.showCancelButton) {
  309. this.dom.cancelBtn = document.createElement('button');
  310. this.dom.cancelBtn.className = `${PREFIX}button ${PREFIX}cancel`;
  311. this.dom.cancelBtn.textContent = this.params.cancelButtonText;
  312. this.dom.cancelBtn.style.backgroundColor = this.params.cancelButtonColor;
  313. this.dom.cancelBtn.onclick = () => this._close(false);
  314. this.dom.actions.appendChild(this.dom.cancelBtn);
  315. }
  316. // Confirm Button
  317. this.dom.confirmBtn = document.createElement('button');
  318. this.dom.confirmBtn.className = `${PREFIX}button ${PREFIX}confirm`;
  319. this.dom.confirmBtn.textContent = this.params.confirmButtonText;
  320. this.dom.confirmBtn.style.backgroundColor = this.params.confirmButtonColor;
  321. this.dom.confirmBtn.onclick = () => this._close(true);
  322. this.dom.actions.appendChild(this.dom.confirmBtn);
  323. this.dom.popup.appendChild(this.dom.actions);
  324. // Event Listeners
  325. if (this.params.closeOnClickOutside) {
  326. this.dom.overlay.addEventListener('click', (e) => {
  327. if (e.target === this.dom.overlay) {
  328. this._close(null); // Dismiss
  329. }
  330. });
  331. }
  332. document.body.appendChild(this.dom.overlay);
  333. // Animation
  334. requestAnimationFrame(() => {
  335. this.dom.overlay.classList.add('show');
  336. });
  337. }
  338. _createIcon(type) {
  339. const icon = document.createElement('div');
  340. icon.className = `${PREFIX}icon ${type}`;
  341. if (type === 'success') {
  342. const tip = document.createElement('div');
  343. tip.className = `${PREFIX}success-line-tip`;
  344. const long = document.createElement('div');
  345. long.className = `${PREFIX}success-line-long`;
  346. icon.appendChild(tip);
  347. icon.appendChild(long);
  348. } else if (type === 'error') {
  349. const xMark = document.createElement('span');
  350. xMark.className = `${PREFIX}error-x-mark`;
  351. const left = document.createElement('span');
  352. left.className = `${PREFIX}error-line left`;
  353. const right = document.createElement('span');
  354. right.className = `${PREFIX}error-line right`;
  355. xMark.appendChild(left);
  356. xMark.appendChild(right);
  357. icon.appendChild(xMark);
  358. } else if (type === 'warning') {
  359. const body = document.createElement('div');
  360. body.className = `${PREFIX}warning-body`;
  361. const dot = document.createElement('div');
  362. dot.className = `${PREFIX}warning-dot`;
  363. icon.appendChild(body);
  364. icon.appendChild(dot);
  365. }
  366. return icon;
  367. }
  368. _close(isConfirmed) {
  369. this.dom.overlay.classList.remove('show');
  370. setTimeout(() => {
  371. if (this.dom.overlay && this.dom.overlay.parentNode) {
  372. this.dom.overlay.parentNode.removeChild(this.dom.overlay);
  373. }
  374. }, 300);
  375. if (isConfirmed === true) {
  376. this.resolve({ isConfirmed: true, isDenied: false, isDismissed: false });
  377. } else if (isConfirmed === false) {
  378. this.resolve({ isConfirmed: false, isDenied: false, isDismissed: true, dismiss: 'cancel' });
  379. } else {
  380. this.resolve({ isConfirmed: false, isDenied: false, isDismissed: true, dismiss: 'backdrop' });
  381. }
  382. }
  383. }
  384. return Layer;
  385. })));