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

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