/** * DataTable for JavaScript Data Editing * jQuery Plugin * * jQuery 1.4 or higher * jQueryUI 1.8 or higher * * @version 1.0 * @author bitofsky@neowiz.com 2012.07.13 * @encoding UTF-8 */ // http://glat.info/jscheck/ /*global $, jQuery, confirm, console, alert, JSON */ // 명료한 Javascript 문법을 사용 한다. "use strict"; (function($, window, document){ var DEFAULT_OPTION = { /** * @var {boolean} option.modifier=false 데이터 수정 모드 활성화 여부 */ 'modifier' : false, /** * @var {array} option.showPath=[] 보여줄 데이터 경로 명시. 명시된 경로의 데이터 노출. */ 'showPath' : [], /** * @var {array} option.hidePath=[] 보여주지 않을 데이터 경로 명시. 명시된 경로의 데이터 미노출 */ 'hidePath' : [], /** * @var {string} option.editEvent='dblclick' 셀 에디터 이벤트 설정 */ 'editEvent' : 'dblclick', /** * @var {array} option.editAllowPath=[] 수정할 데이터 경로 명시. 명시된 경로의 데이터에만 수정 기능이 동작. */ 'editAllowPath': [], /** * @var {array} option.editDenyPath=[] 수정을 금지할 데이터 경로 명시. 명시된 경로의 데이터에는 수정 기능이 미동작. */ 'editDenyPath' : [], /** * @var {plainObject} option.pathName={} 경로의 노출 이름을 명시. Key=경로, Value=이름 */ 'pathName' : {}, /** * @var {number} option.depth=0 몇 depth 까지 기본 노출할지에 대한 설정. 0은 모두 노출 */ 'depth' : 0, /** * @var {string} option.expandLabel='Detail' */ 'expandLabel' : 'Detail', /** * @var {string} option.title=null 테이블 상단 제목 caption */ 'title' : null, /** * @var {boolean} option.keyEdit=false Object/Array 의 서브키를 추가 또는 삭제하는 기능 사용 여부 */ 'keyEdit' : false, /** * @var {boolean} option.html=true String 값으로 들어온 태그를 HTML 로 사용할지 그냥 태그 자체를 보여줄지 설정 */ 'html' : true, /** * @var {boolean} option.allowElement 데이터 중 Element 가 있는 경우 처리 방법. true=append(), false=toString() */ 'allowElement' : false, 'css' : { 'table' : { 'border-collapse': 'separate', 'border-spacing' : '5px 0', 'width' : '100%' }, 'key' : { 'padding' : '0 5px 0 5px', 'vertical-align' : 'top', 'text-align' : 'right', 'width' : '1px', 'white-space' : 'nowrap' } } }; var TAG = { table : '<table/>', thead : '<thead/>', tbody : '<tbody/>', tr : '<tr/>', td : '<td/>', div : '<div/>', text : '<input type=text class=ui-state-default />', textarea : '<textarea class=ui-state-default />', select : '<select class=ui-state-default />', option : '<option/>', number : '<input type=number class=ui-state-default step=1 min=0 required />', button : '<button type=button />', caption : '<caption/>' }; $.fn.dataTable = function(){ return this.append( $.dataTable.apply(null, arguments) ); }; /** * ADM4 DataTable - bitofsky@neowiz.com 2012.07.11 Renewal * * @param {plainObject|array} data * @option {plainObject} option=DEFAULT_OPTION */ $.dataTable = function( data, option, currentDepth, currentPath ){ // dataTable 은 PlainObject 또는 Array 가 아니면 구성을 할 수 없다. if( !checkTableType( data ) ) return null; /** * @var opt DEFAULT_OPTION */ var opt = $.extend(true, {}, DEFAULT_OPTION, option); currentDepth = +currentDepth || 0; currentPath = currentPath || ''; var table = $( TAG.table ).addClass('ui-tabs ui-widget ui-widget-content ui-corner-all').css( opt.css.table ), thead = $( TAG.thead ), tbody = $( TAG.tbody ); if( !opt.depth || opt.depth > currentDepth ){ renderTable(); } else{ var tr = $( TAG.tr ).appendTo( tbody ), td = $( TAG.td ).appendTo( tr ); $( TAG.button ).text( opt.expandLabel ).appendTo( td ).click(function(){ renderTable(); }); } return table.append( thead, tbody ); /** * DataTable 을 실제로 그리는 기능 */ function renderTable(){ $( thead ).add( tbody ).empty(); if( opt.title && currentDepth == 0 ){ $( TAG.caption ).text( opt.title ).addClass('ui-state-default ui-corner-all').appendTo( table ); } // Key 추가 기능 버튼 if( opt.modifier && opt.keyEdit ){ $( TAG.tr ).append( $( TAG.td ).attr('colspan', 2).append( $( TAG.button ).text('Add Key').button({icons:{primary:'ui-icon-plusthick'}}).click( fn_addKey ) ) ).appendTo( thead ); } // 각 Key 별 Row 처리 for( var key in data ){ // opt.showPath 가 명시된 상태에서 현재 Key 가 리스트에 없는 경우 노출 무시 if( checkPath('showPath', key) === false ) continue; // opt.hidePath 가 명시된 상태에서 현재 Key 가 리스트에 있는 경우 노출 무시 if( checkPath('hidePath', key) === true ) continue; var name = getKeyName( key ); var tr_line = $( TAG.tr ).appendTo( tbody ), td_key = $( TAG.td ).text( name ).appendTo( tr_line ).addClass('ui-state-default ui-corner-all').css( opt.css.key ), td_value = $( TAG.td ).appendTo( tr_line ); // value 랜더링 drawCell( key, td_key, td_value ); } } /** * Data Key 추가 팝업 */ function fn_addKey(){ var self = this, i_key = null, d_pop = $( TAG.div ), ta_value = $( TAG.textarea ).width('100%'); if( $.type( data ) == 'array' ) i_key = $( TAG.number ).val( data.length ); else i_key = $( TAG.text ).attr('required',true); d_pop.dataTable({ 'Key Name' : i_key, 'Value [Json]' : ta_value }, { allowElement: true }).dialog({ title: 'Add Key', width: 'auto', height: 'auto', buttons: { 'Add': function(){ if( i_key[0].checkValidity && !i_key[0].checkValidity() ) return alert( i_key[0].validationMessage ); try{ var value = JSON.parse( ta_value.val() || null ); }catch( e ){ return alert( e ); } data[ i_key.val().trim() ] = value; d_pop.dialog('close'); renderTable(); } } }); } function fn_delKey( event ){ var key = event.data; if( !confirm('Remove Key - ' + key + '\nContinue?') ) return; if( $.type(data) === 'array' ) data.splice(key, 1); else delete data[key]; renderTable(); } /** * 값의 유형에 따라 셀을 랜더링 한다. * 수정 내역이 Data 에 직접 Set 되려면 참조(refference)를 유지해야 하며 이를 위해 Key 를 받아 내부에서 Data[key] 를 핸들링 함. * * @param {string|number} key 키 이름 * @param {jQueryObject} o_key Key TD 객체 * @param {jQueryObject} o_cell Value TD 객체 */ function drawCell( key, o_key, o_cell ){ var value = data[key], valueType = $.type(value); o_key.unbind(); o_cell.empty(); // 재귀 DataTable 을 랜더링 한다. if( checkTableType( value ) ){ o_cell.dataTable( value, option, currentDepth+1, currentPath+'.'+key ); } // PlainObject 가 아닌 Object 는 Element 로 본다. else if( valueType == 'object' ){ if( opt.allowElement ) o_cell.append( value ); else o_cell.text( String(value) ); } // 함수.. else if( valueType == 'function' ){ // 줄내림 노출등을 위해 HTML 로 보여줌.. o_cell.html( nl2br(String(value), true) ); } // 기타 null/undefined/boolean/string/number else{ if( opt.html ) o_cell.html( nl2br(String(value), true) ); else $('<pre style="margin:0;"/>').html( String(value).replace(/&/g,'&').replace(/</g,'<') ).appendTo( o_cell ); } // 셀 수정 모드 활성화 if( opt.modifier ) setModifier(); // 더블클릭으로 데이터를 수정할 수 있도록 한다. function setModifier(){ // opt.editAllowPath 가 명시된 상태에서 현재 Key 가 리스트에 없는 경우 수정 기능 비활성화 if( checkPath('editAllowPath', key) === false ){ o_key.addClass('ui-state-disabled'); return; } // opt.editDenyPath 가 명시된 상태에서 현재 Key 가 리스트에 있는 경우 수정 기능 비활성화 if( checkPath('editDenyPath', key) === true ){ o_key.addClass('ui-state-disabled'); return; } o_key.removeClass('ui-state-disabled').bind( opt.editEvent, function(){ // selection 삭제 document.getSelection().removeAllRanges(); var s_type = $( TAG.select ), d_form = $( TAG.div ), b_ok = $( TAG.button ).text('OK').button().click(function(){ try{ data[key] = fn_val(); } catch( e ){ alert( e ); return; } reset(); }), b_cancel = $( TAG.button ).text('Cancel').button().click( reset ), b_remove = $( TAG.button ).text('Del Key').button({icons:{primary:'ui-icon-minusthick'}}).click( key, fn_delKey ), fn_val = null; o_cell.empty().append( s_type, d_form, b_ok, b_cancel ); if( opt.keyEdit ) o_cell.append( b_remove ); ('string number function object array true false null undefined'.split(' ')).forEach(function( v, i ){ s_type.append( $( TAG.option ).val( v ).text( v ) ); }); // 기본 선택 switch( valueType ){ case 'boolean': s_type.val( String(value) ); break; default : s_type.val( valueType ); } s_type.change(function(){ var selectedType = this.value; d_form.empty(); // 선택한 유형이 값과 같은 유형인 경우 기본 데이터 노출 var def = selectedType == valueType ? String(value) : null; switch( true ){ // 문자형 입력 case selectedType == 'string': var ta_str = $( TAG.textarea ).val( def ).appendTo( d_form ).width('100%'); fn_val = function(){ return ta_str.val(); }; break; // 숫자형 입력 case selectedType == 'number': var i_number = $( TAG.number ).val( def ).appendTo( d_form ).width('100%'); fn_val = function(){ return +i_number.val() || 0; }; break; // 함수형 입력. 함수는 특성상 Global scope 를 대상으로 하는 함수만 정의 가능 하다. // 기존 함수가 특정 scope 안에서 동작하도록 되어있는 경우 수정으로 인해 오류가 발생할 수 있다. case selectedType == 'function': var ta_func = $( TAG.textarea ).val( def ).appendTo( d_form ).width('100%'); fn_val = function(){ return new Function( ta_func.val() ); }; break; // Object 나 Array 는 JSON 입력기를 제공 한다. case selectedType == 'object' : case selectedType == 'array' : if( (selectedType == 'array' && valueType == 'array') || (selectedType == 'object' && $.isPlainObject(value)) ) def = value; else def = selectedType == 'object' ? {} : []; try{ var json = JSON.stringify(def); } catch( e ){ alert( e ); } var ta_array = $( TAG.textarea ).val( json || '' ).appendTo( d_form ).width('100%'); fn_val = function(){ var r = JSON.parse(ta_array.val()); if( selectedType == 'object' ) return $.isPlainObject(r) ? r : {}; else return $.isArray(r) ? r : []; }; break; // null undefined false true.. default: fn_val = function(){ return eval( selectedType ); }; } }) // set default value s_type.change(); }); } /** * 셀 새로 그림 */ function reset(){ drawCell( key, o_key, o_cell ); } } function getKeyPath( key ){ return currentPath+'.'+key; } function getKeyName( key ){ var path = getKeyPath( key ); return opt.pathName[path] || key; } function checkPath( type, key ){ var o = opt[type], path = getKeyPath( key ); if( !o.length ) return undefined; for( var i in o ){ // console.dir( type + ' ? ' + path + ' indexof ' + o[i] + ' = ' + path.indexOf( o[i] ) ); if( path.indexOf( o[i] ) == 0 ) return true; } return false; } }; /** * DataTable 구성에 적합한 데이터인지 확인 * @param {mixed} data * @return {boolean} */ function checkTableType( data ){ return $.isArray( data ) || $.isPlainObject( data ); } function nl2br( text, whitespace ){ var str = String(text||'').replace(/\n/g, '<br/>'); if( whitespace ) str = str.replace(/\s/g, ' '); return str; } define('jquery.datatable', ['jquery'], function(){ return $.dataTable; }); })(jQuery, this, this.document);