<commit_before>'use strict';

/**
 * The processor handling an entire result.
 */

const _ = require('lodash');
const cip = require('../../services/cip');
const config = require('../../../shared/config');
const es = require('../../lib/services/elasticsearch');
const Q = require('q');

function AssetIndexingError(catalogAlias, assetId, innerError) {
  this.catalogAlias = catalogAlias;
  this.assetId = assetId;
  this.innerError = innerError;
}

const processAsset = require('./asset');

function saveChangesToCIP(catalogAlias, items) {
  return cip.request(`/metadata/setfieldvalues/${catalogAlias}`, {}, {
    items
  });
}

async function getResultPage(query, catalog, index, pageSize) {
  const operation = `/metadata/search/${catalog}/${config.cip.client.constants.layoutAlias}`;

  const options = {
    querystring: query,
    startindex: index,
    maxreturned: pageSize
  };

  if(config.cip.indexing.additionalFields) {
    options.field = config.cip.indexing.additionalFields;
  }
  const response = await requestWithRetries(operation, options, 4, 2);

  if (!response || !response.body || typeof(response.body.items) === 'undefined') {
    console.error('Unexpected response:', response);
    throw new Error('The request for field values returned an empty result.');
  } else {
    return response.body.items;
  }
}

const errorsToRetry = ['ECONNREFUSED', 'ESOCKETTIMEDOUT', 'ECONNRESET'];

async function requestWithRetries(operation, options, retries, backoff) {
  try {
    return await cip.request(operation, options);
  } catch(error) {
    if(retries <= 1) {
      throw error;
    }
    if(!errorsToRetry.includes(error.code)) {
      throw error;
    }

    await new Promise((resolve) => setTimeout(resolve, backoff*1000));
    return requestWithRetries(operation, options, retries - 1, backoff*backoff);
  }
}
/**
 * Process a specific result page, with assets.
 */
function processResultPage(totalcount, context, seriesLookup, mode, pageIndex) {
  const { query, collection, pageSize } = context

  const totalPages = Math.ceil(totalcount / pageSize);
  const progress = '[' + (pageIndex + 1) + '/' + totalPages + ']';
  console.log(progress + ' Queuing page');

  return getResultPage(query, collection, pageIndex * pageSize, pageSize)
  .then(assets => {
    console.log(progress + ' Received metadata');
    // Perform a processing of all the assets on the page
    const assetPromises = assets
    .filter((asset) => {
      if(!asset.id) {
        console.warn("Skipping asset that does not look right (has no ID)", asset);
        return false;
      }
      return true;
    })
    .map(asset => {
      const assetSeries = parseAssetSeries(asset);
      // Clone the context for every asset
      const clonedContext = _.cloneDeep(context);
      // Keep an object of requested changes to the asset in Cumulus
      clonedContext.changes = {};
      // Define a method to persist values in Cumulus
      clonedContext.persist = (name, value) => {
        // Determine the fields UUID in Cumulus
        if(!config.types || !config.types.asset || !config.types.asset.fields) {
          throw new Error('Cannot get field: Missing config.types.asset.fields');
        }
        const field = config.types.asset.fields.find(field => field.short === name);
        if(!field) {
          throw new Error('Field is missing in config.types.asset.fields: ' + name);
        }
        // Update the value in the changes object
        clonedContext.changes[field.cumulusKey] = value;
      };
      // Process each asset
      return processAsset(asset, assetSeries, clonedContext)
      .catch((err) => {
        const msg = 'ERROR processing ' + collection + '-' + asset.id;
        console.error(msg + (err.message && ': ' + err.message));
        return new AssetIndexingError(collection, asset.id, err);
      });
    });

    // Once all asset promises resolves:
    // 1. changes are saved to Cumulus
    // 2. metedata is indexed in elasticsearch
    // in two bulk calls

    return Q.all(assetPromises)
    .then((assets) => {
      return {
        errors: assets.filter(a => a instanceof AssetIndexingError),
        assets: assets.filter(a => !(a instanceof AssetIndexingError)),
      };
    })
    .then(({ assets, errors }) => {
      if(mode == 'all') {
        assets.forEach(({ assetSeries }) => assetSeries.forEach((series) => {
          if(!seriesLookup[series._id]) {
            seriesLookup[series._id] = series;
          }
        }));
        return {assets, errors, seriesLookup};
      }

      if(mode == 'single' || mode == 'catalog' || mode == 'recent') {
        const assetSeries = _.uniq(
          assets
            .map((asset) => asset.assetSeries)
            .reduce((a, b) => a.concat(b), []),
          '_id'
        );

        const seriesIds = assetSeries
          .map((series) => series._id);

        const assetIds = _.uniq(assets.map(({metadata}) => `${metadata.collection}-${metadata.id}`));

        return es.search({
          index: config.es.seriesIndex,
          body: {
            query: {
              bool: {
                should: [
                  {
                    terms: {
                      _id: seriesIds
                    }
                  },
                  ...assetIds.map((assetId) => ({
                    term: {assets: assetId},
                  }))
                ]
              }
            }
          }
        })
          .then(({body: response}) => {
            const seriesLookup = {};
            response.hits.hits.forEach((elasticSearchSeries) => {
              const series = {
                _id: elasticSearchSeries._id,
                ...elasticSearchSeries._source
              };

              assets.forEach(({metadata}) => {
                const assetIndex = series.assets.findIndex((assetId) => assetId === `${metadata.collection}-${metadata.id}`);
                if(assetIndex !== -1) {
                  series.assets.splice(assetIndex, 1);
                }

                const previewAssetIndex = series.previewAssets
                  .findIndex((previewAsset) => `${previewAsset.collection}-${previewAsset.id}` === `${metadata.collection}-${metadata.id}`);
                if(previewAssetIndex !== -1) {
                  series.previewAssets.splice(previewAssetIndex, 1);
                }
              });

              seriesLookup[series._id] = series;
            });

            assetSeries.forEach((series) => {
              if(seriesLookup[series._id]) {
                // If the series already exists in es, prefer data from cumulus (update it)
                const existingSeries = seriesLookup[series._id];
                Object.keys(series)
                  // Get all keys on series that are not `assets` and `previewAssets`
                  .filter((key) => ![ 'assets', 'previewAssets' ].includes(key))
                  // And update them in the existing series
                  .forEach((key) => existingSeries[key] = series[key]);
              }
              else {
                // Otherwise, add the series
                seriesLookup[series._id] = series;
              }
            });

            return {assets, errors, seriesLookup};
          });
      }
    })
    .then(({assets, errors, seriesLookup}) => {
      assets.forEach(({ metadata, assetSeries }) => {
        // Make series ids attached to assets so we can look it up
        metadata.series_ids = assetSeries.map((series) => series._id);

        // Set up series
        assetSeries.forEach((as) => {
          const series = seriesLookup[as._id];
          if(typeof series.assets == "undefined") {
            series.assets = [];
          }
          if(!series.assets.includes(metadata.collection + '-' + metadata.id)) {
            series.assets.push(metadata.collection + '-' + metadata.id);
          }
          if(typeof series.previewAssets == "undefined") {
            series.previewAssets = [];
          }

          if(series.previewAssets.length < 3) {
            series.previewAssets.push(metadata);
          }
          else {
            // We already have 3 preview assets. Now, we determine if we
            // should randomly shuffle this asset into the list of preview
            // assets. First, we get a random index from [0;assets.length - 1]
            const indexToShuffleInto = Math.floor(Math.random() * series.assets.length);

            // If the calculated index is [0,1,2] we place this asset on that
            // position -- otherwise we missed the list of preview assets, and
            // leave it out.
            if(indexToShuffleInto <= 2) {
              series.previewAssets[indexToShuffleInto] = metadata;
            }
          }
        })
      });

      const assetSeries = Object.values(seriesLookup);

      // Save the changes to the CIP
      const changes = assets
        .filter(({context}) => {
          // Filter out assets without changes
          return context.changes && Object.keys(context.changes).length > 0;
        })
        .map(({metadata, context}) => {
          return Object.assign({
            id: metadata.id
          }, context.changes);
        });
      // If changes to the CIP assets is needed, save them
      if(changes.length > 0) {
        // TODO: Consider the response from the CIP - as a change might fail.
        return saveChangesToCIP(context.collection, changes)
        .then(response => {
          if(response.statusCode === 200) {
            console.log(progress + ' Updated', changes.length, 'assets in Cumulus');
            return { assets, assetSeries, errors };
          } else {
            throw new Error('Error updating assets in Cumulus');
          }
        });
      } else {
        return { assets, assetSeries, errors };
      }
    }).then(({assets, assetSeries, errors}) => {
      // Create a list of items for a bulk call, for assets that are not errors.
      const items = [];
      assets
        .filter(asset => !(asset instanceof AssetIndexingError))
        .forEach(({metadata, context}) => {
          items.push({
            'index' : {
              '_index': config.es.assetIndex,
              '_id': metadata.collection + '-' + metadata.id
            }
          });
          items.push(metadata);
        });

      assetSeries.forEach((series) => {
        if(series.assets.length > 0) {
          items.push({
            'index' : {
              '_index': config.es.seriesIndex,
              '_id': series._id
            }
          });

          items.push({
            ...series,
            _id: undefined
          });
        } else {
          items.push({
            'delete' : {
              '_index': config.es.seriesIndex,
              '_id': series._id
            }
          });
        }
      });

      // Perform the bulk operation
      return es.bulk({
        body: items
      }).then(({body: response}) => {
        const indexedIds = [];
        // Go through the items in the response and replace failures with errors
        // in the assets
        response.items.forEach(item => {
          if(item.index.status >= 200 && item.index.status < 300) {
            indexedIds.push(item.index._id);
          } else {
            // TODO: Consider using the AssetIndexingError instead
            errors.push(new Error('Failed index ' + item.index._id));
          }
        });
        console.log(progress + ' Indexed', indexedIds.length, 'assets and series in ES');
        // Return the result
        return { errors, indexedIds };
      });
    });
  });
}

function parseAssetSeries(asset) {
  const assetSeries = getAssetSeries(asset);
  return assetSeries.map(assetSeries => formatSeries(assetSeries));
}

function getAssetSeries(asset) {
  let assetSeries = [];
  if(asset["{252492cb-6efd-45f3-9bb5-d17824784d30}"]) {
    assetSeries.push({
      url: asset["{44c7a3b9-8ff2-4e58-8120-e0e64ba263ea}"],
      title: asset["{252492cb-6efd-45f3-9bb5-d17824784d30}"],
      description: asset["{095e1a43-d628-4944-8e8f-64d3db8c5df5}"],
      tags: asset["{bef11691-f8a1-49dd-8fbf-45c7d359764f}"],
      dateFrom: asset["{1a29ca44-f655-49c8-b015-223b51f2e4c5}"],
      dateTo: asset["{a22b11c8-5c33-4761-96bc-52467080468a}"]
    });
  }
  if(asset["{665faabb-8f6e-41ef-b300-9433eb5eae6f}"]) {
    assetSeries.push({
      url: asset["{ba67b9d6-8459-430c-9819-e660a294f7e7}"],
      title: asset["{665faabb-8f6e-41ef-b300-9433eb5eae6f}"],
      description: asset["{956b74f0-9cc7-4525-8fda-40e3df807986}"],
      tags: asset["{be219ed7-c0c3-4b38-9991-4dab67dc084f}"],
      dateFrom: asset["{03879423-335a-4772-829c-b31b1d768270}"],
      dateTo: asset["{fe893328-4d92-4c8f-a847-c42cc1f6fd8d}"]
    });
  }
  return assetSeries;
}

function formatSeries(assetSeries) {
  let tags = [];
  if(assetSeries.tags) {
    tags = assetSeries.tags
      .split(",")
      .map((tag) => tag.trim().toLowerCase())
      .filter((tag) => tag);
  }

  const formattedSeries = {
    url: assetSeries.url,
    _id: "series/" + assetSeries.url,
    title: assetSeries.title,
    description: assetSeries.description,
    tags,
  }
  if(assetSeries.dateFrom.year > assetSeries.dateTo.year) {
    formattedSeries.dateFrom = formatDate(assetSeries.dateTo);
    formattedSeries.dateTo = formatDate(assetSeries.dateFrom);
  } else {
    formattedSeries.dateFrom = formatDate(assetSeries.dateFrom);
    formattedSeries.dateTo = formatDate(assetSeries.dateTo);
  }
  return formattedSeries;
}

function formatDate(date) {
  return {
    ...date,
    timestamp: getTimestamp(date)
  };
}

function getTimestamp(date) {
  const month = zeroPad(date.month || 1);
  const day = zeroPad(date.day || 1);
  return `${date.year}-${month}-${day}`;
}

function zeroPad(number) {
  const stringifiedNumber = number.toString();
  if(stringifiedNumber.length >= 2) {
    return stringifiedNumber;
  }
  return stringifiedNumber.padStart(2,"0");
}

function processResultPages(totalcount, context, seriesLookup, mode) {
  // Build up a list of parameters for all the pages in the entire result
  const pageIndecies = [];
  for(let p = context.offset; p * context.pageSize < totalcount; p++) {
    pageIndecies.push(p);
  }
  // Return a promise of process result pages (evaluated one after another)
  return pageIndecies.reduce((idsAndErrors, pageIndex) => {
    return Q.when(idsAndErrors)
    .then(({allIndexedIds, allErrors}) => {
      return processResultPage(totalcount, context, seriesLookup, mode, pageIndex)
      .then(({indexedIds, errors}) => {
        return {
          allIndexedIds: allIndexedIds.concat(indexedIds),
          allErrors: allErrors.concat(errors)
        };
      });
    });
  }, new Q({
    allIndexedIds: [],
    allErrors: []
  }))
  .then(({allIndexedIds, allErrors}) => {
    return {
      indexedIds: allIndexedIds,
      errors: allErrors
    };
  });
}

function processResult(context, seriesLookup, mode, query, totalcount) {
  console.log('Processing a result of ' + totalcount + ' assets');
  return processResultPages(totalcount, context, seriesLookup, mode);
}

module.exports = processResult;
<commit_msg>More useful errors (and immediate) from indexing<commit_after>'use strict';

/**
 * The processor handling an entire result.
 */

const _ = require('lodash');
const cip = require('../../services/cip');
const config = require('../../../shared/config');
const es = require('../../lib/services/elasticsearch');
const Q = require('q');

function AssetIndexingError(catalogAlias, assetId, innerError) {
  this.catalogAlias = catalogAlias;
  this.assetId = assetId;
  this.innerError = innerError;
}

const processAsset = require('./asset');

function saveChangesToCIP(catalogAlias, items) {
  return cip.request(`/metadata/setfieldvalues/${catalogAlias}`, {}, {
    items
  });
}

async function getResultPage(query, catalog, index, pageSize) {
  const operation = `/metadata/search/${catalog}/${config.cip.client.constants.layoutAlias}`;

  const options = {
    querystring: query,
    startindex: index,
    maxreturned: pageSize
  };

  if(config.cip.indexing.additionalFields) {
    options.field = config.cip.indexing.additionalFields;
  }
  const response = await requestWithRetries(operation, options, 4, 2);

  if (!response || !response.body || typeof(response.body.items) === 'undefined') {
    console.error('Unexpected response:', response);
    throw new Error('The request for field values returned an empty result.');
  } else {
    return response.body.items;
  }
}

const errorsToRetry = ['ECONNREFUSED', 'ESOCKETTIMEDOUT', 'ECONNRESET'];

async function requestWithRetries(operation, options, retries, backoff) {
  try {
    return await cip.request(operation, options);
  } catch(error) {
    if(retries <= 1) {
      throw error;
    }
    if(!errorsToRetry.includes(error.code)) {
      throw error;
    }

    await new Promise((resolve) => setTimeout(resolve, backoff*1000));
    return requestWithRetries(operation, options, retries - 1, backoff*backoff);
  }
}
/**
 * Process a specific result page, with assets.
 */
function processResultPage(totalcount, context, seriesLookup, mode, pageIndex) {
  const { query, collection, pageSize } = context

  const totalPages = Math.ceil(totalcount / pageSize);
  const progress = '[' + (pageIndex + 1) + '/' + totalPages + ']';
  console.log(progress + ' Queuing page');

  return getResultPage(query, collection, pageIndex * pageSize, pageSize)
  .then(assets => {
    console.log(progress + ' Received metadata');
    // Perform a processing of all the assets on the page
    const assetPromises = assets
    .filter((asset) => {
      if(!asset.id) {
        console.warn("Skipping asset that does not look right (has no ID)", asset);
        return false;
      }
      return true;
    })
    .map(asset => {
      const assetSeries = parseAssetSeries(asset);
      // Clone the context for every asset
      const clonedContext = _.cloneDeep(context);
      // Keep an object of requested changes to the asset in Cumulus
      clonedContext.changes = {};
      // Define a method to persist values in Cumulus
      clonedContext.persist = (name, value) => {
        // Determine the fields UUID in Cumulus
        if(!config.types || !config.types.asset || !config.types.asset.fields) {
          throw new Error('Cannot get field: Missing config.types.asset.fields');
        }
        const field = config.types.asset.fields.find(field => field.short === name);
        if(!field) {
          throw new Error('Field is missing in config.types.asset.fields: ' + name);
        }
        // Update the value in the changes object
        clonedContext.changes[field.cumulusKey] = value;
      };
      // Process each asset
      return processAsset(asset, assetSeries, clonedContext)
      .catch((err) => {
        const msg = 'ERROR processing ' + collection + '-' + asset.id;
        console.error(msg + (err.message && ': ' + err.message));
        return new AssetIndexingError(collection, asset.id, err);
      });
    });

    // Once all asset promises resolves:
    // 1. changes are saved to Cumulus
    // 2. metedata is indexed in elasticsearch
    // in two bulk calls

    return Q.all(assetPromises)
    .then((assets) => {
      return {
        errors: assets.filter(a => a instanceof AssetIndexingError),
        assets: assets.filter(a => !(a instanceof AssetIndexingError)),
      };
    })
    .then(({ assets, errors }) => {
      if(mode == 'all') {
        assets.forEach(({ assetSeries }) => assetSeries.forEach((series) => {
          if(!seriesLookup[series._id]) {
            seriesLookup[series._id] = series;
          }
        }));
        return {assets, errors, seriesLookup};
      }

      if(mode == 'single' || mode == 'catalog' || mode == 'recent') {
        const assetSeries = _.uniq(
          assets
            .map((asset) => asset.assetSeries)
            .reduce((a, b) => a.concat(b), []),
          '_id'
        );

        const seriesIds = assetSeries
          .map((series) => series._id);

        const assetIds = _.uniq(assets.map(({metadata}) => `${metadata.collection}-${metadata.id}`));

        return es.search({
          index: config.es.seriesIndex,
          body: {
            query: {
              bool: {
                should: [
                  {
                    terms: {
                      _id: seriesIds
                    }
                  },
                  ...assetIds.map((assetId) => ({
                    term: {assets: assetId},
                  }))
                ]
              }
            }
          }
        })
          .then(({body: response}) => {
            const seriesLookup = {};
            response.hits.hits.forEach((elasticSearchSeries) => {
              const series = {
                _id: elasticSearchSeries._id,
                ...elasticSearchSeries._source
              };

              assets.forEach(({metadata}) => {
                const assetIndex = series.assets.findIndex((assetId) => assetId === `${metadata.collection}-${metadata.id}`);
                if(assetIndex !== -1) {
                  series.assets.splice(assetIndex, 1);
                }

                const previewAssetIndex = series.previewAssets
                  .findIndex((previewAsset) => `${previewAsset.collection}-${previewAsset.id}` === `${metadata.collection}-${metadata.id}`);
                if(previewAssetIndex !== -1) {
                  series.previewAssets.splice(previewAssetIndex, 1);
                }
              });

              seriesLookup[series._id] = series;
            });

            assetSeries.forEach((series) => {
              if(seriesLookup[series._id]) {
                // If the series already exists in es, prefer data from cumulus (update it)
                const existingSeries = seriesLookup[series._id];
                Object.keys(series)
                  // Get all keys on series that are not `assets` and `previewAssets`
                  .filter((key) => ![ 'assets', 'previewAssets' ].includes(key))
                  // And update them in the existing series
                  .forEach((key) => existingSeries[key] = series[key]);
              }
              else {
                // Otherwise, add the series
                seriesLookup[series._id] = series;
              }
            });

            return {assets, errors, seriesLookup};
          });
      }
    })
    .then(({assets, errors, seriesLookup}) => {
      assets.forEach(({ metadata, assetSeries }) => {
        // Make series ids attached to assets so we can look it up
        metadata.series_ids = assetSeries.map((series) => series._id);

        // Set up series
        assetSeries.forEach((as) => {
          const series = seriesLookup[as._id];
          if(typeof series.assets == "undefined") {
            series.assets = [];
          }
          if(!series.assets.includes(metadata.collection + '-' + metadata.id)) {
            series.assets.push(metadata.collection + '-' + metadata.id);
          }
          if(typeof series.previewAssets == "undefined") {
            series.previewAssets = [];
          }

          if(series.previewAssets.length < 3) {
            series.previewAssets.push(metadata);
          }
          else {
            // We already have 3 preview assets. Now, we determine if we
            // should randomly shuffle this asset into the list of preview
            // assets. First, we get a random index from [0;assets.length - 1]
            const indexToShuffleInto = Math.floor(Math.random() * series.assets.length);

            // If the calculated index is [0,1,2] we place this asset on that
            // position -- otherwise we missed the list of preview assets, and
            // leave it out.
            if(indexToShuffleInto <= 2) {
              series.previewAssets[indexToShuffleInto] = metadata;
            }
          }
        })
      });

      const assetSeries = Object.values(seriesLookup);

      // Save the changes to the CIP
      const changes = assets
        .filter(({context}) => {
          // Filter out assets without changes
          return context.changes && Object.keys(context.changes).length > 0;
        })
        .map(({metadata, context}) => {
          return Object.assign({
            id: metadata.id
          }, context.changes);
        });
      // If changes to the CIP assets is needed, save them
      if(changes.length > 0) {
        // TODO: Consider the response from the CIP - as a change might fail.
        return saveChangesToCIP(context.collection, changes)
        .then(response => {
          if(response.statusCode === 200) {
            console.log(progress + ' Updated', changes.length, 'assets in Cumulus');
            return { assets, assetSeries, errors };
          } else {
            throw new Error('Error updating assets in Cumulus');
          }
        });
      } else {
        return { assets, assetSeries, errors };
      }
    }).then(({assets, assetSeries, errors}) => {
      // Create a list of items for a bulk call, for assets that are not errors.
      const items = [];
      assets
        .filter(asset => !(asset instanceof AssetIndexingError))
        .forEach(({metadata, context}) => {
          items.push({
            'index' : {
              '_index': config.es.assetIndex,
              '_id': metadata.collection + '-' + metadata.id
            }
          });
          items.push(metadata);
        });

      assetSeries.forEach((series) => {
        if(series.assets.length > 0) {
          items.push({
            'index' : {
              '_index': config.es.seriesIndex,
              '_id': series._id
            }
          });

          items.push({
            ...series,
            _id: undefined
          });
        } else {
          items.push({
            'delete' : {
              '_index': config.es.seriesIndex,
              '_id': series._id
            }
          });
        }
      });

      // Perform the bulk operation
      return es.bulk({
        body: items
      }).then(({body: response}) => {
        const indexedIds = [];
        // Go through the items in the response and replace failures with errors
        // in the assets
        response.items.forEach(item => {
          if(item.index.status >= 200 && item.index.status < 300) {
            indexedIds.push(item.index._id);
          } else {
            // TODO: Consider using the AssetIndexingError instead
            errors.push({
              trace: new Error('Failed index ' + item.index._id),
              item,
            });
          }
        });
        console.log(progress + ' Indexed', indexedIds.length, 'assets and series in ES');
        console.dir(errors, {depth: 10});
        // Return the result
        return { errors, indexedIds };
      });
    });
  });
}

function parseAssetSeries(asset) {
  const assetSeries = getAssetSeries(asset);
  return assetSeries.map(assetSeries => formatSeries(assetSeries));
}

function getAssetSeries(asset) {
  let assetSeries = [];
  if(asset["{252492cb-6efd-45f3-9bb5-d17824784d30}"]) {
    assetSeries.push({
      url: asset["{44c7a3b9-8ff2-4e58-8120-e0e64ba263ea}"],
      title: asset["{252492cb-6efd-45f3-9bb5-d17824784d30}"],
      description: asset["{095e1a43-d628-4944-8e8f-64d3db8c5df5}"],
      tags: asset["{bef11691-f8a1-49dd-8fbf-45c7d359764f}"],
      dateFrom: asset["{1a29ca44-f655-49c8-b015-223b51f2e4c5}"],
      dateTo: asset["{a22b11c8-5c33-4761-96bc-52467080468a}"]
    });
  }
  if(asset["{665faabb-8f6e-41ef-b300-9433eb5eae6f}"]) {
    assetSeries.push({
      url: asset["{ba67b9d6-8459-430c-9819-e660a294f7e7}"],
      title: asset["{665faabb-8f6e-41ef-b300-9433eb5eae6f}"],
      description: asset["{956b74f0-9cc7-4525-8fda-40e3df807986}"],
      tags: asset["{be219ed7-c0c3-4b38-9991-4dab67dc084f}"],
      dateFrom: asset["{03879423-335a-4772-829c-b31b1d768270}"],
      dateTo: asset["{fe893328-4d92-4c8f-a847-c42cc1f6fd8d}"]
    });
  }
  return assetSeries;
}

function formatSeries(assetSeries) {
  let tags = [];
  if(assetSeries.tags) {
    tags = assetSeries.tags
      .split(",")
      .map((tag) => tag.trim().toLowerCase())
      .filter((tag) => tag);
  }

  const formattedSeries = {
    url: assetSeries.url,
    _id: "series/" + assetSeries.url,
    title: assetSeries.title,
    description: assetSeries.description,
    tags,
  }
  if(assetSeries.dateFrom.year > assetSeries.dateTo.year) {
    formattedSeries.dateFrom = formatDate(assetSeries.dateTo);
    formattedSeries.dateTo = formatDate(assetSeries.dateFrom);
  } else {
    formattedSeries.dateFrom = formatDate(assetSeries.dateFrom);
    formattedSeries.dateTo = formatDate(assetSeries.dateTo);
  }
  return formattedSeries;
}

function formatDate(date) {
  return {
    ...date,
    timestamp: getTimestamp(date)
  };
}

function getTimestamp(date) {
  const month = zeroPad(date.month || 1);
  const day = zeroPad(date.day || 1);
  return `${date.year}-${month}-${day}`;
}

function zeroPad(number) {
  const stringifiedNumber = number.toString();
  if(stringifiedNumber.length >= 2) {
    return stringifiedNumber;
  }
  return stringifiedNumber.padStart(2,"0");
}

function processResultPages(totalcount, context, seriesLookup, mode) {
  // Build up a list of parameters for all the pages in the entire result
  const pageIndecies = [];
  for(let p = context.offset; p * context.pageSize < totalcount; p++) {
    pageIndecies.push(p);
  }
  // Return a promise of process result pages (evaluated one after another)
  return pageIndecies.reduce((idsAndErrors, pageIndex) => {
    return Q.when(idsAndErrors)
    .then(({allIndexedIds, allErrors}) => {
      return processResultPage(totalcount, context, seriesLookup, mode, pageIndex)
      .then(({indexedIds, errors}) => {
        return {
          allIndexedIds: allIndexedIds.concat(indexedIds),
          allErrors: allErrors.concat(errors)
        };
      });
    });
  }, new Q({
    allIndexedIds: [],
    allErrors: []
  }))
  .then(({allIndexedIds, allErrors}) => {
    return {
      indexedIds: allIndexedIds,
      errors: allErrors
    };
  });
}

function processResult(context, seriesLookup, mode, query, totalcount) {
  console.log('Processing a result of ' + totalcount + ' assets');
  return processResultPages(totalcount, context, seriesLookup, mode);
}

module.exports = processResult;
<|endoftext|>


<commit_before>import React, { Component } from 'react';
import { View, Text, ScrollView, TouchableOpacity } from 'react-native';
import { Actions } from 'react-native-router-flux';
import Icon from 'react-native-vector-icons/MaterialIcons';
import axios from 'axios';
import {Item} from './';
import Costper from './Costper';
import ContainerSection from '../ContainerSection';
import LoginForm from '../LoginForm';

class ItemsList extends Component {
  static renderRightButton = ({ iconName }) => {
    return (
      <View>
        <TouchableOpacity onPress={() => Actions.addItem()}>
          <Icon size={20} style={{ color: 'black' }} name={iconName} />
        </TouchableOpacity>
      </View>
    );
  }

  // const addItemIcon=(<Icon name="add-circle" size={30} color="FFFFFF"/>)

  constructor(props) {
    super(props);
    this.state = { data: [],
                   name: '',
                   price: '',
                   img_url: '',
                   star: '',
                   user_id: this.props.userId,
                   category_id: ''
                 };
  }

  componentWillMount() {
    var myItem = this;
    axios.get('http://localhost:3000/users/'+this.state.user_id+'/items')
      .then(function(response) {
        myItem.setState({data: response.data})
      })
      .catch(function(error) {
        console.log(error)
      });
    }

    render() {
      console.log('***************');
      console.log('Props are:' + this.state.user_id);
      return (
      <View style={styles.container}>
        <ScrollView>
            <View style={styles.contentcolumns} >
            {this.state.data.map((item) => {
              return(
                <View style={styles.rows}>
                  <Item key={item.item.id}
                    name={item.item.name}
                    price={item.item.price}
                    img_url={item.item.img_url}
                    star={item.item.star}
                    user_id={item.item.user_id}
                    category_id={item.item.category_id}
                  />
                  <Costper key={item.costper.id}
                        costper = {item.costper.costper}
                        item_id = {item.costper.item_id}
                  />
                </View>
              )})}
          </View>
      </ScrollView>
        <View style={styles.footerStyle}>

          <TouchableOpacity onPress={() => null}>
            <Icon size={40} style={{ color: 'white' }} name="home" />
          </TouchableOpacity>

          <TouchableOpacity onPress={() => Actions.addItem()}>
            <Icon size={50} style={{ color: 'white' }} name="add-circle" />
          </TouchableOpacity>

          <TouchableOpacity onPress={() => null}>
            <Icon size={40} style={{ color: 'white' }} name="exit-to-app" />
          </TouchableOpacity>

        </View>
      </View>
      );
    }
}

const styles = ({
  container: {
    flex: 1,
  },
  rows: {
    flexDirection: 'row'
  },
  contentcolumns: {
    flex: 1,
    flexDirection: 'column'
  },
  footerStyle: {
    height: 56,
    position: 'relative',
    backgroundColor: "#16795B",
    flexGrow: 1,
    flexDirection: 'row'
  }

});

export { ItemsList };
<commit_msg>Fix icon spacing.<commit_after>import React, { Component } from 'react';
import { View, Text, ScrollView, TouchableOpacity } from 'react-native';
import { Actions } from 'react-native-router-flux';
import Icon from 'react-native-vector-icons/MaterialIcons';
import axios from 'axios';
import {Item} from './';
import Costper from './Costper';
import ContainerSection from '../ContainerSection';
import LoginForm from '../LoginForm';

class ItemsList extends Component {
  static renderRightButton = ({ iconName }) => {
    return (
      <View>
        <TouchableOpacity onPress={() => Actions.addItem()}>
          <Icon size={20} style={{ color: 'black' }} name={iconName} />
        </TouchableOpacity>
      </View>
    );
  }

  // const addItemIcon=(<Icon name="add-circle" size={30} color="FFFFFF"/>)

  constructor(props) {
    super(props);
    this.state = { data: [],
                   name: '',
                   price: '',
                   img_url: '',
                   star: '',
                   user_id: this.props.userId,
                   category_id: ''
                 };
  }

  componentWillMount() {
    var myItem = this;
    axios.get('http://localhost:3000/users/'+this.state.user_id+'/items')
      .then(function(response) {
        myItem.setState({data: response.data})
      })
      .catch(function(error) {
        console.log(error)
      });
    }

    render() {
      console.log('***************');
      console.log('Props are:' + this.state.user_id);
      return (
      <View style={styles.container}>
        <ScrollView>
            <View style={styles.contentcolumns} >
            {this.state.data.map((item) => {
              return(
                <View style={styles.rows}>
                  <Item key={item.item.id}
                    name={item.item.name}
                    price={item.item.price}
                    img_url={item.item.img_url}
                    star={item.item.star}
                    user_id={item.item.user_id}
                    category_id={item.item.category_id}
                  />
                  <Costper key={item.costper.id}
                        costper = {item.costper.costper}
                        item_id = {item.costper.item_id}
                  />
                </View>
              )})}
          </View>
      </ScrollView>
        <View style={styles.footerStyle}>

            <TouchableOpacity onPress={() => null}>
              <Icon size={40} style={{ color: 'white' }} name="home" />
            </TouchableOpacity>

          <TouchableOpacity onPress={() => Actions.addItem()}>
            <Icon size={50} style={{ color: 'white' }} name="add-circle" />
          </TouchableOpacity>

          <TouchableOpacity onPress={() => null}>
            <Icon size={40} style={{ color: 'white' }} name="exit-to-app" />
          </TouchableOpacity>

        </View>
      </View>
      );
    }
}

const styles = ({
  container: {
    flex: 1,
  },
  rows: {
    flexDirection: 'row'
  },
  contentcolumns: {
    flex: 1,
    flexDirection: 'column'
  },
  footerStyle: {
    height: 56,
    position: 'relative',
    backgroundColor: "#16795B",
    flexDirection: 'row',
    justifyContent: 'space-around',
    alignItems: 'center'
  }

});

export { ItemsList };
<|endoftext|>


<commit_before>(function() {
  'use strict';

  angular.module('weddinvApp').
    controller('MainCtrl', ['$rootScope', '$scope', '$location', 'Session', function($rootScope, $scope, $location, Session) {
      $scope.loggedIn = false;
      Session.isLoggedIn(function() {
        $scope.loggedIn = true;
      });

      $scope.$on('session:login', function() {
        $scope.loggedIn = true;
      });

      $scope.$on('session:logout', function() {
        $scope.loggedIn = false;
      });

      $rootScope.$on('$routeChangeStart', function(e, next, current) {
        if (next.loginRequired && !$scope.loggedIn) {
          $location.path('/login');
        }
      });
    }]);
}());
<commit_msg>Fixing bug in sessions management.<commit_after>(function() {
  'use strict';

  angular.module('weddinvApp').
    controller('MainCtrl', ['$rootScope', '$scope', '$location', '$timeout', 'Session', function($rootScope, $scope, $location, $timeout, Session) {
      var sessionReady;
      $scope.loggedIn = false;

      Session.isLoggedIn(function() {
        sessionReady    = true;
        $scope.loggedIn = true;
      }, function() {
        sessionReady = true;
      });

      $scope.$on('session:login', function() {
        $scope.loggedIn = true;
      });

      $scope.$on('session:logout', function() {
        $scope.loggedIn = false;
      });

      var intervalId = window.setInterval(function() {
        if (sessionReady) {
          $rootScope.$on('$routeChangeStart', function(e, next, current) {
            if (next.loginRequired && !$scope.loggedIn) {
              $location.path('/login');
            }
          });
          clearInterval(intervalId);
        }
      }, 1000);
    }]);
}());
<|endoftext|>


<commit_before>import React from "react";

const fs = window.require("fs");
const global = require("../app/global");
import IO from "../app/IO";

const UserListRemove = React.createClass({
    render: function() {
        let collection = [];

        collection = this.createCollectionArray();

        return (

                {collection}

        );
    },

    createCollectionArray: function() {
        const io = new IO;

        let collection = io.readJSON(global.userItems);

        collection = collection.map((item, i) => {
            return (

                    {item.name} Season:{item.season} Episode:{item.episode}

            );
        });

        return (

                {collection}

        );
    },

    handleItemRemove: function(e) {
        e.preventDefault();

        const io = new IO;

        io.removeJSONEntry(global.userItems, e.target.getAttribute("data-entry"));
    }
});

module.exports = UserListRemove;<commit_msg>Refactor userlistremove for realtime removal<commit_after>import React from "react";

const fs = window.require("fs");
const global = require("../app/global");
import IO from "../app/IO";

const UserListRemove = React.createClass({
    getInitialState: function() {
        return {
            collection: []
        };
    },

    componentDidMount: function() {
        this.createCollectionArray();
    },

    render: function() {
        return (

                    {this.state.collection}

        );
    },

    createCollectionArray: function() {
        const io = new IO;
        let currentCollection = io.readJSON(global.userItems);

        if (currentCollection === "") return;

        currentCollection = currentCollection.map((item, i) => {
            return (

                    {item.name} Season:{item.season} Episode:{item.episode}

            );
        });

        this.setState({collection: currentCollection});
    },

    handleItemRemove: function(e) {
        e.preventDefault();

        const io = new IO;
        let currentCollection = this.state.collection;
        let index = e.target.getAttribute("data-entry");

        io.removeJSONEntry(global.userItems, index);
        currentCollection.splice(index, 1);

        this.setState({collection: currentCollection});
    }
});

module.exports = UserListRemove;<|endoftext|>


<commit_before>/*
 * Copyright (c) 2013, IT Services, Stockholm University
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 *
 * Neither the name of Stockholm University nor the names of its contributors
 * may be used to endorse or promote products derived from this software
 * without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

/**
 * The search view for the map module.
 *
 * @class A Backbone view to handle the search popup.
 * @author Joakim Lundin
 * @type {Backbone.View}
 */
var SearchView = Backbone.View.extend(
    /** @lends SearchView */
    {

      /**
       * @constructs
       * @param options Options for this class. Expects campus name and a searchResult collection to add results to
       */
      initialize: function (options) {
        _.bindAll(this, "render", "populateFilter");

        this.collection.on("reset", this.render);

        this.inputField = $("#search-autocomplete").parent().find("form input");
      },

      /** Registers events */
      events: {
        'focus input': 'showFilteredList',
        'keyup input': 'inputKeyup',
        'click #cancelFilter': 'handleCancelClick',
        'click .autocomplete-link': 'showClickedLoction'
      },

      /**
       * Render the search view.
       */
      render: function () {

        // This is done to show a search icon or text in the mobile keyboard
        this.inputField.get(0).type = "search";
        if (options.placeholderSuffix) {
          // this.inputField.attr("placeholder", "Sök " + options.placeholderSuffix);
          this.inputField.attr("placeholder", i18n.t('map.menu.searchfor') + i18n.t(options.placeholderSuffix).toLowerCase());
        }

        $("#search-autocomplete").listview("option", "filterCallback", this.filterSearch);
        this.populateFilter();
        this.delegateEvents();
      },

      inputKeyup: function (e) {
        if (e.which == 13) {
          $(e.target).trigger("blur");
        }

        var $noresults = $('#noresults');
        var results = $("#search-autocomplete").children(':visible').length;

        if (results === 0 && !$noresults.is(':visible')) {
          $noresults.show();
        }
        else if (results > 0 && $noresults.is(':visible')) {
          $noresults.hide();
        }
      },

      showFilteredList: function () {
        $("#search-box form").addClass("tight");
        $("#cancelFilter").show();

        //if input field not empty trigger new filtering with existing value, else show whole filter
        if ($('div#search-box input').val() !== "") {
          // jquerymobile remembers the old value in the input field.
          // in order to trigger a change event, we must set another value inbetween.
          var prevValue = this.inputField.val();
          this.inputField.val('').trigger('change');
          this.inputField.val(prevValue).trigger('change');
        } else {
          $("#search-autocomplete li").removeClass("ui-screen-hidden");
        }
      },

      /**
       * Handle click on cancel button.
       *
       * @param evt the click event.
       */
      handleCancelClick: function (evt) {
        this.hideFilteredList();

        if (this.inputField.val() === "") {
          this.resetLocations();
        }
      },

      /**
       * Hides the filter list.
       */
      hideFilteredList: function () {
        $("#search-box form").removeClass("tight");
        $("#cancelFilter").hide();
        $("#search-autocomplete li").addClass("ui-screen-hidden");
      },

      /**
       * getClickedLocation takes the clicked html snippet from the search-filter list,
       * i.e: E 421.
       * From this html snippet getClickedLocation extracts the element name, in this
       * case "E 421" and finds it in the this.items collection (containing) all items
       * for the given type (i.e. auditorium).
       *
       *  @returns A new Locations collection with just the inputed item.
       *           If for some reason the item is not found in this.items collection,
       *           an empty Locations collection is returned.
       */
      getClickedLocation: function (target) {
        var modelid = $(target).attr('data-modelid');
        return this.collection.get(modelid);
      },

      showClickedLoction: function (event, ui) {
        this.hideFilteredList();
        var location = this.getClickedLocation(event.target);
        this.trigger("selected", location);
      },

      resetLocations: function () {
        this.collection.trigger("reset");
      },

      populateFilter: function () {
        var html = this.collection.bySearchable().reduce(function (memo, location) {
          return memo + '
' +
              '' + location.get('name') + '' +
              '
';
        }, "");

        var $ul = $('#search-autocomplete');
        $ul.html(html);
        $ul.listview("refresh");
        $ul.trigger("updatelayout");

        // After populating the list, hide it (only show it when search-box has focus)
        // hide the list only if the input doesn't have focus.
        if (!this.inputField.is(":focus")) {
          this.hideFilteredList();
        }
      },

      filterSearch: function (text, searchValue) {
        searchValue = searchValue.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
        var pattern = new RegExp("(^| )" + searchValue, 'i');

        return !pattern.test(text);
      }
    }); //-- End of Search view
<commit_msg>MOB-149 - Fix for broken placeholder.<commit_after>/*
 * Copyright (c) 2013, IT Services, Stockholm University
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 *
 * Neither the name of Stockholm University nor the names of its contributors
 * may be used to endorse or promote products derived from this software
 * without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

/**
 * The search view for the map module.
 *
 * @class A Backbone view to handle the search popup.
 * @author Joakim Lundin
 * @type {Backbone.View}
 */
var SearchView = Backbone.View.extend(
    /** @lends SearchView */
    {

      /**
       * @constructs
       * @param options Options for this class. Expects campus name and a searchResult collection to add results to
       */
      initialize: function (options) {
        _.bindAll(this, "render", "populateFilter");

        this.placeholderSuffix = options.placeholderSuffix;

        this.collection.on("reset", this.render);

        this.inputField = $("#search-autocomplete").parent().find("form input");
      },

      /** Registers events */
      events: {
        'focus input': 'showFilteredList',
        'keyup input': 'inputKeyup',
        'click #cancelFilter': 'handleCancelClick',
        'click .autocomplete-link': 'showClickedLoction'
      },

      /**
       * Render the search view.
       */
      render: function () {
        // This is done to show a search icon or text in the mobile keyboard
        this.inputField.get(0).type = "search";
        if (this.placeholderSuffix) {
          // this.inputField.attr("placeholder", "Sök " + options.placeholderSuffix);
          this.inputField.attr("placeholder", i18n.t('map.menu.searchfor') + i18n.t(this.placeholderSuffix).toLowerCase());
        }

        $("#search-autocomplete").listview("option", "filterCallback", this.filterSearch);
        this.populateFilter();
        this.delegateEvents();
      },

      inputKeyup: function (e) {
        if (e.which == 13) {
          $(e.target).trigger("blur");
        }

        var $noresults = $('#noresults');
        var results = $("#search-autocomplete").children(':visible').length;

        if (results === 0 && !$noresults.is(':visible')) {
          $noresults.show();
        }
        else if (results > 0 && $noresults.is(':visible')) {
          $noresults.hide();
        }
      },

      showFilteredList: function () {
        $("#search-box form").addClass("tight");
        $("#cancelFilter").show();

        //if input field not empty trigger new filtering with existing value, else show whole filter
        if ($('div#search-box input').val() !== "") {
          // jquerymobile remembers the old value in the input field.
          // in order to trigger a change event, we must set another value inbetween.
          var prevValue = this.inputField.val();
          this.inputField.val('').trigger('change');
          this.inputField.val(prevValue).trigger('change');
        } else {
          $("#search-autocomplete li").removeClass("ui-screen-hidden");
        }
      },

      /**
       * Handle click on cancel button.
       *
       * @param evt the click event.
       */
      handleCancelClick: function (evt) {
        this.hideFilteredList();

        if (this.inputField.val() === "") {
          this.resetLocations();
        }
      },

      /**
       * Hides the filter list.
       */
      hideFilteredList: function () {
        $("#search-box form").removeClass("tight");
        $("#cancelFilter").hide();
        $("#search-autocomplete li").addClass("ui-screen-hidden");
      },

      /**
       * getClickedLocation takes the clicked html snippet from the search-filter list,
       * i.e: E 421.
       * From this html snippet getClickedLocation extracts the element name, in this
       * case "E 421" and finds it in the this.items collection (containing) all items
       * for the given type (i.e. auditorium).
       *
       *  @returns A new Locations collection with just the inputed item.
       *           If for some reason the item is not found in this.items collection,
       *           an empty Locations collection is returned.
       */
      getClickedLocation: function (target) {
        var modelid = $(target).attr('data-modelid');
        return this.collection.get(modelid);
      },

      showClickedLoction: function (event, ui) {
        this.hideFilteredList();
        var location = this.getClickedLocation(event.target);
        this.trigger("selected", location);
      },

      resetLocations: function () {
        this.collection.trigger("reset");
      },

      populateFilter: function () {
        var html = this.collection.bySearchable().reduce(function (memo, location) {
          return memo + '
' +
              '' + location.get('name') + '' +
              '
';
        }, "");

        var $ul = $('#search-autocomplete');
        $ul.html(html);
        $ul.listview("refresh");
        $ul.trigger("updatelayout");

        // After populating the list, hide it (only show it when search-box has focus)
        // hide the list only if the input doesn't have focus.
        if (!this.inputField.is(":focus")) {
          this.hideFilteredList();
        }
      },

      filterSearch: function (text, searchValue) {
        searchValue = searchValue.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
        var pattern = new RegExp("(^| )" + searchValue, 'i');

        return !pattern.test(text);
      }
    }); //-- End of Search view
<|endoftext|>


<commit_before>/*jslint undef: true, white: true*/
/*global hub*/
/**
 * Copyright 2011, Maximilian Antoni
 * Released under the MIT license:
 * https://github.com/mantoni/hub.js/raw/master/LICENSE
 */
/**
 *
 */
(function () {

	/**
	 * All peer instances that have been created.
	 *
	 * @type {object}
	 */
	var peers = {};

	/**
	 * All peer definitions.
	 *
	 * @type {object}
	 */
	var definitions = {};

	/**
	 * An empty array used as an internal value object.
	 *
	 * @type {Array}
	 */
	var emptyArray = [];

	function subscriber(chain, scope) {
		return function () {
			return chain.apply(scope, arguments);
		};
	}

	function wire(peer, namespace, isProto) {
		var api = peer.api = {};
		var message;
		for (message in peer) {
			if (peer.hasOwnProperty(message)) {
				var fn = peer[message];
				if (typeof fn === "function") {
					var topic = namespace + "/" + message;
					var pub;
					if (isProto) {
						api[message] = hub.chain(hub.publisher(topic), fn);
					} else {
						api[message] = hub.publisher(topic);
						var sub = subscriber(fn, api);
						hub.subscribe(topic, sub);
					}
				}
			}
		}
	}

	/**
	 *

	 * defines a peer in the hub that publishes and receives messages.
	 *

	 *

	 * Configuration parameters:
	 *

	 *

	 *
is (String|Array): single peer name or list of peer names this peer
	 * inherits from

	 *
scope (String): the peer scope, either hub.SINGLETON or
	 * hub.PROTOTYPE. Defaults to hub.SINGLETON.

	 *

	 *
	 * @param {String} namespace the namespace for the peer
	 * @param {Function} factory the factory for the map of listeners
	 */
	hub.peer = function (namespace, factory) {
		if (definitions[namespace]) {
			throw new Error("hub - peer already defined: " + namespace);
		}
		var definition = {};
		if (typeof factory === "function") {
			definition.factory = factory;
		} else {
			peers[namespace] = factory;
			wire(factory, namespace, false);
		}
		definitions[namespace] = definition;
	};

	hub.singleton = function (name, fn, args) {
		hub.peer(name, typeof fn === "function" ? hub.object(fn, args) : fn);
	};

	/**
	 *

	 * returns the API of the peer with the given namespace. The returned
	 * object implements all messages of the peer as methods that can be
	 * directly invoked.
	 *

	 *

	 * If the peer with the given namespace is a prototype peer, a new
	 * instance is created.
	 *

	 *
	 * @param {String} namespace the namespace.
	 * @return {Object} the API of the peer.
	 */
	hub.get = function (namespace) {
		var args = arguments.length === 1 ? emptyArray :
			Array.prototype.slice.call(arguments, 1);
		var peer = peers[namespace];
		if (peer) {
			return peer.api;
		}
		var definition = definitions[namespace];
		if (!definition) {
			throw new Error("Peer is not defined: " + namespace);
		}
		peer = hub.object(definition.factory, args);
		wire(peer, namespace, true);
		return peer.api;
	};

	hub.resetPeers = function () {
		peers = {};
		var k;
		for (k in definitions) {
			if (definitions.hasOwnProperty(k)) {
				if (k.indexOf("lib.") === -1) {
					delete definitions[k];
				}
			}
		}
	};

}());
<commit_msg>Simplified internal wire implementation.<commit_after>/*jslint undef: true, white: true*/
/*global hub*/
/**
 * Copyright 2011, Maximilian Antoni
 * Released under the MIT license:
 * https://github.com/mantoni/hub.js/raw/master/LICENSE
 */
/**
 *
 */
(function () {

	/**
	 * All peer instances that have been created.
	 *
	 * @type {object}
	 */
	var peers = {};

	/**
	 * All peer definitions.
	 *
	 * @type {object}
	 */
	var definitions = {};

	/**
	 * An empty array used as an internal value object.
	 *
	 * @type {Array}
	 */
	var emptyArray = [];

	function subscriber(chain, scope) {
		return function () {
			return chain.apply(scope, arguments);
		};
	}

	function wire(peer, namespace, isProto) {
		var api = peer.api = {};
		var message;
		for (message in peer) {
			if (peer.hasOwnProperty(message)) {
				var fn = peer[message];
				if (typeof fn === "function") {
					var topic = namespace + "/" + message;
					var pub = hub.publisher(topic);
					if (isProto) {
						api[message] = hub.chain(pub, fn);
					} else {
						api[message] = pub;
						var sub = subscriber(fn, api);
						hub.subscribe(topic, sub);
					}
				}
			}
		}
	}

	/**
	 *

	 * defines a peer in the hub that publishes and receives messages.
	 *

	 *

	 * Configuration parameters:
	 *

	 *

	 *
is (String|Array): single peer name or list of peer names this peer
	 * inherits from

	 *
scope (String): the peer scope, either hub.SINGLETON or
	 * hub.PROTOTYPE. Defaults to hub.SINGLETON.

	 *

	 *
	 * @param {String} namespace the namespace for the peer
	 * @param {Function} factory the factory for the map of listeners
	 */
	hub.peer = function (namespace, factory) {
		if (definitions[namespace]) {
			throw new Error("hub - peer already defined: " + namespace);
		}
		var definition = {};
		if (typeof factory === "function") {
			definition.factory = factory;
		} else {
			peers[namespace] = factory;
			wire(factory, namespace, false);
		}
		definitions[namespace] = definition;
	};

	hub.singleton = function (name, fn, args) {
		hub.peer(name, typeof fn === "function" ? hub.object(fn, args) : fn);
	};

	/**
	 *

	 * returns the API of the peer with the given namespace. The returned
	 * object implements all messages of the peer as methods that can be
	 * directly invoked.
	 *

	 *

	 * If the peer with the given namespace is a prototype peer, a new
	 * instance is created.
	 *

	 *
	 * @param {String} namespace the namespace.
	 * @return {Object} the API of the peer.
	 */
	hub.get = function (namespace) {
		var args = arguments.length === 1 ? emptyArray :
			Array.prototype.slice.call(arguments, 1);
		var peer = peers[namespace];
		if (peer) {
			return peer.api;
		}
		var definition = definitions[namespace];
		if (!definition) {
			throw new Error("Peer is not defined: " + namespace);
		}
		peer = hub.object(definition.factory, args);
		wire(peer, namespace, true);
		return peer.api;
	};

	hub.resetPeers = function () {
		peers = {};
		var k;
		for (k in definitions) {
			if (definitions.hasOwnProperty(k)) {
				if (k.indexOf("lib.") === -1) {
					delete definitions[k];
				}
			}
		}
	};

}());
<|endoftext|>


<commit_before>'user strict';

var WebViewImpl, deprecate, getNextId, guestViewInternal, ipcRenderer, listener, nextId, ref, registerBrowserPluginElement, registerWebViewElement, remote, useCapture, v8Util, webFrame, webViewConstants,
  hasProp = {}.hasOwnProperty,
  slice = [].slice;

ref = require('electron'), deprecate = ref.deprecate, webFrame = ref.webFrame, remote = ref.remote, ipcRenderer = ref.ipcRenderer;

v8Util = process.atomBinding('v8_util');

guestViewInternal = require('./guest-view-internal');

webViewConstants = require('./web-view-constants');

/* ID generator. */

nextId = 0;

getNextId = function() {
  return ++nextId;
};

/* Represents the internal state of the WebView node. */

WebViewImpl = (function() {
  function WebViewImpl(webviewNode) {
    var shadowRoot;
    this.webviewNode = webviewNode;
    v8Util.setHiddenValue(this.webviewNode, 'internal', this);
    this.attached = false;
    this.elementAttached = false;
    this.beforeFirstNavigation = true;

    /* on* Event handlers. */
    this.on = {};
    this.browserPluginNode = this.createBrowserPluginNode();
    shadowRoot = this.webviewNode.createShadowRoot();
    this.setupWebViewAttributes();
    this.setupFocusPropagation();
    this.viewInstanceId = getNextId();
    shadowRoot.appendChild(this.browserPluginNode);

    // Subscribe to host's zoom level changes.
    webFrame.on('zoom-level-changed', (zoomLevel) => {
      this.webviewNode.setZoomLevel(zoomLevel);
    });
  }

  WebViewImpl.prototype.createBrowserPluginNode = function() {

    /*
      We create BrowserPlugin as a custom element in order to observe changes
      to attributes synchronously.
     */
    var browserPluginNode;
    browserPluginNode = new WebViewImpl.BrowserPlugin();
    v8Util.setHiddenValue(browserPluginNode, 'internal', this);
    return browserPluginNode;
  };

  /* Resets some state upon reattaching <webview> element to the DOM. */

  WebViewImpl.prototype.reset = function() {

    /*
      If guestInstanceId is defined then the <webview> has navigated and has
      already picked up a partition ID. Thus, we need to reset the initialization
      state. However, it may be the case that beforeFirstNavigation is false BUT
      guestInstanceId has yet to be initialized. This means that we have not
      heard back from createGuest yet. We will not reset the flag in this case so
      that we don't end up allocating a second guest.
     */
    if (this.guestInstanceId) {
      guestViewInternal.destroyGuest(this.guestInstanceId);
      this.webContents = null;
      this.guestInstanceId = void 0;
      this.beforeFirstNavigation = true;
      this.attributes[webViewConstants.ATTRIBUTE_PARTITION].validPartitionId = true;
    }
    return this.internalInstanceId = 0;
  };

  /* Sets the <webview>.request property. */

  WebViewImpl.prototype.setRequestPropertyOnWebViewNode = function(request) {
    return Object.defineProperty(this.webviewNode, 'request', {
      value: request,
      enumerable: true
    });
  };

  WebViewImpl.prototype.setupFocusPropagation = function() {
    if (!this.webviewNode.hasAttribute('tabIndex')) {

      /*
        <webview> needs a tabIndex in order to be focusable.
        TODO(fsamuel): It would be nice to avoid exposing a tabIndex attribute
        to allow <webview> to be focusable.
        See http://crbug.com/231664.
       */
      this.webviewNode.setAttribute('tabIndex', -1);
    }
    this.webviewNode.addEventListener('focus', (function(_this) {
      return function(e) {

        /* Focus the BrowserPlugin when the <webview> takes focus. */
        return _this.browserPluginNode.focus();
      };
    })(this));
    return this.webviewNode.addEventListener('blur', (function(_this) {
      return function(e) {

        /* Blur the BrowserPlugin when the <webview> loses focus. */
        return _this.browserPluginNode.blur();
      };
    })(this));
  };

  /*
    This observer monitors mutations to attributes of the <webview> and
    updates the BrowserPlugin properties accordingly. In turn, updating
    a BrowserPlugin property will update the corresponding BrowserPlugin
    attribute, if necessary. See BrowserPlugin::UpdateDOMAttribute for more
    details.
   */

  WebViewImpl.prototype.handleWebviewAttributeMutation = function(attributeName, oldValue, newValue) {
    if (!this.attributes[attributeName] || this.attributes[attributeName].ignoreMutation) {
      return;
    }

    /* Let the changed attribute handle its own mutation; */
    return this.attributes[attributeName].handleMutation(oldValue, newValue);
  };

  WebViewImpl.prototype.handleBrowserPluginAttributeMutation = function(attributeName, oldValue, newValue) {
    if (attributeName === webViewConstants.ATTRIBUTE_INTERNALINSTANCEID && !oldValue && !!newValue) {
      this.browserPluginNode.removeAttribute(webViewConstants.ATTRIBUTE_INTERNALINSTANCEID);
      this.internalInstanceId = parseInt(newValue);

      /* Track when the element resizes using the element resize callback. */
      webFrame.registerElementResizeCallback(this.internalInstanceId, this.onElementResize.bind(this));
      if (!this.guestInstanceId) {
        return;
      }
      return guestViewInternal.attachGuest(this.internalInstanceId, this.guestInstanceId, this.buildParams());
    }
  };

  WebViewImpl.prototype.onSizeChanged = function(webViewEvent) {
    var height, maxHeight, maxWidth, minHeight, minWidth, newHeight, newWidth, node, width;
    newWidth = webViewEvent.newWidth;
    newHeight = webViewEvent.newHeight;
    node = this.webviewNode;
    width = node.offsetWidth;
    height = node.offsetHeight;

    /* Check the current bounds to make sure we do not resize <webview> */

    /* outside of current constraints. */
    maxWidth = this.attributes[webViewConstants.ATTRIBUTE_MAXWIDTH].getValue() | width;
    maxHeight = this.attributes[webViewConstants.ATTRIBUTE_MAXHEIGHT].getValue() | width;
    minWidth = this.attributes[webViewConstants.ATTRIBUTE_MINWIDTH].getValue() | width;
    minHeight = this.attributes[webViewConstants.ATTRIBUTE_MINHEIGHT].getValue() | width;
    minWidth = Math.min(minWidth, maxWidth);
    minHeight = Math.min(minHeight, maxHeight);
    if (!this.attributes[webViewConstants.ATTRIBUTE_AUTOSIZE].getValue() || (newWidth >= minWidth && newWidth <= maxWidth && newHeight >= minHeight && newHeight <= maxHeight)) {
      node.style.width = newWidth + 'px';
      node.style.height = newHeight + 'px';

      /*
        Only fire the DOM event if the size of the <webview> has actually
        changed.
       */
      return this.dispatchEvent(webViewEvent);
    }
  };

  WebViewImpl.prototype.onElementResize = function(newSize) {

    /* Dispatch the 'resize' event. */
    var resizeEvent;
    resizeEvent = new Event('resize', {
      bubbles: true
    });
    resizeEvent.newWidth = newSize.width;
    resizeEvent.newHeight = newSize.height;
    this.dispatchEvent(resizeEvent);
    if (this.guestInstanceId) {
      return guestViewInternal.setSize(this.guestInstanceId, {
        normal: newSize
      });
    }
  };

  WebViewImpl.prototype.createGuest = function() {
    return guestViewInternal.createGuest(this.buildParams(), (function(_this) {
      return function(event, guestInstanceId) {
        return _this.attachWindow(guestInstanceId);
      };
    })(this));
  };

  WebViewImpl.prototype.dispatchEvent = function(webViewEvent) {
    return this.webviewNode.dispatchEvent(webViewEvent);
  };

  /* Adds an 'on<event>' property on the webview, which can be used to set/unset */

  /* an event handler. */

  WebViewImpl.prototype.setupEventProperty = function(eventName) {
    var propertyName;
    propertyName = 'on' + eventName.toLowerCase();
    return Object.defineProperty(this.webviewNode, propertyName, {
      get: (function(_this) {
        return function() {
          return _this.on[propertyName];
        };
      })(this),
      set: (function(_this) {
        return function(value) {
          if (_this.on[propertyName]) {
            _this.webviewNode.removeEventListener(eventName, _this.on[propertyName]);
          }
          _this.on[propertyName] = value;
          if (value) {
            return _this.webviewNode.addEventListener(eventName, value);
          }
        };
      })(this),
      enumerable: true
    });
  };

  /* Updates state upon loadcommit. */

  WebViewImpl.prototype.onLoadCommit = function(webViewEvent) {
    var newValue, oldValue;
    oldValue = this.webviewNode.getAttribute(webViewConstants.ATTRIBUTE_SRC);
    newValue = webViewEvent.url;
    if (webViewEvent.isMainFrame && (oldValue !== newValue)) {

      /*
        Touching the src attribute triggers a navigation. To avoid
        triggering a page reload on every guest-initiated navigation,
        we do not handle this mutation.
       */
      return this.attributes[webViewConstants.ATTRIBUTE_SRC].setValueIgnoreMutation(newValue);
    }
  };

  WebViewImpl.prototype.onAttach = function(storagePartitionId) {
    return this.attributes[webViewConstants.ATTRIBUTE_PARTITION].setValue(storagePartitionId);
  };

  WebViewImpl.prototype.buildParams = function() {
    var attribute, attributeName, css, elementRect, params, ref1;
    params = {
      instanceId: this.viewInstanceId,
      userAgentOverride: this.userAgentOverride
    };
    ref1 = this.attributes;
    for (attributeName in ref1) {
      if (!hasProp.call(ref1, attributeName)) continue;
      attribute = ref1[attributeName];
      params[attributeName] = attribute.getValue();
    }

    /*
      When the WebView is not participating in layout (display:none)
      then getBoundingClientRect() would report a width and height of 0.
      However, in the case where the WebView has a fixed size we can
      use that value to initially size the guest so as to avoid a relayout of
      the on display:block.
     */
    css = window.getComputedStyle(this.webviewNode, null);
    elementRect = this.webviewNode.getBoundingClientRect();
    params.elementWidth = parseInt(elementRect.width) || parseInt(css.getPropertyValue('width'));
    params.elementHeight = parseInt(elementRect.height) || parseInt(css.getPropertyValue('height'));
    return params;
  };

  WebViewImpl.prototype.attachWindow = function(guestInstanceId) {
    this.guestInstanceId = guestInstanceId;
    this.webContents = remote.getGuestWebContents(this.guestInstanceId);
    if (!this.internalInstanceId) {
      return true;
    }
    return guestViewInternal.attachGuest(this.internalInstanceId, this.guestInstanceId, this.buildParams());
  };

  return WebViewImpl;

})();

/* Registers browser plugin <object> custom element. */

registerBrowserPluginElement = function() {
  var proto;
  proto = Object.create(HTMLObjectElement.prototype);
  proto.createdCallback = function() {
    this.setAttribute('type', 'application/browser-plugin');
    this.setAttribute('id', 'browser-plugin-' + getNextId());

    /* The <object> node fills in the <webview> container. */
    this.style.display = 'block';
    this.style.width = '100%';
    return this.style.height = '100%';
  };
  proto.attributeChangedCallback = function(name, oldValue, newValue) {
    var internal;
    internal = v8Util.getHiddenValue(this, 'internal');
    if (!internal) {
      return;
    }
    return internal.handleBrowserPluginAttributeMutation(name, oldValue, newValue);
  };
  proto.attachedCallback = function() {

    /* Load the plugin immediately. */
    var unused;
    return unused = this.nonExistentAttribute;
  };
  WebViewImpl.BrowserPlugin = webFrame.registerEmbedderCustomElement('browserplugin', {
    "extends": 'object',
    prototype: proto
  });
  delete proto.createdCallback;
  delete proto.attachedCallback;
  delete proto.detachedCallback;
  return delete proto.attributeChangedCallback;
};

/* Registers <webview> custom element. */

registerWebViewElement = function() {
  var createBlockHandler, createNonBlockHandler, i, j, len, len1, m, methods, nonblockMethods, proto;
  proto = Object.create(HTMLObjectElement.prototype);
  proto.createdCallback = function() {
    return new WebViewImpl(this);
  };
  proto.attributeChangedCallback = function(name, oldValue, newValue) {
    var internal;
    internal = v8Util.getHiddenValue(this, 'internal');
    if (!internal) {
      return;
    }
    return internal.handleWebviewAttributeMutation(name, oldValue, newValue);
  };
  proto.detachedCallback = function() {
    var internal;
    internal = v8Util.getHiddenValue(this, 'internal');
    if (!internal) {
      return;
    }
    guestViewInternal.deregisterEvents(internal.viewInstanceId);
    internal.elementAttached = false;
    return internal.reset();
  };
  proto.attachedCallback = function() {
    var internal;
    internal = v8Util.getHiddenValue(this, 'internal');
    if (!internal) {
      return;
    }
    if (!internal.elementAttached) {
      guestViewInternal.registerEvents(internal, internal.viewInstanceId);
      internal.elementAttached = true;
      return internal.attributes[webViewConstants.ATTRIBUTE_SRC].parse();
    }
  };

  /* Public-facing API methods. */
  methods = ['getURL', 'getTitle', 'isLoading', 'isWaitingForResponse', 'stop', 'reload', 'reloadIgnoringCache', 'canGoBack', 'canGoForward', 'canGoToOffset', 'clearHistory', 'goBack', 'goForward', 'goToIndex', 'goToOffset', 'isCrashed', 'setUserAgent', 'getUserAgent', 'openDevTools', 'closeDevTools', 'isDevToolsOpened', 'isDevToolsFocused', 'inspectElement', 'setAudioMuted', 'isAudioMuted', 'undo', 'redo', 'cut', 'copy', 'paste', 'pasteAndMatchStyle', 'delete', 'selectAll', 'unselect', 'replace', 'replaceMisspelling', 'findInPage', 'stopFindInPage', 'getId', 'downloadURL', 'inspectServiceWorker', 'print', 'printToPDF'];
  nonblockMethods = [
    'executeJavaScript',
    'insertCSS',
    'insertText',
    'send',
    'sendInputEvent',
    'setZoomFactor',
    'setZoomLevel',
    'setZoomLevelLimits',
  ];

  /* Forward proto.foo* method calls to WebViewImpl.foo*. */
  createBlockHandler = function(m) {
    return function() {
      var args, internal, ref1;
      args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
      internal = v8Util.getHiddenValue(this, 'internal');
      return (ref1 = internal.webContents)[m].apply(ref1, args);
    };
  };
  for (i = 0, len = methods.length; i < len; i++) {
    m = methods[i];
    proto[m] = createBlockHandler(m);
  }
  createNonBlockHandler = function(m) {
    return function() {
      var args, internal;
      args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
      internal = v8Util.getHiddenValue(this, 'internal');
      return ipcRenderer.send.apply(ipcRenderer, ['ATOM_BROWSER_ASYNC_CALL_TO_GUEST_VIEW', internal.guestInstanceId, m].concat(slice.call(args)));
    };
  };
  for (j = 0, len1 = nonblockMethods.length; j < len1; j++) {
    m = nonblockMethods[j];
    proto[m] = createNonBlockHandler(m);
  }

  /* Deprecated. */
  deprecate.rename(proto, 'getUrl', 'getURL');
  window.WebView = webFrame.registerEmbedderCustomElement('webview', {
    prototype: proto
  });

  /* Delete the callbacks so developers cannot call them and produce unexpected */

  /* behavior. */
  delete proto.createdCallback;
  delete proto.attachedCallback;
  delete proto.detachedCallback;
  return delete proto.attributeChangedCallback;
};

useCapture = true;

listener = function(event) {
  if (document.readyState === 'loading') {
    return;
  }
  registerBrowserPluginElement();
  registerWebViewElement();
  return window.removeEventListener(event.type, listener, useCapture);
};

window.addEventListener('readystatechange', listener, true);

module.exports = WebViewImpl;
<commit_msg>Unlisten the zoom-level-changed event when webview is detached<commit_after>'user strict';

var WebViewImpl, deprecate, getNextId, guestViewInternal, ipcRenderer, listener, nextId, ref, registerBrowserPluginElement, registerWebViewElement, remote, useCapture, v8Util, webFrame, webViewConstants,
  hasProp = {}.hasOwnProperty,
  slice = [].slice;

ref = require('electron'), deprecate = ref.deprecate, webFrame = ref.webFrame, remote = ref.remote, ipcRenderer = ref.ipcRenderer;

v8Util = process.atomBinding('v8_util');

guestViewInternal = require('./guest-view-internal');

webViewConstants = require('./web-view-constants');

/* ID generator. */

nextId = 0;

getNextId = function() {
  return ++nextId;
};

/* Represents the internal state of the WebView node. */

WebViewImpl = (function() {
  function WebViewImpl(webviewNode) {
    var shadowRoot;
    this.webviewNode = webviewNode;
    v8Util.setHiddenValue(this.webviewNode, 'internal', this);
    this.attached = false;
    this.elementAttached = false;
    this.beforeFirstNavigation = true;

    /* on* Event handlers. */
    this.on = {};
    this.browserPluginNode = this.createBrowserPluginNode();
    shadowRoot = this.webviewNode.createShadowRoot();
    this.setupWebViewAttributes();
    this.setupFocusPropagation();
    this.viewInstanceId = getNextId();
    shadowRoot.appendChild(this.browserPluginNode);

    // Subscribe to host's zoom level changes.
    this.onZoomLevelChanged = (zoomLevel) => {
      this.webviewNode.setZoomLevel(zoomLevel);
    }
    webFrame.on('zoom-level-changed', this.onZoomLevelChanged);
  }

  WebViewImpl.prototype.createBrowserPluginNode = function() {

    /*
      We create BrowserPlugin as a custom element in order to observe changes
      to attributes synchronously.
     */
    var browserPluginNode;
    browserPluginNode = new WebViewImpl.BrowserPlugin();
    v8Util.setHiddenValue(browserPluginNode, 'internal', this);
    return browserPluginNode;
  };

  /* Resets some state upon reattaching <webview> element to the DOM. */

  WebViewImpl.prototype.reset = function() {
    // Unlisten the zoom-level-changed event.
    webFrame.removeListener('zoom-level-changed', this.onZoomLevelChanged);

    /*
      If guestInstanceId is defined then the <webview> has navigated and has
      already picked up a partition ID. Thus, we need to reset the initialization
      state. However, it may be the case that beforeFirstNavigation is false BUT
      guestInstanceId has yet to be initialized. This means that we have not
      heard back from createGuest yet. We will not reset the flag in this case so
      that we don't end up allocating a second guest.
     */
    if (this.guestInstanceId) {
      guestViewInternal.destroyGuest(this.guestInstanceId);
      this.webContents = null;
      this.guestInstanceId = void 0;
      this.beforeFirstNavigation = true;
      this.attributes[webViewConstants.ATTRIBUTE_PARTITION].validPartitionId = true;
    }
    return this.internalInstanceId = 0;
  };

  /* Sets the <webview>.request property. */

  WebViewImpl.prototype.setRequestPropertyOnWebViewNode = function(request) {
    return Object.defineProperty(this.webviewNode, 'request', {
      value: request,
      enumerable: true
    });
  };

  WebViewImpl.prototype.setupFocusPropagation = function() {
    if (!this.webviewNode.hasAttribute('tabIndex')) {

      /*
        <webview> needs a tabIndex in order to be focusable.
        TODO(fsamuel): It would be nice to avoid exposing a tabIndex attribute
        to allow <webview> to be focusable.
        See http://crbug.com/231664.
       */
      this.webviewNode.setAttribute('tabIndex', -1);
    }
    this.webviewNode.addEventListener('focus', (function(_this) {
      return function(e) {

        /* Focus the BrowserPlugin when the <webview> takes focus. */
        return _this.browserPluginNode.focus();
      };
    })(this));
    return this.webviewNode.addEventListener('blur', (function(_this) {
      return function(e) {

        /* Blur the BrowserPlugin when the <webview> loses focus. */
        return _this.browserPluginNode.blur();
      };
    })(this));
  };

  /*
    This observer monitors mutations to attributes of the <webview> and
    updates the BrowserPlugin properties accordingly. In turn, updating
    a BrowserPlugin property will update the corresponding BrowserPlugin
    attribute, if necessary. See BrowserPlugin::UpdateDOMAttribute for more
    details.
   */

  WebViewImpl.prototype.handleWebviewAttributeMutation = function(attributeName, oldValue, newValue) {
    if (!this.attributes[attributeName] || this.attributes[attributeName].ignoreMutation) {
      return;
    }

    /* Let the changed attribute handle its own mutation; */
    return this.attributes[attributeName].handleMutation(oldValue, newValue);
  };

  WebViewImpl.prototype.handleBrowserPluginAttributeMutation = function(attributeName, oldValue, newValue) {
    if (attributeName === webViewConstants.ATTRIBUTE_INTERNALINSTANCEID && !oldValue && !!newValue) {
      this.browserPluginNode.removeAttribute(webViewConstants.ATTRIBUTE_INTERNALINSTANCEID);
      this.internalInstanceId = parseInt(newValue);

      /* Track when the element resizes using the element resize callback. */
      webFrame.registerElementResizeCallback(this.internalInstanceId, this.onElementResize.bind(this));
      if (!this.guestInstanceId) {
        return;
      }
      return guestViewInternal.attachGuest(this.internalInstanceId, this.guestInstanceId, this.buildParams());
    }
  };

  WebViewImpl.prototype.onSizeChanged = function(webViewEvent) {
    var height, maxHeight, maxWidth, minHeight, minWidth, newHeight, newWidth, node, width;
    newWidth = webViewEvent.newWidth;
    newHeight = webViewEvent.newHeight;
    node = this.webviewNode;
    width = node.offsetWidth;
    height = node.offsetHeight;

    /* Check the current bounds to make sure we do not resize <webview> */

    /* outside of current constraints. */
    maxWidth = this.attributes[webViewConstants.ATTRIBUTE_MAXWIDTH].getValue() | width;
    maxHeight = this.attributes[webViewConstants.ATTRIBUTE_MAXHEIGHT].getValue() | width;
    minWidth = this.attributes[webViewConstants.ATTRIBUTE_MINWIDTH].getValue() | width;
    minHeight = this.attributes[webViewConstants.ATTRIBUTE_MINHEIGHT].getValue() | width;
    minWidth = Math.min(minWidth, maxWidth);
    minHeight = Math.min(minHeight, maxHeight);
    if (!this.attributes[webViewConstants.ATTRIBUTE_AUTOSIZE].getValue() || (newWidth >= minWidth && newWidth <= maxWidth && newHeight >= minHeight && newHeight <= maxHeight)) {
      node.style.width = newWidth + 'px';
      node.style.height = newHeight + 'px';

      /*
        Only fire the DOM event if the size of the <webview> has actually
        changed.
       */
      return this.dispatchEvent(webViewEvent);
    }
  };

  WebViewImpl.prototype.onElementResize = function(newSize) {

    /* Dispatch the 'resize' event. */
    var resizeEvent;
    resizeEvent = new Event('resize', {
      bubbles: true
    });
    resizeEvent.newWidth = newSize.width;
    resizeEvent.newHeight = newSize.height;
    this.dispatchEvent(resizeEvent);
    if (this.guestInstanceId) {
      return guestViewInternal.setSize(this.guestInstanceId, {
        normal: newSize
      });
    }
  };

  WebViewImpl.prototype.createGuest = function() {
    return guestViewInternal.createGuest(this.buildParams(), (function(_this) {
      return function(event, guestInstanceId) {
        return _this.attachWindow(guestInstanceId);
      };
    })(this));
  };

  WebViewImpl.prototype.dispatchEvent = function(webViewEvent) {
    return this.webviewNode.dispatchEvent(webViewEvent);
  };

  /* Adds an 'on<event>' property on the webview, which can be used to set/unset */

  /* an event handler. */

  WebViewImpl.prototype.setupEventProperty = function(eventName) {
    var propertyName;
    propertyName = 'on' + eventName.toLowerCase();
    return Object.defineProperty(this.webviewNode, propertyName, {
      get: (function(_this) {
        return function() {
          return _this.on[propertyName];
        };
      })(this),
      set: (function(_this) {
        return function(value) {
          if (_this.on[propertyName]) {
            _this.webviewNode.removeEventListener(eventName, _this.on[propertyName]);
          }
          _this.on[propertyName] = value;
          if (value) {
            return _this.webviewNode.addEventListener(eventName, value);
          }
        };
      })(this),
      enumerable: true
    });
  };

  /* Updates state upon loadcommit. */

  WebViewImpl.prototype.onLoadCommit = function(webViewEvent) {
    var newValue, oldValue;
    oldValue = this.webviewNode.getAttribute(webViewConstants.ATTRIBUTE_SRC);
    newValue = webViewEvent.url;
    if (webViewEvent.isMainFrame && (oldValue !== newValue)) {

      /*
        Touching the src attribute triggers a navigation. To avoid
        triggering a page reload on every guest-initiated navigation,
        we do not handle this mutation.
       */
      return this.attributes[webViewConstants.ATTRIBUTE_SRC].setValueIgnoreMutation(newValue);
    }
  };

  WebViewImpl.prototype.onAttach = function(storagePartitionId) {
    return this.attributes[webViewConstants.ATTRIBUTE_PARTITION].setValue(storagePartitionId);
  };

  WebViewImpl.prototype.buildParams = function() {
    var attribute, attributeName, css, elementRect, params, ref1;
    params = {
      instanceId: this.viewInstanceId,
      userAgentOverride: this.userAgentOverride
    };
    ref1 = this.attributes;
    for (attributeName in ref1) {
      if (!hasProp.call(ref1, attributeName)) continue;
      attribute = ref1[attributeName];
      params[attributeName] = attribute.getValue();
    }

    /*
      When the WebView is not participating in layout (display:none)
      then getBoundingClientRect() would report a width and height of 0.
      However, in the case where the WebView has a fixed size we can
      use that value to initially size the guest so as to avoid a relayout of
      the on display:block.
     */
    css = window.getComputedStyle(this.webviewNode, null);
    elementRect = this.webviewNode.getBoundingClientRect();
    params.elementWidth = parseInt(elementRect.width) || parseInt(css.getPropertyValue('width'));
    params.elementHeight = parseInt(elementRect.height) || parseInt(css.getPropertyValue('height'));
    return params;
  };

  WebViewImpl.prototype.attachWindow = function(guestInstanceId) {
    this.guestInstanceId = guestInstanceId;
    this.webContents = remote.getGuestWebContents(this.guestInstanceId);
    if (!this.internalInstanceId) {
      return true;
    }
    return guestViewInternal.attachGuest(this.internalInstanceId, this.guestInstanceId, this.buildParams());
  };

  return WebViewImpl;

})();

/* Registers browser plugin <object> custom element. */

registerBrowserPluginElement = function() {
  var proto;
  proto = Object.create(HTMLObjectElement.prototype);
  proto.createdCallback = function() {
    this.setAttribute('type', 'application/browser-plugin');
    this.setAttribute('id', 'browser-plugin-' + getNextId());

    /* The <object> node fills in the <webview> container. */
    this.style.display = 'block';
    this.style.width = '100%';
    return this.style.height = '100%';
  };
  proto.attributeChangedCallback = function(name, oldValue, newValue) {
    var internal;
    internal = v8Util.getHiddenValue(this, 'internal');
    if (!internal) {
      return;
    }
    return internal.handleBrowserPluginAttributeMutation(name, oldValue, newValue);
  };
  proto.attachedCallback = function() {

    /* Load the plugin immediately. */
    var unused;
    return unused = this.nonExistentAttribute;
  };
  WebViewImpl.BrowserPlugin = webFrame.registerEmbedderCustomElement('browserplugin', {
    "extends": 'object',
    prototype: proto
  });
  delete proto.createdCallback;
  delete proto.attachedCallback;
  delete proto.detachedCallback;
  return delete proto.attributeChangedCallback;
};

/* Registers <webview> custom element. */

registerWebViewElement = function() {
  var createBlockHandler, createNonBlockHandler, i, j, len, len1, m, methods, nonblockMethods, proto;
  proto = Object.create(HTMLObjectElement.prototype);
  proto.createdCallback = function() {
    return new WebViewImpl(this);
  };
  proto.attributeChangedCallback = function(name, oldValue, newValue) {
    var internal;
    internal = v8Util.getHiddenValue(this, 'internal');
    if (!internal) {
      return;
    }
    return internal.handleWebviewAttributeMutation(name, oldValue, newValue);
  };
  proto.detachedCallback = function() {
    var internal;
    internal = v8Util.getHiddenValue(this, 'internal');
    if (!internal) {
      return;
    }
    guestViewInternal.deregisterEvents(internal.viewInstanceId);
    internal.elementAttached = false;
    return internal.reset();
  };
  proto.attachedCallback = function() {
    var internal;
    internal = v8Util.getHiddenValue(this, 'internal');
    if (!internal) {
      return;
    }
    if (!internal.elementAttached) {
      guestViewInternal.registerEvents(internal, internal.viewInstanceId);
      internal.elementAttached = true;
      return internal.attributes[webViewConstants.ATTRIBUTE_SRC].parse();
    }
  };

  /* Public-facing API methods. */
  methods = ['getURL', 'getTitle', 'isLoading', 'isWaitingForResponse', 'stop', 'reload', 'reloadIgnoringCache', 'canGoBack', 'canGoForward', 'canGoToOffset', 'clearHistory', 'goBack', 'goForward', 'goToIndex', 'goToOffset', 'isCrashed', 'setUserAgent', 'getUserAgent', 'openDevTools', 'closeDevTools', 'isDevToolsOpened', 'isDevToolsFocused', 'inspectElement', 'setAudioMuted', 'isAudioMuted', 'undo', 'redo', 'cut', 'copy', 'paste', 'pasteAndMatchStyle', 'delete', 'selectAll', 'unselect', 'replace', 'replaceMisspelling', 'findInPage', 'stopFindInPage', 'getId', 'downloadURL', 'inspectServiceWorker', 'print', 'printToPDF'];
  nonblockMethods = [
    'executeJavaScript',
    'insertCSS',
    'insertText',
    'send',
    'sendInputEvent',
    'setZoomFactor',
    'setZoomLevel',
    'setZoomLevelLimits',
  ];

  /* Forward proto.foo* method calls to WebViewImpl.foo*. */
  createBlockHandler = function(m) {
    return function() {
      var args, internal, ref1;
      args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
      internal = v8Util.getHiddenValue(this, 'internal');
      return (ref1 = internal.webContents)[m].apply(ref1, args);
    };
  };
  for (i = 0, len = methods.length; i < len; i++) {
    m = methods[i];
    proto[m] = createBlockHandler(m);
  }
  createNonBlockHandler = function(m) {
    return function() {
      var args, internal;
      args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
      internal = v8Util.getHiddenValue(this, 'internal');
      return ipcRenderer.send.apply(ipcRenderer, ['ATOM_BROWSER_ASYNC_CALL_TO_GUEST_VIEW', internal.guestInstanceId, m].concat(slice.call(args)));
    };
  };
  for (j = 0, len1 = nonblockMethods.length; j < len1; j++) {
    m = nonblockMethods[j];
    proto[m] = createNonBlockHandler(m);
  }

  /* Deprecated. */
  deprecate.rename(proto, 'getUrl', 'getURL');
  window.WebView = webFrame.registerEmbedderCustomElement('webview', {
    prototype: proto
  });

  /* Delete the callbacks so developers cannot call them and produce unexpected */

  /* behavior. */
  delete proto.createdCallback;
  delete proto.attachedCallback;
  delete proto.detachedCallback;
  return delete proto.attributeChangedCallback;
};

useCapture = true;

listener = function(event) {
  if (document.readyState === 'loading') {
    return;
  }
  registerBrowserPluginElement();
  registerWebViewElement();
  return window.removeEventListener(event.type, listener, useCapture);
};

window.addEventListener('readystatechange', listener, true);

module.exports = WebViewImpl;
<|endoftext|>