<commit_before>export const SECTIONS = [
  {
    name: "Opinions",
    imageLink: null,
    description: `The Opinions section lets writers share their views on issues relevant within the country, Stuyvesant, and their personal lives. Writers across all grades give their opinions on subjects ranging from politics and economics to technology and culture and share their personal stories. We try to consistently create engaging, substantive, and relevant content to provide students with a closer look at the world and the Stuyvesant community.`,
    quotes: [],
    link:
      "",
  },
  {
    name: "Arts and Entertainment",
    imageLink: null,
    description: `As the only department with more than three article categories, Arts and Entertainment is notable for its versatility and creative freedom. We review recent cultural topics such as art, fashion, film, food, literature, music, and more! A&E writers are also free to experiment with the thinkpiece category, where one can explore cultural phenomena or trends, gender standards, or race from a cultural standpoint. Whether you are an aspiring music enthusiast or cultural connoisseur, everyone can find (or create!) their own niche in A&E.`,
    quotes: [],
    link:
      "",
  },
  {
    name: "Features",
    imageLink: null,
    description: `Features capture the spirit and mood of the school. At times, Features covers Stuyvesant’s evergreen topics, e.g. what is it about PE uniforms that gets everybody so hot and bothered? Other times, writers might profile a teacher. Other times, writers share creative narratives about experiences they have had in the “Voices” section. There is much to be found in Features, so come hither!`,
    quotes: [],
    link:
      "",
  },
  {
    name: "Humor",
    imageLink: null,
    description: `Do you want to lowkey rant about your life and roast everything?
        Would you like to have a personality glow up?
        Do you want to join a low commitment and fun Spec department?
        Join Spec Humor!
        We write funny and satirical pieces for The Spec and unlike other departments, everything we publish is libel and slander which means that you'll be in control of what everyone says!`,
    quotes: [],
    link: "",
  },
  {
    name: "Sports",
    imageLink: null,
    description: `The Sports department covers all things sports, whether it be PSAL sports at Stuyvesant or professional sports. Articles pertaining to Stuyvesant sports range from championship articles and season updates to athlete and coach interviews and opinions. Writers also cover virtually any topic in the larger sports world, from debates on LeBron vs. MJ and college basketball to YouTube boxing and the Champions League. If that’s not enough to convince you to apply, we have an exclusive fantasy football league (with rewards).`,
    quotes: [],
    link:
      "",
  },
  {
    name: "News",
    imageLink: null,
    description: `The News Department covers school events, administrative changes, student organizations, school and student government policies, and out-of-school news. Besides traditional News articles, we also write mini-articles, graphs & surveys, and spreads. If you are interested in knowing Stuyvesant news before it is released, informing the student body, meeting students, teachers, and administrators, or seeing your work on the front page, apply to our department!`,
    quotes: [],
    link:
      "",
  },
  {
    name: "Copy",
    imageLink: null,
    description: `Members of the Copy Department ensures that The Spectator produces clear, grammatically correct, and accurate articles. Did you notice both mistakes in the last sentence? If you did, you should join the Copy Department! Members gain early access to articles and sharpen their grammar skills while editing and fact-checking departments of their choice. We are committed to catching grammatical and factual mistakes in articles with attention to detail, and we look for members who want to contribute to maintaining The Spectator’s high-quality content.`,
    quotes: [],
    link:
      "",
  },
  {
    name: "Web",
    imageLink: null,
    description: `The Web department is responsible for designing and updating The Spectator's website, www.stuyspec.com. It also attempts to facilitate the learning of developing and maintaining a web application based on Ruby on Rails and React. These real-world skills are a great resume builder and provide invaluable experience in working together as a team. This is the perfect department for both computer programming enthusiasts and novices who want to help The Spectator continue to storm the frontier of online journalism.`,
    quotes: [],
    link:
      "",
  },
  {
    name: "Layout",
    imageLink: null,
    description: `A good publication should interest readers before they even pick it up and read it; the Layout department does just that! We are responsible for formatting all of the articles, photos, art, and other media and make sure that the paper is aesthetically appealing and easy to read. After all, no one wants to read ugly and confusing blocks of text. With creativity and a certain magic called InDesign, we present to you what you know as The Spectator. As a staff member, you will be taught how to use InDesign and asked to critique the formatting of each issue as it is released.`,
    quotes: [],
    link:
      "",
  },
  {
    name: "Photo",
    imageLink: null,
    description: `Photo is dedicated to providing interesting and dynamic photographs to support the articles in The Spectator and spearhead photo essay projects. Typical assignments include portraits of students and teachers, photos at school events, and shots of fun things around the city. This department is a good fit for anyone interested in photography, whether you’re just starting out, or you’re an expert looking for a place to practice and refine your skills.`,
    quotes: [],
    link:
      "",
  },
  {
    name: "Business",
    imageLink: null,
    description: `The Business department is the backbone of The Spectator. Due to the efforts of this department, The Spectator is able to print all of its issues while still remaining independent. We negotiate and secure deals with businesses all over New York as well as handle the advertisements on the website and in the paper. Members learn skills that can be applied to real life, including talking in formal settings, learning how to negotiate and compromise, and understanding how to manage money.`,
    quotes: [],
    link:
      "",
  },
  {
    name: "Art",
    imageLink: null,
    description: `It is often difficult to read through dense blocks of text without something to rest your eyes on. In the Art Department, we strive to embellish the newspaper with our unique illustrations, each perfectly suited for its article. In a school where artistic abilities are not seen as a priority, you can easily find your niche as an artist for The Spectator, where your art will be displayed for all of its readers to admire. The limits of your creativity have no bounds; you can explore and develop your art skills by taking on cool and unique requests, from a portrait of Taylor Swift (A&E) to an oily stork (Humor), as well as the occasional thematic spread; the range is infinite. We encourage the use of all media!`,
    quotes: [],
    link:
      "",
  },
  {
    name: "Science",
    imageLink: null,
    description: `The Science Department covers everything science-related, from pandemics and groundbreaking discoveries to Stuyvesant’s very own fascinating science clubs and events. Our department captures the complexity of science through analytical and often opinion-based writing, and we aim to convey the intricacies of the world around us and beyond to the student body.`,
    quotes: [],
    link:
      "",
  },
];
<commit_msg>recruitmentsupdatewithoutsports<commit_after>export const SECTIONS = [
  {
    name: "Opinions",
    imageLink: null,
    description: `The Opinions section lets writers share their views on issues relevant within the country, Stuyvesant, and their personal lives. Writers across all grades give their opinions on subjects ranging from politics and economics to technology and culture and share their personal stories. We try to consistently create engaging, substantive, and relevant content to provide students with a closer look at the world and the Stuyvesant community.`,
    quotes: [],
    link:
      "https://docs.google.com/document/d/1jjRU59bam89yxJ3RvIIRDsbsJrXIBZfDrb8pTkx0Em8/edit",
  },
  {
    name: "Arts and Entertainment",
    imageLink: null,
    description: `As the only department with more than three article categories, Arts and Entertainment is notable for its versatility and creative freedom. We review recent cultural topics such as art, fashion, film, food, literature, music, and more! A&E writers are also free to experiment with the thinkpiece category, where one can explore cultural phenomena or trends, gender standards, or race from a cultural standpoint. Whether you are an aspiring music enthusiast or cultural connoisseur, everyone can find (or create!) their own niche in A&E.`,
    quotes: [],
    link:
      "https://docs.google.com/document/d/1rsEWimDYrCwiG2uT2pGmtc9KHcBHz0-SQEIOeP9u9PY/edit",
  },
  {
    name: "Features",
    imageLink: null,
    description: `Features capture the spirit and mood of the school. At times, Features covers Stuyvesant’s evergreen topics, e.g. what is it about PE uniforms that gets everybody so hot and bothered? Other times, writers might profile a teacher. Other times, writers share creative narratives about experiences they have had in the “Voices” section. There is much to be found in Features, so come hither!`,
    quotes: [],
    link:
      "https://docs.google.com/document/d/1Nkg9uWtkPfYH2s3hzHCph3xFKkprhM2mGZZQD4dK2rw/edit?usp=sharing",
  },
  {
    name: "Humor",
    imageLink: null,
    description: `Do you want to lowkey rant about your life and roast everything?
        Would you like to have a personality glow up?
        Do you want to join a low commitment and fun Spec department?
        Join Spec Humor!
        We write funny and satirical pieces for The Spec and unlike other departments, everything we publish is libel and slander which means that you'll be in control of what everyone says!`,
    quotes: [],
    link: "https://docs.google.com/document/d/1_0KHjM2wTt2p6Ep-IUBd5bHVantbDkWptZOKV-4HW14/edit?usp=sharing",
  },
  {
    name: "Sports",
    imageLink: null,
    description: `The Sports department covers all things sports, whether it be PSAL sports at Stuyvesant or professional sports. Articles pertaining to Stuyvesant sports range from championship articles and season updates to athlete and coach interviews and opinions. Writers also cover virtually any topic in the larger sports world, from debates on LeBron vs. MJ and college basketball to YouTube boxing and the Champions League. If that’s not enough to convince you to apply, we have an exclusive fantasy football league (with rewards).`,
    quotes: [],
    link:
      "",
  },
  {
    name: "News",
    imageLink: null,
    description: `The News Department covers school events, administrative changes, student organizations, school and student government policies, and out-of-school news. Besides traditional News articles, we also write mini-articles, graphs & surveys, and spreads. If you are interested in knowing Stuyvesant news before it is released, informing the student body, meeting students, teachers, and administrators, or seeing your work on the front page, apply to our department!`,
    quotes: [],
    link:
      "https://docs.google.com/forms/d/e/1FAIpQLSdvdVm5oNtvAPmUFTXyI0Ny31pE_uAojJQ0mJzG9-DwKupMxQ/viewform?fbclid=IwAR07YWB7DilgGo8GBNMGqPo8Hc0CBhhMyaMj2wXyo5ZtyV3ur2ikX4qnsW4",
  },
  {
    name: "Copy",
    imageLink: null,
    description: `Members of the Copy Department ensures that The Spectator produces clear, grammatically correct, and accurate articles. Did you notice both mistakes in the last sentence? If you did, you should join the Copy Department! Members gain early access to articles and sharpen their grammar skills while editing and fact-checking departments of their choice. We are committed to catching grammatical and factual mistakes in articles with attention to detail, and we look for members who want to contribute to maintaining The Spectator’s high-quality content.`,
    quotes: [],
    link:
      "https://docs.google.com/forms/d/e/1FAIpQLSfYTigl3rGd1nCnqXPCsb6dHRscupjgnujOs_ryuHAUqfVCeg/viewform",
  },
  {
    name: "Web",
    imageLink: null,
    description: `The Web department is responsible for designing and updating The Spectator's website, www.stuyspec.com. It also attempts to facilitate the learning of developing and maintaining a web application based on Ruby on Rails and React. These real-world skills are a great resume builder and provide invaluable experience in working together as a team. This is the perfect department for both computer programming enthusiasts and novices who want to help The Spectator continue to storm the frontier of online journalism.`,
    quotes: [],
    link:
      "https://docs.google.com/forms/d/1eo1vc3nxKW3tQxYWtbhp-XBXQRlgpiLUADjeFneKUEw/viewform?edit_requested=true",
  },
  {
    name: "Layout",
    imageLink: null,
    description: `A good publication should interest readers before they even pick it up and read it; the Layout department does just that! We are responsible for formatting all of the articles, photos, art, and other media and make sure that the paper is aesthetically appealing and easy to read. After all, no one wants to read ugly and confusing blocks of text. With creativity and a certain magic called InDesign, we present to you what you know as The Spectator. As a staff member, you will be taught how to use InDesign and asked to critique the formatting of each issue as it is released.`,
    quotes: [],
    link:
      "https://docs.google.com/forms/d/e/1FAIpQLSeV9cHby2oNDRjQmr5KSvw8qTDsAFuiY52UMEC_Epk3ONO89A/viewform",
  },
  {
    name: "Photo",
    imageLink: null,
    description: `Photo is dedicated to providing interesting and dynamic photographs to support the articles in The Spectator and spearhead photo essay projects. Typical assignments include portraits of students and teachers, photos at school events, and shots of fun things around the city. This department is a good fit for anyone interested in photography, whether you’re just starting out, or you’re an expert looking for a place to practice and refine your skills.`,
    quotes: [],
    link:
      "https://docs.google.com/forms/d/e/1FAIpQLSfaYY5Ag1KCnjI8nxu_ASWSQ38L3PduwaYtcVlhFNUzGFnP7g/viewform?vc=0&c=0&w=1&flr=0&gxids=7628",
  },
  {
    name: "Business",
    imageLink: null,
    description: `The Business department is the backbone of The Spectator. Due to the efforts of this department, The Spectator is able to print all of its issues while still remaining independent. We negotiate and secure deals with businesses all over New York as well as handle the advertisements on the website and in the paper. Members learn skills that can be applied to real life, including talking in formal settings, learning how to negotiate and compromise, and understanding how to manage money.`,
    quotes: [],
    link:
      "https://docs.google.com/forms/d/e/1FAIpQLSfMFlIJ6Gru2fCbtzXREEGJWBSUJ8DkKqrP2vLA0LkqzMbxjA/viewform",
  },
  {
    name: "Art",
    imageLink: null,
    description: `It is often difficult to read through dense blocks of text without something to rest your eyes on. In the Art Department, we strive to embellish the newspaper with our unique illustrations, each perfectly suited for its article. In a school where artistic abilities are not seen as a priority, you can easily find your niche as an artist for The Spectator, where your art will be displayed for all of its readers to admire. The limits of your creativity have no bounds; you can explore and develop your art skills by taking on cool and unique requests, from a portrait of Taylor Swift (A&E) to an oily stork (Humor), as well as the occasional thematic spread; the range is infinite. We encourage the use of all media!`,
    quotes: [],
    link:
      "https://forms.gle/t89PpenGwbHkXPNy9",
  },
  {
    name: "Science",
    imageLink: null,
    description: `The Science Department covers everything science-related, from pandemics and groundbreaking discoveries to Stuyvesant’s very own fascinating science clubs and events. Our department captures the complexity of science through analytical and often opinion-based writing, and we aim to convey the intricacies of the world around us and beyond to the student body.`,
    quotes: [],
    link:
      "https://docs.google.com/document/d/1lSMaU7nDlcTvIG2h2g7bwTaXsVgS2ifhGdKOXhSc0Tc/edit",
  },
];
<|endoftext|>


<commit_before>
var expect from 'chai').expect;
var moment from 'moment');
let sinon from 'sinon/pkg/sinon.js');
var TimeframeUtils from '../../../lib/js/app/utils/TimeframeUtils');

describe('utils/TimeframeUtils', () => {

  describe('getTimeframe', () => {
    it('should call the right timeframe builder for absolute timeframes', () => {
      var time = {
        start: new Date(moment().subtract(1, 'days').startOf('day').format()),
        end: new Date(moment().startOf('day').format())
      };
      var stub = sinon.stub(TimeframeUtils.timeframeBuilders, 'absolute');

      TimeframeUtils.getTimeframe(time, 'US/Hawaii');
      assert.isTrue(stub.calledOnce);
      TimeframeUtils.timeframeBuilders.absolute.restore();
    });
    it('should call the right timeframe builder for relative timeframes', () => {
      var time = {
        relativity: 'this',
        amount: '1',
        sub_timeframe: 'days'
      };
      var stub = sinon.stub(TimeframeUtils.timeframeBuilders, 'relative');

      TimeframeUtils.getTimeframe(time);
      assert.isTrue(stub.calledOnce);
      TimeframeUtils.timeframeBuilders.relative.restore();
    });
  });

  describe('timeframeBuilders', () => {
    describe('absolute', () => {
      it('should properly build a timeframe object', () => {
        var time = {
          start: new Date(moment().subtract(1, 'days').startOf('day').format()),
          end: new Date(moment().startOf('day').format())
        };
        var timeframe = TimeframeUtils.getTimeframe(time, 'US/Hawaii');

        var expectedFormat = 'YYYY-MM-DDTHH:mm:ss.SSS';
        var expectedStart = moment(new Date(time.start)).format(expectedFormat);
        var expectedEnd = moment(new Date(time.end)).format(expectedFormat);

        assert.deepEqual(timeframe, {
          start: expectedStart,
          end: expectedEnd
        });
      });
    });
    describe('relative', () => {
      var time = {
        relativity: 'this',
        amount: '1',
        sub_timeframe: 'days'
      };

      var timeframe = TimeframeUtils.getTimeframe(time);
      assert.deepEqual(timeframe, 'this_1_days');
    });
  });

  describe('unpackTimeframeParam', () => {
    it('properly unpacks an absolute timeframe', () => {
      var timeframe = {
        start: moment(new Date("June 7, 2015 1:00 PM")).format('YYYY-MM-DDTHH:mm:ss.SSS')+"-10:00",
        end: moment(new Date("June 8, 2015 2:00 PM")).format('YYYY-MM-DDTHH:mm:ss.SSS')+"-10:00"
      };

      console.log(timeframe.start);

      assert.deepEqual(
        TimeframeUtils.unpackTimeframeParam(timeframe, 'US/Hawaii'),
        {
          timezone: 'US/Hawaii',
          time: {
            start: "2015-06-07T13:00:00.000",
            end: "2015-06-08T14:00:00.000"
          }
        }
      );
    });
    it('properly unpacks a relative timeframe', () => {
      var unpacked = TimeframeUtils.unpackTimeframeParam('this_8_days', 'Europe/London');

      assert.deepEqual(unpacked, {
        time: {
          relativity: 'this',
          amount: '8',
          sub_timeframe: 'days'
        },
        timezone: 'Europe/London'
      });
    });
  });

  describe('timeframeType', () => {
    it('recognizes relative timeframes', () => {
      var relative_timeframe = {
        relativity: 'this',
        amount: '8',
        sub_timeframe: 'days'
      }

      assert.equal(TimeframeUtils.timeframeType(relative_timeframe), 'relative');
    });

    it('recognizes absolute timeframes', () => {
      var absolute_timeframe = {
        start: moment().subtract(2, 'days').format(),
        end: moment().subtract(1, 'days').format()
      }

      assert.equal(TimeframeUtils.timeframeType(absolute_timeframe), 'absolute');
    });

    describe('invalid timeframes', () => {
      it('throws error for invalid time object', () => {
        expect(TimeframeUtils.timeframeType.bind(window, {bee: 'boop'})).to.throw("Invalid timeframe type: invalid time value.");
      });

      it('throws errors for non-object time objects', () => {
        expect(TimeframeUtils.timeframeType.bind(window, 'this_3_days')).to.throw("Invalid timeframe type: not a plain object.");
      })
    });
  });

  describe('getTimeParameters', () => {
    it('returns whatever getTimeframe responds with', () => {
      var expectedResult = {cool: 'dude'};

      var stub = sinon.stub(TimeframeUtils, 'getTimeframe').returns(expectedResult);
      sinon.stub(TimeframeUtils, 'timeframeType').returns('absolute');

      assert.deepPropertyVal(TimeframeUtils.getTimeParameters({}, 'US/Hawaii'), 'timeframe', expectedResult);

      TimeframeUtils.getTimeframe.restore();
      TimeframeUtils.timeframeType.restore();
    });

    it('returns the same timezone if the timeframeType is relative', () => {
      var stub = sinon.stub(TimeframeUtils, 'timeframeType').returns('relative');

      assert.deepPropertyVal(TimeframeUtils.getTimeParameters({
        relativity: 'this',
        amount: 1,
        sub_timeframe: 'days'
      }, 'US/Hawaii'), 'timezone', 'US/Hawaii');

      TimeframeUtils.timeframeType.restore();
    });

    it('returns null timeframe with default timezone if passed undefined values', () => {
      assert.deepEqual(TimeframeUtils.getTimeParameters(), {
        timeframe: null,
        timezone: 'UTC'
      });
    });
  });

});
<commit_msg>fix tests for timeframe utils<commit_after>import moment from 'moment';
import TimeframeUtils from '../../../lib/js/app/utils/TimeframeUtils';

describe('utils/TimeframeUtils', () => {

  describe('getTimeframe', () => {
    it('should call the right timeframe builder for absolute timeframes', () => {
      const time = {
        start: new Date(moment().subtract(1, 'days').startOf('day').format()),
        end: new Date(moment().startOf('day').format())
      };
      const stub = jest.spyOn(TimeframeUtils.timeframeBuilders, 'absolute');

      TimeframeUtils.getTimeframe(time, 'US/Hawaii');
      expect(stub).toHaveBeenCalledTimes(1);
      stub.mockRestore();
    });
    it('should call the right timeframe builder for relative timeframes', () => {
      const time = {
        relativity: 'this',
        amount: '1',
        sub_timeframe: 'days'
      };
      const stub = jest.spyOn(TimeframeUtils.timeframeBuilders, 'relative');

      TimeframeUtils.getTimeframe(time);
      expect(stub).toHaveBeenCalledTimes(1);
      stub.mockRestore();
    });
  });

  describe('timeframeBuilders', () => {
    describe('absolute', () => {
      it('should properly build a timeframe object', () => {
        const time = {
          start: new Date(moment().subtract(1, 'days').startOf('day').format()),
          end: new Date(moment().startOf('day').format())
        };
        const timeframe = TimeframeUtils.getTimeframe(time, 'US/Hawaii');

        const expectedFormat = 'YYYY-MM-DDTHH:mm:ss.SSS';
        const expectedStart = moment(new Date(time.start)).format(expectedFormat);
        const expectedEnd = moment(new Date(time.end)).format(expectedFormat);

        expect(timeframe).toEqual({
          start: expectedStart,
          end: expectedEnd
        });
      });
    });
    describe('relative', () => {
      const time = {
        relativity: 'this',
        amount: '1',
        sub_timeframe: 'days'
      };

      const timeframe = TimeframeUtils.getTimeframe(time);
      expect(timeframe).toEqual('this_1_days');
    });
  });

  describe('unpackTimeframeParam', () => {
    it('properly unpacks an absolute timeframe', () => {
      const timeframe = {
        start: moment(new Date("June 7, 2015 1:00 PM")).format('YYYY-MM-DDTHH:mm:ss.SSS')+"-10:00",
        end: moment(new Date("June 8, 2015 2:00 PM")).format('YYYY-MM-DDTHH:mm:ss.SSS')+"-10:00"
      };

      console.log(timeframe.start);

      expect(TimeframeUtils.unpackTimeframeParam(timeframe, 'US/Hawaii')).toEqual(
        {
          timezone: 'US/Hawaii',
          time: {
            start: "2015-06-07T13:00:00.000",
            end: "2015-06-08T14:00:00.000"
          }
        }
      );
    });
    it('properly unpacks a relative timeframe', () => {
      const unpacked = TimeframeUtils.unpackTimeframeParam('this_8_days', 'Europe/London');

      expect(unpacked).toEqual({
        time: {
          relativity: 'this',
          amount: '8',
          sub_timeframe: 'days'
        },
        timezone: 'Europe/London'
      });
    });
  });

  describe('timeframeType', () => {
    it('recognizes relative timeframes', () => {
      const relative_timeframe = {
        relativity: 'this',
        amount: '8',
        sub_timeframe: 'days'
      }

      expect(TimeframeUtils.timeframeType(relative_timeframe)).toEqual('relative');
    });

    it('recognizes absolute timeframes', () => {
      const absolute_timeframe = {
        start: moment().subtract(2, 'days').format(),
        end: moment().subtract(1, 'days').format()
      }

      expect(TimeframeUtils.timeframeType(absolute_timeframe)).toEqual('absolute');
    });

    describe('invalid timeframes', () => {
      it('throws error for invalid time object', () => {
        expect(TimeframeUtils.timeframeType.bind(window, {bee: 'boop'})).toThrow("Invalid timeframe type: invalid time value.");
      });

      it('throws errors for non-object time objects', () => {
        expect(TimeframeUtils.timeframeType.bind(window, 'this_3_days')).toThrow("Invalid timeframe type: not a plain object.");
      })
    });
  });

  describe('getTimeParameters', () => {
    it('returns whatever getTimeframe responds with', () => {
      const expectedResult = {cool: 'dude'};

      const stubGetTimeframe = jest.spyOn(TimeframeUtils, 'getTimeframe').mockReturnValue(expectedResult);
      const stubTimeframeType = jest.spyOn(TimeframeUtils, 'timeframeType').mockReturnValue('absolute');

      expect(TimeframeUtils.getTimeParameters({}, 'US/Hawaii').timeframe).toEqual(expectedResult);

      stubGetTimeframe.mockRestore();
      stubTimeframeType.mockRestore();
    });

    it('returns the same timezone if the timeframeType is relative', () => {
      const stub = jest.spyOn(TimeframeUtils, 'timeframeType').mockReturnValue('relative');

      expect(TimeframeUtils.getTimeParameters({
        relativity: 'this',
        amount: 1,
        sub_timeframe: 'days'
      }, 'US/Hawaii').timezone).toEqual('US/Hawaii');

      stub.mockRestore();
    });

    it('returns null timeframe with default timezone if passed undefined values', () => {
      expect(TimeframeUtils.getTimeParameters()).toEqual({
        timeframe: null,
        timezone: 'UTC'
      });
    });
  });

});
<|endoftext|>


<commit_before>var clutter = require('../build/Release/clutter');

if (clutter.init() != clutter.INIT_SUCCESS) {
	console.log("Failed to initialize clutter.");
	process.exit();
}

/* Create a new stage */
var stage = new clutter.Stage();
stage.setTitle('Text');
stage.resize(500, 500);
stage.setColor(0, 0, 0, 255);
stage.show();

/* Normal to load (default: sync) */
var texture1 = new clutter.Texture;
texture1.loadFile('fred.jpg');
texture1.setPosition(100, 100);
texture1.setOpacity(150);
stage.add(texture1);

console.log('texture1 getOpacity() = ' + texture1.getOpacity());

/* Asynchronize */
var texture2 = new clutter.Texture;
texture2.setLoadAsync(true);
texture2.loadFile('fred.jpg');
texture2.setPosition(200, 200);
texture2.setDepth(-200);
stage.add(texture2);

console.log('texture2 getLoadAsync() = ' + texture2.getLoadAsync());
console.log('texture2 getDepth() = ' + texture2.getDepth());

/* Keep aspect ratio */
var texture3 = new clutter.Texture;
texture3.keepAspectRatio(true);
texture3.loadFile('fred.jpg');
texture3.setWidth(50);
texture3.setPosition(50, 50);
stage.add(texture3);

console.log('texture3 keepAspectRatio() = ' + texture3.keepAspectRatio());
console.log('texture3 (x, y) = (' + texture3.getX() + ', ' + texture3.getY() + ')');
console.log('texture3 (w, h) = (' + texture3.getWidth() + ', ' + texture3.getHeight() + ')');

clutter.main();
<commit_msg>change window title<commit_after>var clutter = require('../build/Release/clutter');

if (clutter.init() != clutter.INIT_SUCCESS) {
	console.log("Failed to initialize clutter.");
	process.exit();
}

/* Create a new stage */
var stage = new clutter.Stage();
stage.setTitle('Texture');
stage.resize(500, 500);
stage.setColor(0, 0, 0, 255);
stage.show();

/* Normal to load (default: sync) */
var texture1 = new clutter.Texture;
texture1.loadFile('fred.jpg');
texture1.setPosition(100, 100);
texture1.setOpacity(150);
stage.add(texture1);

console.log('texture1 getOpacity() = ' + texture1.getOpacity());

/* Asynchronize */
var texture2 = new clutter.Texture;
texture2.setLoadAsync(true);
texture2.loadFile('fred.jpg');
texture2.setPosition(200, 200);
texture2.setDepth(-200);
stage.add(texture2);

console.log('texture2 getLoadAsync() = ' + texture2.getLoadAsync());
console.log('texture2 getDepth() = ' + texture2.getDepth());

/* Keep aspect ratio */
var texture3 = new clutter.Texture;
texture3.keepAspectRatio(true);
texture3.loadFile('fred.jpg');
texture3.setWidth(50);
texture3.setPosition(50, 50);
stage.add(texture3);

console.log('texture3 keepAspectRatio() = ' + texture3.keepAspectRatio());
console.log('texture3 (x, y) = (' + texture3.getX() + ', ' + texture3.getY() + ')');
console.log('texture3 (w, h) = (' + texture3.getWidth() + ', ' + texture3.getHeight() + ')');

clutter.main();
<|endoftext|>


<commit_before>import {WaitingButtonController} from '../src/js/waiting-button.directive';
const angular = require('angular'),
      angularMock = require('angular-mocks');

describe('mbmWaitingButton', () => {
  beforeEach(() => {
    angular.mock.module('mbm.waitingButton');
  });

  it('should toggle waiting class based on operation promise', () => {
    angular.mock.inject(($rootScope, $compile, $timeout, $q) => {
      // Build a Promise to represent an async operation
      const operationDeferred = $q.defer();

      // Fake receiver for the click action
      $rootScope.onClickReceiver = function() {
        return operationDeferred.promise;
      }

      // Define simple template for the directive, and then compile it
      const template = `
        <button mbm-waiting-button="onClickReceiver()">Waiting Button</button>
      `;
      const element = $compile(template)($rootScope);

      // Verify that the element doesn't have the waiting-button loading class
      //  by default.
      expect(element.hasClass(WaitingButtonController.WAITING_CLASS)).toEqual(false);

      // Simulate click to enter "waiting" state
      element[0].click();

      // Run the Angular digest loop once to pick up the changes.
      $rootScope.$digest();

      // Verify that the element now has the waiting-button loading class.
      expect(element.hasClass(WaitingButtonController.WAITING_CLASS)).toEqual(true);

      // Resolve the operation. Should return waiting button to normal state.
      operationDeferred.resolve();

      // Run the Angular digest loop once to pick up the changes.
      $rootScope.$digest();

      // Verify that the element doesn't have the waiting-button loading class
      //  after the operation promise has resolved.
      expect(element.hasClass(WaitingButtonController.WAITING_CLASS)).toEqual(false);
    });
  });

  it('should support customizable waiting classes', () => {
    angular.mock.inject(($rootScope, $compile, $timeout, $q) => {
      // Build a Promise to represent an async operation
      const operationDeferred = $q.defer();

      // Fake receiver for the click action
      $rootScope.onClickReceiver = function() {
        return operationDeferred.promise;
      }

      // Define the custom CSS class we'll be testing for
      const waitingClass = 'waiting';

      // Define simple template for the directive, and then compile it
      const template = `
        <button
          mbm-waiting-button="onClickReceiver()"
          waiting-button-waiting-class="${waitingClass}"
          >Waiting Button</button>
      `;
      const element = $compile(template)($rootScope);

      // Verify that the element doesn't have the waiting-button loading class
      //  by default.
      expect(element.hasClass(waitingClass)).toEqual(false);

      // Simulate click to enter "waiting" state
      element[0].click();

      // Run the Angular digest loop once to pick up the changes.
      $rootScope.$digest();

      // Verify that the element now has the waiting-button loading class.
      expect(element.hasClass(waitingClass)).toEqual(true);

      // Resolve the operation. Should return waiting button to normal state.
      operationDeferred.resolve();

      // Run the Angular digest loop once to pick up the changes.
      $rootScope.$digest();

      // Verify that the element doesn't have the waiting-button loading class
      //  after the operation promise has resolved.
      expect(element.hasClass(waitingClass)).toEqual(false);
    });
  });
});
<commit_msg>Adds test of operation invocation guard.<commit_after>import {WaitingButtonController} from '../src/js/waiting-button.directive';
const angular = require('angular'),
      angularMock = require('angular-mocks');

describe('mbmWaitingButton', () => {
  beforeEach(() => {
    angular.mock.module('mbm.waitingButton');
  });

  it('should toggle waiting class based on operation promise', () => {
    angular.mock.inject(($rootScope, $compile, $timeout, $q) => {
      // Build a Promise to represent an async operation
      const operationDeferred = $q.defer();

      // Fake receiver for the click action
      $rootScope.onClickReceiver = function() {
        return operationDeferred.promise;
      }

      // Define simple template for the directive, and then compile it
      const template = `
        <button mbm-waiting-button="onClickReceiver()">Waiting Button</button>
      `;
      const element = $compile(template)($rootScope);

      // Verify that the element doesn't have the waiting-button loading class
      //  by default.
      expect(element.hasClass(WaitingButtonController.WAITING_CLASS)).toEqual(false);

      // Simulate click to enter "waiting" state
      element[0].click();

      // Run the Angular digest loop once to pick up the changes.
      $rootScope.$digest();

      // Verify that the element now has the waiting-button loading class.
      expect(element.hasClass(WaitingButtonController.WAITING_CLASS)).toEqual(true);

      // Resolve the operation. Should return waiting button to normal state.
      operationDeferred.resolve();

      // Run the Angular digest loop once to pick up the changes.
      $rootScope.$digest();

      // Verify that the element doesn't have the waiting-button loading class
      //  after the operation promise has resolved.
      expect(element.hasClass(WaitingButtonController.WAITING_CLASS)).toEqual(false);
    });
  });

  it('should support customizable waiting classes', () => {
    angular.mock.inject(($rootScope, $compile, $timeout, $q) => {
      // Build a Promise to represent an async operation
      const operationDeferred = $q.defer();

      // Fake receiver for the click action
      $rootScope.onClickReceiver = function() {
        return operationDeferred.promise;
      }

      // Define the custom CSS class we'll be testing for
      const waitingClass = 'waiting';

      // Define simple template for the directive, and then compile it
      const template = `
        <button
          mbm-waiting-button="onClickReceiver()"
          waiting-button-waiting-class="${waitingClass}"
          >Waiting Button</button>
      `;
      const element = $compile(template)($rootScope);

      // Verify that the element doesn't have the waiting-button loading class
      //  by default.
      expect(element.hasClass(waitingClass)).toEqual(false);

      // Simulate click to enter "waiting" state
      element[0].click();

      // Run the Angular digest loop once to pick up the changes.
      $rootScope.$digest();

      // Verify that the element now has the waiting-button loading class.
      expect(element.hasClass(waitingClass)).toEqual(true);

      // Resolve the operation. Should return waiting button to normal state.
      operationDeferred.resolve();

      // Run the Angular digest loop once to pick up the changes.
      $rootScope.$digest();

      // Verify that the element doesn't have the waiting-button loading class
      //  after the operation promise has resolved.
      expect(element.hasClass(waitingClass)).toEqual(false);
    });
  });

  it('should support prevent additional action triggers while in wait state', () => {
    angular.mock.inject(($rootScope, $compile, $timeout, $q) => {
      // Build a Promise to represent an async operation
      const operationDeferred = $q.defer();

      // Fake receiver for the click action
      let receiverInvocationCounter = 0;
      $rootScope.onClickReceiver = function() {
        // TODO: This should updated to use a spy at some point.
        receiverInvocationCounter++;

        return operationDeferred.promise;
      }

      // Define simple template for the directive, and then compile it
      const template = `
        <button mbm-waiting-button="onClickReceiver()">Waiting Button</button>
      `;
      const element = $compile(template)($rootScope);

      // Simulate click to enter "waiting" state
      element[0].click();

      // Run the Angular digest loop once to pick up the changes.
      $rootScope.$digest();

      // Simulate another click
      element[0].click();

      // Run the Angular digest loop once to pick up the changes.
      $rootScope.$digest();

      // Verify that the operation receiver has only been invoked once.
      expect(receiverInvocationCounter).toEqual(1);
    });
  });
});
<|endoftext|>


<commit_before>const Ping = require('./ping');
const _ = require('underscore');
const Events = require('events');

/**
* The amount of active pings (i.e. connections) that can run concurrently.
*
* This should solve the problem of the connection being restored after
* being offline for a while (thus, there being a lot of open connections),
* and new pings not working as the browser already reached the limit
* of open connections.
*
* @type {integer}
*/
const PINGS_CONCURRENCY_LIMIT = 6;

module.exports = class Pings {
  constructor() {
    this.events = new Events();
    this.interval = 1000; /* ms */ // How often pings are created
    this._list = [];
    this._max = 100;
    this._ping(); // Start pinging ASAP
    setInterval(this._ping.bind(this), this.interval);
  }

  get all() {
    return this._list;
  }

  set max(max) {
    this._max = max;
  }

  get max() {
    return this._max;
  }

  get currentLag() {
    const lastRespondedPingLag = this._lastRespondedPing
      ? this._lastRespondedPing.lag
      : 0;
    const firstOfTheLastUnrespondedPingsLag = (() => {
      const ping = this._getFirstOfTheLastUnrespondedPings();
      return ping ? ping.lag : 0;
    })();
    return Math.max(lastRespondedPingLag, firstOfTheLastUnrespondedPingsLag);
  }

  _getFirstOfTheLastUnrespondedPings() {
    let first;
    for (let i = this._list.length - 1; i >= 0; i -= 1) {
      if (this._list[i].end) break;
      first = this._list[i];
    }
    return first;
  }

  _getRunningPings() {
    return this._list.filter(ping => !ping.done);
  }

  _ping() {
    this._removePingsOverLimit();
    this._abortOldestPingsOverConcurrencyLimit();
    this._addPing();
  }

  _removePingsOverLimit() {
    // Don't remove the first of the last unresponded ping, otherwise the lag
    // won't be bigger than the one for the first ping in the list!
    const firstOfTheLastUnrespondedPings = this._getFirstOfTheLastUnrespondedPings();
    while (this._list.length > this._max) {
      const ping = this._list.shift();
      if (ping !== firstOfTheLastUnrespondedPings) ping.abort();
    }
    if (firstOfTheLastUnrespondedPings)
      this._list.unshift(firstOfTheLastUnrespondedPings);
  }

  _abortOldestPingsOverConcurrencyLimit() {
    _.chain(this._getRunningPings())
      .initial(PINGS_CONCURRENCY_LIMIT)
      .invoke('abort');
  }

  _addPing() {
    const ping = new Ping();
    ping.events.on('pong', () => {
      if (this._isLastRespondedPing(ping)) this._lastRespondedPing = ping;
    });
    this._list.push(ping);
    this.events.emit('add', ping);
  }

  _isLastRespondedPing(ping) {
    return (
      !this._lastRespondedPing || this._lastRespondedPing.start < ping.start
    );
  }
};
<commit_msg>Ensure no more than one ping is created at the same time<commit_after>const Ping = require('./ping');
const _ = require('underscore');
const Events = require('events');

/**
* The amount of active pings (i.e. connections) that can run concurrently.
*
* This should solve the problem of the connection being restored after
* being offline for a while (thus, there being a lot of open connections),
* and new pings not working as the browser already reached the limit
* of open connections.
*
* @type {integer}
*/
const PINGS_CONCURRENCY_LIMIT = 6;

module.exports = class Pings {
  constructor() {
    this.events = new Events();
    this.interval = 1000; /* ms */ // How often pings are created
    this._list = [];
    this._max = 100;
    this._pingRepeatedly();
  }

  get all() {
    return this._list;
  }

  set max(max) {
    this._max = max;
  }

  get max() {
    return this._max;
  }

  get currentLag() {
    const lastRespondedPingLag = this._lastRespondedPing
      ? this._lastRespondedPing.lag
      : 0;
    const firstOfTheLastUnrespondedPingsLag = (() => {
      const ping = this._getFirstOfTheLastUnrespondedPings();
      return ping ? ping.lag : 0;
    })();
    return Math.max(lastRespondedPingLag, firstOfTheLastUnrespondedPingsLag);
  }

  _getFirstOfTheLastUnrespondedPings() {
    let first;
    for (let i = this._list.length - 1; i >= 0; i -= 1) {
      if (this._list[i].end) break;
      first = this._list[i];
    }
    return first;
  }

  _getRunningPings() {
    return this._list.filter(ping => !ping.done);
  }

  _ping() {
    this._removePingsOverLimit();
    this._abortOldestPingsOverConcurrencyLimit();
    this._addPing();
  }

  _pingRepeatedly() {
    this._ping();
    setTimeout(this._pingRepeatedly.bind(this), this.interval);
  }

  _removePingsOverLimit() {
    // Don't remove the first of the last unresponded ping, otherwise the lag
    // won't be bigger than the one for the first ping in the list!
    const firstOfTheLastUnrespondedPings = this._getFirstOfTheLastUnrespondedPings();
    while (this._list.length > this._max) {
      const ping = this._list.shift();
      if (ping !== firstOfTheLastUnrespondedPings) ping.abort();
    }
    if (firstOfTheLastUnrespondedPings)
      this._list.unshift(firstOfTheLastUnrespondedPings);
  }

  _abortOldestPingsOverConcurrencyLimit() {
    _.chain(this._getRunningPings())
      .initial(PINGS_CONCURRENCY_LIMIT)
      .invoke('abort');
  }

  _addPing() {
    const ping = new Ping();
    ping.events.on('pong', () => {
      if (this._isLastRespondedPing(ping)) this._lastRespondedPing = ping;
    });
    this._list.push(ping);
    this.events.emit('add', ping);
  }

  _isLastRespondedPing(ping) {
    return (
      !this._lastRespondedPing || this._lastRespondedPing.start < ping.start
    );
  }
};
<|endoftext|>


<commit_before><commit_msg>Fix whitespace, without duplicating the empty object<commit_after><|endoftext|>


<commit_before><commit_msg>if(typeof itemTemplate[i] == 'function') continue;<commit_after><|endoftext|>


<commit_before>jasmine.getFixtures().fixturesPath = '../js';
loadFixtures('melange_fixture.html');<commit_msg>Remove unneeded file.<commit_after><|endoftext|>


<commit_before>import XRegExp from './vendors/xregexp.js';

/**
 * A class for single char analysis.
 * @class CharAnalyzer
 */
export class CharAnalyzer {
    /**
     * Check if char is a white space.
     * @param {String} ch The char to analyze.
     * @return {Boolean}
     */
    static isWhiteSpace(ch) {
        return this.WHITE_SPACES_REGEX.test(ch);
    }
    /**
     * Check if char is a punctuation char.
     * @param {String} ch The char to analyze.
     * @return {Boolean}
     */
    static isPunctuation(ch) {
        return this.PUNCTUATION_REGEX.test(ch);
    }
    /**
     * Check if char is a sentence stop punctuation char.
     * @param {String} ch The char to analyze.
     * @return {Boolean}
     */
    static isStopPunctuation(ch) {
        return this.STOP_PUNCTUATION_REGEX.test(ch);
    }
    /**
     * Check if char is a diacritic char.
     * @param {String} ch The char to analyze.
     * @return {Boolean}
     */
    static isDiacritic(ch) {
        if (ch.length === 2) {
            return this.FULL_DIACRITICS_REGEX.test(ch);
        }
        return this.DIACRITICS_REGEX.test(ch);
    }
}

/**
 * A regexp for white spaces reconition.
 * @type RegExp
 */
CharAnalyzer.WHITE_SPACES_REGEX = new RegExp('[\\s|\\n|\\r]');
/**
 * A regexp for punctuation reconition.
 * @type RegExp
 */
CharAnalyzer.PUNCTUATION_REGEX = new XRegExp('[^\\p{L}|\\s|^\\d]');
/**
 * A regexp for stop punctuation reconition.
 * @type RegExp
 */
CharAnalyzer.STOP_PUNCTUATION_REGEX = new RegExp('[.|!|?|;|·]');
/**
 * A regexp for diacritics reconition.
 * @type RegExp
 */
CharAnalyzer.DIACRITICS_REGEX = new XRegExp('\\p{M}');
/**
 * A regexp for a char followed by diacritic reconition.
 * @type RegExp
 */
CharAnalyzer.FULL_DIACRITICS_REGEX = new XRegExp('.\\p{M}');
<commit_msg>feat: improve perf with a not alphabet preregex<commit_after>import XRegExp from './vendors/xregexp.js';

/**
 * A class for single char analysis.
 * @class CharAnalyzer
 */
export class CharAnalyzer {
    /**
     * Check if char is a white space.
     * @param {String} ch The char to analyze.
     * @return {Boolean}
     */
    static isWhiteSpace(ch) {
        return this.WHITE_SPACES_REGEX.test(ch);
    }
    /**
     * Check if char is a punctuation char.
     * @param {String} ch The char to analyze.
     * @return {Boolean}
     */
    static isPunctuation(ch) {
        return this.ALPHABET_REGEX.test(ch) &&
            this.PUNCTUATION_REGEX.test(ch);
    }
    /**
     * Check if char is a sentence stop punctuation char.
     * @param {String} ch The char to analyze.
     * @return {Boolean}
     */
    static isStopPunctuation(ch) {
        return this.STOP_PUNCTUATION_REGEX.test(ch);
    }
    /**
     * Check if char is a diacritic char.
     * @param {String} ch The char to analyze.
     * @return {Boolean}
     */
    static isDiacritic(ch) {
        if (ch.length === 2) {
            return this.FULL_DIACRITICS_REGEX.test(ch);
        }
        return this.ALPHABET_REGEX.test(ch) &&
            this.DIACRITICS_REGEX.test(ch);
    }
}

/**
 * A regexp for white spaces reconition.
 * @type RegExp
 */
CharAnalyzer.WHITE_SPACES_REGEX = new RegExp('[\\s|\\n|\\r]');
/**
 * A regexp for punctuation reconition.
 * @type RegExp
 */
CharAnalyzer.PUNCTUATION_REGEX = new XRegExp('[^\\p{L}|\\s|^\\d]');
/**
 * A regexp for stop punctuation reconition.
 * @type RegExp
 */
CharAnalyzer.STOP_PUNCTUATION_REGEX = new RegExp('[.|!|?|;|·]');
/**
 * A regexp for diacritics reconition.
 * @type RegExp
 */
CharAnalyzer.DIACRITICS_REGEX = new XRegExp('\\p{M}');
/**
 * A regexp for a char followed by diacritic reconition.
 * @type RegExp
 */
CharAnalyzer.FULL_DIACRITICS_REGEX = new XRegExp('.\\p{M}');
/**
 * A regexp for alphabet chars.
 * @type RegExp
 */
CharAnalyzer.ALPHABET_REGEX = /[^a-zA-Z]/;
<|endoftext|>


<commit_before>(function(d3, helpers, mixins, charts) {
  var utils = helpers.utils;
  var mixin = helpers.mixin;
  var property = helpers.property;
  var di = helpers.di;

  /**
    @class Labels
  */
  charts.Labels = charts.Chart.extend('Labels', mixin(
    mixins.Series,
    mixins.XY,
    mixins.Hover,
    mixins.Transition,
    mixins.StandardLayer,
    {
      initialize: function() {
        // Proxy attach to parent for hover
        var parent = this.options().parent;
        if (parent) {
          this.parent = parent;
          parent.on('attach', function() {
            this.container = parent.container;
            this.trigger('attach');
          }.bind(this));
        }

        // Use StandardLayer for extensibility
        this.standardSeriesLayer('Labels', this.base);
      },

      transform: function(data) {
        if (!helpers.isSeriesData(data))
          data = [{key: 'labels', name: 'Labels', values: data}];

        // TODO Use ticks / domain from xScale
        // ticks = scale.ticks ? scale.ticks.apply(scale, [10]) : scale.domain()
        return data;
      },

      /**
        Formatting function or string (string is passed to d3.format) for label values

        @property format
        @type String|Function
      */
      format: property('format', {
        type: 'Function',
        set: function(value) {
          if (utils.isString(value)) {
            return {
              override: d3.format(value)
            };
          }
        }
      }),

      /**
        Label position relative to data-point

        @property position
        @type String
        @default top
      */
      position: property('position', {
        default_value: 'top',
        validate: function(value) {
          return utils.contains(['top', 'right', 'bottom', 'left'], value);
        }
      }),

      /**
        Offset between data-point and label
        (if number is given, offset is set based on position)

        @property offset
        @type Number|Object
        @default {x: 0, y: 0}
      */
      offset: property('offset', {
        default_value: {x: 0, y: 0},
        set: function(offset) {
          if (utils.isNumber(offset)) {
            offset = {
              top: {x: 0, y: -offset},
              right: {x: offset, y: 0},
              bottom: {x: 0, y: offset},
              left: {x: -offset, y: 0}
            }[this.position()];

            if (!offset)
              offset = {x: 0, y: 0};

            return {
              override: offset
            };
          }
        }
      }),

      /**
        Padding between text and label background

        @property padding
        @type Number
        @default 0
      */
      padding: property('padding', {default_value: 1}),

      /**
        Define text anchor, start, middle, or end
        (set by default based on label position)

        @property anchor
        @type String
        @default middle
      */
      anchor: property('anchor', {
        default_value: function() {
          return {
            'top': 'middle',
            'right': 'start',
            'bottom': 'middle',
            'left': 'end'
          }[this.position()];
        },
        validate: function(value) {
          return utils.contains(['start', 'middle', 'end'], value);
        }
      }),

      /**
        Define text-aligmment, top, middle, or bottom
        (set by default based on label position)

        @property alignment
        @type String
        @default middle
      */
      alignment: property('alignment', {
        default_value: function() {
          return {
            'top': 'bottom',
            'right': 'middle',
            'bottom': 'top',
            'left': 'middle'
          }[this.position()];
        },
        validate: function(value) {
          return utils.contains(['top', 'middle', 'bottom'], value);
        }
      }),

      /**
        Get label text for data-point (uses "label" property or y-value)

        @method labelText
        @return {String}
      */
      labelText: di(function(chart, d, i) {
        var value = helpers.valueOrDefault(d.label, chart.yValue.call(this, d, i));
        var format = chart.format();

        return format ? format(value) : value;
      }),

      /**
        Get class for label group

        @method labelClass
        @return {String}
      */
      labelClass: di(function(chart, d, i) {
        return 'chart-label' + (d['class'] ? ' ' + d['class'] : '');
      }),

      onDataBind: function onDataBind(selection, data) {
        return selection.selectAll('g')
          .data(data, this.key);
      },
      onInsert: function onInsert(selection) {
        return selection.append('g')
          .on('mouseenter', this.mouseEnterPoint)
          .on('mouseleave', this.mouseLeavePoint)
          .call(this.insertLabels);
      },
      onMerge: function onMerge(selection) {
        selection.attr('class', chart.labelClass);

        this.mergeLabels(selection);
        this.layoutLabels(selection);
      },
      onMergeTransition: function onMergeTransition(selection) {
        // Transition labels into position
        this.setupTransition(selection);
        this.transitionLabels(selection);
      },

      // (Override for custom labels)
      insertLabels: function(selection) {
        selection.append('rect')
          .attr('class', 'chart-label-bg');
        selection.append('text')
          .attr('class', 'chart-label-text');
      },

      // (Override for custom labels)
      mergeLabels: function(selection) {
        selection.selectAll('text')
          .text(this.labelText);
      },

      // (Override for custom labels)
      layoutLabels: function(selection) {
        // Calculate layout
        var chart = this;
        var labels = [];
        var options = {
          offset: chart.offset(),
          padding: chart.padding(),
          anchor: chart.anchor(),
          alignment: chart.alignment()
        };
        selection.each(function(d, i, j) {
          if (!labels[j])
            labels[j] = [];

          // Store values for label and calculate layout
          var label = prepareLabel(chart, this, d, i , j);
          labels[j].push(label);

          calculateLayout(chart, options, label);
        });

        // Collision detection
        handleCollisions(chart, labels);

        // Layout labels
        utils.each(labels, function(series) {
          utils.each(series, function(label) {
            setLayout(chart, label);
          });
        });
      },

      // (Override for custom labels)
      transitionLabels: function(selection) {
        selection.attr('opacity', 1);
      }
    }
  ), {
    z_index: 150
  });

  /**
    @class HoverLabels
  */
  d3.chart('Labels').extend('HoverLabels', mixin(mixins.Hover, {
    initialize: function() {
      this.on('attach', function() {
        this.container.on('mouseenter:point', this.onMouseEnterPoint.bind(this));
        this.container.on('mouseleave:point', this.onMouseLeavePoint.bind(this));
      }.bind(this));
    },

    /**
      Maximum distance to find active points

      @property hoverTolerance
      @type Number
      @default 20
    */
    hoverTolerance: property('hoverTolerance', {
      set: function(value) {
        // Pass through hover tolerance to parent (if present)
        if (this.parent && this.parent.hoverTolerance)
          this.parent.hoverTolerance(value);
      },
      default_value: 20
    }),

    // Don't fade in labels, hidden until hover
    transitionLabels: function(selection) {},

    onMouseEnterPoint: function(point) {
      var label = this.findLabelForPoint(point);
      if (label)
        d3.select(label).attr('opacity', 1);
    },
    onMouseLeavePoint: function(point) {
      var label = this.findLabelForPoint(point);
      if (label)
        d3.select(label).attr('opacity', 0);
    },

    findLabelForPoint: function(point) {
      var labels = this.base.selectAll('g.chart-series').selectAll('g');
      var chart = this;
      var label;

      labels.each(function(d, i, j) {
        var series = chart.seriesData.call(this, d, i, j);
        if (d === point.d && series === point.series)
          label = this;
      });

      return label;
    }
  }));

  function prepareLabel(chart, element, d, i, j) {
    var selection = d3.select(element);
    var text = selection.select('text');
    var bg = selection.select('rect');

    return {
      x: chart.x.call(element, d, i),
      y: chart.y.call(element, d, i),
      element: element,
      selection: selection,
      text: {
        element: text.node(),
        selection: text
      },
      bg: {
        element: bg.node(),
        selection: bg
      }
    };
  }

  function calculateLayout(chart, options, label) {
    var text_bounds = label.text.element.getBBox();

    // Need to adjust text for line-height
    var text_y_adjustment = helpers.alignText(label.text.element);

    // Position background
    var layout = label.bg.layout = {
      x: options.offset.x,
      y: options.offset.y,
      width: text_bounds.width + 2*options.padding,
      height: text_bounds.height + 2*options.padding
    };

    // Set width / height of label
    label.width = layout.width;
    label.height = layout.height;

    if (options.anchor == 'end')
      layout.x -= layout.width;
    else if (options.anchor == 'middle')
      layout.x -= (layout.width / 2);

    if (options.alignment == 'bottom')
      layout.y -= layout.height;
    else if (options.alignment == 'middle')
      layout.y -= (layout.height / 2);

    // Center text in background
    label.text.layout = {
      x: layout.x + (layout.width / 2) - (text_bounds.width / 2),
      y: layout.y + (layout.height / 2) - (text_bounds.height / 2) + text_y_adjustment
    };
  }

  function handleCollisions(chart, labels) {
    utils.each(labels, function(series, seriesIndex) {
      // Check through remaining series for collisions
      utils.each(labels.slice(seriesIndex + 1), function(compareSeries) {
        utils.each(compareSeries, function(compareLabel) {
          utils.each(series, function(label) {
            if (checkForOverlap(label, compareLabel))
              groupLabels(label, compareLabel);
          });
        });
      });
    });

    function checkForOverlap(labelA, labelB) {
      var a = getEdges(labelA);
      var b = getEdges(labelB);

      var contained_LR = (b.left < a.left && b.right > a.right);
      var contained_TB = (b.bottom < a.bottom && b.top > a.top);
      var overlap_LR = (b.left >= a.left && b.left < a.right) || (b.right > a.left && b.right <= a.right) || contained_LR;
      var overlap_TB = (b.top >= a.top && b.top < a.bottom) || (b.bottom > a.top && b.bottom <= a.bottom) || contained_TB;

      return overlap_LR && overlap_TB;

      function getEdges(label) {
        return {
          left: label.x,
          right: label.x + label.width,
          top: label.y,
          bottom: label.y + label.height
        };
      }
    }

    function groupLabels(labelA, labelB) {
      if (labelA.group && labelB.group) {
        // Move labelB group labels into labelA group
        utils.each(labelB.group.labels, function(label) {
          labelA.group.labels.push(label);
          label.group = labelA.group;
        });

        updateGroupPositions(labelA.group);
      }
      else if (labelA.group) {
        addLabelToGroup(labelB, labelA.group);
      }
      else if (labelB.group) {
        addLabelToGroup(labelA, labelB.group);
      }
      else {
        var group = {labels: []};
        addLabelToGroup(labelA, group);
        addLabelToGroup(labelB, group);
      }
    }

    function addLabelToGroup(label, group) {
      group.labels.push(label);
      label.group = group;
      label.originalY = label.y;

      updateGroupPositions(group);
    }

    function updateGroupPositions(group) {
      var byY = utils.chain(group.labels)
        .each(function(label) {
          // Reset to original y
          label.y = label.originalY;
        })
        .sortBy(function(label) {
          return label.y;
        })
        .reverse()
        .value();

      utils.each(byY, function(label, index) {
        var prev = utils.first(byY, index);
        var overlap;

        for (var i = prev.length - 1; i >= 0; i--) {
          if (checkForOverlap(label, prev[i])) {
            overlap = prev[i];
            break;
          }
        }

        if (overlap)
          label.y = overlap.y - label.height;
      });
    }
  }

  function setLayout(chart, label) {
    label.bg.selection
      .attr('transform', helpers.translate(label.bg.layout.x, label.bg.layout.y))
      .attr('width', label.bg.layout.width)
      .attr('height', label.bg.layout.height);

    label.text.selection
      .attr('transform', helpers.translate(label.text.layout.x, label.text.layout.y));

    // Position label and set opacity to fade-in
    label.selection
      .attr('transform', helpers.translate(label.x, label.y))
      .attr('opacity', 0);
  }

})(d3, d3.compose.helpers, d3.compose.mixins, d3.compose.charts);
<commit_msg>Update docs (Labels)<commit_after>(function(d3, helpers, mixins, charts) {
  var utils = helpers.utils;
  var mixin = helpers.mixin;
  var property = helpers.property;
  var di = helpers.di;

  /**
    Standalone or "embeddable" labels (uses `mixins.Labels` and `attachLabels` to embed in chart)

    ### Extending

    To extend the `Labels` chart, the following methods are available:

    - `insertLabels`
    - `mergeLabels`
    - `layoutLabels`
    - `transitionLabels`
    - `onDataBind`
    - `onInsert`
    - `onEnter`
    - `onEnterTransition`
    - `onUpdate`
    - `onUpdateTransition`
    - `onMerge`
    - `onMergeTransition`
    - `onExit`
    - `onExitTransition`

    View the `Labels.js` source for the default implementation and more information on these methods.

    @example
    ```js
    var chart = d3.select('#chart').chart('Compose', function(data) {
      return {
        charts: {
          input: {
            type: 'Lines',
            data: data.input,
            // xScale, yScale, other properties...

            // Show labels with default properties
            labels: true
          },
          output: {
            type: 'Bars',
            data: data.output,
            // xScale, yScale, other properties...

            // Pass options to labels
            labels: {
              offset: 2,
              position: 'top',
              style: {
                'font-size': '14px'
              },
              format: d3.format(',0d')
            }
          },
          labels: {
            type: 'Labels',
            data: data.labels,

            // xScale, yScale, other properties...
          }
        }
      };
    });

    chart.draw({
      input: [1, 2, 3],
      output: [10, 20, 30],
      labels: [
        {x: 0, y: 0},
        {x: 0, y: 30, label: 'Override (y by default)'},
        {x: 2, y: 0},
        {x: 2, y: 30}
      ]
    });
    ```
    @class Labels
    @extends Chart, Series, XY, Hover, Transition, StandardLayer
  */
  charts.Labels = charts.Chart.extend('Labels', mixin(
    mixins.Series,
    mixins.XY,
    mixins.Hover,
    mixins.Transition,
    mixins.StandardLayer,
    {
      initialize: function() {
        // Proxy attach to parent for hover
        var parent = this.options().parent;
        if (parent) {
          this.parent = parent;
          parent.on('attach', function() {
            this.container = parent.container;
            this.trigger('attach');
          }.bind(this));
        }

        // Use StandardLayer for extensibility
        this.standardSeriesLayer('Labels', this.base);
      },

      transform: function(data) {
        if (!helpers.isSeriesData(data))
          data = [{key: 'labels', name: 'Labels', values: data}];

        // TODO Use ticks / domain from xScale
        // ticks = scale.ticks ? scale.ticks.apply(scale, [10]) : scale.domain()
        return data;
      },

      /**
        Formatting function or string (string is passed to `d3.format`) for label values

        @property format
        @type String|Function
      */
      format: property('format', {
        type: 'Function',
        set: function(value) {
          if (utils.isString(value)) {
            return {
              override: d3.format(value)
            };
          }
        }
      }),

      /**
        Label position relative to data point
        (top, right, bottom, or left)

        @property position
        @type String
        @default top
      */
      position: property('position', {
        default_value: 'top',
        validate: function(value) {
          return utils.contains(['top', 'right', 'bottom', 'left'], value);
        }
      }),

      /**
        Offset between data point and label
        (if `Number` is given, offset is set based on position)

        @property offset
        @type Number|Object
        @default {x: 0, y: 0}
      */
      offset: property('offset', {
        default_value: {x: 0, y: 0},
        set: function(offset) {
          if (utils.isNumber(offset)) {
            offset = {
              top: {x: 0, y: -offset},
              right: {x: offset, y: 0},
              bottom: {x: 0, y: offset},
              left: {x: -offset, y: 0}
            }[this.position()];

            if (!offset)
              offset = {x: 0, y: 0};

            return {
              override: offset
            };
          }
        }
      }),

      /**
        Padding between text and label background

        @property padding
        @type Number
        @default 1
      */
      padding: property('padding', {default_value: 1}),

      /**
        Define text anchor (start, middle, or end)

        (set by default based on label position)

        @property anchor
        @type String
        @default middle
      */
      anchor: property('anchor', {
        default_value: function() {
          return {
            'top': 'middle',
            'right': 'start',
            'bottom': 'middle',
            'left': 'end'
          }[this.position()];
        },
        validate: function(value) {
          return utils.contains(['start', 'middle', 'end'], value);
        }
      }),

      /**
        Define text-aligmment (top, middle, or bottom)

        (set by default based on label position)

        @property alignment
        @type String
        @default middle
      */
      alignment: property('alignment', {
        default_value: function() {
          return {
            'top': 'bottom',
            'right': 'middle',
            'bottom': 'top',
            'left': 'middle'
          }[this.position()];
        },
        validate: function(value) {
          return utils.contains(['top', 'middle', 'bottom'], value);
        }
      }),

      /**
        Get label text for data-point (uses "label" property or y-value)

        @method labelText
        @param {Any} d
        @param {Number} i
        @return {String}
      */
      labelText: di(function(chart, d, i) {
        var value = helpers.valueOrDefault(d.label, chart.yValue.call(this, d, i));
        var format = chart.format();

        return format ? format(value) : value;
      }),

      /**
        Get class for label group

        @method labelClass
        @param {Any} d
        @param {Number} i
        @return {String}
      */
      labelClass: di(function(chart, d, i) {
        return 'chart-label' + (d['class'] ? ' ' + d['class'] : '');
      }),

      onDataBind: function onDataBind(selection, data) {
        return selection.selectAll('g')
          .data(data, this.key);
      },
      onInsert: function onInsert(selection) {
        return selection.append('g')
          .on('mouseenter', this.mouseEnterPoint)
          .on('mouseleave', this.mouseLeavePoint)
          .call(this.insertLabels);
      },
      onMerge: function onMerge(selection) {
        selection.attr('class', chart.labelClass);

        this.mergeLabels(selection);
        this.layoutLabels(selection);
      },
      onMergeTransition: function onMergeTransition(selection) {
        // Transition labels into position
        this.setupTransition(selection);
        this.transitionLabels(selection);
      },

      // (Override for custom labels)
      insertLabels: function(selection) {
        selection.append('rect')
          .attr('class', 'chart-label-bg');
        selection.append('text')
          .attr('class', 'chart-label-text');
      },

      // (Override for custom labels)
      mergeLabels: function(selection) {
        selection.selectAll('text')
          .text(this.labelText);
      },

      // (Override for custom labels)
      layoutLabels: function(selection) {
        // Calculate layout
        var chart = this;
        var labels = [];
        var options = {
          offset: chart.offset(),
          padding: chart.padding(),
          anchor: chart.anchor(),
          alignment: chart.alignment()
        };
        selection.each(function(d, i, j) {
          if (!labels[j])
            labels[j] = [];

          // Store values for label and calculate layout
          var label = prepareLabel(chart, this, d, i , j);
          labels[j].push(label);

          calculateLayout(chart, options, label);
        });

        // Collision detection
        handleCollisions(chart, labels);

        // Layout labels
        utils.each(labels, function(series) {
          utils.each(series, function(label) {
            setLayout(chart, label);
          });
        });
      },

      // (Override for custom labels)
      transitionLabels: function(selection) {
        selection.attr('opacity', 1);
      }
    }
  ), {
    z_index: 150
  });

  /**
    (in-progress)

    @class HoverLabels
  */
  d3.chart('Labels').extend('HoverLabels', mixin(mixins.Hover, {
    initialize: function() {
      this.on('attach', function() {
        this.container.on('mouseenter:point', this.onMouseEnterPoint.bind(this));
        this.container.on('mouseleave:point', this.onMouseLeavePoint.bind(this));
      }.bind(this));
    },

    /**
      Maximum distance to find active points

      @property hoverTolerance
      @type Number
      @default 20
    */
    hoverTolerance: property('hoverTolerance', {
      set: function(value) {
        // Pass through hover tolerance to parent (if present)
        if (this.parent && this.parent.hoverTolerance)
          this.parent.hoverTolerance(value);
      },
      default_value: 20
    }),

    // Don't fade in labels, hidden until hover
    transitionLabels: function(selection) {},

    onMouseEnterPoint: function(point) {
      var label = this.findLabelForPoint(point);
      if (label)
        d3.select(label).attr('opacity', 1);
    },
    onMouseLeavePoint: function(point) {
      var label = this.findLabelForPoint(point);
      if (label)
        d3.select(label).attr('opacity', 0);
    },

    findLabelForPoint: function(point) {
      var labels = this.base.selectAll('g.chart-series').selectAll('g');
      var chart = this;
      var label;

      labels.each(function(d, i, j) {
        var series = chart.seriesData.call(this, d, i, j);
        if (d === point.d && series === point.series)
          label = this;
      });

      return label;
    }
  }));

  function prepareLabel(chart, element, d, i, j) {
    var selection = d3.select(element);
    var text = selection.select('text');
    var bg = selection.select('rect');

    return {
      x: chart.x.call(element, d, i),
      y: chart.y.call(element, d, i),
      element: element,
      selection: selection,
      text: {
        element: text.node(),
        selection: text
      },
      bg: {
        element: bg.node(),
        selection: bg
      }
    };
  }

  function calculateLayout(chart, options, label) {
    var text_bounds = label.text.element.getBBox();

    // Need to adjust text for line-height
    var text_y_adjustment = helpers.alignText(label.text.element);

    // Position background
    var layout = label.bg.layout = {
      x: options.offset.x,
      y: options.offset.y,
      width: text_bounds.width + 2*options.padding,
      height: text_bounds.height + 2*options.padding
    };

    // Set width / height of label
    label.width = layout.width;
    label.height = layout.height;

    if (options.anchor == 'end')
      layout.x -= layout.width;
    else if (options.anchor == 'middle')
      layout.x -= (layout.width / 2);

    if (options.alignment == 'bottom')
      layout.y -= layout.height;
    else if (options.alignment == 'middle')
      layout.y -= (layout.height / 2);

    // Center text in background
    label.text.layout = {
      x: layout.x + (layout.width / 2) - (text_bounds.width / 2),
      y: layout.y + (layout.height / 2) - (text_bounds.height / 2) + text_y_adjustment
    };
  }

  function handleCollisions(chart, labels) {
    utils.each(labels, function(series, seriesIndex) {
      // Check through remaining series for collisions
      utils.each(labels.slice(seriesIndex + 1), function(compareSeries) {
        utils.each(compareSeries, function(compareLabel) {
          utils.each(series, function(label) {
            if (checkForOverlap(label, compareLabel))
              groupLabels(label, compareLabel);
          });
        });
      });
    });

    function checkForOverlap(labelA, labelB) {
      var a = getEdges(labelA);
      var b = getEdges(labelB);

      var contained_LR = (b.left < a.left && b.right > a.right);
      var contained_TB = (b.bottom < a.bottom && b.top > a.top);
      var overlap_LR = (b.left >= a.left && b.left < a.right) || (b.right > a.left && b.right <= a.right) || contained_LR;
      var overlap_TB = (b.top >= a.top && b.top < a.bottom) || (b.bottom > a.top && b.bottom <= a.bottom) || contained_TB;

      return overlap_LR && overlap_TB;

      function getEdges(label) {
        return {
          left: label.x,
          right: label.x + label.width,
          top: label.y,
          bottom: label.y + label.height
        };
      }
    }

    function groupLabels(labelA, labelB) {
      if (labelA.group && labelB.group) {
        // Move labelB group labels into labelA group
        utils.each(labelB.group.labels, function(label) {
          labelA.group.labels.push(label);
          label.group = labelA.group;
        });

        updateGroupPositions(labelA.group);
      }
      else if (labelA.group) {
        addLabelToGroup(labelB, labelA.group);
      }
      else if (labelB.group) {
        addLabelToGroup(labelA, labelB.group);
      }
      else {
        var group = {labels: []};
        addLabelToGroup(labelA, group);
        addLabelToGroup(labelB, group);
      }
    }

    function addLabelToGroup(label, group) {
      group.labels.push(label);
      label.group = group;
      label.originalY = label.y;

      updateGroupPositions(group);
    }

    function updateGroupPositions(group) {
      var byY = utils.chain(group.labels)
        .each(function(label) {
          // Reset to original y
          label.y = label.originalY;
        })
        .sortBy(function(label) {
          return label.y;
        })
        .reverse()
        .value();

      utils.each(byY, function(label, index) {
        var prev = utils.first(byY, index);
        var overlap;

        for (var i = prev.length - 1; i >= 0; i--) {
          if (checkForOverlap(label, prev[i])) {
            overlap = prev[i];
            break;
          }
        }

        if (overlap)
          label.y = overlap.y - label.height;
      });
    }
  }

  function setLayout(chart, label) {
    label.bg.selection
      .attr('transform', helpers.translate(label.bg.layout.x, label.bg.layout.y))
      .attr('width', label.bg.layout.width)
      .attr('height', label.bg.layout.height);

    label.text.selection
      .attr('transform', helpers.translate(label.text.layout.x, label.text.layout.y));

    // Position label and set opacity to fade-in
    label.selection
      .attr('transform', helpers.translate(label.x, label.y))
      .attr('opacity', 0);
  }

})(d3, d3.compose.helpers, d3.compose.mixins, d3.compose.charts);
<|endoftext|>


<commit_before>
"use strict";

class Model {

    constructor() {
        this.$data = {};
        this.$fields = [];
        this.$deleted = false;
    }

    $load(data = {}, recursive = true) {
        this.$initial = data;
        for (let key in data) {
            var props = {enumerable: true, configurable: true};
            if (!this.hasOwnProperty(key)) {
                props.get = () => {
                    return this.$data[key];
                };
                props.set = value => {
                    this.$data[key] = value;
                };
            }
            Object.defineProperty(this, key, props);
            this.$data[key] = data[key];
            this.$fields.push(key);
        }
        return this;
    }

    $update() {
        for (let field in this) {
            if (field.substring(0, 1) == '$') {
                continue;
            }
            if (typeof this[field] == 'object' && this[field] != null) {
                if ('$delete' in this[field] && this[field].$deleted) {
                    this[field].$delete();
                } else if ('$update' in this[field] && this[field].$dirty) {
                    this[field].$update();
                } else if ('map' in this[field]) {
                    this[field].map(item => {
                        if (typeof item == 'object') {
                            if ('$delete' in item && item.$deleted) {
                                item.$delete();
                            } else if ('$update' in item && item.$dirty) {
                                item.$update();
                            }
                        }
                    });
                }
            }
            if (this.$initial[field]) {
                this.$initial[field] = this.$data[field];
            }
        }
    }

    get $new() {
        return !('id' in this.$data && this.$data.id);
    }

    get $dirty() {
        if (this.$deleted) {
            return true;
        }
        for (let key in this) {
            if (key.substring(0, 1) == '$') {
                continue;
            }
            if (key in this.$data && this.$data[key] != this.$initial[key]) {
                return true;
            } else if (typeof this[key] == 'object' && this[key] != null) {
                if ('map' in this[key]) {
                    let dirty = false;
                    this[key].map(elem => {
                        if (typeof elem == 'object' && elem.$dirty || elem.$deleted) {
                            dirty = true;
                        }
                    });
                    if (dirty) {
                        return true;
                    }
                } else if (this[key].$dirty || this[key].$deleted) {
                    return true;
                }
            }
        }
        return false;
    }

    $pluralize(field) {
        field = field.replace(/y$/, 'ie');
        return this[field + 's'];
    }

    $export() {
        let data = {};
        for (let prop in this) {
            if (prop.substring(0, 1) == '$') {
                continue;
            }
            data[prop] = this[prop];
        }
        return data;
    }

};

export {Model};

<commit_msg>add $create, and handle $new better (some things might not have an id, or might have one erroneously)<commit_after>
"use strict";

class Model {

    constructor() {
        this.$data = {};
        this.$fields = [];
        this.$deleted = false;
        this.$new = true;
    }

    $create(data = {}, recursive = true) {
        let result = (new this.constructor()).$load(data, recursive);
        result.$new = true;
        return result;
    }

    $load(data = {}, recursive = true) {
        this.$initial = data;
        this.$new = false;
        for (let key in data) {
            var props = {enumerable: true, configurable: true};
            if (!this.hasOwnProperty(key)) {
                props.get = () => {
                    return this.$data[key];
                };
                props.set = value => {
                    this.$data[key] = value;
                };
            }
            Object.defineProperty(this, key, props);
            this.$data[key] = data[key];
            this.$fields.push(key);
        }
        return this;
    }

    $update() {
        for (let field in this) {
            if (field.substring(0, 1) == '$') {
                continue;
            }
            if (typeof this[field] == 'object' && this[field] != null) {
                if ('$delete' in this[field] && this[field].$deleted) {
                    this[field].$delete();
                } else if ('$update' in this[field] && this[field].$dirty) {
                    this[field].$update();
                } else if ('map' in this[field]) {
                    this[field].map(item => {
                        if (typeof item == 'object') {
                            if ('$delete' in item && item.$deleted) {
                                item.$delete();
                            } else if ('$update' in item && item.$dirty) {
                                item.$update();
                            }
                        }
                    });
                }
            }
            if (this.$initial[field]) {
                this.$initial[field] = this.$data[field];
            }
        }
    }

    get $dirty() {
        if (this.$deleted) {
            return true;
        }
        for (let key in this) {
            if (key.substring(0, 1) == '$') {
                continue;
            }
            if (key in this.$data && this.$data[key] != this.$initial[key]) {
                return true;
            } else if (typeof this[key] == 'object' && this[key] != null) {
                if ('map' in this[key]) {
                    let dirty = false;
                    this[key].map(elem => {
                        if (typeof elem == 'object' && elem.$dirty || elem.$deleted) {
                            dirty = true;
                        }
                    });
                    if (dirty) {
                        return true;
                    }
                } else if (this[key].$dirty || this[key].$deleted) {
                    return true;
                }
            }
        }
        return false;
    }

    $pluralize(field) {
        field = field.replace(/y$/, 'ie');
        return this[field + 's'];
    }

    $export() {
        let data = {};
        for (let prop in this) {
            if (prop.substring(0, 1) == '$') {
                continue;
            }
            data[prop] = this[prop];
        }
        return data;
    }

};

export {Model};

<|endoftext|>


<commit_before>/***********************/
/**     PAGER CLASS   **/
/***********************/

/**
 * Pager Class Constructor
 * This class behaves like a service, offering paging management.
 * @param {Object} options - An object of options to configure the service
 * @param {ObservableArray} data - The array of data to be paged
 */
Grid.Pager = function (options, gridVM) {
    window.doRegisterPaging();

    // Options
    this.enabled = ko.observable(true);
    this.pageSizes = propertyAsObservable(gridVM.ui, 'pageSizes');
    this.maxPageIndex = propertyAsObservable(gridVM.paging, 'pageCount');
    this.firstItem = propertyAsObservable(gridVM.paging, 'firstItem');
    this.lastItem = propertyAsObservable(gridVM.paging, 'lastItem');
    this.totalItems = propertyAsObservable(gridVM.paging, 'totalItems');
    this.currentPageIndex = ko.pureComputed({ read: function () {
        return gridVM.paging().currentPage;
    },
        write: function (newValue) {
            gridVM.process({ paging: { currentPage: newValue } });
        } });
    this.pageSize = ko.pureComputed({ read: function () {
        return gridVM.paging().pageSize;
    },
        write: function (newValue) {
            gridVM.process({ paging: { pageSize: newValue } });
        } });

    // UI Variables
    this.currentPageIndexUI = ko.observable; // Notify always so currentPageIndexUI re-evaluates after invalid values are entered

    /*****************************/
    /**     PRIVATE FUNCTIONS   **/
    /*****************************/

    /**
     * Goes to the page requested
     * Protects against invalid values
     */
    this.goToPage = function (pageIndex) {
        gridVM.process({ paging: { currentPage: pageIndex } });
    };

    /**
     * Goes to the first page
     */
    this.goToFirstPage = function () {
        this.goToPage(0);
    };

    /**
     * Goes to the last page
     */
    this.goToLastPage = function () {
        this.goToPage(gridVM.paging().pageCount);
    };

    /**
     * Goes to the prev page (if possible)
     */
    this.goToPrevPage = function () {
        this.goToPage(gridVM.paging().currentPage - 1);
    };

    /**
     * Goes to the next page (if possible)
     */
    this.goToNextPage = function () {
        this.goToPage(gridVM.paging().currentPage + 1);
    };

    this.refresh = function () {
        gridVM.process({ time: { refresh: Date.now() } });
    };
};

function propertyAsObservable(obs, prop) {
    return ko.pureComputed(function () {
        var unwrapped = ko.unwrap(obs);
        return unwrapped ? unwrapped[prop] : undefined;
    });
}
<commit_msg>Pager sometimes reverts to wrong value<commit_after>/***********************/
/**     PAGER CLASS   **/
/***********************/

/**
 * Pager Class Constructor
 * This class behaves like a service, offering paging management.
 * @param {Object} options - An object of options to configure the service
 * @param {ObservableArray} data - The array of data to be paged
 */
Grid.Pager = function (options, gridVM) {
    window.doRegisterPaging();

    var self = this;
    // Options
    this.enabled = ko.observable(true);
    this.pageSizes = propertyAsObservable(gridVM.ui, 'pageSizes');
    this.maxPageIndex = propertyAsObservable(gridVM.paging, 'pageCount');
    this.firstItem = propertyAsObservable(gridVM.paging, 'firstItem');
    this.lastItem = propertyAsObservable(gridVM.paging, 'lastItem');
    this.totalItems = propertyAsObservable(gridVM.paging, 'totalItems');
    this.currentPageIndex = ko.pureComputed({ read: function () {
        return gridVM.paging().currentPage;
    },
        write: function (newValue) {
            gridVM.process({ paging: { currentPage: newValue } });
        } });
    this.pageSize = ko.pureComputed({ read: function () {
        return gridVM.paging().pageSize;
    },
        write: function (newValue) {
            //Sometimes the value gets set while the control is in a weird state
            //this doesn't fix that issue, but it prevents the incorrect values from
            //propogating back into the grid.
            //Todo: investigate ko select not initializing correctly
            if (!newValue) {
                self.pageSize.racing = true;
                setTimeout(function() {
                    self.pageSize.racing = false
                },50);
            }
            if (self.pageSize.racing){
                return;
            }
            gridVM.process({ paging: { pageSize: newValue } });
        } });

    // UI Variables
    this.currentPageIndexUI = ko.observable; // Notify always so currentPageIndexUI re-evaluates after invalid values are entered

    /*****************************/
    /**     PRIVATE FUNCTIONS   **/
    /*****************************/

    /**
     * Goes to the page requested
     * Protects against invalid values
     */
    this.goToPage = function (pageIndex) {
        gridVM.process({ paging: { currentPage: pageIndex } });
    };

    /**
     * Goes to the first page
     */
    this.goToFirstPage = function () {
        this.goToPage(0);
    };

    /**
     * Goes to the last page
     */
    this.goToLastPage = function () {
        this.goToPage(gridVM.paging().pageCount);
    };

    /**
     * Goes to the prev page (if possible)
     */
    this.goToPrevPage = function () {
        this.goToPage(gridVM.paging().currentPage - 1);
    };

    /**
     * Goes to the next page (if possible)
     */
    this.goToNextPage = function () {
        this.goToPage(gridVM.paging().currentPage + 1);
    };

    this.refresh = function () {
        gridVM.process({ time: { refresh: Date.now() } });
    };
};

function propertyAsObservable(obs, prop) {
    return ko.pureComputed(function () {
        var unwrapped = ko.unwrap(obs);
        return unwrapped ? unwrapped[prop] : undefined;
    });
}
<|endoftext|>