Skip to content

Commit 4b7377a

Browse files
author
Konrad Michalik
authored
Merge pull request #25 from xima-media/refactor-js
refactor: enhance frontend edit script with improved data handling
2 parents 1680a76 + d522f48 commit 4b7377a

1 file changed

Lines changed: 208 additions & 138 deletions

File tree

Resources/Public/JavaScript/frontend_edit.js

Lines changed: 208 additions & 138 deletions
Original file line numberDiff line numberDiff line change
@@ -1,162 +1,232 @@
11
document.addEventListener('DOMContentLoaded', function () {
2-
const getContentElements = async () => {
3-
let dataItems = {};
2+
/**
3+
* Finds the closest parent element with an ID matching the pattern "c\d+".
4+
* @param {HTMLElement} element - The starting element.
5+
* @returns {HTMLElement|null} - The closest matching element or null if not found.
6+
*/
7+
const getClosestElementWithId = (element) => {
8+
while (element && !element.id.match(/c\d+/)) {
9+
element = element.parentElement;
10+
}
11+
return element;
12+
};
413

5-
document.querySelectorAll('.xima-typo3-frontend-edit--data').forEach(function (element) {
14+
/**
15+
* Collects data items from elements with the class "xima-typo3-frontend-edit--data".
16+
* Groups the data by the closest element's ID.
17+
* @returns {Object} - A dictionary of data items grouped by ID.
18+
*/
19+
const collectDataItems = () => {
20+
const dataItems = {};
21+
document.querySelectorAll('.xima-typo3-frontend-edit--data').forEach((element) => {
622
const data = element.value;
7-
let closestElement = element;
8-
while (closestElement && !closestElement.id.match(/c\d+/)) {
9-
closestElement = closestElement.parentElement;
10-
}
23+
const closestElement = getClosestElementWithId(element);
1124

1225
if (closestElement) {
1326
const id = closestElement.id.replace('c', '');
14-
if (!(id in dataItems)) {
27+
if (!dataItems[id]) {
1528
dataItems[id] = [];
1629
}
17-
1830
dataItems[id].push(JSON.parse(data));
1931
}
2032
});
33+
return dataItems;
34+
};
2135

36+
/**
37+
* Sends a POST request to fetch content elements based on the provided data items.
38+
* @param {Object} dataItems - The data items to send in the request body.
39+
* @returns {Promise<Object>} - The JSON response from the server.
40+
* @throws {Error} - If the request fails.
41+
*/
42+
const fetchContentElements = async (dataItems) => {
2243
const url = new URL(window.location.href);
2344
url.searchParams.set('type', '1729341864');
2445

25-
try {
26-
const response = await fetch(url.toString(), {
27-
cache: 'no-cache',
28-
method: 'POST',
29-
headers: {
30-
"Content-Type": "application/json",
31-
},
32-
body: JSON.stringify(dataItems),
46+
const response = await fetch(url.toString(), {
47+
cache: 'no-cache',
48+
method: 'POST',
49+
headers: {"Content-Type": "application/json"},
50+
body: JSON.stringify(dataItems),
51+
});
52+
53+
if (!response.ok) throw new Error('Failed to fetch content elements');
54+
return response.json();
55+
};
56+
57+
/**
58+
* Creates an edit button for a content element.
59+
* @param {string} uid - The unique ID of the content element.
60+
* @param {Object} contentElement - The content element data.
61+
* @returns {HTMLButtonElement} - The created edit button.
62+
*/
63+
const createEditButton = (uid, contentElement) => {
64+
const editButton = document.createElement('button');
65+
editButton.className = 'xima-typo3-frontend-edit--edit-button';
66+
editButton.title = contentElement.menu.label;
67+
editButton.innerHTML = contentElement.menu.icon;
68+
editButton.setAttribute('data-cid', uid);
69+
return editButton;
70+
};
71+
72+
/**
73+
* Creates a dropdown menu for a content element.
74+
* @param {string} uid - The unique ID of the content element.
75+
* @param {Object} contentElement - The content element data.
76+
* @returns {HTMLDivElement} - The created dropdown menu.
77+
*/
78+
const createDropdownMenu = (uid, contentElement) => {
79+
const dropdownMenu = document.createElement('div');
80+
dropdownMenu.className = 'xima-typo3-frontend-edit--dropdown-menu';
81+
dropdownMenu.setAttribute('data-cid', uid);
82+
83+
const dropdownMenuInner = document.createElement('div');
84+
dropdownMenuInner.className = 'xima-typo3-frontend-edit--dropdown-menu-inner';
85+
dropdownMenu.appendChild(dropdownMenuInner);
86+
87+
for (let actionName in contentElement.menu.children) {
88+
const action = contentElement.menu.children[actionName];
89+
const actionElement = document.createElement(action.type === 'link' ? 'a' : 'div');
90+
if (action.type === 'link') actionElement.href = action.url;
91+
if (action.type === 'divider') actionElement.className = 'xima-typo3-frontend-edit--divider';
92+
93+
actionElement.classList.add(actionName);
94+
actionElement.innerHTML = `${action.icon ?? ''} <span>${action.label}</span>`;
95+
dropdownMenuInner.appendChild(actionElement);
96+
}
97+
98+
return dropdownMenu;
99+
};
100+
101+
/**
102+
* Positions the edit button and dropdown menu relative to the target element.
103+
* @param {HTMLElement} element - The target element.
104+
* @param {HTMLElement} wrapperElement - The wrapper element containing the button and menu.
105+
* @param {HTMLElement} editButton - The edit button.
106+
* @param {HTMLElement} dropdownMenu - The dropdown menu.
107+
*/
108+
const positionElements = (element, wrapperElement, editButton, dropdownMenu) => {
109+
const rect = element.getBoundingClientRect();
110+
const rectInPageContext = {
111+
top: rect.top + document.documentElement.scrollTop,
112+
left: rect.left + document.documentElement.scrollLeft,
113+
width: rect.width,
114+
height: rect.height,
115+
};
116+
117+
let defaultEditButtonMargin = 10;
118+
// if the element is too small, adjust the position of the edit button
119+
if (rect.height < 50) {
120+
defaultEditButtonMargin = (rect.height - 30) / 2;
121+
}
122+
123+
// if the dropdown menu is too close to the bottom of the page, move it to the top
124+
// currently it's not possible to fetch the height of the dropdown menu before it's visible once, so we have to use a fixed value
125+
if (document.documentElement.scrollHeight - rectInPageContext.top - rect.height < 500 &&
126+
rect.height < 700 &&
127+
rectInPageContext.top > 500
128+
) {
129+
dropdownMenu.style.bottom = `19px`;
130+
} else {
131+
dropdownMenu.style.top = `${defaultEditButtonMargin + 30}px`;
132+
}
133+
134+
wrapperElement.style.top = `${rect.top + document.documentElement.scrollTop}px`;
135+
wrapperElement.style.left = `${rect.right - 30}px`;
136+
editButton.style.top = `${defaultEditButtonMargin}px`;
137+
editButton.style.left = `-10px`;
138+
editButton.style.display = 'flex';
139+
};
140+
141+
/**
142+
* Sets up hover events for the target element, edit button, and dropdown menu.
143+
* @param {HTMLElement} element - The target element.
144+
* @param {HTMLElement} wrapperElement - The wrapper element containing the button and menu.
145+
* @param {HTMLElement} editButton - The edit button.
146+
* @param {HTMLElement} dropdownMenu - The dropdown menu.
147+
*/
148+
const setupHoverEvents = (element, wrapperElement, editButton, dropdownMenu) => {
149+
element.addEventListener('mouseover', () => {
150+
positionElements(element, wrapperElement, editButton, dropdownMenu);
151+
element.classList.add('xima-typo3-frontend-edit--edit-container');
152+
});
153+
154+
element.addEventListener('mouseout', (event) => {
155+
if (event.relatedTarget === editButton || event.relatedTarget === dropdownMenu) return;
156+
editButton.style.display = 'none';
157+
dropdownMenu.style.display = 'none';
158+
element.classList.remove('xima-typo3-frontend-edit--edit-container');
159+
});
160+
};
161+
162+
/**
163+
* Sets up events for dropdown menus to handle mouse leave and click outside.
164+
*/
165+
const setupDropdownMenuEvents = () => {
166+
document.querySelectorAll('.xima-typo3-frontend-edit--dropdown-menu').forEach((menu) => {
167+
menu.addEventListener('mouseleave', (event) => {
168+
const cid = menu.getAttribute('data-cid');
169+
menu.style.display = 'none';
170+
document.querySelector(`.xima-typo3-frontend-edit--edit-button[data-cid="${cid}"]`).style.display = 'none';
171+
document.querySelector(`#c${cid}`).classList.remove('xima-typo3-frontend-edit--edit-container');
33172
});
34-
if (!response.ok) return;
35-
36-
const jsonResponse = await response.json();
37-
38-
for (let uid in jsonResponse) {
39-
const contentElement = jsonResponse[uid];
40-
let element = document.querySelector(`#c${uid}`);
41-
if (!element) {
42-
// check for translated element
43-
if (contentElement.element.l10n_source) {
44-
element = document.querySelector(`#c${contentElement.element.l10n_source}`);
45-
if (element) {
46-
uid = contentElement.element.l10n_source;
47-
} else {
48-
continue;
49-
}
50-
} else {
51-
continue;
52-
}
53-
}
173+
});
54174

55-
const editButton = document.createElement('button');
56-
editButton.className = 'xima-typo3-frontend-edit--edit-button';
57-
editButton.title = contentElement.menu.label;
58-
editButton.innerHTML = contentElement.menu.icon;
59-
editButton.setAttribute('data-cid', uid);
60-
61-
const dropdownMenu = document.createElement('div');
62-
dropdownMenu.className = 'xima-typo3-frontend-edit--dropdown-menu';
63-
dropdownMenu.setAttribute('data-cid', uid);
64-
65-
const dropdownMenuInner = document.createElement('div');
66-
dropdownMenuInner.className = 'xima-typo3-frontend-edit--dropdown-menu-inner';
67-
dropdownMenu.appendChild(dropdownMenuInner);
68-
69-
for (let actionName in contentElement.menu.children) {
70-
const action = contentElement.menu.children[actionName];
71-
let actionElement;
72-
73-
if (action.type === 'link') {
74-
actionElement = document.createElement('a');
75-
actionElement.href = action.url;
76-
} else if (action.type === 'divider') {
77-
actionElement = document.createElement('div');
78-
actionElement.className = 'xima-typo3-frontend-edit--divider';
79-
} else {
80-
actionElement = document.createElement('div');
81-
}
82-
83-
actionElement.classList.add(actionName);
84-
actionElement.innerHTML = `${action.icon ?? ''} <span>${action.label}</span>`;
85-
dropdownMenuInner.appendChild(actionElement);
175+
document.addEventListener('click', (event) => {
176+
document.querySelectorAll('.xima-typo3-frontend-edit--dropdown-menu').forEach((menu) => {
177+
const button = menu.previousElementSibling;
178+
if (!menu.contains(event.target) && !button.contains(event.target)) {
179+
menu.style.display = 'none';
86180
}
181+
});
182+
});
183+
};
87184

88-
editButton.addEventListener('click', function (event) {
89-
event.preventDefault();
90-
dropdownMenu.style.display = dropdownMenu.style.display === 'block' ? 'visible' : 'block';
91-
});
92-
93-
94-
let wrapperElement = document.createElement('div');
95-
wrapperElement.className = 'xima-typo3-frontend-edit--wrapper';
96-
wrapperElement.appendChild(editButton);
97-
wrapperElement.appendChild(dropdownMenu);
98-
document.body.appendChild(wrapperElement);
99-
100-
element.addEventListener('mouseover', function () {
101-
let rect = element.getBoundingClientRect();
102-
let rectInPageContext = {
103-
top: rect.top + document.documentElement.scrollTop,
104-
left: rect.left + document.documentElement.scrollLeft,
105-
width: rect.width,
106-
height: rect.height
107-
};
108-
109-
110-
let defaultEditButtonMargin = 10;
111-
let defaultEditButtonHeight = 30;
112-
// if the element is too small, adjust the position of the edit button
113-
if (rect.height < 50) {
114-
defaultEditButtonMargin = (rect.height - defaultEditButtonHeight) / 2;
115-
}
116-
117-
// if the dropdown menu is too close to the bottom of the page, move it to the top
118-
// currently it's not possible to fetch the height of the dropdown menu before it's visible once, so we have to use a fixed value
119-
if (document.documentElement.scrollHeight - rectInPageContext.top - rect.height < 500 && rect.height < 700) {
120-
dropdownMenu.style.bottom = `19px`;
121-
} else {
122-
dropdownMenu.style.top = `${defaultEditButtonMargin + 30}px`;
123-
}
124-
125-
wrapperElement.style.top = `${rect.top + document.documentElement.scrollTop}px`;
126-
wrapperElement.style.left = `${rect.right - 30}px`;
127-
editButton.style.top = `${defaultEditButtonMargin}px`;
128-
editButton.style.left = `-10px`;
129-
editButton.style.display = 'flex';
130-
element.classList.add('xima-typo3-frontend-edit--edit-container');
131-
});
132-
133-
element.addEventListener('mouseout', function (event) {
134-
if (event.relatedTarget === editButton || event.relatedTarget === dropdownMenu) return;
135-
editButton.style.display = 'none';
136-
dropdownMenu.style.display = 'none';
137-
element.classList.remove('xima-typo3-frontend-edit--edit-container');
138-
});
139-
140-
document.querySelectorAll('.xima-typo3-frontend-edit--dropdown-menu').forEach(function (menu) {
141-
menu.addEventListener('mouseleave', function (event) {
142-
const cid = menu.getAttribute('data-cid');
143-
menu.style.display = 'none';
144-
document.querySelector(`.xima-typo3-frontend-edit--edit-button[data-cid="${cid}"]`).style.display = 'none';
145-
document.querySelector(`#c${cid}`).classList.remove('xima-typo3-frontend-edit--edit-container');
146-
});
147-
});
148-
149-
document.addEventListener('click', function (event) {
150-
document.querySelectorAll('.xima-typo3-frontend-edit--dropdown-menu').forEach(function (menu) {
151-
const button = menu.previousElementSibling;
152-
if (!menu.contains(event.target) && !button.contains(event.target)) {
153-
menu.style.display = 'none';
154-
}
155-
});
156-
});
185+
/**
186+
* Renders content elements by creating edit buttons and dropdown menus for each.
187+
* @param {Object} jsonResponse - The JSON response containing content element data.
188+
*/
189+
const renderContentElements = (jsonResponse) => {
190+
for (let uid in jsonResponse) {
191+
const contentElement = jsonResponse[uid];
192+
let element = document.querySelector(`#c${uid}`);
193+
194+
if (contentElement.element.l10n_source) {
195+
element = document.querySelector(`#c${contentElement.element.l10n_source}`);
196+
if (!element) continue;
197+
uid = contentElement.element.l10n_source;
157198
}
199+
200+
const editButton = createEditButton(uid, contentElement);
201+
const dropdownMenu = createDropdownMenu(uid, contentElement);
202+
203+
editButton.addEventListener('click', (event) => {
204+
event.preventDefault();
205+
dropdownMenu.style.display = dropdownMenu.style.display === 'block' ? 'visible' : 'block';
206+
});
207+
208+
const wrapperElement = document.createElement('div');
209+
wrapperElement.className = 'xima-typo3-frontend-edit--wrapper';
210+
wrapperElement.appendChild(editButton);
211+
wrapperElement.appendChild(dropdownMenu);
212+
document.body.appendChild(wrapperElement);
213+
214+
setupHoverEvents(element, wrapperElement, editButton, dropdownMenu);
215+
}
216+
};
217+
218+
/**
219+
* Main function to collect data, fetch content elements, and render them.
220+
* Handles errors during the process.
221+
*/
222+
const getContentElements = async () => {
223+
try {
224+
const dataItems = collectDataItems();
225+
const jsonResponse = await fetchContentElements(dataItems);
226+
renderContentElements(jsonResponse);
227+
setupDropdownMenuEvents();
158228
} catch (error) {
159-
console.log(error);
229+
console.error(error);
160230
}
161231
};
162232

0 commit comments

Comments
 (0)