diff --git a/src/components/editor/chart-editor.vue b/src/components/editor/chart-editor.vue index 56944770..0dd1da46 100644 --- a/src/components/editor/chart-editor.vue +++ b/src/components/editor/chart-editor.vue @@ -12,6 +12,7 @@ class="chart-btn bg-gray-100 cursor-pointer hover:bg-gray-200" id="modal-btn" @click="clearEditor()" + :disabled="!allowMany && chartConfigs.length > 0" >
@@ -44,6 +45,8 @@ :key="`${element.name}-${index}`" :chart="element" :configFileStructure="configFileStructure" + :sourceCounts="sourceCounts" + :lang="lang" @edit="editChart" @delete="$vfm.open(`${element.name}-${index}`)" > @@ -62,11 +65,18 @@ {{ $t('editor.chart.label.edit') }} | - {{ $t('editor.remove') }} + {{ $t('editor.remove') }} @@ -49,10 +49,7 @@ @@ -69,14 +66,8 @@ >
import { Options, Prop, Vue } from 'vue-property-decorator'; import { - ChartConfig, + BasePanel, ChartPanel, ConfigFileStructure, DefaultConfigs, @@ -102,6 +93,7 @@ import { PanelType, SlideshowPanel, SourceCounts, + TextPanel, VideoPanel } from '@/definitions'; @@ -110,12 +102,14 @@ import ImageEditorV from './image-editor.vue'; import TextEditorV from './text-editor.vue'; import MapEditorV from './map-editor.vue'; import VideoEditorV from './video-editor.vue'; +import SlideshowEditorV from './slideshow-editor.vue'; @Options({ components: { 'chart-editor': ChartEditorV, 'image-editor': ImageEditorV, 'text-editor': TextEditorV, + 'slideshow-editor': SlideshowEditorV, 'dynamic-editor': DynamicEditorV, 'map-editor': MapEditorV, 'video-editor': VideoEditorV @@ -130,7 +124,7 @@ export default class DynamicEditorV extends Vue { editors: Record = { text: 'text-editor', image: 'image-editor', - slideshow: 'image-editor', + slideshow: 'slideshow-editor', chart: 'chart-editor', map: 'map-editor', video: 'video-editor' @@ -151,11 +145,16 @@ export default class DynamicEditorV extends Vue { }, slideshow: { type: PanelType.Slideshow, - images: [] + items: [], + userCreated: true + }, + image: { + type: PanelType.Image, + src: '' }, chart: { type: PanelType.Chart, - charts: [] + src: '' }, map: { type: PanelType.Map, @@ -192,19 +191,9 @@ export default class DynamicEditorV extends Vue { // Save slide changes if neccessary and switch to the newly selected slide. this.saveChanges(); this.editingSlide = idx; - - // Image Panel to Slideshow Panel Conversion - if (this.panel.children[this.editingSlide].panel.type === 'image') { - (this.panel.children[this.editingSlide].panel as SlideshowPanel) = { - type: PanelType.Slideshow, - images: [this.panel.children[this.editingSlide].panel as ImagePanel] - }; - } } - removeSlide(item: number): void { - const panel = this.panel.children.find((panel: DynamicChildItem, idx: number) => idx === item)?.panel; - + removeSlide(panel: BasePanel, index?: number): void { // Update source counts based on which panel is removed. switch (panel?.type) { case 'map': { @@ -220,22 +209,27 @@ export default class DynamicEditorV extends Vue { case 'chart': { const chartPanel = panel as ChartPanel; - chartPanel.charts.forEach((chart: ChartConfig) => { - this.sourceCounts[chart.src] -= 1; - if (this.sourceCounts[chart.src] === 0) { - this.configFileStructure.zip.remove(`${chart.src.substring(chart.src.indexOf('/') + 1)}`); - } - }); + this.sourceCounts[chartPanel.src] -= 1; + if (this.sourceCounts[chartPanel.src] === 0) { + this.configFileStructure.zip.remove(`${chartPanel.src.substring(chartPanel.src.indexOf('/') + 1)}`); + } + break; + } + + case 'image': { + const imagePanel = panel as ImagePanel; + + this.sourceCounts[imagePanel.src] -= 1; + if (this.sourceCounts[imagePanel.src] === 0) { + this.configFileStructure.zip.remove(`${imagePanel.src.substring(imagePanel.src.indexOf('/') + 1)}`); + } break; } case 'slideshow': { const slideshowPanel = panel as SlideshowPanel; - slideshowPanel.images.forEach((image: ImagePanel) => { - this.sourceCounts[image.src] -= 1; - if (this.sourceCounts[image.src] === 0) { - this.configFileStructure.zip.remove(`${image.src.substring(image.src.indexOf('/') + 1)}`); - } + slideshowPanel.items.forEach((item: TextPanel | ImagePanel | MapPanel | ChartPanel) => { + this.removeSlide(item); }); break; } @@ -254,12 +248,14 @@ export default class DynamicEditorV extends Vue { } } - // Remove the panel itself. - this.panel.children = this.panel.children.filter((panel: DynamicChildItem, idx: number) => idx !== item); + if (index) { + // Remove the panel itself. + this.panel.children = this.panel.children.filter((panel: DynamicChildItem, idx: number) => idx !== index); - // If the slide being removed is the currently selected slide, unselect it. - if (this.editingSlide === item) { - this.editingSlide = -1; + // If the slide being removed is the currently selected slide, unselect it. + if (this.editingSlide === index) { + this.editingSlide = -1; + } } } @@ -275,6 +271,23 @@ export default class DynamicEditorV extends Vue { this.panel.children.push(newConfig); } + determineEditorType(panel: BasePanel): string { + if (panel.type !== PanelType.Slideshow) return panel.type; + if ((panel as SlideshowPanel).items.length === 0 || (panel as SlideshowPanel).userCreated) + return PanelType.Slideshow; + + // Determine whether the slideshow consists of only charts. If so, display the chart editor. + const allCharts = (panel as SlideshowPanel).items.every((item: BasePanel) => item.type === PanelType.Chart); + if (allCharts) return PanelType.Chart; + + // Determine whether the slideshow consists of only images. If so, display the image editor. + const allImages = (panel as SlideshowPanel).items.every((item: BasePanel) => item.type === PanelType.Image); + if (allImages) return PanelType.Image; + + // Otherwise display the slideshow editor. + return PanelType.Slideshow; + } + saveChanges(): void { if ( this.$refs.slide !== undefined && diff --git a/src/components/editor/helpers/chart-preview.vue b/src/components/editor/helpers/chart-preview.vue index 89447125..b4903d72 100644 --- a/src/components/editor/helpers/chart-preview.vue +++ b/src/components/editor/helpers/chart-preview.vue @@ -67,12 +67,15 @@ import { DQVChartConfig, LineSeriesData, PieDataRow, - PieSeriesData + PieSeriesData, + SourceCounts } from '@/definitions'; export default class ChartPreviewV extends Vue { @Prop() chart!: ChartConfig; @Prop() configFileStructure!: ConfigFileStructure; + @Prop() sourceCounts!: SourceCounts; + @Prop() lang!: string; loading = true; chartIdx = 0; @@ -107,15 +110,30 @@ export default class ChartPreviewV extends Vue { }, (newChart: string) => { const chart = JSON.parse(newChart); - const chartConfig = { - name: chart.title.text, - config: chart, - src: '' - }; - this.$emit('edit', { oldChart: this.chart, newChart: chartConfig }); - this.chartConfig = chartConfig; - this.chartName = chartConfig.name; - this.chartIdx += 1; + const newName = `${this.configFileStructure.uuid}/charts/${this.lang}/${chart.title.text}.json`; + + // Check to see if a chart already exists with the provided name. If so, alert the user and re-prompt. + if (this.sourceCounts[newName] > 0 && chart.title.text != this.chart.name) { + alert( + this.$t('editor.chart.label.nameExists', { + name: chart.title.text + }) + ); + + // Re-open the editor the the issue can be fixed. + setTimeout(() => this.modalEditor.show(), 100); + } else { + const chartConfig = { + name: chart.title.text, + config: chart, + src: '' + }; + + this.$emit('edit', { oldChart: this.chart, newChart: chartConfig }); + this.chartConfig = chartConfig; + this.chartName = chartConfig.name; + this.chartIdx += 1; + } } ); diff --git a/src/components/editor/image-editor.vue b/src/components/editor/image-editor.vue index 33235764..84ba2f4f 100644 --- a/src/components/editor/image-editor.vue +++ b/src/components/editor/image-editor.vue @@ -7,6 +7,7 @@ @dragover.prevent="() => (dragging = true)" @dragleave.prevent="() => (dragging = false)" @drop.prevent="dropImages($event)" + v-if="allowMany || (!allowMany && imagePreviews.length === 0)" >
- + - + {{ $t('editor.image.reorder') }} @@ -77,10 +82,11 @@ import ImagePreviewV from '@/components/editor/helpers/image-preview.vue'; } }) export default class ImageEditorV extends Vue { - @Prop() panel!: SlideshowPanel; + @Prop() panel!: ImagePanel | SlideshowPanel; @Prop() configFileStructure!: ConfigFileStructure; @Prop() lang!: string; @Prop() sourceCounts!: SourceCounts; + @Prop({ default: true }) allowMany!: boolean; dragging = false; edited = false; @@ -95,12 +101,20 @@ export default class ImageEditorV extends Vue { } mounted(): void { - if (this.panel.images !== undefined && this.panel.images.length) { + // This basically allows us to access the image(s) using one consistent variable instead of needing to check panel type. + const images = + this.panel.type === PanelType.Slideshow + ? (this.panel.items as Array) + : this.panel.src + ? [this.panel] + : []; + + if (images !== undefined && images.length) { // Set images as loading until they are all loaded and resolve. this.imagePreviewsLoading = true; // Process each existing image. - this.panel.images.map((image: ImagePanel) => { + images.map((image: ImagePanel) => { // Check if the config file exists in the ZIP folder first. const assetSrc = `${image.src.substring(image.src.indexOf('/') + 1)}`; const filename = image.src.replace(/^.*[\\/]/, ''); @@ -158,7 +172,13 @@ export default class ImageEditorV extends Vue { dropImages(e: DragEvent): void { if (e.dataTransfer !== null) { - const files = [...e.dataTransfer.files]; + let files = [...e.dataTransfer.files]; + + // If allowMany is false, take the first one. + if (!this.allowMany) { + files = [files[0]]; + } + this.imagePreviews.push( ...files.map((file: File) => { // Add the uploaded images to the product ZIP file. @@ -203,14 +223,48 @@ export default class ImageEditorV extends Vue { saveChanges(): void { if (this.edited) { - this.panel.images = this.imagePreviews.map((imageFile: ImageFile) => { - return { - ...imageFile, - src: `${this.configFileStructure.uuid}/assets/${this.lang}/${imageFile.id}`, - type: PanelType.Image - }; + // Delete the existing properties so we can rebuild the object. + Object.keys(this.panel).forEach((key) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + delete this.panel[key]; }); - this.panel.caption = this.slideshowCaption ?? undefined; + + // Handle case where everything is deleted. + if (this.imagePreviews.length === 0) { + this.panel.type = PanelType.Image; + (this.panel as ImagePanel).src = ''; + } else if (this.imagePreviews.length === 1) { + // If there's only one image uploaded, convert this to an image panel. + this.panel.type = PanelType.Image; + + // Grab the one image from the array. + const imageFile = this.imagePreviews[0]; + + // Sort of gross, but required to update the panel config as we're not allowed to directly manipulate props. + Object.keys(imageFile).forEach((key) => { + if (key === 'id') return; // we don't need this one. + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + (this.panel as ImagePanel)[key] = imageFile[key]; + }); + + (this.panel as ImagePanel).src = `${this.configFileStructure.uuid}/assets/${this.lang}/${imageFile.id}`; + } else { + // Otherwise, convert this to a slideshow panel. + this.panel.type = PanelType.Slideshow; + this.panel.caption = this.slideshowCaption ?? undefined; + + // Turn each of the image configs into an image panel and add them to the slidesow. + (this.panel as SlideshowPanel).items = this.imagePreviews.map((imageFile: ImageFile) => { + return { + ...imageFile, + src: `${this.configFileStructure.uuid}/assets/${this.lang}/${imageFile.id}`, + type: PanelType.Image + } as ImagePanel; + }); + } } this.edited = false; } diff --git a/src/components/editor/metadata-editor.vue b/src/components/editor/metadata-editor.vue index 44ef172b..528f9993 100644 --- a/src/components/editor/metadata-editor.vue +++ b/src/components/editor/metadata-editor.vue @@ -160,7 +160,6 @@ import { AxiosResponse } from 'axios'; import { AudioPanel, BasePanel, - ChartConfig, ChartPanel, ConfigFileStructure, DynamicChildItem, @@ -172,6 +171,7 @@ import { SlideshowPanel, SourceCounts, StoryRampConfig, + TextPanel, VideoPanel } from '@/definitions'; import { VueSpinnerOval } from 'vue3-spinners'; @@ -440,16 +440,16 @@ export default class MetadataEditorV extends Vue { }); break; case 'slideshow': - (panel as SlideshowPanel).images.forEach((image: ImagePanel) => { - this.incrementSourceCount(image.src); + (panel as SlideshowPanel).items.forEach((item: ChartPanel | TextPanel | ImagePanel | MapPanel) => { + this.panelSourceHelper(item); }); break; case 'chart': - (panel as ChartPanel).charts.forEach((chart: ChartConfig) => { - this.incrementSourceCount(chart.src); - }); + this.incrementSourceCount((panel as ChartPanel).src); break; case 'image': + this.incrementSourceCount((panel as ImagePanel).src); + break; case 'video': if ((panel as VideoPanel).videoType === 'local') { this.incrementSourceCount((panel as VideoPanel).src); @@ -461,6 +461,8 @@ export default class MetadataEditorV extends Vue { case 'map': this.incrementSourceCount((panel as MapPanel).config); break; + case 'text': + break; default: break; } @@ -567,17 +569,7 @@ export default class MetadataEditorV extends Vue { this.metadata.tocOrientation = config.tocOrientation; this.metadata.dateModified = config.dateModified; - // Conversion for individual image panels to slideshow for gallery display this.slides = config.slides; - this.slides.forEach((slide: Slide) => { - if (slide.panel.length === 2 && slide.panel[1].type === 'image') { - const newSlide = { - type: 'slideshow', - images: [slide.panel[1]] - }; - slide.panel[1] = newSlide; - } - }); const logo = config.introSlide.logo?.src; if (logo) { diff --git a/src/components/editor/slide-editor.vue b/src/components/editor/slide-editor.vue index b194e458..f647411d 100644 --- a/src/components/editor/slide-editor.vue +++ b/src/components/editor/slide-editor.vue @@ -28,7 +28,7 @@ type="checkbox" class="rounded-none cursor-pointer w-4 h-4" v-model="rightOnly" - :disabled="rightOnly && currentSlide.panel[panelIndex].type === 'dynamic'" + :disabled="rightOnly && determineEditorType(currentSlide.panel[panelIndex]) === 'dynamic'" @change.stop="$vfm.open(`right-only-${slideIndex}`)" /> @@ -189,14 +189,12 @@ $vfm.open(`change-slide-${slideIndex}`); newType = ($event.target as HTMLInputElement).value; " - :value="currentSlide.panel[panelIndex].type" + :value="determineEditorType(currentSlide.panel[panelIndex])" > @@ -205,8 +203,8 @@ = { text: 'text-editor', image: 'image-editor', - slideshow: 'image-editor', + slideshow: 'slideshow-editor', chart: 'chart-editor', map: 'map-editor', video: 'video-editor', @@ -328,11 +328,16 @@ export default class SlideEditorV extends Vue { }, slideshow: { type: PanelType.Slideshow, - images: [] + items: [], + userCreated: true + }, + image: { + type: PanelType.Image, + src: '' }, chart: { type: PanelType.Chart, - charts: [] + src: '' }, map: { type: PanelType.Map, @@ -375,24 +380,30 @@ export default class SlideEditorV extends Vue { break; } + case 'image': { + const imagePanel = panel as ImagePanel; + this.sourceCounts[imagePanel.src] -= 1; + if (this.sourceCounts[imagePanel.src] === 0) { + this.configFileStructure.zip.remove(`${imagePanel.src.substring(imagePanel.src.indexOf('/') + 1)}`); + } + + break; + } + case 'chart': { const chartPanel = panel as ChartPanel; - chartPanel.charts.forEach((chart: ChartConfig) => { - this.sourceCounts[chart.src] -= 1; - if (this.sourceCounts[chart.src] === 0) { - this.configFileStructure.zip.remove(`${chart.src.substring(chart.src.indexOf('/') + 1)}`); - } - }); + this.sourceCounts[chartPanel.src] -= 1; + if (this.sourceCounts[chartPanel.src] === 0) { + this.configFileStructure.zip.remove(`${chartPanel.src.substring(chartPanel.src.indexOf('/') + 1)}`); + } + break; } case 'slideshow': { const slideshowPanel = panel as SlideshowPanel; - slideshowPanel.images.forEach((image: ImagePanel) => { - this.sourceCounts[image.src] -= 1; - if (this.sourceCounts[image.src] === 0) { - this.configFileStructure.zip.remove(`${image.src.substring(image.src.indexOf('/') + 1)}`); - } + slideshowPanel.items.forEach((item: TextPanel | ImagePanel | MapPanel | ChartPanel) => { + this.removeSourceCounts(item); }); break; } @@ -417,6 +428,10 @@ export default class SlideEditorV extends Vue { }); break; } + + case 'text': { + break; + } } } @@ -434,7 +449,26 @@ export default class SlideEditorV extends Vue { } cancelTypeChange(): void { - (this.$refs.typeSelector as HTMLSelectElement).value = this.currentSlide.panel[this.panelIndex].type; + (this.$refs.typeSelector as HTMLSelectElement).value = this.determineEditorType( + this.currentSlide.panel[this.panelIndex] + ); + } + + determineEditorType(panel: BasePanel): string { + if (panel.type !== PanelType.Slideshow) return panel.type; + if ((panel as SlideshowPanel).items.length === 0 || (panel as SlideshowPanel).userCreated) + return PanelType.Slideshow; + + // Determine whether the slideshow consists of only charts. If so, display the chart editor. + const allCharts = (panel as SlideshowPanel).items.every((item: BasePanel) => item.type === PanelType.Chart); + if (allCharts) return PanelType.Chart; + + // Determine whether the slideshow consists of only images. If so, display the image editor. + const allImages = (panel as SlideshowPanel).items.every((item: BasePanel) => item.type === PanelType.Image); + if (allImages) return PanelType.Image; + + // Otherwise display the slideshow editor. + return PanelType.Slideshow; } toggleRightOnly(): void { diff --git a/src/components/editor/slide-toc.vue b/src/components/editor/slide-toc.vue index ff82b019..c4a316bc 100644 --- a/src/components/editor/slide-toc.vue +++ b/src/components/editor/slide-toc.vue @@ -252,24 +252,30 @@ export default class SlideTocV extends Vue { break; } + case 'image': { + const imagePanel = panel as ImagePanel; + this.sourceCounts[imagePanel.src] -= 1; + if (this.sourceCounts[imagePanel.src] === 0) { + this.configFileStructure.zip.remove(`${imagePanel.src.substring(imagePanel.src.indexOf('/') + 1)}`); + } + + break; + } + case 'chart': { const chartPanel = panel as ChartPanel; - chartPanel.charts.forEach((chart: ChartConfig) => { - this.sourceCounts[chart.src] -= 1; - if (this.sourceCounts[chart.src] === 0) { - this.configFileStructure.zip.remove(`${chart.src.substring(chart.src.indexOf('/') + 1)}`); - } - }); + this.sourceCounts[chartPanel.src] -= 1; + if (this.sourceCounts[chartPanel.src] === 0) { + this.configFileStructure.zip.remove(`${chartPanel.src.substring(chartPanel.src.indexOf('/') + 1)}`); + } + break; } case 'slideshow': { const slideshowPanel = panel as SlideshowPanel; - slideshowPanel.images.forEach((image: ImagePanel) => { - this.sourceCounts[image.src] -= 1; - if (this.sourceCounts[image.src] === 0) { - this.configFileStructure.zip.remove(`${image.src.substring(image.src.indexOf('/') + 1)}`); - } + slideshowPanel.items.forEach((item: TextPanel | MapPanel | ChartPanel | ImagePanel) => { + this.removeSourceHelper(item); }); break; } @@ -294,6 +300,10 @@ export default class SlideTocV extends Vue { }); break; } + + case 'text': { + break; + } } } diff --git a/src/components/editor/slideshow-editor.vue b/src/components/editor/slideshow-editor.vue new file mode 100644 index 00000000..e043bb5f --- /dev/null +++ b/src/components/editor/slideshow-editor.vue @@ -0,0 +1,319 @@ + + + + + diff --git a/src/definitions.ts b/src/definitions.ts index d15ea233..4beba828 100644 --- a/src/definitions.ts +++ b/src/definitions.ts @@ -192,12 +192,9 @@ export interface ImagePanel extends BasePanel { src: string; width?: number; height?: number; - temp?: string; - class?: string; fullscreen?: boolean; altText?: string; caption?: string; - tooltip?: string; } export interface VideoPanel extends BasePanel { @@ -219,16 +216,19 @@ export interface AudioPanel extends BasePanel { export interface SlideshowPanel extends BasePanel { type: PanelType.Slideshow; - images: ImagePanel[]; - fullscreen?: boolean; + items: Array; loop?: boolean; caption?: string; + userCreated?: boolean; // used to determine whether this was automatically converted to slideshow or not } export interface ChartPanel extends BasePanel { type: PanelType.Chart; - charts: ChartConfig[]; - fullscreen?: boolean; + src: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + config?: any; + name?: string; + options?: DQVOptions; } export interface ChartConfig { @@ -266,4 +266,5 @@ export interface DefaultConfigs { dynamic: DynamicPanel; map: MapPanel; video: VideoPanel; + image: ImagePanel; } diff --git a/src/lang/lang.csv b/src/lang/lang.csv index bf910478..25eead84 100644 --- a/src/lang/lang.csv +++ b/src/lang/lang.csv @@ -78,6 +78,8 @@ editor.chart.label.name,Name,1,Nom,1 editor.chart.label.edit,Edit,1,Éditer,1 editor.chart.label.empty,Empty,1,Vide,1 editor.chart.label.create,Add new chart,1,Ajouter un nouveau graphique,1 +editor.chart.label.info,Interactive chart,1,[FR] Interactive chart,0 +editor.chart.label.nameExists,There is already an existing chart with the name {name}.,1,There is already an existing chart with the name {name}.,0 editor.chart.label.info,Interactive charts ({num}),1,Graphiques interactifs ({num}),1 editor.chart.delete.confirm,Are you sure you want to delete the chart {name}?,1,Voulez-vous vraiment supprimer le graphique {nom}?,1 editor.map.title,Map title,1,Titre de la carte,1 @@ -98,6 +100,11 @@ editor.map.timeslider.warning,⚠️Warning! Please ensure that:,1,⚠️Avertis editor.map.timeslider.warning.bullet1,All range and start values are positive integers.,1,Toutes les valeurs de la tranche et du début sont des nombres entiers positifs.,1 editor.map.timeslider.warning.bullet2,The "to" value is greater than or equal to the "from" value.,1,La valeur « À » est supérieure ou égale à la valeur « De ».,1 editor.map.timeslider.warning.end,"Otherwise, your time slider config cannot be saved.",1,Autrement, la configuration du curseur temporel ne pourra pas être enregistrée.,1 +editor.slideshow.label.info,Slideshow items ({num}),1,[FR] Slideshow items ({num}),0 +editor.slideshow.label.create,Add new item,1,[FR] Add new item,0 +editor.slideshow.label.edit,Edit existing item,1,[FR] Edit existing item,0 +editor.slideshow.label.type,Type,1,[FR] Type,0 +editor.slideshow.label.add,Add,1,[FR] Add,0 editor.slides.title,SLIDES,1,DIAPOSITIVES,1 editor.slides.addSlide,"New Slide",1,Nouvelle diapositive,1 editor.slides.copyFromLang,"Copy slides from the other language",1,"Copier les diapositives de l’autre langue",1