hap-homematic
Version:
provides a homekit bridge to the ccu
929 lines (814 loc) • 22.8 kB
JavaScript
/*
* File: ui.js
* Project: hap-homematic
* File Created: Sunday, 15th March 2020 8:46:03 pm
* Author: Thomas Kluge (th.kluge@me.com)
* -----
* The MIT License (MIT)
*
* Copyright (c) Thomas Kluge <th.kluge@me.com> (https://github.com/thkl)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
* ==========================================================================
*/
export class Label {
constructor (label) {
this.label = $('<div>')
if (label) {
this.label.append(label)
}
}
setStyle (style) {
this.label.attr('style', style)
}
setLabel (label) {
this.label.empty()
this.label.append(label)
}
render () {
return this.label
}
}
export class Spinner {
constructor () {
this.spinner = $('<div>').attr('role', 'status')
}
setActive (active) {
if (active === true) {
this.spinner.addClass('spinner-border')
} else {
this.spinner.removeClass('spinner-border')
}
}
render () {
return this.spinner
}
}
export class Button {
constructor (type, title, onClick, enabled = true) {
this.isActive = enabled
this.button = $('<button>')
this.button.attr('type', 'button')
this.button.addClass('btn')
this.button.addClass('btn-' + type)
if (this.isActive) {
this.button.addClass('active')
} else {
this.button.attr('disabled', 'disabled')
}
this.button.on('click', (e) => {
onClick(e, e.target)
})
this.button.append(title)
}
setStyle (style) {
this.button.attr('style', style)
}
setLabel (lbl) {
this.button.empty()
this.button.append(lbl)
}
setActive (isActive) {
this.isActive = isActive
if (this.isActive) {
this.button.removeAttr('disabled')
this.button.addClass('active')
} else {
this.button.removeClass('active')
this.button.attr('disabled', 'disabled')
}
}
render () {
return this.button
}
}
export class Input {
constructor (id, value, onChange) {
this.input = $('<input>').attr('id', id)
if (onChange) {
this.input.bind('change', (e) => {
onChange(e, e.target)
})
}
this.input.addClass('form-control')
this.input.val(value)
}
setStyle (style) {
this.style = style
if (this.container) {
this.container.attr('style', this.style)
}
}
setLabel (label) {
this.label = $('<label>').attr('for', this.id)
this.label.append(label)
}
setGroupLabel (label) {
this.label = $('<div>').addClass('input-group-prepend')
this.label.attr('style', 'width:100%')
let span = $('<span>').addClass('input-group-text').append(label)
this.label.append(span)
this.isGrouped = true
}
setEnabled (enabled) {
if (enabled) {
this.input.removeAttr('disabled', 'disabled')
} else {
this.input.attr('disabled', 'disabled')
}
}
getFiles(id) {
if (this.inputType === 'file') {
return this.input[0].files[id]
}
return undefined
}
setType (inputType) {
this.inputType = inputType
}
setValue (newValue) {
this.input.val(newValue)
}
getValue () {
return this.input.val()
}
render () {
if (this.label) {
if (this.isGrouped === true) {
this.container = $('<div>').addClass('input-group')
this.container.append(this.label)
this.label.append(this.input)
} else {
this.container = $('<div>').append(this.label).append(this.input)
}
if (this.style) {
this.container.attr('style', this.style)
}
if (this.inputType) {
this.input.attr('type',this.inputType)
}
return this.container
} else {
return this.input
}
}
}
export class CheckBox extends Input {
constructor (id, value, onChange) {
super(id, 'true', onChange)
this.input.attr('type', 'checkbox')
this.setValue(value)
}
setLabel (label) {
this.label = $('<label>').attr('for', this.id)
this.label.addClass('form-check-label')
this.input.removeClass('form-control')
this.input.addClass('form-check-input')
this.label.append(label)
}
setValue (newValue) {
if (newValue === true) {
this.input.attr('checked', 'checked')
} else {
this.input.removeAttr('checked')
}
}
getValue () {
return (this.input.attr('checked') === 'checked')
}
render () {
if (this.label) {
let result = $('<div>').append(this.input).append(this.label)
result.addClass('form-check')
return result
} else {
return this.input
}
}
}
export class ButtonInput {
constructor (id, value, title, onChange, onButton) {
this.container = $('<div>').addClass('input-group mb-3')
this.input = $('<input>').attr('id', id)
if (onChange) {
this.input.bind('change', (e) => {
onChange(e, e.target)
})
}
this.input.addClass('form-control')
this.input.val(value)
this.container.append(this.input)
this.inputGroupAppend = $('<div>').addClass('input-group-append')
let button = $('<button>').addClass('btn btn-secondary').attr('type', 'button').append(title)
button.bind('click', (e) => {
onButton(e, e.target)
})
this.inputGroupAppend.append(button)
this.container.append(this.inputGroupAppend)
}
setLabel (label) {
this.label = $('<label>').attr('for', this.id)
this.label.append(label)
}
setValue (newValue) {
this.input.val(newValue)
this.input.change() // trigger the change event
}
getValue () {
return this.input.val()
}
setStyle (style) {
this.style = style
if (this.container) {
this.container.attr('style', this.style)
}
}
setGroupLabel (label) {
this.label = $('<div>').addClass('input-group-prepend')
let span = $('<span>').addClass('input-group-text').append(label)
this.label.append(span)
this.isGrouped = true
}
addButton(button) {
if (this.inputGroupAppend) {
this.inputGroupAppend.append(button.render())
}
}
render () {
if (this.label) {
if (this.isGrouped === true) {
this.container = $('<div>').addClass('input-group')
this.container.append(this.label)
this.container.append(this.input)
} else {
this.container = $('<div>').addClass('input-group').append(this.label).append(this.input)
}
if (this.style) {
this.container.attr('style', this.style)
}
if (this.inputType) {
this.input.attr('type',this.inputType)
}
if (this.inputGroupAppend) {
this.container.append(this.inputGroupAppend)
this.input.attr('style', 'width:60%')
}
return this.container
} else {
return this.container
}
}
}
export class Dropdown {
constructor (id, title, onClick) {
this.id = id
this.items = []
this.dropDown = $('<div>').addClass('btn-group').attr('style', ' width: 100%;')
this.button = $('<button>').attr('class', 'btn btn-secondary dropdown-toggle').attr('type', 'button')
this.button.attr('id', 'dropdownMenuButton_' + this.id)
this.button.attr('data-toggle', 'dropdown')
this.button.attr('aria-expanded', false)
this.button.append(title)
this.dropDown.append(this.button)
this.dropDownItems = $('<div>').addClass('dropdown-menu').attr('aria-labelledby', 'dropdownMenuButton_' + this.id)
this.dropDown.append(this.dropDownItems)
this.button.bind('click', (e) => {
onClick(e)
})
}
setTitle (newTitle) {
this.button.empty()
this.button.append(newTitle)
}
reset() {
this.dropDownItems.empty()
}
addItem (item) {
let self = this
let mItem = $('<button>').addClass('dropdown-item').attr('type', 'button').attr('data-value', item.value).append(item.title)
mItem.attr('data-title', item.title)
mItem.bind('click', (e) => {
self.setTitle(e.target.getAttribute('data-title'))
if (item.onClick) {
item.onClick(e, e.target.getAttribute('data-value'))
}
})
this.dropDownItems.append(mItem)
}
render () {
return this.dropDown
}
}
export class Pagination {
constructor (parent) {
this.parent = parent
this.pages = []
this.maxPages = 8
this.curStartPage = 0
}
addPage (id, isActive, title, start, onClick) {
this.pages.push({id: id, isActive: isActive, title: title, start: start, onClick: onClick})
}
reset() {
this.pages = []
}
setParent (parent) {
this.parent = parent
}
setActivePage (pageNum) {
this.activePage = pageNum
this.pages.map(page => {
page.isActive = (page.id === pageNum)
})
this.render()
}
reset () {
this.pages = []
this.curStartPage = 0
}
renderPage (num, title, active, enabled, callback) {
let oLi = $('<li>').addClass('page-item')
if (enabled) {
oLi.attr('disabled', 'disabled')
}
if (active) {
oLi.addClass('active')
}
let oAnc = $('<a>').addClass('page-link')
oAnc.on('click', (e) => {
if (callback) {
callback(e, num)
}
})
oAnc.attr('style', 'cursor:pointer')
oAnc.append(title)
oLi.append(oAnc)
return oLi
}
render () {
let self = this
this.parent.empty()
let canvas = this.parent
if (!canvas.is('ul')) {
canvas = $('<ul>')
canvas.addClass('pagination')
this.parent.append(canvas)
}
let lastPage = this.pages.length
if (this.pages.length > this.maxPages) {
let active = (this.curStartPage !== 0)
let opage = this.renderPage(-1, (this.prevTitle || 'Previous'), false, active, (e, num) => {
if (self.curStartPage > 0) {
self.curStartPage = self.curStartPage - 1
}
self.render()
})
canvas.append(opage)
lastPage = this.curStartPage + this.maxPages
}
for (var i = this.curStartPage; i < lastPage; i++) {
let page = this.pages[i]
if (page) {
let opage = this.renderPage(page.id, page.title, page.isActive, true, (e, num) => {
page.onClick(e, page)
})
canvas.append(opage)
}
}
if (this.pages.length > this.maxPages) {
let active = (this.curStartPage !== 0)
let opage = this.renderPage(-1, (this.NextTitle || 'Next'), false, active, (e, num) => {
if (self.curStartPage < (self.pages.length - self.maxPages)) {
self.curStartPage = self.curStartPage + 1
}
self.render()
})
canvas.append(opage)
}
}
}
export class Dialog {
constructor (settings) {
this.dialogId = settings.dialogId
this.dialog = $('<div>').attr('class', 'modal fade').attr('tabindex', '-1').attr('role', 'dialog').attr('id', settings.dialogId)
let dDocument = $('<div>').attr('class', 'modal-dialog').attr('role', 'document')
if (settings.dialogClass) {
dDocument.addClass(settings.dialogClass)
}
if (settings.size) {
dDocument.addClass(settings.size)
} else {
dDocument.addClass('modal-lg')
}
if (settings.scrollable) {
dDocument.addClass('modal-dialog-scrollable')
}
this.dialog.append(dDocument)
let dContent = $('<div>').addClass('modal-content')
dDocument.append(dContent)
let dHeader = $('<div>').addClass('modal-header')
dContent.append(dHeader)
if (settings.title) {
let dTitle = $('<h5>').addClass('modal-title').attr('id', settings.dialogId + '_title')
dTitle.append(settings.title)
dHeader.append(dTitle)
}
let dCloseButton = $('<button>').attr('type', 'button').attr('class', 'close').attr('data-dismiss', 'modal')
if (settings.labelClose) {
dCloseButton.attr('aria-label', settings.labelClose)
} else {
dCloseButton.attr('aria-label', 'Close')
}
dCloseButton.append($('<span>').attr('aria-hidden', 'true').append('×'))
dHeader.append(dCloseButton)
this.body = $('<div>').addClass('modal-body').attr('id', settings.dialogId + '_content')
dContent.append(this.body)
let dFooter = $('<div>').addClass('modal-footer')
dContent.append(dFooter)
if (settings.buttons) {
settings.buttons.map(button => {
if (button) {
dFooter.append(button.render())
}
})
}
}
setBody (item) {
let self = this
this.body.empty()
if (typeof item === 'object') {
if (Array.isArray(item)) {
item.map(iitem => {
self.body.append(iitem)
})
} else {
this.body.append(item)
}
} else {
this.body.append(item)
}
}
open () {
let self = this
$('body').append(this.dialog)
$('#' + this.dialogId)
.on('hide.bs.modal', (e) => {
if (self.beforeClose) {
self.beforeClose()
}
})
$('#' + this.dialogId)
.on('hidden.bs.modal', (e) => {
setTimeout(() => {
self.dialog.remove()
}, 50)
})
this.dialog.modal()
}
close () {
this.dialog.modal('hide')
}
}
export class GridRow {
constructor (id, settings = {}) {
this.id = id
this.cells = []
this.settings = settings
}
addCell (szData, content, clazz,id = 'c1') {
var cContent = content
if (typeof content !== 'object') {
cContent = $('<span>').append(content)
}
this.cells.push({id:this.id + '_' + id, sz: szData, content: cContent, clazz: clazz})
return cContent
}
setClasses (classNames) {
this.cssClazzName = classNames
}
hide() {
this.domObj.hide()
}
render () {
let self = this
this.domObj = $('<div>').addClass('row')
if ((this.settings) && (this.settings.rowStyle)) {
this.domObj.attr('style', this.settings.rowStyle)
}
if (this.cssClazzName) {
this.cssClazzName.split(' ').map(clazz => {
this.domObj.addClass(clazz)
})
}
this.cells.forEach(cell => {
let oCell = $('<div>')
if (cell.id) {
oCell.attr('id',cell.id)
}
if (cell.sz) {
oCell.addClass('col-sm-' + (cell.sz.sm || 12))
oCell.addClass('col-md-' + (cell.sz.md || 6))
oCell.addClass('col-lg-' + (cell.sz.lg || 3))
oCell.addClass('col-xl-' + (cell.sz.xl || cell.sz.lg || 3))
} else {
oCell.addClass('col-auto')
}
oCell.append(cell.content)
self.domObj.append(oCell)
})
return this.domObj
}
}
export class Grid {
constructor (id, settings = {}) {
this.id = id
this.rows = []
this.settings = settings
this.canvas = $('<div>')
}
resetRows () {
this.rows = []
this.canvas.empty()
}
addRow (id, rowSettings) {
if (rowSettings === undefined) {
rowSettings = this.settings
}
let row = new GridRow(this.id + '_' + id, rowSettings)
this.rows.push(row)
return row
}
render () {
let self = this
this.rows.map(row => {
self.canvas.append(row.render())
})
return this.canvas
}
}
export class DatabaseGrid extends Grid {
constructor (id, dataset, settings = {}) {
super(id, settings)
this.dataset = dataset
this.curRecord = 0
this.maxRecords = 10
this.pagination = new Pagination()
this.pagination.maxPages = settings.maxPages || 8
this.gridContainer = $('<div>')
this.columnSort = -1
this.sortReverse = false
}
setColumns (colums) {
this.columns = colums
}
setBeforeQuery(beforeQuery) {
this.beforeQuery = beforeQuery
}
setTitleLabels (titleLabels) {
this.titleLabels = titleLabels
}
addSearchBar (label, clearButtonTitle, filterCallback) {
let self = this
this.searchInput = new ButtonInput(this.id + '_search', '', clearButtonTitle, (e, input) => {
self.filter = input.value
self.pagination.reset()
self.renderPagination(0)
self.updateBody()
}, (e, btn) => {
self.searchInput.setValue('')
self.filter = ''
self.pagination.reset()
self.renderPagination(0)
self.updateBody()
})
if (label) {
this.searchInput.setGroupLabel(label)
}
this.filterCallback = filterCallback
}
resetSearch() {
if (this.searchInput) {
this.searchInput.setValue('')
this.filter = ''
this.pagination.reset()
this.renderPagination(0)
this.updateBody()
}
}
getDataset () {
let self = this
if (this.beforeQuery) {
this.beforeQuery()
}
// first try to ask the delegate for data
if (this.dataset === undefined) {
this.requestRefresh()
}
if (this.dataset === undefined) {
return []
}
if ((this.filter !== undefined) && (this.filterCallback)) {
return this.dataset.filter((element) => {
return self.filterCallback(element, self.filter)
})
} else {
return this.dataset
}
}
getSortedDataset () {
let self = this
let ds = this.getDataset()
if ((this.columnSort > -1) && (this.sortCallback)) {
let sds = ds.sort((a, b) => {
return self.sortCallback(this.columnSort, a, b)
})
return (self.sortReverse ? sds.reverse() : sds)
}
return ds
}
requestRefresh() {
if (this.getDataset !== undefined) {
// this.dataset = this.getDataset()
if (this.beforeQuery) {
this.beforeQuery()
}
}
}
setDataset (dataset) {
this.dataset = dataset
this.curRecord = 0
this.maxRecords = 10
this.updateBody()
}
setRenderer (callback) {
this.renderder = callback
}
setMaxDataRecords (max) {
this.maxRecords = max
}
prepare () {
if (this.searchInput) {
this.gridContainer.append(this.searchInput.render())
}
if (!this.footer) {
this.footer = $('<div>')
}
let divPr = $('<div>').addClass('row')
divPr.attr('style', 'margin-top:15px')
this.footer.append(divPr)
// only add a pager if there are more records than maxRecords
let divPc = $('<div>').addClass('col-auto')
divPr.append(divPc)
this.pagination.setParent(divPc)
this.renderPagination(0)
this.canvas.attr('style', 'margin:15px')
}
sortGridByColumn (col) {
this.columnSort = col
this.updateBody()
}
updateBody () {
// loop thru the first Datasets
let self = this
this.resetRows()
let dataset = this.getSortedDataset()
let max = this.curRecord + this.maxRecords
if (max > dataset.length) {
max = dataset.length
}
var i = 0
var j = 0
// add the titleRow
let row = super.addRow()
row.setClasses('dbgridtitle')
if (this.columns) {
this.columns.map(column => {
let col = row.addCell(column.sz, self.titleLabels[j] || '')
// add a sortable label if column is sortabel
if (column.sort !== undefined) {
col.bind('click', (e) => {
if (column.sort === self.columnSort) {
self.sortReverse = !self.sortReverse
} else {
self.sortReverse = false
}
self.sortGridByColumn(column.sort)
})
col.addClass('clickable')
if (column.sort === self.columnSort) {
col.addClass(self.sortReverse ? 'sortbyreverse' : 'sortedby')
}
}
j = j + 1
})
}
for (let index = this.curRecord; index < max; index++) {
const element = dataset[index]
if (this.renderder) {
let row = super.addRow()
if (i % 2) {
row.setClasses('dbgridline odd')
} else {
row.setClasses('dbgridline')
}
let renderedElements = this.renderder(row, element)
j = 0
this.columns.map(column => {
row.addCell(column.sz, renderedElements[j] || '')
j = j + 1
})
i = i + 1
}
}
super.render()
}
render () {
this.prepare()
this.updateBody()
this.gridContainer.append(this.canvas)
this.gridContainer.append(this.footer)
return this.gridContainer
}
refresh () {
this.pagination.reset()
this.renderPagination(this.pagination.activePage)
this.updateBody()
}
renderPagination (activePage = 0) {
var cnt = 0
var tcnt = 1
let self = this
if (this.getDataset().length > this.maxRecords) {
// ((cnt >= start) && (cnt < start + count))
while (cnt < this.getDataset().length) {
this.pagination.addPage(tcnt - 1, false, tcnt, cnt, (e, page) => {
self.curRecord = page.start
self.updateBody()
self.pagination.setActivePage(page.id)
})
cnt = cnt + this.maxRecords
tcnt = tcnt + 1
}
this.pagination.setActivePage(activePage)
} else {
this.pagination.setActivePage(activePage)
}
}
}
export class SelectInput {
constructor (id, label, onChange) {
this.id = id
this.onChange = onChange
if (label) {
this.setLabel(label)
}
}
setValue (newValue) {
this.value=newValue
}
setOptions(list) {
this.options = list
}
setLabel (label) {
this.label = $('<label>').attr('for', this.id)
this.label.addClass('form-check-label')
this.label.append(label)
}
render () {
let self = this
this.input = $('<select>').addClass('form-control')
this.options.map(option=>{
let iOption = $('<option>').attr('value',option.value).html(option.title)
if (option.value === self.value) {
iOption.attr('selected','selected')
}
this.input.append(iOption)
})
this.input.bind('change',(e)=>{
self.onChange(e,e.target)
})
if (this.label) {
let result = $('<div>').append(this.label).append(this.input)
result.addClass('form-check')
return result
} else {
return this.input
}
}
}