Skip to content

Commit

Permalink
implement dynamic slideshow editor
Browse files Browse the repository at this point in the history
  • Loading branch information
Ryan Coulson committed Mar 21, 2024
1 parent c43cd4b commit 1433eed
Show file tree
Hide file tree
Showing 11 changed files with 609 additions and 130 deletions.
18 changes: 14 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"jszip": "^3.10.1",
"markdown-it": "^12.0.6",
"nouislider": "^15.5.0",
"ramp-storylines": "^3.0.4",
"ramp-storylines": "^3.0.6",
"uuid": "^9.0.0",
"vue": "^3.3.4",
"vue-class-component": "^8.0.0-rc.1",
Expand Down
63 changes: 57 additions & 6 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()"
v-if="allowMany || (!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 @@ -62,11 +63,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 +86,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 +115,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 @@ -218,8 +235,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
105 changes: 59 additions & 46 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 @@ -90,7 +81,7 @@
<script lang="ts">
import { Options, Prop, Vue } from 'vue-property-decorator';
import {
ChartConfig,
BasePanel,
ChartPanel,
ConfigFileStructure,
DefaultConfigs,
Expand All @@ -100,19 +91,22 @@ import {
MapPanel,
PanelType,
SlideshowPanel,
SourceCounts
SourceCounts,
TextPanel
} from '@/definitions';
import ChartEditorV from './chart-editor.vue';
import ImageEditorV from './image-editor.vue';
import TextEditorV from './text-editor.vue';
import MapEditorV from './map-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
}
Expand All @@ -126,7 +120,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'
};
Expand All @@ -146,11 +140,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 @@ -181,19 +180,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 @@ -209,33 +198,40 @@ 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;
}
}
// 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 @@ -251,6 +247,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

0 comments on commit 1433eed

Please sign in to comment.