Скажу честно, нахожусь в долгом свободном поиске идеальной библиотеки web-компонентов, пересмотрел большое количество оных, многие, мне как начинающему, с первого взгляда казались очень громоздкими, другие слишком сложные, третьи...
Для меня идеал - браузерная javascript библиотека по структуре похожая на .Net. Таковой нет. Но в последнее время стал думать о компонентах на основе делегирования, на такие мысли меня навела библиотека
CornerJS, к сожалению, автор не достаточно описал свою библиотеку, но идею я понял и решил создать "велосипед" на свой лад, писал код около 2-х часов, возможны ошибки и недочеты, т.к. это не prodaction, а просто пример делегированных компонентов.
Основные директивы подхода:
1. Все обработчики "вешаются" на body, ну разумеется, в целях производительности, mousemove "вешать" не стоит.
2. Объекты-делегаторы не вешают своих обработчиков, главный объект, при наличии события ищет делегатор и пытается вызвать у него соответствующий событию метод.
3. Главный объект умеет создавать новый dom-компонент из шаблона, при создании, вызывает у его делегатора (и вложенных) метод init().
Плюсы:
- малое количество обработчиков, все "вешаются" на body
- встроенное делегирование событий
- делегаторы, при желании, можно в прямом смысле наследовать через прототип
- использование шаблонов разметки, можно придумать механизм предварительной обработки шаблонов (вставки, псевдо-наследования, сложения аттрибутов ...)
- легкое создание и удаление компонентов из dom
- мизерный движок
- IE8 отдыхает
Минусы:
- возможны тормоза, связанные с обработкой событий, обусловлено поиском делегатора по dom и необходимостью "всплытия" события до body, но это надо проверить
- сложность реализации модульности, компонент состоит из 3-х частей: стиля, разметки и самого js кода, придется думать, как все это "запихать" в один js-файл
- IE8 отдыхает
Предлагаю обсудить подход и реализацию.
Ну вот и сам код, пример окна диалога:
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Delegation</title>
<style>
[template] {
display:none;
}
.dialog {
position:absolute;
left:100px;top:100px;
width:300px;height:250px;
border:1px solid gray;
background-color:#fff;
}
.dialog > .container {
position:relative;
top:0px;left:0px;right:0px;bottom:0px;
}
.dialog > .container > .close-button {
position:absolute;
right:3px;top:3px;
}
.dialog > .container > .title {
cursor:default;
position:absolute;
top:0px;
left:0px;right:0px;
height:20px;
line-height:20px;
padding-left:20px;
overflow:hidden;
background-color:#C7E2FA;
border-bottom:1px solid gray;
}
.dialog > .container > .content {
position:absolute;
top:20px;bottom:0px;
left:0px;right:0px;
}
</style>
</head>
<body>
<button onclick='add_dialog()'>Нажми</button>
<script>
function add_dialog(){
Delegator.create( 'dialog' );
}
</script>
<script>
var Delegator = {
events:['click','mousedown','mouseup'],
start:function(){
var _this = this;
var handler = function(ev){
return _this.on_event(ev);
}
for( var i=0,l=this.events.length; i<l; i++ ){
document.body.addEventListener( this.events[i], handler, false );
}
},
on_event:function(ev){
console.log( ev.type );
var top = document.body;
var target = ev.target;
var type = ev.type;
var delegate_target = null, action_target = null, value_target = null;
var delegate = null, caction = null, cvalue = null;
// поиск делегатора, а за одно действие caction и данные cvalue
do{
if( !cvalue && target.hasAttribute('cvalue') ){
cvalue = target.getAttribute('cvalue');
value_target = target;
}
if( !caction && target.hasAttribute('caction') ){
caction = target.getAttribute('caction');
action_target = target;
}
if( target.hasAttribute('delegate') ){
delegate = target.getAttribute('delegate');
delegate_target = target;
break;
}
}while( target != top && ( target = target.parentElement ) );
if( delegate ){
if( this.delegators[delegate] ){
var obj = this.delegators[delegate];
// дополнение события своими данными
ev.delegate = delegate;
ev.delegate_target = delegate_target;
ev.caction = caction;
ev.action_target = action_target;
ev.cvalue = cvalue;
ev.value_target = value_target;
var method = null;
var method1 = 'on_'+type; // например on_click
var method2 = caction ? ( method1+'_'+caction ) : null; // например on_click_title
var method3 = ( method2 && cvalue ) ? ( method2+'_'+cvalue) : null; // например on_click_button_close
if( typeof(obj[method3]) == 'function' ){
method = method3;
}else if( typeof(obj[method2]) == 'function' ){
method = method2;
}else if( typeof(obj[method1]) == 'function' ){
method = method1;
}
if( method ){
return obj[method]( delegate_target, ev );
}
}else{
console.log('Не найден делегатор '+delegate);
}
}
return false;
},
delegators:{},
create:function( template , parent_element ){
parent_element = parent_element || document.body;
var element = document.querySelector('[template='+template+']');
if(element){
element = element.cloneNode(true); // подмена на клон
element.removeAttribute('template'); // удаляем флаг шаблона
parent_element.appendChild( element );
var all=[]; // запись всех делегируемых элементов в один массив
if( element.hasAttribute('delegate') ) all.push( element );
var nodes = element.querySelectorAll('delegate');
if( nodes && nodes.length ) for( var i=0,l=nodes.length; i<l; i++ ) all.push(nodes[i]);
// поиск метода init и запуск
for( var i=0,l=all.length; i<l; i++ ){
var node = all[i];
var delegate = node.getAttribute('delegate');
if( this.delegators[delegate] ){
var obj = this.delegators[delegate];
if( typeof(obj.init) == 'function' ){
obj.init( node );
}
}else{
console.log('Не найден делегатор '+delegate);
}
}
}else{
console.log('Не найден шаблон '+template);
}
}
}
Delegator.start();
</script>
<div template='dialog' class='dialog' delegate='dialog'>
<div class='container'>
<div class='title' caction='title' >title</div>
<div class='content'>Container
<hr />
<button caction='button' cvalue='add'>Добавить дочернее окно</button>
</div>
<button caction='button' cvalue='close' class='close-button'>X</button>
</div>
</div>
<script>
/* делегатор для dialog */
Delegator.delegators.dialog = {
init:function( element ){
console.log('Элемент dialog создан');
this.to_top( element );
},
on_mousedown_title:function( element, ev ){
this.to_top( element );
var dx = ev.pageX - element.offsetLeft;
var dy = ev.pageY - element.offsetTop;
var move = function( ev ){
element.style.left = ( ev.pageX - dx ) + 'px';
element.style.top = ( ev.pageY - dy ) + 'px';
return false;
}
var stop = function( ev ){
document.removeEventListener( 'mousemove' , move , true );
document.removeEventListener( 'mouseup' , stop , true );
element = move = dx = dy = stop = null;
return false;
}
document.addEventListener( 'mousemove' , move , true );
document.addEventListener( 'mouseup' , stop , true );
return false;
},
on_click_button_close:function( element, ev ){
this.destroy( element );
return false;
},
on_click_button_add:function( element ){
Delegator.create( 'dialog' , element.querySelector('.container') );
return false;
},
on_click:function( element , ev ){
this.to_top( element );
return false;
},
to_top:function(element){
var parent = element.parentElement;
if( !parent.hasAttribute('dialog-z-index') ){
parent.setAttribute('dialog-z-index',10);
}
var z = 1 + parseInt(parent.getAttribute('dialog-z-index'));
parent.setAttribute('dialog-z-index',z);
element.style.zIndex = z;
console.log( z );
},
destroy:function(element){
element.parentElement.removeChild( element );
}
}
</script>
</body>
</html>