├── support ├── flex_forms_error.png ├── admin_print.css ├── flex_forms_dialog.css ├── jquery.tablednd.min.js ├── sha256.js ├── admin.css ├── flex_forms_dialog.js └── flex_forms.js ├── README.md └── index.html /support/flex_forms_error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cubiclesoft/offline-forms/master/support/flex_forms_error.png -------------------------------------------------------------------------------- /support/admin_print.css: -------------------------------------------------------------------------------- 1 | html, body { height: auto; overflow: visible; } 2 | 3 | #menuwrap { display: none; } 4 | #menuwrap.showmenu { display: none; } 5 | 6 | #contentwrap { height: auto; overflow: hidden; } 7 | #contentwrap .proptitlewrap { background-color: #222222; color: #FAFAFA; padding: 0.85em; box-shadow: none; border-bottom: 1px solid #CCCCCC; } 8 | #contentwrap.showmenu #navoverflowwrap { display: none; } 9 | #contentwrap #navbutton { display: none; } 10 | #contentwrap .proptitletext { padding-left: 0em; } 11 | 12 | .formitemdata table.formitemtable > thead > tr > th, .formitemdata table.formitemtable > tbody > tr > td { border-top: 1px solid #CCCCCC; } 13 | .formitemdata table.formitemtable > thead > tr > th:first-child { border-left: 1px solid #CCCCCC; } 14 | .formitemdata table.formitemtable > thead > tr > th:last-child { border-right: 1px solid #CCCCCC; } 15 | .formitemdata table.formitemtable > tbody > tr:first-child > td { border-top: 1px solid #CCCCCC; } 16 | 17 | form.ff_form .formsubmit input { background-color: #FAFAFA; color: #222222; border: 1px solid #222222; } 18 | 19 | .ui-multiselect { width: 100% !important; } 20 | .formitemdata .ui-multiselect.ui-widget { border: 1px solid #c5c5c5 !important; background: #ffffff !important; color: #222222 !important; } 21 | 22 | .multiselect-available-list { display: none; } 23 | .multiselect-selected-list { left: 0 !important; width: 100% !important; } 24 | 25 | .no-print { display: none; } 26 | -------------------------------------------------------------------------------- /support/flex_forms_dialog.css: -------------------------------------------------------------------------------- 1 | .ff_dialogwrap { -webkit-box-shadow: 0px 0px 7px 0px rgba(0, 0, 0, 0.15); -moz-box-shadow: 0px 0px 7px 0px rgba(0, 0, 0, 0.15); box-shadow: 0px 0px 7px 0px rgba(0, 0, 0, 0.15); position: fixed; max-width: 920px; min-width: 20em; margin: 1em; left: 0; top: 0; box-sizing: border-box; min-height: 10em; max-height: 90vh; z-index: 10001; outline: none; } 2 | .ff_dialogwrap .ff_dialog_resizer { position: absolute; left: -8px; top: -8px; right: -8px; bottom: -8px; } 3 | 4 | .ff_dialog_resize_nwse, .ff_dialog_resize_nwse * { cursor: nwse-resize !important; } 5 | .ff_dialog_resize_nesw, .ff_dialog_resize_nesw * { cursor: nesw-resize !important; } 6 | .ff_dialog_resize_ew, .ff_dialog_resize_ew * { cursor: ew-resize !important; } 7 | .ff_dialog_resize_ns, .ff_dialog_resize_ns * { cursor: ns-resize !important; } 8 | 9 | .ff_dialogwrap .ff_dialog_measure_em_size { display: inline-block; position: fixed; left: -9999px; width: 1em; height: 1em; } 10 | 11 | .ff_dialogwrap .ff_dialog_innerwrap { border: 1px solid #AAAAAA; background-color: #FFFFFF; position: relative; overflow: hidden; display: flex; flex-direction: column; height: 100%; box-sizing: border-box; max-height: 90vh; } 12 | .ff_dialogwrap.ff_dialog_focused { -webkit-box-shadow: 0px 0px 9px 0px rgba(0, 0, 0, 0.30); -moz-box-shadow: 0px 0px 9px 0px rgba(0, 0, 0, 0.30); box-shadow: 0px 0px 9px 0px rgba(0, 0, 0, 0.30); } 13 | .ff_dialogwrap.ff_dialog_focused .ff_dialog_innerwrap { border: 1px solid #0063B1; } 14 | 15 | .ff_dialogwrap .ff_dialog_titlewrap { display: flex; align-items: center; user-select: none; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; cursor: default; } 16 | .ff_dialogwrap .ff_dialog_title { flex: 1; font-size: 0.85em; color: #999999; margin-left: 1em; margin-right: 1em; padding: 0.45em 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } 17 | .ff_dialogwrap.ff_dialog_focused .ff_dialog_title { color: inherit; } 18 | 19 | .ff_dialogwrap .ff_dialog_close { padding: 0.2em 0.8em; font-size: 1.2em; line-height: 1.1; color: #999999; background-color: #FFFFFF; outline: none; border: 0 none; } 20 | .ff_dialogwrap .ff_dialog_close::after { content: '\00D7'; } 21 | .ff_dialogwrap.ff_dialog_focused .ff_dialog_close { color: inherit; } 22 | .ff_dialogwrap .ff_dialog_close:hover, .ff_dialogwrap .ff_dialog_close:focus { color: #FFFFFF; background-color: #E81123; } 23 | 24 | .ff_dialogwrap .ff_formwrap { flex: 1; overflow: hidden; font-size: 0.85em; height: 100%; border-top: 1px solid #F0F0F0; } 25 | .ff_dialogwrap .ff_formwrapinner { overflow: hidden; height: 100%; } 26 | .ff_dialogwrap .ff_formwrapinner > form { display: flex; flex-direction: column; overflow: hidden; height: 100%; } 27 | .ff_dialogwrap .ff_formwrapinner .formfields { flex: 1; overflow: auto; width: 100%; height: 100%; } 28 | .ff_dialogwrap .ff_formwrapinner .formfieldsinner { padding: 0 1em 1em 1em; } 29 | .ff_dialogwrap .ff_formwrapinner .formsubmit { margin: 0; padding: 0.5em 0.6em 0.5em; border-top: 1px solid #E0E0E0; } 30 | .ff_dialogwrap .ff_formwrapinner .formsubmitinner { display: flex; justify-content: center; } 31 | .ff_dialogwrap .ff_formwrap .ff_formwrapinner .formsubmit input { font-weight: normal; min-width: 7.5em; margin-left: 0.2em; margin-right: 0.2em; } 32 | 33 | .ff_dialogwrap .ff_formwrap .ff_formwrapinner .formitem { margin-top: 1.1em; } 34 | .ff_dialogwrap .ff_formwrap .ff_formwrapinner .fieldtablewrap > table.rowwrap > tbody > tr > td .formitem { margin-top: 0; } 35 | 36 | .ff_dialogwrap .ff_formwrap .ff_formwrapinner .formitem .formitemdata select { font-size: 1.0em; } 37 | 38 | .ff_dialogwrap .ff_formwrap .ff_formwrapinner .fieldtablewrap { margin-top: 1.1em; } 39 | .ff_dialogwrap .ff_formwrap .ff_formwrapinner .fieldtablewrap > table.rowwrap { border-collapse: collapse; width: 100%; } 40 | .ff_dialogwrap .ff_formwrap .ff_formwrapinner .fieldtablewrap > table.rowwrap > tbody > tr > td { padding: 1.0em 1.0em 0 0; vertical-align: top; } 41 | .ff_dialogwrap .ff_formwrap .ff_formwrapinner .fieldtablewrap > table.rowwrap > tbody > tr:first-child > td { padding-top: 0; } 42 | .ff_dialogwrap .ff_formwrap .ff_formwrapinner .formfieldsresponsive .fieldtablewrap > table.rowwrap > tbody > tr > td:nth-last-child(2) { padding-right: 0; } 43 | .ff_dialogwrap .ff_formwrap .ff_formwrapinner .fieldtablewrap > table.rowwrap > tbody > tr > td:last-child { padding-right: 0; } 44 | .ff_dialogwrap .ff_formwrap .ff_formwrapinner .fieldtablewrap .formitemtitle { white-space: nowrap; } 45 | 46 | .ff_dialogwrap.ff_dialogwrap_small .ff_formwrap .ff_formwrapinner .fieldtablewrap { margin-top: 0; } 47 | .ff_dialogwrap.ff_dialogwrap_small .ff_formwrap .ff_formwrapinner .fieldtablewrap > table.rowwrap { display: block; } 48 | .ff_dialogwrap.ff_dialogwrap_small .ff_formwrap .ff_formwrapinner .fieldtablewrap > table.rowwrap > tbody { display: block; } 49 | .ff_dialogwrap.ff_dialogwrap_small .ff_formwrap .ff_formwrapinner .fieldtablewrap > table.rowwrap > tbody > tr { display: block; } 50 | .ff_dialogwrap.ff_dialogwrap_small .ff_formwrap .ff_formwrapinner .fieldtablewrap > table.rowwrap > tbody > tr > td { display: block; padding: 0; margin-top: 1.0em; max-width: 100%; } 51 | .ff_dialogwrap.ff_dialogwrap_small .ff_formwrap .ff_formwrapinner .formfieldsresponsive .fieldtablewrap > table.rowwrap > tbody > tr > td:last-child { display: none; } 52 | 53 | .ff_dialog_overlay { position: fixed; left: 0; top: 0; width: 100%; height: 100%; z-index: 10000; } 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Offline Forms 2 | ============= 3 | 4 | A form designer and data gathering tool for use in areas with spotty or unknown Internet connectivity. Powered by any standard web browser. Offline Forms is a useful solution to the problem of digitally gathering data even when Internet access is not functional or only partially functional. Choose from a MIT or LGPL license. 5 | 6 | [![Offline Forms Screenshot](https://user-images.githubusercontent.com/1432111/196460161-52411c41-359c-4fc9-ad37-3153a7986fe5.png)](https://cubiclesoft.com/demos/offline-forms/index.html) 7 | 8 | [Live demo](https://cubiclesoft.com/demos/offline-forms/index.html) 9 | 10 | [![Donate](https://cubiclesoft.com/res/donate-shield.png)](https://cubiclesoft.com/donate/) [![Discord](https://img.shields.io/discord/777282089980526602?label=chat&logo=discord)](https://cubiclesoft.com/product-support/github/) 11 | 12 | Features 13 | -------- 14 | 15 | * Can be loaded one time from your web server and then saved offline. See below for details. 16 | * Excellent for trade shows, expos, and conferences for gathering potential client information (e.g. newsletter signups). 17 | * Great for use in areas where Internet connectivity might be spotty or nonexistent (e.g. gathering door-to-door survey responses). 18 | * Blazing fast. Changes to what is displayed happen almost instantaneously. 19 | * Password protected access to manage forms. With caveats - see below. 20 | * Supports custom HTML and Javascript. 21 | * Export and import prepared offline forms. Create on a desktop and then export the form and subsequently import the form on a phone or tablet. 22 | * Export submitted data as CSV and JSON Lines. 23 | * Has a liberal open source license. MIT or LGPL, your choice. 24 | * Designed for relatively painless integration into your data gathering project. 25 | * Sits on GitHub for all of that pull request and issue tracker goodness to easily submit changes and ideas respectively. 26 | 27 | Getting Started 28 | --------------- 29 | 30 | Just follow this simple, 5-step process: 31 | 32 | 1. Upload the contents of this repository to your own web server. Alternatively, use the [Live demo](https://cubiclesoft.com/demos/offline-forms/index.html). 33 | 2. Visit the URL on each device. 34 | 3. Save the page offline (see below). 35 | 4. Access the saved offline page. 36 | 5. Follow the instructions to set a password and start creating and using Offline Forms. 37 | 38 | Offline Use 39 | ----------- 40 | 41 | Each device and web browser has a different way to save webpages offline so they are accessible even when Internet connectivity is spotty or not working at all. Even different versions of device + OS + web browser combination can have vastly different instructions. A search for "make website available offline" plus browser name plus OS or device (or all three) will generally turn up reasonably decent results. 42 | 43 | Here are some helpful Google searches for making web pages available offline in the most popular OSes and web browsers: 44 | 45 | * [Apple/iOS + Safari](https://www.google.com/search?q=make+website+available+offline+ios) 46 | * [Android + Chrome](https://www.google.com/search?q=make+website+available+offline+android) 47 | * [Chrome](https://www.google.com/search?q=make+website+available+offline+chrome) 48 | * [Firefox](https://www.google.com/search?q=make+website+available+offline+firefox) 49 | * [Edge](https://www.google.com/search?q=make+website+available+offline+edge) 50 | * [Safari](https://www.google.com/search?q=make+website+available+offline+safari) 51 | * [Opera](https://www.google.com/search?q=make+website+available+offline+opera) 52 | 53 | Once Offline Forms has been made offline, a good verification test is to go into Airplane mode and try to load Offline Forms. If it still loads, then both the core application and its dependencies will work entirely offline. 54 | 55 | Example Form 56 | ------------ 57 | 58 | Copy and paste the following JSON to import a working example form for handling email newsletter signups: 59 | 60 | ``` 61 | {"id":"form_example_1","title":"Subscribe to our Newsletters","desc":"We have a couple of really great newsletters offering everything from product information to early bird seasonal sales.","init_js":"","enabled":true,"thanks":"Thank you for signing up for our newsletter(s). We really appreciate it and look forward to interacting with you in the future.","options":{"fields":["startrow",{"type":"text","title":"First name","width":"19em","required":true,"name":"firstname","default":"","desc":"","validator":""},{"type":"text","title":"Last name","width":"18em","required":true,"name":"lastname","default":"","desc":"","validator":""},"endrow",{"type":"text","subtype":"email","title":"Email address","width":"38em","required":true,"name":"email","default":"","desc":"We won't spam you or sell your email address.","validator":""},{"type":"checkbox","required":false,"name":"list_main","value":"","display":"Main newsletter - Get new and updated product announcements monthly.","default":true,"desc":"","validator":""},{"type":"checkbox","required":false,"name":"list_deals","value":"","display":"Deals newsletter - The best deals in the business delivered to your inbox.","default":true,"desc":"","validator":""}],"submit":"Submit"}} 62 | ``` 63 | 64 | The form is the same as the screenshot at the top of this README. 65 | 66 | Security Considerations 67 | ----------------------- 68 | 69 | Everything regarding Offline Forms is stored in the local web browser using what is known as Local Storage (aka `window.localStorage`). The management interface may be password protected and the data gathered is stored in Local Storage as well but the way these items are stored means it is relatively easy to circumvent the limited protections in place and extract all of the information that has been stored there. An attacker could also inject Javascript into the Offline Forms application such that it results in an [Advanced Persistent Threat (APT)](https://en.wikipedia.org/wiki/Advanced_persistent_threat). 70 | 71 | Unfortunately, there is no particularly good way to secure Local Storage to create a functional digital rights management (DRM) zone. This tool should therefore not be used to gather and store: User passwords, credit card numbers, social security numbers (SSNs), bank account information, or anything else that, if leaked by an attacker (or even an unscrupulous employee) who gains access to the information, could be severely damaging to the reputation or image of an individual or organization. The password used to protect the management interface is only meant as a deterrent and not an actual security mechanism. Someone signing up for a newsletter or supplying inconsequential contact information, which is the main purpose of Offline Forms, is vastly different from paying for goods and services. 72 | -------------------------------------------------------------------------------- /support/jquery.tablednd.min.js: -------------------------------------------------------------------------------- 1 | !function(t,e,a,n){var i="ontouchstart"in a.documentElement,r="touchstart mousedown",l="touchmove mousemove",o="touchend mouseup";i&&t.each("touchstart touchmove touchend".split(" "),function(e,a){try{t.event.fixHooks[a]=t.event.mouseHooks}catch(n){}}),t(a).ready(function(){function e(t){for(var e={},a=t.match(/([^;:]+)/g)||[];a.length;)e[a.shift()]=a.shift().trim();return e}t("table").each(function(){"dnd"==t(this).data("table")&&t(this).tableDnD({onDragStyle:t(this).data("ondragstyle")&&e(t(this).data("ondragstyle"))||null,onDropStyle:t(this).data("ondropstyle")&&e(t(this).data("ondropstyle"))||null,onDragClass:t(this).data("ondragclass")==n&&"tDnD_whileDrag"||t(this).data("ondragclass"),onDrop:t(this).data("ondrop")&&new Function("table","row",t(this).data("ondrop")),onDragStart:t(this).data("ondragstart")&&new Function("table","row",t(this).data("ondragstart")),onDragStop:t(this).data("ondragstop")&&new Function("table","row",t(this).data("ondragstop")),scrollAmount:t(this).data("scrollamount")||5,sensitivity:t(this).data("sensitivity")||10,hierarchyLevel:t(this).data("hierarchylevel")||0,indentArtifact:t(this).data("indentartifact")||'
 
',autoWidthAdjust:t(this).data("autowidthadjust")||!0,autoCleanRelations:t(this).data("autocleanrelations")||!0,jsonPretifySeparator:t(this).data("jsonpretifyseparator")||" ",serializeRegexp:t(this).data("serializeregexp")&&new RegExp(t(this).data("serializeregexp"))||/[^\-]*$/,serializeParamName:t(this).data("serializeparamname")||!1,dragHandle:t(this).data("draghandle")||null})})}),jQuery.tableDnD={currentTable:null,dragObject:null,mouseOffset:null,oldX:0,oldY:0,build:function(e){return this.each(function(){this.tableDnDConfig=t.extend({onDragStyle:null,onDropStyle:null,onDragClass:"tDnD_whileDrag",onDrop:null,onDragStart:null,onDragStop:null,scrollAmount:5,sensitivity:10,hierarchyLevel:0,indentArtifact:'
 
',autoWidthAdjust:!0,autoCleanRelations:!0,jsonPretifySeparator:" ",serializeRegexp:/[^\-]*$/,serializeParamName:!1,dragHandle:null},e||{}),t.tableDnD.makeDraggable(this),this.tableDnDConfig.hierarchyLevel&&t.tableDnD.makeIndented(this)}),this},makeIndented:function(e){var a,n,i=e.tableDnDConfig,r=e.rows,l=t(r).first().find("td:first")[0],o=0,s=0;if(t(e).hasClass("indtd"))return null;n=t(e).addClass("indtd").attr("style"),t(e).css({whiteSpace:"nowrap"});for(var d=0;dt.vertical&&this.dragObject.parentNode.insertBefore(this.dragObject,e.nextSibling)||00&&t(a).find("td:first").children(":first").remove()&&t(a).data("level",--n),void(0>e.horizontal&&n=n&&t(a).children(":first").prepend(i.indentArtifact)&&t(a).data("level",++n))):null},mousemove:function(e){var a,n,i,r,l,o=t(t.tableDnD.dragObject),s=t.tableDnD.currentTable.tableDnDConfig;return e&&e.preventDefault(),t.tableDnD.dragObject?("touchmove"==e.type&&event.preventDefault(),s.onDragClass&&o.addClass(s.onDragClass)||o.css(s.onDragStyle),n=t.tableDnD.mouseCoords(e),r=n.x-t.tableDnD.mouseOffset.x,l=n.y-t.tableDnD.mouseOffset.y,t.tableDnD.autoScroll(n),a=t.tableDnD.findDropTargetRow(o,l),i=t.tableDnD.findDragDirection(r,l),t.tableDnD.moveVerticle(i,a),t.tableDnD.moveHorizontal(i,a),!1):!1},findDragDirection:function(t,e){var a=this.currentTable.tableDnDConfig.sensitivity,n=this.oldX,i=this.oldY,r=n-a,l=n+a,o=i-a,s=i+a,d={horizontal:t>=r&&l>=t?0:t>n?-1:1,vertical:e>=o&&s>=e?0:e>i?-1:1};return 0!=d.horizontal&&(this.oldX=t),0!=d.vertical&&(this.oldY=e),d},findDropTargetRow:function(e,a){for(var n=0,i=this.currentTable.rows,r=this.currentTable.tableDnDConfig,l=0,o=null,s=0;sl-n&&l+n>a)return e.is(o)||r.onAllowDrop&&!r.onAllowDrop(e,o)||t(o).hasClass("nodrop")?null:o;return null},processMouseup:function(){if(!this.currentTable||!this.dragObject)return null;var e=this.currentTable.tableDnDConfig,n=this.dragObject,i=0,r=0;t(a).unbind(l,this.mousemove).unbind(o,this.mouseup),e.hierarchyLevel&&e.autoCleanRelations&&t(this.currentTable.rows).first().find("td:first").children().each(function(){r=t(this).parents("tr:first").data("level"),r&&t(this).parents("tr:first").data("level",--r)&&t(this).remove()})&&e.hierarchyLevel>1&&t(this.currentTable.rows).each(function(){if(r=t(this).data("level"),r>1)for(i=t(this).prev().data("level");r>i+1;)t(this).find("td:first").children(":first").remove(),t(this).data("level",--r)}),e.onDragClass&&t(n).removeClass(e.onDragClass)||t(n).css(e.onDropStyle),this.dragObject=null,e.onDrop&&this.originalOrder!=this.currentOrder()&&t(n).hide().fadeIn("fast")&&e.onDrop(this.currentTable,n),e.onDragStop&&e.onDragStop(this.currentTable,n),this.currentTable=null},mouseup:function(e){return e&&e.preventDefault(),t.tableDnD.processMouseup(),!1},jsonize:function(t){var e=this.currentTable;return t?JSON.stringify(this.tableData(e),null,e.tableDnDConfig.jsonPretifySeparator):JSON.stringify(this.tableData(e))},serialize:function(){return t.param(this.tableData(this.currentTable))},serializeTable:function(t){for(var e="",a=t.tableDnDConfig.serializeParamName||t.id,n=t.rows,i=0;i0&&(e+="&");var r=n[i].id;r&&t.tableDnDConfig&&t.tableDnDConfig.serializeRegexp&&(r=r.match(t.tableDnDConfig.serializeRegexp)[0],e+=a+"[]="+r)}return e},serializeTables:function(){var e=[];return t("table").each(function(){this.id&&e.push(t.param(this.tableData(this)))}),e.join("&")},tableData:function(e){var a,n,i,r,l=e.tableDnDConfig,o=[],s=0,d=0,h=null,u={};if(e||(e=this.currentTable),!(e&&e.id&&e.rows&&e.rows.length))return{error:{code:500,message:"Not a valid table, no serializable unique id provided."}};r=l.autoCleanRelations&&e.rows||t.makeArray(e.rows),n=l.serializeParamName||e.id,i=n,a=function(t){return t&&l&&l.serializeRegexp?t.match(l.serializeRegexp)[0]:t},u[i]=[],!l.autoCleanRelations&&t(r[0]).data("level")&&r.unshift({id:"undefined"});for(var c=0;cs)o.push([i,s]),i=a(r[c-1].id);else if(s>d)for(var f=0;f=s&&(o[f][1]=0);s=d,t.isArray(u[i])||(u[i]=[]),h=a(r[c].id),h&&u[i].push(h)}else h=a(r[c].id),h&&u[i].push(h);return u}},jQuery.fn.extend({tableDnD:t.tableDnD.build,tableDnDUpdate:t.tableDnD.updateTables,tableDnDSerialize:t.proxy(t.tableDnD.serialize,t.tableDnD),tableDnDSerializeAll:t.tableDnD.serializeTables,tableDnDData:t.proxy(t.tableDnD.tableData,t.tableDnD)})}(jQuery,window,window.document); -------------------------------------------------------------------------------- /support/sha256.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A JavaScript implementation of the SHA family of hashes - defined in FIPS PUB 180-4, FIPS PUB 202, 3 | * and SP 800-185 - as well as the corresponding HMAC implementation as defined in FIPS PUB 198-1. 4 | * 5 | * Copyright 2008-2021 Brian Turek, 1998-2009 Paul Johnston & Contributors 6 | * Distributed under the BSD License 7 | * See http://caligatio.github.com/jsSHA/ for more information 8 | * 9 | * Two ECMAScript polyfill functions carry the following license: 10 | * 11 | * Copyright (c) Microsoft Corporation. All rights reserved. 12 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with 13 | * the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 14 | * 15 | * THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, 16 | * INCLUDING WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, 17 | * MERCHANTABLITY OR NON-INFRINGEMENT. 18 | * 19 | * See the Apache Version 2.0 License for specific language governing permissions and limitations under the License. 20 | */ 21 | !function(r,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(r="undefined"!=typeof globalThis?globalThis:r||self).jsSHA=t()}(this,(function(){"use strict";var r=function(t,n){return(r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(r,t){r.__proto__=t}||function(r,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(r[n]=t[n])})(t,n)};var t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",n="ARRAYBUFFER not supported by this environment",i="UINT8ARRAY not supported by this environment";function e(r,t,n,i){var e,o,u,s=t||[0],f=(n=n||0)>>>3,h=-1===i?3:0;for(e=0;e>>2,s.length<=o&&s.push(0),s[o]|=r[e]<<8*(h+i*(u%4));return{value:s,binLen:8*r.length+n}}function o(r,o,u){switch(o){case"UTF8":case"UTF16BE":case"UTF16LE":break;default:throw new Error("encoding must be UTF8, UTF16BE, or UTF16LE")}switch(r){case"HEX":return function(r,t,n){return function(r,t,n,i){var e,o,u,s;if(0!=r.length%2)throw new Error("String of HEX type must be in byte increments");var f=t||[0],h=(n=n||0)>>>3,a=-1===i?3:0;for(e=0;e>>1)+h)>>>2;f.length<=u;)f.push(0);f[u]|=o<<8*(a+i*(s%4))}return{value:f,binLen:4*r.length+n}}(r,t,n,u)};case"TEXT":return function(r,t,n){return function(r,t,n,i,e){var o,u,s,f,h,a,c,w,v=0,E=n||[0],l=(i=i||0)>>>3;if("UTF8"===t)for(c=-1===e?3:0,s=0;s(o=r.charCodeAt(s))?u.push(o):2048>o?(u.push(192|o>>>6),u.push(128|63&o)):55296>o||57344<=o?u.push(224|o>>>12,128|o>>>6&63,128|63&o):(s+=1,o=65536+((1023&o)<<10|1023&r.charCodeAt(s)),u.push(240|o>>>18,128|o>>>12&63,128|o>>>6&63,128|63&o)),f=0;f>>2;E.length<=h;)E.push(0);E[h]|=u[f]<<8*(c+e*(a%4)),v+=1}else for(c=-1===e?2:0,w="UTF16LE"===t&&1!==e||"UTF16LE"!==t&&1===e,s=0;s>>8),h=(a=v+l)>>>2;E.length<=h;)E.push(0);E[h]|=o<<8*(c+e*(a%4)),v+=2}return{value:E,binLen:8*v+i}}(r,o,t,n,u)};case"B64":return function(r,n,i){return function(r,n,i,e){var o,u,s,f,h,a,c=0,w=n||[0],v=(i=i||0)>>>3,E=-1===e?3:0,l=r.indexOf("=");if(-1===r.search(/^[a-zA-Z0-9=+/]+$/))throw new Error("Invalid character in base-64 string");if(r=r.replace(/=/g,""),-1!==l&&l table.rowwrap { display: block; } 110 | .fieldtablewrap > table.rowwrap > tbody { display: block; } 111 | .fieldtablewrap > table.rowwrap > tbody > tr { display: block; } 112 | .fieldtablewrap > table.rowwrap > tbody > tr > td { display: block; padding: 0; margin-top: 1.0em; max-width: 100%; } 113 | .formfieldsresponsive .fieldtablewrap > table.rowwrap > tbody > tr > td:last-child { display: none; } 114 | } 115 | 116 | 117 | /* FlexForms Extras styles for Admin Pack */ 118 | 119 | .formaccordionwrap.ui-accordion h3.ui-accordion-header { margin-top: 1.0em; border: 1px solid #CCCCCC; background: #F6F6F6; font-weight: bold; color: #222222; } 120 | .formaccordionwrap.ui-accordion:first-child h3.ui-accordion-header:first-child { margin-top: 0; } 121 | .formaccordionwrap.ui-accordion h3.ui-accordion-header.ui-state-hover { border: 1px solid #C5C5C5; background: #F0F0F0; font-weight: bold; color: #222222; } 122 | .formaccordionwrap.ui-accordion h3.ui-accordion-header .ui-icon { background-image: url("jquery_ui_themes/adminpack/images/ui-icons_333333_256x240.png"); } 123 | .formaccordionwrap.ui-accordion h3.ui-accordion-header.ui-state-active { border: 1px solid #444444; background: #222222; font-weight: bold; color: #ffffff; } 124 | .formaccordionwrap.ui-accordion h3.ui-accordion-header.ui-state-active .ui-icon { background-image: url("jquery_ui_themes/adminpack/images/ui-icons_fafafa_256x240.png"); } 125 | .formaccordionwrap.ui-accordion h3.ui-accordion-header.ui-state-active.ui-state-hover { border: 1px solid #444444; background: #333333; font-weight: bold; color: #ffffff; } 126 | 127 | .formaccordionwrap.ui-accordion .ui-helper-reset { line-height: normal; } 128 | .formaccordionwrap.ui-accordion .formaccordionitems.ui-widget-content a { color: #4E88C2; text-decoration: none; } 129 | .formaccordionwrap.ui-accordion .formaccordionitems.ui-widget-content a:hover { color: #297ACC; text-decoration: underline; } 130 | .formaccordionwrap.ui-accordion .formaccordionitems.ui-accordion-content { padding: 0 1.5em 1.0em; } 131 | .formaccordionwrap.ui-accordion .formaccordionitems.ui-accordion-content > .formitem:first-child { margin-top: 1.0em; } 132 | 133 | .formitemdata input.date { box-sizing: border-box; width: 100%; font-size: 0.9em; padding: 0.3em; border: 1px solid #BBBBBB; } 134 | .formitemdata input.date:focus, .formitemdata input.date:hover { border: 1px solid #888888; } 135 | 136 | .ui-state-default.ui-state-highlight, .ui-widget-content .ui-state-default.ui-state-highlight { border: 1px solid #c5c5c5; background: #f6f6f6; color: #454545; } 137 | .ui-state-active.ui-state-highlight, .ui-widget-content .ui-state-active.ui-state-highlight { border: 1px solid #444444; background: #222222; color: #ffffff; } 138 | .ui-state-hover.ui-state-highlight, .ui-widget-content .ui-state-hover.ui-state-highlight { border: 1px solid #444444; background: #333333; color: #ffffff; } 139 | 140 | .formitemdata .uix-multiselect .ui-widget-header div.header-text { white-space: normal; } 141 | .formitemdata .uix-multiselect .ui-widget-header { padding-top: 0.3em; padding-bottom: 0.3em; } 142 | .formitemdata .uix-multiselect .ui-widget-header .uix-control-right { padding-top: 0.8em; } 143 | 144 | .formitemdata .ui-multiselect { padding: 0.3em; } 145 | .formitemdata .ui-multiselect.ui-widget.ui-state-default { border: 1px solid #444444; background: #333333; color: #ffffff; } 146 | .formitemdata .ui-multiselect.ui-widget.ui-state-default span.ui-icon { background-image: url("jquery_ui_themes/adminpack/images/ui-icons_ffffff_256x240.png"); } 147 | .formitemdata .ui-multiselect.ui-widget.ui-state-active { border: 1px solid #444444; background: #222222; color: #ffffff; } 148 | .formitemdata .ui-multiselect.ui-widget.ui-state-hover { border: 1px solid #444444; background: #222222; color: #ffffff; } 149 | .formitemdata .ui-multiselect span.ui-icon { margin-top: 0.2em; } 150 | 151 | .formitemdata .select2-container { display: block; } 152 | .formitemdata .select2-container-multi .select2-choices .select2-search-choice { line-height: 17px; } 153 | .formitemdata .select2-search-choice-close { top: 5px; } 154 | .select2-results .select2-highlighted { background-color: #444444 !important; } 155 | 156 | .formitemdata .draghandle { cursor: move; } 157 | .formitemdata .dragactive { background-color: #E3E3E3; } 158 | 159 | .formitemdata table.tablecards > thead > th:nth-last-child(2) { border-right: 1px solid #222222; } 160 | .formitemdata table.tablecards.tablecard-show > thead > th:nth-last-child(2) { border-right: 1px solid #CCCCCC; } 161 | .formitemdata table.tablecard-show { width: 100%; } 162 | .formitemdata table.tablecard-show-nohead > tbody > tr:first-child > td { border-top: 1px solid #CCCCCC; } 163 | 164 | .formitemdata .tablebodyscroll-scroller3 > table.tablecard-show-nohead > thead > tr { display: none; } 165 | 166 | .formitemdata .tablebodyscroll-shadow-top .tablebodyscroll-scroller-shadow-top, .formitemdata .tablebodyscroll-shadow-both .tablebodyscroll-scroller-shadow-top { border-top: 1px solid #CCCCCC; } 167 | 168 | 169 | /* FlexForms Modules styles for Admin Pack */ 170 | 171 | .formitemdata table.tablecard-show .calendar_day_of_week { margin-top: 0.5em; border-top: 1px solid #E0E0E0; padding-top: 0.5em; font-weight: bold; } 172 | .formitemdata table.tablecard-show .calendar_day_of_week:first-child { margin-top: 0; border-top: none; padding-top: 0; } 173 | 174 | .formitemdata .tablefiltersearchwrap { margin-bottom: 0.5em; } 175 | .formitemdata table.ff_tablefilter > tbody > tr.visible { background-color: #FFFFFF; } 176 | .formitemdata table.ff_tablefilter > tbody > tr.visible.altrow { background-color: #F6F6F6; } 177 | .formitemdata table.ff_tablefilter > tbody > tr.visible.lastrow { border-bottom: 1px solid #CCCCCC; } 178 | -------------------------------------------------------------------------------- /support/flex_forms_dialog.js: -------------------------------------------------------------------------------- 1 | // FlexForms Javascript Dialog class. 2 | // (C) 2022 CubicleSoft. All Rights Reserved. 3 | 4 | (function() { 5 | if (!window.hasOwnProperty('FlexForms') || !window.FlexForms.hasOwnProperty('Designer')) 6 | { 7 | console.error('[FlexForms.Dialog] Error: FlexForms and FlexForms.Designer must be loaded before FlexForms.Dialog.'); 8 | 9 | return; 10 | } 11 | 12 | if (window.FlexForms.hasOwnProperty('Dialog')) return; 13 | 14 | var EscapeHTML = window.FlexForms.EscapeHTML; 15 | var FormatStr = window.FlexForms.FormatStr; 16 | var CreateNode = window.FlexForms.CreateNode; 17 | var DebounceAttributes = window.FlexForms.DebounceAttributes; 18 | var Translate = window.FlexForms.Translate; 19 | 20 | var DialogInternal = function(parentelem, options) { 21 | if (!(this instanceof DialogInternal)) return new DialogInternal(parentelem, options); 22 | 23 | var triggers = {}; 24 | var $this = this; 25 | 26 | var defaults = { 27 | modal: true, 28 | backgroundcloses: false, 29 | move: true, 30 | resize: true, 31 | 32 | title: '', 33 | content: {}, 34 | errors: {}, 35 | request: {}, 36 | 37 | onposition: null, 38 | onsubmit: null, 39 | onclose: null, 40 | ondestroy: null, 41 | 42 | langmap: {} 43 | }; 44 | 45 | $this.settings = Object.assign({}, defaults, options); 46 | 47 | Object.assign(window.FlexForms.settings.langmap, $this.settings.langmap); 48 | 49 | // Internal functions. 50 | var DispatchEvent = function(eventname, params) { 51 | if (!triggers[eventname]) return; 52 | 53 | triggers[eventname].forEach(function(callback) { 54 | if (Array.isArray(params)) callback.apply($this, params); 55 | else callback.call($this, params); 56 | }); 57 | }; 58 | 59 | // Public DOM-style functions. 60 | $this.addEventListener = function(eventname, callback) { 61 | if (!triggers[eventname]) triggers[eventname] = []; 62 | 63 | for (var x in triggers[eventname]) 64 | { 65 | if (triggers[eventname][x] === callback) return; 66 | } 67 | 68 | triggers[eventname].push(callback); 69 | }; 70 | 71 | $this.removeEventListener = function(eventname, callback) { 72 | if (!triggers[eventname]) return; 73 | 74 | for (var x in triggers[eventname]) 75 | { 76 | if (triggers[eventname][x] === callback) 77 | { 78 | triggers[eventname].splice(x, 1); 79 | 80 | return; 81 | } 82 | } 83 | }; 84 | 85 | $this.hasEventListener = function(eventname) { 86 | return (triggers[eventname] && triggers[eventname].length); 87 | }; 88 | 89 | // Register settings callbacks. 90 | if ($this.settings.onposition) $this.addEventListener('position', $this.settings.onposition); 91 | if ($this.settings.onsubmit) $this.addEventListener('submit', $this.settings.onsubmit); 92 | if ($this.settings.onclose) $this.addEventListener('close', $this.settings.onclose); 93 | if ($this.settings.ondestroy) $this.addEventListener('destroy', $this.settings.ondestroy); 94 | 95 | var elems = { 96 | mainwrap: CreateNode('div', ['ff_dialogwrap'], { tabIndex: 0 }, { position: 'fixed', left: '-9999px' }), 97 | resizer: CreateNode('div', ['ff_dialog_resizer']), 98 | measureemsize: CreateNode('div', ['ff_dialog_measure_em_size']), 99 | innerwrap: CreateNode('div', ['ff_dialog_innerwrap']), 100 | 101 | titlewrap: CreateNode('div', ['ff_dialog_titlewrap']), 102 | title: CreateNode('div', ['ff_dialog_title']), 103 | closebutton: CreateNode('button', ['ff_dialog_close'], { title: Translate('Close') }), 104 | 105 | overlay: CreateNode('div', ['ff_dialog_overlay']), 106 | 107 | formwrap: null 108 | }; 109 | 110 | elems.title.innerHTML = EscapeHTML(Translate($this.settings.title)); 111 | 112 | elems.titlewrap.appendChild(elems.title); 113 | if ($this.settings.onclose || $this.settings.backgroundcloses) elems.titlewrap.appendChild(elems.closebutton); 114 | elems.innerwrap.appendChild(elems.titlewrap); 115 | 116 | if ($this.settings.resize) elems.mainwrap.appendChild(elems.resizer); 117 | elems.mainwrap.appendChild(elems.measureemsize); 118 | elems.mainwrap.appendChild(elems.innerwrap); 119 | 120 | // Handle submit buttons. 121 | var SubmitHandler = function(e) { 122 | if (!e.isTrusted) return; 123 | 124 | e.preventDefault(); 125 | 126 | $this.settings.request = FlexForms.GetFormVars(elems.formnode, e); 127 | 128 | DispatchEvent('submit', [$this.settings.request, elems.formnode, e, lastactiveelem]); 129 | }; 130 | 131 | // Useful helper function to return whether or not the errors object contains errors. 132 | $this.HasErrors = function() { 133 | return (Object.keys($this.settings.errors).length > 0); 134 | }; 135 | 136 | // Regenerate and append the form. 137 | $this.UpdateContent = function() { 138 | if (elems.formwrap) elems.formwrap.parentNode.removeChild(elems.formwrap); 139 | 140 | elems.formwrap = FlexForms.Generate(elems.innerwrap, $this.settings.content, $this.settings.errors, $this.settings.request); 141 | elems.formnode = elems.formwrap.querySelector('form'); 142 | 143 | elems.formnode.addEventListener('submit', SubmitHandler); 144 | }; 145 | 146 | if ($this.settings.modal) parentelem.appendChild(elems.overlay); 147 | parentelem.appendChild(elems.mainwrap); 148 | 149 | $this.UpdateContent(); 150 | 151 | // Returns the internal elements object for easier access to various elements. 152 | $this.GetElements = function() { 153 | return elems; 154 | }; 155 | 156 | // Set up focusing rules. 157 | var lastactiveelem = document.activeElement; 158 | var hasfocus = false; 159 | 160 | var MainWrapMouseBlurHandler = function(e) { 161 | if (!e.isTrusted) return; 162 | 163 | var node = e.target; 164 | while (node && node !== elems.mainwrap) node = node.parentNode; 165 | 166 | if (node === elems.mainwrap) elems.mainwrap.classList.add('ff_dialog_focused'); 167 | else 168 | { 169 | if ($this.settings.modal) 170 | { 171 | hasfocus = false; 172 | 173 | e.preventDefault(); 174 | 175 | elems.mainwrap.focus(); 176 | } 177 | else 178 | { 179 | elems.mainwrap.classList.remove('ff_dialog_focused'); 180 | 181 | if (hasfocus && $this.settings.backgroundcloses) 182 | { 183 | lastactiveelem = e.target; 184 | 185 | $this.Close(); 186 | } 187 | 188 | hasfocus = false; 189 | } 190 | } 191 | }; 192 | 193 | window.addEventListener('mousedown', MainWrapMouseBlurHandler, true); 194 | 195 | // Trigger window blur visual appearance changes. 196 | var MainWrapWindowBlurHandler = function(e) { 197 | if (e.target === window || e.target === document) elems.mainwrap.classList.remove('ff_dialog_focused'); 198 | }; 199 | 200 | window.addEventListener('blur', MainWrapWindowBlurHandler, true); 201 | 202 | var MainWrapFocusHandler = function(e) { 203 | // Handle window-level focus events specially. There will be another focus event if actually focused. 204 | if (!$this.settings.modal && (e.target === window || e.target === document)) 205 | { 206 | var node = document.activeElement; 207 | while (node && node !== elems.mainwrap) node = node.parentNode; 208 | 209 | if (node === elems.mainwrap) elems.mainwrap.classList.add('ff_dialog_focused'); 210 | 211 | return; 212 | } 213 | 214 | var node = e.target; 215 | while (node && node !== elems.mainwrap) node = node.parentNode; 216 | 217 | if (node === elems.mainwrap) 218 | { 219 | elems.mainwrap.classList.add('ff_dialog_focused'); 220 | 221 | // Move this dialog to the top of the stack. 222 | if (!hasfocus) 223 | { 224 | window.removeEventListener('focus', MainWrapFocusHandler, true); 225 | 226 | lastactiveelem.focus(); 227 | 228 | parentelem.appendChild(elems.mainwrap); 229 | 230 | elems.mainwrap.focus(); 231 | 232 | window.addEventListener('focus', MainWrapFocusHandler, true); 233 | } 234 | 235 | hasfocus = true; 236 | } 237 | else if ($this.settings.modal) 238 | { 239 | elems.mainwrap.focus(); 240 | } 241 | else 242 | { 243 | elems.mainwrap.classList.remove('ff_dialog_focused'); 244 | 245 | hasfocus = false; 246 | } 247 | }; 248 | 249 | window.addEventListener('focus', MainWrapFocusHandler, true); 250 | 251 | // Some internal tracking variables to control dialog position and size. 252 | var manualsize = false, manualmove = false; 253 | var screenwidth, screenheight, currdialogstyle, dialogwidth, dialogheight; 254 | 255 | // Adjust the dialog and recalculate size information. 256 | $this.UpdateSizes = function() { 257 | elems.mainwrap.classList.remove('ff_dialogwrap_small'); 258 | 259 | if (elems.mainwrap.offsetWidth / elems.measureemsize.offsetWidth < 27) elems.mainwrap.classList.add('ff_dialogwrap_small'); 260 | 261 | screenwidth = (document.documentElement.clientWidth || document.body.clientWidth || window.innerWidth); 262 | screenheight = (document.documentElement.clientHeight || document.body.clientHeight || window.innerHeight); 263 | 264 | if (!manualsize) elems.mainwrap.style.height = null; 265 | 266 | currdialogstyle = elems.mainwrap.currentStyle || window.getComputedStyle(elems.mainwrap); 267 | dialogwidth = elems.mainwrap.offsetWidth + parseFloat(currdialogstyle.marginLeft) + parseFloat(currdialogstyle.marginRight); 268 | dialogheight = elems.mainwrap.offsetHeight + parseFloat(currdialogstyle.marginTop) + parseFloat(currdialogstyle.marginBottom); 269 | 270 | if (!manualsize && dialogheight >= screenheight) 271 | { 272 | elems.mainwrap.style.height = (screenheight - parseFloat(currdialogstyle.marginTop) - parseFloat(currdialogstyle.marginBottom) - 2) + 'px'; 273 | 274 | dialogheight = screenheight; 275 | } 276 | }; 277 | 278 | // Snaps the dialog so it fits on the screen. 279 | $this.SnapToScreen = function() { 280 | var currleft = elems.mainwrap.offsetLeft - parseFloat(currdialogstyle.marginLeft); 281 | var currtop = elems.mainwrap.offsetTop - parseFloat(currdialogstyle.marginTop); 282 | 283 | elems.mainwrap.style.left = '0px'; 284 | elems.mainwrap.style.top = '0px'; 285 | 286 | $this.UpdateSizes(); 287 | 288 | if (currleft < 0) currleft = 0; 289 | if (currtop < 0) currtop = 0; 290 | if (currleft + dialogwidth >= screenwidth) currleft = screenwidth - dialogwidth; 291 | if (currtop + dialogheight >= screenheight) currtop = screenheight - dialogheight; 292 | 293 | elems.mainwrap.style.left = currleft + 'px'; 294 | elems.mainwrap.style.top = currtop + 'px'; 295 | elems.mainwrap.style.height = (dialogheight - parseFloat(currdialogstyle.marginTop) - parseFloat(currdialogstyle.marginBottom)) + 'px'; 296 | 297 | DispatchEvent('position', elems.mainwrap); 298 | }; 299 | 300 | // Move the dialog to the center of the screen unless it has been manually moved. 301 | $this.CenterDialog = function() { 302 | if (manualmove) $this.SnapToScreen(); 303 | else 304 | { 305 | $this.UpdateSizes(); 306 | 307 | elems.mainwrap.style.left = ((screenwidth / 2) - (dialogwidth / 2)) + 'px'; 308 | elems.mainwrap.style.top = ((screenheight / 2) - (dialogheight / 2)) + 'px'; 309 | elems.mainwrap.style.height = (dialogheight - parseFloat(currdialogstyle.marginTop) - parseFloat(currdialogstyle.marginBottom)) + 'px'; 310 | 311 | DispatchEvent('position', elems.mainwrap); 312 | } 313 | }; 314 | 315 | // Set up an offsetWidth/offsetHeight attribute watcher that calls CenterDialog(). 316 | var dialogresizewatch = new DebounceAttributes({ 317 | watchers: [ 318 | { elem: elems.mainwrap, attr: 'offsetWidth', val: -1 }, 319 | { elem: elems.mainwrap, attr: 'offsetHeight', val: -1 } 320 | ], 321 | interval: 50, 322 | stopsame: 5, 323 | callback: $this.CenterDialog, 324 | intervalcallback: $this.CenterDialog 325 | }); 326 | 327 | window.addEventListener('resize', dialogresizewatch.Start, true); 328 | 329 | var LoadedHandler = function() { 330 | $this.CenterDialog(); 331 | 332 | elems.mainwrap.classList.add('ff_dialog_focused'); 333 | 334 | // Bypass the hasfocus checks in MainWrapFocusHandler. 335 | hasfocus = true; 336 | 337 | var node = document.activeElement; 338 | while (node && node !== elems.mainwrap) node = node.parentNode; 339 | 340 | if (node !== elems.mainwrap) elems.mainwrap.focus(); 341 | }; 342 | 343 | window.FlexForms.addEventListener('done', LoadedHandler); 344 | 345 | window.FlexForms.LoadCSS('formdialogcss', window.FlexForms.settings.supporturl + '/flex_forms_dialog.css'); 346 | 347 | // Manual move. 348 | var moveanchorpos; 349 | var MoveDialogDragHandler = function(e) { 350 | if (!e.isTrusted) return; 351 | 352 | // Prevent content selections. 353 | e.preventDefault(); 354 | 355 | var newanchorpos; 356 | var rect = elems.title.getBoundingClientRect(); 357 | 358 | if (e.type === 'touchstart') 359 | { 360 | newanchorpos = { 361 | x: e.touches[0].clientX - rect.left, 362 | y: e.touches[0].clientY - rect.top 363 | }; 364 | } 365 | else 366 | { 367 | newanchorpos = { 368 | x: e.clientX - rect.left, 369 | y: e.clientY - rect.top 370 | }; 371 | } 372 | 373 | var newleft = elems.mainwrap.offsetLeft - parseFloat(currdialogstyle.marginLeft) + newanchorpos.x - moveanchorpos.x; 374 | var newtop = elems.mainwrap.offsetTop - parseFloat(currdialogstyle.marginTop) + newanchorpos.y - moveanchorpos.y; 375 | 376 | if (newleft < 0) newleft = 0; 377 | if (newtop < 0) newtop = 0; 378 | if (newleft + dialogwidth >= screenwidth) newleft = screenwidth - dialogwidth; 379 | if (newtop + dialogheight >= screenheight) newtop = screenheight - dialogheight; 380 | 381 | elems.mainwrap.style.left = newleft + 'px'; 382 | elems.mainwrap.style.top = newtop + 'px'; 383 | 384 | manualmove = true; 385 | 386 | DispatchEvent('position', elems.mainwrap); 387 | }; 388 | 389 | var MoveDialogEndHandler = function(e) { 390 | if (e && !e.isTrusted) return; 391 | 392 | moveanchorpos = null; 393 | 394 | window.removeEventListener('touchmove', MoveDialogDragHandler, true); 395 | window.removeEventListener('touchend', MoveDialogEndHandler, true); 396 | window.removeEventListener('mousemove', MoveDialogDragHandler, true); 397 | window.removeEventListener('mouseup', MoveDialogEndHandler, true); 398 | window.removeEventListener('blur', MoveDialogEndHandler, true); 399 | }; 400 | 401 | var StartMoveDialogHandler = function(e) { 402 | if (!e.isTrusted) return; 403 | 404 | // Disallow scrolling on touch and block drag-and-drop. 405 | e.preventDefault(); 406 | 407 | $this.CenterDialog(); 408 | 409 | var rect = elems.title.getBoundingClientRect(); 410 | 411 | if (e.type === 'touchstart') 412 | { 413 | moveanchorpos = { 414 | x: e.touches[0].clientX - rect.left, 415 | y: e.touches[0].clientY - rect.top 416 | }; 417 | 418 | window.addEventListener('touchmove', MoveDialogDragHandler, true); 419 | window.addEventListener('touchend', MoveDialogEndHandler, true); 420 | } 421 | else 422 | { 423 | moveanchorpos = { 424 | x: e.clientX - rect.left, 425 | y: e.clientY - rect.top 426 | }; 427 | 428 | window.addEventListener('mousemove', MoveDialogDragHandler, true); 429 | window.addEventListener('mouseup', MoveDialogEndHandler, true); 430 | } 431 | 432 | window.addEventListener('blur', MoveDialogEndHandler, true); 433 | }; 434 | 435 | elems.title.addEventListener('mousedown', StartMoveDialogHandler); 436 | elems.title.addEventListener('touchstart', StartMoveDialogHandler); 437 | 438 | // Manual resize. 439 | var resizeclass, resizelocation, resizeanchorpos; 440 | var UpdateResizeHoverClass = function(e) { 441 | if (!e.isTrusted || resizeanchorpos) return; 442 | 443 | var rect = elems.mainwrap.getBoundingClientRect(); 444 | var currpos, newresizeclass; 445 | 446 | if (e.type === 'touchstart') 447 | { 448 | currpos = { 449 | x: e.touches[0].clientX, 450 | y: e.touches[0].clientY 451 | }; 452 | } 453 | else 454 | { 455 | currpos = { 456 | x: e.clientX, 457 | y: e.clientY 458 | }; 459 | } 460 | 461 | if (currpos.y < rect.top + elems.measureemsize.offsetWidth) 462 | { 463 | if (currpos.x < rect.left + elems.measureemsize.offsetWidth) { newresizeclass = 'ff_dialog_resize_nwse'; resizelocation = 1; } 464 | else if (currpos.x >= rect.right - elems.measureemsize.offsetWidth) { newresizeclass = 'ff_dialog_resize_nesw'; resizelocation = 3; } 465 | else { newresizeclass = 'ff_dialog_resize_ns'; resizelocation = 2; } 466 | } 467 | else if (currpos.y >= rect.bottom - elems.measureemsize.offsetWidth) 468 | { 469 | if (currpos.x < rect.left + elems.measureemsize.offsetWidth) { newresizeclass = 'ff_dialog_resize_nesw'; resizelocation = 6; } 470 | else if (currpos.x >= rect.right - elems.measureemsize.offsetWidth) { newresizeclass = 'ff_dialog_resize_nwse'; resizelocation = 8; } 471 | else { newresizeclass = 'ff_dialog_resize_ns'; resizelocation = 7; } 472 | } 473 | else 474 | { 475 | if (currpos.x < rect.left) { newresizeclass = 'ff_dialog_resize_ew'; resizelocation = 4; } 476 | else { newresizeclass = 'ff_dialog_resize_ew'; resizelocation = 5; } 477 | } 478 | 479 | if (newresizeclass !== resizeclass) 480 | { 481 | elems.resizer.className = 'ff_dialog_resizer ' + newresizeclass; 482 | 483 | resizeclass = newresizeclass; 484 | } 485 | }; 486 | 487 | elems.resizer.addEventListener('mousemove', UpdateResizeHoverClass); 488 | 489 | var ResizeDialogDragHandler = function(e) { 490 | if (!e.isTrusted) return; 491 | 492 | // Prevent content selections. 493 | e.preventDefault(); 494 | 495 | var newanchorpos; 496 | var rect = elems.resizer.getBoundingClientRect(); 497 | 498 | if (e.type === 'touchstart') 499 | { 500 | newanchorpos = { 501 | x: e.touches[0].clientX - rect.left, 502 | y: e.touches[0].clientY - rect.top 503 | }; 504 | } 505 | else 506 | { 507 | newanchorpos = { 508 | x: e.clientX - rect.left, 509 | y: e.clientY - rect.top 510 | }; 511 | } 512 | 513 | var newleft = elems.mainwrap.offsetLeft - parseFloat(currdialogstyle.marginLeft); 514 | var newtop = elems.mainwrap.offsetTop - parseFloat(currdialogstyle.marginTop); 515 | var newwidth = elems.mainwrap.offsetWidth; 516 | var newheight = elems.mainwrap.offsetHeight; 517 | var diffx = newanchorpos.x - resizeanchorpos.x; 518 | var diffy = newanchorpos.y - resizeanchorpos.y; 519 | 520 | // 1 2 3 521 | // 4 5 522 | // 6 7 8 523 | if (resizelocation === 1 || resizelocation === 4 || resizelocation === 6) 524 | { 525 | if (newwidth - diffx >= parseFloat(currdialogstyle.maxWidth)) diffx = newwidth - parseFloat(currdialogstyle.maxWidth); 526 | else if (newleft + diffx < 0) diffx = -newleft; 527 | else if (newwidth - diffx < parseFloat(currdialogstyle.minWidth)) diffx = newwidth - parseFloat(currdialogstyle.minWidth); 528 | 529 | newleft += diffx; 530 | newwidth -= diffx; 531 | } 532 | 533 | if (resizelocation === 3 || resizelocation === 5 || resizelocation === 8) 534 | { 535 | if (resizeanchorpos.width + diffx >= parseFloat(currdialogstyle.maxWidth)) diffx = parseFloat(currdialogstyle.maxWidth) - resizeanchorpos.width; 536 | else if (newleft + resizeanchorpos.width + parseFloat(currdialogstyle.marginLeft) + parseFloat(currdialogstyle.marginRight) + diffx >= screenwidth) diffx = screenwidth - newleft - resizeanchorpos.width - parseFloat(currdialogstyle.marginLeft) - parseFloat(currdialogstyle.marginRight); 537 | else if (resizeanchorpos.width + diffx < parseFloat(currdialogstyle.minWidth)) diffx = parseFloat(currdialogstyle.minWidth) - resizeanchorpos.width; 538 | 539 | newwidth = resizeanchorpos.width + diffx; 540 | } 541 | 542 | if (resizelocation === 1 || resizelocation === 2 || resizelocation === 3) 543 | { 544 | if (newheight - diffy >= parseFloat(currdialogstyle.maxHeight)) diffy = newheight - parseFloat(currdialogstyle.maxHeight); 545 | else if (newtop + diffy < 0) diffy = -newtop; 546 | else if (newheight - diffy < parseFloat(currdialogstyle.minHeight)) diffy = newheight - parseFloat(currdialogstyle.minHeight); 547 | 548 | newtop += diffy; 549 | newheight -= diffy; 550 | } 551 | 552 | if (resizelocation === 6 || resizelocation === 7 || resizelocation === 8) 553 | { 554 | if (resizeanchorpos.height + diffy >= parseFloat(currdialogstyle.maxHeight)) diffx = parseFloat(currdialogstyle.maxHeight) - resizeanchorpos.height; 555 | else if (newtop + resizeanchorpos.height + parseFloat(currdialogstyle.marginTop) + parseFloat(currdialogstyle.marginBottom) + diffy >= screenheight) diffy = screenheight - newtop - resizeanchorpos.height - parseFloat(currdialogstyle.marginTop) - parseFloat(currdialogstyle.marginBottom); 556 | else if (resizeanchorpos.height + diffy < parseFloat(currdialogstyle.minHeight)) diffy = parseFloat(currdialogstyle.minHeight) - resizeanchorpos.height; 557 | 558 | newheight = resizeanchorpos.height + diffy; 559 | } 560 | 561 | elems.mainwrap.style.left = newleft + 'px'; 562 | elems.mainwrap.style.top = newtop + 'px'; 563 | elems.mainwrap.style.width = newwidth + 'px'; 564 | elems.mainwrap.style.height = newheight + 'px'; 565 | 566 | manualmove = true; 567 | manualsize = true; 568 | 569 | DispatchEvent('position', elems.mainwrap); 570 | }; 571 | 572 | var ResizeDialogEndHandler = function(e) { 573 | if (e && !e.isTrusted) return; 574 | 575 | resizeanchorpos = null; 576 | 577 | document.body.classList.remove(resizeclass); 578 | 579 | window.removeEventListener('touchmove', ResizeDialogDragHandler, true); 580 | window.removeEventListener('touchend', ResizeDialogEndHandler, true); 581 | window.removeEventListener('mousemove', ResizeDialogDragHandler, true); 582 | window.removeEventListener('mouseup', ResizeDialogEndHandler, true); 583 | window.removeEventListener('blur', ResizeDialogEndHandler, true); 584 | 585 | $this.SnapToScreen(); 586 | }; 587 | 588 | var StartResizeDialogHandler = function(e) { 589 | if (!e.isTrusted) return; 590 | 591 | // Disallow scrolling on touch and block drag-and-drop. 592 | e.preventDefault(); 593 | 594 | $this.CenterDialog(); 595 | 596 | UpdateResizeHoverClass(e); 597 | 598 | document.body.classList.add(resizeclass); 599 | 600 | var rect = elems.resizer.getBoundingClientRect(); 601 | 602 | if (e.type === 'touchstart') 603 | { 604 | resizeanchorpos = { 605 | x: e.touches[0].clientX - rect.left, 606 | y: e.touches[0].clientY - rect.top 607 | }; 608 | 609 | window.addEventListener('touchmove', ResizeDialogDragHandler, true); 610 | window.addEventListener('touchend', ResizeDialogEndHandler, true); 611 | } 612 | else 613 | { 614 | resizeanchorpos = { 615 | x: e.clientX - rect.left, 616 | y: e.clientY - rect.top 617 | }; 618 | 619 | window.addEventListener('mousemove', ResizeDialogDragHandler, true); 620 | window.addEventListener('mouseup', ResizeDialogEndHandler, true); 621 | } 622 | 623 | resizeanchorpos.width = elems.mainwrap.offsetWidth; 624 | resizeanchorpos.height = elems.mainwrap.offsetHeight; 625 | 626 | window.addEventListener('blur', ResizeDialogEndHandler, true); 627 | }; 628 | 629 | elems.resizer.addEventListener('touchstart', StartResizeDialogHandler); 630 | elems.resizer.addEventListener('mousedown', StartResizeDialogHandler); 631 | 632 | // Call close callbacks for the dialog. 633 | $this.Close = function(e) { 634 | if (e && !e.isTrusted) return; 635 | 636 | DispatchEvent('close', lastactiveelem); 637 | }; 638 | 639 | elems.closebutton.addEventListener('click', $this.Close); 640 | 641 | var MainKeyHandler = function(e) { 642 | if (e.keyCode == 27) $this.Close(e); 643 | 644 | if (e.keyCode == 13 && e.target === elems.mainwrap) 645 | { 646 | // Locate the first button. 647 | var buttonnode = elems.formnode.querySelector('input[type=submit]'); 648 | 649 | if (buttonnode) 650 | { 651 | var tempevent = { 652 | isTrusted: true, 653 | target: elems.formnode, 654 | submitter: buttonnode 655 | }; 656 | 657 | $this.settings.request = FlexForms.GetFormVars(elems.formnode, tempevent); 658 | 659 | DispatchEvent('submit', [$this.settings.request, elems.formnode, tempevent, lastactiveelem]); 660 | } 661 | } 662 | }; 663 | 664 | elems.mainwrap.addEventListener('keydown', MainKeyHandler); 665 | 666 | // Destroy this instance. 667 | $this.Destroy = function() { 668 | DispatchEvent('destroy'); 669 | 670 | triggers = {}; 671 | 672 | window.removeEventListener('mousedown', MainWrapMouseBlurHandler, true); 673 | window.removeEventListener('blur', MainWrapWindowBlurHandler, true); 674 | window.removeEventListener('focus', MainWrapFocusHandler, true); 675 | 676 | window.removeEventListener('resize', dialogresizewatch.Start, true); 677 | 678 | dialogresizewatch.Destroy(); 679 | 680 | window.FlexForms.removeEventListener('done', LoadedHandler); 681 | 682 | MoveDialogEndHandler(); 683 | 684 | elems.title.removeEventListener('mousedown', StartMoveDialogHandler); 685 | elems.title.removeEventListener('touchstart', StartMoveDialogHandler); 686 | 687 | ResizeDialogEndHandler(); 688 | 689 | elems.resizer.removeEventListener('touchstart', StartResizeDialogHandler); 690 | elems.resizer.removeEventListener('mousedown', StartResizeDialogHandler); 691 | 692 | elems.formnode.removeEventListener('submit', SubmitHandler); 693 | 694 | elems.closebutton.removeEventListener('click', $this.Close); 695 | 696 | elems.mainwrap.removeEventListener('keydown', MainKeyHandler); 697 | 698 | for (var node in elems) 699 | { 700 | if (elems[node].parentNode) elems[node].parentNode.removeChild(elems[node]); 701 | } 702 | 703 | currdialogstyle = null; 704 | 705 | // Remaining cleanup. 706 | elems = null; 707 | lastactiveelem = null; 708 | 709 | $this.settings = Object.assign({}, defaults); 710 | 711 | $this = null; 712 | parentelem = null; 713 | options = null; 714 | }; 715 | }; 716 | 717 | var AlertDialogInternal = function(title, content, closecallback, timeout) { 718 | var timer; 719 | 720 | var dlgoptions = { 721 | title: title, 722 | 723 | content: (typeof(content) !== 'string' ? content : { 724 | fields: [ 725 | { 726 | type: 'custom', 727 | value: '
' + EscapeHTML(Translate(content)).replaceAll('\n', '
\n') + '
' 728 | } 729 | ], 730 | submit: ['OK'], 731 | submitname: 'submit' 732 | }), 733 | 734 | onsubmit: function(dlgformvars, dlgformnode, e, lastactivelem) { 735 | if (timer) clearTimeout(timer); 736 | 737 | this.Destroy(); 738 | 739 | lastactivelem.focus(); 740 | 741 | if (typeof(closecallback) === 'function') closecallback(); 742 | }, 743 | 744 | onclose: function(lastactivelem) { 745 | if (timer) clearTimeout(timer); 746 | 747 | this.Destroy(); 748 | 749 | lastactivelem.focus(); 750 | 751 | if (typeof(closecallback) === 'function') closecallback(); 752 | } 753 | }; 754 | 755 | var dlg = FlexForms.Dialog(document.body, dlgoptions); 756 | 757 | if (timeout > 0) timer = setTimeout(function() { dlg.Close(); }, timeout); 758 | 759 | return dlg; 760 | }; 761 | 762 | var ConfirmDialogInternal = function(title, content, yesbutton, nobutton, yescallback, nocallback, closecallback) { 763 | var dlgoptions = { 764 | title: title, 765 | 766 | content: (typeof(content) !== 'string' ? content : { 767 | fields: [ 768 | { 769 | type: 'custom', 770 | value: '
' + EscapeHTML(Translate(content)).replaceAll('\n', '
\n') + '
' 771 | } 772 | ], 773 | submit: [yesbutton, nobutton], 774 | submitname: 'submit' 775 | }), 776 | 777 | onsubmit: function(dlgformvars, dlgformnode, e, lastactivelem) { 778 | this.Destroy(); 779 | 780 | lastactivelem.focus(); 781 | 782 | if (dlgformvars.submit === Translate(yesbutton)) 783 | { 784 | if (typeof(yescallback) === 'function') yescallback(1); 785 | } 786 | else 787 | { 788 | if (typeof(nocallback) === 'function') nocallback(0); 789 | } 790 | }, 791 | 792 | onclose: function(lastactivelem) { 793 | this.Destroy(); 794 | 795 | lastactivelem.focus(); 796 | 797 | if (typeof(closecallback) === 'function') closecallback(-1); 798 | else if (typeof(nocallback) === 'function') nocallback(-1); 799 | } 800 | }; 801 | 802 | var dlg = FlexForms.Dialog(document.body, dlgoptions); 803 | 804 | return dlg; 805 | }; 806 | 807 | window.FlexForms.Dialog = DialogInternal; 808 | window.FlexForms.Dialog.Alert = AlertDialogInternal; 809 | window.FlexForms.Dialog.Confirm = ConfirmDialogInternal; 810 | })(); 811 | -------------------------------------------------------------------------------- /support/flex_forms.js: -------------------------------------------------------------------------------- 1 | // FlexForms Javascript base and Designer classes. 2 | // (C) 2022 CubicleSoft. All Rights Reserved. 3 | 4 | (function() { 5 | if (window.hasOwnProperty('FlexForms')) return; 6 | 7 | // FlexForms base class. 8 | var FlexFormsInternal = function() { 9 | if (!(this instanceof FlexFormsInternal)) return new FlexFormsInternal(); 10 | 11 | var triggers = {}, version = '', cssoutput = {}, cssleft = 0, jsqueue = {}, ready = false, initialized = false; 12 | var $this = this; 13 | 14 | // Internal functions. 15 | var DispatchEvent = function(eventname, params) { 16 | if (!triggers[eventname]) return; 17 | 18 | triggers[eventname].forEach(function(callback) { 19 | if (Array.isArray(params)) callback.apply($this, params); 20 | else callback.call($this, params); 21 | }); 22 | }; 23 | 24 | // Public DOM-style functions. 25 | $this.addEventListener = function(eventname, callback) { 26 | if (!triggers[eventname]) triggers[eventname] = []; 27 | 28 | for (var x in triggers[eventname]) 29 | { 30 | if (triggers[eventname][x] === callback) return; 31 | } 32 | 33 | triggers[eventname].push(callback); 34 | }; 35 | 36 | $this.removeEventListener = function(eventname, callback) { 37 | if (!triggers[eventname]) return; 38 | 39 | for (var x in triggers[eventname]) 40 | { 41 | if (triggers[eventname][x] === callback) 42 | { 43 | triggers[eventname].splice(x, 1); 44 | 45 | return; 46 | } 47 | } 48 | }; 49 | 50 | $this.hasEventListener = function(eventname) { 51 | return (triggers[eventname] && triggers[eventname].length); 52 | }; 53 | 54 | $this.modules = {}; 55 | 56 | $this.SetVersion = function(newver) { 57 | version = newver; 58 | }; 59 | 60 | $this.GetVersion = function() { 61 | return version; 62 | }; 63 | 64 | $this.RegisterCSSOutput = function(info) { 65 | Object.assign(cssoutput, info); 66 | }; 67 | 68 | var CheckEmptyAndNotify = function() { 69 | if (cssleft) return; 70 | 71 | for (var x in jsqueue) 72 | { 73 | if (jsqueue.hasOwnProperty(x)) return; 74 | } 75 | 76 | DispatchEvent('done'); 77 | }; 78 | 79 | $this.LoadCSS = function(name, url, cssmedia) { 80 | if (cssoutput[name] !== undefined) 81 | { 82 | CheckEmptyAndNotify(); 83 | 84 | return cssoutput[name]; 85 | } 86 | 87 | if (version !== '') url += (url.indexOf('?') > -1 ? '&' : '?') + version; 88 | 89 | var tag = document.createElement('link'); 90 | 91 | tag._loaded = false; 92 | tag.onload = function(e) { 93 | tag._loaded = true; 94 | 95 | cssleft--; 96 | CheckEmptyAndNotify(); 97 | }; 98 | 99 | cssleft++; 100 | 101 | tag.rel = 'stylesheet'; 102 | tag.type = 'text/css'; 103 | tag.href = url; 104 | tag.media = (cssmedia != undefined ? cssmedia : 'all'); 105 | 106 | document.getElementsByTagName('head')[0].appendChild(tag); 107 | 108 | cssoutput[name] = tag; 109 | 110 | return tag; 111 | }; 112 | 113 | $this.AddCSS = function(name, css, cssmedia) { 114 | if (cssoutput[name] !== undefined) 115 | { 116 | CheckEmptyAndNotify(); 117 | 118 | return cssoutput[name]; 119 | } 120 | 121 | var tag = document.createElement('style'); 122 | tag.type = 'text/css'; 123 | tag.media = (cssmedia != undefined ? cssmedia : 'all'); 124 | 125 | document.getElementsByTagName('head')[0].appendChild(tag); 126 | 127 | if (tag.styleSheet) tag.styleSheet.cssText = css; 128 | else tag.appendChild(document.createTextNode(css)); 129 | 130 | tag._loaded = true; 131 | 132 | cssoutput[name] = tag; 133 | 134 | CheckEmptyAndNotify(); 135 | 136 | return tag; 137 | }; 138 | 139 | $this.AddJSQueueItem = function(name, info) { 140 | jsqueue[name] = info; 141 | }; 142 | 143 | var LoadJSQueueItem = function(name) { 144 | var done = false; 145 | var s = document.createElement('script'); 146 | 147 | jsqueue[name].loading = true; 148 | jsqueue[name].retriesleft = jsqueue[name].retriesleft || 3; 149 | 150 | s.onload = function() { 151 | if (!done) { done = true; delete jsqueue[name]; $this.ProcessJSQueue(); } 152 | }; 153 | 154 | s.onreadystatechange = function() { 155 | if (!done && s.readyState === 'complete') { done = true; delete jsqueue[name]; $this.ProcessJSQueue(); } 156 | }; 157 | 158 | s.onerror = function() { 159 | if (!done) 160 | { 161 | done = true; 162 | 163 | jsqueue[name].retriesleft--; 164 | if (jsqueue[name].retriesleft > 0) 165 | { 166 | jsqueue[name].loading = false; 167 | 168 | setTimeout($this.ProcessJSQueue, 250); 169 | } 170 | } 171 | }; 172 | 173 | s.src = jsqueue[name].src + (version === '' ? '' : (jsqueue[name].src.indexOf('?') > -1 ? '&' : '?') + version); 174 | 175 | document.body.appendChild(s); 176 | }; 177 | 178 | $this.GetObjectFromPath = function(path) { 179 | var obj = window; 180 | path = path.split('.'); 181 | for (var x = 0; x < path.length; x++) 182 | { 183 | if (obj[path[x]] === undefined) return; 184 | 185 | obj = obj[path[x]]; 186 | } 187 | 188 | return obj; 189 | }; 190 | 191 | $this.ProcessJSQueue = function() { 192 | ready = true; 193 | 194 | for (var name in jsqueue) { 195 | if (jsqueue.hasOwnProperty(name)) 196 | { 197 | if ((jsqueue[name].loading === undefined || jsqueue[name].loading === false) && (jsqueue[name].dependency === false || jsqueue[jsqueue[name].dependency] === undefined)) 198 | { 199 | if (jsqueue[name].detect !== undefined && $this.GetObjectFromPath(jsqueue[name].detect) !== undefined) delete jsqueue[name]; 200 | else if (jsqueue[name].mode === "src") LoadJSQueueItem(name); 201 | else if (jsqueue[name].mode === "inline") 202 | { 203 | jsqueue[name].src(); 204 | 205 | delete jsqueue[name]; 206 | } 207 | } 208 | } 209 | } 210 | 211 | CheckEmptyAndNotify(); 212 | }; 213 | 214 | $this.Init = function() { 215 | if (ready) $this.ProcessJSQueue(); 216 | else if (!initialized) 217 | { 218 | if (document.addEventListener) 219 | { 220 | function regevent(event) { 221 | document.removeEventListener("DOMContentLoaded", regevent); 222 | 223 | $this.ProcessJSQueue(); 224 | } 225 | 226 | document.addEventListener("DOMContentLoaded", regevent); 227 | } 228 | else 229 | { 230 | setTimeout($this.ProcessJSQueue(), 250); 231 | } 232 | 233 | initialized = true; 234 | } 235 | }; 236 | }; 237 | 238 | window.FlexForms = new FlexFormsInternal(); 239 | 240 | window.FlexForms.Init(); 241 | })(); 242 | 243 | (function() { 244 | if (window.FlexForms.hasOwnProperty('Designer')) return; 245 | 246 | var EscapeHTML = function(text) { 247 | var map = { 248 | '&': '&', 249 | '<': '<', 250 | '>': '>', 251 | '"': '"', 252 | "'": ''' 253 | }; 254 | 255 | return String(text).replace(/[&<>"']/g, function(m) { return map[m]; }); 256 | } 257 | 258 | var FormatStr = function(format) { 259 | var args = Array.prototype.slice.call(arguments, 1); 260 | 261 | return format.replace(/{(\d+)}/g, function(match, number) { 262 | return (typeof args[number] != 'undefined' ? args[number] : match); 263 | }); 264 | }; 265 | 266 | var CreateNode = function(tag, classes, attrs, styles) { 267 | var elem = document.createElement(tag); 268 | 269 | if (classes) 270 | { 271 | if (typeof classes === 'string') elem.className = classes; 272 | else elem.className = classes.join(' '); 273 | } 274 | 275 | if (attrs) Object.assign(elem, attrs); 276 | 277 | if (styles) Object.assign(elem.style, styles); 278 | 279 | return elem; 280 | }; 281 | 282 | var DebounceAttributes = function(options) { 283 | if (!(this instanceof DebounceAttributes)) return new DebounceAttributes(options); 284 | 285 | var intervaltimer = null, numsame; 286 | var $this = this; 287 | 288 | var defaults = { 289 | watchers: [], 290 | interval: 50, 291 | stopsame: 1, 292 | 293 | callback: null, 294 | intervalcallback: null 295 | }; 296 | 297 | $this.settings = Object.assign({}, defaults, options); 298 | 299 | var MainIntervalHandler = function() { 300 | var nummatches = 0; 301 | 302 | for (var x = 0; x < $this.settings.watchers.length; x++) 303 | { 304 | var watcher = $this.settings.watchers[x]; 305 | 306 | if (watcher.val === watcher.elem[watcher.attr]) nummatches++; 307 | else watcher.val = watcher.elem[watcher.attr]; 308 | } 309 | 310 | if (nummatches < $this.settings.watchers.length) 311 | { 312 | numsame = 0; 313 | 314 | if ($this.settings.intervalcallback) $this.settings.intervalcallback.call($this); 315 | } 316 | else 317 | { 318 | numsame++; 319 | 320 | if (numsame >= $this.settings.stopsame) 321 | { 322 | $this.Stop(); 323 | 324 | if ($this.settings.intervalcallback) $this.settings.intervalcallback.call($this); 325 | 326 | if ($this.settings.callback) $this.settings.callback.call($this); 327 | } 328 | } 329 | }; 330 | 331 | 332 | // Public functions. 333 | 334 | $this.Start = function() { 335 | if (!intervaltimer) 336 | { 337 | numsame = 0; 338 | 339 | intervaltimer = setInterval(MainIntervalHandler, $this.settings.interval); 340 | } 341 | }; 342 | 343 | $this.Stop = function() { 344 | if (intervaltimer) 345 | { 346 | clearInterval(intervaltimer); 347 | 348 | intervaltimer = null; 349 | } 350 | }; 351 | 352 | $this.Destroy = function() { 353 | $this.Stop(); 354 | 355 | $this = null; 356 | } 357 | }; 358 | 359 | 360 | // FlexForms Designer class. Tranforms objects into DOM nodes similar to what the PHP version generates. 361 | var nextform = 1; 362 | var DesignerInternal = function() { 363 | if (!(this instanceof DesignerInternal)) return new DesignerInternal(); 364 | 365 | var triggers = {}; 366 | var $this = this; 367 | 368 | $this.settings = { 369 | formidbase: 'ff_js_form_', 370 | responsive: true, 371 | formtables: true, 372 | formwidths: true, 373 | supporturl: (document.currentScript.src.lastIndexOf('/') > -1 ? document.currentScript.src.substring(0, document.currentScript.src.lastIndexOf('/')) : document.currentScript.src), 374 | 375 | langmap: {} 376 | }; 377 | 378 | // Multilingual translation. 379 | $this.Translate = function(str) { 380 | return ($this.settings.langmap[str] ? $this.settings.langmap[str] : str); 381 | }; 382 | 383 | // Internal functions. 384 | var DispatchEvent = function(eventname, params) { 385 | if (!triggers[eventname]) return; 386 | 387 | triggers[eventname].forEach(function(callback) { 388 | if (Array.isArray(params)) callback.apply($this, params); 389 | else callback.call($this, params); 390 | }); 391 | }; 392 | 393 | // Public DOM-style functions. 394 | $this.addEventListener = function(eventname, callback) { 395 | if (!triggers[eventname]) triggers[eventname] = []; 396 | 397 | for (var x in triggers[eventname]) 398 | { 399 | if (triggers[eventname][x] === callback) return; 400 | } 401 | 402 | triggers[eventname].push(callback); 403 | }; 404 | 405 | $this.removeEventListener = function(eventname, callback) { 406 | if (!triggers[eventname]) return; 407 | 408 | for (var x in triggers[eventname]) 409 | { 410 | if (triggers[eventname][x] === callback) 411 | { 412 | triggers[eventname].splice(x, 1); 413 | 414 | return; 415 | } 416 | } 417 | }; 418 | 419 | $this.hasEventListener = function(eventname) { 420 | return (triggers[eventname] && triggers[eventname].length); 421 | }; 422 | 423 | $this.OutputFormCSS = function() { 424 | FlexForms.LoadCSS('formcss', $this.settings.supporturl + '/flex_forms.css'); 425 | }; 426 | 427 | // Creates a message for insertion into the DOM. 428 | $this.CreateMessage = function(type, message) { 429 | type = type.toLowerCase(); 430 | if (type === 'warn') type = 'warning'; 431 | 432 | var messagewrap = CreateNode('div', ['ff_formmessagewrap']); 433 | var messagewrapinner = CreateNode('div', ['ff_formmessagewrapinner']); 434 | var messagenode = CreateNode('div', ['message', 'message' + EscapeHTML(type)]); 435 | 436 | messagenode.innerHTML = $this.Translate(message); 437 | 438 | messagewrapinner.appendChild(messagenode); 439 | messagewrap.appendChild(messagewrapinner); 440 | 441 | return messagewrap; 442 | }; 443 | 444 | // Creates a Javascript object from submitted form elements for client-side use (e.g. validation). 445 | $this.GetFormVars = function(formelem, e) { 446 | if (e && !e.isTrusted) return null; 447 | 448 | var result = {}; 449 | 450 | for (var x = 0; x < formelem.elements.length; x++) 451 | { 452 | var elem = formelem.elements[x]; 453 | var type = (elem.tagName === 'INPUT' || elem.tagName === 'BUTTON' ? elem.type.toLowerCase() : ''); 454 | 455 | if (!elem.name || elem.disabled) continue; 456 | if (elem.tagName === 'INPUT' && (type === 'checkbox' || type === 'radio') && !elem.checked) continue; 457 | if ((elem.tagName === 'INPUT' || elem.tagName === 'BUTTON') && (type === 'reset' || type === 'button' || ((type === 'submit' || type === 'image' || elem.tagName === 'BUTTON') && (!e || !e.submitter || e.submitter !== elem)))) continue; 458 | 459 | var multi = elem.name.endsWith('[]'); 460 | var name = (multi ? elem.name.substring(0, elem.name.length - 2) : elem.name); 461 | if (!multi && elem.multiple) multi = true; 462 | 463 | if (multi && (!result.hasOwnProperty(name) || !Array.isArray(result[name]))) result[name] = []; 464 | 465 | if (elem.tagName === 'INPUT' && type === 'file') 466 | { 467 | if (elem.files) result[name] = (multi ? elem.files : (elem.files.length ? elem.files[0] : null)); 468 | } 469 | else if (elem.tagName === 'SELECT') 470 | { 471 | for (var x2 = 0; x2 < elem.options.length; x2++) 472 | { 473 | if (elem.options[x2].selected) 474 | { 475 | if (multi) result[name].push(elem.options[x2].value); 476 | else result[name] = elem.options[x2].value; 477 | } 478 | } 479 | } 480 | else if (multi) result[name].push(elem.value); 481 | else result[name] = elem.value; 482 | } 483 | 484 | return result; 485 | }; 486 | 487 | // Generates and injects HTML, CSS, and Javascript into the DOM. Has feature parity with PHP FlexForms. 488 | $this.Generate = function(parentelem, options, errors, request) { 489 | var state = { 490 | formnum: nextform, 491 | formid: $this.settings.formidbase + nextform, 492 | customfieldtypes: {}, 493 | hidden: {}, 494 | insiderow: false, 495 | firstitem: false, 496 | html: '', 497 | css: {}, 498 | js: {}, 499 | request: {} 500 | }; 501 | 502 | if (request) state.request = request; 503 | 504 | nextform++; 505 | 506 | // Deep clone the options. 507 | options = JSON.parse(JSON.stringify(options)); 508 | 509 | // Let form handlers modify the options and state. 510 | DispatchEvent('init', [state, options]); 511 | 512 | $this.OutputFormCSS(); 513 | 514 | if (options.submit || options.useform) 515 | { 516 | state.html += '
'; 517 | 518 | if (options.hidden) 519 | { 520 | for (var x in options.hidden) 521 | { 522 | if (options.hidden.hasOwnProperty(x)) state.html += ''; 523 | } 524 | } 525 | } 526 | 527 | if (Array.isArray(options.fields)) 528 | { 529 | state.html += '
'; 530 | state.html += '
'; 531 | 532 | for (var x = 0; x < options.fields.length; x++) 533 | { 534 | var id = 'f_js' + state.formnum + '_' + x; 535 | 536 | if (typeof options.fields[x] !== 'string' && options.fields[x].name) 537 | { 538 | id += '_' + options.fields[x].name; 539 | 540 | if (errors && errors[options.fields[x].name]) options.fields[x].error = errors[options.fields[x].name]; 541 | } 542 | 543 | ProcessField(state, x, options.fields[x], id); 544 | } 545 | 546 | // Cleanup fields. 547 | if (state.insiderow) 548 | { 549 | if ($this.settings.responsive && state.insiderowwidth) state.html += ''; 550 | 551 | state.html += '
'; 552 | } 553 | 554 | // Let form handlers process other field types. 555 | DispatchEvent('cleanup', state); 556 | 557 | state.html += '
'; 558 | state.html += ''; 559 | } 560 | 561 | // Process submit button(s). 562 | if (options.submit) 563 | { 564 | if (typeof options.submit === 'string') options.submit = [{ name: (options.submitname ? options.submitname : ''), value: options.submit }]; 565 | 566 | state.html += '
'; 567 | state.html += '
'; 568 | 569 | if (Array.isArray(options.submit)) 570 | { 571 | for (var x = 0; x < options.submit.length; x++) 572 | { 573 | if (typeof options.submit[x] === 'string') options.submit[x] = { value: options.submit[x] }; 574 | if (!options.submit[x].name) options.submit[x].name = (options.submitname ? options.submitname : ''); 575 | 576 | state.html += ''; 577 | } 578 | } 579 | 580 | state.html += '
'; 581 | state.html += '
'; 582 | } 583 | 584 | if (options.submit || options.useform) 585 | { 586 | state.html += '
'; 587 | } 588 | 589 | var formwrap = CreateNode('div', ['ff_formwrap']); 590 | var formwrapinner = CreateNode('div', ['ff_formwrapinner']); 591 | 592 | formwrapinner.innerHTML = state.html; 593 | formwrap.appendChild(formwrapinner); 594 | 595 | parentelem.appendChild(formwrap); 596 | 597 | // Load CSS in dependency order. 598 | do 599 | { 600 | var found = false; 601 | 602 | for (var x in state.css) 603 | { 604 | if (state.css.hasOwnProperty(x) && state.css[x].mode === 'link' && (state.css[x].dependency === false || !state.css.hasOwnProperty(state.css[x].dependency))) 605 | { 606 | FlexForms.LoadCSS(x, state.css[x].src, (state.css[x].media ? state.css[x].media : null)); 607 | 608 | delete state.css[x]; 609 | 610 | found = true; 611 | } 612 | } 613 | } while (found); 614 | 615 | // Add inline CSS. 616 | for (var x in state.css) 617 | { 618 | if (state.css.hasOwnProperty(x) && state.css[x].mode === 'inline') FlexForms.AddCSS(x, state.css[x].src, (state.css[x].media ? state.css[x].media : null)); 619 | } 620 | 621 | // Queue Javascript dependencies. 622 | for (var x in state.js) 623 | { 624 | if (state.js.hasOwnProperty(x)) FlexForms.AddJSQueueItem(x, state.js[x]); 625 | } 626 | 627 | // Let form handlers finalize other field types. 628 | DispatchEvent('finalize', state); 629 | 630 | // Focus on an autofocus element. 631 | var focusnode = formwrap.querySelector('[autofocus]'); 632 | if (focusnode) 633 | { 634 | focusnode.focus(); 635 | focusnode.select(); 636 | } 637 | 638 | return formwrap; 639 | }; 640 | 641 | $this.GetSelectValues = function(data) { 642 | var result = {}; 643 | for (var x = 0; x < data.length; x++) result[data[x]] = true; 644 | 645 | return result; 646 | }; 647 | 648 | $this.IsEmptyObject = function(obj) { 649 | for (var x in obj) 650 | { 651 | if (obj.hasOwnProperty(x)) return false; 652 | } 653 | 654 | return true; 655 | }; 656 | 657 | var ProcessField = function(state, num, field, id) { 658 | if (typeof field === 'string') 659 | { 660 | if (field === 'split' && !state.insiderow) state.html += '
'; 661 | else if (field === 'startrow') 662 | { 663 | if (state.insiderow) 664 | { 665 | if ($this.settings.responsive && state.insiderowwidth) state.html += ''; 666 | 667 | state.html += ''; 668 | } 669 | else if ($this.settings.formtables) 670 | { 671 | state.insiderow = true; 672 | state.insiderowwidth = false; 673 | state.html += '
'; 674 | state.firstitem = false; 675 | } 676 | } 677 | else if (field === 'endrow' && $this.settings.formtables && state.insiderow) 678 | { 679 | if ($this.settings.responsive && state.insiderowwidth) state.html += ''; 680 | 681 | state.html += '
'; 682 | 683 | state.insiderow = false; 684 | } 685 | else if (field.startsWith('html:')) 686 | { 687 | state.html += field.substring(5); 688 | } 689 | 690 | // Let form handlers process strings. 691 | DispatchEvent('field_string', [state, num, field, id]); 692 | } 693 | else if (!field.type || (field.hasOwnProperty('use') && !field.use)) 694 | { 695 | // Do nothing if type is not specified. 696 | } 697 | else if (field.type === 'hidden') 698 | { 699 | state.html += ''; 700 | } 701 | else if (state.customfieldtypes.hasOwnProperty(field.type)) 702 | { 703 | // Output custom fields. 704 | DispatchEvent('field_type', [state, num, field, id]); 705 | } 706 | else 707 | { 708 | if (state.insiderow) 709 | { 710 | if (!$this.settings.responsive || !field.hasOwnProperty('width')) state.html += ''; 711 | else 712 | { 713 | state.html += ''; 714 | 715 | state.insiderowwidth = true; 716 | } 717 | } 718 | 719 | state.html += '
'; 720 | 721 | state.firstitem = false; 722 | 723 | if (field.title) 724 | { 725 | if (typeof field.title === 'string') state.html += '
' + EscapeHTML($this.Translate(field.title)) + '
'; 726 | } 727 | else if (field.htmltitle) 728 | { 729 | state.html += '
' + $this.Translate(field.htmltitle) + '
'; 730 | } 731 | else if (field.type === 'checkbox' && state.insiderow) 732 | { 733 | state.html += '
 
'; 734 | } 735 | 736 | if (field.hasOwnProperty('width') && !$this.settings.formwidths) delete field.width; 737 | 738 | if (field.hasOwnProperty('name') && field.hasOwnProperty('default')) 739 | { 740 | if (field.type === 'select') 741 | { 742 | if (!field.select) 743 | { 744 | field.select = (state.request[field.name] ? state.request[field.name] : field.default); 745 | if (Array.isArray(field.select)) field.select = $this.GetSelectValues(field.select); 746 | } 747 | } 748 | else if (field.type === 'checkbox') 749 | { 750 | if (!field.hasOwnProperty('check') && field.hasOwnProperty('value')) field.check = (state.request[field.name] === field.value ? true : ($this.IsEmptyObject(state.request) ? field.default : false)); 751 | } 752 | else if (!field.hasOwnProperty('value')) 753 | { 754 | field.value = (state.request[field.name] ? state.request[field.name] : field.default); 755 | } 756 | } 757 | 758 | if (field.type === 'select' && field.hasOwnProperty('mode') && field.mode === 'formhandler') delete field.mode; 759 | 760 | // Let form handlers process fields. 761 | DispatchEvent('field_type', [state, num, field, id]); 762 | 763 | switch (field.type) 764 | { 765 | case 'static': 766 | { 767 | state.html += '
'; 768 | state.html += '
' + EscapeHTML(field.value) + '
'; 769 | state.html += '
'; 770 | 771 | break; 772 | } 773 | case 'text': 774 | case 'password': 775 | { 776 | state.html += '
'; 777 | state.html += '
'; 778 | state.html += '
'; 779 | 780 | break; 781 | } 782 | case 'checkbox': 783 | { 784 | state.html += '
'; 785 | state.html += '
'; 786 | state.html += ''; 787 | state.html += ' '; 788 | state.html += '
'; 789 | state.html += '
'; 790 | 791 | break; 792 | } 793 | case 'select': 794 | { 795 | var mode, stylewidth, styleheight, idbase; 796 | 797 | if (!field.multiple) mode = (field.mode === 'radio' ? 'radio' : 'select'); 798 | else if (!field.hasOwnProperty('mode') || (field.mode != 'formhandler' && field.mode != 'select')) mode = 'checkbox'; 799 | else mode = field.mode; 800 | 801 | mode = EscapeHTML(mode); 802 | 803 | stylewidth = (field.hasOwnProperty('width') ? ' style="' + ($this.settings.responsive ? 'max-' : '') + 'width: ' + EscapeHTML(field.width) + ';"' : ''); 804 | styleheight = (field.hasOwnProperty('height') ? ' style="height: ' + EscapeHTML(field.height) + ';"' : ''); 805 | 806 | if (!field.hasOwnProperty('select')) field.select = {}; 807 | else if (typeof field.select === 'string') 808 | { 809 | var tempselect = {}; 810 | 811 | tempselect[field.select] = true; 812 | 813 | field.select = tempselect; 814 | } 815 | 816 | state.html += '
'; 817 | 818 | idbase = EscapeHTML(id); 819 | if (mode === 'checkbox' || mode === 'radio') 820 | { 821 | var idnum = 0; 822 | 823 | for (var x = 0; x < field.options.length; x ++) 824 | { 825 | // Fix confusing legacy name/value keys. 826 | if (field.options[x].name && field.options[x].value) 827 | { 828 | if (Array.isArray(field.options[x].value)) 829 | { 830 | field.options[x].display = field.options[x].key; 831 | field.options[x].values = field.options[x].value; 832 | } 833 | else 834 | { 835 | field.options[x].key = field.options[x].name; 836 | field.options[x].display = field.options[x].value; 837 | } 838 | } 839 | 840 | var display = field.options[x].display; 841 | 842 | if (field.options[x].values && Array.isArray(field.options[x].values)) 843 | { 844 | for (var x2 = 0; x2 < field.options[x].values.length; x2++) 845 | { 846 | // Fix confusing legacy name/value keys. 847 | if (field.options[x].values[x2].name && field.options[x].values[x2].value) 848 | { 849 | field.options[x].values[x2].key = field.options[x].values[x2].name; 850 | field.options[x].values[x2].display = field.options[x].values[x2].value; 851 | } 852 | 853 | var key2 = field.options[x].values[x2].key; 854 | var display2 = field.options[x].values[x2].display; 855 | var id2 = EscapeHTML(idbase + (idnum ? '_' + idnum : '')); 856 | 857 | state.html += '
'; 858 | state.html += ''; 859 | state.html += ' '; 860 | state.html += '
'; 861 | 862 | idnum++; 863 | } 864 | } 865 | else 866 | { 867 | var key = field.options[x].key; 868 | var id2 = EscapeHTML(idbase + (idnum ? '_' + idnum : '')); 869 | 870 | state.html += '
'; 871 | state.html += ''; 872 | state.html += ' '; 873 | state.html += '
'; 874 | 875 | idnum++; 876 | } 877 | } 878 | } 879 | else 880 | { 881 | state.html += '
'; 882 | state.html += ''; 933 | state.html += '
'; 934 | } 935 | 936 | state.html += '
'; 937 | 938 | break; 939 | } 940 | case 'textarea': 941 | { 942 | var stylewidth, styleheight; 943 | 944 | stylewidth = (field.hasOwnProperty('width') ? ' style="' + ($this.settings.responsive ? 'max-' : '') + 'width: ' + EscapeHTML(field.width) + ';"' : ''); 945 | styleheight = (field.hasOwnProperty('height') ? ' style="height: ' + EscapeHTML(field.height) + ';"' : ''); 946 | 947 | state.html += '
'; 948 | state.html += '
'; 949 | state.html += '
'; 950 | 951 | break; 952 | } 953 | case 'table': 954 | { 955 | idbase = id + '_table'; 956 | 957 | state.html += '
'; 958 | 959 | if ($this.settings.formtables) 960 | { 961 | state.html += ''; 962 | state.html += ''; 963 | 964 | var trattrs = { class: 'head' }, colattrs = []; 965 | if (!field.hasOwnProperty('cols')) field.cols = []; 966 | for (var x = 0; x < field.cols.length; x++) colattrs.push({}); 967 | 968 | // Let form handlers process the columns. 969 | DispatchEvent('table_row', [state, num, field, idbase, 'head', -1, trattrs, colattrs, field.cols]); 970 | 971 | state.html += ''; 986 | } 987 | 988 | state.html += ''; 989 | state.html += ''; 990 | 991 | colattrs = []; 992 | for (var x = 0; x < field.cols.length; x++) colattrs.push(field.hasOwnProperty('nowrap') && ((typeof field.nowrap === 'string' && field.cols[x] === field.nowrap) || (Array.isArray(field.nowrap) && Array.indexOf(field.cols[x]) > -1)) ? { class: 'nowrap' } : {}); 993 | 994 | var rownum = 0, altrow = false, colattrs2, num2; 995 | if (field.callback) field.rows = field.callback.call($this, field); 996 | while (field.rows.length) 997 | { 998 | for (var y = 0; y < field.rows.length; y++) 999 | { 1000 | trattrs = { class: 'row' + (altrow ? ' altrow' : '') }; 1001 | colattrs2 = colattrs.slice(); 1002 | 1003 | // Let form handlers process the current row. 1004 | DispatchEvent('table_row', [state, num, field, idbase, 'body', rownum, trattrs, colattrs2, field.rows[y]]); 1005 | 1006 | if (field.rows[y].length < colattrs2.length) colattrs2[field.rows[y].length - 1].colspan = colattrs2.length - field.rows[y].length + 1; 1007 | 1008 | state.html += ' num2) 1020 | { 1021 | for (var x2 in colattrs2[num2]) 1022 | { 1023 | if (colattrs2[num2].hasOwnProperty(x2)) state.html += ' ' + EscapeHTML(x2) + '="' + EscapeHTML(colattrs2[num2][x2]) + '"'; 1024 | } 1025 | } 1026 | state.html += '>' + field.rows[y][x] + ''; 1027 | 1028 | num2++; 1029 | } 1030 | 1031 | state.html += ''; 1032 | rownum++; 1033 | altrow = !altrow; 1034 | } 1035 | 1036 | if (field.callback) field.rows = field.callback.call($this, field); 1037 | else field.rows = []; 1038 | } 1039 | 1040 | state.html += ''; 1041 | state.html += '
'; 1042 | } 1043 | else 1044 | { 1045 | state.html += '
'; 1046 | 1047 | var trattrs = { class: 'head' }, headcolattrs = [], colattrs = []; 1048 | if (!field.hasOwnProperty('cols')) field.cols = []; 1049 | for (var x = 0; x < field.cols.length; x++) headcolattrs.push({ class: 'nontable_th' + (x ? '' : ' firstcol') }); 1050 | 1051 | // Let form handlers process the columns. 1052 | DispatchEvent('table_row', [state, num, field, idbase, 'head', -1, trattrs, headcolattrs, field.cols]); 1053 | 1054 | for (var x = 0; x < field.cols.length; x++) headcolattrs.push({ class: 'nontable_td' }); 1055 | 1056 | var rownum = 0, altrow = false, colattrs2, num2; 1057 | if (field.callback) field.rows = field.callback.call($this, field); 1058 | while (field.rows.length) 1059 | { 1060 | for (var y = 0; y < field.rows.length; y++) 1061 | { 1062 | trattrs = { class: 'nontable_row' + (altrow ? ' altrow' : '') + (rownum ? '' : ' firstrow') }; 1063 | colattrs2 = colattrs.slice(); 1064 | 1065 | // Let form handlers process the current row. 1066 | DispatchEvent('table_row', [state, num, field, idbase, 'body', rownum, trattrs, colattrs2, field.rows[y]]); 1067 | 1068 | state.html += ' num2) 1080 | { 1081 | for (var x2 in headcolattrs[num2]) 1082 | { 1083 | if (headcolattrs[num2].hasOwnProperty(x2)) state.html += ' ' + EscapeHTML(x2) + '="' + EscapeHTML(headcolattrs[num2][x2]) + '"'; 1084 | } 1085 | } 1086 | state.html += '>' + (field.cols.length > x ? (field.htmlcols ? field.cols[x] : EscapeHTML(field.cols[x])) : '') + '
'; 1087 | 1088 | state.html += ' num2) 1090 | { 1091 | for (var x2 in colattrs2[num2]) 1092 | { 1093 | if (colattrs2[num2].hasOwnProperty(x2)) state.html += ' ' + EscapeHTML(x2) + '="' + EscapeHTML(colattrs2[num2][x2]) + '"'; 1094 | } 1095 | } 1096 | state.html += '>' + field.rows[y][x] + '
'; 1097 | 1098 | num2++; 1099 | } 1100 | 1101 | state.html += '
'; 1102 | rownum++; 1103 | altrow = !altrow; 1104 | } 1105 | } 1106 | 1107 | state.html += ''; 1108 | } 1109 | 1110 | state.html += ''; 1111 | 1112 | break; 1113 | } 1114 | case 'file': 1115 | { 1116 | state.html += '
'; 1117 | state.html += '
'; 1118 | state.html += '
'; 1119 | 1120 | break; 1121 | } 1122 | case 'custom': 1123 | { 1124 | state.html += '
'; 1125 | state.html += '
'; 1126 | state.html += field.value; 1127 | state.html += '
'; 1128 | state.html += '
'; 1129 | 1130 | break; 1131 | } 1132 | } 1133 | 1134 | if (field.hasOwnProperty('desc') && field.desc != '') 1135 | { 1136 | state.html += '
' + EscapeHTML($this.Translate(field.desc)) + '
'; 1137 | } 1138 | else if (field.hasOwnProperty('htmldesc') && field.htmldesc != '') 1139 | { 1140 | state.html += '
' + $this.Translate(field.htmldesc) + '
'; 1141 | } 1142 | 1143 | if (field.hasOwnProperty('error') && field.error != '') 1144 | { 1145 | state.html += '
'; 1146 | state.html += '
' + EscapeHTML($this.Translate(field.error)) + '
'; 1147 | state.html += '
'; 1148 | } 1149 | 1150 | state.html += ''; 1151 | 1152 | if (state.insiderow) state.html += ''; 1153 | } 1154 | }; 1155 | }; 1156 | 1157 | window.FlexForms.Designer = new DesignerInternal(); 1158 | 1159 | // Merge down into FlexForms. 1160 | window.FlexForms.settings = window.FlexForms.Designer.settings; 1161 | window.FlexForms.EscapeHTML = EscapeHTML; 1162 | window.FlexForms.FormatStr = FormatStr; 1163 | window.FlexForms.CreateNode = CreateNode; 1164 | window.FlexForms.DebounceAttributes = DebounceAttributes; 1165 | 1166 | for (var x in window.FlexForms.Designer) 1167 | { 1168 | if (window.FlexForms.Designer.hasOwnProperty(x) && typeof window.FlexForms.Designer[x] === 'function' && !window.FlexForms.hasOwnProperty(x)) window.FlexForms[x] = window.FlexForms.Designer[x]; 1169 | } 1170 | })(); 1171 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Loading Offline Forms 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 34 | 35 | 36 |
Main Menu
37 | 38 |
39 |
40 | 41 |
42 | 43 | 1606 |
1607 | 1610 | 1611 | 1612 | --------------------------------------------------------------------------------