2
0
Эх сурвалжийг харах

Add support for general non-editable result XForm fields

Updates #3155

Also:
- Add a date input field
JC Brand 1 жил өмнө
parent
commit
92ceec7eff

+ 1 - 0
package-lock.json

@@ -10723,6 +10723,7 @@
       }
     },
     "src/headless": {
+      "name": "@converse/headless",
       "version": "10.1.7",
       "license": "MPL-2.0",
       "dependencies": {

+ 35 - 16
src/headless/shared/parsers.js

@@ -391,6 +391,7 @@ export function isArchived (original_stanza) {
  * @property {boolean} [checked]
  * @property {XFormOption[]} [options]
  * @property {XFormCaptchaURI} [uri]
+ * @property {boolean} readonly
  *
  * @typedef {'result'|'form'} XFormResponseType
  *
@@ -405,13 +406,16 @@ export function isArchived (original_stanza) {
 
 /**
  * @param {Element} field
+ * @param {boolean} readonly
  * @param {Element} stanza
  * @return {XFormField}
  */
-function parseXFormField(field, stanza) {
+function parseXFormField(field, readonly, stanza) {
     const v = field.getAttribute('var');
     const label = field.getAttribute('label') || '';
     const type = field.getAttribute('type');
+    const desc = field.querySelector('desc')?.textContent;
+    const result = { readonly, desc };
 
     if (type === 'list-single' || type === 'list-multi') {
         const values = Array.from(field.querySelectorAll(':scope > value')).map((el) => el?.textContent);
@@ -423,6 +427,7 @@ function parseXFormField(field, stanza) {
                     label: option.getAttribute('label'),
                     selected: values.includes(value),
                     required: !!field.querySelector('required'),
+                    ...result,
                 };
             }
         );
@@ -432,10 +437,11 @@ function parseXFormField(field, stanza) {
             label: field.getAttribute('label'),
             var: v,
             required: !!field.querySelector('required'),
+            ...result,
         };
     } else if (type === 'fixed') {
         const text = field.querySelector('value')?.textContent;
-        return { text, label, type, var: v };
+        return { text, label, type, var: v, ...result };
     } else if (type === 'jid-multi') {
         return {
             type,
@@ -443,6 +449,7 @@ function parseXFormField(field, stanza) {
             label,
             value: field.querySelector('value')?.textContent,
             required: !!field.querySelector('required'),
+            ...result,
         };
     } else if (type === 'boolean') {
         const value = field.querySelector('value')?.textContent;
@@ -451,12 +458,14 @@ function parseXFormField(field, stanza) {
             var: v,
             label,
             checked: ((value === '1' || value === 'true') && true) || false,
+            ...result,
         };
     } else if (v === 'url') {
         return {
             var: v,
             label,
             value: field.querySelector('value')?.textContent,
+            ...result,
         };
     } else if (v === 'username') {
         return {
@@ -465,6 +474,7 @@ function parseXFormField(field, stanza) {
             value: field.querySelector('value')?.textContent,
             required: !!field.querySelector('required'),
             type: getInputType(field),
+            ...result,
         };
     } else if (v === 'password') {
         return {
@@ -472,6 +482,7 @@ function parseXFormField(field, stanza) {
             label,
             value: field.querySelector('value')?.textContent,
             required: !!field.querySelector('required'),
+            ...result,
         };
     } else if (v === 'ocr') { // Captcha
         const uri = field.querySelector('uri');
@@ -484,6 +495,7 @@ function parseXFormField(field, stanza) {
                 data: el?.textContent,
             },
             required: !!field.querySelector('required'),
+            ...result,
         };
     } else {
         return {
@@ -492,6 +504,7 @@ function parseXFormField(field, stanza) {
             required: !!field.querySelector('required'),
             value: field.querySelector('value')?.textContent,
             type: getInputType(field),
+            ...result,
         };
     }
 }
@@ -533,25 +546,31 @@ export function parseXForm(stanza) {
 
     if (type === 'result') {
         const reported = x.querySelector(':scope > reported');
-        const reported_fields = reported?.querySelectorAll(':scope > field');
-        const items = x.querySelectorAll(':scope > item');
-        return /** @type {XForm} */({
-            ...result,
-            reported: /** @type {XFormReportedField[]} */ (Array.from(reported_fields).map(getAttributes)),
-            items: Array.from(items).map((item) => {
-                return Array.from(item.querySelectorAll('field')).map((field) => {
-                    return /** @type {XFormResultItemField} */ ({
-                        ...getAttributes(field),
-                        value: field.querySelector('value')?.textContent ?? '',
+        if (reported) {
+            const reported_fields = reported ? Array.from(reported.querySelectorAll(':scope > field')) : [];
+            const items = Array.from(x.querySelectorAll(':scope > item'));
+            return /** @type {XForm} */({
+                ...result,
+                reported: /** @type {XFormReportedField[]} */ (reported_fields.map(getAttributes)),
+                items: items.map((item) => {
+                    return Array.from(item.querySelectorAll('field')).map((field) => {
+                        return /** @type {XFormResultItemField} */ ({
+                            ...getAttributes(field),
+                            value: field.querySelector('value')?.textContent ?? '',
+                        });
                     });
-                });
-            }),
-        });
+                }),
+            });
+        }
+        return {
+            ...result,
+            fields: Array.from(x.querySelectorAll('field')).map((field) => parseXFormField(field, true, stanza)),
+        };
     } else if (type === 'form') {
         return {
             ...result,
             instructions: x.querySelector('instructions')?.textContent,
-            fields: Array.from(x.querySelectorAll('field')).map((field) => parseXFormField(field, stanza)),
+            fields: Array.from(x.querySelectorAll('field')).map((field) => parseXFormField(field, false, stanza)),
         };
     } else {
         throw new Error(`Invalid type in XForm response stanza: ${type}`);

+ 1 - 0
src/headless/types/shared/parsers.d.ts

@@ -171,6 +171,7 @@ export type XFormField = {
     checked?: boolean;
     options?: XFormOption[];
     uri?: XFormCaptchaURI;
+    readonly: boolean;
 };
 export type XFormResponseType = 'result' | 'form';
 export type XForm = {

+ 1 - 1
src/headless/utils/url.js

@@ -14,7 +14,7 @@ export function isValidURL (text) {
     try {
         return !!(new URL(text));
     } catch (error) {
-        log.error(error);
+        log.debug(error);
         return false;
     }
 }

+ 8 - 3
src/plugins/adhoc-views/templates/ad-hoc-command-form.js

@@ -46,10 +46,10 @@ export default (el, command) => {
     const i18n_cancel = __('Cancel');
 
     return html`
+        <!-- Don't remove this <span>,
+                this is a workaround for a lit bug where a <form> cannot be removed
+                if it contains an <input> with name "remove" -->
         <span>
-            <!-- Don't remove this <span>,
-                 this is a workaround for a lit bug where a <form> cannot be removed
-                 if it contains an <input> with name "remove" -->
             <form class="converse-form">
                 ${command.alert
                     ? html`<div class="alert alert-${command.alert_type}" role="alert">${command.alert}</div>`
@@ -60,6 +60,11 @@ export default (el, command) => {
                       </div>`
                     : ''}
 
+                ${command.type === 'result' && command.title ?
+                        html`<div class="alert alert-info">${command.title}</div>` : ''}
+
+                ${command.type === 'form' && command.title ? html`<h6>${command.title}</h6>` : ''}
+
                 <fieldset class="form-group">
                     <input type="hidden" name="command_node" value="${command.node}" />
                     <input type="hidden" name="command_jid" value="${command.jid}" />

+ 118 - 1
src/plugins/adhoc-views/tests/adhoc.js

@@ -226,7 +226,7 @@ describe("Ad-hoc commands", function () {
         expect(inputs.length).toBe(0);
     }));
 
-    it("may return reported fields, which are not editable and are presented in a table",
+    it("may return reported fields, which are readonly and are presented in a table",
             mock.initConverse([], {}, async (_converse) => {
         const { api } = _converse;
         const entity_jid = 'muc.montague.lit';
@@ -350,6 +350,123 @@ describe("Ad-hoc commands", function () {
         expect(Array.from(rows[2].querySelectorAll('td')).map(h => h.textContent))
             .toEqual(['jabberd', 'off', 'on', 'on', 'on']);
     }));
+
+    it("May return a result form with readonly fields",
+            mock.initConverse([], {}, async (_converse) => {
+        const { api } = _converse;
+        const entity_jid = 'muc.montague.lit';
+        const { IQ_stanzas } = _converse.api.connection.get();
+
+        const jid = _converse.session.get('jid');
+
+        const modal = await api.modal.show('converse-user-settings-modal');
+        await u.waitUntil(() => u.isVisible(modal));
+        modal.querySelector('#commands-tab').click();
+
+        const adhoc_form = modal.querySelector('converse-adhoc-commands');
+        await u.waitUntil(() => u.isVisible(adhoc_form));
+
+        adhoc_form.querySelector('input[name="jid"]').value = entity_jid;
+        adhoc_form.querySelector('input[type="submit"]').click();
+
+        await mock.waitUntilDiscoConfirmed(
+            _converse,
+            entity_jid,
+            [],
+            ['http://jabber.org/protocol/commands'],
+            [],
+            'info'
+        );
+
+        let sel = `iq[to="${entity_jid}"] query[xmlns="http://jabber.org/protocol/disco#items"]`;
+        let iq = await u.waitUntil(() => IQ_stanzas.filter(iq => sizzle(sel, iq).length).pop());
+
+        _converse.api.connection.get()._dataRecv(mock.createRequest(stx`
+            <iq type="result"
+                id="${iq.getAttribute("id")}"
+                to="${_converse.jid}"
+                from="${entity_jid}"
+                xmlns="jabber:client">
+            <query xmlns="http://jabber.org/protocol/disco#items"
+                    node="http://jabber.org/protocol/commands">
+                <item node="list" name="Generate Invite" jid="${entity_jid}"/>
+            </query>
+        </iq>`));
+
+        const heading = await u.waitUntil(() => adhoc_form.querySelector('.list-group-item.active'));
+        expect(heading.textContent).toBe('Commands found:');
+
+        const items = adhoc_form.querySelectorAll('.list-group-item:not(.active)');
+        expect(items.length).toBe(1);
+        expect(items[0].textContent.trim()).toBe('Generate Invite');
+        items[0].querySelector('a').click();
+
+        sel = `iq[to="${entity_jid}"][type="set"] command`;
+        iq = await u.waitUntil(() => IQ_stanzas.filter(iq => sizzle(sel, iq).length).pop());
+
+        expect(Strophe.serialize(iq)).toBe(
+            `<iq id="${iq.getAttribute("id")}" to="${entity_jid}" type="set" xmlns="jabber:client">`+
+                `<command action="execute" node="list" xmlns="http://jabber.org/protocol/commands"/>`+
+            `</iq>`
+        );
+
+        _converse.api.connection.get()._dataRecv(
+            mock.createRequest(stx`
+            <iq type="result" from="${entity_jid}" to="${jid}" id="${iq.getAttribute("id")}" xmlns="jabber:client">
+                <command node="urn:xmpp:invite#create-account"
+                        status="completed"
+                        xmlns="http://jabber.org/protocol/commands"
+                        sessionid="414d39f0-bb39-4114-805b-70cb28c4de9a">
+                    <x xmlns="jabber:x:data" type="result">
+                        <title>Your invite has been created</title>
+                        <field type="text-single" var="username" label="username">
+                            <desc>Your username</desc>
+                            <value>spongebob</value>
+                        </field>
+                        <field type="text-single" label="Invite web page" var="landing-url">
+                            <desc>Share this link</desc>
+                            <value>https://www.conversejs.org</value>
+                        </field>
+                        <field type="text-single" label="Invite URI" var="uri">
+                            <desc>This alternative link can be opened with some XMPP clients</desc>
+                            <value>xmpp:localhost?register;preauth=VpwwVTD7ep3SIZvv6kyj725v</value>
+                        </field>
+                        <field type="text-single" label="Invite valid until" var="expire">
+                            <value>2024-05-14T19:40:32Z</value>
+                        </field>
+                    </x>
+                </command>
+            </iq>`
+        ));
+
+        const form = await u.waitUntil(() => adhoc_form.querySelector('form form'));
+        expect(form).toBeDefined();
+
+        expect(form.querySelector('.alert').textContent).toBe('Your invite has been created');
+
+        const labels = Array.from(form.querySelectorAll('label'));
+        expect(labels.length).toBe(4);
+
+        const descs = Array.from(form.querySelectorAll('small'));
+        expect(descs.length).toBe(3);
+        expect(descs.map((d) => d.textContent)).toEqual([
+            'Your username',
+            'Share this link',
+            'This alternative link can be opened with some XMPP clients',
+        ]);
+
+        const inputs = Array.from(form.querySelectorAll('input:not([type=hidden])'));
+        expect(inputs.length).toBe(2);
+        expect(inputs.map((i) => i.hasAttribute('readonly'))).toEqual([true, true]);
+        expect(inputs.map((i) => i.getAttribute('name'))).toEqual(['username', 'expire']);
+
+        const urls = Array.from(form.querySelectorAll('.form-url'));
+        expect(urls.length).toBe(2);
+        expect(urls.map((u) => u.textContent)).toEqual([
+            'https://www.conversejs.org',
+            'xmpp:localhost?register;preauth=VpwwVTD7ep3SIZvv6kyj725v',
+        ]);
+    }));
 });
 
 describe("Ad-hoc commands consisting of multiple steps", function () {

+ 4 - 1
src/shared/styles/_core.scss

@@ -164,6 +164,10 @@
         bottom: 5px;
     }
 
+    h1, h2, h3, h4, h5, h6 {
+        color: var(--header-color);
+    }
+
     ul li { height: auto; }
     div, span, h1, h2, h3, h4, h5, h6, p, blockquote,
     pre, a, em, img, strong, dl, dt, dd, ol, ul, li,
@@ -174,7 +178,6 @@
         margin: 0;
         padding: 0;
         border: 0;
-        font: inherit;
         vertical-align: baseline;
     }
 

+ 2 - 2
src/shared/styles/alerts.scss

@@ -17,8 +17,8 @@
 
     .alert-info {
         color: var(--background);
-        background-color: var(--primary-color);
-        border-color: var(--primary-color-dark);
+        background-color: var(--info-color);
+        border-color: var(--info-dark);
     }
 
     .alert-danger {

+ 4 - 0
src/shared/styles/forms.scss

@@ -33,6 +33,10 @@
             margin-top: $form-check-input-margin-y;
         }
 
+        .form-control[readonly] {
+            color: var(--disabled-color);
+        }
+
         .form-control {
             color: var(--text-color);
             background-color: var(--background);

+ 2 - 0
src/shared/styles/themes/classic.scss

@@ -18,9 +18,11 @@
     --subdued-color: var(--comment);
     --muc-color: var(--redder-orange);
     --chat-color: var(--green);
+    --disabled-color-bg: lightgray;
     --disabled-color: gray;
     --error-color: var(--dark-red);
     --focus-color: var(--background);
+    --header-color: var(--foreground);
 
     // ---
 

+ 4 - 1
src/shared/styles/themes/concord.scss

@@ -1,4 +1,8 @@
 .conversejs.theme-concord {
+    --foreground: #666;
+    --header-color: var(--foreground);
+    --heading-color: #9B4D;
+
     --controlbox-pane-background-color: #333;
     --panel-divider-color: #333;
     --controlbox-pane-bg-hover-color: #464646;
@@ -16,7 +20,6 @@
     --chatbox-border-radius: 0px;
 
     --heading-display: inline;
-    --heading-color: #9B4D;
 
     --link-hover-color: var(--light-blue);
 

+ 6 - 4
src/shared/styles/themes/dracula.scss

@@ -14,12 +14,16 @@
     // Base variables
     --background: #282a36;
     --foreground: #f8f8f2;
-    --subdued-color: var(--comment);
+    --subdued-color: var(--foreground);
     --muc-color: var(--orange);
     --chat-color: var(--green);
+    --disabled-color-bg: lightgray;
     --disabled-color: var(--comment);
     --error-color: var(--red);
     --focus-color: var(--comment);
+    --gray-color: var(--current-line);
+    --header-color: var(--pink);
+    --heading-color: var(--purple);
 
     // ---
 
@@ -32,7 +36,6 @@
     --headlines-head-border-bottom: 0.15em solid var(--headlines-color);
 
     --icon-hover-color: var(--cyan);
-    --gray-color: var(--comment);
 
     --highlight-color: var(--foreground);
     --highlight-color-darker: var(--comment);
@@ -126,7 +129,6 @@
 
     --message-receipt-color: var(--green);
 
-    --heading-color: var(--purple);
 
     --inverse-link-color: var(--foreground);
     --link-color: var(--cyan);
@@ -139,7 +141,7 @@
     --danger-color-dark: var(--pink);
     --danger-color: var(--pink);
     --error-color: var(--red);
-    --info-color: var(--comment);
+    --info-color: var(--yellow);
     --secondary-color-dark: var(--cyan);
     --secondary-color: var(--cyan);
     --warning-color-dark: var(--orange);

+ 6 - 1
src/templates/form_checkbox.js

@@ -2,6 +2,11 @@ import { html } from "lit";
 
 export default  (o) => html`
     <fieldset class="form-group">
-        <input id="${o.id}" name="${o.name}" type="checkbox" ?checked=${o.checked} ?required=${o.required} />
+        <input id="${o.id}"
+               name="${o.name}"
+               type="checkbox"
+               ?readonly=${o.readonly}
+               ?checked=${o.checked}
+               ?required=${o.required} />
         <label class="form-check-label" for="${o.id}">${o.label}</label>
     </fieldset>`;

+ 16 - 0
src/templates/form_date.js

@@ -0,0 +1,16 @@
+import { html } from "lit";
+
+export default  (o) => html`
+    <div class="form-group">
+        <label for="${o.id}">${o.label}
+            ${(o.desc) ? html`<small class="form-text text-muted">${o.desc}</small>` : ''}
+        </label>
+        <input
+            class="form-control"
+            id="${o.id}"
+            name="${o.name}"
+            type="datetime-local"
+            value="${o.value || ''}"
+            ?readonly=${o.readonly}
+            ?required=${o.required} />
+    </div>`;

+ 4 - 3
src/templates/form_input.js

@@ -2,14 +2,14 @@ import { html } from "lit";
 
 export default  (o) => html`
     <div class="form-group">
-        ${ o.type !== 'hidden' ? html`<label for="${o.id}">${o.label}</label>` : '' }
-
+        ${ o.type !== 'hidden' ? html`<label for="${o.id}">${o.label}
+            ${(o.desc) ? html`<small class="form-text text-muted">${o.desc}</small>` : ''}
+        </label>` : '' }
         <!-- This is a hack to prevent Chrome from auto-filling the username in
              any of the other input fields in the MUC configuration form. -->
         ${ (o.type === 'password' && o.fixed_username) ? html`
             <input class="hidden-username" type="text" autocomplete="username" value="${o.fixed_username}"></input>
         ` : '' }
-
         <input
             autocomplete="${o.autocomplete || ''}"
             class="form-control"
@@ -18,5 +18,6 @@ export default  (o) => html`
             placeholder="${o.placeholder || ''}"
             type="${o.type}"
             value="${o.value || ''}"
+            ?readonly=${o.readonly}
             ?required=${o.required} />
     </div>`;

+ 9 - 3
src/templates/form_textarea.js

@@ -1,12 +1,18 @@
 import { html } from "lit";
 import { u } from '@converse/headless';
 
-export default  (o) => {
+export default (o) => {
     const id = u.getUniqueId();
     return html`
         <div class="form-group">
-            <label class="label-ta" for="${id}">${o.label}</label>
-            <textarea name="${o.name}" id="${id}" class="form-control">${o.value}</textarea>
+            <label class="label-ta" for="${o.id}">${o.label}
+                ${(o.desc) ? html`<small class="form-text text-muted">${o.desc}</small>` : ''}
+            </label>
+            <textarea name="${o.name}"
+                      id="${id}"
+                      ?readonly=${o.readonly}
+                      ?required=${o.required}
+                      class="form-control">${o.value}</textarea>
         </div>
     `;
 };

+ 10 - 4
src/templates/form_url.js

@@ -1,6 +1,12 @@
 import { html } from "lit";
 
-export default  (o) => html`
-    <label>${o.label}
-        <a class="form-url" target="_blank" rel="noopener" href="${o.value}">${o.value}</a>
-    </label>`;
+export default (o) => html`
+    <div class="form-group">
+        <label for="${o.id}">${o.label}
+            ${ o.desc ? html`<small id="o.id" class="form-text text-muted">${o.desc}</small>` : '' }
+        </label>
+        <div>
+            <a class="form-url" target="_blank" rel="noopener" id="${o.id}" href="${o.value}">${o.value}</a>
+        </div>
+    </div>`;
+

+ 19 - 6
src/templates/form_username.js

@@ -1,16 +1,29 @@
-import { html } from "lit";
+import { html } from 'lit';
 
-export default  (o) => html`
+export default (o) => html`
     <div class="form-group">
-        ${ o.label ? html`<label>${o.label}</label>` :  '' }
+        ${
+            o.type !== 'hidden'
+                ? html`<label for="${o.id}"
+                      >${o.label} ${o.desc ? html`<small class="form-text text-muted">${o.desc}</small>` : ''}
+                  </label>`
+                : ''
+        }
         <div class="input-group">
                 <input name="${o.name}"
                        class="form-control"
+                       id="${o.id}"
                        type="${o.type}"
                        value="${o.value || ''}"
-                       ?required="${o.required}" />
-            <div class="input-group-append">
-                <div class="input-group-text" title="${o.domain}">${o.domain}</div>
+                       ?readonly=${o.readonly}
+                       ?required=${o.required} />
+                ${
+                    o.domain
+                        ? html`<div class="input-group-append">
+                              <div class="input-group-text" title="${o.domain}">${o.domain}</div>
+                          </div>`
+                        : ''
+                }
             </div>
         </div>
     </div>`;

+ 0 - 14
src/types/utils/html.d.ts

@@ -72,20 +72,6 @@ export function slideIn(el: HTMLElement, duration?: number): Promise<any>;
  * @returns {TemplateResult}
  */
 export function xFormField2TemplateResult(xfield: XFormField, options?: any): TemplateResult;
-/**
- * @param {Element} field
- */
-export function getInputType(field: Element): any;
-/**
- * Takes an XML field in XMPP XForm (XEP-004: Data Forms) format returns a
- * [TemplateResult](https://lit.polymer-project.org/api/classes/_lit_html_.templateresult.html).
- * @method u#xForm2TemplateResult
- * @param {HTMLElement} field - the field to convert
- * @param {Element} stanza - the containing stanza
- * @param {Object} options
- * @returns {TemplateResult}
- */
-export function xForm2TemplateResult(field: HTMLElement, stanza: Element, options?: any): TemplateResult;
 /**
  * @param {HTMLElement} el
  * @param {boolean} include_margin

+ 39 - 28
src/utils/html.js

@@ -10,6 +10,7 @@ import { Builder, Stanza } from 'strophe.js';
 import { api, converse, log, u } from '@converse/headless';
 import tplAudio from 'templates/audio.js';
 import tplFile from 'templates/file.js';
+import tplDateInput from 'templates/form_date.js';
 import tplFormCaptcha from '../templates/form_captcha.js';
 import tplFormCheckbox from '../templates/form_checkbox.js';
 import tplFormHelp from '../templates/form_help.js';
@@ -21,7 +22,7 @@ import tplFormUsername from '../templates/form_username.js';
 import tplHyperlink from 'templates/hyperlink.js';
 import tplVideo from 'templates/video.js';
 
-const { sizzle, Strophe } = converse.env;
+const { sizzle, Strophe, dayjs } = converse.env;
 const { getURI, isAudioURL, isImageURL, isVideoURL, isValidURL } = u;
 
 const APPROVED_URL_PROTOCOLS = ['http', 'https', 'xmpp', 'mailto'];
@@ -464,57 +465,67 @@ function isVisible (el) {
  * @param {Object} options
  * @returns {TemplateResult}
  */
-export function xFormField2TemplateResult (xfield, options={}) {
+export function xFormField2TemplateResult(xfield, options = {}) {
+    const default_vals = {
+        id: u.getUniqueId(),
+        name: xfield.var,
+    };
+
     if (xfield['type'] === 'list-single' || xfield['type'] === 'list-multi') {
         return tplFormSelect({
-            id: u.getUniqueId(),
+            ...default_vals,
             ...xfield,
             multiple: xfield.type === 'list-multi',
-            name: xfield.var,
         });
+
     } else if (xfield['type'] === 'fixed') {
         return tplFormHelp(xfield);
+
     } else if (xfield['type'] === 'jid-multi') {
-        return tplFormTextarea({
-            name: xfield.var,
-            ...xfield
-        });
+        return tplFormTextarea({ ...default_vals, ...xfield });
+
     } else if (xfield['type'] === 'boolean') {
-        return tplFormCheckbox({
-            id: u.getUniqueId(),
-            name: xfield.var,
-            ...xfield
-        });
-    } else if (xfield['var'] === 'url') {
+        return tplFormCheckbox({ ...default_vals, ...xfield });
+
+    } else if (xfield.var === 'url' || xfield.var === 'uri' || isValidURL(xfield.value)) {
         return tplFormUrl(xfield);
-    } else if (xfield['var'] === 'username') {
+
+    } else if (xfield.var === 'username') {
         return tplFormUsername({
+            ...default_vals,
             domain: options.domain ? ' @' + options.domain : '',
-            name: xfield.var,
             ...xfield,
         });
-    } else if (xfield['var'] === 'password') {
+    } else if (xfield.var === 'password') {
         return tplFormInput({
-            name: xfield['var'],
-            type: 'password',
+            ...default_vals,
             ...xfield,
+            autocomplete: getAutoCompleteProperty(xfield.var, options),
+            fixed_username: options?.fixed_username,
+            type: 'password',
         });
-    } else if (xfield['var'] === 'ocr') {
+    } else if (xfield.var === 'ocr') {
         return tplFormCaptcha({
-            name: xfield['var'],
+            ...default_vals,
+            ...xfield,
             data: xfield.uri.data,
             type: xfield.uri.type,
-            ...xfield,
         });
     } else {
-        const name = xfield['var'];
+        const date = xfield.value ? dayjs(xfield.value) : null;
+        if (date?.isValid()) {
+            return tplDateInput({
+                ...default_vals,
+                ...xfield,
+                value: date.format('YYYY-MM-DDTHH:mm:ss'),
+            });
+        }
+
         return tplFormInput({
-            name,
-            id: u.getUniqueId(),
-            fixed_username: options?.fixed_username,
-            autocomplete: getAutoCompleteProperty(name, options),
-            placeholder: null,
+            ...default_vals,
             ...xfield,
+            autocomplete: getAutoCompleteProperty(xfield.var, options),
+            placeholder: null,
         });
     }
 }