Посты с тэгом «css»

  • Веб-разработка в Eclipse: HTML и CSS

    Когда-то давно, работая ещё в Студии Лебедева, я рассказывал про использование Eclipse IDE в веб-разработке. Тогда моими основными инструментами были Aptana для HTML и CSS и Spket IDE для JS. С тех пор много воды утекло: Aptana всё дальше отходила от идей Eclipse IDE, всё больше превращаясь в самостоятельный продукт для Rails-разработки, а Spket IDE не обновлялся с октября 2009. В это же время проекты, с которыми я работаю, становились всё больше и сложнее и текущего инструментария уже не хватало. Руководствуясь принципом «если хочешь, чтобы что-то было сделано хорошо — сделай это сам» я принялся допиливать IDE до нужного мне состояния.

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

    Как я уже написал, Aptana превратилось в некого монстра, с избыточным для не-Rails технологам функционалом. Поэтому в качестве платформы я выбрал более простой и, главное, лучше интегрированный в IDE плагин — Eclipse Web Tools Project.

    Eclipse Web Tools Project

    Как можно понять из названия, Eclipse WTP — это инструменты для веб-разработки, а не законченный проект (как Aptana, например). Цель этого проекта: создать платформу, на основе которой можно создавать другие, узкоспециализированные инструменты. Этот проект включает в себя редакторы для CSS, HTML, XML, XSL, JavaScript (отдельный большой проект под названием Eclipse JSDT; о нём расскажу в следующих частях). Проект частично включён практически во все официальные сборки Eclipse, но для начинающих пользователей рекомендую ставить Eclipse for JavaScript Web Developers, в ней есть всё.

    Чтобы не пересказывать документацию, кратко приведу основные возможности:

    • подсветка кода;
    • сворачивание/разворачивание кусков кода (code folding);
    • outline и quick outline (на последний рекомендую обратить особое внимание, по умолчанию вызывается через Ctrl+O или ⌘O);
    • выделение границ тэгов (aka Balancing), смотрите в основном меню Edit > Expand Selection To;
    • content assist по тэгам, атрибутам и значениям некоторых атрибутов;
    • quick fix: переименование тэга (автоматически меняет открывающий и закрывающий тэги), обрамление тэгом;
    • форматирование всего/выделенного кода;
    • автоматическое закрытие тэгов.

    Итак, с платформой определились, теперь нужно её настроить для комфортной работы.

    Включаем поддержку HTML5 и CSS3

    Eclipse WTP в версии 3.3 (которая доступна для Eclipse Indigo) появилась поддержка тэгов HTML5 и некоторых свойств CSS3. Однако сразу они не доступны, нужно немного пошаманить с проектом:

    1. Открываем свойства проекта: Project > Properties.
    2. Переходим на вкладку Project Facets и жмём на ссылку Convert to faceted form…
    3. В появившемся окне выбираем фасет Static Web Module, жмём Apply.
    4. Закрываем окно с настройками проекта.

    Теперь в content assist будут доступны новые тэги и свойства. Если нет — снова открываем свойства проекта и идём на вкладку Web Content Settings, где настраиваем профили для HTML и CSS.

    Включаем проверку правописания

    Eclipse IDE с версии 3.4 (если мне не изменяет память) поддерживает проверку правописания, и по умолчанию доступен только английский словарь. Чтобы включить проверку правописания русского языка, делаем следующее:

    1. Скачиваем и распаковываем русский словарь.
    2. Открываем настройки Eclipse, идём в General > Editors > Text Editors > Spelling
    3. В User defined dictionary выбираем распакованный ru.dict.
    4. Ставим кодировку UTF-8

    Всё, теперь, если редактор, которым вы пользуетесь, написан по гайдам Eclipse Platform, вам будет доступна проверка правописания.

    Eclipse WTP Sugar

    Самую большую ломку при переходе с Aptana на Eclipse WTP у меня вызывало отсутствие content assist для путей к файлам (например, в <script src="...">): приходилось писать все пути к файлам вручную, что сильно утомляло. Да и в Aptana он был не идеальным: работал только внутри атрибута src (<script src="...">, <img src="..."> и т.д.), но не работал внутри href (<link href="...">, <a href="...">) и уж тем более не в CSS (background url(...), @import url(...)).

    Поэтому, потратив несколько месяцев на изучение Java и архитектуры Eclipse IDE, я написал свой content assist, который исправит этот недостаток, а заодно несколько других (о них чуть позже).

    Свой проект я назвал Eclipse WTP Sugar, репозиторий для установки: http://media.chikuyonok.ru/eclipse/webdev/updates/, исходный код доступен на GitHub.

    Плагин устанавливается очень просто, как и все остальные:

    1. Идём в Help > Install New Software…
    2. В качестве репозитория (поле Work with) вбиваем http://media.chikuyonok.ru/eclipse/webdev/updates/.
    3. В появившемся списке плагинов выбираем необходимые и жмём кнопку Next.
    4. Далее проходим стандартную процедуру установки (соглашаемся с лицензией, всё время жмём Next или Finish) и перезапускаем Eclipse.

    Теперь, при нажатии Ctrl+пробел внутри атрибутов src и href можно увидеть список файлов:

    ss01

    Список файлов также выдаётся и в CSS, внутри функции url():

    ss02

    Список файлов по возможности фильтруется. Например, если его вызвал внутри тэга <script>, то увидите список файлов с расширением js, если внутри <link rel="stylesheet"> — файлы с расширением css.

    Возвращаясь к разговору о том, что создатели программ для вёрстки сами толком сайты не верстают. Что если нужно использовать абсолютные пути для ссылок на файлы? По умолчанию считается, что абсолютный путь нужно резолвить относительно папки с проектом. Но почему никому не пришла в голову мысль, что веб-пространство проекта не всегда совпадает с корневой папкой проекта? В этом плагине проблема решена: в настройках проекта можно указать, относительно какой папки нужно резолвить абсолютные пути (Project > Properties > Document root):

    ss03

    Quick outline

    Следующее, что хотелось изменить, так это quick outline: специальное контекстное окошко, которое показывает в компактном виде текущую структуру документа:

    ss04

    Эту структуру можно фильтровать, чтобы найти нужны элемент (поиск осуществляется с начала строки, можно использовать символы * и ?). Но, как видно из скриншота, в структуре выводятся только названия тэгов, что делает её малопригодной. WTP Sugar исправляет этот недостаток, выводя гораздо больше полезной информации, по которой, в том числе, можно искать:

    ss05

    Eclipse XV Browser

    Не так давно я написал просмотрщик XML-файлов под названием XV Browser. В том посте я показал, что его можно использовать внутри Eclipse, однако не учёл один важный факт: работать это будет только если у вас Mac, а в качестве основного браузера используется Safari с установленным плагином XV :)

    Eclipse XV Browser исправляет этот недостаток. Он представляет из себя обёртку для встроенного браузера (это может быть Webkit либо Mozilla, в зависимости от ваших настроек и версии Eclipse). С помощью этого плагина можно прросматривать любой сайт в виде XML-структуры (Window > Show View > Other… > XV Browser), либо открывать просматривать XML-файлы проекта (правый клик по файлу, Open With… > XV Browser).

    Пожелания?

    Если есьт какие-то идеи по улучшению плагинов — пишите в Issues проекта. Пока в планах:

    • content assist по классам и идентификаторам, описанных в CSS;
    • быстрый переход к CSS-определению класса или идентификатора;
    • виджеты для редактирования CSS Gradients и CSS Box Shadow.

    В следующей статье рассмотрим, как улучшить работу с Eclipse JSDT (JavaScript).

    Метки: , , ,
  • Список блоков с разным вертикальным выравниванием

    Когда я работал над очередным улучшением сайта БигБаззи, возникла задача добавления нового режима просмотра предложений — по три штуки в ряд, более компактными блоками (было по два крупных блока в ряд):

    example

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

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

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

    scheme

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

    Как ни странно, но решение задачи нашлось, причём довольно элегантное.

    Обычно такие последовательности блоков делаются самым очевидным образом: через float-элементы. Но этот способ нынче не в моде: все правильные ребята уже давно прочитали статью в блоге Мозиллы о том, как делать их через display: inline-block;.

    А что будет, если поставить рядом блоки разной высоты?

    <style type="text/css">
    	.wrap, .h, .f {
    		display: inline-block;
    	}
    
    	.h {
    		background: red;
    		height: 100px;
    		padding: 0 10px;
    	}
    
    	.f {
    		background: blue;
    		height: 20px;
    		padding: 0 10px;
    		color: #fff;
    	}
    </style>
    
    <span class="wrap">
    	<span class="h">header</span>
    	<span class="f">footer</span>
    	<span class="h">header</span>
    	<span class="f">footer</span>
    </span>
    

    Получим вот такой результат:

    ss1

    По умолчанию у всех блоков vertical-align: baseline, то есть вертикальное выравнивание по базовой линии (в данном случае, если проще — по последней строке текста). Если указать для .h{ vertical-align: top; } и для .f { vertical-align: bottom; }, то получим довольно интересное решение:

    ss2

    Блоки выровнены именно так, как нужно. Убеждаемся в правильности решения, изменив высоту одного из блоков:

    ss3

    Дальше всё просто: у шапки и подвала одинаковая и вполне определённая ширина, поэтому прописываем её каждому блоку, а подвал убираем из потока с помощью отрицательного margin’a:

    ss4

    Чтобы семантически объединить шапку и подвал, можно использовать любой элемент с display: inline. Итоговая версия доступна на тестовой странице.

    Самое интересное, что этот код работает даже в IE6. Помним, что inline-block в этом браузере эмулируется с помощью display: inline и волшебного hasLayout, например, так: display: inline; zoom: 1;.

    В этом решении важно помнить следующее:

    1. Шапка и подвал в примере пересекаются, поэтому шапке нужно снизу давать отступ и подтягивать подвал любым доступным способом.
    2. Между элементами конструкции не должно быть переводов строк и пробелов, иначе блоки визуально будут разделены.

    В данном случае мне очень помогло то, что у подвала вполне предсказуемая высота, поэтому у шапки был добавлен снизу большой фиксированный отступ, чтобы блоки не перекрывались. Это решение полностью решило проблему и отображения, и переключения режимов. Можно пойти дальше и подключить CSS3 Media Queries, чтобы обладатели новых браузеров и больших мониторов могли видеть более трёх предложений в ряду, но это уже другая история. Думаю, читатели смогут найти более творческие и интересные применения этого небольшого трюка :)

  • История одной оптимизации

    Летом мы запустили новый купонный проект BigBuzzy. Таких проектов к тому времени было довольно много и чтобы выделиться из толпы мы решили немного поменять бизнес-модель: вместо одного предложения в день выдавать четыре. Но, как это обычно бывает, аппетит приходит во время еды, поэтому уже спустя несколько месяцев на главной странице красовалось не 4, а 30 предложений.

    И мы сразу же начали получать жалобы о жутких тормозах на главной странице. На поиск и устранение проблем у меня ушло два дня. О том, как находились узкие места и будет сегодняшний рассказ. А заодно научимся пользоваться инструментами вроде Web Inspector’s Timeline (если вы их ещё не освоили).

    Поиск проблемы

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

    01-timers

    Самые большие проблемы наблюдались в Firefox: загрузка процессора на главной странице доходила до 70%. Поэтому я начал рассматривать скрипт таймера под микроскопом, а именно в Web Inspector, который по умолчанию входит в состав браузеров Safari и Chrome. Вообще, многие ребята довольно снисходительно относятся к этому инструменту, продолжая по привычке работать в Firebug’е, а зря. Лично для меня Web Inspector стал основным инструментом для отладки: выглядит он приятнее и содержит ряд полезных нововведений.

    Исследуем узкие места

    Так как сам скрипт таймера довольно простой, то не было смысла заниматься его профилированием — проблема явно где-то в reflow и repaint. Поэтому скрипт нужно исследовать через Timeline:

    02-timeline

    Полагаю, что многие читатели ещё ни разу не сталкивались с этим инструментом, поэтому принцип его работы и поиска проблем опишу в небольшом уроке. Стоит отметить, что Web Inspector в Chrome немного круче, чем в Safari, поэтому рекомендую пользоваться первым браузером.

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

    Открываем шаблон в браузере и запускаем Web Inspector, вкладка Timeline. На странице есть красный квадратик и кнопка «Test». Чтобы начать исследование, нужно нажать на кнопку записи вкладки Timeline 03-rec, а потом нажать на кнопку «Test» в основном окне браузера. Наш квадратик посинел и стал больше по высоте, а в Timeline записались следующие события:

    04-test1

    Первые три записи относятся непосредственно к кнопке, которую нажали: применили псевдо-класс :active (Recalculate style), отобразили изменения на экране (Paint), вернули кнопку в исходное состояние, убрав :active (Recalculate style). После того, как пользователь отпустил кнопку мышки сработало событие click, и именно оно и всё, что ниже, нас будет интересовать.

    Во время клика сработал следующий скрипт:

    function test() {
    	var el = document.getElementById('test');
    	el.style.backgroundColor = 'blue';
    	el.style.height = '100px';
    }
    

    Ничего особенного: просто получили ссылку на элемент и поменяли у него цвет фона и высоту. Этот процесс был отображён на временной шкале: пересчитали стили (Recalculate style), пересчитали геометрию объектов (Layout) и отобразили изменения (Paint).

    Как видите, несмотря на то, что мы поменяли два СSS-свойства, пересчёт стилей произошёл всего один раз. Поменяем скрипт:

    function test() {
    	el.style.backgroundColor = 'blue';
    	var height = el.offsetHeight;
    	el.style.width = '100px';
    }
    

    Между присваиванием новых стилей мы решили получить высоту объекта. Но временная шкала сильно преобразилась:

    05-test2

    Теперь у нас уже два события Recalculate style, а у самого события click появилась группировка (треугольник слева от жёлтой полоски), которая указывает, какие именно события произошли во время клика.

    Этот небольшой пример указывает на две очень важные особенности браузеров — это откладывание перерисовки на момент выхода из функции (первый пример) и существование определённых свойств у элемента, которые принудительно вызывают пересчёт стилей (далее restyle; второй пример). О существовании особых свойств, вызывающих restyle, думаю, многие уже знали: это свойства вроде offsetLeft/Right/Width/Height, clientLeft/Right/Width/Height и так далее. Во втором примере, после установки свойства backgroundColor браузер пометил дерево элементов как требующего пересчёта стилей. А обращение к offsetHeight принудительно вызвало этот пересчёт. Затем мы установили свойство width, которое отложило пересчёт стилей, геометрии и отображения на момент выхода из потока JS-функций.

    Отсюда первое правило: нужно стараться не смешивать получение и запись CSS-свойств. Лучше, например, сначала получить нужные свойства элемента, а затем присвоить новые.

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

    function test() {
    	var e = $('#test');
    	var width = e.css('width');
    	if (width == '50px')
    		e.css('width', '100px');
    
    	var height = e.css('height');
    	if (height == '50px')
    		e.css('height', '100px');
    }
    

    Вот его шкала:

    06-test3

    Как видите, помимо лишнего Recalculate style появился Layout (reflow), что сделало выполнение скрипта более медленным. Если немного оптимизировать, переместив получение высоты выше в коде:

    function test() {
    	var e = $('#test');
    	var width = e.css('width'),
    		height = e.css('height');
    
    	if (width == '50px')
    		e.css('width', '100px');
    
    	if (height == '50px')
    		e.css('height', '100px');
    }
    

    …получим совершенно иную картину:

    07-test4

    Лишний Layout (помимо Recalculate style) объясняется тем, что jQuery каждый раз при получении CSS-свойств вызывал window.getComputedStyle(), который принудительно запускает reflow. Справедливости ради стоит отметить, что в функции jQuery.css() есть оптимизация, которая сначала проверяет наличие запрашиваемого свойства в element.style и если его там нет, вызывает window.getComputedStyle(). Но в любом случае, лучше всегда разделять чтение и изменение свойств.

    Таймеры

    Напомню, что я занимался оптимизацией таймеров, которых было несколько. И каждый таймер работает через свой setTimeout(). Посмотрим, что это означает на практике:

    function test() {
    	var el = document.getElementById('test');
    	setTimeout(function(){
    		el.style.backgroundColor = 'blue';
    	}, 10);
    	setTimeout(function(){
    		el.style.width = '100px';
    	}, 10);
    }
    

    08-test5

    У обоих таймеров одинаковый период ожидания и момент исполнения. На шкале видно, что после каждого таймера был запущен пересчёт стилей. Но в реальности момент исполнения будет далеко не всегда одинаковым. Поэтому поменяем задержку у последнего таймера — поставим 11 мс вместо 10 мс:

    function test() {
    	var el = document.getElementById('test');
    	setTimeout(function(){
    		el.style.backgroundColor = 'blue';
    	}, 10);
    	setTimeout(function(){
    		el.style.width = '100px';
    	}, 11);
    }
    

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

    09-test6

    В итоге получалось, что из-за нескольких запущенных setTimeout() срабатывали ненужные перерисовки, которые пользователь всё равно не увидит, но процессор это нагружало прилично. Я переписал код таймеров таким образом, чтобы всё работало через один глобальный таймаут.

    Итак, суммируя всё вышесказанное, для оптимизации я

    • разделил чтение и запись CSS-свойств;
    • дополнительно сделал кэширование текущих значений анимации, чтобы меньше обращаться к элементам;
    • заменил несколько таймаутов на один.

    В итоге в Firefox нагрузка на процессор снизилась… всего на 10%. Вообще, это было крайне странно: даже при наличии всего одного анимированного таймера на странице Firefox грузил процессор на 60%, при том что Webkit грузил всего на 5%. Нужно копать дальше.

    Влияние вёрстки на производительность

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

    Так как все restyle и reflow процессы я оптимизировал, проблема явно была где-то в repaint. Вспомнил, что в Firefox 3.5 появилось событие mozAfterRepaint, которое позволяет увидеть области, которые были перерисованы во время repaint. Для удобства было поставлено расширение Firebug Paint Events, которое позволяет отслеживать перерисовки экрана.

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

    10-bbz

    Я специально оставил только 8 из 30 предложений, чтобы картинка не распирала страницу, но смысл, думаю, ясен: во время анимации даже одного таймера перерисовывалось примерно 90% страницы, 15 раз в секунду. И это при условии, что у цифр таймера указан position:absolute, а у их контейнера overflow:hidden. То есть сама анимация по определению никак не могла повлиять на области вне контейнера (на скриншоте обозначен синим прямоугольником), но перерисовывалась почти вся страница.

    Около часа мне понадобилось на то, чтобы найти причину такого странного поведения. Ей оказалось… свойство float:left у одного из контейнеров. Как только я заменял его на float:none нагрузка на процессор падала ниже 10% (с float:left была около 60%).

    Проблема проявляется стабильно, причём не только в Firefox, но и в Opera и IE8. Я сделал простую демку, где можно в живую увидеть эту проблему. В ней всего несколько блоков, однако у них указан box-shadow — очень тяжелое в плане нагрузки на процессор CSS-свойство. В правом верхнем углу есть кнопка, которая всего лишь переключает float у контейнера. Понаблюдайте за нагрузкой на процессор при разных состояниях кнопки, а также за областью перерисовки.

    В общем виде проблему можно описать так:

    Repaint срабатывает на контейнере самого дальнего родителя, у которого указан float:left|right.

    Схематично это выглядит так:

    11-tree

    Причём проблема не только во float. Я перепробовал различные варианты горизонатльной группировки блоков: display:inline-block, display:table-cell, таблицы и даже новомодные flex box — во всех случаях проблема оставалось. Помогало только абсолютное позиционирование боковых блоков.

    В общем виде я проблему решил: поставил боковую панель в коде перед основным контейнером и только ей указал float. Основной контейнер был без float и repaint происходил именно там, где нужно. Однако на живом сайте решить проблему не удалось, так как на большинстве страниц стояли clearfix-элементы, из-за которых макет разваливался. Поэтому пришлось пока отключить анимацию с таймеров :(

    ***

    Честно говоря, после таких браузерных крендебобелей на всякие пузомерки типа Peackeeper, которыми так хвастаются разработчики с каждым новым релизом своего браузера, без слёз смотреть не получается. Поэтому мой вам совет: заранее узнавайте о всех интерактивных элементах на странице, не увлекайтесь новым CSS3, продумывайте рост сайта заранее и пользуйтесь правильными инструментами для отладки производительности — тогда будет вам счастье и высокая производительность.

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

  • Идея для сниффинга браузеров

    Возникла одна идея, как можно скрестить клиентское и серверное «вынюхивание» браузера.

    Как это делается сейчас? На сервере мы смотрим на заголовок User Agent и определяем, что это за браузер. Но этот заголовок можно легко подменить (причём необязательно это делать в самом браузере, это может сделать прокси-сервер), поэтому такая проверка совсем ненадёжна. На клиентской стороне у нас гораздо больше возможностей: мы можем не только проверить User Agent браузера, но и его возможности (например, поддержку определённых CSS и JS свойств), что даст нам более точные характеристики браузера.

    Собственно, сама идея как раз заключается в том, чтобы скрестить эти два способа: определить с помощью CSS и JS характеристики браузера и в нужный момент сообщить их на сервер. Основа алгоритма — Chunked Transfer Encoding, позволяющий передать ответ (например, HTML-документ) не целиком, а кусками. Итак, сам алгоритм:

    1. Когда пользователь запрашивает HTML-страницу, отдаём ему первый фрагмент (chunk), в котором содержится sniffing-код. После этого соединение удерживается на некоторое время, чтобы отдать второй фрагмент документа.
    2. Браузер, получив первый фрагмент, тут же его парсит и пытается отобразить.
    3. В этом первом фрагменте у нас sniffing-код, результатом работы которого является CSS-свойство background-image, применяемое к элементу <html>.
    4. Путь к этой фоновой картинке является уникальным для каждого браузера и так уж получилось, что как только мы применили это свойство элементу, браузер открывает второе соединение и тут же пытается загрузить эту картинку.
    5. Сервер, получив запрос к нашей «картинке» понимает, каким браузером мы зашли и отдаёт вторую часть ответа в первом соединении.
    6. Если обращения к «картинке» не было (например, зашли поисковым роботом), то через какой-нибудь небольшой промежуток времени отдаём стандартный ответ.
    7. После первого «вынюхивания» можно поставить куку, чтобы потом сразу отдавать нужный ответ.

    В итоге получаем следующие преимущества:

    • Нет никаких редиректов: ответ о типе браузера получаем в одном соединении (полезно для SEO).
    • Очень сложно обмануть такой сниффер: можно до посинения менять строку User Agent, но результат останется прежним. Гугл сможет эффективней банить Оперу.
    • Более сложные проверки: можно не просто определять название браузера, но и его возможности, благодаря CSS Media Queries.

    Для желающих поэкспериментировать я написал демонстрацию этого подхода на node.js. К сожалению, у меня на VPS не хватает памяти, чтобы собрать node.js, поэтому предлагаю читателю самому его установить и проверить у себя. Если кто выложит это в онлайн — буду премного благодарен. Выложили: http://tbms.ru:8126/ (спасибо Николаю Митину).

    Полезные уроки

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

    1. Первый чанк должен быть достаточно большим, чтобы все браузеры смогли его применить. Самый большой лимит — у движка Webkit: около 2 КБ. Поэтому приходится «добивать» первый фрагмент пробелами, чтобы всё работало как надо.
    2. Webkit очень плохо ведёт себя при переопределении CSS-свойств. Например:
      html { background-image:url(image1.png); }
      html { background-image:url(image2.png); }
      		

      В этом случае Webkit (и десктопный, и мобильный) загрузит обе картинки, хотя понятно, что нужна только последняя. Это ещё одно подтверждение, что мобильные версии сайта лучше делать отдельным проектом, а не вставлять через CSS Media Queries. Иначе попытки сократить траффик заменой больших фоновых картинок на маленькие приведёт к обратному эффекту. Хотя тут нужно больше исследований, возможно, это издержки работы с локальным сервером.

    3. Firefox не отработает первый чанк до тех пор, пока не получит элемент <body>. Это довольно серьёзный недостаток в том случае, если вы захотите выдавать уникальные стили и скрипты для браузера в секции <head>. С другой стороны, если послать <script>alert(1)</script>, то чанк отработает нормально, даже без <body>. Что наталкивает на мысль о существовании неких триггеров, которые заставят браузер сделать то, что нам нужно, осталось только найти наиболее безопасный.

    Повторюсь, что это только идея, а не призыв к действию. Буду рад, если кто-то на её основе сделает что-то крутое и интересное.

  • Узнаём строку CSS-правила в Safari/WebKit/Chrome Web Inspector

    В браузерах на движке WebKit есть замечательный инструмент для отладки веб-страниц — Web Inspector. По сути, это аналог Firebug, со своими сильными и слабыми сторонами. В последнее время я всё чаще предпочитаю пользоваться Web Inspector’ом, однако один его недостаток постоянно не давал мне покоя — это отсутствие номера строки редактируемого CSS-правила:

    css-tab

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

    Web Inspector CSS hack

    Установка довольно простая: нужно всего лишь подключить файл SC-CSSAdditions.js в inspector.html (Safari, WebKit) или devtools.html (Google Chrome). Подробная инструкция написана на странице проекта.

    Как это работает

    Весь Web Inspector практически полностью базируется на возможностях браузера, предоставляемых спецификацией W3C. Соответственно, номер строки CSS-правила просто так не узнаешь. Я написал альтернативный CSS-парсер, который достаёт из исходника правила, но при этом сохраняет их позицию в коде. Перед тем, как Web Inspector выводит список правил, принадлежащих элементу, происходит сопоставление этих правил с моими (по позиции в списке или по селектору) и к имени файла добавляется номер строки:

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

  • Улучшаем text-align: justify

    Считается, что выравнивание текста на всю ширину делают только дилетанты. Причина: огромные просветы между словами. Даже если в тексте расставлены переносы, результат всё равно получается не самый лучший:

    Не люб­лю я вос­крес­ные ве­че­ра. Как бы объ­яс­нить… все, что с ни­ми свя­за­но, то есть са­му об­ста­нов­ку вос­крес­но­го ве­че­ра, я не люб­лю. С при­бли­же­ни­ем вос­крес­но­го ве­че­ра у ме­ня обя­за­тель­но на­чи­на­ет дер­гать в го­ло­ве. Ког­да силь­нее, ког­да сла­бее. Но дер­га­ет обя­за­тель­но. Внут­ри вис­ков, в сан­ти­мет­ре или двух от ко­жи, дер­га­ет с обе­их сто­рон так, как буд­то что-то тя­нет мяг­кий бе­лый сгус­ток пло­ти на­ру­жу. Ощу­ще­ние, что из се­ре­ди­ны вис­ка вы­ле­за­ет не­ви­ди­мая нить, а кто-то из­да­ле­ка, схва­тив за са­мый кон­чик, по­ти­хонь­ку дер­га­ет за нее. Мне не осо­бен­но-то и боль­но. Я бы не уди­вил­ся, если бы бы­ло боль­но, но, как ни стран­но, не боль­но. Буд­то глу­бо­ко вве­ли длин­ную иг­лу в оне­мев­шее от нар­ко­за мес­то.

    Но в случае с переносами текст можно заметно улучшить. Достаточно всего лишь уменьшить расстояние между словами с помощью CSS-свойства word-spacing, выставив минимально допустимое расстояние. Важно помнить, что в word-spacing указывается не само расстояние, а его отклонение от стандартного значения: word-spacing: 0 — стандартное расстояние, word-spacing: 10px; — стандартное расстояние + 10 пикселей.

    Немного подтянем слова друг к другу:

    p {
    	text-align: justify;
    	word-spacing: -0.3ex;
    }
    

    …и текст выглядит заметно лучше:

    Было

    Не люб­лю я вос­крес­ные ве­че­ра. Как бы объ­яс­нить… все, что с ни­ми свя­за­но, то есть са­му об­ста­нов­ку вос­крес­но­го ве­че­ра, я не люб­лю. С при­бли­же­ни­ем вос­крес­но­го ве­че­ра у ме­ня обя­за­тель­но на­чи­на­ет дер­гать в го­ло­ве. Ког­да силь­нее, ког­да сла­бее. Но дер­га­ет обя­за­тель­но. Внут­ри вис­ков, в сан­ти­мет­ре или двух от ко­жи, дер­га­ет с обе­их сто­рон так, как буд­то что-то тя­нет мяг­кий бе­лый сгус­ток пло­ти на­ру­жу. Ощу­ще­ние, что из се­ре­ди­ны вис­ка вы­ле­за­ет не­ви­ди­мая нить, а кто-то из­да­ле­ка, схва­тив за са­мый кон­чик, по­ти­хонь­ку дер­га­ет за нее. Мне не осо­бен­но-то и боль­но. Я бы не уди­вил­ся, если бы бы­ло боль­но, но, как ни стран­но, не боль­но. Буд­то глу­бо­ко вве­ли длин­ную иг­лу в оне­мев­шее от нар­ко­за мес­то.

    Стало

    Не люб­лю я вос­крес­ные ве­че­ра. Как бы объ­яс­нить… все, что с ни­ми свя­за­но, то есть са­му об­ста­нов­ку вос­крес­но­го ве­че­ра, я не люб­лю. С при­бли­же­ни­ем вос­крес­но­го ве­че­ра у ме­ня обя­за­тель­но на­чи­на­ет дер­гать в го­ло­ве. Ког­да силь­нее, ког­да сла­бее. Но дер­га­ет обя­за­тель­но. Внут­ри вис­ков, в сан­ти­мет­ре или двух от ко­жи, дер­га­ет с обе­их сто­рон так, как буд­то что-то тя­нет мяг­кий бе­лый сгус­ток пло­ти на­ру­жу. Ощу­ще­ние, что из се­ре­ди­ны вис­ка вы­ле­за­ет не­ви­ди­мая нить, а кто-то из­да­ле­ка, схва­тив за са­мый кон­чик, по­ти­хонь­ку дер­га­ет за нее. Мне не осо­бен­но-то и боль­но. Я бы не уди­вил­ся, если бы бы­ло боль­но, но, как ни стран­но, не боль­но. Буд­то глу­бо­ко вве­ли длин­ную иг­лу в оне­мев­шее от нар­ко­за мес­то.

  • Data:URL средствами браузера

    Думаю, зачем нужен data:url объяснять не стоит. Несмотря на проблемы с применением в IE, data:url незаменим, когда нужно отдавать HTML-страницу в виде одного файла (очень удобно такие файлы кэшировать в приложениях). Мне в последнее время приходится с ним довольно много работать и я был озадачен поиском хорошего инструмента, который смог бы легко кодировать графические файлы в base64. Критерии к инструменту следующие:

    • drag’n’drop;
    • возможность конвертации сразу нескольких файлов;
    • удобное копирование конечного результата;
    • должен работать на Маке;
    • няшный интерфейс с прЕкольными анимашками )))))))

    Беглый поиск в интернете удовлетворительных результатов не дал. Есть утилитка от Sveinbjorn Thordarson, которой я пользовался раньше, но она очень неудобная: принимает только по одному файлу, а результат нужно вычленять из <img>-тэга (там есть ссылка на маковский дроплет, который у меня не завёлся). Duris смущает тем, что требует готовую страницу, доступную где-то в интернете (слишком сложно для преобразования 2—3 файлов).

    Поэтому я решил создать свой веб-сервис, который будет удовлетворять этим требованиям. Когда я только принялся его писать, возникло здоровое чувство жлобства: зачем мне создавать онлайн-сервис, которым бесплатно будут пользоваться десятки, а то и сотни человек ежедневно, да ещё и за трафик платить из своего кармана? Может, современные браузеры уже могут делать такие штуки без помощи сервера?

    Как оказалось, вполне себе могут. Вот что у меня получилось. Работает, правда, не везде: только Firefox 3.6, Safari 4, последний Google Chrome (только Windows-версия). Для Safari и Chrome нужно сначала скачать себе эту страничку и запустить её локально (почему так узнаете, дочитав статью). Несмотря на такие ограничения, поддержки этих браузеров вполне достаточно для современного веб-разработчика.

    Дальше будет много текста про то, как создавался этот сервис.

    Начало

    Помимо выполнения сугубо утилитарных функций (кодирование бинарного изображения в base64 налету), этот сервис в первую очередь был плацдармом для обкатки новых браузерных технологий, о которых все говорят, но толком не используют. В частности, это CSS Transforms, CSS Transitions и CSS Animations. Поэтому работа была более исследовательской, нежели практической. В качестве инструментария я выбрал jQuery для рутинных DOM-операций и свой jTweener для анимаций (его очень легко адаптировать под любые CSS-свойства). Итак, начнём.

    Firefox

    В новом Firefox 3.6 появилось много нововведений, одно из которых — поддержка drag’n’drop. Причём таскать и бросать можно не только локальные блоки на веб-странице, но и внешние файлы. Перетаскиваемые файлы можно «поймать» с помощью JS и что-нибудь с ними сделать, не перезагружая страницу. Для этих целей существуют специальные объекты вроде Clipboard и FileReader, позволяюще получить и тут же прочитать файлы.

    Вообще, в Firefox 3.6 задача кодирования перетаскиваемых файлов в base64 решается элементарно: вешаем на окно обработчик события drop, в котором из объекта события достаём список перетаскиваемых файлов и читаем их с помощью FileReader. Примерный код решения:

    function handleFiles(evt) {
    	// запрещаем бразеру открывать перетаскиваемые файлы
    	evt.stopPropagation();
    	evt.preventDefault();
    
    	// список перетаскиваемых файлов
    	var files = evt.dataTransfer.files;
    
    	// читаем файлы
    	for(var i = 0; i < files.length; i++) {
    		var reader = new FileReader();
    		reader.onloadend = function(e) {
    			// в e.target.result содержится изображение в формате data:url
    			console.log(e.target.result);
    		};
    		reader.readAsDataURL(files[i]);
    	}
    }
    
    document.addEventListener('drop', handleFiles, false);
    

    Обратите внимание вот на что: если используете jQuery, то привязать событие drop через $(document).bind('drop', handleFiles); не получится, привязывать нужно именно через стандартный DOM-метод addEventListener(). Похоже, jQuery ещё не знает о таком событии.

    В принципе, на кодировании изображений только в Firefox 3.6 можно было и остановиться. Но моим главным инструментом является Safari, поэтому решил попытаться реализовать поддержку и это браузера.

    Safari (Webkit)

    Четвёртый Safari тоже поддерживает drag’n’drop, но получить содержимое файла в этом браузере оказалось сложнее (в то же время гораздо интереснее).

    Начнём с того, что Safari не поддерживает FileReader, то есть читать файлы просто нечем. В evt.dataTransfer.files (см. предыдущий пример) содержится список объектов класса File, у которого есть только 2 публичных свойства: fileName и fileSize. Мы можем получить только имя файла, но не полный путь к нему.

    Нужно найти способ, как получить полный путь к файлу, чтобы можно было хоть как-то загрузить его в браузер. Рассматриваем внимательно объект evt.dataTransfer, который является объектом класса Clipboard. У него есть метод getData(), позволяющий получить содержимое буффера обмена. Но этому методу нужно передать строку с названием типа данных, в котором хотим получить данные. Доступные типы определены в свойстве types. Простым перебором выясняем, что если передать тип text/uri-list, то получим список абсолютных путей ко всем файлам, разделённый переводами строк:

    function handleFiles(evt) {
    	// список перетаскиваемых файлов
    	var files = evt.dataTransfer.getData('text/uri-list');
    
    	console.log(files);
    	// выведет:
    	// file:///path/to/image1.png
    	// file:///path/to/image2.jpg
    }
    

    Итак, полдела сделано: мы получили список абсолютных путей к файлам, которые можем загрузить в браузер. Первая мысль, которая у меня возникла: создать <img> тэг, указав в качестве src путь к файлу, а после загрузки отрисовать его в canvas и получить data:url через метод toDataURL(). Однако эту мысль сразу же отбросил: во-первых, мы получим совершенно новое изображение, а не то, которое отдавали. Во-вторых, не понятно, что делать в JPEG-изображениями. В toDataURL() можно отдать тип, в котором хотим получить файл, но это будет то же самое, что сохранить JPEG как PNG, а потом обратно сохранить в JPEG, но уже с неизвестными параметрами сжатия.

    Второй способ, который пришёл мне в голову, это воспользоваться старым добрым XMLHttpRequest для загрузки файла. В принципе, идея неплохая, но есть одно жирное «но»: данные в responseText будут автоматически перекодированы браузером в текущую кодировку, что, естественно, нарушит целостность данных. В свежих версиях XMLHttpRequest (например, в IE8) есть свойство responseStream, в котором содержатся «чистые» байты файла, но Safari его не поддерживает.

    Выходом оказался хак, найденный где-то на MDC. Суть его заключается в том, что если у объекта класса XMLHttpRequest переопределить тип и кодировку файла с помощью метода overrideMimeType(), то в responseText окажутся правильные данные. В последних версиях jQuery у метода jQuery.ajax() в качестве параметра можно отдать метод xhr(), который должен вернуть XMLHttpRequest, с помощью которого будут загружаться данные. Вот как можно загрузить «чистые» данные:

    $.ajax({
    	url: file_path,
    	xhr: function(x) {
    		var xhr = new XMLHttpRequest();
    		xhr.overrideMimeType('text/plain; charset=x-user-defined');
    		return xhr;
    	},
    	success: function(data) {
    		console.log(base64_encode(data));
    	}
    });
    

    В метод success пришло правильное содержание файла, которое теперь нужно просто закодировать в base64 (соответствующая JS-реализация была найдена на просторах интернета).

    Копирование в буфер

    Файлы мы загрузили, перекодировали и вывели, теперь нужно придумать, как их удобно скопировать в буфер. Выводить здровенное <textarea>-поле и заставлять пользователя каждый раз выделять и копировать эти данные как-то совсем некошерно. Нужно сделать кнопочку, по нажатию на которую в буфер обена будут попадать нужные данные. В этом случае не нужно будет перегружать интерфейс ненужным данными, а также можно будет вывести несколько кнопок, которые будут копировать данные в разных форматах: обычный data:url, картинка и background-image.

    Единственный известный мне способ программно запихнуть данные в буфер обмена, это использование небольшой флэшки (флэшукапец, ага). Подробно об этом написано в блоге CSSing.org.ua, я же использовал слегка допиленную версию ZeroClipboard.

    Проблемы использования флэша на сайте уже много раз обсуждались, я же напишу о тех, которые возникли в разрабатываемом сервисе. Не знаю, как на винде, но на Маке одно только присутствие флэша на странице уже нагружает процессор, даже если эта флэшка ничего не делает. Причём, чем больше флэшек, тем больше нагрузка. Уже на 3-х загруженных файлах (у каждого 3 кнопки копирования; итого 3×3=9 флэшек) мой Core 2 Duo был загружен на 25%, при том что ни одного пикселя на странице не шевелилось. Ещё один побочный эффект — это влияние на анимацию. Когда пользователь удаляет блок с картинкой, содержимое блока тут же пропадало.

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

    Только я хотел нажать на кнопку «Опубликовать статью», как внутренне чувство заставило проверить всё ещё раз. Инстинкт меня не подвёл. На Маке я использую флэш-плагин версии 10,0,42,34, а на винде поставил самый свежий — версии 10.0.45.2. Несмотря на небольшую разницу в версиях в самой свежей сборке не работает копирование в буфер при локальном просмотре страницы. То есть пользователи Chrome, скачавшие страничку к себе на компьютер, попросту не смогли бы ничего скопировать. Похоже, Adobe решил окончательно закрутить гайки с буфером обмена.

    После часа безуспешного гугления был придуман хак, который позволяет копировать данные без использования флэша. Суть его заключается в том, что нужно создать на странице блок со свойством contentEditable="true", записать туда необходимую строчку через innerHTML, навести фокус (в этот момент браузер автоматически выделяет содержимое блока) и вызвать document.execCommand('copy'):

    <div id="copy-clip" contenteditable="true"></div>
    <script type="text/javascript">
    	function doCopy() {
    		var obj = document.getElementById('copy-clip');
    		obj.innerHTML = 'my new data';
    		obj.focus();
    		document.execCommand('copy')
    	}
    </script>
    

    По крайней мере при локальном запуске это работает, причём так, как надо.

    Анимация

    А теперь самое вкусное. Я давно присматривался к CSS трансформациям и анимациям, хотелось попробовать их на реальных проектах (подчёркиваю, речь идёт о качественной реализации реальных задач, а не о дурацких демках из разряда «смотрите, оно крутится!»). На данный момент CSS-трансформации поддерживают Safari (Webkit) и Firefox 3.6, CSS Transitions and Animations — только Webkit. Про Opera 10.5 молчу, ибо пользоваться этим абсолютно невозможно из-за чудовищных тормозов.

    Что я могу сказать про CSS-анимации в Safari: реализованы они довольно плохо. Есть довольно много неочевидных проблем. Для своего сервиса мне удалось их кое-как исправить, но не знаю, насколько эти проблемы могут быть решены в более крупных проектах.

    Вот что мне удалось узнать после работы с модными CSS-свойствами:

    1. Свойства вроде -moz-box-shadow очень сильно влияют на производительность. Уже на 7-ом блоке с файлом, у которого указано это свойство, начали появляться тормоза. Чем больше блоков — тем больше тормозит. Пользуйтесь CSS-декорациями вмеру.
    2. Одна из главных проблем при использовании CSS Transitions — это не совсем очевидный способ указания блоку начальных координат. Например, вы указали, что у блока должны плавно изменяться свойства left и top: -webkit-transition-property: left, top. Для того, чтобы переда началом анимации переместить блок в некие начальные координаты, плавные переходы нужно отключить. Сделать это можно обнулив либо -webkit-transition-property, либо -webkit-transition-duration:
      $(elem).css({
      	'-webkit-transition-duration': '0',
      	top: 10,
      	left: 20
      });
      

      А вот для того, чтобы включить плавный переход, нужно сначала вернуть отключённое transition-свойство, а уже потом, через setTimeout выставить нужные координаты:

      $(elem).css({
      	'-webkit-transition-duration': '0.5s'
      });
      
      setTimeout(function(){
      	$(elem).css({
      		top: 100,
      		left: 200
      	});
      }, 1);
      	
    3. В Webkit-браузерах для анимации перемещения объектов лучше использовать связку CSS Transition/Animation и translate(x, y) из CSS Transforms. Трансформации получают аппаратное ускорение (по крайней мере на Маке), а анимации включают субпиксельное сглаживание, что даёт полее плавное и естественное движение:
      subpixel

      Простое указание дробных пикселей (например, так: -webkit-transform: translate(5.5px, 1.6px);), к сожалению, подобного эффекта не дают.

    4. В следствие аппаратного ускорения CSS-преобразований, советую следить за размером блока. Если анимируете блок, ширина или высота которого больше 2000 пикселей (для айфона — 1024 пикселя), перед началом анимации блок «моргнёт». Судя по всему, это как-то связано с размером текстуры в OpenGL.
    5. После того, как такая анимация отработала, блок с файлом начало в прямом смысле слова «колбасить»:
      при наведении курсора на блок (после того, как отработала анимация появления) кнопки копирования соседних блоков начали прыгать куда-то вверх, а у надписей «поломалось» сглаживание:

      bug1

      Помогло, как ни странно, указание контейнеру с блоками position: relative;, а текстовым надписям — background: #fff; (здравствуй, старина IE).

    6. При наведении курсора на картинку она увеличивается/уменьшается с помощью CSS Animations (в Firefox — JS-анимация масштаба). Работает вроде неплохо, за исключением иногда «моргающей» тени и другого типа сглаживания у картинки. Но когда я указал z-index контейнеру с файлами, увидел вот такую картину:
      bug2

      Блоки произвольно исчезали и появлялись во время анимации. Помогло удаление z-index у контейнера. В моём случае это не критично, но подозреваю, в более крупных проектах с этим будут проблемы.

    Заключение

    Ещё года 2—3 назад задачи вроде перекодирования файлов прямо в окне браузера считались невыполнимыми. Но уже сегодня можно смело говорить, что браузеры могут не только страницы показывать, но и выполнять вполе утилитарные задачи. Теперь необязательно изучать фреймворки вроде Qt, чтобы написать кросс-платформенное GUI-приложение. Вполне достаточно накопленных в веб-разработке знаний, а современные JavaScript-движки способны быстро переваривать довольно большие объемы данных. Кто знает, может, скоро начнётся эра повального портирования существующих десктопных инструментов на JavaScript :) Лично я получил огромное удовольствие не только от результата, но и от процесса создания этого небольшого сервиса.

    UPD: Иван Михайлов сделал Cocoa-оболочку для этого сервиса, теперь его можно запускать как обычное Мак-приложение.

  • Равномерный фон под текстом

    Одно из модных направлений веб-дизайна последних лет — оформление заголовков контрастным фоном. Например, вот так:

    example

    Простая, на первый взгляд, задача решается не так уж и просто: первая же мысль «добавить padding» натыкается на то, что отступ добавляется исключительно в начале и в конце текста, игнорируя переносы:

    padding

    Ближе всех к решению задачи когда-то подошёл akella, добавив border у родительского элемента. Но проблема не решена на 100%: в конце первой строки (место, где переносятся слова) всё равно нет отступа. Решения остальных ребят, которые присылались мне в твиттер, грешили одной и той же проблемой: нужно точно указать место разрыва строк.

    В простейшем случае, когда нужно добавить небольшой отступ, решение оказалось до боли простым. Есть одно «паразитное» CSS-свойство, от которого кодеры обычно избавляются — это свойство outline. Его особенность заключается в том, что во всех браузерах (по крайней мере в тех, в которых я проверял: Safari 4, Firefox 3.5, Opera 10, IE8) контур outline точно повторяет границы текстового элемента. Соответственно, эта строчка кода полностью решает нашу проблему:

    span.uniform-bg {
    	outline: red solid 0.3em;
    }
    

    Не обошлось без ложки дёгтя: в данном случае «плохим мальчиком» оказался Firefox. Во-первых, он иногда рисует контур с небольшим отступом от границ текста, а во-вторых — прочерчивает его между строками, перекрывая текст:

    firefox

    Первая проблема решается довольно просто: достаточно немного «втянуть» контур с помощью CSS-свойства outline-offset (либо -moz-outline-offset, эксклюзивно для Firefox), вторая — добавлением ещё одной обёртки с position:relative, чтобы поднять текст над контуром.

    Итоговое решение выглядит так:

    <style type="text/css">
    	.uniform-bg {
    		background:red;
    		position:relative;
    		outline: red solid 0.3em;
    		-moz-outline-offset:-0.04em;
    	}
    
    	.uniform-bg span {
    		position:relative;
    	}
    </style>
    <h2><span class="uniform-bg"><span>Hello everyone</span></span></h2>
    

    Оно не работает в IE6—7, в них не поддерживается свойство outline. Но, так как это исключительно декоративная задача, можно объединить её с решением от akella и получить почти идеальное решение.

    UPD: читатель Roman указал на баг в отрисовке фона в месте переноса строк в IE6—7. Немножко доработать напильником — и получится вполне приличное решение (обновил пример).

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

    Второй способ (upd)

    Тот же читатель Roman предложил ещё один способ решения задачи. Он основан на смещении трех слоёв относительно друг друга: например, если отступ равен x, то второй слой смещается на 2x вправо, а третий на –x, влево:

    method2

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

    bg-problem

    Метки: , , ,
  • CSS-свойство content: копировать или нет?

    Решил я для очередного своего проекта воспользоваться модным CSS-свойством content, чтобы немного облегчить страницу и сделать настройку внешнего вида более гибкой. Так как проект ориентирован на веб-разработчиков, об обратной совместимости со старыми браузерами (IE6—7) можно было не беспокоится. Но, к сожалению, меня ожидало большое разочарование от использования этого свойства. Нет, всё отображалось правильно, но конечному пользователю было бы неудобно пользоваться результатом.

    Что такое CSS-свойство content

    Кому ещё не удалось познакомиться с этим замечательным свойством, кратко расскажу о нём. Само название этого свойства говорит о том, что оно управляет неким содержимым. Согласно спецификации CSS2 это свойство применяется только к псевдо-элементам :before и :after, а с версии CSS3 станет доступно и для обычных элементов (небольшой реверанс в сторону Opera, которая это уже поддерживает).

    С помощью свойства content мы можем через CSS задавать текстовое содержимое для (псевдо-)элементов. Классический пример применения этого свойства — вывод содержимого ссылки рядом с элементом в версии сайта для печати:

    <style type="text/css">
    	a:after {
    		content: ' (' attr(href) ')';
    	}
    </style>
    <p>В нашем <a href="/catalog/">каталоге</a> вы найдёте много чего интересного.</p>
    

    С помощью псевдо-элемента :after мы задали некое содержимое после тэга <a>. Этим содержимым является результат конкатенации строк и функции attr(), которая выводит содержимое атрибута контекстного элемента. Браузер, полностью поддерживающий CSS2, изобразит этот код примерно так:

    content-example

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

    Проблема

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

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

    <style type="text/css">
    	.tag:before {
    		content:'<';
    	}
    
    	.tag:after {
    		content:'>';
    	}
    
    	.attr-value {
    		quotes:'"' '"';
    	}
    
    	.attr-value:before {
    		content:open-quote;
    	}
    
    	.attr-value:after {
    		content:close-quote;
    	}
    </style>
    <div class="tag">div <span class="attr-name">class</span>=<q class="attr-value">demo</q></div>
    

    Плюсы такого подхода: потребуется гораздо меньше элементов для раскраски тэгов (через псевдо-элементы :before и :after я могу задать произвольный цвет у угловых скобок). Для значений атрибутов я воспользовался тэгом <q> и CSS-свойством quotes, через которое определяются открывающие и закрывающие кавычки. Кому-то нравится использовать двойные кавычки в коде, кому-то — ординарные, при таком подходе их можно на лету поменять у всего документа. Как оказалось, выбор этого тэга и CSS-свойства стал важной частью эксперимента.

    В браузерах этот код выглядит замечательно:

    content-example2

    Но XML-документ должен не только красиво выглядеть, но и правильно работать: пользователь имеет право без проблем выделить и скопировать фрагмента документа, чтобы воспользоваться им, например, в своём любимом редакторе. И тут меня ожидало полное разочарование от использования свойства content. Я проверил результат копирования в своих браузерах — Safari 4, Opera 10, Firefox 3.5, IE8 — и получил вот такой результат:

    • Safari: div class=demo
    • Opera: <div class="demo">
    • Firefox: div class="demo"
    • IE8: <div class="demo">

    Как видите, все скопировали текст по-разному: Safari не скопировал content-данные в принципе, Opera и IE8 скопировали всё, а Firefox скопировал только кавычки вокруг атрибута.

    Затем я решил вместо вместо элемента <q> написать обычный <span>, и получил вот такой результат:

    • Safari: div class=demo
    • Opera: <div class="demo">
    • Firefox: div class=demo
    • IE8: <div class="demo">

    Всё то же самое, но Firefox уже не скопировал кавычки.

    Выводы

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

    • Safari в принципе не понимает CSS-свойство quotes. То, что браузер отобразил кавычки вокруг <q> элемента — исключительно стандартная реакция на него. Кавычки нельзя будет поменять через свойство quotes, например, на ординарные — они так и останутся двойными. А если элемент переименовать в <span>, то и вовсе пропадут.
    • Firefox при копировании текста обращает внимание на название элемента: если это <q> — кавычки скопируются, для другого элемента получите пустоту.
    • Firefox всегда копирует двойные кавычки для тэга <q>, даже если вы измените их на что-нибудь другое (на «ёлочки», например). То есть сделать трюк с управлением копирования символов у вас не получится. Либо двойные кавычки, либо ничего.
    • IE8 при копировании обращает внимание на тип элемента: например, если прописать тэгу display: list-item, то скопируется буллит (хотя на странице он не будет отображаться).

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

  • Вёрстка растягивающихся сайтов

    В этой статье я поделюсь способом вёрстки растягивающихся сайтов (а других, как оказалось, я верстать не умею), которым пользуюсь последние 2—3 года. Этот способ применяется для сложных модульных сеток; он лёгок в применении, но сложен в понимании и у него есть ряд недостатков. В целом, представленная здесь информация пригодится и веб-дизайнерам, так как сам макет сайта должен быть правильно подготовлен.

    Начнём с простого. Предположим, у нас есть вот такой макет растягивающегося сайт:

    01

    На первый взгляд всё довольно просто: типовая трёхколоночная сетка (ширина колонки — 25%), одна колонка — меню, две колонки — контент, и всё это располагается по центру. Типовая вёрсткой такого макета может быть следующей: заключаем оба блока в контейнер шириной 75%; меню в этом случае будет шириной 33%, контент — 67%. Саму обёртку выравниваем по центру с помощью margin: 0 auto, либо просто смещаем влево. Первый способ предпочтительнее, так как ведёт себя адекватно, если мы ограничим минимальную ширину обёртки. Визуализация этого решения:

    02

    Проблема

    Казалось бы, ничего сверхъестественного, задача решена. Но вот дизайнер вдруг захотел красиво оформить информацию в одном из разделов сайта, нарисовав выносной блок за пределами контента:

    03

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

    Тут мы приходим к самому главному правилу верстки макетов: контент должен находиться в едином потоке. Я не раз замечал, что ребята начинают верстать новые макеты, если контент вдруг становится многоколоночным или многоблочным. Начинают выделять новые информационные поля в админке, которые обязательно должны быть заполнены, чтобы страница получилось такой, какой была задумана. А если эти блоки вдруг располагаются в другом порядке — делают новые макеты и поля в админке, превращая сайт в помойку ещё до его выпуска.

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

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

    Ширина контента — 67%, меню — 33%. Чтобы блок встал ровно, его нужно сместить на 49.25% (33 / 67 * 100 ≈ 49.25). Во-первых, наш результат получился приблизительным, во-вторых, все браузеры (кроме Firefox 3) довольно своеобразно работают с дробными процентами (да и с процентами вообще), а некоторые вообще их не воспринимают. В итоге получается, что у нас нет кросс-браузерного способа совместить эти блоки при выбранной разметке.

    Поиск решения

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

    01

    Внимательный читатель обратит внимание на то, что ширина меню ровно в 2 раза меньше ширины контента (50 / 25 = 2). Деление получается целочисленным (нет дробей), а значит мы можем создать кросс-браузерное и — самое главное — точное решение проблемы.

    Поступим следующим образом. Завернём оба блока в контейнер, ширина которого будет не 75%, как раньше, а 50% (то есть равна ширине контента). Сам контент будет шириной 100%, а меню — 50%, при этом смещено влево на свою ширину.

    04

    Приблизительный код этого примера:

    <style type="text/css">
    	#page {
    		position:relative;
    		margin:0 auto;
    		width:50%;
    		left:10%;
    	}
    
    	#menu {
    		width:50%;
    		margin-right:-50%; /* чтобы правильно в IE работало */
    		position:relative;
    		left:-50%;
    		float:left;
    	}
    
    	#content {
    		width:100%;
    		float:left; /* Новый контекст форматирования */
    	}
    </style>
    <div id="page">
    	<div id="menu"></div>
    	<div id="content"></div>
    </div>
    

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

    05

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

    Усложняем задачу

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

    06

    Проблема в том, что 100 не делится на 6 без остатка (100 / 6 ≈ 16.66666), а дизайнер по-прежнему хочет выравнивать контентные блоки по границам основных колонок.

    Одним из решений этой проблемы может быть следующее: сделаем размер контейнера равным ширине двух колонок. Размер внутреннего блока в этом случае будет равен 50%, что соответствует размеру основной колонки. А указав этому блоку margin-right:-50% мы уберём его влияние на поток по горизонтали: это позволит нам создать сколько угодно блоков в контейнере, каждый из которых будет влиять на его высоту, но не на ширину.

    07

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

    Проблема с расчётом процентных значений

    Если в предыдущем примере поиграться с размером окна браузера, то можно заметить, что во всех браузерах, кроме Firefox 3, появляются однопиксельные зазоры между некоторыми блоками:

    screenshot

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

    Проценты — это относительная величина, которая пересчитывается в абсолютную — пиксели (ну хорошо, пиксели — это тоже относительная величина, которая зависит, например, от DPI экрана). То есть когда браузер видит, например, процентную ширину у блока, он должен рассчитать значение в пикселях относительно контейнера этого блока. Предположим, ширина контейнера равна 60 пикселям, а внутреннего блока — 50%. Ширина в пикселях этого блока будет равна: 60 × 0.5 = 30px. Значения красиво умножились/поделились. Но что будет, если ширина контейнера равна 61px? 61 × 0.5 = 30.5px — браузер не может отрисовать половину пикселя, поэтому он округляет это значение в меньшую или большую сторону.

    На самом деле алгоритм гораздо сложнее: например, 4 блока подряд шириной 25% в современном браузере должны занимать 100% ширины контейнера; какие-то блоки будут меньше, какие-то больше. Мы же рассматриваем самый простой случай.

    Итак, ширина блока получилась 30.5px, браузер для себя округляет её до 30px. Что будет, если в этот блок мы вложим ещё один, шириной 200%? По идее его ширина должна быть равна ширине контейнера, то есть 61px. Но так не получится, потому что абсолютная ширина промежуточного блока равна 30px, соответственно: 30 × 2 = 60px. Получили расхождение в 1 пиксель. Из всех современных браузеров правильное значение даст только Firefox 3: судя по всему, он где-то внутри хранит истинное значение ширины, от которого делает все расчёты.

    Но! Обратите внимание, что абсолютная ширина нашего нового внутреннего блока всегда будет чётной. Например, если ширина промежуточного блока будет рассчитана как 31px, то внутренний блок будет шириной 31 × 2 = 62px. А чётные числа, как известно, делятся на 2 без остатка.

    Соответственно, решением нашей проблемы с зазорами в шестиколоночном макете будет создание ещё одной обёртки, шириной в одну колонку, а внутренней обёртке задать ширину в 200%, чтобы получилась чётная абсолютная ширина у контейнера. Убеждаемся, что всё работает правильно.

    Выводы

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

    1. Определяем ширину колонки модульной сетки. Тут, конечно, многое зависит от дизайнера: если он будет лепить блоки как попало, то никакие стандартные способы не подойдут. Мне очень повезло, что в САЛ практически у всех дизайнеров есть голова на плечах. Перед тем, как что-то нарисовать, они разбивали макет на несколько колонок одинаковой ширины, по которым выравнивали блоки. Если же макет не укладывался в мою схему, я немного раздвигал блоки, выравнивая их по моим колонкам и показывал дизайнеру — он был не против. Важно понимать, что ширина вашей колонки не всегда будет равна ширине дизайнерской колонки. Это можно увидеть в самом первом примере, где ширина свёрстанной колонки равна двум дизайнерским. Обычно, когда я получаю макет от дизайнера, я использую его гайды исключительно для того, чтобы увидеть принципы выравнивания блоков, после чего я создаю свою разбивку на колонки.
    2. Определяем ширину основного контейнера. Этот контейнер я называю точкой отсчёта (хоть это и не точка вовсе). Важно, чтобы ширина точки отсчёта позволяла создавать ширину колонки, которая делит 100 без остатка: 100%, 50%, 20%, 10%, 5%. Создав колонки такого размера, можно точно позиционировать и раздвигать выносные блоки внутри контента.
    3. Добавляем ещё обёртку для точного округления процентов. Это если вам необходимо точное выравнивание по блокам с видимыми границами. В примере я показал, как создать блок, ширина которого всегда кратна 2. Точно так же можно создавать обёртки, ширина которых кратна, например, 4 (25%/400%).

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

    1. Фотогалерея Imperia Private Banking. Блок с галереей растягивается точно (±1 пискель) по размерам общего контейнера, при этом сам контентный блок занимает половину ширины и располагается по центру контейнера. Галерей на странице может быть сколько угодно (как и контента между ними), при этом они будут находится в нормальном потоке документа. На этом же сайте можно увидеть ещё несколько примеров того, как контентные выносные блоки выравниваются по колонкам макета.
    2. Банк AB.LV. Сайт довольно большой, содержит много разного контента и едва заметных декораций (однопиксельная тень вокруг основного блока, например). На странице «О банке» можно увидеть, как четвёртая колонка в контенте выравнивается точно по основному меню (это чистой воды дрочерство, но дизайнер был доволен, да и мне было не сложно такое сделать).
    3. (примеров, на самом деле, много, но не могу вспомнить самые выразительные).

    Недостатки

    Куда же без них. Самый главный — способ совершенно не подходит для макетов, которые должны растягиваться на всю ширину окна браузера. Из-за тех самых округлений процентных значений есть большой риск того, что какой-нибудь блок выйдет на 1px за пределы окна, создав тем самым горизонтальный скролл. Ещё кому-то может может показаться недостатком то, что размеры блоков меняются с шагом, больше одного пикселя. Ну и не забываем, что дизайнер тоже должен включить мозг перед тем, как что-то нарисовать. Хотя, конечно, если постараться, то можно сверстать и совсем экзотические варианты, выбрав размер колонки по-меньше (например, 10%).

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

    Метки: , ,

← cтарое