Archive for the ‘Javascript’ Category

The question came up about using "European" dates (day/month/year) rather than "American" dates (month/day/year) in flexcal. The biggest problem is that the built-in Date.parse() (which is used by new Date(string)) uses the American format, at least with my browsers and settings.

The code in flexcal that does the formatting are in the following methods:

format({Date})
Converts a Date into a String. Used for external formatting: it determines what string is put in the input element after the user selects a date.
_date2string({Date})
Converts a Date into a String. Used for internal formatting: the rel attribute of each day link in the calendar is set with this.
_createDate(d, oldd)
Attempts to coerce d into a Date. If it is a valid Date, just returns that. Otherwise returns new Date(d). If that does not create a valid Date, returns oldd.
this._createDate(this._date2string(d)) must equal d in order for the calendar to work, and this._createDate(this.format(d)) must equal d for the calendar to be able to parse the string in the input element.

So to use European dates, we have to replace each of those methods. I'll use the "dd.mm.yyyy" format.

<input id="eurodate" value="01.02.2014" /> Should start as February first, not January second.

function euroformat (d){
  return d.getDate()+"."+(d.getMonth()+1)+"."+d.getFullYear();
}
$.widget('bililite.euroflexcal', $.bililite.flexcal, {
  format: euroformat,
  _date2string: euroformat,
  _createDate: function (d, oldd){
    // converts d into a valid date
    oldd = oldd || new Date;
    if (!d) return oldd;
    if (typeof d == 'string' && d.match(/(\d+)\.(\d+)\.(\d+)/)){
      d = RegExp.$2 + '/' + RegExp.$1 + '/' + RegExp.$3; // convert to American style
    }
    if (!(d instanceof Date)) d = new Date(d);
    if (isNaN(d.getTime())) return oldd;
    return d;
  }
});
$('#eurodate').euroflexcal();

There have been a lot of times I have needed some information for a bililiteRange plugin that was associated with the underlying element, rather than a specific range. For instance, in bililiteRange(element).text('foo') then bililiteRange(element).undo() the undo needs to know about the previous text change. jQuery has a data() method that attaches an object to the element and you can add fields to that object. Actually, it only attaches an index that points to the actual object, since at least in some browsers the garbage collector had trouble with Javascript objects attached to DOM elements and you ended up with memory leaks. I'm not sure if that is still a problem, but it's an easy enough pattern to implement so I used it.

I didn't want to be jQuery-dependent and I wanted to be able to some more sophisticated things with my data, so I implemented my own. At its simplest, just use:

var data = bililiteRange(element).data();
data.foo = 'bar';
assert(bililiteRange(element).data().foo == 'bar');

bililiteRange(element).data() returns an object that you can add fields to and they will be saved across multiple calls to bililiteRange.

Continue reading ‘bililiteRange data’ »

Someone asked about adding a "today" button/link to flexcal. It's actually pretty simple:

<input id="date1"/>

$('#date1').flexcal({'class': 'multicalendar', calendars: ['en','he-jewish']});
var todaylink = $('<a>Today</a>').attr({
  'class': 'commit',
  rel: $.bililite.flexcal.date2string(new Date),
  title: $('#date1').flexcal('format', new Date),
  style: 'text-decoration: underline; cursor: pointer'
});
$('#date1').flexcal('box').find('.ui-flexcal').append(todaylink);

The key is the todaylink which I append to the calendar (the $('#date1').flexcal('box').find('.ui-flexcal') line). The underlying code uses the rel attribute to determine what day a given <a> in the calendar should go to; use $.bililite.flexcal.date2string(d) to get the right format for a given Date object. The $('#date1').flexcal('format', d) is the displayed format for a given date; you can override that (say to use a European day-month-year format).

The class of the link determines what do to: 'class': 'commit' sets the date in the input box and hides the datepicker; 'class': 'go' would just have the datepicker display that date.

I liked the way Dabblet does autoindenting (entering a new line copies the whitespace from the beginning of the current line, so you keep the same level of indentation). So I added an option to bililiteRange(element).text() to do that. Now bililiteRange(element).text('text to insert', select, true) with true passed as the last option will autoindent. My Prism editor now has a check box to implement that.

The code to do this is in the bililiteRange utilities, not the original code.

I also added two more bililiteRange plugins:

bililiteRange(element).indent(tabs)
Prepends the string tabs to each line that contains part of the range. Thus bililiteRange(element).bounds('selection').indent('\t') to indent by one tab (and if you want spaces, use those instead; I won't get into Holy Wars).
bililiteRange(element).unindent(n, tabSize)
Removes n tab characters or sequences of tabSize spaces from the start of each line.

And, inspired by jQuery data, added a bililiteRange(element).data() that returns an object tied to element that can be used to store any data on that element (not the bililiteRange) without memory leaks. Thus bililiteRange(element).data().tabSize = 4 can be used in future calls: assert(bililiteRange(element).data().tabSize == 4). In fact, unindent above does exactly that if tabSize is not passed in.

I was just working on adding autoindenting to bililiteRange, and actually took advantage of the fact that I had an automated test harness in place for that library. So I actually used test-driven development: write the tests for the code that doesn't exist yet, then write code until they pass. It's an odd way of thinking, but I realized that it was more fun than my usual development cycle (remember, I'm a hobbyist, so if it ain't fun, I don't have to do it):

  • Think of problem that needs solving (interesting)
  • Write code to solve problem (interesting)
  • Write demo/test code (sort of interesting)
  • Run test-fail test-mutter at code-recode debugging cycle (frustrating)

With TDD, it's more like:

  • Think of problem that needs solving (interesting)
  • Write demo/test code (sort of interesting)
  • Run test-fail test-Write code to solve problem cycle (interesting)

The tedious "debugging" phase is swallowed up in the interesting "write code to solve problem" phase and I enjoy it a lot more. There's still some tedious debugging if the test doesn't work, but the test code is simpler than the "production" code and generally easier to debug. I have had some problems getting my head around testing asynchronous code, but that probably means I need to simplify the whole system.

Now I need to learn the discipline to keep to this style of development and I'll be a happier hacker.

I've finally decided to join the 21st century and use automated testing on my bililiteRange code; I'm using QUnit, with tests on github. I want to bililiteRange test on all three types of editable elements, <input type=text>, <textarea>, and <div contenteditable>, so I created a convenience function to run each test on each type of element, with

var elements = ['div', 'textarea', 'input'];
function multitest (message, fn){
  elements.forEach( function(name){
    test(message, function(){
      var $el = $('<'+name'>').attr('contenteditable','').appendTo('#qunit-fixture');
      fn($el); // the testing function
    });
  });
}

And the testing code calls multitest rather than test.

I feel so modern; maybe I'll start complaining about waterfalls next.

Playing with the kavanot editor, I wanted some way to encapsulate the process of noting that the text is "dirty"—meaning has been changed but not yet saved, and using a Promise to express the idea that the text is soon-to-be-saved, then saved.

I created a plugin that simply represents a state machine with four states, clean, dirty, pending, and failed. So you can do

var monitor = $.savemonitor();
$('textarea').on('input', function() { monitor.dirty() }); // track when the text area changes
// call this function to save (say, as a click handler for a Save button
function save(){
  var text = $('textarea').val();
  monitor.clean($.post('saveit.php', {data: text})); // jQuery Deferred's like $.post can be cast to Promises
}
// and keep track of the status
monitor.on('clean', function() {console.log('saved')})
monitor.on('failed', function() {console.log('not saved')})

See the code on github.

See a simple demo.

See a demo that uses FontAwesome icons and randomly fails to simulate a network failure.

Continue reading ‘New jQuery plugin, savemonitor’ »

I use Range and Selection extensively in bililiteRange in regular elements in standards-based browsers. I initially implemented scrolling a range by inserting an empty element, using scrollIntoView on that, then deleting the element.

But it turns out adding and removing elements messes up the selection. So I had to change it to manipulate scrollTop directly.

Continue reading ‘Odd bug with document ranges and selections’ »

One of those ongoing unsolved problems with web apps is how to keep track of the selection or insertion point in an editing element, when the focus has moved onto some other element. For instance, in a rich-text editor, you want to be able to select a word, then click the "bold" button and make that word bold. But once you click the button, the original word is no longer selected!

One solution that is used by Yahoo mail (and I believe Gmail as well) is to put the editing element in its own <iframe>. That acts as a separate window, and the selection is kept even when that "window" is no longer active. You can then get the selection with window.getSelection() and the like (look at the source for bililiteRange._nativeGetSelection() for the variations on that in different browsers and elements). That's a perfectly good solution, but I would like to be able to use any element on the current page.

There are two related issues we need to deal with:

  1. We want to know what the selection was when the element was active, after the element loses focus.
  2. We want to be able to restore the selection when focus returns to the element.

In each case, we have to deal with standards-based browsers in input and textarea elements (which behave identically) and other elements, and with Internet Explorer 8 ("we" here means just me, unfortunately).

Finding the selection

For input elements, that's easy. The browsers maintains element.selectionStart and element.selectionEnd. For the others, we have to somehow catch the element before it loses focus and grab the selection then. Internet Explorer actually does this right; it fires a beforedeactivate event, so we can use element.attachEvent('onbeforedeactivate', function() { saveSelection(document.selection) }. I don't like IE's nonstandard nomenclature, but at least the event exists.

In standard browsers, there's nothing. The blur event fires after the focus is lost, so the selection is lost. I can't find anything else to do but listen to every user interaction and record the selection each time, and remember to programmatically save the selection whenever it is changed in code: element.addEventListener('mouseup', function() { saveSelection(window.getSelection() }) and similarly for keyup. I'm pretty sure that those are the only two user interactions that browsers fire; all the others, like cut or drop, also fire one of those. It's inefficient to have to check the selection so often, but I don't see another option.

Restoring the focus

With the selection in hand, we can manipulate the text as desired, but when we return the focus to the text, we would like the selection to be in the right place. It's easy enough to do element.addEventListener('focus', function() {window.getSelection().addRange (savedSelection)}) or the equivalent in the other use cases, but that sets the selection even when we don't want it to.

There are three ways to put the focus in an element: click in it, tab into it, and in script with element.focus(). With a mouse click, we want the selection to be where the user clicked, not where it used to be. The effect of tabbing into an element varies (see below), but I would like the selection to be restored. So the only question is whether the focus event happened as the result of a click or not.

Unfortunately, the focus event happens before the mouseup or click events, and the change of the selection happens before that (even for input events that record their own selections), so I can't listen for mouseup. The only solution I found was to check whether the mouse button is down at the time of the focus event:

element.addEventListener('focus', function() {
  if (mouseButtonIsDown) window.getSelection().addRange (savedSelection)
})

But how to get mouseButtonIsDown? There is no way in Javascript in modern browsers. It's a hack, but I had to track mousedown and mouseup:

mouseButtonIsDown= false;
document.addEventListener ('mousedown', function() {
  mouseButtonIsDown = true;
});
document.addEventListener ('mouseup', function() {
  mouseButtonIsDown = false;
});

Ugly, and fails in all sorts of edge cases when the user hits more than one mouse button or drags from one window to the next. But it's not critical; the user gets immediate feedback where the selection/insertion point is.

The other subtlety is tabbing in; I'd like to restore the selection rather than use the browser's default of selecting the whole thing or placing the insertion point at the beginning. With the focus event listener above, there's a flash as the insertion point goes to the default position then moves to where I want it. I don't know how to avoid that.

Conclusion

Putting the pieces together, I can keep the selection in the right place even with other elements programmatically manipulating it. See the source code for bililiteRange.select() and bililiteRange.bounds('selection'), along with all the event listeners in the bililiteRange constructor.

To watch it in action, see the sendkeys demo. Enter text in each box, then click or tab between them or click the radio buttons above them to change the focus with Javascript.

Postscript

Browsers vary in how they handle focussing in response to a tab key or an element.focus(). I tested it on my Windows XP machine:

<input> <textarea> <div contenteditable>
Firefox tab all saved start
Firefox focus() saved saved start
Chrome tab all start start
Chrome focus() saved saved start
IE 8 tab all end start
IE 8 focus() start start start

"All" means the entire text is selected, "start" means the insertion point is placed before the first character, and "saved" means that the selection is restored to where it was when the element lost focus.

I've been looking at the DOM 3 event model and decided that the bililiteRange.text() andbililiteRange.select methods ought to implement those, even though they're not in browsers yet. Nobody implements beforeinput, or event.data in the input event handler, and select only works in <input> and <textarea> (not in contenteditable). And that's in modern browsers.

But that's no excuse for me. beforeinput is now dispatched before changing the text, and input after, with event.data set to the text to be inserted, and event.bounds set to the bounds array in the original text (this last is not part of the spec, but it's hard to work with changing text without it). This is an incompatible change from the way it used to work (with event.detail being set to {text: text, bounds: bounds}), but I don't think that many people are using that aspect of the library.

I also moved a few methods from the utilities library into the main code, since I used them so much.

It doesn't look like my workplace is ever going to get past IE8 (many of the machines are still on Windows XP!), so I really want to support those. There are polyfills for getting the event listeners to work, and I incorporated a quick and cheap version of that into the code, and event listenders to dispatch input events on keystrokes, drag and drop, and cut/paste.

bililiteRange is now at version 2.0.

I will be forever grateful when I can drop IE8.