Open Time - Mot-clé - javascript<p>Open time, open mind, open eyes</p>2024-03-27T08:47:16+01:00Franck Paulurn:md5:61070eb8c883ae7581f861faefddecbfDotclearJ'ai du bouloturn:md5:c769807d26532c799fdbc8e9293f081e2024-03-16T07:34:00+01:002024-03-16T07:34:00+01:00FranckBrèvesdotcleardéveloppementjavascript <p>Après avoir obtenu quelques explications hier, merci Biou et Boris, j’ai finalement du code Javascript qui ne fait plus hurler LSP-Typescript :</p>
<pre><code class="language-javascript">/*global dotclear */
'use strict';
dotclear.ready(() => {
// DOM ready and content loaded
// Give focus to user field
/**
* @type {HTMLInputElement|null}
*/
const uid = document.querySelector('input[name=user_id]');
if (uid) uid.focus();
/**
* @type {HTMLElement|null}
*/
const ckh = document.getElementById('cookie_help');
if (ckh) ckh.style.display = navigator.cookieEnabled ? 'none' : '';
/**
* @type {HTMLInputElement|null}
*/
const upw = document.querySelector('input[name=user_pwd]');
if (!upw || !uid) {
return;
}
// Add an event listener to capture CR key press in user field to give to password field if it is empty
uid.addEventListener('keypress', (/** @type {UIEvent} */ event) => {
if (event.which == 13 && upw.value == '') {
// Password is empty, give focus to it
upw.focus();
// Stop handling of this event (CR keypress)
event.preventDefault();
}
});
});
</code></pre>
<p>Me reste uniquement à revoir l’usage du <code>wich</code> (ligne XXX) vu que c’est obsolète, mais dans l’ensemble c’est satisfaisant.</p>
<p>Accessoirement ça veut dire que j’ai un gros boulot pour reprendre et corriger les sources Javascript de Dotclear…</p>
https://open-time.net/post/2024/03/16/J-ai-du-boulot#comment-formhttps://open-time.net/feed/atom/comments/16021Seriously?urn:md5:3894ed268b5ad080ef8a11bb80cedf2b2024-03-15T06:18:00+01:002024-03-15T08:28:48+01:00FranckBrèvesdéveloppementjavascript <p><a href="https://open-time.net/public/screenshots/2024/auth-js-lsp-typescript.jpg" title="Ouvrir le média"><img src="https://open-time.net/public/screenshots/2024/.auth-js-lsp-typescript_w.jpg" alt="Copie d'écran du code javascript avec les indicateurs d'erreur fournis par LSP-Typescript" class="media-center" height="492" width="800"></a></p>
<p>LSP-Typescript, une fois activé, raconte un peu <em>portnawak</em>, je trouve, pas vous ?</p>
<pre><code class="language-javascript">/*global dotclear */
'use strict';
dotclear.ready(() => {
// DOM ready and content loaded
// Give focus to user field
const uid = document.querySelector('input[name=user_id]');
if (uid) uid.focus();
const ckh = document.getElementById('cookie_help');
if (ckh) ckh.style.display = navigator.cookieEnabled ? 'none' : '';
const upw = document.querySelector('input[name=user_pwd]');
if (!upw) {
return;
}
// Add an event listener to capture CR key press in user field to give to password field if it is empty
uid.addEventListener('keypress', (event) => {
if (event.which == 13 && upw.value == '') {
// Password is empty, give focus to it
upw.focus();
// Stop handling of this event (CR keypress)
event.preventDefault();
}
});
});
</code></pre>
<p>Ligne 9, il m’annonce que la méthode <code>focus()</code> n’est pas <a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus" hreflang="en">connue</a> pour le type <strong>Element</strong> (HTML) retourné par la méthode <code>document.querySelector()</code>, oui oui.<br />
Et puis aussi que la propriété <code>which</code> n’est pas non plus <a href="https://developer.mozilla.org/en-US/docs/Web/API/UIEvent/which" hreflang="en">connue</a> pour le type <strong>Event</strong> fourni en paramètre de la fonction de rappel de la méthode <code>addEventListener()</code>, ben voyons. Par contre il me dit pas que cette propriété est <mark><i lang="en">deprecated</i></mark> !?!</p>
<p>Je sais pas ce qu’ils ont fumé, mais c’est de la bonne !</p>
<p>Ou alors c’est moi qui me plante et j’aimerais très beaucoup qu’on m’explique pourquoi !</p>
<hr />
<p>Alors effectivement c’est moi qui me plante vu que LSP-Typescript infère les types des variables parfois pas tout à fait de la façon que je souhaite. Il faut donc que je documente les types de variable (<a href="https://jsdoc.app/" hreflang="en">JSDoc</a>) et que je corrige mon code le cas échéant.</p>
<p>PS : Je sais qu’il y a des trucs à compléter dans le code ci-dessus, c’est pas encore ceinture/bretelles, mais la question n’est pas là.</p>
https://open-time.net/post/2024/03/15/Seriously#comment-formhttps://open-time.net/feed/atom/comments/16020Parfum vanilleurn:md5:f585e08434bbb26b12532edfac7834742023-12-22T09:30:00+01:002023-12-23T10:22:50+01:00FranckBrèvesdotcleardéveloppementjavascript <figure class="media-center">
<a href="https://open-time.net/public/screenshots/2015/code-javascript.jpg" title="Ouvrir le média"><img src="https://open-time.net/public/screenshots/2015/.code-javascript_w.jpg" alt="Code Javascript" height="680" width="800"></a>
<figcaption>Code Javascript, sept. 2015</figcaption>
</figure>
<p>Je m’amuse en ce moment à convertir du vieux code qui utilise jQuery en javascript parfum <a href="https://fr.wikipedia.org/wiki/Logiciel_vanilla">Vanilla</a> et c’est intéressant de constater que ce que palliait à l’époque — il y a une dizaine d’années et plus — jQuery, est maintenant géré de façon standard par les navigateurs !</p>
<p>Comme quoi certaines normes ont du sens !</p>
https://open-time.net/post/2023/12/22/Parfum-vanille#comment-formhttps://open-time.net/feed/atom/comments/15936Dialog pas Popupurn:md5:a938eafbc9c3a333b6b6268141805f9d2023-12-18T06:10:00+01:002023-12-23T10:24:06+01:00FranckBrèvesdotcleardéveloppementjavascriptnavigateur <figure class="media-center">
<a href="https://open-time.net/public/screenshots/2023/ublock-popup-dc.jpg" title="Ouvrir le média"><img src="https://open-time.net/public/screenshots/2023/ublock-popup-dc.jpg" alt="µBlock origin bloque les popups, déc. 2023" height="293" width="667"></a>
<figcaption>
<p>Copie d’écran du détail du journal de µBlock origin qui bloque un script javascript dont le nom commence par <code>popup_</code></p>
</figcaption>
</figure>
<p>J’ai tourné en rond un moment avant de finir par mettre le doigt sur le truc qui bloquait le chargement d’un script dans une popup de Dotclear (popup gérée par un plugin). Même domaine, les directives Content-Security-Policies correctement positionnées, … Rien n’aurait du bloquer ça.</p>
<p>Sauf que parmi les filtres utilisés par l’extension µBlock origin, il y en a un qui est celui-ci :<br />
<code>.php?*/popup_$script</code><br />
et comme la requête de chargement de ce script est la suivante :<br />
<code>https://admin.chez.moi/index<mark>.php?pf=multipleMedia/js/popup_</mark>media_manager.js</code><br />
et qu’elle valide la règle du filtre en question, ça coinçait !</p>
<p>Petit changement de préfixe de <code>popup_</code> en <code>dialog_</code> (qui ne semble pas être dans les filtres utilisés) et tout est rentré dans l’ordre.</p>
<p>Bon à savoir si vous développez des trucs avec des scripts javascript ;-)</p>
https://open-time.net/post/2023/12/18/Dialog-pas-Popup#comment-formhttps://open-time.net/feed/atom/comments/15932Je sèche un peuurn:md5:706769874344003ce9413f99ea1146272023-12-11T07:35:00+01:002023-12-11T07:45:14+01:00FranckBrèvesdotcleardéveloppementjavascript <p><q>Ou comment ne pas arriver à documenter pour LSP du code javascript avec <a href="https://jsdoc.app/" hreflang="en">JS Doc</a></q></p>
<pre><code class="language-javascript">/* jQuery extensions
-------------------------------------------------------- */
/**
* @name jQuery
* @class
* @typedef {jQuery} $
* @external "jQuery"
*/
/**
* @name fn
* @class
* @memberOf jQuery
* @external "jQuery.fn"
*/
/**
* jQuery helper to check a list of elements
*
* @return {jQuery}
* @function
* @memberof external:"jQuery.fn"
*/
$.fn.check = function () {
return this.each(function () {
if (this.checked != undefined) {
this.checked = true;
}
});
};
</code></pre>
<p>Voilà où j’en suis en ce moment et toujours pas moyen de faire indexer par LSP-Typescript (qui tourne avec du code javascript ouvert dans Sublime Text) les fonctions ajoutées à jQuery.</p>
<p>J’ai testé un peu tout ce que j’ai trouvé sur le net, même le bizarre, mais non, dans le même fichier aucun souci, au survol d’un appel d’une de ces fonctions il m’affiche toute la doc, dans un fichier externe nada !</p>
<p>J’y ai passé quelques heures hier avant d’aller me coucher et ce matin toujours rien de mieux…</p>
<p>En fait c’est particulier à la forme d’appel des fonctions jQuery je pense, du genre :</p>
<pre><code class="language-javascript">$('#form-blogs td input[type=checkbox]').enableShiftClick();
</code></pre>
<p>Avec laquelle la fonction <code>enableShiftClick</code> reste inconnue au bataillon en dehors de son fichier source, alors qu’avec un appel du type :</p>
<pre><code class="language-javascript">$.expandContent(...);
</code></pre>
<p>La fonction <code>expandContent</code> est reconnue partout.</p>
<p>J’ai aussi testé en remplaçant <code>$</code> par <code>jQuery</code> sans plus de succès.</p>
<p>Reste à trouver comment contourner ça…</p>
<p>PS : On survivra si jamais je n’arrivais pas à mes fins, mais j’aimerais bien finir par trouver une solution.</p>
https://open-time.net/post/2023/12/11/Je-seche-un-peu#comment-formhttps://open-time.net/feed/atom/comments/15925Batterieurn:md5:b4ddfbb443742c5a3edd92aaa92780282023-08-11T07:40:00+02:002023-08-11T06:41:30+02:00FranckBrèvesjavascript <p>Juste pour jouer avec les API :</p>
<pre><code class="language-javascript">navigator.getBattery().then((battery) => {
window.alert(`Niveau de batterie = ${battery.level * 100}%`);
});
</code></pre>
https://open-time.net/post/2023/08/11/Batterie#comment-formhttps://open-time.net/feed/atom/comments/15803Ajouter un bouton « Retour en haut »urn:md5:09fbac7d2c072ed91af76a69770a72a62023-07-11T08:53:00+02:002023-07-11T14:59:00+02:00FranckBrèvesdotcleardéveloppementjavascriptthème <p>Il y a <a href="https://git.dotclear.org/dev/dotclear/issues/469">un ticket</a> ouvert au sujet d’une fonctionnalité du thème Berlin qui serait souhaitée sur un autre thème. En attendant que celui soit fermé, voilà ce qu’il faut pour ajouter cette fonctionnalité à votre thème.</p>
<p>Deux choses, la première créer un fichier <var>gotop.js</var> dans un dossier <var>js</var><sup id="fnref:ts1689083940.1"><a href="https://open-time.net/post/2023/07/11/Ajouter-un-bouton-%C2%AB-Retour-en-haut-%C2%BB#fn:ts1689083940.1" class="footnote-ref" role="doc-noteref">1</a></sup> de votre thème, la seconde un ajout à faire, avec l’éditeur de thème dans le fichier <var>user_footer.html</html>.</p>
<p>Pour le premier, copiez-collez le code suivant :</p>
<pre><code class="language-javascript">document.addEventListener('DOMContentLoaded', () => {
// totop init
const gotop_btn = document.getElementById('gotop');
const gotop_link = document.querySelector('#gotop a');
gotop_link.setAttribute('title', gotop_link.textContent);
gotop_link.innerHTML =
'<svg width="24px" height="24px" viewBox="1 -6 524 524" xmlns="http://www.w3.org/2000/svg"><path fill="currentColor" d="M460 321L426 355 262 192 98 355 64 321 262 125 460 321Z"></path></svg>';
gotop_btn.style.width = '32px';
gotop_btn.style.height = '32px';
gotop_btn.style.padding = '3px 0';
// totop scroll
window.addEventListener('scroll', () => {
if (document.querySelector('html').scrollTop === 0) {
gotop_btn.classList.add('hide');
gotop_btn.classList.remove('show');
} else {
gotop_btn.classList.add('show');
gotop_btn.classList.remove('hide');
}
});
gotop.addEventListener('click', (e) => {
const isReduced =
window.matchMedia(`(prefers-reduced-motion: reduce)`) === true ||
window.matchMedia(`(prefers-reduced-motion: reduce)`).matches === true;
if (!!isReduced) {
document.querySelector('html').scrollTop = 0;
} else {
function scrollTo(element, to, duration) {
const easeInOutQuad = (time, ease_start, ease_change, ease_duration) => {
time /= ease_duration / 2;
if (time < 1) return (ease_change / 2) * time * time + ease_start;
time--;
return (-ease_change / 2) * (time * (time - 2) - 1) + ease_start;
};
let currentTime = 0;
const start = element.scrollTop;
const change = to - start;
const increment = 20;
const animateScroll = () => {
currentTime += increment;
element.scrollTop = easeInOutQuad(currentTime, start, change, duration);
if (currentTime < duration) {
setTimeout(animateScroll, increment);
}
};
animateScroll();
}
scrollTo(document.querySelector('html'), 0, 800);
}
e.preventDefault();
});
});
</code></pre>
<p>Avec une petite cerise sur le gâteau qui est que si le visiteur a choisi de réduire les animations alors le retour en haut de page ce fera sans défilement doux.</p>
<p>Pour le second, insérez les lignes suivantes<sup id="fnref:ts1689083940.2"><a href="https://open-time.net/post/2023/07/11/Ajouter-un-bouton-%C2%AB-Retour-en-haut-%C2%BB#fn:ts1689083940.2" class="footnote-ref" role="doc-noteref">2</a></sup> :</p>
<pre><code class="language-xml"><p id="gotop"><a href="https://open-time.net/post/2023/07/11/Ajouter-un-bouton-%C2%AB-Retour-en-haut-%C2%BB#prelude">{{tpl:lang Page top}}</a></p>
<script src="https://open-time.net/post/2023/07/11/{{tpl:BlogThemeURL}}/js/gotop.js"></script>
</code></pre>
<p>C’est tout :-)</p>
<div class="footnotes" role="doc-endnotes">
<hr />
<ol>
<li id="fn:ts1689083940.1" role="doc-endnote">
<p>Dossier à créer s’il n’existe pas. <a href="https://open-time.net/post/2023/07/11/Ajouter-un-bouton-%C2%AB-Retour-en-haut-%C2%BB#fnref:ts1689083940.1" class="footnote-backref" role="doc-backlink">↩︎</a></p>
</li>
<li id="fn:ts1689083940.2" role="doc-endnote">
<p>On peut remplacer <code>{{tpl:lang Page top}}</code> par n’importe quel libellé de son choix, <samp>Haut de page</samp>, par exemple <a href="https://open-time.net/post/2023/07/11/Ajouter-un-bouton-%C2%AB-Retour-en-haut-%C2%BB#fnref:ts1689083940.2" class="footnote-backref" role="doc-backlink">↩︎</a></p>
</li>
</ol>
</div>
https://open-time.net/post/2023/07/11/Ajouter-un-bouton-%C2%AB-Retour-en-haut-%C2%BB#comment-formhttps://open-time.net/feed/atom/comments/15771Satisfactionurn:md5:6f7e48805b8d7e2933d2b72880e93d3f2023-06-26T07:35:00+02:002023-06-26T22:53:51+02:00FranckBrèvesdotcleardéveloppementjavascript <p><img src="https://open-time.net/public/memojis/adore.jpg" alt="" style="margin: 0 auto; display: block;" height="421" width="421" /></p>
<p>Passer de ceci :</p>
<pre><code class="language-javascript"> // Multiple media insertion helpers
dotclear.mm_select.getInfos = (path, list, pref, tb, fn) => {
// Call REST Service
$.get('services.php', {
f: 'getMediaInfos',
xd_check: dotclear.nonce,
path,
list,
pref,
})
.done((data) => {
if ($('rsp[status=failed]', data).length > 0) {
// For debugging purpose only:
// console.log($('rsp',data).attr('message'));
window.console.log('Dotclear REST server error');
} else {
// ret -> status (true/false)
// data -> media infos
const ret = Number($('rsp>mm_select', data).attr('ret'));
if (ret) {
const json = $('rsp>mm_select', data).attr('data');
fn(tb, JSON.parse(json));
}
}
})
.fail((jqXHR, textStatus, errorThrown) => {
window.console.log(`AJAX ${textStatus} (status: ${jqXHR.status} ${errorThrown})`);
})
.always(() => {
// Nothing here
});
return null;
};
</code></pre>
<p>À cela :</p>
<pre><code class="language-javascript"> // Multiple media insertion helpers
dotclear.mm_select.getInfos = (path, list, pref, tb, fn) => {
list = JSON.stringify(list);
pref = JSON.stringify(pref);
// Call REST Service
dotclear.jsonServicesPost(
'getMediaInfos',
(data) => {
if (data.ret) {
fn(tb, data.info);
}
},
{path, list, pref},
);
return null;
};
</code></pre>
<p>J’adore :-)</p>
<p>PS : L’usage de <code>JSON.stringify()</code> permet de passer des paramètres complexes (tableaux, objets, …) et il suffit côté PHP d’utiliser un <code>json_decode()</code> avec le 2e argument à <var>true</var> si c’est un tableau de clé/valeur.</p>
https://open-time.net/post/2023/06/26/Satisfaction#comment-formhttps://open-time.net/feed/atom/comments/15754Table responsiveurn:md5:0e21cf50a52805af04dd62f3ae5c2d392023-06-22T06:20:00+02:002023-06-22T07:57:42+02:00FranckBrèvesCSSjavascript <p>J’ai repris ici le principe des tables responsives de l’admin de Dotclear, qui, je sais, est un pis-aller quand on a des tables avec des contenus bien copieux.</p>
<p>Un peu de javascript :</p>
<pre><code class="language-javascript">/**
* Add headers on each cells (responsive tables)
*
* @param DOM elt table The table
* @param string selector The selector
* @param number [offset=0] The offset = number of firsts columns to ignore
* @param boolean [thead=false] True if titles are in thead rather than in the first tr of the body
*/
const responsiveCellHeaders = (table, selector, offset = 0, thead = false) => {
try {
const THarray = [];
const ths = table.getElementsByTagName('th');
for (const th of ths) {
for (let colspan = th.colSpan; colspan > 0; colspan--) {
THarray.push(th.innerText);
}
}
const tds = table.getElementsByTagName('td');
for (const td of tds) {
const div = document.createElement('div');
div.innerHTML = td.innerHTML;
td.innerHTML = '';
td.appendChild(div);
}
const styleElm = document.createElement('style');
let styleSheet;
document.head.appendChild(styleElm);
styleSheet = styleElm.sheet;
for (let i = offset; i < THarray.length; i++) {
styleSheet.insertRule(
`${selector} td:nth-child(${i + 1})::before {content:"${THarray[i]} ";}`,
styleSheet.cssRules.length,
);
}
table.className += `${table.className === '' ? '' : ' '}rch${thead ? ' rch-thead' : ''}`;
} catch (e) {
console.log(`responsiveCellHeaders(): ${e}`);
}
};
const generateSelector = function (context) {
let index, pathSelector, localName;
if (context == 'null') throw 'not an dom reference';
// call getIndex function
index = getIndex(context);
while (context.tagName) {
// selector path
pathSelector = context.localName + (pathSelector ? '>' + pathSelector : '');
context = context.parentNode;
}
// selector path for nth of type
pathSelector = pathSelector + `:nth-of-type(${index})`;
return pathSelector;
};
const getIndex = function (node) {
// get index for nth of type element
let i = 1;
let tagName = node.tagName;
while (node.previousSibling) {
node = node.previousSibling;
if (node.nodeType === 1 && tagName.toLowerCase() == node.tagName.toLowerCase()) {
i++;
}
}
return i;
};
document.querySelectorAll('#content table').forEach(function (element) {
const selector = generateSelector(element);
const thead = element.tHead ? true : false;
if (element && selector) responsiveCellHeaders(element, selector, 0, thead);
});
</code></pre>
<p>Et un peu de CSS :</p>
<pre><code class="language-css">/* Responsive Cell Header */
.rch td::before {
display: none;
}
@media screen and (max-width: 60em), print and (max-width: 5in) {
table.rch {
display: block;
}
table.rch caption,
table.rch tbody,
table.rch tr,
table.rch td {
display: block;
}
table.rch th,
table.rch tr:first-of-type {
display: none;
}
table.rch td:first-of-type {
border-top: 1px solid;
}
table.rch td::before {
display: inline;
font-weight: bold;
}
table.rch td {
display: grid;
grid-template-columns: 10em auto;
grid-gap: 1em 0.5em;
text-align: left;
border: none;
}
table.rch input,
table.rch select {
align-self: center;
}
table.rch-thead thead {
display: none;
}
table.rch-thead tr:first-of-type {
display: block;
}
}
</code></pre>
<p>Et le tour est joué (a priori).</p>
<p>À tester avec <a href="https://open-time.net/post/2023/06/21/ToDo-list-pour-la-2.27">le billet précédent</a>.</p>
https://open-time.net/post/2023/06/22/Table-responsive#comment-formhttps://open-time.net/feed/atom/comments/15750Module JSurn:md5:cb2acefc4c483f33d63c46ea143a2ea02023-01-22T06:09:00+01:002023-01-22T10:24:37+01:00FranckBrèvesdotcleardéveloppementjavascript <p>On ne sait jamais, vous pourriez avoir envie d’utiliser des scripts javascript via le système de module (import/export), plutôt qu’avec la façon historique.</p>
<p class="information">
Ça fonctionne très bien avec Dotclear, je viens de tester, mais il faut savoir tout de même qu’<strong>on ne peut mélanger des modules et des non-modules</strong>, ce qui limite forcément l’intérêt avec Dotclear qui est farci, pour l’instant, de scripts ordinaires.
</p>
<p>Un exemple avec le plugin notifyMe pour lequel j’ai ajouté une copie du fichier <code>js/notify.js</code> sous le nom <code>js/notify.mjs</code> <sup id="fnref:ts1674379477.1"><a href="https://open-time.net/post/2023/01/22/Module-JS#fn:ts1674379477.1" class="footnote-ref" role="doc-noteref">1</a></sup> et auquel j’ai ajouté une ligne au début :</p>
<pre><code class="language-javascript">export { notifyBrowser };
</code></pre>
<p>La suite étant identique au fichier copié.</p>
<p>Après, création d’un petit fichier de test dans mon plugin de … test avec ce contenu :</p>
<pre><code class="language-javascript">import { notifyBrowser } from './index.php?pf=notifyMe/js/notify.mjs';
// Dom ready
window.addEventListener('load', () => {
notifyBrowser('Et voilà !');
});
</code></pre>
<p>Voire si vous voulez faire ça en mode asynchrone :</p>
<pre><code class="language-javascript">// Dom ready
window.addEventListener('load', async () => {
const notifyMe = await import('./index.php?pf=notifyMe/js/notify.mjs');
await notifyMe.notifyBrowser('Async');
});
</code></pre>
<p>Voilà, <q>Ça juste marche</q> <sup id="fnref:ts1674379477.2"><a href="https://open-time.net/post/2023/01/22/Module-JS#fn:ts1674379477.2" class="footnote-ref" role="doc-noteref">2</a></sup>.</p>
<div class="footnotes" role="doc-endnotes">
<hr />
<ol>
<li id="fn:ts1674379477.1" role="doc-endnote">
<p>J’ai ajouté de quoi charger des fichiers <code>mjs</code> dans la prochaine 2.25. <a href="https://open-time.net/post/2023/01/22/Module-JS#fnref:ts1674379477.1" class="footnote-backref" role="doc-backlink">↩︎</a></p>
</li>
<li id="fn:ts1674379477.2" role="doc-endnote">
<p>Expression prisée par un cador du CSS, <a href="https://www.ffoodd.fr/">Gaël Poupard</a>. <a href="https://open-time.net/post/2023/01/22/Module-JS#fnref:ts1674379477.2" class="footnote-backref" role="doc-backlink">↩︎</a></p>
</li>
</ol>
</div>
https://open-time.net/post/2023/01/22/Module-JS#comment-formhttps://open-time.net/feed/atom/comments/15598Économiser un clicurn:md5:dbc3475fed9dc8a5be1b8d65f54639f92023-01-12T06:07:00+01:002023-01-12T11:57:34+01:00FranckBrèvesjavascript <p>J’utilise <a href="https://github.com/franck-paul/tidyAdmin">tidyAdmin</a> pour diverses choses, entre autres pour modifier un peu l’apparence de mon tableau de bord et j’en ai profité pour rajouter une petite routine javascript qui me permet d’éviter d’avoir à cliquer sur le <i lang="en">collapser</i> qui dévoile ou masque le menu de gauche. Eh ouais, j’suis une feignasse assumée :-)</p>
<p>Donc voilà le code correspondant :</p>
<pre><code class="language-javascript">'use strict';
window.addEventListener('load', () => {
// Main menu collapser
const objMain = $('#wrapper');
const hideMainMenu = 'hide_main_menu';
// Sidebar separator
$('#collapser').on('mouseover', (e) => {
const t = window.setTimeout(() => {
e.preventDefault();
if (objMain.hasClass('hide-mm')) {
// Show sidebar
objMain.removeClass('hide-mm');
dotclear.dropLocalData(hideMainMenu);
$('input#qx').trigger('focus');
return;
}
// Hide sidebar
objMain.addClass('hide-mm');
dotclear.storeLocalData(hideMainMenu, true);
$('#content a.go_home').trigger('focus');
}, 400);
$(e.target).on('mouseleave', () => {
window.clearTimeout(t);
});
});
});
</code></pre>
<details>
<summary>Ou en javascript pur (sans utiliser jQuery)</summary>
<pre><code class="language-javascript">'use strict';
window.addEventListener('load', () => {
// Main menu collapser
const objMain = document.getElementById('wrapper');
const hideMainMenu = 'hide_main_menu';
// Sidebar separator
document.getElementById('collapser').addEventListener('mouseover', (e) => {
const t = setTimeout(() => {
e.preventDefault();
if (objMain.classList.contains('hide-mm')) {
// Show sidebar
objMain.classList.remove('hide-mm');
dotclear.dropLocalData(hideMainMenu);
document.querySelector('input#qx')?.focus();
return;
}
// Hide sidebar
objMain.classList.add('hide-mm');
dotclear.storeLocalData(hideMainMenu, true);
document.querySelector('#content a.go_home')?.focus();
}, 400);
e.target.addEventListener('mouseleave', () => {
clearTimeout(t);
});
});
});
</code></pre>
</details>
<p>Rien d’essentiel ici, juste je détecte le survol du séparateur au bout de 400 millisecondes, et dans ce cas je déclenche les hostilités :-)</p>
<p>Voilà, plus besoin de cliquer, juste à survoler et attendre la magie \o/</p>
https://open-time.net/post/2023/01/12/Economiser-un-clic#comment-formhttps://open-time.net/feed/atom/comments/15588ChatGPT et javascripturn:md5:c8ac458bb8581810459abd93ead799182023-01-08T09:25:00+01:002023-01-08T10:12:11+01:00FranckBrèvesdotcleardéveloppementjavascript <p>Cette <a href="https://chat.openai.com/chat" hreflang="en"><abbr title="Intelligence artificielle">IA</abbr></a> semble plutôt bien fonctionner avec le code, d’après ce que je vois passer sur le net, à la condition bien sûr d’être derrière prêt à vérifier et corriger ce qu’elle produit ; mais ça reste intéressant !</p>
<p>Ce matin j’ai voulu tester quelque chose de spécifique, alors j’ai pris une des fonctions Javascript présente dans Dotclear, fonction qui utilise jQuery et j’ai demandé à ChatGPT de me ré-écrire ladite fonction en javascript pur :</p>
<blockquote>
<p>Rewrite this jquery function without using jquery…</p>
</blockquote>
<p>Avec le code de la fonction :</p>
<pre><code class="language-javascript">dotclear.condSubmit = (chkboxes, target) => {
const checkboxes = $(chkboxes);
const submitButt = $(target);
if (checkboxes === undefined || submitButt === undefined) {
return;
}
// Set initial state
submitButt.attr('disabled', !checkboxes.is(':checked'));
if (checkboxes.is(':checked')) {
submitButt.removeClass('disabled');
} else {
submitButt.addClass('disabled');
}
checkboxes.on('click', () => {
// Update target state
submitButt.attr('disabled', !checkboxes.is(':checked'));
if (checkboxes.is(':checked')) {
submitButt.removeClass('disabled');
} else {
submitButt.addClass('disabled');
}
});
};
</code></pre>
<p>Sa réponse est ci-dessous :</p>
<pre><code class="language-javascript">function condSubmit(chkboxes, target) {
const checkboxes = document.querySelectorAll(chkboxes);
const submitButt = document.querySelector(target);
if (checkboxes === undefined || submitButt === undefined) {
return;
}
// Set initial state
submitButt.disabled = !checkboxes.some(checkbox => checkbox.checked);
if (checkboxes.some(checkbox => checkbox.checked)) {
submitButt.classList.remove('disabled');
} else {
submitButt.classList.add('disabled');
}
checkboxes.forEach(checkbox => {
checkbox.addEventListener('click', () => {
// Update target state
submitButt.disabled = !checkboxes.some(checkbox => checkbox.checked);
if (checkboxes.some(checkbox => checkbox.checked)) {
submitButt.classList.remove('disabled');
} else {
submitButt.classList.add('disabled');
}
});
});
}
</code></pre>
<p>Pas mal, même s’il y a quelques optimisations à faire — sur le code original aussi d’ailleurs, mais ça n’est pas le sujet du test et <strong>quelques erreurs</strong> à corriger.</p>
<p>En effet <code>querySelectorAll()</code> renvoie un objet <code>NodeList</code> sur lequel la méthode <code>some()</code> n’existe pas. Il faut passer par un <code>Array.from()</code> pour pouvoir le faire.</p>
<p>Par ailleurs <code>querySelectorAll()</code> ne renvoie pas <var>undefined</var> si la recherche n’aboutit pas, mais un objet <code>NodeList</code> vide et <code>querySelector()</code> renvoie <var>null</var> plutôt que <var>undefined</var> si la recherche n’aboutit pas. Il faut donc corriger le test qui permet de quitter la fonction si une des deux recherches n’aboutit pas.</p>
<p>Le code, une fois vérifié et corrigé ressemble plutôt à ça :</p>
<pre><code class="language-javascript">dotclear.condSubmit = (chkboxes, target) => {
const checkboxes = Array.from(document.querySelectorAll(chkboxes));
const submitButt = document.querySelector(target);
if (checkboxes.length === 0 || submitButt === null) {
return;
}
// Set initial state
submitButt.disabled = !checkboxes.some((checkbox) => checkbox.checked);
if (submitButt.disabled) {
submitButt.classList.add('disabled');
} else {
submitButt.classList.remove('disabled');
}
checkboxes.forEach((checkbox) => {
checkbox.addEventListener('click', () => {
// Update target state
submitButt.disabled = !checkboxes.some((checkbox) => checkbox.checked);
if (submitButt.disabled) {
submitButt.classList.add('disabled');
} else {
submitButt.classList.remove('disabled');
}
});
});
};
</code></pre>
<p>Du coup je vais pousser cette modification, autant se passer de jQuery tant qu’on peut, sachant qu’il faudra tout de même prendre la précaution de vérifier que basculer de <code>$(target)</code> (par exemple) est bien équivalent à <code>document.querySelector(target)</code> dans le cadre où c’est utilisé car jQuery considère comme <strong>identifiant</strong> l’attribut <strong>name</strong> d’un élément HTML alors que Javascript <strong>pas</strong>. Dis autrement, il faut un <code>id=value</code> pour que ça fonctionne si la chaîne de sélection comporte un <code>#value</code> !</p>
<p>Conclusion, l’<abbr title="Intelligence artificielle">IA</abbr> mâche bien le boulot, mais il faut un développeur derrière pour vérifier et corriger ce qui doit l’être !</p>
<p>PS : J’ai aussi vu passer un billet de blog (me souviens plus où mais ça n’est pas grave) où l’auteur explique s’en servir pour générer le code de tests unitaires ; peut-être quelque chose à tester aussi.</p>
https://open-time.net/post/2023/01/08/ChatGPT-et-javascript#comment-formhttps://open-time.net/feed/atom/comments/15584Les béquilles rouillent aussiurn:md5:f25b3669b85a2638c9699845085ab5162022-09-18T09:03:00+02:002022-09-18T11:28:33+02:00FranckBrèvesdotcleardéveloppementjavascriptplugin <figure style="margin: 0 auto; display: table;">
<a href="https://open-time.net/public/dcim/2010/08/07/IMG_9499.jpg" title="Anneau de rouille, Kerity, France, août 2010"><img src="https://open-time.net/public/dcim/2010/08/07/.IMG_9499_u.jpg" alt="Anneau de rouille, Kerity, France, août 2010" title="Fête des vieux gréments à Kerity" height="600" width="600" /></a>
<figcaption>Fête des vieux gréments à Kerity</figcaption>
</figure>
<p>Suite à mes précédents billets sur <a href="https://open-time.net/post/2022/09/16/L-eco-systeme-rouille">l’éco-système de Dotclear</a> et le <a href="https://open-time.net/post/2022/09/17/La-bequille-de-SQLite">support SQLite</a>, j’ai voulu jeter un œil au code d’un plugin qui rouille, <a href="https://plugins.dotaddict.org/dc2/details/GalleryInsert">GalleryInsert</a>, sorte de boîte à outils pour intégrer dans un billet une série d’image, éventuellement assortie d’un script qui permet d’animer l’ensemble (carousel, …).</p>
<p>Il y a, dans ce plugin, quatre <q>béquilles</q> qui sont :</p>
<ol>
<li><a href="https://github.com/ttelang/divbox" hreflang="en">divbox</a>, qui n’a pas bougé depuis <strong>cinq</strong> ans — je suppose que c’est un avatar de <a href="https://plugins.dotaddict.org/dc2/details/lightbox">lightbox</a>, <a href="https://plugins.dotaddict.org/dc2/details/magnific-popup">magnific-popup</a> et consort</li>
<li><a href="https://github.com/galleriajs/galleria" hreflang="en">galleria</a>, qui, si j’en crois <a href="https://github.com/GalleriaJS/galleria/issues" hreflang="en">les tickets ouverts</a>, n’a pas l’air très vivant non plus</li>
<li><a href="https://github.com/jakubkowalczyk-pl/jgallery/" hreflang="en">jgallery</a>, qui est visiblement encore supporté</li>
<li><a href="https://webscripts.softpedia.com/script/Modal-Windows/jQuery-TosRUs-82289.html" hreflang="en">tosrus</a><sup id="fnref:ts1663493313.1"><a href="https://open-time.net/post/2022/09/18/Les-bequilles-rouillent-aussi#fn:ts1663493313.1" class="footnote-ref" role="doc-noteref">1</a></sup>, qui est mort depuis plus de <strong>sept</strong> ans — il fournit un carousel</li>
</ol>
<p>Toutes ces bibliothèques, sauf une, s’appuient sur <strong>jQuery</strong>, reste à vérifier si elles tournent avec la <strong>3.6.1</strong> qu’on diffuse dans Dotclear, si je liste, dans le même ordre :</p>
<ol>
<li>divbox, c’est jQuery ≥ <strong>1.3.2</strong></li>
<li>galleria, c’est jQuery ≥ <strong>1.9.0</strong></li>
<li>jgallery, c’est <strong>indépendant</strong>, une chose de moins à gérer, tant mieux</li>
<li>tosrus, c’est jQuery ≥ <strong>1.7.0</strong></li>
</ol>
<p>Autant dire qu’à part jgallery, c’est pas gagné question fraîcheur et compatibilité avec l’existant !</p>
<p>Bref, je ne veux pas critiquer les choix faits pour le développement de ce plugin, qui fait/faisait le job qu’on lui demande, par contre je détaille sa construction (côté bibliothèque Javascript) pour montrer que c’est compliqué de s’appuyer sur du code tiers parce qu’il a tendance à rouiller, voire à disparaître !</p>
<p>Alors je sais que c’est le <q>jeu</q> du logiciel libre et ouvert mais ça a ses limites.</p>
<p>Si jamais quelqu’un avait envie de le reprendre je suggèrerai ceci :</p>
<ul>
<li><p>Virer les vieilles béquilles, ou a minima faire en sorte que leur code soit mis au goût du jour et compatible avec <strong>jQuery ≥ 3.6.1</strong></p></li>
<li><p>Virer la gestion des galeries <strong>privées</strong>, il y a beaucoup de code PHP autour de ça et je ne suis pas certain que ce soit une bonne idée de le conserver <sup id="fnref:ts1663493313.2"><a href="https://open-time.net/post/2022/09/18/Les-bequilles-rouillent-aussi#fn:ts1663493313.2" class="footnote-ref" role="doc-noteref">2</a></sup></p></li>
<li><p>Mettre en place un <strong>cache</strong> des répertoires de la médiathèque pour éviter de tout balayer à chaque fois <sup id="fnref:ts1663493313.5"><a href="https://open-time.net/post/2022/09/18/Les-bequilles-rouillent-aussi#fn:ts1663493313.5" class="footnote-ref" role="doc-noteref">3</a></sup>, ou alors utiliser la même technique que pour le changement de répertoire d’un média <sup id="fnref:ts1663493313.3"><a href="https://open-time.net/post/2022/09/18/Les-bequilles-rouillent-aussi#fn:ts1663493313.3" class="footnote-ref" role="doc-noteref">4</a></sup></p></li>
<li><p>En mode wiki : Utiliser une <strong>macro</strong> <code>///gi</code> par exemple plutôt que le marqueur <code>::</code> qui peut être confondu avec une définition (de liste) si inséré en début de ligne <sup id="fnref:ts1663493313.4"><a href="https://open-time.net/post/2022/09/18/Les-bequilles-rouillent-aussi#fn:ts1663493313.4" class="footnote-ref" role="doc-noteref">5</a></sup></p></li>
<li><p>D’une manière générale, plutôt qu’un code générique du style <code>::gallery</code> ou <code>///gi</code>, j’utiliserai carrément une balise HTML spécifique, du genre <code><gallery [attr…]>…</gallery></code> ou <code><gallery [attr…] /></code>, plus simple à gérer ensuite et qui reste tout à fait compatible sans traitement (les navigateurs ignoreront simplement cette balise)</p></li>
</ul>
<p>Enfin côté source PHP du plugin (mécanique Dotclear) en lui-même, il lui faut un petit coup de peinture, mais rien de bien sorcier pour le rendre fonctionnel avec les dernières versions de Dotclear, je pense.</p>
<div class="footnotes" role="doc-endnotes">
<hr />
<ol>
<li id="fn:ts1663493313.1" role="doc-endnote">
<p>J’ai eu du mal à trouver la version (2.4.2) la plus récente à télécharger. <a href="https://open-time.net/post/2022/09/18/Les-bequilles-rouillent-aussi#fnref:ts1663493313.1" class="footnote-backref" role="doc-backlink">↩︎</a></p>
</li>
<li id="fn:ts1663493313.2" role="doc-endnote">
<p>Si jamais on avait besoin d’une galerie privée, autant la glisser dans un billet protégé par un mot de passe, facile à faire. <a href="https://open-time.net/post/2022/09/18/Les-bequilles-rouillent-aussi#fnref:ts1663493313.2" class="footnote-backref" role="doc-backlink">↩︎</a></p>
</li>
<li id="fn:ts1663493313.5" role="doc-endnote">
<p>Je vais rajouter dans la future 2.24 deux <i lang="en">behaviors</i> qui permettront de mettre à jour ce cache à chaque création/suppression de répertoire dans la médiathèque. <a href="https://open-time.net/post/2022/09/18/Les-bequilles-rouillent-aussi#fnref:ts1663493313.5" class="footnote-backref" role="doc-backlink">↩︎</a></p>
</li>
<li id="fn:ts1663493313.3" role="doc-endnote">
<p>Qui s’appuie sur la liste des répertoires connus dans la base de données (possiblement incomplète). <a href="https://open-time.net/post/2022/09/18/Les-bequilles-rouillent-aussi#fnref:ts1663493313.3" class="footnote-backref" role="doc-backlink">↩︎</a></p>
</li>
<li id="fn:ts1663493313.4" role="doc-endnote">
<p>Ça oblige a échapper le premier caractère pour éviter la confusion. <a href="https://open-time.net/post/2022/09/18/Les-bequilles-rouillent-aussi#fnref:ts1663493313.4" class="footnote-backref" role="doc-backlink">↩︎</a></p>
</li>
</ol>
</div>
https://open-time.net/post/2022/09/18/Les-bequilles-rouillent-aussi#comment-formhttps://open-time.net/feed/atom/comments/15471JSON en veux-tu en voilàurn:md5:059c77873efe726f105343cea106e1222022-08-08T08:38:00+02:002022-08-08T16:33:18+02:00FranckBrèvesdotcleardéveloppementjavascriptPHP <p><img src="https://open-time.net/public/memojis/idee.jpg" alt="" style="margin: 0 auto; display: table;" height="421" width="421" /></p>
<p>Ça fait un moment que ça me titille d’utiliser le serveur REST de Dotclear avec des données au format JSON, format plus simple à gérer du côté Javascript. Je viens de pousser une version expérimentale (à utiliser à vos risques et périls donc avec la future version 2.23 de Dotclear).</p>
<p>Je viens de modifier le plugin <a href="https://github.com/franck-paul/wordCount">wordCount</a> en conséquence et ça permet, pour le code PHP (fichier <var>_services.php</var>), de basculer de ceci :</p>
<pre><code class="language-php"><?php
…
class restWordCount
{
/**
* Serve method to update current counters.
*
* @param dcCore $core The core
* @param array $get The get
*
* @return xmlTag The xml tag.
*/
public static function getCounters($core, $get)
{
global $core;
$rsp = new xmlTag('check');
$rsp->ret = false;
if ($core->blog->settings->wordcount->wc_active) {
…
$rsp->html = $html;
$rsp->ret = true;
}
return $rsp;
}
}
</code></pre>
<p>à cela<sup id="fnref:ts1659969198.2"><a href="https://open-time.net/post/2022/08/08/JSON-en-veux-tu-en-voila#fn:ts1659969198.2" class="footnote-ref" role="doc-noteref">1</a></sup> :</p>
<pre><code class="language-php"><?php
…
class restWordCount
{
/**
* Serve method to update current counters.
*
* @param array $get The get
*
* @return The payload.
*/
public static function getCounters($get)
{
$payload = [
'ret' => false,
];
if (dcCore::app()->blog->settings->wordcount->wc_active) {
…
$payload = [
'ret' => true,
'html' => $html,
];
}
return $payload;
}
}
</code></pre>
<p>Et pour le code javascript (fichier <var>js/service.js</var>) de ceci :</p>
<pre><code class="language-javascript">/*global $, dotclear */
'use strict';
dotclear.wordCountGetCounters = () => {
$.get('services.php', {
f: 'wordCountGetCounters',
xd_check: dotclear.nonce,
excerpt: $('#post_excerpt').val(),
content: $('#post_content').val(),
format: $('#post_format').val(),
})
.done((data) => {
if ($('rsp[status=failed]', data).length > 0) {
// For debugging purpose only:
// window.console.log($('rsp', data).attr('message'));
window.console.log('Dotclear REST server error');
} else {
const ret = Number($('rsp>check', data).attr('ret'));
if (ret) {
const html = $('rsp>check', data).attr('html');
const $container = $('div.wordcount details p');
if ($container) {
// Replace current counters
$container.empty().append(html);
}
}
}
})
.fail((jqXHR, textStatus, errorThrown) => {
window.console.log(`AJAX ${textStatus} (status: ${jqXHR.status} ${errorThrown})`);
})
.always(() => {
// Nothing here
});
};
$(() => {
// Set 60 seconds interval between two counters calculation
dotclear.wordCountGetCounters_Timer = setInterval(dotclear.wordCountGetCounters, 60 * 1000);
});
</code></pre>
<p>à cela<sup id="fnref:ts1659969198.1"><a href="https://open-time.net/post/2022/08/08/JSON-en-veux-tu-en-voila#fn:ts1659969198.1" class="footnote-ref" role="doc-noteref">2</a></sup> :</p>
<pre><code class="language-javascript">/*global dotclear */
'use strict';
window.addEventListener('load', () => {
// Set interval between two counters calculation
dotclear.wordcount = dotclear.getData('wordcount');
dotclear.wordcount.getCounters = () => {
dotclear.services(
'wordCountGetCounters',
(data) => {
const response = JSON.parse(data);
if (response?.success) {
if (response?.payload.ret) {
// Replace current counters
const p = document.querySelector('div.wordcount details p');
if (p) {
p.innerHTML = response.payload.html;
}
}
} else {
console.log(dotclear.debug && response?.message ? response.message : 'Dotclear REST server error');
return;
}
},
(error) => {
console.log(error);
},
true, // Use GET method
{
json: 1, // Use JSON format for payload
excerpt: document.querySelector('#post_excerpt').value,
content: document.querySelector('#post_content').value,
format: document.querySelector('#post_format').value,
},
);
};
dotclear.wordcount.timer = setInterval(dotclear.wordcount.getCounters, (dotclear.wordcount?.interval || 60) * 1000);
});
</code></pre>
<p>Notez que ça ne casse pas l’existant et que si vous voulez continuer à utiliser le serveur REST avec du format XML, c’est toujours possible :-)</p>
<div class="footnotes" role="doc-endnotes">
<hr />
<ol>
<li id="fn:ts1659969198.2" role="doc-endnote">
<p>J’en ai profité pour virer le passage de la variable <var>$core</var> à la méthode, seulement dans le cas où le format JSON est requis, puisque c’est <a href="https://open-time.net/post/2022/07/28/Dotclear-223-a-venir-sous-le-capot">inutile</a> depuis la 2.23 de Dotclear. <a href="https://open-time.net/post/2022/08/08/JSON-en-veux-tu-en-voila#fnref:ts1659969198.2" class="footnote-backref" role="doc-backlink">↩︎</a></p>
</li>
<li id="fn:ts1659969198.1" role="doc-endnote">
<p>J’en ai profité pour virer l’utilisation de jQuery au passage. <a href="https://open-time.net/post/2022/08/08/JSON-en-veux-tu-en-voila#fnref:ts1659969198.1" class="footnote-backref" role="doc-backlink">↩︎</a></p>
</li>
</ol>
</div>
https://open-time.net/post/2022/08/08/JSON-en-veux-tu-en-voila#comment-formhttps://open-time.net/feed/atom/comments/15430La doc !urn:md5:e59c99828ca9821f91889d28800fccf62022-06-23T10:36:00+02:002022-06-23T10:20:36+02:00FranckBrèvesdotcleardéveloppementjavascript <p><img src="https://open-time.net/public/memojis/nuage.jpg" alt="" style="margin: 0 auto; display: table;" height="421" width="421" /></p>
<p>J’ai <a href="https://git.dotclear.org/dev/dotclear/commit/9e9dc05ac20aa3ea3d8ea2095fde80f1eb7b14ce" hreflang="en">corrigé</a> un <a href="https://git.dotclear.org/dev/dotclear/issues/181">bug</a> ce matin en virant quelques lignes de code dont je ne sais absolument pas pourquoi elles ont été introduites !</p>
<p>J’ai l’intuition que c’est pour contourner un effet de bord d’un (ou de plusieurs) navigateur(s) de l’époque, mais impossible à affirmer ; ça manquait clairement de doc !</p>
<p>Tiens ça va me faire un bon exercice pour voir comment je peux remonter dans l’historique de ce fichier pour trouver le qui, quoi, pourquoi, comment…</p>
<hr />
<p>Après un petit tour dans l’historique Git, il s’avère que c’était déjà là depuis Dotclear 2.3, en 2011 — autant dire que c’est préhistorique à ce stade ! Va falloir que je fouille dans le dépôt SVN si je veux trouver l’origine de ces quelques lignes…</p>
<hr />
<p>Après fouille rapide il s’avère que je n’ai pas conservé le dépôt SVN sur ma machine. Vais aller fouiller <a href="https://download.dotclear.net/attic/">le grenier</a> des premières versions de Dotclear, juste pour voir…</p>
<hr />
<p>Eh bien c’était présent dans le code dès la première version de Dotclear 2, publiée en 2008 — a minima la 2.0-RC2 que j’ai vérifiée ! Mazette :-)</p>
<p>Ça serait rigolo que je demande pourquoi ce code a celui que je suppose en être l’auteur…</p>
https://open-time.net/post/2022/06/23/La-doc-#comment-formhttps://open-time.net/feed/atom/comments/15408Quelques lignes de codeurn:md5:bad641f88f6d55db4fb770452acc3f9a2022-04-23T08:14:00+02:002022-05-01T12:46:06+02:00FranckBrèvesdotcleardéveloppementjavascriptplugin <p>Juste pour se remettre doucement en selle, et parce que je suis une feignasse assumée, j’ai <a href="https://github.com/franck-paul/hljs/commit/86ca4365c1b20a83fdb87f4b37415e43e79a2a45">ajouté</a> de quoi parcourir les thèmes du plugin <a href="https://github.com/franck-paul/hljs">hljs</a> (sur sa page de réglage) avec les flèches droite et gauche ; c’est plus facile que de cliquer pour ouvrir la liste déroulante et cliquer sur l’option suivante ou précédente !</p>
<pre><code class="language-javascript">// Change theme CSS of code sample on arrow key
function nextTheme(forward = true) {
const e = document.getElementById('theme');
let next = e.selectedIndex;
next = (forward ? ++next : --next + e.options.length) % e.options.length;
e.value = e.options[next].value;
selectTheme();
}
…
$(() => {
…
$('#theme').on('keydown', (e) => {
if (e.which === 39) {
// Right arrow
nextTheme(true);
return false;
} else if (e.which === 37) {
// Left arrow
nextTheme(false);
return false;
}
});
…
</code></pre>
https://open-time.net/post/2022/04/23/Quelques-lignes-de-code#comment-formhttps://open-time.net/feed/atom/comments/15323Déplacer des trucsurn:md5:9044cd56417a51ad32db6fc9a401ce642022-03-15T08:05:00+01:002022-03-15T08:15:51+01:00FranckBrèvesdotcleardéveloppementjavascript <p><img src="https://open-time.net/public/memojis/brain.jpg" alt="" style="margin: 0 auto; display: table;" height="421" width="421" /></p>
<p>Autant pour le champ de recherche qui se trouve au dessus du <strong>menu principal</strong> et déplacé dans l’entête — à la nuance près que pour l’instant c’est ok uniquement sur grand écran, <i lang="en">laptop</i> donc<sup id="fnref:ts1647324951.1"><a href="https://open-time.net/post/2022/03/15/Deplacer-des-trucs#fn:ts1647324951.1" class="footnote-ref" role="doc-noteref">1</a></sup> — c’est facile, autant en faire autant pour pour le champ de recherche de la <strong>médiathèque</strong> c’est tout de suite moins simple puisque :</p>
<ol>
<li><p>Au contraire du premier, ce n’est pas tout le formulaire que je déplace (et restyle) mais <strong>uniquement</strong> le champ et son libellé (en gros).</p></li>
<li><p>Le formulaire parent peut être <strong>caché/masqué</strong> et dans ce cas ça masque aussi le champ de recherche même si visuellement il est en dehors (c’est pas tout à fait ça, mais bon) de la zone habituelle du formulaire de filtre et tri.</p></li>
</ol>
<p>Parce que j’avais commencé à styler avec une <strong>position absolue</strong> mais je n’avais pas pensé à l’état visible/caché du formulaire, c’est ballot ! Va falloir que je me creuse la cervelle pour trouver une solution :</p>
<ol>
<li><p>Est-ce qu’il y a un moyen de rendre <strong>visible</strong> ce champ et son libellé quel que soit l’état du formulaire parent et est-ce que la soumission fonctionne si ce dernier est <strong>caché</strong> ?</p></li>
<li><p>Sinon il faudra peut-être <strong>cloner</strong> ce formulaire en ne gardant que la partie concernée (recherche) et gérer la soumission pour rendre ça <strong>transparent</strong> côté serveur.</p></li>
</ol>
<p>Quoi qu’il en soit, si j’arrive à mes fins, ça sera intégré dans le plugin tidyAdmin, évidemment :-)</p>
<div class="footnotes" role="doc-endnotes">
<hr />
<ol>
<li id="fn:ts1647324951.1" role="doc-endnote">
<p>Les autres résolutions devront être gérées aussi côté CSS pour rendre ça pratique <a href="https://open-time.net/post/2022/03/15/Deplacer-des-trucs#fnref:ts1647324951.1" class="footnote-backref" role="doc-backlink">↩︎</a></p>
</li>
</ol>
</div>
https://open-time.net/post/2022/03/15/Deplacer-des-trucs#comment-formhttps://open-time.net/feed/atom/comments/15284Grand remplacementurn:md5:45374ea9b263403c00c619e641d0ce8e2022-03-11T06:50:00+01:002022-03-11T10:23:01+01:00FranckBrèvesdotcleardéveloppementjavascript <p>Je continue mes explorations pour voir si remplacer jQuery peut être fait sans (trop de) douleurs.</p>
<p>Mes derniers essais à ce sujet concernent les requêtes AJAX qu’on fait un peu partout pour récupérer des infos depuis le serveur sans recharger toute la page. Exemple avec un de mes plugins qui vérifie toutes les 5 minutes si le serveur répond.</p>
<p>Jusqu’alors j’avais ceci pour faire la requête (sachant que la réponse, si elle arrive, est au format XML) :</p>
<pre><code class="language-javascript">$.get('services.php', {
f: 'dmHostingMonitorPing',
xd_check: dotclear.nonce,
})
.done((data) => {
showStatus($('rsp[status=failed]', data).length > 0 ? false : true);
})
.fail((jqXHR, textStatus, errorThrown) => {
window.console.log(`AJAX ${textStatus} (status: ${jqXHR.status} ${errorThrown})`);
showStatus(false);
});
</code></pre>
<p>Je me sers de jQuery pour effectuer la requête GET (ligne 1) et pour faire une recherche dans la réponse (ligne 6) quand elle parvient.</p>
<p>Traduit en javascript pur, ça donne ceci :</p>
<pre><code class="language-javascript">let service = new URL('services.php', window.location.origin + window.location.pathname);
service.search = new URLSearchParams({
f: 'dmHostingMonitorPing',
xd_check: dotclear.nonce,
}).toString();
fetch(service)
.then((p) => {
if (!p.ok) {
throw Error(p.statusText);
}
return p.text();
})
.then((data) => {
const parser = new DOMParser();
const rsp = parser.parseFromString(data, 'text/xml');
const status = rsp.querySelector('rsp[status=failed');
showStatus(status === null);
})
.catch((error) => {
console.log(error);
showStatus(false);
});
</code></pre>
<p>C’est plus verbeux, certes, mais on retire toute dépendance à jQuery.</p>
<p>Ça le serait probablement un peu moins si les réponses étaient retournées sous forme JSON, probablement, puisqu’on pourrait utiliser un <code>return p.json();</code> en ligne 12 et ensuite traiter le paramètre <code>data</code> (ligne 14) comme un objet javascript avec :</p>
<pre><code class="language-javascript"> .then((data) => {
showStatus(data?.rsp?.status !== 'failed');
})
</code></pre>
<p>Cela dit il y a des cas où récupérer la réponse au format XML est pratique, en particulier pour les modules de tableau de bord qui affichent des listes d’éléments (billets, commentaires, …), reçu correctement formatés et affichable dans la foulée.</p>
<p>Je me demande s’il ne faudrait pas étendre un peu le serveur REST pour permettre de définir le format attendu de la réponse (XML par défaut, ou JSON) …</p>
<p>Et pour aller plus loin, je pense qu’on pourrait même coder une fonction un peu plus générique du style<sup id="fnref:ts1646986981.1"><a href="https://open-time.net/post/2022/03/11/Grand-remplacement#fn:ts1646986981.1" class="footnote-ref" role="doc-noteref">1</a></sup> :</p>
<pre><code class="language-javascript">dotclear.services = (fn, onSuccess = (data) => {}, onError = (error) => {}, get = true, params = {}, format = 'XML') => {
const service = new URL('services.php', window.location.origin + window.location.pathname);
dotclear.mergeDeep(params, { f: fn, xd_check: dotclear.nonce });
const init = { method: get ? 'GET' : 'POST' };
if (get) {
service.search = new URLSearchParams(params).toString();
} else {
if (format === 'XML') {
const data = new FormData();
Object.keys(params).forEach((key) => data.append(key, params[key]));
init.body = data;
} else {
init.headers = new Headers({ 'Content-Type': 'application/json' });
init.body = JSON.stringify(params);
}
}
fetch(service, init)
.then((p) => {
if (!p.ok) {
throw Error(p.statusText);
}
return format === 'XML' ? p.text() : p.json();
})
.then((data) => onSuccess(data))
.catch((error) => onError(error));
};
</code></pre>
<p>Pour ensuite l’utiliser comme ceci :</p>
<pre><code class="language-javascript">dotclear.services(
'dmHostingMonitorPing',
(data) => {
const parser = new DOMParser();
const rsp = parser.parseFromString(data, 'text/xml');
const status = rsp.querySelector('rsp[status=failed');
showStatus(status === null);
},
(error) => {
console.log(error);
showStatus(false);
},
);
</code></pre>
<p>Ou comme cela avec le format JSON :</p>
<pre><code class="language-javascript">dotclear.services(
'dmHostingMonitorPing',
(data) => {
showStatus(data?.rsp?.status !== 'failed');
},
(error) => {
console.log(error);
showStatus(false);
},
true,
{},
'JSON'
);
</code></pre>
<p>Dans ce cas on est pas loin de la simplicité d’appel de l’équivalent jQuery (voir premier bloc de code au début de ce billet).</p>
<p>Nota : je n’ai pas testé le cas où le format choisi serait JSON.</p>
<div class="footnotes" role="doc-endnotes">
<hr />
<ol>
<li id="fn:ts1646986981.1" role="doc-endnote">
<p>Attention, c’est juste un premier jet d’une fonction utilitaire, y’a surement moyen d’organiser ça un peu mieux, en particulier sur la gestion des paramètres optionnels <a href="https://open-time.net/post/2022/03/11/Grand-remplacement#fnref:ts1646986981.1" class="footnote-backref" role="doc-backlink">↩︎</a></p>
</li>
</ol>
</div>
https://open-time.net/post/2022/03/11/Grand-remplacement#comment-formhttps://open-time.net/feed/atom/comments/15280Bout de code pour les variables globalesurn:md5:9890aca76211b9e773aaa3af5b8c30fe2022-02-27T06:31:00+01:002022-02-27T06:31:00+01:00FranckBrèvesdotcleardéveloppementjavascript <p>Je ne sais plus où j’ai vu passer ça — je rajouterai la source si je la trouve —, un bout de code à copier-coller dans la console Javascript pour lister les variables globales.</p>
<p>Ça peut être utile pour détecter des trucs visibles qui ne devraient pas l’être :</p>
<pre><code class="language-javascript">// List of global variables
const iframe = window.document.createElement('iframe');
iframe.src = 'about:blank';
window.document.body.appendChild(iframe);
const browserGlobals = Object.keys(iframe.contentWindow);
window.document.body.removeChild(iframe);
// Get the global variables added at runtime by filtering out the browser's
// default global variables from the current window object.
const runtimeGlobals = Object.keys(window).filter((key) => {
const isFromBrowser = browserGlobals.includes(key);
return !isFromBrowser;
});
console.log('Runtime globals', runtimeGlobals);
</code></pre>
<p>L’idée est de créer une <i lang="en">iframe</i> vierge qui permet de récupérer la liste des variables globales créées par le navigateur puis de lister toutes les variables globales qui ne font pas partie de cette liste.</p>
https://open-time.net/post/2022/02/27/Bout-de-code-pour-les-variables-globales#comment-formhttps://open-time.net/feed/atom/comments/15268You might not need jQueryurn:md5:87d2727dd074dbbf50e4858d5ddfe6392022-02-25T08:42:00+01:002022-02-26T07:44:27+01:00FranckBrèvesdotcleardéveloppementjavascript <p>C’est une référence directe à <a href="https://youmightnotneedjquery.com/" hreflang="en">ce site</a> que je consulte régulièrement quand il me vient l’idée de remplacer jQuery par du javascript natif dans le code.</p>
<p>À l’occasion de la création d’un <a href="https://javascript.pruneau.ca/">nouveau blog</a> — de l’autre côté de la mare, comme ils disent là-bas — que je suis depuis quelques jours, je me suis rendu compte qu’il avait conservé le thème Berlin pour l’apparence, sans activer le chargement de jQuery, et forcément, Berlin utilise pour animer deux trois bricoles, un peu beaucoup de jQuery.</p>
<p>C’était donc l’occasion de mettre les mains dans le cambouis et de basculer côté natif pour s’affranchir du chargement de cette bibliothèque dans Berlin et Ductile. Ça n’empêchera pas par ailleurs de demander le chargement de jQuery si un de vos plugins l’utilise côté public ; je ne parle que des scripts spécifiques aux deux thèmes.</p>
<p>J’ai fait ça hier et voilà ce que ça donne pour Berlin où on passe de ceci :</p>
<pre><code class="language-javascript">/*global $, dotclear */
'use strict';
const dotclear_berlin = dotclear.getData('dotclear_berlin');
$('html').addClass('js');
// Show/Hide main menu
$('.header__nav')
.before(`<button id="hamburger" type="button" aria-label="${dotclear_berlin.navigation}" aria-expanded="false"></button>`)
.toggle();
$('#hamburger').on('click', function () {
$(this).attr('aria-expanded', $(this).attr('aria-expanded') == 'true' ? 'false' : 'true');
$(this).toggleClass('open');
$('.header__nav').toggle('easing', () => {
if ($('#hamburger').hasClass('open')) {
$('.header__nav li:first a')[0].focus();
}
});
});
// Show/Hide sidebar on small screens
$('#main').prepend(
`<button id="offcanvas-on" type="button"><span class="visually-hidden">${dotclear_berlin.show_menu}</span></button>`,
);
$('#offcanvas-on').on('click', () => {
const btn = $(
`<button id="offcanvas-off" type="button"><span class="visually-hidden">${dotclear_berlin.hide_menu}</span></button>`,
);
$('#wrapper').addClass('off-canvas');
$('#footer').addClass('off-canvas');
$('#sidebar').prepend(btn);
btn[0].focus({
preventScroll: true,
});
btn.on('click', (evt) => {
$('#wrapper').removeClass('off-canvas');
$('#footer').removeClass('off-canvas');
evt.target.remove();
$('#offcanvas-on')[0].focus();
});
});
$(document).ready(() => {
// totop init
const $btn = $('#gotop');
const $link = $('#gotop a');
$link.attr('title', $link.text());
$link.html(
'<svg width="24px" height="24px" viewBox="1 -6 524 524" xmlns="http://www.w3.org/2000/svg"><path fill="currentColor" d="M460 321L426 355 262 192 98 355 64 321 262 125 460 321Z"></path></svg>',
);
$btn.css({
width: '32px',
height: '32px',
padding: '3px 0',
});
// totop scroll
$(window).scroll(function () {
if ($(this).scrollTop() == 0) {
$btn.fadeOut();
} else {
$btn.fadeIn();
}
});
$btn.on('click', (e) => {
$('body,html').animate(
{
scrollTop: 0,
},
800,
);
e.preventDefault();
});
// scroll comment preview if present
document.getElementById('pr')?.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest' });
});
</code></pre>
<p>à cela :</p>
<pre><code class="language-javascript">/*global dotclear */
'use strict';
const dotclear_berlin = dotclear.getData('dotclear_berlin');
// Button templates
dotclear_berlin.template = {
hamburger: `<button id="hamburger" type="button" aria-label="${dotclear_berlin.navigation}" aria-expanded="false"></button>`,
offcanvas: {
on: `<button id="offcanvas-on" type="button"><span class="visually-hidden">${dotclear_berlin.show_menu}</span></button>`,
off: `<button id="offcanvas-off" type="button"><span class="visually-hidden">${dotclear_berlin.hide_menu}</span></button>`,
},
};
document.querySelector('html').classList.add('js');
{
// Show/Hide main menu
const header_nav = document.querySelector('.header__nav');
const hamburger = new DOMParser().parseFromString(dotclear_berlin.template.hamburger, 'text/html').body.firstElementChild;
header_nav.insertAdjacentElement('beforebegin', hamburger);
header_nav.classList.add('hide');
// Show/Hide sidebar on small screens
const main = document.getElementById('main');
const offcanvas = new DOMParser().parseFromString(dotclear_berlin.template.offcanvas.on, 'text/html').body.firstElementChild;
main.insertBefore(offcanvas, main.firstChild);
}
document.addEventListener('DOMContentLoaded', () => {
// Show/Hide main menu
const header_nav = document.querySelector('.header__nav');
const hamburger = document.getElementById('hamburger');
hamburger.addEventListener('click', () => {
hamburger.classList.toggle('open');
if (hamburger.classList.contains('open')) {
hamburger.setAttribute('aria-expanded', 'true');
header_nav.classList.add('show');
header_nav.classList.remove('hide');
document.querySelector('.header__nav li.li-first a').focus();
return;
}
hamburger.setAttribute('aria-expanded', 'false');
header_nav.classList.add('hide');
header_nav.classList.remove('show');
});
// Show/Hide sidebar on small screens
const offcanvas = document.getElementById('offcanvas-on');
offcanvas.addEventListener('click', () => {
const sidebar = document.getElementById('sidebar');
const wrapper = document.getElementById('wrapper');
const footer = document.getElementById('footer');
const button = new DOMParser().parseFromString(dotclear_berlin.template.offcanvas.off, 'text/html').body.firstElementChild;
wrapper.classList.add('off-canvas');
footer.classList.add('off-canvas');
sidebar.insertBefore(button, sidebar.firstChild);
button.focus({
preventScroll: true,
});
button.addEventListener('click', (evt) => {
wrapper.classList.remove('off-canvas');
footer.classList.remove('off-canvas');
evt.target.remove();
offcanvas.focus();
});
});
// totop init
const gotop_btn = document.getElementById('gotop');
const gotop_link = document.querySelector('#gotop a');
gotop_link.setAttribute('title', gotop_link.textContent);
gotop_link.innerHTML =
'<svg width="24px" height="24px" viewBox="1 -6 524 524" xmlns="http://www.w3.org/2000/svg"><path fill="currentColor" d="M460 321L426 355 262 192 98 355 64 321 262 125 460 321Z"></path></svg>';
gotop_btn.style.width = '32px';
gotop_btn.style.height = '32px';
gotop_btn.style.padding = '3px 0';
// totop scroll
window.addEventListener('scroll', () => {
if (document.querySelector('html').scrollTop === 0) {
gotop_btn.classList.add('hide');
gotop_btn.classList.remove('show');
} else {
gotop_btn.classList.add('show');
gotop_btn.classList.remove('hide');
}
});
gotop.addEventListener('click', (e) => {
function scrollTo(element, to, duration) {
const easeInOutQuad = (time, start, change, duration) => {
time /= duration / 2;
if (time < 1) return (change / 2) * time * time + start;
time--;
return (-change / 2) * (time * (time - 2) - 1) + start;
};
let currentTime = 0;
const start = element.scrollTop;
const change = to - start;
const increment = 20;
const animateScroll = () => {
currentTime += increment;
element.scrollTop = easeInOutQuad(currentTime, start, change, duration);
if (currentTime < duration) {
setTimeout(animateScroll, increment);
}
};
animateScroll();
}
scrollTo(document.querySelector('html'), 0, 800);
e.preventDefault();
});
// scroll comment preview if present
document.getElementById('pr')?.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest' });
});
</code></pre>
<p>Quant à Ductile, on passe de ceci :</p>
<pre><code class="language-javascript">/*global $ */
'use strict';
$(document).ready(() => {
if ($(window).width() < 1024) {
const create_name = (text) =>
text
.toLowerCase()
// Remove leading and trailing spaces, and any non-alphanumeric
// characters except for ampersands, spaces and dashes.
.replace(/^\s+|\s+$|[^a-z0-9&\s-]/g, '')
// Replace '&' with 'and'.
.replace(/&/g, 'and')
// Replaces spaces with dashes.
.replace(/\s/g, '-')
// Squash any duplicate dashes.
.replace(/(-)+\1/g, '$1');
// Set toggle class to each #sidebar h2
$('#sidebar div div h2').addClass('toggle');
// Hide all h2.toggle siblings
$('#sidebar div div h2').nextAll().hide();
// Add a link to each h2.toggle element.
$('h2.toggle').each(function () {
// Convert the h2 element text into a value that
// is safe to use in a name attribute.
const name = create_name($(this).text());
// Create a name attribute in the following sibling
// to act as a fragment anchor.
$(this).next().attr('name', name);
// Replace the h2.toggle element with a link to the
// fragment anchor. Use the h2 text to create the
// link title attribute.
$(this).html(`<a href="https://open-time.net/post/2022/02/25/You-might-not-need-jQuery#${name}" title="Reveal ${$(this).text()} content">${$(this).html()}</a>`);
});
// Add a click event handler to all h2.toggle elements.
$('h2.toggle').on('click', function (event) {
event.preventDefault();
// Toggle the 'expanded' class of the h2.toggle
// element, then apply the slideToggle effect
// to all siblings.
$(this).toggleClass('expanded').nextAll().slideToggle('fast');
});
// Remove the focus from the link tag when accessed with a mouse.
$('h2.toggle a').on('mouseup', function () {
// Use the blur() method to remove focus.
$(this).trigger('blur');
});
}
});
</code></pre>
<p>à cela :</p>
<pre><code class="language-javascript">'use strict';
document.addEventListener('DOMContentLoaded', () => {
// Show/Hide main menu
if (document.body.clientWidth < 1024) {
const create_name = (text) =>
text
.toLowerCase()
// Remove leading and trailing spaces, and any non-alphanumeric
// characters except for ampersands, spaces and dashes.
.replace(/^\s+|\s+$|[^a-z0-9&\s-]/g, '')
// Replace '&' with 'and'.
.replace(/&/g, 'and')
// Replaces spaces with dashes.
.replace(/\s/g, '-')
// Squash any duplicate dashes.
.replace(/(-)+\1/g, '$1');
// Set toggle class to each #sidebar h2
const h2 = document.querySelectorAll('#sidebar div div h2');
h2.forEach((element) => {
element.classList.add('toggle');
element.parentNode.classList.add('hide');
const name = create_name(element.textContent);
element.nextElementSibling.setAttribute('name', name);
element.innerHTML = `<a href="https://open-time.net/post/2022/02/25/You-might-not-need-jQuery#${name}" title="Reveal ${element.textContent} content">${element.innerHTML}</a>`;
element.addEventListener('click', (e) => {
e.preventDefault();
element.parentNode.classList.toggle('hide');
});
});
// Remove the focus from the link tag when accessed with a mouse.
const h2_link = document.querySelectorAll('h2.toggle a');
h2_link.forEach((element) => {
element.addEventListener('mouseup', () => {
const event = new Event('blur', { bubbles: true, cancelable: false });
element.dispatchEvent(event);
});
});
}
});
</code></pre>
<p>Pour être tout à fait honnête j’ai ajouté quelques règles CSS pour gérer les transitions de style et les visibilités de quelques éléments, la plupart étant auparavant prises en charge par jQuery.</p>
<p>Pour Berlin, j’ai ajouté ceci :</p>
<pre><code class="language-css">.header__nav.show {
opacity: 1;
height: auto;
transition: all 1s ease;
}
.header__nav.hide {
overflow: hidden;
opacity: 0;
height: 0;
}
#gotop.show {
display: block;
opacity: 1;
transition: opacity 400ms;
}
#gotop.hide {
opacity: 0;
transition: opacity 400ms;
}
</code></pre>
<p>Et pour Ductile, cela :</p>
<pre><code class="language-css">#sidebar div.hide *:not(h2.toggle):not(h2.toggle *) {
display: none;
}
</code></pre>
<p>Ce qui permet, si on utilise aucun plugin dépendant de jQuery, environ 150 kilo-octets en moins à charger pour les visiteurs, toujours ça gagné !</p>
https://open-time.net/post/2022/02/25/You-might-not-need-jQuery#comment-formhttps://open-time.net/feed/atom/comments/15266