<commit_before>/* convenience funtions */

// capitalize the first letter of a string and return it
String.prototype.capitalize = function() {
    return this.charAt(0).toUpperCase() + this.slice(1);
}

/**

Annotator module for group permissions.

- editor extension adds a multiple select input for each permission
  (read, update, delete).  Groups that user is a member of or
  has access to share with should be passed in as editor options, e.g.:

    editorExtensions: [
      annotation_permissions.getEditorExtension({
        groups: {
          "group:1": "class team 1",
          "group:5": "my annotation friends"
        }
      }),
  ]

- currently required to also be included as a module in order to workaround
  a limitation with the permissions handling in annotator.ui.main:

    .include(annotation_permissions.getModule);

- input fields are currently grouped and styled using bootstrap css

- Extends default SimpleIdentityPolicy to allow checking for group
  and superuser permissions. Those should be set similar to the way that
  the default identity is:

      app.ident.identity = "username";
      app.ident.groups = ['mygroup1', 'group2'];
      app.ident.is_superuser = false;

*/

/* based on annotator SimpleIdentityPolicy */

var IdentityPolicy;

IdentityPolicy = function IdentityPolicy() {
    this.identity = null;
    this.groups = [];
    this.is_superuser = false;
};

IdentityPolicy.prototype.who = function () {
    return this.identity;
};

/* extend built-in authz policy with a method to check superuser and groups */
annotator.authz.AclAuthzPolicy.prototype.extended_permits = function (action, context, identity) {
    // as soon as we get a permitted response; otherwise, keep checking
    if (identity.is_superuser) { return true; }
    // username based check
    if (annotator.authz.AclAuthzPolicy.prototype.permits(action, context, identity.identity)) {
        return true;
    }
    // check each group id in turn
    for (var i = 0; i < identity.groups.length; i++) {
         if (annotator.authz.AclAuthzPolicy.prototype.permits(action, context, identity.groups[i])) {
            return true;
        }
    }
    return false;
}

var annotation_permissions = {
    options: {},
    permissions: ['read', 'update', 'delete'],
    getId: function(mode) {
        return 'annotator-permissions-' + mode;
    },
    getLabel: function(mode) {
        return $('' + mode.capitalize() + '')
            .attr('for', annotation_permissions.getId(mode));
    },

    getEditorExtension: function getEditorExtension(options) {
        // customize the editor to add permission editing

        annotation_permissions.options = options;

        return function editorExtension(editor) {
            // The input element added to the Annotator.Editor wrapped in jQuery.
            // Cached to save having to recreate it everytime the editor is displayed.
            var field, input;

            if ($.isEmptyObject(options.groups)) {
                console.warn('No groups are configured, not enabling permissions');
                return;
            }

            function loadPermissions(field, annotation) {
                // load permissions from the annotation object
                // to preselect groups that already have access
                var id, input;
                if (annotation.permissions)  {
                    for (var mode in annotation.permissions) {
                        id = annotation_permissions.getId(mode);
                        input = $(field).find('[id=' + id + ']');
                        // set input selection using array of group ids
                        input.val(annotation.permissions[mode]);
                    }
                }
            }

            function setPermissions(field, annotation) {
                // set annotation permissions based on selected
                // values on the edit form

                // NOTE: don't add any permissions data if nothing is selected
                var perms = $.extend({}, annotation.permissions),
                    item_index;
                // fixme: merge isn't right; need to make sure any *selected*
                // groups are present, and deselected groups are removed
                // for each permission mode, update who should have access
                $.each(annotation_permissions.permissions, function(index, mode) {
                    // iterate through each option and check if it is selected or not
                    $(field).find('[id=' + annotation_permissions.getId(mode) + ']')
                        .find('option').each(function(idx, opt){
                            if (! (mode in perms)) {
                                // make sure list exists so items can be added
                                perms[mode] = [];
                            }
                            item_index = perms[mode].indexOf(opt.value);

                            // if it is selected, make sure it is in the list
                            if (opt.selected) {
                                if (item_index === -1) {
                                    perms[mode].push(opt.value);
                                }
                            } else {
                                // if not selected, remove if previously listed
                                if (item_index > -1) {
                                    perms[mode].splice(item_index, 1);
                                }
                            }
                        });
                });

                // NOTE: currently, any permissions set here are overwritten
                // in a ui.main submit callback; as a workaround, store them
                // in a special field do be copied in place after that.
                if (perms.read || perms.update || perms.delete) {
                    annotation.rdx_permissions = perms;
                }
            }

            // use the editor add field method to create the initial
            // input and set hooks for load and submit methods
            var mode = annotation_permissions.permissions[0];
            var input_id = annotation_permissions.getId(mode);
            field = editor.addField({
                id: input_id,
                label: _t('permissions') + '\u2026',
                load: loadPermissions,
                submit: setPermissions,
                type: 'select',

            });
            var $field = $(field),
                $heading = $('
Permissions
'),
                $content_div = $('
'),
                $div = $('
');

            // add a label for this section
            $field.prepend($heading);
            $heading.on('click', function() { $content_div.toggle(750); });
            // add a class for styling/identification purposes
            $field.addClass('permission row');
            $field.append($content_div);
            $content_div.append($('
Allow annotation group members access to this annotation.
'));

            // find the initial input and add a label
            input = $(field).find(':input');
            input.addClass('form-control');
            $content_div.append($div);
            input.detach().appendTo($div)
            annotation_permissions.getLabel(mode).insertBefore(input);

            // toggle, so perms are hidden by default

            // configure input to allow multiple selection
            input.attr('multiple', 'multiple')
            // add an option for each group passed in
            for (var group in options.groups) {
                input.append($("<option></option>")
                     .attr("value", group).text(options.groups[group]));
            }

            // copy the input and add labels for each other permission mode
            $.each(annotation_permissions.permissions, function(index, perm) {
                if (index == 0) { return; }  // skip first perm, already done

                // create a new form-group div
                $div = $('
');
                // generate and append a new label
                $div.append(annotation_permissions.getLabel(perm));
                // copy the first input, update the id, and append
                var copy = input.clone();
                copy.attr("id", annotation_permissions.getId(perm));
                $div.append(copy);
                $content_div.append($div);
            });
        }
    },

    _app: null,

    fix_permissions: function(annotation) {

        if ('rdx_permissions' in annotation) {
            // copy local perms over permissions, and delete local permissions
            annotation.permissions = annotation.rdx_permissions;
            delete annotation.rdx_permissions;

            // NOTE: may need to explicitly include the annotation author
            // in all roles, since annotator seems to behave somewhat
            // differently when permissions are set

            // workaround issue in main.ui:
            // if permissions are set, annotator adds the *current* anntotaor
            // identify, rather than the annotation author.
            // If that's present here, update so the author is listed.
            var ident = annotation_permissions._app.registry.getUtility('identityPolicy');
            if (annotation.permissions.read[0] == ident.who() &&
                ident.who() != annotation.user) {
                annotation.permissions.read[0] = annotation.user;
            }
            if (annotation.permissions.update[0] == ident.who() &&
                ident.who() != annotation.user) {
                annotation.permissions.update[0] = annotation.user;
            }

            // re-save the annotation after making changes
            annotation_permissions._app.registry.getUtility('storage').update(annotation);
            // update the display to re-render changes (marginalia specific)
            annotation_permissions.renderExtension(annotation,
                $('.marginalia-item[data-annotation-id=' + annotation.id + ']'));
        }

        return true;
    },

    identity: new IdentityPolicy(),

    getModule: function() {
        return {
            // name, for identification in list of annotator modules
            name: 'annotation-permissions',

            // configuration based on SimpleIdentityPolicy
            configure: function (registry) {
                registry.registerUtility(annotation_permissions.identity, 'identityPolicy');
            },
            beforeAnnotationCreated: function (annotation) {
                annotation.user = annotation_permissions.identity.who();
            },

            start: function (app) {
                // store a reference to the app, in order to retrieve
                // storage and identity
                annotation_permissions._app = app;
            },

            // NOTE: beforeAnnotationCreated and beforeAnnotationUpdated
            // seem like they should be the right hooks to use,
            // but they fire before the editor is loaded and
            // these updates need to happen after the editor is submitted.

            annotationCreated: function (annotation) {
                annotation_permissions.fix_permissions(annotation);
            },

            annotationUpdated: function(annotation) {
                annotation_permissions.fix_permissions(annotation);
            }
        };
    },

    renderExtension: function(annotation, item) {
        item.find('.annotation-permissions').remove();
        // nothing to do if no permissions are set
        if ($.isEmptyObject(annotation.permissions)) {
            // return item unchanged
            return item;
        }
        // get a list of people/groups other than current user with
        // permission on this annotation
        var shared_with = [];
        // combine lists from all permissions
        $.each(annotation.permissions, function(mode, values) {
            $.each(values, function(i, val) {
                // if not annotation author and not already present, add to list
                if (val != annotation.user && shared_with.indexOf(val) === -1) {
                    shared_with.push(val);
                }
            });
        });

        // if there is a list, add an icon
        if (shared_with.length) {
            // convert group identifiers into display names
            var group_names = [];
            $.each(shared_with, function(i, group_id) {
                group_names.push(annotation_permissions.options.groups[group_id]);
            });

            var perms_icon = $('');
            perms_icon.attr('title', 'Shared with ' + group_names);
            perms_icon.append($(''));
            item.find('.annotation-footer').append(perms_icon);
        }
        return item;
    },

};
<commit_msg>Only display permissions edit fields if user has admin access<commit_after>/* convenience funtions */

// capitalize the first letter of a string and return it
String.prototype.capitalize = function() {
    return this.charAt(0).toUpperCase() + this.slice(1);
}

/**

Annotator module for group permissions.

- editor extension adds a multiple select input for each permission
  (read, update, delete).  Groups that user is a member of or
  has access to share with should be passed in as editor options, e.g.:

    editorExtensions: [
      annotation_permissions.getEditorExtension({
        groups: {
          "group:1": "class team 1",
          "group:5": "my annotation friends"
        }
      }),
  ]

- currently required to also be included as a module in order to workaround
  a limitation with the permissions handling in annotator.ui.main:

    .include(annotation_permissions.getModule);

- input fields are currently grouped and styled using bootstrap css

- Extends default SimpleIdentityPolicy to allow checking for group
  and superuser permissions. Those should be set similar to the way that
  the default identity is:

      app.ident.identity = "username";
      app.ident.groups = ['mygroup1', 'group2'];
      app.ident.is_superuser = false;

*/

/* based on annotator SimpleIdentityPolicy */

var IdentityPolicy;

IdentityPolicy = function IdentityPolicy() {
    this.identity = null;
    this.groups = [];
    this.is_superuser = false;
};

IdentityPolicy.prototype.who = function () {
    return this.identity;
};

/* extend built-in authz policy with a method to check superuser and groups */
annotator.authz.AclAuthzPolicy.prototype.extended_permits = function (action, context, identity) {
    // as soon as we get a permitted response; otherwise, keep checking
    if (identity.is_superuser) { return true; }
    // username based check
    if (annotator.authz.AclAuthzPolicy.prototype.permits(action, context, identity.identity)) {
        return true;
    }
    // check each group id in turn
    for (var i = 0; i < identity.groups.length; i++) {
         if (annotator.authz.AclAuthzPolicy.prototype.permits(action, context, identity.groups[i])) {
            return true;
        }
    }
    return false;
}

var annotation_permissions = {
    options: {},
    permissions: ['read', 'update', 'delete'],
    getId: function(mode) {
        return 'annotator-permissions-' + mode;
    },
    getLabel: function(mode) {
        return $('' + mode.capitalize() + '')
            .attr('for', annotation_permissions.getId(mode));
    },

    adminAllowed: function(annotation) {
        // check if the current user has admin access to the
        // specified annotation
        var app = annotation_permissions._app,
            ident = app.registry.getUtility('identityPolicy'),
            authz = app.registry.getUtility('authorizationPolicy');

        return authz.extended_permits('admin', annotation, ident);
    },

    getEditorExtension: function getEditorExtension(options) {
        // customize the editor to add permission editing

        annotation_permissions.options = options;

        return function editorExtension(editor) {
            // The input element added to the Annotator.Editor wrapped in jQuery.
            // Cached to save having to recreate it everytime the editor is displayed.
            var field, input;

            if ($.isEmptyObject(options.groups)) {
                console.warn('No groups are configured, not enabling permissions');
                return;
            }

            function loadPermissions(field, annotation) {
                // load permissions from the annotation object
                // to preselect groups that already have access

                // if current user does not have admin permissions on this
                // annotation, hide permissions
                if (! annotation_permissions.adminAllowed(annotation)) {
                    // find the permission div that contains this fields
                    $(field).closest('.permission').hide();
                    return;
                }

                // ensure permissions are visible, since the editor
                // is re-used for all anotnations and permissions
                // may have previously been hidden
                $(field).closest('.permission').show();

                var id, input;
                if (annotation.permissions)  {
                    for (var mode in annotation.permissions) {
                        id = annotation_permissions.getId(mode);
                        input = $(field).find('[id=' + id + ']');
                        // set input selection using array of group ids
                        input.val(annotation.permissions[mode]);
                    }
                }
            }

            function setPermissions(field, annotation) {
                // set annotation permissions based on selected
                // values on the edit form

                // if current user does not have admin permissions on this
                // annotation, do nothing
                if (! annotation_permissions.adminAllowed(annotation)) {
                    return;
                }

                // NOTE: don't add any permissions data if nothing is selected
                var perms = $.extend({}, annotation.permissions),
                    item_index;
                // fixme: merge isn't right; need to make sure any *selected*
                // groups are present, and deselected groups are removed
                // for each permission mode, update who should have access
                $.each(annotation_permissions.permissions, function(index, mode) {
                    // iterate through each option and check if it is selected or not
                    $(field).find('[id=' + annotation_permissions.getId(mode) + ']')
                        .find('option').each(function(idx, opt){
                            if (! (mode in perms)) {
                                // make sure list exists so items can be added
                                perms[mode] = [];
                            }
                            item_index = perms[mode].indexOf(opt.value);

                            // if it is selected, make sure it is in the list
                            if (opt.selected) {
                                if (item_index === -1) {
                                    perms[mode].push(opt.value);
                                }
                            } else {
                                // if not selected, remove if previously listed
                                if (item_index > -1) {
                                    perms[mode].splice(item_index, 1);
                                }
                            }
                        });
                });

                // NOTE: currently, any permissions set here are overwritten
                // in a ui.main submit callback; as a workaround, store them
                // in a special field do be copied in place after that.
                if (perms.read || perms.update || perms.delete) {
                    annotation.rdx_permissions = perms;
                }
            }

            // use the editor add field method to create the initial
            // input and set hooks for load and submit methods
            var mode = annotation_permissions.permissions[0];
            var input_id = annotation_permissions.getId(mode);
            field = editor.addField({
                id: input_id,
                label: _t('permissions') + '\u2026',
                load: loadPermissions,
                submit: setPermissions,
                type: 'select',

            });
            var $field = $(field),
                $heading = $('
Permissions
'),
                $content_div = $('
'),
                $div = $('
');

            // add a label for this section
            $field.prepend($heading);
            $heading.on('click', function() { $content_div.toggle(750); });
            // add a class for styling/identification purposes
            $field.addClass('permission row');
            $field.append($content_div);
            $content_div.append($('
Allow annotation group members access to this annotation.
'));

            // find the initial input and add a label
            input = $(field).find(':input');
            input.addClass('form-control');
            $content_div.append($div);
            input.detach().appendTo($div)
            annotation_permissions.getLabel(mode).insertBefore(input);

            // toggle, so perms are hidden by default

            // configure input to allow multiple selection
            input.attr('multiple', 'multiple')
            // add an option for each group passed in
            for (var group in options.groups) {
                input.append($("<option></option>")
                     .attr("value", group).text(options.groups[group]));
            }

            // copy the input and add labels for each other permission mode
            $.each(annotation_permissions.permissions, function(index, perm) {
                if (index == 0) { return; }  // skip first perm, already done

                // create a new form-group div
                $div = $('
');
                // generate and append a new label
                $div.append(annotation_permissions.getLabel(perm));
                // copy the first input, update the id, and append
                var copy = input.clone();
                copy.attr("id", annotation_permissions.getId(perm));
                $div.append(copy);
                $content_div.append($div);
            });
        }
    },

    _app: null,

    fix_permissions: function(annotation) {

        if ('rdx_permissions' in annotation) {
            // copy local perms over permissions, and delete local permissions
            annotation.permissions = annotation.rdx_permissions;
            delete annotation.rdx_permissions;

            // NOTE: may need to explicitly include the annotation author
            // in all roles, since annotator seems to behave somewhat
            // differently when permissions are set

            // workaround issue in main.ui:
            // if permissions are set, annotator adds the *current* anntotaor
            // identify, rather than the annotation author.
            // If that's present here, update so the author is listed.
            var ident = annotation_permissions._app.registry.getUtility('identityPolicy');
            if (annotation.permissions.read[0] == ident.who() &&
                ident.who() != annotation.user) {
                annotation.permissions.read[0] = annotation.user;
            }
            if (annotation.permissions.update[0] == ident.who() &&
                ident.who() != annotation.user) {
                annotation.permissions.update[0] = annotation.user;
            }

            // re-save the annotation after making changes
            annotation_permissions._app.registry.getUtility('storage').update(annotation);
            // update the display to re-render changes (marginalia specific)
            annotation_permissions.renderExtension(annotation,
                $('.marginalia-item[data-annotation-id=' + annotation.id + ']'));
        }

        return true;
    },

    identity: new IdentityPolicy(),

    getModule: function() {
        return {
            // name, for identification in list of annotator modules
            name: 'annotation-permissions',

            // configuration based on SimpleIdentityPolicy
            configure: function (registry) {
                registry.registerUtility(annotation_permissions.identity, 'identityPolicy');

            },
            beforeAnnotationCreated: function (annotation) {
                annotation.user = annotation_permissions.identity.who();
            },

            start: function (app) {
                // store a reference to the app, in order to retrieve
                // storage and identity
                annotation_permissions._app = app;
            },

            // NOTE: beforeAnnotationCreated and beforeAnnotationUpdated
            // seem like they should be the right hooks to use,
            // but they fire before the editor is loaded and
            // these updates need to happen after the editor is submitted.

            annotationCreated: function (annotation) {
                annotation_permissions.fix_permissions(annotation);
            },

            annotationUpdated: function(annotation) {
                annotation_permissions.fix_permissions(annotation);
            }
        };
    },

    renderExtension: function(annotation, item) {
        item.find('.annotation-permissions').remove();
        // nothing to do if no permissions are set
        if ($.isEmptyObject(annotation.permissions)) {
            // return item unchanged
            return item;
        }
        // get a list of people/groups other than current user with
        // permission on this annotation
        var shared_with = [];
        // combine lists from all permissions
        $.each(annotation.permissions, function(mode, values) {
            $.each(values, function(i, val) {
                // if not annotation author and not already present, add to list
                if (val != annotation.user && shared_with.indexOf(val) === -1) {
                    shared_with.push(val);
                }
            });
        });

        // if there is a list, add an icon
        if (shared_with.length) {
            // convert group identifiers into display names
            var group_names = [];
            $.each(shared_with, function(i, group_id) {
                group_names.push(annotation_permissions.options.groups[group_id]);
            });

            var perms_icon = $('');
            perms_icon.attr('title', 'Shared with ' + group_names);
            perms_icon.append($(''));
            item.find('.annotation-footer').append(perms_icon);
        }
        return item;
    },

};
<|endoftext|>


<commit_before>'use strict'
const url = require('url')
const Site = require('./site.js')
const cheerio = require('cheerio')
const Bluebird = require('bluebird')

class FanFictionNet extends Site {
  static matches (siteUrlStr) {
    const siteUrl = url.parse(siteUrlStr)
    const hostname = siteUrl.hostname
    if (!/(^|www[.])fanfiction.net$/.test(hostname)) return false
    const path = siteUrl.pathname || siteUrl.path || ''
    if (!/^[/]s[/]\d+[/]\d+/.test(path)) return false
    return true
  }

  constructor (siteUrlStr) {
    super(siteUrlStr)
    this.publisher = 'www.fanfiction.net'
    this.publisherName = 'FanFiction.net'
    const siteUrl = url.parse(siteUrlStr)
    const path = siteUrl.pathname || siteUrl.path || ''
    var ficMatch = path.match(/^[/]s[/](\d+)[/]\d+(?:[/](.*))?/)
    this.ficId = ficMatch[1]
    this.name = ficMatch[2]
  }

  chapterUrl (num) {
    return 'https://www.fanfiction.net/s/' + this.ficId + '/' + num + (this.name ? '/' + this.name : '')
  }

  chapterListUrl () {
    return this.chapterUrl(1)
  }

  getFicMetadata (fetch, fic) {
    fic.link = this.link
    fic.publisher = this.publisherName
    return this.getChapter(fetch, this.chapterListUrl()).then(chapter => {
      const $ = cheerio.load(chapter.raw)
      const $meta = $('#profile_top')
      const $dates = $meta.find('span[data-xutime]')
      fic.title = $meta.find('b.xcontrast_txt').text()
      fic.link = this.normalizeLink(chapter.finalUrl)
      fic.author = chapter.author
      fic.authorUrl = chapter.authorUrl
      fic.created = new Date(Number($($dates[1]).attr('data-xutime'))*1000)
      fic.modified = new Date(Number($($dates[0]).attr('data-xutime'))*1000)
      fic.publisher = this.publisherName
      fic.description = $meta.find('div.xcontrast_txt').text()
      const infoline = $meta.find('span.xgray').text()
      const infomatches = infoline.match(/Rated:\s+(.*)\s+-\s+(\S+)\s+-\s+(.*)\s+-\s+Chapters:\s+\d+\s+-\s+Words:\s+([\d,]+)\s+-\s+Reviews:\s+([,\d]+)\s+-\s+Favs:\s+([,\d]+)\s+-\s+Follows:\s+([,\d]+)/)
      if (infomatches) {
        const rated = infomatches[1]
        fic.language = infomatches[2]
        fic.tags =  infomatches[3].split(/, /).concat(['rated:' + rated])
        const words = infomatches[4]
        const reviews = infomatches[5]
        const favs = infomatches[6]
        const follows = infomatches[7]
      } else {
        console.log('NOMATCH:', infoline)
      }

      const $index = $($('#chap_select')[0])
      const $chapters = $index.find('option')
      $chapters.each((ii, vv) => {
        const chapterName = $(vv).text().match(/^\d+[.](?: (.*))?$/)
        const chapterNum = $(vv).attr('value') || ii
        fic.addChapter(chapterName[1] || (String(chapterNum) + '.'), this.chapterUrl(chapterNum))
      })
    })
  }

  scrapeFicMetadata (fetch, fic) {
    // There's never any reason to scrape FFN content, AFAIK.
    return Bluebird.resolve()
  }

  getChapter (fetch, chapter) {
    return fetch(chapter).spread((meta, html) => {
      const $ = cheerio.load(html)
      const $content = $('#storytextp')
      const base = $('base').attr('href') || meta.finalUrl
      const links = $('a.xcontrast_txt')
      let authorName
      let authorUrl
      links.each(function(ii, vv) {
        const href = $(vv).attr('href')
        if (/^[/]u[/]\d+[/]/.test(href)) {
          authorName = $(vv).text()
          authorUrl = url.resolve(base, href)
        }
      })
      return {
        chapterLink: chapter,
        finalUrl: meta.finalUrl,
        base: base,
        author: authorName,
        authorUrl: authorUrl,
        raw: html,
        content: $content.html()
      }
    })
  }
}
module.exports = FanFictionNet<commit_msg>fanfictionnet: Add word count support to ffnet loader<commit_after>'use strict'
const url = require('url')
const Site = require('./site.js')
const cheerio = require('cheerio')
const Bluebird = require('bluebird')

class FanFictionNet extends Site {
  static matches (siteUrlStr) {
    const siteUrl = url.parse(siteUrlStr)
    const hostname = siteUrl.hostname
    if (!/(^|www[.])fanfiction.net$/.test(hostname)) return false
    const path = siteUrl.pathname || siteUrl.path || ''
    if (!/^[/]s[/]\d+[/]\d+/.test(path)) return false
    return true
  }

  constructor (siteUrlStr) {
    super(siteUrlStr)
    this.publisher = 'www.fanfiction.net'
    this.publisherName = 'FanFiction.net'
    const siteUrl = url.parse(siteUrlStr)
    const path = siteUrl.pathname || siteUrl.path || ''
    var ficMatch = path.match(/^[/]s[/](\d+)[/]\d+(?:[/](.*))?/)
    this.ficId = ficMatch[1]
    this.name = ficMatch[2]
  }

  chapterUrl (num) {
    return 'https://www.fanfiction.net/s/' + this.ficId + '/' + num + (this.name ? '/' + this.name : '')
  }

  chapterListUrl () {
    return this.chapterUrl(1)
  }

  getFicMetadata (fetch, fic) {
    fic.link = this.link
    fic.publisher = this.publisherName
    return this.getChapter(fetch, this.chapterListUrl()).then(chapter => {
      const $ = cheerio.load(chapter.raw)
      const $meta = $('#profile_top')
      const $dates = $meta.find('span[data-xutime]')
      fic.title = $meta.find('b.xcontrast_txt').text()
      fic.link = this.normalizeLink(chapter.finalUrl)
      fic.author = chapter.author
      fic.authorUrl = chapter.authorUrl
      fic.created = new Date(Number($($dates[1]).attr('data-xutime'))*1000)
      fic.modified = new Date(Number($($dates[0]).attr('data-xutime'))*1000)
      fic.publisher = this.publisherName
      fic.description = $meta.find('div.xcontrast_txt').text()
      const infoline = $meta.find('span.xgray').text()
      const infomatches = infoline.match(/Rated:\s+(.*)\s+-\s+(\S+)\s+-\s+(.*)\s+-\s+Chapters:\s+\d+\s+-\s+Words:\s+([\d,]+)\s+-\s+Reviews:\s+([,\d]+)\s+-\s+Favs:\s+([,\d]+)\s+-\s+Follows:\s+([,\d]+)/)
      if (infomatches) {
        const rated = infomatches[1]
        fic.language = infomatches[2]
        fic.tags =  infomatches[3].split(/, /).concat(['rated:' + rated])
        fic.words = infomatches[4]
        const reviews = infomatches[5]
        const favs = infomatches[6]
        const follows = infomatches[7]
      } else {
        console.log('NOMATCH:', infoline)
      }

      const $index = $($('#chap_select')[0])
      const $chapters = $index.find('option')
      $chapters.each((ii, vv) => {
        const chapterName = $(vv).text().match(/^\d+[.](?: (.*))?$/)
        const chapterNum = $(vv).attr('value') || ii
        fic.addChapter(chapterName[1] || (String(chapterNum) + '.'), this.chapterUrl(chapterNum))
      })
    })
  }

  scrapeFicMetadata (fetch, fic) {
    // There's never any reason to scrape FFN content, AFAIK.
    return Bluebird.resolve()
  }

  getChapter (fetch, chapter) {
    return fetch(chapter).spread((meta, html) => {
      const $ = cheerio.load(html)
      const $content = $('#storytextp')
      const base = $('base').attr('href') || meta.finalUrl
      const links = $('a.xcontrast_txt')
      let authorName
      let authorUrl
      links.each(function(ii, vv) {
        const href = $(vv).attr('href')
        if (/^[/]u[/]\d+[/]/.test(href)) {
          authorName = $(vv).text()
          authorUrl = url.resolve(base, href)
        }
      })
      return {
        chapterLink: chapter,
        finalUrl: meta.finalUrl,
        base: base,
        author: authorName,
        authorUrl: authorUrl,
        raw: html,
        content: $content.html()
      }
    })
  }
}
module.exports = FanFictionNet<|endoftext|>


<commit_before>$(function(){
	$("select[name='per_page'], select[name='sort']").selectpicker({style: 'btn-default', menuStyle: 'dropdown-inverse'});
	$('.no-js').removeClass('no-js');
});

$(document).ready(function(){

	var $navbarAffix = $('.navbar-affix');

	if($navbarAffix.length>0){

		anchorTags();
		$(window).resize(function(){
			anchorTags();
		})

		function anchorTags(){

			var $navbarAffix = $('.navbar-affix'),
			$navbarFixedTop = $('.navbar-fixed-top')
			$adjSection = $('.navbar-affix+section'),
			offsetTop = $navbarAffix.offset().top-$navbarAffix.height();
			$navbarAffix.affix({
				offset: {
					top: offsetTop
				}
			});
			$adjSection.on('affixed.bs.affix',function(){
				$(this).css({'padding-top':$navbarAffix.height()});
			});
			$('body').scrollspy({ target: '.navbar-affix', offset: 2*$navbarAffix.height()+$navbarFixedTop.height()})
		}
	}

	var $animateScrollLinks = $(".nav.animate li a");

	if($animateScrollLinks.length>0){
		$animateScrollLinks.on('click',function(evt){
			evt.preventDefault();

			var spacerH = parseInt($(".navbar-fixed-top").height()+ $('.navbar-affix').height()),
			full_url = $(this).attr('href'),
			parts = full_url.split("#"),
			trgt = parts[1],
			target_offset = $("#"+trgt).offset(),
			target_top = target_offset.top-spacerH;

			$(window).resize(function(){
				spacerH = parseInt($(".navbar-fixed-top").height()+ $('.navbar-affix').height());
			})

			$('html, body').animate({scrollTop:target_top+"px"}, 500);
		})
	}

	$('.location-marker .glyphicon').tooltip();

	var q = window.location.search,
	params = {
		load_results: 'load=',
		search_term: 'term=',
		per_page: 'per_page=',
		sort: 'sort=',
		adv:  'adv=',
		topic: 'topic='
	};

	q = q.substring(1,q.length);

	var load = q.substring(q.indexOf(params.load_results)+params.load_results.length, q.indexOf(params.search_term)),
	term = getParam(q,params.search_term),
	per_page = getParam(q,params.per_page),
	sort = getParam(q,params.sort);

	function getParam(q,term){
		var str = q.substring(q.indexOf(term)+term.length, q.length);
		if(str.indexOf('&')>0)
			str = str.substring(0,str.indexOf('&'));
		return str;
	}
	if(window.location.search==''&& $(".container#search").length>0){
		$("#no-results-found").remove();
	}
	if(load.replace('&','')=='true'){
		//loadResults();
	}

	if(q.indexOf(params.search_term)>0){
		$('#id_keyword').val(unescape(term));
	}

	if(q.indexOf(params.adv)>0){
		$('.adv.group').slideToggle(0);
		$('.adv.switch').addClass('active');
	}

	var $perPage = $("#id_per_page + .btn-group .dropdown-toggle .filter-option"),
	currentVisiblePageNumber = $.trim($perPage.html());

	var $sort = $("#id_sort + .btn-group .dropdown-toggle .filter-option"),
	currentVisibleSort = $.trim($sort.html());

	if(currentVisiblePageNumber !==per_page){
		getCurrentDropdownSetting('per_page', per_page);
	}
	var firstSortOption = $("#id_sort+div .dropdown-menu li[rel='0']").text();
	if(currentVisibleSort !==sort && sort!=firstSortOption){
		getCurrentDropdownSetting('sort', sort);
	}

	function getCurrentDropdownSetting(id, value){
		var val = unescape(value)
		$("#id_"+id+" + .btn-group li span").each(function(){
			var num = $(this).html();
			if(num.length>0){
				num = $.trim(num);
			}
			if(num == val){
				$(this).parents('.dropdown-menu').siblings('button').find('.filter-option').html(num);
				$(this).parents('li').addClass('selected').siblings().removeClass('selected');
			}
		});
	}

	var $form = $('form');

	$( "input" )
	.keyup(function() {
		var value = $( this ).val();
		$(this).attr('value',value);
	});

	$('#search .option select').bind('change',function(){
		search.get();
	});

	$('.browse .option select').bind('change',function(){
		var topic = getParam(q,params.topic),
		search = '?topic='+topic+'&'+ getQueryStringFromForm('.browse'),
		url = window.location.origin + window.location.pathname + search;

		window.location = url;
	});

	$form.on('keyup',function(e){
		e.preventDefault();
		var code = e.keyCode || e.which,
		$input = $(document.activeElement);

		if(code == 13){
			$(this).submit();
		}
	});
	$(".home form").on('submit',function(e){
		e.preventDefault();
		search.get(true);
	});
	$('#submit, #submit2').on('click', function(e){
		e.preventDefault();
		search.get();
	});

	$('.home #submit, .home #submit2').on('click', function(e){
		e.preventDefault();
		search.get(true);
	});

	var $scrollbar = $('.scrollbar');
	if($scrollbar.length>0){

		$scrollbar.scroller({
			horizontal: true
		}).animate({'opacity':1},1000,function(){
			$scrollbar.scroller("scroll", '.active');
		});
		$scrollbar.scroller("scroll", '.active');

	}
	$("#date-range input").datepicker( {
		format: "yyyy",
		viewMode: "years",
		minViewMode: "years"
	});

});//end doc.ready

var search = {
	get: getResults,
	load: loadResults
}

function getResults(move){
	var $form = $('form'),
	protocol = window.location.protocol,
	host = window.location.host,
	path = window.location.pathname;
	query = getQueryStringFromForm($form),
	$keywordInput = $('.input-group input#id_keyword'),
	keyword=$keywordInput.attr('value');

	if(path.indexOf('search')<0){
		path='/search/';
	}
	request = protocol+'//'+host+path+'?'+query;

	// if(keyword!='' && keyword!==undefined){
		$('.dropdown-menu, .dropdown-arrow').hide();

		if(move){
			moveElementsForResults(request);
		}
		else{
			loadResults(request, query);
		}
	// }
	// //keyword input is empty
	// else{
	// 	$keywordInput.parent().addClass('has-error');
	// 	$keywordInput.parent().shake();
	// }
}

function loadResults(request_url){

	var $loader = $('.loader'),
	$results = $('.results');

	$('.loader').slideDown(500);
	$('.loader').animate({'opacity':'1'},1000);

	$('.results').animate({'opacity':'0'},300)

	$.get(request_url,function(data){
		changeLocation(request_url);
	});
}

function moveElementsForResults(url){
	var $header = $('#search h1'),
	$recent = $('.recent'),
	$filter = $('.page-options'),
	$footer =$('.footer');

	if($filter.not(":visible").length>0){
		$filter.slideUp(0);
	}
	$('.slideup').slideUp(500);
	$footer.hide();

	$('.loader').slideDown(500);

	$('.home').css({'background-color':'#F7F7F7'});
	if($filter.not(":visible").length>0){
		$filter.slideDown(500)
		$filter.fadeIn(500);
	}
	$recent.fadeOut(500);
	$header.slideUp(500, function(){
		$(this).animate({'opacity':'0'},1000,function(){
			changeLocation(url);
		})

	});
}
function changeLocation(url){
	window.location=url;
}

function getQueryStringFromForm(form){
	var $form = $(form);
	if($form.length<0){
		return '';
	}
	var str ='', count=0;
	$form.find('input').each(function(){
		var $this = $(this);
		if($this.parent('.sr-only').length<=0){
			if($this.attr('name')!=undefined){
				if(count>0){
					str+='&';
				}
				var value = $this.attr('value') || $this.val() || '';

				str+=$this.attr('name')+'='+escape(value);
			}
			count++;
		}
	});
	$form.find('select').each(function(){
		var $this = $(this);
		if($this.parent('.sr-only').length<=0){
			if(count>0){
				str+='&';
			}
			str+=$this.attr('name')+'='+$.trim($this.children('option:selected').html());
			count++;
		}
	});
	if($('.adv.group:visible').length>0){
		str+='&adv=true';
	}
	return str;
}

function replaceQueryString(url,param,value) {
	var re = new RegExp("([?|&])" + param + "=.*?(&|$)","i");
	if (url.match(re))
		return url.replace(re,'$1' + param + "=" + value + '$2');
	else
		return url + '&' + param + "=" + value;
}

//Shake function
(function( $ ){
	$.fn.shake = function() {
		var $this = $(this);

		$this.animate({'left':'-20px'},40, function(){
			$(this).animate({left:'10px'},80, function(){
				$(this).animate({'left':'0px'},50)
			})
		});

		return this;
	};
})( jQuery );

//debug
function log(){
	console.log.apply(console, arguments);
}

<commit_msg>Minor JS fix for Firefox<commit_after>$(function(){
	$("select[name='per_page'], select[name='sort']").selectpicker({style: 'btn-default', menuStyle: 'dropdown-inverse'});
	$('.no-js').removeClass('no-js');
});

$(document).ready(function(){

	var $navbarAffix = $('.navbar-affix');

	if($navbarAffix.length>0){
		function anchorTags(){

			var $navbarAffix = $('.navbar-affix'),
			$navbarFixedTop = $('.navbar-fixed-top')
			$adjSection = $('.navbar-affix+section'),
			offsetTop = $navbarAffix.offset().top-$navbarAffix.height();
			$navbarAffix.affix({
				offset: {
					top: offsetTop
				}
			});
			$adjSection.on('affixed.bs.affix',function(){
				$(this).css({'padding-top':$navbarAffix.height()});
			});
			$('body').scrollspy({ target: '.navbar-affix', offset: 2*$navbarAffix.height()+$navbarFixedTop.height()})
		}
		anchorTags();
		$(window).resize(function(){
			anchorTags();
		})

	}

	var $animateScrollLinks = $(".nav.animate li a");

	if($animateScrollLinks.length>0){
		$animateScrollLinks.on('click',function(evt){
			evt.preventDefault();

			var spacerH = parseInt($(".navbar-fixed-top").height()+ $('.navbar-affix').height()),
			full_url = $(this).attr('href'),
			parts = full_url.split("#"),
			trgt = parts[1],
			target_offset = $("#"+trgt).offset(),
			target_top = target_offset.top-spacerH;

			$(window).resize(function(){
				spacerH = parseInt($(".navbar-fixed-top").height()+ $('.navbar-affix').height());
			})

			$('html, body').animate({scrollTop:target_top+"px"}, 500);
		})
	}

	$('.location-marker .glyphicon').tooltip();

	var q = window.location.search,
	params = {
		load_results: 'load=',
		search_term: 'term=',
		per_page: 'per_page=',
		sort: 'sort=',
		adv:  'adv=',
		topic: 'topic='
	};

	q = q.substring(1,q.length);

	var load = q.substring(q.indexOf(params.load_results)+params.load_results.length, q.indexOf(params.search_term)),
	term = getParam(q,params.search_term),
	per_page = getParam(q,params.per_page),
	sort = getParam(q,params.sort);

	function getParam(q,term){
		var str = q.substring(q.indexOf(term)+term.length, q.length);
		if(str.indexOf('&')>0)
			str = str.substring(0,str.indexOf('&'));
		return str;
	}
	if(window.location.search==''&& $(".container#search").length>0){
		$("#no-results-found").remove();
	}
	if(load.replace('&','')=='true'){
		//loadResults();
	}

	if(q.indexOf(params.search_term)>0){
		$('#id_keyword').val(unescape(term));
	}

	if(q.indexOf(params.adv)>0){
		$('.adv.group').slideToggle(0);
		$('.adv.switch').addClass('active');
	}

	var $perPage = $("#id_per_page + .btn-group .dropdown-toggle .filter-option"),
	currentVisiblePageNumber = $.trim($perPage.html());

	var $sort = $("#id_sort + .btn-group .dropdown-toggle .filter-option"),
	currentVisibleSort = $.trim($sort.html());

	if(currentVisiblePageNumber !==per_page){
		getCurrentDropdownSetting('per_page', per_page);
	}
	var firstSortOption = $("#id_sort+div .dropdown-menu li[rel='0']").text();
	if(currentVisibleSort !==sort && sort!=firstSortOption){
		getCurrentDropdownSetting('sort', sort);
	}

	function getCurrentDropdownSetting(id, value){
		var val = unescape(value)
		$("#id_"+id+" + .btn-group li span").each(function(){
			var num = $(this).html();
			if(num.length>0){
				num = $.trim(num);
			}
			if(num == val){
				$(this).parents('.dropdown-menu').siblings('button').find('.filter-option').html(num);
				$(this).parents('li').addClass('selected').siblings().removeClass('selected');
			}
		});
	}

	var $form = $('form');

	$( "input" )
	.keyup(function() {
		var value = $( this ).val();
		$(this).attr('value',value);
	});

	$('#search .option select').bind('change',function(){
		search.get();
	});

	$('.browse .option select').bind('change',function(){
		var topic = getParam(q,params.topic),
		search = '?topic='+topic+'&'+ getQueryStringFromForm('.browse'),
		url = window.location.origin + window.location.pathname + search;

		window.location = url;
	});

	$form.on('keyup',function(e){
		e.preventDefault();
		var code = e.keyCode || e.which,
		$input = $(document.activeElement);

		if(code == 13){
			$(this).submit();
		}
	});
	$(".home form").on('submit',function(e){
		e.preventDefault();
		search.get(true);
	});
	$('#submit, #submit2').on('click', function(e){
		e.preventDefault();
		search.get();
	});

	$('.home #submit, .home #submit2').on('click', function(e){
		e.preventDefault();
		search.get(true);
	});

	var $scrollbar = $('.scrollbar');
	if($scrollbar.length>0){

		$scrollbar.scroller({
			horizontal: true
		}).animate({'opacity':1},1000,function(){
			$scrollbar.scroller("scroll", '.active');
		});
		$scrollbar.scroller("scroll", '.active');

	}
	$("#date-range input").datepicker( {
		format: "yyyy",
		viewMode: "years",
		minViewMode: "years"
	});

});//end doc.ready

var search = {
	get: getResults,
	load: loadResults
}

function getResults(move){
	var $form = $('form'),
	protocol = window.location.protocol,
	host = window.location.host,
	path = window.location.pathname;
	query = getQueryStringFromForm($form),
	$keywordInput = $('.input-group input#id_keyword'),
	keyword=$keywordInput.attr('value');

	if(path.indexOf('search')<0){
		path='/search/';
	}
	request = protocol+'//'+host+path+'?'+query;

	// if(keyword!='' && keyword!==undefined){
		$('.dropdown-menu, .dropdown-arrow').hide();

		if(move){
			moveElementsForResults(request);
		}
		else{
			loadResults(request, query);
		}
	// }
	// //keyword input is empty
	// else{
	// 	$keywordInput.parent().addClass('has-error');
	// 	$keywordInput.parent().shake();
	// }
}

function loadResults(request_url){

	var $loader = $('.loader'),
	$results = $('.results');

	$('.loader').slideDown(500);
	$('.loader').animate({'opacity':'1'},1000);

	$('.results').animate({'opacity':'0'},300)

	$.get(request_url,function(data){
		changeLocation(request_url);
	});
}

function moveElementsForResults(url){
	var $header = $('#search h1'),
	$recent = $('.recent'),
	$filter = $('.page-options'),
	$footer =$('.footer');

	if($filter.not(":visible").length>0){
		$filter.slideUp(0);
	}
	$('.slideup').slideUp(500);
	$footer.hide();

	$('.loader').slideDown(500);

	$('.home').css({'background-color':'#F7F7F7'});
	if($filter.not(":visible").length>0){
		$filter.slideDown(500)
		$filter.fadeIn(500);
	}
	$recent.fadeOut(500);
	$header.slideUp(500, function(){
		$(this).animate({'opacity':'0'},1000,function(){
			changeLocation(url);
		})

	});
}
function changeLocation(url){
	window.location=url;
}

function getQueryStringFromForm(form){
	var $form = $(form);
	if($form.length<0){
		return '';
	}
	var str ='', count=0;
	$form.find('input').each(function(){
		var $this = $(this);
		if($this.parent('.sr-only').length<=0){
			if($this.attr('name')!=undefined){
				if(count>0){
					str+='&';
				}
				var value = $this.attr('value') || $this.val() || '';

				str+=$this.attr('name')+'='+escape(value);
			}
			count++;
		}
	});
	$form.find('select').each(function(){
		var $this = $(this);
		if($this.parent('.sr-only').length<=0){
			if(count>0){
				str+='&';
			}
			str+=$this.attr('name')+'='+$.trim($this.children('option:selected').html());
			count++;
		}
	});
	if($('.adv.group:visible').length>0){
		str+='&adv=true';
	}
	return str;
}

function replaceQueryString(url,param,value) {
	var re = new RegExp("([?|&])" + param + "=.*?(&|$)","i");
	if (url.match(re))
		return url.replace(re,'$1' + param + "=" + value + '$2');
	else
		return url + '&' + param + "=" + value;
}

//Shake function
(function( $ ){
	$.fn.shake = function() {
		var $this = $(this);

		$this.animate({'left':'-20px'},40, function(){
			$(this).animate({left:'10px'},80, function(){
				$(this).animate({'left':'0px'},50)
			})
		});

		return this;
	};
})( jQuery );

//debug
function log(){
	console.log.apply(console, arguments);
}

<|endoftext|>


<commit_before>/**
 * AnalyticsDashboardWidgetTopPagesTable component.
 *
 * Site Kit by Google, Copyright 2021 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * External dependencies
 */
import classnames from 'classnames';
import PropTypes from 'prop-types';

/**
 * WordPress dependencies
 */
import { __ } from '@wordpress/i18n';

/**
 * Internal dependencies
 */
import Data from 'googlesitekit-data';
import { getTimeInSeconds, numFmt } from '../../../../util';
import withData from '../../../../components/higherorder/withData';
import { STORE_NAME, DATE_RANGE_OFFSET } from '../../datastore/constants';
import { CORE_USER } from '../../../../googlesitekit/datastore/user/constants';
import { TYPE_MODULES } from '../../../../components/data';
import { getDataTableFromData } from '../../../../components/data-table';
import PreviewTable from '../../../../components/PreviewTable';
import { getTopPagesReportDataDefaults } from '../../util';
import TableOverflowContainer from '../../../../components/TableOverflowContainer';
import Link from '../../../../components/Link';
import { generateDateRangeArgs } from '../../util/report-date-range-args';

const { useSelect, withSelect } = Data;

const AnalyticsDashboardWidgetTopPagesTable = ( props ) => {
	const { data, colspan } = props;
	const dateRangeDates = useSelect( ( select ) => select( CORE_USER ).getDateRangeDates( {
		offsetDays: DATE_RANGE_OFFSET,
	} ) );

	if ( ! data || ! data.length ) {
		return null;
	}

	if ( ! Array.isArray( data[ 0 ].data.rows ) ) {
		return null;
	}

	const headers = [
		{
			title: __( 'Title', 'google-site-kit' ),
			tooltip: __( 'Page Title', 'google-site-kit' ),
			primary: true,
		},
		{
			title: __( 'Pageviews', 'google-site-kit' ),
			tooltip: __( 'Pageviews', 'google-site-kit' ),
		},
		{
			title: __( 'Unique Pageviews', 'google-site-kit' ),
			tooltip: __( 'Unique Pageviews', 'google-site-kit' ),
		},
		{
			title: __( 'Bounce Rate', 'google-site-kit' ),
			tooltip: __( 'Bounce Rate', 'google-site-kit' ),
		},
	];

	const links = [];
	const dataMapped = data[ 0 ].data.rows.map( ( row, i ) => {
		const percent = Number( row.metrics[ 0 ].values[ 2 ] ) / 100;
		const [ title, url ] = row.dimensions;
		links[ i ] = url;

		return [
			title,
			numFmt( row.metrics[ 0 ].values[ 0 ], { style: 'decimal' } ),
			numFmt( row.metrics[ 0 ].values[ 1 ], { style: 'decimal' } ),
			(

					{ numFmt( percent, '%' ) }

			),
		];
	} );

	const options = {
		hideHeader: false,
		chartsEnabled: false,
		links,
		hideColumns: {
			mobile: [ 2, 3 ],
		},
		PrimaryLink: withSelect( ( select, { href = '/' } ) => {
			const serviceURL = select( STORE_NAME ).getServiceReportURL( 'content-drilldown', {
				'explorer-table.plotKeys': '[]',
				'_r.drilldown': `analytics.pagePath:${ href }`,
				...generateDateRangeArgs( dateRangeDates ),
			} );

			return {
				href: serviceURL,
				external: true,
			};
		} )( Link ),
	};

	const dataTable = getDataTableFromData( dataMapped, headers, options );

	return (
		<div className={ classnames(
			'mdc-layout-grid__cell',
			`mdc-layout-grid__cell--span-${ colspan }`
		) }>
			<TableOverflowContainer>
				{ dataTable }
			</TableOverflowContainer>

	);
};

AnalyticsDashboardWidgetTopPagesTable.propTypes = {
	data: PropTypes.array,
	colspan: PropTypes.number,
};

AnalyticsDashboardWidgetTopPagesTable.defaultProps = {
	data: null,
	colspan: 12,
};

export default withData(
	AnalyticsDashboardWidgetTopPagesTable,
	[
		{
			type: TYPE_MODULES,
			identifier: 'analytics',
			datapoint: 'report',
			data: getTopPagesReportDataDefaults(),
			priority: 1,
			maxAge: getTimeInSeconds( 'day' ),
			context: [ 'Single', 'Dashboard' ],
		},
	],
	<PreviewTable padding />,
	{ createGrid: true }
);
<commit_msg>Refactor AnalyticsDashboardWidgetTopPagesTable with ReportTable.<commit_after>/**
 * AnalyticsDashboardWidgetTopPagesTable component.
 *
 * Site Kit by Google, Copyright 2021 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * External dependencies
 */
import classnames from 'classnames';
import PropTypes from 'prop-types';

/**
 * WordPress dependencies
 */
import { __ } from '@wordpress/i18n';

/**
 * Internal dependencies
 */
import Data from 'googlesitekit-data';
import { getTimeInSeconds, numFmt } from '../../../../util';
import withData from '../../../../components/higherorder/withData';
import { STORE_NAME, DATE_RANGE_OFFSET } from '../../datastore/constants';
import { CORE_USER } from '../../../../googlesitekit/datastore/user/constants';
import { TYPE_MODULES } from '../../../../components/data';
import PreviewTable from '../../../../components/PreviewTable';
import { getTopPagesReportDataDefaults } from '../../util';
import TableOverflowContainer from '../../../../components/TableOverflowContainer';
import Link from '../../../../components/Link';
import { generateDateRangeArgs } from '../../util/report-date-range-args';
import ReportTable from '../../../../components/ReportTable';

const { useSelect } = Data;

const AnalyticsDashboardWidgetTopPagesTable = ( props ) => {
	const { data, colspan } = props;

	if ( ! data || ! data.length ) {
		return null;
	}

	if ( ! Array.isArray( data[ 0 ].data.rows ) ) {
		return null;
	}

	return (
		<div className={ classnames(
			'mdc-layout-grid__cell',
			`mdc-layout-grid__cell--span-${ colspan }`
		) }>
			<TableOverflowContainer>
				<ReportTable
					rows={ data[ 0 ].data.rows }
					columns={ tableColumns }
				/>
			</TableOverflowContainer>

	);
};

const tableColumns = [
	{
		title: __( 'Title', 'google-site-kit' ),
		description: __( 'Page Title', 'google-site-kit' ),
		primary: true,
		Component: ( { row } ) => {
			const [ title, url ] = row.dimensions;
			const serviceURL = useSelect( ( select ) => {
				const dateRangeDates = select( CORE_USER ).getDateRangeDates( {
					offsetDays: DATE_RANGE_OFFSET,
				} );
				return select( STORE_NAME ).getServiceReportURL( 'content-drilldown', {
					'explorer-table.plotKeys': '[]',
					'_r.drilldown': `analytics.pagePath:${ url }`,
					...generateDateRangeArgs( dateRangeDates ),
				} );
			} );
			return (
				<Link
					href={ serviceURL }
					external
					inherit
				>
					{ title }
				</Link>
			);
		},
	},
	{
		title: __( 'Pageviews', 'google-site-kit' ),
		description: __( 'Pageviews', 'google-site-kit' ),
		Component: ( { row } ) => numFmt( row.metrics[ 0 ].values[ 0 ], { style: 'decimal' } ),
	},
	{
		title: __( 'Unique Pageviews', 'google-site-kit' ),
		description: __( 'Unique Pageviews', 'google-site-kit' ),
		className: 'hidden-on-mobile',
		Component: ( { row } ) => numFmt( row.metrics[ 0 ].values[ 1 ], { style: 'decimal' } ),
	},
	{
		title: __( 'Bounce Rate', 'google-site-kit' ),
		description: __( 'Bounce Rate', 'google-site-kit' ),
		className: 'hidden-on-mobile',
		Component: ( { row } ) => {
			const percent = Number( row.metrics[ 0 ].values[ 2 ] ) / 100;
			return numFmt( percent, '%' );
		},
	},
];

AnalyticsDashboardWidgetTopPagesTable.propTypes = {
	data: PropTypes.array,
	colspan: PropTypes.number,
};

AnalyticsDashboardWidgetTopPagesTable.defaultProps = {
	data: null,
	colspan: 12,
};

export default withData(
	AnalyticsDashboardWidgetTopPagesTable,
	[
		{
			type: TYPE_MODULES,
			identifier: 'analytics',
			datapoint: 'report',
			data: getTopPagesReportDataDefaults(),
			priority: 1,
			maxAge: getTimeInSeconds( 'day' ),
			context: [ 'Single', 'Dashboard' ],
		},
	],
	<PreviewTable padding />,
	{ createGrid: true }
);
<|endoftext|>


<commit_before>/*
 * Copyright (C) 2013 salesforce.com, inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
({

    testThrowable: {
        attributes : {
            throwableClass:"java.lang.Throwable",
            throwableCause:"couldn't decide"
        },
        test: function(cmp){
            var expectedMessage = "java://org.auraframework.components.test.java.controller.JavaTestController: " +
                    "java.lang.Throwable: couldn't decide";
            this.checkUnHandledExceptionResponse(cmp, "ERROR", expectedMessage);
        }
    },

    testRuntimeException: {
        attributes : {
            throwableClass:"java.lang.RuntimeException",
            throwableCause:"java.lang.IllegalAccessException"
        },
        test: function(cmp){
            var expectedMessage = "java://org.auraframework.components.test.java.controller.JavaTestController: " +
                    "java.lang.RuntimeException: java.lang.IllegalAccessException";
            this.checkUnHandledExceptionResponse(cmp, "ERROR", expectedMessage);
        }
    },

    testError: {
        attributes : {
            throwableClass:"java.lang.Error",
            throwableCause:"java.lang.RuntimeException"
        },
        test: function(cmp){
            var expectedMessage = "java://org.auraframework.components.test.java.controller.JavaTestController: " +
                    "java.lang.Error: java.lang.RuntimeException";
            this.checkUnHandledExceptionResponse(cmp, "ERROR", expectedMessage);
        }
    },

    testInvocationTargetException: {
        attributes : {
            throwableClass:"java.lang.reflect.InvocationTargetException",
            throwableCause:"java.lang.IllegalArgumentException"
        },
        test: function(cmp){
            var expectedMessage = "java://org.auraframework.components.test.java.controller.JavaTestController: " +
                    "java.lang.reflect.InvocationTargetException";
            this.checkUnHandledExceptionResponse(cmp, "ERROR", expectedMessage);
        }
    },

    testInvocationTargetExceptionHanlded: {
        attributes : {
            throwableClass:"java.lang.reflect.InvocationTargetException",
            throwableCause:"aura.throwable.AuraHandledException"
        },
        test: function(cmp){
            var expectedMessage = "java://org.auraframework.components.test.java.controller.JavaTestController: " +
                    "java.lang.reflect.InvocationTargetException";
            this.checkUnHandledExceptionResponse(cmp, "ERROR", expectedMessage);
        }
    },

    testIllegalArgumentException: {
        attributes : {
            throwableClass:"java.lang.IllegalArgumentException",
            throwableCause:"you're not listening"
        },
        test: function(cmp){
            var expectedMessage = "java://org.auraframework.components.test.java.controller.JavaTestController: " +
                    "java.lang.IllegalArgumentException: you're not listening";
            this.checkUnHandledExceptionResponse(cmp, "ERROR", expectedMessage);
        }
    },

    testIllegalAccessException: {
        attributes : {
            throwableClass:"java.lang.IllegalAccessException",
            throwableCause:"under 21"
        },
        test: function(cmp){
            var expectedMessage = "java://org.auraframework.components.test.java.controller.JavaTestController: " +
                    "java.lang.IllegalAccessException: under 21";
            this.checkUnHandledExceptionResponse(cmp, "ERROR", expectedMessage);
        }
    },

    testAuraHandledException: {
        attributes : {
            throwableClass:"aura.throwable.AuraHandledException",
            throwableCause:"java.lang.IllegalArgumentException"
        },
        test: function(cmp){
            var expectedStack = "org.auraframework.throwable.AuraHandledException";
            this.checkHandledExceptionResponse(cmp, "ERROR", "", expectedStack);
        }
    },

    testAuraHandledExceptionString: {
        attributes : {
            throwableClass: "aura.throwable.AuraHandledException",
            throwableCause: "something to say"
        },
        test: function(cmp) {
            var expectedMessage = "something to say";
            var expectedStack = "org.auraframework.throwable.AuraHandledException: something to say";
            this.checkHandledExceptionResponse(cmp, "ERROR", expectedMessage, expectedStack);
        }
    },

    /**
     * Test that an XHR-level exception fires the corresponding app-level event
     * and sets this action to ERROR state.
     *
     * Note: Test.js' setup pushes an access context so it's not possible for a cmp test
     * to create and verify behavior of an access context-less XHR receive loop.
     */
    testXhrLevelException: {
        test: [
            function(cmp){
                // set an invalid token to trigger an XHR-level exception event
                $A.clientService.resetToken("invalid");

                cmp.find("trigger").get("e.press").fire();
                $A.test.addWaitForWithFailureMessage(true, function(){ return !!cmp.get("v.response");},
                    "response attribute is never set",
                    function() {
                        var action = cmp.get("v.response");
                        $A.test.assertEquals("ERROR", action.state, "Found unexpected state");

                        var msg = action.error[0].message;
                        var expectedMessage = "Received exception event from server";
                        $A.test.assertStartsWith(expectedMessage, msg, "Found unexpected error message");

                        // For client side error, different browsers may have different format of stacktrace.
                        // Only check stack exists.
                        // On IE, stack is added when error is thrown. For error in action, we create a new Error
                        // instance in AuraClientService.prototype.processErrors, so there is no stack for IE.
                        var stack = action.error[0].stack;
                        var browser = $A.get("$Browser");
                        if(!browser.isIE10 && !browser.isIE11) {
                            $A.test.assertTrue(stack.length > 0, "Unexpected stack: " + stack);
                        }
                    }
                );
            },
            function(cmp) {
                $A.test.assertTrue(cmp.get("v.invalidSessionReceived"), "aura:invalidSession event was not fired / received");
            }
        ]
    },

    //Because AuraHandledExceptions are treated differently
    checkHandledExceptionResponse : function(cmp, expectedState, expectedMessage, expectedStack){
        cmp.find("trigger").get("e.press").fire();
        $A.test.addWaitForWithFailureMessage(true, function(){ return !!cmp.get("v.response");},
            "response attribute is never set",
            function() {
                var action = cmp.get("v.response");
                var msg = action.error[0].message;
                var stack = action.error[0].stack;
                $A.test.assertEquals(expectedState, action.state, "Found unexpected state");

                if(expectedMessage){
                    $A.test.assertStartsWith(expectedMessage, msg, "Found unexpected error message");
                }

                if(expectedStack){
                    $A.test.assertTrue(stack.indexOf(expectedStack) > -1, "Unexpected stack: " + stack);
                }
            }
        );
    },

    checkUnHandledExceptionResponse : function(cmp, expectedState, expectedMessage, expectedStack){
        cmp.find("trigger").get("e.press").fire();
        $A.test.runAfterIf(
            function(){ return cmp.get("v.response"); },
            function(){
                var action = cmp.get("v.response");
                var msg = action.error[0].message;
                var stack = action.error[0].stack;
                $A.test.assertEquals(expectedState, action.state, "Unexpected state: ");
                if(expectedMessage){
                    $A.test.assertNotNull(
                            msg.match(/^An internal server error has occurred\nError ID:.*\n\norg\.auraframework\.throwable\.AuraExecutionException:/)
                            ||
                            msg.match(/^Unable to process your request\n\norg\.auraframework\.throwable\.AuraExecutionException:/)
                    );
                    $A.test.assertTrue(msg.indexOf(expectedMessage)!= -1,
                            "Expected error message not seen: Expected {"+expectedMessage+"} but saw {"+msg+"}");
                }
                if(expectedStack){
                    $A.test.assertTrue(stack.indexOf(expectedStack) === 0, "Unexpected stack: " + stack);
                } else {
                    $A.test.assertTrue(stack.indexOf("org.auraframework.throwable.AuraUnhandledException: ") === 0, "Unexpected stack: " + stack);
                }
            }
        );
    }
})
<commit_msg>Last test failure<commit_after>/*
 * Copyright (C) 2013 salesforce.com, inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
({

    testThrowable: {
        attributes : {
            throwableClass:"java.lang.Throwable",
            throwableCause:"couldn't decide"
        },
        test: function(cmp){
            var expectedMessage = "java://org.auraframework.components.test.java.controller.JavaTestController: " +
                    "java.lang.Throwable: couldn't decide";
            this.checkUnHandledExceptionResponse(cmp, "ERROR", expectedMessage);
        }
    },

    testRuntimeException: {
        attributes : {
            throwableClass:"java.lang.RuntimeException",
            throwableCause:"java.lang.IllegalAccessException"
        },
        test: function(cmp){
            var expectedMessage = "java://org.auraframework.components.test.java.controller.JavaTestController: " +
                    "java.lang.RuntimeException: java.lang.IllegalAccessException";
            this.checkUnHandledExceptionResponse(cmp, "ERROR", expectedMessage);
        }
    },

    testError: {
        attributes : {
            throwableClass:"java.lang.Error",
            throwableCause:"java.lang.RuntimeException"
        },
        test: function(cmp){
            var expectedMessage = "java://org.auraframework.components.test.java.controller.JavaTestController: " +
                    "java.lang.Error: java.lang.RuntimeException";
            this.checkUnHandledExceptionResponse(cmp, "ERROR", expectedMessage);
        }
    },

    testInvocationTargetException: {
        attributes : {
            throwableClass:"java.lang.reflect.InvocationTargetException",
            throwableCause:"java.lang.IllegalArgumentException"
        },
        test: function(cmp){
            var expectedMessage = "java://org.auraframework.components.test.java.controller.JavaTestController: " +
                    "java.lang.reflect.InvocationTargetException";
            this.checkUnHandledExceptionResponse(cmp, "ERROR", expectedMessage);
        }
    },

    testInvocationTargetExceptionHanlded: {
        attributes : {
            throwableClass:"java.lang.reflect.InvocationTargetException",
            throwableCause:"aura.throwable.AuraHandledException"
        },
        test: function(cmp){
            var expectedMessage = "java://org.auraframework.components.test.java.controller.JavaTestController: " +
                    "java.lang.reflect.InvocationTargetException";
            this.checkUnHandledExceptionResponse(cmp, "ERROR", expectedMessage);
        }
    },

    testIllegalArgumentException: {
        attributes : {
            throwableClass:"java.lang.IllegalArgumentException",
            throwableCause:"you're not listening"
        },
        test: function(cmp){
            var expectedMessage = "java://org.auraframework.components.test.java.controller.JavaTestController: " +
                    "java.lang.IllegalArgumentException: you're not listening";
            this.checkUnHandledExceptionResponse(cmp, "ERROR", expectedMessage);
        }
    },

    testIllegalAccessException: {
        attributes : {
            throwableClass:"java.lang.IllegalAccessException",
            throwableCause:"under 21"
        },
        test: function(cmp){
            var expectedMessage = "java://org.auraframework.components.test.java.controller.JavaTestController: " +
                    "java.lang.IllegalAccessException: under 21";
            this.checkUnHandledExceptionResponse(cmp, "ERROR", expectedMessage);
        }
    },

    testAuraHandledException: {
        attributes : {
            throwableClass:"aura.throwable.AuraHandledException",
            throwableCause:"java.lang.IllegalArgumentException"
        },
        test: function(cmp){
            var expectedStack = "org.auraframework.throwable.AuraHandledException";
            this.checkHandledExceptionResponse(cmp, "ERROR", "", expectedStack);
        }
    },

    testAuraHandledExceptionString: {
        attributes : {
            throwableClass: "aura.throwable.AuraHandledException",
            throwableCause: "something to say"
        },
        test: function(cmp) {
            var expectedMessage = "something to say";
            var expectedStack = "org.auraframework.throwable.AuraHandledException: something to say";
            this.checkHandledExceptionResponse(cmp, "ERROR", expectedMessage, expectedStack);
        }
    },

    /**
     * Test that an XHR-level exception fires the corresponding app-level event
     * and sets this action to ERROR state.
     *
     * Note: Test.js' setup pushes an access context so it's not possible for a cmp test
     * to create and verify behavior of an access context-less XHR receive loop.
     */
    testXhrLevelException: {
        test: [
            function(cmp){
                // set an invalid token to trigger an XHR-level exception event
                $A.clientService.setToken("invalid");

                cmp.find("trigger").get("e.press").fire();
                $A.test.addWaitForWithFailureMessage(true, function(){ return !!cmp.get("v.response");},
                    "response attribute is never set",
                    function() {
                        var action = cmp.get("v.response");
                        $A.test.assertEquals("ERROR", action.state, "Found unexpected state");

                        var msg = action.error[0].message;
                        var expectedMessage = "Received exception event from server";
                        $A.test.assertStartsWith(expectedMessage, msg, "Found unexpected error message");

                        // For client side error, different browsers may have different format of stacktrace.
                        // Only check stack exists.
                        // On IE, stack is added when error is thrown. For error in action, we create a new Error
                        // instance in AuraClientService.prototype.processErrors, so there is no stack for IE.
                        var stack = action.error[0].stack;
                        var browser = $A.get("$Browser");
                        if(!browser.isIE10 && !browser.isIE11) {
                            $A.test.assertTrue(stack.length > 0, "Unexpected stack: " + stack);
                        }
                    }
                );
            },
            function(cmp) {
                $A.test.assertTrue(cmp.get("v.invalidSessionReceived"), "aura:invalidSession event was not fired / received");
            }
        ]
    },

    //Because AuraHandledExceptions are treated differently
    checkHandledExceptionResponse : function(cmp, expectedState, expectedMessage, expectedStack){
        cmp.find("trigger").get("e.press").fire();
        $A.test.addWaitForWithFailureMessage(true, function(){ return !!cmp.get("v.response");},
            "response attribute is never set",
            function() {
                var action = cmp.get("v.response");
                var msg = action.error[0].message;
                var stack = action.error[0].stack;
                $A.test.assertEquals(expectedState, action.state, "Found unexpected state");

                if(expectedMessage){
                    $A.test.assertStartsWith(expectedMessage, msg, "Found unexpected error message");
                }

                if(expectedStack){
                    $A.test.assertTrue(stack.indexOf(expectedStack) > -1, "Unexpected stack: " + stack);
                }
            }
        );
    },

    checkUnHandledExceptionResponse : function(cmp, expectedState, expectedMessage, expectedStack){
        cmp.find("trigger").get("e.press").fire();
        $A.test.runAfterIf(
            function(){ return cmp.get("v.response"); },
            function(){
                var action = cmp.get("v.response");
                var msg = action.error[0].message;
                var stack = action.error[0].stack;
                $A.test.assertEquals(expectedState, action.state, "Unexpected state: ");
                if(expectedMessage){
                    $A.test.assertNotNull(
                            msg.match(/^An internal server error has occurred\nError ID:.*\n\norg\.auraframework\.throwable\.AuraExecutionException:/)
                            ||
                            msg.match(/^Unable to process your request\n\norg\.auraframework\.throwable\.AuraExecutionException:/)
                    );
                    $A.test.assertTrue(msg.indexOf(expectedMessage)!= -1,
                            "Expected error message not seen: Expected {"+expectedMessage+"} but saw {"+msg+"}");
                }
                if(expectedStack){
                    $A.test.assertTrue(stack.indexOf(expectedStack) === 0, "Unexpected stack: " + stack);
                } else {
                    $A.test.assertTrue(stack.indexOf("org.auraframework.throwable.AuraUnhandledException: ") === 0, "Unexpected stack: " + stack);
                }
            }
        );
    }
})
<|endoftext|>


<commit_before>import React from 'react';
import { render, fireEvent, screen } from '@testing-library/react';
import VisibleToggle from '../visible-toggle';

describe('VisibleToggle', () => {
  test('Indicates that a submission is public', () => {
    render(
      <VisibleToggle
        submission={{ is_public: true }}
        handleVisibleToggle={() => {}}
      />
    );
    expect(screen.getByLabelText('Toggle visibility')).toHaveClass('submissions__public-icon--visible');
  });

  test('Indicates that a submission is private', () => {
    render(
      <VisibleToggle
        submission={{ is_public: false }}
        handleVisibleToggle={() => {}}
      />
    );
    expect(screen.getByLabelText('Toggle visibility')).not.toHaveClass('submissions__public-icon--visible');
  });

  test('Toggles between public and private on click', () => {
    const submission = { is_public: true };
    const mockHandleVisibleToggle = jest.fn()
      .mockImplementation(() => submission.is_public = !submission.is_public)
    const { rerender } = render(
      <VisibleToggle
        submission={submission}
        handleVisibleToggle={mockHandleVisibleToggle}
      />
    );
    const component = screen.getByLabelText('Toggle visibility');

    fireEvent.click(screen.getByLabelText('Toggle visibility'));
    expect(component).toHaveClass('submissions__public-icon--visible');

    rerender(
      <VisibleToggle
        submission={submission}
        handleVisibleToggle={mockHandleVisibleToggle}
      />
    )
    expect(component).not.toHaveClass('submissions__public-icon--visible');
  });
});
<commit_msg>Add on click test<commit_after>import React from 'react';
import { render, fireEvent, screen } from '@testing-library/react';
import VisibleToggle from '../visible-toggle';

describe('VisibleToggle', () => {
  test('Indicates that a submission is public', () => {
    render(
      <VisibleToggle
        submission={{ is_public: true }}
        handleVisibleToggle={() => {}}
      />
    );
    expect(screen.getByLabelText('Toggle visibility')).toHaveClass('submissions__public-icon--visible');
  });

  test('Indicates that a submission is private', () => {
    render(
      <VisibleToggle
        submission={{ is_public: false }}
        handleVisibleToggle={() => {}}
      />
    );
    expect(screen.getByLabelText('Toggle visibility')).not.toHaveClass('submissions__public-icon--visible');
  });

  test('Toggles between public and private on click', () => {
    const submission = { is_public: true };
    const mockHandleVisibleToggle = jest.fn()
      .mockImplementation(() => submission.is_public = !submission.is_public)
    const { rerender } = render(
      <VisibleToggle
        submission={submission}
        handleVisibleToggle={mockHandleVisibleToggle}
      />
    );
    const component = screen.getByLabelText('Toggle visibility');

    fireEvent.click(component);
    expect(component).toHaveClass('submissions__public-icon--visible');

    rerender(
      <VisibleToggle
        submission={submission}
        handleVisibleToggle={mockHandleVisibleToggle}
      />
    )

    expect(component).not.toHaveClass('submissions__public-icon--visible');
  });
});
<|endoftext|>


<commit_before>define(["Tone/core/Tone", "Tone/type/Ticks"], function(Tone){

	/**
	 *  @class Tone.TransportEvent is an internal class used by (Tone.Transport)[Transport]
	 *         to schedule events. Do no invoke this class directly, it is
	 *         handled from within Tone.Transport.
	 *  @extends {Tone}
	 *  @param {Object} options
	 */
	Tone.TransportEvent = function(Transport, options){

		options = Tone.defaultArg(options, Tone.TransportEvent.defaults);
		Tone.call(this);

		/**
		 * Reference to the Transport that created it
		 * @type {Tone.Transport}
		 */
		this.Transport = Transport;

		/**
		 * The unique id of the event
		 * @type {Number}
		 */
		this.id = Tone.TransportEvent._eventId++;

		/**
		 * The time the event starts
		 * @type {Ticks}
		 */
		this.time = Tone.Ticks(options.time);

		/**
		 * The callback to invoke
		 * @type {Function}
		 */
		this.callback = options.callback;

		/**
		 * If the event should be removed after being created.
		 * @type {Boolean}
		 * @private
		 */
		this._once = options.once;
	};

	Tone.extend(Tone.TransportEvent);

	/**
	 * The defaults
	 * @static
	 * @type {Object}
	 */
	Tone.TransportEvent.defaults = {
		"once" : false,
		"callback" : Tone.noOp,
	};

	/**
	 * Current ID counter
	 * @private
	 * @static
	 * @type {Number}
	 */
	Tone.TransportEvent._eventId = 0;

	/**
	 * Invoke the callback even callback.
	 * @param  {Time} time  The AudioContext time in seconds of the event
	 */
	Tone.TransportEvent.prototype.invoke = function(time){
		if (this.callback){
			this.callback(time);
			if (this._once && this.Transport){
				this.Transport.clear(this.id);
			}
		}
	};

	/**
	 * Clean up
	 * @return {Tone.TransportEvent} this
	 */
	Tone.TransportEvent.prototype.dispose = function(){
		Tone.prototype.dispose.call(this);
		this.Transport = null;
		this.callback = null;
		return this;
	};

	return Tone.TransportEvent;
});
<commit_msg>clean up time value on dispose<commit_after>define(["Tone/core/Tone", "Tone/type/Ticks"], function(Tone){

	/**
	 *  @class Tone.TransportEvent is an internal class used by (Tone.Transport)[Transport]
	 *         to schedule events. Do no invoke this class directly, it is
	 *         handled from within Tone.Transport.
	 *  @extends {Tone}
	 *  @param {Object} options
	 */
	Tone.TransportEvent = function(Transport, options){

		options = Tone.defaultArg(options, Tone.TransportEvent.defaults);
		Tone.call(this);

		/**
		 * Reference to the Transport that created it
		 * @type {Tone.Transport}
		 */
		this.Transport = Transport;

		/**
		 * The unique id of the event
		 * @type {Number}
		 */
		this.id = Tone.TransportEvent._eventId++;

		/**
		 * The time the event starts
		 * @type {Ticks}
		 */
		this.time = Tone.Ticks(options.time);

		/**
		 * The callback to invoke
		 * @type {Function}
		 */
		this.callback = options.callback;

		/**
		 * If the event should be removed after being created.
		 * @type {Boolean}
		 * @private
		 */
		this._once = options.once;
	};

	Tone.extend(Tone.TransportEvent);

	/**
	 * The defaults
	 * @static
	 * @type {Object}
	 */
	Tone.TransportEvent.defaults = {
		"once" : false,
		"callback" : Tone.noOp,
	};

	/**
	 * Current ID counter
	 * @private
	 * @static
	 * @type {Number}
	 */
	Tone.TransportEvent._eventId = 0;

	/**
	 * Invoke the callback even callback.
	 * @param  {Time} time  The AudioContext time in seconds of the event
	 */
	Tone.TransportEvent.prototype.invoke = function(time){
		if (this.callback){
			this.callback(time);
			if (this._once && this.Transport){
				this.Transport.clear(this.id);
			}
		}
	};

	/**
	 * Clean up
	 * @return {Tone.TransportEvent} this
	 */
	Tone.TransportEvent.prototype.dispose = function(){
		Tone.prototype.dispose.call(this);
		this.Transport = null;
		this.callback = null;
		this.time = null;
		return this;
	};

	return Tone.TransportEvent;
});
<|endoftext|>


<commit_before>define(["Tone/core/Tone", "Tone/effect/Effect", "Tone/component/Split",
	"Tone/component/Merge", "Tone/component/CrossFade"],
function(Tone){

	"use strict";

	/**
	 *  @class Base class for Stereo effects. Provides effectSendL/R and effectReturnL/R.
	 *
	 *	@constructor
	 *	@extends {Tone}
	 */
	Tone.StereoEffect = function(){

		//get the defaults
		var options = Tone.defaults(arguments, ["wet"], Tone.Effect);
		this.createInsOuts(1, 1);

		/**
		 *  the drywet knob to control the amount of effect
		 *  @type {Tone.CrossFade}
		 *  @private
		 */
		this._dryWet = new Tone.CrossFade(options.wet);

		/**
		 *  The wet control, i.e. how much of the effected
		 *  will pass through to the output.
		 *  @type {NormalRange}
		 *  @signal
		 */
		this.wet = this._dryWet.fade;

		/**
		 *  then split it
		 *  @type {Tone.Split}
		 *  @private
		 */
		this._split = new Tone.Split();

		/**
		 *  the effects send LEFT
		 *  @type {GainNode}
		 *  @private
		 */
		this.effectSendL = this._split.left;

		/**
		 *  the effects send RIGHT
		 *  @type {GainNode}
		 *  @private
		 */
		this.effectSendR = this._split.right;

		/**
		 *  the stereo effect merger
		 *  @type {Tone.Merge}
		 *  @private
		 */
		this._merge = new Tone.Merge();

		/**
		 *  the effect return LEFT
		 *  @type {GainNode}
		 *  @private
		 */
		this.effectReturnL = this._merge.left;

		/**
		 *  the effect return RIGHT
		 *  @type {GainNode}
		 *  @private
		 */
		this.effectReturnR = this._merge.right;

		//connections
		this.input.connect(this._split);
		//dry wet connections
		this.input.connect(this._dryWet, 0, 0);
		this._merge.connect(this._dryWet, 0, 1);
		this._dryWet.connect(this.output);
		this._readOnly(["wet"]);
	};

	Tone.extend(Tone.StereoEffect, Tone.Effect);

	/**
	 *  Clean up.
	 *  @returns {Tone.StereoEffect} this
	 */
	Tone.StereoEffect.prototype.dispose = function(){
		Tone.prototype.dispose.call(this);
		this._dryWet.dispose();
		this._dryWet = null;
		this._split.dispose();
		this._split = null;
		this._merge.dispose();
		this._merge = null;
		this.effectSendL = null;
		this.effectSendR = null;
		this.effectReturnL = null;
		this.effectReturnR = null;
		this._writable(["wet"]);
		this.wet = null;
		return this;
	};

	return Tone.StereoEffect;
});<commit_msg>updating extends jsdoc to reflect inheritance<commit_after>define(["Tone/core/Tone", "Tone/effect/Effect", "Tone/component/Split",
	"Tone/component/Merge", "Tone/component/CrossFade"],
function(Tone){

	"use strict";

	/**
	 *  @class Base class for Stereo effects. Provides effectSendL/R and effectReturnL/R.
	 *
	 *	@constructor
	 *	@extends {Tone.Effect}
	 */
	Tone.StereoEffect = function(){

		//get the defaults
		var options = Tone.defaults(arguments, ["wet"], Tone.Effect);
		this.createInsOuts(1, 1);

		/**
		 *  the drywet knob to control the amount of effect
		 *  @type {Tone.CrossFade}
		 *  @private
		 */
		this._dryWet = new Tone.CrossFade(options.wet);

		/**
		 *  The wet control, i.e. how much of the effected
		 *  will pass through to the output.
		 *  @type {NormalRange}
		 *  @signal
		 */
		this.wet = this._dryWet.fade;

		/**
		 *  then split it
		 *  @type {Tone.Split}
		 *  @private
		 */
		this._split = new Tone.Split();

		/**
		 *  the effects send LEFT
		 *  @type {GainNode}
		 *  @private
		 */
		this.effectSendL = this._split.left;

		/**
		 *  the effects send RIGHT
		 *  @type {GainNode}
		 *  @private
		 */
		this.effectSendR = this._split.right;

		/**
		 *  the stereo effect merger
		 *  @type {Tone.Merge}
		 *  @private
		 */
		this._merge = new Tone.Merge();

		/**
		 *  the effect return LEFT
		 *  @type {GainNode}
		 *  @private
		 */
		this.effectReturnL = this._merge.left;

		/**
		 *  the effect return RIGHT
		 *  @type {GainNode}
		 *  @private
		 */
		this.effectReturnR = this._merge.right;

		//connections
		this.input.connect(this._split);
		//dry wet connections
		this.input.connect(this._dryWet, 0, 0);
		this._merge.connect(this._dryWet, 0, 1);
		this._dryWet.connect(this.output);
		this._readOnly(["wet"]);
	};

	Tone.extend(Tone.StereoEffect, Tone.Effect);

	/**
	 *  Clean up.
	 *  @returns {Tone.StereoEffect} this
	 */
	Tone.StereoEffect.prototype.dispose = function(){
		Tone.prototype.dispose.call(this);
		this._dryWet.dispose();
		this._dryWet = null;
		this._split.dispose();
		this._split = null;
		this._merge.dispose();
		this._merge = null;
		this.effectSendL = null;
		this.effectSendR = null;
		this.effectReturnL = null;
		this.effectReturnR = null;
		this._writable(["wet"]);
		this.wet = null;
		return this;
	};

	return Tone.StereoEffect;
});
<|endoftext|>


<commit_before>define(["Tone/core/Tone", "Tone/shim/AudioContext"], function(Tone){

	if (Tone.supported){

		//fixes safari only bug which is still present in 11
		var ua = navigator.userAgent.toLowerCase();
		var isSafari = ua.indexOf("safari") !== -1 && ua.indexOf("chrome") === -1;
		if (isSafari){

			var WaveShaperNode = function(context){

				this._internalNode = this.input = this.output = context._native_createWaveShaper();

				this._curve = null;

				for (var prop in this._internalNode){
					defineProperty(this, this._internalNode, prop);
				}
			};

			Object.defineProperty(WaveShaperNode.prototype, "curve", {
				get : function(){
					return this._curve;
				},
				set : function(curve){
					this._curve = curve;
					var array = new Float32Array(curve.length+1);
					array.set(curve, 1);
					array[0] = curve[0];
					this._internalNode.curve = array;
				}
			});

			function defineProperty(self, context, prop){
				if (Tone.isUndef(self[prop])){
					Object.defineProperty(self, prop, {
						get : function(){
							if (typeof context[prop] === "function"){
								return context[prop].bind(context);
							} else {
								return context[prop];
							}
						},
						set : function(val){
							context[prop] = val;
						}
					});
				}
			}

			AudioContext.prototype._native_createWaveShaper = AudioContext.prototype.createWaveShaper;
			AudioContext.prototype.createWaveShaper = function(){
				return new WaveShaperNode(this);
			};
		}
	}

});
<commit_msg>linting<commit_after>define(["Tone/core/Tone", "Tone/shim/AudioContext"], function(Tone){

	if (Tone.supported){

		//fixes safari only bug which is still present in 11
		var ua = navigator.userAgent.toLowerCase();
		var isSafari = ua.indexOf("safari") !== -1 && ua.indexOf("chrome") === -1;
		if (isSafari){

			var WaveShaperNode = function(context){

				this._internalNode = this.input = this.output = context._native_createWaveShaper();

				this._curve = null;

				for (var prop in this._internalNode){
					this._defineProperty(this._internalNode, prop);
				}
			};

			Object.defineProperty(WaveShaperNode.prototype, "curve", {
				get : function(){
					return this._curve;
				},
				set : function(curve){
					this._curve = curve;
					var array = new Float32Array(curve.length+1);
					array.set(curve, 1);
					array[0] = curve[0];
					this._internalNode.curve = array;
				}
			});

			WaveShaperNode.prototype._defineProperty = function(context, prop){
				if (Tone.isUndef(this[prop])){
					Object.defineProperty(this, prop, {
						get : function(){
							if (typeof context[prop] === "function"){
								return context[prop].bind(context);
							} else {
								return context[prop];
							}
						},
						set : function(val){
							context[prop] = val;
						}
					});
				}
			}

			AudioContext.prototype._native_createWaveShaper = AudioContext.prototype.createWaveShaper;
			AudioContext.prototype.createWaveShaper = function(){
				return new WaveShaperNode(this);
			};
		}
	}

});
<|endoftext|>