forms.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443
  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_required = $(filter_for_element).attr("required");
  143. var is_nullable = $(filter_for_element).attr("nullable");
  144. var is_visible = $(filter_for_element).is(":visible");
  145. var value = $(filter_for_element).val();
  146. if (param_name && is_visible) {
  147. if (value) {
  148. parameters[param_name] = value;
  149. } else if (is_required && is_nullable) {
  150. parameters[param_name] = "null";
  151. }
  152. }
  153. });
  154. // Conditional query params
  155. $.each(element.attributes, function(index, attr){
  156. if (attr.name.includes("data-conditional-query-param-")){
  157. var conditional = attr.name.split("data-conditional-query-param-")[1].split("__");
  158. var field = $("#id_" + conditional[0]);
  159. var field_value = conditional[1];
  160. if ($('option:selected', field).attr('api-value') === field_value){
  161. var _val = attr.value.split("=");
  162. parameters[_val[0]] = _val[1];
  163. }
  164. }
  165. });
  166. // Additional query params
  167. $.each(element.attributes, function(index, attr){
  168. if (attr.name.includes("data-additional-query-param-")){
  169. var param_name = attr.name.split("data-additional-query-param-")[1];
  170. if (param_name in parameters) {
  171. if (Array.isArray(parameters[param_name])) {
  172. parameters[param_name].push(attr.value)
  173. } else {
  174. parameters[param_name] = [parameters[param_name], attr.value]
  175. }
  176. } else {
  177. parameters[param_name] = attr.value;
  178. }
  179. }
  180. });
  181. // This will handle params with multiple values (i.e. for list filter forms)
  182. return $.param(parameters, true);
  183. },
  184. processResults: function (data) {
  185. var element = this.$element[0];
  186. $(element).children('option').attr('disabled', false);
  187. var results = data.results;
  188. results = results.reduce((results,record,idx) => {
  189. record.text = record[element.getAttribute('display-field')] || record.name;
  190. record.id = record[element.getAttribute('value-field')] || record.id;
  191. if(element.getAttribute('disabled-indicator') && record[element.getAttribute('disabled-indicator')]) {
  192. // The disabled-indicator equated to true, so we disable this option
  193. record.disabled = true;
  194. }
  195. if( record.group !== undefined && record.group !== null && record.site !== undefined && record.site !== null ) {
  196. results[record.site.name + ":" + record.group.name] = results[record.site.name + ":" + record.group.name] || { text: record.site.name + " / " + record.group.name, children: [] }
  197. results[record.site.name + ":" + record.group.name].children.push(record);
  198. }
  199. else if( record.group !== undefined && record.group !== null ) {
  200. results[record.group.name] = results[record.group.name] || { text: record.group.name, children: [] }
  201. results[record.group.name].children.push(record);
  202. }
  203. else if( record.site !== undefined && record.site !== null ) {
  204. results[record.site.name] = results[record.site.name] || { text: record.site.name, children: [] }
  205. results[record.site.name].children.push(record);
  206. }
  207. else if ( (record.group !== undefined || record.group == null) && (record.site !== undefined || record.site === null) ) {
  208. results['global'] = results['global'] || { text: 'Global', children: [] }
  209. results['global'].children.push(record);
  210. }
  211. else {
  212. results[idx] = record
  213. }
  214. return results;
  215. },Object.create(null));
  216. results = Object.values(results);
  217. // Handle the null option, but only add it once
  218. if (element.getAttribute('data-null-option') && data.previous === null) {
  219. var null_option = $(element).children()[0];
  220. results.unshift({
  221. id: null_option.value,
  222. text: null_option.text
  223. });
  224. }
  225. // Check if there are more results to page
  226. var page = data.next !== null;
  227. return {
  228. results: results,
  229. pagination: {
  230. more: page
  231. }
  232. };
  233. }
  234. }
  235. });
  236. // Flatpickr selectors
  237. $('.date-picker').flatpickr({
  238. allowInput: true
  239. });
  240. $('.datetime-picker').flatpickr({
  241. allowInput: true,
  242. enableSeconds: true,
  243. enableTime: true,
  244. time_24hr: true
  245. });
  246. $('.time-picker').flatpickr({
  247. allowInput: true,
  248. enableSeconds: true,
  249. enableTime: true,
  250. noCalendar: true,
  251. time_24hr: true
  252. });
  253. // API backed tags
  254. var tags = $('#id_tags');
  255. if (tags.length > 0 && tags.val().length > 0){
  256. tags = $('#id_tags').val().split(/,\s*/);
  257. } else {
  258. tags = [];
  259. }
  260. tag_objs = $.map(tags, function (tag) {
  261. return {
  262. id: tag,
  263. text: tag,
  264. selected: true
  265. }
  266. });
  267. // Replace the django issued text input with a select element
  268. $('#id_tags').replaceWith('<select name="tags" id="id_tags" class="form-control"></select>');
  269. $('#id_tags').select2({
  270. tags: true,
  271. data: tag_objs,
  272. multiple: true,
  273. allowClear: true,
  274. placeholder: "Tags",
  275. theme: "bootstrap",
  276. width: "off",
  277. ajax: {
  278. delay: 250,
  279. url: netbox_api_path + "extras/tags/",
  280. data: function(params) {
  281. // Paging. Note that `params.page` indexes at 1
  282. var offset = (params.page - 1) * 50 || 0;
  283. var parameters = {
  284. q: params.term,
  285. brief: 1,
  286. limit: 50,
  287. offset: offset,
  288. };
  289. return parameters;
  290. },
  291. processResults: function (data) {
  292. var results = $.map(data.results, function (obj) {
  293. // If tag contains space add double quotes
  294. if (/\s/.test(obj.name))
  295. obj.name = '"' + obj.name + '"'
  296. return {
  297. id: obj.name,
  298. text: obj.name
  299. }
  300. });
  301. // Check if there are more results to page
  302. var page = data.next !== null;
  303. return {
  304. results: results,
  305. pagination: {
  306. more: page
  307. }
  308. };
  309. }
  310. }
  311. });
  312. $('#id_tags').closest('form').submit(function(event){
  313. // django-taggit can only accept a single comma seperated string value
  314. var value = $('#id_tags').val();
  315. if (value.length > 0){
  316. var final_tags = value.join(', ');
  317. $('#id_tags').val(null).trigger('change');
  318. var option = new Option(final_tags, final_tags, true, true);
  319. $('#id_tags').append(option).trigger('change');
  320. }
  321. });
  322. if( $('select#id_mode').length > 0 ) {
  323. $('select#id_mode').on('change', function () {
  324. if ($(this).val() == '') {
  325. $('select#id_untagged_vlan').val();
  326. $('select#id_untagged_vlan').trigger('change');
  327. $('select#id_tagged_vlans').val([]);
  328. $('select#id_tagged_vlans').trigger('change');
  329. $('select#id_untagged_vlan').parent().parent().hide();
  330. $('select#id_tagged_vlans').parent().parent().hide();
  331. }
  332. else if ($(this).val() == 'access') {
  333. $('select#id_tagged_vlans').val([]);
  334. $('select#id_tagged_vlans').trigger('change');
  335. $('select#id_untagged_vlan').parent().parent().show();
  336. $('select#id_tagged_vlans').parent().parent().hide();
  337. }
  338. else if ($(this).val() == 'tagged') {
  339. $('select#id_untagged_vlan').parent().parent().show();
  340. $('select#id_tagged_vlans').parent().parent().show();
  341. }
  342. else if ($(this).val() == 'tagged-all') {
  343. $('select#id_tagged_vlans').val([]);
  344. $('select#id_tagged_vlans').trigger('change');
  345. $('select#id_untagged_vlan').parent().parent().show();
  346. $('select#id_tagged_vlans').parent().parent().hide();
  347. }
  348. });
  349. $('select#id_mode').trigger('change');
  350. }
  351. // Scroll up an offset equal to the first nav element if a hash is present
  352. // Cannot use '#navbar' because it is not always visible, like in small windows
  353. function headerOffsetScroll() {
  354. if (window.location.hash) {
  355. // Short wait needed to allow the page to scroll to the element
  356. setTimeout(function() {
  357. window.scrollBy(0, -$('nav').height())
  358. }, 10);
  359. }
  360. }
  361. // Account for the header height when hash-scrolling
  362. window.addEventListener('load', headerOffsetScroll);
  363. window.addEventListener('hashchange', headerOffsetScroll);
  364. // Offset between the preview window and the window edges
  365. const IMAGE_PREVIEW_OFFSET_X = 20;
  366. const IMAGE_PREVIEW_OFFSET_Y = 10;
  367. // Preview an image attachment when the link is hovered over
  368. $('a.image-preview').on('mouseover', function(e) {
  369. // Twice the offset to account for all sides of the picture
  370. var maxWidth = window.innerWidth - (e.clientX + (IMAGE_PREVIEW_OFFSET_X * 2));
  371. var maxHeight = window.innerHeight - (e.clientY + (IMAGE_PREVIEW_OFFSET_Y * 2));
  372. var img = $('<img>').attr('id', 'image-preview-window').css({
  373. display: 'none',
  374. position: 'absolute',
  375. maxWidth: maxWidth + 'px',
  376. maxHeight: maxHeight + 'px',
  377. left: e.pageX + IMAGE_PREVIEW_OFFSET_X + 'px',
  378. top: e.pageY + IMAGE_PREVIEW_OFFSET_Y + 'px',
  379. boxShadow: '0 0px 12px 3px rgba(0, 0, 0, 0.4)',
  380. });
  381. // Remove any existing preview windows and add the current one
  382. $('#image-preview-window').remove();
  383. $('body').append(img);
  384. // Once loaded, show the preview if the image is indeed an image
  385. img.on('load', function(e) {
  386. if (e.target.complete && e.target.naturalWidth) {
  387. $('#image-preview-window').fadeIn('fast');
  388. }
  389. });
  390. // Begin loading
  391. img.attr('src', e.target.href);
  392. });
  393. // Fade the image out; it will be deleted when another one is previewed
  394. $('a.image-preview').on('mouseout', function() {
  395. $('#image-preview-window').fadeOut('fast');
  396. });
  397. });