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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408
  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. };
  117. // Adding custom fields
  118. for(let fieldName in customFields) {
  119. let field = customFields[fieldName];
  120. DOMFields[fieldName] = getField(field);
  121. }
  122. // Add the message's textarea
  123. DOMFields['text'] = getField({
  124. name: items.text,
  125. label: lang.form_mesg_label,
  126. type: 'textarea',
  127. required: true
  128. });
  129. // Adding all nodes to document
  130. for(let field in DOMFields) {
  131. el.appendChild(DOMFields[field]);
  132. }
  133. // Adding submit button
  134. el.appendChild(getSubmitButton('form_subm', lang.form_subm_label));
  135. // Retrieve the token from the server
  136. getToken();
  137. }
  138. // Get the HTML element for a given field
  139. // fieldInfos: object describing the field
  140. // required: boolean on whether the field is required or optional
  141. // return: a block containing the field and a label describing it (if enabled)
  142. function getField(fieldInfos) {
  143. var block = document.createElement('div');
  144. block.setAttribute('id', 'form_' + fieldInfos.name);
  145. // Declare the variable first
  146. let field = {};
  147. // Easily add new supported input types
  148. switch(fieldInfos.type) {
  149. case 'textarea': field = getTextarea(fieldInfos);
  150. break;
  151. case 'select': field = getSelectField(fieldInfos);
  152. break;
  153. default: field = getInputField(fieldInfos);
  154. break;
  155. }
  156. // We need the input field's ID to bind it to the label, so we generate the
  157. // field first
  158. if(labels) {
  159. block.appendChild(getLabel(fieldInfos.label, field.id));
  160. }
  161. // Assemble the block and return it
  162. block.appendChild(field);
  163. return block;
  164. }
  165. // Returns a label
  166. // content: label's inner content
  167. // id: field HTML identifier
  168. // return: a label node the field's description
  169. function getLabel(content, id) {
  170. var label = document.createElement('label');
  171. label.setAttribute('for', id);
  172. label.innerHTML = content;
  173. return label;
  174. }
  175. // Returns a <select> HTML element
  176. // fieldInfos: object describing the field
  177. // required: boolean on whether the field is required or optional
  178. // return: a <select> element corresponding to the info passed as input
  179. function getSelectField(fieldInfos) {
  180. let field = document.createElement('select');
  181. // Set attributes when necessary
  182. if(fieldInfos.required) {
  183. field.setAttribute('required', 'required');
  184. }
  185. field.setAttribute('id', prefix + '_' + fieldInfos.name + '_select');
  186. let index = 0;
  187. // Add header option, useful if the field is required
  188. let header = document.createElement('option');
  189. // The value must be an empty string so the browser can block the submit
  190. // event if the field is required
  191. header.setAttribute('value', '');
  192. // If the labels are disabled, set the header option's inner text as the
  193. // field's label
  194. if(labels) {
  195. header.innerHTML = lang.form_select_header_option;
  196. } else {
  197. header.innerHTML = fieldInfos.label;
  198. }
  199. field.appendChild(header);
  200. // Add all options to select
  201. for(let choice of fieldInfos.options) {
  202. let option = document.createElement('option');
  203. // Options' values are incremental numeric indexes
  204. option.setAttribute('value', index);
  205. // Set the value defined by the user
  206. option.innerHTML = choice;
  207. field.appendChild(option);
  208. // Increment the index
  209. index++;
  210. }
  211. return field
  212. }
  213. // Returns a <input> HTML element with desired type
  214. // fieldInfos: object describing the field
  215. // required: boolean on whether the field is required or optional
  216. // type: type of the input field (text, email, date...)
  217. // return: a <input> HTML element corresponding to the info passed as input
  218. function getInputField(fieldInfos, required) {
  219. let field = getBaseField(fieldInfos, required, 'input')
  220. field.setAttribute('type', fieldInfos.type);
  221. return field;
  222. }
  223. // Returns a <textarea> HTML element
  224. // fieldInfos: object describing the field
  225. // required: boolean on whether the field is required or optional
  226. // return: a <textarea> element corresponding to the info passed as input
  227. function getTextarea(fieldInfos, required) {
  228. return getBaseField(fieldInfos, required, 'textarea');
  229. }
  230. // Returns a base HTML element with generic info to be processed by functions at
  231. // higher level
  232. // fieldInfos: object describing the field
  233. // required: boolean on whether the field is required or optional
  234. // tag: the HTML tag the field element must have
  235. // return: a HTML element of the given tag with basic info given as input
  236. function getBaseField(fieldInfos, required, tag) {
  237. let field = document.createElement(tag);
  238. if(fieldInfos.required) {
  239. field.setAttribute('required', 'required');
  240. }
  241. field.setAttribute('placeholder', fieldInfos.label);
  242. field.setAttribute('id', prefix + '_' + fieldInfos.name + '_' + tag);
  243. return field;
  244. }
  245. // Returns a submit button
  246. // id: button HTML identifier
  247. // text: button text
  248. // return: a div node containing the button
  249. function getSubmitButton(id, text) {
  250. var submit = document.createElement('div');
  251. submit.setAttribute('id', id);
  252. var button = document.createElement('button');
  253. button.setAttribute('type', 'submit');
  254. button.setAttribute('id', id + '_btn');
  255. button.innerHTML = text;
  256. submit.appendChild(button);
  257. return submit;
  258. }
  259. // Send form data through the XHR object
  260. // return: nothing
  261. function sendForm() {
  262. // Clear status
  263. let status = document.getElementById('form_status');
  264. status.setAttribute('class', 'sending');
  265. status.innerHTML = lang.send_status_progress;
  266. xhr.send.open('POST', server + '/send');
  267. xhr.send.setRequestHeader('Content-Type', 'application/json');
  268. xhr.send.send(JSON.stringify(getFormData()));
  269. // Get a new token
  270. getToken();
  271. }
  272. // Fetch form inputs from HTML elements
  273. // return: an object containing all the user's input
  274. function getFormData() {
  275. let data = {};
  276. data.token = token;
  277. data.custom = {};
  278. // Select the field
  279. let index = 0;
  280. if(labels) {
  281. index = 1;
  282. }
  283. // Iterate over all the fields
  284. for(let field in DOMFields) {
  285. let el = DOMFields[field].children[index];
  286. // Do we need to push this field into default or custom fields?
  287. if(field in customFields) {
  288. data.custom[field] = el.value;
  289. } else {
  290. data[field] = el.value;
  291. }
  292. }
  293. return data;
  294. }
  295. // Empties the form fields
  296. // return: nothing
  297. function cleanForm() {
  298. // Select the field
  299. let index = 0;
  300. if(labels) {
  301. index = 1;
  302. }
  303. // Iterate over all the fields
  304. for(let field in DOMFields) {
  305. let el = DOMFields[field].children[index];
  306. // If it's a <select> element, select the first element so it looks
  307. // like a reset
  308. if(!el.tagName.toLowerCase().localeCompare('select')) {
  309. el.children[0].selected = true;
  310. } else {
  311. el.value = '';
  312. }
  313. }
  314. }
  315. // Asks the server for a token
  316. // return: nothing
  317. function getToken() {
  318. xhr.token.open('GET', server + '/register');
  319. xhr.token.send();
  320. }
  321. // Asks the server for translated strings to display
  322. // return: notghing
  323. function getLangSync() {
  324. xhr.lang.open('GET', server + '/lang', false);
  325. xhr.lang.send();
  326. }
  327. // Asks the server for the custom fields if there's one or more set in the
  328. // configuration file
  329. // return: nothing
  330. function getCustomFieldsSync() {
  331. xhr.customFields.open('GET', server + '/fields', false);
  332. xhr.customFields.send();
  333. }