let alertElement = null
let alertTimeout = null
/**
* alert<br>
* <props-opt></props-opt>
* <table class="classtable">
* <tr><td>msg</td><td>message [ default: "Alert!"]</td>
* <tr><td>delay</td><td>delay ms</td>
* <tr><td>kind</td><td>kind [ info / success / error ]</td>
* </table>
* @param props {object} props <opt-param />
*/
function alert(propsOpt){
let props = propsOpt || {}
if(!alertElement) alertElement = div().poa().ac("alertelement")
let de = document.documentElement
de.style.position = "relative"
de.appendChild(alertElement.e)
alertElement.html(props.msg || "Alert!").dispi().rc("info success error")
if(props.kind) alertElement.ac(props.kind)
if(alertTimeout) clearTimeout(alertTimeout)
alertTimeout = setTimeout(_ => {
alertElement.x().dispn()
}, props.delay || 3000)
}
/**
* get a value from localStorage
* @param path {string} path
* @param defaultValue {any} default value ( returned if stored value is not available or non parsable )
*/
function getLocal(path, defaultValue){
let stored = localStorage.getItem(path)
if(stored){
try{
let value = JSON.parse(stored)
return value
}catch(err){}
}
return defaultValue
}
/**
* store a value in localStorage
* @param path {string} path
* @param value {any} value ( should be JSON serializable )
* @returns true on success, false otherwise
*/
function storeLocal(path, value){
try{
localStorage.setItem(path, JSON.stringify(value))
return true
}catch(err){}
return false
}
/**
* translate option initializer
* @param obj {*} dicionary / array / string
*/
function translateOption(obj){
if(obj instanceof Array){
return ({
value: obj[0],
display: obj[1]
})
}
if(typeof obj == "string"){
return ({
value: obj,
display: obj
})
}
return obj
}
/**
* Classes
*/
/**
* base class of smartdom wrapper
* <props-opt></props-opt>
* @param props {object} props <opt-param />
*/
class SmartDomElement_{
constructor(props){
this.props = props || {}
let tag = this.props.tag || "div"
this.id = this.props.id
this.childs = []
this.e = document.createElement(tag)
this.state = {}
}
/**
* append childs
* @param childs {...any} child elements, either single elements or arrays of elements
* @example
* div().a(
* div().html("Single div appended."),
* [
* div().html("Div appended as array element 0."),
* div().html("Div appended as array element 1."),
* ]
* )
*/
a(...childs){
let childList = []
for(let child of childs){
if(child instanceof Array) childList = childList.concat(child)
else childList.push(child)
}
let index = 0
for(let child of childList){
child.parent = this
child.index = index++
this.childs.push(child)
this.e.appendChild(child.e)
}
return this
}
/**
* add style and return the instance
* @param name {string} style name
* @param value {string} style value
*/
addStyle(name, value){
this.e.style[name] = value
return this
}
/**
* add classes
* @param classes {string} space separated list of classes
*/
ac(classes){
for(let klass of classes.split(" ")) this.e.classList.add(klass)
return this
}
/**
* remove classes
* @param classes {string} space separated list of classes
*/
rc(classes){
for(let klass of classes.split(" ")) this.e.classList.remove(klass)
return this
}
/**
* set HTML element attribute
* @param name {string} name
* @param value {any} value
*/
setAttribute(name, value){
this.e.setAttribute(name, value)
return this
}
/**
* get HTML element attribute
* @param name {string} name
*/
getAttribute(name){
return this.e.getAttribute(name)
}
/**
* return element value
*/
value(){
return this.e.value
}
/**
* set element value
* @param value {any} value
*/
setValue(value){
this.e.value = value
}
/**
* add event listeners with a callback
* @param events {string} events separated by space
* @param callback {function} callback
*/
ae(events, callback){
for(let event of events.split(" ")){
this.e.addEventListener(event, callback)
}
}
/**
* delete content of element
*/
x() {this.e.innerHTML="";return this}
/**
* set display
* @param x {string} display
*/
disp(x) {return this.addStyle("display", x)}
/**
* display none
*/
dispn() {return this.disp("none")}
/**
* display initial
*/
dispi() {return this.disp("initial")}
/**
* set width
* @param x {number} width in pixels
*/
w(x) {return this.addStyle("width", x + "px")}
/**
* set height
* @param x {number} height in pixels
*/
h(x) {return this.addStyle("height", x + "px")}
/**
* set padding
* @param x {number} padding in pixels
*/
pad(x) {return this.addStyle("padding", x + "px")}
/**
* set color
* @param x {string} color
*/
c(x) {return this.addStyle("color", x)}
/**
* set background-color
* @param x {string} background color
*/
bc(x) {return this.addStyle("backgroundColor", x)}
/**
* set position
* @param x {string} position
*/
pos(x) {return this.addStyle("position", x)}
/**
* position relative
*/
por() {return this.pos("relative")}
/**
* position absolute
*/
poa() {return this.pos("absolute")}
/**
* set inner html
* @param x {string} HTML string
*/
html(x) {this.e.innerHTML = x;return this}
/**
* the id used as a path element
*/
pathId(){
return this.id
}
/**
* list of path ids leading to the element
*/
pathList(){
let pathList = []
let current = this
while(current){
if(current.pathId()) pathList.unshift(current.pathId())
current = current.parent
}
return pathList
}
/**
* path to the element
*/
path(){
let pathList = this.pathList()
if(!pathList.length) return null
return pathList.join("/")
}
/**
* store path of the element, by default it is the element path,
* but this can be overridden with props.forceStorePath
*/
storePath(){
if(this.props.forceStorePath) return this.props.forceStorePath
return this.path()
}
/**
* store the element state in localStorage if it has a path
*/
storeState(){
if(this.storePath()){
storeLocal(this.storePath(), this.state)
}
}
/**
* retrieve the element state from localStorage if it has a path
*/
retrieveState(){
if(this.storePath()){
this.state = getLocal(this.storePath(), this.state)
}
}
/**
* initialize state from props, should be implemented by derived classes
*/
initState(){
// abstract
}
/**
* build element from scratch, should be implemented by derived classes
*/
build(){
// abstract
}
/**
* mount element
*/
mount(){
this.retrieveState()
this.initState()
this.build()
this.mountChilds()
}
/**
* mount childs of the element
*/
mountChilds(){
for(let child of this.childs){
child.mount()
}
}
}
/**
* wrapper class for HTML div element
*/
class div_ extends SmartDomElement_{
constructor(props){
super(props)
}
}
/**
* returns a new div_ instance
* @param props {object} props <opt-param />
* @example
* // creates a div with content "I'm a div."
* div().html("I'm a div.")
*/
function div(props){return new div_(props)}
/**
* wrapper class for HTML button element
*/
class Button_ extends SmartDomElement_{
/**
* @param caption {string} caption
* @param callback {function} callback
* @param props {object} props <opt-param />
*/
constructor(caption, callback, props){
super({...props, ...{
tag: "button"
}})
this.html(caption)
if(callback) this.ae("click", callback)
}
}
/**
* returns a new Button_ instance
* @param caption {string} caption
* @param callback {function} callback
* @param props {object} props <opt-param />
*/
function Button(caption, callback, props){return new Button_(caption, callback, props)}
/**
* wrapper for HTML input element
* @param props (object) props <opt-param />
*/
class input_ extends SmartDomElement_{
/**
* <props-opt></props-opt>
* <table class="classtable">
* <tr><td>type</td><td>input type [ default: "text" ]</td>
* </table>
*/
constructor(props){
super({...props, ...{
tag: "input"
}})
this.setAttribute("type", props.type || "text")
}
}
/**
* returns a new input_ instance
* @param props {object} props <opt-param />
*/
function input(props){return input_(props)}
/**
* wrapper class for HTML table element
* @param props {object} props <opt-param />
*/
class table_ extends SmartDomElement_{
/**
* <props-opt></props-opt>
* <table class="classtable">
* <tr><td>cellpadding</td><td>cell padding</td>
* <tr><td>cellspacing</td><td>cell spacing</td>
* <tr><td>border</td><td>border width</td>
* </table>
*/
constructor(props){
super({...props, ...{
tag: "table"
}})
if(typeof this.props.cellpadding != "undefined") this.setAttribute("cellpadding", this.props.cellpadding)
if(typeof this.props.cellspacing != "undefined") this.setAttribute("cellspacing", this.props.cellspacing)
if(typeof this.props.border != "undefined") this.setAttribute("border", this.props.border)
}
}
/**
* returns a new table_ instance
* @param props {object} props <opt-param />
*/
function table(props){return new table_(props)}
/**
* wrapper class for HTML table head element
*/
class thead_ extends SmartDomElement_{
constructor(props){
super({...props, ...{
tag: "thead"
}})
}
}
/**
* returns a new thead_ instance
* @param props {object} props <opt-param />
*/
function thead(props){return new thead_(props)}
/**
* wrapper class for HTML table body element
*/
class tbody_ extends SmartDomElement_{
constructor(props){
super({...props, ...{
tag: "tbody"
}})
}
}
/**
* returns a new tbody_ instance
* @param props {object} props <opt-param />
*/
function tbody(props){return new tbody_(props)}
/**
* wrapper class for HTML table row element
*/
class tr_ extends SmartDomElement_{
constructor(props){
super({...props, ...{
tag: "tr"
}})
}
}
/**
* returns a new tr_ instance
* @param props {object} props <opt-param />
*/
function tr(props){return new tr_(props)}
/**
* wrapper class for HTML select element
*/
class select_ extends SmartDomElement_{
constructor(props){
super({...props, ...{
tag: "select"
}})
}
}
/**
* returns a new select_ instance
* @param props {object} props <opt-param />
*/
function select(props){return new select_(props)}
/**
* wrapper class for HTML option element
* @param props {object} props <opt-param />, see class constructor
*/
class option_ extends SmartDomElement_{
/**
* <props-opt></props-opt>
* <table class="classtable">
* <tr><td>value</td><td>option value</td>
* <tr><td>display</td><td>option display</td>
* </table>
*/
constructor(props){
super({...props, ...{
tag: "option"
}})
if(this.props.value) this.setAttribute("value", this.props.value)
if(this.props.display) this.html(this.props.display)
}
}
/**
* returns a new option_ instance
* @param props {object} props <opt-param />, see class constructor
*/
function option(props){return new option_(props)}
/**
* wrapper class for HTML checkbox input element
* @param props {object} props <opt-param />, see class constructor
*/
class CheckBoxInput_ extends input_{
/**
* <props-opt></props-opt>
* <table class="classtable">
* <tr><td>forceChecked</td><td>boolean, force checked status to true or false</td>
* <tr><td>changeCallback</td><td>change callback</td>
* </table>
*/
constructor(props){
super({...props, ...{
type: "checkbox"
}})
this.ae("change", this.changed.bind(this))
}
/**
* handle change event
*/
changed(){
this.state.checked = this.e.checked
this.storeState()
if(this.props.changeCallback) this.props.changeCallback(this.state.checked)
}
/**
* init state
*/
initState(){
if(typeof this.props.forceChecked != "undefined") this.state.checked = this.props.forceChecked
}
/**
* build
*/
build(){
this.e.checked = this.state.checked
}
}
/**
* returns a new CheckBoxInput_ instance
* @param props {object} props <opt-param />, see class constructor
*/
function CheckBoxInput(props){return new CheckBoxInput_(props)}
/**
* wrapper class for HTML table cell element
*/
class td_ extends SmartDomElement_{
constructor(props){
super({...props, ...{
tag: "td"
}})
}
}
/**
* returns a new td_ instance
* @param props {object} props <opt-param />
*/
function td(props){return new td_(props)}
/**
* combo
* @param props {object} props <opt-param />, see class constructor
*/
class Combo_ extends select_{
/**
* <props-opt></props-opt>
* <table class="classtable">
* <tr><td>forceOptions</td><td>list of options, allowed option formats {value: "foo", display: "bar"} / ["foo", "bar"] / "foo" ( display will also be "foo")</td>
* <tr><td>forceSelected</td><td>selected option value</td>
* <tr><td>changeCallback</td><td>change callback</td>
* </table>
*/
constructor(props){
super({...props, ...{
}})
this.ae("change", this.changed.bind(this))
}
/**
* handle change event
*/
changed(){
this.state.selected = this.value()
this.storeState()
if(this.props.changeCallback) this.props.changeCallback(this.state.selected)
}
/**
* init state
*/
initState(){
if(!this.state.options) this.state.options = []
if(this.props.forceOptions) this.state.options = this.props.forceOptions
if(this.props.forceSelected) this.state.selected = this.props.forceSelected
this.translateOptions()
this.storeState()
}
/**
* translate options
*/
translateOptions(){
this.state.options = this.state.options.map(opt => translateOption(opt))
}
/**
* build
*/
build(){
this.translateOptions()
this.x().a(
this.state.options.map(
opt => {
let o = option(opt)
if(opt.value == this.state.selected) o.setAttribute("selected", true)
return o
}
)
)
}
}
/**
* returns a new Combo_ instance
* @param props {object} props <opt-param />, see class constructor
*/
function Combo(props){return new Combo_(props)}
/**
* options table
* @param props {object} see class constructor
*/
class OptionsTable_ extends table_{
/**
* props should have an options field
* <table class="classtable">
* <tr><td>options</td><td>array of input elements,
* each input element should have a display field in its props, telling the name of the option</td>
* </table>
*/
constructor(props){
super({...props, ...{
}})
}
/**
* build
*/
build(){
this.a(
thead().a(
tr().a(
td().html("Option Name"),
td().html("Option Value"),
)
),
tbody().a(
this.props.options.map(option => tr().a(
td().html(option.props.display),
td().a(option),
))
)
)
}
}
/**
* returns a new OptionsTable_ instance
* @param props {object} props, see class constructor
*/
function OptionsTable(props){return new OptionsTable_(props)}
module.exports = {
div: div,
input: input,
CheckBoxInput: CheckBoxInput,
table: table,
thead: thead,
tbody: tbody,
tr: tr,
td: td,
OptionsTable: OptionsTable,
select: select,
option: option,
Combo: Combo,
Button: Button,
alert: alert,
}