@gudhub/ssg-web-components-library 1.0.99 → 1.0.101

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.99",
3
+ "version": "1.0.101",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -1,10 +1,8 @@
1
1
  import html from './edit-mode.html';
2
2
  import './edit-mode.scss';
3
-
4
3
  import './gudhub-login-popup.scss';
5
4
 
6
5
  import { EditModeNotifications } from './EditModeNotifications.webcomponent.js';
7
-
8
6
  import { isUndefined } from './Helpers.js';
9
7
 
10
8
  class EditMode extends GHComponent {
@@ -13,6 +11,9 @@ class EditMode extends GHComponent {
13
11
 
14
12
  this.notificationComponentRendered = false;
15
13
  this.editModeActive = false;
14
+
15
+ this._arrayControlsCleanup = null;
16
+ this._ghIdClickCleanup = null;
16
17
  }
17
18
 
18
19
  onServerRender() {
@@ -23,16 +24,17 @@ class EditMode extends GHComponent {
23
24
  if (!document.querySelector('script#gudhub-library')) {
24
25
  let gudhubLibraryScript = document.createElement('script');
25
26
  gudhubLibraryScript.id = 'gudhub-library';
26
- gudhubLibraryScript.setAttribute('src', 'https://unpkg.com/@gudhub/core@1.2.0/umd/library.min.js');
27
+ gudhubLibraryScript.setAttribute('src', 'https://unpkg.com/@gudhub/core/umd/library.min.js');
27
28
  document.body.append(gudhubLibraryScript);
28
29
  }
30
+
29
31
  this.editModeActive = !this.editModeActive;
30
32
 
31
- if(this.editModeActive === true && this.notificationComponentRendered === false) {
33
+ if (this.editModeActive === true && this.notificationComponentRendered === false) {
32
34
  this.renderNotificationsComponent();
33
35
  }
34
36
 
35
- if(this.editModeActive) {
37
+ if (this.editModeActive) {
36
38
  this.initEditors();
37
39
  } else {
38
40
  this.disableEditors();
@@ -40,64 +42,59 @@ class EditMode extends GHComponent {
40
42
  }
41
43
 
42
44
  disableEditors() {
45
+ if (this._arrayControlsCleanup) {
46
+ this._arrayControlsCleanup();
47
+ this._arrayControlsCleanup = null;
48
+ }
49
+
50
+ if (this._ghIdClickCleanup) {
51
+ this._ghIdClickCleanup();
52
+ this._ghIdClickCleanup = null;
53
+ }
54
+
43
55
  const body = document.getElementsByTagName('body')[0];
44
56
  body.classList.remove('edit-mode-enabled');
57
+
45
58
  const elementsToEdit = document.querySelectorAll('[gh-id]');
46
- if(elementsToEdit.length) {
59
+ if (elementsToEdit.length) {
47
60
  elementsToEdit.forEach(element => {
48
- if(element.classList.contains('mce-content-body')) {
61
+ if (element.classList.contains('mce-content-body')) {
49
62
  tinymce.get(element.id).destroy();
50
63
  }
51
64
  element.replaceWith(element.cloneNode(true));
52
- })
65
+ });
53
66
  }
54
67
  }
55
68
 
56
69
  initEditors() {
57
70
  const body = document.getElementsByTagName('body')[0];
58
71
  body.classList.add('edit-mode-enabled');
72
+
59
73
  const showLoginPopup = () => this.showLoginPopup();
60
74
  const initGudHub = (auth_key) => this.initGudHub(auth_key);
61
- if(!document.querySelector('script[id="tinymce_script"]')) {
62
- const script = document.createElement('script');
63
75
 
76
+ if (!document.querySelector('script[id="tinymce_script"]')) {
77
+ const script = document.createElement('script');
64
78
  script.setAttribute('src', 'https://cdn.tiny.cloud/1/ts08yt1lknwldsqs5d4iohomcmpfd2wolmcy7lao74r3ita3/tinymce/6/tinymce.min.js');
65
79
  script.setAttribute('referrerpolicy', 'origin');
66
80
  script.setAttribute('id', 'tinymce_script');
67
-
68
81
  document.querySelector('head').appendChild(script);
69
82
  }
70
83
 
71
- return new Promise(async (resolve) => {
72
- const elementsToEdit = document.querySelectorAll('[gh-id]');
73
- if(elementsToEdit.length) {
74
- elementsToEdit.forEach(element => {
75
- element.addEventListener('click', e => {
76
- if(isUndefined(tinymce)) {
77
- alert('TinyMCE not ready yet')
78
- } else if(element.getAttribute('contenteditable') != true) {
79
- const parentGhComponent = (() => {
80
- let parentElement = element.parentElement;
81
- while (parentElement) {
82
- if (parentElement.tagName.includes('-')) {
83
- return parentElement;
84
- }
85
- parentElement = parentElement.parentElement;
86
- }
87
-
88
- return null;
89
- })()
90
- initTinyMce(element, parentGhComponent);
91
- }
92
- });
93
- })
94
- }
95
- resolve();
96
- });
84
+ if (!this._arrayControlsCleanup) {
85
+ this._arrayControlsCleanup = initArrayItemControls();
86
+ }
87
+
88
+ if (!this._ghIdClickCleanup) {
89
+ this._ghIdClickCleanup = initGhIdClickDelegation();
90
+ }
91
+
92
+ return Promise.resolve();
97
93
 
98
94
  function initTinyMce(element, parentGhComponent) {
99
95
  const self = parentGhComponent;
100
- let editorInstance = tinymce.init({
96
+
97
+ tinymce.init({
101
98
  target: element,
102
99
  plugins: 'advlist autolink lists link image charmap preview anchor pagebreak',
103
100
  toolbar_mode: 'floating',
@@ -112,39 +109,55 @@ class EditMode extends GHComponent {
112
109
  setup: (editor) => {
113
110
  editor.on('init', e => {
114
111
  e.target.focus();
115
- })
112
+ });
113
+
114
+ editor.on('remove', () => {
115
+ element.classList.remove('gh-id-editing');
116
+ });
117
+
116
118
  editor.ui.registry.addButton('saveButton', {
117
119
  text: 'Save',
118
120
  onAction: async (_) => {
119
- if(isUndefined(window.gudhub)) {
120
- if(window.localStorage.getItem('gudhub_auth_key')) {
121
+ if (isUndefined(window.gudhub)) {
122
+ if (window.localStorage.getItem('gudhub_auth_key')) {
121
123
  initGudHub(window.localStorage.getItem('gudhub_auth_key'));
122
124
  } else {
123
125
  showLoginPopup();
124
126
  return;
125
127
  }
126
128
  }
127
- let ids = await self.findIds();
128
129
 
130
+ let ids = await self.findIds();
129
131
  const currentChapter = window?.constants?.currentChapter || 'pages';
130
132
 
131
- const data = await gudhub.getDocument({app_id: ids.appId, item_id: ids.itemId, element_id: document.querySelector('html').getAttribute(`data-${currentChapter}-json_field_id`)});
133
+ const data = await gudhub.getDocument({
134
+ app_id: ids.appId,
135
+ item_id: ids.itemId,
136
+ element_id: document.querySelector('html').getAttribute(`data-${currentChapter}-json_field_id`)
137
+ });
138
+
132
139
  const json = JSON.parse(data.data);
140
+
133
141
  const content = editor.getContent();
134
142
  const ghId = element.getAttribute('gh-id');
135
- if(ghId.indexOf('.') === -1) {
143
+
144
+ if (ghId.indexOf('.') === -1) {
136
145
  json[ghId] = content;
137
146
  } else {
138
147
  const findedJson = self.findValueByPath(json, ghId.substring(0, ghId.lastIndexOf('.')));
139
148
  findedJson[ghId.substring(ghId.lastIndexOf('.') + 1, ghId.length)] = content;
140
149
  }
141
- await gudhub.createDocument({app_id: ids.appId, item_id: ids.itemId, element_id: document.querySelector('html').getAttribute(`data-${currentChapter}-json_field_id`), data: JSON.stringify(json) });
150
+
151
+ await gudhub.createDocument({
152
+ app_id: ids.appId,
153
+ item_id: ids.itemId,
154
+ element_id: document.querySelector('html').getAttribute(`data-${currentChapter}-json_field_id`),
155
+ data: JSON.stringify(json)
156
+ });
142
157
 
143
158
  window.dispatchEvent(new CustomEvent('add-edit-mode-notification', {
144
- detail: {
145
- text: 'Saved'
146
- }
147
- }))
159
+ detail: { text: 'Saved' }
160
+ }));
148
161
  }
149
162
  });
150
163
 
@@ -170,42 +183,511 @@ class EditMode extends GHComponent {
170
183
 
171
184
  editor.ui.registry.addButton('accentColor', {
172
185
  text: 'Color',
173
- onAction: function() {
186
+ onAction: function () {
174
187
  editor.formatter.toggle('accentColor');
175
188
  }
176
189
  });
177
190
 
178
191
  editor.ui.registry.addButton('accentBackground', {
179
192
  text: 'Background',
180
- onAction: function() {
193
+ onAction: function () {
181
194
  editor.formatter.toggle('accentBackground');
182
195
  }
183
196
  });
184
197
  }
185
- });
198
+ });
199
+ }
200
+
201
+ function initGhIdClickDelegation() {
202
+ const onClick = (e) => {
203
+ const element = e.target?.closest?.('[gh-id]');
204
+ const isTinyUI = e.target?.closest?.('.tox, .tox-toolbar, .tox-editor-container');
205
+ const isArrayPanel = e.target?.closest?.('#gh-array-panel');
206
+
207
+ if (!element && !isTinyUI && !isArrayPanel) {
208
+ document.querySelectorAll('.gh-id-editing').forEach(el => {
209
+ el.classList.remove('gh-id-editing');
210
+
211
+ if (el.classList.contains('mce-content-body')) {
212
+ const ed = tinymce.get(el.id);
213
+ if (ed) ed.destroy();
214
+ }
215
+ });
216
+ return;
217
+ }
218
+
219
+ if (!element) return;
220
+
221
+ if (isUndefined(tinymce)) {
222
+ alert('TinyMCE not ready yet');
223
+ return;
224
+ }
225
+
226
+ if (element.getAttribute('contenteditable') == 'true') return;
227
+
228
+ const parentGhComponent = (() => {
229
+ let parentElement = element;
230
+ while (parentElement) {
231
+ if (parentElement.tagName && parentElement.tagName.includes('-') && typeof parentElement.findIds === 'function') {
232
+ return parentElement;
233
+ }
234
+ parentElement = parentElement.parentElement;
235
+ }
236
+ return null;
237
+ })();
238
+
239
+ if (!parentGhComponent) return;
240
+
241
+ const panel = document.querySelector('#gh-array-panel');
242
+ if (panel) {
243
+ panel.style.display = 'none';
244
+ panel.setAttribute('aria-hidden', 'true');
245
+ }
246
+
247
+ document
248
+ .querySelectorAll('.gh-id-editing')
249
+ .forEach(el => el.classList.remove('gh-id-editing'));
250
+
251
+ element.classList.add('gh-id-editing');
252
+ initTinyMce(element, parentGhComponent);
253
+ };
254
+
255
+ document.addEventListener('click', onClick, true);
256
+
257
+ return () => document.removeEventListener('click', onClick, true);
258
+ }
259
+
260
+ function initArrayItemControls({ root = document } = {}) {
261
+ function stopEvent(event) {
262
+ event.preventDefault();
263
+ event.stopPropagation();
264
+ }
265
+
266
+ function deepClone(value) {
267
+ try { return structuredClone(value); }
268
+ catch { return JSON.parse(JSON.stringify(value)); }
269
+ }
270
+
271
+ function getValueByPath(object, pathArray) {
272
+ let currentValue = object;
273
+ for (const key of pathArray) {
274
+ if (currentValue == null) return undefined;
275
+ currentValue = currentValue[key];
276
+ }
277
+ return currentValue;
278
+ }
279
+
280
+ function setValueByPath(object, pathArray, value) {
281
+ if (!object || !pathArray?.length) return;
282
+
283
+ let current = object;
284
+ for (let i = 0; i < pathArray.length - 1; i++) {
285
+ const key = pathArray[i];
286
+ if (current[key] == null) {
287
+ current[key] = (typeof pathArray[i + 1] === 'number') ? [] : {};
288
+ }
289
+ current = current[key];
290
+ }
291
+ current[pathArray[pathArray.length - 1]] = value;
292
+ }
293
+
294
+ function parseGhId(ghId) {
295
+ if (!ghId) return null;
296
+
297
+ const pathParts = ghId.split('.').map(part =>
298
+ String(+part) === part ? Number(part) : part
299
+ );
300
+
301
+ const numericIndexes = pathParts
302
+ .map((value, index) =>
303
+ typeof value === 'number' && Number.isFinite(value) ? index : -1
304
+ )
305
+ .filter(index => index !== -1);
306
+
307
+ if (!numericIndexes.length) return null;
308
+
309
+ const lastIndexPosition = numericIndexes[numericIndexes.length - 1];
310
+
311
+ return {
312
+ fullPath: pathParts,
313
+ arrayPath: pathParts.slice(0, lastIndexPosition),
314
+ itemIndexPosition: lastIndexPosition,
315
+ itemIndex: pathParts[lastIndexPosition],
316
+ };
317
+ }
318
+
319
+ let controlPanel = document.querySelector('#gh-array-panel');
320
+
321
+ if (!controlPanel) {
322
+ controlPanel = document.createElement('div');
323
+ controlPanel.id = 'gh-array-panel';
324
+
325
+ controlPanel.innerHTML = `
326
+ <button type="button" data-action="add" class="gh-panel-btn">
327
+ Add
328
+ </button>
329
+ <button type="button" data-action="remove" class="gh-panel-btn">
330
+ Remove
331
+ </button>
332
+ `;
333
+
334
+ document.body.appendChild(controlPanel);
335
+ }
336
+
337
+ async function findIdsFromElement(element) {
338
+ let parent = element.parentElement;
339
+
340
+ while (parent) {
341
+ if (
342
+ parent.tagName &&
343
+ parent.tagName.includes('-') &&
344
+ typeof parent.findIds === 'function'
345
+ ) {
346
+ return parent.findIds();
347
+ }
348
+ parent = parent.parentElement;
349
+ }
350
+
351
+ return null;
352
+ }
353
+
354
+ function ensureGudhubInitialized() {
355
+ if (window.gudhub) return true;
356
+
357
+ const authKey = localStorage.getItem('gudhub_auth_key');
358
+ if (!authKey) {
359
+ showLoginPopup();
360
+ return false;
361
+ }
362
+
363
+ initGudHub(authKey);
364
+ return true;
365
+ }
366
+
367
+ let cachedDocumentKey = null;
368
+ let cachedJson = null;
369
+
370
+ async function loadJsonForElement(element) {
371
+ const ids = await findIdsFromElement(element);
372
+ if (!ids) return null;
373
+
374
+ const currentChapter = window?.constants?.currentChapter || 'pages';
375
+ const elementId = document.documentElement.getAttribute(`data-${currentChapter}-json_field_id`);
376
+
377
+ const cacheKey = `${ids.appId}:${ids.itemId}:${elementId}`;
378
+
379
+ if (cacheKey === cachedDocumentKey && cachedJson) {
380
+ return { ids, elementId, json: cachedJson };
381
+ }
382
+
383
+ const response = await gudhub.getDocument({
384
+ app_id: ids.appId,
385
+ item_id: ids.itemId,
386
+ element_id: elementId,
387
+ });
388
+
389
+ const parsedJson = JSON.parse(response.data);
390
+
391
+ cachedDocumentKey = cacheKey;
392
+ cachedJson = parsedJson;
393
+
394
+ return { ids, elementId, json: parsedJson };
395
+ }
396
+
397
+ function updateGhIdsIndexInside(rootEl, itemIndexPosition, newIndex) {
398
+ const nodes = rootEl.querySelectorAll('[gh-id]');
399
+ nodes.forEach(node => {
400
+ const ghId = node.getAttribute('gh-id');
401
+ if (!ghId) return;
402
+
403
+ const parts = ghId.split('.');
404
+ if (itemIndexPosition < 0 || itemIndexPosition >= parts.length) return;
405
+
406
+ parts[itemIndexPosition] = String(newIndex);
407
+ node.setAttribute('gh-id', parts.join('.'));
408
+ });
409
+ }
410
+
411
+ function getPathPartsFromGhId(ghId) {
412
+ return ghId.split('.').map(part =>
413
+ String(+part) === part ? Number(part) : part
414
+ );
415
+ }
416
+
417
+ function fillGhIdValuesFromJson(rootEl, json) {
418
+ const nodes = rootEl.querySelectorAll('[gh-id]');
419
+ nodes.forEach(node => {
420
+ const ghId = node.getAttribute('gh-id');
421
+ if (!ghId) return;
422
+
423
+ const path = getPathPartsFromGhId(ghId);
424
+ const value = getValueByPath(json, path);
425
+
426
+ node.innerHTML = value ?? '';
427
+ });
428
+ }
429
+
430
+ function getDomValue(node) {
431
+ if (node.id && window.tinymce) {
432
+ const ed = tinymce.get(node.id);
433
+ if (ed) return ed.getContent();
434
+ }
435
+ return node.innerHTML;
436
+ }
437
+
438
+ function syncDomToJsonForContainer(container, json) {
439
+ const nodes = container.querySelectorAll('[gh-id]');
440
+ nodes.forEach(node => {
441
+ const ghId = node.getAttribute('gh-id');
442
+ if (!ghId) return;
443
+
444
+ const path = getPathPartsFromGhId(ghId);
445
+ setValueByPath(json, path, getDomValue(node));
446
+ });
447
+ }
448
+
449
+ function rerenderArrayContainerFromJson({ targetElement, json, arrayPath }) {
450
+ const container = targetElement.closest('[gh-array]');
451
+ if (!container) return false;
452
+
453
+ const arr = getValueByPath(json, arrayPath);
454
+ if (!Array.isArray(arr)) return false;
455
+
456
+ const itemSelector = container.querySelector('[gh-item]') ? '[gh-item]' : '.item';
457
+ const firstItem = container.querySelector(itemSelector);
458
+ if (!firstItem) return false;
459
+
460
+ const template = firstItem.cloneNode(true);
461
+
462
+ const anyGh = template.querySelector('[gh-id]');
463
+ const parsed = anyGh ? parseGhId(anyGh.getAttribute('gh-id')) : null;
464
+ const itemIndexPosition = parsed?.itemIndexPosition;
465
+ if (typeof itemIndexPosition !== 'number') return false;
466
+
467
+ container.innerHTML = '';
468
+
469
+ for (let i = 0; i < arr.length; i++) {
470
+ const clone = template.cloneNode(true);
471
+
472
+ updateGhIdsIndexInside(clone, itemIndexPosition, i);
473
+ fillGhIdValuesFromJson(clone, json);
474
+
475
+ container.appendChild(clone);
476
+ }
477
+
478
+ return true;
479
+ }
480
+
481
+ let currentHover = null;
482
+ let hideTimer = null;
483
+
484
+ function scheduleHidePanel() {
485
+ clearTimeout(hideTimer);
486
+ hideTimer = setTimeout(() => hidePanel(), 250);
487
+ }
488
+
489
+ function cancelHidePanel() {
490
+ clearTimeout(hideTimer);
491
+ hideTimer = null;
492
+ }
493
+
494
+ function hidePanel() {
495
+ controlPanel.style.display = 'none';
496
+ currentHover = null;
497
+ lastHoveredItem = null;
498
+
499
+ document.querySelectorAll('.gh-item-hovered').forEach(el => el.classList.remove('gh-item-hovered'));
500
+ controlPanel.setAttribute('aria-hidden', 'true');
501
+ }
502
+
503
+ function showPanelOverItem(itemEl) {
504
+ const rect = itemEl.getBoundingClientRect();
505
+
506
+ const panelW = 220;
507
+ const panelH = 36;
508
+ const top = Math.max(8, rect.top - panelH - 8);
509
+ const left = Math.max(
510
+ 8,
511
+ Math.min(window.innerWidth - panelW - 8, rect.left + (rect.width / 2) - (panelW / 2))
512
+ );
513
+
514
+ controlPanel.style.top = `${top}px`;
515
+ controlPanel.style.left = `${left}px`;
516
+ controlPanel.style.display = 'flex';
517
+
518
+ controlPanel.setAttribute('aria-hidden', 'false');
519
+ }
520
+
521
+ function getItemElementFromTarget(target) {
522
+ return target?.closest?.('[gh-array] [gh-item]') || target?.closest?.('[gh-array] .item');
523
+ }
524
+
525
+ function getAnyGhIdInsideItem(itemEl) {
526
+ return itemEl?.querySelector?.('[gh-id]') || null;
527
+ }
528
+
529
+ let animationFrameId = 0;
530
+ let lastHoveredItem = null;
531
+
532
+ function handlePointerMove(event) {
533
+ if (animationFrameId) return;
534
+
535
+ animationFrameId = requestAnimationFrame(async () => {
536
+ animationFrameId = 0;
537
+
538
+ if (controlPanel.contains(event.target)) {
539
+ cancelHidePanel();
540
+ return;
541
+ }
542
+
543
+ const hoveredItem = getItemElementFromTarget(event.target);
544
+
545
+ if (!hoveredItem) {
546
+ scheduleHidePanel();
547
+ return;
548
+ }
549
+
550
+ cancelHidePanel();
551
+
552
+ if (hoveredItem === lastHoveredItem) return;
553
+ lastHoveredItem = hoveredItem;
554
+
555
+ const anyGh = getAnyGhIdInsideItem(hoveredItem);
556
+ if (!anyGh) {
557
+ scheduleHidePanel();
558
+ return;
559
+ }
560
+
561
+ const parsedPath = parseGhId(anyGh.getAttribute('gh-id'));
562
+ if (!parsedPath) {
563
+ scheduleHidePanel();
564
+ return;
565
+ }
566
+
567
+ if (!ensureGudhubInitialized()) {
568
+ scheduleHidePanel();
569
+ return;
570
+ }
571
+
572
+ const loadedData = await loadJsonForElement(anyGh);
573
+ if (!loadedData) {
574
+ scheduleHidePanel();
575
+ return;
576
+ }
577
+
578
+ const targetArray = getValueByPath(loadedData.json, parsedPath.arrayPath);
579
+ if (!Array.isArray(targetArray)) {
580
+ scheduleHidePanel();
581
+ return;
582
+ }
583
+
584
+ currentHover = { element: anyGh, itemEl: hoveredItem, meta: parsedPath };
585
+
586
+ document.querySelectorAll('.gh-item-hovered').forEach(el => el.classList.remove('gh-item-hovered'));
587
+ hoveredItem.classList.add('gh-item-hovered');
588
+
589
+ showPanelOverItem(hoveredItem);
590
+ });
591
+ }
592
+
593
+
594
+ controlPanel.addEventListener('pointerdown', stopEvent, true);
595
+ controlPanel.addEventListener('pointerenter', () => {
596
+ cancelHidePanel();
597
+ }, true);
598
+
599
+ controlPanel.addEventListener('pointerleave', () => {
600
+ scheduleHidePanel();
601
+ }, true);
602
+
603
+
604
+ controlPanel.addEventListener('click', async event => {
605
+ stopEvent(event);
606
+
607
+ const button = event.target.closest('button[data-action]');
608
+ if (!button || !currentHover) return;
609
+
610
+ if (!ensureGudhubInitialized()) return;
611
+
612
+ const actionType = button.getAttribute('data-action');
613
+
614
+ const loadedData = await loadJsonForElement(currentHover.element);
615
+ if (!loadedData) return;
616
+
617
+ const { ids, elementId, json } = loadedData;
618
+
619
+ const container = currentHover.itemEl?.closest?.('[gh-array]') || currentHover.element.closest('[gh-array]');
620
+ if (container) {
621
+ syncDomToJsonForContainer(container, json);
622
+ }
623
+
624
+ const targetArray = getValueByPath(json, currentHover.meta.arrayPath);
625
+ if (!Array.isArray(targetArray)) return;
626
+
627
+ const itemIndex = currentHover.meta.itemIndex;
628
+
629
+ if (actionType === 'add') {
630
+ targetArray.splice(itemIndex + 1, 0, deepClone(targetArray[itemIndex] ?? {}));
631
+ }
632
+
633
+ if (actionType === 'remove') {
634
+ if (targetArray.length <= 1) return;
635
+ targetArray.splice(itemIndex, 1);
636
+ }
637
+
638
+ await gudhub.createDocument({
639
+ app_id: ids.appId,
640
+ item_id: ids.itemId,
641
+ element_id: elementId,
642
+ data: JSON.stringify(json),
643
+ });
644
+
645
+ cachedJson = json;
646
+
647
+ rerenderArrayContainerFromJson({
648
+ targetElement: currentHover.element,
649
+ json,
650
+ arrayPath: currentHover.meta.arrayPath,
651
+ });
652
+
653
+ lastHoveredItem = null;
654
+ hidePanel();
655
+ }, true);
656
+
657
+ root.addEventListener('pointermove', handlePointerMove, true);
658
+ window.addEventListener('scroll', scheduleHidePanel, true);
659
+ window.addEventListener('resize', scheduleHidePanel, true);
660
+
661
+ return () => {
662
+ root.removeEventListener('pointermove', handlePointerMove, true);
663
+ window.removeEventListener('scroll', scheduleHidePanel, true);
664
+ window.removeEventListener('resize', scheduleHidePanel, true);
665
+ hidePanel();
666
+ };
667
+
186
668
  }
187
669
  }
188
670
 
189
671
  renderNotificationsComponent() {
190
- if(!window.customElements.get('edit-mode-notifications')) {
672
+ if (!window.customElements.get('edit-mode-notifications')) {
191
673
  window.customElements.define('edit-mode-notifications', EditModeNotifications);
192
674
  }
193
675
 
194
- if(!document.querySelector('edit-mode-notifications')) {
676
+ if (!document.querySelector('edit-mode-notifications')) {
195
677
  document.body.append(document.createElement('edit-mode-notifications'));
196
678
  }
197
679
  }
198
680
 
199
681
  initListeners() {
200
682
  const listener = (e) => {
201
- this.initGudHub(e.detail.data.auth_key);
202
- window.localStorage.setItem('gudhub_auth_key', e.detail.data.auth_key);
203
- window.removeEventListener('gudhub-login', listener, false);
683
+ this.initGudHub(e.detail.data.auth_key);
684
+ window.localStorage.setItem('gudhub_auth_key', e.detail.data.auth_key);
685
+ window.removeEventListener('gudhub-login', listener, false);
204
686
  }
205
687
  window.addEventListener('gudhub-login', listener, false);
206
688
 
207
689
  const outsideClickListener = (e) => {
208
- if(e.target === document.querySelector('.gudhub-login-popup__bg')) {
690
+ if (e.target === document.querySelector('.gudhub-login-popup__bg')) {
209
691
  this.hideLoginPopup();
210
692
  window.removeEventListener('click', outsideClickListener, false);
211
693
  }
@@ -218,25 +700,23 @@ class EditMode extends GHComponent {
218
700
  let GudHub;
219
701
  try {
220
702
  GudHub = !isUndefined(GudHubLibrary) ? GudHubLibrary.GudHub : GudHub;
221
- } catch(err) {
703
+ } catch (err) {
222
704
  console.error('GHCOMPONENT ERROR: you need to import GudHub library (@gudhub/core) before using GHComponent!');
223
705
  }
224
706
 
225
- window.gudhub = !isUndefined(window.gudhub) ? window.gudhub : new GudHub(isUndefined(auth_key) ? window.getConfig().auth_key : auth_key, {
226
- server_url: 'https://gudhub.com/GudHub_Test',
227
- file_server_url: 'https://gudhub.com',
228
- async_modules_path: 'build/latest/async_modules/',
229
- });
707
+ window.gudhub = !isUndefined(window.gudhub)
708
+ ? window.gudhub
709
+ : new GudHub(isUndefined(auth_key) ? window.getConfig().auth_key : auth_key, {
710
+ server_url: 'https://app.gudhub.com/GudHub',
711
+ file_server_url: 'https://app.gudhub.com',
712
+ async_modules_path: 'build/latest/async_modules/',
713
+ });
230
714
 
231
715
  this.hideLoginPopup();
232
716
  }
233
717
 
234
- checkGudhub() {
235
- return !isUndefined(window.gudhub);
236
- }
237
-
238
- connectLoginSript() {
239
- if(!document.querySelector('script#gudhub_login_component')) {
718
+ connectLoginScript() {
719
+ if (!document.querySelector('script#gudhub_login_component')) {
240
720
  const script = document.createElement('script');
241
721
  script.id = 'gudhub-login-component';
242
722
  script.src = 'https://unpkg.com/@gudhub/gh-login/index.js';
@@ -246,7 +726,7 @@ class EditMode extends GHComponent {
246
726
  }
247
727
 
248
728
  createLoginPopup() {
249
- if(!document.querySelector('#gudhub_login_popup')) {
729
+ if (!document.querySelector('#gudhub_login_popup')) {
250
730
  const div = document.createElement('div');
251
731
  div.classList.add('gudhub-login-popup__bg');
252
732
  div.id = 'gudhub_login_popup';
@@ -262,20 +742,19 @@ class EditMode extends GHComponent {
262
742
  }
263
743
 
264
744
  showLoginPopup() {
265
- this.connectLoginSript();
745
+ this.connectLoginScript();
266
746
  this.createLoginPopup();
267
747
  document.querySelector('.gudhub-login-popup__bg').classList.add('active');
268
748
  this.initListeners();
269
749
  }
270
750
 
271
751
  hideLoginPopup() {
272
- if(document.querySelector('.gudhub-login-popup__bg')) {
752
+ if (document.querySelector('.gudhub-login-popup__bg')) {
273
753
  document.querySelector('.gudhub-login-popup__bg').classList.remove('active');
274
754
  }
275
755
  }
276
-
277
756
  }
278
757
 
279
- if(!window.customElements.get('edit-mode')) {
758
+ if (!window.customElements.get('edit-mode')) {
280
759
  window.customElements.define('edit-mode', EditMode);
281
- }
760
+ }
@@ -1,8 +1,22 @@
1
1
  body.edit-mode-enabled {
2
- [gh-id] {
3
- &:hover {
4
- outline: 1px solid #2b61fa;
5
- }
2
+ .gh-item-hovered {
3
+ outline: 2px solid rgba(43, 97, 250, 0.6);
4
+ outline-offset: 2px;
5
+ }
6
+
7
+ [gh-id]:hover {
8
+ outline: 1px dashed rgba(43, 97, 250, 0.5);
9
+ cursor: text;
10
+ }
11
+
12
+ [gh-id].gh-id-editing {
13
+ outline: 2px solid #2b61fa;
14
+ outline-offset: 2px;
15
+ background: rgba(43, 97, 250, 0.05);
16
+ }
17
+
18
+ .gh-item-hovered:has(.gh-id-editing) {
19
+ outline-color: transparent;
6
20
  }
7
21
  }
8
22
 
@@ -75,3 +89,64 @@ edit-mode {
75
89
  color: #fff;
76
90
  }
77
91
  }
92
+
93
+ #gh-array-panel {
94
+ position: fixed;
95
+ z-index: 999;
96
+ display: none;
97
+ gap: 6px;
98
+ padding: 10px 12px;
99
+ border-radius: 10px;
100
+ background: rgba(255, 255, 255, 0.92);
101
+ color: black;
102
+ border: 1px solid lightgrey;
103
+ box-shadow: 0 10px 30px rgba(0,0,0,.25);
104
+ user-select: none;
105
+ }
106
+
107
+ .gh-panel-btn {
108
+ all: unset;
109
+ cursor: pointer;
110
+
111
+ padding: 6px 12px;
112
+ border-radius: 8px;
113
+ border: 1px solid rgba(0,0,0,0.12);
114
+ background: rgba(255,255,255,0.7);
115
+
116
+ font: 14px system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
117
+ color: #000;
118
+
119
+ transition:
120
+ transform 0.3s cubic-bezier(0.22, 1, 0.36, 1),
121
+ box-shadow 0.3s cubic-bezier(0.22, 1, 0.36, 1),
122
+ background-color 0.3s ease-in-out,
123
+ opacity 0.3s ease-in-out;
124
+
125
+ box-shadow:
126
+ 0 1px 1px rgba(0,0,0,0.04),
127
+ 0 4px 10px rgba(0,0,0,0.08);
128
+
129
+ will-change: transform;
130
+ }
131
+
132
+ .gh-panel-btn:hover {
133
+ transform: translateY(-1px) scale(1.001);
134
+ background: rgba(255,255,255,0.9);
135
+ box-shadow:
136
+ 0 4px 10px rgba(0,0,0,0.12),
137
+ 0 8px 18px rgba(0,0,0,0.16);
138
+ }
139
+
140
+ .gh-panel-btn:active {
141
+ transform: translateY(0) scale(0.97);
142
+ box-shadow:
143
+ 0 2px 6px rgba(0,0,0,0.18);
144
+ opacity: 0.9;
145
+ }
146
+
147
+ .gh-panel-btn:focus-visible {
148
+ outline: none;
149
+ box-shadow:
150
+ 0 0 0 3px rgba(0, 125, 250, 0.35),
151
+ 0 6px 16px rgba(0,0,0,0.15);
152
+ }
@@ -57,7 +57,7 @@ class ImageComponent extends GHComponent {
57
57
  const buildImagePath = (meta, route) => {
58
58
  const url = new URL(meta.url);
59
59
  const extension = url.pathname.split('.').pop();
60
- return `/assets/images${route}/${meta.file_name}.${extension}`;
60
+ return `/assets/images${route}${meta.file_name}.${extension}`;
61
61
  };
62
62
 
63
63
  try {
@@ -75,13 +75,16 @@ class ImageComponent extends GHComponent {
75
75
  }
76
76
  }
77
77
 
78
- this.generatedImageSrc = relativeImagePath;
78
+ const normalizeUrlPath = (value) =>
79
+ value ? value.trim().replace(/\s+/g, '-') : value;
80
+
81
+ this.generatedImageSrc = normalizeUrlPath(relativeImagePath);
79
82
 
80
83
  // Download image from GudHub (this.dataUrl) to cache (this.generatedImageSrc)
81
84
  if (window?.imagesRegeneration) {
82
85
  if (this.generatedImageSrc && this.dataUrl) {
83
86
  try {
84
- await fetch(`${this.generatedImageSrc}?source=${this.dataUrl}&mode=ssr`);
87
+ await fetch(`${this.generatedImageSrc}?source=${normalizeUrlPath(this.dataUrl)}&mode=ssr`);
85
88
  this.src = this.generatedImageSrc;
86
89
  } catch (error) {
87
90
  console.error('Failed to fetch generatedImageSrc:', error);