Files
tipper/static/formobj.js
2026-07-21 05:29:37 +03:00

2324 lines
70 KiB
JavaScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* @property {boolean} editable - флаг, показывает можно ли изменять модель (добавлять/обновлять).
В чем проблема с binded полями?
Есть поле deviceID, которое получаем из SELECT. Поле deviceID нужно отправить на сервер.
Но при построении интерфейса нужно больше:
- объект device,
чтобы вывести имя объекта в списке ListModel, например "Sharky 775 (m-bus)"
чтобы заполнить addr SELECT, который зависит от device.connector.connectorID
- список объектов из device SELECT, чтобы заполнить SELECT без обращения к серверу,
когда юзер будет кликать по модемам, пытаясь что-то отредактировать.
- deviceID, чтобы отправить на сервер как поле объекта и чтобы выбрать в SELECT
нужный пункт, когда юзер щелкает пытаясь что-то отредактировать.
Список можно не хранить, а загружать снова с сервера.
Но объект и ID хранить обязательно.
Либо можно вызывать обработчик, которому передавать объект, а тот в свою очередь
будет строить addr SELECT, либо внедрит поле 'name' в formobj, которое будет
выводится как заголовок в списке ListModel. Минус - на сервер будет отправляться
поле 'name', которое там не ждут.
Можно пойти еще дальше. Обработчик внедряет поле '_name', с префиксом '_'. Когда мы
получаем полный объект для отправки на сервер (Форма без привязанной модели) - рекурсивно
обходит полученный объект и удаляем все служебные поля с префиксом '_'
*/
function Formobj(opt) {
Emiter.call(this)
// Родительская форма
this._parent = opt.parent
this._isEditable = opt.editable
this._form = getElemSafe('form', opt.form)
//console.log('BUILD:', this._form)
//
this._errorClass = opt.errorClass || 'errmsg'
this._validate = opt.validate
// Для FIX
this._version = 0
//this._embed = opt.embed || {}
this._fields = {}
this._predefined = opt.predefined || {}
//this._schema = opt.schema || {}
//this._nonUpdatable = opt.nonUpdatable || {}
this._transformData = opt.transformData
this._userOnSubmit = opt.onSubmit
// result - успешный ответ API функции submitFunc
// API ошибка автоматически вызовет showError
this._onSubmitResult = opt.onSubmitResult
if (opt.model) {
this._model = opt.model
this._model.on('select', obj => this.setObj(obj))
} else if (opt.models) {
this._models = opt.models
this._getModel = opt.getModel
if (typeof opt.getModel != 'function') {
throw new Error(`'models' option requires 'getModel' option`)
}
for (let prop in this._models) {
let model = this._models[prop]
model.on('select', obj => {
if (obj) {
// снимаем выделение с других моделей, если есть
for (let modelKey in this._models) {
let item = this._models[modelKey]
if (modelKey != prop) {
console.log('UNSELECT:', modelKey)
item.unselect()
}
}
}
this.setObj(obj)
})
}
}
this._tree = this._buildTree(opt)
console.log('TREE:', this._tree)
//this._plain = x.plain
//this._custom = x.custom
//this._embed = x.embed
this._form.addEventListener('submit', (e) => {
e.preventDefault()
this._onSubmit()
})
if (opt.submit) {
this._submit = getElemSafe('submit', opt.submit)
} else {
// Поиск submit-кнопки
for (let i=0; i<this._form.elements.length; i++) {
let elem = this._form.elements.item(i)
if (elem.getAttribute('type') == 'submit') {
this._submit = elem
}
}
}
//if (!this._submit) {
// throw new Error('submit button not found')
//}
if (this._submit) {
if (this._submit.tagName == 'INPUT') {
this._btnSubmitText = this._submit.value
} else if (this._submit.tagName == 'BUTTON') {
this._btnSubmitText = this._submit.textContent
}
this._isSubmitDisabledByDefault = this._submit.disabled
}
this._btnUpdateText = opt.btnUpdateText || this._btnSubmitText
this._api = opt.api
//this._updateFunc = opt.updateFunc
if (opt.submitFunc) {
this._getSubmitFunc = function() {
return opt.submitFunc
}
} else if (opt.getSubmitFunc) {
this._getSubmitFunc = opt.getSubmitFunc
}
//this._submitFunc = opt.submitFunc
//this._removeFunc = opt.removeFunc
if (opt.removeFunc) {
this._getRemoveFunc = function() {
return opt.removeFunc
}
} else if (opt.getRemoveFunc) {
this._getRemoveFunc = opt.getRemoveFunc
}
this._getRemoveData = opt.getRemoveData
this._btnRemoveText = opt.btnRemoveText
this._btnRemoveClass = opt.btnRemoveClass
this._key = opt.key
/*
if (this._getRemoveFunc) {
if (!this._model) {
throw new Error(`'removeFunc' option requires 'model' option`)
}
//if (!this._) {
//throw new Error(`'removeFunc' option requires 'key' option`)
//}
}
*/
if (opt.btnRemove) {
this._btnRemove = getElemSafe('btnRemove', opt.btnRemove)
if (!this._getRemoveFunc) {
throw new Error(`'btnRemove' option requires 'removeFunc' option`)
}
this._isBtnRemoveDisabledByDefault = this._btnRemove.disabled
this._btnRemove.addEventListener('click', () => {
// На модель не проверяем, ибо уже проверили в конструкторе
let selected = this._model.getSelected()
if (selected) {
let p = {}
if (this._getRemoveData) {
p = this._getRemoveData(selected)
} else {
let id = selected[this._key]
//p[this._key] = id
p = id // data - ключ вместо объекта
}
this._btnRemove.disabled = true
let url = this._getRemoveFunc(selected)
this._api.req({
func: url,
data: p,
onError: err => {
this._btnRemove.disabled = false
this.showError(err)
},
onSuccess: resp => {
if (this._model) {
// Именно перезагружаем, чтобы избежать ошибок, если модель с параметром.
console.log('model reload, form reset (removeFunc)')
this._model.reload()
this.reset()
} else {
if (this._isSubmitDisabledByDefault) {
this._calcSignature()
}
if (this._submit) {
this._submit.disabled = this._isSubmitDisabledByDefault
}
this._btnRemove.disabled = this._isBtnRemoveDisabledByDefault
}
// FIX
if (this._userOnSubmit) {
this._userOnSubmit()
}
}
})
return
}
})
}
/*
if (this._updateFunc) {
if (!this._api) {
throw new Error("'updateFunc' requires 'api' option")
}
}
*/
if (this._getSubmitFunc) {
if (!this._api) {
throw new Error("'submitFunc' requires 'api' option")
}
}
// добавить setifdisabled
this._createObjects(this._tree)
if (this._isSubmitDisabledByDefault) {
//this._listenChanges()
this._calcSignature()
}
// init custom elements
//console.log('INIT:', this._tree.custom)
for (let prop in this._tree.custom) {
let c = this._tree.custom[prop]
if (c.getFunc) {
this._loadData(c)
}
}
}
Formobj.prototype = Object.create(Emiter.prototype)
Formobj.prototype.constructor = Formobj
Formobj.EDITABLE = 1
Formobj.READONLY = 2
Formobj.DISABLED = 3
Formobj.prototype.isDisabled = function() {
/*
if (this._parent) {
return this._parent.isDisabled()
}
if (this._model && !this._isEditable && this._model.getSelected()) {
return true
}
return false
*/
if (this._isEditable) {
return false
} else {
if (this._settingObj) {
return true
}
return false
}
}
/*
Formobj.prototype.restoreState = function() {
let stateStr = localStorage.getItem('x')
if (stateStr) {
let state = JSON.parse(stateStr)
console.log('SET STATE:', stateStr)
this.setObj(state)
}
}
*/
// po - parent object
Formobj.prototype._loadData = function(item, po, id) {
//console.log('_loadData:', item, po, id)
// В кэше нет - загружаем
let version = this._version
if (this._settingObj) {
this._settingObj.loadingCount++
}
item.loading = true
this._api.req({
func: item.getFunc(po),
data: id,
onError: err => {
item.loading = false
if (version !== this._version) {
//console.log('version changed! this._version != version', this._version, version)
return
}
item.divLoadError = insertError({
after: item.elem.getAnchor(),
err: err,
class: this._errorClass
})
//console.log("load:", err)
if (this._settingObj) {
this._settingObj.loadingCount--
this._onFormLoaded()
}
},
onSuccess: resp => {
item.loading = false
if (version !== this._version) {
//console.log('version changed! this._version != version', this._version, version)
return
}
// Добавляем в кэш
// fix перенести в Attached
//let m = this._cache[item.prop]
//m.set(id, resp)
let cleanResp = resp
if (item.responseModifier) {
cleanResp = item.responseModifier(resp)
}
item.cache.set(id, cleanResp)
item.elem.setList(cleanResp)
this._setValueIfEditMode(item)
if (this._settingObj) {
this._settingObj.loadingCount--
this._onFormLoaded()
}
}
})
}
Formobj.prototype._onFormLoaded = function() {
if (this._settingObj.loadingCount == 0) {
//console.log('_onFormLoaded:', this, this._isSubmitDisabledByDefault)
if (this._isSubmitDisabledByDefault) {
this._calcSignature()
}
this._settingObj = null
}
}
Formobj.prototype._calcSignature = function() {
// Нет проблемы если AJAX элементы не загрузились.
// Все 'list' элементы вернут [], все 'select' элементы вернут undefined
this._signature = JSON.stringify(this.cleanObj(this.getFormData()))
//this._signature = JSON.stringify(this.getFormData())
console.log('sign:', this._signature)
}
/*
Formobj.prototype._listenChanges = function() {
//console.log('listenChanges: ', this, JSON.stringify(this._tree))
if (!this._isSubmitDisabledByDefault) {
return
}
let x = this._tree
// Повесим на все элементы формы обработчики FIX
for (let i=0; i<this._form.elements.length; i++) {
let elem = this._form.elements[i]
//console.log('elem:', elem.name)
//console.log('x.plain:', x.plain)
//console.log('x.keys:', Object.keys(x))
// Если такого поля нет в схеме
if (x.plain && !x.plain[elem.name]) {
continue
}
let eventName = 'input' // для input, textarea
// Чтобы избежать багов в разных браузерах - для select, checkbox и
// radio - повесим обработчик на другое событие - 'change'
if (elem.nodeName == 'SELECT') {
eventName = 'change'
} else if (elem.nodeName == 'INPUT') {
if (elem.type == 'radio' || elem.type == 'checkbox') {
eventName = 'change'
}
}
elem.addEventListener(eventName, () => this._onAnyChange())
}
for (let prop in x.embed) {
x.embed[prop].model.on('listChanged', () => {
//console.log('listChanged -> on any change')
this._onAnyChange()
})
}
// только топ уровень
if (x.custom) {
//let items = this._attachedObj.listItems()
for (let prop in x.custom) {
let a = x.custom[prop]
//console.log('listen select on:', a.type)
switch (a.elem.getType()) {
case 'select':
// Select, RadioList
a.elem.on('select', () => this._onAnyChange())
break;
case 'list':
// InputList, CheckList
a.elem.on('changed', () => this._onAnyChange())
break;
}
}
}
this._calcSignature()
}
*/
Formobj.prototype._onAnyChange = function() {
//console.log('select changed:', this.isDisabled(), JSON.stringify(this.cleanObj(this.getFormData())))
//if (this._loadingCount > 0) {
// console.log('loadingCount > 0')
// return
//}
if (this._reseting) {
//console.log('reseting:', this._reseting, this._form)
return
}
if (this._settingObj) {
//console.log('settingObj:', this._settingObj, this._form)
return
}
if (this.isDisabled()) {
//console.log('disabled:', this._form)
return
}
//console.log('anyChange current:', this._signature)
//console.log('anyChange new:', JSON.stringify(this.cleanObj(this.getFormData())))
let stateStr = JSON.stringify(this.cleanObj(this.getFormData()))
//console.log('anyChange current sign:', this._signature)
//console.log('anyChange current:', stateStr)
if (this._signature != stateStr) {
this._submit.disabled = false
} else {
this._submit.disabled = true
}
//localStorage.setItem('x', stateStr)
// fix
//console.log("form changed")
//this.emit('change')
}
//Formobj.prototype.isDisabled = function() {
//}
Formobj.prototype._setValueIfEditMode = function(item) {
//console.log('!!!!!!!!!! setValueIfEditMode:', item)
// если режим редактирования
if (this._settingObj) {
if (item.manage) {
//console.log('setValueIfEditMode', item)
let nestedObj = this._settingObj.obj[item.prop]
// удалили, после выбора юзером другого пункта в процессе редактирования
//console.log('!!!!!!!_setValueIfEditMode:', this._version, item, nestedObj)
//if (nestedObj) {
switch (item.elem.getType()) {
case 'select':
//let nestedObj = this._settingObj.obj[item.prop]
if (nestedObj) {
//console.log('select (edit mode): ', nestedObj)
if (item.key) {
//console.log('SELECT: ', nestedObj[b.key], b.elem)
item.elem.select(nestedObj[item.key])
} else {
item.elem.select(nestedObj)
}
}
//item.elem.select(nestedObj[item.key])
break
case 'list':
// Хитрый код для такого кейса:
// отмечаем пункты галочками - на сервер уходит массив ключей, например,
// commandIDs. С сервера получаем массив объектов commands. Чтобы отметить
// галочками нужные пункты при setObj, нужно знать имя поля (commands) с
// массивом объектов, а также имя поля с ID объекта (commandID). Для этой
// служат опции setProp и setKey
if (nestedObj) {
// Просмотр добавленого через JS, до отправки на сервер
item.elem.setValues(nestedObj)
} else if (item.setProp && item.setKey) {
let objects = this._settingObj.obj[item.setProp]
if (objects) {
let keys = []
objects.forEach(o => {
keys.push(o[item.setKey])
})
item.elem.setValues(keys)
}
}
break;
case 'input':
if (nestedObj) {
if (item.key) {
item.elem.setValue(nestedObj[item.key])
} else {
item.elem.setValue(nestedObj)
}
//item.elem.setValue(nestedObj)
}
break;
}
// let obj = this._settingObj.obj
let t = item.tree
if (t) {
//console.log('tree:', t)
if (t.current) {
this._setObj(t.current)
}
}
// отключаем зависимые элементы, которые depends от кого-то
/*
if (this.isDisabled()) {
item.elem.disable()
}
*/
//}
}
// отключаем независимые attached элементы (у которых нет depends),
if (this._isEditable) {
// Если форма редактируемая, а поле нет - отключаем
if (item.editmode != Formobj.EDITABLE) {
item.elem.disable()
}
} else {
// Если форма НЕ редактируемая - просто отключаем
item.elem.disable()
}
/*
if (this.isDisabled()) {
item.elem.disable()
} else {
if (item.readonly) {
item.elem.disable()
}
// WARNING! NEW FUTURE
// SCHEMA FUTURE
//if (this._nonUpdatable[item.prop]) {
// item.elem.disable()
// }
}
*/
}
}
/*
Formobj.prototype.setIsDisabledFunc = function(f) {
this.isDisabled = f
for (let prop in this._tree.embed) {
let embed = this._tree.embed[prop]
embed.form.setIsDisabledFunc(f)
if (embed.model) {
embed.model.setIsDisabledFunc(f)
} else {
for (let modelKey in embed.multiModel) {
embed.multiModel[modelKey].setIsDisabledFunc(f)
}
}
}
}
*/
Formobj.prototype._resetTree = function() {
}
Formobj.prototype._resetFields = function(x) {
if (x.schema) {
for (let prop in x.schema) {
let f = x.schema[prop]
//if (elem)
//console.log('RESET prop:', prop, 'value:', f.defaultValue)
f.elem.value = f.defaultValue
f.elem.disabled = false
}
}
if (this._disabledElems) {
this._disabledElems.forEach((elem) => {
//console.log('ENABLE:', elem.name)
elem.disabled = false
})
this._disabledElems = null
}
if (this._readonlyElems) {
this._readonlyElems.forEach((elem) => {
elem.readOnly = false
})
this._readonlyElems = null
}
// reset custom
if (x.custom) {
for (let prop in x.custom) {
let c = x.custom[prop]
// независимые элемены сбрасываем, а зависимые очищаем
if (c.manage) {
c.elem.reset()
}
// делаем unlock для независимых элементов
// зависимые очищаются (clear), поэтому enable делать не смысла
c.elem.enable()
//c.chains.forEach((nested) => {
// this._clearDepended(nested)
//})
let t = c.tree
if (t) {
if (t.current) {
this._resetFields(t.current)
}
//for (let prop in c.tree) {
//let t = c.tree[prop]
//console.log('reset tree:', t)
//}
}
}
}
for (let prop in x.embed) {
let embed = x.embed[prop]
//console.log('reset embed')
embed.form.reset()
if (embed.model) {
embed.model.clear()
} else {
for (let modelKey in embed.multiModel) {
embed.multiModel[modelKey].clear()
}
}
}
}
Formobj.prototype.reset = function() {
//console.log('reset:', this)
this._reseting = true
this.hideError()
//this._form.reset()
this._resetFields(this._tree)
//console.log('ENABLE list:', this._disabledElems, this)
//console.log(this._readonlyElems)
if (this._submit) {
this._submit.classList.remove(this._btnRemoveClass)
if (this.isDisabled()) {
this._submit.disabled = true
} else {
this._setSubmitText(this._btnSubmitText)
this._submit.disabled = this._isSubmitDisabledByDefault
}
}
if (this._btnRemove) {
this._btnRemove.disabled = this._isBtnRemoveDisabledByDefault
}
this._version++
this._settingObj = null
this._reseting = false
//console.log('reseted form:', this._form)
}
Formobj.prototype._setObj = function(x) {
let obj = this._settingObj.obj
console.log('_setObj:', x, obj)
//console.log('setSchemaFields:', this)
// Быстрые ссылки на disabled элементы из schema. В список попадают только элементы
// c disabled = false
this._disabledElems = []
this._readonlyElems = []
//if (x.plain)
for (let prop in x.schema) {
let f = x.schema[prop] // field
let elem = f.elem
//console.log('set schema value:', prop, f, obj[prop])
if (obj[prop] !== undefined) {
let v = obj[prop]
if (f.dataType == 'bool') {
if (v) {
f.elem.checked = true
} else {
f.elem.checked = false
}
} else {
if (f.dataType == 'hex') {
if (v) {
v = base64ToHex(v)
}
}
if (f.set) {
elem.value = f.set(v)
} else {
elem.value = v
}
}
}
if (this._isEditable) {
switch (f.editmode) {
case Formobj.DISABLED:
elem.disabled = true
this._disabledElems.push(elem)
break
case Formobj.READONLY:
if (elem.tagName == 'SELECT') {
elem.disabled = true
this._disabledElems.push(elem)
} else {
elem.readOnly = true
this._readonlyElems.push(elem)
}
break
//case Formobj.EDITABLE:
// break
}
} else {
elem.disabled = true
this._disabledElems.push(elem)
}
/*
if (this.isDisabled()) {
console.log('disable:', prop)
if (!elem.disabled) {
elem.disabled = true
this._disabledElems.push(elem)
}
} else {
console.log('not disabled:', prop, f)
// WARNING! NEW FUTURE
if (f.readonly) {
if (elem.tagName == 'SELECT') {
elem.disabled = true
this._disabledElems.push(elem)
} else {
elem.readOnly = true
this._readonlyElems.push(elem)
}
}
}
*/
}
//console.log('DISABLED:', this._disabledElems)
if (x.custom) {
// только топ уровень
for (let prop in x.custom) {
let c = x.custom[prop]
console.log('custom:', c)
//if (!c.manage) {
//continue
//}
if (c.loading) {
console.log('loading')
continue
}
let nestedObj = obj[prop]
//console.log('c:', c, 'nestedObj:', nestedObj, 'type:', c.elem.getType())
switch (c.elem.getType()) {
case 'select':
if (c.key) {
//console.log('select (edit mode): ', nestedObj[c.key])
c.elem.select(nestedObj[c.key])
} else {
// console.log('select (edit mode): ', nestedObj)
c.elem.select(nestedObj)
}
//console.log('selected:', c.elem.getSelected())
//item.elem.select(nestedObj[item.key])
break
case 'list':
c.elem.setValues(nestedObj)
break;
case 'input':
if (c.key) {
c.elem.setValue(nestedObj[c.key])
} else {
c.elem.setValue(nestedObj)
}
//item.elem.setValue(nestedObj)
break;
}
if (this._isEditable) {
if (c.editmode != Formobj.EDITABLE) {
c.elem.disable()
}
} else {
c.elem.disable()
}
let t = c.tree
if (t) {
//console.log('tree:', t)
if (t.current) {
this._setObj(t.current)
}
}
}
}
if (x.embed) {
for (let prop in x.embed) {
let embed = x.embed[prop]
if (embed.model) {
embed.model.setList(obj[prop])
} else {
for (let modelKey in embed.multiModel) {
embed.multiModel[modelKey].setList(obj[modelKey])
}
}
}
}
}
// типа expand
Formobj.prototype.setObj = function(obj) {
console.log('setObj:', obj, this)
//if (!obj) {
//console.log('caller:', arguments.callee.caller.toString())
//console.trace()
//}
this.reset()
if (!obj) {
return
}
this._settingObj = {
loadingCount: 0,
obj: obj,
}
this._setObj(this._tree)
//if (this._attachedObj) {
//this._attachedObj.setObj(obj)
//}
// this._setSchemaFields(this._schema, obj)
//for (let prop in this._embed) {
// this._embed[prop].model.setList(obj[prop])
//}
// /console.log('SETOBJ: isDisabled:', this.isDisabled())
if (this.isDisabled()) {
if (this._submit) {
/*
if (this._removeFunc) {
this._setSubmitText(this._btnRemoveText)
this._submit.classList.add(this._btnRemoveClass)
this._submit.disabled = false
}
*/
}
//this._submit.disabled = true // кнопка установлена в reset()
} else {
if (this._submit) {
this._setSubmitText(this._btnUpdateText)
}
// Если форма целиком заполнена - считаем signature
// Кнопку отключать не нужно, ибо в reset уже отключили
this._onFormLoaded()
}
if (this._btnRemove) {
this._btnRemove.disabled = false
}
}
Formobj.prototype._setSubmitText = function(txt) {
if (this._submit.tagName == 'INPUT') {
this._submit.value = txt
} else if (this._submit.tagName == 'BUTTON') {
this._submit.textContent = txt
}
}
/*
Сделать общую мапу для error элементов?
Ошибки:
- для plain элементов,
- для custom элементов,
- для отдельного пункта в списке (списки простых значений допускаются только в custom элементах)
- для модели в целом (например пустой список недопустим)
_fields
*/
Formobj.prototype.getElem = function(name) {
let f = this._fields[name]
if (!f) {
throw new Error(`Unknown field ${name}`)
}
if (f.type == 'custom' || f.type == 'plain') {
return f.elem
}
throw new Error(`Unknown field ${name}`)
}
Formobj.prototype.showError = function(err) {
this.hideError()
//console.log('showErr: ', err)
//console.log('fields:', this._fields)
if (Array.isArray(err.path) && err.path.length > 0) {
/*
let leg = err.path.shift()
let embed = this._tree.embed[leg.field]
if (!embed) {
throw new Error(`Wrong leg: ${JSON.stringify(leg)}`)
}
if (embed.model.getSelected()) {
embed.model.unselect()
}
embed.model.select(leg.idx)
embed.form.showError(err)
*/
let leg = err.path.shift()
let f = this._fields[leg.field]
if (!f || f.type != 'embed') {
throw new Error(`Wrong leg: ${JSON.stringify(leg)}`)
}
if (f.model.getSelected()) {
f.model.unselect()
}
f.model.select(leg.idx)
f.form.showError(err)
} else {
let anchor;
if (err.field) {
let f = this._fields[err.field]
if (!f) {
throw new Error(`Error message for unknown field ${err.field}`)
}
switch (f.type) {
case 'custom':
anchor = f.elem.getAnchor()
break;
case 'plain':
anchor = f.elem
break;
case 'embed':
this._error = f.model.setError(err)
return
/*
for (let prop in this._tree.embed) {
let embed = this._embed[prop]
if (embed.model.getSelected()) {
embed.model.unselect()
}
}
*/
default:
throw new Error(`Unknown field type ${f.type}`)
}
} else if (err.list) {
let f = this._fields[err.list]
if (!f || f.type != "custom") {
throw new Error(`Error message for unknown list ${err.list}`)
}
anchor = f.elem.getAnchor(err.idx)
}
if (anchor) {
this._error = insertError({
after: anchor,
err: err,
class: this._errorClass
})
} else {
// Если якорь не найден (поля field и list пустые) - ошибка относится к форме
// целиком. Поэтому показываем перед submit кнопкой.
this._error = insertError({
before: this._submit,
err: err,
class: this._errorClass
})
}
// нужно ли ?
/* // multiModel ???
for (let prop in this._tree.embed) {
let embed = this._tree.embed[prop]
if (embed.model.getSelected()) {
embed.model.unselect()
}
}
*/
}
}
Formobj.prototype.hideError = function() {
if (this._error) {
this._error.remove()
}
}
Formobj.prototype._onSubmit = function() {
this.hideError()
let obj = this.getFormData()
if (this._validate) {
let err = this._validate(obj)
if (err) {
this.showError(err)
return
}
}
//console.log('formData:', obj)
// Кейсы:
// 1. Модель загружается по api. Форма добавляет объект по api. Редактирования нет.
// Привязка нужна для просмотра.
// 2. Модель загружается по api. Форма добавляет объект по api, редактирует по api.
// Привязка нужна для редактирования. Модель canUpdate = false, но непустой
// _updateFunc позволит не отключать форму.
// 3. Модель заполняется и редактируется из формы.
/*
if (this._removeFunc) {
// На модель не проверяем, ибо уже проверили в конструкторе
let selected = this._model.getSelected()
if (selected) {
let id = selected[this._key]
let p = {}
p[this._key] = id
this._submit.disabled = true
this._api.req({
"func": this._removeFunc,
"data": p,
"onError": err => {
this._submit.disabled = false
this.showError(err)
},
"onSuccess": resp => {
if (this._model) {
// Именно перезагружаем, чтобы избежать ошибок, если модель с параметром.
console.log('model reload, form reset (removeFunc)')
this._model.reload()
this.reset()
} else {
if (this._isSubmitDisabledByDefault) {
this._calcSignature()
}
this._setSubmitText(this._btnSubmitText)
this._submit.disabled = this._isSubmitDisabledByDefault
}
// FIX
if (this._userOnSubmit) {
this._userOnSubmit()
}
}
})
return
}
}
*/
if (this._getSubmitFunc) {
let url = this._getSubmitFunc(obj)
if (!url) {
console.log(`getSubmitFunc returns: ${url}`)
return
}
console.log('SUBMIT:', this.cleanObj(obj))
this._submit.disabled = true
this._api.req({
"func": url,
"data": this.cleanObj(obj),
"onError": err => {
this._submit.disabled = false
this.showError(err)
},
"onSuccess": resp => {
if (this._model) {
// Именно перезагружаем, чтобы избежать ошибок, если модель с параметром.
console.log('model reload, form reset (submitFunc)')
this._model.reload()
this.reset()
//} else if (this._models) {
// this.reset()
} else {
if (this._isSubmitDisabledByDefault) {
this._calcSignature()
}
this._setSubmitText(this._btnSubmitText)
this._submit.disabled = this._isSubmitDisabledByDefault
}
// FIX
if (this._userOnSubmit) {
this._userOnSubmit()
}
if (this._onSubmitResult) {
this._onSubmitResult(resp)
}
}
})
} else if (this._model) {
// без ajax
if (this._model.getSelected()) {
//console.log('UPDATE MODEL:', obj)
this._model.updateSelected(obj)
// Если обновление пройдет успешно - получим событие 'select'
// с пустым аргументом и форма сбросится автоматически
} else {
//console.log('ADD TO MODEL:', obj)
let ok = this._model.add(obj)
if (ok) {
//console.log('ADDED.')
console.log('after model add')
this.reset()
}
}
} else if (this._models) {
// без ajax
let modelKey = this._getModel(obj)
let model = this._models[modelKey]
if (!model) {
throw new Error(`not found model for modelKey ${modelKey}`)
}
if (model.getSelected()) {
//console.log('UPDATE MODEL:', obj)
model.updateSelected(obj)
// Если обновление пройдет успешно - получим событие 'select'
// с пустым аргументом и форма сбросится автоматически
} else {
//console.log('ADD TO MODEL:', obj)
let ok = model.add(obj)
if (ok) {
//console.log('ADDED.')
console.log('after model add')
this.reset()
}
}
} else if (this._userOnSubmit) {
// без ajax, без модели
this._userOnSubmit(obj)
}
/*
if (this._model) {
if (this._model.getSelected()) {
if (this._updateFunc) {
this._sendUpdateReq(obj)
} else {
// без ajax
this._model.updateSelected(obj)
// Если обновление пройдет успешно - получим событие 'select'
// с пустым аргументом и форма сбросится автоматически
}
} else {
if (this._submitFunc) {
this._sendSubmitReq(obj)
} else {
// без ajax
let ok = this._model.add(obj)
if (ok) {
this.reset()
}
}
}
} else {
if (this._submitFunc) {
this._sendSubmitReq(obj)
} else if (this._userOnSubmit) {
// без ajax, без модели
this._userOnSubmit(obj)
}
}
*/
}
/*
Formobj.prototype._sendUpdateReq = function(obj) {
this._submit.disabled = true
let x = this.cleanObj(obj)
this._api.req({
func: this._updateFunc,
data: x,
onError: err => this._onAjaxSubmitError(err),
onSuccess: resp => this._onAjaxSubmitSuccess(resp)
})
}
Formobj.prototype._sendSubmitReq = function(obj) {
this._submit.disabled = true
let x = this.cleanObj(obj)
this._api.req({
func: this._submitFunc,
data: x,
onError: err => this._onAjaxSubmitError(err),
onSuccess: resp => this._onAjaxSubmitSuccess(resp)
})
}
Formobj.prototype._onAjaxSubmitError = function(err) {
this._submit.disabled = false
this.showError(err)
}
Formobj.prototype._onAjaxSubmitSuccess = function(resp) {
if (this._model) {
// Именно перезагружаем, чтобы избежать ошибок, если модель с параметром.
this._model.reload()
this.reset()
} else {
if (this._isSubmitDisabledByDefault) {
this._calcSignature()
}
this._setSubmitText(this._btnSubmitText)
this._submit.disabled = this._isSubmitDisabledByDefault
}
}
*/
Formobj.prototype.getCustomElemData = function(custom) {
let obj = {}
for (let prop in custom) {
let a = custom[prop]
//console.log('getCustomElemData:', a)
switch (a.elem.getType()) {
case 'list':
obj[prop] = a.elem.getValues()
break;
case 'select':
obj[prop] = a.elem.getSelected()
break
case 'input':
obj[prop] = a.elem.getValue()
//console.log('X:', prop, obj[prop])
break
}
if (a.chains) {
// fix добавить рекурсивный обход только custom
obj = Object.assign(obj, this.getCustomElemData(a.chains))
}
if (a.tree) {
let cur = a.tree.current
if (cur) {
//console.log('cur:', cur)
obj = Object.assign(obj, this.getData(cur))
}
}
}
return obj
}
Formobj.prototype.getFormData = function() {
let obj = {}
for (let name in this._predefined) {
obj[name] = this._predefined[name]
}
obj = Object.assign(obj, this.getData(this._tree))
if (this._transformData) {
obj = this._transformData(obj)
}
//console.log('getFOrmData:', JSON.stringify(obj))
return obj
}
Formobj.prototype.getData = function(x) {
//console.log('gteData x:', x)
let obj = {}
// SCHEMA FUTURE
if (x.schema) {
let fd = new FormData(this._form)
// SCHEMA
for (let prop in x.schema) {
let dataType = x.schema[prop].dataType
let value = fd.get(prop)
let v
// Обрезаем пробелы
if (typeof value == 'string') {
value = value.trim()
}
switch (dataType) {
case 'int':
v = parseInt(value) || 0
break
case 'float':
v = parseFloat(value) || 0
break
case 'bool':
console.log('bool:', value)
if (value == 'on') {
//if ()
v = true
} else {
v = false
}
break
case 'str':
v = value || ''
break
case 'hex':
try {
v = hexToBytes(value)
} catch (e) {
v = ''
}
break
default:
throw new Error(`Unknown type: ${dataType}`)
}
obj[prop] = v
}
}
// fix как подружить объекты и схемы?
// type, elem, chains, tree
if (x.custom) {
obj = Object.assign(obj, this.getCustomElemData(x.custom))
}
if (x.embed) {
for (let prop in x.embed) {
let embed = x.embed[prop]
if (embed.model) {
obj[prop] = embed.model.getList()
} else if (embed.multiModel) {
for (let modelKey in embed.multiModel) {
obj[modelKey] = embed.multiModel[modelKey].getList()
}
}
}
}
return obj
}
Formobj.prototype._buildTree = function(opt) {
let r = {
//plain: opt.schema,
custom: {},
embed: opt.embed,
// temp
schema: {}
}
if (opt.schema) {
for (let prop in opt.schema) {
let v = opt.schema[prop]
if (typeof v == 'string') {
r.schema[prop] = {
dataType: v,
editmode: Formobj.EDITABLE
}
} else {
let f = {
dataType: v.dataType,
get: v.get,
set: v.set,
}
if (v.editmode) {
switch (v.editmode) {
case Formobj.DISABLED:
f.editmode = Formobj.DISABLED
break
case Formobj.READONLY:
f.editmode = Formobj.READONLY
break
case Formobj.EDITABLE:
f.editmode = Formobj.EDITABLE
break
default:
throw new Error(`Unknown '${prop}' editmode: ${v.editmode}`)
}
} else {
f.editmode = Formobj.EDITABLE
}
r.schema[prop] = f
}
}
}
//console.log('buildTree:', opt)
if (opt.attached) {
for (let prop in opt.attached) {
let c = opt.attached[prop]
r.custom[prop] = this._buildCustom(c, prop)
}
}
//console.log('returnned tree:', opt, r)
return r
}
Formobj.prototype._buildCustom = function(c, prop) {
if (!c.elemConstructor) {
throw new Error(`'elemConstructor' option is required for custom element ${prop}`)
}
let f = {
prop: prop,
setProp: c.setProp,
setKey: c.setKey,
//readonly: c.readonly,
virtual: c.virtual, // вирутальное поле, например RadioList, в form.elements не проверять
elemConstructor: c.elemConstructor,
opts: c.opts,
//func: c.func,
//getFunc: c.getFunc,
key: c.key,
manage: true,
eventHandler: c.eventHandler,
cache: new Map(),
requestModifier: c.requestModifier,
responseModifier: c.responseModifier,
chains: c.chains || {}
}
if (c.editmode) {
switch (c.editmode) {
case Formobj.DISABLED:
f.editmode = Formobj.DISABLED
break
case Formobj.READONLY:
f.editmode = Formobj.READONLY
break
case Formobj.EDITABLE:
f.editmode = Formobj.EDITABLE
break
default:
throw new Error(`Unknown '${prop}' editmode: ${c.editmode}`)
}
} else {
f.editmode = Formobj.EDITABLE
}
if (c.func) {
f.getFunc = function() {
return c.func
}
} else if (c.getFunc) {
f.getFunc = c.getFunc
}
if ((f.setProp && !f.setKey) || (!f.setProp && f.setKey)) {
throw new Error("'setKey' and 'setProp' options must be set or not set both")
}
//console.log('opt/f', opt, f)
if (c.div) {
f.div = getElemSafe('div', c.div)
}
if (c.manage === false) {
f.manage = false
}
// создаем экземпляр кастомного элемента для проверки
//let elem = new c.elem(c.opts)
// для правильного считывания/установки значения
//c.type = elem.type
// tree
let t = c.tree
if (t) {
let branches = new Map()
let x = {
getValue: t.getValue,
container: t.container,
branches: branches,
}
t.cases.forEach(b => {
let branch = {
templateId: b.templateId,
templates: b.templates,
value: b.value,
values: b.values,
}
let tmp = this._buildTree(b)
branch = Object.assign(branch, tmp)
if (b.value) {
branches.set(branch.value, branch)
} else if (Array.isArray(b.values)) {
b.values.forEach(value => {
branches.set(value, branch)
})
} else {
throw new Error(`'value' or 'values' option required in branch settings`)
}
})
f.tree = x
}
for (let prop in c.chains) {
let d = c.chains[prop]
f.chains[prop] = this._buildCustom(d, prop)
}
return f
}
Formobj.prototype.cleanObj = function(obj) {
//console.log('cleanData:', JSON.stringify(obj))
let r = {}
this._cleanData(r, this._tree, obj)
//console.log('after cleanData:', JSON.stringify(r))
if (this._transformData) {
r = this._transformData(r)
}
//console.log('getCleanData return:', r)
return r
}
// r - result object, a - custom object
Formobj.prototype._cleanCustomElem = function(r, a, obj) { //, prop) {
//r.xxxx = 1900000
//console.log('CLEAN ATTACHED:', a)
//console.log('CLEAN ATTACHED obj:', obj)
let valueObj = obj[a.prop]
if (valueObj !== undefined) {
if (a.key) {
// копируем только ID
// Объекта может не быть, если юзер ничего не выбрал (например, в выпадающем списке)
r[a.key] = valueObj[a.key]
// console.log('REPLACED:', a.key, id, 'DELETED', prop)
} else {
//console.log('DELETED', prop)
r[a.prop] = valueObj
}
}
if (a.chains) {
for (let prop in a.chains) {
this._cleanCustomElem(r, a.chains[prop], obj)
//console.log('_CLEAN CUSTOM: AFTER CLEAN ATTACHED:', JSON.stringify(r))
}
}
let tree = a.tree
if (tree) {
var v;
if (tree.getValue) {
v = tree.getValue(valueObj) // obj
} else {
if (a.key) {
v = r[a.key]
} else {
v = valueObj
}
}
let branch = tree.branches.get(v)
//console.log('v:', v, 'branch:', branch)
if (branch) {
if (branch.custom) {
r = this._cleanData(r, branch, obj)
}
}
}
//console.log('CLEAN ATTACHED:', JSON.stringify(r))
}
Formobj.prototype._cleanData = function(r, x, obj) {
/*
SCHEMA FUTURE
if (x.plain) {
for (let prop in x.plain) {
r[prop] = obj[prop]
}
}
*/
if (x.schema) {
for (let prop in x.schema) {
r[prop] = obj[prop]
}
}
if (x.custom) {
for (let prop in x.custom) {
//let a = x.custom[prop]
this._cleanCustomElem(r, x.custom[prop], obj) //, prop)
//console.log('_cleanDATA: AFTER CLEAN ATTACHED:', JSON.stringify(r), a)
}
}
if (x.embed) {
for (let prop in x.embed) {
let embed = x.embed[prop]
console.log('_cleanData embed:', embed)
if (embed.model) {
//console.log('e:', e)
let dirtyList = obj[prop]
//console.log('dirtyList:', dirtyList)
let cleanItems = []
if (Array.isArray(dirtyList)) {
dirtyList.forEach(dirtyItem => {
let cleanItem = embed.form.cleanObj(dirtyItem)
cleanItems.push(cleanItem)
})
}
r[prop] = cleanItems
} else if (embed.multiModel) {
for (let modelKey in embed.multiModel) {
let dirtyList = obj[modelKey]
//console.log('dirtyList:', dirtyList)
let cleanItems = []
if (Array.isArray(dirtyList)) {
dirtyList.forEach(dirtyItem => {
let cleanItem = embed.form.cleanObj(dirtyItem)
cleanItems.push(cleanItem)
})
}
r[modelKey] = cleanItems
}
} else {
//throw new Error(`bug: embed doesn't contain 'model' or 'multiModel' option`)
}
}
}
}
Formobj.prototype._createCustomElemsOnly = function(customElems) {
if (customElems) {
for (let prop in customElems) {
let c = customElems[prop]
let opts = Object.assign({}, c.opts)
// для удобного отображения ошибок
let name = prop
if (c.key) {
name = c.key
}
if (!c.virtual) {
let field = this._form.elements[name]
if (!field) {
throw new Error(`Field '${name}' not found`)
}
opts.field = field
}
//if (c.type != 'select' && c.type != 'list' && item.type != 'input') {
// throw new Error(`Unknown attached elem type ${item.type}. Valid types: select, list, input`)
//}
// создаем экземпляр кастомного элемента
c.elem = new c.elemConstructor(opts)
this._fields[name] = {
type: 'custom',
elem: c.elem
}
if (this._isSubmitDisabledByDefault) {
switch (c.elem.getType()) {
case 'select':
// Select, RadioList
c.elem.on('select', () => this._onAnyChange())
break;
case 'list':
// InputList, CheckList
c.elem.on('changed', () => this._onAnyChange())
break;
}
}
//if (c.func) {
// console.log(this, 'loadData')
//this._loadData(c)
//}
/*
Пофиксить
if (item.key) {
this._attachedElems[item.key] = item.elem
} else {
this._attachedElems[prop] = item.elem
}
*/
// для правильного считывания/установки значения
//c.type = elem.type
if (c.tree || c.chains) {
c.event = c.elem.getActionType()
if (c.event != 'select' && c.event != 'input') {
throw new Error(`Custom element '${prop}' has wrong action type '${c.event}'`)
}
// fix
c.elem.on(c.event, (id) => this._onElemEvent(id, c))
}
// объекты из chains создаем сразу, а заполняем (и, по желанию,
// скрываем/показываем) в ответ на событие
if (c.chains) {
// создаем дальше, chains - тоже map
this._createCustomElemsOnly(c.chains)
}
// объекты из Tree создаются только в ответ на событие
}
}
}
Formobj.prototype._createObjects = function(opt) {
if (opt.schema) {
for (let prop in opt.schema) {
let f = opt.schema[prop]
let elem = this._form.elements[prop]
if (!elem) {
throw new Error(`Field '${prop}' not found`)
}
if (!(elem instanceof Element)) {
//throw new Error(`Form element '${prop}' not found`)
throw new Error(`Field ${prop} must be instanceof Element. Not ${Object.prototype.toString.call(elem)}`)
}
f.elem = elem
f.defaultValue = elem.value
// для удобного отображения ошибок
this._fields[prop] = {
type: 'plain',
elem: elem
}
if (f.dataType == 'bool') {
if (elem.nodeName != 'INPUT') {
throw new Error('bool elem must be checkbox')
}
if (elem.type != 'checkbox') {
throw new Error('bool elem must be checkbox')
}
//elem.value = ''
}
// Ловим события изменения
if (this._isSubmitDisabledByDefault) {
let eventName = 'input' // для input, textarea
// Чтобы избежать багов в разных браузерах - для select, checkbox и
// radio - повесим обработчик на другое событие - 'change'
if (elem.nodeName == 'SELECT') {
eventName = 'change'
} else if (elem.nodeName == 'INPUT') {
if (elem.type == 'radio' || elem.type == 'checkbox') {
eventName = 'change'
}
}
elem.addEventListener(eventName, () => this._onAnyChange())
}
}
}
/*
// SCHEMA FUTURE
opt.plainElems = {}
if (opt.plain) {
for (let prop in opt.plain) {
let elem = this._form.elements[prop]
if (!elem) {
throw new Error(`Field '${prop}' not found`)
}
if (!(elem instanceof Element)) {
//throw new Error(`Form element '${prop}' not found`)
throw new Error(`Field ${prop} must be instanceof Element. Not ${Object.prototype.toString.call(elem)}`)
}
opt.plainElems[prop] = elem
// для удобного отображения ошибок
this._fields[prop] = {
type: 'plain',
elem: elem
}
// Ловим события изменения
if (this._isSubmitDisabledByDefault) {
let eventName = 'input' // для input, textarea
// Чтобы избежать багов в разных браузерах - для select, checkbox и
// radio - повесим обработчик на другое событие - 'change'
if (elem.nodeName == 'SELECT') {
eventName = 'change'
} else if (elem.nodeName == 'INPUT') {
if (elem.type == 'radio' || elem.type == 'checkbox') {
eventName = 'change'
}
}
elem.addEventListener(eventName, () => this._onAnyChange())
}
}
}
*/
if (opt.custom) {
this._createCustomElemsOnly(opt.custom)
}
for (let prop in opt.embed) {
//console.log('listenChanges:', this._listenChanges)
let e = opt.embed[prop]
if (e.modelOpts) {
let model = new ListModel(e.modelOpts)
let formOpts = Object.assign({}, e.formOpts)
formOpts.model = model
formOpts.parent = this
let form = new Formobj(formOpts)
//form.setIsDisabledFunc(this.isDisabled)
model.setIsDisabledFunc(this.isDisabled.bind(this))
//embed[prop] = {
// model: model,
// form: form
//}
e.model = model
e.form = form
// для удобного отображения ошибок
this._fields[prop] = {
type: 'embed',
model: model,
form: form
}
if (this._isSubmitDisabledByDefault) {
model.on('listChanged', () => {
//console.log('listChanged -> on any change')
this._onAnyChange()
})
}
} else if (e.multiModelOpts) {
let models = {}
for (let modelKey in e.multiModelOpts) {
let modelOpts = e.multiModelOpts[modelKey]
let model = new ListModel(modelOpts)
model.setIsDisabledFunc(this.isDisabled.bind(this))
if (this._isSubmitDisabledByDefault) {
model.on('listChanged', () => {
//console.log('listChanged -> on any change')
this._onAnyChange()
})
}
models[modelKey] = model
}
let formOpts = Object.assign({}, e.formOpts)
formOpts.models = models
formOpts.parent = this
let form = new Formobj(formOpts)
//form.setIsDisabledFunc(this.isDisabled)
for (let modelKey in models) {
let model = models[modelKey]
// для удобного отображения ошибок
this._fields[modelKey] = {
type: 'embed',
model: model,
form: form
}
}
e.multiModel = models
e.form = form
} else {
throw new Error(`embed requires 'modelOpts' or 'multiModelOpts'`)
}
}
//this._onAnyChange()
//this._calcSignature()
//console.log('_createObjects:', opt)
// Загружаем все AJAX-элементы топ уровня (которые ни от кого не зависят)
// fix
/*
this._chains.forEach(c => {
if (!c.func) {
return
}
this._loadData(item)
})
*/
}
Formobj.prototype._onElemEvent = function(id, c) {
console.log('elem event:', id, c)
let p;
switch (c.elem.getType()) {
case 'list':
p = c.elem.getValues()
break;
case 'select':
p = c.elem.getSelected()
break
case 'input':
p = c.elem.getValue()
//console.log('X:', prop, obj[prop])
break
}
if (c.eventHandler) {
if (!c.manage && this._settingObj) {
// pass
} else {
c.eventHandler(p)
}
}
if (c.chains) {
for (let prop in c.chains) {
this._clearDepended(c.chains[prop]) // fix elem param?
}
if (id) {
for (let prop in c.chains) {
let d = c.chains[prop] // dependend
//console.log('chain item:', d)
/*
// fix цикл по chains
if (item.doFunc) {
let data;
switch (c.event) {
case 'select':
data = elem.getSelected()
break
case 'input':
data = elem.getValue()
break
}
// что-нибудь типа setList для InputList, либо buildAddrSelect для addr
c.doFunc(data)
//console.log('SETvALUE after doFunc')
this._setValueIfEditMode(c) // fix elem param?
} else {
*/
// Если данные есть в кэше - достаем из кэша
//let m = this._cache[item.prop]
//let list = m.get(id)
let cleanParam = id
if (d.requestModifier) {
cleanParam = d.requestModifier(p)
}
//console.log('d:', d)
//console.log('p:', p)
//console.log('id:', id)
//console.log('cleanParam:', cleanParam)
let list = d.cache.get(cleanParam)
if (list) {
d.elem.setList(list)
this._setValueIfEditMode(d) // fix elem param?
} else {
this._loadData(d, p, cleanParam) // fix elem param?
}
}
//}
//if (c.div) {
// c.div.hidden = false
//}
}
}
let t = c.tree
if (t) {
let cleanId = id
if (t.getValue) {
cleanId = t.getValue(p)
}
let branch = t.branches.get(cleanId)
//console.log(id)
console.log('branch:', branch)
console.log(t)
//console.log('container:', t.container)
let container = document.getElementById(t.container)
if (!container) {
throw new Error(`container '${t.container}' not found ('${t.prop}' tree)`)
}
if (!branch) {
if (container) {
container.innerHTML = ''
}
if (t.containers) {
t.containers.forEach(containerId => {
document.getElementById(containerId).innerHTML = ''
})
t.containers = null
}
t.current = undefined
// fix удалить elems в текущем и дочерних элементах
//console.log('t:', t)
} else {
if (t.containers) {
t.containers.forEach(containerId => {
document.getElementById(containerId).innerHTML = ''
})
t.containers = null
}
// Шаблон необязателен. Пример: один объект состоит только из полей по умолчанию
// Второй объект требует дополнительных полей (например для COM-порта нужны
// наройки интерфейса RS485)
if (branch.templateId) {
copyTemplateInto(branch.templateId, container)
this._createObjects(branch)
} else if (branch.templates) {
t.containers = []
for (let container in branch.templates) {
let templateId = branch.templates[container]
if (templateId != '') {
copyTemplateInto(templateId, container)
t.containers.push(container) // id
} else {
document.getElementById(container).innerHTML = ''
}
}
this._createObjects(branch)
}
t.current = branch
for (let prop in branch.custom) {
let c = branch.custom[prop]
if (c.getFunc) {
let cleanParam = id
if (c.requestModifier) {
cleanParam = c.requestModifier(p)
}
//console.log('id:', id, 'p:', p)
this._loadData(c, p, cleanParam) // fix
}
}
}
}
if (c.div) {
if (id) {
c.div.hidden = false
} else {
c.div.hidden = true
}
}
}
Formobj.prototype._clearDepended = function(c) {
c.elem.clear()
if (c.div) {
c.div.hidden = true
}
// FIX if item.after ?
if (c.divLoadError) {
c.divLoadError.remove()
}
if (c.chains) {
for (let prop in c.chains) {
this._clearDepended(c.chains[prop])
}
}
}
// hex fields
function base64Decode(text){
if (!text) {
return []
}
text = text.replace(/\s/g,"");
if(!(/^[a-z0-9\+\/\s]+\={0,2}$/i.test(text)) || text.length % 4 > 0){
throw new Error("Not a base64-encoded string.");
}
//local variables
var digits = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",
cur, prev, digitNum,
i=0,
result = [];
text = text.replace(/=/g, "");
while(i < text.length){
cur = digits.indexOf(text.charAt(i));
digitNum = i % 4;
switch(digitNum){
//case 0: first digit - do nothing, not enough info to work with
case 1: //second digit
result.push(prev << 2 | cur >> 4);
break;
case 2: //third digit
result.push((prev & 0x0f) << 4 | cur >> 2);
break;
case 3: //fourth digit
result.push((prev & 3) << 6 | cur);
break;
}
prev = cur;
i++;
}
return result;
}
function base64ToHex(base64Str) {
let buf = base64Decode(base64Str)
let hexes = []
buf.forEach(b => {
let hex = b.toString(16)
if (hex.length == 1) {
hex = '0' + hex
}
hexes.push(hex)
})
return hexes.join(' ')
}
// принимает строку с опциональными пробелами между байтами
function hexToBytes(str) {
let bytes = []
let arr = str.split(' ')
arr.forEach(elem => {
if (elem == '') {
return
}
if (elem.length % 2) {
throw new Error("Wrong hex string")
}
// секции вида: 0c, ddff
//console.log('elem:', elem)
for (let i=0; i<elem.length; i+=2) {
let hex = elem.substr(i, 2)
//console.log('i:', i, 'hex:', hex)
let num = parseInt(hex, 16)
if (isNaN(num)) {
throw new Error("Wrong hex string")
}
bytes.push(num)
}
})
return bytes
}