pageflow 17.1.0 → 17.2.0

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 (302) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +319 -345
  3. data/README.md +3 -4
  4. data/admins/pageflow/accounts.rb +6 -6
  5. data/admins/pageflow/entry.rb +10 -8
  6. data/admins/pageflow/membership.rb +11 -11
  7. data/admins/pageflow/translations.rb +5 -5
  8. data/app/assets/images/pageflow/admin/icons/comment.svg +1 -0
  9. data/app/assets/javascripts/pageflow/dist/ui.js +1350 -630
  10. data/app/assets/javascripts/pageflow/editor/vendor.js +0 -1
  11. data/app/assets/javascripts/pageflow/ui.js +0 -1
  12. data/app/assets/stylesheets/pageflow/admin/entry_comments_indicator.scss +45 -0
  13. data/app/assets/stylesheets/pageflow/admin/permalink_input.scss +8 -1
  14. data/app/assets/stylesheets/pageflow/admin.scss +1 -0
  15. data/app/assets/stylesheets/pageflow/animations/spin.scss +9 -0
  16. data/app/assets/stylesheets/pageflow/animations.scss +1 -0
  17. data/app/assets/stylesheets/pageflow/editor/background_positioning.scss +21 -5
  18. data/app/assets/stylesheets/pageflow/editor/base.scss +9 -1
  19. data/app/assets/stylesheets/pageflow/editor/drop_down_button.scss +13 -1
  20. data/app/assets/stylesheets/pageflow/editor/edit_configuration_view.scss +5 -0
  21. data/app/assets/stylesheets/pageflow/editor/file_folders.scss +127 -0
  22. data/app/assets/stylesheets/pageflow/editor/file_meta_data.scss +0 -1
  23. data/app/assets/stylesheets/pageflow/editor/file_meta_data_overlay.scss +45 -0
  24. data/app/assets/stylesheets/pageflow/editor/file_preview.scss +138 -0
  25. data/app/assets/stylesheets/pageflow/editor/file_references.scss +68 -0
  26. data/app/assets/stylesheets/pageflow/editor/file_settings_dialog.scss +1 -1
  27. data/app/assets/stylesheets/pageflow/editor/file_stage_icons.scss +17 -0
  28. data/app/assets/stylesheets/pageflow/editor/file_stages.scss +25 -32
  29. data/app/assets/stylesheets/pageflow/editor/file_thumbnails.scss +31 -48
  30. data/app/assets/stylesheets/pageflow/editor/file_type_pills.scss +73 -0
  31. data/app/assets/stylesheets/pageflow/editor/files.scss +230 -50
  32. data/app/assets/stylesheets/pageflow/editor/filtered_files.scss +142 -14
  33. data/app/assets/stylesheets/pageflow/editor/folder_breadcrumb.scss +40 -0
  34. data/app/assets/stylesheets/pageflow/editor/info_box.scss +12 -0
  35. data/app/assets/stylesheets/pageflow/editor/inputs/edit_defaults_button.scss +15 -0
  36. data/app/assets/stylesheets/pageflow/editor/inputs.scss +1 -0
  37. data/app/assets/stylesheets/pageflow/editor/list.scss +88 -38
  38. data/app/assets/stylesheets/pageflow/editor/manage_files.scss +9 -0
  39. data/app/assets/stylesheets/pageflow/editor/menu.scss +14 -0
  40. data/app/assets/stylesheets/pageflow/editor/move_to_folder_dialog.scss +83 -0
  41. data/app/assets/stylesheets/pageflow/mixins/pageflow.scss +9 -5
  42. data/app/assets/stylesheets/pageflow/ui/color_picker.scss +230 -0
  43. data/app/assets/stylesheets/pageflow/ui/forms.scss +34 -4
  44. data/app/assets/stylesheets/pageflow/ui/functions.scss +12 -2
  45. data/app/assets/stylesheets/pageflow/ui/input/color_input.scss +9 -16
  46. data/app/assets/stylesheets/pageflow/ui/input/text_area_input.scss +15 -0
  47. data/app/assets/stylesheets/pageflow/ui/overrides.scss +8 -0
  48. data/app/assets/stylesheets/pageflow/ui/properties.scss +29 -0
  49. data/app/assets/stylesheets/pageflow/ui/tabs_view.scss +8 -3
  50. data/app/assets/stylesheets/pageflow/ui.scss +4 -2
  51. data/app/controllers/pageflow/editor/entries_controller.rb +1 -0
  52. data/app/controllers/pageflow/editor/file_folders_controller.rb +80 -0
  53. data/app/controllers/pageflow/editor/file_import_controller.rb +1 -0
  54. data/app/controllers/pageflow/editor/files_controller.rb +15 -6
  55. data/app/controllers/pageflow/editor/oembeds_controller.rb +21 -0
  56. data/app/controllers/pageflow/entries_controller.rb +17 -13
  57. data/app/controllers/pageflow/review/comment_thread_reads_controller.rb +30 -0
  58. data/app/controllers/pageflow/review/comment_threads_controller.rb +60 -0
  59. data/app/controllers/pageflow/review/comments_controller.rb +53 -0
  60. data/app/controllers/pageflow/revisions_controller.rb +4 -1
  61. data/app/helpers/pageflow/admin/entries_helper.rb +40 -0
  62. data/app/helpers/pageflow/admin/widgets_helper.rb +2 -2
  63. data/app/helpers/pageflow/common_entry_seed_helper.rb +5 -3
  64. data/app/helpers/pageflow/editor/config_helper.rb +3 -1
  65. data/app/helpers/pageflow/entries_helper.rb +24 -42
  66. data/app/helpers/pageflow/entry_json_seed_helper.rb +4 -4
  67. data/app/helpers/pageflow/public_i18n_helper.rb +9 -8
  68. data/app/helpers/pageflow/structured_data_helper.rb +4 -1
  69. data/app/helpers/pageflow/themes_helper.rb +1 -3
  70. data/app/helpers/pageflow/widgets_helper.rb +2 -1
  71. data/app/inputs/pageflow_permalink_input.rb +6 -2
  72. data/app/models/concerns/pageflow/auto_generated_perma_id.rb +11 -4
  73. data/app/models/concerns/pageflow/reusable_file.rb +1 -1
  74. data/app/models/concerns/pageflow/serialization_blacklist.rb +2 -4
  75. data/app/models/concerns/pageflow/translated_attributes.rb +101 -0
  76. data/app/models/concerns/pageflow/uploadable_file.rb +1 -1
  77. data/app/models/pageflow/comment.rb +30 -0
  78. data/app/models/pageflow/comment_thread.rb +65 -0
  79. data/app/models/pageflow/comment_thread_read.rb +22 -0
  80. data/app/models/pageflow/customized_theme.rb +3 -2
  81. data/app/models/pageflow/draft_entry.rb +20 -8
  82. data/app/models/pageflow/entry.rb +2 -0
  83. data/app/models/pageflow/entry_at_revision.rb +9 -1
  84. data/app/models/pageflow/entry_comment_summary.rb +114 -0
  85. data/app/models/pageflow/file_folder.rb +74 -0
  86. data/app/models/pageflow/file_reuse.rb +3 -2
  87. data/app/models/pageflow/file_usage.rb +8 -0
  88. data/app/models/pageflow/image_file.rb +24 -20
  89. data/app/models/pageflow/image_file_url_templates.rb +3 -3
  90. data/app/models/pageflow/legal_info.rb +38 -0
  91. data/app/models/pageflow/nested_revision_component_copy.rb +1 -1
  92. data/app/models/pageflow/permalink.rb +1 -0
  93. data/app/models/pageflow/revision.rb +6 -2
  94. data/app/models/pageflow/site.rb +10 -0
  95. data/app/models/pageflow/used_file.rb +8 -2
  96. data/app/models/pageflow/widget.rb +2 -2
  97. data/app/views/pageflow/audio_files/_audio_file.json.jbuilder +0 -1
  98. data/app/views/pageflow/editor/config/_seeds.json.jbuilder +4 -2
  99. data/app/views/pageflow/editor/entries/_entry.json.jbuilder +2 -1
  100. data/app/views/pageflow/editor/entries/seed.json.erb +1 -0
  101. data/app/views/pageflow/editor/file_folders/_file_folder.json.jbuilder +5 -0
  102. data/app/views/pageflow/editor/file_folders/create.json.jbuilder +1 -0
  103. data/app/views/pageflow/editor/file_folders/index.json.jbuilder +3 -0
  104. data/app/views/pageflow/editor/files/_file.json.jbuilder +2 -1
  105. data/app/views/pageflow/editor/image_files/_image_file.json.jbuilder +3 -0
  106. data/app/views/pageflow/editor/video_files/_video_file.json.jbuilder +3 -0
  107. data/app/views/pageflow/files/_file.json.jbuilder +2 -0
  108. data/app/views/pageflow/image_files/_image_file.json.jbuilder +0 -1
  109. data/app/views/pageflow/review/comment_threads/_comment_thread.json.jbuilder +20 -0
  110. data/app/views/pageflow/review/comment_threads/create.json.jbuilder +2 -0
  111. data/app/views/pageflow/review/comment_threads/index.json.jbuilder +13 -0
  112. data/app/views/pageflow/review/comments/_comment.json.jbuilder +13 -0
  113. data/app/views/pageflow/review/comments/create.json.jbuilder +1 -0
  114. data/app/views/pageflow/structured_data/_entry.json.jbuilder +2 -3
  115. data/app/views/pageflow/video_files/_video_file.json.jbuilder +0 -1
  116. data/config/locales/de.yml +152 -14
  117. data/config/locales/en.yml +150 -14
  118. data/config/routes.rb +14 -0
  119. data/db/migrate/20140418225525_setup_schema.rb +2 -2
  120. data/db/migrate/20251015000000_add_structured_data_type_name_to_revisions.rb +5 -0
  121. data/db/migrate/20251029142849_convert_legacy_social_embeds_to_unified_social_embed.rb +38 -0
  122. data/db/migrate/20251119083347_fix_main_storyline_positions.rb +16 -0
  123. data/db/migrate/20251119083348_ensure_scrolled_entries_have_excursion_storyline.rb +15 -0
  124. data/db/migrate/20260303000000_add_layout_version_to_revisions.rb +6 -0
  125. data/db/migrate/20260323000000_create_comment_threads_and_comments.rb +30 -0
  126. data/db/migrate/20260414000000_add_subject_range_to_comment_threads.rb +5 -0
  127. data/db/migrate/20260415000000_add_section_perma_id_to_comment_threads.rb +5 -0
  128. data/db/migrate/20260727000000_add_quote_to_comments.rb +5 -0
  129. data/db/migrate/20260728000000_create_file_folders.rb +17 -0
  130. data/db/migrate/20260805000000_add_attribute_translations_to_sites.rb +5 -0
  131. data/db/migrate/20260806000000_add_edited_at_to_comments.rb +5 -0
  132. data/db/migrate/20260810000000_create_pageflow_scrolled_lottie_files.rb +24 -0
  133. data/db/migrate/20260817000000_create_comment_thread_reads.rb +16 -0
  134. data/db/migrate/20260819000000_add_unread_comments_since_at_to_users.rb +18 -0
  135. data/entry_types/paged/app/assets/javascripts/pageflow_paged/dist/editor.js +8665 -3890
  136. data/entry_types/paged/app/assets/javascripts/pageflow_paged/dist/frontend.js +979 -3571
  137. data/entry_types/paged/app/assets/javascripts/pageflow_paged/dist/react-client.js +20199 -28120
  138. data/entry_types/paged/app/assets/javascripts/pageflow_paged/dist/react-server.js +18652 -26584
  139. data/entry_types/scrolled/app/assets/stylesheets/pageflow_scrolled/ui.scss +2 -0
  140. data/entry_types/scrolled/app/controllers/pageflow_scrolled/editor/content_elements_controller.rb +33 -3
  141. data/entry_types/scrolled/app/controllers/pageflow_scrolled/editor/sections_controller.rb +2 -3
  142. data/entry_types/scrolled/app/controllers/pageflow_scrolled/entries_controller.rb +21 -1
  143. data/entry_types/scrolled/app/helpers/pageflow_scrolled/cache_helper.rb +3 -3
  144. data/entry_types/scrolled/app/helpers/pageflow_scrolled/editor/entry_json_seed_helper.rb +7 -5
  145. data/entry_types/scrolled/app/helpers/pageflow_scrolled/editor/seed_html_helper.rb +5 -2
  146. data/entry_types/scrolled/app/helpers/pageflow_scrolled/entry_json_seed_helper.rb +13 -5
  147. data/entry_types/scrolled/app/helpers/pageflow_scrolled/i18n_helper.rb +28 -13
  148. data/entry_types/scrolled/app/helpers/pageflow_scrolled/packs_helper.rb +26 -11
  149. data/entry_types/scrolled/app/helpers/pageflow_scrolled/react_server_side_rendering_helper.rb +2 -2
  150. data/entry_types/scrolled/app/helpers/pageflow_scrolled/sprockets_helper.rb +16 -0
  151. data/entry_types/scrolled/app/helpers/pageflow_scrolled/themes_helper.rb +174 -3
  152. data/entry_types/scrolled/app/models/pageflow_scrolled/content_element.rb +49 -7
  153. data/entry_types/scrolled/app/models/pageflow_scrolled/lottie_file.rb +6 -0
  154. data/entry_types/scrolled/app/models/pageflow_scrolled/lottie_file_url_templates.rb +16 -0
  155. data/entry_types/scrolled/app/models/pageflow_scrolled/storyline.rb +1 -0
  156. data/entry_types/scrolled/app/views/pageflow_scrolled/entries/show.html.erb +13 -12
  157. data/entry_types/scrolled/app/views/pageflow_scrolled/entry_json_seed/_entry.json.jbuilder +30 -10
  158. data/entry_types/scrolled/app/views/pageflow_scrolled/entry_json_seed/_theme.json.jbuilder +9 -0
  159. data/entry_types/scrolled/app/views/pageflow_scrolled/favicons/_entry.html.erb +1 -1
  160. data/entry_types/scrolled/config/configuration_schemas/pageflow-scrolled.json +414 -0
  161. data/entry_types/scrolled/config/locales/de.yml +512 -65
  162. data/entry_types/scrolled/config/locales/en.yml +518 -52
  163. data/entry_types/scrolled/lib/pageflow_scrolled/additional_seed_data.rb +2 -2
  164. data/entry_types/scrolled/lib/pageflow_scrolled/configuration.rb +51 -6
  165. data/entry_types/scrolled/lib/pageflow_scrolled/configuration_schemas.rb +91 -0
  166. data/entry_types/scrolled/lib/pageflow_scrolled/engine.rb +2 -1
  167. data/entry_types/scrolled/lib/pageflow_scrolled/entry_file_reference_locations.rb +71 -0
  168. data/entry_types/scrolled/lib/pageflow_scrolled/entry_structured_data_types/faq_page.rb +58 -0
  169. data/entry_types/scrolled/lib/pageflow_scrolled/file_reference_locations.rb +69 -0
  170. data/entry_types/scrolled/lib/pageflow_scrolled/plugin.rb +205 -22
  171. data/entry_types/scrolled/lib/pageflow_scrolled/react_widget_type.rb +2 -4
  172. data/entry_types/scrolled/lib/pageflow_scrolled/seeds.rb +56 -36
  173. data/entry_types/scrolled/lib/pageflow_scrolled/theme_options_default_scale.rb +35 -0
  174. data/entry_types/scrolled/lib/pageflow_scrolled.rb +13 -0
  175. data/entry_types/scrolled/lib/tasks/pageflow_scrolled/storybook.rake +19 -1
  176. data/entry_types/scrolled/package/config/configurationSchemas.js +70 -0
  177. data/entry_types/scrolled/package/config/rollup/configurationSchemas.js +19 -0
  178. data/entry_types/scrolled/package/config/webpack.js +72 -0
  179. data/entry_types/scrolled/package/contentElements/hotspots-frontend.css +1 -1
  180. data/entry_types/scrolled/package/contentElements/hotspots-frontend.js +1181 -915
  181. data/entry_types/scrolled/package/contentElements/inlineBeforeAfter-frontend.css +1 -0
  182. data/entry_types/scrolled/package/contentElements/inlineBeforeAfter-frontend.js +219 -0
  183. data/entry_types/scrolled/package/contentElements/lottieAnimation-frontend.css +1 -0
  184. data/entry_types/scrolled/package/contentElements/lottieAnimation-frontend.js +244 -0
  185. data/entry_types/scrolled/package/contentElements/socialEmbed-frontend.css +1 -0
  186. data/entry_types/scrolled/package/contentElements/socialEmbed-frontend.js +823 -0
  187. data/entry_types/scrolled/package/contentElements/tikTokEmbed-frontend.js +71 -21
  188. data/entry_types/scrolled/package/contentElements/twitterEmbed-frontend.css +1 -0
  189. data/entry_types/scrolled/package/contentElements/twitterEmbed-frontend.js +277 -0
  190. data/entry_types/scrolled/package/contentElements/videoEmbed-frontend.css +1 -0
  191. data/entry_types/scrolled/package/contentElements/videoEmbed-frontend.js +244 -0
  192. data/entry_types/scrolled/package/contentElements-editor.js +1915 -1097
  193. data/entry_types/scrolled/package/contentElements-frontend.css +1 -1
  194. data/entry_types/scrolled/package/contentElements-frontend.js +3093 -1679
  195. data/entry_types/scrolled/package/editor.css +1 -0
  196. data/entry_types/scrolled/package/editor.js +8667 -4077
  197. data/entry_types/scrolled/package/entryState.js +1600 -0
  198. data/entry_types/scrolled/package/frontend/FloatingPortalRootProvider-20c600de.js +1979 -0
  199. data/entry_types/scrolled/package/frontend/FloatingPortalRootProvider-58189220.js +2538 -0
  200. data/entry_types/scrolled/package/frontend/{PhonePlatformContext-9f76033e.js → PhonePlatformContext-035a99fa.js} +7 -12
  201. data/entry_types/scrolled/package/frontend/PhonePlatformContext-9da43797.js +33 -0
  202. data/entry_types/scrolled/package/frontend/Placeholder.module-bf0966b4.js +5267 -0
  203. data/entry_types/scrolled/package/frontend/Placeholder.module-d22a9335.js +4180 -0
  204. data/entry_types/scrolled/package/frontend/ThemeIcon-ab834849.js +754 -0
  205. data/entry_types/scrolled/package/frontend/ThemeIcon-f6cb3a8f.js +773 -0
  206. data/entry_types/scrolled/package/frontend/{ToggleFullscreenCornerButton-788e9cdb.js → ToggleFullscreenCornerButton-30fd1124.js} +6 -6
  207. data/entry_types/scrolled/package/frontend/ToggleFullscreenCornerButton-61c25224.js +72 -0
  208. data/entry_types/scrolled/package/frontend/Viewer-1074fd59.js +147 -0
  209. data/entry_types/scrolled/package/frontend/Viewer-1482ca34.js +480 -0
  210. data/entry_types/scrolled/package/frontend/Viewer-70504231.js +210 -0
  211. data/entry_types/scrolled/package/frontend/{Viewer-0345ce57.js → Viewer-cc8d4072.js} +84 -92
  212. data/entry_types/scrolled/package/frontend/Wavesurfer-656e3c36.js +302 -0
  213. data/entry_types/scrolled/package/frontend/Wavesurfer-b753b838.js +385 -0
  214. data/entry_types/scrolled/package/frontend/commenting.css +1 -0
  215. data/entry_types/scrolled/package/frontend/extensions-167ae143.js +1455 -0
  216. data/entry_types/scrolled/package/frontend/extensions-2318f92b.js +5918 -0
  217. data/entry_types/scrolled/package/frontend/extensions-a461fd13.js +4890 -0
  218. data/entry_types/scrolled/package/frontend/extensions-c98269fd.js +1982 -0
  219. data/entry_types/scrolled/package/frontend/i18n-493cd2a6.js +93 -0
  220. data/entry_types/scrolled/package/frontend/i18n-e06cac14.js +100 -0
  221. data/entry_types/scrolled/package/frontend/{index-eb670c2f.js → index-2e5546f7.js} +36 -44
  222. data/entry_types/scrolled/package/frontend/index-de2b418d.js +130 -0
  223. data/entry_types/scrolled/package/frontend/index.css +1 -1
  224. data/entry_types/scrolled/package/frontend/index.js +1870 -875
  225. data/entry_types/scrolled/package/frontend/inlineEditing.css +1 -0
  226. data/entry_types/scrolled/package/frontend/useContentElementEditorState-a084912e.js +25 -0
  227. data/entry_types/scrolled/package/frontend/useDelayedBoolean-a387d85b.js +24 -0
  228. data/entry_types/scrolled/package/frontend/{usePhonePlatform-c2ba875d.js → usePhonePlatform-4ecb96d4.js} +1 -1
  229. data/entry_types/scrolled/package/frontend/usePhonePlatform-ce9ff08d.js +8 -0
  230. data/entry_types/scrolled/package/frontend-server.js +20 -209
  231. data/entry_types/scrolled/package/package.json +5 -3
  232. data/entry_types/scrolled/package/review.css +1 -0
  233. data/entry_types/scrolled/package/review.js +3306 -0
  234. data/entry_types/scrolled/package/testHelpers.js +836 -359
  235. data/entry_types/scrolled/package/widgets/consentBar.css +1 -1
  236. data/entry_types/scrolled/package/widgets/consentBar.js +269 -290
  237. data/entry_types/scrolled/package/widgets/defaultNavigation.css +2 -2
  238. data/entry_types/scrolled/package/widgets/defaultNavigation.js +911 -352
  239. data/entry_types/scrolled/package/widgets/excursionSheet.css +1 -1
  240. data/entry_types/scrolled/package/widgets/excursionSheet.js +87 -124
  241. data/entry_types/scrolled/package/widgets/iconInlineFileRights.css +1 -1
  242. data/entry_types/scrolled/package/widgets/iconInlineFileRights.js +62 -29
  243. data/entry_types/scrolled/package/widgets/iconScrollIndicator.js +32 -13
  244. data/entry_types/scrolled/package/widgets/mainStorylineSheet.css +1 -1
  245. data/entry_types/scrolled/package/widgets/mainStorylineSheet.js +56 -100
  246. data/entry_types/scrolled/package/widgets/textInlineFileRights.css +1 -1
  247. data/entry_types/scrolled/package/widgets/textInlineFileRights.js +33 -28
  248. data/entry_types/scrolled/package/widgets-server.js +1 -0
  249. data/entry_types/scrolled/spec/factories/lottie_files.rb +29 -0
  250. data/entry_types/scrolled/spec/fixtures/animation.lottie +0 -0
  251. data/entry_types/scrolled/spec/fixtures/font.woff +0 -0
  252. data/entry_types/scrolled/spec/fixtures/font.woff2 +0 -0
  253. data/lib/generators/pageflow/resque/resque_generator.rb +0 -2
  254. data/lib/pageflow/configuration.rb +49 -7
  255. data/lib/pageflow/deprecation.rb +19 -0
  256. data/lib/pageflow/engine.rb +1 -1
  257. data/lib/pageflow/entries_controller_env_helper.rb +32 -2
  258. data/lib/pageflow/entry_export_import/entry_serialization.rb +4 -4
  259. data/lib/pageflow/entry_export_import/page_type_versions.rb +2 -2
  260. data/lib/pageflow/entry_export_import/revision_serialization/import.rb +10 -0
  261. data/lib/pageflow/entry_export_import/revision_serialization.rb +3 -0
  262. data/lib/pageflow/entry_structured_data_types.rb +43 -0
  263. data/lib/pageflow/entry_type_configuration.rb +1 -0
  264. data/lib/pageflow/file_type.rb +2 -2
  265. data/lib/pageflow/file_types.rb +1 -1
  266. data/lib/pageflow/oembed_providers.rb +42 -0
  267. data/lib/pageflow/primary_domain_entry_redirect.rb +6 -0
  268. data/lib/pageflow/rails_version.rb +13 -2
  269. data/lib/pageflow/themes.rb +41 -0
  270. data/lib/pageflow/user_mixin.rb +12 -0
  271. data/lib/pageflow/version.rb +1 -1
  272. data/lib/pageflow.rb +1 -0
  273. data/package/config/eslint-rules/documented-in-toc.js +125 -0
  274. data/package/config/jest/index.js +0 -1
  275. data/package/config/webpack.js +0 -1
  276. data/package/config/webpack5.js +0 -1
  277. data/package/editor.js +4378 -1897
  278. data/package/frontend.js +647 -886
  279. data/package/package.json +2 -1
  280. data/package/review.js +384 -0
  281. data/package/testHelpers.js +248 -188
  282. data/package/ui.js +1346 -630
  283. data/package/vendor/stubs/wysihtml5.js +2 -0
  284. data/spec/factories/comment_thread_reads.rb +10 -0
  285. data/spec/factories/comment_threads.rb +10 -0
  286. data/spec/factories/comments.rb +9 -0
  287. data/spec/factories/file_folders.rb +8 -0
  288. data/spec/factories/published_entries.rb +2 -0
  289. metadata +188 -67
  290. data/app/assets/stylesheets/pageflow/editor/select_button.scss +0 -51
  291. data/entry_types/scrolled/app/helpers/pageflow_scrolled/generated_media_queries_helper.rb +0 -55
  292. data/entry_types/scrolled/package/frontend/FloatingPortalRootProvider-51914be7.js +0 -5439
  293. data/entry_types/scrolled/package/frontend/ThemeIcon-81f2f066.js +0 -1932
  294. data/entry_types/scrolled/package/frontend/Viewer-cdc549cc.js +0 -147
  295. data/entry_types/scrolled/package/frontend/Wavesurfer-1cdc3925.js +0 -313
  296. data/entry_types/scrolled/package/frontend/components-96660ffd.js +0 -3803
  297. data/entry_types/scrolled/package/frontend/inherits-539844a6.js +0 -99
  298. data/entry_types/scrolled/package/frontend/useContentElementEditorState-77fe6c79.js +0 -51
  299. data/entry_types/scrolled/package/widgets/excursionDialog.css +0 -1
  300. data/entry_types/scrolled/package/widgets/excursionDialog.js +0 -109
  301. data/lib/generators/pageflow/resque/templates/resque_enqueue_after_commit_patch.rb +0 -25
  302. data/package/vendor/jquery.minicolors.js +0 -1108
@@ -0,0 +1,1600 @@
1
+ import React, { useReducer, useMemo, useCallback } from 'react';
2
+ import { createContext, useContextSelector } from 'use-context-selector';
3
+ import { createSelectorCreator, defaultMemoize, createSelector } from 'reselect';
4
+ import slugify from 'slugify';
5
+ import { features } from 'pageflow/frontend';
6
+
7
+ const PREFIX = 'PAGEFLOW_SCROLLED_COLLECTION';
8
+ const RESET = `${PREFIX}_RESET`;
9
+ const ADD = `${PREFIX}_ADD`;
10
+ const CHANGE = `${PREFIX}_CHANGE`;
11
+ const PATCH_CONFIGURATION = `${PREFIX}_PATCH_CONFIGURATION`;
12
+ const REMOVE = `${PREFIX}_REMOVE`;
13
+ const SORT = `${PREFIX}_SORT`;
14
+ function useCollections(seed = {}, {
15
+ keyAttribute
16
+ } = {}) {
17
+ return useReducer(reducer, Object.keys(seed).reduce((result, key) => {
18
+ result[key] = init(seed[key], keyAttribute);
19
+ return result;
20
+ }, {}));
21
+ }
22
+ function reducer(state, action) {
23
+ const collectionName = action.payload.collectionName;
24
+ const keyAttribute = action.payload.keyAttribute;
25
+ switch (action.type) {
26
+ case RESET:
27
+ return {
28
+ ...state,
29
+ [collectionName]: init(action.payload.items, keyAttribute)
30
+ };
31
+ case ADD:
32
+ return {
33
+ ...state,
34
+ [collectionName]: {
35
+ order: action.payload.order,
36
+ items: {
37
+ ...state[collectionName].items,
38
+ [action.payload.attributes[keyAttribute]]: action.payload.attributes
39
+ }
40
+ }
41
+ };
42
+ case CHANGE:
43
+ return {
44
+ ...state,
45
+ [collectionName]: {
46
+ order: state[collectionName].order,
47
+ items: {
48
+ ...state[collectionName].items,
49
+ [action.payload.attributes[keyAttribute]]: action.payload.attributes
50
+ }
51
+ }
52
+ };
53
+ case PATCH_CONFIGURATION:
54
+ const key = action.payload.key;
55
+ return {
56
+ ...state,
57
+ [collectionName]: {
58
+ order: state[collectionName].order,
59
+ items: {
60
+ ...state[collectionName].items,
61
+ [key]: {
62
+ ...state[collectionName].items[key],
63
+ configuration: {
64
+ ...state[collectionName].items[key].configuration,
65
+ ...action.payload.configuration
66
+ }
67
+ }
68
+ }
69
+ }
70
+ };
71
+ case REMOVE:
72
+ const clonedItems = {
73
+ ...state[collectionName].items
74
+ };
75
+ delete clonedItems[action.payload.key];
76
+ return {
77
+ ...state,
78
+ [collectionName]: {
79
+ order: action.payload.order,
80
+ items: clonedItems
81
+ }
82
+ };
83
+ case SORT:
84
+ return {
85
+ ...state,
86
+ [collectionName]: {
87
+ order: action.payload.order,
88
+ items: state[collectionName].items
89
+ }
90
+ };
91
+ default:
92
+ return state;
93
+ }
94
+ }
95
+ function init(items, keyAttribute = 'id') {
96
+ items = items.filter(item => item[keyAttribute]);
97
+ return {
98
+ order: items.map(item => item[keyAttribute]),
99
+ items: items.reduce((result, item) => {
100
+ result[item[keyAttribute]] = item;
101
+ return result;
102
+ }, {})
103
+ };
104
+ }
105
+ function updateConfiguration({
106
+ dispatch,
107
+ name,
108
+ key,
109
+ configuration
110
+ }) {
111
+ dispatch({
112
+ type: PATCH_CONFIGURATION,
113
+ payload: {
114
+ collectionName: name,
115
+ key,
116
+ configuration
117
+ }
118
+ });
119
+ }
120
+ function collectionSnapshot(collection, {
121
+ attributes,
122
+ includeConfiguration
123
+ }) {
124
+ return collection.map(model => getAttributes(model, {
125
+ attributeNames: attributes,
126
+ includeConfiguration
127
+ }));
128
+ }
129
+ function watchCollection(collection, {
130
+ name,
131
+ dispatch,
132
+ attributes,
133
+ includeConfiguration,
134
+ keyAttribute = 'id'
135
+ }) {
136
+ const handle = {};
137
+ const options = {
138
+ attributeNames: attributes,
139
+ includeConfiguration
140
+ };
141
+ let tearingDown = false;
142
+ const watchedAttributeNames = getWatchedAttributeNames(attributes);
143
+ const sourceKeyAttribute = findSourceAttributeName(attributes, keyAttribute);
144
+ dispatch({
145
+ type: RESET,
146
+ payload: {
147
+ collectionName: name,
148
+ keyAttribute: keyAttribute,
149
+ items: collection.map(model => getAttributes(model, options))
150
+ }
151
+ });
152
+ collection.on('add change:id', model => {
153
+ if (!model.isNew()) {
154
+ dispatch({
155
+ type: ADD,
156
+ payload: {
157
+ collectionName: name,
158
+ keyAttribute: keyAttribute,
159
+ order: collection.pluck(sourceKeyAttribute).filter(Boolean),
160
+ attributes: getAttributes(model, options)
161
+ }
162
+ });
163
+ }
164
+ }, handle);
165
+ collection.on('change', model => {
166
+ if (hasChangedAttributes(model, watchedAttributeNames) && !model.isNew()) {
167
+ dispatch({
168
+ type: CHANGE,
169
+ payload: {
170
+ collectionName: name,
171
+ keyAttribute: keyAttribute,
172
+ attributes: getAttributes(model, options)
173
+ }
174
+ });
175
+ }
176
+ }, handle);
177
+ if (includeConfiguration) {
178
+ collection.on('change:configuration', (model, value, {
179
+ ignoreInWatchCollection
180
+ } = {}) => {
181
+ if (!model.isNew() && !ignoreInWatchCollection) {
182
+ dispatch({
183
+ type: CHANGE,
184
+ payload: {
185
+ collectionName: name,
186
+ keyAttribute: keyAttribute,
187
+ attributes: getAttributes(model, options)
188
+ }
189
+ });
190
+ }
191
+ }, handle);
192
+ }
193
+ collection.on('remove', model => {
194
+ if (!tearingDown && !model.isNew()) {
195
+ dispatch({
196
+ type: REMOVE,
197
+ payload: {
198
+ collectionName: name,
199
+ order: collection.pluck(sourceKeyAttribute).filter(Boolean),
200
+ key: model.attributes[sourceKeyAttribute]
201
+ }
202
+ });
203
+ }
204
+ }, handle);
205
+ collection.on('sort', model => dispatch({
206
+ type: SORT,
207
+ payload: {
208
+ collectionName: name,
209
+ order: collection.pluck(sourceKeyAttribute).filter(Boolean)
210
+ }
211
+ }), handle);
212
+ return function () {
213
+ tearingDown = true;
214
+ collection.off(null, null, handle);
215
+ };
216
+ }
217
+ function findSourceAttributeName(attributeNames, targetAttributeName) {
218
+ const mapping = attributeNames.find(attributeName => typeof attributeName === 'object' && mappedAttributeTarget(attributeName) === targetAttributeName);
219
+ return mapping ? mappedAttributeSource(mapping) : targetAttributeName;
220
+ }
221
+ function hasChangedAttributes(model, attributeNames) {
222
+ return attributeNames.some(attributeName => model.hasChanged(attributeName));
223
+ }
224
+ function getWatchedAttributeNames(attributeNames) {
225
+ return attributeNames.flatMap(attributeName => typeof attributeName == 'object' ? mappedAttributeSource(attributeName) : attributeName);
226
+ }
227
+ function mappedAttributeSource(mapping) {
228
+ const attributeName = Object.keys(mapping)[0];
229
+ const source = mapping[attributeName];
230
+ if (typeof source === 'string') {
231
+ return source;
232
+ } else if (Array.isArray(source)) {
233
+ return source.slice(0, -1);
234
+ } else {
235
+ return attributeName;
236
+ }
237
+ }
238
+ function mappedAttributeTarget(attributeName) {
239
+ return Object.keys(attributeName)[0];
240
+ }
241
+ function getAttributes(model, {
242
+ attributeNames,
243
+ includeConfiguration
244
+ }) {
245
+ const result = attributeNames.reduce((result, attributeName) => {
246
+ if (typeof attributeName == 'object') {
247
+ const key = Object.keys(attributeName)[0];
248
+ const value = attributeName[key];
249
+ if (Array.isArray(value)) {
250
+ const attributeNames = value.slice(0, -1);
251
+ const fn = value[value.length - 1];
252
+ result[key] = fn.apply(null, attributeNames.map(attributeName => model.get(attributeName)));
253
+ } else if (typeof value == 'function') {
254
+ result[key] = value(model.get(key));
255
+ } else {
256
+ result[key] = model.get(value);
257
+ }
258
+ } else {
259
+ result[attributeName] = model.get(attributeName);
260
+ }
261
+ return result;
262
+ }, {});
263
+ if (includeConfiguration) {
264
+ result.configuration = {
265
+ ...model.configuration.attributes
266
+ };
267
+ }
268
+ return result;
269
+ }
270
+ function getItem(state, collectionName, key) {
271
+ if (state[collectionName]) {
272
+ return state[collectionName].items[key];
273
+ }
274
+ }
275
+ function createMultipleItemsSelector(collectionNames, filter) {
276
+ return createSelector(...collectionNames.map(collectionName => collections => collections[collectionName]), (...collections) => {
277
+ return collectionNames.reduce((result, collectionName, index) => {
278
+ result[collectionName] = toOrderedItems(collections[index]);
279
+ return result;
280
+ }, {});
281
+ });
282
+ }
283
+ function createItemsSelector(collectionName, filter) {
284
+ if (filter) {
285
+ const itemsSelector = createItemsSelector(collectionName);
286
+ return createShallowEqualArraysSelector(collections => itemsSelector(collections).filter(filter), items => items);
287
+ }
288
+ return createSelector(collections => collections[collectionName], toOrderedItems);
289
+ }
290
+ function toOrderedItems(collection) {
291
+ if (collection) {
292
+ const items = collection.items;
293
+ return collection.order.map(key => items[key]);
294
+ } else {
295
+ return [];
296
+ }
297
+ }
298
+ const createShallowEqualArraysSelector = createSelectorCreator(defaultMemoize, shallowEqualArrays);
299
+ function shallowEqualArrays(a, b) {
300
+ return a.length === b.length && a.every((item, index) => item === b[index]);
301
+ }
302
+
303
+ var _jsxFileName = "/home/tfischbach/code/pageflow/entry_types/scrolled/package/src/entryState/EntryStateProvider.js";
304
+ const Context = createContext();
305
+ function EntryStateProvider({
306
+ seed,
307
+ children
308
+ }) {
309
+ const [collections, dispatch] = useCollections(seed.collections, {
310
+ keyAttribute: 'permaId'
311
+ });
312
+ const value = useMemo(() => ({
313
+ entryState: {
314
+ collections,
315
+ config: seed.config
316
+ },
317
+ dispatch
318
+ }), [collections, dispatch, seed]);
319
+ return /*#__PURE__*/React.createElement(Context.Provider, {
320
+ value: value,
321
+ __self: this,
322
+ __source: {
323
+ fileName: _jsxFileName,
324
+ lineNumber: 25,
325
+ columnNumber: 5
326
+ }
327
+ }, children);
328
+ }
329
+ function useEntryState(selector = entryState => entryState) {
330
+ return useContextSelector(Context, value => selector(value.entryState));
331
+ }
332
+ function useEntryStateDispatch() {
333
+ return useContextSelector(Context, value => value.dispatch);
334
+ }
335
+ function useEntryStateConfig() {
336
+ return useEntryState(entryState => entryState.config);
337
+ }
338
+ function useEntryStateCollectionItem(collectionName, key) {
339
+ return useEntryState(entryState => getItem(entryState.collections, collectionName, key));
340
+ }
341
+ function useEntryStateCollectionItems(collectionName, filter) {
342
+ const itemsSelector = useMemo(() => createItemsSelector(collectionName, filter), [collectionName, filter]);
343
+ return useEntryState(entryState => itemsSelector(entryState.collections));
344
+ }
345
+ function useMultipleEntryStateCollectionItems(collectionNames) {
346
+ const multipleItemsSelector = useMemo(() => createMultipleItemsSelector(collectionNames), [collectionNames]);
347
+ return useEntryState(entryState => multipleItemsSelector(entryState.collections));
348
+ }
349
+
350
+ /**
351
+ * Returns data generated by a Ruby lambda registered server
352
+ * side via `config.additional_frontend_seed_data.register`.
353
+ *
354
+ * @example
355
+ *
356
+ * const data = useAdditionalSeedData('someSeed');
357
+ */
358
+ function useAdditionalSeedData(name) {
359
+ const config = useEntryStateConfig();
360
+ if (!config.additionalSeedData[name]) {
361
+ throw new Error(`Could not find additional seed data with name '${name}'.`);
362
+ }
363
+ return config.additionalSeedData[name];
364
+ }
365
+
366
+ function useContentElementConsentVendor({
367
+ contentElementId
368
+ }) {
369
+ const config = useEntryStateConfig();
370
+ const vendorName = config.contentElementConsentVendors[contentElementId];
371
+ return config.consentVendors.find(vendor => vendor.name === vendorName);
372
+ }
373
+
374
+ function useCutOff() {
375
+ const config = useEntryStateConfig();
376
+ return config.cutOff;
377
+ }
378
+
379
+ function useEmbedOriginUrl() {
380
+ const config = useEntryStateConfig();
381
+ return config.embed ? config.originUrl : undefined;
382
+ }
383
+
384
+ /**
385
+ * Returns an object containing theme asset paths.
386
+ *
387
+ * @example
388
+ *
389
+ * const theme = useTheme();
390
+ * theme // =>
391
+ * {
392
+ * assets: {
393
+ * logoDesktop: 'path/to/logoDesktop.svg',
394
+ * logoMobile: 'path/to/logoMobile.svg'
395
+ * },
396
+ * options: {
397
+ * // options passed to `themes.register` in `pageflow.rb` initializer
398
+ * // with camleized keys.
399
+ * }
400
+ * }
401
+ */
402
+ function useTheme() {
403
+ const config = useEntryStateConfig();
404
+ return config.theme;
405
+ }
406
+
407
+ /**
408
+ * Returns a nested data structure representing the metadata of the entry.
409
+ *
410
+ * @example
411
+ *
412
+ * const metaData = useEntryMetadata();
413
+ * metaData // =>
414
+ * {
415
+ * id: 5,
416
+ * locale: 'en',
417
+ * shareProviders: {email: false, facebook: true},
418
+ * share_url: 'http://test.host/test',
419
+ * credits: 'Credits: Pageflow',
420
+ * configuration: {darkWidgets: true}
421
+ * }
422
+ */
423
+ function useEntryMetadata() {
424
+ const entries = useEntryStateCollectionItems('entries');
425
+ return useMemo(() => {
426
+ return entries[0];
427
+ }, [entries]);
428
+ }
429
+
430
+ /**
431
+ * Returns boolean indicating whether dark variant has been activated for
432
+ * the widgets of the entry.
433
+ */
434
+ function useDarkWidgets() {
435
+ const theme = useTheme();
436
+ return useEntryMetadata().configuration.darkWidgets || theme.options.darkWidgets;
437
+ }
438
+
439
+ /**
440
+ * Returns a list of attributes (iconName, name and url) of all configured share
441
+ * providers of the entry. The url provides a %<url>s placeholder where the link
442
+ * can be inserted. iconName can be passed to ThemeIcon to render a theme
443
+ * specific icon.
444
+ *
445
+ * @example
446
+ *
447
+ * const shareProviders = useShareProviders(options);
448
+ * shareProviders // =>
449
+ * [
450
+ * {
451
+ * iconName: 'facebook',
452
+ * name: 'Facebook',
453
+ * url: http://www.facebook.com/sharer/sharer.php?u=%<url>s
454
+ * },
455
+ * {
456
+ * iconName: 'twitter',
457
+ * name: 'Twitter',
458
+ * url: https://x.com/intent/post?url=%<url>s
459
+ * }
460
+ * ]
461
+ */
462
+ function useShareProviders({
463
+ isPhonePlatform
464
+ }) {
465
+ const config = useEntryStateConfig();
466
+ const entryMetadata = useEntryMetadata();
467
+ const shareProviders = (entryMetadata === null || entryMetadata === void 0 ? void 0 : entryMetadata.shareProviders) || {};
468
+ const urlTemplates = config.shareUrlTemplates;
469
+ return useMemo(() => {
470
+ const sharing = {
471
+ bluesky: {
472
+ iconName: 'bluesky',
473
+ name: 'Bluesky',
474
+ url: urlTemplates.bluesky
475
+ },
476
+ email: {
477
+ iconName: 'email',
478
+ name: 'Mail',
479
+ url: urlTemplates.email
480
+ },
481
+ facebook: {
482
+ iconName: 'facebook',
483
+ name: 'Facebook',
484
+ url: urlTemplates.facebook
485
+ },
486
+ linked_in: {
487
+ iconName: 'linkedIn',
488
+ name: 'LinkedIn',
489
+ url: urlTemplates.linked_in
490
+ },
491
+ telegram: {
492
+ iconName: 'telegram',
493
+ name: 'Telegram',
494
+ url: urlTemplates.telegram
495
+ },
496
+ threads: {
497
+ iconName: 'threads',
498
+ name: 'threads',
499
+ url: urlTemplates.threads
500
+ },
501
+ twitter: {
502
+ iconName: 'twitter',
503
+ name: 'X',
504
+ url: urlTemplates.twitter
505
+ },
506
+ whats_app: {
507
+ iconName: 'whatsApp',
508
+ name: 'WhatsApp',
509
+ url: urlTemplates.whats_app
510
+ }
511
+ };
512
+ return activeShareProviders(shareProviders, isPhonePlatform).map(provider => {
513
+ const config = sharing[provider];
514
+ return {
515
+ name: config.name,
516
+ iconName: config.iconName,
517
+ url: config.url
518
+ };
519
+ });
520
+ }, [shareProviders, isPhonePlatform, urlTemplates]);
521
+ }
522
+ function activeShareProviders(shareProvidersConfig, isPhonePlatform) {
523
+ const providers = filterShareProviders(shareProvidersConfig, isPhonePlatform);
524
+ return providers.filter(function (provider) {
525
+ return shareProvidersConfig[provider] !== false;
526
+ });
527
+ }
528
+ function filterShareProviders(shareProvidersConfig, isPhonePlatform) {
529
+ if (!isPhonePlatform) {
530
+ return Object.keys(shareProvidersConfig).filter(function (provider) {
531
+ return provider !== 'telegram' && provider !== 'whats_app';
532
+ });
533
+ }
534
+ return Object.keys(shareProvidersConfig);
535
+ }
536
+
537
+ /**
538
+ * Returns the share url of the entry.
539
+ *
540
+ * @example
541
+ *
542
+ * const shareUrl = useShareUrl();
543
+ * shareUrl // => "http://test.host/test"
544
+ */
545
+ function useShareUrl() {
546
+ const entryMetadata = useEntryMetadata();
547
+ const config = useEntryStateConfig();
548
+ if (entryMetadata) {
549
+ return entryMetadata.shareUrl ? entryMetadata.shareUrl : config.prettyUrl;
550
+ } else {
551
+ return config.shareUrl;
552
+ }
553
+ }
554
+
555
+ function useEntryTranslations() {
556
+ const config = useEntryStateConfig();
557
+ return config.entryTranslations;
558
+ }
559
+
560
+ function getChapterSlugs(chapters) {
561
+ const result = {};
562
+ const usedSlugs = {};
563
+ chapters.forEach(chapter => {
564
+ let chapterSlug = chapter.configuration.title;
565
+ if (chapterSlug) {
566
+ chapterSlug = slugify(chapterSlug, {
567
+ lower: true,
568
+ locale: 'de',
569
+ strict: true
570
+ });
571
+ if (usedSlugs[chapterSlug]) {
572
+ chapterSlug = chapterSlug + '-' + chapter.permaId;
573
+ }
574
+ usedSlugs[chapterSlug] = true;
575
+ } else {
576
+ chapterSlug = 'chapter-' + chapter.permaId;
577
+ }
578
+ result[chapter.permaId] = chapterSlug;
579
+ });
580
+ return result;
581
+ }
582
+
583
+ /**
584
+ * Returns a nested data structure representing the chapters and sections
585
+ * of the entry.
586
+ *
587
+ * @private
588
+ *
589
+ * @example
590
+ *
591
+ * const structure = useEntryStructure();
592
+ * structure // =>
593
+ * {
594
+ * main: [
595
+ * {
596
+ * permaId: 5,
597
+ * title: 'Chapter 1',
598
+ * summary: 'An introductory chapter',
599
+ * sections: [
600
+ * {
601
+ * id: 1,
602
+ * permaId: 101,
603
+ * chapterId: 3,
604
+ * sectionIndex: 0,
605
+ * transition: 'scroll',
606
+ *
607
+ * // references to parent chapter
608
+ * chapter: { ... },
609
+ *
610
+ * // references to adjacent section objects
611
+ * previousSection: { ... },
612
+ * nextSection: { ... },
613
+ * }
614
+ * ],
615
+ * }
616
+ * ],
617
+ * excursions: [ ... ],
618
+ * mainSectionsCount: 2
619
+ * }
620
+ */
621
+ function useEntryStructure() {
622
+ const mainStoryline = useMainStoryline();
623
+ const chapters = useChapters();
624
+ const sections = useEntryStateCollectionItems('sections');
625
+ return useMemo(() => buildEntryStructure({
626
+ mainStoryline,
627
+ chapters,
628
+ sections
629
+ }), [mainStoryline, chapters, sections]);
630
+ }
631
+
632
+ /**
633
+ * Like {@link useEntryStructure}, but additionally nests an ordered
634
+ * `contentElements` array (including backdrop elements) into each
635
+ * section. Kept separate so the more frequently used
636
+ * `useEntryStructure` does not re-derive when content elements change.
637
+ *
638
+ * @private
639
+ */
640
+ function useEntryStructureWithContentElements() {
641
+ const mainStoryline = useMainStoryline();
642
+ const chapters = useChapters();
643
+ const sections = useEntryStateCollectionItems('sections');
644
+ const contentElements = useEntryStateCollectionItems('contentElements');
645
+ return useMemo(() => {
646
+ const contentElementsBySectionId = {};
647
+ contentElements.forEach(contentElement => {
648
+ const sectionContentElements = contentElementsBySectionId[contentElement.sectionId] || (contentElementsBySectionId[contentElement.sectionId] = []);
649
+ sectionContentElements.push(contentElementSubjectData(contentElement));
650
+ });
651
+ return buildEntryStructure({
652
+ mainStoryline,
653
+ chapters,
654
+ sections,
655
+ contentElementsBySectionId
656
+ });
657
+ }, [mainStoryline, chapters, sections, contentElements]);
658
+ }
659
+ function buildEntryStructure({
660
+ mainStoryline,
661
+ chapters,
662
+ sections,
663
+ contentElementsBySectionId
664
+ }) {
665
+ const enrichedSections = sections.map(section => ({
666
+ ...sectionData(section),
667
+ ...(contentElementsBySectionId && {
668
+ contentElements: contentElementsBySectionId[section.id] || []
669
+ })
670
+ }));
671
+ const main = [];
672
+ const excursions = [];
673
+ chapters.forEach(chapter => {
674
+ const chapterSections = enrichedSections.filter(item => item.chapterId === chapter.id);
675
+ const isExcursion = chapter.storylineId !== mainStoryline.id;
676
+ chapter = {
677
+ ...chapter,
678
+ isExcursion,
679
+ sections: chapterSections
680
+ };
681
+ chapterSections.forEach(section => section.chapter = chapter);
682
+ if (isExcursion) {
683
+ excursions.push(chapter);
684
+ } else {
685
+ main.push(chapter);
686
+ }
687
+ });
688
+ const mainSections = main.flatMap(chapter => chapter.sections);
689
+ linkAndIndexSections(mainSections);
690
+ excursions.forEach(excursion => linkAndIndexSections(excursion.sections));
691
+ return {
692
+ main,
693
+ excursions,
694
+ mainSectionsCount: mainSections.length
695
+ };
696
+ }
697
+ function contentElementSubjectData(contentElement) {
698
+ return {
699
+ id: contentElement.id,
700
+ permaId: contentElement.permaId,
701
+ sectionId: contentElement.sectionId,
702
+ type: contentElement.typeName
703
+ };
704
+ }
705
+ function linkAndIndexSections(sections) {
706
+ sections.forEach((section, index) => {
707
+ section.sectionIndex = index;
708
+ section.previousSection = sections[index - 1];
709
+ section.nextSection = sections[index + 1];
710
+ });
711
+ }
712
+
713
+ /**
714
+ * Returns an array of sections each with a chapter property containing
715
+ * data about the parent chapter.
716
+ *
717
+ * @private
718
+ *
719
+ * @example
720
+ *
721
+ * const sections = useSectionsWithChapter();
722
+ * sections // =>
723
+ * [
724
+ * {
725
+ * id: 1,
726
+ * permaId: 101,
727
+ * chapterId: 3,
728
+ * transition: 'scroll',
729
+ * chapter: {
730
+ * id: 3,
731
+ * permaId: 5,
732
+ * title: 'Chapter 1',
733
+ * summary: 'An introductory chapter',
734
+ * chapterSlug: 'chapter-1'
735
+ * },
736
+ * }
737
+ * ]
738
+ */
739
+ function useSectionsWithChapter() {
740
+ const chapters = useChapters();
741
+ const sections = useEntryStateCollectionItems('sections');
742
+ const chaptersById = useMemo(() => chapters.reduce((result, chapter) => {
743
+ result[chapter.id] = chapter;
744
+ return result;
745
+ }, {}), [chapters]);
746
+ return useMemo(() => {
747
+ return sections.map((section, sectionIndex) => ({
748
+ sectionIndex,
749
+ ...sectionData(section),
750
+ chapter: chaptersById[section.chapterId]
751
+ }));
752
+ }, [chaptersById, sections]);
753
+ }
754
+
755
+ /**
756
+ * Returns a nested data structure representing the content elements
757
+ * of section.
758
+ *
759
+ * @param {Object} options
760
+ * @param {number} options.sectionPermaId
761
+ *
762
+ * @private
763
+ *
764
+ * @example
765
+ *
766
+ * const section = useSection({sectionPermaId: 4});
767
+ * section // =>
768
+ * {
769
+ * id: 100,
770
+ * permaId: 4,
771
+ * chapterId: 1,
772
+ * transition: 'scroll'
773
+ * }
774
+ */
775
+ function useSection({
776
+ sectionPermaId
777
+ }) {
778
+ const section = useEntryStateCollectionItem('sections', sectionPermaId);
779
+ return sectionData(section);
780
+ }
781
+ function sectionData(section) {
782
+ return section && {
783
+ permaId: section.permaId,
784
+ id: section.id,
785
+ chapterId: section.chapterId,
786
+ ...normalizeSectionConfigurationData(section.configuration)
787
+ };
788
+ }
789
+ function normalizeSectionConfigurationData(configuration) {
790
+ return {
791
+ ...configuration,
792
+ ...(configuration.backdropType === 'contentElement' ? {
793
+ fullHeight: true
794
+ } : {})
795
+ };
796
+ }
797
+ function useSectionForegroundContentElements({
798
+ sectionId,
799
+ layout,
800
+ phoneLayout
801
+ }) {
802
+ const filter = useCallback(contentElement => contentElement.sectionId === sectionId && contentElement.configuration.position !== 'backdrop', [sectionId]);
803
+ const contentElements = useEntryStateCollectionItems('contentElements', filter);
804
+ return contentElements.map(contentElement => contentElementData(contentElement, layout, phoneLayout));
805
+ }
806
+ function useContentElement({
807
+ permaId,
808
+ layout
809
+ }) {
810
+ const contentElement = useEntryStateCollectionItem('contentElements', permaId);
811
+ return useMemo(() => contentElement && contentElementData(contentElement, layout), [contentElement, layout]);
812
+ }
813
+
814
+ /**
815
+ * Returns the perma id of the section a comment subject lives in: the
816
+ * subject's perma id itself for a section subject, the parent section's
817
+ * perma id for a content element subject. Persisted with new threads so
818
+ * they stay grouped under their section once the subject is deleted.
819
+ *
820
+ * @private
821
+ */
822
+ function useSectionPermaIdOfSubject({
823
+ subjectType,
824
+ subjectId
825
+ }) {
826
+ const contentElement = useEntryStateCollectionItem('contentElements', subjectId);
827
+ const sections = useEntryStateCollectionItems('sections');
828
+ return useMemo(() => {
829
+ var _sections$find;
830
+ switch (subjectType) {
831
+ case 'Section':
832
+ return subjectId;
833
+ case 'ContentElement':
834
+ return (_sections$find = sections.find(section => section.id === (contentElement === null || contentElement === void 0 ? void 0 : contentElement.sectionId))) === null || _sections$find === void 0 ? void 0 : _sections$find.permaId;
835
+ default:
836
+ return undefined;
837
+ }
838
+ }, [subjectType, subjectId, contentElement, sections]);
839
+ }
840
+ function contentElementData(contentElement, layout, phoneLayout) {
841
+ const position = getPosition(contentElement, layout, phoneLayout);
842
+ return {
843
+ id: contentElement.id,
844
+ permaId: contentElement.permaId,
845
+ sectionId: contentElement.sectionId,
846
+ type: contentElement.typeName,
847
+ position,
848
+ width: getWidth(contentElement, position, phoneLayout),
849
+ standAlone: contentElement.configuration.position === 'standAlone',
850
+ alignment: contentElement.configuration.alignment,
851
+ props: contentElement.configuration
852
+ };
853
+ }
854
+ const supportedPositions = {
855
+ center: ['inline', 'left', 'right'],
856
+ centerRagged: ['inline', 'left', 'right'],
857
+ left: ['inline', 'side', 'sticky'],
858
+ right: ['inline', 'side', 'sticky'],
859
+ backdrop: ['backdrop']
860
+ };
861
+ function getPosition(contentElement, layout, phoneLayout) {
862
+ const position = contentElement.configuration.position;
863
+ if (contentElement.configuration.fullWidthInPhoneLayout && phoneLayout) {
864
+ return 'inline';
865
+ }
866
+ return supportedPositions[layout || 'left'].includes(position) ? position : 'inline';
867
+ }
868
+ const legacyPositionWidths = {
869
+ wide: 2,
870
+ full: 3
871
+ };
872
+ const clampedWidthPositions = ['sticky', 'left', 'right'];
873
+ function getWidth(contentElement, position, phoneLayout) {
874
+ const width = typeof contentElement.configuration.width === 'number' ? contentElement.configuration.width : legacyPositionWidths[contentElement.configuration.position] || 0;
875
+ if (contentElement.configuration.fullWidthInPhoneLayout && phoneLayout) {
876
+ return 3;
877
+ } else if (clampedWidthPositions.includes(position)) {
878
+ return Math.min(Math.max(width || 0, -2), 2);
879
+ } else {
880
+ return width;
881
+ }
882
+ }
883
+ function useChapter({
884
+ permaId
885
+ }) {
886
+ const chapters = useChapters();
887
+ return useMemo(() => chapters.find(chapter => chapter.permaId === permaId), [chapters, permaId]);
888
+ }
889
+ function useChapters() {
890
+ const chapters = useEntryStateCollectionItems('chapters');
891
+ return useMemo(() => {
892
+ const chapterSlugs = getChapterSlugs(chapters);
893
+ return chapters.map((chapter, index) => ({
894
+ id: chapter.id,
895
+ permaId: chapter.permaId,
896
+ storylineId: chapter.storylineId,
897
+ chapterSlug: chapterSlugs[chapter.permaId],
898
+ index,
899
+ ...chapter.configuration
900
+ }));
901
+ }, [chapters]);
902
+ }
903
+ function useMainChapters() {
904
+ const chapters = useChapters();
905
+ const mainStoryline = useMainStoryline();
906
+ return useMemo(() => chapters.filter(chapter => chapter.storylineId === mainStoryline.id), [chapters, mainStoryline]);
907
+ }
908
+ function useMainStoryline() {
909
+ const storylines = useEntryStateCollectionItems('storylines');
910
+ return useMemo(() => storylines.find(storyline => storyline.configuration.main), [storylines]);
911
+ }
912
+
913
+ function extendFile(collectionName, file, config) {
914
+ if (!file) {
915
+ return null;
916
+ }
917
+ const variants = file.variants ? ['original', ...file.variants] : Object.keys(config.fileUrlTemplates[collectionName] || {});
918
+ return {
919
+ ...file,
920
+ modelType: resolveModelType(collectionName, config.fileModelTypes),
921
+ urls: buildUrls(collectionName, file, variants, config.fileUrlTemplates),
922
+ variantWidths: computeVariantWidths(collectionName, file, variants)
923
+ };
924
+ }
925
+ function resolveModelType(collectionName, modelTypes) {
926
+ if (!modelTypes[collectionName]) {
927
+ throw new Error(`Could not find model type for collection name ${collectionName}`);
928
+ }
929
+ return modelTypes[collectionName];
930
+ }
931
+ function buildUrls(collectionName, file, variants, urlTemplates) {
932
+ if (!urlTemplates[collectionName]) {
933
+ throw new Error(`No file url templates found for ${collectionName}`);
934
+ }
935
+ return variants.reduce((result, variant) => {
936
+ const url = getFileUrl(collectionName, file, variant, urlTemplates);
937
+ if (url) {
938
+ result[variant] = url;
939
+ }
940
+ return result;
941
+ }, {});
942
+ }
943
+ function getFileUrl(collectionName, file, quality, urlTemplates) {
944
+ const templates = urlTemplates[collectionName];
945
+ const template = templates[quality];
946
+ if (template) {
947
+ return template.replace(':id_partition', idPartition(file.id)).replace(':basename', file.basename).replace(':extension', file.extension).replace(':processed_extension', file.processedExtension).replace(':pageflow_hls_qualities', () => hlsQualities(file));
948
+ }
949
+ }
950
+ function idPartition(id) {
951
+ return partition(pad(id, 9));
952
+ }
953
+ function partition(string, separator) {
954
+ return string.replace(/./g, function (c, i, a) {
955
+ return i && (a.length - i) % 3 === 0 ? '/' + c : c;
956
+ });
957
+ }
958
+ function pad(string, size) {
959
+ return (Array(size).fill(0).join('') + string).slice(-size);
960
+ }
961
+ function hlsQualities(file) {
962
+ return ['low', 'medium', 'high', 'fullhd', '4k'].filter(quality => file.variants.includes(quality)).join(',');
963
+ }
964
+ const variantGeometries = {
965
+ imageFiles: {
966
+ medium: 1024,
967
+ large: 1920,
968
+ ultra: 3840
969
+ }
970
+ };
971
+ function computeVariantWidths(collectionName, file, variants) {
972
+ const geometries = variantGeometries[collectionName];
973
+ if (!geometries) {
974
+ return undefined;
975
+ }
976
+ const widthToVariant = {};
977
+ variants.forEach(variant => {
978
+ const geometrySize = geometries[variant];
979
+ if (geometrySize) {
980
+ const key = variantWidth(file, geometrySize) + 'w';
981
+ if (!widthToVariant[key]) {
982
+ widthToVariant[key] = variant;
983
+ }
984
+ }
985
+ });
986
+ return Object.entries(widthToVariant);
987
+ }
988
+ function variantWidth(file, geometrySize) {
989
+ const {
990
+ width,
991
+ height
992
+ } = file;
993
+ if (!width || !height) {
994
+ return geometrySize;
995
+ }
996
+ const scale = Math.min(geometrySize / width, geometrySize / height, 1);
997
+ return Math.round(width * scale);
998
+ }
999
+
1000
+ /**
1001
+ * Look up a file by its collection and perma id.
1002
+ *
1003
+ * @param {Object} options
1004
+ * @param {String} options.collectionName - Collection name of file type to look for (in camel case).
1005
+ * @param {String} options.permaId - Perma id of file look up
1006
+ *
1007
+ * @example
1008
+ * const imageFile = useFile({collectionName: 'imageFiles', permaId: 5});
1009
+ * imageFile // =>
1010
+ * {
1011
+ * id: 102,
1012
+ * permaId: 5,
1013
+ * width: 1000,
1014
+ * height: 500,
1015
+ * urls: {
1016
+ * large: 'https://...'
1017
+ * },
1018
+ * configuration: {
1019
+ * alt: '...'
1020
+ * }
1021
+ * }
1022
+ */
1023
+ function useFile({
1024
+ collectionName,
1025
+ permaId
1026
+ }) {
1027
+ const file = useEntryStateCollectionItem(collectionName, permaId);
1028
+ const config = useEntryStateConfig();
1029
+ return useMemo(() => extendFile(collectionName, file, config), [collectionName, file, config]);
1030
+ }
1031
+
1032
+ function useFileWithInlineRights({
1033
+ configuration,
1034
+ collectionName,
1035
+ propertyName
1036
+ }) {
1037
+ const file = useFile({
1038
+ collectionName,
1039
+ permaId: configuration[propertyName]
1040
+ });
1041
+ const config = useEntryStateConfig();
1042
+ return file && {
1043
+ ...file,
1044
+ license: file.configuration.license && config.fileLicenses[file.configuration.license],
1045
+ inlineRights: file.configuration.rights_display === 'inline' && !configuration[propertyName === 'id' ? 'inlineRightsHidden' : `${propertyName.replace('Id', '')}InlineRightsHidden`]
1046
+ };
1047
+ }
1048
+
1049
+ function useDownloadableFile(options) {
1050
+ const file = useFile(options);
1051
+ return file && {
1052
+ ...file,
1053
+ urls: {
1054
+ ...file.urls,
1055
+ download: `${file.urls.original}?download=${encodeURIComponent(file.displayName)}`
1056
+ }
1057
+ };
1058
+ }
1059
+
1060
+ function useNestedFiles({
1061
+ collectionName,
1062
+ parent
1063
+ }) {
1064
+ const config = useEntryStateConfig();
1065
+ const files = useEntryStateCollectionItems(collectionName, file => {
1066
+ return parent && file.parentFileId === parent.id && file.parentFileModelType === parent.modelType;
1067
+ });
1068
+ return files.map(file => extendFile(collectionName, file, config));
1069
+ }
1070
+
1071
+ function getFileUrlTemplateHost(seed, collectionName, variant) {
1072
+ const hlsUrlTemplate = seed.config.fileUrlTemplates[collectionName][variant];
1073
+ return hlsUrlTemplate.split('//')[1].split('/')[0];
1074
+ }
1075
+
1076
+ /**
1077
+ * Find the file references a configuration contains.
1078
+ *
1079
+ * @param {Object} options
1080
+ * @param {Array} options.locations - File reference locations of the subject's schema.
1081
+ * @param {Object} options.configuration - Configuration to read perma ids from.
1082
+ *
1083
+ * @private
1084
+ */
1085
+ function collectFileReferences({
1086
+ locations,
1087
+ configuration
1088
+ }) {
1089
+ return locations.flatMap(location => valuesAt(configuration, location.path).map(({
1090
+ path,
1091
+ value
1092
+ }) => ({
1093
+ path,
1094
+ permaId: toPermaId(value)
1095
+ })).filter(({
1096
+ permaId
1097
+ }) => permaId).map(({
1098
+ path,
1099
+ permaId
1100
+ }) => ({
1101
+ collectionName: location.collection,
1102
+ permaId,
1103
+ path,
1104
+ active: isActive(location.activeIf, configuration)
1105
+ })));
1106
+ }
1107
+
1108
+ // Backdrops of legacy sections store a color in the property that
1109
+ // otherwise holds an image perma id.
1110
+ function toPermaId(value) {
1111
+ const permaId = Number(value);
1112
+ return Number.isInteger(permaId) && permaId > 0 ? permaId : null;
1113
+ }
1114
+ function valuesAt(value, path, resolvedPath = []) {
1115
+ if (value === null || value === undefined) {
1116
+ return [];
1117
+ }
1118
+ if (!path.length) {
1119
+ return [{
1120
+ path: resolvedPath,
1121
+ value
1122
+ }];
1123
+ }
1124
+ const [segment, ...rest] = path;
1125
+ if (segment === '*') {
1126
+ return Object.entries(value).flatMap(([key, item]) => valuesAt(item, rest, [...resolvedPath, key]));
1127
+ }
1128
+ return valuesAt(value[segment], rest, [...resolvedPath, segment]);
1129
+ }
1130
+ function isActive(activeIf, configuration) {
1131
+ if (!activeIf) {
1132
+ return true;
1133
+ }
1134
+ return [activeIf].flat().every(condition => matches(condition, configuration));
1135
+ }
1136
+ function matches(condition, configuration) {
1137
+ const value = valueAt(configuration, condition.path);
1138
+ if ('present' in condition) {
1139
+ return (value !== null && value !== undefined) === condition.present;
1140
+ }
1141
+ if ('not' in condition) {
1142
+ return ![condition.not].flat().includes(value);
1143
+ }
1144
+ return [condition.value].flat().includes(value);
1145
+ }
1146
+ function valueAt(configuration, path) {
1147
+ return path.reduce((value, segment) => value === null || value === void 0 ? void 0 : value[segment], configuration);
1148
+ }
1149
+
1150
+ /**
1151
+ * Find the file references of an entry, indexed by file.
1152
+ *
1153
+ * @param {Object} options
1154
+ * @param {Object} options.collections - Entry state collections.
1155
+ * @param {Object} options.locations - File reference locations by subject.
1156
+ * @param {Object} options.fileModelTypes - Model type by file collection name.
1157
+ *
1158
+ * @example
1159
+ *
1160
+ * const references = collectEntryFileReferences({collections, locations, fileModelTypes});
1161
+ * references.of('imageFiles', 5)
1162
+ * // => [{subject: {model: 'section', permaId: 12},
1163
+ * // path: ['backdrop', 'image'], active: true}]
1164
+ *
1165
+ * @private
1166
+ */
1167
+ function collectEntryFileReferences({
1168
+ collections,
1169
+ locations,
1170
+ fileModelTypes
1171
+ }) {
1172
+ const references = {};
1173
+ subjects(collections, locations).forEach(subject => collectFileReferences({
1174
+ locations: subject.locations,
1175
+ configuration: subject.configuration
1176
+ }).forEach(reference => add(references, reference, subject.subject)));
1177
+ addNestedFileReferences(references, collections, fileModelTypes);
1178
+ return {
1179
+ of(collectionName, permaId) {
1180
+ return references[key({
1181
+ collectionName,
1182
+ permaId
1183
+ })] || [];
1184
+ }
1185
+ };
1186
+ }
1187
+ function subjects({
1188
+ entries = [],
1189
+ sections = [],
1190
+ contentElements = []
1191
+ }, locations) {
1192
+ const bySectionId = groupBySectionId(contentElements);
1193
+ return [...entries.map(entry => ({
1194
+ subject: {
1195
+ model: 'entry'
1196
+ },
1197
+ configuration: entry,
1198
+ locations: locations.entry || []
1199
+ })), ...sections.flatMap(section => [{
1200
+ subject: {
1201
+ model: 'section',
1202
+ permaId: section.permaId
1203
+ },
1204
+ configuration: section.configuration,
1205
+ locations: locations.sections || []
1206
+ }, ...(bySectionId[section.id] || []).map(contentElement => ({
1207
+ subject: {
1208
+ model: 'contentElement',
1209
+ permaId: contentElement.permaId
1210
+ },
1211
+ configuration: contentElement.configuration,
1212
+ locations: (locations.contentElements || {})[contentElement.typeName] || []
1213
+ }))])];
1214
+ }
1215
+
1216
+ // Text tracks and the like are not referenced by any configuration.
1217
+ function addNestedFileReferences(references, collections, fileModelTypes = {}) {
1218
+ const filesById = indexFilesById(collections, fileModelTypes);
1219
+ Object.keys(fileModelTypes).forEach(collectionName => (collections[collectionName] || []).forEach(file => {
1220
+ var _references$key;
1221
+ if (!file.parentFileId) {
1222
+ return;
1223
+ }
1224
+ const parent = filesById[modelTypeAndId(file.parentFileModelType, file.parentFileId)];
1225
+ (_references$key = references[key(parent || {})]) === null || _references$key === void 0 ? void 0 : _references$key.forEach(({
1226
+ subject,
1227
+ path,
1228
+ active
1229
+ }) => add(references, {
1230
+ collectionName,
1231
+ permaId: file.permaId,
1232
+ path,
1233
+ active
1234
+ }, subject));
1235
+ }));
1236
+ }
1237
+ function indexFilesById(collections, fileModelTypes) {
1238
+ return Object.entries(fileModelTypes).reduce((result, [collectionName, modelType]) => {
1239
+ (collections[collectionName] || []).forEach(file => {
1240
+ result[modelTypeAndId(modelType, file.id)] = {
1241
+ collectionName,
1242
+ permaId: file.permaId
1243
+ };
1244
+ });
1245
+ return result;
1246
+ }, {});
1247
+ }
1248
+ function add(references, {
1249
+ collectionName,
1250
+ permaId,
1251
+ path,
1252
+ active
1253
+ }, subject) {
1254
+ const list = references[key({
1255
+ collectionName,
1256
+ permaId
1257
+ })] = references[key({
1258
+ collectionName,
1259
+ permaId
1260
+ })] || [];
1261
+ list.push({
1262
+ subject,
1263
+ path,
1264
+ active
1265
+ });
1266
+ }
1267
+ function groupBySectionId(contentElements) {
1268
+ return contentElements.reduce((result, contentElement) => {
1269
+ (result[contentElement.sectionId] = result[contentElement.sectionId] || []).push(contentElement);
1270
+ return result;
1271
+ }, {});
1272
+ }
1273
+ function key({
1274
+ collectionName,
1275
+ permaId
1276
+ }) {
1277
+ return `${collectionName}/${permaId}`;
1278
+ }
1279
+ function modelTypeAndId(modelType, id) {
1280
+ return `${modelType}/${id}`;
1281
+ }
1282
+
1283
+ /**
1284
+ * Returns the file references of the entry, indexed by file.
1285
+ *
1286
+ * @example
1287
+ *
1288
+ * const references = useFileReferences();
1289
+ * references.of('imageFiles', 5)
1290
+ * // => [{subject: {model: 'section', permaId: 12}, active: true}]
1291
+ *
1292
+ * @private
1293
+ */
1294
+ function useFileReferences() {
1295
+ const config = useEntryStateConfig();
1296
+ const entry = useEntryMetadata();
1297
+ const sections = useEntryStateCollectionItems('sections');
1298
+ const contentElements = useEntryStateCollectionItems('contentElements');
1299
+ const collectionNames = useMemo(() => Object.keys(config.fileModelTypes), [config]);
1300
+ const files = useMultipleEntryStateCollectionItems(collectionNames);
1301
+ return useMemo(() => collectEntryFileReferences({
1302
+ collections: {
1303
+ entries: [entry],
1304
+ sections,
1305
+ contentElements,
1306
+ ...files
1307
+ },
1308
+ locations: config.fileReferenceLocations || {},
1309
+ fileModelTypes: config.fileModelTypes
1310
+ }), [config, entry, sections, contentElements, files]);
1311
+ }
1312
+
1313
+ /**
1314
+ * Returns a function telling whether a file is referenced in the entry.
1315
+ *
1316
+ * Considers every file referenced unless the file_rights_from_references
1317
+ * feature is enabled, since schemas do not cover all content element
1318
+ * types yet.
1319
+ *
1320
+ * @example
1321
+ *
1322
+ * const isFileReferenced = useIsFileReferenced();
1323
+ * isFileReferenced('imageFiles', 5) // => true
1324
+ *
1325
+ * @private
1326
+ */
1327
+ function useIsFileReferenced() {
1328
+ const references = useFileReferences();
1329
+ return useMemo(() => {
1330
+ if (!features.isEnabled('file_rights_from_references')) {
1331
+ return () => true;
1332
+ }
1333
+ return (collectionName, permaId) => references.of(collectionName, permaId).some(({
1334
+ active
1335
+ }) => active);
1336
+ }, [references]);
1337
+ }
1338
+
1339
+ /**
1340
+ * Returns a collection of rights and source urls of the files
1341
+ * referenced in the entry. If none of the files has a rights attribute
1342
+ * configured, it falls back to the default file rights of the
1343
+ * entry's site, otherwise returns an empty array.
1344
+ *
1345
+ * Lists all files of the entry unless the file_rights_from_references
1346
+ * feature is enabled, since schemas do not cover all content element
1347
+ * types yet.
1348
+ *
1349
+ * @example
1350
+ *
1351
+ * const fileRights = useFileRights();
1352
+ * fileRights // => [{text: 'author of image 1', urls: ['https://example.com/source-url']}]
1353
+ */
1354
+ function useFileRights() {
1355
+ var _config$defaultFileRi;
1356
+ const config = useEntryStateConfig();
1357
+ const fileCollectionNames = Object.keys(config.fileModelTypes);
1358
+ const files = useMultipleEntryStateCollectionItems(fileCollectionNames);
1359
+ const isFileReferenced = useIsFileReferenced();
1360
+ const defaultFileRights = (_config$defaultFileRi = config.defaultFileRights) === null || _config$defaultFileRi === void 0 ? void 0 : _config$defaultFileRi.trim();
1361
+ const items = {};
1362
+ Object.keys(files).forEach(collectionName => files[collectionName].filter(file => file.configuration.rights_display !== 'inline').filter(file => isFileReferenced(collectionName, file.permaId)).forEach(file => {
1363
+ var _file$rights;
1364
+ const text = ((_file$rights = file.rights) === null || _file$rights === void 0 ? void 0 : _file$rights.trim()) || defaultFileRights;
1365
+ if (text) {
1366
+ var _file$configuration$s;
1367
+ items[text] = items[text] || {
1368
+ text,
1369
+ urls: new Set()
1370
+ };
1371
+ if ((_file$configuration$s = file.configuration.source_url) === null || _file$configuration$s === void 0 ? void 0 : _file$configuration$s.trim()) {
1372
+ items[text].urls.add(file.configuration.source_url);
1373
+ }
1374
+ }
1375
+ }));
1376
+ return Object.values(items).map(item => ({
1377
+ ...item,
1378
+ urls: Array.from(item.urls).sort()
1379
+ })).sort((a, b) => a.text.localeCompare(b.text));
1380
+ }
1381
+
1382
+ /**
1383
+ * Returns a nested data structure representing the legal info of the entry.
1384
+ * Each legal info is separated into label and url to use in links.
1385
+ * Both label and url can be blank, depending on the configuration.
1386
+ *
1387
+ * @example
1388
+ *
1389
+ * const legalInfo = useLegalInfo();
1390
+ * legalInfo // =>
1391
+ * {
1392
+ * imprint: {
1393
+ * label: '',
1394
+ * url: ''
1395
+ * },
1396
+ * copyright: {
1397
+ * label: '',
1398
+ * url: ''
1399
+ * },
1400
+ * privacy: {
1401
+ * label: '',
1402
+ * url: ''
1403
+ * }
1404
+ * }
1405
+ */
1406
+ function useLegalInfo() {
1407
+ const config = useEntryStateConfig();
1408
+ return config.legalInfo;
1409
+ }
1410
+
1411
+ /**
1412
+ * Returns the credits string (rich text) of the entry.
1413
+ *
1414
+ * @example
1415
+ *
1416
+ * const credits = useCredits();
1417
+ * credits // => "Credits: <a href="http://pageflow.com">pageflow.com</a>"
1418
+ */
1419
+ function useCredits() {
1420
+ const entryMetadata = useEntryMetadata();
1421
+ let credits = '';
1422
+ if (entryMetadata) {
1423
+ credits = entryMetadata.credits;
1424
+ }
1425
+ return credits;
1426
+ }
1427
+
1428
+ const qualities = ['medium', 'fullhd', '4k'];
1429
+ function useAvailableQualities(file) {
1430
+ if (!file) {
1431
+ return [];
1432
+ }
1433
+ return ['auto', ...qualities.filter(name => file.variants.includes(name))];
1434
+ }
1435
+
1436
+ function useWidget({
1437
+ role
1438
+ }) {
1439
+ const widgets = useEntryStateCollectionItems('widgets');
1440
+ return useMemo(() => widgets.find(widget => widget.role === role && widget.typeName), [role, widgets]);
1441
+ }
1442
+ function useActiveWidgets() {
1443
+ const widgets = useEntryStateCollectionItems('widgets');
1444
+ return useMemo(() => widgets.filter(widget => widget.typeName), [widgets]);
1445
+ }
1446
+
1447
+ /**
1448
+ * Entry state collections of an entry, taken from its Backbone models
1449
+ * once. Use `watchCollections` instead to keep collections in sync.
1450
+ *
1451
+ * @private
1452
+ */
1453
+ function collectionsSnapshot(entry) {
1454
+ return collectionSpecs(entry).reduce((result, {
1455
+ collection,
1456
+ name,
1457
+ ...spec
1458
+ }) => {
1459
+ result[name] = collectionSnapshot(collection, spec);
1460
+ return result;
1461
+ }, {});
1462
+ }
1463
+ function watchCollections(entry, {
1464
+ dispatch
1465
+ }) {
1466
+ const teardownFns = collectionSpecs(entry).map(({
1467
+ collection,
1468
+ ...spec
1469
+ }) => watchCollection(collection, {
1470
+ ...spec,
1471
+ dispatch
1472
+ }));
1473
+ return function () {
1474
+ teardownFns.forEach(fn => fn());
1475
+ };
1476
+ }
1477
+ function collectionSpecs(entry) {
1478
+ const {
1479
+ storylines,
1480
+ chapters,
1481
+ sections,
1482
+ contentElements,
1483
+ widgets,
1484
+ files
1485
+ } = entry;
1486
+ return [{
1487
+ collection: new window.Backbone.Collection([entry.metadata]),
1488
+ name: 'entries',
1489
+ attributes: ['locale', {
1490
+ id: () => entry.id
1491
+ }, {
1492
+ permaId: () => entry.id
1493
+ },
1494
+ // Make sure key attribute is present
1495
+ {
1496
+ shareProviders: 'share_providers'
1497
+ }, {
1498
+ shareUrl: 'share_url'
1499
+ }, {
1500
+ shareImageId: 'share_image_id'
1501
+ }, 'credits'],
1502
+ keyAttribute: 'permaId',
1503
+ includeConfiguration: true
1504
+ }, {
1505
+ collection: storylines,
1506
+ name: 'storylines',
1507
+ attributes: ['id', 'permaId'],
1508
+ keyAttribute: 'permaId',
1509
+ includeConfiguration: true
1510
+ }, {
1511
+ collection: chapters,
1512
+ name: 'chapters',
1513
+ attributes: ['id', 'permaId', 'storylineId'],
1514
+ keyAttribute: 'permaId',
1515
+ includeConfiguration: true
1516
+ }, {
1517
+ collection: sections,
1518
+ name: 'sections',
1519
+ attributes: ['id', 'permaId', 'chapterId'],
1520
+ keyAttribute: 'permaId',
1521
+ includeConfiguration: true
1522
+ }, {
1523
+ collection: contentElements,
1524
+ name: 'contentElements',
1525
+ attributes: ['id', 'permaId', 'typeName', 'sectionId'],
1526
+ keyAttribute: 'permaId',
1527
+ includeConfiguration: true
1528
+ }, {
1529
+ // Only sync widgets whose frontend packs the server loads in the editor.
1530
+ // Editor-disabled widgets have no registered widget type here, so rendering
1531
+ // them in the preview would fail.
1532
+ collection: widgets.withWidgetType({
1533
+ insertPoint: 'react',
1534
+ enabledInEditor: true
1535
+ }),
1536
+ name: 'widgets',
1537
+ attributes: [{
1538
+ typeName: 'type_name'
1539
+ }, 'role', {
1540
+ permaId: 'role'
1541
+ }],
1542
+ keyAttribute: 'permaId',
1543
+ includeConfiguration: true
1544
+ }, ...Object.keys(files).map(collectionName => ({
1545
+ collection: files[collectionName],
1546
+ name: camelize(collectionName),
1547
+ attributes: ['id', {
1548
+ permaId: 'perma_id'
1549
+ }, 'width', 'height', 'basename', 'extension', 'rights', {
1550
+ displayName: ['display_name', 'file_name', (displayName, fileName) => displayName || fileName]
1551
+ }, {
1552
+ processedExtension: 'processed_extension'
1553
+ }, {
1554
+ isReady: 'is_ready'
1555
+ }, {
1556
+ variants: variants => variants && variants.map(variant => camelize(variant))
1557
+ }, {
1558
+ durationInMs: 'duration_in_ms'
1559
+ }, {
1560
+ parentFileId: 'parent_file_id'
1561
+ }, {
1562
+ parentFileModelType: 'parent_file_model_type'
1563
+ }],
1564
+ keyAttribute: 'permaId',
1565
+ includeConfiguration: true
1566
+ }))];
1567
+ }
1568
+ function camelize(snakeCase) {
1569
+ return snakeCase.replace(/_[a-z]/g, function (match) {
1570
+ return match[1].toUpperCase();
1571
+ });
1572
+ }
1573
+
1574
+ function updateContentElementConfiguration({
1575
+ dispatch,
1576
+ permaId,
1577
+ configuration
1578
+ }) {
1579
+ updateConfiguration({
1580
+ dispatch,
1581
+ name: 'contentElements',
1582
+ key: permaId,
1583
+ configuration
1584
+ });
1585
+ }
1586
+
1587
+ function updateWidgetConfiguration({
1588
+ dispatch,
1589
+ role,
1590
+ configuration
1591
+ }) {
1592
+ updateConfiguration({
1593
+ dispatch,
1594
+ name: 'widgets',
1595
+ key: role,
1596
+ configuration
1597
+ });
1598
+ }
1599
+
1600
+ export { EntryStateProvider, collectionsSnapshot, getFileUrlTemplateHost, normalizeSectionConfigurationData, updateContentElementConfiguration, updateWidgetConfiguration, useActiveWidgets, useAdditionalSeedData, useAvailableQualities, useChapter, useChapters, useContentElement, useContentElementConsentVendor, useCredits, useCutOff, useDarkWidgets, useDownloadableFile, useEmbedOriginUrl, useEntryMetadata, useEntryStateDispatch, useEntryStructure, useEntryStructureWithContentElements, useEntryTranslations, useFile, useFileReferences, useFileRights, useFileWithInlineRights, useIsFileReferenced, useLegalInfo, useMainChapters, useMainStoryline, useNestedFiles, useSection, useSectionForegroundContentElements, useSectionPermaIdOfSubject, useSectionsWithChapter, useShareProviders, useShareUrl, useTheme, useWidget, watchCollections };