SMAM (short for Send Me A Mail) is a free (as in freedom) contact form embedding software.

form.js 10KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399
  1. /***************************************************************
  2. * *
  3. * SMAM: Send Me A Mail *
  4. * *
  5. * Made with ♥ by Brendan Abolivier <foss@brendanabolivier.com> *
  6. * Source code available under GPLv3 license here: *
  7. * https://github.com/babolivier/smam/ *
  8. * *
  9. ***************************************************************/
  10. var prefix = 'form'
  11. var items = {
  12. name: 'name',
  13. addr: 'addr',
  14. subj: 'subj',
  15. text: 'text',
  16. };
  17. var DOMFields = {};
  18. var server = getServer();
  19. var token = "";
  20. var labels = true;
  21. var lang = [];
  22. var customFields = {};
  23. var xhr = {
  24. customFields: new XMLHttpRequest(),
  25. lang: new XMLHttpRequest(),
  26. token: new XMLHttpRequest(),
  27. send: new XMLHttpRequest()
  28. }
  29. // XHR callbacks
  30. xhr.customFields.onreadystatechange = function() {
  31. if(xhr.customFields.readyState == XMLHttpRequest.DONE) {
  32. customFields = JSON.parse(xhr.customFields.responseText);
  33. for(let field in customFields) {
  34. customFields[field].name = field;
  35. }
  36. }
  37. };
  38. xhr.token.onreadystatechange = function() {
  39. if(xhr.token.readyState == XMLHttpRequest.DONE) {
  40. token = xhr.token.responseText;
  41. }
  42. };
  43. xhr.lang.onreadystatechange = function() {
  44. if(xhr.lang.readyState == XMLHttpRequest.DONE) {
  45. let response = JSON.parse(xhr.lang.responseText);
  46. lang = response.translations;
  47. labels = response.labels;
  48. }
  49. };
  50. xhr.send.onreadystatechange = function() {
  51. if(xhr.send.readyState == XMLHttpRequest.DONE) {
  52. let status = document.getElementById('form_status');
  53. status.setAttribute('class', '');
  54. if(xhr.send.status === 200) {
  55. cleanForm();
  56. status.setAttribute('class', 'success');
  57. status.innerHTML = lang.send_status_success;
  58. } else {
  59. status.setAttribute('class', 'failure');
  60. status.innerHTML = lang.send_status_failure;
  61. }
  62. }
  63. };
  64. // Returns the server's base URI based on the user's script tag
  65. // return: the SMAM server's base URI
  66. function getServer() {
  67. var scripts = document.getElementsByTagName('script');
  68. // Parsing all the <script> tags to find the URL to our file
  69. for(var i = 0; i < scripts.length; i++) {
  70. let script = scripts[i];
  71. if(script.src) {
  72. let url = script.src;
  73. // This should be our script
  74. if(url.match(/form\.js$/)) {
  75. // Port has been found
  76. return url.match(/^(https?:\/\/.+)\/form\.js/)[1];
  77. }
  78. }
  79. }
  80. }
  81. // Creates a form
  82. // id: HTML identifier of the document's block to create the form into
  83. // return: nothing
  84. function generateForm(id) {
  85. // Get translated strings
  86. getLangSync();
  87. // Get custom fields if defined in the configuration
  88. getCustomFieldsSync();
  89. var el = document.getElementById(id);
  90. // Set the form's behaviour
  91. el.setAttribute('onsubmit', 'sendForm(); return false;');
  92. // Add an empty paragraph for status
  93. var status = document.createElement('p');
  94. status.setAttribute('id', 'form_status');
  95. el.appendChild(status);
  96. // Default fields
  97. DOMFields = {
  98. name: getField({
  99. name: items.name,
  100. label: lang.form_name_label,
  101. type: 'text',
  102. required: true
  103. }),
  104. addr: getField({
  105. name: items.addr,
  106. label: lang.form_addr_label,
  107. type: 'email',
  108. required: true
  109. }),
  110. subj: getField({
  111. name: items.subj,
  112. label: lang.form_subj_label,
  113. type: 'text',
  114. required: true
  115. }),
  116. text: getField({
  117. name: items.text,
  118. label: lang.form_mesg_label,
  119. type: 'textarea',
  120. required: true
  121. })
  122. };
  123. // Adding custom fields
  124. for(let fieldName in customFields) {
  125. let field = customFields[fieldName];
  126. DOMFields[fieldName] = getField(field);
  127. }
  128. // Adding all nodes to document
  129. for(let field in DOMFields) {
  130. el.appendChild(DOMFields[field]);
  131. }
  132. // Adding submit button
  133. el.appendChild(getSubmitButton('form_subm', lang.form_subm_label));
  134. // Retrieve the token from the server
  135. getToken();
  136. }
  137. // Get the HTML element for a given field
  138. // fieldInfos: object describing the field
  139. // required: boolean on whether the field is required or optional
  140. // return: a block containing the field and a label describing it (if enabled)
  141. function getField(fieldInfos) {
  142. var block = document.createElement('div');
  143. block.setAttribute('id', fieldInfos.name);
  144. // Declare the variable first
  145. let field = {};
  146. // Easily add new supported input types
  147. switch(fieldInfos.type) {
  148. case 'textarea': field = getTextarea(fieldInfos);
  149. break;
  150. case 'select': field = getSelectField(fieldInfos);
  151. break;
  152. default: field = getInputField(fieldInfos);
  153. break;
  154. }
  155. // We need the input field's ID to bind it to the label, so we generate the
  156. // field first
  157. if(labels) {
  158. block.appendChild(getLabel(fieldInfos.label, field.id));
  159. }
  160. // Assemble the block and return it
  161. block.appendChild(field);
  162. return block;
  163. }
  164. // Returns a label
  165. // content: label's inner content
  166. // id: field HTML identifier
  167. // return: a label node the field's description
  168. function getLabel(content, id) {
  169. var label = document.createElement('label');
  170. label.setAttribute('for', id);
  171. label.innerHTML = content;
  172. return label;
  173. }
  174. // Returns a <select> HTML element
  175. // fieldInfos: object describing the field
  176. // required: boolean on whether the field is required or optional
  177. // return: a <select> element corresponding to the info passed as input
  178. function getSelectField(fieldInfos) {
  179. let field = document.createElement('select');
  180. // Set attributes when necessary
  181. if(fieldInfos.required) {
  182. field.setAttribute('required', 'required');
  183. }
  184. field.setAttribute('id', prefix + '_' + fieldInfos.name + '_select');
  185. let index = 0;
  186. // Add header option, useful if the field is required
  187. let header = document.createElement('option');
  188. // The value must be an empty string so the browser can block the submit
  189. // event if the field is required
  190. header.setAttribute('value', '');
  191. header.innerHTML = lang.form_select_header_option;
  192. field.appendChild(header);
  193. // Add all options to select
  194. for(let choice of fieldInfos.options) {
  195. let option = document.createElement('option');
  196. // Options' values are incremental numeric indexes
  197. option.setAttribute('value', index);
  198. // Set the value defined by the user
  199. option.innerHTML = choice;
  200. field.appendChild(option);
  201. // Increment the index
  202. index++;
  203. }
  204. return field
  205. }
  206. // Returns a <input> HTML element with desired type
  207. // fieldInfos: object describing the field
  208. // required: boolean on whether the field is required or optional
  209. // type: type of the input field (text, email, date...)
  210. // return: a <input> HTML element corresponding to the info passed as input
  211. function getInputField(fieldInfos, required) {
  212. let field = getBaseField(fieldInfos, required, 'input')
  213. field.setAttribute('type', fieldInfos.type);
  214. return field;
  215. }
  216. // Returns a <textarea> HTML element
  217. // fieldInfos: object describing the field
  218. // required: boolean on whether the field is required or optional
  219. // return: a <textarea> element corresponding to the info passed as input
  220. function getTextarea(fieldInfos, required) {
  221. return getBaseField(fieldInfos, required, 'textarea');
  222. }
  223. // Returns a base HTML element with generic info to be processed by functions at
  224. // higher level
  225. // fieldInfos: object describing the field
  226. // required: boolean on whether the field is required or optional
  227. // tag: the HTML tag the field element must have
  228. // return: a HTML element of the given tag with basic info given as input
  229. function getBaseField(fieldInfos, required, tag) {
  230. let field = document.createElement(tag);
  231. if(fieldInfos.required) {
  232. field.setAttribute('required', 'required');
  233. }
  234. field.setAttribute('placeholder', fieldInfos.label);
  235. field.setAttribute('id', prefix + '_' + fieldInfos.name + '_' + tag);
  236. return field;
  237. }
  238. // Returns a submit button
  239. // id: button HTML identifier
  240. // text: button text
  241. // return: a div node containing the button
  242. function getSubmitButton(id, text) {
  243. var submit = document.createElement('div');
  244. submit.setAttribute('id', id);
  245. var button = document.createElement('button');
  246. button.setAttribute('type', 'submit');
  247. button.setAttribute('id', id + '_btn');
  248. button.innerHTML = text;
  249. submit.appendChild(button);
  250. return submit;
  251. }
  252. // Send form data through the XHR object
  253. // return: nothing
  254. function sendForm() {
  255. // Clear status
  256. let status = document.getElementById('form_status');
  257. status.setAttribute('class', 'sending');
  258. status.innerHTML = lang.send_status_progress;
  259. xhr.send.open('POST', server + '/send');
  260. xhr.send.setRequestHeader('Content-Type', 'application/json');
  261. xhr.send.send(JSON.stringify(getFormData()));
  262. // Get a new token
  263. getToken();
  264. }
  265. // Fetch form inputs from HTML elements
  266. // return: an object containing all the user's input
  267. function getFormData() {
  268. let data = {};
  269. data.token = token;
  270. data.custom = {};
  271. // Select the field
  272. let index = 0;
  273. if(labels) {
  274. index = 1;
  275. }
  276. // Iterate over all the fields
  277. for(let field in DOMFields) {
  278. let el = DOMFields[field].children[index];
  279. // Do we need to push this field into default or custom fields?
  280. if(field in customFields) {
  281. data.custom[field] = el.value;
  282. } else {
  283. data[field] = el.value;
  284. }
  285. }
  286. return data;
  287. }
  288. // Empties the form fields
  289. // return: nothing
  290. function cleanForm() {
  291. // Select the field
  292. let index = 0;
  293. if(labels) {
  294. index = 1;
  295. }
  296. // Iterate over all the fields
  297. for(let field in DOMFields) {
  298. let el = DOMFields[field].children[index];
  299. // If it's a <select> element, select the first element so it looks
  300. // like a reset
  301. if(!el.tagName.toLowerCase().localeCompare('select')) {
  302. el.children[0].selected = true;
  303. } else {
  304. el.value = '';
  305. }
  306. }
  307. }
  308. // Asks the server for a token
  309. // return: nothing
  310. function getToken() {
  311. xhr.token.open('GET', server + '/register');
  312. xhr.token.send();
  313. }
  314. // Asks the server for translated strings to display
  315. // return: notghing
  316. function getLangSync() {
  317. xhr.lang.open('GET', server + '/lang', false);
  318. xhr.lang.send();
  319. }
  320. // Asks the server for the custom fields if there's one or more set in the
  321. // configuration file
  322. // return: nothing
  323. function getCustomFieldsSync() {
  324. xhr.customFields.open('GET', server + '/fields', false);
  325. xhr.customFields.send();
  326. }