Skip to content
Open
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

* [PR-2](https://github.com/itk-dev/itk-project-database/pull/2)
Add Symfony UX Turbo and stimulus
* [PR-1](https://github.com/itk-dev/itk-project-database/pull/1)
Initial Symfony 8 rebuild of the project database.
1 change: 1 addition & 0 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ tasks:
- "{{.PRETTIER}} 'assets/**/*.js' --write"
- "{{.PRETTIER}} 'assets/**/*.{css,scss}' --write"
- "{{.PRETTIER}} '**/*.{yml,yaml}' --write"
- "{{.MARKDOWNLINT}} '**/*.md' --fix"

static-analysis:
desc: 'Run PHPStan static analysis'
Expand Down
32 changes: 24 additions & 8 deletions assets/app.js
Original file line number Diff line number Diff line change
@@ -1,33 +1,33 @@
import "./stimulus_bootstrap.js";
import "./styles/app.css";
import TomSelect from "tom-select";
import "tom-select/dist/css/tom-select.default.min.css";

function initUserMenu() {
const toggle = document.getElementById("userMenuToggle");
const menu = document.getElementById("userMenu");
if (!toggle || !menu) {
if (!toggle || !menu || toggle.dataset.bound) {
return;
}
toggle.dataset.bound = "1";

toggle.addEventListener("click", (event) => {
event.stopPropagation();
menu.classList.toggle("is-open");
});

document.addEventListener("click", (event) => {
if (!menu.contains(event.target) && event.target !== toggle) {
menu.classList.remove("is-open");
}
});
}

function initCollections() {
document.querySelectorAll("[data-collection]").forEach((collection) => {
if (collection.dataset.bound) {
return;
}
const list = collection.querySelector("[data-collection-list]");
const addButton = collection.querySelector("[data-collection-add]");
if (!list || !addButton) {
return;
}
collection.dataset.bound = "1";

let index = list.querySelectorAll("[data-collection-item]").length;

Expand Down Expand Up @@ -65,6 +65,10 @@ function initCollections() {

function initContactSelect() {
document.querySelectorAll("[data-contact-select]").forEach((select) => {
if (select.dataset.bound) {
return;
}
select.dataset.bound = "1";
new TomSelect(select, {
plugins: ["remove_button"],
hideSelected: true,
Expand All @@ -73,7 +77,19 @@ function initContactSelect() {
});
}

document.addEventListener("DOMContentLoaded", () => {
// The document survives Turbo navigations, so the outside-click handler is
// registered once here rather than re-added on every page.
document.addEventListener("click", (event) => {
const menu = document.getElementById("userMenu");
const toggle = document.getElementById("userMenuToggle");
if (menu && !menu.contains(event.target) && event.target !== toggle) {
menu.classList.remove("is-open");
}
});

// Turbo Drive swaps <body> on each visit and never fires DOMContentLoaded;
// turbo:load runs on the first load and on every subsequent visit.
document.addEventListener("turbo:load", () => {
initUserMenu();
initCollections();
initContactSelect();
Expand Down
18 changes: 18 additions & 0 deletions assets/controllers.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"controllers": {
"@symfony/ux-turbo": {
"turbo-core": {
"enabled": true,
"fetch": "eager",
"autoimport": {
"@symfony/ux-turbo/dist/mercure_stream_source_element.js": true
}
},
"mercure-turbo-stream": {
"enabled": false,
"fetch": "eager"
}
}
},
"entrypoints": []
}
123 changes: 123 additions & 0 deletions assets/controllers/csrf_protection_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
const nameCheck = /^[-_a-zA-Z0-9]{4,22}$/;
const tokenCheck = /^[-_/+a-zA-Z0-9]{24,}$/;

// Generate and double-submit a CSRF token in a form field and a cookie, as defined by Symfony's SameOriginCsrfTokenManager
// Use `form.requestSubmit()` to ensure that the submit event is triggered. Using `form.submit()` will not trigger the event
// and thus this event-listener will not be executed.
document.addEventListener(
"submit",
function (event) {
generateCsrfToken(event.target);
},
true,
);

// When @hotwired/turbo handles form submissions, send the CSRF token in a header in addition to a cookie
// The `framework.csrf_protection.check_header` config option needs to be enabled for the header to be checked
document.addEventListener("turbo:submit-start", function (event) {
const h = generateCsrfHeaders(event.detail.formSubmission.formElement);
Object.keys(h).map(function (k) {
event.detail.formSubmission.fetchRequest.headers[k] = h[k];
});
});

// When @hotwired/turbo handles form submissions, remove the CSRF cookie once a form has been submitted
document.addEventListener("turbo:submit-end", function (event) {
removeCsrfToken(event.detail.formSubmission.formElement);
});

export function generateCsrfToken(formElement) {
const csrfField = formElement.querySelector(
'input[data-controller="csrf-protection"], input[name="_csrf_token"]',
);

if (!csrfField) {
return;
}

let csrfCookie = csrfField.getAttribute(
"data-csrf-protection-cookie-value",
);
let csrfToken = csrfField.value;

if (!csrfCookie && nameCheck.test(csrfToken)) {
csrfField.setAttribute(
"data-csrf-protection-cookie-value",
(csrfCookie = csrfToken),
);
csrfField.defaultValue = csrfToken = btoa(
String.fromCharCode.apply(
null,
(window.crypto || window.msCrypto).getRandomValues(
new Uint8Array(18),
),
),
);
}
csrfField.dispatchEvent(new Event("change", { bubbles: true }));

if (csrfCookie && tokenCheck.test(csrfToken)) {
const cookie =
csrfCookie +
"_" +
csrfToken +
"=" +
csrfCookie +
"; path=/; samesite=strict";
document.cookie =
window.location.protocol === "https:"
? "__Host-" + cookie + "; secure"
: cookie;
}
}

export function generateCsrfHeaders(formElement) {
const headers = {};
const csrfField = formElement.querySelector(
'input[data-controller="csrf-protection"], input[name="_csrf_token"]',
);

if (!csrfField) {
return headers;
}

const csrfCookie = csrfField.getAttribute(
"data-csrf-protection-cookie-value",
);

if (tokenCheck.test(csrfField.value) && nameCheck.test(csrfCookie)) {
headers[csrfCookie] = csrfField.value;
}

return headers;
}

export function removeCsrfToken(formElement) {
const csrfField = formElement.querySelector(
'input[data-controller="csrf-protection"], input[name="_csrf_token"]',
);

if (!csrfField) {
return;
}

const csrfCookie = csrfField.getAttribute(
"data-csrf-protection-cookie-value",
);

if (tokenCheck.test(csrfField.value) && nameCheck.test(csrfCookie)) {
const cookie =
csrfCookie +
"_" +
csrfField.value +
"=0; path=/; samesite=strict; max-age=0";

document.cookie =
window.location.protocol === "https:"
? "__Host-" + cookie + "; secure"
: cookie;
}
}

/* stimulusFetch: 'lazy' */
export default "csrf-protection-controller";
17 changes: 17 additions & 0 deletions assets/controllers/hello_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Controller } from "@hotwired/stimulus";

/*
* This is an example Stimulus controller!
*
* Any element with a data-controller="hello" attribute will cause
* this controller to be executed. The name "hello" comes from the filename:
* hello_controller.js -> "hello"
*
* Delete this file or adapt it for your use!
*/
export default class extends Controller {
connect() {
this.element.textContent =
"Hello Stimulus! Edit me in assets/controllers/hello_controller.js";
}
}
5 changes: 5 additions & 0 deletions assets/stimulus_bootstrap.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { startStimulusApp } from "@symfony/stimulus-bundle";

const app = startStimulusApp();
// register any custom, 3rd party controllers here
// app.register('some_controller_name', SomeImportedController);
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"symfony/security-bundle": "~8.1.0",
"symfony/translation": "~8.1.0",
"symfony/twig-bundle": "~8.1.0",
"symfony/ux-turbo": "^3.2",
"symfony/validator": "~8.1.0",
"symfony/yaml": "~8.1.0",
"twig/extra-bundle": "^2.12 || ^3.0",
Expand Down
Loading
Loading