/*
Начальный расчет signature делаем после загрузки главной формы.
Как узнать когда загрузилась главная форма?
- Заводим для формы счетчик loadingCount,
увеличиваем его при создании элемента с функцией loadFunc, уменьшаем при заврешении
http-запроса loadFunc (с ошибкой или без). Уменьшив счетчик проверяем - если 0 - форма
загрузилась.
Таким же образом рассчитываем начальный signature после restore / edit.
Как правильно отобразить ошибку, если юзер удалил ошибочные элементы после submit формы
на сервер но до получения ответа от сервера?
- Перед отправкой на сервер clean объекта, нужно сохранить dirty объект.
Если сервер вернул ошибку - делаем restore(dirty) и затем showError(err)
Все элементы формы генерируют события когда юзер что-то набирает в текстовом поле либо
выбирает в списке. Обработчик события извлекает 2 значения: clean и dirty. Затем
обновляет цепочку объектов до топ-уровня (обновляя _clean и _dirty объекты).
Затем (!) если есть зависимые элементы (fork, chains) - генерирует последующие события.
_clean - это объект, который будет отправлен на сервер, а также выступает в роли
текущей signature.
_dirty - это объект который нужен для восстановления (restore) состояния формы.
Записывается в sessionStorage / localStorage для восстановления формы после перезагрузки
страницы. Снимок состояния в момент submit формы на сервер. Если сервер вернет ошибку -
можно восстановить форму и корректно показать ошибку несмотря на изменения, внесенные
юзером во время выполнения http-запроса.
Методы:
fill - заполняет поля формы будто это делает человек, не меняя режим filling. Зачем нужен этот метод?
// Представим ситуацию: есть кликабельный список модемов, у каждого модема есть список приборов, у каждого прибора есть список метрик. На странице модема
edit - заполняет поля формы, устанавливает режим в editing, чтобы можно было отключить (disabled=true) нередактируемые поля формы, а также рассчитывает initSignature после заполнения всех полей.
restore - восстанавливает состояние формы из снимка (snapshot)
Snapshot
Снимок формы хранит - dirty объект, initSignature
*/
/*
Разделение на getObj и cleanObj.
getObj - возвращает объект в том виде, в котором можно вызвать setObj, то есть device вместо
deviceID, queries вместо queryIDs.
Если имеем многоуровневую форму и после setObj вызываем getObj, единственный способ получить
"чистый" объект для отправки на сервер - это сделать clean операцию на уже готовом объекте.
Список объектов для Formobj.
Одноуровневый список. Описываем в schema, создаем под капотом.
Топ-уровень - это модель, а вложенные списки объектов - это ObjectList?
Операции:
- add, с проверкой fingerprint
- replaceSelected
- removeChecked
- removeSelected
События:
- select (если параметр undefined - это unselect)
*/
/*
Модель (многоуровневая) для отображения данных из БД (или сервера).
У каждого объекта должен быть первичный ключ
- updateSelected, когда сервер присылает только некоторые поля объекта, например изменившийся статус
- replaceSelected
///////////////// setObj
заводим state object, который хранит obj и количество загружаемых элементов.
В конце setObj проверяем счетчик - если > 0 блокируем форму.
После загрузки успешной или нет - счетчик уменьшаем. Как только стал в 0 - разблокируем форму.
3 кейса для вложенных форм:
1. раскрывается под списком
2. в отдельном диве (например справа). Нужно передать ID блока
3. кастомная реализация. Нужно объявить 2 метода - showForm и hideForm
Разделить createForm для главной и вложенных. Другие кнопки, другие обработчики
DISABLED в createSchemaObjects
Для вложенных форм isEditable вместо editmode
*/
const filling = 0
const restoring = 1
const editing = 2
var _labelClass = 'omg-form-label'
function Form(opt) {
//this.formId = opt.form
this._mode = filling
this._loadingCount = 0
this._version = 0
this._enableSubmit = opt.enableSubmit
this._api = opt.api
this._errorClass = opt.errorClass || 'errmsg'
this._key = opt.key
this._context = opt.context
this._btnSubmitText = opt.btnSubmitText || 'Добавить'
this._btnUpdateText = opt.btnUpdateText || 'Обновить'
this._btnRemoveText = opt.btnRemoveText || 'Удалить'
//this._onSubmit = opt.onSubmit
//this._onSubmitSuccess = opt.onSubmitSuccess
//this._onUpdateSuccess = opt.onUpdateSuccess
//this._onRemoveSuccess = opt.onRemoveSuccess
//this._submitFunc = opt.submitFunc
//this._updateFunc = opt.updateFunc || this._submitFunc
//this._removeFunc = opt.removeFunc
this._validate = opt.validate
//this._form = document.getElementById(opt.form)
//if (!this._form) {
// throw new Error(`html form '${opt.form}' not found`)
//}
//this._schema = {}
//this._lists = {}
this.form
// temp
this._fields = {}
if (!opt.formContainer) {
throw new Error(`formContainer option is required`)
}
if (opt.formContainer instanceof Element) {
this._formContainer = opt.formContainer
} else {
let formContainer = document.getElementById(opt.formContainer)
if (!formContainer) {
throw new Error(`form container '${opt.formContainer}' not found`)
}
this._formContainer = formContainer
}
let form = {
isEditable: opt.isEditable || false,
schema: this._schema,
fields: {}, // для ошибок,
//loadingCount: 0
selected: {},
dirty: {}
}
//this._schema = this._buildTree(opt.schema, opt.form)
this._schema = this._buildTree(opt.schema, form)
this.form = form
this.form.schema = this._schema
//console.log('SCHEMA:', this._schema)
//this._createSchemaObjects(this._schema)
// fix - return ?
//this._createMainForm(this._schema, formContainer, opt.btnSubmitText)
// add submit btn to top form
//console.log(this._schema)
//this._loadDataForCustomElems(this._schema)
/*
let str = localStorage.getItem('x')
if (str) {
let s
try {
s = JSON.parse(str)
} catch (e) {
console.log(e)
console.log(str)
}
console.log('localStore:', s.obj)
this._restore(s)
}
*/
//console.log(this)
//this.init()
}
Form.prototype = Object.create(Emiter.prototype)
Form.prototype.constructor = Form
Form.EDITABLE = 1 // стандартное поле
Form.UNEDITABLE = 2 // активно при добавлении, отключено при редактировании
Form.READONLY = 3 // всегда отключено, например ID, передается на сервер
/*
Form.E = 1 // editable
Form.R = 2 // readonly
Form.D = 3 // disabled
Form.S = 4 // standard
Form.C = 5 // custom
Form.L = 6 // list
*/
Form.prototype.init = function() {
let loader = new Loader({
api: this._api,
t: this.form, // пустой dirty
context: this._context,
onError: err => {
//console.log('Loader.err: ', err)
},
onLoaded:() => {
this._createMainForm()
}
})
loader.load()
}
Form.prototype.onRemove = function(func) {
this._onRemove = func
}
Form.prototype.onSubmit = function(func) {
this._onSubmit = func
}
// ВАЖНО!
// Вложенные коллекции (в chains или tree) рекурсивно искать сложно, поэтому
// в момент создания объектов нужно сохранить на них ссылки, а в момент reset - удалить.
Form.prototype.showError = function(err) {
this._showError(this.form.fields, err)
}
/////////////// FIX
// сделать map из форм
Form.prototype._showError = function(fields, err) {
if (this._error) {
this._error.remove()
}
let path = []
if (Array.isArray(err.path)) {
path = err.path
}
while (true) {
let step = path.shift()
if (!step) {
break
}
//if (!schema) {
// throw new Error(`wrong path step ${JSON.stringify(step)}, not found`)
//}
let p = fields[step.field]
/*
schema.forEach(x => {
if (x.name == step.field) {
p = x
}
})
*/
if (!p || p.type != 'list') {
throw new Error(`wrong path step ${JSON.stringify(step)}`)
}
if (p.arr.length <= step.idx) {
throw new Error(`wrong path step ${JSON.stringify(step)}, idx out of range`)
}
if (!p.isFormOpened) {
this._select(p, p.arr[step.idx])
}
fields = p.fields
}
let anchor; //, found = false
if (err.field) {
//schema.forEach(p => {
// if (p.name == err.field) {
let p = fields[err.field]
//console.log('ERR FIELD:', p)
if (p) {
switch (p.type) {
case '':
// для скрытых элементов anchor не определяем
break
case 'line':
case 'text':
case 'select':
anchor = p.elem
break
case 'checkbox':
anchor = p.labelElem
break
case 'checkboxes':
case 'radios':
case 'model':
anchor = p.cover
break
case 'list':
anchor = p.btnDiv
break
}
} else {
throw new Error(`field '${err.field}' not found`)
}
} else {
// FIX
//let p = schema[err.list]
//anchor = p.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._btnSubmit,
err: err,
class: this._errorClass
})
}
}
Form.prototype._buildTree = function(schemaConfig, form) {
let schema = []
if (Array.isArray(schemaConfig)) {
schemaConfig.forEach(v => {
let f;
if (v.type == 'list') {
f = this._buildList(v, form)
} else {
f = this._buildField(v, form)
}
schema.push(f)
})
}
//console.log('returnned tree:', opt, r)
return schema
}
// Цель build функций - пройтись ко конфигурации схемы на всю глубину, провалидировать
// описание каждого свойства формы, создать внутренние структуры, описывающие свойства
// формы.
// Никаких манипуляций с DOM данные функции не производят. Для fork случаев создание
// schema элементов с нуля после действий пользователя.
//Form.prototype._buildTree = function(schemaConfig, formId) {
Form.prototype._buildField = function(v, form) {
let type = v.type
if (!type) {
type = ''
}
let f = {
type: type,
label: v.label,
comment: v.comment,
placeholder: v.placeholder,
dataType: v.dataType || 'str',
name: v.name,
key: v.key,
scalar: v.scalar,
get: v.get,
set: v.set,
data: v.data,
getData: v.getData,
getName: v.getName, // fix
loadFunc: v.loadFunc,
loadFuncContextParam: v.loadFuncContextParam,
chains: [],
virtual: v.virtual, // вирутальное поле, например RadioList, в form.elements не проверять
manage: true,
onChange: v.onChange,
cache: new Map(),
getLoadFuncArg: v.getLoadFuncArg,
//requestModifier: v.requestModifier,
responseModifier: v.responseModifier,
form: form,
modelOptions: v.modelOptions,
modelType: v.modelType,
modelChangeType: v.modelChangeType,
modelSelectType: v.modelSelectType,
contextVar: v.contextVar,
width: v.width,
//height: v.height,
rows: v.rows,
cols: v.cols,
}
if (v.noCache === true) {
f.caching = false
} else {
f.caching = true
}
// FIX
/*
if (v.key) {
f.key = v.key
} else {
f.scalar = true
}
*/
//console.log('build:', f)
if (v.getName) {
if (typeof v.getName == 'string') {
f.getName = obj => {
return obj[v.getName]
}
} else {
f.getName = v.getName
}
} else {
f.getName = obj => obj
}
/*
if (v.container) {
f.containerId = v.container
} else {
f.containerId = formContainerId
}
*/
if (v.editmode) {
// Если указан editmode - сперва проверяем значение на валидность
if (v.editmode != Form.UNEDITABLE && v.editmode != Form.EDITABLE && v.editmode != Form.READONLY) {
throw new Error(`Unknown '${v.name}' editmode: ${v.editmode}`)
}
// Если поле readonly - неважно какая форма - поле всегда отключено.
if (v.editmode == Form.READONLY) {
f.editmode = Form.READONLY
} else {
// Если форма редактируемая - поле может быть 2 видов
if (form.isEditable) {
f.editmode = v.editmode // editable / uneditable
} else {
// Если форма нередактируемая - поле только uneditable
f.editmode = Form.UNEDITABLE
}
}
} else {
if (form.isEditable) {
f.editmode = Form.EDITABLE
} else {
f.editmode = Form.UNEDITABLE
}
}
switch (type) {
case '':
case 'checkboxes':
case 'radios':
case 'select':
case 'line':
case 'text':
case 'checkbox':
//case 'custom-list':
case 'model':
break
default:
throw new Error(`unknown type in '${v.name}' schema property`)
}
if (Array.isArray(v.chains)) {
v.chains.forEach(x => {
//console.log('CHAINS: =====>', JSON.stringify(x))
let field = this._buildField(x, form)
f.chains.push(field)
//let y = this._buildField(x)
//console.log()
})
}
if (v.fork) {
f.fork = this._buildFork(v.fork, form)
}
return f
}
// c - config item, k - property name (key)
// form - родительская форма - нужня в первую очередь для проверки isEditable
Form.prototype._buildList = function(c, form) {
//if (!c.btnSubmit) {
// throw new Error(`list elem '${c.name}' must have 'btnSubmit' option`)
//}
let f = {
type: 'list',
label: c.label,
name: c.name,
isEditable: c.isEditable || false,
//containerId: c.container,
//btnSubmitId: c.btnSubmit,
//btnHideId: c.btnHide,
//formOpts: c.form,
//templateOpts: c.template,
//view: c.view,
activeClass: c.activeClass || 'selected',
//sort: c.sort,
validate: c.validate,
//parentFormId: formId,
//formId: c.form,
showForm: c.showForm,
hideForm: c.hideForm,
formContainerClass: c.formContainerClass,
//btnAddText: c.btnAddText,
//btnUpdateText: c.btnUpdateText,
//formContainer: document.getElementById(c.formContainer),
emptyListMsg: c.emptyListMsg || '',
form: form,
fields: {},
loadingCount: 0
}
// Если родительская форма нередактируемая, то все вложенные тоже
if (!form.isEditable) {
f.isEditable = false
}
if (!c.getItemName) {
throw new Error(`getItemName option is required`)
}
if (typeof c.getItemName == 'string') {
f.getItemName = (obj) => {
return obj[c.getItemName]
}
} else {
f.getItemName = c.getItemName
}
// для всех schema элементов и ниже
// FIX
//if (!c.formContainer) {
// throw new Error(`formContainer option is required for list ${JSON.stringify(c)}`)
//}
f.formContainerId = c.formContainer
f.formContainer = document.getElementById(c.formContainer)
// Для массива
//if (!c.itemContainer) {
// throw new Error(`itemContainer option is required for list ${JSON.stringify(c)}`)
//}
//f.itemContainerId = c.itemContainer
// для самого поля в родительской форме
//if (c.container) {
// f.containerId = c.container
//} else {
// f.containerId = parentFormContainerId
//}
/*
if (c.editmode) {
f.editmode = c.editmode
if (f.editmode != Form.UNEDITABLE && f.editmode != Form.EDITABLE && f.editmode != Form.READONLY) {
throw new Error(`Unknown '${c.name}' editmode: ${c.editmode}`)
}
} else {
f.editmode = Form.EDITABLE
}
*/
/*
if (c.showForm) {
f.showForm = () => {
if (f.selected) {
console.log('select')
this._select(f) // снимаем выделение
}
c.showForm()
f.
}
}
*/
f.schema = this._buildTree(c.schema, f)
return f
}
Form.prototype._getObjectValue = function(p) {
let clean, dirty
switch (p.type) {
case 'checkboxes':
clean = [] // массив ID
dirty = [] // массив объектов
let key = p.name
if (p.key) {
key = p.key
}
p.elems.forEach((x, idx) => {
if (x.checked) {
let v = p.valueObjects[idx]
dirty.push(v)
if (p.scalar) {
clean.push(v)
} else {
clean.push(v[key])
}
}
})
//upd[p.name] = checkedIDs
break
case 'radios':
if (Array.isArray(p.elems)) {
for (let i=0; i
0) {
dirty = p.valueObjects[p.elem.selectedIndex - 1]
}
if (dirty !== undefined) {
if (p.scalar) {
clean = dirty
} else {
clean = dirty[p.name]
}
}
break
case 'checkbox':
dirty = p.elem.checked
// fix - объекты вместо скаляра?
//upd[p.name] = dirty
clean = dirty
break
case '':
//if (p.contextVar) {
// dirty = this._context[p.contextVar]
//clean = this._context[p.contextVar]
//} else {
dirty = p.value
clean = p.value
//}
break
case 'line':
case 'text':
dirty = p.elem.value
// Обрезаем пробелы
if (typeof dirty == 'string') {
dirty = dirty.trim()
}
let v;
// Важно для расчета signature. Ибо до того, как напечатали в поле ввода символ
// - в signature нет такого property, а когда символ напечатали и удалили, - в
// signature появится property c пустой строкой или нулем
if (dirty == '') {
v = undefined
} else {
switch (p.dataType) {
case 'int':
v = parseInt(dirty) || 0
break
case 'float':
v = parseFloat(dirty) || 0
break
case 'str':
v = dirty || ''
break
case 'hex':
try {
v = hexToBytes(dirty)
} catch (e) {
v = ''
}
break
default:
throw new Error(`Unknown type: ${p.dataType}`)
}
if (p.get) {
v = p.get(v)
}
}
dirty = v
clean = v
break
//case 'custom-list':
//return p.custom.getValues()
case 'model':
if (p.modelChangeType == 'radio') {
dirty = p.model.getSelected(p.modelSelectType)
} else {
dirty = p.model.listChecked(p.modelSelectType)
}
//console.log('DIRTY model object:', dirty)
if (dirty !== undefined) {
if (p.scalar) {
clean = dirty
} else {
clean = dirty[p.name]
}
}
break
default:
throw new Error(`bug: unknown property type '${p.type}'`)
}
return {
dirty: dirty,
clean: clean
}
}
Form.prototype._disableField = function(p) {
//console.log('disablefield')
if (p.type == 'list') {
p.arr.forEach(item => {
item.checkbox.disabled = true
})
p.btnShow.hidden = true
} else if (p.type == 'model') {
p.model.disable()
} else if (p.elem) {
p.elem.disabled = true
} else if (p.elems) {
p.elems.forEach(elem => {
elem.disabled = true
})
}
}
Form.prototype._createField = function(p, container) {
//if (p.type == '') {
// return
//}
//console.log('_createField...', p.name, p.type)
let control
let contextValue
let labelText = p.name
if (p.label) {
labelText = p.label
}
//console.log('_createField: data=', p.data)
let div = document.createElement('div')
div.setAttribute('class', 'form-group')
p.cover = div
let label = document.createElement('label')
label.textContent = labelText
label.classList.add(_labelClass)
switch (p.type) {
case '':
//p.cover = document.createElement('div')
container.appendChild(p.cover)
// FIX - генерация события?
if (p.contextVar) {
contextValue = this._context[p.contextVar]
if (contextValue !== undefined) {
p.value = contextValue
this.form.selected[p.name] = contextValue
} else {
delete this.form.selected[p.name]
}
}
break
case 'checkboxes':
div.style.display = 'flex'
div.style.flexDirection = 'column'
div.appendChild(label)
p.optionContainer = div
p.elems = []
// FIX - генерация события?
if (p.contextVar) {
contextValue = this._context[p.contextVar]
this._fillOptionsField(p, contextValue)
} else if (p.data) {
this._fillOptionsField(p, p.data)
} else if (p.getData) {
this._fillOptionsField(p, p.getData())
} else if (p.caching) {
let cached = p.cache.get()
if (cached) {
this._fillOptionsField(p, cached)
}
}
container.appendChild(div)
break
case 'radios':
div.style.display = 'flex'
div.style.flexDirection = 'column'
div.appendChild(label)
p.optionContainer = div
p.elems = []
// FIX - генерация события?
if (p.contextVar) {
contextValue = this._context[p.contextVar]
this._fillOptionsField(p, contextValue)
} else if (p.data) {
this._fillOptionsField(p, p.data)
} else if (p.getData) {
this._fillOptionsField(p, p.getData())
} else if (p.caching) {
let cached = p.cache.get()
if (cached) {
this._fillOptionsField(p, cached)
}
}
//console.log('RADIOs CREATED', p.elems)
container.appendChild(div)
break
case 'select':
control = document.createElement('select')
control.setAttribute('class', 'form-control')
if (p.width) {
control.style.width = p.width
}
if (p.className) {
control.classList.add(p.className)
}
p.elem = control
div.appendChild(label)
div.appendChild(control)
if (p.comment) {
// FIX от 15 секунд и выше
let small = document.createElement('small')
small.classList.add('form-text', 'text-muted')
small.textContent = p.comment
div.appendChild(small)
}
// FIX - генерация события?
if (p.contextVar) {
contextValue = this._context[p.contextVar]
this._fillOptionsField(p, contextValue)
} else if (p.data) {
this._fillOptionsField(p, p.data)
} else if (p.getData) {
this._fillOptionsField(p, p.getData())
} else if (p.caching) {
let cached;
//console.log(p.cache, p.loadFuncContextParam)
if (p.getLoadFuncArg) {
let arg = p.getLoadFuncArg(p.form.selected)
cached = p.cache.get(arg)
} else {
cached = p.cache.get()
}
//console.log(cached)
if (cached) {
this._fillOptionsField(p, cached)
}
}
p.elem.addEventListener('change', () => {
this._onElemEvent(p)
})
container.appendChild(div)
break
case 'line':
control = document.createElement('input')
control.setAttribute('type', 'text')
control.setAttribute('class', 'form-control')
if (p.width) {
control.style.width = p.width
}
if (p.className) {
control.classList.add(p.className)
}
if (p.placeholder) {
control.setAttribute('placeholder', p.placeholder)
}
control.addEventListener('input', () => {
this._onElemEvent(p)
})
div.appendChild(label)
div.appendChild(control)
if (p.comment) {
// FIX от 15 секунд и выше
let small = document.createElement('small')
small.classList.add('form-text', 'text-muted')
small.textContent = p.comment
div.appendChild(small)
}
container.appendChild(div)
p.elem = control
//p.defaultValue = control.value
// FIX - генерация события?
if (p.contextVar) {
contextValue = this._context[p.contextVar]
p.elem.value = contextValue
}
break
case 'text':
control = document.createElement('textarea')
control.setAttribute('class', 'form-control')
if (p.rows) {
control.rows = p.rows
}
if (p.cols) {
control.cols = p.cols
}
if (p.width) {
control.style.width = p.width
}
if (p.className) {
control.classList.add(p.className)
}
control.addEventListener('input', () => {
this._onElemEvent(p)
})
div.appendChild(label)
div.appendChild(control)
if (p.comment) {
// FIX от 15 секунд и выше
let small = document.createElement('small')
small.classList.add('form-text', 'text-muted')
small.textContent = p.comment
div.appendChild(small)
}
container.appendChild(div)
p.elem = control
//p.defaultValue = control.value
// FIX - генерация события?
if (p.contextVar) {
contextValue = this._context[p.contextVar]
p.elem.value = contextValue
}
break
case 'checkbox':
control = document.createElement('input')
control.setAttribute('type', 'checkbox')
//console.log('CHECKBOX:', control, labelText)
//label.classList.remove(_labelClass)
label.innerHTML = ''
label.appendChild(control)
label.appendChild(document.createTextNode(` ${labelText}`))
control.addEventListener('change', () => {
this._onElemEvent(p)
})
div.appendChild(label)
container.appendChild(div)
p.elem = control
p.labelElem = label
// FIX - генерация события?
if (p.contextVar) {
contextValue = this._context[p.contextVar]
p.elem.value = contextValue
}
break
//case 'custom-list':
// p.custom.create()
//break
case 'model':
//label.style.borderBottom = "1px solid #ddd"
//label.style.paddingBottom = "5px"
//p.cover.appendChild(label)
p.modelOptions.forEach(type => {
type.container = p.cover
})
p.model = new Model(p.modelOptions)
// FIX - генерация события?
if (p.contextVar) {
contextValue = this._context[p.contextVar]
this._fillOptionsField(p, contextValue)
} else if (p.data) {
this._fillOptionsField(p, p.data)
} else if (p.getData) {
this._fillOptionsField(p, p.getData())
} else if (p.caching) {
let cached;
//console.log(p.cache, p.loadFuncContextParam)
if (p.getLoadFuncArg) {
let arg = p.getLoadFuncArg(p.form.selected)
cached = p.cache.get(arg)
} else {
cached = p.cache.get()
}
//console.log(cached)
if (cached) {
this._fillOptionsField(p, cached)
}
}
//if (p.changeType == 'few') {
p.model.onChange = () => {
this._onElemEvent(p)
}
//}
//p.model
container.appendChild(p.cover)
break
}
// Отключаем readonly поля при генерации формы (генерация главной формы или
// клик по ссылке "добавить")
if (p.editmode == Form.READONLY && !p.loadFunc) {
this._disableField(p)
}
p.form.fields[p.name] = p
}
// fix - if initedor not inited
Form.prototype._removeField = function(p) {
//console.log('_removeField:', p.name)
let upd = {}
upd = Object.assign(upd, this._removeDependentFields(p))
if (p.type == 'list') {
this._removeForm(p)
// Удаляем элементы связанные со списком items
p.btnShow = undefined
p.btnDiv = undefined
p.btnRemove = undefined
p.container = undefined
p.arr = []
p.cover.remove()
} else {
if (p.optionContainer) {
p.optionContainer = undefined
}
// checkbox
if (p.labelElem) {
p.labelElem = undefined
}
//p.custom.reset()
if (p.model) {
p.model = undefined
}
p.elem = undefined
if (Array.isArray(p.elems)) {
p.elems = []
}
if (p.cover) {
p.cover.remove()
}
if (p.selected) {
p.selected = undefined
}
// Срос загружающихся в данный момент
if (p.loading) {
p.loading = false
p.paramSignature = undefined
p.form.loadingCount--
this._loadingCount--
}
}
delete p.form.fields[p.name]
upd[p.name] = undefined
return upd
}
Form.prototype._buildFork = function(t, form) {
if (!t) {
return
}
let roads = new Map()
//console.log('build FORK:', t)
t.roads.forEach(b => {
let road = {
//templateId: b.templateId,
//templates: b.templates,
value: b.value,
values: b.values,
init: b.init,
cleanup: b.cleanup,
//schema: b.schema,
}
road.schema = this._buildTree(b.schema, form)
//branch = Object.assign(branch, tmp)
if (b.value !== undefined) {
roads.set(b.value, road)
} else if (Array.isArray(b.values)) {
b.values.forEach(value => {
roads.set(value, road)
})
} else {
throw new Error(`'value' or 'values' option required in road settings`)
}
})
return {
getValue: t.getValue,
roads: roads,
//container: t.container,
}
}
/*
Form.prototype.getObj = function(schema) {
let x = this._getObj(schema)
//console.log('getObj:', x)
return x
}
*/
Form.prototype._createMainForm = function() {
//console.log('create main form')
let schema = this._schema
let formContainer = this._formContainer
formContainer.innerHTML = ''
let divFields = document.createElement('div')
divFields.setAttribute('class', 'form-group')
let divButtons = document.createElement('div')
divButtons.style.marginTop = '1.5rem'
//divButtons.setAttribute('class', 'buttons-group')
//divButtons.style.display = 'flex'
//divButtons.style.justifyContent = 'space-between'
let btnSubmit = document.createElement('button')
btnSubmit.setAttribute('class', 'btn btn-outline-success')
btnSubmit.textContent = this._btnSubmitText
//btnSubmit.disabled = true fix
this._btnSubmit = btnSubmit
this._btnSubmit.addEventListener('click', () => {
if (this._error) {
this._error.remove()
}
//lett dirty = this.getObj(this._schema)
//let cleanData = this.cleanObj(dirty)
//console.log('CLEAN FORM DATA:', this.form.selected)
if (this._validate) {
let err = this._validate(this.form.selected)
if (err) {
this.showError(err)
}
}
if (this._onSubmit) {
//this._onSubmit(this._clean)
this._onSubmit(JSON.parse(JSON.stringify(this.form.selected)))
}
/*
if (this._api) {
let func, successFunc
switch (this._mode) {
case filling:
func = this._submitFunc
successFunc = this._onSubmitSuccess
//console.log('filling: func:', func)
break
case editing:
func = this._updateFunc
successFunc = this._onUpdateSuccess
//console.log('editing: func:', func)
break
}
if (func) {
let snapshotStr = JSON.stringify({
version: this._version,
clean: this.form.selected,
signature: this._signature,
mode: this._mode,
})
this._api.req({
func: func,
//data: this._clean,
data: this.form.selected,
onError: err => {
this.restore(snapshotStr)
this.showError(err)
},
onSuccess: resp => {
//this.reset()
if (successFunc) {
successFunc(resp)
}
}
})
}
}
*/
})
divButtons.appendChild(btnSubmit)
let btnRemove = document.createElement('button')
// ml-5 отступ
btnRemove.setAttribute('class', 'btn btn-outline-danger ml-5')
btnRemove.textContent = this._btnRemoveText
btnRemove.hidden = true
this._btnRemove = btnRemove
btnRemove.addEventListener('click', () => {
if (this._onRemove) {
this._onRemove()
}
})
divButtons.appendChild(btnRemove)
this._createSchemaObjects(schema, {append: divFields})
formContainer.appendChild(divFields)
formContainer.appendChild(divButtons)
//console.log('MAIN FORM CREATED!')
this._signature = JSON.stringify(this.form.selected)
//this._initSignature = this._signature
this._initSnapshot = JSON.stringify({
version: this._version,
clean: this.form.selected,
signature: this._signature,
mode: this._mode,
})
if (!this._enableSubmit) {
this._btnSubmit.disabled = true
}
//console.log("INIT SIGNATURE:", this._signature)
}
Form.prototype._onFormDataChanged = function() {
this._version++
// рекурсивно обновляем родительскую коллекцию
// Считаем подпись
//let y = this.getObj(this._schema)
let newSignature = JSON.stringify(this.form.selected)
/////console.log('SIGNATURE:', newSignature)
if (this._signature != newSignature) {
this._btnSubmit.disabled = false
} else {
if (!this._enableSubmit) {
this._btnSubmit.disabled = true
}
}
//let snapshot =
/*
localStorage.setItem('x', JSON.stringify({
signature: this._signature,
obj: y,
btnSubmit: this._btnSubmit.disabled,
mode: this._mode
}))
*/
}
/*
*/
Form.prototype._createForm = function(schema, formContainer) {
//console.log('CREATE:', formContainer)
formContainer.innerHTML = ''
/*
let divFields = document.createElement('div')
divFields.setAttribute('class', 'form-group')
let divButtons = document.createElement('div')
divButtons.setAttribute('class', 'buttons-group')
let btnSubmit = document.createElement('button')
btnSubmit.setAttribute('class', 'btn')
btnSubmit.textContent = btnAddText
let btnClose = document.createElement('button')
btnClose.setAttribute('class', 'btn btn-link')
btnClose.textContent = 'close'
divButtons.appendChild(btnSubmit)
divButtons.appendChild(btnClose)
*/
this._createSchemaObjects(schema, {append: formContainer})
//formContainer.appendChild(divFields)
//formContainer.appendChild(divButtons)
return {
fieldsContainer: formContainer,
//btnSubmit: btnSubmit,
//btnClose: btnClose
}
}
//Form.prototype._createSchemaObjects = function(formId, schema, formContainer) {
Form.prototype._createSchemaObjects = function(schema, anchors) {
//console.log('_createSchemaObjects...')
let container;
if (anchors.append) {
container = anchors.append
} else {
container = document.createDocumentFragment()
}
//console.log('create objects:', formId) //, schema)
schema.forEach(p => {
//let container = document.getElementById(p.containerId)
//if (!container) {
// throw new Error(`container '${p.containerId}' not found`)
//}
if (p.type == 'list') {
this._createList(p, container)
} else {
this._createField(p, container)
}
})
if (anchors.after) {
anchors.after.parentNode.insertBefore(container, anchors.after.nextSibling)
}
//this._onAnyChange()
//this._calcSignature()
//console.log('_createSchemaObjects:', opt)
// Загружаем все AJAX-элементы топ уровня (которые ни от кого не зависят)
// fix
/*
this._chains.forEach(c => {
if (!c.func) {
return
}
this._loadData(item)
})
*/
}
// Заполняем поле вариантами на выбор
Form.prototype._fillOptionsField = function(p, list) {
switch (p.type) {
case 'select':
p.elem.innerHTML = ''
//console.log('innerHTML')
// пустой option
p.elem.appendChild(document.createElement('option'))
//console.log('append empty option')
if (Array.isArray(list)) {
list.forEach(x => {
let value
if (p.scalar) {
value = x
} else {
value = x[p.name]
}
let option = document.createElement('option')
option.innerHTML = p.getName(x)
option.value = value
p.elem.appendChild(option)
//console.log('append value option')
})
}
break
case 'checkboxes':
if (Array.isArray(list)) {
list.forEach(x => {
let cb = document.createElement('input')
cb.setAttribute('type', 'checkbox')
let cbLabel = document.createElement('label')
//cbLabel.style.paddingTop = '5px'
//cbLabel.style.paddingBottom = '5px'
cbLabel.appendChild(cb)
cbLabel.appendChild(document.createTextNode(` ${p.getName(x)}`))
p.optionContainer.appendChild(cbLabel)
p.elems.push(cb)
})
}
p.elems.forEach(checkbox => {
checkbox.addEventListener('change', () => {
this._onElemEvent(p)
})
})
break
case 'radios':
if (Array.isArray(list)) {
list.forEach(x => {
let cb = document.createElement('input')
cb.setAttribute('type', 'radio')
cb.setAttribute('name', p.name)
//cb.addEventListener('click', () => {
// this._onElemEvent(p, x) // fix
//})
let cbLabel = document.createElement('label')
//cbLabel.style.paddingTop = '5px'
//cbLabel.style.paddingBottom = '5px'
cbLabel.appendChild(cb)
cbLabel.appendChild(document.createTextNode(` ${p.getName(x)}`))
p.optionContainer.appendChild(cbLabel)
p.elems.push(cb)
//p.selected = undefined
})
}
p.elems.forEach(radio => {
radio.addEventListener('click', () => {
//console.log('RADIO CLICKED', p.selected)
if (p.selected) {
if (p.selected == radio) {
radio.checked = false
p.selected = undefined
} else {
p.selected.checked = false
p.selected = radio
}
} else {
radio.checked = true
p.selected = radio
}
//console.log('RADIOS ON ELEM EVENT')
this._onElemEvent(p)
})
})
//console.log('RADIO ELEMS:', p.elems)
break
//case 'custom-list':
// p.custom.setList(list)
// / break
case 'model':
//console.log('FILL OPTION FIELDS', list)
p.model.add(p.modelType, list)
//if (Array.isArray(list)) {
// list.forEach(x => {
//})
//}
break
default:
throw new Error(`bug: unknown property type ${p.type}`)
}
p.valueObjects = list
}
/*
Form.prototype._fillCheckboxes = function(p, list) {
}
Form.prototype._fillRadios = function(p, list) {
}
*/
Form.prototype._createList = function(p, fieldsContainer) {
//console.log('create list')
/*
let container = document.getElementById(p.containerId)
if (!container) {
throw new Error(`container '${p.containerId}' not found`)
}
*/
let labelText = p.name
if (p.label) {
labelText = p.label
}
let div = document.createElement('div')
div.setAttribute('class', 'form-group')
//div.style.border = '1px solid #ddd'
p.cover = div
let label = document.createElement('label')
label.setAttribute('class', 'list-header')
label.textContent = labelText
label.style.borderBottom = '1px solid #ddd'
label.style.paddingBottom = '5px'
label.classList.add(_labelClass)
let arrContainer = document.createElement('div')
//arrContainer.setAttribute('class', 'form-group')
arrContainer.textContent = p.emptyListMsg
p.container = arrContainer
let btnWrapper = document.createElement('div')
btnWrapper.setAttribute('class', 'form-group')
//let btn = document.createElement('button')
//btn.textContent = 'добавить...'
//btn.setAttribute('class', 'btn btn-link')
let btnDiv = document.createElement('div')
btnDiv.setAttribute('class', 'show-link')
btnDiv.style.marginTop = '0.75rem'
let btn = document.createElement('a')
btn.textContent = '+ добавить'
btn.setAttribute('href', '#')
let addBtnDiv = document.createElement('div')
//addBtnDiv.style.marginTop = '0.5rem'
//addBtnDiv.style.marginBottom = '0.5rem'
addBtnDiv.appendChild(btn)
//console.log('btn', btn)
btnDiv.appendChild(addBtnDiv)
p.btnShow = btn
p.btnDiv = btnDiv // для вставки сообщений об ошибке сразу после btnDiv, но внутри form-group
p.btnRemove = document.createElement('button')
p.btnRemove.textContent = 'удалить'
p.btnRemove.style.color = 'white'
p.btnRemove.style.backgroundColor = '#ff6961'
p.btnRemove.setAttribute('class', 'btn')
p.btnRemove.hidden = true
p.btnRemove.addEventListener('click', () => {
this._removeCheckedObjects(p)
p.btnRemove.hidden = true
//p.btnShow.hidden = false
})
//btnWrapper.appendChild(btn)
btnWrapper.appendChild(btnDiv)
btnWrapper.appendChild(p.btnRemove)
div.appendChild(label)
//div.appendChild(document.createElement('hr'))
div.appendChild(arrContainer)
div.appendChild(btnWrapper)
//container.appendChild(div)
fieldsContainer.appendChild(div)
//p.btnSubmit = btn
//p.container = arrContainer
p.arr = []
btn.addEventListener('click', (e) => {
e.preventDefault()
if (p.selected) {
this._select(p, p.selected)
}
// Одновременный select
this._createListItem(p)
// console.log('NEEEEEEEEEEEW ITEEEEEEEEM:', item)
//this._select(p, p.arr[p.arr.length - 1])
//let item = p.arr[p.arr.length - 1]
//item.select.classList.add(p.activeClass)
//p.selected = item
//console.log('PUSHED:', item)
//this._openForm(p)
})
//if (this._inEditMode) {
if (this._mode == editing) {
if (p.isEditable) {
btn.hidden = false
} else {
btn.hidden = true
}
}
p.form.fields[p.name] = p
}
Form.prototype._createNestedForm = function(t, obj) {
let formContainer;
if (t.formContainerId) {
formContainer = document.getElementById(t.formContainerId)
if (!formContainer) {
throw new Error(`form container '${t.formContainerId}' not found`)
}
formContainer.hidden = false
} else {
formContainer = document.createElement('div')
formContainer.setAttribute('class', t.formContainerClass)
//formContainer.style.borderLeft = '4px solid #72bcd4'
//formContainer.style.border = '1px solid #eee'
formContainer.style.padding = '1rem'
formContainer.style.marginLeft = '-1rem'
formContainer.style.marginRight = '-1rem'
formContainer.style.backgroundColor = '#e8f4f8'
//formContainer.hidden = true
t.cover.appendChild(formContainer)
}
// fix add showForm()
t.formContainer = formContainer
t.dirty = {}
// fix - return ?
let r = this._createForm(t.schema, formContainer)
t.isFormOpened = true
t.schema.forEach(p => {
this._setFieldValue(p)
})
}
Form.prototype._openForm = function(t) {
//console.log('LOADER clean:', JSON.stringify(t.selected.clean))
let loader = new Loader({
api: this._api,
t: t,
clean: t.selected.clean,
context: this._context,
onError: err => {
//console.log('Loader.err: ', err)
},
onLoaded:() => {
this._createNestedForm(t)
}
})
loader.load()
//console.log('OOOOOOOOOOOOOOOOPEN FORM:', p.isFormOpened)
/*
if (t.isFormOpened) {
this._reset(t)
} else {
//console.log('show form')
let formContainer;
if (t.formContainerId) {
formContainer = document.getElementById(t.formContainerId)
if (!formContainer) {
throw new Error(`form container '${t.formContainerId}' not found`)
}
formContainer.hidden = false
} else {
formContainer = document.createElement('div')
formContainer.setAttribute('class', t.formContainerClass)
//formContainer.hidden = true
t.cover.appendChild(formContainer)
}
// fix add showForm()
t.formContainer = formContainer
// fix - return ?
let r = this._createForm(t.schema, formContainer)
this._loadDataForCustomElems(t.schema)
*/
////////////////////////
////////////////////////
//if (p.formContainer) {
// p.formContainer.hidden = false
//} else if (p.showForm) {
// p.showForm()
//p.isFormOpened = true
//} else {
// create container
//}
//btn.textContent = 'скрыть'
//p.btnShow.hidden = true
/*
t.isFormOpened = true
}
// Всегда инициатор, ибо если форма уже была открыта и грузилась - _reset все сбросил
//t.upd = {}
t.schema.forEach(p => {
this._setFieldValue(p)
})
*/
//if (t.loadingCount == 0) {
// this._onFormLoaded(t)
//}
//console.log('EEEEEEND OF OOOOOOOOOOOOOOOOPEN FORM:', p.isFormOpened)
}
/*
Form.prototype._closeForm = function(t) {
this._removeForm(t)
t.arr = []
}
*/
// FIX разделить на топ и вложенные, топ снимаем выделение со списка, вложенный обнуляем arr
Form.prototype._removeForm = function(t) {
//console.log('_removeForm')
//console.log('CLOOOOOOOOOOOOOOOOSE FORM:', p)
if (!t.isFormOpened) {
return
}
t.schema.forEach(p => {
this._removeField(p)
})
// скрываем если форма открыта
if (t.formContainerId) {
// внешний div, в документе
t.formContainer.hidden = true
t.formContainer.innerHTML = ''
} else if (t.hideForm) {
t.hideForm()
} else {
// inline форма - просто удаляем
t.formContainer.remove()
}
t.fields = {}
t.settingObj = undefined
t.upd = undefined
t.selected = undefined
t.dirty = {}
if (t.loadingCount > 0) {
throw new Error(`Bug: form.loadingCount=${t.loadingCount} after remove fields`)
}
t.isFormOpened = false
}
Form.prototype._select = function(t, item) {
let selected = t.selected
if (t.selected) {
t.selected.select.classList.remove(t.activeClass)
}
this._removeForm(t)
if (selected != item) {
item.select.classList.add(t.activeClass)
t.selected = item
this._openForm(t)
}
}
Form.prototype._check = function(p) {
if (p.btnRemove) {
let q = 0
p.arr.forEach(item => {
if (item.checkbox && item.checkbox.checked) {
q++
}
})
//console.log('checked:', q)
if (q > 0) {
//p.btnShow.hidden = true
p.btnRemove.hidden = false
p.btnShow.hidden = true
} else {
p.btnRemove.hidden = true
p.btnShow.hidden = false
}
}
}
Form.prototype._buildItem = function(p, clean) {
//console.log('buildItem:', p.name, JSON.stringify(clean))
let item = {
clean: clean || {},
}
if (clean === undefined) {
// Создаем пустой item
item.clean.itemName = p.getItemName()
}
let row = document.createElement('div')
row.style.paddingTop = '3px'
row.style.paddingBottom = '3px'
let checkbox = document.createElement('input')
checkbox.setAttribute('type', 'checkbox')
let select = document.createElement('div')
select.style.display = 'inline'
select.style.padding = '7px'
select.style.marginLeft = '5px'
//select.textContent = p.getItemName(obj)
// dirty - object or undefined
//let itemName = p.getItemName(p.dirty)
//item.clean.itemName = itemName
//select.innerHTML = p.getItemName(dirty)
select.innerHTML = item.clean.itemName
row.appendChild(checkbox)
row.appendChild(select)
item.elem = row
item.select = select
item.checkbox = checkbox
// Область для select кликов
select.style.cursor = 'pointer'
select.addEventListener('click', () => {
//if (this._loadingCount == 0) {
//console.log('SELECT ITEM:', JSON.stringify(item.clean))
this._select(p, item)
//}
})
// Чекбоксы
checkbox.setAttribute('type', 'checkbox')
checkbox.addEventListener('change', () => this._check(p))
return item
}
Form.prototype._insertItem = function(p, item) {
// Удаляем надпись emptyListMsg
if (p.arr.length == 0) {
p.container.innerHTML = ''
}
// вставляем в массив
p.arr.push(item)
if (!p.isEditable) {
item.checkbox.disabled = true
}
// вставляем в DOM
p.container.appendChild(item.elem)
}
Form.prototype._up = function(p) {
let cleanList = [];
// цикл по items
p.arr.forEach(x => {
cleanList.push(x.clean)
})
let t = p.form
if (t.type == 'list') {
let item = t.selected
if (item === undefined) {
return // fix bug
}
item.clean[p.name] = cleanList
this._up(t)
} else {
this.form.selected[p.name] = cleanList
// calc signature
this._onFormDataChanged()
}
}
// Для использования в setFieldValue
// Метод создает item, но не запускает уведомление родителя (ибо уже там есть) и
// не выбирает (select) добавленный элемент
Form.prototype._appendListItem = function(p, clean) {
//console.log('pushObject into:', p.name, JSON.stringify(clean))
let item = this._buildItem(p, clean)
// FIX обновляем имя в списке объектов
//item.select.textContent = p.getItemName(obj)
//item.select.innerHTML = p.getItemName(obj)
// добавляем новый
this._insertItem(p, item)
}
Form.prototype._createListItem = function(p) {
//console.log('pushObject into:', p.name)
// вместо clean and dirty - undefined
let item = this._buildItem(p)
// FIX обновляем имя в списке объектов
//item.select.textContent = p.getItemName(obj)
//item.select.innerHTML = p.getItemName(obj)
// добавляем новый
this._insertItem(p, item)
this._select(p, item)
this._up(p)
}
// recursiveUpdateField
// clean и dirty - объекты вида {name: value}
Form.prototype._updateListItem = function(p, upd) {
if (p.type == 'list') {
//console.log(p)
let item = p.selected
// Обновляем рекурсивно до топ уровня
if (item === undefined) {
throw new Error(`Bug: updateListItem of non selected item`)
}
for (let k in upd) {
let v = upd[k]
if (v === undefined) {
//console.log('DELETE PROP:', k, ' IN:', obj)
delete item.clean[k]
} else {
item.clean[k] = v
}
}
let itemName = p.getItemName(p.dirty)
item.clean.itemName = itemName
//item.select.textContent = p.getItemName(item.obj)
item.select.innerHTML = itemName
this._up(p)
} else {
for (let k in upd) {
let v = upd[k]
if (v === undefined) {
delete this.form.selected[k]
} else {
this.form.selected[k] = v
}
}
this._onFormDataChanged()
}
}
Form.prototype._removeCheckedObjects = function(p) {
if (Array.isArray(p.arr)) {
let filtered = []
p.arr.forEach(item => {
if (item.checkbox && item.checkbox.checked) {
// Снимаем выделение
if (p.selected == item) {
this._select(p, p.selected)
}
p.container.removeChild(item.elem)
} else {
filtered.push(item)
}
})
p.arr = filtered
if (p.arr.length == 0) {
p.container.textContent = p.emptyListMsg
}
p.btnShow.hidden = false
this._up(p)
/*
if (p.form.type == 'list') {
//this._pushObject(p.form, this.getObj(p.form.schema))
let upd = {}, objectList = [];
p.arr.forEach(item => {
objectList.push(item.obj)
})
upd[p.name] = objectList
this._updateObject(p.form, upd)
} else {
//this._onFormLoaded(this.form)
this._onFormDataChanged()
}
*/
}
}
Form.prototype._removeDependentFields = function(p) {
//console.log('_removeDependentFields')
let upd = {}
if (Array.isArray(p.chains)) {
// d - depends
p.chains.forEach(d => {
upd = Object.assign(upd, this._removeField(d))
})
}
let fork = p.fork
if (fork) {
//upd = Object.assign(upd, this._resetFork(p.fork))
//console.log('RESET ROAD:', schema)
let cur = fork.current
if (!cur) {
return
}
cur.schema.forEach(d => {
upd = Object.assign(upd, this._removeField(d))
})
if (cur.cleanup) {
cur.cleanup()
}
fork.current = undefined
}
return upd
}
// c - custom property
Form.prototype._onElemEvent = function(c) {
let form = c.form;
if (form.reseting) {
return
}
// Достаем объект и затем ID
let tmp = this._getObjectValue(c)
let cleanValue = tmp.clean
let dirtyValue = tmp.dirty
form.dirty[c.name] = dirtyValue
// ?
//console.log('ON ELEM EVENT:', c.name)
//console.log('cleanValue:', cleanValue)
//console.log('dirtyValue:', dirtyValue)
// bubble update
let upd = {}
upd[c.name] = cleanValue
this._updateListItem(form, upd)
// Очищаем зависимые цепочки и развилки, а сам объект события не трогаем
// FIX обновляем цепочки внутри метода или как сейчас?
//let upd = Object.assign(upd, )
this._removeDependentFields(c)
//console.log('SELECTED OBJECT:', form.selected)
if (dirtyValue !== undefined) {
// Общий контейнер для всех новых полей
let container = document.createDocumentFragment()
c.chains.forEach(d => {
// создаем DOM элементы
this._createField(d, container)
if (d.loadFunc) {
this._loadData(d)
} else if (d.data) {
this._fillOptionsField(d, d.data)
this._setFieldValue(d)
} else {
this._setFieldValue(d) // FIX ????
}
})
let f = c.fork
if (f) {
let cleanId = cleanValue
if (f.getValue) {
// ВАЖНО!
// getValue на вход получает объект целиком
cleanId = f.getValue(dirtyValue)
}
//console.log('fork clean param:', cleanId)
// затем устанавливаем новую (если есть)
let road = f.roads.get(cleanId)
if (road) {
//console.log('SET CURRENT ROAD:', JSON.stringify(road))
if (road.init) {
road.init()
}
//console.log('CONTAINER:', c.cover)
this._createSchemaObjects(road.schema, {after: c.cover})
// в цикле для custom элементов загружаем loadFunc
road.schema.forEach(x => {
if (x.loadFunc) {
this._loadData(x)
} else {
this._setFieldValue(x)
}
})
f.current = road
//road.schema.forEach(x => {
//this._setFieldValue(x)
//})
}
}
// Вставляем все новые поля после поля события
c.cover.parentNode.insertBefore(container, c.cover.nextSibling)
}
if (c.onChange) {
c.onChange(dirtyValue)
}
}
// Загружает данные для построения формы (openForm, init, _select)
// Опциональный объект с уже заполненными полями - clean
function Loader(opt) {
if (!opt.t.selected) {
throw new Error(`Where is selected?`)
}
this._counter = 0
this._api = opt.api
// Для отслеживания - переключил юзер item или нет пока загружались данные
// и для доступа к схеме
this._t = opt.t
// Для отслеживания - переключил юзер item или нет пока загружались данные
this._selected = this._t.selected
this._clean = opt.clean || {}
this._context = opt.context
this._onError = opt.onError
this._onLoaded = opt.onLoaded
}
// Метод загружает данные и добавляем в кэш элементов. Не занимается отображением данных
Loader.prototype._loadElem = function(p, id) {
// Если значение уже закешировано - запрос не отправляем
if (p.caching) {
let v = p.cache.get(id)
if (v) {
return
}
}
this._counter++
//let arg;
if (p.getLoadFuncArg) {
// параметр функции - это объект-снимок формы со всеми заполненными полями
// на текущий момент. Форма та, которой принадлежит свойство p.
id = p.getLoadFuncArg(this._selected) // FIX whole form object
}
console.log(`_loadElem: ${p.name} = ${JSON.stringify(id)}`)
this._api.req({
func: p.loadFunc,
data: id,
onError: err => {
//console.log(err)
// Прерываем загрузку
this._onError(err)
},
onSuccess: resp => {
//console.log(resp)
if (p.responseModifier) {
resp = p.responseModifier(resp)
}
// Добавляем в кэш
if (p.loadFuncContextParam) {
p.cache.set(undefined, resp)
} else {
p.cache.set(id, resp)
}
// resp - это список объектов (например список checboxes или options в select)
// this._clean - это заполненные поля (ID-шники)
// нужна функция, которая из списка объектов методом найдет нужный по ID
// (методом перебора).
let cleanValue = this._clean[p.name]
let dirtyValue = this._findSelectedObject(p, resp, cleanValue)
if (dirtyValue !== undefined) {
if (Array.isArray(p.chains)) {
p.chains.forEach(d => {
//
if (d.loadFunc) {
this._loadElem(d)
}
})
}
let fork = p.fork
if (fork) {
//
let cleanId = cleanValue
if (fork.getValue) {
// ВАЖНО!
// getValue на вход получает объект целиком
cleanId = fork.getValue(dirtyValue)
}
//console.log('fork clean param:', cleanId)
// затем устанавливаем новую (если есть)
let road = fork.roads.get(cleanId)
if (road) {
road.schema.forEach(d => {
if (d.loadFunc) {
this._loadElem(d)
}
})
}
}
}
this._counter--
if (this._counter == 0) {
if (this._selected == this._t.selected) {
this._onLoaded()
}
}
}
})
}
Loader.prototype._findSelectedObject = function(p, resp, id) {
if (!Array.isArray(resp)) {
return
}
if (p.scalar) {
// это список id, а не список объектов
if (p.type == 'checkboxes') {
// fix добавить сюда модель с чекбоксами
if (!Array.isArray(id)) {
return []
}
let result = []
// временный словарь для удобной фильтрации
let checkedIDs = new Map()
id.forEach(checkedID => {
checkedIDs.set(checkedID, true)
})
let key = p.name
if (p.key) {
key = p.key
}
for (let i=0; i {
checkedIDs.set(checkedID, true)
})
let key = p.name
if (p.key) {
key = p.key
}
for (let i=0; i {
if (p.loadFunc) {
if (p.loadFuncContextParam) {
let param = this._context[p.loadFuncContextParam]
this._loadElem(p, param)
} else {
this._loadElem(p)
}
}
})
if (this._counter == 0) {
if (this._selected == this._t.selected) {
this._onLoaded()
}
}
}
// po - parent object
Form.prototype._loadData = function(p) {
let arg;
if (p.getLoadFuncArg) {
// параметр функции - это объект-снимок формы со всеми заполненными полями
// на текущий момент. Форма та, которой принадлежит свойство p.
arg = p.getLoadFuncArg(p.form.selected, this.form.selected)
}
console.log(`_loadData: ${p.name} = ${JSON.stringify(arg)}`)
let paramSignature = JSON.stringify(arg)
//onsole.log('_LOAD DATA ____________________', p.name, p.caching)
if (p.caching) {
let list = p.cache.get(paramSignature)
if (list) {
//console.log('_loadData (from cache):', p, arg)
// Заполняем поля выбора (select, radios, checkboxes) объектами из списка,
// который прислал сервер
this._fillOptionsField(p, list)
// Выбираем элемент(ы), если форма в режиме редактирования
// если режим редактирования этой формы
this._setFieldValue(p)
// Закешированные данные нашли, поэтому выходим
return
}
}
if (p.loading) {
if (p.paramSignature === paramSignature) {
// Запрос уже отправлен, ничего не делаем
// Пример такой ситуации: юзер кликнул по первому объекту, на сервер ушел запрос,
// затем кликнул по второму объекту и снова по первому.
return
}
// Обновляем только сигнатуру параметра, loadingCount уже увеличен, флаг loading уже выставлен
p.paramSignature = paramSignature
} else {
p.loading = true
p.paramSignature = paramSignature
p.form.loadingCount++
this._loadingCount++
}
//console.log('_loadData (loading...):', p, arg)
this._api.req({
func: p.loadFunc,
data: arg,
onError: err => {
if (!p.loading) {
// UI элемент был удален, поэтому сообщение об ошибке не нужно
// loadingCount уже сброшен при удалении элемента
return
}
if (p.paramSignature !== paramSignature) {
// Уже отправлен более свежий запрос, поэтому сообщение об ошибке не нужно
return
}
p.loading = false
p.paramSignature = undefined
p.form.loadingCount--
this._loadingCount--
p.divLoadError = insertError({
//after: item.elem.getAnchor(),
after: p.cover,
err: err,
class: this._errorClass
})
//console.log(`item ${p.name} loaded. Loading count-- (error): ${this._loadingCount}`)
//if (p.form.loadingCount == 0) {
// this._onFormLoaded(p.form)
//}
},
onSuccess: resp => {
if (!p.loading) {
// UI элемент был удален, поэтому загруженные данные больше не нужны
// loadingCount уже сброшен при удалении элемента
//console.log(`_loadData: prop ${p.name} not loading`)
return
}
if (p.paramSignature !== paramSignature) {
// Уже отправлен более свежий запрос, поэтому загруженные данные больше не нужны
//console.log(`_loadData: prop ${p.name} paramSignature changed (has: '${p.paramSignature}', expect: '${paramSignature}')`)
return
}
p.loading = false
p.paramSignature = undefined
p.form.loadingCount--
this._loadingCount--
if (p.responseModifier) {
resp = p.responseModifier(resp)
}
// Добавляем в кэш
if (p.caching) {
p.cache.set(paramSignature, resp)
}
//console.log(`item ${p.name} loaded. Loading count--: ${this._loadingCount}`)
// Заполняем поля выбора (select, radios, checkboxes) объектами из списка,
// который прислал сервер
this._fillOptionsField(p, resp)
// Выбираем элемент(ы), если форма в режиме редактирования
// если режим редактирования этой формы
this._setFieldValue(p)
}
})
}
// FIX сделать метод локальным для формы?
Form.prototype._onFormLoaded = function(loader) {
//console.log('ON_FORM_LOADED:', form, ' loadingCount:', this._loadingCount)
/*
let t = loader.t
if (t.selected != loader.selected) {
return
}
// create form
this._createSchemaObjects(t.schema)
loader.onFormLoaded()
t.schema.forEach(p => {
this._setFieldValue(p, t.selected.clean, t.selected.dirty)
})
*/
//let upd = form.upd
//form.settingObj = undefined
//form.upd = undefined
//this._updateObject(form, upd)
//this.enable()
// editing mode остается до reset формы
// FIXXXXX
/*
if (this._mode == restoring) {
this._mode = this._restoringMode
this._signature = this._restoringSignature
this._btnSubmit.disabled = this._restoringBtnSubmit
this._restoringMode = undefined
this._restoringMode = undefined
this._restoringBtnSubmit = undefined
}
*/
//if (this._mode == editing) {
// if (!this.form.isEditable) {
//this._btnSubmit.disabled = true
//this._btnReset.disabled = true
// }
//}
//if (form.type != 'list') {
//}
}
// Отключает все элементы формы во время загрузки данных с сервера
/*
Form.prototype.disable = function() {
this._schema.forEach(p => {
this._disableField2(p)
})
this._btnSubmit.disabled = true
this._btnReset.disabled = true
}
Form.prototype.enable = function() {
this._schema.forEach(p => {
this._enableField2(p)
})
this._btnSubmit.disabled = false
this._btnReset.disabled = false
}
*/
// Важный метод сбрасывает все поля главной формы и скрывает все открытые формы
Form.prototype.reset = function(opt) {
//console.log('RESET')
try {
let snapshot = JSON.parse(this._initSnapshot)
this._signature = JSON.stringify(snapshot.clean)
this.form.selected = snapshot.clean
} catch (e) {
//console.log(this)
throw new Error(`form not initialized:`)
this.form.selected = {}
}
this._reset(this.form)
//this._btnSubmit.disabled = true
this._mode = filling
if (this._error) {
this._error.remove()
}
//if (opt && opt.ignoreButtons) {
if (!this._enableSubmit) {
this._btnSubmit.disabled = true
}
this._btnSubmit.textContent = this._btnSubmitText
this._btnRemove.hidden = true
//}
//this._onFormDataChanged()
//console.log('reset mode:', this._mode)
//console.log('reset clean:', this.form.selected)
}
// Метод сбрасывает значения всех элементов формы, удаляет зависимые элементы
// и сбрасывает списки, а именно - очищает список, закрывает (удаляет) форму.
// Причем форму закрывает рекурсивно, корректно удаляя все элементы вложенных форм.
// Сброс формы важен чтобы правильно очистить loadingCount, поле loading и т.д.
Form.prototype._reset = function(t) {
//console.log('_RESET', t)
// Флаг, который позволит в методе onElemEvent игнорировать события при
// сбросе формы
t.reseting = true
t.schema.forEach(p => {
if (p.type == 'list') {
//console.log('RESET LIST FIELD:', p)
this._removeForm(p)
// Делаем reset, не удаляя элементы ???
p.arr = []
p.container.innerHTML = ''
p.container.textContent = p.emptyListMsg
p.btnShow.hidden = false
p.btnRemove.hidden = true
} else {
var contextValue
if (p.contextVar) {
contextValue = this._context[p.contextVar]
}
switch (p.type) {
case '':
if (p.contextVar) {
p.value = contextValue
//console.log('"" CONTEXT VALUE:', p.value)
if (contextValue !== undefined) {
this.form.selected[p.name] = contextValue
} else {
delete this.form.selected[p.name]
}
} else {
p.value = undefined
}
break
case 'line':
case 'text':
if (p.contextVar) {
p.elem.value = contextValue
if (contextValue !== undefined) {
this.form.selected[p.name] = contextValue
} else {
delete this.form.selected[p.name]
}
} else {
p.elem.value = ''
}
if (p.editmode != Form.READONLY) {
p.elem.disabled = false
}
// fix dispachevent
break
case 'checkbox':
if (p.contextVar) {
p.elem.value = contextValue
} else {
p.elem.checked = false
}
if (p.editmode != Form.READONLY) {
p.elem.disabled = false
}
break
case 'select':
if (p.contextVar) {
this._fillOptionsField(p, contextValue)
}
if (p.valueObjects) {
if (p.valueObjects.length > 0) {
p.elem.selectedIndex = 0
} else {
p.elem.selectedIndex = -1
}
if (p.editmode != Form.READONLY) {
p.elem.disabled = false
}
p.elem.dispatchEvent(new Event('change'))
}
break
case 'radios':
if (p.contextVar) {
this._fillOptionsField(p, contextValue)
}
// fix context
// fix dispachevent - if checked changed
p.elems.forEach(elem => {
// fix - if checked
if (elem.checked) {
//elem.checked = false
//console.log('RADIOS reset: before click is checked', elem.checked)
elem.dispatchEvent(new Event('click'))
//console.log('RADIOS reset: after click is checked', elem.checked)
}
if (p.editmode != Form.READONLY) {
elem.disabled = false
}
})
break
case 'checkboxes':
if (p.contextVar) {
this._fillOptionsField(p, contextValue)
}
// fix context
// fix dispachevent - if checked changed
p.elems.forEach(elem => {
// fix - if checked
if (elem.checked) {
elem.checked = false
}
if (p.editmode != Form.READONLY) {
elem.disabled = false
}
})
break
case 'model':
// FIX - just enable?
//p.model.enable()
p.modelOptions.forEach(type => {
type.container = p.cover
})
p.model = new Model(p.modelOptions)
// FIX - генерация события?
/*
if (p.contextVar) {
this._fillOptionsField(p, contextValue)
} else if (p.data) {
this._fillOptionsField(p, p.data)
} else if (p.getData) {
this._fillOptionsField(p, p.getData())
} else {
let cached = p.cache.get()
if (cached) {
this._fillOptionsField(p, cached)
}
}
*/
//if (p.changeType == 'few') {
p.model.onChange = () => {
this._onElemEvent(p)
}
break
//case 'custom-list':
// fix custom reset
// break
}
}
if (p.loading) {
p.loading = false
p.paramSignature = undefined
t.loadingCount-- // form
this._loadingCount--
}
this._removeDependentFields(p)
})
t.settingObj = undefined
t.reseting = false
//localStorage.removeItem('x')
}
Form.prototype.hideError = function() {
}
/*
Form.prototype.restore = function(s) {
console.log('restore:', s)
//if (!obj) {
// return
//}
if (this._waitingObj) {
throw new Error(`Can't restoreObj while waiting`)
}
// ВАЖНО!
// Обнулять _loadingCount нельзя, ибо в данный момент может грузится форма
this._mode = restoring
this.form.settingObj = s.obj
this._restoringSignature = s.signature
this._restoringMode = s.mode
this._restoringBtnSubmit = s.btnSubmit
this._schema.forEach(p => {
this._setFieldValue(p)
})
if (this._loadingCount > 0) {
//console.log('disable while loading')
//this.disable() // отключаем форму
// Отключать ли submit?
} else {
console.log('restore, no loading elems')
// fix btnsubmit
this._onFormDataChanged()
this._onFormLoaded() // fix
}
}
*/
// restore - восстанавливает mode, obj, signature и рассчитывает состояние кнопки
Form.prototype.restore = function(snapshotStr) {
let snapshot = JSON.parse(snapshotStr)
if (this._version == snapshot.version) {
return
}
let obj = snapshot.dirty
//console.log('restore:', obj)
//if (!obj) {
// return
//}
// Создаем копию объекта
//obj = JSON.parse(JSON.stringify(obj))
this.reset()
this.form.selected = obj // ? fix что делать с дефолтами
//this._cleanDirtyObject(this._schema, obj, this._clean)
//this._cleanDirtyObject(this._schema, obj, this.form.selected.clean)
//console.log('CLEANED:', this.form.selected.clean)
//console.log('restoring')
this._mode = snapshot.mode
let loader = new Loader({
api: this._api,
t: this.form,
clean: this.form.selected,
context: this._context,
onError: err => {
//console.log('Loader.err: ', err)
},
onLoaded:() => {
this._schema.forEach(p => {
this._setFieldValue(p)
})
let newSignature = JSON.stringify(this.form.selected)
this._signature = snapshot.signature
if (this._signature != newSignature) {
this._btnSubmit.disabled = false
} else {
if (!this._enableSubmit) {
this._btnSubmit.disabled = true
}
}
//console.log('restored signature:', this._signature)
}
})
loader.load()
}
// edit
Form.prototype.edit = function(obj, isRemoveAllowed) {
//console.log('EDIT:', obj)
if (!obj) {
return
}
obj = JSON.parse(JSON.stringify(obj))
//this.hide()
this.reset({
ignoreButtons: true,
})
this.form.selected = obj // ? fix что делать с дефолтами
//this.form.selected.dirty = obj
//this.form.selected.clean = {}
//this._cleanDirtyObject(this._schema, obj, this._clean)
//this._cleanDirtyObject(this._schema, obj, this.form.selected.clean)
//console.log('CLEANED:', this.form.selected.clean)
//console.log('editing')
this._mode = editing
let loader = new Loader({
api: this._api,
t: this.form,
clean: this.form.selected,
context: this._context,
onError: err => {
//console.log('Loader.err: ', err)
//this.show()
},
onLoaded:() => {
this._schema.forEach(p => {
this._setFieldValue(p)
})
this._signature = JSON.stringify(this.form.selected)
if (!this._enableSubmit) {
this._btnSubmit.disabled = true
}
this._btnSubmit.textContent = this._btnUpdateText
if (isRemoveAllowed) {
this._btnRemove.hidden = false
}
//console.log('edit.signature:', this._signature)
//console.log('edit.mode:', this._mode)
//this.show()
}
})
loader.load()
}
Form.prototype.fill = function(obj) {
//console.log('fill:', obj)
if (!obj) {
return
}
obj = JSON.parse(JSON.stringify(obj))
this.reset()
this.form.selected = obj // ? fix что делать с дефолтами
//this.form.selected.dirty = obj
//this.form.selected.clean = {}
//this._cleanDirtyObject(this._schema, obj, this._clean)
//this._cleanDirtyObject(this._schema, obj, this.form.selected.clean)
//console.log('CLEANED:', this.form.selected.clean)
this._mode = filling
let loader = new Loader({
api: this._api,
t: this.form,
clean: this.form.selected,
context: this._context,
onError: err => {
/// console.log('Loader.err: ', err)
},
onLoaded:() => {
this._schema.forEach(p => {
this._setFieldValue(p)
})
// this._signature = JSON.stringify(this.form.selected.clean)
// this._btnSubmit.disabled = true
// console.log('edit:', this._signature)
}
})
loader.load()
}
Form.prototype._setFieldValue = function(f) {
if (f.loading) {
//console.log(`prop ${f.name} loading`)
return
}
let obj
if (this.form == f.form) {
obj = f.form.selected
} else {
obj = f.form.selected.clean
}
let v = obj[f.name]
//console.log(`_setFieldValue: ${f.name}=${v}`)
if (f.type == 'list') {
//this._openForm(f)
if (Array.isArray(v)) {
// fix
v.forEach((itemObj, idx) => {
//let cObj = cleanObj[f.name][idx]
//this._appendListItem(f, cObj, dObj)
this._appendListItem(f, itemObj)
})
}
if (this._mode == editing) {
if (!f.isEditable) {
this._disableField(f)
}
}
} else {
switch (f.type) {
case 'checkboxes':
// fix - event
let checkedIDs = {}
if (Array.isArray(v)) {
v.forEach(x => {
checkedIDs[x] = true
})
if (f.scalar) {
f.valueObjects.forEach((item, idx) => {
//console.log('CHECK IF:', item, idx, checkedIDs)
if (checkedIDs[item]) {
f.elems[idx].checked = true
}
})
} else {
let key = f.name
if (f.key) {
key = f.key
}
f.valueObjects.forEach((item, idx) => {
//console.log('CHECK IF:', item, idx, checkedIDs)
if (checkedIDs[item[key]]) {
f.elems[idx].checked = true
}
})
}
}
break
case 'radios':
//console.log('RADIOS:', f.valueObjects)
if (v && Array.isArray(f.valueObjects)) {
for (let i=0; i {
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 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;
}
// fix - hide открытые вложенные формы
Form.prototype.hide = function() {
this._formContainer.hidden = true
}
// fix - show открытые вложенные формы
Form.prototype.show = function() {
this._formContainer.hidden = false
}