RayZ
14:08 20-04-2009 Javacript:Исследование на тему замены стандартных кнопок (ретрансляция)
В процессе работы над интерфейсом одного продукта, появилась надобность в изготовлении собственного дизайна кнопок. За это время код, который заменяет стандартную кнопку на требуемую несколько раз переписывался и в данный момент тоже далёк от идеала. Учитывая все текущие проблемы кросс-браузерности, за это время выяснились и получилось нижеописанное.

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

Кнопка должна:
  1. быть inline-блоком
  2. иметь возможность наследовать стилизацию родителя (в пределах возможностей)
  3. Наследовать некоторые свойства и методы родителя.

На первом этапе я попробовал максимально минимизировать код, создавая подобную кнопку двумя контейнерами один из которых вложен во второй со смещениями для отображения красивого бэкграунда с закруглениями, в конце концов, этого не достаточно для того что бы в процессе была возможность наследовать свойства и методы самой кнопки, то есть сама кнопка как элемент должна присутствовать внутри контейнеров (<span><div><button/></div></span>).

Для упрощения эксперимента используем jQuery,

Спустя некоторое время выяснилось, что крайне сложно под IE, FF, Opera контролировать размеры и внешний вид всех контейнеров с кнопкой вкупе c помощью обычных CSS. Результат превратился в борьбу хаков над стилями. Тут же я впервые столкнулся с надобностью написания различных стилей для FF2 и FF3.

В конце концов за основу была взята концепция, используемая в ExtJS, а именно - таблица.

[левый край|кнопка|правый край]

В связи с этим, количество вопросов с позиционированием значительно сократилось, и вместе с ним - количество CSS.

Чит-коды

Допустим, что исходные кнопки задается следующим образом
<input type='submit' value='Submit' class='replaceMe w100'>
<input type='button' value='Pushme' class='replaceMe ico ico-image' disabled='disabled'>

В данном случае у нас получается что класс replaceMe будет являтся селектором для замены, класс w100 - вспомогательный класс (допустим, отвечающий за ширину кнопки), который должен будет наследоваться. ico и ico-image - классы отвечающие за иконки.

Картинка-спрайт для прототипа:
[изображение]

CSS прототипа:

.ico {
	  padding-left: 20px !important;
	  padding-bottom: 1px !important;
	  background-position: 0px 0px;
	  background-repeat: no-repeat;
	}

	.ico-image {  background-image: url("page_tick.gif") !important;}



	/* Перебиваем все настройки которые могли бы помешать дизайну свыше*/
	.xBtn tr td {
	  border: 0 none !important;
	  border-bottom: 0 none !important;
	  padding: 0;
	  font:normal 11px sans-serif, tahoma, verdana, helvetica ;
	  height: 21px;
	  min-height: 21px;
	}

	/* Стиль таблицы */
	.xBtn {
	  cursor:pointer;
	  border-collapse: collapse;
	  white-space: nowrap;
	  display: inline;
	  width: auto;
	}

	/* Стиль кнопки внутри центральной ячейки. Убираем все возможные отступы, пробелы.
	 * Имеем ввиду, что у IE есть такая странность: Добавлять лишние пробелы по краям кнопки, 
	 * нечто походящее на padding: 0 1.3em;
	 * */
	.xBtn button {
	  border:0 none;
	  background:transparent no-repeat;
	  font:normal 11px tahoma,verdana,helvetica;
	  padding-left:3px;
	  padding-right:3px;
	  height: 21px;
	  cursor:pointer;
	  margin:0;
	  overflow:visible;
	  width:auto;
	  -moz-outline:0 none;
	  outline:0 none;
	}

	/* Допольнение для IE */
	* html .xBtn button {width: 1px;}
	*+html .xBtn button {width: 1px;}
	*+html .xBtn button {padding-top:3px;}

	/* если кнопка будет с иконкой */
	.xBtn .xBtn-text-ico {
	  background-position: 0 0px;
	  background-repeat: no-repeat;
	  height: 16px;
	  padding: 0 0 2px 18px;
	  margin-top: 1px;
	}

	/* А возможно это будет просто текст */
	.xBtn .xBtn-text {
	  background-position: 0 0px;
	  background-repeat: no-repeat;
	  padding-top:0px;
	  padding-bottom:2px;
	  padding-right:0;
	  margin-top: 1px;
	  height: 16px;
	}

	/* борьба с IE */
	*+html .xBtn .xBtn-text {  padding-top:1px;  margin-top: 2px;}

	.xBtn-Left, .xBtn-Right {
	  font-size:1px;
	  line-height:1px;
	  width:3px;
	  height:21px;
	}

	.xBtn-Left   {  background: url(btn-sprite.png) no-repeat 0 0;}
	.xBtn-Right {  background: url(btn-sprite.png) no-repeat 0 -21px;}

	.xBtn .xBtn-Left i, .xBtn .xBtn-Right i {
	  display:block;
	  width:3px;
	  overflow:hidden;
	  font-size:1px;
	  line-height:1px;
	}

	.xBtn .xBtn-Center {
	  background:url(btn-sprite.png) repeat-x 0 -42px;
	  vertical-align: middle;
	  text-align:center;
	  cursor:pointer;
	  white-space:nowrap;
	}

	.xBtn-over .xBtn-Left{    background: url(btn-sprite.png) repeat-x 0 -63px !important; }
	.xBtn-over .xBtn-Right{   background: url(btn-sprite.png) repeat-x 0 -84px !important; }
	.xBtn-over .xBtn-Center {  background: url(btn-sprite.png) repeat-x 0 -105px !important;}
	.xBtn-click .xBtn-Left {  background: url(btn-sprite.png) repeat-x 0 -126px !important;}
	.xBtn-click .xBtn-Right {  background: url(btn-sprite.png) repeat-x 0 -147px !important; }
	.xBtn-click .xBtn-Center {  background: url(btn-sprite.png) repeat-x 0 -168px !important;  }

	.xBtn em {
	  font-style:normal;
	  font-weight:normal;
	  height: 16px;
	}

	* This source code was highlighted with Source Code Highlighter.
И собственно, сам код:
$('.replaceMe').each(function(){
	  // у исходника тут же убираем уже ненужный класс
	  $(this).removeClass('replaceMe');

	  // Создаем таблицу
	  var BtnTable = document.createElement('table');
	  var BtnTableRow = BtnTable.insertRow(0);
	  var LeftBtnCell = BtnTableRow.insertCell(0);
	  var CenterBtnCell = BtnTableRow.insertCell(1);
	  var RightBtnCell = BtnTableRow.insertCell(2);
	  
	  // Что бы ячейки таблицы небыли пустыми, 
	  // создаем для них какие-нибуть контролируемые элементы DOM
	  var newBtnContainer = document.createElement('em');
	  var newBtnSideLContainer = document.createElement('i');
	  var newBtnSideRContainer = document.createElement('i');

	  // Назначаем классы, вставляем в контейнеры
	  $(LeftBtnCell).addClass('xBtn-Left').append(newBtnSideLContainer);
	  $(RightBtnCell).addClass('xBtn-Right').append(newBtnSideRContainer);

	  // Замечательный атрибут, 
	  // предотвращающий выделение текстовой информации внутри блока 
	  newBtnContainer.setAttribute('uselectable', 'on');

	  $(BtnTable)

	    // Назначаем класс для самой таблицы-кнопки
	    .addClass('xBtn')

	    // Переносим из исходной кнопки ее значение в атрибут Title, для красоты эксперимента 
	    .attr('title', $(this).attr('value') || '')

	    // Изменение классов при наведении и клике на таблице-кнопке
	    .hover(
	      function(){
	        if ($('button:enabled', $(BtnTable)).length) $(this).addClass('xBtn-over');
	      },
	      function(){
	        $(this).removeClass('xBtn-over');
	        $(this).removeClass('xBtn-click');
	      }
	    )
	    .mousedown(function(){
	      //$(newBtn).focus();
	      $(this).addClass('xBtn-click');
	    })
	    .mouseup(function(){
	      $(this).removeClass('xBtn-click');
	    });


	  // Будем считать, что иконки на кнопках будут задаваться двумя классами
	  // первый из которых будет отвечать за место под кнопку, второй - за само изображение.
	  // ico ico-image

	  // Определяем, есть ли у нас иконка, и что за...
	  var xBtnClasses = this.className.split(' ');
	  var hasIco = $(this).hasClass('ico');
	  var icoClassName = '';
	  for (var i = 0; i < xBtnClasses.length; i++ ) {
	    if (xBtnClasses[i].toString().match(/ico-\w+/)) icoClassName = xBtnClasses[i].toString();
	  }

	  // Убираем из исходника классы отвечающие за иконки.
	  if (hasIco && icoClassName) {
	    $(this).removeClass('ico').removeClass(icoClassName)
	  }

	  // Копируем в центральную ячейку с будущей кнопкой классы из исходника (оставшиеся классы)
	  // Обозначаем ее собственным классом и вставляем в нее контейнер для будущей кнопки
	  $(CenterBtnCell).append(newBtnContainer).addClass(this.className).addClass('xBtn-Center');;
	  
	  // Копируем событие onclick исходника
	  var onClickEv = $(this).attr('onclick');

	  // Если есть какое событие, то устанавливаем его на всю таблицу-кнопку
	  if (jQuery.isFunction(onClickEv)) {
	    $(BtnTable).bind('click', function(e){
	      onClickEv();
	    });

	  // Если нет события, то может быть наш родитель был submit-ом?
	  // Нажимаем на кнопку и ждем спецэффектов
	  } else if ( this.type == 'submit' ) {
	    $(BtnTable).bind('click', function(e){
	      if ($(this).find('button').length) {
	        var f = $(this).find('button')[0];
	        f.click();
	      }
	    });
	  }
	  
	  // Скрываем нашего прородителя и вставляем перед ним прототип нашей кнопки
	  $(this).hide().before(BtnTable);

	  // Вместо кода написанного ниже сначала была попытка создать кнопку методами JavaScript.
	  // Но в связи с существующей проблемой под IE, которая запрещает изменять атрибут type 
	  // кнопку создаем обычным способом:
	  
	  var Btn = '<button ' +
	    // Трансплантируем тип кнопки
	    'type="' +  ($(this).attr('type') || 'button') +  '" ' +
	    
	    // Назначаем ID исходника
	    'id="' +  this.id +  '" ' +
	    
	    // Передаем классы: иконок, если есть,
	    'class="' +  ((hasIco && icoClassName) ? 'xBtn-text-ico ico ' + icoClassName : 'xBtn-text') + (($(this).attr('disabled')) ? ' disabled"' : '') + '" ' +
	    
	    // Передаем остальные аттрибуты
	    (($(this).attr('disabled')) ? 'disabled="disabled"' : '') +
	    'name="' +  $(this).attr('name')  +  '" ' +
	    'title="' +  $(this).attr('title')  +  '" ' +
	    'style="' +  (($(this).attr('value') == '') ? 'width:16px;' : '')  +  '" ' +
	    '>' + $(this).attr('value') +
	    '</button>';

	  // Tadaaaa!
	  newBtnContainer.innerHTML = Btn;

	  // Небольшие жонглирования с шириной под IE, котрый не совсем понимает, что такое ширина, когда нет содержимого текста
	  if ($.browser.msie && $(this).attr('value') != '') {
	    if ($(CenterBtnCell).width()) {
	      $(CenterBtnCell).find('button').css('width', $(CenterBtnCell).width() + 'px');
	    } else {
	      $(CenterBtnCell).find('button').css('width', TextMetrixWidth(this) + 18 + 'px');
	    }
	  }

	  $(this).remove();
	});
	
	* This source code was highlighted with Source Code Highlighter.
update: Демонстрация работы, по просьбам трудящихся.