@strapi/content-manager 5.49.0 → 5.50.1

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 (381) hide show
  1. package/dist/admin/components/ComponentIcon.js.map +1 -1
  2. package/dist/admin/components/ComponentIcon.mjs.map +1 -1
  3. package/dist/admin/components/ConfigurationForm/Fields.js.map +1 -1
  4. package/dist/admin/components/ConfigurationForm/Fields.mjs.map +1 -1
  5. package/dist/admin/components/DragLayer.js.map +1 -1
  6. package/dist/admin/components/DragLayer.mjs.map +1 -1
  7. package/dist/admin/components/LeftMenu.js +66 -67
  8. package/dist/admin/components/LeftMenu.js.map +1 -1
  9. package/dist/admin/components/LeftMenu.mjs +68 -69
  10. package/dist/admin/components/LeftMenu.mjs.map +1 -1
  11. package/dist/admin/history/components/VersionInputRenderer.js +3 -3
  12. package/dist/admin/history/components/VersionInputRenderer.js.map +1 -1
  13. package/dist/admin/history/components/VersionInputRenderer.mjs +3 -3
  14. package/dist/admin/history/components/VersionInputRenderer.mjs.map +1 -1
  15. package/dist/admin/history/routes.js.map +1 -1
  16. package/dist/admin/history/routes.mjs.map +1 -1
  17. package/dist/admin/hooks/useDocumentActions.js +15 -8
  18. package/dist/admin/hooks/useDocumentActions.js.map +1 -1
  19. package/dist/admin/hooks/useDocumentActions.mjs +15 -8
  20. package/dist/admin/hooks/useDocumentActions.mjs.map +1 -1
  21. package/dist/admin/hooks/useDocumentContext.js.map +1 -1
  22. package/dist/admin/hooks/useDocumentContext.mjs.map +1 -1
  23. package/dist/admin/index.js.map +1 -1
  24. package/dist/admin/index.mjs.map +1 -1
  25. package/dist/admin/layout.js +21 -7
  26. package/dist/admin/layout.js.map +1 -1
  27. package/dist/admin/layout.mjs +23 -9
  28. package/dist/admin/layout.mjs.map +1 -1
  29. package/dist/admin/pages/ComponentConfigurationPage.js.map +1 -1
  30. package/dist/admin/pages/ComponentConfigurationPage.mjs.map +1 -1
  31. package/dist/admin/pages/EditView/components/DocumentActions.js +168 -78
  32. package/dist/admin/pages/EditView/components/DocumentActions.js.map +1 -1
  33. package/dist/admin/pages/EditView/components/DocumentActions.mjs +168 -79
  34. package/dist/admin/pages/EditView/components/DocumentActions.mjs.map +1 -1
  35. package/dist/admin/pages/EditView/components/FormInputs/BlocksInput/Blocks/List.js +6 -3
  36. package/dist/admin/pages/EditView/components/FormInputs/BlocksInput/Blocks/List.js.map +1 -1
  37. package/dist/admin/pages/EditView/components/FormInputs/BlocksInput/Blocks/List.mjs +6 -3
  38. package/dist/admin/pages/EditView/components/FormInputs/BlocksInput/Blocks/List.mjs.map +1 -1
  39. package/dist/admin/pages/EditView/components/FormInputs/BlocksInput/BlocksInput.js +1 -1
  40. package/dist/admin/pages/EditView/components/FormInputs/BlocksInput/BlocksInput.js.map +1 -1
  41. package/dist/admin/pages/EditView/components/FormInputs/BlocksInput/BlocksInput.mjs +1 -1
  42. package/dist/admin/pages/EditView/components/FormInputs/BlocksInput/BlocksInput.mjs.map +1 -1
  43. package/dist/admin/pages/EditView/components/FormInputs/BlocksInput/BlocksToolbar.js.map +1 -1
  44. package/dist/admin/pages/EditView/components/FormInputs/BlocksInput/BlocksToolbar.mjs.map +1 -1
  45. package/dist/admin/pages/EditView/components/FormInputs/Component/Initializer.js.map +1 -1
  46. package/dist/admin/pages/EditView/components/FormInputs/Component/Initializer.mjs.map +1 -1
  47. package/dist/admin/pages/EditView/components/FormInputs/Component/Input.js +1 -1
  48. package/dist/admin/pages/EditView/components/FormInputs/Component/Input.js.map +1 -1
  49. package/dist/admin/pages/EditView/components/FormInputs/Component/Input.mjs +1 -1
  50. package/dist/admin/pages/EditView/components/FormInputs/Component/Input.mjs.map +1 -1
  51. package/dist/admin/pages/EditView/components/FormInputs/Component/Repeatable.js +2 -1
  52. package/dist/admin/pages/EditView/components/FormInputs/Component/Repeatable.js.map +1 -1
  53. package/dist/admin/pages/EditView/components/FormInputs/Component/Repeatable.mjs +2 -1
  54. package/dist/admin/pages/EditView/components/FormInputs/Component/Repeatable.mjs.map +1 -1
  55. package/dist/admin/pages/EditView/components/FormInputs/DynamicZone/ComponentCategory.js +6 -1
  56. package/dist/admin/pages/EditView/components/FormInputs/DynamicZone/ComponentCategory.js.map +1 -1
  57. package/dist/admin/pages/EditView/components/FormInputs/DynamicZone/ComponentCategory.mjs +2 -1
  58. package/dist/admin/pages/EditView/components/FormInputs/DynamicZone/ComponentCategory.mjs.map +1 -1
  59. package/dist/admin/pages/EditView/components/FormInputs/DynamicZone/DynamicComponent.js +6 -2
  60. package/dist/admin/pages/EditView/components/FormInputs/DynamicZone/DynamicComponent.js.map +1 -1
  61. package/dist/admin/pages/EditView/components/FormInputs/DynamicZone/DynamicComponent.mjs +3 -2
  62. package/dist/admin/pages/EditView/components/FormInputs/DynamicZone/DynamicComponent.mjs.map +1 -1
  63. package/dist/admin/pages/EditView/components/FormInputs/DynamicZone/Field.js +6 -1
  64. package/dist/admin/pages/EditView/components/FormInputs/DynamicZone/Field.js.map +1 -1
  65. package/dist/admin/pages/EditView/components/FormInputs/DynamicZone/Field.mjs +6 -1
  66. package/dist/admin/pages/EditView/components/FormInputs/DynamicZone/Field.mjs.map +1 -1
  67. package/dist/admin/pages/EditView/components/FormInputs/Relations/Relations.js +8 -5
  68. package/dist/admin/pages/EditView/components/FormInputs/Relations/Relations.js.map +1 -1
  69. package/dist/admin/pages/EditView/components/FormInputs/Relations/Relations.mjs +8 -5
  70. package/dist/admin/pages/EditView/components/FormInputs/Relations/Relations.mjs.map +1 -1
  71. package/dist/admin/pages/EditView/components/FormInputs/UID.js +5 -4
  72. package/dist/admin/pages/EditView/components/FormInputs/UID.js.map +1 -1
  73. package/dist/admin/pages/EditView/components/FormInputs/UID.mjs +5 -4
  74. package/dist/admin/pages/EditView/components/FormInputs/UID.mjs.map +1 -1
  75. package/dist/admin/pages/EditView/components/FormInputs/Wysiwyg/Editor.js.map +1 -1
  76. package/dist/admin/pages/EditView/components/FormInputs/Wysiwyg/Editor.mjs.map +1 -1
  77. package/dist/admin/pages/EditView/components/FormInputs/Wysiwyg/Field.js +2 -2
  78. package/dist/admin/pages/EditView/components/FormInputs/Wysiwyg/Field.js.map +1 -1
  79. package/dist/admin/pages/EditView/components/FormInputs/Wysiwyg/Field.mjs +2 -2
  80. package/dist/admin/pages/EditView/components/FormInputs/Wysiwyg/Field.mjs.map +1 -1
  81. package/dist/admin/pages/EditView/components/FormInputs/Wysiwyg/utils/utils.js.map +1 -1
  82. package/dist/admin/pages/EditView/components/FormInputs/Wysiwyg/utils/utils.mjs.map +1 -1
  83. package/dist/admin/pages/EditView/components/Panels.js.map +1 -1
  84. package/dist/admin/pages/EditView/components/Panels.mjs.map +1 -1
  85. package/dist/admin/pages/EditView/utils/data.js.map +1 -1
  86. package/dist/admin/pages/EditView/utils/data.mjs.map +1 -1
  87. package/dist/admin/pages/EditView/utils/draftRelationCounts.js +117 -0
  88. package/dist/admin/pages/EditView/utils/draftRelationCounts.js.map +1 -0
  89. package/dist/admin/pages/EditView/utils/draftRelationCounts.mjs +109 -0
  90. package/dist/admin/pages/EditView/utils/draftRelationCounts.mjs.map +1 -0
  91. package/dist/admin/pages/ListConfiguration/components/Settings.js.map +1 -1
  92. package/dist/admin/pages/ListConfiguration/components/Settings.mjs.map +1 -1
  93. package/dist/admin/pages/ListConfiguration/components/SortDisplayedFields.js.map +1 -1
  94. package/dist/admin/pages/ListConfiguration/components/SortDisplayedFields.mjs.map +1 -1
  95. package/dist/admin/pages/ListView/ListViewPage.js +71 -11
  96. package/dist/admin/pages/ListView/ListViewPage.js.map +1 -1
  97. package/dist/admin/pages/ListView/ListViewPage.mjs +73 -13
  98. package/dist/admin/pages/ListView/ListViewPage.mjs.map +1 -1
  99. package/dist/admin/pages/ListView/components/BulkActions/Actions.js +2 -1
  100. package/dist/admin/pages/ListView/components/BulkActions/Actions.js.map +1 -1
  101. package/dist/admin/pages/ListView/components/BulkActions/Actions.mjs +2 -1
  102. package/dist/admin/pages/ListView/components/BulkActions/Actions.mjs.map +1 -1
  103. package/dist/admin/pages/ListView/components/BulkActions/ConfirmBulkActionDialog.js +1 -1
  104. package/dist/admin/pages/ListView/components/BulkActions/ConfirmBulkActionDialog.js.map +1 -1
  105. package/dist/admin/pages/ListView/components/BulkActions/ConfirmBulkActionDialog.mjs +1 -1
  106. package/dist/admin/pages/ListView/components/BulkActions/ConfirmBulkActionDialog.mjs.map +1 -1
  107. package/dist/admin/pages/ListView/components/BulkActions/PublishAction.js.map +1 -1
  108. package/dist/admin/pages/ListView/components/BulkActions/PublishAction.mjs.map +1 -1
  109. package/dist/admin/pages/ListView/components/Filters.js +5 -3
  110. package/dist/admin/pages/ListView/components/Filters.js.map +1 -1
  111. package/dist/admin/pages/ListView/components/Filters.mjs +5 -3
  112. package/dist/admin/pages/ListView/components/Filters.mjs.map +1 -1
  113. package/dist/admin/pages/ListView/components/TableActions.js +37 -0
  114. package/dist/admin/pages/ListView/components/TableActions.js.map +1 -1
  115. package/dist/admin/pages/ListView/components/TableActions.mjs +39 -2
  116. package/dist/admin/pages/ListView/components/TableActions.mjs.map +1 -1
  117. package/dist/admin/pages/ListView/components/TableCells/CellContent.js +28 -3
  118. package/dist/admin/pages/ListView/components/TableCells/CellContent.js.map +1 -1
  119. package/dist/admin/pages/ListView/components/TableCells/CellContent.mjs +28 -4
  120. package/dist/admin/pages/ListView/components/TableCells/CellContent.mjs.map +1 -1
  121. package/dist/admin/pages/ListView/components/TableCells/CellValue.js +8 -7
  122. package/dist/admin/pages/ListView/components/TableCells/CellValue.js.map +1 -1
  123. package/dist/admin/pages/ListView/components/TableCells/CellValue.mjs +8 -7
  124. package/dist/admin/pages/ListView/components/TableCells/CellValue.mjs.map +1 -1
  125. package/dist/admin/pages/ListView/components/TableCells/Components.js.map +1 -1
  126. package/dist/admin/pages/ListView/components/TableCells/Components.mjs.map +1 -1
  127. package/dist/admin/pages/ListView/components/TableCells/Relations.js +1 -1
  128. package/dist/admin/pages/ListView/components/TableCells/Relations.js.map +1 -1
  129. package/dist/admin/pages/ListView/components/TableCells/Relations.mjs +1 -1
  130. package/dist/admin/pages/ListView/components/TableCells/Relations.mjs.map +1 -1
  131. package/dist/admin/preview/pages/Preview.js +40 -11
  132. package/dist/admin/preview/pages/Preview.js.map +1 -1
  133. package/dist/admin/preview/pages/Preview.mjs +41 -12
  134. package/dist/admin/preview/pages/Preview.mjs.map +1 -1
  135. package/dist/admin/preview/routes.js.map +1 -1
  136. package/dist/admin/preview/routes.mjs.map +1 -1
  137. package/dist/admin/preview/utils/constants.js +9 -15
  138. package/dist/admin/preview/utils/constants.js.map +1 -1
  139. package/dist/admin/preview/utils/constants.mjs +10 -15
  140. package/dist/admin/preview/utils/constants.mjs.map +1 -1
  141. package/dist/admin/preview/utils/fieldUtils.js.map +1 -1
  142. package/dist/admin/preview/utils/fieldUtils.mjs.map +1 -1
  143. package/dist/admin/router.js.map +1 -1
  144. package/dist/admin/router.mjs.map +1 -1
  145. package/dist/admin/services/documents.js.map +1 -1
  146. package/dist/admin/services/documents.mjs.map +1 -1
  147. package/dist/admin/src/components/ComponentIcon.d.ts +4 -3
  148. package/dist/admin/src/components/DragLayer.d.ts +1 -1
  149. package/dist/admin/src/components/InjectionZone.d.ts +1 -2
  150. package/dist/admin/src/content-manager.d.ts +15 -15
  151. package/dist/admin/src/history/routes.d.ts +2 -2
  152. package/dist/admin/src/history/services/historyVersion.d.ts +3 -3
  153. package/dist/admin/src/hooks/useContentTypeSchema.d.ts +1 -1
  154. package/dist/admin/src/hooks/useDocument.d.ts +8 -9
  155. package/dist/admin/src/hooks/useDocumentActions.d.ts +17 -16
  156. package/dist/admin/src/hooks/useDocumentContext.d.ts +1 -1
  157. package/dist/admin/src/hooks/useDocumentLayout.d.ts +3 -3
  158. package/dist/admin/src/hooks/useDragAndDrop.d.ts +4 -4
  159. package/dist/admin/src/hooks/useKeyboardDragAndDrop.d.ts +1 -1
  160. package/dist/admin/src/hooks/usePersistentQueryParams.d.ts +0 -1
  161. package/dist/admin/src/index.d.ts +1 -1
  162. package/dist/admin/src/modules/app.d.ts +5 -5
  163. package/dist/admin/src/pages/EditView/EditViewPage.d.ts +1 -1
  164. package/dist/admin/src/pages/EditView/components/DocumentActions.d.ts +12 -1
  165. package/dist/admin/src/pages/EditView/components/EditorToolbarObserver.d.ts +1 -1
  166. package/dist/admin/src/pages/EditView/components/FormInputs/BlocksInput/BlocksEditor.d.ts +1 -1
  167. package/dist/admin/src/pages/EditView/components/FormInputs/BlocksInput/utils/conversions.d.ts +2 -2
  168. package/dist/admin/src/pages/EditView/components/FormInputs/ComponentContext.d.ts +1 -2
  169. package/dist/admin/src/pages/EditView/components/FormInputs/Relations/Relations.d.ts +4 -4
  170. package/dist/admin/src/pages/EditView/components/FormInputs/UID.d.ts +1 -1
  171. package/dist/admin/src/pages/EditView/components/FormInputs/Wysiwyg/Editor.d.ts +1 -1
  172. package/dist/admin/src/pages/EditView/components/FormInputs/Wysiwyg/EditorLayout.d.ts +2 -2
  173. package/dist/admin/src/pages/EditView/components/FormInputs/Wysiwyg/WysiwygStyles.d.ts +19 -20
  174. package/dist/admin/src/pages/EditView/components/FormInputs/Wysiwyg/utils/utils.d.ts +7 -1
  175. package/dist/admin/src/pages/EditView/components/FormLayout.d.ts +12 -12
  176. package/dist/admin/src/pages/EditView/utils/draftRelationCounts.d.ts +26 -0
  177. package/dist/admin/src/pages/ListView/components/BulkActions/PublishAction.d.ts +1 -1
  178. package/dist/admin/src/pages/ListView/components/Filters.d.ts +1 -1
  179. package/dist/admin/src/pages/ListView/components/TableCells/CellContent.d.ts +11 -2
  180. package/dist/admin/src/pages/ListView/components/TableCells/CellValue.d.ts +1 -1
  181. package/dist/admin/src/pages/formatComponentConfigurationEditLayout.d.ts +1 -1
  182. package/dist/admin/src/preview/routes.d.ts +2 -2
  183. package/dist/admin/src/preview/services/preview.d.ts +1 -1
  184. package/dist/admin/src/preview/utils/constants.d.ts +1 -7
  185. package/dist/admin/src/preview/utils/getSendMessage.d.ts +0 -1
  186. package/dist/admin/src/router.d.ts +2 -2
  187. package/dist/admin/src/services/api.d.ts +1 -1
  188. package/dist/admin/src/services/components.d.ts +2 -2
  189. package/dist/admin/src/services/contentTypes.d.ts +3 -3
  190. package/dist/admin/src/services/documents.d.ts +49 -89
  191. package/dist/admin/src/services/homepage.d.ts +1 -1
  192. package/dist/admin/src/services/init.d.ts +1 -1
  193. package/dist/admin/src/services/relations.d.ts +8 -22
  194. package/dist/admin/src/services/uid.d.ts +8 -8
  195. package/dist/admin/src/utils/api.d.ts +2 -2
  196. package/dist/admin/src/utils/conditionalFields.d.ts +1 -0
  197. package/dist/admin/src/utils/layouts/normalizeContentManagerLayout.d.ts +4 -4
  198. package/dist/admin/src/utils/objects.d.ts +7 -4
  199. package/dist/admin/src/utils/users.d.ts +1 -1
  200. package/dist/admin/src/utils/validation.d.ts +2 -1
  201. package/dist/admin/translations/en.json.js +6 -2
  202. package/dist/admin/translations/en.json.js.map +1 -1
  203. package/dist/admin/translations/en.json.mjs +6 -2
  204. package/dist/admin/translations/en.json.mjs.map +1 -1
  205. package/dist/admin/translations/ja.json.js +262 -87
  206. package/dist/admin/translations/ja.json.js.map +1 -1
  207. package/dist/admin/translations/ja.json.mjs +262 -87
  208. package/dist/admin/translations/ja.json.mjs.map +1 -1
  209. package/dist/admin/utils/api.js +6 -1
  210. package/dist/admin/utils/api.js.map +1 -1
  211. package/dist/admin/utils/api.mjs +6 -1
  212. package/dist/admin/utils/api.mjs.map +1 -1
  213. package/dist/admin/utils/conditionalFields.js.map +1 -1
  214. package/dist/admin/utils/conditionalFields.mjs.map +1 -1
  215. package/dist/admin/utils/objects.js +43 -13
  216. package/dist/admin/utils/objects.js.map +1 -1
  217. package/dist/admin/utils/objects.mjs +43 -13
  218. package/dist/admin/utils/objects.mjs.map +1 -1
  219. package/dist/admin/utils/validation.js.map +1 -1
  220. package/dist/admin/utils/validation.mjs.map +1 -1
  221. package/dist/server/controllers/collection-types.js +6 -3
  222. package/dist/server/controllers/collection-types.js.map +1 -1
  223. package/dist/server/controllers/collection-types.mjs +6 -3
  224. package/dist/server/controllers/collection-types.mjs.map +1 -1
  225. package/dist/server/controllers/relations.js +5 -4
  226. package/dist/server/controllers/relations.js.map +1 -1
  227. package/dist/server/controllers/relations.mjs +5 -4
  228. package/dist/server/controllers/relations.mjs.map +1 -1
  229. package/dist/server/controllers/single-types.js +2 -2
  230. package/dist/server/controllers/single-types.js.map +1 -1
  231. package/dist/server/controllers/single-types.mjs +2 -2
  232. package/dist/server/controllers/single-types.mjs.map +1 -1
  233. package/dist/server/controllers/validation/model-configuration.js +3 -1
  234. package/dist/server/controllers/validation/model-configuration.js.map +1 -1
  235. package/dist/server/controllers/validation/model-configuration.mjs +3 -1
  236. package/dist/server/controllers/validation/model-configuration.mjs.map +1 -1
  237. package/dist/server/homepage/services/homepage.js +27 -7
  238. package/dist/server/homepage/services/homepage.js.map +1 -1
  239. package/dist/server/homepage/services/homepage.mjs +27 -7
  240. package/dist/server/homepage/services/homepage.mjs.map +1 -1
  241. package/dist/server/preview/controllers/preview.js +9 -0
  242. package/dist/server/preview/controllers/preview.js.map +1 -1
  243. package/dist/server/preview/controllers/preview.mjs +9 -0
  244. package/dist/server/preview/controllers/preview.mjs.map +1 -1
  245. package/dist/server/preview/controllers/previewScript.js +1450 -0
  246. package/dist/server/preview/routes/preview.js +11 -0
  247. package/dist/server/preview/routes/preview.js.map +1 -1
  248. package/dist/server/preview/routes/preview.mjs +11 -0
  249. package/dist/server/preview/routes/preview.mjs.map +1 -1
  250. package/dist/server/services/document-manager.js +13 -19
  251. package/dist/server/services/document-manager.js.map +1 -1
  252. package/dist/server/services/document-manager.mjs +13 -19
  253. package/dist/server/services/document-manager.mjs.map +1 -1
  254. package/dist/server/services/permission.js +4 -3
  255. package/dist/server/services/permission.js.map +1 -1
  256. package/dist/server/services/permission.mjs +4 -3
  257. package/dist/server/services/permission.mjs.map +1 -1
  258. package/dist/server/services/utils/draft-relations.js +9 -0
  259. package/dist/server/services/utils/draft-relations.js.map +1 -0
  260. package/dist/server/services/utils/draft-relations.mjs +7 -0
  261. package/dist/server/services/utils/draft-relations.mjs.map +1 -0
  262. package/dist/server/services/utils/draft.js +118 -25
  263. package/dist/server/services/utils/draft.js.map +1 -1
  264. package/dist/server/services/utils/draft.mjs +118 -25
  265. package/dist/server/services/utils/draft.mjs.map +1 -1
  266. package/dist/server/services/utils/populate.js +15 -1
  267. package/dist/server/services/utils/populate.js.map +1 -1
  268. package/dist/server/services/utils/populate.mjs +15 -1
  269. package/dist/server/services/utils/populate.mjs.map +1 -1
  270. package/dist/server/src/controllers/collection-types.d.ts.map +1 -1
  271. package/dist/server/src/controllers/index.d.ts +1 -1
  272. package/dist/server/src/controllers/relations.d.ts.map +1 -1
  273. package/dist/server/src/controllers/utils/clone.d.ts.map +1 -1
  274. package/dist/server/src/controllers/utils/document-status.d.ts +1 -1
  275. package/dist/server/src/controllers/utils/document-status.d.ts.map +1 -1
  276. package/dist/server/src/controllers/utils/metadata.d.ts +5 -5
  277. package/dist/server/src/controllers/utils/metadata.d.ts.map +1 -1
  278. package/dist/server/src/controllers/validation/dimensions.d.ts.map +1 -1
  279. package/dist/server/src/controllers/validation/index.d.ts +3 -3
  280. package/dist/server/src/controllers/validation/index.d.ts.map +1 -1
  281. package/dist/server/src/controllers/validation/model-configuration.d.ts.map +1 -1
  282. package/dist/server/src/controllers/validation/relations.d.ts +2 -2
  283. package/dist/server/src/history/controllers/history-version.d.ts +1 -7
  284. package/dist/server/src/history/controllers/history-version.d.ts.map +1 -1
  285. package/dist/server/src/history/controllers/index.d.ts +2 -1
  286. package/dist/server/src/history/controllers/index.d.ts.map +1 -1
  287. package/dist/server/src/history/controllers/validation/history-version.d.ts +1 -1
  288. package/dist/server/src/history/services/history.d.ts +0 -2
  289. package/dist/server/src/history/services/history.d.ts.map +1 -1
  290. package/dist/server/src/history/services/lifecycles.d.ts.map +1 -1
  291. package/dist/server/src/history/services/utils.d.ts +3 -3
  292. package/dist/server/src/history/services/utils.d.ts.map +1 -1
  293. package/dist/server/src/history/utils.d.ts +1 -18
  294. package/dist/server/src/history/utils.d.ts.map +1 -1
  295. package/dist/server/src/homepage/controllers/homepage.d.ts +0 -1
  296. package/dist/server/src/homepage/controllers/homepage.d.ts.map +1 -1
  297. package/dist/server/src/homepage/controllers/index.d.ts +2 -1
  298. package/dist/server/src/homepage/controllers/index.d.ts.map +1 -1
  299. package/dist/server/src/homepage/index.d.ts +4 -8
  300. package/dist/server/src/homepage/index.d.ts.map +1 -1
  301. package/dist/server/src/homepage/services/homepage-query-utils.d.ts.map +1 -1
  302. package/dist/server/src/homepage/services/homepage.d.ts +3 -4
  303. package/dist/server/src/homepage/services/homepage.d.ts.map +1 -1
  304. package/dist/server/src/homepage/services/index.d.ts +4 -8
  305. package/dist/server/src/homepage/services/index.d.ts.map +1 -1
  306. package/dist/server/src/index.d.ts +79 -85
  307. package/dist/server/src/index.d.ts.map +1 -1
  308. package/dist/server/src/mcp/derive-content-type-mcp-tools.d.ts.map +1 -1
  309. package/dist/server/src/mcp/handlers/collection-handlers.d.ts.map +1 -1
  310. package/dist/server/src/mcp/handlers/single-type-handlers.d.ts.map +1 -1
  311. package/dist/server/src/mcp/permissions.d.ts +1 -1
  312. package/dist/server/src/mcp/permissions.d.ts.map +1 -1
  313. package/dist/server/src/mcp/register-content-manager-mcp-tools.d.ts.map +1 -1
  314. package/dist/server/src/mcp/sanitizers/shape-relations.d.ts.map +1 -1
  315. package/dist/server/src/mcp/schemas/blocks-schema.d.ts.map +1 -1
  316. package/dist/server/src/mcp/schemas/data-schema.d.ts.map +1 -1
  317. package/dist/server/src/mcp/schemas/filters-schema.d.ts.map +1 -1
  318. package/dist/server/src/mcp/schemas/output-schemas.d.ts.map +1 -1
  319. package/dist/server/src/mcp/schemas/sort-schema.d.ts.map +1 -1
  320. package/dist/server/src/mcp/utils.d.ts.map +1 -1
  321. package/dist/server/src/middlewares/routing.d.ts.map +1 -1
  322. package/dist/server/src/policies/hasPermissions.d.ts.map +1 -1
  323. package/dist/server/src/policies/index.d.ts.map +1 -1
  324. package/dist/server/src/preview/controllers/index.d.ts +2 -1
  325. package/dist/server/src/preview/controllers/index.d.ts.map +1 -1
  326. package/dist/server/src/preview/controllers/preview.d.ts +5 -0
  327. package/dist/server/src/preview/controllers/preview.d.ts.map +1 -1
  328. package/dist/server/src/preview/controllers/validation/preview.d.ts +1 -2
  329. package/dist/server/src/preview/controllers/validation/preview.d.ts.map +1 -1
  330. package/dist/server/src/preview/routes/preview.d.ts.map +1 -1
  331. package/dist/server/src/preview/services/index.d.ts +1 -1
  332. package/dist/server/src/preview/services/index.d.ts.map +1 -1
  333. package/dist/server/src/preview/services/preview-config.d.ts +1 -1
  334. package/dist/server/src/preview/services/preview-config.d.ts.map +1 -1
  335. package/dist/server/src/preview/services/preview.d.ts.map +1 -1
  336. package/dist/server/src/preview/utils.d.ts +1 -16
  337. package/dist/server/src/preview/utils.d.ts.map +1 -1
  338. package/dist/server/src/services/components.d.ts +2 -2
  339. package/dist/server/src/services/components.d.ts.map +1 -1
  340. package/dist/server/src/services/configuration.d.ts.map +1 -1
  341. package/dist/server/src/services/content-types.d.ts.map +1 -1
  342. package/dist/server/src/services/data-mapper.d.ts +5 -5
  343. package/dist/server/src/services/data-mapper.d.ts.map +1 -1
  344. package/dist/server/src/services/document-manager.d.ts +14 -14
  345. package/dist/server/src/services/document-manager.d.ts.map +1 -1
  346. package/dist/server/src/services/document-metadata.d.ts +6 -6
  347. package/dist/server/src/services/document-metadata.d.ts.map +1 -1
  348. package/dist/server/src/services/field-sizes.d.ts +2 -2
  349. package/dist/server/src/services/field-sizes.d.ts.map +1 -1
  350. package/dist/server/src/services/index.d.ts +78 -84
  351. package/dist/server/src/services/index.d.ts.map +1 -1
  352. package/dist/server/src/services/metrics.d.ts.map +1 -1
  353. package/dist/server/src/services/permission-checker.d.ts +12 -31
  354. package/dist/server/src/services/permission-checker.d.ts.map +1 -1
  355. package/dist/server/src/services/permission.d.ts.map +1 -1
  356. package/dist/server/src/services/populate-builder.d.ts +4 -4
  357. package/dist/server/src/services/populate-builder.d.ts.map +1 -1
  358. package/dist/server/src/services/uid.d.ts.map +1 -1
  359. package/dist/server/src/services/utils/configuration/attributes.d.ts.map +1 -1
  360. package/dist/server/src/services/utils/configuration/settings.d.ts.map +1 -1
  361. package/dist/server/src/services/utils/count.d.ts.map +1 -1
  362. package/dist/server/src/services/utils/draft-relations.d.ts +19 -0
  363. package/dist/server/src/services/utils/draft-relations.d.ts.map +1 -0
  364. package/dist/server/src/services/utils/draft.d.ts +8 -7
  365. package/dist/server/src/services/utils/draft.d.ts.map +1 -1
  366. package/dist/server/src/services/utils/populate.d.ts.map +1 -1
  367. package/dist/server/src/services/utils/store.d.ts.map +1 -1
  368. package/dist/server/src/utils/index.d.ts.map +1 -1
  369. package/dist/server/src/validation/policies/hasPermissions.d.ts +1 -1
  370. package/dist/server/src/validation/zod.d.ts +1 -1
  371. package/dist/server/src/validation/zod.d.ts.map +1 -1
  372. package/dist/shared/contracts/collection-types.d.ts +4 -1
  373. package/dist/shared/contracts/collection-types.d.ts.map +1 -1
  374. package/dist/shared/contracts/single-types.d.ts +4 -1
  375. package/dist/shared/contracts/single-types.d.ts.map +1 -1
  376. package/package.json +9 -9
  377. package/dist/admin/preview/utils/previewScript.js +0 -1068
  378. package/dist/admin/preview/utils/previewScript.js.map +0 -1
  379. package/dist/admin/preview/utils/previewScript.mjs +0 -1066
  380. package/dist/admin/preview/utils/previewScript.mjs.map +0 -1
  381. package/dist/admin/src/preview/utils/previewScript.d.ts +0 -32
@@ -0,0 +1,1450 @@
1
+ /* eslint-disable */
2
+ // @ts-check
3
+
4
+ /**
5
+ * STANDALONE PREVIEW SCRIPT
6
+ *
7
+ * This file is NOT bundled. It is served verbatim by the content-manager server
8
+ * (GET /content-manager/preview/script) and injected into the user's site inside
9
+ * the preview iframe by the admin (over postMessage, the STRAPI_SCRIPT event).
10
+ *
11
+ * Because it runs in the user's page, it CANNOT use any imports or refer to any
12
+ * variable outside of its own scope. Everything it needs is either defined within
13
+ * `previewScript` or passed in through the `config` argument that the admin injects
14
+ * at invocation time. Writing in JS and keeping it out of any bundler is deliberate:
15
+ * it guarantees the served text is exactly what's written here (no helper hoisting, no wrapping),
16
+ * which is what makes it safe to evaluate as-is in the iframe.
17
+ */
18
+
19
+ /**
20
+ * @typedef {Object} PreviewScriptColors
21
+ * @property {string} highlightHoverColor
22
+ * @property {string} highlightActiveColor
23
+ */
24
+
25
+ /**
26
+ * @typedef {typeof import('../../../../admin/src/preview/utils/constants').INTERNAL_EVENTS} InternalEvents
27
+ */
28
+
29
+ /**
30
+ * @typedef {Object} PreviewScriptConfig
31
+ * @property {PreviewScriptColors} colors
32
+ * @property {InternalEvents} events Internal event names, owned by the admin (INTERNAL_EVENTS).
33
+ * @property {string} parentOrigin Admin panel origin used to validate and target postMessage traffic.
34
+ */
35
+
36
+ /**
37
+ * @typedef {Object} WindowExtensions
38
+ * @property {string} [STRAPI_HIGHLIGHT_HOVER_COLOR] Optional override for the highlight hover color, injected by the admin.
39
+ * @property {string} [STRAPI_HIGHLIGHT_ACTIVE_COLOR] Optional override for the highlight active color, injected by the admin.
40
+ * @property {boolean} [STRAPI_DISABLE_STEGA_DECODING] Optional flag to disable steganography decoding, injected by the admin.
41
+ * @property {() => void} [__strapi_previewCleanup] Optional cleanup function, injected by the admin.
42
+ */
43
+
44
+ /**
45
+ * @param {PreviewScriptConfig} config
46
+ */
47
+ function previewScript(config) {
48
+ const { colors, events: INTERNAL_EVENTS, parentOrigin } = config;
49
+
50
+ /** @type {typeof window & WindowExtensions} */
51
+ const win = window;
52
+
53
+ /* -----------------------------------------------------------------------------------------------
54
+ * Params
55
+ * ---------------------------------------------------------------------------------------------*/
56
+ const HIGHLIGHT_PADDING = 2; // in pixels
57
+ const HIGHLIGHT_HOVER_COLOR = win.STRAPI_HIGHLIGHT_HOVER_COLOR ?? colors.highlightHoverColor;
58
+ const HIGHLIGHT_ACTIVE_COLOR = win.STRAPI_HIGHLIGHT_ACTIVE_COLOR ?? colors.highlightActiveColor;
59
+ const HIGHLIGHT_STYLES_ID = 'strapi-preview-highlight-styles';
60
+ const DOUBLE_CLICK_TIMEOUT = 300; // milliseconds to wait for potential double-click
61
+
62
+ const DISABLE_STEGA_DECODING = win.STRAPI_DISABLE_STEGA_DECODING ?? false;
63
+ const SOURCE_ATTRIBUTE = 'data-strapi-source';
64
+ const OVERLAY_ID = 'strapi-preview-overlay';
65
+
66
+ /* -----------------------------------------------------------------------------------------------
67
+ * Utils
68
+ * ---------------------------------------------------------------------------------------------*/
69
+
70
+ /**
71
+ * @param {InternalEvents[keyof InternalEvents]} type
72
+ * @param {unknown} payload
73
+ */
74
+ const sendMessage = (type, payload) => {
75
+ window.parent.postMessage({ type, payload }, parentOrigin);
76
+ };
77
+
78
+ /**
79
+ * @param {string} path
80
+ */
81
+ const getElementsByPath = (path) => {
82
+ return document.querySelectorAll(`[${SOURCE_ATTRIBUTE}*="path=${path}"]`);
83
+ };
84
+
85
+ /**
86
+ * @param {Element} element
87
+ * @returns {boolean}
88
+ */
89
+ const isMediaElement = (element) => {
90
+ return element.tagName === 'IMG' || element.tagName === 'VIDEO' || element.tagName === 'AUDIO';
91
+ };
92
+
93
+ /**
94
+ * When a media field's mime type changes (e.g. image -> video), we can't
95
+ * swap the rendered DOM tag in place — the host framework (e.g. React)
96
+ * owns the original element and throws on removeChild if we replaceChild
97
+ * it. Instead we leave the original where it is, hide it, and insert our
98
+ * own sibling element of the correct tag. The injection lives outside the
99
+ * host framework's virtual DOM, so reconciliation never touches it.
100
+ *
101
+ * On save the host framework re-renders with the saved data and unmounts
102
+ * the original, leaving our injection orphaned next to its replacement —
103
+ * which is what was producing duplicate previews. The childList observer
104
+ * below uses these maps to drop the injection whenever its associated
105
+ * original is removed.
106
+ *
107
+ * @type {Map<HTMLElement, HTMLElement>}
108
+ */
109
+ const originalToInjection = new Map();
110
+ /** @type {Map<HTMLElement, HTMLElement>} */
111
+ const injectionToOriginal = new Map();
112
+
113
+ /**
114
+ * @param {HTMLElement} original
115
+ * @param {HTMLElement} injection
116
+ */
117
+ const trackInjection = (original, injection) => {
118
+ originalToInjection.set(original, injection);
119
+ injectionToOriginal.set(injection, original);
120
+ };
121
+
122
+ /**
123
+ * @param {HTMLElement} injection
124
+ */
125
+ const untrackInjection = (injection) => {
126
+ const original = injectionToOriginal.get(injection);
127
+ if (original) originalToInjection.delete(original);
128
+ injectionToOriginal.delete(injection);
129
+ };
130
+
131
+ /**
132
+ * When a media field is cleared, the original media element gets hidden and
133
+ * has no bounding box, so the highlight system stops drawing a clickable
134
+ * area for it. That leaves the user no way to re-add media from the preview.
135
+ * We insert a sized placeholder div carrying the original's source attribute
136
+ * so the highlight system keeps tracking the spot. The placeholder is
137
+ * tracked in the same injection maps as a real injection, so the next
138
+ * `setMediaElement` call (when the user picks new media) tears it down and
139
+ * restores/replaces the original element via the existing flow.
140
+ */
141
+ const PLACEHOLDER_ATTRIBUTE = 'data-strapi-media-placeholder';
142
+ /**
143
+ * For a multi-media field, growing the rendered DOM (e.g. 1 image → 2)
144
+ * needs a slot for the new item, but the host framework only re-renders
145
+ * the gallery on save. We synthesize the missing slots by cloning the
146
+ * last rendered item and marking each clone with this attribute so the
147
+ * ghost-sweep below can drop them once the framework eventually re-renders
148
+ * with the saved value.
149
+ */
150
+ const CLONE_ATTRIBUTE = 'data-strapi-media-clone';
151
+ /**
152
+ * Galleries commonly wrap each media element in its own layout cell
153
+ * (e.g. `<div class="grid"><div><img/></div><div><img/></div></div>`),
154
+ * so cloning just the IMG makes the new item stack inside the existing
155
+ * wrapper. We walk up to the closest ancestor whose parent uses a sibling
156
+ * layout (flex/grid) and clone that instead, marking the cloned wrapper
157
+ * so cleanup removes the wrapper rather than leaving an empty cell.
158
+ */
159
+ const CLONE_WRAPPER_ATTRIBUTE = 'data-strapi-media-clone-wrapper';
160
+
161
+ /**
162
+ * @param {string} sourceAttr
163
+ * @param {DOMRect} rect
164
+ * @returns {HTMLElement}
165
+ */
166
+ const createMediaPlaceholder = (sourceAttr, rect) => {
167
+ const placeholder = document.createElement('div');
168
+ placeholder.setAttribute(PLACEHOLDER_ATTRIBUTE, '');
169
+ placeholder.setAttribute(SOURCE_ATTRIBUTE, sourceAttr);
170
+
171
+ const width = rect.width > 0 ? rect.width : 200;
172
+ const height = rect.height > 0 ? rect.height : 200;
173
+ placeholder.style.cssText = `
174
+ display: inline-block;
175
+ width: ${width}px;
176
+ height: ${height}px;
177
+ background-color: rgba(0, 0, 0, 0.04);
178
+ border: 2px dashed rgba(0, 0, 0, 0.15);
179
+ border-radius: 4px;
180
+ box-sizing: border-box;
181
+ vertical-align: top;
182
+ `;
183
+ return placeholder;
184
+ };
185
+
186
+ /**
187
+ * Form values for media fields carry the raw Strapi URL (often relative,
188
+ * e.g. "/uploads/foo.jpg"). The host framework typically renders the
189
+ * original element with an absolute URL (resolved against the Strapi
190
+ * backend, not the iframe origin). If we naively set the relative form
191
+ * value as src, the browser resolves it against the iframe origin and
192
+ * fails to load. Resolve against the original element's existing src
193
+ * origin so the swap targets the same backend.
194
+ *
195
+ * @param {HTMLElement} originalEl
196
+ * @param {string} rawUrl
197
+ * @returns {string}
198
+ */
199
+ const resolveMediaUrl = (originalEl, rawUrl) => {
200
+ if (/^(?:[a-z]+:)?\/\//i.test(rawUrl) || rawUrl.startsWith('data:')) {
201
+ return rawUrl;
202
+ }
203
+ const currentSrc = originalEl.getAttribute('src');
204
+ if (!currentSrc) return rawUrl;
205
+ try {
206
+ const currentUrl = new URL(currentSrc, window.location.href);
207
+ return new URL(rawUrl, currentUrl.origin).href;
208
+ } catch {
209
+ return rawUrl;
210
+ }
211
+ };
212
+
213
+ const SAFE_MEDIA_URL_PROTOCOLS = new Set(['http:', 'https:', 'data:']);
214
+
215
+ /**
216
+ * Reject dangerous URL schemes before assigning media `src` attributes.
217
+ *
218
+ * @param {string | null | undefined} url
219
+ * @returns {string | null}
220
+ */
221
+ const sanitizeMediaUrl = (url) => {
222
+ if (!url) return null;
223
+
224
+ try {
225
+ const parsed = new URL(url, window.location.href);
226
+ if (!SAFE_MEDIA_URL_PROTOCOLS.has(parsed.protocol)) {
227
+ return null;
228
+ }
229
+ return parsed.href;
230
+ } catch {
231
+ return url.startsWith('/') ? url : null;
232
+ }
233
+ };
234
+
235
+ /**
236
+ * Extract a recognizable media filename from a URL string. Works for
237
+ * direct backend URLs, relative paths, and proxy URLs that embed the
238
+ * real path in a query parameter (e.g. Next.js's "/_next/image?url=...").
239
+ * Used to detect whether the rendered element already shows the same
240
+ * asset as the form value, so we can skip src updates that would only
241
+ * substitute one URL representation for another.
242
+ *
243
+ * @param {string} raw
244
+ * @returns {string}
245
+ */
246
+ const getMediaFilename = (raw) => {
247
+ if (!raw) return '';
248
+ let decoded = raw;
249
+ try {
250
+ decoded = decodeURIComponent(raw);
251
+ } catch {}
252
+ const match = decoded.match(
253
+ /([^/?#&=\s]+\.(?:jpg|jpeg|png|gif|webp|avif|svg|mp4|mov|webm|ogg|m4v|mp3|wav|m4a|aac|flac|opus))(?:[?#&]|$)/i
254
+ );
255
+ return match ? match[1].toLowerCase() : '';
256
+ };
257
+
258
+ /**
259
+ * Get the field path to use for focusing a media field.
260
+ * - For IMG/VIDEO elements: the path was already normalized (stripped of .url) during stega decoding
261
+ * - For tracked injections (e.g., placeholder divs we insert when media is cleared): the source
262
+ * attribute was copied from the post-normalization media element, so it's already correct
263
+ * - For non-media elements with model=plugin::upload.file (e.g., caption text): strip the last
264
+ * segment to focus the parent media field (e.g., "hero.caption" -> "hero")
265
+ *
266
+ * @param {string} sourceAttr
267
+ * @param {Element} element
268
+ * @returns {string}
269
+ */
270
+ const getFieldPathForMedia = (sourceAttr, element) => {
271
+ if (isMediaElement(element) || injectionToOriginal.has(/** @type {HTMLElement} */ (element))) {
272
+ return sourceAttr;
273
+ }
274
+
275
+ // For non-media elements, check if it's a media asset field
276
+ const params = new URLSearchParams(sourceAttr);
277
+ if (params.get('model') === 'plugin::upload.file') {
278
+ const elementPath = params.get('path');
279
+ if (elementPath) {
280
+ // Strip the last segment (e.g., "hero.caption" -> "hero")
281
+ const parentPath = elementPath.split('.').slice(0, -1).join('.');
282
+ params.set('path', parentPath);
283
+ return params.toString();
284
+ }
285
+ }
286
+
287
+ return sourceAttr;
288
+ };
289
+
290
+ /* -----------------------------------------------------------------------------------------------
291
+ * Functionality pieces
292
+ * ---------------------------------------------------------------------------------------------*/
293
+
294
+ const setupStegaDOMObserver = async () => {
295
+ if (DISABLE_STEGA_DECODING) {
296
+ return;
297
+ }
298
+
299
+ let stegaDecode;
300
+ let stegaClean;
301
+
302
+ try {
303
+ // The specifier is a plain external URL. This file is never bundled, so the
304
+ // import is left exactly as written and evaluated natively inside the iframe.
305
+ ({ vercelStegaDecode: stegaDecode, vercelStegaClean: stegaClean } = await import(
306
+ // @ts-ignore it's not a local dependency
307
+ 'https://cdn.jsdelivr.net/npm/@vercel/stega@0.1.2/+esm'
308
+ ));
309
+ } catch {
310
+ return;
311
+ }
312
+
313
+ /**
314
+ * @param {Element} element
315
+ */
316
+ const applyStegaToElement = (element) => {
317
+ // Handle img and video tags - check src attribute for stega encoding
318
+ if (isMediaElement(element)) {
319
+ const src = element.getAttribute('src');
320
+ if (src) {
321
+ try {
322
+ const result = stegaDecode(src);
323
+ if (result && 'strapiSource' in result) {
324
+ // Parse the source and remove .url suffix to point to the media field
325
+ const sourceValue = result.strapiSource;
326
+ const pathMatch = sourceValue.match(/path=([^&]+)/);
327
+ if (pathMatch) {
328
+ const originalPath = pathMatch[1];
329
+ // Remove .url to get the media field path
330
+ const mediaPath = originalPath.replace(/\.url$/, '');
331
+ const newSource = sourceValue.replace(`path=${originalPath}`, `path=${mediaPath}`);
332
+ element.setAttribute(SOURCE_ATTRIBUTE, newSource);
333
+ }
334
+ }
335
+ // Clean the src attribute so the resource can load
336
+ const cleanedSrc = stegaClean(src);
337
+ if (cleanedSrc !== src) {
338
+ element.setAttribute('src', cleanedSrc);
339
+ }
340
+ } catch (error) {}
341
+ }
342
+ return;
343
+ }
344
+
345
+ const directTextNodes = Array.from(element.childNodes).filter(
346
+ (node) => node.nodeType === Node.TEXT_NODE
347
+ );
348
+
349
+ const directTextContent = directTextNodes.map((node) => node.textContent || '').join('');
350
+
351
+ if (directTextContent) {
352
+ try {
353
+ // TODO: check if we can call split instead of decode+clean
354
+ const result = stegaDecode(directTextContent);
355
+ if (result && 'strapiSource' in result) {
356
+ element.setAttribute(SOURCE_ATTRIBUTE, result.strapiSource);
357
+
358
+ // Remove encoded part from DOM text content (to avoid breaking links for example)
359
+ directTextNodes.forEach((node) => {
360
+ if (node.textContent) {
361
+ const cleanedText = stegaClean(node.textContent);
362
+ if (cleanedText !== node.textContent) {
363
+ node.textContent = cleanedText;
364
+ }
365
+ }
366
+ });
367
+ }
368
+ } catch (error) {}
369
+ }
370
+ };
371
+
372
+ // Process all existing elements
373
+ const allElements = document.querySelectorAll('*');
374
+ Array.from(allElements).forEach(applyStegaToElement);
375
+
376
+ // Create observer for new elements and text changes
377
+ const observer = new MutationObserver((mutations) => {
378
+ mutations.forEach((mutation) => {
379
+ // Handle added nodes
380
+ if (mutation.type === 'childList') {
381
+ mutation.addedNodes.forEach((node) => {
382
+ if (node.nodeType === Node.ELEMENT_NODE) {
383
+ const element = /** @type {Element} */ (node);
384
+ // Process the added element
385
+ applyStegaToElement(element);
386
+ // Process all child elements
387
+ const childElements = element.querySelectorAll('*');
388
+ Array.from(childElements).forEach(applyStegaToElement);
389
+ }
390
+ });
391
+ }
392
+
393
+ // Handle text content changes
394
+ if (mutation.type === 'characterData' && mutation.target.parentElement) {
395
+ applyStegaToElement(mutation.target.parentElement);
396
+ }
397
+
398
+ // Handle src attribute changes for img/video elements
399
+ if (mutation.type === 'attributes' && mutation.attributeName === 'src') {
400
+ const target = /** @type {Element} */ (mutation.target);
401
+ if (isMediaElement(target)) {
402
+ applyStegaToElement(target);
403
+ }
404
+ }
405
+ });
406
+ });
407
+
408
+ observer.observe(document, {
409
+ childList: true,
410
+ subtree: true,
411
+ characterData: true,
412
+ attributes: true,
413
+ attributeFilter: ['src'],
414
+ });
415
+
416
+ return observer;
417
+ };
418
+
419
+ const createHighlightStyles = () => {
420
+ const existingStyles = document.getElementById(HIGHLIGHT_STYLES_ID);
421
+ // Remove existing styles to avoid duplicates
422
+ if (existingStyles) {
423
+ existingStyles.remove();
424
+ }
425
+
426
+ const styleElement = document.createElement('style');
427
+ styleElement.id = HIGHLIGHT_STYLES_ID;
428
+ styleElement.textContent = `
429
+ .strapi-highlight {
430
+ position: absolute;
431
+ outline: 2px solid transparent;
432
+ pointer-events: auto;
433
+ border-radius: 2px;
434
+ background-color: transparent;
435
+ will-change: transform;
436
+ transition: outline-color 0.1s ease-in-out;
437
+ }
438
+
439
+ .strapi-highlight:hover {
440
+ outline-color: ${HIGHLIGHT_HOVER_COLOR} !important;
441
+ }
442
+
443
+ .strapi-highlight.strapi-highlight-focused {
444
+ outline-color: ${HIGHLIGHT_ACTIVE_COLOR} !important;
445
+ outline-width: 3px !important;
446
+ }
447
+ `;
448
+
449
+ document.head.appendChild(styleElement);
450
+ return styleElement;
451
+ };
452
+
453
+ const createOverlaySystem = () => {
454
+ // Clean up before creating a new overlay so we can safely call previewScript multiple times
455
+ win.__strapi_previewCleanup?.();
456
+ document.getElementById(OVERLAY_ID)?.remove();
457
+
458
+ const overlay = document.createElement('div');
459
+ overlay.id = OVERLAY_ID;
460
+ overlay.style.cssText = `
461
+ position: fixed;
462
+ top: 0;
463
+ left: 0;
464
+ width: 100%;
465
+ height: 100%;
466
+ pointer-events: none;
467
+ z-index: 9999;
468
+ `;
469
+
470
+ window.document.body.appendChild(overlay);
471
+ return overlay;
472
+ };
473
+
474
+ /**
475
+ * @typedef {{ element: HTMLElement | Window; type: keyof HTMLElementEventMap | 'message'; handler: EventListener; }} EventListenerEntry
476
+ * @typedef {EventListenerEntry[]} EventListenersList
477
+ */
478
+
479
+ /**
480
+ * Group all elements that share the exact same `data-strapi-source` value
481
+ * under one highlight. This is what makes a multi-media field render as a
482
+ * single bounding box in the preview (and what makes the popover open the
483
+ * multi-media input rather than treating each item as its own field). The
484
+ * grouping is keyed off the source string so other accidental same-source
485
+ * renders (e.g. the title rendered twice) also collapse to one highlight,
486
+ * which matches how the focused state already behaves.
487
+ *
488
+ * @typedef {{ highlight: HTMLElement; elements: Set<HTMLElement>; }} HighlightGroup
489
+ */
490
+
491
+ /**
492
+ * @param {HTMLElement} overlay
493
+ */
494
+ const createHighlightManager = (overlay) => {
495
+ /** @type {Map<string, HighlightGroup>} */
496
+ const groups = new Map();
497
+ /** @type {Map<HTMLElement, string>} */
498
+ const elementToGroupKey = new Map();
499
+ /** @type {EventListenersList} */
500
+ const eventListeners = [];
501
+ /** @type {HTMLElement[]} */
502
+ const focusedHighlights = [];
503
+ /** @type {Map<HighlightGroup, number>} */
504
+ const pendingClicks = new Map();
505
+ /** @type {string | null} */
506
+ let focusedField = null;
507
+
508
+ /**
509
+ * @param {HighlightGroup} group
510
+ */
511
+ const computeGroupRect = (group) => {
512
+ let minLeft = Infinity;
513
+ let minTop = Infinity;
514
+ let maxRight = -Infinity;
515
+ let maxBottom = -Infinity;
516
+ let any = false;
517
+ group.elements.forEach((el) => {
518
+ const r = el.getBoundingClientRect();
519
+ if (r.width === 0 && r.height === 0) return;
520
+ any = true;
521
+ if (r.left < minLeft) minLeft = r.left;
522
+ if (r.top < minTop) minTop = r.top;
523
+ if (r.right > maxRight) maxRight = r.right;
524
+ if (r.bottom > maxBottom) maxBottom = r.bottom;
525
+ });
526
+ if (!any) return null;
527
+ return {
528
+ left: minLeft,
529
+ top: minTop,
530
+ width: maxRight - minLeft,
531
+ height: maxBottom - minTop,
532
+ };
533
+ };
534
+
535
+ /**
536
+ * @param {HighlightGroup} group
537
+ */
538
+ const drawGroup = (group) => {
539
+ const rect = computeGroupRect(group);
540
+ if (!rect) {
541
+ group.highlight.style.display = 'none';
542
+ return;
543
+ }
544
+ group.highlight.style.display = '';
545
+ group.highlight.style.width = `${rect.width + HIGHLIGHT_PADDING * 2}px`;
546
+ group.highlight.style.height = `${rect.height + HIGHLIGHT_PADDING * 2}px`;
547
+ group.highlight.style.transform = `translate(${rect.left - HIGHLIGHT_PADDING}px, ${rect.top - HIGHLIGHT_PADDING}px)`;
548
+ };
549
+
550
+ const updateAllHighlights = () => {
551
+ groups.forEach(drawGroup);
552
+ };
553
+
554
+ /**
555
+ * Pick the underlying source element under the pointer so single-click
556
+ * redispatch hits the specific item the user clicked, even when the group
557
+ * highlight covers several elements (multi-media gallery).
558
+ *
559
+ * @param {HighlightGroup} group
560
+ * @param {number} x
561
+ * @param {number} y
562
+ * @returns {HTMLElement | null}
563
+ */
564
+ const pickElementAtPoint = (group, x, y) => {
565
+ for (const el of group.elements) {
566
+ const r = el.getBoundingClientRect();
567
+ if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) {
568
+ return el;
569
+ }
570
+ }
571
+ const first = group.elements.values().next().value;
572
+ return first ?? null;
573
+ };
574
+
575
+ /**
576
+ * @param {string} groupKey
577
+ * @returns {HighlightGroup}
578
+ */
579
+ const createGroup = (groupKey) => {
580
+ const highlight = document.createElement('div');
581
+ highlight.className = 'strapi-highlight';
582
+ /** @type {HighlightGroup} */
583
+ const group = { highlight, elements: new Set() };
584
+
585
+ /**
586
+ * @param {MouseEvent} event
587
+ */
588
+ const clickHandler = (event) => {
589
+ if (/** @type {any} */ (event).__strapi_redispatched) {
590
+ return;
591
+ }
592
+
593
+ event.preventDefault();
594
+ event.stopPropagation();
595
+
596
+ const existingTimeout = pendingClicks.get(group);
597
+ if (existingTimeout) {
598
+ window.clearTimeout(existingTimeout);
599
+ pendingClicks.delete(group);
600
+ }
601
+
602
+ const timeout = window.setTimeout(() => {
603
+ pendingClicks.delete(group);
604
+
605
+ sendMessage(INTERNAL_EVENTS.STRAPI_FIELD_SINGLE_CLICK_HINT, null);
606
+
607
+ // Pick the specific underlying element under the pointer so the
608
+ // redispatched click targets the right item in a multi-element group
609
+ const underlying = pickElementAtPoint(group, event.clientX, event.clientY);
610
+ if (!underlying) return;
611
+
612
+ /** @type {HTMLElement} */
613
+ let targetElement = underlying;
614
+ if (isMediaElement(underlying)) {
615
+ let parent = underlying.parentElement;
616
+ while (parent) {
617
+ if (
618
+ parent.tagName === 'A' ||
619
+ parent.tagName === 'BUTTON' ||
620
+ parent.hasAttribute('onclick') ||
621
+ parent.getAttribute('role') === 'button' ||
622
+ parent.getAttribute('role') === 'link'
623
+ ) {
624
+ targetElement = parent;
625
+ break;
626
+ }
627
+ parent = parent.parentElement;
628
+ }
629
+ }
630
+
631
+ const newEvent = new MouseEvent('click', {
632
+ bubbles: true,
633
+ cancelable: true,
634
+ view: window,
635
+ detail: 1,
636
+ button: event.button,
637
+ buttons: event.buttons,
638
+ clientX: event.clientX,
639
+ clientY: event.clientY,
640
+ ctrlKey: event.ctrlKey,
641
+ altKey: event.altKey,
642
+ shiftKey: event.shiftKey,
643
+ metaKey: event.metaKey,
644
+ });
645
+ /** @type {any} */ (newEvent).__strapi_redispatched = true;
646
+ targetElement.dispatchEvent(newEvent);
647
+ }, DOUBLE_CLICK_TIMEOUT);
648
+
649
+ pendingClicks.set(group, timeout);
650
+ };
651
+
652
+ /**
653
+ * @param {MouseEvent} event
654
+ */
655
+ const doubleClickHandler = (event) => {
656
+ event.preventDefault();
657
+ event.stopPropagation();
658
+
659
+ const existingTimeout = pendingClicks.get(group);
660
+ if (existingTimeout) {
661
+ clearTimeout(existingTimeout);
662
+ pendingClicks.delete(group);
663
+ }
664
+
665
+ const anchor = pickElementAtPoint(group, event.clientX, event.clientY);
666
+ if (!anchor) return;
667
+ const sourceAttribute = anchor.getAttribute(SOURCE_ATTRIBUTE);
668
+ if (!sourceAttribute) return;
669
+ const path = getFieldPathForMedia(sourceAttribute, anchor);
670
+ const rect = computeGroupRect(group);
671
+ if (!rect) return;
672
+ sendMessage(INTERNAL_EVENTS.STRAPI_FIELD_FOCUS_INTENT, {
673
+ path,
674
+ position: {
675
+ top: rect.top,
676
+ left: rect.left,
677
+ right: rect.left + rect.width,
678
+ bottom: rect.top + rect.height,
679
+ width: rect.width,
680
+ height: rect.height,
681
+ },
682
+ });
683
+ };
684
+
685
+ /**
686
+ * @param {MouseEvent} event
687
+ */
688
+ const mouseDownHandler = (event) => {
689
+ if (event.detail >= 2) {
690
+ event.preventDefault();
691
+ }
692
+ };
693
+
694
+ highlight.addEventListener('click', clickHandler);
695
+ highlight.addEventListener('dblclick', doubleClickHandler);
696
+ highlight.addEventListener('mousedown', mouseDownHandler);
697
+
698
+ eventListeners.push(
699
+ { element: highlight, type: 'click', handler: /** @type {EventListener} */ (clickHandler) },
700
+ {
701
+ element: highlight,
702
+ type: 'dblclick',
703
+ handler: /** @type {EventListener} */ (doubleClickHandler),
704
+ },
705
+ {
706
+ element: highlight,
707
+ type: 'mousedown',
708
+ handler: /** @type {EventListener} */ (mouseDownHandler),
709
+ }
710
+ );
711
+
712
+ overlay.appendChild(highlight);
713
+ groups.set(groupKey, group);
714
+ return group;
715
+ };
716
+
717
+ /**
718
+ * @param {string} groupKey
719
+ * @param {HighlightGroup} group
720
+ */
721
+ const destroyGroup = (groupKey, group) => {
722
+ const pendingTimeout = pendingClicks.get(group);
723
+ if (pendingTimeout) {
724
+ window.clearTimeout(pendingTimeout);
725
+ pendingClicks.delete(group);
726
+ }
727
+
728
+ const focusedIndex = focusedHighlights.indexOf(group.highlight);
729
+ if (focusedIndex !== -1) {
730
+ focusedHighlights.splice(focusedIndex, 1);
731
+ }
732
+
733
+ group.highlight.remove();
734
+
735
+ const listenersToRemove = eventListeners.filter(
736
+ (listener) => listener.element === group.highlight
737
+ );
738
+ listenersToRemove.forEach(({ element, type, handler }) => {
739
+ element.removeEventListener(type, handler);
740
+ });
741
+ eventListeners.splice(
742
+ 0,
743
+ eventListeners.length,
744
+ ...eventListeners.filter((listener) => listener.element !== group.highlight)
745
+ );
746
+
747
+ groups.delete(groupKey);
748
+ };
749
+
750
+ /**
751
+ * @param {HTMLElement} element
752
+ */
753
+ const createHighlightForElement = (element) => {
754
+ if (elementToGroupKey.has(element)) {
755
+ return;
756
+ }
757
+ const groupKey = element.getAttribute(SOURCE_ATTRIBUTE);
758
+ if (!groupKey) return;
759
+
760
+ let group = groups.get(groupKey);
761
+
762
+ // When a framework-rendered media element joins a group that still
763
+ // holds stale ghosts from a previous clear (a standalone placeholder,
764
+ // our injection paired with a hidden placeholder, or a clone we
765
+ // synthesized to give an added array item a slot), sweep the ghosts
766
+ // so we don't end up with two visible elements stacked on top of
767
+ // each other after a save commits a real value.
768
+ if (
769
+ group &&
770
+ isMediaElement(element) &&
771
+ !injectionToOriginal.has(element) &&
772
+ !element.hasAttribute(CLONE_ATTRIBUTE)
773
+ ) {
774
+ /** @type {HTMLElement[]} */
775
+ const ghostInjections = [];
776
+ /** @type {HTMLElement[]} */
777
+ const ghostPlaceholders = [];
778
+ /** @type {HTMLElement[]} */
779
+ const ghostClones = [];
780
+ group.elements.forEach((el) => {
781
+ if (injectionToOriginal.has(el)) {
782
+ ghostInjections.push(el);
783
+ } else if (el.hasAttribute(CLONE_ATTRIBUTE)) {
784
+ ghostClones.push(el);
785
+ } else if (el.hasAttribute(PLACEHOLDER_ATTRIBUTE)) {
786
+ ghostPlaceholders.push(el);
787
+ }
788
+ });
789
+ ghostInjections.forEach((injection) => {
790
+ const orig = injectionToOriginal.get(injection);
791
+ untrackInjection(injection);
792
+ if (orig?.hasAttribute(CLONE_ATTRIBUTE)) {
793
+ // Tag-swap injection of a clone (e.g. user added a video as
794
+ // the new item, swapping our cloned IMG for a VIDEO). Remove
795
+ // the wrapper so we don't leave an empty layout cell behind.
796
+ const wrapper = /** @type {HTMLElement | null} */ (
797
+ orig.closest(`[${CLONE_WRAPPER_ATTRIBUTE}]`)
798
+ );
799
+ (wrapper ?? orig).remove();
800
+ } else if (orig?.hasAttribute(PLACEHOLDER_ATTRIBUTE)) {
801
+ orig.remove();
802
+ }
803
+ group?.elements.delete(injection);
804
+ elementToGroupKey.delete(injection);
805
+ injection.remove();
806
+ });
807
+ ghostPlaceholders.forEach((placeholder) => {
808
+ group?.elements.delete(placeholder);
809
+ elementToGroupKey.delete(placeholder);
810
+ placeholder.remove();
811
+ });
812
+ // Remove the wrapper if we cloned an ancestor for layout — falling
813
+ // back to the clone itself when it's its own root. Removing via
814
+ // .remove() also triggers the mutation observer's removedNodes
815
+ // branch, which untracks any injection paired with descendants.
816
+ ghostClones.forEach((clone) => {
817
+ group?.elements.delete(clone);
818
+ elementToGroupKey.delete(clone);
819
+ const wrapper = /** @type {HTMLElement | null} */ (
820
+ clone.closest(`[${CLONE_WRAPPER_ATTRIBUTE}]`)
821
+ );
822
+ (wrapper ?? clone).remove();
823
+ });
824
+ }
825
+
826
+ if (!group) {
827
+ group = createGroup(groupKey);
828
+ }
829
+ group.elements.add(element);
830
+ elementToGroupKey.set(element, groupKey);
831
+ drawGroup(group);
832
+ };
833
+
834
+ /**
835
+ * @param {Element} element
836
+ */
837
+ const removeHighlightForElement = (element) => {
838
+ const groupKey = elementToGroupKey.get(/** @type {HTMLElement} */ (element));
839
+ if (!groupKey) return;
840
+ const group = groups.get(groupKey);
841
+ if (!group) return;
842
+
843
+ group.elements.delete(/** @type {HTMLElement} */ (element));
844
+ elementToGroupKey.delete(/** @type {HTMLElement} */ (element));
845
+
846
+ if (group.elements.size === 0) {
847
+ destroyGroup(groupKey, group);
848
+ } else {
849
+ drawGroup(group);
850
+ }
851
+ };
852
+
853
+ /**
854
+ * Resolve groups whose elements match a focused field path. We delegate
855
+ * to `getElementsByPath` so this preserves the same loose substring match
856
+ * the highlight system has always used (e.g. focusing `hero` matches
857
+ * `hero.caption` too).
858
+ *
859
+ * @param {string} path
860
+ * @returns {HighlightGroup[]}
861
+ */
862
+ const findGroupForPath = (path) => {
863
+ /** @type {Set<HighlightGroup>} */
864
+ const matched = new Set();
865
+ getElementsByPath(path).forEach((el) => {
866
+ const key = elementToGroupKey.get(/** @type {HTMLElement} */ (el));
867
+ if (!key) return;
868
+ const group = groups.get(key);
869
+ if (group) matched.add(group);
870
+ });
871
+ return Array.from(matched);
872
+ };
873
+
874
+ // Process all existing elements with source attributes
875
+ const initialElements = window.document.querySelectorAll(`[${SOURCE_ATTRIBUTE}]`);
876
+ Array.from(initialElements).forEach((element) => {
877
+ if (element instanceof HTMLElement) {
878
+ createHighlightForElement(element);
879
+ }
880
+ });
881
+
882
+ return {
883
+ get elements() {
884
+ return Array.from(elementToGroupKey.keys());
885
+ },
886
+ get groups() {
887
+ return Array.from(groups.values());
888
+ },
889
+ updateAllHighlights,
890
+ eventListeners,
891
+ focusedHighlights,
892
+ createHighlightForElement,
893
+ removeHighlightForElement,
894
+ findGroupForPath,
895
+ /** @param {string | null} field */
896
+ setFocusedField: (field) => {
897
+ focusedField = field;
898
+ },
899
+ getFocusedField: () => focusedField,
900
+ clearAllPendingClicks: () => {
901
+ pendingClicks.forEach((timeout) => clearTimeout(timeout));
902
+ pendingClicks.clear();
903
+ },
904
+ };
905
+ };
906
+
907
+ /**
908
+ * @typedef {ReturnType<typeof createHighlightManager>} HighlightManager
909
+ */
910
+
911
+ /**
912
+ * We need to track scroll in all the element parents in order to keep the highlight position
913
+ * in sync with the element position. Listening to window scroll is not enough because the
914
+ * element can be inside one or more scrollable containers.
915
+ *
916
+ * @param {HighlightManager} highlightManager
917
+ */
918
+ const setupScrollManagement = (highlightManager) => {
919
+ const updateOnScroll = () => {
920
+ highlightManager.updateAllHighlights();
921
+ };
922
+
923
+ /** @type {Set<Element | Window>} */
924
+ const scrollableElements = new Set();
925
+ scrollableElements.add(window);
926
+
927
+ // Find all scrollable ancestors for all tracked elements and set up scroll listeners
928
+ highlightManager.elements.forEach((element) => {
929
+ let parent = element.parentElement;
930
+ while (parent) {
931
+ const computedStyle = window.getComputedStyle(parent);
932
+ const overflow = computedStyle.overflow + computedStyle.overflowX + computedStyle.overflowY;
933
+
934
+ if (overflow.includes('scroll') || overflow.includes('auto')) {
935
+ scrollableElements.add(parent);
936
+ }
937
+
938
+ parent = parent.parentElement;
939
+ }
940
+ });
941
+
942
+ // Add scroll listeners to all scrollable elements
943
+ scrollableElements.forEach((element) => {
944
+ if (element === window) {
945
+ window.addEventListener('scroll', updateOnScroll);
946
+ window.addEventListener('resize', updateOnScroll);
947
+ } else {
948
+ element.addEventListener('scroll', updateOnScroll);
949
+ }
950
+ });
951
+
952
+ const cleanup = () => {
953
+ scrollableElements.forEach((element) => {
954
+ if (element === window) {
955
+ window.removeEventListener('scroll', updateOnScroll);
956
+ window.removeEventListener('resize', updateOnScroll);
957
+ } else {
958
+ /** @type {Element} */ (element).removeEventListener('scroll', updateOnScroll);
959
+ }
960
+ });
961
+ };
962
+
963
+ return { cleanup };
964
+ };
965
+
966
+ /**
967
+ * @param {HighlightManager} highlightManager
968
+ * @param {MutationObserver | undefined} stegaObserver
969
+ */
970
+ const setupObservers = (highlightManager, stegaObserver) => {
971
+ const resizeObserver = new ResizeObserver(() => {
972
+ highlightManager.updateAllHighlights();
973
+ });
974
+
975
+ /**
976
+ * @param {Element} element
977
+ */
978
+ const observeElementForResize = (element) => {
979
+ resizeObserver.observe(element);
980
+ };
981
+
982
+ // Observe existing elements
983
+ highlightManager.elements.forEach(observeElementForResize);
984
+ resizeObserver.observe(document.documentElement);
985
+
986
+ // Create highlight observer to watch for new elements with source attributes
987
+ const highlightObserver = new MutationObserver((mutations) => {
988
+ mutations.forEach((mutation) => {
989
+ if (mutation.type === 'attributes' && mutation.attributeName === SOURCE_ATTRIBUTE) {
990
+ const target = /** @type {HTMLElement} */ (mutation.target);
991
+ if (target.hasAttribute(SOURCE_ATTRIBUTE)) {
992
+ highlightManager.removeHighlightForElement(target);
993
+ highlightManager.createHighlightForElement(target);
994
+ observeElementForResize(target);
995
+ } else {
996
+ highlightManager.removeHighlightForElement(target);
997
+ }
998
+ }
999
+
1000
+ if (mutation.type === 'childList') {
1001
+ mutation.addedNodes.forEach((node) => {
1002
+ if (node.nodeType === Node.ELEMENT_NODE) {
1003
+ const element = /** @type {Element} */ (node);
1004
+ // Check if the added element has source attribute
1005
+ if (element.hasAttribute(SOURCE_ATTRIBUTE) && element instanceof HTMLElement) {
1006
+ highlightManager.createHighlightForElement(element);
1007
+ observeElementForResize(element);
1008
+ }
1009
+ // Check all child elements for source attributes
1010
+ const elementsWithSource = element.querySelectorAll(`[${SOURCE_ATTRIBUTE}]`);
1011
+ Array.from(elementsWithSource).forEach((childElement) => {
1012
+ if (childElement instanceof HTMLElement) {
1013
+ highlightManager.createHighlightForElement(childElement);
1014
+ observeElementForResize(childElement);
1015
+ }
1016
+ });
1017
+ }
1018
+ });
1019
+
1020
+ mutation.removedNodes.forEach((node) => {
1021
+ if (node.nodeType === Node.ELEMENT_NODE) {
1022
+ const element = /** @type {Element} */ (node);
1023
+ highlightManager.removeHighlightForElement(element);
1024
+ element.querySelectorAll(`[${SOURCE_ATTRIBUTE}]`).forEach((childElement) => {
1025
+ highlightManager.removeHighlightForElement(childElement);
1026
+ });
1027
+
1028
+ // If a tracked media original (or its ancestor) was removed —
1029
+ // typically when the host framework re-renders after save and
1030
+ // unmounts the previous element — drop our matching injection
1031
+ // so we don't end up with duplicate previews next to the new one.
1032
+ // This applies to placeholders too: once the framework owns
1033
+ // the layout for a cleared field (and likely renders its own
1034
+ // empty state), our placeholder would just stack next to it.
1035
+ originalToInjection.forEach((injection, original) => {
1036
+ if (element === original || element.contains(original)) {
1037
+ untrackInjection(injection);
1038
+ injection.remove();
1039
+ } else if (element === injection || element.contains(injection)) {
1040
+ // Our injection itself was removed (e.g. parent unmounted)
1041
+ untrackInjection(injection);
1042
+ }
1043
+ });
1044
+ }
1045
+ });
1046
+ }
1047
+ });
1048
+ });
1049
+
1050
+ highlightObserver.observe(document, {
1051
+ childList: true,
1052
+ subtree: true,
1053
+ attributes: true,
1054
+ attributeFilter: [SOURCE_ATTRIBUTE],
1055
+ });
1056
+
1057
+ return {
1058
+ resizeObserver,
1059
+ highlightObserver,
1060
+ stegaObserver,
1061
+ };
1062
+ };
1063
+
1064
+ /**
1065
+ * @param {HighlightManager} highlightManager
1066
+ */
1067
+ const setupEventHandlers = (highlightManager) => {
1068
+ /**
1069
+ * @param {HTMLElement} el
1070
+ * @param {string | null} url
1071
+ * @param {string} [mime]
1072
+ */
1073
+ const setMediaElement = (el, url, mime) => {
1074
+ const original = injectionToOriginal.get(el) ?? el;
1075
+ const injection = originalToInjection.get(original) ?? null;
1076
+
1077
+ const removeInjection = () => {
1078
+ if (!injection) return;
1079
+ // Move the source attribute back to the original so highlights track it again
1080
+ const sourceAttr = injection.getAttribute(SOURCE_ATTRIBUTE);
1081
+ if (sourceAttr) original.setAttribute(SOURCE_ATTRIBUTE, sourceAttr);
1082
+ untrackInjection(injection);
1083
+ injection.remove();
1084
+ };
1085
+
1086
+ if (!url) {
1087
+ // Capture rect from whatever's currently visible before any DOM
1088
+ // changes, so the placeholder can be sized to match.
1089
+ const visibleEl = injection ?? original;
1090
+ const rect = visibleEl.getBoundingClientRect();
1091
+
1092
+ // removeInjection moves the source attr back to original, so we read
1093
+ // it after.
1094
+ removeInjection();
1095
+
1096
+ const sourceAttr = original.getAttribute(SOURCE_ATTRIBUTE);
1097
+ if (!sourceAttr) {
1098
+ original.style.display = 'none';
1099
+ return;
1100
+ }
1101
+
1102
+ // Hide the host-framework-managed original. We deliberately keep
1103
+ // its `src` attribute around so `resolveMediaUrl` can still derive
1104
+ // the backend origin when the user later picks a new media file
1105
+ // with a relative URL.
1106
+ original.style.display = 'none';
1107
+
1108
+ // Move the source attr onto a placeholder div so the highlight
1109
+ // system keeps tracking the spot and the user can click to re-add
1110
+ // media.
1111
+ original.removeAttribute(SOURCE_ATTRIBUTE);
1112
+ const placeholder = createMediaPlaceholder(sourceAttr, rect);
1113
+ original.parentNode?.insertBefore(placeholder, original.nextSibling);
1114
+ trackInjection(original, placeholder);
1115
+ return;
1116
+ }
1117
+
1118
+ const desiredTag = mime?.startsWith('image/')
1119
+ ? 'IMG'
1120
+ : mime?.startsWith('video/')
1121
+ ? 'VIDEO'
1122
+ : mime?.startsWith('audio/')
1123
+ ? 'AUDIO'
1124
+ : null;
1125
+
1126
+ // If the rendered element already shows the same media file as the
1127
+ // form value, do nothing. Compare by filename so we're agnostic to
1128
+ // URL representation — the host framework may have rendered a proxy
1129
+ // URL (e.g. Next.js's "/_next/image?url=...") that wouldn't match a
1130
+ // raw form-value URL string-wise but resolves to the same asset.
1131
+ const active = injection ?? original;
1132
+ const newFilename = getMediaFilename(url);
1133
+ const currentFilename = getMediaFilename(active.getAttribute('src') ?? '');
1134
+ const tagAlreadyMatches = !desiredTag || active.tagName === desiredTag;
1135
+ if (tagAlreadyMatches && newFilename && newFilename === currentFilename) {
1136
+ active.style.display = '';
1137
+ return;
1138
+ }
1139
+
1140
+ // Resolve relative form-value URLs against the existing src's origin
1141
+ // so the swap targets the Strapi backend, not the iframe origin.
1142
+ const resolvedUrl = resolveMediaUrl(original, url);
1143
+ const safeUrl = sanitizeMediaUrl(resolvedUrl);
1144
+ if (!safeUrl) {
1145
+ return;
1146
+ }
1147
+
1148
+ // No mime info — fall back to updating whatever's active
1149
+ if (!desiredTag) {
1150
+ if (active.getAttribute('src') !== safeUrl) {
1151
+ active.setAttribute('src', safeUrl);
1152
+ }
1153
+ active.style.display = '';
1154
+ return;
1155
+ }
1156
+
1157
+ // Original's tag matches: restore it (removing any previous injection)
1158
+ if (original.tagName === desiredTag) {
1159
+ removeInjection();
1160
+ if (original.getAttribute('src') !== safeUrl) {
1161
+ original.setAttribute('src', safeUrl);
1162
+ }
1163
+ original.style.display = '';
1164
+ return;
1165
+ }
1166
+
1167
+ // Existing injection of the right tag: just update its src
1168
+ if (injection && injection.tagName === desiredTag) {
1169
+ if (injection.getAttribute('src') !== safeUrl) {
1170
+ injection.setAttribute('src', safeUrl);
1171
+ }
1172
+ return;
1173
+ }
1174
+
1175
+ // Need a fresh injection of the correct tag
1176
+ removeInjection();
1177
+
1178
+ const newInjection = /** @type {HTMLElement} */ (document.createElement(desiredTag));
1179
+ // Mirror the original's attributes so styling/classes carry over.
1180
+ // Skip src/srcset — those carry the *previous* media's URL and would
1181
+ // make the new element try to load it (a video element fed an image
1182
+ // URL renders a permanent "No supported format" error). We set src
1183
+ // explicitly below.
1184
+ // For audio, skip visual-sizing attributes too — image/video classes
1185
+ // (e.g. aspect-square, h-96) don't suit a horizontal audio control
1186
+ // and produce a giant empty box around the player.
1187
+ const isAudio = desiredTag === 'AUDIO';
1188
+ const skipForAudio = new Set(['class', 'style', 'width', 'height']);
1189
+ Array.from(original.attributes).forEach((attr) => {
1190
+ if (attr.name === 'id' || attr.name === 'src' || attr.name === 'srcset') return;
1191
+ if (isAudio && skipForAudio.has(attr.name)) return;
1192
+ newInjection.setAttribute(attr.name, attr.value);
1193
+ });
1194
+ newInjection.setAttribute('src', safeUrl);
1195
+ if (desiredTag === 'VIDEO' || desiredTag === 'AUDIO') {
1196
+ newInjection.setAttribute('controls', '');
1197
+ }
1198
+ if (!isAudio) {
1199
+ newInjection.style.cssText = original.style.cssText;
1200
+ }
1201
+ newInjection.style.display = '';
1202
+
1203
+ // Hide the host-framework-managed original and let highlights track our injection
1204
+ original.style.display = 'none';
1205
+ original.removeAttribute(SOURCE_ATTRIBUTE);
1206
+ original.parentNode?.insertBefore(newInjection, original.nextSibling);
1207
+ trackInjection(original, newInjection);
1208
+ };
1209
+
1210
+ /**
1211
+ * @param {MessageEvent} event
1212
+ */
1213
+ const handleMessage = (event) => {
1214
+ if (!event.data?.type) return;
1215
+ if (event.source !== window.parent || event.origin !== parentOrigin) return;
1216
+
1217
+ // The user typed in an input, reflect the change in the preview
1218
+ if (event.data.type === INTERNAL_EVENTS.STRAPI_FIELD_CHANGE) {
1219
+ const { field, value } = event.data.payload;
1220
+ if (!field) return;
1221
+
1222
+ const matchedElements = /** @type {HTMLElement[]} */ (
1223
+ Array.from(getElementsByPath(field)).filter((el) => el instanceof HTMLElement)
1224
+ );
1225
+
1226
+ // A cleared media field is represented by a placeholder div sitting
1227
+ // where the (hidden or unmounted) original used to be. Treat it like
1228
+ // a media element here so the next change routes through
1229
+ // setMediaElement and tears the placeholder down. Standalone
1230
+ // placeholders (those whose original was unmounted by the host
1231
+ // framework after save) are not in the injection maps but still
1232
+ // need to be matchable.
1233
+ /**
1234
+ * @param {Element} el
1235
+ */
1236
+ const isMediaTarget = (el) =>
1237
+ isMediaElement(el) ||
1238
+ injectionToOriginal.has(/** @type {HTMLElement} */ (el)) ||
1239
+ el.hasAttribute(PLACEHOLDER_ATTRIBUTE);
1240
+
1241
+ // Multi-media field: value is an array of media items. After the
1242
+ // server change, every item shares `path=<field>`, so we map array
1243
+ // items to media elements in DOM order. Removing items reuses
1244
+ // existing elements (extras get cleared into placeholders); adding
1245
+ // items synthesizes new slots by cloning the last rendered element,
1246
+ // since the host framework only re-renders the gallery on save.
1247
+ if (Array.isArray(value)) {
1248
+ const mediaEls = matchedElements.filter(isMediaTarget);
1249
+ if (mediaEls.length > 0 && mediaEls.length < value.length) {
1250
+ const lastEl = mediaEls[mediaEls.length - 1];
1251
+ // Walk up until we reach an ancestor whose parent lays out
1252
+ // children as siblings (flex/grid). Record indices so we can
1253
+ // re-find the media element inside each cloned wrapper.
1254
+ /** @type {number[]} */
1255
+ const indices = [];
1256
+ /** @type {HTMLElement} */
1257
+ let template = lastEl;
1258
+ let parent = template.parentElement;
1259
+ const SIBLING_LAYOUTS = new Set(['flex', 'inline-flex', 'grid', 'inline-grid']);
1260
+ const MAX_DEPTH = 5;
1261
+ for (let depth = 0; parent && depth < MAX_DEPTH; depth++) {
1262
+ if (SIBLING_LAYOUTS.has(window.getComputedStyle(parent).display)) {
1263
+ break;
1264
+ }
1265
+ indices.push(Array.prototype.indexOf.call(parent.children, template));
1266
+ template = parent;
1267
+ parent = parent.parentElement;
1268
+ }
1269
+
1270
+ /** @type {HTMLElement} */
1271
+ let insertAfter = template;
1272
+ for (let i = mediaEls.length; i < value.length; i++) {
1273
+ const wrapperClone = /** @type {HTMLElement} */ (template.cloneNode(true));
1274
+ /** @type {HTMLElement} */
1275
+ let cloneMedia = wrapperClone;
1276
+ for (let j = indices.length - 1; j >= 0; j--) {
1277
+ cloneMedia = /** @type {HTMLElement} */ (cloneMedia.children[indices[j]]);
1278
+ }
1279
+ cloneMedia.setAttribute(CLONE_ATTRIBUTE, '');
1280
+ cloneMedia.removeAttribute('id');
1281
+ if (wrapperClone !== cloneMedia) {
1282
+ wrapperClone.setAttribute(CLONE_WRAPPER_ATTRIBUTE, '');
1283
+ }
1284
+ insertAfter.parentNode?.insertBefore(wrapperClone, insertAfter.nextSibling);
1285
+ mediaEls.push(cloneMedia);
1286
+ insertAfter = wrapperClone;
1287
+ }
1288
+ }
1289
+ mediaEls.forEach((el, index) => {
1290
+ const item = value[index];
1291
+ if (item && typeof item === 'object') {
1292
+ const url = item.url;
1293
+ const mime = item.mime;
1294
+ setMediaElement(el, url || null, mime);
1295
+ } else {
1296
+ setMediaElement(el, null);
1297
+ }
1298
+ });
1299
+ } else {
1300
+ matchedElements.forEach((element) => {
1301
+ if (isMediaTarget(element)) {
1302
+ const url = typeof value === 'object' && value !== null ? value.url : value;
1303
+ const mime = typeof value === 'object' && value !== null ? value.mime : undefined;
1304
+ setMediaElement(element, url || null, mime);
1305
+ } else {
1306
+ element.textContent = value == null ? '' : String(value);
1307
+ }
1308
+ });
1309
+ }
1310
+
1311
+ // Handle nested media asset fields (caption, alt, etc.)
1312
+ // These are identified by model=plugin::upload.file in the source attribute
1313
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
1314
+ const allSourceElements = document.querySelectorAll(`[${SOURCE_ATTRIBUTE}]`);
1315
+
1316
+ allSourceElements.forEach((element) => {
1317
+ const sourceAttr = element.getAttribute(SOURCE_ATTRIBUTE);
1318
+ if (!sourceAttr) return;
1319
+
1320
+ // Parse the source attribute as URL search params
1321
+ const params = new URLSearchParams(sourceAttr);
1322
+ const model = params.get('model');
1323
+ const elementPath = params.get('path');
1324
+
1325
+ // Only process media asset fields
1326
+ if (model !== 'plugin::upload.file' || !elementPath) return;
1327
+
1328
+ // Check if this element's path starts with the field path (e.g., "hero.caption" starts with "hero")
1329
+ if (elementPath.startsWith(`${field}.`)) {
1330
+ // Extract the property name (e.g., "caption" from "hero.caption")
1331
+ const propertyName = elementPath.slice(field.length + 1);
1332
+ const propertyValue = value[propertyName];
1333
+ if (element instanceof HTMLElement && propertyValue !== undefined) {
1334
+ element.textContent = propertyValue == null ? '' : String(propertyValue);
1335
+ }
1336
+ }
1337
+ });
1338
+ }
1339
+
1340
+ // Update highlight dimensions since the new text content may affect them
1341
+ highlightManager.updateAllHighlights();
1342
+ return;
1343
+ }
1344
+
1345
+ // The user focused a new input, update the highlights in the preview
1346
+ if (event.data.type === INTERNAL_EVENTS.STRAPI_FIELD_FOCUS) {
1347
+ const { field } = event.data.payload;
1348
+ if (!field) return;
1349
+
1350
+ // Clear existing focused highlights
1351
+ highlightManager.focusedHighlights.forEach((highlight) => {
1352
+ highlight.classList.remove('strapi-highlight-focused');
1353
+ });
1354
+ highlightManager.focusedHighlights.length = 0;
1355
+
1356
+ // Set new focused field and highlight matching groups
1357
+ highlightManager.setFocusedField(field);
1358
+ const matchingGroups = highlightManager.findGroupForPath(field);
1359
+ matchingGroups.forEach((group, index) => {
1360
+ if (index === 0) {
1361
+ const first = group.elements.values().next().value;
1362
+ first?.scrollIntoView({ behavior: 'smooth', block: 'center' });
1363
+ }
1364
+ group.highlight.classList.add('strapi-highlight-focused');
1365
+ highlightManager.focusedHighlights.push(group.highlight);
1366
+ });
1367
+ return;
1368
+ }
1369
+
1370
+ // The user is no longer focusing an input, remove the highlights
1371
+ if (event.data.type === INTERNAL_EVENTS.STRAPI_FIELD_BLUR) {
1372
+ const { field } = event.data.payload;
1373
+ if (field !== highlightManager.getFocusedField()) return;
1374
+
1375
+ highlightManager.focusedHighlights.forEach((highlight) => {
1376
+ highlight.classList.remove('strapi-highlight-focused');
1377
+ });
1378
+ highlightManager.focusedHighlights.length = 0;
1379
+ highlightManager.setFocusedField(null);
1380
+ }
1381
+ };
1382
+
1383
+ window.addEventListener('message', handleMessage);
1384
+
1385
+ // Add the message handler to the cleanup list
1386
+ const messageEventListener = {
1387
+ element: window,
1388
+ type: /** @type {keyof HTMLElementEventMap} */ ('message'),
1389
+ handler: /** @type {EventListener} */ (handleMessage),
1390
+ };
1391
+
1392
+ return [...highlightManager.eventListeners, messageEventListener];
1393
+ };
1394
+
1395
+ /**
1396
+ * @param {HTMLElement} overlay
1397
+ * @param {ReturnType<typeof setupObservers>} observers
1398
+ * @param {ReturnType<typeof setupScrollManagement>} scrollManager
1399
+ * @param {EventListenersList} eventHandlers
1400
+ * @param {HighlightManager} highlightManager
1401
+ */
1402
+ const createCleanupSystem = (
1403
+ overlay,
1404
+ observers,
1405
+ scrollManager,
1406
+ eventHandlers,
1407
+ highlightManager
1408
+ ) => {
1409
+ win.__strapi_previewCleanup = () => {
1410
+ observers.resizeObserver.disconnect();
1411
+ observers.highlightObserver.disconnect();
1412
+ observers.stegaObserver?.disconnect();
1413
+
1414
+ // Clean up scroll listeners
1415
+ scrollManager.cleanup();
1416
+
1417
+ // Clear all pending click timeouts
1418
+ highlightManager.clearAllPendingClicks();
1419
+
1420
+ // Remove highlight event listeners
1421
+ eventHandlers.forEach(({ element, type, handler }) => {
1422
+ element.removeEventListener(type, handler);
1423
+ });
1424
+
1425
+ // Clean up CSS styles
1426
+ const existingStyles = document.getElementById(HIGHLIGHT_STYLES_ID);
1427
+ if (existingStyles) {
1428
+ existingStyles.remove();
1429
+ }
1430
+
1431
+ overlay.remove();
1432
+ };
1433
+ };
1434
+
1435
+ /* -----------------------------------------------------------------------------------------------
1436
+ * Orchestration
1437
+ * ---------------------------------------------------------------------------------------------*/
1438
+
1439
+ setupStegaDOMObserver()
1440
+ .catch(() => undefined)
1441
+ .then((stegaObserver) => {
1442
+ createHighlightStyles();
1443
+ const overlay = createOverlaySystem();
1444
+ const highlightManager = createHighlightManager(overlay);
1445
+ const observers = setupObservers(highlightManager, stegaObserver);
1446
+ const scrollManager = setupScrollManagement(highlightManager);
1447
+ const eventHandlers = setupEventHandlers(highlightManager);
1448
+ createCleanupSystem(overlay, observers, scrollManager, eventHandlers, highlightManager);
1449
+ });
1450
+ }