@sv443-network/userutils 7.0.0 → 7.1.0
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/CHANGELOG.md +13 -0
- package/README.md +241 -6
- package/dist/index.global.js +495 -1
- package/dist/index.js +471 -31
- package/dist/lib/Dialog.d.ts +131 -0
- package/dist/lib/NanoEmitter.d.ts +20 -0
- package/dist/lib/index.d.ts +2 -0
- package/package.json +6 -3
- package/dist/index.mjs +0 -844
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
import { createNanoEvents } from 'nanoevents';
|
|
2
2
|
|
|
3
3
|
var __defProp = Object.defineProperty;
|
|
4
4
|
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
@@ -531,6 +531,475 @@ function getSiblingsFrame(refElement, siblingAmount, refElementAlignment = "cent
|
|
|
531
531
|
return [...siblings.slice(elemSiblIdx - siblingAmount + Number(includeRef), elemSiblIdx + Number(includeRef))];
|
|
532
532
|
return [];
|
|
533
533
|
}
|
|
534
|
+
var NanoEmitter = class {
|
|
535
|
+
constructor(options = {}) {
|
|
536
|
+
__publicField(this, "events", createNanoEvents());
|
|
537
|
+
__publicField(this, "eventUnsubscribes", []);
|
|
538
|
+
__publicField(this, "emitterOptions");
|
|
539
|
+
this.emitterOptions = __spreadValues({
|
|
540
|
+
publicEmit: false
|
|
541
|
+
}, options);
|
|
542
|
+
}
|
|
543
|
+
/** Subscribes to an event - returns a function that unsubscribes the event listener */
|
|
544
|
+
on(event, cb) {
|
|
545
|
+
let unsub;
|
|
546
|
+
const unsubProxy = () => {
|
|
547
|
+
if (!unsub)
|
|
548
|
+
return;
|
|
549
|
+
unsub();
|
|
550
|
+
this.eventUnsubscribes = this.eventUnsubscribes.filter((u) => u !== unsub);
|
|
551
|
+
};
|
|
552
|
+
unsub = this.events.on(event, cb);
|
|
553
|
+
this.eventUnsubscribes.push(unsub);
|
|
554
|
+
return unsubProxy;
|
|
555
|
+
}
|
|
556
|
+
/** Subscribes to an event and calls the callback or resolves the Promise only once */
|
|
557
|
+
once(event, cb) {
|
|
558
|
+
return new Promise((resolve) => {
|
|
559
|
+
let unsub;
|
|
560
|
+
const onceProxy = (...args) => {
|
|
561
|
+
unsub();
|
|
562
|
+
cb == null ? void 0 : cb(...args);
|
|
563
|
+
resolve(args);
|
|
564
|
+
};
|
|
565
|
+
unsub = this.on(event, onceProxy);
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
/** Emits an event on this instance - Needs `publicEmit` to be set to true in the constructor! */
|
|
569
|
+
emit(event, ...args) {
|
|
570
|
+
if (this.emitterOptions.publicEmit) {
|
|
571
|
+
this.events.emit(event, ...args);
|
|
572
|
+
return true;
|
|
573
|
+
}
|
|
574
|
+
return false;
|
|
575
|
+
}
|
|
576
|
+
/** Unsubscribes all event listeners */
|
|
577
|
+
unsubscribeAll() {
|
|
578
|
+
for (const unsub of this.eventUnsubscribes)
|
|
579
|
+
unsub();
|
|
580
|
+
this.eventUnsubscribes = [];
|
|
581
|
+
}
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
// lib/Dialog.ts
|
|
585
|
+
var defaultDialogCss = `.uu-no-select {
|
|
586
|
+
user-select: none;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
.uu-dialog-bg {
|
|
590
|
+
--uu-dialog-bg: #333333;
|
|
591
|
+
--uu-dialog-bg-highlight: #252525;
|
|
592
|
+
--uu-scroll-indicator-bg: rgba(10, 10, 10, 0.7);
|
|
593
|
+
--uu-dialog-separator-color: #797979;
|
|
594
|
+
--uu-dialog-border-radius: 10px;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
.uu-dialog-bg {
|
|
598
|
+
display: block;
|
|
599
|
+
position: fixed;
|
|
600
|
+
width: 100%;
|
|
601
|
+
height: 100%;
|
|
602
|
+
top: 0;
|
|
603
|
+
left: 0;
|
|
604
|
+
z-index: 5;
|
|
605
|
+
background-color: rgba(0, 0, 0, 0.6);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
.uu-dialog {
|
|
609
|
+
--uu-calc-dialog-height: calc(min(100vh - 40px, var(--uu-dialog-height-max)));
|
|
610
|
+
position: absolute;
|
|
611
|
+
display: flex;
|
|
612
|
+
flex-direction: column;
|
|
613
|
+
width: calc(min(100% - 60px, var(--uu-dialog-width-max)));
|
|
614
|
+
border-radius: var(--uu-dialog-border-radius);
|
|
615
|
+
height: auto;
|
|
616
|
+
max-height: var(--uu-calc-dialog-height);
|
|
617
|
+
left: 50%;
|
|
618
|
+
top: 50%;
|
|
619
|
+
transform: translate(-50%, -50%);
|
|
620
|
+
z-index: 6;
|
|
621
|
+
color: #fff;
|
|
622
|
+
background-color: var(--uu-dialog-bg);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
.uu-dialog.align-top {
|
|
626
|
+
top: 0;
|
|
627
|
+
transform: translate(-50%, 40px);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
.uu-dialog.align-bottom {
|
|
631
|
+
top: 100%;
|
|
632
|
+
transform: translate(-50%, -100%);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
.uu-dialog-body {
|
|
636
|
+
font-size: 1.5rem;
|
|
637
|
+
padding: 20px;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
.uu-dialog-body.small {
|
|
641
|
+
padding: 15px;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
#uu-dialog-opts {
|
|
645
|
+
display: flex;
|
|
646
|
+
flex-direction: column;
|
|
647
|
+
position: relative;
|
|
648
|
+
padding: 30px 0px;
|
|
649
|
+
overflow-y: auto;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
.uu-dialog-header {
|
|
653
|
+
display: flex;
|
|
654
|
+
justify-content: space-between;
|
|
655
|
+
align-items: center;
|
|
656
|
+
margin-bottom: 6px;
|
|
657
|
+
padding: 15px 20px 15px 20px;
|
|
658
|
+
background-color: var(--uu-dialog-bg);
|
|
659
|
+
border: 2px solid var(--uu-dialog-separator-color);
|
|
660
|
+
border-style: none none solid none !important;
|
|
661
|
+
border-radius: var(--uu-dialog-border-radius) var(--uu-dialog-border-radius) 0px 0px;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
.uu-dialog-header.small {
|
|
665
|
+
padding: 10px 15px;
|
|
666
|
+
border-style: none none solid none !important;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
.uu-dialog-header-pad {
|
|
670
|
+
content: " ";
|
|
671
|
+
min-height: 32px;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
.uu-dialog-header-pad.small {
|
|
675
|
+
min-height: 24px;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
.uu-dialog-titlecont {
|
|
679
|
+
display: flex;
|
|
680
|
+
align-items: center;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
.uu-dialog-titlecont-no-title {
|
|
684
|
+
display: flex;
|
|
685
|
+
justify-content: flex-end;
|
|
686
|
+
align-items: center;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
.uu-dialog-title {
|
|
690
|
+
position: relative;
|
|
691
|
+
display: inline-block;
|
|
692
|
+
font-size: 22px;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
.uu-dialog-close {
|
|
696
|
+
cursor: pointer;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
.uu-dialog-header-img,
|
|
700
|
+
.uu-dialog-close
|
|
701
|
+
{
|
|
702
|
+
width: 32px;
|
|
703
|
+
height: 32px;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
.uu-dialog-header-img.small,
|
|
707
|
+
.uu-dialog-close.small
|
|
708
|
+
{
|
|
709
|
+
width: 24px;
|
|
710
|
+
height: 24px;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
.uu-dialog-footer {
|
|
714
|
+
font-size: 17px;
|
|
715
|
+
text-decoration: underline;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
.uu-dialog-footer.hidden {
|
|
719
|
+
display: none;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
.uu-dialog-footer-cont {
|
|
723
|
+
margin-top: 6px;
|
|
724
|
+
padding: 15px 20px;
|
|
725
|
+
background: var(--uu-dialog-bg);
|
|
726
|
+
background: linear-gradient(to bottom, rgba(0, 0, 0, 0) 0%, var(--uu-dialog-bg) 30%, var(--uu-dialog-bg) 100%);
|
|
727
|
+
border: 2px solid var(--uu-dialog-separator-color);
|
|
728
|
+
border-style: solid none none none !important;
|
|
729
|
+
border-radius: 0px 0px var(--uu-dialog-border-radius) var(--uu-dialog-border-radius);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
.uu-dialog-footer-buttons-cont button:not(:last-of-type) {
|
|
733
|
+
margin-right: 15px;
|
|
734
|
+
}`;
|
|
735
|
+
var currentDialogId = null;
|
|
736
|
+
var openDialogs = [];
|
|
737
|
+
var defaultStrings = {
|
|
738
|
+
closeDialogTooltip: "Click to close the dialog"
|
|
739
|
+
};
|
|
740
|
+
var Dialog = class _Dialog extends NanoEmitter {
|
|
741
|
+
constructor(options) {
|
|
742
|
+
super();
|
|
743
|
+
/** Options passed to the dialog in the constructor */
|
|
744
|
+
__publicField(this, "options");
|
|
745
|
+
/** ID that gets added to child element IDs - has to be unique and conform to HTML ID naming rules! */
|
|
746
|
+
__publicField(this, "id");
|
|
747
|
+
/** Strings used in the dialog (used for translations) */
|
|
748
|
+
__publicField(this, "strings");
|
|
749
|
+
__publicField(this, "dialogOpen", false);
|
|
750
|
+
__publicField(this, "dialogMounted", false);
|
|
751
|
+
const _a = options, { strings } = _a, opts = __objRest(_a, ["strings"]);
|
|
752
|
+
this.strings = __spreadValues(__spreadValues({}, defaultStrings), strings != null ? strings : {});
|
|
753
|
+
this.options = __spreadValues({
|
|
754
|
+
closeOnBgClick: true,
|
|
755
|
+
closeOnEscPress: true,
|
|
756
|
+
destroyOnClose: false,
|
|
757
|
+
unmountOnClose: true,
|
|
758
|
+
removeListenersOnDestroy: true,
|
|
759
|
+
smallHeader: false,
|
|
760
|
+
verticalAlign: "center",
|
|
761
|
+
dialogCss: defaultDialogCss
|
|
762
|
+
}, opts);
|
|
763
|
+
this.id = opts.id;
|
|
764
|
+
}
|
|
765
|
+
//#region public
|
|
766
|
+
/** Call after DOMContentLoaded to pre-render the dialog and invisibly mount it in the DOM */
|
|
767
|
+
mount() {
|
|
768
|
+
return __async(this, null, function* () {
|
|
769
|
+
if (this.dialogMounted)
|
|
770
|
+
return;
|
|
771
|
+
this.dialogMounted = true;
|
|
772
|
+
if (!document.querySelector("style.uu-dialog-css"))
|
|
773
|
+
addGlobalStyle(this.options.dialogCss).classList.add("uu-dialog-css");
|
|
774
|
+
const bgElem = document.createElement("div");
|
|
775
|
+
bgElem.id = `uu-${this.id}-dialog-bg`;
|
|
776
|
+
bgElem.classList.add("uu-dialog-bg");
|
|
777
|
+
if (this.options.closeOnBgClick)
|
|
778
|
+
bgElem.ariaLabel = bgElem.title = this.getString("closeDialogTooltip");
|
|
779
|
+
bgElem.style.setProperty("--uu-dialog-width-max", `${this.options.width}px`);
|
|
780
|
+
bgElem.style.setProperty("--uu-dialog-height-max", `${this.options.height}px`);
|
|
781
|
+
bgElem.style.visibility = "hidden";
|
|
782
|
+
bgElem.style.display = "none";
|
|
783
|
+
bgElem.inert = true;
|
|
784
|
+
bgElem.appendChild(yield this.getDialogContent());
|
|
785
|
+
document.body.appendChild(bgElem);
|
|
786
|
+
this.attachListeners(bgElem);
|
|
787
|
+
this.events.emit("render");
|
|
788
|
+
return bgElem;
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
/** Closes the dialog and clears all its contents (unmounts elements from the DOM) in preparation for a new rendering call */
|
|
792
|
+
unmount() {
|
|
793
|
+
var _a;
|
|
794
|
+
this.close();
|
|
795
|
+
this.dialogMounted = false;
|
|
796
|
+
const clearSelectors = [
|
|
797
|
+
`#uu-${this.id}-dialog-bg`,
|
|
798
|
+
`#uu-style-dialog-${this.id}`
|
|
799
|
+
];
|
|
800
|
+
for (const sel of clearSelectors)
|
|
801
|
+
(_a = document.querySelector(sel)) == null ? void 0 : _a.remove();
|
|
802
|
+
this.events.emit("clear");
|
|
803
|
+
}
|
|
804
|
+
/** Clears the DOM of the dialog and then renders it again */
|
|
805
|
+
remount() {
|
|
806
|
+
return __async(this, null, function* () {
|
|
807
|
+
this.unmount();
|
|
808
|
+
yield this.mount();
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
/**
|
|
812
|
+
* Opens the dialog - also mounts it if it hasn't been mounted yet
|
|
813
|
+
* Prevents default action and immediate propagation of the passed event
|
|
814
|
+
*/
|
|
815
|
+
open(e) {
|
|
816
|
+
return __async(this, null, function* () {
|
|
817
|
+
var _a;
|
|
818
|
+
e == null ? void 0 : e.preventDefault();
|
|
819
|
+
e == null ? void 0 : e.stopImmediatePropagation();
|
|
820
|
+
if (this.isOpen())
|
|
821
|
+
return;
|
|
822
|
+
this.dialogOpen = true;
|
|
823
|
+
if (openDialogs.includes(this.id))
|
|
824
|
+
throw new Error(`A dialog with the same ID of '${this.id}' already exists and is open!`);
|
|
825
|
+
if (!this.isMounted())
|
|
826
|
+
yield this.mount();
|
|
827
|
+
const dialogBg = document.querySelector(`#uu-${this.id}-dialog-bg`);
|
|
828
|
+
if (!dialogBg)
|
|
829
|
+
return console.warn(`Couldn't find background element for dialog with ID '${this.id}'`);
|
|
830
|
+
dialogBg.style.visibility = "visible";
|
|
831
|
+
dialogBg.style.display = "block";
|
|
832
|
+
dialogBg.inert = false;
|
|
833
|
+
currentDialogId = this.id;
|
|
834
|
+
openDialogs.unshift(this.id);
|
|
835
|
+
for (const dialogId of openDialogs)
|
|
836
|
+
if (dialogId !== this.id)
|
|
837
|
+
(_a = document.querySelector(`#uu-${dialogId}-dialog-bg`)) == null ? void 0 : _a.setAttribute("inert", "true");
|
|
838
|
+
document.body.classList.remove("uu-no-select");
|
|
839
|
+
document.body.setAttribute("inert", "true");
|
|
840
|
+
this.events.emit("open");
|
|
841
|
+
return dialogBg;
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
/** Closes the dialog - prevents default action and immediate propagation of the passed event */
|
|
845
|
+
close(e) {
|
|
846
|
+
var _a, _b;
|
|
847
|
+
e == null ? void 0 : e.preventDefault();
|
|
848
|
+
e == null ? void 0 : e.stopImmediatePropagation();
|
|
849
|
+
if (!this.isOpen())
|
|
850
|
+
return;
|
|
851
|
+
this.dialogOpen = false;
|
|
852
|
+
const dialogBg = document.querySelector(`#uu-${this.id}-dialog-bg`);
|
|
853
|
+
if (!dialogBg)
|
|
854
|
+
return console.warn(`Couldn't find background element for dialog with ID '${this.id}'`);
|
|
855
|
+
dialogBg.style.visibility = "hidden";
|
|
856
|
+
dialogBg.style.display = "none";
|
|
857
|
+
dialogBg.inert = true;
|
|
858
|
+
openDialogs.splice(openDialogs.indexOf(this.id), 1);
|
|
859
|
+
currentDialogId = (_a = openDialogs[0]) != null ? _a : null;
|
|
860
|
+
if (currentDialogId)
|
|
861
|
+
(_b = document.querySelector(`#uu-${currentDialogId}-dialog-bg`)) == null ? void 0 : _b.removeAttribute("inert");
|
|
862
|
+
if (openDialogs.length === 0) {
|
|
863
|
+
document.body.classList.add("uu-no-select");
|
|
864
|
+
document.body.removeAttribute("inert");
|
|
865
|
+
}
|
|
866
|
+
this.events.emit("close");
|
|
867
|
+
if (this.options.destroyOnClose)
|
|
868
|
+
this.destroy();
|
|
869
|
+
else if (this.options.unmountOnClose)
|
|
870
|
+
this.unmount();
|
|
871
|
+
}
|
|
872
|
+
/** Returns true if the dialog is currently open */
|
|
873
|
+
isOpen() {
|
|
874
|
+
return this.dialogOpen;
|
|
875
|
+
}
|
|
876
|
+
/** Returns true if the dialog is currently mounted */
|
|
877
|
+
isMounted() {
|
|
878
|
+
return this.dialogMounted;
|
|
879
|
+
}
|
|
880
|
+
/** Clears the DOM of the dialog and removes all event listeners */
|
|
881
|
+
destroy() {
|
|
882
|
+
this.unmount();
|
|
883
|
+
this.events.emit("destroy");
|
|
884
|
+
this.options.removeListenersOnDestroy && this.unsubscribeAll();
|
|
885
|
+
}
|
|
886
|
+
//#region static
|
|
887
|
+
/** Returns the ID of the top-most dialog (the dialog that has been opened last) */
|
|
888
|
+
static getCurrentDialogId() {
|
|
889
|
+
return currentDialogId;
|
|
890
|
+
}
|
|
891
|
+
/** Returns the IDs of all currently open dialogs, top-most first */
|
|
892
|
+
static getOpenDialogs() {
|
|
893
|
+
return openDialogs;
|
|
894
|
+
}
|
|
895
|
+
//#region protected
|
|
896
|
+
getString(key) {
|
|
897
|
+
var _a;
|
|
898
|
+
return (_a = this.strings[key]) != null ? _a : defaultStrings[key];
|
|
899
|
+
}
|
|
900
|
+
/** Called once to attach all generic event listeners */
|
|
901
|
+
attachListeners(bgElem) {
|
|
902
|
+
if (this.options.closeOnBgClick) {
|
|
903
|
+
bgElem.addEventListener("click", (e) => {
|
|
904
|
+
var _a;
|
|
905
|
+
if (this.isOpen() && ((_a = e.target) == null ? void 0 : _a.id) === `uu-${this.id}-dialog-bg`)
|
|
906
|
+
this.close(e);
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
if (this.options.closeOnEscPress) {
|
|
910
|
+
document.body.addEventListener("keydown", (e) => {
|
|
911
|
+
if (e.key === "Escape" && this.isOpen() && _Dialog.getCurrentDialogId() === this.id)
|
|
912
|
+
this.close(e);
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
//#region protected
|
|
917
|
+
/**
|
|
918
|
+
* Adds generic, accessible interaction listeners to the passed element.
|
|
919
|
+
* All listeners have the default behavior prevented and stop propagation (for keyboard events only as long as the captured key is valid).
|
|
920
|
+
* @param listenerOptions Provide a {@linkcode listenerOptions} object to configure the listeners
|
|
921
|
+
*/
|
|
922
|
+
onInteraction(elem, listener, listenerOptions) {
|
|
923
|
+
const _a = listenerOptions != null ? listenerOptions : {}, { preventDefault = true, stopPropagation = true } = _a, listenerOpts = __objRest(_a, ["preventDefault", "stopPropagation"]);
|
|
924
|
+
const interactionKeys = ["Enter", " ", "Space"];
|
|
925
|
+
const proxListener = (e) => {
|
|
926
|
+
if (e instanceof KeyboardEvent) {
|
|
927
|
+
if (interactionKeys.includes(e.key)) {
|
|
928
|
+
preventDefault && e.preventDefault();
|
|
929
|
+
stopPropagation && e.stopPropagation();
|
|
930
|
+
} else
|
|
931
|
+
return;
|
|
932
|
+
} else if (e instanceof MouseEvent) {
|
|
933
|
+
preventDefault && e.preventDefault();
|
|
934
|
+
stopPropagation && e.stopPropagation();
|
|
935
|
+
}
|
|
936
|
+
(listenerOpts == null ? void 0 : listenerOpts.once) && e.type === "keydown" && elem.removeEventListener("click", proxListener, listenerOpts);
|
|
937
|
+
(listenerOpts == null ? void 0 : listenerOpts.once) && e.type === "click" && elem.removeEventListener("keydown", proxListener, listenerOpts);
|
|
938
|
+
listener(e);
|
|
939
|
+
};
|
|
940
|
+
elem.addEventListener("click", proxListener, listenerOpts);
|
|
941
|
+
elem.addEventListener("keydown", proxListener, listenerOpts);
|
|
942
|
+
}
|
|
943
|
+
/** Returns the dialog content element and all its children */
|
|
944
|
+
getDialogContent() {
|
|
945
|
+
return __async(this, null, function* () {
|
|
946
|
+
var _a, _b, _c, _d;
|
|
947
|
+
const header = (_b = (_a = this.options).renderHeader) == null ? void 0 : _b.call(_a);
|
|
948
|
+
const footer = (_d = (_c = this.options).renderFooter) == null ? void 0 : _d.call(_c);
|
|
949
|
+
const dialogWrapperEl = document.createElement("div");
|
|
950
|
+
dialogWrapperEl.id = `uu-${this.id}-dialog`;
|
|
951
|
+
dialogWrapperEl.classList.add("uu-dialog");
|
|
952
|
+
dialogWrapperEl.ariaLabel = dialogWrapperEl.title = "";
|
|
953
|
+
dialogWrapperEl.role = "dialog";
|
|
954
|
+
dialogWrapperEl.setAttribute("aria-labelledby", `uu-${this.id}-dialog-title`);
|
|
955
|
+
dialogWrapperEl.setAttribute("aria-describedby", `uu-${this.id}-dialog-body`);
|
|
956
|
+
if (this.options.verticalAlign !== "center")
|
|
957
|
+
dialogWrapperEl.classList.add(`align-${this.options.verticalAlign}`);
|
|
958
|
+
const headerWrapperEl = document.createElement("div");
|
|
959
|
+
headerWrapperEl.classList.add("uu-dialog-header");
|
|
960
|
+
this.options.small && headerWrapperEl.classList.add("small");
|
|
961
|
+
if (header) {
|
|
962
|
+
const headerTitleWrapperEl = document.createElement("div");
|
|
963
|
+
headerTitleWrapperEl.id = `uu-${this.id}-dialog-title`;
|
|
964
|
+
headerTitleWrapperEl.classList.add("uu-dialog-title-wrapper");
|
|
965
|
+
headerTitleWrapperEl.role = "heading";
|
|
966
|
+
headerTitleWrapperEl.ariaLevel = "1";
|
|
967
|
+
headerTitleWrapperEl.appendChild(header instanceof Promise ? yield header : header);
|
|
968
|
+
headerWrapperEl.appendChild(headerTitleWrapperEl);
|
|
969
|
+
} else {
|
|
970
|
+
const padEl = document.createElement("div");
|
|
971
|
+
padEl.classList.add("uu-dialog-header-pad", this.options.small ? "small" : "");
|
|
972
|
+
headerWrapperEl.appendChild(padEl);
|
|
973
|
+
}
|
|
974
|
+
if (this.options.renderCloseBtn) {
|
|
975
|
+
const closeBtnEl = yield this.options.renderCloseBtn();
|
|
976
|
+
closeBtnEl.classList.add("uu-dialog-close");
|
|
977
|
+
this.options.small && closeBtnEl.classList.add("small");
|
|
978
|
+
closeBtnEl.tabIndex = 0;
|
|
979
|
+
if (closeBtnEl.hasAttribute("alt"))
|
|
980
|
+
closeBtnEl.setAttribute("alt", this.getString("closeDialogTooltip"));
|
|
981
|
+
closeBtnEl.title = closeBtnEl.ariaLabel = this.getString("closeDialogTooltip");
|
|
982
|
+
this.onInteraction(closeBtnEl, () => this.close());
|
|
983
|
+
headerWrapperEl.appendChild(closeBtnEl);
|
|
984
|
+
}
|
|
985
|
+
dialogWrapperEl.appendChild(headerWrapperEl);
|
|
986
|
+
const dialogBodyElem = document.createElement("div");
|
|
987
|
+
dialogBodyElem.id = `uu-${this.id}-dialog-body`;
|
|
988
|
+
dialogBodyElem.classList.add("uu-dialog-body");
|
|
989
|
+
this.options.small && dialogBodyElem.classList.add("small");
|
|
990
|
+
const body = this.options.renderBody();
|
|
991
|
+
dialogBodyElem.appendChild(body instanceof Promise ? yield body : body);
|
|
992
|
+
dialogWrapperEl.appendChild(dialogBodyElem);
|
|
993
|
+
if (footer) {
|
|
994
|
+
const footerWrapper = document.createElement("div");
|
|
995
|
+
footerWrapper.classList.add("uu-dialog-footer-cont");
|
|
996
|
+
dialogWrapperEl.appendChild(footerWrapper);
|
|
997
|
+
footerWrapper.appendChild(footer instanceof Promise ? yield footer : footer);
|
|
998
|
+
}
|
|
999
|
+
return dialogWrapperEl;
|
|
1000
|
+
});
|
|
1001
|
+
}
|
|
1002
|
+
};
|
|
534
1003
|
|
|
535
1004
|
// lib/misc.ts
|
|
536
1005
|
function autoPlural(word, num) {
|
|
@@ -843,33 +1312,4 @@ tr.getLanguage = () => {
|
|
|
843
1312
|
return curLang;
|
|
844
1313
|
};
|
|
845
1314
|
|
|
846
|
-
|
|
847
|
-
exports.DataStoreSerializer = DataStoreSerializer;
|
|
848
|
-
exports.SelectorObserver = SelectorObserver;
|
|
849
|
-
exports.addGlobalStyle = addGlobalStyle;
|
|
850
|
-
exports.addParent = addParent;
|
|
851
|
-
exports.autoPlural = autoPlural;
|
|
852
|
-
exports.clamp = clamp;
|
|
853
|
-
exports.compress = compress;
|
|
854
|
-
exports.computeHash = computeHash;
|
|
855
|
-
exports.debounce = debounce;
|
|
856
|
-
exports.decompress = decompress;
|
|
857
|
-
exports.fetchAdvanced = fetchAdvanced;
|
|
858
|
-
exports.getSiblingsFrame = getSiblingsFrame;
|
|
859
|
-
exports.getUnsafeWindow = getUnsafeWindow;
|
|
860
|
-
exports.insertValues = insertValues;
|
|
861
|
-
exports.interceptEvent = interceptEvent;
|
|
862
|
-
exports.interceptWindowEvent = interceptWindowEvent;
|
|
863
|
-
exports.isScrollable = isScrollable;
|
|
864
|
-
exports.mapRange = mapRange;
|
|
865
|
-
exports.observeElementProp = observeElementProp;
|
|
866
|
-
exports.openInNewTab = openInNewTab;
|
|
867
|
-
exports.pauseFor = pauseFor;
|
|
868
|
-
exports.preloadImages = preloadImages;
|
|
869
|
-
exports.randRange = randRange;
|
|
870
|
-
exports.randomId = randomId;
|
|
871
|
-
exports.randomItem = randomItem;
|
|
872
|
-
exports.randomItemIndex = randomItemIndex;
|
|
873
|
-
exports.randomizeArray = randomizeArray;
|
|
874
|
-
exports.takeRandomItem = takeRandomItem;
|
|
875
|
-
exports.tr = tr;
|
|
1315
|
+
export { DataStore, DataStoreSerializer, Dialog, NanoEmitter, SelectorObserver, addGlobalStyle, addParent, autoPlural, clamp, compress, computeHash, currentDialogId, debounce, decompress, defaultDialogCss, defaultStrings, fetchAdvanced, getSiblingsFrame, getUnsafeWindow, insertValues, interceptEvent, interceptWindowEvent, isScrollable, mapRange, observeElementProp, openDialogs, openInNewTab, pauseFor, preloadImages, randRange, randomId, randomItem, randomItemIndex, randomizeArray, takeRandomItem, tr };
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { NanoEmitter } from "./NanoEmitter.js";
|
|
2
|
+
export declare const defaultDialogCss = ".uu-no-select {\n user-select: none;\n}\n\n.uu-dialog-bg {\n --uu-dialog-bg: #333333;\n --uu-dialog-bg-highlight: #252525;\n --uu-scroll-indicator-bg: rgba(10, 10, 10, 0.7);\n --uu-dialog-separator-color: #797979;\n --uu-dialog-border-radius: 10px;\n}\n\n.uu-dialog-bg {\n display: block;\n position: fixed;\n width: 100%;\n height: 100%;\n top: 0;\n left: 0;\n z-index: 5;\n background-color: rgba(0, 0, 0, 0.6);\n}\n\n.uu-dialog {\n --uu-calc-dialog-height: calc(min(100vh - 40px, var(--uu-dialog-height-max)));\n position: absolute;\n display: flex;\n flex-direction: column;\n width: calc(min(100% - 60px, var(--uu-dialog-width-max)));\n border-radius: var(--uu-dialog-border-radius);\n height: auto;\n max-height: var(--uu-calc-dialog-height);\n left: 50%;\n top: 50%;\n transform: translate(-50%, -50%);\n z-index: 6;\n color: #fff;\n background-color: var(--uu-dialog-bg);\n}\n\n.uu-dialog.align-top {\n top: 0;\n transform: translate(-50%, 40px);\n}\n\n.uu-dialog.align-bottom {\n top: 100%;\n transform: translate(-50%, -100%);\n}\n\n.uu-dialog-body {\n font-size: 1.5rem;\n padding: 20px;\n}\n\n.uu-dialog-body.small {\n padding: 15px;\n}\n\n#uu-dialog-opts {\n display: flex;\n flex-direction: column;\n position: relative;\n padding: 30px 0px;\n overflow-y: auto;\n}\n\n.uu-dialog-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n margin-bottom: 6px;\n padding: 15px 20px 15px 20px;\n background-color: var(--uu-dialog-bg);\n border: 2px solid var(--uu-dialog-separator-color);\n border-style: none none solid none !important;\n border-radius: var(--uu-dialog-border-radius) var(--uu-dialog-border-radius) 0px 0px;\n}\n\n.uu-dialog-header.small {\n padding: 10px 15px;\n border-style: none none solid none !important;\n}\n\n.uu-dialog-header-pad {\n content: \" \";\n min-height: 32px;\n}\n\n.uu-dialog-header-pad.small {\n min-height: 24px;\n}\n\n.uu-dialog-titlecont {\n display: flex;\n align-items: center;\n}\n\n.uu-dialog-titlecont-no-title {\n display: flex;\n justify-content: flex-end;\n align-items: center;\n}\n\n.uu-dialog-title {\n position: relative;\n display: inline-block;\n font-size: 22px;\n}\n\n.uu-dialog-close {\n cursor: pointer;\n}\n\n.uu-dialog-header-img,\n.uu-dialog-close\n{\n width: 32px;\n height: 32px;\n}\n\n.uu-dialog-header-img.small,\n.uu-dialog-close.small\n{\n width: 24px;\n height: 24px;\n}\n\n.uu-dialog-footer {\n font-size: 17px;\n text-decoration: underline;\n}\n\n.uu-dialog-footer.hidden {\n display: none;\n}\n\n.uu-dialog-footer-cont {\n margin-top: 6px;\n padding: 15px 20px;\n background: var(--uu-dialog-bg);\n background: linear-gradient(to bottom, rgba(0, 0, 0, 0) 0%, var(--uu-dialog-bg) 30%, var(--uu-dialog-bg) 100%);\n border: 2px solid var(--uu-dialog-separator-color);\n border-style: solid none none none !important;\n border-radius: 0px 0px var(--uu-dialog-border-radius) var(--uu-dialog-border-radius);\n}\n\n.uu-dialog-footer-buttons-cont button:not(:last-of-type) {\n margin-right: 15px;\n}";
|
|
3
|
+
/** ID of the last opened (top-most) dialog */
|
|
4
|
+
export declare let currentDialogId: string | null;
|
|
5
|
+
/** IDs of all currently open dialogs, top-most first */
|
|
6
|
+
export declare const openDialogs: string[];
|
|
7
|
+
export declare const defaultStrings: {
|
|
8
|
+
closeDialogTooltip: string;
|
|
9
|
+
};
|
|
10
|
+
/** Options passed to the Dialog constructor */
|
|
11
|
+
export interface DialogOptions {
|
|
12
|
+
/** ID that gets added to child element IDs - has to be unique and conform to HTML ID naming rules! */
|
|
13
|
+
id: string;
|
|
14
|
+
/** Target and max width of the dialog in pixels */
|
|
15
|
+
width: number;
|
|
16
|
+
/** Target and max height of the dialog in pixels */
|
|
17
|
+
height: number;
|
|
18
|
+
/** Whether the dialog should close when the background is clicked - defaults to true */
|
|
19
|
+
closeOnBgClick?: boolean;
|
|
20
|
+
/** Whether the dialog should close when the escape key is pressed - defaults to true */
|
|
21
|
+
closeOnEscPress?: boolean;
|
|
22
|
+
/** Whether the dialog should be destroyed when it's closed - defaults to false */
|
|
23
|
+
destroyOnClose?: boolean;
|
|
24
|
+
/** Whether the dialog should be unmounted when it's closed - defaults to true - superseded by destroyOnClose */
|
|
25
|
+
unmountOnClose?: boolean;
|
|
26
|
+
/** Whether all listeners should be removed when the dialog is destroyed - defaults to true */
|
|
27
|
+
removeListenersOnDestroy?: boolean;
|
|
28
|
+
/** Whether the dialog should have a smaller overall appearance - defaults to false */
|
|
29
|
+
small?: boolean;
|
|
30
|
+
/** Where to align or anchor the dialog vertically - defaults to "center" */
|
|
31
|
+
verticalAlign?: "top" | "center" | "bottom";
|
|
32
|
+
/** Strings used in the dialog (used for translations) - defaults to the default English strings */
|
|
33
|
+
strings?: Partial<typeof defaultStrings>;
|
|
34
|
+
/** CSS to apply to the dialog - defaults to the {@linkcode defaultDialogCss} */
|
|
35
|
+
dialogCss?: string;
|
|
36
|
+
/** Called to render the body of the dialog */
|
|
37
|
+
renderBody: () => HTMLElement | Promise<HTMLElement>;
|
|
38
|
+
/** Called to render the header of the dialog - leave undefined for a blank header */
|
|
39
|
+
renderHeader?: () => HTMLElement | Promise<HTMLElement>;
|
|
40
|
+
/** Called to render the footer of the dialog - leave undefined for no footer */
|
|
41
|
+
renderFooter?: () => HTMLElement | Promise<HTMLElement>;
|
|
42
|
+
/** Called to render the close button of the dialog - leave undefined for no close button */
|
|
43
|
+
renderCloseBtn?: () => HTMLElement | Promise<HTMLElement>;
|
|
44
|
+
}
|
|
45
|
+
/** Creates and manages a modal dialog element */
|
|
46
|
+
export declare class Dialog extends NanoEmitter<{
|
|
47
|
+
/** Emitted just **after** the dialog is closed */
|
|
48
|
+
close: () => void;
|
|
49
|
+
/** Emitted just **after** the dialog is opened */
|
|
50
|
+
open: () => void;
|
|
51
|
+
/** Emitted just **after** the dialog contents are rendered */
|
|
52
|
+
render: () => void;
|
|
53
|
+
/** Emitted just **after** the dialog contents are cleared */
|
|
54
|
+
clear: () => void;
|
|
55
|
+
/** Emitted just **after** the dialog is destroyed and **before** all listeners are removed */
|
|
56
|
+
destroy: () => void;
|
|
57
|
+
}> {
|
|
58
|
+
/** Options passed to the dialog in the constructor */
|
|
59
|
+
readonly options: {
|
|
60
|
+
/** ID that gets added to child element IDs - has to be unique and conform to HTML ID naming rules! */
|
|
61
|
+
id: string;
|
|
62
|
+
/** Target and max width of the dialog in pixels */
|
|
63
|
+
width: number;
|
|
64
|
+
/** Target and max height of the dialog in pixels */
|
|
65
|
+
height: number;
|
|
66
|
+
closeOnBgClick: boolean;
|
|
67
|
+
closeOnEscPress: boolean;
|
|
68
|
+
destroyOnClose: boolean;
|
|
69
|
+
unmountOnClose: boolean;
|
|
70
|
+
removeListenersOnDestroy: boolean;
|
|
71
|
+
/** Whether the dialog should have a smaller overall appearance - defaults to false */
|
|
72
|
+
small?: boolean | undefined;
|
|
73
|
+
verticalAlign: string;
|
|
74
|
+
dialogCss: string;
|
|
75
|
+
/** Called to render the body of the dialog */
|
|
76
|
+
renderBody: () => HTMLElement | Promise<HTMLElement>;
|
|
77
|
+
/** Called to render the header of the dialog - leave undefined for a blank header */
|
|
78
|
+
renderHeader?: (() => HTMLElement | Promise<HTMLElement>) | undefined;
|
|
79
|
+
/** Called to render the footer of the dialog - leave undefined for no footer */
|
|
80
|
+
renderFooter?: (() => HTMLElement | Promise<HTMLElement>) | undefined;
|
|
81
|
+
/** Called to render the close button of the dialog - leave undefined for no close button */
|
|
82
|
+
renderCloseBtn?: (() => HTMLElement | Promise<HTMLElement>) | undefined;
|
|
83
|
+
smallHeader: boolean;
|
|
84
|
+
};
|
|
85
|
+
/** ID that gets added to child element IDs - has to be unique and conform to HTML ID naming rules! */
|
|
86
|
+
readonly id: string;
|
|
87
|
+
/** Strings used in the dialog (used for translations) */
|
|
88
|
+
strings: {
|
|
89
|
+
closeDialogTooltip: string;
|
|
90
|
+
};
|
|
91
|
+
protected dialogOpen: boolean;
|
|
92
|
+
protected dialogMounted: boolean;
|
|
93
|
+
constructor(options: DialogOptions);
|
|
94
|
+
/** Call after DOMContentLoaded to pre-render the dialog and invisibly mount it in the DOM */
|
|
95
|
+
mount(): Promise<HTMLDivElement | undefined>;
|
|
96
|
+
/** Closes the dialog and clears all its contents (unmounts elements from the DOM) in preparation for a new rendering call */
|
|
97
|
+
unmount(): void;
|
|
98
|
+
/** Clears the DOM of the dialog and then renders it again */
|
|
99
|
+
remount(): Promise<void>;
|
|
100
|
+
/**
|
|
101
|
+
* Opens the dialog - also mounts it if it hasn't been mounted yet
|
|
102
|
+
* Prevents default action and immediate propagation of the passed event
|
|
103
|
+
*/
|
|
104
|
+
open(e?: MouseEvent | KeyboardEvent): Promise<void | HTMLElement>;
|
|
105
|
+
/** Closes the dialog - prevents default action and immediate propagation of the passed event */
|
|
106
|
+
close(e?: MouseEvent | KeyboardEvent): void;
|
|
107
|
+
/** Returns true if the dialog is currently open */
|
|
108
|
+
isOpen(): boolean;
|
|
109
|
+
/** Returns true if the dialog is currently mounted */
|
|
110
|
+
isMounted(): boolean;
|
|
111
|
+
/** Clears the DOM of the dialog and removes all event listeners */
|
|
112
|
+
destroy(): void;
|
|
113
|
+
/** Returns the ID of the top-most dialog (the dialog that has been opened last) */
|
|
114
|
+
static getCurrentDialogId(): string | null;
|
|
115
|
+
/** Returns the IDs of all currently open dialogs, top-most first */
|
|
116
|
+
static getOpenDialogs(): string[];
|
|
117
|
+
protected getString(key: keyof typeof defaultStrings): string;
|
|
118
|
+
/** Called once to attach all generic event listeners */
|
|
119
|
+
protected attachListeners(bgElem: HTMLElement): void;
|
|
120
|
+
/**
|
|
121
|
+
* Adds generic, accessible interaction listeners to the passed element.
|
|
122
|
+
* All listeners have the default behavior prevented and stop propagation (for keyboard events only as long as the captured key is valid).
|
|
123
|
+
* @param listenerOptions Provide a {@linkcode listenerOptions} object to configure the listeners
|
|
124
|
+
*/
|
|
125
|
+
protected onInteraction<TElem extends HTMLElement>(elem: TElem, listener: (evt: MouseEvent | KeyboardEvent) => void, listenerOptions?: AddEventListenerOptions & {
|
|
126
|
+
preventDefault?: boolean;
|
|
127
|
+
stopPropagation?: boolean;
|
|
128
|
+
}): void;
|
|
129
|
+
/** Returns the dialog content element and all its children */
|
|
130
|
+
protected getDialogContent(): Promise<HTMLDivElement>;
|
|
131
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type DefaultEvents, type EventsMap, type Unsubscribe } from "nanoevents";
|
|
2
|
+
export interface NanoEmitterOptions {
|
|
3
|
+
/** If set to true, allows emitting events through the public method emit() */
|
|
4
|
+
publicEmit: boolean;
|
|
5
|
+
}
|
|
6
|
+
/** Class that can be extended or instantiated by itself to create an event emitter with helper methods and a strongly typed event map */
|
|
7
|
+
export declare class NanoEmitter<TEvtMap extends EventsMap = DefaultEvents> {
|
|
8
|
+
protected readonly events: import("nanoevents").Emitter<TEvtMap>;
|
|
9
|
+
protected eventUnsubscribes: Unsubscribe[];
|
|
10
|
+
protected emitterOptions: NanoEmitterOptions;
|
|
11
|
+
constructor(options?: Partial<NanoEmitterOptions>);
|
|
12
|
+
/** Subscribes to an event - returns a function that unsubscribes the event listener */
|
|
13
|
+
on<TKey extends keyof TEvtMap>(event: TKey | "_", cb: TEvtMap[TKey]): () => void;
|
|
14
|
+
/** Subscribes to an event and calls the callback or resolves the Promise only once */
|
|
15
|
+
once<TKey extends keyof TEvtMap>(event: TKey | "_", cb?: TEvtMap[TKey]): Promise<Parameters<TEvtMap[TKey]>>;
|
|
16
|
+
/** Emits an event on this instance - Needs `publicEmit` to be set to true in the constructor! */
|
|
17
|
+
emit<TKey extends keyof TEvtMap>(event: TKey, ...args: Parameters<TEvtMap[TKey]>): boolean;
|
|
18
|
+
/** Unsubscribes all event listeners */
|
|
19
|
+
unsubscribeAll(): void;
|
|
20
|
+
}
|
package/dist/lib/index.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
export * from "./array.js";
|
|
2
2
|
export * from "./DataStore.js";
|
|
3
3
|
export * from "./DataStoreSerializer.js";
|
|
4
|
+
export * from "./Dialog.js";
|
|
4
5
|
export * from "./dom.js";
|
|
5
6
|
export * from "./math.js";
|
|
6
7
|
export * from "./misc.js";
|
|
8
|
+
export * from "./NanoEmitter.js";
|
|
7
9
|
export * from "./SelectorObserver.js";
|
|
8
10
|
export * from "./translation.js";
|
|
9
11
|
export * from "./types.js";
|