.burger-icon { width: 200px; height: 200px; /* taille du SVG */ border: 2px dotted #ddd; /* bordure = simple repère */ } Le tracé de notre premier rectangle est un jeu d’enfant aussi : l’élément SVG rect est fait pour ça, attribuons-lui des coordonnées (x=0 et y=0) ainsi qu’une largeur de "100" et une hauteur de "20". Vous aurez compris qu’à partir d’un premier rectangle, il n’est pas difficile de produire les deux suivants. Et voilà ! Pour ce qui est des coins arrondis, là aussi SVG a tout prévu sous la forme de l’attribut rx, à qui une valeur de "5" semble tout à fait parfaite. Le résultat est bluffant et on se rend compte de la puissance insoupçonnée d’un éditeur de code. Plus sérieusement, ce n’était vraiment pas compliqué, non ? Par contre, ce qui est vraiment dommage c’est de répéter les mêmes choses plusieurs fois… Mais justement, il se trouve que… la plupart des attributs SVG existent également sous forme de propriétés CSS ! Voici par conséquent comment nous allons pouvoir améliorer notre code actuel : rect { x: 0; rx: 5px; width: 100px; height: 20px; } .rect-1 { y: 0; } .rect-2 { y: 40px; } .rect-3 { y: 80px; } Autre avantage loin d’être anodin, ces propriétés CSS-SVG ont la bonne idée d’être animables : on peut par exemple effectuer une transition sur la propriété… y ! rect { ... transition: y 1s; } .rect-1:hover { y: 15px; } 2. Préparer le SVG et le rendre accessible Nous allons à présent nous atteler à transformer notre icône SVG en un véritable "bouton Burger", fonctionnel et accessible. Pour ce faire, on commence par placer le SVG dans un Notre icône SVG est considérée comme purement décorative, car c’est le bouton qui portera l’information. Nous veillons à lui appliquer les attributs suivants : Un attribut aria-hidden="true" Un attribut focusable="false" pour éviter de naviguer au sein du SVG. Aucun élément ni <desc> ni d’attribut title, aria-label, aria-labelledby, ni role="img" ... <svg class="burger-icon" aria-hidden="true" focusable="false" viewBox="0 0 100 100"> </svg> ... Le bouton, quant à lui, nécessite les éléments suivants : Un nom accessible (via aria-label ou un texte masqué à la ".sr-only") En option, et selon les cas de figure, un attribut aria-controls pour lier à la cible et un attribut aria-expanded pour signaler l’état du bouton. Dans notre cas, ce n’est pas néessaire. <button class="burger-button" aria-label="Menu" data-expanded="false"> ... </button> Voici le script JavaScript destiné à gérer l’interaction et la mise à jour des attributs data-, et déclencher l’animation de l’icône : (function () { function toggleNav() { // Define targets const button = document.querySelector(’.burger-button’); const target = document.querySelector(’#navigation’); button.addEventListener(’click’, () => { const currentState = target.getAttribute("data-state"); if (!currentState || currentState === "closed") { target.setAttribute("data-state", "opened"); button.setAttribute("data-expanded", "true"); } else { target.setAttribute("data-state", "closed"); button.setAttribute("data-expanded", "false"); } }); } // end toggleNav() toggleNav(); }()); Pouquoi JavaScript ? Très sincèrement parce que c’est son job de déclencher des actions au clic et de modifier des classes ou des attibuts en conséquence. Cette mission aurait été réalisable en CSS au moyen de cases à cocher mais, ne nous mentons pas, c’est un peu de la bidouille. 3. Les étapes de l’animation Pour être très précis, nous n’allons pas employer une "animation" pour nos effets, mais une combinaison de trois "transitions", qui se révèleront amplement suffisantes pour notre besoin. Voici le scénario étape par étape qui doit se réaliser : L’action de clic ou de touch sur l’élément button doit déclencher une série de trois transitions; La transition 1 consiste en un déplacement vertical de .rect-1 et .rect-3 qui se rejoignent au centre du SVG; La transition 2 consiste à faire disparaître .rect-2 qui traîne dans nos pattes. En terme de timing, cette transition doit se dérouler en même temps que la transition 1; La transition 3 se compose d’une rotation de 45 degrés de .rect-1 et .rect-3 et doit de déclencher juste après les transitions précédentes). Transition 1 et 2 : "translate" et "opacity" La propriété transitionest appliquée sur l’élément à l’état initial (hors événement) afin d’assurer une transition au retour lorsque l’événement est quitté. /* transition sur la propriété y et opacity, durée 0.3s */ rect { transition: y 0.3s, opacity 0.3s; } /* coordonnées y initiales */ .rect-1 { y: 0; } .rect-2 { y: 40px; } .rect-3 { y: 80px; } Au clic, le bouton passe en data-expanded "true" et on déplace verticalement deux rectangles au centre et on masque le 3e rectangle central. [data-expanded="true"] .rect-1 { y: 40px; } [data-expanded="true"] .rect-2 { opacity: 0; } [data-expanded="true"] .rect-3 { y: 40px; } Transition 3 : "rotate" Aux deux transitions précédentes, on ajoute une transition sur la propriété rotate sans oublier de la faire débuter après un léger délai. /* on attend un delai de 0.3s avant de commencer rotate */ rect { transition: y 0.3s, opacity 0.3s, rotate 0.3s 0.3s; } Au clic, les trois transitions se déclenchent. [data-expanded="true"] .rect-1 { y: 40px; rotate: 45deg; } [data-expanded="true"] .rect-2 { opacity: 0; } [data-expanded="true"] .rect-3 { y: 40px; rotate: -45deg; } ⚠️ J’imagine que cela ne vous a pas échappé : tout se passe très bien à l’aller, mais malheureusement pas au retour. L’explication provient du fait que la transition se déroule dans le sens inverse au retour et que la rotation se déclenche trop tôt. Il va nous falloir une transition différente à l’aller et au retour et gérer des délais différents entre la transition et la rotation. /* transition au retour (quand on perd le clic) */ /* on attend un delai de 0.3s avant de commencer y */ rect { transition: y 0.3s 0.3s, opacity 0.3s, rotate 0.3s; } /* transition à l’aller (quand on clique) */ /* on attend un delai de 0.3s avant de commencer rotate */ [data-expanded="true"] rect { transition: y 0.3s, opacity 0.3s, rotate 0.3s 0.3s; } Grâce à cette adaptation subtile, notre effet fonctionne parfaitement à l’aller et au retour lors de l’interaction. Pour finir en beauté, le truc en plus consiste en une petite accélération sous forme de cubic-bezier pour un effet de "rebond". [data-expanded="true"] rect { transition: y 0.3s, opacity 0.3s, rotate 0.3s 0.3s cubic-bezier(.55,-0.65,0,2.32); } CSS final Voici les styles CSS complets de ce tutoriel. Notez qu’ils prennent en compte les préférences utilisateur grâce au media query prefers-reduced-motion : si la personne a choisi dans ses réglages système de réduire les animations, celles-ci ne seront tout simplement pas déclenchées. Pour voir le résultat et aller plus loin, une petite collection CodePen de boutons burger animés a été rassemblée à cette adresse : https://codepen.io/collection/VYqwJK .rect-1 { y: 0; } .rect-2 { y: 40px; } .rect-3 { y: 80px; } [data-expanded="true"] .rect-1 { y: 40px; rotate: 45deg; } [data-expanded="true"] .rect-2 { opacity: 0; } [data-expanded="true"] .rect-3 { y: 40px; rotate: -45deg; } /* transitions si acceptées */ @media (prefers-reduced-motion: no-preference) { rect { transition: y 0.3s 0.3s, opacity 0.3s, rotate 0.3s; } [data-expanded="true"] rect { transition: y 0.3s, opacity 0.3s, rotate 0.3s 0.3s cubic-bezier(.55,-0.65,0,2.32); } } Retrouvez l’intégralité de ce tutoriel en ligne sur Alsacreations.com"> <meta property="og:image" content="https://feedbot.net/storage/thumbnails/2023_12/1b9392230da6166d97e929c0d06e3cd4-2bb58e10e10418b365be670dfe51b0f8.jpg"> <meta name="twitter:card" content="summary_large_image"> <meta name="twitter:site" content="@feedbotnet"> <meta name="twitter:title" content="Animer un bouton burger simple avec SVG et CSS"> <meta property="twitter:description" content="Le format SVG peut paraître parfois un peu intimidant, et l’associer à des transitions ou des animations CSS semble encore plus audacieux pour bon nombre de personnes. Cependant, dans certains cas, l’alchimie entre SVG et CSS est aussi bénéfique qu’extrêmement simple à mettre en oeuvre. Dans ce tutoriel, nous allons suivre étape par étape comment animer un bouton burger simple avec SVG et CSS. Quels outils ? La liste des outils nécessaires pour atteindre nos objectifs est particulièrement réduite puisqu’un simple éditeur de code fait le job (n’importe lequel fait l’affaire, Visual Studio Code étant mon choix personnel). Pour aller plus loin, et en guise de bonus, on peut également piocher : Un éditeur SVG en ligne (parce que ça peut toujours servir) Des recommendations concernant l’accessibilité des SVG (au hasard les Guidelines Alsacréations) Un éditeur de courbes de Bezier (pour des animations originales) SVG c’est quoi ? En trois mots, voici comment résumer SVG : SVG est un format graphique vectoriel (composé de tracés et de courbes) Il est développé et maintenu depuis 1999 par le W3C (standard officiel, open source) Il est conçu en XML (compatible HTML) (on peut le créer et le lire avec un simple éditeur de texte) 1. Produire le burger bouton en SVG Si l’on y regarde de plus près, une "icône Burger" c’est bêtement trois rectangles horizontaux espacés et avec des coins arrondis. Notre éditeur de code préféré est amplement suffisant pour s’aquitter de la tâche de dessiner des rectangles : on va tout d’abord dessiner un élément SVG vide avec une fenêtre de "100 x 100". C’est une dimension purement indicative car tout est proportionnel et adaptable en SVG. <svg class="burger-icon" viewBox="0 0 100 100"> </svg> .burger-icon { width: 200px; height: 200px; /* taille du SVG */ border: 2px dotted #ddd; /* bordure = simple repère */ } Le tracé de notre premier rectangle est un jeu d’enfant aussi : l’élément SVG rect est fait pour ça, attribuons-lui des coordonnées (x=0 et y=0) ainsi qu’une largeur de "100" et une hauteur de "20". <svg class="burger-icon" viewBox="0 0 100 100"> <rect x="0" y="0" width="100" height="20" /> </svg> Vous aurez compris qu’à partir d’un premier rectangle, il n’est pas difficile de produire les deux suivants. Et voilà ! <svg class="burger-icon" viewBox="0 0 100 100"> <rect x="0" y="0" width="100" height="20" /> <rect x="0" y="40" width="100" height="20" /> <rect x="0" y="80" width="100" height="20" /> </svg> Pour ce qui est des coins arrondis, là aussi SVG a tout prévu sous la forme de l’attribut rx, à qui une valeur de "5" semble tout à fait parfaite. <svg class="burger-icon" viewBox="0 0 100 100"> <rect x="0" y="0" width="100" height="20" rx="5" /> <rect x="0" y="40" width="100" height="20" rx="5" /> <rect x="0" y="80" width="100" height="20" rx="5" /> </svg> Le résultat est bluffant et on se rend compte de la puissance insoupçonnée d’un éditeur de code. Plus sérieusement, ce n’était vraiment pas compliqué, non ? Par contre, ce qui est vraiment dommage c’est de répéter les mêmes choses plusieurs fois… Mais justement, il se trouve que… la plupart des attributs SVG existent également sous forme de propriétés CSS ! Voici par conséquent comment nous allons pouvoir améliorer notre code actuel : <svg class="burger-icon" viewBox="0 0 100 100"> <rect class="rect-1" /> <rect class="rect-2" /> <rect class="rect-3" /> </svg> rect { x: 0; rx: 5px; width: 100px; height: 20px; } .rect-1 { y: 0; } .rect-2 { y: 40px; } .rect-3 { y: 80px; } Autre avantage loin d’être anodin, ces propriétés CSS-SVG ont la bonne idée d’être animables : on peut par exemple effectuer une transition sur la propriété… y ! rect { ... transition: y 1s; } .rect-1:hover { y: 15px; } 2. Préparer le SVG et le rendre accessible Nous allons à présent nous atteler à transformer notre icône SVG en un véritable "bouton Burger", fonctionnel et accessible. Pour ce faire, on commence par placer le SVG dans un <button> qui sera l’élément interactif au clic / touch et qui déclenchera l’animation. <button class="burger-button"> <svg class="burger-icon" viewBox="0 0 100 100"> <rect class="rect-1" /> <rect class="rect-2" /> <rect class="rect-3" /> </svg> </button> Notre icône SVG est considérée comme purement décorative, car c’est le bouton qui portera l’information. Nous veillons à lui appliquer les attributs suivants : Un attribut aria-hidden="true" Un attribut focusable="false" pour éviter de naviguer au sein du SVG. Aucun élément <title> ni <desc> ni d’attribut title, aria-label, aria-labelledby, ni role="img" ... <svg class="burger-icon" aria-hidden="true" focusable="false" viewBox="0 0 100 100"> </svg> ... Le bouton, quant à lui, nécessite les éléments suivants : Un nom accessible (via aria-label ou un texte masqué à la ".sr-only") En option, et selon les cas de figure, un attribut aria-controls pour lier à la cible et un attribut aria-expanded pour signaler l’état du bouton. Dans notre cas, ce n’est pas néessaire. <button class="burger-button" aria-label="Menu" data-expanded="false"> ... </button> Voici le script JavaScript destiné à gérer l’interaction et la mise à jour des attributs data-, et déclencher l’animation de l’icône : (function () { function toggleNav() { // Define targets const button = document.querySelector(’.burger-button’); const target = document.querySelector(’#navigation’); button.addEventListener(’click’, () => { const currentState = target.getAttribute("data-state"); if (!currentState || currentState === "closed") { target.setAttribute("data-state", "opened"); button.setAttribute("data-expanded", "true"); } else { target.setAttribute("data-state", "closed"); button.setAttribute("data-expanded", "false"); } }); } // end toggleNav() toggleNav(); }()); Pouquoi JavaScript ? Très sincèrement parce que c’est son job de déclencher des actions au clic et de modifier des classes ou des attibuts en conséquence. Cette mission aurait été réalisable en CSS au moyen de cases à cocher mais, ne nous mentons pas, c’est un peu de la bidouille. 3. Les étapes de l’animation Pour être très précis, nous n’allons pas employer une "animation" pour nos effets, mais une combinaison de trois "transitions", qui se révèleront amplement suffisantes pour notre besoin. Voici le scénario étape par étape qui doit se réaliser : L’action de clic ou de touch sur l’élément button doit déclencher une série de trois transitions; La transition 1 consiste en un déplacement vertical de .rect-1 et .rect-3 qui se rejoignent au centre du SVG; La transition 2 consiste à faire disparaître .rect-2 qui traîne dans nos pattes. En terme de timing, cette transition doit se dérouler en même temps que la transition 1; La transition 3 se compose d’une rotation de 45 degrés de .rect-1 et .rect-3 et doit de déclencher juste après les transitions précédentes). Transition 1 et 2 : "translate" et "opacity" La propriété transitionest appliquée sur l’élément à l’état initial (hors événement) afin d’assurer une transition au retour lorsque l’événement est quitté. /* transition sur la propriété y et opacity, durée 0.3s */ rect { transition: y 0.3s, opacity 0.3s; } /* coordonnées y initiales */ .rect-1 { y: 0; } .rect-2 { y: 40px; } .rect-3 { y: 80px; } Au clic, le bouton passe en data-expanded "true" et on déplace verticalement deux rectangles au centre et on masque le 3e rectangle central. [data-expanded="true"] .rect-1 { y: 40px; } [data-expanded="true"] .rect-2 { opacity: 0; } [data-expanded="true"] .rect-3 { y: 40px; } Transition 3 : "rotate" Aux deux transitions précédentes, on ajoute une transition sur la propriété rotate sans oublier de la faire débuter après un léger délai. /* on attend un delai de 0.3s avant de commencer rotate */ rect { transition: y 0.3s, opacity 0.3s, rotate 0.3s 0.3s; } Au clic, les trois transitions se déclenchent. [data-expanded="true"] .rect-1 { y: 40px; rotate: 45deg; } [data-expanded="true"] .rect-2 { opacity: 0; } [data-expanded="true"] .rect-3 { y: 40px; rotate: -45deg; } ⚠️ J’imagine que cela ne vous a pas échappé : tout se passe très bien à l’aller, mais malheureusement pas au retour. L’explication provient du fait que la transition se déroule dans le sens inverse au retour et que la rotation se déclenche trop tôt. Il va nous falloir une transition différente à l’aller et au retour et gérer des délais différents entre la transition et la rotation. /* transition au retour (quand on perd le clic) */ /* on attend un delai de 0.3s avant de commencer y */ rect { transition: y 0.3s 0.3s, opacity 0.3s, rotate 0.3s; } /* transition à l’aller (quand on clique) */ /* on attend un delai de 0.3s avant de commencer rotate */ [data-expanded="true"] rect { transition: y 0.3s, opacity 0.3s, rotate 0.3s 0.3s; } Grâce à cette adaptation subtile, notre effet fonctionne parfaitement à l’aller et au retour lors de l’interaction. Pour finir en beauté, le truc en plus consiste en une petite accélération sous forme de cubic-bezier pour un effet de "rebond". [data-expanded="true"] rect { transition: y 0.3s, opacity 0.3s, rotate 0.3s 0.3s cubic-bezier(.55,-0.65,0,2.32); } CSS final Voici les styles CSS complets de ce tutoriel. Notez qu’ils prennent en compte les préférences utilisateur grâce au media query prefers-reduced-motion : si la personne a choisi dans ses réglages système de réduire les animations, celles-ci ne seront tout simplement pas déclenchées. Pour voir le résultat et aller plus loin, une petite collection CodePen de boutons burger animés a été rassemblée à cette adresse : https://codepen.io/collection/VYqwJK .rect-1 { y: 0; } .rect-2 { y: 40px; } .rect-3 { y: 80px; } [data-expanded="true"] .rect-1 { y: 40px; rotate: 45deg; } [data-expanded="true"] .rect-2 { opacity: 0; } [data-expanded="true"] .rect-3 { y: 40px; rotate: -45deg; } /* transitions si acceptées */ @media (prefers-reduced-motion: no-preference) { rect { transition: y 0.3s 0.3s, opacity 0.3s, rotate 0.3s; } [data-expanded="true"] rect { transition: y 0.3s, opacity 0.3s, rotate 0.3s 0.3s cubic-bezier(.55,-0.65,0,2.32); } } Retrouvez l’intégralité de ce tutoriel en ligne sur Alsacreations.com"> <meta name="twitter:image" content="https://feedbot.net/storage/thumbnails/2023_12/1b9392230da6166d97e929c0d06e3cd4-2bb58e10e10418b365be670dfe51b0f8.jpg"> <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/amplitudejs@v5.3.2/dist/amplitude.js"></script> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <link rel="stylesheet" href="https://feedbot.net/assets/colors-dark.css?version=1735164589" id="theme"> <link rel="stylesheet" href="https://feedbot.net/assets/style.css?version=1735295619"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/fork-awesome@1.2.0/css/fork-awesome.min.css" integrity="sha256-XoaMnoYC5TH6/+ihMEnospgm0J1PM/nioxbOUdnM8HY=" crossorigin="anonymous"> <meta name="viewport" content="width=device-width, height=device-height, initial-scale=1.0,user-scalable=no, shrink-to-fit=yes" /> <meta name="apple-mobile-web-app-title" content="Feedbot" /> <meta name="apple-mobile-web-app-capable" content="yes"> <meta name="theme-color" content="#11101D" /> <meta name="apple-mobile-web-app-status-bar-style" content="black"> <link rel="apple-touch-startup-image" media="screen and (device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="assets/icons/iPhone_14__iPhone_13_Pro__iPhone_13__iPhone_12_Pro__iPhone_12_landscape.png"> <link rel="apple-touch-startup-image" media="screen and (device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="assets/icons/iPhone_11__iPhone_XR_landscape.png"> <link rel="apple-touch-startup-image" media="screen and (device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="assets/icons/iPhone_13_mini__iPhone_12_mini__iPhone_11_Pro__iPhone_XS__iPhone_X_portrait.png"> <link rel="apple-touch-startup-image" media="screen and (device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="assets/icons/iPhone_14_Pro_Max_landscape.png"> <link rel="apple-touch-startup-image" media="screen and (device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="assets/icons/10.5__iPad_Air_portrait.png"> <link rel="apple-touch-startup-image" media="screen and (device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="assets/icons/11__iPad_Pro__10.5__iPad_Pro_portrait.png"> <link rel="apple-touch-startup-image" media="screen and (device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="assets/icons/iPhone_14_Plus__iPhone_13_Pro_Max__iPhone_12_Pro_Max_landscape.png"> <link rel="apple-touch-startup-image" media="screen and (device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="assets/icons/12.9__iPad_Pro_portrait.png"> <link rel="apple-touch-startup-image" media="screen and (device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="assets/icons/iPhone_11_Pro_Max__iPhone_XS_Max_portrait.png"> <link rel="apple-touch-startup-image" media="screen and (device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="assets/icons/iPhone_8_Plus__iPhone_7_Plus__iPhone_6s_Plus__iPhone_6_Plus_portrait.png"> <link rel="apple-touch-startup-image" media="screen and (device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="assets/icons/4__iPhone_SE__iPod_touch_5th_generation_and_later_landscape.png"> <link rel="apple-touch-startup-image" media="screen and (device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="assets/icons/11__iPad_Pro__10.5__iPad_Pro_landscape.png"> <link rel="apple-touch-startup-image" media="screen and (device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="assets/icons/iPhone_8__iPhone_7__iPhone_6s__iPhone_6__4.7__iPhone_SE_landscape.png"> <link rel="apple-touch-startup-image" media="screen and (device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="assets/icons/4__iPhone_SE__iPod_touch_5th_generation_and_later_portrait.png"> <link rel="apple-touch-startup-image" media="screen and (device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="assets/icons/9.7__iPad_Pro__7.9__iPad_mini__9.7__iPad_Air__9.7__iPad_portrait.png"> <link rel="apple-touch-startup-image" media="screen and (device-width: 393px) and (device-height: 852px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="assets/icons/iPhone_14_Pro_landscape.png"> <link rel="apple-touch-startup-image" media="screen and (device-width: 820px) and (device-height: 1180px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="assets/icons/10.9__iPad_Air_portrait.png"> <link rel="apple-touch-startup-image" media="screen and (device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="assets/icons/10.2__iPad_portrait.png"> <link rel="apple-touch-startup-image" media="screen and (device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="assets/icons/10.5__iPad_Air_landscape.png"> <link rel="apple-touch-startup-image" media="screen and (device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="assets/icons/9.7__iPad_Pro__7.9__iPad_mini__9.7__iPad_Air__9.7__iPad_landscape.png"> <link rel="apple-touch-startup-image" media="screen and (device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="assets/icons/12.9__iPad_Pro_landscape.png"> <link rel="apple-touch-startup-image" media="screen and (device-width: 820px) and (device-height: 1180px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="assets/icons/10.9__iPad_Air_landscape.png"> <link rel="apple-touch-startup-image" media="screen and (device-width: 393px) and (device-height: 852px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="assets/icons/iPhone_14_Pro_portrait.png"> <link rel="apple-touch-startup-image" media="screen and (device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="assets/icons/iPhone_14__iPhone_13_Pro__iPhone_13__iPhone_12_Pro__iPhone_12_portrait.png"> <link rel="apple-touch-startup-image" media="screen and (device-width: 744px) and (device-height: 1133px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="assets/icons/8.3__iPad_Mini_portrait.png"> <link rel="apple-touch-startup-image" media="screen and (device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="assets/icons/iPhone_8_Plus__iPhone_7_Plus__iPhone_6s_Plus__iPhone_6_Plus_landscape.png"> <link rel="apple-touch-startup-image" media="screen and (device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="assets/icons/iPhone_13_mini__iPhone_12_mini__iPhone_11_Pro__iPhone_XS__iPhone_X_landscape.png"> <link rel="apple-touch-startup-image" media="screen and (device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="assets/icons/iPhone_11_Pro_Max__iPhone_XS_Max_landscape.png"> <link rel="apple-touch-startup-image" media="screen and (device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="assets/icons/iPhone_8__iPhone_7__iPhone_6s__iPhone_6__4.7__iPhone_SE_portrait.png"> <link rel="apple-touch-startup-image" media="screen and (device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="assets/icons/iPhone_14_Pro_Max_portrait.png"> <link rel="apple-touch-startup-image" media="screen and (device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="assets/icons/iPhone_14_Plus__iPhone_13_Pro_Max__iPhone_12_Pro_Max_portrait.png"> <link rel="apple-touch-startup-image" media="screen and (device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="assets/icons/10.2__iPad_landscape.png"> <link rel="apple-touch-startup-image" media="screen and (device-width: 744px) and (device-height: 1133px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="assets/icons/8.3__iPad_Mini_landscape.png"> <link rel="apple-touch-startup-image" media="screen and (device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="assets/icons/iPhone_11__iPhone_XR_portrait.png"> <link rel="manifest" href="./manifest.json"> <link rel="icon" type="image/png" href="https://feedbot.net/assets/icons/icon.png" /> <link rel="apple-touch-icon" href="https://feedbot.net/assets/icons/icon.png" /> <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/amplitudejs@{{version-number}}/dist/amplitude.js"></script> <script src="https://feedbot.net/assets/jquery-3.6.3.min.js"></script> <script type="text/javascript"> var website = 'https://feedbot.net'; $(document).ready(function () { $(".article_menu_dots").on("click", function(e){ e.stopPropagation(); var menu_id = $(this).attr("data-id"); $(".article_menu").not("[data-id=" + menu_id + "]").hide(); $(".article_menu[data-id=" + menu_id + "]").toggle(); }); $(document).on("click", function(){ $(".article_menu").hide(); }); $(".article_menu").on("click", function(e){ e.stopPropagation(); }); }); function hideelement($param) { $(".feed_" + $param).hide(); $(".share_" + $param).hide(); } function bookmark($param) { // Variable to hold request var request; // Bind to the submit event of our form $(".bookmark_" + $param).submit(function(event){ // Prevent default posting of form - put here to work in case of errors event.preventDefault(); // Abort any pending request if(request){ request.abort(); } // setup some local variables var $form = $(this); // Let's select and cache all the fields var $inputs = $form.find("input, select, button, textarea"); // Serialize the data in the form var serializedData = $form.serialize(); // Let's disable the inputs for the duration of the Ajax request. // Note: we disable elements AFTER the form data has been serialized. // Disabled form elements will not be serialized. $inputs.prop("disabled", true); // Fire off the request to /form.php request = $.ajax({ url: website + "/includes/action.php", type: "post", data: serializedData }); // Callback handler that will be called on success request.done(function (response, textStatus, jqXHR){ // Log a message to the console $(".bookmark_" + $param).hide(); $(".unbookmark_" + $param).show(); }); // Callback handler that will be called on failure request.fail(function (jqXHR, textStatus, errorThrown){ // Log the error to the console console.error( "The following error occurred: "+ textStatus, errorThrown ); }); // Callback handler that will be called regardless // if the request failed or succeeded request.always(function (){ // Reenable the inputs $inputs.prop("disabled", false); }); }); } function unbookmark($param) { // Variable to hold request var request; // Bind to the submit event of our form $(".unbookmark_" + $param).submit(function(event){ // Prevent default posting of form - put here to work in case of errors event.preventDefault(); // Abort any pending request if (request) { request.abort(); } // setup some local variables var $form = $(this); // Let's select and cache all the fields var $inputs = $form.find("input, select, button, textarea"); // Serialize the data in the form var serializedData = $form.serialize(); // Let's disable the inputs for the duration of the Ajax request. // Note: we disable elements AFTER the form data has been serialized. // Disabled form elements will not be serialized. $inputs.prop("disabled", true); // Fire off the request to /form.php request = $.ajax({ url: website + "/includes/action.php", type: "post", data: serializedData }); // Callback handler that will be called on success request.done(function (response, textStatus, jqXHR){ // Log a message to the console $(".unbookmark_" + $param).hide(); $(".bookmark_" + + $param).show(); }); // Callback handler that will be called on failure request.fail(function (jqXHR, textStatus, errorThrown){ // Log the error to the console console.error( "The following error occurred: "+ textStatus, errorThrown ); }); // Callback handler that will be called regardless // if the request failed or succeeded request.always(function (){ // Reenable the inputs $inputs.prop("disabled", false); }); }); } function subscribe($param) { // Variable to hold request var request; // Bind to the submit event of our form $(".subscribe_" + $param).submit(function(event){ // Prevent default posting of form - put here to work in case of errors event.preventDefault(); // Abort any pending request if (request) { request.abort(); } // setup some local variables var $form = $(this); // Let's select and cache all the fields var $inputs = $form.find("input, select, button, textarea"); // Serialize the data in the form var serializedData = $form.serialize(); // Let's disable the inputs for the duration of the Ajax request. // Note: we disable elements AFTER the form data has been serialized. // Disabled form elements will not be serialized. $inputs.prop("disabled", true); // Fire off the request to /form.php request = $.ajax({ url: website + "/includes/action.php", type: "post", data: serializedData }); // Callback handler that will be called on success request.done(function (response, textStatus, jqXHR){ // Log a message to the console $(".subscribe_" + $param).hide(); $(".suggestion_" + $param).hide(); $(".unsubscribe_" + $param).show(); $(".unsubscribe_" + $param).children("input[name='sub_id']").val(''+response+''); }); // Callback handler that will be called on failure request.fail(function (jqXHR, textStatus, errorThrown){ // Log the error to the console console.error( "The following error occurred: "+ textStatus, errorThrown ); }); // Callback handler that will be called regardless // if the request failed or succeeded request.always(function (){ // Reenable the inputs $inputs.prop("disabled", false); }); }); } function unsubscribe($param) { // Variable to hold request var request; // Bind to the submit event of our form $(".unsubscribe_" + $param).submit(function(event){ // Prevent default posting of form - put here to work in case of errors event.preventDefault(); // Abort any pending request if (request) { request.abort(); } // setup some local variables var $form = $(this); // Let's select and cache all the fields var $inputs = $form.find("input, select, button, textarea"); // Serialize the data in the form var serializedData = $form.serialize(); // Let's disable the inputs for the duration of the Ajax request. // Note: we disable elements AFTER the form data has been serialized. // Disabled form elements will not be serialized. $inputs.prop("disabled", true); // Fire off the request to /form.php request = $.ajax({ url: website + "/includes/action.php", type: "post", data: serializedData }); // Callback handler that will be called on success request.done(function (response, textStatus, jqXHR){ // Log a message to the console $(".unsubscribe_" + $param).hide(); $(".subscribe_" + $param).show(); }); // Callback handler that will be called on failure request.fail(function (jqXHR, textStatus, errorThrown){ // Log the error to the console console.error( "The following error occurred: "+ textStatus, errorThrown ); }); // Callback handler that will be called regardless // if the request failed or succeeded request.always(function (){ // Reenable the inputs $inputs.prop("disabled", false); }); }); } function unshare($param) { // Variable to hold request var request; // Bind to the submit event of our form $(".unshare_" + $param).submit(function(event){ // Prevent default posting of form - put here to work in case of errors event.preventDefault(); // Abort any pending request if (request) { request.abort(); } // setup some local variables var $form = $(this); // Let's select and cache all the fields var $inputs = $form.find("input, select, button, textarea"); // Serialize the data in the form var serializedData = $form.serialize(); // Let's disable the inputs for the duration of the Ajax request. // Note: we disable elements AFTER the form data has been serialized. // Disabled form elements will not be serialized. $inputs.prop("disabled", true); // Fire off the request to /form.php request = $.ajax({ url: website + "/includes/action.php", type: "post", data: serializedData }); // Callback handler that will be called on success request.done(function (response, textStatus, jqXHR){ // Log a message to the console $(".unshare_" + $param).hide(); $(".share_" + $param).show(); }); // Callback handler that will be called on failure request.fail(function (jqXHR, textStatus, errorThrown){ // Log the error to the console console.error( "The following error occurred: "+ textStatus, errorThrown ); }); // Callback handler that will be called regardless // if the request failed or succeeded request.always(function (){ // Reenable the inputs $inputs.prop("disabled", false); }); }); } function autoshare_on($param) { // Variable to hold request var request; // Bind to the submit event of our form $(".autoshare_on_" + $param).submit(function(event){ // Prevent default posting of form - put here to work in case of errors event.preventDefault(); // Abort any pending request if (request) { request.abort(); } // setup some local variables var $form = $(this); // Let's select and cache all the fields var $inputs = $form.find("input, select, button, textarea"); // Serialize the data in the form var serializedData = $form.serialize(); // Let's disable the inputs for the duration of the Ajax request. // Note: we disable elements AFTER the form data has been serialized. // Disabled form elements will not be serialized. $inputs.prop("disabled", true); // Fire off the request to /form.php request = $.ajax({ url: website + "/includes/action.php", type: "post", data: serializedData }); // Callback handler that will be called on success request.done(function (response, textStatus, jqXHR){ // Log a message to the console $(".autoshare_on_" + $param).hide(); $(".autoshare_off_" + $param).show(); $(".sharing_options_" + $param).show(); }); // Callback handler that will be called on failure request.fail(function (jqXHR, textStatus, errorThrown){ // Log the error to the console console.error( "The following error occurred: "+ textStatus, errorThrown ); }); // Callback handler that will be called regardless // if the request failed or succeeded request.always(function (){ // Reenable the inputs $inputs.prop("disabled", false); }); }); } function autoshare_off($param) { // Variable to hold request var request; // Bind to the submit event of our form $(".autoshare_off_" + $param).submit(function(event){ // Prevent default posting of form - put here to work in case of errors event.preventDefault(); // Abort any pending request if (request) { request.abort(); } // setup some local variables var $form = $(this); // Let's select and cache all the fields var $inputs = $form.find("input, select, button, textarea"); // Serialize the data in the form var serializedData = $form.serialize(); // Let's disable the inputs for the duration of the Ajax request. // Note: we disable elements AFTER the form data has been serialized. // Disabled form elements will not be serialized. $inputs.prop("disabled", true); // Fire off the request to /form.php request = $.ajax({ url: website + "/includes/action.php", type: "post", data: serializedData }); // Callback handler that will be called on success request.done(function (response, textStatus, jqXHR){ // Log a message to the console $(".autoshare_off_" + $param).hide(); $(".sharing_options_" + $param).hide(); $(".autoshare_on_" + $param).show(); }); // Callback handler that will be called on failure request.fail(function (jqXHR, textStatus, errorThrown){ // Log the error to the console console.error( "The following error occurred: "+ textStatus, errorThrown ); }); // Callback handler that will be called regardless // if the request failed or succeeded request.always(function (){ // Reenable the inputs $inputs.prop("disabled", false); }); }); } function sharetitle_on($param) { // Variable to hold request var request; // Bind to the submit event of our form $(".sharetitle_on_" + $param).submit(function(event){ // Prevent default posting of form - put here to work in case of errors event.preventDefault(); // Abort any pending request if (request) { request.abort(); } // setup some local variables var $form = $(this); // Let's select and cache all the fields var $inputs = $form.find("input, select, button, textarea"); // Serialize the data in the form var serializedData = $form.serialize(); // Let's disable the inputs for the duration of the Ajax request. // Note: we disable elements AFTER the form data has been serialized. // Disabled form elements will not be serialized. $inputs.prop("disabled", true); // Fire off the request to /form.php request = $.ajax({ url: website + "/includes/action.php", type: "post", data: serializedData }); // Callback handler that will be called on success request.done(function (response, textStatus, jqXHR){ // Log a message to the console $(".sharetitle_on_" + $param).hide(); $(".sharetitle_off_" + $param).show(); }); // Callback handler that will be called on failure request.fail(function (jqXHR, textStatus, errorThrown){ // Log the error to the console console.error( "The following error occurred: "+ textStatus, errorThrown ); }); // Callback handler that will be called regardless // if the request failed or succeeded request.always(function (){ // Reenable the inputs $inputs.prop("disabled", false); }); }); } function sharetitle_off($param) { // Variable to hold request var request; // Bind to the submit event of our form $(".sharetitle_off_" + $param).submit(function(event){ // Prevent default posting of form - put here to work in case of errors event.preventDefault(); // Abort any pending request if (request) { request.abort(); } // setup some local variables var $form = $(this); // Let's select and cache all the fields var $inputs = $form.find("input, select, button, textarea"); // Serialize the data in the form var serializedData = $form.serialize(); // Let's disable the inputs for the duration of the Ajax request. // Note: we disable elements AFTER the form data has been serialized. // Disabled form elements will not be serialized. $inputs.prop("disabled", true); // Fire off the request to /form.php request = $.ajax({ url: website + "/includes/action.php", type: "post", data: serializedData }); // Callback handler that will be called on success request.done(function (response, textStatus, jqXHR){ // Log a message to the console $(".sharetitle_off_" + $param).hide(); $(".sharetitle_on_" + $param).show(); }); // Callback handler that will be called on failure request.fail(function (jqXHR, textStatus, errorThrown){ // Log the error to the console console.error( "The following error occurred: "+ textStatus, errorThrown ); }); // Callback handler that will be called regardless // if the request failed or succeeded request.always(function (){ // Reenable the inputs $inputs.prop("disabled", false); }); }); } function sharedescription_on($param) { // Variable to hold request var request; // Bind to the submit event of our form $(".sharedescription_on_" + $param).submit(function(event){ // Prevent default posting of form - put here to work in case of errors event.preventDefault(); // Abort any pending request if (request) { request.abort(); } // setup some local variables var $form = $(this); // Let's select and cache all the fields var $inputs = $form.find("input, select, button, textarea"); // Serialize the data in the form var serializedData = $form.serialize(); // Let's disable the inputs for the duration of the Ajax request. // Note: we disable elements AFTER the form data has been serialized. // Disabled form elements will not be serialized. $inputs.prop("disabled", true); // Fire off the request to /form.php request = $.ajax({ url: website + "/includes/action.php", type: "post", data: serializedData }); // Callback handler that will be called on success request.done(function (response, textStatus, jqXHR){ // Log a message to the console $(".sharedescription_on_" + $param).hide(); $(".sharedescription_off_" + $param).show(); }); // Callback handler that will be called on failure request.fail(function (jqXHR, textStatus, errorThrown){ // Log the error to the console console.error( "The following error occurred: "+ textStatus, errorThrown ); }); // Callback handler that will be called regardless // if the request failed or succeeded request.always(function (){ // Reenable the inputs $inputs.prop("disabled", false); }); }); } function sharedescription_off($param) { // Variable to hold request var request; // Bind to the submit event of our form $(".sharedescription_off_" + $param).submit(function(event){ // Prevent default posting of form - put here to work in case of errors event.preventDefault(); // Abort any pending request if (request) { request.abort(); } // setup some local variables var $form = $(this); // Let's select and cache all the fields var $inputs = $form.find("input, select, button, textarea"); // Serialize the data in the form var serializedData = $form.serialize(); // Let's disable the inputs for the duration of the Ajax request. // Note: we disable elements AFTER the form data has been serialized. // Disabled form elements will not be serialized. $inputs.prop("disabled", true); // Fire off the request to /form.php request = $.ajax({ url: website + "/includes/action.php", type: "post", data: serializedData }); // Callback handler that will be called on success request.done(function (response, textStatus, jqXHR){ // Log a message to the console $(".sharedescription_off_" + $param).hide(); $(".sharedescription_on_" + $param).show(); }); // Callback handler that will be called on failure request.fail(function (jqXHR, textStatus, errorThrown){ // Log the error to the console console.error( "The following error occurred: "+ textStatus, errorThrown ); }); // Callback handler that will be called regardless // if the request failed or succeeded request.always(function (){ // Reenable the inputs $inputs.prop("disabled", false); }); }); } function shareimage_on($param) { // Variable to hold request var request; // Bind to the submit event of our form $(".shareimage_on_" + $param).submit(function(event){ // Prevent default posting of form - put here to work in case of errors event.preventDefault(); // Abort any pending request if (request) { request.abort(); } // setup some local variables var $form = $(this); // Let's select and cache all the fields var $inputs = $form.find("input, select, button, textarea"); // Serialize the data in the form var serializedData = $form.serialize(); // Let's disable the inputs for the duration of the Ajax request. // Note: we disable elements AFTER the form data has been serialized. // Disabled form elements will not be serialized. $inputs.prop("disabled", true); // Fire off the request to /form.php request = $.ajax({ url: website + "/includes/action.php", type: "post", data: serializedData }); // Callback handler that will be called on success request.done(function (response, textStatus, jqXHR){ // Log a message to the console $(".shareimage_on_" + $param).hide(); $(".shareimage_off_" + $param).show(); }); // Callback handler that will be called on failure request.fail(function (jqXHR, textStatus, errorThrown){ // Log the error to the console console.error( "The following error occurred: "+ textStatus, errorThrown ); }); // Callback handler that will be called regardless // if the request failed or succeeded request.always(function (){ // Reenable the inputs $inputs.prop("disabled", false); }); }); } function shareimage_off($param) { // Variable to hold request var request; // Bind to the submit event of our form $(".shareimage_off_" + $param).submit(function(event){ // Prevent default posting of form - put here to work in case of errors event.preventDefault(); // Abort any pending request if (request) { request.abort(); } // setup some local variables var $form = $(this); // Let's select and cache all the fields var $inputs = $form.find("input, select, button, textarea"); // Serialize the data in the form var serializedData = $form.serialize(); // Let's disable the inputs for the duration of the Ajax request. // Note: we disable elements AFTER the form data has been serialized. // Disabled form elements will not be serialized. $inputs.prop("disabled", true); // Fire off the request to /form.php request = $.ajax({ url: website + "/includes/action.php", type: "post", data: serializedData }); // Callback handler that will be called on success request.done(function (response, textStatus, jqXHR){ // Log a message to the console $(".shareimage_off_" + $param).hide(); $(".shareimage_on_" + $param).show(); }); // Callback handler that will be called on failure request.fail(function (jqXHR, textStatus, errorThrown){ // Log the error to the console console.error( "The following error occurred: "+ textStatus, errorThrown ); }); // Callback handler that will be called regardless // if the request failed or succeeded request.always(function (){ // Reenable the inputs $inputs.prop("disabled", false); }); }); } function is_sensitive_on($param) { // Variable to hold request var request; // Bind to the submit event of our form $(".is_sensitive_on_" + $param).submit(function(event){ // Prevent default posting of form - put here to work in case of errors event.preventDefault(); // Abort any pending request if (request) { request.abort(); } // setup some local variables var $form = $(this); // Let's select and cache all the fields var $inputs = $form.find("input, select, button, textarea"); // Serialize the data in the form var serializedData = $form.serialize(); // Let's disable the inputs for the duration of the Ajax request. // Note: we disable elements AFTER the form data has been serialized. // Disabled form elements will not be serialized. $inputs.prop("disabled", true); // Fire off the request to /form.php request = $.ajax({ url: website + "/includes/action.php", type: "post", data: serializedData }); // Callback handler that will be called on success request.done(function (response, textStatus, jqXHR){ // Log a message to the console $(".is_sensitive_on_" + $param).hide(); $(".is_sensitive_off_" + $param).show(); }); // Callback handler that will be called on failure request.fail(function (jqXHR, textStatus, errorThrown){ // Log the error to the console console.error( "The following error occurred: "+ textStatus, errorThrown ); }); // Callback handler that will be called regardless // if the request failed or succeeded request.always(function (){ // Reenable the inputs $inputs.prop("disabled", false); }); }); } function is_sensitive_off($param) { // Variable to hold request var request; // Bind to the submit event of our form $(".is_sensitive_off_" + $param).submit(function(event){ // Prevent default posting of form - put here to work in case of errors event.preventDefault(); // Abort any pending request if (request) { request.abort(); } // setup some local variables var $form = $(this); // Let's select and cache all the fields var $inputs = $form.find("input, select, button, textarea"); // Serialize the data in the form var serializedData = $form.serialize(); // Let's disable the inputs for the duration of the Ajax request. // Note: we disable elements AFTER the form data has been serialized. // Disabled form elements will not be serialized. $inputs.prop("disabled", true); // Fire off the request to /form.php request = $.ajax({ url: website + "/includes/action.php", type: "post", data: serializedData }); // Callback handler that will be called on success request.done(function (response, textStatus, jqXHR){ // Log a message to the console $(".is_sensitive_off_" + $param).hide(); $(".is_sensitive_on_" + $param).show(); }); // Callback handler that will be called on failure request.fail(function (jqXHR, textStatus, errorThrown){ // Log the error to the console console.error( "The following error occurred: "+ textStatus, errorThrown ); }); // Callback handler that will be called regardless // if the request failed or succeeded request.always(function (){ // Reenable the inputs $inputs.prop("disabled", false); }); }); } function sensitive_text($param) { // Variable to hold request var request; // Bind to the submit event of our form $(".sensitive_text_" + $param).submit(function(event){ // Prevent default posting of form - put here to work in case of errors event.preventDefault(); // Abort any pending request if (request) { request.abort(); } // setup some local variables var $form = $(this); // Let's select and cache all the fields var $inputs = $form.find("input, select, button, textarea"); // Serialize the data in the form var serializedData = $form.serialize(); // Let's disable the inputs for the duration of the Ajax request. // Note: we disable elements AFTER the form data has been serialized. // Disabled form elements will not be serialized. $inputs.prop("disabled", true); // Fire off the request to /form.php request = $.ajax({ url: website + "/includes/action.php", type: "post", data: serializedData }); // Callback handler that will be called on success request.done(function (response, textStatus, jqXHR){ // Log a message to the console $(".sensitive_button_on_" + $param).hide(); $(".sensitive_button_off_" + $param).show(); }); // Callback handler that will be called on failure request.fail(function (jqXHR, textStatus, errorThrown){ // Log the error to the console console.error( "The following error occurred: "+ textStatus, errorThrown ); }); // Callback handler that will be called regardless // if the request failed or succeeded request.always(function (){ // Reenable the inputs $inputs.prop("disabled", false); }); }); } function set_visibility($param) { // Variable to hold request var request; // Bind to the submit event of our form $(".visibility_" + $param).change(function (event){ // Prevent default posting of form - put here to work in case of errors event.preventDefault(); // Abort any pending request if (request) { request.abort(); } // setup some local variables var $form = $(this); // Let's select and cache all the fields var $inputs = $form.find("input, select, button, textarea"); // Serialize the data in the form var serializedData = $form.serialize(); // Let's disable the inputs for the duration of the Ajax request. // Note: we disable elements AFTER the form data has been serialized. // Disabled form elements will not be serialized. $inputs.prop("disabled", true); // Fire off the request to /form.php request = $.ajax({ url: website + "/includes/action.php", type: "post", data: serializedData }); // Callback handler that will be called on success request.done(function (response, textStatus, jqXHR){ // Log a message to the console }); // Callback handler that will be called on failure request.fail(function (jqXHR, textStatus, errorThrown){ // Log the error to the console console.error( "The following error occurred: "+ textStatus, errorThrown ); }); // Callback handler that will be called regardless // if the request failed or succeeded request.always(function (){ // Reenable the inputs $inputs.prop("disabled", false); }); }); } function telegram_on($param) { // Variable to hold request var request; // Bind to the submit event of our form $(".telegram_on_" + $param).submit(function(event){ // Prevent default posting of form - put here to work in case of errors event.preventDefault(); // Abort any pending request if (request) { request.abort(); } // setup some local variables var $form = $(this); // Let's select and cache all the fields var $inputs = $form.find("input, select, button, textarea"); // Serialize the data in the form var serializedData = $form.serialize(); // Let's disable the inputs for the duration of the Ajax request. // Note: we disable elements AFTER the form data has been serialized. // Disabled form elements will not be serialized. $inputs.prop("disabled", true); // Fire off the request to /form.php request = $.ajax({ url: website + "/includes/action.php", type: "post", data: serializedData }); // Callback handler that will be called on success request.done(function (response, textStatus, jqXHR){ // Log a message to the console $(".telegram_on_" + $param).hide(); $(".telegram_off_" + $param).show(); }); // Callback handler that will be called on failure request.fail(function (jqXHR, textStatus, errorThrown){ // Log the error to the console console.error( "The following error occurred: "+ textStatus, errorThrown ); }); // Callback handler that will be called regardless // if the request failed or succeeded request.always(function (){ // Reenable the inputs $inputs.prop("disabled", false); }); }); } function telegram_off($param) { // Variable to hold request var request; // Bind to the submit event of our form $(".telegram_off_" + $param).submit(function(event){ // Prevent default posting of form - put here to work in case of errors event.preventDefault(); // Abort any pending request if (request) { request.abort(); } // setup some local variables var $form = $(this); // Let's select and cache all the fields var $inputs = $form.find("input, select, button, textarea"); // Serialize the data in the form var serializedData = $form.serialize(); // Let's disable the inputs for the duration of the Ajax request. // Note: we disable elements AFTER the form data has been serialized. // Disabled form elements will not be serialized. $inputs.prop("disabled", true); // Fire off the request to /form.php request = $.ajax({ url: website + "/includes/action.php", type: "post", data: serializedData }); // Callback handler that will be called on success request.done(function (response, textStatus, jqXHR){ // Log a message to the console $(".telegram_off_" + $param).hide(); $(".telegram_on_" + $param).show(); }); // Callback handler that will be called on failure request.fail(function (jqXHR, textStatus, errorThrown){ // Log the error to the console console.error( "The following error occurred: "+ textStatus, errorThrown ); }); // Callback handler that will be called regardless // if the request failed or succeeded request.always(function (){ // Reenable the inputs $inputs.prop("disabled", false); }); }); } function publish() { // Variable to hold request var request; // Bind to the submit event of our form $(".publish").submit(function(event){ // Prevent default posting of form - put here to work in case of errors event.preventDefault(); // Abort any pending request if (request) { request.abort(); } // setup some local variables var $form = $(this); // Let's select and cache all the fields var $inputs = $form.find("input, select, button, textarea"); // Serialize the data in the form var serializedData = $form.serialize(); // Let's disable the inputs for the duration of the Ajax request. // Note: we disable elements AFTER the form data has been serialized. // Disabled form elements will not be serialized. $inputs.prop("disabled", true); // Fire off the request to /form.php request = $.ajax({ url: website + "/includes/action.php", type: "post", data: serializedData }); // Callback handler that will be called on success request.done(function (response, textStatus, jqXHR){ // Log a message to the console $(".publish_button").hide(); $(".publish_area").val(""); $(".publish_button_sent").show(); setTimeout(function(){ $(".publish_button").show(); $(".publish_button_sent").hide(); }, 3000); }); // Callback handler that will be called on failure request.fail(function (jqXHR, textStatus, errorThrown){ // Log the error to the console console.error( "The following error occurred: "+ textStatus, errorThrown ); }); // Callback handler that will be called regardless // if the request failed or succeeded request.always(function (){ // Reenable the inputs $inputs.prop("disabled", false); }); }); } function counter(val) { var len = val.value.length; if(len >= 500){ val.value = val.value.substring(0, 500); } else{ $('.counter').text(500 - len); } } function counter_share(val) { var len = val.value.length; if(len >= 460){ val.value = val.value.substring(0, 460); } else{ $('.counter').text(460 - len); } } function share($param) { // Variable to hold request var request; // Bind to the submit event of our form $(".share_" + $param).submit(function(event){ // Prevent default posting of form - put here to work in case of errors event.preventDefault(); // Abort any pending request if (request) { request.abort(); } // setup some local variables var $form = $(this); // Let's select and cache all the fields var $inputs = $form.find("input, select, button, textarea"); // Serialize the data in the form var serializedData = $form.serialize(); // Fire off the request to /form.php request = $.ajax({ url: website + "/includes/share.php", type: "POST", data: serializedData }); // Callback handler that will be called on success request.done(function (data, response, textStatus, jqXHR){ // Log a message to the console $(".publish_popup").empty(); $(".publish_popup").append(data); $(".publish_popup").show(); $('html, body').css({overflow: 'hidden'}); }); // Callback handler that will be called on failure request.fail(function (jqXHR, textStatus, errorThrown){ // Log the error to the console console.error( "The following error occurred: "+ textStatus, errorThrown ); }); }); } function share2($param) { // Variable to hold request var request; // Bind to the submit event of our form $(".share_button").hide(); $(".shared_button").show(); $(".share_content_" + $param).submit(function(event){ // Prevent default posting of form - put here to work in case of errors event.preventDefault(); // Abort any pending request if (request) { request.abort(); } // setup some local variables var $form = $(this); // Let's select and cache all the fields var $inputs = $form.find("input, select, button, textarea"); // Serialize the data in the form var serializedData = $form.serialize(); // Fire off the request to /form.php request = $.ajax({ url: website + "/includes/action.php", type: "POST", data: serializedData }); // Callback handler that will be called on success request.done(function (response, textStatus, jqXHR){ // Log a message to the console $(".share_" + $param).hide(); $(".unshare_" + $param).show(); $(".unshare_" + $param).children("input[name='status_id']").val(''+response+''); $(".publish_popup").hide(); $(".publish_popup").empty(); $('html, body').css({overflow: ''}); }); // Callback handler that will be called on failure request.fail(function (jqXHR, textStatus, errorThrown){ // Log the error to the console console.error( "The following error occurred: "+ textStatus, errorThrown ); }); }); } function hide_publish(back_url) { var back = back_url; $(".publish_popup").hide(); $(".publish_popup").empty(); $('html, body').css({overflow: ''}); window.history.pushState("data", "Title", back); Amplitude.stop(); } function story(media_id){ var new_url = website + "/story/" + media_id; $.ajax({ url: website + '/includes/stories.php?media=' + media_id, type: "GET", beforeSend: function(){ $('.ajax-load').show(); $(".publish_popup").empty(); } }).done(function(data){ $('.ajax-load').hide(); $(".publish_popup").append(data); window.history.pushState("data", "Title", new_url); $(".publish_popup").show(); $('html, body').css({overflow: 'hidden'}); }).fail(function(jqXHR, ajaxOptions, thrownError){ }); } function podcast(id){ var new_url = website + "/podcast/" + id; $.ajax({ url: website + '/includes/podcast.php?id=' + id, type: "GET", beforeSend: function(){ $('.ajax-load').show(); $(".publish_popup").empty(); } }).done(function(data){ $('.ajax-load').hide(); $(".publish_popup").append(data); window.history.pushState("data", "Title", new_url); $(".publish_popup").show(); $('html, body').css({overflow: 'hidden'}); }).fail(function(jqXHR, ajaxOptions, thrownError){ }); } function dark_mode() { $('#theme').attr('href', 'https://feedbot.net/assets/colors-dark.css'); $('.light_mode').show(); $('.dark_mode').hide(); document.cookie = "theme=dark; path=/; max-age=" + 30*24*60*60; } function light_mode() { $('#theme').attr('href', 'https://feedbot.net/assets/colors-light.css'); $('.light_mode').hide(); $('.dark_mode').show(); document.cookie = "theme=light; path=/; max-age=" + 30*24*60*60; $(".video_cinema").hide(); } function constructFeed(json, getJSON = true){ return new Promise((resolve, reject) => { var constructFeed; var num_content = json_content.length - 1; $.each(json, function(index, value){ if(json_content[index]){ var feed_id = json_content[index]["feed_id"]; if(json_content[index]["youtube_id"]){ var youtube_id = json_content[index]["youtube_id"]; } else{ var youtube_id = ""; } if(json_content[index]["peertube_id"]){ var peertube_id = json_content[index]["peertube_id"]; } else{ var peertube_id = ""; } if(json_content[index]["description"]){ var description = json_content[index]["description"]; description = description.replace(/\\n/g, '<br>'); } constructFeed += ` <div class="content-home" style="padding-top: 15px; padding-bottom: 0px;" id="` + json_content[index]["date"] + `"> <div style="height:75px;">`; if(json_feeds[feed_id]["is_subscribed"] == 1){ constructFeed += ` <form class="follow-button subscribe_` + json_content[index]["feed_id"] + `" style="display:none;" title="SUBSCRIBE_TO ` + json_feeds[feed_id]["name"] + `"> <input type="hidden" name="action" value="subscribe"> <input type="hidden" name="feed_id" value="` + json_content[index]["feed_id"] + `"> <button onclick="subscribe(` + json_content[index]["feed_id"] + `)"><i class="fa fa-rss" aria-hidden="true"></i></button> </form> <form class="follow-button-subscribed unsubscribe_` + json_content[index]["feed_id"] + `" title="UNSUBSCRIBE_TO ` + json_feeds[feed_id]["name"] + `"> <input type="hidden" name="action" value="unsuscribe"> <input type="hidden" name="feed_id" value="` + json_content[index]["feed_id"] + `"> <input type="hidden" name="sub_id" value="` + json_feeds[feed_id]["sub_id"] + `"> <button onclick="unsubscribe(` + json_content[index]["feed_id"] + `)"> <i class="fa fa-rss" aria-hidden="true"></i> </button> </form>`; } else{ constructFeed += ` <form class="follow-button subscribe_` + json_content[index]["feed_id"] + `" title="SUBSCRIBE_TO ` + json_feeds[feed_id]["name"] + `"> <input type="hidden" name="action" value="subscribe"> <input type="hidden" name="feed_id" value="` + json_content[index]["feed_id"] + `"> <button onclick="subscribe(` + json_content[index]["feed_id"] + `)"><i class="fa fa-rss" aria-hidden="true"></i></button> </form> <form class="follow-button-subscribed unsubscribe_` + json_content[index]["feed_id"] + `" style="display:none;" title="UNSUBSCRIBE_TO ` + json_feeds[feed_id]["name"] + `"> <input type="hidden" name="action" value="unsuscribe"> <input type="hidden" name="feed_id" value="` + json_content[index]["feed_id"] + `"> <input type="hidden" name="sub_id" value="` + json_feeds[feed_id]["sub_id"] + `"> <button onclick="unsubscribe(` + json_content[index]["feed_id"] + `)"> <i class="fa fa-rss" aria-hidden="true"></i> </button> </form>`; } constructFeed += ` <div style="width: 60px; aspect-ratio: 1 / 1; background-image: url(` + json_feeds[feed_id]["avatar"] + `); background-size: cover; background-position: center; border-radius: 50%; float:left; overflow: hidden;"> <a href="https://feedbot.net/feed/` + json_content[index]["feed_id"] + `" style="display:block; width:100%; height:100%;"></a> </div> <div style="margin-left: 74px; padding-top: 13px; line-height: 16px;"> <div style="display: flex; grid-gap: 4px;"> <a href="https://feedbot.net/feed/` + json_content[index]["feed_id"] + `" style="color:var(--feedbot-title); font-weight: bold; text-decoration: none; display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical; overflow: hidden;" title="` + json_feeds[feed_id]["name"] + `">` + json_feeds[feed_id]["name"] +`</a>`; if(json_content[index]["youtube_id"]){ constructFeed += ` <img src="https://feedbot.net/assets/youtube.png" alt="YouTube channel" style="width:20px; margin-left: 4px; margin-bottom: 1px; vertical-align: middle;" />`; } if(json_content[index]["peertube_id"]){ constructFeed += ` <img src="https://feedbot.net/assets/peertube.png" alt="PeerTube channel" style="width:14px; margin-left: 5px; vertical-align: middle;" />`; } constructFeed += ` </div>`; if(!json_content[index]["youtube_id"] && !json_content[index]["peertube_id"]){ constructFeed += ` <a href="https://feedbot.net/feed/` + json_content[index]["feed_id"] + `/` + json_content[index]["article_id"] + `" class="relative_date" style="font-size: 12px; text-decoration: none; color: var(--feedbot-text);" data-timestamp="` + json_content[index]["date"] + `">` + relativedate(json_content[index]["date"]) + `</a>`; } else if(json_content[index]["youtube_id"] && !json_content[index]["peertube_id"]){ constructFeed += ` <a href="https://feedbot.net/watch/` + json_content[index]["youtube_id"] + `" class="relative_date" style="font-size: 12px; text-decoration: none; color: var(--feedbot-text);" data-timestamp="` + json_content[index]["date"] + `">` + relativedate(json_content[index]["date"]) + `</a>`; } else if(!json_content[index]["youtube_id"] && json_content[index]["peertube_id"]){ constructFeed += ` <a href="https://feedbot.net/watch/` + json_content[index]["peertube_id"] + `" class="relative_date" style="font-size: 12px; text-decoration: none; color: var(--feedbot-text);" data-timestamp="` + json_content[index]["date"] + `">` + relativedate(json_content[index]["date"]) + `</a>`; } constructFeed += ` </div> </div>`; if(json_content[index]["description"]){ constructFeed += ` <div class="description" id="description-` + json_content[index]["article_id"] + `" data-article-id="` + json_content[index]["article_id"] + `">` + description + `</div> <div style="height:20px;"> </div>`; } constructFeed +=` <div class="timeline-thumbnail"> <div class="timeline-thumbnail-content" style="background-image: url(` + json_content[index]["thumbnail"] + `);">`; if(json_content[index]["youtube_id"]){ constructFeed += ` <a href="https://feedbot.net/watch/` + json_content[index]["youtube_id"] + `" style="display:block; width:100%; height:100%;"></a> </div> <div class="timeline-title"> <a href="https://feedbot.net/watch/` + json_content[index]["youtube_id"] + `" style="text-decoration: none;"><span style="color:var(--feedbot-gray); font-size: 12px; text-transform: uppercase;">` + json_content[index]["media_url"] + `</span><br /><span style="color:var(--feedbot-title); font-weight:bold;">` + json_content[index]["title"] + `</span></a> </div> </div>`; } if(json_content[index]["peertube_id"]){ constructFeed += ` <a href="https://feedbot.net/watch/` + json_content[index]["peertube_id"] + `" style="display:block; width:100%; height:100%;"></a> </div> <div class="timeline-title"> <a href="https://feedbot.net/watch/` + json_content[index]["peertube_id"] + `" style="text-decoration: none;"><span style="color:var(--feedbot-gray); font-size: 12px; text-transform: uppercase;">` + json_content[index]["media_url"] + `</span><br /><span style="color:var(--feedbot-title); font-weight:bold;">` + json_content[index]["title"] + `</span></a> </div> </div>`; } if(!json_content[index]["youtube_id"] && !json_content[index]["peertube_id"]){ constructFeed += ` <a href="` + json_content[index]["url"] + `" target="_blank" style="display:block; width:100%; height:100%;"></a> </div> <div class="timeline-title"> <a href="` + json_content[index]["url"] + `" target="_blank" title="` + json_content[index]["title"] + `" style="text-decoration: none;"><span style="color:var(--feedbot-gray); font-size: 12px; text-transform: uppercase;">` + json_content[index]["media_url"] + `</span><br /><span style="color:var(--feedbot-title); font-weight:bold; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;">` + json_content[index]["title"] + `</span></a> </div> </div>`; } constructFeed += ` <div style="position:relative; display: block; height: 28px;"> <div class="timeline-buttons-left">`; if(json_content[index]["is_shared"] !== "1"){ constructFeed += ` <form class="timeline-buttons share_` + json_content[index]["article_id"] + `"> <input type="hidden" name="article_id" value="` + json_content[index]["article_id"] + `"> <input type="hidden" name="feed_id" value="` + json_content[index]["feed_id"] + `"> <input type="hidden" name="site_id" value="` + json_content[index]["site_id"] + `"> <input type="hidden" name="messagetitle" value="` + json_content[index]["title"] + `"> <input type="hidden" name="message" value="` + json_content[index]["description_link"] + `"> <input type="hidden" name="url" value="` + json_content[index]["url"] + `"> <input type="hidden" name="peertubeid" value="` + peertube_id + `"> <input type="hidden" name="youtubeid" value="` + youtube_id + `"> <input type="hidden" name="thumbnail" value="` + json_content[index]["thumbnail"] + `"> <button onclick="share(` + json_content[index]["article_id"] + `)" class="timeline-buttons" title="SHARE"> <i class="fa fa-retweet" aria-hidden="true" style="margin-right: 5px;"></i> SHARE</span> </button> </form> <form class="timeline-buttons unshare_` + json_content[index]["article_id"] + `" style="display: none;"> <input type="hidden" name="action" value="delete_status"> <input type="hidden" name="article_id" value="` + json_content[index]["article_id"] + `"> <input type="hidden" name="status_id" value=""> <button onclick="unshare(` + json_content[index]["article_id"] + `)" type="submit" class="timeline-buttons" title="SHARED : ` + json_content[index]["relative_date"] + `" style="color:var(--feedbot-purple);"> <i class="fa fa-retweet" aria-hidden="true" style="margin-right: 5px;"></i> SHARED</span> </button> </form>`; } else{ constructFeed += ` <form class="timeline-buttons share_` + json_content[index]["article_id"] + `" style="display: none;"> <input type="hidden" name="article_id" value="` + json_content[index]["article_id"] + `"> <input type="hidden" name="feed_id" value="` + json_content[index]["feed_id"] + `"> <input type="hidden" name="site_id" value="` + json_content[index]["site_id"] + `"> <input type="hidden" name="messagetitle" value="` + json_content[index]["title"] + `"> <input type="hidden" name="message" value="` + json_content[index]["description_link"] + `"> <input type="hidden" name="url" value="` + json_content[index]["url"] + `"> <input type="hidden" name="peertubeid" value="` + peertube_id + `"> <input type="hidden" name="youtubeid" value="` + youtube_id + `"> <input type="hidden" name="thumbnail" value="` + json_content[index]["thumbnail"] + `"> <button onclick="share(` + json_content[index]["article_id"] + `)" class="timeline-buttons" title="SHARE"> <i class="fa fa-retweet" aria-hidden="true" style="margin-right: 5px;"></i> SHARE</span> </button> </form> <form class="timeline-buttons unshare_` + json_content[index]["article_id"] + `"> <input type="hidden" name="action" value="delete_status"> <input type="hidden" name="article_id" value="` + json_content[index]["article_id"] + `"> <input type="hidden" name="status_id" value="` + json_content[index]["post_id"] + `"> <button onclick="unshare(` + json_content[index]["article_id"] + `)" type="submit" class="timeline-buttons" title="SHARED : ` + json_content[index]["relative_date"] + `" style="color:var(--feedbot-purple);"> <i class="fa fa-retweet" aria-hidden="true" style="margin-right: 5px;"></i> SHARED</span> </button> </form>`; } constructFeed += ` </div> <div class="timeline-buttons-right">`; if(json_content[index]["bookmarked"] !== "1"){ constructFeed += ` <form class="timeline-buttons bookmark_` + json_content[index]["article_id"] + `" class="timeline-buttons-div"> <input type="hidden" name="action" value="bookmark"> <input type="hidden" name="article_id" value="` + json_content[index]["article_id"] + `"> <input type="hidden" name="feed_id" value="` + json_content[index]["feed_id"] + `"> <input type="hidden" name="site_id" value="` + json_content[index]["site_id"] + `"> <button onclick="bookmark(` + json_content[index]["article_id"] + `)" class="timeline-buttons" title="READ_LATER"> <i class="fa fa-bookmark" aria-hidden="true" style="margin-right: 5px;"></i> READ_LATER </button> </form> <form class="timeline-buttons unbookmark_` + json_content[index]["article_id"] + `" style="display:none;" class="timeline-buttons-div"> <input type="hidden" name="action" value="delete_bookmark"> <input type="hidden" name="article_id" value="` + json_content[index]["article_id"] + `"> <button onclick="unbookmark(` + json_content[index]["article_id"] + `)" class="timeline-buttons" title="READ_LATER" style="color:red;"> <i class="fa fa-bookmark" aria-hidden="true" style="margin-right: 5px;"></i> READ_LATER </button> </form>`; } else{ constructFeed += ` <form class="timeline-buttons bookmark_` + json_content[index]["article_id"] + `" style="display:none;" class="timeline-buttons-div"> <input type="hidden" name="action" value="bookmark"> <input type="hidden" name="article_id" value="` + json_content[index]["article_id"] + `"> <input type="hidden" name="feed_id" value="` + json_content[index]["feed_id"] + `"> <input type="hidden" name="site_id" value="` + json_content[index]["site_id"] + `"> <button onclick="bookmark(` + json_content[index]["article_id"] + `)" class="timeline-buttons" title="READ_LATER"> <i class="fa fa-bookmark" aria-hidden="true" style="margin-right: 5px;"></i> READ_LATER </button> </form> <form class="timeline-buttons unbookmark_` + json_content[index]["article_id"] + `" method="post" class="timeline-buttons-div"> <input type="hidden" name="action" value="delete_bookmark"> <input type="hidden" name="article_id" value="` + json_content[index]["article_id"] + `"> <button onclick="unbookmark(` + json_content[index]["article_id"] + `)" class="timeline-buttons" title="UNBOOKMARKS" style="color:red;"> <i class="fa fa-bookmark" aria-hidden="true" style="margin-right: 5px;"></i> READ_LATER </button> </form>`; } constructFeed += ` </div> </div> </div>`; } }); constructFeed = constructFeed.replace("undefined", ''); if(getJSON){ $.ajax({ url: "https://feedbot.net" + "/includes/infinite.php?last_id=" + json_content[num_content]["date"] + "&feed=" + feed_id + "&page=" + page + "&search=" + search, type: "GET", dataType: 'json', beforeSend: function(){ $("#post-data").append(` <div class="wait" style="display: none; align-items: center; justify-content: center; padding: 20px;"> <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"> <circle cx="12" cy="3" r="0" fill="currentColor"> <animate id="svgSpinners6DotsScaleMiddle0" attributeName="r" begin="0;svgSpinners6DotsScaleMiddle2.end-0.5s" calcMode="spline" dur="0.6s" keySplines=".27,.42,.37,.99;.53,0,.61,.73" values="0;2;0"/> </circle> <circle cx="16.5" cy="4.21" r="0" fill="currentColor"> <animate id="svgSpinners6DotsScaleMiddle1" attributeName="r" begin="svgSpinners6DotsScaleMiddle0.begin+0.1s" calcMode="spline" dur="0.6s" keySplines=".27,.42,.37,.99;.53,0,.61,.73" values="0;2;0"/> </circle> <circle cx="7.5" cy="4.21" r="0" fill="currentColor"> <animate id="svgSpinners6DotsScaleMiddle2" attributeName="r" begin="svgSpinners6DotsScaleMiddle4.begin+0.1s" calcMode="spline" dur="0.6s" keySplines=".27,.42,.37,.99;.53,0,.61,.73" values="0;2;0"/> </circle> <circle cx="19.79" cy="7.5" r="0" fill="currentColor"> <animate id="svgSpinners6DotsScaleMiddle3" attributeName="r" begin="svgSpinners6DotsScaleMiddle1.begin+0.1s" calcMode="spline" dur="0.6s" keySplines=".27,.42,.37,.99;.53,0,.61,.73" values="0;2;0"/> </circle> <circle cx="4.21" cy="7.5" r="0" fill="currentColor"> <animate id="svgSpinners6DotsScaleMiddle4" attributeName="r" begin="svgSpinners6DotsScaleMiddle6.begin+0.1s" calcMode="spline" dur="0.6s" keySplines=".27,.42,.37,.99;.53,0,.61,.73" values="0;2;0"/> </circle> <circle cx="21" cy="12" r="0" fill="currentColor"> <animate id="svgSpinners6DotsScaleMiddle5" attributeName="r" begin="svgSpinners6DotsScaleMiddle3.begin+0.1s" calcMode="spline" dur="0.6s" keySplines=".27,.42,.37,.99;.53,0,.61,.73" values="0;2;0"/> </circle> <circle cx="3" cy="12" r="0" fill="currentColor"> <animate id="svgSpinners6DotsScaleMiddle6" attributeName="r" begin="svgSpinners6DotsScaleMiddle8.begin+0.1s" calcMode="spline" dur="0.6s" keySplines=".27,.42,.37,.99;.53,0,.61,.73" values="0;2;0"/> </circle> <circle cx="19.79" cy="16.5" r="0" fill="currentColor"> <animate id="svgSpinners6DotsScaleMiddle7" attributeName="r" begin="svgSpinners6DotsScaleMiddle5.begin+0.1s" calcMode="spline" dur="0.6s" keySplines=".27,.42,.37,.99;.53,0,.61,.73" values="0;2;0"/> </circle> <circle cx="4.21" cy="16.5" r="0" fill="currentColor"> <animate id="svgSpinners6DotsScaleMiddle8" attributeName="r" begin="svgSpinners6DotsScaleMiddlea.begin+0.1s" calcMode="spline" dur="0.6s" keySplines=".27,.42,.37,.99;.53,0,.61,.73" values="0;2;0"/> </circle> <circle cx="16.5" cy="19.79" r="0" fill="currentColor"> <animate id="svgSpinners6DotsScaleMiddle9" attributeName="r" begin="svgSpinners6DotsScaleMiddle7.begin+0.1s" calcMode="spline" dur="0.6s" keySplines=".27,.42,.37,.99;.53,0,.61,.73" values="0;2;0"/> </circle> <circle cx="7.5" cy="19.79" r="0" fill="currentColor"> <animate id="svgSpinners6DotsScaleMiddlea" attributeName="r" begin="svgSpinners6DotsScaleMiddleb.begin+0.1s" calcMode="spline" dur="0.6s" keySplines=".27,.42,.37,.99;.53,0,.61,.73" values="0;2;0"/> </circle> <circle cx="12" cy="21" r="0" fill="currentColor"> <animate id="svgSpinners6DotsScaleMiddleb" attributeName="r" begin="svgSpinners6DotsScaleMiddle9.begin+0.1s" calcMode="spline" dur="0.6s" keySplines=".27,.42,.37,.99;.53,0,.61,.73" values="0;2;0"/> </circle> </svg> </div>`); }, success: function(response) { if(response){ json_content = response["content"]; json_feeds = response["feeds"]; } else{ json_content = false; } $(".wait").remove(); isActive = false; resolve(constructFeed); }, }) } else{ resolve(constructFeed); } }); } function get_sticky_position($elem){ event_scroll_position = $(window).scrollTop(); window_width = $(window).width(); var window_height = $(window).height(); var elem_height = $elem.outerHeight(true); if(window_width > 1010){ if(elem_height > window_height){ if(event_scroll_position >= scroll_position){ max_sticky_position = $(window).height() - elem_height - 18; var scroll_difference = scroll_position - event_scroll_position; sticky_position = sticky_position + scroll_difference; if(sticky_position <= max_sticky_position){ sticky_position = max_sticky_position; } $elem.css("top", sticky_position); } else{ var scroll_difference = scroll_position - event_scroll_position; sticky_position = sticky_position + scroll_difference; if(sticky_position > 18){ sticky_position = 18; } $elem.css("top", sticky_position); } } else{ sticky_position = 18; $elem.css("top", sticky_position); } } else{ $elem.css("top", "unset"); } scroll_position = $(window).scrollTop(); } function relativedate(ts){ // Vérifie si le timestamp est un entier, sinon essaie de le convertir if (!/^\d+$/.test(ts)) { ts = Date.parse(ts) / 1000; // Convertit en secondes } // Si ts est invalide après conversion if (isNaN(ts)) return "Invalid date"; let diff = Math.floor(Date.now() / 1000) - ts; // Différence en secondes if (diff === 0) { return "JUST_NOW"; } else if (diff > 0) { let day_diff = Math.floor(diff / 86400); if (day_diff === 0) { if (diff < 60) return "JUST_NOW"; if (diff < 120) return "ONE_MINUTE_AGO"; if (diff < 3600) return `THERE_IS ${Math.floor(diff / 60)} MINUTESAGO`; if (diff < 7200) return "ONE_HOUR_AGO"; if (diff < 86400) return `THERE_IS ${Math.floor(diff / 3600)} HOURSAGO`; } if (day_diff === 1) return "YESTERDAY"; if (day_diff < 7) return `THERE_IS ${day_diff} DAYSAGO`; if (day_diff < 31) return `THERE_IS ${Math.ceil(day_diff / 7)} WEEKSAGO`; if (day_diff < 60) return "LAST_MONTH"; return new Date(ts * 1000).toLocaleString(); } else { diff = Math.abs(diff); // Différence positive pour le futur let day_diff = Math.floor(diff / 86400); if (day_diff === 0) { if (diff < 120) return "In a minute"; if (diff < 3600) return `In ${Math.floor(diff / 60)} minutes`; if (diff < 7200) return "In an hour"; if (diff < 86400) return `In ${Math.floor(diff / 3600)} hours`; } if (day_diff === 1) return "Tomorrow"; if (day_diff < 4) return new Date(ts * 1000).toLocaleDateString("en-US", { weekday: 'long' }); if (day_diff < 7 + (7 - new Date().getDay())) return "Next week"; if (Math.ceil(day_diff / 7) < 4) return `In ${Math.ceil(day_diff / 7)} weeks`; if (new Date(ts * 1000).getMonth() === new Date().getMonth() + 1) return "Next month"; return new Date(ts * 1000).toLocaleString("en-US", { month: 'long', year: 'numeric' }); } } function refresh_dates(){ $("#post-data").find('.relative_date').each(function(){ var timestamp = $(this).attr("data-timestamp"); $(this).html(relativedate(timestamp)); }); } function checkNewContent(){ $.ajax({ url: "https://feedbot.net/includes/action.php", type: "POST", data: { action: "check_new_content", from: page, first_id: first_id }, success: function(response){ if(response == "yes"){ $("#new_content").css("display", "flex"); refresh_content = false; } } }); refresh_dates(); } function getNewContent(){ $.ajax({ url: "https://feedbot.net/includes/action.php", type: "POST", data: { action: "get_new_content", from: page, first_id: first_id }, dataType: "json", success: function(response){ var temp_json_content = json_content; var temp_json_feeds = json_feeds; if(response){ json_content = response.content; json_feeds = response.feeds; if(json_content){ constructFeed(json_content, getJSON = false).then((construct) => { var construct = construct; json_content = temp_json_content; json_feeds = temp_json_feeds; $(construct).insertAfter($("#new_content")); $("#post-data").find('.description:not([data-loaded]').each(function(){ linesCount(this); $(this).attr('data-loaded', 'true'); }); topFunction(); first_id = $(".content-home:first").attr("id"); $("#new_content").hide(); refresh_content = true; }); } } } }); } </script> <script type="text/javascript"> var window_width = $(window).width(); var scroll_position = $(window).scrollTop(); var sticky_position = 18; var search; var json_content; var json_feeds; console.log(json_content); </script> </head> <style type="text/css"> .lists_list{ max-height: 300px; display: flex; flex-direction: column; overflow: scroll; scrollbar-width: thin; } .lists_list_item{ display: flex; align-items: center; grid-gap: 10px; } .lists_popup{ position: fixed; display: none; align-items: center; justify-content: center; height: 100dvh; z-index: 3; background-color: #0F0F0FEE; backdrop-filter: blur(8px); } .lists_popup_list{ padding: 10px; width: 90%; max-width: 500px; display: flex; flex-direction: column; grid-gap: 15px; background-color: var(--feedbot-content-background); border-radius: 12px; } .create_list{ background-color: var(--feedbot-background); } .create_list, .lists_list_item{ cursor: pointer; padding: 10px; border-radius: 8px; } .create_list:hover, .lists_list_item:hover{ background-color: var(--feedbot-thumbnails-background); } .create_list_button{ display: flex; align-items: center; justify-content: center; grid-gap: 10px; } .create_list_form{ display: none; align-items: center; grid-gap: 10px; } </style> <body ontouchstart=""> <!-- Sidebar source https://www.codinglabweb.com/2021/04/responsive-side-navigation-bar-in-html.html --> <div class="sidebar "> <div class="logo-details" style="cursor: pointer;"> <img src="https://feedbot.net/assets/feedbot-logo.png" class="icon" style="height:28px; margin:8px;" onclick="window.location='https://feedbot.net/'" title="Feedbot"/> <div class="logo_name" onclick="window.location='https://feedbot.net/'" title="Feedbot"> Feedbot </div> <i class="fa fa-bars" aria-hidden="true" id="btn"></i> </div> <ul class="nav-list"> <li> <a href="https://feedbot.net/global"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 15 15"> <path fill="currentColor" fill-rule="evenodd" d="M0 7.5a7.5 7.5 0 1 1 15 0a7.5 7.5 0 0 1-15 0Zm1 0a6.502 6.502 0 0 1 5.527-6.428L7 1.31v.382l-.724.362a.5.5 0 0 0-.053.863l1.5 1A.5.5 0 0 0 8 4h.5a.5.5 0 0 0 .5-.5V3h.5v.5a.5.5 0 0 0 .146.354l.354.353v.586L9.793 5h-.675l-1.894-.947a.5.5 0 0 0-.448 0l-.894.447H4.5a.5.5 0 0 0-.485.379l-.5 2a.5.5 0 0 0 .131.475l1.5 1.5a.5.5 0 0 0 .13.093L6 9.31v1.19a.5.5 0 0 0 .146.354l.354.353V12a.5.5 0 0 0 .053.224l.5 1a.5.5 0 0 0 .447.276h1a.5.5 0 0 0 .416-.223l1-1.5a.5.5 0 0 0 .031-.053l.5-1a.5.5 0 0 0 .053-.224v-.833L11.9 7.8a.5.5 0 0 0 .047-.524L11.81 7h.691a.5.5 0 0 0 .5-.5V6h.826A6.5 6.5 0 1 1 1 7.5Z" clip-rule="evenodd"/> </svg> <span class="links_name">GLOBAL_FEED</span> </a> <span class="tooltip">GLOBAL_FEED</span> </li> <li style="display:none;" class="dark_mode"> <a onclick="dark_mode()"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 15 15"> <path fill="currentColor" d="M7.707.003a.5.5 0 0 0-.375.846a6 6 0 0 1-5.569 10.024a.5.5 0 0 0-.519.765A7.5 7.5 0 1 0 7.707.003Z"/> </svg> <span class="links_name">Mode sombre</span> </a> <span class="tooltip" style="margin-top: 7px;">Mode sombre</span> </li> <li class="light_mode"> <a onclick="light_mode()"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 15 15"> <path fill="currentColor" d="M8 2V0H7v2h1Zm-4.793.498L1.5.792L.793 1.5L2.5 3.206l.707-.708Zm9.293.708L14.207 1.5L13.5.792l-1.707 1.706l.707.708Zm-5 .791a3.499 3.499 0 1 0 0 6.996a3.499 3.499 0 1 0 0-6.996ZM2 6.995H0v1h2v-1Zm13 0h-2v1h2v-1ZM1.5 14.199l1.707-1.707l-.707-.707l-1.707 1.706l.707.708Zm12.707-.708L12.5 11.785l-.707.707L13.5 14.2l.707-.708ZM8 14.99v-1.998H7v1.999h1Z"/> </svg> <span class="links_name">Mode clair</span> </a> <span class="tooltip" style="margin-top: 7px;">Mode clair</span> </li> <li class="profile"> <div class="profile-details"> <img src="https://feedbot.net/assets/avatar-nopreview.png" alt="profileImg" onclick="location.href='https://feedbot.net/?p=signin';" style="cursor: pointer;"> <div class="name_job"> <div class="name"> GUEST </div> <div class="job"> </div> </div> </div> <i class="fa fa-sign-in" aria-hidden="true" id="log_in" onclick="location.href='https://feedbot.net/?p=signin';" style="cursor: pointer;"></i> </li> </ul> </div> <script> $(document).on("click", "#btn", function(){ $(".sidebar").toggleClass("open"); menuBtnChange(); }); $(document).on("click", ".bx-search", function(){ $(".sidebar").toggleClass("open"); menuBtnChange(); }); function menuBtnChange(){ window_width = $(window).width(); var content_width = $(".home-section").width(); if($(".sidebar").hasClass("open")){ $("#btn").removeClass("fa-bars").addClass("fa-align-right"); document.cookie = "open=open; path=/; max-age=" + 30 * 24 * 60 * 60; if(window_width <= 720){ $(".home-section").css("width", content_width); $("body").css("overflow-x", "hidden"); } } else{ $("#btn").removeClass("fa-align-right").addClass("fa-bars"); document.cookie = "open=close; path=/; max-age=" + 30 * 24 * 60 * 60; $(".home-section").css("width", ""); $("body").css("overflow-x", ""); } } $(document).on("submit", "#feedbot_search", function(e){ e.preventDefault; e.stopImmediatePropagation; var search = encodeURIComponent($("#feedbot_search input[type=text]").val()); console.log("https://feedbot.net/search/" + search); document.location.href="https://feedbot.net/search/" + search; return false; }); </script> <div class="publish_popup zoomin" style="display: none;"> </div> <section class="home-section"> <div class="lists_popup"> <div class="lists_popup_list"> <div class="title" style="height: unset; padding-top: unset; padding: 15px; margin-bottom: unset; font-size: 24px;"> <div style="display: flex; align-items: center; justify-content: center; grid-gap: 10px;"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" style="min-width: 24px; min-height: 24px;" viewBox="0 0 24 24"> <path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M18 18h2m2 0h-2m0 0v-2m0 2v2M2 11h18M2 17h12M2 5h18"/> </svg> Ajouter à une liste </div> </div> <div class="lists_list"> </div> <div class="create_list"> <div class="create_list_button" tabindex="0"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 15 15"> <path fill="currentColor" fill-rule="evenodd" d="M7 7V1h1v6h6v1H8v6H7V8H1V7h6Z" clip-rule="evenodd"/> </svg> Créer une liste </div> <div class="create_list_form" tabindex="0"> <input id="new_list_title" type="text" placeholder="Titre de la liste..." style="margin: unset; border-radius: 6px;"> <button style="padding: 10px 14px; margin-top: unset;"> Créer </button> </div> <input type="hidden" name="list_article"> </div> </div> </div> <script type="text/javascript"> $(document).on("click", function(e){ if (!$(e.target).closest(".lists_popup_list").length){ close_lists_popup(); } }); $(document).on("keydown", function(e){ if(e.key === "Escape" || e.keyCode === 27) { close_lists_popup(); } }); $(document).on("click", ".create_list_button", function(){ $(".create_list_button").hide(); $(".create_list_form").css("display", "flex"); $("#new_list_title").focus(); }); $(document).on("click", ".lists_list_item", function(e){ var list_id = $(this).attr("data-list-id"); var isChecked = $(".lists_list_item input[data-list-id='" + list_id + "']").prop("checked"); if(isChecked){ $(".lists_list_item input[data-list-id='" + list_id + "']").prop("checked", false).trigger("change"); } else{ $(".lists_list_item input[data-list-id='" + list_id + "']").prop("checked", true).trigger("change"); } }); $(document).on("click", ".create_list_form button", function(){ var list_title = $("#new_list_title").val(); create_playlist(list_title); }); $(document).on("keydown", "#new_list_title", function(e){ if(e.key === "Enter" || e.keyCode === 13){ var list_title = $("#new_list_title").val(); create_playlist(list_title); } }); $(document).on("change", "input[data-list-id]", function(){ if($(this).attr("data-update") == "false"){ return; } var article_id = $("input[name='list_article']").val(); var list_id = $(this).attr("data-list-id"); var isChecked = $(this).prop("checked"); $.ajax({ url: "https://feedbot.net/includes/action.php", method: "POST", data: { action: "add_to_list", article_id: article_id, list_id: list_id, checked: isChecked ? 1 : 0 }, success: function(response){ if(response == "success"){ console.log(list_id); var list_item = $(".lists_list_item[data-list-id='" + list_id + "']").detach(); $(".lists_list").prepend(list_item); } } }); }); function close_lists_popup(){ $("body").css("overflow-y", ""); $(".lists_popup").hide(); $("#new_list_title").val(""); $("input[name='list_article']").val(""); $(".create_list_form").hide(); $(".create_list_button").css("display", "flex"); $(".lists_list_item input[type='checkbox']").each(function() { $(this).prop("checked", false); $(this).attr("data-update", "false"); }); } function create_playlist(title){ var article_id = $("input[name='list_article']").val(); $.ajax({ url: "https://feedbot.net/includes/action.php", type: "POST", data: { action: "create_list", title: title }, dataType: "json", success: function(response){ if(response.response == "success"){ var list_id = response.id; var list_title = response.title; $.ajax({ url: "https://feedbot.net/includes/action.php", method: "POST", data: { action: "add_to_list", article_id: article_id, list_id: list_id, checked: 1 }, success: function(response){ $(".lists_list").prepend(` <div class="lists_list_item" tabindex="0" data-list-id="` + list_id + `"> <div> <input type="checkbox" data-list-id="` + list_id + `" data-update="true" checked> </div> <div> ` + list_title + ` </div> </div>`); $(".create_list_form").hide(); $("#new_list_title").val(""); $(".create_list_button").css("display", "flex"); } }); } } }); } </script> <button onclick="topFunction();" id="myBtn" title="Go to top"> <i class="fa fa-arrow-up" aria-hidden="true"></i> </button> <div class="banner-contenair"> <div class="single-feed-banner" style="background-image: url(https://feedbot.net/storage/thumbnails/2023_12/1b9392230da6166d97e929c0d06e3cd4-2bb58e10e10418b365be670dfe51b0f8.jpg);"> </div> <div class="single-feed-avatar" style="background-image: url(https://feedbot.net/storage/icons/505d4634449bab300618b607328ae6c0aac872de.png);"> </div> <div class="single-feed-title"> <p style="font-size:20px; font-weight:bold; overflow:hidden; white-space:nowrap; text-overflow:ellipsis;">Alsacreations.com - Actualités </p> <span style="font-size: 16px;">www.alsacreations.com</span> </div> <div class="single-feed-subscribe"> <form action="./?p=signin" method="post"> <button type="submit" title="SUBSCRIBE_TO Alsacreations.com - Actualités"/> <span><i class="fa fa-rss" aria-hidden="true" style="margin-right: 5px;"></i> SUBSCRIBE</span> </button> </form> </div> </div> <div class="contenair-home"> <div id="post-data"> <div class="content-home" style="padding-top: 15px; padding-bottom: 0px;" id="1702989427"> <div style="height:75px;"> <form action="https://feedbot.net" class="follow-button subscribe_376" method="post" title="SUBSCRIBE_TO Alsacreations.com - Actualités"> <button type="submit"><i class="fa fa-rss" aria-hidden="true"></i></button> </form> <div style="width: 60px; aspect-ratio: 1 / 1; background-image: url(https://feedbot.net/storage/icons/505d4634449bab300618b607328ae6c0aac872de.png); background-size: cover; background-position: center; border-radius: 50%; float:left; overflow: hidden;"> <a href="https://feedbot.net/feed/376" style="display:block; width:100%; height:100%;"></a> </div> <div style="margin-left: 74px; padding-top: 13px; line-height: 16px;"> <a href="https://feedbot.net/feed/376" style="color:var(--feedbot-title); font-weight: bold; text-decoration: none; display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical; overflow: hidden;">Alsacreations.com - Actualités</a> <a href="https://feedbot.net/feed/376/371959" style="font-size: 12px; text-decoration: none; color: var(--feedbot-text);">19/12/2023 01:37</a> </div> </div> <div class="description" data-article-id="371959" id="description-371959">Le format SVG peut paraître parfois un peu intimidant, et l’associer à des transitions ou des animations CSS semble encore plus audacieux pour bon nombre de personnes. Cependant, dans certains cas, l’alchimie entre SVG et CSS est aussi bénéfique qu’extrêmement simple à mettre en oeuvre. Dans ce tutoriel, nous allons suivre étape par étape comment animer un bouton burger simple avec SVG et CSS. Quels outils ? La liste des outils nécessaires pour atteindre nos objectifs est particulièrement réduite puisqu’un simple éditeur de code fait le job (n’importe lequel fait l’affaire, Visual Studio Code étant mon choix personnel). Pour aller plus loin, et en guise de bonus, on peut également piocher :<br />Un éditeur SVG en ligne (parce que ça peut toujours servir) Des recommendations concernant l’accessibilité des SVG (au hasard les Guidelines Alsacréations) Un éditeur de courbes de Bezier (pour des animations originales)<br />SVG c’est quoi ? En trois mots, voici comment résumer SVG :<br />SVG est un format graphique vectoriel (composé de tracés et de courbes) Il est développé et maintenu depuis 1999 par le W3C (standard officiel, open source) Il est conçu en XML (compatible HTML) (on peut le créer et le lire avec un simple éditeur de texte)<br />1. Produire le burger bouton en SVG Si l’on y regarde de plus près, une "icône Burger" c’est bêtement trois rectangles horizontaux espacés et avec des coins arrondis. Notre éditeur de code préféré est amplement suffisant pour s’aquitter de la tâche de dessiner des rectangles : on va tout d’abord dessiner un élément SVG vide avec une fenêtre de "100 x 100". C’est une dimension purement indicative car tout est proportionnel et adaptable en SVG. <svg class="burger-icon" viewBox="0 0 100 100"> </svg><br />.burger-icon { width: 200px; height: 200px; /* taille du SVG */ border: 2px dotted #ddd; /* bordure = simple repère */ } Le tracé de notre premier rectangle est un jeu d’enfant aussi : l’élément SVG rect est fait pour ça, attribuons-lui des coordonnées (x=0 et y=0) ainsi qu’une largeur de "100" et une hauteur de "20". <svg class="burger-icon" viewBox="0 0 100 100"> <rect x="0" y="0" width="100" height="20" /> </svg> Vous aurez compris qu’à partir d’un premier rectangle, il n’est pas difficile de produire les deux suivants. Et voilà ! <svg class="burger-icon" viewBox="0 0 100 100"> <rect x="0" y="0" width="100" height="20" /> <rect x="0" y="40" width="100" height="20" /> <rect x="0" y="80" width="100" height="20" /> </svg> Pour ce qui est des coins arrondis, là aussi SVG a tout prévu sous la forme de l’attribut rx, à qui une valeur de "5" semble tout à fait parfaite. <svg class="burger-icon" viewBox="0 0 100 100"> <rect x="0" y="0" width="100" height="20" rx="5" /> <rect x="0" y="40" width="100" height="20" rx="5" /> <rect x="0" y="80" width="100" height="20" rx="5" /> </svg> Le résultat est bluffant et on se rend compte de la puissance insoupçonnée d’un éditeur de code. Plus sérieusement, ce n’était vraiment pas compliqué, non ? Par contre, ce qui est vraiment dommage c’est de répéter les mêmes choses plusieurs fois… Mais justement, il se trouve que… la plupart des attributs SVG existent également sous forme de propriétés CSS ! Voici par conséquent comment nous allons pouvoir améliorer notre code actuel : <svg class="burger-icon" viewBox="0 0 100 100"> <rect class="rect-1" /> <rect class="rect-2" /> <rect class="rect-3" /> </svg><br />rect { x: 0; rx: 5px; width: 100px; height: 20px; } .rect-1 { y: 0; } .rect-2 { y: 40px; } .rect-3 { y: 80px; }<br />Autre avantage loin d’être anodin, ces propriétés CSS-SVG ont la bonne idée d’être animables : on peut par exemple effectuer une transition sur la propriété… y ! rect { ... transition: y 1s; } .rect-1:hover { y: 15px; } 2. Préparer le SVG et le rendre accessible Nous allons à présent nous atteler à transformer notre icône SVG en un véritable "bouton Burger", fonctionnel et accessible. Pour ce faire, on commence par placer le SVG dans un <button> qui sera l’élément interactif au clic / touch et qui déclenchera l’animation. <button class="burger-button"> <svg class="burger-icon" viewBox="0 0 100 100"> <rect class="rect-1" /> <rect class="rect-2" /> <rect class="rect-3" /> </svg> </button><br />Notre icône SVG est considérée comme purement décorative, car c’est le bouton qui portera l’information. Nous veillons à lui appliquer les attributs suivants :<br />Un attribut aria-hidden="true" Un attribut focusable="false" pour éviter de naviguer au sein du SVG. Aucun élément <title> ni <desc> ni d’attribut title, aria-label, aria-labelledby, ni role="img"<br />... <svg class="burger-icon" aria-hidden="true" focusable="false" viewBox="0 0 100 100"> </svg> ...<br />Le bouton, quant à lui, nécessite les éléments suivants :<br />Un nom accessible (via aria-label ou un texte masqué à la ".sr-only") En option, et selon les cas de figure, un attribut aria-controls pour lier à la cible et un attribut aria-expanded pour signaler l’état du bouton. Dans notre cas, ce n’est pas néessaire.<br /><button class="burger-button" aria-label="Menu" data-expanded="false"> ... </button><br />Voici le script JavaScript destiné à gérer l’interaction et la mise à jour des attributs data-, et déclencher l’animation de l’icône : (function () { function toggleNav() { // Define targets const button = document.querySelector(’.burger-button’); const target = document.querySelector(’#navigation’); button.addEventListener(’click’, () => { const currentState = target.getAttribute("data-state"); if (!currentState || currentState === "closed") { target.setAttribute("data-state", "opened"); button.setAttribute("data-expanded", "true"); } else { target.setAttribute("data-state", "closed"); button.setAttribute("data-expanded", "false"); } }); } // end toggleNav() toggleNav(); }()); Pouquoi JavaScript ? Très sincèrement parce que c’est son job de déclencher des actions au clic et de modifier des classes ou des attibuts en conséquence. Cette mission aurait été réalisable en CSS au moyen de cases à cocher mais, ne nous mentons pas, c’est un peu de la bidouille.<br />3. Les étapes de l’animation Pour être très précis, nous n’allons pas employer une "animation" pour nos effets, mais une combinaison de trois "transitions", qui se révèleront amplement suffisantes pour notre besoin.<br />Voici le scénario étape par étape qui doit se réaliser :<br />L’action de clic ou de touch sur l’élément button doit déclencher une série de trois transitions; La transition 1 consiste en un déplacement vertical de .rect-1 et .rect-3 qui se rejoignent au centre du SVG; La transition 2 consiste à faire disparaître .rect-2 qui traîne dans nos pattes. En terme de timing, cette transition doit se dérouler en même temps que la transition 1; La transition 3 se compose d’une rotation de 45 degrés de .rect-1 et .rect-3 et doit de déclencher juste après les transitions précédentes).<br />Transition 1 et 2 : "translate" et "opacity" La propriété transitionest appliquée sur l’élément à l’état initial (hors événement) afin d’assurer une transition au retour lorsque l’événement est quitté. /* transition sur la propriété y et opacity, durée 0.3s */ rect { transition: y 0.3s, opacity 0.3s; } /* coordonnées y initiales */ .rect-1 { y: 0; } .rect-2 { y: 40px; } .rect-3 { y: 80px; }<br />Au clic, le bouton passe en data-expanded "true" et on déplace verticalement deux rectangles au centre et on masque le 3e rectangle central. [data-expanded="true"] .rect-1 { y: 40px; } [data-expanded="true"] .rect-2 { opacity: 0; } [data-expanded="true"] .rect-3 { y: 40px; }<br />Transition 3 : "rotate" Aux deux transitions précédentes, on ajoute une transition sur la propriété rotate sans oublier de la faire débuter après un léger délai. /* on attend un delai de 0.3s avant de commencer rotate */ rect { transition: y 0.3s, opacity 0.3s, rotate 0.3s 0.3s; }<br />Au clic, les trois transitions se déclenchent. [data-expanded="true"] .rect-1 { y: 40px; rotate: 45deg; } [data-expanded="true"] .rect-2 { opacity: 0; } [data-expanded="true"] .rect-3 { y: 40px; rotate: -45deg; } ⚠️ J’imagine que cela ne vous a pas échappé : tout se passe très bien à l’aller, mais malheureusement pas au retour. L’explication provient du fait que la transition se déroule dans le sens inverse au retour et que la rotation se déclenche trop tôt. Il va nous falloir une transition différente à l’aller et au retour et gérer des délais différents entre la transition et la rotation. /* transition au retour (quand on perd le clic) */ /* on attend un delai de 0.3s avant de commencer y */ rect { transition: y 0.3s 0.3s, opacity 0.3s, rotate 0.3s; }<br />/* transition à l’aller (quand on clique) */ /* on attend un delai de 0.3s avant de commencer rotate */ [data-expanded="true"] rect { transition: y 0.3s, opacity 0.3s, rotate 0.3s 0.3s; } Grâce à cette adaptation subtile, notre effet fonctionne parfaitement à l’aller et au retour lors de l’interaction. Pour finir en beauté, le truc en plus consiste en une petite accélération sous forme de cubic-bezier pour un effet de "rebond". [data-expanded="true"] rect { transition: y 0.3s, opacity 0.3s, rotate 0.3s 0.3s cubic-bezier(.55,-0.65,0,2.32); } CSS final Voici les styles CSS complets de ce tutoriel. Notez qu’ils prennent en compte les préférences utilisateur grâce au media query prefers-reduced-motion : si la personne a choisi dans ses réglages système de réduire les animations, celles-ci ne seront tout simplement pas déclenchées. Pour voir le résultat et aller plus loin, une petite collection CodePen de boutons burger animés a été rassemblée à cette adresse : https://codepen.io/collection/VYqwJK .rect-1 { y: 0; } .rect-2 { y: 40px; } .rect-3 { y: 80px; } [data-expanded="true"] .rect-1 { y: 40px; rotate: 45deg; } [data-expanded="true"] .rect-2 { opacity: 0; } [data-expanded="true"] .rect-3 { y: 40px; rotate: -45deg; } /* transitions si acceptées */ @media (prefers-reduced-motion: no-preference) { rect { transition: y 0.3s 0.3s, opacity 0.3s, rotate 0.3s; } [data-expanded="true"] rect { transition: y 0.3s, opacity 0.3s, rotate 0.3s 0.3s cubic-bezier(.55,-0.65,0,2.32); } } Retrouvez l’intégralité de ce tutoriel en ligne sur Alsacreations.com</div> <div style="height:20px;"> </div> <div class="timeline-thumbnail" title="Animer un bouton burger simple avec SVG et CSS" > <div class="timeline-thumbnail-content" style="background-image: url(https://feedbot.net/storage/thumbnails/2023_12/1b9392230da6166d97e929c0d06e3cd4-2bb58e10e10418b365be670dfe51b0f8.jpg);"> <a href="https://www.alsacreations.com/tuto/lire/1921-Animer-un-bouton-burger-simple-avec-SVG-et-CSS.html" target="_blank" style="display:block; width:100%; height:100%;"></a> </div> <div class="timeline-title"> <a href="https://www.alsacreations.com/tuto/lire/1921-Animer-un-bouton-burger-simple-avec-SVG-et-CSS.html" target="_blank" style="text-decoration: none;"><span style="color:var(--feedbot-gray); font-size: 12px; text-transform: uppercase;">www.alsacreations.com</span><br /><span style="color: var(--feedbot-title); font-weight: bold; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;">Animer un bouton burger simple avec SVG et CSS</span></a> </div> </div> <div style="position:relative; display: block; height: 28px;"> <div class="timeline-buttons-left"> <form class="timeline-buttons share_371959"> <input type="hidden" name="article_id" value="371959"> <input type="hidden" name="feed_id" value="376"> <input type="hidden" name="site_id" value="172"> <input type="hidden" name="messagetitle" value="Animer un bouton burger simple avec SVG et CSS"> <input type="hidden" name="message" value="« Le format SVG peut paraître parfois un peu intimidant, et l’associer à des transitions ou des animations CSS semble encore plus audacieux pour bon nombre de personnes.Cependant, dans certains cas, l’alchimie entre SVG et CSS est aussi bénéfique qu’extrêmement simple à mettre en oeuvre. Dans ce tutoriel, nous allons suivre étape par étape comment animer un bouton burger simple avec SVG et CSS. Quels outils ?La liste des outils… »"> <input type="hidden" name="url" value="https://www.alsacreations.com/tuto/lire/1921-Animer-un-bouton-burger-simple-avec-SVG-et-CSS.html"> <input type="hidden" name="peertubeid" value=""> <input type="hidden" name="youtubeid" value=""> <input type="hidden" name="thumbnail" value="https://feedbot.net/storage/thumbnails/2023_12/1b9392230da6166d97e929c0d06e3cd4-2bb58e10e10418b365be670dfe51b0f8.jpg"> <button onclick="share(371959)" class="timeline-buttons" title="SHARE"> <i class="fa fa-retweet" aria-hidden="true" style="margin-right: 5px;"></i> SHARE</span> </button> </form> <form class="timeline-buttons unshare_371959" style="display: none;"> <input type="hidden" name="action" value="delete_status"> <input type="hidden" name="article_id" value="371959"> <input type="hidden" name="status_id" value=""> <button onclick="unshare(371959)" type="submit" class="timeline-buttons" title="SHARED : 19/12/2023 01:37" style="color:var(--feedbot-purple);"> <i class="fa fa-retweet" aria-hidden="true" style="margin-right: 5px;"></i> SHARED</span> </button> </form> </div> <div class="timeline-buttons-right"> <form class="timeline-buttons bookmark_371959" class="timeline-buttons-div"> <input type="hidden" name="action" value="bookmark"> <input type="hidden" name="article_id" value="371959"> <input type="hidden" name="feed_id" value="376"> <input type="hidden" name="site_id" value="172"> <button onclick="bookmark(371959)" class="timeline-buttons" title="READ_LATER"> <i class="fa fa-bookmark" aria-hidden="true" style="margin-right: 5px;"></i> READ_LATER </button> </form> <form class="timeline-buttons unbookmark_371959" style="display:none;" class="timeline-buttons-div"> <input type="hidden" name="action" value="delete_bookmark"> <input type="hidden" name="article_id" value="371959"> <button onclick="unbookmark(371959)" class="timeline-buttons" title="READ_LATER" style="color:red;"> <i class="fa fa-bookmark" aria-hidden="true" style="margin-right: 5px;"></i> READ_LATER </button> </form> </div> </div></div> </div> <div class="widget-home"> <div class="widget-home-content small_screens_hide" style="margin-top: 20px; text-align: center;"> <h4 style="font-size:20px; margin-bottom: 20px; color:var(--feedbot-title);">ENJOY_USING Feedbot ?</h4> <p style="margin-bottom: 20px;">FUNDING</p> <script src="https://liberapay.com/raph/widgets/button.js"></script> <noscript><a href="https://liberapay.com/raph/donate"><img alt="Donate using Liberapay" src="https://liberapay.com/assets/widgets/donate.svg"></a></noscript> </div><div class="footer small_screens_hide">COPYRIGHT <a href="https://tooter.social/@raph" target="_blank" rel="me">Raph <i class="fa fa-mastodon" aria-hidden="true"></i></a><br /> Logo by <a href="https://maous.fr/" target="_blank">Maous <img src="https://feedbot.net/assets/maous.png" style="width:22px; margin-bottom:6px; vertical-align: middle;"/></a></div> </div> </div> <script> var isActive = false; var page = "single-feed"; var feed_id = "376"; var search = ""; $(window).scroll(function(){ get_sticky_position($(".widget-home")); }); $(window).on("resize", function(){ get_sticky_position($(".widget-home")); }); function linesCount($elem){ var article_id = $($elem).attr("data-article-id"); var lineHeight = parseFloat($($elem).css('line-height')); var elementHeight = $($elem)[0].scrollHeight; var lineCount = Math.floor(elementHeight / lineHeight); var read_more_elem = $(".read-more[data-article-id=" + article_id + "]").length; if(lineCount > 5){ $($elem).addClass("description-short"); if(read_more_elem == 0){ $($elem).addClass("description-short"); $(`<div class="read-more" data-article-id="` + article_id + `" style="width:100%; font-weight: bold; text-align: center; margin-top: 10px; cursor: pointer;">READ_MORE</div>`).insertAfter($elem); } } else{ $($elem).removeClass("description-short"); $(".read-more[data-article-id=" + article_id + "]").remove(); } } $(document).on("click", ".read-more", function(){ var article_id = $(this).attr("data-article-id"); $(".description[data-article-id=" + article_id + "]").removeClass("description-short"); $(".description[data-article-id=" + article_id + "]").attr("data-expanded", "true"); $(this).remove(); }); get_sticky_position($(".widget-home")); </script> </section> <script type="text/javascript"> // Mobile Safari in standalone mode if(("standalone" in window.navigator) && window.navigator.standalone){ // If you want to prevent remote links in standalone web apps opening Mobile Safari, change 'remotes' to true var noddy, remotes = false; document.addEventListener('click', function(event) { noddy = event.target; // Bubble up until we hit link or top HTML element. Warning: BODY element is not compulsory so better to stop on HTML while(noddy.nodeName !== "A" && noddy.nodeName !== "HTML"){ noddy = noddy.parentNode; } if('href' in noddy && noddy.href.indexOf('http') !== -1 && (noddy.href.indexOf(document.location.host) !== -1 || remotes)){ event.preventDefault(); document.location.href = noddy.href; } },false); } </script> <script type="text/javascript"> // Get the button: let mybutton = document.getElementById("myBtn"); // When the user scrolls down 20px from the top of the document, show the button window.onscroll = function() {scrollFunction()}; function scrollFunction() { if(document.body.scrollTop > 5000 || document.documentElement.scrollTop > 5000){ mybutton.style.display = "block"; } else{ mybutton.style.display = "none"; } } // When the user clicks on the button, scroll to the top of the document function topFunction(){ $('html, body').animate({ scrollTop: 0 }, 'medium'); } </script> </body> </html>