Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implement dynamic slideshow editor #269

Merged
merged 1 commit into from
Apr 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 71 additions & 10 deletions src/components/editor/chart-editor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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"
>
<div class="flex items-center">
<svg height="18px" width="18px" viewBox="0 0 23 21" xmlns="http://www.w3.org/2000/svg">
Expand Down Expand Up @@ -44,6 +45,8 @@
:key="`${element.name}-${index}`"
:chart="element"
:configFileStructure="configFileStructure"
:sourceCounts="sourceCounts"
:lang="lang"
@edit="editChart"
@delete="$vfm.open(`${element.name}-${index}`)"
></ChartPreview>
Expand All @@ -62,11 +65,18 @@

<script lang="ts">
import { Options, Prop, Vue } from 'vue-property-decorator';
import { ChartConfig, ChartPanel, ConfigFileStructure, Highchart, SourceCounts } from '@/definitions';
import {
ChartConfig,
ChartPanel,
ConfigFileStructure,
Highchart,
PanelType,
SlideshowPanel,
SourceCounts
} from '@/definitions';
import ChartPreviewV from '@/components/editor/helpers/chart-preview.vue';
import ConfirmationModalV from '@/components/editor/helpers/confirmation-modal.vue';
import draggable from 'vuedraggable';
import { chart } from 'highcharts';

@Options({
components: {
Expand All @@ -78,10 +88,11 @@ import { chart } from 'highcharts';
}
})
export default class ChartEditorV extends Vue {
@Prop() panel!: ChartPanel;
@Prop() panel!: ChartPanel | SlideshowPanel;
@Prop() configFileStructure!: ConfigFileStructure;
@Prop() lang!: string;
@Prop() sourceCounts!: SourceCounts;
@Prop({ default: true }) allowMany!: boolean;

edited = false;

Expand All @@ -106,9 +117,17 @@ export default class ChartEditorV extends Vue {
);
});

// This allows us to access the chart(s) using one consistent variable instead of needing to check panel type.
const charts =
this.panel.type === PanelType.Slideshow
? (this.panel.items as Array<ChartPanel>)
: this.panel.src
? [this.panel]
: [];

// load charts from existing storylines product
if (this.panel.charts !== undefined && this.panel.charts.length) {
this.chartConfigs = this.panel.charts.map((chart: ChartConfig) => {
if (charts !== undefined && charts.length) {
this.chartConfigs = charts.map((chart: ChartPanel) => {
let chartName = '';
// extract chart name
if (chart.options && chart.options.title) {
Expand Down Expand Up @@ -150,11 +169,19 @@ export default class ChartEditorV extends Vue {

createNewChart(chartInfo: string): void {
const chart = JSON.parse(chartInfo);
// prevent duplicate chart names (alternative is to assign a unique ID for each chart)
if (this.chartConfigs.some((chartConfig) => chartConfig.name === chart.title.text)) {
alert('Existing chart already has the same chart name.');
const chartSrc = `${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[chartSrc] > 0) {
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 chartSrc = `${this.configFileStructure.uuid}/charts/${this.lang}/${chart.title.text}.json`;
const chartConfig = {
name: chart.title.text,
src: chartSrc
Expand Down Expand Up @@ -218,8 +245,42 @@ export default class ChartEditorV extends Vue {

saveChanges(): void {
if (this.edited) {
this.panel.charts = this.chartConfigs; // option to delete config property as is redundant
// 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];
});

// Handle case where every image is deleted.
if (this.chartConfigs.length === 0) {
this.panel.type = PanelType.Chart;
(this.panel as ChartPanel).src = '';
} else if (this.chartConfigs.length === 1) {
this.panel.type = PanelType.Chart;

// Grab the one chart config from the array.
const newChart = this.chartConfigs[0];

// Sort of gross, but required to update the panel config as we're not allowed to directly manipulate props.
Object.keys(newChart).forEach((key) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
(this.panel as ChartPanel)[key] = newChart[key];
});
} else {
this.panel.type = PanelType.Slideshow;

// Turn each of the chart configs into a chart panel and add them to the slideshow.
(this.panel as SlideshowPanel).items = this.chartConfigs.map((chart: ChartConfig) => {
return {
...chart,
type: PanelType.Chart
} as ChartPanel;
});
}
}

this.edited = false;
}

Expand Down
103 changes: 58 additions & 45 deletions src/components/editor/dynamic-editor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,10 @@
</tr>
<tr class="table-contents" v-for="(item, idx) in panel.children" :key="idx">
<td>{{ item.id }}</td>
<td>{{ item.panel.type }}</td>
<td>{{ determineEditorType(item.panel) }}</td>
<td>
<span @click="() => switchSlide(idx)">{{ $t('editor.chart.label.edit') }}</span> |
<span @click="() => removeSlide(idx)">{{ $t('editor.remove') }}</span>
<span @click="() => removeSlide(item, idx)">{{ $t('editor.remove') }}</span>
</td>
</tr>
<tr class="table-add-row">
Expand All @@ -49,10 +49,7 @@
</th>
<th>
<select v-model="newSlideType">
<option
v-for="thing in Object.keys(editors).filter((editor) => editor !== 'image')"
:key="thing"
>
<option v-for="thing in Object.keys(editors)" :key="thing">
{{ thing }}
</option>
</select>
Expand All @@ -69,14 +66,8 @@
><br />
<component
ref="slide"
:is="
editors[
panel.children[editingSlide].panel.type === 'image'
? 'slideshow'
: panel.children[editingSlide].panel.type
]
"
:key="editingSlide + panel.children[editingSlide].panel.type"
:is="editors[determineEditorType(panel.children[editingSlide].panel)]"
:key="editingSlide + determineEditorType(panel.children[editingSlide].panel)"
:panel="panel.children[editingSlide].panel"
:configFileStructure="configFileStructure"
:lang="lang"
Expand All @@ -91,7 +82,7 @@
<script lang="ts">
import { Options, Prop, Vue } from 'vue-property-decorator';
import {
ChartConfig,
BasePanel,
ChartPanel,
ConfigFileStructure,
DefaultConfigs,
Expand All @@ -102,6 +93,7 @@ import {
PanelType,
SlideshowPanel,
SourceCounts,
TextPanel,
VideoPanel
} from '@/definitions';

Expand All @@ -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
Expand All @@ -130,7 +124,7 @@ export default class DynamicEditorV extends Vue {
editors: Record<string, string> = {
text: 'text-editor',
image: 'image-editor',
slideshow: 'image-editor',
slideshow: 'slideshow-editor',
chart: 'chart-editor',
map: 'map-editor',
video: 'video-editor'
Expand All @@ -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,
Expand Down Expand Up @@ -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': {
Expand All @@ -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;
}
Expand All @@ -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;
}
}
}

Expand All @@ -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 &&
Expand Down
Loading
Loading