1515 </BrandedButton >
1616 </div >
1717
18- <div
19- v-for =" (bloc, index) in workingBlocs"
20- :key =" bloc.id"
21- class =" relative"
22- :class =" bloc.class !== 'HeroBloc' && (striped ? 'py-24 odd:bg-gray-some even:bg-white' : 'py-12')"
23- >
24- <!-- Add button above the bloc (absolute positioned) -->
25- <div
26- v-if =" isEditing"
27- class =" absolute -top-4 left-0 right-0 flex items-center justify-center"
28- >
29- <AddBlocDropdown
30- :for-organization =" forOrganization "
31- @new-bloc =" workingBlocs .splice (index , 0 , $event )"
32- >
33- <BrandedButton
34- color="tertiary"
35- size="xs"
36- class="bg-white shadow-sm"
37- :icon =" RiAddLine "
38- >
39- {{ $t('Ajouter un bloc') }}
40- </BrandedButton >
41- </AddBlocDropdown >
42- </div >
43- <!-- Bloc toolbar in edit mode -->
44- <div
45- v-if =" isEditing"
46- class =" absolute left-4 top-4 flex flex-row gap-2 z-10 min-[1530px]:left-8 min-[1530px]:top-1/2 min-[1530px]:-translate-y-1/2 min-[1530px]:flex-col"
47- >
48- <BrandedButton
49- color="tertiary"
50- :icon =" RiArrowUpLine "
51- :disabled =" index === 0 "
52- :title =" $t (' Monter' )"
53- @click =" moveBloc (index , - 1 )"
54- />
55- <BrandedButton
56- color="tertiary"
57- :icon =" RiArrowDownLine "
58- :disabled =" index === workingBlocs .length - 1 "
59- :title =" $t (' Descendre' )"
60- @click =" moveBloc (index , 1 )"
61- />
62- <BrandedButton
63- color="tertiary"
64- :icon =" RiDeleteBinLine "
65- :title =" $t (' Supprimer' )"
66- @click =" workingBlocs .splice (index , 1 )"
67- />
68- </div >
69-
70- <!-- Full-width blocs (like HeroBloc) -->
18+ <!-- Side menu, outside of the edit mode whose toolbar lives in the side margins: the
19+ full-width blocs opening the page stay above it, the others go next to it -->
20+ <template v-if =" sidemenu && ! isEditing " >
7121 <component
7222 :is =" blocsTypes[bloc.class].component"
73- v-if = " blocsTypes[ bloc.class] && 'fullWidth' in blocsTypes[bloc.class] && blocsTypes[bloc.class].component "
74- v-model = " (workingBlocs[index] as any) "
75- :edit = " isEditing "
76- v-bind = " bloc.class === 'LinksListBloc' ? { 'main-color': mainColor } : {} "
23+ v-for = " bloc in leadingFullWidthBlocs "
24+ :key = " bloc.id "
25+ :model-value = " (bloc as any) "
26+ :edit = " false "
7727 />
28+ <div class =" container md:flex md:gap-8 py-12" >
29+ <HeadingsSidemenu
30+ v-if =" sidemenuHeadings .length "
31+ :headings =" sidemenuHeadings "
32+ class="md:w-1 /4 shrink-0"
33+ />
34+ <div class =" flex-1 min-w-0 space-y-12" >
35+ <template
36+ v-for =" bloc in workingBlocs .slice (leadingFullWidthBlocs .length )"
37+ :key =" bloc .id "
38+ >
39+ <component
40+ :is =" blocsTypes[bloc.class].component"
41+ v-if =" blocsTypes[bloc.class]?.component"
42+ :model-value =" (bloc as any)"
43+ :edit =" false"
44+ v-bind =" bloc.class === 'LinksListBloc' ? { 'main-color': mainColor } : {}"
45+ />
46+ </template >
47+ </div >
48+ </div >
49+ </template >
7850
79- <!-- Other blocs use container layout -- >
51+ < template v-else >
8052 <div
81- v-else
82- class =" container"
53+ v-for =" (bloc, index) in workingBlocs"
54+ :key =" bloc.id"
55+ class =" relative"
56+ :class =" bloc.class !== 'HeroBloc' && (striped ? 'py-24 odd:bg-gray-some even:bg-white' : 'py-12')"
8357 >
58+ <!-- Add button above the bloc (absolute positioned) -->
59+ <div
60+ v-if =" isEditing"
61+ class =" absolute -top-4 left-0 right-0 flex items-center justify-center"
62+ >
63+ <AddBlocDropdown
64+ :for-organization =" forOrganization "
65+ @new-bloc =" workingBlocs .splice (index , 0 , $event )"
66+ >
67+ <BrandedButton
68+ color="tertiary"
69+ size="xs"
70+ class="bg-white shadow-sm"
71+ :icon =" RiAddLine "
72+ >
73+ {{ $t('Ajouter un bloc') }}
74+ </BrandedButton >
75+ </AddBlocDropdown >
76+ </div >
77+ <!-- Bloc toolbar in edit mode -->
78+ <div
79+ v-if =" isEditing"
80+ class =" absolute left-4 top-4 flex flex-row gap-2 z-10 min-[1530px]:left-8 min-[1530px]:top-1/2 min-[1530px]:-translate-y-1/2 min-[1530px]:flex-col"
81+ >
82+ <BrandedButton
83+ color="tertiary"
84+ :icon =" RiArrowUpLine "
85+ :disabled =" index === 0 "
86+ :title =" $t (' Monter' )"
87+ @click =" moveBloc (index , - 1 )"
88+ />
89+ <BrandedButton
90+ color="tertiary"
91+ :icon =" RiArrowDownLine "
92+ :disabled =" index === workingBlocs .length - 1 "
93+ :title =" $t (' Descendre' )"
94+ @click =" moveBloc (index , 1 )"
95+ />
96+ <BrandedButton
97+ color="tertiary"
98+ :icon =" RiDeleteBinLine "
99+ :title =" $t (' Supprimer' )"
100+ @click =" workingBlocs .splice (index , 1 )"
101+ />
102+ </div >
103+
104+ <!-- Full-width blocs (like HeroBloc) -->
84105 <component
85106 :is =" blocsTypes[bloc.class].component"
86- v-if =" blocsTypes[bloc.class]? .component"
107+ v-if =" blocsTypes[bloc.class] && 'fullWidth' in blocsTypes[bloc.class] && blocsTypes[bloc.class] .component"
87108 v-model =" (workingBlocs[index] as any)"
88109 :edit =" isEditing"
89110 v-bind =" bloc.class === 'LinksListBloc' ? { 'main-color': mainColor } : {}"
90111 />
91- </div >
92112
93- <!-- Add button below the last bloc -->
94- <div
95- v-if =" isEditing && index === workingBlocs.length - 1"
96- class =" absolute -bottom-4 left-0 right-0 flex items-center justify-center"
97- >
98- <AddBlocDropdown
99- :for-organization =" forOrganization "
100- @new-bloc =" workingBlocs .push ($event )"
113+ <!-- Other blocs use container layout -->
114+ <div
115+ v-else
116+ class =" container"
101117 >
102- <BrandedButton
103- color="tertiary"
104- size="xs"
105- class="bg-white shadow-sm"
106- :icon =" RiAddLine "
118+ <component
119+ :is =" blocsTypes[bloc.class].component"
120+ v-if =" blocsTypes[bloc.class]?.component"
121+ v-model =" (workingBlocs[index] as any)"
122+ :edit =" isEditing"
123+ v-bind =" bloc.class === 'LinksListBloc' ? { 'main-color': mainColor } : {}"
124+ />
125+ </div >
126+
127+ <!-- Add button below the last bloc -->
128+ <div
129+ v-if =" isEditing && index === workingBlocs.length - 1"
130+ class =" absolute -bottom-4 left-0 right-0 flex items-center justify-center"
131+ >
132+ <AddBlocDropdown
133+ :for-organization =" forOrganization "
134+ @new-bloc =" workingBlocs .push ($event )"
107135 >
108- {{ $t('Ajouter un bloc') }}
109- </BrandedButton >
110- </AddBlocDropdown >
136+ <BrandedButton
137+ color="tertiary"
138+ size="xs"
139+ class="bg-white shadow-sm"
140+ :icon =" RiAddLine "
141+ >
142+ {{ $t('Ajouter un bloc') }}
143+ </BrandedButton >
144+ </AddBlocDropdown >
145+ </div >
111146 </div >
112- </div >
147+ </template >
113148
114149 <!-- Empty state. In edit mode it invites adding a first bloc; in view mode it
115150 is only shown when the host opts in via `emptyCtaLabel` (e.g. an org admin
177212<script setup lang="ts">
178213import type { ComponentProps } from ' vue-component-type-helpers'
179214import * as Sentry from ' @sentry/nuxt'
180- import { BrandedButton } from ' @datagouv/components-next'
215+ import { BrandedButton , formatMarkdown } from ' @datagouv/components-next'
181216import { RiAddLine , RiArrowDownLine , RiArrowUpLine , RiDeleteBinLine , RiEdit2Line } from ' @remixicon/vue'
182217import AddBlocDropdown from ' ./AddBlocDropdown.vue'
218+ import HeadingsSidemenu from ' ~/components/Sidemenu/HeadingsSidemenu.vue'
183219import type { PageBloc } from ' ~/types/pages'
220+ import type { Heading } from ' ~/utils/headings'
184221import { isMeAdmin } from ' ~/utils/auth'
185222
186223const blocsTypes = useBlocsTypes ()
@@ -190,6 +227,7 @@ const props = withDefaults(defineProps<{
190227 edit? : boolean
191228 editable? : boolean
192229 striped? : boolean
230+ sidemenu? : boolean
193231 hideEditButton? : boolean
194232 // Trims the "add bloc" menu for organization presentation pages (no Hero, Markdown
195233 // moved into the layout group). See AddBlocDropdown.
@@ -204,6 +242,7 @@ const props = withDefaults(defineProps<{
204242 edit: false ,
205243 editable: false ,
206244 striped: true ,
245+ sidemenu: false ,
207246 hideEditButton: false ,
208247 forOrganization: false ,
209248 mainColor: ' primary' ,
@@ -238,6 +277,21 @@ watchEffect(() => {
238277
239278const workingBlocs = computed (() => localBlocs .value ?? props .blocs )
240279
280+ const isFullWidth = (bloc : PageBloc ) => ' fullWidth' in (blocsTypes [bloc .class ] ?? {})
281+ const leadingFullWidthBlocs = computed (() => {
282+ const index = workingBlocs .value .findIndex (bloc => ! isFullWidth (bloc ))
283+ return workingBlocs .value .slice (0 , index === - 1 ? workingBlocs .value .length : index )
284+ })
285+ // The title of each bloc, then the headings of its Markdown, rendered with the same options
286+ // as MarkdownBloc so that their ids are the ones on the page
287+ const sidemenuHeadings = computed <Array <Heading >>(() => workingBlocs .value
288+ .filter (bloc => ! isFullWidth (bloc ))
289+ .flatMap (bloc => [
290+ ... (bloc .title ? [{ id: slugify (bloc .title ), label: bloc .title , level: 2 }] : []),
291+ ... (bloc .class === ' MarkdownBloc' ? anchorHeadings (formatMarkdown (bloc .content , { minDepth: 2 , noHeadings: false })).headings : []),
292+ ]),
293+ )
294+
241295// Check if there are unsaved changes
242296const hasUnsavedChanges = computed (() => {
243297 return localBlocs .value !== null && JSON .stringify (toRaw (localBlocs .value )) !== JSON .stringify (toRaw (props .blocs ))
0 commit comments