<commit_before>const qu = require('async/queue');
const co = require('co');
const moment = require('moment');
const _ = require('lodash');

const constants = require('../common/constants.js');
const ga = require('../common/ga.js');
const cache = require('../common/data-cache.js')();
const serviceUrl = `https://${constants.hostname}`;

const alarmPeriodMinutes = 12 * 60;
const alarmKey = 'macheteSync';
const entityIdKey = 'spEntityIds';
const sellerDomainKey = 'sellerDomains';
const lastVersionKey = 'lastVersion';

chrome.runtime.onInstalled.addListener(details => {
    const manifest = chrome.runtime.getManifest();
    if (details.reason == 'install') {
        chrome.tabs.create({ url: `${serviceUrl}/${process.env.PRODUCT}/welcome` });
    }
    localStorage.setItem(lastVersionKey, manifest.version);
    setAlarm();
});

chrome.pageAction.onClicked.addListener(ga.mcatch(() => {
    chrome.tabs.create({ url: `${serviceUrl}/profile` });
}));

function messageListener(handler) {
    chrome.runtime.onMessage.addListener(ga.mcatch((req, sender, sendResponse) => {
        console.log('Handling message:', req);
        if (sender.tab.incognito) {
            sendResponse({
                error: {
                    handled: true,
                    authError: 'incognito',
                    message: 'Machete cannot be used in incognito mode',
                }
            });
            return true;
        }

        chrome.pageAction.show(sender.tab.id);
        ga.mga('event', 'background-message', req.action);
        const begin = performance.now();

        co(function*() {
            return yield* handler(req, sender);
        })
        .then(data => {
            const response = { data };
            console.log('Success handling message:', req, "response", response);
            sendResponse(response);
        })
        .catch(error => {
            if (handlePortDisconnected(error, req.action))
                return;
            if (handleMessageTooLong(error, req.action))
                return;

            const response = { status: error.message, error: ga.errorToObject(error) };
            const authError = handleServerErrors(error, req.action);
            if (authError) {
                response.error.handled = true;
                response.error.authError = authError;
                console.warn(error);
            }
            else {
                ga.merror(req, error);
            }

            try {
                sendResponse(response);
            }
            catch (ex) {
                handlePortDisconnected(ex, req.action);
            }
        })
        .then(() => {
            const end = performance.now();
            ga.mga('timing', 'Background Task', req.action, Math.round(end - begin));
        });

        return true;
    }));
}

function* getUser() {
    return yield ajax(`${serviceUrl}/api/user`, {
        method: 'GET',
        responseType: 'json'
    });
}

function startSession(req) {
    return co(dataGather(req));
}

function* dataGather(req) {
    console.log('Data sync start at', moment().format());
    const lastSync = JSON.parse(localStorage.getItem('lastSync')) || {};

    // Store entityIds and domains for further use
    if (req.entityId) {
        addEntityId(req.domain, req.entityId);
    }
    else if (req.domain) {
        addSellerDomain(req.domain);
    }

    /* These requires MUST go here to avoid a circular require */
    const kdp = require('./kdp.js'); // eslint-disable-line global-require
    const seller = require('./seller.js'); // eslint-disable-line global-require
    const sp = require('./sp.js'); // eslint-disable-line global-require

    const oldSync = Math.max(..._.values(lastSync));
    let newSync = 0;
    for (const mod of [sp, seller, kdp]) {
        if (!mod.name) {
            console.error('looking at nameless module', mod);
            throw new Error('module has no name');
        }

        if (lastSync[mod.name]) {
            const moduleSync = moment(lastSync[mod.name]);
            console.log('Last data sync for', mod.name, 'at', moduleSync.format());
            if (moment().isSame(moduleSync, 'day')) {
                console.log(mod.name, 'sync is up-to-date');
                continue;
            }
        }
        else {
            console.log('No recorded sync for', mod.name);
        }

        try {
            console.log('Data sync', mod.name, 'start at', moment().format());
            yield* mod.dataGather(req);
            newSync = Date.now();
            lastSync[mod.name] = newSync;
        }
        catch (ex) {
            if (!handleServerErrors(ex, 'dataGather:'+mod.name))
                ga.merror(ex);
        }
        console.log('Data sync', mod.name, 'finish at', moment().format());
    }

    cache.clear();
    localStorage.setItem('lastSync', JSON.stringify(lastSync));

    console.log('Data sync finish at', moment().format());
    return Math.max(newSync, oldSync);
}

function addEntityId(domain, entityId) {
    if (!domain || !entityId) {
        ga.merror("bad arguments to addEntityId:", JSON.stringify([domain, entityId]));
        return;
    }

    const ids = JSON.parse(localStorage.getItem(entityIdKey)) || [];
    if (!ids.find(x => x.entityId == entityId || x == entityId)) {
        ids.push({ domain, entityId });
        localStorage.setItem(entityIdKey, JSON.stringify(ids));
    }
}

function getEntityIds() {
    const entityIds = JSON.parse(localStorage.getItem(entityIdKey)) || [];

    // TODO: This moves old-style entityIds to the new system. Eventually delete
    // this once all users have been updated.
    const toDelete = [];
    for (let i = 0; i < localStorage.length; i++) {
        const key = localStorage.key(i);
        if (key.match(/^campaignData_/)) {
            toDelete.push(key);

            const entityId = key.replace('campaignData_', '');
            if (entityId) {
                entityIds.push({ entityId });
            }
        }
    }
    for (const key of toDelete) {
        localStorage.removeItem(key);
    }

    // Fix up entityIds to ensure that every one has a stored domain
    let ids = entityIds.map(item => {
        if (typeof item == 'string') {
            return { domain: 'ams.amazon.com', entityId: item };
        }
        else if (!item.domain) {
            item.domain = 'ams.amazon.com';
        }
        return item;
    });

    ids = _.uniqBy(ids, 'entityId');
    localStorage.setItem(entityIdKey, JSON.stringify(ids));

    return ids;
}

function addSellerDomain(domain) {
    if (!domain) {
        ga.merror("bad arguments to addEntityId:", JSON.stringify(domain));
        return;
    }

    const ids = JSON.parse(localStorage.getItem(sellerDomainKey)) || [];
    if (!ids.includes(domain)) {
        ids.push(domain);
        localStorage.setItem(sellerDomainKey, JSON.stringify(ids));
    }
}

function getSellerDomains() {
    return JSON.parse(localStorage.getItem(sellerDomainKey)) || [];
}

function setAlarm() {
    chrome.alarms.onAlarm.addListener(ga.mcatch(alarm => {
        if (alarm.name == alarmKey) {
            co(dataGather({})).catch(ga.mex);
        }
    }));
    return ga.mpromise(resolve => {
        chrome.alarms.get(alarmKey, alarm => {
            if (!alarm) {
                const when = Date.now() + 1000;
                chrome.alarms.create(alarmKey, {
                    when,
                    periodInMinutes: alarmPeriodMinutes,
                });
                console.log('set alarm ', alarmKey, 'for', moment(when).format());
                resolve(true);
            }
            resolve(false);
        });
    });
}

function handleServerErrors(ex, desc) {
    if (ex.url && ex.url.match(/machete-app.com/)) {
        if (ex.message.match(/^401/)) {
            ga.mga('event', 'error-handled', 'auth-error-401', desc);
            return 'notLoggedIn';
        }
        if (ex.message.match(/^402/)) {
            ga.mga('event', 'error-handled', 'auth-error-402', desc);
            return 'notAllowed';
        }
        if (ex.message.match(/^403/)) {
            ga.mga('event', 'error-handled', 'auth-error-403', desc);
            return 'notOwned';
        }
    }
    if (ex.message.match(/^401/)) {
        ga.mga('event', 'error-handled', 'network-error-401', desc);
        return 'amazonNotLoggedIn';
    }
    if (ex.message.match(/^404/)) {
        ga.mga('event', 'error-handled', 'network-error-404', desc);
        return 'notFound';
    }
    if (ex.message.match(/^50/)) {
        ga.mga('event', 'error-handled', 'server-error', desc);
        return 'serverError';
    }
    if (ex.message.match(/0 error/)) {
        ga.mga('event', 'error-handled', 'network-error-unknown', desc);
        return 'networkError';
    }
    return null;
}

function handlePortDisconnected(ex, desc) {
    if (ex.message.match(/disconnected port object/)) {
        ga.mga('event', 'error-handled', 'port-disconnected', desc);
        return true;
    }
    return false;
}

function handleMessageTooLong(ex, req) {
    if (ex.message.match(/exceeded maximum allowed length/)) {
        ga.merror(ex.message, req);
        return true;
    }
    return false;
}

function* ajax(url, opts) {
    const init = {
        method: opts.method,
        headers: new Headers(),
        mode: 'cors',
        credentials: 'include',
    };

    if (opts.queryData) {
        const q = new URLSearchParams();
        for (const key of Object.keys(opts.queryData)) {
            q.append(key, opts.queryData[key]);
        }
        url += '?' + q.toString();
    }

    if (opts.formData) {
        init.body = new URLSearchParams();
        for (const key of Object.keys(opts.formData)) {
            init.body.append(key, opts.formData[key]);
        }
        init.headers.set('Content-Type', 'application/x-www-form-urlencoded');
    }
    else if (opts.jsonData) {
        init.body = JSON.stringify(opts.jsonData);
        init.headers.set('Content-Type', 'application/json');
    }

    const origStack = new Error().stack;
    try {
        const response = yield window.fetch(url, init);
        if (!response.ok) {
            throw new Error(`${response.status} ${response.statusText}`);
        }

        if (response.status == 204)
            return null;
        if (opts.responseType == 'json')
            return yield response.json();
        return yield response.text();
    }
    catch (ex) {
        ex.method = opts.method;
        ex.url = url;
        ex.stack = origStack;
        throw ex;
    }
}

function parallelQueue(items, fn) {
    return new Promise((resolve, reject) => {
        const results = [];
        const queue = qu((item, callback) => {
            co(fn(item))
            .then(value => {
                results.push(value);
                callback(null, value);
            })
            .catch(callback);
        }, 3);

        queue.drain = () => resolve(results);
        queue.error = reject;
        queue.push(items);
    });
}

module.exports = {
    serviceUrl,
    messageListener,
    getUser: cache.coMemo(getUser, { maxAge: 2 * constants.timespan.minute }),
    handleServerErrors,
    ajax,
    parallelQueue,
    cache,
    startSession,
    addEntityId,
    getEntityIds,
    addSellerDomain,
    getSellerDomains,
};
<commit_msg>catch basic 'Failed to fetch' errors and log more productively<commit_after>const qu = require('async/queue');
const co = require('co');
const moment = require('moment');
const _ = require('lodash');

const constants = require('../common/constants.js');
const ga = require('../common/ga.js');
const cache = require('../common/data-cache.js')();
const serviceUrl = `https://${constants.hostname}`;

const alarmPeriodMinutes = 12 * 60;
const alarmKey = 'macheteSync';
const entityIdKey = 'spEntityIds';
const sellerDomainKey = 'sellerDomains';
const lastVersionKey = 'lastVersion';

chrome.runtime.onInstalled.addListener(details => {
    const manifest = chrome.runtime.getManifest();
    if (details.reason == 'install') {
        chrome.tabs.create({ url: `${serviceUrl}/${process.env.PRODUCT}/welcome` });
    }
    localStorage.setItem(lastVersionKey, manifest.version);
    setAlarm();
});

chrome.pageAction.onClicked.addListener(ga.mcatch(() => {
    chrome.tabs.create({ url: `${serviceUrl}/profile` });
}));

function messageListener(handler) {
    chrome.runtime.onMessage.addListener(ga.mcatch((req, sender, sendResponse) => {
        console.log('Handling message:', req);
        if (sender.tab.incognito) {
            sendResponse({
                error: {
                    handled: true,
                    authError: 'incognito',
                    message: 'Machete cannot be used in incognito mode',
                }
            });
            return true;
        }

        chrome.pageAction.show(sender.tab.id);
        ga.mga('event', 'background-message', req.action);
        const begin = performance.now();

        co(function*() {
            return yield* handler(req, sender);
        })
        .then(data => {
            const response = { data };
            console.log('Success handling message:', req, "response", response);
            sendResponse(response);
        })
        .catch(error => {
            if (handlePortDisconnected(error, req.action))
                return;
            if (handleMessageTooLong(error, req.action))
                return;

            const response = { status: error.message, error: ga.errorToObject(error) };
            const authError = handleServerErrors(error, req.action);
            if (authError) {
                response.error.handled = true;
                response.error.authError = authError;
                console.warn(error);
            }
            else {
                ga.merror(req, error);
            }

            try {
                sendResponse(response);
            }
            catch (ex) {
                handlePortDisconnected(ex, req.action);
            }
        })
        .then(() => {
            const end = performance.now();
            ga.mga('timing', 'Background Task', req.action, Math.round(end - begin));
        });

        return true;
    }));
}

function* getUser() {
    return yield ajax(`${serviceUrl}/api/user`, {
        method: 'GET',
        responseType: 'json'
    });
}

function startSession(req) {
    return co(dataGather(req));
}

function* dataGather(req) {
    console.log('Data sync start at', moment().format());
    const lastSync = JSON.parse(localStorage.getItem('lastSync')) || {};

    // Store entityIds and domains for further use
    if (req.entityId) {
        addEntityId(req.domain, req.entityId);
    }
    else if (req.domain) {
        addSellerDomain(req.domain);
    }

    /* These requires MUST go here to avoid a circular require */
    const kdp = require('./kdp.js'); // eslint-disable-line global-require
    const seller = require('./seller.js'); // eslint-disable-line global-require
    const sp = require('./sp.js'); // eslint-disable-line global-require

    const oldSync = Math.max(..._.values(lastSync));
    let newSync = 0;
    for (const mod of [sp, seller, kdp]) {
        if (!mod.name) {
            console.error('looking at nameless module', mod);
            throw new Error('module has no name');
        }

        if (lastSync[mod.name]) {
            const moduleSync = moment(lastSync[mod.name]);
            console.log('Last data sync for', mod.name, 'at', moduleSync.format());
            if (moment().isSame(moduleSync, 'day')) {
                console.log(mod.name, 'sync is up-to-date');
                continue;
            }
        }
        else {
            console.log('No recorded sync for', mod.name);
        }

        try {
            console.log('Data sync', mod.name, 'start at', moment().format());
            yield* mod.dataGather(req);
            newSync = Date.now();
            lastSync[mod.name] = newSync;
        }
        catch (ex) {
            if (!handleServerErrors(ex, 'dataGather:'+mod.name))
                ga.merror(ex);
        }
        console.log('Data sync', mod.name, 'finish at', moment().format());
    }

    cache.clear();
    localStorage.setItem('lastSync', JSON.stringify(lastSync));

    console.log('Data sync finish at', moment().format());
    return Math.max(newSync, oldSync);
}

function addEntityId(domain, entityId) {
    if (!domain || !entityId) {
        ga.merror("bad arguments to addEntityId:", JSON.stringify([domain, entityId]));
        return;
    }

    const ids = JSON.parse(localStorage.getItem(entityIdKey)) || [];
    if (!ids.find(x => x.entityId == entityId || x == entityId)) {
        ids.push({ domain, entityId });
        localStorage.setItem(entityIdKey, JSON.stringify(ids));
    }
}

function getEntityIds() {
    const entityIds = JSON.parse(localStorage.getItem(entityIdKey)) || [];

    // TODO: This moves old-style entityIds to the new system. Eventually delete
    // this once all users have been updated.
    const toDelete = [];
    for (let i = 0; i < localStorage.length; i++) {
        const key = localStorage.key(i);
        if (key.match(/^campaignData_/)) {
            toDelete.push(key);

            const entityId = key.replace('campaignData_', '');
            if (entityId) {
                entityIds.push({ entityId });
            }
        }
    }
    for (const key of toDelete) {
        localStorage.removeItem(key);
    }

    // Fix up entityIds to ensure that every one has a stored domain
    let ids = entityIds.map(item => {
        if (typeof item == 'string') {
            return { domain: 'ams.amazon.com', entityId: item };
        }
        else if (!item.domain) {
            item.domain = 'ams.amazon.com';
        }
        return item;
    });

    ids = _.uniqBy(ids, 'entityId');
    localStorage.setItem(entityIdKey, JSON.stringify(ids));

    return ids;
}

function addSellerDomain(domain) {
    if (!domain) {
        ga.merror("bad arguments to addEntityId:", JSON.stringify(domain));
        return;
    }

    const ids = JSON.parse(localStorage.getItem(sellerDomainKey)) || [];
    if (!ids.includes(domain)) {
        ids.push(domain);
        localStorage.setItem(sellerDomainKey, JSON.stringify(ids));
    }
}

function getSellerDomains() {
    return JSON.parse(localStorage.getItem(sellerDomainKey)) || [];
}

function setAlarm() {
    chrome.alarms.onAlarm.addListener(ga.mcatch(alarm => {
        if (alarm.name == alarmKey) {
            co(dataGather({})).catch(ga.mex);
        }
    }));
    return ga.mpromise(resolve => {
        chrome.alarms.get(alarmKey, alarm => {
            if (!alarm) {
                const when = Date.now() + 1000;
                chrome.alarms.create(alarmKey, {
                    when,
                    periodInMinutes: alarmPeriodMinutes,
                });
                console.log('set alarm ', alarmKey, 'for', moment(when).format());
                resolve(true);
            }
            resolve(false);
        });
    });
}

function handleServerErrors(ex, desc) {
    if (ex.url && ex.url.match(/machete-app.com/)) {
        if (ex.message.match(/^401/)) {
            ga.mga('event', 'error-handled', 'auth-error-401', desc);
            return 'notLoggedIn';
        }
        if (ex.message.match(/^402/)) {
            ga.mga('event', 'error-handled', 'auth-error-402', desc);
            return 'notAllowed';
        }
        if (ex.message.match(/^403/)) {
            ga.mga('event', 'error-handled', 'auth-error-403', desc);
            return 'notOwned';
        }
    }
    if (ex.message.match(/^401/)) {
        ga.mga('event', 'error-handled', 'network-error-401', desc);
        return 'amazonNotLoggedIn';
    }
    if (ex.message.match(/^404/)) {
        ga.mga('event', 'error-handled', 'network-error-404', desc);
        return 'notFound';
    }
    if (ex.message.match(/^50/)) {
        ga.mga('event', 'error-handled', 'server-error', desc);
        return 'serverError';
    }
    if (ex.message.match(/0 error/) || ex.message.match(/Failed to fetch/)) {
        ga.mga('event', 'error-handled', 'network-error-unknown', desc);
        return 'networkError';
    }
    return null;
}

function handlePortDisconnected(ex, desc) {
    if (ex.message.match(/disconnected port object/)) {
        ga.mga('event', 'error-handled', 'port-disconnected', desc);
        return true;
    }
    return false;
}

function handleMessageTooLong(ex, req) {
    if (ex.message.match(/exceeded maximum allowed length/)) {
        ga.merror(ex.message, req);
        return true;
    }
    return false;
}

function* ajax(url, opts) {
    const init = {
        method: opts.method,
        headers: new Headers(),
        mode: 'cors',
        credentials: 'include',
    };

    if (opts.queryData) {
        const q = new URLSearchParams();
        for (const key of Object.keys(opts.queryData)) {
            q.append(key, opts.queryData[key]);
        }
        url += '?' + q.toString();
    }

    if (opts.formData) {
        init.body = new URLSearchParams();
        for (const key of Object.keys(opts.formData)) {
            init.body.append(key, opts.formData[key]);
        }
        init.headers.set('Content-Type', 'application/x-www-form-urlencoded');
    }
    else if (opts.jsonData) {
        init.body = JSON.stringify(opts.jsonData);
        init.headers.set('Content-Type', 'application/json');
    }

    const origStack = new Error().stack;
    try {
        const response = yield window.fetch(url, init);
        if (!response.ok) {
            throw new Error(`${response.status} ${response.statusText}`);
        }

        if (response.status == 204)
            return null;
        if (opts.responseType == 'json')
            return yield response.json();
        return yield response.text();
    }
    catch (ex) {
        ex.method = opts.method;
        ex.url = url;
        ex.origStack = origStack;
        throw ex;
    }
}

function parallelQueue(items, fn) {
    return new Promise((resolve, reject) => {
        const results = [];
        const queue = qu((item, callback) => {
            co(fn(item))
            .then(value => {
                results.push(value);
                callback(null, value);
            })
            .catch(callback);
        }, 3);

        queue.drain = () => resolve(results);
        queue.error = reject;
        queue.push(items);
    });
}

module.exports = {
    serviceUrl,
    messageListener,
    getUser: cache.coMemo(getUser, { maxAge: 2 * constants.timespan.minute }),
    handleServerErrors,
    ajax,
    parallelQueue,
    cache,
    startSession,
    addEntityId,
    getEntityIds,
    addSellerDomain,
    getSellerDomains,
};
<|endoftext|>


<commit_before>var React = require('react')
var r = require('r-dom')
var debug = require('debug')('ui:index')
var getDefaultRoleTypes = require('graph-view/get-default-role-types')
var getRoleTypes = require('graph-view/get-role-types')
var getUniLinkLabels = require('graph-view/get-uni-link-labels')
var each = require('lodash.foreach')
var clone = require('lodash.clone')
var contains = require('lodash.contains')
var types = require('types')
var Layout = require('layout')
var extend = require('xtend')

var FourOhFourPage = require('./404')

module.exports = React.createClass({
  getInitialState: function () {
    debug('getInitialState:model.roles', this.props.model.roles)

    var initialState = {
      roleTypes: getRoleTypes(
        getDefaultRoleTypes(),
        this.props.model.roles
      ),
      linkLabels: getUniLinkLabels(this.props.model.relationshipGraph)
    }

    return initialState
  },

  render: function () {
    var route = this.props.route

    debug('linkLabels', this.state.linkLabels)

    var props = extend(
      this.props,
      {
        roleTypes: this.state.roleTypes,
        linkLabels: this.state.linkLabels,
        updateActiveTags: this.updateActiveTags.bind(this)
      }
    )

    var Page
    if (route) {
      Page = types.indexedByCollection[route.collection].Page
    } else {
      Page = FourOhFourPage
    }

    debug('render', props)
    return (
      r(Layout, props,
        r(Page, props)
      )
    )
  },

  componentDidMount: function () {
    this.props.model.on('change', function (model, val) {
      var nextRoleTypes = getRoleTypes(
        this.state.roleTypes,
        model.relationships
      )

      var nextLinkLabels = getUniLinkLabels(model.relationshipGraph)

      this.setState({
        roleTypes: nextRoleTypes,
        linkLabels: nextLinkLabels
      })

      this.forceUpdate()
    }, this)
  },

  updateActiveTags: function (type, activeTags) {
    var tagObject = clone(this.state[type])
    debug('tagObject',  tagObject, type, activeTags)
    var newState = {}

    each(tagObject, function (tagType, tagTypeId) {
      if (contains(activeTags, tagTypeId)) {
        tagType.active = true
      }
      else {
        tagType.active = false
      }

    })

    newState[type] = tagObject
    debug('newState', newState)
    this.setState(newState)
  }
})
<commit_msg>tag control functions in ui/index.js<commit_after>var React = require('react')
var r = require('r-dom')
var debug = require('debug')('ui:index')
var getDefaultRoleTypes = require('graph-view/get-default-role-types')
var getRoleTypes = require('graph-view/get-role-types')
var getUniLinkLabels = require('graph-view/get-uni-link-labels')
var each = require('lodash.foreach')
var clone = require('lodash.clone')
var contains = require('lodash.contains')
var types = require('types')
var Layout = require('layout')
var extend = require('xtend')

var FourOhFourPage = require('./404')

module.exports = React.createClass({
  getInitialState: function () {
    debug('getInitialState:model.roles', this.props.model.roles)

    var initialState = {
      roleTypes: getRoleTypes(
        getDefaultRoleTypes(),
        this.props.model.roles
      ),
      linkLabels: getUniLinkLabels(this.props.model.relationshipGraph)
    }

    return initialState
  },

  render: function () {
    var route = this.props.route

    debug('linkLabels', this.state.linkLabels)

    var props = extend(
      this.props,
      {
        roleTypes: this.state.roleTypes,
        linkLabels: this.state.linkLabels,
        updateActiveTag: this.updateActiveTag.bind(this),
        clearTag: this.clearTag.bind(this)
      }
    )

    var Page
    if (route) {
      Page = types.indexedByCollection[route.collection].Page
    } else {
      Page = FourOhFourPage
    }

    debug('render', props)
    return (
      r(Layout, props,
        r(Page, props)
      )
    )
  },

  componentDidMount: function () {
    this.props.model.on('change', function (model, val) {
      var nextRoleTypes = getRoleTypes(
        this.state.roleTypes,
        model.relationships
      )

      var nextLinkLabels = getUniLinkLabels(model.relationshipGraph)

      this.setState({
        roleTypes: nextRoleTypes,
        linkLabels: nextLinkLabels
      })

      this.forceUpdate()
    }, this)
  },

  // updateActiveTags: function (type, activeTags) {
  //   var tagObject = clone(this.state[type])
  //   debug('tagObject',  tagObject, type, activeTags)
  //   var newState = {}
  //
  //   each(tagObject, function (tagType, tagTypeId) {
  //     if (contains(activeTags, tagTypeId)) {
  //       tagType.active = true
  //     }
  //     else {
  //       tagType.active = false
  //     }
  //
  //   })
  //
  //   newState[type] = tagObject
  //   debug('newState', newState)
  //   this.setState(newState)
  // },

  updateActiveTag: function (type, newActiveTag) {
    var newState = {}
    var tagObject = clone(this.state[type])
    each(tagObject, function (tagType, tagTypeId) {
      if (newActiveTag === tagTypeId || newActiveTag === tagType.model.pluralName ) {
        tagType.active = true
      }
    })
    newState[type] = tagObject
    debug('newState', newState)
    this.setState(newState)
  },

  clearTag: function (type, tag) {
    return function () {
      var newState = {}
      var tagObject = clone(this.state[type])
      each(tagObject, function (tagType, tagTypeId) {
        if (tag === tagTypeId || tag === tagType.model.pluralName ) {
          tagType.active = false
        }
      })
      newState[type] = tagObject
      this.setState(newState)
    }.bind(this)
  }

})
<|endoftext|>


<commit_before>/*
 * weh - WebExtensions Help
 *
 * @summary workflow and base code for developing WebExtensions browser add-ons
 * @author Michel Gutierrez
 * @link https://github.com/mi-g/weh
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */

weh.ui = (function() {

    var manifest = browser.runtime.getManifest();
	var panels = {};

    function Void() {}

 	function UpdateOptions(options) {
        options.type = options.type || "panel";
        options.contentURL = options.contentURL || null;
        options.onShow = options.onShow || Void;
        options.onHide = options.onHide || Void;
        options.onMessage = options.onMessage || Void;
        options.prefs = options.prefs!==false;
        return options;
	}

    function GotoTab(url, callback) {
        var foundTab = false;
        browser.tabs.query({}, function (tabs) {
            tabs.forEach(function (tab) {
                if (tab.url === url) {
                    browser.tabs.update(tab.id, {
                        selected: true
                    });
                    browser.windows.update(tab.windowId, {
                        focused: true
                    });
                    foundTab = true;
                }
            });
            if (callback) callback(foundTab);
        });
    }

	browser.runtime.onConnect.addListener(function(port) {
		var m = /^weh:panel:(.*):(.*)/.exec(port.name);
		if(!m)
			return;
        if(m[1]!=browser.runtime.id)
            return;
		var panelName = m[2];
		var panel = panels[panelName];

		if(!panel)
            panel = panels[panelName] = {
                options: UpdateOptions({}),
            }
        panel.open = true;

		panel.post = function(message) {
            if(panel.open)
                port.postMessage(message);
		}

		port.onMessage.addListener(function(message) {
			switch(message.type) {
                case "weh#on":
                    panel.open = true;
                    panel.post({
                        type: "weh#panel-info",
                        details: {
                            panelType: panel.options.type
                        }
                    });
                    if(panel.options.prefs) {
                        panel.post({
                            type: "weh#prefs-specs",
                            specs: weh.prefs.getSpecs()
                        });
                        panel.post({
                            type: "weh#prefs",
                            prefs: weh.prefs.getAll()
                        });
                    }
                    break;
                case "weh#prefs":
                    if(message.prefs)
                        for(var k in message.prefs)
                            weh.prefs[k] = message.prefs[k];
                    break;
                case "weh#get-prefs":
                    var prefs = {};
                    if(typeof message.prefs=="undefined")
                        prefs = weh.prefs.getAll();
                    else {
                        if(!Array.isArray(message.prefs))
                            message.prefs = [message.prefs];
                        message.prefs.forEach(function(pref) {
                            prefs[pref] = weh.prefs[pref];
                        });
                    }
                    panel.post({
                        type: "weh#prefs",
                        prefs: prefs
                    });
                    break;
                case "weh#get-prefs-specs":
                    var specs = {};
                    var allSpecs = weh.prefs.getSpecs();
                    if(typeof message.specs=="undefined")
                        specs = allSpecs;
                    else {
                        if(!Array.isArray(message.specs))
                            message.specs = [message.specs];
                        message.specs.forEach(function(pref) {
                            specs[pref] = allSpecs[pref];
                        });
                        panel.post({
                            type: "weh#prefs-specs",
                            specs: specs
                        });
                    }
                    break;
                default:
				    panel.options.onMessage(message,panel.post,panelName);
			}
		});
		port.onDisconnect.addListener(function(msg) {
			panel.open = false;
			panel.options.onHide(panelName);
		});
		panel.options.onShow(panel.post,panelName);
	});

    weh.prefs.on("",function(pref,value) {
        console.info("ui prefs change",pref,value);
        for(var k in panels) {
            var panel = panels[k];
            if(panel.open) {
                var prefs = {};
                prefs[pref] = value;
                panel.post({
                    type: "weh#prefs",
                    prefs: prefs
                });
            }
        }
    });

    function UrlAddIds(url,panelName) {
        var m = /^([^\?]*)(?:\?(.*))?$/.exec(url);
        var params = {};
        if(m[2])
            m[2].split("&").forEach(function(paramExpr) {
                var terms = paramExpr.split("=");
                params[terms[0]] = decodeURIComponent(terms[1]);
            });
        params["panel"] = panelName;
        params["addon"] = browser.runtime.id;
        var paramExprs = [];
        for(var param in params)
            paramExprs.push(param + "=" + encodeURIComponent(params[param]));
        return m[1] + "?" + paramExprs.join("&");
    }

    var defaultPopup = manifest.browser_action && manifest.browser_action.default_popup;
    if(defaultPopup) {
        defaultPopup = UrlAddIds(defaultPopup,"default");
        browser.browserAction.setPopup({
            popup: defaultPopup
        });
    }

    return {

        update: function(panelName,options) {
            var panel = panels[panelName] || {
                open: false,
                options: options,
            }
            UpdateOptions(panel.options);
            panels[panelName] = panel;
            return panel;
        },

        open: function(panelName, options) {
            options = options || {};
            var panel = panels[panelName] || this.update(panelName,options);
            UpdateOptions(panel.options);

            if(panel.options.type=="panel")
                browser.windows.getCurrent(function(currentWindow) {
                    var width = 500;
                    var cwcParam = {
                        url: browser.extension.getURL(panel.options.contentURL+"?panel="+panelName+"&addon="+browser.runtime.id),
                        type: "detached_panel",
                        //width: width,
                        //height: 100,
                        left: Math.round((currentWindow.width-width)/2+currentWindow.left),
                        top: currentWindow.top,
                    };
                    if(weh.is("chrome","opera"))
                        cwcParam.focused = true;
                    if(weh.is("edge","opera"))
                        cwcParam.type = "popup";

                    browser.windows.create(cwcParam,function(window) {
                        panels[panelName].windowId = window.id;
                        function OnFocusChanged(focusedWindowId) {
                            browser.windows.getCurrent(function(currentWindow) {
                                if(currentWindow.id!=window.id)
                                    try {
                                    browser.windows.remove(window.id,function() {});
                                    } catch(e) { }
                            });
                        }
                        function OnRemoved(removedWindowId) {
                            if(removedWindowId==window.id) {
                                browser.windows.onFocusChanged.removeListener(OnFocusChanged);
                                browser.windows.onFocusChanged.removeListener(OnRemoved);
                            }
                        }
                        browser.windows.onFocusChanged.addListener(OnFocusChanged);
                        browser.windows.onRemoved.addListener(OnRemoved);
                    });
                });
            else if(panel.options.type=="tab") {
                var url = browser.extension.getURL(panel.options.contentURL+"?panel="+panelName+"&addon="+browser.runtime.id);
                GotoTab(url,function(foundTab) {
                    if(!foundTab)
                        browser.tabs.create({
                            url: url,
                        },function(tabs) {});
                });
            } else if(panel.options.type=="popup") {
                console.warn("weh: a popup cannot be opened programmatically");
            }
        },

        close: function(panelName) {
            var panel = panels[panelName];
            if(panel && panel.open)
                panel.post({
                    type: "weh#close"
                });
        },

        post: function() {
            var message;
            if(typeof arguments[0]=="string") {
                var panelName = arguments[0];
                message = arguments[1];
                var panel = panels[panelName];
                if(panel && panel.post)
                    panel.post(message);
            } else {
                message = arguments[0];
                for(var panelName in panels) {
                    var panel = panels[panelName];
                    if(panel.post)
                        panel.post(message);
                }
            }
        },

    }

})();
<commit_msg>edge fix<commit_after>/*
 * weh - WebExtensions Help
 *
 * @summary workflow and base code for developing WebExtensions browser add-ons
 * @author Michel Gutierrez
 * @link https://github.com/mi-g/weh
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */

weh.ui = (function() {

    var manifest = browser.runtime.getManifest();
	var panels = {};

    function Void() {}

 	function UpdateOptions(options) {
        options.type = options.type || "panel";
        options.contentURL = options.contentURL || null;
        options.onShow = options.onShow || Void;
        options.onHide = options.onHide || Void;
        options.onMessage = options.onMessage || Void;
        options.prefs = options.prefs!==false;
        return options;
	}

    function GotoTab(url, callback) {
        var foundTab = false;
        browser.tabs.query({}, function (tabs) {
            tabs.forEach(function (tab) {
                if (tab.url === url) {
                    browser.tabs.update(tab.id, {
                        selected: true
                    });
                    browser.windows.update(tab.windowId, {
                        focused: true
                    });
                    foundTab = true;
                }
            });
            if (callback) callback(foundTab);
        });
    }

	browser.runtime.onConnect.addListener(function(port) {
		var m = /^weh:panel:(.*):(.*)/.exec(port.name);
		if(!m)
			return;
        if(m[1]!=browser.runtime.id)
            return;
		var panelName = m[2];
		var panel = panels[panelName];

		if(!panel)
            panel = panels[panelName] = {
                name: panelName,
                options: UpdateOptions({}),
            }
        panel.open = true;

		panel.post = function(message) {
            try {
                if(panel.open)
                    port.postMessage(message);
            } catch(e) {
                /* Edge bug: port disconnect event not sent on panel closed */
                panel.open = false;
            }
		}

		port.onMessage.addListener(function(message) {
			switch(message.type) {
                case "weh#on":
                    panel.open = true;
                    panel.post({
                        type: "weh#panel-info",
                        details: {
                            panelType: panel.options.type
                        }
                    });
                    if(panel.options.prefs) {
                        panel.post({
                            type: "weh#prefs-specs",
                            specs: weh.prefs.getSpecs()
                        });
                        panel.post({
                            type: "weh#prefs",
                            prefs: weh.prefs.getAll()
                        });
                    }
                    break;
                case "weh#prefs":
                    if(message.prefs)
                        for(var k in message.prefs)
                            weh.prefs[k] = message.prefs[k];
                    break;
                case "weh#get-prefs":
                    var prefs = {};
                    if(typeof message.prefs=="undefined")
                        prefs = weh.prefs.getAll();
                    else {
                        if(!Array.isArray(message.prefs))
                            message.prefs = [message.prefs];
                        message.prefs.forEach(function(pref) {
                            prefs[pref] = weh.prefs[pref];
                        });
                    }
                    panel.post({
                        type: "weh#prefs",
                        prefs: prefs
                    });
                    break;
                case "weh#get-prefs-specs":
                    var specs = {};
                    var allSpecs = weh.prefs.getSpecs();
                    if(typeof message.specs=="undefined")
                        specs = allSpecs;
                    else {
                        if(!Array.isArray(message.specs))
                            message.specs = [message.specs];
                        message.specs.forEach(function(pref) {
                            specs[pref] = allSpecs[pref];
                        });
                        panel.post({
                            type: "weh#prefs-specs",
                            specs: specs
                        });
                    }
                    break;
                default:
				    panel.options.onMessage(message,panel.post,panelName);
			}
		});
		port.onDisconnect.addListener(function(msg) {
			panel.open = false;
			panel.options.onHide(panelName);
		});
		panel.options.onShow(panel.post,panelName);
	});

    weh.prefs.on("",function(pref,value) {
        for(var k in panels) {
            var panel = panels[k];
            if(panel.open) {
                var prefs = {};
                prefs[pref] = value;
                panel.post({
                    type: "weh#prefs",
                    prefs: prefs
                });
            }
        }
    });

    function UrlAddIds(url,panelName) {
        var m = /^([^\?]*)(?:\?(.*))?$/.exec(url);
        var params = {};
        if(m[2])
            m[2].split("&").forEach(function(paramExpr) {
                var terms = paramExpr.split("=");
                params[terms[0]] = decodeURIComponent(terms[1]);
            });
        params["panel"] = panelName;
        params["addon"] = browser.runtime.id;
        var paramExprs = [];
        for(var param in params)
            paramExprs.push(param + "=" + encodeURIComponent(params[param]));
        return m[1] + "?" + paramExprs.join("&");
    }

    var defaultPopup = manifest.browser_action && manifest.browser_action.default_popup;
    if(defaultPopup) {
        defaultPopup = UrlAddIds(defaultPopup,"default");
        browser.browserAction.setPopup({
            popup: defaultPopup
        });
    }

    return {

        update: function(panelName,options) {
            var panel = panels[panelName] || {
                name: panelName,
                open: false,
                options: options,
            }
            UpdateOptions(panel.options);
            panels[panelName] = panel;
            return panel;
        },

        open: function(panelName, options) {
            options = options || {};
            var panel = panels[panelName] || this.update(panelName,options);
            UpdateOptions(panel.options);

            if(panel.options.type=="panel")
                browser.windows.getCurrent(function(currentWindow) {
                    var width = 500;
                    var cwcParam = {
                        url: browser.extension.getURL(panel.options.contentURL+"?panel="+panelName+"&addon="+browser.runtime.id),
                        type: "detached_panel",
                        left: Math.round((currentWindow.width-width)/2+currentWindow.left),
                        top: currentWindow.top,
                    };
                    if(weh.is("chrome","opera"))
                        cwcParam.focused = true;
                    if(weh.is("edge","opera"))
                        cwcParam.type = "popup";

                    browser.windows.create(cwcParam,function(window) {
                        panels[panelName].windowId = window.id;
                        function OnFocusChanged(focusedWindowId) {
                            browser.windows.getCurrent(function(currentWindow) {
                                if(currentWindow.id!=window.id)
                                    try {
                                    browser.windows.remove(window.id,function() {});
                                    } catch(e) { }
                            });
                        }
                        function OnRemoved(removedWindowId) {
                            if(removedWindowId==window.id) {
                                browser.windows.onFocusChanged.removeListener(OnFocusChanged);
                                browser.windows.onFocusChanged.removeListener(OnRemoved);
                            }
                        }
                        browser.windows.onFocusChanged.addListener(OnFocusChanged);
                        browser.windows.onRemoved.addListener(OnRemoved);
                    });
                });
            else if(panel.options.type=="tab") {
                var url = browser.extension.getURL(panel.options.contentURL+"?panel="+panelName+"&addon="+browser.runtime.id);
                GotoTab(url,function(foundTab) {
                    if(!foundTab)
                        browser.tabs.create({
                            url: url,
                        },function(tabs) {});
                });
            } else if(panel.options.type=="popup") {
                console.warn("weh: a popup cannot be opened programmatically");
            }
        },

        close: function(panelName) {
            var panel = panels[panelName];
            if(panel && panel.open)
                panel.post({
                    type: "weh#close"
                });
        },

        post: function() {
            var message;
            if(typeof arguments[0]=="string") {
                var panelName = arguments[0];
                message = arguments[1];
                var panel = panels[panelName];
                if(panel && panel.post)
                    panel.post(message);
            } else {
                message = arguments[0];
                for(var panelName in panels) {
                    var panel = panels[panelName];
                    if(panel.post)
                        panel.post(message);
                }
            }
        },

    }

})();
<|endoftext|>


<commit_before>/**
 * @module ol/control/FullScreen
 */
import Control from './Control.js';
import EventType from '../events/EventType.js';
import {CLASS_CONTROL, CLASS_UNSELECTABLE, CLASS_UNSUPPORTED} from '../css.js';
import {listen} from '../events.js';
import {replaceNode} from '../dom.js';

const events = [
  'fullscreenchange',
  'webkitfullscreenchange',
  'MSFullscreenChange',
];

/**
 * @enum {string}
 */
const FullScreenEventType = {
  /**
   * Triggered after the map entered fullscreen.
   * @event FullScreenEventType#enterfullscreen
   * @api
   */
  ENTERFULLSCREEN: 'enterfullscreen',

  /**
   * Triggered after the map leave fullscreen.
   * @event FullScreenEventType#leavefullscreen
   * @api
   */
  LEAVEFULLSCREEN: 'leavefullscreen',
};

/**
 * @typedef {Object} Options
 * @property {string} [className='ol-full-screen'] CSS class name.
 * @property {string|Text} [label='\u2922'] Text label to use for the button.
 * Instead of text, also an element (e.g. a `span` element) can be used.
 * @property {string|Text} [labelActive='\u00d7'] Text label to use for the
 * button when full-screen is active.
 * @property {string} [activeClassName=className + '-true'] CSS class name for the button
 * when full-screen is active.
 * @property {string} [inactiveClassName=className + '-false'] CSS class name for the button
 * when full-screen is inactive.
 * Instead of text, also an element (e.g. a `span` element) can be used.
 * @property {string} [tipLabel='Toggle full-screen'] Text label to use for the button tip.
 * @property {boolean} [keys=false] Full keyboard access.
 * @property {HTMLElement|string} [target] Specify a target if you want the
 * control to be rendered outside of the map's viewport.
 * @property {HTMLElement|string} [source] The element to be displayed
 * fullscreen. When not provided, the element containing the map viewport will
 * be displayed fullscreen.
 */

/**
 * @classdesc
 * Provides a button that when clicked fills up the full screen with the map.
 * The full screen source element is by default the element containing the map viewport unless
 * overridden by providing the `source` option. In which case, the dom
 * element introduced using this parameter will be displayed in full screen.
 *
 * When in full screen mode, a close button is shown to exit full screen mode.
 * The [Fullscreen API](http://www.w3.org/TR/fullscreen/) is used to
 * toggle the map in full screen mode.
 *
 * @fires FullScreenEventType#enterfullscreen
 * @fires FullScreenEventType#leavefullscreen
 * @api
 */
class FullScreen extends Control {
  /**
   * @param {Options=} opt_options Options.
   */
  constructor(opt_options) {
    const options = opt_options ? opt_options : {};

    super({
      element: document.createElement('div'),
      target: options.target,
    });

    /**
     * @private
     * @type {string}
     */
    this.cssClassName_ =
      options.className !== undefined ? options.className : 'ol-full-screen';

    /**
     * @private
     * @type {string}
     */
    this.activeClassName_ =
      options.activeClassName !== undefined
        ? options.activeClassName.split(' ')
        : [this.cssClassName_ + '-true'];

    /**
     * @private
     * @type {string}
     */
    this.inactiveClassName_ =
      options.inactiveClassName !== undefined
        ? options.inactiveClassName.split(' ')
        : [this.cssClassName_ + '-false'];

    const label = options.label !== undefined ? options.label : '\u2922';

    /**
     * @private
     * @type {Text}
     */
    this.labelNode_ =
      typeof label === 'string' ? document.createTextNode(label) : label;

    const labelActive =
      options.labelActive !== undefined ? options.labelActive : '\u00d7';

    /**
     * @private
     * @type {Text}
     */
    this.labelActiveNode_ =
      typeof labelActive === 'string'
        ? document.createTextNode(labelActive)
        : labelActive;

    /**
     * @private
     * @type {HTMLElement}
     */
    this.button_ = document.createElement('button');

    const tipLabel = options.tipLabel ? options.tipLabel : 'Toggle full-screen';
    this.setClassName_(this.button_, isFullScreen());
    this.button_.setAttribute('type', 'button');
    this.button_.title = tipLabel;
    this.button_.appendChild(this.labelNode_);

    this.button_.addEventListener(
      EventType.CLICK,
      this.handleClick_.bind(this),
      false
    );

    const cssClasses =
      this.cssClassName_ +
      ' ' +
      CLASS_UNSELECTABLE +
      ' ' +
      CLASS_CONTROL +
      ' ' +
      (!isFullScreenSupported() ? CLASS_UNSUPPORTED : '');
    const element = this.element;
    element.className = cssClasses;
    element.appendChild(this.button_);

    /**
     * @private
     * @type {boolean}
     */
    this.keys_ = options.keys !== undefined ? options.keys : false;

    /**
     * @private
     * @type {HTMLElement|string|undefined}
     */
    this.source_ = options.source;
  }

  /**
   * @param {MouseEvent} event The event to handle
   * @private
   */
  handleClick_(event) {
    event.preventDefault();
    this.handleFullScreen_();
  }

  /**
   * @private
   */
  handleFullScreen_() {
    if (!isFullScreenSupported()) {
      return;
    }
    const map = this.getMap();
    if (!map) {
      return;
    }
    if (isFullScreen()) {
      exitFullScreen();
    } else {
      let element;
      if (this.source_) {
        element =
          typeof this.source_ === 'string'
            ? document.getElementById(this.source_)
            : this.source_;
      } else {
        element = map.getTargetElement();
      }
      if (this.keys_) {
        requestFullScreenWithKeys(element);
      } else {
        requestFullScreen(element);
      }
    }
  }

  /**
   * @private
   */
  handleFullScreenChange_() {
    const map = this.getMap();
    if (isFullScreen()) {
      this.setClassName_(this.button_, true);
      replaceNode(this.labelActiveNode_, this.labelNode_);
      this.dispatchEvent(FullScreenEventType.ENTERFULLSCREEN);
    } else {
      this.setClassName_(this.button_, false);
      replaceNode(this.labelNode_, this.labelActiveNode_);
      this.dispatchEvent(FullScreenEventType.LEAVEFULLSCREEN);
    }
    if (map) {
      map.updateSize();
    }
  }

  /**
   * @param {HTMLElement} element Target element
   * @param {boolean} fullscreen True if fullscreen class name should be active
   * @private
   */
  setClassName_(element, fullscreen) {
    const activeClassName = this.activeClassName_;
    const inactiveClassName = this.inactiveClassName_;
    const nextClassName = fullscreen ? activeClassName : inactiveClassName;
    element.classList.remove(...activeClassName);
    element.classList.remove(...inactiveClassName);
    element.classList.add(...nextClassName);
  }

  /**
   * Remove the control from its current map and attach it to the new map.
   * Subclasses may set up event handlers to get notified about changes to
   * the map here.
   * @param {import("../PluggableMap.js").default} map Map.
   * @api
   */
  setMap(map) {
    super.setMap(map);
    if (map) {
      for (let i = 0, ii = events.length; i < ii; ++i) {
        this.listenerKeys.push(
          listen(document, events[i], this.handleFullScreenChange_, this)
        );
      }
    }
  }
}

/**
 * @return {boolean} Fullscreen is supported by the current platform.
 */
function isFullScreenSupported() {
  const body = document.body;
  return !!(
    body['webkitRequestFullscreen'] ||
    (body['msRequestFullscreen'] && document['msFullscreenEnabled']) ||
    (body.requestFullscreen && document.fullscreenEnabled)
  );
}

/**
 * @return {boolean} Element is currently in fullscreen.
 */
function isFullScreen() {
  return !!(
    document['webkitIsFullScreen'] ||
    document['msFullscreenElement'] ||
    document.fullscreenElement
  );
}

/**
 * Request to fullscreen an element.
 * @param {HTMLElement} element Element to request fullscreen
 */
function requestFullScreen(element) {
  if (element.requestFullscreen) {
    element.requestFullscreen();
  } else if (element['msRequestFullscreen']) {
    element['msRequestFullscreen']();
  } else if (element['webkitRequestFullscreen']) {
    element['webkitRequestFullscreen']();
  }
}

/**
 * Request to fullscreen an element with keyboard input.
 * @param {HTMLElement} element Element to request fullscreen
 */
function requestFullScreenWithKeys(element) {
  if (element['webkitRequestFullscreen']) {
    element['webkitRequestFullscreen']();
  } else {
    requestFullScreen(element);
  }
}

/**
 * Exit fullscreen.
 */
function exitFullScreen() {
  if (document.exitFullscreen) {
    document.exitFullscreen();
  } else if (document['msExitFullscreen']) {
    document['msExitFullscreen']();
  } else if (document['webkitExitFullscreen']) {
    document['webkitExitFullscreen']();
  }
}

export default FullScreen;
<commit_msg>fix comments<commit_after>/**
 * @module ol/control/FullScreen
 */
import Control from './Control.js';
import EventType from '../events/EventType.js';
import {CLASS_CONTROL, CLASS_UNSELECTABLE, CLASS_UNSUPPORTED} from '../css.js';
import {listen} from '../events.js';
import {replaceNode} from '../dom.js';

const events = [
  'fullscreenchange',
  'webkitfullscreenchange',
  'MSFullscreenChange',
];

/**
 * @enum {string}
 */
const FullScreenEventType = {
  /**
   * Triggered after the map entered fullscreen.
   * @event FullScreenEventType#enterfullscreen
   * @api
   */
  ENTERFULLSCREEN: 'enterfullscreen',

  /**
   * Triggered after the map leave fullscreen.
   * @event FullScreenEventType#leavefullscreen
   * @api
   */
  LEAVEFULLSCREEN: 'leavefullscreen',
};

/**
 * @typedef {Object} Options
 * @property {string} [className='ol-full-screen'] CSS class name.
 * @property {string|Text} [label='\u2922'] Text label to use for the button.
 * Instead of text, also an element (e.g. a `span` element) can be used.
 * @property {string|Text} [labelActive='\u00d7'] Text label to use for the
 * button when full-screen is active.
 * @property {string} [activeClassName=className + '-true'] CSS class name for the button
 * when full-screen is active.
 * @property {string} [inactiveClassName=className + '-false'] CSS class name for the button
 * when full-screen is inactive.
 * Instead of text, also an element (e.g. a `span` element) can be used.
 * @property {string} [tipLabel='Toggle full-screen'] Text label to use for the button tip.
 * @property {boolean} [keys=false] Full keyboard access.
 * @property {HTMLElement|string} [target] Specify a target if you want the
 * control to be rendered outside of the map's viewport.
 * @property {HTMLElement|string} [source] The element to be displayed
 * fullscreen. When not provided, the element containing the map viewport will
 * be displayed fullscreen.
 */

/**
 * @classdesc
 * Provides a button that when clicked fills up the full screen with the map.
 * The full screen source element is by default the element containing the map viewport unless
 * overridden by providing the `source` option. In which case, the dom
 * element introduced using this parameter will be displayed in full screen.
 *
 * When in full screen mode, a close button is shown to exit full screen mode.
 * The [Fullscreen API](http://www.w3.org/TR/fullscreen/) is used to
 * toggle the map in full screen mode.
 *
 * @fires FullScreenEventType#enterfullscreen
 * @fires FullScreenEventType#leavefullscreen
 * @api
 */
class FullScreen extends Control {
  /**
   * @param {Options=} opt_options Options.
   */
  constructor(opt_options) {
    const options = opt_options ? opt_options : {};

    super({
      element: document.createElement('div'),
      target: options.target,
    });

    /**
     * @private
     * @type {string}
     */
    this.cssClassName_ =
      options.className !== undefined ? options.className : 'ol-full-screen';

    /**
     * @private
     * @type {Array<string>}
     */
    this.activeClassName_ =
      options.activeClassName !== undefined
        ? options.activeClassName.split(' ')
        : [this.cssClassName_ + '-true'];

    /**
     * @private
     * @type {Array<string>}
     */
    this.inactiveClassName_ =
      options.inactiveClassName !== undefined
        ? options.inactiveClassName.split(' ')
        : [this.cssClassName_ + '-false'];

    const label = options.label !== undefined ? options.label : '\u2922';

    /**
     * @private
     * @type {Text}
     */
    this.labelNode_ =
      typeof label === 'string' ? document.createTextNode(label) : label;

    const labelActive =
      options.labelActive !== undefined ? options.labelActive : '\u00d7';

    /**
     * @private
     * @type {Text}
     */
    this.labelActiveNode_ =
      typeof labelActive === 'string'
        ? document.createTextNode(labelActive)
        : labelActive;

    /**
     * @private
     * @type {HTMLElement}
     */
    this.button_ = document.createElement('button');

    const tipLabel = options.tipLabel ? options.tipLabel : 'Toggle full-screen';
    this.setClassName_(this.button_, isFullScreen());
    this.button_.setAttribute('type', 'button');
    this.button_.title = tipLabel;
    this.button_.appendChild(this.labelNode_);

    this.button_.addEventListener(
      EventType.CLICK,
      this.handleClick_.bind(this),
      false
    );

    const cssClasses =
      this.cssClassName_ +
      ' ' +
      CLASS_UNSELECTABLE +
      ' ' +
      CLASS_CONTROL +
      ' ' +
      (!isFullScreenSupported() ? CLASS_UNSUPPORTED : '');
    const element = this.element;
    element.className = cssClasses;
    element.appendChild(this.button_);

    /**
     * @private
     * @type {boolean}
     */
    this.keys_ = options.keys !== undefined ? options.keys : false;

    /**
     * @private
     * @type {HTMLElement|string|undefined}
     */
    this.source_ = options.source;
  }

  /**
   * @param {MouseEvent} event The event to handle
   * @private
   */
  handleClick_(event) {
    event.preventDefault();
    this.handleFullScreen_();
  }

  /**
   * @private
   */
  handleFullScreen_() {
    if (!isFullScreenSupported()) {
      return;
    }
    const map = this.getMap();
    if (!map) {
      return;
    }
    if (isFullScreen()) {
      exitFullScreen();
    } else {
      let element;
      if (this.source_) {
        element =
          typeof this.source_ === 'string'
            ? document.getElementById(this.source_)
            : this.source_;
      } else {
        element = map.getTargetElement();
      }
      if (this.keys_) {
        requestFullScreenWithKeys(element);
      } else {
        requestFullScreen(element);
      }
    }
  }

  /**
   * @private
   */
  handleFullScreenChange_() {
    const map = this.getMap();
    if (isFullScreen()) {
      this.setClassName_(this.button_, true);
      replaceNode(this.labelActiveNode_, this.labelNode_);
      this.dispatchEvent(FullScreenEventType.ENTERFULLSCREEN);
    } else {
      this.setClassName_(this.button_, false);
      replaceNode(this.labelNode_, this.labelActiveNode_);
      this.dispatchEvent(FullScreenEventType.LEAVEFULLSCREEN);
    }
    if (map) {
      map.updateSize();
    }
  }

  /**
   * @param {HTMLElement} element Target element
   * @param {boolean} fullscreen True if fullscreen class name should be active
   * @private
   */
  setClassName_(element, fullscreen) {
    const activeClassName = this.activeClassName_;
    const inactiveClassName = this.inactiveClassName_;
    const nextClassName = fullscreen ? activeClassName : inactiveClassName;
    element.classList.remove(...activeClassName);
    element.classList.remove(...inactiveClassName);
    element.classList.add(...nextClassName);
  }

  /**
   * Remove the control from its current map and attach it to the new map.
   * Subclasses may set up event handlers to get notified about changes to
   * the map here.
   * @param {import("../PluggableMap.js").default} map Map.
   * @api
   */
  setMap(map) {
    super.setMap(map);
    if (map) {
      for (let i = 0, ii = events.length; i < ii; ++i) {
        this.listenerKeys.push(
          listen(document, events[i], this.handleFullScreenChange_, this)
        );
      }
    }
  }
}

/**
 * @return {boolean} Fullscreen is supported by the current platform.
 */
function isFullScreenSupported() {
  const body = document.body;
  return !!(
    body['webkitRequestFullscreen'] ||
    (body['msRequestFullscreen'] && document['msFullscreenEnabled']) ||
    (body.requestFullscreen && document.fullscreenEnabled)
  );
}

/**
 * @return {boolean} Element is currently in fullscreen.
 */
function isFullScreen() {
  return !!(
    document['webkitIsFullScreen'] ||
    document['msFullscreenElement'] ||
    document.fullscreenElement
  );
}

/**
 * Request to fullscreen an element.
 * @param {HTMLElement} element Element to request fullscreen
 */
function requestFullScreen(element) {
  if (element.requestFullscreen) {
    element.requestFullscreen();
  } else if (element['msRequestFullscreen']) {
    element['msRequestFullscreen']();
  } else if (element['webkitRequestFullscreen']) {
    element['webkitRequestFullscreen']();
  }
}

/**
 * Request to fullscreen an element with keyboard input.
 * @param {HTMLElement} element Element to request fullscreen
 */
function requestFullScreenWithKeys(element) {
  if (element['webkitRequestFullscreen']) {
    element['webkitRequestFullscreen']();
  } else {
    requestFullScreen(element);
  }
}

/**
 * Exit fullscreen.
 */
function exitFullScreen() {
  if (document.exitFullscreen) {
    document.exitFullscreen();
  } else if (document['msExitFullscreen']) {
    document['msExitFullscreen']();
  } else if (document['webkitExitFullscreen']) {
    document['webkitExitFullscreen']();
  }
}

export default FullScreen;
<|endoftext|>


<commit_before>/**
 * @module ol/renderer/canvas/Map
 */
import {CLASS_UNSELECTABLE} from '../css.js';
import {visibleAtResolution} from '../layer/Layer.js';
import RenderEvent from '../render/Event.js';
import RenderEventType from '../render/EventType.js';
import MapRenderer from './Map.js';
import SourceState from '../source/State.js';
import {replaceChildren} from '../dom.js';

/**
 * @classdesc
 * Canvas map renderer.
 * @api
 */
class CompositeMapRenderer extends MapRenderer {

  /**
   * @param {import("../PluggableMap.js").default} map Map.
   */
  constructor(map) {
    super(map);

    /**
     * @private
     * @type {HTMLDivElement}
     */
    this.element_ = document.createElement('div');
    const style = this.element_.style;
    style.width = '100%';
    style.height = '100%';

    this.element_.className = CLASS_UNSELECTABLE + ' ol-layers';

    const container = map.getViewport();
    container.insertBefore(this.element_, container.firstChild || null);

    /**
     * @private
     * @type {Array<HTMLElement>}
     */
    this.children_ = [];

    /**
     * @private
     * @type {boolean}
     */
    this.renderedVisible_ = true;
  }

  /**
   * @param {import("../render/EventType.js").default} type Event type.
   * @param {import("../PluggableMap.js").FrameState} frameState Frame state.
   */
  dispatchRenderEvent(type, frameState) {
    const map = this.getMap();
    if (map.hasListener(type)) {
      const event = new RenderEvent(type, undefined, frameState);
      map.dispatchEvent(event);
    }
  }

  /**
   * @inheritDoc
   */
  renderFrame(frameState) {
    if (!frameState) {
      if (this.renderedVisible_) {
        this.element_.style.display = 'none';
        this.renderedVisible_ = false;
      }
      return;
    }

    this.calculateMatrices2D(frameState);
    this.dispatchRenderEvent(RenderEventType.PRECOMPOSE, frameState);

    const layerStatesArray = frameState.layerStatesArray;
    const viewResolution = frameState.viewState.resolution;

    this.children_.length = 0;
    for (let i = 0, ii = layerStatesArray.length; i < ii; ++i) {
      const layerState = layerStatesArray[i];
      if (!visibleAtResolution(layerState, viewResolution) || layerState.sourceState != SourceState.READY) {
        continue;
      }

      const layer = layerState.layer;
      const element = layer.render(frameState);
      if (element) {
        const zIndex = layerState.zIndex;
        if (zIndex !== element.style.zIndex) {
          element.style.zIndex = zIndex;
        }
        this.children_.push(element);
      }
    }

    replaceChildren(this.element_, this.children_);

    this.dispatchRenderEvent(RenderEventType.POSTCOMPOSE, frameState);

    if (!this.renderedVisible_) {
      this.element_.style.display = '';
      this.renderedVisible_ = true;
    }

    this.scheduleRemoveUnusedLayerRenderers(frameState);
    this.scheduleExpireIconCache(frameState);
  }

  /**
   * @inheritDoc
   */
  forEachLayerAtPixel(pixel, frameState, hitTolerance, callback, layerFilter) {
    const viewState = frameState.viewState;
    const viewResolution = viewState.resolution;

    const layerStates = frameState.layerStatesArray;
    const numLayers = layerStates.length;

    for (let i = numLayers - 1; i >= 0; --i) {
      const layerState = layerStates[i];
      const layer = layerState.layer;
      if (visibleAtResolution(layerState, viewResolution) && layerFilter(layer)) {
        const layerRenderer = this.getLayerRenderer(layer);
        if (!layerRenderer) {
          continue;
        }
        const data = layerRenderer.getDataAtPixel(pixel, frameState, hitTolerance);
        if (data) {
          const result = callback(layer, data);
          if (result) {
            return result;
          }
        }
      }
    }
    return undefined;
  }

}

export default CompositeMapRenderer;
<commit_msg>Avoid layers getting rendered above the overlay container<commit_after>/**
 * @module ol/renderer/canvas/Map
 */
import {CLASS_UNSELECTABLE} from '../css.js';
import {visibleAtResolution} from '../layer/Layer.js';
import RenderEvent from '../render/Event.js';
import RenderEventType from '../render/EventType.js';
import MapRenderer from './Map.js';
import SourceState from '../source/State.js';
import {replaceChildren} from '../dom.js';

/**
 * @classdesc
 * Canvas map renderer.
 * @api
 */
class CompositeMapRenderer extends MapRenderer {

  /**
   * @param {import("../PluggableMap.js").default} map Map.
   */
  constructor(map) {
    super(map);

    /**
     * @private
     * @type {HTMLDivElement}
     */
    this.element_ = document.createElement('div');
    const style = this.element_.style;
    style.width = '100%';
    style.height = '100%';
    style.position = 'relative';
    style.zIndex = '0';

    this.element_.className = CLASS_UNSELECTABLE + ' ol-layers';

    const container = map.getViewport();
    container.insertBefore(this.element_, container.firstChild || null);

    /**
     * @private
     * @type {Array<HTMLElement>}
     */
    this.children_ = [];

    /**
     * @private
     * @type {boolean}
     */
    this.renderedVisible_ = true;
  }

  /**
   * @param {import("../render/EventType.js").default} type Event type.
   * @param {import("../PluggableMap.js").FrameState} frameState Frame state.
   */
  dispatchRenderEvent(type, frameState) {
    const map = this.getMap();
    if (map.hasListener(type)) {
      const event = new RenderEvent(type, undefined, frameState);
      map.dispatchEvent(event);
    }
  }

  /**
   * @inheritDoc
   */
  renderFrame(frameState) {
    if (!frameState) {
      if (this.renderedVisible_) {
        this.element_.style.display = 'none';
        this.renderedVisible_ = false;
      }
      return;
    }

    this.calculateMatrices2D(frameState);
    this.dispatchRenderEvent(RenderEventType.PRECOMPOSE, frameState);

    const layerStatesArray = frameState.layerStatesArray;
    const viewResolution = frameState.viewState.resolution;

    this.children_.length = 0;
    for (let i = 0, ii = layerStatesArray.length; i < ii; ++i) {
      const layerState = layerStatesArray[i];
      if (!visibleAtResolution(layerState, viewResolution) || layerState.sourceState != SourceState.READY) {
        continue;
      }

      const layer = layerState.layer;
      const element = layer.render(frameState);
      if (element) {
        const zIndex = layerState.zIndex;
        if (zIndex !== element.style.zIndex) {
          element.style.zIndex = zIndex;
        }
        this.children_.push(element);
      }
    }

    replaceChildren(this.element_, this.children_);

    this.dispatchRenderEvent(RenderEventType.POSTCOMPOSE, frameState);

    if (!this.renderedVisible_) {
      this.element_.style.display = '';
      this.renderedVisible_ = true;
    }

    this.scheduleRemoveUnusedLayerRenderers(frameState);
    this.scheduleExpireIconCache(frameState);
  }

  /**
   * @inheritDoc
   */
  forEachLayerAtPixel(pixel, frameState, hitTolerance, callback, layerFilter) {
    const viewState = frameState.viewState;
    const viewResolution = viewState.resolution;

    const layerStates = frameState.layerStatesArray;
    const numLayers = layerStates.length;

    for (let i = numLayers - 1; i >= 0; --i) {
      const layerState = layerStates[i];
      const layer = layerState.layer;
      if (visibleAtResolution(layerState, viewResolution) && layerFilter(layer)) {
        const layerRenderer = this.getLayerRenderer(layer);
        if (!layerRenderer) {
          continue;
        }
        const data = layerRenderer.getDataAtPixel(pixel, frameState, hitTolerance);
        if (data) {
          const result = callback(layer, data);
          if (result) {
            return result;
          }
        }
      }
    }
    return undefined;
  }

}

export default CompositeMapRenderer;
<|endoftext|>


<commit_before>const prompts = require('./prompts')
const { merge, pipe, assoc, omit, __ } = require('ramda')
const { getReactNativeVersion } = require('../lib/react-native-version')
const Insight = require('../lib/insight')
const generateFiles = require('./files')
const fs = require('fs-extra')
const pkg = require('../../package')

/**
 * Is Android installed?
 *
 * $ANDROID_HOME/tools folder has to exist.
 *
 * @param {*} context - The gluegun context.
 * @returns {boolean}
 */
const isAndroidInstalled = function (context) {
  const androidHome = process.env['ANDROID_HOME']
  const hasAndroidEnv = !context.strings.isBlank(androidHome)
  const hasAndroid = hasAndroidEnv && context.filesystem.exists(`${androidHome}/tools`) === 'dir'

  return Boolean(hasAndroid)
}

/**
 * Let's install.
 *
 * @param {any} context - The gluegun context.
 */
async function install (context) {
  const {
    filesystem,
    parameters,
    ignite,
    reactNative,
    print,
    system,
    prompt,
    template
  } = context

  const perfStart = (new Date()).getTime()

  const name = parameters.third
  const spinner = print
    .spin(`Generating a React Native client for JHipster apps`)
    .succeed()

  let props = {
    jhipsterDirectory: parameters.options['jh-dir'] || '',
    disableInsight: parameters.options['disable-insight'] || false
  }
  let jhipsterConfig
  let jhipsterDirectory
  let jhipsterConfigPath

  if (props.jhipsterDirectory) {
    if (!fs.existsSync(`../${props.jhipsterDirectory}/.yo-rc.json`)) {
      print.error(`No JHipster configuration file found at ${props.jhipsterDirectory}/.yo-rc.json`)
      return
    }
    print.success(`Found the JHipster config at ${props.jhipsterDirectory}/.yo-rc.json`)
    jhipsterConfig = await fs.readJson(`../${props.jhipsterDirectory}/.yo-rc.json`)
    jhipsterDirectory = props.jhipsterDirectory
    jhipsterConfigPath = `${jhipsterDirectory}/.yo-rc.json`
  } else {
    // prompt the user until an JHipster configuration file is found
    while (true) {
      let jhipsterPathAnswer = await prompt.ask(prompts.jhipsterPath)
      // strip the trailing slash from the directory
      jhipsterDirectory = `${jhipsterPathAnswer.filePath}`.replace(/\/$/, ``)
      jhipsterConfigPath = `${jhipsterDirectory}/.yo-rc.json`
      print.info(`Looking for ${jhipsterConfigPath}`)
      if (fs.existsSync(`../${jhipsterConfigPath}`)) {
        print.success(`Found JHipster config file at ${jhipsterConfigPath}`)
        jhipsterConfig = await fs.readJson(`../${jhipsterConfigPath}`)
        break
      } else {
        print.error(`Could not find JHipster config file, please try again.`)
      }
    }
  }

  if (!props.disableInsight && Insight.insight.optOut === undefined) {
    Insight.insight.optOut = !((await prompt.ask(prompts.insight)).insight)
  }

  props.skipGit = parameters.options['skip-git']
  props.skipLint = parameters.options['skip-lint']

  // very hacky but correctly handles both strings and booleans and converts to boolean
  props.disableInsight = JSON.parse(props.disableInsight)

  // attempt to install React Native or die trying
  const rnInstall = await reactNative.install({
    name,
    version: getReactNativeVersion(context)
  })
  if (rnInstall.exitCode > 0) process.exit(rnInstall.exitCode)

  // remove the __tests__ directory that come with React Native
  filesystem.remove('__tests__')
  filesystem.remove('App.js')

  props.name = name
  props.igniteVersion = ignite.version
  props.reactNativeVersion = rnInstall.version
  props.jhipsterDirectory = `../${props.jhipsterDirectory}`
  props.authType = jhipsterConfig['generator-jhipster'].authenticationType
  props.searchEngine = !!jhipsterConfig['generator-jhipster'].searchEngine
  props.websockets = !!jhipsterConfig['generator-jhipster'].websocket
  props.socialLogin = !!jhipsterConfig['generator-jhipster'].enableSocialSignIn
  props.packageVersion = pkg.version
  await generateFiles(context, props, jhipsterConfig)

  /**
   * Merge the package.json from our template into the one provided from react-native init.
   */
  async function mergePackageJsons () {
    // transform our package.json incase we need to replace variables
    const rawJson = await template.generate({
      directory: `${ignite.ignitePluginPath()}/boilerplate`,
      template: 'package.json.ejs',
      props: props
    })
    const newPackageJson = JSON.parse(rawJson)

    // read in the react-native created package.json
    const currentPackage = filesystem.read('package.json', 'json')

    // deep merge, lol
    const newPackage = pipe(
      assoc(
        'dependencies',
        merge(currentPackage.dependencies, newPackageJson.dependencies)
      ),
      assoc(
        'devDependencies',
        merge(currentPackage.devDependencies, newPackageJson.devDependencies)
      ),
      assoc('scripts', merge(currentPackage.scripts, newPackageJson.scripts)),
      merge(
        __,
        omit(['dependencies', 'devDependencies', 'scripts'], newPackageJson)
      )
    )(currentPackage)

    // write this out
    filesystem.write('package.json', newPackage, { jsonIndent: 2 })
  }
  await mergePackageJsons()

  spinner.stop()

  // react native link -- must use spawn & stdio: ignore or it hangs!! :(
  spinner.text = `▸ linking native libraries`
  spinner.start()
  await system.spawn('react-native link', { stdio: 'ignore' })
  spinner.stop()

  // pass long the debug flag if we're running in that mode
  const debugFlag = parameters.options.debug ? '--debug' : ''

  /**
   * Append to files
   */
  // https://github.com/facebook/react-native/issues/12724
  filesystem.appendAsync('.gitattributes', '*.bat text eol=crlf')
  filesystem.append('.gitignore', 'coverage/')
  filesystem.append('.gitignore', '\n# Misc\n#')
  filesystem.append('.gitignore', '.env\n')
  filesystem.append('.gitignore', 'ios/Index/DataStore\n')

  try {
    const ignitePluginConfigPath = `${__dirname}/ignite.json`
    const newConfig = filesystem.read(ignitePluginConfigPath, 'json')
    ignite.setIgnitePluginPath(__dirname)
    ignite.saveIgniteConfig(newConfig)

    fs.mkdirSync(`.jhipster`)
    fs.writeJsonSync('.jhipster/yo-rc.json', jhipsterConfig, { spaces: '\t' })
    print.success(`JHipster config saved to your app's .jhipster folder.`)

    // link vector-icons again so it for sure links
    await system.spawn('react-native link react-native-vector-icons')

    // todo move any addModule calls directly into package.json.ejs (good first contribution)
    if (props.authType === 'session' || props.authType === 'uaa') {
      await ignite.addModule('react-native-cookies', {version: '3.2.0', link: true})
    }

    if (props.websockets) {
      // install websocket dependencies
      await ignite.addModule('stompjs', { version: '2.3.3' })
      // this is a github module for a react-native specific fix that hasn't been released yet
      await ignite.addModule('sockjs-client', { version: 'https://github.com/sockjs/sockjs-client#4d18fd56a6c4fb476c3e1931543a6cb9daaa6eba' })
      await ignite.addModule('net', { version: '1.0.2' })
    }

    if (props.socialLogin) {
      // install social login dependencies
      await ignite.addModule('react-native-simple-auth', { version: '2.2.0' })
    }

    if (!props.skipLint) {
      await system.spawn(`ignite add standard ${debugFlag}`, { stdio: 'inherit' })
      // ignore the ignite folder
      let pkg = filesystem.read(`package.json`, 'json')
      pkg.standard['ignore'] = [ 'ignite/**' ]
      filesystem.write(`package.json`, pkg)
    }
  } catch (e) {
    ignite.log(e)
    throw e
  }

  // git configuration
  const gitExists = await filesystem.exists('./.git')
  if (!gitExists && !props.skipGit && system.which('git')) {
    // initial git
    const spinner = print.spin('configuring git')

    // TODO: Add git hooks flag.  Disabled by default for now
    // "husky": "0.14.3",
    const huskyCmd = '' // `&& node node_modules/husky/bin/install .`
    system.run(`git init . && git add . && git commit -m "Initial commit." ${huskyCmd}`)

    spinner.succeed(`configured git`)
  }

  const perfDuration = parseInt(((new Date()).getTime() - perfStart) / 10) / 100
  spinner.succeed(`ignited ${print.colors.yellow(name)} in ${perfDuration}s`)

  Insight.trackAppOptions(context, props)

  // Wrap it up with our success message.
  print.info('')
  print.info('🍽 Time to get cooking!')
  print.info('')
  if (props.websockets) {
    print.info('To enable the websockets example, see docs/websockets.md')
    print.info('')
  }
  if (props.socialLogin) {
    print.info('To configure Social Login, see docs/social-login.md')
    print.info('')
  }
  if (props.authType === 'oauth2') {
    print.info('To configure OAuth2 OIDC Login, see docs/oauth2-oidc.md')
    print.info('')
  }
  print.info('To run in iOS:')
  print.info(print.colors.bold(`  cd ${name}`))
  print.info(print.colors.bold('  react-native run-ios'))
  print.info('')
  if (isAndroidInstalled(context)) {
    print.info('To run in Android:')
  } else {
    print.info(`To run in Android, make sure you've followed the latest react-native setup instructions at https://facebook.github.io/react-native/docs/getting-started.html before using ignite.\nYou won't be able to run ${print.colors.bold('react-native run-android')} successfully until you have. Then:`)
  }
  print.info(print.colors.bold(`  cd ${name}`))
  print.info(print.colors.bold('  react-native run-android'))
  print.info('')
  print.info('To see what JHipster generators are available:')
  print.info(print.colors.bold(`  cd ${name}`))
  print.info(print.colors.bold('  ignite generate'))
  print.info('')
}

module.exports = {
  install
}
<commit_msg>use release of sockjs-client instead of specific commit<commit_after>const prompts = require('./prompts')
const { merge, pipe, assoc, omit, __ } = require('ramda')
const { getReactNativeVersion } = require('../lib/react-native-version')
const Insight = require('../lib/insight')
const generateFiles = require('./files')
const fs = require('fs-extra')
const pkg = require('../../package')

/**
 * Is Android installed?
 *
 * $ANDROID_HOME/tools folder has to exist.
 *
 * @param {*} context - The gluegun context.
 * @returns {boolean}
 */
const isAndroidInstalled = function (context) {
  const androidHome = process.env['ANDROID_HOME']
  const hasAndroidEnv = !context.strings.isBlank(androidHome)
  const hasAndroid = hasAndroidEnv && context.filesystem.exists(`${androidHome}/tools`) === 'dir'

  return Boolean(hasAndroid)
}

/**
 * Let's install.
 *
 * @param {any} context - The gluegun context.
 */
async function install (context) {
  const {
    filesystem,
    parameters,
    ignite,
    reactNative,
    print,
    system,
    prompt,
    template
  } = context

  const perfStart = (new Date()).getTime()

  const name = parameters.third
  const spinner = print
    .spin(`Generating a React Native client for JHipster apps`)
    .succeed()

  let props = {
    jhipsterDirectory: parameters.options['jh-dir'] || '',
    disableInsight: parameters.options['disable-insight'] || false
  }
  let jhipsterConfig
  let jhipsterDirectory
  let jhipsterConfigPath

  if (props.jhipsterDirectory) {
    if (!fs.existsSync(`../${props.jhipsterDirectory}/.yo-rc.json`)) {
      print.error(`No JHipster configuration file found at ${props.jhipsterDirectory}/.yo-rc.json`)
      return
    }
    print.success(`Found the JHipster config at ${props.jhipsterDirectory}/.yo-rc.json`)
    jhipsterConfig = await fs.readJson(`../${props.jhipsterDirectory}/.yo-rc.json`)
    jhipsterDirectory = props.jhipsterDirectory
    jhipsterConfigPath = `${jhipsterDirectory}/.yo-rc.json`
  } else {
    // prompt the user until an JHipster configuration file is found
    while (true) {
      let jhipsterPathAnswer = await prompt.ask(prompts.jhipsterPath)
      // strip the trailing slash from the directory
      jhipsterDirectory = `${jhipsterPathAnswer.filePath}`.replace(/\/$/, ``)
      jhipsterConfigPath = `${jhipsterDirectory}/.yo-rc.json`
      print.info(`Looking for ${jhipsterConfigPath}`)
      if (fs.existsSync(`../${jhipsterConfigPath}`)) {
        print.success(`Found JHipster config file at ${jhipsterConfigPath}`)
        jhipsterConfig = await fs.readJson(`../${jhipsterConfigPath}`)
        break
      } else {
        print.error(`Could not find JHipster config file, please try again.`)
      }
    }
  }

  if (!props.disableInsight && Insight.insight.optOut === undefined) {
    Insight.insight.optOut = !((await prompt.ask(prompts.insight)).insight)
  }

  props.skipGit = parameters.options['skip-git']
  props.skipLint = parameters.options['skip-lint']

  // very hacky but correctly handles both strings and booleans and converts to boolean
  props.disableInsight = JSON.parse(props.disableInsight)

  // attempt to install React Native or die trying
  const rnInstall = await reactNative.install({
    name,
    version: getReactNativeVersion(context)
  })
  if (rnInstall.exitCode > 0) process.exit(rnInstall.exitCode)

  // remove the __tests__ directory that come with React Native
  filesystem.remove('__tests__')
  filesystem.remove('App.js')

  props.name = name
  props.igniteVersion = ignite.version
  props.reactNativeVersion = rnInstall.version
  props.jhipsterDirectory = `../${props.jhipsterDirectory}`
  props.authType = jhipsterConfig['generator-jhipster'].authenticationType
  props.searchEngine = !!jhipsterConfig['generator-jhipster'].searchEngine
  props.websockets = !!jhipsterConfig['generator-jhipster'].websocket
  props.socialLogin = !!jhipsterConfig['generator-jhipster'].enableSocialSignIn
  props.packageVersion = pkg.version
  await generateFiles(context, props, jhipsterConfig)

  /**
   * Merge the package.json from our template into the one provided from react-native init.
   */
  async function mergePackageJsons () {
    // transform our package.json incase we need to replace variables
    const rawJson = await template.generate({
      directory: `${ignite.ignitePluginPath()}/boilerplate`,
      template: 'package.json.ejs',
      props: props
    })
    const newPackageJson = JSON.parse(rawJson)

    // read in the react-native created package.json
    const currentPackage = filesystem.read('package.json', 'json')

    // deep merge, lol
    const newPackage = pipe(
      assoc(
        'dependencies',
        merge(currentPackage.dependencies, newPackageJson.dependencies)
      ),
      assoc(
        'devDependencies',
        merge(currentPackage.devDependencies, newPackageJson.devDependencies)
      ),
      assoc('scripts', merge(currentPackage.scripts, newPackageJson.scripts)),
      merge(
        __,
        omit(['dependencies', 'devDependencies', 'scripts'], newPackageJson)
      )
    )(currentPackage)

    // write this out
    filesystem.write('package.json', newPackage, { jsonIndent: 2 })
  }
  await mergePackageJsons()

  spinner.stop()

  // react native link -- must use spawn & stdio: ignore or it hangs!! :(
  spinner.text = `▸ linking native libraries`
  spinner.start()
  await system.spawn('react-native link', { stdio: 'ignore' })
  spinner.stop()

  // pass long the debug flag if we're running in that mode
  const debugFlag = parameters.options.debug ? '--debug' : ''

  /**
   * Append to files
   */
  // https://github.com/facebook/react-native/issues/12724
  filesystem.appendAsync('.gitattributes', '*.bat text eol=crlf')
  filesystem.append('.gitignore', 'coverage/')
  filesystem.append('.gitignore', '\n# Misc\n#')
  filesystem.append('.gitignore', '.env\n')
  filesystem.append('.gitignore', 'ios/Index/DataStore\n')

  try {
    const ignitePluginConfigPath = `${__dirname}/ignite.json`
    const newConfig = filesystem.read(ignitePluginConfigPath, 'json')
    ignite.setIgnitePluginPath(__dirname)
    ignite.saveIgniteConfig(newConfig)

    fs.mkdirSync(`.jhipster`)
    fs.writeJsonSync('.jhipster/yo-rc.json', jhipsterConfig, { spaces: '\t' })
    print.success(`JHipster config saved to your app's .jhipster folder.`)

    // link vector-icons again so it for sure links
    await system.spawn('react-native link react-native-vector-icons')

    // todo move any addModule calls directly into package.json.ejs (good first contribution)
    if (props.authType === 'session' || props.authType === 'uaa') {
      await ignite.addModule('react-native-cookies', {version: '3.2.0', link: true})
    }

    if (props.websockets) {
      // install websocket dependencies
      await ignite.addModule('stompjs', { version: '2.3.3' })
      // this is a github module for a react-native specific fix that hasn't been released yet
      await ignite.addModule('sockjs-client', { version: '1.1.5' })
      await ignite.addModule('net', { version: '1.0.2' })
    }

    if (props.socialLogin) {
      // install social login dependencies
      await ignite.addModule('react-native-simple-auth', { version: '2.2.0' })
    }

    if (!props.skipLint) {
      await system.spawn(`ignite add standard ${debugFlag}`, { stdio: 'inherit' })
      // ignore the ignite folder
      let pkg = filesystem.read(`package.json`, 'json')
      pkg.standard['ignore'] = [ 'ignite/**' ]
      filesystem.write(`package.json`, pkg)
    }
  } catch (e) {
    ignite.log(e)
    throw e
  }

  // git configuration
  const gitExists = await filesystem.exists('./.git')
  if (!gitExists && !props.skipGit && system.which('git')) {
    // initial git
    const spinner = print.spin('configuring git')

    // TODO: Add git hooks flag.  Disabled by default for now
    // "husky": "0.14.3",
    const huskyCmd = '' // `&& node node_modules/husky/bin/install .`
    system.run(`git init . && git add . && git commit -m "Initial commit." ${huskyCmd}`)

    spinner.succeed(`configured git`)
  }

  const perfDuration = parseInt(((new Date()).getTime() - perfStart) / 10) / 100
  spinner.succeed(`ignited ${print.colors.yellow(name)} in ${perfDuration}s`)

  Insight.trackAppOptions(context, props)

  // Wrap it up with our success message.
  print.info('')
  print.info('🍽 Time to get cooking!')
  print.info('')
  if (props.websockets) {
    print.info('To enable the websockets example, see docs/websockets.md')
    print.info('')
  }
  if (props.socialLogin) {
    print.info('To configure Social Login, see docs/social-login.md')
    print.info('')
  }
  if (props.authType === 'oauth2') {
    print.info('To configure OAuth2 OIDC Login, see docs/oauth2-oidc.md')
    print.info('')
  }
  print.info('To run in iOS:')
  print.info(print.colors.bold(`  cd ${name}`))
  print.info(print.colors.bold('  react-native run-ios'))
  print.info('')
  if (isAndroidInstalled(context)) {
    print.info('To run in Android:')
  } else {
    print.info(`To run in Android, make sure you've followed the latest react-native setup instructions at https://facebook.github.io/react-native/docs/getting-started.html before using ignite.\nYou won't be able to run ${print.colors.bold('react-native run-android')} successfully until you have. Then:`)
  }
  print.info(print.colors.bold(`  cd ${name}`))
  print.info(print.colors.bold('  react-native run-android'))
  print.info('')
  print.info('To see what JHipster generators are available:')
  print.info(print.colors.bold(`  cd ${name}`))
  print.info(print.colors.bold('  ignite generate'))
  print.info('')
}

module.exports = {
  install
}
<|endoftext|>