PageRenderTime 61ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 1ms

/ckan/public/scripts/application.js

https://bitbucket.org/philippkueng/ckan-liip
JavaScript | 1891 lines | 1506 code | 145 blank | 240 comment | 135 complexity | 6f9551a0fb2e3b4b85889a9dc9c779da MD5 | raw file

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

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