Source: applait.finder.js

/**
 * @file
 * File picker and finder for device storages on Firefox OS devices
 *
 * This library provides an easy-to-use asynchronous interface for other Firefox OS apps to search for files
 * on Firefox OS devices. The library is based on an event-based architecture, letting developers build
 * beautiful asynchronous API for their apps.
 *
 * The `Finder` library is best used by developers looking to pick a file from the `sdcard` for their apps.
 *
 * This library depends on [EventEmitter](https://github.com/Wolfy87/EventEmitter) by Wolfy87, included with the
 * package.
 *
 * @version 1.1.3
 * @license The MIT License (MIT)
 * @author Applait Technologies LLP
 * @copyright Copyright (c) 2014 Applait Technologies LLP
 */

/**
 * Core Applait namespace for Applait libraries.
 *
 * @namespace
 */
var Applait = Applait || {};

/**
 * Core `Finder` class. Provides the constructor for applications to instantiate.
 *
 * @memberOf Applait
 * @constructor
 *
 * @property {object} options - Easy access to `options` passed in constructor argument.
 * @property {string} type - The type of DeviceStorage specified.
 * @property {boolean} hidden - Boolean specifying whether hidden files will be included in search or not.
 * @property {boolean} casesensitive - Boolean specifying whether searches will be case sensitive or not.
 * @property {boolean} debugmode - Boolean activating or deactivating debug mode.
 * @property {array} storages - Array of device storage cursors based on `this.type`.
 * @property {number} searchcompletecount - Number of device storages searched through in latest search.
 * @property {number} filematchcount - Number of files found in latest search
 * @property {string} searchkey - Search term used in last search
 *
 * @param {object=} options - Default options for the `Finder` constructor. It can include any of the following
 * properties:
 *
 * - `type` : `{string}` : Can be one of `sdcard`, `music`, `pictures`, `videos`. Defaults to `sdcard`.
 * - `minSearchLength`: `{number}` : The minimum length of search string without which search will not be triggered.
 * Defaults to `3`.
 * - `hidden`: `{boolean}` : If set to `true`, searches hidden files as well. Defaults to `false`.
 * - `caseSensitive` : `{boolean}` : If set to `true`, searches will be case sensitive. Defaults to `false`.
 * - `debugMode`: `{boolean}` : If `true`, enables debug mode which logs all messages to the browser console. This
 * should be disabled in production mode to reduce memory footprint.
 *
 * @example
 * var finder = new Applait.Finder({ type: "sdcard", debugMode: true });
 */
Applait.Finder = function (options) {

    this.options = options || {};

    this.type = this.options.type || "sdcard";

    this.hidden = this.options.hidden || false;

    this.casesensitive = this.options.caseSensitive || false;

    this.minsearchlength = (this.options.minSearchLength && typeof this.options.minSearchLength === "number") ?
        options.minSearchLength : 3;

    this.debugmode = this.options.debugMode ? true : false;

    this.storages = navigator.getDeviceStorages && navigator.getDeviceStorages(this.type);

    this.searchcompletecount = 0;

    this.filematchcount = 0;

    this.searchkey = "";
};


/**
 * Make `Applait.Finder` inherit the `EventEmitter` prototype chain.
 */
Applait.Finder.prototype = new EventEmitter();


/**
 * Match hidden files based on settings
 *
 * @param {string} filename - The filename to test
 * @return {boolean} - `true` if file is a hidden file and if `hidden`
 * is `true` in constructor options.
 */
Applait.Finder.prototype.checkhidden = function (fullpath) {
    if (fullpath[fullpath.indexOf(".") - 1] === "/" && this.hidden !== true) {
        return false;
    }
    return true;
};


/**
 * Instantiate search
 *
 * @memberOf Applait.Finder
 * @param {string} needle - The string to match file names from the device storage.
 * @return {null} - Only if `needle` length is less than `minsearchlength` or if no DeviceStorages are found.
 */
Applait.Finder.prototype.search = function (needle) {

    var self = this;

    self.reset();
    self.searchkey = !self.casesensitive ? needle.trim().toLowerCase() : needle.trim();

    if (self.searchkey.length < self.minsearchlength) {
        self.log("searchCancelled",
                 ["Search string should be at least " + self.minsearchlength + " characters"]);
        self.emitEvent("searchCancelled",
                       ["Search string should be at least " + self.minsearchlength + " characters"]);
        return null;
    }

    if (self.storagecount() < 1) {
        self.log("empty", [self.searchkey]);
        self.emitEvent("empty", [self.searchkey]);
        return null;
    }

    self.log("searchBegin", [self.searchkey]);
    self.emitEvent("searchBegin", [self.searchkey]);

    self.storages.forEach(function (storage) {

        var cursor = storage.enumerate();

        self.log("storageSearchBegin", [storage.storageName, self.searchkey]);
        self.emitEvent("storageSearchBegin", [storage.storageName, self.searchkey]);

        cursor.onsuccess = function () {

            if (this.result) {

                var file = this.result;
                var fileinfo = self.splitname(file.name);

                if (self.matchname(fileinfo.name, file.name)) {
                    self.filematchcount++;
                    self.log("fileFound", [file, fileinfo, storage.storageName]);
                    self.emitEvent("fileFound", [file, fileinfo, storage.storageName]);
                }
                if (!this.done) {
                    this.continue();
                } else {
                    self.searchcompletecount++;
                    self.log("storageSearchComplete", [storage.storageName, self.searchkey]);
                    self.emitEvent("storageSearchComplete", [storage.storageName, self.searchkey]);
                }
            } else {
                self.searchcompletecount++;
                self.log("storageSearchComplete", [storage.storageName, self.searchkey]);
                self.emitEvent("storageSearchComplete", [storage.storageName, self.searchkey]);
            }

            if (self.searchcompletecount === self.storagecount()) {
                self.log("searchComplete", [self.searchkey, self.filematchcount]);
                self.emitEvent("searchComplete", [self.searchkey, self.filematchcount]);
            }

        };

        cursor.onerror = function () {
            self.log("error", ["Error accessing device storage '" + storage.storageName + "'", this.error]);
            self.emitEvent("error", ["Error accessing device storage '" + storage.storageName + "'",
                                     this.error]);
        };

    });
};


/**
 * Splits full file path into basename and path to directory.
 *
 * @memberOf Applait.Finder
 * @param {string} filename - Filename obtained for `File.filename`
 * @return {object} - An object with two keys:
 *
 * - `name` - the basename of the file with extension
 * - `path` - path to the file's directory.
 */
Applait.Finder.prototype.splitname = function (filename) {
    filename = filename.split(/[\\/]/);

    return { "name": filename.pop(), "path": filename.join("/") };
};


/**
 * Return the number of storages being used
 *
 * @memberOf Applait.Finder
 * @return {number} - The length of storages
 */
Applait.Finder.prototype.storagecount = function () {
    return this.storages && this.storages.length ? this.storages.length : 0;
};


/**
 * Reset internals
 *
 * @memberOf Applait.Finder
 */
Applait.Finder.prototype.reset = function () {
    this.filematchcount = 0;
    this.searchcompletecount = 0;
    this.searchkey = "";
};


/**
 * Generic logging method, dependent on debugMode
 *
 * @memberOf Applait.Finder
 * @param {string} message - A string identifying this log
 * @param {array} args - An array of stuff to print
 */
Applait.Finder.prototype.log = function (message, args) {
    if (this.debugmode) {
        console.log(message, args);
    }
};


/**
 * Match name
 *
 * @memberOf Applait.Finder
 * @param {string} name - Filename
 * @return {boolean}
 */
Applait.Finder.prototype.matchname = function (name, fullpath) {
    name = !this.casesensitive ? name.trim().toLowerCase() : name.trim();
    return (name.indexOf(this.searchkey) > -1 && this.checkhidden(fullpath));
};