diff options
| author | 2013-06-09 18:10:22 +0200 | |
|---|---|---|
| committer | 2013-06-09 18:10:23 +0200 | |
| commit | 16af85004c84d0d6c626b4f8424ce9647669a0c1 (patch) | |
| tree | 025d479862d376dbc17e934f4ed20031c8cd97d1 /pyload/web/app/scripts | |
| parent | adapted to jshint config (diff) | |
| download | pyload-16af85004c84d0d6c626b4f8424ce9647669a0c1.tar.xz | |
moved everything from module to pyload
Diffstat (limited to 'pyload/web/app/scripts')
57 files changed, 7911 insertions, 0 deletions
| diff --git a/pyload/web/app/scripts/app.js b/pyload/web/app/scripts/app.js new file mode 100644 index 000000000..427cb1bc8 --- /dev/null +++ b/pyload/web/app/scripts/app.js @@ -0,0 +1,105 @@ +/*  + *  Global Application Object + *  Contains all necessary logic shared across views + */ +define([ + +    // Libraries. +    'jquery', +    'underscore', +    'backbone', +    'utils/initHB', +    'utils/animations', +    'utils/lazyRequire', +    'utils/dialogs', +    'marionette', +    'bootstrap', +    'animate' + +], function($, _, Backbone, Handlebars) { +    'use strict'; + +    Backbone.Marionette.TemplateCache.prototype.compileTemplate = function(rawTemplate) { +        return Handlebars.compile(rawTemplate); +    }; + +    // TODO: configurable root +    var App = new Backbone.Marionette.Application({ +        root: '/' +    }); + +    App.addRegions({ +        header: '#header', +        notification: '#notification-area', +        selection: '#selection-area', +        content: '#content', +        actionbar: '#actionbar' +    }); + +    App.navigate = function(url) { +        return Backbone.history.navigate(url, true); +    }; + +    App.apiUrl = function(path) { +        var url = window.hostProtocol + window.hostAddress + ':' + window.hostPort + window.pathPrefix + path; +        return url; +    }; + +    // Add Global Helper functions +    // Generates options dict that can be used for xhr requests +    App.apiRequest = function(method, data, options) { +        options || (options = {}); +        options.url = App.apiUrl('api/' + method); +        options.dataType = 'json'; + +        if (data) { +            options.type = 'POST'; +            options.data = {}; +            // Convert arguments to json +            _.keys(data).map(function(key) { +                options.data[key] = JSON.stringify(data[key]); +            }); +        } + +        return options; +    }; + +    App.setTitle = function(name) { +        var title = window.document.title; +        var newTitle; +        // page name separator +        var index = title.indexOf('-'); +        if (index >= 0) +            newTitle = name + ' - ' + title.substr(index + 2, title.length); +        else +            newTitle = name + ' - ' + title; + +        window.document.title = newTitle; +    }; + +    App.openWebSocket = function(path) { +        // TODO +        return new WebSocket(window.wsAddress.replace('%s', window.hostAddress) + path); +    }; + +    App.on('initialize:after', function() { +//        TODO pushState variable +        Backbone.history.start({ +            pushState: false, +            root: App.root +        }); + +        // All links should be handled by backbone +        $(document).on('click', 'a[data-nav]', function(evt) { +            var href = { prop: $(this).prop('href'), attr: $(this).attr('href') }; +            var root = location.protocol + '//' + location.host + App.root; +            if (href.prop.slice(0, root.length) === root) { +                evt.preventDefault(); +                Backbone.history.navigate(href.attr, true); +            } +        }); +    }); + +    // Returns the app object to be available to other modules through require.js. +    return App; +});
\ No newline at end of file diff --git a/pyload/web/app/scripts/collections/AccountList.js b/pyload/web/app/scripts/collections/AccountList.js new file mode 100644 index 000000000..bfc2af5a3 --- /dev/null +++ b/pyload/web/app/scripts/collections/AccountList.js @@ -0,0 +1,24 @@ +define(['jquery', 'backbone', 'underscore', 'app', 'models/Account'], function($, Backbone, _, App, Account) { +    'use strict'; + +    return Backbone.Collection.extend({ + +        model: Account, + +        comparator: function(account) { +            return account.get('plugin'); +        }, + +        initialize: function() { + +        }, + +        fetch: function(options) { +            // TODO: refresh options? +            options = App.apiRequest('getAccounts/false', null, options); +            return Backbone.Collection.prototype.fetch.call(this, options); +        } + +    }); + +});
\ No newline at end of file diff --git a/pyload/web/app/scripts/collections/FileList.js b/pyload/web/app/scripts/collections/FileList.js new file mode 100644 index 000000000..873f4c0e3 --- /dev/null +++ b/pyload/web/app/scripts/collections/FileList.js @@ -0,0 +1,18 @@ +define(['jquery', 'backbone', 'underscore', 'models/File'], function($, Backbone, _, File) { +    'use strict'; + +    return Backbone.Collection.extend({ + +        model: File, + +        comparator: function(file) { +            return file.get('fileorder'); +        }, + +        initialize: function() { + +        } + +    }); + +});
\ No newline at end of file diff --git a/pyload/web/app/scripts/collections/InteractionList.js b/pyload/web/app/scripts/collections/InteractionList.js new file mode 100644 index 000000000..24f8b9248 --- /dev/null +++ b/pyload/web/app/scripts/collections/InteractionList.js @@ -0,0 +1,49 @@ +define(['jquery', 'backbone', 'underscore', 'app', 'models/InteractionTask'], +    function($, Backbone, _, App, InteractionTask) { +        'use strict'; + +        return Backbone.Collection.extend({ + +            model: InteractionTask, + +            comparator: function(task) { +                return task.get('iid'); +            }, + +            fetch: function(options) { +                options = App.apiRequest('getInteractionTasks/0', null, options); +                var self = this; +                options.success = function(data) { +                    self.set(data); +                }; + +                return $.ajax(options); +            }, + +            toJSON: function() { +                var data = {queries: 0, notifications: 0}; + +                this.map(function(task) { +                    if (task.isNotification()) +                        data.notifications++; +                    else +                        data.queries++; +                }); + +                return data; +            }, + +            // a task is waiting for attention (no notification) +            hasTaskWaiting: function() { +                var tasks = 0; +                this.map(function(task) { +                    if (!task.isNotification()) +                        tasks++; +                }); + +                return tasks > 0; +            } + +        }); + +    });
\ No newline at end of file diff --git a/pyload/web/app/scripts/collections/PackageList.js b/pyload/web/app/scripts/collections/PackageList.js new file mode 100644 index 000000000..7bee861a4 --- /dev/null +++ b/pyload/web/app/scripts/collections/PackageList.js @@ -0,0 +1,16 @@ +define(['jquery', 'backbone', 'underscore', 'models/Package'], function($, Backbone, _, Package) { +    'use strict'; + +    return Backbone.Collection.extend({ + +        model: Package, + +        comparator: function(pack) { +            return pack.get('packageorder'); +        }, + +        initialize: function() { +        } + +    }); +});
\ No newline at end of file diff --git a/pyload/web/app/scripts/collections/ProgressList.js b/pyload/web/app/scripts/collections/ProgressList.js new file mode 100644 index 000000000..51849d8de --- /dev/null +++ b/pyload/web/app/scripts/collections/ProgressList.js @@ -0,0 +1,18 @@ +define(['jquery', 'backbone', 'underscore', 'models/Progress'], function($, Backbone, _, Progress) { +    'use strict'; + +    return Backbone.Collection.extend({ + +        model: Progress, + +        comparator: function(progress) { +            return progress.get('eta'); +        }, + +        initialize: function() { + +        } + +    }); + +});
\ No newline at end of file diff --git a/pyload/web/app/scripts/config.js b/pyload/web/app/scripts/config.js new file mode 100644 index 000000000..398d97e11 --- /dev/null +++ b/pyload/web/app/scripts/config.js @@ -0,0 +1,73 @@ +// Sets the require.js configuration for your application. +'use strict'; +require.config({ + +    deps: ['default'], + +    paths: { + +        jquery: '../components/jquery/jquery', +        flot: '../components/flot/jquery.flot', +        transit: '../components/jquery.transit/jquery.transit', +        animate: '../components/jquery.animate-enhanced/scripts/src/jquery.animate-enhanced', +        cookie: '../components/jquery.cookie/jquery.cookie', +        omniwindow: 'vendor/jquery.omniwindow', +        select2: '../components/select2/select2', +        bootstrap: 'vendor/bootstrap-2.3.2', +        underscore: '../components/underscore/underscore', +        backbone: '../components/backbone/backbone', +        marionette: '../components/backbone.marionette/lib/backbone.marionette', +//        handlebars: '../components/handlebars.js/dist/handlebars', +        handlebars: 'vendor/Handlebars-1.0rc1', +        jed: '../components/jed/jed', + +        // TODO: Two hbs dependencies could be replaced +        i18nprecompile: '../components/require-handlebars-plugin/hbs/i18nprecompile', +        json2: '../components/require-handlebars-plugin/hbs/json2', + +        // Plugins +        text: '../components/requirejs-text/text', +        hbs: '../components/require-handlebars-plugin/hbs', + +        // Shortcut +        tpl: '../templates/default' +    }, + +    hbs: { +        disableI18n: true, +        helperPathCallback:       // Callback to determine the path to look for helpers +            function(name) { +                // Some helpers are accumulated into one file +                if (name.indexOf('file') === 0) +                    name = 'fileHelper'; + +                return 'helpers/' + name; +            }, +        templateExtension: 'html' +    }, + +    // Sets the configuration for your third party scripts that are not AMD compatible +    shim: { +        underscore: { +            exports: '_' +        }, + +        backbone: { +            deps: ['underscore', 'jquery'], +            exports: 'Backbone' +        }, + +        marionette: ['backbone'], +//        handlebars: { +//            exports: 'Handlebars' +//        }, + +        flot: ['jquery'], +        transit: ['jquery'], +        cookie: ['jquery'], +        omniwindow: ['jquery'], +        select2: ['jquery'], +        bootstrap: ['jquery'], +        animate: ['jquery'] +    } +});
\ No newline at end of file diff --git a/pyload/web/app/scripts/controller.js b/pyload/web/app/scripts/controller.js new file mode 100644 index 000000000..05237914d --- /dev/null +++ b/pyload/web/app/scripts/controller.js @@ -0,0 +1,67 @@ +define([ +    'app', +    'backbone', + +    // Views +    'views/headerView', +    'views/notificationView', +    'views/dashboard/dashboardView', +    'views/dashboard/selectionView', +    'views/dashboard/filterView', +    'views/loginView', +    'views/settings/settingsView', +    'views/accounts/accountListView' +], function( +    App, Backbone, HeaderView, NotificationView, DashboardView, SelectionView, FilterView, LoginView, SettingsView, AccountListView) { +    'use strict'; +    // TODO some views does not need to be loaded instantly + +    return { + +        header: function() { +            if (!App.header.currentView) { +                App.header.show(new HeaderView()); +                App.header.currentView.init(); +                App.notification.attachView(new NotificationView()); +            } +        }, + +        dashboard: function() { +            this.header(); + +            App.actionbar.show(new FilterView()); +            // TODO: not completly visible after reattaching +            App.selection.attachView(new SelectionView()); +            App.content.show(new DashboardView()); +        }, + +        login: function() { +            App.content.show(new LoginView()); +        }, + +        logout: function() { +            alert('Not implemented'); +        }, + +        settings: function() { +            this.header(); + +            var view = new SettingsView(); +            App.actionbar.show(new view.actionbar()); +            App.content.show(view); +        }, + +        accounts: function() { +            this.header(); + +            var view = new AccountListView(); +            App.actionbar.show(new view.actionbar()); +            App.content.show(view); +        }, + +        admin: function() { +            alert('Not implemented'); +        } +    }; + +}); diff --git a/pyload/web/app/scripts/default.js b/pyload/web/app/scripts/default.js new file mode 100644 index 000000000..a337cee21 --- /dev/null +++ b/pyload/web/app/scripts/default.js @@ -0,0 +1,30 @@ +define('default', ['backbone', 'jquery', 'app', 'router', 'models/userSession'], +    function(Backbone, $, App, Router, UserSession) { +        'use strict'; + +        // Global ajax options +        var options = { +            statusCode: { +                401: function() { +                    console.log('Not logged in.'); +                    App.navigate('login'); +                } +            }, +            xhrFields: {withCredentials: true} +        }; + +        $.ajaxSetup(options); + +        Backbone.ajax = function() { +            Backbone.$.ajaxSetup.call(Backbone.$, options); +            return Backbone.$.ajax.apply(Backbone.$, arguments); +        }; + +        $(function() { +            App.session = new UserSession(); +            App.router = new Router(); +            App.start(); +        }); + +        return App; +    });
\ No newline at end of file diff --git a/pyload/web/app/scripts/helpers/fileHelper.js b/pyload/web/app/scripts/helpers/fileHelper.js new file mode 100644 index 000000000..156be58f0 --- /dev/null +++ b/pyload/web/app/scripts/helpers/fileHelper.js @@ -0,0 +1,55 @@ +// Helpers to render the file view +define('helpers/fileHelper', ['handlebars', 'utils/apitypes', 'helpers/formatTime'], +    function(Handlebars, Api, formatTime) { +        'use strict'; + +        function fileClass(file, options) { +            if (file.finished) +                return 'finished'; +            else if (file.failed) +                return 'failed'; +            else if (file.offline) +                return 'offline'; +            else if (file.online) +                return 'online'; +            else if (file.waiting) +                return 'waiting'; +            else if (file.downloading) +                return 'downloading'; + +            return ''; +        } + +        // TODO +        function fileIcon(media, options) { +            return 'icon-music'; +        } + +        // TODO rest of the states +        function fileStatus(file, options) { +            var s; +            var msg = file.download.statusmsg; + +            if (file.failed) { +                s = '<i class="icon-remove"></i> '; +                if (file.download.error) +                    s += file.download.error; +                else s += msg; +            } else if (file.finished) +                s = '<i class="icon-ok"></i> ' + msg; +            else if (file.downloading) +                s = '<div class="progress"><div class="bar" style="width: ' + file.progress + '%">  ' + +                    formatTime(file.eta) + '</div></div>'; +            else if (file.waiting) +                s = '<i class="icon-time"></i> ' + formatTime(file.eta); +            else +                s = msg; + +            return new Handlebars.SafeString(s); +        } + +        Handlebars.registerHelper('fileClass', fileClass); +        Handlebars.registerHelper('fileIcon', fileIcon); +        Handlebars.registerHelper('fileStatus', fileStatus); +        return fileClass; +    });
\ No newline at end of file diff --git a/pyload/web/app/scripts/helpers/formatSize.js b/pyload/web/app/scripts/helpers/formatSize.js new file mode 100644 index 000000000..3b62e74c7 --- /dev/null +++ b/pyload/web/app/scripts/helpers/formatSize.js @@ -0,0 +1,15 @@ +// Format bytes in human readable format +define('helpers/formatSize', ['handlebars'], function(Handlebars) { +    'use strict'; + +    var sizes = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB']; +    function formatSize(bytes, options) { +        if (!bytes || bytes === 0) return '0 B'; +        var i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)), 10); +        // round to two digits +        return (bytes / Math.pow(1024, i)).toFixed(2) + ' ' + sizes[i]; +    } + +    Handlebars.registerHelper('formatSize', formatSize); +    return formatSize; +});
\ No newline at end of file diff --git a/pyload/web/app/scripts/helpers/formatTime.js b/pyload/web/app/scripts/helpers/formatTime.js new file mode 100644 index 000000000..757ff73ad --- /dev/null +++ b/pyload/web/app/scripts/helpers/formatTime.js @@ -0,0 +1,17 @@ +// Format bytes in human readable format +define('helpers/formatTime', ['handlebars', 'vendor/remaining'], function(Handlebars, Remaining) { +    'use strict'; + +    function formatTime(seconds, options) { +        if (seconds === Infinity) +            return '∞'; +        else if (!seconds || seconds <= 0) +            return '-'; + +        // TODO: digital or written string +        return Remaining.getStringDigital(seconds, window.dates); +    } + +    Handlebars.registerHelper('formatTime', formatTime); +    return formatTime; +});
\ No newline at end of file diff --git a/pyload/web/app/scripts/helpers/pluginIcon.js b/pyload/web/app/scripts/helpers/pluginIcon.js new file mode 100644 index 000000000..6b2fdc67f --- /dev/null +++ b/pyload/web/app/scripts/helpers/pluginIcon.js @@ -0,0 +1,14 @@ +// Resolves name of plugin to icon path +define('helpers/pluginIcon', ['handlebars', 'app'], function(Handlebars, App) { +    'use strict'; + +    function pluginIcon(name) { +        if (typeof name === 'object' && typeof name.get === 'function') +            name = name.get('plugin'); + +        return App.apiUrl('icons/' + name); +    } + +    Handlebars.registerHelper('pluginIcon', pluginIcon); +    return pluginIcon; +});
\ No newline at end of file diff --git a/pyload/web/app/scripts/models/Account.js b/pyload/web/app/scripts/models/Account.js new file mode 100644 index 000000000..a2e24b056 --- /dev/null +++ b/pyload/web/app/scripts/models/Account.js @@ -0,0 +1,51 @@ +define(['jquery', 'backbone', 'underscore', 'app', 'utils/apitypes'], function($, Backbone, _, App, Api) { +    'use strict'; + +    return Backbone.Model.extend({ + +        // TODO +        // generated, not submitted +        idAttribute: 'user', + +        defaults: { +            plugin: null, +            loginname: null, +            owner: -1, +            valid: false, +            validuntil: -1, +            trafficleft: -1, +            maxtraffic: -1, +            premium: false, +            activated: false, +            shared: false, +            options: null +        }, + +        // Model Constructor +        initialize: function() { +        }, + +        // Any time a model attribute is set, this method is called +        validate: function(attrs) { + +        }, + +        save: function(options) { +            options = App.apiRequest('updateAccountInfo', {account: this.toJSON()}, options); +            return $.ajax(options); +        }, + +        destroy: function(options) { +            options = App.apiRequest('removeAccount', {account: this.toJSON()}, options); +            var self = this; +            options.success = function() { +                self.trigger('destroy', self, self.collection, options); +            }; + +            // TODO request is not dispatched +//            return Backbone.Model.prototype.destroy.call(this, options); +            return $.ajax(options); +        } +    }); + +});
\ No newline at end of file diff --git a/pyload/web/app/scripts/models/ConfigHolder.js b/pyload/web/app/scripts/models/ConfigHolder.js new file mode 100644 index 000000000..40efbc7c0 --- /dev/null +++ b/pyload/web/app/scripts/models/ConfigHolder.js @@ -0,0 +1,68 @@ +define(['jquery', 'backbone', 'underscore', 'app', './ConfigItem'], +    function($, Backbone, _, App, ConfigItem) { +        'use strict'; + +        return Backbone.Model.extend({ + +            defaults: { +                name: '', +                label: '', +                description: '', +                long_description: null, +                // simple list but no collection +                items: null, +                info: null +            }, + +            // Model Constructor +            initialize: function() { + +            }, + +            // Loads it from server by name +            fetch: function(options) { +                options = App.apiRequest('loadConfig/"' + this.get('name') + '"', null, options); +                return Backbone.Model.prototype.fetch.call(this, options); +            }, + +            save: function(options) { +                var config = this.toJSON(); +                var items = []; +                // Convert changed items to json +                _.each(config.items, function(item) { +                    if (item.isChanged()) { +                        items.push(item.prepareSave()); +                    } +                }); +                config.items = items; +                // TODO: only set new values on success + +                options = App.apiRequest('saveConfig', {config: config}, options); + +                return $.ajax(options); +            }, + +            parse: function(resp) { +                // Create item models +                resp.items = _.map(resp.items, function(item) { +                    return new ConfigItem(item); +                }); + +                return Backbone.Model.prototype.parse.call(this, resp); +            }, + +            isLoaded: function() { +                return this.has('items') || this.has('long_description'); +            }, + +            // check if any of the items has changes +            hasChanges: function() { +                var items = this.get('items'); +                if (!items) return false; +                return _.reduce(items, function(a, b) { +                    return a || b.isChanged(); +                }, false); +            } + +        }); +    });
\ No newline at end of file diff --git a/pyload/web/app/scripts/models/ConfigItem.js b/pyload/web/app/scripts/models/ConfigItem.js new file mode 100644 index 000000000..2d325c2a2 --- /dev/null +++ b/pyload/web/app/scripts/models/ConfigItem.js @@ -0,0 +1,40 @@ +define(['jquery', 'backbone', 'underscore', 'app', 'utils/apitypes'], +    function($, Backbone, _, App, Api) { +        'use strict'; + +        return Backbone.Model.extend({ + +            defaults: { +                name: '', +                label: '', +                description: '', +                input: null, +                default_value: null, +                value: null, +                // additional attributes +                inputView: null +            }, + +            // Model Constructor +            initialize: function() { + +            }, + +            isChanged: function() { +                return this.get('inputView') && this.get('inputView').getVal() !== this.get('value'); +            }, + +            // set new value and return json +            prepareSave: function() { +                // set the new value +                if (this.get('inputView')) +                    this.set('value', this.get('inputView').getVal()); + +                var data = this.toJSON(); +                delete data.inputView; +                delete data.description; + +                return data; +            } +        }); +    });
\ No newline at end of file diff --git a/pyload/web/app/scripts/models/File.js b/pyload/web/app/scripts/models/File.js new file mode 100644 index 000000000..3beb7f270 --- /dev/null +++ b/pyload/web/app/scripts/models/File.js @@ -0,0 +1,92 @@ +define(['jquery', 'backbone', 'underscore', 'app', 'utils/apitypes'], function($, Backbone, _, App, Api) { +    'use strict'; + +    var Finished = [Api.DownloadStatus.Finished, Api.DownloadStatus.Skipped]; +    var Failed = [Api.DownloadStatus.Failed, Api.DownloadStatus.Aborted, Api.DownloadStatus.TempOffline, Api.DownloadStatus.Offline]; +    // Unfinished - Other + +    return Backbone.Model.extend({ + +        idAttribute: 'fid', + +        defaults: { +            fid: -1, +            name: null, +            package: -1, +            owner: -1, +            size: -1, +            status: -1, +            media: -1, +            added: -1, +            fileorder: -1, +            download: null, + +            // UI attributes +            selected: false, +            visible: true, +            progress: 0, +            eta: 0 +        }, + +        // Model Constructor +        initialize: function() { + +        }, + +        fetch: function(options) { +            options = App.apiRequest( +                'getFileInfo', +                {fid: this.get('fid')}, +                options); + +            return Backbone.Model.prototype.fetch.call(this, options); +        }, + +        destroy: function(options) { +            // also not working when using data +            options = App.apiRequest( +                'deleteFiles/[' + this.get('fid') + ']', +                null, options); +            options.method = 'post'; + +            return Backbone.Model.prototype.destroy.call(this, options); +        }, + +        // Does not send a request to the server +        destroyLocal: function(options) { +            this.trigger('destroy', this, this.collection, options); +        }, + +        restart: function(options) { +            options = App.apiRequest( +                'restartFile', +                {fid: this.get('fid')}, +                options); + +            return $.ajax(options); +        }, + +        // Any time a model attribute is set, this method is called +        validate: function(attrs) { + +        }, + +        isDownload: function() { +            return this.has('download'); +        }, + +        isFinished: function() { +            return _.indexOf(Finished, this.get('download').status) > -1; +        }, + +        isUnfinished: function() { +            return _.indexOf(Finished, this.get('download').status) === -1 && _.indexOf(Failed, this.get('download').status) === -1; +        }, + +        isFailed: function() { +            return _.indexOf(Failed, this.get('download').status) > -1; +        } + +    }); + +});
\ No newline at end of file diff --git a/pyload/web/app/scripts/models/InteractionTask.js b/pyload/web/app/scripts/models/InteractionTask.js new file mode 100644 index 000000000..54c739d4b --- /dev/null +++ b/pyload/web/app/scripts/models/InteractionTask.js @@ -0,0 +1,41 @@ +define(['jquery', 'backbone', 'underscore', 'app', 'utils/apitypes'], +    function($, Backbone, _, App, Api) { +        'use strict'; + +        return Backbone.Model.extend({ + +            idAttribute: 'iid', + +            defaults: { +                iid: -1, +                type: null, +                input: null, +                default_value: null, +                title: '', +                description: '', +                plugin: '', +                // additional attributes +                result: '' +            }, + +            // Model Constructor +            initialize: function() { + +            }, + +            save: function(options) { +                options = App.apiRequest('setInteractionResult/' + this.get('iid'), +                    {result: this.get('result')}, options); + +                return $.ajax(options); +            }, + +            isNotification: function() { +                return this.get('type') === Api.Interaction.Notification; +            }, + +            isCaptcha: function() { +                return this.get('type') === Api.Interaction.Captcha; +            } +        }); +    });
\ No newline at end of file diff --git a/pyload/web/app/scripts/models/Package.js b/pyload/web/app/scripts/models/Package.js new file mode 100644 index 000000000..a34ec1c69 --- /dev/null +++ b/pyload/web/app/scripts/models/Package.js @@ -0,0 +1,119 @@ +define(['jquery', 'backbone', 'underscore', 'app', 'collections/FileList', 'require'], +    function($, Backbone, _, App, FileList, require) { +        'use strict'; + +        return Backbone.Model.extend({ + +            idAttribute: 'pid', + +            defaults: { +                pid: -1, +                name: null, +                folder: '', +                root: -1, +                owner: -1, +                site: '', +                comment: '', +                password: '', +                added: -1, +                tags: null, +                status: -1, +                shared: false, +                packageorder: -1, +                stats: null, +                fids: null, +                pids: null, +                files: null, // Collection +                packs: null, // Collection + +                selected: false // For Checkbox +            }, + +            // Model Constructor +            initialize: function() { +            }, + +            toJSON: function(options) { +                var obj = Backbone.Model.prototype.toJSON.call(this, options); +                obj.percent = Math.round(obj.stats.linksdone * 100 / obj.stats.linkstotal); + +                return obj; +            }, + +            // Changes url + method and delegates call to super class +            fetch: function(options) { +                options = App.apiRequest( +                    'getFileTree/' + this.get('pid'), +                    {full: false}, +                    options); + +                return Backbone.Model.prototype.fetch.call(this, options); +            }, + +            // Create a pseudo package und use search to populate data +            search: function(qry, options) { +                options = App.apiRequest( +                    'findFiles', +                    {pattern: qry}, +                    options); + +                return Backbone.Model.prototype.fetch.call(this, options); +            }, + +            save: function(options) { +                // TODO +            }, + +            destroy: function(options) { +                // TODO: Not working when using data?, array seems to break it +                options = App.apiRequest( +                    'deletePackages/[' + this.get('pid') + ']', +                    null, options); +                options.method = 'post'; + +                console.log(options); + +                return Backbone.Model.prototype.destroy.call(this, options); +            }, + +            restart: function(options) { +                options = App.apiRequest( +                    'restartPackage', +                    {pid: this.get('pid')}, +                    options); + +                var self = this; +                options.success = function() { +                    self.fetch(); +                }; +                return $.ajax(options); +            }, + +            parse: function(resp) { +                // Package is loaded from tree collection +                if (_.has(resp, 'root')) { +                    if (!this.has('files')) +                        resp.root.files = new FileList(_.values(resp.files)); +                    else +                        this.get('files').set(_.values(resp.files)); + +                    // circular dependencies needs to be avoided +                    var PackageList = require('collections/PackageList'); + +                    if (!this.has('packs')) +                        resp.root.packs = new PackageList(_.values(resp.packages)); +                    else +                        this.get('packs').set(_.values(resp.packages)); + +                    return resp.root; +                } +                return Backbone.model.prototype.parse.call(this, resp); +            }, + +            // Any time a model attribute is set, this method is called +            validate: function(attrs) { + +            } + +        }); +    });
\ No newline at end of file diff --git a/pyload/web/app/scripts/models/Progress.js b/pyload/web/app/scripts/models/Progress.js new file mode 100644 index 000000000..b0bbb684d --- /dev/null +++ b/pyload/web/app/scripts/models/Progress.js @@ -0,0 +1,50 @@ +define(['jquery', 'backbone', 'underscore', 'utils/apitypes'], function($, Backbone, _, Api) { +    'use strict'; + +    return Backbone.Model.extend({ + +        // generated, not submitted +        idAttribute: 'pid', + +        defaults: { +            pid: -1, +            plugin: null, +            name: null, +            statusmsg: -1, +            eta: -1, +            done: -1, +            total: -1, +            download: null +        }, + +        getPercent: function() { +            if (this.get('total') > 0) +                return Math.round(this.get('done') * 100 / this.get('total')); +            return  0; +        }, + +        // Model Constructor +        initialize: function() { + +        }, + +        // Any time a model attribute is set, this method is called +        validate: function(attrs) { + +        }, + +        toJSON: function(options) { +            var obj = Backbone.Model.prototype.toJSON.call(this, options); +            obj.percent = this.getPercent(); +            obj.downloading = this.isDownload() && this.get('download').status === Api.DownloadStatus.Downloading; + +            return obj; +        }, + +        isDownload : function() { +            return this.has('download'); +        } + +    }); + +});
\ No newline at end of file diff --git a/pyload/web/app/scripts/models/ServerStatus.js b/pyload/web/app/scripts/models/ServerStatus.js new file mode 100644 index 000000000..59739b41e --- /dev/null +++ b/pyload/web/app/scripts/models/ServerStatus.js @@ -0,0 +1,47 @@ +define(['jquery', 'backbone', 'underscore'], +    function($, Backbone, _) { +        'use strict'; + +        return Backbone.Model.extend({ + +            defaults: { +                speed: 0, +                linkstotal: 0, +                linksqueue: 0, +                sizetotal: 0, +                sizequeue: 0, +                notifications: -1, +                paused: false, +                download: false, +                reconnect: false +            }, + +            // Model Constructor +            initialize: function() { + +            }, + +            fetch: function(options) { +                options || (options = {}); +                options.url = 'api/getServerStatus'; + +                return Backbone.Model.prototype.fetch.call(this, options); +            }, + +            toJSON: function(options) { +                var obj = Backbone.Model.prototype.toJSON.call(this, options); + +                obj.linksdone = obj.linkstotal - obj.linksqueue; +                obj.sizedone = obj.sizetotal - obj.sizequeue; +                if (obj.speed && obj.speed > 0) +                    obj.eta = Math.round(obj.sizequeue / obj.speed); +                else if (obj.sizequeue > 0) +                    obj.eta = Infinity; +                else +                    obj.eta = 0; + +                return obj; +            } + +        }); +    });
\ No newline at end of file diff --git a/pyload/web/app/scripts/models/TreeCollection.js b/pyload/web/app/scripts/models/TreeCollection.js new file mode 100644 index 000000000..2f761e6cc --- /dev/null +++ b/pyload/web/app/scripts/models/TreeCollection.js @@ -0,0 +1,50 @@ +define(['jquery', 'backbone', 'underscore', 'app', 'models/Package', 'collections/FileList', 'collections/PackageList'], +    function($, Backbone, _, App, Package, FileList, PackageList) { +        'use strict'; + +        // TreeCollection +        // A Model and not a collection, aggregates other collections +        return Backbone.Model.extend({ + +            defaults: { +                root: null, +                packages: null, +                files: null +            }, + +            initialize: function() { + +            }, + +            fetch: function(options) { +                options || (options = {}); +                var pid = options.pid || -1; + +                options = App.apiRequest( +                    'getFileTree/' + pid, +                    {full: false}, +                    options); + +                console.log('Fetching package tree ' + pid); +                return Backbone.Model.prototype.fetch.call(this, options); +            }, + +            // Parse the response and updates the collections +            parse: function(resp) { +                var ret = {}; +                if (!this.has('packages')) +                    ret.packages = new PackageList(_.values(resp.packages)); +                else +                    this.get('packages').set(_.values(resp.packages)); + +                if (!this.has('files')) +                    ret.files = new FileList(_.values(resp.files)); +                else +                    this.get('files').set(_.values(resp.files)); + +                ret.root = new Package(resp.root); +                return ret; +            } + +        }); +    });
\ No newline at end of file diff --git a/pyload/web/app/scripts/models/UserSession.js b/pyload/web/app/scripts/models/UserSession.js new file mode 100644 index 000000000..a7e9aa848 --- /dev/null +++ b/pyload/web/app/scripts/models/UserSession.js @@ -0,0 +1,20 @@ +define(['jquery', 'backbone', 'underscore',  'utils/apitypes', 'cookie'], +    function($, Backbone, _, Api) { +        'use strict'; + +        return Backbone.Model.extend({ + +            idAttribute: 'username', + +            defaults: { +                username: null, +                permissions: null, +                session: null +            }, + +            // Model Constructor +            initialize: function() { +                this.set('session', $.cookie('beaker.session.id')); +            } +        }); +    });
\ No newline at end of file diff --git a/pyload/web/app/scripts/router.js b/pyload/web/app/scripts/router.js new file mode 100644 index 000000000..68ea5575d --- /dev/null +++ b/pyload/web/app/scripts/router.js @@ -0,0 +1,29 @@ +/** + * Router defines routes that are handled by registered controller + */ +define([ +    // Libraries +    'backbone', +    'marionette', + +    // Modules +    'controller' +], +    function(Backbone, Marionette, Controller) { +        'use strict'; + +        return Backbone.Marionette.AppRouter.extend({ + +            appRoutes: { +                '': 'dashboard', +                'login': 'login', +                'logout': 'logout', +                'settings': 'settings', +                'accounts': 'accounts', +                'admin': 'admin' +            }, + +            // Our controller to handle the routes +            controller: Controller +        }); +    }); diff --git a/pyload/web/app/scripts/routers/defaultRouter.js b/pyload/web/app/scripts/routers/defaultRouter.js new file mode 100644 index 000000000..4b00d160c --- /dev/null +++ b/pyload/web/app/scripts/routers/defaultRouter.js @@ -0,0 +1,30 @@ +define(['jquery', 'backbone', 'views/headerView'], function($, Backbone, HeaderView) { +    'use strict'; + +    var Router = Backbone.Router.extend({ + +        initialize: function() { +            Backbone.history.start(); +        }, + +        // All of your Backbone Routes (add more) +        routes: { + +            // When there is no hash bang on the url, the home method is called +            '': 'home' + +        }, + +        'home': function() { +            // Instantiating mainView and anotherView instances +            var headerView = new HeaderView(); + +            // Renders the mainView template +            headerView.render(); + +        } +    }); + +    // Returns the Router class +    return Router; +});
\ No newline at end of file diff --git a/pyload/web/app/scripts/routers/mobileRouter.js b/pyload/web/app/scripts/routers/mobileRouter.js new file mode 100644 index 000000000..e24cb7a34 --- /dev/null +++ b/pyload/web/app/scripts/routers/mobileRouter.js @@ -0,0 +1,56 @@ +define(['jquery', 'backbone', 'underscore'], function($, Backbone, _) { +    'use strict'; + +    return Backbone.Router.extend({ + +        initialize: function() { +            _.bindAll(this, 'changePage'); + +            this.$el = $('#content'); + +            // Tells Backbone to start watching for hashchange events +            Backbone.history.start(); + +        }, + +        // All of your Backbone Routes (add more) +        routes: { + +            // When there is no hash bang on the url, the home method is called +            '': 'home' + +        }, + +        'home': function() { + +            var self = this; + +            $('#p1').fastClick(function() { +                self.changePage($('<div class=\'page\' style=\'background-color: #9acd32;\'><h1>Page 1</h1><br>some content<br>sdfdsf<br>sdffg<h3>oiuzz</h3></div>')); +            }); + +            $('#p2').bind('click', function() { +                self.changePage($('<div class=\'page\' style=\'background-color: blue;\'><h1>Page 2</h1><br>some content<br>sdfdsf<br><h2>sdfsdf</h2>sdffg</div>')); +            }); + +        }, + +        changePage: function(content) { + +            var oldpage = this.$el.find('.page'); +            content.css({x: '100%'}); +            this.$el.append(content); +            content.transition({x: 0}, function() { +                window.setTimeout(function() { +                    oldpage.remove(); +                }, 400); +            }); + +//            $("#viewport").transition({x: "100%"}, function(){ +//                $("#viewport").html(content); +//                $("#viewport").transition({x: 0}); +//            }); +        } + +    }); +});
\ No newline at end of file diff --git a/pyload/web/app/scripts/utils/animations.js b/pyload/web/app/scripts/utils/animations.js new file mode 100644 index 000000000..7f89afef1 --- /dev/null +++ b/pyload/web/app/scripts/utils/animations.js @@ -0,0 +1,129 @@ +define(['jquery', 'underscore', 'transit'], function(jQuery, _) { +    'use strict'; + +    // Adds an element and computes its height, which is saved as data attribute +    // Important function to have slide animations +    jQuery.fn.appendWithHeight = function(element, hide) { +        var o = jQuery(this[0]); +        element = jQuery(element); + +        // TODO: additionally it could be placed out of viewport first +        // The real height can only be retrieved when element is on DOM and display:true +        element.css('visibility', 'hidden'); +        o.append(element); + +        var height = element.height(); + +        // Hide the element +        if (hide === true) { +            element.hide(); +            element.height(0); +        } + +        element.css('visibility', ''); +        element.data('height', height); + +        return this; +    }; + +    // Shortcut to have a animation when element is added +    jQuery.fn.appendWithAnimation = function(element, animation) { +        var o = jQuery(this[0]); +        element = jQuery(element); + +        if (animation === true) +            element.hide(); + +        o.append(element); + +        if (animation === true) +            element.fadeIn(); + +//        element.calculateHeight(); + +        return this; +    }; + +    // calculate the height and write it to data, should be used on invisible elements +    jQuery.fn.calculateHeight = function(setHeight) { +        var o = jQuery(this[0]); +        var height = o.height(); +        if (!height) { +            var display = o.css('display'); +            o.css('visibility', 'hidden'); +            o.show(); +            height = o.height(); + +            o.css('display', display); +            o.css('visibility', ''); +        } + +        if (setHeight) +            o.css('height', height); + +        o.data('height', height); +        return this; +    }; + +    // TODO: carry arguments, optional height argument + +    // reset arguments, sets overflow hidden +    jQuery.fn.slideOut = function(reset) { +        var o = jQuery(this[0]); +        o.animate({height: o.data('height'), opacity: 'show'}, function() { +            // reset css attributes; +            if (reset) { +                this.css('overflow', ''); +                this.css('height', ''); +            } +        }); +        return this; +    }; + +    jQuery.fn.slideIn = function(reset) { +        var o = jQuery(this[0]); +        if (reset) { +            o.css('overflow', 'hidden'); +        } +        o.animate({height: 0, opacity: 'hide'}); +        return this; +    }; + +    jQuery.fn.initTooltips = function(placement) { +        placement || (placement = 'top'); + +        var o = jQuery(this[0]); +        o.find('[data-toggle="tooltip"]').tooltip( +            { +                delay: {show: 800, hide: 100}, +                placement: placement +            }); + +        return this; +    }; + +    jQuery.fn._transit = jQuery.fn.transit; + +    // Overriding transit plugin to support hide and show +    jQuery.fn.transit = jQuery.fn.transition = function(props, duration, easing, callback) { +        var self = this; +        var cb = callback; +        var newprops = _.extend({}, props); + +        if (newprops && (newprops.opacity === 'hide')) { +            newprops.opacity = 0; + +            callback = function() { +                self.css({display: 'none'}); +                if (typeof cb === 'function') { +                    cb.apply(self); +                } +            }; +        } else if (newprops && (newprops.opacity === 'show')) { +            newprops.opacity = 1; +            this.css({display: 'block'}); +        } + +        return this._transit(newprops, duration, easing, callback); +    }; +});
\ No newline at end of file diff --git a/pyload/web/app/scripts/utils/apitypes.js b/pyload/web/app/scripts/utils/apitypes.js new file mode 100644 index 000000000..cbbc9064f --- /dev/null +++ b/pyload/web/app/scripts/utils/apitypes.js @@ -0,0 +1,16 @@ +// Autogenerated, do not edit! +/*jslint -W070: false*/ +define([], function() { +	'use strict'; +	return { +		DownloadState: {'Failed': 3, 'All': 0, 'Unmanaged': 4, 'Finished': 1, 'Unfinished': 2}, +		DownloadStatus: {'Downloading': 10, 'NA': 0, 'Processing': 14, 'Waiting': 9, 'Decrypting': 13, 'Paused': 4, 'Failed': 7, 'Finished': 5, 'Skipped': 6, 'Unknown': 16, 'Aborted': 12, 'Online': 2, 'TempOffline': 11, 'Offline': 1, 'Custom': 15, 'Starting': 8, 'Queued': 3}, +		FileStatus: {'Remote': 2, 'Ok': 0, 'Missing': 1}, +		InputType: {'Multiple': 10, 'Int': 2, 'NA': 0, 'List': 11, 'Bool': 7, 'File': 3, 'Text': 1, 'Table': 12, 'Folder': 4, 'Password': 6, 'Click': 8, 'Select': 9, 'Textbox': 5}, +		Interaction: {'Captcha': 2, 'All': 0, 'Query': 4, 'Notification': 1}, +		MediaType: {'All': 0, 'Audio': 2, 'Image': 4, 'Other': 1, 'Video': 8, 'Document': 16, 'Archive': 32}, +		PackageStatus: {'Paused': 1, 'Remote': 3, 'Folder': 2, 'Ok': 0}, +		Permission: {'All': 0, 'Interaction': 32, 'Modify': 4, 'Add': 1, 'Accounts': 16, 'Plugins': 64, 'Download': 8, 'Delete': 2}, +		Role: {'Admin': 0, 'User': 1}, +	}; +});
\ No newline at end of file diff --git a/pyload/web/app/scripts/utils/dialogs.js b/pyload/web/app/scripts/utils/dialogs.js new file mode 100644 index 000000000..4933b7ed2 --- /dev/null +++ b/pyload/web/app/scripts/utils/dialogs.js @@ -0,0 +1,16 @@ +// Loads all helper and set own handlebars rules +define(['jquery', 'underscore', 'views/abstract/modalView'], function($, _, Modal) { +    'use strict'; + +    // Shows the confirm dialog for given context +    // on success executes func with context +    _.confirm = function(template, func, context) { +        template = 'text!tpl/' + template; +        _.requireOnce([template], function(html) { +            var template = _.compile(html); +            var dialog = new Modal(template, _.bind(func, context)); +            dialog.show(); +        }); + +    }; +});
\ No newline at end of file diff --git a/pyload/web/app/scripts/utils/initHB.js b/pyload/web/app/scripts/utils/initHB.js new file mode 100644 index 000000000..d7f582521 --- /dev/null +++ b/pyload/web/app/scripts/utils/initHB.js @@ -0,0 +1,11 @@ +// Loads all helper and set own handlebars rules +define(['underscore', 'handlebars', +    'helpers/formatSize', 'helpers/fileHelper', 'helpers/formatTime'], +    function(_, Handlebars) { +        'use strict'; +        // Replace with own lexer rules compiled from handlebars.l +        Handlebars.Parser.lexer.rules = [/^(?:[^\x00]*?(?=(<%)))/, /^(?:[^\x00]+)/, /^(?:[^\x00]{2,}?(?=(\{\{|$)))/, /^(?:\{\{>)/, /^(?:<%=)/, /^(?:<%\/)/, /^(?:\{\{\^)/, /^(?:<%\s*else\b)/, /^(?:\{<%%)/, /^(?:\{\{&)/, /^(?:<%![\s\S]*?%>)/, /^(?:<%)/, /^(?:=)/, /^(?:\.(?=[%} ]))/, /^(?:\.\.)/, /^(?:[\/.])/, /^(?:\s+)/, /^(?:%%>)/, /^(?:%>)/, /^(?:"(\\["]|[^"])*")/, /^(?:'(\\[']|[^'])*')/, /^(?:@[a-zA-Z]+)/, /^(?:true(?=[%}\s]))/, /^(?:false(?=[%}\s]))/, /^(?:[0-9]+(?=[%}\s]))/, /^(?:[a-zA-Z0-9_$-]+(?=[=%}\s\/.]))/, /^(?:\[[^\]]*\])/, /^(?:.)/, /^(?:$)/]; +        _.compile = Handlebars.compile; + +        return Handlebars; +    });
\ No newline at end of file diff --git a/pyload/web/app/scripts/utils/lazyRequire.js b/pyload/web/app/scripts/utils/lazyRequire.js new file mode 100644 index 000000000..96c07aa24 --- /dev/null +++ b/pyload/web/app/scripts/utils/lazyRequire.js @@ -0,0 +1,97 @@ +// Define the module. +define( +	[ +		'require', 'underscore' +	], +	function( require, _ ){ +        'use strict'; + + +		// Define the states of loading for a given set of modules +		// within a require() statement. +		var states = { +			unloaded: 'UNLOADED', +			loading: 'LOADING', +			loaded: 'LOADED' +		}; + + +		// Define the top-level module container. Mostly, we're making +		// the top-level container a non-Function so that users won't +		// try to invoke this without calling the once() method below. +		var lazyRequire = {}; + + +		// I will return a new, unique instance of the requrieOnce() +		// method. Each instance will only call the require() method +		// once internally. +		lazyRequire.once = function(){ + +			// The modules start in an unloaded state before +			// requireOnce() is invoked by the calling code. +			var state = states.unloaded; +            var args; + +			var requireOnce = function(dependencies, loadCallback ){ + +				// Use the module state to determine which method to +				// invoke (or just to ignore the invocation). +				if (state === states.loaded){ +					loadCallback.apply(null, args); + +				// The modules have not yet been requested - let's +				// lazy load them. +				} else if (state !== states.loading){ + +					// We're about to load the modules asynchronously; +					// flag the interim state. +					state = states.loading; + +					// Load the modules. +					require( +						dependencies, +						function(){ + +                            args = arguments; +							loadCallback.apply( null, args ); +                            state = states.loaded; + + +						} +					); + +				// RequireJS is currently loading the modules +				// asynchronously, but they have not finished +				// loading yet. +				} else { + +					// Simply ignore this call. +					return; + +				} + +			}; + +			// Return the new lazy loader. +			return( requireOnce ); + +		}; + + +		// -------------------------------------------------- // +		// -------------------------------------------------- // + +        // Set up holder for underscore +        var instances = {}; +        _.requireOnce = function(dependencies, loadCallback) { +            if (!_.has(instances, dependencies)) +                instances[dependencies] = lazyRequire.once(); + +            return instances[dependencies](dependencies, loadCallback); +        }; + + +		// Return the module definition. +		return( lazyRequire ); +	} +);
\ No newline at end of file diff --git a/pyload/web/app/scripts/vendor/Handlebars-1.0rc1.js b/pyload/web/app/scripts/vendor/Handlebars-1.0rc1.js new file mode 100644 index 000000000..991242461 --- /dev/null +++ b/pyload/web/app/scripts/vendor/Handlebars-1.0rc1.js @@ -0,0 +1,1927 @@ +// lib/handlebars/base.js +(function () { +/*jshint eqnull:true*/ +this.Handlebars = {}; + +(function(Handlebars) { + +Handlebars.VERSION = "1.0.rc.1"; + +Handlebars.helpers  = {}; +Handlebars.partials = {}; + +Handlebars.registerHelper = function(name, fn, inverse) { +  if(inverse) { fn.not = inverse; } +  this.helpers[name] = fn; +}; + +Handlebars.registerPartial = function(name, str) { +  this.partials[name] = str; +}; + +Handlebars.registerHelper('helperMissing', function(arg) { +  if(arguments.length === 2) { +    return undefined; +  } else { +    throw new Error("Could not find property '" + arg + "'"); +  } +}); + +var toString = Object.prototype.toString, functionType = "[object Function]"; + +Handlebars.registerHelper('blockHelperMissing', function(context, options) { +  var inverse = options.inverse || function() {}, fn = options.fn; + + +  var ret = ""; +  var type = toString.call(context); + +  if(type === functionType) { context = context.call(this); } + +  if(context === true) { +    return fn(this); +  } else if(context === false || context == null) { +    return inverse(this); +  } else if(type === "[object Array]") { +    if(context.length > 0) { +      return Handlebars.helpers.each(context, options); +    } else { +      return inverse(this); +    } +  } else { +    return fn(context); +  } +}); + +Handlebars.K = function() {}; + +Handlebars.createFrame = Object.create || function(object) { +  Handlebars.K.prototype = object; +  var obj = new Handlebars.K(); +  Handlebars.K.prototype = null; +  return obj; +}; + +Handlebars.registerHelper('each', function(context, options) { +  var fn = options.fn, inverse = options.inverse; +  var ret = "", data; + +  if (options.data) { +    data = Handlebars.createFrame(options.data); +  } + +  if(context && context.length > 0) { +    for(var i=0, j=context.length; i<j; i++) { +      if (data) { data.index = i; } +      ret = ret + fn(context[i], { data: data }); +    } +  } else { +    ret = inverse(this); +  } +  return ret; +}); + +Handlebars.registerHelper('if', function(context, options) { +  var type = toString.call(context); +  if(type === functionType) { context = context.call(this); } + +  if(!context || Handlebars.Utils.isEmpty(context)) { +    return options.inverse(this); +  } else { +    return options.fn(this); +  } +}); + +Handlebars.registerHelper('unless', function(context, options) { +  var fn = options.fn, inverse = options.inverse; +  options.fn = inverse; +  options.inverse = fn; + +  return Handlebars.helpers['if'].call(this, context, options); +}); + +Handlebars.registerHelper('with', function(context, options) { +  return options.fn(context); +}); + +Handlebars.registerHelper('log', function(context) { +  Handlebars.log(context); +}); + +}(this.Handlebars)); +; +// lib/handlebars/compiler/parser.js +/* Jison generated parser */ +var handlebars = (function(){ +var parser = {trace: function trace() { }, +yy: {}, +symbols_: {"error":2,"root":3,"program":4,"EOF":5,"statements":6,"simpleInverse":7,"statement":8,"openInverse":9,"closeBlock":10,"openBlock":11,"mustache":12,"partial":13,"CONTENT":14,"COMMENT":15,"OPEN_BLOCK":16,"inMustache":17,"CLOSE":18,"OPEN_INVERSE":19,"OPEN_ENDBLOCK":20,"path":21,"OPEN":22,"OPEN_UNESCAPED":23,"OPEN_PARTIAL":24,"params":25,"hash":26,"DATA":27,"param":28,"STRING":29,"INTEGER":30,"BOOLEAN":31,"hashSegments":32,"hashSegment":33,"ID":34,"EQUALS":35,"pathSegments":36,"SEP":37,"$accept":0,"$end":1}, +terminals_: {2:"error",5:"EOF",14:"CONTENT",15:"COMMENT",16:"OPEN_BLOCK",18:"CLOSE",19:"OPEN_INVERSE",20:"OPEN_ENDBLOCK",22:"OPEN",23:"OPEN_UNESCAPED",24:"OPEN_PARTIAL",27:"DATA",29:"STRING",30:"INTEGER",31:"BOOLEAN",34:"ID",35:"EQUALS",37:"SEP"}, +productions_: [0,[3,2],[4,3],[4,1],[4,0],[6,1],[6,2],[8,3],[8,3],[8,1],[8,1],[8,1],[8,1],[11,3],[9,3],[10,3],[12,3],[12,3],[13,3],[13,4],[7,2],[17,3],[17,2],[17,2],[17,1],[17,1],[25,2],[25,1],[28,1],[28,1],[28,1],[28,1],[28,1],[26,1],[32,2],[32,1],[33,3],[33,3],[33,3],[33,3],[33,3],[21,1],[36,3],[36,1]], +performAction: function anonymous(yytext,yyleng,yylineno,yy,yystate,$$,_$) { + +var $0 = $$.length - 1; +switch (yystate) { +case 1: return $$[$0-1];  +break; +case 2: this.$ = new yy.ProgramNode($$[$0-2], $$[$0]);  +break; +case 3: this.$ = new yy.ProgramNode($$[$0]);  +break; +case 4: this.$ = new yy.ProgramNode([]);  +break; +case 5: this.$ = [$$[$0]];  +break; +case 6: $$[$0-1].push($$[$0]); this.$ = $$[$0-1];  +break; +case 7: this.$ = new yy.BlockNode($$[$0-2], $$[$0-1].inverse, $$[$0-1], $$[$0]);  +break; +case 8: this.$ = new yy.BlockNode($$[$0-2], $$[$0-1], $$[$0-1].inverse, $$[$0]);  +break; +case 9: this.$ = $$[$0];  +break; +case 10: this.$ = $$[$0];  +break; +case 11: this.$ = new yy.ContentNode($$[$0]);  +break; +case 12: this.$ = new yy.CommentNode($$[$0]);  +break; +case 13: this.$ = new yy.MustacheNode($$[$0-1][0], $$[$0-1][1]);  +break; +case 14: this.$ = new yy.MustacheNode($$[$0-1][0], $$[$0-1][1]);  +break; +case 15: this.$ = $$[$0-1];  +break; +case 16: this.$ = new yy.MustacheNode($$[$0-1][0], $$[$0-1][1]);  +break; +case 17: this.$ = new yy.MustacheNode($$[$0-1][0], $$[$0-1][1], true);  +break; +case 18: this.$ = new yy.PartialNode($$[$0-1]);  +break; +case 19: this.$ = new yy.PartialNode($$[$0-2], $$[$0-1]);  +break; +case 20:  +break; +case 21: this.$ = [[$$[$0-2]].concat($$[$0-1]), $$[$0]];  +break; +case 22: this.$ = [[$$[$0-1]].concat($$[$0]), null];  +break; +case 23: this.$ = [[$$[$0-1]], $$[$0]];  +break; +case 24: this.$ = [[$$[$0]], null];  +break; +case 25: this.$ = [[new yy.DataNode($$[$0])], null];  +break; +case 26: $$[$0-1].push($$[$0]); this.$ = $$[$0-1];  +break; +case 27: this.$ = [$$[$0]];  +break; +case 28: this.$ = $$[$0];  +break; +case 29: this.$ = new yy.StringNode($$[$0]);  +break; +case 30: this.$ = new yy.IntegerNode($$[$0]);  +break; +case 31: this.$ = new yy.BooleanNode($$[$0]);  +break; +case 32: this.$ = new yy.DataNode($$[$0]);  +break; +case 33: this.$ = new yy.HashNode($$[$0]);  +break; +case 34: $$[$0-1].push($$[$0]); this.$ = $$[$0-1];  +break; +case 35: this.$ = [$$[$0]];  +break; +case 36: this.$ = [$$[$0-2], $$[$0]];  +break; +case 37: this.$ = [$$[$0-2], new yy.StringNode($$[$0])];  +break; +case 38: this.$ = [$$[$0-2], new yy.IntegerNode($$[$0])];  +break; +case 39: this.$ = [$$[$0-2], new yy.BooleanNode($$[$0])];  +break; +case 40: this.$ = [$$[$0-2], new yy.DataNode($$[$0])];  +break; +case 41: this.$ = new yy.IdNode($$[$0]);  +break; +case 42: $$[$0-2].push($$[$0]); this.$ = $$[$0-2];  +break; +case 43: this.$ = [$$[$0]];  +break; +} +}, +table: [{3:1,4:2,5:[2,4],6:3,8:4,9:5,11:6,12:7,13:8,14:[1,9],15:[1,10],16:[1,12],19:[1,11],22:[1,13],23:[1,14],24:[1,15]},{1:[3]},{5:[1,16]},{5:[2,3],7:17,8:18,9:5,11:6,12:7,13:8,14:[1,9],15:[1,10],16:[1,12],19:[1,19],20:[2,3],22:[1,13],23:[1,14],24:[1,15]},{5:[2,5],14:[2,5],15:[2,5],16:[2,5],19:[2,5],20:[2,5],22:[2,5],23:[2,5],24:[2,5]},{4:20,6:3,8:4,9:5,11:6,12:7,13:8,14:[1,9],15:[1,10],16:[1,12],19:[1,11],20:[2,4],22:[1,13],23:[1,14],24:[1,15]},{4:21,6:3,8:4,9:5,11:6,12:7,13:8,14:[1,9],15:[1,10],16:[1,12],19:[1,11],20:[2,4],22:[1,13],23:[1,14],24:[1,15]},{5:[2,9],14:[2,9],15:[2,9],16:[2,9],19:[2,9],20:[2,9],22:[2,9],23:[2,9],24:[2,9]},{5:[2,10],14:[2,10],15:[2,10],16:[2,10],19:[2,10],20:[2,10],22:[2,10],23:[2,10],24:[2,10]},{5:[2,11],14:[2,11],15:[2,11],16:[2,11],19:[2,11],20:[2,11],22:[2,11],23:[2,11],24:[2,11]},{5:[2,12],14:[2,12],15:[2,12],16:[2,12],19:[2,12],20:[2,12],22:[2,12],23:[2,12],24:[2,12]},{17:22,21:23,27:[1,24],34:[1,26],36:25},{17:27,21:23,27:[1,24],34:[1,26],36:25},{17:28,21:23,27:[1,24],34:[1,26],36:25},{17:29,21:23,27:[1,24],34:[1,26],36:25},{21:30,34:[1,26],36:25},{1:[2,1]},{6:31,8:4,9:5,11:6,12:7,13:8,14:[1,9],15:[1,10],16:[1,12],19:[1,11],22:[1,13],23:[1,14],24:[1,15]},{5:[2,6],14:[2,6],15:[2,6],16:[2,6],19:[2,6],20:[2,6],22:[2,6],23:[2,6],24:[2,6]},{17:22,18:[1,32],21:23,27:[1,24],34:[1,26],36:25},{10:33,20:[1,34]},{10:35,20:[1,34]},{18:[1,36]},{18:[2,24],21:41,25:37,26:38,27:[1,45],28:39,29:[1,42],30:[1,43],31:[1,44],32:40,33:46,34:[1,47],36:25},{18:[2,25]},{18:[2,41],27:[2,41],29:[2,41],30:[2,41],31:[2,41],34:[2,41],37:[1,48]},{18:[2,43],27:[2,43],29:[2,43],30:[2,43],31:[2,43],34:[2,43],37:[2,43]},{18:[1,49]},{18:[1,50]},{18:[1,51]},{18:[1,52],21:53,34:[1,26],36:25},{5:[2,2],8:18,9:5,11:6,12:7,13:8,14:[1,9],15:[1,10],16:[1,12],19:[1,11],20:[2,2],22:[1,13],23:[1,14],24:[1,15]},{14:[2,20],15:[2,20],16:[2,20],19:[2,20],22:[2,20],23:[2,20],24:[2,20]},{5:[2,7],14:[2,7],15:[2,7],16:[2,7],19:[2,7],20:[2,7],22:[2,7],23:[2,7],24:[2,7]},{21:54,34:[1,26],36:25},{5:[2,8],14:[2,8],15:[2,8],16:[2,8],19:[2,8],20:[2,8],22:[2,8],23:[2,8],24:[2,8]},{14:[2,14],15:[2,14],16:[2,14],19:[2,14],20:[2,14],22:[2,14],23:[2,14],24:[2,14]},{18:[2,22],21:41,26:55,27:[1,45],28:56,29:[1,42],30:[1,43],31:[1,44],32:40,33:46,34:[1,47],36:25},{18:[2,23]},{18:[2,27],27:[2,27],29:[2,27],30:[2,27],31:[2,27],34:[2,27]},{18:[2,33],33:57,34:[1,58]},{18:[2,28],27:[2,28],29:[2,28],30:[2,28],31:[2,28],34:[2,28]},{18:[2,29],27:[2,29],29:[2,29],30:[2,29],31:[2,29],34:[2,29]},{18:[2,30],27:[2,30],29:[2,30],30:[2,30],31:[2,30],34:[2,30]},{18:[2,31],27:[2,31],29:[2,31],30:[2,31],31:[2,31],34:[2,31]},{18:[2,32],27:[2,32],29:[2,32],30:[2,32],31:[2,32],34:[2,32]},{18:[2,35],34:[2,35]},{18:[2,43],27:[2,43],29:[2,43],30:[2,43],31:[2,43],34:[2,43],35:[1,59],37:[2,43]},{34:[1,60]},{14:[2,13],15:[2,13],16:[2,13],19:[2,13],20:[2,13],22:[2,13],23:[2,13],24:[2,13]},{5:[2,16],14:[2,16],15:[2,16],16:[2,16],19:[2,16],20:[2,16],22:[2,16],23:[2,16],24:[2,16]},{5:[2,17],14:[2,17],15:[2,17],16:[2,17],19:[2,17],20:[2,17],22:[2,17],23:[2,17],24:[2,17]},{5:[2,18],14:[2,18],15:[2,18],16:[2,18],19:[2,18],20:[2,18],22:[2,18],23:[2,18],24:[2,18]},{18:[1,61]},{18:[1,62]},{18:[2,21]},{18:[2,26],27:[2,26],29:[2,26],30:[2,26],31:[2,26],34:[2,26]},{18:[2,34],34:[2,34]},{35:[1,59]},{21:63,27:[1,67],29:[1,64],30:[1,65],31:[1,66],34:[1,26],36:25},{18:[2,42],27:[2,42],29:[2,42],30:[2,42],31:[2,42],34:[2,42],37:[2,42]},{5:[2,19],14:[2,19],15:[2,19],16:[2,19],19:[2,19],20:[2,19],22:[2,19],23:[2,19],24:[2,19]},{5:[2,15],14:[2,15],15:[2,15],16:[2,15],19:[2,15],20:[2,15],22:[2,15],23:[2,15],24:[2,15]},{18:[2,36],34:[2,36]},{18:[2,37],34:[2,37]},{18:[2,38],34:[2,38]},{18:[2,39],34:[2,39]},{18:[2,40],34:[2,40]}], +defaultActions: {16:[2,1],24:[2,25],38:[2,23],55:[2,21]}, +parseError: function parseError(str, hash) { +    throw new Error(str); +}, +parse: function parse(input) { +    var self = this, stack = [0], vstack = [null], lstack = [], table = this.table, yytext = "", yylineno = 0, yyleng = 0, recovering = 0, TERROR = 2, EOF = 1; +    this.lexer.setInput(input); +    this.lexer.yy = this.yy; +    this.yy.lexer = this.lexer; +    this.yy.parser = this; +    if (typeof this.lexer.yylloc == "undefined") +        this.lexer.yylloc = {}; +    var yyloc = this.lexer.yylloc; +    lstack.push(yyloc); +    var ranges = this.lexer.options && this.lexer.options.ranges; +    if (typeof this.yy.parseError === "function") +        this.parseError = this.yy.parseError; +    function popStack(n) { +        stack.length = stack.length - 2 * n; +        vstack.length = vstack.length - n; +        lstack.length = lstack.length - n; +    } +    function lex() { +        var token; +        token = self.lexer.lex() || 1; +        if (typeof token !== "number") { +            token = self.symbols_[token] || token; +        } +        return token; +    } +    var symbol, preErrorSymbol, state, action, a, r, yyval = {}, p, len, newState, expected; +    while (true) { +        state = stack[stack.length - 1]; +        if (this.defaultActions[state]) { +            action = this.defaultActions[state]; +        } else { +            if (symbol === null || typeof symbol == "undefined") { +                symbol = lex(); +            } +            action = table[state] && table[state][symbol]; +        } +        if (typeof action === "undefined" || !action.length || !action[0]) { +            var errStr = ""; +            if (!recovering) { +                expected = []; +                for (p in table[state]) +                    if (this.terminals_[p] && p > 2) { +                        expected.push("'" + this.terminals_[p] + "'"); +                    } +                if (this.lexer.showPosition) { +                    errStr = "Parse error on line " + (yylineno + 1) + ":\n" + this.lexer.showPosition() + "\nExpecting " + expected.join(", ") + ", got '" + (this.terminals_[symbol] || symbol) + "'"; +                } else { +                    errStr = "Parse error on line " + (yylineno + 1) + ": Unexpected " + (symbol == 1?"end of input":"'" + (this.terminals_[symbol] || symbol) + "'"); +                } +                this.parseError(errStr, {text: this.lexer.match, token: this.terminals_[symbol] || symbol, line: this.lexer.yylineno, loc: yyloc, expected: expected}); +            } +        } +        if (action[0] instanceof Array && action.length > 1) { +            throw new Error("Parse Error: multiple actions possible at state: " + state + ", token: " + symbol); +        } +        switch (action[0]) { +        case 1: +            stack.push(symbol); +            vstack.push(this.lexer.yytext); +            lstack.push(this.lexer.yylloc); +            stack.push(action[1]); +            symbol = null; +            if (!preErrorSymbol) { +                yyleng = this.lexer.yyleng; +                yytext = this.lexer.yytext; +                yylineno = this.lexer.yylineno; +                yyloc = this.lexer.yylloc; +                if (recovering > 0) +                    recovering--; +            } else { +                symbol = preErrorSymbol; +                preErrorSymbol = null; +            } +            break; +        case 2: +            len = this.productions_[action[1]][1]; +            yyval.$ = vstack[vstack.length - len]; +            yyval._$ = {first_line: lstack[lstack.length - (len || 1)].first_line, last_line: lstack[lstack.length - 1].last_line, first_column: lstack[lstack.length - (len || 1)].first_column, last_column: lstack[lstack.length - 1].last_column}; +            if (ranges) { +                yyval._$.range = [lstack[lstack.length - (len || 1)].range[0], lstack[lstack.length - 1].range[1]]; +            } +            r = this.performAction.call(yyval, yytext, yyleng, yylineno, this.yy, action[1], vstack, lstack); +            if (typeof r !== "undefined") { +                return r; +            } +            if (len) { +                stack = stack.slice(0, -1 * len * 2); +                vstack = vstack.slice(0, -1 * len); +                lstack = lstack.slice(0, -1 * len); +            } +            stack.push(this.productions_[action[1]][0]); +            vstack.push(yyval.$); +            lstack.push(yyval._$); +            newState = table[stack[stack.length - 2]][stack[stack.length - 1]]; +            stack.push(newState); +            break; +        case 3: +            return true; +        } +    } +    return true; +} +}; +/* Jison generated lexer */ +var lexer = (function(){ +var lexer = ({EOF:1, +parseError:function parseError(str, hash) { +        if (this.yy.parser) { +            this.yy.parser.parseError(str, hash); +        } else { +            throw new Error(str); +        } +    }, +setInput:function (input) { +        this._input = input; +        this._more = this._less = this.done = false; +        this.yylineno = this.yyleng = 0; +        this.yytext = this.matched = this.match = ''; +        this.conditionStack = ['INITIAL']; +        this.yylloc = {first_line:1,first_column:0,last_line:1,last_column:0}; +        if (this.options.ranges) this.yylloc.range = [0,0]; +        this.offset = 0; +        return this; +    }, +input:function () { +        var ch = this._input[0]; +        this.yytext += ch; +        this.yyleng++; +        this.offset++; +        this.match += ch; +        this.matched += ch; +        var lines = ch.match(/(?:\r\n?|\n).*/g); +        if (lines) { +            this.yylineno++; +            this.yylloc.last_line++; +        } else { +            this.yylloc.last_column++; +        } +        if (this.options.ranges) this.yylloc.range[1]++; + +        this._input = this._input.slice(1); +        return ch; +    }, +unput:function (ch) { +        var len = ch.length; +        var lines = ch.split(/(?:\r\n?|\n)/g); + +        this._input = ch + this._input; +        this.yytext = this.yytext.substr(0, this.yytext.length-len-1); +        //this.yyleng -= len; +        this.offset -= len; +        var oldLines = this.match.split(/(?:\r\n?|\n)/g); +        this.match = this.match.substr(0, this.match.length-1); +        this.matched = this.matched.substr(0, this.matched.length-1); + +        if (lines.length-1) this.yylineno -= lines.length-1; +        var r = this.yylloc.range; + +        this.yylloc = {first_line: this.yylloc.first_line, +          last_line: this.yylineno+1, +          first_column: this.yylloc.first_column, +          last_column: lines ? +              (lines.length === oldLines.length ? this.yylloc.first_column : 0) + oldLines[oldLines.length - lines.length].length - lines[0].length: +              this.yylloc.first_column - len +          }; + +        if (this.options.ranges) { +            this.yylloc.range = [r[0], r[0] + this.yyleng - len]; +        } +        return this; +    }, +more:function () { +        this._more = true; +        return this; +    }, +less:function (n) { +        this.unput(this.match.slice(n)); +    }, +pastInput:function () { +        var past = this.matched.substr(0, this.matched.length - this.match.length); +        return (past.length > 20 ? '...':'') + past.substr(-20).replace(/\n/g, ""); +    }, +upcomingInput:function () { +        var next = this.match; +        if (next.length < 20) { +            next += this._input.substr(0, 20-next.length); +        } +        return (next.substr(0,20)+(next.length > 20 ? '...':'')).replace(/\n/g, ""); +    }, +showPosition:function () { +        var pre = this.pastInput(); +        var c = new Array(pre.length + 1).join("-"); +        return pre + this.upcomingInput() + "\n" + c+"^"; +    }, +next:function () { +        if (this.done) { +            return this.EOF; +        } +        if (!this._input) this.done = true; + +        var token, +            match, +            tempMatch, +            index, +            col, +            lines; +        if (!this._more) { +            this.yytext = ''; +            this.match = ''; +        } +        var rules = this._currentRules(); +        for (var i=0;i < rules.length; i++) { +            tempMatch = this._input.match(this.rules[rules[i]]); +            if (tempMatch && (!match || tempMatch[0].length > match[0].length)) { +                match = tempMatch; +                index = i; +                if (!this.options.flex) break; +            } +        } +        if (match) { +            lines = match[0].match(/(?:\r\n?|\n).*/g); +            if (lines) this.yylineno += lines.length; +            this.yylloc = {first_line: this.yylloc.last_line, +                           last_line: this.yylineno+1, +                           first_column: this.yylloc.last_column, +                           last_column: lines ? lines[lines.length-1].length-lines[lines.length-1].match(/\r?\n?/)[0].length : this.yylloc.last_column + match[0].length}; +            this.yytext += match[0]; +            this.match += match[0]; +            this.matches = match; +            this.yyleng = this.yytext.length; +            if (this.options.ranges) { +                this.yylloc.range = [this.offset, this.offset += this.yyleng]; +            } +            this._more = false; +            this._input = this._input.slice(match[0].length); +            this.matched += match[0]; +            token = this.performAction.call(this, this.yy, this, rules[index],this.conditionStack[this.conditionStack.length-1]); +            if (this.done && this._input) this.done = false; +            if (token) return token; +            else return; +        } +        if (this._input === "") { +            return this.EOF; +        } else { +            return this.parseError('Lexical error on line '+(this.yylineno+1)+'. Unrecognized text.\n'+this.showPosition(), +                    {text: "", token: null, line: this.yylineno}); +        } +    }, +lex:function lex() { +        var r = this.next(); +        if (typeof r !== 'undefined') { +            return r; +        } else { +            return this.lex(); +        } +    }, +begin:function begin(condition) { +        this.conditionStack.push(condition); +    }, +popState:function popState() { +        return this.conditionStack.pop(); +    }, +_currentRules:function _currentRules() { +        return this.conditions[this.conditionStack[this.conditionStack.length-1]].rules; +    }, +topState:function () { +        return this.conditionStack[this.conditionStack.length-2]; +    }, +pushState:function begin(condition) { +        this.begin(condition); +    }}); +lexer.options = {}; +lexer.performAction = function anonymous(yy,yy_,$avoiding_name_collisions,YY_START) { + +var YYSTATE=YY_START +switch($avoiding_name_collisions) { +case 0: +                                   if(yy_.yytext.slice(-1) !== "\\") this.begin("mu"); +                                   if(yy_.yytext.slice(-1) === "\\") yy_.yytext = yy_.yytext.substr(0,yy_.yyleng-1), this.begin("emu"); +                                   if(yy_.yytext) return 14; +                                  +break; +case 1: return 14;  +break; +case 2: +                                   if(yy_.yytext.slice(-1) !== "\\") this.popState(); +                                   if(yy_.yytext.slice(-1) === "\\") yy_.yytext = yy_.yytext.substr(0,yy_.yyleng-1); +                                   return 14; +                                  +break; +case 3: return 24;  +break; +case 4: return 16;  +break; +case 5: return 20;  +break; +case 6: return 19;  +break; +case 7: return 19;  +break; +case 8: return 23;  +break; +case 9: return 23;  +break; +case 10: yy_.yytext = yy_.yytext.substr(3,yy_.yyleng-5); this.popState(); return 15;  +break; +case 11: return 22;  +break; +case 12: return 35;  +break; +case 13: return 34;  +break; +case 14: return 34;  +break; +case 15: return 37;  +break; +case 16: /*ignore whitespace*/  +break; +case 17: this.popState(); return 18;  +break; +case 18: this.popState(); return 18;  +break; +case 19: yy_.yytext = yy_.yytext.substr(1,yy_.yyleng-2).replace(/\\"/g,'"'); return 29;  +break; +case 20: yy_.yytext = yy_.yytext.substr(1,yy_.yyleng-2).replace(/\\"/g,'"'); return 29;  +break; +case 21: yy_.yytext = yy_.yytext.substr(1); return 27;  +break; +case 22: return 31;  +break; +case 23: return 31;  +break; +case 24: return 30;  +break; +case 25: return 34;  +break; +case 26: yy_.yytext = yy_.yytext.substr(1, yy_.yyleng-2); return 34;  +break; +case 27: return 'INVALID';  +break; +case 28: return 5;  +break; +} +}; +lexer.rules = [/^(?:[^\x00]*?(?=(\{\{)))/,/^(?:[^\x00]+)/,/^(?:[^\x00]{2,}?(?=(\{\{|$)))/,/^(?:\{\{>)/,/^(?:\{\{#)/,/^(?:\{\{\/)/,/^(?:\{\{\^)/,/^(?:\{\{\s*else\b)/,/^(?:\{\{\{)/,/^(?:\{\{&)/,/^(?:\{\{![\s\S]*?\}\})/,/^(?:\{\{)/,/^(?:=)/,/^(?:\.(?=[} ]))/,/^(?:\.\.)/,/^(?:[\/.])/,/^(?:\s+)/,/^(?:\}\}\})/,/^(?:\}\})/,/^(?:"(\\["]|[^"])*")/,/^(?:'(\\[']|[^'])*')/,/^(?:@[a-zA-Z]+)/,/^(?:true(?=[}\s]))/,/^(?:false(?=[}\s]))/,/^(?:[0-9]+(?=[}\s]))/,/^(?:[a-zA-Z0-9_$-]+(?=[=}\s\/.]))/,/^(?:\[[^\]]*\])/,/^(?:.)/,/^(?:$)/]; +lexer.conditions = {"mu":{"rules":[3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28],"inclusive":false},"emu":{"rules":[2],"inclusive":false},"INITIAL":{"rules":[0,1,28],"inclusive":true}}; +return lexer;})() +parser.lexer = lexer; +function Parser () { this.yy = {}; }Parser.prototype = parser;parser.Parser = Parser; +return new Parser; +})(); +if (typeof require !== 'undefined' && typeof exports !== 'undefined') { +exports.parser = handlebars; +exports.Parser = handlebars.Parser; +exports.parse = function () { return handlebars.parse.apply(handlebars, arguments); } +exports.main = function commonjsMain(args) { +    if (!args[1]) +        throw new Error('Usage: '+args[0]+' FILE'); +    var source, cwd; +    if (typeof process !== 'undefined') { +        source = require('fs').readFileSync(require('path').resolve(args[1]), "utf8"); +    } else { +        source = require("file").path(require("file").cwd()).join(args[1]).read({charset: "utf-8"}); +    } +    return exports.parser.parse(source); +} +if (typeof module !== 'undefined' && require.main === module) { +  exports.main(typeof process !== 'undefined' ? process.argv.slice(1) : require("system").args); +} +}; +; +// lib/handlebars/compiler/base.js +Handlebars.Parser = handlebars; + +Handlebars.parse = function(string) { +  Handlebars.Parser.yy = Handlebars.AST; +  return Handlebars.Parser.parse(string); +}; + +Handlebars.print = function(ast) { +  return new Handlebars.PrintVisitor().accept(ast); +}; + +Handlebars.logger = { +  DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3, level: 3, + +  // override in the host environment +  log: function(level, str) {} +}; + +Handlebars.log = function(level, str) { Handlebars.logger.log(level, str); }; +; +// lib/handlebars/compiler/ast.js +(function() { + +  Handlebars.AST = {}; + +  Handlebars.AST.ProgramNode = function(statements, inverse) { +    this.type = "program"; +    this.statements = statements; +    if(inverse) { this.inverse = new Handlebars.AST.ProgramNode(inverse); } +  }; + +  Handlebars.AST.MustacheNode = function(rawParams, hash, unescaped) { +    this.type = "mustache"; +    this.escaped = !unescaped; +    this.hash = hash; + +    var id = this.id = rawParams[0]; +    var params = this.params = rawParams.slice(1); + +    // a mustache is an eligible helper if: +    // * its id is simple (a single part, not `this` or `..`) +    var eligibleHelper = this.eligibleHelper = id.isSimple; + +    // a mustache is definitely a helper if: +    // * it is an eligible helper, and +    // * it has at least one parameter or hash segment +    this.isHelper = eligibleHelper && (params.length || hash); + +    // if a mustache is an eligible helper but not a definite +    // helper, it is ambiguous, and will be resolved in a later +    // pass or at runtime. +  }; + +  Handlebars.AST.PartialNode = function(id, context) { +    this.type    = "partial"; + +    // TODO: disallow complex IDs + +    this.id      = id; +    this.context = context; +  }; + +  var verifyMatch = function(open, close) { +    if(open.original !== close.original) { +      throw new Handlebars.Exception(open.original + " doesn't match " + close.original); +    } +  }; + +  Handlebars.AST.BlockNode = function(mustache, program, inverse, close) { +    verifyMatch(mustache.id, close); +    this.type = "block"; +    this.mustache = mustache; +    this.program  = program; +    this.inverse  = inverse; + +    if (this.inverse && !this.program) { +      this.isInverse = true; +    } +  }; + +  Handlebars.AST.ContentNode = function(string) { +    this.type = "content"; +    this.string = string; +  }; + +  Handlebars.AST.HashNode = function(pairs) { +    this.type = "hash"; +    this.pairs = pairs; +  }; + +  Handlebars.AST.IdNode = function(parts) { +    this.type = "ID"; +    this.original = parts.join("."); + +    var dig = [], depth = 0; + +    for(var i=0,l=parts.length; i<l; i++) { +      var part = parts[i]; + +      if(part === "..") { depth++; } +      else if(part === "." || part === "this") { this.isScoped = true; } +      else { dig.push(part); } +    } + +    this.parts    = dig; +    this.string   = dig.join('.'); +    this.depth    = depth; + +    // an ID is simple if it only has one part, and that part is not +    // `..` or `this`. +    this.isSimple = parts.length === 1 && !this.isScoped && depth === 0; +  }; + +  Handlebars.AST.DataNode = function(id) { +    this.type = "DATA"; +    this.id = id; +  }; + +  Handlebars.AST.StringNode = function(string) { +    this.type = "STRING"; +    this.string = string; +  }; + +  Handlebars.AST.IntegerNode = function(integer) { +    this.type = "INTEGER"; +    this.integer = integer; +  }; + +  Handlebars.AST.BooleanNode = function(bool) { +    this.type = "BOOLEAN"; +    this.bool = bool; +  }; + +  Handlebars.AST.CommentNode = function(comment) { +    this.type = "comment"; +    this.comment = comment; +  }; + +})();; +// lib/handlebars/utils.js +Handlebars.Exception = function(message) { +  var tmp = Error.prototype.constructor.apply(this, arguments); + +  for (var p in tmp) { +    if (tmp.hasOwnProperty(p)) { this[p] = tmp[p]; } +  } + +  this.message = tmp.message; +}; +Handlebars.Exception.prototype = new Error(); + +// Build out our basic SafeString type +Handlebars.SafeString = function(string) { +  this.string = string; +}; +Handlebars.SafeString.prototype.toString = function() { +  return this.string.toString(); +}; + +(function() { +  var escape = { +    "&": "&", +    "<": "<", +    ">": ">", +    '"': """, +    "'": "'", +    "`": "`" +  }; + +  var badChars = /[&<>"'`]/g; +  var possible = /[&<>"'`]/; + +  var escapeChar = function(chr) { +    return escape[chr] || "&"; +  }; + +  Handlebars.Utils = { +    escapeExpression: function(string) { +      // don't escape SafeStrings, since they're already safe +      if (string instanceof Handlebars.SafeString) { +        return string.toString(); +      } else if (string == null || string === false) { +        return ""; +      } + +      if(!possible.test(string)) { return string; } +      return string.replace(badChars, escapeChar); +    }, + +    isEmpty: function(value) { +      if (typeof value === "undefined") { +        return true; +      } else if (value === null) { +        return true; +      } else if (value === false) { +        return true; +      } else if(Object.prototype.toString.call(value) === "[object Array]" && value.length === 0) { +        return true; +      } else { +        return false; +      } +    } +  }; +})();; +// lib/handlebars/compiler/compiler.js + +/*jshint eqnull:true*/ +Handlebars.Compiler = function() {}; +Handlebars.JavaScriptCompiler = function() {}; + +(function(Compiler, JavaScriptCompiler) { +  // the foundHelper register will disambiguate helper lookup from finding a +  // function in a context. This is necessary for mustache compatibility, which +  // requires that context functions in blocks are evaluated by blockHelperMissing, +  // and then proceed as if the resulting value was provided to blockHelperMissing. + +  Compiler.prototype = { +    compiler: Compiler, + +    disassemble: function() { +      var opcodes = this.opcodes, opcode, out = [], params, param; + +      for (var i=0, l=opcodes.length; i<l; i++) { +        opcode = opcodes[i]; + +        if (opcode.opcode === 'DECLARE') { +          out.push("DECLARE " + opcode.name + "=" + opcode.value); +        } else { +          params = []; +          for (var j=0; j<opcode.args.length; j++) { +            param = opcode.args[j]; +            if (typeof param === "string") { +              param = "\"" + param.replace("\n", "\\n") + "\""; +            } +            params.push(param); +          } +          out.push(opcode.opcode + " " + params.join(" ")); +        } +      } + +      return out.join("\n"); +    }, + +    guid: 0, + +    compile: function(program, options) { +      this.children = []; +      this.depths = {list: []}; +      this.options = options; + +      // These changes will propagate to the other compiler components +      var knownHelpers = this.options.knownHelpers; +      this.options.knownHelpers = { +        'helperMissing': true, +        'blockHelperMissing': true, +        'each': true, +        'if': true, +        'unless': true, +        'with': true, +        'log': true +      }; +      if (knownHelpers) { +        for (var name in knownHelpers) { +          this.options.knownHelpers[name] = knownHelpers[name]; +        } +      } + +      return this.program(program); +    }, + +    accept: function(node) { +      return this[node.type](node); +    }, + +    program: function(program) { +      var statements = program.statements, statement; +      this.opcodes = []; + +      for(var i=0, l=statements.length; i<l; i++) { +        statement = statements[i]; +        this[statement.type](statement); +      } +      this.isSimple = l === 1; + +      this.depths.list = this.depths.list.sort(function(a, b) { +        return a - b; +      }); + +      return this; +    }, + +    compileProgram: function(program) { +      var result = new this.compiler().compile(program, this.options); +      var guid = this.guid++, depth; + +      this.usePartial = this.usePartial || result.usePartial; + +      this.children[guid] = result; + +      for(var i=0, l=result.depths.list.length; i<l; i++) { +        depth = result.depths.list[i]; + +        if(depth < 2) { continue; } +        else { this.addDepth(depth - 1); } +      } + +      return guid; +    }, + +    block: function(block) { +      var mustache = block.mustache, +          program = block.program, +          inverse = block.inverse; + +      if (program) { +        program = this.compileProgram(program); +      } + +      if (inverse) { +        inverse = this.compileProgram(inverse); +      } + +      var type = this.classifyMustache(mustache); + +      if (type === "helper") { +        this.helperMustache(mustache, program, inverse); +      } else if (type === "simple") { +        this.simpleMustache(mustache); + +        // now that the simple mustache is resolved, we need to +        // evaluate it by executing `blockHelperMissing` +        this.opcode('pushProgram', program); +        this.opcode('pushProgram', inverse); +        this.opcode('pushLiteral', '{}'); +        this.opcode('blockValue'); +      } else { +        this.ambiguousMustache(mustache, program, inverse); + +        // now that the simple mustache is resolved, we need to +        // evaluate it by executing `blockHelperMissing` +        this.opcode('pushProgram', program); +        this.opcode('pushProgram', inverse); +        this.opcode('pushLiteral', '{}'); +        this.opcode('ambiguousBlockValue'); +      } + +      this.opcode('append'); +    }, + +    hash: function(hash) { +      var pairs = hash.pairs, pair, val; + +      this.opcode('push', '{}'); + +      for(var i=0, l=pairs.length; i<l; i++) { +        pair = pairs[i]; +        val  = pair[1]; + +        this.accept(val); +        this.opcode('assignToHash', pair[0]); +      } +    }, + +    partial: function(partial) { +      var id = partial.id; +      this.usePartial = true; + +      if(partial.context) { +        this.ID(partial.context); +      } else { +        this.opcode('push', 'depth0'); +      } + +      this.opcode('invokePartial', id.original); +      this.opcode('append'); +    }, + +    content: function(content) { +      this.opcode('appendContent', content.string); +    }, + +    mustache: function(mustache) { +      var options = this.options; +      var type = this.classifyMustache(mustache); + +      if (type === "simple") { +        this.simpleMustache(mustache); +      } else if (type === "helper") { +        this.helperMustache(mustache); +      } else { +        this.ambiguousMustache(mustache); +      } + +      if(mustache.escaped && !options.noEscape) { +        this.opcode('appendEscaped'); +      } else { +        this.opcode('append'); +      } +    }, + +    ambiguousMustache: function(mustache, program, inverse) { +      var id = mustache.id, name = id.parts[0]; + +      this.opcode('getContext', id.depth); + +      this.opcode('pushProgram', program); +      this.opcode('pushProgram', inverse); + +      this.opcode('invokeAmbiguous', name); +    }, + +    simpleMustache: function(mustache, program, inverse) { +      var id = mustache.id; + +      if (id.type === 'DATA') { +        this.DATA(id); +      } else if (id.parts.length) { +        this.ID(id); +      } else { +        // Simplified ID for `this` +        this.addDepth(id.depth); +        this.opcode('getContext', id.depth); +        this.opcode('pushContext'); +      } + +      this.opcode('resolvePossibleLambda'); +    }, + +    helperMustache: function(mustache, program, inverse) { +      var params = this.setupFullMustacheParams(mustache, program, inverse), +          name = mustache.id.parts[0]; + +      if (this.options.knownHelpers[name]) { +        this.opcode('invokeKnownHelper', params.length, name); +      } else if (this.knownHelpersOnly) { +        throw new Error("You specified knownHelpersOnly, but used the unknown helper " + name); +      } else { +        this.opcode('invokeHelper', params.length, name); +      } +    }, + +    ID: function(id) { +      this.addDepth(id.depth); +      this.opcode('getContext', id.depth); + +      var name = id.parts[0]; +      if (!name) { +        this.opcode('pushContext'); +      } else { +        this.opcode('lookupOnContext', id.parts[0]); +      } + +      for(var i=1, l=id.parts.length; i<l; i++) { +        this.opcode('lookup', id.parts[i]); +      } +    }, + +    DATA: function(data) { +      this.options.data = true; +      this.opcode('lookupData', data.id); +    }, + +    STRING: function(string) { +      this.opcode('pushString', string.string); +    }, + +    INTEGER: function(integer) { +      this.opcode('pushLiteral', integer.integer); +    }, + +    BOOLEAN: function(bool) { +      this.opcode('pushLiteral', bool.bool); +    }, + +    comment: function() {}, + +    // HELPERS +    opcode: function(name) { +      this.opcodes.push({ opcode: name, args: [].slice.call(arguments, 1) }); +    }, + +    declare: function(name, value) { +      this.opcodes.push({ opcode: 'DECLARE', name: name, value: value }); +    }, + +    addDepth: function(depth) { +      if(isNaN(depth)) { throw new Error("EWOT"); } +      if(depth === 0) { return; } + +      if(!this.depths[depth]) { +        this.depths[depth] = true; +        this.depths.list.push(depth); +      } +    }, + +    classifyMustache: function(mustache) { +      var isHelper   = mustache.isHelper; +      var isEligible = mustache.eligibleHelper; +      var options    = this.options; + +      // if ambiguous, we can possibly resolve the ambiguity now +      if (isEligible && !isHelper) { +        var name = mustache.id.parts[0]; + +        if (options.knownHelpers[name]) { +          isHelper = true; +        } else if (options.knownHelpersOnly) { +          isEligible = false; +        } +      } + +      if (isHelper) { return "helper"; } +      else if (isEligible) { return "ambiguous"; } +      else { return "simple"; } +    }, + +    pushParams: function(params) { +      var i = params.length, param; + +      while(i--) { +        param = params[i]; + +        if(this.options.stringParams) { +          if(param.depth) { +            this.addDepth(param.depth); +          } + +          this.opcode('getContext', param.depth || 0); +          this.opcode('pushStringParam', param.string); +        } else { +          this[param.type](param); +        } +      } +    }, + +    setupMustacheParams: function(mustache) { +      var params = mustache.params; +      this.pushParams(params); + +      if(mustache.hash) { +        this.hash(mustache.hash); +      } else { +        this.opcode('pushLiteral', '{}'); +      } + +      return params; +    }, + +    // this will replace setupMustacheParams when we're done +    setupFullMustacheParams: function(mustache, program, inverse) { +      var params = mustache.params; +      this.pushParams(params); + +      this.opcode('pushProgram', program); +      this.opcode('pushProgram', inverse); + +      if(mustache.hash) { +        this.hash(mustache.hash); +      } else { +        this.opcode('pushLiteral', '{}'); +      } + +      return params; +    } +  }; + +  var Literal = function(value) { +    this.value = value; +  }; + +  JavaScriptCompiler.prototype = { +    // PUBLIC API: You can override these methods in a subclass to provide +    // alternative compiled forms for name lookup and buffering semantics +    nameLookup: function(parent, name, type) { +      if (/^[0-9]+$/.test(name)) { +        return parent + "[" + name + "]"; +      } else if (JavaScriptCompiler.isValidJavaScriptVariableName(name)) { +        return parent + "." + name; +      } +      else { +        return parent + "['" + name + "']"; +      } +    }, + +    appendToBuffer: function(string) { +      if (this.environment.isSimple) { +        return "return " + string + ";"; +      } else { +        return "buffer += " + string + ";"; +      } +    }, + +    initializeBuffer: function() { +      return this.quotedString(""); +    }, + +    namespace: "Handlebars", +    // END PUBLIC API + +    compile: function(environment, options, context, asObject) { +      this.environment = environment; +      this.options = options || {}; + +      Handlebars.log(Handlebars.logger.DEBUG, this.environment.disassemble() + "\n\n"); + +      this.name = this.environment.name; +      this.isChild = !!context; +      this.context = context || { +        programs: [], +        aliases: { } +      }; + +      this.preamble(); + +      this.stackSlot = 0; +      this.stackVars = []; +      this.registers = { list: [] }; +      this.compileStack = []; + +      this.compileChildren(environment, options); + +      var opcodes = environment.opcodes, opcode; + +      this.i = 0; + +      for(l=opcodes.length; this.i<l; this.i++) { +        opcode = opcodes[this.i]; + +        if(opcode.opcode === 'DECLARE') { +          this[opcode.name] = opcode.value; +        } else { +          this[opcode.opcode].apply(this, opcode.args); +        } +      } + +      return this.createFunctionContext(asObject); +    }, + +    nextOpcode: function() { +      var opcodes = this.environment.opcodes, opcode = opcodes[this.i + 1]; +      return opcodes[this.i + 1]; +    }, + +    eat: function(opcode) { +      this.i = this.i + 1; +    }, + +    preamble: function() { +      var out = []; + +      if (!this.isChild) { +        var namespace = this.namespace; +        var copies = "helpers = helpers || " + namespace + ".helpers;"; +        if (this.environment.usePartial) { copies = copies + " partials = partials || " + namespace + ".partials;"; } +        if (this.options.data) { copies = copies + " data = data || {};"; } +        out.push(copies); +      } else { +        out.push(''); +      } + +      if (!this.environment.isSimple) { +        out.push(", buffer = " + this.initializeBuffer()); +      } else { +        out.push(""); +      } + +      // track the last context pushed into place to allow skipping the +      // getContext opcode when it would be a noop +      this.lastContext = 0; +      this.source = out; +    }, + +    createFunctionContext: function(asObject) { +      var locals = this.stackVars.concat(this.registers.list); + +      if(locals.length > 0) { +        this.source[1] = this.source[1] + ", " + locals.join(", "); +      } + +      // Generate minimizer alias mappings +      if (!this.isChild) { +        var aliases = []; +        for (var alias in this.context.aliases) { +          this.source[1] = this.source[1] + ', ' + alias + '=' + this.context.aliases[alias]; +        } +      } + +      if (this.source[1]) { +        this.source[1] = "var " + this.source[1].substring(2) + ";"; +      } + +      // Merge children +      if (!this.isChild) { +        this.source[1] += '\n' + this.context.programs.join('\n') + '\n'; +      } + +      if (!this.environment.isSimple) { +        this.source.push("return buffer;"); +      } + +      var params = this.isChild ? ["depth0", "data"] : ["Handlebars", "depth0", "helpers", "partials", "data"]; + +      for(var i=0, l=this.environment.depths.list.length; i<l; i++) { +        params.push("depth" + this.environment.depths.list[i]); +      } + +      if (asObject) { +        params.push(this.source.join("\n  ")); + +        return Function.apply(this, params); +      } else { +        var functionSource = 'function ' + (this.name || '') + '(' + params.join(',') + ') {\n  ' + this.source.join("\n  ") + '}'; +        Handlebars.log(Handlebars.logger.DEBUG, functionSource + "\n\n"); +        return functionSource; +      } +    }, + +    // [blockValue] +    // +    // On stack, before: hash, inverse, program, value +    // On stack, after: return value of blockHelperMissing +    // +    // The purpose of this opcode is to take a block of the form +    // `{{#foo}}...{{/foo}}`, resolve the value of `foo`, and +    // replace it on the stack with the result of properly +    // invoking blockHelperMissing. +    blockValue: function() { +      this.context.aliases.blockHelperMissing = 'helpers.blockHelperMissing'; + +      var params = ["depth0"]; +      this.setupParams(0, params); + +      this.replaceStack(function(current) { +        params.splice(1, 0, current); +        return current + " = blockHelperMissing.call(" + params.join(", ") + ")"; +      }); +    }, + +    // [ambiguousBlockValue] +    // +    // On stack, before: hash, inverse, program, value +    // Compiler value, before: lastHelper=value of last found helper, if any +    // On stack, after, if no lastHelper: same as [blockValue] +    // On stack, after, if lastHelper: value +    ambiguousBlockValue: function() { +      this.context.aliases.blockHelperMissing = 'helpers.blockHelperMissing'; + +      var params = ["depth0"]; +      this.setupParams(0, params); + +      var current = this.topStack(); +      params.splice(1, 0, current); + +      this.source.push("if (!" + this.lastHelper + ") { " + current + " = blockHelperMissing.call(" + params.join(", ") + "); }"); +    }, + +    // [appendContent] +    // +    // On stack, before: ... +    // On stack, after: ... +    // +    // Appends the string value of `content` to the current buffer +    appendContent: function(content) { +      this.source.push(this.appendToBuffer(this.quotedString(content))); +    }, + +    // [append] +    // +    // On stack, before: value, ... +    // On stack, after: ... +    // +    // Coerces `value` to a String and appends it to the current buffer. +    // +    // If `value` is truthy, or 0, it is coerced into a string and appended +    // Otherwise, the empty string is appended +    append: function() { +      var local = this.popStack(); +      this.source.push("if(" + local + " || " + local + " === 0) { " + this.appendToBuffer(local) + " }"); +      if (this.environment.isSimple) { +        this.source.push("else { " + this.appendToBuffer("''") + " }"); +      } +    }, + +    // [appendEscaped] +    // +    // On stack, before: value, ... +    // On stack, after: ... +    // +    // Escape `value` and append it to the buffer +    appendEscaped: function() { +      var opcode = this.nextOpcode(), extra = ""; +      this.context.aliases.escapeExpression = 'this.escapeExpression'; + +      if(opcode && opcode.opcode === 'appendContent') { +        extra = " + " + this.quotedString(opcode.args[0]); +        this.eat(opcode); +      } + +      this.source.push(this.appendToBuffer("escapeExpression(" + this.popStack() + ")" + extra)); +    }, + +    // [getContext] +    // +    // On stack, before: ... +    // On stack, after: ... +    // Compiler value, after: lastContext=depth +    // +    // Set the value of the `lastContext` compiler value to the depth +    getContext: function(depth) { +      if(this.lastContext !== depth) { +        this.lastContext = depth; +      } +    }, + +    // [lookupOnContext] +    // +    // On stack, before: ... +    // On stack, after: currentContext[name], ... +    // +    // Looks up the value of `name` on the current context and pushes +    // it onto the stack. +    lookupOnContext: function(name) { +      this.pushStack(this.nameLookup('depth' + this.lastContext, name, 'context')); +    }, + +    // [pushContext] +    // +    // On stack, before: ... +    // On stack, after: currentContext, ... +    // +    // Pushes the value of the current context onto the stack. +    pushContext: function() { +      this.pushStackLiteral('depth' + this.lastContext); +    }, + +    // [resolvePossibleLambda] +    // +    // On stack, before: value, ... +    // On stack, after: resolved value, ... +    // +    // If the `value` is a lambda, replace it on the stack by +    // the return value of the lambda +    resolvePossibleLambda: function() { +      this.context.aliases.functionType = '"function"'; + +      this.replaceStack(function(current) { +        return "typeof " + current + " === functionType ? " + current + "() : " + current; +      }); +    }, + +    // [lookup] +    // +    // On stack, before: value, ... +    // On stack, after: value[name], ... +    // +    // Replace the value on the stack with the result of looking +    // up `name` on `value` +    lookup: function(name) { +      this.replaceStack(function(current) { +        return current + " == null || " + current + " === false ? " + current + " : " + this.nameLookup(current, name, 'context'); +      }); +    }, + +    // [lookupData] +    // +    // On stack, before: ... +    // On stack, after: data[id], ... +    // +    // Push the result of looking up `id` on the current data +    lookupData: function(id) { +      this.pushStack(this.nameLookup('data', id, 'data')); +    }, + +    // [pushStringParam] +    // +    // On stack, before: ... +    // On stack, after: string, currentContext, ... +    // +    // This opcode is designed for use in string mode, which +    // provides the string value of a parameter along with its +    // depth rather than resolving it immediately. +    pushStringParam: function(string) { +      this.pushStackLiteral('depth' + this.lastContext); +      this.pushString(string); +    }, + +    // [pushString] +    // +    // On stack, before: ... +    // On stack, after: quotedString(string), ... +    // +    // Push a quoted version of `string` onto the stack +    pushString: function(string) { +      this.pushStackLiteral(this.quotedString(string)); +    }, + +    // [push] +    // +    // On stack, before: ... +    // On stack, after: expr, ... +    // +    // Push an expression onto the stack +    push: function(expr) { +      this.pushStack(expr); +    }, + +    // [pushLiteral] +    // +    // On stack, before: ... +    // On stack, after: value, ... +    // +    // Pushes a value onto the stack. This operation prevents +    // the compiler from creating a temporary variable to hold +    // it. +    pushLiteral: function(value) { +      this.pushStackLiteral(value); +    }, + +    // [pushProgram] +    // +    // On stack, before: ... +    // On stack, after: program(guid), ... +    // +    // Push a program expression onto the stack. This takes +    // a compile-time guid and converts it into a runtime-accessible +    // expression. +    pushProgram: function(guid) { +      if (guid != null) { +        this.pushStackLiteral(this.programExpression(guid)); +      } else { +        this.pushStackLiteral(null); +      } +    }, + +    // [invokeHelper] +    // +    // On stack, before: hash, inverse, program, params..., ... +    // On stack, after: result of helper invocation +    // +    // Pops off the helper's parameters, invokes the helper, +    // and pushes the helper's return value onto the stack. +    // +    // If the helper is not found, `helperMissing` is called. +    invokeHelper: function(paramSize, name) { +      this.context.aliases.helperMissing = 'helpers.helperMissing'; + +      var helper = this.lastHelper = this.setupHelper(paramSize, name); +      this.register('foundHelper', helper.name); + +      this.pushStack("foundHelper ? foundHelper.call(" + +        helper.callParams + ") " + ": helperMissing.call(" + +        helper.helperMissingParams + ")"); +    }, + +    // [invokeKnownHelper] +    // +    // On stack, before: hash, inverse, program, params..., ... +    // On stack, after: result of helper invocation +    // +    // This operation is used when the helper is known to exist, +    // so a `helperMissing` fallback is not required. +    invokeKnownHelper: function(paramSize, name) { +      var helper = this.setupHelper(paramSize, name); +      this.pushStack(helper.name + ".call(" + helper.callParams + ")"); +    }, + +    // [invokeAmbiguous] +    // +    // On stack, before: hash, inverse, program, params..., ... +    // On stack, after: result of disambiguation +    // +    // This operation is used when an expression like `{{foo}}` +    // is provided, but we don't know at compile-time whether it +    // is a helper or a path. +    // +    // This operation emits more code than the other options, +    // and can be avoided by passing the `knownHelpers` and +    // `knownHelpersOnly` flags at compile-time. +    invokeAmbiguous: function(name) { +      this.context.aliases.functionType = '"function"'; + +      this.pushStackLiteral('{}'); +      var helper = this.setupHelper(0, name); + +      var helperName = this.lastHelper = this.nameLookup('helpers', name, 'helper'); +      this.register('foundHelper', helperName); + +      var nonHelper = this.nameLookup('depth' + this.lastContext, name, 'context'); +      var nextStack = this.nextStack(); + +      this.source.push('if (foundHelper) { ' + nextStack + ' = foundHelper.call(' + helper.callParams + '); }'); +      this.source.push('else { ' + nextStack + ' = ' + nonHelper + '; ' + nextStack + ' = typeof ' + nextStack + ' === functionType ? ' + nextStack + '() : ' + nextStack + '; }'); +    }, + +    // [invokePartial] +    // +    // On stack, before: context, ... +    // On stack after: result of partial invocation +    // +    // This operation pops off a context, invokes a partial with that context, +    // and pushes the result of the invocation back. +    invokePartial: function(name) { +      var params = [this.nameLookup('partials', name, 'partial'), "'" + name + "'", this.popStack(), "helpers", "partials"]; + +      if (this.options.data) { +        params.push("data"); +      } + +      this.context.aliases.self = "this"; +      this.pushStack("self.invokePartial(" + params.join(", ") + ");"); +    }, + +    // [assignToHash] +    // +    // On stack, before: value, hash, ... +    // On stack, after: hash, ... +    // +    // Pops a value and hash off the stack, assigns `hash[key] = value` +    // and pushes the hash back onto the stack. +    assignToHash: function(key) { +      var value = this.popStack(); +      var hash = this.topStack(); + +      this.source.push(hash + "['" + key + "'] = " + value + ";"); +    }, + +    // HELPERS + +    compiler: JavaScriptCompiler, + +    compileChildren: function(environment, options) { +      var children = environment.children, child, compiler; + +      for(var i=0, l=children.length; i<l; i++) { +        child = children[i]; +        compiler = new this.compiler(); + +        this.context.programs.push('');     // Placeholder to prevent name conflicts for nested children +        var index = this.context.programs.length; +        child.index = index; +        child.name = 'program' + index; +        this.context.programs[index] = compiler.compile(child, options, this.context); +      } +    }, + +    programExpression: function(guid) { +      this.context.aliases.self = "this"; + +      if(guid == null) { +        return "self.noop"; +      } + +      var child = this.environment.children[guid], +          depths = child.depths.list, depth; + +      var programParams = [child.index, child.name, "data"]; + +      for(var i=0, l = depths.length; i<l; i++) { +        depth = depths[i]; + +        if(depth === 1) { programParams.push("depth0"); } +        else { programParams.push("depth" + (depth - 1)); } +      } + +      if(depths.length === 0) { +        return "self.program(" + programParams.join(", ") + ")"; +      } else { +        programParams.shift(); +        return "self.programWithDepth(" + programParams.join(", ") + ")"; +      } +    }, + +    register: function(name, val) { +      this.useRegister(name); +      this.source.push(name + " = " + val + ";"); +    }, + +    useRegister: function(name) { +      if(!this.registers[name]) { +        this.registers[name] = true; +        this.registers.list.push(name); +      } +    }, + +    pushStackLiteral: function(item) { +      this.compileStack.push(new Literal(item)); +      return item; +    }, + +    pushStack: function(item) { +      this.source.push(this.incrStack() + " = " + item + ";"); +      this.compileStack.push("stack" + this.stackSlot); +      return "stack" + this.stackSlot; +    }, + +    replaceStack: function(callback) { +      var item = callback.call(this, this.topStack()); + +      this.source.push(this.topStack() + " = " + item + ";"); +      return "stack" + this.stackSlot; +    }, + +    nextStack: function(skipCompileStack) { +      var name = this.incrStack(); +      this.compileStack.push("stack" + this.stackSlot); +      return name; +    }, + +    incrStack: function() { +      this.stackSlot++; +      if(this.stackSlot > this.stackVars.length) { this.stackVars.push("stack" + this.stackSlot); } +      return "stack" + this.stackSlot; +    }, + +    popStack: function() { +      var item = this.compileStack.pop(); + +      if (item instanceof Literal) { +        return item.value; +      } else { +        this.stackSlot--; +        return item; +      } +    }, + +    topStack: function() { +      var item = this.compileStack[this.compileStack.length - 1]; + +      if (item instanceof Literal) { +        return item.value; +      } else { +        return item; +      } +    }, + +    quotedString: function(str) { +      return '"' + str +        .replace(/\\/g, '\\\\') +        .replace(/"/g, '\\"') +        .replace(/\n/g, '\\n') +        .replace(/\r/g, '\\r') + '"'; +    }, + +    setupHelper: function(paramSize, name) { +      var params = []; +      this.setupParams(paramSize, params); +      var foundHelper = this.nameLookup('helpers', name, 'helper'); + +      return { +        params: params, +        name: foundHelper, +        callParams: ["depth0"].concat(params).join(", "), +        helperMissingParams: ["depth0", this.quotedString(name)].concat(params).join(", ") +      }; +    }, + +    // the params and contexts arguments are passed in arrays +    // to fill in +    setupParams: function(paramSize, params) { +      var options = [], contexts = [], param, inverse, program; + +      options.push("hash:" + this.popStack()); + +      inverse = this.popStack(); +      program = this.popStack(); + +      // Avoid setting fn and inverse if neither are set. This allows +      // helpers to do a check for `if (options.fn)` +      if (program || inverse) { +        if (!program) { +          this.context.aliases.self = "this"; +          program = "self.noop"; +        } + +        if (!inverse) { +         this.context.aliases.self = "this"; +          inverse = "self.noop"; +        } + +        options.push("inverse:" + inverse); +        options.push("fn:" + program); +      } + +      for(var i=0; i<paramSize; i++) { +        param = this.popStack(); +        params.push(param); + +        if(this.options.stringParams) { +          contexts.push(this.popStack()); +        } +      } + +      if (this.options.stringParams) { +        options.push("contexts:[" + contexts.join(",") + "]"); +      } + +      if(this.options.data) { +        options.push("data:data"); +      } + +      params.push("{" + options.join(",") + "}"); +      return params.join(", "); +    } +  }; + +  var reservedWords = ( +    "break else new var" + +    " case finally return void" + +    " catch for switch while" + +    " continue function this with" + +    " default if throw" + +    " delete in try" + +    " do instanceof typeof" + +    " abstract enum int short" + +    " boolean export interface static" + +    " byte extends long super" + +    " char final native synchronized" + +    " class float package throws" + +    " const goto private transient" + +    " debugger implements protected volatile" + +    " double import public let yield" +  ).split(" "); + +  var compilerWords = JavaScriptCompiler.RESERVED_WORDS = {}; + +  for(var i=0, l=reservedWords.length; i<l; i++) { +    compilerWords[reservedWords[i]] = true; +  } + +  JavaScriptCompiler.isValidJavaScriptVariableName = function(name) { +    if(!JavaScriptCompiler.RESERVED_WORDS[name] && /^[a-zA-Z_$][0-9a-zA-Z_$]+$/.test(name)) { +      return true; +    } +    return false; +  }; + +})(Handlebars.Compiler, Handlebars.JavaScriptCompiler); + +Handlebars.precompile = function(string, options) { +  options = options || {}; + +  var ast = Handlebars.parse(string); +  var environment = new Handlebars.Compiler().compile(ast, options); +  return new Handlebars.JavaScriptCompiler().compile(environment, options); +}; + +Handlebars.compile = function(string, options) { +  options = options || {}; + +  var compiled; +  function compile() { +    var ast = Handlebars.parse(string); +    var environment = new Handlebars.Compiler().compile(ast, options); +    var templateSpec = new Handlebars.JavaScriptCompiler().compile(environment, options, undefined, true); +    return Handlebars.template(templateSpec); +  } + +  // Template is only compiled on first use and cached after that point. +  return function(context, options) { +    if (!compiled) { +      compiled = compile(); +    } +    return compiled.call(this, context, options); +  }; +}; +; +// lib/handlebars/runtime.js +Handlebars.VM = { +  template: function(templateSpec) { +    // Just add water +    var container = { +      escapeExpression: Handlebars.Utils.escapeExpression, +      invokePartial: Handlebars.VM.invokePartial, +      programs: [], +      program: function(i, fn, data) { +        var programWrapper = this.programs[i]; +        if(data) { +          return Handlebars.VM.program(fn, data); +        } else if(programWrapper) { +          return programWrapper; +        } else { +          programWrapper = this.programs[i] = Handlebars.VM.program(fn); +          return programWrapper; +        } +      }, +      programWithDepth: Handlebars.VM.programWithDepth, +      noop: Handlebars.VM.noop +    }; + +    return function(context, options) { +      options = options || {}; +      return templateSpec.call(container, Handlebars, context, options.helpers, options.partials, options.data); +    }; +  }, + +  programWithDepth: function(fn, data, $depth) { +    var args = Array.prototype.slice.call(arguments, 2); + +    return function(context, options) { +      options = options || {}; + +      return fn.apply(this, [context, options.data || data].concat(args)); +    }; +  }, +  program: function(fn, data) { +    return function(context, options) { +      options = options || {}; + +      return fn(context, options.data || data); +    }; +  }, +  noop: function() { return ""; }, +  invokePartial: function(partial, name, context, helpers, partials, data) { +    var options = { helpers: helpers, partials: partials, data: data }; + +    if(partial === undefined) { +      throw new Handlebars.Exception("The partial " + name + " could not be found"); +    } else if(partial instanceof Function) { +      return partial(context, options); +    } else if (!Handlebars.compile) { +      throw new Handlebars.Exception("The partial " + name + " could not be compiled when running in runtime-only mode"); +    } else { +      partials[name] = Handlebars.compile(partial, {data: data !== undefined}); +      return partials[name](context, options); +    } +  } +}; + +Handlebars.template = Handlebars.VM.template; +; + +// AMD Define +define(function(){ +    return Handlebars; +}); + +})(); diff --git a/pyload/web/app/scripts/vendor/bootstrap-2.3.2.js b/pyload/web/app/scripts/vendor/bootstrap-2.3.2.js new file mode 100755 index 000000000..96fed1387 --- /dev/null +++ b/pyload/web/app/scripts/vendor/bootstrap-2.3.2.js @@ -0,0 +1,2291 @@ +/* =================================================== + * bootstrap-transition.js v2.3.2 + * http://twitter.github.com/bootstrap/javascript.html#transitions + * =================================================== + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================================================== */ + + +!function ($) { + +  "use strict"; // jshint ;_; + + +  /* CSS TRANSITION SUPPORT (http://www.modernizr.com/) +   * ======================================================= */ + +  $(function () { + +    $.support.transition = (function () { + +      var transitionEnd = (function () { + +        var el = document.createElement('bootstrap') +          , transEndEventNames = { +               'WebkitTransition' : 'webkitTransitionEnd' +            ,  'MozTransition'    : 'transitionend' +            ,  'OTransition'      : 'oTransitionEnd otransitionend' +            ,  'transition'       : 'transitionend' +            } +          , name + +        for (name in transEndEventNames){ +          if (el.style[name] !== undefined) { +            return transEndEventNames[name] +          } +        } + +      }()) + +      return transitionEnd && { +        end: transitionEnd +      } + +    })() + +  }) + +}(window.jQuery); +/* ========================================================= + * bootstrap-modal.js v2.3.2 + * http://twitter.github.com/bootstrap/javascript.html#modals + * ========================================================= + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================================================= */ + + +!function ($) { + +  "use strict"; // jshint ;_; + + + /* MODAL CLASS DEFINITION +  * ====================== */ + +  var Modal = function (element, options) { +    this.options = options +    this.$element = $(element) +      .delegate('[data-dismiss="modal"]', 'click.dismiss.modal', $.proxy(this.hide, this)) +    this.options.remote && this.$element.find('.modal-body').load(this.options.remote) +  } + +  Modal.prototype = { + +      constructor: Modal + +    , toggle: function () { +        return this[!this.isShown ? 'show' : 'hide']() +      } + +    , show: function () { +        var that = this +          , e = $.Event('show') + +        this.$element.trigger(e) + +        if (this.isShown || e.isDefaultPrevented()) return + +        this.isShown = true + +        this.escape() + +        this.backdrop(function () { +          var transition = $.support.transition && that.$element.hasClass('fade') + +          if (!that.$element.parent().length) { +            that.$element.appendTo(document.body) //don't move modals dom position +          } + +          that.$element.show() + +          if (transition) { +            that.$element[0].offsetWidth // force reflow +          } + +          that.$element +            .addClass('in') +            .attr('aria-hidden', false) + +          that.enforceFocus() + +          transition ? +            that.$element.one($.support.transition.end, function () { that.$element.focus().trigger('shown') }) : +            that.$element.focus().trigger('shown') + +        }) +      } + +    , hide: function (e) { +        e && e.preventDefault() + +        var that = this + +        e = $.Event('hide') + +        this.$element.trigger(e) + +        if (!this.isShown || e.isDefaultPrevented()) return + +        this.isShown = false + +        this.escape() + +        $(document).off('focusin.modal') + +        this.$element +          .removeClass('in') +          .attr('aria-hidden', true) + +        $.support.transition && this.$element.hasClass('fade') ? +          this.hideWithTransition() : +          this.hideModal() +      } + +    , enforceFocus: function () { +        var that = this +        $(document).on('focusin.modal', function (e) { +          if (that.$element[0] !== e.target && !that.$element.has(e.target).length) { +            that.$element.focus() +          } +        }) +      } + +    , escape: function () { +        var that = this +        if (this.isShown && this.options.keyboard) { +          this.$element.on('keyup.dismiss.modal', function ( e ) { +            e.which == 27 && that.hide() +          }) +        } else if (!this.isShown) { +          this.$element.off('keyup.dismiss.modal') +        } +      } + +    , hideWithTransition: function () { +        var that = this +          , timeout = setTimeout(function () { +              that.$element.off($.support.transition.end) +              that.hideModal() +            }, 500) + +        this.$element.one($.support.transition.end, function () { +          clearTimeout(timeout) +          that.hideModal() +        }) +      } + +    , hideModal: function () { +        var that = this +        this.$element.hide() +        this.backdrop(function () { +          that.removeBackdrop() +          that.$element.trigger('hidden') +        }) +      } + +    , removeBackdrop: function () { +        this.$backdrop && this.$backdrop.remove() +        this.$backdrop = null +      } + +    , backdrop: function (callback) { +        var that = this +          , animate = this.$element.hasClass('fade') ? 'fade' : '' + +        if (this.isShown && this.options.backdrop) { +          var doAnimate = $.support.transition && animate + +          this.$backdrop = $('<div class="modal-backdrop ' + animate + '" />') +            .appendTo(document.body) + +          this.$backdrop.click( +            this.options.backdrop == 'static' ? +              $.proxy(this.$element[0].focus, this.$element[0]) +            : $.proxy(this.hide, this) +          ) + +          if (doAnimate) this.$backdrop[0].offsetWidth // force reflow + +          this.$backdrop.addClass('in') + +          if (!callback) return + +          doAnimate ? +            this.$backdrop.one($.support.transition.end, callback) : +            callback() + +        } else if (!this.isShown && this.$backdrop) { +          this.$backdrop.removeClass('in') + +          $.support.transition && this.$element.hasClass('fade')? +            this.$backdrop.one($.support.transition.end, callback) : +            callback() + +        } else if (callback) { +          callback() +        } +      } +  } + + + /* MODAL PLUGIN DEFINITION +  * ======================= */ + +  var old = $.fn.modal + +  $.fn.modal = function (option) { +    return this.each(function () { +      var $this = $(this) +        , data = $this.data('modal') +        , options = $.extend({}, $.fn.modal.defaults, $this.data(), typeof option == 'object' && option) +      if (!data) $this.data('modal', (data = new Modal(this, options))) +      if (typeof option == 'string') data[option]() +      else if (options.show) data.show() +    }) +  } + +  $.fn.modal.defaults = { +      backdrop: true +    , keyboard: true +    , show: true +  } + +  $.fn.modal.Constructor = Modal + + + /* MODAL NO CONFLICT +  * ================= */ + +  $.fn.modal.noConflict = function () { +    $.fn.modal = old +    return this +  } + + + /* MODAL DATA-API +  * ============== */ + +  $(document).on('click.modal.data-api', '[data-toggle="modal"]', function (e) { +    var $this = $(this) +      , href = $this.attr('href') +      , $target = $($this.attr('data-target') || (href && href.replace(/.*(?=#[^\s]+$)/, ''))) //strip for ie7 +      , option = $target.data('modal') ? 'toggle' : $.extend({ remote:!/#/.test(href) && href }, $target.data(), $this.data()) + +    e.preventDefault() + +    $target +      .modal(option) +      .one('hide', function () { +        $this.focus() +      }) +  }) + +}(window.jQuery); + +/* ============================================================ + * bootstrap-dropdown.js v2.3.2 + * http://twitter.github.com/bootstrap/javascript.html#dropdowns + * ============================================================ + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================ */ + + +!function ($) { + +  "use strict"; // jshint ;_; + + + /* DROPDOWN CLASS DEFINITION +  * ========================= */ + +  var toggle = '[data-toggle=dropdown]' +    , Dropdown = function (element) { +        var $el = $(element).on('click.dropdown.data-api', this.toggle) +        $('html').on('click.dropdown.data-api', function () { +          $el.parent().removeClass('open') +        }) +      } + +  Dropdown.prototype = { + +    constructor: Dropdown + +  , toggle: function (e) { +      var $this = $(this) +        , $parent +        , isActive + +      if ($this.is('.disabled, :disabled')) return + +      $parent = getParent($this) + +      isActive = $parent.hasClass('open') + +      clearMenus() + +      if (!isActive) { +        if ('ontouchstart' in document.documentElement) { +          // if mobile we we use a backdrop because click events don't delegate +          $('<div class="dropdown-backdrop"/>').insertBefore($(this)).on('click', clearMenus) +        } +        $parent.toggleClass('open') +      } + +      $this.focus() + +      return false +    } + +  , keydown: function (e) { +      var $this +        , $items +        , $active +        , $parent +        , isActive +        , index + +      if (!/(38|40|27)/.test(e.keyCode)) return + +      $this = $(this) + +      e.preventDefault() +      e.stopPropagation() + +      if ($this.is('.disabled, :disabled')) return + +      $parent = getParent($this) + +      isActive = $parent.hasClass('open') + +      if (!isActive || (isActive && e.keyCode == 27)) { +        if (e.which == 27) $parent.find(toggle).focus() +        return $this.click() +      } + +      $items = $('[role=menu] li:not(.divider):visible a', $parent) + +      if (!$items.length) return + +      index = $items.index($items.filter(':focus')) + +      if (e.keyCode == 38 && index > 0) index--                                        // up +      if (e.keyCode == 40 && index < $items.length - 1) index++                        // down +      if (!~index) index = 0 + +      $items +        .eq(index) +        .focus() +    } + +  } + +  function clearMenus() { +    $('.dropdown-backdrop').remove() +    $(toggle).each(function () { +      getParent($(this)).removeClass('open') +    }) +  } + +  function getParent($this) { +    var selector = $this.attr('data-target') +      , $parent + +    if (!selector) { +      selector = $this.attr('href') +      selector = selector && /#/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 +    } + +    $parent = selector && $(selector) + +    if (!$parent || !$parent.length) $parent = $this.parent() + +    return $parent +  } + + +  /* DROPDOWN PLUGIN DEFINITION +   * ========================== */ + +  var old = $.fn.dropdown + +  $.fn.dropdown = function (option) { +    return this.each(function () { +      var $this = $(this) +        , data = $this.data('dropdown') +      if (!data) $this.data('dropdown', (data = new Dropdown(this))) +      if (typeof option == 'string') data[option].call($this) +    }) +  } + +  $.fn.dropdown.Constructor = Dropdown + + + /* DROPDOWN NO CONFLICT +  * ==================== */ + +  $.fn.dropdown.noConflict = function () { +    $.fn.dropdown = old +    return this +  } + + +  /* APPLY TO STANDARD DROPDOWN ELEMENTS +   * =================================== */ + +  $(document) +    .on('click.dropdown.data-api', clearMenus) +    .on('click.dropdown.data-api', '.dropdown form', function (e) { e.stopPropagation() }) +    .on('click.dropdown.data-api'  , toggle, Dropdown.prototype.toggle) +    .on('keydown.dropdown.data-api', toggle + ', [role=menu]' , Dropdown.prototype.keydown) + +}(window.jQuery); + +/* ============================================================= + * bootstrap-scrollspy.js v2.3.2 + * http://twitter.github.com/bootstrap/javascript.html#scrollspy + * ============================================================= + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================== */ + + +!function ($) { + +  "use strict"; // jshint ;_; + + + /* SCROLLSPY CLASS DEFINITION +  * ========================== */ + +  function ScrollSpy(element, options) { +    var process = $.proxy(this.process, this) +      , $element = $(element).is('body') ? $(window) : $(element) +      , href +    this.options = $.extend({}, $.fn.scrollspy.defaults, options) +    this.$scrollElement = $element.on('scroll.scroll-spy.data-api', process) +    this.selector = (this.options.target +      || ((href = $(element).attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) //strip for ie7 +      || '') + ' .nav li > a' +    this.$body = $('body') +    this.refresh() +    this.process() +  } + +  ScrollSpy.prototype = { + +      constructor: ScrollSpy + +    , refresh: function () { +        var self = this +          , $targets + +        this.offsets = $([]) +        this.targets = $([]) + +        $targets = this.$body +          .find(this.selector) +          .map(function () { +            var $el = $(this) +              , href = $el.data('target') || $el.attr('href') +              , $href = /^#\w/.test(href) && $(href) +            return ( $href +              && $href.length +              && [[ $href.position().top + (!$.isWindow(self.$scrollElement.get(0)) && self.$scrollElement.scrollTop()), href ]] ) || null +          }) +          .sort(function (a, b) { return a[0] - b[0] }) +          .each(function () { +            self.offsets.push(this[0]) +            self.targets.push(this[1]) +          }) +      } + +    , process: function () { +        var scrollTop = this.$scrollElement.scrollTop() + this.options.offset +          , scrollHeight = this.$scrollElement[0].scrollHeight || this.$body[0].scrollHeight +          , maxScroll = scrollHeight - this.$scrollElement.height() +          , offsets = this.offsets +          , targets = this.targets +          , activeTarget = this.activeTarget +          , i + +        if (scrollTop >= maxScroll) { +          return activeTarget != (i = targets.last()[0]) +            && this.activate ( i ) +        } + +        for (i = offsets.length; i--;) { +          activeTarget != targets[i] +            && scrollTop >= offsets[i] +            && (!offsets[i + 1] || scrollTop <= offsets[i + 1]) +            && this.activate( targets[i] ) +        } +      } + +    , activate: function (target) { +        var active +          , selector + +        this.activeTarget = target + +        $(this.selector) +          .parent('.active') +          .removeClass('active') + +        selector = this.selector +          + '[data-target="' + target + '"],' +          + this.selector + '[href="' + target + '"]' + +        active = $(selector) +          .parent('li') +          .addClass('active') + +        if (active.parent('.dropdown-menu').length)  { +          active = active.closest('li.dropdown').addClass('active') +        } + +        active.trigger('activate') +      } + +  } + + + /* SCROLLSPY PLUGIN DEFINITION +  * =========================== */ + +  var old = $.fn.scrollspy + +  $.fn.scrollspy = function (option) { +    return this.each(function () { +      var $this = $(this) +        , data = $this.data('scrollspy') +        , options = typeof option == 'object' && option +      if (!data) $this.data('scrollspy', (data = new ScrollSpy(this, options))) +      if (typeof option == 'string') data[option]() +    }) +  } + +  $.fn.scrollspy.Constructor = ScrollSpy + +  $.fn.scrollspy.defaults = { +    offset: 10 +  } + + + /* SCROLLSPY NO CONFLICT +  * ===================== */ + +  $.fn.scrollspy.noConflict = function () { +    $.fn.scrollspy = old +    return this +  } + + + /* SCROLLSPY DATA-API +  * ================== */ + +  $(window).on('load', function () { +    $('[data-spy="scroll"]').each(function () { +      var $spy = $(this) +      $spy.scrollspy($spy.data()) +    }) +  }) + +}(window.jQuery); +/* ======================================================== + * bootstrap-tab.js v2.3.2 + * http://twitter.github.com/bootstrap/javascript.html#tabs + * ======================================================== + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ======================================================== */ + + +!function ($) { + +  "use strict"; // jshint ;_; + + + /* TAB CLASS DEFINITION +  * ==================== */ + +  var Tab = function (element) { +    this.element = $(element) +  } + +  Tab.prototype = { + +    constructor: Tab + +  , show: function () { +      var $this = this.element +        , $ul = $this.closest('ul:not(.dropdown-menu)') +        , selector = $this.attr('data-target') +        , previous +        , $target +        , e + +      if (!selector) { +        selector = $this.attr('href') +        selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 +      } + +      if ( $this.parent('li').hasClass('active') ) return + +      previous = $ul.find('.active:last a')[0] + +      e = $.Event('show', { +        relatedTarget: previous +      }) + +      $this.trigger(e) + +      if (e.isDefaultPrevented()) return + +      $target = $(selector) + +      this.activate($this.parent('li'), $ul) +      this.activate($target, $target.parent(), function () { +        $this.trigger({ +          type: 'shown' +        , relatedTarget: previous +        }) +      }) +    } + +  , activate: function ( element, container, callback) { +      var $active = container.find('> .active') +        , transition = callback +            && $.support.transition +            && $active.hasClass('fade') + +      function next() { +        $active +          .removeClass('active') +          .find('> .dropdown-menu > .active') +          .removeClass('active') + +        element.addClass('active') + +        if (transition) { +          element[0].offsetWidth // reflow for transition +          element.addClass('in') +        } else { +          element.removeClass('fade') +        } + +        if ( element.parent('.dropdown-menu') ) { +          element.closest('li.dropdown').addClass('active') +        } + +        callback && callback() +      } + +      transition ? +        $active.one($.support.transition.end, next) : +        next() + +      $active.removeClass('in') +    } +  } + + + /* TAB PLUGIN DEFINITION +  * ===================== */ + +  var old = $.fn.tab + +  $.fn.tab = function ( option ) { +    return this.each(function () { +      var $this = $(this) +        , data = $this.data('tab') +      if (!data) $this.data('tab', (data = new Tab(this))) +      if (typeof option == 'string') data[option]() +    }) +  } + +  $.fn.tab.Constructor = Tab + + + /* TAB NO CONFLICT +  * =============== */ + +  $.fn.tab.noConflict = function () { +    $.fn.tab = old +    return this +  } + + + /* TAB DATA-API +  * ============ */ + +  $(document).on('click.tab.data-api', '[data-toggle="tab"], [data-toggle="pill"]', function (e) { +    e.preventDefault() +    $(this).tab('show') +  }) + +}(window.jQuery); +/* =========================================================== + * bootstrap-tooltip.js v2.3.2 + * http://twitter.github.com/bootstrap/javascript.html#tooltips + * Inspired by the original jQuery.tipsy by Jason Frame + * =========================================================== + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================================================== */ + + +!function ($) { + +  "use strict"; // jshint ;_; + + + /* TOOLTIP PUBLIC CLASS DEFINITION +  * =============================== */ + +  var Tooltip = function (element, options) { +    this.init('tooltip', element, options) +  } + +  Tooltip.prototype = { + +    constructor: Tooltip + +  , init: function (type, element, options) { +      var eventIn +        , eventOut +        , triggers +        , trigger +        , i + +      this.type = type +      this.$element = $(element) +      this.options = this.getOptions(options) +      this.enabled = true + +      triggers = this.options.trigger.split(' ') + +      for (i = triggers.length; i--;) { +        trigger = triggers[i] +        if (trigger == 'click') { +          this.$element.on('click.' + this.type, this.options.selector, $.proxy(this.toggle, this)) +        } else if (trigger != 'manual') { +          eventIn = trigger == 'hover' ? 'mouseenter' : 'focus' +          eventOut = trigger == 'hover' ? 'mouseleave' : 'blur' +          this.$element.on(eventIn + '.' + this.type, this.options.selector, $.proxy(this.enter, this)) +          this.$element.on(eventOut + '.' + this.type, this.options.selector, $.proxy(this.leave, this)) +        } +      } + +      this.options.selector ? +        (this._options = $.extend({}, this.options, { trigger: 'manual', selector: '' })) : +        this.fixTitle() +    } + +  , getOptions: function (options) { +      options = $.extend({}, $.fn[this.type].defaults, this.$element.data(), options) + +      if (options.delay && typeof options.delay == 'number') { +        options.delay = { +          show: options.delay +        , hide: options.delay +        } +      } + +      return options +    } + +  , enter: function (e) { +      var defaults = $.fn[this.type].defaults +        , options = {} +        , self + +      this._options && $.each(this._options, function (key, value) { +        if (defaults[key] != value) options[key] = value +      }, this) + +      self = $(e.currentTarget)[this.type](options).data(this.type) + +      if (!self.options.delay || !self.options.delay.show) return self.show() + +      clearTimeout(this.timeout) +      self.hoverState = 'in' +      this.timeout = setTimeout(function() { +        if (self.hoverState == 'in') self.show() +      }, self.options.delay.show) +    } + +  , leave: function (e) { +      var self = $(e.currentTarget)[this.type](this._options).data(this.type) + +      if (this.timeout) clearTimeout(this.timeout) +      if (!self.options.delay || !self.options.delay.hide) return self.hide() + +      self.hoverState = 'out' +      this.timeout = setTimeout(function() { +        if (self.hoverState == 'out') self.hide() +      }, self.options.delay.hide) +    } + +  , show: function () { +      var $tip +        , pos +        , actualWidth +        , actualHeight +        , placement +        , tp +        , e = $.Event('show') + +      if (this.hasContent() && this.enabled) { +        this.$element.trigger(e) +        if (e.isDefaultPrevented()) return +        $tip = this.tip() +        this.setContent() + +        if (this.options.animation) { +          $tip.addClass('fade') +        } + +        placement = typeof this.options.placement == 'function' ? +          this.options.placement.call(this, $tip[0], this.$element[0]) : +          this.options.placement + +        $tip +          .detach() +          .css({ top: 0, left: 0, display: 'block' }) + +        this.options.container ? $tip.appendTo(this.options.container) : $tip.insertAfter(this.$element) + +        pos = this.getPosition() + +        actualWidth = $tip[0].offsetWidth +        actualHeight = $tip[0].offsetHeight + +        switch (placement) { +          case 'bottom': +            tp = {top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2} +            break +          case 'top': +            tp = {top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2} +            break +          case 'left': +            tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth} +            break +          case 'right': +            tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width} +            break +        } + +        this.applyPlacement(tp, placement) +        this.$element.trigger('shown') +      } +    } + +  , applyPlacement: function(offset, placement){ +      var $tip = this.tip() +        , width = $tip[0].offsetWidth +        , height = $tip[0].offsetHeight +        , actualWidth +        , actualHeight +        , delta +        , replace + +      $tip +        .offset(offset) +        .addClass(placement) +        .addClass('in') + +      actualWidth = $tip[0].offsetWidth +      actualHeight = $tip[0].offsetHeight + +      if (placement == 'top' && actualHeight != height) { +        offset.top = offset.top + height - actualHeight +        replace = true +      } + +      if (placement == 'bottom' || placement == 'top') { +        delta = 0 + +        if (offset.left < 0){ +          delta = offset.left * -2 +          offset.left = 0 +          $tip.offset(offset) +          actualWidth = $tip[0].offsetWidth +          actualHeight = $tip[0].offsetHeight +        } + +        this.replaceArrow(delta - width + actualWidth, actualWidth, 'left') +      } else { +        this.replaceArrow(actualHeight - height, actualHeight, 'top') +      } + +      if (replace) $tip.offset(offset) +    } + +  , replaceArrow: function(delta, dimension, position){ +      this +        .arrow() +        .css(position, delta ? (50 * (1 - delta / dimension) + "%") : '') +    } + +  , setContent: function () { +      var $tip = this.tip() +        , title = this.getTitle() + +      $tip.find('.tooltip-inner')[this.options.html ? 'html' : 'text'](title) +      $tip.removeClass('fade in top bottom left right') +    } + +  , hide: function () { +      var that = this +        , $tip = this.tip() +        , e = $.Event('hide') + +      this.$element.trigger(e) +      if (e.isDefaultPrevented()) return + +      $tip.removeClass('in') + +      function removeWithAnimation() { +        var timeout = setTimeout(function () { +          $tip.off($.support.transition.end).detach() +        }, 500) + +        $tip.one($.support.transition.end, function () { +          clearTimeout(timeout) +          $tip.detach() +        }) +      } + +      $.support.transition && this.$tip.hasClass('fade') ? +        removeWithAnimation() : +        $tip.detach() + +      this.$element.trigger('hidden') + +      return this +    } + +  , fixTitle: function () { +      var $e = this.$element +      if ($e.attr('title') || typeof($e.attr('data-original-title')) != 'string') { +        $e.attr('data-original-title', $e.attr('title') || '').attr('title', '') +      } +    } + +  , hasContent: function () { +      return this.getTitle() +    } + +  , getPosition: function () { +      var el = this.$element[0] +      return $.extend({}, (typeof el.getBoundingClientRect == 'function') ? el.getBoundingClientRect() : { +        width: el.offsetWidth +      , height: el.offsetHeight +      }, this.$element.offset()) +    } + +  , getTitle: function () { +      var title +        , $e = this.$element +        , o = this.options + +      title = $e.attr('data-original-title') +        || (typeof o.title == 'function' ? o.title.call($e[0]) :  o.title) + +      return title +    } + +  , tip: function () { +      return this.$tip = this.$tip || $(this.options.template) +    } + +  , arrow: function(){ +      return this.$arrow = this.$arrow || this.tip().find(".tooltip-arrow") +    } + +  , validate: function () { +      if (!this.$element[0].parentNode) { +        this.hide() +        this.$element = null +        this.options = null +      } +    } + +  , enable: function () { +      this.enabled = true +    } + +  , disable: function () { +      this.enabled = false +    } + +  , toggleEnabled: function () { +      this.enabled = !this.enabled +    } + +  , toggle: function (e) { +      var self = e ? $(e.currentTarget)[this.type](this._options).data(this.type) : this +      self.tip().hasClass('in') ? self.hide() : self.show() +    } + +  , destroy: function () { +      this.hide().$element.off('.' + this.type).removeData(this.type) +    } + +  } + + + /* TOOLTIP PLUGIN DEFINITION +  * ========================= */ + +  var old = $.fn.tooltip + +  $.fn.tooltip = function ( option ) { +    return this.each(function () { +      var $this = $(this) +        , data = $this.data('tooltip') +        , options = typeof option == 'object' && option +      if (!data) $this.data('tooltip', (data = new Tooltip(this, options))) +      if (typeof option == 'string') data[option]() +    }) +  } + +  $.fn.tooltip.Constructor = Tooltip + +  $.fn.tooltip.defaults = { +    animation: true +  , placement: 'top' +  , selector: false +  , template: '<div class="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>' +  , trigger: 'hover focus' +  , title: '' +  , delay: 0 +  , html: false +  , container: false +  } + + + /* TOOLTIP NO CONFLICT +  * =================== */ + +  $.fn.tooltip.noConflict = function () { +    $.fn.tooltip = old +    return this +  } + +}(window.jQuery); + +/* =========================================================== + * bootstrap-popover.js v2.3.2 + * http://twitter.github.com/bootstrap/javascript.html#popovers + * =========================================================== + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * =========================================================== */ + + +!function ($) { + +  "use strict"; // jshint ;_; + + + /* POPOVER PUBLIC CLASS DEFINITION +  * =============================== */ + +  var Popover = function (element, options) { +    this.init('popover', element, options) +  } + + +  /* NOTE: POPOVER EXTENDS BOOTSTRAP-TOOLTIP.js +     ========================================== */ + +  Popover.prototype = $.extend({}, $.fn.tooltip.Constructor.prototype, { + +    constructor: Popover + +  , setContent: function () { +      var $tip = this.tip() +        , title = this.getTitle() +        , content = this.getContent() + +      $tip.find('.popover-title')[this.options.html ? 'html' : 'text'](title) +      $tip.find('.popover-content')[this.options.html ? 'html' : 'text'](content) + +      $tip.removeClass('fade top bottom left right in') +    } + +  , hasContent: function () { +      return this.getTitle() || this.getContent() +    } + +  , getContent: function () { +      var content +        , $e = this.$element +        , o = this.options + +      content = (typeof o.content == 'function' ? o.content.call($e[0]) :  o.content) +        || $e.attr('data-content') + +      return content +    } + +  , tip: function () { +      if (!this.$tip) { +        this.$tip = $(this.options.template) +      } +      return this.$tip +    } + +  , destroy: function () { +      this.hide().$element.off('.' + this.type).removeData(this.type) +    } + +  }) + + + /* POPOVER PLUGIN DEFINITION +  * ======================= */ + +  var old = $.fn.popover + +  $.fn.popover = function (option) { +    return this.each(function () { +      var $this = $(this) +        , data = $this.data('popover') +        , options = typeof option == 'object' && option +      if (!data) $this.data('popover', (data = new Popover(this, options))) +      if (typeof option == 'string') data[option]() +    }) +  } + +  $.fn.popover.Constructor = Popover + +  $.fn.popover.defaults = $.extend({} , $.fn.tooltip.defaults, { +    placement: 'right' +  , trigger: 'click' +  , content: '' +  , template: '<div class="popover"><div class="arrow"></div><h3 class="popover-title"></h3><div class="popover-content"></div></div>' +  }) + + + /* POPOVER NO CONFLICT +  * =================== */ + +  $.fn.popover.noConflict = function () { +    $.fn.popover = old +    return this +  } + +}(window.jQuery); + +/* ========================================================== + * bootstrap-affix.js v2.3.2 + * http://twitter.github.com/bootstrap/javascript.html#affix + * ========================================================== + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================================================== */ + + +!function ($) { + +  "use strict"; // jshint ;_; + + + /* AFFIX CLASS DEFINITION +  * ====================== */ + +  var Affix = function (element, options) { +    this.options = $.extend({}, $.fn.affix.defaults, options) +    this.$window = $(window) +      .on('scroll.affix.data-api', $.proxy(this.checkPosition, this)) +      .on('click.affix.data-api',  $.proxy(function () { setTimeout($.proxy(this.checkPosition, this), 1) }, this)) +    this.$element = $(element) +    this.checkPosition() +  } + +  Affix.prototype.checkPosition = function () { +    if (!this.$element.is(':visible')) return + +    var scrollHeight = $(document).height() +      , scrollTop = this.$window.scrollTop() +      , position = this.$element.offset() +      , offset = this.options.offset +      , offsetBottom = offset.bottom +      , offsetTop = offset.top +      , reset = 'affix affix-top affix-bottom' +      , affix + +    if (typeof offset != 'object') offsetBottom = offsetTop = offset +    if (typeof offsetTop == 'function') offsetTop = offset.top() +    if (typeof offsetBottom == 'function') offsetBottom = offset.bottom() + +    affix = this.unpin != null && (scrollTop + this.unpin <= position.top) ? +      false    : offsetBottom != null && (position.top + this.$element.height() >= scrollHeight - offsetBottom) ? +      'bottom' : offsetTop != null && scrollTop <= offsetTop ? +      'top'    : false + +    if (this.affixed === affix) return + +    this.affixed = affix +    this.unpin = affix == 'bottom' ? position.top - scrollTop : null + +    this.$element.removeClass(reset).addClass('affix' + (affix ? '-' + affix : '')) +  } + + + /* AFFIX PLUGIN DEFINITION +  * ======================= */ + +  var old = $.fn.affix + +  $.fn.affix = function (option) { +    return this.each(function () { +      var $this = $(this) +        , data = $this.data('affix') +        , options = typeof option == 'object' && option +      if (!data) $this.data('affix', (data = new Affix(this, options))) +      if (typeof option == 'string') data[option]() +    }) +  } + +  $.fn.affix.Constructor = Affix + +  $.fn.affix.defaults = { +    offset: 0 +  } + + + /* AFFIX NO CONFLICT +  * ================= */ + +  $.fn.affix.noConflict = function () { +    $.fn.affix = old +    return this +  } + + + /* AFFIX DATA-API +  * ============== */ + +  $(window).on('load', function () { +    $('[data-spy="affix"]').each(function () { +      var $spy = $(this) +        , data = $spy.data() + +      data.offset = data.offset || {} + +      data.offsetBottom && (data.offset.bottom = data.offsetBottom) +      data.offsetTop && (data.offset.top = data.offsetTop) + +      $spy.affix(data) +    }) +  }) + + +}(window.jQuery); +/* ========================================================== + * bootstrap-alert.js v2.3.2 + * http://twitter.github.com/bootstrap/javascript.html#alerts + * ========================================================== + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================================================== */ + + +!function ($) { + +  "use strict"; // jshint ;_; + + + /* ALERT CLASS DEFINITION +  * ====================== */ + +  var dismiss = '[data-dismiss="alert"]' +    , Alert = function (el) { +        $(el).on('click', dismiss, this.close) +      } + +  Alert.prototype.close = function (e) { +    var $this = $(this) +      , selector = $this.attr('data-target') +      , $parent + +    if (!selector) { +      selector = $this.attr('href') +      selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 +    } + +    $parent = $(selector) + +    e && e.preventDefault() + +    $parent.length || ($parent = $this.hasClass('alert') ? $this : $this.parent()) + +    $parent.trigger(e = $.Event('close')) + +    if (e.isDefaultPrevented()) return + +    $parent.removeClass('in') + +    function removeElement() { +      $parent +        .trigger('closed') +        .remove() +    } + +    $.support.transition && $parent.hasClass('fade') ? +      $parent.on($.support.transition.end, removeElement) : +      removeElement() +  } + + + /* ALERT PLUGIN DEFINITION +  * ======================= */ + +  var old = $.fn.alert + +  $.fn.alert = function (option) { +    return this.each(function () { +      var $this = $(this) +        , data = $this.data('alert') +      if (!data) $this.data('alert', (data = new Alert(this))) +      if (typeof option == 'string') data[option].call($this) +    }) +  } + +  $.fn.alert.Constructor = Alert + + + /* ALERT NO CONFLICT +  * ================= */ + +  $.fn.alert.noConflict = function () { +    $.fn.alert = old +    return this +  } + + + /* ALERT DATA-API +  * ============== */ + +  $(document).on('click.alert.data-api', dismiss, Alert.prototype.close) + +}(window.jQuery); +/* ============================================================ + * bootstrap-button.js v2.3.2 + * http://twitter.github.com/bootstrap/javascript.html#buttons + * ============================================================ + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================ */ + + +!function ($) { + +  "use strict"; // jshint ;_; + + + /* BUTTON PUBLIC CLASS DEFINITION +  * ============================== */ + +  var Button = function (element, options) { +    this.$element = $(element) +    this.options = $.extend({}, $.fn.button.defaults, options) +  } + +  Button.prototype.setState = function (state) { +    var d = 'disabled' +      , $el = this.$element +      , data = $el.data() +      , val = $el.is('input') ? 'val' : 'html' + +    state = state + 'Text' +    data.resetText || $el.data('resetText', $el[val]()) + +    $el[val](data[state] || this.options[state]) + +    // push to event loop to allow forms to submit +    setTimeout(function () { +      state == 'loadingText' ? +        $el.addClass(d).attr(d, d) : +        $el.removeClass(d).removeAttr(d) +    }, 0) +  } + +  Button.prototype.toggle = function () { +    var $parent = this.$element.closest('[data-toggle="buttons-radio"]') + +    $parent && $parent +      .find('.active') +      .removeClass('active') + +    this.$element.toggleClass('active') +  } + + + /* BUTTON PLUGIN DEFINITION +  * ======================== */ + +  var old = $.fn.button + +  $.fn.button = function (option) { +    return this.each(function () { +      var $this = $(this) +        , data = $this.data('button') +        , options = typeof option == 'object' && option +      if (!data) $this.data('button', (data = new Button(this, options))) +      if (option == 'toggle') data.toggle() +      else if (option) data.setState(option) +    }) +  } + +  $.fn.button.defaults = { +    loadingText: 'loading...' +  } + +  $.fn.button.Constructor = Button + + + /* BUTTON NO CONFLICT +  * ================== */ + +  $.fn.button.noConflict = function () { +    $.fn.button = old +    return this +  } + + + /* BUTTON DATA-API +  * =============== */ + +  $(document).on('click.button.data-api', '[data-toggle^=button]', function (e) { +    var $btn = $(e.target) +    if (!$btn.hasClass('btn')) $btn = $btn.closest('.btn') +    $btn.button('toggle') +  }) + +}(window.jQuery); +/* ============================================================= + * bootstrap-collapse.js v2.3.2 + * http://twitter.github.com/bootstrap/javascript.html#collapse + * ============================================================= + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================ */ + + +!function ($) { + +  "use strict"; // jshint ;_; + + + /* COLLAPSE PUBLIC CLASS DEFINITION +  * ================================ */ + +  var Collapse = function (element, options) { +    this.$element = $(element) +    this.options = $.extend({}, $.fn.collapse.defaults, options) + +    if (this.options.parent) { +      this.$parent = $(this.options.parent) +    } + +    this.options.toggle && this.toggle() +  } + +  Collapse.prototype = { + +    constructor: Collapse + +  , dimension: function () { +      var hasWidth = this.$element.hasClass('width') +      return hasWidth ? 'width' : 'height' +    } + +  , show: function () { +      var dimension +        , scroll +        , actives +        , hasData + +      if (this.transitioning || this.$element.hasClass('in')) return + +      dimension = this.dimension() +      scroll = $.camelCase(['scroll', dimension].join('-')) +      actives = this.$parent && this.$parent.find('> .accordion-group > .in') + +      if (actives && actives.length) { +        hasData = actives.data('collapse') +        if (hasData && hasData.transitioning) return +        actives.collapse('hide') +        hasData || actives.data('collapse', null) +      } + +      this.$element[dimension](0) +      this.transition('addClass', $.Event('show'), 'shown') +      $.support.transition && this.$element[dimension](this.$element[0][scroll]) +    } + +  , hide: function () { +      var dimension +      if (this.transitioning || !this.$element.hasClass('in')) return +      dimension = this.dimension() +      this.reset(this.$element[dimension]()) +      this.transition('removeClass', $.Event('hide'), 'hidden') +      this.$element[dimension](0) +    } + +  , reset: function (size) { +      var dimension = this.dimension() + +      this.$element +        .removeClass('collapse') +        [dimension](size || 'auto') +        [0].offsetWidth + +      this.$element[size !== null ? 'addClass' : 'removeClass']('collapse') + +      return this +    } + +  , transition: function (method, startEvent, completeEvent) { +      var that = this +        , complete = function () { +            if (startEvent.type == 'show') that.reset() +            that.transitioning = 0 +            that.$element.trigger(completeEvent) +          } + +      this.$element.trigger(startEvent) + +      if (startEvent.isDefaultPrevented()) return + +      this.transitioning = 1 + +      this.$element[method]('in') + +      $.support.transition && this.$element.hasClass('collapse') ? +        this.$element.one($.support.transition.end, complete) : +        complete() +    } + +  , toggle: function () { +      this[this.$element.hasClass('in') ? 'hide' : 'show']() +    } + +  } + + + /* COLLAPSE PLUGIN DEFINITION +  * ========================== */ + +  var old = $.fn.collapse + +  $.fn.collapse = function (option) { +    return this.each(function () { +      var $this = $(this) +        , data = $this.data('collapse') +        , options = $.extend({}, $.fn.collapse.defaults, $this.data(), typeof option == 'object' && option) +      if (!data) $this.data('collapse', (data = new Collapse(this, options))) +      if (typeof option == 'string') data[option]() +    }) +  } + +  $.fn.collapse.defaults = { +    toggle: true +  } + +  $.fn.collapse.Constructor = Collapse + + + /* COLLAPSE NO CONFLICT +  * ==================== */ + +  $.fn.collapse.noConflict = function () { +    $.fn.collapse = old +    return this +  } + + + /* COLLAPSE DATA-API +  * ================= */ + +  $(document).on('click.collapse.data-api', '[data-toggle=collapse]', function (e) { +    var $this = $(this), href +      , target = $this.attr('data-target') +        || e.preventDefault() +        || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') //strip for ie7 +      , option = $(target).data('collapse') ? 'toggle' : $this.data() +    $this[$(target).hasClass('in') ? 'addClass' : 'removeClass']('collapsed') +    $(target).collapse(option) +  }) + +}(window.jQuery); +/* ========================================================== + * bootstrap-carousel.js v2.3.2 + * http://twitter.github.com/bootstrap/javascript.html#carousel + * ========================================================== + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================================================== */ + + +!function ($) { + +  "use strict"; // jshint ;_; + + + /* CAROUSEL CLASS DEFINITION +  * ========================= */ + +  var Carousel = function (element, options) { +    this.$element = $(element) +    this.$indicators = this.$element.find('.carousel-indicators') +    this.options = options +    this.options.pause == 'hover' && this.$element +      .on('mouseenter', $.proxy(this.pause, this)) +      .on('mouseleave', $.proxy(this.cycle, this)) +  } + +  Carousel.prototype = { + +    cycle: function (e) { +      if (!e) this.paused = false +      if (this.interval) clearInterval(this.interval); +      this.options.interval +        && !this.paused +        && (this.interval = setInterval($.proxy(this.next, this), this.options.interval)) +      return this +    } + +  , getActiveIndex: function () { +      this.$active = this.$element.find('.item.active') +      this.$items = this.$active.parent().children() +      return this.$items.index(this.$active) +    } + +  , to: function (pos) { +      var activeIndex = this.getActiveIndex() +        , that = this + +      if (pos > (this.$items.length - 1) || pos < 0) return + +      if (this.sliding) { +        return this.$element.one('slid', function () { +          that.to(pos) +        }) +      } + +      if (activeIndex == pos) { +        return this.pause().cycle() +      } + +      return this.slide(pos > activeIndex ? 'next' : 'prev', $(this.$items[pos])) +    } + +  , pause: function (e) { +      if (!e) this.paused = true +      if (this.$element.find('.next, .prev').length && $.support.transition.end) { +        this.$element.trigger($.support.transition.end) +        this.cycle(true) +      } +      clearInterval(this.interval) +      this.interval = null +      return this +    } + +  , next: function () { +      if (this.sliding) return +      return this.slide('next') +    } + +  , prev: function () { +      if (this.sliding) return +      return this.slide('prev') +    } + +  , slide: function (type, next) { +      var $active = this.$element.find('.item.active') +        , $next = next || $active[type]() +        , isCycling = this.interval +        , direction = type == 'next' ? 'left' : 'right' +        , fallback  = type == 'next' ? 'first' : 'last' +        , that = this +        , e + +      this.sliding = true + +      isCycling && this.pause() + +      $next = $next.length ? $next : this.$element.find('.item')[fallback]() + +      e = $.Event('slide', { +        relatedTarget: $next[0] +      , direction: direction +      }) + +      if ($next.hasClass('active')) return + +      if (this.$indicators.length) { +        this.$indicators.find('.active').removeClass('active') +        this.$element.one('slid', function () { +          var $nextIndicator = $(that.$indicators.children()[that.getActiveIndex()]) +          $nextIndicator && $nextIndicator.addClass('active') +        }) +      } + +      if ($.support.transition && this.$element.hasClass('slide')) { +        this.$element.trigger(e) +        if (e.isDefaultPrevented()) return +        $next.addClass(type) +        $next[0].offsetWidth // force reflow +        $active.addClass(direction) +        $next.addClass(direction) +        this.$element.one($.support.transition.end, function () { +          $next.removeClass([type, direction].join(' ')).addClass('active') +          $active.removeClass(['active', direction].join(' ')) +          that.sliding = false +          setTimeout(function () { that.$element.trigger('slid') }, 0) +        }) +      } else { +        this.$element.trigger(e) +        if (e.isDefaultPrevented()) return +        $active.removeClass('active') +        $next.addClass('active') +        this.sliding = false +        this.$element.trigger('slid') +      } + +      isCycling && this.cycle() + +      return this +    } + +  } + + + /* CAROUSEL PLUGIN DEFINITION +  * ========================== */ + +  var old = $.fn.carousel + +  $.fn.carousel = function (option) { +    return this.each(function () { +      var $this = $(this) +        , data = $this.data('carousel') +        , options = $.extend({}, $.fn.carousel.defaults, typeof option == 'object' && option) +        , action = typeof option == 'string' ? option : options.slide +      if (!data) $this.data('carousel', (data = new Carousel(this, options))) +      if (typeof option == 'number') data.to(option) +      else if (action) data[action]() +      else if (options.interval) data.pause().cycle() +    }) +  } + +  $.fn.carousel.defaults = { +    interval: 5000 +  , pause: 'hover' +  } + +  $.fn.carousel.Constructor = Carousel + + + /* CAROUSEL NO CONFLICT +  * ==================== */ + +  $.fn.carousel.noConflict = function () { +    $.fn.carousel = old +    return this +  } + + /* CAROUSEL DATA-API +  * ================= */ + +  $(document).on('click.carousel.data-api', '[data-slide], [data-slide-to]', function (e) { +    var $this = $(this), href +      , $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) //strip for ie7 +      , options = $.extend({}, $target.data(), $this.data()) +      , slideIndex + +    $target.carousel(options) + +    if (slideIndex = $this.attr('data-slide-to')) { +      $target.data('carousel').pause().to(slideIndex).cycle() +    } + +    e.preventDefault() +  }) + +}(window.jQuery); +/* ============================================================= + * bootstrap-typeahead.js v2.3.2 + * http://twitter.github.com/bootstrap/javascript.html#typeahead + * ============================================================= + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================ */ + + +!function($){ + +  "use strict"; // jshint ;_; + + + /* TYPEAHEAD PUBLIC CLASS DEFINITION +  * ================================= */ + +  var Typeahead = function (element, options) { +    this.$element = $(element) +    this.options = $.extend({}, $.fn.typeahead.defaults, options) +    this.matcher = this.options.matcher || this.matcher +    this.sorter = this.options.sorter || this.sorter +    this.highlighter = this.options.highlighter || this.highlighter +    this.updater = this.options.updater || this.updater +    this.source = this.options.source +    this.$menu = $(this.options.menu) +    this.shown = false +    this.listen() +  } + +  Typeahead.prototype = { + +    constructor: Typeahead + +  , select: function () { +      var val = this.$menu.find('.active').attr('data-value') +      this.$element +        .val(this.updater(val)) +        .change() +      return this.hide() +    } + +  , updater: function (item) { +      return item +    } + +  , show: function () { +      var pos = $.extend({}, this.$element.position(), { +        height: this.$element[0].offsetHeight +      }) + +      this.$menu +        .insertAfter(this.$element) +        .css({ +          top: pos.top + pos.height +        , left: pos.left +        }) +        .show() + +      this.shown = true +      return this +    } + +  , hide: function () { +      this.$menu.hide() +      this.shown = false +      return this +    } + +  , lookup: function (event) { +      var items + +      this.query = this.$element.val() + +      if (!this.query || this.query.length < this.options.minLength) { +        return this.shown ? this.hide() : this +      } + +      items = $.isFunction(this.source) ? this.source(this.query, $.proxy(this.process, this)) : this.source + +      return items ? this.process(items) : this +    } + +  , process: function (items) { +      var that = this + +      items = $.grep(items, function (item) { +        return that.matcher(item) +      }) + +      items = this.sorter(items) + +      if (!items.length) { +        return this.shown ? this.hide() : this +      } + +      return this.render(items.slice(0, this.options.items)).show() +    } + +  , matcher: function (item) { +      return ~item.toLowerCase().indexOf(this.query.toLowerCase()) +    } + +  , sorter: function (items) { +      var beginswith = [] +        , caseSensitive = [] +        , caseInsensitive = [] +        , item + +      while (item = items.shift()) { +        if (!item.toLowerCase().indexOf(this.query.toLowerCase())) beginswith.push(item) +        else if (~item.indexOf(this.query)) caseSensitive.push(item) +        else caseInsensitive.push(item) +      } + +      return beginswith.concat(caseSensitive, caseInsensitive) +    } + +  , highlighter: function (item) { +      var query = this.query.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, '\\$&') +      return item.replace(new RegExp('(' + query + ')', 'ig'), function ($1, match) { +        return '<strong>' + match + '</strong>' +      }) +    } + +  , render: function (items) { +      var that = this + +      items = $(items).map(function (i, item) { +        i = $(that.options.item).attr('data-value', item) +        i.find('a').html(that.highlighter(item)) +        return i[0] +      }) + +      items.first().addClass('active') +      this.$menu.html(items) +      return this +    } + +  , next: function (event) { +      var active = this.$menu.find('.active').removeClass('active') +        , next = active.next() + +      if (!next.length) { +        next = $(this.$menu.find('li')[0]) +      } + +      next.addClass('active') +    } + +  , prev: function (event) { +      var active = this.$menu.find('.active').removeClass('active') +        , prev = active.prev() + +      if (!prev.length) { +        prev = this.$menu.find('li').last() +      } + +      prev.addClass('active') +    } + +  , listen: function () { +      this.$element +        .on('focus',    $.proxy(this.focus, this)) +        .on('blur',     $.proxy(this.blur, this)) +        .on('keypress', $.proxy(this.keypress, this)) +        .on('keyup',    $.proxy(this.keyup, this)) + +      if (this.eventSupported('keydown')) { +        this.$element.on('keydown', $.proxy(this.keydown, this)) +      } + +      this.$menu +        .on('click', $.proxy(this.click, this)) +        .on('mouseenter', 'li', $.proxy(this.mouseenter, this)) +        .on('mouseleave', 'li', $.proxy(this.mouseleave, this)) +    } + +  , eventSupported: function(eventName) { +      var isSupported = eventName in this.$element +      if (!isSupported) { +        this.$element.setAttribute(eventName, 'return;') +        isSupported = typeof this.$element[eventName] === 'function' +      } +      return isSupported +    } + +  , move: function (e) { +      if (!this.shown) return + +      switch(e.keyCode) { +        case 9: // tab +        case 13: // enter +        case 27: // escape +          e.preventDefault() +          break + +        case 38: // up arrow +          e.preventDefault() +          this.prev() +          break + +        case 40: // down arrow +          e.preventDefault() +          this.next() +          break +      } + +      e.stopPropagation() +    } + +  , keydown: function (e) { +      this.suppressKeyPressRepeat = ~$.inArray(e.keyCode, [40,38,9,13,27]) +      this.move(e) +    } + +  , keypress: function (e) { +      if (this.suppressKeyPressRepeat) return +      this.move(e) +    } + +  , keyup: function (e) { +      switch(e.keyCode) { +        case 40: // down arrow +        case 38: // up arrow +        case 16: // shift +        case 17: // ctrl +        case 18: // alt +          break + +        case 9: // tab +        case 13: // enter +          if (!this.shown) return +          this.select() +          break + +        case 27: // escape +          if (!this.shown) return +          this.hide() +          break + +        default: +          this.lookup() +      } + +      e.stopPropagation() +      e.preventDefault() +  } + +  , focus: function (e) { +      this.focused = true +    } + +  , blur: function (e) { +      this.focused = false +      if (!this.mousedover && this.shown) this.hide() +    } + +  , click: function (e) { +      e.stopPropagation() +      e.preventDefault() +      this.select() +      this.$element.focus() +    } + +  , mouseenter: function (e) { +      this.mousedover = true +      this.$menu.find('.active').removeClass('active') +      $(e.currentTarget).addClass('active') +    } + +  , mouseleave: function (e) { +      this.mousedover = false +      if (!this.focused && this.shown) this.hide() +    } + +  } + + +  /* TYPEAHEAD PLUGIN DEFINITION +   * =========================== */ + +  var old = $.fn.typeahead + +  $.fn.typeahead = function (option) { +    return this.each(function () { +      var $this = $(this) +        , data = $this.data('typeahead') +        , options = typeof option == 'object' && option +      if (!data) $this.data('typeahead', (data = new Typeahead(this, options))) +      if (typeof option == 'string') data[option]() +    }) +  } + +  $.fn.typeahead.defaults = { +    source: [] +  , items: 8 +  , menu: '<ul class="typeahead dropdown-menu"></ul>' +  , item: '<li><a href="#"></a></li>' +  , minLength: 1 +  } + +  $.fn.typeahead.Constructor = Typeahead + + + /* TYPEAHEAD NO CONFLICT +  * =================== */ + +  $.fn.typeahead.noConflict = function () { +    $.fn.typeahead = old +    return this +  } + + + /* TYPEAHEAD DATA-API +  * ================== */ + +  $(document).on('focus.typeahead.data-api', '[data-provide="typeahead"]', function (e) { +    var $this = $(this) +    if ($this.data('typeahead')) return +    $this.typeahead($this.data()) +  }) + +}(window.jQuery); diff --git a/pyload/web/app/scripts/vendor/jquery.omniwindow.js b/pyload/web/app/scripts/vendor/jquery.omniwindow.js new file mode 100644 index 000000000..e1f0b8f77 --- /dev/null +++ b/pyload/web/app/scripts/vendor/jquery.omniwindow.js @@ -0,0 +1,141 @@ +// jQuery OmniWindow plugin +// @version:  0.7.0 +// @author:   Rudenka Alexander (mur.mailbox@gmail.com) +// @license:  MIT + +;(function($) { +  "use strict"; +  $.fn.extend({ +    omniWindow: function(options) { + +      options = $.extend(true, { +        animationsPriority: { +          show: ['overlay', 'modal'], +          hide: ['modal', 'overlay'] +        }, +        overlay: { +          selector: '.ow-overlay', +          hideClass: 'ow-closed', +          animations: { +            show: function(subjects, internalCallback) { return internalCallback(subjects); }, +            hide: function(subjects, internalCallback) { return internalCallback(subjects); }, +            internal: { +              show: function(subjects){ subjects.overlay.removeClass(options.overlay.hideClass); }, +              hide: function(subjects){ subjects.overlay.addClass(options.overlay.hideClass); } +            } +          } +        }, +        modal:   { +          hideClass: 'ow-closed', +          animations: { +            show: function(subjects, internalCallback) { return internalCallback(subjects); }, +            hide: function(subjects, internalCallback) { return internalCallback(subjects); }, +            internal: { +              show: function(subjects){ subjects.modal.removeClass(options.modal.hideClass); }, +              hide: function(subjects){ subjects.modal.addClass(options.modal.hideClass); } +            } +          }, +          internal: { +            stateAttribute: 'ow-active' +          } +        }, +        eventsNames: { +          show: 'show.ow', +          hide: 'hide.ow', +          internal: { +            overlayClick:  'click.ow', +            keyboardKeyUp: 'keyup.ow' +          } +        }, +        callbacks: {                                                                                  // Callbacks execution chain +          beforeShow:  function(subjects, internalCallback) { return internalCallback(subjects); },   // 1 (stop if retruns false) +          positioning: function(subjects, internalCallback) { return internalCallback(subjects); },   // 2 +          afterShow:   function(subjects, internalCallback) { return internalCallback(subjects); },   // 3 +          beforeHide:  function(subjects, internalCallback) { return internalCallback(subjects); },   // 4 (stop if retruns false) +          afterHide:   function(subjects, internalCallback) { return internalCallback(subjects); },   // 5 +          internal: { +            beforeShow: function(subjects) { +              if (subjects.modal.data(options.modal.internal.stateAttribute)) { +                return false; +              } else { +                subjects.modal.data(options.modal.internal.stateAttribute, true); +                return true; +              } +            }, +            afterShow: function(subjects) { +              $(document).on(options.eventsNames.internal.keyboardKeyUp, function(e) { +                if (e.keyCode === 27) {                                              // if the key pressed is the ESC key +                  subjects.modal.trigger(options.eventsNames.hide); +                } +              }); + +              subjects.overlay.on(options.eventsNames.internal.overlayClick, function(){ +                subjects.modal.trigger(options.eventsNames.hide); +              }); +            }, +            positioning: function(subjects) { +              subjects.modal.css('margin-left', Math.round(subjects.modal.outerWidth() / -2)); +            }, +            beforeHide: function(subjects) { +              if (subjects.modal.data(options.modal.internal.stateAttribute)) { +                subjects.modal.data(options.modal.internal.stateAttribute, false); +                return true; +              } else { +                return false; +              } +            }, +            afterHide: function(subjects) { +              subjects.overlay.off(options.eventsNames.internal.overlayClick); +              $(document).off(options.eventsNames.internal.keyboardKeyUp); + +              subjects.overlay.css('display', ''); // clear inline styles after jQ animations +              subjects.modal.css('display', ''); +            } +          } +        } +      }, options); + +      var animate = function(process, subjects, callbackName) { +        var first  = options.animationsPriority[process][0], +            second = options.animationsPriority[process][1]; + +        options[first].animations[process](subjects, function(subjs) {        // call USER's    FIRST animation (depends on priority) +          options[first].animations.internal[process](subjs);                 // call internal  FIRST animation + +          options[second].animations[process](subjects, function(subjs) {     // call USER's    SECOND animation +            options[second].animations.internal[process](subjs);              // call internal  SECOND animation + +                                                                              // then we need to call USER's +                                                                              // afterShow of afterHide callback +            options.callbacks[callbackName](subjects, options.callbacks.internal[callbackName]); +          }); +        }); +      }; + +      var showModal = function(subjects) { +        if (!options.callbacks.beforeShow(subjects, options.callbacks.internal.beforeShow)) { return; } // cancel showing if beforeShow callback return false + +        options.callbacks.positioning(subjects, options.callbacks.internal.positioning); + +        animate('show', subjects, 'afterShow'); +      }; + +      var hideModal = function(subjects) { +        if (!options.callbacks.beforeHide(subjects, options.callbacks.internal.beforeHide)) { return; } // cancel hiding if beforeHide callback return false + +        animate('hide', subjects, 'afterHide'); +      }; + + +      var $overlay = $(options.overlay.selector); + +      return this.each(function() { +        var $modal  = $(this); +        var subjects = {modal: $modal, overlay: $overlay}; + +        $modal.bind(options.eventsNames.show, function(){ showModal(subjects); }) +              .bind(options.eventsNames.hide, function(){ hideModal(subjects); }); +      }); +    } +  }); +})(jQuery);
\ No newline at end of file diff --git a/pyload/web/app/scripts/vendor/remaining.js b/pyload/web/app/scripts/vendor/remaining.js new file mode 100644 index 000000000..d66a2931a --- /dev/null +++ b/pyload/web/app/scripts/vendor/remaining.js @@ -0,0 +1,149 @@ +/** + * Javascript Countdown + * Copyright (c) 2009 Markus Hedlund + * Version 1.1 + * Licensed under MIT license + * http://www.opensource.org/licenses/mit-license.php + * http://labs.mimmin.com/countdown + */ +define([], function() { +    var remaining = { +        /** +         * Get the difference of the passed date, and now. The different formats of the taget parameter are: +         * January 12, 2009 15:14:00     (Month dd, yyyy hh:mm:ss) +         * January 12, 2009              (Month dd, yyyy) +         * 09,00,12,15,14,00             (yy,mm,dd,hh,mm,ss) Months range from 0-11, not 1-12. +         * 09,00,12                      (yy,mm,dd)          Months range from 0-11, not 1-12. +         * 500                           (milliseconds) +         * 2009-01-12 15:14:00           (yyyy-mm-dd hh-mm-ss) +         * 2009-01-12 15:14              (yyyy-mm-dd hh-mm) +         * @param target Target date. Can be either a date object or a string (formated like '24 December, 2010 15:00:00') +         * @return Difference in seconds +         */ +        getSeconds: function(target) { +            var today = new Date(); + +            if (typeof(target) == 'object') { +                var targetDate = target; +            } else { +                var matches = target.match(/(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2})(:(\d{2}))?/);   // YYYY-MM-DD HH-MM-SS +                if (matches != null) { +                    matches[7] = typeof(matches[7]) == 'undefined' ? '00' : matches[7]; +                    var targetDate = new Date(matches[1], matches[2] - 1, matches[3], matches[4], matches[5], matches[7]); +                } else { +                    var targetDate = new Date(target); +                } +            } + +            return Math.floor((targetDate.getTime() - today.getTime()) / 1000); +        }, + +        /** +         * @param seconds Difference in seconds +         * @param i18n A language object (see code) +         * @param onlyLargestUnit Return only the largest unit (see documentation) +         * @param hideEmpty Hide empty units (see documentation) +         * @return String formated something like '1 week, 1 hours, 1 second' +         */ +        getString: function(seconds, i18n, onlyLargestUnit, hideEmpty) { +            if (seconds < 1) { +                return ''; +            } + +            if (typeof(hideEmpty) == 'undefined' || hideEmpty == null) { +                hideEmpty = true; +            } +            if (typeof(onlyLargestUnit) == 'undefined' || onlyLargestUnit == null) { +                onlyLargestUnit = false; +            } +            if (typeof(i18n) == 'undefined' || i18n == null) { +                i18n = { +                    weeks: ['week', 'weeks'], +                    days: ['day', 'days'], +                    hours: ['hour', 'hours'], +                    minutes: ['minute', 'minutes'], +                    seconds: ['second', 'seconds'] +                }; +            } + +            var units = { +                weeks: 7 * 24 * 60 * 60, +                days: 24 * 60 * 60, +                hours: 60 * 60, +                minutes: 60, +                seconds: 1 +            }; + +            var returnArray = []; +            var value; +            for (unit in units) { +                value = units[unit]; +                if (seconds / value >= 1 || unit == 'seconds' || !hideEmpty) { +                    secondsConverted = Math.floor(seconds / value); +                    var i18nUnit = i18n[unit][secondsConverted == 1 ? 0 : 1]; +                    returnArray.push(secondsConverted + ' ' + i18nUnit); +                    seconds -= secondsConverted * value; + +                    if (onlyLargestUnit) { +                        break; +                    } +                } +            } +            ; + +            return returnArray.join(', '); +        }, + +        /** +         * @param seconds Difference in seconds +         * @return String formated something like '169:00:01' +         */ +        getStringDigital: function(seconds) { +            if (seconds < 1) { +                return ''; +            } + +            remainingTime = remaining.getArray(seconds); + +            for (index in remainingTime) { +                remainingTime[index] = remaining.padNumber(remainingTime[index]); +            } +            ; + +            return remainingTime.join(':'); +        }, + +        /** +         * @param seconds Difference in seconds +         * @return Array with hours, minutes and seconds +         */ +        getArray: function(seconds) { +            if (seconds < 1) { +                return []; +            } + +            var units = [60 * 60, 60, 1]; + +            var returnArray = []; +            var value; +            for (index in units) { +                value = units[index]; +                secondsConverted = Math.floor(seconds / value); +                returnArray.push(secondsConverted); +                seconds -= secondsConverted * value; +            } +            ; + +            return returnArray; +        }, + +        /** +         * @param number An integer +         * @return Integer padded with a 0 if necessary +         */ +        padNumber: function(number) { +            return (number >= 0 && number < 10) ? '0' + number : number; +        } +    }; +    return remaining; +});
\ No newline at end of file diff --git a/pyload/web/app/scripts/views/abstract/itemView.js b/pyload/web/app/scripts/views/abstract/itemView.js new file mode 100644 index 000000000..c37118a4c --- /dev/null +++ b/pyload/web/app/scripts/views/abstract/itemView.js @@ -0,0 +1,47 @@ +define(['jquery', 'backbone', 'underscore'], function($, Backbone, _) { +    'use strict'; + +    // A view that is meant for temporary displaying +    // All events must be unbound in onDestroy +    return Backbone.View.extend({ + +        tagName: 'li', +        destroy: function() { +            this.undelegateEvents(); +            this.unbind(); +            if (this.onDestroy) { +                this.onDestroy(); +            } +            this.$el.removeData().unbind(); +            this.remove(); +        }, + +        hide: function() { +            this.$el.slideUp(); +        }, + +        show: function() { +            this.$el.slideDown(); +        }, + +        unrender: function() { +            var self = this; +            this.$el.slideUp(function() { +                self.destroy(); +            }); +        }, + +        deleteItem: function(e) { +            if (e) +                e.stopPropagation(); +            this.model.destroy(); +        }, + +        restart: function(e) { +            if(e) +                e.stopPropagation(); +            this.model.restart(); +        } + +    }); +});
\ No newline at end of file diff --git a/pyload/web/app/scripts/views/abstract/modalView.js b/pyload/web/app/scripts/views/abstract/modalView.js new file mode 100644 index 000000000..9d1d72869 --- /dev/null +++ b/pyload/web/app/scripts/views/abstract/modalView.js @@ -0,0 +1,125 @@ +define(['jquery', 'backbone', 'underscore', 'omniwindow'], function($, Backbone, _) { +    'use strict'; + +    return Backbone.View.extend({ + +        events: { +            'click .btn-confirm': 'confirm', +            'click .btn-close': 'hide', +            'click .close': 'hide' +        }, + +        template: null, +        dialog: null, + +        onHideDestroy: false, +        confirmCallback: null, + +        initialize: function(template, confirm) { +            this.confirmCallback = confirm; +            var self = this; +            if (this.template === null) { +                if (template) { +                    this.template = template; +                    // When template was provided this is a temporary dialog +                    this.onHideDestroy = true; +                } +                else +                    require(['text!tpl/default/modal.html'], function(template) { +                        self.template = template; +                    }); +            } + +        }, + +        // TODO: whole modal stuff is not very elegant +        render: function() { +            this.$el.html(this.template(this.renderContent())); +            this.onRender(); + +            if (this.dialog === null) { +                this.$el.addClass('modal hide'); +                this.$el.css({opacity: 0, scale: 0.7}); + +                var self = this; +                $('body').append(this.el); +                this.dialog = this.$el.omniWindow({ +                    overlay: { +                        selector: '#modal-overlay', +                        hideClass: 'hide', +                        animations: { +                            hide: function(subjects, internalCallback) { +                                subjects.overlay.transition({opacity: 'hide', delay: 100}, 300, function() { +                                    internalCallback(subjects); +                                    self.onHide(); +                                    if (self.onHideDestroy) +                                        self.destroy(); +                                }); +                            }, +                            show: function(subjects, internalCallback) { +                                subjects.overlay.fadeIn(300); +                                internalCallback(subjects); +                            }}}, +                    modal: { +                        hideClass: 'hide', +                        animations: { +                            hide: function(subjects, internalCallback) { +                                subjects.modal.transition({opacity: 'hide', scale: 0.7}, 300); +                                internalCallback(subjects); +                            }, + +                            show: function(subjects, internalCallback) { +                                subjects.modal.transition({opacity: 'show', scale: 1, delay: 100}, 300, function() { +                                    internalCallback(subjects); +                                }); +                            }} +                    }}); +            } + +            return this; +        }, + +        onRender: function() { + +        }, + +        renderContent: function() { +            return {}; +        }, + +        show: function() { +            if (this.dialog === null) +                this.render(); + +            this.dialog.trigger('show'); + +            this.onShow(); +        }, + +        onShow: function() { + +        }, + +        hide: function() { +            this.dialog.trigger('hide'); +        }, + +        onHide: function() { + +        }, + +        confirm: function() { +            if (this.confirmCallback) +                this.confirmCallback.apply(); + +            this.hide(); +        }, + +        destroy: function() { +            this.$el.remove(); +            this.dialog = null; +            this.remove(); +        } + +    }); +});
\ No newline at end of file diff --git a/pyload/web/app/scripts/views/accounts/accountListView.js b/pyload/web/app/scripts/views/accounts/accountListView.js new file mode 100644 index 000000000..4eb5bfe7d --- /dev/null +++ b/pyload/web/app/scripts/views/accounts/accountListView.js @@ -0,0 +1,52 @@ +define(['jquery', 'underscore', 'backbone', 'app', 'collections/AccountList', './accountView', +    'hbs!tpl/accounts/layout', 'hbs!tpl/accounts/actionbar'], +    function($, _, Backbone, App, AccountList, accountView, template, templateBar) { +        'use strict'; + +        // Renders settings over view page +        return Backbone.Marionette.CollectionView.extend({ + +            itemView: accountView, +            template: template, + +            collection: null, +            modal: null, + +            initialize: function() { +                this.actionbar = Backbone.Marionette.ItemView.extend({ +                    template: templateBar, +                    events: { +                        'click .btn': 'addAccount' +                    }, +                    addAccount: _.bind(this.addAccount, this) +                }); + +                this.collection = new AccountList(); +                this.update(); + +                this.listenTo(App.vent, 'accounts:updated', this.update); +            }, + +            update: function() { +                this.collection.fetch(); +            }, + +            onBeforeRender: function() { +                this.$el.html(template()); +            }, + +            appendHtml: function(collectionView, itemView, index) { +                this.$('.account-list').append(itemView.el); +            }, + +            addAccount: function() { +                var self = this; +                _.requireOnce(['views/accounts/accountModal'], function(Modal) { +                    if (self.modal === null) +                        self.modal = new Modal(); + +                    self.modal.show(); +                }); +            } +        }); +    });
\ No newline at end of file diff --git a/pyload/web/app/scripts/views/accounts/accountModal.js b/pyload/web/app/scripts/views/accounts/accountModal.js new file mode 100644 index 000000000..6c2b226df --- /dev/null +++ b/pyload/web/app/scripts/views/accounts/accountModal.js @@ -0,0 +1,72 @@ +define(['jquery', 'underscore', 'app', 'views/abstract/modalView', 'hbs!tpl/dialogs/addAccount', 'helpers/pluginIcon', 'select2'], +    function($, _, App, modalView, template, pluginIcon) { +        'use strict'; +        return modalView.extend({ + +            events: { +                'submit form': 'add', +                'click .btn-add': 'add' +            }, +            template: template, +            plugins: null, +            select: null, + +            initialize: function() { +                // Inherit parent events +                this.events = _.extend({}, modalView.prototype.events, this.events); +                var self = this; +                $.ajax(App.apiRequest('getAccountTypes', null, {success: function(data) { +                    self.plugins = _.sortBy(data, function(item) { +                        return item; +                    }); +                    self.render(); +                }})); +            }, + +            onRender: function() { +                // TODO: could be a separate input type if needed on multiple pages +                if (this.plugins) +                    this.select = this.$('#pluginSelect').select2({ +                        escapeMarkup: function(m) { +                            return m; +                        }, +                        formatResult: this.format, +                        formatSelection: this.format, +                        data: {results: this.plugins, text: function(item) { +                            return item; +                        }}, +                        id: function(item) { +                            return item; +                        } +                    }); +            }, + +            onShow: function() { +            }, + +            onHide: function() { +            }, + +            format: function(data) { +                return '<img class="logo-select" src="' + pluginIcon(data) + '"> ' + data; +            }, + +            add: function(e) { +                e.stopPropagation(); +                if (this.select) { +                    var plugin = this.select.val(), +                        login = this.$('#login').val(), +                        password = this.$('#password').val(), +                        self = this; + +                    $.ajax(App.apiRequest('updateAccount', { +                        plugin: plugin, login: login, password: password +                    }, { success: function() { +                        App.vent.trigger('accounts:updated'); +                        self.hide(); +                    }})); +                } +                return false; +            } +        }); +    });
\ No newline at end of file diff --git a/pyload/web/app/scripts/views/accounts/accountView.js b/pyload/web/app/scripts/views/accounts/accountView.js new file mode 100644 index 000000000..89f69d7e7 --- /dev/null +++ b/pyload/web/app/scripts/views/accounts/accountView.js @@ -0,0 +1,18 @@ +define(['jquery', 'underscore', 'backbone', 'app', 'hbs!tpl/accounts/account'], +    function($, _, Backbone, App, template) { +        'use strict'; + +        return Backbone.Marionette.ItemView.extend({ + +            tagName: 'tr', +            template: template, + +            events: { +                'click .btn-danger': 'deleteAccount' +            }, + +            deleteAccount: function() { +                this.model.destroy(); +            } +        }); +    });
\ No newline at end of file diff --git a/pyload/web/app/scripts/views/dashboard/dashboardView.js b/pyload/web/app/scripts/views/dashboard/dashboardView.js new file mode 100644 index 000000000..e7adba475 --- /dev/null +++ b/pyload/web/app/scripts/views/dashboard/dashboardView.js @@ -0,0 +1,168 @@ +define(['jquery', 'backbone', 'underscore', 'app', 'models/TreeCollection', +    './packageView', './fileView', 'hbs!tpl/dashboard/layout', 'select2'], +    function($, Backbone, _, App, TreeCollection, PackageView, FileView, template) { +        'use strict'; +        // Renders whole dashboard +        return Backbone.Marionette.ItemView.extend({ + +            // TODO:  refactor +            active: $('.breadcrumb .active'), + +            template: template, + +            events: { +            }, + +            ui: { +                'packages': '.package-list', +                'files': '.file-list' +            }, + +            // Package tree +            tree: null, +            // Current open files +            files: null, +            // True when loading animation is running +            isLoading: false, + +            initialize: function() { +                App.dashboard = this; +                this.tree = new TreeCollection(); + +                var self = this; +                // When package is added we reload the data +                App.vent.on('package:added', function() { +                    console.log('Package tree caught, package:added event'); +                    self.tree.fetch(); +                }); + +                App.vent.on('file:updated', _.bind(this.fileUpdated, this)); + +                // TODO: merge? +                this.init(); +                // TODO: file:added +                // TODO: package:deleted +                // TODO: package:updated +            }, + +            init: function() { +                var self = this; +                // TODO: put in separated function +                // TODO: order of elements? +                // Init the tree and callback for package added +                this.tree.fetch({success: function() { +                    self.update(); +                    self.tree.get('packages').on('add', function(pack) { +                        console.log('Package ' + pack.get('pid') + ' added to tree'); +                        self.appendPackage(pack, 0, true); +                        self.openPackage(pack); +                    }); +                }}); + +                this.$('.input').select2({tags: ['a', 'b', 'sdf']}); +            }, + +            update: function() { +                console.log('Update package list'); + +                // TODO: Both null +                var packs = this.tree.get('packages'); +                this.files = this.tree.get('files'); + +                if (packs) +                    packs.each(_.bind(this.appendPackage, this)); + +                if (!this.files || this.files.length === 0) { +                    // no files are displayed +                    this.files = null; +                    // Open the first package +                    if (packs && packs.length >= 1) +                        this.openPackage(packs.at(0)); +                } +                else +                    this.files.each(_.bind(this.appendFile, this)); + +                return this; +            }, + +            // TODO sorting ?! +            // Append a package to the list, index, animate it +            appendPackage: function(pack, i, animation) { +                var el = new PackageView({model: pack}).render().el; +                $(this.ui.packages).appendWithAnimation(el, animation); +            }, + +            appendFile: function(file, i, animation) { +                var el = new FileView({model: file}).render().el; +                $(this.ui.files).appendWithAnimation(el, animation); +            }, + +            // Show content of the packages on main view +            openPackage: function(pack) { +                var self = this; + +                // load animation only when something is shown and its different from current package +                if (this.files && this.files !== pack.get('files')) +                    self.loading(); + +                pack.fetch({silent: true, success: function() { +                    console.log('Package ' + pack.get('pid') + ' loaded'); +                    self.active.text(pack.get('name')); +                    self.contentReady(pack.get('files')); +                }, failure: function() { +                    self.failure(); +                }}); + +            }, + +            contentReady: function(files) { +                var old_files = this.files; +                this.files = files; +                App.vent.trigger('dashboard:contentReady'); + +                // show the files when no loading animation is running and not already open +                if (!this.isLoading && old_files !== files) +                    this.show(); +            }, + +            // Do load animation, remove the old stuff +            loading: function() { +                this.isLoading = true; +                this.files = null; +                var self = this; +                $(this.ui.files).fadeOut({complete: function() { +                    // All file views should vanish +                    App.vent.trigger('dashboard:destroyContent'); + +                    // Loading was faster than animation +                    if (self.files) +                        self.show(); + +                    self.isLoading = false; +                }}); +            }, + +            failure: function() { +                // TODO +            }, + +            show: function() { +                // fileUL has to be resetted before +                this.files.each(_.bind(this.appendFile, this)); +                //TODO: show placeholder when nothing is displayed (filtered content empty) +                $(this.ui.files).fadeIn(); +                App.vent.trigger('dashboard:updated'); +            }, + +            // Refresh the file if it is currently shown +            fileUpdated: function(data) { +                // this works with ids and object +                var file = this.files.get(data); +                if (file) +                    if (_.isObject(data)) // update directly +                        file.set(data); +                    else // fetch from server +                        file.fetch(); +            } +        }); +    });
\ No newline at end of file diff --git a/pyload/web/app/scripts/views/dashboard/fileView.js b/pyload/web/app/scripts/views/dashboard/fileView.js new file mode 100644 index 000000000..4e5884ed8 --- /dev/null +++ b/pyload/web/app/scripts/views/dashboard/fileView.js @@ -0,0 +1,102 @@ +define(['jquery', 'backbone', 'underscore', 'app', 'utils/apitypes', 'views/abstract/itemView', 'helpers/formatTime', 'hbs!tpl/dashboard/file'], +    function($, Backbone, _, App, Api, ItemView, formatTime, template) { +        'use strict'; + +        // Renders single file item +        return ItemView.extend({ + +            tagName: 'li', +            className: 'file-view row-fluid', +            template: template, +            events: { +                'click .checkbox': 'select', +                'click .btn-delete': 'deleteItem', +                'click .btn-restart': 'restart' +            }, + +            initialize: function() { +                this.listenTo(this.model, 'change', this.render); +                // This will be triggered manually and changed before with silent=true +                this.listenTo(this.model, 'change:visible', this.visibility_changed); +                this.listenTo(this.model, 'change:progress', this.progress_changed); +                this.listenTo(this.model, 'remove', this.unrender); +                this.listenTo(App.vent, 'dashboard:destroyContent', this.destroy); +            }, + +            onDestroy: function() { +            }, + +            render: function() { +                var data = this.model.toJSON(); +                if (data.download) { +                    var status = data.download.status; +                    if (status === Api.DownloadStatus.Offline || status === Api.DownloadStatus.TempOffline) +                        data.offline = true; +                    else if (status === Api.DownloadStatus.Online) +                        data.online = true; +                    else if (status === Api.DownloadStatus.Waiting) +                        data.waiting = true; +                    else if (status === Api.DownloadStatus.Downloading) +                        data.downloading = true; +                    else if (this.model.isFailed()) +                        data.failed = true; +                    else if (this.model.isFinished()) +                        data.finished = true; +                } + +                this.$el.html(this.template(data)); +                if (this.model.get('selected')) +                    this.$el.addClass('ui-selected'); +                else +                    this.$el.removeClass('ui-selected'); + +                if (this.model.get('visible')) +                    this.$el.show(); +                else +                    this.$el.hide(); + +                return this; +            }, + +            select: function(e) { +                e.preventDefault(); +                var checked = this.$el.hasClass('ui-selected'); +                // toggle class immediately, so no re-render needed +                this.model.set('selected', !checked, {silent: true}); +                this.$el.toggleClass('ui-selected'); +                App.vent.trigger('file:selection'); +            }, + +            visibility_changed: function(visible) { +                // TODO: improve animation, height is not available when element was not visible +                if (visible) +                    this.$el.slideOut(true); +                else { +                    this.$el.calculateHeight(true); +                    this.$el.slideIn(true); +                } +            }, + +            progress_changed: function() { +                if (!this.model.isDownload()) +                    return; + +                if (this.model.get('download').status === Api.DownloadStatus.Downloading) { +                    var bar = this.$('.progress .bar'); +                    if (!bar) { // ensure that the dl bar is rendered +                        this.render(); +                        bar = this.$('.progress .bar'); +                    } + +                    bar.width(this.model.get('progress') + '%'); +                    bar.html('  ' + formatTime(this.model.get('eta'))); +                } else if (this.model.get('download').status === Api.DownloadStatus.Waiting) { +                    this.$('.second').html( +                        '<i class="icon-time"></i> ' + formatTime(this.model.get('eta'))); + +                } else // Every else state can be renderred normally +                    this.render(); + +            } +        }); +    });
\ No newline at end of file diff --git a/pyload/web/app/scripts/views/dashboard/filterView.js b/pyload/web/app/scripts/views/dashboard/filterView.js new file mode 100644 index 000000000..64bc56724 --- /dev/null +++ b/pyload/web/app/scripts/views/dashboard/filterView.js @@ -0,0 +1,133 @@ +define(['jquery', 'backbone', 'underscore', 'app', 'utils/apitypes', 'models/Package', 'hbs!tpl/dashboard/actionbar'], +    /*jslint -W040: false*/ +    function($, Backbone, _, App, Api, Package, template) { +        'use strict'; + +        // Modified version of type ahead show, nearly the same without absolute positioning +        function show() { +            this.$menu +                .insertAfter(this.$element) +                .show(); + +            this.shown = true; +            return this; +        } + +        // Renders the actionbar for the dashboard, handles everything related to filtering displayed files +        return Backbone.Marionette.ItemView.extend({ + +            events: { +                'click .filter-type': 'filter_type', +                'click .filter-state': 'switch_filter', +                'submit .form-search': 'search' +            }, + +            ui: { +                'search': '.search-query', +                'stateMenu': '.dropdown-toggle .state' +            }, + +            template: template, +            state: null, + +            initialize: function() { +                this.state = Api.DownloadState.All; + +                // Apply the filter before the content is shown +                App.vent.on('dashboard:contentReady', _.bind(this.apply_filter, this)); +            }, + +            onRender: function() { +                                // use our modified method +                $.fn.typeahead.Constructor.prototype.show = show; +                this.ui.search.typeahead({ +                    minLength: 2, +                    source: this.getSuggestions +                }); + +            }, + +            // TODO: app level api request +            search: function(e) { +                e.stopPropagation(); +                var query = this.ui.search.val(); +                this.ui.search.val(''); + +                var pack = new Package(); +                // Overwrite fetch method to use a search +                // TODO: quite hackish, could be improved to filter packages +                //       or show performed search +                pack.fetch = function(options) { +                    pack.search(query, options); +                }; + +                App.dashboard.openPackage(pack); +            }, + +            getSuggestions: function(query, callback) { +                $.ajax(App.apiRequest('searchSuggestions', {pattern: query}, { +                    method: 'POST', +                    success: function(data) { +                        callback(data); +                    } +                })); +            }, + +            switch_filter: function(e) { +                e.stopPropagation(); +                var element = $(e.target); +                var state = parseInt(element.data('state'), 10); +                var menu = this.ui.stateMenu.parent().parent(); +                menu.removeClass('open'); + +                if (state === Api.DownloadState.Finished) { +                    menu.removeClass().addClass('dropdown finished'); +                } else if (state === Api.DownloadState.Unfinished) { +                    menu.removeClass().addClass('dropdown active'); +                } else if (state === Api.DownloadState.Failed) { +                    menu.removeClass().addClass('dropdown failed'); +                } else { +                    menu.removeClass().addClass('dropdown'); +                } + +                this.state = state; +                this.ui.stateMenu.text(element.text()); +                this.apply_filter(); +            }, + +            // Applies the filtering to current open files +            apply_filter: function() { +                if (!App.dashboard.files) +                    return; + +                var self = this; +                App.dashboard.files.map(function(file) { +                    var visible = file.get('visible'); +                    if (visible !== self.is_visible(file)) { +                        file.set('visible', !visible, {silent: true}); +                        file.trigger('change:visible', !visible); +                    } +                }); + +                App.vent.trigger('dashboard:filtered'); +            }, + +            // determine if a file should be visible +            // TODO: non download files +            is_visible: function(file) { +                if (this.state === Api.DownloadState.Finished) +                    return file.isFinished(); +                else if (this.state === Api.DownloadState.Unfinished) +                    return file.isUnfinished(); +                else if (this.state === Api.DownloadState.Failed) +                    return file.isFailed(); + +                return true; +            }, + +            filter_type: function(e) { + +            } + +        }); +    });
\ No newline at end of file diff --git a/pyload/web/app/scripts/views/dashboard/packageView.js b/pyload/web/app/scripts/views/dashboard/packageView.js new file mode 100644 index 000000000..2738fcbea --- /dev/null +++ b/pyload/web/app/scripts/views/dashboard/packageView.js @@ -0,0 +1,75 @@ +define(['jquery', 'app', 'views/abstract/itemView', 'underscore', 'hbs!tpl/dashboard/package'], +    function($, App, itemView, _, template) { +        'use strict'; + +        // Renders a single package item +        return itemView.extend({ + +            tagName: 'li', +            className: 'package-view', +            template: template, +            events: { +                'click .package-name, .btn-open': 'open', +                'click .icon-refresh': 'restart', +                'click .select': 'select', +                'click .btn-delete': 'deleteItem' +            }, + +            // Ul for child packages (unused) +            ul: null, +            // Currently unused +            expanded: false, + +            initialize: function() { +                this.listenTo(this.model, 'filter:added', this.hide); +                this.listenTo(this.model, 'filter:removed', this.show); +                this.listenTo(this.model, 'change', this.render); +                this.listenTo(this.model, 'remove', this.unrender); + +                // Clear drop down menu +                var self = this; +                this.$el.on('mouseleave', function() { +                    self.$('.dropdown-menu').parent().removeClass('open'); +                }); +            }, + +            onDestroy: function() { +            }, + +            // Render everything, optional only the fileViews +            render: function() { +                this.$el.html(this.template(this.model.toJSON())); +                this.$el.initTooltips(); + +                return this; +            }, + +            unrender: function() { +                itemView.prototype.unrender.apply(this); + +                // TODO: display other package +                App.vent.trigger('dashboard:loading', null); +            }, + + +            // TODO +            // Toggle expanding of packages +            expand: function(e) { +                e.preventDefault(); +            }, + +            open: function(e) { +                e.preventDefault(); +                App.dashboard.openPackage(this.model); +            }, + +            select: function(e) { +                e.preventDefault(); +                var checked = this.$('.select').hasClass('icon-check'); +                // toggle class immediately, so no re-render needed +                this.model.set('selected', !checked, {silent: true}); +                this.$('.select').toggleClass('icon-check').toggleClass('icon-check-empty'); +                App.vent.trigger('package:selection'); +            } +        }); +    });
\ No newline at end of file diff --git a/pyload/web/app/scripts/views/dashboard/selectionView.js b/pyload/web/app/scripts/views/dashboard/selectionView.js new file mode 100644 index 000000000..8685fd849 --- /dev/null +++ b/pyload/web/app/scripts/views/dashboard/selectionView.js @@ -0,0 +1,155 @@ +define(['jquery', 'backbone', 'underscore', 'app', 'hbs!tpl/dashboard/select'], +    function($, Backbone, _, App, template) { +        'use strict'; + +        // Renders context actions for selection packages and files +        return Backbone.Marionette.ItemView.extend({ + +            el: '#selection-area', +            template: template, + +            events: { +                'click .icon-check': 'deselect', +                'click .icon-pause': 'pause', +                'click .icon-trash': 'trash', +                'click .icon-refresh': 'restart' +            }, + +            // Element of the action bar +            actionBar: null, +            // number of currently selected elements +            current: 0, + +            initialize: function() { +                this.$el.calculateHeight().height(0); +                var render = _.bind(this.render, this); + +                App.vent.on('dashboard:updated', render); +                App.vent.on('dashboard:filtered', render); +                App.vent.on('package:selection', render); +                App.vent.on('file:selection', render); + +                this.actionBar = $('.actionbar .btn-check'); +                this.actionBar.parent().click(_.bind(this.select_toggle, this)); + +                // API events, maybe better to rely on internal ones? +                App.vent.on('package:deleted', render); +                App.vent.on('file:deleted', render); +            }, + +            get_files: function(all) { +                var files = []; +                if (App.dashboard.files) +                    if (all) +                        files = App.dashboard.files.where({visible: true}); +                    else +                        files = App.dashboard.files.where({selected: true, visible: true}); + +                return files; +            }, + +            get_packs: function() { +                if (!App.dashboard.tree.get('packages')) +                    return []; // TODO + +                return App.dashboard.tree.get('packages').where({selected: true}); +            }, + +            render: function() { +                var files = this.get_files().length; +                var packs = this.get_packs().length; + +                if (files + packs > 0) { +                    this.$el.html(this.template({files: files, packs: packs})); +                    this.$el.initTooltips('bottom'); +                } + +                if (files + packs > 0 && this.current === 0) +                    this.$el.slideOut(); +                else if (files + packs === 0 && this.current > 0) +                    this.$el.slideIn(); + +                // TODO: accessing ui directly, should be events +                if (files > 0) { +                    this.actionBar.addClass('icon-check').removeClass('icon-check-empty'); +                    App.dashboard.ui.packages.addClass('ui-files-selected'); +                } +                else { +                    this.actionBar.addClass('icon-check-empty').removeClass('icon-check'); +                    App.dashboard.ui.packages.removeClass('ui-files-selected'); +                } + +                this.current = files + packs; +            }, + +            // Deselects all items +            deselect: function() { +                this.get_files().map(function(file) { +                    file.set('selected', false); +                }); + +                this.get_packs().map(function(pack) { +                    pack.set('selected', false); +                }); + +                this.render(); +            }, + +            pause: function() { +                alert('Not implemented yet'); +                this.deselect(); +            }, + +            trash: function() { +                _.confirm('default/confirmDialog.html', function() { + +                    var pids = []; +                    // TODO: delete many at once +                    this.get_packs().map(function(pack) { +                        pids.push(pack.get('pid')); +                        pack.destroy(); +                    }); + +                    // get only the fids of non deleted packages +                    var fids = _.filter(this.get_files(),function(file) { +                        return !_.contains(pids, file.get('package')); +                    }).map(function(file) { +                            file.destroyLocal(); +                            return file.get('fid'); +                        }); + +                    if (fids.length > 0) +                        $.ajax(App.apiRequest('deleteFiles', {fids: fids})); + +                    this.deselect(); +                }, this); +            }, + +            restart: function() { +                this.get_files().map(function(file) { +                    file.restart(); +                }); +                this.get_packs().map(function(pack) { +                    pack.restart(); +                }); + +                this.deselect(); +            }, + +            // Select or deselect all visible files +            select_toggle: function() { +                var files = this.get_files(); +                if (files.length === 0) { +                    this.get_files(true).map(function(file) { +                        file.set('selected', true); +                    }); + +                } else +                    files.map(function(file) { +                        file.set('selected', false); +                    }); + +                this.render(); +            } +        }); +    });
\ No newline at end of file diff --git a/pyload/web/app/scripts/views/headerView.js b/pyload/web/app/scripts/views/headerView.js new file mode 100644 index 000000000..512c7259b --- /dev/null +++ b/pyload/web/app/scripts/views/headerView.js @@ -0,0 +1,240 @@ +define(['jquery', 'underscore', 'backbone', 'app', 'models/ServerStatus', 'collections/ProgressList', +    'views/progressView', 'views/notificationView', 'helpers/formatSize', 'hbs!tpl/header/layout', +    'hbs!tpl/header/status', 'hbs!tpl/header/progressbar' , 'flot'], +    function($, _, Backbone, App, ServerStatus, ProgressList, ProgressView, NotificationView, formatSize, +             template, templateStatus, templateHeader) { +        'use strict'; +        // Renders the header with all information +        return Backbone.Marionette.ItemView.extend({ + +            events: { +                'click .icon-list': 'toggle_taskList', +                'click .popover .close': 'toggle_taskList', +                'click .btn-grabber': 'open_grabber' +            }, + +            ui: { +                progress: '.progress-list', +                speedgraph: '#speedgraph' +            }, + +            // todo: maybe combine these +            template: template, +            templateStatus: templateStatus, +            templateHeader: templateHeader, + +            // view +            grabber: null, +            speedgraph: null, + +            // models and data +            ws: null, +            status: null, +            progressList: null, +            speeds: null, + +            // sub view +            notificationView: null, + +            // save if last progress was empty +            wasEmpty: false, + +            initialize: function() { +                var self = this; +                this.notificationView = new NotificationView(); + +                this.status = new ServerStatus(); +                this.listenTo(this.status, 'change', this.update); + +                this.progressList = new ProgressList(); +                this.listenTo(this.progressList, 'add', function(model) { +                    self.ui.progress.appendWithAnimation(new ProgressView({model: model}).render().el); +                }); + +                // TODO: button to start stop refresh +                var ws = App.openWebSocket('/async'); +                ws.onopen = function() { +                    ws.send(JSON.stringify('start')); +                }; +                // TODO compare with polling +                ws.onmessage = _.bind(this.onData, this); +                ws.onerror = function(error) { +                    console.log(error); +                    alert('WebSocket error' + error); +                }; + +                this.ws = ws; +            }, + +            initGraph: function() { +                var totalPoints = 120; +                var data = []; + +                // init with empty data +                while (data.length < totalPoints) +                    data.push([data.length, 0]); + +                this.speeds = data; +                this.speedgraph = $.plot(this.ui.speedgraph, [this.speeds], { +                    series: { +                        lines: { show: true, lineWidth: 2 }, +                        shadowSize: 0, +                        color: '#fee247' +                    }, +                    xaxis: { ticks: [] }, +                    yaxis: { ticks: [], min: 1, autoscaleMargin: 0.1, tickFormatter: function(data) { +                        return formatSize(data * 1024); +                    }, position: 'right' }, +                    grid: { +                        show: true, +//            borderColor: "#757575", +                        borderColor: 'white', +                        borderWidth: 1, +                        labelMargin: 0, +                        axisMargin: 0, +                        minBorderMargin: 0 +                    } +                }); + +            }, + +            // Must be called after view was attached +            init: function() { +                this.initGraph(); +                this.update(); +            }, + +            update: function() { +                // TODO: what should be displayed in the header +                // queue/processing size? + +                var status = this.status.toJSON(); +                status.maxspeed = _.max(this.speeds, function(speed) { +                    return speed[1]; +                })[1] * 1024; +                this.$('.status-block').html( +                    this.templateStatus(status) +                ); + +                var data = {tasks: 0, downloads: 0, speed: 0, single: false}; +                this.progressList.each(function(progress) { +                    if (progress.isDownload()) { +                        data.downloads += 1; +                        data.speed += progress.get('download').speed; +                    } else +                        data.tasks++; +                }); + +                // Show progress of one task +                if (data.tasks + data.downloads === 1) { +                    var progress = this.progressList.at(0); +                    data.single = true; +                    data.eta = progress.get('eta'); +                    data.percent = progress.getPercent(); +                    data.name = progress.get('name'); +                    data.statusmsg = progress.get('statusmsg'); +                } +                // TODO: better progressbar rendering + +                data.etaqueue = status.eta; +                data.linksqueue = status.linksqueue; +                data.sizequeue = status.sizequeue; + +                this.$('#progress-info').html( +                    this.templateHeader(data) +                ); +                return this; +            }, + +            toggle_taskList: function() { +                this.$('.popover').animate({opacity: 'toggle'}); +            }, + +            open_grabber: function() { +                var self = this; +                _.requireOnce(['views/linkGrabberModal'], function(ModalView) { +                    if (self.grabber === null) +                        self.grabber = new ModalView(); + +                    self.grabber.show(); +                }); +            }, + +            onData: function(evt) { +                var data = JSON.parse(evt.data); +                if (data === null) return; + +                if (data['@class'] === 'ServerStatus') { +                    // TODO: load interaction when none available +                    this.status.set(data); + +                    // There tasks at the server, but not in queue: so fetch them +                    // or there are tasks in our queue but not on the server +                    if (this.status.get('notifications') && !this.notificationView.tasks.hasTaskWaiting() || +                        !this.status.get('notifications') && this.notificationView.tasks.hasTaskWaiting()) +                        this.notificationView.tasks.fetch(); + +                    this.speeds = this.speeds.slice(1); +                    this.speeds.push([this.speeds[this.speeds.length - 1][0] + 1, Math.floor(data.speed / 1024)]); + +                    // TODO: if everything is 0 re-render is not needed +                    this.speedgraph.setData([this.speeds]); +                    // adjust the axis +                    this.speedgraph.setupGrid(); +                    this.speedgraph.draw(); + +                } +                else if (_.isArray(data)) +                    this.onProgressUpdate(data); +                else if (data['@class'] === 'EventInfo') +                    this.onEvent(data.eventname, data.event_args); +                else +                    console.log('Unknown Async input', data); + +            }, + +            onProgressUpdate: function(progress) { +                // generate a unique id +                _.each(progress, function(prog) { +                    if (prog.download) +                        prog.pid = prog.download.fid; +                    else +                        prog.pid = prog.plugin + prog.name; +                }); + +                this.progressList.set(progress); +                // update currently open files with progress +                this.progressList.each(function(prog) { +                    if (prog.isDownload() && App.dashboard.files) { +                        var file = App.dashboard.files.get(prog.get('download').fid); +                        if (file) { +                            file.set({ +                                progress: prog.getPercent(), +                                eta: prog.get('eta') +                            }, {silent: true}); + +                            file.trigger('change:progress'); +                        } +                    } +                }); + +                if (progress.length === 0) { +                    // only render one time when last was not empty already +                    if (!this.wasEmpty) { +                        this.update(); +                        this.wasEmpty = true; +                    } +                } else { +                    this.wasEmpty = false; +                    this.update(); +                } +            }, + +            onEvent: function(event, args) { +                args.unshift(event); +                console.log('Core send event', args); +                App.vent.trigger.apply(App.vent, args); +            } + +        }); +    });
\ No newline at end of file diff --git a/pyload/web/app/scripts/views/input/inputLoader.js b/pyload/web/app/scripts/views/input/inputLoader.js new file mode 100644 index 000000000..11665abb4 --- /dev/null +++ b/pyload/web/app/scripts/views/input/inputLoader.js @@ -0,0 +1,8 @@ +define(['./textInput'], function(textInput) { +    'use strict'; + +    // selects appropriate input element +    return function(input, value, default_value, description) { +        return textInput; +    }; +});
\ No newline at end of file diff --git a/pyload/web/app/scripts/views/input/inputView.js b/pyload/web/app/scripts/views/input/inputView.js new file mode 100644 index 000000000..1fbe5042d --- /dev/null +++ b/pyload/web/app/scripts/views/input/inputView.js @@ -0,0 +1,86 @@ +define(['jquery', 'backbone', 'underscore'], function($, Backbone, _) { +    'use strict'; + +    // Renders input elements +    return Backbone.View.extend({ + +        tagName: 'input', + +        input: null, +        value: null, +        default_value: null, +        description: null, + +        // enables tooltips +        tooltip: true, + +        initialize: function(options) { +            this.input = options.input; +            this.value = options.value; +            this.default_value = options.default_value; +            this.description = options.description; +        }, + +        render: function() { +            this.renderInput(); +            // data for tooltips +            if (this.description && this.tooltip) { +                this.$el.data('content', this.description); +                // TODO: render default value in popup? +//                this.$el.data('title', "TODO: title"); +                this.$el.popover({ +                    placement: 'right', +                    trigger: 'hover' +//                    delay: { show: 500, hide: 100 } +                }); +            } + +            return this; +        }, + +        renderInput: function() { +            // Overwrite this +        }, + +        showTooltip: function() { +            if (this.description && this.tooltip) +                this.$el.popover('show'); +        }, + +        hideTooltip: function() { +            if (this.description && this.tooltip) +                this.$el.popover('hide'); +        }, + +        destroy: function() { +            this.undelegateEvents(); +            this.unbind(); +            if (this.onDestroy) { +                this.onDestroy(); +            } +            this.$el.removeData().unbind(); +            this.remove(); +        }, + +        // focus the input element +        focus: function() { +            this.$el.focus(); +        }, + +        // Clear the input +        clear: function() { + +        }, + +        // retrieve value of the input +        getVal: function() { +            return this.value; +        }, + +        // the child class must call this when the value changed +        setVal: function(value) { +            this.value = value; +            this.trigger('change', value); +        } +    }); +});
\ No newline at end of file diff --git a/pyload/web/app/scripts/views/input/textInput.js b/pyload/web/app/scripts/views/input/textInput.js new file mode 100644 index 000000000..0eebbf91e --- /dev/null +++ b/pyload/web/app/scripts/views/input/textInput.js @@ -0,0 +1,36 @@ +define(['jquery', 'backbone', 'underscore', './inputView'], function($, Backbone, _, inputView) { +    'use strict'; + +    return inputView.extend({ + +        // TODO +        tagName: 'input', +        events: { +            'keyup': 'onChange', +            'focus': 'showTooltip', +            'focusout': 'hideTooltip' +        }, + +        renderInput: function() { +            this.$el.attr('type', 'text'); +            this.$el.attr('name', 'textInput'); + +            if (this.default_value) +                this.$el.attr('placeholder', this.default_value); + +            if (this.value) +                this.$el.val(this.value); + +            return this; +        }, + +        clear: function() { +            this.$el.val(''); +        }, + +        onChange: function(e) { +            this.setVal(this.$el.val()); +        } + +    }); +});
\ No newline at end of file diff --git a/pyload/web/app/scripts/views/linkGrabberModal.js b/pyload/web/app/scripts/views/linkGrabberModal.js new file mode 100644 index 000000000..e6f59c134 --- /dev/null +++ b/pyload/web/app/scripts/views/linkGrabberModal.js @@ -0,0 +1,49 @@ +define(['jquery', 'underscore', 'app', 'views/abstract/modalView', 'hbs!tpl/dialogs/linkgrabber'], +    function($, _, App, modalView, template) { +        'use strict'; +        // Modal dialog for package adding - triggers package:added when package was added +        return modalView.extend({ + +            events: { +                'click .btn-success': 'addPackage', +                'keypress #inputPackageName': 'addOnEnter' +            }, + +            template: template, + +            initialize: function() { +                // Inherit parent events +                this.events = _.extend({}, modalView.prototype.events, this.events); +            }, + +            addOnEnter: function(e) { +                if (e.keyCode !== 13) return; +                this.addPackage(e); +            }, + +            addPackage: function(e) { +                var self = this; +                var options = App.apiRequest('addPackage', +                    { +                        name: $('#inputPackageName').val(), +                        // TODO: better parsing / tokenization +                        links: $('#inputLinks').val().split('\n') +                    }, +                    { +                        success: function() { +                            App.vent.trigger('package:added'); +                            self.hide(); +                        } +                    }); + +                $.ajax(options); +                $('#inputPackageName').val(''); +                $('#inputLinks').val(''); +            }, + +            onShow: function() { +                this.$('#inputPackageName').focus(); +            } + +        }); +    });
\ No newline at end of file diff --git a/pyload/web/app/scripts/views/loginView.js b/pyload/web/app/scripts/views/loginView.js new file mode 100644 index 000000000..891b3ec99 --- /dev/null +++ b/pyload/web/app/scripts/views/loginView.js @@ -0,0 +1,37 @@ +define(['jquery', 'backbone', 'underscore', 'app', 'hbs!tpl/login'], +    function($, Backbone, _, App, template) { +        'use strict'; + +        // Renders context actions for selection packages and files +        return Backbone.Marionette.ItemView.extend({ +            template: template, + +            events: { +                'submit form': 'login' +            }, + +            ui: { +                'form': 'form' +            }, + +            login: function(e) { +                e.stopPropagation(); + +                var options = App.apiRequest('login', null, { +                    data: this.ui.form.serialize(), +                    type : 'post', +                    success: function(data) { +                        // TODO: go to last page, better error +                        if (data) +                            App.navigate(''); +                        else +                            alert('Wrong login'); +                    } +                }); + +                $.ajax(options); +                return false; +            } + +        }); +    });
\ No newline at end of file diff --git a/pyload/web/app/scripts/views/notificationView.js b/pyload/web/app/scripts/views/notificationView.js new file mode 100644 index 000000000..abfcd8079 --- /dev/null +++ b/pyload/web/app/scripts/views/notificationView.js @@ -0,0 +1,83 @@ +define(['jquery', 'backbone', 'underscore', 'app', 'collections/InteractionList', 'hbs!tpl/notification'], +    function($, Backbone, _, App, InteractionList, queryModal, template) { +        'use strict'; + +        // Renders context actions for selection packages and files +        return Backbone.View.extend({ + +            // Only view for this area so it's hardcoded +            el: '#notification-area', +            template: template, + +            events: { +                'click .btn-query': 'openQuery', +                'click .btn-notification': 'openNotifications' +            }, + +            tasks: null, +            // area is slided out +            visible: false, +            // the dialog +            modal: null, + +            initialize: function() { +                this.tasks = new InteractionList(); + +                this.$el.calculateHeight().height(0); + +                App.vent.on('interaction:added', _.bind(this.onAdd, this)); +                App.vent.on('interaction:deleted', _.bind(this.onDelete, this)); + +                var render = _.bind(this.render, this); +                this.listenTo(this.tasks, 'add', render); +                this.listenTo(this.tasks, 'remove', render); + +            }, + +            onAdd: function(task) { +                this.tasks.add(task); +            }, + +            onDelete: function(task) { +                this.tasks.remove(task); +            }, + +            render: function() { + +                // only render when it will be visible +                if (this.tasks.length > 0) +                    this.$el.html(this.template(this.tasks.toJSON())); + +                if (this.tasks.length > 0 && !this.visible) { +                    this.$el.slideOut(); +                    this.visible = true; +                } +                else if (this.tasks.length === 0 && this.visible) { +                    this.$el.slideIn(); +                    this.visible = false; +                } + +                return this; +            }, + +            openQuery: function() { +                var self = this; + +                _.requireOnce(['views/queryModal'], function(ModalView) { +                    if (self.modal === null) { +                        self.modal = new ModalView(); +                        self.modal.parent = self; +                    } + +                    self.modal.model = self.tasks.at(0); +                    self.modal.render(); +                    self.modal.show(); +                }); + +            }, + +            openNotifications: function() { + +            } +        }); +    });
\ No newline at end of file diff --git a/pyload/web/app/scripts/views/progressView.js b/pyload/web/app/scripts/views/progressView.js new file mode 100644 index 000000000..3a4bb2825 --- /dev/null +++ b/pyload/web/app/scripts/views/progressView.js @@ -0,0 +1,33 @@ +define(['jquery', 'backbone', 'underscore', 'app', 'utils/apitypes', 'views/abstract/itemView', +    'hbs!tpl/header/progress', 'helpers/pluginIcon'], +    function($, Backbone, _, App, Api, ItemView, template, pluginIcon) { +        'use strict'; + +        // Renders single file item +        return ItemView.extend({ + +            idAttribute: 'pid', +            tagName: 'li', +            template: template, +            events: { +            }, + +            initialize: function() { +                this.listenTo(this.model, 'change', this.render); +                this.listenTo(this.model, 'remove', this.unrender); +            }, + +            onDestroy: function() { +            }, + +            render: function() { +                // TODO: icon +                // TODO: other states +                // TODO: non download progress +                // TODO: better progressbar rendering +                this.$el.css('background-image', 'url('+ pluginIcon('todo') +')'); +                this.$el.html(this.template(this.model.toJSON())); +                return this; +            } +        }); +    });
\ No newline at end of file diff --git a/pyload/web/app/scripts/views/queryModal.js b/pyload/web/app/scripts/views/queryModal.js new file mode 100644 index 000000000..7c6439b49 --- /dev/null +++ b/pyload/web/app/scripts/views/queryModal.js @@ -0,0 +1,69 @@ +define(['jquery', 'underscore', 'app', 'views/abstract/modalView', './input/inputLoader', 'text!tpl/default/queryDialog.html'], +    function($, _, App, modalView, load_input, template) { +        'use strict'; +        return modalView.extend({ + +            // TODO: submit on enter reloads the page sometimes +            events: { +                'click .btn-success': 'submit', +                'submit form': 'submit' +            }, +            template: _.compile(template), + +            // the notificationView +            parent: null, + +            model: null, +            input: null, + +            initialize: function() { +                // Inherit parent events +                this.events = _.extend({}, modalView.prototype.events, this.events); +            }, + +            renderContent: function() { +                var data = { +                    title: this.model.get('title'), +                    plugin: this.model.get('plugin'), +                    description: this.model.get('description') +                }; + +                var input = this.model.get('input').data; +                if (this.model.isCaptcha()) { +                    data.captcha = input[0]; +                    data.type = input[1]; +                } +                return data; +            }, + +            onRender: function() { +                // instantiate the input +                var input = this.model.get('input'); +                var InputView = load_input(input); +                this.input = new InputView(input); +                // only renders after wards +                this.$('#inputField').append(this.input.render().el); +            }, + +            submit: function(e) { +                e.stopPropagation(); +                // TODO: load next task + +                this.model.set('result', this.input.getVal()); +                var self = this; +                this.model.save({success: function() { +                    self.hide(); +                }}); + +                this.input.clear(); +            }, + +            onShow: function() { +                this.input.focus(); +            }, + +            onHide: function() { +                this.input.destroy(); +            } +        }); +    });
\ No newline at end of file diff --git a/pyload/web/app/scripts/views/settings/configSectionView.js b/pyload/web/app/scripts/views/settings/configSectionView.js new file mode 100644 index 000000000..e05701b2a --- /dev/null +++ b/pyload/web/app/scripts/views/settings/configSectionView.js @@ -0,0 +1,99 @@ +define(['jquery', 'underscore', 'backbone', 'app', '../abstract/itemView', '../input/inputLoader', +    'hbs!tpl/settings/config', 'hbs!tpl/settings/configItem'], +    function($, _, Backbone, App, itemView, load_input, template, templateItem) { +        'use strict'; + +        // Renders settings over view page +        return itemView.extend({ + +            tagName: 'div', + +            template: template, +            templateItem: templateItem, + +            // Will only render one time with further attribute updates +            rendered: false, + +            events: { +                'click .btn-primary': 'submit', +                'click .btn-reset': 'reset' +            }, + +            initialize: function() { +                this.listenTo(this.model, 'destroy', this.destroy); +            }, + +            render: function() { +                if (!this.rendered) { +                    this.$el.html(this.template(this.model.toJSON())); + +                    // initialize the popover +                    this.$('.page-header a').popover({ +                        placement: 'left' +//                        trigger: 'hover' +                    }); + +                    var container = this.$('.control-content'); +                    var self = this; +                    _.each(this.model.get('items'), function(item) { +                        var json = item.toJSON(); +                        var el = $('<div>').html(self.templateItem(json)); +                        var InputView = load_input(item.get('input')); +                        var input = new InputView(json).render(); +                        item.set('inputView', input); + +                        self.listenTo(input, 'change', _.bind(self.render, self)); +                        el.find('.controls').append(input.el); +                        container.append(el); +                    }); +                    this.rendered = true; +                } +                // Enable button if something is changed +                if (this.model.hasChanges()) +                    this.$('.btn-primary').removeClass('disabled'); +                else +                    this.$('.btn-primary').addClass('disabled'); + +                // Mark all inputs that are modified +                _.each(this.model.get('items'), function(item) { +                    var input = item.get('inputView'); +                    var el = input.$el.parent().parent(); +                    if (item.isChanged()) +                        el.addClass('info'); +                    else +                        el.removeClass('info'); +                }); + +                return this; +            }, + +            onDestroy: function() { +                // TODO: correct cleanup after building up so many views and models +            }, + +            submit: function(e) { +                e.stopPropagation(); +                // TODO: success / failure popups +                var self = this; +                this.model.save({success: function() { +                    self.render(); +                    App.settingsView.refresh(); +                }}); + +            }, + +            reset: function(e) { +                e.stopPropagation(); +                // restore the original value +                _.each(this.model.get('items'), function(item) { +                    if (item.has('inputView')) { +                        var input = item.get('inputView'); +                        input.setVal(item.get('value')); +                        input.render(); +                    } +                }); +                this.render(); +            } + +        }); +    });
\ No newline at end of file diff --git a/pyload/web/app/scripts/views/settings/pluginChooserModal.js b/pyload/web/app/scripts/views/settings/pluginChooserModal.js new file mode 100644 index 000000000..91e9f11b3 --- /dev/null +++ b/pyload/web/app/scripts/views/settings/pluginChooserModal.js @@ -0,0 +1,69 @@ +define(['jquery', 'underscore', 'app', 'views/abstract/modalView', 'hbs!tpl/dialogs/addPluginConfig', +    'helpers/pluginIcon', 'select2'], +    function($, _, App, modalView, template, pluginIcon) { +        'use strict'; +        return modalView.extend({ + +            events: { +                'click .btn-add': 'add' +            }, +            template: template, +            plugins: null, +            select: null, + +            initialize: function() { +                // Inherit parent events +                this.events = _.extend({}, modalView.prototype.events, this.events); +                var self = this; +                $.ajax(App.apiRequest('getAvailablePlugins', null, {success: function(data) { +                    self.plugins = _.sortBy(data, function(item) { +                        return item.name; +                    }); +                    self.render(); +                }})); +            }, + +            onRender: function() { +                // TODO: could be a seperate input type if needed on multiple pages +                if (this.plugins) +                    this.select = this.$('#pluginSelect').select2({ +                        escapeMarkup: function(m) { +                            return m; +                        }, +                        formatResult: this.format, +                        formatSelection: this.formatSelection, +                        data: {results: this.plugins, text: function(item) { +                            return item.label; +                        }}, +                        id: function(item) { +                            return item.name; +                        } +                    }); +            }, + +            onShow: function() { +            }, + +            onHide: function() { +            }, + +            format: function(data) { +                var s = '<div class="plugin-select" style="background-image: url(' + pluginIcon(data.name) +')">' + data.label; +                s += '<br><span>' + data.description + '<span></div>'; +                return s; +            }, + +            formatSelection: function(data) { +                return '<img class="logo-select" src="' + pluginIcon(data.name) + '"> ' + data.label; +            }, + +            add: function(e) { +                e.stopPropagation(); +                if (this.select) { +                    var plugin = this.select.val(); +                    App.vent.trigger('config:open', plugin); +                    this.hide(); +                } +            } +        }); +    });
\ No newline at end of file diff --git a/pyload/web/app/scripts/views/settings/settingsView.js b/pyload/web/app/scripts/views/settings/settingsView.js new file mode 100644 index 000000000..cad5ab075 --- /dev/null +++ b/pyload/web/app/scripts/views/settings/settingsView.js @@ -0,0 +1,184 @@ +define(['jquery', 'underscore', 'backbone', 'app', 'models/ConfigHolder', './configSectionView', +    'hbs!tpl/settings/layout', 'hbs!tpl/settings/menu', 'hbs!tpl/settings/actionbar'], +    function($, _, Backbone, App, ConfigHolder, ConfigSectionView, template, templateMenu, templateBar) { +        'use strict'; + +        // Renders settings over view page +        return Backbone.Marionette.ItemView.extend({ + +            template: template, +            templateMenu: templateMenu, + +            events: { +                'click .settings-menu li > a': 'change_section', +                'click .btn-add': 'choosePlugin', // TODO not in scope +                'click .icon-remove': 'deleteConfig' +            }, + +            ui: { +                'menu': '.settings-menu', +                'content': '.setting-box > form' +            }, + +            selected: null, +            modal: null, + +            coreConfig: null, // It seems collections are not needed +            pluginConfig: null, + +            // currently open configHolder +            config: null, +            lastConfig: null, +            isLoading: false, + +            initialize: function() { +                this.actionbar = Backbone.Marionette.ItemView.extend({ +                    template: templateBar, +                    events: { +                        'click .btn': 'choosePlugin' +                    }, +                    choosePlugin: _.bind(this.choosePlugin, this) + +                }); +                this.listenTo(App.vent, 'config:open', this.openConfig); + +                this.refresh(); +            }, + +            refresh: function() { +                var self = this; +                $.ajax(App.apiRequest('getCoreConfig', null, {success: function(data) { +                    self.coreConfig = data; +                    self.renderMenu(); +                }})); +                $.ajax(App.apiRequest('getPluginConfig', null, {success: function(data) { +                    self.pluginConfig = data; +                    self.renderMenu(); +                }})); +            }, + +            onRender: function() { +                // set a height with css so animations will work +                this.ui.content.height(this.ui.content.height()); +            }, + +            renderMenu: function() { +                var plugins = [], +                    addons = []; + +                // separate addons and default plugins +                // addons have an activated state +                _.each(this.pluginConfig, function(item) { +                    if (item.activated === null) +                        plugins.push(item); +                    else +                        addons.push(item); +                }); + +                this.ui.menu.html(this.templateMenu({ +                    core: this.coreConfig, +                    plugin: plugins, +                    addon: addons +                })); + +                // mark the selected element +                this.$('li[data-name="' + this.selected + '"]').addClass('active'); +            }, + +            openConfig: function(name) { +                // Do nothing when this config is already open +                if (this.config && this.config.get('name') === name) +                    return; + +                this.lastConfig = this.config; +                this.config = new ConfigHolder({name: name}); +                this.loading(); + +                var self = this; +                this.config.fetch({success: function() { +                    if (!self.isLoading) +                        self.show(); + +                }, failure: _.bind(this.failure, this)}); + +            }, + +            loading: function() { +                this.isLoading = true; +                var self = this; +                this.ui.content.fadeOut({complete: function() { +                    if (self.config.isLoaded()) +                        self.show(); + +                    self.isLoading = false; +                }}); + +            }, + +            show: function() { +                // TODO animations are bit sloppy +                this.ui.content.css('display', 'block'); +                var oldHeight = this.ui.content.height(); + +                // this will destroy the old view +                if (this.lastConfig) +                    this.lastConfig.trigger('destroy'); +                else +                    this.ui.content.empty(); + +                // reset the height +                this.ui.content.css('height', ''); +                // append the new element +                this.ui.content.append(new ConfigSectionView({model: this.config}).render().el); +                // get the new height +                var height = this.ui.content.height(); +                // set the old height again +                this.ui.content.height(oldHeight); +                this.ui.content.animate({ +                    opacity: 'show', +                    height: height +                }); +            }, + +            failure: function() { +                // TODO +                this.config = null; +            }, + +            change_section: function(e) { +                // TODO check for changes +                // TODO move this into render? + +                var el = $(e.target).closest('li'); + +                this.selected = el.data('name'); +                this.openConfig(this.selected); + +                this.ui.menu.find('li.active').removeClass('active'); +                el.addClass('active'); +                e.preventDefault(); +            }, + +            choosePlugin: function(e) { +                var self = this; +                _.requireOnce(['views/settings/pluginChooserModal'], function(Modal) { +                    if (self.modal === null) +                        self.modal = new Modal(); + +                    self.modal.show(); +                }); +            }, + +            deleteConfig: function(e) { +                e.stopPropagation(); +                var el = $(e.target).parent().parent(); +                var name = el.data('name'); +                var self = this; +                $.ajax(App.apiRequest('deleteConfig', {plugin: name}, { success: function() { +                    self.refresh(); +                }})); + +            } + +        }); +    });
\ No newline at end of file | 
