forms.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440
  1. $(document).ready(function() {
  2. // Pagination
  3. $('select#per_page').change(function() {
  4. this.form.submit();
  5. });
  6. // "Toggle" checkbox for object lists (PK column)
  7. $('input:checkbox.toggle').click(function() {
  8. $(this).closest('table').find('input:checkbox[name=pk]:visible').prop('checked', $(this).prop('checked'));
  9. // Show the "select all" box if present
  10. if ($(this).is(':checked')) {
  11. $('#select_all_box').removeClass('hidden');
  12. } else {
  13. $('#select_all').prop('checked', false);
  14. }
  15. });
  16. // Uncheck the "toggle" and "select all" checkboxes if an item is unchecked
  17. $('input:checkbox[name=pk]').click(function (event) {
  18. if (!$(this).attr('checked')) {
  19. $('input:checkbox.toggle, #select_all').prop('checked', false);
  20. }
  21. });
  22. // Enable hidden buttons when "select all" is checked
  23. $('#select_all').click(function() {
  24. if ($(this).is(':checked')) {
  25. $('#select_all_box').find('button').prop('disabled', '');
  26. } else {
  27. $('#select_all_box').find('button').prop('disabled', 'disabled');
  28. }
  29. });
  30. // Slugify
  31. function slugify(s, num_chars) {
  32. s = s.replace(/[^\-\.\w\s]/g, ''); // Remove unneeded chars
  33. s = s.replace(/^[\s\.]+|[\s\.]+$/g, ''); // Trim leading/trailing spaces
  34. s = s.replace(/[\-\.\s]+/g, '-'); // Convert spaces and decimals to hyphens
  35. s = s.toLowerCase(); // Convert to lowercase
  36. return s.substring(0, num_chars); // Trim to first num_chars chars
  37. }
  38. var slug_field = $('#id_slug');
  39. slug_field.change(function() {
  40. $(this).attr('_changed', true);
  41. });
  42. if (slug_field) {
  43. var slug_source = $('#id_' + slug_field.attr('slug-source'));
  44. var slug_length = slug_field.attr('maxlength');
  45. slug_source.on('keyup change', function() {
  46. if (slug_field && !slug_field.attr('_changed')) {
  47. slug_field.val(slugify($(this).val(), (slug_length ? slug_length : 50)));
  48. }
  49. })
  50. }
  51. // Bulk edit nullification
  52. $('input:checkbox[name=_nullify]').click(function() {
  53. $('#id_' + this.value).toggle('disabled');
  54. });
  55. // Set formaction and submit using a link
  56. $('a.formaction').click(function(event) {
  57. event.preventDefault();
  58. var form = $(this).closest('form');
  59. form.attr('action', $(this).attr('href'));
  60. form.submit();
  61. });
  62. // Parse URLs which may contain variable refrences to other field values
  63. function parseURL(url) {
  64. var filter_regex = /\{\{([a-z_]+)\}\}/g;
  65. var match;
  66. var rendered_url = url;
  67. var filter_field;
  68. while (match = filter_regex.exec(url)) {
  69. filter_field = $('#id_' + match[1]);
  70. var custom_attr = $('option:selected', filter_field).attr('api-value');
  71. if (custom_attr) {
  72. rendered_url = rendered_url.replace(match[0], custom_attr);
  73. } else if (filter_field.val()) {
  74. rendered_url = rendered_url.replace(match[0], filter_field.val());
  75. } else if (filter_field.attr('nullable') == 'true') {
  76. rendered_url = rendered_url.replace(match[0], 'null');
  77. }
  78. }
  79. return rendered_url
  80. }
  81. // Assign color picker selection classes
  82. function colorPickerClassCopy(data, container) {
  83. if (data.element) {
  84. // Swap the style
  85. $(container).attr('style', $(data.element).attr("style"));
  86. }
  87. return data.text;
  88. }
  89. // Color Picker
  90. $('.netbox-select2-color-picker').select2({
  91. allowClear: true,
  92. placeholder: "---------",
  93. theme: "bootstrap",
  94. templateResult: colorPickerClassCopy,
  95. templateSelection: colorPickerClassCopy,
  96. width: "off"
  97. });
  98. // Static choice selection
  99. $('.netbox-select2-static').select2({
  100. allowClear: true,
  101. placeholder: "---------",
  102. theme: "bootstrap",
  103. width: "off"
  104. });
  105. // API backed selection
  106. // Includes live search and chained fields
  107. // The `multiple` setting may be controled via a data-* attribute
  108. $('.netbox-select2-api').select2({
  109. allowClear: true,
  110. placeholder: "---------",
  111. theme: "bootstrap",
  112. width: "off",
  113. ajax: {
  114. delay: 500,
  115. url: function(params) {
  116. var element = this[0];
  117. var url = parseURL(element.getAttribute("data-url"));
  118. if (url.includes("{{")) {
  119. // URL is not fully rendered yet, abort the request
  120. return false;
  121. }
  122. return url;
  123. },
  124. data: function(params) {
  125. var element = this[0];
  126. // Paging. Note that `params.page` indexes at 1
  127. var offset = (params.page - 1) * 50 || 0;
  128. // Base query params
  129. var parameters = {
  130. q: params.term,
  131. limit: 50,
  132. offset: offset,
  133. };
  134. // Allow for controlling the brief setting from within APISelect
  135. parameters.brief = ( $(element).is('[data-full]') ? undefined : true );
  136. // filter-for fields from a chain
  137. var attr_name = "data-filter-for-" + $(element).attr("name");
  138. var form = $(element).closest('form');
  139. var filter_for_elements = form.find("select[" + attr_name + "]");
  140. filter_for_elements.each(function(index, filter_for_element) {
  141. var param_name = $(filter_for_element).attr(attr_name);
  142. var is_nullable = $(filter_for_element).attr("nullable");
  143. var is_visible = $(filter_for_element).is(":visible");
  144. var value = $(filter_for_element).val();
  145. if (param_name && is_visible && value) {
  146. parameters[param_name] = value;
  147. } else if (param_name && is_visible && is_nullable) {
  148. parameters[param_name] = "null";
  149. }
  150. });
  151. // Conditional query params
  152. $.each(element.attributes, function(index, attr){
  153. if (attr.name.includes("data-conditional-query-param-")){
  154. var conditional = attr.name.split("data-conditional-query-param-")[1].split("__");
  155. var field = $("#id_" + conditional[0]);
  156. var field_value = conditional[1];
  157. if ($('option:selected', field).attr('api-value') === field_value){
  158. var _val = attr.value.split("=");
  159. parameters[_val[0]] = _val[1];
  160. }
  161. }
  162. });
  163. // Additional query params
  164. $.each(element.attributes, function(index, attr){
  165. if (attr.name.includes("data-additional-query-param-")){
  166. var param_name = attr.name.split("data-additional-query-param-")[1];
  167. if (param_name in parameters) {
  168. if (Array.isArray(parameters[param_name])) {
  169. parameters[param_name].push(attr.value)
  170. } else {
  171. parameters[param_name] = [parameters[param_name], attr.value]
  172. }
  173. } else {
  174. parameters[param_name] = attr.value;
  175. }
  176. }
  177. });
  178. // This will handle params with multiple values (i.e. for list filter forms)
  179. return $.param(parameters, true);
  180. },
  181. processResults: function (data) {
  182. var element = this.$element[0];
  183. $(element).children('option').attr('disabled', false);
  184. var results = data.results;
  185. results = results.reduce((results,record,idx) => {
  186. record.text = record[element.getAttribute('display-field')] || record.name;
  187. record.id = record[element.getAttribute('value-field')] || record.id;
  188. if(element.getAttribute('disabled-indicator') && record[element.getAttribute('disabled-indicator')]) {
  189. // The disabled-indicator equated to true, so we disable this option
  190. record.disabled = true;
  191. }
  192. if( record.group !== undefined && record.group !== null && record.site !== undefined && record.site !== null ) {
  193. results[record.site.name + ":" + record.group.name] = results[record.site.name + ":" + record.group.name] || { text: record.site.name + " / " + record.group.name, children: [] }
  194. results[record.site.name + ":" + record.group.name].children.push(record);
  195. }
  196. else if( record.group !== undefined && record.group !== null ) {
  197. results[record.group.name] = results[record.group.name] || { text: record.group.name, children: [] }
  198. results[record.group.name].children.push(record);
  199. }
  200. else if( record.site !== undefined && record.site !== null ) {
  201. results[record.site.name] = results[record.site.name] || { text: record.site.name, children: [] }
  202. results[record.site.name].children.push(record);
  203. }
  204. else if ( (record.group !== undefined || record.group == null) && (record.site !== undefined || record.site === null) ) {
  205. results['global'] = results['global'] || { text: 'Global', children: [] }
  206. results['global'].children.push(record);
  207. }
  208. else {
  209. results[idx] = record
  210. }
  211. return results;
  212. },Object.create(null));
  213. results = Object.values(results);
  214. // Handle the null option, but only add it once
  215. if (element.getAttribute('data-null-option') && data.previous === null) {
  216. var null_option = $(element).children()[0];
  217. results.unshift({
  218. id: null_option.value,
  219. text: null_option.text
  220. });
  221. }
  222. // Check if there are more results to page
  223. var page = data.next !== null;
  224. return {
  225. results: results,
  226. pagination: {
  227. more: page
  228. }
  229. };
  230. }
  231. }
  232. });
  233. // Flatpickr selectors
  234. $('.date-picker').flatpickr({
  235. allowInput: true
  236. });
  237. $('.datetime-picker').flatpickr({
  238. allowInput: true,
  239. enableSeconds: true,
  240. enableTime: true,
  241. time_24hr: true
  242. });
  243. $('.time-picker').flatpickr({
  244. allowInput: true,
  245. enableSeconds: true,
  246. enableTime: true,
  247. noCalendar: true,
  248. time_24hr: true
  249. });
  250. // API backed tags
  251. var tags = $('#id_tags');
  252. if (tags.length > 0 && tags.val().length > 0){
  253. tags = $('#id_tags').val().split(/,\s*/);
  254. } else {
  255. tags = [];
  256. }
  257. tag_objs = $.map(tags, function (tag) {
  258. return {
  259. id: tag,
  260. text: tag,
  261. selected: true
  262. }
  263. });
  264. // Replace the django issued text input with a select element
  265. $('#id_tags').replaceWith('<select name="tags" id="id_tags" class="form-control"></select>');
  266. $('#id_tags').select2({
  267. tags: true,
  268. data: tag_objs,
  269. multiple: true,
  270. allowClear: true,
  271. placeholder: "Tags",
  272. theme: "bootstrap",
  273. width: "off",
  274. ajax: {
  275. delay: 250,
  276. url: netbox_api_path + "extras/tags/",
  277. data: function(params) {
  278. // Paging. Note that `params.page` indexes at 1
  279. var offset = (params.page - 1) * 50 || 0;
  280. var parameters = {
  281. q: params.term,
  282. brief: 1,
  283. limit: 50,
  284. offset: offset,
  285. };
  286. return parameters;
  287. },
  288. processResults: function (data) {
  289. var results = $.map(data.results, function (obj) {
  290. // If tag contains space add double quotes
  291. if (/\s/.test(obj.name))
  292. obj.name = '"' + obj.name + '"'
  293. return {
  294. id: obj.name,
  295. text: obj.name
  296. }
  297. });
  298. // Check if there are more results to page
  299. var page = data.next !== null;
  300. return {
  301. results: results,
  302. pagination: {
  303. more: page
  304. }
  305. };
  306. }
  307. }
  308. });
  309. $('#id_tags').closest('form').submit(function(event){
  310. // django-taggit can only accept a single comma seperated string value
  311. var value = $('#id_tags').val();
  312. if (value.length > 0){
  313. var final_tags = value.join(', ');
  314. $('#id_tags').val(null).trigger('change');
  315. var option = new Option(final_tags, final_tags, true, true);
  316. $('#id_tags').append(option).trigger('change');
  317. }
  318. });
  319. if( $('select#id_mode').length > 0 ) {
  320. $('select#id_mode').on('change', function () {
  321. if ($(this).val() == '') {
  322. $('select#id_untagged_vlan').val();
  323. $('select#id_untagged_vlan').trigger('change');
  324. $('select#id_tagged_vlans').val([]);
  325. $('select#id_tagged_vlans').trigger('change');
  326. $('select#id_untagged_vlan').parent().parent().hide();
  327. $('select#id_tagged_vlans').parent().parent().hide();
  328. }
  329. else if ($(this).val() == 100) {
  330. $('select#id_tagged_vlans').val([]);
  331. $('select#id_tagged_vlans').trigger('change');
  332. $('select#id_untagged_vlan').parent().parent().show();
  333. $('select#id_tagged_vlans').parent().parent().hide();
  334. }
  335. else if ($(this).val() == 200) {
  336. $('select#id_untagged_vlan').parent().parent().show();
  337. $('select#id_tagged_vlans').parent().parent().show();
  338. }
  339. else if ($(this).val() == 300) {
  340. $('select#id_tagged_vlans').val([]);
  341. $('select#id_tagged_vlans').trigger('change');
  342. $('select#id_untagged_vlan').parent().parent().show();
  343. $('select#id_tagged_vlans').parent().parent().hide();
  344. }
  345. });
  346. $('select#id_mode').trigger('change');
  347. }
  348. // Scroll up an offset equal to the first nav element if a hash is present
  349. // Cannot use '#navbar' because it is not always visible, like in small windows
  350. function headerOffsetScroll() {
  351. if (window.location.hash) {
  352. // Short wait needed to allow the page to scroll to the element
  353. setTimeout(function() {
  354. window.scrollBy(0, -$('nav').height())
  355. }, 10);
  356. }
  357. }
  358. // Account for the header height when hash-scrolling
  359. window.addEventListener('load', headerOffsetScroll);
  360. window.addEventListener('hashchange', headerOffsetScroll);
  361. // Offset between the preview window and the window edges
  362. const IMAGE_PREVIEW_OFFSET_X = 20;
  363. const IMAGE_PREVIEW_OFFSET_Y = 10;
  364. // Preview an image attachment when the link is hovered over
  365. $('a.image-preview').on('mouseover', function(e) {
  366. // Twice the offset to account for all sides of the picture
  367. var maxWidth = window.innerWidth - (e.clientX + (IMAGE_PREVIEW_OFFSET_X * 2));
  368. var maxHeight = window.innerHeight - (e.clientY + (IMAGE_PREVIEW_OFFSET_Y * 2));
  369. var img = $('<img>').attr('id', 'image-preview-window').css({
  370. display: 'none',
  371. position: 'absolute',
  372. maxWidth: maxWidth + 'px',
  373. maxHeight: maxHeight + 'px',
  374. left: e.pageX + IMAGE_PREVIEW_OFFSET_X + 'px',
  375. top: e.pageY + IMAGE_PREVIEW_OFFSET_Y + 'px',
  376. boxShadow: '0 0px 12px 3px rgba(0, 0, 0, 0.4)',
  377. });
  378. // Remove any existing preview windows and add the current one
  379. $('#image-preview-window').remove();
  380. $('body').append(img);
  381. // Once loaded, show the preview if the image is indeed an image
  382. img.on('load', function(e) {
  383. if (e.target.complete && e.target.naturalWidth) {
  384. $('#image-preview-window').fadeIn('fast');
  385. }
  386. });
  387. // Begin loading
  388. img.attr('src', e.target.href);
  389. });
  390. // Fade the image out; it will be deleted when another one is previewed
  391. $('a.image-preview').on('mouseout', function() {
  392. $('#image-preview-window').fadeOut('fast');
  393. });
  394. });