Tags:
I've spent the last couple of years learning German, and one of the things I've come to rely on greatly are tools to help select the correct characters that aren't immediately available on my standard QWERTY keyboard. Even beyond foreign languages, there are countless times when I've needed to use something that wasn't easily typed, so these tools have become invaluable.
Contents
Not All Tools Are Made Equal
Although every operating system has a built-in tool to do this, not all tools are as useful as they could be:
- Windows Charmap is very basic, and hasn't evolved very much at all since it's early days in Windows 3.11. You can pick the a font, a browse through the available characters, but it still doesn't have a search function.
- Macs have the Character Viewer, which behaves in a similar fashion to Finder, allowing you to drill down by category, or search for a keywoard in the description.
- Linux has a few options, but my go-to is KCharselect, which comes with KDE. It allows you to show a range of characters by category, but the search feature is an absolute killer, even bringing up similar characters when searching by a single letter.
While it is possible to install KCharSelect on Windows, it's not easy, and there's no guarantee it will continue to work with future versions of Windows. This leaves web-based tools to plug the gap for most people, but I found that even the best of these had the same limitations that the Mac Character Viewer had: it wouldn't suggest alternative forms of letters for a search.
Building A Character Picker
A tool like this is actually very simple, and the key is the search feature itself. I decided on splitting this into 2 parts, a search running on the backend, and a front-end interface to show the results.
Search Backend
Although the search feature seems simple, it's deceptive, and there was one large problem: searching for letters.
Conider a typical use case, where we want to find and copy the letter ä (an a with an umlaut). I know what it looks like, but I can't remember the exact name of it.
In the back end I achieve this using 2 queries, one for single character searches, and another for anything longer. The long search is pretty simple and uses a standard wildcard LIKE
in SQL to find all matches in the description
field of the database:
SELECT *
FROM characters
WHERE description LIKE "%face%"
The single character search is a little bit more complicated though:
SELECT c1.*
FROM characters AS c1
WHERE c1.decimal_code = 97
UNION DISTINCT
SELECT c2.*
FROM characters AS c2
WHERE BINARY c2.description REGEXP " a( |\\b|$)"
UNION DISTINCT
SELECT c4.*
FROM characters AS c3
INNER JOIN characters AS c4 ON c4.description LIKE CONCAT("%", c3.description, "%")
WHERE c3.decimal_code = 97
There's a lot to unpack here, but really it's just showing the result of 3 separate queries together:
- The first query looks up the exact character by its code, which I get with
mb_ord($character, 'UTF-8')
- The second query looks to find a match using a regular expression match using the letter we're searching for. This ensures that we don't accidentally match descriptions where that letter is part of a word.
- Finally, we repeat the search from step one, but instead of using the results directly, we use the description and search again using that instead. So, the description for 'a' is Latin small letter a, which we plug back in. One of the results that comes back is Latin small letter a, diaeresis, which is the exact character we wanted!
Frontend
I needed the frontend of this to be fairly light, as it doesn't really do anything all that complicated, so React seemed like an obvious choice.
I split the frontend into 4 components:
- Charselect - this is the main app which is responsible for showing other sub-components based on state, such as only showing a list of matching characters when a search has been completed.
- CharselectSearch - this is for showing a search form and handles the request to the backend if a search is made. I used a timeout here for debouncing to reduce the overall number of searches being made because I was performing searches when the search field value was changed.
- CharselectCharacter - this is a repeated component, used once for each character match found and returned by the backend. It does nothing much beyond bubble events when a character is clicked on.
- CharselectCharDetails - this component shows more information on a character, such as the full name and a list of representations to allow you to more easily use it escaped in HTML or CSS. It does also have copy to clipboard functionality, which is done with the following Javascript:
copyToClipboard(event) {
let textarea = this.createTextarea(this.props.symbol);
this.addTextareaToPage(textarea, event.target);
textarea.select();
document.execCommand('copy');
this.removeTextareaFromPage(textarea, event.target);
// selecting text in this temporary textarea moves focus and leaves it in a weird state,
// so we have to manually move it back to where it came from
this.reFocusTriggerElement(event.target);
}
createTextarea(text) {
let textarea = document.createElement('textarea');
textarea.setAttribute('aria-hidden','true');
textarea.setAttribute('class', 'accessible-hidden');
textarea.value = text;
return textarea;
}
addTextareaToPage(textarea, focusTriggerElement) {
focusTriggerElement.parentNode.insertBefore(textarea, focusTriggerElement);
}
removeTextareaFromPage(textarea, focusTriggerElement) {
focusTriggerElement.parentNode.removeChild(textarea);
}
reFocusTriggerElement(focusTriggerElement) {
focusTriggerElement.focus();
}
I chose not to use the Clipboard API as I found it had limitations in the support across browsers.
The Final Result: Charselect
The final result has some rough edges, but it's working beautifully. If you want to play around with it try it out, and please let me know if you find any issues with it.
Comments