I've noted in the past that the CSS pseudoelements represent inserted content in the element (i.e. span::before refers to a virtual element inserted within the <span> before the rest of its contents, not to content that is inserted before the <span> itself. That makes it useful for things like decorative guillemets for next and prev links ( a[rel=next]::before { content: '« '; } a[rel=prev]::after { content: ' »'; } ) (note using rel rather than classes to keep everything really semantic).

But you can insert content even in elements that can't have content, like <hr>. See the last two examples on CSS tricks; especially the last one that sticks a glyph in the middle of the horizonal rule:

hr {
    text-align: center;
}
hr::after {
    content: "§";
    display: inline-block;
    position: relative; 
    top: -0.7em;  
    font-size: 1.5em;
    padding: 0 0.25em;
    background: white;
}

Or you can use the pseudoelement to create custom underlines. text-decoration: underline gives a solid underline in the text color. border-bottom gives far more options, but is generally too far away from the text for my taste. You can increase the distance with padding-bottom but you can't use negative values to decrease it. Instead, use a full-width ::after element with a bottom border, and move that:

a{
     position: relative;
     text-decoration: none;
}
a::after{
     position: absolute;
     left: 0px;
     bottom: 6px; /* or whatever looks right */
     content: '';
     width: 100%;
     border-bottom: 1px dotted green
}

Or for numbering paragraphs, use a CSS counter with the ::before element (this is what I used for my line numbering function, and for numbering quotes on kavanot.me):

p {
	margin-left: 10em;
	position: relative;
	counter-increment: p-counter;
}
p::before {
	content: counter(p-counter);
	font-weight: bold;
	position: absolute;
	left: -2em;
}

Typography

Butterick has a few rules for block quotations: Reduce the point size and line spacing slightly, and indent the text block be­tween half an inch and a full inch on the left side, and op­tion­al­ly the same on the right.

Since I'm using both Hebrew and English quotes, I'll indent on both sides. Rather indenting with margin, I set the width and use margin: auto to center it. That way, if the quote ends up wider than the fixed width (see the issues with comparing texts, below), it will remain centered.

blockquote {
  font-size: 92%;
  line-height: $linespacing;
  width: $linewidth - 1in;
  margin: 0 auto;
}

Note that I'm using SCSS, so I can use variables and math. I have to repeat the line-height since what is inherited is the actual amount of the line height, not the relative amount (I set line-height: 1.35 on the <body> which is 21px, but I want the <blockquote> line-height to be 1.35 times its own font-size).

Continue reading ‘Using <blockquote>’ »

I haven't been blogging or programming a whole lot over the past year, since I started teaching two Tanach (Bible) classes. Even two hours a week, irregularly, represents a lot of work, and pretty much has sucked up all my intellectual free time. So not much Javascript. But I have been recording the notes (sort of condensed essays, 1000–2000 words) online so I could refer to them, and I've learned something about creating a website that is text, not data or doing-stuff, oriented. The site is kavanot.me; the classes are Shiurim and Parasha.

I don't expect any reader of this blog to understand the Hebrew or the details of Biblical exegesis, but these notes are more about design issues and resources that would be relevant to almost anyone.

Continue reading ‘Writing and the Web’ »

I wanted to be able to wrap arbitrary text with HTML elements, so I added a wrap method to my bililiteRange routines. Now I can do something like bililiteRange(el).find(/foo/).wrap(document.createElement('strong')) and I have instant highlighting!

It obviously can only work if the range is defined on an actual HTML element, not on the text of a <input> or <text> element (it throws an error if you try).

For standards-based browsers, there is a simple surroundContents method that does it in one step. Note that it uses the actual node passed in, so if you need to reuse it, remember to clone it.

IE < 10 doesn't have anything so nice, so I have to hack it by extracting and re-inserting the HTML with htmltext and pasteHTML.

It is possible to attempt to create invalid HTML; for instance wrap a range containing a <p> with a <span> element. Depending on the browser, this may throw an error or create undefined results.

Turns out Firefox uses a different keycode for the ;/: key from Chrome. I thought jQuery was supposed to normalize everything across browsers! Added the correct keycodes for that key to the keymap plugin. It's now at version 1.2.

Fixed a bug where the selection was not set correctly (a regression from when I updated bililiteRange). It's now at version 2.2.

This post is obsolete; the $.keymap has changed significantly. Documentation is now at http://bililite.com/blog/2015/01/12/rethinking-keymap/.

Inspired by John Resig's hotkeys, I created a version that uses the modern on, and uses an object rather than a string for the keys (which would be confused with a selector). It also uses my $.keymap codes.

It's written as a plugin to $.keymap.

Download the code.

See the demo, or the demo with regular expressions.

It's part of my bililiteRange repo on github.

Usage

This modifies the event handling code so that you can listen for specific keys or sequences of keys (note that you can use keyup as well as keydown, but that is less useful):

$('input').on('keydown', {keys: '^A'}, function() {alert('Control A was pressed')} );

The original $('input').on('keydown', function() {alert('A key was pressed')} ); is unchanged.

Remove the handler with $('input').off('keydown', {keys: '^A'} );. Use exactly the same keys expression as when the handler was attached.

It handles selectors correctly (I think):

$('body').on('keydown', {keys: '^A'}, function() {alert('Control A')} );
$('body').on('keydown', 'input', {keys: '^A'}, function() {alert('Control A was pressed in an input')} );

Attaches the handler for <body> and keys ^A, then adds a handler that is only triggered when the target element is an <input> and the event bubbled up to the <body>.

Remove selector-specific handlers with $('body').off('keydown', 'input', {keys: '^A'} );.

Note that the handler function is replaced internally, so removing handlers with $('body').off('keydown', handler); will fail. It's not a good idea anyway (see the documentation); use namespaces instead ($('body').off('keydown.myinstance');).

Sequences of keystrokes are indicated by a space-delimited list:

$('body').on('keydown', {
    keys: '{up} {up} {down} {down} {left} {right} {left} {right} b a'
  }, 
  function() {alert('Konami!')}
);

Listens for the Konami code.

Normally, when listening for a sequence, the keydown events that match are discarded. If you want them to be passed to other handlers, use allowDefault:

$('body').on('keydown', {
    keys: '{up} {up} {down} {down} {left} {right} {left} {right} b a',
    allowDefault: true // the arrow keys will still work
  }, 
  function() {alert('Konami!')}
);

To prevent other handling on the final event of a sequence, use the usual preventDefault or return false in the handler.

Regular Expressions

In addition to specifying the keys with a string, you can use a regular expression:

$('body').on('keydown', { keys: /\d/ }, 
  function(event) {alert('A digit key was pressed:' + event.hotkeys)}
);

The actual key or sequence of keys is returned in event.hotkeys. Note that passing the keys as a string will "normalize" the keys with $.keymap.normalize to match that returned by $.keymap (i.e. {keys: 'ctrl-a'} will work). Passing a regular expression needs to be normalized (i.e. {keys: /ctrl-\w/} will not work; use {keys: /\^\w/}). Multiple keys need to be separated by a single space ({keys: /\d \d/} to catch two digits in a row). This will succeed on the first sequence of characters matched, with no look-ahead, so you can't do {keys: /(\d )+/} to match multiple digits; it will succeed on the first digit pressed.

Events

The plugin also triggers two other events: 'keymapprefix' when the event is the prefix of a valid sequence, and 'keymapcomplete' when a sequence is matched. Both are sent with an additional parameter indicating what keys were pressed so far (in $.keymap notation). So:

$('body').on('keymapprefix keymapcomplete', function(event, keys){
	alert ('sequence pressed: '+keys);
})

More Information

Understanding special event handling in jQuery is very complicated; there is an article on learn.jquery.com that is invaluable. But this is one area where you will have to go through the source code. No way around that, but it's a very good way to get your brain about the way jQuery works, in a deep way.

Well, I've finally decided to join the 21st century and get my projects on github. Some of them are useful enough that others have expressed an interest in extending them, and it will force me to have the discipline to use version control consistently. The projects I have on there are:

bililiteRange
Includes the original bililiteRange (for which I know there are some bugs, especially in IE and Opera; another advantage of github is to allow others to formally raise bug reports) and sendkeys, plus other associated libraries and jQuery plugins.
jquery-css-parser
My CSS parser, along with the demo page and ancillary functions.
flexcal
My jQuery UI date picker for multiple calendar systems, along with timepickr, a mouse-friendly time picker, and their dependencies.

Edited 2013-12-11 to use focusout rather than blur event

Continuing my adventures in developing an online editor, I wanted to be able to do "live" searching: having the text scroll to the sought string as it is being typed. I can use my bililiteRange utilities to find an arbitrary regular expression, but after much hair-pulling, realized that my texthighlight plugin is just too flaky and hack-dependent to be used reliably. So I went a different route: instead of trying to highlight strings in a <textarea>, I overlay a <pre> element desinged to look similar enough to the <textarea> and highlight the string in that with normal HTML. When the user is done typing his search string (signaled by a blur event on the <input> element being typed in), remove the <pre> and select the desired string in the original <textarea>. There's a jump but not as bad as with texthighlight.

Download the code.

See a simple demo, a demo that uses the parameters, and a demo that searches three elements at once.

Continue reading ‘New jQuery plugin, livesearch’ »

I liked the hack that I used in the bililiteRange utilities demo to highlight the ranges in a textarea: creating a "shadow" preelement and using that to set the background of the desired text. It's based on this trick (explained in a ddforum post) by someone with a screen name of Trinithis.

I put it into a jQuery plugin, $.fn.texthighlight(bounds, id, color) that lets you quickly highlight a section of text in a textarea that will scroll with the text (having the highlighting stay with the text as it's edited is a different story; see the source for the bililiteRange utilities demo for that). It is a hack, and remains flaky (especially for Internet Explorer, for which I cannot get white-space: pre-wrap to work consistently).

Download the code.

See the demo.

Parameters

bounds
{Array [start, end]} | "remove" | "delete". Required. The starting and ending character positions to highlight. "remove" removes the given highlighter, and "delete" removes all of them and cleans up the wrapping element and data.
id
{String}. Optional; if falsy, uses "default". An arbitrary string to identify the highighter for future changing or removing.
color
{String}. Optional; if falsy, uses "#f0f" (a nice, bright magenta:    ). The CSS color to use for the highighting (at 0.25 opacity).

Events

After highlighting, triggers a "texthighlight" event, with parameters [span, bounds] where span is a jQuery collection with one item, the highlighter elemeent, and bounds is the array originally passed into the plugin. So $('textarea').on('texthighlight', function (event, span, bounds) {span[0].scrollIntoView()}) will scroll the highlighted area to the top of the window whenever the highlighter is set.