<commit_before>/**
 * Components and binding handlers for the dashboard "onboarding" interface.
 * Includes a custom component for OSF project typeahead search, as well
 * the viewmodels for each of the individual onboarding widgets.
 */
;(function (global, factory) {
    if (typeof define === 'function' && define.amd) {
        define(['jquery', 'knockout'], factory);
    } else if (typeof $script === 'function') {
        $script.ready(['typeahead'], function() {
            factory(jQuery, ko);
            $script.done('onboarder');
        });
    } else {
        factory(jQuery, ko);
    }
}(this, function ($, ko) {
    'use strict';

    function noop() {}
    var MAX_RESULTS = 14;
    var DEFAULT_FETCH_URL = '/api/v1/dashboard/get_nodes/';

    var substringMatcher = function(strs) {
        return function findMatches(q, cb) {

            // an array that will be populated with substring matches
            var matches = [];

            // regex used to determine if a string contains the substring `q`

            var substrRegex = new RegExp(q, 'i');
            var count = 0;
            // iterate through the pool of strings and for any string that
            // contains the substring `q`, add it to the `matches` array
            $.each(strs, function(i, str) {
                if (substrRegex.test(str.name)) {
                    count += 1;
                    // the typeahead jQuery plugin expects suggestions to a
                    // JavaScript object, refer to typeahead docs for more info
                    matches.push({ value: str });

                    //alex's hack to limit number of results
                    if(count > MAX_RESULTS){
                        return false;
                    }
                    // above you can return name or a dict of name/node id,
                }
                // add an event to the input box -- listens for keystrokes and if there is a keystroke then it should clearrr
                //
            });

            cb(matches);
        };
    };

    function initTypeahead(element, myProjects, viewModel, params){
        var $inputElem = $(element);
        $inputElem.typeahead({
            hint: false,
            highlight: true,
            minLength: 0
        },
        {
            // name: 'projectSearch' + nodeType + namespace,
            displayKey: function(data) {
                return data.value.name;
            },
            templates: {
                suggestion: Handlebars.compile('<p>{{value.name}}</p> ' +
                                               '<p><small class="ob-suggestion-date text-muted">' +
                                               'modified {{value.dateModified.local}}</small></p>')
            },
            source: substringMatcher(myProjects)
        });

        $inputElem.bind('typeahead:selected', function(obj, datum) {
            // Call the parent viewModel's onSelected
            var onSelected = params.onSelected || viewModel.onSelected;
            onSelected(datum.value);
        });
        return $inputElem;
    }

    // Defines the format of items in the typeahead data source
    function serializeNode(node) {
        var dateModified = new $.osf.FormattableDate(node.date_modified);
        return {
            name: node.title,
            id: node.id,
            dateModified: dateModified,
            urls: {
                web: node.url,
                api: node.api_url,
                register: node.url + 'register/',
                upload: node.api_url + 'osffiles/',
                files: node.url + 'files/',
                children: node.api_url + 'get_children/'
            }
        };
    }

    /**
     * Binding handler for attaching an OSF typeahead search input.
     * Takes an optional parameter onSelected, which is called when a project
     * is selected.
     *
     * Params:
     *  data: An Array of data or a URL where to fetch nodes. Defaults to the dashboard node endpoint.
     *  onSelected: Callback for when a node is selected.
     *  onFetched: Callback for when nodes are fetched from server.
     *
     * Example:
     *
     *  <div data-bind="projectSearch: {url: '/api/v1/dashboard/get_nodes/',
     *                                     onSelected: onSelected}></div>
     */
    ko.bindingHandlers.projectSearch = {
        update: function(element, valueAccessor, allBindings, viewModel) {
            var params = valueAccessor() || {};
            // Either an Array of nodes or a URL
            var nodesOrURL = ko.unwrap(params.data);

            if (Array.isArray(nodesOrURL)) {
                var nodes = params.data;
                // Compute relevant URLs for each search result
                var myProjects = nodes.map(serializeNode);
                var $typeahead = initTypeahead(element, myProjects, viewModel, params);
                // Attach $typeahead element to viewModel
                viewModel.$typeahead = $typeahead;
                var onFetched = ko.unwrap(params.onFetched);
                if (onFetched) {
                    onFetched(myProjects);
                }
            // TODO: Repetition here. Refactor.
            } else if (typeof nodesOrURL === 'string') { // params.data is a URL
                var url = nodesOrURL;
                var request = $.getJSON(url, function (projects) {
                    // Compute relevant URLs for each search result
                    var myProjects = projects.nodes.map(serializeNode);
                    var $typeahead = initTypeahead(element, myProjects, viewModel, params);
                    // Attach $typeahead element to viewModel
                    viewModel.$typeahead = $typeahead;
                    var onFetched = ko.unwrap(params.onFetched);
                    if (onFetched) {
                        onFetched(myProjects);
                    }
                });
                request.fail(function(xhr, textStatus, error) {
                    Raven.captureMessage('Could not fetch dashboard nodes.', {
                        url: url, textStatus: textStatus, error: error
                    });
                });
            }
        }
    };

    /**
     * ViewModel for the OSF project typeahead search widget.
     *
     * Template: osf-project-search element in components/dashboard_templates.mako
     *
     * Params:
     *  onSubmit: Function to call on submit. Receives the selected item.
     */
    function ProjectSearchViewModel(params) {
        var self = this;
        self.params = params || {};
        self.heading = params.heading;

        self.data = params.data || DEFAULT_FETCH_URL;

        /* Observables */
        // If params.enableComponents is passed in, use that value, otherwise default to true
        var enableComps = params.enableComponents;
        self.showComponents = ko.observable(typeof enableComps !== 'undefined' ? enableComps : true);
        self.selectedProject = ko.observable(null);
        self.selectedComponent = ko.observable(null);
        /* Computeds */
        self.hasSelectedProject = ko.computed(function() {
            return self.selectedProject() !== null;
        });
        self.hasSelectedComponent = ko.computed(function() {
            return self.selectedComponent() !== null;
        });

        self.showSubmit = ko.computed(function() {
            return self.hasSelectedProject();
        });

        // Project name to display in the text input
        self.selectedProjectName = ko.computed(function() {
            return self.selectedProject() ? self.selectedProject().name : '';
        });
        // Component name to display in the text input
        self.selectedComponentName = ko.computed(function() {
            return self.selectedComponent() ? self.selectedComponent().name : '';
        });

        self.componentURL = ko.computed(function() {
            return self.selectedProject() ? self.selectedProject().urls.children : null;
        });

        /* Functions */
        self.onSubmit = function() {
            var func = params.onSubmit || noop;
            var selected = self.selectedComponent() || self.selectedProject();
            func(selected);
        };
        self.onSelectedProject = function(selected) {
            self.selectedProject(selected);
        };
        self.onSelectedComponent = function(selected) {
            self.selectedComponent(selected);
        };
        self.onFetchedComponents = function(components) {
            // Show component search only if selected project has components
            self.showComponents(Boolean(components.length));
            var func = params.onFetchedComponents || noop;
            func(components);
        };
        self.clearSearch = function() {
            self.selectedComponent(null);
            self.selectedProject(null);
            // This must be set after clearing selectedProject
            // to avoid sending extra request in the projectSearch
            // binding handler
            self.showComponents(true);
        };
    }

    ko.components.register('osf-project-search', {
        viewModel: ProjectSearchViewModel,
        template: {element: 'osf-project-search'}
    });


    ///// Register /////

    /**
     * ViewModel for the onboarding project registration component.
     *
     * Template: osf-ob-register element in components/dashboard_templates.mako
     */
    function OBRegisterViewModel(params) {
        var self = this;
        self.params = params || {};
        self.data = params.data || DEFAULT_FETCH_URL;
        /* Observables */
        self.isOpen = ko.observable(false);
        /* Functions */
        self.open = function() {
            self.isOpen(true);
        };
        self.close = function() {
            self.isOpen(false);
        };
        self.toggle = function() {
            if (!self.isOpen()) {
                self.open();
            } else {
                self.close();
            }
        };
        /* On submit, redirect to the selected page's registration page */
        self.onRegisterSubmit = function(selected) {
            window.location = selected.urls.register;
        };
    }

    ko.components.register('osf-ob-register', {
        viewModel: OBRegisterViewModel,
        template: {element: 'osf-ob-register'}
    });

    ///// UPLOADER //////
    var iconList = [
    '_blank',
    '_page',
    'aac',
    'ai',
    'aiff',
    'avi',
    'bmp',
    'c',
    'cpp',
    'css',
    'dat',
    'dmg',
    'doc',
    'dotx',
    'dwg',
    'dxf',
    'eps',
    'exe',
    'flv',
    'gif',
    'h',
    'hpp',
    'html',
    'ics',
    'iso',
    'java',
    'jpg',
    'js',
    'key',
    'less',
    'mid',
    'mp3',
    'mp4',
    'mpg',
    'odf',
    'ods',
    'odt',
    'otp',
    'ots',
    'ott',
    'pdf',
    'php',
    'png',
    'ppt',
    'psd',
    'py',
    'qt',
    'rar',
    'rb',
    'rtf',
    'sass',
    'scss',
    'sql',
    'tga',
    'tgz',
    'tiff',
    'txt',
    'wav',
    'xls',
    'xlsx',
    'xml',
    'yml',
    'zip'
    ];

    // this takes a filename and finds the icon for it
    function getFiletypeIcon(file_name){
        var baseUrl ='/static/img/upload_icons/';
        var ext = file_name.split('.').pop().toLowerCase();
        if(iconList.indexOf(ext)  !== -1){
            return baseUrl + ext + '.png';
        }else{
            return baseUrl + '_blank.png';
        }
    }

    // if has an extention return it, else return the last three chars
    function getExtension(string){
        if(string.indexOf('.') !== -1){
            return string.split('.').pop().toLowerCase();
        }else{
            return string.substring(string.length - 3);
        }
    }

    // truncate long file names
    function truncateFilename(string){
        var ext = getExtension(string);
        if (string.length > 40){
            return string.substring(0, 40-ext.length-3) + '...' + ext;
        } else{
            return string;
        }
    }

    /**
     * ViewModel for the onboarding uploader
     */
    function OBUploaderViewModel(params) {
        var self = this;
        self.params = params || {};
        self.selector = self.params.selector || '#obDropzone';
        self.data = params.data || DEFAULT_FETCH_URL;
        /* Observables */
        self.progress = ko.observable(0);
        self.showProgress = ko.observable(false);
        self.errorMessage = ko.observable('');
        self.enableUpload = ko.observable(true);
        self.filename = ko.observable('');
        self.iconSrc = ko.observable('//:0');
        self.uploadCount = ko.observable(1);
        self.disableUpload = ko.observable(false);
        self.disableComponents = ko.observable(false);
        // Flashed messages
        self.message = ko.observable('');
        self.messageClass = ko.observable('text-info');
        // The target node to upload to to
        self.target = ko.observable(null);
        /* Functions */
        self.startUpload = function(selected) {
            if (!self.dropzone.getQueuedFiles().length) {
                self.changeMessage('Please select at least one file to upload.', 'text-danger');
                return false;
            }
            self.target(selected);
            self.clearMessages();
            self.showProgress(true);
            self.dropzone.options.url = selected.urls.upload;
            self.dropzone.processQueue(); // Tell Dropzone to process all queued files.
        };
        self.clearMessages = function() {
            self.message('');
            self.messageClass('text-info');
        };
        self.clearDropzone = function() {
            self.enableUpload(true);
            self.dropzone.removeAllFiles();
            self.uploadCount(1);
            self.clearMessages();
        };
        self.onFetchedComponents = function(components) {
            if (!components.length) {
                self.disableComponents(true);
            }
        };
        /** Change the flashed message. */
        self.changeMessage = function(text, css, timeout) {
            self.message(text);
            var cssClass = css || 'text-info';
            self.messageClass(cssClass);
            if (timeout) {
                // Reset message after timeout period
                setTimeout(function() {
                    self.clearMessages();
                }, timeout);
            }
        };


        var dropzoneOpts = {

            url: '/', // specified per upload
            autoProcessQueue: false,
            createImageThumbnails: false,
            //over
            maxFiles: 9001,
            uploadMultiple: false,
            //in mib
            maxFilesize: 128,

            uploadprogress: function(file, progress) { // progress bar update
                self.progress(progress);
            },

            init: function() {
                var dropzone = this;

                // file add error logic
                this.on('error', function(file){
                    var fileName = file.name;
                    var fileSize = file.size;
                    dropzone.removeFile(file);
                    if (dropzone.files.length === 0){
                        self.enableUpload(true);
                        dropzone.removeAllFiles();
                    }
                    if (fileSize > dropzone.options.maxFilesize){
                        self.changeMessage(fileName + ' is too big (max = ' +
                                         dropzone.options.maxFilesize +
                                         ' MiB) and was not added to the upload queue.', 'text-danger');
                    } else {
                        self.changeMessage(fileName + 'could not be added to the upload queue', 'text-danger');
                        Raven.captureMessage('Could not upload: ' + fileName);
                    }
                });
                this.on('drop',function(){ // clear errors on drop or click
                    self.clearMessages();
                });
                // upload and process queue logic
                this.on('success',function(){
                    self.filename(self.uploadCount() + ' / ' + dropzone.files.length + ' files');
                    dropzone.processQueue(); // this is a bit hackish -- it fails to process full queue but this ensures it runs the process again after each success.
                    var oldCount = self.uploadCount();
                    self.uploadCount(oldCount + 1);

                    if(self.uploadCount() > dropzone.files.length){ // when finished redirect to project/component page where uploaded.
                        self.changeMessage('Success!', 'text-success');
                        window.location = self.target().urls.files;
                    }
                });

                // add file logic and dropzone to file display swap
                this.on('addedfile', function() {
                    if(dropzone.files.length>1){
                        self.iconSrc('/static/img/upload_icons/multiple_blank.png');
                        self.filename(dropzone.files.length + ' files');
                    }else{
                        var fileName = truncateFilename(dropzone.files[0].name);
                        self.iconSrc(getFiletypeIcon(fileName));
                        self.filename(fileName);
                    }
                    self.enableUpload(false);
                });
            }
        };
        self.dropzone = new Dropzone(self.selector, dropzoneOpts);
    }

    ko.components.register('osf-ob-uploader', {
        viewModel: OBUploaderViewModel,
        template: {element: 'osf-ob-uploader'}
    });
}));
<commit_msg>Reduce duplication in typahead initialization<commit_after>/**
 * Components and binding handlers for the dashboard "onboarding" interface.
 * Includes a custom component for OSF project typeahead search, as well
 * the viewmodels for each of the individual onboarding widgets.
 */
;(function (global, factory) {
    if (typeof define === 'function' && define.amd) {
        define(['jquery', 'knockout'], factory);
    } else if (typeof $script === 'function') {
        $script.ready(['typeahead'], function() {
            factory(jQuery, ko);
            $script.done('onboarder');
        });
    } else {
        factory(jQuery, ko);
    }
}(this, function ($, ko) {
    'use strict';

    function noop() {}
    var MAX_RESULTS = 14;
    var DEFAULT_FETCH_URL = '/api/v1/dashboard/get_nodes/';

    var substringMatcher = function(strs) {
        return function findMatches(q, cb) {

            // an array that will be populated with substring matches
            var matches = [];

            // regex used to determine if a string contains the substring `q`

            var substrRegex = new RegExp(q, 'i');
            var count = 0;
            // iterate through the pool of strings and for any string that
            // contains the substring `q`, add it to the `matches` array
            $.each(strs, function(i, str) {
                if (substrRegex.test(str.name)) {
                    count += 1;
                    // the typeahead jQuery plugin expects suggestions to a
                    // JavaScript object, refer to typeahead docs for more info
                    matches.push({ value: str });

                    //alex's hack to limit number of results
                    if(count > MAX_RESULTS){
                        return false;
                    }
                    // above you can return name or a dict of name/node id,
                }
                // add an event to the input box -- listens for keystrokes and if there is a keystroke then it should clearrr
                //
            });

            cb(matches);
        };
    };

    function initTypeahead(element, nodes, viewModel, params){
        var $inputElem = $(element);
        var myProjects = nodes.map(serializeNode);
        $inputElem.typeahead({
            hint: false,
            highlight: true,
            minLength: 0
        },
        {
            // name: 'projectSearch' + nodeType + namespace,
            displayKey: function(data) {
                return data.value.name;
            },
            templates: {
                suggestion: Handlebars.compile('<p>{{value.name}}</p> ' +
                                               '<p><small class="ob-suggestion-date text-muted">' +
                                               'modified {{value.dateModified.local}}</small></p>')
            },
            source: substringMatcher(myProjects)
        });

        $inputElem.bind('typeahead:selected', function(obj, datum) {
            // Call the parent viewModel's onSelected
            var onSelected = params.onSelected || viewModel.onSelected;
            onSelected(datum.value);
        });
        var onFetched = ko.unwrap(params.onFetched);
        if (onFetched) {
            onFetched(myProjects);
        }
        // Attach $typeahead element to viewModel
        viewModel.$typeahead = $inputElem;
        return $inputElem;
    }

    // Defines the format of items in the typeahead data source
    function serializeNode(node) {
        var dateModified = new $.osf.FormattableDate(node.date_modified);
        return {
            name: node.title,
            id: node.id,
            dateModified: dateModified,
            urls: {
                web: node.url,
                api: node.api_url,
                register: node.url + 'register/',
                upload: node.api_url + 'osffiles/',
                files: node.url + 'files/',
                children: node.api_url + 'get_children/'
            }
        };
    }

    /**
     * Binding handler for attaching an OSF typeahead search input.
     * Takes an optional parameter onSelected, which is called when a project
     * is selected.
     *
     * Params:
     *  data: An Array of data or a URL where to fetch nodes. Defaults to the dashboard node endpoint.
     *  onSelected: Callback for when a node is selected.
     *  onFetched: Callback for when nodes are fetched from server.
     *
     * Example:
     *
     *  <div data-bind="projectSearch: {url: '/api/v1/dashboard/get_nodes/',
     *                                     onSelected: onSelected}></div>
     */
    ko.bindingHandlers.projectSearch = {
        update: function(element, valueAccessor, allBindings, viewModel) {
            var params = valueAccessor() || {};
            // Either an Array of nodes or a URL
            var nodesOrURL = ko.unwrap(params.data);

            if (Array.isArray(nodesOrURL)) {
                var nodes = params.data;
                // Compute relevant URLs for each search result
                initTypeahead(element, nodes, viewModel, params);
            } else if (typeof nodesOrURL === 'string') { // params.data is a URL
                var url = nodesOrURL;
                var request = $.getJSON(url, function (response) {
                    // Compute relevant URLs for each search result
                    initTypeahead(element, response.nodes, viewModel, params);
                });
                request.fail(function(xhr, textStatus, error) {
                    Raven.captureMessage('Could not fetch dashboard nodes.', {
                        url: url, textStatus: textStatus, error: error
                    });
                });
            }
        }
    };

    /**
     * ViewModel for the OSF project typeahead search widget.
     *
     * Template: osf-project-search element in components/dashboard_templates.mako
     *
     * Params:
     *  onSubmit: Function to call on submit. Receives the selected item.
     */
    function ProjectSearchViewModel(params) {
        var self = this;
        self.params = params || {};
        self.heading = params.heading;

        self.data = params.data || DEFAULT_FETCH_URL;

        /* Observables */
        // If params.enableComponents is passed in, use that value, otherwise default to true
        var enableComps = params.enableComponents;
        self.showComponents = ko.observable(typeof enableComps !== 'undefined' ? enableComps : true);
        self.selectedProject = ko.observable(null);
        self.selectedComponent = ko.observable(null);
        /* Computeds */
        self.hasSelectedProject = ko.computed(function() {
            return self.selectedProject() !== null;
        });
        self.hasSelectedComponent = ko.computed(function() {
            return self.selectedComponent() !== null;
        });

        self.showSubmit = ko.computed(function() {
            return self.hasSelectedProject();
        });

        // Project name to display in the text input
        self.selectedProjectName = ko.computed(function() {
            return self.selectedProject() ? self.selectedProject().name : '';
        });
        // Component name to display in the text input
        self.selectedComponentName = ko.computed(function() {
            return self.selectedComponent() ? self.selectedComponent().name : '';
        });

        self.componentURL = ko.computed(function() {
            return self.selectedProject() ? self.selectedProject().urls.children : null;
        });

        /* Functions */
        self.onSubmit = function() {
            var func = params.onSubmit || noop;
            var selected = self.selectedComponent() || self.selectedProject();
            func(selected);
        };
        self.onSelectedProject = function(selected) {
            self.selectedProject(selected);
        };
        self.onSelectedComponent = function(selected) {
            self.selectedComponent(selected);
        };
        self.onFetchedComponents = function(components) {
            // Show component search only if selected project has components
            self.showComponents(Boolean(components.length));
            var func = params.onFetchedComponents || noop;
            func(components);
        };
        self.clearSearch = function() {
            self.selectedComponent(null);
            self.selectedProject(null);
            // This must be set after clearing selectedProject
            // to avoid sending extra request in the projectSearch
            // binding handler
            self.showComponents(true);
        };
    }

    ko.components.register('osf-project-search', {
        viewModel: ProjectSearchViewModel,
        template: {element: 'osf-project-search'}
    });


    ///// Register /////

    /**
     * ViewModel for the onboarding project registration component.
     *
     * Template: osf-ob-register element in components/dashboard_templates.mako
     */
    function OBRegisterViewModel(params) {
        var self = this;
        self.params = params || {};
        self.data = params.data || DEFAULT_FETCH_URL;
        /* Observables */
        self.isOpen = ko.observable(false);
        /* Functions */
        self.open = function() {
            self.isOpen(true);
        };
        self.close = function() {
            self.isOpen(false);
        };
        self.toggle = function() {
            if (!self.isOpen()) {
                self.open();
            } else {
                self.close();
            }
        };
        /* On submit, redirect to the selected page's registration page */
        self.onRegisterSubmit = function(selected) {
            window.location = selected.urls.register;
        };
    }

    ko.components.register('osf-ob-register', {
        viewModel: OBRegisterViewModel,
        template: {element: 'osf-ob-register'}
    });

    ///// UPLOADER //////
    var iconList = [
    '_blank',
    '_page',
    'aac',
    'ai',
    'aiff',
    'avi',
    'bmp',
    'c',
    'cpp',
    'css',
    'dat',
    'dmg',
    'doc',
    'dotx',
    'dwg',
    'dxf',
    'eps',
    'exe',
    'flv',
    'gif',
    'h',
    'hpp',
    'html',
    'ics',
    'iso',
    'java',
    'jpg',
    'js',
    'key',
    'less',
    'mid',
    'mp3',
    'mp4',
    'mpg',
    'odf',
    'ods',
    'odt',
    'otp',
    'ots',
    'ott',
    'pdf',
    'php',
    'png',
    'ppt',
    'psd',
    'py',
    'qt',
    'rar',
    'rb',
    'rtf',
    'sass',
    'scss',
    'sql',
    'tga',
    'tgz',
    'tiff',
    'txt',
    'wav',
    'xls',
    'xlsx',
    'xml',
    'yml',
    'zip'
    ];

    // this takes a filename and finds the icon for it
    function getFiletypeIcon(file_name){
        var baseUrl ='/static/img/upload_icons/';
        var ext = file_name.split('.').pop().toLowerCase();
        if(iconList.indexOf(ext)  !== -1){
            return baseUrl + ext + '.png';
        }else{
            return baseUrl + '_blank.png';
        }
    }

    // if has an extention return it, else return the last three chars
    function getExtension(string){
        if(string.indexOf('.') !== -1){
            return string.split('.').pop().toLowerCase();
        }else{
            return string.substring(string.length - 3);
        }
    }

    // truncate long file names
    function truncateFilename(string){
        var ext = getExtension(string);
        if (string.length > 40){
            return string.substring(0, 40-ext.length-3) + '...' + ext;
        } else{
            return string;
        }
    }

    /**
     * ViewModel for the onboarding uploader
     */
    function OBUploaderViewModel(params) {
        var self = this;
        self.params = params || {};
        self.selector = self.params.selector || '#obDropzone';
        self.data = params.data || DEFAULT_FETCH_URL;
        /* Observables */
        self.progress = ko.observable(0);
        self.showProgress = ko.observable(false);
        self.errorMessage = ko.observable('');
        self.enableUpload = ko.observable(true);
        self.filename = ko.observable('');
        self.iconSrc = ko.observable('//:0');
        self.uploadCount = ko.observable(1);
        self.disableUpload = ko.observable(false);
        self.disableComponents = ko.observable(false);
        // Flashed messages
        self.message = ko.observable('');
        self.messageClass = ko.observable('text-info');
        // The target node to upload to to
        self.target = ko.observable(null);
        /* Functions */
        self.startUpload = function(selected) {
            if (!self.dropzone.getQueuedFiles().length) {
                self.changeMessage('Please select at least one file to upload.', 'text-danger');
                return false;
            }
            self.target(selected);
            self.clearMessages();
            self.showProgress(true);
            self.dropzone.options.url = selected.urls.upload;
            self.dropzone.processQueue(); // Tell Dropzone to process all queued files.
        };
        self.clearMessages = function() {
            self.message('');
            self.messageClass('text-info');
        };
        self.clearDropzone = function() {
            self.enableUpload(true);
            self.dropzone.removeAllFiles();
            self.uploadCount(1);
            self.clearMessages();
        };
        self.onFetchedComponents = function(components) {
            if (!components.length) {
                self.disableComponents(true);
            }
        };
        /** Change the flashed message. */
        self.changeMessage = function(text, css, timeout) {
            self.message(text);
            var cssClass = css || 'text-info';
            self.messageClass(cssClass);
            if (timeout) {
                // Reset message after timeout period
                setTimeout(function() {
                    self.clearMessages();
                }, timeout);
            }
        };


        var dropzoneOpts = {

            url: '/', // specified per upload
            autoProcessQueue: false,
            createImageThumbnails: false,
            //over
            maxFiles: 9001,
            uploadMultiple: false,
            //in mib
            maxFilesize: 128,

            uploadprogress: function(file, progress) { // progress bar update
                self.progress(progress);
            },

            init: function() {
                var dropzone = this;

                // file add error logic
                this.on('error', function(file){
                    var fileName = file.name;
                    var fileSize = file.size;
                    dropzone.removeFile(file);
                    if (dropzone.files.length === 0){
                        self.enableUpload(true);
                        dropzone.removeAllFiles();
                    }
                    if (fileSize > dropzone.options.maxFilesize){
                        self.changeMessage(fileName + ' is too big (max = ' +
                                         dropzone.options.maxFilesize +
                                         ' MiB) and was not added to the upload queue.', 'text-danger');
                    } else {
                        self.changeMessage(fileName + 'could not be added to the upload queue', 'text-danger');
                        Raven.captureMessage('Could not upload: ' + fileName);
                    }
                });
                this.on('drop',function(){ // clear errors on drop or click
                    self.clearMessages();
                });
                // upload and process queue logic
                this.on('success',function(){
                    self.filename(self.uploadCount() + ' / ' + dropzone.files.length + ' files');
                    dropzone.processQueue(); // this is a bit hackish -- it fails to process full queue but this ensures it runs the process again after each success.
                    var oldCount = self.uploadCount();
                    self.uploadCount(oldCount + 1);

                    if(self.uploadCount() > dropzone.files.length){ // when finished redirect to project/component page where uploaded.
                        self.changeMessage('Success!', 'text-success');
                        window.location = self.target().urls.files;
                    }
                });

                // add file logic and dropzone to file display swap
                this.on('addedfile', function() {
                    if(dropzone.files.length>1){
                        self.iconSrc('/static/img/upload_icons/multiple_blank.png');
                        self.filename(dropzone.files.length + ' files');
                    }else{
                        var fileName = truncateFilename(dropzone.files[0].name);
                        self.iconSrc(getFiletypeIcon(fileName));
                        self.filename(fileName);
                    }
                    self.enableUpload(false);
                });
            }
        };
        self.dropzone = new Dropzone(self.selector, dropzoneOpts);
    }

    ko.components.register('osf-ob-uploader', {
        viewModel: OBUploaderViewModel,
        template: {element: 'osf-ob-uploader'}
    });
}));

{'fraction_non_alphanumeric': 0.11529472124045148, 'fraction_numerical': 0.0018811994071371565, 'mean_word_length': 1.5748568912373404, 'pattern_counts': {'":': 0, '<': 19, '<?xml version=': 0, '>': 29, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 2}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 22, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3198027', 'n_tokens_mistral': 9305, 'n_tokens_neox': 8902, 'n_words': 2232}