Category Archives: javascript

Комментарии VKontakte/Facebook – перехватываем и отправляем нам на почту

Добавляем виджеты комментариев VK/FB на свою страничку

Создаём приложения

Добавить комменты VK/FB на свой сайт относительно просто – создаём соотв. приложения в VK/FB, получаем их id. Вот адреса страничек для создания приложений: https://vk.com/dev/widgets_for_sites для ВК и https://developers.facebook.com/apps/ для Фэйсбук.

Подробно на этой теме останавливаться нет смысла, так как у них меняется всё очень часто, я имею в виду всякую воду и интерфейс, и каждый раз вникать в одно и тоже под другим соусом не особенно хочется. Но принцип видимо остаётся одним и тем же, и созданное нами несколько лет назад приложения продолжают работать. Честно говоря, назвать приложением это от FB/VK у меня язык с трудом поворачивается, скорее это надстройка над виджетами, контроллер виджетов точнее. Главное – они обеспечивают связь нашего сайта с многотысячной армией пользователей этих популярных сетей.
Continue reading

Node.js: пишем простой сервер с middleware, используя Connect

Предисловие. Пара слов о том, почему Node.js

Да, Node.js – это Вещь (с большой буквы, как и написано).
Чем больше я читаю и узнаю о нём, и пробую его, тем больше он мне нравится. С Ruby и Python‘ом пока не складывается (все мои проекты были на PHP, поэтому поддержка новой платформы была не актуальна – это учить новые языки, новые библиотеки, соглашения, короче, всю платформу, тогда как на PHP я программирую более-менее активно уже более 10 лет).

Но вот пришёл Node.js, который может заменить собой полностью серверную платформу, и он использует язык, на котором я также программирую свыше 10 лет – великий и ужасный JavaScript.

Причём пользоваться им легко и приятно, как оказалось, с учётом npmnode package manager‘а, который предоставляет быстрый и удобный доступ к тысячам или десяткам тысяч пакетов для Node.js с учётом всех зависимостей и прочая и прочая.

Пишем собственный (custom) middleware

Итак, давайте сваяем простой сервер на Connect, используя добавочно свою middleware. У меня в папке git скопилось наверное сотня разных проектов, и для серфинга по ним я использую либо Total Commander для просмотра и редактирования файлов, либо браузер ч/з запущенный сервер Apache или nginx также для наглядного просмотра папок и файлов.

Но с приходом Node.js логичней будет использовать сервер на нём для тренировки и вообще.

Так как в проектах на git частенько используется файл README.md в Markdown-разметке, добавим к нашему серверу функцию просмотра таких файлов в виде HTML на лету, написав простенький middleware.

Как известно, middleware для Connect/Express – это функция с тремя параметрами – req, res и next – запрос, ответ и функция обратного вызова для продолжения цепочки обработки запроса клиента.

Если наш middleware оканчивает запрос, он должен послать команду res.end(…) (в случае использования Connect‘а, в Express выбор богаче, это отдельная тема) для отдачи ответа клиенту, иначе он должен вызвать функцию next без параметров для обычного продолжения обработки, либо next(err) – с объектом ошибки – для дальнейшей обработки и рендеринга возникшей ошибки.

Вкратце алгоритм нашего middleware (назовём его any_md) будет таков: если строка запрашиваемого ресурса оканчивается на `.md`, то мы читаем файл ресурса, прогоняем его через marked (модуль формирования HTML из MD) и отдаём клиенту как обычный HTML файл. Считается, что файлы расположены начиная от текущей папки (git).
Круто? Круто. Неслыханно? Да. Восхитительно? Ещё как!

Вот как это выглядит на JavaScript под платформу Node.js:

// MIDDLEWARE: отдаём запрошенный файл .md как .html
var any_md = function(req, res, next){
// это md-файл?
if(req.url.toLowerCase().slice(-3) == '.md') {
 fs.readFile('.' + req.url, function(err, data){
  if(err) return next(err); // ошибку отправляем дальше
 
  // преобразуем MD -> HTML и отсылаем клиенту
  var html = '<!doctype html>\n<html lang="en">\n' +
  '<head>\n\t<meta charset="utf-8">\n\t<title>' + 
  req.url.slice(1) + '</title>\n</head>\n' + 
  '<body>\n' + marked(data.toString()) + '\n</body>\n</html>';
 
  res.writeHead(200, {
   'Content-Length': Buffer.byteLength(html),
   'Content-Type': 'text/html; charset=utf-8',
  });
  res.end(html);
 });
}
else
 next();
};

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

Так же в нашем сервере мы не рассматриваем кастомную обработку ошибок, оставляя её на совесть Connect‘а, точнее модуля errorhandler.

Готовый node.js сервер на Connect

Для нашего сервера мы будем использовать следующие middleware (модули) для Connect/Express:

  • compression – gzip/deflate сжатие данных на лету
  • morgan – бывший logger, лог запросов на консоль либо в файл
  • serve-favicon – отдача браузеру favicon.ico
  • serve-index – отдача списка файлов (каталога) клиенту
  • serve-static – отдача статических файлов (т.е. в нашем случае всех) клиенту
  • errorhandler – обработка ошибок
  • marked – рендер md разметки в html

Итак, для создания сервера заходим в папку git, где у нас расположены папки десятков или сотен git-проектов, и выполняем команду:

npm install -g errorhandler connect compression morgan serve-favicon serve-index serve-static marked

Заметьте, что мы установили все модули глобально, чтобы не загрязнять нашу папку git папкой node_modules. В Windows может возникнуть проблема доступа к глобально установленным модулям, тогда следует прописать такой параметр среды:

NODE_PATH=C:\Users\Andrey\AppData\Roaming\npm\node_modules

Тут `Andrey` надо поменять на имя пользователя, которое используете вы.
Также модули compression, morgan, serve-favicon, и в принципе errorhandler, которые отвечают за сжатие запроса, логирование, обслуживание favicon и обработку ошибок, не обязательны и используются тут в качестве примера создания более функционального сервера.

Затем создаём такой файл Node-сервера с именем git-server.js:

/* **************************************** */
/* Статический сервер на текущий каталог    */
/* с возможностью просмотров папок и файлов */
/* **************************************** */
var connect = require('connect');
var errorhandler = require('errorhandler');
var compression = require('compression');	// gzip/deflate
var morgan = require('morgan'); // logger
var favicon = require('serve-favicon');	// favorite icon
var serveIndex = require('serve-index');	// directory
var serveStatic = require('serve-static');	// static
 
var fs = require('fs');
var marked = require('marked');
 
// MIDDLEWARE: отдаём запрошенный файл .md как .html
var any_md = function(req, res, next){
 // это md-файл?
 if(req.url.toLowerCase().slice(-3) == '.md') {
   fs.readFile('.' + req.url, function(err, data){
    if(err) return next(err);
    // преобразуем MD -> HTML и отсылаем клиенту
    var html = '<!doctype html>\n<html lang="en">\n' +
    '<head>\n\t<meta charset="utf-8">\n\t<title>' + 
    req.url.slice(1) + '</title>\n</head>\n' + 
    '<body>\n' + marked(data.toString()) + '\n</body>\n</html>';
 
    res.writeHead(200, {
       'Content-Length': Buffer.byteLength(html),
       'Content-Type': 'text/html; charset=utf-8',
    });
    res.end(html);
   });
 }
 else
  next();
};
 
var app = connect()
.use(morgan('combined'))
.use(favicon('./favicon.ico'))
.use(any_md)
.use(compression({level: 3, memLevel: 5}))
.use(serveStatic('.'))
.use(serveIndex('.', {'icons': true}));
 
//if (process.env.NODE_ENV === 'development') {
// такую обработку ошибок использовать
// только при разработке и тестинге 
app.use(errorhandler());
//}
 
app.listen(80, function(){
	console.log('Server started at localhost port 80');
});

Для использования модуля favicon‘а закиньте файл favicon.ico сюда же, в каталог git, если лень, закомментируйте строчку `.use(favicon(‘./favicon.ico’))`.

Итак, запускаем наш сервер:

node git-server.js

Сервер работает по адресу localhost, порт 80 (в Linux для использования этого порта могут понадобиться права root, поэтому лучше поменять порт на 3000, например).

Теперь заходим браузером по адресу localhost (если мы используем порт 80 его тут указывать не нужно) и видим список папок git-проектов. Зайдя в какую-нибудь, где есть файл README.md (или любой md-файл), щёлкнем на нём и увидим его как обычный HTML-файл в браузере.

На сим всё, откланиваюсь, крепкой чашечки кофе утром и сладкого бокала чая с чебрецом и лимоном вечером,
а также вкусной котлеты по-киевски, которую я сейчас пойду кушать.

Но главное – это любовь и хорошее настроения, блин.

bcrypt в NodeJS в Windows 10 Home и Linux Mint

Я работаю с nodejs в Windows 10 Home (параллельно иногда загружаюсь в Linux Mint, но редко).

Изучаю книгу `Node.JS в действии`, она, к сожалению, в некоторых местах устарела, но в целом даёт представление о теме.

Итак, в главе 9 нам нужен был brcypt, но простой npm install bcrypt –save завершился с ошибкой.
Пошарив инете пару минут, я понял, что просто так его не установить, и я уже думал проверять примеры в Linux Mint‘е своём любимом, но наткнулся здесь: http://stackoverflow.com/questions/14573488/error-compiling-bcrypt-node-js

Оказывается, bcrypt можно заменить bcrypt-nodejs, который у меня установился без проблем за пару секунд.
Далее по теме увидел такую надпись: The performance is a lot slower though, то есть, мол, производительность гораздо ниже, и даётся ссылка на https://github.com/adrianblynch/bcrypt-vs-bcrypt/blob/master/index.js.

Там тестятся следующие bcrypt пакеты для nodejs:

И даётся простенький скрипт-тестирование (его мы дополним и рассмотрим ниже).

Можно запустить, потестить какие результаты даст, сделаю это через какое-то время в Linux Mint’е (т.к. под свою Windows bcrypt я не установил и не буду заморачиваться, время терять).

Но в этом тесте нет bcrypt-nodejs, я его добавлю тогда, ОК, и результаты позже выведу **** обещанного три года ждут. 🙂

Но какова мораль сей басни? Nodejs настолько стремительно развивающийся проект со столькими вовлеченными и увлеченными пользователями, что для любой более-менее известной и популярной либы есть несколько решений под эту удивительную платформу. И если какая-то либа по тем или иным причинам не подойдёт (как например в нашем случае с bcrypt под Windows 10 (8, 8.1, 7 да любой наверное), мы с большой вероятностью можем найти полноценную замену.

Например, для mongodb есть mongoose, и есть monk, для sqlite куча пакетов, и т.д.

А почему стремительно развивающийся? Например, на оф.страничке Express (http://expressjs.com/en/guide/migrating-4.html) есть устаревший уже пример с multer (обработка запросов multipart/form-data) – там приводится пример для работы с “multer”: “^0.1.3”, а сейчас уже есть multer 1.1.0 (на 4 февраля 2016 года – 2016-02-04), который не совместим с указанным multer‘ом, то есть он не может напрямую выступать в качестве middleware теперь, а надо использовать по особому его методы single, array и т.п. (http://stackoverflow.com/questions/31495499/multer-configuration-with-app-use-returns-typeerror/31495796#31495796):

multer({dest:'./uploads/'}).single(...)
multer({dest:'./uploads/'}).array(...)
multer({dest:'./uploads/'}).fields(...)

На сим всё, откланиваюсь – за мной ещё тесты производительности, если не забуду. )))

Тесты производительности различных пакетов bcrypt для NodeJS

Итак, что мы делаем?
Перезагржаемся в LinuxMint, смотрим систему:

$ uname -a
Linux justy 3.13.0-51-generic-tuxonice #84~ppa1-Ubuntu SMP
Wed Apr 29 19:39:16 UTC 2015 x86_64 x86_64 x86_64 GNU/Linux

Создаём папку, в ней nodejs-проект

$ mkdir bcrypt-bench
$ cd bcrypt-bench/
$ npm init
...
... - все параметры можно оставить по умолчанию,
они в принципе не играют большой роли здесь

Инсталлируем нужные пакеты с добавлением в packaje.json (флаг –save)

$ npm install bcrypt bcryptjs twin-bcrypt bcrypt-nodejs commander --save
 
> bcrypt@0.8.5 install /home/justy/nodejs/bcrypt-bench/node_modules/bcrypt
> node-gyp rebuild
 
make: Вход в каталог `/home/justy/nodejs/bcrypt-bench/node_modules/bcrypt/build'
  CXX(target) Release/obj.target/bcrypt_lib/src/blowfish.o
  CXX(target) Release/obj.target/bcrypt_lib/src/bcrypt.o
  CXX(target) Release/obj.target/bcrypt_lib/src/bcrypt_node.o
  SOLINK_MODULE(target) Release/obj.target/bcrypt_lib.node
  COPY Release/bcrypt_lib.node
make: Выход из каталога `/home/justy/nodejs/bcrypt-bench/node_modules/bcrypt/build'
bcrypt-bench@1.0.0 /home/justy/nodejs/bcrypt-bench
├─┬ bcrypt@0.8.5
│ ├── bindings@1.2.1
│ └── nan@2.0.5
├── bcrypt-nodejs@0.0.3
├── bcryptjs@2.3.0
├─┬ commander@2.9.0
│ └── graceful-readlink@1.0.1
└── twin-bcrypt@2.1.1
 
npm WARN EPACKAGEJSON bcrypt-bench@1.0.0 No repository field.

В результате наш package.json будет выглядеть примерно так:

{
  "name": "bcrypt-bench",
  "version": "1.0.0",
  "description": "Simple benchmarking for varios nodejs bcrypt packages",
  "main": "test.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [
    "brcypt",
    "bench",
    "test"
  ],
  "author": "adrianblynch",
  "license": "MIT",
  "dependencies": {
    "bcrypt": "^0.8.5",
    "bcrypt-nodejs": "0.0.3",
    "bcryptjs": "^2.3.0",
    "commander": "^2.9.0",
    "twin-bcrypt": "^2.1.1"
  }
}

Теперь создаём скрипт test.js, скопировав его отсюда: https://github.com/adrianblynch/bcrypt-vs-bcrypt/blob/master/index.js и добавив в него пакет bcrypt-nodejs:

var bcrypt = require('bcrypt');
var bcryptJS = require('bcryptjs');
var twinBcrypt = require('twin-bcrypt');
var bcryptNodeJs = require('bcrypt-nodejs');
 
var commander = require('commander');
 
commander
.option('-r, --rounds [num]', 'Number of rounds to use', parseInt)
.option('-i, --iterations [num]', 'Number of iterations to use', parseInt)
.parse(process.argv);
 
var password = "password";
var rounds = commander.rounds || 12;
var iterations = commander.iterations || 10;
var hash, start, end, i, total;
 
console.log("Bcrypts ready? Fight!");
 
// bcrypt
 
total = 0;
 
for (i = 0; i < iterations; i++) {
	start = new Date();
	hash = bcrypt.hashSync(password, bcrypt.genSaltSync(rounds));
	end = new Date();
	total += (end - start);
}
 
console.log("bcrypt - " + iterations + " iterations took " + total + "ms which is an average of " + parseInt(total / iterations) + "ms each");
 
// bcryptjs
 
total = 0;
 
for (i = 0; i < iterations; i++) {
	start = new Date();
	hash = bcryptJS.hashSync(password, bcryptJS.genSaltSync(rounds));
	end = new Date();
	total += (end - start);
}
 
console.log("bcryptjs - " + iterations + " iterations took " + total + "ms which is an average of " + parseInt(total / iterations) + "ms each");
 
// twin-bcrypt
 
total = 0;
 
for (i = 0; i < iterations; i++) {
	start = new Date();
	hash = twinBcrypt.hashSync(password, twinBcrypt.genSalt(rounds));
	end = new Date();
	total += (end - start);
}
 
console.log("twin-bcrypt - " + iterations + " iterations took " + total + "ms which is an average of " + parseInt(total / iterations) + "ms each");
 
// bcrypt-nodejs
 
total = 0;
 
for (i = 0; i < iterations; i++) {
	start = new Date();
	hash = bcryptNodeJs.hashSync(password, bcryptNodeJs.genSaltSync(rounds));
	end = new Date();
	total += (end - start);
}
 
console.log("bcrypt-nodejs - " + iterations + " iterations took " + total + "ms which is an average of " + parseInt(total / iterations) + "ms each");

Всё готово для запуска тестов:

$ node test.js
Bcrypts ready? Fight!
bcrypt - 10 iterations took 2813ms which is an average of 281ms each
bcryptjs - 10 iterations took 8523ms which is an average of 852ms each
twin-bcrypt - 10 iterations took 8650ms which is an average of 865ms each
bcrypt-nodejs - 10 iterations took 8144ms which is an average of 814ms each

Как видим, стандартный bcrypt практически в 3 раза быстрее своих собратьев,
что не удивительно, так как он использует пре-компилированную версию, в то время как остальные написаны на чистом JavaScript и прогоняются ч/з интерпретатор V8, как бы он быстр не был.

Вторая мораль сей басни такова – если в нашем проекте требуется много и часто хэшировать с помощью алгоритма bcrypt’а, следует предпочесть bcrypt.

Сладкого чая и вкусной слойки на столе! С цукерками в вазочке.

Emmet: balance tag исправляем

Здравствуйте.
На страничке http://docs.emmet.io/actions/match-pair/ у Сергея написано следующее (перевод мой с английского):
********************************************************************************

Balance

“Balance Outward” (Ctrl+D)

“Balance Inward (Shift+Ctrl+D)

Известная балансировка тэгов: поиск границ тэгов и выбор их от текущей позиции каретки. Она может быть расширенной (outward balancing) или суженной (inward balancing) при вызове несколько раз подряд. Не каждый редактор поддерживает одновременно суженную и расширенную балансировку.

Балансировка тэгов Emmet достаточно уникальна. В отличие от остальных, она ищет границы тегов от текущей позиции каретки, а не от начала документа. Это означает, что она может работать не только в HTML документах.
********************************************************************************

Да, классная штука, правда, в реальной жизни нас скорее интересует текущий тег – просмотр границ и переход в начало конец этого тэга (для этого у Emmet‘а есть действие matching pair).

Но если она (балансировка) есть, хотелось бы использовать её на полную мощь. Так как я добавил поддержку Emmet для редактора AkelPad (http://akelpad.sourceforge.net/forum/viewtopic.php?p=30266#30266), мне захотелось подробнее разобраться в вопросе, а именно меня задели слова, что “Не каждый редактор поддерживает одновременно суженную и расширенную балансировку.“.

Экспериментируя с Emmet‘ом в AkelPad‘e, я заметил недоделанность эту (отсутствие балансировки вглубь, в частности), и решил посмотреть, что можно сделать.

Итак, нас интересует такой участок кода в файле emmet-app.js (я добавлял поддержку Emmet’а для AkelPad’а, используя плагин для Komodo Edit‘а, а потом проанализировал подобный файл от плагина к Notepad++):

/**
 * HTML pair matching (balancing) actions
 * @constructor
 * @memberOf __matchPairActionDefine
 * @param {Function} require
 * @param {Underscore} _
 */
emmet.exec(function(require, _) {
	/** @type emmet.actions */
	var actions = require('actions');
	var matcher = require('htmlMatcher');
	var lastMatch = null;
 
	/**
	 * Find and select HTML tag pair
	 * @param {IEmmetEditor} editor Editor instance
	 * @param {String} direction Direction of pair matching: 'in' or 'out'. 
	 * Default is 'out'
	 */
	function matchPair(editor, direction) {
		direction = String((direction || 'out').toLowerCase());
		var info = require('editorUtils').outputInfo(editor);
 
		var range = require('range');
		/** @type Range */
		var sel = range.create(editor.getSelectionRange());
		var content = info.content;
 
		// validate previous match
		if (lastMatch && !lastMatch.range.equal(sel)) {
			lastMatch = null;
		}
 
		if (lastMatch && sel.length()) {
			if (direction == 'in') {
				// user has previously selected tag and wants to move inward
				if (lastMatch.type == 'tag' && !lastMatch.close) {
					// unary tag was selected, can't move inward
					return false;
				} else {
					if (lastMatch.range.equal(lastMatch.outerRange)) {
						lastMatch.range = lastMatch.innerRange;
					} else {
						var narrowed = require('utils').narrowToNonSpace(content, lastMatch.innerRange);
						lastMatch = matcher.find(content, narrowed.start + 1);
						if (lastMatch && lastMatch.range.equal(sel) && lastMatch.outerRange.equal(sel)) {
							lastMatch.range = lastMatch.innerRange;
						}
					}
				}
			} else {
				if (
						!lastMatch.innerRange.equal(lastMatch.outerRange) 
						&& lastMatch.range.equal(lastMatch.innerRange) 
						&& sel.equal(lastMatch.range)) {
					lastMatch.range = lastMatch.outerRange;
				} else {
					lastMatch = matcher.find(content, sel.start);
					if (lastMatch && lastMatch.range.equal(sel) && lastMatch.innerRange.equal(sel)) {
						lastMatch.range = lastMatch.outerRange;
					}
				}
			}
		} else {
			lastMatch = matcher.find(content, sel.start);
		}
 
		if (lastMatch && !lastMatch.range.equal(sel)) {
			editor.createSelection(lastMatch.range.start, lastMatch.range.end);
			return true;
		}
 
		lastMatch = null;
		return false;
	}
 
	...
 
}

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

		// validate previous match
		if (lastMatch && !lastMatch.range.equal(sel)) {
			lastMatch = null;
		}

Вон оно что. Тут у нас статическая переменная lastMatch. Дело в том, что если JavaScript скрипт каждый раз подключается заново, статические переменные теряют смысл, так как они каждый раз заново инициализируются.

В браузерах после загрузки страницы все скрипты остаются в памяти и статические переменные работают как надо. В Notepad++ также подключение скрипта emmet-app.js происходит только один раз при первом вызове (используется Python-wrapper для JavaScript), поэтому там также всё работает более-менее нормально.

А в AkelPad‘е каждый раз мы запускаем скрипт заново, то есть он не висит в памяти, занимая пространство и время редактора, поэтому в нашем случае эта статическая переменная бесполезна.

Штука такая, что на самом деле от статической составляющей lastMatch можно уйти. Предположив, что если у нас что-то выбрано, lastMatch уже отработал и нам надо искать дальше.

То есть мы в любом случае ищем как сначала, переставив местами части.

Вот что в итоге получается:

/**
 * HTML pair matching (balancing) actions
 * @constructor
 * @memberOf __matchPairActionDefine
 * @param {Function} require
 * @param {Underscore} _
 */
emmet.exec(function(require, _) {
	/** @type emmet.actions */
	var actions = require('actions');
	var matcher = require('htmlMatcher');
	var lastMatch = null;
 
	/**
	 * Find and select HTML tag pair
	 * @param {IEmmetEditor} editor Editor instance
	 * @param {String} direction Direction of pair matching: 'in' or 'out'. 
	 * Default is 'out'
	 */
	function matchPair(editor, direction) {
		direction = String((direction || 'out').toLowerCase());
		var info = require('editorUtils').outputInfo(editor);
 
		var range = require('range');
		/** @type Range */
		var sel = range.create(editor.getSelectionRange());
		var content = info.content;
 
		// we do not have static lastMatch here
		lastMatch = matcher.find(content, sel.start);
		// try for in balancing
		if(!lastMatch)
			lastMatch = matcher.find(content, sel.start + 1);
 
		if (lastMatch && sel.length()) {
			if (direction == 'in') {
				// user has previously selected tag and wants to move inward
				if (lastMatch.type == 'tag' && !lastMatch.close) {
					// unary tag was selected, can't move inward
					return false;
				} else {
					if (lastMatch.range.equal(lastMatch.outerRange)) {
						lastMatch.range = lastMatch.innerRange;
					} else {
						var narrowed = require('utils').narrowToNonSpace(content, lastMatch.innerRange);
						lastMatch = matcher.find(content, narrowed.start + 1);
						if (lastMatch && lastMatch.range.equal(sel) && lastMatch.outerRange.equal(sel)) {
							lastMatch.range = lastMatch.innerRange;
						}
					}
				}
			} else {
				if (
						!lastMatch.innerRange.equal(lastMatch.outerRange) 
						&& lastMatch.range.equal(lastMatch.innerRange) 
						&& sel.equal(lastMatch.range)) {
					lastMatch.range = lastMatch.outerRange;
				} else {
					lastMatch = matcher.find(content, sel.start);
					if (lastMatch && lastMatch.range.equal(sel) && lastMatch.innerRange.equal(sel)) {
						lastMatch.range = lastMatch.outerRange;
					}
				}
			}
		}
 
		if (lastMatch && !lastMatch.range.equal(sel)) {
			editor.createSelection(lastMatch.range.start, lastMatch.range.end);
			return true;
		}
 
		return false;
	}
 
	...
 
}

Для balancing inward я добавил такую фичу:

		// try for in balancing
		if(!lastMatch)
			lastMatch = matcher.find(content, sel.start + 1);

то есть пытаемся найти с позиции следующей после каретки, углубляясь глубже в дерево DOM.

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

Но сам движок Сергея по поиску HTML/XML тэгов (matcher.find) нуждается в доработке, так он вне HTML может спотыкаться при использовании экранированных кавычек и ряда других случаях. Но этим, насколько я мог заметить, грешат практически все наверное подобные алгоритмы, так как найти matching tag в определённых случаях бывает довольно тяжело.

На сим всё, низкий поклон, пошёл завтракать яичницу с мясом (не беконом, а свининой, оставшейся после вчерашнего ужина).

Благодарю Вас за чтение и Господа за еду.

Читаем/пишем бинарники на JavaScript (JScript) для Win Wscript

Да, проблема в том, что JavaScript пытается преобразовать полученные байты в юникод (unicode), и портит большинство байт, которые больше 0x7F. Поэтому при чтении/записи бинарников получается не то, что мы хотим.

Пошарив какое-то время в инете, я нашёл табличку преобразований символов больше либо равным 0x80, но она в итоге оказалась не полной, и мои бинарники читались и писались с ошибками.

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

Идея такая – создать бинарный файл, состоящий из последовательности байт от 0 до 255, то есть размером 256 байт, а потом распрасить его JavaScript’ом и проанализировать полученные символы.

Пишем краткий сценрий на PHP, ибо PHP всегда под рукой, что в Виньде ,что в Линуксе, что на серверах.

<?php
/* create file of bytes from 0 to 255 */
	$s = '';
	for($i = 0; $i < 256; $i++)
		$s .= chr($i);
 
	file_put_contents('256.dat', $s);

В итоге, мы имеем файл 256.dat, который проанализируем JavaScript’ и составим табличку преобразований на лету.

var name = '256.dat', i; // путь к нашему файлу и переменная д/цикла
 
/* ******************************************
* Используем API AkelPad's Scripts плагина  *
******************************************* */
var bytes = AkelPad.ReadFile(name, 0);	// binary
log('bytes.length=' + bytes.length); // должно быть 256
 
var abytes = bytes.split('');
log('abytes.length=' + abytes.length); // должно быть 256
 
var ascii = '', fromAscii = '', c;
for(i = 0; i < abytes.length; i++) {
        // table asciiCodeAt
        if(i > 127)
	  ascii += 'case 0x' + bin2hex(abytes[i]) +
            ': return 0x' + i.toString(16) + '; break;\n'
	// REVERSE TABLE fromAscii
	c = bin2hex(abytes[i]);
	if(c.length == 3)
		c = '0' + c;
	else
	if(c.length == 2)
		c = '00' + c;
	// count only high values
	if(i > 127)
	  fromAscii += 'case 0x' + i.toString(16) + ': s += \'\\u' +
            c.toUpperCase() + '\'; break;\n'
}
 
log('// ASCII:');
log(ascii)
log('// fromAscii:');
log(fromAscii);
 
function bin2hex(s) {
  //  discuss at: http://phpjs.org/functions/bin2hex/
  // original by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
  // bugfixed by: Onno Marsman
  // bugfixed by: Linuxworld
  // improved by: ntoniazzi (http://phpjs.org/functions/bin2hex:361#comment_177616)
  //   example 1: bin2hex('Kev');
  //   returns 1: '4b6576'
  //   example 2: bin2hex(String.fromCharCode(0x00));
  //   returns 2: '00'
 
  var i, l, o = '',
    n;
 
  s += '';
 
  for (i = 0, l = s.length; i < l; i++) {
    n = s.charCodeAt(i)
      .toString(16);
    o += n.length < 2 ? '0' + n : n;
  }
 
  return o;
}
 
// Логируем текст с помощью AkelPad'овского Log плагина
function log(text)
{
  AkelPad.Call("Log::Output", 4, text + '\n');
}

Потом копируем полученный вывод в отдельный файл, добавляем необохдимую разметку и получаем в итоге полные таблицы преобразований to ASCII и from Ascii Code для JavaScript:

/*
	Needed for reading from binary files
 	See actionUtils.getImageSize method in emmet-app.js
*/
String.prototype.asciiCodeAt = function(i) {
    // charCodeAt returns some bytes translated to unicode characters. 
    // this function means to counteract that.
    switch(this.charCodeAt(i)) {
        case 0x402: return 0x80; break;
				case 0x403: return 0x81; break;
				case 0x201a: return 0x82; break;
				case 0x453: return 0x83; break;
				case 0x201e: return 0x84; break;
				case 0x2026: return 0x85; break;
				case 0x2020: return 0x86; break;
				case 0x2021: return 0x87; break;
				case 0x20ac: return 0x88; break;
				case 0x2030: return 0x89; break;
				case 0x409: return 0x8a; break;
				case 0x2039: return 0x8b; break;
				case 0x40a: return 0x8c; break;
				case 0x40c: return 0x8d; break;
				case 0x40b: return 0x8e; break;
				case 0x40f: return 0x8f; break;
				case 0x452: return 0x90; break;
				case 0x2018: return 0x91; break;
				case 0x2019: return 0x92; break;
				case 0x201c: return 0x93; break;
				case 0x201d: return 0x94; break;
				case 0x2022: return 0x95; break;
				case 0x2013: return 0x96; break;
				case 0x2014: return 0x97; break;
				case 0x98: return 0x98; break;
				case 0x2122: return 0x99; break;
				case 0x459: return 0x9a; break;
				case 0x203a: return 0x9b; break;
				case 0x45a: return 0x9c; break;
				case 0x45c: return 0x9d; break;
				case 0x45b: return 0x9e; break;
				case 0x45f: return 0x9f; break;
				case 0xa0: return 0xa0; break;
				case 0x40e: return 0xa1; break;
				case 0x45e: return 0xa2; break;
				case 0x408: return 0xa3; break;
				case 0xa4: return 0xa4; break;
				case 0x490: return 0xa5; break;
				case 0xa6: return 0xa6; break;
				case 0xa7: return 0xa7; break;
				case 0x401: return 0xa8; break;
				case 0xa9: return 0xa9; break;
				case 0x404: return 0xaa; break;
				case 0xab: return 0xab; break;
				case 0xac: return 0xac; break;
				case 0xad: return 0xad; break;
				case 0xae: return 0xae; break;
				case 0x407: return 0xaf; break;
				case 0xb0: return 0xb0; break;
				case 0xb1: return 0xb1; break;
				case 0x406: return 0xb2; break;
				case 0x456: return 0xb3; break;
				case 0x491: return 0xb4; break;
				case 0xb5: return 0xb5; break;
				case 0xb6: return 0xb6; break;
				case 0xb7: return 0xb7; break;
				case 0x451: return 0xb8; break;
				case 0x2116: return 0xb9; break;
				case 0x454: return 0xba; break;
				case 0xbb: return 0xbb; break;
				case 0x458: return 0xbc; break;
				case 0x405: return 0xbd; break;
				case 0x455: return 0xbe; break;
				case 0x457: return 0xbf; break;
				case 0x410: return 0xc0; break;
				case 0x411: return 0xc1; break;
				case 0x412: return 0xc2; break;
				case 0x413: return 0xc3; break;
				case 0x414: return 0xc4; break;
				case 0x415: return 0xc5; break;
				case 0x416: return 0xc6; break;
				case 0x417: return 0xc7; break;
				case 0x418: return 0xc8; break;
				case 0x419: return 0xc9; break;
				case 0x41a: return 0xca; break;
				case 0x41b: return 0xcb; break;
				case 0x41c: return 0xcc; break;
				case 0x41d: return 0xcd; break;
				case 0x41e: return 0xce; break;
				case 0x41f: return 0xcf; break;
				case 0x420: return 0xd0; break;
				case 0x421: return 0xd1; break;
				case 0x422: return 0xd2; break;
				case 0x423: return 0xd3; break;
				case 0x424: return 0xd4; break;
				case 0x425: return 0xd5; break;
				case 0x426: return 0xd6; break;
				case 0x427: return 0xd7; break;
				case 0x428: return 0xd8; break;
				case 0x429: return 0xd9; break;
				case 0x42a: return 0xda; break;
				case 0x42b: return 0xdb; break;
				case 0x42c: return 0xdc; break;
				case 0x42d: return 0xdd; break;
				case 0x42e: return 0xde; break;
				case 0x42f: return 0xdf; break;
				case 0x430: return 0xe0; break;
				case 0x431: return 0xe1; break;
				case 0x432: return 0xe2; break;
				case 0x433: return 0xe3; break;
				case 0x434: return 0xe4; break;
				case 0x435: return 0xe5; break;
				case 0x436: return 0xe6; break;
				case 0x437: return 0xe7; break;
				case 0x438: return 0xe8; break;
				case 0x439: return 0xe9; break;
				case 0x43a: return 0xea; break;
				case 0x43b: return 0xeb; break;
				case 0x43c: return 0xec; break;
				case 0x43d: return 0xed; break;
				case 0x43e: return 0xee; break;
				case 0x43f: return 0xef; break;
				case 0x440: return 0xf0; break;
				case 0x441: return 0xf1; break;
				case 0x442: return 0xf2; break;
				case 0x443: return 0xf3; break;
				case 0x444: return 0xf4; break;
				case 0x445: return 0xf5; break;
				case 0x446: return 0xf6; break;
				case 0x447: return 0xf7; break;
				case 0x448: return 0xf8; break;
				case 0x449: return 0xf9; break;
				case 0x44a: return 0xfa; break;
				case 0x44b: return 0xfb; break;
				case 0x44c: return 0xfc; break;
				case 0x44d: return 0xfd; break;
				case 0x44e: return 0xfe; break;
				case 0x44f: return 0xff; break;
 
        default: return this.charCodeAt(i);
    }
}
 
/*
	Needed for writing to binary files.
 	See base64.decode method in emmet-app.js
*/
String.fromAsciiCode = function() {
    // reverse for asciiCodeAt() method
    var s = '', i, c = arguments.length;
 
    for(i = 0; i < c; i++)
    	switch(arguments[i]) {
				case 0x80: s += '\u0402'; break;
				case 0x81: s += '\u0403'; break;
				case 0x82: s += '\u201A'; break;
				case 0x83: s += '\u0453'; break;
				case 0x84: s += '\u201E'; break;
				case 0x85: s += '\u2026'; break;
				case 0x86: s += '\u2020'; break;
				case 0x87: s += '\u2021'; break;
				case 0x88: s += '\u20AC'; break;
				case 0x89: s += '\u2030'; break;
				case 0x8a: s += '\u0409'; break;
				case 0x8b: s += '\u2039'; break;
				case 0x8c: s += '\u040A'; break;
				case 0x8d: s += '\u040C'; break;
				case 0x8e: s += '\u040B'; break;
				case 0x8f: s += '\u040F'; break;
				case 0x90: s += '\u0452'; break;
				case 0x91: s += '\u2018'; break;
				case 0x92: s += '\u2019'; break;
				case 0x93: s += '\u201C'; break;
				case 0x94: s += '\u201D'; break;
				case 0x95: s += '\u2022'; break;
				case 0x96: s += '\u2013'; break;
				case 0x97: s += '\u2014'; break;
				//case 0x98: s += '\u0098'; break;
				case 0x99: s += '\u2122'; break;
				case 0x9a: s += '\u0459'; break;
				case 0x9b: s += '\u203A'; break;
				case 0x9c: s += '\u045A'; break;
				case 0x9d: s += '\u045C'; break;
				case 0x9e: s += '\u045B'; break;
				case 0x9f: s += '\u045F'; break;
				//case 0xa0: s += '\u00A0'; break;
				case 0xa1: s += '\u040E'; break;
				case 0xa2: s += '\u045E'; break;
				case 0xa3: s += '\u0408'; break;
				//case 0xa4: s += '\u00A4'; break;
				case 0xa5: s += '\u0490'; break;
				//case 0xa6: s += '\u00A6'; break;
				//case 0xa7: s += '\u00A7'; break;
				case 0xa8: s += '\u0401'; break;
				//case 0xa9: s += '\u00A9'; break;
				case 0xaa: s += '\u0404'; break;
				//case 0xab: s += '\u00AB'; break;
				//case 0xac: s += '\u00AC'; break;
				//case 0xad: s += '\u00AD'; break;
				//case 0xae: s += '\u00AE'; break;
				case 0xaf: s += '\u0407'; break;
				//case 0xb0: s += '\u00B0'; break;
				//case 0xb1: s += '\u00B1'; break;
				case 0xb2: s += '\u0406'; break;
				case 0xb3: s += '\u0456'; break;
				case 0xb4: s += '\u0491'; break;
				//case 0xb5: s += '\u00B5'; break;
				//case 0xb6: s += '\u00B6'; break;
				//case 0xb7: s += '\u00B7'; break;
				case 0xb8: s += '\u0451'; break;
				case 0xb9: s += '\u2116'; break;
				case 0xba: s += '\u0454'; break;
				//case 0xbb: s += '\u00BB'; break;
				case 0xbc: s += '\u0458'; break;
				case 0xbd: s += '\u0405'; break;
				case 0xbe: s += '\u0455'; break;
				case 0xbf: s += '\u0457'; break;
				case 0xc0: s += '\u0410'; break;
				case 0xc1: s += '\u0411'; break;
				case 0xc2: s += '\u0412'; break;
				case 0xc3: s += '\u0413'; break;
				case 0xc4: s += '\u0414'; break;
				case 0xc5: s += '\u0415'; break;
				case 0xc6: s += '\u0416'; break;
				case 0xc7: s += '\u0417'; break;
				case 0xc8: s += '\u0418'; break;
				case 0xc9: s += '\u0419'; break;
				case 0xca: s += '\u041A'; break;
				case 0xcb: s += '\u041B'; break;
				case 0xcc: s += '\u041C'; break;
				case 0xcd: s += '\u041D'; break;
				case 0xce: s += '\u041E'; break;
				case 0xcf: s += '\u041F'; break;
				case 0xd0: s += '\u0420'; break;
				case 0xd1: s += '\u0421'; break;
				case 0xd2: s += '\u0422'; break;
				case 0xd3: s += '\u0423'; break;
				case 0xd4: s += '\u0424'; break;
				case 0xd5: s += '\u0425'; break;
				case 0xd6: s += '\u0426'; break;
				case 0xd7: s += '\u0427'; break;
				case 0xd8: s += '\u0428'; break;
				case 0xd9: s += '\u0429'; break;
				case 0xda: s += '\u042A'; break;
				case 0xdb: s += '\u042B'; break;
				case 0xdc: s += '\u042C'; break;
				case 0xdd: s += '\u042D'; break;
				case 0xde: s += '\u042E'; break;
				case 0xdf: s += '\u042F'; break;
				case 0xe0: s += '\u0430'; break;
				case 0xe1: s += '\u0431'; break;
				case 0xe2: s += '\u0432'; break;
				case 0xe3: s += '\u0433'; break;
				case 0xe4: s += '\u0434'; break;
				case 0xe5: s += '\u0435'; break;
				case 0xe6: s += '\u0436'; break;
				case 0xe7: s += '\u0437'; break;
				case 0xe8: s += '\u0438'; break;
				case 0xe9: s += '\u0439'; break;
				case 0xea: s += '\u043A'; break;
				case 0xeb: s += '\u043B'; break;
				case 0xec: s += '\u043C'; break;
				case 0xed: s += '\u043D'; break;
				case 0xee: s += '\u043E'; break;
				case 0xef: s += '\u043F'; break;
				case 0xf0: s += '\u0440'; break;
				case 0xf1: s += '\u0441'; break;
				case 0xf2: s += '\u0442'; break;
				case 0xf3: s += '\u0443'; break;
				case 0xf4: s += '\u0444'; break;
				case 0xf5: s += '\u0445'; break;
				case 0xf6: s += '\u0446'; break;
				case 0xf7: s += '\u0447'; break;
				case 0xf8: s += '\u0448'; break;
				case 0xf9: s += '\u0449'; break;
				case 0xfa: s += '\u044A'; break;
				case 0xfb: s += '\u044B'; break;
				case 0xfc: s += '\u044C'; break;
				case 0xfd: s += '\u044D'; break;
				case 0xfe: s += '\u044E'; break;
				case 0xff: s += '\u044F'; break;
 
        default: s += String.fromCharCode(arguments[i]);
    }
    return s;
}

Первую табличку мы используем при чтении из бинарного файла средствами JavaScript, вторую – при записи в бинарник.

Например, используем эти таблички для base64 кодирования/декодирования бинарных данных (методы из emmet-app.js)

/**
 * @author Sergey Chikuyonok (serge.che@gmail.com)
 * @link http://chikuyonok.ru
 */
emmet.define('base64', function(require, _) {
	var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
 
	return {
		/**
		 * Encodes data using base64 algorithm
		 * @author Tyler Akins (http://rumkin.com)
		 * @param {String} input
		 * @returns {String}
		 * @memberOf emmet.base64
		 */
		encode : function(input) {
			var output = [];
			var chr1, chr2, chr3, enc1, enc2, enc3, enc4, cdp1, cdp2, cdp3;
			var i = 0, il = input.length, b64 = chars;
 
			while (i < il) {
 
				// call .asciiCodeAt() instead of .charCodeAt()
				// see AkelEmmet.js in Scripts folder for details
				cdp1 = input.asciiCodeAt(i++);
				cdp2 = input.asciiCodeAt(i++);
				cdp3 = input.asciiCodeAt(i++);
 
				chr1 = cdp1 & 0xff;
				chr2 = cdp2 & 0xff;
				chr3 = cdp3 & 0xff;
 
				enc1 = chr1 >> 2;
				enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
				enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
				enc4 = chr3 & 63;
 
				if (isNaN(cdp2)) {
					enc3 = enc4 = 64;
				} else if (isNaN(cdp3)) {
					enc4 = 64;
				}
 
				output.push(b64.charAt(enc1) + b64.charAt(enc2) + b64.charAt(enc3) + b64.charAt(enc4));
			}
 
			return output.join('');
		},
 
		/**
		 * Decodes string using MIME base64 algorithm
		 * 
		 * @author Tyler Akins (http://rumkin.com)
		 * @param {String} data
		 * @return {String}
		 */
		decode : function(data) {
			var o1, o2, o3, h1, h2, h3, h4, bits, i = 0, ac = 0, tmpArr = [];
			var b64 = chars, il = data.length;
 
			if (!data) {
				return data;
			}
 
			data += '';
 
			do { // unpack four hexets into three octets using index points in b64
				h1 = b64.indexOf(data.charAt(i++));
				h2 = b64.indexOf(data.charAt(i++));
				h3 = b64.indexOf(data.charAt(i++));
				h4 = b64.indexOf(data.charAt(i++));
 
				bits = h1 << 18 | h2 << 12 | h3 << 6 | h4;
 
				o1 = bits >> 16 & 0xff;
				o2 = bits >> 8 & 0xff;
				o3 = bits & 0xff;
 
				if (h3 == 64) {
					tmpArr[ac++] = String.fromAsciiCode(o1); // String.fromCharCode(o1);
				} else if (h4 == 64) {
					tmpArr[ac++] = String.fromAsciiCode(o1, o2); //String.fromCharCode(o1, o2);
				} else {
					tmpArr[ac++] = String.fromAsciiCode(o1, o2, o3); //String.fromCharCode(o1, o2, o3);
				}
			} while (i < il);
 
			return tmpArr.join('');
		}
	};
});

Крепкого Вам чая в бокале, сладкого лимона и главное конечно, я знаю, что банален, но желаю хорошего настроения. От чего оно зависит?

JavaScript тест на Flash и на мобильный девайс – пара полезных проверок

JavaScript тест на Flash и мобильный девайс iOS/Android

Тест на поддержку Adobe Flash

Для программирования под JavaScript иногда бывает нужно узнать, поддерживает ли браузер читателя технологию Adobe Flash.
Вот простоая функция, которую я одолжил у audio.js Колбера (kolber.github.io/audiojs/)

/**
	Test UA for Adobe Flash support 
	Borrowed from audio.js by kolber
	kolber.github.io/audiojs/
*/
var	uaHasFlash = function () {
	if (navigator.plugins && navigator.plugins.length && navigator.plugins["Shockwave Flash"])
		return true;
	else if (navigator.mimeTypes && navigator.mimeTypes.length) {
		var b =	navigator.mimeTypes["application/x-shockwave-flash"];
		return b && b.enabledPlugin;
	} else
		try {
			new ActiveXObject("ShockwaveFlash.ShockwaveFlash");
			return true;
		} catch (a) {}
 
	return false;
}

Тест на мобильный девайс – смартфон/планшет с iOS/Android

А чтобы опрделить, что у читателя Вашего сайта смартфон/планшет Andriod или от Apple (на iOS, то есть iPad или iPhone или iPod), достаточно такой наверное простейшей проверки:

if(/(ipod|iphone|ipad|Android)/i.test(navigator.userAgent)) {
// да, у читателя устройство с iOS/Android
}

Пока всё, пока-пока, до связи!

P.S. Вкусного чая с имбирём в сахаре.

С Новым 2015 Годом! или Добавляем гирлянды и снег на наши странички

С Новым 2015 Годом!

Счастлив поздравить на страницах моего блога всех читателей. Через пару дней начнётся один из самых лучших праздников мира – встреча Нового Года. А я в качестве подарка хочу предложить украшательство для Ваших новогодних страничек – это цепочка гирлянд, которые взрываются при наведении на них указателя мыши, а также падающий снег.

Пример такой странички Вы можете наблюдать здесь – http://goloslogos.ru/ny2015.html. Только там гирлянды раскалываются без звука, потому что меня попросили его убрать, и ниже я покажу Вам как это сделать.

Начнём

Итак, гирляндочки и снег придумал не я, а Скот Шиллер. Родная страничка снежного шторма (snowstorm) здесь – http://www.schillmania.com/projects/snowstorm/

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

Итак, заходим на https://github.com/scottschiller/snowstorm и скачиваем проект как ZIP-архив (кнопка справа Download ZIP) ну или естесно клонируем её:

git clone https://github.com/scottschiller/Snowstorm.git

Из всего этого проекта нам нужна папочка lights (это виджет с гирляндами) и скрипт snowstorm.js.

Чтобы все это у меня лежало в порядке в одном месте, я закидываю скриптик snowstorm.js в папку lights, а саму папку lights мы закидываем в наш проект.

Затем на страничке, на которой мы хотим устроить новогодний снежный шторм, подключаем соотв. виджеты:

  <link rel="stylesheet" media="screen" href="lights/lights.css" />
  <script src="lights/soundmanager2-nodebug-jsmin.js"></script>
  <script src="http://yui.yahooapis.com/combo?2.6.0/build/yahoo-dom-event/yahoo-dom-event.js&2.6.0/build/animation/animation-min.js"></script>
  <script src="lights/christmaslights.js?2015"></script>
 
  <script src="lights/snowstorm.js"></script>

Гирляндочки тянут за собой soundmanager2 (крутая либа от того же автора) – для звука взрывающихся гирлянд, и yahoo-dom-event – либу от Yahoo для удобного манипулирования домом. Заметьте, если Вам нужен только снег без гирляндочек, все эти файлики не нужны, надо подключить лишь snowstorm.js и всё!

Но какой Новый Год без гирляндочек, верно? Поэтому нам придётся повозиться.

Далее на страничке где-то вверху ставим div для гирлянд:

<div id="lights">
<!-- Гирлянды будут здесь -->
</div>

В принципе и всё. Можно перезагрузить страницу и наслаждаться гирляндами и снегом! Ура, с праздником вас!

Но! jQuery?

Для работы снега и гирлянд jQuery не нужен.

Но. Но если мы используем jQuery и ествественно везде у нас стоит знак доллара, то lights будут ругаться. Дело в том, что автор скрипта додумался использовтаь этот знак для обертки функции getElementById. Открываем файлик christmaslights.js и в самом верху видим такое:

function $(sID) {
  return document.getElementById(sID);
}

Ну что за… Меняем здесь $ на что-то нейтральное, например, на __gid, а затем меняем на этой же страничке ниже все вхождения $ на __gid (их всего три что ли, не помню точно, но мало, можно было вообще с оберткой не заморачиваться имхо).

Всё, после этого jQuery заработает параллельно с lights!

Убираем звук

После того, как я поставил эти виджеты на страничку, мне позвонили и сказали – А что, там гирлянды со звуком взрываются? Я – Да, это же прикольно! Мне – Убери его, он пугает.

Кого может напугать этот милый чудесный звук лопающихся гирлянд?

Но хозяин – барин, и мне пришлось скрепя сердце убрать звук. Как? Не скажу, чтобы вы тоже этого не делали (шутка).

В том же самом файлике christmaslights.js находим (примерно на 318 строке) такой кусок кода:

if (soundManager && soundManager.ok()) {
  soundManager.play(self.soundID,{pan:self.pan});
  // soundManager.sounds[self.soundID].play({pan:self.pan});
  // if (self.bonusSound != null) window.setTimeout(self.smashBonus,1000);
}

(здесь комментарии не мои, а автора).

Как нетрудно догадаться, почесав репу пару минут (а может и пару часов), здесь происходит создание звука. Теперь просто закомментируем строчку soundManager.play(self.soundID,{pan:self.pan});, сохраним файлик, аплодим его, обновляем HTML – и вуаля. Звука нет!

И это ещё не всё!

Внимательный читатель заметит на страничке http://www.schillmania.com/projects/snowstorm/ описание целого API для управления снегом. Не буду перечислять все возможности этого мощного API, но кое-что отмечу.

Мы использовали такой код в придачу при старте снега (читайте комментарии того, что делает каждая строка):

snowStorm.excludeMobile = false; // включаем поддержку на мобильных устройствах
snowStorm.autoStart = false; // при загрузке страницы не стартуем сразу снег
snowStorm.flakesMaxActive = 50; // максимально кол-во одновременно активных снежинок
snowStorm.freezeOnBlur = true; // останавливать эффект снега при деактивации странички (наверное, полезно всем)
setInterval(snowStorm.toggleSnow, 15000); // вкл/выкл. снег каждые 15 секунд - это уже моё великое изобретение :)

Ещё пара интересных имхо свойств и методов этого замечательного API:

Свойства

snowStorm.followMouse = true; – заставим снег следовать за горизонтальным перемещением указателя мыши.
snowStorm.snowColor = ‘#fff’; – цвет снега – белый, он и так белый, не жёлтый же!
snowStorm.useTwinkleEffect = true; – позволим снегу мерцать периодически во время падения

Методы

snowStorm.freeze(); – заморозим снег
snowStorm.resume(); – разбудим снег из замороженного состояния
snowStorm.toggleSnow(); – вкл/выкл эффект снега
snowStorm.stop(); – полностью убрать эффект снега со странички. Чтобы не замерзнуть совсем…

Всё, на сим раскланиваюсь, надо FTP дорабатывать, я встал что-то на нём.

Благодарю автора снега Скотта Шиллера (Scott Schiller) за этот и другие классные проекты, а также вас, уважаемый читатель, за внимание и стремление к новым знаниям!

Удачи, успеха и удовольствия от работы!
С наступающими праздниками вас, друзья – всех с Великим Новым Годом, и православных – с Великим Рождеством!

JavaScript: обход атрибутов элемента

Методы JavaScript DOM .getAttribute() и jquery .attr() позволяют получить только один атрибут элемента по его имени. А что делать если нам нужно обойти все заданные атрибуты элемента, чтобы, например, определить какие-то параметры, и нам не важно точное имя атрибута?

Здесь показан обход аттрибутов методом JavaScript DOM и заполнение массивов nodes и values их именами и значениями соответственно (с http://stackoverflow.com/questions/2048720/get-all-attributes-from-a-html-element-with-javascript-jquery, ответ Roland Bouman, который получил всех больше плюсов):

var nodes=[], values=[],
    el = document.getElementById('someId');
for (var attr, i = 0, attrs = el.attributes, n = attrs.length; i < n; i++){
    attr = attrs[i];
    nodes.push(attr.nodeName);
    values.push(attr.nodeValue);
}

А если мы используем jquery, можно получить атрибуты так (c http://stackoverflow.com/questions/14645806/get-all-attributes-of-an-element-using-jquery, красивый ответ pimvdb) :

$(this).each(function() {
  $.each(this.attributes, function() {
    if(this.specified) {
      // Теперь this.name - имя атрибута
      // this.value - его значение (JavaScript DOM)
    }
  });
});

Примечание: в стрых IE мы получаем все возможные атрибуты, не только те, которые представлены в элементе. Поэтому мы используем свойство specified.

Автор ответа тут же указал дополнение к jquery, добавляющее вызов метода .attr() без параметров к любому элементу, который возвращает объект со всеми атрибутами этого элемента:

(function(old) {
  $.fn.attr = function() {
    if(arguments.length === 0) {
      if(this.length === 0) {
        return null;
      }
 
      var obj = {};
      $.each(this[0].attributes, function() {
        if(this.specified) {
          obj[this.name] = this.value;
        }
      });
      return obj;
    }
 
    return old.apply(this, arguments);
  };
})($.fn.attr);
 
// Использование:
var $div = $("<div data-a='1' id='b'>");
$div.attr();  // { "data-a": "1", "id": "b" }

Красиво, да? Автору ответа – большой плюс.

Правда сам на практике я ещё не проверял эти методы (кроме первого, он прекрасно работает).

До встречи, друзья, отличного настроения, крепкого сна и чистого неба над головой!

Хачим jquery.easyBox

Представляем EasyBox

Недавно нашёл неплохой lightbox-плагин для jQuery – EasyBox (https://code.google.com/p/easybox/)

Вот его описание и главные характеристики:
EasyBox это клон lightbox’а базирующийся на скрипте Slimbox2. Он предлагает множество различных дополнений и возможностей, как то:

  • отображение видео YouTube, Vimeo и DailyMotion
  • отображение строчного контента
  • отображение iframe’ов
  • улучшенная анимация
  • поддержка тем

И ещё:
Что делает EasyBox отличным?

  • распознает пропорции и разеры видео
  • отображает сообщения об ошибке если рисунок или ссылка недоступны
  • сжимает контент до размера страницы
  • сохраняет события JavaScript на строчных элементах
  • поддерживает управление колесиком мыши
  • поддерживает слайдшоу
  • поддерживает Drag & Drop

Неплохо, да?

Подключаем EasyBox к нашему проекту

Подключаем его так (самый простой вариант):

Скачиваем его отсюда например: http://easybox.googlecode.com/files/easybox-v1.4.zip
(если вы читаете это не в 2014-начале 2015 года, возможно, появится новая версия, можно пошариться на его родном сайте, хотя я не знаю что там можно ещё улучшить).

Распаковываем его в наш проект в отдельную папку (конечно, иерархию папок вы можете использовать абсолютно любую).

В head страницы добавляем ссылки на CSS с нужной темой:

<link rel="stylesheet" href="/easybox/styles/default/easybox.min.css" type="text/css" media="screen" />

В данном случае это тема default, но можно поиграться и с другими темами.

Подключим jquery, например так:

<script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js"></script>

А затем ссылки на сам easybox, либо так:

<!-- объединенный -->
<script type="text/javascript" src="/easybox/distrib.min.js"></script>

Или три ссылки:

<script type="text/javascript" src="/easybox/easybox.min.js"></script>
<script type="text/javascript" src="/easybox/handlers.min.js"></script>
<script type="text/javascript" src="/easybox/extras/autoload.min.js"></script>

Теперь, чтобы создать ссылку на лайтбокс, помечаем её классом lightbox, например так:

<a href="images/diplom.jpg" title="Красный диплом института искусств им. П. А. Серебрякова" class="lightbox">
  <img src="images/diplom_thumb.jpg" class="white shadow" alt="Диплом">
</a>

В данном случае на странице отображается небольшой рисунок диплома (images/diplom_thumb.jpg), а по щелчку на нем открывается лайтбокс с изображением увеличенного диплома (images/diplom.jpg).

Аналогично можно добавить ссылки на YouTube или iframe, так чтобы они открывались в лайтбоксе:

<a href="http://www.youtube.com/watch?v=VIDEO_ID" title="Video caption" class="lightbox">Моё видео на Ютубе!</a>
<a href="http://www.example.com" title="Example page" class="lightbox" data-width="320" data-height="240">Мой iframe в лайтбоксе!</a>

Вместо VIDEO_ID подставляем естественно ид видео на Ютубе. Для iframe мы также указали его размеры (это делать не обязательно).

Хачим EasyBox

Тут вы спросите резонно – но если этот самый EasyBox так крут, что там можно хачить?
А я так же резонно отвечу – хачить есть всегда что, идеальных продуктов наверное не бывает.

Несмотря на то, что в описании EasyBox‘а помимо всего прочего написано: EasyBox’ look is fully customizable, на самом деле это конечно не так.

Что нам нужно?

  1. Панель управления с описание и кнопкой закрытия (крестиком) вверху окна лайтбокса.
  2. Чтобы верхняя часть содержимого была на виду (если не помещается по вышине, то должна быть у границы верхнего viewport’а браузера, а не выезажть за её границу).

EasyBox раскрывает лайтбокс с содержимым, центрируя его по высоте и ширине, при необходимости его чуть ужимая – но большое содержимое всё равно может не влезть целиком в окно (viewport) браузера.

Панель управления (называется bottom) с подписью и кнопкой закрытия (а при необходимости кнопками управления слайдшоу) находится внизу окна, и никак его вверх опцией не поставишь.

Что делать?
Берём девелоперскую версию easybox.js (не minified) с github’а и работаем с ней.
Версия: Easybox v1.4

Итак, для выполнения первого пункта ищем инициализацию лайтбокса, именно там должны выставляться элементы в определённом порядке.

В пределах 70-ой строки находим такой код:

/*
	Initialization
*/
$(function() {
	$(doc.body).append(
		$([
			overlay = $('<div id="easyOverlay" />').click(userClose)[0],
			center = $('<div id="easyCenter" />').append([
				container = $('<div id="easyContainer" />')[0],
				loadingIndicator = $('<div id="easyLoadingIndicator" />')[0],
				bottom = $('<div id="easyBottom" />').append([
					navLinks = $('<div id="easyNavigation" />').append([
						prevLink = $('<a id="easyPrevLink" href="#" />').click(previous)[0],
						nextLink = $('<a id="easyNextLink" href="#" />').click(next)[0]
					])[0],
					closeLink = $('<a id="easyCloseLink" href="#" />').click(userClose)[0],
					slideLink = $('<a id="easySlideLink" href="#" />').click(toggleSlide)[0],
					caption = $('<div id="easyCaption" />')[0],
					number = $('<div id="easyNumber" />')[0]
				])[0]
			])[0]
		]).css("display", "none")
	);
});

Лайтбокс состоит из 2-х основных частей – overlay, необходимого для затеменения и перекрытия главного окна браузера, и center – центральная часть лайтбокса с содержимым и панелью управления.

Блок center в свою очередь содержит три части – container – контейнер для содержимого, loadingIndicator – индикатор загрузки, прячущайси в итоге под содержимым, и bottom – так у EasyBox неприхотливо названа панель управления. Само название bottom (с англ.: низ, дно) свидетельнствует о том, что автор изначально помещает его под содержимым.

А нам надо над. И мы берем строку с container, вырезаем её и переносим под bottom, убрав после него запятую (разделяющую элементы массива). А после bottom соотв. запятую убираем, иначе JavaScript парсер будет жестко ругаться.

В итоге получаем такой код:

	/*
		Initialization
	*/
	$(function() {
		$(doc.body).append(
			$([
				overlay = $('<div id="easyOverlay" />').click(userClose)[0],
				center = $('<div id="easyCenter" />').append([
					loadingIndicator = $('<div id="easyLoadingIndicator" />')[0],
					bottom = $('<div id="easyBottom" />').append([
						navLinks = $('<div id="easyNavigation" />').append([
							prevLink = $('<a id="easyPrevLink" href="#" />').click(previous)[0],
							nextLink = $('<a id="easyNextLink" href="#" />').click(next)[0]
						])[0],
						closeLink = $('<a id="easyCloseLink" href="#" />').click(userClose)[0],
						slideLink = $('<a id="easySlideLink" href="#" />').click(toggleSlide)[0],
						caption = $('<div id="easyCaption" />')[0],
						number = $('<div id="easyNumber" />')[0]
					])[0],
					container = $('<div id="easyContainer" />')[0]
				])[0]
			]).css("display", "none")
		);
	});

Красота! Один пункт задачи мы выполинили, переходим к следующему.

Парсим глазами скрипт easybox.js далее, ищем место где лайтбокс появляется на экране (EasyBox использует шикарную анимацию).

Мы помним, что основной блок лайтбокса называется center, поэтому ищем что-то связанное с ним (я просто искал по целому слову center).

Нам надо найти изменение CSS-свойства top – ведь мы хотим разместить лайтбокс вверху окна, если он не влезает во viewport, и поэтому нам нужна вертикальная координата.

Но не находим ничего похожего по смыслу. Зато наш взгляд упирается в функцию animateCenter (начало её на строке 260). Судя по всему, эта функция как раз отвечает за анимирование и позиционирование лайтбокса в окне браузера. Наш пристальный и жесткий взгляд упирается в эту строчку:

// resize center
if ((center.offsetHeight != size[1]) || (center.offsetWidth != size[0]))
  p = {height: size[1], marginTop: -size[1]/2, width: size[0], marginLeft: -size[0]/2};

Да, именно здесь происходит позиционирование лайтбокса. Оказывается, easyBox использует не CSS-свойство top, а CSS-свойство margin-top (или marginTop в JS, что то же самое).

Значение marginTop: -size[1]/2 помещает лайтбокс в центр окна по вертикали (size[1] – размер лайтбокса по высоте).
Именно его нам надо подобратьь таким образом, чтобы лайтбокс начинался от верха окна, если он слишком высок.

Начинаем экспериментрировать. Пишем здесь marginTop: 0 и тестируем появление лайтбокса. Ууупс, он появился с верхом по центру окна. Значит, нам надо задать значение, равное половине высоты окна со знаком минус (чтобы лайтбокс ушел вверх, а не вниз от центра). В jquery высота viewport’а браузера находится простым методом $(window).height().

Значит, мы задаем значение так: marginTop: -($( window ).height() / 2)

Сохраняем скрипт, тестируем страничку – все получилось, вроде это то, что нам нужно!

Задание готово, получаем гонорар и идём спать! 🙂 Или читать интересные книги.

Да, итоговая функция animateCenter станет выглядеть так:

	function animateCenter(size, opacity, duration) {
		centerSize = size.slice();
		var p = {};
 
		// resize center и
		// подгоняем его к верхней границе browser's viewport
		if ((center.offsetHeight != size[1]) || (center.offsetWidth != size[0]))
			p = {height: size[1], marginTop: -($( window ).height() / 2), width: size[0], marginLeft: -size[0]/2};
		if (opacity > -1)
			p.opacity = opacity;
		$(center).animate(p, duration, options.resizeEasing);
	}

До встречи, друзья, вкусного чёрного чая с имбирем в сахаре!

Запуск Youtube или onclick на iframe

Цели

Сегодня встала задача – при начале воспроизведения YouTube-ролика, вставленно через iframe, запустить таймер.

Попытка №1 – videojs

Можно обернуть youtube ролик videojs и используя плагин videojs-youtube (https://github.com/eXon/videojs-youtube) наверное отлавливать событие типа onplay или как оно там называется.

Но мне это показалось геморрно, тем более на страничке с плагином videojs-youtube (по ссылке выше) написано:

Install

You can use bower (bower install videojs-youtube), npm (npm install videojs-youtube) or the source and build it using grunt. Then, the only file you need is dist/vjs.youtube.js.

Интересно, в каком мире живет автор этого плагина? Какого хера, я сижу в Windows 8.1, где мне взять этот гребанный vjs.youtube.js? Я полазил по папкам плагина здесь же, на github’е, но не нашел. Задним умом я понимаю что поиском через минуту он окажется у меня, но злость моя затмила мне разум, и я послал этот плагин далеко.

Итак, videojs при всём уважении к авторам отпадает.

Способ №2 – YouTube Player API Reference for iframe Embeds

У гугловодов есть два вида API посвященный процессингу воспроизведения роликов с Youtube.

Первый – YouTube JavaScript Player API Reference предназаначен для проигрывателей встраиваемых в страницу через object тэг (Flash Player 10.1 или более поздней версии) или через Chromeless Player.

Но этот способ мне не подходжил, так как я имел видео через iframe.

И тут же вижу прекрасную ссылку на YouTube Player API Reference for iframe Embeds и радостно потираю руки – https://developers.google.com/youtube/iframe_api_reference#Loading_a_Video_Player

Как раз то что мне нужно, вроде бы! Беглый обзор этого API позволил создать тестовый прообраз player’а, через который можно было перехватывать событие onStateChange, чтобы в итоге перехватить событие onplay.

Но ни хера! При производстве ролика с ид M7lc1UVf-VE всё было ОК, все функции API работали как заявлено в руководстве, но стоило мне поменять ид видео на 9kyNeSNO8Ck, как событие onStateChange переставало перехватываться, о чём свидетельствовали вызовы console.log в нужных местах.

Используя Fireox Firebug панель, я увидел такую ошибку:

"NetworkError: 404 Not Found - http://www.youtube.com/get_video?noflv=1&video_id=9kyNeSNO8Ck&cpn=0OwdBht95fxY5Vtu&el=embedded&referrer&eurl=http%3A%2F%2Fgoloslogos.ru%2Fgoloslad.html&fmt=18&ptk=youtube_none"

Во время воспроизводства предыдущего ролика такой ошибки не было. Похоже, дело в ней. Но откуда она берется? Ведь всё, что я сделал, это поменял видео ид для YouTube, больше ничего!

Беглый обзор гуглопоиска показал эту проблему – https://code.google.com/p/gdata-issues/issues/detail?id=6529 – и то что она до сих пор не решена. Похоже корни этой проблемы лежат в настройках автора YouTube видео, то есть является ли он партнёром YouTube ли нет, но дальше копать не было времени и желания, какого хрена я за гугль должен решать их проблему, они мне миллиардов не платят.

Поэтому YouTube Player API – в топку!

Способ №3: iframe.onclick

И тут меня по голове тюкнуло – мне нужно перехватить только запуск видео, который происходит по щелчку в любом месте прогрывателя, спрятанного в iframe. Но проблема в том, что обычный jQuery $(window).click(function(){}) не ловит это событие внутри iframe.

Поиск показал ложное решение этой проблемы:

document.getElementById("iframe_id").contentWindow.document.body.onclick = 
function() {
  alert("iframe clicked");
}

Дело в том, что этот в принципе верный способ не работает при кросс-доменных запросах, а youtube.com и даже youtu.be никак не относятся к моему домену.

Ниже я нашел это (вольный перевод с английского):

“Можно использовать решение, основанное на событиях blur и mouseover/mouseout. Плагин господина Vince iframeTracker jQuery plugin разработан как раз для детекта кликов по iframe’ам : https://github.com/finalclap/iframeTracker-jquery”

Итак, плагин iframeTracker-jquery, очень интересно и звучит заманчиво. Я его подключил к нашему проекту, написал такой прмерно код:

jQuery(document).ready(function($){
// id моего iframe с YouTube - player
    $('#player').iframeTracker({
        blurCallback: function(){
        // здесь происходит событие клик на iframe
            startMyTimer();
        }
    });
});

И всё получилось! При клике на ролике YouTube он запускался, и также запускалась наша функция startMyTimer()!

Благодарности

Спасибо автору этого замечательного jQuery плагина, низкий ему поклон за работу.

Моя задача оказалась успешно решенной, я получил свои очередные $1100 (тысячу сто баксов) и пошёл заниматься своими делами, коих у меня невпроворот.

Чего и Вам искренне советую, дорогие друзья!

До встречи на Альбионе.