forms.js 19 KB

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