|
1 | 1 | 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 | + }; |
4 | 13 |
|
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) => { |
6 | 22 | 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); |
11 | 24 |
|
12 | 25 | if (closestElement) { |
13 | 26 | const id = closestElement.id.replace('c', ''); |
14 | | - if (!(id in dataItems)) { |
| 27 | + if (!dataItems[id]) { |
15 | 28 | dataItems[id] = []; |
16 | 29 | } |
17 | | - |
18 | 30 | dataItems[id].push(JSON.parse(data)); |
19 | 31 | } |
20 | 32 | }); |
| 33 | + return dataItems; |
| 34 | + }; |
21 | 35 |
|
| 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) => { |
22 | 43 | const url = new URL(window.location.href); |
23 | 44 | url.searchParams.set('type', '1729341864'); |
24 | 45 |
|
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'); |
33 | 172 | }); |
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 | + }); |
54 | 174 |
|
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'; |
86 | 180 | } |
| 181 | + }); |
| 182 | + }); |
| 183 | + }; |
87 | 184 |
|
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; |
157 | 198 | } |
| 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(); |
158 | 228 | } catch (error) { |
159 | | - console.log(error); |
| 229 | + console.error(error); |
160 | 230 | } |
161 | 231 | }; |
162 | 232 |
|
|
0 commit comments