@gudhub/ssg-web-components-library 1.0.115 → 1.0.116

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gudhub/ssg-web-components-library",
3
- "version": "1.0.115",
3
+ "version": "1.0.116",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -10,6 +10,9 @@
10
10
  <button type="submit" class="btn">${buttonText ? buttonText : 'Get in touch'}</button>
11
11
  <div class="loader"></div>
12
12
  </div>
13
+ <div class="recaptcha-notice">
14
+ This site is protected by reCAPTCHA.
15
+ </div>
13
16
  </form>
14
17
  <div class="overflow success">
15
18
  <div class="icon_wrapper">
@@ -5,6 +5,7 @@ import defaultConfigs from './get-in-touch-form-data.json';
5
5
  import { checkInputsValidations } from './inputsValidation.js';
6
6
 
7
7
  let recaptchaPromise = null;
8
+ let recaptchaBadgeObserver = null;
8
9
 
9
10
  function loadRecaptcha(siteKey) {
10
11
  if (!siteKey) {
@@ -35,16 +36,41 @@ function loadRecaptcha(siteKey) {
35
36
  return recaptchaPromise;
36
37
  }
37
38
 
39
+ function hideRecaptchaBadge() {
40
+ const badge = document.querySelector('.grecaptcha-badge');
41
+
42
+ if (!badge) return;
43
+
44
+ badge.style.setProperty('visibility', 'hidden', 'important');
45
+ badge.style.setProperty('opacity', '0', 'important');
46
+ badge.style.setProperty('pointer-events', 'none', 'important');
47
+ }
48
+
49
+ function observeRecaptchaBadge() {
50
+ hideRecaptchaBadge();
51
+
52
+ if (recaptchaBadgeObserver) return;
53
+
54
+ recaptchaBadgeObserver = new MutationObserver(() => {
55
+ hideRecaptchaBadge();
56
+ });
57
+
58
+ recaptchaBadgeObserver.observe(document.body, {
59
+ childList: true,
60
+ subtree: true
61
+ });
62
+ }
63
+
38
64
  class GetInTouchForm extends GHComponent {
39
65
  constructor() {
40
66
  super();
41
67
 
42
- this.formId = this.getAttribute("data-form-id");
68
+ this.formId = this.getAttribute('data-form-id');
43
69
  this.defaultConfigs = defaultConfigs;
44
70
 
45
71
  this.generateInput = this.generateInput;
46
72
  this.isFormSubmitted = false;
47
-
73
+
48
74
  this.placement = 'main';
49
75
  this.config = window.getConfig()?.componentsConfigs?.formConfig || window.getConfig()?.formConfig;
50
76
 
@@ -66,7 +92,11 @@ class GetInTouchForm extends GHComponent {
66
92
  this.attachEventListeners();
67
93
 
68
94
  if (this.recaptcha_site_key) {
69
- loadRecaptcha(this.recaptcha_site_key).catch(console.error);
95
+ loadRecaptcha(this.recaptcha_site_key)
96
+ .then(() => {
97
+ observeRecaptchaBadge();
98
+ })
99
+ .catch(console.error);
70
100
  }
71
101
  }
72
102
  }
@@ -76,15 +106,26 @@ class GetInTouchForm extends GHComponent {
76
106
  super.render(html);
77
107
  this.attachEventListeners();
78
108
 
79
- loadRecaptcha(this.recaptcha_site_key).catch(console.error);
109
+ if (this.recaptcha_site_key) {
110
+ loadRecaptcha(this.recaptcha_site_key)
111
+ .then(() => {
112
+ observeRecaptchaBadge();
113
+ })
114
+ .catch(console.error);
115
+ }
80
116
  }
81
117
 
82
118
  attachEventListeners() {
83
- this.getElementsByTagName('form')[0]
84
- .addEventListener('submit', (e) => this.handleSubmit(e));
119
+ const form = this.getElementsByTagName('form')[0];
120
+ const restartButton = this.getElementsByClassName('restart_button')[0];
85
121
 
86
- this.getElementsByClassName('restart_button')[0]
87
- .addEventListener('click', () => this.hideFail());
122
+ if (form) {
123
+ form.addEventListener('submit', (e) => this.handleSubmit(e));
124
+ }
125
+
126
+ if (restartButton) {
127
+ restartButton.addEventListener('click', () => this.hideFail());
128
+ }
88
129
  }
89
130
 
90
131
  onParentPopupClose() {
@@ -99,7 +140,7 @@ class GetInTouchForm extends GHComponent {
99
140
  initConfig(formConfigs) {
100
141
  try {
101
142
  this.config = formConfigs.find(({ id }) => id === this.formId);
102
- if (!this.config) throw new Error("Config not found");
143
+ if (!this.config) throw new Error('Config not found');
103
144
  } catch {
104
145
  const defaultId = this.isInPopup ? 'default popup' : 'default';
105
146
  this.config = defaultConfigs.find(({ id }) => id === defaultId);
@@ -107,7 +148,7 @@ class GetInTouchForm extends GHComponent {
107
148
 
108
149
  this.titleName = this.getAttribute('data-form-title') || this.config.title;
109
150
  this.subtitleName = this.getAttribute('data-form-subtitle') || this.config.subtitle;
110
- this.placement = this.getAttribute('data-form-placement') || "main";
151
+ this.placement = this.getAttribute('data-form-placement') || 'main';
111
152
  this.buttonText = this.getAttribute('data-form-button-text') || this.config.button_text;
112
153
  }
113
154
 
@@ -121,6 +162,8 @@ class GetInTouchForm extends GHComponent {
121
162
 
122
163
  return new Promise((resolve, reject) => {
123
164
  grecaptcha.ready(() => {
165
+ hideRecaptchaBadge();
166
+
124
167
  grecaptcha.execute(this.recaptcha_site_key, { action })
125
168
  .then(resolve)
126
169
  .catch(reject);
@@ -146,7 +189,6 @@ class GetInTouchForm extends GHComponent {
146
189
  }
147
190
 
148
191
  this.handleSuccessFormValidation(form, token);
149
-
150
192
  } catch (error) {
151
193
  console.error(error);
152
194
  this.showFail();
@@ -179,7 +221,7 @@ class GetInTouchForm extends GHComponent {
179
221
  inputsValidation = async (form) => {
180
222
  const inputs = Array.from(form.querySelectorAll('input'));
181
223
  return checkInputsValidations(inputs);
182
- }
224
+ };
183
225
 
184
226
  verifyRecaptcha = async (recaptchaToken) => {
185
227
  const response = await fetch('/api/verify-recaptcha', {
@@ -200,7 +242,7 @@ class GetInTouchForm extends GHComponent {
200
242
  }
201
243
 
202
244
  return data;
203
- }
245
+ };
204
246
 
205
247
  createDataObject(form, placement, recaptchaToken) {
206
248
  const inputs = {};
@@ -230,13 +272,24 @@ class GetInTouchForm extends GHComponent {
230
272
 
231
273
  addLoader() {
232
274
  this.classList.add('loading');
233
- this.querySelector('button[type="submit"]').disabled = true;
275
+
276
+ const btn = this.querySelector('button[type="submit"]');
277
+
278
+ if (btn) {
279
+ btn.disabled = true;
280
+ }
234
281
  }
235
282
 
236
283
  removeLoader() {
237
284
  this.classList.remove('loading');
285
+
238
286
  const btn = this.querySelector('button[type="submit"]');
239
- setTimeout(() => btn.disabled = false, 500);
287
+
288
+ if (btn) {
289
+ setTimeout(() => {
290
+ btn.disabled = false;
291
+ }, 500);
292
+ }
240
293
  }
241
294
 
242
295
  showSuccess({ email, phone }) {
@@ -278,6 +331,9 @@ class GetInTouchForm extends GHComponent {
278
331
 
279
332
  hideFail() {
280
333
  const el = this.querySelector('.overflow.fail');
334
+
335
+ if (!el) return;
336
+
281
337
  el.style.opacity = 0;
282
338
 
283
339
  setTimeout(() => {
@@ -315,4 +371,4 @@ class GetInTouchForm extends GHComponent {
315
371
 
316
372
  if (!customElements.get('get-in-touch-form')) {
317
373
  customElements.define('get-in-touch-form', GetInTouchForm);
318
- }
374
+ }
@@ -271,6 +271,12 @@ get-in-touch-form {
271
271
  }
272
272
  }
273
273
  }
274
+ .recaptcha-notice {
275
+ margin-top: 10px;
276
+ font-size: 12px;
277
+ line-height: 1.4;
278
+ opacity: 0.7;
279
+ }
274
280
  &[data-in-popup] {
275
281
  .get-in-touch-form {
276
282
  padding-top: 0;
@@ -368,4 +374,10 @@ popup-container[data-position="bottom-right"] {
368
374
  }
369
375
  }
370
376
  }
377
+ }
378
+
379
+ body .grecaptcha-badge {
380
+ visibility: hidden !important;
381
+ opacity: 0 !important;
382
+ pointer-events: none !important;
371
383
  }
@@ -6,16 +6,74 @@
6
6
  </div>
7
7
 
8
8
  <div class="blocks-wrapper">
9
- <div class="block" gh-array="${ghId}.items" itemscope="" itemtype="https://schema.org/Table">
10
- ${json.items.reduce((acc, item, index) => {
11
- return acc + `
12
- <div class="item" gh-item itemprop="about" itemscope="" itemtype="https://schema.org/Thing">
13
- <p class="item-title" gh-id="${ghId}.items.${index}.title" itemprop="name"></p>
14
- <p class="item-price" gh-id="${ghId}.items.${index}.price" itemprop="description"></p>
15
- </div>
16
- `
17
- }, '')}
18
- </div>
9
+ <table
10
+ class="block"
11
+ itemscope
12
+ itemtype="${json.schema?.tableType || 'https://schema.org/Table'}"
13
+ >
14
+ ${json.headers?.length ? `
15
+ <thead>
16
+ <tr class="table-head" gh-array="${ghId}.headers">
17
+ ${json.headers.reduce((acc, item, index) => {
18
+ return acc + `
19
+ <th gh-item>
20
+ <span gh-id="${ghId}.headers.${index}"></span>
21
+ </th>
22
+ `
23
+ }, '')}
24
+ </tr>
25
+ </thead>
26
+ ` : ''}
27
+
28
+ <tbody gh-array="${ghId}.items">
29
+ ${json.items.reduce((acc, item, index) => {
30
+ return acc + `
31
+ <tr
32
+ class="item"
33
+ gh-item
34
+ itemprop="${json.schema?.rowProp || 'about'}"
35
+ itemscope
36
+ itemtype="${json.schema?.rowType || 'https://schema.org/Thing'}"
37
+ >
38
+ <td>
39
+ <p
40
+ class="item-title"
41
+ gh-id="${ghId}.items.${index}.title"
42
+ itemprop="${json.schema?.titleProp || 'name'}"
43
+ ></p>
44
+ </td>
45
+
46
+ ${json.schema?.valueType === 'offer' ? `
47
+ <td
48
+ itemprop="${json.schema?.offerProp || 'offers'}"
49
+ itemscope
50
+ itemtype="${json.schema?.offerType || 'https://schema.org/Offer'}"
51
+ >
52
+ <p class="item-price">
53
+ $<span
54
+ gh-id="${ghId}.items.${index}.price"
55
+ itemprop="${json.schema?.priceProp || 'price'}"
56
+ ></span>
57
+ <span
58
+ gh-id="${ghId}.items.${index}.currency"
59
+ itemprop="${json.schema?.currencyProp || 'priceCurrency'}"
60
+ ></span>
61
+ </p>
62
+ </td>
63
+ ` : `
64
+ <td>
65
+ <p
66
+ class="item-price"
67
+ gh-id="${ghId}.items.${index}.price"
68
+ itemprop="${json.schema?.valueProp || 'description'}"
69
+ ></p>
70
+ </td>
71
+ `}
72
+ </tr>
73
+ `
74
+ }, '')}
75
+ </tbody>
76
+ </table>
19
77
  </div>
20
78
  </div>
21
79
  </section>
@@ -1,22 +1,41 @@
1
1
  {
2
- "title": "Lorem Ipsum Pricing",
3
- "subtitle": "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
4
- "items": [
5
- {
6
- "title": "Basic Plan",
7
- "price": "$19 / month"
2
+ "title": "Lorem Ipsum Pricing",
3
+ "subtitle": "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
4
+ "schema": {
5
+ "tableType": "https://schema.org/Table",
6
+ "rowProp": "about",
7
+ "rowType": "https://schema.org/Service",
8
+ "titleProp": "name",
9
+ "valueType": "offer",
10
+ "offerProp": "offers",
11
+ "offerType": "https://schema.org/Offer",
12
+ "priceProp": "price",
13
+ "currencyProp": "priceCurrency"
8
14
  },
9
- {
10
- "title": "Standard Plan",
11
- "price": "$49 / month"
12
- },
13
- {
14
- "title": "Premium Plan",
15
- "price": "$99 / month"
16
- },
17
- {
18
- "title": "Enterprise Plan",
19
- "price": "Custom pricing"
20
- }
21
- ]
22
- }
15
+ "headers": [
16
+ "Service Name",
17
+ "Price Starts at"
18
+ ],
19
+ "items": [
20
+ {
21
+ "title": "Lorem Ipsum Service",
22
+ "price": "120",
23
+ "currency": "USD"
24
+ },
25
+ {
26
+ "title": "Dolor Sit Service",
27
+ "price": "240",
28
+ "currency": "USD"
29
+ },
30
+ {
31
+ "title": "Consectetur Service",
32
+ "price": "360",
33
+ "currency": "USD"
34
+ },
35
+ {
36
+ "title": "Adipiscing Service",
37
+ "price": "480",
38
+ "currency": "USD"
39
+ }
40
+ ]
41
+ }
@@ -1,40 +1,176 @@
1
- # Component data-object:
1
+ Ось оновлений README під твій новий динамічний компонент з `headers` та `schema`.
2
2
 
3
- ("?": means "unnecessary")
3
+ ````md
4
+ # Component data-object
5
+
6
+ `("?": means "unnecessary")`
4
7
 
5
8
  ```json
6
9
  {
7
- "title?": "Lorem Ipsum Pricing",
10
+ "title?": "Lorem Ipsum Overview",
8
11
  "subtitle?": "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
12
+ "schema?": {
13
+ "tableType": "https://schema.org/Table",
14
+ "rowProp": "about",
15
+ "rowType": "https://schema.org/Thing",
16
+ "titleProp": "name",
17
+ "valueProp": "description"
18
+ },
19
+ "headers?": [
20
+ "Feature Name",
21
+ "Description"
22
+ ],
9
23
  "items": [
10
24
  {
11
- "title": "Basic Plan",
12
- "price": "$19 / month"
25
+ "title": "Lorem Ipsum Feature",
26
+ "price": "Lorem ipsum dolor sit amet"
13
27
  },
14
28
  {
15
- "title": "Standard Plan",
16
- "price": "$49 / month"
29
+ "title": "Dolor Sit Amet",
30
+ "price": "Consectetur adipiscing elit"
17
31
  },
18
32
  {
19
- "title": "Premium Plan",
20
- "price": "$99 / month"
33
+ "title": "Sed Do Eiusmod",
34
+ "price": "Tempor incididunt ut labore"
21
35
  },
22
36
  {
23
- "title": "Enterprise Plan",
24
- "price": "Custom pricing"
37
+ "title": "Ut Enim Ad Minim",
38
+ "price": "Veniam quis nostrud exercitation"
25
39
  }
26
40
  ]
27
41
  }
28
- ```
42
+ ````
29
43
 
30
44
  ## Fields description
31
45
 
32
- `title` - component title. If field does not exist, title will not be rendered.
46
+ `title` - component title. If the field does not exist, the title will not be rendered.
47
+
48
+ `subtitle` - component subtitle. If the field does not exist, the subtitle will not be rendered.
49
+
50
+ `schema` - optional object for configuring microdata markup.
51
+
52
+ `schema.tableType` - Schema.org type for the table. Default value is `https://schema.org/Table`.
53
+
54
+ `schema.rowProp` - item property used for each table row. Default value is `about`.
55
+
56
+ `schema.rowType` - Schema.org type for each table row. Default value is `https://schema.org/Thing`.
57
+
58
+ `schema.titleProp` - item property for the first column value. Default value is `name`.
59
+
60
+ `schema.valueProp` - item property for the second column value. Default value is `description`.
61
+
62
+ `headers` - optional array of table header labels. If the field does not exist or is empty, the table header will not be rendered.
63
+
64
+ `items` - array of table rows.
65
+
66
+ `items.title` - item title. Usually rendered in the first table column.
67
+
68
+ `items.price` - item price/value/description. Usually rendered in the second table column.
69
+
70
+ ---
71
+
72
+ # Pricing table example
73
+
74
+ Use this structure when the table contains services and prices.
75
+
76
+ ```json
77
+ {
78
+ "title": "3D Rendering Services Pricing",
79
+ "subtitle": "Explore starting prices for different 3D rendering services.",
80
+ "schema": {
81
+ "tableType": "https://schema.org/Table",
82
+ "rowProp": "about",
83
+ "rowType": "https://schema.org/Service",
84
+ "titleProp": "name",
85
+ "valueType": "offer",
86
+ "offerProp": "offers",
87
+ "offerType": "https://schema.org/Offer",
88
+ "priceProp": "price",
89
+ "currencyProp": "priceCurrency"
90
+ },
91
+ "headers": [
92
+ "Service Name",
93
+ "Price Starts at"
94
+ ],
95
+ "items": [
96
+ {
97
+ "title": "Interior 3D Rendering",
98
+ "price": "547",
99
+ "currency": "CAD"
100
+ },
101
+ {
102
+ "title": "Exterior 3D Rendering",
103
+ "price": "683",
104
+ "currency": "CAD"
105
+ },
106
+ {
107
+ "title": "Commercial Interior 3D Rendering",
108
+ "price": "820",
109
+ "currency": "CAD"
110
+ },
111
+ {
112
+ "title": "Commercial Exterior 3D Rendering",
113
+ "price": "957",
114
+ "currency": "CAD"
115
+ }
116
+ ]
117
+ }
118
+ ```
119
+
120
+ ## Additional fields for pricing table
121
+
122
+ `schema.valueType` - defines how the second column should be rendered. If the value is `offer`, the component renders the second column as a Schema.org `Offer`.
123
+
124
+ `schema.offerProp` - item property for the offer object. Default value is `offers`.
125
+
126
+ `schema.offerType` - Schema.org type for the offer. Default value is `https://schema.org/Offer`.
127
+
128
+ `schema.priceProp` - item property for the price value. Default value is `price`.
129
+
130
+ `schema.currencyProp` - item property for the currency value. Default value is `priceCurrency`.
131
+
132
+ `items.currency` - price currency. Required when `schema.valueType` is set to `offer`.
133
+
134
+ ---
135
+
136
+ # Explanation
137
+
138
+ The component supports two table formats.
33
139
 
34
- `subtitle` - component subtitle. If field does not exist, subtitle will not be rendered.
140
+ The first format is a regular informational table. In this case, each row is marked as a Schema.org `Thing`, the first column is marked as `name`, and the second column is marked as `description`.
35
141
 
36
- `items` - array of table items.
142
+ Example output structure:
37
143
 
38
- `items.title` - item title.
144
+ ```text
145
+ Table
146
+ └── about: Thing
147
+ ├── name: Lorem Ipsum Feature
148
+ └── description: Lorem ipsum dolor sit amet
149
+ ```
150
+
151
+ The second format is a pricing table. In this case, each row is marked as a Schema.org `Service`, and the second column is rendered as an `Offer` with separate `price` and `priceCurrency` values.
152
+
153
+ Example output structure:
154
+
155
+ ```text
156
+ Table
157
+ └── about: Service
158
+ ├── name: Interior 3D Rendering
159
+ └── offers: Offer
160
+ ├── price: 547
161
+ └── priceCurrency: CAD
162
+ ```
163
+
164
+ For regular text tables, use:
165
+
166
+ ```json
167
+ "valueProp": "description"
168
+ ```
169
+
170
+ For pricing tables, use:
171
+
172
+ ```json
173
+ "valueType": "offer"
174
+ ```
39
175
 
40
- `items.price` - item price/value.
176
+ This allows the same component to be reused for simple informational tables and for service pricing tables with more correct microdata markup.
@@ -4,53 +4,90 @@ info-table {
4
4
  }
5
5
 
6
6
  .top-wrapper {
7
- display: flex;
8
- align-items: center;
7
+ margin-bottom: 30px;
9
8
 
10
- h2 {
11
- width: 50%;
12
- font-size: 18px;
9
+ .title {
10
+ color: #05183A;
13
11
  font-weight: 700;
14
- color: black;
15
- line-height: 120%;
12
+ font-size: 30px;
13
+ line-height: 136%;
14
+ letter-spacing: 0%;
15
+ text-align: center;
16
+ text-transform: uppercase;
17
+ margin-bottom: 12px;
16
18
  }
17
19
 
18
- p {
19
- width: 50%;
20
+ .subtitle {
21
+ font-weight: 400;
20
22
  font-size: 18px;
21
- font-weight: 700 !important;
22
- color: black !important;
23
- line-height: 120%;
23
+ line-height: 136%;
24
+ letter-spacing: 0%;
25
+ text-align: justify;
26
+ color: #63636F;
24
27
  text-align: center;
25
28
  }
26
-
27
- .title {
28
- margin-bottom: 30px;
29
- }
30
29
  }
30
+
31
31
  .block {
32
+ width: 100%;
32
33
  margin-bottom: 25px;
33
34
  border: 1px solid #C8C8C8;
35
+ border-collapse: collapse;
36
+ table-layout: fixed;
37
+ }
38
+
39
+ .table-head {
40
+ th {
41
+ width: 50%;
42
+ height: 52px;
43
+ padding: 0 15px;
44
+ border-bottom: 1px solid #C8C8C8;
45
+ vertical-align: middle;
46
+ text-align: center;
47
+ font-size: 18px;
48
+ font-weight: 700;
49
+ color: black;
50
+ line-height: 120%;
51
+ }
52
+
53
+ th:not(:last-child) {
54
+ border-right: 1px solid #C8C8C8;
55
+ }
34
56
  }
35
- .item {
36
- display: flex;
37
57
 
58
+ .item {
38
59
  &:nth-child(even) {
39
60
  background-color: #F6F6F6;
40
61
  }
41
- &:not(:last-child) {
62
+
63
+ td {
64
+ width: 50%;
65
+ height: 48px;
66
+ padding: 0 15px;
42
67
  border-bottom: 1px solid #C8C8C8;
68
+ vertical-align: middle;
69
+ text-align: center;
70
+ }
71
+
72
+ td:not(:last-child) {
73
+ border-right: 1px solid #C8C8C8;
74
+ }
75
+
76
+ &:last-child {
77
+ td {
78
+ border-bottom: none;
79
+ }
43
80
  }
44
81
 
45
82
  &-title,
46
83
  &-price {
47
- line-height: 48px;
84
+ width: 100%;
85
+ margin: 0;
86
+ font-size: 16px;
87
+ font-weight: 400;
88
+ color: black;
89
+ line-height: 140%;
48
90
  text-align: center;
49
- width: 50%;
50
- height: 48px;
51
- }
52
- &-price {
53
- border-left: 1px solid #C8C8C8;
54
91
  }
55
92
  }
56
93
 
@@ -70,6 +107,38 @@ info-table {
70
107
  @media (max-width: 700px) {
71
108
  section {
72
109
  padding-bottom: 40px;
73
- }
110
+ }
111
+
112
+ .top-wrapper {
113
+ margin-bottom: 24px;
114
+
115
+ .title {
116
+ font-size: 26px;
117
+ }
118
+
119
+ .subtitle {
120
+ font-size: 16px;
121
+ }
122
+ }
123
+
124
+ .table-head {
125
+ th {
126
+ height: 48px;
127
+ padding: 0 10px;
128
+ font-size: 16px;
129
+ }
130
+ }
131
+
132
+ .item {
133
+ td {
134
+ height: 46px;
135
+ padding: 0 10px;
136
+ }
137
+
138
+ &-title,
139
+ &-price {
140
+ font-size: 15px;
141
+ }
142
+ }
74
143
  }
75
- }
144
+ }