<commit_before>/**
 This is a slightly modified JS port of hot code push android client from here:
 https://github.com/meteor/cordova-plugin-meteor-webapp

 The MIT License (MIT)

 Copyright (c) 2015 Meteor Development Group

 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
 in the Software without restriction, including without limitation the rights
 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 copies of the Software, and to permit persons to whom the Software is
 furnished to do so, subject to the following conditions:

 The above copyright notice and this permission notice shall be included in all
 copies or substantial portions of the Software.

 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 SOFTWARE.

 This file is based on:
 /cordova-plugin-meteor-webapp/blob/master/src/android/AssetBundleDownloader.java

 */

import fs from 'fs';
import originalFs from 'original-fs';
import url from 'url';
// TODO: maybe use node-fetch?
import request from 'request';
import queue from 'queue';
import IsDesktopInjector from './isDesktopInjector';

export default class AssetBundleDownloader {
    /**
     * Assets downloader - responsible for downloading an asset version.
     *
     * @param {object}      log           - Winston reference.
     * @param {object}      configuration - Configuration object.
     * @param {AssetBundle} assetBundle   - Parent asset bundle.
     * @param {string}      baseUrl       - Url of the meteor server.
     * @param {[Asset]}     missingAssets - Array of assets to download.
     * @constructor
     */
    constructor(log, configuration, assetBundle, baseUrl, missingAssets) {
        this.log = log.getLoggerFor('AssetBundleDownloader');
        this.log.debug(`downloader created for ${assetBundle.directoryUri}`);

        this.configuration = configuration;
        this.assetBundle = assetBundle;
        this.baseUrl = baseUrl;
        this.injector = new IsDesktopInjector();
        this.httpClient = request;

        this.eTagWithSha1HashPattern = new RegExp('"([0-9a-f]{40})"');

        this.missingAssets = missingAssets;
        this.assetsDownloading = [];
        this.onFinished = null;
        this.onFailure = null;
        this.cancelInvoked = false;

        this.queue = queue();
    }

    /**
     * Asset bundle getter.
     */
    getAssetBundle() {
        return this.assetBundle;
    }

    /**
     * Stores callbacks.
     *
     * @param {function} onFinished - Callback for success.
     * @param {function} onFailure  - Callback for failure.
     */
    setCallbacks(onFinished, onFailure) {
        this.onFinished = onFinished;
        this.onFailure = onFailure;
    }

    /**
     * Starts the download.
     */
    resume() {
        const self = this;

        this.log.verbose(
            `started downloading assets from bundle with version: ${this.assetBundle.getVersion()}`
        );

        /**
         * @param {Asset} asset  - Asset whose downloading failed.
         * @param {string} cause - The cause.
         */
        function onFailure(asset, cause) {
            self.assetsDownloading.splice(self.assetsDownloading.indexOf(asset), 1);

            if (!self.cancelInvoked) {
                self.didFail(`error downloading asset: ${asset.filePath}: ${cause}`);
            }
        }

        /**
         * @param {Asset} asset - Asset that was downloaded.
         * @param {Object} response - Response object from `request`.
         * @param {Buffer} body - Body of downloaded the file.
         */
        function onResponse(asset, response, body) {
            const fileContents = body;
            self.assetsDownloading.splice(self.assetsDownloading.indexOf(asset), 1);

            try {
                self.verifyResponse(response, asset, fileContents);
            } catch (e) {
                self.didFail(`failed at verifyResponse: ${e.message}`);
                return;
            }

            try {
                // Unfortunately on every hot code push we need to ensure that we will not loose
                // `Meteor.isDesktop`. Here we will inject it into the code that arrived from HCP.
                if (asset.fileType === 'js') {
                    const fileContentsString = fileContents.toString('UTF-8');
                    const result = self.injector.processFileContents(fileContentsString);
                    if (result.injected || result.injectedStartupDidComplete) {
                        fs.writeFileSync(asset.getFile(), result.fileContents, 'UTF-8');
                    } else {
                        fs.writeFileSync(asset.getFile(), fileContents);
                    }
                } else {
                    originalFs.writeFileSync(asset.getFile(), fileContents);
                }
            } catch (e) {
                self.didFail(`failed at injecting isDesktop and writing to disk: ${e.message}`);
                return;
            }

            // We don't have a hash for the index page, so we have to parse the runtime config
            // and compare autoupdateVersionCordova to the version in the manifest to verify
            // if we downloaded the expected version.
            if (asset.filePath === 'index.html') {
                const runtimeConfig = self.assetBundle.getRuntimeConfig();
                if (runtimeConfig !== null) {
                    try {
                        self.verifyRuntimeConfig(runtimeConfig);
                    } catch (e) {
                        self.didFail(`fail at verifyRuntimeConfig: ${e}`);
                        return;
                    }
                }
            }

            self.log.verbose(`saving ${asset.urlPath}`);

            self.missingAssets.splice(self.missingAssets.indexOf(asset), 1);

            if (self.missingAssets.length === 0) {
                self.log.verbose(
                    'finished downloading new asset bundle version:' +
                    `${self.assetBundle.getVersion()}`
                );

                if (self.onFinished) {
                    self.onFinished();
                }
            }
        }

        this.missingAssets.forEach((asset) => {
            if (!~self.assetsDownloading.indexOf(asset)) {
                self.assetsDownloading.push(asset);
                const downloadUrl = self.downloadUrlForAsset(asset);
                self.queue.push((callback) => {
                    self.httpClient(
                        { uri: downloadUrl, encoding: null },
                        (error, response, body) => {
                            if (!error) {
                                onResponse(asset, response, body);
                            } else {
                                onFailure(asset, error);
                            }
                            callback();
                        });
                });
            }
        });
        self.queue.start();
    }

    /**
     * Cancels downloading.
     */
    cancel() {
        this.cancelInvoked = true;
        this.queue.end();
    }

    /**
     * Computes a download url for asset.
     *
     * @param {Asset} asset - Asset for which the url is created.
     * @returns {string}
     * @private
     */
    downloadUrlForAsset(asset) {
        let urlPath = asset.urlPath;

        // Remove leading / from URL path because the path should be
        // interpreted relative to the base URL.
        if (urlPath[0] === '/') {
            urlPath = urlPath.substring(1);
        }

        const builder = url.parse(url.resolve(this.baseUrl, urlPath));

        // To avoid inadvertently downloading the default index page when an asset
        // is not found, we add meteor_dont_serve_index=true to the URL unless we
        // are actually downloading the index page.
        if (asset.filePath !== 'index.html') {
            builder.query = { meteor_dont_serve_index: 'true' };
        }

        return url.format(builder);
    }

    /**
     * Verifies response from the server.
     *
     * @param {Object} response - Http response object.
     * @param {Asset}  asset    - Asset which was downloaded.
     * @param {Buffer} body     - Body of the file as a Buffer.
     * @private
     */
    verifyResponse(response, asset, body) {
        if (response.statusCode !== 200) {
            throw new Error(
                `non-success status code ${response.statusCode} for asset: ${asset.filePath}`
            );
        }

        // If we have a hash for the asset, and the ETag header also specifies
        // a hash, we compare these to verify if we received the expected asset version.
        const expectedHash = asset.hash;

        if (expectedHash !== null) {
            const eTag = response.headers.etag;

            if (eTag !== null) {
                const matches = eTag.match(this.eTagWithSha1HashPattern);

                if (this.eTagWithSha1HashPattern.test(eTag)) {
                    const actualHash = matches[1];

                    if (actualHash !== expectedHash) {
                        throw new Error(
                            `hash mismatch for asset: ${asset.filePath} - expected hash:` +
                            `${expectedHash} != ${actualHash}`
                        );
                    } else if (asset.entrySize !== body.length) {
                        // This check is specific to this integration. It is not present in
                        // Cordova integration.
                        // For now will not throw here as it is accepted on Cordova.
                        this.log.debug(`wrong size for: ${asset.filePath} - expected: ` +
                            `${asset.entrySize} != ${body.length}`);
                    }
                } else {
                    this.log.warn(`invalid etag format for ${asset.urlPath}: ${eTag}`);
                }
            } else {
                this.log.warn(`no eTag served for ${asset.urlPath}`);
            }
        }
    }

    /**
     * Fail handler.
     *
     * @param {string} cause - Error message;
     * @private
     */
    didFail(cause) {
        if (this.cancelInvoked) return;

        this.cancel();

        this.log.debug(`failure: ${cause}`);
        if (this.onFailure !== null) {
            this.onFailure(cause);
        }
    }

    /**
     * Verifies runtime config.
     *
     * @param {Object} runtimeConfig - Runtime config.
     * @private
     */
    verifyRuntimeConfig(runtimeConfig) {
        const expectedVersion = this.assetBundle.getVersion();
        const actualVersion = runtimeConfig.autoupdateVersionCordova;

        if (actualVersion) {
            if (actualVersion !== expectedVersion) {
                throw new Error(
                    `version mismatch for index page, expected: ${expectedVersion}` +
                    `, actual: ${actualVersion}`);
            }
        }

        if (!('ROOT_URL' in runtimeConfig)) {
            throw new Error('could not find ROOT_URL in downloaded asset bundle');
        }

        const rootUrlString = runtimeConfig.ROOT_URL;

        const rootUrl = url.parse(rootUrlString);
        const previousRootUrl = url.parse(this.configuration.rootUrlString);

        if (previousRootUrl.hostname !== 'localhost' && rootUrl.hostname === 'localhost') {
            throw new Error(
                'ROOT_URL in downloaded asset bundle would change current ROOT_URL ' +
                'to localhost. Make sure ROOT_URL has been configured correctly on the server.'
            );
        }

        if (!('appId' in runtimeConfig)) {
            throw new Error('could not find appId in downloaded asset bundle.');
        }

        const appId = runtimeConfig.appId;

        if (appId !== this.configuration.appId) {
            throw new Error(
                'appId in downloaded asset bundle does not match current appId. Make sure the' +
                ` server at ${rootUrlString} is serving the right app.`
            );
        }
    }
}

module.exports = AssetBundleDownloader;
<commit_msg>Fix wrong assumption on `eTag` being null when missing<commit_after>/**
 This is a slightly modified JS port of hot code push android client from here:
 https://github.com/meteor/cordova-plugin-meteor-webapp

 The MIT License (MIT)

 Copyright (c) 2015 Meteor Development Group

 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
 in the Software without restriction, including without limitation the rights
 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 copies of the Software, and to permit persons to whom the Software is
 furnished to do so, subject to the following conditions:

 The above copyright notice and this permission notice shall be included in all
 copies or substantial portions of the Software.

 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 SOFTWARE.

 This file is based on:
 /cordova-plugin-meteor-webapp/blob/master/src/android/AssetBundleDownloader.java

 */

import fs from 'fs';
import originalFs from 'original-fs';
import url from 'url';
// TODO: maybe use node-fetch?
import request from 'request';
import queue from 'queue';
import IsDesktopInjector from './isDesktopInjector';

export default class AssetBundleDownloader {
    /**
     * Assets downloader - responsible for downloading an asset version.
     *
     * @param {object}      log           - Winston reference.
     * @param {object}      configuration - Configuration object.
     * @param {AssetBundle} assetBundle   - Parent asset bundle.
     * @param {string}      baseUrl       - Url of the meteor server.
     * @param {[Asset]}     missingAssets - Array of assets to download.
     * @constructor
     */
    constructor(log, configuration, assetBundle, baseUrl, missingAssets) {
        this.log = log.getLoggerFor('AssetBundleDownloader');
        this.log.debug(`downloader created for ${assetBundle.directoryUri}`);

        this.configuration = configuration;
        this.assetBundle = assetBundle;
        this.baseUrl = baseUrl;
        this.injector = new IsDesktopInjector();
        this.httpClient = request;

        this.eTagWithSha1HashPattern = new RegExp('"([0-9a-f]{40})"');

        this.missingAssets = missingAssets;
        this.assetsDownloading = [];
        this.onFinished = null;
        this.onFailure = null;
        this.cancelInvoked = false;

        this.queue = queue();
    }

    /**
     * Asset bundle getter.
     */
    getAssetBundle() {
        return this.assetBundle;
    }

    /**
     * Stores callbacks.
     *
     * @param {function} onFinished - Callback for success.
     * @param {function} onFailure  - Callback for failure.
     */
    setCallbacks(onFinished, onFailure) {
        this.onFinished = onFinished;
        this.onFailure = onFailure;
    }

    /**
     * Starts the download.
     */
    resume() {
        const self = this;

        this.log.verbose(
            `started downloading assets from bundle with version: ${this.assetBundle.getVersion()}`
        );

        /**
         * @param {Asset} asset  - Asset whose downloading failed.
         * @param {string} cause - The cause.
         */
        function onFailure(asset, cause) {
            self.assetsDownloading.splice(self.assetsDownloading.indexOf(asset), 1);

            if (!self.cancelInvoked) {
                self.didFail(`error downloading asset: ${asset.filePath}: ${cause}`);
            }
        }

        /**
         * @param {Asset} asset - Asset that was downloaded.
         * @param {Object} response - Response object from `request`.
         * @param {Buffer} body - Body of downloaded the file.
         */
        function onResponse(asset, response, body) {
            const fileContents = body;
            self.assetsDownloading.splice(self.assetsDownloading.indexOf(asset), 1);

            try {
                self.verifyResponse(response, asset, fileContents);
            } catch (e) {
                self.didFail(`failed at verifyResponse: ${e.message}`);
                return;
            }

            try {
                // Unfortunately on every hot code push we need to ensure that we will not loose
                // `Meteor.isDesktop`. Here we will inject it into the code that arrived from HCP.
                if (asset.fileType === 'js') {
                    const fileContentsString = fileContents.toString('UTF-8');
                    const result = self.injector.processFileContents(fileContentsString);
                    if (result.injected || result.injectedStartupDidComplete) {
                        fs.writeFileSync(asset.getFile(), result.fileContents, 'UTF-8');
                    } else {
                        fs.writeFileSync(asset.getFile(), fileContents);
                    }
                } else {
                    originalFs.writeFileSync(asset.getFile(), fileContents);
                }
            } catch (e) {
                self.didFail(`failed at injecting isDesktop and writing to disk: ${e.message}`);
                return;
            }

            // We don't have a hash for the index page, so we have to parse the runtime config
            // and compare autoupdateVersionCordova to the version in the manifest to verify
            // if we downloaded the expected version.
            if (asset.filePath === 'index.html') {
                const runtimeConfig = self.assetBundle.getRuntimeConfig();
                if (runtimeConfig !== null) {
                    try {
                        self.verifyRuntimeConfig(runtimeConfig);
                    } catch (e) {
                        self.didFail(`fail at verifyRuntimeConfig: ${e}`);
                        return;
                    }
                }
            }

            self.log.verbose(`saving ${asset.urlPath}`);

            self.missingAssets.splice(self.missingAssets.indexOf(asset), 1);

            if (self.missingAssets.length === 0) {
                self.log.verbose(
                    'finished downloading new asset bundle version:' +
                    `${self.assetBundle.getVersion()}`
                );

                if (self.onFinished) {
                    self.onFinished();
                }
            }
        }

        this.missingAssets.forEach((asset) => {
            if (!~self.assetsDownloading.indexOf(asset)) {
                self.assetsDownloading.push(asset);
                const downloadUrl = self.downloadUrlForAsset(asset);
                self.queue.push((callback) => {
                    self.httpClient(
                        { uri: downloadUrl, encoding: null },
                        (error, response, body) => {
                            if (!error) {
                                onResponse(asset, response, body);
                            } else {
                                onFailure(asset, error);
                            }
                            callback();
                        });
                });
            }
        });
        self.queue.start();
    }

    /**
     * Cancels downloading.
     */
    cancel() {
        this.cancelInvoked = true;
        this.queue.end();
    }

    /**
     * Computes a download url for asset.
     *
     * @param {Asset} asset - Asset for which the url is created.
     * @returns {string}
     * @private
     */
    downloadUrlForAsset(asset) {
        let urlPath = asset.urlPath;

        // Remove leading / from URL path because the path should be
        // interpreted relative to the base URL.
        if (urlPath[0] === '/') {
            urlPath = urlPath.substring(1);
        }

        const builder = url.parse(url.resolve(this.baseUrl, urlPath));

        // To avoid inadvertently downloading the default index page when an asset
        // is not found, we add meteor_dont_serve_index=true to the URL unless we
        // are actually downloading the index page.
        if (asset.filePath !== 'index.html') {
            builder.query = { meteor_dont_serve_index: 'true' };
        }

        return url.format(builder);
    }

    /**
     * Verifies response from the server.
     *
     * @param {Object} response - Http response object.
     * @param {Asset}  asset    - Asset which was downloaded.
     * @param {Buffer} body     - Body of the file as a Buffer.
     * @private
     */
    verifyResponse(response, asset, body) {
        if (response.statusCode !== 200) {
            throw new Error(
                `non-success status code ${response.statusCode} for asset: ${asset.filePath}`
            );
        }

        // If we have a hash for the asset, and the ETag header also specifies
        // a hash, we compare these to verify if we received the expected asset version.
        const expectedHash = asset.hash;

        if (expectedHash !== null) {
            const eTag = response.headers.etag;

            if (typeof eTag === 'string') {
                const matches = eTag.match(this.eTagWithSha1HashPattern);

                if (this.eTagWithSha1HashPattern.test(eTag)) {
                    const actualHash = matches[1];

                    if (actualHash !== expectedHash) {
                        throw new Error(
                            `hash mismatch for asset: ${asset.filePath} - expected hash:` +
                            `${expectedHash} != ${actualHash}`
                        );
                    } else if (asset.entrySize !== body.length) {
                        // This check is specific to this integration. It is not present in
                        // Cordova integration.
                        // For now will not throw here as it is accepted on Cordova.
                        this.log.debug(`wrong size for: ${asset.filePath} - expected: ` +
                            `${asset.entrySize} != ${body.length}`);
                    }
                } else {
                    this.log.warn(`invalid etag format for ${asset.urlPath}: ${eTag}`);
                }
            } else {
                this.log.warn(`no eTag served for ${asset.urlPath}`);
            }
        }
    }

    /**
     * Fail handler.
     *
     * @param {string} cause - Error message;
     * @private
     */
    didFail(cause) {
        if (this.cancelInvoked) return;

        this.cancel();

        this.log.debug(`failure: ${cause}`);
        if (this.onFailure !== null) {
            this.onFailure(cause);
        }
    }

    /**
     * Verifies runtime config.
     *
     * @param {Object} runtimeConfig - Runtime config.
     * @private
     */
    verifyRuntimeConfig(runtimeConfig) {
        const expectedVersion = this.assetBundle.getVersion();
        const actualVersion = runtimeConfig.autoupdateVersionCordova;

        if (actualVersion) {
            if (actualVersion !== expectedVersion) {
                throw new Error(
                    `version mismatch for index page, expected: ${expectedVersion}` +
                    `, actual: ${actualVersion}`);
            }
        }

        if (!('ROOT_URL' in runtimeConfig)) {
            throw new Error('could not find ROOT_URL in downloaded asset bundle');
        }

        const rootUrlString = runtimeConfig.ROOT_URL;

        const rootUrl = url.parse(rootUrlString);
        const previousRootUrl = url.parse(this.configuration.rootUrlString);

        if (previousRootUrl.hostname !== 'localhost' && rootUrl.hostname === 'localhost') {
            throw new Error(
                'ROOT_URL in downloaded asset bundle would change current ROOT_URL ' +
                'to localhost. Make sure ROOT_URL has been configured correctly on the server.'
            );
        }

        if (!('appId' in runtimeConfig)) {
            throw new Error('could not find appId in downloaded asset bundle.');
        }

        const appId = runtimeConfig.appId;

        if (appId !== this.configuration.appId) {
            throw new Error(
                'appId in downloaded asset bundle does not match current appId. Make sure the' +
                ` server at ${rootUrlString} is serving the right app.`
            );
        }
    }
}

module.exports = AssetBundleDownloader;
<|endoftext|>


<commit_before>window.addEventListener('load', () => {
  if (!window.$) return;
  if (!$('#lyrics').length) return;
  let animate = false;
  let animationTimer;
  let noLyricsTimer;
  let jumpDetect;
  let isPlaying = false;

  // Handle new lyrics strings
  const lyricsHandler = (lyrics) => {
    if (!lyrics) {
      $('#lyrics').html('
lyrics-loading-message
');
      $('#lyrics p').stop();
      animate = false;
      clearTimeout(noLyricsTimer);
      noLyricsTimer = setTimeout(() => {
        $('#lyrics').html('
lyrics-failed-message
');
      }, 4000);
    } else {
      clearTimeout(noLyricsTimer);
      const scroll = Settings.get('scrollLyrics', true);
      const lyricsHTML = lyrics.replace(/\n/g, '
');
      $('#lyrics').html(`<p ${scroll ? 'data-scroll' : ''}>${lyricsHTML}</p>`);
      animate = scroll;
    }
  };
  // Handle playing and pausing
  const stateHandler = (remoteIsPlaying) => {
    isPlaying = remoteIsPlaying;
    if (!isPlaying) return $('#lyrics p').stop();
    animate = true;
  };
  // Handle time progression of a song
  const timeHandler = (timeObj) => {
    $('#lyrics_bar').width(`${(timeObj.total === 0 ? 0 : timeObj.current / timeObj.total) * 100}%`);
    let jumped = false;
    if (Math.abs(timeObj.current - jumpDetect) > 1000 && $('#lyrics p').attr('data-scroll')) {
      animate = true;
      jumped = true;
    }

    jumpDetect = timeObj.current;
    if (!isPlaying || !animate || !timeObj.total || !$('#lyrics p').get(0)) return;
    const lyricsP = $('#lyrics p');
    const maxHeight = parseInt(lyricsP.get(0).scrollHeight, 10);
    const viewPortHeight = parseInt(lyricsP.innerHeight(), 10);
    const waitTime = (viewPortHeight / maxHeight) * timeObj.total * 0.3;
    const actualWaitTime = Math.max(0, waitTime - timeObj.current);
    clearTimeout(animationTimer);
    if (jumped) {
      lyricsP.stop();
      lyricsP.scrollTop(maxHeight * (Math.max(0, timeObj.current - actualWaitTime) / timeObj.total));
    }
    animationTimer = setTimeout(() => {
      lyricsP.stop().animate({
        scrollTop: maxHeight - viewPortHeight,
      }, timeObj.total - timeObj.current - actualWaitTime - waitTime, 'linear');
    }, actualWaitTime);
    animate = false;
  };

  const scrollSettingsHandler = (state) => {
    const lyricsP = $('#lyrics p');
    animate = state;
    if (state) {
      lyricsP.attr('data-scroll', true);
    } else {
      lyricsP.removeAttr('data-scroll');
      clearTimeout(animationTimer);
      lyricsP.stop();
    }
  };

  Emitter.on('PlaybackAPI:change:lyrics', (e, arg) => lyricsHandler(arg));
  Emitter.on('PlaybackAPI:change:state', (e, arg) => stateHandler(arg));
  Emitter.on('PlaybackAPI:change:time', (e, arg) => timeHandler(arg));
  Emitter.on('settings:set:scrollLyrics', (e, arg) => scrollSettingsHandler(arg));

  window.addEventListener('resize', () => { animate = true; });
  $('#lyrics_back').click(() => $('#lyrics_back').removeClass('vis'));
});

Emitter.on('lyrics:show', () => $('#lyrics_back').addClass('vis'));
<commit_msg>Further scrolling fix<commit_after>window.addEventListener('load', () => {
  if (!window.$) return;
  if (!$('#lyrics').length) return;
  let animate = false;
  let animationTimer;
  let noLyricsTimer;
  let jumpDetect;
  let isPlaying = false;

  // Handle new lyrics strings
  const lyricsHandler = (lyrics) => {
    if (!lyrics) {
      $('#lyrics').html('
lyrics-loading-message
');
      $('#lyrics p').stop();
      animate = false;
      clearTimeout(noLyricsTimer);
      noLyricsTimer = setTimeout(() => {
        $('#lyrics').html('
lyrics-failed-message
');
      }, 4000);
    } else {
      clearTimeout(noLyricsTimer);
      const scroll = Settings.get('scrollLyrics', true);
      const lyricsHTML = lyrics.replace(/\n/g, '
');
      $('#lyrics').html(`<p ${scroll ? 'data-scroll' : ''}>${lyricsHTML}</p>`);
      animate = scroll;
    }
  };
  // Handle playing and pausing
  const stateHandler = (remoteIsPlaying) => {
    isPlaying = remoteIsPlaying;
    if (!isPlaying) return $('#lyrics p').stop();
    animate = Settings.get('scrollLyrics', true);
  };
  // Handle time progression of a song
  const timeHandler = (timeObj) => {
    $('#lyrics_bar').width(`${(timeObj.total === 0 ? 0 : timeObj.current / timeObj.total) * 100}%`);
    let jumped = false;
    if (Math.abs(timeObj.current - jumpDetect) > 1000 && $('#lyrics p').attr('data-scroll')) {
      animate = true;
      jumped = true;
    }

    jumpDetect = timeObj.current;
    if (!isPlaying || !animate || !timeObj.total || !$('#lyrics p').get(0)) return;
    const lyricsP = $('#lyrics p');
    const maxHeight = parseInt(lyricsP.get(0).scrollHeight, 10);
    const viewPortHeight = parseInt(lyricsP.innerHeight(), 10);
    const waitTime = (viewPortHeight / maxHeight) * timeObj.total * 0.3;
    const actualWaitTime = Math.max(0, waitTime - timeObj.current);
    clearTimeout(animationTimer);
    if (jumped) {
      lyricsP.stop();
      lyricsP.scrollTop(maxHeight * (Math.max(0, timeObj.current - actualWaitTime) / timeObj.total));
    }
    animationTimer = setTimeout(() => {
      lyricsP.stop().animate({
        scrollTop: maxHeight - viewPortHeight,
      }, timeObj.total - timeObj.current - actualWaitTime - waitTime, 'linear');
    }, actualWaitTime);
    animate = false;
  };

  const scrollSettingsHandler = (state) => {
    const lyricsP = $('#lyrics p');
    animate = state;
    if (state) {
      lyricsP.attr('data-scroll', true);
    } else {
      lyricsP.removeAttr('data-scroll');
      clearTimeout(animationTimer);
      lyricsP.stop();
    }
  };

  Emitter.on('PlaybackAPI:change:lyrics', (e, arg) => lyricsHandler(arg));
  Emitter.on('PlaybackAPI:change:state', (e, arg) => stateHandler(arg));
  Emitter.on('PlaybackAPI:change:time', (e, arg) => timeHandler(arg));
  Emitter.on('settings:set:scrollLyrics', (e, arg) => scrollSettingsHandler(arg));

  window.addEventListener('resize', () => { animate = true; });
  $('#lyrics_back').click(() => $('#lyrics_back').removeClass('vis'));
});

Emitter.on('lyrics:show', () => $('#lyrics_back').addClass('vis'));
<|endoftext|>


<commit_before>const Cookies        = require('js-cookie');
const BinarySocket   = require('./socket');
const Client         = require('../base/client');
const isEuCountry    = require('../common/country_base').isEuCountry;
const getElementById = require('../../_common/common_functions').getElementById;
const LocalStore     = require('../../_common/storage').LocalStore;
const State          = require('../../_common/storage').State;

const Footer = (() => {
    const onLoad = () => {
        BinarySocket.wait('website_status', 'authorize', 'landing_company').then(() => {
            // show CFD warning to logged in maltainvest clients or
            // logged in virtual clients with maltainvest financial landing company or
            // logged out clients with EU IP address
            if (Client.isLoggedIn()) {
                showWarning((Client.get('landing_company_shortcode') === 'maltainvest' ||
                    (Client.get('is_virtual') && State.getResponse('landing_company.financial_company.shortcode') === 'maltainvest')));
            } else {
                showWarning(isEuCountry());
            }
        });
    };

    const showWarning = (should_show_warning) => {
        $('#footer-regulatory .eu-only').setVisibility(should_show_warning);
    };

    const clearNotification = () => {
        const $status_notification = $('#status_notification');
        $status_notification.slideUp(200);
    };

    // by default elevio is 8px above bottom of page, and scrollup is 18px above elevio
    const adjustNotifElevioAndScrollup = (elevio_height = 8, scrollup_height = 18, status_height = 8) => {
        const $elevio_button = $('#_elev_io ._6byvm');
        const $scrollup = $('#scrollup');
        const $status_notification = $('#status_notification');

        $elevio_button.attr('style', `bottom: ${elevio_height}px !important`);
        $status_notification.attr('style', `bottom: ${status_height}px !important`);
        $scrollup.attr('style', `bottom: ${$elevio_button.height() + scrollup_height}px`);
    };

    const clearDialogMessage = () => {
        const $dialog_notification = $('#dialog_notification');
        const el_footer = getElementById('footer');

        el_footer.style.paddingBottom = '0px';
        $dialog_notification.slideUp(200);
        adjustNotifElevioAndScrollup();
    };

    const displayDialogMessage = () => {
        BinarySocket.wait('website_status', 'authorize', 'landing_company').then(() => {
            if (isEuCountry()) {
                const $dialog_notification = $('#dialog_notification');
                const el_dialog_notification_accept = getElementById('dialog_notification_accept');
                const el_footer = getElementById('footer');
                const gap_dialog_to_elevio = 30;
                const gap_elevio_to_scrollup = 10;

                $dialog_notification.css('display', 'flex');
                el_footer.style.paddingBottom = `${$dialog_notification.height()}px`;
                adjustNotifElevioAndScrollup($dialog_notification.height() + gap_dialog_to_elevio,
                    $dialog_notification.height() + gap_dialog_to_elevio + gap_elevio_to_scrollup,
                    $dialog_notification.height() + gap_dialog_to_elevio);

                el_dialog_notification_accept
                    .addEventListener('click', () => {
                        adjustNotifElevioAndScrollup();
                        $dialog_notification.slideUp(200);
                        el_footer.style.paddingBottom = '0px';
                        Cookies.set('CookieConsent', 1);
                    });
                window.addEventListener('resize', () => {
                    adjustNotifElevioAndScrollup($dialog_notification.height() + gap_dialog_to_elevio,
                        $dialog_notification.height() + gap_dialog_to_elevio + gap_elevio_to_scrollup);
                });
            }
        });
    };

    const displayNotification = (message) => {
        BinarySocket.wait('time').then((response) => {
            const notification_storage = LocalStore.getObject('status_notification');
            const time_difference = (parseInt(response.time) - (parseInt(notification_storage.close_time) || 0));
            const required_difference = 30 * 60;

            if (time_difference > required_difference || notification_storage.message !== message) {
                const $status_message_text = $('#status_notification_text');
                const $close_icon = $('#status_notification_close');
                const $status_notification = $('#status_notification');

                $status_notification.css('display', 'flex');
                $status_message_text.html(message);
                $close_icon
                    .off('click')
                    .on('click', () => {
                        $status_notification.slideUp(200);
                        notification_storage.message = message;
                        notification_storage.close_time = response.time;
                        LocalStore.setObject('status_notification', notification_storage);
                    });
            }
        });
    };

    return {
        onLoad,
        clearNotification,
        displayNotification,
        displayDialogMessage,
        clearDialogMessage,
    };
})();

module.exports = Footer;
<commit_msg>handling status notif display<commit_after>const Cookies        = require('js-cookie');
const BinarySocket   = require('./socket');
const Client         = require('../base/client');
const isEuCountry    = require('../common/country_base').isEuCountry;
const getElementById = require('../../_common/common_functions').getElementById;
const LocalStore     = require('../../_common/storage').LocalStore;
const State          = require('../../_common/storage').State;

const Footer = (() => {
    const onLoad = () => {
        BinarySocket.wait('website_status', 'authorize', 'landing_company').then(() => {
            // show CFD warning to logged in maltainvest clients or
            // logged in virtual clients with maltainvest financial landing company or
            // logged out clients with EU IP address
            if (Client.isLoggedIn()) {
                showWarning((Client.get('landing_company_shortcode') === 'maltainvest' ||
                    (Client.get('is_virtual') && State.getResponse('landing_company.financial_company.shortcode') === 'maltainvest')));
            } else {
                showWarning(isEuCountry());
            }
        });
    };

    const showWarning = (should_show_warning) => {
        $('#footer-regulatory .eu-only').setVisibility(should_show_warning);
    };

    const clearNotification = () => {
        const $status_notification = $('#status_notification');
        $status_notification.slideUp(200);
    };

    // by default elevio is 8px above bottom of page, and scrollup is 18px above elevio
    const adjustElevioAndScrollup = (elevio_height = 8, scrollup_height = 18) => {
        const $elevio_button = $('#_elev_io ._6byvm');
        const $scrollup = $('#scrollup');

        $elevio_button.attr('style', `bottom: ${elevio_height}px !important`);
        $scrollup.attr('style', `bottom: ${$elevio_button.height() + scrollup_height}px`);
    };

    const clearDialogMessage = () => {
        const $dialog_notification = $('#dialog_notification');
        const el_footer = getElementById('footer');
        const $status_notification = $('#status_notification');
        const gap_to_notification = 8;

        $status_notification.css('bottom', `${gap_to_notification}px`);
        el_footer.style.paddingBottom = '0px';
        $dialog_notification.slideUp(200);
        adjustElevioAndScrollup();
    };

    const displayDialogMessage = () => {
        BinarySocket.wait('website_status', 'authorize', 'landing_company').then(() => {
            if (isEuCountry()) {
                const $dialog_notification = $('#dialog_notification');
                const $status_notification = $('#status_notification');
                const el_dialog_notification_accept = getElementById('dialog_notification_accept');
                const el_footer = getElementById('footer');
                const gap_dialog_to_elevio = 30;
                const gap_elevio_to_scrollup = 10;
                const gap_to_notification = 8;

                if ($status_notification.css('display') !== 'none') {
                    $status_notification.css('bottom', `${$dialog_notification.height()}px`);
                }

                $dialog_notification.css('display', 'flex');
                el_footer.style.paddingBottom = `${$dialog_notification.height()}px`;
                adjustElevioAndScrollup($dialog_notification.height() + gap_dialog_to_elevio,
                    $dialog_notification.height() + gap_dialog_to_elevio + gap_elevio_to_scrollup);

                el_dialog_notification_accept
                    .addEventListener('click', () => {
                        adjustElevioAndScrollup();
                        $dialog_notification.slideUp(200);
                        el_footer.style.paddingBottom = '0px';
                        $status_notification.css('bottom', `${gap_to_notification}px`);
                        Cookies.set('CookieConsent', 1);
                    });
                window.addEventListener('resize', () => {
                    adjustElevioAndScrollup($dialog_notification.height() + gap_dialog_to_elevio,
                        $dialog_notification.height() + gap_dialog_to_elevio + gap_elevio_to_scrollup);
                });
            }
        });
    };

    const displayNotification = (message) => {
        BinarySocket.wait('time').then((response) => {
            const notification_storage = LocalStore.getObject('status_notification');
            const $dialog_notification = $('#dialog_notification');
            const $status_notification = $('#status_notification');
            const time_difference = (parseInt(response.time) - (parseInt(notification_storage.close_time) || 0));
            const required_difference = 30 * 60;

            if (time_difference > required_difference || notification_storage.message !== message) {
                const $status_message_text = $('#status_notification_text');
                const $close_icon = $('#status_notification_close');

                if ($dialog_notification.css('display') !== 'none') {
                    $status_notification.css('bottom', `${$dialog_notification.height()}px`);
                }

                $status_notification.css('display', 'flex');
                $status_message_text.html(message);
                $close_icon
                    .off('click')
                    .on('click', () => {
                        $status_notification.slideUp(200);
                        notification_storage.message = message;
                        notification_storage.close_time = response.time;
                        LocalStore.setObject('status_notification', notification_storage);
                    });
            }
        });
    };

    return {
        onLoad,
        clearNotification,
        displayNotification,
        displayDialogMessage,
        clearDialogMessage,
    };
})();

module.exports = Footer;
<|endoftext|>


<commit_before>var path = require('path');
var webpack = require('webpack');

module.exports = {
  devtool: ['eval','sourcemap'],
  entry: [
    'webpack-hot-middleware/client',
    './src/index'
  ],
  output: {
    path: path.join(__dirname, 'devel'),
    filename: 'bundle.js',
    publicPath: '/dist/'
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin(),
    new webpack.NoErrorsPlugin()
  ],
  module: {
    loaders: [
      {
        test: /\.js$/,
        loaders: ['babel'],
        include: path.join(__dirname, 'src')
      },
      {
        test: /\.json$/, loader: "json-loader"
      }
    ]
  }
};
<commit_msg>tmp production build on dev for perf test - revert me<commit_after>var path = require('path');
var webpack = require('webpack');

module.exports = {
  devtool: ['eval','sourcemap'],
  entry: [
    'webpack-hot-middleware/client',
    './src/index'
  ],
  output: {
    path: path.join(__dirname, 'devel'),
    filename: 'bundle.js',
    publicPath: '/dist/'
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin(),
    // perf test on nodes - remove this line to get warnings back.
    new webpack.DefinePlugin({
      "process.env": {
        "NODE_ENV": JSON.stringify("production")
      }
    }),
    new webpack.NoErrorsPlugin()
  ],
  module: {
    loaders: [
      {
        test: /\.js$/,
        loaders: ['babel'],
        include: path.join(__dirname, 'src')
      },
      {
        test: /\.json$/, loader: "json-loader"
      }
    ]
  }
};
<|endoftext|>


<commit_before>(function(root, undefined) {

		/**
		 * Cached regex used to parse event string
		 * @type {RegExp}
		 */
	var _NAME_REGEX = /(?:([\w-]+):)?([\w-]*)(?:.([\w-]+))?/,
		/**
		 * Quick reference to Array.prototype.splice
		 * for duplicating arrays (while removing the
		 * first parameter ala trigger)
		 * @type {Function}
		 */
	var _splicer = [].splice,

		/**
		 * Object merger
		 * @param {Objects}
		 * @return {Object}
		 */
		_extend = function() {
			var args = arguments,
				base = args[0],
				idx = 1, length = args.length,
				key, merger;
			for (; idx < length; idx += 1) {
				merger = args[idx];

				for (key in merger) {
					base[key] = merger[key];
				}
			}
		},
		/**
		 * Unique Id
		 * @type {Number}
		 */
		_id = 0,
		_uniqueId = function() {
			return _id++;
		},

		/**
		 * Holds cached, parsed event keys by string
		 * @type {Object}
		 */
		this._cache = {};

		/**
		 * Holds active events by handle + event + namespace
		 * @type {Object}
		 */
		this._active = {};

		/**
		 * Holds inactive events by handle - lazy creation
		 * @type {Object}
		 */
		// this._inactive;

		/**
		 * Holds subscriptions - lazy creation
		 * @type {Object}
		 */
		// this._subscriptions;
	};

	/**
	 * Returns a new Signal instance
	 * @return {Signal}
	 */
	Signal.construct = function() {
		return new Signal();
	};

	/**
	 * Klass extend method
	 * @param  {Function} constructor
	 * @param  {Object} extension   prototype extension
	 * @return {Function} constructor
	 */
	Signal.extend = function(constructor, extension) {
		var hasConstructor = (typeof constructor === 'function');
		if (!hasConstructor) { extension = constructor; }

		var self = this,
			fn = function() {
				var ret = self.apply(this, arguments);
				if (hasConstructor) {
					ret = constructor.apply(this, arguments);
				}
				return ret;
			};

		// Add properties to the object
		_extend(fn, this);

		// Duplicate the prototype
		var NoOp = function() {};
		NoOp.prototype = this.prototype;
		fn.prototype = new NoOp();

		// Merge the prototypes
		_extend(fn.prototype, this.prototype, extension);
		fn.prototype.constructor = constructor || fn;

		return fn;
	};

	Signal.prototype = {

		constructor: Signal,

		subscribe: function(name, func) {
			this._subscriptions = this._subscriptions || {};

			var id = _uniqueId(),
				location = this._subscriptions[name] || (this._subscriptions[name] = []);

			func.__subid__ = id;
			location.push(func);

			return id;
		},

		unsubscribe: function(name, id) {
			this._subscriptions = this._subscriptions || {};

			var location = this._subscriptions[name];
			if (!location) { return; }

			var idx = 0, length = location.length;
			for (; idx < length; idx += 1) {
				if (location[idx].__subid__ === id) {
					location.splice(idx, 1);
					return true;
				}
			}

			return false;
		},

		dispatch: function() {
			this._subscriptions = this._subscriptions || {};

			var args = arguments,
				name = _splicer.call(args, 0, 1)[0],
				location = this._subscriptions[name] || (this._subscriptions[name] = []),
				idx = 0, length = location.length,
				func;
			for (; idx < length; idx++) {
				func = location[idx];
				if (func) { func.apply(null, args); }
			}
		},

		// Disable | Enable *************************************
		disable: function(handle) {
			this._inactive = this._inactive || {};
			this._inactive[handle] = this._inactive[handle] || {};
			this._inactive[handle] = _extend({}, this._active[handle]);
			delete this._active[handle];

			return this;
		},

		enable: function(handle) {
			this._inactive = this._inactive || {};
			this._active[handle] = this._active[handle] || {};
			this._active[handle] = _extend({}, this._inactive[handle]);
			delete this._inactive[handle];

			return this;
		},

		// On | Off ************************************************
		on: function(eventname, callback) {
			var eventConfig, location,
				cacheConfig = this._cache[eventname];

			if (cacheConfig) {
				eventConfig = cacheConfig;
				location = this._getEventLocation(eventConfig);
			} else {
				eventConfig = this._cache[eventname] = this._parseConfig(eventname);
				location = this._getEventLocation(eventConfig);
			}

			location.push(callback);

			return this;
		},
		bind: function() { this.on.apply(this, arguments); },

		off: function(eventname) {
			var eventConfig,
				cacheConfig = this._cache[eventname];

			if (cacheConfig) {
				eventConfig = cacheConfig;
			} else {
				eventConfig = this._cache[eventname] = this._parseConfig(eventname);
			}

			if (eventConfig.evt === '') { // Removing a namespace

				var events = this._active[eventConfig.handle],
					eventName,
					namespaceName;
				for (eventName in events) {
					for (namespaceName in events[eventName]) {
						if (namespaceName === eventConfig.namespace) {
							this._active[eventConfig.handle][eventName][namespaceName].length = 0;
						}
					}
				}

			} else if (eventConfig.namespace !== '') { // Has a namespace
				this._active[eventConfig.handle][eventConfig.evt][eventConfig.namespace].length = 0;
			} else { // Does not have a namespace
				this._active[eventConfig.handle][eventConfig.evt] = { '': [] };
			}

			return this;
		},
		unbind: function() { this.off.apply(this, arguments); },

		once: function(eventname, callback) {
			var hasRan = false, memo;

			return this.on(eventname, function() {
				return function() {
					if (hasRan) { return memo; }
					hasRan = true;

					memo = callback.apply(this, arguments);
					callback = null;

					return memo;
				};
			});
		},

		// Trigger ************************************************
		trigger: function() {
			var args = arguments,
				eventname = _splicer.call(args, 0, 1)[0],
				cacheConfig = this._cache[eventname];

			if (cacheConfig) {
				eventConfig = cacheConfig;
			} else {
				eventConfig = this._cache[eventname] = this._parseConfig(eventname);
			}

			var location = this._getEventLocation(eventConfig);

			if (eventConfig.namespace !== '') { // If there's a namespace, trigger only that array
				this._callEventArray(location, args);
			} else { // Else, trigger everything registered to the event
				var subSignal = this._active[eventConfig.handle][eventConfig.evt], key;
				for (key in subSignal) {
					this._callEventArray(subSignal[key], args);
				}
			}

			return this;
		},

		// ListenTo | StopListening ********************************
		listenTo: function(obj, eventname, callback) {
			obj.on(eventname, callback);
			return this;
		},
		stopListening: function(obj, eventname) {
			obj.off(eventname);
			return this;
		},

		/* Private *************************************************/
		_uniqueSubId: function() {
			return 's' + _subid++;
		},

		_callEventArray: function(events, args) {
			args = args || [];

			var idx = 0, length = events.length,
				evt;
			for (; idx < length; idx += 1) {
				evt = events[idx];
				if (!evt) { continue; }
				if (evt.apply(null, args) === false) { return; }
			}
		},

		_parseConfig: function(eventname) {
			var match = _NAME_REGEX.exec(eventname);
			return {
				// [0] : the entire match, don't care!
				// [1] : handle
				handle:    (match[1] === undefined) ? '' : match[1],
				// [2] : event
				evt:       (match[2] === undefined) ? '' : match[2],
				// [3] : namespace
				namespace: (match[3] === undefined) ? '' : match[3]
			};
		},

		_getEventLocation: function(eventConfig, location) {
			location = location || this._active;

			var handle = location[eventConfig.handle] || (location[eventConfig.handle] = {}),
				evt = handle[eventConfig.evt] || (handle[eventConfig.evt] = {}),
				namespace = evt[eventConfig.namespace] || (evt[eventConfig.namespace] = []);

			return namespace;
		},

		toString: function() {
			return '[Signal]';
		}
	};

	// Create a pub/sub to expose Signal as
	// e.g. Signal.on(), Signal.trigger()
	var pubSub = new Signal();

	// Attach the Signal object as a property
	// of the exposed object so that new instances
	// can be constructed/extended
	// e.g. Signal.core.construct(), Signal.core.extend({})
	pubSub.core = Signal;

	// Expose
	root.Signal = pubSub;

}(this));<commit_msg>Remove subid method attached to object<commit_after>(function(root, undefined) {

		/**
		 * Cached regex used to parse event string
		 * @type {RegExp}
		 */
	var _NAME_REGEX = /(?:([\w-]+):)?([\w-]*)(?:.([\w-]+))?/,
		/**
		 * Quick reference to Array.prototype.splice
		 * for duplicating arrays (while removing the
		 * first parameter ala trigger)
		 * @type {Function}
		 */
	var _splicer = [].splice,

		/**
		 * Object merger
		 * @param {Objects}
		 * @return {Object}
		 */
		_extend = function() {
			var args = arguments,
				base = args[0],
				idx = 1, length = args.length,
				key, merger;
			for (; idx < length; idx += 1) {
				merger = args[idx];

				for (key in merger) {
					base[key] = merger[key];
				}
			}
		},
		/**
		 * Unique Id
		 * @type {Number}
		 */
		_id = 0,
		_uniqueId = function() {
			return _id++;
		},

		/**
		 * Holds cached, parsed event keys by string
		 * @type {Object}
		 */
		this._cache = {};

		/**
		 * Holds active events by handle + event + namespace
		 * @type {Object}
		 */
		this._active = {};

		/**
		 * Holds inactive events by handle - lazy creation
		 * @type {Object}
		 */
		// this._inactive;

		/**
		 * Holds subscriptions - lazy creation
		 * @type {Object}
		 */
		// this._subscriptions;
	};

	/**
	 * Returns a new Signal instance
	 * @return {Signal}
	 */
	Signal.construct = function() {
		return new Signal();
	};

	/**
	 * Klass extend method
	 * @param  {Function} constructor
	 * @param  {Object} extension   prototype extension
	 * @return {Function} constructor
	 */
	Signal.extend = function(constructor, extension) {
		var hasConstructor = (typeof constructor === 'function');
		if (!hasConstructor) { extension = constructor; }

		var self = this,
			fn = function() {
				var ret = self.apply(this, arguments);
				if (hasConstructor) {
					ret = constructor.apply(this, arguments);
				}
				return ret;
			};

		// Add properties to the object
		_extend(fn, this);

		// Duplicate the prototype
		var NoOp = function() {};
		NoOp.prototype = this.prototype;
		fn.prototype = new NoOp();

		// Merge the prototypes
		_extend(fn.prototype, this.prototype, extension);
		fn.prototype.constructor = constructor || fn;

		return fn;
	};

	Signal.prototype = {

		constructor: Signal,

		subscribe: function(name, func) {
			this._subscriptions = this._subscriptions || {};

			var id = _uniqueId(),
				location = this._subscriptions[name] || (this._subscriptions[name] = []);

			func.__subid__ = id;
			location.push(func);

			return id;
		},

		unsubscribe: function(name, id) {
			this._subscriptions = this._subscriptions || {};

			var location = this._subscriptions[name];
			if (!location) { return; }

			var idx = 0, length = location.length;
			for (; idx < length; idx += 1) {
				if (location[idx].__subid__ === id) {
					location.splice(idx, 1);
					return true;
				}
			}

			return false;
		},

		dispatch: function() {
			this._subscriptions = this._subscriptions || {};

			var args = arguments,
				name = _splicer.call(args, 0, 1)[0],
				location = this._subscriptions[name] || (this._subscriptions[name] = []),
				idx = 0, length = location.length,
				func;
			for (; idx < length; idx++) {
				func = location[idx];
				if (func) { func.apply(null, args); }
			}
		},

		// Disable | Enable *************************************
		disable: function(handle) {
			this._inactive = this._inactive || {};
			this._inactive[handle] = this._inactive[handle] || {};
			this._inactive[handle] = _extend({}, this._active[handle]);
			delete this._active[handle];

			return this;
		},

		enable: function(handle) {
			this._inactive = this._inactive || {};
			this._active[handle] = this._active[handle] || {};
			this._active[handle] = _extend({}, this._inactive[handle]);
			delete this._inactive[handle];

			return this;
		},

		// On | Off ************************************************
		on: function(eventname, callback) {
			var eventConfig, location,
				cacheConfig = this._cache[eventname];

			if (cacheConfig) {
				eventConfig = cacheConfig;
				location = this._getEventLocation(eventConfig);
			} else {
				eventConfig = this._cache[eventname] = this._parseConfig(eventname);
				location = this._getEventLocation(eventConfig);
			}

			location.push(callback);

			return this;
		},
		bind: function() { this.on.apply(this, arguments); },

		off: function(eventname) {
			var eventConfig,
				cacheConfig = this._cache[eventname];

			if (cacheConfig) {
				eventConfig = cacheConfig;
			} else {
				eventConfig = this._cache[eventname] = this._parseConfig(eventname);
			}

			if (eventConfig.evt === '') { // Removing a namespace

				var events = this._active[eventConfig.handle],
					eventName,
					namespaceName;
				for (eventName in events) {
					for (namespaceName in events[eventName]) {
						if (namespaceName === eventConfig.namespace) {
							this._active[eventConfig.handle][eventName][namespaceName].length = 0;
						}
					}
				}

			} else if (eventConfig.namespace !== '') { // Has a namespace
				this._active[eventConfig.handle][eventConfig.evt][eventConfig.namespace].length = 0;
			} else { // Does not have a namespace
				this._active[eventConfig.handle][eventConfig.evt] = { '': [] };
			}

			return this;
		},
		unbind: function() { this.off.apply(this, arguments); },

		once: function(eventname, callback) {
			var hasRan = false, memo;

			return this.on(eventname, function() {
				return function() {
					if (hasRan) { return memo; }
					hasRan = true;

					memo = callback.apply(this, arguments);
					callback = null;

					return memo;
				};
			});
		},

		// Trigger ************************************************
		trigger: function() {
			var args = arguments,
				eventname = _splicer.call(args, 0, 1)[0],
				cacheConfig = this._cache[eventname];

			if (cacheConfig) {
				eventConfig = cacheConfig;
			} else {
				eventConfig = this._cache[eventname] = this._parseConfig(eventname);
			}

			var location = this._getEventLocation(eventConfig);

			if (eventConfig.namespace !== '') { // If there's a namespace, trigger only that array
				this._callEventArray(location, args);
			} else { // Else, trigger everything registered to the event
				var subSignal = this._active[eventConfig.handle][eventConfig.evt], key;
				for (key in subSignal) {
					this._callEventArray(subSignal[key], args);
				}
			}

			return this;
		},

		// ListenTo | StopListening ********************************
		listenTo: function(obj, eventname, callback) {
			obj.on(eventname, callback);
			return this;
		},
		stopListening: function(obj, eventname) {
			obj.off(eventname);
			return this;
		},

		_callEventArray: function(events, args) {
			args = args || [];

			var idx = 0, length = events.length,
				evt;
			for (; idx < length; idx += 1) {
				evt = events[idx];
				if (!evt) { continue; }
				if (evt.apply(null, args) === false) { return; }
			}
		},

		_parseConfig: function(eventname) {
			var match = _NAME_REGEX.exec(eventname);
			return {
				// [0] : the entire match, don't care!
				// [1] : handle
				handle:    (match[1] === undefined) ? '' : match[1],
				// [2] : event
				evt:       (match[2] === undefined) ? '' : match[2],
				// [3] : namespace
				namespace: (match[3] === undefined) ? '' : match[3]
			};
		},

		_getEventLocation: function(eventConfig, location) {
			location = location || this._active;

			var handle = location[eventConfig.handle] || (location[eventConfig.handle] = {}),
				evt = handle[eventConfig.evt] || (handle[eventConfig.evt] = {}),
				namespace = evt[eventConfig.namespace] || (evt[eventConfig.namespace] = []);

			return namespace;
		},

		toString: function() {
			return '[Signal]';
		}
	};

	// Create a pub/sub to expose Signal as
	// e.g. Signal.on(), Signal.trigger()
	var pubSub = new Signal();

	// Attach the Signal object as a property
	// of the exposed object so that new instances
	// can be constructed/extended
	// e.g. Signal.core.construct(), Signal.core.extend({})
	pubSub.core = Signal;

	// Expose
	root.Signal = pubSub;

}(this));<|endoftext|>


<commit_before>/**
 * Copyright 2014 Solinea, Inc.
 *
 * Licensed under the Solinea Software License Agreement (goldstone),
 * Version 1.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.solinea.com/goldstone/LICENSE.pdf
 *
 * 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.
 *
 * Author: Alex Jacobs
 */

var EventsReportView = Backbone.View.extend({

    defaults: {},

    urlGen: function() {

        // urlGen is instantiated inside the beforeSend AJAX hook
        // which means it is run again before every dataTables server query

        var now = +new Date();
        var lookback = +new Date() - (1000 * 60 * this.defaults.globalLookback);
        var urlRouteConstruction = '/core/events?source_name=' +
            this.defaults.hostName + '&created__lte=' + now + '&created__gte=' + lookback;
        this.defaults.url = urlRouteConstruction;
    },

    initialize: function(options) {
        this.options = options || {};
        this.defaults = _.clone(this.defaults);
        this.el = options.el;
        this.defaults.width = options.width;
        this.defaults.hostName = options.nodeName;
        this.defaults.globalLookback = options.globalLookback;

        var ns = this.defaults;
        var self = this;

        // required in case spinner loading takes
        // longer than chart loading
        ns.spinnerDisplay = 'inline';

        var spinnerLocation = this.el;
        $('').load(function() {
            $(this).appendTo(spinnerLocation).css({
                'position': 'relative',
                'margin-top': -20,
                'margin-left': (ns.width / 2),
                'display': ns.spinnerDisplay
            });
        });

        // appends display and modal html elements to this.el
        this.render();

        // this is triggered by a listener set on nodeReportView.js
        this.on('selectorChanged', function() {
            this.defaults.globalLookback = $('#global-lookback-range').val();
        });

    },

    dataPrep: function(data) {
        var ns = this.defaults;
        var self = this;

        // initial result is stringified JSON
        var tableData = JSON.parse(data);

        var finalResults = [];

        _.each(tableData.results, function(item) {

            // if any field is undefined, dataTables throws an alert
            item.id = item.id || '';
            item.event_type = item.event_type || '';
            item.source_id = item.source_id || '';
            item.source_name = item.source_name || '';
            item.message = item.message || '';
            item.created = item.created || '';

            finalResults.push([item.created, item.event_type, item.message, item.id, item.source_id, item.source_name]);
        });

        return {
            recordsTotal: tableData.count,
            recordsFiltered: tableData.count,
            result: finalResults
        };
    },

    drawSearchTable: function(location) {

        var ns = this.defaults;
        var self = this;

        ns.spinnerDisplay = 'none';
        $(this.el).find('#spinner').hide();

        var oTable;
        var oTableParams = {
            "info": true,
            "processing": true,
            "lengthChange": true,
            "paging": true,
            "searching": true,
            "order": [
                [0, 'desc']
            ],
            "ordering": true,
            "serverSide": true,
            "ajax": {
                beforeSend: function(obj, settings) {

                    // warning: as dataTable features are enabled/
                    // disabled , check if the structure of settings.
                    // url changes significantly. Be sure to
                    // reference the correct array indices when
                    // comparing, or scraping data

                    self.urlGen();

                    var pageSize = $(self.el).find('select.form-control').val();
                    var searchQuery = $(self.el).find('input.form-control').val();
                    var paginationStart = settings.url.match(/start=\d{1,}&/gi);
                    paginationStart = paginationStart[0].slice(paginationStart[0].indexOf('=') + 1, paginationStart[0].lastIndexOf('&'));
                    var computeStartPage = Math.floor(paginationStart / pageSize) + 1;
                    var urlColumnOrdering = decodeURIComponent(settings.url).match(/order\[0\]\[column\]=\d*/gi);

                    // capture which column was clicked
                    // and which direction the sort is called for

                    var urlOrderingDirection = decodeURIComponent(settings.url).match(/order\[0\]\[dir\]=(asc|desc)/gi);
                    settings.url = self.defaults.url + "&page_size=" + pageSize + "&page=" + computeStartPage;

                    if (searchQuery) {
                        settings.url = settings.url + "&message__prefix=" + searchQuery;
                    }

                    // if no interesting sort, ignore it
                    if (urlColumnOrdering[0] !== "order[0][column]=0" || urlOrderingDirection[0] !== "order[0][dir]=desc") {
                        // or, if something has changed, capture the
                        // column to sort by, and the sort direction

                        var columnLabelHash = {
                            0: 'created',
                            1: 'event_type',
                            2: 'message'
                        };

                        var orderByColumn = urlColumnOrdering[0].slice(urlColumnOrdering[0].indexOf('=') + 1);

                        var orderByDirection = urlOrderingDirection[0].slice(urlOrderingDirection[0].indexOf('=') + 1);

                        var ascDec;
                        if (orderByDirection === 'asc') {
                            ascDec = '';
                        } else {
                            ascDec = '-';
                        }

                        settings.url = settings.url + "&ordering=" +
                            ascDec + columnLabelHash[orderByColumn];
                    }

                },
                dataFilter: function(data) {

                    // runs result through this.dataPrep
                    var result = self.dataPrep(data);

                    // dataTables expects JSON encoded result
                    return JSON.stringify(result);
                },
                // tells dataTable to look for 'result' param of result object
                dataSrc: "result"
            },
            "columnDefs": [{
                "name": "created",
                "type": "date",
                "targets": 0,
                "render": function(data, type, full, meta) {
                    return moment(data).format();
                }
            }, {
                "name": "event_type",
                "targets": 1
            }, {
                "name": "message",
                "targets": 2
            }, {
                "name": "id",
                "targets": 3
            }, {
                "name": "source_id",
                "targets": 4
            }, {
                "name": "source_name",
                "targets": 5
            }, {
                "visible": false,
                "targets": [3, 4, 5]
            }]
        };

        oTable = $(location).DataTable(oTableParams);
    },

    render: function() {
        $(this.el).append(this.template());
        this.drawSearchTable('#events-report-table');
        return this;
    },

    template: _.template(

        '
' +
        '
' +
        '
' +
        '
' +
        '
 Events Report' +
        '
' +
        '
' +
        '
' +
        '
' +
        '
' +
        '
' +
        'Created	' +
        'Event Type	' +
        'Message	' +
        '
' +
        '
' +
        '
' +
        '
' +
        '
' +
        '
' +
        '
')
});
<commit_msg>Adapts "events" tab report data chart to use goldstone.raiseAlert for error messages.<commit_after>/**
 * Copyright 2014 Solinea, Inc.
 *
 * Licensed under the Solinea Software License Agreement (goldstone),
 * Version 1.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.solinea.com/goldstone/LICENSE.pdf
 *
 * 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.
 *
 * Author: Alex Jacobs
 */

var EventsReportView = Backbone.View.extend({

    defaults: {},

    urlGen: function() {

        // urlGen is instantiated inside the beforeSend AJAX hook
        // which means it is run again before every dataTables server query

        var now = +new Date();
        var lookback = +new Date() - (1000 * 60 * this.defaults.globalLookback);
        var urlRouteConstruction = '/core/events?source_name=' +
            this.defaults.hostName + '&created__lte=' + now + '&created__gte=' + lookback;
        this.defaults.url = urlRouteConstruction;
    },

    initialize: function(options) {
        this.options = options || {};
        this.defaults = _.clone(this.defaults);
        this.el = options.el;
        this.defaults.width = options.width;
        this.defaults.hostName = options.nodeName;
        this.defaults.globalLookback = options.globalLookback;

        var ns = this.defaults;
        var self = this;

        // required in case spinner loading takes
        // longer than chart loading
        ns.spinnerDisplay = 'inline';

        var spinnerLocation = this.el;
        $('').load(function() {
            $(this).appendTo(spinnerLocation).css({
                'position': 'relative',
                'margin-top': -20,
                'margin-left': (ns.width / 2),
                'display': ns.spinnerDisplay
            });
        });

        // appends display and modal html elements to this.el
        this.render();

        // this is triggered by a listener set on nodeReportView.js
        this.on('selectorChanged', function() {
            this.defaults.globalLookback = $('#global-lookback-range').val();
        });

    },

    clearDataErrorMessage: function() {
        // if error message already exists on page,
        // remove it in case it has changed
        if ($(this.el).find('.popup-message').length) {
            $(this.el).find('.popup-message').fadeOut("slow");
        }
    },

    dataErrorMessage: function(message, errorMessage) {

        // 2nd parameter will be supplied in the case of an
        // 'error' event such as 504 error. Othewise,
        // function will append message supplied such as 'no data'.

        if (errorMessage !== undefined) {
            message = errorMessage.responseText;
            message = '' + errorMessage.status + ' error: ' + message;
        }

        // calling raiseAlert with the 3rd param will supress auto-hiding
        goldstone.raiseAlert($(this.el).find('.popup-message'), message, true);
    },

    dataPrep: function(data) {
        var ns = this.defaults;
        var self = this;

        // initial result is stringified JSON
        var tableData = JSON.parse(data);

        var finalResults = [];

        _.each(tableData.results, function(item) {

            // if any field is undefined, dataTables throws an alert
            item.id = item.id || '';
            item.event_type = item.event_type || '';
            item.source_id = item.source_id || '';
            item.source_name = item.source_name || '';
            item.message = item.message || '';
            item.created = item.created || '';

            finalResults.push([item.created, item.event_type, item.message, item.id, item.source_id, item.source_name]);
        });

        return {
            recordsTotal: tableData.count,
            recordsFiltered: tableData.count,
            result: finalResults
        };
    },

    drawSearchTable: function(location) {

        var ns = this.defaults;
        var self = this;

        ns.spinnerDisplay = 'none';
        $(this.el).find('#spinner').hide();

        var oTable;
        var oTableParams = {
            "info": true,
            "processing": true,
            "lengthChange": true,
            "paging": true,
            "searching": true,
            "order": [
                [0, 'desc']
            ],
            "ordering": true,
            "serverSide": true,
            "ajax": {
                beforeSend: function(obj, settings) {

                    // warning: as dataTable features are enabled/
                    // disabled , check if the structure of settings.
                    // url changes significantly. Be sure to
                    // reference the correct array indices when
                    // comparing, or scraping data

                    self.urlGen();

                    var pageSize = $(self.el).find('select.form-control').val();
                    var searchQuery = $(self.el).find('input.form-control').val();
                    var paginationStart = settings.url.match(/start=\d{1,}&/gi);
                    paginationStart = paginationStart[0].slice(paginationStart[0].indexOf('=') + 1, paginationStart[0].lastIndexOf('&'));
                    var computeStartPage = Math.floor(paginationStart / pageSize) + 1;
                    var urlColumnOrdering = decodeURIComponent(settings.url).match(/order\[0\]\[column\]=\d*/gi);

                    // capture which column was clicked
                    // and which direction the sort is called for

                    var urlOrderingDirection = decodeURIComponent(settings.url).match(/order\[0\]\[dir\]=(asc|desc)/gi);
                    settings.url = self.defaults.url + "&page_size=" + pageSize + "&page=" + computeStartPage;

                    if (searchQuery) {
                        settings.url = settings.url + "&message__prefix=" + searchQuery;
                    }

                    // if no interesting sort, ignore it
                    if (urlColumnOrdering[0] !== "order[0][column]=0" || urlOrderingDirection[0] !== "order[0][dir]=desc") {
                        // or, if something has changed, capture the
                        // column to sort by, and the sort direction

                        var columnLabelHash = {
                            0: 'created',
                            1: 'event_type',
                            2: 'message'
                        };

                        var orderByColumn = urlColumnOrdering[0].slice(urlColumnOrdering[0].indexOf('=') + 1);

                        var orderByDirection = urlOrderingDirection[0].slice(urlOrderingDirection[0].indexOf('=') + 1);

                        var ascDec;
                        if (orderByDirection === 'asc') {
                            ascDec = '';
                        } else {
                            ascDec = '-';
                        }

                        settings.url = settings.url + "&ordering=" +
                            ascDec + columnLabelHash[orderByColumn];
                    }

                },
                dataFilter: function(data) {

                    /* dataFilter is analagous to the purpose of 'success',
                    but you can't also use 'success' as then dataFilter
                    will not be triggered */

                    // clear error messages when data begins to flow again
                    self.clearDataErrorMessage();

                    // runs result through this.dataPrep
                    var result = self.dataPrep(data);

                    // dataTables expects JSON encoded result
                    return JSON.stringify(result);
                },
                error: function(data) {
                    // append error message to '.popup-message'
                    self.dataErrorMessage(null, data);
                },
                // tells dataTable to look for 'result' param of result object
                dataSrc: "result"
            },
            "columnDefs": [{
                "name": "created",
                "type": "date",
                "targets": 0,
                "render": function(data, type, full, meta) {
                    return moment(data).format();
                }
            }, {
                "name": "event_type",
                "targets": 1
            }, {
                "name": "message",
                "targets": 2
            }, {
                "name": "id",
                "targets": 3
            }, {
                "name": "source_id",
                "targets": 4
            }, {
                "name": "source_name",
                "targets": 5
            }, {
                "visible": false,
                "targets": [3, 4, 5]
            }]
        };

        oTable = $(location).DataTable(oTableParams);
    },

    render: function() {
        $(this.el).append(this.template());
        this.drawSearchTable('#events-report-table');
        return this;
    },

    template: _.template(

        '
' +
        '
' +
        '
' +
        '
' +
        '
 Events Report' +
        '
' +
        '
' +
        '

' +
        '
' +
        '
' +
        '
' +
        '
' +
        'Created	' +
        'Event Type	' +
        'Message	' +
        '
' +
        '
' +
        '
' +
        '
' +
        '
' +
        '
' +
        '
')
});
<|endoftext|>


<commit_before>var getClassName = Y.ClassNameManager.getClassName,
    slice = Array.prototype.slice;

/**
A lighweight base class for developing simple plugins that behave like widgets.

@class Lite.Widget
@param config {Object} Object literal specifying configuration properties.
**/
function WidgetLite(config) {
    WidgetLite.superclass.constructor.apply(this, arguments);

    config = config || {};

    var host = config.host;

    /**
    @property boundingBox
    @type Node
    **/
    this.boundingBox = host;

    /**
    @property contentBox
    @type Node
    **/
    this.contentBox = host.one(this.CONTENT_SELECTOR) || host;

    /**
    A list of event handles attached during the lifetime of the
    plugin. All this handles get detached when the plugin is detached.
    You're encouraged to add handles to this list when extending the
    Y.Lite.Widget class.

    @property _handles
    @type Array
    @protected
    **/
    this._handles = [];

    this.init(config);
}

/**
@property NS
@type String
@value widget
@static
**/
WidgetLite.NS = 'widget';

/**
@property CLASS_NAMES
@type Object
@static
**/
WidgetLite.CLASS_NAMES = {};

Y.extend(WidgetLite, Y.EventTarget, {

    /**
    A selector that matches the content box of the plugin.
    If set to `null` the `contentBox` property will point to
    the bounding box.

    @property CONTENT_SELECTOR
    @type String
    @protected
    **/
    CONTENT_SELECTOR: '> *',

    /**
    Normalizes class names according to what Y.Widget does
    It loops over the prototype chain and adds classes like yui3-{ns} to the
    bounding box and yui3-{ns}-content to the content box where {ns}
    is the namespace of the plugin (its NS property).
    It also adds classes based on the CLASS_NAMES static property
    (see [CLASS_NAMES](#property_CLASS_NAMES)).

    @method _renderClassNames
    @private
    **/
    _renderClassNames: function () {
        var self = this,
            boundingBox = this.boundingBox;

        function addClassByName(className) {
            this.addClass(self.getClassName(className));
        }

        function addClassesFromOptions(classNames, selector) {
            var nodes = boundingBox.all(selector);
            if (Y.Lang.isArray(classNames)) {
                Y.Array.each(classNames, addClassByName, nodes);
            } else {
                addClassByName.call(nodes, classNames);
            }
        }

        this.contentBox.addClass(this.getClassName('content'));

        Y.Array.each(this.constructor._classes, function (_class) {
            boundingBox.addClass(getClassName(_class.NS));
            Y.Object.each(_class.CLASS_NAMES, addClassesFromOptions);
        });
    },

    /**
    Returns a normalized class name based on Y.ClassNameManager.
    Takes any number of string arguments.

    @method getClassName
    @param {String} classes*
    @return String
    **/
    getClassName: function () {
        return getClassName.apply(Y.ClassNameManager, [this.constructor.NS].concat(slice.call(arguments)));
    },

    /**
    @method init
    @private
    **/
    init: function (config) {
        var self = this,
            superclass = this.constructor,
            classes = [];

        if (!this.constructor._classes) {
            while (superclass && superclass !== WidgetLite.superclass.constructor) {
                classes.push(superclass);
                superclass = superclass.superclass && superclass.superclass.constructor;
            }
            /**
            A list with all the constructors in the prototype chain

            @property _classes
            @type Array
            @private
            @static
            **/
            this.constructor._classes = classes.reverse();
        }

        Y.Array.each(this.constructor._classes, function (_class) {
            if (_class.prototype.hasOwnProperty('initializer')) {
                _class.prototype.initializer.call(self, config);
            }
        });

        this.render();
    },

    renderUI: function () {},

    bindUI: function () {},

    syncUI: function () {},

    /**
    @method render
    @protected
    @chainable
    **/
    render: function () {
        this._renderClassNames();

        this.renderUI();
        this.bindUI();
        this.syncUI();

        return this;
    },

    /**
    @method destroy
    **/
    destroy: function () {
        var self = this;

        Y.Array.each(this._handles, function (handle) {
            handle.detach();
        });

        Y.Array.each(this.constructor._classes, function (_class) {
            if (_class.prototype.hasOwnProperty('destructor')) {
                _class.prototype.destructor.call(self);
            }
        });

        if (this.contentBox !== this.boundingBox) {
            this.contentBox.destroy();
        }

        this.contentBox = this.boundingBox = this._classes = this._handles = null;
    }

});

Y.Lite = {
    Widget: WidgetLite
};<commit_msg>this,_classes no longer exists<commit_after>var getClassName = Y.ClassNameManager.getClassName,
    slice = Array.prototype.slice;

/**
A lighweight base class for developing simple plugins that behave like widgets.

@class Lite.Widget
@param config {Object} Object literal specifying configuration properties.
**/
function WidgetLite(config) {
    WidgetLite.superclass.constructor.apply(this, arguments);

    config = config || {};

    var host = config.host;

    /**
    @property boundingBox
    @type Node
    **/
    this.boundingBox = host;

    /**
    @property contentBox
    @type Node
    **/
    this.contentBox = host.one(this.CONTENT_SELECTOR) || host;

    /**
    A list of event handles attached during the lifetime of the
    plugin. All this handles get detached when the plugin is detached.
    You're encouraged to add handles to this list when extending the
    Y.Lite.Widget class.

    @property _handles
    @type Array
    @protected
    **/
    this._handles = [];

    this.init(config);
}

/**
@property NS
@type String
@value widget
@static
**/
WidgetLite.NS = 'widget';

/**
@property CLASS_NAMES
@type Object
@static
**/
WidgetLite.CLASS_NAMES = {};

Y.extend(WidgetLite, Y.EventTarget, {

    /**
    A selector that matches the content box of the plugin.
    If set to `null` the `contentBox` property will point to
    the bounding box.

    @property CONTENT_SELECTOR
    @type String
    @protected
    **/
    CONTENT_SELECTOR: '> *',

    /**
    Normalizes class names according to what Y.Widget does
    It loops over the prototype chain and adds classes like yui3-{ns} to the
    bounding box and yui3-{ns}-content to the content box where {ns}
    is the namespace of the plugin (its NS property).
    It also adds classes based on the CLASS_NAMES static property
    (see [CLASS_NAMES](#property_CLASS_NAMES)).

    @method _renderClassNames
    @private
    **/
    _renderClassNames: function () {
        var self = this,
            boundingBox = this.boundingBox;

        function addClassByName(className) {
            this.addClass(self.getClassName(className));
        }

        function addClassesFromOptions(classNames, selector) {
            var nodes = boundingBox.all(selector);
            if (Y.Lang.isArray(classNames)) {
                Y.Array.each(classNames, addClassByName, nodes);
            } else {
                addClassByName.call(nodes, classNames);
            }
        }

        this.contentBox.addClass(this.getClassName('content'));

        Y.Array.each(this.constructor._classes, function (_class) {
            boundingBox.addClass(getClassName(_class.NS));
            Y.Object.each(_class.CLASS_NAMES, addClassesFromOptions);
        });
    },

    /**
    Returns a normalized class name based on Y.ClassNameManager.
    Takes any number of string arguments.

    @method getClassName
    @param {String} classes*
    @return String
    **/
    getClassName: function () {
        return getClassName.apply(Y.ClassNameManager, [this.constructor.NS].concat(slice.call(arguments)));
    },

    /**
    @method init
    @private
    **/
    init: function (config) {
        var self = this,
            superclass = this.constructor,
            classes = [];

        if (!this.constructor._classes) {
            while (superclass && superclass !== WidgetLite.superclass.constructor) {
                classes.push(superclass);
                superclass = superclass.superclass && superclass.superclass.constructor;
            }
            /**
            A list with all the constructors in the prototype chain

            @property _classes
            @type Array
            @private
            @static
            **/
            this.constructor._classes = classes.reverse();
        }

        Y.Array.each(this.constructor._classes, function (_class) {
            if (_class.prototype.hasOwnProperty('initializer')) {
                _class.prototype.initializer.call(self, config);
            }
        });

        this.render();
    },

    renderUI: function () {},

    bindUI: function () {},

    syncUI: function () {},

    /**
    @method render
    @protected
    @chainable
    **/
    render: function () {
        this._renderClassNames();

        this.renderUI();
        this.bindUI();
        this.syncUI();

        return this;
    },

    /**
    @method destroy
    **/
    destroy: function () {
        var self = this;

        Y.Array.each(this._handles, function (handle) {
            handle.detach();
        });

        Y.Array.each(this.constructor._classes, function (_class) {
            if (_class.prototype.hasOwnProperty('destructor')) {
                _class.prototype.destructor.call(self);
            }
        });

        if (this.contentBox !== this.boundingBox) {
            this.contentBox.destroy();
        }

        this.contentBox = this.boundingBox = this._handles = null;
    }

});

Y.Lite = {
    Widget: WidgetLite
};<|endoftext|>