Category Archives: General

If I don’t know where else to put something or don’t think a post warrants a new category, it will just end up here.

Helpful CSS Classes for Flipping and Rotating Elements

Useful CSS transformation classes for use with Content Security Policy (CSP).

/* Flip Horizontal */
.flip-h {
	transform: scaleX(-1);
}
/* Flip Vertical */
.flip-v {
	transform: scaleY(-1);
}
/* Rotation */
.rotate-0, .rotate-360 {
	transform: rotate(0deg);
}
.rotate-45 {
	transform: rotate(45deg);
}
.rotate-90 {
	transform: rotate(90deg);
}
.rotate-135 {
	transform: rotate(135deg);
}
.rotate-180 {
	transform: rotate(180deg);
}
.rotate-225 {
	transform: rotate(225deg);
}
.rotate-270 {
	transform: rotate(270deg);
}
.rotate-315 {
	transform: rotate(315deg);
}

Combining flip and rotate classes can achieve most desired display transformations.

Disable CodeIgniter 4 Cache Handler During Development

We recently ran into an issue where CodeIgniter’s cache() produced unexpected results during development. Our problem was forgetting to change the cache handler from ‘file’ to ‘dummy’ outside of the production environment. So we do not have to remember to change the handler, we modified our App’s Config\Cache::handler as follows…

public string $handler = (ENVIRONMENT === 'production') ? 'file' : 'dummy';

That one simple “set it and forget it” change could have saved wasted time spent debugging the problem.

Update: While discussing this with other developers, one alternative would be updating the .env file.

Continue reading

Hide Bootstrap 5 Tooltips When Clicked

Here’s an easy way to hide Bootstrap tooltips onclick. Useful for focusable elements that do not change the current document’s location.

document.querySelectorAll('[data-bs-toggle="tooltip"], .add-bs-tooltip').forEach(element => {
	new bootstrap.Tooltip(element)
	
	element.addEventListener('click', event => {
		bootstrap.Tooltip.getInstance(event.currentTarget).hide()
	})
})

Bonus: Our .add-bs-tooltip selector is for eliminating the need to wrap other Bootstrap elements that already have the data-bs-toggle attribute (modals, dropdowns, etc). 😉

Block Top-Level Domains (TLDs) Using cPanel Email Filters

Initially we had a long list of “From ends with” filters like below trying cut down on our most common spam sources.

From ends with .ru
From ends with .pk
From ends with .in
From ends with .de
From ends with .jp
From ends with .kr

Recently I noticed those rules were effectively blocking those TLDs. It did not take long to figure out why. Here are example FROM headers email systems might encounter.

From: "John Doe" <john@example.com>
From: Jane <jane@example.com>
From: bob@example.com

The problem with using “From ends with” is the header frequently ends with a greater-than symbol. So we decided to use “From matches regex” instead.

Continue reading

Automatically Adding Default Headers to ALL Fetch (AJAX) Requests

Below is an unobtrusive way to ensure AJAX headers are set with every fetch() request, which compliments our post regarding fetch() wrappers.

const originalFetch = fetch
fetch = ((url, options) => {
	if (!options) options = {}
	Object.assign(options, { headers: { 'X-Requested-With': 'XMLHttpRequest' } })
	const promise = originalFetch(url, options).catch(error => { console.error(error) })
	return promise
})

Now every fetch() request includes the X-Requested-With: XMLHttpRequest header.

We ultimately ended up with the following alternative to avoid replacing other existing headers.

Continue reading

Add PATTERN Support to TEXTAREA for Validation

While unfortunate, by now it should be well-known that textarea elements do not support pattern attributes. Why W3C would omit that attribute for multiline text fields is beyond me… maybe it’s because we cannot specify flags in our patterns? 🤔

Since we really wanted our textarea to validate by pattern, and until/if such support is added to the HTML standards, we modified a basic Bootstrap Validation script to make it work with minimal changes.

Continue reading

How to fix “Blocked aria-hidden on an element because its descendant retained focus.” in console

We regularly run into this Chrome console accessibility warning implementing Bootstrap Modals. There is supposed to be a fix coming in a future release of Bootstrap, but until then, we want to eliminate the warning. Our solution should be adaptable to other packages and components generating the same warning.

Blocked aria-hidden on an element because its descendant retained focus. The focus must not be hidden from assistive technology users. Avoid using aria-hidden on a focused element or its ancestor. Consider using the inert attribute instead, which will also prevent focus. For more details, see the aria-hidden section of the WAI-ARIA specification at https://w3c.github.io/aria/#aria-hidden.
Element with focus: <button.btn-close>
Ancestor with aria-hidden: <div.modal fade#modal>

Continue reading

Automatically Resize TEXTAREA Using Vanilla JavaScript

I posted a while back about resizing TEXTAREA inputs with jQuery, but haven’t used jQuery for a very long time. Favoring vanilla JS for some time, here is how to do the same thing without a JavaScript library.

<script>
(() => {
    'use strict'
    
    document.querySelectorAll('textarea.h-auto').forEach(textarea => {
        textarea.addEventListener('input', event => {
            const style = window.getComputedStyle(textarea)
            const borders = parseInt(style.borderTopWidth) + parseInt(style.borderBottomWidth)
            textarea.style.height = 'auto'
            textarea.style.setProperty('height', `${textarea.scrollHeight + borders}px`, 'important')
        })
        
        textarea.addEventListener('keyup', event => {
            textarea.dispatchEvent(new Event('input'))
        })
        
        textarea.dispatchEvent(new Event('input'))
        
        window.addEventListener('resize', event => {
            textarea.dispatchEvent(new Event('input'))
        })
    })
})()
</script>

Notes

Displaying a progress cursor during all fetch() requests

While working on a system that uses frequent AJAX requests, and updates document.body.style.cursor before each fetch() and within finally(), I created this wrapper to eliminate repetition…

const originalFetch = fetch;
fetch = (url, options) => {
	document.body.style.cursor = 'progress'
	const promise = originalFetch(url, options).catch(error => {
		console.error(error)
	}).finally(() => {
		document.body.style.cursor = 'auto'
	})
	return promise
}

Usage

fetch(url).then(response => {
	if (response.ok) return response.text()
	throw new Error(`Response status: ${response.status}`)
}).then(text => {
	// do something
})

Before creating the wrapper our fetch() calls looked more like…

document.body.style.cursor = 'progress'

fetch(url).then((response) => {
	if (!response.ok) {
		throw new Error(`Response status: ${response.status}`)
	}
	
	return response.text()
}).then((text) => {
	// do something
}).catch((error) => {
	console.error(error)
}).finally(() => {
	document.body.style.cursor = 'auto'
})

Every… single… place… fetch() was being used 😒


Noticed this post sitting in my drafts… not sure why it was not published it, so here it is (better late than never I guess)

AI generated Image of a merdog (half fish, half dog).

Sloppy AI Code by Example: Automatically logging out inactive users.

We wanted an activity monitor to force logging off authenticated users after a certain period of inactivity. So naturally we searched the web to see how others implemented similar solutions. The prominent AI summary suggested the following, which we initially implemented.

<script>
let inactivityTimeout;

// Function to log the user out
function logOut() {
  alert("You have been logged out due to inactivity.");
  // Redirect to a logout page or perform logout logic
  window.location.href = "/logout";
}

// Reset the inactivity timer
function resetTimer() {
  clearTimeout(inactivityTimeout);
  inactivityTimeout = setTimeout(logOut, 5 * 60 * 1000); // 5 minutes
}

// Add event listeners for user activity
function setupInactivityTimer() {
  document.addEventListener("mousemove", resetTimer);
  document.addEventListener("keydown", resetTimer);
  document.addEventListener("click", resetTimer);
  document.addEventListener("scroll", resetTimer);
  resetTimer(); // Start the timer initially
}

// Initialize the inactivity timer when the page loads
window.onload = setupInactivityTimer;
</script>

While that gets the job done, the constant recycling of inactivityTimeout struck us as grossly inefficient. Considering how many times those events are triggered while interacting with pages, it was determined a poor solution.

We instead decided to use a Date variable, updated as each event is triggered, and monitored in reasonable intervals. This example below shows how we check that variable once a minute, determine if more than 10 minutes have passed since last activity, and automatically log out.

Continue reading

TinyMCE Update Element on Blur (onBlur)

How to make TinyMCE play nice with Bootstrap’s form validation example starter JavaScript when replacing textarea elements.

<script>
tinymce.init({
    selector: '.wysiwyg',
    setup: (editor) => {
        editor.on('blur', () => {
            editor.getElement().value = editor.getContent({ format: 'html' })
        })
    },
})
</script>

That setup was all we needed for .invalid-feedback elements to toggle during form submission without modifying the existing form validation script.

Bonus: Adjust the TinyMCE editor’s border color to correspond with .was-validated 😉

Continue reading

Using Google reCaptcha with forms that already have submit event handlers.

We had to jump through some hoops to get Google reCAPTCHA v3 to work with OpenCart 4.0.2.3 data-oc-toggle="ajax" form attributes. reCAPTCHA implementation is simple and straight-forward, and I really liked the ajax form processor instead of the post/response/redirect flow. I was not aware however that those new ajax forms were going to be a headache while writing a our grecaptcha module.

Other developers opt to populate the challenge response during page load, even though that can easily result in expired challenges, and is discouraged by Google. So we attempted various ways to call grecaptcha.execute() before form submission, but simply could not trigger our listener before other listeners that were added earlier in the DOM.

Continue reading