Несколько интересностей и полезностей от веб-разработчика *

в 7:41, , рубрики: addeventlistener, balalaikajs, ballalaika, html, indeterminate, javascript, jquery, jstree, Matreshka, mousedown, procrastinate, vanillatree, Веб-разработка, куча тегов

* Надеюсь, ilusha_sergeevich не обвинит меня в плагиате.
Если что, пост переименую.

image
(Иллюстрация к donut.js)

Всем привет! За время работы постепенно накапливаются наработки, которыми можно было бы поделиться с сообществом. Но ни одна из этих наработок не тянет на большой полноценный пост. Поэтому я собрал все мелочи, что вспомнил, в одной статье: несколько простых опен-сорц проектов, пара советов и находок. Каждый из предложенных скриптов в этой статье поставляется как есть, под лицензией WTFPL (кроме Балалайки). С радостью приму пулл реквесты с исправлением багов или изменениями в README.

donut.js — микро-библиотека, рисующая бубликовые (donut) и круговые диаграммы

Во время работы над очередным проектом, появилась задача нарисовать много информативных бубликов на карте мира, и не просто нарисовать, а еще и поддержать ИЕ8, который, как известно, не умет SVG, а только безобразный VML. Первое, что приходит в голову, это Raphael. Порывшись некоторое время, я нашел это решение. К сожалению, автор проявил изобретательность простым хаком: на круговой диаграмме (pie chart) он нарисовал белый круг. Это решение не подошло, так как дырка бублика должна быть прозрачной. Изучение возможности рисования при помощи Raphael таких диаграмм мне показалось чересчур трудоёмким. Остальные скрипты на просторах интернета мне так же не подошли. Пришлось писать свой костыль, взяв за основу математику рисования арок этого проекта. Арки для VML версии нарисованы используя элемент arc.

var myDonutDiv = donut(options);


Функция donut возвращает div с классом donut, содержащий бублик в виде SVG или VML. Список опций:

  • el (Node) — куда вставлять бублик (опционально)
  • data (Array) — массив данных (объектов вида {value: 42, name: 'some name'})
  • size (Number) — диаметр булика (по умолчанию 100)
  • weight (Number) — толщина линии (диаметр бублика минус диаметр дырки) (по умолчанию 20)
  • colors (Array) — список цветов (по умолчанию, массив из одного элемента: ['#555']). Если цветов больше, чем данных, цвета повторяются.

Пример:

var myDonut = donut({
  el: document.getElementById( 'container' ),
  size: 150,
  weight: 30,
  data: [{
    value: 1,
    name: 'A'
  },{
    value: 2,
    name: 'B',
    customData: 'Yeah' // об этом ниже
  },{
    value: 3,
    name: 'C'
  },{
    value: 4,
    name: 'D'
  }],
  colors: [ '#80a8cc', '#da3b3e', '#ffa921', 'red' ]
});

Если толщина арки равна половине диаметра бублика, получается круговая диаграмма:
image

У функции donut есть два метода:
setColor(arc, color) — установка цвета для арки
data(arc[, data]) — получение или установка данных для арки

Аргумент arc — DOM узел (path для SVG и arc для VML), который можно получить, обратившись к элементу с селектором [data-name="имя"], где имя — значение name из данных.

B_Arc = document.getElementById( 'container' ).querySelector( '[data-name="B"]' )
donut.data(B_Arc).customData; // получение данных, в том числе, кастомных
donut.setColor(B_Arc, '#8dc700'); // установка цвета

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

text = myDonut.appendChild( document.createElement('span') );
text.className = 'donut-text';
text.innerHTML = '3';
.donut-text {
  position: absolute;
  left: 0;
  line-height: 150px; // диаметр бублика
  width: 150px;  // диаметр бублика
  text-align: center;
  font-size: 70px;
}

Позицию можно задать в CSS или используя свойство style.

Решение работает в восьмом осле. В устаревших ослах не проверялось, но у меня нет причин полагать, что в них не сработает. Скрипт не зависит от наличия других библиотек. Удобную реализацию вставки текста, тултипов и пр. оставляю за вами.

Живой пример
Репозиторий

Балалайка

Балалайка — крошечная jQuery-подобная DOM библиотека, ужатая до предела, имеющая малый, но достаточный для vanilla.js-пасанов набор методов. В её основу вложена идея того, что для «получения элемента по ID» не нужно подключать jQuery. Крошечный размер позволяет встраивать её куда угодно.

Например, вместо подключения jQuery, как основную библиотеку:

<script>
$=код_балалайки
</script>

Полный код

<script>
$=(function(n,e,k,h,p,m,l,b,d,g,f,c){c=function(a,b){return new c.i(a,b)};c.i=function(a,d){k.push.apply(this,a?a.nodeType||a==n?[a]:""+a===a?/</.test(a)?((b=e.createElement(d||"div")).innerHTML=a,b.children):(d&&c(d)[0]||e).querySelectorAll(a):/f/.test(typeof a)?/c/.test(e.readyState)?a():c(e).on("DOMContentLoaded",a):a:k)};c.i[f="prototype"]=(c.extend=function(a){g=arguments;for(b=1;b<g.length;b++)if(f=g[b])for(d in f)a[d]=f[d];return a})(c.fn=c[f]=k,{on:function(a,d){a=a.split(h);this.map(function(c){(h[b=
a[0]+(c.b$=c.b$||++p)]=h[b]||[]).push([d,a[1]]);c["add"+m](a[0],d)});return this},off:function(a,c){a=a.split(h);f="remove"+m;this.map(function(e){if(b=(g=h[a[0]+e.b$])&&g.length)for(;d=g[--b];)c&&c!=d[0]||a[1]&&a[1]!=d[1]||(e[f](a[0],d[0]),g.splice(b,1));else!a[1]&&e[f](a[0],c)});return this},is:function(a){b=this[0];d=!!b&&(b.matches||b["webkit"+l]||b["moz"+l]||b["ms"+l]);return!!d&&d.call(b,a)}});return c})(window,document,[],/.(.+)/,0,"EventListener","MatchesSelector");
</script>

(похоже на вставку какого-то счетчика, только немного массивнее)

Как известно, обычный GET запрос требует время и немалого трафика. Инлайновая вставка позволит сэкономить драгоценные миллисекунды до полной загрузки страницы.

Или в качестве локальной переменной для вашего скрипта:

(function($) {
  //...
})(код_балалайки)

Полный код

function($) {
    // your code starts here
    $(function() {
        $('.my-selector').on('click', function() {
            alert('I need my balalaika');
        });
    });
  // your code ends here
})((function(n,e,k,h,p,m,l,b,d,g,f,c){c=function(a,b){return new c.i(a,b)};c.i=function(a,d){k.push.apply(this,a?a.nodeType||a==n?[a]:""+a===a?/</.test(a)?((b=e.createElement(d||"div")).innerHTML=a,b.children):(d&&c(d)[0]||e).querySelectorAll(a):/f/.test(typeof a)?/c/.test(e.readyState)?a():c(e).on("DOMContentLoaded",a):a:k)};c.i[f="prototype"]=(c.extend=function(a){g=arguments;for(b=1;b<g.length;b++)if(f=g[b])for(d in f)a[d]=f[d];return a})(c.fn=c[f]=k,{on:function(a,d){a=a.split(h);this.map(function(c){(h[b=
a[0]+(c.b$=c.b$||++p)]=h[b]||[]).push([d,a[1]]);c["add"+m](a[0],d)});return this},off:function(a,c){a=a.split(h);f="remove"+m;this.map(function(e){if(b=(g=h[a[0]+e.b$])&&g.length)for(;d=g[--b];)c&&c!=d[0]||a[1]&&a[1]!=d[1]||(e[f](a[0],d[0]),g.splice(b,1));else!a[1]&&e[f](a[0],c)});return this},is:function(a){b=this[0];d=!!b&&(b.matches||b["webkit"+l]||b["moz"+l]||b["ms"+l]);return!!d&&d.call(b,a)}});return c})(window,document,[],/.(.+)/,0,"EventListener","MatchesSelector"));

Так как Балалайка наследуется от массива, в распоряжении разработчика наличествуют все методы массивов:

Кроме этого, у Балалайки есть jQuery-подобные методы on, off, is, extend и событие загрузки документа:

$('.my-selector').on('click.namespace', function() {
    alert('I need my balalaika');
});


$('.my-selector').off('click.namespace');

$('.my-selector').on('click', function(evt) {
    if($(evt.target).is('.another-selector')) {
        alert('I need my balalaika');
    }
});

var myObject = {a:1};
$.extend(myObject,{
    b: 2
});

$(function() {
    // Do something with DOM
});

Такой, на первый взгляд, скудный набор методов, позволяет делать многое. А из-за простоты кода и ванильных функций, балалайка сильно выигрывает у массивных библиотек в производительности.

Несколько примеров

Парсинг:

var elements = $('<div><span class="yeah"></span></div>');

Поиск одного элемента в другом:

var myElement = $('.my-selector', node);

Установка стиля:

$('.my-selector').forEach(function(el) {
    $.extend( el.style, {
        width: '30px',
        backgroundColor: 'red'
    });
});

Делегирование события:

$('.my-selector').on('click', function(evt) {
    var node = evt.target;
    while(node !== this) {
        if($(node).is('.delegated-selector')) {
            // Handle it!
            break;
        }
        node = node.parentNode;
    }
});

Простой плагин:

$.fn.addClass = function( className ) {
    this.forEach( function( item ) {
        var classList = item.classList;
        classList.add.apply( classList, className.split( /s/ ) );
    });
    return this;
};

Я не предлагаю отказываться от jQuery, просто не забывайте, что есть микро-библиотека со смешным названием. Балалайка используется во фреймворке Матрешка (статьи о Матрешке)

Библиотека поддерживается всеми браузерами, начиная с ИЕ9.
Ссылка на репозиторий

Функция procrastinate

Позвольте мне лениво процитировать самого себя.

Представьте себе следующую ситуацию (взята из моей практики). У вас есть форма с некими текстовыми полями: чекбосами и пр. Когда меняется значение одного из элементов формы, приложение должно отправить запрос на сервер, который, в свою очередь, возвращает данные для рендеринга трёх графиков. Рисование графиков — дело тяжелое для процессора и на слабом компьютере занимает полсекунды (Highcharts он такой). Теперь представьте пользователя, которому скучно и он решил многократно кликнуть на чекбокс. Что произойдет? Отправится куча запросов, вернется куча ответов, которые так же многократно отрисуют график. Что обычно делают в таком случае? Отменяют запрос на сервер. Спрашивается: зачем было этот запрос посылать, если можно было дождаться, пока тот угомонится? :)

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

var doSomethingHeavy = function( i ) {
	console.log( 'Ok', i );
};

var procrastinateSomethingHeavy = procrastinate( doSomethingHeavy );

for( var i = 0; i < 100; i++ ) {
	procrastinateSomethingHeavy( i );
}

// >> Ok 100

Код функции

var procrastinate = function ( f, d, thisArg ) {
	var timeout;
	if( typeof d !== 'number' ) {
		thisArg = d;
		d = 0;
	}
	return function() {
		var args = arguments,
			_this = this;
		clearTimeout( timeout );
		timeout = setTimeout( function() {
			f.apply( thisArg || _this, args );
		}, d || 0 );
	};
};

Метод, кроме «прокрастинирующей» функции, принимает задержку, и контекст в качестве аргументов. Задержка отвечает за то, на сколько миллисикунд будет отложен реальный вызов функции при очередной попытке её вызова.

А вот пример случая, когда функция никогда не будет вызвана (для лучшего понимания).

var procrastinateSomethingHeavy = MK.procrastinate( function() {
	console.log( 'Ok' );
}, 1000 );

setInterval( function() {
	procrastinateSomethingHeavy();
}, 500 ); // интервал меньше задержки

ссылка на gist

Состояние чекбокса indeterminate

image
А знали ли вы о том, что чекбокс имеет «среднее» состояние? Я не знал. Таки да, имеет! Оно задаётся исключительно с помощью JavaScript:

checkbox.indeterminate = true;

А обратиться к чекбоксу из CSS с таким состоянием можно используя псевдокласс :indeterminate.

Подробнее опсевдоклассе
Статья на английском (даже если не знаете языка, код говорит сам за себя)

Противный зловред

В поисках конкретного выпуска интернет-шоу This is Хорошо на сайте, выдающем себя за официальный (или действительно на официальном, но взломанном), я наткнулся на страницу «МВД Украины», где меня обвинили в просмотре порно с малолетками и прочим гадостям. На странице появилось незакрывающееся окно с текстом о том, что должен заплатить штраф, отправив СМС на определенный номер. Я и так и так пытался закрыть страницу, не получалось… В итоге, я откоючил сеть на ноуте и несколько раз перезагрузил Хром, чтоб очистить хеш. Естественно, мне стало любопытно, как скрипту удалось потрепать нервы опытному веб-разработчику, и я открыл эту страницу в прекрасном браузере, который называется links (если что, это консольный браузер).

Код зловреда был таков:

onbeforeunload=function(){
	location.reload();
	return "Ваш браузер був заблокований з міркувань безпеки.Вся інформації на вашому комп'ютері заарештована. Усі  ваші файли зашифровані.  Протягом 12 годин кримінальну справу буде передано до суду. Однак, у Вас ще є 12 годин, щоб заплатити штраф, - таким чином, Ви можете уникнути пред'явлення кримінальної справи."
};

onload=function(){
	location.reload();
}

Страница пытается перезагрузиться, и здесь срабатывает обработчик onbeforeunload, который так же пытается перезагрузить страницу, вызывая бесконечное появления глупого текста.

Как попробовать зловред у себя в браузере? Создайте локально HTML файл со слудующим содержимым:

<html>
  <head>
    <title></title>
	<meta charset="utf-8"> 
    <style></style>
  </head>
  <body>
	<script>
		onbeforeunload=function(){
			location.reload();
			return "Текст зловреда"
		};



		onload=function(){
			location.reload();
		}
	</script>
  </body>
</html>

Откройте страницу в Хроме (в Файерфоксе зловред не такой зловредный) и наслаждайтесь. Когда надоест, удалите или переименуйте файл.

Совет: mousedown вместо click

Хотите субъективно увеличить скорость срабатывания интерфейса? Замените событие click на mousedown, если это возможно (если нет драг-н-дропов и событий двойного щелчка). Это позволит избежать задержки в десятые или сотые доли секунды, прежде чем пользователь отпустит кнопку мыши. Пользователю вашего приложения покажется, что интерфейс работает шустрее, так как срабатывать код будет немного быстрее, чем моментально (исключая тяжелые операции и запросы на сервер).

Небольшая демка.

Совет: меньше используйте CDN

Запрос библиотеки — это сложный HTTP запрос. Разработчики с опытом это знают и кладут весь JS код в один файл с помощью Grunt, r.js или другого сборщика. Это позволит сэкономить время на лишних запросах и, часто, трафик, так как GET даже с ответом 304 весит немало. Кроме этого, CDN может не отвечать. Программисты часто решают это failback-ом, но это плохой вариант, так как браузер не сразу поймет, что сервер лежит, а пользователю прийдется наслаждаться загружающейся страницей несколько драгоценных секунд.

Редактор SuitUp и чистка WYSIWYG от мусора

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

Представляю небольшую функцию (тоже jQuery плагин, но с ванильным содержанием), которая с помощью методов DOM, без регулярок, рекурсивно чистит любой contenteditable блок от комментариев, от тегов XML, SCRIPT, STYLE, LINK, META, от атрибутов style и align. «Чистельщик» создан неделю назад, поэтому не исключены баги и неучтенные случаи грязного HTML.

jQuery( editor ).on( 'paste', function( evt ) {
	setTimeout( function() {
		jQuery( this ).mswordFilter();
	}.bind( this ) );
});

Ссылка на Gist

Простейший полифил для addEventListener

Восьмой осёл, как известно, не умеет метод addEventListener, у него есть небогоугодный attachEvent. Для программистов, которым не нужны кастомные события и прочие навороты, предлагаю свой небольшое полифил, используемый в Матрешке.

Ссылка на AMD версию
Сылка на обычную версию

Репорт об опечатках на сайте

Есть такая замечательная система оповещений об опечатках на сайте, называемая Orphus. Пользователь, найдя опечатку или неточность может выделить соответствующий текст и нажать Ctrl+Enter. Открывается окно, в котором юзер может ввести комментарий к ошибке, а разработчик, затем, логинясь на сайте Орфуса видит все оповещения и, если хочет, исправляет ошибки. Мне захотелось написать свою оповещалку для опечаток для страницы документации Матрешки. За основу я взял известный хак отправки формы на страницу гуглоформы, который не требует от разработчика поднятия собственного сервера.

Как подключить?
1. Создайте Гугл-форму с тремя полями: текст с ошибкой, комментарий пользователя, ссылка на страницу. Пример.
2. Используя веб инспектор, исследуйте форму. Нам нужно значение атрибута формы action и имена инпутов (атрибут name)
3. Подключите скрипт перед </body>
4. Запустите функцию:

typo({
    formURL: FORM_ACTION_URL,
    selectionName: NAME_OF_SELECTION_INPUT,
    commentName: NAME_OF_COMMENT_INPUT,
    pageName: NAME_OF_URL_INPUT
});

Например:

typo({
    formURL: 'https://docs.google.com/forms/d/1sQhv81wN65__7H4quwhDbecvtUxzAGZ-lMmlwF9MKcc/formResponse',
    selectionName: 'entry.1972481987',
    commentName: 'entry.1777335671',
    pageName: 'entry.339184258'
});

Репозиторий
Живая демка
«Опечатки» из демки будут появляться в этой таблице.

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

vanillatree — ванильная замена jstree

image
jstree — jQuery плагин, создающий вложенное дерево списков. Меня этот плагин не устроил сложным, громоздким API. vanillatree — компактная замена jstree, не требующая подключения какой-либо сторонней библиотеки. В список фич входит: кастомное контекстное меню для каждой ветки, выбор ветки открытие или закрытие дерева под веткой по клику (или используя методы), перемещение ветки, удаление ветки и соответствующие события.

Как использовать?

Подключите соответствующий JS и CSS файл и, когда DOM дерево будет готово, создайте инстанс VanillaTree

var tree = new VanillaTree(treeElement, options);

Первым аргументом должен быть элемент или селектор элемента, вторым — необязательные опции:
placeholder (String) — показывается, когда деоево пустое
contextmenu (Array) — список объектов, отвечающих за контекстное меню ({label: метка, action: действие})

var tree = new VanillaTree('.my-selector', {
  placeholder: 'None of leafs is added yet',
  contextmenu: [{
    label: 'Label 1',
    action: function(id) {
      // someAction
    }
  },{
    label: 'Label 2',
    action: function(id) {
      // someAction
    }
  }]
});

После инициализации, в дерево можно добавлять листья методом add.

tree.add({
  label: 'Label A', // текст ветки
  id: 'a', // ID ветки, опционально, если не задан, будет рандомным
  parent: 'b', // ID родителя (опционально)
  opened: true, // открыта ли ветка по умолчанию (опционально)
  selected: true // выбрана ли ветка по умолчанию (опционально)
});

Вот полный список методов:

  • add(options) — Добавляет ветку
  • move(id, parentId) — Перемещает ветку к другому родителю
  • remove(id) — Удаляет ветку с заданным id
  • open(id) — Раскрывает ветку
  • close(id) — Закрывает ветку
  • toggle(id) — Закрывает или открывает ветку в зваисимости от текущего состояния
  • select(id) — Выбирает ветку с заданным id

VanillaTree вызывает кастомные события

  • vtree-add — добавление ветки
  • vtree-move — перемещение ветки
  • vtree-remove — удаление ветки
  • vtree-open — открытие ветки
  • vtree-close — закрытие ветки
  • vtree-select — выбор ветки

«Ванильное дерево» использует метод dispatchEvent и конструктор CustomEvent, если это возможно, для генерации кастомных событий. ID ветки, на которой сработало событие содержится в объекте evt.detail. Все события всплывающие, т. е. вы можете слушать события конкретной ветки в родителе, родителе родителя,… и так далее, до document.

treeElement.addEventListener('vtree-open', function(evt) {
  info.innerHTML = evt.detail.id + ' is opened';
});

treeElement.addEventListener('vtree-close', function(evt) {
  info.innerHTML = evt.detail.id + ' is closed';
});

treeElement.addEventListener('vtree-select', function(evt) {
  info.innerHTML = evt.detail.id + ' is selected';
});

//...

Обратите внимание, что vanillatree использует Балалайку в качестве «локальной библиотеки». Взгляните на 197 строку.

Скрипт работает во всех современных браузерах, включая ИЕ10 (накануне я заменил dataset на getAttribute/setAttribute, a CustomEvent на initEvent). Для ИЕ9 нужен полифилл classList.

Ссылка на живой пример
Ссылка на репозиторий

Всем добра!

Автор: Finom

Источник

Поделиться

* - обязательные к заполнению поля