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

3550 lines
99 KiB
JavaScript
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.
/*
Начальный расчет 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<p.elems.length; i++) {
if (p.elems[i].checked) {
dirty = p.valueObjects[i]
if (p.scalar) {
clean = dirty
} else {
clean = dirty[p.name]
}
break
}
}
}
//upd[p.name] = dirty
break
case 'select':
// отнимаем пустой option
//console.log('SELECTED INDEX:', p, p.elem.selectedIndex)
// Если элементы еще не загрузились по сети
if (Array.isArray(p.valueObjects) && p.valueObjects.length > 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 <small class="form-text text-muted">от 15 секунд и выше</small>
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 <small class="form-text text-muted">от 15 секунд и выше</small>
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 <small class="form-text text-muted">от 15 секунд и выше</small>
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
}))
*/
}
/*
<div class="form-group">
<label>ain devices</label>
<div id="ainDevices"></div>
// remove + add
<div class="form-group">
<button id="add-ain-device" class="btn btn-link">добавить</button>
</div>
</div>
*/
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<resp.length; i++) {
let v = resp[i]
if (checkedIDs.has(v)) {
result.push(v)
}
}
return result
} else {
for (let i=0; i<resp.length; i++) {
if (resp[i] == id) {
return id
}
}
}
} else {
// это список объектов
if (p.type == 'checkboxes') {
// fix добавить сюда модель с чекбоксами
// id - это список ID, а нам нужно найти список объектов
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<resp.length; i++) {
let v = resp[i]
if (checkedIDs.has(v[key])) {
result.push(v)
}
}
return result
} else {
for (let i=0; i<resp.length; i++) {
let v = resp[i]
if (v[p.name] == id) {
return v
}
}
}
}
}
Loader.prototype.load = function() {
//console.log('LOADER.loading....')
this._t.schema.forEach(p => {
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<f.valueObjects.length; i++) {
let item = f.valueObjects[i]
if (f.scalar) {
if (item == v) {
let radio = f.elems[i]
//radio.checked = true
// Поле selected важно для снятия отметки при клике
// на уже выделенном radio
//f.selected = radio
//console.log
radio.dispatchEvent(new Event('click'))
break
}
} else {
if (item[f.name] == v) {
let radio = f.elems[i]
//console.log('RADIOS: before click is checked', radio.checked)
radio.dispatchEvent(new Event('click'))
break
}
}
}
}
break
case 'select':
f.elem.value = v
f.elem.dispatchEvent(new Event('change'))
break
case '':
f.value = v
this._onElemEvent(f)
break
case 'line':
case 'text':
if (v !== undefined) {
if (f.dataType == 'hex') {
if (v) {
v = base64ToHex(v)
}
} else if (f.dataType == 'int' && v === 0) {
v = ''
}
if (f.set) {
f.elem.value = f.set(v)
} else {
f.elem.value = v
}
}
//f.elem.dispatchEvent(new Event('input'))
this._onElemEvent(f)
break
case 'checkbox':
f.elem.checked = v
f.elem.dispatchEvent(new Event('change'))
break
case 'model':
//console.log('modelSelectType:', f.modelSelectType)
//console.log('modelChangeType:', f.modelChangeType)
if (f.modelChangeType == 'radio') {
f.model.setSelected(f.modelSelectType, v)
} else {
f.model.checkList(f.modelSelectType, v)
}
this._onElemEvent(f)
break
}
// Отключать поля можно только если форма в режиме редактирования (именно топ форма)
if (this._mode == editing) {
if (f.editmode == Form.UNEDITABLE) {
this._disableField(f)
}
}
}
}
function insertError(opt) {
let div = document.createElement('div')
if (opt.class) {
div.className = opt.class
}
let msg;
if (typeof opt.err == 'string') {
msg = opt.err
} else {
msg = opt.err.message
}
div.textContent = msg
if (opt.after) {
opt.after.parentNode.insertBefore(div, opt.after.nextSibling)
} else if (opt.before) {
// например перед submit кнопкой
opt.before.parentNode.insertBefore(div, opt.before)
}
return div
}
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
}
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;
}
// fix - hide открытые вложенные формы
Form.prototype.hide = function() {
this._formContainer.hidden = true
}
// fix - show открытые вложенные формы
Form.prototype.show = function() {
this._formContainer.hidden = false
}