kerfjs 4.2.0-beta.2 → 4.2.0-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/overlay.js CHANGED
@@ -125,9 +125,10 @@ function confirm(message, options = {}) {
125
125
  title,
126
126
  okText = "OK",
127
127
  cancelText = "Cancel",
128
- danger = false
128
+ danger = false,
129
+ render
129
130
  } = options;
130
- const body = jsx("div", {
131
+ const body = render !== void 0 ? render({ message, ok: { "data-confirm": "ok" }, cancel: { "data-confirm": "cancel" } }) : jsx("div", {
131
132
  class: "kerf-confirm",
132
133
  children: [
133
134
  title !== void 0 ? jsx("h2", { class: "kerf-confirm__title", children: title }) : "",
@@ -150,7 +151,7 @@ function confirm(message, options = {}) {
150
151
  container,
151
152
  className: danger ? `${className} kerf-confirm--danger` : className,
152
153
  dismiss: ["escape", "backdrop"],
153
- initialFocus: ".kerf-confirm__ok",
154
+ initialFocus: '[data-confirm="ok"]',
154
155
  trap: true
155
156
  });
156
157
  delegate(handle.el, "click", "[data-confirm]", (_event, el) => {
@@ -168,20 +169,27 @@ function prompt(message, options = {}) {
168
169
  inputType = "text",
169
170
  okText = "OK",
170
171
  cancelText = "Cancel",
171
- validate
172
+ validate,
173
+ render
172
174
  } = options;
173
- const body = jsx("div", {
175
+ const inputAttrs = {
176
+ "data-prompt-input": "",
177
+ type: inputType,
178
+ value: defaultValue,
179
+ ...placeholder !== void 0 ? { placeholder } : {}
180
+ };
181
+ const body = render !== void 0 ? render({
182
+ message,
183
+ input: inputAttrs,
184
+ error: { "data-prompt-error": "" },
185
+ ok: { "data-prompt": "ok" },
186
+ cancel: { "data-prompt": "cancel" }
187
+ }) : jsx("div", {
174
188
  class: "kerf-prompt",
175
189
  children: [
176
190
  title !== void 0 ? jsx("h2", { class: "kerf-prompt__title", children: title }) : "",
177
191
  jsx("label", { class: "kerf-prompt__message", children: message }),
178
- jsx("input", {
179
- class: "kerf-prompt__input",
180
- type: inputType,
181
- value: defaultValue,
182
- ...placeholder !== void 0 ? { placeholder } : {},
183
- "data-prompt-input": ""
184
- }),
192
+ jsx("input", { class: "kerf-prompt__input", ...inputAttrs }),
185
193
  jsx("p", { class: "kerf-prompt__error", "data-prompt-error": "", children: "" }),
186
194
  jsx("div", {
187
195
  class: "kerf-prompt__actions",
@@ -201,18 +209,20 @@ function prompt(message, options = {}) {
201
209
  container,
202
210
  className,
203
211
  dismiss: ["escape", "backdrop"],
204
- initialFocus: ".kerf-prompt__input",
212
+ initialFocus: "[data-prompt-input]",
205
213
  trap: true
206
214
  });
207
215
  const input = handle.el.querySelector("[data-prompt-input]");
208
216
  const errorEl = handle.el.querySelector("[data-prompt-error]");
209
- errorEl.hidden = true;
217
+ if (errorEl !== null) errorEl.hidden = true;
210
218
  function attemptOk() {
211
219
  const value = input.value;
212
220
  const error = validate?.(value);
213
221
  if (typeof error === "string" && error.length > 0) {
214
- errorEl.textContent = error;
215
- errorEl.hidden = false;
222
+ if (errorEl !== null) {
223
+ errorEl.textContent = error;
224
+ errorEl.hidden = false;
225
+ }
216
226
  input.focus();
217
227
  return;
218
228
  }
@@ -231,8 +241,24 @@ function prompt(message, options = {}) {
231
241
  return handle.result.then((value) => typeof value === "string" ? value : null);
232
242
  }
233
243
  function form(fields, options = {}) {
234
- const { container, className = "kerf-overlay", title, okText = "OK", cancelText = "Cancel" } = options;
235
- const body = jsx("div", {
244
+ const { container, className = "kerf-overlay", title, okText = "OK", cancelText = "Cancel", render } = options;
245
+ const fieldAttrs = (field) => ({
246
+ "data-field": field.name,
247
+ name: field.name,
248
+ type: field.type ?? "text",
249
+ value: field.defaultValue ?? "",
250
+ ...field.placeholder !== void 0 ? { placeholder: field.placeholder } : {}
251
+ });
252
+ const body = render !== void 0 ? render({
253
+ fields: fields.map((field) => ({
254
+ name: field.name,
255
+ label: field.label ?? field.name,
256
+ input: fieldAttrs(field),
257
+ error: { "data-field-error": field.name }
258
+ })),
259
+ ok: { "data-form": "ok" },
260
+ cancel: { "data-form": "cancel" }
261
+ }) : jsx("div", {
236
262
  class: "kerf-form",
237
263
  children: [
238
264
  title !== void 0 ? jsx("h2", { class: "kerf-form__title", children: title }) : "",
@@ -241,19 +267,8 @@ function form(fields, options = {}) {
241
267
  class: "kerf-form__field",
242
268
  children: [
243
269
  jsx("label", { class: "kerf-form__label", children: field.label ?? field.name }),
244
- jsx("input", {
245
- class: "kerf-form__input",
246
- type: field.type ?? "text",
247
- name: field.name,
248
- value: field.defaultValue ?? "",
249
- ...field.placeholder !== void 0 ? { placeholder: field.placeholder } : {},
250
- "data-field": field.name
251
- }),
252
- jsx("p", {
253
- class: "kerf-form__error",
254
- "data-field-error": field.name,
255
- children: ""
256
- })
270
+ jsx("input", { class: "kerf-form__input", ...fieldAttrs(field) }),
271
+ jsx("p", { class: "kerf-form__error", "data-field-error": field.name, children: "" })
257
272
  ]
258
273
  })
259
274
  ),
@@ -275,14 +290,18 @@ function form(fields, options = {}) {
275
290
  container,
276
291
  className,
277
292
  dismiss: ["escape", "backdrop"],
278
- initialFocus: ".kerf-form__input",
293
+ initialFocus: "[data-field]",
279
294
  trap: true
280
295
  });
281
296
  const byAttr = (attr, name) => Array.from(handle.el.querySelectorAll(`[${attr}]`)).find(
282
297
  (el) => el.getAttribute(attr) === name
283
298
  );
299
+ const errorFor = (name) => Array.from(handle.el.querySelectorAll("[data-field-error]")).find(
300
+ (el) => el.getAttribute("data-field-error") === name
301
+ ) ?? null;
284
302
  for (const field of fields) {
285
- byAttr("data-field-error", field.name).hidden = true;
303
+ const errorEl = errorFor(field.name);
304
+ if (errorEl !== null) errorEl.hidden = true;
286
305
  }
287
306
  function attemptOk() {
288
307
  const record = {};
@@ -292,12 +311,14 @@ function form(fields, options = {}) {
292
311
  const value = el.value;
293
312
  record[field.name] = value;
294
313
  const error = field.validate?.(value);
295
- const errorEl = byAttr("data-field-error", field.name);
314
+ const errorEl = errorFor(field.name);
296
315
  if (typeof error === "string" && error.length > 0) {
297
- errorEl.textContent = error;
298
- errorEl.hidden = false;
316
+ if (errorEl !== null) {
317
+ errorEl.textContent = error;
318
+ errorEl.hidden = false;
319
+ }
299
320
  if (firstInvalid === null) firstInvalid = el;
300
- } else {
321
+ } else if (errorEl !== null) {
301
322
  errorEl.hidden = true;
302
323
  }
303
324
  }
@@ -321,6 +342,83 @@ function form(fields, options = {}) {
321
342
  (value) => value !== null && typeof value === "object" ? value : null
322
343
  );
323
344
  }
345
+ function choice(message, actions, options = {}) {
346
+ const { container, className = "kerf-overlay", title, defaultValue, render } = options;
347
+ const hasDefault = "defaultValue" in options;
348
+ const actionAttrs = actions.map((_, i) => ({ "data-choice": String(i) }));
349
+ const body = render !== void 0 ? render({ message, actions: actionAttrs }) : jsx("div", {
350
+ class: "kerf-choice",
351
+ children: [
352
+ title !== void 0 ? jsx("h2", { class: "kerf-choice__title", children: title }) : "",
353
+ jsx("p", { class: "kerf-choice__message", children: message }),
354
+ jsx("div", {
355
+ class: "kerf-choice__actions",
356
+ children: actions.map(
357
+ (action, i) => jsx("button", {
358
+ type: "button",
359
+ class: action.className !== void 0 ? `kerf-choice__action ${action.className}` : "kerf-choice__action",
360
+ ...actionAttrs[i],
361
+ children: action.label
362
+ })
363
+ )
364
+ })
365
+ ]
366
+ });
367
+ let resolveChoice;
368
+ const result = new Promise((resolve) => {
369
+ resolveChoice = resolve;
370
+ });
371
+ const handle = overlay(body, {
372
+ container,
373
+ className,
374
+ dismiss: ["escape", "backdrop"],
375
+ initialFocus: "[data-choice]",
376
+ trap: true
377
+ });
378
+ delegate(handle.el, "click", "[data-choice]", (_event, el) => {
379
+ resolveChoice(actions[Number(el.getAttribute("data-choice"))].value);
380
+ handle.close();
381
+ });
382
+ if (hasDefault) {
383
+ handle.el.addEventListener("keydown", (event) => {
384
+ if (event.key === "Enter") {
385
+ event.preventDefault();
386
+ resolveChoice(defaultValue);
387
+ handle.close();
388
+ }
389
+ });
390
+ }
391
+ void handle.result.then(() => resolveChoice(null));
392
+ return result;
393
+ }
394
+ function positionAnchored(el, anchor, options = {}) {
395
+ const { placement = "bottom", align = "start", gap = 4 } = options;
396
+ const a = anchor.getBoundingClientRect();
397
+ const p = el.getBoundingClientRect();
398
+ const vw = window.innerWidth;
399
+ const vh = window.innerHeight;
400
+ const belowTop = a.bottom + gap;
401
+ const aboveTop = a.top - gap - p.height;
402
+ let below = placement !== "top";
403
+ if (below && belowTop + p.height > vh && aboveTop >= 0) below = false;
404
+ else if (!below && aboveTop < 0 && belowTop + p.height <= vh) below = true;
405
+ let left = align === "end" ? a.right - p.width : a.left;
406
+ left = Math.max(0, Math.min(left, vw - p.width));
407
+ el.style.position = "fixed";
408
+ el.style.margin = "0";
409
+ el.style.left = `${left}px`;
410
+ el.style.top = `${below ? belowTop : aboveTop}px`;
411
+ }
412
+ function autoReposition(el, anchor, options = {}) {
413
+ const reposition = () => positionAnchored(el, anchor, options);
414
+ reposition();
415
+ window.addEventListener("scroll", reposition, true);
416
+ window.addEventListener("resize", reposition);
417
+ return () => {
418
+ window.removeEventListener("scroll", reposition, true);
419
+ window.removeEventListener("resize", reposition);
420
+ };
421
+ }
324
422
  function popover(anchor, content, options = {}) {
325
423
  const {
326
424
  container,
@@ -343,32 +441,62 @@ function popover(anchor, content, options = {}) {
343
441
  onDismiss,
344
442
  outsideIgnore: [anchor, ...extraIgnore]
345
443
  });
346
- handle.el.style.position = "fixed";
347
- handle.el.style.margin = "0";
348
- const reposition = () => {
349
- const a = anchor.getBoundingClientRect();
350
- const p = handle.el.getBoundingClientRect();
351
- const vw = window.innerWidth;
352
- const vh = window.innerHeight;
353
- const belowTop = a.bottom + gap;
354
- const aboveTop = a.top - gap - p.height;
355
- let below = placement !== "top";
356
- if (below && belowTop + p.height > vh && aboveTop >= 0) below = false;
357
- else if (!below && aboveTop < 0 && belowTop + p.height <= vh) below = true;
358
- let left = align === "end" ? a.right - p.width : a.left;
359
- left = Math.max(0, Math.min(left, vw - p.width));
360
- handle.el.style.left = `${left}px`;
361
- handle.el.style.top = `${below ? belowTop : aboveTop}px`;
362
- };
363
- reposition();
364
- window.addEventListener("scroll", reposition, true);
365
- window.addEventListener("resize", reposition);
366
- void handle.result.then(() => {
367
- window.removeEventListener("scroll", reposition, true);
368
- window.removeEventListener("resize", reposition);
369
- });
444
+ const stopReposition = autoReposition(handle.el, anchor, { placement, align, gap });
445
+ void handle.result.then(stopReposition);
370
446
  return handle;
371
447
  }
448
+ function tooltip(anchor, content, options = {}) {
449
+ const {
450
+ container,
451
+ className = "kerf-tooltip",
452
+ delay = 400,
453
+ hideDelay = 100,
454
+ role = "tooltip",
455
+ placement = "top",
456
+ align = "start",
457
+ gap = 4
458
+ } = options;
459
+ const body = typeof content === "function" ? content : typeof content === "string" ? jsx("span", { class: `${className}__text`, children: content }) : content;
460
+ const timers = {};
461
+ let current;
462
+ function show() {
463
+ const handle = overlay(body, { container, className, dismiss: false, trap: false, initialFocus: false });
464
+ handle.el.setAttribute("role", role);
465
+ const stop = autoReposition(handle.el, anchor, { placement, align, gap });
466
+ current = { handle, stop };
467
+ }
468
+ function hide() {
469
+ if (current === void 0) return;
470
+ current.stop();
471
+ current.handle.close();
472
+ current = void 0;
473
+ }
474
+ const onEnter = () => {
475
+ if (timers.hide !== void 0) clearTimeout(timers.hide);
476
+ if (current !== void 0) return;
477
+ if (timers.show !== void 0) clearTimeout(timers.show);
478
+ timers.show = setTimeout(show, delay);
479
+ };
480
+ const onLeave = () => {
481
+ if (timers.show !== void 0) clearTimeout(timers.show);
482
+ if (current === void 0) return;
483
+ timers.hide = setTimeout(hide, hideDelay);
484
+ };
485
+ anchor.addEventListener("pointerenter", onEnter);
486
+ anchor.addEventListener("pointerleave", onLeave);
487
+ anchor.addEventListener("focus", onEnter);
488
+ anchor.addEventListener("blur", onLeave);
489
+ return () => {
490
+ anchor.removeEventListener("pointerenter", onEnter);
491
+ anchor.removeEventListener("pointerleave", onLeave);
492
+ anchor.removeEventListener("focus", onEnter);
493
+ anchor.removeEventListener("blur", onLeave);
494
+ if (timers.show !== void 0) clearTimeout(timers.show);
495
+ if (timers.hide !== void 0) clearTimeout(timers.hide);
496
+ hide();
497
+ };
498
+ }
499
+ var TOAST_SET = /* @__PURE__ */ Symbol("kerf.toasts");
372
500
  function toastRegion(container) {
373
501
  if (container !== void 0) return container;
374
502
  const existing = document.querySelector(".kerf-toasts");
@@ -380,27 +508,68 @@ function toastRegion(container) {
380
508
  return region;
381
509
  }
382
510
  function toast(content, options = {}) {
383
- const { container, className = "kerf-toast", duration = 4e3, role = "status" } = options;
511
+ const {
512
+ container,
513
+ className = "kerf-toast",
514
+ duration = 4e3,
515
+ role = "status",
516
+ mode = "stack",
517
+ collapse = "fade",
518
+ variant,
519
+ enterClass,
520
+ exitClass,
521
+ exitDuration = 0
522
+ } = options;
523
+ const region = toastRegion(container);
524
+ const active = region[TOAST_SET] ??= /* @__PURE__ */ new Set();
525
+ if (mode === "replace") {
526
+ for (const d of [...active]) {
527
+ if (collapse === "instant") d.removeNow();
528
+ else d();
529
+ }
530
+ }
384
531
  const el = document.createElement("div");
385
532
  el.className = className;
533
+ if (variant !== void 0) el.classList.add(`${className}--${variant}`);
386
534
  el.setAttribute("role", role);
387
- toastRegion(container).appendChild(el);
535
+ region.appendChild(el);
388
536
  const disposeMount = mount(el, typeof content === "function" ? content : () => content);
389
- const state = {
390
- dismissed: false,
391
- timer: void 0
537
+ const state = { dismissed: false, timer: void 0 };
538
+ if (enterClass !== void 0) {
539
+ globalThis.requestAnimationFrame(() => {
540
+ if (!state.dismissed) el.classList.add(enterClass);
541
+ });
542
+ }
543
+ const remove = () => {
544
+ disposeMount();
545
+ el.remove();
546
+ active.delete(dismiss);
392
547
  };
393
- function dismiss() {
548
+ const finish = (instant) => {
394
549
  if (state.dismissed) return;
395
550
  state.dismissed = true;
396
551
  if (state.timer !== void 0) clearTimeout(state.timer);
397
- disposeMount();
398
- el.remove();
552
+ if (instant) {
553
+ remove();
554
+ return;
555
+ }
556
+ if (enterClass !== void 0) el.classList.remove(enterClass);
557
+ if (exitClass !== void 0) el.classList.add(exitClass);
558
+ if (exitClass !== void 0 || exitDuration > 0) {
559
+ setTimeout(remove, exitDuration);
560
+ } else {
561
+ remove();
562
+ }
563
+ };
564
+ function dismiss() {
565
+ finish(false);
399
566
  }
567
+ dismiss.removeNow = () => finish(true);
568
+ active.add(dismiss);
400
569
  if (duration > 0) state.timer = setTimeout(dismiss, duration);
401
- return dismiss;
570
+ return { el, dismiss };
402
571
  }
403
572
 
404
- export { confirm, form, overlay, popover, prompt, toast };
573
+ export { autoReposition, choice, confirm, form, overlay, popover, positionAnchored, prompt, toast, tooltip };
405
574
  //# sourceMappingURL=overlay.js.map
406
575
  //# sourceMappingURL=overlay.js.map