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