<commit_before>var expect = require('expect');
var Path = require('../Path');

describe('Path.extractParamNames', function () {
  describe('when a pattern contains no dynamic segments', function () {
    it('returns an empty array', function () {
      expect(Path.extractParamNames('a/b/c')).toEqual([]);
    });
  });

  describe('when a pattern contains :a and :b dynamic segments', function () {
    it('returns the correct names', function () {
      expect(Path.extractParamNames('/comments/:a/:b/edit')).toEqual([ 'a', 'b' ]);
    });
  });

  describe('when a pattern has a *', function () {
    it('uses the name "splat"', function () {
      expect(Path.extractParamNames('/files/*.jpg')).toEqual([ 'splat' ]);
    });
  });
});

describe('Path.extractParams', function () {
  describe('when a pattern does not have dynamic segments', function () {
    var pattern = 'a/b/c';

    describe('and the path matches', function () {
      it('returns an empty object', function () {
        expect(Path.extractParams(pattern, pattern)).toEqual({});
      });
    });

    describe('and the path does not match', function () {
      it('returns null', function () {
        expect(Path.extractParams(pattern, 'd/e/f')).toBe(null);
      });
    });
  });

  describe('when a pattern has dynamic segments', function () {
    var pattern = 'comments/:id.:ext/edit';

    describe('and the path matches', function () {
      it('returns an object with the params', function () {
        expect(Path.extractParams(pattern, 'comments/abc.js/edit')).toEqual({ id: 'abc', ext: 'js' });
      });
    });

    describe('and the path does not match', function () {
      it('returns null', function () {
        expect(Path.extractParams(pattern, 'users/123')).toBe(null);
      });
    });

    describe('and the path matches with a segment containing a .', function () {
      it('returns an object with the params', function () {
        expect(Path.extractParams(pattern, 'comments/foo.bar/edit')).toEqual({ id: 'foo', ext: 'bar' });
      });
    });
  });

  describe('when a pattern has characters that have special URL encoding', function () {
    var pattern = 'one, two';

    describe('and the path matches', function () {
      it('returns an empty object', function () {
        expect(Path.extractParams(pattern, 'one%2C+two')).toEqual({});
      });
    });

    describe('and the path does not match', function () {
      it('returns null', function () {
        expect(Path.extractParams(pattern, 'one+two')).toBe(null);
      });
    });
  });

  describe('when a pattern has dynamic segments and characters that have special URL encoding', function () {
    var pattern = '/comments/:id/edit now';

    describe('and the path matches', function () {
      it('returns an object with the params', function () {
        expect(Path.extractParams(pattern, '/comments/abc/edit+now')).toEqual({ id: 'abc' });
      });
    });

    describe('and the path does not match', function () {
      it('returns null', function () {
        expect(Path.extractParams(pattern, '/users/123')).toBe(null);
      });
    });
  });

  describe('when a pattern has a *', function () {
    describe('and the path matches', function () {
      it('returns an object with the params', function () {
        expect(Path.extractParams('/files/*', '/files/my/photo.jpg')).toEqual({ splat: 'my/photo.jpg' });
        expect(Path.extractParams('/files/*', '/files/my/photo.jpg.zip')).toEqual({ splat: 'my/photo.jpg.zip' });
        expect(Path.extractParams('/files/*.jpg', '/files/my/photo.jpg')).toEqual({ splat: 'my/photo' });
      });
    });

    describe('and the path does not match', function () {
      it('returns null', function () {
        expect(Path.extractParams('/files/*.jpg', '/files/my/photo.png')).toBe(null);
      });
    });
  });

  describe('when a pattern has a ?', function () {
    var pattern = '/archive/?:name?';

    describe('and the path matches', function () {
      it('returns an object with the params', function () {
        expect(Path.extractParams(pattern, '/archive')).toEqual({ name: undefined });
        expect(Path.extractParams(pattern, '/archive/')).toEqual({ name: undefined });
        expect(Path.extractParams(pattern, '/archive/foo')).toEqual({ name: 'foo' });
        expect(Path.extractParams(pattern, '/archivefoo')).toEqual({ name: 'foo' });
      });
    });

    describe('and the path does not match', function () {
      it('returns null', function () {
        expect(Path.extractParams(pattern, '/archiv')).toBe(null);
      });
    });
  });

  describe('when a param has dots', function () {
    var pattern = '/:query/with/:domain';

    describe('and the path matches', function () {
      it('returns an object with the params', function () {
        expect(Path.extractParams(pattern, '/foo/with/foo.app')).toEqual({ query: 'foo', domain: 'foo.app' });
        expect(Path.extractParams(pattern, '/foo.ap/with/foo')).toEqual({ query: 'foo.ap', domain: 'foo' });
        expect(Path.extractParams(pattern, '/foo.ap/with/foo.app')).toEqual({ query: 'foo.ap', domain: 'foo.app' });
      });
    });

    describe('and the path does not match', function () {
      it('returns null', function () {
        expect(Path.extractParams(pattern, '/foo.ap')).toBe(null);
      });
    });
  });
});

describe('Path.injectParams', function () {
  describe('when a pattern does not have dynamic segments', function () {
    var pattern = 'a/b/c';

    it('returns the pattern', function () {
      expect(Path.injectParams(pattern, {})).toEqual(pattern);
    });
  });

  describe('when a pattern has dynamic segments', function () {
    var pattern = 'comments/:id/edit';

    describe('and a param is missing', function () {
      it('throws an Error', function () {
        expect(function () {
          Path.injectParams(pattern, {})
        }).toThrow(Error);
      });
    });

    describe('and all params are present', function () {
      it('returns the correct path', function () {
        expect(Path.injectParams(pattern, { id: 'abc' })).toEqual('comments/abc/edit');
      });

      it('returns the correct path when the value is 0', function () {
        expect(Path.injectParams(pattern, { id: 0 })).toEqual('comments/0/edit');
      });
    });

    describe('and some params have special URL encoding', function () {
      it('returns the correct path', function () {
        expect(Path.injectParams(pattern, { id: 'one, two' })).toEqual('comments/one%2C+two/edit');
      });
    });

    describe('and a param has a forward slash', function () {
      it('preserves the forward slash', function () {
        expect(Path.injectParams(pattern, { id: 'the/id' })).toEqual('comments/the/id/edit');
      });
    });

    describe('and some params contain dots', function () {
      it('returns the correct path', function () {
        expect(Path.injectParams(pattern, { id: 'alt.black.helicopter' })).toEqual('comments/alt.black.helicopter/edit');
      });
    });
  });

  describe('when a pattern has one splat', function () {
    it('returns the correct path', function () {
      expect(Path.injectParams('/a/*/d', { splat: 'b/c' })).toEqual('/a/b/c/d');
    });
  });

  describe('when a pattern has multiple splats', function () {
    it('returns the correct path', function () {
      expect(Path.injectParams('/a/*/c/*', { splat: [ 'b', 'd' ] })).toEqual('/a/b/c/d');
    });
  });
});

describe('Path.extractQuery', function () {
  describe('when the path contains a query string', function () {
    it('returns the parsed query object', function () {
      expect(Path.extractQuery('/?id=def&show=true')).toEqual({ id: 'def', show: 'true' });
    });

    it('properly handles arrays', function () {
      expect(Path.extractQuery('/?id%5B%5D=a&id%5B%5D=b')).toEqual({ id: [ 'a', 'b' ] });
    });
  });

  describe('when the path does not contain a query string', function () {
    it('returns null', function () {
      expect(Path.extractQuery('/a/b/c')).toBe(null);
    });
  });
});

describe('Path.withoutQuery', function () {
  it('removes the query string', function () {
    expect(Path.withoutQuery('/a/b/c?id=def')).toEqual('/a/b/c');
  });
});

describe('Path.withQuery', function () {
  it('appends the query string', function () {
    expect(Path.withQuery('/a/b/c', { id: 'def' })).toEqual('/a/b/c?id=def');
  });

  it('merges two query strings', function () {
    expect(Path.withQuery('/path?a=b', { c: [ 'd', 'e' ]})).toEqual('/path?a=b&c%5B0%5D=d&c%5B1%5D=e');
  });
});

describe('Path.normalize', function () {
  describe('on a path with no slashes at the beginning', function () {
    it('adds a slash', function () {
      expect(Path.normalize('a/b/c')).toEqual('/a/b/c');
    });
  });

  describe('on a path with a single slash at the beginning', function () {
    it('preserves the slash', function () {
      expect(Path.normalize('/a/b/c')).toEqual('/a/b/c');
    });
  });

  describe('on a path with many slashes at the beginning', function () {
    it('reduces them to a single slash', function () {
      expect(Path.normalize('//a/b/c')).toEqual('/a/b/c');
    });
  });
});
<commit_msg>more splat tests<commit_after>var expect = require('expect');
var Path = require('../Path');

describe('Path.extractParamNames', function () {
  describe('when a pattern contains no dynamic segments', function () {
    it('returns an empty array', function () {
      expect(Path.extractParamNames('a/b/c')).toEqual([]);
    });
  });

  describe('when a pattern contains :a and :b dynamic segments', function () {
    it('returns the correct names', function () {
      expect(Path.extractParamNames('/comments/:a/:b/edit')).toEqual([ 'a', 'b' ]);
    });
  });

  describe('when a pattern has a *', function () {
    it('uses the name "splat"', function () {
      expect(Path.extractParamNames('/files/*.jpg')).toEqual([ 'splat' ]);
    });
  });
});

describe('Path.extractParams', function () {
  describe('when a pattern does not have dynamic segments', function () {
    var pattern = 'a/b/c';

    describe('and the path matches', function () {
      it('returns an empty object', function () {
        expect(Path.extractParams(pattern, pattern)).toEqual({});
      });
    });

    describe('and the path does not match', function () {
      it('returns null', function () {
        expect(Path.extractParams(pattern, 'd/e/f')).toBe(null);
      });
    });
  });

  describe('when a pattern has dynamic segments', function () {
    var pattern = 'comments/:id.:ext/edit';

    describe('and the path matches', function () {
      it('returns an object with the params', function () {
        expect(Path.extractParams(pattern, 'comments/abc.js/edit')).toEqual({ id: 'abc', ext: 'js' });
      });
    });

    describe('and the path does not match', function () {
      it('returns null', function () {
        expect(Path.extractParams(pattern, 'users/123')).toBe(null);
      });
    });

    describe('and the path matches with a segment containing a .', function () {
      it('returns an object with the params', function () {
        expect(Path.extractParams(pattern, 'comments/foo.bar/edit')).toEqual({ id: 'foo', ext: 'bar' });
      });
    });
  });

  describe('when a pattern has characters that have special URL encoding', function () {
    var pattern = 'one, two';

    describe('and the path matches', function () {
      it('returns an empty object', function () {
        expect(Path.extractParams(pattern, 'one%2C+two')).toEqual({});
      });
    });

    describe('and the path does not match', function () {
      it('returns null', function () {
        expect(Path.extractParams(pattern, 'one+two')).toBe(null);
      });
    });
  });

  describe('when a pattern has dynamic segments and characters that have special URL encoding', function () {
    var pattern = '/comments/:id/edit now';

    describe('and the path matches', function () {
      it('returns an object with the params', function () {
        expect(Path.extractParams(pattern, '/comments/abc/edit+now')).toEqual({ id: 'abc' });
      });
    });

    describe('and the path does not match', function () {
      it('returns null', function () {
        expect(Path.extractParams(pattern, '/users/123')).toBe(null);
      });
    });
  });

  describe('when a pattern has a *', function () {
    describe('and the path matches', function () {
      it('returns an object with the params', function () {
        expect(Path.extractParams('/files/*', '/files/my/photo.jpg')).toEqual({ splat: 'my/photo.jpg' });
        expect(Path.extractParams('/files/*', '/files/my/photo.jpg.zip')).toEqual({ splat: 'my/photo.jpg.zip' });
        expect(Path.extractParams('/files/*.jpg', '/files/my/photo.jpg')).toEqual({ splat: 'my/photo' });
      });
    });

    describe('and the path does not match', function () {
      it('returns null', function () {
        expect(Path.extractParams('/files/*.jpg', '/files/my/photo.png')).toBe(null);
      });
    });
  });

  describe('when a pattern has a ?', function () {
    var pattern = '/archive/?:name?';

    describe('and the path matches', function () {
      it('returns an object with the params', function () {
        expect(Path.extractParams(pattern, '/archive')).toEqual({ name: undefined });
        expect(Path.extractParams(pattern, '/archive/')).toEqual({ name: undefined });
        expect(Path.extractParams(pattern, '/archive/foo')).toEqual({ name: 'foo' });
        expect(Path.extractParams(pattern, '/archivefoo')).toEqual({ name: 'foo' });
      });
    });

    describe('and the path does not match', function () {
      it('returns null', function () {
        expect(Path.extractParams(pattern, '/archiv')).toBe(null);
      });
    });
  });

  describe('when a param has dots', function () {
    var pattern = '/:query/with/:domain';

    describe('and the path matches', function () {
      it('returns an object with the params', function () {
        expect(Path.extractParams(pattern, '/foo/with/foo.app')).toEqual({ query: 'foo', domain: 'foo.app' });
        expect(Path.extractParams(pattern, '/foo.ap/with/foo')).toEqual({ query: 'foo.ap', domain: 'foo' });
        expect(Path.extractParams(pattern, '/foo.ap/with/foo.app')).toEqual({ query: 'foo.ap', domain: 'foo.app' });
      });
    });

    describe('and the path does not match', function () {
      it('returns null', function () {
        expect(Path.extractParams(pattern, '/foo.ap')).toBe(null);
      });
    });
  });
});

describe('Path.injectParams', function () {
  describe('when a pattern does not have dynamic segments', function () {
    var pattern = 'a/b/c';

    it('returns the pattern', function () {
      expect(Path.injectParams(pattern, {})).toEqual(pattern);
    });
  });

  describe('when a pattern has dynamic segments', function () {
    var pattern = 'comments/:id/edit';

    describe('and a param is missing', function () {
      it('throws an Error', function () {
        expect(function () {
          Path.injectParams(pattern, {})
        }).toThrow(Error);
      });
    });

    describe('and all params are present', function () {
      it('returns the correct path', function () {
        expect(Path.injectParams(pattern, { id: 'abc' })).toEqual('comments/abc/edit');
      });

      it('returns the correct path when the value is 0', function () {
        expect(Path.injectParams(pattern, { id: 0 })).toEqual('comments/0/edit');
      });
    });

    describe('and some params have special URL encoding', function () {
      it('returns the correct path', function () {
        expect(Path.injectParams(pattern, { id: 'one, two' })).toEqual('comments/one%2C+two/edit');
      });
    });

    describe('and a param has a forward slash', function () {
      it('preserves the forward slash', function () {
        expect(Path.injectParams(pattern, { id: 'the/id' })).toEqual('comments/the/id/edit');
      });
    });

    describe('and some params contain dots', function () {
      it('returns the correct path', function () {
        expect(Path.injectParams(pattern, { id: 'alt.black.helicopter' })).toEqual('comments/alt.black.helicopter/edit');
      });
    });
  });

  describe('when a pattern has one splat', function () {
    it('returns the correct path', function () {
      expect(Path.injectParams('/a/*/d', { splat: 'b/c' })).toEqual('/a/b/c/d');
    });
  });

  describe('when a pattern has multiple splats', function () {
    it('returns the correct path', function () {
      expect(Path.injectParams('/a/*/c/*', { splat: [ 'b', 'd' ] })).toEqual('/a/b/c/d');
    });

    it('complains if not given enough splat values', function () {
      expect(function() {
        Path.injectParams('/a/*/c/*', { splat: [ 'b' ] });
      }).toThrow(Error);
    });
  });
});

describe('Path.extractQuery', function () {
  describe('when the path contains a query string', function () {
    it('returns the parsed query object', function () {
      expect(Path.extractQuery('/?id=def&show=true')).toEqual({ id: 'def', show: 'true' });
    });

    it('properly handles arrays', function () {
      expect(Path.extractQuery('/?id%5B%5D=a&id%5B%5D=b')).toEqual({ id: [ 'a', 'b' ] });
    });
  });

  describe('when the path does not contain a query string', function () {
    it('returns null', function () {
      expect(Path.extractQuery('/a/b/c')).toBe(null);
    });
  });
});

describe('Path.withoutQuery', function () {
  it('removes the query string', function () {
    expect(Path.withoutQuery('/a/b/c?id=def')).toEqual('/a/b/c');
  });
});

describe('Path.withQuery', function () {
  it('appends the query string', function () {
    expect(Path.withQuery('/a/b/c', { id: 'def' })).toEqual('/a/b/c?id=def');
  });

  it('merges two query strings', function () {
    expect(Path.withQuery('/path?a=b', { c: [ 'd', 'e' ]})).toEqual('/path?a=b&c%5B0%5D=d&c%5B1%5D=e');
  });
});

describe('Path.normalize', function () {
  describe('on a path with no slashes at the beginning', function () {
    it('adds a slash', function () {
      expect(Path.normalize('a/b/c')).toEqual('/a/b/c');
    });
  });

  describe('on a path with a single slash at the beginning', function () {
    it('preserves the slash', function () {
      expect(Path.normalize('/a/b/c')).toEqual('/a/b/c');
    });
  });

  describe('on a path with many slashes at the beginning', function () {
    it('reduces them to a single slash', function () {
      expect(Path.normalize('//a/b/c')).toEqual('/a/b/c');
    });
  });
});
<|endoftext|>


<commit_before>import { t } from '../util/locale';
import { matcher, brands } from 'name-suggestion-index';

import { actionChangePreset } from '../actions/change_preset';
import { actionChangeTags } from '../actions/change_tags';
import { actionUpgradeTags } from '../actions/upgrade_tags';
import { osmEntity } from '../osm/entity';
import { osmIsOldMultipolygonOuterMember, osmOldMultipolygonOuterMemberOfRelation } from '../osm/multipolygon';
import { utilDisplayLabel, utilTagDiff } from '../util';
import { validationIssue, validationIssueFix } from '../core/validation';

export function validationOutdatedTags() {
    var type = 'outdated_tags';

    // initialize name-suggestion-index matcher
    var nsiMatcher = matcher();
    nsiMatcher.buildMatchIndex(brands.brands);
    var nsiKeys = ['amenity', 'shop', 'tourism', 'leisure', 'office'];

    function oldTagIssues(entity, context) {
        var graph = context.graph();
        var oldTags = Object.assign({}, entity.tags);  // shallow copy
        var preset = context.presets().match(entity, graph);
        var explicitPresetUpgrade = preset.replacement;
        var subtype = 'deprecated_tags';

        // upgrade preset..
        if (preset.replacement) {
            var newPreset = context.presets().item(preset.replacement);
            graph = actionChangePreset(entity.id, preset, newPreset, true)(graph);  // true = skip field defaults
            entity = graph.entity(entity.id);
            preset = newPreset;
        }

        // upgrade tags..
        var deprecatedTags = entity.deprecatedTags();
        if (deprecatedTags.length) {
            deprecatedTags.forEach(function(tag) {
                graph = actionUpgradeTags(entity.id, tag.old, tag.replace)(graph);
            });
            entity = graph.entity(entity.id);
        }

        // add missing addTags..
        var newTags = Object.assign({}, entity.tags);  // shallow copy
        if (preset.tags !== preset.addTags) {
            Object.keys(preset.addTags).forEach(function(k) {
                if (!newTags[k]) {
                    newTags[k] = preset.addTags[k];
                    if (!explicitPresetUpgrade) {
                        subtype = 'incomplete_tags';
                    }
                }
            });
        }

        // search name-suggestion-index
        if (newTags.name) {
            for (var i = 0; i < nsiKeys.length; i++) {
                var k = nsiKeys[i];
                if (!newTags[k]) continue;

                var match = nsiMatcher.matchKVN(k, newTags[k], newTags.name);
                if (!match) continue;

                // for now skip ambiguous matches (like Target~(USA) vs Target~(Australia))
                if (match.d) continue;

                var brand = brands.brands[match.kvnd];
                if (brand && brand.tags['brand:wikidata']) {
                    Object.assign(newTags, brand.tags);
                    break;
                }
            }
        }

        // determine diff
        var tagDiff = utilTagDiff(oldTags, newTags);
        if (!tagDiff.length) return [];

        var prefix = subtype === 'incomplete_tags' ? 'incomplete.' : '';

        return [new validationIssue({
            type: type,
            subtype: subtype,
            severity: 'warning',
            message: function() {
                var entity = context.hasEntity(this.entityIds[0]);
                return entity ? t('issues.outdated_tags.' + prefix + 'message', { feature: utilDisplayLabel(entity, context) }) : '';
            },
            reference: showReference,
            entityIds: [entity.id],
            hash: osmEntity.key(entity),
            fixes: [
                new validationIssueFix({
                    autoArgs: [doUpgrade, t('issues.fix.upgrade_tags.annotation')],
                    title: t('issues.fix.upgrade_tags.title'),
                    onClick: function() {
                        context.perform(doUpgrade, t('issues.fix.upgrade_tags.annotation'));
                    }
                })
            ]
        })];

        function doUpgrade(graph) {
            return actionChangeTags(entity.id, newTags)(graph);
        }

        function showReference(selection) {
            var enter = selection.selectAll('.issue-reference')
                .data([0])
                .enter();

            enter
                .append('div')
                .attr('class', 'issue-reference')
                .text(t('issues.outdated_tags.' + prefix + 'reference'));

            enter
                .append('strong')
                .text(t('issues.suggested'));

            enter
                .append('table')
                .attr('class', 'tagDiff-table')
                .selectAll('.tagDiff-row')
                .data(tagDiff)
                .enter()
                .append('tr')
                .attr('class', 'tagDiff-row')
                .append('td')
                .attr('class', function(d) {
                    var klass = d.type === '+' ? 'add' : 'remove';
                    return 'tagDiff-cell tagDiff-cell-' + klass;
                })
                .text(function(d) { return d.display; });
        }
    }

    function oldMultipolygonIssues(entity, context) {

        var graph = context.graph();

        var multipolygon, outerWay;
        if (entity.type === 'relation') {
            outerWay = osmOldMultipolygonOuterMemberOfRelation(entity, graph);
            multipolygon = entity;
        } else if (entity.type === 'way') {
            multipolygon = osmIsOldMultipolygonOuterMember(entity, graph);
            outerWay = entity;
        } else {
            return [];
        }

        if (!multipolygon || !outerWay) return [];

        return [new validationIssue({
            type: type,
            subtype: 'old_multipolygon',
            severity: 'warning',
            message: function() {
                var entity = context.hasEntity(this.issue.entityIds[1]);
                return entity ? t('issues.old_multipolygon.message', { multipolygon: utilDisplayLabel(entity, context) }) : '';
            },
            reference: showReference,
            entityIds: [outerWay.id, multipolygon.id],
            fixes: [
                new validationIssueFix({
                    autoArgs: [doUpgrade, t('issues.fix.move_tags.annotation')],
                    title: t('issues.fix.move_tags.title'),
                    onClick: function() {
                        context.perform(doUpgrade, t('issues.fix.move_tags.annotation'));
                    }
                })
            ]
        })];

        function doUpgrade(graph) {
            multipolygon = multipolygon.mergeTags(outerWay.tags);
            graph = graph.replace(multipolygon);
            return actionChangeTags(outerWay.id, {})(graph);
        }

        function showReference(selection) {
            selection.selectAll('.issue-reference')
                .data([0])
                .enter()
                .append('div')
                .attr('class', 'issue-reference')
                .text(t('issues.old_multipolygon.reference'));
        }
    }

    var validation = function checkOutdatedTags(entity, context) {
        var issues = oldMultipolygonIssues(entity, context);
        if (!issues.length) issues = oldTagIssues(entity, context);
        return issues;
    };

    validation.type = type;

    return validation;
}
<commit_msg>Undo 5b8db55b4 and instead get latest entity at time of fix<commit_after>import { t } from '../util/locale';
import { matcher, brands } from 'name-suggestion-index';

import { actionChangePreset } from '../actions/change_preset';
import { actionChangeTags } from '../actions/change_tags';
import { actionUpgradeTags } from '../actions/upgrade_tags';
import { osmIsOldMultipolygonOuterMember, osmOldMultipolygonOuterMemberOfRelation } from '../osm/multipolygon';
import { utilDisplayLabel, utilTagDiff } from '../util';
import { validationIssue, validationIssueFix } from '../core/validation';

export function validationOutdatedTags() {
    var type = 'outdated_tags';

    // initialize name-suggestion-index matcher
    var nsiMatcher = matcher();
    nsiMatcher.buildMatchIndex(brands.brands);
    var nsiKeys = ['amenity', 'shop', 'tourism', 'leisure', 'office'];

    function oldTagIssues(entity, context) {
        var graph = context.graph();
        var oldTags = Object.assign({}, entity.tags);  // shallow copy
        var preset = context.presets().match(entity, graph);
        var explicitPresetUpgrade = preset.replacement;
        var subtype = 'deprecated_tags';

        // upgrade preset..
        if (preset.replacement) {
            var newPreset = context.presets().item(preset.replacement);
            graph = actionChangePreset(entity.id, preset, newPreset, true)(graph);  // true = skip field defaults
            entity = graph.entity(entity.id);
            preset = newPreset;
        }

        // upgrade tags..
        var deprecatedTags = entity.deprecatedTags();
        if (deprecatedTags.length) {
            deprecatedTags.forEach(function(tag) {
                graph = actionUpgradeTags(entity.id, tag.old, tag.replace)(graph);
            });
            entity = graph.entity(entity.id);
        }

        // add missing addTags..
        var newTags = Object.assign({}, entity.tags);  // shallow copy
        if (preset.tags !== preset.addTags) {
            Object.keys(preset.addTags).forEach(function(k) {
                if (!newTags[k]) {
                    newTags[k] = preset.addTags[k];
                    if (!explicitPresetUpgrade) {
                        subtype = 'incomplete_tags';
                    }
                }
            });
        }

        // search name-suggestion-index
        if (newTags.name) {
            for (var i = 0; i < nsiKeys.length; i++) {
                var k = nsiKeys[i];
                if (!newTags[k]) continue;

                var match = nsiMatcher.matchKVN(k, newTags[k], newTags.name);
                if (!match) continue;

                // for now skip ambiguous matches (like Target~(USA) vs Target~(Australia))
                if (match.d) continue;

                var brand = brands.brands[match.kvnd];
                if (brand && brand.tags['brand:wikidata']) {
                    Object.assign(newTags, brand.tags);
                    break;
                }
            }
        }

        // determine diff
        var tagDiff = utilTagDiff(oldTags, newTags);
        if (!tagDiff.length) return [];

        var prefix = subtype === 'incomplete_tags' ? 'incomplete.' : '';

        return [new validationIssue({
            type: type,
            subtype: subtype,
            severity: 'warning',
            message: showMessage,
            reference: showReference,
            entityIds: [entity.id],
            hash: JSON.stringify(tagDiff),
            fixes: [
                new validationIssueFix({
                    autoArgs: [doUpgrade, t('issues.fix.upgrade_tags.annotation')],
                    title: t('issues.fix.upgrade_tags.title'),
                    onClick: function() {
                        context.perform(doUpgrade, t('issues.fix.upgrade_tags.annotation'));
                    }
                })
            ]
        })];

        function doUpgrade(graph) {
            var currEntity = context.hasEntity(entity.id);
            if (!currEntity) return graph;

            var newTags = Object.assign({}, currEntity.tags);  // shallow copy
            tagDiff.forEach(function(diff) {
                if (diff.type === '-') {
                    delete newTags[diff.key];
                } else if (diff.type === '+') {
                    newTags[diff.key] = diff.newVal;
                }
            });

            return actionChangeTags(currEntity.id, newTags)(graph);
        }

        function showMessage() {
            var currEntity = context.hasEntity(entity.id);
            if (!currEntity) return '';

            return t('issues.outdated_tags.' + prefix + 'message',
                { feature: utilDisplayLabel(currEntity, context) }
            );
        }

        function showReference(selection) {
            var enter = selection.selectAll('.issue-reference')
                .data([0])
                .enter();

            enter
                .append('div')
                .attr('class', 'issue-reference')
                .text(t('issues.outdated_tags.' + prefix + 'reference'));

            enter
                .append('strong')
                .text(t('issues.suggested'));

            enter
                .append('table')
                .attr('class', 'tagDiff-table')
                .selectAll('.tagDiff-row')
                .data(tagDiff)
                .enter()
                .append('tr')
                .attr('class', 'tagDiff-row')
                .append('td')
                .attr('class', function(d) {
                    var klass = d.type === '+' ? 'add' : 'remove';
                    return 'tagDiff-cell tagDiff-cell-' + klass;
                })
                .text(function(d) { return d.display; });
        }
    }

    function oldMultipolygonIssues(entity, context) {
        var graph = context.graph();

        var multipolygon, outerWay;
        if (entity.type === 'relation') {
            outerWay = osmOldMultipolygonOuterMemberOfRelation(entity, graph);
            multipolygon = entity;
        } else if (entity.type === 'way') {
            multipolygon = osmIsOldMultipolygonOuterMember(entity, graph);
            outerWay = entity;
        } else {
            return [];
        }

        if (!multipolygon || !outerWay) return [];

        return [new validationIssue({
            type: type,
            subtype: 'old_multipolygon',
            severity: 'warning',
            message: showMessage,
            reference: showReference,
            entityIds: [outerWay.id, multipolygon.id],
            fixes: [
                new validationIssueFix({
                    autoArgs: [doUpgrade, t('issues.fix.move_tags.annotation')],
                    title: t('issues.fix.move_tags.title'),
                    onClick: function() {
                        context.perform(doUpgrade, t('issues.fix.move_tags.annotation'));
                    }
                })
            ]
        })];

        function doUpgrade(graph) {
            var currMultipolygon = context.hasEntity(multipolygon.id);
            var currOuterWay = context.hasEntity(outerWay.id);
            if (!currMultipolygon || !currOuterWay) return graph;

            currMultipolygon = currMultipolygon.mergeTags(currOuterWay.tags);
            graph = graph.replace(currMultipolygon);
            return actionChangeTags(currOuterWay.id, {})(graph);
        }

        function showMessage() {
            var currMultipolygon = context.hasEntity(multipolygon.id);
            if (!currMultipolygon) return '';

            return t('issues.old_multipolygon.message',
                { multipolygon: utilDisplayLabel(currMultipolygon, context) }
            );
        }

        function showReference(selection) {
            selection.selectAll('.issue-reference')
                .data([0])
                .enter()
                .append('div')
                .attr('class', 'issue-reference')
                .text(t('issues.old_multipolygon.reference'));
        }
    }

    var validation = function checkOutdatedTags(entity, context) {
        var issues = oldMultipolygonIssues(entity, context);
        if (!issues.length) issues = oldTagIssues(entity, context);
        return issues;
    };

    validation.type = type;

    return validation;
}
<|endoftext|>


<commit_before>/* -*- Mode: Javascript; indent-tabs-mode:nil; js-indent-level: 2 -*- */
/* vim: set ts=2 et sw=2 tw=80: */

/*************************************************************
 *
 *  MathJax/extensions/AssistiveMML.js
 *
 *  Implements an extension that inserts hidden MathML into the
 *  page for screen readers or other asistive technology.
 *
 *  ---------------------------------------------------------------------
 *
 *  Copyright (c) 2015 The MathJax Consortium
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

(function (AJAX,CALLBACK,HUB,HTML) {
  var SETTINGS = HUB.config.menuSettings;

  var AssistiveMML = MathJax.Extension["AssistiveMML"] = {
    version: "2.6.1",

    config: HUB.CombineConfig("AssistiveMML",{
      disabled: false,
      styles: {
        ".MJX_Assistive_MathML": {
          position:"absolute!important",
          top: 0, left: 0,
          clip: (HUB.Browser.isMSIE && (document.documentMode||0) < 8 ?
                 "rect(1px 1px 1px 1px)" : "rect(1px, 1px, 1px, 1px)"),
          padding: "1px 0 0 0!important",
          border: "0!important",
          height: "1px!important",
          width: "1px!important",
          overflow: "hidden!important",
          display:"block!important",
          //
          //  Don't allow the assistive MathML become part of the selection
          //
          "-webkit-touch-callout": "none",
          "-webkit-user-select": "none",
          "-khtml-user-select": "none",
          "-moz-user-select": "none",
          "-ms-user-select": "none",
          "user-select": "none"
        },
        ".MJX_Assistive_MathML.MJX_Assistive_MathML_Block": {
          width: "100%!important"
        }
      }
    }),

    Config: function () {
      if (!this.config.disabled && SETTINGS.assistiveMML == null)
        HUB.Config({menuSettings:{assistiveMML:true}});
      AJAX.Styles(this.config.styles);
      HUB.Register.MessageHook("End Math",function (msg) {
        if (SETTINGS.assistiveMML) return AssistiveMML.AddAssistiveMathML(msg[1])
      });
    },

    //
    //  This sets up a state object that lists the jax and index into the jax,
    //    and a dummy callback that is used to synchronizing with MathJax.
    //    It will be called when the jax are all processed, and that will
    //    let the MathJax queue continue (it will block until then).
    //
    AddAssistiveMathML: function (node) {
      var state = {
        jax: HUB.getAllJax(node), i: 0,
        callback: MathJax.Callback({})
      };
      this.HandleMML(state);
      return state.callback;
    },

    //
    //  This removes the data-mathml attribute and the assistive MathML from
    //  all the jax.
    //
    RemoveAssistiveMathML: function (node) {
      var jax = HUB.getAllJax(node), frame;
      for (var i = 0, m = jax.length; i < m; i++) {
        frame = document.getElementById(jax[i].inputID+"-Frame");
        if (frame && frame.getAttribute("data-mathml")) {
          frame.removeAttribute("data-mathml");
          if (frame.lastChild && frame.lastChild.className.match(/MJX_Assistive_MathML/))
            frame.removeChild(frame.lastChild);
        }
      }
    },

    //
    //  For each jax in the state, look up the frame.
    //  If the jax doesn't use NativeMML and hasn't already been handled:
    //    Get the MathML for the jax, taking resets into account.
    //    Add a data-mathml attribute to the frame, and
    //    Create a span that is not visible on screen and put the MathML in it,
    //      and add it to the frame.
    //  When all the jax are processed, call the callback.
    //
    HandleMML: function (state) {
      var m = state.jax.length, jax, mml, frame, span;
      while (state.i < m) {
        jax = state.jax[state.i];
        frame = document.getElementById(jax.inputID+"-Frame");
        if (jax.outputJax !== "NativeMML" && frame && !frame.getAttribute("data-mathml")) {
          try {
            mml = jax.root.toMathML("").replace(/\n */g,"").replace(/<!--.*?-->/g,"");
          } catch (err) {
            if (!err.restart) throw err; // an actual error
            return MathJax.Callback.After(["HandleMML",this,state],err.restart);
          }
          frame.setAttribute("data-mathml",mml);
          span = HTML.addElement(frame,"span",{
            isMathJax: true, unselectable: "on",
            className: "MJX_Assistive_MathML"
              + (jax.root.Get("display") === "block" ? " MJX_Assistive_MathML_Block" : "")
          });
          span.innerHTML = mml;
          frame.style.position = "relative";
          frame.setAttribute("role","presentation");
          frame.firstChild.setAttribute("aria-hidden","true");
          span.setAttribute("role","presentation");
        }
        state.i++;
      }
      state.callback();
    }

  };

  HUB.Startup.signal.Post("AssistiveMML Ready");

})(MathJax.Ajax,MathJax.Callback,MathJax.Hub,MathJax.HTML);

//
//  Make sure the toMathML extension is loaded before we signal
//  the load complete for this extension.  Then wait for the end
//  of the user configuration before configuring this extension.
//
MathJax.Callback.Queue(
  ["Require",MathJax.Ajax,"[MathJax]/extensions/toMathML.js"],
  ["loadComplete",MathJax.Ajax,"[MathJax]/extensions/AssistiveMML.js"],
  function () {
    MathJax.Hub.Register.StartupHook("End Config",["Config",MathJax.Extension.AssistiveMML]);
  }
);

<commit_msg>Catch IE8 errors with inserting MathML from AssistiveMML extension.  Since there is no reader tht can handle this with IE8 anyway, there isn't much point in trying to figure out whatever bug is the source of this error.  Resolves issue #1477.<commit_after>/* -*- Mode: Javascript; indent-tabs-mode:nil; js-indent-level: 2 -*- */
/* vim: set ts=2 et sw=2 tw=80: */

/*************************************************************
 *
 *  MathJax/extensions/AssistiveMML.js
 *
 *  Implements an extension that inserts hidden MathML into the
 *  page for screen readers or other asistive technology.
 *
 *  ---------------------------------------------------------------------
 *
 *  Copyright (c) 2015 The MathJax Consortium
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

(function (AJAX,CALLBACK,HUB,HTML) {
  var SETTINGS = HUB.config.menuSettings;

  var AssistiveMML = MathJax.Extension["AssistiveMML"] = {
    version: "2.6.1",

    config: HUB.CombineConfig("AssistiveMML",{
      disabled: false,
      styles: {
        ".MJX_Assistive_MathML": {
          position:"absolute!important",
          top: 0, left: 0,
          clip: (HUB.Browser.isMSIE && (document.documentMode||0) < 8 ?
                 "rect(1px 1px 1px 1px)" : "rect(1px, 1px, 1px, 1px)"),
          padding: "1px 0 0 0!important",
          border: "0!important",
          height: "1px!important",
          width: "1px!important",
          overflow: "hidden!important",
          display:"block!important",
          //
          //  Don't allow the assistive MathML become part of the selection
          //
          "-webkit-touch-callout": "none",
          "-webkit-user-select": "none",
          "-khtml-user-select": "none",
          "-moz-user-select": "none",
          "-ms-user-select": "none",
          "user-select": "none"
        },
        ".MJX_Assistive_MathML.MJX_Assistive_MathML_Block": {
          width: "100%!important"
        }
      }
    }),

    Config: function () {
      if (!this.config.disabled && SETTINGS.assistiveMML == null)
        HUB.Config({menuSettings:{assistiveMML:true}});
      AJAX.Styles(this.config.styles);
      HUB.Register.MessageHook("End Math",function (msg) {
        if (SETTINGS.assistiveMML) return AssistiveMML.AddAssistiveMathML(msg[1])
      });
    },

    //
    //  This sets up a state object that lists the jax and index into the jax,
    //    and a dummy callback that is used to synchronizing with MathJax.
    //    It will be called when the jax are all processed, and that will
    //    let the MathJax queue continue (it will block until then).
    //
    AddAssistiveMathML: function (node) {
      var state = {
        jax: HUB.getAllJax(node), i: 0,
        callback: MathJax.Callback({})
      };
      this.HandleMML(state);
      return state.callback;
    },

    //
    //  This removes the data-mathml attribute and the assistive MathML from
    //  all the jax.
    //
    RemoveAssistiveMathML: function (node) {
      var jax = HUB.getAllJax(node), frame;
      for (var i = 0, m = jax.length; i < m; i++) {
        frame = document.getElementById(jax[i].inputID+"-Frame");
        if (frame && frame.getAttribute("data-mathml")) {
          frame.removeAttribute("data-mathml");
          if (frame.lastChild && frame.lastChild.className.match(/MJX_Assistive_MathML/))
            frame.removeChild(frame.lastChild);
        }
      }
    },

    //
    //  For each jax in the state, look up the frame.
    //  If the jax doesn't use NativeMML and hasn't already been handled:
    //    Get the MathML for the jax, taking resets into account.
    //    Add a data-mathml attribute to the frame, and
    //    Create a span that is not visible on screen and put the MathML in it,
    //      and add it to the frame.
    //  When all the jax are processed, call the callback.
    //
    HandleMML: function (state) {
      var m = state.jax.length, jax, mml, frame, span;
      while (state.i < m) {
        jax = state.jax[state.i];
        frame = document.getElementById(jax.inputID+"-Frame");
        if (jax.outputJax !== "NativeMML" && frame && !frame.getAttribute("data-mathml")) {
          try {
            mml = jax.root.toMathML("").replace(/\n */g,"").replace(/<!--.*?-->/g,"");
          } catch (err) {
            if (!err.restart) throw err; // an actual error
            return MathJax.Callback.After(["HandleMML",this,state],err.restart);
          }
          frame.setAttribute("data-mathml",mml);
          span = HTML.addElement(frame,"span",{
            isMathJax: true, unselectable: "on",
            className: "MJX_Assistive_MathML"
              + (jax.root.Get("display") === "block" ? " MJX_Assistive_MathML_Block" : "")
          });
          try {span.innerHTML = mml} catch (err) {}
          frame.style.position = "relative";
          frame.setAttribute("role","presentation");
          frame.firstChild.setAttribute("aria-hidden","true");
          span.setAttribute("role","presentation");
        }
        state.i++;
      }
      state.callback();
    }

  };

  HUB.Startup.signal.Post("AssistiveMML Ready");

})(MathJax.Ajax,MathJax.Callback,MathJax.Hub,MathJax.HTML);

//
//  Make sure the toMathML extension is loaded before we signal
//  the load complete for this extension.  Then wait for the end
//  of the user configuration before configuring this extension.
//
MathJax.Callback.Queue(
  ["Require",MathJax.Ajax,"[MathJax]/extensions/toMathML.js"],
  ["loadComplete",MathJax.Ajax,"[MathJax]/extensions/AssistiveMML.js"],
  function () {
    MathJax.Hub.Register.StartupHook("End Config",["Config",MathJax.Extension.AssistiveMML]);
  }
);

<|endoftext|>


<commit_before>// var  mn = $("navigation");
//     mns = "navigation--scrolled";
    // hdr = $('.header').height();

// $(function(){
//     $(window).scroll(function() {
//         if ($(this).scrollTop() >= 101) {
//             $('.nav-bar').addClass('is-sticky');
//         }
//         else {
//             $('.nav-bar').removeClass('is-sticky');
//         }
//     });
// });

/*
 * Document ready start
 */

$(document).ready(function() {

  /*
   * Center active menu item
   */

  var activeMenu = function() {

    if ( $('.main-nav__item.active').length ) {

      var navi = $('.main-nav');
      var target = $('.main-nav__item.active');

      var w = navi.width();
      // console.log("navi.width = " + w );

      var x = navi[0].scrollWidth;
      // console.log("navi.scrollwidth = " + x );

      var y = target.outerWidth(true);
      // console.log("target.outerWidth = " + y );

      var p = target.position().left;
      // console.log("target.position = " + p );

      var t = (((w / 2) - (y / 2)) - p);
      // console.log("move by = " + t );

      navi.scrollLeft( -t );

    }

  };

  activeMenu();

  $('.main-nav__link').click(function(){
    $('.main-nav__item.active').removeClass("active");
    $(this).addClass("active");
  });

  // the animation slide-duration corresponds with values in the css
  // that set the fading bg-color
  var accordionSlide = function() {

    // hide all answers, except the first one
    $('.faq-answer').not(':eq(0)').hide();

    $('.faq-question').click(function(){

      var currentQ = $(this);
      var currentA = $(this).next('.faq-answer');
      var lastQ    = $('.faq-question.is-open');
      var lastA    = lastQ.next('.faq-answer');

      // only slide if the click is on a different question than last time
      if ( this !== lastQ.get(0) ) {
        lastQ.addClass('is-closing').removeClass('is-open');
        currentQ.addClass('is-open');

        lastA.removeClass('is-visible').addClass('is-closing').slideUp({
            duration: 400,
            queue: false,
            complete: function () {
              // change class when slide finishes
              // remove classes, used for css-keyframe-animations
              $(this).removeClass('is-closing');
              lastQ.removeClass('is-closing');
            }
          });
        currentA.addClass('is-visible').slideDown(400);
      }
    });

  }

  accordionSlide();

  /*
   * Trigger "plus de details / moins de details" in .category-content on list pages
   */
  $(function(){
    $(".js-show-info").click(function(e) {
        $(this).closest(".js-info-toggler").nextAll(".js-info-to-toggle:first").toggle().addClass('is-shown');
        $(this).next(".js-hide-info").css('display','inline-block');
        $(this).css('display','none !important');
        e.preventDefault();
    });
    $(".js-hide-info").click(function(e) {
        $(this).closest(".js-info-toggler").nextAll(".js-info-to-toggle:first").hide().toggleClass('is-shown');
        $(".js-show-info").css('display','inline-block');
        $(".js-hide-info").css('display','none !important');
        e.preventDefault();
    });
});

  jQuery(".row-with-title figcaption").fitText(0.4, { minFontSize: '24px', maxFontSize: '42px' });

  var $timeline_block = $('.timeline-row, .block--cta');

  //hide timeline blocks which are outside the viewport
  $timeline_block.each(function(){
    if($(this).offset().top > $(window).scrollTop()+$(window).height()*0.90) {
      $(this).find('> p, .timeline-row__info, .headline--cta').addClass('is-hidden');
    }
  });

  //on scolling, show/animate timeline blocks when enter the viewport
  $(window).on('scroll', function(){
    $timeline_block.each(function(){
      if( $(this).offset().top <= $(window).scrollTop()+$(window).height()*0.90 && $(this).find('> p, .headline--cta').hasClass('is-hidden') ) {
        $(this).find('> p, .timeline-row__info, .timeline-row__info, .headline--cta').removeClass('is-hidden').addClass('bounce-in');
      }
    });
  });

  /*
   * Button scroll to top
   */
  var id = 'js-scrolltop';
  $('.' + id).click(function(){
      $('html, body').animate({ scrollTop: 0 }, 150);
  });
  window.onscroll = function () {
      if(window.pageYOffset < 1000) {
          $('.' + id).fadeOut(300);
      }
      else {
          $('.' + id).fadeIn(300);
      }
  }

	/*
   * Button scroll menu
   */
  $('.js-scrollmenu').click(function(){
      if ( $( this ).hasClass( "active" ) ) {
      	$('.main-nav').animate({ scrollLeft: "-=350" }, 350);
      	$( this ).toggleClass("active");
			} else {
      	$('.main-nav').animate({ scrollLeft: "+=350" }, 350);
      	$( this ).toggleClass("active");
      }
  });

  /*
   * Button scroll block
   */

  if ( $('.js-scrollblock').length ) {
    var firstItem = $('a.card:first-of-type');
    var fip = firstItem.position().left;
    var fipi = fip; // initial offset for first card
  }

  $('.js-scrollblock').click(function(){
    var wrapper = $('.block__scroll-wrapper');
    var ww = wrapper[0].scrollWidth;
    var sw = $( window ).width();
    var $this = $(this);

    if ( $this.hasClass( "reverse" ) ) {

      wrapper.animate({ scrollLeft: "-=" + (sw - (fipi / 2) ) },
        { complete: function(){
            var fip = firstItem.position().left;
            // console.log("new fip = " + ( -fip - fipi ));

            if ( ( fip - fipi ) == 0 ) {
              $this.toggleClass("reverse");
            }
          }
        },
      500);

    } else {

      wrapper.animate({ scrollLeft: "+=" + (sw - (fipi / 2) ) },
        { complete: function(){
            var fip = firstItem.position().left;
            // console.log("new fip = " + ( -fip + fipi ));
            // console.log("result = " + (( -fip + fipi) >= ( ww - sw )) );

            if ( ( -fip + fipi) == ( ww - sw )  ) {
              $this.toggleClass("reverse");
            }
          }
        },
      500);
    }

    // console.log( "firstitem position - firstitem initial position = " + (fip - fipi) );
    // console.log( "wrapperwidth - screenwidth = " + (ww - sw) );
    // document.activeElement = null;
  });

});<commit_msg>fsgdf<commit_after>// var  mn = $("navigation");
//     mns = "navigation--scrolled";
    // hdr = $('.header').height();

// $(function(){
//     $(window).scroll(function() {
//         if ($(this).scrollTop() >= 101) {
//             $('.nav-bar').addClass('is-sticky');
//         }
//         else {
//             $('.nav-bar').removeClass('is-sticky');
//         }
//     });
// });

/*
 * Document ready start
 */

$(document).ready(function() {

  /*
   * Center active menu item
   */

  var activeMenu = function() {

    if ( $('.main-nav__item.active').length ) {

      var navi = $('.main-nav');
      var target = $('.main-nav__item.active');

      var w = navi.width();
      // console.log("navi.width = " + w );

      var x = navi[0].scrollWidth;
      // console.log("navi.scrollwidth = " + x );

      var y = target.outerWidth(true);
      // console.log("target.outerWidth = " + y );

      var p = target.position().left;
      // console.log("target.position = " + p );

      var t = (((w / 2) - (y / 2)) - p);
      // console.log("move by = " + t );

      navi.scrollLeft( -t );

    }

  };

  activeMenu();

  $('.main-nav__link').click(function(){
    $('.main-nav__item.active').removeClass("active");
    $(this).addClass("active");
  });

  // the animation slide-duration corresponds with values in the css
  // that set the fading bg-color
  var accordionSlide = function() {

    // hide all answers, except the first one
    $('.faq-answer').not(':eq(0)').hide();

    $('.faq-question').click(function(){

      var currentQ = $(this);
      var currentA = $(this).next('.faq-answer');
      var lastQ    = $('.faq-question.is-open');
      var lastA    = lastQ.next('.faq-answer');

      // only slide if the click is on a different question than last time
      if ( this !== lastQ.get(0) ) {
        lastQ.addClass('is-closing').removeClass('is-open');
        currentQ.addClass('is-open');

        lastA.removeClass('is-visible').addClass('is-closing').slideUp({
            duration: 400,
            queue: false,
            complete: function () {
              // change class when slide finishes
              // remove classes, used for css-keyframe-animations
              $(this).removeClass('is-closing');
              lastQ.removeClass('is-closing');
            }
          });
        currentA.addClass('is-visible').slideDown(400);
      }
    });

  }

  accordionSlide();

  /*
   * Trigger "plus de details / moins de details" in .category-content on list pages
   */
  $(function(){
    $(".js-show-info").click(function(e) {
        $(this).closest(".js-info-toggler").nextAll(".js-info-to-toggle:first").toggle().addClass('is-shown');
        $(this).siblings(".js-hide-info").css('display','inline-block');
        $(this).css('display','none !important');
        e.preventDefault();
    });
    $(".js-hide-info").click(function(e) {
        $(this).closest(".js-info-toggler").nextAll(".js-info-to-toggle:first").hide().toggleClass('is-shown');
        $(this).siblings(".js-show-info").css('display','inline-block');
        $(this).css('display','none !important');
        e.preventDefault();
    });
});

  jQuery(".row-with-title figcaption").fitText(0.4, { minFontSize: '24px', maxFontSize: '42px' });

  var $timeline_block = $('.timeline-row, .block--cta');

  //hide timeline blocks which are outside the viewport
  $timeline_block.each(function(){
    if($(this).offset().top > $(window).scrollTop()+$(window).height()*0.90) {
      $(this).find('> p, .timeline-row__info, .headline--cta').addClass('is-hidden');
    }
  });

  //on scolling, show/animate timeline blocks when enter the viewport
  $(window).on('scroll', function(){
    $timeline_block.each(function(){
      if( $(this).offset().top <= $(window).scrollTop()+$(window).height()*0.90 && $(this).find('> p, .headline--cta').hasClass('is-hidden') ) {
        $(this).find('> p, .timeline-row__info, .timeline-row__info, .headline--cta').removeClass('is-hidden').addClass('bounce-in');
      }
    });
  });

  /*
   * Button scroll to top
   */
  var id = 'js-scrolltop';
  $('.' + id).click(function(){
      $('html, body').animate({ scrollTop: 0 }, 150);
  });
  window.onscroll = function () {
      if(window.pageYOffset < 1000) {
          $('.' + id).fadeOut(300);
      }
      else {
          $('.' + id).fadeIn(300);
      }
  }

	/*
   * Button scroll menu
   */
  $('.js-scrollmenu').click(function(){
      if ( $( this ).hasClass( "active" ) ) {
      	$('.main-nav').animate({ scrollLeft: "-=350" }, 350);
      	$( this ).toggleClass("active");
			} else {
      	$('.main-nav').animate({ scrollLeft: "+=350" }, 350);
      	$( this ).toggleClass("active");
      }
  });

  /*
   * Button scroll block
   */

  if ( $('.js-scrollblock').length ) {
    var firstItem = $('a.card:first-of-type');
    var fip = firstItem.position().left;
    var fipi = fip; // initial offset for first card
  }

  $('.js-scrollblock').click(function(){
    var wrapper = $('.block__scroll-wrapper');
    var ww = wrapper[0].scrollWidth;
    var sw = $( window ).width();
    var $this = $(this);

    if ( $this.hasClass( "reverse" ) ) {

      wrapper.animate({ scrollLeft: "-=" + (sw - (fipi / 2) ) },
        { complete: function(){
            var fip = firstItem.position().left;
            // console.log("new fip = " + ( -fip - fipi ));

            if ( ( fip - fipi ) == 0 ) {
              $this.toggleClass("reverse");
            }
          }
        },
      500);

    } else {

      wrapper.animate({ scrollLeft: "+=" + (sw - (fipi / 2) ) },
        { complete: function(){
            var fip = firstItem.position().left;
            // console.log("new fip = " + ( -fip + fipi ));
            // console.log("result = " + (( -fip + fipi) >= ( ww - sw )) );

            if ( ( -fip + fipi) == ( ww - sw )  ) {
              $this.toggleClass("reverse");
            }
          }
        },
      500);
    }

    // console.log( "firstitem position - firstitem initial position = " + (fip - fipi) );
    // console.log( "wrapperwidth - screenwidth = " + (ww - sw) );
    // document.activeElement = null;
  });

});<|endoftext|>


<commit_before>/**
 * jQuery Formset 1.2
 * @author Stanislaus Madueke (stan DOT madueke AT gmail DOT com)
 * @requires jQuery 1.2.6 or later
 *
 * Copyright (c) 2009, Stanislaus Madueke
 * All rights reserved.
 *
 * Licensed under the New BSD License
 * See: http://www.opensource.org/licenses/bsd-license.php
 */
;(function($) {
    $.fn.formset = function(opts)
    {
        var options = $.extend({}, $.fn.formset.defaults, opts),
            flatExtraClasses = options.extraClasses.join(' '),
            $$ = $(this),

            applyExtraClasses = function(row, ndx) {
                if (options.extraClasses) {
                    row.removeClass(flatExtraClasses);
                    row.addClass(options.extraClasses[ndx % options.extraClasses.length]);
                }
            },

            updateElementIndex = function(elem, prefix, ndx) {
                var idRegex = new RegExp('(' + prefix + '-\\d+-)|(^)'),
                    replacement = prefix + '-' + ndx + '-';
                if (elem.attr("for")) elem.attr("for", elem.attr("for").replace(idRegex, replacement));
                if (elem.attr('id')) elem.attr('id', elem.attr('id').replace(idRegex, replacement));
                if (elem.attr('name')) elem.attr('name', elem.attr('name').replace(idRegex, replacement));
            },

            hasChildElements = function(row) {
                return row.find('input,select,textarea,label').length > 0;
            },

            insertDeleteLink = function(row) {
                if (row.is('TR')) {
                    // If the forms are laid out in table rows, insert
                    // the remove button into the last table cell:
                    row.children(':last').append('' + options.deleteText + '');
                } else if (row.is('UL') || row.is('OL')) {
                    // If they're laid out as an ordered/unordered list,
                    // insert an
 after the last list item:
                    row.append('
' + options.deleteText +'
');
                } else {
                    // Otherwise, just insert the remove button as the
                    // last child element of the form's container:
                    row.append('' + options.deleteText +'');
                }
                row.find('a.' + options.deleteCssClass).click(function() {
                    var row = $(this).parents('.' + options.formCssClass),
                        del = row.find('input:hidden[id $= "-DELETE"]');
                    if (del.length) {
                        // We're dealing with an inline formset; rather than remove
                        // this form from the DOM, we'll mark it as deleted and hide
                        // it, then let Django handle the deleting:
                        del.val('on');
                        row.hide();
                    } else {
                        row.remove();
                        // Update the TOTAL_FORMS form count.
                        // Also update names and IDs for all remaining form controls so they remain in sequence:
                        var forms = $('.' + options.formCssClass).not('.formset-custom-template');
                        $('#id_' + options.prefix + '-TOTAL_FORMS').val(forms.length);
                        for (var i=0, formCount=forms.length; i<formCount; i++) {
                            applyExtraClasses(forms.eq(i), i);
                            forms.eq(i).find('input,select,textarea,label').each(function() {
                                updateElementIndex($(this), options.prefix, i);
                            });
                        }
                    }
                    // If a post-delete callback was provided, call it with the deleted form:
                    if (options.removed) options.removed(row);
                    return false;
                });
            };

        $$.each(function(i) {
            var row = $(this),
                del = row.find('input:checkbox[id $= "-DELETE"]');
            if (del.length) {
                // If you specify "can_delete = True" when creating an inline formset,
                // Django adds a checkbox to each form in the formset.
                // Replace the default checkbox with a hidden field:
                del.before('<input type="hidden" name="' + del.attr('name') +'" id="' + del.attr('id') +'" />');
                del.remove();
            }
            if (hasChildElements(row)) {
                insertDeleteLink(row);
                row.addClass(options.formCssClass);
                applyExtraClasses(row, i);
            }
        });

        if ($$.length) {
            var addButton, template;
            if (options.formTemplate) {
                // If a form template was specified, we'll clone it to generate new form instances:
                template = (options.formTemplate instanceof $) ? options.formTemplate : $(options.formTemplate);
                template.removeAttr('id').addClass(options.formCssClass).addClass('formset-custom-template');
                template.find('input,select,textarea,label').each(function() {
                    updateElementIndex($(this), options.prefix, 2012);
                });
                insertDeleteLink(template);
            } else {
                // Otherwise, use the last form in the formset; this works much better if you've got
                // extra (>= 1) forms (thnaks to justhamade for pointing this out):
                template = $('.' + options.formCssClass + ':last').clone(true).removeAttr('id');
                template.find('input:hidden[id $= "-DELETE"]').remove();
                template.find('input,select,textarea,label').each(function() {
                    var elem = $(this);
                    // If this is a checkbox or radiobutton, uncheck it.
                    // This fixes Issue 1, reported by Wilson.Andrew.J:
                    if (elem.is('input:checkbox') || elem.is('input:radio')) {
                        elem.attr('checked', false);
                    } else {
                        elem.val('');
                    }
                });
            }
            // FIXME: Perhaps using $.data would be a better idea?
            options.formTemplate = template;

            if ($$.attr('tagName') == 'TR') {
                // If forms are laid out as table rows, insert the
                // "add" button in a new table row:
                var numCols = $$.eq(0).children().length;
                $$.parent().append('
' + options.addText + '
');
                addButton = $$.parent().find('tr:last a');
                addButton.parents('tr').addClass(options.formCssClass + '-add');
            } else {
                // Otherwise, insert it immediately after the last form:
                $$.filter(':last').after('' + options.addText + '');
                addButton = $$.filter(':last').next();
            }
            addButton.click(function() {
                var formCount = parseInt($('#id_' + options.prefix + '-TOTAL_FORMS').val()),
                    row = options.formTemplate.clone(true).removeClass('formset-custom-template'),
                    buttonRow = $(this).parents('tr.' + options.formCssClass + '-add').get(0) || this;
                applyExtraClasses(row, formCount);
                row.insertBefore($(buttonRow)).show();
                row.find('input,select,textarea,label').each(function() {
                    updateElementIndex($(this), options.prefix, formCount);
                });
                $('#id_' + options.prefix + '-TOTAL_FORMS').val(formCount + 1);
                // If a post-add callback was supplied, call it with the added form:
                if (options.added) options.added(row);
                return false;
            });
        }

        return $$;
    }

    /* Setup plugin defaults */
    $.fn.formset.defaults = {
        prefix: 'form',                  // The form prefix for your django formset
        formTemplate: null,              // The jQuery selection cloned to generate new form instances
        addText: '
add another',          // Text for the add link
        deleteText: 'remove',            // Text for the delete link
        addCssClass: 'add-row',          // CSS class applied to the add link
        deleteCssClass: 'delete-row',    // CSS class applied to the delete link
        formCssClass: 'dynamic-form',    // CSS class applied to each form in a formset
        extraClasses: [],                // Additional CSS classes, which will be applied to each form in turn
        added: null,                     // Function called each time a new form is added
        removed: null                    // Function called each time a form is deleted
    };
})(jQuery)
<commit_msg>fixes lousy javascript<commit_after>/**
 * jQuery Formset 1.2
 * @author Stanislaus Madueke (stan DOT madueke AT gmail DOT com)
 * @requires jQuery 1.2.6 or later
 *
 * Copyright (c) 2009, Stanislaus Madueke
 * All rights reserved.
 *
 * Licensed under the New BSD License
 * See: http://www.opensource.org/licenses/bsd-license.php
 */
(function($) {
    $.fn.formset = function(opts)
    {
        var options = $.extend({}, $.fn.formset.defaults, opts),
            flatExtraClasses = options.extraClasses.join(' '),
            $$ = $(this),

            applyExtraClasses = function(row, ndx) {
                if (options.extraClasses) {
                    row.removeClass(flatExtraClasses);
                    row.addClass(options.extraClasses[ndx % options.extraClasses.length]);
                }
            },

            updateElementIndex = function(elem, prefix, ndx) {
                var idRegex = new RegExp('(' + prefix + '-\\d+-)|(^)'),
                    replacement = prefix + '-' + ndx + '-';
                if (elem.attr("for")) elem.attr("for", elem.attr("for").replace(idRegex, replacement));
                if (elem.attr('id')) elem.attr('id', elem.attr('id').replace(idRegex, replacement));
                if (elem.attr('name')) elem.attr('name', elem.attr('name').replace(idRegex, replacement));
            },

            hasChildElements = function(row) {
                return row.find('input,select,textarea,label').length > 0;
            },

            insertDeleteLink = function(row) {
                if (row.is('TR')) {
                    // If the forms are laid out in table rows, insert
                    // the remove button into the last table cell:
                    row.children(':last').append('' + options.deleteText + '');
                } else if (row.is('UL') || row.is('OL')) {
                    // If they're laid out as an ordered/unordered list,
                    // insert an
 after the last list item:
                    row.append('
' + options.deleteText +'
');
                } else {
                    // Otherwise, just insert the remove button as the
                    // last child element of the form's container:
                    row.append('' + options.deleteText +'');
                }
                row.find('a.' + options.deleteCssClass).click(function() {
                    var row = $(this).parents('.' + options.formCssClass),
                        del = row.find('input:hidden[id $= "-DELETE"]');
                    if (del.length) {
                        // We're dealing with an inline formset; rather than remove
                        // this form from the DOM, we'll mark it as deleted and hide
                        // it, then let Django handle the deleting:
                        del.val('on');
                        row.hide();
                    } else {
                        row.remove();
                        // Update the TOTAL_FORMS form count.
                        // Also update names and IDs for all remaining form controls so they remain in sequence:
                        var forms = $('.' + options.formCssClass).not('.formset-custom-template');
                        $('#id_' + options.prefix + '-TOTAL_FORMS').val(forms.length);
                        for (var i=0, formCount=forms.length; i<formCount; i++) {
                            applyExtraClasses(forms.eq(i), i);
                            forms.eq(i).find('input,select,textarea,label').each(function() {
                                updateElementIndex($(this), options.prefix, i);
                            });
                        }
                    }
                    // If a post-delete callback was provided, call it with the deleted form:
                    if (options.removed) options.removed(row);
                    return false;
                });
            };

        $$.each(function(i) {
            var row = $(this),
                del = row.find('input:checkbox[id $= "-DELETE"]');
            if (del.length) {
                // If you specify "can_delete = True" when creating an inline formset,
                // Django adds a checkbox to each form in the formset.
                // Replace the default checkbox with a hidden field:
                del.before('<input type="hidden" name="' + del.attr('name') +'" id="' + del.attr('id') +'" />');
                del.remove();
            }
            if (hasChildElements(row)) {
                insertDeleteLink(row);
                row.addClass(options.formCssClass);
                applyExtraClasses(row, i);
            }
        });

        if ($$.length) {
            var addButton, template;
            if (options.formTemplate) {
                // If a form template was specified, we'll clone it to generate new form instances:
                template = (options.formTemplate instanceof $) ? options.formTemplate : $(options.formTemplate);
                template.removeAttr('id').addClass(options.formCssClass).addClass('formset-custom-template');
                template.find('input,select,textarea,label').each(function() {
                    updateElementIndex($(this), options.prefix, 2012);
                });
                insertDeleteLink(template);
            } else {
                // Otherwise, use the last form in the formset; this works much better if you've got
                // extra (>= 1) forms (thnaks to justhamade for pointing this out):
                template = $('.' + options.formCssClass + ':last').clone(true).removeAttr('id');
                template.find('input:hidden[id $= "-DELETE"]').remove();
                template.find('input,select,textarea,label').each(function() {
                    var elem = $(this);
                    // If this is a checkbox or radiobutton, uncheck it.
                    // This fixes Issue 1, reported by Wilson.Andrew.J:
                    if (elem.is('input:checkbox') || elem.is('input:radio')) {
                        elem.attr('checked', false);
                    } else {
                        elem.val('');
                    }
                });
            }
            // FIXME: Perhaps using $.data would be a better idea?
            options.formTemplate = template;

            if ($$.attr('tagName') == 'TR') {
                // If forms are laid out as table rows, insert the
                // "add" button in a new table row:
                var numCols = $$.eq(0).children().length;
                $$.parent().append('
' + options.addText + '
');
                addButton = $$.parent().find('tr:last a');
                addButton.parents('tr').addClass(options.formCssClass + '-add');
            } else {
                // Otherwise, insert it immediately after the last form:
                $$.filter(':last').after('' + options.addText + '');
                addButton = $$.filter(':last').next();
            }
            addButton.click(function() {
                var formCount = parseInt($('#id_' + options.prefix + '-TOTAL_FORMS').val()),
                    row = options.formTemplate.clone(true).removeClass('formset-custom-template'),
                    buttonRow = $(this).parents('tr.' + options.formCssClass + '-add').get(0) || this;
                applyExtraClasses(row, formCount);
                row.insertBefore($(buttonRow)).show();
                row.find('input,select,textarea,label').each(function() {
                    updateElementIndex($(this), options.prefix, formCount);
                });
                $('#id_' + options.prefix + '-TOTAL_FORMS').val(formCount + 1);
                // If a post-add callback was supplied, call it with the added form:
                if (options.added) options.added(row);
                return false;
            });
        }

        return $$;
    }

    /* Setup plugin defaults */
    $.fn.formset.defaults = {
        prefix: 'form',                  // The form prefix for your django formset
        formTemplate: null,              // The jQuery selection cloned to generate new form instances
        addText: '
add another',          // Text for the add link
        deleteText: 'remove',            // Text for the delete link
        addCssClass: 'add-row',          // CSS class applied to the add link
        deleteCssClass: 'delete-row',    // CSS class applied to the delete link
        formCssClass: 'dynamic-form',    // CSS class applied to each form in a formset
        extraClasses: [],                // Additional CSS classes, which will be applied to each form in turn
        added: null,                     // Function called each time a new form is added
        removed: null                    // Function called each time a form is deleted
    };
})(jQuery);
<|endoftext|>


<commit_before>import { fetchBuilder, extractData, buildWith } from './model_builder';
import {
  processSectionDates,
  processSectionMeetings,
} from './schedule/common';

function postProcess(response, urlExtra) {
  let data = extractData(response);

  if (Array.isArray(data) && data.length === 0) {
    data = null;
  }

  // Flatten section_data
  data?.terms?.forEach((term) => {
    term.courses?.forEach((course) => {
      for (let i = 0; i < course.sections.length; i++) {
        course.sections[i] = course.sections[i].section_data;
        const section = course.sections[i]
        section.anchor = (section.curriculum_abbr.replace(/ /g, '-') +
          '-' + section.course_number +
          '-' + section.section_id);
        section.id = `${term.year}-${term.quarter}-${section.anchor}`;
        processSectionDates(section);
        processSectionMeetings(section);
      }
    });
  });

  return {[urlExtra]: data};
}

const customActions = {
  fetch: function (vuexObject, {year, quarter}) {
    return fetchBuilder(
      '/api/v1/myplan/', postProcess, 'json'
    )(vuexObject, `${year}/${quarter}`);
  },
};

export default buildWith(
  { customActions },
);<commit_msg>mypolan data doesn't need to processSectionDates<commit_after>import { fetchBuilder, extractData, buildWith } from './model_builder';
import {
  processSectionDates,
  processSectionMeetings,
} from './schedule/common';

function postProcess(response, urlExtra) {
  let data = extractData(response);

  if (Array.isArray(data) && data.length === 0) {
    data = null;
  }

  // Flatten section_data
  data?.terms?.forEach((term) => {
    term.courses?.forEach((course) => {
      for (let i = 0; i < course.sections.length; i++) {
        course.sections[i] = course.sections[i].section_data;
        const section = course.sections[i]
        section.anchor = (section.curriculum_abbr.replace(/ /g, '-') +
          '-' + section.course_number +
          '-' + section.section_id);
        section.id = `${term.year}-${term.quarter}-${section.anchor}`;
        processSectionMeetings(section);
      }
    });
  });

  return {[urlExtra]: data};
}

const customActions = {
  fetch: function (vuexObject, {year, quarter}) {
    return fetchBuilder(
      '/api/v1/myplan/', postProcess, 'json'
    )(vuexObject, `${year}/${quarter}`);
  },
};

export default buildWith(
  { customActions },
);<|endoftext|>


<commit_before>var FinalExams = {
    FRIDAY: 5,
    SATURDAY: 6,
    SUNDAY: 0,
    show_finals: function(course_index) {
        showLoading();
        WSData.fetch_course_data(FinalExams.render_exams, [course_index]);
    },

    render_exams: function(course_index) {
        var course_data = WSData.course_data();
        var index = 0;
        var tbd_or_nonexistent = [];
        var scheduled_finals = [];

        var last_day_of_finals = new Date(course_data.term.last_final_exam_date);
        var max_date = last_day_of_finals;
        var min_date;

        // If there's something unexpected, show a list, not the visual schedule
        var show_list_instead_of_visual = false;

        $("#page-header").page_header({
            year: course_data.year,
            quarter: course_data.quarter,
            view_name: 'Final Exams'
        });

        for (index = 0; index < course_data.sections.length; index++) {
            var section = course_data.sections[index];
            // We need to set this here, since the code that displays links doesn't have access
            // to the full list of sections, necessarily
            section.index = index;
            if (section.final_exam) {
                var final_exam = section.final_exam;
                var start_date = new Date(final_exam.start_date);

                if (final_exam.start_date) {
                    if (max_date == null || max_date < start_date) {
                        max_date = start_date;
                    }
                    if (min_date == null || min_date > start_date) {
                        min_date = start_date;
                    }
                    if (start_date > last_day_of_finals) {
                        show_list_instead_of_visual = true;
                    }
                    scheduled_finals.push(section);
                }
                else {
                    tbd_or_nonexistent.push(section);
                }
            }
            else {
                tbd_or_nonexistent.push(section);
            }
        }

        // This shouldn't happen, but if we have over a week span of finals, just list them out.
        if (scheduled_finals.length) {
            if ((max_date - min_date) > (1000 * 60 * 60 * 24 * 7)) {
                show_list_instead_of_visual = true;
            }
        }

        // It's our current understanding that the last day of finals is always a friday -
        // grades are due on the following tuesday.  If the last day of finals isn't a friday,
        // fall back to the list view.
        if (last_day_of_finals.getDay() != FinalExams.FRIDAY) {
            show_list_instead_of_visual = true;
        }

        var over_one_week = [];
        var visual_data = {};

        if (show_list_instead_of_visual) {
            over_one_week = scheduled_finals;
        }
        else if (course_data.quarter != "summer") {
            // summer quarter doesn't have properly scheduled finals
            visual_data = FinalExams._build_visual_schedule_data(scheduled_finals, course_data.term);
        }

        var template_data = {
            tbd: tbd_or_nonexistent,
            over_one_week: over_one_week,
            is_summer: (course_data.quarter == "summer"),
            visual_data: visual_data,
        };

        var source = $("#final_exams").html();
        var template = Handlebars.compile(source);
        $("#courselist").html(template(template_data));

        if (!course_index) {
            Modal.hide();
        }
        else {
            if (course_index < course_data.sections.length) {
                FinalsModal.show_finals_modal(course_index);
            }
        }

        $(".show_section_details").bind("click", function(ev) {
            var course_id = this.rel;
            var log_course_id = ev.currentTarget.getAttribute("class").replace(/[^a-z0-9]/gi, '_');

            WSData.log_interaction("open_finals_modal_"+log_course_id);
            var hist = window.History;
            hist.pushState({
                state: "final_exams",
                course_index: course_id
            },  "", "/mobile/final_exams/"+course_id);

            CourseModal.show_course_modal(course_id);

            return false;
        });

    },

    _build_visual_schedule_data: function(sections_with_finals, term) {
        var visual_data = {
            latest_ending: 0,
            earliest_start: 24*60,
            monday: [],
            tuesday: [],
            wednesday: [],
            thursday: [],
            friday: [],
            saturday: [],
            sunday: [],
            display_hours: [],
            has_7_days: false,
            courses_meeting_tbd: [],
            end_date: term.last_final_exam_date
        };

        var index = 0;
        for (index = 0; index < sections_with_finals.length; index++) {
            var section = sections_with_finals[index];
            var final_exam = section.final_exam;

            var start_date = new Date(final_exam.start_date);
            var end_date = new Date(final_exam.end_date);

            if (start_date.getDay() == FinalExams.SATURDAY) {
                visual_data.has_7_days = true;
            }
            if (start_date.getDay() == FinalExams.SUNDAY) {
                visual_data.has_7_days = true;
            }

            var start_minutes = start_date.getHours() * 60 + start_date.getMinutes();
            var end_minutes = end_date.getHours() * 60 + end_date.getMinutes();

            if (start_minutes < visual_data.earliest_start) {
                visual_data.earliest_start = start_minutes;
            }
            if (end_minutes > visual_data.latest_ending) {
                visual_data.latest_ending = end_minutes;
            }

            var exam_info = {
                start: start_minutes,
                end: end_minutes,
                color_id: section.color_id,
                curriculum: section.curriculum_abbr,
                course_number: section.course_number,
                section_id: section.section_id,
                section_index: section.index,
                is_confirmed: final_exam.is_confirmed
            };

            var days = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"];
            var day = start_date.getDay();
            visual_data[days[day]].push(exam_info);
        }

        // Make it so the start and end times are always on the hour:
        var start_hour = parseInt(visual_data.earliest_start / 60, 10);
        if ((start_hour * 60) === visual_data.earliest_start) {
            visual_data.start_time = (start_hour * 60) - 60;
        }
        else {
            visual_data.start_time = (start_hour * 60);
        }

        var end_hour = parseInt(visual_data.latest_ending / 60, 10) + 1;
        if ((end_hour * 60) === visual_data.latest_ending) {
            visual_data.end_time = (end_hour * 60) - 60;
        }
        else {
            visual_data.end_time = (end_hour * 60);
        }

        var i = 0;
        // We don't want to add the last hour - it's just off the end of the visual schedule
        for (i = visual_data.start_time; i <= visual_data.end_time - 1; i += 60) {
            visual_data.display_hours.push({
                hour: (i / 60),
                position: i
            });
        }

        visual_data.total_hours = (visual_data.end_time - visual_data.start_time) / 60;

        var hours_count = parseInt((visual_data.end_time - visual_data.start_time) / 60, 0);
        if (hours_count <= 6) {
            visual_data.schedule_hours_class = "six-hour";
        }
        else if (hours_count <= 12) {
            visual_data.schedule_hours_class = "twelve-hour";
        }
        else {
            visual_data.schedule_hours_class = "twelve-plus";
        }

        return visual_data;
    }
};

<commit_msg>Sorts the out-of-bounds / unexpected situation finals by start date/time.  muwm-591<commit_after>var FinalExams = {
    FRIDAY: 5,
    SATURDAY: 6,
    SUNDAY: 0,
    show_finals: function(course_index) {
        showLoading();
        WSData.fetch_course_data(FinalExams.render_exams, [course_index]);
    },

    sort_by_finals_date: function(a, b) {
        var a_date = new Date(a.final_exam.start_date);
        var b_date = new Date(b.final_exam.start_date);

        return a_date - b_date;
    },
    render_exams: function(course_index) {
        var course_data = WSData.course_data();
        var index = 0;
        var tbd_or_nonexistent = [];
        var scheduled_finals = [];

        var last_day_of_finals = new Date(course_data.term.last_final_exam_date);
        var max_date = last_day_of_finals;
        var min_date;

        // If there's something unexpected, show a list, not the visual schedule
        var show_list_instead_of_visual = false;

        $("#page-header").page_header({
            year: course_data.year,
            quarter: course_data.quarter,
            view_name: 'Final Exams'
        });

        for (index = 0; index < course_data.sections.length; index++) {
            var section = course_data.sections[index];
            // We need to set this here, since the code that displays links doesn't have access
            // to the full list of sections, necessarily
            section.index = index;
            if (section.final_exam) {
                var final_exam = section.final_exam;
                var start_date = new Date(final_exam.start_date);

                if (final_exam.start_date) {
                    if (max_date == null || max_date < start_date) {
                        max_date = start_date;
                    }
                    if (min_date == null || min_date > start_date) {
                        min_date = start_date;
                    }
                    if (start_date > last_day_of_finals) {
                        show_list_instead_of_visual = true;
                    }
                    scheduled_finals.push(section);
                }
                else {
                    tbd_or_nonexistent.push(section);
                }
            }
            else {
                tbd_or_nonexistent.push(section);
            }
        }

        // This shouldn't happen, but if we have over a week span of finals, just list them out.
        if (scheduled_finals.length) {
            if ((max_date - min_date) > (1000 * 60 * 60 * 24 * 7)) {
                show_list_instead_of_visual = true;
            }
        }

        // It's our current understanding that the last day of finals is always a friday -
        // grades are due on the following tuesday.  If the last day of finals isn't a friday,
        // fall back to the list view.
        if (last_day_of_finals.getDay() != FinalExams.FRIDAY) {
            show_list_instead_of_visual = true;
        }

        var over_one_week = [];
        var visual_data = {};

        if (show_list_instead_of_visual) {
            over_one_week = scheduled_finals.sort(FinalExams.sort_by_finals_date);
        }
        else if (course_data.quarter != "summer") {
            // summer quarter doesn't have properly scheduled finals
            visual_data = FinalExams._build_visual_schedule_data(scheduled_finals, course_data.term);
        }

        var template_data = {
            tbd: tbd_or_nonexistent,
            over_one_week: over_one_week,
            is_summer: (course_data.quarter == "summer"),
            visual_data: visual_data,
        };

        var source = $("#final_exams").html();
        var template = Handlebars.compile(source);
        $("#courselist").html(template(template_data));

        if (!course_index) {
            Modal.hide();
        }
        else {
            if (course_index < course_data.sections.length) {
                FinalsModal.show_finals_modal(course_index);
            }
        }

        $(".show_section_details").bind("click", function(ev) {
            var course_id = this.rel;
            var log_course_id = ev.currentTarget.getAttribute("class").replace(/[^a-z0-9]/gi, '_');

            WSData.log_interaction("open_finals_modal_"+log_course_id);
            var hist = window.History;
            hist.pushState({
                state: "final_exams",
                course_index: course_id
            },  "", "/mobile/final_exams/"+course_id);

            CourseModal.show_course_modal(course_id);

            return false;
        });

    },

    _build_visual_schedule_data: function(sections_with_finals, term) {
        var visual_data = {
            latest_ending: 0,
            earliest_start: 24*60,
            monday: [],
            tuesday: [],
            wednesday: [],
            thursday: [],
            friday: [],
            saturday: [],
            sunday: [],
            display_hours: [],
            has_7_days: false,
            courses_meeting_tbd: [],
            end_date: term.last_final_exam_date
        };

        var index = 0;
        for (index = 0; index < sections_with_finals.length; index++) {
            var section = sections_with_finals[index];
            var final_exam = section.final_exam;

            var start_date = new Date(final_exam.start_date);
            var end_date = new Date(final_exam.end_date);

            if (start_date.getDay() == FinalExams.SATURDAY) {
                visual_data.has_7_days = true;
            }
            if (start_date.getDay() == FinalExams.SUNDAY) {
                visual_data.has_7_days = true;
            }

            var start_minutes = start_date.getHours() * 60 + start_date.getMinutes();
            var end_minutes = end_date.getHours() * 60 + end_date.getMinutes();

            if (start_minutes < visual_data.earliest_start) {
                visual_data.earliest_start = start_minutes;
            }
            if (end_minutes > visual_data.latest_ending) {
                visual_data.latest_ending = end_minutes;
            }

            var exam_info = {
                start: start_minutes,
                end: end_minutes,
                color_id: section.color_id,
                curriculum: section.curriculum_abbr,
                course_number: section.course_number,
                section_id: section.section_id,
                section_index: section.index,
                is_confirmed: final_exam.is_confirmed
            };

            var days = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"];
            var day = start_date.getDay();
            visual_data[days[day]].push(exam_info);
        }

        // Make it so the start and end times are always on the hour:
        var start_hour = parseInt(visual_data.earliest_start / 60, 10);
        if ((start_hour * 60) === visual_data.earliest_start) {
            visual_data.start_time = (start_hour * 60) - 60;
        }
        else {
            visual_data.start_time = (start_hour * 60);
        }

        var end_hour = parseInt(visual_data.latest_ending / 60, 10) + 1;
        if ((end_hour * 60) === visual_data.latest_ending) {
            visual_data.end_time = (end_hour * 60) - 60;
        }
        else {
            visual_data.end_time = (end_hour * 60);
        }

        var i = 0;
        // We don't want to add the last hour - it's just off the end of the visual schedule
        for (i = visual_data.start_time; i <= visual_data.end_time - 1; i += 60) {
            visual_data.display_hours.push({
                hour: (i / 60),
                position: i
            });
        }

        visual_data.total_hours = (visual_data.end_time - visual_data.start_time) / 60;

        var hours_count = parseInt((visual_data.end_time - visual_data.start_time) / 60, 0);
        if (hours_count <= 6) {
            visual_data.schedule_hours_class = "six-hour";
        }
        else if (hours_count <= 12) {
            visual_data.schedule_hours_class = "twelve-hour";
        }
        else {
            visual_data.schedule_hours_class = "twelve-plus";
        }

        return visual_data;
    }
};

<|endoftext|>