aboutsummaryrefslogtreecommitdiff
path: root/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'src/components')
-rw-r--r--src/components/InfoModal.vue152
-rw-r--r--src/components/SCollapsible.vue61
-rw-r--r--src/components/TimeLine.vue16
-rw-r--r--src/components/icon/DownIcon.vue6
-rw-r--r--src/components/icon/ExternalLinkIcon.vue8
5 files changed, 238 insertions, 5 deletions
diff --git a/src/components/InfoModal.vue b/src/components/InfoModal.vue
new file mode 100644
index 0000000..6ab2bde
--- /dev/null
+++ b/src/components/InfoModal.vue
@@ -0,0 +1,152 @@
+<template>
+<div class="fixed left-0 top-0 w-full h-full bg-black/40" @click="onClose" ref="rootRef">
+ <div class="absolute left-1/2 -translate-x-1/2 top-1/2 -translate-y-1/2
+ max-w-[80%] w-2/3 min-w-[8cm] md:w-[14cm] lg:w-[16cm] max-h-[80%]" :style="styles">
+ <close-icon @click.stop="onClose" ref="closeRef"
+ class="absolute top-1 right-1 md:top-2 md:right-2 w-8 h-8 hover:cursor-pointer"/>
+ <h1 class="text-center text-xl font-bold pt-2 pb-1 md:text-2xl md:pt-3 md:pb-1">
+ {{event.title}}
+ </h1>
+ <p class="text-center text-sm md:text-base">{{datesDisplayStr}}</p>
+ <div class="border-t border-stone-400 p-2 md:p-3">
+ <div class="mt-1 mr-2 md:mb-2 md:mr-4 md:float-left">
+ <!-- Image -->
+ <a :href="eventInfo.imgInfo.url" target="_blank" class="block w-fit mx-auto" :style="imgStyles"></a>
+ <!-- Image Source -->
+ <s-collapsible class="text-sm text-center w-fit max-w-full md:max-w-[200px] mx-auto">
+ <template v-slot:summary="slotProps">
+ <div class="py-1 hover:underline">
+ <down-icon class="inline-block w-4 h-4 mr-1 transition-transform duration-300"
+ :class="{'-rotate-90': slotProps.open}"/>
+ Image Source
+ </div>
+ </template>
+ <template v-slot:content>
+ <ul class="rounded overflow-x-auto p-1"
+ :style="{backgroundColor: store.color.bg, color: store.color.text}">
+ <li>
+ <span :style="{color: store.color.altDark}">Source: </span>
+ <a :href="eventInfo.imgInfo.url" target="_blank">Link</a>
+ <external-link-icon class="inline-block w-3 h-3 ml-1"/>
+ </li>
+ <li class="whitespace-nowrap">
+ <span :style="{color: store.color.altDark}">Artist: </span>
+ {{eventInfo.imgInfo.artist}}
+ </li>
+ <li v-if="eventInfo.imgInfo.credit != ''" class="whitespace-nowrap">
+ <span :style="{color: store.color.altDark}">Credits: </span>
+ {{eventInfo.imgInfo.credit}}
+ </li>
+ <li>
+ <span :style="{color: store.color.altDark}">License: </span>
+ <a :href="licenseToUrl(eventInfo.imgInfo.license)" target="_blank">
+ {{eventInfo.imgInfo.license}}
+ </a>
+ <external-link-icon class="inline-block w-3 h-3 ml-1"/>
+ </li>
+ <li>
+ <span :style="{color: store.color.altDark}">Obtained via: </span>
+ <a href="https://www.wikipedia.org/">Wikipedia</a>
+ <external-link-icon class="inline-block w-3 h-3 ml-1"/>
+ </li>
+ <li>
+ <span :style="{color: store.color.altDark}">Changes: </span>
+ Cropped and resized
+ </li>
+ </ul>
+ </template>
+ </s-collapsible>
+ </div>
+ <div>{{eventInfo.desc}}</div>
+ <div class="text-sm text-right">
+ <a :href="'https://en.wikipedia.org/?curid=' + eventInfo.wikiId" target="_blank">From Wikipedia</a>
+ (via <a :href="'https://www.wikidata.org/wiki/Q' + event.id" target="_blank">Wikidata</a>)
+ <external-link-icon class="inline-block w-3 h-3 ml-1"/>
+ </div>
+ </div>
+ </div>
+</div>
+</template>
+
+<script setup lang="ts">
+import {ref, computed, PropType} from 'vue';
+import SCollapsible from './SCollapsible.vue';
+import CloseIcon from './icon/CloseIcon.vue';
+import DownIcon from './icon/DownIcon.vue';
+import ExternalLinkIcon from './icon/ExternalLinkIcon.vue';
+import {HistEvent, EventInfo} from '../lib';
+import {useStore} from '../store';
+
+// Refs
+const rootRef = ref(null as HTMLDivElement | null);
+const closeRef = ref(null as typeof CloseIcon | null);
+
+// Global store
+const store = useStore();
+
+// Props + events
+const props = defineProps({
+ event: {type: Object as PropType<HistEvent>, required: true},
+ eventInfo: {type: Object as PropType<EventInfo>, required: true},
+});
+const emit = defineEmits(['close']);
+
+// For data display
+const datesDisplayStr = computed(() => {
+ return props.event.start.toString() + (props.event.end == null ? '' : ' to ' + props.event.end.toString())
+});
+function licenseToUrl(license: string){
+ license = license.toLowerCase().replaceAll('-', ' ');
+ if (license == 'cc0'){
+ return 'https://creativecommons.org/publicdomain/zero/1.0/';
+ } else if (license == 'cc publicdomain'){
+ return 'https://creativecommons.org/licenses/publicdomain/';
+ } else {
+ const regex = /cc by( nc)?( sa)?( ([0-9.]+)( [a-z]+)?)?/;
+ let results = regex.exec(license);
+ if (results != null){
+ let url = 'https://creativecommons.org/licenses/by';
+ if (results[1] != null){
+ url += '-nc';
+ }
+ if (results[2] != null){
+ url += '-sa';
+ }
+ if (results[4] != null){
+ url += '/' + results[4];
+ } else {
+ url += '/4.0';
+ }
+ if (results[5] != null){
+ url += '/' + results[5].substring(1);
+ }
+ return url;
+ }
+ return "[INVALID LICENSE]";
+ }
+}
+
+// Close handling
+function onClose(evt: Event){
+ if (evt.target == rootRef.value || closeRef.value!.$el.contains(evt.target)){
+ emit('close');
+ }
+}
+
+// Styles
+const styles = computed(() => ({
+ backgroundColor: store.color.bgAlt,
+ borderRadius: store.borderRadius + 'px',
+ overflow: 'visible auto',
+}));
+const imgStyles = computed(() => {
+ return {
+ width: '200px',
+ height: '200px',
+ //backgroundImage:
+ backgroundColor: store.color.bgDark,
+ //backgroundSize: 'cover',
+ borderRadius: store.borderRadius + 'px',
+ };
+});
+</script>
diff --git a/src/components/SCollapsible.vue b/src/components/SCollapsible.vue
new file mode 100644
index 0000000..39b4283
--- /dev/null
+++ b/src/components/SCollapsible.vue
@@ -0,0 +1,61 @@
+<template>
+<div :style="styles">
+ <div class="hover:cursor-pointer" @click="onClick">
+ <slot name="summary" :open="open">(Summary)</slot>
+ </div>
+ <transition @enter="onEnter" @after-enter="onAfterEnter" @leave="onLeave" @before-leave="onBeforeLeave">
+ <div v-show="open" :style="contentStyles" class="max-h-0" ref="content">
+ <slot name="content">(Content)</slot>
+ </div>
+ </transition>
+</div>
+</template>
+
+<script setup lang="ts">
+import {ref, computed, watch} from 'vue';
+
+// Props + events
+const props = defineProps({
+ modelValue: {type: Boolean, default: false}, // For using v-model on the component
+});
+const emit = defineEmits(['update:modelValue', 'open']);
+
+// For open status
+const open = ref(false);
+watch(() => props.modelValue, (newVal) => {open.value = newVal})
+function onClick(){
+ open.value = !open.value;
+ emit('update:modelValue', open.value);
+ if (open.value){
+ emit('open');
+ }
+}
+
+// Styles
+const styles = computed(() => ({
+ overflow: open.value ? 'visible' : 'hidden',
+}));
+const contentStyles = computed(() => ({
+ overflow: 'hidden',
+ opacity: open.value ? '1' : '0',
+ transitionProperty: 'max-height, opacity',
+ transitionDuration: '300ms',
+ transitionTimingFunction: 'ease-in-out',
+}));
+
+// Open/close transitions
+function onEnter(el: HTMLDivElement){
+ el.style.maxHeight = el.scrollHeight + 'px';
+}
+function onAfterEnter(el: HTMLDivElement){
+ el.style.maxHeight = 'none';
+ // Allows the content to grow after the transition ends, as the scrollHeight sometimes is too short
+}
+function onBeforeLeave(el: HTMLDivElement){
+ el.style.maxHeight = el.scrollHeight + 'px';
+ el.offsetWidth; // Triggers reflow
+}
+function onLeave(el: HTMLDivElement){
+ el.style.maxHeight = '0';
+}
+</script>
diff --git a/src/components/TimeLine.vue b/src/components/TimeLine.vue
index fea8fb3..277a263 100644
--- a/src/components/TimeLine.vue
+++ b/src/components/TimeLine.vue
@@ -1,5 +1,5 @@
<template>
-<div class="touch-none relative overflow-hidden" ref="rootRef"
+<div class="touch-none relative overflow-hidden z-0" ref="rootRef"
@wheel.exact.prevent="onWheel" @wheel.shift.exact.prevent="onShiftWheel">
<template v-if="store.showEventCounts">
<div v-for="[tickIdx, count] in tickToCount.entries()" :key="ticks[tickIdx].date.toInt()"
@@ -47,7 +47,8 @@
<!-- Events -->
<div v-for="id in idToPos.keys()" :key="id" class="absolute animate-fadein z-20" :style="eventStyles(id)">
<!-- Image -->
- <div class="rounded-full" :style="eventImgStyles(id)"></div>
+ <div class="rounded-full cursor-pointer hover:brightness-125" :style="eventImgStyles(id)"
+ @click="onEventClick(id)"></div>
<!-- Label -->
<div class="text-center text-stone-100 text-sm whitespace-nowrap text-ellipsis overflow-hidden">
{{idToEvent.get(id)!.title}}
@@ -60,7 +61,7 @@
<!-- Buttons -->
<icon-button v-if="closeable" :size="30" class="absolute top-2 right-2 z-20"
:style="{color: store.color.text, backgroundColor: store.color.altDark2}"
- @click="emit('remove')" title="Remove timeline">
+ @click="emit('close')" title="Close timeline">
<close-icon/>
</icon-button>
</div>
@@ -76,7 +77,7 @@ import CloseIcon from './icon/CloseIcon.vue';
import {WRITING_MODE_HORZ, MIN_DATE, MAX_DATE, MONTH_SCALE, DAY_SCALE, SCALES, MONTH_NAMES, MIN_CAL_DATE,
getDaysInMonth, HistDate, stepDate, getScaleRatio, getNumSubUnits, getUnitDiff,
getEventPrecision, dateToUnit,
- moduloPositive, TimelineState, HistEvent, getImagePath} from '../lib';
+ moduloPositive, TimelineState, HistEvent} from '../lib';
import {useStore} from '../store';
import {RBTree} from '../rbtree';
@@ -95,7 +96,7 @@ const props = defineProps({
eventTree: {type: Object as PropType<RBTree<HistEvent>>, required: true},
unitCountMaps: {type: Object as PropType<Map<number, number>[]>, required: true},
});
-const emit = defineEmits(['remove', 'state-chg', 'event-req', 'event-display']);
+const emit = defineEmits(['close', 'state-chg', 'event-display', 'event-click']);
// For size tracking
const width = ref(0);
@@ -1108,6 +1109,11 @@ watch(firstDate, onStateChg);
const skipTransition = ref(true);
onMounted(() => setTimeout(() => {skipTransition.value = false}, 100));
+// Click handling
+function onEventClick(eventId: number){
+ emit('event-click', eventId);
+}
+
// Styles
const mainlineStyles = computed(() => {
return {
diff --git a/src/components/icon/DownIcon.vue b/src/components/icon/DownIcon.vue
new file mode 100644
index 0000000..f7a5835
--- /dev/null
+++ b/src/components/icon/DownIcon.vue
@@ -0,0 +1,6 @@
+<template>
+<svg viewBox="0 0 24 24" fill="none"
+ stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+ <polyline points="6 9 12 15 18 9"></polyline>
+</svg>
+</template>
diff --git a/src/components/icon/ExternalLinkIcon.vue b/src/components/icon/ExternalLinkIcon.vue
new file mode 100644
index 0000000..f672f3a
--- /dev/null
+++ b/src/components/icon/ExternalLinkIcon.vue
@@ -0,0 +1,8 @@
+<template>
+<svg viewBox="0 0 24 24" fill="none"
+ stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+ <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
+ <polyline points="15 3 21 3 21 9"></polyline>
+ <line x1="10" y1="14" x2="21" y2="3"></line>
+</svg>
+</template>