alchemy-form 0.1.5 → 0.1.6

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.
Files changed (36) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/assets/stylesheets/form/alchemy_field_array.scss +4 -0
  3. package/assets/stylesheets/form/alchemy_toggle.scss +2 -0
  4. package/assets/stylesheets/form/query_builder.scss +185 -0
  5. package/config/routes.js +8 -0
  6. package/controller/form_api_controller.js +34 -2
  7. package/element/20_query_builder_base.js +82 -0
  8. package/element/alchemy_field.js +19 -2
  9. package/element/alchemy_select.js +37 -2
  10. package/element/alchemy_select_item.js +9 -0
  11. package/element/alchemy_table.js +123 -21
  12. package/element/query_builder.js +90 -0
  13. package/element/query_builder_entry.js +388 -0
  14. package/element/query_builder_group.js +221 -0
  15. package/element/query_builder_value.js +435 -0
  16. package/helper/form_actions/00_form_action.js +328 -0
  17. package/helper/form_actions/url_action.js +69 -0
  18. package/helper/query_builder_variable_definition/00_variable_definition.js +351 -0
  19. package/helper/query_builder_variable_definition/boolean_variable_definition.js +24 -0
  20. package/helper/query_builder_variable_definition/number_variable_definition.js +106 -0
  21. package/helper/query_builder_variable_definition/string_variable_definition.js +46 -0
  22. package/helper_field/query_builder_assignment.js +11 -0
  23. package/helper_field/query_builder_field.js +91 -0
  24. package/helper_field/query_builder_value.js +56 -0
  25. package/package.json +1 -1
  26. package/view/form/elements/alchemy_field_array.hwk +3 -1
  27. package/view/form/elements/alchemy_field_array_entry.hwk +3 -1
  28. package/view/form/elements/alchemy_select_item.hwk +6 -1
  29. package/view/form/elements/query_builder.hwk +1 -0
  30. package/view/form/elements/query_builder_entry.hwk +33 -0
  31. package/view/form/elements/query_builder_group.hwk +64 -0
  32. package/view/form/elements/query_builder_value.hwk +10 -0
  33. package/view/form/inputs/edit/query_builder.hwk +5 -0
  34. package/view/form/inputs/edit/query_builder_assignment.hwk +6 -0
  35. package/view/form/inputs/edit/query_builder_value.hwk +11 -0
  36. package/view/form/select/qb_item.hwk +7 -0
package/CHANGELOG.md CHANGED
@@ -1,3 +1,12 @@
1
+ ## 0.1.6 (2022-05-31)
2
+
3
+ * Fix `FormApi#related` action searching through multiple fields in an `and` group
4
+ * Use microcopy translations in buttons
5
+ * Add `Action` class for user interaction
6
+ * Add context-menu support to `alchemy-table`
7
+ * Add support for using custom templates for `alchemy-select` options
8
+ * Add QueryBuilder
9
+
1
10
  ## 0.1.5 (2022-03-21)
2
11
 
3
12
  * Print `alchemy-select-item` contents as text, not html
@@ -10,4 +10,8 @@ alchemy-field-array-entry {
10
10
  align-content: center;
11
11
  padding-left: 0.5rem;
12
12
  }
13
+
14
+ .button .remove > * {
15
+ writing-mode: vertical-rl;
16
+ }
13
17
  }
@@ -34,6 +34,7 @@ alchemy-toggle {
34
34
  height: 36px;
35
35
  flex: 0 0 134px;
36
36
  border-radius: 4px;
37
+ min-width: 134px;
37
38
 
38
39
  transition: background-color 0.3s cubic-bezier(0, 1, 0.5, 1);
39
40
  background: #848484;
@@ -43,6 +44,7 @@ alchemy-toggle {
43
44
  &:after {
44
45
  text-transform: uppercase;
45
46
  text-align: center;
47
+ box-sizing: inherit;
46
48
  }
47
49
 
48
50
  &:before {
@@ -0,0 +1,185 @@
1
+ :root {
2
+ --qb-border-color: #d3d3d3;
3
+ --qb-btn-background-color: #fafafa;
4
+ --qb-text-color: #3e3e3e;
5
+
6
+ --qb-btn-primary: #286090;
7
+ --qb-btn-primary-border: #204d74;
8
+ --qb-btn-primary-text-color: #fafafa;
9
+
10
+ --qb-btn-active: #3da50e;
11
+ --qb-btn-active-text: #fff;
12
+ --qb-btn-border-color: #d3d3d3;
13
+ }
14
+
15
+ alchemy-query-builder {
16
+ display: block;
17
+ border-color: var(--qb-border-color);
18
+ padding: 1rem;
19
+ position: relative;
20
+ }
21
+
22
+ alchemy-query-builder-group {
23
+ display: block;
24
+ padding: 10px;
25
+ padding-bottom: 6px;
26
+ background-color: rgba(250,240,210,.5);
27
+ border: 1px solid #dcc896;
28
+ position: relative;
29
+
30
+
31
+ margin: 4px 0;
32
+ padding: 5px;
33
+ border: 1px solid #eee;
34
+ border-radius: 3px;
35
+
36
+ .qb-group-header {
37
+ margin-bottom: 10px;
38
+ }
39
+
40
+ .qb-group-actions {
41
+ float: right;
42
+ }
43
+
44
+ .qb-group-body {
45
+ .qb-rules-list {
46
+ display: flex;
47
+ flex-flow: column;
48
+ gap: 1rem;
49
+ padding: 0 0 0 15px;
50
+
51
+ & > :first-child::before {
52
+ top: -11px;
53
+ height: calc(50% + 14px);
54
+ }
55
+
56
+ & > :last-child::before {
57
+ border-radius: 0 0 0 4px;
58
+ }
59
+
60
+ & > ::after {
61
+ top: 50%;
62
+ border-width: 0 0 0 2px;
63
+ }
64
+
65
+ & >:last-child::after {
66
+ display: none;
67
+ }
68
+
69
+ > ::before {
70
+ top: -2px;
71
+ border-width: 0 0 2px 2px;
72
+ }
73
+
74
+ > ::before,
75
+ > ::after {
76
+ content: '';
77
+ position: absolute;
78
+ left: -10px;
79
+ width: 10px;
80
+ height: calc(50% + 4px);
81
+ border-color: #ccc;
82
+ border-style: solid;
83
+ }
84
+ }
85
+ }
86
+ }
87
+
88
+ alchemy-query-builder-value {
89
+ min-width: 10rem;
90
+ }
91
+
92
+ alchemy-query-builder-entry {
93
+ min-width: 40rem;
94
+ }
95
+
96
+ alchemy-query-builder-value,
97
+ alchemy-query-builder-entry {
98
+ display: flex;
99
+ gap: 0.7rem;
100
+
101
+ alchemy-select {
102
+ min-width: 11rem;
103
+ }
104
+
105
+ .qb-delete-wrapper {
106
+ flex: 1;
107
+ text-align: right;
108
+ }
109
+
110
+ .qb-value-wrapper {
111
+ display: flex;
112
+ gap: 0.3rem;
113
+ }
114
+
115
+ .qb-value-input {
116
+ height: 100%;
117
+ color: black;
118
+ padding: 0 0.5rem;
119
+ }
120
+ }
121
+
122
+ .qb-group-invert,
123
+ .qb-button-group,
124
+ .qb-group-type {
125
+ display: inline-flex;
126
+ box-shadow: 0 1px 1px -2px rgba(0,0,0,.2),0 2px 2px 0 rgba(0,0,0,.14),0 1px 5px 0 rgba(0,0,0,.12);
127
+ user-select: none;
128
+
129
+ > input {
130
+ height: 1px;
131
+ width: 1px;
132
+ opacity: 0;
133
+ position: absolute;
134
+ margin: 0 0 0 -1px;
135
+ }
136
+
137
+ > input + label {
138
+ opacity: 0.7;
139
+ }
140
+
141
+ > input:checked + label {
142
+ background-color: var(--qb-btn-active);
143
+ border-color: var(--qb-btn-active);
144
+ color: var(--qb-btn-active-text);
145
+ opacity: 1;
146
+ }
147
+ }
148
+
149
+ .qb-primary {
150
+ --qb-btn-background-color: var(--qb-btn-primary);
151
+ --qb-text-color: var(--qb-btn-primary-text-color);
152
+ --qb-btn-border-color: var(--qb-btn-primary-border);
153
+ }
154
+
155
+ button.qb-btn {
156
+ border-color: var(--qb-btn-border-color);
157
+ }
158
+
159
+ .qb-btn {
160
+ background-color: var(--qb-btn-background-color);
161
+ color: var(--qb-text-color);
162
+ transition: box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1);
163
+ border-radius: 2px;
164
+ cursor: pointer;
165
+ display: inline-block;
166
+ font-weight: bold;
167
+ text-align: center;
168
+ padding: 2px 12px 1px;
169
+ }
170
+
171
+ .qb-select-item {
172
+ display: flex;
173
+ flex-flow: column;
174
+ padding-right: 1rem;
175
+
176
+ .option-title {
177
+ font-weight: bold;
178
+ font-size: 1rem;
179
+ white-space: nowrap;
180
+ }
181
+
182
+ .option-description {
183
+ min-width: 10rem;
184
+ }
185
+ }
package/config/routes.js CHANGED
@@ -2,4 +2,12 @@ Router.add({
2
2
  name : 'FormApi#related',
3
3
  methods : 'post',
4
4
  paths : '/api/form/data/related',
5
+ policy : 'logged_in',
6
+ });
7
+
8
+ Router.add({
9
+ name : 'FormApi#queryBuilderData',
10
+ methods : 'post',
11
+ paths : '/api/form/data/qbdata',
12
+ policy : 'logged_in',
5
13
  });
@@ -17,7 +17,7 @@ const FormApi = Function.inherits('Alchemy.Controller', 'FormApi');
17
17
  *
18
18
  * @author Jelle De Loecker <jelle@elevenways.be>
19
19
  * @since 0.1.0
20
- * @version 0.1.5
20
+ * @version 0.1.6
21
21
  *
22
22
  * @param {Conduit} conduit
23
23
  */
@@ -28,6 +28,7 @@ FormApi.setAction(async function related(conduit) {
28
28
  const model = this.getModel(body.assoc_model);
29
29
  let crit = model.find();
30
30
  crit.limit(50);
31
+ crit.setOption('scenario', 'related_data');
31
32
 
32
33
  if (body.config && body.config.search) {
33
34
  let display_fields = Array.cast(model.displayField);
@@ -40,7 +41,7 @@ FormApi.setAction(async function related(conduit) {
40
41
  continue;
41
42
  }
42
43
 
43
- crit.where(field).equals(rx);
44
+ or.where(field).equals(rx);
44
45
  }
45
46
  }
46
47
 
@@ -53,3 +54,34 @@ FormApi.setAction(async function related(conduit) {
53
54
 
54
55
  conduit.end(result);
55
56
  });
57
+
58
+
59
+ /**
60
+ * The related action
61
+ *
62
+ * @author Jelle De Loecker <jelle@elevenways.be>
63
+ * @since 0.1.6
64
+ * @version 0.1.6
65
+ *
66
+ * @param {Conduit} conduit
67
+ */
68
+ FormApi.setAction(async function queryBuilderData(conduit) {
69
+
70
+ const body = conduit.body;
71
+ const config = body.config || {};
72
+ let result;
73
+
74
+ if (body && body.model) {
75
+ const model = this.getModel(body.model);
76
+
77
+ if (body.$pk) {
78
+ const doc = await model.findByPk(body.$pk);
79
+
80
+ if (doc) {
81
+ result = await doc.loadQueryBuilderData(config);
82
+ }
83
+ }
84
+ }
85
+
86
+ conduit.end(result);
87
+ });
@@ -0,0 +1,82 @@
1
+ /**
2
+ * The base abstract query builder custom element
3
+ *
4
+ * @author Jelle De Loecker <jelle@elevenways.be>
5
+ * @since 0.1.6
6
+ * @version 0.1.6
7
+ */
8
+ const QueryBuilderBase = Function.inherits('Alchemy.Element.Form.Base', 'QueryBuilderBase');
9
+
10
+ /**
11
+ * Don't register this as a custom element,
12
+ * but don't let child classes inherit this
13
+ *
14
+ * @author Jelle De Loecker <jelle@elevenways.be>
15
+ * @since 0.1.6
16
+ * @version 0.1.6
17
+ */
18
+ QueryBuilderBase.setStatic('is_abstract_class', true, false);
19
+
20
+ /**
21
+ * The stylesheet to load for this element
22
+ *
23
+ * @author Jelle De Loecker <jelle@elevenways.be>
24
+ * @since 0.1.6
25
+ * @version 0.1.6
26
+ */
27
+ QueryBuilderBase.setStylesheetFile('form/query_builder');
28
+
29
+ /**
30
+ * Get the dataprovider
31
+ *
32
+ * @author Jelle De Loecker <jelle@elevenways.be>
33
+ * @since 0.1.6
34
+ * @version 0.1.6
35
+ */
36
+ QueryBuilderBase.enforceProperty(function dataprovider(new_value) {
37
+
38
+ if (new_value == null) {
39
+ if (this.assigned_data.dataprovider) {
40
+ new_value = this.assigned_data.dataprovider;
41
+ } else if (this.root_query_builder && this.root_query_builder.dataprovider) {
42
+ new_value = this.root_query_builder.dataprovider;
43
+ }
44
+ } else {
45
+ this.assigned_data.dataprovider = new_value;
46
+ }
47
+
48
+ return new_value;
49
+ });
50
+
51
+ /**
52
+ * Getter for the rules list element
53
+ *
54
+ * @author Jelle De Loecker <jelle@elevenways.be>
55
+ * @since 0.1.6
56
+ * @version 0.1.6
57
+ */
58
+ QueryBuilderBase.setProperty(function root_query_builder() {
59
+ return this.queryParents('alchemy-query-builder');
60
+ });
61
+
62
+ /**
63
+ * Get the value type of the given input
64
+ *
65
+ * @author Jelle De Loecker <jelle@elevenways.be>
66
+ * @since 0.1.6
67
+ * @version 0.1.6
68
+ *
69
+ * @param {HTMLElement}
70
+ *
71
+ * @return {String}
72
+ */
73
+ QueryBuilderBase.setMethod(function getValueType(element) {
74
+
75
+ if (!element) {
76
+ return null;
77
+ }
78
+
79
+ let result = element.value_type || element.type;
80
+
81
+ return result;
82
+ });
@@ -708,16 +708,33 @@ Field.setMethod(function retained() {
708
708
  *
709
709
  * @author Jelle De Loecker <jelle@elevenways.be>
710
710
  * @since 0.1.0
711
- * @version 0.1.0
711
+ * @version 0.1.6
712
712
  *
713
713
  * @param {Object} config
714
714
  * @param {HTMLElement} element
715
715
  */
716
- Field.setMethod(function loadData(config, element) {
716
+ Field.setMethod(async function loadData(config, element) {
717
717
 
718
718
  let field = this.config;
719
719
 
720
720
  if (field) {
721
+
722
+ let result;
723
+
724
+ if (typeof field.loadData == 'function') {
725
+
726
+ try {
727
+ result = await field.loadData(config, element);
728
+ } catch (err) {
729
+ // Ignore
730
+ console.error('Error loading field data:', err);
731
+ }
732
+
733
+ if (result) {
734
+ return result;
735
+ }
736
+ }
737
+
721
738
  return this.hawkejs_helpers.Alchemy.getResource({
722
739
  name : 'FormApi#related',
723
740
  post : true,
@@ -36,6 +36,26 @@ AlchemySelect.setProperty('_clear_count', 0);
36
36
  */
37
37
  AlchemySelect.setAttribute('total-item-count', {number: true});
38
38
 
39
+ /**
40
+ * The optional template to use for value items
41
+ * (The chosen value)
42
+ *
43
+ * @author Jelle De Loecker <jelle@develry.be>
44
+ * @since 0.1.6
45
+ * @version 0.1.6
46
+ */
47
+ AlchemySelect.setAttribute('value-item-template');
48
+
49
+ /**
50
+ * The optional template to use for option items
51
+ * (The items visible in the dropdown)
52
+ *
53
+ * @author Jelle De Loecker <jelle@develry.be>
54
+ * @since 0.1.6
55
+ * @version 0.1.6
56
+ */
57
+ AlchemySelect.setAttribute('option-item-template');
58
+
39
59
  /**
40
60
  * The hawkejs template to use
41
61
  *
@@ -809,12 +829,16 @@ AlchemySelect.setMethod(function _processPreloadedValues() {
809
829
  *
810
830
  * @author Jelle De Loecker <jelle@develry.be>
811
831
  * @since 0.1.0
812
- * @version 0.1.0
832
+ * @version 0.1.6
813
833
  *
814
834
  * @param {Object} response
815
835
  */
816
836
  AlchemySelect.setMethod(function _processResponseData(response) {
817
837
 
838
+ if (!response) {
839
+ response = {};
840
+ }
841
+
818
842
  if (response.available) {
819
843
  this.total_item_count = response.available;
820
844
  }
@@ -823,6 +847,10 @@ AlchemySelect.setMethod(function _processResponseData(response) {
823
847
  record,
824
848
  item;
825
849
 
850
+ if (!records) {
851
+ records = [];
852
+ }
853
+
826
854
  for (record of records) {
827
855
  item = this._makeOption(record._id || record.id, record);
828
856
  this.addToDropdown(item);
@@ -1166,7 +1194,7 @@ AlchemySelect.setMethod(function close(event) {
1166
1194
  *
1167
1195
  * @author Jelle De Loecker <jelle@develry.be>
1168
1196
  * @since 0.1.0
1169
- * @version 0.1.5
1197
+ * @version 0.1.6
1170
1198
  *
1171
1199
  * @param {String} type "value" or "option"
1172
1200
  * @param {Mixed} value The actual value of this item
@@ -1181,6 +1209,13 @@ AlchemySelect.setMethod(function _makeValueItem(type, value, data) {
1181
1209
  // Set the type ("value" or "option")
1182
1210
  item.type = type;
1183
1211
 
1212
+ // Set the custom template to use, if any
1213
+ let custom_template = this[type + '-item-template'];
1214
+
1215
+ if (custom_template) {
1216
+ item.custom_template = custom_template;
1217
+ }
1218
+
1184
1219
  // Assign the value
1185
1220
  item.value = value;
1186
1221
 
@@ -37,6 +37,15 @@ Item.setAttribute('type');
37
37
  */
38
38
  Item.setAttribute('selected', {boolean: true});
39
39
 
40
+ /**
41
+ * Should a custom template be used?
42
+ *
43
+ * @author Jelle De Loecker <jelle@elevenways.be>
44
+ * @since 0.1.6
45
+ * @version 0.1.6
46
+ */
47
+ Item.setAttribute('custom-template');
48
+
40
49
  /**
41
50
  * The value of this item
42
51
  *
@@ -513,7 +513,7 @@ Table.setMethod(function getCurrentStateUrl() {
513
513
  *
514
514
  * @author Jelle De Loecker <jelle@elevenways.be>
515
515
  * @since 0.1.0
516
- * @version 0.1.0
516
+ * @version 0.1.6
517
517
  *
518
518
  * @param {Array} records
519
519
  */
@@ -530,6 +530,7 @@ Table.setMethod(function setRecords(records) {
530
530
  }
531
531
 
532
532
  this.showPagination();
533
+ this.attachContextMenus();
533
534
  });
534
535
 
535
536
  /**
@@ -593,7 +594,7 @@ Table.setMethod(function showPagination() {
593
594
  *
594
595
  * @author Jelle De Loecker <jelle@elevenways.be>
595
596
  * @since 0.1.0
596
- * @version 0.1.1
597
+ * @version 0.1.6
597
598
  *
598
599
  * @param {Object} entry
599
600
  *
@@ -604,7 +605,10 @@ Table.setMethod(function createDataRow(entry) {
604
605
  let field,
605
606
  value,
606
607
  tr = this.createElement('tr'),
607
- td;
608
+ td,
609
+ id = entry.$pk || entry._id || entry.id;
610
+
611
+ tr.dataset.pk = id;
608
612
 
609
613
  for (field of this.fieldset) {
610
614
  td = this.createElement('td');
@@ -623,31 +627,22 @@ Table.setMethod(function createDataRow(entry) {
623
627
  td.classList.add('aft-actions');
624
628
  tr.append(td);
625
629
 
626
- let actions;
627
-
628
- if (entry.$hold && entry.$hold.actions) {
629
- actions = entry.$hold.actions;
630
- } else {
631
- actions = entry.$actions;
632
- }
630
+ let actions = this.getEntryActions(entry, 'row');
633
631
 
634
632
  if (actions && actions.length) {
633
+
635
634
  let action,
636
635
  anchor;
637
636
 
637
+ // Iterate over all the defined actions
638
638
  for (action of actions) {
639
- anchor = this.createElement('a');
640
- anchor.dataset.name = action.name;
641
-
642
- if (action.icon) {
643
- let alico = this.createElement('al-ico');
644
- alico.setAttribute('type', action.icon);
645
- anchor.append(alico);
646
- } else {
647
- anchor.textContent = action.title || action.name;
639
+
640
+ anchor = action.constructElement(this);
641
+
642
+ if (!anchor) {
643
+ continue;
648
644
  }
649
645
 
650
- anchor.setAttribute('href', action.url);
651
646
  td.append(anchor);
652
647
  }
653
648
  }
@@ -658,6 +653,111 @@ Table.setMethod(function createDataRow(entry) {
658
653
  return tr;
659
654
  });
660
655
 
656
+ /**
657
+ * Get a list of actions
658
+ *
659
+ * @author Jelle De Loecker <jelle@elevenways.be>
660
+ * @since 0.1.6
661
+ * @version 0.1.6
662
+ *
663
+ * @param {Object} record
664
+ * @param {Stirng} filter
665
+ *
666
+ * @return {Alchemy.Form.Action.Action[]}
667
+ */
668
+ Table.setMethod(function getEntryActions(record, filter) {
669
+
670
+ let result = [];
671
+
672
+ if (!record) {
673
+ return result;
674
+ }
675
+
676
+ let actions;
677
+
678
+ if (record.$hold && record.$hold.actions) {
679
+ actions = record.$hold.actions;
680
+ } else {
681
+ actions = record.$actions;
682
+ }
683
+
684
+ if(!actions || !actions.length) {
685
+ return result;
686
+ }
687
+
688
+ for (let action of actions) {
689
+
690
+ if (!action.type) {
691
+ action.type = 'url';
692
+ }
693
+
694
+ // Make sure it's an action instance
695
+ action = Classes.Alchemy.Form.Action.Action.cast(action);
696
+
697
+ if (!action || (filter && !action.isAllowedIn(filter))) {
698
+ continue;
699
+ }
700
+
701
+ result.push(action);
702
+ }
703
+
704
+ return result;
705
+ });
706
+
707
+ /**
708
+ * Add context menus to rows
709
+ *
710
+ * @author Jelle De Loecker <jelle@elevenways.be>
711
+ * @since 0.1.6
712
+ * @version 0.1.6
713
+ *
714
+ * @param {Object} entry
715
+ */
716
+ Table.setMethod(function attachContextMenus() {
717
+
718
+ if (!Blast.isBrowser) {
719
+ return;
720
+ }
721
+
722
+ if (!this.assigned_data.records || !this.assigned_data.records.length) {
723
+ return;
724
+ }
725
+
726
+ for (let record of this.assigned_data.records) {
727
+
728
+ let context_actions = this.getEntryActions(record, 'context');
729
+
730
+ if (!context_actions || !context_actions.length) {
731
+ contintue;
732
+ }
733
+
734
+ let pk = record.$pk || record._id || record.id;
735
+
736
+ if (!pk) {
737
+ continue;
738
+ }
739
+
740
+ let tr = this.querySelector('[data-pk="' + pk + '"]');
741
+
742
+ if (!tr) {
743
+ continue;
744
+ }
745
+
746
+ tr.addEventListener('contextmenu', e => {
747
+
748
+ this.selectRow(tr);
749
+
750
+ let menu = this.createElement('he-context-menu');
751
+
752
+ for (let action of context_actions) {
753
+ action.addToContextMenu(menu);
754
+ }
755
+
756
+ menu.show(e);
757
+ });
758
+ }
759
+ });
760
+
661
761
  /**
662
762
  * On new fieldset
663
763
  *
@@ -931,12 +1031,14 @@ Table.setMethod(function sortData() {
931
1031
  *
932
1032
  * @author Jelle De Loecker <jelle@elevenways.be>
933
1033
  * @since 0.1.0
934
- * @version 0.1.0
1034
+ * @version 0.1.6
935
1035
  */
936
1036
  Table.setMethod(function introduced() {
937
1037
 
938
1038
  const that = this;
939
1039
 
1040
+ this.attachContextMenus();
1041
+
940
1042
  this.addEventListener('click', function onClick(e) {
941
1043
 
942
1044
  let target = e.target,