From e5ccd8dada399e9fb20e88cb8433a8ddb05ec741 Mon Sep 17 00:00:00 2001 From: Aron Carroll Date: Thu, 28 Jun 2012 15:49:03 +0100 Subject: [PATCH] [#2617] Moved all JS into public directory for development --- ckan/html_resources/datapreview/config.ini | 2 - .../html_resources/datapreview/datapreview.js | 1862 ---- ckan/html_resources/datapreview/templates.js | 169 - .../base/datapreview/css/datapreview.css | 88 + .../css/datapreview.table-view.css} | 6 +- ckan/public/base/datapreview/css/leaflet.css | 323 + .../base/datapreview/css/leaflet.ie.css | 48 + .../base/datapreview/css/recline.graph.css | 55 + .../base/datapreview/css/recline.grid.css | 319 + .../base/datapreview/css/recline.map.css | 28 + .../datapreview/css/slick.columnpicker.css | 30 + .../base/datapreview/css/slick.grid.css | 153 + .../images}/icon-sprite.png | Bin .../datapreview/images/leaflet/layers.png | Bin 0 -> 3945 bytes .../images/leaflet/marker-shadow.png | Bin 0 -> 1649 bytes .../datapreview/images/leaflet/marker.png | Bin 0 -> 2519 bytes .../images/leaflet/popup-close.png | Bin 0 -> 1125 bytes .../datapreview/images/leaflet/zoom-in.png | Bin 0 -> 963 bytes .../datapreview/images/leaflet/zoom-out.png | Bin 0 -> 959 bytes .../images}/loading.gif | Bin .../datapreview/images/recline/edit-map.png | Bin 0 -> 1569 bytes .../images/recline/menu-dropdown.png | Bin 0 -> 1123 bytes .../images/recline/small-spinner.gif | Bin 0 -> 1849 bytes .../datapreview/images/slickgrid/actions.gif | Bin 0 -> 170 bytes .../images/slickgrid/ajax-loader-small.gif | Bin 0 -> 1849 bytes .../images/slickgrid/arrow_redo.png | Bin 0 -> 625 bytes .../slickgrid/arrow_right_peppermint.png | Bin 0 -> 240 bytes .../slickgrid/arrow_right_spearmint.png | Bin 0 -> 240 bytes .../images/slickgrid/arrow_undo.png | Bin 0 -> 631 bytes .../images/slickgrid/bullet_blue.png | Bin 0 -> 289 bytes .../images/slickgrid/bullet_star.png | Bin 0 -> 347 bytes .../images/slickgrid/bullet_toggle_minus.png | Bin 0 -> 207 bytes .../images/slickgrid/bullet_toggle_plus.png | Bin 0 -> 209 bytes .../datapreview/images/slickgrid/calendar.gif | Bin 0 -> 1035 bytes .../datapreview/images/slickgrid/collapse.gif | Bin 0 -> 846 bytes .../images/slickgrid/comment_yellow.gif | Bin 0 -> 257 bytes .../datapreview/images/slickgrid/down.gif | Bin 0 -> 59 bytes .../images/slickgrid/drag-handle.png | Bin 0 -> 1223 bytes .../images/slickgrid/editor-helper-bg.gif | Bin 0 -> 1164 bytes .../datapreview/images/slickgrid/expand.gif | Bin 0 -> 851 bytes .../images/slickgrid/header-bg.gif | Bin 0 -> 872 bytes .../images/slickgrid/header-columns-bg.gif | Bin 0 -> 836 bytes .../slickgrid/header-columns-over-bg.gif | Bin 0 -> 823 bytes .../datapreview/images/slickgrid/help.png | Bin 0 -> 510 bytes .../datapreview/images/slickgrid/info.gif | Bin 0 -> 80 bytes .../datapreview/images/slickgrid/listview.gif | Bin 0 -> 2380 bytes .../datapreview/images/slickgrid/pencil.gif | Bin 0 -> 914 bytes .../images/slickgrid/row-over-bg.gif | Bin 0 -> 823 bytes .../datapreview/images/slickgrid/sort-asc.gif | Bin 0 -> 830 bytes .../datapreview/images/slickgrid/sort-asc.png | Bin 0 -> 163 bytes .../images/slickgrid/sort-desc.gif | Bin 0 -> 833 bytes .../images/slickgrid/sort-desc.png | Bin 0 -> 161 bytes .../datapreview/images/slickgrid/stripes.png | Bin 0 -> 1238 bytes .../datapreview/images/slickgrid/tag_red.png | Bin 0 -> 592 bytes .../datapreview/images/slickgrid/tick.png | Bin 0 -> 537 bytes .../images/slickgrid/user_identity.gif | Bin 0 -> 905 bytes .../images/slickgrid/user_identity_plus.gif | Bin 0 -> 546 bytes .../datapreview/javascript/datapreview.js | 384 + .../javascript}/table-view-template.js | 0 .../datapreview/javascript}/table-view.js | 0 .../datapreview/javascript}/table-view.ui.js | 0 .../datapreview/javascript/vendor/backbone.js | 1149 ++ .../javascript/vendor/jquery.event.drag.js | 6 + .../javascript/vendor/jquery.flot.js | 2599 +++++ .../javascript/vendor/jquery.mustache.js | 346 + .../datapreview/javascript/vendor/leaflet.js | 6 + .../datapreview/javascript/vendor/recline.js | 3850 +++++++ .../javascript/vendor/slick.columnpicker.js | 105 + .../javascript/vendor/slick.grid.js | 2504 +++++ .../javascript/vendor/underscore.js | 807 ++ .../ckan => public/base/javascript}/main.js | 0 .../base/javascript}/vendor/bootstrap.js | 0 ckan/public/base/javascript/vendor/jquery.js | 9404 +++++++++++++++++ 73 files changed, 22207 insertions(+), 2036 deletions(-) delete mode 100644 ckan/html_resources/datapreview/config.ini delete mode 100644 ckan/html_resources/datapreview/datapreview.js delete mode 100644 ckan/html_resources/datapreview/templates.js create mode 100644 ckan/public/base/datapreview/css/datapreview.css rename ckan/public/base/{css/datapreview/table-view.css => datapreview/css/datapreview.table-view.css} (95%) create mode 100644 ckan/public/base/datapreview/css/leaflet.css create mode 100644 ckan/public/base/datapreview/css/leaflet.ie.css create mode 100644 ckan/public/base/datapreview/css/recline.graph.css create mode 100644 ckan/public/base/datapreview/css/recline.grid.css create mode 100644 ckan/public/base/datapreview/css/recline.map.css create mode 100755 ckan/public/base/datapreview/css/slick.columnpicker.css create mode 100755 ckan/public/base/datapreview/css/slick.grid.css rename ckan/public/base/{images/datapreview => datapreview/images}/icon-sprite.png (100%) create mode 100644 ckan/public/base/datapreview/images/leaflet/layers.png create mode 100644 ckan/public/base/datapreview/images/leaflet/marker-shadow.png create mode 100644 ckan/public/base/datapreview/images/leaflet/marker.png create mode 100644 ckan/public/base/datapreview/images/leaflet/popup-close.png create mode 100644 ckan/public/base/datapreview/images/leaflet/zoom-in.png create mode 100644 ckan/public/base/datapreview/images/leaflet/zoom-out.png rename ckan/public/base/{images/datapreview => datapreview/images}/loading.gif (100%) create mode 100755 ckan/public/base/datapreview/images/recline/edit-map.png create mode 100755 ckan/public/base/datapreview/images/recline/menu-dropdown.png create mode 100755 ckan/public/base/datapreview/images/recline/small-spinner.gif create mode 100755 ckan/public/base/datapreview/images/slickgrid/actions.gif create mode 100755 ckan/public/base/datapreview/images/slickgrid/ajax-loader-small.gif create mode 100755 ckan/public/base/datapreview/images/slickgrid/arrow_redo.png create mode 100755 ckan/public/base/datapreview/images/slickgrid/arrow_right_peppermint.png create mode 100755 ckan/public/base/datapreview/images/slickgrid/arrow_right_spearmint.png create mode 100755 ckan/public/base/datapreview/images/slickgrid/arrow_undo.png create mode 100755 ckan/public/base/datapreview/images/slickgrid/bullet_blue.png create mode 100755 ckan/public/base/datapreview/images/slickgrid/bullet_star.png create mode 100755 ckan/public/base/datapreview/images/slickgrid/bullet_toggle_minus.png create mode 100755 ckan/public/base/datapreview/images/slickgrid/bullet_toggle_plus.png create mode 100755 ckan/public/base/datapreview/images/slickgrid/calendar.gif create mode 100755 ckan/public/base/datapreview/images/slickgrid/collapse.gif create mode 100755 ckan/public/base/datapreview/images/slickgrid/comment_yellow.gif create mode 100755 ckan/public/base/datapreview/images/slickgrid/down.gif create mode 100755 ckan/public/base/datapreview/images/slickgrid/drag-handle.png create mode 100755 ckan/public/base/datapreview/images/slickgrid/editor-helper-bg.gif create mode 100755 ckan/public/base/datapreview/images/slickgrid/expand.gif create mode 100755 ckan/public/base/datapreview/images/slickgrid/header-bg.gif create mode 100755 ckan/public/base/datapreview/images/slickgrid/header-columns-bg.gif create mode 100755 ckan/public/base/datapreview/images/slickgrid/header-columns-over-bg.gif create mode 100755 ckan/public/base/datapreview/images/slickgrid/help.png create mode 100755 ckan/public/base/datapreview/images/slickgrid/info.gif create mode 100755 ckan/public/base/datapreview/images/slickgrid/listview.gif create mode 100755 ckan/public/base/datapreview/images/slickgrid/pencil.gif create mode 100755 ckan/public/base/datapreview/images/slickgrid/row-over-bg.gif create mode 100755 ckan/public/base/datapreview/images/slickgrid/sort-asc.gif create mode 100755 ckan/public/base/datapreview/images/slickgrid/sort-asc.png create mode 100755 ckan/public/base/datapreview/images/slickgrid/sort-desc.gif create mode 100755 ckan/public/base/datapreview/images/slickgrid/sort-desc.png create mode 100755 ckan/public/base/datapreview/images/slickgrid/stripes.png create mode 100755 ckan/public/base/datapreview/images/slickgrid/tag_red.png create mode 100755 ckan/public/base/datapreview/images/slickgrid/tick.png create mode 100755 ckan/public/base/datapreview/images/slickgrid/user_identity.gif create mode 100755 ckan/public/base/datapreview/images/slickgrid/user_identity_plus.gif create mode 100644 ckan/public/base/datapreview/javascript/datapreview.js rename ckan/{html_resources/datapreview => public/base/datapreview/javascript}/table-view-template.js (100%) rename ckan/{html_resources/datapreview => public/base/datapreview/javascript}/table-view.js (100%) rename ckan/{html_resources/datapreview => public/base/datapreview/javascript}/table-view.ui.js (100%) create mode 100644 ckan/public/base/datapreview/javascript/vendor/backbone.js create mode 100755 ckan/public/base/datapreview/javascript/vendor/jquery.event.drag.js create mode 100644 ckan/public/base/datapreview/javascript/vendor/jquery.flot.js create mode 100755 ckan/public/base/datapreview/javascript/vendor/jquery.mustache.js create mode 100644 ckan/public/base/datapreview/javascript/vendor/leaflet.js create mode 100644 ckan/public/base/datapreview/javascript/vendor/recline.js create mode 100755 ckan/public/base/datapreview/javascript/vendor/slick.columnpicker.js create mode 100755 ckan/public/base/datapreview/javascript/vendor/slick.grid.js create mode 100644 ckan/public/base/datapreview/javascript/vendor/underscore.js rename ckan/{html_resources/ckan => public/base/javascript}/main.js (100%) rename ckan/{html_resources => public/base/javascript}/vendor/bootstrap.js (100%) create mode 100644 ckan/public/base/javascript/vendor/jquery.js diff --git a/ckan/html_resources/datapreview/config.ini b/ckan/html_resources/datapreview/config.ini deleted file mode 100644 index 88bc94fd213..00000000000 --- a/ckan/html_resources/datapreview/config.ini +++ /dev/null @@ -1,2 +0,0 @@ -[groups] -datapreview = datapreview table-view-template table-view table-view.ui templates diff --git a/ckan/html_resources/datapreview/datapreview.js b/ckan/html_resources/datapreview/datapreview.js deleted file mode 100644 index 2eecaa34bd8..00000000000 --- a/ckan/html_resources/datapreview/datapreview.js +++ /dev/null @@ -1,1862 +0,0 @@ -var CKAN = CKAN || {}; - -CKAN.View = CKAN.View || {}; -CKAN.Model = CKAN.Model || {}; -CKAN.Utils = CKAN.Utils || {}; - -/* ================================= */ -/* == Initialise CKAN Application == */ -/* ================================= */ -(function ($) { - $(document).ready(function () { - CKAN.Utils.relatedSetup($("#form-add-related")); - CKAN.Utils.setupUserAutocomplete($('input.autocomplete-user')); - CKAN.Utils.setupOrganizationUserAutocomplete($('input.autocomplete-organization-user')); - CKAN.Utils.setupGroupAutocomplete($('input.autocomplete-group')); - CKAN.Utils.setupAuthzGroupAutocomplete($('input.autocomplete-authzgroup')); - CKAN.Utils.setupPackageAutocomplete($('input.autocomplete-dataset')); - CKAN.Utils.setupTagAutocomplete($('input.autocomplete-tag')); - $('input.autocomplete-format').live('keyup', function(){ - CKAN.Utils.setupFormatAutocomplete($(this)); - }); - CKAN.Utils.setupMarkdownEditor($('.markdown-editor')); - // bootstrap collapse - $('.collapse').collapse({toggle: false}); - - // Buttons with href-action should navigate when clicked - $('input.href-action').click(function(e) { - e.preventDefault(); - window.location = ($(e.target).attr('action')); - }); - - var isGroupView = $('body.group.read').length > 0; - if (isGroupView) { - // Show extract of notes field - CKAN.Utils.setupNotesExtract(); - } - - var isDatasetView = $('body.package.read').length > 0; - if (isDatasetView) { - // Show extract of notes field - CKAN.Utils.setupNotesExtract(); - } - - var isResourceView = $('body.package.resource_read').length > 0; - if (isResourceView) { - CKAN.DataPreview.loadPreviewDialog(preload_resource); - } - - var isEmbededDataviewer = $('body.package.resource_embedded_dataviewer').length > 0; - if (isEmbededDataviewer) { - CKAN.DataPreview.loadEmbeddedPreview(preload_resource, reclineState); - } - - if ($(document.body).hasClass('search')) { - // Calculate the optimal width for the search input regardless of the - // width of the submit button (which can vary depending on translation). - (function resizeSearchInput() { - var form = $('#dataset-search'), - input = form.find('[name=q]'), - button = form.find('[type=submit]'), - offset = parseFloat(button.css('margin-left')); - - // Grab the horizontal properties of the input that affect the width. - $.each(['padding-left', 'padding-right', 'border-left-width', 'border-right-width'], function (i, prop) { - offset += parseFloat(input.css(prop)) || 0; - }); - - input.width(form.outerWidth() - button.outerWidth() - offset); - })(); - } - - var isDatasetNew = $('body.package.new').length > 0; - if (isDatasetNew) { - // Set up magic URL slug editor - var urlEditor = new CKAN.View.UrlEditor({ - slugType: 'package' - }); - $('#save').val(CKAN.Strings.addDataset); - $("#title").focus(); - } - var isGroupNew = $('body.group.new').length > 0; - if (isGroupNew) { - // Set up magic URL slug editor - var urlEditor = new CKAN.View.UrlEditor({ - slugType: 'group' - }); - $('#save').val(CKAN.Strings.addGroup); - $("#title").focus(); - } - - var isDatasetEdit = $('body.package.edit').length > 0; - if (isDatasetEdit) { - CKAN.Utils.warnOnFormChanges($('form#dataset-edit')); - var urlEditor = new CKAN.View.UrlEditor({ - slugType: 'package' - }); - - // Set up dataset delete button - CKAN.Utils.setupDatasetDeleteButton(); - } - var isDatasetResourceEdit = $('body.package.editresources').length > 0; - if (isDatasetNew || isDatasetResourceEdit) { - // Selectively enable the upload button - var storageEnabled = $.inArray('storage',CKAN.plugins)>=0; - if (storageEnabled) { - $('li.js-upload-file').show(); - } - // Backbone collection class - var CollectionOfResources = Backbone.Collection.extend({model: CKAN.Model.Resource}); - // 'resources_json' was embedded into the page - var view = new CKAN.View.ResourceEditor({ - collection: new CollectionOfResources(resources_json), - el: $('form#dataset-edit') - }); - view.render(); - - $( ".drag-drop-list" ).sortable({ - distance: 10 - }); - $( ".drag-drop-list" ).disableSelection(); - } - - var isGroupEdit = $('body.group.edit').length > 0; - if (isGroupEdit) { - var urlEditor = new CKAN.View.UrlEditor({ - slugType: 'group' - }); - } - // OpenID hack - // We need to remember the language we are using whilst logging in - // we set this in the user session so we don't forget then - // carry on as before. - if (window.openid && openid.signin){ - openid._signin = openid.signin; - openid.signin = function (arg) { - $.get(CKAN.SITE_URL + '/user/set_lang/' + CKAN.LANG, function (){openid._signin(arg);}) - }; - } - if ($('#login').length){ - $('#login').submit( function () { - $.ajax(CKAN.SITE_URL + '/user/set_lang/' + CKAN.LANG, {async:false}); - }); - } - }); -}(jQuery)); - -/* =============================== */ -/* jQuery Plugins */ -/* =============================== */ - -jQuery.fn.truncate = function (max, suffix) { - return this.each(function () { - var element = jQuery(this), - isTruncated = element.hasClass('truncated'), - cached, length, text, expand; - - if (isTruncated) { - element.html(element.data('truncate:' + (max === 'expand' ? 'original' : 'truncated'))); - return; - } - - cached = element.text(); - length = max || element.data('truncate') || 30; - text = cached.slice(0, length); - expand = jQuery('').text(suffix || 'ยป'); - - // Bail early if there is nothing to truncate. - if (cached.length < length) { - return; - } - - // Try to truncate to nearest full word. - while ((/\S/).test(text[text.length - 1])) { - text = text.slice(0, text.length - 1); - } - - element.html(jQuery.trim(text)); - expand.appendTo(element.append(' ')); - expand.click(function (event) { - event.preventDefault(); - element.html(cached); - }); - - element.addClass('truncated'); - element.data('truncate:original', cached); - element.data('truncate:truncated', element.html()); - }); -}; - -/* =============================== */ -/* Backbone Model: Resource object */ -/* =============================== */ -CKAN.Model.Resource = Backbone.Model.extend({ - constructor: function Resource() { - Backbone.Model.prototype.constructor.apply(this, arguments); - }, - toTemplateJSON: function() { - var obj = Backbone.Model.prototype.toJSON.apply(this, arguments); - obj.displaytitle = obj.description ? obj.description : 'No description ...'; - return obj; - } -}); - - - -/* ============================== */ -/* == Backbone View: UrlEditor == */ -/* ============================== */ -CKAN.View.UrlEditor = Backbone.View.extend({ - initialize: function() { - _.bindAll(this,'titleToSlug','titleChanged','urlChanged','checkSlugIsValid','apiCallback'); - - // Initial state - var self = this; - this.updateTimer = null; - this.titleInput = $('.js-title'); - this.urlInput = $('.js-url-input'); - this.validMsg = $('.js-url-is-valid'); - this.lengthMsg = $('.url-is-long'); - this.lastTitle = ""; - this.disableTitleChanged = false; - - // Settings - this.regexToHyphen = [ new RegExp('[ .:/_]', 'g'), - new RegExp('[^a-zA-Z0-9-_]', 'g'), - new RegExp('-+', 'g')]; - this.regexToDelete = [ new RegExp('^-*', 'g'), - new RegExp('-*$', 'g')]; - - // Default options - if (!this.options.apiUrl) { - this.options.apiUrl = CKAN.SITE_URL + '/api/2/util/is_slug_valid'; - } - if (!this.options.MAX_SLUG_LENGTH) { - this.options.MAX_SLUG_LENGTH = 90; - } - this.originalUrl = this.urlInput.val(); - - // Hook title changes to the input box - CKAN.Utils.bindInputChanges(this.titleInput, this.titleChanged); - CKAN.Utils.bindInputChanges(this.urlInput, this.urlChanged); - - // If you've bothered typing a URL, I won't overwrite you - function disable() { - self.disableTitleChanged = true; - }; - this.urlInput.keyup (disable); - this.urlInput.keydown (disable); - this.urlInput.keypress(disable); - - // Set up the form - this.urlChanged(); - }, - - titleToSlug: function(title) { - var slug = title; - $.each(this.regexToHyphen, function(idx,regex) { slug = slug.replace(regex, '-'); }); - $.each(this.regexToDelete, function(idx,regex) { slug = slug.replace(regex, ''); }); - slug = slug.toLowerCase(); - - if (slug.length'+CKAN.Strings.urlIsTooShort+''); - } - else if (slug==this.originalUrl) { - this.validMsg.html(''+CKAN.Strings.urlIsUnchanged+''); - } - else { - this.validMsg.html(''+CKAN.Strings.checking+''); - var self = this; - this.updateTimer = setTimeout(function () { - self.checkSlugIsValid(slug); - }, 200); - } - if (slug.length>20) { - this.lengthMsg.show(); - } - else { - this.lengthMsg.hide(); - } - }, - - checkSlugIsValid: function(slug) { - $.ajax({ - url: this.options.apiUrl, - data: 'type='+this.options.slugType+'&slug=' + slug, - dataType: 'jsonp', - type: 'get', - jsonpCallback: 'callback', - success: this.apiCallback - }); - }, - - /* Called when the slug-validator gets back to us */ - apiCallback: function(data) { - if (data.valid) { - this.validMsg.html(''+CKAN.Strings.urlIsAvailable+''); - } else { - this.validMsg.html(''+CKAN.Strings.urlIsNotAvailable+''); - } - } -}); - - -/* =================================== */ -/* == Backbone View: ResourceEditor == */ -/* =================================== */ -CKAN.View.ResourceEditor = Backbone.View.extend({ - initialize: function() { - // Init bindings - _.bindAll(this, 'resourceAdded', 'resourceRemoved', 'sortStop', 'openFirstPanel', 'closePanel', 'openAddPanel'); - this.collection.bind('add', this.resourceAdded); - this.collection.bind('remove', this.resourceRemoved); - this.collection.each(this.resourceAdded); - this.el.find('.resource-list-edit').bind("sortstop", this.sortStop); - - // Delete the barebones editor. We will populate our own form. - this.el.find('.js-resource-edit-barebones').remove(); - - // Warn on form changes - var flashWarning = CKAN.Utils.warnOnFormChanges(this.el); - this.collection.bind('add', flashWarning); - this.collection.bind('remove', flashWarning); - - // Trigger the Add Resource pane - this.el.find('.js-resource-add').click(this.openAddPanel); - - // Tabs for adding resources - new CKAN.View.ResourceAddUrl({ - collection: this.collection, - el: this.el.find('.js-add-url-form'), - mode: 'file' - }); - new CKAN.View.ResourceAddUrl({ - collection: this.collection, - el: this.el.find('.js-add-api-form'), - mode: 'api' - }); - new CKAN.View.ResourceAddUpload({ - collection: this.collection, - el: this.el.find('.js-add-upload-form') - }); - - - // Close details button - this.el.find('.resource-panel-close').click(this.closePanel); - - // Did we embed some form errors? - if (typeof global_form_errors == 'object') { - if (global_form_errors.resources) { - var openedOne = false; - for (i in global_form_errors.resources) { - var resource_errors = global_form_errors.resources[i]; - if (CKAN.Utils.countObject(resource_errors) > 0) { - var resource = this.collection.at(i); - resource.view.setErrors(resource_errors); - if (!openedOne) { - resource.view.openMyPanel(); - openedOne = true; - } - } - } - } - } - else { - // Initial state - this.openFirstPanel(); - } - }, - /* - * Called when the page loads or the current resource is deleted. - * Reset page state to the first available edit panel. - */ - openFirstPanel: function() { - if (this.collection.length>0) { - this.collection.at(0).view.openMyPanel(); - } - else { - this.openAddPanel(); - } - }, - /* - * Open the 'Add New Resource' special-case panel on the right. - */ - openAddPanel: function(e) { - if (e) { e.preventDefault(); } - var panel = this.el.find('.resource-panel'); - var addLi = this.el.find('.resource-list-add > li'); - this.el.find('.resource-list > li').removeClass('active'); - $('.resource-details').hide(); - this.el.find('.resource-details.resource-add').show(); - addLi.addClass('active'); - panel.show(); - }, - /* - * Close the panel on the right. - */ - closePanel: function(e) { - if (e) { e.preventDefault(); } - this.el.find('.resource-list > li').removeClass('active'); - this.el.find('.resource-panel').hide(); - }, - /* - * Update the resource__N__field names to match - * new sort order. - */ - sortStop: function(e,ui) { - this.collection.each(function(resource) { - // Ask the DOM for the new sort order - var index = resource.view.li.index(); - resource.view.options.position = index; - // Update the form element names - var table = resource.view.table; - $.each(table.find('input,textarea,select'), function(input_index, input) { - var name = $(input).attr('name'); - if (name) { - name = name.replace(/(resources__)\d+(.*)/, '$1'+index+'$2'); - $(input).attr('name',name); - } - }); - }); - }, - /* - * Calculate id of the next resource to create - */ - nextIndex: function() { - var maxId=-1; - var root = this.el.find('.resource-panel'); - root.find('input').each(function(idx,input) { - var name = $(input).attr('name') || ''; - var splitName=name.split('__'); - if (splitName.length>1) { - var myId = parseInt(splitName[1],10); - maxId = Math.max(myId, maxId); - } - }); - return maxId+1; - }, - /* - * Create DOM elements for new resource. - */ - resourceAdded: function(resource) { - var self = this; - resource.view = new CKAN.View.Resource({ - position: this.nextIndex(), - model: resource, - callback_deleteMe: function() { self.collection.remove(resource); } - }); - this.el.find('.resource-list-edit').append(resource.view.li); - this.el.find('.resource-panel').append(resource.view.table); - if (resource.isNew()) { - resource.view.openMyPanel(); - } - }, - /* - * Destroy DOM elements for deleted resource. - */ - resourceRemoved: function(resource) { - resource.view.removeFromDom(); - delete resource.view; - this.openFirstPanel(); - } -}); - - -/* ============================== */ -/* == Backbone View: Resource == */ -/* ============================== */ - -CKAN.View.Resource = Backbone.View.extend({ - initialize: function() { - this.el = $(this.el); - _.bindAll(this,'updateName','updateIcon','name','askToDelete','openMyPanel','setErrors','setupDynamicExtras','addDynamicExtra', 'onDatastoreEnabledChange'); - this.render(); - }, - render: function() { - this.raw_resource = this.model.toTemplateJSON(); - var resource_object = { - resource: this.raw_resource, - num: this.options.position, - resource_icon: '/images/icons/page_white.png', - resourceTypeOptions: [ - ['file', CKAN.Strings.dataFile], - ['api', CKAN.Strings.api], - ['visualization', CKAN.Strings.visualization], - ['image', CKAN.Strings.image], - ['metadata', CKAN.Strings.metadata], - ['documentation', CKAN.Strings.documentation], - ['code', CKAN.Strings.code], - ['example', CKAN.Strings.example] - ] - }; - // Generate DOM elements - this.li = $($.tmpl(CKAN.Templates.resourceEntry, resource_object)); - this.table = $($.tmpl(CKAN.Templates.resourceDetails, resource_object)); - - // Hook to changes in name - this.nameBox = this.table.find('input.js-resource-edit-name'); - this.descriptionBox = this.table.find('textarea.js-resource-edit-description'); - CKAN.Utils.bindInputChanges(this.nameBox,this.updateName); - CKAN.Utils.bindInputChanges(this.descriptionBox,this.updateName); - // Hook to changes in format - this.formatBox = this.table.find('input.js-resource-edit-format'); - CKAN.Utils.bindInputChanges(this.formatBox,this.updateIcon); - // Hook to open panel link - this.li.find('.resource-open-my-panel').click(this.openMyPanel); - this.table.find('.js-resource-edit-delete').click(this.askToDelete); - this.table.find('.js-datastore-enabled-checkbox').change(this.onDatastoreEnabledChange); - // Hook to markdown editor - CKAN.Utils.setupMarkdownEditor(this.table.find('.markdown-editor')); - if (resource_object.resource.webstore_url) { - this.table.find('.js-datastore-enabled-checkbox').prop('checked', true); - } - - // Set initial state - this.updateName(); - this.updateIcon(); - this.setupDynamicExtras(); - this.hasErrors = false; - }, - /* - * Process a JSON object of errors attached to this resource - */ - setErrors: function(obj) { - if (CKAN.Utils.countObject(obj) > 0) { - this.hasErrors = true; - this.errors = obj; - this.li.addClass('hasErrors'); - var errorList = $('
').addClass('errorList'); - $.each(obj,function(k,v) { - var errorText = ''; - var newLine = false; - $.each(v,function(index,value) { - if (newLine) { errorText += '
'; } - errorText += value; - newLine = true; - }); - errorList.append($('
').html(k)); - errorList.append($('
').html(errorText)); - }); - this.table.find('.resource-errors').append(errorList).show(); - } - }, - /* - * Work out what I should be called. Rough-match - * of helpers.py:resource_display_name. - */ - name: function() { - var name = this.nameBox.val(); - if (!name) { - name = this.descriptionBox.val(); - if (!name) { - if (this.model.isNew()) { - name = '[new resource]'; - } - else { - name = '[no name] ' + this.model.id; - } - } - } - if (name.length>45) { - name = name.substring(0,45)+'...'; - } - return name; - }, - /* - * Called when the user types to update the name in - * my
  • to match the values. - */ - updateName: function() { - // Need to structurally modify the DOM to force a re-render of text - var $link = this.li.find('.js-resource-edit-name'); - $link.html(''+this.name()+''); - }, - /* - * Called when the user types to update the icon - * tags. Uses server API to select icon. - */ - updateIcon: function() { - var self = this; - if (self.updateIconTimer) { - clearTimeout(self.updateIconTimer); - } - self.updateIconTimer = setTimeout(function() { - // AJAX to server API - $.getJSON(CKAN.SITE_URL + '/api/2/util/resource/format_icon?format='+encodeURIComponent(self.formatBox.val()), function(data) { - if (data && data.icon && data.format==self.formatBox.val()) { - self.li.find('.js-resource-icon').attr('src',data.icon); - self.table.find('.js-resource-icon').attr('src',data.icon); - } - }); - delete self.updateIconTimer; - }, - 100); - }, - /* - * Closes all other panels on the right and opens my editor panel. - */ - openMyPanel: function(e) { - if (e) { e.preventDefault(); } - // Close all tables - var panel = this.table.parents('.resource-panel'); - panel.find('.resource-details').hide(); - this.li.parents('fieldset#resources').find('.resource-list > li').removeClass('active'); - panel.show(); - this.table.show(); - this.table.find('.js-resource-edit-name').focus(); - this.li.addClass('active'); - }, - /* - * Called when my delete button is clicked. Calls back to the parent - * resource editor. - */ - askToDelete: function(e) { - e.preventDefault(); - var confirmMessage = CKAN.Strings.deleteThisResourceQuestion.replace('%name%', this.name()); - if (confirm(confirmMessage)) { - this.options.callback_deleteMe(); - } - }, - /* - * Set up the dynamic-extras section of the table. - */ - setupDynamicExtras: function() { - var self = this; - $.each(this.raw_resource, function(key,value) { - // Skip the known keys - if (self.reservedWord(key)) { return; } - self.addDynamicExtra(key,value); - }); - this.table.find('.add-resource-extra').click(function(e) { - e.preventDefault(); - self.addDynamicExtra('',''); - }); - }, - addDynamicExtra: function(key,value) { - // Create elements - var dynamicExtra = $($.tmpl(CKAN.Templates.resourceExtra, { - num: this.options.position, - key: key, - value: value})); - this.table.find('.dynamic-extras').append(dynamicExtra); - // Captured values - var inputKey = dynamicExtra.find('.extra-key'); - var inputValue = dynamicExtra.find('.extra-value'); - // Callback function - var self = this; - var setExtraName = function() { - var _key = inputKey.val(); - var key = $.trim(_key).replace(/\s+/g,''); - // Don't allow you to create an extra called mimetype (etc) - if (self.reservedWord(key)) { key=''; } - // Set or unset the field's name - if (key.length) { - var newName = 'resources__'+self.options.position+'__'+key; - inputValue.attr('name',newName); - inputValue.removeClass('strikethrough'); - } - else { - inputValue.removeAttr('name'); - inputValue.addClass('strikethrough'); - } - }; - // Callback function - var clickRemove = function(e) { - e.preventDefault(); - dynamicExtra.remove(); - }; - // Init with bindings - CKAN.Utils.bindInputChanges(dynamicExtra.find('.extra-key'), setExtraName); - dynamicExtra.find('.remove-resource-extra').click(clickRemove); - setExtraName(); - }, - - - - reservedWord: function(word) { - return word=='cache_last_updated' || - word=='cache_url' || - word=='dataset' || - word=='description' || - word=='displaytitle' || - word=='extras' || - word=='format' || - word=='hash' || - word=='id' || - word=='created' || - word=='last_modified' || - word=='mimetype' || - word=='mimetype_inner' || - word=='name' || - word=='package_id' || - word=='position' || - word=='resource_group_id' || - word=='resource_type' || - word=='revision_id' || - word=='revision_timestamp' || - word=='size' || - word=='size_extra' || - word=='state' || - word=='url' || - word=='webstore_last_updated' || - word=='webstore_url'; - }, - /* - * Called when my model is destroyed. Remove me from the page. - */ - removeFromDom: function() { - this.li.remove(); - this.table.remove(); - }, - onDatastoreEnabledChange: function(e) { - var isChecked = this.table.find('.js-datastore-enabled-checkbox').prop('checked'); - var webstore_url = isChecked ? 'enabled' : null; - this.model.set({webstore_url: webstore_url}); - this.table.find('.js-datastore-enabled-text').val(webstore_url); - } -}); - - -/* ============================================= */ -/* Backbone View: Add Resource by uploading file */ -/* ============================================= */ -CKAN.View.ResourceAddUpload = Backbone.View.extend({ - tagName: 'div', - - initialize: function(options) { - this.el = $(this.el); - _.bindAll(this, 'render', 'updateFormData', 'setMessage', 'uploadFile'); - $(CKAN.Templates.resourceUpload).appendTo(this.el); - this.$messages = this.el.find('.alert'); - this.setupFileUpload(); - }, - - events: { - 'click input[type="submit"]': 'uploadFile' - }, - - setupFileUpload: function() { - var self = this; - this.el.find('.fileupload').fileupload({ - // needed because we are posting to remote url - forceIframeTransport: true, - replaceFileInput: false, - autoUpload: false, - fail: function(e, data) { - alert('Upload failed'); - }, - add: function(e,data) { - self.fileData = data; - self.fileUploadData = data; - self.key = self.makeUploadKey(data.files[0].name); - self.updateFormData(self.key); - }, - send: function(e, data) { - self.setMessage(CKAN.Strings.uploadingFile +' '); - }, - done: function(e, data) { - self.onUploadComplete(self.key); - } - }) - }, - - ISODateString: function(d) { - function pad(n) {return n<10 ? '0'+n : n}; - return d.getUTCFullYear()+'-' - + pad(d.getUTCMonth()+1)+'-' - + pad(d.getUTCDate())+'T' - + pad(d.getUTCHours())+':' - + pad(d.getUTCMinutes())+':' - + pad(d.getUTCSeconds()); - }, - - // Create an upload key/label for this file. - // - // Form: {current-date}/file-name. Do not just use the file name as this - // would lead to collisions. - // (Could add userid/username and/or a small random string to reduce - // collisions but chances seem very low already) - makeUploadKey: function(fileName) { - // google storage replaces ' ' with '+' which breaks things - // See http://trac.ckan.org/ticket/1518 for more. - var corrected = fileName.replace(/ /g, '-'); - // note that we put hh mm ss as hhmmss rather hh:mm:ss (former is 'basic - // format') - var now = new Date(); - // replace ':' with nothing - var str = this.ISODateString(now).replace(':', '').replace(':', ''); - return str + '/' + corrected; - }, - - updateFormData: function(key) { - var self = this; - self.setMessage(CKAN.Strings.checkingUploadPermissions + ' '); - self.el.find('.fileinfo').text(key); - $.ajax({ - url: CKAN.SITE_URL + '/api/storage/auth/form/' + key, - async: false, - success: function(data) { - self.el.find('form').attr('action', data.action); - _tmpl = ''; - var $hidden = $(self.el.find('form div.hidden-inputs')[0]); - $.each(data.fields, function(idx, item) { - $hidden.append($.tmpl(_tmpl, item)); - }); - self.hideMessage(); - }, - error: function(jqXHR, textStatus, errorThrown) { - // TODO: more graceful error handling (e.g. of 409) - self.setMessage(CKAN.Strings.failedToGetCredentialsForUpload, 'error'); - self.el.find('input[name="add-resource-upload"]').hide(); - } - }); - }, - - uploadFile: function(e) { - e.preventDefault(); - if (!this.fileData) { - alert('No file selected'); - return; - } - var jqXHR = this.fileData.submit(); - }, - - onUploadComplete: function(key) { - var self = this; - $.ajax({ - url: CKAN.SITE_URL + '/api/storage/metadata/' + self.key, - success: function(data) { - var name = data._label; - if (name && name.length > 0 && name[0] === '/') { - name = name.slice(1); - } - var d = new Date(data._last_modified); - var lastmod = self.ISODateString(d); - var newResource = new CKAN.Model.Resource({}); - newResource.set({ - url: data._location - , name: name - , size: data._content_length - , last_modified: lastmod - , format: data._format - , mimetype: data._format - , resource_type: 'file.upload' - , owner: data['uploaded-by'] - , hash: data._checksum - , cache_url: data._location - , cache_url_updated: lastmod - , webstore_url: data._location - } - , { - error: function(model, error) { - var msg = 'Filed uploaded OK but error adding resource: ' + error + '.'; - msg += 'You may need to create a resource directly. Uploaded file at: ' + data._location; - self.setMessage(msg, 'error'); - } - } - ); - self.collection.add(newResource); - self.setMessage('File uploaded OK and resource added', 'success'); - } - }); - }, - - setMessage: function(msg, category) { - var category = category || 'alert-info'; - this.$messages.removeClass('alert-info alert-success alert-error'); - this.$messages.addClass(category); - this.$messages.show(); - this.$messages.html(msg); - }, - - hideMessage: function() { - this.$messages.hide('slow'); - } -}); - -/* ======================================== */ -/* == Backbone View: Add resource by URL == */ -/* ======================================== */ -CKAN.View.ResourceAddUrl = Backbone.View.extend({ - initialize: function(options) { - _.bindAll(this, 'clickSubmit'); - }, - - clickSubmit: function(e) { - e.preventDefault(); - - var self = this; - var newResource = new CKAN.Model.Resource({}); - - this.el.find('input[name="add-resource-save"]').addClass("disabled"); - var urlVal = this.el.find('input[name="add-resource-url"]').val(); - var qaEnabled = $.inArray('qa',CKAN.plugins)>=0; - - if(qaEnabled && this.options.mode=='file') { - $.ajax({ - url: CKAN.SITE_URL + '/qa/link_checker', - context: newResource, - data: {url: urlVal}, - dataType: 'json', - error: function(){ - newResource.set({url: urlVal, resource_type: 'file'}); - self.collection.add(newResource); - self.resetForm(); - }, - success: function(data){ - data = data[0]; - newResource.set({ - url: urlVal, - resource_type: 'file', - format: data.format, - size: data.size, - mimetype: data.mimetype, - last_modified: data.last_modified, - webstore_url: 'enabled', - url_error: (data.url_errors || [""])[0] - }); - self.collection.add(newResource); - self.resetForm(); - } - }); - } - else { - newResource.set({url: urlVal, resource_type: this.options.mode}); - if (newResource.get('resource_type')=='file') { - newResource.set({webstore_url: 'enabled'}); - } - this.collection.add(newResource); - this.resetForm(); - } - }, - - resetForm: function() { - this.el.find('input[name="add-resource-save"]').removeClass("disabled"); - this.el.find('input[name="add-resource-url"]').val(''); - }, - - events: { - 'click .btn': 'clickSubmit' - } -}); - - - -/* ================ */ -/* == CKAN.Utils == */ -/* ================ */ -CKAN.Utils = function($, my) { - // Animate the appearance of an element by expanding its height - my.animateHeight = function(element, animTime) { - if (!animTime) { animTime = 350; } - element.show(); - var finalHeight = element.height(); - element.height(0); - element.animate({height:finalHeight}, animTime); - }; - - my.bindInputChanges = function(input, callback) { - input.keyup(callback); - input.keydown(callback); - input.keypress(callback); - input.change(callback); - }; - - my.setupDatasetDeleteButton = function() { - var select = $('select.dataset-delete'); - select.attr('disabled','disabled'); - select.css({opacity: 0.3}); - $('button.dataset-delete').click(function(e) { - select.removeAttr('disabled'); - select.fadeTo('fast',1.0); - $(e.target).css({opacity:0}); - $(e.target).attr('disabled','disabled'); - return false; - }); - }; - - // Attach dataset autocompletion to provided elements - // - // Requires: jquery-ui autocomplete - my.setupPackageAutocomplete = function(elements) { - elements.autocomplete({ - minLength: 0, - source: function(request, callback) { - var url = '/dataset/autocomplete?q=' + request.term; - $.ajax({ - url: url, - success: function(data) { - // atm is a string with items broken by \n and item = title (name)|name - var out = []; - var items = data.split('\n'); - $.each(items, function(idx, value) { - var _tmp = value.split('|'); - var _newItem = { - label: _tmp[0], - value: _tmp[1] - }; - out.push(_newItem); - }); - callback(out); - } - }); - } - , select: function(event, ui) { - var input_box = $(this); - input_box.val(''); - var old_name = input_box.attr('name'); - var field_name_regex = /^(\S+)__(\d+)__(\S+)$/; - var split = old_name.match(field_name_regex); - - var new_name = split[1] + '__' + (parseInt(split[2],10) + 1) + '__' + split[3]; - - input_box.attr('name', new_name); - input_box.attr('id', new_name); - - var $new = $('

    '); - $new.append($('').attr('name', old_name).val(ui.item.value)); - $new.append(' '); - $new.append(ui.item.label); - input_box.after($new); - - // prevent setting value in autocomplete box - return false; - } - }); - }; - - // Attach tag autocompletion to provided elements - // - // Requires: jquery-ui autocomplete - my.setupTagAutocomplete = function(elements) { - // don't navigate away from the field on tab when selecting an item - elements.bind( "keydown", - function( event ) { - if ( event.keyCode === $.ui.keyCode.TAB && $( this ).data( "autocomplete" ).menu.active ) { - event.preventDefault(); - } - } - ).autocomplete({ - minLength: 1, - source: function(request, callback) { - // here request.term is whole list of tags so need to get last - var _realTerm = $.trim(request.term.split(',').pop()); - var url = CKAN.SITE_URL + '/api/2/util/tag/autocomplete?incomplete=' + _realTerm; - $.getJSON(url, function(data) { - // data = { ResultSet: { Result: [ {Name: tag} ] } } (Why oh why?) - var tags = $.map(data.ResultSet.Result, function(value, idx) { - return value.Name; - }); - callback( $.ui.autocomplete.filter(tags, _realTerm) ); - }); - }, - focus: function() { - // prevent value inserted on focus - return false; - }, - select: function( event, ui ) { - var terms = this.value.split(','); - // remove the current input - terms.pop(); - // add the selected item - terms.push( " "+ui.item.value ); - // add placeholder to get the comma-and-space at the end - terms.push( " " ); - this.value = terms.join( "," ); - return false; - } - }); - }; - - // Attach tag autocompletion to provided elements - // - // Requires: jquery-ui autocomplete - my.setupFormatAutocomplete = function(elements) { - elements.autocomplete({ - minLength: 1, - source: function(request, callback) { - var url = CKAN.SITE_URL + '/api/2/util/resource/format_autocomplete?incomplete=' + request.term; - $.getJSON(url, function(data) { - // data = { ResultSet: { Result: [ {Name: tag} ] } } (Why oh why?) - var formats = $.map(data.ResultSet.Result, function(value, idx) { - return value.Format; - }); - callback(formats); - }); - } - }); - }; - - my.setupOrganizationUserAutocomplete = function(elements) { - elements.autocomplete({ - minLength: 2, - source: function(request, callback) { - var url = '/api/2/util/user/autocomplete?q=' + request.term; - $.getJSON(url, function(data) { - $.each(data, function(idx, userobj) { - var label = userobj.name; - if (userobj.fullname) { - label += ' [' + userobj.fullname + ']'; - } - userobj.label = label; - userobj.value = userobj.name; - }); - callback(data); - }); - }, - select: function(event, ui) { - var input_box = $(this); - input_box.val(''); - var parent_dd = input_box.parent('dd'); - var old_name = input_box.attr('name'); - var field_name_regex = /^(\S+)__(\d+)__(\S+)$/; - var split = old_name.match(field_name_regex); - - var new_name = split[1] + '__' + (parseInt(split[2],10) + 1) + '__' + split[3]; - input_box.attr('name', new_name); - input_box.attr('id', new_name); - - parent_dd.before( - '' + - '' + - '
    ' + ui.item.label + '
    ' - ); - - return false; // to cancel the event ;) - } - }); - }; - - - // Attach user autocompletion to provided elements - // - // Requires: jquery-ui autocomplete - my.setupUserAutocomplete = function(elements) { - elements.autocomplete({ - minLength: 2, - source: function(request, callback) { - var url = CKAN.SITE_URL + '/api/2/util/user/autocomplete?q=' + request.term; - $.getJSON(url, function(data) { - $.each(data, function(idx, userobj) { - var label = userobj.name; - if (userobj.fullname) { - label += ' [' + userobj.fullname + ']'; - } - userobj.label = label; - userobj.value = userobj.name; - }); - callback(data); - }); - } - }); - }; - - - my.relatedSetup = function(form) { - $('[rel=popover]').popover(); - - function addAlert(msg) { - $('
    ').html(msg).hide().prependTo(form).fadeIn(); - } - - function relatedRequest(action, method, data) { - return $.ajax({ - type: method, - dataType: 'json', - contentType: 'application/json', - url: CKAN.SITE_URL + '/api/3/action/related_' + action, - data: data ? JSON.stringify(data) : undefined, - error: function(err, txt, w) { - // This needs to be far more informative. - addAlert('Error: Unable to ' + action + ' related item'); - } - }); - } - - // Center thumbnails vertically. - var relatedItems = $('.related-items'); - relatedItems.find('li').each(function () { - var item = $(this), description = item.find('.description'); - - function vertiallyAlign() { - var img = $(this), - height = img.height(), - parent = img.parent().height(), - top = (height - parent) / 2; - - if (parent < height) { - img.css('margin-top', -top); - } - } - - item.find('img').load(vertiallyAlign); - description.data('height', description.height()).truncate(); - }); - - relatedItems.on('mouseenter mouseleave', '.description.truncated', function (event) { - var isEnter = event.type === 'mouseenter' - description = $(this) - timer = description.data('hover-intent'); - - function update() { - var parent = description.parents('li:first'), - difference = description.data('height') - description.height(); - - description.truncate(isEnter ? 'expand' : 'collapse'); - parent.toggleClass('expanded-description', isEnter); - - // Adjust the bottom margin of the item relative to it's current value - // to allow the description to expand without breaking the grid. - parent.css('margin-bottom', isEnter ? '-=' + difference + 'px' : ''); - description.removeData('hover-intent'); - } - - if (!isEnter && timer) { - // User has moused out in the time set so cancel the action. - description.removeData('hover-intent'); - return clearTimeout(timer); - } else if (!isEnter && !timer) { - update(); - } else { - // Delay the hover action slightly to wait to see if the user mouses - // out again. This prevents unwanted actions. - description.data('hover-intent', setTimeout(update, 200)); - } - }); - - // Add a handler for the delete buttons. - relatedItems.on('click', '[data-action=delete]', function (event) { - var id = $(this).data('relatedId'); - relatedRequest('delete', 'POST', {id: id}).done(function () { - $('#related-item-' + id).remove(); - }); - event.preventDefault(); - }); - - $(form).submit(function (event) { - event.preventDefault(); - - // Validate the form - var form = $(this), data = {}; - jQuery.each(form.serializeArray(), function () { - data[this.name] = this.value; - }); - - form.find('.alert').remove(); - form.find('.error').removeClass('error'); - if (!data.title) { - addAlert('Missing field: A title is required'); - $('[name=title]').parent().addClass('error'); - return; - } - if (!data.url) { - addAlert('Missing field: A url is required'); - $('[name=url]').parent().addClass('error'); - return; - } - - relatedRequest('create', this.method, data).done(function () { - // TODO: Insert item dynamically. - window.location.reload(); - }); - }); - }; - - // Attach authz group autocompletion to provided elements - // - // Requires: jquery-ui autocomplete - my.setupAuthzGroupAutocomplete = function(elements) { - elements.autocomplete({ - minLength: 2, - source: function(request, callback) { - var url = CKAN.SITE_URL + '/api/2/util/authorizationgroup/autocomplete?q=' + request.term; - $.getJSON(url, function(data) { - $.each(data, function(idx, userobj) { - var label = userobj.name; - userobj.label = label; - userobj.value = userobj.name; - }); - callback(data); - }); - } - }); - }; - - my.setupGroupAutocomplete = function(elements) { - elements.autocomplete({ - minLength: 2, - source: function(request, callback) { - var url = CKAN.SITE_URL + '/api/2/util/group/autocomplete?q=' + request.term; - $.getJSON(url, function(data) { - $.each(data, function(idx, userobj) { - var label = userobj.name; - userobj.label = label; - userobj.value = userobj.name; - }); - callback(data); - }); - } - }); - }; - - my.setupMarkdownEditor = function(markdownEditor) { - // Markdown editor hooks - markdownEditor.find('button, div.markdown-preview').live('click', function(e) { - e.preventDefault(); - var $target = $(e.target); - // Extract neighbouring elements - var markdownEditor=$target.closest('.markdown-editor'); - markdownEditor.find('button').removeClass('depressed'); - var textarea = markdownEditor.find('.markdown-input'); - var preview = markdownEditor.find('.markdown-preview'); - // Toggle the preview - if ($target.is('.js-markdown-preview')) { - $target.addClass('depressed'); - raw_markdown=textarea.val(); - preview.html(""+CKAN.Strings.loading+""); - $.post(CKAN.SITE_URL + "/api/util/markdown", { q: raw_markdown }, - function(data) { preview.html(data); } - ); - preview.width(textarea.width()); - preview.height(textarea.height()); - textarea.hide(); - preview.show(); - } else { - markdownEditor.find('.js-markdown-edit').addClass('depressed'); - textarea.show(); - preview.hide(); - textarea.focus(); - } - return false; - }); - }; - - // If notes field is more than 1 paragraph, just show the - // first paragraph with a 'Read more' link that will expand - // the div if clicked - my.setupNotesExtract = function() { - var notes = $('#content div.notes'); - var paragraphs = notes.find('#notes-extract > *'); - if (paragraphs.length===0) { - notes.hide(); - } - else if (paragraphs.length > 1) { - var remainder = notes.find('#notes-remainder'); - $.each(paragraphs,function(i,para) { - if (i > 0) { remainder.append($(para).remove()); } - }); - var finalHeight = remainder.height(); - remainder.height(0); - notes.find('#notes-toggle').show(); - notes.find('#notes-toggle button').click( - function(event){ - notes.find('#notes-toggle button').toggle(); - if ($(event.target).hasClass('more')) { - remainder.animate({'height':finalHeight}); - } - else { - remainder.animate({'height':0}); - } - return false; - } - ); - } - }; - - my.warnOnFormChanges = function() { - var boundToUnload = false; - return function($form) { - var flashWarning = function() { - if (boundToUnload) { return; } - boundToUnload = true; - // Bind to the window departure event - window.onbeforeunload = function () { - return CKAN.Strings.youHaveUnsavedChanges; - }; - }; - // Hook form modifications to flashWarning - $form.find('input,select').live('change', function(e) { - $target = $(e.target); - // Entering text in the 'add' box does not represent a change - if ($target.closest('.resource-add').length===0) { - flashWarning(); - } - }); - // Don't stop us leaving - $form.submit(function() { - window.onbeforeunload = null; - }); - // Calling functions might hook to flashWarning - return flashWarning; - }; - }(); - - my.countObject = function(obj) { - var count=0; - $.each(obj, function() { - count++; - }); - return count; - }; - - function followButtonClicked(event) { - var button = event.currentTarget; - if (button.id === 'user_follow_button') { - var object_type = 'user'; - } else if (button.id === 'dataset_follow_button') { - var object_type = 'dataset'; - } - else { - // This shouldn't happen. - return; - } - var object_id = button.getAttribute('data-obj-id'); - if (button.getAttribute('data-state') === "follow") { - if (object_type == 'user') { - var url = '/api/action/follow_user'; - } else if (object_type == 'dataset') { - var url = '/api/action/follow_dataset'; - } else { - // This shouldn't happen. - return; - } - var data = JSON.stringify({ - id: object_id, - }); - var nextState = 'unfollow'; - var nextString = CKAN.Strings.unfollow; - } else if (button.getAttribute('data-state') === "unfollow") { - if (object_type == 'user') { - var url = '/api/action/unfollow_user'; - } else if (object_type == 'dataset') { - var url = '/api/action/unfollow_dataset'; - } else { - // This shouldn't happen. - return; - } - var data = JSON.stringify({ - id: object_id, - }); - var nextState = 'follow'; - var nextString = CKAN.Strings.follow; - } - else { - // This shouldn't happen. - return; - } - $.ajax({ - contentType: 'application/json', - url: url, - data: data, - dataType: 'json', - processData: false, - type: 'POST', - success: function(data) { - button.setAttribute('data-state', nextState); - button.innerHTML = nextString; - }, - }); - }; - - // This only needs to happen on dataset pages, but it doesn't seem to do - // any harm to call it anyway. - $('#user_follow_button').on('click', followButtonClicked); - $('#dataset_follow_button').on('click', followButtonClicked); - - return my; -}(jQuery, CKAN.Utils || {}); - - - -/* ==================== */ -/* == Data Previewer == */ -/* ==================== */ -CKAN.DataPreview = function ($, my) { - my.jsonpdataproxyUrl = 'http://jsonpdataproxy.appspot.com/'; - my.dialogId = 'ckanext-datapreview'; - my.$dialog = $('#' + my.dialogId); - - // **Public: Loads a data previewer for an embedded page** - // - // Uses the provided reclineState to restore the Dataset. Creates a single - // view for the Dataset (the one defined by reclineState.currentView). And - // then passes the constructed Dataset, the constructed View, and the - // reclineState into the DataExplorer constructor. - my.loadEmbeddedPreview = function(resourceData, reclineState) { - my.$dialog.html('

    Loading ...

    '); - - // Restore the Dataset from the given reclineState. - var dataset = recline.Model.Dataset.restore(reclineState); - - // Only create the view defined in reclineState.currentView. - // TODO: tidy this up. - var views = null; - if (reclineState.currentView === 'grid') { - views = [ { - id: 'grid', - label: 'Grid', - view: new recline.View.Grid({ - model: dataset, - state: reclineState['view-grid'] - }) - }]; - } else if (reclineState.currentView === 'graph') { - views = [ { - id: 'graph', - label: 'Graph', - view: new recline.View.Graph({ - model: dataset, - state: reclineState['view-graph'] - }) - }]; - } else if (reclineState.currentView === 'map') { - views = [ { - id: 'map', - label: 'Map', - view: new recline.View.Map({ - model: dataset, - state: reclineState['view-map'] - }) - }]; - } - - // Finally, construct the DataExplorer. Again, passing in the reclineState. - var dataExplorer = new recline.View.DataExplorer({ - el: my.$dialog, - model: dataset, - state: reclineState, - views: views - }); - - Backbone.history.start(); - }; - - // **Public: Creates a link to the embeddable page. - // - // For a given DataExplorer state, this function constructs and returns the - // url to the embeddable view of the current dataexplorer state. - my.makeEmbedLink = function(explorerState) { - var state = explorerState.toJSON(); - state.state_version = 1; - - var queryString = '?'; - var items = []; - $.each(state, function(key, value) { - if (typeof(value) === 'object') { - value = JSON.stringify(value); - } - items.push(key + '=' + escape(value)); - }); - queryString += items.join('&'); - return embedPath + queryString; - }; - - // **Public: Loads a data preview** - // - // Fetches the preview data object from the link provided and loads the - // parsed data from the webstore displaying it in the most appropriate - // manner. - // - // link - Preview button. - // - // Returns nothing. - my.loadPreviewDialog = function(resourceData) { - my.$dialog.html('

    Loading ...

    '); - - function initializeDataExplorer(dataset) { - var views = [ - { - id: 'grid', - label: 'Grid', - view: new recline.View.Grid({ - model: dataset - }) - }, - { - id: 'graph', - label: 'Graph', - view: new recline.View.Graph({ - model: dataset - }) - }, - { - id: 'map', - label: 'Map', - view: new recline.View.Map({ - model: dataset - }) - } - ]; - var dataExplorer = new recline.View.DataExplorer({ - el: my.$dialog, - model: dataset, - views: views, - config: { - readOnly: true - } - }); - - // ----------------------------- - // Setup the Embed modal dialog. - // ----------------------------- - - // embedLink holds the url to the embeddable view of the current DataExplorer state. - var embedLink = $('.embedLink'); - - // embedIframeText contains the '', - { - link: link.replace(/"/g, '"'), - width: width, - height: height - })); - embedLink.attr('href', link); - } - - // Bind changes to the DataExplorer, or the two width and height inputs - // to re-calculate the url. - dataExplorer.state.bind('change', updateLink); - for (var i=0; i 1) { - resourceData.formatNormalized = ext[ext.length-1]; - } - } - - if (resourceData.webstore_url) { - resourceData.elasticsearch_url = '/api/data/' + resourceData.id; - var dataset = new recline.Model.Dataset(resourceData, 'elasticsearch'); - initializeDataExplorer(dataset); - } - else if (resourceData.formatNormalized in {'csv': '', 'xls': ''}) { - // set format as this is used by Recline in setting format for DataProxy - resourceData.format = resourceData.formatNormalized; - var dataset = new recline.Model.Dataset(resourceData, 'dataproxy'); - initializeDataExplorer(dataset); - $('.recline-query-editor .text-query').hide(); - } - else if (resourceData.formatNormalized in { - 'rdf+xml': '', - 'owl+xml': '', - 'xml': '', - 'n3': '', - 'n-triples': '', - 'turtle': '', - 'plain': '', - 'atom': '', - 'tsv': '', - 'rss': '', - 'txt': '' - }) { - // HACK: treat as plain text / csv - // pass url to jsonpdataproxy so we can load remote data (and tell dataproxy to treat as csv!) - var _url = my.jsonpdataproxyUrl + '?type=csv&url=' + resourceData.url; - my.getResourceDataDirect(_url, function(data) { - my.showPlainTextData(data); - }); - } - else if (resourceData.formatNormalized in {'html':'', 'htm':''} - || resourceData.url.substring(0,23)=='http://docs.google.com/') { - // we displays a fullscreen dialog with the url in an iframe. - my.$dialog.empty(); - var el = $(''); - el.attr('src', resourceData.url); - el.attr('width', '100%'); - el.attr('height', '100%'); - my.$dialog.append(el); - } - // images - else if (resourceData.formatNormalized in {'png':'', 'jpg':'', 'gif':''} - || resourceData.resource_type=='image') { - // we displays a fullscreen dialog with the url in an iframe. - my.$dialog.empty(); - var el = $(''); - el.attr('src', resourceData.url); - el.css('max-width', '100%'); - el.css('border', 'solid 4px black'); - my.$dialog.append(el); - } - else { - // Cannot reliably preview this item - with no mimetype/format information, - // can't guarantee it's not a remote binary file such as an executable. - my.showError({ - title: CKAN.Strings.previewNotAvailableForDataType + resourceData.formatNormalized, - message: '' - }); - } - }; - - // Public: Requests the formatted resource data from the webstore and - // passes the data into the callback provided. - // - // preview - A preview object containing resource metadata. - // callback - A Function to call with the data when loaded. - // - // Returns nothing. - my.getResourceDataDirect = function(url, callback) { - // $.ajax() does not call the "error" callback for JSONP requests so we - // set a timeout to provide the callback with an error after x seconds. - var timeout = 5000; - var timer = setTimeout(function error() { - callback({ - error: { - title: 'Request Error', - message: 'Dataproxy server did not respond after ' + (timeout / 1000) + ' seconds' - } - }); - }, timeout); - - // have to set jsonp because webstore requires _callback but that breaks jsonpdataproxy - var jsonp = '_callback'; - if (url.indexOf('jsonpdataproxy') != -1) { - jsonp = 'callback'; - } - - // We need to provide the `cache: true` parameter to prevent jQuery appending - // a cache busting `={timestamp}` parameter to the query as the webstore - // currently cannot handle custom parameters. - $.ajax({ - url: url, - cache: true, - dataType: 'jsonp', - jsonp: jsonp, - success: function(data) { - clearTimeout(timer); - callback(data); - } - }); - }; - - // Public: Displays a String of data in a fullscreen dialog. - // - // data - An object of parsed CSV data returned by the webstore. - // - // Returns nothing. - my.showPlainTextData = function(data) { - if(data.error) { - my.showError(data.error); - } else { - var content = $('
    ');
    -      for (var i=0; i<%= title %>
    <%= message %>
    ', - error - ); - my.$dialog.html(_html); - }; - - my.normalizeFormat = function(format) { - var out = format.toLowerCase(); - out = out.split('/'); - out = out[out.length-1]; - return out; - }; - - my.normalizeUrl = function(url) { - if (url.indexOf('https') === 0) { - return 'http' + url.slice(5); - } else { - return url; - } - } - - // Public: Escapes HTML entities to prevent broken layout and XSS attacks - // when inserting user generated or external content. - // - // string - A String of HTML. - // - // Returns a String with HTML special characters converted to entities. - my.escapeHTML = function (string) { - return string.replace(/&(?!\w+;|#\d+;|#x[\da-f]+;)/gi, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, ''') - .replace(/\//g,'/'); - }; - - - // Export the CKANEXT object onto the window. - $.extend(true, window, {CKANEXT: {}}); - CKANEXT.DATAPREVIEW = my; - return my; -}(jQuery, CKAN.DataPreview || {}); - diff --git a/ckan/html_resources/datapreview/templates.js b/ckan/html_resources/datapreview/templates.js deleted file mode 100644 index 6ecad116a6b..00000000000 --- a/ckan/html_resources/datapreview/templates.js +++ /dev/null @@ -1,169 +0,0 @@ -var CKAN = CKAN || {}; -CKAN.Templates = CKAN.Templates || {}; - -CKAN.Templates.resourceUpload = ' \ -
    \ -
    \ - \ -
    \ - \ -
    \ -
    \ - \ -
    \ - \ -
    '; - - - -CKAN.Templates.resourceEntry = ' \ -
  • \ - \ - \ - ${resource.name}\ - \ -
  • '; - -var youCanUseMarkdownString = CKAN.Strings.youCanUseMarkdown.replace('%a', '').replace('%b', ''); -var shouldADataStoreBeEnabledString = CKAN.Strings.shouldADataStoreBeEnabled.replace('%a', '').replace('%b', ''); -var datesAreInISOString = CKAN.Strings.datesAreInISO.replace('%a', '').replace('%b', '').replace('%c', '').replace('%d', ''); - -// TODO it would be nice to unify this with the markdown editor specified in helpers.py -CKAN.Templates.resourceDetails = ' \ - ', + error + ); + my.$dialog.html(_html); + }; + + my.normalizeFormat = function(format) { + var out = format.toLowerCase(); + out = out.split('/'); + out = out[out.length-1]; + return out; + }; + + my.normalizeUrl = function(url) { + if (url.indexOf('https') === 0) { + return 'http' + url.slice(5); + } else { + return url; + } + } + + // Public: Escapes HTML entities to prevent broken layout and XSS attacks + // when inserting user generated or external content. + // + // string - A String of HTML. + // + // Returns a String with HTML special characters converted to entities. + my.escapeHTML = function (string) { + return string.replace(/&(?!\w+;|#\d+;|#x[\da-f]+;)/gi, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') + .replace(/\//g,'/'); + }; + + + // Export the CKANEXT object onto the window. + $.extend(true, window, {CKANEXT: {}}); + CKANEXT.DATAPREVIEW = my; + return my; +}(jQuery, CKAN.DataPreview || {}); + diff --git a/ckan/html_resources/datapreview/table-view-template.js b/ckan/public/base/datapreview/javascript/table-view-template.js similarity index 100% rename from ckan/html_resources/datapreview/table-view-template.js rename to ckan/public/base/datapreview/javascript/table-view-template.js diff --git a/ckan/html_resources/datapreview/table-view.js b/ckan/public/base/datapreview/javascript/table-view.js similarity index 100% rename from ckan/html_resources/datapreview/table-view.js rename to ckan/public/base/datapreview/javascript/table-view.js diff --git a/ckan/html_resources/datapreview/table-view.ui.js b/ckan/public/base/datapreview/javascript/table-view.ui.js similarity index 100% rename from ckan/html_resources/datapreview/table-view.ui.js rename to ckan/public/base/datapreview/javascript/table-view.ui.js diff --git a/ckan/public/base/datapreview/javascript/vendor/backbone.js b/ckan/public/base/datapreview/javascript/vendor/backbone.js new file mode 100644 index 00000000000..18d484386b0 --- /dev/null +++ b/ckan/public/base/datapreview/javascript/vendor/backbone.js @@ -0,0 +1,1149 @@ +// Backbone.js 0.5.1 +// (c) 2010 Jeremy Ashkenas, DocumentCloud Inc. +// Backbone may be freely distributed under the MIT license. +// For all details and documentation: +// http://documentcloud.github.com/backbone + +(function(){ + + // Initial Setup + // ------------- + + // Save a reference to the global object. + var root = this; + + // Save the previous value of the `Backbone` variable. + var previousBackbone = root.Backbone; + + // The top-level namespace. All public Backbone classes and modules will + // be attached to this. Exported for both CommonJS and the browser. + var Backbone; + if (typeof exports !== 'undefined') { + Backbone = exports; + } else { + Backbone = root.Backbone = {}; + } + + // Current version of the library. Keep in sync with `package.json`. + Backbone.VERSION = '0.5.1'; + + // Require Underscore, if we're on the server, and it's not already present. + var _ = root._; + if (!_ && (typeof require !== 'undefined')) _ = require('underscore')._; + + // For Backbone's purposes, jQuery or Zepto owns the `$` variable. + var $ = root.jQuery || root.Zepto; + + // Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable + // to its previous owner. Returns a reference to this Backbone object. + Backbone.noConflict = function() { + root.Backbone = previousBackbone; + return this; + }; + + // Turn on `emulateHTTP` to use support legacy HTTP servers. Setting this option will + // fake `"PUT"` and `"DELETE"` requests via the `_method` parameter and set a + // `X-Http-Method-Override` header. + Backbone.emulateHTTP = false; + + // Turn on `emulateJSON` to support legacy servers that can't deal with direct + // `application/json` requests ... will encode the body as + // `application/x-www-form-urlencoded` instead and will send the model in a + // form param named `model`. + Backbone.emulateJSON = false; + + // Backbone.Events + // ----------------- + + // A module that can be mixed in to *any object* in order to provide it with + // custom events. You may `bind` or `unbind` a callback function to an event; + // `trigger`-ing an event fires all callbacks in succession. + // + // var object = {}; + // _.extend(object, Backbone.Events); + // object.bind('expand', function(){ alert('expanded'); }); + // object.trigger('expand'); + // + Backbone.Events = { + + // Bind an event, specified by a string name, `ev`, to a `callback` function. + // Passing `"all"` will bind the callback to all events fired. + bind : function(ev, callback) { + var calls = this._callbacks || (this._callbacks = {}); + var list = calls[ev] || (calls[ev] = []); + list.push(callback); + return this; + }, + + // Remove one or many callbacks. If `callback` is null, removes all + // callbacks for the event. If `ev` is null, removes all bound callbacks + // for all events. + unbind : function(ev, callback) { + var calls; + if (!ev) { + this._callbacks = {}; + } else if (calls = this._callbacks) { + if (!callback) { + calls[ev] = []; + } else { + var list = calls[ev]; + if (!list) return this; + for (var i = 0, l = list.length; i < l; i++) { + if (callback === list[i]) { + list[i] = null; + break; + } + } + } + } + return this; + }, + + // Trigger an event, firing all bound callbacks. Callbacks are passed the + // same arguments as `trigger` is, apart from the event name. + // Listening for `"all"` passes the true event name as the first argument. + trigger : function(eventName) { + var list, calls, ev, callback, args; + var both = 2; + if (!(calls = this._callbacks)) return this; + while (both--) { + ev = both ? eventName : 'all'; + if (list = calls[ev]) { + for (var i = 0, l = list.length; i < l; i++) { + if (!(callback = list[i])) { + list.splice(i, 1); i--; l--; + } else { + args = both ? Array.prototype.slice.call(arguments, 1) : arguments; + callback.apply(this, args); + } + } + } + } + return this; + } + + }; + + // Backbone.Model + // -------------- + + // Create a new model, with defined attributes. A client id (`cid`) + // is automatically generated and assigned for you. + Backbone.Model = function(attributes, options) { + var defaults; + attributes || (attributes = {}); + if (defaults = this.defaults) { + if (_.isFunction(defaults)) defaults = defaults(); + attributes = _.extend({}, defaults, attributes); + } + this.attributes = {}; + this._escapedAttributes = {}; + this.cid = _.uniqueId('c'); + this.set(attributes, {silent : true}); + this._changed = false; + this._previousAttributes = _.clone(this.attributes); + if (options && options.collection) this.collection = options.collection; + this.initialize(attributes, options); + }; + + // Attach all inheritable methods to the Model prototype. + _.extend(Backbone.Model.prototype, Backbone.Events, { + + // A snapshot of the model's previous attributes, taken immediately + // after the last `"change"` event was fired. + _previousAttributes : null, + + // Has the item been changed since the last `"change"` event? + _changed : false, + + // The default name for the JSON `id` attribute is `"id"`. MongoDB and + // CouchDB users may want to set this to `"_id"`. + idAttribute : 'id', + + // Initialize is an empty function by default. Override it with your own + // initialization logic. + initialize : function(){}, + + // Return a copy of the model's `attributes` object. + toJSON : function() { + return _.clone(this.attributes); + }, + + // Get the value of an attribute. + get : function(attr) { + return this.attributes[attr]; + }, + + // Get the HTML-escaped value of an attribute. + escape : function(attr) { + var html; + if (html = this._escapedAttributes[attr]) return html; + var val = this.attributes[attr]; + return this._escapedAttributes[attr] = escapeHTML(val == null ? '' : '' + val); + }, + + // Returns `true` if the attribute contains a value that is not null + // or undefined. + has : function(attr) { + return this.attributes[attr] != null; + }, + + // Set a hash of model attributes on the object, firing `"change"` unless you + // choose to silence it. + set : function(attrs, options) { + + // Extract attributes and options. + options || (options = {}); + if (!attrs) return this; + if (attrs.attributes) attrs = attrs.attributes; + var now = this.attributes, escaped = this._escapedAttributes; + + // Run validation. + if (!options.silent && this.validate && !this._performValidation(attrs, options)) return false; + + // Check for changes of `id`. + if (this.idAttribute in attrs) this.id = attrs[this.idAttribute]; + + // We're about to start triggering change events. + var alreadyChanging = this._changing; + this._changing = true; + + // Update attributes. + for (var attr in attrs) { + var val = attrs[attr]; + if (!_.isEqual(now[attr], val)) { + now[attr] = val; + delete escaped[attr]; + this._changed = true; + if (!options.silent) this.trigger('change:' + attr, this, val, options); + } + } + + // Fire the `"change"` event, if the model has been changed. + if (!alreadyChanging && !options.silent && this._changed) this.change(options); + this._changing = false; + return this; + }, + + // Remove an attribute from the model, firing `"change"` unless you choose + // to silence it. `unset` is a noop if the attribute doesn't exist. + unset : function(attr, options) { + if (!(attr in this.attributes)) return this; + options || (options = {}); + var value = this.attributes[attr]; + + // Run validation. + var validObj = {}; + validObj[attr] = void 0; + if (!options.silent && this.validate && !this._performValidation(validObj, options)) return false; + + // Remove the attribute. + delete this.attributes[attr]; + delete this._escapedAttributes[attr]; + if (attr == this.idAttribute) delete this.id; + this._changed = true; + if (!options.silent) { + this.trigger('change:' + attr, this, void 0, options); + this.change(options); + } + return this; + }, + + // Clear all attributes on the model, firing `"change"` unless you choose + // to silence it. + clear : function(options) { + options || (options = {}); + var attr; + var old = this.attributes; + + // Run validation. + var validObj = {}; + for (attr in old) validObj[attr] = void 0; + if (!options.silent && this.validate && !this._performValidation(validObj, options)) return false; + + this.attributes = {}; + this._escapedAttributes = {}; + this._changed = true; + if (!options.silent) { + for (attr in old) { + this.trigger('change:' + attr, this, void 0, options); + } + this.change(options); + } + return this; + }, + + // Fetch the model from the server. If the server's representation of the + // model differs from its current attributes, they will be overriden, + // triggering a `"change"` event. + fetch : function(options) { + options || (options = {}); + var model = this; + var success = options.success; + options.success = function(resp, status, xhr) { + if (!model.set(model.parse(resp, xhr), options)) return false; + if (success) success(model, resp); + }; + options.error = wrapError(options.error, model, options); + return (this.sync || Backbone.sync).call(this, 'read', this, options); + }, + + // Set a hash of model attributes, and sync the model to the server. + // If the server returns an attributes hash that differs, the model's + // state will be `set` again. + save : function(attrs, options) { + options || (options = {}); + if (attrs && !this.set(attrs, options)) return false; + var model = this; + var success = options.success; + options.success = function(resp, status, xhr) { + if (!model.set(model.parse(resp, xhr), options)) return false; + if (success) success(model, resp, xhr); + }; + options.error = wrapError(options.error, model, options); + var method = this.isNew() ? 'create' : 'update'; + return (this.sync || Backbone.sync).call(this, method, this, options); + }, + + // Destroy this model on the server if it was already persisted. Upon success, the model is removed + // from its collection, if it has one. + destroy : function(options) { + options || (options = {}); + if (this.isNew()) return this.trigger('destroy', this, this.collection, options); + var model = this; + var success = options.success; + options.success = function(resp) { + model.trigger('destroy', model, model.collection, options); + if (success) success(model, resp); + }; + options.error = wrapError(options.error, model, options); + return (this.sync || Backbone.sync).call(this, 'delete', this, options); + }, + + // Default URL for the model's representation on the server -- if you're + // using Backbone's restful methods, override this to change the endpoint + // that will be called. + url : function() { + var base = getUrl(this.collection) || this.urlRoot || urlError(); + if (this.isNew()) return base; + return base + (base.charAt(base.length - 1) == '/' ? '' : '/') + encodeURIComponent(this.id); + }, + + // **parse** converts a response into the hash of attributes to be `set` on + // the model. The default implementation is just to pass the response along. + parse : function(resp, xhr) { + return resp; + }, + + // Create a new model with identical attributes to this one. + clone : function() { + return new this.constructor(this); + }, + + // A model is new if it has never been saved to the server, and lacks an id. + isNew : function() { + return this.id == null; + }, + + // Call this method to manually fire a `change` event for this model. + // Calling this will cause all objects observing the model to update. + change : function(options) { + this.trigger('change', this, options); + this._previousAttributes = _.clone(this.attributes); + this._changed = false; + }, + + // Determine if the model has changed since the last `"change"` event. + // If you specify an attribute name, determine if that attribute has changed. + hasChanged : function(attr) { + if (attr) return this._previousAttributes[attr] != this.attributes[attr]; + return this._changed; + }, + + // Return an object containing all the attributes that have changed, or false + // if there are no changed attributes. Useful for determining what parts of a + // view need to be updated and/or what attributes need to be persisted to + // the server. + changedAttributes : function(now) { + now || (now = this.attributes); + var old = this._previousAttributes; + var changed = false; + for (var attr in now) { + if (!_.isEqual(old[attr], now[attr])) { + changed = changed || {}; + changed[attr] = now[attr]; + } + } + return changed; + }, + + // Get the previous value of an attribute, recorded at the time the last + // `"change"` event was fired. + previous : function(attr) { + if (!attr || !this._previousAttributes) return null; + return this._previousAttributes[attr]; + }, + + // Get all of the attributes of the model at the time of the previous + // `"change"` event. + previousAttributes : function() { + return _.clone(this._previousAttributes); + }, + + // Run validation against a set of incoming attributes, returning `true` + // if all is well. If a specific `error` callback has been passed, + // call that instead of firing the general `"error"` event. + _performValidation : function(attrs, options) { + var error = this.validate(attrs); + if (error) { + if (options.error) { + options.error(this, error, options); + } else { + this.trigger('error', this, error, options); + } + return false; + } + return true; + } + + }); + + // Backbone.Collection + // ------------------- + + // Provides a standard collection class for our sets of models, ordered + // or unordered. If a `comparator` is specified, the Collection will maintain + // its models in sort order, as they're added and removed. + Backbone.Collection = function(models, options) { + options || (options = {}); + if (options.comparator) this.comparator = options.comparator; + _.bindAll(this, '_onModelEvent', '_removeReference'); + this._reset(); + if (models) this.reset(models, {silent: true}); + this.initialize.apply(this, arguments); + }; + + // Define the Collection's inheritable methods. + _.extend(Backbone.Collection.prototype, Backbone.Events, { + + // The default model for a collection is just a **Backbone.Model**. + // This should be overridden in most cases. + model : Backbone.Model, + + // Initialize is an empty function by default. Override it with your own + // initialization logic. + initialize : function(){}, + + // The JSON representation of a Collection is an array of the + // models' attributes. + toJSON : function() { + return this.map(function(model){ return model.toJSON(); }); + }, + + // Add a model, or list of models to the set. Pass **silent** to avoid + // firing the `added` event for every new model. + add : function(models, options) { + if (_.isArray(models)) { + for (var i = 0, l = models.length; i < l; i++) { + this._add(models[i], options); + } + } else { + this._add(models, options); + } + return this; + }, + + // Remove a model, or a list of models from the set. Pass silent to avoid + // firing the `removed` event for every model removed. + remove : function(models, options) { + if (_.isArray(models)) { + for (var i = 0, l = models.length; i < l; i++) { + this._remove(models[i], options); + } + } else { + this._remove(models, options); + } + return this; + }, + + // Get a model from the set by id. + get : function(id) { + if (id == null) return null; + return this._byId[id.id != null ? id.id : id]; + }, + + // Get a model from the set by client id. + getByCid : function(cid) { + return cid && this._byCid[cid.cid || cid]; + }, + + // Get the model at the given index. + at: function(index) { + return this.models[index]; + }, + + // Force the collection to re-sort itself. You don't need to call this under normal + // circumstances, as the set will maintain sort order as each item is added. + sort : function(options) { + options || (options = {}); + if (!this.comparator) throw new Error('Cannot sort a set without a comparator'); + this.models = this.sortBy(this.comparator); + if (!options.silent) this.trigger('reset', this, options); + return this; + }, + + // Pluck an attribute from each model in the collection. + pluck : function(attr) { + return _.map(this.models, function(model){ return model.get(attr); }); + }, + + // When you have more items than you want to add or remove individually, + // you can reset the entire set with a new list of models, without firing + // any `added` or `removed` events. Fires `reset` when finished. + reset : function(models, options) { + models || (models = []); + options || (options = {}); + this.each(this._removeReference); + this._reset(); + this.add(models, {silent: true}); + if (!options.silent) this.trigger('reset', this, options); + return this; + }, + + // Fetch the default set of models for this collection, resetting the + // collection when they arrive. If `add: true` is passed, appends the + // models to the collection instead of resetting. + fetch : function(options) { + options || (options = {}); + var collection = this; + var success = options.success; + options.success = function(resp, status, xhr) { + collection[options.add ? 'add' : 'reset'](collection.parse(resp, xhr), options); + if (success) success(collection, resp); + }; + options.error = wrapError(options.error, collection, options); + return (this.sync || Backbone.sync).call(this, 'read', this, options); + }, + + // Create a new instance of a model in this collection. After the model + // has been created on the server, it will be added to the collection. + // Returns the model, or 'false' if validation on a new model fails. + create : function(model, options) { + var coll = this; + options || (options = {}); + model = this._prepareModel(model, options); + if (!model) return false; + var success = options.success; + options.success = function(nextModel, resp, xhr) { + coll.add(nextModel, options); + if (success) success(nextModel, resp, xhr); + }; + model.save(null, options); + return model; + }, + + // **parse** converts a response into a list of models to be added to the + // collection. The default implementation is just to pass it through. + parse : function(resp, xhr) { + return resp; + }, + + // Proxy to _'s chain. Can't be proxied the same way the rest of the + // underscore methods are proxied because it relies on the underscore + // constructor. + chain: function () { + return _(this.models).chain(); + }, + + // Reset all internal state. Called when the collection is reset. + _reset : function(options) { + this.length = 0; + this.models = []; + this._byId = {}; + this._byCid = {}; + }, + + // Prepare a model to be added to this collection + _prepareModel: function(model, options) { + if (!(model instanceof Backbone.Model)) { + var attrs = model; + model = new this.model(attrs, {collection: this}); + if (model.validate && !model._performValidation(attrs, options)) model = false; + } else if (!model.collection) { + model.collection = this; + } + return model; + }, + + // Internal implementation of adding a single model to the set, updating + // hash indexes for `id` and `cid` lookups. + // Returns the model, or 'false' if validation on a new model fails. + _add : function(model, options) { + options || (options = {}); + model = this._prepareModel(model, options); + if (!model) return false; + var already = this.getByCid(model) || this.get(model); + if (already) throw new Error(["Can't add the same model to a set twice", already.id]); + this._byId[model.id] = model; + this._byCid[model.cid] = model; + var index = options.at != null ? options.at : + this.comparator ? this.sortedIndex(model, this.comparator) : + this.length; + this.models.splice(index, 0, model); + model.bind('all', this._onModelEvent); + this.length++; + if (!options.silent) model.trigger('add', model, this, options); + return model; + }, + + // Internal implementation of removing a single model from the set, updating + // hash indexes for `id` and `cid` lookups. + _remove : function(model, options) { + options || (options = {}); + model = this.getByCid(model) || this.get(model); + if (!model) return null; + delete this._byId[model.id]; + delete this._byCid[model.cid]; + this.models.splice(this.indexOf(model), 1); + this.length--; + if (!options.silent) model.trigger('remove', model, this, options); + this._removeReference(model); + return model; + }, + + // Internal method to remove a model's ties to a collection. + _removeReference : function(model) { + if (this == model.collection) { + delete model.collection; + } + model.unbind('all', this._onModelEvent); + }, + + // Internal method called every time a model in the set fires an event. + // Sets need to update their indexes when models change ids. All other + // events simply proxy through. "add" and "remove" events that originate + // in other collections are ignored. + _onModelEvent : function(ev, model, collection, options) { + if ((ev == 'add' || ev == 'remove') && collection != this) return; + if (ev == 'destroy') { + this._remove(model, options); + } + if (model && ev === 'change:' + model.idAttribute) { + delete this._byId[model.previous(model.idAttribute)]; + this._byId[model.id] = model; + } + this.trigger.apply(this, arguments); + } + + }); + + // Underscore methods that we want to implement on the Collection. + var methods = ['forEach', 'each', 'map', 'reduce', 'reduceRight', 'find', 'detect', + 'filter', 'select', 'reject', 'every', 'all', 'some', 'any', 'include', + 'contains', 'invoke', 'max', 'min', 'sortBy', 'sortedIndex', 'toArray', 'size', + 'first', 'rest', 'last', 'without', 'indexOf', 'lastIndexOf', 'isEmpty']; + + // Mix in each Underscore method as a proxy to `Collection#models`. + _.each(methods, function(method) { + Backbone.Collection.prototype[method] = function() { + return _[method].apply(_, [this.models].concat(_.toArray(arguments))); + }; + }); + + // Backbone.Router + // ------------------- + + // Routers map faux-URLs to actions, and fire events when routes are + // matched. Creating a new one sets its `routes` hash, if not set statically. + Backbone.Router = function(options) { + options || (options = {}); + if (options.routes) this.routes = options.routes; + this._bindRoutes(); + this.initialize.apply(this, arguments); + }; + + // Cached regular expressions for matching named param parts and splatted + // parts of route strings. + var namedParam = /:([\w\d]+)/g; + var splatParam = /\*([\w\d]+)/g; + var escapeRegExp = /[-[\]{}()+?.,\\^$|#\s]/g; + + // Set up all inheritable **Backbone.Router** properties and methods. + _.extend(Backbone.Router.prototype, Backbone.Events, { + + // Initialize is an empty function by default. Override it with your own + // initialization logic. + initialize : function(){}, + + // Manually bind a single named route to a callback. For example: + // + // this.route('search/:query/p:num', 'search', function(query, num) { + // ... + // }); + // + route : function(route, name, callback) { + Backbone.history || (Backbone.history = new Backbone.History); + if (!_.isRegExp(route)) route = this._routeToRegExp(route); + Backbone.history.route(route, _.bind(function(fragment) { + var args = this._extractParameters(route, fragment); + callback.apply(this, args); + this.trigger.apply(this, ['route:' + name].concat(args)); + }, this)); + }, + + // Simple proxy to `Backbone.history` to save a fragment into the history. + navigate : function(fragment, triggerRoute) { + Backbone.history.navigate(fragment, triggerRoute); + }, + + // Bind all defined routes to `Backbone.history`. We have to reverse the + // order of the routes here to support behavior where the most general + // routes can be defined at the bottom of the route map. + _bindRoutes : function() { + if (!this.routes) return; + var routes = []; + for (var route in this.routes) { + routes.unshift([route, this.routes[route]]); + } + for (var i = 0, l = routes.length; i < l; i++) { + this.route(routes[i][0], routes[i][1], this[routes[i][1]]); + } + }, + + // Convert a route string into a regular expression, suitable for matching + // against the current location hash. + _routeToRegExp : function(route) { + route = route.replace(escapeRegExp, "\\$&") + .replace(namedParam, "([^\/]*)") + .replace(splatParam, "(.*?)"); + return new RegExp('^' + route + '$'); + }, + + // Given a route, and a URL fragment that it matches, return the array of + // extracted parameters. + _extractParameters : function(route, fragment) { + return route.exec(fragment).slice(1); + } + + }); + + // Backbone.History + // ---------------- + + // Handles cross-browser history management, based on URL fragments. If the + // browser does not support `onhashchange`, falls back to polling. + Backbone.History = function() { + this.handlers = []; + _.bindAll(this, 'checkUrl'); + }; + + // Cached regex for cleaning hashes. + var hashStrip = /^#*/; + + // Cached regex for detecting MSIE. + var isExplorer = /msie [\w.]+/; + + // Has the history handling already been started? + var historyStarted = false; + + // Set up all inheritable **Backbone.History** properties and methods. + _.extend(Backbone.History.prototype, { + + // The default interval to poll for hash changes, if necessary, is + // twenty times a second. + interval: 50, + + // Get the cross-browser normalized URL fragment, either from the URL, + // the hash, or the override. + getFragment : function(fragment, forcePushState) { + if (fragment == null) { + if (this._hasPushState || forcePushState) { + fragment = window.location.pathname; + var search = window.location.search; + if (search) fragment += search; + if (fragment.indexOf(this.options.root) == 0) fragment = fragment.substr(this.options.root.length); + } else { + fragment = window.location.hash; + } + } + return fragment.replace(hashStrip, ''); + }, + + // Start the hash change handling, returning `true` if the current URL matches + // an existing route, and `false` otherwise. + start : function(options) { + + // Figure out the initial configuration. Do we need an iframe? + // Is pushState desired ... is it available? + if (historyStarted) throw new Error("Backbone.history has already been started"); + this.options = _.extend({}, {root: '/'}, this.options, options); + this._wantsPushState = !!this.options.pushState; + this._hasPushState = !!(this.options.pushState && window.history && window.history.pushState); + var fragment = this.getFragment(); + var docMode = document.documentMode; + var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7)); + if (oldIE) { + this.iframe = $('