2520 lines
75 KiB
JavaScript
Executable File
2520 lines
75 KiB
JavaScript
Executable File
|
||
|
||
function copyTemplateInto(templateId, dstId) {
|
||
let t = document.getElementById(templateId)
|
||
if (!t) {
|
||
throw new Error(`tag id=${templateId} not found`)
|
||
}
|
||
//let dst = document.getElementById(dstId)
|
||
//if (!dst) {
|
||
// throw new Error(`tag id=${dstId} not found`)
|
||
//}
|
||
let dst = getElemSafe('container', dstId)
|
||
let node = t.content.cloneNode(true)
|
||
dst.innerHTML = ''
|
||
dst.appendChild(node)
|
||
}
|
||
|
||
function Router() {
|
||
let u = new URL(location.href)
|
||
// Базовый url вида http://localhost:8081
|
||
this._origin = u.origin
|
||
this._routes = {}
|
||
}
|
||
|
||
|
||
// goto . Аргумент - полный URL, либо path + searchParams вида '/configs?deviceID=7'.
|
||
// Полный URL обычно получаем при чтении текущего адрес location.href, либо при чтении
|
||
// ссылки somelink.href. Path получим если меню реализовать в виде ListModel, где
|
||
// ключем очевидно будет path, а не полный URL.
|
||
Router.prototype.goto = function(url, params) {
|
||
if (!url) {
|
||
throw new Error(`Wrong 'url' argument: ${url}`)
|
||
}
|
||
|
||
console.log('goto:', url)
|
||
|
||
if (url.startsWith('/')) {
|
||
url = this._origin + url
|
||
}
|
||
|
||
let u = new URL(url)
|
||
let r = this._routes[u.pathname]
|
||
if (!r) {
|
||
return
|
||
}
|
||
|
||
let path = u.pathname
|
||
|
||
if (params) {
|
||
//let searchParams = new URLSearchParams()
|
||
let searchParams = u.searchParams
|
||
for (let name in params) {
|
||
searchParams.set(name, params[name])
|
||
}
|
||
let queryString = searchParams.toString()
|
||
if (queryString !== '') {
|
||
path += '?' + queryString
|
||
}
|
||
} else {
|
||
if (u.searchParams.toString()) {
|
||
//console.log('serachParams:', u.searchParams.toString())
|
||
path += '?' + u.searchParams
|
||
}
|
||
}
|
||
|
||
path += u.hash
|
||
url = this._origin + path
|
||
|
||
|
||
console.log('route:', r)
|
||
console.log('group:', this._currentGroup)
|
||
|
||
if (r.group != this._currentGroup) {
|
||
if (this._currentGroup) {
|
||
if (r.group.base != this._currentGroup.base) {
|
||
location.href = url
|
||
return
|
||
}
|
||
}
|
||
this._currentGroup = r.group
|
||
if (r.group.init) {
|
||
//console.log('call init and wait')
|
||
r.group.init((ctx) => {
|
||
this._ctx = ctx
|
||
this._processRoute(r, url, path)
|
||
})
|
||
} else {
|
||
this._ctx = {}
|
||
this._processRoute(r, url, path)
|
||
}
|
||
} else {
|
||
this._processRoute(r, url, path)
|
||
}
|
||
}
|
||
|
||
Router.prototype._processRoute = function(r, url, path) {
|
||
if (r.group.toggle) {
|
||
r.group.toggle(this._ctx, r.data)
|
||
}
|
||
|
||
let state = {}
|
||
state['omg.router.url'] = url
|
||
if (url != location.href) {
|
||
console.log('pushState:', url)
|
||
// В первую очередь проверка позволяет не добавлять в историю страницу,
|
||
// которая только что загрузилась (реакция на событие DOMContentLoaded)
|
||
history.pushState(state, "", path)
|
||
} else {
|
||
console.log('replaceState:', url)
|
||
history.replaceState(state, "", path)
|
||
}
|
||
|
||
r.func(this._ctx)
|
||
}
|
||
|
||
Router.prototype.add = function(routeGroup) {
|
||
let group = {
|
||
base: routeGroup.base,
|
||
init: routeGroup.init,
|
||
toggle: routeGroup.toggle
|
||
}
|
||
routeGroup.routes.forEach((route) => {
|
||
if (!route.path) {
|
||
throw new Error(`Empty path in route: ${JSON.stringify(route)}`)
|
||
}
|
||
this._routes[route.path] = {
|
||
func: route.func,
|
||
data: route.data,
|
||
group: group
|
||
}
|
||
})
|
||
}
|
||
|
||
// Читает path из URL и запускает соответсвующий обработчик
|
||
Router.prototype.init = function() {
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
let links = document.getElementsByClassName('route')
|
||
//console.log(links)
|
||
for (let i=0; i<links.length; i++) {
|
||
let link = links[i]
|
||
//console.log(link)
|
||
link.addEventListener('click', (e) => {
|
||
let u = new URL(link.href)
|
||
let r = this._routes[u.pathname]
|
||
if (r) {
|
||
// if current path in routes
|
||
e.preventDefault()
|
||
//console.log('CLICK:', link.href)
|
||
this.goto(link.href)
|
||
} else {
|
||
console.log("not in routes")
|
||
}
|
||
})
|
||
}
|
||
|
||
window.addEventListener('popstate', (e) => {
|
||
if (!e.state) {
|
||
return
|
||
}
|
||
let url = e.state['omg.router.url']
|
||
//console.log('popstate:', url)
|
||
this.goto(url)
|
||
})
|
||
|
||
this.goto(location.href)
|
||
})
|
||
}
|
||
|
||
/**
|
||
* A opt
|
||
* @typedef {Object} Opt
|
||
* @property {string} key - ключ.
|
||
* @property {Element} cover - обложка
|
||
*/
|
||
|
||
|
||
/**
|
||
*
|
||
* @param {Opt} opt - объект
|
||
*/
|
||
|
||
/*
|
||
opt - Опции
|
||
- container {Element} HTML-элемент, контейнер для элементов модели
|
||
- cover * {Element} HTML-элемент, внешний контейнер для всей модели. Нужен для одного
|
||
кейса - когда необходимо показать сообщение об ошибке (например, ошибка загрузки по API),
|
||
а добавление ошибки после container невозможна. Например, container - это TBODY.
|
||
По умолчанию - нет.
|
||
- unique * {func} Функция вычисляет hashcode для каждого элемента модели и не позволяет
|
||
добавлять в модель дубликаты. Применяется в методах add() и updateSelected()
|
||
- activeClass * {string} CSS класс для выбранного (кликом мыши) элемента модели.
|
||
По умолчанию 'selected'.
|
||
- errorClass * {string} CSS класс для сообщения об ошибке (например, при загрузке модели
|
||
по API или удалении элементов по API). По умолчанию 'errmsg'.
|
||
- messageClass * {string} CSS класс для сообщения о пустой модели.
|
||
- emptyMessage * {string} Сообщение, которое нужно показать если setList устанавливает
|
||
пустую модель.
|
||
- key * {string} Имя id-поля объекта. Используется в единственном месте - для AJAX-запроса
|
||
на удаление элемента.
|
||
- btnRemove * {Element} HTML-кнопка для удаления элементов модели.
|
||
Если объявлена removeFunc - удаление будет происходить AJAX запросами к API, иначе сразу
|
||
из модели. Если по умолчанию кнопка disabled - модель сама будет включать и отключать
|
||
кнопку в зависимости от проставленных галочек напротив элемментов модели.
|
||
- template {Object} Настройки шаблона для рендера элемента модели
|
||
- id {string} ID template элемента
|
||
- renders * {Object} Рендер-функции шаблонов (смотри Template)
|
||
- sort * {Object} Сортировка элементов модели
|
||
- key * {string} имя поля по которому сортируем
|
||
- func * {func} собственная функция сортировки
|
||
- reverse * {boolean} флаг, показывает что все нужно сортировать наоборот
|
||
По умолчанию не сортирует, а элементы добавляет в конец списка. Если указать reverse=true,
|
||
добавлять элементы будет в начало списка. Сортировка используется в методах add() и
|
||
updateSelected()
|
||
- ajax * {Object} Настройки для загрузки модели и/или удаления элементов модели по API
|
||
- api {API} смотри API
|
||
- listFunc * {string} имя API метода для загрузки модели
|
||
- removeFunc * {string} имя API метода для удаления элемента модели по ID. Имя поля
|
||
объекта, который хранит ID задается в опции key.
|
||
|
||
|
||
*/
|
||
function ListModel(opt) {
|
||
Emiter.call(this)
|
||
|
||
this._container = getElemSafe('container', opt.container)
|
||
// Внешний блок к которому можно добавлять сообщения об ошибках.
|
||
// Нужен для стандартной ситуации - в HTML разметке есть таблица с THEAD и TBODY.
|
||
// TBODY - это контейнер для добавления элементов модели. Добавить к нему DIV с
|
||
// сообщением об ошибке нельзя. Нужна ссылка на TABLE, которую передаем через
|
||
// опцию 'cover'.
|
||
// Если cover не указан, то cover = container
|
||
if (opt.cover) {
|
||
this._cover = getElemSafe('cover', opt.cover)
|
||
} else {
|
||
this._cover = this._container
|
||
}
|
||
this._defaultChild = this._container.firstChild
|
||
|
||
this._key = opt.key
|
||
// Для отслеживания уникальности элементов модели. Сигнатура: func(obj) hashcode
|
||
// Хешкод добавляется в _hashcodes
|
||
this._unique = opt.unique
|
||
this._preFunc = opt.preFunc
|
||
|
||
this._activeClass = opt.activeClass || 'selected'
|
||
this._errorClass = opt.errorClass || 'errmsg'
|
||
this._messageClass = opt.messageClass
|
||
|
||
if (opt.btnRemove) {
|
||
this._btnRemove = getElemSafe('btnRemove', opt.btnRemove)
|
||
|
||
this._btnRemove.addEventListener('click', () => {
|
||
if (this._removeFunc) {
|
||
this._btnRemove.disabled = true
|
||
this._disableCheckboxes()
|
||
// Удаляем сообщение об ошибке от предыдущего API запроса
|
||
if (this._divRemoveError) {
|
||
this._divRemoveError.remove()
|
||
}
|
||
this._removing = true
|
||
// удаляем элементы по очереди
|
||
this._removeNext()
|
||
} else {
|
||
this.removeChecked()
|
||
}
|
||
})
|
||
|
||
this._isBtnRemoveDisabledByDefault = this._btnRemove.disabled
|
||
this._isBtnRemoveHiddenByDefault = this._btnRemove.hidden
|
||
|
||
if (this._isBtnRemoveHiddenByDefault) {
|
||
this.on('checkListChanged', () => {
|
||
if (this._removing) {
|
||
return
|
||
}
|
||
if (this.countChecked() > 0) {
|
||
this._btnRemove.hidden = false
|
||
} else {
|
||
this._btnRemove.hidden = true
|
||
}
|
||
})
|
||
} else if (this._isBtnRemoveDisabledByDefault) {
|
||
this.on('checkListChanged', () => {
|
||
if (this._removing) {
|
||
return
|
||
}
|
||
if (this.countChecked() > 0) {
|
||
this._btnRemove.disabled = false
|
||
} else {
|
||
this._btnRemove.disabled = true
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
let ajax = opt.ajax
|
||
|
||
if (ajax) {
|
||
this._api = ajax.api
|
||
if (!ajax.api) {
|
||
throw new Error("'ajax.api' option is required")
|
||
}
|
||
this._listFunc = ajax.listFunc
|
||
this._removeFunc = ajax.removeFunc
|
||
if (this._removeFunc) {
|
||
if (!this._key) {
|
||
throw new Error("'ajax.removeFunc' option is requires 'key' option")
|
||
}
|
||
}
|
||
}
|
||
|
||
this._template = new Template({
|
||
id: opt.template.id,
|
||
renders: opt.template.renders,
|
||
});
|
||
|
||
// Сортировка (опционально)
|
||
let by = opt.sort
|
||
if (by) {
|
||
if (by.key) {
|
||
if (by.reverse) {
|
||
this._cmp = function(a, b) {
|
||
if (a[by.key] < b[by.key]) {
|
||
return -1
|
||
}
|
||
return 1
|
||
}
|
||
} else {
|
||
this._cmp = function(a, b) {
|
||
if (a[by.key] < b[by.key]) {
|
||
return 1
|
||
}
|
||
return -1
|
||
}
|
||
}
|
||
} else if (by.func) {
|
||
if (by.reverse) {
|
||
this._cmp = (a, b) => {
|
||
return by.func(b, a)
|
||
}
|
||
} else {
|
||
this._cmp = by.func
|
||
}
|
||
} else {
|
||
if (by.reverse) {
|
||
// 3й кейс - добавляем в начало
|
||
this._isReverse = true
|
||
}
|
||
}
|
||
}
|
||
this._arr = []
|
||
this._hashcodes = new Map()
|
||
}
|
||
|
||
ListModel.prototype = Object.create(Emiter.prototype)
|
||
ListModel.prototype.constructor = ListModel
|
||
|
||
|
||
ListModel.prototype.isAjax = function() {
|
||
return this._api !== undefined
|
||
}
|
||
|
||
|
||
ListModel.prototype.disable = function() {
|
||
if (this._btnRemove) {
|
||
this._btnRemove.disabled = true
|
||
}
|
||
this._disableCheckboxes()
|
||
}
|
||
|
||
ListModel.prototype.countChecked = function() {
|
||
let q = 0
|
||
this._arr.forEach((item) => {
|
||
if (item.checkbox && item.checkbox.checked) {
|
||
q++
|
||
}
|
||
})
|
||
return q
|
||
}
|
||
|
||
ListModel.prototype.listChecked = function() {
|
||
let list = []
|
||
this._arr.forEach((item) => {
|
||
if (item.checkbox.checked) {
|
||
list.push(item.obj)
|
||
}
|
||
})
|
||
return list
|
||
}
|
||
|
||
ListModel.prototype.removeChecked = function() {
|
||
while (true) {
|
||
let idx = this._arr.findIndex(item => item.checkbox.checked)
|
||
if (idx == -1) {
|
||
return
|
||
}
|
||
this.remove(idx)
|
||
}
|
||
}
|
||
|
||
ListModel.prototype._disableCheckboxes = function() {
|
||
this._arr.forEach((item) => {
|
||
item.checkbox.disabled = true
|
||
})
|
||
}
|
||
|
||
ListModel.prototype._enableCheckboxes = function() {
|
||
this._arr.forEach((item) => {
|
||
item.checkbox.disabled = false
|
||
})
|
||
}
|
||
|
||
ListModel.prototype._clear = function() {
|
||
// Для отслеживания уникальности элементов модели (если объявлена unique функция)
|
||
this._hashcodes = new Map()
|
||
this._container.innerHTML = ''
|
||
this._arr = []
|
||
this._selectedItem = null
|
||
if (this._divLoadError) {
|
||
this._divLoadError.remove()
|
||
}
|
||
if (this._divMessage) {
|
||
this._divMessage.remove()
|
||
}
|
||
// Блок с сообщением об ошибке удаления (по API)
|
||
if (this._divRemoveError) {
|
||
this._divRemoveError.remove()
|
||
}
|
||
if (this._btnRemove) {
|
||
this._btnRemove.disabled = this._isBtnRemoveDisabledByDefault
|
||
this._btnRemove.hidden = this._isBtnRemoveHiddenByDefault
|
||
}
|
||
}
|
||
|
||
ListModel.prototype.clear = function() {
|
||
let wasChecked = this.countChecked()
|
||
let selected = this._selectedItem
|
||
this._clear()
|
||
|
||
this._setEmptyMessageIfNeeded()
|
||
|
||
if (selected) {
|
||
this.emit('select')
|
||
}
|
||
if (wasChecked > 0) {
|
||
this.emit('checkListChanged')
|
||
}
|
||
this.emit('listChanged')
|
||
}
|
||
|
||
ListModel.prototype.setIsDisabledFunc = function(f) {
|
||
this._isDisabledFunc = f
|
||
}
|
||
|
||
ListModel.prototype.setList = function(list) {
|
||
//console.trace()
|
||
// Запоминаем сколько элементов отмечено и выделено, чтобы отправить события
|
||
// замены списка
|
||
let wasChecked = this.countChecked()
|
||
let selected = this._selectedItem
|
||
this._clear()
|
||
|
||
if (Array.isArray(list) && list.length > 0) {
|
||
list.forEach(obj => this._add(obj))
|
||
|
||
if (this._isDisabledFunc) {
|
||
if (this._isDisabledFunc()) {
|
||
this.disable()
|
||
}
|
||
}
|
||
} else {
|
||
this._setEmptyMessageIfNeeded()
|
||
}
|
||
if (selected) {
|
||
this.emit('select')
|
||
}
|
||
if (wasChecked > 0) {
|
||
this.emit('checkListChanged')
|
||
}
|
||
this.emit('listChanged')
|
||
this.emit('setList')
|
||
}
|
||
|
||
ListModel.prototype._add = function(obj) {
|
||
if (this._preFunc) {
|
||
this._preFunc(obj)
|
||
}
|
||
if (this._arr.length == 0) {
|
||
this._container.innerHTML = ''
|
||
}
|
||
// console.log('model.add:', obj)
|
||
// проверка на уникальность + сохраняем hashcode
|
||
let hashcode;
|
||
if (this._unique) {
|
||
hashcode = this._unique(obj)
|
||
if (this._hashcodes.get(hashcode)) {
|
||
return
|
||
}
|
||
}
|
||
let idx = 0;
|
||
// добавляем в начало массива. 2 кейса:
|
||
// - нет cmp функции и reverse=true
|
||
// - есть cmp функция, но модель пустая
|
||
if (this._cmp) {
|
||
// ищем индекс, по которому нужно вставить элемент
|
||
for (; idx < this._arr.length; idx++) {
|
||
if (this._cmp(obj, this._arr[idx].obj) == -1) {
|
||
break
|
||
}
|
||
}
|
||
} else if (!this._isReverse) {
|
||
// добавляем в конец массива
|
||
idx = this._arr.length
|
||
}
|
||
let item = this._renderObj(obj)
|
||
if (this._unique) {
|
||
item.hashcode = hashcode
|
||
this._hashcodes.set(hashcode, true)
|
||
}
|
||
// вставляем в массив
|
||
this._arr.splice(idx, 0, item)
|
||
// вставляем в DOM
|
||
this._container.insertBefore(item.elem, this._container.children[idx])
|
||
return true
|
||
}
|
||
|
||
ListModel.prototype.add = function(obj) {
|
||
//console.log('add to model:', obj)
|
||
let ok = this._add(obj)
|
||
if (ok) {
|
||
this.emit('listChanged')
|
||
return true
|
||
}
|
||
}
|
||
|
||
// keyFunc - return true if found, updateFunc(tags, obj, upd)
|
||
ListModel.prototype.partialUpdateBy = function(updateFunc, keyFunc) {
|
||
for (let idx=0; idx<this._arr.length; idx++) {
|
||
let item = this._arr[idx]
|
||
if (keyFunc(item.obj)) {
|
||
updateFunc(item.tags, item.obj)
|
||
return
|
||
}
|
||
}
|
||
}
|
||
|
||
ListModel.prototype.updateBy = function(newobj, keyFunc) {
|
||
for (let idx=0; idx<this._arr.length; idx++) {
|
||
let item = this._arr[idx]
|
||
if (keyFunc(item.obj)) {
|
||
this._update(newobj, item)
|
||
return
|
||
}
|
||
}
|
||
}
|
||
|
||
// fix add silent select after update
|
||
ListModel.prototype._update = function(newobj, si) {
|
||
if (this._preFunc) {
|
||
this._preFunc(newobj)
|
||
}
|
||
|
||
let hashcode;
|
||
if (this._unique) {
|
||
hashcode = this._unique(newobj)
|
||
if (si.hashcode != hashcode && this._hashcodes.get(hashcode)) {
|
||
// дубликат
|
||
return
|
||
}
|
||
}
|
||
|
||
let checked;
|
||
if (si.checkbox) {
|
||
checked = si.checkbox.checked
|
||
}
|
||
|
||
let item;
|
||
if (this._cmp) {
|
||
// удаляем
|
||
this._arr = this._arr.filter(item => item != si)
|
||
this._container.removeChild(si.elem)
|
||
|
||
let idx = 0
|
||
// ищем индекс, по которому нужно вставить элемент
|
||
for (; idx < this._arr.length; idx++) {
|
||
if (this._cmp(newobj, this._arr[idx].obj) == -1) {
|
||
break
|
||
}
|
||
}
|
||
// рендерим
|
||
item = this._renderObj(newobj, checked)
|
||
// вставляем в массив
|
||
this._arr.splice(idx, 0, item)
|
||
// вставляем в DOM
|
||
this._container.insertBefore(item.elem, this._container.children[idx])
|
||
} else {
|
||
// нужно сохранить позицию элемента в списке
|
||
item = this._renderObj(newobj, checked)
|
||
|
||
this._container.replaceChild(item.elem, si.elem)
|
||
|
||
let idx = this._arr.findIndex(x => x == si)
|
||
this._arr[idx] = item
|
||
}
|
||
if (this._unique) {
|
||
item.hashcode = hashcode
|
||
if (hashcode != si.hashcode) {
|
||
// удаляем старый, добавляем новый
|
||
this._hashcodes.delete(si.hashcode)
|
||
this._hashcodes.set(hashcode, true)
|
||
}
|
||
}
|
||
if (si == this._selectedItem) {
|
||
this._selectedItem = item
|
||
}
|
||
}
|
||
|
||
ListModel.prototype.updateSelected = function(newobj) {
|
||
let si = this._selectedItem
|
||
|
||
if (!si) {
|
||
return
|
||
}
|
||
|
||
this._update(newobj, si)
|
||
|
||
// /console.log('UPDATED:', this._arr)
|
||
this._selectedItem = null
|
||
this.emit('select')
|
||
this.emit('listChanged')
|
||
return true
|
||
}
|
||
|
||
// При обновлении элемента модели нужно сохранить галочку checked.
|
||
ListModel.prototype._renderObj = function(obj, checked) {
|
||
let r = this._template.render(obj)
|
||
let item = {
|
||
elem: r.elem,
|
||
tags: r.tags,
|
||
obj: obj
|
||
}
|
||
// Область для select кликов
|
||
if (r.tags.select) {
|
||
item.select = r.tags.select
|
||
item.select.style.cursor = 'pointer'
|
||
item.select.addEventListener('click', () => this._select(item))
|
||
}
|
||
|
||
// Чекбоксы
|
||
let checkbox = r.tags.id
|
||
if (checkbox) {
|
||
checkbox.setAttribute('type', 'checkbox')
|
||
if (checked) {
|
||
checkbox.checked = true
|
||
}
|
||
checkbox.addEventListener('change', () => this.emit('checkListChanged'))
|
||
item.checkbox = checkbox
|
||
}
|
||
return item
|
||
}
|
||
|
||
|
||
/**
|
||
* Выделяет текущий элемент по индексу
|
||
* @param {number} idx индекс элемента
|
||
*/
|
||
ListModel.prototype.select = function(idx) {
|
||
let item = this._arr[idx]
|
||
if (!item) {
|
||
return
|
||
}
|
||
this._select(item)
|
||
}
|
||
|
||
// Выделяет текущий элемент по индексу
|
||
ListModel.prototype.unselect = function(silent) {
|
||
if (this._selectedItem) {
|
||
if (this._selectedItem.select) {
|
||
this._selectedItem.select.classList.remove(this._activeClass)
|
||
}
|
||
this._selectedItem = null
|
||
if (!silent) {
|
||
this.emit('select')
|
||
}
|
||
}
|
||
}
|
||
|
||
// Выделяет текущий элемент по индексу
|
||
ListModel.prototype.selectBy = function(f) {
|
||
for (let idx=0; idx<this._arr.length; idx++) {
|
||
let item = this._arr[idx]
|
||
if (f(item.obj)) {
|
||
this._select(item)
|
||
return
|
||
}
|
||
}
|
||
}
|
||
|
||
ListModel.prototype.selectByKey = function(id, silent) {
|
||
for (let i=0; i<this._arr.length; i++) {
|
||
let item = this._arr[i]
|
||
if (item.obj[this._key] === id) {
|
||
this._select(item, silent)
|
||
return
|
||
}
|
||
}
|
||
}
|
||
|
||
// Идея метода следующая. Представим левое меню и 2 случая:
|
||
// - юзер кликает по пункту, меняется URL, запускается обработчик,
|
||
// который по getSelected берет выделенный элемент и что-то делает
|
||
// - юзер после клика обновляет страницу. JS router исходя из адреса
|
||
// запускает обработчик. А обработчик должен во первых понять что в модели
|
||
// нет выделенных элементов, затем найти в модели нужный элемент по ID
|
||
// (взятому из URL), затем тихо выделить (silent select, без генерации события
|
||
// 'select') пункт меню (элемент модели) и в конце что-то сделать с элементом.
|
||
//
|
||
// Эти 2 кейса можно реализовать в 1 методе и не усложнять клиентскую логику и код.
|
||
// Как работает метод: проверяет есть ли selected элемент, если есть и его ключ
|
||
// совпадает с аргументом - возвращает объект. Если selected элемента нет - ищет
|
||
// элемент по ключу, тихо выделяет (без генерации 'select' события) и возвращает.
|
||
ListModel.prototype.silentSelectByKey = function(id) {
|
||
let obj = this.getSelected()
|
||
if (obj) {
|
||
//console.log('item:', item)
|
||
if (obj[this._key] === id) {
|
||
return obj
|
||
}
|
||
}
|
||
for (let i=0; i<this._arr.length; i++) {
|
||
let item = this._arr[i]
|
||
if (item.obj[this._key] === id) {
|
||
if (this._selectedItem != item) {
|
||
if (this._selectedItem && this._selectedItem.select) {
|
||
this._selectedItem.select.classList.remove(this._activeClass)
|
||
}
|
||
|
||
if (item.select) {
|
||
item.select.classList.add(this._activeClass)
|
||
}
|
||
this._selectedItem = item
|
||
}
|
||
return item.obj
|
||
}
|
||
}
|
||
}
|
||
|
||
ListModel.prototype._select = function(item, silent) {
|
||
// клик на любом элементе - снимает выделение с текущего
|
||
if (this._selectedItem && this._selectedItem.select) {
|
||
this._selectedItem.select.classList.remove(this._activeClass)
|
||
if (!silent) {
|
||
this.emit('select', undefined, this._selectedItem.tags)
|
||
}
|
||
}
|
||
|
||
if (this._selectedItem == item) {
|
||
// Клик на текущем эелементе - снимает выделение
|
||
this._selectedItem = null
|
||
} else {
|
||
if (item.select) {
|
||
item.select.classList.add(this._activeClass)
|
||
}
|
||
this._selectedItem = item
|
||
if (!silent) {
|
||
this.emit('select', item.obj, item.tags)
|
||
}
|
||
}
|
||
}
|
||
|
||
ListModel.prototype.get = function(idx) {
|
||
let item = this._arr[idx]
|
||
if (item) {
|
||
return item.obj
|
||
}
|
||
}
|
||
|
||
// Выделяет текущий элемент по индексу
|
||
ListModel.prototype.getByKey = function(id) {
|
||
for (let i=0; i<this._arr.length; i++) {
|
||
let item = this._arr[i]
|
||
if (item.obj[this._key] === id) {
|
||
return item.obj
|
||
}
|
||
}
|
||
}
|
||
|
||
ListModel.prototype.remove = function(idx) {
|
||
let item = this._arr[idx]
|
||
if (!item) {
|
||
return
|
||
}
|
||
this._container.removeChild(item.elem)
|
||
// удаляем
|
||
this._arr.splice(idx, 1)
|
||
if (this._unique) {
|
||
this._hashcodes.delete(item.hashcode)
|
||
}
|
||
|
||
this._setEmptyMessageIfNeeded()
|
||
|
||
if (this._selectedItem == item) {
|
||
this._selectedItem = null
|
||
this.emit('select')
|
||
}
|
||
if (item.checkbox && item.checkbox.checked) {
|
||
this.emit('checkListChanged')
|
||
}
|
||
this.emit('listChanged')
|
||
}
|
||
|
||
ListModel.prototype.removeSelected = function() {
|
||
let idx = this._arr.findIndex(item => item == this._selectedItem)
|
||
if (idx != -1) {
|
||
this.remove(idx)
|
||
}
|
||
}
|
||
|
||
// функция должна вернуть true. Удялет только 1 элемент
|
||
ListModel.prototype.removeBy = function(f) {
|
||
for (let i=0; i<this._arr.length; i++) {
|
||
if (f(this._arr[i].obj)) {
|
||
this.remove(i)
|
||
return
|
||
}
|
||
}
|
||
}
|
||
|
||
ListModel.prototype.getList = function() {
|
||
let arr = []
|
||
this._arr.forEach(item => arr.push(item.obj))
|
||
return arr
|
||
}
|
||
|
||
ListModel.prototype.getSelected = function() {
|
||
if (this._selectedItem) {
|
||
return this._selectedItem.obj
|
||
}
|
||
}
|
||
|
||
ListModel.prototype.size = function() {
|
||
return this._arr.length
|
||
}
|
||
|
||
// FIX - либо сделать on('setList') либо встроить в setList - selectBy и параметр из URL
|
||
// load - отдельный метод load нужен для такого случая - юзер выбирает
|
||
// из выпадающего списка прибор, а для уже для конкретного прибора загружается
|
||
// список конфигураций и строится модель
|
||
ListModel.prototype.load = function(opt) {
|
||
if (!this._api) {
|
||
//return
|
||
throw new Error('Try load non ajax ListModel')
|
||
}
|
||
this._opt = opt || {}
|
||
// В случае повторных ошибок при попытке загрузить модель - можем получить
|
||
// список ошибок, поэтому предыдущую удаляем
|
||
if (this._divLoadError) {
|
||
this._divLoadError.remove()
|
||
}
|
||
this._api.req({
|
||
"func": this._opt.listFunc || this._listFunc,
|
||
"data": this._opt.data,
|
||
"onError": (err) => {
|
||
let anchor;
|
||
if (this._divMessage) {
|
||
anchor = this._divMessage
|
||
} else {
|
||
anchor = this._cover
|
||
}
|
||
this._divLoadError = insertError({
|
||
after: anchor,
|
||
err: err,
|
||
class: this._errorClass
|
||
})
|
||
if (this._opt.onError) {
|
||
this._opt.onError(err)
|
||
}
|
||
},
|
||
"onSuccess": (resp) => {
|
||
this.setList(resp)
|
||
// Success callback нужен для следующего кейса: после загрузки (и
|
||
// setList !) модели берем ID из URL и делаем selectBy
|
||
if (this._opt.onSuccess) {
|
||
this._opt.onSuccess(resp)
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
// Перезагружаем модель. Зачем нужен такой метод? Простой кейс - после submit формы на
|
||
// сервер, Formobj перезагружает привязанную модель. Но, например, в случае добавления
|
||
// новой конфигурации к прибору - модель у нас не простая, а привязана к deviceID.
|
||
ListModel.prototype.reload = function() {
|
||
this.load(this._opt)
|
||
}
|
||
|
||
ListModel.prototype.setError = function(err) {
|
||
let anchor;
|
||
if (this._divMessage) {
|
||
anchor = this._divMessage
|
||
} else if (this._divLoadError) {
|
||
anchor = this._divLoadError
|
||
} else {
|
||
anchor = this._cover
|
||
}
|
||
return insertError({
|
||
after: anchor,
|
||
err: err,
|
||
class: this._errorClass
|
||
})
|
||
}
|
||
|
||
ListModel.prototype._removeNext = function() {
|
||
let idx = this._arr.findIndex((item) => item.checkbox.checked)
|
||
if (idx == -1) {
|
||
// не нашли элемент с галочкой - все удалены
|
||
this._whenRemoveEnds()
|
||
} else {
|
||
// ВАЖНО!
|
||
// Вместо id отправляется объект вида {configID: 231}
|
||
//let p = {}
|
||
//p[this._key] = this._arr[idx].obj[this._key]
|
||
let p = this._arr[idx].obj[this._key]
|
||
this._api.req({
|
||
"func": this._removeFunc,
|
||
"data": p,
|
||
"onError": err => {
|
||
this._divRemoveError = insertError({
|
||
before: this._btnRemove,
|
||
err: err,
|
||
class: this._errorClass
|
||
})
|
||
this._whenRemoveEnds()
|
||
},
|
||
"onSuccess": () => {
|
||
this.remove(idx)
|
||
this._removeNext()
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
ListModel.prototype._whenRemoveEnds = function() {
|
||
this._removing = false
|
||
if (this.countChecked() > 0) {
|
||
// после ошибки могли
|
||
this._btnRemove.disabled = false
|
||
this._btnRemove.hidden = false
|
||
} else {
|
||
this._btnRemove.disabled = this._isBtnRemoveDisabledByDefault
|
||
this._btnRemove.hidden = this._isBtnRemoveHiddenByDefault
|
||
}
|
||
this._enableCheckboxes()
|
||
this._setEmptyMessageIfNeeded()
|
||
}
|
||
|
||
ListModel.prototype._setEmptyMessageIfNeeded = function() {
|
||
console.log(this._arr.length, this._defaultChild)
|
||
if (this._arr.length == 0) {
|
||
if (this._defaultChild) {
|
||
this._container.appendChild(this._defaultChild)
|
||
}
|
||
}
|
||
}
|
||
///////////////
|
||
|
||
/*
|
||
Шаблонизатор нужен в 2 случаях:
|
||
- для отрисовки модели
|
||
- для построения интерфейса.
|
||
Пример - список конфигурационных параметров с полями ввода
|
||
|
||
|
||
|
||
Модель можно сделать отдельной или встроенной в Select, Div-list и т.д.
|
||
|
||
Методы модели:
|
||
- добавить элемент
|
||
- удалить элемент
|
||
- выделить элемент
|
||
- снять выделение
|
||
- получить выделенный объект
|
||
- сортировка элементов списка?
|
||
- добавление / удаление элементов без перерисовки всего списка, чтобы сохранились
|
||
выделения других элементов или взаимодействия с DOM в других элементах ?
|
||
|
||
|
||
Для рендера отдельных элементов в шаблонизаторе нужно реализовать 2 метода:
|
||
- рендер, который возвращает отдельно HTMLElement
|
||
- рендер, который проходит по списку объектов, для каждого рендерит HTMLElement,
|
||
и добавляет в контейнер
|
||
*/
|
||
|
||
|
||
function Template(opt) {
|
||
if (opt.id) {
|
||
this._loadTemplate(opt.id)
|
||
} else {
|
||
throw new Error("'id' option is required")
|
||
}
|
||
this._renders = opt.renders || {}
|
||
this._templateID = opt.id
|
||
}
|
||
|
||
Template.prototype._loadTemplate = function(id) {
|
||
let tag = document.getElementById(id)
|
||
if (!tag) {
|
||
throw new Error(`Template id=${id} not found`)
|
||
}
|
||
if (tag.tagName != 'TEMPLATE') {
|
||
throw new Error(`Tag id=${id} must be template tag`)
|
||
}
|
||
return tag.content
|
||
}
|
||
/*
|
||
Template.prototype.fromString = function(templateString) {
|
||
let tag = document.createElement('template')
|
||
tag.innerHTML = templateString
|
||
this._frag = tag.content
|
||
return this
|
||
}
|
||
*/
|
||
Template.prototype.renderTo = function(list, container) {
|
||
container.innerHTML = ''
|
||
if (Array.isArray(list)) {
|
||
list.forEach((item)=>{
|
||
let r = this._render(this._templateID, item)
|
||
container.appendChild(r.elem)
|
||
})
|
||
}
|
||
}
|
||
|
||
// Возвращает объект вида {elem: Element, tags: tags object},
|
||
// где elem - это отрендеренный HTML элемент (НЕ DocumentFragment),
|
||
// а tags - это карта быстрых ссылок на HTML элементы шаблона, отмеченные
|
||
// аттрибутом data-id="name"
|
||
Template.prototype.render = function(item) {
|
||
return this._render(this._templateID, item)
|
||
}
|
||
|
||
Template.prototype._render = function(templateID, obj) {
|
||
let getvalue = function(obj, path) {
|
||
let v = obj
|
||
path.split('.').forEach((prop) => {
|
||
if (v === undefined) {
|
||
throw new Error(`Cannot get property '${prop}' of undefined in chain '${path}'`)
|
||
}
|
||
v = v[prop]
|
||
})
|
||
return v
|
||
}
|
||
|
||
let frag = this._loadTemplate(templateID)
|
||
frag = frag.cloneNode(true)
|
||
let elems = frag.querySelectorAll('[data-prop], [data-id], [data-value]')
|
||
let tags = {}
|
||
|
||
for (let elem of elems) {
|
||
let id = elem.dataset.id
|
||
let prop = elem.dataset.prop
|
||
let nestedTemplateID = elem.dataset.template
|
||
let value = elem.dataset.value
|
||
|
||
if (id) {
|
||
tags[id] = elem
|
||
}
|
||
|
||
if (prop) {
|
||
|
||
if (nestedTemplateID) {
|
||
let list = getvalue(obj, prop)
|
||
|
||
//console.log('prop:', prop, '/ nestedTemplateID:', nestedTemplateID, '/ list:', list, ' /elem:', elem)
|
||
|
||
if (Array.isArray(list)) {
|
||
list.forEach((item)=>{
|
||
let r = this._render(nestedTemplateID, item)
|
||
elem.appendChild(r.elem)
|
||
})
|
||
}
|
||
} else {
|
||
let x = getvalue(obj, prop)
|
||
if (x !== undefined) {
|
||
elem.textContent = x
|
||
}
|
||
//console.log('prop:', prop, '/ x:', x, ' /elem:', elem)
|
||
}
|
||
}
|
||
|
||
if (value) {
|
||
let x = getvalue(obj, value)
|
||
if (x !== undefined) {
|
||
elem.value = x
|
||
}
|
||
}
|
||
}
|
||
|
||
// /console.log(tags, elems, obj)
|
||
|
||
let f = this._renders[templateID]
|
||
if (f) {
|
||
f(tags, obj)
|
||
}
|
||
|
||
return {
|
||
elem: frag.firstElementChild,
|
||
tags: tags
|
||
}
|
||
}
|
||
|
||
|
||
/*
|
||
метод onChange нужен чтобы:
|
||
- показывать / скрывать кнопку удалить (если выделен или нет элемент списка)
|
||
- отображать детали по выделенному элементу. Например, клик по названию в списке -
|
||
в соседней форме отображаем все поля (для редактирования).
|
||
|
||
Сохранять elem нужно, чтобы убрать activeClass при выборе другого элемента
|
||
Сохранять obj нужно чтобы, например, при нажатии кнопки "удалить" знать какой объект
|
||
удаляем.
|
||
*/
|
||
|
||
|
||
|
||
|
||
/////////////////////
|
||
|
||
|
||
|
||
|
||
/*
|
||
Options:
|
||
url
|
||
data
|
||
responseType (по умолчанию 'json')
|
||
timeout (milliseconds)
|
||
onDone (errorMessage, obj)
|
||
|
||
data - параметр передается в метод xhr.send() как есть
|
||
|
||
errorMessage - строка вида "statusCode: statusText" для HTTP кодов отличных
|
||
от 200, строка "0: Unknown status" для незавершенных запросов (прерванных по
|
||
таймауту, вызовом метода abort(), из-за сетевой ошибки), строка вида
|
||
"parse json: error message" для ошибки парсинга ответа.
|
||
Если ошибки нет - undefined.
|
||
|
||
Разделять ошибки на timeout, abort, error (сетевые), как это сделано в jQuery,
|
||
нет смысла, ибо JS не дает никакой информации по ошибке кроме вызова
|
||
соответствующего обработчика. А для сетевых ошибок вообще кидает в консоль
|
||
исключение, которое нельзя перехватить.
|
||
|
||
onDone - одна функция вместо двух onError и onSuccess по простой причине -
|
||
большинство ajax запросов - это отправка данных формы. До отправки некоторые
|
||
контролы нужно заблокировать (disabled = true), а после получения ответа от
|
||
сервера - разблокировать, причем независимо от результата.
|
||
*/
|
||
|
||
function ajaxreq(opt) {
|
||
let xhr = new XMLHttpRequest()
|
||
|
||
xhr.addEventListener('loadend', function() {
|
||
if (xhr.status != 200) {
|
||
let txt
|
||
if (xhr.status == 0) {
|
||
txt = 'Unknown status'
|
||
} else {
|
||
txt = xhr.statusText
|
||
}
|
||
opt.onDone(xhr.status + ': ' + txt)
|
||
} else {
|
||
let obj
|
||
switch (opt.responseType || 'json') {
|
||
case 'json':
|
||
try {
|
||
obj = JSON.parse(xhr.responseText)
|
||
} catch (e) {
|
||
opt.onDone('json parse: ' + e.message)
|
||
return
|
||
}
|
||
break
|
||
default:
|
||
obj = xhr.responseText
|
||
break
|
||
}
|
||
opt.onDone(undefined, obj)
|
||
}
|
||
})
|
||
xhr.open('POST', opt.url)
|
||
if (opt.timeout) {
|
||
xhr.timeout = opt.timeout
|
||
}
|
||
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest')
|
||
xhr.setRequestHeader('Content-Type', 'text/json; charset=utf-8')
|
||
xhr.send(opt.data)
|
||
}
|
||
|
||
|
||
|
||
//////////////////// WS
|
||
|
||
export class WebsocketClient {
|
||
constructor({url, path, reconnectDelay, lostConnectionTimeout, sid}) {
|
||
if (!lostConnectionTimeout) {
|
||
throw new Error('lostConnectionTimeout option is required')
|
||
}
|
||
if (url) {
|
||
this._url = url
|
||
} else if (path) {
|
||
if (!path.startsWith('/')) {
|
||
throw new Error("path must starts with '/'")
|
||
}
|
||
let u = new URL(location.href)
|
||
this._url = `${u.protocol == 'http:' ? 'ws' : 'wss'}://${u.host}${path}`
|
||
} else {
|
||
throw new Error('url or path option is required')
|
||
}
|
||
|
||
this._reconnectDelay = reconnectDelay || 5000
|
||
this._lostConnTimeout = lostConnectionTimeout * 1000
|
||
this._sid = sid
|
||
this._map = new Map()
|
||
this.autoReconnect = true
|
||
this._isConnected = false
|
||
this._funcMap = new Map()
|
||
this._prefixMap = new Map()
|
||
}
|
||
|
||
connect() {
|
||
this._ws = new WebSocket(this._url)
|
||
this._ws.onmessage = this._onMessage.bind(this)
|
||
this._ws.onclose = this._onClose.bind(this)
|
||
this._ws.onerror = this._onError.bind(this)
|
||
this._ws.onopen = this._onOpen.bind(this)
|
||
}
|
||
|
||
_onOpen() {
|
||
this._isConnected = true
|
||
if (this._sid) {
|
||
this.sendBytes(JSON.stringify(this._sid))
|
||
}
|
||
this._prefixMap.forEach(x => {
|
||
for (let channel of x.channels) {
|
||
this.send('subscribe', channel)
|
||
}
|
||
})
|
||
this._pingIntervalId = setInterval(_ => {
|
||
this.send('ping')
|
||
}, this._lostConnTimeout/2)
|
||
this._resetLostConnTimeout()
|
||
this.onConnected?.()
|
||
}
|
||
|
||
_onClose() {
|
||
this._isConnected = false
|
||
this._ws = null
|
||
this.onDisconnected?.()
|
||
if (this.autoReconnect) {
|
||
this._reconnectTimeoutId = setTimeout(this.connect.bind(this), this._reconnectDelay)
|
||
}
|
||
}
|
||
|
||
_onError(err) {
|
||
this._isConnected = false
|
||
}
|
||
|
||
_onMessage(e) {
|
||
if (this.onMessage) {
|
||
this.onMessage(e.data)
|
||
} else {
|
||
let envelope;
|
||
try {
|
||
envelope = JSON.parse(e.data)
|
||
} catch (err) {
|
||
throw new Error(`JSON.parse websocket message: ${err}`)
|
||
}
|
||
let func = this._funcMap.get(envelope.type)
|
||
if (func) {
|
||
func(envelope.data)
|
||
}
|
||
}
|
||
this._resetLostConnTimeout()
|
||
}
|
||
|
||
// задает 2 таймера - на отправку пинга и на протухание соединения
|
||
_resetLostConnTimeout() {
|
||
if (this._lostConnTimeoutId) {
|
||
clearTimeout(this._lostConnTimeoutId)
|
||
}
|
||
this._lostConnTimeoutId = setTimeout(_ => {
|
||
if (this._ws) {
|
||
this._ws.close()
|
||
}
|
||
}, this._lostConnTimeout)
|
||
}
|
||
|
||
send(messageType, param) {
|
||
if (this._isConnected) {
|
||
this._ws.send(JSON.stringify({
|
||
type: messageType,
|
||
data: param
|
||
}))
|
||
}
|
||
}
|
||
|
||
sendBytes(data) {
|
||
if (this._isConnected) {
|
||
this._ws.send(data)
|
||
}
|
||
}
|
||
|
||
handle(channelPrefix, handler) {
|
||
if (this._prefixMap.has(channelPrefix)) {
|
||
throw new Error(`channel prefix '${channelPrefix}' already has handler`)
|
||
}
|
||
let funcNames = []
|
||
for (let funcName in handler) {
|
||
if (this._funcMap.has(funcName)) {
|
||
throw new Error(`message type '${funcName}' already has handle func`)
|
||
}
|
||
this._funcMap.set(funcName, handler[funcName])
|
||
funcNames.push(funcName)
|
||
}
|
||
this._prefixMap.set(channelPrefix, {
|
||
channels: [],
|
||
funcNames: funcNames
|
||
})
|
||
}
|
||
|
||
unhandle(channelPrefix) {
|
||
let x = this._prefixMap.get(channelPrefix)
|
||
x.funcNames.forEach(funcName => {
|
||
this._funcMap.delete(funcName)
|
||
})
|
||
for (let channel of x.channels) {
|
||
this.send('unsubscribe', channel)
|
||
}
|
||
this._prefixMap.delete(channelPrefix)
|
||
}
|
||
|
||
handleFunc(funcName, f) {
|
||
if (this._funcMap.has(funcName)) {
|
||
throw new Error(`message type '${funcName}' already has handle func`)
|
||
}
|
||
this._funcMap.set(funcName, f)
|
||
}
|
||
|
||
unhandleFunc(funcName) {
|
||
// fix проверить что удаляем одинокую функцию
|
||
this._funcMap.delete(funcName)
|
||
}
|
||
|
||
subscribe(channel) {
|
||
let channelPrefix = channel.split('=')[0]
|
||
let x = this._prefixMap.get(channelPrefix)
|
||
if (!x) {
|
||
throw new Error(`handler for channel prefix ${channelPrefix} is not set`)
|
||
}
|
||
for (let ch of x.channels) {
|
||
if (ch == channel) {
|
||
return
|
||
}
|
||
}
|
||
x.channels.push(channel)
|
||
this.send('subscribe', channel)
|
||
}
|
||
|
||
unsubscribe(channel) {
|
||
let channelPrefix = channel.split('=')[0]
|
||
let x = this._prefixMap.get(channelPrefix)
|
||
if (!x) {
|
||
throw new Error(`handler for channel prefix ${channelPrefix} is not set`)
|
||
}
|
||
let idx = x.channels.findIndex(ch => ch == channel)
|
||
if (idx >= 0) {
|
||
x.channels.splice(idx, 1)
|
||
this.send('unsubscribe', channel)
|
||
}
|
||
}
|
||
|
||
close() {
|
||
this._prefixMap.clear()
|
||
this._funcMap.clear()
|
||
this.autoReconnect = false
|
||
if (this._reconnectTimeoutId) {
|
||
clearTimeout(this._reconnectTimeoutId)
|
||
}
|
||
if (this._lostConnTimeoutId) {
|
||
clearTimeout(this._lostConnTimeoutId)
|
||
}
|
||
if (this._pingIntervalId) {
|
||
clearInterval(this._pingIntervalId)
|
||
}
|
||
if (this._ws) {
|
||
this._ws.close()
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
/*
|
||
message вида:
|
||
{
|
||
func: "getAccount",
|
||
param: {
|
||
id: 123
|
||
}
|
||
}
|
||
*/
|
||
//////////////////////////////////////////////////////////////////////////////////
|
||
|
||
function StatusMessage(opt) {
|
||
if (!opt.elem) {
|
||
throw new Error("'elem' option is required")
|
||
}
|
||
if (typeof opt.elem == 'string') {
|
||
this._elem = document.getElementById(opt.elem)
|
||
if (!this._elem) {
|
||
throw new Error(`'elem' (id=${opt.elem}) not found`)
|
||
}
|
||
} else {
|
||
this._elem = opt.elem
|
||
}
|
||
|
||
this._okClass = opt.okClass || StatusMessage.okClass
|
||
this._errorClass = opt.errorClass || StatusMessage.errorClass
|
||
this._delay = opt.delay || 4000
|
||
this._hide = opt.hide
|
||
}
|
||
|
||
StatusMessage.prototype.flash = function(msg) {
|
||
//console.log('FLASH msg:', msg)
|
||
|
||
this._elem.classList.remove(this._errorClass)
|
||
this._elem.classList.add(this._okClass)
|
||
this._elem.textContent = msg
|
||
if (this._timerId) {
|
||
clearTimeout(this._timerId)
|
||
}
|
||
this._timerId = setTimeout(() => {
|
||
if (this._hide) {
|
||
this._elem.style.display = 'none'
|
||
}
|
||
this._elem.textContent = ''
|
||
}, this._delay)
|
||
}
|
||
|
||
StatusMessage.prototype.error = function(msg) {
|
||
//console.log('ERROR msg:', msg)
|
||
|
||
if (this._timerId) {
|
||
clearTimeout(this._timerId)
|
||
}
|
||
this._elem.classList.remove(this._okClass)
|
||
this._elem.classList.add(this._errorClass)
|
||
this._elem.textContent = msg
|
||
if (this._hide) {
|
||
this._elem.style.display = 'block'
|
||
}
|
||
}
|
||
|
||
StatusMessage.prototype.ok = function(msg) {
|
||
//console.log('OK msg:', msg)
|
||
if (this._timerId) {
|
||
clearTimeout(this._timerId)
|
||
}
|
||
this._elem.classList.remove(this._errorClass)
|
||
this._elem.classList.add(this._okClass)
|
||
this._elem.textContent = msg
|
||
if (this._hide) {
|
||
this._elem.style.display = 'block'
|
||
}
|
||
}
|
||
|
||
StatusMessage.prototype.clear = function() {
|
||
//console.log('CLEAR msg')
|
||
if (this._timerId) {
|
||
clearTimeout(this._timerId)
|
||
}
|
||
if (this._hide) {
|
||
this._elem.style.display = 'none'
|
||
}
|
||
this._elem.textContent = ''
|
||
}
|
||
|
||
//////////// API
|
||
|
||
/*
|
||
Два типа запросов: которые возвращают данные и запросы действия, которые ничего не
|
||
возвращают.
|
||
|
||
Для первого типа запросов нужно выводить ошибки, для второго типа - ошибки
|
||
и (опционально) сообщение об успехе (типа "параметры обновлены, интерфейсы удалены")
|
||
*/
|
||
|
||
|
||
/*
|
||
Опции:
|
||
- url, адрес API
|
||
- timeout, таймаут для ajax запросов
|
||
- hideStatus, скрывать status (display: none) в момент отправки запроса или нет (true/false)
|
||
- errorClass, css класс для сообщений об ошибке
|
||
- okClass, css класс для ok-сообщений
|
||
- flash, длительность показа ok-сообщения (миллисекунды). По истечении времени,
|
||
сообщение скрывается. Если hideStatus=true, блок также будет скрыт.
|
||
По умолчанию, ok-сообщение не скрывается.
|
||
*/
|
||
function API(opt) {
|
||
this._url = opt.url
|
||
this._timeout = opt.timeout
|
||
}
|
||
|
||
/*
|
||
Опции:
|
||
- status, html элемент для вывода ошибок и ok-сообщений
|
||
- ok, сообщение, которое будет выведено в status с стилем okClass, если сервер
|
||
вернул что-то кроме ошибки, null.
|
||
- empty, сообщение, которое будет показано если сервер вернет null. Например,
|
||
юзер выбрал в списке модем, а на сервере его уже нет. Сервер вернет null, юзер
|
||
увидит сообщение.
|
||
|
||
onError - колбек вызывается в случае ошибки, получает 1 параметр:
|
||
- либо errmsg, строка которая содержит сетевую ошибку либо ошибку парсинга JSON
|
||
- либо error объект из ответа сервера. По умолчанию {code: int, message: string}
|
||
|
||
onSuccess - колбек, вызывается если не было ошибок
|
||
*/
|
||
API.prototype.req = function(opt) {
|
||
let p = {
|
||
func: opt.func
|
||
}
|
||
if (opt.data !== undefined) {
|
||
p.data = opt.data
|
||
}
|
||
if (opt.status) {
|
||
opt.status.clear()
|
||
}
|
||
ajaxreq({
|
||
"url": this._url,
|
||
"data": JSON.stringify(p),
|
||
"timeout": this._timeout,
|
||
"onDone": (errmsg, resp) => {
|
||
if (errmsg) {
|
||
if (opt.status) {
|
||
opt.status.error(errmsg)
|
||
} else {
|
||
console.log('ajaxreq:', errmsg)
|
||
}
|
||
if (opt.onError) {
|
||
opt.onError(errmsg)
|
||
}
|
||
return
|
||
}
|
||
if (resp && resp.error) {
|
||
if (opt.status) {
|
||
opt.status.error(resp.error.message)
|
||
} else {
|
||
console.log('ajaxreq resp:', resp.error.message)
|
||
}
|
||
if (opt.onError) {
|
||
opt.onError(resp.error)
|
||
}
|
||
return
|
||
}
|
||
if (opt.onSuccess) {
|
||
opt.onSuccess(resp)
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
|
||
///////////////
|
||
|
||
function Emiter() {
|
||
this._emit_map = {}
|
||
}
|
||
|
||
Emiter.prototype.on = function(event, func) {
|
||
let arr = this._emit_map[event]
|
||
if (arr) {
|
||
arr.push(func)
|
||
} else {
|
||
this._emit_map[event] = [func]
|
||
}
|
||
}
|
||
|
||
Emiter.prototype.emit = function(event, ...obj) {
|
||
let arr = this._emit_map[event]
|
||
if (arr) {
|
||
arr.forEach((func) => {
|
||
func(...obj)
|
||
})
|
||
}
|
||
}
|
||
|
||
|
||
/////////////////// SELECT
|
||
|
||
//function ExistingSelect(opt) {
|
||
//thi
|
||
//}
|
||
|
||
|
||
// Событие 'select', обработчик получает на вход ID
|
||
function Select(opt) {
|
||
Emiter.call(this)
|
||
|
||
if (!opt.field) {
|
||
throw new Error("'field' option is required")
|
||
}
|
||
this._select = opt.field
|
||
/*
|
||
if (typeof opt.select == 'string') {
|
||
this._select = document.getElementById(opt.select)
|
||
if (!this._select) {
|
||
throw new Error(`'elem' (id=${opt.select}) not found`)
|
||
}
|
||
} else {
|
||
this._select = opt.select
|
||
}
|
||
*/
|
||
|
||
//if (!opt.key) {
|
||
// throw new Error("'key' option is required")
|
||
//}
|
||
this._select.addEventListener('change', this._onchange.bind(this))
|
||
this._arr = []
|
||
this._key = opt.key
|
||
this._noZero = opt.noZero
|
||
this._errorClass = opt.errorClass || 'errmsg'
|
||
|
||
if (opt.name) {
|
||
if (typeof opt.name == 'string') {
|
||
this._getName = (item) => {
|
||
return item[opt.name]
|
||
}
|
||
} else {
|
||
// fix function
|
||
this._getName = opt.name
|
||
}
|
||
} else {
|
||
this._getName = (item) => {
|
||
return item
|
||
}
|
||
}
|
||
|
||
|
||
if (opt.filled) {
|
||
for (let i=0; i<this._select.options.length; i++) {
|
||
let option = this._select.options[i]
|
||
if (option.value !== '') {
|
||
let v;
|
||
let value = option.value.trim()
|
||
|
||
if (opt.valueType) {
|
||
switch (opt.valueType) {
|
||
case 'int':
|
||
v = parseInt(value)
|
||
if (!isNaN(v)) {
|
||
this._arr.push(v)
|
||
}
|
||
break
|
||
case 'float':
|
||
v = parseFloat(value)
|
||
if (!isNaN(v)) {
|
||
this._arr.push(v)
|
||
}
|
||
break
|
||
default:
|
||
throw new Error(`Unknown valueType '${opt.valueType}'`)
|
||
}
|
||
} else {
|
||
this._arr.push(value)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
//console.log('filled:', this._arr)
|
||
}
|
||
|
||
Select.prototype = Object.create(Emiter.prototype)
|
||
Select.prototype.constructor = Select
|
||
|
||
Select.prototype.getActionType = function() {
|
||
return 'select'
|
||
}
|
||
|
||
Select.prototype.getType = function() {
|
||
return 'select'
|
||
}
|
||
|
||
Select.prototype.enable = function() {
|
||
this._select.disabled = false
|
||
}
|
||
|
||
Select.prototype.disable = function() {
|
||
this._select.disabled = true
|
||
}
|
||
|
||
Select.prototype.getAnchor = function() {
|
||
return this._select
|
||
}
|
||
|
||
Select.prototype.getCover = function() {
|
||
return this._select
|
||
}
|
||
|
||
Select.prototype.setList = function(list) {
|
||
//console.log('SET LIST select:', list)
|
||
this._select.innerHTML = ''
|
||
if (Array.isArray(list)) {
|
||
if (!this._noZero) {
|
||
// добавляем пустой option
|
||
this._select.appendChild(document.createElement('option'))
|
||
}
|
||
// заполняем данными
|
||
this._arr = list
|
||
this._arr.forEach((item) => {
|
||
let option = document.createElement('option')
|
||
if (this._key) {
|
||
option.value = item[this._key]
|
||
} else {
|
||
option.value = item
|
||
}
|
||
option.textContent = this._getName(item)
|
||
this._select.appendChild(option)
|
||
})
|
||
this.emit('listChanged')
|
||
}
|
||
}
|
||
|
||
Select.prototype.clear = function() {
|
||
//console.log('CLEAR select:', this._select)
|
||
this._select.innerHTML = ''
|
||
this._arr = []
|
||
if (this._divLoadError) {
|
||
this._divLoadError.remove()
|
||
}
|
||
this.emit('listChanged')
|
||
}
|
||
|
||
Select.prototype.likeReset = function() {
|
||
this.emit('select')
|
||
}
|
||
|
||
Select.prototype.reset = function() {
|
||
//console.log('RESET select:', this._select)
|
||
if (this._arr.length == 0) {
|
||
return
|
||
}
|
||
if (this._noZero) {
|
||
let item = this._arr[0]
|
||
if (this._key) {
|
||
this._select.value = item[this._key]
|
||
} else {
|
||
this._select.value = item
|
||
}
|
||
} else {
|
||
this._select.value = ""
|
||
}
|
||
this._select.dispatchEvent(new Event('change'))
|
||
}
|
||
|
||
Select.prototype.getSelected = function() {
|
||
//console.log(this._select.selectedIndex)
|
||
let idx = this._select.selectedIndex
|
||
// индекс 0 - это пустой option, поэтому idx-1
|
||
if (!this._noZero) {
|
||
idx--
|
||
}
|
||
return this._arr[idx]
|
||
}
|
||
/*
|
||
Select.prototype.setError = function(err) {
|
||
let div;
|
||
if (this._divLoadError) {
|
||
div = insertError({
|
||
after: this._divLoadError,
|
||
err: err,
|
||
class: this._errorClass
|
||
})
|
||
} else {
|
||
div = insertError({
|
||
after: this._select,
|
||
err: err,
|
||
class: this._errorClass
|
||
})
|
||
}
|
||
return div
|
||
}
|
||
*/
|
||
Select.prototype._onchange = function() {
|
||
//console.log('on change:')
|
||
//this.emit('select', this.getSelected())
|
||
let item = this.getSelected()
|
||
if (!item) {
|
||
this.emit('select')
|
||
} else if (this._key) {
|
||
this.emit('select', item[this._key])
|
||
} else {
|
||
this.emit('select', item)
|
||
}
|
||
}
|
||
|
||
Select.prototype.select = function(id) {
|
||
//console.log('SELECT select:', id)
|
||
//console.log("select.select:", this._select.value, id)
|
||
this._select.value = id
|
||
this._select.dispatchEvent(new Event('change'))
|
||
}
|
||
|
||
Select.prototype.getList = function() {
|
||
return this._arr
|
||
}
|
||
|
||
////////// RADIO LIST
|
||
|
||
function RadioList(opt) {
|
||
Emiter.call(this)
|
||
/*
|
||
let required = ['templateId', 'container', 'key']
|
||
|
||
required.forEach((option) => {
|
||
if (!opt[option]) {
|
||
throw new Error(`'${option}' option is required`)
|
||
}
|
||
})
|
||
*/
|
||
|
||
this._errorClass = opt.errorClass || 'errmsg'
|
||
this._setEmptyMessage = opt.setEmptyMessage
|
||
|
||
if (opt.templateId) {
|
||
let renders = {}
|
||
renders[opt.templateId] = (tags) => {
|
||
if (!tags.radio) {
|
||
return
|
||
}
|
||
tags.radio.setAttribute('type', 'radio')
|
||
let idx = this._buttons.length
|
||
this._buttons.push(tags.radio)
|
||
tags.radio.addEventListener('click', () => this._onclick(idx))
|
||
}
|
||
this._template = new Template({
|
||
id: opt.templateId,
|
||
renders: renders,
|
||
})
|
||
} else {
|
||
|
||
}
|
||
|
||
this._container = getElemSafe('container', opt.container)
|
||
if (opt.cover) {
|
||
this._cover = getElemSafe('cover', opt.cover)
|
||
} else {
|
||
this._cover = this._container
|
||
}
|
||
|
||
this._key = opt.key
|
||
this._buttons = []
|
||
this._arr = []
|
||
}
|
||
|
||
RadioList.prototype = Object.create(Emiter.prototype)
|
||
RadioList.prototype.constructor = RadioList
|
||
|
||
RadioList.prototype.getActionType = function() {
|
||
return 'select'
|
||
}
|
||
|
||
RadioList.prototype.getType = function() {
|
||
return 'select'
|
||
}
|
||
|
||
RadioList.prototype.getAnchor = function() {
|
||
return this._cover
|
||
}
|
||
|
||
RadioList.prototype.getCover = function() {
|
||
return this._cover
|
||
}
|
||
|
||
RadioList.prototype.disable = function() {
|
||
//console.log('DISABLE RADIO', this._buttons)
|
||
this._buttons.forEach((radio) => {
|
||
radio.disabled = true
|
||
})
|
||
}
|
||
|
||
RadioList.prototype.enable = function() {
|
||
//console.log('ENABLE RADIO', this._buttons)
|
||
this._buttons.forEach((radio) => {
|
||
radio.disabled = false
|
||
})
|
||
}
|
||
|
||
RadioList.prototype.clear = function() {
|
||
//console.log('CLEAR radio:')
|
||
this._currentIdx = undefined
|
||
this._container.innerHTML = ''
|
||
this._buttons = []
|
||
this._arr = []
|
||
if (this._messageDiv) {
|
||
this._messageDiv.remove()
|
||
}
|
||
if (this._divLoadError) {
|
||
this._divLoadError.remove()
|
||
}
|
||
}
|
||
|
||
RadioList.prototype.setList = function(list) {
|
||
//console.log('setList radio:', list)
|
||
this.clear()
|
||
if (Array.isArray(list)) {
|
||
this._arr = list
|
||
this._template.renderTo(list, this._container)
|
||
} else {
|
||
if (this._setEmptyMessage) {
|
||
let div = document.createElement('div')
|
||
div.textContent = this._setEmptyMessage
|
||
|
||
this._messageDiv = div
|
||
|
||
this._cover.parentNode.insertBefore(div, this._cover.nextSibling)
|
||
}
|
||
}
|
||
}
|
||
|
||
RadioList.prototype.getList = function() {
|
||
return this._arr
|
||
}
|
||
|
||
RadioList.prototype.select = function(id) {
|
||
//console.log('SELECT radio:', id, this._key, this._arr)
|
||
//console.log(this._arr)
|
||
this._arr.forEach((obj, idx) => {
|
||
if (id === obj[this._key]) {
|
||
this._onclick(idx)
|
||
return
|
||
}
|
||
})
|
||
}
|
||
|
||
RadioList.prototype.getSelected = function() {
|
||
if (this._currentIdx !== undefined) {
|
||
return this._arr[this._currentIdx]
|
||
}
|
||
}
|
||
|
||
|
||
/*
|
||
RadioList.prototype.setError = function(err) {
|
||
return insertError({
|
||
after: this._cover,
|
||
err: err,
|
||
class: this._errorClass
|
||
})
|
||
}
|
||
*/
|
||
|
||
RadioList.prototype._onclick = function(idx) {
|
||
//console.log('radioList select:', idx, this._currentIdx, this._buttons)
|
||
let radio = this._buttons[idx]
|
||
let obj = this._arr[idx]
|
||
let id = obj[this._key]
|
||
|
||
if (this._currentIdx === idx) {
|
||
// снимаем отметку
|
||
radio.checked = false
|
||
this._currentIdx = undefined
|
||
this.emit('select')
|
||
} else {
|
||
let prev = this._buttons[this._currentIdx]
|
||
if (prev) {
|
||
prev.checked = false
|
||
}
|
||
radio.checked = true
|
||
this._currentIdx = idx
|
||
this.emit('select', id)
|
||
}
|
||
}
|
||
/////////////////////////
|
||
|
||
|
||
function ReadonlyInput(opt) {
|
||
Emiter.call(this)
|
||
|
||
this._key = opt.key
|
||
this._name = opt.name
|
||
this._input = getElemSafe('input', opt.input)
|
||
//this._input.addEventListener('input', () => this.emit('changed'))
|
||
this._errorClass = opt.errorClass || 'errmsg'
|
||
}
|
||
|
||
ReadonlyInput.prototype = Object.create(Emiter.prototype)
|
||
ReadonlyInput.prototype.constructor = ReadonlyInput
|
||
|
||
ReadonlyInput.prototype.getActionType = function() {
|
||
return 'input'
|
||
}
|
||
|
||
ReadonlyInput.prototype.getType = function() {
|
||
return 'input'
|
||
}
|
||
|
||
ReadonlyInput.prototype.disable = function() {
|
||
this._input.disabled = true
|
||
}
|
||
|
||
ReadonlyInput.prototype.enable = function() {
|
||
this._input.disabled = false
|
||
}
|
||
|
||
ReadonlyInput.prototype.reset = function() {
|
||
this._input.value = undefined
|
||
this.emit('change')
|
||
}
|
||
|
||
ReadonlyInput.prototype.clear = function() {
|
||
this._input.value = ''
|
||
this._value = undefined
|
||
this.emit('change')
|
||
}
|
||
|
||
ReadonlyInput.prototype.getValue = function() {
|
||
return this._value
|
||
}
|
||
|
||
ReadonlyInput.prototype.setValue = function(value) {
|
||
// console.log('setValue:', value)
|
||
if (value === undefined) {
|
||
this._value = undefined
|
||
this._input.value = ''
|
||
this.emit('change')
|
||
} else {
|
||
this._value = value
|
||
if (this._key) {
|
||
this.emit('change', value[this._key])
|
||
} else {
|
||
this.emit('change', value)
|
||
}
|
||
if (this._name) {
|
||
this._input.value = value[this._name]
|
||
} else {
|
||
this._input.value = value
|
||
}
|
||
}
|
||
}
|
||
|
||
ReadonlyInput.prototype.getAnchor = function() {
|
||
return this._input
|
||
}
|
||
|
||
/*
|
||
ReadonlyInput.prototype.setError = function(err) {
|
||
return insertError({
|
||
after: this._input,
|
||
err: err,
|
||
class: this._errorClass
|
||
})
|
||
}
|
||
*/
|
||
|
||
////////////////////////
|
||
|
||
function InputList(opt) {
|
||
Emiter.call(this)
|
||
|
||
if (!opt.templateId) {
|
||
throw new Error("option 'templateId' is required")
|
||
}
|
||
let renders = {}
|
||
renders[opt.templateId] = (tags) => {
|
||
if (!tags.input) {
|
||
return
|
||
}
|
||
this._inputs.push(tags.input)
|
||
tags.input.addEventListener('input', () => {
|
||
|
||
this.emit('changed')
|
||
})
|
||
}
|
||
this._template = new Template({
|
||
id: opt.templateId,
|
||
renders: renders,
|
||
})
|
||
|
||
this._container = getElemSafe('container', opt.container)
|
||
if (opt.cover) {
|
||
this._cover = getElemSafe('cover', opt.cover)
|
||
} else {
|
||
this._cover = this._container
|
||
}
|
||
this._errorClass = opt.errorClass || 'errmsg'
|
||
this._inputs = []
|
||
|
||
this._setEmptyMessage = opt.setEmptyMessage
|
||
}
|
||
|
||
InputList.prototype = Object.create(Emiter.prototype)
|
||
InputList.prototype.constructor = InputList
|
||
|
||
InputList.prototype.getType = function() {
|
||
return 'list'
|
||
}
|
||
|
||
|
||
InputList.prototype.disable = function() {
|
||
this._inputs.forEach((input) => {
|
||
input.disabled = true
|
||
})
|
||
}
|
||
|
||
InputList.prototype.enable = function() {
|
||
this._inputs.forEach((input) => {
|
||
input.disabled = false
|
||
})
|
||
}
|
||
|
||
|
||
InputList.prototype.reset = function() {
|
||
// FIX
|
||
}
|
||
|
||
InputList.prototype.clear = function() {
|
||
this._container.innerHTML = ''
|
||
this._inputs = []
|
||
if (this._messageDiv) {
|
||
this._messageDiv.remove()
|
||
}
|
||
}
|
||
|
||
InputList.prototype.setList = function(list) {
|
||
//console.log('INPUT LIST: setList', list)
|
||
if (Array.isArray(list)) {
|
||
this._template.renderTo(list, this._container)
|
||
} else {
|
||
if (this._setEmptyMessage) {
|
||
let div = document.createElement('div')
|
||
div.textContent = this._setEmptyMessage
|
||
|
||
this._messageDiv = div
|
||
|
||
this._cover.parentNode.insertBefore(div, this._cover.nextSibling)
|
||
}
|
||
}
|
||
}
|
||
|
||
InputList.prototype.getList = function() {
|
||
//console.log('getList')
|
||
return []
|
||
}
|
||
|
||
InputList.prototype.getValues = function() {
|
||
//console.log('getValues')
|
||
let arr = []
|
||
this._inputs.forEach((input) => {
|
||
arr.push(input.value)
|
||
})
|
||
return arr
|
||
}
|
||
|
||
InputList.prototype.setValues = function(values) {
|
||
//console.log('setValues')
|
||
this._inputs.forEach((input, idx) => {
|
||
input.value = values[idx]
|
||
})
|
||
}
|
||
|
||
/*
|
||
InputList.prototype.setError = function(err, idx) {
|
||
let anchor = this._inputs[idx]
|
||
return insertError({
|
||
after: anchor,
|
||
err: err,
|
||
class: this._errorClass
|
||
})
|
||
}
|
||
*/
|
||
|
||
InputList.prototype.getAnchor = function(idx) {
|
||
if (idx !== undefined ) {
|
||
return this._inputs[idx] // fix out of range
|
||
}
|
||
return this._cover
|
||
}
|
||
|
||
InputList.prototype.getCover = function() {
|
||
return this._cover
|
||
}
|
||
|
||
////////// CHECK LIST
|
||
|
||
function CheckList(opt) {
|
||
Emiter.call(this)
|
||
|
||
this._errorClass = opt.errorClass || 'errmsg'
|
||
|
||
if (!opt.templateId) {
|
||
throw new Error("option 'templateId' is required")
|
||
}
|
||
let renders = {}
|
||
renders[opt.templateId] = (tags) => {
|
||
if (!tags.checkbox) {
|
||
return
|
||
}
|
||
tags.checkbox.setAttribute('type', 'checkbox')
|
||
let idx = this._buttons.length
|
||
this._buttons.push(tags.checkbox)
|
||
tags.checkbox.addEventListener('click', () => this._onclick(idx))
|
||
}
|
||
this._template = new Template({
|
||
id: opt.templateId,
|
||
renders: renders,
|
||
})
|
||
|
||
this._container = getElemSafe('container', opt.container)
|
||
if (opt.cover) {
|
||
this._cover = getElemSafe('cover', opt.cover)
|
||
} else {
|
||
this._cover = this._container
|
||
}
|
||
|
||
if (!opt.key) {
|
||
throw new Error("option 'key' is required")
|
||
}
|
||
|
||
this._key = opt.key
|
||
this._buttons = []
|
||
this._arr = []
|
||
}
|
||
|
||
CheckList.prototype = Object.create(Emiter.prototype)
|
||
CheckList.prototype.constructor = CheckList
|
||
|
||
CheckList.prototype.getType = function() {
|
||
return 'list'
|
||
}
|
||
|
||
CheckList.prototype.getActionType = function() {
|
||
return 'input'
|
||
}
|
||
|
||
|
||
CheckList.prototype.disable = function() {
|
||
//console.log('DISABEL RADIO', this._buttons)
|
||
this._buttons.forEach((c) => {
|
||
c.disabled = true
|
||
})
|
||
}
|
||
|
||
CheckList.prototype.enable = function() {
|
||
//console.log('DISABEL RADIO', this._buttons)
|
||
this._buttons.forEach((c) => {
|
||
c.disabled = false
|
||
})
|
||
}
|
||
|
||
CheckList.prototype.reset = function() {
|
||
this._buttons.forEach((checkbox) => {
|
||
checkbox.checked = false
|
||
})
|
||
}
|
||
|
||
CheckList.prototype.clear = function() {
|
||
//console.log('CLEAR radio:')
|
||
this._currentIdx = undefined
|
||
this._container.innerHTML = ''
|
||
this._buttons = []
|
||
this._arr = []
|
||
|
||
if (this._messageDiv) {
|
||
this._messageDiv.remove()
|
||
}
|
||
if (this._divLoadError) {
|
||
this._divLoadError.remove()
|
||
}
|
||
}
|
||
|
||
CheckList.prototype.setList = function(list) {
|
||
|
||
this.clear()
|
||
if (Array.isArray(list)) {
|
||
this._arr = list
|
||
this._template.renderTo(list, this._container)
|
||
} else {
|
||
if (this._setEmptyMessage) {
|
||
let div = document.createElement('div')
|
||
div.textContent = this._setEmptyMessage
|
||
|
||
this._messageDiv = div
|
||
|
||
this._container.parentNode.insertBefore(div, this._container.nextSibling)
|
||
}
|
||
}
|
||
}
|
||
|
||
CheckList.prototype.getList = function() {
|
||
return this._arr
|
||
}
|
||
|
||
CheckList.prototype.setValues = function(ids) {
|
||
console.log('SET VALUES:', ids, this._arr)
|
||
if (!Array.isArray(ids)) {
|
||
return
|
||
}
|
||
ids.forEach(id => {
|
||
this._arr.forEach((obj, idx) => {
|
||
if (obj[this._key] === id) {
|
||
this._buttons[idx].checked = true
|
||
}
|
||
})
|
||
})
|
||
this.emit('changed')
|
||
}
|
||
|
||
CheckList.prototype.getValues = function() {
|
||
let r = []
|
||
this._arr.forEach((obj, idx) => {
|
||
if (this._buttons[idx].checked) {
|
||
r.push(obj[this._key])
|
||
}
|
||
})
|
||
console.log('GET VALUES:', r)
|
||
return r
|
||
}
|
||
// getAnchor - возвращает HTML Element, к которому можно привязать ошибку.
|
||
// Если idx не указан или ошибочный чекбокс не будет найден - вернет ссылку на cover.
|
||
// Иначе вернет ссылку на родительский элемент для ошибочного чекбокса, предполагается
|
||
// что это будет LABEL.
|
||
// Idx считается только для отмеченных чекбоксов. То есть idx=1 будет указывать на 2й
|
||
// отмеченный чекбокс.
|
||
CheckList.prototype.getAnchor = function(idx) {
|
||
if (idx === undefined) {
|
||
return this._cover
|
||
}
|
||
let checkedIdx = 0
|
||
for (let i=0; i<this._buttons.length; i++) {
|
||
let checkbox = this._buttons[i]
|
||
if (checkbox.checked) {
|
||
if (idx === checkedIdx) {
|
||
return checkbox.parentNode
|
||
}
|
||
checkedIdx++
|
||
}
|
||
}
|
||
return this._cover
|
||
}
|
||
|
||
/*
|
||
CheckList.prototype.setError = function(err, idx) {
|
||
if (idx !== undefined) {
|
||
// ссылка на label, а не checkbox
|
||
let elem = this._buttons[idx].parentNode
|
||
return insertError({
|
||
after: elem,
|
||
err: err,
|
||
class: this._errorClass
|
||
})
|
||
}
|
||
return insertError({
|
||
after: this._cover,
|
||
err: err,
|
||
class: this._errorClass
|
||
})
|
||
}
|
||
*/
|
||
|
||
CheckList.prototype.getCover = function() {
|
||
return this._cover
|
||
}
|
||
|
||
CheckList.prototype._onclick = function(idx) {
|
||
this.emit('changed')
|
||
}
|
||
|
||
///////////////////// URL
|
||
|
||
// Добавляем в URL переменную, не трогая остальные
|
||
function setParamToURL(name, value) {
|
||
// по умолчанию state = null
|
||
if (history.state) {
|
||
// Позволяет устранить проблему: когда юзер выбирает пункт в select и когда
|
||
// программа выбирает пункт при обработке popstate события (юзер нажал
|
||
// кнопку Назад) - происходит зацикливание истории (кнопка назад срабатывает
|
||
// 1 раз и затем страница не меняется).
|
||
if (history.state[name] === value) {
|
||
return
|
||
}
|
||
}
|
||
const u = new URL(location.href)
|
||
let usp = new URLSearchParams(u.searchParams)
|
||
if (value === undefined) {
|
||
// например, выбрали в select пустой элемент
|
||
usp.delete(name)
|
||
} else {
|
||
usp.set(name, value)
|
||
}
|
||
let state = {}
|
||
state[name] = value
|
||
// если склеивать path без условных проверок - получим путь вида /configs?
|
||
// при сбросе параметра (value = undefined)
|
||
let path = u.pathname
|
||
const searchParams = usp.toString()
|
||
if (searchParams !== '') {
|
||
path += '?' + searchParams
|
||
}
|
||
path += u.hash
|
||
history.pushState(state, "", path)
|
||
}
|
||
|
||
function getParamFromURL(name, dataType) {
|
||
const u = new URL(location.href)
|
||
let usp = new URLSearchParams(u.searchParams)
|
||
if (!dataType) {
|
||
dataType = 'str'
|
||
}
|
||
let v;
|
||
switch (dataType) {
|
||
case 'str':
|
||
v = usp.get(name) || ''
|
||
break
|
||
|
||
case 'int':
|
||
v = usp.get(name)
|
||
v = parseInt(v) || 0
|
||
break
|
||
|
||
case 'float':
|
||
v = usp.get(name)
|
||
v = parseFloat(v) || 0
|
||
|
||
case 'bool':
|
||
v = usp.get(name)
|
||
if (v == 'on') {
|
||
v = true
|
||
} else {
|
||
v = false
|
||
}
|
||
break
|
||
|
||
default:
|
||
throw new Error(`Unknown type: ${dataType}`)
|
||
}
|
||
return v
|
||
}
|
||
|
||
/////////////////
|
||
|
||
function insertError(opt) {
|
||
let div = document.createElement('div')
|
||
if (opt.class) {
|
||
div.className = opt.class
|
||
}
|
||
let msg;
|
||
if (typeof opt.err == 'string') {
|
||
msg = opt.err
|
||
} else {
|
||
msg = opt.err.message
|
||
}
|
||
div.textContent = msg
|
||
|
||
if (opt.after) {
|
||
opt.after.parentNode.insertBefore(div, opt.after.nextSibling)
|
||
} else if (opt.before) {
|
||
// например перед submit кнопкой
|
||
opt.before.parentNode.insertBefore(div, opt.before)
|
||
}
|
||
return div
|
||
}
|
||
|
||
function insertMessage(opt) {
|
||
let div = document.createElement('div')
|
||
if (opt.class) {
|
||
div.className = opt.class
|
||
}
|
||
div.textContent = opt.msg
|
||
|
||
if (opt.after) {
|
||
opt.after.parentNode.insertBefore(div, opt.after.nextSibling)
|
||
} else if (opt.before) {
|
||
// например перед submit кнопкой
|
||
opt.before.parentNode.insertBefore(div, opt.before)
|
||
}
|
||
return div
|
||
}
|
||
|
||
//////////////////
|
||
|
||
function getElemSafe(name, opt) {
|
||
if (opt instanceof Element) {
|
||
return opt
|
||
}
|
||
if (typeof opt == 'string') {
|
||
let elem = document.getElementById(opt)
|
||
if (!elem) {
|
||
throw new Error(`'${name}' option not found in DOM by id=${opt}`)
|
||
}
|
||
return elem
|
||
}
|
||
throw new Error(`'${name}' option must be Element or id string, not ${JSON.stringify(opt)}`)
|
||
}
|
||
|
||
/*
|
||
1. Все параметры создаются вместе с конфигом через админку http://167.99.157.181:8081/configs в разделе Field Set Configs.
|
||
2. Конфиг и параметры редактировать нельзя, Вы можете привязыватся к имени параметра без проблем.
|
||
После создания имя изменить нельзя, конфиг (таблица configs) отредактировать нельзя.
|
||
Если создан конфиг, например, с 3 параметрами, имена и количество
|
||
|
||
Создавать таблицу где имя параметра будет именем колонки?
|
||
Если я буду программно добавлять/удалять колонки (вместо строк) - как-то нереляционно.
|
||
Если вы предполагаете что все параметры можно предусмотреть заранее - это ошибка.
|
||
Завтра будут новые приборы, новые конфиги и новые параметры. Делать для каждого
|
||
нового параметра вручную ALTER TABLE ADD COLUMN - неправильно.
|
||
*/
|
||
|
||
|
||
|