From 10abfbf3bd73f11ca55ae795e5d0fc9c6bc48e93 Mon Sep 17 00:00:00 2001 From: Dusko Bogdanovski Date: Thu, 17 Aug 2017 17:19:39 +0200 Subject: [PATCH] Added extracting and downloading functionality in several file formats --- .coveragerc | 5 + .gitignore | 42 + .travis.yml | 11 + LICENSE | 661 + MANIFEST.in | 4 + README.rst | 166 + bin/travis-build.bash | 43 + bin/travis-run.sh | 6 + ckanext/__init__.py | 9 + ckanext/dataexplorer/__init__.py | 0 ckanext/dataexplorer/controllers/__init__.py | 0 .../dataexplorer/controllers/dataexplorer.py | 68 + ckanext/dataexplorer/fanstatic/.gitignore | 0 ckanext/dataexplorer/helpers.py | 57 + ckanext/dataexplorer/lib/__init__.py | 1 + .../dataexplorer/lib/file_writer_service.py | 194 + ckanext/dataexplorer/plugin.py | 263 + ckanext/dataexplorer/public/.gitignore | 0 ckanext/dataexplorer/public/css/recline.css | 394 + .../dataexplorer/public/css/recline.min.css | 1 + .../public/img/ajaxload-circle.gif | Bin 0 -> 4176 bytes ckanext/dataexplorer/public/recline_view.js | 249 + .../dataexplorer/public/recline_view.min.js | 15 + ckanext/dataexplorer/public/resource.config | 53 + .../public/vendor/backbone/1.0.0/backbone.js | 1571 +++ .../bootstrap/3.2.0/css/bootstrap-theme.css | 442 + .../vendor/bootstrap/3.2.0/css/bootstrap.css | 6203 ++++++++++ .../fonts/glyphicons-halflings-regular.eot | Bin 0 -> 20335 bytes .../fonts/glyphicons-halflings-regular.svg | 229 + .../fonts/glyphicons-halflings-regular.ttf | Bin 0 -> 41280 bytes .../fonts/glyphicons-halflings-regular.woff | Bin 0 -> 23320 bytes .../vendor/bootstrap/3.2.0/js/bootstrap.js | 2114 ++++ .../public/vendor/ckan.js/ckan.js | 255 + .../public/vendor/flot/excanvas.js | 1428 +++ .../public/vendor/flot/excanvas.min.js | 1 + .../public/vendor/flot/jquery.flot.js | 3061 +++++ .../public/vendor/flot/jquery.flot.time.js | 431 + .../public/vendor/flotr2/flotr2.js | 6128 ++++++++++ .../public/vendor/flotr2/flotr2.min.js | 489 + .../public/vendor/jquery/1.7.1/jquery.js | 9266 ++++++++++++++ .../public/vendor/jquery/1.7.1/jquery.min.js | 4 + .../dataexplorer/public/vendor/json/json2.js | 486 + .../public/vendor/json/json2.min.js | 25 + .../MarkerCluster.Default.css | 60 + .../leaflet.markercluster/MarkerCluster.css | 6 + .../leaflet.markercluster-src.js | 2163 ++++ .../leaflet.markercluster.js | 6 + .../vendor/leaflet/0.7.7/images/layers-2x.png | Bin 0 -> 1585 bytes .../vendor/leaflet/0.7.7/images/layers.png | Bin 0 -> 913 bytes .../leaflet/0.7.7/images/marker-icon-2x.png | Bin 0 -> 4032 bytes .../leaflet/0.7.7/images/marker-icon.png | Bin 0 -> 1747 bytes .../leaflet/0.7.7/images/marker-shadow.png | Bin 0 -> 681 bytes .../vendor/leaflet/0.7.7/leaflet-src.js | 9168 ++++++++++++++ .../public/vendor/leaflet/0.7.7/leaflet.css | 479 + .../public/vendor/leaflet/0.7.7/leaflet.js | 9 + .../public/vendor/moment/2.0.0/moment.js | 1400 +++ .../vendor/mustache/0.5.0-dev/mustache.js | 536 + .../vendor/mustache/0.5.0-dev/mustache.min.js | 36 + .../public/vendor/recline/flot.css | 26 + .../public/vendor/recline/map.css | 28 + .../public/vendor/recline/recline.css | 644 + .../public/vendor/recline/recline.dataset.js | 896 ++ .../public/vendor/recline/recline.js | 4508 +++++++ .../public/vendor/recline/slickgrid.css | 188 + .../vendor/showdown/20120615/showdown.js | 1341 +++ .../vendor/showdown/20120615/showdown.min.js | 48 + .../vendor/slickgrid/2.2/MIT-LICENSE.txt | 20 + .../public/vendor/slickgrid/2.2/README.md | 25 + .../2.2/controls/slick.columnpicker.css | 31 + .../2.2/controls/slick.columnpicker.js | 152 + .../slickgrid/2.2/controls/slick.pager.css | 41 + .../slickgrid/2.2/controls/slick.pager.js | 154 + .../images/ui-bg_flat_0_aaaaaa_40x100.png | Bin 0 -> 86 bytes .../images/ui-bg_flat_75_ffffff_40x100.png | Bin 0 -> 74 bytes .../images/ui-bg_glass_55_fbf9ee_1x400.png | Bin 0 -> 111 bytes .../images/ui-bg_glass_65_ffffff_1x400.png | Bin 0 -> 90 bytes .../images/ui-bg_glass_75_dadada_1x400.png | Bin 0 -> 102 bytes .../images/ui-bg_glass_75_e6e6e6_1x400.png | Bin 0 -> 102 bytes .../images/ui-bg_glass_95_fef1ec_1x400.png | Bin 0 -> 115 bytes .../ui-bg_highlight-soft_75_cccccc_1x100.png | Bin 0 -> 86 bytes .../images/ui-icons_222222_256x240.png | Bin 0 -> 3687 bytes .../images/ui-icons_2e83ff_256x240.png | Bin 0 -> 3687 bytes .../images/ui-icons_454545_256x240.png | Bin 0 -> 3687 bytes .../images/ui-icons_888888_256x240.png | Bin 0 -> 3687 bytes .../images/ui-icons_cd0a0a_256x240.png | Bin 0 -> 3687 bytes .../smoothness/jquery-ui-1.8.16.custom.css | 409 + .../vendor/slickgrid/2.2/images/actions.gif | Bin 0 -> 170 bytes .../2.2/images/ajax-loader-small.gif | Bin 0 -> 1849 bytes .../slickgrid/2.2/images/arrow_redo.png | Bin 0 -> 550 bytes .../2.2/images/arrow_right_peppermint.png | Bin 0 -> 126 bytes .../2.2/images/arrow_right_spearmint.png | Bin 0 -> 126 bytes .../slickgrid/2.2/images/arrow_undo.png | Bin 0 -> 555 bytes .../slickgrid/2.2/images/bullet_blue.png | Bin 0 -> 231 bytes .../slickgrid/2.2/images/bullet_star.png | Bin 0 -> 273 bytes .../2.2/images/bullet_toggle_minus.png | Bin 0 -> 154 bytes .../2.2/images/bullet_toggle_plus.png | Bin 0 -> 156 bytes .../vendor/slickgrid/2.2/images/calendar.gif | Bin 0 -> 1035 bytes .../vendor/slickgrid/2.2/images/collapse.gif | Bin 0 -> 846 bytes .../slickgrid/2.2/images/comment_yellow.gif | Bin 0 -> 257 bytes .../vendor/slickgrid/2.2/images/down.gif | Bin 0 -> 59 bytes .../slickgrid/2.2/images/drag-handle.png | Bin 0 -> 98 bytes .../slickgrid/2.2/images/editor-helper-bg.gif | Bin 0 -> 1164 bytes .../vendor/slickgrid/2.2/images/expand.gif | Bin 0 -> 851 bytes .../vendor/slickgrid/2.2/images/header-bg.gif | Bin 0 -> 872 bytes .../2.2/images/header-columns-bg.gif | Bin 0 -> 836 bytes .../2.2/images/header-columns-over-bg.gif | Bin 0 -> 823 bytes .../vendor/slickgrid/2.2/images/help.png | Bin 0 -> 328 bytes .../vendor/slickgrid/2.2/images/info.gif | Bin 0 -> 80 bytes .../vendor/slickgrid/2.2/images/listview.gif | Bin 0 -> 2380 bytes .../vendor/slickgrid/2.2/images/pencil.gif | Bin 0 -> 914 bytes .../slickgrid/2.2/images/row-over-bg.gif | Bin 0 -> 823 bytes .../vendor/slickgrid/2.2/images/sort-asc.gif | Bin 0 -> 830 bytes .../vendor/slickgrid/2.2/images/sort-asc.png | Bin 0 -> 104 bytes .../vendor/slickgrid/2.2/images/sort-desc.gif | Bin 0 -> 833 bytes .../vendor/slickgrid/2.2/images/sort-desc.png | Bin 0 -> 106 bytes .../vendor/slickgrid/2.2/images/stripes.png | Bin 0 -> 94 bytes .../vendor/slickgrid/2.2/images/tag_red.png | Bin 0 -> 529 bytes .../vendor/slickgrid/2.2/images/tick.png | Bin 0 -> 465 bytes .../slickgrid/2.2/images/user_identity.gif | Bin 0 -> 905 bytes .../2.2/images/user_identity_plus.gif | Bin 0 -> 546 bytes .../vendor/slickgrid/2.2/jquery-1.7.min.js | 4 + .../slickgrid/2.2/jquery-ui-1.8.16.custom.js | 611 + .../slickgrid/2.2/jquery.event.drag-2.2.js | 402 + .../slickgrid/2.2/jquery.event.drop-2.2.js | 302 + .../2.2/plugins/slick.autotooltips.js | 83 + .../2.2/plugins/slick.cellcopymanager.js | 86 + .../2.2/plugins/slick.cellrangedecorator.js | 66 + .../2.2/plugins/slick.cellrangeselector.js | 113 + .../2.2/plugins/slick.cellselectionmodel.js | 154 + .../2.2/plugins/slick.checkboxselectcolumn.js | 153 + .../2.2/plugins/slick.headerbuttons.css | 39 + .../2.2/plugins/slick.headerbuttons.js | 177 + .../2.2/plugins/slick.headermenu.css | 59 + .../slickgrid/2.2/plugins/slick.headermenu.js | 275 + .../2.2/plugins/slick.rowmovemanager.js | 138 + .../2.2/plugins/slick.rowselectionmodel.js | 187 + .../slickgrid/2.2/slick-default-theme.css | 118 + .../public/vendor/slickgrid/2.2/slick.core.js | 467 + .../vendor/slickgrid/2.2/slick.dataview.js | 1126 ++ .../vendor/slickgrid/2.2/slick.editors.js | 512 + .../vendor/slickgrid/2.2/slick.formatters.js | 59 + .../vendor/slickgrid/2.2/slick.grid.css | 157 + .../public/vendor/slickgrid/2.2/slick.grid.js | 3422 ++++++ .../2.2/slick.groupitemmetadataprovider.js | 158 + .../vendor/slickgrid/2.2/slick.remotemodel.js | 173 + .../public/vendor/timeline/LICENSE | 365 + .../public/vendor/timeline/README | 1 + .../public/vendor/timeline/css/loading.gif | Bin 0 -> 6909 bytes .../public/vendor/timeline/css/timeline.css | 284 + .../public/vendor/timeline/css/timeline.png | Bin 0 -> 14048 bytes .../vendor/timeline/css/timeline@2x.png | Bin 0 -> 36430 bytes .../public/vendor/timeline/js/timeline.js | 10015 ++++++++++++++++ .../0.4.0/underscore.deferred.js | 445 + .../0.4.0/underscore.deferred.min.js | 17 + .../vendor/underscore/1.4.4/underscore.js | 1227 ++ .../dataexplorer/public/widget.recordcount.js | 29 + .../public/widget.recordcount.min.js | 3 + ckanext/dataexplorer/templates/.gitignore | 0 .../templates/recline_graph_form.html | 8 + .../templates/recline_map_form.html | 11 + .../dataexplorer/templates/recline_view.html | 22 + ckanext/dataexplorer/tests/__init__.py | 0 ckanext/dataexplorer/tests/test_view.py | 180 + dev-requirements.txt | 0 requirements.txt | 1 + setup.cfg | 21 + setup.py | 99 + test.ini | 49 + 168 files changed, 78567 insertions(+) create mode 100644 .coveragerc create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 LICENSE create mode 100644 MANIFEST.in create mode 100644 README.rst create mode 100644 bin/travis-build.bash create mode 100644 bin/travis-run.sh create mode 100644 ckanext/__init__.py create mode 100644 ckanext/dataexplorer/__init__.py create mode 100644 ckanext/dataexplorer/controllers/__init__.py create mode 100644 ckanext/dataexplorer/controllers/dataexplorer.py create mode 100644 ckanext/dataexplorer/fanstatic/.gitignore create mode 100644 ckanext/dataexplorer/helpers.py create mode 100644 ckanext/dataexplorer/lib/__init__.py create mode 100644 ckanext/dataexplorer/lib/file_writer_service.py create mode 100644 ckanext/dataexplorer/plugin.py create mode 100644 ckanext/dataexplorer/public/.gitignore create mode 100644 ckanext/dataexplorer/public/css/recline.css create mode 100644 ckanext/dataexplorer/public/css/recline.min.css create mode 100644 ckanext/dataexplorer/public/img/ajaxload-circle.gif create mode 100644 ckanext/dataexplorer/public/recline_view.js create mode 100644 ckanext/dataexplorer/public/recline_view.min.js create mode 100644 ckanext/dataexplorer/public/resource.config create mode 100644 ckanext/dataexplorer/public/vendor/backbone/1.0.0/backbone.js create mode 100644 ckanext/dataexplorer/public/vendor/bootstrap/3.2.0/css/bootstrap-theme.css create mode 100644 ckanext/dataexplorer/public/vendor/bootstrap/3.2.0/css/bootstrap.css create mode 100644 ckanext/dataexplorer/public/vendor/bootstrap/3.2.0/fonts/glyphicons-halflings-regular.eot create mode 100644 ckanext/dataexplorer/public/vendor/bootstrap/3.2.0/fonts/glyphicons-halflings-regular.svg create mode 100644 ckanext/dataexplorer/public/vendor/bootstrap/3.2.0/fonts/glyphicons-halflings-regular.ttf create mode 100644 ckanext/dataexplorer/public/vendor/bootstrap/3.2.0/fonts/glyphicons-halflings-regular.woff create mode 100644 ckanext/dataexplorer/public/vendor/bootstrap/3.2.0/js/bootstrap.js create mode 100644 ckanext/dataexplorer/public/vendor/ckan.js/ckan.js create mode 100755 ckanext/dataexplorer/public/vendor/flot/excanvas.js create mode 100755 ckanext/dataexplorer/public/vendor/flot/excanvas.min.js create mode 100755 ckanext/dataexplorer/public/vendor/flot/jquery.flot.js create mode 100755 ckanext/dataexplorer/public/vendor/flot/jquery.flot.time.js create mode 100644 ckanext/dataexplorer/public/vendor/flotr2/flotr2.js create mode 100644 ckanext/dataexplorer/public/vendor/flotr2/flotr2.min.js create mode 100644 ckanext/dataexplorer/public/vendor/jquery/1.7.1/jquery.js create mode 100644 ckanext/dataexplorer/public/vendor/jquery/1.7.1/jquery.min.js create mode 100644 ckanext/dataexplorer/public/vendor/json/json2.js create mode 100644 ckanext/dataexplorer/public/vendor/json/json2.min.js create mode 100644 ckanext/dataexplorer/public/vendor/leaflet.markercluster/MarkerCluster.Default.css create mode 100644 ckanext/dataexplorer/public/vendor/leaflet.markercluster/MarkerCluster.css create mode 100644 ckanext/dataexplorer/public/vendor/leaflet.markercluster/leaflet.markercluster-src.js create mode 100644 ckanext/dataexplorer/public/vendor/leaflet.markercluster/leaflet.markercluster.js create mode 100644 ckanext/dataexplorer/public/vendor/leaflet/0.7.7/images/layers-2x.png create mode 100644 ckanext/dataexplorer/public/vendor/leaflet/0.7.7/images/layers.png create mode 100644 ckanext/dataexplorer/public/vendor/leaflet/0.7.7/images/marker-icon-2x.png create mode 100644 ckanext/dataexplorer/public/vendor/leaflet/0.7.7/images/marker-icon.png create mode 100644 ckanext/dataexplorer/public/vendor/leaflet/0.7.7/images/marker-shadow.png create mode 100644 ckanext/dataexplorer/public/vendor/leaflet/0.7.7/leaflet-src.js create mode 100644 ckanext/dataexplorer/public/vendor/leaflet/0.7.7/leaflet.css create mode 100644 ckanext/dataexplorer/public/vendor/leaflet/0.7.7/leaflet.js create mode 100755 ckanext/dataexplorer/public/vendor/moment/2.0.0/moment.js create mode 100644 ckanext/dataexplorer/public/vendor/mustache/0.5.0-dev/mustache.js create mode 100644 ckanext/dataexplorer/public/vendor/mustache/0.5.0-dev/mustache.min.js create mode 100644 ckanext/dataexplorer/public/vendor/recline/flot.css create mode 100644 ckanext/dataexplorer/public/vendor/recline/map.css create mode 100755 ckanext/dataexplorer/public/vendor/recline/recline.css create mode 100755 ckanext/dataexplorer/public/vendor/recline/recline.dataset.js create mode 100755 ckanext/dataexplorer/public/vendor/recline/recline.js create mode 100644 ckanext/dataexplorer/public/vendor/recline/slickgrid.css create mode 100644 ckanext/dataexplorer/public/vendor/showdown/20120615/showdown.js create mode 100644 ckanext/dataexplorer/public/vendor/showdown/20120615/showdown.min.js create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/MIT-LICENSE.txt create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/README.md create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/controls/slick.columnpicker.css create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/controls/slick.columnpicker.js create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/controls/slick.pager.css create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/controls/slick.pager.js create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/css/smoothness/images/ui-bg_flat_0_aaaaaa_40x100.png create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/css/smoothness/images/ui-bg_flat_75_ffffff_40x100.png create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/css/smoothness/images/ui-bg_glass_55_fbf9ee_1x400.png create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/css/smoothness/images/ui-bg_glass_65_ffffff_1x400.png create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/css/smoothness/images/ui-bg_glass_75_dadada_1x400.png create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/css/smoothness/images/ui-bg_glass_75_e6e6e6_1x400.png create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/css/smoothness/images/ui-bg_glass_95_fef1ec_1x400.png create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/css/smoothness/images/ui-bg_highlight-soft_75_cccccc_1x100.png create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/css/smoothness/images/ui-icons_222222_256x240.png create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/css/smoothness/images/ui-icons_2e83ff_256x240.png create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/css/smoothness/images/ui-icons_454545_256x240.png create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/css/smoothness/images/ui-icons_888888_256x240.png create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/css/smoothness/images/ui-icons_cd0a0a_256x240.png create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/css/smoothness/jquery-ui-1.8.16.custom.css create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/images/actions.gif create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/images/ajax-loader-small.gif create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/images/arrow_redo.png create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/images/arrow_right_peppermint.png create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/images/arrow_right_spearmint.png create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/images/arrow_undo.png create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/images/bullet_blue.png create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/images/bullet_star.png create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/images/bullet_toggle_minus.png create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/images/bullet_toggle_plus.png create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/images/calendar.gif create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/images/collapse.gif create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/images/comment_yellow.gif create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/images/down.gif create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/images/drag-handle.png create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/images/editor-helper-bg.gif create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/images/expand.gif create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/images/header-bg.gif create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/images/header-columns-bg.gif create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/images/header-columns-over-bg.gif create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/images/help.png create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/images/info.gif create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/images/listview.gif create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/images/pencil.gif create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/images/row-over-bg.gif create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/images/sort-asc.gif create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/images/sort-asc.png create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/images/sort-desc.gif create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/images/sort-desc.png create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/images/stripes.png create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/images/tag_red.png create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/images/tick.png create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/images/user_identity.gif create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/images/user_identity_plus.gif create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/jquery-1.7.min.js create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/jquery-ui-1.8.16.custom.js create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/jquery.event.drag-2.2.js create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/jquery.event.drop-2.2.js create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/plugins/slick.autotooltips.js create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/plugins/slick.cellcopymanager.js create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/plugins/slick.cellrangedecorator.js create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/plugins/slick.cellrangeselector.js create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/plugins/slick.cellselectionmodel.js create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/plugins/slick.checkboxselectcolumn.js create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/plugins/slick.headerbuttons.css create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/plugins/slick.headerbuttons.js create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/plugins/slick.headermenu.css create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/plugins/slick.headermenu.js create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/plugins/slick.rowmovemanager.js create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/plugins/slick.rowselectionmodel.js create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/slick-default-theme.css create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/slick.core.js create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/slick.dataview.js create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/slick.editors.js create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/slick.formatters.js create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/slick.grid.css create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/slick.grid.js create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/slick.groupitemmetadataprovider.js create mode 100644 ckanext/dataexplorer/public/vendor/slickgrid/2.2/slick.remotemodel.js create mode 100644 ckanext/dataexplorer/public/vendor/timeline/LICENSE create mode 100644 ckanext/dataexplorer/public/vendor/timeline/README create mode 100755 ckanext/dataexplorer/public/vendor/timeline/css/loading.gif create mode 100755 ckanext/dataexplorer/public/vendor/timeline/css/timeline.css create mode 100755 ckanext/dataexplorer/public/vendor/timeline/css/timeline.png create mode 100755 ckanext/dataexplorer/public/vendor/timeline/css/timeline@2x.png create mode 100755 ckanext/dataexplorer/public/vendor/timeline/js/timeline.js create mode 100644 ckanext/dataexplorer/public/vendor/underscore.deferred/0.4.0/underscore.deferred.js create mode 100644 ckanext/dataexplorer/public/vendor/underscore.deferred/0.4.0/underscore.deferred.min.js create mode 100755 ckanext/dataexplorer/public/vendor/underscore/1.4.4/underscore.js create mode 100644 ckanext/dataexplorer/public/widget.recordcount.js create mode 100644 ckanext/dataexplorer/public/widget.recordcount.min.js create mode 100644 ckanext/dataexplorer/templates/.gitignore create mode 100644 ckanext/dataexplorer/templates/recline_graph_form.html create mode 100644 ckanext/dataexplorer/templates/recline_map_form.html create mode 100644 ckanext/dataexplorer/templates/recline_view.html create mode 100644 ckanext/dataexplorer/tests/__init__.py create mode 100644 ckanext/dataexplorer/tests/test_view.py create mode 100644 dev-requirements.txt create mode 100644 requirements.txt create mode 100644 setup.cfg create mode 100644 setup.py create mode 100644 test.ini diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..3138593 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,5 @@ +[report] +omit = + */site-packages/* + */python?.?/* + ckan/* \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e9876d1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,42 @@ +.ropeproject +node_modules +bower_components + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +sdist/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.cache +nosetests.xml +coverage.xml + +# Sphinx documentation +docs/_build/ \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..8419923 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,11 @@ +language: python +sudo: required +python: + - "2.7" +env: PGVERSION=9.1 +install: + - bash bin/travis-build.bash + - pip install coveralls +script: sh bin/travis-run.sh +after_success: + - coveralls diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3ffc567 --- /dev/null +++ b/LICENSE @@ -0,0 +1,661 @@ +GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. \ No newline at end of file diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..372362c --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,4 @@ +include README.rst +include LICENSE +include requirements.txt +recursive-include ckanext/dataexplorer *.html *.json *.js *.less *.css *.mo *.config \ No newline at end of file diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..f907d56 --- /dev/null +++ b/README.rst @@ -0,0 +1,166 @@ +.. You should enable this project on travis-ci.org and coveralls.io to make + these badges work. The necessary Travis and Coverage config files have been + generated for you. + +.. image:: https://travis-ci.org/duskobogdanovski/ckanext-dataexplorer.svg?branch=master + :target: https://travis-ci.org/duskobogdanovski/ckanext-dataexplorer + +.. image:: https://coveralls.io/repos/duskobogdanovski/ckanext-dataexplorer/badge.svg + :target: https://coveralls.io/r/duskobogdanovski/ckanext-dataexplorer + +.. image:: https://pypip.in/download/ckanext-dataexplorer/badge.svg + :target: https://pypi.python.org/pypi//ckanext-dataexplorer/ + :alt: Downloads + +.. image:: https://pypip.in/version/ckanext-dataexplorer/badge.svg + :target: https://pypi.python.org/pypi/ckanext-dataexplorer/ + :alt: Latest Version + +.. image:: https://pypip.in/py_versions/ckanext-dataexplorer/badge.svg + :target: https://pypi.python.org/pypi/ckanext-dataexplorer/ + :alt: Supported Python versions + +.. image:: https://pypip.in/status/ckanext-dataexplorer/badge.svg + :target: https://pypi.python.org/pypi/ckanext-dataexplorer/ + :alt: Development Status + +.. image:: https://pypip.in/license/ckanext-dataexplorer/badge.svg + :target: https://pypi.python.org/pypi/ckanext-dataexplorer/ + :alt: License + +============= +ckanext-dataexplorer +============= + +.. Put a description of your extension here: + What does it do? What features does it have? + Consider including some screenshots or embedding a video! + + +------------ +Requirements +------------ + +For example, you might want to mention here which versions of CKAN this +extension works with. + + +------------ +Installation +------------ + +.. Add any additional install steps to the list below. + For example installing any non-Python dependencies or adding any required + config settings. + +To install ckanext-dataexplorer: + +1. Activate your CKAN virtual environment, for example:: + + . /usr/lib/ckan/default/bin/activate + +2. Install the ckanext-dataexplorer Python package into your virtual environment:: + + pip install ckanext-dataexplorer + +3. Add ``dataexplorer`` to the ``ckan.plugins`` setting in your CKAN + config file (by default the config file is located at + ``/etc/ckan/default/production.ini``). + +4. Restart CKAN. For example if you've deployed CKAN with Apache on Ubuntu:: + + sudo service apache2 reload + + +--------------- +Config Settings +--------------- + +Document any optional config settings here. For example:: + + # The minimum number of hours to wait before re-checking a resource + # (optional, default: 24). + ckanext.dataexplorer.some_setting = some_default_value + + +------------------------ +Development Installation +------------------------ + +To install ckanext-dataexplorer for development, activate your CKAN virtualenv and +do:: + + git clone https://github.com/duskobogdanovski/ckanext-dataexplorer.git + cd ckanext-dataexplorer + python setup.py develop + pip install -r dev-requirements.txt + + +----------------- +Running the Tests +----------------- + +To run the tests, do:: + + nosetests --nologcapture --with-pylons=test.ini + +To run the tests and produce a coverage report, first make sure you have +coverage installed in your virtualenv (``pip install coverage``) then run:: + + nosetests --nologcapture --with-pylons=test.ini --with-coverage --cover-package=ckanext.dataexplorer --cover-inclusive --cover-erase --cover-tests + + +--------------------------------- +Registering ckanext-dataexplorer on PyPI +--------------------------------- + +ckanext-dataexplorer should be availabe on PyPI as +https://pypi.python.org/pypi/ckanext-dataexplorer. If that link doesn't work, then +you can register the project on PyPI for the first time by following these +steps: + +1. Create a source distribution of the project:: + + python setup.py sdist + +2. Register the project:: + + python setup.py register + +3. Upload the source distribution to PyPI:: + + python setup.py sdist upload + +4. Tag the first release of the project on GitHub with the version number from + the ``setup.py`` file. For example if the version number in ``setup.py`` is + 0.0.1 then do:: + + git tag 0.0.1 + git push --tags + + +---------------------------------------- +Releasing a New Version of ckanext-dataexplorer +---------------------------------------- + +ckanext-dataexplorer is availabe on PyPI as https://pypi.python.org/pypi/ckanext-dataexplorer. +To publish a new version to PyPI follow these steps: + +1. Update the version number in the ``setup.py`` file. + See `PEP 440 `_ + for how to choose version numbers. + +2. Create a source distribution of the new version:: + + python setup.py sdist + +3. Upload the source distribution to PyPI:: + + python setup.py sdist upload + +4. Tag the new release of the project on GitHub with the version number from + the ``setup.py`` file. For example if the version number in ``setup.py`` is + 0.0.2 then do:: + + git tag 0.0.2 + git push --tags diff --git a/bin/travis-build.bash b/bin/travis-build.bash new file mode 100644 index 0000000..67ed7ad --- /dev/null +++ b/bin/travis-build.bash @@ -0,0 +1,43 @@ +#!/bin/bash +set -e + +echo "This is travis-build.bash..." + +echo "Installing the packages that CKAN requires..." +sudo apt-get update -qq +sudo apt-get install postgresql-$PGVERSION solr-jetty libcommons-fileupload-java:amd64=1.2.2-1 + +echo "Installing CKAN and its Python dependencies..." +git clone https://github.com/ckan/ckan +cd ckan +export latest_ckan_release_branch=`git branch --all | grep remotes/origin/release-v | sort -r | sed 's/remotes\/origin\///g' | head -n 1` +echo "CKAN branch: $latest_ckan_release_branch" +git checkout $latest_ckan_release_branch +python setup.py develop +pip install -r requirements.txt --allow-all-external +pip install -r dev-requirements.txt --allow-all-external +cd - + +echo "Creating the PostgreSQL user and database..." +sudo -u postgres psql -c "CREATE USER ckan_default WITH PASSWORD 'pass';" +sudo -u postgres psql -c 'CREATE DATABASE ckan_test WITH OWNER ckan_default;' + +echo "SOLR config..." +# Solr is multicore for tests on ckan master, but it's easier to run tests on +# Travis single-core. See https://github.com/ckan/ckan/issues/2972 +sed -i -e 's/solr_url.*/solr_url = http:\/\/127.0.0.1:8983\/solr/' ckan/test-core.ini + +echo "Initialising the database..." +cd ckan +paster db init -c test-core.ini +cd - + +echo "Installing ckanext-dataexplorer and its requirements..." +python setup.py develop +pip install -r dev-requirements.txt + +echo "Moving test.ini into a subdir..." +mkdir subdir +mv test.ini subdir + +echo "travis-build.bash is done." \ No newline at end of file diff --git a/bin/travis-run.sh b/bin/travis-run.sh new file mode 100644 index 0000000..e8a2297 --- /dev/null +++ b/bin/travis-run.sh @@ -0,0 +1,6 @@ +#!/bin/sh -e + +echo "NO_START=0\nJETTY_HOST=127.0.0.1\nJETTY_PORT=8983\nJAVA_HOME=$JAVA_HOME" | sudo tee /etc/default/jetty +sudo cp ckan/ckan/config/solr/schema.xml /etc/solr/conf/schema.xml +sudo service jetty restart +nosetests --nologcapture --with-pylons=subdir/test.ini --with-coverage --cover-package=ckanext.dataexplorer --cover-inclusive --cover-erase --cover-tests \ No newline at end of file diff --git a/ckanext/__init__.py b/ckanext/__init__.py new file mode 100644 index 0000000..ed48ed0 --- /dev/null +++ b/ckanext/__init__.py @@ -0,0 +1,9 @@ +# encoding: utf-8 + +# this is a namespace package +try: + import pkg_resources + pkg_resources.declare_namespace(__name__) +except ImportError: + import pkgutil + __path__ = pkgutil.extend_path(__path__, __name__) diff --git a/ckanext/dataexplorer/__init__.py b/ckanext/dataexplorer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ckanext/dataexplorer/controllers/__init__.py b/ckanext/dataexplorer/controllers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ckanext/dataexplorer/controllers/dataexplorer.py b/ckanext/dataexplorer/controllers/dataexplorer.py new file mode 100644 index 0000000..3d0e25b --- /dev/null +++ b/ckanext/dataexplorer/controllers/dataexplorer.py @@ -0,0 +1,68 @@ +import logging +import json + +try: + # CKAN 2.7 and later + from ckan.common import config +except ImportError: + # CKAN 2.6 and earlier + from pylons import config + +from ckan.lib import base +from ckan.plugins import toolkit +from ckan.plugins.toolkit import abort, ObjectNotFound, ValidationError +from ckan import model, logic +from ckan.common import c, _, request, response +from ckanext.dataexplorer.lib import FileWriterService + +log = logging.getLogger(__name__) + +DUMP_FORMATS = 'csv', 'xlsx', 'json', 'xml' + +class DataExplorer(base.BaseController): + + ctrl = 'ckanext.dataexplorer.controllers.dataexplorer:DataExplorer' + + def _get_ctx(self): + return { + 'model': model, 'session': model.Session, + 'user': c.user, + 'auth_user_obj': c.userobj, + 'for_view': True + } + + def _get_action(self, action, data_dict): + return toolkit.get_action(action)(self._get_ctx(), data_dict) + + def extract(self): + + writer = FileWriterService() + columns = [] + + if request.method == 'POST': + data_dict = dict(request.POST) + data = json.loads(data_dict['extract_data']) + format = data.pop('format') + + resource_data_info = self._get_action('datastore_info', {'id': data['resource_id']}) + print resource_data_info + + for key in resource_data_info['schema']: + columns.append(key) + + try: + resource_data = self._get_action('datastore_search', data) + except ObjectNotFound: + abort(404, _('DataStore resource not found')) + + try: + writer.write_to_file(columns, + resource_data.get('records'), + format, + response) + except ValidationError: + abort(400, _( + u'Format: must be one of %s') % u', '.join(DUMP_FORMATS)) + + + diff --git a/ckanext/dataexplorer/fanstatic/.gitignore b/ckanext/dataexplorer/fanstatic/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/ckanext/dataexplorer/helpers.py b/ckanext/dataexplorer/helpers.py new file mode 100644 index 0000000..119c3c4 --- /dev/null +++ b/ckanext/dataexplorer/helpers.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 - + +try: + # CKAN 2.7 and later + from ckan.common import config +except ImportError: + # CKAN 2.6 and earlier + from pylons import config + + +import logging +import json +from datetime import date, timedelta, datetime +from decimal import Decimal + +log = logging.getLogger(__name__) + +NAIVE_DATETIME_FORMAT = '%Y-%m-%d %H:%M:%S' +DATE_FORMAT = '%Y-%m-%d' + + +class CustomJSONEncoder(json.JSONEncoder): + def default(self, obj): + try: + return json.JSONEncoder.default(self, obj) + except TypeError: + if type(obj) is date: + return obj.strftime(DATE_FORMAT) + + if type(obj) is datetime: + return obj.strftime(NAIVE_DATETIME_FORMAT) + + if type(obj) is timedelta: + # return it as rounded milliseconds + return int(obj.total_seconds() * 1000) + + if type(obj) is Decimal: + return str(obj) + + raise + +def _get_logic_functions(module_root, logic_functions={}): + '''Helper function that scans extension logic dir for all logic functions.''' + for module_name in ['create', 'delete', 'get', 'patch', 'update']: + module_path = '%s.%s' % (module_root, module_name,) + + module = __import__(module_path) + + for part in module_path.split('.')[1:]: + module = getattr(module, part) + + for key, value in module.__dict__.items(): + if not key.startswith('_') and (hasattr(value, '__call__') + and (value.__module__ == module_path)): + logic_functions[key] = value + + return logic_functions \ No newline at end of file diff --git a/ckanext/dataexplorer/lib/__init__.py b/ckanext/dataexplorer/lib/__init__.py new file mode 100644 index 0000000..46574de --- /dev/null +++ b/ckanext/dataexplorer/lib/__init__.py @@ -0,0 +1 @@ +from .file_writer_service import FileWriterService \ No newline at end of file diff --git a/ckanext/dataexplorer/lib/file_writer_service.py b/ckanext/dataexplorer/lib/file_writer_service.py new file mode 100644 index 0000000..3b91bad --- /dev/null +++ b/ckanext/dataexplorer/lib/file_writer_service.py @@ -0,0 +1,194 @@ +try: + # CKAN 2.7 and later + from ckan.common import config +except ImportError: + # CKAN 2.6 and earlier + from pylons import config + +import logging +import json +import csv +import cStringIO + +import ckan.logic as l + +from email.utils import encode_rfc2231 +from xlsxwriter.workbook import Workbook +from xml.etree.cElementTree import Element, SubElement, ElementTree +from ckan.common import _ +from ckanext.dataexplorer.helpers import CustomJSONEncoder + + +DUMP_FORMATS = 'csv', 'json', 'xml', 'xlsx' + +UTF8_BOM = u'\uFEFF'.encode(u'utf-8') + +log = logging.getLogger(__name__) + + +class XMLWriter(object): + def __init__(self, output, columns): + + self.delimiter = config.get('ckanext.dataexplorer.headers_names_delimiter', "_") + self.output = output + self.id_col = columns[0] == u'_id' + if self.id_col: + columns = columns[1:] + columns_fixed = [] + for column in columns: + columns_fixed.append(column.replace(" ", self.delimiter)) + self.columns = columns_fixed + log.debug(self.columns) + + def writerow(self, row): + root = Element(u'row') + if self.id_col: + root.attrib[u'_id'] = unicode(row[0]) + row = row[1:] + for k, v in zip(self.columns, row): + if v is None: + SubElement(root, k).text = u'NULL' + continue + SubElement(root, k).text = unicode(v) + ElementTree(root).write(self.output, encoding=u'utf-8') + self.output.write(b'\n') + + +class JSONWriter(object): + def __init__(self, output, columns): + self.output = output + self.columns = columns + self.first = True + + def writerow(self, row): + + if self.first: + self.first = False + self.output.write(b'\n ') + else: + self.output.write(b',\n ') + self.output.write(json.dumps( + row, + ensure_ascii=False, + separators=(u',', u':'), + sort_keys=True, + cls=CustomJSONEncoder).encode('utf-8')) + + +class FileWriterService(): + def _csv_writer(self, columns, records, response): + + name = 'test' + + if hasattr(response, u'headers'): + response.headers['Content-Type'] = b'text/csv; charset=utf-8' + if name: + response.headers['Content-disposition'] = ( + b'attachment; filename="{name}.csv"'.format( + name=encode_rfc2231(name))) + + writer = csv.writer(response, delimiter=',') + + # Writing headers + writer.writerow([c.encode("utf-8") for c in columns]) + + # Writing records + for record in records: + writer.writerow([record[column] for column in columns]) + + def _json_writer(self, columns, records, response): + + output = cStringIO.StringIO() + name = 'test' + + if hasattr(response, u'headers'): + response.headers['Content-Type'] = ( + b'application/json; charset=utf-8') + if name: + response.headers['Content-disposition'] = ( + b'attachment; filename="{name}.json"'.format( + name=encode_rfc2231(name))) + + response.write( + b'{\n "fields": %s,\n "records": [' % json.dumps( + columns, ensure_ascii=False, separators=(u',', u':')).encode(u'utf-8')) + + # Initiate json writer and columns + wr = JSONWriter(response, [c.encode("utf-8") for c in columns]) + + # Write records + for record in records: + wr.writerow([record[column] for column in columns]) + + response.write(b'\n]}\n') + + def _xml_writer(self, columns, records, response): + + name = 'test' + + if hasattr(response, u'headers'): + response.headers['Content-Type'] = ( + b'text/xml; charset=utf-8') + if name: + response.headers['Content-disposition'] = ( + b'attachment; filename="{name}.xml"'.format( + name=encode_rfc2231(name))) + + response.write(b'\n') + + # Initiate xml writer and columns + wr = XMLWriter(response, [c.encode("utf-8") for c in columns]) + + # Write records + for record in records: + wr.writerow([record[column] for column in columns]) + + response.write(b'\n') + + def _xlsx_writer(self, columns, records, response): + + output = cStringIO.StringIO() + name = 'test' + + if hasattr(response, u'headers'): + response.headers['Content-Type'] = ( + b'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet; charset=utf-8') + if name: + response.headers['Content-disposition'] = ( + b'attachment; filename="{name}.xlsx"'.format( + name=encode_rfc2231(name))) + + workbook = Workbook(output) + worksheet = workbook.add_worksheet() + + # Writing headers + col = 0 + for c in columns: + worksheet.write(0, col, c) + col += 1 + + # Writing records + row = 1 + for record in records: + col = 0 + for column in columns: + worksheet.write(row, col, record[column]) + col += 1 + row += 1 + + workbook.close() + response.write(output.getvalue()) + + def write_to_file(self, columns, records, format, response): + + format = format.lower() + if format == 'csv': + return self._csv_writer(columns, records, response) + if format == 'json': + return self._json_writer(columns, records, response) + if format == 'xml': + return self._xml_writer(columns, records, response) + if format == 'xlsx': + return self._xlsx_writer(columns, records, response) + raise l.ValidationError(_( + u'format: must be one of %s') % u', '.join(DUMP_FORMATS)) diff --git a/ckanext/dataexplorer/plugin.py b/ckanext/dataexplorer/plugin.py new file mode 100644 index 0000000..4aafb4f --- /dev/null +++ b/ckanext/dataexplorer/plugin.py @@ -0,0 +1,263 @@ +# encoding: utf-8 + +from logging import getLogger + +from ckan.common import json, config +import ckan.plugins as p +import ckan.plugins.toolkit as toolkit + +log = getLogger(__name__) +ignore_empty = p.toolkit.get_validator('ignore_empty') +natural_number_validator = p.toolkit.get_validator('natural_number_validator') +Invalid = p.toolkit.Invalid + + +def get_mapview_config(): + ''' + Extracts and returns map view configuration of the reclineview extension. + ''' + namespace = 'ckanext.spatial.common_map.' + return dict([(k.replace(namespace, ''), v) for k, v in config.iteritems() + if k.startswith(namespace)]) + + +def in_list(list_possible_values): + ''' + Validator that checks that the input value is one of the given + possible values. + + :param list_possible_values: function that returns list of possible values + for validated field + :type possible_values: function + ''' + def validate(key, data, errors, context): + if not data[key] in list_possible_values(): + raise Invalid('"{0}" is not a valid parameter'.format(data[key])) + return validate + + +def datastore_fields(resource, valid_field_types): + ''' + Return a list of all datastore fields for a given resource, as long as + the datastore field type is in valid_field_types. + + :param resource: resource dict + :type resource: dict + :param valid_field_types: field types to include in returned list + :type valid_field_types: list of strings + ''' + data = {'resource_id': resource['id'], 'limit': 0} + fields = toolkit.get_action('datastore_search')({}, data)['fields'] + return [{'value': f['id'], 'text': f['id']} for f in fields + if f['type'] in valid_field_types] + + +class ReclineViewBase(p.SingletonPlugin): + ''' + This base class for the Recline view extensions. + ''' + p.implements(p.IConfigurer, inherit=True) + p.implements(p.IResourceView, inherit=True) + p.implements(p.ITemplateHelpers, inherit=True) + p.implements(p.IRoutes, inherit=True) + + def update_config(self, config): + ''' + Set up the resource library, public directory and + template directory for the view + ''' + toolkit.add_public_directory(config, 'public') + toolkit.add_template_directory(config, 'templates') + toolkit.add_resource('public', 'dataexplorer') + + # IRoutes + + def before_map(self, map): + ctrl = 'ckanext.dataexplorer.controllers.dataexplorer:DataExplorer' + + map.connect('resource_extract', + '/dataexplorer/extract', + controller=ctrl, + action='extract') + return map + + def can_view(self, data_dict): + resource = data_dict['resource'] + return (resource.get('datastore_active') or + '_datastore_only_resource' in resource.get('url', '')) + + def setup_template_variables(self, context, data_dict): + return {'resource_json': json.dumps(data_dict['resource']), + 'resource_view_json': json.dumps(data_dict['resource_view'])} + + def view_template(self, context, data_dict): + return 'recline_view.html' + + def get_helpers(self): + return { + 'get_map_config': get_mapview_config + } + + +class ReclineView(ReclineViewBase): + ''' + This extension views resources using a Recline MultiView. + ''' + + def info(self): + return {'name': 'recline_view', + 'title': 'Data Explorer', + 'filterable': True, + 'icon': 'table', + 'requires_datastore': False, + 'default_title': p.toolkit._('Data Explorer'), + } + + def can_view(self, data_dict): + resource = data_dict['resource'] + + if (resource.get('datastore_active') or + '_datastore_only_resource' in resource.get('url', '')): + return True + resource_format = resource.get('format', None) + if resource_format: + return resource_format.lower() in ['csv', 'xls', 'xlsx', 'tsv'] + else: + return False + + +class ReclineGridView(ReclineViewBase): + ''' + This extension views resources using a Recline grid. + ''' + + def info(self): + return {'name': 'recline_grid_view', + 'title': 'Grid', + 'filterable': True, + 'icon': 'table', + 'requires_datastore': True, + 'default_title': p.toolkit._('Table'), + } + + +class ReclineGraphView(ReclineViewBase): + ''' + This extension views resources using a Recline graph. + ''' + + graph_types = [{'value': 'lines-and-points', + 'text': 'Lines and points'}, + {'value': 'lines', 'text': 'Lines'}, + {'value': 'points', 'text': 'Points'}, + {'value': 'bars', 'text': 'Bars'}, + {'value': 'columns', 'text': 'Columns'}] + + datastore_fields = [] + + datastore_field_types = ['numeric', 'int4', 'timestamp'] + + def list_graph_types(self): + return [t['value'] for t in self.graph_types] + + def list_datastore_fields(self): + return [t['value'] for t in self.datastore_fields] + + def info(self): + # in_list validator here is passed functions because this + # method does not know what the possible values of the + # datastore fields are (requires a datastore search) + schema = { + 'offset': [ignore_empty, natural_number_validator], + 'limit': [ignore_empty, natural_number_validator], + 'graph_type': [ignore_empty, in_list(self.list_graph_types)], + 'group': [ignore_empty, in_list(self.list_datastore_fields)], + 'series': [ignore_empty, in_list(self.list_datastore_fields)] + } + return {'name': 'recline_graph_view', + 'title': 'Graph', + 'filterable': True, + 'icon': 'bar-chart-o', + 'requires_datastore': True, + 'schema': schema, + 'default_title': p.toolkit._('Graph'), + } + + def setup_template_variables(self, context, data_dict): + self.datastore_fields = datastore_fields(data_dict['resource'], + self.datastore_field_types) + vars = ReclineViewBase.setup_template_variables(self, context, + data_dict) + vars.update({'graph_types': self.graph_types, + 'graph_fields': self.datastore_fields}) + return vars + + def form_template(self, context, data_dict): + return 'recline_graph_form.html' + + +class ReclineMapView(ReclineViewBase): + ''' + This extension views resources using a Recline map. + ''' + + map_field_types = [{'value': 'lat_long', + 'text': 'Latitude / Longitude fields'}, + {'value': 'geojson', 'text': 'GeoJSON'}] + + datastore_fields = [] + + datastore_field_latlon_types = ['numeric'] + + datastore_field_geojson_types = ['text'] + + def list_map_field_types(self): + return [t['value'] for t in self.map_field_types] + + def list_datastore_fields(self): + return [t['value'] for t in self.datastore_fields] + + def info(self): + # in_list validator here is passed functions because this + # method does not know what the possible values of the + # datastore fields are (requires a datastore search) + schema = { + 'offset': [ignore_empty, natural_number_validator], + 'limit': [ignore_empty, natural_number_validator], + 'map_field_type': [ignore_empty, + in_list(self.list_map_field_types)], + 'latitude_field': [ignore_empty, + in_list(self.list_datastore_fields)], + 'longitude_field': [ignore_empty, + in_list(self.list_datastore_fields)], + 'geojson_field': [ignore_empty, + in_list(self.list_datastore_fields)], + 'auto_zoom': [ignore_empty], + 'cluster_markers': [ignore_empty] + } + return {'name': 'recline_map_view', + 'title': 'Map', + 'schema': schema, + 'filterable': True, + 'icon': 'map-marker', + 'default_title': p.toolkit._('Map'), + } + + def setup_template_variables(self, context, data_dict): + map_latlon_fields = datastore_fields( + data_dict['resource'], self.datastore_field_latlon_types) + map_geojson_fields = datastore_fields( + data_dict['resource'], self.datastore_field_geojson_types) + + self.datastore_fields = map_latlon_fields + map_geojson_fields + + vars = ReclineViewBase.setup_template_variables(self, context, + data_dict) + vars.update({'map_field_types': self.map_field_types, + 'map_latlon_fields': map_latlon_fields, + 'map_geojson_fields': map_geojson_fields + }) + return vars + + def form_template(self, context, data_dict): + return 'recline_map_form.html' diff --git a/ckanext/dataexplorer/public/.gitignore b/ckanext/dataexplorer/public/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/ckanext/dataexplorer/public/css/recline.css b/ckanext/dataexplorer/public/css/recline.css new file mode 100644 index 0000000..8d3b3e7 --- /dev/null +++ b/ckanext/dataexplorer/public/css/recline.css @@ -0,0 +1,394 @@ +body { + background-color: #fff; +} + +.recline-data-explorer { + position: relative; + overflow: auto; +} + +.recline-record-count { + float: left; + margin-left: 10px; + line-height: 26px; +} + +.loading-dialog { + width: 120px; +} + +.recline-slickgrid { + height: 600px; +} +/* +.recline-timeline .vmm-timeline { + height: 600px; +} + +.recline-pager .pagination { + margin: 0; + float: left; +} + +.recline-pager .pagination a { + line-height: 26px; +} + +.recline-pager .pagination input { + width: 30px; + margin: 0; + padding: 2px 4px; +} +*/ +/* +.recline-query-editor { + float: right; +} +*/ +.loading-spinner { + float: right; + background-image: url('../img/ajaxload-circle.gif'); + width: 32px; + height: 32px; +} + +.recline-data-explorer .data-view-sidebar { + float: right; + margin-left: 8px; + width: 220px; +} + +.recline-data-explorer .header .navigation { + margin-bottom: 8px; +} + +.recline-data-explorer .header .navigation, +.recline-data-explorer .header .pagination, +.recline-data-explorer .header .pagination form +{ + display: inline; +} + +.recline-data-explorer .header .navigation { + float: left; +} + +.recline-data-explorer .header .menu-right { + float: right; + margin-left: 5px; + padding-left: 5px; +} + +.recline-results-info { + line-height: 35px; + margin-left: 20px; + float: left; +} + +.recline-data-explorer .data-view-sidebar > div { + margin-top: 5px; + margin-bottom: 10px; +} + +.recline-data-explorer .radio, +.recline-data-explorer .checkbox { + padding-left: 20px; +} + +.recline-data-explorer .editor-update-map { + margin: 30px 0px 20px 0px; +} + +.recline-data-explorer label { + font-weight: normal; +} + +/********************************************************** + * Query Editor + *********************************************************/ + +.recline-query-editor { + float: right; + height: 35px; + padding-right: 5px; + margin-right: 5px; + border-right: solid 2px #ddd; +} + +.header .input-prepend { + margin-bottom: auto; +} + +.header .add-on { + float: left; +} + +/* needed for Chrome but not FF */ +.header .add-on { + margin-left: -27px; +} + +/* needed for FF but not chrome */ +.header .input-prepend { + vertical-align: top; +} + +.recline-query-editor form button { + vertical-align: top; +} + +/* label for screen reader */ +.recline-query-editor .form-inline label { + position: absolute; + top:0; + left:-9999px +} + +/********************************************************** + * Pager + *********************************************************/ + +.recline-pager { + float: left; + margin: auto; + display: block; + margin-left: 20px; +} + +.recline-pager .pagination li { + display: inline-block; +} + +.recline-pager .pagination label { + display:none; +} + +.recline-pager .pagination input { + width: 40px; + height: 25px; + padding: 2px 4px; + margin: 0; + margin-top: -2px; + + border: 1px solid #cccccc; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + transition: border linear 0.2s, box-shadow linear 0.2s; + border-radius: 4px; + + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -webkit-transition: border linear 0.2s, box-shadow linear 0.2s; + -webkit-border-radius: 4px; + + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -moz-transition: border linear 0.2s, box-shadow linear 0.2s; + -moz-border-radius: 4px; + + -o-transition: border linear 0.2s, box-shadow linear 0.2s; +} + +.recline-pager .pagination a { + float: none; + margin-left: -5px; + color: #555; +} + +.recline-pager .pagination .page-range { + height: 34px; + padding: 5px 8px; + margin-left: -5px; + border: 1px solid #ddd; +} + +.recline-pager .pagination .page-range a { + padding: 0px 12px; + border: none; +} + +.recline-pager .pagination .page-range a:hover { + background-color: #ffffff; +} + +.recline-pager .pagination > li:first-child > a { + border-bottom-left-radius: 4px; + border-top-left-radius: 4px; + border-bottom-right-radius: 0px; + border-top-right-radius: 0px; + height: 34px; +} + +.recline-pager .pagination > li:last-child > a { + border-bottom-right-radius: 4px; + border-top-right-radius: 4px; + border-bottom-left-radius: 0px; + border-top-left-radius: 0px; + height: 34px; +} + +/********************************************************** + * Filter Editor + *********************************************************/ + +.recline-filter-editor { + padding: 8px; + display: none; +} + +.recline-filter-editor .filters { + margin: 20px 0px; +} + +.recline-filter-editor h3 { + margin-top: 4px; +} + +.recline-filter-editor .filter { + margin-top: 20px; +} + +.recline-filter-editor .filter .form-group { + margin-bottom: 0px; +} + +.recline-filter-editor .filter input, +.recline-filter-editor .filter label { + margin: 0px; +} + +.recline-filter-editor .js-edit button { + margin: 25px 0px 0px 0px; +} + +.recline-filter-editor .filter-term a { + font-size: 18px; +} + +.recline-filter-editor input, +.recline-filter-editor select +{ + width: 175px; +} + +.recline-filter-editor input { + margin-top: 0.5em; + margin-bottom: 10px; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + border: 1px solid #cccccc; +} + +.recline-filter-editor label { + font-weight: normal; + display: block; +} + +.recline-filter-editor legend { + margin-bottom: 5px; +} + +.recline-filter-editor .add-filter { + margin-top: 1em; + margin-bottom: 2em; +} + +.recline-filter-editor .update-filter { + margin-top: 1em; +} + +/********************************************************** + * Extract & Download + *********************************************************/ + +.recline-data-extractor { + padding: 8px; + display: none; +} + +.recline-data-extractor select +{ + width: 175px; +} + +/********************************************************** + * Fields Widget + *********************************************************/ + +.recline-fields-view { + display: none; +} + +.recline-fields-view .fields-list { + padding: 0; +} + +.recline-fields-view .panel { + background-color: #f5f5f5; + border: 1px solid #e5e5e5; +} + +.recline-fields-view .panel-group h3 { + padding-left: 10px; +} + +.recline-fields-view .fields-list .panel-heading { + padding: 2px 5px; + margin: 1px 0px 1px 5px; +} + +.recline-fields-view .panel a, +.recline-fields-view .panel h4 { + display: inline; +} + +.recline-fields-view .panel a { + padding: 0; +} + +.recline-fields-view .panel h4 { + word-wrap: break-word +} + +.recline-fields-view .clear { + clear: both; +} + +.recline-fields-view .facet-items { + list-style-type: none; + margin-left: 0; +} + +.recline-fields-view .facet-item .term { + font-weight: bold; +} + +.recline-fields-view .facet-item .count { +} + +/********************************************************** + * Notifications + *********************************************************/ + +.recline-data-explorer .notification-loader { + width: 18px; + margin-left: 5px; + background-image: url(data:image/gif;base64,R0lGODlhEAAQAPQAAP///wAAAPDw8IqKiuDg4EZGRnp6egAAAFhYWCQkJKysrL6+vhQUFJycnAQEBDY2NmhoaAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh/hpDcmVhdGVkIHdpdGggYWpheGxvYWQuaW5mbwAh+QQJCgAAACwAAAAAEAAQAAAFdyAgAgIJIeWoAkRCCMdBkKtIHIngyMKsErPBYbADpkSCwhDmQCBethRB6Vj4kFCkQPG4IlWDgrNRIwnO4UKBXDufzQvDMaoSDBgFb886MiQadgNABAokfCwzBA8LCg0Egl8jAggGAA1kBIA1BAYzlyILczULC2UhACH5BAkKAAAALAAAAAAQABAAAAV2ICACAmlAZTmOREEIyUEQjLKKxPHADhEvqxlgcGgkGI1DYSVAIAWMx+lwSKkICJ0QsHi9RgKBwnVTiRQQgwF4I4UFDQQEwi6/3YSGWRRmjhEETAJfIgMFCnAKM0KDV4EEEAQLiF18TAYNXDaSe3x6mjidN1s3IQAh+QQJCgAAACwAAAAAEAAQAAAFeCAgAgLZDGU5jgRECEUiCI+yioSDwDJyLKsXoHFQxBSHAoAAFBhqtMJg8DgQBgfrEsJAEAg4YhZIEiwgKtHiMBgtpg3wbUZXGO7kOb1MUKRFMysCChAoggJCIg0GC2aNe4gqQldfL4l/Ag1AXySJgn5LcoE3QXI3IQAh+QQJCgAAACwAAAAAEAAQAAAFdiAgAgLZNGU5joQhCEjxIssqEo8bC9BRjy9Ag7GILQ4QEoE0gBAEBcOpcBA0DoxSK/e8LRIHn+i1cK0IyKdg0VAoljYIg+GgnRrwVS/8IAkICyosBIQpBAMoKy9dImxPhS+GKkFrkX+TigtLlIyKXUF+NjagNiEAIfkECQoAAAAsAAAAABAAEAAABWwgIAICaRhlOY4EIgjH8R7LKhKHGwsMvb4AAy3WODBIBBKCsYA9TjuhDNDKEVSERezQEL0WrhXucRUQGuik7bFlngzqVW9LMl9XWvLdjFaJtDFqZ1cEZUB0dUgvL3dgP4WJZn4jkomWNpSTIyEAIfkECQoAAAAsAAAAABAAEAAABX4gIAICuSxlOY6CIgiD8RrEKgqGOwxwUrMlAoSwIzAGpJpgoSDAGifDY5kopBYDlEpAQBwevxfBtRIUGi8xwWkDNBCIwmC9Vq0aiQQDQuK+VgQPDXV9hCJjBwcFYU5pLwwHXQcMKSmNLQcIAExlbH8JBwttaX0ABAcNbWVbKyEAIfkECQoAAAAsAAAAABAAEAAABXkgIAICSRBlOY7CIghN8zbEKsKoIjdFzZaEgUBHKChMJtRwcWpAWoWnifm6ESAMhO8lQK0EEAV3rFopIBCEcGwDKAqPh4HUrY4ICHH1dSoTFgcHUiZjBhAJB2AHDykpKAwHAwdzf19KkASIPl9cDgcnDkdtNwiMJCshACH5BAkKAAAALAAAAAAQABAAAAV3ICACAkkQZTmOAiosiyAoxCq+KPxCNVsSMRgBsiClWrLTSWFoIQZHl6pleBh6suxKMIhlvzbAwkBWfFWrBQTxNLq2RG2yhSUkDs2b63AYDAoJXAcFRwADeAkJDX0AQCsEfAQMDAIPBz0rCgcxky0JRWE1AmwpKyEAIfkECQoAAAAsAAAAABAAEAAABXkgIAICKZzkqJ4nQZxLqZKv4NqNLKK2/Q4Ek4lFXChsg5ypJjs1II3gEDUSRInEGYAw6B6zM4JhrDAtEosVkLUtHA7RHaHAGJQEjsODcEg0FBAFVgkQJQ1pAwcDDw8KcFtSInwJAowCCA6RIwqZAgkPNgVpWndjdyohACH5BAkKAAAALAAAAAAQABAAAAV5ICACAimc5KieLEuUKvm2xAKLqDCfC2GaO9eL0LABWTiBYmA06W6kHgvCqEJiAIJiu3gcvgUsscHUERm+kaCxyxa+zRPk0SgJEgfIvbAdIAQLCAYlCj4DBw0IBQsMCjIqBAcPAooCBg9pKgsJLwUFOhCZKyQDA3YqIQAh+QQJCgAAACwAAAAAEAAQAAAFdSAgAgIpnOSonmxbqiThCrJKEHFbo8JxDDOZYFFb+A41E4H4OhkOipXwBElYITDAckFEOBgMQ3arkMkUBdxIUGZpEb7kaQBRlASPg0FQQHAbEEMGDSVEAA1QBhAED1E0NgwFAooCDWljaQIQCE5qMHcNhCkjIQAh+QQJCgAAACwAAAAAEAAQAAAFeSAgAgIpnOSoLgxxvqgKLEcCC65KEAByKK8cSpA4DAiHQ/DkKhGKh4ZCtCyZGo6F6iYYPAqFgYy02xkSaLEMV34tELyRYNEsCQyHlvWkGCzsPgMCEAY7Cg04Uk48LAsDhRA8MVQPEF0GAgqYYwSRlycNcWskCkApIyEAOwAAAAAAAAAAAA%3D%3D); + display: inline-block; + background-repeat: no-repeat; +} + +.recline-data-explorer .alert-loader { + position: absolute; + width: 200px; + left: 50%; + margin-left: -100px; + z-index: 10000; + padding: 40px 0px 40px 0px; + margin-top: -10px; + text-align: center; + font-size: 16px; + font-weight: bold; + -webkit-border-radius: 0px; + -moz-border-radius: 0px; + border-radius: 0px; + border-top: none; +} diff --git a/ckanext/dataexplorer/public/css/recline.min.css b/ckanext/dataexplorer/public/css/recline.min.css new file mode 100644 index 0000000..44fd9cb --- /dev/null +++ b/ckanext/dataexplorer/public/css/recline.min.css @@ -0,0 +1 @@ +body{background-color:#fff}.recline-data-explorer{position:relative;overflow:auto}.recline-record-count{float:left;margin-left:10px;line-height:26px}.loading-dialog{width:120px}.recline-slickgrid{height:600px}.loading-spinner{float:right;background-image:url('../img/ajaxload-circle.gif');width:32px;height:32px}.recline-data-explorer .data-view-sidebar{float:right;margin-left:8px;width:220px}.recline-data-explorer .header .navigation{margin-bottom:8px}.recline-data-explorer .header .navigation,.recline-data-explorer .header .pagination,.recline-data-explorer .header .pagination form{display:inline}.recline-data-explorer .header .navigation{float:left}.recline-data-explorer .header .menu-right{float:right;margin-left:5px;padding-left:5px}.recline-results-info{line-height:35px;margin-left:20px;float:left}.recline-data-explorer .data-view-sidebar>div{margin-top:5px;margin-bottom:10px}.recline-data-explorer .radio,.recline-data-explorer .checkbox{padding-left:20px}.recline-data-explorer .editor-update-map{margin:30px 0px 20px 0px}.recline-data-explorer label{font-weight:normal}.recline-query-editor{float:right;height:35px;padding-right:5px;margin-right:5px;border-right:solid 2px #ddd}.header .input-prepend{margin-bottom:auto}.header .add-on{float:left}.header .add-on{margin-left:-27px}.header .input-prepend{vertical-align:top}.recline-query-editor form button{vertical-align:top}.recline-query-editor .form-inline label{position:absolute;top:0;left:-9999px}.recline-pager{float:left;margin:auto;display:block;margin-left:20px}.recline-pager .pagination li{display:inline-block}.recline-pager .pagination label{display:none}.recline-pager .pagination input{width:40px;height:25px;padding:2px 4px;margin:0;margin-top:-2px;border:1px solid #cccccc;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);transition:border linear 0.2s,box-shadow linear 0.2s;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-webkit-transition:border linear 0.2s,box-shadow linear 0.2s;-webkit-border-radius:4px;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-transition:border linear 0.2s,box-shadow linear 0.2s;-moz-border-radius:4px;-o-transition:border linear 0.2s,box-shadow linear 0.2s}.recline-pager .pagination a{float:none;margin-left:-5px;color:#555}.recline-pager .pagination .page-range{height:34px;padding:5px 8px;margin-left:-5px;border:1px solid #ddd}.recline-pager .pagination .page-range a{padding:0px 12px;border:none}.recline-pager .pagination .page-range a:hover{background-color:#ffffff}.recline-pager .pagination>li:first-child>a{border-bottom-left-radius:4px;border-top-left-radius:4px;border-bottom-right-radius:0px;border-top-right-radius:0px;height:34px}.recline-pager .pagination>li:last-child>a{border-bottom-right-radius:4px;border-top-right-radius:4px;border-bottom-left-radius:0px;border-top-left-radius:0px;height:34px}.recline-filter-editor{padding:8px;display:none}.recline-filter-editor .filters{margin:20px 0px}.recline-filter-editor h3{margin-top:4px}.recline-filter-editor .filter{margin-top:20px}.recline-filter-editor .filter .form-group{margin-bottom:0px}.recline-filter-editor .filter input,.recline-filter-editor .filter label{margin:0px}.recline-filter-editor .js-edit button{margin:25px 0px 0px 0px}.recline-filter-editor .filter-term a{font-size:18px}.recline-filter-editor input,.recline-filter-editor select{width:175px}.recline-filter-editor input{margin-top:0.5em;margin-bottom:10px;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;border:1px solid #cccccc}.recline-filter-editor label{font-weight:normal;display:block}.recline-filter-editor legend{margin-bottom:5px}.recline-filter-editor .add-filter{margin-top:1em;margin-bottom:2em}.recline-filter-editor .update-filter{margin-top:1em}.recline-fields-view{display:none}.recline-fields-view .fields-list{padding:0}.recline-fields-view .panel{background-color:#f5f5f5;border:1px solid #e5e5e5}.recline-fields-view .panel-group h3{padding-left:10px}.recline-fields-view .fields-list .panel-heading{padding:2px 5px;margin:1px 0px 1px 5px}.recline-fields-view .panel a,.recline-fields-view .panel h4{display:inline}.recline-fields-view .panel a{padding:0}.recline-fields-view .panel h4{word-wrap:break-word}.recline-fields-view .clear{clear:both}.recline-fields-view .facet-items{list-style-type:none;margin-left:0}.recline-fields-view .facet-item .term{font-weight:bold}.recline-fields-view .facet-item .count{}.recline-data-explorer .notification-loader{width:18px;margin-left:5px;background-image:url(data:image/gif;base64,R0lGODlhEAAQAPQAAP///wAAAPDw8IqKiuDg4EZGRnp6egAAAFhYWCQkJKysrL6+vhQUFJycnAQEBDY2NmhoaAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh/hpDcmVhdGVkIHdpdGggYWpheGxvYWQuaW5mbwAh+QQJCgAAACwAAAAAEAAQAAAFdyAgAgIJIeWoAkRCCMdBkKtIHIngyMKsErPBYbADpkSCwhDmQCBethRB6Vj4kFCkQPG4IlWDgrNRIwnO4UKBXDufzQvDMaoSDBgFb886MiQadgNABAokfCwzBA8LCg0Egl8jAggGAA1kBIA1BAYzlyILczULC2UhACH5BAkKAAAALAAAAAAQABAAAAV2ICACAmlAZTmOREEIyUEQjLKKxPHADhEvqxlgcGgkGI1DYSVAIAWMx+lwSKkICJ0QsHi9RgKBwnVTiRQQgwF4I4UFDQQEwi6/3YSGWRRmjhEETAJfIgMFCnAKM0KDV4EEEAQLiF18TAYNXDaSe3x6mjidN1s3IQAh+QQJCgAAACwAAAAAEAAQAAAFeCAgAgLZDGU5jgRECEUiCI+yioSDwDJyLKsXoHFQxBSHAoAAFBhqtMJg8DgQBgfrEsJAEAg4YhZIEiwgKtHiMBgtpg3wbUZXGO7kOb1MUKRFMysCChAoggJCIg0GC2aNe4gqQldfL4l/Ag1AXySJgn5LcoE3QXI3IQAh+QQJCgAAACwAAAAAEAAQAAAFdiAgAgLZNGU5joQhCEjxIssqEo8bC9BRjy9Ag7GILQ4QEoE0gBAEBcOpcBA0DoxSK/e8LRIHn+i1cK0IyKdg0VAoljYIg+GgnRrwVS/8IAkICyosBIQpBAMoKy9dImxPhS+GKkFrkX+TigtLlIyKXUF+NjagNiEAIfkECQoAAAAsAAAAABAAEAAABWwgIAICaRhlOY4EIgjH8R7LKhKHGwsMvb4AAy3WODBIBBKCsYA9TjuhDNDKEVSERezQEL0WrhXucRUQGuik7bFlngzqVW9LMl9XWvLdjFaJtDFqZ1cEZUB0dUgvL3dgP4WJZn4jkomWNpSTIyEAIfkECQoAAAAsAAAAABAAEAAABX4gIAICuSxlOY6CIgiD8RrEKgqGOwxwUrMlAoSwIzAGpJpgoSDAGifDY5kopBYDlEpAQBwevxfBtRIUGi8xwWkDNBCIwmC9Vq0aiQQDQuK+VgQPDXV9hCJjBwcFYU5pLwwHXQcMKSmNLQcIAExlbH8JBwttaX0ABAcNbWVbKyEAIfkECQoAAAAsAAAAABAAEAAABXkgIAICSRBlOY7CIghN8zbEKsKoIjdFzZaEgUBHKChMJtRwcWpAWoWnifm6ESAMhO8lQK0EEAV3rFopIBCEcGwDKAqPh4HUrY4ICHH1dSoTFgcHUiZjBhAJB2AHDykpKAwHAwdzf19KkASIPl9cDgcnDkdtNwiMJCshACH5BAkKAAAALAAAAAAQABAAAAV3ICACAkkQZTmOAiosiyAoxCq+KPxCNVsSMRgBsiClWrLTSWFoIQZHl6pleBh6suxKMIhlvzbAwkBWfFWrBQTxNLq2RG2yhSUkDs2b63AYDAoJXAcFRwADeAkJDX0AQCsEfAQMDAIPBz0rCgcxky0JRWE1AmwpKyEAIfkECQoAAAAsAAAAABAAEAAABXkgIAICKZzkqJ4nQZxLqZKv4NqNLKK2/Q4Ek4lFXChsg5ypJjs1II3gEDUSRInEGYAw6B6zM4JhrDAtEosVkLUtHA7RHaHAGJQEjsODcEg0FBAFVgkQJQ1pAwcDDw8KcFtSInwJAowCCA6RIwqZAgkPNgVpWndjdyohACH5BAkKAAAALAAAAAAQABAAAAV5ICACAimc5KieLEuUKvm2xAKLqDCfC2GaO9eL0LABWTiBYmA06W6kHgvCqEJiAIJiu3gcvgUsscHUERm+kaCxyxa+zRPk0SgJEgfIvbAdIAQLCAYlCj4DBw0IBQsMCjIqBAcPAooCBg9pKgsJLwUFOhCZKyQDA3YqIQAh+QQJCgAAACwAAAAAEAAQAAAFdSAgAgIpnOSonmxbqiThCrJKEHFbo8JxDDOZYFFb+A41E4H4OhkOipXwBElYITDAckFEOBgMQ3arkMkUBdxIUGZpEb7kaQBRlASPg0FQQHAbEEMGDSVEAA1QBhAED1E0NgwFAooCDWljaQIQCE5qMHcNhCkjIQAh+QQJCgAAACwAAAAAEAAQAAAFeSAgAgIpnOSoLgxxvqgKLEcCC65KEAByKK8cSpA4DAiHQ/DkKhGKh4ZCtCyZGo6F6iYYPAqFgYy02xkSaLEMV34tELyRYNEsCQyHlvWkGCzsPgMCEAY7Cg04Uk48LAsDhRA8MVQPEF0GAgqYYwSRlycNcWskCkApIyEAOwAAAAAAAAAAAA%3D%3D);display:inline-block}.recline-data-explorer .alert-loader{position:absolute;width:200px;left:50%;margin-left:-100px;z-index:10000;padding:40px 0px 40px 0px;margin-top:-10px;text-align:center;font-size:16px;font-weight:bold;-webkit-border-radius:0px;-moz-border-radius:0px;border-radius:0px;border-top:none} \ No newline at end of file diff --git a/ckanext/dataexplorer/public/img/ajaxload-circle.gif b/ckanext/dataexplorer/public/img/ajaxload-circle.gif new file mode 100644 index 0000000000000000000000000000000000000000..e15844396f9b8c4d915f5cf595e2a22014a24426 GIT binary patch literal 4176 zcmb`KYgAK*md8&{PI6984tX4MLVzSBA%TD-1Qd!GFefCDmzZGG)JGc-y3jHfEn2R% zy?K*FpaCNlZnXgcAMI7Ns8}Bp5GkUy$fDLdy)J5P2OaCs4!2{wy6V_j_v`I^nf-aM zz4qF__1}B{SCp9YmR3ms3GfpDJbU&G0Dxb9`Q`ro`_t3YGcz+!o;>;W*I$47>8Ii0 z;rjY|gTYWyQQ`OdZ{NPXu&^*bK3-m44ndGuES{a6ee~$jp+koT1_plo@yB2=n2?Z= zo16Rb#~0fL;fF$@aAIPD&*!5knv#;@@pwi@M+*xJaU5@KZ0zXh2!%odfuNMOh5e){z3!NI|%rly-WZ@OGAo6VM;ojo=-_Ta&Tr%#{G z&(DAN-FNBf>DATMFTM2A+}vD4L&Nil{eOk@8MDG@U0p<)joE4qmGZo<^tXhf9ko@B zwKbAAwlvmBs_gJPHeVXFgqG$8g^Qv69&tLBgT;^`WT}qP<06t4;X^4&1PrsZgqIMp10X{(G@i%^ z0p~qwWu^+m&*n?V&+w<4p+p5r_akw#FK}sUxS9CumLG<&j4v?5wsJvsZrUv%0nU%8* zGAFgAie1oDC51EJ3>?V)*zM4dL!9NgR~6OH@yeZYyMUT4S2ri9b8CubC`N3v)TAtO zkcq2tMV+z7b=2j$F{FD6NFqpwJatF4nc^;Cq#2mIW+I0_X`y`F5>?*10uH}n+A8SE zJ~QQp78ly)8g%brn@w6If3~NDa@`MEkA~RJQ1VczAr*RoNBP@D)vqhdy2mj^%9nAt z-ErZt?w`YQ!-UnE276oE+Sr^DrLruO{Z3aaxWtrz;|V4X*gXIcWwDV+YzcA1OJ_j@ zNSew&jF`w%E|?Xg#B3`KrVF^@(!T;!`3HEuvZ7=BSC!xlrxSC-zq33+rfZ|$NTc6~@jbG-&^qowT+xH?whc8z1U{NCPF zOr|V$^lVPXXg&@@AGy7JpGT9IbsF%}HfWe40p0^0jY*Y^>wyuF5@f$^UqpiYK4O{l zN(XJ0($>Fn*>1ngZO{U6_d+;YUBv7;!fDJZ??!+ROiCf_!Hj&?0buM$ zPj+6XFnE7HBF=m8X6;BY*>V~YheyExSCO>F9I6=QDl0^y!Rf<>))eCKV7Ej5`q)|f ziUXl&ZD|UYXV5rLNyuYVng>anI_vI0$}Vpm=Nc!8OMtdG>y!*7^u*S#e1{XP7}V}| zJQ~@2`kGXta|)_2Z}tkMubV|Jmx|keUVvfnp9Av(urCk{z=UxV$=DvgSOg}U-SJU@vlF!1wIS>Rql1-b#%5~_{E{T3*7gDfQx z&3rj9#8fEqF~&;nP4%icu3}h?S_AZ${Na?PWcrG}3~ohy%1xXpT~DcKTo3-of|N8|q4rf~;gy#?08V zhdPwa%HJ+tc{i3$U0=q|*8$h?qz+}jWLA;W!?G-i0?7F(w=gGcJ^)ow(_+oli%NR{ zlSj7CRqwRi|K@Kn&&R?Wj41C`P@EU&_|swlOYW6Rbu+0YrHB%GxNKr%IC<&44^pfS zGLW1%ckX=&ePcxYCYA3ZcX<@W>m`3c5(Z)K^F8UJL#tQv0)0J0KgVtlbSX9!OyIlebD=fHuxcH@*26)Clxsov(o4PwC> zv%S|7P(p&77tNF5K>Tk!QB%+8L`TYHe$3Q#vJVm{^ZGV4PLbZOAe1*z<#B>ivT}Cg z8KfwrUe}Out)VuWsu9t#GmU`DC03UMce&? z`o-W|hAGN}$#P0seJp4)IQn_2VuX=4Yos!zG#c!UjJj9Rt-`?6xBvY;$+nrPre|yP z6gbnrPSWt3%4Yu~lD}U(KtLKucCg*c)>^ozR?yJ_(@Yv%V==<+{=XpXBXD6Nep06e z0YqdZy8|#Qj&BITnOp^~z&3$&28vmdiGavfkZs}g6U%kgOg@1WZOHFot_?L8<(w?w z|2rY%ubdJr&pwGST0G^04Oj5QiuYn0QHN8w;>r1@Y`}stm9iys=0Of9W|nHwDEeac z+k}5gV7_IhF|l039nx~k+0hJHo2tC9lOnKh1H0^>;7o|a+kO6Zj*G7>ujBMMqQ_uE zhV9F9AGx{JC(CP!DfV~4EQuNmLp`5dsy5{}dIxnbCb4ni4Sf5~lLlV8ODjJ%Vh>Io zr1rMFsi2prDWW|WHRkYISNcZ1qqs6MOQGa*MH&vIXDXMkhq^vLzyZOfy%Dp0guFTJ zb=kQ;pyQx)J6kk2KU($*#Lv~7PYnWFibRDOI{ldtMdkia=irWu9H%OlceEf}U9zw) zXTX|j{d%;u+yh;t-~9GNR&pO+@U-(cnwcaIEeu-pMbKCM1weajKVk-07C}#x@#*(+ z$6Krel};+nJa5?o9tHo+C)_1K{y?U`!*#*zmxJ##3o`Yqd0^-OyvbCcTS`3;Vw+QN z#RdcHhL4Bfcin7$_J9dG4)Zb%#9#gNP3y=((D;f3;}ZZ3l`^@D$;)1lu|ZNIb|b9S zoJ&Aj@erP<7x)D+u{g7E#>+#W%YM=N>&=LYTY#R!*VLJVw76REq;2j;9T!9@b7!J@ z;C2rc(b&ot98p5dO7KmNJsNShCUd38j&H}9R1eF3)FDv-6i#o$!*(p?BUJy&5^ns~ z=)PS8W*x_4N6`2rU8~&Dd+3rlmCiMDD8b8Sve#A%y5Mu*roOgvDdcAfWY-ECmAubB zr?UCIhA%CB1D}#0{i)zqFOvci<^e57e#Ir0Y}4$qo-?D&#K$E_d(8=*82S|iR@=XD zWYT)0QyJ3H(BuovnhCQn-kXFCJItX-$(q~D?&vEmHK>nDXkI;Je=yogbXOUqkvUz^ zm-pvaJKKAV`;+ri48tK=7!c>!${fzCBOwvmQT{VpqT7d*+y3JK`H{XEeb~qI;OaU) z6;W~i9tW_ORCySRDBwa5sRY-V@rIr;nn<~}`2Ujb^Cb)tqBOW=pNkfc@`-Q@!cR<4 zaytGJI4DyvmmCHJJRZA6P&Md=N=)W%7s0PYE7F4Wb?dbPH^QS4&nLGD)2K~`PyLW- z1xg0l$xw5p&Mljj!|HJAD;d=*Wq$H z0ZGMiiM%wNBwkz40FYk{z!i>YXpjEwU_&7gLY!_v>rT!PsTUkfCN4&SR*< zO*LzJgvY3vy$c*?@71g#${Pv0fv;Dv#6>hlWOnly0j9<=I1Y)C=5aqdW{cJqg%?%b zV)rXMBAc(H#k<4EkrAYIk0-rFQ|Tt#SRD40j(cLeUr4YVKUDGnki|81-myzR&1tYvZF#JB=sC)?QM5xpF+KjI1F+%{~Xyu%mI{br&Jrwh&rfOH<292LR zA-NQx!O7~7 zAj3F|PV|{?QneX_>$X*LG{rh*zkP(CI1`C->Qr+7Tk9I8oR%Q;M^Do~EC~R=47YXw zY^YGwzHcwNN@*s#_ZpPaB&vr^XScr7^_=Xg=ST@vj4BcYMTOK8U>a}81r}i`7y}8? z`5RPYexza}o*4H*fW2~=;YI+apkyS5x%db8muCD2N2F^+F7V;lb4B69>J*LS6sh)Hl7?5Q9L5x{PD(Dj^4ZIy?Hn zs1{li@-;Oh#;oO-waz{|0C85za^-fz3I<0n)(+!6o)Q7dKV6z7LxULW$xdD6W)O?V z28v&`I*Z?B&CWI*G&_x6S`sZ?S=;#8lpCd0?74%}VCN+-WSxt=z;(r0+QNizP8V+tDI8ic|b!Wg(ANO~_O93Qi3awQoi+bCm(?xG64ACMjXtYIfUBifj Y#`Dx6nd>%kIx0NRp)dR0fBtX$58}9UkpKVy literal 0 HcmV?d00001 diff --git a/ckanext/dataexplorer/public/recline_view.js b/ckanext/dataexplorer/public/recline_view.js new file mode 100644 index 0000000..6b3bc6f --- /dev/null +++ b/ckanext/dataexplorer/public/recline_view.js @@ -0,0 +1,249 @@ +this.ckan.module('recline_view', function (jQuery, _) { + return { + options: { + site_url: "", + controlsClassName: "controls" + }, + + initialize: function () { + jQuery.proxyAll(this, /_on/); + this.options.resource = JSON.parse(this.options.resource); + this.options.resourceView = JSON.parse(this.options.resourceView); + this.el.ready(this._onReady); + // hack to make leaflet use a particular location to look for images + L.Icon.Default.imagePath = this.options.site_url + 'vendor/leaflet/0.7.7/images'; + }, + + _onReady: function() { + var resourceData = this.options.resource, + resourceView = this.options.resourceView; + + this.loadView(resourceData, resourceView); + }, + + loadView: function (resourceData, reclineView) { + var self = this; + + function showError(msg){ + msg = msg || self._('error loading view').fetch(); + window.parent.ckan.pubsub.publish('data-viewer-error', msg); + } + + if (resourceData.formatNormalized === '') { + var tmp = resourceData.url.split('/'); + tmp = tmp[tmp.length - 1]; + tmp = tmp.split('?'); // query strings + tmp = tmp[0]; + var ext = tmp.split('.'); + if (ext.length > 1) { + resourceData.formatNormalized = ext[ext.length-1]; + } + } + + var errorMsg, dataset, map_config; + + if (!resourceData.datastore_active) { + recline.Backend.DataProxy.timeout = 10000; + resourceData.backend = 'dataproxy'; + } else { + resourceData.backend = 'ckan'; + resourceData.endpoint = jQuery('body').data('site-root') + 'api'; + } + + dataset = new recline.Model.Dataset(resourceData); + + map_config = this.options.map_config; + + var query = new recline.Model.Query(); + query.set({ size: reclineView.limit || 100 }); + query.set({ from: reclineView.offset || 0 }); + + var urlFilters = {}; + try { + if (window.parent.ckan.views && window.parent.ckan.views.filters) { + urlFilters = window.parent.ckan.views.filters.get(); + } + } catch(e) {} + var defaultFilters = reclineView.filters || {}, + filters = jQuery.extend({}, defaultFilters, urlFilters); + jQuery.each(filters, function (field,values) { + query.addFilter({type: 'term', field: field, term: values}); + }); + + dataset.queryState.set(query.toJSON(), {silent: true}); + errorMsg = _('Could not load view').fetch() + ': '; + if (resourceData.backend == 'ckan') { + errorMsg += _('DataStore returned an error').fetch(); + } else if (resourceData.backend == 'dataproxy'){ + errorMsg += _('DataProxy returned an error').fetch(); + } + dataset.fetch() + .done(function(dataset){ + self.initializeView(dataset, reclineView); + }) + .fail(function(error){ + if (error.message) errorMsg += ' (' + error.message + ')'; + showError(errorMsg); + }); + }, + + initializeView: function (dataset, reclineView) { + var view, + state, + controls = []; + + if(reclineView.view_type === "recline_graph_view") { + state = { + "graphType": reclineView.graph_type, + "group": reclineView.group, + "series": [reclineView.series] + }; + view = new recline.View.Graph({model: dataset, state: state}); + } else if(reclineView.view_type === "recline_map_view") { + state = { + geomField: null, + latField: null, + lonField: null, + autoZoom: Boolean(reclineView.auto_zoom), + cluster: Boolean(reclineView.cluster_markers) + }; + + if(reclineView.map_field_type === "geojson") { + state.geomField = reclineView.geojson_field; + } else { + state.latField = reclineView.latitude_field; + state.lonField = reclineView.longitude_field; + } + + view = new recline.View.Map(this._reclineMapViewOptions(dataset, this.options.map_config)); + } else if(reclineView.view_type === "recline_view") { + view = this._newDataExplorer(dataset, this.options.map_config); + } else { + // default to Grid + view = new recline.View.SlickGrid({model: dataset}); + controls = [ + new recline.View.Pager({model: view.model}), + new recline.View.RecordCount({model: dataset}), + new recline.View.QueryEditor({model: view.model.queryState}) + ]; + } + + // recline_view automatically adds itself to the DOM, so we don't + // need to bother with it. + if(reclineView.view_type !== 'recline_view') { + var newElements = jQuery('
'); + this._renderControls(newElements, controls, this.options.controlsClassName); + newElements.append(view.el); + jQuery(this.el).html(newElements); + view.visible = true; + view.render(); + } + + if(reclineView.view_type === "recline_graph_view") { + view.redraw(); + } + }, + + _reclineMapViewOptions: function(dataset, map_config) { + var tile_url, attribution, subdomains; + tile_url = attribution = subdomains = ''; + + if (map_config.type == 'mapbox') { + // MapBox base map + if (!map_config['mapbox.map_id'] || !map_config['mapbox.access_token']) { + throw '[CKAN Map Widgets] You need to provide a map ID ' + + '([account].[handle]) and an access token when using a ' + + 'MapBox layer. See ' + + 'http://www.mapbox.com/developers/api-overview/ for details'; + } + tile_url = '//{s}.tiles.mapbox.com/v4/' + + map_config['mapbox.map_id'] + + '/{z}/{x}/{y}.png?access_token=' + + map_config['mapbox.access_token']; + handle = map_config['mapbox.map_id']; + subdomains = map_config.subdomains || 'abcd'; + attribution = map_config.attribution || + 'Data: OpenStreetMap, Design: MapBox'; + } else if (map_config.type == 'custom') { + // Custom XYZ layer + tile_url = map_config['custom.url'] || ''; + attribution = map_config.attribution || ''; + subdomains = map_config.subdomains || ''; + + if (map_config['custom.tms']) + var tms = map_config['custom.tms']; + } + + return { + model: dataset, + mapTilesURL: tile_url, + mapTilesAttribution: attribution, + mapTilesSubdomains: subdomains + }; + }, + + _newDataExplorer: function (dataset, map_config) { + var views = [ + { + id: 'grid', + label: _('Grid').fetch(), + view: new recline.View.SlickGrid({ + model: dataset + }) + }, + { + id: 'graph', + label: _('Graph').fetch(), + view: new recline.View.Graph({ + model: dataset + }) + }, + { + id: 'map', + label: _('Map').fetch(), + view: new recline.View.Map(this._reclineMapViewOptions(dataset, map_config)) + } + ]; + + var sidebarViews = [ + { + id: 'valueFilter', + label: _('Filters').fetch(), + view: new recline.View.ValueFilter({ + model: dataset + }) + }, + { + id: 'extractor', + label: _('Extract').fetch(), + view: new recline.View.Extractor({ + model: dataset + }) + } + ]; + + var dataExplorer = new recline.View.MultiView({ + el: this.el, + model: dataset, + views: views, + sidebarViews: sidebarViews, + config: { + readOnly: true + } + }); + + return dataExplorer; + }, + + _renderControls: function (el, controls, className) { + var controlsEl = jQuery("
"); + for (var i = 0; i < controls.length; i++) { + controlsEl.append(controls[i].el); + } + jQuery(el).append(controlsEl); + } + }; +}); diff --git a/ckanext/dataexplorer/public/recline_view.min.js b/ckanext/dataexplorer/public/recline_view.min.js new file mode 100644 index 0000000..b66728e --- /dev/null +++ b/ckanext/dataexplorer/public/recline_view.min.js @@ -0,0 +1,15 @@ +this.ckan.module('recline_view',function(jQuery){return{options:{site_url:"",controlsClassName:"controls"},initialize:function(){jQuery.proxyAll(this,/_on/);this.options.resource=JSON.parse(this.options.resource);this.options.resourceView=JSON.parse(this.options.resourceView);this.el.ready(this._onReady);L.Icon.Default.imagePath=this.options.site_url+'vendor/leaflet/0.7.7/images';},_onReady:function(){var resourceData=this.options.resource,resourceView=this.options.resourceView;this.loadView(resourceData,resourceView);},loadView:function(resourceData,reclineView){var self=this;function showError(msg){msg=msg||self._('error loading view');window.parent.ckan.pubsub.publish('data-viewer-error',msg);} +if(resourceData.formatNormalized===''){var tmp=resourceData.url.split('/');tmp=tmp[tmp.length-1];tmp=tmp.split('?');tmp=tmp[0];var ext=tmp.split('.');if(ext.length>1){resourceData.formatNormalized=ext[ext.length-1];}} +var errorMsg,dataset,map_config;if(!resourceData.datastore_active){recline.Backend.DataProxy.timeout=10000;resourceData.backend='dataproxy';}else{resourceData.backend='ckan';resourceData.endpoint=jQuery('body').data('site-root')+'api';} +dataset=new recline.Model.Dataset(resourceData);map_config=this.options.map_config;var query=new recline.Model.Query();query.set({size:reclineView.limit||100});query.set({from:reclineView.offset||0});var urlFilters={};try{if(window.parent.ckan.views&&window.parent.ckan.views.filters){urlFilters=window.parent.ckan.views.filters.get();}}catch(e){} +var defaultFilters=reclineView.filters||{},filters=jQuery.extend({},defaultFilters,urlFilters);jQuery.each(filters,function(field,values){query.addFilter({type:'term',field:field,term:values});});dataset.queryState.set(query.toJSON(),{silent:true});errorMsg=this._('Could not load view')+': ';if(resourceData.backend=='ckan'){errorMsg+=this._('DataStore returned an error');}else if(resourceData.backend=='dataproxy'){errorMsg+=this._('DataProxy returned an error');} +dataset.fetch().done(function(dataset){self.initializeView(dataset,reclineView);}).fail(function(error){if(error.message)errorMsg+=' ('+error.message+')';showError(errorMsg);});},initializeView:function(dataset,reclineView){var view,state,controls=[];if(reclineView.view_type==="recline_graph_view"){state={"graphType":reclineView.graph_type,"group":reclineView.group,"series":[reclineView.series]};view=new recline.View.Graph({model:dataset,state:state});}else if(reclineView.view_type==="recline_map_view"){state={geomField:null,latField:null,lonField:null,autoZoom:Boolean(reclineView.auto_zoom),cluster:Boolean(reclineView.cluster_markers)};if(reclineView.map_field_type==="geojson"){state.geomField=reclineView.geojson_field;}else{state.latField=reclineView.latitude_field;state.lonField=reclineView.longitude_field;} +view=new recline.View.Map(this._reclineMapViewOptions(dataset,this.options.map_config));}else if(reclineView.view_type==="recline_view"){view=this._newDataExplorer(dataset,this.options.map_config);}else{view=new recline.View.SlickGrid({model:dataset});controls=[new recline.View.Pager({model:view.model}),new recline.View.RecordCount({model:dataset}),new recline.View.QueryEditor({model:view.model.queryState})];} +if(reclineView.view_type!=='recline_view'){var newElements=jQuery('
');this._renderControls(newElements,controls,this.options.controlsClassName);newElements.append(view.el);jQuery(this.el).html(newElements);view.visible=true;view.render();} +if(reclineView.view_type==="recline_graph_view"){view.redraw();}},_reclineMapViewOptions:function(dataset,map_config){var tile_url,attribution,subdomains;tile_url=attribution=subdomains='';if(map_config.type=='mapbox'){if(!map_config['mapbox.map_id']||!map_config['mapbox.access_token']){throw'[CKAN Map Widgets] You need to provide a map ID '+'([account].[handle]) and an access token when using a '+'MapBox layer. See '+'http://www.mapbox.com/developers/api-overview/ for details';} +tile_url='//{s}.tiles.mapbox.com/v4/'+ +map_config['mapbox.map_id']+'/{z}/{x}/{y}.png?access_token='+ +map_config['mapbox.access_token'];handle=map_config['mapbox.map_id'];subdomains=map_config.subdomains||'abcd';attribution=map_config.attribution||'Data: OpenStreetMap, Design: MapBox';}else if(map_config.type=='custom'){tile_url=map_config['custom.url']||'';attribution=map_config.attribution||'';subdomains=map_config.subdomains||'';if(map_config['custom.tms']) +var tms=map_config['custom.tms'];} +return{model:dataset,mapTilesURL:tile_url,mapTilesAttribution:attribution,mapTilesSubdomains:subdomains};},_newDataExplorer:function(dataset,map_config){var views=[{id:'grid',label:this._('Grid'),view:new recline.View.SlickGrid({model:dataset})},{id:'graph',label:this._('Graph'),view:new recline.View.Graph({model:dataset})},{id:'map',label:this._('Map'),view:new recline.View.Map(this._reclineMapViewOptions(dataset,map_config))}];var sidebarViews=[{id:'valueFilter',label:this._('Filters'),view:new recline.View.ValueFilter({model:dataset})}];var dataExplorer=new recline.View.MultiView({el:this.el,model:dataset,views:views,sidebarViews:sidebarViews,config:{readOnly:true}});return dataExplorer;},_renderControls:function(el,controls,className){var controlsEl=jQuery("
");for(var i=0;i').attr(attrs); + this.setElement($el, false); + } else { + this.setElement(_.result(this, 'el'), false); + } + } + + }); + + // Backbone.sync + // ------------- + + // Override this function to change the manner in which Backbone persists + // models to the server. You will be passed the type of request, and the + // model in question. By default, makes a RESTful Ajax request + // to the model's `url()`. Some possible customizations could be: + // + // * Use `setTimeout` to batch rapid-fire updates into a single request. + // * Send up the models as XML instead of JSON. + // * Persist models via WebSockets instead of Ajax. + // + // Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests + // as `POST`, with a `_method` parameter containing the true HTTP method, + // as well as all requests with the body as `application/x-www-form-urlencoded` + // instead of `application/json` with the model in a param named `model`. + // Useful when interfacing with server-side languages like **PHP** that make + // it difficult to read the body of `PUT` requests. + Backbone.sync = function(method, model, options) { + var type = methodMap[method]; + + // Default options, unless specified. + _.defaults(options || (options = {}), { + emulateHTTP: Backbone.emulateHTTP, + emulateJSON: Backbone.emulateJSON + }); + + // Default JSON-request options. + var params = {type: type, dataType: 'json'}; + + // Ensure that we have a URL. + if (!options.url) { + params.url = _.result(model, 'url') || urlError(); + } + + // Ensure that we have the appropriate request data. + if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) { + params.contentType = 'application/json'; + params.data = JSON.stringify(options.attrs || model.toJSON(options)); + } + + // For older servers, emulate JSON by encoding the request into an HTML-form. + if (options.emulateJSON) { + params.contentType = 'application/x-www-form-urlencoded'; + params.data = params.data ? {model: params.data} : {}; + } + + // For older servers, emulate HTTP by mimicking the HTTP method with `_method` + // And an `X-HTTP-Method-Override` header. + if (options.emulateHTTP && (type === 'PUT' || type === 'DELETE' || type === 'PATCH')) { + params.type = 'POST'; + if (options.emulateJSON) params.data._method = type; + var beforeSend = options.beforeSend; + options.beforeSend = function(xhr) { + xhr.setRequestHeader('X-HTTP-Method-Override', type); + if (beforeSend) return beforeSend.apply(this, arguments); + }; + } + + // Don't process data on a non-GET request. + if (params.type !== 'GET' && !options.emulateJSON) { + params.processData = false; + } + + // If we're sending a `PATCH` request, and we're in an old Internet Explorer + // that still has ActiveX enabled by default, override jQuery to use that + // for XHR instead. Remove this line when jQuery supports `PATCH` on IE8. + if (params.type === 'PATCH' && window.ActiveXObject && + !(window.external && window.external.msActiveXFilteringEnabled)) { + params.xhr = function() { + return new ActiveXObject("Microsoft.XMLHTTP"); + }; + } + + // Make the request, allowing the user to override any Ajax options. + var xhr = options.xhr = Backbone.ajax(_.extend(params, options)); + model.trigger('request', model, xhr, options); + return xhr; + }; + + // Map from CRUD to HTTP for our default `Backbone.sync` implementation. + var methodMap = { + 'create': 'POST', + 'update': 'PUT', + 'patch': 'PATCH', + 'delete': 'DELETE', + 'read': 'GET' + }; + + // Set the default implementation of `Backbone.ajax` to proxy through to `$`. + // Override this if you'd like to use a different library. + Backbone.ajax = function() { + return Backbone.$.ajax.apply(Backbone.$, 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. + var Router = 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 optionalParam = /\((.*?)\)/g; + var namedParam = /(\(\?)?:\w+/g; + var splatParam = /\*\w+/g; + var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g; + + // Set up all inheritable **Backbone.Router** properties and methods. + _.extend(Router.prototype, 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) { + if (!_.isRegExp(route)) route = this._routeToRegExp(route); + if (_.isFunction(name)) { + callback = name; + name = ''; + } + if (!callback) callback = this[name]; + var router = this; + Backbone.history.route(route, function(fragment) { + var args = router._extractParameters(route, fragment); + callback && callback.apply(router, args); + router.trigger.apply(router, ['route:' + name].concat(args)); + router.trigger('route', name, args); + Backbone.history.trigger('route', router, name, args); + }); + return this; + }, + + // Simple proxy to `Backbone.history` to save a fragment into the history. + navigate: function(fragment, options) { + Backbone.history.navigate(fragment, options); + return this; + }, + + // 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; + this.routes = _.result(this, 'routes'); + var route, routes = _.keys(this.routes); + while ((route = routes.pop()) != null) { + this.route(route, this.routes[route]); + } + }, + + // Convert a route string into a regular expression, suitable for matching + // against the current location hash. + _routeToRegExp: function(route) { + route = route.replace(escapeRegExp, '\\$&') + .replace(optionalParam, '(?:$1)?') + .replace(namedParam, function(match, optional){ + return optional ? match : '([^\/]+)'; + }) + .replace(splatParam, '(.*?)'); + return new RegExp('^' + route + '$'); + }, + + // Given a route, and a URL fragment that it matches, return the array of + // extracted decoded parameters. Empty or unmatched parameters will be + // treated as `null` to normalize cross-browser behavior. + _extractParameters: function(route, fragment) { + var params = route.exec(fragment).slice(1); + return _.map(params, function(param) { + return param ? decodeURIComponent(param) : null; + }); + } + + }); + + // Backbone.History + // ---------------- + + // Handles cross-browser history management, based on either + // [pushState](http://diveintohtml5.info/history.html) and real URLs, or + // [onhashchange](https://developer.mozilla.org/en-US/docs/DOM/window.onhashchange) + // and URL fragments. If the browser supports neither (old IE, natch), + // falls back to polling. + var History = Backbone.History = function() { + this.handlers = []; + _.bindAll(this, 'checkUrl'); + + // Ensure that `History` can be used outside of the browser. + if (typeof window !== 'undefined') { + this.location = window.location; + this.history = window.history; + } + }; + + // Cached regex for stripping a leading hash/slash and trailing space. + var routeStripper = /^[#\/]|\s+$/g; + + // Cached regex for stripping leading and trailing slashes. + var rootStripper = /^\/+|\/+$/g; + + // Cached regex for detecting MSIE. + var isExplorer = /msie [\w.]+/; + + // Cached regex for removing a trailing slash. + var trailingSlash = /\/$/; + + // Has the history handling already been started? + History.started = false; + + // Set up all inheritable **Backbone.History** properties and methods. + _.extend(History.prototype, Events, { + + // The default interval to poll for hash changes, if necessary, is + // twenty times a second. + interval: 50, + + // Gets the true hash value. Cannot use location.hash directly due to bug + // in Firefox where location.hash will always be decoded. + getHash: function(window) { + var match = (window || this).location.href.match(/#(.*)$/); + return match ? match[1] : ''; + }, + + // 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 || !this._wantsHashChange || forcePushState) { + fragment = this.location.pathname; + var root = this.root.replace(trailingSlash, ''); + if (!fragment.indexOf(root)) fragment = fragment.substr(root.length); + } else { + fragment = this.getHash(); + } + } + return fragment.replace(routeStripper, ''); + }, + + // Start the hash change handling, returning `true` if the current URL matches + // an existing route, and `false` otherwise. + start: function(options) { + if (History.started) throw new Error("Backbone.history has already been started"); + History.started = true; + + // Figure out the initial configuration. Do we need an iframe? + // Is pushState desired ... is it available? + this.options = _.extend({}, {root: '/'}, this.options, options); + this.root = this.options.root; + this._wantsHashChange = this.options.hashChange !== false; + this._wantsPushState = !!this.options.pushState; + this._hasPushState = !!(this.options.pushState && this.history && this.history.pushState); + var fragment = this.getFragment(); + var docMode = document.documentMode; + var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7)); + + // Normalize root to always include a leading and trailing slash. + this.root = ('/' + this.root + '/').replace(rootStripper, '/'); + + if (oldIE && this._wantsHashChange) { + this.iframe = Backbone.$('': +"");a._keyEvent=false;return w},_generateMonthYearHeader:function(a,b,c,e,f,h,i,g){var j=this._get(a,"changeMonth"),l=this._get(a,"changeYear"),u=this._get(a,"showMonthAfterYear"),k='
',o="";if(h||!j)o+=''+i[b]+"";else{i=e&&e.getFullYear()==c;var m=f&&f.getFullYear()==c;o+='"}u||(k+=o+(h||!(j&&l)?" ":""));if(!a.yearshtml){a.yearshtml="";if(h||!l)k+=''+c+"";else{g=this._get(a,"yearRange").split(":");var s=(new Date).getFullYear();i=function(q){q=q.match(/c[+-].*/)?c+parseInt(q.substring(1),10):q.match(/[+-].*/)?s+parseInt(q,10):parseInt(q,10);return isNaN(q)?s:q};b=i(g[0]);g=Math.max(b,i(g[1]||""));b=e?Math.max(b, +e.getFullYear()):b;g=f?Math.min(g,f.getFullYear()):g;for(a.yearshtml+='";k+=a.yearshtml;a.yearshtml=null}}k+=this._get(a,"yearSuffix");if(u)k+=(h||!(j&&l)?" ":"")+o;k+="
";return k},_adjustInstDate:function(a,b,c){var e=a.drawYear+(c=="Y"?b:0),f=a.drawMonth+ +(c=="M"?b:0);b=Math.min(a.selectedDay,this._getDaysInMonth(e,f))+(c=="D"?b:0);e=this._restrictMinMax(a,this._daylightSavingAdjust(new Date(e,f,b)));a.selectedDay=e.getDate();a.drawMonth=a.selectedMonth=e.getMonth();a.drawYear=a.selectedYear=e.getFullYear();if(c=="M"||c=="Y")this._notifyChange(a)},_restrictMinMax:function(a,b){var c=this._getMinMaxDate(a,"min");a=this._getMinMaxDate(a,"max");b=c&&ba?a:b},_notifyChange:function(a){var b=this._get(a,"onChangeMonthYear");if(b)b.apply(a.input? +a.input[0]:null,[a.selectedYear,a.selectedMonth+1,a])},_getNumberOfMonths:function(a){a=this._get(a,"numberOfMonths");return a==null?[1,1]:typeof a=="number"?[1,a]:a},_getMinMaxDate:function(a,b){return this._determineDate(a,this._get(a,b+"Date"),null)},_getDaysInMonth:function(a,b){return 32-this._daylightSavingAdjust(new Date(a,b,32)).getDate()},_getFirstDayOfMonth:function(a,b){return(new Date(a,b,1)).getDay()},_canAdjustMonth:function(a,b,c,e){var f=this._getNumberOfMonths(a);c=this._daylightSavingAdjust(new Date(c, +e+(b<0?b:f[0]*f[1]),1));b<0&&c.setDate(this._getDaysInMonth(c.getFullYear(),c.getMonth()));return this._isInRange(a,c)},_isInRange:function(a,b){var c=this._getMinMaxDate(a,"min");a=this._getMinMaxDate(a,"max");return(!c||b.getTime()>=c.getTime())&&(!a||b.getTime()<=a.getTime())},_getFormatConfig:function(a){var b=this._get(a,"shortYearCutoff");b=typeof b!="string"?b:(new Date).getFullYear()%100+parseInt(b,10);return{shortYearCutoff:b,dayNamesShort:this._get(a,"dayNamesShort"),dayNames:this._get(a, +"dayNames"),monthNamesShort:this._get(a,"monthNamesShort"),monthNames:this._get(a,"monthNames")}},_formatDate:function(a,b,c,e){if(!b){a.currentDay=a.selectedDay;a.currentMonth=a.selectedMonth;a.currentYear=a.selectedYear}b=b?typeof b=="object"?b:this._daylightSavingAdjust(new Date(e,c,b)):this._daylightSavingAdjust(new Date(a.currentYear,a.currentMonth,a.currentDay));return this.formatDate(this._get(a,"dateFormat"),b,this._getFormatConfig(a))}});d.fn.datepicker=function(a){if(!this.length)return this; +if(!d.datepicker.initialized){d(document).mousedown(d.datepicker._checkExternalClick).find("body").append(d.datepicker.dpDiv);d.datepicker.initialized=true}var b=Array.prototype.slice.call(arguments,1);if(typeof a=="string"&&(a=="isDisabled"||a=="getDate"||a=="widget"))return d.datepicker["_"+a+"Datepicker"].apply(d.datepicker,[this[0]].concat(b));if(a=="option"&&arguments.length==2&&typeof arguments[1]=="string")return d.datepicker["_"+a+"Datepicker"].apply(d.datepicker,[this[0]].concat(b));return this.each(function(){typeof a== +"string"?d.datepicker["_"+a+"Datepicker"].apply(d.datepicker,[this].concat(b)):d.datepicker._attachDatepicker(this,a)})};d.datepicker=new M;d.datepicker.initialized=false;d.datepicker.uuid=(new Date).getTime();d.datepicker.version="1.8.16";window["DP_jQuery_"+B]=d})(jQuery); +;/* + * jQuery UI Effects 1.8.16 + * + * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Effects/ + */ +jQuery.effects||function(f,j){function m(c){var a;if(c&&c.constructor==Array&&c.length==3)return c;if(a=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(c))return[parseInt(a[1],10),parseInt(a[2],10),parseInt(a[3],10)];if(a=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(c))return[parseFloat(a[1])*2.55,parseFloat(a[2])*2.55,parseFloat(a[3])*2.55];if(a=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(c))return[parseInt(a[1], +16),parseInt(a[2],16),parseInt(a[3],16)];if(a=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(c))return[parseInt(a[1]+a[1],16),parseInt(a[2]+a[2],16),parseInt(a[3]+a[3],16)];if(/rgba\(0, 0, 0, 0\)/.exec(c))return n.transparent;return n[f.trim(c).toLowerCase()]}function s(c,a){var b;do{b=f.curCSS(c,a);if(b!=""&&b!="transparent"||f.nodeName(c,"body"))break;a="backgroundColor"}while(c=c.parentNode);return m(b)}function o(){var c=document.defaultView?document.defaultView.getComputedStyle(this,null):this.currentStyle, +a={},b,d;if(c&&c.length&&c[0]&&c[c[0]])for(var e=c.length;e--;){b=c[e];if(typeof c[b]=="string"){d=b.replace(/\-(\w)/g,function(g,h){return h.toUpperCase()});a[d]=c[b]}}else for(b in c)if(typeof c[b]==="string")a[b]=c[b];return a}function p(c){var a,b;for(a in c){b=c[a];if(b==null||f.isFunction(b)||a in t||/scrollbar/.test(a)||!/color/i.test(a)&&isNaN(parseFloat(b)))delete c[a]}return c}function u(c,a){var b={_:0},d;for(d in a)if(c[d]!=a[d])b[d]=a[d];return b}function k(c,a,b,d){if(typeof c=="object"){d= +a;b=null;a=c;c=a.effect}if(f.isFunction(a)){d=a;b=null;a={}}if(typeof a=="number"||f.fx.speeds[a]){d=b;b=a;a={}}if(f.isFunction(b)){d=b;b=null}a=a||{};b=b||a.duration;b=f.fx.off?0:typeof b=="number"?b:b in f.fx.speeds?f.fx.speeds[b]:f.fx.speeds._default;d=d||a.complete;return[c,a,b,d]}function l(c){if(!c||typeof c==="number"||f.fx.speeds[c])return true;if(typeof c==="string"&&!f.effects[c])return true;return false}f.effects={};f.each(["backgroundColor","borderBottomColor","borderLeftColor","borderRightColor", +"borderTopColor","borderColor","color","outlineColor"],function(c,a){f.fx.step[a]=function(b){if(!b.colorInit){b.start=s(b.elem,a);b.end=m(b.end);b.colorInit=true}b.elem.style[a]="rgb("+Math.max(Math.min(parseInt(b.pos*(b.end[0]-b.start[0])+b.start[0],10),255),0)+","+Math.max(Math.min(parseInt(b.pos*(b.end[1]-b.start[1])+b.start[1],10),255),0)+","+Math.max(Math.min(parseInt(b.pos*(b.end[2]-b.start[2])+b.start[2],10),255),0)+")"}});var n={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0, +0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211, +211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0],transparent:[255,255,255]},q=["add","remove","toggle"],t={border:1,borderBottom:1,borderColor:1,borderLeft:1,borderRight:1,borderTop:1,borderWidth:1,margin:1,padding:1};f.effects.animateClass=function(c,a,b, +d){if(f.isFunction(b)){d=b;b=null}return this.queue(function(){var e=f(this),g=e.attr("style")||" ",h=p(o.call(this)),r,v=e.attr("class");f.each(q,function(w,i){c[i]&&e[i+"Class"](c[i])});r=p(o.call(this));e.attr("class",v);e.animate(u(h,r),{queue:false,duration:a,easing:b,complete:function(){f.each(q,function(w,i){c[i]&&e[i+"Class"](c[i])});if(typeof e.attr("style")=="object"){e.attr("style").cssText="";e.attr("style").cssText=g}else e.attr("style",g);d&&d.apply(this,arguments);f.dequeue(this)}})})}; +f.fn.extend({_addClass:f.fn.addClass,addClass:function(c,a,b,d){return a?f.effects.animateClass.apply(this,[{add:c},a,b,d]):this._addClass(c)},_removeClass:f.fn.removeClass,removeClass:function(c,a,b,d){return a?f.effects.animateClass.apply(this,[{remove:c},a,b,d]):this._removeClass(c)},_toggleClass:f.fn.toggleClass,toggleClass:function(c,a,b,d,e){return typeof a=="boolean"||a===j?b?f.effects.animateClass.apply(this,[a?{add:c}:{remove:c},b,d,e]):this._toggleClass(c,a):f.effects.animateClass.apply(this, +[{toggle:c},a,b,d])},switchClass:function(c,a,b,d,e){return f.effects.animateClass.apply(this,[{add:a,remove:c},b,d,e])}});f.extend(f.effects,{version:"1.8.16",save:function(c,a){for(var b=0;b
").addClass("ui-effects-wrapper").css({fontSize:"100%",background:"transparent",border:"none",margin:0,padding:0}), +d=document.activeElement;c.wrap(b);if(c[0]===d||f.contains(c[0],d))f(d).focus();b=c.parent();if(c.css("position")=="static"){b.css({position:"relative"});c.css({position:"relative"})}else{f.extend(a,{position:c.css("position"),zIndex:c.css("z-index")});f.each(["top","left","bottom","right"],function(e,g){a[g]=c.css(g);if(isNaN(parseInt(a[g],10)))a[g]="auto"});c.css({position:"relative",top:0,left:0,right:"auto",bottom:"auto"})}return b.css(a).show()},removeWrapper:function(c){var a,b=document.activeElement; +if(c.parent().is(".ui-effects-wrapper")){a=c.parent().replaceWith(c);if(c[0]===b||f.contains(c[0],b))f(b).focus();return a}return c},setTransition:function(c,a,b,d){d=d||{};f.each(a,function(e,g){unit=c.cssUnit(g);if(unit[0]>0)d[g]=unit[0]*b+unit[1]});return d}});f.fn.extend({effect:function(c){var a=k.apply(this,arguments),b={options:a[1],duration:a[2],callback:a[3]};a=b.options.mode;var d=f.effects[c];if(f.fx.off||!d)return a?this[a](b.duration,b.callback):this.each(function(){b.callback&&b.callback.call(this)}); +return d.call(this,b)},_show:f.fn.show,show:function(c){if(l(c))return this._show.apply(this,arguments);else{var a=k.apply(this,arguments);a[1].mode="show";return this.effect.apply(this,a)}},_hide:f.fn.hide,hide:function(c){if(l(c))return this._hide.apply(this,arguments);else{var a=k.apply(this,arguments);a[1].mode="hide";return this.effect.apply(this,a)}},__toggle:f.fn.toggle,toggle:function(c){if(l(c)||typeof c==="boolean"||f.isFunction(c))return this.__toggle.apply(this,arguments);else{var a=k.apply(this, +arguments);a[1].mode="toggle";return this.effect.apply(this,a)}},cssUnit:function(c){var a=this.css(c),b=[];f.each(["em","px","%","pt"],function(d,e){if(a.indexOf(e)>0)b=[parseFloat(a),e]});return b}});f.easing.jswing=f.easing.swing;f.extend(f.easing,{def:"easeOutQuad",swing:function(c,a,b,d,e){return f.easing[f.easing.def](c,a,b,d,e)},easeInQuad:function(c,a,b,d,e){return d*(a/=e)*a+b},easeOutQuad:function(c,a,b,d,e){return-d*(a/=e)*(a-2)+b},easeInOutQuad:function(c,a,b,d,e){if((a/=e/2)<1)return d/ +2*a*a+b;return-d/2*(--a*(a-2)-1)+b},easeInCubic:function(c,a,b,d,e){return d*(a/=e)*a*a+b},easeOutCubic:function(c,a,b,d,e){return d*((a=a/e-1)*a*a+1)+b},easeInOutCubic:function(c,a,b,d,e){if((a/=e/2)<1)return d/2*a*a*a+b;return d/2*((a-=2)*a*a+2)+b},easeInQuart:function(c,a,b,d,e){return d*(a/=e)*a*a*a+b},easeOutQuart:function(c,a,b,d,e){return-d*((a=a/e-1)*a*a*a-1)+b},easeInOutQuart:function(c,a,b,d,e){if((a/=e/2)<1)return d/2*a*a*a*a+b;return-d/2*((a-=2)*a*a*a-2)+b},easeInQuint:function(c,a,b, +d,e){return d*(a/=e)*a*a*a*a+b},easeOutQuint:function(c,a,b,d,e){return d*((a=a/e-1)*a*a*a*a+1)+b},easeInOutQuint:function(c,a,b,d,e){if((a/=e/2)<1)return d/2*a*a*a*a*a+b;return d/2*((a-=2)*a*a*a*a+2)+b},easeInSine:function(c,a,b,d,e){return-d*Math.cos(a/e*(Math.PI/2))+d+b},easeOutSine:function(c,a,b,d,e){return d*Math.sin(a/e*(Math.PI/2))+b},easeInOutSine:function(c,a,b,d,e){return-d/2*(Math.cos(Math.PI*a/e)-1)+b},easeInExpo:function(c,a,b,d,e){return a==0?b:d*Math.pow(2,10*(a/e-1))+b},easeOutExpo:function(c, +a,b,d,e){return a==e?b+d:d*(-Math.pow(2,-10*a/e)+1)+b},easeInOutExpo:function(c,a,b,d,e){if(a==0)return b;if(a==e)return b+d;if((a/=e/2)<1)return d/2*Math.pow(2,10*(a-1))+b;return d/2*(-Math.pow(2,-10*--a)+2)+b},easeInCirc:function(c,a,b,d,e){return-d*(Math.sqrt(1-(a/=e)*a)-1)+b},easeOutCirc:function(c,a,b,d,e){return d*Math.sqrt(1-(a=a/e-1)*a)+b},easeInOutCirc:function(c,a,b,d,e){if((a/=e/2)<1)return-d/2*(Math.sqrt(1-a*a)-1)+b;return d/2*(Math.sqrt(1-(a-=2)*a)+1)+b},easeInElastic:function(c,a,b, +d,e){c=1.70158;var g=0,h=d;if(a==0)return b;if((a/=e)==1)return b+d;g||(g=e*0.3);if(h
").css({position:"absolute",visibility:"visible",left:-f*(h/d),top:-e*(i/c)}).parent().addClass("ui-effects-explode").css({position:"absolute",overflow:"hidden",width:h/d,height:i/c,left:g.left+f*(h/d)+(a.options.mode=="show"?(f-Math.floor(d/2))*(h/d):0),top:g.top+e*(i/c)+(a.options.mode=="show"?(e-Math.floor(c/2))*(i/c):0),opacity:a.options.mode=="show"?0:1}).animate({left:g.left+f*(h/d)+(a.options.mode=="show"?0:(f-Math.floor(d/2))*(h/d)),top:g.top+ +e*(i/c)+(a.options.mode=="show"?0:(e-Math.floor(c/2))*(i/c)),opacity:a.options.mode=="show"?1:0},a.duration||500);setTimeout(function(){a.options.mode=="show"?b.css({visibility:"visible"}):b.css({visibility:"visible"}).hide();a.callback&&a.callback.apply(b[0]);b.dequeue();j("div.ui-effects-explode").remove()},a.duration||500)})}})(jQuery); +;/* + * jQuery UI Effects Fade 1.8.16 + * + * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Effects/Fade + * + * Depends: + * jquery.effects.core.js + */ +(function(b){b.effects.fade=function(a){return this.queue(function(){var c=b(this),d=b.effects.setMode(c,a.options.mode||"hide");c.animate({opacity:d},{queue:false,duration:a.duration,easing:a.options.easing,complete:function(){a.callback&&a.callback.apply(this,arguments);c.dequeue()}})})}})(jQuery); +;/* + * jQuery UI Effects Fold 1.8.16 + * + * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Effects/Fold + * + * Depends: + * jquery.effects.core.js + */ +(function(c){c.effects.fold=function(a){return this.queue(function(){var b=c(this),j=["position","top","bottom","left","right"],d=c.effects.setMode(b,a.options.mode||"hide"),g=a.options.size||15,h=!!a.options.horizFirst,k=a.duration?a.duration/2:c.fx.speeds._default/2;c.effects.save(b,j);b.show();var e=c.effects.createWrapper(b).css({overflow:"hidden"}),f=d=="show"!=h,l=f?["width","height"]:["height","width"];f=f?[e.width(),e.height()]:[e.height(),e.width()];var i=/([0-9]+)%/.exec(g);if(i)g=parseInt(i[1], +10)/100*f[d=="hide"?0:1];if(d=="show")e.css(h?{height:0,width:g}:{height:g,width:0});h={};i={};h[l[0]]=d=="show"?f[0]:g;i[l[1]]=d=="show"?f[1]:0;e.animate(h,k,a.options.easing).animate(i,k,a.options.easing,function(){d=="hide"&&b.hide();c.effects.restore(b,j);c.effects.removeWrapper(b);a.callback&&a.callback.apply(b[0],arguments);b.dequeue()})})}})(jQuery); +;/* + * jQuery UI Effects Highlight 1.8.16 + * + * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Effects/Highlight + * + * Depends: + * jquery.effects.core.js + */ +(function(b){b.effects.highlight=function(c){return this.queue(function(){var a=b(this),e=["backgroundImage","backgroundColor","opacity"],d=b.effects.setMode(a,c.options.mode||"show"),f={backgroundColor:a.css("backgroundColor")};if(d=="hide")f.opacity=0;b.effects.save(a,e);a.show().css({backgroundImage:"none",backgroundColor:c.options.color||"#ffff99"}).animate(f,{queue:false,duration:c.duration,easing:c.options.easing,complete:function(){d=="hide"&&a.hide();b.effects.restore(a,e);d=="show"&&!b.support.opacity&& +this.style.removeAttribute("filter");c.callback&&c.callback.apply(this,arguments);a.dequeue()}})})}})(jQuery); +;/* + * jQuery UI Effects Pulsate 1.8.16 + * + * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Effects/Pulsate + * + * Depends: + * jquery.effects.core.js + */ +(function(d){d.effects.pulsate=function(a){return this.queue(function(){var b=d(this),c=d.effects.setMode(b,a.options.mode||"show");times=(a.options.times||5)*2-1;duration=a.duration?a.duration/2:d.fx.speeds._default/2;isVisible=b.is(":visible");animateTo=0;if(!isVisible){b.css("opacity",0).show();animateTo=1}if(c=="hide"&&isVisible||c=="show"&&!isVisible)times--;for(c=0;c
').appendTo(document.body).addClass(a.options.className).css({top:d.top,left:d.left,height:b.innerHeight(),width:b.innerWidth(),position:"absolute"}).animate(c,a.duration,a.options.easing,function(){f.remove();a.callback&&a.callback.apply(b[0],arguments); +b.dequeue()})})}})(jQuery); +; \ No newline at end of file diff --git a/ckanext/dataexplorer/public/vendor/slickgrid/2.2/jquery.event.drag-2.2.js b/ckanext/dataexplorer/public/vendor/slickgrid/2.2/jquery.event.drag-2.2.js new file mode 100644 index 0000000..f2c1d57 --- /dev/null +++ b/ckanext/dataexplorer/public/vendor/slickgrid/2.2/jquery.event.drag-2.2.js @@ -0,0 +1,402 @@ +/*! + * jquery.event.drag - v 2.2 + * Copyright (c) 2010 Three Dub Media - http://threedubmedia.com + * Open Source MIT License - http://threedubmedia.com/code/license + */ +// Created: 2008-06-04 +// Updated: 2012-05-21 +// REQUIRES: jquery 1.7.x + +;(function( $ ){ + +// add the jquery instance method +$.fn.drag = function( str, arg, opts ){ + // figure out the event type + var type = typeof str == "string" ? str : "", + // figure out the event handler... + fn = $.isFunction( str ) ? str : $.isFunction( arg ) ? arg : null; + // fix the event type + if ( type.indexOf("drag") !== 0 ) + type = "drag"+ type; + // were options passed + opts = ( str == fn ? arg : opts ) || {}; + // trigger or bind event handler + return fn ? this.bind( type, opts, fn ) : this.trigger( type ); +}; + +// local refs (increase compression) +var $event = $.event, +$special = $event.special, +// configure the drag special event +drag = $special.drag = { + + // these are the default settings + defaults: { + which: 1, // mouse button pressed to start drag sequence + distance: 0, // distance dragged before dragstart + not: ':input', // selector to suppress dragging on target elements + handle: null, // selector to match handle target elements + relative: false, // true to use "position", false to use "offset" + drop: true, // false to suppress drop events, true or selector to allow + click: false // false to suppress click events after dragend (no proxy) + }, + + // the key name for stored drag data + datakey: "dragdata", + + // prevent bubbling for better performance + noBubble: true, + + // count bound related events + add: function( obj ){ + // read the interaction data + var data = $.data( this, drag.datakey ), + // read any passed options + opts = obj.data || {}; + // count another realted event + data.related += 1; + // extend data options bound with this event + // don't iterate "opts" in case it is a node + $.each( drag.defaults, function( key, def ){ + if ( opts[ key ] !== undefined ) + data[ key ] = opts[ key ]; + }); + }, + + // forget unbound related events + remove: function(){ + $.data( this, drag.datakey ).related -= 1; + }, + + // configure interaction, capture settings + setup: function(){ + // check for related events + if ( $.data( this, drag.datakey ) ) + return; + // initialize the drag data with copied defaults + var data = $.extend({ related:0 }, drag.defaults ); + // store the interaction data + $.data( this, drag.datakey, data ); + // bind the mousedown event, which starts drag interactions + $event.add( this, "touchstart mousedown", drag.init, data ); + // prevent image dragging in IE... + if ( this.attachEvent ) + this.attachEvent("ondragstart", drag.dontstart ); + }, + + // destroy configured interaction + teardown: function(){ + var data = $.data( this, drag.datakey ) || {}; + // check for related events + if ( data.related ) + return; + // remove the stored data + $.removeData( this, drag.datakey ); + // remove the mousedown event + $event.remove( this, "touchstart mousedown", drag.init ); + // enable text selection + drag.textselect( true ); + // un-prevent image dragging in IE... + if ( this.detachEvent ) + this.detachEvent("ondragstart", drag.dontstart ); + }, + + // initialize the interaction + init: function( event ){ + // sorry, only one touch at a time + if ( drag.touched ) + return; + // the drag/drop interaction data + var dd = event.data, results; + // check the which directive + if ( event.which != 0 && dd.which > 0 && event.which != dd.which ) + return; + // check for suppressed selector + if ( $( event.target ).is( dd.not ) ) + return; + // check for handle selector + if ( dd.handle && !$( event.target ).closest( dd.handle, event.currentTarget ).length ) + return; + + drag.touched = event.type == 'touchstart' ? this : null; + dd.propagates = 1; + dd.mousedown = this; + dd.interactions = [ drag.interaction( this, dd ) ]; + dd.target = event.target; + dd.pageX = event.pageX; + dd.pageY = event.pageY; + dd.dragging = null; + // handle draginit event... + results = drag.hijack( event, "draginit", dd ); + // early cancel + if ( !dd.propagates ) + return; + // flatten the result set + results = drag.flatten( results ); + // insert new interaction elements + if ( results && results.length ){ + dd.interactions = []; + $.each( results, function(){ + dd.interactions.push( drag.interaction( this, dd ) ); + }); + } + // remember how many interactions are propagating + dd.propagates = dd.interactions.length; + // locate and init the drop targets + if ( dd.drop !== false && $special.drop ) + $special.drop.handler( event, dd ); + // disable text selection + drag.textselect( false ); + // bind additional events... + if ( drag.touched ) + $event.add( drag.touched, "touchmove touchend", drag.handler, dd ); + else + $event.add( document, "mousemove mouseup", drag.handler, dd ); + // helps prevent text selection or scrolling + if ( !drag.touched || dd.live ) + return false; + }, + + // returns an interaction object + interaction: function( elem, dd ){ + var offset = $( elem )[ dd.relative ? "position" : "offset" ]() || { top:0, left:0 }; + return { + drag: elem, + callback: new drag.callback(), + droppable: [], + offset: offset + }; + }, + + // handle drag-releatd DOM events + handler: function( event ){ + // read the data before hijacking anything + var dd = event.data; + // handle various events + switch ( event.type ){ + // mousemove, check distance, start dragging + case !dd.dragging && 'touchmove': + event.preventDefault(); + case !dd.dragging && 'mousemove': + // drag tolerance, x≤ + y≤ = distance≤ + if ( Math.pow( event.pageX-dd.pageX, 2 ) + Math.pow( event.pageY-dd.pageY, 2 ) < Math.pow( dd.distance, 2 ) ) + break; // distance tolerance not reached + event.target = dd.target; // force target from "mousedown" event (fix distance issue) + drag.hijack( event, "dragstart", dd ); // trigger "dragstart" + if ( dd.propagates ) // "dragstart" not rejected + dd.dragging = true; // activate interaction + // mousemove, dragging + case 'touchmove': + event.preventDefault(); + case 'mousemove': + if ( dd.dragging ){ + // trigger "drag" + drag.hijack( event, "drag", dd ); + if ( dd.propagates ){ + // manage drop events + if ( dd.drop !== false && $special.drop ) + $special.drop.handler( event, dd ); // "dropstart", "dropend" + break; // "drag" not rejected, stop + } + event.type = "mouseup"; // helps "drop" handler behave + } + // mouseup, stop dragging + case 'touchend': + case 'mouseup': + default: + if ( drag.touched ) + $event.remove( drag.touched, "touchmove touchend", drag.handler ); // remove touch events + else + $event.remove( document, "mousemove mouseup", drag.handler ); // remove page events + if ( dd.dragging ){ + if ( dd.drop !== false && $special.drop ) + $special.drop.handler( event, dd ); // "drop" + drag.hijack( event, "dragend", dd ); // trigger "dragend" + } + drag.textselect( true ); // enable text selection + // if suppressing click events... + if ( dd.click === false && dd.dragging ) + $.data( dd.mousedown, "suppress.click", new Date().getTime() + 5 ); + dd.dragging = drag.touched = false; // deactivate element + break; + } + }, + + // re-use event object for custom events + hijack: function( event, type, dd, x, elem ){ + // not configured + if ( !dd ) + return; + // remember the original event and type + var orig = { event:event.originalEvent, type:event.type }, + // is the event drag related or drog related? + mode = type.indexOf("drop") ? "drag" : "drop", + // iteration vars + result, i = x || 0, ia, $elems, callback, + len = !isNaN( x ) ? x : dd.interactions.length; + // modify the event type + event.type = type; + // remove the original event + event.originalEvent = null; + // initialize the results + dd.results = []; + // handle each interacted element + do if ( ia = dd.interactions[ i ] ){ + // validate the interaction + if ( type !== "dragend" && ia.cancelled ) + continue; + // set the dragdrop properties on the event object + callback = drag.properties( event, dd, ia ); + // prepare for more results + ia.results = []; + // handle each element + $( elem || ia[ mode ] || dd.droppable ).each(function( p, subject ){ + // identify drag or drop targets individually + callback.target = subject; + // force propagtion of the custom event + event.isPropagationStopped = function(){ return false; }; + // handle the event + result = subject ? $event.dispatch.call( subject, event, callback ) : null; + // stop the drag interaction for this element + if ( result === false ){ + if ( mode == "drag" ){ + ia.cancelled = true; + dd.propagates -= 1; + } + if ( type == "drop" ){ + ia[ mode ][p] = null; + } + } + // assign any dropinit elements + else if ( type == "dropinit" ) + ia.droppable.push( drag.element( result ) || subject ); + // accept a returned proxy element + if ( type == "dragstart" ) + ia.proxy = $( drag.element( result ) || ia.drag )[0]; + // remember this result + ia.results.push( result ); + // forget the event result, for recycling + delete event.result; + // break on cancelled handler + if ( type !== "dropinit" ) + return result; + }); + // flatten the results + dd.results[ i ] = drag.flatten( ia.results ); + // accept a set of valid drop targets + if ( type == "dropinit" ) + ia.droppable = drag.flatten( ia.droppable ); + // locate drop targets + if ( type == "dragstart" && !ia.cancelled ) + callback.update(); + } + while ( ++i < len ) + // restore the original event & type + event.type = orig.type; + event.originalEvent = orig.event; + // return all handler results + return drag.flatten( dd.results ); + }, + + // extend the callback object with drag/drop properties... + properties: function( event, dd, ia ){ + var obj = ia.callback; + // elements + obj.drag = ia.drag; + obj.proxy = ia.proxy || ia.drag; + // starting mouse position + obj.startX = dd.pageX; + obj.startY = dd.pageY; + // current distance dragged + obj.deltaX = event.pageX - dd.pageX; + obj.deltaY = event.pageY - dd.pageY; + // original element position + obj.originalX = ia.offset.left; + obj.originalY = ia.offset.top; + // adjusted element position + obj.offsetX = obj.originalX + obj.deltaX; + obj.offsetY = obj.originalY + obj.deltaY; + // assign the drop targets information + obj.drop = drag.flatten( ( ia.drop || [] ).slice() ); + obj.available = drag.flatten( ( ia.droppable || [] ).slice() ); + return obj; + }, + + // determine is the argument is an element or jquery instance + element: function( arg ){ + if ( arg && ( arg.jquery || arg.nodeType == 1 ) ) + return arg; + }, + + // flatten nested jquery objects and arrays into a single dimension array + flatten: function( arr ){ + return $.map( arr, function( member ){ + return member && member.jquery ? $.makeArray( member ) : + member && member.length ? drag.flatten( member ) : member; + }); + }, + + // toggles text selection attributes ON (true) or OFF (false) + textselect: function( bool ){ + $( document )[ bool ? "unbind" : "bind" ]("selectstart", drag.dontstart ) + .css("MozUserSelect", bool ? "" : "none" ); + // .attr("unselectable", bool ? "off" : "on" ) + document.unselectable = bool ? "off" : "on"; + }, + + // suppress "selectstart" and "ondragstart" events + dontstart: function(){ + return false; + }, + + // a callback instance contructor + callback: function(){} + +}; + +// callback methods +drag.callback.prototype = { + update: function(){ + if ( $special.drop && this.available.length ) + $.each( this.available, function( i ){ + $special.drop.locate( this, i ); + }); + } +}; + +// patch $.event.$dispatch to allow suppressing clicks +var $dispatch = $event.dispatch; +$event.dispatch = function( event ){ + if ( $.data( this, "suppress."+ event.type ) - new Date().getTime() > 0 ){ + $.removeData( this, "suppress."+ event.type ); + return; + } + return $dispatch.apply( this, arguments ); +}; + +// event fix hooks for touch events... +var touchHooks = +$event.fixHooks.touchstart = +$event.fixHooks.touchmove = +$event.fixHooks.touchend = +$event.fixHooks.touchcancel = { + props: "clientX clientY pageX pageY screenX screenY".split( " " ), + filter: function( event, orig ) { + if ( orig ){ + var touched = ( orig.touches && orig.touches[0] ) + || ( orig.changedTouches && orig.changedTouches[0] ) + || null; + // iOS webkit: touchstart, touchmove, touchend + if ( touched ) + $.each( touchHooks.props, function( i, prop ){ + event[ prop ] = touched[ prop ]; + }); + } + return event; + } +}; + +// share the same special event configuration with related events... +$special.draginit = $special.dragstart = $special.dragend = drag; + +})( jQuery ); \ No newline at end of file diff --git a/ckanext/dataexplorer/public/vendor/slickgrid/2.2/jquery.event.drop-2.2.js b/ckanext/dataexplorer/public/vendor/slickgrid/2.2/jquery.event.drop-2.2.js new file mode 100644 index 0000000..7599ef9 --- /dev/null +++ b/ckanext/dataexplorer/public/vendor/slickgrid/2.2/jquery.event.drop-2.2.js @@ -0,0 +1,302 @@ +/*! + * jquery.event.drop - v 2.2 + * Copyright (c) 2010 Three Dub Media - http://threedubmedia.com + * Open Source MIT License - http://threedubmedia.com/code/license + */ +// Created: 2008-06-04 +// Updated: 2012-05-21 +// REQUIRES: jquery 1.7.x, event.drag 2.2 + +;(function($){ // secure $ jQuery alias + +// Events: drop, dropstart, dropend + +// add the jquery instance method +$.fn.drop = function( str, arg, opts ){ + // figure out the event type + var type = typeof str == "string" ? str : "", + // figure out the event handler... + fn = $.isFunction( str ) ? str : $.isFunction( arg ) ? arg : null; + // fix the event type + if ( type.indexOf("drop") !== 0 ) + type = "drop"+ type; + // were options passed + opts = ( str == fn ? arg : opts ) || {}; + // trigger or bind event handler + return fn ? this.bind( type, opts, fn ) : this.trigger( type ); +}; + +// DROP MANAGEMENT UTILITY +// returns filtered drop target elements, caches their positions +$.drop = function( opts ){ + opts = opts || {}; + // safely set new options... + drop.multi = opts.multi === true ? Infinity : + opts.multi === false ? 1 : !isNaN( opts.multi ) ? opts.multi : drop.multi; + drop.delay = opts.delay || drop.delay; + drop.tolerance = $.isFunction( opts.tolerance ) ? opts.tolerance : + opts.tolerance === null ? null : drop.tolerance; + drop.mode = opts.mode || drop.mode || 'intersect'; +}; + +// local refs (increase compression) +var $event = $.event, +$special = $event.special, +// configure the drop special event +drop = $.event.special.drop = { + + // these are the default settings + multi: 1, // allow multiple drop winners per dragged element + delay: 20, // async timeout delay + mode: 'overlap', // drop tolerance mode + + // internal cache + targets: [], + + // the key name for stored drop data + datakey: "dropdata", + + // prevent bubbling for better performance + noBubble: true, + + // count bound related events + add: function( obj ){ + // read the interaction data + var data = $.data( this, drop.datakey ); + // count another realted event + data.related += 1; + }, + + // forget unbound related events + remove: function(){ + $.data( this, drop.datakey ).related -= 1; + }, + + // configure the interactions + setup: function(){ + // check for related events + if ( $.data( this, drop.datakey ) ) + return; + // initialize the drop element data + var data = { + related: 0, + active: [], + anyactive: 0, + winner: 0, + location: {} + }; + // store the drop data on the element + $.data( this, drop.datakey, data ); + // store the drop target in internal cache + drop.targets.push( this ); + }, + + // destroy the configure interaction + teardown: function(){ + var data = $.data( this, drop.datakey ) || {}; + // check for related events + if ( data.related ) + return; + // remove the stored data + $.removeData( this, drop.datakey ); + // reference the targeted element + var element = this; + // remove from the internal cache + drop.targets = $.grep( drop.targets, function( target ){ + return ( target !== element ); + }); + }, + + // shared event handler + handler: function( event, dd ){ + // local vars + var results, $targets; + // make sure the right data is available + if ( !dd ) + return; + // handle various events + switch ( event.type ){ + // draginit, from $.event.special.drag + case 'mousedown': // DROPINIT >> + case 'touchstart': // DROPINIT >> + // collect and assign the drop targets + $targets = $( drop.targets ); + if ( typeof dd.drop == "string" ) + $targets = $targets.filter( dd.drop ); + // reset drop data winner properties + $targets.each(function(){ + var data = $.data( this, drop.datakey ); + data.active = []; + data.anyactive = 0; + data.winner = 0; + }); + // set available target elements + dd.droppable = $targets; + // activate drop targets for the initial element being dragged + $special.drag.hijack( event, "dropinit", dd ); + break; + // drag, from $.event.special.drag + case 'mousemove': // TOLERATE >> + case 'touchmove': // TOLERATE >> + drop.event = event; // store the mousemove event + if ( !drop.timer ) + // monitor drop targets + drop.tolerate( dd ); + break; + // dragend, from $.event.special.drag + case 'mouseup': // DROP >> DROPEND >> + case 'touchend': // DROP >> DROPEND >> + drop.timer = clearTimeout( drop.timer ); // delete timer + if ( dd.propagates ){ + $special.drag.hijack( event, "drop", dd ); + $special.drag.hijack( event, "dropend", dd ); + } + break; + + } + }, + + // returns the location positions of an element + locate: function( elem, index ){ + var data = $.data( elem, drop.datakey ), + $elem = $( elem ), + posi = $elem.offset() || {}, + height = $elem.outerHeight(), + width = $elem.outerWidth(), + location = { + elem: elem, + width: width, + height: height, + top: posi.top, + left: posi.left, + right: posi.left + width, + bottom: posi.top + height + }; + // drag elements might not have dropdata + if ( data ){ + data.location = location; + data.index = index; + data.elem = elem; + } + return location; + }, + + // test the location positions of an element against another OR an X,Y coord + contains: function( target, test ){ // target { location } contains test [x,y] or { location } + return ( ( test[0] || test.left ) >= target.left && ( test[0] || test.right ) <= target.right + && ( test[1] || test.top ) >= target.top && ( test[1] || test.bottom ) <= target.bottom ); + }, + + // stored tolerance modes + modes: { // fn scope: "$.event.special.drop" object + // target with mouse wins, else target with most overlap wins + 'intersect': function( event, proxy, target ){ + return this.contains( target, [ event.pageX, event.pageY ] ) ? // check cursor + 1e9 : this.modes.overlap.apply( this, arguments ); // check overlap + }, + // target with most overlap wins + 'overlap': function( event, proxy, target ){ + // calculate the area of overlap... + return Math.max( 0, Math.min( target.bottom, proxy.bottom ) - Math.max( target.top, proxy.top ) ) + * Math.max( 0, Math.min( target.right, proxy.right ) - Math.max( target.left, proxy.left ) ); + }, + // proxy is completely contained within target bounds + 'fit': function( event, proxy, target ){ + return this.contains( target, proxy ) ? 1 : 0; + }, + // center of the proxy is contained within target bounds + 'middle': function( event, proxy, target ){ + return this.contains( target, [ proxy.left + proxy.width * .5, proxy.top + proxy.height * .5 ] ) ? 1 : 0; + } + }, + + // sort drop target cache by by winner (dsc), then index (asc) + sort: function( a, b ){ + return ( b.winner - a.winner ) || ( a.index - b.index ); + }, + + // async, recursive tolerance execution + tolerate: function( dd ){ + // declare local refs + var i, drp, drg, data, arr, len, elem, + // interaction iteration variables + x = 0, ia, end = dd.interactions.length, + // determine the mouse coords + xy = [ drop.event.pageX, drop.event.pageY ], + // custom or stored tolerance fn + tolerance = drop.tolerance || drop.modes[ drop.mode ]; + // go through each passed interaction... + do if ( ia = dd.interactions[x] ){ + // check valid interaction + if ( !ia ) + return; + // initialize or clear the drop data + ia.drop = []; + // holds the drop elements + arr = []; + len = ia.droppable.length; + // determine the proxy location, if needed + if ( tolerance ) + drg = drop.locate( ia.proxy ); + // reset the loop + i = 0; + // loop each stored drop target + do if ( elem = ia.droppable[i] ){ + data = $.data( elem, drop.datakey ); + drp = data.location; + if ( !drp ) continue; + // find a winner: tolerance function is defined, call it + data.winner = tolerance ? tolerance.call( drop, drop.event, drg, drp ) + // mouse position is always the fallback + : drop.contains( drp, xy ) ? 1 : 0; + arr.push( data ); + } while ( ++i < len ); // loop + // sort the drop targets + arr.sort( drop.sort ); + // reset the loop + i = 0; + // loop through all of the targets again + do if ( data = arr[ i ] ){ + // winners... + if ( data.winner && ia.drop.length < drop.multi ){ + // new winner... dropstart + if ( !data.active[x] && !data.anyactive ){ + // check to make sure that this is not prevented + if ( $special.drag.hijack( drop.event, "dropstart", dd, x, data.elem )[0] !== false ){ + data.active[x] = 1; + data.anyactive += 1; + } + // if false, it is not a winner + else + data.winner = 0; + } + // if it is still a winner + if ( data.winner ) + ia.drop.push( data.elem ); + } + // losers... + else if ( data.active[x] && data.anyactive == 1 ){ + // former winner... dropend + $special.drag.hijack( drop.event, "dropend", dd, x, data.elem ); + data.active[x] = 0; + data.anyactive -= 1; + } + } while ( ++i < len ); // loop + } while ( ++x < end ) // loop + // check if the mouse is still moving or is idle + if ( drop.last && xy[0] == drop.last.pageX && xy[1] == drop.last.pageY ) + delete drop.timer; // idle, don't recurse + else // recurse + drop.timer = setTimeout(function(){ + drop.tolerate( dd ); + }, drop.delay ); + // remember event, to compare idleness + drop.last = drop.event; + } + +}; + +// share the same special event configuration with related events... +$special.dropinit = $special.dropstart = $special.dropend = drop; + +})(jQuery); // confine scope \ No newline at end of file diff --git a/ckanext/dataexplorer/public/vendor/slickgrid/2.2/plugins/slick.autotooltips.js b/ckanext/dataexplorer/public/vendor/slickgrid/2.2/plugins/slick.autotooltips.js new file mode 100644 index 0000000..955684f --- /dev/null +++ b/ckanext/dataexplorer/public/vendor/slickgrid/2.2/plugins/slick.autotooltips.js @@ -0,0 +1,83 @@ +(function ($) { + // Register namespace + $.extend(true, window, { + "Slick": { + "AutoTooltips": AutoTooltips + } + }); + + /** + * AutoTooltips plugin to show/hide tooltips when columns are too narrow to fit content. + * @constructor + * @param {boolean} [options.enableForCells=true] - Enable tooltip for grid cells + * @param {boolean} [options.enableForHeaderCells=false] - Enable tooltip for header cells + * @param {number} [options.maxToolTipLength=null] - The maximum length for a tooltip + */ + function AutoTooltips(options) { + var _grid; + var _self = this; + var _defaults = { + enableForCells: true, + enableForHeaderCells: false, + maxToolTipLength: null + }; + + /** + * Initialize plugin. + */ + function init(grid) { + options = $.extend(true, {}, _defaults, options); + _grid = grid; + if (options.enableForCells) _grid.onMouseEnter.subscribe(handleMouseEnter); + if (options.enableForHeaderCells) _grid.onHeaderMouseEnter.subscribe(handleHeaderMouseEnter); + } + + /** + * Destroy plugin. + */ + function destroy() { + if (options.enableForCells) _grid.onMouseEnter.unsubscribe(handleMouseEnter); + if (options.enableForHeaderCells) _grid.onHeaderMouseEnter.unsubscribe(handleHeaderMouseEnter); + } + + /** + * Handle mouse entering grid cell to add/remove tooltip. + * @param {jQuery.Event} e - The event + */ + function handleMouseEnter(e) { + var cell = _grid.getCellFromEvent(e); + if (cell) { + var $node = $(_grid.getCellNode(cell.row, cell.cell)); + var text; + if ($node.innerWidth() < $node[0].scrollWidth) { + text = $.trim($node.text()); + if (options.maxToolTipLength && text.length > options.maxToolTipLength) { + text = text.substr(0, options.maxToolTipLength - 3) + "..."; + } + } else { + text = ""; + } + $node.attr("title", text); + } + } + + /** + * Handle mouse entering header cell to add/remove tooltip. + * @param {jQuery.Event} e - The event + * @param {object} args.column - The column definition + */ + function handleHeaderMouseEnter(e, args) { + var column = args.column, + $node = $(e.target).closest(".slick-header-column"); + if (!column.toolTip) { + $node.attr("title", ($node.innerWidth() < $node[0].scrollWidth) ? column.name : ""); + } + } + + // Public API + $.extend(this, { + "init": init, + "destroy": destroy + }); + } +})(jQuery); \ No newline at end of file diff --git a/ckanext/dataexplorer/public/vendor/slickgrid/2.2/plugins/slick.cellcopymanager.js b/ckanext/dataexplorer/public/vendor/slickgrid/2.2/plugins/slick.cellcopymanager.js new file mode 100644 index 0000000..c74018d --- /dev/null +++ b/ckanext/dataexplorer/public/vendor/slickgrid/2.2/plugins/slick.cellcopymanager.js @@ -0,0 +1,86 @@ +(function ($) { + // register namespace + $.extend(true, window, { + "Slick": { + "CellCopyManager": CellCopyManager + } + }); + + + function CellCopyManager() { + var _grid; + var _self = this; + var _copiedRanges; + + function init(grid) { + _grid = grid; + _grid.onKeyDown.subscribe(handleKeyDown); + } + + function destroy() { + _grid.onKeyDown.unsubscribe(handleKeyDown); + } + + function handleKeyDown(e, args) { + var ranges; + if (!_grid.getEditorLock().isActive()) { + if (e.which == $.ui.keyCode.ESCAPE) { + if (_copiedRanges) { + e.preventDefault(); + clearCopySelection(); + _self.onCopyCancelled.notify({ranges: _copiedRanges}); + _copiedRanges = null; + } + } + + if (e.which == 67 && (e.ctrlKey || e.metaKey)) { + ranges = _grid.getSelectionModel().getSelectedRanges(); + if (ranges.length != 0) { + e.preventDefault(); + _copiedRanges = ranges; + markCopySelection(ranges); + _self.onCopyCells.notify({ranges: ranges}); + } + } + + if (e.which == 86 && (e.ctrlKey || e.metaKey)) { + if (_copiedRanges) { + e.preventDefault(); + clearCopySelection(); + ranges = _grid.getSelectionModel().getSelectedRanges(); + _self.onPasteCells.notify({from: _copiedRanges, to: ranges}); + _copiedRanges = null; + } + } + } + } + + function markCopySelection(ranges) { + var columns = _grid.getColumns(); + var hash = {}; + for (var i = 0; i < ranges.length; i++) { + for (var j = ranges[i].fromRow; j <= ranges[i].toRow; j++) { + hash[j] = {}; + for (var k = ranges[i].fromCell; k <= ranges[i].toCell; k++) { + hash[j][columns[k].id] = "copied"; + } + } + } + _grid.setCellCssStyles("copy-manager", hash); + } + + function clearCopySelection() { + _grid.removeCellCssStyles("copy-manager"); + } + + $.extend(this, { + "init": init, + "destroy": destroy, + "clearCopySelection": clearCopySelection, + + "onCopyCells": new Slick.Event(), + "onCopyCancelled": new Slick.Event(), + "onPasteCells": new Slick.Event() + }); + } +})(jQuery); \ No newline at end of file diff --git a/ckanext/dataexplorer/public/vendor/slickgrid/2.2/plugins/slick.cellrangedecorator.js b/ckanext/dataexplorer/public/vendor/slickgrid/2.2/plugins/slick.cellrangedecorator.js new file mode 100644 index 0000000..0cbe71d --- /dev/null +++ b/ckanext/dataexplorer/public/vendor/slickgrid/2.2/plugins/slick.cellrangedecorator.js @@ -0,0 +1,66 @@ +(function ($) { + // register namespace + $.extend(true, window, { + "Slick": { + "CellRangeDecorator": CellRangeDecorator + } + }); + + /*** + * Displays an overlay on top of a given cell range. + * + * TODO: + * Currently, it blocks mouse events to DOM nodes behind it. + * Use FF and WebKit-specific "pointer-events" CSS style, or some kind of event forwarding. + * Could also construct the borders separately using 4 individual DIVs. + * + * @param {Grid} grid + * @param {Object} options + */ + function CellRangeDecorator(grid, options) { + var _elem; + var _defaults = { + selectionCssClass: 'slick-range-decorator', + selectionCss: { + "zIndex": "9999", + "border": "2px dashed red" + } + }; + + options = $.extend(true, {}, _defaults, options); + + + function show(range) { + if (!_elem) { + _elem = $("
", {css: options.selectionCss}) + .addClass(options.selectionCssClass) + .css("position", "absolute") + .appendTo(grid.getCanvasNode()); + } + + var from = grid.getCellNodeBox(range.fromRow, range.fromCell); + var to = grid.getCellNodeBox(range.toRow, range.toCell); + + _elem.css({ + top: from.top - 1, + left: from.left - 1, + height: to.bottom - from.top - 2, + width: to.right - from.left - 2 + }); + + return _elem; + } + + function hide() { + if (_elem) { + _elem.remove(); + _elem = null; + } + } + + $.extend(this, { + "show": show, + "hide": hide + }); + } +})(jQuery); diff --git a/ckanext/dataexplorer/public/vendor/slickgrid/2.2/plugins/slick.cellrangeselector.js b/ckanext/dataexplorer/public/vendor/slickgrid/2.2/plugins/slick.cellrangeselector.js new file mode 100644 index 0000000..520b17f --- /dev/null +++ b/ckanext/dataexplorer/public/vendor/slickgrid/2.2/plugins/slick.cellrangeselector.js @@ -0,0 +1,113 @@ +(function ($) { + // register namespace + $.extend(true, window, { + "Slick": { + "CellRangeSelector": CellRangeSelector + } + }); + + + function CellRangeSelector(options) { + var _grid; + var _canvas; + var _dragging; + var _decorator; + var _self = this; + var _handler = new Slick.EventHandler(); + var _defaults = { + selectionCss: { + "border": "2px dashed blue" + } + }; + + + function init(grid) { + options = $.extend(true, {}, _defaults, options); + _decorator = new Slick.CellRangeDecorator(grid, options); + _grid = grid; + _canvas = _grid.getCanvasNode(); + _handler + .subscribe(_grid.onDragInit, handleDragInit) + .subscribe(_grid.onDragStart, handleDragStart) + .subscribe(_grid.onDrag, handleDrag) + .subscribe(_grid.onDragEnd, handleDragEnd); + } + + function destroy() { + _handler.unsubscribeAll(); + } + + function handleDragInit(e, dd) { + // prevent the grid from cancelling drag'n'drop by default + e.stopImmediatePropagation(); + } + + function handleDragStart(e, dd) { + var cell = _grid.getCellFromEvent(e); + if (_self.onBeforeCellRangeSelected.notify(cell) !== false) { + if (_grid.canCellBeSelected(cell.row, cell.cell)) { + _dragging = true; + e.stopImmediatePropagation(); + } + } + if (!_dragging) { + return; + } + + _grid.focus(); + + var start = _grid.getCellFromPoint( + dd.startX - $(_canvas).offset().left, + dd.startY - $(_canvas).offset().top); + + dd.range = {start: start, end: {}}; + + return _decorator.show(new Slick.Range(start.row, start.cell)); + } + + function handleDrag(e, dd) { + if (!_dragging) { + return; + } + e.stopImmediatePropagation(); + + var end = _grid.getCellFromPoint( + e.pageX - $(_canvas).offset().left, + e.pageY - $(_canvas).offset().top); + + if (!_grid.canCellBeSelected(end.row, end.cell)) { + return; + } + + dd.range.end = end; + _decorator.show(new Slick.Range(dd.range.start.row, dd.range.start.cell, end.row, end.cell)); + } + + function handleDragEnd(e, dd) { + if (!_dragging) { + return; + } + + _dragging = false; + e.stopImmediatePropagation(); + + _decorator.hide(); + _self.onCellRangeSelected.notify({ + range: new Slick.Range( + dd.range.start.row, + dd.range.start.cell, + dd.range.end.row, + dd.range.end.cell + ) + }); + } + + $.extend(this, { + "init": init, + "destroy": destroy, + + "onBeforeCellRangeSelected": new Slick.Event(), + "onCellRangeSelected": new Slick.Event() + }); + } +})(jQuery); \ No newline at end of file diff --git a/ckanext/dataexplorer/public/vendor/slickgrid/2.2/plugins/slick.cellselectionmodel.js b/ckanext/dataexplorer/public/vendor/slickgrid/2.2/plugins/slick.cellselectionmodel.js new file mode 100644 index 0000000..74bc3eb --- /dev/null +++ b/ckanext/dataexplorer/public/vendor/slickgrid/2.2/plugins/slick.cellselectionmodel.js @@ -0,0 +1,154 @@ +(function ($) { + // register namespace + $.extend(true, window, { + "Slick": { + "CellSelectionModel": CellSelectionModel + } + }); + + + function CellSelectionModel(options) { + var _grid; + var _canvas; + var _ranges = []; + var _self = this; + var _selector = new Slick.CellRangeSelector({ + "selectionCss": { + "border": "2px solid black" + } + }); + var _options; + var _defaults = { + selectActiveCell: true + }; + + + function init(grid) { + _options = $.extend(true, {}, _defaults, options); + _grid = grid; + _canvas = _grid.getCanvasNode(); + _grid.onActiveCellChanged.subscribe(handleActiveCellChange); + _grid.onKeyDown.subscribe(handleKeyDown); + grid.registerPlugin(_selector); + _selector.onCellRangeSelected.subscribe(handleCellRangeSelected); + _selector.onBeforeCellRangeSelected.subscribe(handleBeforeCellRangeSelected); + } + + function destroy() { + _grid.onActiveCellChanged.unsubscribe(handleActiveCellChange); + _grid.onKeyDown.unsubscribe(handleKeyDown); + _selector.onCellRangeSelected.unsubscribe(handleCellRangeSelected); + _selector.onBeforeCellRangeSelected.unsubscribe(handleBeforeCellRangeSelected); + _grid.unregisterPlugin(_selector); + } + + function removeInvalidRanges(ranges) { + var result = []; + + for (var i = 0; i < ranges.length; i++) { + var r = ranges[i]; + if (_grid.canCellBeSelected(r.fromRow, r.fromCell) && _grid.canCellBeSelected(r.toRow, r.toCell)) { + result.push(r); + } + } + + return result; + } + + function setSelectedRanges(ranges) { + _ranges = removeInvalidRanges(ranges); + _self.onSelectedRangesChanged.notify(_ranges); + } + + function getSelectedRanges() { + return _ranges; + } + + function handleBeforeCellRangeSelected(e, args) { + if (_grid.getEditorLock().isActive()) { + e.stopPropagation(); + return false; + } + } + + function handleCellRangeSelected(e, args) { + setSelectedRanges([args.range]); + } + + function handleActiveCellChange(e, args) { + if (_options.selectActiveCell && args.row != null && args.cell != null) { + setSelectedRanges([new Slick.Range(args.row, args.cell)]); + } + } + + function handleKeyDown(e) { + /*** + * Кey codes + * 37 left + * 38 up + * 39 right + * 40 down + */ + var ranges, last; + var active = _grid.getActiveCell(); + + if ( active && e.shiftKey && !e.ctrlKey && !e.altKey && + (e.which == 37 || e.which == 39 || e.which == 38 || e.which == 40) ) { + + ranges = getSelectedRanges(); + if (!ranges.length) + ranges.push(new Slick.Range(active.row, active.cell)); + + // keyboard can work with last range only + last = ranges.pop(); + + // can't handle selection out of active cell + if (!last.contains(active.row, active.cell)) + last = new Slick.Range(active.row, active.cell); + + var dRow = last.toRow - last.fromRow, + dCell = last.toCell - last.fromCell, + // walking direction + dirRow = active.row == last.fromRow ? 1 : -1, + dirCell = active.cell == last.fromCell ? 1 : -1; + + if (e.which == 37) { + dCell -= dirCell; + } else if (e.which == 39) { + dCell += dirCell ; + } else if (e.which == 38) { + dRow -= dirRow; + } else if (e.which == 40) { + dRow += dirRow; + } + + // define new selection range + var new_last = new Slick.Range(active.row, active.cell, active.row + dirRow*dRow, active.cell + dirCell*dCell); + if (removeInvalidRanges([new_last]).length) { + ranges.push(new_last); + var viewRow = dirRow > 0 ? new_last.toRow : new_last.fromRow; + var viewCell = dirCell > 0 ? new_last.toCell : new_last.fromCell; + _grid.scrollRowIntoView(viewRow); + _grid.scrollCellIntoView(viewRow, viewCell); + } + else + ranges.push(last); + + setSelectedRanges(ranges); + + e.preventDefault(); + e.stopPropagation(); + } + } + + $.extend(this, { + "getSelectedRanges": getSelectedRanges, + "setSelectedRanges": setSelectedRanges, + + "init": init, + "destroy": destroy, + + "onSelectedRangesChanged": new Slick.Event() + }); + } +})(jQuery); diff --git a/ckanext/dataexplorer/public/vendor/slickgrid/2.2/plugins/slick.checkboxselectcolumn.js b/ckanext/dataexplorer/public/vendor/slickgrid/2.2/plugins/slick.checkboxselectcolumn.js new file mode 100644 index 0000000..83d8d50 --- /dev/null +++ b/ckanext/dataexplorer/public/vendor/slickgrid/2.2/plugins/slick.checkboxselectcolumn.js @@ -0,0 +1,153 @@ +(function ($) { + // register namespace + $.extend(true, window, { + "Slick": { + "CheckboxSelectColumn": CheckboxSelectColumn + } + }); + + + function CheckboxSelectColumn(options) { + var _grid; + var _self = this; + var _handler = new Slick.EventHandler(); + var _selectedRowsLookup = {}; + var _defaults = { + columnId: "_checkbox_selector", + cssClass: null, + toolTip: "Select/Deselect All", + width: 30 + }; + + var _options = $.extend(true, {}, _defaults, options); + + function init(grid) { + _grid = grid; + _handler + .subscribe(_grid.onSelectedRowsChanged, handleSelectedRowsChanged) + .subscribe(_grid.onClick, handleClick) + .subscribe(_grid.onHeaderClick, handleHeaderClick) + .subscribe(_grid.onKeyDown, handleKeyDown); + } + + function destroy() { + _handler.unsubscribeAll(); + } + + function handleSelectedRowsChanged(e, args) { + var selectedRows = _grid.getSelectedRows(); + var lookup = {}, row, i; + for (i = 0; i < selectedRows.length; i++) { + row = selectedRows[i]; + lookup[row] = true; + if (lookup[row] !== _selectedRowsLookup[row]) { + _grid.invalidateRow(row); + delete _selectedRowsLookup[row]; + } + } + for (i in _selectedRowsLookup) { + _grid.invalidateRow(i); + } + _selectedRowsLookup = lookup; + _grid.render(); + + if (selectedRows.length && selectedRows.length == _grid.getDataLength()) { + _grid.updateColumnHeader(_options.columnId, "", _options.toolTip); + } else { + _grid.updateColumnHeader(_options.columnId, "", _options.toolTip); + } + } + + function handleKeyDown(e, args) { + if (e.which == 32) { + if (_grid.getColumns()[args.cell].id === _options.columnId) { + // if editing, try to commit + if (!_grid.getEditorLock().isActive() || _grid.getEditorLock().commitCurrentEdit()) { + toggleRowSelection(args.row); + } + e.preventDefault(); + e.stopImmediatePropagation(); + } + } + } + + function handleClick(e, args) { + // clicking on a row select checkbox + if (_grid.getColumns()[args.cell].id === _options.columnId && $(e.target).is(":checkbox")) { + // if editing, try to commit + if (_grid.getEditorLock().isActive() && !_grid.getEditorLock().commitCurrentEdit()) { + e.preventDefault(); + e.stopImmediatePropagation(); + return; + } + + toggleRowSelection(args.row); + e.stopPropagation(); + e.stopImmediatePropagation(); + } + } + + function toggleRowSelection(row) { + if (_selectedRowsLookup[row]) { + _grid.setSelectedRows($.grep(_grid.getSelectedRows(), function (n) { + return n != row + })); + } else { + _grid.setSelectedRows(_grid.getSelectedRows().concat(row)); + } + } + + function handleHeaderClick(e, args) { + if (args.column.id == _options.columnId && $(e.target).is(":checkbox")) { + // if editing, try to commit + if (_grid.getEditorLock().isActive() && !_grid.getEditorLock().commitCurrentEdit()) { + e.preventDefault(); + e.stopImmediatePropagation(); + return; + } + + if ($(e.target).is(":checked")) { + var rows = []; + for (var i = 0; i < _grid.getDataLength(); i++) { + rows.push(i); + } + _grid.setSelectedRows(rows); + } else { + _grid.setSelectedRows([]); + } + e.stopPropagation(); + e.stopImmediatePropagation(); + } + } + + function getColumnDefinition() { + return { + id: _options.columnId, + name: "", + toolTip: _options.toolTip, + field: "sel", + width: _options.width, + resizable: false, + sortable: false, + cssClass: _options.cssClass, + formatter: checkboxSelectionFormatter + }; + } + + function checkboxSelectionFormatter(row, cell, value, columnDef, dataContext) { + if (dataContext) { + return _selectedRowsLookup[row] + ? "" + : ""; + } + return null; + } + + $.extend(this, { + "init": init, + "destroy": destroy, + + "getColumnDefinition": getColumnDefinition + }); + } +})(jQuery); \ No newline at end of file diff --git a/ckanext/dataexplorer/public/vendor/slickgrid/2.2/plugins/slick.headerbuttons.css b/ckanext/dataexplorer/public/vendor/slickgrid/2.2/plugins/slick.headerbuttons.css new file mode 100644 index 0000000..0ba79ea --- /dev/null +++ b/ckanext/dataexplorer/public/vendor/slickgrid/2.2/plugins/slick.headerbuttons.css @@ -0,0 +1,39 @@ +.slick-column-name, +.slick-sort-indicator { + /** + * This makes all "float:right" elements after it that spill over to the next line + * display way below the lower boundary of the column thus hiding them. + */ + display: inline-block; + float: left; + margin-bottom: 100px; +} + +.slick-header-button { + display: inline-block; + float: right; + vertical-align: top; + margin: 1px; + /** + * This makes all "float:right" elements after it that spill over to the next line + * display way below the lower boundary of the column thus hiding them. + */ + margin-bottom: 100px; + height: 15px; + width: 15px; + background-repeat: no-repeat; + background-position: center center; + cursor: pointer; +} + +.slick-header-button-hidden { + width: 0; + + -webkit-transition: 0.2s width; + -ms-transition: 0.2s width; + transition: 0.2s width; +} + +.slick-header-column:hover > .slick-header-button { + width: 15px; +} \ No newline at end of file diff --git a/ckanext/dataexplorer/public/vendor/slickgrid/2.2/plugins/slick.headerbuttons.js b/ckanext/dataexplorer/public/vendor/slickgrid/2.2/plugins/slick.headerbuttons.js new file mode 100644 index 0000000..8e61273 --- /dev/null +++ b/ckanext/dataexplorer/public/vendor/slickgrid/2.2/plugins/slick.headerbuttons.js @@ -0,0 +1,177 @@ +(function ($) { + // register namespace + $.extend(true, window, { + "Slick": { + "Plugins": { + "HeaderButtons": HeaderButtons + } + } + }); + + + /*** + * A plugin to add custom buttons to column headers. + * + * USAGE: + * + * Add the plugin .js & .css files and register it with the grid. + * + * To specify a custom button in a column header, extend the column definition like so: + * + * var columns = [ + * { + * id: 'myColumn', + * name: 'My column', + * + * // This is the relevant part + * header: { + * buttons: [ + * { + * // button options + * }, + * { + * // button options + * } + * ] + * } + * } + * ]; + * + * Available button options: + * cssClass: CSS class to add to the button. + * image: Relative button image path. + * tooltip: Button tooltip. + * showOnHover: Only show the button on hover. + * handler: Button click handler. + * command: A command identifier to be passed to the onCommand event handlers. + * + * The plugin exposes the following events: + * onCommand: Fired on button click for buttons with 'command' specified. + * Event args: + * grid: Reference to the grid. + * column: Column definition. + * command: Button command identified. + * button: Button options. Note that you can change the button options in your + * event handler, and the column header will be automatically updated to + * reflect them. This is useful if you want to implement something like a + * toggle button. + * + * + * @param options {Object} Options: + * buttonCssClass: a CSS class to use for buttons (default 'slick-header-button') + * @class Slick.Plugins.HeaderButtons + * @constructor + */ + function HeaderButtons(options) { + var _grid; + var _self = this; + var _handler = new Slick.EventHandler(); + var _defaults = { + buttonCssClass: "slick-header-button" + }; + + + function init(grid) { + options = $.extend(true, {}, _defaults, options); + _grid = grid; + _handler + .subscribe(_grid.onHeaderCellRendered, handleHeaderCellRendered) + .subscribe(_grid.onBeforeHeaderCellDestroy, handleBeforeHeaderCellDestroy); + + // Force the grid to re-render the header now that the events are hooked up. + _grid.setColumns(_grid.getColumns()); + } + + + function destroy() { + _handler.unsubscribeAll(); + } + + + function handleHeaderCellRendered(e, args) { + var column = args.column; + + if (column.header && column.header.buttons) { + // Append buttons in reverse order since they are floated to the right. + var i = column.header.buttons.length; + while (i--) { + var button = column.header.buttons[i]; + var btn = $("
") + .addClass(options.buttonCssClass) + .data("column", column) + .data("button", button); + + if (button.showOnHover) { + btn.addClass("slick-header-button-hidden"); + } + + if (button.image) { + btn.css("backgroundImage", "url(" + button.image + ")"); + } + + if (button.cssClass) { + btn.addClass(button.cssClass); + } + + if (button.tooltip) { + btn.attr("title", button.tooltip); + } + + if (button.command) { + btn.data("command", button.command); + } + + if (button.handler) { + btn.bind("click", button.handler); + } + + btn + .bind("click", handleButtonClick) + .appendTo(args.node); + } + } + } + + + function handleBeforeHeaderCellDestroy(e, args) { + var column = args.column; + + if (column.header && column.header.buttons) { + // Removing buttons via jQuery will also clean up any event handlers and data. + // NOTE: If you attach event handlers directly or using a different framework, + // you must also clean them up here to avoid memory leaks. + $(args.node).find("." + options.buttonCssClass).remove(); + } + } + + + function handleButtonClick(e) { + var command = $(this).data("command"); + var columnDef = $(this).data("column"); + var button = $(this).data("button"); + + if (command != null) { + _self.onCommand.notify({ + "grid": _grid, + "column": columnDef, + "command": command, + "button": button + }, e, _self); + + // Update the header in case the user updated the button definition in the handler. + _grid.updateColumnHeader(columnDef.id); + } + + // Stop propagation so that it doesn't register as a header click event. + e.preventDefault(); + e.stopPropagation(); + } + + $.extend(this, { + "init": init, + "destroy": destroy, + + "onCommand": new Slick.Event() + }); + } +})(jQuery); \ No newline at end of file diff --git a/ckanext/dataexplorer/public/vendor/slickgrid/2.2/plugins/slick.headermenu.css b/ckanext/dataexplorer/public/vendor/slickgrid/2.2/plugins/slick.headermenu.css new file mode 100644 index 0000000..8b0b6a9 --- /dev/null +++ b/ckanext/dataexplorer/public/vendor/slickgrid/2.2/plugins/slick.headermenu.css @@ -0,0 +1,59 @@ +/* Menu button */ +.slick-header-menubutton { + position: absolute; + right: 0; + top: 0; + bottom: 0; + width: 14px; + background-repeat: no-repeat; + background-position: left center; + background-image: url(../images/down.gif); + cursor: pointer; + + display: none; + border-left: thin ridge silver; +} + +.slick-header-column:hover > .slick-header-menubutton, +.slick-header-column-active .slick-header-menubutton { + display: inline-block; +} + +/* Menu */ +.slick-header-menu { + position: absolute; + display: inline-block; + margin: 0; + padding: 2px; + cursor: default; +} + + +/* Menu items */ +.slick-header-menuitem { + list-style: none; + margin: 0; + padding: 0; + cursor: pointer; +} + +.slick-header-menuicon { + display: inline-block; + width: 16px; + height: 16px; + vertical-align: middle; + margin-right: 4px; + background-repeat: no-repeat; + background-position: center center; +} + +.slick-header-menucontent { + display: inline-block; + vertical-align: middle; +} + + +/* Disabled */ +.slick-header-menuitem-disabled { + color: silver; +} diff --git a/ckanext/dataexplorer/public/vendor/slickgrid/2.2/plugins/slick.headermenu.js b/ckanext/dataexplorer/public/vendor/slickgrid/2.2/plugins/slick.headermenu.js new file mode 100644 index 0000000..ec8244d --- /dev/null +++ b/ckanext/dataexplorer/public/vendor/slickgrid/2.2/plugins/slick.headermenu.js @@ -0,0 +1,275 @@ +(function ($) { + // register namespace + $.extend(true, window, { + "Slick": { + "Plugins": { + "HeaderMenu": HeaderMenu + } + } + }); + + + /*** + * A plugin to add drop-down menus to column headers. + * + * USAGE: + * + * Add the plugin .js & .css files and register it with the grid. + * + * To specify a menu in a column header, extend the column definition like so: + * + * var columns = [ + * { + * id: 'myColumn', + * name: 'My column', + * + * // This is the relevant part + * header: { + * menu: { + * items: [ + * { + * // menu item options + * }, + * { + * // menu item options + * } + * ] + * } + * } + * } + * ]; + * + * + * Available menu options: + * tooltip: Menu button tooltip. + * + * + * Available menu item options: + * title: Menu item text. + * disabled: Whether the item is disabled. + * tooltip: Item tooltip. + * command: A command identifier to be passed to the onCommand event handlers. + * iconCssClass: A CSS class to be added to the menu item icon. + * iconImage: A url to the icon image. + * + * + * The plugin exposes the following events: + * onBeforeMenuShow: Fired before the menu is shown. You can customize the menu or dismiss it by returning false. + * Event args: + * grid: Reference to the grid. + * column: Column definition. + * menu: Menu options. Note that you can change the menu items here. + * + * onCommand: Fired on menu item click for buttons with 'command' specified. + * Event args: + * grid: Reference to the grid. + * column: Column definition. + * command: Button command identified. + * button: Button options. Note that you can change the button options in your + * event handler, and the column header will be automatically updated to + * reflect them. This is useful if you want to implement something like a + * toggle button. + * + * + * @param options {Object} Options: + * buttonCssClass: an extra CSS class to add to the menu button + * buttonImage: a url to the menu button image (default '../images/down.gif') + * @class Slick.Plugins.HeaderButtons + * @constructor + */ + function HeaderMenu(options) { + var _grid; + var _self = this; + var _handler = new Slick.EventHandler(); + var _defaults = { + buttonCssClass: null, + buttonImage: null + }; + var $menu; + var $activeHeaderColumn; + + + function init(grid) { + options = $.extend(true, {}, _defaults, options); + _grid = grid; + _handler + .subscribe(_grid.onHeaderCellRendered, handleHeaderCellRendered) + .subscribe(_grid.onBeforeHeaderCellDestroy, handleBeforeHeaderCellDestroy); + + // Force the grid to re-render the header now that the events are hooked up. + _grid.setColumns(_grid.getColumns()); + + // Hide the menu on outside click. + $(document.body).bind("mousedown", handleBodyMouseDown); + } + + + function destroy() { + _handler.unsubscribeAll(); + $(document.body).unbind("mousedown", handleBodyMouseDown); + } + + + function handleBodyMouseDown(e) { + if ($menu && $menu[0] != e.target && !$.contains($menu[0], e.target)) { + hideMenu(); + } + } + + + function hideMenu() { + if ($menu) { + $menu.remove(); + $menu = null; + $activeHeaderColumn + .removeClass("slick-header-column-active"); + } + } + + function handleHeaderCellRendered(e, args) { + var column = args.column; + var menu = column.header && column.header.menu; + + if (menu) { + var $el = $("
") + .addClass("slick-header-menubutton") + .data("column", column) + .data("menu", menu); + + if (options.buttonCssClass) { + $el.addClass(options.buttonCssClass); + } + + if (options.buttonImage) { + $el.css("background-image", "url(" + options.buttonImage + ")"); + } + + if (menu.tooltip) { + $el.attr("title", menu.tooltip); + } + + $el + .bind("click", showMenu) + .appendTo(args.node); + } + } + + + function handleBeforeHeaderCellDestroy(e, args) { + var column = args.column; + + if (column.header && column.header.menu) { + $(args.node).find(".slick-header-menubutton").remove(); + } + } + + + function showMenu(e) { + var $menuButton = $(this); + var menu = $menuButton.data("menu"); + var columnDef = $menuButton.data("column"); + + // Let the user modify the menu or cancel altogether, + // or provide alternative menu implementation. + if (_self.onBeforeMenuShow.notify({ + "grid": _grid, + "column": columnDef, + "menu": menu + }, e, _self) == false) { + return; + } + + + if (!$menu) { + $menu = $("
") + .appendTo(_grid.getContainerNode()); + } + $menu.empty(); + + + // Construct the menu items. + for (var i = 0; i < menu.items.length; i++) { + var item = menu.items[i]; + + var $li = $("
") + .data("command", item.command || '') + .data("column", columnDef) + .data("item", item) + .bind("click", handleMenuItemClick) + .appendTo($menu); + + if (item.disabled) { + $li.addClass("slick-header-menuitem-disabled"); + } + + if (item.tooltip) { + $li.attr("title", item.tooltip); + } + + var $icon = $("
") + .appendTo($li); + + if (item.iconCssClass) { + $icon.addClass(item.iconCssClass); + } + + if (item.iconImage) { + $icon.css("background-image", "url(" + item.iconImage + ")"); + } + + $("") + .text(item.title) + .appendTo($li); + } + + + // Position the menu. + $menu + .offset({ top: $(this).offset().top + $(this).height(), left: $(this).offset().left }); + + + // Mark the header as active to keep the highlighting. + $activeHeaderColumn = $menuButton.closest(".slick-header-column"); + $activeHeaderColumn + .addClass("slick-header-column-active"); + + // Stop propagation so that it doesn't register as a header click event. + e.preventDefault(); + e.stopPropagation(); + } + + + function handleMenuItemClick(e) { + var command = $(this).data("command"); + var columnDef = $(this).data("column"); + var item = $(this).data("item"); + + if (item.disabled) { + return; + } + + hideMenu(); + + if (command != null && command != '') { + _self.onCommand.notify({ + "grid": _grid, + "column": columnDef, + "command": command, + "item": item + }, e, _self); + } + + // Stop propagation so that it doesn't register as a header click event. + e.preventDefault(); + e.stopPropagation(); + } + + $.extend(this, { + "init": init, + "destroy": destroy, + + "onBeforeMenuShow": new Slick.Event(), + "onCommand": new Slick.Event() + }); + } +})(jQuery); diff --git a/ckanext/dataexplorer/public/vendor/slickgrid/2.2/plugins/slick.rowmovemanager.js b/ckanext/dataexplorer/public/vendor/slickgrid/2.2/plugins/slick.rowmovemanager.js new file mode 100644 index 0000000..5f87a1e --- /dev/null +++ b/ckanext/dataexplorer/public/vendor/slickgrid/2.2/plugins/slick.rowmovemanager.js @@ -0,0 +1,138 @@ +(function ($) { + // register namespace + $.extend(true, window, { + "Slick": { + "RowMoveManager": RowMoveManager + } + }); + + function RowMoveManager(options) { + var _grid; + var _canvas; + var _dragging; + var _self = this; + var _handler = new Slick.EventHandler(); + var _defaults = { + cancelEditOnDrag: false + }; + + function init(grid) { + options = $.extend(true, {}, _defaults, options); + _grid = grid; + _canvas = _grid.getCanvasNode(); + _handler + .subscribe(_grid.onDragInit, handleDragInit) + .subscribe(_grid.onDragStart, handleDragStart) + .subscribe(_grid.onDrag, handleDrag) + .subscribe(_grid.onDragEnd, handleDragEnd); + } + + function destroy() { + _handler.unsubscribeAll(); + } + + function handleDragInit(e, dd) { + // prevent the grid from cancelling drag'n'drop by default + e.stopImmediatePropagation(); + } + + function handleDragStart(e, dd) { + var cell = _grid.getCellFromEvent(e); + + if (options.cancelEditOnDrag && _grid.getEditorLock().isActive()) { + _grid.getEditorLock().cancelCurrentEdit(); + } + + if (_grid.getEditorLock().isActive() || !/move|selectAndMove/.test(_grid.getColumns()[cell.cell].behavior)) { + return false; + } + + _dragging = true; + e.stopImmediatePropagation(); + + var selectedRows = _grid.getSelectedRows(); + + if (selectedRows.length == 0 || $.inArray(cell.row, selectedRows) == -1) { + selectedRows = [cell.row]; + _grid.setSelectedRows(selectedRows); + } + + var rowHeight = _grid.getOptions().rowHeight; + + dd.selectedRows = selectedRows; + + dd.selectionProxy = $("
") + .css("position", "absolute") + .css("zIndex", "99999") + .css("width", $(_canvas).innerWidth()) + .css("height", rowHeight * selectedRows.length) + .appendTo(_canvas); + + dd.guide = $("
") + .css("position", "absolute") + .css("zIndex", "99998") + .css("width", $(_canvas).innerWidth()) + .css("top", -1000) + .appendTo(_canvas); + + dd.insertBefore = -1; + } + + function handleDrag(e, dd) { + if (!_dragging) { + return; + } + + e.stopImmediatePropagation(); + + var top = e.pageY - $(_canvas).offset().top; + dd.selectionProxy.css("top", top - 5); + + var insertBefore = Math.max(0, Math.min(Math.round(top / _grid.getOptions().rowHeight), _grid.getDataLength())); + if (insertBefore !== dd.insertBefore) { + var eventData = { + "rows": dd.selectedRows, + "insertBefore": insertBefore + }; + + if (_self.onBeforeMoveRows.notify(eventData) === false) { + dd.guide.css("top", -1000); + dd.canMove = false; + } else { + dd.guide.css("top", insertBefore * _grid.getOptions().rowHeight); + dd.canMove = true; + } + + dd.insertBefore = insertBefore; + } + } + + function handleDragEnd(e, dd) { + if (!_dragging) { + return; + } + _dragging = false; + e.stopImmediatePropagation(); + + dd.guide.remove(); + dd.selectionProxy.remove(); + + if (dd.canMove) { + var eventData = { + "rows": dd.selectedRows, + "insertBefore": dd.insertBefore + }; + // TODO: _grid.remapCellCssClasses ? + _self.onMoveRows.notify(eventData); + } + } + + $.extend(this, { + "onBeforeMoveRows": new Slick.Event(), + "onMoveRows": new Slick.Event(), + + "init": init, + "destroy": destroy + }); + } +})(jQuery); \ No newline at end of file diff --git a/ckanext/dataexplorer/public/vendor/slickgrid/2.2/plugins/slick.rowselectionmodel.js b/ckanext/dataexplorer/public/vendor/slickgrid/2.2/plugins/slick.rowselectionmodel.js new file mode 100644 index 0000000..0de8dd3 --- /dev/null +++ b/ckanext/dataexplorer/public/vendor/slickgrid/2.2/plugins/slick.rowselectionmodel.js @@ -0,0 +1,187 @@ +(function ($) { + // register namespace + $.extend(true, window, { + "Slick": { + "RowSelectionModel": RowSelectionModel + } + }); + + function RowSelectionModel(options) { + var _grid; + var _ranges = []; + var _self = this; + var _handler = new Slick.EventHandler(); + var _inHandler; + var _options; + var _defaults = { + selectActiveRow: true + }; + + function init(grid) { + _options = $.extend(true, {}, _defaults, options); + _grid = grid; + _handler.subscribe(_grid.onActiveCellChanged, + wrapHandler(handleActiveCellChange)); + _handler.subscribe(_grid.onKeyDown, + wrapHandler(handleKeyDown)); + _handler.subscribe(_grid.onClick, + wrapHandler(handleClick)); + } + + function destroy() { + _handler.unsubscribeAll(); + } + + function wrapHandler(handler) { + return function () { + if (!_inHandler) { + _inHandler = true; + handler.apply(this, arguments); + _inHandler = false; + } + }; + } + + function rangesToRows(ranges) { + var rows = []; + for (var i = 0; i < ranges.length; i++) { + for (var j = ranges[i].fromRow; j <= ranges[i].toRow; j++) { + rows.push(j); + } + } + return rows; + } + + function rowsToRanges(rows) { + var ranges = []; + var lastCell = _grid.getColumns().length - 1; + for (var i = 0; i < rows.length; i++) { + ranges.push(new Slick.Range(rows[i], 0, rows[i], lastCell)); + } + return ranges; + } + + function getRowsRange(from, to) { + var i, rows = []; + for (i = from; i <= to; i++) { + rows.push(i); + } + for (i = to; i < from; i++) { + rows.push(i); + } + return rows; + } + + function getSelectedRows() { + return rangesToRows(_ranges); + } + + function setSelectedRows(rows) { + setSelectedRanges(rowsToRanges(rows)); + } + + function setSelectedRanges(ranges) { + _ranges = ranges; + _self.onSelectedRangesChanged.notify(_ranges); + } + + function getSelectedRanges() { + return _ranges; + } + + function handleActiveCellChange(e, data) { + if (_options.selectActiveRow && data.row != null) { + setSelectedRanges([new Slick.Range(data.row, 0, data.row, _grid.getColumns().length - 1)]); + } + } + + function handleKeyDown(e) { + var activeRow = _grid.getActiveCell(); + if (activeRow && e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey && (e.which == 38 || e.which == 40)) { + var selectedRows = getSelectedRows(); + selectedRows.sort(function (x, y) { + return x - y + }); + + if (!selectedRows.length) { + selectedRows = [activeRow.row]; + } + + var top = selectedRows[0]; + var bottom = selectedRows[selectedRows.length - 1]; + var active; + + if (e.which == 40) { + active = activeRow.row < bottom || top == bottom ? ++bottom : ++top; + } else { + active = activeRow.row < bottom ? --bottom : --top; + } + + if (active >= 0 && active < _grid.getDataLength()) { + _grid.scrollRowIntoView(active); + _ranges = rowsToRanges(getRowsRange(top, bottom)); + setSelectedRanges(_ranges); + } + + e.preventDefault(); + e.stopPropagation(); + } + } + + function handleClick(e) { + var cell = _grid.getCellFromEvent(e); + if (!cell || !_grid.canCellBeActive(cell.row, cell.cell)) { + return false; + } + + if (!_grid.getOptions().multiSelect || ( + !e.ctrlKey && !e.shiftKey && !e.metaKey)) { + return false; + } + + var selection = rangesToRows(_ranges); + var idx = $.inArray(cell.row, selection); + + if (idx === -1 && (e.ctrlKey || e.metaKey)) { + selection.push(cell.row); + _grid.setActiveCell(cell.row, cell.cell); + } else if (idx !== -1 && (e.ctrlKey || e.metaKey)) { + selection = $.grep(selection, function (o, i) { + return (o !== cell.row); + }); + _grid.setActiveCell(cell.row, cell.cell); + } else if (selection.length && e.shiftKey) { + var last = selection.pop(); + var from = Math.min(cell.row, last); + var to = Math.max(cell.row, last); + selection = []; + for (var i = from; i <= to; i++) { + if (i !== last) { + selection.push(i); + } + } + selection.push(last); + _grid.setActiveCell(cell.row, cell.cell); + } + + _ranges = rowsToRanges(selection); + setSelectedRanges(_ranges); + e.stopImmediatePropagation(); + + return true; + } + + $.extend(this, { + "getSelectedRows": getSelectedRows, + "setSelectedRows": setSelectedRows, + + "getSelectedRanges": getSelectedRanges, + "setSelectedRanges": setSelectedRanges, + + "init": init, + "destroy": destroy, + + "onSelectedRangesChanged": new Slick.Event() + }); + } +})(jQuery); \ No newline at end of file diff --git a/ckanext/dataexplorer/public/vendor/slickgrid/2.2/slick-default-theme.css b/ckanext/dataexplorer/public/vendor/slickgrid/2.2/slick-default-theme.css new file mode 100644 index 0000000..efc7415 --- /dev/null +++ b/ckanext/dataexplorer/public/vendor/slickgrid/2.2/slick-default-theme.css @@ -0,0 +1,118 @@ +/* +IMPORTANT: +In order to preserve the uniform grid appearance, all cell styles need to have padding, margin and border sizes. +No built-in (selected, editable, highlight, flashing, invalid, loading, :focus) or user-specified CSS +classes should alter those! +*/ + +.slick-header-columns { + background: url('images/header-columns-bg.gif') repeat-x center bottom; + border-bottom: 1px solid silver; +} + +.slick-header-column { + background: url('images/header-columns-bg.gif') repeat-x center bottom; + border-right: 1px solid silver; +} + +.slick-header-column:hover, .slick-header-column-active { + background: white url('images/header-columns-over-bg.gif') repeat-x center bottom; +} + +.slick-headerrow { + background: #fafafa; +} + +.slick-headerrow-column { + background: #fafafa; + border-bottom: 0; + height: 100%; +} + +.slick-row.ui-state-active { + background: #F5F7D7; +} + +.slick-row { + position: absolute; + background: white; + border: 0px; + line-height: 20px; +} + +.slick-row.selected { + z-index: 10; + background: #DFE8F6; +} + +.slick-cell { + padding-left: 4px; + padding-right: 4px; +} + +.slick-group { + border-bottom: 2px solid silver; +} + +.slick-group-toggle { + width: 9px; + height: 9px; + margin-right: 5px; +} + +.slick-group-toggle.expanded { + background: url(images/collapse.gif) no-repeat center center; +} + +.slick-group-toggle.collapsed { + background: url(images/expand.gif) no-repeat center center; +} + +.slick-group-totals { + color: gray; + background: white; +} + +.slick-cell.selected { + background-color: beige; +} + +.slick-cell.active { + border-color: gray; + border-style: solid; +} + +.slick-sortable-placeholder { + background: silver !important; +} + +.slick-row.odd { + background: #fafafa; +} + +.slick-row.ui-state-active { + background: #F5F7D7; +} + +.slick-row.loading { + opacity: 0.5; + filter: alpha(opacity = 50); +} + +.slick-cell.invalid { + border-color: red; + -moz-animation-duration: 0.2s; + -webkit-animation-duration: 0.2s; + -moz-animation-name: slickgrid-invalid-hilite; + -webkit-animation-name: slickgrid-invalid-hilite; +} + +@-moz-keyframes slickgrid-invalid-hilite { + from { box-shadow: 0 0 6px red; } + to { box-shadow: none; } +} + +@-webkit-keyframes slickgrid-invalid-hilite { + from { box-shadow: 0 0 6px red; } + to { box-shadow: none; } +} \ No newline at end of file diff --git a/ckanext/dataexplorer/public/vendor/slickgrid/2.2/slick.core.js b/ckanext/dataexplorer/public/vendor/slickgrid/2.2/slick.core.js new file mode 100644 index 0000000..2f097b1 --- /dev/null +++ b/ckanext/dataexplorer/public/vendor/slickgrid/2.2/slick.core.js @@ -0,0 +1,467 @@ +/*** + * Contains core SlickGrid classes. + * @module Core + * @namespace Slick + */ + +(function ($) { + // register namespace + $.extend(true, window, { + "Slick": { + "Event": Event, + "EventData": EventData, + "EventHandler": EventHandler, + "Range": Range, + "NonDataRow": NonDataItem, + "Group": Group, + "GroupTotals": GroupTotals, + "EditorLock": EditorLock, + + /*** + * A global singleton editor lock. + * @class GlobalEditorLock + * @static + * @constructor + */ + "GlobalEditorLock": new EditorLock() + } + }); + + /*** + * An event object for passing data to event handlers and letting them control propagation. + *

This is pretty much identical to how W3C and jQuery implement events.

+ * @class EventData + * @constructor + */ + function EventData() { + var isPropagationStopped = false; + var isImmediatePropagationStopped = false; + + /*** + * Stops event from propagating up the DOM tree. + * @method stopPropagation + */ + this.stopPropagation = function () { + isPropagationStopped = true; + }; + + /*** + * Returns whether stopPropagation was called on this event object. + * @method isPropagationStopped + * @return {Boolean} + */ + this.isPropagationStopped = function () { + return isPropagationStopped; + }; + + /*** + * Prevents the rest of the handlers from being executed. + * @method stopImmediatePropagation + */ + this.stopImmediatePropagation = function () { + isImmediatePropagationStopped = true; + }; + + /*** + * Returns whether stopImmediatePropagation was called on this event object.\ + * @method isImmediatePropagationStopped + * @return {Boolean} + */ + this.isImmediatePropagationStopped = function () { + return isImmediatePropagationStopped; + } + } + + /*** + * A simple publisher-subscriber implementation. + * @class Event + * @constructor + */ + function Event() { + var handlers = []; + + /*** + * Adds an event handler to be called when the event is fired. + *

Event handler will receive two arguments - an EventData and the data + * object the event was fired with.

+ * @method subscribe + * @param fn {Function} Event handler. + */ + this.subscribe = function (fn) { + handlers.push(fn); + }; + + /*** + * Removes an event handler added with subscribe(fn). + * @method unsubscribe + * @param fn {Function} Event handler to be removed. + */ + this.unsubscribe = function (fn) { + for (var i = handlers.length - 1; i >= 0; i--) { + if (handlers[i] === fn) { + handlers.splice(i, 1); + } + } + }; + + /*** + * Fires an event notifying all subscribers. + * @method notify + * @param args {Object} Additional data object to be passed to all handlers. + * @param e {EventData} + * Optional. + * An EventData object to be passed to all handlers. + * For DOM events, an existing W3C/jQuery event object can be passed in. + * @param scope {Object} + * Optional. + * The scope ("this") within which the handler will be executed. + * If not specified, the scope will be set to the Event instance. + */ + this.notify = function (args, e, scope) { + e = e || new EventData(); + scope = scope || this; + + var returnValue; + for (var i = 0; i < handlers.length && !(e.isPropagationStopped() || e.isImmediatePropagationStopped()); i++) { + returnValue = handlers[i].call(scope, e, args); + } + + return returnValue; + }; + } + + function EventHandler() { + var handlers = []; + + this.subscribe = function (event, handler) { + handlers.push({ + event: event, + handler: handler + }); + event.subscribe(handler); + + return this; // allow chaining + }; + + this.unsubscribe = function (event, handler) { + var i = handlers.length; + while (i--) { + if (handlers[i].event === event && + handlers[i].handler === handler) { + handlers.splice(i, 1); + event.unsubscribe(handler); + return; + } + } + + return this; // allow chaining + }; + + this.unsubscribeAll = function () { + var i = handlers.length; + while (i--) { + handlers[i].event.unsubscribe(handlers[i].handler); + } + handlers = []; + + return this; // allow chaining + } + } + + /*** + * A structure containing a range of cells. + * @class Range + * @constructor + * @param fromRow {Integer} Starting row. + * @param fromCell {Integer} Starting cell. + * @param toRow {Integer} Optional. Ending row. Defaults to fromRow. + * @param toCell {Integer} Optional. Ending cell. Defaults to fromCell. + */ + function Range(fromRow, fromCell, toRow, toCell) { + if (toRow === undefined && toCell === undefined) { + toRow = fromRow; + toCell = fromCell; + } + + /*** + * @property fromRow + * @type {Integer} + */ + this.fromRow = Math.min(fromRow, toRow); + + /*** + * @property fromCell + * @type {Integer} + */ + this.fromCell = Math.min(fromCell, toCell); + + /*** + * @property toRow + * @type {Integer} + */ + this.toRow = Math.max(fromRow, toRow); + + /*** + * @property toCell + * @type {Integer} + */ + this.toCell = Math.max(fromCell, toCell); + + /*** + * Returns whether a range represents a single row. + * @method isSingleRow + * @return {Boolean} + */ + this.isSingleRow = function () { + return this.fromRow == this.toRow; + }; + + /*** + * Returns whether a range represents a single cell. + * @method isSingleCell + * @return {Boolean} + */ + this.isSingleCell = function () { + return this.fromRow == this.toRow && this.fromCell == this.toCell; + }; + + /*** + * Returns whether a range contains a given cell. + * @method contains + * @param row {Integer} + * @param cell {Integer} + * @return {Boolean} + */ + this.contains = function (row, cell) { + return row >= this.fromRow && row <= this.toRow && + cell >= this.fromCell && cell <= this.toCell; + }; + + /*** + * Returns a readable representation of a range. + * @method toString + * @return {String} + */ + this.toString = function () { + if (this.isSingleCell()) { + return "(" + this.fromRow + ":" + this.fromCell + ")"; + } + else { + return "(" + this.fromRow + ":" + this.fromCell + " - " + this.toRow + ":" + this.toCell + ")"; + } + } + } + + + /*** + * A base class that all special / non-data rows (like Group and GroupTotals) derive from. + * @class NonDataItem + * @constructor + */ + function NonDataItem() { + this.__nonDataRow = true; + } + + + /*** + * Information about a group of rows. + * @class Group + * @extends Slick.NonDataItem + * @constructor + */ + function Group() { + this.__group = true; + + /** + * Grouping level, starting with 0. + * @property level + * @type {Number} + */ + this.level = 0; + + /*** + * Number of rows in the group. + * @property count + * @type {Integer} + */ + this.count = 0; + + /*** + * Grouping value. + * @property value + * @type {Object} + */ + this.value = null; + + /*** + * Formatted display value of the group. + * @property title + * @type {String} + */ + this.title = null; + + /*** + * Whether a group is collapsed. + * @property collapsed + * @type {Boolean} + */ + this.collapsed = false; + + /*** + * GroupTotals, if any. + * @property totals + * @type {GroupTotals} + */ + this.totals = null; + + /** + * Rows that are part of the group. + * @property rows + * @type {Array} + */ + this.rows = []; + + /** + * Sub-groups that are part of the group. + * @property groups + * @type {Array} + */ + this.groups = null; + + /** + * A unique key used to identify the group. This key can be used in calls to DataView + * collapseGroup() or expandGroup(). + * @property groupingKey + * @type {Object} + */ + this.groupingKey = null; + } + + Group.prototype = new NonDataItem(); + + /*** + * Compares two Group instances. + * @method equals + * @return {Boolean} + * @param group {Group} Group instance to compare to. + */ + Group.prototype.equals = function (group) { + return this.value === group.value && + this.count === group.count && + this.collapsed === group.collapsed && + this.title === group.title; + }; + + /*** + * Information about group totals. + * An instance of GroupTotals will be created for each totals row and passed to the aggregators + * so that they can store arbitrary data in it. That data can later be accessed by group totals + * formatters during the display. + * @class GroupTotals + * @extends Slick.NonDataItem + * @constructor + */ + function GroupTotals() { + this.__groupTotals = true; + + /*** + * Parent Group. + * @param group + * @type {Group} + */ + this.group = null; + + /*** + * Whether the totals have been fully initialized / calculated. + * Will be set to false for lazy-calculated group totals. + * @param initialized + * @type {Boolean} + */ + this.initialized = false; + } + + GroupTotals.prototype = new NonDataItem(); + + /*** + * A locking helper to track the active edit controller and ensure that only a single controller + * can be active at a time. This prevents a whole class of state and validation synchronization + * issues. An edit controller (such as SlickGrid) can query if an active edit is in progress + * and attempt a commit or cancel before proceeding. + * @class EditorLock + * @constructor + */ + function EditorLock() { + var activeEditController = null; + + /*** + * Returns true if a specified edit controller is active (has the edit lock). + * If the parameter is not specified, returns true if any edit controller is active. + * @method isActive + * @param editController {EditController} + * @return {Boolean} + */ + this.isActive = function (editController) { + return (editController ? activeEditController === editController : activeEditController !== null); + }; + + /*** + * Sets the specified edit controller as the active edit controller (acquire edit lock). + * If another edit controller is already active, and exception will be thrown. + * @method activate + * @param editController {EditController} edit controller acquiring the lock + */ + this.activate = function (editController) { + if (editController === activeEditController) { // already activated? + return; + } + if (activeEditController !== null) { + throw "SlickGrid.EditorLock.activate: an editController is still active, can't activate another editController"; + } + if (!editController.commitCurrentEdit) { + throw "SlickGrid.EditorLock.activate: editController must implement .commitCurrentEdit()"; + } + if (!editController.cancelCurrentEdit) { + throw "SlickGrid.EditorLock.activate: editController must implement .cancelCurrentEdit()"; + } + activeEditController = editController; + }; + + /*** + * Unsets the specified edit controller as the active edit controller (release edit lock). + * If the specified edit controller is not the active one, an exception will be thrown. + * @method deactivate + * @param editController {EditController} edit controller releasing the lock + */ + this.deactivate = function (editController) { + if (activeEditController !== editController) { + throw "SlickGrid.EditorLock.deactivate: specified editController is not the currently active one"; + } + activeEditController = null; + }; + + /*** + * Attempts to commit the current edit by calling "commitCurrentEdit" method on the active edit + * controller and returns whether the commit attempt was successful (commit may fail due to validation + * errors, etc.). Edit controller's "commitCurrentEdit" must return true if the commit has succeeded + * and false otherwise. If no edit controller is active, returns true. + * @method commitCurrentEdit + * @return {Boolean} + */ + this.commitCurrentEdit = function () { + return (activeEditController ? activeEditController.commitCurrentEdit() : true); + }; + + /*** + * Attempts to cancel the current edit by calling "cancelCurrentEdit" method on the active edit + * controller and returns whether the edit was successfully cancelled. If no edit controller is + * active, returns true. + * @method cancelCurrentEdit + * @return {Boolean} + */ + this.cancelCurrentEdit = function cancelCurrentEdit() { + return (activeEditController ? activeEditController.cancelCurrentEdit() : true); + }; + } +})(jQuery); + + diff --git a/ckanext/dataexplorer/public/vendor/slickgrid/2.2/slick.dataview.js b/ckanext/dataexplorer/public/vendor/slickgrid/2.2/slick.dataview.js new file mode 100644 index 0000000..f1c1b5e --- /dev/null +++ b/ckanext/dataexplorer/public/vendor/slickgrid/2.2/slick.dataview.js @@ -0,0 +1,1126 @@ +(function ($) { + $.extend(true, window, { + Slick: { + Data: { + DataView: DataView, + Aggregators: { + Avg: AvgAggregator, + Min: MinAggregator, + Max: MaxAggregator, + Sum: SumAggregator + } + } + } + }); + + + /*** + * A sample Model implementation. + * Provides a filtered view of the underlying data. + * + * Relies on the data item having an "id" property uniquely identifying it. + */ + function DataView(options) { + var self = this; + + var defaults = { + groupItemMetadataProvider: null, + inlineFilters: false + }; + + + // private + var idProperty = "id"; // property holding a unique row id + var items = []; // data by index + var rows = []; // data by row + var idxById = {}; // indexes by id + var rowsById = null; // rows by id; lazy-calculated + var filter = null; // filter function + var updated = null; // updated item ids + var suspend = false; // suspends the recalculation + var sortAsc = true; + var fastSortField; + var sortComparer; + var refreshHints = {}; + var prevRefreshHints = {}; + var filterArgs; + var filteredItems = []; + var compiledFilter; + var compiledFilterWithCaching; + var filterCache = []; + + // grouping + var groupingInfoDefaults = { + getter: null, + formatter: null, + comparer: function(a, b) { return a.value - b.value; }, + predefinedValues: [], + aggregators: [], + aggregateEmpty: false, + aggregateCollapsed: false, + aggregateChildGroups: false, + collapsed: false, + displayTotalsRow: true, + lazyTotalsCalculation: false + }; + var groupingInfos = []; + var groups = []; + var toggledGroupsByLevel = []; + var groupingDelimiter = ':|:'; + + var pagesize = 0; + var pagenum = 0; + var totalRows = 0; + + // events + var onRowCountChanged = new Slick.Event(); + var onRowsChanged = new Slick.Event(); + var onPagingInfoChanged = new Slick.Event(); + + options = $.extend(true, {}, defaults, options); + + + function beginUpdate() { + suspend = true; + } + + function endUpdate() { + suspend = false; + refresh(); + } + + function setRefreshHints(hints) { + refreshHints = hints; + } + + function setFilterArgs(args) { + filterArgs = args; + } + + function updateIdxById(startingIndex) { + startingIndex = startingIndex || 0; + var id; + for (var i = startingIndex, l = items.length; i < l; i++) { + id = items[i][idProperty]; + if (id === undefined) { + throw "Each data element must implement a unique 'id' property"; + } + idxById[id] = i; + } + } + + function ensureIdUniqueness() { + var id; + for (var i = 0, l = items.length; i < l; i++) { + id = items[i][idProperty]; + if (id === undefined || idxById[id] !== i) { + throw "Each data element must implement a unique 'id' property"; + } + } + } + + function getItems() { + return items; + } + + function setItems(data, objectIdProperty) { + if (objectIdProperty !== undefined) { + idProperty = objectIdProperty; + } + items = filteredItems = data; + idxById = {}; + updateIdxById(); + ensureIdUniqueness(); + refresh(); + } + + function setPagingOptions(args) { + if (args.pageSize != undefined) { + pagesize = args.pageSize; + pagenum = pagesize ? Math.min(pagenum, Math.max(0, Math.ceil(totalRows / pagesize) - 1)) : 0; + } + + if (args.pageNum != undefined) { + pagenum = Math.min(args.pageNum, Math.max(0, Math.ceil(totalRows / pagesize) - 1)); + } + + onPagingInfoChanged.notify(getPagingInfo(), null, self); + + refresh(); + } + + function getPagingInfo() { + var totalPages = pagesize ? Math.max(1, Math.ceil(totalRows / pagesize)) : 1; + return {pageSize: pagesize, pageNum: pagenum, totalRows: totalRows, totalPages: totalPages}; + } + + function sort(comparer, ascending) { + sortAsc = ascending; + sortComparer = comparer; + fastSortField = null; + if (ascending === false) { + items.reverse(); + } + items.sort(comparer); + if (ascending === false) { + items.reverse(); + } + idxById = {}; + updateIdxById(); + refresh(); + } + + /*** + * Provides a workaround for the extremely slow sorting in IE. + * Does a [lexicographic] sort on a give column by temporarily overriding Object.prototype.toString + * to return the value of that field and then doing a native Array.sort(). + */ + function fastSort(field, ascending) { + sortAsc = ascending; + fastSortField = field; + sortComparer = null; + var oldToString = Object.prototype.toString; + Object.prototype.toString = (typeof field == "function") ? field : function () { + return this[field] + }; + // an extra reversal for descending sort keeps the sort stable + // (assuming a stable native sort implementation, which isn't true in some cases) + if (ascending === false) { + items.reverse(); + } + items.sort(); + Object.prototype.toString = oldToString; + if (ascending === false) { + items.reverse(); + } + idxById = {}; + updateIdxById(); + refresh(); + } + + function reSort() { + if (sortComparer) { + sort(sortComparer, sortAsc); + } else if (fastSortField) { + fastSort(fastSortField, sortAsc); + } + } + + function setFilter(filterFn) { + filter = filterFn; + if (options.inlineFilters) { + compiledFilter = compileFilter(); + compiledFilterWithCaching = compileFilterWithCaching(); + } + refresh(); + } + + function getGrouping() { + return groupingInfos; + } + + function setGrouping(groupingInfo) { + if (!options.groupItemMetadataProvider) { + options.groupItemMetadataProvider = new Slick.Data.GroupItemMetadataProvider(); + } + + groups = []; + toggledGroupsByLevel = []; + groupingInfo = groupingInfo || []; + groupingInfos = (groupingInfo instanceof Array) ? groupingInfo : [groupingInfo]; + + for (var i = 0; i < groupingInfos.length; i++) { + var gi = groupingInfos[i] = $.extend(true, {}, groupingInfoDefaults, groupingInfos[i]); + gi.getterIsAFn = typeof gi.getter === "function"; + + // pre-compile accumulator loops + gi.compiledAccumulators = []; + var idx = gi.aggregators.length; + while (idx--) { + gi.compiledAccumulators[idx] = compileAccumulatorLoop(gi.aggregators[idx]); + } + + toggledGroupsByLevel[i] = {}; + } + + refresh(); + } + + /** + * @deprecated Please use {@link setGrouping}. + */ + function groupBy(valueGetter, valueFormatter, sortComparer) { + if (valueGetter == null) { + setGrouping([]); + return; + } + + setGrouping({ + getter: valueGetter, + formatter: valueFormatter, + comparer: sortComparer + }); + } + + /** + * @deprecated Please use {@link setGrouping}. + */ + function setAggregators(groupAggregators, includeCollapsed) { + if (!groupingInfos.length) { + throw new Error("At least one grouping must be specified before calling setAggregators()."); + } + + groupingInfos[0].aggregators = groupAggregators; + groupingInfos[0].aggregateCollapsed = includeCollapsed; + + setGrouping(groupingInfos); + } + + function getItemByIdx(i) { + return items[i]; + } + + function getIdxById(id) { + return idxById[id]; + } + + function ensureRowsByIdCache() { + if (!rowsById) { + rowsById = {}; + for (var i = 0, l = rows.length; i < l; i++) { + rowsById[rows[i][idProperty]] = i; + } + } + } + + function getRowById(id) { + ensureRowsByIdCache(); + return rowsById[id]; + } + + function getItemById(id) { + return items[idxById[id]]; + } + + function mapIdsToRows(idArray) { + var rows = []; + ensureRowsByIdCache(); + for (var i = 0, l = idArray.length; i < l; i++) { + var row = rowsById[idArray[i]]; + if (row != null) { + rows[rows.length] = row; + } + } + return rows; + } + + function mapRowsToIds(rowArray) { + var ids = []; + for (var i = 0, l = rowArray.length; i < l; i++) { + if (rowArray[i] < rows.length) { + ids[ids.length] = rows[rowArray[i]][idProperty]; + } + } + return ids; + } + + function updateItem(id, item) { + if (idxById[id] === undefined || id !== item[idProperty]) { + throw "Invalid or non-matching id"; + } + items[idxById[id]] = item; + if (!updated) { + updated = {}; + } + updated[id] = true; + refresh(); + } + + function insertItem(insertBefore, item) { + items.splice(insertBefore, 0, item); + updateIdxById(insertBefore); + refresh(); + } + + function addItem(item) { + items.push(item); + updateIdxById(items.length - 1); + refresh(); + } + + function deleteItem(id) { + var idx = idxById[id]; + if (idx === undefined) { + throw "Invalid id"; + } + delete idxById[id]; + items.splice(idx, 1); + updateIdxById(idx); + refresh(); + } + + function getLength() { + return rows.length; + } + + function getItem(i) { + var item = rows[i]; + + // if this is a group row, make sure totals are calculated and update the title + if (item && item.__group && item.totals && !item.totals.initialized) { + var gi = groupingInfos[item.level]; + if (!gi.displayTotalsRow) { + calculateTotals(item.totals); + item.title = gi.formatter ? gi.formatter(item) : item.value; + } + } + // if this is a totals row, make sure it's calculated + else if (item && item.__groupTotals && !item.initialized) { + calculateTotals(item); + } + + return item; + } + + function getItemMetadata(i) { + var item = rows[i]; + if (item === undefined) { + return null; + } + + // overrides for grouping rows + if (item.__group) { + return options.groupItemMetadataProvider.getGroupRowMetadata(item); + } + + // overrides for totals rows + if (item.__groupTotals) { + return options.groupItemMetadataProvider.getTotalsRowMetadata(item); + } + + return null; + } + + function expandCollapseAllGroups(level, collapse) { + if (level == null) { + for (var i = 0; i < groupingInfos.length; i++) { + toggledGroupsByLevel[i] = {}; + groupingInfos[i].collapsed = collapse; + } + } else { + toggledGroupsByLevel[level] = {}; + groupingInfos[level].collapsed = collapse; + } + refresh(); + } + + /** + * @param level {Number} Optional level to collapse. If not specified, applies to all levels. + */ + function collapseAllGroups(level) { + expandCollapseAllGroups(level, true); + } + + /** + * @param level {Number} Optional level to expand. If not specified, applies to all levels. + */ + function expandAllGroups(level) { + expandCollapseAllGroups(level, false); + } + + function expandCollapseGroup(level, groupingKey, collapse) { + toggledGroupsByLevel[level][groupingKey] = groupingInfos[level].collapsed ^ collapse; + refresh(); + } + + /** + * @param varArgs Either a Slick.Group's "groupingKey" property, or a + * variable argument list of grouping values denoting a unique path to the row. For + * example, calling collapseGroup('high', '10%') will collapse the '10%' subgroup of + * the 'high' group. + */ + function collapseGroup(varArgs) { + var args = Array.prototype.slice.call(arguments); + var arg0 = args[0]; + if (args.length == 1 && arg0.indexOf(groupingDelimiter) != -1) { + expandCollapseGroup(arg0.split(groupingDelimiter).length - 1, arg0, true); + } else { + expandCollapseGroup(args.length - 1, args.join(groupingDelimiter), true); + } + } + + /** + * @param varArgs Either a Slick.Group's "groupingKey" property, or a + * variable argument list of grouping values denoting a unique path to the row. For + * example, calling expandGroup('high', '10%') will expand the '10%' subgroup of + * the 'high' group. + */ + function expandGroup(varArgs) { + var args = Array.prototype.slice.call(arguments); + var arg0 = args[0]; + if (args.length == 1 && arg0.indexOf(groupingDelimiter) != -1) { + expandCollapseGroup(arg0.split(groupingDelimiter).length - 1, arg0, false); + } else { + expandCollapseGroup(args.length - 1, args.join(groupingDelimiter), false); + } + } + + function getGroups() { + return groups; + } + + function extractGroups(rows, parentGroup) { + var group; + var val; + var groups = []; + var groupsByVal = {}; + var r; + var level = parentGroup ? parentGroup.level + 1 : 0; + var gi = groupingInfos[level]; + + for (var i = 0, l = gi.predefinedValues.length; i < l; i++) { + val = gi.predefinedValues[i]; + group = groupsByVal[val]; + if (!group) { + group = new Slick.Group(); + group.value = val; + group.level = level; + group.groupingKey = (parentGroup ? parentGroup.groupingKey + groupingDelimiter : '') + val; + groups[groups.length] = group; + groupsByVal[val] = group; + } + } + + for (var i = 0, l = rows.length; i < l; i++) { + r = rows[i]; + val = gi.getterIsAFn ? gi.getter(r) : r[gi.getter]; + group = groupsByVal[val]; + if (!group) { + group = new Slick.Group(); + group.value = val; + group.level = level; + group.groupingKey = (parentGroup ? parentGroup.groupingKey + groupingDelimiter : '') + val; + groups[groups.length] = group; + groupsByVal[val] = group; + } + + group.rows[group.count++] = r; + } + + if (level < groupingInfos.length - 1) { + for (var i = 0; i < groups.length; i++) { + group = groups[i]; + group.groups = extractGroups(group.rows, group); + } + } + + groups.sort(groupingInfos[level].comparer); + + return groups; + } + + function calculateTotals(totals) { + var group = totals.group; + var gi = groupingInfos[group.level]; + var isLeafLevel = (group.level == groupingInfos.length); + var agg, idx = gi.aggregators.length; + + if (!isLeafLevel && gi.aggregateChildGroups) { + // make sure all the subgroups are calculated + var i = group.groups.length; + while (i--) { + if (!group.groups[i].initialized) { + calculateTotals(group.groups[i]); + } + } + } + + while (idx--) { + agg = gi.aggregators[idx]; + agg.init(); + if (!isLeafLevel && gi.aggregateChildGroups) { + gi.compiledAccumulators[idx].call(agg, group.groups); + } else { + gi.compiledAccumulators[idx].call(agg, group.rows); + } + agg.storeResult(totals); + } + totals.initialized = true; + } + + function addGroupTotals(group) { + var gi = groupingInfos[group.level]; + var totals = new Slick.GroupTotals(); + totals.group = group; + group.totals = totals; + if (!gi.lazyTotalsCalculation) { + calculateTotals(totals); + } + } + + function addTotals(groups, level) { + level = level || 0; + var gi = groupingInfos[level]; + var groupCollapsed = gi.collapsed; + var toggledGroups = toggledGroupsByLevel[level]; + var idx = groups.length, g; + while (idx--) { + g = groups[idx]; + + if (g.collapsed && !gi.aggregateCollapsed) { + continue; + } + + // Do a depth-first aggregation so that parent group aggregators can access subgroup totals. + if (g.groups) { + addTotals(g.groups, level + 1); + } + + if (gi.aggregators.length && ( + gi.aggregateEmpty || g.rows.length || (g.groups && g.groups.length))) { + addGroupTotals(g); + } + + g.collapsed = groupCollapsed ^ toggledGroups[g.groupingKey]; + g.title = gi.formatter ? gi.formatter(g) : g.value; + } + } + + function flattenGroupedRows(groups, level) { + level = level || 0; + var gi = groupingInfos[level]; + var groupedRows = [], rows, gl = 0, g; + for (var i = 0, l = groups.length; i < l; i++) { + g = groups[i]; + groupedRows[gl++] = g; + + if (!g.collapsed) { + rows = g.groups ? flattenGroupedRows(g.groups, level + 1) : g.rows; + for (var j = 0, jj = rows.length; j < jj; j++) { + groupedRows[gl++] = rows[j]; + } + } + + if (g.totals && gi.displayTotalsRow && (!g.collapsed || gi.aggregateCollapsed)) { + groupedRows[gl++] = g.totals; + } + } + return groupedRows; + } + + function getFunctionInfo(fn) { + var fnRegex = /^function[^(]*\(([^)]*)\)\s*{([\s\S]*)}$/; + var matches = fn.toString().match(fnRegex); + return { + params: matches[1].split(","), + body: matches[2] + }; + } + + function compileAccumulatorLoop(aggregator) { + var accumulatorInfo = getFunctionInfo(aggregator.accumulate); + var fn = new Function( + "_items", + "for (var " + accumulatorInfo.params[0] + ", _i=0, _il=_items.length; _i<_il; _i++) {" + + accumulatorInfo.params[0] + " = _items[_i]; " + + accumulatorInfo.body + + "}" + ); + fn.displayName = fn.name = "compiledAccumulatorLoop"; + return fn; + } + + function compileFilter() { + var filterInfo = getFunctionInfo(filter); + + var filterBody = filterInfo.body + .replace(/return false\s*([;}]|$)/gi, "{ continue _coreloop; }$1") + .replace(/return true\s*([;}]|$)/gi, "{ _retval[_idx++] = $item$; continue _coreloop; }$1") + .replace(/return ([^;}]+?)\s*([;}]|$)/gi, + "{ if ($1) { _retval[_idx++] = $item$; }; continue _coreloop; }$2"); + + // This preserves the function template code after JS compression, + // so that replace() commands still work as expected. + var tpl = [ + //"function(_items, _args) { ", + "var _retval = [], _idx = 0; ", + "var $item$, $args$ = _args; ", + "_coreloop: ", + "for (var _i = 0, _il = _items.length; _i < _il; _i++) { ", + "$item$ = _items[_i]; ", + "$filter$; ", + "} ", + "return _retval; " + //"}" + ].join(""); + tpl = tpl.replace(/\$filter\$/gi, filterBody); + tpl = tpl.replace(/\$item\$/gi, filterInfo.params[0]); + tpl = tpl.replace(/\$args\$/gi, filterInfo.params[1]); + + var fn = new Function("_items,_args", tpl); + fn.displayName = fn.name = "compiledFilter"; + return fn; + } + + function compileFilterWithCaching() { + var filterInfo = getFunctionInfo(filter); + + var filterBody = filterInfo.body + .replace(/return false\s*([;}]|$)/gi, "{ continue _coreloop; }$1") + .replace(/return true\s*([;}]|$)/gi, "{ _cache[_i] = true;_retval[_idx++] = $item$; continue _coreloop; }$1") + .replace(/return ([^;}]+?)\s*([;}]|$)/gi, + "{ if ((_cache[_i] = $1)) { _retval[_idx++] = $item$; }; continue _coreloop; }$2"); + + // This preserves the function template code after JS compression, + // so that replace() commands still work as expected. + var tpl = [ + //"function(_items, _args, _cache) { ", + "var _retval = [], _idx = 0; ", + "var $item$, $args$ = _args; ", + "_coreloop: ", + "for (var _i = 0, _il = _items.length; _i < _il; _i++) { ", + "$item$ = _items[_i]; ", + "if (_cache[_i]) { ", + "_retval[_idx++] = $item$; ", + "continue _coreloop; ", + "} ", + "$filter$; ", + "} ", + "return _retval; " + //"}" + ].join(""); + tpl = tpl.replace(/\$filter\$/gi, filterBody); + tpl = tpl.replace(/\$item\$/gi, filterInfo.params[0]); + tpl = tpl.replace(/\$args\$/gi, filterInfo.params[1]); + + var fn = new Function("_items,_args,_cache", tpl); + fn.displayName = fn.name = "compiledFilterWithCaching"; + return fn; + } + + function uncompiledFilter(items, args) { + var retval = [], idx = 0; + + for (var i = 0, ii = items.length; i < ii; i++) { + if (filter(items[i], args)) { + retval[idx++] = items[i]; + } + } + + return retval; + } + + function uncompiledFilterWithCaching(items, args, cache) { + var retval = [], idx = 0, item; + + for (var i = 0, ii = items.length; i < ii; i++) { + item = items[i]; + if (cache[i]) { + retval[idx++] = item; + } else if (filter(item, args)) { + retval[idx++] = item; + cache[i] = true; + } + } + + return retval; + } + + function getFilteredAndPagedItems(items) { + if (filter) { + var batchFilter = options.inlineFilters ? compiledFilter : uncompiledFilter; + var batchFilterWithCaching = options.inlineFilters ? compiledFilterWithCaching : uncompiledFilterWithCaching; + + if (refreshHints.isFilterNarrowing) { + filteredItems = batchFilter(filteredItems, filterArgs); + } else if (refreshHints.isFilterExpanding) { + filteredItems = batchFilterWithCaching(items, filterArgs, filterCache); + } else if (!refreshHints.isFilterUnchanged) { + filteredItems = batchFilter(items, filterArgs); + } + } else { + // special case: if not filtering and not paging, the resulting + // rows collection needs to be a copy so that changes due to sort + // can be caught + filteredItems = pagesize ? items : items.concat(); + } + + // get the current page + var paged; + if (pagesize) { + if (filteredItems.length < pagenum * pagesize) { + pagenum = Math.floor(filteredItems.length / pagesize); + } + paged = filteredItems.slice(pagesize * pagenum, pagesize * pagenum + pagesize); + } else { + paged = filteredItems; + } + + return {totalRows: filteredItems.length, rows: paged}; + } + + function getRowDiffs(rows, newRows) { + var item, r, eitherIsNonData, diff = []; + var from = 0, to = newRows.length; + + if (refreshHints && refreshHints.ignoreDiffsBefore) { + from = Math.max(0, + Math.min(newRows.length, refreshHints.ignoreDiffsBefore)); + } + + if (refreshHints && refreshHints.ignoreDiffsAfter) { + to = Math.min(newRows.length, + Math.max(0, refreshHints.ignoreDiffsAfter)); + } + + for (var i = from, rl = rows.length; i < to; i++) { + if (i >= rl) { + diff[diff.length] = i; + } else { + item = newRows[i]; + r = rows[i]; + + if ((groupingInfos.length && (eitherIsNonData = (item.__nonDataRow) || (r.__nonDataRow)) && + item.__group !== r.__group || + item.__group && !item.equals(r)) + || (eitherIsNonData && + // no good way to compare totals since they are arbitrary DTOs + // deep object comparison is pretty expensive + // always considering them 'dirty' seems easier for the time being + (item.__groupTotals || r.__groupTotals)) + || item[idProperty] != r[idProperty] + || (updated && updated[item[idProperty]]) + ) { + diff[diff.length] = i; + } + } + } + return diff; + } + + function recalc(_items) { + rowsById = null; + + if (refreshHints.isFilterNarrowing != prevRefreshHints.isFilterNarrowing || + refreshHints.isFilterExpanding != prevRefreshHints.isFilterExpanding) { + filterCache = []; + } + + var filteredItems = getFilteredAndPagedItems(_items); + totalRows = filteredItems.totalRows; + var newRows = filteredItems.rows; + + groups = []; + if (groupingInfos.length) { + groups = extractGroups(newRows); + if (groups.length) { + addTotals(groups); + newRows = flattenGroupedRows(groups); + } + } + + var diff = getRowDiffs(rows, newRows); + + rows = newRows; + + return diff; + } + + function refresh() { + if (suspend) { + return; + } + + var countBefore = rows.length; + var totalRowsBefore = totalRows; + + var diff = recalc(items, filter); // pass as direct refs to avoid closure perf hit + + // if the current page is no longer valid, go to last page and recalc + // we suffer a performance penalty here, but the main loop (recalc) remains highly optimized + if (pagesize && totalRows < pagenum * pagesize) { + pagenum = Math.max(0, Math.ceil(totalRows / pagesize) - 1); + diff = recalc(items, filter); + } + + updated = null; + prevRefreshHints = refreshHints; + refreshHints = {}; + + if (totalRowsBefore != totalRows) { + onPagingInfoChanged.notify(getPagingInfo(), null, self); + } + if (countBefore != rows.length) { + onRowCountChanged.notify({previous: countBefore, current: rows.length}, null, self); + } + if (diff.length > 0) { + onRowsChanged.notify({rows: diff}, null, self); + } + } + + /*** + * Wires the grid and the DataView together to keep row selection tied to item ids. + * This is useful since, without it, the grid only knows about rows, so if the items + * move around, the same rows stay selected instead of the selection moving along + * with the items. + * + * NOTE: This doesn't work with cell selection model. + * + * @param grid {Slick.Grid} The grid to sync selection with. + * @param preserveHidden {Boolean} Whether to keep selected items that go out of the + * view due to them getting filtered out. + * @param preserveHiddenOnSelectionChange {Boolean} Whether to keep selected items + * that are currently out of the view (see preserveHidden) as selected when selection + * changes. + * @return {Slick.Event} An event that notifies when an internal list of selected row ids + * changes. This is useful since, in combination with the above two options, it allows + * access to the full list selected row ids, and not just the ones visible to the grid. + * @method syncGridSelection + */ + function syncGridSelection(grid, preserveHidden, preserveHiddenOnSelectionChange) { + var self = this; + var inHandler; + var selectedRowIds = self.mapRowsToIds(grid.getSelectedRows()); + var onSelectedRowIdsChanged = new Slick.Event(); + + function setSelectedRowIds(rowIds) { + if (selectedRowIds.join(",") == rowIds.join(",")) { + return; + } + + selectedRowIds = rowIds; + + onSelectedRowIdsChanged.notify({ + "grid": grid, + "ids": selectedRowIds + }, new Slick.EventData(), self); + } + + function update() { + if (selectedRowIds.length > 0) { + inHandler = true; + var selectedRows = self.mapIdsToRows(selectedRowIds); + if (!preserveHidden) { + setSelectedRowIds(self.mapRowsToIds(selectedRows)); + } + grid.setSelectedRows(selectedRows); + inHandler = false; + } + } + + grid.onSelectedRowsChanged.subscribe(function(e, args) { + if (inHandler) { return; } + var newSelectedRowIds = self.mapRowsToIds(grid.getSelectedRows()); + if (!preserveHiddenOnSelectionChange || !grid.getOptions().multiSelect) { + setSelectedRowIds(newSelectedRowIds); + } else { + // keep the ones that are hidden + var existing = $.grep(selectedRowIds, function(id) { return self.getRowById(id) === undefined; }); + // add the newly selected ones + setSelectedRowIds(existing.concat(newSelectedRowIds)); + } + }); + + this.onRowsChanged.subscribe(update); + + this.onRowCountChanged.subscribe(update); + + return onSelectedRowIdsChanged; + } + + function syncGridCellCssStyles(grid, key) { + var hashById; + var inHandler; + + // since this method can be called after the cell styles have been set, + // get the existing ones right away + storeCellCssStyles(grid.getCellCssStyles(key)); + + function storeCellCssStyles(hash) { + hashById = {}; + for (var row in hash) { + var id = rows[row][idProperty]; + hashById[id] = hash[row]; + } + } + + function update() { + if (hashById) { + inHandler = true; + ensureRowsByIdCache(); + var newHash = {}; + for (var id in hashById) { + var row = rowsById[id]; + if (row != undefined) { + newHash[row] = hashById[id]; + } + } + grid.setCellCssStyles(key, newHash); + inHandler = false; + } + } + + grid.onCellCssStylesChanged.subscribe(function(e, args) { + if (inHandler) { return; } + if (key != args.key) { return; } + if (args.hash) { + storeCellCssStyles(args.hash); + } + }); + + this.onRowsChanged.subscribe(update); + + this.onRowCountChanged.subscribe(update); + } + + $.extend(this, { + // methods + "beginUpdate": beginUpdate, + "endUpdate": endUpdate, + "setPagingOptions": setPagingOptions, + "getPagingInfo": getPagingInfo, + "getItems": getItems, + "setItems": setItems, + "setFilter": setFilter, + "sort": sort, + "fastSort": fastSort, + "reSort": reSort, + "setGrouping": setGrouping, + "getGrouping": getGrouping, + "groupBy": groupBy, + "setAggregators": setAggregators, + "collapseAllGroups": collapseAllGroups, + "expandAllGroups": expandAllGroups, + "collapseGroup": collapseGroup, + "expandGroup": expandGroup, + "getGroups": getGroups, + "getIdxById": getIdxById, + "getRowById": getRowById, + "getItemById": getItemById, + "getItemByIdx": getItemByIdx, + "mapRowsToIds": mapRowsToIds, + "mapIdsToRows": mapIdsToRows, + "setRefreshHints": setRefreshHints, + "setFilterArgs": setFilterArgs, + "refresh": refresh, + "updateItem": updateItem, + "insertItem": insertItem, + "addItem": addItem, + "deleteItem": deleteItem, + "syncGridSelection": syncGridSelection, + "syncGridCellCssStyles": syncGridCellCssStyles, + + // data provider methods + "getLength": getLength, + "getItem": getItem, + "getItemMetadata": getItemMetadata, + + // events + "onRowCountChanged": onRowCountChanged, + "onRowsChanged": onRowsChanged, + "onPagingInfoChanged": onPagingInfoChanged + }); + } + + function AvgAggregator(field) { + this.field_ = field; + + this.init = function () { + this.count_ = 0; + this.nonNullCount_ = 0; + this.sum_ = 0; + }; + + this.accumulate = function (item) { + var val = item[this.field_]; + this.count_++; + if (val != null && val !== "" && val !== NaN) { + this.nonNullCount_++; + this.sum_ += parseFloat(val); + } + }; + + this.storeResult = function (groupTotals) { + if (!groupTotals.avg) { + groupTotals.avg = {}; + } + if (this.nonNullCount_ != 0) { + groupTotals.avg[this.field_] = this.sum_ / this.nonNullCount_; + } + }; + } + + function MinAggregator(field) { + this.field_ = field; + + this.init = function () { + this.min_ = null; + }; + + this.accumulate = function (item) { + var val = item[this.field_]; + if (val != null && val !== "" && val !== NaN) { + if (this.min_ == null || val < this.min_) { + this.min_ = val; + } + } + }; + + this.storeResult = function (groupTotals) { + if (!groupTotals.min) { + groupTotals.min = {}; + } + groupTotals.min[this.field_] = this.min_; + } + } + + function MaxAggregator(field) { + this.field_ = field; + + this.init = function () { + this.max_ = null; + }; + + this.accumulate = function (item) { + var val = item[this.field_]; + if (val != null && val !== "" && val !== NaN) { + if (this.max_ == null || val > this.max_) { + this.max_ = val; + } + } + }; + + this.storeResult = function (groupTotals) { + if (!groupTotals.max) { + groupTotals.max = {}; + } + groupTotals.max[this.field_] = this.max_; + } + } + + function SumAggregator(field) { + this.field_ = field; + + this.init = function () { + this.sum_ = null; + }; + + this.accumulate = function (item) { + var val = item[this.field_]; + if (val != null && val !== "" && val !== NaN) { + this.sum_ += parseFloat(val); + } + }; + + this.storeResult = function (groupTotals) { + if (!groupTotals.sum) { + groupTotals.sum = {}; + } + groupTotals.sum[this.field_] = this.sum_; + } + } + + // TODO: add more built-in aggregators + // TODO: merge common aggregators in one to prevent needles iterating + +})(jQuery); diff --git a/ckanext/dataexplorer/public/vendor/slickgrid/2.2/slick.editors.js b/ckanext/dataexplorer/public/vendor/slickgrid/2.2/slick.editors.js new file mode 100644 index 0000000..04b20d2 --- /dev/null +++ b/ckanext/dataexplorer/public/vendor/slickgrid/2.2/slick.editors.js @@ -0,0 +1,512 @@ +/*** + * Contains basic SlickGrid editors. + * @module Editors + * @namespace Slick + */ + +(function ($) { + // register namespace + $.extend(true, window, { + "Slick": { + "Editors": { + "Text": TextEditor, + "Integer": IntegerEditor, + "Date": DateEditor, + "YesNoSelect": YesNoSelectEditor, + "Checkbox": CheckboxEditor, + "PercentComplete": PercentCompleteEditor, + "LongText": LongTextEditor + } + } + }); + + function TextEditor(args) { + var $input; + var defaultValue; + var scope = this; + + this.init = function () { + $input = $("") + .appendTo(args.container) + .bind("keydown.nav", function (e) { + if (e.keyCode === $.ui.keyCode.LEFT || e.keyCode === $.ui.keyCode.RIGHT) { + e.stopImmediatePropagation(); + } + }) + .focus() + .select(); + }; + + this.destroy = function () { + $input.remove(); + }; + + this.focus = function () { + $input.focus(); + }; + + this.getValue = function () { + return $input.val(); + }; + + this.setValue = function (val) { + $input.val(val); + }; + + this.loadValue = function (item) { + defaultValue = item[args.column.field] || ""; + $input.val(defaultValue); + $input[0].defaultValue = defaultValue; + $input.select(); + }; + + this.serializeValue = function () { + return $input.val(); + }; + + this.applyValue = function (item, state) { + item[args.column.field] = state; + }; + + this.isValueChanged = function () { + return (!($input.val() == "" && defaultValue == null)) && ($input.val() != defaultValue); + }; + + this.validate = function () { + if (args.column.validator) { + var validationResults = args.column.validator($input.val()); + if (!validationResults.valid) { + return validationResults; + } + } + + return { + valid: true, + msg: null + }; + }; + + this.init(); + } + + function IntegerEditor(args) { + var $input; + var defaultValue; + var scope = this; + + this.init = function () { + $input = $(""); + + $input.bind("keydown.nav", function (e) { + if (e.keyCode === $.ui.keyCode.LEFT || e.keyCode === $.ui.keyCode.RIGHT) { + e.stopImmediatePropagation(); + } + }); + + $input.appendTo(args.container); + $input.focus().select(); + }; + + this.destroy = function () { + $input.remove(); + }; + + this.focus = function () { + $input.focus(); + }; + + this.loadValue = function (item) { + defaultValue = item[args.column.field]; + $input.val(defaultValue); + $input[0].defaultValue = defaultValue; + $input.select(); + }; + + this.serializeValue = function () { + return parseInt($input.val(), 10) || 0; + }; + + this.applyValue = function (item, state) { + item[args.column.field] = state; + }; + + this.isValueChanged = function () { + return (!($input.val() == "" && defaultValue == null)) && ($input.val() != defaultValue); + }; + + this.validate = function () { + if (isNaN($input.val())) { + return { + valid: false, + msg: "Please enter a valid integer" + }; + } + + return { + valid: true, + msg: null + }; + }; + + this.init(); + } + + function DateEditor(args) { + var $input; + var defaultValue; + var scope = this; + var calendarOpen = false; + + this.init = function () { + $input = $(""); + $input.appendTo(args.container); + $input.focus().select(); + $input.datepicker({ + showOn: "button", + buttonImageOnly: true, + buttonImage: "../images/calendar.gif", + beforeShow: function () { + calendarOpen = true + }, + onClose: function () { + calendarOpen = false + } + }); + $input.width($input.width() - 18); + }; + + this.destroy = function () { + $.datepicker.dpDiv.stop(true, true); + $input.datepicker("hide"); + $input.datepicker("destroy"); + $input.remove(); + }; + + this.show = function () { + if (calendarOpen) { + $.datepicker.dpDiv.stop(true, true).show(); + } + }; + + this.hide = function () { + if (calendarOpen) { + $.datepicker.dpDiv.stop(true, true).hide(); + } + }; + + this.position = function (position) { + if (!calendarOpen) { + return; + } + $.datepicker.dpDiv + .css("top", position.top + 30) + .css("left", position.left); + }; + + this.focus = function () { + $input.focus(); + }; + + this.loadValue = function (item) { + defaultValue = item[args.column.field]; + $input.val(defaultValue); + $input[0].defaultValue = defaultValue; + $input.select(); + }; + + this.serializeValue = function () { + return $input.val(); + }; + + this.applyValue = function (item, state) { + item[args.column.field] = state; + }; + + this.isValueChanged = function () { + return (!($input.val() == "" && defaultValue == null)) && ($input.val() != defaultValue); + }; + + this.validate = function () { + return { + valid: true, + msg: null + }; + }; + + this.init(); + } + + function YesNoSelectEditor(args) { + var $select; + var defaultValue; + var scope = this; + + this.init = function () { + $select = $(""); + $select.appendTo(args.container); + $select.focus(); + }; + + this.destroy = function () { + $select.remove(); + }; + + this.focus = function () { + $select.focus(); + }; + + this.loadValue = function (item) { + $select.val((defaultValue = item[args.column.field]) ? "yes" : "no"); + $select.select(); + }; + + this.serializeValue = function () { + return ($select.val() == "yes"); + }; + + this.applyValue = function (item, state) { + item[args.column.field] = state; + }; + + this.isValueChanged = function () { + return ($select.val() != defaultValue); + }; + + this.validate = function () { + return { + valid: true, + msg: null + }; + }; + + this.init(); + } + + function CheckboxEditor(args) { + var $select; + var defaultValue; + var scope = this; + + this.init = function () { + $select = $(""); + $select.appendTo(args.container); + $select.focus(); + }; + + this.destroy = function () { + $select.remove(); + }; + + this.focus = function () { + $select.focus(); + }; + + this.loadValue = function (item) { + defaultValue = !!item[args.column.field]; + if (defaultValue) { + $select.prop('checked', true); + } else { + $select.prop('checked', false); + } + }; + + this.serializeValue = function () { + return $select.prop('checked'); + }; + + this.applyValue = function (item, state) { + item[args.column.field] = state; + }; + + this.isValueChanged = function () { + return (this.serializeValue() !== defaultValue); + }; + + this.validate = function () { + return { + valid: true, + msg: null + }; + }; + + this.init(); + } + + function PercentCompleteEditor(args) { + var $input, $picker; + var defaultValue; + var scope = this; + + this.init = function () { + $input = $(""); + $input.width($(args.container).innerWidth() - 25); + $input.appendTo(args.container); + + $picker = $("

").appendTo(args.container); + $picker.append("
"); + + $picker.find(".editor-percentcomplete-buttons").append("

"); + + $input.focus().select(); + + $picker.find(".editor-percentcomplete-slider").slider({ + orientation: "vertical", + range: "min", + value: defaultValue, + slide: function (event, ui) { + $input.val(ui.value) + } + }); + + $picker.find(".editor-percentcomplete-buttons button").bind("click", function (e) { + $input.val($(this).attr("val")); + $picker.find(".editor-percentcomplete-slider").slider("value", $(this).attr("val")); + }) + }; + + this.destroy = function () { + $input.remove(); + $picker.remove(); + }; + + this.focus = function () { + $input.focus(); + }; + + this.loadValue = function (item) { + $input.val(defaultValue = item[args.column.field]); + $input.select(); + }; + + this.serializeValue = function () { + return parseInt($input.val(), 10) || 0; + }; + + this.applyValue = function (item, state) { + item[args.column.field] = state; + }; + + this.isValueChanged = function () { + return (!($input.val() == "" && defaultValue == null)) && ((parseInt($input.val(), 10) || 0) != defaultValue); + }; + + this.validate = function () { + if (isNaN(parseInt($input.val(), 10))) { + return { + valid: false, + msg: "Please enter a valid positive number" + }; + } + + return { + valid: true, + msg: null + }; + }; + + this.init(); + } + + /* + * An example of a "detached" editor. + * The UI is added onto document BODY and .position(), .show() and .hide() are implemented. + * KeyDown events are also handled to provide handling for Tab, Shift-Tab, Esc and Ctrl-Enter. + */ + function LongTextEditor(args) { + var $input, $wrapper; + var defaultValue; + var scope = this; + + this.init = function () { + var $container = $("body"); + + $wrapper = $("
") + .appendTo($container); + + $input = $("