@optionfactory/ful 0.48.0 → 0.50.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/dist/ful.mjs CHANGED
@@ -562,7 +562,7 @@ const timing = {
562
562
  class SyncEvent extends CustomEvent {
563
563
  #results;
564
564
  constructor(type, options) {
565
- super(type, options);
565
+ super(type, {...options, cancelable: true});
566
566
  this.#results = [];
567
567
  }
568
568
 
@@ -571,9 +571,11 @@ class SyncEvent extends CustomEvent {
571
571
  // event handlers asynchronously via the event loop, dispatchEvent()
572
572
  // invokes event handlers synchronously.
573
573
  // see: https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/dispatchEvent
574
- const success = el.dispatchEvent(this);
574
+ el.dispatchEvent(this);
575
+ //we ignore the result of dispatchEvent and use defaultPrevented instead
576
+ //because handlers can be async
575
577
  const results = await Promise.all(this.#results);
576
- return [success, results];
578
+ return [!this.defaultPrevented, results];
577
579
  }
578
580
 
579
581
  static on(el, type, h, useCapture) {
@@ -585,6 +587,11 @@ class SyncEvent extends CustomEvent {
585
587
  }
586
588
 
587
589
  class Fragments {
590
+ /**
591
+ *
592
+ * @param {...string} html
593
+ * @returns
594
+ */
588
595
  static fromHtml(...html) {
589
596
  const el = document.createElement("div");
590
597
  el.innerHTML = html.join("");
@@ -592,16 +599,31 @@ class Fragments {
592
599
  fragment.append(...el.childNodes);
593
600
  return fragment;
594
601
  }
602
+ /**
603
+ *
604
+ * @param {DocumentFragment} fragment
605
+ * @returns
606
+ */
595
607
  static toHtml(fragment) {
596
608
  var r = document.createElement("div");
597
609
  r.appendChild(fragment);
598
610
  return r.innerHTML;
599
611
  }
612
+ /**
613
+ *
614
+ * @param {...Node} nodes
615
+ * @returns
616
+ */
600
617
  static from(...nodes) {
601
618
  const fragment = new DocumentFragment();
602
619
  fragment.append(...nodes);
603
620
  return fragment;
604
621
  }
622
+ /**
623
+ *
624
+ * @param {HTMLElement} el
625
+ * @returns
626
+ */
605
627
  static fromChildNodes(el) {
606
628
  const fragment = new DocumentFragment();
607
629
  fragment.append(...el.childNodes);
@@ -611,18 +633,41 @@ class Fragments {
611
633
 
612
634
  class Attributes {
613
635
  static id = 0;
636
+ /**
637
+ *
638
+ * @param {string} prefix
639
+ * @returns
640
+ */
614
641
  static uid(prefix) {
615
642
  return `${prefix}-${++Attributes.id}`;
616
643
  }
644
+ /**
645
+ *
646
+ * @param {any} value
647
+ * @returns
648
+ */
617
649
  static asBoolean(value) {
618
650
  return value !== null && value !== undefined && value !== false;
619
651
  }
652
+ /**
653
+ *
654
+ * @param {HTMLElement} el
655
+ * @param {string} k
656
+ * @param {string} v
657
+ * @returns
658
+ */
620
659
  static defaultValue(el, k, v) {
621
660
  if (!el.hasAttribute(k)) {
622
661
  el.setAttribute(k, v);
623
662
  }
624
663
  return el.getAttribute(k);
625
664
  }
665
+ /**
666
+ *
667
+ * @param {string} prefix
668
+ * @param {HTMLElement} from
669
+ * @param {HTMLElement} to
670
+ */
626
671
  static forward(prefix, from, to) {
627
672
  from.getAttributeNames()
628
673
  .filter(a => a.startsWith(prefix))
@@ -637,7 +682,12 @@ class Attributes {
637
682
  }
638
683
  }
639
684
 
640
- class Slots {
685
+ class LightSlots {
686
+ /**
687
+ *
688
+ * @param {HTMLElement} el
689
+ * @returns the slots
690
+ */
641
691
  static from(el) {
642
692
  const namedSlots = Array.from(el.childNodes)
643
693
  .filter(el => el.matches && el.matches('[slot]'))
@@ -647,10 +697,10 @@ class Slots {
647
697
  el.removeAttribute("slot");
648
698
  return [slot, el];
649
699
  });
650
- const slotted = Object.fromEntries(namedSlots);
651
- slotted.default = new DocumentFragment();
652
- slotted.default.append(...el.childNodes);
653
- return slotted;
700
+ const slots = Object.fromEntries(namedSlots);
701
+ slots.default = new DocumentFragment();
702
+ slots.default.append(...el.childNodes);
703
+ return slots;
654
704
  }
655
705
  }
656
706
 
@@ -665,20 +715,20 @@ class Nodes {
665
715
  }
666
716
  }
667
717
 
668
- class TemplateRegistry {
718
+ class TemplatesRegistry {
669
719
  #idToFragment = {};
670
720
  #idToTemplate = {};
671
721
  #ec;
672
722
  put(k, fragment) {
673
723
  if (this.#ec) {
674
- this.#idToTemplate[k] = ftl.Template.fromFragment(fragment, ec);
724
+ this.#idToTemplate[k] = Template.fromFragment(fragment, ec);
675
725
  return;
676
726
  }
677
727
  this.#idToFragment[k] = fragment;
678
728
  }
679
729
  get(k) {
680
730
  if (!this.#ec) {
681
- throw new Error("evaluationContext is not configured");
731
+ throw new Error("TemplatesRegistry is not configured");
682
732
  }
683
733
  const tpl = this.#idToTemplate[k];
684
734
  if (!tpl) {
@@ -695,7 +745,50 @@ class TemplateRegistry {
695
745
  }
696
746
  }
697
747
 
698
- const templates = new TemplateRegistry();
748
+
749
+ class ElementsRegistry {
750
+ #templates;
751
+ #tagToclass;
752
+ #configured;
753
+ #id = 0;
754
+ constructor(){
755
+ this.#templates = new TemplatesRegistry();
756
+ this.#tagToclass = {};
757
+ }
758
+ defineTemplate(html){
759
+ if(html === null || html === undefined){
760
+ return undefined;
761
+ }
762
+ const name = `unnamed-${++this.#id}`;
763
+ this.#templates.put(name, Fragments.fromHtml(html));
764
+ return name;
765
+ }
766
+ template(k){
767
+ if(k === null || k === undefined){
768
+ return undefined;
769
+ }
770
+ return this.#templates.get(k);
771
+ }
772
+ define(tag, klass){
773
+ if(!this.#configured){
774
+ this.#tagToclass[tag] = klass;
775
+ return this;
776
+ }
777
+ customElements.define(tag, klass);
778
+ return this;
779
+ }
780
+ configure(ec) {
781
+ this.#templates.configure(ec);
782
+ for(const [tag, klass] of Object.entries(this.#tagToclass)) {
783
+ customElements.define(tag, klass);
784
+ delete this.#tagToclass[tag];
785
+ }
786
+ this.#configured = true;
787
+ }
788
+ }
789
+
790
+ const elements = new ElementsRegistry();
791
+
699
792
 
700
793
  class UpgradeQueue {
701
794
  #q = [];
@@ -715,22 +808,29 @@ class UpgradeQueue {
715
808
 
716
809
  const upgradeQueue = new UpgradeQueue();
717
810
 
718
- const ParsedElement = (flags, others) => {
811
+ const ParsedElement = (conf) => {
812
+ const {flags, attrs, template, slots} = conf || {};
719
813
 
720
814
  const observed_flags = flags || [];
721
- const observed_others = others || [];
815
+ const observed_others = attrs || [];
722
816
  const observed = [].concat(observed_flags).concat(observed_others);
723
817
 
818
+ const templateId = elements.defineTemplate(template);
819
+
724
820
  const k = class extends HTMLElement {
725
821
  static get observedAttributes() {
726
822
  return observed;
727
823
  }
728
824
  #parsed;
825
+ #initialized;
729
826
  #internals;
730
827
  constructor(...args) {
731
828
  super(...args);
732
829
  this.#internals = this.attachInternals();
733
830
  }
831
+ get initialized(){
832
+ return this.#initialized;
833
+ }
734
834
  get internals() {
735
835
  return this.#internals;
736
836
  }
@@ -766,34 +866,50 @@ const ParsedElement = (flags, others) => {
766
866
  return;
767
867
  }
768
868
  this.#parsed = true;
769
- await this.render();
869
+ await this.render(elements.template(templateId), slots ? LightSlots.from(this) : undefined);
770
870
  for (const flag of observed_flags) {
771
- if(this.hasAttribute(flag)){
871
+ if (this.hasAttribute(flag)) {
772
872
  this[flag] = true;
773
873
  }
774
874
  }
775
875
  for (const other of observed_others) {
776
- if(this.hasAttribute(other)){
876
+ if (this.hasAttribute(other)) {
777
877
  this[other] = this.getAttribute(other);
778
878
  }
779
879
  }
780
- }
880
+ this.#initialized = true;
881
+ }
781
882
  };
782
883
 
783
884
  for (const flag of observed_flags) {
885
+ const state = `--${flag};`;
784
886
  Object.defineProperty(k.prototype, flag, {
785
887
  enumerable: true,
786
888
  configurable: true,
787
889
  get() {
788
- return this.internals.states.has(`--${flag}`);
890
+ return this.internals.states.has(state);
789
891
  },
790
892
  set(value) {
791
- //see https://developer.mozilla.org/en-US/docs/Web/API/CustomStateSet#using_double_dash_prefixed_idents
792
- if (Attributes.asBoolean(value)) {
793
- this.internals.states.add(`--${flag}`);
794
- return;
795
- }
796
- this.internals.states.delete(`--${flag}`);
893
+ const v = Attributes.asBoolean(value);
894
+ const et = this.initialized ? 'changed' : 'init';
895
+ const event = new SyncEvent(`${flag}:${et}`, {
896
+ detail: {
897
+ target: this,
898
+ value: v
899
+ }
900
+ });
901
+ (async () => {
902
+ const [success, results] = await event.dispatchTo(this);
903
+ if (!success) {
904
+ return;
905
+ }
906
+ if (v) {
907
+ //see https://developer.mozilla.org/en-US/docs/Web/API/CustomStateSet#using_double_dash_prefixed_idents
908
+ this.internals.states.add(state);
909
+ return;
910
+ }
911
+ this.internals.states.delete(state);
912
+ })();
797
913
  }
798
914
  });
799
915
  }
@@ -950,21 +1066,8 @@ class Form extends ParsedElement() {
950
1066
  }
951
1067
  }
952
1068
 
953
- templates.put('ful-input', Fragments.fromHtml(`
954
- <label data-tpl-for="id" class="form-label">{{{{ slotted.default }}}}</label>
955
- <div class="input-group">
956
- <span data-tpl-if="slotted.ibefore" class="input-group-text">{{{{ slotted.ibefore }}}}</span>
957
- <div data-tpl-if="slotted.before" data-tpl-remove="tag">{{{{ slotted.before }}}}</div>
958
- {{{{ slotted.input }}}}
959
- <div data-tpl-if="slotted.after" data-tpl-remove="tag">{{{{ slotted.after }}}}</div>
960
- <span data-tpl-if="slotted.iafter" class="input-group-text">{{{{ slotted.iafter }}}}</span>
961
- </div>
962
- <ful-field-error data-tpl-if="name" data-tpl-field="name"></ful-field-error>
963
- `));
964
-
965
-
966
- const makeInputFragment = (el, slotted) => {
967
- const input = el.input = slotted.input = slotted.input || (() => {
1069
+ const makeInputFragment = (el, template, slots) => {
1070
+ const input = el.input = slots.input = slots.input || (() => {
968
1071
  const el = document.createElement("input");
969
1072
  el.classList.add("form-control");
970
1073
  return el;
@@ -972,18 +1075,32 @@ const makeInputFragment = (el, slotted) => {
972
1075
  input.setAttribute('ful-validation-target', '');
973
1076
 
974
1077
  const id = input.getAttribute('id') || el.getAttribute('input-id') || Attributes.uid('ful-input');
975
- Attributes.forward('input-', el, slotted.input);
976
- Attributes.defaultValue(slotted.input, "id", id);
977
- Attributes.defaultValue(slotted.input, "type", "text");
978
- Attributes.defaultValue(slotted.input, "placeholder", " ");
1078
+ Attributes.forward('input-', el, slots.input);
1079
+ Attributes.defaultValue(slots.input, "id", id);
1080
+ Attributes.defaultValue(slots.input, "type", "text");
1081
+ Attributes.defaultValue(slots.input, "placeholder", " ");
979
1082
  const name = el.getAttribute('name');
980
- return templates.get('ful-input').render(el, { id, name, slotted });
1083
+ return template.render(el, { id, name, slots });
981
1084
  };
982
1085
 
983
- class Input extends ParsedElement([], ['value']) {
984
- render() {
985
- const slotted = Slots.from(el);
986
- const fragment = makeInputFragment(this, slotted);
1086
+ class Input extends ParsedElement({
1087
+ flags: [],
1088
+ attrs: ['value'],
1089
+ slots: true,
1090
+ template: `
1091
+ <label data-tpl-for="id" class="form-label">{{{{ slots.default }}}}</label>
1092
+ <div class="input-group">
1093
+ <span data-tpl-if="slots.ibefore" class="input-group-text">{{{{ slots.ibefore }}}}</span>
1094
+ <div data-tpl-if="slots.before" data-tpl-remove="tag">{{{{ slots.before }}}}</div>
1095
+ {{{{ slots.input }}}}
1096
+ <div data-tpl-if="slots.after" data-tpl-remove="tag">{{{{ slots.after }}}}</div>
1097
+ <span data-tpl-if="slots.iafter" class="input-group-text">{{{{ slots.iafter }}}}</span>
1098
+ </div>
1099
+ <ful-field-error data-tpl-if="name" data-tpl-field="name"></ful-field-error>
1100
+ `
1101
+ }){
1102
+ render(template, slots) {
1103
+ const fragment = makeInputFragment(this, template, slots);
987
1104
  this.replaceChildren(fragment);
988
1105
  }
989
1106
  get value() {
@@ -999,33 +1116,33 @@ class Input extends ParsedElement([], ['value']) {
999
1116
  * <link href="tom-select.bootstrap5.css" rel="stylesheet" />
1000
1117
  */
1001
1118
 
1002
- templates.put('ful-select', Fragments.fromHtml(`
1003
- <label data-tpl-for="tsId" class="form-label">{{{{ slotted.default }}}}</label>
1004
- {{{{ input }}}}
1005
- <div class="input-group">
1006
- <span data-tpl-if="slotted.ibefore" class="input-group-text">{{{{ slotted.ibefore }}}}</span>
1007
- <div data-tpl-if="slotted.before" data-tpl-remove="tag">{{{{ slotted.before }}}}</div>
1008
- {{{{ slotted.input }}}}
1009
- <div data-tpl-if="slotted.after" data-tpl-remove="tag">{{{{ slotted.after }}}}</div>
1010
- <span data-tpl-if="slotted.iafter" class="input-group-text">{{{{ slotted.iafter }}}}</span>
1011
- </div>
1012
- <ful-field-error data-tpl-if="name" data-tpl-field="name"></ful-field-error>
1013
- `));
1014
-
1015
-
1016
- class Select extends ParsedElement([], ["value"]) {
1119
+ class Select extends ParsedElement({
1120
+ flags: [],
1121
+ attrs: ["value"],
1122
+ slots: true,
1123
+ template: `
1124
+ <label data-tpl-for="tsId" class="form-label">{{{{ slots.default }}}}</label>
1125
+ {{{{ input }}}}
1126
+ <div class="input-group">
1127
+ <span data-tpl-if="slots.ibefore" class="input-group-text">{{{{ slots.ibefore }}}}</span>
1128
+ <div data-tpl-if="slots.before" data-tpl-remove="tag">{{{{ slots.before }}}}</div>
1129
+ {{{{ slots.input }}}}
1130
+ <div data-tpl-if="slots.after" data-tpl-remove="tag">{{{{ slots.after }}}}</div>
1131
+ <span data-tpl-if="slots.iafter" class="input-group-text">{{{{ slots.iafter }}}}</span>
1132
+ </div>
1133
+ <ful-field-error data-tpl-if="name" data-tpl-field="name"></ful-field-error>
1134
+ `
1135
+ }) {
1017
1136
  constructor(tsConfig) {
1018
1137
  super();
1019
1138
  this.tsConfig = tsConfig;
1020
1139
  }
1021
- render() {
1022
- const slotted = Slots.from(this);
1023
-
1140
+ render(template, slots) {
1024
1141
  const type = this.getAttribute("type") || 'local';
1025
1142
  const remote = type != 'local';
1026
1143
  const loadOnce = this.getAttribute('load') != 'always';
1027
1144
  const name = this.getAttribute('name');
1028
- const input = slotted.input = slotted.input || (() => {
1145
+ const input = slots.input = slots.input || (() => {
1029
1146
  return document.createElement("select");
1030
1147
  })();
1031
1148
  input.setAttribute('ful-validation-target', '');
@@ -1038,7 +1155,7 @@ class Select extends ParsedElement([], ["value"]) {
1038
1155
 
1039
1156
  //tomselect needs the input to have a parent.
1040
1157
  //se we move the input to a fragment
1041
- slotted.input = Fragments.from(input);
1158
+ slots.input = Fragments.from(input);
1042
1159
 
1043
1160
  this.loaded = !remote;
1044
1161
 
@@ -1061,7 +1178,7 @@ class Select extends ParsedElement([], ["value"]) {
1061
1178
  }
1062
1179
  const type = query && query.hasOwnProperty('byId') ? 'id' : 'query';
1063
1180
  const qvalue = type === 'id' ? query.byId : query;
1064
- const data = await (this.loader ? this.loader(qvalue, type) : []);
1181
+ const data = await (this.#loader ? this.#loader(qvalue, type) : []);
1065
1182
  if (type !== 'id') {
1066
1183
  this.loaded = true;
1067
1184
  }
@@ -1076,7 +1193,15 @@ class Select extends ParsedElement([], ["value"]) {
1076
1193
  } : {}, tsDefaultConfig, this.tsConfig));
1077
1194
  //we remove the input to move it
1078
1195
  input.remove();
1079
- templates.get('ful-select').renderTo(this, { id, tsId, name, input, slotted });
1196
+ template.renderTo(this, { id, tsId, name, input, slots });
1197
+ }
1198
+ #loader;
1199
+ set loader(l){
1200
+ this.#loader = l;
1201
+ // loader can be configured later so we load now
1202
+ if(this.hasAttribute('value')){
1203
+ this.value = this.getAttribute("value");
1204
+ }
1080
1205
  }
1081
1206
  set value(v) {
1082
1207
  (async () => {
@@ -1092,33 +1217,34 @@ class Select extends ParsedElement([], ["value"]) {
1092
1217
  }
1093
1218
  }
1094
1219
 
1095
- templates.put('ful-radio-group', Fragments.fromHtml(`
1096
- <fieldset>
1097
- <legend class="form-label">
1098
- {{{{ slotted.default }}}}
1099
- </legend>
1100
- <header data-tpl-if="slotted.header">
1101
- {{{{ slotted.header }}}}
1102
- </header>
1103
- <section>
1104
- <label data-tpl-each="inputsAndLabels" data-tpl-var="ial">
1105
- {{{{ ial[0] }}}}
1106
- {{{{ ial[1] }}}}
1107
- </label>
1108
- </section>
1109
- <ful-field-error data-tpl-if="name" data-tpl-field="name"></ful-field-error>
1110
- <footer data-tpl-if="slotted.footer">
1111
- {{{{ slotted.footer }}}}
1112
- </footer>
1113
- </fieldset>
1114
- `));
1115
-
1116
-
1117
- class RadioGroup extends ParsedElement(['disabled'], ['value']) {
1118
- render() {
1119
- const slotted = Slots.from(this);
1220
+ class RadioGroup extends ParsedElement({
1221
+ flags: ['disabled'],
1222
+ attrs: ['value'],
1223
+ slots: true,
1224
+ template: `
1225
+ <fieldset>
1226
+ <legend class="form-label">
1227
+ {{{{ slots.default }}}}
1228
+ </legend>
1229
+ <header data-tpl-if="slots.header">
1230
+ {{{{ slots.header }}}}
1231
+ </header>
1232
+ <section>
1233
+ <label data-tpl-each="inputsAndLabels" data-tpl-var="ial">
1234
+ {{{{ ial[0] }}}}
1235
+ {{{{ ial[1] }}}}
1236
+ </label>
1237
+ </section>
1238
+ <ful-field-error data-tpl-if="name" data-tpl-field="name"></ful-field-error>
1239
+ <footer data-tpl-if="slots.footer">
1240
+ {{{{ slots.footer }}}}
1241
+ </footer>
1242
+ </fieldset>
1243
+ `
1244
+ }) {
1245
+ render(template, slots) {
1120
1246
  const name = this.getAttribute('name') || Attributes.uid('ful-radiogroup');
1121
- const radioEls = Array.from(slotted.default.querySelectorAll('ful-radio'));
1247
+ const radioEls = Array.from(slots.default.querySelectorAll('ful-radio'));
1122
1248
  const inputsAndLabels = radioEls.map(el => {
1123
1249
  const input = document.createElement('input');
1124
1250
  input.setAttribute('type', 'radio');
@@ -1131,11 +1257,7 @@ class RadioGroup extends ParsedElement(['disabled'], ['value']) {
1131
1257
  return [input, label];
1132
1258
  });
1133
1259
  radioEls.forEach(el => el.remove());
1134
- templates.get('ful-radio-group').renderTo(this, {
1135
- name: name,
1136
- slotted: slotted,
1137
- inputsAndLabels: inputsAndLabels
1138
- });
1260
+ template.renderTo(this, {name, slots, inputsAndLabels});
1139
1261
  }
1140
1262
  get value() {
1141
1263
  const checked = this.querySelector('input[type=radio]:checked');
@@ -1146,17 +1268,17 @@ class RadioGroup extends ParsedElement(['disabled'], ['value']) {
1146
1268
  }
1147
1269
  }
1148
1270
 
1149
- templates.put('ful-spinner', Fragments.fromHtml(`
1150
- <div class="ful-spinner-wrapper">
1151
- <div class="ful-spinner-text">{{{{ slotted.default }}}}</div>
1152
- <div class="ful-spinner-icon"></div>
1153
- </div>
1154
- `));
1155
-
1156
- class Spinner extends ParsedElement() {
1157
- render() {
1158
- const slotted = Slots.from(this);
1159
- templates.get('ful-spinner').renderTo(this, { slotted });
1271
+ class Spinner extends ParsedElement({
1272
+ slots: true,
1273
+ template: `
1274
+ <div class="ful-spinner-wrapper">
1275
+ <div class="ful-spinner-text">{{{{ slots.default }}}}</div>
1276
+ <div class="ful-spinner-icon"></div>
1277
+ </div>
1278
+ `
1279
+ }) {
1280
+ render(template, slots) {
1281
+ template.renderTo(this, { slots });
1160
1282
  }
1161
1283
  }
1162
1284
 
@@ -1232,16 +1354,5 @@ class Wizard extends HTMLElement {
1232
1354
  }
1233
1355
  }
1234
1356
 
1235
- class CustomElements {
1236
- static configure(ec) {
1237
- templates.configure(ec);
1238
- customElements.define('ful-spinner', Spinner);
1239
- customElements.define('ful-input', Input);
1240
- customElements.define('ful-radio-group', RadioGroup);
1241
- customElements.define('ful-select', Select);
1242
- customElements.define('ful-form', Form);
1243
- }
1244
- }
1245
-
1246
- export { Attributes, AuthorizationCodeFlow, AuthorizationCodeFlowInterceptor, AuthorizationCodeFlowSession, Base64, CustomElements, Failure, Form, Fragments, Hex, HttpClient, Input, LocalStorage, Nodes, ParsedElement, RadioGroup, Select, SessionStorage, Slots, Spinner, SyncEvent, TemplateRegistry, VersionedStorage, Wizard, jsonPatch, jsonPost, jsonPut, jsonRequest, makeInputFragment, templates, timing };
1357
+ export { Attributes, AuthorizationCodeFlow, AuthorizationCodeFlowInterceptor, AuthorizationCodeFlowSession, Base64, ElementsRegistry, Failure, Form, Fragments, Hex, HttpClient, Input, LightSlots, LocalStorage, Nodes, ParsedElement, RadioGroup, Select, SessionStorage, Spinner, SyncEvent, TemplatesRegistry, VersionedStorage, Wizard, elements, jsonPatch, jsonPost, jsonPut, jsonRequest, makeInputFragment, timing };
1247
1358
  //# sourceMappingURL=ful.mjs.map