rewritten input widget controller

This commit is contained in:
dcore94 2023-09-26 17:14:47 +02:00
parent 82d5b25ff4
commit 50ee54a8ff
3 changed files with 320 additions and 306 deletions

View File

@ -61,7 +61,7 @@ class CCPExecutionForm extends HTMLElement{
<h5>Inputs</h5>
</div>
<div class="card-body ccp-inputs">
<div class="card-body">
<div>
<div class="form-group"></div>
</div>
</div>

View File

@ -1,181 +1,319 @@
class CCPInputWidgetController extends HTMLElement {
#input = null;
#renderer = null;
#data = null;
constructor(){
super()
this.#input = JSON.parse(this.getAttribute("input"))
this.#renderer = Renderer.instance(this.#input)
super()
}
connectedCallback(){
console.log("Widget connected")
this.innerHTML = this.render()
this.#renderer.connectedCallback(this)
this.#data = JSON.parse(this.getAttribute("input"))
if(this.isEnum()){
const opts = this.#data.schema.enum.join(",")
this.innerHTML += `<d4s-ccp-input-enum readonly="${this.#data.schema.readOnly}" options="${opts}" default="${this.#data.schema.default||''}" maxOccurs="${this.#data.maxOccurs||1}" minOccurs="${this.#data.minOccurs||1}" name="${this.#data.id}" description="${this.#data.description}" title="${this.#data.title}"></d4s-ccp-input-enum>`
}else if(this.isCode()){
this.innerHTML += `<d4s-ccp-input-textarea readonly="${this.#data.schema.readOnly}" default="${this.#data.schema.default||''}" maxOccurs="${this.#data.maxOccurs||1}" minOccurs="${this.#data.minOccurs||1}" name="${this.#data.id}" description="${this.#data.description}" title="${this.#data.title}"></d4s-ccp-input-textarea>`
}else if(this.isDateTime()){
const t = this.#data.schema.format.toLowerCase() === "datetime" ?
"datetime-local" : this.#data.schema.format.toLowerCase()
this.innerHTML += `<d4s-ccp-input-simple type="${t}" readonly="${this.#data.schema.readOnly}" default="${this.#data.schema.default||''}" maxOccurs="${this.#data.maxOccurs||1}" minOccurs="${this.#data.minOccurs||1}" name="${this.#data.id}" description="${this.#data.description}" title="${this.#data.title}"></d4s-ccp-input-simple>`
}else if(this.isFile()){
this.innerHTML += `<d4s-ccp-input-file readonly="${this.#data.schema.readOnly}" default="${this.#data.schema.default||''}" maxOccurs="${this.#data.maxOccurs||1}" minOccurs="${this.#data.minOccurs||1}" name="${this.#data.id}" description="${this.#data.description}" title="${this.#data.title}"></d4s-ccp-input-file>`
}else if(this.isSecret()){
this.innerHTML += `<d4s-ccp-input-secret readonly="${this.#data.schema.readOnly}" default="${this.#data.schema.default||''}" maxOccurs="${this.#data.maxOccurs||1}" minOccurs="${this.#data.minOccurs||1}" name="${this.#data.id}" description="${this.#data.description}" title="${this.#data.title}"></d4s-ccp-input-secret>`
}else{
this.innerHTML += `<d4s-ccp-input-simple readonly="${this.#data.schema.readOnly}" default="${this.#data.schema.default||''}" maxOccurs="${this.#data.maxOccurs||1}" minOccurs="${this.#data.minOccurs||1}" name="${this.#data.id}" description="${this.#data.description}" title="${this.#data.title}"></d4s-ccp-input-simple>`
}
}
render(){
return this.#renderer.render()
}
get name(){
return this.#renderer.name
set value(value){
this.firstElementChild.value = value
}
get value(){
return this.#renderer.getValue(this)
return this.firstElementChild.value.length === 1 ?
this.firstElementChild.value[0] :
this.firstElementChild.value
}
set value(v){
return this.#renderer.setValue(this, v)
get name(){
return this.firstElementChild.name
}
isEnum(input){
return (this.#data.schema.type === "string") && ("enum" in this.#data.schema)
}
isSecret(){
return (this.#data.schema.type === "string") &&
("format" in this.#data.schema) &&
(this.#data.schema.format != null) &&
(this.#data.schema.format.toLowerCase() === "secret")
}
isCode(){
return (this.#data.schema.type === "string") &&
("format" in this.#data.schema) &&
(this.#data.schema.format != null) &&
(this.#data.schema.format.toLowerCase() === "code")
}
isFile(){
return (this.#data.schema.type === "string") &&
("format" in this.#data.schema) &&
(this.#data.schema.format != null) &&
(this.#data.schema.format.toLowerCase() === "file")
}
isDateTime(){
return (this.#data.schema.type === "string") &&
("format" in this.#data.schema) &&
(this.#data.schema.format != null) &&
(["date", "time", "datetime"].indexOf(this.#data.schema.format.toLowerCase()) !== -1)
}
}
window.customElements.define('d4s-ccp-input', CCPInputWidgetController);
class Renderer{
class CCPBaseInputWidgetController extends HTMLElement{
#rootdoc = null;
#minOccurs = 1;
#maxOccurs = 1;
#description = ""
#title = ""
#name = ""
#default = null
#options = null
#value = null
#readonly = false
#input = null;
//useful to avoid having a custom element for every basic input type
#type = null
constructor(input){
this.#input = input
#count = 1
constructor(){
super()
this.#rootdoc = this//this.attachShadow({ mode: "open" });
this.#name = this.getAttribute("name")
this.#title = this.getAttribute("title")
this.#description = this.getAttribute("description")
this.#default = this.getAttribute("default")
this.#minOccurs = Number(this.getAttribute("minoccurs") ? this.getAttribute("minoccurs") : 1)
this.#maxOccurs = Number(this.getAttribute("maxoccurs") ? this.getAttribute("maxoccurs") : 1)
this.#readonly = this.getAttribute("readonly") === "true"
// coalesce all basic input types
this.#type = this.getAttribute("type")
// Handle enum case
this.#options = (this.getAttribute("options") ? this.getAttribute("options").split(",") : null)
this.value = Array(Math.max(this.#minOccurs,1)).fill(this.#default)
}
connectedCallback(controller){
set value(v){
const actual = Array.isArray(v) ? v : [v]
if(actual.length < this.#minOccurs || actual.length > this.#maxOccurs){
throw `Value with length ${v.length} does not respect bounds [${this.minOccurs},${this.maxOccurs}]`
}
this.#value = actual
this.render()
}
get schema(){
return this.#input.schema
get value(){
return this.#value
}
get name(){
return this.#input.id
}
get title(){
return this.#input.title
}
get description(){
return this.#input.description
get rootdoc(){
return this.#rootdoc
}
get required(){
return this.#input.minOccurs > 0
return this.#minOccurs > 0
}
get readOnly(){
return this.#input.schema.readOnly
get readonly(){
return this.#readonly
}
static instance(input){
if(this.isEnum(input)){
return new EnumInputRenderer(input)
}
if(this.isCode(input)){
return new CodeInputRenderer(input)
}
if(this.isDateTime(input)){
return new DateTimeInputRenderer(input)
}
if(this.isSecret(input)){
return new SecretInputRenderer(input)
}
if(this.isFile(input)){
return new FileInputRenderer(input)
}
return new SimpleInputRenderer(input)
isIncrementable(){
return this.#value.length < this.#maxOccurs
}
static isEnum(input){
return (input.schema.type === "string") && ("enum" in input.schema)
isDecrementable(){
return this.#value.length > this.#minOccurs && this.#value.length > 1
}
static isSecret(input){
return (input.schema.type === "string") &&
("format" in input.schema) &&
(input.schema.format != null) &&
(input.schema.format.toLowerCase() === "secret")
get count(){
return this.#count
}
static isCode(input){
return (input.schema.type === "string") &&
("format" in input.schema) &&
(input.schema.format != null) &&
(input.schema.format.toLowerCase() === "code")
get name(){
return this.#name
}
static isFile(input){
return (input.schema.type === "string") &&
("format" in input.schema) &&
(input.schema.format != null) &&
(input.schema.format.toLowerCase() === "file")
get title(){
return this.#title
}
static isDateTime(input){
return (input.schema.type === "string") &&
("format" in input.schema) &&
(input.schema.format != null) &&
(["date", "time", "datetime"].indexOf(input.schema.format.toLowerCase()) !== -1)
}
}
class SimpleInputRenderer extends Renderer{
#html = null;
constructor(input){
super(input)
get description(){
return this.#description
}
getValue(parent){
return parent.querySelector("input").value
get default(){
return this.#default
}
setValue(parent, v){
parent.querySelector("input").value = v
get options(){
return this.#options
}
set default(v){
this.#default = v
}
get type(){
return this.#type
}
render(){
let required = this.required ? 'required="required"' : ""
let readonly = this.readOnly ? 'readonly="readOnly"' : ""
this.#html = `
<div class="ccp-input-widget form-field">
<label>
this.rootdoc.innerHTML = `
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0/dist/css/bootstrap.min.css" rel="stylesheet">
<div name="root" class="my-3 ccp-input-widget form-field row">
<label class="form-label">
${this.required ? `<span title="Required" class="p-1 text-danger">*</span>` : ``}
${this.title}
<span class="ccp-help-icon" title="${this.description}" alt="${this.description}">?</span>
<span class="badge text-primary" title="${this.#description}" alt="${this.#description}">?</span>
<div style="float:right">
${this.isIncrementable() ? `<span name="plus" title="add one" class="badge text-success border-success btn">+</span>` : ``}
${this.isDecrementable() ? `<span name="minus" title="remove one" class="badge border border-danger text-danger btn">-</span>` : ``}
</div>
${this.content()}
</label>
<input class="ccp-input-widget form-control" name="${this.name}" value="${this.schema.default}" ${required} ${readonly}></input>
<span style="user-select:none;position:relative;top:-1.6rem;left:95%;cursor:pointer" name="password_toggle" class="d-none">&#128065;</span>
</div>
</div>
`
return this.#html
this.#rootdoc.querySelector("div[name=root]").addEventListener("click", ev=>{
const src = ev.target.getAttribute("name")
if(src === "plus"){
this.#value.push(this.#default)
this.render()
}else if(src === "minus"){
this.#value.pop()
this.render()
}
})
}
}
class FileInputRenderer extends Renderer{
class CCPSimpleInputWidgetController extends CCPBaseInputWidgetController{
#html = null;
#content = null;
constructor(input){
super(input)
constructor(){
super()
}
connectedCallback(controller){
controller.querySelector(`input[name=${this.name}]`).addEventListener("change", ev=>{
connectedCallback(){
this.rootdoc.addEventListener("input", ev=>{
if(ev.target.getAttribute("name") === this.name){
const index = Number(ev.target.getAttribute("data-index"))
this.value[index] = ev.target.value
}
})
}
content(){
if(this.value.length <= 1){
return `<input data-index="0" value="${this.value.length ? this.value[0] : ''}" class="my-2 form-control" placeholder="${this.title}" type="${this.type}" name="${this.name}" ${this.readonly ? 'readonly' : ''} ${this.required ? 'required' : ''}/>`
}
var out =
this.value.map((c,i)=>{
return `
<input name="${this.name}" value="${c}" data-index="${i}" class="ccp-input my-2 form-control" placeholder="${this.title}" type="${this.type}" name="${this.name}" ${this.readonly ? 'readonly' : ''} ${this.required ? 'required' : ''}/>
`
}).join("\n")
return out
}
}
window.customElements.define('d4s-ccp-input-simple', CCPSimpleInputWidgetController);
class CCPEnumInputWidgetController extends CCPBaseInputWidgetController{
constructor(){
super()
}
connectedCallback(){
this.rootdoc.addEventListener("change", ev=>{
if(ev.target.getAttribute("name") === this.name){
const index = Number(ev.target.getAttribute("data-index"))
this.value[index] = ev.target.value
}
})
}
content(){
const opts = this.options.map(o=>`<option value="${o}">${o}</option>`).join("\n")
if(this.value.length <= 1){
return `<select data-index="0" value="${this.value.length ? this.value[0] : ''}" class="my-2 form-control" placeholder="${this.title}" name="${this.name}" ${this.readonly ? 'disabled' : ''} ${this.required ? 'required' : ''}>${opts}</select>`
}
var out =
this.value.map((c,i)=>{
return `
<select data-index="${i}" value="${this.value.length ? this.value[0] : ''}" class="my-2 form-control" placeholder="${this.title}" name="${this.name}" ${this.readonly ? 'disabled' : ''} ${this.required ? 'required' : ''}>${opts}</select>
`
}).join("\n")
return out
}
}
window.customElements.define('d4s-ccp-input-enum', CCPEnumInputWidgetController);
class CCPTextAreaWidgetController extends CCPBaseInputWidgetController{
constructor(){
super()
}
connectedCallback(){
this.rootdoc.addEventListener("input", ev=>{
if(ev.target.getAttribute("name") === this.name){
const index = Number(ev.target.getAttribute("data-index"))
this.value[index] = ev.target.value
}
})
}
content(){
if(this.value.length <= 1){
return `<textarea data-index="0" class="my-2 form-control" placeholder="${this.title}" type="${this.type}" name="${this.name}" rows="5" ${this.readonly ? 'readonly' : ''} ${this.required ? 'required' : ''}>${this.value.length ? this.value[0] : ''}</textarea>`
}
var out =
this.value.map((c,i)=>{
return `
<textarea name="${this.name}" data-index="${i}" class="ccp-input my-2 form-control" placeholder="${this.title}" type="${this.type}" name="${this.name}" rows="5" ${this.readonly ? 'readonly' : ''} ${this.required ? 'required' : ''}>${c}</textarea>
`
}).join("\n")
return out
}
}
window.customElements.define('d4s-ccp-input-textarea', CCPTextAreaWidgetController);
class CCPFileInputWidgetController extends CCPBaseInputWidgetController{
constructor(){
super()
}
connectedCallback(){
this.rootdoc.addEventListener("change", ev=>{
const tgt = ev.target
const ename = tgt.getAttribute("name")
if(ename === this.name){
if(tgt.getAttribute("name") === this.name){
const index = Number(tgt.getAttribute("data-index"))
const file = ev.target.files[0]
/*if(file.type !== this.schema.contentMediaType){
alert("Unsupported media type. Must be " + this.schema.contentMediaType)
ev.stopPropagation()
ev.preventDefault()
tgt.value = null
return false
}*/
if(file.size > 100*1024){
alert("This input allows only small files (100K). Use references instead ")
ev.stopPropagation()
@ -189,197 +327,73 @@ class FileInputRenderer extends Renderer{
if ((encoded.length % 4) > 0) {
encoded += '='.repeat(4 - (encoded.length % 4));
}
this.#content = encoded
this.value[index] = encoded
this.querySelector("small[name=preview]").textContent = encoded.substr(0,5) + "..." + encoded.substr(encoded.length-5)
})
reader.readAsDataURL(file)
}
})
}
getValue(parent){
return this.#content
}
setValue(parent, v){
parent.querySelector("input").value = v
}
render(){
let required = this.required ? 'required="required"' : ""
let readonly = this.readOnly ? 'readonly="readOnly"' : ""
this.#html = `
<div class="ccp-input-widget form-field">
<label>
${this.title}
<span class="ccp-help-icon" title="${this.description}" alt="${this.description}">?</span>
</label>
<input type="file" class="ccp-input-widget form-control" name="${this.name}" value="${this.schema.default}" ${required} ${readonly}></input>
</div>
`
return this.#html
content(){
if(this.value.length <= 1){
const v = this.value.length ? this.value[0] : ''
return `
<input type="file" data-index="0" class="my-2 form-control" placeholder="${this.title}" name="${this.name}" ${this.readonly ? 'readonly' : ''} ${this.required ? 'required' : ''} value="${v}"/>
<small name ="preview" class="form-text text-muted">${v.substr(0,5) + "..." + v.substr(v.length-5)}</small>
`
}
var out =
this.value.map((c,i)=>{
return `
<input type="file" data-index="${i}" class="my-2 form-control" placeholder="${this.title}" name="${this.name}" ${this.readonly ? 'readonly' : ''} ${this.required ? 'required' : ''} value="${c}"/>
<small name ="preview" class="form-text text-muted">${c.substr(0,5) + "..." + c.substr(s.length-5)}</small>
`
}).join("\n")
return out
}
}
window.customElements.define('d4s-ccp-input-file', CCPFileInputWidgetController);
class SecretInputRenderer extends Renderer{
class CCPSecretInputWidgetController extends CCPBaseInputWidgetController{
#html = null;
constructor(input){
super(input)
constructor(){
super()
}
getValue(parent){
return parent.querySelector("input").value
}
connectedCallback(){
this.rootdoc.addEventListener("input", ev=>{
if(ev.target.getAttribute("name") === this.name){
const index = Number(ev.target.getAttribute("data-index"))
this.value[index] = ev.target.value
}
})
setValue(parent, v){
parent.querySelector("input").value = v
}
connectedCallback(controller){
controller.addEventListener("click", ev=>{
const ename = ev.target.getAttribute("name")
if(ename === "password_toggle"){
const w = controller.querySelector("div.ccp-input-widget input")
w.type = (w.type === "password" ? "" : "password")
ev.preventDefault()
this.rootdoc.addEventListener("click", ev=>{
if(ev.target.getAttribute("name") === "password_toggle"){
const index = Number(ev.target.getAttribute("data-index"))
const field = this.rootdoc.querySelector(`input[data-index='${index}']`)
if(field.type === "text") field.type="password";
else field.type = "text";
}
})
}
render(){
let required = this.required ? 'required="required"' : ""
let readonly = this.readOnly ? 'readonly="readOnly"' : ""
this.#html = `
<div class="ccp-input-widget ccp-input-widget form-field">
<label>
${this.title}
<span class="ccp-help-icon" title="${this.description}" alt="${this.description}">?</span>
</label>
<input type="password" class="ccp-input-widget form-control" name="${this.name}" value="${this.schema.default}" ${required} ${readonly}></input>
<span style="user-select:none;position:relative;top:-1.6rem;float:right;cursor:pointer" name="password_toggle">&#128065;</span>
</div>
`
return this.#html
}
}
class DateTimeInputRenderer extends Renderer{
#html = null;
constructor(input){
super(input)
}
getValue(parent){
return parent.querySelector("input").value
}
setValue(parent, v){
parent.querySelector("input").value = v
}
render(){
let required = this.required ? 'required="required"' : ""
let readonly = this.schema.readOnly ? 'readonly="readOnly"' : ""
let t = this.schema.format.toLowerCase() === "datetime" ? "datetime-local" : this.schema.format.toLowerCase()
this.#html = `
<div class="ccp-input-widget form-field">
<label>
${this.title}
<span class="ccp-help-icon" title="${this.description}" alt="${this.description}">?</span>
</label>
<input type="${t}" class="ccp-input-widget form-control" name="${this.name}" value="${this.schema.default}" ${required} ${readonly}></input>
</div>
`
return this.#html
}
}
class EnumInputRenderer extends Renderer{
#html = null;
constructor(input){
super(input)
}
getValue(parent){
return parent.querySelector("select").value
}
setValue(parent, v){
parent.querySelector("select").value = v
}
render(){
let options = this.schema.enum.map(e => {
return e === this.schema.default ?
`<option name="${e}" value="${e}" selected="selected">${e}</option>` :
`<option name="${e}" value="${e}">${e}</option>`
})
let required = this.required ? 'required="required"' : ""
let readonly = this.schema.readOnly ? 'readonly="readOnly"' : ""
this.#html = `
<div class="ccp-input-widget form-field">
<label>
${this.title}
<span class="ccp-help-icon" title="${this.description}" alt="${this.description}">?</span>
</label>
<select class="ccp-input-widget form-control" name="${this.name}" value="${this.schema.default}" ${required}>
${options.join("")}
</select>
</div>
`
return this.#html
}
}
class CodeInputRenderer extends Renderer{
#html = null;
#codemirror = null;
constructor(input){
super(input)
}
getValue(parent){
return parent.querySelector("textarea").value
}
setValue(parent, v){
parent.querySelector("textarea").value = v
}
connectedCallback(controller){
/*const ta = controller.querySelector("textarea")
const opts = {
lineNumbers: true,
indentUnit: 4,
matchBrackets: true,
mode: this.schema.contentMediaType,
readOnly : this.schema.readOnly ? true : false
content(){
if(this.value.length <= 1){
return `
<input data-index="0" value="${this.value.length ? this.value[0] : ''}" class="my-2 form-control" placeholder="${this.title}" type="password" name="${this.name}" ${this.readonly ? 'readonly' : ''} ${this.required ? 'required' : ''}/>
<span data-index="0" class="btn" style="position:relative;top:-2.7rem;float:right" name="password_toggle">&#128065;</span>
`
}
this.#codemirror = CodeMirror.fromTextArea(ta, opts)
this.#codemirror.setValue(this.schema.default)
this.#codemirror.refresh()*/
}
render(){
let required = this.required ? 'required="required"' : ""
let readonly = this.schema.readOnly ? 'readonly="readOnly"' : ""
this.#html = `
<div class="ccp-input-widget form-field">
<label>
${this.title}
<span class="ccp-help-icon" title="${this.description}" alt="${this.description}">?</span>
</label>
<textarea class="ccp-input-widget form-control" ${required} ${readonly}>${this.schema.default}</textarea>
</div>
`
return this.#html
var out =
this.value.map((c,i)=>{
return `
<input name="${this.name}" value="${c}" data-index="${i}" class="ccp-input my-2 form-control" placeholder="${this.title}" type="password" name="${this.name}" ${this.readonly ? 'readonly' : ''} ${this.required ? 'required' : ''}/>
<span data-index="${i}" class="btn" style="position:relative;top:-2.7rem;float:right" name="password_toggle">&#128065;</span>
`
}).join("\n")
return out
}
}
window.customElements.define('d4s-ccp-input-secret', CCPSecretInputWidgetController);

View File

@ -92,14 +92,14 @@ class CCPInputWidgetEditorController extends HTMLElement{
</div>
</div>
<div class="col">
<div class="form-field" title="Minimum">
<label>Min</label>
<div class="form-field" title="Minimum occurrences">
<label>Min occurs</label>
<input value="${this.#input.minOccurs}" type="number" min="0" step="1" name="minOccurs" value="${this.#input.minOccurs}" required="required" class="form-control" placeholder="minOccurs" ${ input.id === 'ccpimage' ? 'readonly' : ''}/>
</div>
</div>
<div class="col">
<div class="form-field" title="Maximum">
<label>Max</label>
<div class="form-field" title="Maximum occurrences">
<label>Max occurs</label>
<input value="${this.#input.maxOccurs}" type="number" min="0" step="1" name="maxOccurs" value="${this.#input.maxOccurs}" required="required" class="form-control" placeholder="maxOccurs" ${ input.id === 'ccpimage' ? 'readonly' : ''}/>
</div>
</div>