Tom Adler’s blog

программное отключение монитора

несмотря на то, что ночная москва довольно ярко светит в мое окно, мониторы все равно ярче, и я предпочитаю отключать их, когда ложусь спать. С другой стороны, не хочу отказываться от моментального включения в ответ на шевеление мышью. Но на них не делают аппаратной кнопки перевода в standby-режим, поэтому приходится делать это программным способом:

xset dpms force standby

снова о hover

в предыдущем посте я обещал рассказать о том, как можно с пользой применить метод .getDelayedHandlers() — избежать лишнего мельтешения на экране и быть более терпимым к ошибкам пользователя. Выполняя обещание, опишу метод .delayedHover(), который хотя и не применяется так широко, как «улучшенный hover», но всё равно нередко служит добрую службу.

сам по себе метод очень прост:

Element.Methods.delayedHover = 
function(element, handler, delay) {
    element = $(element);
    var delayed = handler.getDelayedHandlers(delay);

    var names = Prototype.Browser.IE 
        ? ['mouseenter', 'mouseleave'] 
        : ['mouseover', 'mouseout'];

    element.observe(names[0], delayed.handlers.over);
    element.observe(names[1], delayed.handlers.out);

    return delayed;
}

на мышиные события элемента навешиваются «отложенные» обработчики, созданные при помощи .getDelayedHandlers(). В результате исходный обработчик вызывается только тогда, когда есть уверенность, что mouseover или mouseout вызваны намеренно, а не из-за нетвёрдой руки пользователя. Применяется метод очевидным образом, например:

$('menu').delayedHover(function(hover){ 
    $('submenu')[hover ? 'show' : 'hide'](); 
});

впрочем, интереснее рассмотреть немного более сложный случай, когда какой-то причине условное «подменю» не вложено в «главное меню». Например, если вы используете один и тот же dom-объект для отображения подсказок к разным элементам страницы. Из соображений производительности неразумно каждый раз переносить его в новое место в дереве документа, гораздо быстрее просто абсолютно позиционировать его в новом месте. Но тогда мы столкнемся с другой проблемой — при наведении мыши на подсказку на исходном объекте случится mouseout, и подсказка исчезнет.

если взглянуть на ситуацию под другим углом, можно прийти к такому заключению: есть команда «показать подсказку», которая выполняется и при наведении на «непонятый» объект, и при наведении на саму подсказку. Есть противоположная команда, выполняющаяся при отводе мыши от объекта или подсказки. В нашем случае эти команды — delayed.handlers.over и delayed.handlers.out, причем они уже навешены на события исходного объекта. Осталось только назначать их событиям подсказки:

var sub = $('submenu');
$('menu').delayedHover(function(hover, delayed){ 
    sub[hover ? 'show' : 'hide'](); 
    var method = hover ? 'observe' : 'stopObserving';
    sub[method]('mouseover', delayed.handlers.over);
    sub[method]('mouseout',  delayed.handlers.out);
});

вуаля! С временны́ми параметрами по умолчанию (400мс/200мс) событие mouseout на исходном объекте не успеет произойти до того, как его отменит mouseover на подсказке. В результате подсказка никуда не исчезнет.

сходным образом .getDelayedHandlers() можно использовать для обнаружения даблкликов или эмуляции события input из грядущего html5.

да, конечно, этого же результата можно добиться и простыми setTimeout/clearTimeout, но предложенный метод мне кажется более наглядным.

, , ,

hardy heron beta

когда я вижу два человеческих органа, я сшиваю их друг с другом, и смотрю, что получится

— доктор Зойдберг

неудачно обновил убунту до следующей версии: в новеньком ядре по умолчанию нет звуковых драйверов, и модуля с ними тоже нет. Собираю альсу вручную из некомпилирующегося пакета. Слушаю под это дело музыку, подключив колонки к флешкоплееру, который без пользы носил с собой последний год.

осторожнее с сырыми бетками : )

отложенные вызов и отмена функции

интерфейс, который моментально реагирует на действия пользователя — это не всегда здорово. Людям свойственно делать мелкие ошибки и тут же исправлять их, поэтому нередко хорошей идеей оказывается снисходительность к таким ошибкам. В своём докладе на ClientSide'07 я рассказывал об этом подробнее (см. последнюю четверть текста), но формат устного выступления не предполагает внимательное изучение кода. Сейчас я хочу полнее рассмотреть одну из функций, позволяющих добиться отложенной реакции интерфейса

задача стоит следующим образом: спустя определенное время после возникновения какого-то события нужно выполнить определенный код, если с тех пор не случилось другое событие, отменяющее эту команду. Например, если пользователь быстро провел мышью над пунктом меню, событие mouseover должно показать подменю, но событие mouseout отменяет этот вызов. Разумно решать эту задачу симметричным образом, чтобы повторный вызов «включения» в свою очередь отменял предыдущее «выключение»

я реализовал эту функциональность методом функции, подобно .delay() или .curry(). Метод получает необязательным параметром объект, в котором передаются опциональные же значения задержек «включения/выключения». А для максимальной гибкости в использовании метод возвращает целых четыре значения в другом объекте. Два из них — это таймауты, возвращаемые глобальной функцией setTimeout, а два других — это зеркальные функции, отменяющие друг друга. Впрочем, код лучше объяснит себя сам! Вначале объявление метода:

Function.prototype.getDelayedHandlers = function(delay)

инициализация значений задержек:

delay = delay || {};
var overDelay = ('over' in delay) ? delay.over : 0.4;
var outDelay =  ('out'  in delay) ? delay.out  : 0.2;

очевидно, что over — задержка для «включения», а out — задержка «выключения». Теперь нам понадобится пара локальных переменных для сохранения в текущем замыкании:

var data = {timeouts: {}, handlers: {}}; 
var __function = this;

а вот и собственно зеркальные функции:

data.handlers.over = function() { 
    if (data.timeouts.out) 
        clearTimeout(data.timeouts.out);
    data.timeouts.over = 
        __function.curry(true, data).delay(overDelay);
};
data.handlers.out = function() { 
    if (data.timeouts.over) 
        clearTimeout(data.timeouts.over);
    data.timeouts.out = 
        __function.curry(false, data).delay(outDelay);
};
return data;

мы видим, что каждая из них предотвращает вызов другой, после чего планирует выполнение исходной функции через соответствующий интервал времени. Важно заметить, что исходная функция получает два параметра: флаг, означающий тип вызова — «включение» или «выключение», а также объект data, содержащий всю необходимую информацию о состоянии отложенных вызовов. Этот же объект потом возвращается из getDelayedHandlers

практическое применение этого метода я опишу в следующем посте, а напоследок дам полный код функции:

Function.prototype.getDelayedHandlers = function(delay) {
    delay = delay || {};
    var overDelay = ('over' in delay) ? delay.over : 0.4;
    var outDelay =  ('out'  in delay) ? delay.out  : 0.2;

    var data = {timeouts: {}, handlers: {}}; 
    var __function = this;

    data.handlers.over = function() { 
        if (data.timeouts.out) 
            clearTimeout(data.timeouts.out);
        data.timeouts.over = 
            __function.curry(true, data).delay(overDelay);
    };
    data.handlers.out = function() { 
        if (data.timeouts.over) 
            clearTimeout(data.timeouts.over);
        data.timeouts.out = 
            __function.curry(false, data).delay(outDelay);
    };

    return data;
}

, , ,

улучшенный hover

интерфейс, элементы которого разумно реагируют на наведение мыши, воспринимается гораздо лучше. Видимо, поэтому в CSS есть псевдокласс :hover. Однако, как мы все знаем, его реализация в IE6- оставляет желать лучшего — абсолютное большинство элементов не получают этот псевдокласс. Исправляя такое поведение при помощи javascript, мы можем заодно расширить возможности обычного hover

итак, задача стоит следующим образом: при наведении мыши на элемент нужно добавить ему класс hover или другой, заказанный верстальщиком; при уведении мыши с элемента этот класс нужно убрать; опционально при этом нужно вызывать какие-то функции для дополнительной обработки интерфейса. Значит, наш метод, помимо самого элемента, будет получать опциональными параметрами имя класса и функции-обработчики. Впрочем, разумнее будет сделать один обработчик, в который мы передадим тип происходящего события: mouseover или mouseout. И скорее всего он сам справится с добавлением/удалением класса.

объявление нашего метода выглядит так:

Element.Methods.hover = function(element, className)

вторым параметром может быть undefined, имя класса или функция, поэтому имя навешиваемого класса вычислим вот так:

className = (className instanceof Function) 
    ? 'hover' 
    : (className || 'hover');

обработчик по умолчанию будет добавлять или удалять класс в зависимости от своего параметра, который удобно сделать булевым значением, кодирующим mouseover в true, mouseout в false:

function(hover){ 
    element[hover 
        ? 'addClassName' 
        : 'removeClassName'
    ](className); 
}

но, поскольку обработчик можно подменить параметром, конечная функция вычисляется так:

var func  = (className instanceof Function) 
    ? className 
    : function(hover){ element[hover ? 'addClassName' : 'removeClassName'](className); };

теперь нужно упомянуть, что в IE, помимо стандартной пары событий mouseover/mouseout есть еще одна, больше подходящая для нашей задачи: невсплывающие mouseenter/mouseleave. Дело в том, что первые два события срабатывают на рассматриваемом элементе даже тогда, когда мышь пересекает границу дочернего элемента (см. event bubbling), что порождает огромное количество ненужных в нашем случае вызовов. Борьба с ними — отдельная тема, а в рассмативаемом примере мы просто будем использовать возможности IE:

var names = Prototype.Browser.IE 
    ? ['mouseenter', 'mouseleave'] 
    : ['mouseover', 'mouseout'];

теперь осталось только подключить обработчик к элементу, используя описанный в предыдущем посте карринг:

return element.
    observe(names[0], func.curry(true)).
    observe(names[1], func.curry(false));

в итоге полный текст метода с несколькими примерами вызова выглядит так:

Element.Methods.hover = function(element, className){
    element = $(element);
    var func  = (className instanceof Function) 
        ? className 
        : function(hover){ element[hover ? 'addClassName' : 'removeClassName'](className); };
    className = (className instanceof Function) ? 'hover' : (className || 'hover');

    var names = Prototype.Browser.IE ? ['mouseenter', 'mouseleave'] : ['mouseover', 'mouseout'];

    return element.
        observe(names[0], func.curry(true)).
        observe(names[1], func.curry(false));
};

element.hover();
element.hover('specific_hover_class');
element.hover(function(hover){ treasure[hover ? 'show' : 'hide']() });

, ,