<commit_before>const ManagerBase = require('../index.js').ManagerBase;

describe("ManagerBase", function() {
    beforeEach(function() {
        this.managerBase = new ManagerBase();
    });

    it('comm_target_name', function() {
        expect(ManagerBase.comm_target_name).to.be.undefined;
        expect(this.managerBase.comm_target_name).to.equal('ipython.widget');
    });

    it('register_widget_model', function() {
        expect(ManagerBase.register_widget_model).to.not.be.undefined;
        expect(() => ManagerBase.register_widget_model('test', {})).to.not.throw(Error);
    });

    it('register_widget_view', function() {
        expect(ManagerBase.register_widget_view).to.not.be.undefined;
        expect(() => ManagerBase.register_widget_view('test', {})).to.not.throw(Error);
    });

    it('createStyleTag', function() {
        expect(this.managerBase.createStyleTag).to.not.be.undefined;
        expect(this.managerBase.createStyleTag).to.not.throw(Error);
        expect(this.managerBase.createStyleTag().tagName).to.equal('STYLE');
    });

    it('_create_comm', function() {
        expect(this.managerBase._create_comm).to.not.be.undefined;
        return this.managerBase._create_comm().then(() => {
            assert.ok(false, 'Promise should fail');
        }).catch(err => {});
    });

    it('_get_comm_info', function() {
        expect(this.managerBase._get_comm_info).to.not.be.undefined;
        return this.managerBase._get_comm_info().then(() => {
            assert.ok(false, 'Promise should fail');
        }).catch(err => {});
    });

    it('get_state', function() {
        expect(this.managerBase.get_state).to.not.be.undefined;
    });

    it('set_state', function() {
        expect(this.managerBase.set_state).to.not.be.undefined;
    });

    it('new_model', function() {
        expect(this.managerBase.new_model).to.not.be.undefined;
    });

    it('new_widget', function() {
        expect(this.managerBase.new_widget).to.not.be.undefined;
    });

    it('handle_comm_open', function() {
        expect(this.managerBase.handle_comm_open).to.not.be.undefined;
    });

    it('get_model', function() {
        expect(this.managerBase.get_model).to.not.be.undefined;
    });

    it('callbacks', function() {
        expect(this.managerBase.callbacks).to.not.be.undefined;
        expect(this.managerBase.callbacks).to.not.throw(Error);

        // Cell-less call
        let c = this.managerBase.callbacks();
        expect(c).to.be.an('object');
        expect(c.iopub).to.be.undefined;

        // Spoof a call with a cell
        c = this.managerBase.callbacks({ options: { cell: true } });
        expect(c).to.be.an('object');
        expect(c.iopub).to.be.an('object');
        expect(c.iopub.output).to.not.be.undefined;
        expect(c.iopub.clear_output).to.not.be.undefined;
        expect(c.iopub.get_cell).to.be.an('function');
    });

    it('create_view', function() {
        expect(this.managerBase.create_view).to.not.be.undefined;
    });

    it('loadClass', function() {
        expect(this.managerBase.loadClass).to.not.be.undefined;
    });

    it('display_view', function() {
        expect(this.managerBase.display_view).to.not.be.undefined;
        expect(this.managerBase.display_view).to.throw(Error);
    });

    it('display_model', function() {
        expect(this.managerBase.loadClass).to.not.be.undefined;
    });
});
<commit_msg>Remove createStyleTag test (because the func is gone)<commit_after>const ManagerBase = require('../index.js').ManagerBase;

describe("ManagerBase", function() {
    beforeEach(function() {
        this.managerBase = new ManagerBase();
    });

    it('comm_target_name', function() {
        expect(ManagerBase.comm_target_name).to.be.undefined;
        expect(this.managerBase.comm_target_name).to.equal('ipython.widget');
    });

    it('register_widget_model', function() {
        expect(ManagerBase.register_widget_model).to.not.be.undefined;
        expect(() => ManagerBase.register_widget_model('test', {})).to.not.throw(Error);
    });

    it('register_widget_view', function() {
        expect(ManagerBase.register_widget_view).to.not.be.undefined;
        expect(() => ManagerBase.register_widget_view('test', {})).to.not.throw(Error);
    });

    it('_create_comm', function() {
        expect(this.managerBase._create_comm).to.not.be.undefined;
        return this.managerBase._create_comm().then(() => {
            assert.ok(false, 'Promise should fail');
        }).catch(err => {});
    });

    it('_get_comm_info', function() {
        expect(this.managerBase._get_comm_info).to.not.be.undefined;
        return this.managerBase._get_comm_info().then(() => {
            assert.ok(false, 'Promise should fail');
        }).catch(err => {});
    });

    it('get_state', function() {
        expect(this.managerBase.get_state).to.not.be.undefined;
    });

    it('set_state', function() {
        expect(this.managerBase.set_state).to.not.be.undefined;
    });

    it('new_model', function() {
        expect(this.managerBase.new_model).to.not.be.undefined;
    });

    it('new_widget', function() {
        expect(this.managerBase.new_widget).to.not.be.undefined;
    });

    it('handle_comm_open', function() {
        expect(this.managerBase.handle_comm_open).to.not.be.undefined;
    });

    it('get_model', function() {
        expect(this.managerBase.get_model).to.not.be.undefined;
    });

    it('callbacks', function() {
        expect(this.managerBase.callbacks).to.not.be.undefined;
        expect(this.managerBase.callbacks).to.not.throw(Error);

        // Cell-less call
        let c = this.managerBase.callbacks();
        expect(c).to.be.an('object');
        expect(c.iopub).to.be.undefined;

        // Spoof a call with a cell
        c = this.managerBase.callbacks({ options: { cell: true } });
        expect(c).to.be.an('object');
        expect(c.iopub).to.be.an('object');
        expect(c.iopub.output).to.not.be.undefined;
        expect(c.iopub.clear_output).to.not.be.undefined;
        expect(c.iopub.get_cell).to.be.an('function');
    });

    it('create_view', function() {
        expect(this.managerBase.create_view).to.not.be.undefined;
    });

    it('loadClass', function() {
        expect(this.managerBase.loadClass).to.not.be.undefined;
    });

    it('display_view', function() {
        expect(this.managerBase.display_view).to.not.be.undefined;
        expect(this.managerBase.display_view).to.throw(Error);
    });

    it('display_model', function() {
        expect(this.managerBase.loadClass).to.not.be.undefined;
    });
});
<|endoftext|>


<commit_before>module.exports = simpleGet

var http = require('http')
var https = require('https')
var once = require('once')
var url = require('url')
var unzipResponse = require('unzip-response') // excluded from browser build
var objectAssign = require('object-assign')

function simpleGet (opts, cb) {
  opts = typeof opts === 'string' ? { url: opts } : objectAssign({}, opts)
  cb = once(cb)

  // Follow redirects
  if (opts.maxRedirects === 0) {
    cb(new Error('too many redirects'))
    return
  }
  if (!opts.maxRedirects) opts.maxRedirects = 10

  if (opts.url) parseOptsUrl(opts)
  if (!opts.headers) opts.headers = {}

  var body = opts.body
  opts.body = undefined
  if (body && !opts.method) opts.method = 'POST'

  // Request gzip/deflate
  var customAcceptEncoding = Object.keys(opts.headers).some(function (h) {
    return h.toLowerCase() === 'accept-encoding'
  })
  if (!customAcceptEncoding) opts.headers['accept-encoding'] = 'gzip, deflate'

  // Support http: and https: urls
  var protocol = opts.protocol === 'https:' ? https : http
  var req = protocol.request(opts, function (res) {
    // Follow 3xx redirects
    if (res.statusCode >= 300 && res.statusCode < 400 && 'location' in res.headers) {
      opts.url = res.headers.location
      parseOptsUrl(opts)
      res.resume() // Discard response
      opts.maxRedirects -= 1
      return simpleGet(opts, cb)
    }

    cb(null, typeof unzipResponse === 'function' ? unzipResponse(res) : res)
  })
  req.on('error', cb)
  req.end(body)
  return req
}

module.exports.concat = function (opts, cb) {
  return simpleGet(opts, function (err, res) {
    if (err) return cb(err)
    var chunks = []
    res.on('data', function (chunk) {
      chunks.push(chunk)
    })
    res.on('end', function () {
      cb(null, Buffer.concat(chunks), res)
    })
  })
}

;['get', 'post', 'put', 'patch', 'head', 'delete'].forEach(function (method) {
  module.exports[method] = function (opts, cb) {
    if (typeof opts === 'string') opts = { url: opts }
    opts.method = method.toUpperCase()
    return simpleGet(opts, cb)
  }
})

function parseOptsUrl (opts) {
  var loc = url.parse(opts.url)
  if (loc.hostname) opts.hostname = loc.hostname
  if (loc.port) opts.port = loc.port
  if (loc.protocol) opts.protocol = loc.protocol
  opts.path = loc.path
  delete opts.url
}
<commit_msg>allow maxRedirects === 0<commit_after>module.exports = simpleGet

var http = require('http')
var https = require('https')
var once = require('once')
var url = require('url')
var unzipResponse = require('unzip-response') // excluded from browser build
var objectAssign = require('object-assign')

function simpleGet (opts, cb) {
  opts = typeof opts === 'string' ? { url: opts } : objectAssign({}, opts)
  cb = once(cb)

  if (opts.url) parseOptsUrl(opts)
  if (opts.headers == null) opts.headers = {}
  if (opts.maxRedirects == null) opts.maxRedirects = 10

  var body = opts.body
  opts.body = undefined
  if (body && !opts.method) opts.method = 'POST'

  // Request gzip/deflate
  var customAcceptEncoding = Object.keys(opts.headers).some(function (h) {
    return h.toLowerCase() === 'accept-encoding'
  })
  if (!customAcceptEncoding) opts.headers['accept-encoding'] = 'gzip, deflate'

  // Support http: and https: urls
  var protocol = opts.protocol === 'https:' ? https : http
  var req = protocol.request(opts, function (res) {
    // Follow 3xx redirects
    if (res.statusCode >= 300 && res.statusCode < 400 && 'location' in res.headers) {
      opts.url = res.headers.location
      parseOptsUrl(opts)
      res.resume() // Discard response

      opts.maxRedirects -= 1
      if (opts.maxRedirects > 0) simpleGet(opts, cb)
      else cb(new Error('too many redirects'))

      return
    }

    cb(null, typeof unzipResponse === 'function' ? unzipResponse(res) : res)
  })
  req.on('error', cb)
  req.end(body)
  return req
}

module.exports.concat = function (opts, cb) {
  return simpleGet(opts, function (err, res) {
    if (err) return cb(err)
    var chunks = []
    res.on('data', function (chunk) {
      chunks.push(chunk)
    })
    res.on('end', function () {
      cb(null, Buffer.concat(chunks), res)
    })
  })
}

;['get', 'post', 'put', 'patch', 'head', 'delete'].forEach(function (method) {
  module.exports[method] = function (opts, cb) {
    if (typeof opts === 'string') opts = { url: opts }
    opts.method = method.toUpperCase()
    return simpleGet(opts, cb)
  }
})

function parseOptsUrl (opts) {
  var loc = url.parse(opts.url)
  if (loc.hostname) opts.hostname = loc.hostname
  if (loc.port) opts.port = loc.port
  if (loc.protocol) opts.protocol = loc.protocol
  opts.path = loc.path
  delete opts.url
}
<|endoftext|>


<commit_before>import { createSelector } from 'reselect';
import isEmpty from 'lodash/isEmpty';
import uniqBy from 'lodash/uniqBy';
import groupBy from 'lodash/groupBy';
import intersection from 'lodash/intersection';
import { CALCULATION_OPTIONS } from 'app/data/constants';

import {
  getYColumnValue,
  getThemeConfig,
  getTooltipConfig,
  sortEmissionsByValue,
  sortLabelByAlpha,
  getColorPalette
} from 'utils/graphs';

// constants needed for data parsing
const DATA_SCALE = 1000000;

const BASE_COLORS = ['#25597C', '#DFE9ED'];

const AXES_CONFIG = {
  xBottom: {
    name: 'Year',
    unit: 'date',
    format: 'YYYY'
  },
  yLeft: {
    name: 'Emissions',
    unit: 'CO2e',
    format: 'number'
  }
};

const INCLUDED_SECTORS = {
  CAIT: [
    'Energy',
    'Industrial Processes',
    'Agriculture',
    'Waste',
    'Bunker Fuels'
  ],
  PIK: [
    'Energy',
    'Agriculture',
    'Waste',
    'Solvent sector',
    'Industrial process',
    'Other'
  ],
  UNFCCC: [
    'Energy',
    'Industrial Processes',
    'Solvent and Other Product Use',
    'Agriculture',
    'Waste',
    'Other'
  ]
};

const calculationKeys = Object.keys(CALCULATION_OPTIONS);
const options = calculationKeys.map(
  calculationKey => CALCULATION_OPTIONS[calculationKey]
);

// meta data for selectors
const getMeta = state => state.meta || {};
const getQuantifications = state =>
  uniqBy(state.quantifications, 'year') || null;
const getCalculationData = state =>
  (state.calculationData && state.calculationData[state.iso]) || [];

// values from search
const getSourceSelection = state => state.search.source || null;
const getCalculationSelection = state => state.search.calculation || null;
const getVersionSelection = state => state.search.version || null;
const getFilterSelection = state => state.search.filter || null;

// data for the graph
const getData = state => state.data || [];

// Sources selectors
export const getSources = createSelector(
  getMeta,
  meta => meta.data_source || null
);
export const getVersions = createSelector(getMeta, meta => meta.gwp || null);

export const getSourceOptions = createSelector(getSources, sources => {
  if (!sources) return [];
  return sources.map(d => ({
    label: d.label,
    value: d.value,
    source: d.source
  }));
});

const parseCalculationData = createSelector([getCalculationData], data => {
  if (!data || !data.length) return null;
  return groupBy(data, 'year');
});

export const getCalculationOptions = createSelector(
  parseCalculationData,
  calculationData => {
    if (!calculationData) return [];
    return options;
  }
);

export const getSourceSelected = createSelector(
  [getSourceOptions, getSourceSelection],
  (sources, selected) => {
    if (!sources || !sources.length) return {};
    if (!selected) return sources[0];
    return sources.find(category => category.value === parseInt(selected, 10));
  }
);

export const getCalculationSelected = createSelector(
  [getCalculationSelection],
  selected => {
    if (!selected) return options[0];
    return options.find(calculation => calculation.value === selected);
  }
);

// Versions selectors
export const getVersionOptions = createSelector(
  [getVersions, getSources, getSourceSelected],
  (versions, sources, sourceSelected) => {
    if (!sourceSelected || !versions) return [];
    const sourceData = sources.find(d => sourceSelected.value === d.value);
    return versions.filter(filter => sourceData.gwp.indexOf(filter.value) > -1);
  }
);

export const getVersionSelected = createSelector(
  [getVersionOptions, getVersionSelection],
  (versions, selected) => {
    if (!versions || !versions.length) return {};
    if (!selected) return versions[0];
    return versions.find(version => version.value === parseInt(selected, 10));
  }
);

// Filters selector
export const getFilterOptions = createSelector(
  [getMeta, getSourceSelected],
  (meta, sourceSelected) => {
    if (!sourceSelected || isEmpty(meta)) return [];
    const activeSourceData = meta.data_source.find(
      source => source.value === sourceSelected.value
    );
    const activeFilterKeys = activeSourceData.sector;
    const filteredSelected = meta.sector.filter(
      filter => activeFilterKeys.indexOf(filter.value) > -1
    );
    return sortLabelByAlpha(filteredSelected);
  }
);

export const getFiltersSelected = createSelector(
  [getFilterOptions, getFilterSelection],
  (filters, selected) => {
    if (!filters || !filters.length) return [];
    if (!selected) return filters;
    let selectedFilters = [];
    const selectedValues = selected.split(',');
    const selectedValuesNum = selectedValues.map(d => parseInt(d, 10));
    selectedFilters = filters.filter(
      filter => selectedValuesNum.indexOf(filter.value) > -1
    );
    return selectedFilters;
  }
);

// get selector defaults
export const getSelectorDefaults = createSelector(
  [getSources, getSourceSelected],
  (sources, sourceSelected) => {
    if (!sources || !sources.length || !sourceSelected) return {};
    const sourceData = sources.find(d => d.value === sourceSelected.value);
    return {
      sector: sourceData.sector[0],
      gas: sourceData.gas[0],
      source: sources[0].value
    };
  }
);

// Map the data from the API
export const filterData = createSelector(
  [getData, getSourceSelected],
  (data, sourceSelected) => {
    if (!data || !data.length) return [];
    return sortEmissionsByValue(
      data.filter(
        d =>
          INCLUDED_SECTORS[sourceSelected.label].indexOf(d.sector.trim()) >= 0
      )
    );
  }
);

const calculatedRatio = (selected, calculationData, x) => {
  if (selected === CALCULATION_OPTIONS.PER_GDP.value) {
    // GDP is in dollars and we want to display it in million dollars
    return calculationData[x][0].gdp / DATA_SCALE;
  }
  if (selected === CALCULATION_OPTIONS.PER_CAPITA.value) {
    return calculationData[x][0].population;
  }
  return 1;
};

export const getQuantificationsData = createSelector(
  getQuantifications,
  quantifications => {
    if (!quantifications) return [];
    return quantifications.map(q => ({
      y: q.value * DATA_SCALE,
      x: q.year
    }));
  }
);

export const getChartData = createSelector(
  [
    filterData,
    getFiltersSelected,
    parseCalculationData,
    getCalculationSelected,
    getQuantifications
  ],
  (data, filters, calculationData, calculationSelected) => {
    if (
      !data ||
      !data.length ||
      !filters ||
      !calculationData ||
      !calculationSelected
    ) {
      return [];
    }

    let xValues = [];
    xValues = data[0].emissions.map(d => d.year);
    if (
      calculationSelected.value !== CALCULATION_OPTIONS.ABSOLUTE_VALUE.value
    ) {
      xValues = intersection(
        xValues,
        Object.keys(calculationData).map(y => parseInt(y, 10))
      );
    }

    const dataParsed = xValues.map(x => {
      const yItems = {};
      data.forEach(d => {
        const yKey = getYColumnValue(d.sector);
        const yData = d.emissions.find(e => e.year === x);
        const calculationRatio = calculatedRatio(
          calculationSelected.value,
          calculationData,
          x
        );
        if (yData) {
          const scaledYData = yData.value * DATA_SCALE;
          yItems[yKey] = scaledYData / calculationRatio;
        }
      });
      const item = {
        x,
        ...yItems
      };
      return item;
    });
    return dataParsed;
  }
);

export const getChartConfig = createSelector(
  [filterData, getCalculationSelected],
  (data, calculationSelected) => {
    if (!data || !data.length) return {};
    const yColumns = data.map(d => ({
      label: d.sector,
      value: getYColumnValue(d.sector)
    }));
    const yColumnsChecked = uniqBy(yColumns, 'value');
    const theme = getThemeConfig(
      yColumnsChecked,
      getColorPalette(BASE_COLORS, yColumnsChecked.length)
    );
    const tooltip = getTooltipConfig(yColumnsChecked);
    let unit = AXES_CONFIG.yLeft.unit;
    if (calculationSelected.value === CALCULATION_OPTIONS.PER_GDP.value) {
      unit = `${unit}/ million $ GDP`;
    } else if (
      calculationSelected.value === CALCULATION_OPTIONS.PER_CAPITA.value
    ) {
      unit = `${unit} per capita`;
    }
    const axes = {
      ...AXES_CONFIG,
      yLeft: {
        ...AXES_CONFIG.yLeft,
        unit
      }
    };
    return {
      axes,
      theme,
      tooltip,
      columns: {
        x: [{ label: 'year', value: 'x' }],
        y: yColumnsChecked
      }
    };
  }
);

export default {
  getSourceOptions,
  getSourceSelected,
  getFilterOptions,
  getFiltersSelected,
  getCalculationData
};
<commit_msg>Render emissions chart in country page even when sociodemographic data isn't available<commit_after>import { createSelector } from 'reselect';
import isEmpty from 'lodash/isEmpty';
import uniqBy from 'lodash/uniqBy';
import groupBy from 'lodash/groupBy';
import intersection from 'lodash/intersection';
import { CALCULATION_OPTIONS } from 'app/data/constants';

import {
  getYColumnValue,
  getThemeConfig,
  getTooltipConfig,
  sortEmissionsByValue,
  sortLabelByAlpha,
  getColorPalette
} from 'utils/graphs';

// constants needed for data parsing
const DATA_SCALE = 1000000;

const BASE_COLORS = ['#25597C', '#DFE9ED'];

const AXES_CONFIG = {
  xBottom: {
    name: 'Year',
    unit: 'date',
    format: 'YYYY'
  },
  yLeft: {
    name: 'Emissions',
    unit: 'CO2e',
    format: 'number'
  }
};

const INCLUDED_SECTORS = {
  CAIT: [
    'Energy',
    'Industrial Processes',
    'Agriculture',
    'Waste',
    'Bunker Fuels'
  ],
  PIK: [
    'Energy',
    'Agriculture',
    'Waste',
    'Solvent sector',
    'Industrial process',
    'Other'
  ],
  UNFCCC: [
    'Energy',
    'Industrial Processes',
    'Solvent and Other Product Use',
    'Agriculture',
    'Waste',
    'Other'
  ]
};

const calculationKeys = Object.keys(CALCULATION_OPTIONS);
const options = calculationKeys.map(
  calculationKey => CALCULATION_OPTIONS[calculationKey]
);

// meta data for selectors
const getMeta = state => state.meta || {};
const getQuantifications = state =>
  uniqBy(state.quantifications, 'year') || null;
const getCalculationData = state =>
  (state.calculationData && state.calculationData[state.iso]) || [];

// values from search
const getSourceSelection = state => state.search.source || null;
const getCalculationSelection = state => state.search.calculation || null;
const getVersionSelection = state => state.search.version || null;
const getFilterSelection = state => state.search.filter || null;

// data for the graph
const getData = state => state.data || [];

// Sources selectors
export const getSources = createSelector(
  getMeta,
  meta => meta.data_source || null
);
export const getVersions = createSelector(getMeta, meta => meta.gwp || null);

export const getSourceOptions = createSelector(getSources, sources => {
  if (!sources) return [];
  return sources.map(d => ({
    label: d.label,
    value: d.value,
    source: d.source
  }));
});

const parseCalculationData = createSelector([getCalculationData], data => {
  if (!data || !data.length) return null;
  return groupBy(data, 'year');
});

export const getCalculationOptions = createSelector(
  parseCalculationData,
  calculationData => {
    if (!calculationData) return [];
    return options;
  }
);

export const getSourceSelected = createSelector(
  [getSourceOptions, getSourceSelection],
  (sources, selected) => {
    if (!sources || !sources.length) return {};
    if (!selected) return sources[0];
    return sources.find(category => category.value === parseInt(selected, 10));
  }
);

export const getCalculationSelected = createSelector(
  [getCalculationSelection],
  selected => {
    if (!selected) return options[0];
    return options.find(calculation => calculation.value === selected);
  }
);

// Versions selectors
export const getVersionOptions = createSelector(
  [getVersions, getSources, getSourceSelected],
  (versions, sources, sourceSelected) => {
    if (!sourceSelected || !versions) return [];
    const sourceData = sources.find(d => sourceSelected.value === d.value);
    return versions.filter(filter => sourceData.gwp.indexOf(filter.value) > -1);
  }
);

export const getVersionSelected = createSelector(
  [getVersionOptions, getVersionSelection],
  (versions, selected) => {
    if (!versions || !versions.length) return {};
    if (!selected) return versions[0];
    return versions.find(version => version.value === parseInt(selected, 10));
  }
);

// Filters selector
export const getFilterOptions = createSelector(
  [getMeta, getSourceSelected],
  (meta, sourceSelected) => {
    if (!sourceSelected || isEmpty(meta)) return [];
    const activeSourceData = meta.data_source.find(
      source => source.value === sourceSelected.value
    );
    const activeFilterKeys = activeSourceData.sector;
    const filteredSelected = meta.sector.filter(
      filter => activeFilterKeys.indexOf(filter.value) > -1
    );
    return sortLabelByAlpha(filteredSelected);
  }
);

export const getFiltersSelected = createSelector(
  [getFilterOptions, getFilterSelection],
  (filters, selected) => {
    if (!filters || !filters.length) return [];
    if (!selected) return filters;
    let selectedFilters = [];
    const selectedValues = selected.split(',');
    const selectedValuesNum = selectedValues.map(d => parseInt(d, 10));
    selectedFilters = filters.filter(
      filter => selectedValuesNum.indexOf(filter.value) > -1
    );
    return selectedFilters;
  }
);

// get selector defaults
export const getSelectorDefaults = createSelector(
  [getSources, getSourceSelected],
  (sources, sourceSelected) => {
    if (!sources || !sources.length || !sourceSelected) return {};
    const sourceData = sources.find(d => d.value === sourceSelected.value);
    return {
      sector: sourceData.sector[0],
      gas: sourceData.gas[0],
      source: sources[0].value
    };
  }
);

// Map the data from the API
export const filterData = createSelector(
  [getData, getSourceSelected],
  (data, sourceSelected) => {
    if (!data || !data.length) return [];
    return sortEmissionsByValue(
      data.filter(
        d =>
          INCLUDED_SECTORS[sourceSelected.label].indexOf(d.sector.trim()) >= 0
      )
    );
  }
);

const calculatedRatio = (selected, calculationData, x) => {
  if (selected === CALCULATION_OPTIONS.PER_GDP.value) {
    // GDP is in dollars and we want to display it in million dollars
    return calculationData[x][0].gdp / DATA_SCALE;
  }
  if (selected === CALCULATION_OPTIONS.PER_CAPITA.value) {
    return calculationData[x][0].population;
  }
  return 1;
};

export const getQuantificationsData = createSelector(
  getQuantifications,
  quantifications => {
    if (!quantifications) return [];
    return quantifications.map(q => ({
      y: q.value * DATA_SCALE,
      x: q.year
    }));
  }
);

export const getChartData = createSelector(
  [
    filterData,
    getFiltersSelected,
    parseCalculationData,
    getCalculationSelected,
    getQuantifications
  ],
  (data, filters, calculationData, calculationSelected) => {
    if (!data || !data.length || !filters || !calculationSelected) {
      return [];
    }

    let xValues = [];
    xValues = data[0].emissions.map(d => d.year);
    if (
      calculationSelected.value !== CALCULATION_OPTIONS.ABSOLUTE_VALUE.value
    ) {
      xValues = intersection(
        xValues,
        Object.keys(calculationData || []).map(y => parseInt(y, 10))
      );
    }

    const dataParsed = xValues.map(x => {
      const yItems = {};
      data.forEach(d => {
        const yKey = getYColumnValue(d.sector);
        const yData = d.emissions.find(e => e.year === x);
        const calculationRatio = calculatedRatio(
          calculationSelected.value,
          calculationData,
          x
        );
        if (yData) {
          const scaledYData = yData.value * DATA_SCALE;
          yItems[yKey] = scaledYData / calculationRatio;
        }
      });
      const item = {
        x,
        ...yItems
      };
      return item;
    });
    return dataParsed;
  }
);

export const getChartConfig = createSelector(
  [filterData, getCalculationSelected],
  (data, calculationSelected) => {
    if (!data || !data.length) return {};
    const yColumns = data.map(d => ({
      label: d.sector,
      value: getYColumnValue(d.sector)
    }));
    const yColumnsChecked = uniqBy(yColumns, 'value');
    const theme = getThemeConfig(
      yColumnsChecked,
      getColorPalette(BASE_COLORS, yColumnsChecked.length)
    );
    const tooltip = getTooltipConfig(yColumnsChecked);
    let unit = AXES_CONFIG.yLeft.unit;
    if (calculationSelected.value === CALCULATION_OPTIONS.PER_GDP.value) {
      unit = `${unit}/ million $ GDP`;
    } else if (
      calculationSelected.value === CALCULATION_OPTIONS.PER_CAPITA.value
    ) {
      unit = `${unit} per capita`;
    }
    const axes = {
      ...AXES_CONFIG,
      yLeft: {
        ...AXES_CONFIG.yLeft,
        unit
      }
    };
    return {
      axes,
      theme,
      tooltip,
      columns: {
        x: [{ label: 'year', value: 'x' }],
        y: yColumnsChecked
      }
    };
  }
);

export default {
  getSourceOptions,
  getSourceSelected,
  getFilterOptions,
  getFiltersSelected,
  getCalculationData
};
<|endoftext|>


<commit_before>function DEBUG(msg){
    console.log(msg);
}

function jsonTable(dataSource, options){
    // Hang onto the source DOM object.
    var $dataSource = $(dataSource);
    var data = {};
    var $table;
    var errors = [];

    var defaults = {
        baseRowHeaders: [],
        baseColumnHeaders: [],
        jsonIndent: '    ',
        customValidator: function(data, errors){
            return true;
        },
        defaultRowHeaderValue: "",
        defaultColumnHeaderValue: "",
        display: "inline",
        stripEmptyData: true,
        rule: {
            days:[],
            start:null, finish:null,
            period:null, length:null
        }
    };

    $.extend(defaults, options);

    // DEBUG(defaults);

    function updateDataFromSource(){
        data = JSON.parse($dataSource.val());

        if ($table) {
            $table.detach();
        }

        if (defaults.display == "inline"){
            $table = $.tmpl('table', templateData()).appendTo($dataSource.parent());
        } else {
            $table = $.tmpl('table', templateData()).appendTo('body');
        }
    }

    function getDataFromTable(){
        var newData = {};
        $table.find('input.cell-value').each(function(i, el){
            $el = $(el);
            var row = $el.attr('row');
            var column = $el.attr('column');
            if (!newData[row]){
                newData[row] = {}
            }
            if ($el.val()) {
                newData[row][column] = {rate: $el.val()};
                if (getVisibleRules(column).length > 0) {
                    newData[row][column]['rules'] = getVisibleRules(column);
                }
            }
        });
        return newData;
    }

    function stripEmptyData(){
        $.each(data, function(row, rowData){
            $.each(rowData, function(column, value){
                if (!value){
                    delete rowData[column];
                }
            });
            if ($.isEmptyObject(rowData)){
                delete data[row];
            }
        });
    }

    function updateSourceFromData(){
        console.log("Updating source from data.");
        data = getDataFromTable();
        runValidations();

        if (defaults.stripEmptyData){
            stripEmptyData();
        }
        if (!errors.length){
            $dataSource.val(JSON.stringify(data, null, defaults.jsonIndent));
        }
    }

    function runValidations(){
        // DEBUG("Running validations");
        preValidate();
        findDuplicateHeaders();
        defaults.customValidator(data, errors);
        updateErrors();
        // DEBUG("Finished running validations (" + errors.length + " errors found)");
        // if (errors.length) {
        //     DEBUG(errors);
        // }
    }

    function updateErrors(){
        $table.find('tr, td, th').removeClass('errors');
        $table.parent().find('.errormsg').detach();
        // DEBUG(errors);
        $.each(errors, function(i,el){
            $($(el).selector).addClass("errors");
            $($(el).selector).children().first().before('
' + el.reason + "
")
        });
        if (errors.length){
            $table.before('
Errors prevented saving
');
        }
    }

    function templateData(){
        var result = {
            headers: getHeaderNames(),
            conditions: getConditions(),
            defaults: defaults,
            data: data
        };

        return result;
    }

    function getHeaderNames(){
        var rowHeaders = [], columnHeaders = [];
        $.each(data, function(row, rowData){
            if (!inAny(row, [rowHeaders, defaults.baseRowHeaders])){
                rowHeaders.push(row);
            }
            $.each(rowData, function(column, cell){
                if (!inAny(column, [columnHeaders, defaults.baseColumnHeaders])){
                    columnHeaders.push(column);
                }
            });
        });
        return {
            rowHeaders: $.merge(defaults.baseRowHeaders.slice(), rowHeaders.sort().reverse().slice()),
            columnHeaders: $.merge(defaults.baseColumnHeaders.slice(), columnHeaders.sort().reverse().slice())
        };
    }

    function getVisibleRules(conditionName) {
        var rules = [];
        $table
            .find('th.column-header input[value="' + conditionName + '"]')
                .closest('th')
                    .find('.condition-rule')
                        .each(function(i, form){
                            var value = form2object(form);
                            if (value['days'].length){
                                value['days'] = $.map(value['days'], function(i, el){ return parseInt(el, 10);});
                            } else {
                                delete value['days'];
                            }
                            if (!$.isEmptyObject(value)) {
                                rules.push(value);
                            }
        });
        // TODO: Remove duplicates.
        return rules;
    }

    function getVisibleHeaders($table) {
        return {
            rowHeaders: $table.find('th.row-header input.heading-value').map(function(i,x){return x.value}),
            columnHeaders: $table.find('th.column-header input.condition-name').map(function(i,x){return x.value})
        };
    }

    function getRules(conditionName){
        var rules = [];
        var jsonRules = [];

        $.each(data, function(row, rowData){
            if (rowData[conditionName] && rowData[conditionName]['rules']) {
                $.each(rowData[conditionName]['rules'], function(i, rule){
                    var newRule = $.extend({}, defaults.rule, rule);
                    var jsonRule = JSON.stringify(rule);
                    if ($.inArray(jsonRule, jsonRules)){
                        rules.push(newRule);
                        jsonRules.push(jsonRule);
                    }
                });
            }
        });
        return rules;
    }

    function getConditions(){
        conditionNames = getHeaderNames()['columnHeaders'];
        conditions = {};
        $.each(conditionNames, function(i, conditionName){
            conditions[conditionName] = getRules(conditionName);
        });
        console.log(conditions);
        return conditions;
    }

    function inAny(value, arrays){
        // is the value in any of the passed in arrays?
        var result = false;
        $.each(arrays, function(i, val){
            if ($.inArray(value, val) > -1) {
                result = true;
                return false;
            }
        });
        return result;
    }

    function combineFields($fields, sep, innerSep){
        /*
            Given several jQuery DOM objects, combine the values they
            contain into one value, using sep as seperator.

            If any object is a multiple select, then combine the values
            it returns with innerSep
        */
        return $fields.map(function(i, el){
            $el = $(el);
            if ($el.attr('multiple')){
                return $el.val().join(innerSep);
            }
            return $el.val();
        }).get().join(sep);
    }

    function addRow(evt){
        evt.preventDefault();
        $.tmpl('row', {
            row:defaults.defaultRowHeaderValue,
            headers: getVisibleHeaders($table),
            data: data,
            row_id: $table.find('tr.data-row').length
        }).insertBefore($(this).closest('tr'));
        // Disable adding a new row until this one has been edited.
        // $(this).attr('disabled','disabled');
        return false;
        runValidations();

    }

    function addColumn(evt){
        evt.preventDefault();
        // Add a new column header.
        var column = defaults.defaultColumnHeaderValue;
        console.log(column);
        var column_id = $table.find('tr.header-row th.column-header').length;
        console.log(column_id);
        $.tmpl('column-header', {
            column: column,
            column_id: column_id,
            rules: [defaults.rule]
        }).insertBefore($(this).closest('th'));
        // Now add a cell to each row in this table.
        $table.find('tr.data-row').each(function(i, el){
            $.tmpl('cell', {
                data:{},
                row_id:i,
                row:$(el).find('th input.heading-value').val(),
                column_id: column_id,
                column: column
            }).appendTo($(el));
        });
        runValidations();
    }

    function updateColumnName(evt) {
        evt.preventDefault();
        var $target = $(evt.target);
        var newName = $target.val();
        var column_id = $target.attr('column_id');
        $table.find('input.cell-value').filter(function(i){
            return $(this).attr('column_id') == column_id;
        }).attr('column', newName);
    }

    function updateRowHeader(evt) {
        evt.preventDefault();
        var $th = $(evt.target).closest('th');
        var newValue = combineFields($th.find(':input.fragment'), ',');
        console.log(newValue);
        $th.find(':input:hidden').val(newValue);
        $th.closest('tr').find('input.cell-value').attr('row', newValue);
    }

    function addRule(evt) {
        // Add a new rule to the current condition
        evt.preventDefault();
        $.tmpl($('#rule-template'), defaults.rule).insertBefore($(evt.target))
    }

    function deleteRule(evt) {
        // Remove the current rule from the current condition
        evt.preventDefault();
        $(evt.target).closest('.condition-rule').detach();
        updateSourceFromData();
    }

    function preValidate(){
        errors = [];
    }

    function findDuplicateHeaders(){
        // Ensure that there are no duplicated row or column headings.
        $table.find('.condition-name').each(function(j, col){
            $this = $(col);
            selector = $table.find('.condition-name[value="' + $this.val() + '"]').not($this).closest('th');
            if (selector.length){
                errors.push({
                    selector:selector,
                    reason: "Duplicate " + defaults.columnName
                });
            }
        });

        $table.find('th.row-header').each(function(j,row){
            $this = $(row).find('input:hidden');
            selector = $table.find('th.row-header input:hidden[value="'+ $this.val() + '"]').not($this).closest('th');
            if (selector.length){
                errors.push({
                    selector:selector,
                    reason: "Duplicate " + defaults.rowName
                });
            }
        });
    }

    // Load the templates for performance reasons.
    // The default templates will have already been loaded
    // from json-table-templates.js (also part of the media for the widget)
    // This will override any of these where the correctly named DOM element
    // exists.
    var template_names = [
        'table',
        'row',
        'column-header',
        'column-header-widget',
        'row-header-widget',
        'cell'
    ];
    $.each(template_names, function(i, name){
        if ($('#' + name + '-template').length){
            $.template(name, $('#' + name + '-template'));
        }
    });

    // DEBUG($.template('column-header-widget'));

    updateDataFromSource();

    // DEBUG('Updated Data');
    /*
    Attach all of the event handlers
    */

    $('.json-data-table :input').live('change blur', updateSourceFromData);
    $('.json-data-table th .condition-name').live('change blur', updateColumnName);
    $('.json-data-table th.row-header :input').live('change blur', updateRowHeader);
    $('.add-row').live('click', addRow);
    $('.add-column').live('click', addColumn);
    $('.add-rule').live('click', addRule);
    $('.delete-rule').live('click', deleteRule);
    $dataSource.live('change blur', updateDataFromSource);

    // $dataSource.hide();

    return $;
}

<commit_msg>Only add rules if they haven't been added.<commit_after>function DEBUG(msg){
    console.log(msg);
}

function jsonTable(dataSource, options){
    // Hang onto the source DOM object.
    var $dataSource = $(dataSource);
    var data = {};
    var $table;
    var errors = [];

    var defaults = {
        baseRowHeaders: [],
        baseColumnHeaders: [],
        jsonIndent: '    ',
        customValidator: function(data, errors){
            return true;
        },
        defaultRowHeaderValue: "",
        defaultColumnHeaderValue: "",
        display: "inline",
        stripEmptyData: true,
        rule: {
            days:[],
            start:null, finish:null,
            period:null, length:null
        }
    };

    $.extend(defaults, options);

    // DEBUG(defaults);

    function updateDataFromSource(){
        data = JSON.parse($dataSource.val());

        if ($table) {
            $table.detach();
        }

        if (defaults.display == "inline"){
            $table = $.tmpl('table', templateData()).appendTo($dataSource.parent());
        } else {
            $table = $.tmpl('table', templateData()).appendTo('body');
        }
    }

    function getDataFromTable(){
        var newData = {};
        $table.find('input.cell-value').each(function(i, el){
            $el = $(el);
            var row = $el.attr('row');
            var column = $el.attr('column');
            if (!newData[row]){
                newData[row] = {}
            }
            if ($el.val()) {
                newData[row][column] = {rate: $el.val()};
                if (getVisibleRules(column).length > 0) {
                    newData[row][column]['rules'] = getVisibleRules(column);
                }
            }
        });
        return newData;
    }

    function stripEmptyData(){
        $.each(data, function(row, rowData){
            $.each(rowData, function(column, value){
                if (!value){
                    delete rowData[column];
                }
            });
            if ($.isEmptyObject(rowData)){
                delete data[row];
            }
        });
    }

    function updateSourceFromData(){
        console.log("Updating source from data.");
        data = getDataFromTable();
        runValidations();

        if (defaults.stripEmptyData){
            stripEmptyData();
        }
        if (!errors.length){
            $dataSource.val(JSON.stringify(data, null, defaults.jsonIndent));
        }
    }

    function runValidations(){
        // DEBUG("Running validations");
        preValidate();
        findDuplicateHeaders();
        defaults.customValidator(data, errors);
        updateErrors();
        // DEBUG("Finished running validations (" + errors.length + " errors found)");
        // if (errors.length) {
        //     DEBUG(errors);
        // }
    }

    function updateErrors(){
        $table.find('tr, td, th').removeClass('errors');
        $table.parent().find('.errormsg').detach();
        // DEBUG(errors);
        $.each(errors, function(i,el){
            $($(el).selector).addClass("errors");
            $($(el).selector).children().first().before('
' + el.reason + "
")
        });
        if (errors.length){
            $table.before('
Errors prevented saving
');
        }
    }

    function templateData(){
        var result = {
            headers: getHeaderNames(),
            conditions: getConditions(),
            defaults: defaults,
            data: data
        };

        return result;
    }

    function getHeaderNames(){
        var rowHeaders = [], columnHeaders = [];
        $.each(data, function(row, rowData){
            if (!inAny(row, [rowHeaders, defaults.baseRowHeaders])){
                rowHeaders.push(row);
            }
            $.each(rowData, function(column, cell){
                if (!inAny(column, [columnHeaders, defaults.baseColumnHeaders])){
                    columnHeaders.push(column);
                }
            });
        });
        return {
            rowHeaders: $.merge(defaults.baseRowHeaders.slice(), rowHeaders.sort().reverse().slice()),
            columnHeaders: $.merge(defaults.baseColumnHeaders.slice(), columnHeaders.sort().reverse().slice())
        };
    }

    function getVisibleRules(conditionName) {
        var rules = [];
        $table
            .find('th.column-header input[value="' + conditionName + '"]')
                .closest('th')
                    .find('.condition-rule')
                        .each(function(i, form){
                            var value = form2object(form);
                            if (value['days'].length){
                                value['days'] = $.map(value['days'], function(i, el){ return parseInt(el, 10);});
                            } else {
                                delete value['days'];
                            }
                            if (!$.isEmptyObject(value)) {
                                rules.push(value);
                            }
        });
        // TODO: Remove duplicates.
        return rules;
    }

    function getVisibleHeaders($table) {
        return {
            rowHeaders: $table.find('th.row-header input.heading-value').map(function(i,x){return x.value}),
            columnHeaders: $table.find('th.column-header input.condition-name').map(function(i,x){return x.value})
        };
    }

    function getRules(conditionName){
        var rules = [];
        var jsonRules = [];

        $.each(data, function(row, rowData){
            if (rowData[conditionName] && rowData[conditionName]['rules']) {
                $.each(rowData[conditionName]['rules'], function(i, rule){
                    var newRule = $.extend({}, defaults.rule, rule);
                    var jsonRule = JSON.stringify(rule);
                    if ($.inArray(jsonRule, jsonRules) == -1){
                        rules.push(newRule);
                        jsonRules.push(jsonRule);
                    }
                });
            }
        });
        return rules;
    }

    function getConditions(){
        conditionNames = getHeaderNames()['columnHeaders'];
        conditions = {};
        $.each(conditionNames, function(i, conditionName){
            conditions[conditionName] = getRules(conditionName);
        });
        console.log(conditions);
        return conditions;
    }

    function inAny(value, arrays){
        // is the value in any of the passed in arrays?
        var result = false;
        $.each(arrays, function(i, val){
            if ($.inArray(value, val) > -1) {
                result = true;
                return false;
            }
        });
        return result;
    }

    function combineFields($fields, sep, innerSep){
        /*
            Given several jQuery DOM objects, combine the values they
            contain into one value, using sep as seperator.

            If any object is a multiple select, then combine the values
            it returns with innerSep
        */
        return $fields.map(function(i, el){
            $el = $(el);
            if ($el.attr('multiple')){
                return $el.val().join(innerSep);
            }
            return $el.val();
        }).get().join(sep);
    }

    function addRow(evt){
        evt.preventDefault();
        $.tmpl('row', {
            row:defaults.defaultRowHeaderValue,
            headers: getVisibleHeaders($table),
            data: data,
            row_id: $table.find('tr.data-row').length
        }).insertBefore($(this).closest('tr'));
        // Disable adding a new row until this one has been edited.
        // $(this).attr('disabled','disabled');
        return false;
        runValidations();

    }

    function addColumn(evt){
        evt.preventDefault();
        // Add a new column header.
        var column = defaults.defaultColumnHeaderValue;
        console.log(column);
        var column_id = $table.find('tr.header-row th.column-header').length;
        console.log(column_id);
        $.tmpl('column-header', {
            column: column,
            column_id: column_id,
            rules: [defaults.rule]
        }).insertBefore($(this).closest('th'));
        // Now add a cell to each row in this table.
        $table.find('tr.data-row').each(function(i, el){
            $.tmpl('cell', {
                data:{},
                row_id:i,
                row:$(el).find('th input.heading-value').val(),
                column_id: column_id,
                column: column
            }).appendTo($(el));
        });
        runValidations();
    }

    function updateColumnName(evt) {
        evt.preventDefault();
        var $target = $(evt.target);
        var newName = $target.val();
        var column_id = $target.attr('column_id');
        $table.find('input.cell-value').filter(function(i){
            return $(this).attr('column_id') == column_id;
        }).attr('column', newName);
    }

    function updateRowHeader(evt) {
        evt.preventDefault();
        var $th = $(evt.target).closest('th');
        var newValue = combineFields($th.find(':input.fragment'), ',');
        console.log(newValue);
        $th.find(':input:hidden').val(newValue);
        $th.closest('tr').find('input.cell-value').attr('row', newValue);
    }

    function addRule(evt) {
        // Add a new rule to the current condition
        evt.preventDefault();
        $.tmpl($('#rule-template'), defaults.rule).insertBefore($(evt.target))
    }

    function deleteRule(evt) {
        // Remove the current rule from the current condition
        evt.preventDefault();
        $(evt.target).closest('.condition-rule').detach();
        updateSourceFromData();
    }

    function preValidate(){
        errors = [];
    }

    function findDuplicateHeaders(){
        // Ensure that there are no duplicated row or column headings.
        $table.find('.condition-name').each(function(j, col){
            $this = $(col);
            selector = $table.find('.condition-name[value="' + $this.val() + '"]').not($this).closest('th');
            if (selector.length){
                errors.push({
                    selector:selector,
                    reason: "Duplicate " + defaults.columnName
                });
            }
        });

        $table.find('th.row-header').each(function(j,row){
            $this = $(row).find('input:hidden');
            selector = $table.find('th.row-header input:hidden[value="'+ $this.val() + '"]').not($this).closest('th');
            if (selector.length){
                errors.push({
                    selector:selector,
                    reason: "Duplicate " + defaults.rowName
                });
            }
        });
    }

    // Load the templates for performance reasons.
    // The default templates will have already been loaded
    // from json-table-templates.js (also part of the media for the widget)
    // This will override any of these where the correctly named DOM element
    // exists.
    var template_names = [
        'table',
        'row',
        'column-header',
        'column-header-widget',
        'row-header-widget',
        'cell'
    ];
    $.each(template_names, function(i, name){
        if ($('#' + name + '-template').length){
            $.template(name, $('#' + name + '-template'));
        }
    });

    // DEBUG($.template('column-header-widget'));

    updateDataFromSource();

    // DEBUG('Updated Data');
    /*
    Attach all of the event handlers
    */

    $('.json-data-table :input').live('change blur', updateSourceFromData);
    $('.json-data-table th .condition-name').live('change blur', updateColumnName);
    $('.json-data-table th.row-header :input').live('change blur', updateRowHeader);
    $('.add-row').live('click', addRow);
    $('.add-column').live('click', addColumn);
    $('.add-rule').live('click', addRule);
    $('.delete-rule').live('click', deleteRule);
    $dataSource.live('change blur', updateDataFromSource);

    // $dataSource.hide();

    return $;
}

<|endoftext|>


<commit_before>(function($) {
    $.entwine('ss', function($) {
        $('.cms-edit-form').entwine({
            onadd: function() {
                this._super();

                var self=$(this);

                /***** Bind Keyboard Events *****/
                //Global Events
                Mousetrap.bindGlobal('mod+s', function(e) {return self._saveForm(e);}); //Save Draft/Save

                //CMS Page Edit Controller Only
                if(self.hasClass('CMSPageEditController') || self.hasClass('CMSPageSettingsController')) {
                    Mousetrap.bindGlobal('mod+shift+s', function(e) {return self._saveAndPublish(e);}); //Publish
                    Mousetrap.bindGlobal('mod+k', function(e) {return self._switchStage(e);}); //Preview Stage Switch
                    Mousetrap.bindGlobal('mod+m', function(e) {return self._switchPreviewMode(e);}); //Preview View Mode Switch
                    Mousetrap.bindGlobal('mod+j', function(e) {return self._switchPreviewSize(e);}); //Preview View Size Switch
                }

                //CMS Page Edit and CMS Add Page controllers only
                if(self.hasClass('CMSPageEditController') || self.hasClass('CMSPageAddController')) {
                    Mousetrap.bindGlobal('mod+alt+n', function(e) {return self._addPage(e);}); //Add Page
                }
            },
            onremove: function() {
                this._super();

                /***** Unbind Keyboard Events *****/
                Mousetrap.reset();
            },

            //---------- Handlers ----------//
            /**
             * Targets the save button in CMSMain, GridField and SiteConfig
             */
            _saveForm: function(e) {
                var button=$('#Form_EditForm_action_save, #Form_ItemEditForm_action_doSave, #Form_EditForm_action_save_siteconfig');
                if(button.length>0) {
                    button.click();

                    return false;
                }
            },

            /**
             * Targets the publish button in CMSMain
             */
            _saveAndPublish: function(e) {
                var button=$('#Form_EditForm_action_publish');
                if(button.length>0) {
                    button.click();

                    return false;
                }
            },

            /**
             * Targets the  Add New button in CMSMain
             */
            _addPage: function(e) {
                var button=$('.cms-page-add-button, .cms-content-addpage-button, #Form_AddForm_action_doAdd');
                if(button.length>0) {
                    button.click();

                    return false;
                }
            },

            /**
             * Targets the preview stage controls in CMSMain
             */
            _switchStage: function(e) {
                var draftStage=$('.cms-preview .cms-preview-states input[data-name=StageLink]');
                var liveStage=$('.cms-preview .cms-preview-states input[data-name=LiveLink]');
                if(draftStage.length>0 && liveStage.length>0) {
                    if(draftStage.is(':checked')) {
                        //Current is stage, switch to live
                        jQuery('.cms-preview').entwine('ss.preview').changeState('LiveLink', false);
                    }else {
                        //Current is live, switch to stage
                        jQuery('.cms-preview').entwine('ss.preview').changeState('StageLink', false);
                    }

                    return false;
                }
            },

            /**
             * Targets the preview mode controls in CMSMain
             */
            _switchPreviewMode: function(e) {
                var modeDropdown=$('.cms-preview .preview-mode-selector .preview-dropdown');
                if(modeDropdown.length>0) {
                    if(modeDropdown.val()=='split') {
                        jQuery('.cms-preview').entwine('ss.preview').changeMode('content', true);
                    }else if(modeDropdown.val()=='content') {
                        jQuery('.cms-preview').entwine('ss.preview').changeMode('preview', true);
                    }else {
                        jQuery('.cms-preview').entwine('ss.preview').changeMode('split', true);
                    }

                    return false;
                }
            },

            /**
             * Targets the preview size controls in CMSMain
             */
            _switchPreviewSize: function(e) {
                var sizeDropdown=$('.cms-preview .preview-size-selector .preview-dropdown');
                if(sizeDropdown.length>0) {
                    if(sizeDropdown.val()=='auto') {
                        jQuery('.cms-preview').entwine('ss.preview').changeSize('desktop', true);
                    }else if(sizeDropdown.val()=='desktop') {
                        jQuery('.cms-preview').entwine('ss.preview').changeSize('tablet', true);
                    }else if(sizeDropdown.val()=='tablet') {
                        jQuery('.cms-preview').entwine('ss.preview').changeSize('mobile', true);
                    }else {
                        jQuery('.cms-preview').entwine('ss.preview').changeSize('auto', true);
                    }

                    return false;
                }
            }
        });

        $('.CMSPagesController').entwine({
            onadd: function() {
                this._super();

                var self=$(this);

                /***** Bind Keyboard Events *****/
                Mousetrap.bindGlobal('mod+alt+n', function(e) {return self._addPage(e);}); //Add Page
            },
            onremove: function() {
                this._super();

                /***** Unbind Keyboard Events *****/
                Mousetrap.reset();
            },

            //---------- Handlers ----------//
            /**
             * Targets the  Add New button in CMSMain
             */
            _addPage: function(e) {
                var button=$('.cms-page-add-button, .cms-content-addpage-button, #Form_AddForm_action_doAdd');
                if(button.length>0) {
                    button.click();

                    return false;
                }
            }
        });

        $('.ModelAdmin').entwine({
            onadd: function() {
                this._super();

                var self=$(this);

                /***** Bind Keyboard Events *****/
                Mousetrap.bindGlobal('mod+alt+n', function(e) {return self._modelAdminNew(e);}); //ModelAdmin add new
            },
            onremove: function() {
                this._super();

                /***** Unbind Keyboard Events *****/
                Mousetrap.reset();
            },

            //---------- Handlers ----------//
            /**
             * Handles adding a new item in a model admin
             */
            _modelAdminNew: function(e) {
                var button=$('.cms-edit-form > fieldset > .ss-gridfield .ss-gridfield-buttonrow-before .new-link');
                if(button.length>0) {
                    button.click();

                    return false;
                }
            }
        });

        $('.cms-content').entwine({
            onadd: function() {
                this._super();

                var self=$(this);

                /***** Bind Keyboard Events *****/
                //Bind search
                if($('.cms-content-header #filters-button').length>0) {
                    Mousetrap.bindGlobal('mod+f', function(e) {return self._toggleSearch(e);}); //Toggle the search panel
                }

                //Bind clear search filters for cms main as well as model admin
                if(self.hasClass('CMSPagesController') || self.hasClass('CMSPageEditController') || self.hasClass('CMSPageSettingsController') || self.hasClass('ModelAdmin')) {
                    Mousetrap.bindGlobal('mod+shift+f', function(e) {return self._clearSearchFilters(e);}); //Clear all search filters
                }
            },
            onremove: function() {
                this._super();

                /***** Unbind Keyboard Events *****/
                Mousetrap.reset();
            },

            //---------- Handlers ----------//
            /**
             * Toggles showing/hiding of the search
             */
            _toggleSearch: function(e) {
                var button=$('#filters-button');
                if(button.length>0) {
                    button.showHide();

                    if(button.data('collapsed')) {
                        $('.cms-search-form').find('input:not(.action):visible:first').focus();
                    }else {
                        $('.cms-search-form').find('input:not(.action):visible:first').blur();
                    }

                    return false;
                }
            },

            /**
             * Clears the search filters
             */
            _clearSearchFilters: function(e) {
                var button=$('#cms-content-treeview .cms-tree-filtered > a.clear-filter');
                if(button.length>0) {
                    button.click();

                    return false;
                }

                button=$('#cms-content-tools-ModelAdmin form.cms-search-form .resetformaction');
                if(button.length>0) {
                    try {button.click();} catch(e) {}

                    //Click the apply button to submit
                    try {$('#cms-content-tools-ModelAdmin #Form_SearchForm_action_search').click();} catch(e) {}

                    return false;
                }
            }
        });

        //TinyMCE Binding
        $('textarea.htmleditor').entwine({
            MousetrapListeners: [],

            /**
             * Handles binding the event listeners to tinymce when the editor is initialized
             */
            oneditorinit: function() {
                this._super();

                var listeners=this.getMousetrapListeners();
                if(listeners.length>0) {
                    var editorBody=this.getEditor().getInstance().contentWindow.document.body;

                    //Purge all keyboard shortcuts
                    Mousetrap(editorBody).reset();

                    //Remap all keyboard shortcuts
                    for(var i=0;i<listeners.length;++i) {
                        Mousetrap(editorBody).noTinyMCEBind(listeners[i].key, listeners[i].callback, listeners[i].action);
                    }
                }
            },

            /**
             * Caches the listener to bind to
             * @param {string} key Keyboard combination to bind to
             * @param {function} callback Callback function
             * @param {string} action Keyboard key action
             */
            bindMousetrap: function(key, callback, action) {
                var listeners=this.getMousetrapListeners();
                listeners.push({
                    'key': key,
                    'callback': callback,
                    'action': action
                });

                this.setMousetrapListeners(listeners);

                //Bind the listener if the editor's window is ready
                if(this.getEditor().getInstance().contentWindow) {
                    Mousetrap(this.getEditor().getInstance().contentWindow.document.body).noTinyMCEBind(key, callback, action);
                }
            }
        });
    });
})(jQuery);

(function(Mousetrap) {
    var _oldBind=Mousetrap.prototype.bind;

    Mousetrap.prototype.bind=function(keys, callback, action) {
        _oldBind.call(this, keys, callback, action);

        jQuery('textarea.htmleditor').entwine('ss').bindMousetrap(keys, callback, action);
    };

    Mousetrap.prototype.noTinyMCEBind=function(keys, callback, action) {
        _oldBind.call(this, keys, callback, action);
    };

    Mousetrap.init();
})(Mousetrap);<commit_msg>Fixed issue where tinymce would get some shortcuts bound multiple times<commit_after>(function($) {
    $.entwine('ss', function($) {
        $('.cms-edit-form').entwine({
            onadd: function() {
                this._super();

                var self=$(this);

                /***** Bind Keyboard Events *****/
                //Global Events
                Mousetrap.bindGlobal('mod+s', function(e) {return self._saveForm(e);}); //Save Draft/Save

                //CMS Page Edit Controller Only
                if(self.hasClass('CMSPageEditController') || self.hasClass('CMSPageSettingsController')) {
                    Mousetrap.bindGlobal('mod+shift+s', function(e) {return self._saveAndPublish(e);}); //Publish
                    Mousetrap.bindGlobal('mod+k', function(e) {return self._switchStage(e);}); //Preview Stage Switch
                    Mousetrap.bindGlobal('mod+m', function(e) {return self._switchPreviewMode(e);}); //Preview View Mode Switch
                    Mousetrap.bindGlobal('mod+j', function(e) {return self._switchPreviewSize(e);}); //Preview View Size Switch
                }

                //CMS Page Edit and CMS Add Page controllers only
                if(self.hasClass('CMSPageEditController') || self.hasClass('CMSPageAddController')) {
                    Mousetrap.bindGlobal('mod+alt+n', function(e) {return self._addPage(e);}); //Add Page
                }
            },
            onremove: function() {
                this._super();

                /***** Unbind Keyboard Events *****/
                Mousetrap.reset();
            },

            //---------- Handlers ----------//
            /**
             * Targets the save button in CMSMain, GridField and SiteConfig
             */
            _saveForm: function(e) {
                var button=$('#Form_EditForm_action_save, #Form_ItemEditForm_action_doSave, #Form_EditForm_action_save_siteconfig');
                if(button.length>0) {
                    button.click();

                    return false;
                }
            },

            /**
             * Targets the publish button in CMSMain
             */
            _saveAndPublish: function(e) {
                var button=$('#Form_EditForm_action_publish');
                if(button.length>0) {
                    button.click();

                    return false;
                }
            },

            /**
             * Targets the  Add New button in CMSMain
             */
            _addPage: function(e) {
                var button=$('.cms-page-add-button, .cms-content-addpage-button, #Form_AddForm_action_doAdd');
                if(button.length>0) {
                    button.click();

                    return false;
                }
            },

            /**
             * Targets the preview stage controls in CMSMain
             */
            _switchStage: function(e) {
                var draftStage=$('.cms-preview .cms-preview-states input[data-name=StageLink]');
                var liveStage=$('.cms-preview .cms-preview-states input[data-name=LiveLink]');
                if(draftStage.length>0 && liveStage.length>0) {
                    if(draftStage.is(':checked')) {
                        //Current is stage, switch to live
                        jQuery('.cms-preview').entwine('ss.preview').changeState('LiveLink', false);
                    }else {
                        //Current is live, switch to stage
                        jQuery('.cms-preview').entwine('ss.preview').changeState('StageLink', false);
                    }

                    return false;
                }
            },

            /**
             * Targets the preview mode controls in CMSMain
             */
            _switchPreviewMode: function(e) {
                var modeDropdown=$('.cms-preview .preview-mode-selector .preview-dropdown');
                if(modeDropdown.length>0) {
                    if(modeDropdown.val()=='split') {
                        jQuery('.cms-preview').entwine('ss.preview').changeMode('content', true);
                    }else if(modeDropdown.val()=='content') {
                        jQuery('.cms-preview').entwine('ss.preview').changeMode('preview', true);
                    }else {
                        jQuery('.cms-preview').entwine('ss.preview').changeMode('split', true);
                    }

                    return false;
                }
            },

            /**
             * Targets the preview size controls in CMSMain
             */
            _switchPreviewSize: function(e) {
                var sizeDropdown=$('.cms-preview .preview-size-selector .preview-dropdown');
                if(sizeDropdown.length>0) {
                    if(sizeDropdown.val()=='auto') {
                        jQuery('.cms-preview').entwine('ss.preview').changeSize('desktop', true);
                    }else if(sizeDropdown.val()=='desktop') {
                        jQuery('.cms-preview').entwine('ss.preview').changeSize('tablet', true);
                    }else if(sizeDropdown.val()=='tablet') {
                        jQuery('.cms-preview').entwine('ss.preview').changeSize('mobile', true);
                    }else {
                        jQuery('.cms-preview').entwine('ss.preview').changeSize('auto', true);
                    }

                    return false;
                }
            }
        });

        $('.CMSPagesController').entwine({
            onadd: function() {
                this._super();

                var self=$(this);

                /***** Bind Keyboard Events *****/
                Mousetrap.bindGlobal('mod+alt+n', function(e) {return self._addPage(e);}); //Add Page
            },
            onremove: function() {
                this._super();

                /***** Unbind Keyboard Events *****/
                Mousetrap.reset();
            },

            //---------- Handlers ----------//
            /**
             * Targets the  Add New button in CMSMain
             */
            _addPage: function(e) {
                var button=$('.cms-page-add-button, .cms-content-addpage-button, #Form_AddForm_action_doAdd');
                if(button.length>0) {
                    button.click();

                    return false;
                }
            }
        });

        $('.ModelAdmin').entwine({
            onadd: function() {
                this._super();

                var self=$(this);

                /***** Bind Keyboard Events *****/
                Mousetrap.bindGlobal('mod+alt+n', function(e) {return self._modelAdminNew(e);}); //ModelAdmin add new
            },
            onremove: function() {
                this._super();

                /***** Unbind Keyboard Events *****/
                Mousetrap.reset();
            },

            //---------- Handlers ----------//
            /**
             * Handles adding a new item in a model admin
             */
            _modelAdminNew: function(e) {
                var button=$('.cms-edit-form > fieldset > .ss-gridfield .ss-gridfield-buttonrow-before .new-link');
                if(button.length>0) {
                    button.click();

                    return false;
                }
            }
        });

        $('.cms-content').entwine({
            onadd: function() {
                this._super();

                var self=$(this);

                /***** Bind Keyboard Events *****/
                //Bind search
                if($('.cms-content-header #filters-button').length>0) {
                    Mousetrap.bindGlobal('mod+f', function(e) {return self._toggleSearch(e);}); //Toggle the search panel
                }

                //Bind clear search filters for cms main as well as model admin
                if(self.hasClass('CMSPagesController') || self.hasClass('CMSPageEditController') || self.hasClass('CMSPageSettingsController') || self.hasClass('ModelAdmin')) {
                    Mousetrap.bindGlobal('mod+shift+f', function(e) {return self._clearSearchFilters(e);}); //Clear all search filters
                }
            },
            onremove: function() {
                this._super();

                /***** Unbind Keyboard Events *****/
                Mousetrap.reset();
            },

            //---------- Handlers ----------//
            /**
             * Toggles showing/hiding of the search
             */
            _toggleSearch: function(e) {
                var button=$('#filters-button');
                if(button.length>0) {
                    button.showHide();

                    if(button.data('collapsed')) {
                        $('.cms-search-form').find('input:not(.action):visible:first').focus();
                    }else {
                        $('.cms-search-form').find('input:not(.action):visible:first').blur();
                    }

                    return false;
                }
            },

            /**
             * Clears the search filters
             */
            _clearSearchFilters: function(e) {
                var button=$('#cms-content-treeview .cms-tree-filtered > a.clear-filter');
                if(button.length>0) {
                    button.click();

                    return false;
                }

                button=$('#cms-content-tools-ModelAdmin form.cms-search-form .resetformaction');
                if(button.length>0) {
                    try {button.click();} catch(e) {}

                    //Click the apply button to submit
                    try {$('#cms-content-tools-ModelAdmin #Form_SearchForm_action_search').click();} catch(e) {}

                    return false;
                }
            }
        });

        //TinyMCE Binding
        $('textarea.htmleditor').entwine({
            MousetrapListeners: {},
            BoundListeners: {},

            /**
             * Handles binding the event listeners to tinymce when the editor is initialized
             */
            oneditorinit: function() {
                this._super();

                var listeners=this.getMousetrapListeners();
                if(this.getEditor().getInstance().contentWindow) {
                    var editorBody=this.getEditor().getInstance().contentWindow.document.body;

                    //Purge all keyboard shortcuts
                    Mousetrap(editorBody).reset();

                    //Remap all keyboard shortcuts
                    var boundListeners={};
                    for(var key in listeners) {
                        var listener=listeners[key];
                        Mousetrap(editorBody).noTinyMCEBind(key, listener.callback, listener.action);

                        boundListeners[key]=true;
                    }

                    this.setBoundListeners(boundListeners);
                }
            },

            /**
             * Removes all tinymce listeners on remove of the editor
             */
            onremove: function() {
                if(this.getEditor().getInstance().contentWindow) {
                    var editorBody=this.getEditor().getInstance().contentWindow.document.body;

                    //Purge all keyboard shortcuts
                    Mousetrap(editorBody).reset();
                }

                //Reset the object of listeners
                this.setMousetrapListeners({});
                this.setBoundListeners({});

                this._super();
            },

            /**
             * Caches the listener to bind to
             * @param {string} key Keyboard combination to bind to
             * @param {function} callback Callback function
             * @param {string} action Keyboard key action
             */
            bindMousetrap: function(key, callback, action) {
                var listeners=this.getMousetrapListeners();
                listeners[key]={
                                'callback': callback,
                                'action': action
                            };

                this.setMousetrapListeners(listeners);

                //Bind the listener if the editor's window is ready and the keyboard shortcut is not already bound
                if(this.getEditor().getInstance().contentWindow) {
                    var editorBody=this.getEditor().getInstance().contentWindow.document.body;
                    var boundListeners=this.getBoundListeners();

                    if(typeof boundListeners[key]=='undefined') {
                        Mousetrap(editorBody).noTinyMCEBind(key, callback, action);

                        boundListeners[key]=true;

                        this.setBoundListeners(boundListeners);
                    }
                }
            }
        });
    });
})(jQuery);

(function(Mousetrap) {
    var _oldBind=Mousetrap.prototype.bind;
    Mousetrap.prototype.bind=function(keys, callback, action) {
        _oldBind.call(this, keys, callback, action);

        jQuery('textarea.htmleditor').entwine('ss').bindMousetrap(keys, callback, action);
    };

    Mousetrap.prototype.noTinyMCEBind=function(keys, callback, action) {
        _oldBind.call(this, keys, callback, action);
    };

    Mousetrap.init();
})(Mousetrap);<|endoftext|>


<commit_before>function DEBUG(msg){
    console.log(msg);
}

function jsonTable(dataSource, options){
    // Hang onto the source DOM object.
    var $dataSource = $(dataSource);
    var data = {};
    var $tables;
    var errors = [];

    var defaults = {
        baseTableNames: [],
        baseRowHeaders: [],
        baseColumnHeaders: [],
        jsonIndent: '    ',
        customValidator: function(data, errors){
            return true;
        },
        defaultRowHeaderValue: "",
        defaultColumnHeaderValue: "",
        display: "inline",
        stripEmptyData: true
    };

    $.extend(defaults, options);

    // DEBUG(defaults);

    function updateDataFromSource(){
        data = JSON.parse($dataSource.val());

        // TODO: Better handle this - we need to build up some structure?
        //       Change the sorted order of the JS object?

        $.each(defaults.baseTableNames, function(i, el){
            if (!data[el]){
                data[el] = {};
            }
        });

        if ($tables) {
            $tables.detach();
        }

        if (defaults.display == "inline"){
            $tables = $.tmpl('table', templateData()).appendTo($dataSource.parent());
        } else {
            $tables = $.tmpl('table', templateData()).appendTo('body');
        }
    }

    function getDataFromTable(){
        var newData = {};
        $tables.find('input.cell-value').each(function(i, el){
            $el = $(el);
            // if ($el.val()){
                var table = $el.attr('table');
                var row = $el.attr('row');
                var column = $el.attr('column');
                pathValue(newData, [table, row], column, $el.val());
            // }
        });
        return newData;
    }

    function stripEmptyData(){
        $.each(data, function(label, table){
            $.each(table, function(row, rowData){
                $.each(rowData, function(column, value){
                    if (!value){
                        delete rowData[column];
                    }
                });
                if ($.isEmptyObject(rowData)){
                    delete table[row];
                }
            });
            if ($.isEmptyObject(table)){
                delete data[label];
            };
        });
    }

    function updateSourceFromData(){
        data = getDataFromTable();
        runValidations();

        if (defaults.stripEmptyData){
            stripEmptyData();
        }
        if (!errors.length){
            $dataSource.val(JSON.stringify(data, null, defaults.jsonIndent));
        }
    }

    function runValidations(){
        // DEBUG("Running validations");
        preValidate();
        findDuplicateHeaders();
        defaults.customValidator(data, errors);
        updateErrors();
        // DEBUG("Finished running validations (" + errors.length + " errors found)");
        // if (errors.length) {
        //     DEBUG(errors);
        // }
    }

    function updateErrors(){
        $tables.find('tr, td, th').removeClass('errors');
        $tables.parent().find('.errormsg').detach();
        // DEBUG(errors);
        $.each(errors, function(i,el){
            $($(el).selector).addClass("errors");
            $($(el).selector).children().first().before('
' + el.reason + "
")
        });
        if (errors.length){
            $tables.first().before('
Errors prevented saving
');
        }
    }

    function pathValue(data, keys, last_key, value){
        var curr = data;
        $.each(keys, function(i, val){
            if(!curr[val]){
                curr[val] = {};
            }
            curr = curr[val];
        });
        curr[last_key] = value;
    }

    function templateData(){
        var tdata = [];
        $.each(data, function(key, val){
            tdata.push({
                label: key,
                headers: getHeaders(val),
                data: val,
                defaults: defaults
            });
        });
        return tdata;
    }

    function getHeaders(data){
        var rowHeaders = [], columnHeaders = [];
        $.each(data, function(row, rowData){
            if (!inAny(row, [rowHeaders, defaults.baseRowHeaders])){
                rowHeaders.push(row);
            }
            $.each(rowData, function(column, cell){
                if (!inAny(column, [columnHeaders, defaults.baseColumnHeaders])){
                    columnHeaders.push(column);
                }
            });
        });
        return {
            rowHeaders: $.merge(defaults.baseRowHeaders.slice(), rowHeaders.sort().reverse().slice()),
            columnHeaders: $.merge(defaults.baseColumnHeaders.slice(), columnHeaders.sort().reverse().slice())
        };
    }

    function inAny(value, arrays){
        // is the value in any of the passed in arrays?
        result = false;
        $.each(arrays, function(i, val){
            if ($.inArray(value, val) > -1) {
                result = true;
                return false;
            }
        });
        return result;
    }

    function combineFields($fields, sep, innerSep){
        /*
            Given several jQuery DOM objects, combine the values they
            contain into one value, using sep as seperator.

            If any object is a multiple select, then combine the values
            it returns with innerSep
        */
        return $fields.map(function(i, el){
            $el = $(el);
            if ($el.attr('multiple')){
                return $el.val().join(innerSep);
            }
            return $el.val();
        }).get().join(sep);
    }

    function headerChanged(kind, sep, innerSep){
        return function(evt){
            var $th = $(this).closest('th');
            var $value = $th.find('input:hidden.heading-value');
            var kind_id = $value.attr(kind + '_id');
            var newValue = combineFields($th.find('.fragment').not(':hidden'), sep, innerSep);
            $th.closest('table').find('input[' + kind + '_id="' + kind_id + '"]').attr(kind, newValue);
            $value.val(newValue);
            updateSourceFromData();
            // DEBUG(newValue);
        };
    }
    var rowHeaderChanged = headerChanged('row', ',', '');
    var columnHeaderChanged = headerChanged('column', ';', ',');

    function addRow(evt){
        var $table = $(this).closest('table');
        var label = $table.attr('label');
        $.tmpl('row', {
            row:defaults.defaultRowHeaderValue,
            headers: getHeaders(data[label]),
            data: data,
            label: label,
            row_id: $table.find('tr.data-row').length
        }).insertBefore($(this).closest('tr'));
        // Disable adding a new row until this one has been edited.
        // $(this).attr('disabled','disabled');
        runValidations();
        return false;
    }

    function addColumn(evt){
        // Add a new column header.
        var $table = $(this).closest('table');
        var label = $table.attr('label');
        var column = defaults.defaultColumnHeaderValue;
        var column_id = $table.find('tr.header-row th.column-header').length;
        $.tmpl('column-header', {
            column: column,
            column_id: column_id
        }).insertBefore($(this).closest('th'));
        // Now add a cell to each row in this table.
        $table.find('tr.data-row').each(function(i, el){
            $.tmpl('cell', {
                data:{},
                row_id:i,
                row:$(el).find('th input').val(),
                column_id: column_id,
                column: column,
                table: label
            }).appendTo($(el));
        });
        runValidations();
        return false;
    }

    function preValidate(){
        errors = [];
    }

    function findDuplicateHeaders(data){
        // Ensure that there are no duplicated row or column headings.
        $tables.each(function(i, table){
            $table = $(table);
            $table.find('th.column-header').each(function(j, col){
                $this = $(col).find('input:hidden.heading-value');
                selector = $table.find('th.column-header input:hidden[value="' + $this.val() + '"]').not($this).closest('th');
                if (selector.length){
                    errors.push({
                        selector:selector,
                        reason: "Duplicate " + defaults.columnName
                    });
                }

            });
            $table.find('th.row-header').each(function(j,row){
                $this = $(row).find('input:hidden');
                // DEBUG($this.val());
                selector = $table.find('th.row-header input:hidden[value="'+ $this.val() + '"]').not($this).closest('th');
                if (selector.length){
                    errors.push({
                        selector:selector,
                        reason: "Duplicate " + defaults.rowName
                    });
                }
            });
        });

    }

    // Load the templates for performance reasons.
    // The default templates will have already been loaded
    // from json-table-templates.js (also part of the media for the widget)
    // This will override any of these where the correctly named DOM element
    // exists.
    var template_names = [
        'table',
        'row',
        'column-header',
        'column-header-widget',
        'row-header-widget',
        'cell'
    ];
    $.each(template_names, function(i, name){
        if ($('#' + name + '-template').length){
            $.template(name, $('#' + name + '-template'));
        }
    });

    // DEBUG($.template('column-header-widget'));

    updateDataFromSource();

    // DEBUG('Updated Data');
    /*
    Attach all of the event handlers
    */

    $('th.column-header :input').live('change blur', columnHeaderChanged);
    $('th.row-header :input').live('change blur', rowHeaderChanged);
    $(':input.cell-value').live('change blur', updateSourceFromData);
    $('.add-row').live('click', addRow);
    $('.add-column').live('click', addColumn);
    $dataSource.live('change blur', updateDataFromSource);

    $dataSource.hide();

    return $;
}

<commit_msg>Prevent cells being given incorrect row header values when new columns created. Prevent missing cells when creating columns and rows without editing the headers.<commit_after>function DEBUG(msg){
    console.log(msg);
}

function jsonTable(dataSource, options){
    // Hang onto the source DOM object.
    var $dataSource = $(dataSource);
    var data = {};
    var $tables;
    var errors = [];

    var defaults = {
        baseTableNames: [],
        baseRowHeaders: [],
        baseColumnHeaders: [],
        jsonIndent: '    ',
        customValidator: function(data, errors){
            return true;
        },
        defaultRowHeaderValue: "",
        defaultColumnHeaderValue: "",
        display: "inline",
        stripEmptyData: true
    };

    $.extend(defaults, options);

    // DEBUG(defaults);

    function updateDataFromSource(){
        data = JSON.parse($dataSource.val());

        // TODO: Better handle this - we need to build up some structure?
        //       Change the sorted order of the JS object?

        $.each(defaults.baseTableNames, function(i, el){
            if (!data[el]){
                data[el] = {};
            }
        });

        if ($tables) {
            $tables.detach();
        }

        if (defaults.display == "inline"){
            $tables = $.tmpl('table', templateData()).appendTo($dataSource.parent());
        } else {
            $tables = $.tmpl('table', templateData()).appendTo('body');
        }
    }

    function getDataFromTable(){
        var newData = {};
        $tables.find('input.cell-value').each(function(i, el){
            $el = $(el);
            // if ($el.val()){
                var table = $el.attr('table');
                var row = $el.attr('row');
                var column = $el.attr('column');
                pathValue(newData, [table, row], column, $el.val());
            // }
        });
        return newData;
    }

    function stripEmptyData(){
        $.each(data, function(label, table){
            $.each(table, function(row, rowData){
                $.each(rowData, function(column, value){
                    if (!value){
                        delete rowData[column];
                    }
                });
                if ($.isEmptyObject(rowData)){
                    delete table[row];
                }
            });
            if ($.isEmptyObject(table)){
                delete data[label];
            };
        });
    }

    function updateSourceFromData(){
        data = getDataFromTable();
        runValidations();

        if (defaults.stripEmptyData){
            stripEmptyData();
        }
        if (!errors.length){
            $dataSource.val(JSON.stringify(data, null, defaults.jsonIndent));
        }
    }

    function runValidations(){
        // DEBUG("Running validations");
        preValidate();
        findDuplicateHeaders();
        defaults.customValidator(data, errors);
        updateErrors();
        // DEBUG("Finished running validations (" + errors.length + " errors found)");
        // if (errors.length) {
        //     DEBUG(errors);
        // }
    }

    function updateErrors(){
        $tables.find('tr, td, th').removeClass('errors');
        $tables.parent().find('.errormsg').detach();
        // DEBUG(errors);
        $.each(errors, function(i,el){
            $($(el).selector).addClass("errors");
            $($(el).selector).children().first().before('
' + el.reason + "
")
        });
        if (errors.length){
            $tables.first().before('
Errors prevented saving
');
        }
    }

    function pathValue(data, keys, last_key, value){
        var curr = data;
        $.each(keys, function(i, val){
            if(!curr[val]){
                curr[val] = {};
            }
            curr = curr[val];
        });
        curr[last_key] = value;
    }

    function templateData(){
        var tdata = [];
        $.each(data, function(key, val){
            tdata.push({
                label: key,
                headers: getHeaders(val),
                data: val,
                defaults: defaults
            });
        });
        return tdata;
    }

    function getHeaders(data){
        var rowHeaders = [], columnHeaders = [];
        $.each(data, function(row, rowData){
            if (!inAny(row, [rowHeaders, defaults.baseRowHeaders])){
                rowHeaders.push(row);
            }
            $.each(rowData, function(column, cell){
                if (!inAny(column, [columnHeaders, defaults.baseColumnHeaders])){
                    columnHeaders.push(column);
                }
            });
        });
        return {
            rowHeaders: $.merge(defaults.baseRowHeaders.slice(), rowHeaders.sort().reverse().slice()),
            columnHeaders: $.merge(defaults.baseColumnHeaders.slice(), columnHeaders.sort().reverse().slice())
        };
    }

    function getVisibleHeaders($table) {
        return {
            rowHeaders: $table.find('th.row-header input.heading-value').map(function(i,x){return x.value}),
            columnHeaders: $table.find('th.column-header input.heading-value').map(function(i,x){return x.value})
        };
    }

    function inAny(value, arrays){
        // is the value in any of the passed in arrays?
        result = false;
        $.each(arrays, function(i, val){
            if ($.inArray(value, val) > -1) {
                result = true;
                return false;
            }
        });
        return result;
    }

    function combineFields($fields, sep, innerSep){
        /*
            Given several jQuery DOM objects, combine the values they
            contain into one value, using sep as seperator.

            If any object is a multiple select, then combine the values
            it returns with innerSep
        */
        return $fields.map(function(i, el){
            $el = $(el);
            if ($el.attr('multiple')){
                return $el.val().join(innerSep);
            }
            return $el.val();
        }).get().join(sep);
    }

    function headerChanged(kind, sep, innerSep){
        return function(evt){
            var $th = $(this).closest('th');
            var $value = $th.find('input:hidden.heading-value');
            var kind_id = $value.attr(kind + '_id');
            var newValue = combineFields($th.find('.fragment').not(':hidden'), sep, innerSep);
            $th.closest('table').find('input[' + kind + '_id="' + kind_id + '"]').attr(kind, newValue);
            $value.val(newValue);
            updateSourceFromData();
            // DEBUG(newValue);
        };
    }
    var rowHeaderChanged = headerChanged('row', ',', '');
    var columnHeaderChanged = headerChanged('column', ';', ',');

    function addRow(evt){
        var $table = $(this).closest('table');
        var label = $table.attr('label');
        $.tmpl('row', {
            row:defaults.defaultRowHeaderValue,
            headers: getVisibleHeaders($table),
            data: data,
            label: label,
            row_id: $table.find('tr.data-row').length
        }).insertBefore($(this).closest('tr'));
        // Disable adding a new row until this one has been edited.
        // $(this).attr('disabled','disabled');
        runValidations();
        return false;
    }

    function addColumn(evt){
        // Add a new column header.
        var $table = $(this).closest('table');
        var label = $table.attr('label');
        var column = defaults.defaultColumnHeaderValue;
        var column_id = $table.find('tr.header-row th.column-header').length;
        $.tmpl('column-header', {
            column: column,
            column_id: column_id
        }).insertBefore($(this).closest('th'));
        // Now add a cell to each row in this table.
        $table.find('tr.data-row').each(function(i, el){
            $.tmpl('cell', {
                data:{},
                row_id:i,
                row:$(el).find('th input.heading-value').val(),
                column_id: column_id,
                column: column,
                table: label
            }).appendTo($(el));
        });
        runValidations();
        return false;
    }

    function preValidate(){
        errors = [];
    }

    function findDuplicateHeaders(data){
        // Ensure that there are no duplicated row or column headings.
        $tables.each(function(i, table){
            $table = $(table);
            $table.find('th.column-header').each(function(j, col){
                $this = $(col).find('input:hidden.heading-value');
                selector = $table.find('th.column-header input:hidden[value="' + $this.val() + '"]').not($this).closest('th');
                if (selector.length){
                    errors.push({
                        selector:selector,
                        reason: "Duplicate " + defaults.columnName
                    });
                }

            });
            $table.find('th.row-header').each(function(j,row){
                $this = $(row).find('input:hidden');
                // DEBUG($this.val());
                selector = $table.find('th.row-header input:hidden[value="'+ $this.val() + '"]').not($this).closest('th');
                if (selector.length){
                    errors.push({
                        selector:selector,
                        reason: "Duplicate " + defaults.rowName
                    });
                }
            });
        });

    }

    // Load the templates for performance reasons.
    // The default templates will have already been loaded
    // from json-table-templates.js (also part of the media for the widget)
    // This will override any of these where the correctly named DOM element
    // exists.
    var template_names = [
        'table',
        'row',
        'column-header',
        'column-header-widget',
        'row-header-widget',
        'cell'
    ];
    $.each(template_names, function(i, name){
        if ($('#' + name + '-template').length){
            $.template(name, $('#' + name + '-template'));
        }
    });

    // DEBUG($.template('column-header-widget'));

    updateDataFromSource();

    // DEBUG('Updated Data');
    /*
    Attach all of the event handlers
    */

    $('th.column-header :input').live('change blur', columnHeaderChanged);
    $('th.row-header :input').live('change blur', rowHeaderChanged);
    $(':input.cell-value').live('change blur', updateSourceFromData);
    $('.add-row').live('click', addRow);
    $('.add-column').live('click', addColumn);
    $dataSource.live('change blur', updateDataFromSource);

    $dataSource.hide();

    return $;
}

<|endoftext|>