YUI DataTable Custom CellEditor
При создании интерактивных веб приложений очень часто приходиться иметь дело с формами состоящими из зарание не определенного числа элементов. Для того чтобы каждый раз не изобретать велосипед стоит обратить внимание на уже готовые решения, такие как библиотека YUI и ее класс DataTable.
Очень здорово если класс DataTable и его CellEditor’ы решат задачу сразу, но что делать если нужно создать custom celleditor для своих данных?Для начала необходимо подключить все необходимые для работы библиотеки (я в своем примере подключаю не минимизированные – для того чтобы можно было в случае необходимости проследить что где и как работает) и создать простую таблицу на основе данных.
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<link rel="stylesheet" type="text/css" href="http://yui.yahooapis.com/2.8.0r4/build/fonts/fonts-min.css" />
<link rel="stylesheet" type="text/css" href="http://yui.yahooapis.com/2.8.0r4/build/datatable/assets/skins/sam/datatable.css" />
<script type="text/javascript" src="http://yui.yahooapis.com/2.8.0r4/build/yahoo-dom-event/yahoo-dom-event.js"></script>
<script type="text/javascript" src="http://yui.yahooapis.com/2.8.0r4/build/element/element.js"></script>
<script type="text/javascript" src="http://yui.yahooapis.com/2.8.0r4/build/datasource/datasource.js"></script>
<script type="text/javascript" src="http://yui.yahooapis.com/2.8.0r4/build/datatable/datatable.js"></script>
</head>
<body class="yui-skin-sam">
<div id="languagesDiv"></div>
<script type="text/javascript">
YAHOO.namespace('example')
YAHOO.example.Data = {
languages: [
{ label: 'english', value: 1 },
{ label: 'russian', value: 2 },
{ label: 'ukrainian', value: 3 }
],
skills: [
{ label: 'little', value: 1 },
{ label: 'middle', value: 2 },
{ label: 'good', value: 3 }
],
lng: [
{ language: 2, skill: 3 },
{ language: 3, skill: 3 }
]
}
YAHOO.example.getLabel = function (opts, val) {
for (var i = 0; i < opts.length; i++) {
if (opts[i].value == val) {
return opts[i].label
}
}
return '- N/A -'
}
var formatLanguage = function (elCell, oRecord, oColumn, oData) {
var langname = YAHOO.example.getLabel(YAHOO.example.Data.languages, oRecord.getData('language'))
var langskill = YAHOO.example.getLabel(YAHOO.example.Data.skills, oRecord.getData('skill'))
elCell.innerHTML = langname + ' (' + langskill + ')'
}
var myColumnDefs = [{ formatter: formatLanguage, label: 'LANGUAGES', key: 'language' }]
var myDataSource = new YAHOO.util.DataSource(YAHOO.example.Data.lng)
myDataSource.responseType = YAHOO.util.DataSource.TYPE_JSARRAY
myDataSource.responseSchema = { fields: ['language', 'skill'] }
var myDataTable = new YAHOO.widget.DataTable('languagesDiv', myColumnDefs, myDataSource, {})
</script>
</body>
</html>
Все делается точно так же как и в примерах документации к YUI виджету DataTable.
Сами данные в моем случае хранятся в массиве YAHOO.example.Data.lng
, массивы YAHOO.example.Data.languages
и YAHOO.example.Data.skills
– вспомогательные и служат для форматирования выводимых данных, а так же в будущем для генерации списков в редакторе.
Дальше самое интересное и сложное. Создание редактора началось с копирования класса TextboxCellEditor и его постепенного переписывания.
Редактор наследует класс BaseCellEditor и обязан реализовать ряд методов, что очень хорошо видно в исходниках.
;(function () {
YAHOO.widget.LanguageCellEditor = function (oConfigs) {
this._sId = 'yui-languageceditor' + YAHOO.widget.BaseCellEditor._nCount++
YAHOO.widget.LanguageCellEditor.superclass.constructor.call(this, 'language', oConfigs)
}
YAHOO.lang.extend(YAHOO.widget.LanguageCellEditor, YAHOO.widget.BaseCellEditor, {
textbox: null,
renderForm: function () {
var elTextbox
// Bug 1802582: SF3/Mac needs a form element
// wrapping the input
if (YAHOO.env.ua.webkit > 420) {
elTextbox = this.getContainerEl().appendChild(document.createElement('form')).appendChild(document.createElement('input'))
} else {
elTextbox = this.getContainerEl().appendChild(document.createElement('input'))
}
elTextbox.type = 'text'
this.textbox = elTextbox
// Save on enter by default
// Bug: 1802582 Set up a listener on each textbox to
// track on keypress
// since SF/OP can't preventDefault on keydown
YAHOO.util.Event.addListener(
elTextbox,
'keypress',
function (v) {
if (v.keyCode === 13) {
// Prevent form submit
YAHOO.util.Event.preventDefault(v)
this.save()
}
},
this,
true
)
if (this.disableBtns) {
// By default this is no-op since enter saves by
// default
this.handleDisabledBtns()
}
},
move: function () {
this.textbox.style.width = this.getTdEl().offsetWidth + 'px'
YAHOO.widget.LanguageCellEditor.superclass.move.call(this)
},
resetForm: function () {
this.textbox.value = YAHOO.lang.isValue(this.value) ? this.value.toString() : ''
},
focus: function () {
// Bug 2303181, Bug 2263600
this.getDataTable()._focusEl(this.textbox)
this.textbox.select()
},
getInputValue: function () {
return this.textbox.value
}
})
YAHOO.lang.augmentObject(YAHOO.widget.LanguageCellEditor, YAHOO.widget.BaseCellEditor)
})()
Все что я сделал:
- первым делом это скопировал содержимое их исходников
- удалил все что не касалось объявления TextboxCellEditor
- переименовал
TextboxCellEditor
наLanguageCellEditor
Основной задачей всех этих манипуляций было получение своего редактора для дальнейшего его изменения.
Получилось вот так:
Уже сейчас все это дело работает, изменяя цифру мы тем самым меняем язык.
Собственно забавно получается, кода стало еще больше, а эффект все еще смешной, но есть обнадеживающий факт – у меня уже есть свой редактор который я могу изменять как угодно. Изучив немного его исходники стало понятно как он работает, но остался один не закрытый вопрос, как редактировать запись целиком (язык и уровень владения), а не её отдельную часть (язык). После изучения исходников класса BaseCellEditor
нашел два места которые нужно изменить, собственно методы attach
и save
, в них идет основная работа с редактируемыми значениями и их нужно переопределить. Так же нужно в редакторе показывать два списка вместо текстового поля, большую часть кода подсмотрел из класса DropdownCellEditor
.
;(function () {
YAHOO.widget.LanguageCellEditor = function (oConfigs) {
this._sId = 'yui-languageceditor' + YAHOO.widget.BaseCellEditor._nCount++
YAHOO.widget.LanguageCellEditor.superclass.constructor.call(this, 'language', oConfigs)
}
YAHOO.lang.extend(YAHOO.widget.LanguageCellEditor, YAHOO.widget.BaseCellEditor, {
// CELL EDITOR OVERRIDEN METHOD, TO EDIT ENTIRE ROW
// RATHER THAT CELL
// START: DO NOT CHANGE THIS
attach: function (oDataTable, elCell) {
if (YAHOO.widget.LanguageCellEditor.superclass.attach.call(this, oDataTable, elCell) == false) return false
// CHANGE: rewrite current value with entire record
this.value = this._oRecord
return true
},
save: function () {
// Get new value
var inputValue = this.getInputValue()
var validValue = inputValue
// Validate new value
if (this.validator) {
validValue = this.validator.call(this.getDataTable(), inputValue, this.value, this)
if (validValue === undefined) {
if (this.resetInvalidData) {
this.resetForm()
}
this.fireEvent('invalidDataEvent', {
editor: this,
oldData: this.value,
newData: inputValue
})
YAHOO.log('Could not save Cell Editor input due to invalid data ' + YAHOO.lang.dump(inputValue), 'warn', this.toString())
return
}
}
var oSelf = this
var finishSave = function (bSuccess, oNewValue) {
var oOrigValue = oSelf.value
if (bSuccess) {
// Update new value
oSelf.value = oNewValue
// CHANGED: we update entire row rather that
// single cell
// oSelf.getDataTable().updateCell(oSelf.getRecord(),
// oSelf.getColumn(), oNewValue);
oSelf.getDataTable().updateRow(oSelf.getRecord(), oNewValue)
// Hide CellEditor
oSelf.getContainerEl().style.display = 'none'
oSelf.isActive = false
oSelf.getDataTable()._oCellEditor = null
oSelf.fireEvent('saveEvent', {
editor: oSelf,
oldData: oOrigValue,
newData: oSelf.value
})
YAHOO.log('Cell Editor input saved', 'info', this.toString())
} else {
oSelf.resetForm()
oSelf.fireEvent('revertEvent', {
editor: oSelf,
oldData: oOrigValue,
newData: oNewValue
})
YAHOO.log('Could not save Cell Editor input ' + YAHOO.lang.dump(oNewValue), 'warn', oSelf.toString())
}
oSelf.unblock()
}
this.block()
if (YAHOO.lang.isFunction(this.asyncSubmitter)) {
this.asyncSubmitter.call(this, finishSave, validValue)
} else {
finishSave(true, validValue)
}
},
// END: DO NOT CHANGE THIS
// CELL EDITOR PROPERTIES
ddlLanguageName: null,
ddlLanguageSkill: null,
languageOptions: null,
languageSkillOptions: null,
// CELL EDITOR IMPLEMENTED METHODS
renderForm: function () {
var elLanguageName = this.getContainerEl().appendChild(document.createElement('select'))
elLanguageName.style.zoom = 1
this.ddlLanguageName = elLanguageName
var elNAOption = document.createElement('option')
elNAOption.value = 0
elNAOption.innerHTML = '- N/A -'
elLanguageName.appendChild(elNAOption)
if (YAHOO.lang.isArray(this.languageOptions)) {
var dropdownOption, elOption
for (var i = 0, j = this.languageOptions.length; i < j; i++) {
dropdownOption = this.languageOptions[i]
elOption = document.createElement('option')
elOption.value = YAHOO.lang.isValue(dropdownOption.value) ? dropdownOption.value : dropdownOption
elOption.innerHTML = YAHOO.lang.isValue(dropdownOption.label) ? dropdownOption.label : dropdownOption
elOption = elLanguageName.appendChild(elOption)
}
}
var elLanguageSkill = this.getContainerEl().appendChild(document.createElement('select'))
elLanguageSkill.style.zoom = 1
this.ddlLanguageSkill = elLanguageSkill
var elNAOption = document.createElement('option')
elNAOption.value = 0
elNAOption.innerHTML = '- N/A -'
elLanguageSkill.appendChild(elNAOption)
if (YAHOO.lang.isArray(this.languageSkillOptions)) {
var dropdownOption, elOption
for (var i = 0, j = this.languageSkillOptions.length; i < j; i++) {
dropdownOption = this.languageSkillOptions[i]
elOption = document.createElement('option')
elOption.value = YAHOO.lang.isValue(dropdownOption.value) ? dropdownOption.value : dropdownOption
elOption.innerHTML = YAHOO.lang.isValue(dropdownOption.label) ? dropdownOption.label : dropdownOption
elOption = elLanguageSkill.appendChild(elOption)
}
}
if (this.disableBtns) {
this.handleDisabledBtns()
}
},
resetForm: function () {
this.ddlLanguageName.value = YAHOO.lang.isValue(this.value.getData('language')) ? this.value.getData('language') : 0
this.ddlLanguageSkill.value = YAHOO.lang.isValue(this.value.getData('skill')) ? this.value.getData('skill') : 0
},
getInputValue: function () {
var res = {}
res.language = this.ddlLanguageName.value
res.skill = this.ddlLanguageSkill.value
return res
}
})
YAHOO.lang.augmentObject(YAHOO.widget.LanguageCellEditor, YAHOO.widget.BaseCellEditor)
})()
Основная часть задачи уже решена, в методах attach
и save
комментариями отмечены места которые были изменены, для достижения результата, теперь наш редактор гоняет запись целиком, вместо того чтобы гонять значение одной ячейки.
Следующим логичным этапом будет добавление возможности добавления и удаления записей, что делается весьма просто.
Я просто добавил соответствующие ссылки в шапку таблицы и к каждой записи:
var formatLanguage = function (elCell, oRecord, oColumn, oData) {
var langname = YAHOO.example.getLabel(YAHOO.example.Data.languages, oRecord.getData('language'))
var langskill = YAHOO.example.getLabel(YAHOO.example.Data.skills, oRecord.getData('skill'))
elCell.innerHTML =
'<a href="javascript:void(0)" onclick="deleteLanguage(event, \'' +
oRecord.getId() +
'\')" style="text-decoration:none;color:#f00;">x</a> ' +
langname +
' (' +
langskill +
')'
}
var myColumnDefs = [
{
label: 'LANGUAGES <button onclick="addLanguage(event)">add</button>',
key: 'language',
formatter: formatLanguage,
editor: new YAHOO.widget.LanguageCellEditor({
languageOptions: YAHOO.example.Data.languages,
languageSkillOptions: YAHOO.example.Data.skills
})
}
]
и добавил две новые функции:
function addLanguage(e) {
// PREVENT BLUR EVENTS
e = e || window.event
YAHOO.util.Event.stopEvent(e)
var r = {}
r.language = 0
r.skill = 0
myDataTable.addRow(r)
myDataTable.showCellEditor(myDataTable.getLastTrEl().cells[0])
}
function deleteLanguage(e, recordId) {
// PREVENT SHOW CELL EDITOR EVENT
e = e || window.event
YAHOO.util.Event.stopEvent(e)
myDataTable.deleteRow(recordId)
}
Теперь уже совсем то что нужно, но не для меня, мне нужно добиться совсем другого чем я имею сейчас. Первое что мне нужно это чтобы окно редактирования не плавало по странице, а как бы заменяло ячейку, естественно так глубоко я не залез, и пошел простым путем, все что я сделал, это метод
fixTdHeight: function(bSetHeightToAuto){
this.getTdEl().style.height = (bSetHeightToAuto) ? 'auto' : this.getContainerEl().offsetHeight + 'px';
}
который делал высоту ячейки таблицы такой же как и высота редактора, и вызывал этот метод когда окно редактора появлялось либо пряталось (методы: save
, move
, cancel
, show
).
move: function(){
this.getContainerEl().style.width = (this.getTdEl().offsetWidth - 14) + "px";
YAHOO.widget.LanguageCellEditor.superclass.move.call(this);
},
show: function(){
YAHOO.widget.LanguageCellEditor.superclass.show.call(this);
this.fixTdHeight(false);
},
cancel: function(){
YAHOO.widget.LanguageCellEditor.superclass.cancel.call(this);
this.fixTdHeight(true);
},
Вот теперь это уже почти то что мне нужно, но остался еще один важный момент – валидация. Для его реализации пришлось перелопатить целую кучу кода, собственно тут уже каждый сам для себя должен определить что и как ему нужно. Дело в том что поведение редактора по умолчанию меня совсем не устраивает, так например, нажав на кнопку добавления записи, мы добавляем новую запись со значениями равными нулю, мне бы хотелось чтобы при отмене эта запись удалялась. Так же при попытке сохранить значение не удовлетворяющее правилам валидации, редактор просто закрывается и не сохраняет значение – мне нужно оставлять его открытым и показывать уведомление об ошибке.
По концовке получился вот такой контрол:
Вот что из этого всего получилось в реальных условиях:
К сожалению посмотреть пощупать так просто не получится, придеться довольствоваться видео