Этот коммит содержится в:
Maarten 2024-09-27 09:37:41 +02:00
родитель a011a1b2a6
Коммит c9a418f0b7
7 изменённых файлов: 428 добавлений и 3 удалений

47
src/lib/components/Checkbox.svelte Обычный файл
Просмотреть файл

@ -0,0 +1,47 @@
<script>
// custom checkbox
export let id;
export let checked = false;
</script>
<input type="checkbox"
id="checkbox-{id}"
class:checked
on:click|stopPropagation />
<label for="checkbox-{id}" class="choice-entry-name">
<slot></slot>
</label>
<style>
label {
display: inline-block;
width: 100%;
padding-left: 1.5rem;
cursor: pointer;
pointer-events: all;
}
input[type="checkbox"] {
display: none;
pointer-events: all;
}
input[type="checkbox"] + label::before {
display: block;
width: 15px;
height: 15px;
margin-top: 3px;
margin-right: 0.4rem;
border: 2px solid var(--usa-blue);
border-radius: 3px;
background-color: var(--bg);
content: "";
position: absolute;
left: 0.5rem;
}
input[type="checkbox"].checked + label::before {
box-shadow: inset 0px 0px 0px 3px var(--bg);
background-color: var(--usa-blue);
}
</style>

100
src/lib/components/Controls.svelte Обычный файл
Просмотреть файл

@ -0,0 +1,100 @@
<script>
import Dropdown from '$lib/components/Dropdown.svelte'
let items = [
{
"id": "Facebook",
"name": "Facebook",
"selected": true,
"count": 34,
"liveCount": 34
},
{
"id": "Instagram",
"name": "Instagram",
"selected": true,
"count": 21,
"liveCount": 21
},
{
"id": "Twitter",
"name": "Twitter",
"selected": true,
"count": 34,
"liveCount": 34
},
{
"id": "YouTube",
"name": "YouTube",
"selected": true,
"count": 16,
"liveCount": 16
},
{
"id": "unspecified",
"name": "unspecified",
"selected": true,
"count": 33,
"liveCount": 33
},
{
"id": "Reddit",
"name": "Reddit",
"selected": true,
"count": 13,
"liveCount": 13
},
{
"id": "WhatsApp",
"name": "WhatsApp",
"selected": true,
"count": 2,
"liveCount": 2
},
{
"id": "SMS",
"name": "SMS",
"selected": true,
"count": 2,
"liveCount": 2
},
{
"id": "Telegram",
"name": "Telegram",
"selected": true,
"count": 1,
"liveCount": 1
},
{
"id": "Quora",
"name": "Quora",
"selected": true,
"count": 1,
"liveCount": 1
},
{
"id": "Forum Board",
"name": "Forum Board",
"selected": true,
"count": 1,
"liveCount": 1
},
{
"id": "Parler",
"name": "Parler",
"selected": true,
"count": 1,
"liveCount": 1
},
{
"id": "Gab",
"name": "Gab",
"selected": true,
"count": 1,
"liveCount": 1
}
]
</script>
<Dropdown items={items} label='Platform'></Dropdown>

241
src/lib/components/Dropdown.svelte Обычный файл
Просмотреть файл

@ -0,0 +1,241 @@
<script>
// a custom dropdown
import { createEventDispatcher } from 'svelte';
import { slide } from 'svelte/transition';
import { sortConsistently } from '$lib/utils/misc';
import Checkbox from '$lib/components/Checkbox.svelte';
export let items = [];
export let label = '';
export let nameField = 'id';
export let hideOneHitWonders = false;
export let superior = false;
const dispatch = createEventDispatcher();
let elem;
let expanded = false;
function handleBodyClick() {
expanded = false
}
function toggleExpanded() {
expanded = !expanded;
}
function selectAll() {
dispatch('itemsAdded', items.map((d) => d.id));
}
function unselectAll() {
dispatch('itemsRemoved', items.map((d) => d.id));
}
function handleDropdownClick() {
toggleExpanded();
}
function handleChoiceClick(id) {
if (!items.filter((d) => d.selected).map((d) => d.id).includes(id)) {
dispatch('itemsAdded', id);
} else {
dispatch('itemsRemoved', id);
}
}
</script>
<svelte:body on:click={(e) => handleBodyClick(e)}></svelte:body>
<div class="dropdown" bind:this={elem}>
<div class="label">
{label}
</div>
<div class="selected-items" on:click|stopPropagation={handleDropdownClick}>
<span class="selected-items-icon"></span>
<span class="selected-items-text">
{items.filter((d) => d.selected).length === 0
? 'none'
: (items.every((d) => d.selected && items.length > 1)
? 'all'
: items.filter((d) => d.selected).map((d) => d[nameField]).join(', '))}
</span>
<button class="selected-items-arrow">
<svg class:expanded width="15" height="10">
<path d="M0 0L15 0L7.5 10Z"></path>
</svg>
</button>
</div>
<div class="choice-wrapper">
{#if (expanded)}
<div class="choice" transition:slide class:superior>
<div class="choice-controls">
<button class="choice-controls-selectall" on:click|stopPropagation={selectAll}>Select all</button>
<button class="choice-controls-unselectall" on:click|stopPropagation={unselectAll}>Unselect all</button>
</div>
<ul class="choice-list">
{#each items.sort((a, b) => -sortConsistently(a, b, 'id', 'id')) as item, i (item.id)}
{#if (!(hideOneHitWonders && item.count === 1))}
<li on:click|stopPropagation>
<Checkbox id="{label}-{i}"
checked={item.selected}
on:click={() => handleChoiceClick(item.id)}>
<span class="choice-entry-name">{item[nameField]}</span>
{#if (item.liveCount)}
<span class="choice-entry-count">({item.liveCount})</span>
{:else if (item.source)}
<span class="choice-entry-source">({item.source})</span>
{/if}
</Checkbox>
</li>
{/if}
{/each}
</ul>
{#if (hideOneHitWonders)}
<p class="info">{label}s with only one result in the dataset are hidden.</p>
{/if}
</div>
{/if}
</div>
</div>
<style>
.dropdown {
align-self: flex-end;
display: flex;
flex-direction: column;
font-family: var(--font-02);
width: 200px;
max-width: 200px;
min-width: 200px;
margin: 0.3rem 0.3rem 0 0.3rem;
position: relative;
pointer-events: all;
}
.label {
margin: 0 0 0.1rem 0;
font-size: 0.7rem;
color: var(--usa-blue);
}
.selected-items {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
height: 1.7rem;
padding: 0.1rem 0.3rem;
font-size: 0.8rem;
background-color: var(--bg);
border: 2px solid var(--usa-blue);
border-radius: 3px;
cursor: pointer;
}
.selected-items-text {
width: 100%;
color: var(--text-black);
overflow: hidden;
white-space: nowrap;
}
.selected-items-arrow {
background: none;
border: none;
outline: none;
cursor: pointer;
}
.selected-items-arrow svg {
margin-top: 0.2rem;
stroke: none;
fill: var(--usa-blue);
transition: transform 400ms ease;
}
.selected-items-arrow svg.expanded{
transform: rotate(-540deg);
}
.choice-wrapper {
width: 100%;
height: 0;
position: relative;
}
.choice {
width: 100%;
border: 2px solid var(--usa-lightblue);
border-radius: 3px;
background-color: var(--bg);
position: absolute;
z-index: 12000;
top: 0;
}
.superior {
z-index: 100000 !important;
}
.choice-controls {
display: flex;
align-items: center;
width: 100%;
}
.choice-controls button {
margin: 0.5rem;
padding: 0.2rem 0.5rem;
color: var(--bg);
border: none;
border-radius: 5px;
outline: none;
cursor: pointer;
}
button.choice-controls-selectall {
background-color: var(--usa-lightblue);
font-size: 0.8rem;
}
button.choice-controls-unselectall {
background-color: var(--usa-lightred);
font-size: 0.8rem;
}
ul.choice-list {
width: 100%;
max-height: 600px;
overflow-y: scroll;
list-style-type: none;
font-size: 0.8rem;
position: relative;
}
ul.choice-list li {
padding: 0.4rem 0.5rem;
cursor: pointer;
background-color: var(--bg);
transition: background-color 200ms ease;
}
ul.choice-list li:hover {
background-color: var(--usa-lightblue);
}
.choice-entry-count, .choice-entry-source {
font-size: 0.8em;
}
.choice-entry-source {
display: block;
}
p.info {
padding: 0.4rem 0.5rem;
font-size: 0.7rem;
color: var(--dfrlab-gray);
}
</style>

21
src/lib/utils/misc.js Обычный файл
Просмотреть файл

@ -0,0 +1,21 @@
// consistent sort function
export const sortConsistently = (itemA, itemB, property, key) => {
let valueA = itemA[property];
let valueB = itemB[property];
if (typeof valueA === 'string') valueA = valueA.trim().toLowerCase();
if (typeof valueB === 'string') valueB = valueB.trim().toLowerCase();
if (typeof valueA === 'number') valueA = +valueA;
if (typeof valueB === 'number') valueB = +valueB;
if (typeof valueA === 'number' && isNaN(valueA)) valueA = 0;
if (typeof valueB === 'number' && isNaN(valueB)) valueB = 0;
let r = valueA > valueB ? -1 : valueA < valueB ? 1 : 0;
if (r === 0) {
r = typeof itemA[key] !== 'undefined' && typeof itemB[key] !== 'undefined'
? +itemA[key] - +itemB[key]
: 0;
}
return r;
};

5
src/routes/+layout.svelte Обычный файл
Просмотреть файл

@ -0,0 +1,5 @@
<script>
import '../style.css'
</script>
<slot />

Просмотреть файл

@ -6,7 +6,8 @@
import CaseCard from '$lib/components/CaseCard.svelte';
import CaseTable from '$lib/components/CaseTable.svelte';
import Timeline from '$lib/components/Timeline.svelte';
import Select from 'svelte-select';
//import Select from 'svelte-select';
import Controls from '$lib/components/Controls.svelte';
let cases = [];
@ -38,14 +39,15 @@
</section>
<section class="section">
<div class="select-container">
<Controls></Controls>
<!--div class="select-container">
<Select
items={actorNationsUnique}
multiple={true}
bind:value={selectedActorNations}
placeholder={'Select 1 or more actor countries'}
></Select>
</div>
</div-->
</section>
<section class="section">

9
src/style.css Обычный файл
Просмотреть файл

@ -0,0 +1,9 @@
:root {
--bg: #F9F8F8;
--usa-blue: #3c3b6e;
--usa-lightblue: #c9c7eb;
--usa-red: #b22234;
--usa-lightred: #c5888f;
}