katalyst-tables 3.3.1 → 3.3.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/app/assets/builds/katalyst/tables.esm.js +332 -6
  4. data/app/assets/builds/katalyst/tables.js +332 -6
  5. data/app/assets/builds/katalyst/tables.min.js +1 -1
  6. data/app/assets/builds/katalyst/tables.min.js.map +1 -1
  7. data/app/assets/stylesheets/katalyst/tables/_index.scss +1 -1
  8. data/app/assets/stylesheets/katalyst/tables/_query.scss +109 -0
  9. data/app/assets/stylesheets/katalyst/tables/typed-columns/_date.scss +1 -1
  10. data/app/assets/stylesheets/katalyst/tables/typed-columns/_datetime.scss +1 -1
  11. data/app/components/katalyst/table_component.rb +13 -2
  12. data/app/components/katalyst/tables/cells/currency_component.rb +21 -2
  13. data/app/components/katalyst/tables/cells/number_component.rb +31 -1
  14. data/app/components/katalyst/tables/query/input_component.html.erb +13 -0
  15. data/app/components/katalyst/tables/query/input_component.rb +50 -0
  16. data/app/components/katalyst/tables/query/modal_component.html.erb +33 -0
  17. data/app/components/katalyst/tables/query/modal_component.rb +89 -0
  18. data/app/components/katalyst/tables/query_component.html.erb +8 -0
  19. data/app/components/katalyst/tables/{filter_component.rb → query_component.rb} +37 -30
  20. data/app/controllers/concerns/katalyst/tables/backend.rb +14 -0
  21. data/app/helpers/katalyst/tables/frontend.rb +14 -8
  22. data/app/javascript/tables/application.js +8 -3
  23. data/app/javascript/tables/query_controller.js +108 -0
  24. data/app/javascript/tables/query_input_controller.js +228 -0
  25. data/app/models/concerns/katalyst/tables/collection/core.rb +2 -2
  26. data/app/models/concerns/katalyst/tables/collection/query/array_value_parser.rb +13 -26
  27. data/app/models/concerns/katalyst/tables/collection/query/parser.rb +16 -13
  28. data/app/models/concerns/katalyst/tables/collection/query/single_value_parser.rb +11 -3
  29. data/app/models/concerns/katalyst/tables/collection/query/value_parser.rb +10 -5
  30. data/app/models/concerns/katalyst/tables/collection/query.rb +42 -5
  31. data/app/models/concerns/katalyst/tables/collection/sorting.rb +11 -1
  32. data/app/models/katalyst/tables/collection/base.rb +10 -0
  33. data/app/models/katalyst/tables/collection/filter.rb +10 -0
  34. data/config/locales/tables.en.yml +2 -0
  35. data/{app/models → lib}/katalyst/tables/collection/type/boolean.rb +11 -1
  36. data/lib/katalyst/tables/collection/type/date.rb +57 -0
  37. data/lib/katalyst/tables/collection/type/enum.rb +32 -0
  38. data/lib/katalyst/tables/collection/type/float.rb +21 -0
  39. data/lib/katalyst/tables/collection/type/helpers/delegate.rb +32 -0
  40. data/{app/models → lib}/katalyst/tables/collection/type/helpers/extensions.rb +14 -0
  41. data/lib/katalyst/tables/collection/type/helpers/multiple.rb +60 -0
  42. data/lib/katalyst/tables/collection/type/helpers/range.rb +59 -0
  43. data/lib/katalyst/tables/collection/type/integer.rb +21 -0
  44. data/{app/models → lib}/katalyst/tables/collection/type/value.rb +22 -2
  45. data/{app/models → lib}/katalyst/tables/collection/type.rb +16 -0
  46. data/lib/katalyst/tables/collection.rb +11 -0
  47. data/lib/katalyst/tables.rb +6 -0
  48. metadata +26 -21
  49. data/app/assets/stylesheets/katalyst/tables/_filter.scss +0 -43
  50. data/app/components/katalyst/tables/filter/modal_component.html.erb +0 -25
  51. data/app/components/katalyst/tables/filter/modal_component.rb +0 -112
  52. data/app/components/katalyst/tables/filter_component.html.erb +0 -18
  53. data/app/javascript/tables/filter/modal_controller.js +0 -13
  54. data/app/models/katalyst/tables/collection/type/date.rb +0 -60
  55. data/app/models/katalyst/tables/collection/type/enum.rb +0 -21
  56. data/app/models/katalyst/tables/collection/type/float.rb +0 -57
  57. data/app/models/katalyst/tables/collection/type/helpers/delegate.rb +0 -50
  58. data/app/models/katalyst/tables/collection/type/helpers/multiple.rb +0 -30
  59. data/app/models/katalyst/tables/collection/type/integer.rb +0 -57
  60. /data/{app/models → lib}/katalyst/tables/collection/type/query.rb +0 -0
  61. /data/{app/models → lib}/katalyst/tables/collection/type/search.rb +0 -0
  62. /data/{app/models → lib}/katalyst/tables/collection/type/string.rb +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1431a1666522842053ab70ae3da4f2610bf6fd819be4fe289d992f17c78e0d87
4
- data.tar.gz: be2d0612b1620a052c58e6115b46e388ce3b54bd732d86ba45681bc679e19d68
3
+ metadata.gz: d65cbfb413fd7b0af947d48bad9dc622e1c83f7a659ba4cda14ed993c9aecb7d
4
+ data.tar.gz: 280f74a21d9e14bc418b83274f338b5dc568117df255043336c18cfccea3e4d1
5
5
  SHA512:
6
- metadata.gz: 9d04228e59216901da2268c3aac3dc6076f623fca6c61121a896b215d278265ae2a2fc5f06df97652be02a53a409eefe08fa7d21892a8d28c5d5457efe90a138
7
- data.tar.gz: 25adbd55a147762f4a28aea24f194032eef8e1c27167bfca66a40bd48055b7a018235c94bbe95d6b1a7fe4d49d62d9f5c402034936a0755b394f923cab3668dc
6
+ metadata.gz: 2c3500672cee22ac792eeb2f93eb3107fc94ce7f20320ced58f70a99de7f7e5a66543847615ac379a5004740ade01c553c60ef810ca8c7f854998beedb22219c
7
+ data.tar.gz: f73225573194edcab9e26a2517d6752dfdb7852c0e3284a82cd2c6fd5f47f47aeb185bece9a5f46b3c1191a0b3dfd203165b3eb68a9738e506b836afb342d280
data/README.md CHANGED
@@ -227,7 +227,7 @@ query expressions such as `first_name:Aaron` or `created_at:>2024-01-01` and the
227
227
  will be automatically parsed and applied to the collection attribute, and the collection
228
228
  will automatically generate and apply ActiveRecord conditions to filter the given scope.
229
229
 
230
- There's also a frontend utility, `filter_with(collection:)` that will generate the form
230
+ There's also a frontend utility, `table_query_with(collection:)` that will generate the form
231
231
  and show a modal that helps users to interact with the query interface. More details available
232
232
  in the [query](docs/query.md) documentation.
233
233
 
@@ -533,15 +533,337 @@ class SelectionItemController extends Controller {
533
533
  }
534
534
  }
535
535
 
536
- class FilterModalController extends Controller {
536
+ class QueryController extends Controller {
537
537
  static targets = ["modal"];
538
538
 
539
- close(e) {
539
+ disconnect() {
540
+ delete this.pending;
541
+
542
+ document.removeEventListener("selectionchange", this.selection);
543
+ }
544
+
545
+ focus() {
546
+ if (document.activeElement === this.query) return;
547
+
548
+ this.query.addEventListener(
549
+ "focusin",
550
+ (e) => {
551
+ e.target.setSelectionRange(-1, -1);
552
+ },
553
+ { once: true },
554
+ );
555
+
556
+ this.query.focus();
557
+ }
558
+
559
+ closeModal() {
540
560
  delete this.modalTarget.dataset.open;
561
+
562
+ if (document.activeElement === this.query) document.activeElement.blur();
563
+
564
+ document.removeEventListener("selectionchange", this.selection);
565
+ }
566
+
567
+ openModal() {
568
+ this.modalTarget.dataset.open = true;
569
+
570
+ document.addEventListener("selectionchange", this.selection);
571
+ }
572
+
573
+ clear() {
574
+ if (this.query.value === "") {
575
+ // if the user presses escape once, browser clears the input
576
+ // if the user presses escape again, get them out of here
577
+ this.closeModal();
578
+ }
579
+ }
580
+
581
+ submit() {
582
+ const hasFocus = this.isFocused;
583
+ const position = hasFocus && this.query.selectionStart;
584
+
585
+ if (this.pending) {
586
+ clearTimeout(this.pending);
587
+ delete this.pending;
588
+ }
589
+
590
+ // prevent an unnecessary `?q=` parameter from appearing in the URL
591
+ if (this.query.value === "") {
592
+ this.query.disabled = true;
593
+
594
+ // restore input and focus after form submission
595
+ setTimeout(() => {
596
+ this.query.disabled = false;
597
+ if (hasFocus) this.query.focus();
598
+ }, 0);
599
+ }
600
+
601
+ // add/remove current cursor position
602
+ if (hasFocus && position) {
603
+ this.position.value = position;
604
+ this.position.disabled = false;
605
+ } else {
606
+ this.position.value = "";
607
+ this.position.disabled = true;
608
+ }
609
+ }
610
+
611
+ update = () => {
612
+ if (this.pending) clearTimeout(this.pending);
613
+ this.pending = setTimeout(() => {
614
+ this.element.requestSubmit();
615
+ }, 300);
616
+ };
617
+
618
+ selection = () => {
619
+ if (this.isFocused) this.update();
620
+ };
621
+
622
+ beforeMorphAttribute(e) {
623
+ switch (e.detail.attributeName) {
624
+ case "data-open":
625
+ e.preventDefault();
626
+ break;
627
+ }
541
628
  }
542
629
 
543
- open(e) {
544
- this.modalTarget.dataset.open = "true";
630
+ get query() {
631
+ return this.element.querySelector("input[type=search]");
632
+ }
633
+
634
+ get position() {
635
+ return this.element.querySelector("input[name=p]");
636
+ }
637
+
638
+ get isFocused() {
639
+ return this.query === document.activeElement;
640
+ }
641
+ }
642
+
643
+ class QueryInputController extends Controller {
644
+ static targets = ["input", "highlight"];
645
+ static values = { query: String };
646
+
647
+ connect() {
648
+ this.queryValue = this.inputTarget.value;
649
+ }
650
+
651
+ update() {
652
+ this.queryValue = this.inputTarget.value;
653
+ }
654
+
655
+ queryValueChanged(query) {
656
+ this.highlightTarget.innerHTML = "";
657
+
658
+ new Parser().parse(query).tokens.forEach((token) => {
659
+ this.highlightTarget.appendChild(token.render());
660
+ });
661
+ }
662
+ }
663
+
664
+ class Parser {
665
+ constructor() {
666
+ this.tokens = [];
667
+ this.values = null;
668
+ }
669
+
670
+ parse(input) {
671
+ const query = new StringScanner(input);
672
+
673
+ while (!query.isEos()) {
674
+ this.push(this.skipWhitespace(query));
675
+
676
+ const value = this.takeTagged(query) || this.takeUntagged(query);
677
+
678
+ if (!this.push(value)) break;
679
+ }
680
+
681
+ return this;
682
+ }
683
+
684
+ push(token) {
685
+ if (token) {
686
+ this.values ? this.values.push(token) : this.tokens.push(token);
687
+ }
688
+
689
+ return !!token;
690
+ }
691
+
692
+ skipWhitespace(query) {
693
+ if (!query.scan(/\s+/)) return;
694
+
695
+ return new Token(query.matched());
696
+ }
697
+
698
+ takeUntagged(query) {
699
+ if (!query.scan(/\S+/)) return;
700
+
701
+ return new Untagged(query.matched());
702
+ }
703
+
704
+ takeTagged(query) {
705
+ if (!query.scan(/(\w+(?:\.\w+)?)(:\s*)/)) return;
706
+
707
+ const key = query.valueAt(1);
708
+ const separator = query.valueAt(2);
709
+
710
+ const value =
711
+ this.takeArrayValue(query) || this.takeSingleValue(query) || new Token();
712
+
713
+ return new Tagged(key, separator, value);
714
+ }
715
+
716
+ takeArrayValue(query) {
717
+ if (!query.scan(/\[\s*/)) return;
718
+
719
+ const start = new Token(query.matched());
720
+ const values = (this.values = []);
721
+
722
+ while (!query.isEos()) {
723
+ if (!this.push(this.takeSingleValue(query))) break;
724
+ if (!this.push(this.takeDelimiter(query))) break;
725
+ }
726
+
727
+ query.scan(/\s*]/);
728
+ const end = new Token(query.matched());
729
+
730
+ this.values = null;
731
+
732
+ return new Array$1(start, values, end);
733
+ }
734
+
735
+ takeDelimiter(query) {
736
+ if (!query.scan(/\s*,\s*/)) return;
737
+
738
+ return new Token(query.matched());
739
+ }
740
+
741
+ takeSingleValue(query) {
742
+ return this.takeQuotedValue(query) || this.takeUnquotedValue(query);
743
+ }
744
+
745
+ takeQuotedValue(query) {
746
+ if (!query.scan(/"([^"]*)"/)) return;
747
+
748
+ return new Value(query.matched());
749
+ }
750
+
751
+ takeUnquotedValue(query) {
752
+ if (!query.scan(/[^ \],]*/)) return;
753
+
754
+ return new Value(query.matched());
755
+ }
756
+ }
757
+
758
+ class Token {
759
+ constructor(value = "") {
760
+ this.value = value;
761
+ }
762
+
763
+ render() {
764
+ return document.createTextNode(this.value);
765
+ }
766
+ }
767
+
768
+ class Value extends Token {
769
+ render() {
770
+ const span = document.createElement("span");
771
+ span.className = "value";
772
+ span.innerText = this.value;
773
+
774
+ return span;
775
+ }
776
+ }
777
+
778
+ class Tagged extends Token {
779
+ constructor(key, separator, value) {
780
+ super();
781
+
782
+ this.key = key;
783
+ this.separator = separator;
784
+ this.value = value;
785
+ }
786
+
787
+ render() {
788
+ const span = document.createElement("span");
789
+ span.className = "tag";
790
+
791
+ const key = document.createElement("span");
792
+ key.className = "key";
793
+ key.innerText = this.key;
794
+
795
+ span.appendChild(key);
796
+ span.appendChild(document.createTextNode(this.separator));
797
+ span.appendChild(this.value.render());
798
+
799
+ return span;
800
+ }
801
+ }
802
+
803
+ class Untagged extends Token {
804
+ render() {
805
+ const span = document.createElement("span");
806
+ span.className = "untagged";
807
+ span.innerText = this.value;
808
+ return span;
809
+ }
810
+ }
811
+
812
+ let Array$1 = class Array extends Token {
813
+ constructor(start, values, end) {
814
+ super();
815
+
816
+ this.start = start;
817
+ this.values = values;
818
+ this.end = end;
819
+ }
820
+
821
+ render() {
822
+ const array = document.createElement("span");
823
+ array.className = "array-values";
824
+ array.appendChild(this.start.render());
825
+
826
+ this.values.forEach((value) => {
827
+ const span = document.createElement("span");
828
+ span.appendChild(value.render());
829
+ array.appendChild(span);
830
+ });
831
+
832
+ array.appendChild(this.end.render());
833
+
834
+ return array;
835
+ }
836
+ };
837
+
838
+ class StringScanner {
839
+ constructor(input) {
840
+ this.input = input;
841
+ this.position = 0;
842
+ this.last = null;
843
+ }
844
+
845
+ isEos() {
846
+ return this.position >= this.input.length;
847
+ }
848
+
849
+ scan(regex) {
850
+ const match = regex.exec(this.input.substring(this.position));
851
+ if (match?.index === 0) {
852
+ this.last = match;
853
+ this.position += match[0].length;
854
+ return true;
855
+ } else {
856
+ this.last = {};
857
+ return false;
858
+ }
859
+ }
860
+
861
+ matched() {
862
+ return this.last && this.last[0];
863
+ }
864
+
865
+ valueAt(index) {
866
+ return this.last && this.last[index];
545
867
  }
546
868
  }
547
869
 
@@ -567,8 +889,12 @@ const Definitions = [
567
889
  controllerConstructor: SelectionItemController,
568
890
  },
569
891
  {
570
- identifier: "tables--filter--modal",
571
- controllerConstructor: FilterModalController,
892
+ identifier: "tables--query",
893
+ controllerConstructor: QueryController,
894
+ },
895
+ {
896
+ identifier: "tables--query-input",
897
+ controllerConstructor: QueryInputController,
572
898
  },
573
899
  ];
574
900