@optionfactory/ful 0.42.0 → 0.44.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,85 @@ 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
+ const method = this[`on${name.charAt(0).toUpperCase()}${name.substr(1).toLowerCase()}Changed`];
715
+ method?.call(this, newValue, oldValue);
716
+ }
717
+ upgrade() {
718
+ if (this.#parsed) {
719
+ return;
720
+ }
721
+ this.#parsed = true;
722
+ return this.ready();
723
+ }
724
+ }
725
+
726
+
727
+ const Templated = (SuperClass, template) => {
728
+ return class extends SuperClass {
729
+ async ready() {
644
730
  const slotted = Slots.from(this);
645
731
  const fragment = await Promise.resolve(this.render(slotted, template));
646
732
  this.innerHTML = '';
647
733
  if (fragment) {
648
734
  this.appendChild(fragment);
649
735
  }
650
- this.#rendered = true;
651
736
  }
652
737
  };
653
738
  };
@@ -656,40 +741,35 @@ const Stateful = (SuperClass, flags, others) => {
656
741
 
657
742
  const all = [].concat(flags).concat(others || []);
658
743
 
659
- return class extends SuperClass {
744
+ const k = class extends SuperClass {
660
745
  static get observedAttributes() {
661
746
  return all;
662
747
  }
663
748
  constructor(...args) {
664
749
  super(...args);
665
750
  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
751
  }
692
752
  };
753
+
754
+ for (const flag of flags) {
755
+ Object.defineProperty(k.prototype, flag, {
756
+ enumerable: true,
757
+ configurable: true,
758
+ get() {
759
+ return this.internals_.states.has(`--${flag}`);
760
+ },
761
+ set(value) {
762
+ //see https://developer.mozilla.org/en-US/docs/Web/API/CustomStateSet#using_double_dash_prefixed_idents
763
+ if (Attributes.asBoolean(value)) {
764
+ this.internals_.states.add(`--${flag}`);
765
+ return;
766
+ }
767
+ this.internals_.states.delete(`--${flag}`);
768
+ }
769
+ });
770
+ }
771
+
772
+ return k;
693
773
  };
694
774
 
695
775
  /* global Infinity, CSS */
@@ -706,10 +786,65 @@ function flatten(obj, prefix) {
706
786
  }, {});
707
787
  }
708
788
 
709
- class Form extends Templated(HTMLElement) {
710
- static IGNORED_CHILDREN_SELECTOR = '.d-none, [hidden]';
789
+ function providePath(result, path, value) {
790
+ const keys = path.split(".").map((k) => k.match(/^[0-9]+$/) ? +k : k);
791
+ let current = result;
792
+ let previous = null;
793
+ for (let i = 0; ; ++i) {
794
+ const ckey = keys[i];
795
+ const pkey = keys[i - 1];
796
+ if (Number.isInteger(ckey) && !Array.isArray(current)) {
797
+ if (previous !== null) {
798
+ previous[pkey] = current = [];
799
+ } else {
800
+ result = current = [];
801
+ }
802
+ }
803
+ if (i === keys.length - 1) {
804
+ //when value is undefined we only want to define the property if it's not defined
805
+ current[ckey] = value !== undefined ? value : (ckey in current ? current[ckey] : null);
806
+ return result;
807
+ }
808
+ if (current[ckey] === undefined) {
809
+ current[ckey] = {};
810
+ }
811
+ previous = current;
812
+ current = current[ckey];
813
+ }
814
+ }
711
815
 
712
- render(slotted, template) {
816
+ function extract(el) {
817
+ if (el.getAttribute('type') === 'radio') {
818
+ if (!el.checked) {
819
+ return undefined;
820
+ }
821
+ return el.dataset['fulBindType'] === 'boolean' ? el.value === 'true' : el.value;
822
+ }
823
+ if (el.getAttribute('type') === 'checkbox') {
824
+ return el.checked;
825
+ }
826
+ if (el.dataset['fulBindType'] === 'boolean') {
827
+ return !el.value ? null : el.value === 'true';
828
+ }
829
+ return el.value || null;
830
+ }
831
+
832
+ function mutate(el, raw) {
833
+ if (el.getAttribute('type') === 'radio') {
834
+ el.checked = el.getAttribute('value') === raw;
835
+ return;
836
+ }
837
+ if (el.getAttribute('type') === 'checkbox') {
838
+ el.checked = raw;
839
+ return;
840
+ }
841
+ el.value = raw;
842
+ }
843
+
844
+ class Form extends Templated(ParsedElement) {
845
+ static IGNORED_CHILDREN_SELECTOR = '.d-none, [hidden]';
846
+ static SCROLL_OFFSET = 50;
847
+ render(slotted) {
713
848
  const form = document.createElement('form');
714
849
  form.append(slotted.default);
715
850
  form.addEventListener('submit', async (e) => {
@@ -717,11 +852,11 @@ class Form extends Templated(HTMLElement) {
717
852
  this.spinner(true);
718
853
  try {
719
854
  if (this.submitter) {
720
- await this.submitter(this.getValues(), this);
855
+ await this.submitter(this.values, this);
721
856
  }
722
857
  } catch (e) {
723
858
  if (e instanceof Failure) {
724
- this.setErrors(e.problems);
859
+ this.errors = e.problems;
725
860
  return;
726
861
  }
727
862
  throw e;
@@ -732,21 +867,15 @@ class Form extends Templated(HTMLElement) {
732
867
  return form;
733
868
  }
734
869
  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
- });
870
+ this.querySelectorAll('ful-spinner').forEach(el => el.hidden = !spin);
871
+ this.querySelectorAll('[type=submit],[type=reset]').forEach(el => el.disabled = spin);
741
872
  }
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
- });
873
+ set values(vs) {
874
+ for (const [flattenedKey, value] of Object.entries(flatten(vs, ''))) {
875
+ this.querySelectorAll(`[name='${CSS.escape(flattenedKey)}']`).forEach(el => mutate(el, value));
747
876
  }
748
877
  }
749
- getValues() {
878
+ get values() {
750
879
  return Array.from(this.querySelectorAll('[name]'))
751
880
  .filter((el) => {
752
881
  if (el.dataset['fulBindInclude'] === 'never') {
@@ -755,29 +884,30 @@ class Form extends Templated(HTMLElement) {
755
884
  return el.dataset['fulBindInclude'] === 'always' || el.closest(Form.IGNORED_CHILDREN_SELECTOR) === null;
756
885
  })
757
886
  .reduce((result, el) => {
758
- return Form.providePath(result, el.getAttribute('name'), Form.extract(el));
887
+ return providePath(result, el.getAttribute('name'), extract(el));
759
888
  }, {});
760
889
  }
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
-
890
+ set errors(es) {
891
+ const fieldErrors = es.filter((e) => e.type === 'FIELD_ERROR' || e.type === 'INVALID_FORMAT');
892
+ const globalErrors = es.filter((e) => e.type !== 'FIELD_ERROR' && e.type !== 'INVALID_FORMAT');
893
+ this.querySelectorAll('.is-invalid').forEach(el => el.classList.remove('is-invalid'));
894
+ this.querySelectorAll("ful-errors").forEach(el => {
895
+ el.innerHTML = '';
896
+ el.setAttribute('hidden', '');
897
+ });
898
+ fieldErrors.forEach((e) => {
899
+ const name = e.context.replace("[", ".").replace("].", ".");
900
+ const validationTargetsSelector = `[name='${CSS.escape(name)}'] [ful-validation-target],[name='${CSS.escape(name)}']:not(:has([ful-validation-target]))`;
901
+ this.querySelectorAll(validationTargetsSelector).forEach(input => input.classList.add('is-invalid'));
902
+ const fieldErrorsSelector = `ful-field-error[field='${CSS.escape(name)}']`;
903
+ this.querySelectorAll(fieldErrorsSelector).forEach(el => el.innerText = e.reason);
904
+ });
905
+ this.querySelectorAll("ful-errors").forEach(el => {
906
+ el.innerText = globalErrors.map(e => e.reason).join("\n");
907
+ if (globalErrors.length !== 0) {
908
+ el.removeAttribute('hidden');
909
+ }
910
+ });
781
911
  if (!this.hasAttribute('scroll-on-error')) {
782
912
  return;
783
913
  }
@@ -786,69 +916,7 @@ class Form extends Templated(HTMLElement) {
786
916
  .map(el => el.getBoundingClientRect().y + window.scrollY);
787
917
  const miny = Math.min(...ys);
788
918
  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];
919
+ window.scroll(window.scrollX, miny > Form.SCROLL_OFFSET ? miny - Form.SCROLL_OFFSET : 0);
852
920
  }
853
921
  }
854
922
  static configure() {
@@ -872,7 +940,7 @@ const ful_input_template_ = globalThis.ful_input_template || ftl.Template.fromHt
872
940
  <ful-field-error data-tpl-if="name" data-tpl-field="name"></ful-field-error>
873
941
  `, ful_input_ec);
874
942
 
875
- class StatelessInput extends Templated(HTMLElement, ful_input_template_) {
943
+ class StatelessInput extends Templated(ParsedElement, ful_input_template_) {
876
944
  render(slotted, template) {
877
945
  const input = this.input = slotted.input = slotted.input || (() => {
878
946
  const el = document.createElement("input");
@@ -899,16 +967,9 @@ class Input extends Stateful(StatelessInput, [], ['value']) {
899
967
  return fragment;
900
968
  }
901
969
  get value() {
902
- if (!this.input) {
903
- return this.getAttribute('value');
904
- }
905
970
  return this.input.value;
906
971
  }
907
972
  set value(value) {
908
- if (!this.input) {
909
- //handled during rendering
910
- return;
911
- }
912
973
  this.input.value = value;
913
974
  }
914
975
  static configure() {
@@ -938,7 +999,7 @@ const ful_select_template_ = globalThis.ful_select_template || ftl.Template.from
938
999
  `, ful_select_ec);
939
1000
 
940
1001
 
941
- class Select extends Templated(HTMLElement, ful_select_template_) {
1002
+ class Select extends Stateful(Templated(ParsedElement, ful_select_template_), [], ["value"]) {
942
1003
  constructor(tsConfig) {
943
1004
  super();
944
1005
  this.tsConfig = tsConfig;
@@ -1046,11 +1107,14 @@ const ful_radiougroup_template_ = globalThis.ful_radiogroup_template || ftl.Temp
1046
1107
  </label>
1047
1108
  </section>
1048
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>
1049
1113
  </fieldset>
1050
1114
  `, ful_radiogroup_ec);
1051
1115
 
1052
1116
 
1053
- class RadioGroup extends Stateful(Templated(HTMLElement, ful_radiougroup_template_), ['disabled']) {
1117
+ class RadioGroup extends Stateful(Templated(ParsedElement, ful_radiougroup_template_), ['disabled'], ['value']) {
1054
1118
  render(slotted, template) {
1055
1119
  const name = this.getAttribute('name') || Attributes.uid('ful-radiogroup');
1056
1120
  const radioEls = Array.from(slotted.default.querySelectorAll('ful-radio'));
@@ -1098,7 +1162,7 @@ const ful_spinner_template_ = globalThis.ful_spinner_template || ftl.Template.fr
1098
1162
  `, ful_spinner_ec);
1099
1163
 
1100
1164
 
1101
- class Spinner extends Templated(HTMLElement, ful_spinner_template_) {
1165
+ class Spinner extends Templated(ParsedElement, ful_spinner_template_) {
1102
1166
  render(slotted, template) {
1103
1167
  return template.render({ slotted });
1104
1168
  }
@@ -1158,7 +1222,7 @@ class Wizard extends HTMLElement {
1158
1222
  cancelable: true
1159
1223
  }));
1160
1224
  }
1161
- moveTo = function (n) {
1225
+ moveTo(n) {
1162
1226
  this.progress.forEach(p => {
1163
1227
  const children = [...p.children];
1164
1228
  const current = children.filter(e => e.matches(".active"))[0];
@@ -1174,17 +1238,10 @@ class Wizard extends HTMLElement {
1174
1238
  cancelable: true
1175
1239
  }));
1176
1240
  }
1177
- static custom(tagName, configuration) {
1178
- customElements.define(tagName, class extends Wizard {
1179
- constructor() {
1180
- super(configuration);
1181
- }
1182
- });
1183
- }
1184
1241
  static configure() {
1185
1242
  return Wizard.custom('ful-wizard');
1186
1243
  }
1187
1244
  }
1188
1245
 
1189
- 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 };
1246
+ export { Attributes, AuthorizationCodeFlow, AuthorizationCodeFlowInterceptor, AuthorizationCodeFlowSession, Base64, 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 };
1190
1247
  //# sourceMappingURL=ful.mjs.map