PageRenderTime 32ms CodeModel.GetById 5ms RepoModel.GetById 0ms app.codeStats 1ms

/ckan/public/scripts/application.js

https://gitlab.com/ceamso/ckan
JavaScript | 1870 lines | 1489 code | 144 blank | 237 comment | 135 complexity | d6b605c77083547aaebbabeef98968c1 MD5 | raw file
Possible License(s): Apache-2.0

Large files files are truncated, but you can click here to view the full file

  1. var CKAN = CKAN || {};
  2. CKAN.View = CKAN.View || {};
  3. CKAN.Model = CKAN.Model || {};
  4. CKAN.Utils = CKAN.Utils || {};
  5. /* ================================= */
  6. /* == Initialise CKAN Application == */
  7. /* ================================= */
  8. (function ($) {
  9. $(document).ready(function () {
  10. CKAN.Utils.relatedSetup($("#form-add-related"));
  11. CKAN.Utils.setupUserAutocomplete($('input.autocomplete-user'));
  12. CKAN.Utils.setupOrganizationUserAutocomplete($('input.autocomplete-organization-user'));
  13. CKAN.Utils.setupGroupAutocomplete($('input.autocomplete-group'));
  14. CKAN.Utils.setupPackageAutocomplete($('input.autocomplete-dataset'));
  15. CKAN.Utils.setupTagAutocomplete($('input.autocomplete-tag'));
  16. $('input.autocomplete-format').live('keyup', function(){
  17. CKAN.Utils.setupFormatAutocomplete($(this));
  18. });
  19. CKAN.Utils.setupMarkdownEditor($('.markdown-editor'));
  20. // bootstrap collapse
  21. $('.collapse').collapse({toggle: false});
  22. // Buttons with href-action should navigate when clicked
  23. $('input.href-action').click(function(e) {
  24. e.preventDefault();
  25. window.location = ($(e.target).attr('action'));
  26. });
  27. var isGroupView = $('body.group.read').length > 0;
  28. if (isGroupView) {
  29. // Show extract of notes field
  30. CKAN.Utils.setupNotesExtract();
  31. }
  32. var isDatasetView = $('body.package.read').length > 0;
  33. if (isDatasetView) {
  34. // Show extract of notes field
  35. CKAN.Utils.setupNotesExtract();
  36. }
  37. var isResourceView = false; //$('body.package.resource_read').length > 0;
  38. if (isResourceView) {
  39. CKAN.DataPreview.loadPreviewDialog(preload_resource);
  40. }
  41. var isEmbededDataviewer = false;//$('body.package.resource_embedded_dataviewer').length > 0;
  42. if (isEmbededDataviewer) {
  43. CKAN.DataPreview.loadEmbeddedPreview(preload_resource, reclineState);
  44. }
  45. if ($(document.body).hasClass('search')) {
  46. // Calculate the optimal width for the search input regardless of the
  47. // width of the submit button (which can vary depending on translation).
  48. (function resizeSearchInput() {
  49. var form = $('#dataset-search'),
  50. input = form.find('[name=q]'),
  51. button = form.find('[type=submit]'),
  52. offset = parseFloat(button.css('margin-left'));
  53. // Grab the horizontal properties of the input that affect the width.
  54. $.each(['padding-left', 'padding-right', 'border-left-width', 'border-right-width'], function (i, prop) {
  55. offset += parseFloat(input.css(prop)) || 0;
  56. });
  57. input.width(form.outerWidth() - button.outerWidth() - offset);
  58. })();
  59. }
  60. var isDatasetNew = $('body.package.new').length > 0;
  61. if (isDatasetNew) {
  62. // Set up magic URL slug editor
  63. var urlEditor = new CKAN.View.UrlEditor({
  64. slugType: 'package'
  65. });
  66. $('#save').val(CKAN.Strings.addDataset);
  67. $("#title").focus();
  68. }
  69. var isGroupNew = $('body.group.new').length > 0;
  70. if (isGroupNew) {
  71. // Set up magic URL slug editor
  72. var urlEditor = new CKAN.View.UrlEditor({
  73. slugType: 'group'
  74. });
  75. $('#save').val(CKAN.Strings.addGroup);
  76. $("#title").focus();
  77. }
  78. var isDatasetEdit = $('body.package.edit').length > 0;
  79. if (isDatasetEdit) {
  80. CKAN.Utils.warnOnFormChanges($('form#dataset-edit'));
  81. var urlEditor = new CKAN.View.UrlEditor({
  82. slugType: 'package'
  83. });
  84. // Set up dataset delete button
  85. CKAN.Utils.setupDatasetDeleteButton();
  86. }
  87. var isDatasetResourceEdit = $('body.package.editresources').length > 0;
  88. if (isDatasetNew || isDatasetResourceEdit) {
  89. // Selectively enable the upload button
  90. var storageEnabled = $.inArray('storage',CKAN.plugins)>=0;
  91. if (storageEnabled) {
  92. $('li.js-upload-file').show();
  93. }
  94. // Backbone collection class
  95. var CollectionOfResources = Backbone.Collection.extend({model: CKAN.Model.Resource});
  96. // 'resources_json' was embedded into the page
  97. var view = new CKAN.View.ResourceEditor({
  98. collection: new CollectionOfResources(resources_json),
  99. el: $('form#dataset-edit')
  100. });
  101. view.render();
  102. $( ".drag-drop-list" ).sortable({
  103. distance: 10
  104. });
  105. $( ".drag-drop-list" ).disableSelection();
  106. }
  107. var isGroupEdit = $('body.group.edit').length > 0;
  108. if (isGroupEdit) {
  109. var urlEditor = new CKAN.View.UrlEditor({
  110. slugType: 'group'
  111. });
  112. }
  113. // OpenID hack
  114. // We need to remember the language we are using whilst logging in
  115. // we set this in the user session so we don't forget then
  116. // carry on as before.
  117. if (window.openid && openid.signin){
  118. openid._signin = openid.signin;
  119. openid.signin = function (arg) {
  120. $.get(CKAN.SITE_URL + '/user/set_lang/' + CKAN.LANG, function (){openid._signin(arg);})
  121. };
  122. }
  123. if ($('#login').length){
  124. $('#login').submit( function () {
  125. $.ajax(CKAN.SITE_URL + '/user/set_lang/' + CKAN.LANG, {async:false});
  126. });
  127. }
  128. });
  129. }(jQuery));
  130. /* =============================== */
  131. /* jQuery Plugins */
  132. /* =============================== */
  133. jQuery.fn.truncate = function (max, suffix) {
  134. return this.each(function () {
  135. var element = jQuery(this),
  136. isTruncated = element.hasClass('truncated'),
  137. cached, length, text, expand;
  138. if (isTruncated) {
  139. element.html(element.data('truncate:' + (max === 'expand' ? 'original' : 'truncated')));
  140. return;
  141. }
  142. cached = element.text();
  143. length = max || element.data('truncate') || 30;
  144. text = cached.slice(0, length);
  145. expand = jQuery('<a href="#" />').text(suffix || '»');
  146. // Bail early if there is nothing to truncate.
  147. if (cached.length < length) {
  148. return;
  149. }
  150. // Try to truncate to nearest full word.
  151. while ((/\S/).test(text[text.length - 1])) {
  152. text = text.slice(0, text.length - 1);
  153. }
  154. element.html(jQuery.trim(text));
  155. expand.appendTo(element.append(' '));
  156. expand.click(function (event) {
  157. event.preventDefault();
  158. element.html(cached);
  159. });
  160. element.addClass('truncated');
  161. element.data('truncate:original', cached);
  162. element.data('truncate:truncated', element.html());
  163. });
  164. };
  165. /* =============================== */
  166. /* Backbone Model: Resource object */
  167. /* =============================== */
  168. CKAN.Model.Resource = Backbone.Model.extend({
  169. constructor: function Resource() {
  170. Backbone.Model.prototype.constructor.apply(this, arguments);
  171. },
  172. toTemplateJSON: function() {
  173. var obj = Backbone.Model.prototype.toJSON.apply(this, arguments);
  174. obj.displaytitle = obj.description ? obj.description : 'No description ...';
  175. return obj;
  176. }
  177. });
  178. /* ============================== */
  179. /* == Backbone View: UrlEditor == */
  180. /* ============================== */
  181. CKAN.View.UrlEditor = Backbone.View.extend({
  182. initialize: function() {
  183. _.bindAll(this,'titleToSlug','titleChanged','urlChanged','checkSlugIsValid','apiCallback');
  184. // Initial state
  185. var self = this;
  186. this.updateTimer = null;
  187. this.titleInput = $('.js-title');
  188. this.urlInput = $('.js-url-input');
  189. this.validMsg = $('.js-url-is-valid');
  190. this.lengthMsg = $('.url-is-long');
  191. this.lastTitle = "";
  192. this.disableTitleChanged = false;
  193. // Settings
  194. this.regexToHyphen = [ new RegExp('[ .:/_]', 'g'),
  195. new RegExp('[^a-zA-Z0-9-_]', 'g'),
  196. new RegExp('-+', 'g')];
  197. this.regexToDelete = [ new RegExp('^-*', 'g'),
  198. new RegExp('-*$', 'g')];
  199. // Default options
  200. if (!this.options.apiUrl) {
  201. this.options.apiUrl = CKAN.SITE_URL + '/api/2/util/is_slug_valid';
  202. }
  203. if (!this.options.MAX_SLUG_LENGTH) {
  204. this.options.MAX_SLUG_LENGTH = 90;
  205. }
  206. this.originalUrl = this.urlInput.val();
  207. // Hook title changes to the input box
  208. CKAN.Utils.bindInputChanges(this.titleInput, this.titleChanged);
  209. CKAN.Utils.bindInputChanges(this.urlInput, this.urlChanged);
  210. // If you've bothered typing a URL, I won't overwrite you
  211. function disable() {
  212. self.disableTitleChanged = true;
  213. };
  214. this.urlInput.keyup (disable);
  215. this.urlInput.keydown (disable);
  216. this.urlInput.keypress(disable);
  217. // Set up the form
  218. this.urlChanged();
  219. },
  220. titleToSlug: function(title) {
  221. var slug = title;
  222. $.each(this.regexToHyphen, function(idx,regex) { slug = slug.replace(regex, '-'); });
  223. $.each(this.regexToDelete, function(idx,regex) { slug = slug.replace(regex, ''); });
  224. slug = slug.toLowerCase();
  225. if (slug.length<this.options.MAX_SLUG_LENGTH) {
  226. slug=slug.substring(0,this.options.MAX_SLUG_LENGTH);
  227. }
  228. return slug;
  229. },
  230. /* Called when the title changes */
  231. titleChanged: function() {
  232. if (this.disableTitleChanged) { return; }
  233. var title = this.titleInput.val();
  234. if (title == this.lastTitle) { return; }
  235. this.lastTitle = title;
  236. slug = this.titleToSlug(title);
  237. this.urlInput.val(slug);
  238. this.urlInput.change();
  239. },
  240. /* Called when the url is changed */
  241. urlChanged: function() {
  242. var slug = this.urlInput.val();
  243. if (this.updateTimer) { clearTimeout(this.updateTimer); }
  244. if (slug.length<2) {
  245. this.validMsg.html('<span style="font-weight: bold; color: #444;">'+CKAN.Strings.urlIsTooShort+'</span>');
  246. }
  247. else if (slug==this.originalUrl) {
  248. this.validMsg.html('<span style="font-weight: bold; color: #000;">'+CKAN.Strings.urlIsUnchanged+'</span>');
  249. }
  250. else {
  251. this.validMsg.html('<span style="color: #777;">'+CKAN.Strings.checking+'</span>');
  252. var self = this;
  253. this.updateTimer = setTimeout(function () {
  254. self.checkSlugIsValid(slug);
  255. }, 200);
  256. }
  257. if (slug.length>20) {
  258. this.lengthMsg.show();
  259. }
  260. else {
  261. this.lengthMsg.hide();
  262. }
  263. },
  264. checkSlugIsValid: function(slug) {
  265. $.ajax({
  266. url: this.options.apiUrl,
  267. data: 'type='+this.options.slugType+'&slug=' + slug,
  268. dataType: 'jsonp',
  269. type: 'get',
  270. jsonpCallback: 'callback',
  271. success: this.apiCallback
  272. });
  273. },
  274. /* Called when the slug-validator gets back to us */
  275. apiCallback: function(data) {
  276. if (data.valid) {
  277. this.validMsg.html('<span style="font-weight: bold; color: #0c0">'+CKAN.Strings.urlIsAvailable+'</span>');
  278. } else {
  279. this.validMsg.html('<span style="font-weight: bold; color: #c00">'+CKAN.Strings.urlIsNotAvailable+'</span>');
  280. }
  281. }
  282. });
  283. /* =================================== */
  284. /* == Backbone View: ResourceEditor == */
  285. /* =================================== */
  286. CKAN.View.ResourceEditor = Backbone.View.extend({
  287. initialize: function() {
  288. // Init bindings
  289. _.bindAll(this, 'resourceAdded', 'resourceRemoved', 'sortStop', 'openFirstPanel', 'closePanel', 'openAddPanel');
  290. this.collection.bind('add', this.resourceAdded);
  291. this.collection.bind('remove', this.resourceRemoved);
  292. this.collection.each(this.resourceAdded);
  293. this.el.find('.resource-list-edit').bind("sortstop", this.sortStop);
  294. // Delete the barebones editor. We will populate our own form.
  295. this.el.find('.js-resource-edit-barebones').remove();
  296. // Warn on form changes
  297. var flashWarning = CKAN.Utils.warnOnFormChanges(this.el);
  298. this.collection.bind('add', flashWarning);
  299. this.collection.bind('remove', flashWarning);
  300. // Trigger the Add Resource pane
  301. this.el.find('.js-resource-add').click(this.openAddPanel);
  302. // Tabs for adding resources
  303. new CKAN.View.ResourceAddUrl({
  304. collection: this.collection,
  305. el: this.el.find('.js-add-url-form'),
  306. mode: 'file'
  307. });
  308. new CKAN.View.ResourceAddUrl({
  309. collection: this.collection,
  310. el: this.el.find('.js-add-api-form'),
  311. mode: 'api'
  312. });
  313. new CKAN.View.ResourceAddUpload({
  314. collection: this.collection,
  315. el: this.el.find('.js-add-upload-form')
  316. });
  317. // Close details button
  318. this.el.find('.resource-panel-close').click(this.closePanel);
  319. // Did we embed some form errors?
  320. if (typeof global_form_errors == 'object') {
  321. if (global_form_errors.resources) {
  322. var openedOne = false;
  323. for (i in global_form_errors.resources) {
  324. var resource_errors = global_form_errors.resources[i];
  325. if (CKAN.Utils.countObject(resource_errors) > 0) {
  326. var resource = this.collection.at(i);
  327. resource.view.setErrors(resource_errors);
  328. if (!openedOne) {
  329. resource.view.openMyPanel();
  330. openedOne = true;
  331. }
  332. }
  333. }
  334. }
  335. }
  336. else {
  337. // Initial state
  338. this.openFirstPanel();
  339. }
  340. },
  341. /*
  342. * Called when the page loads or the current resource is deleted.
  343. * Reset page state to the first available edit panel.
  344. */
  345. openFirstPanel: function() {
  346. if (this.collection.length>0) {
  347. this.collection.at(0).view.openMyPanel();
  348. }
  349. else {
  350. this.openAddPanel();
  351. }
  352. },
  353. /*
  354. * Open the 'Add New Resource' special-case panel on the right.
  355. */
  356. openAddPanel: function(e) {
  357. if (e) { e.preventDefault(); }
  358. var panel = this.el.find('.resource-panel');
  359. var addLi = this.el.find('.resource-list-add > li');
  360. this.el.find('.resource-list > li').removeClass('active');
  361. $('.resource-details').hide();
  362. this.el.find('.resource-details.resource-add').show();
  363. addLi.addClass('active');
  364. panel.show();
  365. },
  366. /*
  367. * Close the panel on the right.
  368. */
  369. closePanel: function(e) {
  370. if (e) { e.preventDefault(); }
  371. this.el.find('.resource-list > li').removeClass('active');
  372. this.el.find('.resource-panel').hide();
  373. },
  374. /*
  375. * Update the resource__N__field names to match
  376. * new sort order.
  377. */
  378. sortStop: function(e,ui) {
  379. this.collection.each(function(resource) {
  380. // Ask the DOM for the new sort order
  381. var index = resource.view.li.index();
  382. resource.view.options.position = index;
  383. // Update the form element names
  384. var table = resource.view.table;
  385. $.each(table.find('input,textarea,select'), function(input_index, input) {
  386. var name = $(input).attr('name');
  387. if (name) {
  388. name = name.replace(/(resources__)\d+(.*)/, '$1'+index+'$2');
  389. $(input).attr('name',name);
  390. }
  391. });
  392. });
  393. },
  394. /*
  395. * Calculate id of the next resource to create
  396. */
  397. nextIndex: function() {
  398. var maxId=-1;
  399. var root = this.el.find('.resource-panel');
  400. root.find('input').each(function(idx,input) {
  401. var name = $(input).attr('name') || '';
  402. var splitName=name.split('__');
  403. if (splitName.length>1) {
  404. var myId = parseInt(splitName[1],10);
  405. maxId = Math.max(myId, maxId);
  406. }
  407. });
  408. return maxId+1;
  409. },
  410. /*
  411. * Create DOM elements for new resource.
  412. */
  413. resourceAdded: function(resource) {
  414. var self = this;
  415. resource.view = new CKAN.View.Resource({
  416. position: this.nextIndex(),
  417. model: resource,
  418. callback_deleteMe: function() { self.collection.remove(resource); }
  419. });
  420. this.el.find('.resource-list-edit').append(resource.view.li);
  421. this.el.find('.resource-panel').append(resource.view.table);
  422. if (resource.isNew()) {
  423. resource.view.openMyPanel();
  424. }
  425. },
  426. /*
  427. * Destroy DOM elements for deleted resource.
  428. */
  429. resourceRemoved: function(resource) {
  430. resource.view.removeFromDom();
  431. delete resource.view;
  432. this.openFirstPanel();
  433. }
  434. });
  435. /* ============================== */
  436. /* == Backbone View: Resource == */
  437. /* ============================== */
  438. CKAN.View.Resource = Backbone.View.extend({
  439. initialize: function() {
  440. this.el = $(this.el);
  441. _.bindAll(this,'updateName','updateIcon','name','askToDelete','openMyPanel','setErrors','setupDynamicExtras','addDynamicExtra' );
  442. this.render();
  443. },
  444. render: function() {
  445. this.raw_resource = this.model.toTemplateJSON();
  446. var resource_object = {
  447. resource: this.raw_resource,
  448. num: this.options.position,
  449. resource_icon: '/images/icons/page_white.png',
  450. resourceTypeOptions: [
  451. ['file', CKAN.Strings.dataFile],
  452. ['api', CKAN.Strings.api],
  453. ['visualization', CKAN.Strings.visualization],
  454. ['image', CKAN.Strings.image],
  455. ['metadata', CKAN.Strings.metadata],
  456. ['documentation', CKAN.Strings.documentation],
  457. ['code', CKAN.Strings.code],
  458. ['example', CKAN.Strings.example]
  459. ]
  460. };
  461. // Generate DOM elements
  462. this.li = $($.tmpl(CKAN.Templates.resourceEntry, resource_object));
  463. this.table = $($.tmpl(CKAN.Templates.resourceDetails, resource_object));
  464. // Hook to changes in name
  465. this.nameBox = this.table.find('input.js-resource-edit-name');
  466. this.descriptionBox = this.table.find('textarea.js-resource-edit-description');
  467. CKAN.Utils.bindInputChanges(this.nameBox,this.updateName);
  468. CKAN.Utils.bindInputChanges(this.descriptionBox,this.updateName);
  469. // Hook to changes in format
  470. this.formatBox = this.table.find('input.js-resource-edit-format');
  471. CKAN.Utils.bindInputChanges(this.formatBox,this.updateIcon);
  472. // Hook to open panel link
  473. this.li.find('.resource-open-my-panel').click(this.openMyPanel);
  474. this.table.find('.js-resource-edit-delete').click(this.askToDelete);
  475. // Hook to markdown editor
  476. CKAN.Utils.setupMarkdownEditor(this.table.find('.markdown-editor'));
  477. // Set initial state
  478. this.updateName();
  479. this.updateIcon();
  480. this.setupDynamicExtras();
  481. this.hasErrors = false;
  482. },
  483. /*
  484. * Process a JSON object of errors attached to this resource
  485. */
  486. setErrors: function(obj) {
  487. if (CKAN.Utils.countObject(obj) > 0) {
  488. this.hasErrors = true;
  489. this.errors = obj;
  490. this.li.addClass('hasErrors');
  491. var errorList = $('<dl/>').addClass('errorList');
  492. $.each(obj,function(k,v) {
  493. var errorText = '';
  494. var newLine = false;
  495. $.each(v,function(index,value) {
  496. if (newLine) { errorText += '<br/>'; }
  497. errorText += value;
  498. newLine = true;
  499. });
  500. errorList.append($('<dt/>').html(k));
  501. errorList.append($('<dd/>').html(errorText));
  502. });
  503. this.table.find('.resource-errors').append(errorList).show();
  504. }
  505. },
  506. /*
  507. * Work out what I should be called. Rough-match
  508. * of helpers.py:resource_display_name.
  509. */
  510. name: function() {
  511. var name = this.nameBox.val();
  512. if (!name) {
  513. name = this.descriptionBox.val();
  514. if (!name) {
  515. if (this.model.isNew()) {
  516. name = '<em>[new resource]</em>';
  517. }
  518. else {
  519. name = '[no name] ' + this.model.id;
  520. }
  521. }
  522. }
  523. if (name.length>45) {
  524. name = name.substring(0,45)+'...';
  525. }
  526. return name;
  527. },
  528. /*
  529. * Called when the user types to update the name in
  530. * my <li> to match the <input> values.
  531. */
  532. updateName: function() {
  533. // Need to structurally modify the DOM to force a re-render of text
  534. var $link = this.li.find('.js-resource-edit-name');
  535. $link.html('<span>'+this.name()+'</span>');
  536. },
  537. /*
  538. * Called when the user types to update the icon <img>
  539. * tags. Uses server API to select icon.
  540. */
  541. updateIcon: function() {
  542. var self = this;
  543. if (self.updateIconTimer) {
  544. clearTimeout(self.updateIconTimer);
  545. }
  546. self.updateIconTimer = setTimeout(function() {
  547. // AJAX to server API
  548. $.getJSON(CKAN.SITE_URL + '/api/2/util/resource/format_icon?format='+encodeURIComponent(self.formatBox.val()), function(data) {
  549. if (data && data.icon && data.format==self.formatBox.val()) {
  550. self.li.find('.js-resource-icon').attr('src',data.icon);
  551. self.table.find('.js-resource-icon').attr('src',data.icon);
  552. }
  553. });
  554. delete self.updateIconTimer;
  555. },
  556. 100);
  557. },
  558. /*
  559. * Closes all other panels on the right and opens my editor panel.
  560. */
  561. openMyPanel: function(e) {
  562. if (e) { e.preventDefault(); }
  563. // Close all tables
  564. var panel = this.table.parents('.resource-panel');
  565. panel.find('.resource-details').hide();
  566. this.li.parents('fieldset#resources').find('.resource-list > li').removeClass('active');
  567. panel.show();
  568. this.table.show();
  569. this.table.find('.js-resource-edit-name').focus();
  570. this.li.addClass('active');
  571. },
  572. /*
  573. * Called when my delete button is clicked. Calls back to the parent
  574. * resource editor.
  575. */
  576. askToDelete: function(e) {
  577. e.preventDefault();
  578. var confirmMessage = CKAN.Strings.deleteThisResourceQuestion.replace('%name%', this.name());
  579. if (confirm(confirmMessage)) {
  580. this.options.callback_deleteMe();
  581. }
  582. },
  583. /*
  584. * Set up the dynamic-extras section of the table.
  585. */
  586. setupDynamicExtras: function() {
  587. var self = this;
  588. $.each(this.raw_resource, function(key,value) {
  589. // Skip the known keys
  590. if (self.reservedWord(key)) { return; }
  591. self.addDynamicExtra(key,value);
  592. });
  593. this.table.find('.add-resource-extra').click(function(e) {
  594. e.preventDefault();
  595. self.addDynamicExtra('','');
  596. });
  597. },
  598. addDynamicExtra: function(key,value) {
  599. // Create elements
  600. var dynamicExtra = $($.tmpl(CKAN.Templates.resourceExtra, {
  601. num: this.options.position,
  602. key: key,
  603. value: value}));
  604. this.table.find('.dynamic-extras').append(dynamicExtra);
  605. // Captured values
  606. var inputKey = dynamicExtra.find('.extra-key');
  607. var inputValue = dynamicExtra.find('.extra-value');
  608. // Callback function
  609. var self = this;
  610. var setExtraName = function() {
  611. var _key = inputKey.val();
  612. var key = $.trim(_key).replace(/\s+/g,'');
  613. // Don't allow you to create an extra called mimetype (etc)
  614. if (self.reservedWord(key)) { key=''; }
  615. // Set or unset the field's name
  616. if (key.length) {
  617. var newName = 'resources__'+self.options.position+'__'+key;
  618. inputValue.attr('name',newName);
  619. inputValue.removeClass('strikethrough');
  620. }
  621. else {
  622. inputValue.removeAttr('name');
  623. inputValue.addClass('strikethrough');
  624. }
  625. };
  626. // Callback function
  627. var clickRemove = function(e) {
  628. e.preventDefault();
  629. dynamicExtra.remove();
  630. };
  631. // Init with bindings
  632. CKAN.Utils.bindInputChanges(dynamicExtra.find('.extra-key'), setExtraName);
  633. dynamicExtra.find('.remove-resource-extra').click(clickRemove);
  634. setExtraName();
  635. },
  636. reservedWord: function(word) {
  637. return word=='cache_last_updated' ||
  638. word=='cache_url' ||
  639. word=='dataset' ||
  640. word=='description' ||
  641. word=='displaytitle' ||
  642. word=='extras' ||
  643. word=='format' ||
  644. word=='hash' ||
  645. word=='id' ||
  646. word=='created' ||
  647. word=='last_modified' ||
  648. word=='mimetype' ||
  649. word=='mimetype_inner' ||
  650. word=='name' ||
  651. word=='package_id' ||
  652. word=='position' ||
  653. word=='resource_group_id' ||
  654. word=='resource_type' ||
  655. word=='revision_id' ||
  656. word=='revision_timestamp' ||
  657. word=='size' ||
  658. word=='size_extra' ||
  659. word=='state' ||
  660. word=='url' ||
  661. word=='webstore_last_updated' ||
  662. word=='webstore_url';
  663. },
  664. /*
  665. * Called when my model is destroyed. Remove me from the page.
  666. */
  667. removeFromDom: function() {
  668. this.li.remove();
  669. this.table.remove();
  670. }
  671. });
  672. /* ============================================= */
  673. /* Backbone View: Add Resource by uploading file */
  674. /* ============================================= */
  675. CKAN.View.ResourceAddUpload = Backbone.View.extend({
  676. tagName: 'div',
  677. initialize: function(options) {
  678. this.el = $(this.el);
  679. _.bindAll(this, 'render', 'updateFormData', 'setMessage', 'uploadFile');
  680. $(CKAN.Templates.resourceUpload).appendTo(this.el);
  681. this.$messages = this.el.find('.alert');
  682. this.setupFileUpload();
  683. },
  684. events: {
  685. 'click input[type="submit"]': 'uploadFile'
  686. },
  687. setupFileUpload: function() {
  688. var self = this;
  689. this.el.find('.fileupload').fileupload({
  690. // needed because we are posting to remote url
  691. forceIframeTransport: true,
  692. replaceFileInput: false,
  693. autoUpload: false,
  694. fail: function(e, data) {
  695. alert('Upload failed');
  696. },
  697. add: function(e,data) {
  698. self.fileData = data;
  699. self.fileUploadData = data;
  700. self.key = self.makeUploadKey(data.files[0].name);
  701. self.updateFormData(self.key);
  702. },
  703. send: function(e, data) {
  704. self.setMessage(CKAN.Strings.uploadingFile +' <img src="http://assets.okfn.org/images/icons/ajaxload-circle.gif" class="spinner" />');
  705. },
  706. done: function(e, data) {
  707. self.onUploadComplete(self.key);
  708. }
  709. })
  710. },
  711. ISODateString: function(d) {
  712. function pad(n) {return n<10 ? '0'+n : n};
  713. return d.getUTCFullYear()+'-'
  714. + pad(d.getUTCMonth()+1)+'-'
  715. + pad(d.getUTCDate())+'T'
  716. + pad(d.getUTCHours())+':'
  717. + pad(d.getUTCMinutes())+':'
  718. + pad(d.getUTCSeconds());
  719. },
  720. // Create an upload key/label for this file.
  721. //
  722. // Form: {current-date}/file-name. Do not just use the file name as this
  723. // would lead to collisions.
  724. // (Could add userid/username and/or a small random string to reduce
  725. // collisions but chances seem very low already)
  726. makeUploadKey: function(fileName) {
  727. // google storage replaces ' ' with '+' which breaks things
  728. // See http://trac.ckan.org/ticket/1518 for more.
  729. var corrected = fileName.replace(/ /g, '-');
  730. // note that we put hh mm ss as hhmmss rather hh:mm:ss (former is 'basic
  731. // format')
  732. var now = new Date();
  733. // replace ':' with nothing
  734. var str = this.ISODateString(now).replace(':', '').replace(':', '');
  735. return str + '/' + corrected;
  736. },
  737. updateFormData: function(key) {
  738. var self = this;
  739. self.setMessage(CKAN.Strings.checkingUploadPermissions + ' <img src="http://assets.okfn.org/images/icons/ajaxload-circle.gif" class="spinner" />');
  740. self.el.find('.fileinfo').text(key);
  741. $.ajax({
  742. url: CKAN.SITE_URL + '/api/storage/auth/form/' + key,
  743. async: false,
  744. success: function(data) {
  745. self.el.find('form').attr('action', data.action);
  746. _tmpl = '<input type="hidden" name="${name}" value="${value}" />';
  747. var $hidden = $(self.el.find('form div.hidden-inputs')[0]);
  748. $.each(data.fields, function(idx, item) {
  749. $hidden.append($.tmpl(_tmpl, item));
  750. });
  751. self.hideMessage();
  752. },
  753. error: function(jqXHR, textStatus, errorThrown) {
  754. // TODO: more graceful error handling (e.g. of 409)
  755. self.setMessage(CKAN.Strings.failedToGetCredentialsForUpload, 'error');
  756. self.el.find('input[name="add-resource-upload"]').hide();
  757. }
  758. });
  759. },
  760. uploadFile: function(e) {
  761. e.preventDefault();
  762. if (!this.fileData) {
  763. alert('No file selected');
  764. return;
  765. }
  766. var jqXHR = this.fileData.submit();
  767. },
  768. onUploadComplete: function(key) {
  769. var self = this;
  770. $.ajax({
  771. url: CKAN.SITE_URL + '/api/storage/metadata/' + self.key,
  772. success: function(data) {
  773. var name = data._label;
  774. if (name && name.length > 0 && name[0] === '/') {
  775. name = name.slice(1);
  776. }
  777. var d = new Date(data._last_modified);
  778. var lastmod = self.ISODateString(d);
  779. var newResource = new CKAN.Model.Resource({});
  780. newResource.set({
  781. url: data._location
  782. , name: name
  783. , size: data._content_length
  784. , last_modified: lastmod
  785. , format: data._format
  786. , mimetype: data._format
  787. , resource_type: 'file.upload'
  788. , owner: data['uploaded-by']
  789. , hash: data._checksum
  790. , cache_url: data._location
  791. , cache_url_updated: lastmod
  792. }
  793. , {
  794. error: function(model, error) {
  795. var msg = 'Filed uploaded OK but error adding resource: ' + error + '.';
  796. msg += 'You may need to create a resource directly. Uploaded file at: ' + data._location;
  797. self.setMessage(msg, 'error');
  798. }
  799. }
  800. );
  801. self.collection.add(newResource);
  802. self.setMessage('File uploaded OK and resource added', 'success');
  803. }
  804. });
  805. },
  806. setMessage: function(msg, category) {
  807. var category = category || 'alert-info';
  808. this.$messages.removeClass('alert-info alert-success alert-error');
  809. this.$messages.addClass(category);
  810. this.$messages.show();
  811. this.$messages.html(msg);
  812. },
  813. hideMessage: function() {
  814. this.$messages.hide('slow');
  815. }
  816. });
  817. /* ======================================== */
  818. /* == Backbone View: Add resource by URL == */
  819. /* ======================================== */
  820. CKAN.View.ResourceAddUrl = Backbone.View.extend({
  821. initialize: function(options) {
  822. _.bindAll(this, 'clickSubmit');
  823. },
  824. clickSubmit: function(e) {
  825. e.preventDefault();
  826. var self = this;
  827. var newResource = new CKAN.Model.Resource({});
  828. this.el.find('input[name="add-resource-save"]').addClass("disabled");
  829. var urlVal = this.el.find('input[name="add-resource-url"]').val();
  830. var qaEnabled = $.inArray('qa',CKAN.plugins)>=0;
  831. if(qaEnabled && this.options.mode=='file') {
  832. $.ajax({
  833. url: CKAN.SITE_URL + '/qa/link_checker',
  834. context: newResource,
  835. data: {url: urlVal},
  836. dataType: 'json',
  837. error: function(){
  838. newResource.set({url: urlVal, resource_type: 'file'});
  839. self.collection.add(newResource);
  840. self.resetForm();
  841. },
  842. success: function(data){
  843. data = data[0];
  844. newResource.set({
  845. url: urlVal,
  846. resource_type: 'file',
  847. format: data.format,
  848. size: data.size,
  849. mimetype: data.mimetype,
  850. last_modified: data.last_modified,
  851. url_error: (data.url_errors || [""])[0]
  852. });
  853. self.collection.add(newResource);
  854. self.resetForm();
  855. }
  856. });
  857. }
  858. else {
  859. newResource.set({url: urlVal, resource_type: this.options.mode});
  860. this.collection.add(newResource);
  861. this.resetForm();
  862. }
  863. },
  864. resetForm: function() {
  865. this.el.find('input[name="add-resource-save"]').removeClass("disabled");
  866. this.el.find('input[name="add-resource-url"]').val('');
  867. },
  868. events: {
  869. 'click .btn': 'clickSubmit'
  870. }
  871. });
  872. /* ================ */
  873. /* == CKAN.Utils == */
  874. /* ================ */
  875. CKAN.Utils = function($, my) {
  876. // Animate the appearance of an element by expanding its height
  877. my.animateHeight = function(element, animTime) {
  878. if (!animTime) { animTime = 350; }
  879. element.show();
  880. var finalHeight = element.height();
  881. element.height(0);
  882. element.animate({height:finalHeight}, animTime);
  883. };
  884. my.bindInputChanges = function(input, callback) {
  885. input.keyup(callback);
  886. input.keydown(callback);
  887. input.keypress(callback);
  888. input.change(callback);
  889. };
  890. my.setupDatasetDeleteButton = function() {
  891. var select = $('select.dataset-delete');
  892. select.attr('disabled','disabled');
  893. select.css({opacity: 0.3});
  894. $('button.dataset-delete').click(function(e) {
  895. select.removeAttr('disabled');
  896. select.fadeTo('fast',1.0);
  897. $(e.target).css({opacity:0});
  898. $(e.target).attr('disabled','disabled');
  899. return false;
  900. });
  901. };
  902. // Attach dataset autocompletion to provided elements
  903. //
  904. // Requires: jquery-ui autocomplete
  905. my.setupPackageAutocomplete = function(elements) {
  906. elements.autocomplete({
  907. minLength: 0,
  908. source: function(request, callback) {
  909. var url = '/dataset/autocomplete?q=' + request.term;
  910. $.ajax({
  911. url: url,
  912. success: function(data) {
  913. // atm is a string with items broken by \n and item = title (name)|name
  914. var out = [];
  915. var items = data.split('\n');
  916. $.each(items, function(idx, value) {
  917. var _tmp = value.split('|');
  918. var _newItem = {
  919. label: _tmp[0],
  920. value: _tmp[1]
  921. };
  922. out.push(_newItem);
  923. });
  924. callback(out);
  925. }
  926. });
  927. }
  928. , select: function(event, ui) {
  929. var input_box = $(this);
  930. input_box.val('');
  931. var old_name = input_box.attr('name');
  932. var field_name_regex = /^(\S+)__(\d+)__(\S+)$/;
  933. var split = old_name.match(field_name_regex);
  934. var new_name = split[1] + '__' + (parseInt(split[2],10) + 1) + '__' + split[3];
  935. input_box.attr('name', new_name);
  936. input_box.attr('id', new_name);
  937. var $new = $('<div class="ckan-dataset-to-add"><p></p></div>');
  938. $new.append($('<input type="hidden" />').attr('name', old_name).val(ui.item.value));
  939. $new.append('<i class="icon-plus-sign"></i> ');
  940. $new.append(ui.item.label);
  941. input_box.after($new);
  942. // prevent setting value in autocomplete box
  943. return false;
  944. }
  945. });
  946. };
  947. // Attach tag autocompletion to provided elements
  948. //
  949. // Requires: jquery-ui autocomplete
  950. my.setupTagAutocomplete = function(elements) {
  951. // don't navigate away from the field on tab when selecting an item
  952. elements.bind( "keydown",
  953. function( event ) {
  954. if ( event.keyCode === $.ui.keyCode.TAB && $( this ).data( "autocomplete" ).menu.active ) {
  955. event.preventDefault();
  956. }
  957. }
  958. ).autocomplete({
  959. minLength: 1,
  960. source: function(request, callback) {
  961. // here request.term is whole list of tags so need to get last
  962. var _realTerm = $.trim(request.term.split(',').pop());
  963. var url = CKAN.SITE_URL + '/api/2/util/tag/autocomplete?incomplete=' + _realTerm;
  964. $.getJSON(url, function(data) {
  965. // data = { ResultSet: { Result: [ {Name: tag} ] } } (Why oh why?)
  966. var tags = $.map(data.ResultSet.Result, function(value, idx) {
  967. return value.Name;
  968. });
  969. callback( $.ui.autocomplete.filter(tags, _realTerm) );
  970. });
  971. },
  972. focus: function() {
  973. // prevent value inserted on focus
  974. return false;
  975. },
  976. select: function( event, ui ) {
  977. var terms = this.value.split(',');
  978. // remove the current input
  979. terms.pop();
  980. // add the selected item
  981. terms.push( " "+ui.item.value );
  982. // add placeholder to get the comma-and-space at the end
  983. terms.push( " " );
  984. this.value = terms.join( "," );
  985. return false;
  986. }
  987. });
  988. };
  989. // Attach tag autocompletion to provided elements
  990. //
  991. // Requires: jquery-ui autocomplete
  992. my.setupFormatAutocomplete = function(elements) {
  993. elements.autocomplete({
  994. minLength: 1,
  995. source: function(request, callback) {
  996. var url = CKAN.SITE_URL + '/api/2/util/resource/format_autocomplete?incomplete=' + request.term;
  997. $.getJSON(url, function(data) {
  998. // data = { ResultSet: { Result: [ {Name: tag} ] } } (Why oh why?)
  999. var formats = $.map(data.ResultSet.Result, function(value, idx) {
  1000. return value.Format;
  1001. });
  1002. callback(formats);
  1003. });
  1004. }
  1005. });
  1006. };
  1007. my.setupOrganizationUserAutocomplete = function(elements) {
  1008. elements.autocomplete({
  1009. minLength: 2,
  1010. source: function(request, callback) {
  1011. var url = '/api/2/util/user/autocomplete?q=' + request.term;
  1012. $.getJSON(url, function(data) {
  1013. $.each(data, function(idx, userobj) {
  1014. var label = userobj.name;
  1015. if (userobj.fullname) {
  1016. label += ' [' + userobj.fullname + ']';
  1017. }
  1018. userobj.label = label;
  1019. userobj.value = userobj.name;
  1020. });
  1021. callback(data);
  1022. });
  1023. },
  1024. select: function(event, ui) {
  1025. var input_box = $(this);
  1026. input_box.val('');
  1027. var parent_dd = input_box.parent('dd');
  1028. var old_name = input_box.attr('name');
  1029. var field_name_regex = /^(\S+)__(\d+)__(\S+)$/;
  1030. var split = old_name.match(field_name_regex);
  1031. var new_name = split[1] + '__' + (parseInt(split[2],10) + 1) + '__' + split[3];
  1032. input_box.attr('name', new_name);
  1033. input_box.attr('id', new_name);
  1034. parent_dd.before(
  1035. '<input type="hidden" name="' + old_name + '" value="' + ui.item.value + '">' +
  1036. '<input type="hidden" name="' + old_name.replace('__name','__capacity') + '" value="editor">' +
  1037. '<dd>' + ui.item.label + '</dd>'
  1038. );
  1039. return false; // to cancel the event ;)
  1040. }
  1041. });
  1042. };
  1043. // Attach user autocompletion to provided elements
  1044. //
  1045. // Requires: jquery-ui autocomplete
  1046. my.setupUserAutocomplete = function(elements) {
  1047. elements.autocomplete({
  1048. minLength: 2,
  1049. source: function(request, callback) {
  1050. var url = CKAN.SITE_URL + '/api/2/util/user/autocomplete?q=' + request.term;
  1051. $.getJSON(url, function(data) {
  1052. $.each(data, function(idx, userobj) {
  1053. var label = userobj.name;
  1054. if (userobj.fullname) {
  1055. label += ' [' + userobj.fullname + ']';
  1056. }
  1057. userobj.label = label;
  1058. userobj.value = userobj.name;
  1059. });
  1060. callback(data);
  1061. });
  1062. }
  1063. });
  1064. };
  1065. my.relatedSetup = function(form) {
  1066. $('[rel=popover]').popover();
  1067. function addAlert(msg) {
  1068. $('<div class="alert alert-error" />').html(msg).hide().prependTo(form).fadeIn();
  1069. }
  1070. function relatedRequest(action, method, data) {
  1071. return $.ajax({
  1072. type: method,
  1073. dataType: 'json',
  1074. contentType: 'application/json',
  1075. url: CKAN.SITE_URL + '/api/3/action/related_' + action,
  1076. data: data ? JSON.stringify(data) : undefined,
  1077. error: function(err, txt, w) {
  1078. // This needs to be far more informative.
  1079. addAlert('<strong>Error:</strong> Unable to ' + action + ' related item');
  1080. }
  1081. });
  1082. }
  1083. // Center thumbnails vertically.
  1084. var relatedItems = $('.related-items');
  1085. relatedItems.find('li').each(function () {
  1086. var item = $(this), description = item.find('.description');
  1087. function vertiallyAlign() {
  1088. var img = $(this),
  1089. height = img.height(),
  1090. parent = img.parent().height(),
  1091. top = (height - parent) / 2;
  1092. if (parent < height) {
  1093. img.css('margin-top', -top);
  1094. }
  1095. }
  1096. item.find('img').load(vertiallyAlign);
  1097. description.data('height', description.height()).truncate();
  1098. });
  1099. relatedItems.on('mouseenter mouseleave', '.description.truncated', function (event) {
  1100. var isEnter = event.type === 'mouseenter'
  1101. description = $(this)
  1102. timer = description.data('hover-intent');
  1103. function update() {
  1104. var parent = description.parents('li:first'),
  1105. difference = description.data('height') - description.height();
  1106. description.truncate(isEnter ? 'expand' : 'collapse');
  1107. parent.toggleClass('expanded-description', isEnter);
  1108. // Adjust the bottom margin of the item relative to it's current value
  1109. // to allow the description to expand without breaking the grid.
  1110. parent.css('margin-bottom', isEnter ? '-=' + difference + 'px' : '');
  1111. description.removeData('hover-intent');
  1112. }
  1113. if (!isEnter && timer) {
  1114. // User has moused out in the time set so cancel the action.
  1115. description.removeData('hover-intent');
  1116. return clearTimeout(timer);
  1117. } else if (!isEnter && !timer) {
  1118. update();
  1119. } else {
  1120. // Delay the hover action slightly to wait to see if the user mouses
  1121. // out again. This prevents unwanted actions.
  1122. description.data('hover-intent', setTimeout(update, 200));
  1123. }
  1124. });
  1125. // Add a handler for the delete buttons.
  1126. relatedItems.on('click', '[data-action=delete]', function (event) {
  1127. var id = $(this).data('relatedId');
  1128. relatedRequest('delete', 'POST', {id: id}).done(function () {
  1129. $('#related-item-' + id).remove();
  1130. });
  1131. event.preventDefault();
  1132. });
  1133. $(form).submit(function (event) {
  1134. event.preventDefault();
  1135. // Validate the form
  1136. var form = $(this), data = {};
  1137. jQuery.each(form.serializeArray(), function () {
  1138. data[this.name] = this.value;
  1139. });
  1140. form.find('.alert').remove();
  1141. form.find('.error').removeClass('error');
  1142. if (!data.title) {
  1143. addAlert('<strong>Missing field:</strong> A title is required');
  1144. $('[name=title]').parent().addClass('error');
  1145. return;
  1146. }
  1147. if (!data.url) {
  1148. addAlert('<strong>Missing field:</strong> A url is required');
  1149. $('[name=url]').parent().addClass('error');
  1150. return;
  1151. }
  1152. relatedRequest('create', this.method, data).done(function () {
  1153. // TODO: Insert item dynamically.
  1154. window.location.reload();
  1155. });
  1156. });
  1157. };
  1158. my.setupGroupAutocomplete = function(elements) {
  1159. elements.autocomplete({
  1160. minLength: 2,
  1161. source: function(request, callback) {
  1162. var url = CKAN.SITE_URL + '/api/2/util/group/autocomplete?q=' + request.term;
  1163. $.getJSON(url, function(data) {
  1164. $.each(data, function(idx, userobj) {
  1165. var label = userobj.name;
  1166. userobj.label = label;
  1167. userobj.value = userobj.name;
  1168. });
  1169. callback(data);
  1170. });
  1171. }
  1172. });
  1173. };
  1174. my.setupMarkdownEditor = function(markdownEditor) {
  1175. // Markdown editor hooks
  1176. markdownEditor.find('button, div.markdown-preview').live('click', function(e) {
  1177. e.preventDefault();
  1178. var $target = $(e.target);
  1179. // Extract neighbouring elements
  1180. var markdownEditor=$target.closest('.markdown-editor');
  1181. markdownEditor.find('button').removeClass('depressed');
  1182. var textarea = markdownEditor.find('.markdown-input');
  1183. var preview = markdownEditor.find('.markdown-preview');
  1184. // Toggle the preview
  1185. if ($target.is('.js-markdown-preview')) {
  1186. $target.addClass('depressed');
  1187. raw_markdown=textarea.val();
  1188. preview.html("<em>"+CKAN.Strings.loading+"<em>");
  1189. $.post(CKAN.SITE_URL + "/api/util/markdown", { q: raw_markdown },
  1190. function(data) { preview.html(data); }
  1191. );
  1192. preview.width(textarea.width());
  1193. preview.height(textarea.height());
  1194. textarea.hide();
  1195. preview.show();
  1196. } else {
  1197. markdownEditor.find('.js-markdown-edit').addClass('depressed');
  1198. textarea.show();
  1199. preview.hide();
  1200. textarea.focus();
  1201. }
  1202. return false;
  1203. });
  1204. };
  1205. // If notes field is more than 1 paragraph, just show the
  1206. // first paragraph with a 'Read more' link that will expand
  1207. // the div if clicked
  1208. my.setupNotesExtract = function() {
  1209. var notes = $('#content div.notes');
  1210. var paragraphs = notes.find('#notes-extract > *');
  1211. if (paragraphs.length===0) {
  1212. notes.hide();
  1213. }
  1214. else if (paragraphs.length > 1) {
  1215. var remainder = notes.find('#notes-remainder');
  1216. $.each(paragraphs,function(i,para) {
  1217. if (i > 0) { remainder.append($(para).remove()); }
  1218. });
  1219. var finalHeight = remainder.height();
  1220. remainder.height(0);
  1221. notes.find('#notes-toggle').show();
  1222. notes.find('#notes-toggle button').click(
  1223. function(event){
  1224. notes.find('#notes-toggle button').toggle();
  1225. if ($(event.target).hasClass('more')) {
  1226. remainder.animate({'height':finalHeight});
  1227. }
  1228. else {
  1229. remainder.animate({'height':0});
  1230. }
  1231. return false;
  1232. }
  1233. );
  1234. }
  1235. };
  1236. my.warnOnFormChanges = function() {
  1237. var boundToUnload = false;
  1238. return function($form) {
  1239. var flashWarning = function() {
  1240. if (boundToUnload) { return; }
  1241. boundToUnload = true;
  1242. // Bind to the window departure event
  1243. window.onbeforeunload = function () {
  1244. return CKAN.Strings.youHaveUnsavedChanges;
  1245. };
  1246. };
  1247. // Hook form modifications to flashWarning
  1248. $form.find('input,select').live('change', function(e) {
  1249. $target = $(e.target);
  1250. // Entering text in the 'add' box does not represent a change
  1251. if ($target.closest('.resource-add').length===0) {
  1252. flashWarning();
  1253. }
  1254. });
  1255. // Don't stop us leaving
  1256. $form.submit(function() {
  1257. window.onbeforeunload = null;
  1258. });
  1259. // Calling functions might hook to flashWarning
  1260. return flashWarning;
  1261. };
  1262. }();
  1263. my.countObject = function(obj) {
  1264. var count=0;
  1265. $.each(obj, function() {
  1266. count++;
  1267. });
  1268. return count;
  1269. };
  1270. function followButtonClicked(event) {
  1271. var button = event.currentTarget;
  1272. if (button.id === 'user_follow_button') {
  1273. var object_type = 'user';
  1274. } else if (button.id === 'dataset_follow_button') {
  1275. var object_type = 'dataset';
  1276. }
  1277. else {
  1278. // This shouldn't happen.
  1279. return;
  1280. }
  1281. var object_id = button.getAttribute('data-obj-id');
  1282. if (button.getAttribute('data-state') === "follow") {
  1283. if (object_type == 'user') {
  1284. var url = '/api/action/follow_user';
  1285. } else if (object_type == 'dataset') {
  1286. var url = '/api/action/follow_dataset';
  1287. } else {
  1288. // This shouldn't happen.
  1289. return;
  1290. }
  1291. var data = JSON.stringify({
  1292. id: object_id
  1293. });
  1294. var nextState = 'unfollow';
  1295. var nextString = CKAN.Strings.unfollow;
  1296. } else if (button.getAttribute('data-state') === "unfollow") {
  1297. if (object_type == 'user') {
  1298. var url = '/api/action/unfollow_user';
  1299. } else if (object_type == 'dataset') {
  1300. var url = '/api/action/unfollow_dataset';
  1301. } else {
  1302. // This shouldn't happen.
  1303. return;
  1304. }
  1305. var data = JSON.stringify({
  1306. id: object_id
  1307. });
  1308. var nextState = 'follow';
  1309. var nextString = CKAN.Strings.follow;
  1310. }
  1311. else {
  1312. // This shouldn't happen.
  1313. return;
  1314. }
  1315. $.ajax({
  1316. contentType: 'application/json',
  1317. url: url,
  1318. data: data,
  1319. dataType: 'json',
  1320. processData: false,
  1321. type: 'POST',
  1322. success: function(data) {
  1323. button.setAttribute('data-state', nextState);
  1324. button.innerHTML = nextString;
  1325. }
  1326. });
  1327. };
  1328. // This only needs to happen on dataset pages, but it doesn't seem to do
  1329. // any harm to call it anyway.
  1330. $('#user_follow_button').on('click', followButtonClicked);
  1331. $('#dataset_follow_button').on('click', followButtonClicked);
  1332. return my;
  1333. }(jQuery, CKAN.Utils || {});
  1334. /* ==================== */
  1335. /* == Data Previewer == */
  1336. /* ==================== */
  1337. CKAN.DataPreview = function ($, my) {
  1338. my.jsonpdataproxyUrl = 'http://jsonpdataproxy.appspot.com/';
  1339. my.dialogId = 'ckanext-datapreview';
  1340. my.$dialog = $('#' + my.dialogId);
  1341. // **Public: Loads a data previewer for an embedded page**
  1342. //
  1343. // Uses the provided reclineState to restore the Dataset. Creates a single
  1344. // view for the Dataset (the one defined by reclineState.currentView). And
  1345. // then passes the constructed Dataset, the constructed View, and the
  1346. // reclineState into the DataExplorer constructor.
  1347. my.loadEmbeddedPreview = function(resourceData, reclineState) {
  1348. my.$dialog.html('<h4>Loading ... <img src="http://assets.okfn.org/images/icons/ajaxload-circle.gif" class="loading-spinner" /></h4>');
  1349. // Restore the Dataset from the given reclineState.
  1350. var datasetInfo = _.extend({
  1351. url: reclineState.url,
  1352. backend: reclineState.backend
  1353. },
  1354. reclineState.dataset
  1355. );
  1356. var dataset = new recline.Model.Dataset(datasetInfo);
  1357. // Only create the view defined in reclineState.currentView.
  1358. // TODO: tidy this up.
  1359. var views = null;
  1360. if (reclineState.currentView === 'grid') {
  1361. views = [ {
  1362. id: 'grid',
  1363. label: 'Grid',
  1364. view: new recline.View.SlickGrid({
  1365. model: dataset,
  1366. state: reclineState['view-grid']
  1367. })
  1368. }];
  1369. } else if (reclineState.currentView === 'graph') {
  1370. views = [ {
  1371. id: 'graph',
  1372. label: 'Graph',
  1373. view: new recline.View.Graph({
  1374. model: dataset,
  1375. state: reclineState['view-graph']
  1376. })
  1377. }];
  1378. } else if (reclineState.currentView === 'map') {
  1379. views = [ {
  1380. id: 'map',
  1381. label: 'Map',
  1382. view: new recline.View.Map({
  1383. model: dataset,
  1384. state: reclineState['view-map']
  1385. })
  1386. }];
  1387. }
  1388. // Finally, construct the DataExplorer. Again, passing in the reclineState.
  1389. var dataExplorer = new recline.View.MultiView({
  1390. el: my.$dialog,
  1391. model: dataset,
  1392. state: reclineState,
  1393. views: views
  1394. });
  1395. };
  1396. // **Public: Creates a link to the embeddable page.
  1397. //
  1398. // For a given DataExplorer state, this function constructs and returns the
  1399. // url to the embeddable view of the current dataexplorer state.
  1400. my.makeEmbedLink = function(explorerState) {
  1401. var state = explorerState.toJSON();
  1402. state.state_version = 1;
  1403. var queryString = '?';
  1404. var items = [];
  1405. $.each(state, function(key, value) {
  1406. if (typeof(value) === 'object') {
  1407. value = JSON.stringify(value);
  1408. }
  1409. items.pu

Large files files are truncated, but you can click here to view the full file