diff --git a/public/site/blueprints/pages/recit.yml b/public/site/blueprints/pages/recit.yml
index 92a7489..7640015 100644
--- a/public/site/blueprints/pages/recit.yml
+++ b/public/site/blueprints/pages/recit.yml
@@ -7,22 +7,25 @@ columns:
fields:
type: fields
fields:
- author:
+ blueprint:
+ type: headline
+ label: Page "{{ page.intendedTemplate }}"
+ author:
label: Auteur·ice(s)
type: text
width: 1/2
- cover:
+ cover:
label: Image de couverture
type: files
multiple: false
width: 1/2
- introduction:
+ introduction:
label: Introduction
type: writer
pages:
label: Pages
type: pages
- template:
+ template:
- carte
- geoformat
sidebar:
@@ -31,6 +34,3 @@ columns:
files:
label: Fichiers
type: files
-
-
-
diff --git a/public/site/plugins/virtual-print-page/index.php b/public/site/plugins/virtual-print-page/index.php
new file mode 100644
index 0000000..0e4bce9
--- /dev/null
+++ b/public/site/plugins/virtual-print-page/index.php
@@ -0,0 +1,37 @@
+ [
+ [
+ 'pattern' => '(:all)/print',
+ 'action' => function ($parentPath) {
+ // Trouver la page parente (le récit)
+ $parent = page($parentPath);
+
+ if (!$parent || $parent->intendedTemplate()->name() !== 'recit') {
+ return $this->next();
+ }
+
+ // Créer la page virtuelle avec Page::factory()
+ return Page::factory([
+ 'slug' => 'print',
+ 'template' => 'print',
+ 'parent' => $parent,
+ 'content' => [
+ 'title' => 'Impression - ' . $parent->title()->value(),
+ 'uuid' => Uuid::generate()
+ ]
+ ]);
+ }
+ ]
+ ]
+]);
diff --git a/public/site/snippets/header.php b/public/site/snippets/header.php
index 75fd430..e420f16 100644
--- a/public/site/snippets/header.php
+++ b/public/site/snippets/header.php
@@ -26,5 +26,5 @@
-
+ data-recit-url="= $recitJsonUrl ?>">
\ No newline at end of file
diff --git a/public/site/templates/print.php b/public/site/templates/print.php
new file mode 100644
index 0000000..8546ada
--- /dev/null
+++ b/public/site/templates/print.php
@@ -0,0 +1,17 @@
+parent();
+
+// Construire l'URL JSON du récit
+$recitJsonUrl = $recit->url() . '.json';
+?>
+ $recitJsonUrl]) ?>
+
+
diff --git a/public/site/templates/recit.json.php b/public/site/templates/recit.json.php
new file mode 100644
index 0000000..6c4ad10
--- /dev/null
+++ b/public/site/templates/recit.json.php
@@ -0,0 +1,202 @@
+isEmpty()) {
+ return null;
+ }
+
+ $file = $field->toFile();
+ if ($file) {
+ return $file->url();
+ }
+
+ return null;
+}
+
+/**
+ * Résout les images dans le contenu HTML
+ */
+function resolveImagesInHtml($html, $page) {
+ if (!$html) return $html;
+
+ // Remplacer les références file:// par les URLs réelles
+ return preg_replace_callback(
+ '/file:\/\/([a-z0-9]+)/i',
+ function($matches) use ($page) {
+ $uuid = $matches[1];
+ $file = $page->file("file://{$uuid}");
+ if (!$file) {
+ // Chercher dans les fichiers du site
+ $file = site()->file("file://{$uuid}");
+ }
+ return $file ? $file->url() : $matches[0];
+ },
+ $html
+ );
+}
+
+/**
+ * Parse les blocks Kirby en structure JSON
+ */
+function parseBlocks($blocksField, $page) {
+ if (!$blocksField || $blocksField->isEmpty()) {
+ return [];
+ }
+
+ $blocks = [];
+ foreach ($blocksField->toBlocks() as $block) {
+ $blockData = [
+ 'id' => $block->id(),
+ 'type' => $block->type(),
+ 'isHidden' => $block->isHidden(),
+ ];
+
+ switch ($block->type()) {
+ case 'text':
+ $blockData['content'] = [
+ 'text' => resolveImagesInHtml($block->text()->value(), $page)
+ ];
+ break;
+
+ case 'heading':
+ $blockData['content'] = [
+ 'level' => $block->level()->value() ?? 'h2',
+ 'text' => $block->text()->value()
+ ];
+ break;
+
+ case 'image':
+ $image = $block->image()->toFile();
+ $blockData['content'] = [
+ 'url' => $image ? $image->url() : null,
+ 'alt' => $block->alt()->value() ?? '',
+ 'caption' => $block->caption()->value() ?? '',
+ 'width' => $block->width()->value() ?? '100%',
+ 'position' => $block->position()->value() ?? 'auto'
+ ];
+ break;
+
+ case 'list':
+ $blockData['content'] = [
+ 'text' => $block->text()->value()
+ ];
+ break;
+
+ case 'quote':
+ $blockData['content'] = [
+ 'text' => $block->text()->value(),
+ 'citation' => $block->citation()->value() ?? ''
+ ];
+ break;
+
+ case 'video':
+ $blockData['content'] = [
+ 'url' => $block->url()->value(),
+ 'caption' => $block->caption()->value() ?? ''
+ ];
+ break;
+
+ case 'map':
+ $blockData['content'] = [
+ 'map' => $block->map()->value()
+ ];
+ break;
+
+ default:
+ $blockData['content'] = $block->content()->toArray();
+ }
+
+ $blocks[] = $blockData;
+ }
+
+ return $blocks;
+}
+
+/**
+ * Parse un chapitre
+ */
+function parseChapitre($chapitre) {
+ return [
+ 'id' => $chapitre->id(),
+ 'uuid' => $chapitre->uuid()->toString(),
+ 'template' => 'chapitre',
+ 'title' => $chapitre->title()->value(),
+ 'slug' => $chapitre->slug(),
+ 'blocks' => parseBlocks($chapitre->text(), $chapitre)
+ ];
+}
+
+/**
+ * Parse une carte
+ */
+function parseCarte($carte) {
+ return [
+ 'id' => $carte->id(),
+ 'uuid' => $carte->uuid()->toString(),
+ 'template' => 'carte',
+ 'title' => $carte->title()->value(),
+ 'slug' => $carte->slug(),
+ 'tags' => $carte->tags()->isNotEmpty() ? $carte->tags()->split() : [],
+ 'text' => resolveImagesInHtml($carte->text()->value(), $carte)
+ ];
+}
+
+/**
+ * Parse un geoformat
+ */
+function parseGeoformat($geoformat) {
+ $chapitres = [];
+ foreach ($geoformat->children()->listed() as $child) {
+ if ($child->intendedTemplate()->name() === 'chapitre') {
+ $chapitres[] = parseChapitre($child);
+ }
+ }
+
+ return [
+ 'id' => $geoformat->id(),
+ 'uuid' => $geoformat->uuid()->toString(),
+ 'template' => 'geoformat',
+ 'title' => $geoformat->title()->value(),
+ 'slug' => $geoformat->slug(),
+ 'subtitle' => $geoformat->subtitle()->value() ?? '',
+ 'tags' => $geoformat->tags()->isNotEmpty() ? $geoformat->tags()->split() : [],
+ 'cover' => resolveFileUrl($geoformat->cover(), $geoformat),
+ 'text' => resolveImagesInHtml($geoformat->text()->value(), $geoformat),
+ 'children' => $chapitres
+ ];
+}
+
+// Construction de la réponse JSON
+$data = [
+ 'id' => $page->id(),
+ 'uuid' => $page->uuid()->toString(),
+ 'template' => 'recit',
+ 'title' => $page->title()->value(),
+ 'slug' => $page->slug(),
+ 'author' => $page->author()->value() ?? '',
+ 'cover' => resolveFileUrl($page->cover(), $page),
+ 'introduction' => resolveImagesInHtml($page->introduction()->value(), $page),
+ 'children' => []
+];
+
+// Parser les enfants (geoformats et cartes)
+foreach ($page->children()->listed() as $child) {
+ $template = $child->intendedTemplate()->name();
+
+ if ($template === 'geoformat') {
+ $data['children'][] = parseGeoformat($child);
+ } elseif ($template === 'carte') {
+ $data['children'][] = parseCarte($child);
+ }
+}
+
+echo json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
diff --git a/public/site/templates/recit.php b/public/site/templates/recit.php
new file mode 100644
index 0000000..e0d016f
--- /dev/null
+++ b/public/site/templates/recit.php
@@ -0,0 +1,33 @@
+
+
+
+
+ = $page->title() ?>
+
+ author()->isNotEmpty()): ?>
+ = $page->author() ?>
+
+
+ cover()->isNotEmpty()): ?>
+
+ cover()->toFile()): ?>
+
+
+
+
+
+ introduction()->isNotEmpty()): ?>
+
+ = $page->introduction() ?>
+
+
+
+ Ouvrir l'éditeur d'impression
+
+
+
diff --git a/src/App.vue b/src/App.vue
index 5d129f6..d3b69f8 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -6,9 +6,14 @@ import PagePopup from './components/PagePopup.vue';
import PreviewLoader from './components/PreviewLoader.vue';
import { onMounted, ref, watch, computed, provide } from 'vue';
import { useStylesheetStore } from './stores/stylesheet';
+import { useRecitStore } from './stores/recit';
import Coloris from '@melloware/coloris';
const stylesheetStore = useStylesheetStore();
+const recitStore = useRecitStore();
+
+// Get recit URL from body data attribute (set by print.php template)
+const recitUrl = document.body.dataset.recitUrl || null;
const previewFrame1 = ref(null);
const previewFrame2 = ref(null);
const elementPopup = ref(null);
@@ -500,6 +505,16 @@ watch(
}
);
+// Re-render when recit data changes
+watch(
+ () => recitStore.data,
+ () => {
+ if (recitStore.data) {
+ renderPreview();
+ }
+ }
+);
+
// Print the PagedJS content
const printPreview = async () => {
const frame = activeFrame.value;
@@ -559,7 +574,15 @@ const printPreview = async () => {
}, 100);
};
-onMounted(() => renderPreview(true));
+onMounted(async () => {
+ // Load recit data if URL is provided (print mode)
+ if (recitUrl) {
+ await recitStore.loadRecit(recitUrl);
+ }
+
+ // Render preview after data is loaded
+ renderPreview(true);
+});
diff --git a/src/components/PagedJsWrapper.vue b/src/components/PagedJsWrapper.vue
index 5e45aab..3da03d0 100644
--- a/src/components/PagedJsWrapper.vue
+++ b/src/components/PagedJsWrapper.vue
@@ -1,54 +1,194 @@
-
-
- Accumsan arcu tristique purus eros pellentesque rutrum hendrerit phasellus
- euismod maximus rutrum vivamus dolor erat sollicitudin ut quam metus
- gravida proin nisl lacus sed lacus. Morbi fusce eros euismod varius ex
- ipsum erat lacus arcu nunc cursus a scelerisque tristique ipsum congue
- adipiscing suspendisse facilisis dolor morbi nulla orci massa. Vivamus nec
- nisl amet eros consectetur ut consectetur phasellus maecenas morbi felis
- pellentesque pellentesque ipsum ut a arcu sem facilisis eros tempus eu
- euismod sollicitudin. Nisl facilisis tempus tempus placerat lorem sed leo
- sit a leo tempus amet tristique felis gravida morbi congue aliquam nunc
- maximus ipsum ex nisl a. Leo felis leo gravida fusce lacus orci
- condimentum morbi eros amet portaest sit quam a hendrerit fusce quam
- tristique arcu id maximus nunc fusce suspendisse.
-
-
- Accumsan arcu tristique purus eros pellentesque rutrum hendrerit phasellus
- euismod maximus rutrum vivamus dolor erat sollicitudin ut quam metus
- gravida proin nisl lacus sed lacus. Morbi fusce eros euismod varius ex
- ipsum erat lacus arcu nunc cursus a scelerisque tristique ipsum congue
- adipiscing suspendisse facilisis dolor morbi nulla orci massa. Vivamus nec
- nisl amet eros consectetur ut consectetur phasellus maecenas morbi felis
- pellentesque pellentesque ipsum ut a arcu sem facilisis eros tempus eu
- euismod sollicitudin. Nisl facilisis tempus tempus placerat lorem sed leo
- sit a leo tempus amet tristique felis gravida morbi congue aliquam nunc
- maximus ipsum ex nisl a. Leo felis leo gravida fusce lacus orci
- condimentum morbi eros amet portaest sit quam a hendrerit fusce quam
- tristique arcu id maximus nunc fusce suspendisse.
-
-
- Lorem ipsum dolor sit amet consectetur adipiscing elit. Duis nibh tortor
-
-
+
+
+
+
+ Accumsan arcu tristique purus eros pellentesque rutrum hendrerit
+ phasellus euismod maximus rutrum vivamus dolor erat sollicitudin ut quam
+ metus gravida proin nisl lacus sed lacus.
+
+
+
-
- Chapter 1
- Lorem ipsum dolor sit amet
-
+
+
+
+
+
+
+ {{ item.title }}
+ {{ item.author }}
+
+
-
- Chapter 2
- consectetur adipiscing elit
-
+
+
-
- Chapter 3
- Duis nibh tortor, pellentesque eu suscipit vel
-
+
+
+ {{ item.title }}
+
+
+
+
+
+
+
+ {{ item.title }}
+
+ {{ tag }}
+
+
+
+
+
-
+
+
+
diff --git a/src/components/blocks/HeadingBlock.vue b/src/components/blocks/HeadingBlock.vue
new file mode 100644
index 0000000..2f16c27
--- /dev/null
+++ b/src/components/blocks/HeadingBlock.vue
@@ -0,0 +1,23 @@
+
+ {{ content.text }}
+
+
+
diff --git a/src/components/blocks/ImageBlock.vue b/src/components/blocks/ImageBlock.vue
new file mode 100644
index 0000000..057db60
--- /dev/null
+++ b/src/components/blocks/ImageBlock.vue
@@ -0,0 +1,40 @@
+
+
+
+ {{ content.caption }}
+
+
+
+
diff --git a/src/components/blocks/ListBlock.vue b/src/components/blocks/ListBlock.vue
new file mode 100644
index 0000000..45c368c
--- /dev/null
+++ b/src/components/blocks/ListBlock.vue
@@ -0,0 +1,12 @@
+
+
+
+
+
diff --git a/src/components/blocks/MapBlock.vue b/src/components/blocks/MapBlock.vue
new file mode 100644
index 0000000..92f6041
--- /dev/null
+++ b/src/components/blocks/MapBlock.vue
@@ -0,0 +1,14 @@
+
+
+
[Carte: {{ content.map }}]
+
+
+
+
diff --git a/src/components/blocks/QuoteBlock.vue b/src/components/blocks/QuoteBlock.vue
new file mode 100644
index 0000000..ba83c58
--- /dev/null
+++ b/src/components/blocks/QuoteBlock.vue
@@ -0,0 +1,15 @@
+
+
+
+ {{ content.citation }}
+
+
+
+
diff --git a/src/components/blocks/TextBlock.vue b/src/components/blocks/TextBlock.vue
new file mode 100644
index 0000000..f764be3
--- /dev/null
+++ b/src/components/blocks/TextBlock.vue
@@ -0,0 +1,12 @@
+
+
+
+
+
diff --git a/src/components/blocks/VideoBlock.vue b/src/components/blocks/VideoBlock.vue
new file mode 100644
index 0000000..0707ae4
--- /dev/null
+++ b/src/components/blocks/VideoBlock.vue
@@ -0,0 +1,38 @@
+
+
+
+ {{ content.caption }}
+
+
+
+
diff --git a/src/components/blocks/index.js b/src/components/blocks/index.js
new file mode 100644
index 0000000..9701a76
--- /dev/null
+++ b/src/components/blocks/index.js
@@ -0,0 +1,18 @@
+export { default as TextBlock } from './TextBlock.vue';
+export { default as HeadingBlock } from './HeadingBlock.vue';
+export { default as ImageBlock } from './ImageBlock.vue';
+export { default as ListBlock } from './ListBlock.vue';
+export { default as QuoteBlock } from './QuoteBlock.vue';
+export { default as VideoBlock } from './VideoBlock.vue';
+export { default as MapBlock } from './MapBlock.vue';
+
+// Block type to component mapping
+export const blockComponents = {
+ text: 'TextBlock',
+ heading: 'HeadingBlock',
+ image: 'ImageBlock',
+ list: 'ListBlock',
+ quote: 'QuoteBlock',
+ video: 'VideoBlock',
+ map: 'MapBlock'
+};
diff --git a/src/stores/recit.js b/src/stores/recit.js
new file mode 100644
index 0000000..bbe1c3c
--- /dev/null
+++ b/src/stores/recit.js
@@ -0,0 +1,127 @@
+import { defineStore } from 'pinia';
+import { ref, computed } from 'vue';
+
+export const useRecitStore = defineStore('recit', () => {
+ const data = ref(null);
+ const loading = ref(false);
+ const error = ref(null);
+
+ // Computed properties
+ const title = computed(() => data.value?.title ?? '');
+ const author = computed(() => data.value?.author ?? '');
+ const cover = computed(() => data.value?.cover ?? null);
+ const introduction = computed(() => data.value?.introduction ?? '');
+ const children = computed(() => data.value?.children ?? []);
+
+ // Flatten all content for PagedJS rendering
+ const flattenedContent = computed(() => {
+ if (!data.value) return [];
+
+ const items = [];
+
+ // Add recit intro as first section
+ items.push({
+ id: data.value.id,
+ template: 'recit',
+ title: data.value.title,
+ author: data.value.author,
+ cover: data.value.cover,
+ introduction: data.value.introduction
+ });
+
+ // Recursively flatten children
+ const flattenChildren = (children, depth = 0) => {
+ for (const child of children) {
+ if (child.template === 'geoformat') {
+ // Add geoformat header
+ items.push({
+ id: child.id,
+ template: 'geoformat',
+ title: child.title,
+ subtitle: child.subtitle,
+ tags: child.tags,
+ cover: child.cover,
+ text: child.text
+ });
+
+ // Add geoformat chapters
+ if (child.children && child.children.length > 0) {
+ flattenChildren(child.children, depth + 1);
+ }
+ } else if (child.template === 'chapitre') {
+ items.push({
+ id: child.id,
+ template: 'chapitre',
+ title: child.title,
+ blocks: child.blocks
+ });
+ } else if (child.template === 'carte') {
+ items.push({
+ id: child.id,
+ template: 'carte',
+ title: child.title,
+ tags: child.tags,
+ text: child.text
+ });
+ }
+ }
+ };
+
+ flattenChildren(data.value.children);
+
+ return items;
+ });
+
+ // Load recit data from URL
+ const loadRecit = async (url) => {
+ if (!url) {
+ error.value = 'No recit URL provided';
+ return;
+ }
+
+ loading.value = true;
+ error.value = null;
+
+ try {
+ const response = await fetch(url);
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ data.value = await response.json();
+ } catch (e) {
+ console.error('Error loading recit:', e);
+ error.value = e.message;
+ data.value = null;
+ } finally {
+ loading.value = false;
+ }
+ };
+
+ // Reset store
+ const reset = () => {
+ data.value = null;
+ loading.value = false;
+ error.value = null;
+ };
+
+ return {
+ // State
+ data,
+ loading,
+ error,
+
+ // Computed
+ title,
+ author,
+ cover,
+ introduction,
+ children,
+ flattenedContent,
+
+ // Actions
+ loadRecit,
+ reset
+ };
+});