Commit cfb9732c authored by Bastien Le Querrec's avatar Bastien Le Querrec
Browse files

initial js

parent 07405b3a
.vscode/
scripts/data/
\ No newline at end of file
scripts/data/
json/
an:
mkdir -p json/
yq -c -s . data/an/*.yml > json/an.json
yq -c . data/an.yml > json/an-config.json
# ParlementairesJS - Display information about MPs and engage your audience to contact them
ParlementairesJS displays a simple frame to show phone numbers, email addresses, Twitter accounts and Facebook links about MPs. Filters can be configured to match campaign's requirements.
## Usage
1. In the `<head>` section, you must include the script and its CSS:
```html
<script src="https://parlementairesjs.laquadrature.net/parlementaires.js"></script>
<link rel='stylesheet' href='https://parlementairesjs.laquadrature.net/parlementaires.css' type='text/css' media='all' />
```
You can download a copy of the `parlementaires.js` and `parlementaires.css` and load your local copy.
2. You need to add a `<div>` element in your web page, where the frame will be displayed. This `<div>` must have an ID.
3. Add the following code at the end of your web page, before the `</body>`:
```html
<script>
ParlementairesJS({
dataset: "/json/an.json",
datasetConfig: "/json/an-config.json"
}).display('id-of-your-div');
</script>
```
You must set at least the `dataset` and `datasetConfig` values. Change the `id-of-your-div` with the ID of your `<div>` you added in stage 2.
## Options
| Option | Mandatory? | Description | Default |
| ------------------ | ---------- | ----------------------------------------------------------------------------------------- | --------------------- |
| `dataset` | true | URL of the MPs' dataset. | N/A |
| `datasetConfig` | true | URL of the dataset's configuration. | N/A |
| `commissionFilter` | false | Display MPs that are in the specified commission. | No filter is applied. |
| `writeHTML` | false | Write the HTML in the `<div>`. Disable this if you want to change the current stylesheet. | `true` |
| `photoInImg` | false | Put the image in a `<img>` instead of in a `<div>`. | `false` |
| `phoneFilter` | false | Display phone numbers that start with this value. | No filter is aplied. |
| `mailFilter` | false | Display emails with the specified domain name. | No filter is aplied. |
| `twitter` | false | Display Twitter link. | `true` |
| `facebook` | false | Display Facebook link. | `true` |
## Generated HTML
The following HTML is generated:
```html
<div id="id-of-your-div">
<div id="parlementairesjs_introphone_wrapper">
<p class="parlementairesjs_introphone">
Au hasard parmi les <span id="parlementairesjs_mp_total">576</span> député·es de
<select id="parlementairesjs_select_group">
<option value="0">tous les groupes</option>
</select>
<br> et de
<select id="parlementairesjs_select_county">
<option value="0">toutes les circonscription</option>
</select>
</p>
</div>
<div id="parlementairesjs_mp_photo_info_wrapper">
<div id="parlementairesjs_mp_photo_wrapper">
<div id="parlementairesjs_mp_photo" style="width: 150px; height: 192px; background-image: url('http://localhost:8000//images/an/334116.jpg');"></div>
</div>
<div id="parlementairesjs_mp_info" style="display: block;">
<p id="parlementairesjs_mp_name_wrapper">
<span id="parlementairesjs_mp_first_name">John</span> <span id="parlementairesjs_mp_last_name">Doe</span>
</p>
<p id="parlementairesjs_mp_group_county_wrapper">
<span id="parlementairesjs_mp_group">Some Group</span><span id="parlementairesjs_mp_county">Some county</span>
</p>
<div id="parlementairesjs_mp_phone">
<p>
<a class="parlementairesjs_mp_phone_child" href="tel:+33123456789">01 23 45 67 89</a>
<a class="parlementairesjs_mp_phone_child" href="tel:+33600000000">+33600000000</a>
</p>
</div>
<div id="parlementairesjs_mp_mail">
<p>
<a class="parlementairesjs_mp_mail_child" href="mailto:john.doe@example.org">john.doe@example.org</a>
<a class="parlementairesjs_mp_mail_child" href="mailto:john.doe2@example.org">john.doe2@example.org</a>
</p>
</div>
<p id="parlementairesjs_mp_twi">
<a class="parlementairesjs_mp_twi_child" href="https://twitter.com/SommeTwitterAccount" target="_blank">@SommeTwitterAccount</a>
</p>
<p id="parlementairesjs_mp_fb">
<a class="parlementairesjs_mp_fb_child" href="https://www.facebook.com/SomeFacebookLink" target="_blank">SomeFacebookLink</a>
</p>
</div>
</div>
<div id="parlementairesjs_mp_next_wrapper">
<p id="parlementairesjs_mp_next">Député·e suivant·e &gt;</p>
</div>
</div>
```
You can override CSS style using those HTML classes and IDs.
If you set `writeHTML` to `false`, your HTML must have the following elements:
- `parlementairesjs_mp_total`
- `parlementairesjs_select_group`
- `parlementairesjs_mp_photo` (if `photoToImg` is `false`)
- `parlementairesjs_mp_photo_img` (if `photoToImg` is `true`)
- `parlementairesjs_mp_first_name`
- `parlementairesjs_mp_last_name`
- `parlementairesjs_mp_group`
- `parlementairesjs_mp_county`
- `parlementairesjs_mp_phone`
- `parlementairesjs_mp_mail`
- `parlementairesjs_mp_twi`
- `parlementairesjs_mp_fb`
- `parlementairesjs_mp_info`
- `parlementairesjs_mp_next`
## Datasets and their configuration
Datasets must be shipped with their configuration. Datasets contain MPs, configurations contain information about formatting.
Available datasets:
| Name | ID | Description | Data's source |
| ---------------------------- | ---- | ------------------------- | -------------------------------------------------------------------------------------------- |
| Assemblée nationale (France) | `an` | French national assembly. | Chamber's open data + additional listing for phone numbers (see `scripts/an.sh` for details) |
Datasets are available at `https://parlementairesjs.laquadrature.net/json/<dataset>.json` and their configuration at `https://parlementairesjs.laquadrature.net/json/<dataset>-config.json`.
You can generate a dataset with `make <dataset-id>`.
## Create your dataset
Datasets are generated from YML files located at `data/<id>/` (one file per MP with `<last name>_<first name>.yml` filename) and their configuration from `data/<id>-config.yml`. If you want to use a script to get a first bunch of data, put it in `scripts/<id>.sh`.
YML MPs files must follow that format:
```yml
id: XXXXXX
last_name: John
first_name: Doe
group: Some group
county: Some county
commissions:
- "A first commission"
- "Another commission"
phone:
- "0123456789"
- "+33600000000"
email:
- "john.doe@example.org"
- "john.doe2@example.org"
twitter: SommeTwitterAccount
facebook: SomeFacebookLink
photo: XXXXXX
```
Mandatory fields are `last_name`, `first_name`. You should add a phone number or an email address.
Configuration files must follow that format:
```yml
designation:
singular: "singular form"
plural: "plural form"
phone:
local_prefix: "33"
separator: " "
group_size: 2
img:
baseURL: '/images/<id>/'
width: "150px"
height: "192px"
```
All fields are mandatory. `baseURL` can be a relative URL or with a FQDN. For privacy issues, you should get a copy of MPs' photos in `images/<id>/` and use a relative `baseURL`.
When your dataset is ready, add this entry in the `Makefile`:
```
<id>:
mkdir -p json/
yq -c -s . data/<id>/*.yml > json/<id>.json
yq -c . data/<id>.yml > json/<id>-config.json
```
## License
Source code is MIT licensed. Data may have a different license.
designation:
singular: "député·e"
plural: "député·es"
phone:
local_prefix: "33"
separator: " "
group_size: 2
img:
baseURL: '/images/an/'
width: "150px"
height: "192px"
<!DOCTYPE html>
<html>
<head>
<title>ParlementairesJS</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<script src="parlementaires.js"></script>
<link rel='stylesheet' href='parlementaires.css' type='text/css' media='all' />
<style>
html {
font-family: sans-serif;
}
body {
padding: 0;
margin: 0;
}
html, body, #map {
height: 100%;
width: 100%;
}
#parlementaires {
color: black;
padding: 1em 3em 1em 3em;
overflow: hidden;
margin-bottom: 2em;
border: 2px solid black;
}
</style>
</head>
<body>
<div id="parlementaires"></div>
<script type="text/javascript">
ParlementairesJS({
dataset: "/json/an.json",
datasetConfig: "/json/an-config.json",
//commissionFilter: "Lois" // Show only MPs in this commission (optional; default: no filte)
//writeHTML: false, // Do not write HTML (optional; default: true)
//photoInImg: true, // Use a <img> instead of a <div>+background (optional; default: false)
//phoneFilter: "014063", // Show only phone numbers that start with this (optional; default: no filter)
//mailFilter: "assemblee-nationale.fr", // Show only email addresses with this domain name (optional; default: no filter)
//twitter: false, // Hide Twitter links (optional; default: true)
//facebook: false, // Hide Facebook links (optional; default: true)
}).display('parlementaires');
</script>
</body>
</html>
\ No newline at end of file
<svg width="1536" height="1536" xmlns="http://www.w3.org/2000/svg">
<path d="M959 12v264H802c-123 0-146 59-146 144v189h293l-39 296H656v759H350V905H95V609h255V391C350 138 505 0 731 0c108 0 201 8 228 12z"/>
</svg>
<?xml version='1.0' encoding='utf-8'?><svg version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' viewBox='0 0 176 173.1' style='enable-background:new 0 0 176 173.1;' xml:space='preserve'><g><path d='M166.2,81.2c-0.6,15.9-4.6,28.5-11.9,38c-7.3,9.4-17,14.1-29.2,14.1c-5,0-9.4-1.1-13.2-3.2c-3.8-2.2-6.8-5.2-8.9-9.2 c-6,8-14,12-24,12c-5.8,0-11-1.7-15.3-5.2c-4.4-3.4-7.6-8.3-9.6-14.6c-2-6.3-2.7-13.5-1.9-21.5c1.8-15.2,6.7-27.4,14.7-36.7 c8-9.2,17.7-13.9,29.3-13.9c8.9,0,16.6,2.1,23.1,6.2l5.7,3.8l-4.7,53.6c-1,8.5,1.9,12.7,8.8,12.7c5.2,0,9.5-3.3,13-9.9 c3.5-6.6,5.4-15,5.8-25.3c1-20.3-3.2-36-12.6-47.1c-9.5-11.1-23.3-16.6-41.6-16.6c-11.6,0-21.9,3-31,8.9S46.5,41.6,41.3,52.5 C36,63.3,33,75.7,32.4,89.6c-1,21.3,3.3,37.6,12.8,48.9c9.5,11.3,23.7,16.9,42.7,16.9c5.1,0,10.5-0.6,16.2-1.8 c5.7-1.2,10.4-2.6,14-4.2l3.4,15.1c-3.7,2.4-8.7,4.4-15,5.9c-6.3,1.5-12.6,2.2-18.9,2.2c-16.6,0-30.6-3.2-42.1-9.5 c-11.5-6.4-20-15.8-25.6-28.2c-5.6-12.5-8.1-27.6-7.5-45.4c0.7-16.8,4.6-31.8,11.6-45.1c7-13.3,16.6-23.6,28.8-30.8 C65,6.3,78.7,2.7,94.1,2.7c15.3,0,28.6,3.2,39.7,9.5c11.1,6.4,19.5,15.5,25.1,27.4C164.4,51.6,166.9,65.4,166.2,81.2z M75,91.6 c-0.7,7.9,0,13.9,1.9,17.9c1.9,4,5,6.1,9.3,6.1c2.9,0,5.5-1.3,7.9-3.8c2.4-2.6,4.4-6.2,6-10.9l3.7-41.6c-1.8-0.5-3.7-0.7-5.7-0.7 c-6.8,0-12.1,2.7-15.7,8C78.6,71.8,76.1,80.1,75,91.6z'/></g></svg>
\ No newline at end of file
<?xml version='1.0' encoding='utf-8'?><svg version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' viewBox='0 0 176 173.1' style='enable-background:new 0 0 176 173.1;' xml:space='preserve'><g><path d='M126.5,173.1c-12.7-0.7-27-6.4-40.4-14.4c-29.3-17.6-54-40-69.8-71c-5.7-11.2-9.5-23-11.6-35.5 C2.4,38.3,6.9,26.7,17.1,17.7c5.2-4.6,11.7-8,18-11.2c3.4-1.7,6.5-0.3,8.7,3.4c6.1,10.5,12.5,20.8,19,31.1 c2.8,4.4,2.4,8.2-1.4,11.5c-4.6,4.1-9.2,8.1-14,12.1c-4.1,3.4-5.4,7.5-2.6,11.9c5.2,8.1,9.8,16.9,16.7,23.3 c12.4,11.5,26,21.7,39.4,32.1c5.9,4.6,8.7,3.8,13.5-2c3.3-4,6.3-8.2,9.7-12c4.6-5.2,7-5.6,13-2.1c10.2,5.9,20.3,12.1,30.6,17.8 c3.8,2.1,4.6,4.7,3.7,8.6C167.6,159.6,149.2,173.2,126.5,173.1z'/></g></svg>
\ No newline at end of file
<svg width="1536" height="1536" xmlns="http://www.w3.org/2000/svg">
<path d="M1620 408c-44 64-99 121-162 167 1 14 1 28 1 42 0 427-325 919-919 919-183 0-353-53-496-145 26 3 51 4 78 4 151 0 290-51 401-138-142-3-261-96-302-224 20 3 40 5 61 5 29 0 58-4 85-11-148-30-259-160-259-317v-4c43 24 93 39 146 41-87-58-144-157-144-269 0-60 16-115 44-163 159 196 398 324 666 338-5-24-8-49-8-74 0-178 144-323 323-323 93 0 177 39 236 102 73-14 143-41 205-78-24 75-75 138-142 178 65-7 128-25 186-50z"/>
</svg>
.parlementairesjs_introphone {
font-weight: bolder;
}
#parlementairesjs_introphone_wrapper,
#parlementairesjs_mp_photo_info_wrapper,
#parlementairesjs_mp_next_wrapper
{
display: inline-block;
width: 100%;
}
#parlementairesjs_mp_photo_info_wrapper {
overflow: hidden;
}
#parlementairesjs_mp_photo_wrapper,
#parlementairesjs_mp_info {
float: left;
display: block;
}
#parlementairesjs_mp_photo,
#parlementairesjs_mp_photo_img {
background: gray;
background-repeat: no-repeat;
margin-right: 2em;
margin-top: 1em;
}
#parlementairesjs_mp_name_wrapper {
width: inherit;
}
#parlementairesjs_mp_name_wrapper {
font-weight: bold;
}
#parlementairesjs_mp_phone,
#parlementairesjs_mp_mail,
#parlementairesjs_mp_twi,
#parlementairesjs_mp_fb {
background-repeat: no-repeat;
background-size: 16px 16px;
padding-left: 1.5em;
background-position: center left;
}
#parlementairesjs_mp_phone a,
#parlementairesjs_mp_mail a,
#parlementairesjs_mp_twi a,
#parlementairesjs_mp_fb a {
text-decoration: none;
color: black;
}
#parlementairesjs_mp_phone {
background-image: url("/icons/phone.svg");
}
#parlementairesjs_mp_mail {
background-image: url("/icons/mail.svg");
}
#parlementairesjs_mp_twi {
background-image: url("/icons/twitter.svg");
}
#parlementairesjs_mp_fb {
background-image: url("/icons/fb.svg");
}
.parlementairesjs_mp_phone_child,
.parlementairesjs_mp_mail_child,
.parlementairesjs_mp_twi_child,
.parlementairesjs_mp_fb_child {
display: block;
width: max-content;
}
#parlementairesjs_mp_next {
float: right;
color: black;
padding: 1em 3em 1em 3em;
overflow: hidden;
margin-bottom: 2em;
border: 2px solid black;
font-weight: bold;
cursor: default;
}
\ No newline at end of file
"use strict";
function ParlementairesJS(options) {
return Object.create({
options: options,
display: display
});
};
function display(targetId) {
if (!this.options) throw new Error("Options are missing");
if (!this.options.dataset) throw new Error("Empty data set.");
if (!targetId) throw new Error("HTML ID missing.");
var target = document.getElementById(targetId);
if (!target) throw new Error("Cannot find target with id " + targetId + ".");
// Initiate vars
var mps = [];
var groups = [];
var counties = [];
var selected = [];
var group = 0;
var county = 0;
var currentMp = 0;
// HTML vars
var parlementairesjsMpTotal,
parlementairesjsSelectGroupDefaultOption,
parlementairesjsSelectGroup,
parlementairesjsSelectCountyDefaultOption,
parlementairesjsSelectCounty,
parlementairesjsIntrophone,
parlementairesjsIntrophoneWrapper,
parlementairesjsMpPhoto,
parlementairesjsMpPhotoWrapper,
parlementairesjsMpFirstName,
parlementairesjsMpLastName,
parlementairesjsMpNameWrapper,
parlementairesjsMpGroup,
parlementairesjsMpCounty,
parlementairesjsMpGroupCountyWrapper,
parlementairesjsMpPhone,
parlementairesjsMpMail,
parlementairesjsMpTwi,
parlementairesjsMpFb,
parlementairesjsMpInfo,
parlementairesjsMpPhotoInfoWrapper,
parlementairesjsMpNextWrapper,
parlementairesjsMpNext;
if (!this.options.datasetConfig) throw new Error("Dataset config is missing");
var options = this.options;
var requester = new XMLHttpRequest();
requester.open("GET", this.options.datasetConfig);
requester.onreadystatechange = function() {
if (this.readyState == XMLHttpRequest.DONE) {
if (this.status != 200) throw new Error("Cannot get dataset config (status " + this.status + ").");
var url = options.datasetConfig;
options.datasetConfig = JSON.parse(requester.responseText);
options.datasetConfig.url = url;
loadData();
}
};
requester.send();
function loadData() {
if (!options.datasetConfig.img) throw new Error("Image options are missing");
if (!options.datasetConfig.img.baseURL) throw new Error("Image base URL is missing");
// If the images' base URL in dataset's config is relative, rewrite it
if (options.datasetConfig.img.baseURL.lastIndexOf("http://", 0) !== 0 &&
options.datasetConfig.img.baseURL.lastIndexOf("https://", 0) !== 0) {
// If the dataset config is loaded from a relative URL, use the current hostname
if (options.datasetConfig.url.lastIndexOf("http://", 0) !== 0 &&
options.datasetConfig.url.lastIndexOf("https://", 0) !== 0) {
options.datasetConfig.img.baseURL = window.location.protocol + "//" + window.location.host + "/" + options.datasetConfig.img.baseURL;
} else { // If not, use the dataset config's URL
var hostnameExtractor = document.createElement("a");
hostnameExtractor.href = options.datasetConfig.url;
options.datasetConfig.img.baseURL = hostnameExtractor.protocol + "//" + hostnameExtractor.host + "/" + options.datasetConfig.img.baseURL;
}
}
if (!options.datasetConfig.img.width || !options.datasetConfig.img.height) throw new Error("Image size is missing");
if (!options.datasetConfig.designation ||
!options.datasetConfig.designation.singular ||
!options.datasetConfig.designation.plural)
throw new Error("Designations are missing");
if (options.writeHTML == null || options.writeHTML == undefined) options.writeHTML = true;
if (options.twitter == null || options.twitter == undefined) options.twitter = true;
if (options.facebook == null || options.facebook == undefined) options.facebook = true;
var requester = new XMLHttpRequest();
requester.open("GET", options.dataset, true);
requester.onreadystatechange = function() {
if (this.readyState == XMLHttpRequest.DONE) {
if (this.status != 200) throw new Error("Cannot get data (status " + this.status + ").");
mps = JSON.parse(requester.responseText);
showData();
}
};
requester.send();
// Load data and show fetched MPs
function showData() {
constructHTML();
// Remove filtered MPs
if (options.commissionFilter) {
var filteredMps = [];
for (var i=0; i<mps.length; i++) {
if (validateCommissionFilter(mps[i].commissions, options.commissionFilter)) {
filteredMps.push(mps[i]);
}
}
mps = filteredMps;
}
// List available groups and counties
for (var i=0; i<mps.length; i++) {
if (mps[i].county && counties.indexOf(mps[i].county) == -1) counties.push(mps[i].county);
if (mps[i].group && groups.indexOf(mps[i].group) == -1) groups.push(mps[i].group);
}
// Fill in group select
groups.sort();
for (var i=0; i<groups.length; i++) {
var groupOption = document.createElement("option");
groupOption.value = groups[i];
groupOption.innerHTML = groups[i];
parlementairesjsSelectGroup.appendChild(groupOption);
}
// Fill in counties selector
counties.sort();
for (var i=0; i<counties.length; i++) {
var countiesOption = document.createElement("option");
countiesOption.value = counties[i];
countiesOption.innerHTML = counties[i];
parlementairesjsSelectCounty.appendChild(countiesOption);
}
// Start a first filter
filter();
}
// Filter MPs according to group and/or county
function filter() {
// Get list of wanted MPs
selected = [];
for (var i=0; i<mps.length; i++) {
if ((group == 0 || mps[i].group == group) &&
(county == 0 || mps[i].county == county) &&
validateCommissionFilter(mps[i].commissions, options.commissionFilter))
{
selected.push(mps[i]);
}
}
// Update displayed MPs
parlementairesjsMpTotal.innerHTML = selected.length;
// Choose a random MP
currentMp = Math.floor(Math.random()*selected.length);
// Display information block if MP is available
if (selected.length) {
parlementairesjsMpInfo.style.display = "block";
next();
} else {
parlementairesjsMpPhoto.style.backgroundImage = "url()";
parlementairesjsMpInfo.style.display = "none";
}
}
// Select the next MP or loop
function next() {
currentMp++;
if (currentMp == selected.length) currentMp = 0;
update();
}
// Update MP informations
function update()
{
var mp = selected[currentMp];
resetInfo()
parlementairesjsMpFirstName.append(mp.first_name);
parlementairesjsMpLastName.append(mp.last_name);
if (mp.photo) {
if (!options.photoInImg) {
parlementairesjsMpPhoto.style.backgroundImage = "url('"+options.datasetConfig.img.baseURL+mp.photo+".jpg')";
} else {
parlementairesjsMpPhoto.src = options.datasetConfig.img.baseURL+mp.photo+".jpg";
}
}
if (mp.group) {
parlementairesjsMpGroup.append(mp.group);
}
if (mp.county) {
parlementairesjsMpCounty.append(mp.county);
}
if (mp.phone) {
var parlementairesjsMpPhoneWrapper = document.createElement("p");
parlementairesjsMpPhone.appendChild(parlementairesjsMpPhoneWrapper);
for (var i=0; i<mp.phone.length; i++) {
if (!options.phoneFilter || mp.phone[i].lastIndexOf(options.phoneFilter, 0) === 0) {
var parlementairesjsMpPhoneChild = document.createElement("a");
parlementairesjsMpPhoneChild.className = "parlementairesjs_mp_phone_child"
if (mp.phone[i].charAt(0) == "+") {
parlementairesjsMpPhoneChild.href = "tel:" + mp.phone[i];
parlementairesjsMpPhoneChild.append(mp.phone[i]);
} else {
parlementairesjsMpPhoneChild.href = "tel:+" + options.datasetConfig.phone.local_prefix + mp.phone[i].substring(1).replace(/ /g, "");
parlementairesjsMpPhoneChild.append(mp.phone[i].match(new RegExp("(.{1," + options.datasetConfig.phone.group_size + "})", "g"))
.join(options.datasetConfig.phone.separator));
}
parlementairesjsMpPhoneWrapper.appendChild(parlementairesjsMpPhoneChild);
}
}
}
if (mp.email) {
var parlementairesjsMpMailWrapper = document.createElement("p");
parlementairesjsMpMail.appendChild(parlementairesjsMpMailWrapper);
for (var i=0; i<mp.email.length; i++) {
if (!options.mailFilter || mp.email[i].split("@")[1] == options.mailFilter) {
var parlementairesjsMpMailChild = document.createElement("a");
parlementairesjsMpMailChild.className = "parlementairesjs_mp_mail_child"