@optionfactory/ful 0.43.0 → 0.45.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
@@ -557,16 +557,41 @@ const timing = {
557
557
  }
558
558
  };
559
559
 
560
+ // SyncEvent.on($0, 'asd', async e => { await ful.timing.sleep(10_000); return 3; })
561
+ // const [success, results] = await new SyncEvent("asd").dispatchTo($0);
562
+ class SyncEvent extends CustomEvent {
563
+ #results;
564
+ constructor(type, options) {
565
+ super(type, options);
566
+ this.#results = [];
567
+ }
568
+
569
+ async dispatchTo(el) {
570
+ // unlike "native" events, which are fired by the browser and invoke
571
+ // event handlers asynchronously via the event loop, dispatchEvent()
572
+ // invokes event handlers synchronously.
573
+ // see: https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/dispatchEvent
574
+ const success = el.dispatchEvent(this);
575
+ const results = await Promise.all(this.#results);
576
+ return [success, results];
577
+ }
578
+
579
+ static on(el, type, h, useCapture) {
580
+ el.addEventListener(type, e => {
581
+ //e *must* be an async event
582
+ e.#results.push(h(e));
583
+ }, useCapture);
584
+ }
585
+ }
586
+
560
587
  class Fragments {
561
588
  static fromHtml(...html) {
562
589
  const el = document.createElement('div');
563
590
  el.innerHTML = html.join("");
564
591
  const fragment = new DocumentFragment();
565
- Array.from(el.childNodes).forEach(node => {
566
- fragment.appendChild(node);
567
- });
592
+ fragment.append(...el.childNodes);
568
593
  return fragment;
569
- }
594
+ }
570
595
  static toHtml(fragment) {
571
596
  var r = document.createElement("root");
572
597
  r.appendChild(fragment);
@@ -574,17 +599,12 @@ class Fragments {
574
599
  }
575
600
  static from(...nodes) {
576
601
  const fragment = new DocumentFragment();
577
- for (let i = 0; i !== nodes.length; ++i) {
578
- fragment.appendChild(nodes[i]);
579
- }
602
+ fragment.append(...nodes);
580
603
  return fragment;
581
604
  }
582
605
  static fromChildNodes(el) {
583
- const nodes = Array.from(el.childNodes);
584
606
  const fragment = new DocumentFragment();
585
- for (let i = 0; i !== nodes.length; ++i) {
586
- fragment.appendChild(nodes[i]);
587
- }
607
+ fragment.append(...el.childNodes);
588
608
  return fragment;
589
609
  }
590
610
  }
@@ -634,20 +654,83 @@ class Slots {
634
654
  }
635
655
  }
636
656
 
637
- const Templated = (SuperClass, template) => {
638
- return class extends SuperClass {
639
- #rendered;
640
- async connectedCallback() {
641
- if (this.#rendered) {
657
+ class Nodes {
658
+ static isParsed(el) {
659
+ for (var c = el; c; c = c.parentNode) {
660
+ if (c.nextSibling) {
661
+ return true;
662
+ }
663
+ }
664
+ return false;
665
+ }
666
+ }
667
+
668
+ class UpgradeQueue {
669
+ #q = [];
670
+ constructor() {
671
+ document.addEventListener('DOMContentLoaded', this.dequeue.bind(this));
672
+ }
673
+ enqueue(el) {
674
+ if (!this.#q.length) {
675
+ requestAnimationFrame(this.dequeue.bind(this));
676
+ }
677
+ this.#q.push(el);
678
+ }
679
+ dequeue() {
680
+ this.#q.splice(0).forEach(el => el.upgrade());
681
+ }
682
+ }
683
+
684
+ const upgradeQueue = new UpgradeQueue();
685
+
686
+ class ParsedElement extends HTMLElement {
687
+ #parsed;
688
+ connectedCallback() {
689
+ if (this.#parsed) {
690
+ return;
691
+ }
692
+ if (this.ownerDocument.readyState === 'complete' || Nodes.isParsed(this)) {
693
+ upgradeQueue.enqueue(this);
694
+ return;
695
+ }
696
+ this.ownerDocument.addEventListener('DOMContentLoaded', () => {
697
+ observer.disconnect();
698
+ upgradeQueue.enqueue(this);
699
+ });
700
+ const observer = new MutationObserver(() => {
701
+ if (!Nodes.isParsed(this)) {
642
702
  return;
643
703
  }
704
+ observer.disconnect();
705
+ upgradeQueue.enqueue(this);
706
+ });
707
+ observer.observe(this.parentNode, { childList: true, subtree: true });
708
+ }
709
+ attributeChangedCallback(name, oldValue, newValue) {
710
+ if (!this.#parsed || oldValue === newValue) {
711
+ return;
712
+ }
713
+ this[name] = newValue;
714
+ }
715
+ upgrade() {
716
+ if (this.#parsed) {
717
+ return;
718
+ }
719
+ this.#parsed = true;
720
+ return this.ready();
721
+ }
722
+ }
723
+
724
+
725
+ const Templated = (SuperClass, template) => {
726
+ return class extends SuperClass {
727
+ async ready() {
644
728
  const slotted = Slots.from(this);
645
729
  const fragment = await Promise.resolve(this.render(slotted, template));
646
730
  this.innerHTML = '';
647
731
  if (fragment) {
648
732
  this.appendChild(fragment);
649
733
  }
650
- this.#rendered = true;
651
734
  }
652
735
  };
653
736
  };
@@ -656,40 +739,35 @@ const Stateful = (SuperClass, flags, others) => {
656
739
 
657
740
  const all = [].concat(flags).concat(others || []);
658
741
 
659
- return class extends SuperClass {
742
+ const k = class extends SuperClass {
660
743
  static get observedAttributes() {
661
744
  return all;
662
745
  }
663
746
  constructor(...args) {
664
747
  super(...args);
665
748
  this.internals_ = this.internals_ || this.attachInternals();
666
- for (const flag of flags) {
667
- Object.defineProperty(this, flag, {
668
- get() {
669
- return this.internals_.states.has(`--${flag}`);
670
- },
671
- set(value) {
672
- //see https://developer.mozilla.org/en-US/docs/Web/API/CustomStateSet#using_double_dash_prefixed_idents
673
- if (Attributes.asBoolean(value)) {
674
- this.internals_.states.add(`--${flag}`);
675
- this.setAttribute(flag, '');
676
- return;
677
- }
678
- this.internals_.states.delete(`--${flag}`);
679
- this.removeAttribute(flag);
680
- }
681
- });
682
- }
683
- }
684
- attributeChangedCallback(name, oldValue, newValue) {
685
- if (oldValue === newValue) {
686
- return;
687
- }
688
- this[name] = newValue;
689
- const method = this[`on${name.charAt(0).toUpperCase()}${name.substr(1).toLowerCase()}Changed`];
690
- method?.call(this, newValue, oldValue);
691
749
  }
692
750
  };
751
+
752
+ for (const flag of flags) {
753
+ Object.defineProperty(k.prototype, flag, {
754
+ enumerable: true,
755
+ configurable: true,
756
+ get() {
757
+ return this.internals_.states.has(`--${flag}`);
758
+ },
759
+ set(value) {
760
+ //see https://developer.mozilla.org/en-US/docs/Web/API/CustomStateSet#using_double_dash_prefixed_idents
761
+ if (Attributes.asBoolean(value)) {
762
+ this.internals_.states.add(`--${flag}`);
763
+ return;
764
+ }
765
+ this.internals_.states.delete(`--${flag}`);
766
+ }
767
+ });
768
+ }
769
+
770
+ return k;
693
771
  };
694
772
 
695
773
  /* global Infinity, CSS */
@@ -706,10 +784,65 @@ function flatten(obj, prefix) {
706
784
  }, {});
707
785
  }
708
786
 
709
- class Form extends Templated(HTMLElement) {
710
- static IGNORED_CHILDREN_SELECTOR = '.d-none, [hidden]';
787
+ function providePath(result, path, value) {
788
+ const keys = path.split(".").map((k) => k.match(/^[0-9]+$/) ? +k : k);
789
+ let current = result;
790
+ let previous = null;
791
+ for (let i = 0; ; ++i) {
792
+ const ckey = keys[i];
793
+ const pkey = keys[i - 1];
794
+ if (Number.isInteger(ckey) && !Array.isArray(current)) {
795
+ if (previous !== null) {
796
+ previous[pkey] = current = [];
797
+ } else {
798
+ result = current = [];
799
+ }
800
+ }
801
+ if (i === keys.length - 1) {
802
+ //when value is undefined we only want to define the property if it's not defined
803
+ current[ckey] = value !== undefined ? value : (ckey in current ? current[ckey] : null);
804
+ return result;
805
+ }
806
+ if (current[ckey] === undefined) {
807
+ current[ckey] = {};
808
+ }
809
+ previous = current;
810
+ current = current[ckey];
811
+ }
812
+ }
711
813
 
712
- render(slotted, template) {
814
+ function extract(el) {
815
+ if (el.getAttribute('type') === 'radio') {
816
+ if (!el.checked) {
817
+ return undefined;
818
+ }
819
+ return el.dataset['fulBindType'] === 'boolean' ? el.value === 'true' : el.value;
820
+ }
821
+ if (el.getAttribute('type') === 'checkbox') {
822
+ return el.checked;
823
+ }
824
+ if (el.dataset['fulBindType'] === 'boolean') {
825
+ return !el.value ? null : el.value === 'true';
826
+ }
827
+ return el.value || null;
828
+ }
829
+
830
+ function mutate(el, raw) {
831
+ if (el.getAttribute('type') === 'radio') {
832
+ el.checked = el.getAttribute('value') === raw;
833
+ return;
834
+ }
835
+ if (el.getAttribute('type') === 'checkbox') {
836
+ el.checked = raw;
837
+ return;
838
+ }
839
+ el.value = raw;
840
+ }
841
+
842
+ class Form extends Templated(ParsedElement) {
843
+ static IGNORED_CHILDREN_SELECTOR = '.d-none, [hidden]';
844
+ static SCROLL_OFFSET = 50;
845
+ render(slotted) {
713
846
  const form = document.createElement('form');
714
847
  form.append(slotted.default);
715
848
  form.addEventListener('submit', async (e) => {
@@ -717,11 +850,11 @@ class Form extends Templated(HTMLElement) {
717
850
  this.spinner(true);
718
851
  try {
719
852
  if (this.submitter) {
720
- await this.submitter(this.getValues(), this);
853
+ await this.submitter(this.values, this);
721
854
  }
722
855
  } catch (e) {
723
856
  if (e instanceof Failure) {
724
- this.setErrors(e.problems);
857
+ this.errors = e.problems;
725
858
  return;
726
859
  }
727
860
  throw e;
@@ -732,21 +865,15 @@ class Form extends Templated(HTMLElement) {
732
865
  return form;
733
866
  }
734
867
  spinner(spin) {
735
- this.querySelectorAll('ful-spinner').forEach(el => {
736
- el.hidden = !spin;
737
- });
738
- this.querySelectorAll('[type=submit],[type=reset]').forEach(el => {
739
- el.disabled = spin;
740
- });
868
+ this.querySelectorAll('ful-spinner').forEach(el => el.hidden = !spin);
869
+ this.querySelectorAll('[type=submit],[type=reset]').forEach(el => el.disabled = spin);
741
870
  }
742
- setValues(values) {
743
- for (const [flattenedKey, value] of Object.entries(flatten(values, ''))) {
744
- Array.from(this.querySelectorAll(`[name='${CSS.escape(flattenedKey)}']`)).forEach((el) => {
745
- Form.mutate(el, value);
746
- });
871
+ set values(vs) {
872
+ for (const [flattenedKey, value] of Object.entries(flatten(vs, ''))) {
873
+ this.querySelectorAll(`[name='${CSS.escape(flattenedKey)}']`).forEach(el => mutate(el, value));
747
874
  }
748
875
  }
749
- getValues() {
876
+ get values() {
750
877
  return Array.from(this.querySelectorAll('[name]'))
751
878
  .filter((el) => {
752
879
  if (el.dataset['fulBindInclude'] === 'never') {
@@ -755,29 +882,30 @@ class Form extends Templated(HTMLElement) {
755
882
  return el.dataset['fulBindInclude'] === 'always' || el.closest(Form.IGNORED_CHILDREN_SELECTOR) === null;
756
883
  })
757
884
  .reduce((result, el) => {
758
- return Form.providePath(result, el.getAttribute('name'), Form.extract(el));
885
+ return providePath(result, el.getAttribute('name'), extract(el));
759
886
  }, {});
760
887
  }
761
- setErrors(errors) {
762
- this.clearErrors();
763
- errors
764
- .filter((e) => e.type === 'FIELD_ERROR' || e.type === 'INVALID_FORMAT')
765
- .forEach((e) => {
766
- const name = e.context.replace("[", ".").replace("].", ".");
767
- this.querySelectorAll(`[name='${CSS.escape(name)}'] [ful-validation-target],[name='${CSS.escape(name)}']:not(:has([ful-validation-target]))`)
768
- .forEach(input => input.classList.add('is-invalid'));
769
- this.querySelectorAll(`ful-field-error[field='${CSS.escape(name)}']`)
770
- .forEach(el => el.innerText = e.reason);
771
- });
772
- this.querySelectorAll("ful-errors")
773
- .forEach(el => {
774
- const globalErrors = errors.filter((e) => e.type !== 'FIELD_ERROR' && e.type !== 'INVALID_FORMAT');
775
- el.innerHTML = globalErrors.map(e => e.reason).join("\n");
776
- if (globalErrors.length !== 0) {
777
- el.removeAttribute('hidden');
778
- }
779
- });
780
-
888
+ set errors(es) {
889
+ const fieldErrors = es.filter((e) => e.type === 'FIELD_ERROR' || e.type === 'INVALID_FORMAT');
890
+ const globalErrors = es.filter((e) => e.type !== 'FIELD_ERROR' && e.type !== 'INVALID_FORMAT');
891
+ this.querySelectorAll('.is-invalid').forEach(el => el.classList.remove('is-invalid'));
892
+ this.querySelectorAll("ful-errors").forEach(el => {
893
+ el.innerHTML = '';
894
+ el.setAttribute('hidden', '');
895
+ });
896
+ fieldErrors.forEach((e) => {
897
+ const name = e.context.replace("[", ".").replace("].", ".");
898
+ const validationTargetsSelector = `[name='${CSS.escape(name)}'] [ful-validation-target],[name='${CSS.escape(name)}']:not(:has([ful-validation-target]))`;
899
+ this.querySelectorAll(validationTargetsSelector).forEach(input => input.classList.add('is-invalid'));
900
+ const fieldErrorsSelector = `ful-field-error[field='${CSS.escape(name)}']`;
901
+ this.querySelectorAll(fieldErrorsSelector).forEach(el => el.innerText = e.reason);
902
+ });
903
+ this.querySelectorAll("ful-errors").forEach(el => {
904
+ el.innerText = globalErrors.map(e => e.reason).join("\n");
905
+ if (globalErrors.length !== 0) {
906
+ el.removeAttribute('hidden');
907
+ }
908
+ });
781
909
  if (!this.hasAttribute('scroll-on-error')) {
782
910
  return;
783
911
  }
@@ -786,74 +914,9 @@ class Form extends Templated(HTMLElement) {
786
914
  .map(el => el.getBoundingClientRect().y + window.scrollY);
787
915
  const miny = Math.min(...ys);
788
916
  if (miny !== Infinity) {
789
- window.scroll(window.scrollX, miny > 100 ? miny - 100 : 0);
790
- }
791
- }
792
- clearErrors() {
793
- this.querySelectorAll('.is-invalid')
794
- .forEach(el => el.classList.remove('is-invalid'));
795
- this.querySelectorAll("ful-errors")
796
- .forEach(el => {
797
- el.innerHTML = '';
798
- el.setAttribute('hidden', '');
799
- });
800
- }
801
- static extract(el) {
802
- if (el.getAttribute('type') === 'radio') {
803
- if (!el.checked) {
804
- return undefined;
805
- }
806
- return el.dataset['fulBindType'] === 'boolean' ? el.value === 'true' : el.value;
807
- }
808
- if (el.getAttribute('type') === 'checkbox') {
809
- return el.checked;
810
- }
811
- if (el.dataset['fulBindType'] === 'boolean') {
812
- return !el.value ? null : el.value === 'true';
813
- }
814
- return el.value || null;
815
- }
816
- static mutate(el, raw) {
817
- if (el.getAttribute('type') === 'radio') {
818
- el.checked = el.getAttribute('value') === raw;
819
- return;
820
- }
821
- if (el.getAttribute('type') === 'checkbox') {
822
- el.checked = raw;
823
- return;
824
- }
825
- el.value = raw;
826
- }
827
-
828
- static providePath(result, path, value) {
829
- const keys = path.split(".").map((k) => k.match(/^[0-9]+$/) ? +k : k);
830
- let current = result;
831
- let previous = null;
832
- for (let i = 0; ; ++i) {
833
- const ckey = keys[i];
834
- const pkey = keys[i - 1];
835
- if (Number.isInteger(ckey) && !Array.isArray(current)) {
836
- if (previous !== null) {
837
- previous[pkey] = current = [];
838
- } else {
839
- result = current = [];
840
- }
841
- }
842
- if (i === keys.length - 1) {
843
- //when value is undefined we only want to define the property if it's not defined
844
- current[ckey] = value !== undefined ? value : (ckey in current ? current[ckey] : null);
845
- return result;
846
- }
847
- if (current[ckey] === undefined) {
848
- current[ckey] = {};
849
- }
850
- previous = current;
851
- current = current[ckey];
917
+ window.scroll(window.scrollX, miny > Form.SCROLL_OFFSET ? miny - Form.SCROLL_OFFSET : 0);
852
918
  }
853
919
  }
854
- static configure() {
855
- customElements.define('ful-form', Form);
856
- }
857
920
  }
858
921
 
859
922
  const ful_input_ec = globalThis.ec || ftl.EvaluationContext.configure({
@@ -872,7 +935,7 @@ const ful_input_template_ = globalThis.ful_input_template || ftl.Template.fromHt
872
935
  <ful-field-error data-tpl-if="name" data-tpl-field="name"></ful-field-error>
873
936
  `, ful_input_ec);
874
937
 
875
- class StatelessInput extends Templated(HTMLElement, ful_input_template_) {
938
+ class StatelessInput extends Templated(ParsedElement, ful_input_template_) {
876
939
  render(slotted, template) {
877
940
  const input = this.input = slotted.input = slotted.input || (() => {
878
941
  const el = document.createElement("input");
@@ -899,21 +962,11 @@ class Input extends Stateful(StatelessInput, [], ['value']) {
899
962
  return fragment;
900
963
  }
901
964
  get value() {
902
- if (!this.input) {
903
- return this.getAttribute('value');
904
- }
905
965
  return this.input.value;
906
966
  }
907
967
  set value(value) {
908
- if (!this.input) {
909
- //handled during rendering
910
- return;
911
- }
912
968
  this.input.value = value;
913
969
  }
914
- static configure() {
915
- customElements.define('ful-input', Input);
916
- }
917
970
  }
918
971
 
919
972
  /**
@@ -938,7 +991,7 @@ const ful_select_template_ = globalThis.ful_select_template || ftl.Template.from
938
991
  `, ful_select_ec);
939
992
 
940
993
 
941
- class Select extends Templated(HTMLElement, ful_select_template_) {
994
+ class Select extends Stateful(Templated(ParsedElement, ful_select_template_), [], ["value"]) {
942
995
  constructor(tsConfig) {
943
996
  super();
944
997
  this.tsConfig = tsConfig;
@@ -1014,17 +1067,6 @@ class Select extends Templated(HTMLElement, ful_select_template_) {
1014
1067
  const v = this.ts.getValue();
1015
1068
  return v === '' ? null : v;
1016
1069
  }
1017
- static custom(tagName, configuration) {
1018
- customElements.define(tagName, class extends Select {
1019
- constructor() {
1020
- super(configuration);
1021
- }
1022
- });
1023
- }
1024
- static configure() {
1025
- return Select.custom('ful-select');
1026
- }
1027
-
1028
1070
  }
1029
1071
 
1030
1072
  const ful_radiogroup_ec = globalThis.ec || ftl.EvaluationContext.configure({
@@ -1053,7 +1095,7 @@ const ful_radiougroup_template_ = globalThis.ful_radiogroup_template || ftl.Temp
1053
1095
  `, ful_radiogroup_ec);
1054
1096
 
1055
1097
 
1056
- class RadioGroup extends Stateful(Templated(HTMLElement, ful_radiougroup_template_), ['disabled']) {
1098
+ class RadioGroup extends Stateful(Templated(ParsedElement, ful_radiougroup_template_), ['disabled'], ['value']) {
1057
1099
  render(slotted, template) {
1058
1100
  const name = this.getAttribute('name') || Attributes.uid('ful-radiogroup');
1059
1101
  const radioEls = Array.from(slotted.default.querySelectorAll('ful-radio'));
@@ -1084,9 +1126,6 @@ class RadioGroup extends Stateful(Templated(HTMLElement, ful_radiougroup_templat
1084
1126
  set value(value){
1085
1127
  this.querySelector(`input[type=radio][value=${CSS.escape(value)}]`).checked = true;
1086
1128
  }
1087
- static configure() {
1088
- customElements.define('ful-radio-group', RadioGroup);
1089
- }
1090
1129
  }
1091
1130
 
1092
1131
  const ful_spinner_ec = globalThis.ec || ftl.EvaluationContext.configure({
@@ -1101,13 +1140,10 @@ const ful_spinner_template_ = globalThis.ful_spinner_template || ftl.Template.fr
1101
1140
  `, ful_spinner_ec);
1102
1141
 
1103
1142
 
1104
- class Spinner extends Templated(HTMLElement, ful_spinner_template_) {
1143
+ class Spinner extends Templated(ParsedElement, ful_spinner_template_) {
1105
1144
  render(slotted, template) {
1106
1145
  return template.render({ slotted });
1107
1146
  }
1108
- static configure() {
1109
- customElements.define('ful-spinner', Spinner);
1110
- }
1111
1147
  }
1112
1148
 
1113
1149
  class Wizard extends HTMLElement {
@@ -1161,7 +1197,7 @@ class Wizard extends HTMLElement {
1161
1197
  cancelable: true
1162
1198
  }));
1163
1199
  }
1164
- moveTo = function (n) {
1200
+ moveTo(n) {
1165
1201
  this.progress.forEach(p => {
1166
1202
  const children = [...p.children];
1167
1203
  const current = children.filter(e => e.matches(".active"))[0];
@@ -1177,17 +1213,20 @@ class Wizard extends HTMLElement {
1177
1213
  cancelable: true
1178
1214
  }));
1179
1215
  }
1180
- static custom(tagName, configuration) {
1181
- customElements.define(tagName, class extends Wizard {
1182
- constructor() {
1183
- super(configuration);
1184
- }
1185
- });
1186
- }
1187
1216
  static configure() {
1188
1217
  return Wizard.custom('ful-wizard');
1189
1218
  }
1190
1219
  }
1191
1220
 
1192
- export { Attributes, AuthorizationCodeFlow, AuthorizationCodeFlowInterceptor, AuthorizationCodeFlowSession, Base64, Failure, Form, Fragments, Hex, HttpClient, Input, LocalStorage, RadioGroup, Select, SessionStorage, Slots, Spinner, Stateful, StatelessInput, Templated, VersionedStorage, Wizard, jsonPatch, jsonPost, jsonPut, jsonRequest, timing };
1221
+ class CustomElements {
1222
+ static configure() {
1223
+ customElements.define('ful-spinner', Spinner);
1224
+ customElements.define('ful-input', Input);
1225
+ customElements.define('ful-radio-group', RadioGroup);
1226
+ customElements.define('ful-select', Select);
1227
+ customElements.define('ful-form', Form);
1228
+ }
1229
+ }
1230
+
1231
+ export { Attributes, AuthorizationCodeFlow, AuthorizationCodeFlowInterceptor, AuthorizationCodeFlowSession, Base64, CustomElements, Failure, Form, Fragments, Hex, HttpClient, Input, LocalStorage, Nodes, ParsedElement, RadioGroup, Select, SessionStorage, Slots, Spinner, Stateful, StatelessInput, SyncEvent, Templated, VersionedStorage, Wizard, jsonPatch, jsonPost, jsonPut, jsonRequest, timing };
1193
1232
  //# sourceMappingURL=ful.mjs.map