/* Начальный расчет 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 })) */ } /*
// remove + add
*/ 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 }