Skip to content

Commit 9c60018

Browse files
committed
feature #891 [Website] Animate model name in code example (chr-hertel)
This PR was merged into the main branch. Discussion ---------- [Website] Animate model name in code example | Q | A | ------------- | --- | Bug fix? | no | New feature? | yes | Docs? | no | Issues | | License | MIT [Screencast from 2025-11-18 22-57-15.webm](https://github.com/user-attachments/assets/10569fcd-584d-4615-8756-c166716788a4) Commits ------- 6ef2131 Animate model name in code example
2 parents 26299d3 + 6ef2131 commit 9c60018

File tree

11 files changed

+180
-2
lines changed

11 files changed

+180
-2
lines changed

ai.symfony.com/assets/app.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import './stimulus_bootstrap.js';
12
import 'bootstrap';
23
import 'bootstrap/dist/css/bootstrap.min.css';
34
import './styles/app.css';
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"controllers": [],
3+
"entrypoints": []
4+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
const nameCheck = /^[-_a-zA-Z0-9]{4,22}$/;
2+
const tokenCheck = /^[-_/+a-zA-Z0-9]{24,}$/;
3+
4+
// Generate and double-submit a CSRF token in a form field and a cookie, as defined by Symfony's SameOriginCsrfTokenManager
5+
// Use `form.requestSubmit()` to ensure that the submit event is triggered. Using `form.submit()` will not trigger the event
6+
// and thus this event-listener will not be executed.
7+
document.addEventListener('submit', function (event) {
8+
generateCsrfToken(event.target);
9+
}, true);
10+
11+
// When @hotwired/turbo handles form submissions, send the CSRF token in a header in addition to a cookie
12+
// The `framework.csrf_protection.check_header` config option needs to be enabled for the header to be checked
13+
document.addEventListener('turbo:submit-start', function (event) {
14+
const h = generateCsrfHeaders(event.detail.formSubmission.formElement);
15+
Object.keys(h).map(function (k) {
16+
event.detail.formSubmission.fetchRequest.headers[k] = h[k];
17+
});
18+
});
19+
20+
// When @hotwired/turbo handles form submissions, remove the CSRF cookie once a form has been submitted
21+
document.addEventListener('turbo:submit-end', function (event) {
22+
removeCsrfToken(event.detail.formSubmission.formElement);
23+
});
24+
25+
export function generateCsrfToken (formElement) {
26+
const csrfField = formElement.querySelector('input[data-controller="csrf-protection"], input[name="_csrf_token"]');
27+
28+
if (!csrfField) {
29+
return;
30+
}
31+
32+
let csrfCookie = csrfField.getAttribute('data-csrf-protection-cookie-value');
33+
let csrfToken = csrfField.value;
34+
35+
if (!csrfCookie && nameCheck.test(csrfToken)) {
36+
csrfField.setAttribute('data-csrf-protection-cookie-value', csrfCookie = csrfToken);
37+
csrfField.defaultValue = csrfToken = btoa(String.fromCharCode.apply(null, (window.crypto || window.msCrypto).getRandomValues(new Uint8Array(18))));
38+
}
39+
csrfField.dispatchEvent(new Event('change', { bubbles: true }));
40+
41+
if (csrfCookie && tokenCheck.test(csrfToken)) {
42+
const cookie = csrfCookie + '_' + csrfToken + '=' + csrfCookie + '; path=/; samesite=strict';
43+
document.cookie = window.location.protocol === 'https:' ? '__Host-' + cookie + '; secure' : cookie;
44+
}
45+
}
46+
47+
export function generateCsrfHeaders (formElement) {
48+
const headers = {};
49+
const csrfField = formElement.querySelector('input[data-controller="csrf-protection"], input[name="_csrf_token"]');
50+
51+
if (!csrfField) {
52+
return headers;
53+
}
54+
55+
const csrfCookie = csrfField.getAttribute('data-csrf-protection-cookie-value');
56+
57+
if (tokenCheck.test(csrfField.value) && nameCheck.test(csrfCookie)) {
58+
headers[csrfCookie] = csrfField.value;
59+
}
60+
61+
return headers;
62+
}
63+
64+
export function removeCsrfToken (formElement) {
65+
const csrfField = formElement.querySelector('input[data-controller="csrf-protection"], input[name="_csrf_token"]');
66+
67+
if (!csrfField) {
68+
return;
69+
}
70+
71+
const csrfCookie = csrfField.getAttribute('data-csrf-protection-cookie-value');
72+
73+
if (tokenCheck.test(csrfField.value) && nameCheck.test(csrfCookie)) {
74+
const cookie = csrfCookie + '_' + csrfField.value + '=0; path=/; samesite=strict; max-age=0';
75+
76+
document.cookie = window.location.protocol === 'https:' ? '__Host-' + cookie + '; secure' : cookie;
77+
}
78+
}
79+
80+
/* stimulusFetch: 'lazy' */
81+
export default 'csrf-protection-controller';
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { Controller } from '@hotwired/stimulus';
2+
import Typed from 'typed.js';
3+
4+
export default class extends Controller {
5+
static values = {
6+
strings: Array,
7+
typeSpeed: { type: Number, default: 30 },
8+
smartBackspace: { type: Boolean, default: true },
9+
startDelay: Number,
10+
backSpeed: Number,
11+
shuffle: Boolean,
12+
backDelay: { type: Number, default: 700 },
13+
fadeOut: Boolean,
14+
fadeOutClass: { type: String, default: 'typed-fade-out' },
15+
fadeOutDelay: { type: Number, default: 500 },
16+
loop: Boolean,
17+
loopCount: { type: Number, default: Number.POSITIVE_INFINITY },
18+
showCursor: { type: Boolean, default: true },
19+
cursorChar: { type: String, default: '.' },
20+
autoInsertCss: { type: Boolean, default: true },
21+
attr: String,
22+
bindInputFocusEvents: Boolean,
23+
contentType: { type: String, default: 'html' },
24+
};
25+
26+
connect() {
27+
const options = {
28+
strings: this.stringsValue,
29+
typeSpeed: this.typeSpeedValue,
30+
smartBackspace: this.smartBackspaceValue,
31+
startDelay: this.startDelayValue,
32+
backSpeed: this.backSpeedValue,
33+
shuffle: this.shuffleValue,
34+
backDelay: this.backDelayValue,
35+
fadeOut: this.fadeOutValue,
36+
fadeOutClass: this.fadeOutClassValue,
37+
fadeOutDelay: this.fadeOutDelayValue,
38+
loop: this.loopValue,
39+
loopCount: this.loopCountValue,
40+
showCursor: this.showCursorValue,
41+
cursorChar: this.cursorCharValue,
42+
autoInsertCss: this.autoInsertCssValue,
43+
attr: this.attrValue,
44+
bindInputFocusEvents: this.bindInputFocusEventsValue,
45+
contentType: this.contentTypeValue,
46+
};
47+
48+
this.dispatchEvent('pre-connect', { options });
49+
const typed = new Typed(this.element, options);
50+
this.dispatchEvent('connect', { typed, options });
51+
}
52+
53+
dispatchEvent(name, payload) {
54+
this.dispatch(name, { detail: payload, prefix: 'typed' });
55+
}
56+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { startStimulusApp } from '@symfony/stimulus-bundle';
2+
3+
const app = startStimulusApp();
4+
// register any custom, 3rd party controllers here
5+
// app.register('some_controller_name', SomeImportedController);

ai.symfony.com/composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"symfony/flex": "^2",
1515
"symfony/framework-bundle": "*",
1616
"symfony/runtime": "*",
17+
"symfony/stimulus-bundle": "^2.31",
1718
"symfony/twig-bundle": "*",
1819
"symfony/ux-icons": "^2.31",
1920
"symfony/yaml": "*",

ai.symfony.com/config/bundles.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@
66
Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true],
77
Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
88
Symfony\UX\Icons\UXIconsBundle::class => ['all' => true],
9+
Symfony\UX\StimulusBundle\StimulusBundle::class => ['all' => true],
910
];

ai.symfony.com/importmap.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,13 @@
2626
'version' => '5.3.8',
2727
'type' => 'css',
2828
],
29+
'typed.js' => [
30+
'version' => '2.1.0',
31+
],
32+
'@hotwired/stimulus' => [
33+
'version' => '3.2.2',
34+
],
35+
'@symfony/stimulus-bundle' => [
36+
'path' => './vendor/symfony/stimulus-bundle/assets/dist/loader.js',
37+
],
2938
];

ai.symfony.com/symfony.lock

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,21 @@
8181
"config/routes.yaml"
8282
]
8383
},
84+
"symfony/stimulus-bundle": {
85+
"version": "2.31",
86+
"recipe": {
87+
"repo": "github.com/symfony/recipes",
88+
"branch": "main",
89+
"version": "2.24",
90+
"ref": "3357f2fa6627b93658d8e13baa416b2a94a50c5f"
91+
},
92+
"files": [
93+
"assets/controllers.json",
94+
"assets/controllers/csrf_protection_controller.js",
95+
"assets/controllers/typed_controller.js",
96+
"assets/stimulus_bootstrap.js"
97+
]
98+
},
8499
"symfony/twig-bundle": {
85100
"version": "7.3",
86101
"recipe": {

ai.symfony.com/templates/_header.html.twig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<nav class="navbar navbar-expand-lg navbar-dark">
33
<div class="container py-3">
44
<a class="navbar-brand" href="{{ path('homepage') }}">
5-
{{ ux_icon('logos:symfony-ai', {width: 'auto', height: 64}) }}
5+
{{ ux_icon('logos:symfony-ai', {width: 225, height: 64}) }}
66
</a>
77

88
<button

0 commit comments

Comments
 (0)